本文属于专栏《构建工业级QPS百万级服务》
继续上篇《QPS百万级的有状态服务实践》01 - 存储选型实践。如图1架构,我们已经解决了数据生产的问题。
图1
但是我们的服务已经在运行了,并实时处理大量的请求,我们如何把内存中的数据版本更新呢。直接加载数据更新内存肯定是不行,因为如果来了一个请求,想查看4月30日,到5月2日中间的节假日数量,而这个时候五一假期从0430-0502更正到0501-0503,而我们的算法是遍历0420到0502每一天是否是节假日。那么在遍历到0501时,数据发生了替换,由于之前已经判断了0430是节假日,之后又认为0503也是节假日,则最终会认为0430-0503都是节假日,则错误地返回了4天。或者同时来了多份需要更新的数据,则请求返回更难预期。
任何一个请求在处理过程中,不能更新依赖数据版本。业界常见的解决办法为冷启动和热更新。冷启动可以理解为停车-换轮胎-开车,热更新是开着车换轮胎。
冷启动大部分的人都接触过,比如游戏需要退出重启更新,银行会在某些时段更新系统,并停止访问。对于我们的系统,冷启动可以是提前告知用户,凌晨2:00-4:00,不提供服务。然后关闭服务,重启应用加载最新的节假日依赖数据。总的来说,冷启动适合服务变化大,或者服务运行时兼容数据更新成本很大的情况。从代码逻辑层面,冷启动不会增加复杂度,如图2,只要关闭程序,更新节假日数据就行了。更新的方式可以是用新的数据替换之前目录下的数据。
图2
热更新,最大的优势则是对用户是无感,但是它增加了程序的复杂度。热更新的逻辑则如图3。数据每次使用时先在步骤1上读锁,防止读的过程中数据更新,然后步骤2使用数据,使用完之后在步骤3释放读锁。同时程序有另外一个专门用于数据更新的线程,当发现有数据时,会在步骤4新开辟一片内存,存储新版数据B,然后在步骤5上写锁,防止数据更新的过程中数据被读取,然后在步骤6,替换数据,在步骤7释放写锁,这样之后的请求读取的都是数据版本B了。数据更新时则上写锁完成数据替换。当然这里还有优化的空间,就是减少读锁的获取时间。比如上读锁之后,直接获取数据版本A的指针,然后释放锁。更新时,不是替换数据的内存,而是替换指针的内存。但是本质思想和图3没有区别,这里就不再展开。
图3
那业务容器如何发现数据更新呢,对于冷启动时,我们可以写一个脚本,逻辑是“关闭进程,覆盖节假日数据”。对于热更新,有两种方式,一种是服务定期轮询,另一种是数据准备好后主动触发+服务重启时取数据。
定期轮询很简单,每分钟都去oss指定目录下遍历,看看有没有比内存中更新的数据。如果有,就下载数据并更新。
主动触发看似简单,只要数据生产的程序在生产完之后,通过http告知我们的业务容器就行了。但是我们的服务器有很多台,并且有的机器可能正在重启或者置换,ip也会变化,这个时候nginx则不够用了,不仅需要支持更复杂规则的负载均衡服务器,也会降低负载均衡的效率,所以这不是一个好的办法。我们的一般解决办法,是增加一个能把消息持久化,并发送给所有机器的工具,我们把它叫做消息队列中间件。于是我们的架构又升级到图4了。
图4
截止目前为止,我们的架构已经可以支持节假日依赖数据更新,并更新过程对用户无感了。并且我们通过消息队列还能在节假日数据更新的第一时间,让业务容器感知并更新数据。虽然目前还有数据一致性的问题。但是下一部分,我会先详细说一下消息队列相关经验,因为它太重要,太常见了。