Chapter 2 - 使用 Spring Boot 构建微服务
Created by : Mr Dk.
2020 / 08 / 12 11:25
@Nanjing, Jiangsu, China
传统的单体架构具有的特点:
- 紧耦合 - 业务逻辑的调用发生在 编程语言 层面,而不是通过实现中立的协议
- 有漏洞 - 数据几种存储,组件内部的数据结构实现细节泄漏到整个应用程序中
- 单体的 - 组件存放在单一代码库中,任何对代码的修改都需要重新编译、运行、测试、部署
而采用微服务架构后具有的特点:
- 有约束 - 微服务具有范围有限的单一职责,每个服务只做好一件事
- 松耦合 - 服务之间使用非专属调用协议 (比如 HTTP 和 REST) 进行交互,只要接口不变,可以对服务进行任意修改
- 抽象的 - 微服务完全拥有自己的数据结构和数据源,并锁定微服务数据库的访问限制,只允许微服务访问它
- 独立的 - 每个微服务可以独立于其它服务进行编译、部署 - 这样对变化进行隔离和测试更容易
微服务的出现满足了以下需求:
- 微服务能够快速交付,客户不必等待漫长的程序发布周期
- 基于微服务的应用可以将故障隔离在应用程序的特定功能中
- 不均匀的容量需求,微服务应用更容易在云服务器上水平伸缩
下面从三个视角来看微服务的设计。
2.1 架构师的故事:设计微服务架构
2.1.1 分解业务问题
将处理的问题分解为可管理的块,这样就不必把所有细节都考虑进来。
2.1.2 建立服务粒度
将代码打包到单独的项目中,梳理出服务访问的数据库表,只允许单独的服务访问特定域中的表。糟糕的微服务一般有以下特点。如果粒度过粗:
- 服务承担了过多的职责 (业务逻辑很复杂)
- 服务跨大量表管理数据
- 服务测试用例太多
如果粒度过细:
- 微服务数量上升快,每个服务只与一个数据库表交互
- 微服务之间严重相互依赖
- 微服务除了 CRUD 相关逻辑以外什么都不做
2.1.3 互相交流:定义服务接口
应用程序应当使用下面的设计方式交流:
- 拥抱 REST 理念,使用 HTTP 动词表示行为
- 使用 URI 来传达意图
- 请求和相应使用 JSON (轻量级数据序列化)
- 使用 HTTP 状态码传达结果
2.2 何时不应该使用微服务
- 微服务需要高度的运维成熟度 (因为是分布式的)
- 服务器散乱,管理和监控服务器的操作复杂性巨大
- 微服务对高度弹性、伸缩性的应用程序非常有用
- 微服务间执行事务没有标准
2.3 开发人员的故事:用 Spring Boot 和 Java 构建微服务
2.3.2 引导 Spring Boot 应用程序:编写引导类
微服务中极其重要的两个类:
- Spring 引导类,可被 Spring Boot 用于启动和初始化应用程序
- Spring 控制器类,用于公开可被其它服务调用的 HTTP end point
使用 @SpringBootApplication
注解表示这是一个引导类,然后在改类的 main()
中调用 run()
启动微服务。服务的核心初始化逻辑应该在这个类中。
2.3.3 构建微服务的入口:Spring Boot 控制器
将数据从传入的 HTTP 请求映射到处理该请求的 Java 函数。
在基于 HTTP 的微服务间发送数据时,有多种可选协议。与其它协议相比,JSON 非常轻量级,可以在没有太多文本开销的情况下传输数据。但是如果要使用比 JSON 更有效率的通信协议,最小化在链路上发送数据的大小,那么就要使用一些二进制协议。
端点命名问题:
- 使用明确的 URL 确立服务所代表的资源
- 使用 URL 来确立资源之间的关系 (比如父子关系)
- 尽早建立 URL 的版本控制方案 (使用版本号作为前缀添加到所有端点上)
2.4 DevOps 工程师的故事:构建运行时的严谨性
在这里,微服务的设计关乎在投入生产后如何管理服务。微服务应当基于如下原则进行构建:
- 可独立部署 - 多个服务实例可以使用单个软件制品进行启动和拆卸
- 可配置 - 实例启动时,应当从中央位置读取自身的配置数据,无需人为干预
- 微服务实例对用户透明 - 客户端不应知道服务的确切位置,而是与 服务代理 通信
- 微服务应当传达它的健康信息 - 客户端需要绕开不良的服务实例
上述原则被映射到了微服务运行的生命周期中:
- 服务装配 - 打包、部署服务,保证服务的可重复性、一致性
- 服务引导 - 配置与代码分开,在任意环境中快速启动、部署实例,无需人为干预
- 服务注册 / 发现 - 如何让新启动的服务实例被其它客户端发现
- 服务监控 - 保证服务高可用,监控微服务实例,状态不佳的服务实例被拆卸
一些经验总结:
- 代码库 - 每个微服务都应该有独立的版本控制
- 依赖 - 通过构建工具 (如 Maven) 明确地生命应用的依赖项 (特定版本号)
- 配置 - 应用程序配置与代码 分开存储
- 后端服务 - 确保随时可以将数据库从内部管理切换为第三方管理
- 构建、发布和运行 - 三个流程完全分开,保证构建后开发人员无法对运行时代码进行更改
- 进程 - 微服务应该始终是无状态的,可以随时被杀死和替换
- 端口绑定 - 服务应该在命令行上自行启动,通过公开的端口访问
- 并发 - 启动更多的微服务实例水平伸缩
- 可任意处置 - 微服务可根据需要启动和停止
- 开发环境与生产环境等同 - 最小化服务运行的所有环境之间存在的差距
- 日志 - 流式传输,写入中央位置
- 管理进程
2.4.1 服务装配:打包和部署微服务
微服务需要作为 带有所有依赖项的单个制品 进行打包和安装,然后这个制品可以部署到任何服务器上。几乎所有微服务框架都包含可以打包和部署的运行时引擎。比如可以将工程通过 Maven 打包为 JAR,并具有嵌入式的 Tomcat 引擎内置其中。这样这个 JAR 可以在服务器上通过命令行直接启动。
将运行时引擎 (如 Tomcat) 内置在可部署制品中的做法,消除了配置漂移的可能性。
2.4.2 服务引导:管理微服务的配置
服务引导发生在微服务 首次启动并需要加载应用程序配置信息 的时候。由于微服务的部署范围广,将配置数据存储在服务器外部能够解决管理复杂的问题。
- 配置数据的结构简单,且读取频繁不常写入,可以直接用文件系统来管理
- 配置数据需要具有低延迟可读性
- 数据存储必须高可用
2.4.3 服务注册和发现:客户端如何与微服务通信
从微服务消费者的角度看,微服务的位置应该是 透明 的。微服务架构可以通过运行多个服务实例来实现高度的 可伸缩性 和 可用性,因此每个实例都会被分配一个 唯一、非永久 的 IP 地址。缺点在于,随着服务实例的启动和拆卸,手动管理 IP 地址很麻烦。
微服务实例需要向第三方代理进行注册,这个过程被称为服务发现。实例需要告诉代理两方面数据:
- 实例的 IP 地址或域名
- 应用程序查找服务的逻辑名称
某些服务发现代理还要求能访问到服务实例暴露的 URL,以便执行健康检查。
2.4.4 传达微服务的健康状况
服务代理负责监视其注册的每个服务实例的健康状况,并从路由表中移除有问题的服务实例,确保客户端不会访问已经发生故障的服务实例。
发现微服务后,服务代理将会持续监视、ping 健康检查接口,以确保服务可用。如果发现实例存在问题,可以采取一些纠正措施,比如关闭实例或启动另外的实例。
Spring Actuator 提供了开箱即用的运维 end point。在访问实例的 /health
end point 后,可以查看实例是否正在运行,还可以获取一些服务器状态信息,如磁盘占用空间等。这样可以获得更丰富的监控体验。