微服务是一种初创公司可能负担不起的税
微服务是一种初创公司可能负担不起的税
2025年5月5日 • Oleg Pustovit
为什么过早地拆分你的代码库会悄悄地破坏你团队的速度——以及应该怎么做。
在初创公司中,你的生存取决于你迭代、发布功能和为最终用户交付价值的速度。 这就是你初创公司的基础架构发挥重要作用的地方;此外,诸如你的技术栈和编程语言的选择等因素直接影响你团队的速度。错误的架构,尤其是过早的微服务,会大大损害生产力,并导致在交付软件方面错过目标。
我在早期初创公司的全新项目上工作时有过这种经历,在软件架构方面做出了有问题的决策,导致半成品服务和_脆弱的、过度设计的和损坏的本地设置_,以及士气低落的团队,他们努力维护不必要的复杂性。
在深入研究具体的陷阱之前,这里是你过早引入微服务时实际要承担的:
早期采用微服务:你付出的代价
| 痛点 | 现实表现 | 开发者成本 |
| -------------------- | --------------------------------------------- | -------------------- |
| 部署复杂性 | 为单个功能编排 5+ 个服务 | 每次发布损失的时间 |
| 本地开发脆弱性 | Docker
蔓延,脚本损坏,特定于平台的 hack | 缓慢的入职,频繁的损坏 |
| CI/CD 重复性 | 具有重复逻辑的多个流水线 | 每个服务的额外苦工 |
| 跨服务耦合 | 通过共享状态紧密连接的“解耦”服务 | 更慢的更改,协调成本 |
| 可观察性开销 | 分布式追踪、日志记录、监控 | 数周才能正确配置 |
| 测试套件碎片化 | 测试分散在各个服务中 | 脆弱的测试,低信心 |
让我们来剖析一下为什么微服务通常会在早期适得其反,它们在哪些方面真正有帮助,以及如何构建你的初创公司的系统以实现速度和生存。
单体架构不是敌人
如果你正在构建一些 SaaS 产品,即使是一个简单的 SQL 数据库包装器,最终也可能在你业务逻辑的工作方式中带来很多内部复杂性;此外,你还可以进行各种集成和后台任务,从而将一组数据转换为另一组数据。
随着时间的推移,有时是不必要的功能,你的应用程序不可避免地会变得混乱。单体架构的优点是:它们仍然有效。 单体架构,即使是混乱的,也能让你的团队专注于最重要的事情:
- 生存
- 交付客户价值
单体架构的最大优点是其部署的简单性。通常,此类项目是围绕现有框架构建的——可能是 Python
的 Django
,C#
的 ASP.Net
,Node.js
应用的 Nest.js
等。当坚持单体架构时,你会获得优于花哨的微服务的最大优势——开源社区和项目维护者(他们主要设计这些框架作为单个进程、单体应用运行)的广泛支持。
在我领导前端团队并在技术选择方面偶尔咨询后端团队的一家房地产初创公司,我们有一个基于 Laravel
的应用程序的有趣演变。最初只是一个供房地产经纪人管理交易的小型仪表板,逐渐发展成一个更大的系统。
随着时间的推移,它演变成一个功能丰富的套件,处理数百 GB 的文档并与数十个第三方服务集成。然而,它仍然建立在运行在 Apache
上的相当基本的 PHP
堆栈上。
该团队大量借鉴了 Laravel
社区推荐的最佳实践。这种纪律得到了回报,我们能够在满足业务需求和期望的同时显着扩展应用程序的功能。
有趣的是,我们从未需要将系统解耦到微服务中或采用更复杂的基础设施模式。我们避免了很多意外的复杂性。架构的简单性给了我们杠杆作用。这与其他人所写的相呼应——比如 Basecamp
对 “Majestic Monolith” 的看法,它阐述了为什么简单性是早期的超能力。
人们经常指出,单体架构很难扩展,但正是单体架构_内部_的糟糕模块化可能会带来这些问题。
重点:结构良好的单体架构可以使你的团队专注于交付,而不是救火。
但微服务不是“最佳实践”吗?
很多工程师很早就开始使用微服务,认为它们是“正确的方式”。当然——在规模上,它们会有所帮助。但在初创公司中,同样的复杂性变成了阻力。
只有当你遇到真正的扩展瓶颈、大型团队或独立发展的领域时,微服务才会得到回报。在那之前呢?你是在付出代价而没有获得好处:重复的基础设施、脆弱的本地设置和缓慢的迭代。 例如,Segment
最终 撤销了他们的微服务拆分 正是因为这个原因——成本太高,价值不足。
重点:微服务是一种扩展工具——而不是起始模板。
微服务出错的地方(尤其是在早期)
在我建议的一个早期团队中,拆分服务的决定所带来的项目管理-工程协调开销超过了技术收益。架构不仅影响了代码,还影响了我们如何计划、估算和交付。这种组织税很容易被忽略——直到为时已晚。
**图:**协调开销随着服务的增加呈线性增长——当添加产品经理、截止日期和时间线不一致时,呈指数增长。
以下是早期出现的常见反模式。
1. 任意的服务边界
从理论上讲,你经常会看到关于按业务逻辑领域拆分应用程序的建议——用户服务、产品服务、订单服务等等。这通常借鉴了领域驱动设计或整洁架构的概念——这在规模上是有意义的,但在早期产品中,它们可能会过早地固化结构,而产品本身尚未稳定或经过验证。你最终会得到:
- 共享数据库
- 简单工作流程的跨服务调用
- 伪装成“分离”的耦合
在一个项目中,我看到一个团队将用户、身份验证和授权分离到单独的服务中,这导致了部署复杂性和在他们构建的任何 API 操作中服务协调的困难。
实际上,业务逻辑并不直接映射到服务边界。过早的分离会使系统更加脆弱,并且通常难以快速引入更改。
相反,基于实际的扩展痛点(而不是理论上的优雅性)来有选择地隔离瓶颈。
当我指导早期团队时,我们有时会使用内部标志或部署切换来模拟未来的服务拆分——而没有立即产生的运营负担。这为产品和工程提供了有机探索边界的空间,而无需锁定过早的基础架构。
重点:不要按理论拆分——按实际瓶颈拆分。
2. 仓库和基础设施蔓延
在处理应用程序时,通常下一组事情很重要:
- 代码风格一致性(linting)
- 测试基础设施,包括集成测试
- 本地环境配置
- 文档
- CI/CD 配置
在处理微服务时,你需要将这些要求乘以服务的数量。如果你的项目结构化为单体仓库,你可以通过拥有中央 CI/CD 配置来简化你的生活(当使用 GitHub Actions
或 GitLab CI
时)。有些团队将微服务分离到单独的仓库中,这使得在没有额外努力或工具的情况下维护代码一致性和相同的配置集变得更加困难。
对于一个三人团队来说,这是残酷的。跨仓库和工具的环境切换会增加每个交付功能的开发时间。
通过使用单体仓库和单一编程语言来缓解问题
有很多方法可以缓解这个问题。对于早期项目,最重要的一件事是——将你的代码保存在单体仓库中。这确保了在生产中存在单个版本的代码,并且对于较小的团队来说,协调代码审查和协作要容易得多。
对于 Node.js
项目,我强烈建议使用像 nx
或 turborepo
这样的单体仓库工具。两者:
- 简化跨子项目的 CI/CD 配置
- 支持基于依赖关系图的构建缓存
- 让你将内部服务视为
TypeScript
库(通过 ES6 导入)
这些工具可以节省原本用于编写粘合代码或重新发明编排的时间。也就是说,它们带来了真正的权衡:
- 复杂的依赖关系树会快速增长
- CI 性能调整并非易事
- 你可能需要更快的工具(如
bun
)来缩短构建时间
总结:如果你愿意投入精力来保持它们的清洁,像 nx
或 turborepo
这样的工具可以为小型团队提供单体仓库速度。
当开发基于 go
的微服务时,早期开发的一个好主意是使用单个 go
工作区,并在 go.mod
中使用 replace
指令。最终,随着软件的扩展,可以将 go
模块轻松地分离到单独的仓库中。
重点:具有共享基础设施的单体仓库可以为你争取时间、一致性和理智。
3. 损坏的本地开发 = 损坏的速度
如果运行你的应用程序需要三个小时、一个自定义的 shell 脚本和一个 Docker
马拉松,那么你已经失去了速度。
早期项目经常遇到以下问题:
- 缺少文档
- 过时的依赖项
- 特定于 OS 的 hack(你好,仅
Linux
设置)
根据我的经验,当我从过去的开发团队那里收到项目时,它们通常是为单个操作系统开发的。有些开发人员喜欢在 macOS
上构建,并且从不费心在 Windows
上支持他们的 shell 脚本。在我过去的团队中,我有工程师在 Windows
机器上工作,并且通常需要重写 shell 脚本或完全理解和逆向工程运行本地环境的过程。随着时间的推移,我们标准化了跨开发 OS 的环境设置,以减少入职摩擦——这是一项小小的投资,可以为每位新工程师节省数小时的时间。这令人沮丧——但它教会了我一个持久的教训,即让代码在新开发人员可能使用的任何笔记本电脑上运行是多么重要。
在另一个项目中,一位独立开发人员创建了一个脆弱的微服务设置,该设置的工作流程是将 Docker
容器挂载到本地文件系统。当然,当你的计算机运行 Linux
时,你会为将进程作为容器运行付出一点代价。
但是,让一位拥有旧 Windows
笔记本电脑的新前端开发人员入职变成了一场噩梦。他们必须启动十个容器才能查看 UI。所有内容都中断了——卷、网络、容器兼容性——并且设置记录非常糟糕。这在入职期间造成了主要的摩擦点。
我们最终拼凑了一个 Node.js
代理,该代理模拟了没有容器的 nginx
/Docker
配置。它并不优雅,但它让开发人员能够解除阻塞并开始贡献。
重点:如果你的应用程序仅在一个 OS 上运行,那么你团队的生产力就离灾难只有一台笔记本电脑的距离。
提示: 理想情况下,目标是 git clone <repo> && make up
以使项目在本地运行。如果不可能,则必须维护包含 Windows
/macOS
/Linux
说明的最新 README 文件。如今,有些编程语言和工具链在 Windows
上运行不佳(如 OCaml
),但是现代广泛使用的堆栈在每个广泛使用的操作系统上都运行良好;将你的本地设置限制为单个操作系统通常是 DX 投资不足的症状。
4. 技术不匹配
除了架构之外,你的技术栈还会影响微服务变得多么痛苦——并非每种语言都适合微服务架构。
Node.js
和Python
: 非常适合快速迭代,但是管理跨服务的构建工件、依赖项版本和运行时一致性会很快变得痛苦。Go
: 编译为静态二进制文件,构建时间快,运营开销低。在真正需要拆分时,更自然地适合。
尽早选择正确的技术栈非常重要。如果你追求性能,可以考虑 JVM
及其生态系统,以及大规模部署工件并在基于微服务的架构中运行它们的能力。如果你进行非常快速的迭代并快速构建原型,而不必担心扩展你的部署基础架构——那么你可以使用 Python
之类的东西。
团队经常意识到他们的技术选择存在最初并不明显的重大问题,并且他们不得不付出代价以不同的编程语言重建后端(就像 那些家伙 不得不采取措施解决遗留的 Python 2
代码库并迁移到 Go
)。
但另一方面,如果你真的需要,你可以使用诸如 gRPC
或异步消息通信之类的协议来桥接多种编程语言。而且这通常是解决问题的方法。当你想要使用机器学习功能或基于 ETL 的作业来丰富你的功能集时,你只需单独构建你的基于 Python
的 ML 基础设施,因为它具有丰富的特定于领域的库生态系统,这是任何其他编程语言自然缺乏的。但是,只有当有足够的人力来证明这种风险是合理的时,才应做出此类决定;否则,小团队将永远陷入将多个软件堆栈桥接在一起的无尽复杂性中。
重点:将技术与你的约束相匹配,而不是你的雄心。
5. 隐藏的复杂性:通信和监控
微服务引入了一个不可见的需要网络:
- 服务发现
- API 版本控制
- 重试、断路器、回退
- 分布式跟踪
- 集中式日志记录和警报
在单体架构中,错误可能是一个简单的堆栈跟踪。在分布式系统中,它是“当 B 的部署落后 C 30 秒时,为什么服务 A 失败?” 你必须彻底投资于你的可观察性堆栈。要“正确”地做到这一点,需要以特定的方式检测你的应用程序,例如集成 OpenTelemetry
以支持跟踪,或者如果你使用复杂的无服务器系统,则依赖你的云提供商的工具(如 AWS XRay
)。但是在这一点上,你必须将你的注意力从应用程序代码完全转移到构建复杂的监控基础设施上,以验证你的架构实际上是否在生产中运行。
当然,需要在单体应用程序上执行一些可观察性检测,但是与以一致的方式在服务数量方面执行检测相比,这要简单得多。
提示: 要了解 分布式系统_不是免费的。_ 它们是对一整类新的工程挑战的承诺。
微服务_确实_有意义的情况
尽管微服务存在上述困难,但在某些时候,服务级别的解耦实际上非常有利。在某些情况下,它肯定会有所帮助:
- 工作负载隔离: 一个常见的例子是 AWS 上关于使用
S3
事件通知的最佳实践——当图像加载到S3
时,触发图像大小调整/OCR 过程等。它为什么有用:我们可以将模糊的数据处理库解耦到自我隔离的服务中,并使其 API 仅专注于图像处理和从上传的数据生成输出。将数据上传到S3
的上游客户端未与此服务耦合,并且由于其相对简单性,检测此类服务的开销较小。 - 不同的可扩展性需求: — 想象一下你正在构建一个 AI 产品。系统的一部分(Web API)触发 ML 工作负载并显示过去的结果,它不是资源密集型的,它是轻量级的,因为它主要与数据库交互。相反,
ML
模型在GPU
上运行实际上运行起来很繁重,并且需要具有GPU
支持和额外配置的特殊机器。通过将应用程序的这些部分拆分为在不同机器上运行的单独服务,你可以独立扩展它们。 - 不同的运行时需求: — 假设你有一些用
C++
编写的遗留代码。你有 2 个选择——神奇地将其转换为你的核心编程语言,或者找到将其与代码库集成的方法。根据该遗留应用程序的复杂性,你将不得不编写粘合代码,实施额外的网络/协议以建立与该服务的交互,但最重要的是——由于运行时不兼容性,你可能必须将该应用程序作为单独的服务分离出来。我想说的是,你甚至可以用C++
编写你的主应用程序,但是由于不同的编译器配置和库依赖关系,你将无法轻松地将事物编译成单个二进制文件。
大型工程组织已经解决了类似的挑战。例如,Uber
的工程团队 记录了他们向面向领域的微服务架构的转变 ——不是出于理论上的纯粹性,而是为了响应跨团队和扩展边界的真实复杂性。他们的帖子很好地说明了当你拥有组织成熟度和运营开销来支持它们时,微服务如何工作。
在一个项目中,恰好也是房地产项目,我们有来自之前团队的代码,该代码运行基于 Python
的分析工作负载,并将数据加载到 MS-SQL
数据库中,我们发现在此基础上构建 Django
应用程序是一种浪费。该代码具有不同的运行时依赖关系并且相当自我隔离,因此我们将其分开,仅在某些东西无法按预期工作时才重新审视它。即使对于一个小团队来说,这对我们来说也适用,因为此分析生成服务是需要很少更改或维护的一部分。
重点:在工作负载不同时使用微服务——而不仅仅是因为它们听起来很干净。
初创公司的实用指南
如果你要交付你的第一个产品,这是我推荐的剧本:
- 从单体架构开始。 选择一个通用框架并专注于完成这些功能。所有已知的框架都足以构建一些 API 或网站并为用户提供服务。不要追随炒作,坚持采用无聊的方式做事;你以后会感谢自己的。
- 单一仓库。 不要费心将你的代码拆分为多个仓库。我曾与想要分离仓库的创始人合作过,以降低承包商复制 IP 的风险——这是一个有效的担忧。但在实践中,它增加的摩擦多于安全性:更慢的构建、分散的 CI/CD 和跨团队的糟糕可见性。边际 IP 保护不值得运营阻力,尤其是在单体仓库内部的正确访问控制更容易管理时。对于早期团队来说,清晰度和速度往往比理论上的安全收益更重要。
- 极其简单的本地设置。 让
make up
工作。如果需要更多步骤,请非常具体地说明步骤,录制视频/Loom,并添加屏幕截图。如果你的代码将由实习生或初级开发人员运行,他们可能会遇到障碍,并且你将花费时间解释如何排除问题。我发现记录每个操作系统上每个可能的问题可以消除花在澄清本地设置中某些事物为何不起作用的时间。 - 尽早投资 CI/CD。 即使它只是你可以手动
scp
到服务器的简单 HTML,你也可以自动化它并依靠具有 CI/CD 的源代码控制来做到这一点。当设置正确自动化时,你只需忘记你的持续集成基础设施并专注于功能。我见过许多团队和创始人在与外包团队合作时经常在 CI/CD 上很吝啬,这导致团队士气低落,并且手动部署流程令人恼火。 - 有选择地拆分。 仅在明确解决痛苦的瓶颈时才拆分。否则,请投资于单体架构内部的模块化和测试——它更快且更易于维护。
最重要的是:优化开发人员速度。
速度是你初创公司的氧气。 过早的微服务会缓慢地泄漏氧气——直到有一天,你无法呼吸。
重点:从简单开始,保持务实,并且仅在必要时才拆分。
如果你采用基于微服务的方法
我创建过一些早于应有的基于微服务的项目,以下是我可以提供的下一个建议:
- 评估你的技术栈,该技术栈为你的基于微服务的架构提供支持。投资于开发人员体验工具。当你进行基于服务的隔离时,现在需要考虑自动化你的微服务堆栈,自动化跨本地和生产环境的配置。在某些项目中,我不得不构建一个单独的 CLI,该 CLI 在单体仓库上执行管理任务。我曾经有一个项目包含 15-20 个微服务部署,并且对于本地环境,我不得不创建一个 cli 工具,用于动态生成
docker-compose.yml
文件,以便为普通开发人员实现无缝的一键启动。 - 专注于围绕服务通信的可靠通信协议。 如果它是异步消息传递,请确保你的消息模式是一致且标准化的。如果它是 REST,请专注于
OpenAPI
文档。服务间通信客户端必须实现许多非开箱即用的功能:具有指数退避的重试、超时。一个典型的简单gRPC
客户端要求你手动考虑这些额外的因素,以确保你不会遭受瞬时错误。 - 确保你的单元测试、集成测试和端到端测试设置 是稳定的,并且可以随着你引入代码库的服务级别分离的数量进行扩展。
- 在使用基于微服务的工作负载的较小项目中,你可能会默认使用共享库,其中包含用于以一致的方式检测你的可观察性、通信代码的通用助手。这里的一个重要考虑因素——尽可能保持你的共享库小。 任何重大更改都会强制重建所有依赖服务——即使是不相关的。
- 尽早了解可观察性。 添加结构化的
JSON
日志,并为调试应用程序部署后出现的问题创建各种关联 ID。即使是输出丰富日志信息的基本助手(直到你使用适当的日志记录/跟踪工具检测你的应用程序)通常也可以节省时间来找出不稳定的用户流程。
总而言之:如果你仍然选择微服务,则应事先了解你将为额外的开发时间和维护付出代价,以使该设置对你的团队中的每位工程师都可行。
重点:如果你接受复杂性,请充分投资以使其可管理。
结论
过早的微服务是你负担不起的税。保持简单。保持生存。 仅当痛苦使它变得明显时才拆分。
先生存。稍后扩展。选择有效的最简单的系统——并赢得你添加的每一层复杂性。
相关资源
- Monolith First — Martin Fowler
- The Majestic Monolith — DHH / Basecamp
- Goodbye Microservices: From 100s of problem children to 1 superstar — Segment Eng.
- Deconstructing the Monolith — Shopify Eng.
- Domain‑Oriented Microservice Architecture — Uber Eng.
- Go + Services = One Goliath Project — Khan Academy
#microservices #monolith #architecture #node.js #go
加入新闻通讯
获取最新的技术深度分析和创业见解。
订阅
上一个 在 300 行 Go
代码中构建你自己的 HTTPS 隧道
返回顶部
版权所有 © Oleg Pustovit 2025 | 隐私政策
07:20 PM GMT+3
关注我