微服务是一种初创公司可能负担不起的税

2025年5月5日 • Oleg Pustovit

为什么过早地拆分你的代码库会悄悄地破坏你团队的速度——以及应该怎么做。

microservice vs monolith

在初创公司中,你的生存取决于你迭代、发布功能和为最终用户交付价值的速度。 这就是你初创公司的基础架构发挥重要作用的地方;此外,诸如你的技术栈和编程语言的选择等因素直接影响你团队的速度。错误的架构,尤其是过早的微服务,会大大损害生产力,并导致在交付软件方面错过目标。

我在早期初创公司的全新项目上工作时有过这种经历,在软件架构方面做出了有问题的决策,导致半成品服务和_脆弱的、过度设计的和损坏的本地设置_,以及士气低落的团队,他们努力维护不必要的复杂性。

在深入研究具体的陷阱之前,这里是你过早引入微服务时实际要承担的:

早期采用微服务:你付出的代价

| 痛点 | 现实表现 | 开发者成本 | | -------------------- | --------------------------------------------- | -------------------- | | 部署复杂性 | 为单个功能编排 5+ 个服务 | 每次发布损失的时间 | | 本地开发脆弱性 | Docker 蔓延,脚本损坏,特定于平台的 hack | 缓慢的入职,频繁的损坏 | | CI/CD 重复性 | 具有重复逻辑的多个流水线 | 每个服务的额外苦工 | | 跨服务耦合 | 通过共享状态紧密连接的“解耦”服务 | 更慢的更改,协调成本 | | 可观察性开销 | 分布式追踪、日志记录、监控 | 数周才能正确配置 | | 测试套件碎片化 | 测试分散在各个服务中 | 脆弱的测试,低信心 |

让我们来剖析一下为什么微服务通常会在早期适得其反,它们在哪些方面真正有帮助,以及如何构建你的初创公司的系统以实现速度和生存。

单体架构不是敌人

Monolith architecture

如果你正在构建一些 SaaS 产品,即使是一个简单的 SQL 数据库包装器,最终也可能在你业务逻辑的工作方式中带来很多内部复杂性;此外,你还可以进行各种集成和后台任务,从而将一组数据转换为另一组数据。

随着时间的推移,有时是不必要的功能,你的应用程序不可避免地会变得混乱。单体架构的优点是:它们仍然有效。 单体架构,即使是混乱的,也能让你的团队专注于最重要的事情

单体架构的最大优点是其部署的简单性。通常,此类项目是围绕现有框架构建的——可能是 PythonDjangoC#ASP.NetNode.js 应用的 Nest.js 等。当坚持单体架构时,你会获得优于花哨的微服务的最大优势——开源社区和项目维护者(他们主要设计这些框架作为单个进程、单体应用运行)的广泛支持。

在我领导前端团队并在技术选择方面偶尔咨询后端团队的一家房地产初创公司,我们有一个基于 Laravel 的应用程序的有趣演变。最初只是一个供房地产经纪人管理交易的小型仪表板,逐渐发展成一个更大的系统。

随着时间的推移,它演变成一个功能丰富的套件,处理数百 GB 的文档并与数十个第三方服务集成。然而,它仍然建立在运行在 Apache 上的相当基本的 PHP 堆栈上。

该团队大量借鉴了 Laravel 社区推荐的最佳实践。这种纪律得到了回报,我们能够在满足业务需求和期望的同时显着扩展应用程序的功能。

有趣的是,我们从未需要将系统解耦到微服务中或采用更复杂的基础设施模式。我们避免了很多意外的复杂性。架构的简单性给了我们杠杆作用。这与其他人所写的相呼应——比如 Basecamp“Majestic Monolith” 的看法,它阐述了为什么简单性是早期的超能力。

人们经常指出,单体架构很难扩展,但正是单体架构_内部_的糟糕模块化可能会带来这些问题。

重点:结构良好的单体架构可以使你的团队专注于交付,而不是救火。

但微服务不是“最佳实践”吗?

很多工程师很早就开始使用微服务,认为它们是“正确的方式”。当然——在规模上,它们会有所帮助。但在初创公司中,同样的复杂性变成了阻力。

只有当你遇到真正的扩展瓶颈、大型团队或独立发展的领域时,微服务才会得到回报。在那之前呢?你是在付出代价而没有获得好处:重复的基础设施、脆弱的本地设置和缓慢的迭代。 例如,Segment 最终 撤销了他们的微服务拆分 正是因为这个原因——成本太高,价值不足。

重点:微服务是一种扩展工具——而不是起始模板。

微服务出错的地方(尤其是在早期)

在我建议的一个早期团队中,拆分服务的决定所带来的项目管理-工程协调开销超过了技术收益。架构不仅影响了代码,还影响了我们如何计划、估算和交付。这种组织税很容易被忽略——直到为时已晚。

Coordination between teams

