微服务(一):驱动因素、挑战与常用模式
目录
单体应用架构的挑战#
微服务架构是对应于单体架构的一种设计思路。在单体架构中,所有的功能模块都被打包在一个应用程序中进行部署和运行。这种方式在应用程序较小且功能简单时是可行的,但随着应用程序的复杂度增加,单体架构会面临以下问题:
-
可维护性差:单体应用程序的代码通常是紧密耦合的,导致修改一个功能可能会影响到其他功能,增加了维护的难度。
-
扩展性差:在单体架构中,应用程序的所有功能都部署在一起,难以针对某个功能进行独立扩展。
-
技术栈限制:单体应用通常使用单一的技术栈,限制了开发团队选择最佳工具和技术的灵活性。
-
部署风险高:单体应用的部署往往需要停机,影响用户体验。而微服务架构可以实现独立部署,降低了风险。
迁移到微服务和分布式架构的驱动因素#
微服务架构(将大型单体应用分解成单独且更小的部分)带来了以下关键优势和驱动力:
-
提高可维护性 (Maintainability)
大型单体架构通常维护性较低,因为功能是按技术分层而非领域划分的,组件之间耦合紧密,且领域凝聚力弱。
- 隔离变更范围:在单体应用中,对某个领域的更改必须在应用的各个层面传播。而在微服务架构中,变更范围被限制在特定的领域级别或功能级别内。
- 降低耦合度:随着微服务架构水平的提高,可维护性也随之提高,使得添加、更改或移除功能更加容易。
-
改善可测试性 (Testability)
微服务架构(将应用分解成更小的部署单元)能把一次改动的测试范围收窄,测试通常更容易做透。
- 减少测试范围:每个微服务可以独立测试,不必每次都回归整个系统。
-
增强可部署性 (Deployability)
可部署性不仅指部署的便利性,还包括部署的频率和整体风险。
- 降低单次部署的风险:部署风险主要来自一次发布打包进来的变更量:发布间隔越长,攒下的更改越多,出问题时越难定位、回滚代价也越大。微服务把更改隔离到单个服务,单次发布的变更集更小,风险随之下降,也让更频繁的发布变得可行。
-
提升可扩展性与弹性 (Scalability & Elasticity)
单体应用增长时,往往很快会耗尽机器资源(线程、内存和 CPU)。由于单体应用需要整体扩展,无法针对特定功能进行独立扩展。微服务架构带来的优势包括:
- 按需独立扩展:每个微服务可以根据其负载需求独立扩展,高负载的服务可以部署更多实例,而低负载的服务保持最小规模,从而更有效地利用资源。
- 快速弹性伸缩:微服务可以快速响应负载变化,在流量高峰时自动增加实例,在流量低谷时自动减少实例,实现动态的资源调配。
-
确保可用性/容错性 (Availability/Fault Tolerance)
在单体架构中,所有功能共享同一个数据库和同一个运行时,故障域就是整个应用:数据库出问题,所有功能一起受影响。
- 故障隔离:微服务架构通过将应用分解成单独的部署单元,把故障域切小。某个服务出问题时,受影响的主要是它自己和依赖它的调用方,而不是整个系统。
- 数据分解:拆分数据库同样是在切小故障域,而不是「消除」故障:某个库出故障,受影响的是拥有它的服务。代价是跨服务的数据一致性要靠应用层来保证,要运维的数据库实例也变多了。
定义微服务#
微服务架构是把单体应用拆分成更小组件的架构风格,主要为了解决两件事:
- 更快的开发速度,支持持续部署。
- 更容易扩展(手动或自动)。
一个微服务首先是一个**自治的(autonomous)**软件组件,可以独立地升级、替换和扩展。要实现自治,它必须满足以下几个关键标准:
微服务的关键标准#
-
共享-无架构(Shared-Nothing Architecture) 微服务之间不共享数据库里的数据。
-
通过明确定义的接口通信
- 可以使用 API 和同步服务,但首选通过异步发送消息。因为同步调用会引入耦合,影响可用性和扩展性。
- API 和消息格式必须稳定、文档完善,并遵循明确的版本控制策略。
-
部署为独立的运行时进程 每个微服务实例都在一个独立的运行时进程中运行(例如一个 container 里)。
-
实例是无状态的(Stateless) 任何进入的请求都可以由该微服务的任何实例来处理。
满足这些标准后,相比于扩展一个庞大的单体应用,扩展一个独立的微服务(增加实例数量)或利用云中的 自动扩展(autoscaling) 能力都会更容易。
微服务的大小准则#
对于“微服务应该有多大?”这个问题,有两条经验法则:
- 足够小:小到一个开发人员可以轻松掌握。
- 足够大:大到不会危及性能(延迟)和/或数据一致性(例如,不同微服务存储的数据之间不再能依赖 SQL 外键来保证一致性)。
微服务架构的挑战#
微服务作为自治(autonomous)的软件组件,带来了许多固有的挑战,这些挑战在使用同步通信时会进一步加剧。
微服务数量的增加会带来管理和维护上的复杂性#
- 连锁故障 (Chain of Failure):许多使用同步通信的小组件在高负载下可能导致连锁故障问题,一个组件的失败迅速蔓延到其他依赖它的组件。
- 配置管理 (Configuration Management):保持许多小组件的配置最新和同步非常困难。
- 分布式追踪 (Distributed Tracing):当一个请求的处理涉及许多组件时,要追踪其整个流程非常困难。这给**根源分析(root cause analysis)**带来了挑战,因为每个组件都只存储自己的本地日志记录。
- 硬件资源分析 (Resource Analysis):监控和分析每个微服务实例的资源使用情况(CPU、内存、网络等)变得更加复杂。
- 手动管理成本高且易错 (Manual Management): 随着微服务数量的增加,手动管理和维护变得越来越困难且容易出错。
分布式系统的固有难题#
将应用分解为一组自治组件的另一个重要结果是,它们形成了一个分布式系统(Distributed System)。
L. Peter Deutsch 在 1994 年列了七条「分布式计算的谬误」(fallacies of distributed computing),James Gosling 在 1997 年补上第八条「网络是同构的」。这些是开发者第一次构建分布式应用时容易做出、但实践中基本都站不住的假设:
| #谬误 (Fallacy) | 解释 (What Developers Often Assume) |
|---|---|
| 1. 网络是可靠的 | 网络连接和数据传输永远不会失败。 |
| 2. 延迟是零 | 数据传输是瞬时的,没有等待时间。 |
| 3. 带宽是无限的 | 传输数据量不受限制,速度永不下降。 |
| 4. 网络是安全的 | 通信通道是完全安全的,无需加密或身份验证。 |
| 5. 拓扑结构不会改变 | 网络的布局和组件位置是静态不变的。 |
| 6. 只有一个管理员 | 整个系统只需要一个管理员来管理。 |
| 7. 传输成本是零 | 数据传输和网络使用不会产生任何费用或开销。 |
| 8. 网络是同构的 | 网络中所有的组件、协议和技术都是相同且兼容的。 |
应对挑战的设计原则#
微服务架构需要把**弹性(Resilience)**做进设计里,以自动化方式应对故障。
系统恢复能力#
- 目标 (Goal): 能够自我修复和维护持续运行。
- 自动化实现要点 (Key Automation Implementation Points):
- 故障检测:快速、准确地识别失败或异常的组件。
- 组件重启:自动重启已失败的微服务实例。
客户端弹性#
- 目标 (Goal): 隔离故障,防止一个服务的问题蔓延到整个系统。
- 自动化实现要点 (Key Automation Implementation Points):
- 避免请求失败实例:客户端(调用方)必须设计为能够检测到并避免向已知的失败微服务实例发送请求。
- 请求恢复:当失败的微服务被修复或重启后,客户端应能自动恢复向其发送请求。
自动化要求#
- 目标 (Goal): 确保系统在面对大量微服务时能够高效运维。
- 自动化实现要点 (Key Automation Implementation Points):
- 完全自动化:所有故障检测、重启、请求隔离和恢复机制都必须是完全自动化的。
- 原因:随着微服务数量的增加,运维人员手动处理这些问题是不可行且成本高昂的。
微服务设计模式#
设计模式 (Design Pattern) 是一个成熟的概念,说的是在特定上下文里对某类问题的可重用解法。微服务架构里也有几个常用的设计模式。
服务发现#
| 描述 | |
|---|---|
| 问题 | 客户端如何找到微服务及其实例的动态 IP 地址?(微服务实例通常在容器中动态分配 IP 地址。) |
| 解决方案 | 添加一个 服务发现服务(Service Discovery Service) 组件。它跟踪当前可用的微服务及其 IP 地址。 |
| 要求 | 自动注册/注销实例;客户端请求被路由到逻辑端点;请求在可用实例间进行负载均衡;能够检测并避开不健康的实例。 |
| 实现策略 | 客户端路由(Client-side Routing,客户端使用库查询发现服务)或服务端路由(Server-side Routing,请求先到反向代理,由代理转发)。 |
API Gateway#
| 描述 | |
|---|---|
| 问题 | 在微服务体系结构中,如何安全地暴露部分微服务给外部世界,同时隐藏其余内部服务,并保护暴露的服务免受恶意请求? |
| 解决方案 | 添加一个 API Gateway 组件(通常也称为 Edge Server),所有外部请求都必须通过它,它充当了系统的单一入口点。 |
| 要求 | 隐藏不应外部访问的内部服务;保护外部服务(例如,使用 OAuth、OIDC、JWT 等标准安全协议)。 |
| 实现 | API Gateway 通常作为反向代理工作,并可与服务发现集成,提供动态负载均衡。 |
响应式微服务#
| 描述 | |
|---|---|
| 问题 | 传统的阻塞 I/O(如同步 HTTP 请求)会为每个请求分配一个操作系统线程。在微服务调用链中,线程等待响应会很快耗尽服务器的线程池,导致响应变慢甚至崩溃。 |
| 解决方案 | 两条路线:一是非阻塞 I/O 编程模型(如 Project Reactor,事件循环 + 操作符链);二是 Java Virtual Threads 这类轻量线程模型,代码仍是阻塞式写法,但阻塞的是廉价的虚拟线程,等待 I/O 时底层 carrier 线程会被释放去执行其他任务。两者都避免了「每个等待中的请求独占一个 OS 线程」,但编程模型不同。 |
| 要求 | 尽可能使用异步编程模型(消息驱动);如果必须使用同步模型,则使用支持非阻塞 I/O 的框架;微服务必须弹性(Resilient)和自愈(Self-Healing)。 |
集中配置#
| 描述 | |
|---|---|
| 问题 | 如何获取所有运行微服务实例的完整配置视图?如何在不重新部署的情况下更新配置并确保所有受影响的实例都被正确更新? |
| 解决方案 | 添加一个**配置服务器(Configuration Server)**组件,用于存储所有微服务的配置。 |
| 要求 | 能够在单一位置存储和管理一组微服务的配置信息,并支持针对不同环境(如 dev、test、prod)的不同设置。 |
集中日志分析#
| 描述 | |
|---|---|
| 问题 | 当每个微服务实例将日志写入其本地文件系统时,如何获取整个微服务体系结构环境的运行概览?如何快速发现错误并进行根源分析? |
| 解决方案 | 添加一个集中式日志管理组件。它能够检测新实例、从它们那里收集日志事件、以结构化和可搜索的方式存储日志,并提供查询和分析工具。 |
| 要求 | 微服务将日志事件流式传输到标准输出(stdout);日志事件必须使用关联 ID(Correlation ID)进行标记;定义规范化日志格式以便于查询和分析。 |
分布式追踪#
| 描述 | |
|---|---|
| 问题 | 如何追踪一个外部请求在多个协作微服务之间流动的路径(例如,定位延迟或识别导致故障的根源微服务)? |
| 解决方案 | 确保所有相关的请求和消息都使用一个**共同的关联 ID(Correlation ID)**进行标记,并将此 ID 包含在所有日志事件中。收集请求/响应进出每个微服务实例的时间戳,用于分析延迟。 |
| 要求 | 对所有传入或新请求分配唯一的关联 ID;微服务在发出传出请求或消息时必须带上此 ID;所有日志事件必须包含关联 ID;创建**追踪记录(Trace Records)**以记录请求的进入和退出时间。 |
熔断器#
| 描述 | |
|---|---|
| 问题 | 当一个微服务停止响应时,使用同步通信的调用链可能导致连锁故障,耗尽客户端的线程池,使系统大面积瘫痪。 |
| 解决方案 | 添加一个熔断器(Circuit Breaker)。它监控对外部服务的调用,一旦发现问题,就阻止新的传出请求。 |
| 要求 | 打开电路(Open Circuit):如果检测到服务有问题,立即快速失败(fail fast),不等待超时;半开电路(Half-Open):定期允许单个请求通过以探测服务是否恢复;关闭电路(Close Circuit):如果探测成功,关闭熔断器,恢复正常流量(实现自愈)。 |
控制循环#
| 描述 | |
|---|---|
| 问题 | 在有大量微服务实例的系统中,手动检测和纠正问题(如实例崩溃或挂起)非常困难。 |
| 解决方案 | 添加一个控制循环(Control Loop)组件。它持续观察系统的实际状态,并将其与运维人员设定的期望状态进行比较。如果两者不符,则采取行动使实际状态等于期望状态。 |
| 实现 | 在容器的世界中,Kubernetes 等容器编排器是该模式的典型实现。 |
集中监控和告警#
| 描述 | |
|---|---|
| 问题 | 当响应时间或硬件资源使用率过高时,很难发现问题的根源,例如,难以分析每个微服务消耗的硬件资源。 |
| 解决方案 | 添加一个监控服务(Monitor Service)组件。它能够收集每个微服务实例级别的硬件资源使用情况指标(Metrics)。 |
| 要求 | 能够从所有服务器(包括自动扩展服务器)收集指标;能够自动检测新启动的微服务实例并开始收集其指标;提供查询和分析指标的工具;能够定义警报(Alerts),在指标超过特定阈值时触发。 |
微服务的技术选型#
| 设计模式 | Spring Boot / Java | Spring Cloud | Kubernetes (平台原生) | Istio (服务网格) |
|---|---|---|---|---|
| 服务发现 | Spring Cloud LoadBalancer (客户端均衡) | Service 资源 (kube-proxy DNS) | ||
| API Gateway | Spring Cloud Gateway (L7 路由和过滤) | Gateway API (Gateway, HTTPRoute 资源) | ingress gateway (基于 Gateway API) | |
| 响应式微服务 | Java 虚拟线程 (Project Loom) 或 Spring WebFlux (非阻塞 I/O) | |||
| 集中配置 | Spring Config Server | ConfigMaps 和 Secrets | ||
| 集中日志分析 | EFK/Loki 栈 (作为集群附加组件) | |||
| 分布式追踪 | Micrometer Tracing (API) | Zipkin (服务器) | Jaeger (服务器) | |
| 熔断器 | Resilience4j (更现代的替代方案) | Outlier detection | ||
| 控制循环 | Controller Managers | |||
| 集中监控与告警 | Prometheus (指标收集), Grafana (可视化) | Kiali (服务网格可观测性) |
注意:服务发现、API Gateway、集中配置这几个模式,
Spring Cloud、Kubernetes、Istio 都能提供实现,各有取舍。
其他需要考虑的因素#
DevOps 的重要性#
-
目标:微服务的目标之一是实现更短的交付周期,甚至是持续交付(Continuous Delivery)。
-
理念:要实现快速交付,组织需要建立「你构建,你运行」(you build it, you run it)的文化。
-
组织结构:开发(Dev)和运维(Ops)团队必须更紧密地合作,组建对一个或一组微服务拥有端到端生命周期全责的团队。
-
自动化:团队必须自动化交付链,即建立**交付流水线(Delivery Pipeline)**来自动化构建、测试、打包和部署微服务的步骤。
组织架构与康威定律#
- 康威定律:“任何设计系统的组织,其设计出的结构都会与其自身的沟通结构一模一样。”
- 影响:传统的按技术专长(如 UX 团队、业务逻辑团队、数据库团队)划分的 IT 组织结构,往往会产生一个庞大的三层单体应用
- 微服务要求:要成功实施微服务,组织需要转变为专注于一个或一组相关微服务的团队,并且该团队拥有所需的全栈技能(包括业务逻辑和数据持久化技术)。
将单体应用分解为微服务#
-
挑战:这是最困难且成本高昂的决策之一。错误的分解可能导致:
- 交付缓慢:业务需求变更影响过多微服务。
- 性能差:完成一个业务功能需要微服务间进行大量请求,导致响应时间长。
- 数据不一致:相关数据分散在不同微服务中,可能随时间推移出现不一致。
-
良好方法:用领域驱动设计(Domain-Driven Design, DDD)的 Bounded Context 概念来确定微服务的边界。
- Bounded Context:定义了一个边界(通常是一个子系统或特定团队的工作范围),在这个边界内,一个特定的领域模型(model)被定义和适用。由 Bounded Context 定义出来的微服务,会有一份边界清晰的自有数据模型。
API 设计的重要性#
- 原则:如果微服务对外暴露公共 API,这些 API 必须易于理解并遵循良好的设计指南:
- 命名和数据类型一致性:在多个 API 中使用的相同概念,其命名和数据类型必须保持一致。
- 版本控制:API 必须以独立但受控的方式演进,需要采用适当的版本控制方案(例如 Semantic Versioning),让客户端按自己的节奏迁移到新的主版本。
The Twelve-Factor App#
The Twelve-Factor App 是一份关于如何构建软件即服务(SaaS)应用的方法论,包含 12 项指导原则,每条对应应用构建和运维中的一个方面。
| # | 要素名称 (Factor) | 核心思想 (Core Idea) |
|---|---|---|
| I | 基准代码 (Codebase) | 一个基准代码,多处部署。所有部署都来自同一个代码库,使用版本控制系统(如 Git)。 |
| II | 依赖 (Dependencies) | 显式声明和隔离依赖。应用不应依赖系统预装的包,所有依赖应明确声明并隔离。 |
| III | 配置 (Config) | 在环境中存储配置。将所有随部署环境变化的配置(如数据库密码、外部服务凭证)存储在环境变量中。 |
| IV | 后端服务 (Backing Services) | 把后端服务当作附加资源。无论是本地数据库还是第三方服务,都通过统一配置(如 URL/凭证)来访问,不区分本地或远程。 |
| V | 构建,发布,运行 (Build, Release, Run) | 严格分离构建(Build)、发布(Release)、运行(Run)三个阶段。 |
| VI | 进程 (Processes) | 将应用作为一个或多个无状态进程运行。任何需要持久化的数据都应存储在后端服务中。 |
| VII | 端口绑定 (Port Binding) | 通过端口绑定提供服务。应用应完全自包含,通过绑定到某个端口对外提供服务(无需外部注入 Web 服务器)。 |
| VIII | 并发 (Concurrency) | 通过进程模型进行扩展。通过增加无差别进程的数量,水平扩展应用的吞吐能力。 |
| IX | 可丢弃性 (Disposability) | 快速启动和优雅终止。应用进程应该快速启动,并且在需要关闭或重启时能够优雅地终止。 |
| X | 开发环境与生产环境等价 (Dev/Prod Parity) | 尽可能保持开发、预发和生产环境的等价性。 |
| XI | 日志 (Logs) | 把日志当作事件流。将日志作为无缓冲的事件流(通常写入 stdout 和 stderr),由执行环境负责收集、聚合和分析。 |
| XII | 管理进程 (Admin Processes) | 后台管理任务应作为一次性进程运行。管理/迁移脚本应与应用代码一起发布,并在隔离的环境中执行。 |
小结#
这一篇是概念梳理,我自己整理下来记住的是三点:
- 微服务的收益基本都落在「变更范围」上:可维护性、可测试性、可部署性、按需扩展,说到底都是把一次改动和一次故障圈在单个服务里。
- 代价是分布式系统那一整套问题,Deutsch 那几条谬误一条都躲不掉。所以服务发现、集中配置、集中日志、分布式追踪、熔断器这些模式不算加分项,而是补齐代价的必需品。
- 同一个模式往往有 Spring Cloud、Kubernetes、Istio 三种实现,选哪一层不只是技术判断,还要看团队愿意长期维护什么。
拆分边界(Bounded Context)和组织结构(康威定律)这两块,这篇只记到结论,没有展开,也还没有我自己的落地经验。