48个微服务:为什么要拆这么细?
当我们告诉新同事"我们的系统有40多个微服务"时,他们的表情通常是这样的:😳
"拆这么细?有必要吗?"
今天就来聊聊,为什么我们走上了这条路,以及这一路上的血泪教训。
一切从"大泥球"说起
还记得单体应用的日子吗?一个代码仓库,几百个模块,部署一次半小时。
修改一个功能,可能影响十个模块;发布一个版本,需要协调三个团队。
[配图建议:一个巨大的、缠绕的毛线球,象征单体架构的复杂性]
单体架构的问题,本质上是耦合度的问题:
- 代码耦合:改一处,动全身
- 团队耦合:你等我,我等你
- 部署耦合:一荣俱荣,一损俱损
- 扩展耦合:为了一个模块的高负载,整个系统都要扩容
当业务简单时,单体架构没问题。
但当业务复杂到一定程度,团队规模超过一定阈值,单体就会变成"大泥球"——没人能完全理解它,没人敢轻易改动它。
一个真实的故事
2019年,我们的系统还是一个单体应用。有一次,一个新同事改了一行代码,影响了用户登录模块。测试环境没问题,上线后才发现:这行代码导致VIP用户的折扣计算出错。
结果呢?回滚、道歉、赔偿优惠券,折腾了一整天。
这就是"大泥球"的典型症状——模块之间的边界模糊,牵一发而动全身。
微服务:拆了真的好吗?
微服务的核心思想很简单:把大系统拆成小服务,每个服务专注一件事。
听起来很美好,但收益到底在哪?
🎯 收益一:独立部署
服务A的更新,不影响服务B的运行。
发布频率可以从"一个月一次"提升到"一天多次"。
团队可以自主决定发布时机,不再需要协调所有相关方。
🎯 收益二:技术选型自由
服务A用Java,服务B用Go,服务C用Node.js。
每个团队可以选择最适合自己场景的技术栈。
不用再为"统一技术栈"而妥协。
🎯 收益三:独立扩展
只有用户服务负载高?那就只扩容用户服务。
不用为整个系统买单,资源利用率更高。
🎯 收益四:故障隔离
服务A挂了,服务B还能正常运行。
问题不会像病毒一样蔓延到整个系统。
[配图建议:一边是倒下的多米诺骨牌(单体),一边是独立的积木块(微服务),其中一个倒了不影响其他]
微服务拆分的设计原则
拆分不是乱砍一气,而是要遵循一定的设计原则。这些原则决定了你拆出来的服务是"优雅的微服务"还是"混乱的分布式单体"。
原则一:单一职责原则(SRP)
这听起来简单,但实践中很容易跑偏。
- 用户中心:只管用户基本信息、登录认证
- 积分服务:只管积分增减、积分规则
- 等级服务:只管等级计算、等级权益
每个服务都有独立的团队、独立的数据库、独立的发布周期。
原则二:高内聚低耦合
原则三:围绕业务能力拆分
这是领域驱动设计(DDD)的核心思想:服务的边界要和业务的边界一致。
- 识别业务领域:和业务方聊,理解他们的语言。他们说的"订单"、"库存"、"用户"就是天然的边界。
- 定义限界上下文(Bounded Context):同一个词在不同上下文中可能有不同含义。比如"用户"在营销上下文中是"潜在客户",在交易上下文中是"买家"。
- 一个限界上下文 = 一个服务:不要强行合并不同的上下文,也不要把同一个上下文拆到不同服务。
- 游戏服务中,角色有等级、装备、技能
- 充值服务中,角色只有ID和充值记录
如果强行用同一个"角色服务",就会导致游戏逻辑影响充值逻辑,反之亦然。所以它们是两个独立的服务。
原则四:数据所有权清晰
这条原则非常重要,但经常被忽视。
原则五:渐进式拆分
微服务拆分是一个渐进的过程,不是一次性的项目。
- 先拆边界最清晰的:比如支付、短信、推送,这些和其他业务耦合最少。
- 再拆变化最频繁的:经常改动的模块,拆出来后可以独立发布。
- 最后拆最复杂的:复杂的模块拆分风险大,有了前面的经验再动手。
服务边界划分方法
知道了原则,具体怎么划分边界呢?这里介绍几种实用的方法。
方法一:事件风暴(Event Storming)
这是DDD社区最流行的方法,非常适合复杂业务领域的边界划分。
- 准备便利贴:不同颜色代表不同元素:
- 蓝色 = 命令(比如"创建订单"、"处理支付") - 黄色 = 聚合/实体(比如"订单"、"用户") - 绿色 = 策略/规则(比如"VIP用户享受9折")
- 组织工作坊:把业务方、产品、开发都叫来,一起贴便利贴。
- 按时间线排列事件:从左到右,按照业务流程的时间顺序排列。
- 识别边界:找到事件之间的"边界",通常是一组相关的事件和命令。
- 用户边界:用户登录 → 用户信息获取
- 商品边界:商品查询 → 库存检查
- 价格边界:价格计算 → 优惠计算 → 最终定价
- 订单边界:订单创建 → 订单支付 → 订单完成
每个边界后来都成了一个独立的服务。
方法二:CRUD矩阵法
这是一种更简单直接的方法,适合快速识别数据边界。
- 列出所有数据实体(用户、订单、商品、库存……)
- 列出所有功能模块
- 画出矩阵,标记每个模块对每个实体的操作(C=创建,R=读取,U=更新,D=删除)
- 分析矩阵:
- 如果多个模块都写同一个实体,说明可能有边界问题
方法三:依赖分析
通过分析代码依赖,发现潜在的边界问题。
- 使用工具(如Dependency-Check、ArchUnit)分析代码依赖
- 画出依赖图
- 识别问题:
- 过度依赖:一个模块被太多模块依赖 - 跨层依赖:绕过中间层直接访问底层
方法四:团队对齐
有时候边界划分不是技术问题,而是组织问题。
- 让团队自己决定边界:他们最了解自己的业务
- 一个团队负责的服务数量要合理:太少会无聊,太多会失控
- 服务边界要和团队边界对齐:跨团队的服务是灾难
拆多细合适?这是个玄学问题
拆分不是越细越好。
⚠️ 过细的代价
想象一下,一个简单的"用户下单"流程,需要调用:
- 用户服务(验证登录)
- 商品服务(查询库存)
- 价格服务(计算优惠)
- 订单服务(创建订单)
- 支付服务(处理付款)
- 通知服务(发送短信)
如果每个服务都拆得更细呢?
比如"价格服务"再拆成"基础定价"、"会员折扣"、"活动优惠"、"优惠券计算"……
一个流程调用十几个服务,任何一个环节出问题,整个流程就挂了。
🎯 如何找到平衡点?
几个经验法则:
不要按"技术功能"拆(比如"数据库服务"、"缓存服务"),要按"业务能力"拆(比如"订单管理"、"库存管理")。
如果一个服务会被其他服务频繁调用,把它拆得太细会增加网络开销。
如果两个功能总是需要同时访问同一份数据,强行拆开会带来分布式事务的噩梦。
一开始不要拆太细,等问题暴露了再细化。
一个服务的复杂度,应该和一个5-8人的团队能掌控的范围匹配。
如果一个服务需要20个人维护,说明太大了;如果一个服务只需要1个人维护,说明太小了。
服务粒度的量化指标
除了经验法则,我们还有一些量化指标来判断服务粒度是否合理:
| 指标 | 太粗 | 合适 | 太细 |
|---|---|---|---|
| 代码行数 | >10万行 | 1-5万行 | <5000行 |
| 数据库表数 | >50张 | 10-20张 | <3张 |
| API数量 | >100个 | 20-50个 | <5个 |
| 团队人数 | >15人 | 5-8人 | 1-2人 |
| 发布频率 | <1次/月 | 1-5次/周 | >10次/天 |
| 依赖服务数 | >20个 | 5-10个 | 0-2个 |
注意:这些只是参考值,具体情况要具体分析。
我们的实践:按领域划分的40+服务
回到开头的问题:为什么是40多个?
因为我们是按领域划分的。
🗂️ 领域划分
我们的系统主要分为三大块:
- 用户相关:用户中心、认证服务、权限服务、黑名单服务、用户画像、实名认证、第三方登录
- 支付相关:支付网关、退款服务、对账服务、钱包服务
- 运营相关:活动中心、优惠券服务、礼包服务、抽奖服务、任务系统、积分服务
- 游戏相关:游戏中心、区服管理、角色服务、道具服务
- 基础服务:公告服务、推送服务
- 归因服务、点击追踪、展示追踪、转化追踪
- 渠道管理、素材管理、报表服务、反作弊服务、数据同步
- 网关服务、配置中心、监控服务、日志采集
[配图建议:一个三栏布局,分别展示三大块服务,每块标注服务数量]
🎨 服务分层
每个领域内部,服务还分了三层:
这种分层让每个服务的职责更清晰,也方便了横向扩展。
服务拆分时间线
微服务治理:拆完不是结束,是开始
很多团队以为服务拆完就大功告成了,其实拆分只是第一步,真正的挑战在于治理。
微服务治理包含很多方面,这里重点讲几个最重要的。
1. 服务注册与发现
我们使用腾讯云 TKE(Tencent Kubernetes Engine),K8s 自带的服务注册和发现功能完全够用。
- 服务启动时,K8s 自动将其注册到 API Server
- Service 资源创建 DNS 记录,其他服务通过服务名访问
- K8s 通过 kube-proxy 做负载均衡,将请求分发到健康的 Pod
- Pod 副本增减时,服务发现自动更新
- 健康检查:K8s 定期检查 Pod 健康状态,不健康自动剔除
- 服务类型:使用 ClusterIP 实现集群内访问
- 就绪探针:Pod 启动后才加入 Service,避免启动期流量
我们的业务复杂度还不够高,K8s 原生能力已经满足需求。多引入一个组件,就多一份运维成本。
2. 配置管理
我们没有使用开源方案(如 Apollo/Nacos),而是基于业务需求自研了配置中心,原因是:
- 更灵活的配置管理界面
- 更好的权限控制和审计
- 与业务系统深度集成
全局配置(所有服务共享)
└── 环境配置(dev/test/prod)
└── 服务配置(服务特有的配置)
└── 实例配置(特定实例的配置)
3. API网关
我们选择了自研而不是用 Spring Cloud Gateway,原因是:
- 更灵活的路由策略
- 更好的性能优化空间
- 与业务深度定制
- 路由:根据URL把请求转发到对应的服务
- 认证:统一处理登录验证,后端服务不需要关心
- 限流:保护后端服务不被打垮(这是我们的重点)
- 日志:统一记录访问日志,方便排查问题
- 协议转换:HTTP/1.1 ↔ HTTP/2
我们在网关层实现了多维度限流:
| 限流维度 | 策略 | 目的 |
|---|---|---|
| IP | 令牌桶算法 | 防止单个IP恶意请求 |
| 用户 | 滑动窗口 | 保护用户账户安全 |
| API | 固定窗口 | 保护后端服务 |
| 全局 | 漏桶算法 | 保护整个系统 |
4. 服务间通信
- 适用于需要立即返回结果的场景
- 比如:查询用户信息、创建订单
- HTTP/2 的多路复用提升了性能
- 适用于不需要立即返回的场景
- 比如:发送通知、更新统计数据、触发后续流程
- Redis 的 LIST 或 Stream 实现简单队列
我们的业务复杂度还不够高,RocketMQ/Kafka 这样的重量级消息队列有点杀鸡用牛刀。Redis 队列已经能满足需求。
| 场景 | 实现方式 | 示例 |
|---|---|---|
| 业务解耦 | Redis LIST | 订单创建后发送通知 |
| 削峰填谷 | Redis Stream | 大促时的订单处理 |
| 事件通知 | Redis Pub/Sub | 用户注册后触发营销流程 |
5. 服务容错
- 网关限流:防止突发流量打垮服务
- 健康检查:K8s 自动剔除不健康实例
- 超时控制:每个服务调用都有超时时间
- 重试机制:有限次重试,避免重试风暴
目前的服务规模和业务复杂度,还不需要这么复杂的容错机制。网关的限流已经能应对大部分场景。
等业务继续增长,我们会逐步引入服务网格(Istio)来实现更精细的流量控制。
6. 链路追踪
Jaeger 是 Uber 开源的分布式追踪系统,CNCF 孵化项目。
- 每个请求生成一个唯一的 TraceID
- 每个服务调用生成一个 Span
- 所有 Span 都关联到同一个 TraceID
- 通过 TraceID 可以串联整个调用链
- 看到请求经过了哪些服务
- 每个服务的耗时
- 哪个服务出错
- 调用拓扑图
7. 监控告警
| 类型 | 指标 | 告警阈值 |
|---|---|---|
| 基础指标 | CPU使用率 | >80% |
| 基础指标 | 内存使用率 | >85% |
| 应用指标 | QPS | 突然下降50% |
| 应用指标 | 响应时间 | P99 > 1s |
| 应用指标 | 错误率 | >1% |
| 业务指标 | 订单量 | 环比下降30% |
- P0(紧急):核心服务不可用
- 响应要求:5分钟内响应
- P1(严重):核心功能异常
- 响应要求:30分钟内响应
- P2(一般):非核心功能异常
- 响应要求:2小时内响应
- P3(提示):需要关注但不紧急
- 响应要求:工作时间处理
- 告警直接推送到飞书群
- 支持 @相关人员
- 可以直接点击跳转到 Grafana 看板
8. 日志管理
- 统一日志格式(JSON)
- 必须包含 TraceID(关联同一请求的所有日志)
- 区分日志级别(DEBUG/INFO/WARN/ERROR)
- 敏感信息脱敏
各服务 → Filebeat(采集) →
Logstash(处理) →
Elasticsearch(存储) →
Kibana(展示)
# 查询某个用户的所有操作日志
userId: "12345"
# 查询某个TraceID的所有日志
traceId: "abc-def-ghi"
# 查询最近1小时的错误日志
level: ERROR AND @timestamp: [now-1h TO now]
拆完之后:那些没人告诉你的坑
如果以为拆完就万事大吉了,那就太天真了。
🌐 挑战一:服务间通信
单体应用里,调用一个方法就完了。
微服务里,调用另一个服务要:
- 找到服务地址(服务发现)
- 建立网络连接
- 序列化请求
- 等待响应
- 处理超时、重试
- 超时设置不当:曾经把超时设成60秒,结果数据库慢查询导致整个系统卡死。后来改成:连接超时500ms,读取超时3s。
- 重试策略错误:曾经设置"失败无限重试",结果一个服务挂了,重试风暴把其他服务也打挂了。后来改成:最多重试3次,指数退避。
- 同步调用过多:曾经一个请求同步调用10个服务,任何一个慢了整个请求就慢。后来把非核心调用改成异步,响应时间降低了50%。
💾 挑战二:数据一致性
单体应用里,一个数据库事务搞定。
微服务里,每个服务有自己的数据库,跨服务的事务是噩梦。
比如"用户下单"这个场景:
- 订单服务创建了订单
- 库存服务扣减了库存
- 但支付服务处理失败了
怎么办?回滚?但订单和库存已经改了啊!
这就需要分布式事务方案。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Saga | 长流程业务 | 实现相对简单 | 最终一致性 |
| 本地消息表 | 简单场景 | 简单可靠 | 需要定时任务 |
| 异步补偿 | 高吞吐场景 | 性能好 | 需要设计补偿逻辑 |
- 订单+库存:使用 Saga 模式,订单创建后发消息扣库存,库存不足则发消息取消订单
- 支付+订单:使用异步补偿,支付失败后触发订单取消流程
🔧 挑战三:运维复杂度
40多个服务意味着:
- 40多个代码仓库(或者一个大仓库)
- 40多条CI/CD流水线
- 40多个日志收集点
- 40多个监控指标
- 40多个可能的故障点
- 容器化:所有服务都打包成 Docker 镜像,统一管理
- 容器编排:使用腾讯云 TKE 管理服务部署、扩缩容、故障恢复
- CI/CD:使用 GitLab CI 实现自动化构建、测试、部署
- 服务网格:使用 Istio 统一管理服务间通信、监控、安全
代码提交 → 单元测试 → 构建镜像 → 推送到镜像仓库 →
自动部署到测试环境 → 自动化测试 → 人工审批 →
部署到生产环境(滚动更新)
🔍 挑战四:调试困难
用户反馈"下单失败",你开始排查:
- 订单服务说"我正常"
- 库存服务说"我正常"
- 支付服务说"我正常"
- 用户服务说"我正常"
但组合起来就是不行。
没有它,调试就像在黑屋子里找黑猫。
- Jaeger:链路追踪,看到请求经过了哪些服务
- Kibana:日志聚合,通过 TraceID 查所有相关日志
- Grafana:指标监控,看服务是否正常
[配图建议:一张复杂的网状图,节点是各个服务,连线是调用关系,标注"这就是你的系统"]
微服务拆分的成本收益分析
在决定是否拆分之前,先算一笔账。
成本
- 初期架构改造:3-6个月,2-3人
- 持续维护:每10个服务需要1个SRE
- 学习曲线:团队适应期3-6个月
- TKE 集群费用
- ELK 集群费用
- Prometheus + Grafana 服务器
- Redis 集群
- 拆分后开发效率短期内会下降(跨服务协调、分布式问题)
- 通常6-12个月后才能恢复甚至超过单体时代的效率
收益
- 独立部署:发布频率提升5-10倍
- 故障隔离:核心服务可用性从99.5%提升到99.9%
- 技术自由:可以选择最适合的技术栈
- 独立扩展:资源利用率提升30-50%
- 故障恢复:从小时级降到分钟级
- 团队自治:减少跨团队协调
- 职责清晰:每个团队对自己的服务负责
什么时候值得拆?
- 团队超过20人,单体架构已经严重影响效率
- 业务复杂,模块边界清晰
- 有专门的运维团队
- 有足够的时间窗口(至少6个月)
- 团队小于10人
- 业务简单,单体够用
- 没有运维能力
- 项目周期紧
总结:拆还是不拆?
最后,回到最根本的问题:到底要不要拆成微服务?
✅ 适合拆的场景
- 团队规模超过20人
- 业务复杂度高,模块边界清晰
- 需要独立扩展某些模块
- 发布频率高,需要快速迭代
- 技术栈需要多样化
- 有足够的时间和技术储备
❌ 不适合拆的场景
- 团队规模小(<10人)
- 业务简单,单体够用
- 没有自动化运维能力
- 项目周期紧,没时间重构
- 对一致性要求极高
- 没有分布式系统经验
🎯 核心原则
微服务是手段,不是目的。
目的是让系统更易于开发、部署、扩展、维护。
如果拆分反而让这些变得更难,那就是过度拆分。
要点总结
- 单体架构的核心问题是耦合,微服务通过拆分来解决耦合问题,但也带来了新的复杂度。
- 拆分原则:单一职责、高内聚低耦合、围绕业务能力、数据所有权清晰、渐进式拆分。
- 边界划分方法:事件风暴、CRUD矩阵、依赖分析、团队对齐。选择适合自己团队的方法。
- 拆分不是越细越好,要找到"业务能力"和"技术复杂度"的平衡点,避免"纳米服务"。
- 微服务治理是关键:K8s服务发现、自研配置中心、自研网关、HTTP/2通信、Redis队列、Jaeger追踪、Prometheus监控、ELK日志。
- 微服务不是银弹,是否拆分要根据团队规模、业务复杂度、运维能力综合判断,不要盲目跟风。
- 成本收益要算清楚:微服务前期投入大,回报周期长,要有足够的耐心和资源。
- 技术选型要务实:不要追求最新最炫的技术,选择适合自己业务复杂度的方案。我们的 K8s + Redis + Jaeger + Prometheus + ELK 组合简单但够用。
- 篇1:从0到1:一个游戏发行平台是如何炼成的
- 篇2:48个微服务:拆分的艺术 👈 当前
- 篇3:API网关:所有请求的"第一道门"
- 篇4:服务发现:微服务如何找到彼此
- 篇5:配置中心:改动秒级生效的秘密
💡 写在最后
微服务不是终点,而是一段旅程。
在这段旅程中,你会遇到各种坑,也会收获各种成长。
48个服务不是答案,而是我们在这个阶段的选择。
你的选择可能不同,但思考的过程是一样的。
愿你在微服务的路上,少踩坑,多收获!🚀
- 《微服务设计》- Sam Newman
- 《领域驱动设计》- Eric Evans
- 《凤凰架构》- 周志明
- Kubernetes 官方文档
- Jaeger 官方文档
- Prometheus 官方文档
- Elastic Stack 官方文档
💬 评论 (0)