**图:**协调开销随着服务的增加呈线性增长——当添加产品经理、截止日期和时间线不一致时,呈指数增长。

以下是早期出现的常见反模式。

1. 任意的服务边界

Arbitrary service boundaries in microservices

从理论上讲,你经常会看到关于按业务逻辑领域拆分应用程序的建议——用户服务、产品服务、订单服务等等。这通常借鉴了领域驱动设计或整洁架构的概念——这在规模上是有意义的,但在早期产品中,它们可能会过早地固化结构,而产品本身尚未稳定或经过验证。你最终会得到:

在一个项目中,我看到一个团队将用户、身份验证和授权分离到单独的服务中,这导致了部署复杂性和在他们构建的任何 API 操作中服务协调的困难。

实际上,业务逻辑并不直接映射到服务边界。过早的分离会使系统更加脆弱,并且通常难以快速引入更改。

相反,基于实际的扩展痛点(而不是理论上的优雅性)来有选择地隔离瓶颈。

当我指导早期团队时,我们有时会使用内部标志或部署切换来模拟未来的服务拆分——而没有立即产生的运营负担。这为产品和工程提供了有机探索边界的空间,而无需锁定过早的基础架构。

重点:不要按理论拆分——按实际瓶颈拆分。

2. 仓库和基础设施蔓延

在处理应用程序时,通常下一组事情很重要:

在处理微服务时,你需要将这些要求乘以服务的数量。如果你的项目结构化为单体仓库,你可以通过拥有中央 CI/CD 配置来简化你的生活(当使用 GitHub ActionsGitLab CI 时)。有些团队将微服务分离到单独的仓库中,这使得在没有额外努力或工具的情况下维护代码一致性和相同的配置集变得更加困难。

tests environments diagram

对于一个三人团队来说,这是残酷的。跨仓库和工具的环境切换会增加每个交付功能的开发时间。

通过使用单体仓库和单一编程语言来缓解问题

有很多方法可以缓解这个问题。对于早期项目,最重要的一件事是——将你的代码保存在单体仓库中。这确保了在生产中存在单个版本的代码,并且对于较小的团队来说,协调代码审查和协作要容易得多。

对于 Node.js 项目,我强烈建议使用像 nxturborepo 这样的单体仓库工具。两者:

这些工具可以节省原本用于编写粘合代码或重新发明编排的时间。也就是说,它们带来了真正的权衡:

总结:如果你愿意投入精力来保持它们的清洁,像 nxturborepo 这样的工具可以为小型团队提供单体仓库速度。

当开发基于 go 的微服务时,早期开发的一个好主意是使用单个 go 工作区,并在 go.mod 中使用 replace 指令。最终,随着软件的扩展,可以将 go 模块轻松地分离到单独的仓库中。

重点:具有共享基础设施的单体仓库可以为你争取时间、一致性和理智。

3. 损坏的本地开发 = 损坏的速度

如果运行你的应用程序需要三个小时、一个自定义的 shell 脚本和一个 Docker 马拉松,那么你已经失去了速度。

早期项目经常遇到以下问题:

根据我的经验,当我从过去的开发团队那里收到项目时,它们通常是为单个操作系统开发的。有些开发人员喜欢在 macOS 上构建,并且从不费心在 Windows 上支持他们的 shell 脚本。在我过去的团队中,我有工程师在 Windows 机器上工作,并且通常需要重写 shell 脚本或完全理解和逆向工程运行本地环境的过程。随着时间的推移,我们标准化了跨开发 OS 的环境设置,以减少入职摩擦——这是一项小小的投资,可以为每位新工程师节省数小时的时间。这令人沮丧——但它教会了我一个持久的教训,即让代码在新开发人员可能使用的任何笔记本电脑上运行是多么重要。

在另一个项目中,一位独立开发人员创建了一个脆弱的微服务设置,该设置的工作流程是将 Docker 容器挂载到本地文件系统。当然,当你的计算机运行 Linux 时,你会为将进程作为容器运行付出一点代价。

但是,让一位拥有旧 Windows 笔记本电脑的新前端开发人员入职变成了一场噩梦。他们必须启动十个容器才能查看 UI。所有内容都中断了——卷、网络、容器兼容性——并且设置记录非常糟糕。这在入职期间造成了主要的摩擦点。

我们最终拼凑了一个 Node.js 代理,该代理模拟了没有容器的 nginx/Docker 配置。它并不优雅,但它让开发人员能够解除阻塞并开始贡献。

complex dev environment in microservices

重点:如果你的应用程序仅在一个 OS 上运行,那么你团队的生产力就离灾难只有一台笔记本电脑的距离。

提示: 理想情况下,目标是 git clone <repo> && make up 以使项目在本地运行。如果不可能,则必须维护包含 Windows/macOS/Linux 说明的最新 README 文件。如今,有些编程语言和工具链在 Windows 上运行不佳(如 OCaml),但是现代广泛使用的堆栈在每个广泛使用的操作系统上都运行良好;将你的本地设置限制为单个操作系统通常是 DX 投资不足的症状。

