微服务
什么是微服务
随着领域驱动设计、持续交付、按需虚拟化、基础设施自动化、小型自治团队、大型集群系统这些实践的流行,微服务也应运而生。
微服务:把应用程序功能性分解为一组协同工作的小而自治的服务的架构风格。每个服务是一组专注的、内聚的功能职责组成。 每个服务是松耦合的,有自己的私有数据库,通过 API 通信。每个服务可以独立设计、开发、测试、部署和扩展。
微服务特点
- 很小,专注于做好一件事,一个微服务应该可以在两周内完全重写。
- 自治性:修改一个服务并对其进行部署,而不影响其他任何服务。
微服务优势
- 技术异构性:开发成本低,支持不同技术栈服务,开发人员可使用自己擅长的技术,不同服务场景可以选择最合适的技术实现
- 容错性:可靠性高,去中心化、集群化,降低了单点故障及性能瓶颈
- 灵活扩展:只对需要扩展的服务进行扩展,这样就可以把那些不需要扩展的服务运行在更小的、性能稍差的硬件上
- 硬件投入少:高峰期自动扩容、低谷时自动缩减,可实现对资源恰到好处的利用
- 简化部署:各个服务的部署是独立的,这样就可以更快地对特定部分的代码进行部署
- 团队自治:与组织结构匹配,避免出现过大的代码库,从而获得理想的团队大小及生产力
- 可组合性:代码复用率高,更小的粒度意味着更多的可复用性,避免重复造轮子
- 方便替代和升级:业务响应快,按细粒度业务单元拆分,新增或业务变更只需要修改小部分服务即可提测、发布
微服务劣势
- 服务拆分和定义是一项挑战(糟糕的是搞成了分布式单体应用)
- 分布式系统带来的各种复杂性,使开发、部署和测试变得更困难
- 跨服务开发需要协调多个团队。服务部署可能要按照依赖关系排序
- 什么阶段使用微服务?初创公司几乎都是从单体应用开始
- 复杂性从代码转移到基础设施
微服务的挑战
微服务并不能消除风险,而是将这个成本移到了系统生命周期的后半阶段:降低了开发过程中的冲突,但是增加了运维阶段系统部署、验证以及监控的复杂度。
- 识别和划定微服务范围需要大量专业的业务领域知识。
- 正确识别服务间的边界和契约是很困难的,而且一旦确定,是很难对它们进行改动的。
- 微服务是分布式系统,所以需要对状态、一致性和网络可靠性这些内容做出不同的假设。
- 跨网络分发系统组件以及不断增长的技术差异性,会导致微服务出现新的故障形式。
- 越来越难以了解和验证在正常运行过程中会发生什么事情(可观测性)。
- 不断增加的服务使得故障点增多。
微服务主流方案
- 微服务 SDK
- 服务网格
微服务组件
- 服务注册和发现
- 服务监控
- 熔断降级
- 流量控制
- 安全性
- 配置管理
- 网关
- 容器化
微服务设计
微服务设计可以参考领域驱动设计
微服务架构
微服务应用的 4 层架构如下
-
平台层——微服务平台提供了工具、基础架构和一些高级的基本部件,以支持微服务的快速开发、运行和部署。一个成熟的平台层会让技术人员把重心放在功能开发而非一些底层的工作上。
平台层需要提供以下支持:
- 服务运行的部署目标,包括基础设施的基本元件,如负载均衡器和虚拟机。
- 日志聚合和监控聚合用于观测服务运行情况。
- 一致且可重复的部署流水线,用于测试和发布新服务或者新版本。
- 支持安全运行,如网络控制、涉密信息管理和应用加固。
- 通信通道和服务发现方案,用于支持服务间交互。
-
服务层——在这一层,开发的各个服务会借助下层的平台层的支持来相互作用,以提供业务和技术功能。
-
边界层——客户端会通过定义好的边界和应用进行交互。这个边界会暴露底层的各个功能,以满足外部消费者的需求。
边界层可以实现一些其他面向客户端的功能:
- SSL 终止
- 认证和授权——验证 API 客户端的身份和权限;
- 限流——对客户端的滥用进行防卫;
- 缓存——降低后端整体的负载;
- 日志和指标收集——可以对客户端的请求进行分析和监控。
边界层 3 种相关但又不同的应用边界模式:
- API 网关,(缺点:承担的职责就会越来越多)
- 服务于前端的后端(BFF),会为每种客户端类型提供一个 API 网关
- 消费者驱动的网关,可以只构建一个“超级”API 来让消费者决定他们所需要的响应数据的样子。
-
客户端层——与微服务后端交互的客户端应用,如网站和移动应用。
微服务拆分
为了处理不确定性,服务应该从粗粒度开始,再慢慢的进一步分解,之后先扩张再收缩(下线和迁移)。
拆分的指导原则
- 单一职责: 改变类应该只有一个理由
- 闭包原则: 包中包含的所有类应该是对同类变化的一个集合。
- 按照业务能力和限界上下文(bounded context)划分——服务将对应于粒度相对粗一些但又紧密团结成一个整体的业务功能领域。
- 按用例(使用案例)划分——这种服务应该是一个“动词”型,它反映了系统中将发生的动作。
- 按易变性划分——服务会将那些未来可能发生变化的领域封装在内部。
- 按技术能力划分
- 在面向业务的服务中包含这个功能会使得服务过于复杂、增加未来替换的复杂度。
- 许多服务都需要的技术能力——比如,发送邮件通知。
- 可以独立于业务能力进行修改的技术能力——比如,重要的第三方系统集成。
微服务事务与查询
分布式事务的成本很高,服务拆分尽量避免产生跨服务事务,能合则合。如无法合并则优先考虑 TCC 或基于 MQ 的柔性事务,尽可能规避 2PC 等对性能影响很大的事务方案。TCC 可完全替换 2PC,但开发成本偏高,需要调用各方都同步修改以支持 Try、Confirm 和 Cancel 操作,某些场景会调用三方服务,其代码不受我们控制,此时可以考虑使用 MQ 实现异步消息和补偿性事务。
基于事件的最终一致性
异步事件能够帮助我们解除服务之间的耦合和提高系统整体的可用性,但是这也促使服务的开发者开始思考最终一致性(eventual consistency)。
当从单体应用向微服务应用迁移时,面向事件的服务通信方案是非常出色的。单体应用发出事件消息,而开发者在那些并行开发的微服务中消费这些消息。通过这种方式,开发者就可以在新的服务上开发新的功能,而不用担心新服务与原有的单体应用耦合太紧。
使用 Saga 管理事务
Saga 模式是一个本地事务序列,其每个事务在一个单独的微服务内更新数据存储并发布一个事件或消息。
Saga 中的首个事务是由外部请求(事件或动作)初始化的,一旦本地事务完成(数据已保存在数据存储且消息或事件已发布),那么发布的消息或事件则会触发 Saga 中的下一个本地事务。
如任一正向操作执行失败,则事务会执行之前各参与者(发布给之前的参与者消息或事件)的逆向回滚操作,回滚已提交的参与者,直至事务退回至其初始状态。
CQRS
CQRS 将服务分成了命令和查询两部分,每一部分分别由不同的数据存储来提供支持。
查询服务也可通过其他服务发出的事件消息来组建复合型的数据视图。
微服务可靠性
故障主要发生在四大领域中:
- 硬件——服务运行所依赖的底层物理基础设施和虚拟化的基础设施;
- 主机
- 数据中心
- 主机配置
- 物理网络
- 操作系统和资源隔离
- 通信——不同服务之间的协作以及服务与外部第三方之间的协作;
- 网络:网络连接会中断
- 防火墙
- DNS 错误:主机名不能在应用中被正确的传播或解析
- 消息传输
- 健康检查不充分:健康检查不能正确体现实例的状态,导致请求被路由到出现问题的实例
- 依赖——服务自身所依赖的服务的故障;
- 超时:请求超时
- 功能下线或者向后不兼容
- 内部组件故障:数据库或者缓存服务出现问题导致服务不能正常工作
- 外部依赖:外部系统(第三方 API)不能正常运行或者执行不符合预期
- 内部——服务本身的代码错误,比如工程师引入的代码缺陷。
可靠性设计
-
重试
- 永远要限制重试的总次数;
- 使用带抖动的指数退避策略来均匀地分配重试请求和避免进一步加剧负载;
- 仔细考虑哪些错误情况应该触发重试,以及哪些重试不大可能成功、哪些重试永远不会成功;
- 需要考虑幂等性。
- 加乐观锁
- 使用去重索引
- 使用 token 机制
-
后备方案
- 优雅降级
- 缓存
- 功能冗余
- 桩数据
-
超时:合理设置请求超时
-
断路器
如果在一定时间窗口内失败数或者失败率超过某个阈值,这时断路器就会被断开。这种情况下,我们的服务就不再尝试向协作服务发起请求了,而是会绕过这个请求,并在可能的情况下执行适当的后备方案——返回一个桩消息、路由到其他服务或者返回缓存的数据。
-
异步通信
使用类似消息队列这样的通信代理来设计异步的服务交互是提高可靠性的另一大技术。
-
负载均衡与服务健康
负载均衡器负责执行健康检查并会利用到检查的结果。
-
限流
- 丢弃超过容量的请求
- 关键数据请求优先
- 丢弃不常见的客户端
- 限制并行请求量
-
验证可靠性和容错性
-
压力测试
-
混沌测试
混沌测试会倒逼着微服务应用在生产环境中出现故障。通过引入不稳定性因素以及故障,混沌测试可以精确模拟真实系统的故障,同时也让工程团队得到训练,使得他们能够处理这些故障。
-
微服务底座
- 从一开始就支持在容器调度器中部署服务(CI/CD)。
- 可观测性
- 支持日志聚合。
- 收集度量指标数据。
- 错误报告。
- 具备同步和异步通信机制。
- 服务注册和发现。
- 配置获取。
- 数据存储设置。
- 负载均衡和健康检查。
微服务部署
微服务极大地增加了系统中活动部件的数量,从而增大了部署的复杂性。在部署微服务时,开发者将面临四大挑战:
- 面对大量的发布和组件变更时应保持稳定性;
- 避免会导致组件在构建阶段或者发布阶段产生依赖关系的紧耦合;
- 服务 API 发布不兼容的变更可能会对客户端产生非常大的负面影响;
- 服务下线。
微服务生产环境具备以下六大基础功能。
- 部署目标或者运行平台,也就是服务所运行的地方,比如虚拟机[理想情况下,工程师可以使用 API 来配置、部署和更新服务配置。开发者还可以将这种 API 称为控制面板(control pane)]、容器。
- 运行时管理,比如自动愈合和自动扩容。这样服务环境就可以动态地响应失败或者负载变化,而不需要人为干预(如果某个服务实例出现故障,它应该会被自动替换掉)。
- 日志和监控,用来监测服务的运行情况并方便工程师对服务执行的过程有深入了解。
- 支持安全运维,比如网络控制、密码凭据管理以及应用加固。
- 负载均衡、DNS 以及其他路由组件可将用户侧的请求以及微服务之间的请求路由分发出去。
- 部署流水线,安全地将服务从代码交付到生产环境中运行。
这些组件是微服务架构栈中平台层的组成部分。
不停机部署有 3 种常见的部署模式。
- 滚动部署:在启动新实例(版本为 N+1)时,逐步将旧实例(版本为 N)从服务中剔除,确保在部署期间最小比例的负载容量得到保证。
- 金丝雀部署:开发者在服务中添加一个新实例来验证 N+1 版本的可靠性,然后再全面推出。这种模式在常规滚动部署之外提供了附加安全措施。
- 蓝绿部署:创建一个运行新版本代码的并行服务组(绿色集合),开发者逐步将请求从旧版本(蓝色集合)中转移出去。在服务消费者对错误率高度敏感、无法接受不健康的金丝雀风险的情况下,这种方法比金丝雀部署模式更有效。
部署模式:基于语言特定的发布包格式
可以执行的 JAR/WAR 文件
好处:
- 快速部署
- 高效的资源利用
缺点:
- 缺乏技术栈的封装
- 无法约束服务实例消耗的资源
- 服务之间缺少隔离
- 难以自动判定放置服务实例的位置
- 尽量避免使用,除非所获效率的价值远在其他所有考量之上
部署模式:将服务部署为虚拟机
作为虚拟机镜像打包的服务部署到生产环境中,每个服务实例都是一个虚拟机。
优点:封装了技术栈;服务实例隔离;使用成熟云计算基础设施;
缺点:资源利用率低;部署速度慢;系统管理开销
服务部署为容器
将作为容器镜像打包的服务部署到生产环境,每个服务实例都是一个容器。
容器镜像:由应用程序和运行服务所需的依赖软件组成的文件系统镜像
好处:封装技术栈;实例隔离;资源受限
弊端:大量容器镜像管理工作
使用 Kubernets 部署
docker 编排框架,是 docker 之上的一个软件层,将一组计算机硬件资源转变为用于运行服务的单一资源池。
- 资源管理: 一组计算机视为 cpu、内存和存储卷构成的资源池,将计算机集群视为一台计算机
- 调度: 选择要运行容器的机器
- 服务管理:负载均衡,滚动升级等
Istio 服务网格
Serverless 部署
AWS Lambda
可观测性
利用度量指标、链路追踪和日志信息构建一套监控系统,以更加丰富全面地监测微服务应用,并且收集的信号来设置告警。
监控
应该把客户端、边界层、服务层和平台层这 4 层都监控起来。把各层的度量指标都收集到统一的地方以便观测。
利用 Prometheus 和 Grafana 搭建监控平台。
度量指标
从面向用户的系统中收集度量指标时,开发者应该关注四大黄金标志:时延、错误量、通信量和饱和度。
度量指标的类型:计数器、计量器、直方图。
告警
- 系统出错时哪些人需要知悉
- 触发告警的应该是症状,而非原因,比如用户会遇到的错误。
尽可能减少告警通知的数量并保持这些通知的可操作性来避免出现告警疲劳。系统正常行为的每次偏差都生成一条告警通知很快就会导致人们不再关心这些告警或者认为这些告警并不重要。这样会导致一些重要告警被忽视。
日志
生成一致的、结构化的、人类可读的日志,可以使用 ELK 或 EFK 进行收集、存储、展示。
日志中的有用信息:
- 时间戳,最好以毫秒为单位。时间戳还应该包含时区,建议开发者尽可能使用 GMT/UTC 来收集数据。
- 标识符,请求 ID、用户 ID 和其他唯一标识符是非常重要的。
- 来源,开发者可以使用的典型数据来源包括主机名、类名或模块名、函数名和文件名。
- 日志等级或类别
链路追踪
分布式链路追踪系统以可视化形式展示各个服务间的执行流程,同时展示每步操作所耗费的时间。这是非常有价值的,不仅有助于了解请求在各个服务间的流动顺序,还有助于发现可能的系统瓶颈。
OpenTracing API 是一个与供应商无关的分布式链路追踪开放标准。许多分布式跟踪系统(如 Dapper、Zipkin、HTrace、X-Trace)都提供了链路追踪功能,但使用的是互不兼容的 API。选择其中一个系统通常意味着可能要与使用不同编程语言的系统紧密耦合到一起,从而形成一个解决方案。OpenTracing 的目的是为链路追踪的信息收集提供一组约定的、标准化的 API。类库可用于不同的语言和框架。
请求关联:trace 和 span
trace 是由单个或多个 span 组成的有向无环图(DAG),这些 span 的边称为 reference。trace 用于聚合和关联整个系统的执行流。为此,需要传播一些信息。一个 trace 记录整个流程。
每个 span 包含如下信息:操作名称、起始时间戳和完成时间戳、零个或者多个 span 标签(键值对)、零个或多个 span 日志(带时间戳的键值对)、span 上下文(context)以及引用零个或多个 span 的参考(通过 span 上下文)。
这些 span 可以由同一个应用触发,也可以由不同的应用触发。唯一的要求是在触发新的 span 时,要传递父 span 的 ID,这样新的 span 就拥有了对父 span 的引用。
参考
https://zhuanlan.zhihu.com/p/34862889
https://www.infoq.cn/article/kdw69bdimlx6fsgz1bg3
http://autumn200.com/2019/04/24/Micro-service-architecture-design/