Microservice(1) Overview
目录
单体应用架构的挑战#
微服务架构是对应于单体架构的一种设计思路。在单体架构中,所有的功能模块都被打包在一个应用程序中进行部署和运行。这种方式在应用程序较小且功能简单时是可行的,但随着应用程序的复杂度增加,单体架构会面临以下问题:
-
可维护性差:单体应用程序的代码通常是紧密耦合的,导致修改一个功能可能会影响到其他功能,增加了维护的难度。
-
扩展性差:在单体架构中,应用程序的所有功能都部署在一起,难以针对某个功能进行独立扩展。
-
技术栈限制:单体应用通常使用单一的技术栈,限制了开发团队选择最佳工具和技术的灵活性。
-
部署风险高:单体应用的部署往往需要停机,影响用户体验。而微服务架构可以实现独立部署,降低了风险。
迁移到微服务和分布式架构的驱动因素#
微服务架构(将大型单体应用分解成单独且更小的部分)带来了以下关键优势和驱动力:
-
提高可维护性 (Maintainability)
大型单体架构通常维护性较低,因为功能是按技术分层而非领域划分的,组件之间耦合紧密,且领域凝聚力弱。
- 隔离变更范围:在单体应用中,对某个领域的更改必须在应用的各个层面传播。而在微服务架构中,变更范围被限制在特定的领域级别或功能级别内。
- 降低耦合度:随着微服务架构水平的提高,可维护性也随之提高,使得添加、更改或移除功能更加容易。
-
改善可测试性 (Testability)
微服务架构(将应用分解成更小的部署单元)显著减少了对服务更改的总体测试范围,从而实现了更彻底和更便捷的测试。
- 减少测试范围:每个微服务可以独立测试,无需对整个系统进行全面测试,大大提高了测试效率。
-
增强可部署性 (Deployability)
可部署性不仅指部署的便利性,还包括部署的频率和整体风险。
- 降低部署风险:频繁的部署操作(例如每两周一次或更频繁)会增加总体部署风险,因为多个更改被组合在一起。通过微服务架构,可以将更改隔离到单个服务,从而减少部署风险,允许更频繁地进行部署。
-
提升可扩展性与弹性 (Scalability & Elasticity)
单体应用增长时,往往很快会耗尽机器资源(线程、内存和 CPU)。由于单体应用需要整体扩展,无法针对特定功能进行独立扩展。微服务架构带来的优势包括:
- 按需独立扩展:每个微服务可以根据其负载需求独立扩展,高负载的服务可以部署更多实例,而低负载的服务保持最小规模,从而更有效地利用资源。
- 快速弹性伸缩:微服务可以快速响应负载变化,在流量高峰时自动增加实例,在流量低谷时自动减少实例,实现动态的资源调配。
-
确保可用性/容错性 (Availability/Fault Tolerance)
在单体架构中,由于所有服务共享一个数据库,数据库成为单点故障(SPOF)。如果数据库出现问题,整个应用程序都会受到影响。
- 故障隔离:微服务架构通过将应用分解成单独的部署单元,将灾难性故障隔离到特定服务,从而允许系统的其余部分正常运行。即使某个服务频繁出现问题,也不会影响其他核心功能的可用性。
- 数据分解:通过拆分数据库,消除单点故障。即使某个数据库发生故障,系统的其他部分也能继续运行,提高整体容错性。
定义微服务 (Defining a Microservice)#
微服务架构本质上是将单体应用拆分成更小组件的架构风格,旨在实现两个主要目标:
- 更快的开发速度,支持持续部署。
- 更容易扩展(手动或自动)。
一个微服务本质上是一个**自治的(autonomous)**软件组件,它可以独立地升级、替换和扩展。要实现自治,它必须满足以下几个关键标准:
微服务的关键标准 (Key Microservice Criteria)#
-
共享-无架构(Shared-Nothing Architecture) 微服务之间不能共享数据库中的数据!
-
通过明确定义的接口通信
- 可以使用 API 和同步服务,但首选通过异步发送消息。因为同步调用会引入耦合,影响可用性和扩展性。
- API 和消息格式必须稳定、文档完善,并遵循明确的版本控制策略。
-
部署为独立的运行时进程 每个微服务实例都在一个独立的运行时进程中运行(例如,一个container中)。
-
实例是无状态的(Stateless) 任何进入的请求都可以由该微服务的任何实例来处理。
满足这些标准后,相比于扩展一个庞大的单体应用,扩展一个独立的微服务(增加实例数量)或利用云中的 自动扩展(autoscaling) 能力都会更容易。
微服务的大小准则 (Rules of Thumb for Size)#
对于“微服务应该有多大?”这个问题,有两条经验法则:
- 足够小:小到一个开发人员可以轻松掌握。
- 足够大:大到不会危及性能(延迟)和/或数据一致性(例如,不同微服务存储的数据之间不再能依赖 SQL 外键来保证一致性)。
微服务架构的挑战#
微服务作为自治(autonomous)的软件组件,带来了许多固有的挑战,这些挑战在使用同步通信时会进一步加剧。
微服务数量的增加会带来管理和维护上的复杂性#
- 连锁故障 (Chain of Failure):许多使用同步通信的小组件在高负载下可能导致连锁故障问题,一个组件的失败迅速蔓延到其他依赖它的组件。
- 配置管理 (Configuration Management):保持许多小组件的配置最新和同步非常困难。
- 分布式追踪 (Distributed Tracing):当一个请求的处理涉及许多组件时,要追踪其整个流程非常困难。这给**根源分析(root cause analysis)**带来了挑战,因为每个组件都只存储自己的本地日志记录。
- 硬件资源分析 (Resource Analysis):监控和分析每个微服务实例的资源使用情况(CPU、内存、网络等)变得更加复杂。
- 手动管理成本高且易错 (Manual Management): 随着微服务数量的增加,手动管理和维护变得越来越困难且容易出错。
分布式系统的固有难题#
将应用分解为一组自治组件的另一个重要结果是,它们形成了一个分布式系统(Distributed System)。
Peter Deutsch 在 1994 年提出了著名的“分布式计算的八大谬误”(The 8 fallacies of distributed computing)。这些是开发者在首次构建分布式应用时经常做出的、但在实践中都会被证明是错误的假设,它们会带来巨大的麻烦:
| #谬误 (Fallacy) | 解释 (What Developers Often Assume) |
|---|---|
| 1. 网络是可靠的 | 网络连接和数据传输永远不会失败。 |
| 2. 延迟是零 | 数据传输是瞬时的,没有等待时间。 |
| 3. 带宽是无限的 | 传输数据量不受限制,速度永不下降。 |
| 4. 网络是安全的 | 通信通道是完全安全的,无需加密或身份验证。 |
| 5. 拓扑结构不会改变 | 网络的布局和组件位置是静态不变的。 |
| 6. 只有一个管理员 | 整个系统只需要一个管理员来管理。 |
| 7. 传输成本是零 | 数据传输和网络使用不会产生任何费用或开销。 |
| 8. 网络是同构的 | 网络中所有的组件、协议和技术都是相同且兼容的。 |
应对挑战的设计原则 (Design Principles for Resilience)#
微服务架构必须设计为具有强大的**弹性(Resilience)**能力,以自动化方式应对故障。
核心设计原则 (Core Design Principles)#
1. 系统恢复能力 (System Recovery)#
- 目标 (Goal): 能够自我修复和维护持续运行。
- 自动化实现要点 (Key Automation Implementation Points):
- 故障检测:快速、准确地识别失败或异常的组件。
- 组件重启:自动重启已失败的微服务实例。
2. 客户端弹性 (Client-side Resilience)#
- 目标 (Goal): 隔离故障,防止一个服务的问题蔓延到整个系统。
- 自动化实现要点 (Key Automation Implementation Points):
- 避免请求失败实例:客户端(调用方)必须设计为能够检测到并避免向已知的失败微服务实例发送请求。
- 请求恢复:当失败的微服务被修复或重启后,客户端应能自动恢复向其发送请求。
3. 自动化要求 (Automation Requirements)#
- 目标 (Goal): 确保系统在面对大量微服务时能够高效运维。
- 自动化实现要点 (Key Automation Implementation Points):
- 完全自动化:所有故障检测、重启、请求隔离和恢复机制都必须是完全自动化的。
- 原因:随着微服务数量的增加,运维人员手动处理这些问题是不可行且成本高昂的。
微服务设计模式#
设计模式 (Design Pattern) 是一个成熟的概念,本质上是描述了在特定上下文中对一个问题提供可重用的解决方案。 微服务架构中有许多设计模式可以帮助解决常见的挑战和问题。
服务发现 (Service Discovery)#
| 描述 | |
|---|---|
| 问题 | 客户端如何找到微服务及其实例的动态 IP 地址?(微服务实例通常在容器中动态分配 IP 地址。) |
| 解决方案 | 添加一个 服务发现服务(Service Discovery Service 组件。它跟踪当前可用的微服务及其 IP 地址。 |
| 要求 | 自动注册/注销实例;客户端请求被路由到逻辑端点;请求在可用实例间进行负载均衡;能够检测并避开不健康的实例。 |
| 实现策略 | 客户端路由(Client-side Routing,客户端使用库查询发现服务)或服务端路由(Server-side Routing,请求先到反向代理,由代理转发)。 |
API 网关 (API Gateway)#
| 描述 | |
|---|---|
| 问题 | 在微服务体系结构中,如何安全地暴露部分微服务给外部世界,同时隐藏其余内部服务,并保护暴露的服务免受恶意请求? |
| 解决方案 | 添加一个 API网关(API Gateway) 组件(通常也称为Edge Server),所有外部请求都必须通过它,它充当了系统的单一入口点。 |
| 要求 | 隐藏不应外部访问的内部服务;保护外部服务(例如,使用 OAuth、OIDC、JWT 等标准安全协议)。 |
| 实现 | API网关通常作为反向代理工作,并可与服务发现集成,提供动态负载均衡。 |
响应式微服务 (Reactive Microservices)#
| 描述 | |
|---|---|
| 问题 | 传统的阻塞 I/O(如同步 HTTP 请求)会为每个请求分配一个操作系统线程。在微服务调用链中,线程等待响应会很快耗尽服务器的线程池,导致响应变慢甚至崩溃。 |
| 解决方案 | 使用非阻塞 I/O,例如采用Project Reactor或虚拟线程等编程模型。这样,线程在等待其他服务处理时不会被分配。 |
| 要求 | 尽可能使用异步编程模型(消息驱动);如果必须使用同步模型,则使用支持非阻塞I/O的框架;微服务必须弹性(Resilient)和自愈(Self-Healing)。 |
集中配置 (Central Configuration)#
| 描述 | |
|---|---|
| 问题 | 如何获取所有运行微服务实例的完整配置视图?如何在不重新部署的情况下更新配置并确保所有受影响的实例都被正确更新? |
| 解决方案 | 添加一个**配置服务器(Configuration Server)**组件,用于存储所有微服务的配置。 |
| 要求 | 能够在单一位置存储和管理一组微服务的配置信息,并支持针对不同环境(如 dev、test、prod)的不同设置。 |
集中日志分析 (Centralized Log Analysis)#
| 描述 | |
|---|---|
| 问题 | 当每个微服务实例将日志写入其本地文件系统时,如何获取整个微服务体系结构环境的运行概览?如何快速发现错误并进行根源分析? |
| 解决方案 | 添加一个集中式日志管理组件。它能够检测新实例、从它们那里收集日志事件、以结构化和可搜索的方式存储日志,并提供查询和分析工具。 |
| 要求 | 微服务将日志事件流式传输到标准输出(stdout);日志事件必须使用关联 ID(Correlation ID)进行标记;定义规范化日志格式以便于查询和分析。 |
分布式追踪 (Distributed Tracing)#
| 描述 | |
|---|---|
| 问题 | 如何追踪一个外部请求在多个协作微服务之间流动的路径(例如,定位延迟或识别导致故障的根源微服务)? |
| 解决方案 | 确保所有相关的请求和消息都使用一个**共同的关联 ID(Correlation ID)**进行标记,并将此 ID 包含在所有日志事件中。收集请求/响应进出每个微服务实例的时间戳,用于分析延迟。 |
| 要求 | 对所有传入或新请求分配唯一的关联 ID;微服务在发出传出请求或消息时必须带上此 ID;所有日志事件必须包含关联 ID;创建**追踪记录(Trace Records)**以记录请求的进入和退出时间。 |
熔断器 (Circuit Breaker)#
| 描述 | |
|---|---|
| 问题 | 当一个微服务停止响应时,使用同步通信的调用链可能导致连锁故障,耗尽客户端的线程池,使系统大面积瘫痪。 |
| 解决方案 | 添加一个熔断器(Circuit Breaker)。它监控对外部服务的调用,一旦发现问题,就阻止新的传出请求。 |
| 要求 | 打开电路(Open Circuit):如果检测到服务有问题,立即快速失败(fail fast),不等待超时;半开电路(Half-Open):定期允许单个请求通过以探测服务是否恢复;关闭电路(Close Circuit):如果探测成功,关闭熔断器,恢复正常流量(实现自愈)。 |
控制循环 (Control Loop)#
| 描述 | |
|---|---|
| 问题 | 在有大量微服务实例的系统中,手动检测和纠正问题(如实例崩溃或挂起)非常困难。 |
| 解决方案 | 添加一个控制循环(Control Loop)组件。它持续观察系统的实际状态,并将其与运维人员设定的期望状态进行比较。如果两者不符,则采取行动使实际状态等于期望状态。 |
| 实现 | 在容器的世界中,Kubernetes 等容器编排器是该模式的典型实现。 |
集中监控和告警 (Centralized Monitoring and Alarms)#
| 描述 | |
|---|---|
| 问题 | 当响应时间或硬件资源使用率过高时,很难发现问题的根源,例如,难以分析每个微服务消耗的硬件资源。 |
| 解决方案 | 添加一个监控服务(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 网关和集中配置等模式,
Spring Cloud、Kubernetes或Istio都可以提供实现。但是它们有各自的优缺点。
其他需要考虑的因素#
DevOps 的重要性#
-
目标:微服务的目标之一是实现更短的交付周期,甚至是持续交付(Continuous Delivery)。
-
理念:要实现快速交付,组织需要建立 “你构建,你运行”(you built it, you run it) 的文化。
-
组织结构:开发(Dev)和运维(Ops)团队必须更紧密地合作,组建对一个或一组微服务拥有端到端生命周期全责的团队。
-
自动化:团队必须自动化交付链,即建立**交付流水线(Delivery Pipeline)**来自动化构建、测试、打包和部署微服务的步骤。
组织架构与康威定律 (Conway’s Law)#
- 康威定律:“任何设计系统的组织,其设计出的结构都会与其自身的沟通结构一模一样。”
- 影响:传统的按技术专长(如 UX 团队、业务逻辑团队、数据库团队)划分的 IT 组织结构,往往会产生一个庞大的三层单体应用
- 微服务要求:要成功实施微服务,组织需要转变为专注于一个或一组相关微服务的团队,并且该团队拥有所需的全栈技能(包括业务逻辑和数据持久化技术)。
将单体应用分解为微服务#
-
挑战:这是最困难且成本高昂的决策之一。错误的分解可能导致:
- 交付缓慢:业务需求变更影响过多微服务。
- 性能差:完成一个业务功能需要微服务间进行大量请求,导致响应时间长。
- 数据不一致:相关数据分散在不同微服务中,可能随时间推移出现不一致。
-
良好方法:应用领域驱动设计(Domain-Driven Design, DDD)及其Bounded Contexts概念来确定微服务的边界。
- Bounded Contexts:定义了一个边界(通常是一个子系统或特定团队的工作范围),在这个边界内,一个特定的领域模型(model)被定义和适用。由Bounded Contexts定义的微服务将拥有定义清晰的自身数据模型。
API 设计的重要性#
- 原则:如果微服务对外暴露公共 API,这些 API 必须易于理解并遵循良好的设计指南:
- 命名和数据类型一致性:在多个 API 中使用的相同概念,其命名和数据类型必须保持一致。
- 版本控制:API 必须以独立但受控的方式演进,需要采用适当的版本控制方案(例如Semantic Versioning语义化版本控制),允许客户端按自己的节奏迁移到新的主要版本。
The 12-Factor App#
TheTwelve-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) | 后台管理任务应作为一次性进程运行。管理/迁移脚本应与应用代码一起发布,并在隔离的环境中执行。 |
总结#
从单体架构到微服务架构的转型,不仅仅是技术层面的变革,更涉及到开发流程、团队文化和组织架构的全方位调整。微服务通过提升可维护性、可扩展性和可部署性,为快速迭代的现代化应用提供了坚实的基础。
然而,这种架构也带来了分布式系统的固有复杂性,如服务发现、配置管理、分布式追踪等挑战。幸运的是,通过应用成熟的设计模式(如 API 网关、熔断器)和遵循“十二要素应用”等最佳实践,我们可以有效地应对这些挑战。
选择合适的工具(如Spring Cloud, Kubernetes, Istio)并结合 DevOps 文化,是成功实施微服务架构的关键。