01 动机
长期以来,百度文库App服务端采用 PHP 作为主要开发语言,高效地支撑了业务迭代发展。随着平台流量的持续增长,服务端的负载越来越大逐渐接近系统瓶颈。为了提升系统的负载能力,我们采取了一些优化手段,其中最快最有效的方法是增加在线集群的实例数量。此外,还采用过lua开发项目,承接一些逻辑简单而访问量大的接口来分担负载。由于lua本身的一些局限性,不适合做复杂的业务逻辑。
伴随着IT技术的发展潮流,我们积极响应公司降本增效的号召,决定在2022年年中迁移并重构服务端技术栈。旨在升级技术架构,提升系统负载能力。
把握技术栈迁移和项目重构的时机是很难的一件事情,特别是成熟的团队要进行大的系统改动。如果没有出现真正的痛点,即使研发同学认为技术实现上已经出现诸多设计不合理和有风险的地方,往往并不被允许花大量时间去做技术项目。可一旦连业务人员(产品经理、销售、运营)也觉得系统功能需要升级的时候,比如在用户体验上,App文档搜索接口延迟比较长,产品同学认为如果首屏渲染能明显提速的话,对点击率、付费率都会有大幅的提升,但是研发这边基于老的技术栈已经难以做优化了,那么此刻就很适合做迁移重构。
恰逢其时,产品同学提出一些提升用户体验,同时适合项目重构的需求,比如:加速搜索结果页首屏渲染、新App首页,AIGC智能创作。这些大需求十分有利于迁移重构工作的启动,它们把迁移重构所需增加的额外人力占用降到最低。在已有的功能上做迁移重构,更快更稳定的接口响应带来流畅的用户体验,这有利于促进整体团队的okr目标达成。
回过头来,梳理下当时服务端基于php5.6的技术债务:
1、底层技术:语言版本老旧,特性落后,存在执行效率低,安全风险,资源浪费的缺陷;
2、开发质效:业务逻辑交叉耦合,大量废弃的接口和下线的业务逻辑,降低了代码可读性、可维护性,持续增加项目迭代的难度。
02 启动之前的状态
服务部署上,文库App的服务端部署方式是nginx+hhvm(HipHop VM 3.0.1;baidu version:1.1.6 (rel)),HHVM 是 Facebook 开发的高性能 PHP 虚拟机,是传统的nginx+php-fpm的一个性能优化版本。近年已经失去了hhvm原创团队的持续维护迭代,它支持的语法特性和执行效率相对落后,存在一定安全风险。
查看启动迁移之初的服务端实例用量,有赖于日常运维,首先确认在线服务的实例cpu、内存和磁盘使用率在合理的阈值内,排除了利用率较低导致资源浪费、利用率过高会有容灾风险的情况。在应用层,我们一共使用了数以千计的php5实例。
03 远景
重构的投入与回报并非呈线性关系。
—— 《领域驱动设计:软件核心复杂性应对之道》
直观的说,我们希望服务端升级能带来更少的代码,更稳定的系统,更高的质量效率,更佳的用户体验。这体现在下面几点:
1、技术升级:采用先进的语言框架,支撑项目高效迭代提供强劲底层引擎、安全性和成熟的应用生态;
2、改善设计:梳理代码逻辑,治理冗余,解决代码中的坏味道,构建高复用、低耦合、可扩展的业务架构;
3、降本增效:一方面底座升级,提升代码执行效率,降低平响,提升服务可用性、可观测性;一方面在运维实践上,合理分配容器实例的cpu,内存和磁盘的配额,优化资源效能。
服务端升级的成功与否,可以从两个方面来努力达成,分别是技术栈迁移和改善既有代码设计的重构。
04 做技术选型
我们不打算使用较为小众、生态孤立的语言作为文库App服务端的技术栈。同时参考兄弟团队的技术栈升级方向,最终进入技术选型决赛圈的是两种厂内框架,基于php7的odp3框架(Online Develop Platform)和基于go的gdp2框架(Go Develop Platform)。
选项一:PHP7框架和Phaster
PHP7框架是公司发布的在线业务开发平台,其提供了标准的webserver环境、标准php环境、AP框架、基础库、资源访问层、通用服务等组件,统一业务的逻辑和部署结构。框架的亮点在于Phaster。Phaster能让你使用PHP语言开发高性能的Http、Fastcgi、Nshead服务,进行高性能的RPC调用,以极低的成本实现业务代码并行化。
Phaster和其它业界框架的对比如下。
Phaster可以作为http server,也可以作为fastcgi server。相对传统nginx+cgi的方式,Phaster基于以上的能力实现数倍的性能提升。具备以下亮点:
1、传统的hhvm或者php-fpm处理请求的逻辑是,每一个请求在处理时,都要先初始化php上下文,请求结束时清理上下文,回收各种资源。而phaster在开启上下文复用的情况下,可以节省类加载,文件加载,初始化等过程耗费的时间。举个例子,如果你的接口每次都要读取一个大文件配置,可以把读取操作放到初始化文件里。在100个请求内,这个读取操作只执行一次就够了;
2、hhvm或php-fpm并不直接支持http协议,往往前面会加上nginx作为http服务器,两者之间通过fastcgi通信。而Phaster可以直接作为http服务器启动,减少一层nginx的处理转发;
3、协程的支持为IO密集型的业务场景,提供了高并发的基础。对于阻塞性的IO,可以放入协程里做,将阻塞变为非阻塞,在使用同步编程方案的同时,享受异步效果带来的IO性能提升。
值得一提的是,Go都支持这些能力。
选项二:Go框架
Go 语言是由谷歌于 2009 年发布,近几年伴随着云计算、微服务、分布式的发展而迅速崛起,跻身主流编程语言之列,和Java类似但不像Java那么啰嗦,它是一门静态的、强类型的、编译型编程语言,为并发而生,所以天生适用于并发编程(网络编程)。
GDP2( Go Develop Platform ) 框架是一个对厂内基础设施支持好,可扩展性好、易配置、易组装、易测试的 Go 开发框架。具备完善的 RPC Client 和 RPC Server 能力,以及配套的通用基础库,可以用来开发 API、Web 及后端服务等各种应用。具备以下亮点:
1、对厂内基础设施支持好;
2、可扩展性好、易配置、易组装;
3、易用性好、对测试友好 (易 mock,多种 testServer、testClient);
4、组件内部状态易观察 ;
5、全链路超时&流程控制机制,稳定性好 ;
6、厂内大规模应用,稳定可靠 (基本所有 Go 项目都有使用,共有几千项目使用)。
综合对比以上对两种框架的特点和落地的可行性等因素,最终我们更倾向于向GDP框架迁移。
05 进行重构的关键路径
做好技术选型后,我们就开始下一步的工作。和常见的web项目一样,文库App业务迭代速度快、任务重,难以保证有充足的人力长期投入到技术项目。所以,技术栈升级重构的前提是在保证业务需求不停的情况下进行,需要有持续重构的意识,往往采用『敏捷式迭代』。
5.1 敏捷式迭代
第一步,工作量预估。通过日志聚合分析,得出当下有流量的App接口路由(老项目很多接口没有流量,关联需求已下线)。实际操作下,发现刚好按照qps值从大到小排序的top 50的接口的流量占比达到了总流量的99%+,这也确定了接口迁移顺序的优先级。
第二步,制定策略。服务端技术栈go迁移的落地,本质上体现为由php承接的流量转为go承接,当所有流量都在go实例上运行,且对php项目无底层调用的依赖关系,即可认为升级完成。因此,不断扩大go实例集群在App服务端总的流量占比,就是我们迁移的工作目标。以此可以大概可以总结为两种方式:
1、根据业务需求,结合接口重要性和流量占比确定优先级,进行迁移;
2、新需求的代码实现和php项目不存在强依赖关系,直接在go项目开发。
有相当长的一段时间是处于php+go进行混合编程的共存状态。由于App的B/S架构特性,重构完的接口需要通过接入层网关做代理转发,切换服务端承接流量的具体应用层集群(php->go),让客户端保持path不变,从而实现App老版本的高可用。采取混合编程的思路在重构初期,可能会一些比较特殊的需求情况,比如:同一段业务逻辑,需要用go写一遍,用php写一遍,无疑增加了一定的工作量,当然这也是避免不了的。
在重构的时候避免走到一个误区:瀑布模型,一口气把整个项目都重构了。从时间、人力成本和稳定性上来讲,这种方式风险比较大,不推荐。综合来看接口粒度的分批进行重构,这样不管是内化的技术迭代,还是外化的业务影响,都是有明显感知的。用作实现流量迁移的方式更为合适。
5.2 配套golang的基础建设
区别于php项目的work flow,有以下几点不同。
1、脚手架:除了定义路由、逻辑分层、生成配置等框架属性外,go需要额外对协程进行封装,提供给研发同学一个开箱即用的脚手架。
2、发布:封装build逻辑,实现打包编译、环境变量管理。
3、部署:是整个二进制文件覆盖,需要重启服务,使用热重启模块,可以实现无损上线,以及更快的上线速度。
4、流量:CS架构的App的接口迁移需要接入层做路由重写,协调网关变更。
5、监控:日志分级,微服务间保持trace透传,各类日志落盘符合agent采集的格式规范。
5.3 写第一个接口
一方面,在开始技术栈迁移的时候,需要了解到go语言层面支持并发,可以很轻松的开发异步程序强类型语言。go是强类型的静态语言,编译时确定类型,不如PHP灵活,但是更严谨,更安全,可以在编译阶段检查出来隐藏的绝大多数问题。
△类型转换
另一方面,重构项目如何治理陈旧代码?概括的说,可以参考《重构-改善既有代码的设计》一书提出的 23 种代码坏味道,有针对性地对代码进行重构,驯服成整洁和易于阅读的代码。
把前期调研和迁移策略确定好了,实际的代码开发变得得心应手。在迁移老接口流量的时候,我们需要在新接口用go重新实现一遍,调用方式上完全等同老接口,包括path、method、验签、header规则、参数结构、响应结构、错误码。只有应用层上的虚拟域名不同。
5.4 质量保障
代码ready了,区别于php项目的常规测试流程,go不能绕过性能测试。因为我们写php几乎不需要关注GC和内存泄露,但是go需要,有时候手动测试和黑盒测试是OK的,但是到线上遇到有一定并发的业务场景,就会暴露问题,常常表现为实例的cpu或者内存利用率持续上涨,直至宕机。
应对go的内存泄露问题。一方面需要在测试流程中增加压测环节;一方面需要日常多关注一下监控仪表盘的实例资源利用率、接口平响、稳定性指标是否符合预期,因为有的隐藏bug即使压测也不能覆盖到。这时需要提升go服务的可观测性,以便及时发现风险。
Go质量保障能力全景矩阵如下:
构建线下质量保障能力:
构建线上质量保障能力:
5.5 流量迁移
如上图所示,go项目上线后,实际流量还在老项目承接。开始做流量迁移,用户流量首先到达接入层,在这一层我们根据不同的访问域名和路由,分流到不同的应用层load balancer ,为了兼容老版本的App,需要在域名路由不变更的情况下,完成流量迁移。在接入层网关做分流,把分流到php的规则应用到go应用层load balancer 上,就完成了流量迁移。注意,如果是非常核心的接口,我们需要进行灰度发布,可以采用nginx+lua的方式实现,或者采用著名的开源网关ApiSix、BFE项目,它们都支持灰度发布。
5.6 核心功能重构实践
这次重构比较突出的亮点,体现在百度文库App的全新首页和搜索结果页优化。
(1)定制化新首页
文库用户个性化需求较分散,希望通过将垂类用户内容需求共性抽象,对连续型特征且使用较高的内容进行提取,采用中心化集中推荐的方式,提高用户垂类内容结构化满足,进而提升用户留存率及续费意愿。重构了App首页的布局和内容展示。增加了个性化的『我的资料库』,『教学进度』,『推荐频道』,定制化展现文档榜单和文件夹榜单。
App新首页的技术方案是全新的,重构的动机来自"业务驱动",而非"质量驱动"。需求实现上,底层不依赖php老项目。所以直接在go项目开发上线,提供接口服务。这样上线后,go自然替换掉了php原本承接的首页流量。
△文库新首页
(2)搜索结果页优化
服务端这边主要重构对象是一个搜索接口,实际开发中,和产品沟通是否可以下线不要的tab列表和内化的推荐逻辑;清理多处已经下线的AB实验的业务逻辑,去掉已经推全AB实验的代码判断;优化文档排序算法,和产品、前端同学对齐当前必须的字段,去除冗余;善用协程优化串行逻辑。
结合前端去除懒加载代码,图片本地化,使用端能力缓存接口数据,搭建离线包服务等技术手段,搜索结果页优化取得了不错的成果。大幅降低搜索结果页的加载速度,安卓平均降低延迟41%;IOS平均降低延迟43% ,搜索结果点击率和成交的订单量也有一定提升。
【新老搜索结果页】AB实验时的白屏时长统计
06 目标达成
从2022年8月启动go迁移至今,接近完成App服务端的全部流量迁移工作。
1、技术迭代:得益于go语言特性先进、内存管理和丰富的生态,提升了代码执行效率、安全性和可观测性;通过梳理业务逻辑,治理冗余,清理代码中的坏味道,封装公共类等方法,提升质量效率,代码可读性和可维护性;
2、提升性能:一方面通过协程、通道技术改变同步阻塞的代码执行方式;另一方面,编译后的二进制文件执行效率远高于nginx+php-cgi的网络模型。平均减少了约30%的接口耗时,TP90减少了35%的耗时;
3、降本增效:得益于go语言高性能的特性,应用层实例的负载能力得到提升。流量迁移后,文库App服务端在线集群缩减了约50%的的实例数量。
07 思考与总结
1、手机App属于CS架构的应用,在迁移过程中要保证老版本Client可以使用服务;
2、在面对一个长期项目时,拆解目标是很重要的,快步试错即时反馈也是互联网思维的一部分;
3、迁移理论无损,但需要把风险同步pm同学,及时关注各业务指标,同时制定预案,保证可回滚的灵活性;
4、接口刚上线或者AB实验推全后,迁移的接口流量上升,要养成经常观察可用性仪表盘的习惯,及时处理http status异常的问题,避免风险扩大化为故障。
08 结语
知而不行,是为不知;行而不知,可以致知。
回想项目迁移重构的整个过程,最有意思的是做技术选型和讨论流量迁移具体实行方案的起步阶段,那时面对臃肿庞大的php单体项目如何进行迁移,是有些迷茫的。在实践的摸索过程中逐渐加深对项目的理解,通过所得的启发来推导制定下一步的行动,形成正向循环。希望本文的内容对大家的工作实践有所帮助。