老项目起死回生实录:从技术债务泥潭里爬出来的血泪史

云计算Cloud
2026-01-22 14:33
阅读 1385

去年双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 = ? —— 没错,连主键查询都慢,因为表没索引!

我当时真的想砸电脑。

紧急措施:

  1. 临时加索引(DBA差点报警)
  2. 限流降级,用Sentinel挡掉非核心流量
  3. 重启服务,释放内存泄漏(后来发现是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);
    }
}

这代码写出来,我感觉自己在参加“最丑代码大赛”。

但没办法,这就是现实。很多同学在面试时被问“如何处理技术债务”,答得头头是道,但真上手才发现——技术债不是算法题,没有标准答案,只有妥协和权衡。

第三阶段:监控、告警、自动化——让系统自己说话

老系统最大的问题是“黑盒”。出了问题,全靠人肉排查。

我们做了三件事:

  1. Metrics埋点:用Micrometer + Prometheus,暴露JVM、HTTP、DB指标
  2. 链路追踪:接入Jaeger,哪怕前端是jQuery,也能通过埋入traceId实现端到端追踪
  3. 自动化巡检:写了个脚本,每天凌晨跑健康检查,发钉钉通知

效果立竿见影。上周五晚上,系统自动告警:“数据库连接池使用率 > 80%”。我还没起床,值班同学已经扩容了Pod副本数,问题秒解。

成果:从“随时爆炸”到“稳如老狗”

指标 改造前 改造后
平均响应时间 1200ms 180ms
CPU使用率 85%~95% 30%~45%
部署频率 1次/月 10+次/天
故障恢复时间 2小时+ <5分钟

最重要的是,我们终于敢在需求评审会上说“这个需求可以接”了,而不是“先问问老系统同不同意”。

给同行的几点真心话

  1. 技术债务不是你的错,但解决它是你的责任
    别抱怨前任,他们可能也是一脸懵。重要的是,用最小成本止损,再逐步优化。

  2. 不要追求“完美重构”
    我见过太多人想一把推倒重来,结果项目延期、老板暴怒、自己背锅。渐进式演进才是王道。

  3. 前端也是技术债务重灾区
    很多后端同学觉得“前端乱不关我事”,但前后端耦合的系统,前端不改,后端寸步难行。建议和前端约个饭,一起骂产品经理,感情好了,协作才顺。

  4. 云原生不是银弹,但能救命
    K8s、Service Mesh这些工具,本质是帮你把“运维复杂度”转化为“架构能力”。前提是,你得懂它们,而不是只会抄YAML。

  5. 保持生活节奏,别被工作榨干
    我在成都,晚上八点准时下班,周末爬山喝茶。代码可以烂,但生活不能。技术债可以慢慢还,命只有一条。

最后:关于“面试题挑战”的真相

最近帮公司面试,总有人问:“你们怎么处理技术债务?”
我一般反问:“如果给你一个jQuery老系统,要求三天内上K8s,你怎么搞?”

90%的人开始讲微服务、DDD、Clean Architecture……
但真实世界里,你可能连改一行前端代码的权限都没有。

所以,别光背八股文。多想想:如果明天你接手一个没人敢碰的“遗产系统”,你能不能活着把它救回来?

能,那你就是真·高级工程师。
不能,那你可能还需要在成都的茶馆里,多喝几杯盖碗茶,静下心来,学点实战经验。

——
作者:字节跳动基础架构组后端开发,Mac党,K8s重度用户,现居成都。日常搬砖,偶尔写博,梦想是有一天不用再救火。

评论 0

最热最新
暂无评论
云计算CloudLv.1
0
影响力
0
文章
0
粉丝