建筑学
何时从整体迁移到微服务(何时不迁移)
保持单一,直到您拥有超过 20 名工程师并且每周遇到两次部署冲突。 微服务的基础设施成本为 500-5,000 美元/月,而单体架构的成本为 50-200 美元/月,团队将 20-30% 的容量用于运营。 当出现特定的疼痛信号时,使用扼杀者无花果图案一次提取一项服务。
大多数过早采用微服务的初创公司都会后悔。 大多数长期坚持单体应用的企业都会后悔。 问题不在于哪个更好。 到了该换的时候了。
本文介绍了单体架构和微服务架构之间的真正权衡、告诉您是时候迁移的信号、告诉您不是的信号,以及避免最常见(也是最昂贵)错误的分步方法。
什么是整体架构以及它为何有效
单体是单个可部署单元。 一个代码库、一个数据库、一个构建管道、一个部署。 您的身份验证模块、您的计费系统、您的通知服务、您的 API 层; 它们都位于同一个存储库中并一起部署。
这听起来有限制,但对于大多数团队来说,情况恰恰相反。 巨石给你速度。 您可以在单个拉取请求中跨模块边界进行重构。 您运行一个测试套件。 您部署一个工件。 您可以通过读取一组日志进行调试。 服务之间没有网络延迟,因为没有网络调用; 这都是进程内函数调用。
每一家成功的科技公司都是从一个整体开始的。 Shopify 运行一个单一的 Rails 应用程序,为数十亿美元的交易提供服务。 Stack Overflow 通过一个整体为每月 1 亿访问者提供服务。 这些并不是无法弄清楚微服务的公司。 他们的公司明白,单体应用是解决他们问题的正确工具。
如果您使用同一代码库的工程师少于 20 名,那么单体架构几乎肯定是正确的选择。 句号。
什么是微服务以及它们的成本是多少
微服务架构将您的应用程序拆分为独立的服务。 每个服务都拥有自己的数据库,独立部署,并通过 API(通常是 REST 或 gRPC)或消息队列与其他服务通信。
承诺:团队可以独立部署、扩展各个组件,并在有意义的地方使用不同的技术。 计费服务可以扩展以处理月末发票处理,而无需扩展整个应用程序。 搜索服务可以使用 Elasticsearch,而应用程序的其余部分在 PostgreSQL 上运行。
成本:您现在正在运行一个分布式系统。 分布式系统更难构建、更难测试、更难调试。 这是您注册的目的:
- 服务之间的网络故障。在整体中,函数调用要么起作用,要么抛出异常。 在微服务中,请求可能会超时、返回部分响应或静默失败。 您需要为每个服务间调用提供重试逻辑、断路器和回退策略。
- 分布式交易。当单个操作跨越两项服务(向客户收费,然后更新库存)时,您需要传奇或最终一致性模式。 这些构建起来很复杂,调试起来也很痛苦。
- 可观测性开销。单个请求可能会涉及 6 个服务。 如果没有分布式跟踪(Jaeger、Zipkin 或 Datadog APM),调试失败的请求意味着搜索 6 个独立的日志流,希望关联时间戳。
- 部署复杂性。您需要为每个服务提供容器编排(Kubernetes 或 ECS)、服务发现、API 网关和 CI/CD 管道。 1条管道变成15条。
- 数据一致性挑战。每个服务都拥有自己的数据库。 跨服务连接数据意味着 API 调用,而不是 SQL 连接。 报告成为一个工程项目,而不是一个查询。
这些问题都不是无法解决的。 但每一项都会增加工程时间、基础设施成本和运营复杂性,这是单体架构所不具备的。
单体架构一直很好,直到出现问题为止
四个特定信号告诉您您的巨石已经无法满足自身需求:
1.部署频率冲突
A 团队今天需要发布计费修复程序。 B 团队的半成品功能位于同一部署管道中,并且它破坏了分段。 A队等待。 B 团队急于修复他们的代码。 两者的发货都比计划晚。 当这种情况每月发生一次时,这只是一个小烦恼。 当这种情况每周发生两次时,您就会因为协调开销而浪费数天的工程时间。
2.团队互相踩踏
合并共享模块中的冲突。 两个团队在同一个冲刺中更改相同的数据库架构。 需要来自三个不同功能区域的上下文的代码审查。 这些迹象表明您的代码库已经超出了单个团队可以拥有的范围。 当工程师花在协调上的时间多于编码时,单体就会减慢你的速度。
3. 一个模块的错误会毁掉一切
图像处理代码中的内存泄漏会导致整个应用程序崩溃,包括结帐。 报告模块中的数据库查询速度缓慢会降低所有用户的 API 响应时间。 在整体中,模块共享资源。 一个组件的故障会级联到所有其他组件。 如果您最关键的收入路径(结账、支付、核心 API)因不太关键的模块出现故障而中断,这是一个明确的信号。
4. 扩展一项功能意味着扩展整个应用程序
您的视频转码模块需要 API 层 CPU 的 8 倍。 但它们作为一个单元进行部署,因此您为整个应用程序运行 8 个 CPU 来满足一个组件的需求。 您的基础设施费用比实际需要高出 5-10 倍,因为您无法独立扩展组件。
如果您持续遇到两个或多个这些信号,那么是时候开始考虑提取了。 不是完全重写。 萃取。
迹象表明您还没有准备好使用微服务
在开始计划迁移之前,请检查这五个条件。 如果其中任何一个适用,请继续使用您的整体架构。
- 您的工程师少于 5 名。微服务解决团队协调问题。 3人小组不存在协调问题; 它有一个 Slack 频道和一个白板。 运行分布式基础设施的开销将消耗小团队容量的 30-40%。
- 您为不到 10,000 名用户提供服务。在流量较低的情况下,单个服务器可以轻松处理所有事情。 微服务增加了延迟(服务之间的网络跃点)和复杂性,但在此数量上却无法提供有意义的扩展优势。
- 您在团队中没有 DevOps 经验。微服务需要容器编排、服务网格配置、分布式日志记录和自动化部署管道。 如果没有人在生产中操作过这些系统,您将花费数月时间来构建基础设施而不是功能。
- 您没有适当的监控或可观察性。如果您今天无法通过整体应用程序跟踪请求,那么明天您肯定无法跨 10 个服务跟踪该请求。 在拆分任何内容之前,设置结构化日志记录、错误跟踪 (Sentry) 和应用程序性能监控(Datadog、New Relic)。
- 您尚未在整体中定义明确的模块边界。如果您的代码是一个复杂的网络,其中计费模块直接从用户会话表读取并且通知系统写入订单数据库,那么提取服务将是一场噩梦。 首先清理边界。
迁移路径:绞杀无花果图案
团队犯的最大错误是尝试大爆炸重写。 他们花费 6-12 个月将整个系统重建为微服务,同时整体系统继续在生产中运行。 当重写“完成”时,单体应用将拥有重写所没有的 6-12 个月的新功能。 重写永远赶不上。 该项目被取消。 球队士气低落。
绞杀无花果图案完全避免了这一点。 这种方法以围绕寄主树生长并逐渐取代它的绞杀无花果树命名,该方法分三个步骤进行:
- 步骤一:确定要提取的一种成分。 通过 API 网关或代理路由其流量。
- 步骤2:与整体架构一起构建新服务。 两者同时在生产中运行。 代理将流量路由到新服务以获取提取的组件,并将流量路由到整体以获取其他所有内容。
- 步骤3:一旦新服务可靠地处理 100% 的流量,请从整体中删除旧代码。 对下一个组件重复此操作。
这种方法风险较低,因为您永远不会立即更换整个系统。 如果新服务出现问题,代理会将流量路由回单体应用。 用户永远不会注意到。
首先要提取什么
对于第一次提取,您有两个不错的选择:
选项 A:最常更改的组件。查看你的 git 日志。 哪个目录在过去 6 个月内提交次数最多? 这是引起最多部署冲突的组件。 提取它可以让您的团队立即实现部署独立性。
选项B:需要独立缩放的组件。如果您的视频处理模块消耗 API 层资源的 10 倍,则提取它可以让您独立扩展(并支付)每个组件。 仅节省成本就足以证明迁移工作的合理性。
不要从最复杂的组件开始。 不要从对其他模块依赖性最强的组件开始。 选择具有清晰边界、清晰 API 界面以及准备好端到端拥有它的团队的东西。 您的第一次提取既是一次学习练习,也是一次架构改进。
导致微服务迁移失败的常见错误
提取太多服务太快
第一次成功提取后,团队会很兴奋,并尝试立即分割所有内容。 突然间,他们运行了 12 项服务、12 个部署管道、12 组日志和 12 个潜在故障点。 运营负担压垮了团队。 提取一项服务,在生产中运行 4-6 周,从操作挑战中学习,然后提取下一项服务。
分布式整体架构
这是最常见的故障模式。 您将应用程序拆分为 8 个服务,但它们都共享相同的数据库。 它们全部部署在一起,因为一项服务的更改需要影响其他服务的架构更改。 您拥有微服务的所有操作复杂性,却没有任何好处。 每个服务必须完全拥有自己的数据。 如果两个服务需要相同的数据,它们将通过 API 或事件进行通信,而不是共享表。
没有服务网格或可观察性
在没有分布式跟踪的情况下运行微服务就像在夜间开车时关闭车灯一样。 除非用户抱怨,否则您不会知道服务质量正在下降。 如果不手动搜索多个日志流,您将不知道哪个服务导致了故障。 在提取第一个服务之前,请设置分布式跟踪、集中式日志记录和运行状况检查端点。 这不是可选的基础设施。 这是一个先决条件。
成本比较:单体应用与微服务
这些架构之间的基础设施成本差异很大,而团队始终低估它。
| 成本类别 | 巨石 | 微服务 |
|---|---|---|
| 计算(服务器/容器) | $50 - $200/月 | $300 - $2,000/月 |
| 容器编排(K8s/ECS) | $0(不需要) | $75 - $500/月 |
| 监控和可观察性 | $0 - $50/月 | $100 - $1,000/月 |
| 数据库(多个数据库与单个数据库) | $15 - $100/月 | $100 - $1,000/月 |
| 消息队列/事件总线 | $0(不需要) | $25 - $500/月 |
| 每月基础设施总额 | $50 - $200 | $500 - $5,000 |
这些范围反映了中期初创公司和成长期公司。 企业规模的部署可以在两端运行得更高。
基础设施成本是看得见的部分。 隐藏的成本是工程时间。 运行微服务的团队将 20-30% 的容量用于操作任务:更新 Kubernetes 配置、调试服务间通信、管理跨多个服务的数据库迁移以及维护 CI/CD 管道。 这些工程时间并没有用于实现用户关心的功能。
“模块化整体”中间地带
大多数架构文章都会跳过第三个选项:模块化整体。 您保留单个可部署单元,但在其中强制执行严格的模块边界。
每个模块都拥有自己的数据库表(或模式)。 模块通过明确定义的内部 API 进行通信,而不是通过访问彼此的数据库表或调用私有函数。 代码的组织方式使得将模块提取到独立服务中需要更改传输层(从进程内函数调用到 HTTP/gRPC),而无需重写业务逻辑。
模块化单体为您提供微服务的大部分组织优势(清晰的所有权、模块内的独立开发、强制边界),而无需运营成本。 您部署一个工件。 您运行一台数据库服务器。 您搜索一个日志流。
Shopify 的架构是最著名的例子。 他们运行一个具有严格执行的模块边界的整体 Rails 应用程序。 当模块需要成为服务时(由于扩展要求或团队独立性需要),提取很简单,因为边界已经干净了。
对于 5 到 30 名工程师的团队来说,模块化整体架构通常是正确的答案。 您无需支付基础设施和运营税即可获得微服务思维的结构和规则。
萨维推荐什么
我们为金融科技、电子商务和 SaaS 领域的客户构建了整体架构、模块化整体架构和微服务架构。 这是我们遵循的剧本:
- 开始单体化。每个新产品都是从一个整体开始的。 首要任务是发布功能、验证市场和快速迭代。 如果没有人使用你的产品,架构的纯粹性就不重要了。
- 从第一天起就强制执行模块边界。即使在一个整体中,每个模块也应该拥有自己的数据并公开一个清晰的接口。 这几乎不需要任何前期成本,并且可以节省数月的重构时间。
- 尽早建立可观察性。结构化日志记录、错误跟踪和基本性能监控。 无论您是保持整体还是迁移,您都需要它。 这是赌注。
- 仅当出现特定疼痛时才提取服务。部署冲突阻止发布。 模块之间的资源争用。 团队协调开销会消耗工程能力。 这些是真实的信号,而不是理论上的担忧。
- 使用绞杀无花果图案进行提取。一次一项服务。 它与整体一起运行。 验证它是否有效。 然后继续下一个。 永远不要进行大爆炸重写。
- 将模块化整体视为您的长期架构。对于许多产品来说,这是两全其美的。 只有当模块化单体的单一部署约束成为瓶颈时,您才会转向真正的微服务。
最好的架构是能让您的团队提供客户想要的功能的架构。 对于大多数阶段的大多数公司来说,这是一个结构良好的整体。 对于一些处于某些拐点的公司来说,这是对特定服务的有针对性的提取。 对于极少数公司来说,这是一个完整的微服务架构。
不要根据 Netflix 或 Uber 使用的内容来选择架构。 根据您的团队规模、流量模式、部署频率以及您今天遇到的具体瓶颈来选择它。 不是你两年内可能遇到的瓶颈。
常见问题
我什么时候应该从整体迁移到微服务?
当您看到以下两个或多个信号时,请进行迁移:部署冲突每周两次阻止发布、团队在共享模块中互相踩踏、一个模块的错误导致整个应用程序崩溃,或者扩展一项功能迫使您扩展所有内容。 如果您的工程师少于 20 名,那么整体架构几乎肯定是正确的选择。
与整体服务相比,微服务的成本是多少?
单体应用的基础设施费用为每月 50-200 美元。 如果添加容器编排(75-500 美元)、多个数据库(100-1,000 美元)、可观察性工具(100-1,000 美元)和消息队列(25-500 美元),微服务的成本为 500-5,000 美元/月。 团队还将 20-30% 的工程能力用于运营任务。
微服务迁移的扼杀者模式是什么?
扼杀者无花果模式一次从您的巨石中提取一项服务。 通过 API 网关路由流量,与单体一起构建新服务,并逐渐转移流量。 如果新服务失败,则将流量路由回来。 这避免了大爆炸重写,大爆炸重写会失败,因为重写永远赶不上 6-12 个月的新整体功能。
什么是模块化整体架构?我应该使用它吗?
模块化单体是具有严格内部模块边界的单个可部署单元。 每个模块都拥有自己的数据库表,并通过定义的内部 API 进行通信。 对于 5-30 名工程师的团队来说,它可以提供微服务的组织优势(清晰的所有权、强制边界),而无需每月支付 500-5,000 美元的基础设施成本。
转向微服务时我应该首先提取什么?
提取最常更改的组件(检查 git 日志中 6 个月内提交次数最多的目录)或需要独立缩放的组件(例如使用 API 的 10 倍 CPU 的视频处理模块)。 避免首先提取最复杂的组件。 您的第一次提取是一次学习练习。