老项目起死回生实录:从技术债务泥潭里爬出来的血泪史
去年双11前两周,我正躺在成都家里阳台晒太阳,啃着兔头刷着K8s新版本的release note,突然钉钉“叮”一声——不是普通消息,是红色加急@全员。点开一看,后端组长发来一句:“XX老系统CPU飙到90%,再不救活,明天就上故障复盘会。”
那一刻,我真想把Mac合上,假装没看到。但没办法,谁让我是字节跳动基础架构组那个“专门收拾烂摊子”的后端五年老兵呢?坐标成都,生活节奏慢,代码节奏却快得像被产品经理拿鞭子抽着跑。
这系统,说起来都是泪。它诞生于2018年,前端用jQuery写的SPA(对,你没看错,jQuery),后端是Spring Boot 1.5 + MyBatis + 一个自研的RPC框架,部署在裸机上,日志靠grep,监控靠人肉盯。上线时号称“能扛百万QPS”,结果现在连一千都喘不过气。
技术债务?不,这是技术沼泽
第一次接手这个项目,我就知道完了——代码仓库里有37个分支没人合并,CI/CD流程写在README里,靠人工执行脚本发布。最离谱的是,数据库连接池配置居然是硬编码在Java类里的,改一次要重新编译打包。
当时我心里OS:这哪是项目,这是考古现场吧?
但吐槽归吐槽,饭还得吃,bug还得修。领导一句话:“你搞云原生不是挺熟?K8s玩得溜,正好练手。” 好家伙,合着我是来当“技术债务清道夫”的。
第一阶段:止血,别让系统当场去世
线上CPU高,第一步肯定是查瓶颈。
top看进程,Java进程占满一个核jstack打线程栈,发现大量线程卡在com.xxx.service.UserService.getUserById- 再看数据库,慢查询日志里全是
SELECT * FROM user WHERE id = ?—— 没错,连主键查询都慢,因为表没索引!
我当时真的想砸电脑。
紧急措施:
- 临时加索引(DBA差点报警)
- 限流降级,用Sentinel挡掉非核心流量
- 重启服务,释放内存泄漏(后来发现是MyBatis缓存没关)
三天三夜,终于把系统稳住。但我知道,这只是开始。
第二阶段:重构?不,是重建
真正的挑战来了:怎么把这坨“屎山”搬进现代架构?
我们定了几个原则:
- 不重写,只迁移:老板明确说了,没预算重做,只能“边飞边换引擎”
- 前端不动:前端团队早跑路了,只剩一个实习生维护,所以API必须完全兼容
- 云原生优先:既然我在基础架构组,那就用K8s+Service Mesh+Prometheus全家桶
后端改造:从裸机到K8s
第一步,容器化。
原系统启动命令是 nohup java -jar app.jar &,部署靠scp。我把它塞进Dockerfile:
FROM openjdk:8-jre-slim
COPY target/app.jar /app/
EXPOSE 8080
CMD ["java", "-Xmx1g", "-jar", "/app/app.jar"]
然后写Helm Chart,接入公司内部的K8s平台。
结果第一次部署就翻车了:应用启动后疯狂CrashLoopBackOff。
查日志发现:它依赖一个本地文件 /etc/config.properties,而容器里根本没有。
解决方案:
- 配置外置到ConfigMap
- 敏感信息走Secret
- 日志输出到stdout,由Fluentd采集
第二步,拆单体。
虽然不能大拆,但至少把用户服务、订单服务、通知服务抽成独立Deployment。用Istio做流量切分,灰度发布。
前端?别提了,简直是“面试题挑战”现场
原前端是jQuery + 自己造的路由轮子,页面跳转全靠 window.location.href。更离谱的是,它和后端耦合极深——比如登录成功后,前端直接读取Cookie里的 user_id,然后拼URL去拉数据。
我一度怀疑这代码是实习生毕业设计。
但我们不能动前端,所以后端必须100%兼容旧接口。这意味着:
- 不能改返回字段名
- 不能改HTTP状态码逻辑(比如401必须返回HTML,不是JSON)
- 甚至不能改URL路径
于是,我在新服务里加了一层“兼容网关”:
@RestController
public class LegacyCompatController {
@GetMapping("/user/{id}")
public ResponseEntity<String> getUserLegacy(@PathVariable String id) {
// 调用新服务
User user = userService.findById(id);
// 手动拼成旧格式的JSON字符串(因为旧前端解析不了标准JSON)
String legacyJson = "{\"code\":200,\"data\":" + JSON.toJSONString(user) + "}";
return ResponseEntity.ok(legacyJson);
}
}
这代码写出来,我感觉自己在参加“最丑代码大赛”。
但没办法,这就是现实。很多同学在面试时被问“如何处理技术债务”,答得头头是道,但真上手才发现——技术债不是算法题,没有标准答案,只有妥协和权衡。
第三阶段:监控、告警、自动化——让系统自己说话
老系统最大的问题是“黑盒”。出了问题,全靠人肉排查。
我们做了三件事:
- Metrics埋点:用Micrometer + Prometheus,暴露JVM、HTTP、DB指标
- 链路追踪:接入Jaeger,哪怕前端是jQuery,也能通过埋入traceId实现端到端追踪
- 自动化巡检:写了个脚本,每天凌晨跑健康检查,发钉钉通知
效果立竿见影。上周五晚上,系统自动告警:“数据库连接池使用率 > 80%”。我还没起床,值班同学已经扩容了Pod副本数,问题秒解。
成果:从“随时爆炸”到“稳如老狗”
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 平均响应时间 | 1200ms | 180ms |
| CPU使用率 | 85%~95% | 30%~45% |
| 部署频率 | 1次/月 | 10+次/天 |
| 故障恢复时间 | 2小时+ | <5分钟 |
最重要的是,我们终于敢在需求评审会上说“这个需求可以接”了,而不是“先问问老系统同不同意”。
给同行的几点真心话
技术债务不是你的错,但解决它是你的责任
别抱怨前任,他们可能也是一脸懵。重要的是,用最小成本止损,再逐步优化。不要追求“完美重构”
我见过太多人想一把推倒重来,结果项目延期、老板暴怒、自己背锅。渐进式演进才是王道。前端也是技术债务重灾区
很多后端同学觉得“前端乱不关我事”,但前后端耦合的系统,前端不改,后端寸步难行。建议和前端约个饭,一起骂产品经理,感情好了,协作才顺。云原生不是银弹,但能救命
K8s、Service Mesh这些工具,本质是帮你把“运维复杂度”转化为“架构能力”。前提是,你得懂它们,而不是只会抄YAML。保持生活节奏,别被工作榨干
我在成都,晚上八点准时下班,周末爬山喝茶。代码可以烂,但生活不能。技术债可以慢慢还,命只有一条。
最后:关于“面试题挑战”的真相
最近帮公司面试,总有人问:“你们怎么处理技术债务?”
我一般反问:“如果给你一个jQuery老系统,要求三天内上K8s,你怎么搞?”
90%的人开始讲微服务、DDD、Clean Architecture……
但真实世界里,你可能连改一行前端代码的权限都没有。
所以,别光背八股文。多想想:如果明天你接手一个没人敢碰的“遗产系统”,你能不能活着把它救回来?
能,那你就是真·高级工程师。
不能,那你可能还需要在成都的茶馆里,多喝几杯盖碗茶,静下心来,学点实战经验。
——
作者:字节跳动基础架构组后端开发,Mac党,K8s重度用户,现居成都。日常搬砖,偶尔写博,梦想是有一天不用再救火。

评论 0