内部系统用奇怪的项目代号并不少见,真正少见的是把“事多”两个字写进日常描述里。“马桶基地”不是生活服务类产品,而是一套长期接管业务单据转发、消息推送、权限校验和对外回调的内部基础服务。团队成员提起它时,经常把它面对的大量琐碎变更直接称为“b事多”。这个项目运行了很多年,功能不断增加,配置散落,日志格式混乱,升级路径几乎没有人能完整讲清楚。后来团队准备动手治理,摆在面前的问题不是“用什么语言重写”,而是两条代号为“飞天”和“泰电”的技术路线之争。这篇文章是这次治理工作的番外记录,重点是分析两条路线的差异、如何把改造目标量化,以及在实际落地时那些容易被忽略的坑。
很多技术团队在规划改造时,第一反应是列出一堆中间件和框架,但真正决定项目命运的,往往不是技术本身,而是对现有问题的定位是否准确。先弄清楚为什么一个内部基础服务会变得如此难维护,再谈该选择哪种改造路线,顺序不能颠倒。
1. 为什么一个内部基础服务会变成“b事多”重灾区
1.1 项目画像:表面稳定,实质被大量特例拖住
“马桶基地”这个内部代号虽然听起来随意,但它承担的工作并不简单。它可以理解成一套中间层服务:上游业务系统把单据、状态变更、通知请求交给它,它做权限校验、参数补全、字段转换,再调用下游的财务、仓储、短信、推送等系统。
这类服务最典型的特征是:每个接入方都觉得自己没有做大改动,每个人都只是在原有逻辑上加了一个判断分支。一段时间之后,核心链路里就会出现大量“如果来源是A平台就走旧逻辑”“如果字段为空就走默认值”“如果时间大于某个点就兼容一次异常数据”这类特判代码。
这些特判并不是完全无意义,它们每一个都对应一个曾经真实发生过的业务问题。问题在于特判没有统一收敛,而是散落在不同方法、不同配置、不同状态码里。任何一个新需求进来,开发人员都很难判断这个改动会影响哪个分支,测试人员也不知道该回归哪条路径。
所以“b事多”并不是指这个服务本身有多繁忙,而是指它被高频的、低质量的、口径不一致的变更反复消耗。每次改动看起来都很快,但长期累积下来,系统变得越来越难改,越来越不可预测。
1.2 “事多”带来的显性成本
在决定改造之前,团队曾经做过一次粗略统计,结果并不乐观。以内部观察到的现象为例:
- 一个季度内,该服务被修改超过三十次,其中一半是小需求,但每次都需要完整回归。
- 不同团队会修改同一个核心方法,经常出现改完一个字段后另一个团队的调用逻辑反而出错。
- 部署时因为多个变更排队,发布窗口经常被挤压到周五晚上,回滚时需要靠人工翻聊天记录确认旧版本号。
- 日志缺少统一 traceId,排查一个问题经常要跨三个系统人工关联记录。
这些现象反映到指标上,就是部署频率偏高但变更成功率偏低,恢复时间偏长。团队内部一度出现“谁改动这个服务谁紧张”的氛围。
这里要特别说明,团队不需要在改造前就把每个指标做到工厂级标准,但至少要把现状数字记录下来。没有基线,后面就无法判断改造是有效还是无效。建议至少记录四个指标:部署频率、变更失败率、平均恢复时间、变更前置时间。后面会单独说明这些指标如何计算。
1.3 要定性:这是技术债问题,不是一次重写能解决
很多项目一旦出现这类症状,团队会本能地想到“重写”或者“换框架”。但在这个案例里,真正的问题不只是代码不好看,而是系统缺少边界、缺少测试、缺少可观测性、缺少回滚机制。
如果只是把一套旧代码翻译成新框架,部署方式不变、配置管理方式不变、回归策略不变,那么问题只会在新系统里重新出现一遍。这也是为什么“飞天”和“泰电”两条路线的对比,不应当只比性能和功能,而要比谁能更有效地降低未来变更的复杂度和风险。
在进入路线对比之前,先明确一个判断:这次改造的目标不是“把项目变成微服务”,而是“降低变更链路中的不确定性和返工成本”。目标不同,方案选择会完全不同。
2. “飞天”和“泰电”是哪两条技术路线
2.1 飞天路线:弹性优先的云原生改造
“飞天”在项目组内部不是某个具体产品的名字,而是代指以云原生、弹性扩展、动态调度为核心的改造路线。它的典型做法包括:
- 把服务拆小,按业务域拆分成可独立部署的模块。
- 引入服务注册与发现、配置中心、统一网关。
- 将无状态应用容器化,部署到 Kubernetes,使用 HPA 按 CPU 或 QPS 自动扩缩容。
- 对下游调用增加超时、重试、熔断、幂等等标准化处理。
这条路线吸引人的地方在于,它把“系统无法支撑突发流量”“单点故障影响全局”“资源利用率低”等问题一并纳入设计范围。理论上,改造完成后,原来某个模块出问题不会拖垮整个服务,流量增加时也能通过扩容快速扛住。
下面是一个常见的 HPA 配置示例,用于在 CPU 超过阈值时自动扩容。这里只展示思路,实际项目中需要根据自己的 Deployment 名称、命名空间和资源规格调整。
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: toilet-base-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: toilet-base minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70这里有一个很容易踩的误区:HPA 的 maxReplicas 并不建议拍脑袋写成很大的值。某个服务能不能水平扩容,不取决于它自己被扩成多少个 Pod,而取决于它依赖的下游资源是否也具备扩展能力。如果数据库连接池有限、文件存储带宽有限、第三方接口有 QPS 限制,那么 Pod 越多,反而越容易先打爆下游。飞天路线真正的难度不在 Kubernetes 本身,而在分布式场景下的数据一致性、超时控制和调试成本。
2.2 泰电路线:稳定优先的保守演进
“泰电”在项目组内部代指另一条路线:尽量不改变现有架构模型,只做必要的基础设施标准化。它的目标不是“彻底云原生”,而是“减少环境差异和发布风险”,可以用下面几个动作概括:
- 统一构建产物,使用容器镜像解决环境依赖问题。
- 保持单体应用或模块化单体结构,不急于拆分。
- 建立标准化发布流程,所有变更走同一套流水线。
- 把散落在代码里的配置外置到环境变量或配置中心。
- 增加基础的健康检查、日志采集和告警。
这条路线听起来不够“先进”,但它需要的改造量小、回归范围更可控,特别适合那些团队人手有限、业务不能长时间停机、外部依赖方很多的内部服务。
下面是一个典型的 Java 服务 Dockerfile,它解决的核心问题是“开发环境能跑,生产环境跑不了”这一类的环境差异问题,而不是架构问题。
FROM eclipse-temurin:17-jre ENV TZ=Asia/Shanghai ENV JAVA_OPTS="-Xms512m -Xmx1024m -Djava.security.egd=file:/dev/./urandom" WORKDIR /app COPY target/toilet-base.jar app.jar EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]这里要注意,容器化只解决“打包、分发、启动”的一致性,它不会自动解决业务逻辑混乱问题。如果代码中到处是硬编码 IP、本地文件路径、手工维护的数据字典,那么容器化之后这些问题仍然存在,只是换了一台更标准的机器运行。泰电路线的核心价值,是把过高频次的部署和人工操作标准化,而不是掩盖业务复杂度。
2.3 用一张表先把两条路线差异摆清楚
在做技术选型时,不要一开始就争论哪个方案更好,先列出两条路线在每个关键维度上的差异,团队讨论才有共同语言。下面是一个适合内部讨论的对比表。
| 维度 | 飞天路线 | 泰电路线 |
|---|---|---|
| 核心目标 | 弹性、可用性、模块自治 | 稳定、可控、降低回归风险 |
| 改造范围 | 应用拆分、服务治理、容器编排 | 构建统一、配置外置、发布标准化 |
| 实施周期 | 长,通常以季度甚至半年计 | 短,几周内可以看到成效 |
| 团队要求 | 需要熟悉 Kubernetes、分布式链路、SRE 能力 | 主要依赖原有研发团队和基本运维能力 |
| 回归风险 | 高,因为调用链和部署单元都变了 | 低,架构没有大动,回归以功能验证为主 |
| 突发流量能力 | 强,可以水平扩容 | 弱,受限于单应用部署规模 |
| 故障定位难度 | 需要链路追踪和日志平台配合 | 相对集中,单体日志更容易关联 |
| 投入产出比 | 适合长期扩张、有明确规模化目标 | 适合内部系统稳定运行、减少琐碎劳动 |
表格并不是最终裁决,它用于帮助团队看清楚:选择飞天路线,本质上是在买未来的扩展空间;选择泰电路线,本质上是在买当前的可控性。没有哪条绝对正确,关键是当前团队状态和业务阶段匹配哪条。
3. 在选路线之前,先把改造目标量化成指标
3.1 用四个指标替代“感觉快了”“感觉稳定了”
技术讨论中最容易出现的无效争论,是双方都在凭感觉表达。有人说“现在发布太频繁了,需要治理”,另一个人说“发布频繁说明敏捷,不用改”。要想让争论变成可执行的改造计划,必须先把“感觉”换算成指标。
这里推荐使用 DORA 四个核心指标作为基线:
| 指标 | 计算方式 | 关注点 |
|---|---|---|
| 部署频率 | 有效生产发布次数 / 周期天数 | 发布是顺畅还是经常排队 |
| 变更前置时间 | 代码提交到正式上线的平均时长 | 流程是否存在等待和手工环节 |
| 变更失败率 | 导致故障的发布次数 / 总发布次数 | 每次变更是否足够安全 |
| 平均恢复时间 | 故障发生到恢复的平均时长 | 是否有回滚机制和排查效率 |
改造前先统计两周到一个月的数据。统计口径不要求百分之百精确,但发布次数和故障次数必须能对上。如果一个服务发布一次就能让下游全挂,那么无论它部署得多频繁,都应该先把变更成功率提上去。
一个合理的观察值是:发布失败率高于百分之十、恢复时间超过一小时,就应该优先考虑“泰电”式的标准化动作。如果业务确实经常出现大流量冲击,且现有机器规格已经扛不住,才需要考虑“飞天”式弹性改造。
3.2 建立一套最小但有效的告警模板
指标需要一个抓手,否则只是事后统计的数字。对“马桶基地”这类基础服务而言,最先应该处理的不是“请求量上涨”,而是错误率和响应时间异常。
下面是一个基于 Prometheus 的告警规则示例。它监控的就是最常见的两个问题:5xx 比例过高、P99 延迟异常。规则文件可以按服务独立维护。
groups: - name: toilet-base-alerts rules: - alert: HighErrorRate expr: | sum(rate(http_requests_total{service="toilet-base", status=~"5.."}[5m])) / sum(rate(http_requests_total{service="toilet-base"}[5m])) > 0.01 for: 5m labels: severity: critical annotations: summary: "toilet-base 5xx rate over 1%" description: "service toilet-base 5xx ratio is high in last 5m" - alert: HighLatency expr: | histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{service="toilet-base"}[5m])) by (le) ) > 2 for: 10m labels: severity: warning annotations: summary: "toilet-base P99 over 2s"这里有两个细节值得解释。第一,表达式中的for: 5m是为了消除瞬时抖动,某个请求慢不代表服务不可用,持续五分钟才告警更合适。第二,为什么 5xx 比例用“1%”而不是“0%”?因为外部依赖偶发失败会导致 5xx 瞬间上升,把阈值设成 0 会让告警失去意义,重点应放在持续异常而不是偶发抖动。
3.3 用决策矩阵过滤掉不适合当前团队的方案
选型不能只看技术先进性,还要看团队能不能接住这套系统。一个很常见的失败案例是:公司没有专职运维,却选择了飞天路线,最后 Kubernetes 集群出了问题没人能处理,服务反而比改造前更不稳定。
决策矩阵可以用几个问题来构建,每个问题答案越明确,越容易筛选方案:
| 决策问题 | 选飞天路线的前提 | 选泰电路线的前提 |
|---|---|---|
| 团队有足够的人维护基础设施吗 | 有专职或近专职运维/SRE | 主要靠研发兼职运维 |
| 业务允许长时间灰度迁移吗 | 能接受较长改造周期和分批切换 | 需要尽快降低现有风险 |
| 流量是否持续有突发增长 | 是,常见于大促、活动、推广期 | 否,日常 QPS 平稳 |
| 下游调用是否都具备扩容能力 | 是,数据库、第三方接口都弹性 | 否,下游有硬性 QPS 限制 |
| 故障恢复是否依赖快速水平扩展 | 是,例如资源到期前必须扩容 | 否,更依赖快速回滚和日志定位 |
如果答案大多数都偏向右边,建议先选择泰电路线;如果偏向左边,飞天路线才值得投入。真实项目里还有一种选择:先做泰电式治理,把发布、配置、日志、回滚全部标准化,再在某个独立模块上试点飞天式弹性改造。这种做法风险更低,也更容易积累经验。
4. 重构落地时最容易踩的三类坑
4.1 坑一:把“顺手的框架”当成“架构方案”
在路线确定之后,团队容易立刻进入“选框架”阶段。比如有人建议把 Spring Boot 换成另一种框架,认为这样可以提升性能;有人建议换消息队列,认为这样吞吐更高。这些动作都只是工具替换,而不是架构治理。
真正应该先做的是画出依赖边界:这个服务依赖哪些数据源?下游有哪些系统?哪些调用是同步的,哪些是异步的?哪些模块经常被不同团队同时修改?把这些问题回答清楚,改动方案才能落在“边界”上。否则,换完框架后会发现,新框架里同样写满了原来的特判逻辑,甚至因为框架不熟悉而增加更多问题。
建议在动手写代码前,先用一周时间做“代码考古”:找出核心方法、列出所有调用方、统计所有特判条件。这个动作不会立刻让系统变好,但它能避免重构时改到关键逻辑。
4.2 坑二:迁移方案没有回滚路径
很多改造项目只设计了“如何从旧系统切到新系统”,没有设计“如果新系统出问题如何切回旧系统”。这种单程票式的迁移风险极高,尤其对内部基础服务来说,一旦新系统在凌晨出现严重故障,团队可能连恢复工具都没有。
一个比较稳妥的做法是双跑验证。以流量切换为例,可以采用以下顺序:
- 新系统与旧系统并行运行,把新系统的输入流量复制一份。
- 对比两个系统的核心处理结果,先纠正字段映射错误。
- 将少量真实流量切到新系统,比如百分之五,观察四十八小时。
- 如果没有异常,再切到百分之二十,观察指标和下游反馈。
- 逐步放大到百分之百,同时保留旧系统的部署环境至少两个发布周期。
这个过程中最重要的不是“切多少比例”,而是每一步都定义清楚“什么现象算失败”。比如错误率超过百分之零点五、某个下游开始收到重复消息、积压消息持续上涨,这些都是回滚信号。
如果原始材料没有给出具体的迁移工具或调度平台,那么在实施前需要先确认团队是否有流量染色、灰度发布或网关按比例转发的基础设施。没有这些能力时,不要强行按照“先切百分之五”的流程做,因为手动切流量只适合小规模试点,不具备全量灰度基础。
4.3 坑三:自动化只覆盖正常流程,不覆盖异常
重构过程中,很多团队会补自动化测试,但常见的测试用例只有“正常请求返回 200”。对基础服务来说,真正容易出问题的往往不是正常路径,而是异常路径。
下面是一个简易测试矩阵,可以直接作为内部评审时的用例清单:
| 用例编号 | 场景 | 预期行为 |
|---|---|---|
| TC01 | 正常请求,下游返回成功 | 返回成功,记录 traceId |
| TC02 | 下游超时 | 返回业务失败,不重复提交 |
| TC03 | 下游返回 500 | 触发重试或熔断,最终有明确结果 |
| TC04 | 重复请求,相同业务单号 | 幂等返回第一次的结果 |
| TC05 | 请求参数缺少必填字段 | 返回参数错误,不落库 |
| TC06 | 消息队列积压 | 消费端不崩溃,延迟指标可观测 |
| TC07 | 应用重启 | 未处理完的任务能恢复或安全跳过 |
只有把异常路径纳入回归范围,自动化测试才有实际保护价值。否则它只是给团队一种“有覆盖”的安全感,真正发生故障时仍然要靠人去手工翻日志。
5. 番外:这场对比最终留下的七条可复用清单
5.1 排查一个“事多”系统时先看什么
当团队接手一个高频变更、低稳定性的内部服务时,不要第一时间打开代码搜索 bug,先按下面的顺序做诊断:
- 画出系统依赖图,列出上游调用方和下游依赖方。
- 找出最近三个月变更最频繁的模块或方法。
- 统计所有特殊兼容逻辑和临时开关,确认它们的属主。
- 检查日志中是否有统一 traceId,能否串起一次完整请求。
- 确认配置项分布,哪些散落在代码里,哪些依赖手工维护。
- 查看发布记录,找出哪些发布曾经回滚,回滚原因是什么。
- 记录当前部署频率和变更失败率的基线。
这套清单在治理前跑一遍,能快速定位问题集中区,也能在团队讨论时提供事实依据。
5.2 技术选型时不要只看“优点清单”
很多技术方案都会列出一堆优势,但选型真正应该看的是代价。下面是一份最少必要清单:
- 明确要解决的问题是什么,不要用方案反推问题。
- 列出当前团队不具备的能力,例如 K8s 运维、分布式事务调优。
- 写出试点方案,至少做一个两周级别的技术验证。
- 定义可量化成功标准,例如部署时间从两小时降到二十分钟。
- 设计回滚方案,新系统失败时如何切回旧系统。
- 确认业务可承受的停机窗口和兼容周期。
不管选飞天还是泰电,都建议先拿一个低风险模块试点,跑完一个完整发布周期后再决定是否推广。技术选型的价值不在于选得惊艳,而在于选完还能持续推进。
5.3 对“b事多”系统最务实的一条长期建议
最后一条建议比选型本身更重要:不要试图靠一次大改造解决所有问题。内部基础服务之所以变成“事多”,是因为它长期承担了过多职责,并且缺少足够的守护机制。真正有效的长期做法是持续做减法:收紧变更入口,强制走标准化流水线;补关键自动化回归,让每次改动的成本下降;定期清理那些已经没人能解释的特判逻辑。
另外,不要在周五下午进行这类基础服务的大版本发布。即使已经做了灰度验证,也要保留完整的回滚预案。对基础服务来说,稳定性不是某一次改造的成果,而是每一次变更都足够克制的累积结果。把发布频率降下来、把失败恢复时间缩短、把配置和日志管理好,这个系统即使仍然叫“马桶基地”,也不会再被大家用“b事多”来评价。