软件系统设计-架构(7) 微服务架构
1. 微服务架构基础知识
考其中某一个或某几个特性
1.1 微服务架构
概括性描述
微服务架构是把应用程序功能性分解为一组服务的架构风格,每一个服务都是由一组专注、内聚的功能职责组成。
定义
微服务架构是一种将单体应用拆分为细粒度的服务,并使其运行在独立进程中,服务之间采用轻量级通信机制(如HTTP RESTful API)进行交互的架构风格。这些服务围绕系统的业务能力构建,且可以通过全自动的部署机制进行独立部署。服务可以进行分布式管理,从而支持不同的编程语言进行开发和不同的数据存储技术进行存储
1.2 主要特性
1.2.1 通过服务组件化
- 组件:可以独立替换和升级的软件单元
- 进程内组件
- 类、对象或库的形式
- 一般直接调用、以内存方式进行功能调用(共享内存)
- 进程外组件
- 微服务架构中的独立服务
- 实现组件化的方式是分解成服务
- 通过Web服务请求或RPC机制通信
- 轻量级消息传递机制(如RabbitMQ)
- 产生明确的组件发布接口,封装(区别于文档)
- 耦合度低,隔离性好、独立开发、部署
- 远程调用性能损耗、合适的API粒度
1.2.2 围绕业务能力组织
- 传统软件系统开发管理通常聚焦在技术层面
- UI团队、服务逻辑团队、数据库团队…
- 跨团队的沟通、交接和预算审批等
- 采用围绕业务能力的划分方法
- 服务业务领域内的宽栈实现
- 团队跨职能、全方位开发技能
- 如用户体验、数据库、项目管理…
- 采用产品开发模式
- 传统:项目模式, 开发-维护,开发完解散
- 开发团队负责软件的整个产品周期
- 持续关注软件如何帮助用户提升业务能力,实现价值交付
1.2.3 内聚和解耦
- 内聚:单一职责,有各自的领域逻辑
- 解耦:微服务间尽量减少直接依赖,独立自治
- 服务边界的确定(划分)有助于澄清和强化分离
- 业务功能分解:每个微服务负责独立的业务能力
- 领域驱动设计:通用领域划分为多个子域,识别限界上下文(Bounded Context)- 服务边界
1.2.4 去中心化
- 去中心化治理
- 构建微服务时可以有服务自己的技术栈选择
- 服务之间只需约定接口,无需关注内部实现
- 运维只需了解服务部署规范
- 去中心化数据存储
- 让每个微服务管理自己的数据库
- 或同一数据库技术的不同实例
- 或完全不同的数据库系统
- 去中心化数据管理
- 传统架构采用事务保证数据一致性,分布式微服务架构中数据管理困难
- 强调服务间的无事务协作,最终一致性和补偿策略
- 需权衡更大一致性的业务损失与修复错误的代价
1.2.5 基础设施自动化
- 依赖自动化的基础设施,降低开发和运维微服务的操作复杂度
- 持续部署和交付:编码、管理代码库、集成(构建、测试、打包)、部署、监控和运维
- 测试:单元测试、集成测试、组件化测试、契约测试和端到端测试等
1.2.6 服务设计与演进
- 高可用设计
- 容忍服务失败,客户端须尽可能有效地做出响应
- 完善的监控和日志记录,架构元素或业务指标、链路追踪
- 快速发现不良突发行为并尽早修复
- 演进式设计
- 传统架构软件变更难以预测且改造成本高昂
- 合理设计实现频繁、快速且控制良好的增量变更和演化
- 合适的服务解耦,只需重新部署修改的服务
- 变更频率不同,拆分(相同,合并)
- 架构适应度函数(Architectural fitness function)
2. 微服务架构核心设计模式
对某一个问题的理解:上下文,需求约束,模式,模式间的关系…
2.1 微服务架构拆分模式
2.1.1 问题:如何将应用拆分为微服务?
需求
- 高内聚:实现一组密切相关的功能
- 松耦合:封装内部细节,API交互
- 单一职责原则(SRP)
- 共同封闭原则(CCP)
- 双披萨团队开发
- 团队自治
模式1:根据业务能力进行服务拆分
- 为企业产生价值的商业活动
- 保险:承保、理赔管理、账务管理等
- FTGO:供应商、消费者、订单获取和执行、记账管理
- 业务能力可分解:顶级能力和子能力
- 能力层次结构中各级别能力映射到服务中
结果:
- 内聚和解耦
- 能力与服务的映射具有主观性
- 过多的进程间通信导致重新分解或组合
相关模式:
- 根据子域进行服务拆分
模式2:根据子域进行服务拆分
- 领域驱动设计DDD核心:子域和限界上下文
- 领域:描述应用程序问题域的术语(包含子领域)
- 拆分过程:
- 分析业务
- 识别子领域(领域模型)
- 子领域模型边界(限界上下文、微服务边界)
- 订单获取、餐馆管理、配送、记账等
结果:
- 高内聚和松耦合
- 单独的领域模型来消除上帝类
- 领域模型由团队独立开发、支持团队自治
相关模式:
- 根据业务能力进行服务拆分
2.2 微服务架构通信模式
2.2.1 问题:同步通信中如何避免由于服务故障或网络中断所引起的故障蔓延到其他服务?
上下文
- 微服务,分布式,进程间调用
- 服务请求可能面临局部故障(故障/停机/过载)
- 同步通信客户端等待响应被阻塞,蔓延
需求
- 处理网络超时/无响应服务的能力
- 限制客户端向服务器发出请求的数量
- 决定如何从失败的远程服务中恢复
模式:断路器(Circuit Breaker)
- 服务客户端应通过代理调用远程服务(类似于电路断路器)
- 闭合状态:对程序的请求的直接引起方法调用
- 断开状态:对程序的请求会立即返回错误响应
- 半断开状态:当连续失败的次数超过阈值时
- 超时期限内,所有调用远程服务的尝试都将立即失败
- 超时到期后,断路器允许有限数量的测试请求通过
- 如果这些请求成功,断路器将恢复正常运行
- 否则,如果出现故障,超时期限将重新开始
结果
- 防止不断地尝试执行可能会失败的操作
- 使程序能够诊断错误是否已经修正,进而再次尝试调用操作
相关模式
- API网关模式、服务器端服务发现模式
2.2.2 问题:服务的客户端(包括API网关或者其它服务)如何在网络上发现服务实例的位置?
上下文
- 不同微服务之间通常需要进程间调用
- 在传统的分布式系统部署下,服务在固定且已知的位置(主机与端口)运行,从而确保各服务可利用 REST 或 RPC 机制进行相互调用
- 微服务通常在虚拟化或者容器化环境中运行,服务实例数量和位置动态变化
需求
- 每一服务实例的特定位置(主机与端口)信息
- 服务端实例的具体数量及位置动态变化信息
- 虚拟机与容器分配的动态IP地址信息
- 服务实例的数量信息的(EC自动伸缩组会根据负载情况随时调整实例数量)
模式1:应用层服务发现模式
- 自注册:服务实例调用服务注册表的注册 API 来注册其网络位置的(服务注册表定期调用心跳 API)
- 客户端发现:客户端查询服务注册表以获取服务实例的列表(缓存+负载均衡,提高性能)
结果:
- 相较于服务器端服务发现,其活动部件与网络中转数量更少
- 需要为应用中使用的每种编程语言/框架建立客户端服务发现逻辑,例如Netflix Prana 为非 JVM 客户端提供了一套基于 HTTP 代理的服务发现方案
- 客户端与服务注册表相耦合
- 开发者负责设置和管理服务注册表,分散精力
相关模式:
- 替代模式:平台层服务发现模式
模式2:平台层服务发现模式
- 第三方注册:由第三方负责(注册服务器,通常是部署平台的一部分)处理注册,而不是服务本身向服务注册表注册自己。
- 服务端发现:客户端无需查询服务注册表,而是向 DNS 名称发出请求,对该 DNS 名称的请求被解析到路由器,路由器查询服务注册表并对请求进行负载均衡。
结果:
- 完全交给部署平台,服务端、客户端代码减负
- 多语言支持度较高
- 存在平台约束
- 相较于客户端服务发现,需要更多的网络跳转
相关模式:
- 替代模式:应用层服务发现模式
2.2.3 问题:如何处理外部客户端与服务之间的通讯?
上下文
- 多个版本客户端需要开发多个适配的用户界面
- 产品信息通过API访问
- 数据分布在多项服务之间
需求
- 微服务通常提供的是细粒度API,客户端需要同多项服务进行交互
- 不同客户端需要不同的数据(桌面浏览器版本通常较复杂)
- 不同客户端的网络性能亦有所区别(移动网络速度更慢)。服务器端 Web 应用能够向后端服务发送多条请求,不会影响用户体验,但移动客户端则只能发送少量请求
- 服务实例数量与其位置(主机与端口)会发生动态变化
- 服务的划分方式会随时间的推移而改变,且不应被客户端所感知
模式1:API Gateway模式
- 实现一个服务,外部 API 客户端进入基于微服务应用的入口点(类似于外观模式)
- 部分请求会被直接代理/路由至对应的服务
- 部分请求则需要接入多项服务
- 针对不同客户端提供不同的 API
结果:
- 确保客户端无法察觉应用程序是如何被拆分为多项微服务的
- 确保客户端不受服务实例的位置的影响
- 为每套客户端提供最优API
- 降低请求/往返次数
- 将从客户端调用多项服务的逻辑转换为从API网关处调用,从而简化整个客户端
- API组合、协议转换和边缘功能,身份验证等
- 问题:复杂性、性能和可扩展性、局部故障等
相关模式:
- 后续模式:断路器模式、服务发现模式
模式2:后端前置模式
- 为每种类型的客户端实现单独的API Gateway
- 针对不同客户端提供不同的API
- 现成产品或服务、自研
结果:
- 封装应用程序内部结构,减少交互次数
- API组合、协议转换和边缘功能,身份验证等
- 解决API Gateway职责不明确问题
- API模块彼此隔离、可独立扩展、减少启动时间
相关模式:
- 替代模式:API Gateway模式
2.3 微服务架构部署模式
2.3.1 问题:如何部署?
上下文
- 微服务架构包含一组服务
- 每个服务都部署为一组服务实例,以实现吞吐量和可用性
需求
- 服务使用各种语言、框架和框架版本编写
- 需要快速构建、独立部署和扩展服务
- 服务实例需相互隔离
- 需要监控每个服务实例的行为、部署可靠
- 需限制服务消耗的资源(CPU和内存)
- 尽可能经济高效地部署应用程序
模式1:单主机部署多个服务实例
- 资源需求冲突的风险
- 在主机(物理机或虚拟机)上运行不同服务的多个实例。
- 有多种方法可以在共享主机上部署服务实例,包括:
- 将每个服务实例部署为一个 JVM 进程。例如,每个服务实例一个 Tomcat 或 Jetty 实例。
- 在同一个 JVM 中部署多个服务实例。例如,作为 Web 应用程序或 OSGI 包。
优点:
- 资源利用率相对较高
缺点:
- 资源需求冲突的风险
- 依赖版本冲突的风险
- 难以限制服务实例消耗的资源
- 在同一个进程中部署多个服务实例,很难监控每个服务实例的资源消耗,也不可能隔离每个实例
相关模式:
- 替代模式:单主机部署单个服务实例
模式2:单主机部署多个服务实例
- 在自己的主机上部署单个服务实例
优点:
- 服务实例彼此隔离
- 不存在资源需求或依赖版本冲突的可能性
- 一个服务实例最多只能消耗单个主机的资源
- 监控、管理和重新部署每个服务实例非常简单
缺点:
- 与单主机部署多个服务实例模式相比,资源利用效率可能较低,因为主机更多
相关模式:
- 替代模式:单主机部署多个服务实例、无服务器部署
- 特化模式:将服务部署到虚拟机、将服务部署到容器
模式3:将服务部署到虚拟机
- 将服务打包为虚拟机镜像,并将每个服务实例部署为单独的 VM
- 比如 Netflix 部署流水线将每个服务打包为一个 EC2 AMI(包含服务运行所需要的所有内容)
- 运行时每个服务实例是该镜像实例化的虚拟机,如 EC2 实例
- EC2 弹性负栽均衡器(Elastic Load Balancer)将请求路由到对应的实例
优点:
- 通过增加实例数量来扩展服务很简单。Amazon Autoscaling Groups 可以根据负载自动执行此操作
- VM 封装了用于构建服务的技术细节,例如所有服务都以完全相同的方式启动和停止
- 每个服务实例都是隔离的
- VM 对服务实例消耗的 CPU 和内存施加限制
- AWS 等 IaaS 解决方案为部署和管理虚拟机提供了成熟且功能丰富的基础设施,如弹性负载均衡器、自动缩放组
缺点:
- 资源利用效率较低(整台虚拟机)
- 部署速度相对较慢(分钟级)
- 系统管理的额外开销(操作系统、运行补丁)
相关模式:
- 替代模式:将服务部署到容器
- 泛化模式:单主机部署单个服务实例
模式4:将服务部署到容器
- 将服务打包为 (Docker) 容器镜像并将每个服务实例部署到容器
- 容器是一种更现代、更轻量级的部署机制,操作系统级的虚拟化机制
- 容器由在隔离的沙箱中运行的一个或多个进程组成,多个容器通常在一台机器上运行,容器共享操作系统
- 从在容器中运行的进程的角度来看,它就好像在自己的机器上运行一样,有独立IP、可消除端口冲突
- 使用 Docker 编排框架指定并协调容器资源,如 Kubernetes、Marathon/Mesos、Amazon EC2 Container Service
- 部署过程:
- 构建Docker镜像:在构建时,部署流水线使用容器镜像构建工具,该工具读取服务代码和镜像描述,以创建容器镜像并将其存储在镜像仓库中。
- 运行Docker镜像:在运行时,从镜像仓库中拉取容器镜像,并用于创建容器。
优点:
- 通过更改容器实例的数量可以直接扩展和缩减服务
- 容器封装了用于构建服务的技术细节,所有服务都以完全相同的方式启动和停止
- 每个服务实例都是隔离的
- 容器对服务实例消耗的 CPU 和内存施加限制
- 容器的构建和启动速度非常快
- 将应用程序打包为 Docker 容器比将其打包为 AMI 快 100 倍
- Docker 容器启动速度明显快于 VM(仅启动应用程序进程而非整个操作系统)
缺点:
- 大量的容器镜像管理工作(操作系统补丁、基础设施)
- 部署容器的基础设施不如部署虚拟机的基础设施丰富
相关模式:
- 替代模式:将服务部署到虚拟机
- 泛化模式:单主机部署单个服务实例
模式5:服务部署平台
- 使用部署平台作为应用程序部署的自动化基础设施
- 提供服务抽象(一组命名的、高度可用的服务实例)
- Docker 编排框架,包括 Docker swarm 模式和 Kubernetes
- 无服务器平台,例如 AWS Lambda
- PaaS,包括 Cloud Foundry 和 AWS Elastic Beanstalk
- Docker编排框架将运行Docker的一组计算机转变为资源集群,将容器分配给机器,提供资源管理、调度、服务管理功能
相关模式:
- 后续模式:将服务部署到虚拟机、将服务部署容器、无服务器部署
模式6:无服务器部署
- 使用公有云提供的 serverless 部署机制部署服务
- 部署细节对用户隐藏,用户和其组织不负责管理低级基础设施(无服务器概念)
- 基础设施获取服务代码并运行,根据消耗的资源为每个请求付费
- 需打包代码(例如ZIP),将其上传到部署基础设施
- 公有云serverless平台:AWS Lambda、Google Cloud Functions、Azure Functions
- 开源serverless框架:Apache Openwhisk、Fission on Kubernetes
优点:
- AWS服务集成简单:AWS服务生成事件、AWS API Gateway处理HTTP请求的Lambda函数
- 消除系统管理任务:底层系统管理、操作系统或运行时打补丁,专注于开发应用程序
- 弹性伸缩:AWS Lambda运行应用程序所需的多个实例以动态处理负载
- 基于使用情况的定价:与典型的laaS云不同,AWS Lambda按请求所消耗的资源收费
缺点:
- 长尾延迟:AWS Lambda动态运行代码,需花费时间配置和启动应用,某些请求具有高延迟(Java服务通常需要至少几秒钟,不适合对延迟敏感的服务)
- 基于有限事件与请求的编程模型:不用于长时间运行的服务(使用第三方消息代理的消息服务)
相关模式:
- 替代模式:单主机部署单个服务实例
2.4 微服务架构可观测性模式
2.4.0 上下文
- 多台机器上、多个服务和服务实例
- 请求跨越多服务实例,每个服务通过执行一个或多个操作来处理请求
- 以标准化格式将操作信息写入日志文件,跟踪用户行为和代码异常
- 服务实例可能无法处理请求但仍在运行
2.4.1 问题:如何理解用户和应用程序的行为并解决问题?
模式1:日志聚合模式
- 使用集中式日志记录服务聚合来自每个服务实例的日志
- 用户可搜索和分析日志
- 可配置当某些消息出现在日志中时触发的警报
- 实例:AWS Cloud Watch, Logstash (ELK)
需求:
- 任何解决方案都应该具有最小的运行时开销
缺点:
- 处理大量日志需要大量的基础设施
相关模式:
- 分布式追踪、异常跟踪
日志记录的基础设施:
- ELK
- Elasticsearch:面向文本搜索的NoSQL数据库,用作日志记录服务器
- Logstash:聚合服务日志并将其写入 Elasticsearch 的日志流水线
- Kibana: Elasticsearch的可视化工具
- 开源日志流水线包括 Fluentd 和 Apache Flume
- 商用如 AWS Cloud Watch Logs 等
模式2:审计日志模式
- 向业务逻辑中添加审计日志代码
- 创建审核日志条目并保存在数据库中
需求:
- 了解用户最近执行了哪些操作,帮助支持、确保合规性、安全性和可疑行为等
优点:
- 提供用户操作的记录
缺点:
- 审计代码与业务逻辑交织,使业务逻辑复杂化
相关模式:
- 后续模式:事件溯源(实施审计的可靠方式)
模式3:应用程序指标模式
- 检测服务以收集有关各个操作的统计信息,在集中式指标服务中聚合指标,提供报告和警报。聚合指标两种模型:
- push - 服务将指标推送到指标服务
- pull - 指标服务从服务中提取指标
- 实例:Coda Hale/Yammer Java 指标库、Prometheus(普罗米修斯)、AWS Cloud Watch等
需求:
- 任何解决方案都应该具有最小的运行时开销
优点:
- 提供对应用程序行为的深入洞察
缺点:
- 指标代码与业务逻辑交织在一起,使其更加复杂
- 聚合指标可能需要大量的基础设施
相关模式:其他可观测性模式
模式4:分布式追踪模式
- 记录单次请求范围以内的信息
- 为每个外部请求分配一个唯一的外部请求 ID
- 并在提供可视化和分析的集中式服务器中记录请求如何从一个服务流向下一个服务
- 在所有日志消息中包含外部请求 ID
- 记录在集中服务中处理外部请求时执行的请求和操作的信息(例如开始时间、结束时间)
需求:
- 外部监控只报告总体响应时间和调用次数,无法深入了解各个操作
- 任何解决方案都应该具有最小的运行时开销
- 请求的日志条目分散在许多日志中
实例
- Spring Cloud Sleuth - Spring Cloud 应用程序的分布式跟踪
- Open Zipkin - 用于记录和显示跟踪信息的服务
- Open Tracing - 用于分布式跟踪的标准化 API
优点:
- 提供了对系统行为的有用洞察,包括延迟的来源
- 使开发人员能够通过在聚合日志中搜索其外部请求 ID 来查看单个请求是如何处理的
缺点:
- 聚合和存储追踪数据可能需要大量的基础设施
相关模式:
- 日志聚合 - 外部请求 ID 包含在每个日志消息中
模式5:异常跟踪模式
- 向集中式异常跟踪服务报告所有异常,该服务聚合和跟踪异常并通知开发人员。
- 实例:Sentry Datadog、PagerDuty
上下文:
- 处理请求时有时会发生错误。发生错误时,服务实例会抛出异常,其中包含错误消息和堆栈跟踪
需求:
- 异常必须由开发人员去重、记录、调查并解决潜在问题
- 任何解决方案都应该具有最小的运行时开销
优点:
- 更容易查看异常并跟踪其解决方案
缺点:
- 异常跟踪服务是额外的基础设施
相关模式:
- 日志聚合 - 应记录异常并报告给跟踪服务
2.4.2 问题:如何检测正在运行的服务实例无法处理请求?
需求
- 当服务实例失败时应生成警报,请求应该被路由到正常工作的服务实例
模式:健康检查API模式
- 服务具有 /health 返回服务健康状况的健康检查 API 端点(实例Spring Boot Actuator),执行检查:
- 服务实例使用的基础设施服务的连接状态
- 主机的状态,例如磁盘空间
- 应用程序特定逻辑
- 监控服务、服务注册表或负载均衡可以定期 “ping” 调用端点来检查服务实例的健康状况
- 使用 Spring Boot 和 Spring Cloud 作为微服务框架
- 提供健康检查端点,由 Spring Boot Actuator 模块实现
- 配置调用 /health 可扩展健康检查逻辑的 HTTP 端点。
优点:
- 定期测试服务实例的健康状况
缺点:
- 不够全面
- 服务实例可能在健康检查之间失败
相关模式:
- 前置模式:服务注册与发现模式、部署相关模式