喜茶公司自建互联网产品技术中心以负责 IT 管理、技术产品研发和数字营销,一直都是公司战略部署的一部分,可以说非常具有挑战性和前瞻性。在我们大多数人的印象中,奶茶店无非就是做茶饮生意,与外卖平台相结合,但在新中式茶饮市场,数字化营销却成了长久布局。
自 2019 年 12 月新型冠状病毒首次被发现以来,来自各行业的群体都受到不同程度的影响,喜茶互联网产品技术中心在这一段时间里,不间断地利用线上渠道为公司线下门店提供主力支持,公司的互联网化和数字化作为新式茶饮的发力点在此期间更凸显出其前瞻性,而且我们还在不断地投入资源和精力,不断地探索和完善。
距离上一篇博客《喜茶基于 Spring Cloud 和 Kubernetes 构建云原始应用的落地 》已有半年之久,新架构已落地完毕,重构之路也一直在进行中。这篇博客主要介绍一下我们点单系统的重构历程。
为什么要重构整个系统?
我们的点单系统系统最初是由 PHP 语言编写的。因为 PHP 语言的特性,实现业务需求极其快速,这点无疑在互联网事业部刚起步时如虎添翼。最极限的速度就是业内常调侃的“这个需求很简单,怎么实现我不管,明天上线”这种速度,当时我们经常做“需求不隔夜”这种事情,也一度让我们陷入到“敏捷开发”的幻觉中,其实那时我们更像是在“完成公司的指令”,虽然一片和谐欢乐,实则少了那么一点灵魂。
很快,我们的系统迅速膨胀,随着曝光度不断提高,QPS 陡增,数据剧增,此时,我们相当成功。
也很快,在同一年内,我们的会员数据增长到接近两千万,订单数据增长到接近一亿,点单系统开始出现一些压力,我们还没有来得及反应过来,就被现实从幻觉中拽了出来,并摁在地上摩擦,我们甚至不是主动发现这些数据增长给系统带来的压力。在我们开始醒悟过来,到开始着手进行优化系统,之间仅差三个月左右的时间,大概是在 2018 年第四季度,系统会在公司发互动推文时、QPS 突发情况下出现间歇性奔溃的现象,一度因为系统崩溃而上微博热搜,说起来真是一把辛酸泪。
一开始我们并没有考虑重构整个系统,我们想通过优化的手段,用最低的成本解决根本的问题。在全方位检查整个系统,结合出现崩溃的存档日志和代码,发现有几个瓶颈无法通过优化得到根本上的解决,因为是由 PHP 语言特性决定的,比如数据库连接池(也可能是我们对 PHP 技术的研究还过于浅薄,欢迎指正!)。
先简单说一下我们当时的架构情况,以更好地还原当时遇到的问题和解决方案。
当时,我们的点单系统由两个基于 laravel 的单体 PHP 应用组成,member-server 负责会员相关业务,sale-server 负责点单相关业务,应用以虚拟机 VM 的方式部署在公有云上,应用上层是云厂商 LB。虽然两个应用负责不同业务,却共用同一个数据库,数据库里面的表耦合度非常高。
从这些信息可以看出架构非常的简陋,虽然可以集群部署,也支持服务级别的横向扩容,但周边配套几乎空白:数据库层面业务故障隔离性差,没有用于异步处理场景的消息中间件,没有流量保护,没有监控和告警等等。
缺乏周边支持的集群是非常乏力的,如同虚胖。每当流量升高的时候,数据库里面的几张大数据量表轮流着拖垮两个应用,因为耦合度太高,故障没法隔离,业务又没有降级处理,很多本可以用异步方式处理的业务没有用正确的方式实现,流量也没法限制,最终系统雪崩。
当时云厂商提供的 LB 还处于开发阶段,比较原始,功能还不完善,也不支持限流保护,我们考虑过自己加一层比如 Nginx 之类的网关做 API 限流,但是,虚拟机扩容和缩容机制是由云厂商提供的,也没有提供 API 可以和 Nginx 做整合,也就是说需要放弃自动扩容和缩容机制,改用手工去扩容和缩容虚拟机,然后添加信息到 Nginx 的配置才能达到限流效果。如此折腾也只能做到盲目限流,没办法做到业务级别的流量控制,如果在流量高的时候,有些人下单成功了,但是因为盲目限流,极有可能导致这些人没办法再次进入系统完成剩下的业务,这种限流保护形同虚设。
每次系统崩溃,由于没有监控和告警机制来协助诊断系统和恢复系统,运维和开发的同学都是忙得团团转,像热锅里的蚂蚁,但解决问题的效果却几乎为零,更别说触及到根本问题的源头,只能说解决问题的态度很好。整个系统像个黑盒子,仅有的监控和告警都是买云厂商产品自带的零零散散几个,远远不够使用,我们完全看不到自己的应用内部都在干什么,只知道日志一直打出来,有时甚至干脆都不打日志了,事后分析才发现都阻塞在里面了。
这些大概就是当时我们遇到的主要问题。既然能找到问题,就能想到办法去解决他们,关键看问题的难易程度和轻重缓急。
这其中还有一些其他问题是由 PHP 语言特点导致的,PHP 总是在接收到一个新请求时新建一个进程去处理它,每次请求结束,都会释放掉执行中建立和使用的所有资源。在高并发下,我们并不希望所有的资源总是被一次次地新建然后又销毁掉,池化技术在高并发场景下是必不可少的。如此一来,寻找 PHP 池化技术方案,或者替代编程语言就顺理成章。
在寻找解决方案的过程中,我们确实还对旧系统寄予厚望,花了相当大的力气,做了很多代码优化和预备方案,比如:fpm 参数调优、容器化、流量保护、降级、监控、引入中间件等等。最终发现,符合我们预期的方案仅剩下一个读写分离!这点让我们自己都感到有些失望,也有一点慌,因为这意味着系统需要进行重构才行,重构又分换语言重构和继续使用 PHP 重构,还得再评估一番。总的这一阶段耗时三个月左右,收效甚微,眼看着系统快不行了。
重构相对优化成本自然高太多了,最致命的一点是前面我提到的 -- 我们陷入“敏捷开发”的幻觉中,其实我们只是在进行口口相传的需求和代码实现,什么文档、设计都是空白的。这种处境,对于任何系统重构来讲,没有文档描述,仅靠阅读代码进行重构,任务是非常艰巨的。事实就摆在面前,无论如何,时间很紧迫,没有太多时间可以浪费,考虑到成本和未来布局,最终还是定下了解决方案:Spring Cloud 和 Kubernetes。我们不得不大胆假设、谨慎行动,说是有魄力也好,说是大胆鲁莽也行,我们还是成功落地了新架构,并成功过渡到新架构。现在新架构也已经撑起了业务几乎全部流量压力。
对于 PHP 旧系统,并非成也萧何败也萧何。她依然在我们发展的历程上留下了辉煌的一笔,只是我们需要走得更远。这就是为什么要重构整个系统的根本原因。
重构中新架构的中流砥柱
我们没有预留一年半载的时间出来专门做系统重构,重构过程中做到了新业务开发不暂停。这全归功于我们设计的过渡策略。
这里说明一下我们是怎么过渡到新架构的。如下图所示,左边是我们的旧系统,右边是我们的过渡架构。
可以看出来,即使是在过渡阶段,两个 PHP 应用依然保持不动。重构初期,我们想过把两个 PHP 应用也纳入到 base-gateway
网关的保护之下,这样旧系统就有了一层保护,能够为重构争取一些时间,在验证后发现,PHP 的请求参数有一些是比较特殊、不符合 HTTP 标准的,在经过网关转发之后,参数会丢失,只好作罢。真的是屋漏偏逢连夜雨,船迟又遇打头风。
再后来,我们制定了以下几个重构策略:
-
新架构实现所有新业务。
为了快速验证新架构的可行性,我们在初期挑选了一些非关键的业务,在新架构上实现,通过观察架构和业务运行状况,一阶段后才开始全面铺开。这一验证阶段进展非常快速。
-
新架构分阶段快速重写 PHP 旧业务。
重写旧业务并没有涉及到重构工作,因为那样子做的话进度会变得很缓慢,也就说是这一阶段 PHP 旧业务代码怎么实现的,Java 重写一遍,当然,其中很多明显不合理的设计和实现是需要同时矫正过来的。计划大概分四个季度完成旧业务的全部重写,按照电商三个常规领域:会员、商品、订单,顺序完成重写。
为什么我们这一步不和重构一起做了?原因在于,PHP 旧系统压力很大,已经接近没办法提供服务的地步,受限于 PHP 语言特点,已经很难再做出可观的优化,与其花力气没效果,不如换一种大胆的做法,风险相当,成效的希望更大。这一策略可以说需要的是
“快”
,否则,等到整个系统完全死掉了,再好的重构也变得毫无意义。而且也刚好提前验证新架构是否符合我们的期望。 -
新旧架构共用同一个数据库、新旧架构之间不互相调用。
为了配合快速重写策略,加快代码的重写速度,我们推迟了数据库层面的重构,新旧系统共用一个数据库,通过不同的数据库账号分配不同的数据库资源,从而做到简陋的资源隔离和故障隔离效果。
为了后台管理那些重量级的数据库操作接口不要影响到用户端使用,我们专门拆出一些服务负责后台管理的数据库读操作,使用数据库的从库来提供大批量的数据读操作,从而隔离后台管理和用户端的资源和操作影响。
为了进一步稳固新旧两个系统的稳定性,严格限制他们之间的调用链。如果出现需要互相调用的情况,则在新架构里面实现旧架构的那些所需接口和功能,用这种方式避开调用链混乱,顺便把旧业务也重写了。虽然在时间上出现了紧张情况,偶尔对正常业务开发产生了延期影响,但大敌当前,还是情有可原。
-
旧业务重构。
最后就是旧业务重构,相对于系统重构,业务的重构优先级还不算最高,但也很紧迫。在我们的系统里面有很多业务存在逻辑 bug 或者不合理的地方,如果重新去梳理这些业务,会发现,进了一个业务流程后再也没办法走出来,这些有问题的业务直接产生了绵绵不断的客户投诉问题。
-
新架构演进。
至此,新架构已经是中流砥柱。但还不够,我们解决了燃眉之急,接下来就是要防止同样的问题再次出现,聪明的人不应该犯同样的错误。演进架构是一个长远之计,打一开始新架构就是采用微服务模式,全都是采用标准的、规范的方式来实现的,所以,新架构里面的每一个部分都是可插拔、可替换的,执行难度和成本都不是很大。其中数据库按业务拆分和后续业务内再拆分是当务之急。
重构过程中遇到很多很繁琐的问题,重构策略基本是不会变了,只是战术不得不随时随地调整和适配,需要考虑的细节也很多,我们一向保持谨慎态度,对事情的轻重缓急保持清醒的认识。比如:先把所有 PHP 旧系统接口进行分类,分为后台管理接口和客户端接口。对于后台管理接口,都是在保证功能没问题的情况下不考虑接口兼容的问题,进行大批量的重写,因为都是在内部使用,所以对 bug 敏感度没那么高,修复及时性要求也没那么高,实在不行执行回退方案即可;对于客户端的接口,则采取小批量迁移,小步快跑的战术,有时是一个一个接口迁移,为的是保持影响面可控,另外,在无法做到接口兼容的情况下,采取开辟新接口的方式来实现迁移工作。我们甚至无需启用蓝绿发布之类的运维技术,因为在类似我们的这种新旧系统过渡场景下根本没有用武之地。
新架构在周边配套和中间件上做了充分的准备,流程全部采用自动化、规范化和标准化。得益于 Spring Boot 和 Spring Cloud 的超级内置功能集合,使得我们能快速构建出企业级的应用,新架构的每一个点、每一个面都是可预知、可监控、可告警;也得益于 Kubernetes 的云厂商无关特性,使得我们构建的应用能不和特定云厂商绑定,从而能做更长远的海外部署布局和沉淀。其实我们也一直在进行技术中台的布局,为的是下一阶段业务重构能更方便、快速、有序和稳定地进行。
重构后新架构的沉淀和畅想
新架构的示意图如下,service 层的服务为业务聚合层,center 层的服务为业务中台层。service 层和 center 层之间偶尔会加入一层非必需的业务网关服务,我们已经开始使用 center 层的服务去操作数据库和对接第三方,从而从技术层面上为业务中台提供基本的技术沉淀工作。
我们现在开始设想未来的样子。架构重构之后,需要开始对业务进行重构,以便能够不断的提炼出公共业务和复杂的业务,将这些业务下沉到一个稳定的位置,慢慢形成业务中台,让业务中台能更好地使用技术中台。再往后便能沉淀形成技术和业务相结合的数据中台。那么,在喜茶这些中台该如何被具体地实现出来呢?我们后续的博客再一一来揭晓。
-- 2020.06.21 完