上图所示,是微信红包的系统架构。包括微信统一接入层,下面是微信红包系统 API,包括发、抢、拆、查红包详情、查红包用户列表。再下面是封装微信红包关键业务的逻辑服务;最下面一层是数据存储层,微信红包最主要的数据是订单数据,包括发红包订单和拆红包订单两部分。业务逻辑和存储服务器之间是数据接入层,它最重要的作用是封装数据库操作的领域逻辑,使得业务逻辑服务不需要感知对 MySQL 的连接管理、性能、容灾等问题。
接下来是微信红包订单存储设计。上图是 2014 年微信红包存储层的模型。业务逻辑层请求数据层操作时,使用订单号 hash 路由到订单 SERVER。订单 SERVER 与每一组 MYSQL 数据库连接。
微信红包的订单号是在发红包时系统生成唯一标识,使用序列号服务生成唯一 ID,后面拼接三位微信红包的订单分库表的标识。所以,总共可以分一百个逻辑库,每个逻辑库含有十张表。一百个逻辑库均匀地分布到十组物理 DB,每组 DB 存十个逻辑库。
这个架构的最大问题是,一组 DB 故障时,会影响其他 DB。2014-2015 年期间,微信红包量涨得特别快,扩容速度跟不上业务增长速度。一组 DB 的性能出现瓶颈时,数据操作变慢, 拆红包的事务操作在 MYSQL 排队等待。由于所有十组 DB 机器与所有的订单 SERVER 连接,导致所有的订单 SERVER 都被拖住,从而影响红包整体的可用性。这个架构的另一个问题是扩容不方便,后面会介绍。
为解决 DB 间的相互影响,需要将 DB 间相互隔离,订单存储层 SET 化。SET 化指订单 DB 和订单接入 SERVER 垂直 stick 一起。业务逻辑层访问订单时,根据订单倒数第二、三位数字找到所属订单 SET,一个 SET 的请求不能路由到其他 SET。
找到对应的订单接入服务器之后,在服务器内的多个进程中找到指定进程,让同个红包的所有拆请求串行化。当一组 DB 出现故障,只会影响该组 DB 对应的 SERVER。
这里有一个问题,DB 故障拖住某些订单 SERVER,会不会也拖住更上层业务逻辑服务?业务逻辑层为什么不一起 SET 化?业务逻辑层承载了用户维度相关的业务操作,不可以按照订单的维度分业务逻辑,例如务逻辑层会请求用户的头像、昵称等,如果继续按照订单分业务逻辑,会导致跨地域调用。
微信红包系统采取的方案是,在订单 SERVER 服务端增加快速拒绝服务的能力。SERVER 主动监控 DB 的性能情况,DB 性能下降、自身的 CPU 使用升高,或者发现其他的监控维度超标时,订单 SERVER 直接向上层报错,不再去访问 DB,以此保证业务逻辑层的可用性。
一组 DB 故障不会影响整个系统的可用性。有影响的,只有十分之一,若扩成 100 组,影响便只有一百分之一。所以通过 SET 化得到的好处是,控制 DB 连接数、隔离故障影响和分流并发。
完成 SET 化之后,DB 故障仍对业务有十分之一影响,那么这十分之一该怎么解决?通过对系统进行研究分析之后,发现 DB 可以做到故障自愈。
如上图所示,所设尾号 90-99 的 SET 故障时,如果业务逻辑服务后续不再生成属于这个 SET 的订单,那后续的业务就可以逐渐恢复。
也就是在发生故障时,业务逻辑层发布一个版本,屏蔽故障号段的单号生成,就可以恢复业务。进一步想,除了人为发版本,有没有方法可以让 DB 故障时自动恢复?在 DB 故障导致业务失败时,业务逻辑层可获取到故障 DB 的号段,在发红包时,将这些故障的号段,换一个可用的号段就可恢复业务。订单号除了最后三位,前面的部分已能保证该红包唯一性,后面的数字只代表着分库表信息,故障时只需要将最后三位换另外一个 SET 便可自动恢复。
完成这个设计后,即使 DB 出现故障,业务的可用性也不会有影响。这里还有一点,新的发红包请求可避免 DB 故障的影响,但那些故障之前已发出未被领取的红包,红包消息已发送到微信群,单号已确定,拆红包时还是失败。对这种情况,由于不会有增量,采用正常的主备切换解决即可。