微信序列号生成器架构设计及演变

  这时候假如我们的AllocSvr采用了主备容灾模型的话,事情就变得简单多了。我们可以在client配置文件里写:对于某个uid号段,要么是AllocSvrA加载,要么是AllocSvrB加载。Client端发起请求时,尽管Client端并不清楚AllocSvrA和AllocSvrB哪一台真正加载了目标uid号段,但是Client端可以先尝试给其中任意一台AllocSvr发请求,就算这次请求了错误的AllocSvr,那么就知道另外一台是正确的AllocSvr,再发起一次请求即可。

  也就是说,对于主备容灾模型,最多也只会浪费一次的试探请求来确定AllocSvr的服务状态,额外消耗少,编码也简单。可是,如果Svr端采用了其它复杂的容灾策略,那么基于静态配置的框架就很难去确定Svr端的服务状态:Svr发生状态变更,Client端无法确定应该向哪台Svr发起请求。这也是为什么一开始选择了主备容灾的原因之一。

  主备容灾的缺陷

  在我们的实际运营中,容灾1.0架构存在两个重大的不足:

  扩容、缩容非常麻烦

  一个Set的主备机都过载,无法使用其他Set的机器进行容灾

  在主备容灾中,Client和AllocSvr需要使用完全一致的配置文件。变更这个配置文件的时候,由于无法实现在同一时间更新给所有的Client和AllocSvr,因此需要非常复杂的人工操作来保证变更的正确性(包括需要使用iptables来做请求转发,具体的详情这里不做展开)。

  对于第二个问题,常见的方法是用一致性Hash算法替代主备,一个Set有多台机器,过载机器的请求被分摊到多台机器,容灾效果会更好。在seqsvr中使用类似一致性Hash的容灾策略也是可行的,只要Client端与仲裁服务都使用完全一样的一致性Hash算法,这样Client端可以启发式地去尝试,直到找到正确的AllocSvr。例如对于某个uid,仲裁服务会优先把它分配到AllocSvrA,如果AllocSvrA挂掉则分配到AllocSvrB,再不行分配到AllocSvrC。那么Client在访问AllocSvr时,按照AllocSvrA -> AllocSvrB -> AllocSvrC的顺序去访问,也能实现容灾的目的。但这种方法仍然没有克服前面主备容灾面临的配置文件变更的问题,运营起来也很麻烦。

  六、容灾2.0架构:嵌入式路由表容灾

  最后我们另辟蹊径,采用了一种不同的思路:既然Client端与AllocSvr存在路由状态不一致的问题,那么让AllocSvr把当前的路由状态传递给Client端,打破之前只能根据本地Client配置文件做路由决策的限制,从根本上解决这个问题。

  所以在2.0架构中,我们把AllocSvr的路由状态嵌入到Client请求sequence的响应包中,在不带来额外的资源消耗的情况下,实现了Client端与AllocSvr之间的路由状态一致。具体实现方案如下:

  seqsvr所有模块使用了统一的路由表,描述了uid号段到AllocSvr的全映射。这份路由表由仲裁服务根据AllocSvr的服务状态生成,写到StoreSvr中,由AllocSvr当作租约读出,最后在业务返回包里旁路给Client端。

物联网

  图9. 容灾2.0架构:动态号段迁移容灾

  把路由表嵌入到请求响应包看似很简单的架构变动,却是整个seqsvr容灾架构的技术奇点。利用它解决了路由状态不一致的问题后,可以实现一些以前不容易实现的特性。例如灵活的容灾策略,让所有机器都互为备机,在机器故障时,把故障机上的号段均匀地迁移到其它可用的AllocSvr上;还可以根据AllocSvr的负载情况,进行负载均衡,有效缓解AllocSvr请求不均的问题,大幅提升机器使用率。

  另外在运营上也得到了大幅简化。之前对机器进行运维操作有着繁杂的操作步骤,而新架构只需要更新路由即可轻松实现上线、下线、替换机器,不需要关心配置文件不一致的问题,避免了一些由于人工误操作引发的故障。

物联网

  图10. 机器故障号段迁移

  路由同步优化

  把路由表嵌入到取sequence的请求响应包中,那么会引入一个类似“先有鸡还是先有蛋”的哲学命题:没有路由表,怎么知道去哪台AllocSvr取路由表?另外,取sequence是一个超高频的请求,如何避免嵌入路由表带来的带宽消耗?

  这里通过在Client端内存缓存路由表以及路由版本号来解决,请求步骤如下: