
在上一篇文章《微服务是毒瘤吗?》中,我们强调了架构设计和发展需要适应业务的需求和变化。这篇文章就是一个非常好的架构随业务、时间变迁而重构的观察对象,可以帮助大家搞清楚什么情况下平迁,什么情况下重建。
作者团队通过一次为期 24 个月的重构战役,最终实现了这些业务收益:
可用性从不足 99% 到 99.99%;
核心链路响应时间下降 50%;
代码仓库从 200+合并成一个大仓,代码行数从 300 万行精简到 30 万行;
推荐中台成本下降 58%,Redis 成本下降 70%,亿次 PV 成本下降 95%。
关注腾讯云开发者,一手技术干货提前解锁👇
任何业务系统重构里面最核心的原则就是康威定律,即组织的沟通结构决定了业务系统的架构。换句话说组织架构要服务于合理的系统架构,业务重构发生前组织重构要先行。但是本文落脚点并不在于讨论公司治理的效率和组织能力,因而我们假设公司的组织架构是有足够的效率和弹性的,业务系统重构本身并不受公司组织架构的制约。
“重构”在互联网里面已经成为了一个司空见惯的名词,在各个公司也算是一个老生常谈的问题。但是什么时候重构?为什么要重构?怎么重构?要回答好这些问题似乎又不是那么的简单,今天想通过自己过去做过的几个案例跟大家分享一下我对于重构的一些理解。
首先,我们先简单定义一下“重构”,什么样的改动才能称之为“重构”呢?我们尝试从程序设计这个微观层面来定义一下:重构是一种有规范、有组织、在不改变软件外部行为的前提下,改善其内部结构的代码修改方式。从这个角度来讲并非所有的代码改动都可以称之为“重构”,重构的行为本身必须遵循一定的原则,其中最重要的一点就是不改变原有程序的observable behavior(外部的可观测性),简单来说就是:在重构前后,你的软件对于任何相同的输入,都应该产生完全相同的输出。但是在实际的业务场景中,系统的优化往往会横跨多个团队、多个模块、多种语言,因而我们从宏观的视角来重新定义一下实际业务系统层面的重构:重构是在不改变系统外部可观察行为的前提下,通过改善其内部结构(代码、架构、数据模型),以提升非功能性指标(如可维护性、扩展性、性能),并为未来业务发展奠定坚实技术基础的系统性工程。
因而在业务系统的重构中一般来说不可变量为原有系统的功能性或者说业务流程的完整性,局部会要求接口的幂等性,但是具体到不同的业务场景不变性的约束又不尽相同。举个例子:假如我们重构的是一个交易系统,那么内部不可变量就非常明确:整个交易流程的完整性以及交易数据的幂等性就是一个强约束。但是如果我们改造的是类似腾讯新闻这样的推荐系统,这里面的不可变约束就变成了局部业务流程的完整性(人工干预的有效性、内容时效性约束、负反馈的有效性等)以及业务系统核心指标的稳定性,整体而言对于不变性的约束会变弱。因而我们得出另外一个重要的原则,在业务系统重构的过程中不变性约束的强弱和范围跟原有业务系统对于数据一致性要求的强弱和范围相关。
现在似乎我们对于重构有了一个较为明确的定义,然后我们尝试去回答第二个问题,为什么要去重构?其实在上述概念定义里面也可以寻得蛛丝马迹,一般来讲我们可以概括为两种情形:其一当原有的系统在非功能性指标的各个维度上出现了较为严重的问题,进而影响了业务的功能性需求的时候,我们就需要考虑重构;其二就是业务面临转型期,核心的功能定位产生了变化,原有系统已经不能够支撑新的功能的演进。在一个大的业务系统里面往往两种情况交叠出现。
21年底,我加入腾讯新闻,恰好适逢腾讯新闻业务的转型期,按照新的业务方向来评估原有的业务系统是没办法满足个性化引擎驱动的社区内容分发的需求的,所以业务系统的重构迫在眉睫。说实话,刚拿到这个任务时,我的第一反应不是兴奋,而是焦虑。这套系统已经稳定运行了接近20年,同时系统的设计跟原有门户的产品形态是比较匹配的,每天服务大几千万用户,任何闪失都可能上热搜。但不重构又不行——老架构已经严重制约了业务发展,技术债像滚雪球一样越滚越大。
先说几个数字让大家感受一下当时的状况:2021年全年,推荐系统引发事故多次,可用性不足99%。存在200多个代码仓库、300多万行代码、2000多个物理服务,同时运营成本居高不下,维护这套系统就像在泥潭里跋涉,在这种情况下重构就成为了势在必行的选择。
我们从系统非功能性指标来看,三个最直接的痛点:
第一是性能瓶颈。推荐系统最核心的召回、排序链路,响应时间已经从最初的500ms飙到了1200ms+。我们尝试过各种优化,该加的缓存加了,该拆的服务拆了,但收效甚微。根本原因是架构设计时没考虑到分发侧策略的复杂度相比原有运营驱动的体系有指数级的增加,单单一个精排模型的耗时就超过了200ms。更要命的是,系统里跑着400多个业务场景、1000多种推荐策略,各个场景之间的复用程度极低,基本上是“一个场景一套服务”,资源利用率很差。
第二是扩展困难。产品经理隔三差五就提新需求:“能不能支持多路召回?”“能不能加个实时特征?”“能不能做个冷启动优化?”每个需求听起来都合理,但落到代码层面,牵一发而动全身。因为早期是单体架构,各个模块耦合严重,改一处代码要回归测试好几天。一个需求从提出到上线,平均要一个月,期间要拉十几个人开大会、建大群、联调各种接口。团队的迭代速度已经跟不上业务节奏了。
第三是维护成本高。老代码里充斥着“临时方案”和“历史包袱”。有些模块的注释还停留在三年前,原作者早已离职。每次出问题排查都像考古,得花大量时间理解上下文。更头疼的是,因为架构老旧,想招有经验的人来接手都难——谁愿意天天维护一套“过时”的系统?我们统计过,几个核心开发陆续提离职,理由都差不多:“不想再做系统迁移的工作了”,同时系统的可解释性差,出了线上问题定位困难,运维压力贼大。
问题定义清楚了系统重构的目标就呼之欲出,核心链路响应时间降到800ms以内,支持更灵活的策略配置,可用性提升到99.9%以上,为未来三到五年的业务增长留足空间。这些目标倒逼着我们必须动手术,而且得是个“大手术”。
很多人问我,怎么判断一个系统是该修修补补,还是该推倒重来?这个问题没有标准答案,但我总结了一套“三看”原则。
看技术债务的“利息”有多高。技术债就像借钱,少量的债务可以用“还利息”(打补丁、局部优化)的方式维持,但当利息高到让你喘不过气,就得考虑“还本金”(重构)了。我们做过一次统计,团队30%的时间都在处理因为老架构导致的各种问题——性能优化、bug修复、兼容性调整。这意味着我们实际的研发效能只有70%,这个“利息”已经太高了。
看业务目标能否实现。如果业务方提的需求,用现有架构根本做不到,或者要付出巨大代价才能做到,那就是明确的信号。我们当时遇到的情况是:产品想做“千人千面”的精准推荐,需要实时特征和复杂的多路召回策略,但老系统连基本的AB实验能力都很弱,更别提支持这么复杂的策略了。
看团队士气。这点经常被忽略,但其实很关键。如果团队成员都在抱怨代码难维护,优秀的人才流失严重,新人上手周期越来越长,这就是系统该换代的信号了。我们当时的情况是,几个核心开发陆续提离职,理由都差不多:“不想再维护这套系统了”。
当然,光有问题还不够,还得评估重构的ROI。我们当时算了笔账:如果不重构,按照当前的事故频率和人力投入,光是多招人来维护老系统、应对频繁的线上问题,一年要多花几百万人力成本;机器成本这块,因为架构不合理导致的资源浪费居高不下。如果重构成功,性能提升30%意味着可以节省至少xx万/月的服务器成本,架构统一后服务数量从2000+降到几十个,运维成本至少降低70%,再加上业务指标提升(因为支持更好的推荐策略带来的用户增长和广告收入),综合算下来一年能省下数千万。这笔账一算,管理层也就同意了。
决定要重构之后,还有一个更关键的问题:是做“平迁”还是“重建”?这个选择直接决定了项目的复杂度、风险和收益。
平迁,就是保留老的业务逻辑,只换底层的技术架构。就像老房子翻新,外观和格局不变,但把水电管道、墙体结构全部换新的。代码层面,业务规则、算法策略、数据模型基本不动,主要是换框架、拆服务、优化性能。
重建,就是推倒重来,重新设计业务逻辑。就像拆掉老城区盖新城,不仅建筑材料是新的,整个城市规划都重新做。代码层面,连业务逻辑都要重新梳理,可能会砍掉一些历史包袱功能,也会加入新的业务能力。
那怎么选?我总结了一套判断标准。
选择平迁的三种情况:
第一种,业务逻辑本身是对的,只是架构扛不住了。比如推荐算法经过多年打磨已经很成熟,效果也不错,问题只是老架构的性能、扩展性跟不上了。这种情况下没必要动业务逻辑,把架构升级就够了。
我们推荐系统的召回、排序这套逻辑,经过2年多的迭代,效果已经很稳定了。问题是老架构里各个场景的代码都是独立维护的,数百个独立召回服务,代码复用程度极低。我们的平迁方案就是:把这些分散的召回逻辑统一抽象成可配置的策略,底层换成分布式的索引服务,同时将各路召回抽象成独立的DAG图,公共逻辑抽象成不同的算子,来实现召回编排的灵活性和底层代码的复用性,但召回的核心算法(协同过滤、内容标签匹配、热度排序等)基本保留。结果是架构简化了,性能提升了,但业务方无感知,推荐效果不受影响。

第二种,业务逻辑复杂到没人敢动,重写风险太大。有些系统运行了很多年,积累了大量的业务规则和边界case处理,这些规则可能都没有完整文档,散落在代码的各个角落里。如果贸然重写,很可能遗漏某些边界情况,上线后引发问题。
我们的特征服务就属于这种情况。线上跑着数千个特征,涉及用户画像、内容分析、实时行为等十几个数据源,每个特征的计算逻辑都不一样,有些还有复杂的时间窗口和聚合规则。如果重新设计这些特征,工作量巨大,而且很容易出错。所以我们选择平迁:把特征抽取的框架重构了(算子化、配置化),但每个特征的计算逻辑基本照搬过来,只做代码层面的重构和性能优化。这样既降低了风险,又保证了离在线特征的一致性。
原有的特征服务

分散式抽取
抽取逻辑保持不变优化系统的架构

集中式特征处理
第三种,时间紧迫,必须快速见效。如果业务方给的时间窗口很紧,没有足够的时间重新设计业务逻辑、做充分的AB实验验证,那就只能选择平迁。至少能先解决性能和稳定性的燃眉之急,业务逻辑的优化可以放到二期。
选择重建的三种情况:
第一种,业务逻辑本身就有问题,继续延续没有意义。有些系统当年设计时的业务假设已经不成立了,硬要保留老逻辑反而是包袱。比如最早推荐系统可能只考虑了图文推荐,后来要加视频、短视频、直播,如果还沿用老的图文推荐逻辑,就会很别扭。
我们的曝光排重服务就是典型案例。老系统的设计是“接口曝光”,只要内容被推荐接口返回就算曝光,不管用户有没有真正看到。这导致很多优质内容被误伤——明明用户没看到,系统却以为曝光过了,不再推荐。我们重建时彻底改了逻辑:引入“真实曝光”概念,只有用户真正看到了才算曝光,并且设计了优质内容的快速回收机制。这个改动大幅提升了用户体验,业务提效明显。

曝光服务流批一体新架构
第二种,老逻辑的技术债务太重,平迁的成本不比重建低。有些代码写得实在太乱,耦合严重、缺少抽象、充斥临时方案,想要在保留逻辑的前提下重构,改动面可能比重写还大。这种情况下,不如重新设计,至少新代码的质量有保证。
我们的索引平台就经历了部分重建。老的索引系统用的是自研的内存数据结构,代码有十几年历史,很多优化都是针对特定场景打的补丁,代码质量很差。我们评估后发现,如果要保留所有老逻辑,代码改动量超过80%,还不如重新设计。最后我们基于跳表重新实现了索引引擎,并且重新梳理了内容入库、更新、过滤的整套流程,将分散的单体索引改造成了统一的分布式索引。虽然工作量大,但新系统的可维护性和扩展性都好得多。

第三种,有明确的业务升级诉求,需要新能力支撑。如果业务方提出的新需求,老逻辑架构根本支撑不了,那就得重建。与其在老架构上硬凑,不如借这次机会彻底升级。
插件的推荐系统升级就有类似的问题。在进行插件场景系统优化的时候,我们首先对这个业务场景进行了抽象,首先该场景有别于传统的推荐系统以提升用户的消费指标为核心目标,插件作为拉新拉活的主战场,其核心目标重点在于拉新和促活。另外插件场景触达的用户量级是远高于端内的,同时绝大部分用户行为比较稀疏,不太活跃,因而对于计算的ROI要求会非常的苛刻。同时,由于插件能够触达海量用户,因而要同时传达我们产品的战略意图,形成用户心智(精品资讯:效率感、获得感以及共鸣感)。由于老系统在时效性上受限于原有的架构设计,无法保证资讯场景对于第一时间触达用户的需求,因而我们从很容易得出结论就是:在原有的系统上无法支撑业务进一步的迭代。

插件原有的离线架构
结合新的业务抽象升级之后的架构:

高能效半在线引擎架构
我们项目的实际选择
腾讯新闻推荐系统的重构,采用的是“以平迁为主,局部重建”的混合策略:
这个选择让我们既控制了风险(核心推荐逻辑不动,业务指标稳定),又抓住了机会(借重构解决了一些长期存在的业务痛点)。如果全部平迁,虽然风险最低,但很多问题还是解决不了;如果全部重建,8个月根本做不完,而且业务风险太大。
最后提醒一点:不要为了炫技术而选择重建。我见过不少团队,明明平迁就能解决问题,非要借重构的机会“尝试新技术”、“用最佳实践重新设计”。结果项目延期、风险失控,到最后连原来的稳定性都保不住。重构的目标是解决业务问题,不是展示技术能力。选平迁还是重建,永远要从ROI和风险的角度出发。
还有个经常被问到的问题:要不要“推倒重来”?我的答案是:99%的情况下都不要。完全推倒重来风险太大,而且很容易低估工作量。更稳妥的做法是“渐进式重构”——一边跑着一边换轮子。
确定要重构之后,下一个问题是:用什么策略?业界常见的有几种:大爆炸式重写、双轨并行、绞杀者模式。我们最终选择了绞杀者模式(Strangler Fig Pattern),这个名字来源于一种植物——绞杀榕,它会缠绕在宿主树上慢慢生长,最终取代宿主。
为什么选这个策略?因为它最符合我们的实际情况:业务不能停,风险要可控,还要能持续交付价值。
具体怎么做?分四个阶段。
第一阶段是“建代理层。我们在老系统前面加了一层API网关,所有请求都先打到网关,网关再根据路由规则决定转发到老系统还是新系统。刚开始100%的流量都给老系统,新系统处于待命状态。这个代理层是整个重构的“总开关”,后面所有的流量切换都通过它来控制。
第二阶段是“模块化拆分”。我们没有一上来就重构整个推荐链路,而是按照业务价值和技术难度,把系统拆成了几个独立模块:召回服务、排序服务、特征服务、策略服务。优先重构那些“收益大、风险小”的模块。比如我们第一个啃的是特征服务,因为它相对独立,出问题影响面小,而且新特征能直接提升推荐效果,能很快看到价值。
第三阶段是“逐步切流量”。每完成一个模块的重构,我们就通过网关把这部分流量切到新系统。切的过程也不是一步到位,而是用灰度发布:先切1%观察24小时,没问题再切5%,然后10%、50%,最后才是100%。每次切流量前,我们都会做压测和数据对比,确保新系统的性能和准确性都不低于老系统。
第四阶段是“下线老模块”。当某个模块的流量100%切到新系统,并且稳定运行了一个月以上,我们才会把老代码下线。这个过程虽然慢,但很安全。我们整个推荐系统的重构历时24个月,期间没有发生一次线上事故。
这里有个实战细节值得分享:数据对比怎么做?我们采用了“双写+异步校验”的方案。新旧系统同时处理同一个请求,但只返回老系统的结果给用户(保证业务不受影响),新系统的结果存到日志里。然后我们写了个离线任务,每天对比两边的结果,计算一致性指标和效果指标。只有当新系统的表现稳定优于老系统时,我们才会切真实流量。
绞杀者模式最大的好处是风险可控。每次只改一小块,出问题可以迅速回滚。而且整个过程中,业务方完全无感知,他们该提需求提需求,该看数据看数据。但这个模式也有成本:你要同时维护两套系统,还要额外开发网关和监控工具,资源投入会比较大。

图1:绞杀者模式实施流程。从建立代理层开始,通过模块化拆分和灰度发布,逐步用新系统替换老系统,最终安全下线旧模块。
重构最怕的就是翻车。我见过太多项目,前期规划得很好,中途出了问题,最后骑虎难下。所以从一开始,我们就把风险控制放在第一位。
第一道防线是测试。 但不是那种“改完代码跑个单测”的测试,而是系统化的测试体系。我们做了三层:
为了更好能够满足不同模块重构验证的需求,我们抽象出了一个通用的diff平台,能够针对不同模块的重构,进行请求的回放和结果的diff,来保证系统重构中的不变性约束。

第二道防线是监控告警。 重构期间,监控的密度要比平时高得多。我们重点盯这几个指标:接口响应时间、错误率、推荐效果指标(点击率、停留时长)、资源使用率。任何一个指标波动超过阈值,都会立即告警。我们设置的阈值很严格,比如响应时间只要比平时慢20%就告警,宁可误报也不能漏报。
印象最深的一次,我们刚切了5%流量到新的排序服务,半夜两点监控告警:响应时间突然涨了30%。值班同学立即回滚,第二天排查发现是新服务里一个特征现场的kv存储被打满了,原因是数据的过期时间设置不合理。如果不是监控及时发现,等到第二天切更大流量,问题就严重了。
第三道防线是回滚预案。 这点很关键但经常被忽略。很多团队做重构,只想着怎么往前推进,没想过万一出问题怎么退回来。我们的原则是:每次变更都要有明确的回滚方案,而且回滚操作要足够简单——最好是一键回滚。
我们在网关层做了配置开关,只要修改一个配置项,就能把流量切回老系统,延迟不超过10秒。数据库层面,我们也保留了向后兼容,新表结构能读老数据,老表结构也能读新数据(只是会丢失部分新字段)。这样即使真的要回滚,数据也不会乱。
还有一点常被低估的风险是团队协作。重构是个跨团队的事情,涉及研发、测试、运维、DBA、业务方等多个角色。我们当时就栽过跟头:有一次特征服务要上线,研发这边准备好了,但DBA那边还没扩容数据库,结果一上线就把数据库打爆了。
后来我们建立了“重构作战室”机制:每双周开一次同步会,所有相关方都要参加,提前暴露风险和依赖。每次重大变更前,都要拉一个checklist,确认每个环节都OK了才能动手。听起来有点繁琐,但确实避免了很多低级失误。
如果问我整个重构过程中哪个环节最难,我会毫不犹豫地说:数据迁移。代码可以重写,架构可以重新设计,但数据是企业的核心资产,容不得半点闪失。
我们遇到的挑战主要有三个:数据量大、不能停服(推荐系统24小时在线)、新旧特征格式兼容性的问题(新架构做了比较大的数据建模调整)。
最终我们采用了“五步走”的在线迁移方案:
第一步,全量+增量同步。 先用数据同步工具(我们用的是腾讯自研的DTS)把老库的存量数据全量同步到新库,然后通过订阅公共流水,实时将增量变更同步过去。这个阶段大概持续了一周,期间不影响线上业务。
第二步,双写。 改造应用层代码,所有写操作同时写老库和新库。为了不影响主链路性能,对新库的写入我们用的是异步方式,写失败了会进入重试队列。这个阶段持续了两周,主要是为了保证新库的数据是最新的。
第三步,数据校验。 这一步特别关键。我们写了个离线任务,每天凌晨对比老库和新库的数据,从三个维度校验:数量是否一致、关键字段的值是否一致、数据分布是否一致。刚开始一致性只有95%,经过两周的bug修复和数据补偿,最终稳定在99.9%以上。
第四步,切读。确认数据一致性OK之后,我们开始把读请求从老库切到新库。同样是灰度发布,先切1%,观察性能和业务指标,没问题再逐步放量。这个过程中,我们发现新库的一些查询比老库慢,原来是索引没建全,赶紧补上。
第五步,切写。等所有读流量都稳定切到新库了,最后才把写流量也切过去,停掉双写。至此,数据迁移才算真正完成。
这个过程中踩过不少坑。最大的一个坑是数据一致性校验。一开始我们只对比行数和部分字段,觉得差不多就行了。结果切流量时发现,有些关键字段(比如用户的兴趣标签)在新老系统里完全对不上,导致推荐效果出现波动。后来我们专门开发了一套完整的数据diff工具,能够精确定位到每一条不一致的记录,才把这个问题解决。
另一个坑是性能预估不足。我们在测试环境压测时,新库的性能表现很好。但切到线上后,发现在高峰期的QPS下,新库的响应时间明显变慢。原因是测试环境的数据量只有线上的十分之一,很多性能问题没有暴露出来。最后我们临时加了一层Redis缓存,才扛住了线上流量。
数据迁移没有捷径,只能稳扎稳打,步步为营。核心是做好校验,确保每一步都是可回退的。

图2:在线数据迁移的五步走方案。通过全量同步、双写、校验、切读、切写的精细化流程,确保数据平稳过渡且随时可回滚。
重构不只是换个技术栈那么简单,更重要的是要想清楚:什么样的架构才能支撑未来三到五年的业务发展?这个问题的答案,藏在一些经得起时间考验的架构原则里。
第一个原则:高内聚、低耦合。 这是老生常谈,但真正做到很难。我们在拆分服务时,判断标准是“职责单一”和“独立演进”。比如召回服务只负责从海量内容中筛选候选集,不关心排序逻辑;排序服务只负责打分排序,不关心召回策略。这样的好处是,将来要替换召回算法,不需要动排序服务的一行代码。
但低耦合不是零耦合。服务之间总要有交互,关键是把耦合控制在接口层面,而不是实现层面。我们定义了标准的服务接口和数据协议,各个服务只依赖接口,不依赖具体实现。这样即使某个服务要重构,只要接口不变,其他服务完全不受影响。
举个具体例子:老架构里,各个场景的推荐服务都是独立部署的,要闻频道一套、视频频道一套、二级频道又是一套,400多个场景对应300多个独立的物理池。每次改个公共逻辑,都要改几十个地方。新架构我们做了彻底的服务化拆分,召回、排序、特征、策略都是独立服务,通过统一的推荐中控来编排。现在要改个策略,只需要在策略平台上配置一下,分钟级生效,不需要重启任何服务。
第二个原则:拥抱变化,为扩展而设计。 推荐系统的特点是变化快——今天流行协同过滤,明天可能就要上深度学习模型。如果架构不支持灵活扩展,每次换策略都要改一堆代码,就没法快速响应业务需求。
我们的做法是引入“策略模式”。所有的召回策略、排序模型、特征计算逻辑,都抽象成可插拔的“策略”。要上新策略,只需要实现统一的接口,配置一下就能生效,不需要改核心框架。现在我们线上同时跑着十几套召回策略和五六个排序模型,可以通过配置灵活组合,而不是硬编码在代码里。
以索引平台为例。老架构是为每个场景单独维护一个物理索引池,要闻有要闻的池子、视频有视频的池子,总共300多个池子,运维成本巨大。新架构我们做了“去池化”改造:把物理池统一成两个大集群(视频和图文),但保留了逻辑池的概念,通过PoolTag来标识不同的内容集合。运营想看某个场景有哪些内容,只需要查PoolTag,所见即所得。这样既保证了运营的灵活性,又大幅降低了维护成本——索引服务数量从300+降到2个,成本下降95%。
第三个原则:在CAP之间做合理的权衡。 分布式系统绕不开CAP定理:一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者不可兼得。对于推荐系统,我们的选择是AP而不是CP——优先保证可用性。
具体来说,用户画像数据、内容特征这些,我们允许短时间内不一致,但不能因为某个节点挂了就整个服务不可用。所以我们采用了最终一致性的设计:通过消息队列异步同步数据,加上多级缓存保证服务高可用。这个选择的代价是,用户可能会在短时间内看到不太精准的推荐,但总比打开APP直接报错要好。
这个权衡是业务场景决定的。如果是支付系统,一致性就比可用性重要,宁可服务暂时不可用,也不能出现账务错误。架构设计没有银弹,关键是理解业务的核心诉求。
第四个原则:性能和成本的平衡。 推荐系统的性能优化是无止境的,但资源是有限的。我们在设计时,不追求极致性能,而是追求“够用的性能”。
比如排序环节,如果用最复杂的深度模型,响应时间可能要300ms,但推荐效果只比简单模型好3%。而业务方要求响应时间不超过100ms。怎么办?我们设计了“粗排+精排”的两阶段架构:粗排用快速模型从几千个候选里筛出几百个,精排再用复杂模型精确打分。这样既保证了效果,又控制了延迟。
成本也是类似的道理。我们在设计缓存策略时,不是把所有数据都缓存,而是根据访问频率分级:热点数据放内存缓存,次热数据放Redis,冷数据直接查数据库。这样用有限的缓存资源,覆盖了99%的请求,成本只有全量缓存的五分之一。
再举个具体数字:特征平台重构之前,因为架构设计不合理,离线特征抽取的Flink集群需要上千核CPU、4.3T内存,网络传输压力巨大。重构后我们做了三个优化:一是按场景设置namespace共享抽取流程,避免重复计算;二是采用SOS(腾讯自研的对象存储)中心化存储,避免特征在链路里流动;三是打通无量(腾讯的模型训练平台),面向模型抽取配置,避免无效抽取。优化完成后,CPU降到700+核,内存降到800G,网络压力下降90%,整体成本节省降幅70%。

新的特征链路
第五个原则:可观测性是架构的一部分。 这点经常被忽视,但其实很重要。一个好的架构,应该是“白盒”的,而不是“黑盒”的——你能清楚地看到系统内部发生了什么。
我们在设计时,从一开始就把监控、日志、链路追踪考虑进去了。每个服务都输出标准化的日志和指标,通过分布式追踪系统(我们用的是OpenTelemetry),可以看到一个请求从进来到返回的完整链路。这不仅方便排查问题,也能帮助我们持续优化性能。
我们还专门孵化了一个一站式分析平台叫“诊脉”,把日志分析、链路追踪、AB实验数据、业务指标监控全部打通。以前排查一个问题,要横跨多个工具,占用各个环节的研发人力,现在在诊脉上就能完成从发现问题到定位根因的全流程,Debug效率从小时级提升到分钟级。
这些原则听起来很抽象,但落到实处就是一个个具体的设计决策。架构设计的本质,是在各种约束条件下,找到当前场景的最优解。
复盘与反思
回头看这24个月的重构历程,结果是好的。我们先用数据说话:
稳定性方面,可用性从不足99%提升到99.99%,全年P5级以上事故数降到0。要闻、视频底层页、落地页、PUSH、插件等核心场景的成功率全部达到99.99%以上,抗住了多次热点大事件的冲击。
性能方面,核心链路响应时间下降了50%。索引视频集群平均耗时下降95%;图文集群下降75%。网关入口要闻推荐P99.9耗时优化到xx毫秒以下。
迭代效率方面,需求周期从月级缩减到周级,提升50%;全年支持AB实验数量超过4500次。内容实验效率从周级提升到天级,优化80%。索引策略修改从需要开发人力改代码,到运营自助配置分钟级生效。
成本方面,这个收益最直观。在线服务CPU核数下降70%;推荐中台成本下降58%;Redis成本下降70%。仅索引平台一项,服务数量从300+降到2个集群,亿次PV成本下降95%。
架构治理方面,代码仓库从200+合并成一个大仓,代码行数从300万行精简到30万行,下降90%;单测覆盖率从不足10%提升到60%;在线服务集群从200+缩减到40套左右,下降80%;特征和画像生产使用链路全部收归统一管理。
但如果重来一次,我会做哪些调整?
第一,更早地引入业务方。我们前期主要是技术团队自己在推进,业务方参与度不高。这导致有些设计没有充分考虑业务的实际需求,到后期又要调整。下次我会从一开始就让业务方深度参与,确保技术方案和业务目标对齐。
第二,预留更多缓冲时间。我们最初的计划是12个月完成重构,实际用了24个月。虽然整体项目的结果符合预期,但是中间几次为了赶进度,牺牲了一些代码质量和文档完善度。现在看来,宁可一开始就留足余量,也不要搞得团队疲于奔命。
第三,重视文档和知识沉淀。重构完成后,很多设计决策和踩坑经验都散落在各个人的脑子里,没有系统地整理出来。新同学加入后,上手还是很慢。如果当时能边做边记录,现在回过头看,这些文档的价值会很大。
第四,不要迁就老城区的技术债务。虽然我们在前面的陈述里面始终建议渐进式重构,但是我们同时要保有直面老城区技术债务的勇气,敢于亮剑。例如在微视外显推荐架构重构的过程中我们就走了几个月的弯路,低估了技术债务平迁治理的难度,在这种情况下我们要果断决策,建设一个新城区轻装上阵,避免过度内耗。
第五,不要试图使用合理的系统架构来主导组织架构的变更。这个原则似乎跟我们一开始提到康威定律有些矛盾,在业务场景里面需要管理者先了解合理的系统架构再配置相应的组织架构,然而实际情况却往往事与愿违,越是高层的管理者对于架构不合理性的判断会越有偏差,难以实现合理系统架构的规划,也就谈不上组织架构的优化。因此很多公司为了解决这个问题试行了TL机制,希望能够从技术层面给出合理系统架构的设计,进而推动组织架构的演进,但收效甚微。这种情况下组织治理就脱离了架构能力的评价,转而向结果导向倾斜,行就上不行就走。这可能是庞大组织的无奈之举。
第六,要时刻警惕X/Y问题(源自:陈皓)。这个原则的意思是,问题的定义远比解决方案更重要。我们解决问题的过程中,要先判断问题本身是否存在问题。我经历的很多项目中都会遇到类似的情形,大家以为要解决的问题是X,但是如果溯源到问题的提出方可能应该是Y,如果问题的定义有偏差,也就谈不上架构合理性了。
最后想说的是,系统重构是一场马拉松,不是百米冲刺。它考验的不仅是技术能力,更是项目管理、风险控制、团队协作的综合能力。没有完美的架构,只有在当下约束条件下的最优选择。做好规划,控制风险,小步快跑,这是我这次重构最大的收获。
跟大家分享一下我自己非常喜欢的两句古文“善战者无智名无勇功”,“履霜坚冰至”——一个好的架构师最大的能力不在于主导多么庞大的系统的重构,而是防患于未然,类似于中医里面的“治未病”,“事了拂衣去深藏功与名”,以此共勉。
如果你也在考虑重构,希望这篇文章能给你一些启发。欢迎在评论区分享你的经验和问题,我们一起交流。
-End-
原创作者|董道祥