4. 技术不匹配

除了架构之外,你的技术栈还会影响微服务变得多么痛苦——并非每种语言都适合微服务架构。

尽早选择正确的技术栈非常重要。如果你追求性能,可以考虑 JVM 及其生态系统,以及大规模部署工件并在基于微服务的架构中运行它们的能力。如果你进行非常快速的迭代并快速构建原型,而不必担心扩展你的部署基础架构——那么你可以使用 Python 之类的东西。

团队经常意识到他们的技术选择存在最初并不明显的重大问题,并且他们不得不付出代价以不同的编程语言重建后端(就像 那些家伙 不得不采取措施解决遗留的 Python 2 代码库并迁移到 Go)。

但另一方面,如果你真的需要,你可以使用诸如 gRPC 或异步消息通信之类的协议来桥接多种编程语言。而且这通常是解决问题的方法。当你想要使用机器学习功能或基于 ETL 的作业来丰富你的功能集时,你只需单独构建你的基于 Python 的 ML 基础设施,因为它具有丰富的特定于领域的库生态系统,这是任何其他编程语言自然缺乏的。但是,只有当有足够的人力来证明这种风险是合理的时,才应做出此类决定;否则,小团队将永远陷入将多个软件堆栈桥接在一起的无尽复杂性中。

重点:将技术与你的约束相匹配,而不是你的雄心。

5. 隐藏的复杂性:通信和监控

微服务引入了一个不可见的需要网络:

在单体架构中,错误可能是一个简单的堆栈跟踪。在分布式系统中,它是“当 B 的部署落后 C 30 秒时,为什么服务 A 失败?” 你必须彻底投资于你的可观察性堆栈。要“正确”地做到这一点,需要以特定的方式检测你的应用程序,例如集成 OpenTelemetry 以支持跟踪,或者如果你使用复杂的无服务器系统,则依赖你的云提供商的工具(如 AWS XRay)。但是在这一点上,你必须将你的注意力从应用程序代码完全转移到构建复杂的监控基础设施上,以验证你的架构实际上是否在生产中运行。

当然,需要在单体应用程序上执行一些可观察性检测,但是与以一致的方式在服务数量方面执行检测相比,这要简单得多。

提示: 要了解 分布式系统_不是免费的。_ 它们是对一整类新的工程挑战的承诺。

微服务_确实_有意义的情况

尽管微服务存在上述困难,但在某些时候,服务级别的解耦实际上非常有利。在某些情况下,它肯定会有所帮助:

大型工程组织已经解决了类似的挑战。例如,Uber 的工程团队 记录了他们向面向领域的微服务架构的转变 ——不是出于理论上的纯粹性,而是为了响应跨团队和扩展边界的真实复杂性。他们的帖子很好地说明了当你拥有组织成熟度和运营开销来支持它们时,微服务如何工作。

在一个项目中,恰好也是房地产项目,我们有来自之前团队的代码,该代码运行基于 Python 的分析工作负载,并将数据加载到 MS-SQL 数据库中,我们发现在此基础上构建 Django 应用程序是一种浪费。该代码具有不同的运行时依赖关系并且相当自我隔离,因此我们将其分开,仅在某些东西无法按预期工作时才重新审视它。即使对于一个小团队来说,这对我们来说也适用,因为此分析生成服务是需要很少更改或维护的一部分。

重点:在工作负载不同时使用微服务——而不仅仅是因为它们听起来很干净。

初创公司的实用指南

如果你要交付你的第一个产品,这是我推荐的剧本:

最重要的是:优化开发人员速度。

速度是你初创公司的氧气。 过早的微服务会缓慢地泄漏氧气——直到有一天,你无法呼吸。

重点:从简单开始,保持务实,并且仅在必要时才拆分。

如果你采用基于微服务的方法

我创建过一些早于应有的基于微服务的项目,以下是我可以提供的下一个建议:

总而言之:如果你仍然选择微服务,则应事先了解你将为额外的开发时间和维护付出代价,以使该设置对你的团队中的每位工程师都可行。

重点:如果你接受复杂性,请充分投资以使其可管理。

结论

过早的微服务是你负担不起的税。保持简单。保持生存。 仅当痛苦使它变得明显时才拆分。

先生存。稍后扩展。选择有效的最简单的系统——并赢得你添加的每一层复杂性。

相关资源

#microservices #monolith #architecture #node.js #go

加入新闻通讯

获取最新的技术深度分析和创业见解。 订阅 上一个 在 300 行 Go 代码中构建你自己的 HTTPS 隧道 返回顶部 版权所有 © Oleg Pustovit 2025 | 隐私政策 07:20 PM GMT+3 关注我