本文以记录一次高并发程序的设计收获与问题反思。
今天好友分享来一篇文章,从报名拥堵到筛选前移:一次舰艇开放报名的流程错位,大体意思是广州某舰艇开放活动预约系统崩溃和官方抽签流程导致的公众吐槽。近日,各地舰艇开放活动相继开展,预约系统拥堵卡顿甚至崩溃成热点,恰好最近参与了一次舰艇预约程序的设计和落地,目前活动已结束,现记录一下这其中的收获和还需要进行的改进,同时也对一些细节进行分享。
首先,是云计算的应用,很多人一听说云,第一反应想到的是搞机房,把一堆服务器搞一块再开个ssh提供服务,特别是还沉浸在一些传统架构中的开发者,个别系统直接部署在单台服务器,这在高并发场景下是灾难性的。对上云的服务,最大的优势我认为并不是将一台本地服务器挪到了数据中心那么简单,而是通过云实现的动态扩缩容以达到性能、可用和成本的平衡。
这是云计算的初衷之一也是优势之一,贯穿云计算自始至今的发展,但是在各种五花八门的数据中心建设中,往往不太重视这方面的实现,而是更重视将服务器搞到一块的硬件工程,这在一些地方政府主导的数据中心建设中尤为明显,这些所谓的云我认为不能称作云计算,伴随来的就是各类各种用户的吐槽和巨大的运维压力。
如今k8s、Docker等集群和虚拟技术已经非常成熟,将数据中心集群化、虚拟化,在这基础上实现动态扩缩容并非难事,例如阿里云、腾讯云等我们国内的商业云平台实际上很早就走完了这一步,在此基础上更进一步还提供了severless服务,让开发者更注重业务实现而不是运维,但一些地方的数据中心建设似乎并不重视这点。
第二个要聊的是架构问题,在最底层上说到集群化基本是k8s和Docker的天下,这在上面已经说了很多,这一部分更想说的是在这上层如何更易用、更方面,让我们更进一步聚焦于业务。上面提到的severless+云数据库+云存储+前端托管是个不错的选择,我们的地方云计算中心能提供这样的服务就很值得竖个大拇指了,基于提供这类云服务的商业云之前我也做过很多项目,存在的主要问题是各类服务比较分散,在服务使用上需要分别考虑各类服务收费规则来计算项目成本,还是比较费力。
于是乎,各类base服务诞生了,将计算、存储、数据、托管甚至AI能力等打包为一个环境,商业云是以计算消耗的核时、存储用量、数据流量等等来计费,比如Firebase、Supabase、CloudBase,这类称为后端即服务(Backend-as-a-Service, BaaS)平台。Supabase在提供商业服务的同时,践行了雷锋精神,将整个架构开源了,使得我们可以方便的在自建集群上部署BaaS服务;同时,这类服务配套的前端SDK可以集成在各类主流前端框架中,使得在前端就可以进行身份认证并直接调用后端的数据库、存储等服务,而且还可以通过设计私有协议,实现全链路加密,具备了反爬能力,安全性也得到提升。
有人又要说了,“我不喜欢别人把饭给我喂到嘴里”,想更自由的使用一个弹性环境,空环境即可,不需要提供边缘函数、数据库等服务,有自己的想法。有需求的地方就有市场,这类服务我觉得比较好用的是Zeabur,号称AI运维工程师,当然我一般不会用AI去部署后端服务,总觉得不是那么可靠,这AI有蹭热度的嫌疑;这类DevOps环境可以结合git实现CI/CD,当然也可以通过Docker部署计算或者存储实例,如果你愿意的话还可以部署Supabase,变身为BaaS。
第三个回头聊聊手头刚结束的这个预约程序,这种预约程序在逻辑实现上没什么难度,业务流程非常单一,无非就是采集用户数据、对预约数据CRUD,棘手的问题在于如何处理高并发。这个问题处理不好,Nginx报502就很正常了,常用的处理思路也很简单,一种是做QPS流控防止后端资源全部耗尽或超出阈值,QPS上限后直接返回rate limit错误,前端配合提示用户“活动火爆、请重试”之类的提示,通过用户重试使用户抢入下一个服务周期;另一种思路是使用MQ队列,让消息排队处理,前端配合提示用户“排队中,稍等”之类的提示,让用户等待返回处理结果。如果不采取任何措施,高并发很容易耗尽后端计算或者内存资源,导致直接报502错误,即使动态扩容也存在底层物理资源全部耗尽的风险。综合来说,动态扩缩容是必要的,这要兼顾既能应对流量高峰、又能在流量低谷保持低成本运行;流控或排队机制也是必要的,防止不可预测的超高并发耗尽物理资源或影响集群其他服务。
预约前端跑在微信小程序上,无论是小程序还是公众号H5,使用TCB无疑是最简单、方便、安全的,毕竟都是一家的东西。标准版TCB环境QPS是500,鸡贼的腾子果然是不可能不限制你的QPS,想要更高就加钱吧,平衡一下业务成本,500就500吧,大不了用户多点几次,反正前端广大网友们抢购都养成了狂点的习惯。都限制我QPS了还搞啥MQ,直接流量从云函数推送到存储层,TCB文档型数据库根据调用次数收费,找了半天TCB帮助文档也不知道连接上限是啥,可能有连接池机制,反正是MongoDB封装的,Mongo热数据也会优先放入内存,在速度上应对这种预约场景问题不大。TCB的云函数是冷启动机制,根据你的并发量启动实例,首次启动是冷启动,但是实例运行结束后不会立即销毁,用以继续处理新的请求,直到所有冷启动的所有实例足以承载QPS限制的访问量上限,流量高峰过后再销毁不再使用的实例,通过这种方式实现了算力和内存的扩缩容。在这种机制下,前端用户感觉是第一次点击加载速度会略慢于多次后续点击,这是因为第一次点击是实例冷启动,为了降低高并发状态下这类不良体验,在实践中可以开启预置并发,本质就是提前启动一些实例,当然这也会产生一定成本。
最后对预约程序在实际生产环境中的表现聊几点,首先是拍摄身份证上传提示的识别失败重试问题,从后端日志来看,主要原因是引发了QPS流控,一个服务周期内服务用户的数量达到上限,前端接收到rate limit提示重试,这也是为什么有的用户多次重试后可以成功,这些重试用户抢入了下一个服务周期。再者是图片资源加载延迟问题,TCB的云存储实际是基于COS的存储桶+CDN分发,在这种高并发状态下,达到COS服务的带宽上限,这是种正常现象,可以通过将部分静态资源打包到前端解决,但是小程序对前端代码包有限制,在不影响业务流程下,没有太大必要。还有一个在预料中的错误是出现了“超卖”现象,这是因为预约程序并没有建立库存表,直接对预约信息表进行了aggregate,因为业务卡的不是很严格,一两个超卖情况在容忍范围内,没有特别处理,改进方法也很简单,可以再建立一张库存表,使用where判断库存后原子性扣减,扣减后再创建预约信息,两步操作使用事务绑定。
当然还有预料之外的问题,问题也比较低级,与个人开发没有充分测试有一定关系。首先比较严重的是,收集身份证照片仅使用了timestamp命名,即使毫秒级精度也无法承载高并发,导致数据核验有一人人证不符,说明有两人图片毫秒命名重复,还好没引发太大灾难,下步应考虑采用uuid或timestamp+random防止出现重名,小程序环境受限,没有浏览器原生api的crypto.randomUUID(),在小程序原生环境代码实现uuid生成需要random随机数,众所周知random是伪随机,小程序提供了安全随机数生成接口wx.getRandomValues(),可以考虑使用这个接口封装uuid生成函数,有前辈已经做过了WeChat-MiniProgram-UUIDv4-Generator,可以直接拿来用,但是要注意因为wx.getRandomValues()是异步的,导致这个Generator也是异步的,在调用时要通过async/await语法糖处理好同步异步问题。还有个更低级问题是没有对身份证号码进行校验,主要原因是我过于相信微信的ocr.idcard服务,没想到这个服务在识别不完整的情况下会按照正确json结构返回残缺数据,导致在数据清洗时有个别异常数据被清洗,与没有充分测试也有一定关系,算是吃一堑长一智了。最后有一风险点,是ocr调用次数预留了2000次,还好观望当时热度有点高,提前扩容到12000次,否则会更进一步加重前端的重试次数。
如有不当,请批评指正。