后端架构演进:从单体到云原生——一个快手老炮的血泪复盘
大家好,我是阿哲,坐标杭州,在快手干了快6年架构师,从0到1搭过商品中台、直播互动系统、还有现在负责的短视频推荐链路。平时写代码喜欢边听陈奕迅边敲键盘(别笑,Eason的《K歌之王》配Java编译器莫名搭),最近被Rust圈粉,感觉它那套零成本抽象和内存安全机制,简直是后端人的新宠。
今天想聊聊这几年我们团队踩过的坑、熬过的夜,以及怎么把一个“祖传”单体应用,硬生生拆成云原生微服务的故事。这篇文章不灌鸡汤,全是生产环境里摔出来的经验,顺便夹带点私货——比如我书架上那本翻烂的《微服务架构设计模式》,还有面试时总被问崩的“如何优雅下线服务”这种灵魂拷问。
起因:那个差点让我在双11被祭天的单体系统
时间回到2021年夏天。我们的核心内容管理系统还是个典型的Spring Boot单体应用:用户管理、视频审核、标签推荐、评论互动……全塞在一个jar包里。数据库就一个MySQL主库+两个只读从库,Redis缓存也共用一套。当时日活百万,勉强扛得住。
但去年双11前两周,产品经理突然甩来需求:“我们要支持实时弹幕+虚拟礼物连击特效,延迟必须低于200ms。” 我看了一眼当前架构——所有请求都要走同一个Tomcat线程池,DB连接池快打满,GC停顿时不时飙到800ms。心里咯噔一下:完了,这船要沉。
更惨的是,运维兄弟在群里@我:“阿哲,你那个CMS又OOM了,dump文件3.2G,赶紧看!” 打开一看,果然是某个算法同学写的推荐逻辑没加缓存,直接全表扫描user_behavior_log,把堆内存干爆了。那一刻我真的想砸电脑——单体架构最大的问题不是性能,而是“牵一发而动全身”。一个模块出问题,整个服务雪崩。
第一步:拆!但别乱拆
很多人以为微服务就是“把大项目切成小项目”,Too young。我在《微服务架构设计模式》第47页划了重点:“拆分的核心依据是业务边界,而不是代码行数。”
我们先做了DDD(领域驱动设计)建模,拉着产品、算法、测试开了三天“撕逼大会”,终于划清了四个核心域:
| 领域 | 职责 | 技术栈 |
|---|---|---|
| User Service | 用户注册/登录/资料 | Java 17 + Spring Boot 3 |
| Content Service | 视频上传/审核/元数据 | Java 17 + MyBatis-Plus |
| Interaction Service | 评论/点赞/弹幕 | Java 17 + WebSocket + Redis Streams |
| Recommendation Service | 个性化推荐算法 | Python + TensorFlow Serving + gRPC |
注意,这里算法模块独立出来了!以前算法同学直接在CMS里写Python脚本调Java接口,现在他们用gRPC暴露服务,我们通过Protobuf定义契约。不仅解耦,还避免了JVM和Python环境打架(别问我怎么知道的,上周五晚上加班就是因为这个)。
拆的时候最头疼的是数据库垂直分库。我们用了ShardingSphere做透明分片,但初期犯了个低级错误:把user_id和video_id混在一起当分片键。结果查询“某用户所有视频”时要跨16个库,慢得像乌龟。后来改成按业务域分库,每个服务独享DB,这才稳住。
第二步:拥抱云原生,但别被“云”忽悠了
拆完微服务只是开始。真正的挑战是如何让这些服务在云上跑得稳、跑得省。
我们选型时对比过K8s原生方案 vs 服务网格(Service Mesh)。考虑到团队规模(15人后端)和运维能力,最终决定用K8s + Istio渐进式落地。理由很简单:Istio能无侵入实现熔断、限流、链路追踪,不用每行代码都写Hystrix。
举个真实案例:Interaction Service上线第一天,因为没设QPS阈值,被恶意刷弹幕打挂。Istio的VirtualService配置救了命:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
http:
- route:
- destination:
host: interaction-service
fault:
abort:
percentage:
value: 100
httpStatus: 429
match:
- headers:
user-agent:
exact: "EvilBot/1.0"
当然,云原生不是银弹。有次半夜报警,说是Recommendation Service延迟飙升。查了半天发现是K8s的HPA(水平扩缩容)策略太激进——CPU超过70%就扩容,但算法服务启动要加载2GB模型,冷启动期间反而拖垮集群。后来改成基于gRPC请求数+GPU显存使用率的复合指标,这才消停。
关键技术细节:那些书里不会告诉你的坑
1. 服务注册与发现:别再用Eureka了!
早期我们用Spring Cloud Netflix全家桶,结果Eureka在节点超过200时同步延迟高得离谱。迁移到Nacos后,服务注册平均耗时从800ms降到80ms,而且Nacos的AP/CP切换模式对混合云部署特别友好。
2. 分布式事务:Saga模式真香
拆库后最怕“创建视频+扣积分”这种跨服务操作。我们试过Seata的AT模式,但死锁频发。最后采用Saga模式+补偿事务:
// 伪代码:视频发布流程
public void publishVideo(Video video) {
// 1. 内容服务创建视频
contentClient.create(video);
// 2. 用户服务扣积分(可能失败)
try {
userClient.deductPoints(video.getUserId(), 10);
} catch (Exception e) {
// 补偿:删除刚创建的视频
contentClient.delete(video.getId());
throw new BusinessException("积分不足");
}
}
虽然要手写补偿逻辑,但胜在简单可控。毕竟生产环境里,可维护性比理论完美更重要。
3. 监控告警:Prometheus + Grafana 是底线
以前靠ELK看日志,等发现问题黄花菜都凉了。现在每个服务暴露/metrics端点,用Prometheus抓取,Grafana画板监控:
- JVM GC次数/暂停时间
- gRPC请求P99延迟
- DB连接池使用率
- K8s Pod重启次数
有一次凌晨三点,Grafana突然标红——Redis连接数突增。 查日志发现是Interaction Service的WebSocket连接没做心跳检测,客户端断网后连接残留。加了Netty的IdleStateHandler后,连接数回归正常。这种问题,没监控根本发现不了。
面试题里的“陷阱”:真实场景 vs 理论题
最近帮公司面试,问候选人:“微服务如何保证数据一致性?” 很多人背八股文:“用分布式事务,2PC、TCC...”。我反问:“如果TCC的Confirm阶段失败了怎么办?” 大部分人懵了。
其实生产环境里,我们更倾向最终一致性 + 幂等设计。比如用户积分变更,消息队列发事件,消费者处理时先查是否已处理过(用业务ID去重),而不是强依赖事务。
另一个高频题:“服务如何优雅下线?” 正确答案不是“调shutdown hook”,而是:
- 先从注册中心注销(Nacos deregister)
- 停止接收新请求(K8s preStop hook sleep 30s)
- 等待存量请求处理完(Tomcat awaitTermination)
有次没做preStop,滚动更新时大量502,被SRE追着骂。面试题考的是原理,但线上事故专治各种不服。
效果与反思:省了钱,但也花了更多钱
改造完成后,双11大促期间:
- 系统吞吐量提升3倍(从1.2k QPS到3.8k QPS)
- 故障隔离率100%(Interaction Service崩了不影响Content Service)
- 资源成本下降40%(K8s自动扩缩容 + Spot实例)
但代价也不小:运维复杂度指数级上升。以前一个运维盯10个服务,现在要管50+微服务+Istio+Prometheus+ELK。所以我们又搞了内部DevOps平台,封装K8s操作,让开发能自助部署。
给兄弟们的建议
- 别为了微服务而微服务:日活不到10万?单体+模块化足矣。
- 先治理再拆分:日志、监控、链路追踪没做好,拆了等于埋雷。
- 算法同学请用gRPC:别再往Java项目里塞Python脚本了!
- 多翻《微服务架构设计模式》:Chris Richardson那本书,第7章讲服务拆分,我看了三遍。
- 保持学习:我现在学Rust,就是想用它写高性能网关(Tokio真香)。技术人不能只守着Java吃老本。
最后说句掏心窝子的话:架构演进没有终点,只有不断平衡。平衡性能与成本,平衡创新与稳定,平衡老板的KPI和自己的头发。上周团建,CTO拍我肩膀说:“阿哲,你们搞的云原生,今年省了200万服务器钱。” 我笑了笑,心想:那我掉的头发,能报销吗?
P.S. 本文所有方案已在快手生产环境验证,但切勿直接照搬。每个公司的业务场景、团队能力、历史包袱都不同。就像我书架上那本《算法导论》——道理都懂,但LeetCode Hard照样做不出来 😅

评论 0