万亿级调用下的优雅——微信序列号生成器架构设计及演变(下)

版权声明:本文由曾钦松原创文章,转载请注明出处: 
文章原文连接:https://www.qcloud.com/community/article/201算法

来源:腾云阁 https://www.qcloud.com/community缓存

 

上一篇文章介绍了seqsvr的原型,这篇会简单地介绍下seqsvr容灾架构的演变。咱们知道,后台系统绝大部分状况下并无一种惟一的、完美的解决方案,一样的需求在不一样的环境背景下甚至有可能演化出两种大相径庭的架构。既然架构是多变的,那纯粹讲架构的意义并非特别大,这里也会讲下seqsvr容灾设计时的一些思考和权衡,但愿对你们有所帮助。微信

容灾设计

接下来咱们会介绍seqsvr的容灾架构。咱们知道,后台系统绝大部分状况下并无一种惟一的、完美的解决方案,一样的需求在不一样的环境背景下甚至有可能演化出两种大相径庭的架构。既然架构是多变的,那纯粹讲架构的意义并非特别大,期间也会讲下seqsvr容灾设计时的一些思考和权衡,但愿对你们有所帮助。架构

seqsvr的容灾模型在五年中进行过一次比较大的重构,提高了可用性、机器利用率等方面。其中无论是重构前仍是重构后的架构,seqsvr一直遵循着两条架构设计原则:负载均衡

  1. 保持自身架构简单框架

  2. 避免对外部模块的强依赖运维

这两点都是基于seqsvr可靠性考虑的,毕竟seqsvr是一个与整个微信服务端正常运行息息相关的模块。按照咱们对这个世界的认识,系统的复杂度每每是跟可靠性成反比的,想获得一个可靠的系统一个关键点就是要把它作简单。相信你们身边都有一些这样的例子,设计方案里有不少高大上、复杂的东西,同时也总能看到他们在默默地填一些高大上的坑。固然简单的系统不意味着粗制滥造,咱们要作的是理出最核心的点,而后在知足这些核心点的基础上,针对性地提出一个足够简单的解决方案。异步

那么,seqsvr最核心的点是什么呢?每一个uid的sequence申请要递增不回退。这里咱们发现,若是seqsvr知足这么一个约束:任意时刻任意uid有且仅有一台AllocSvr提供服务,就能够比较容易地实现sequence递增不回退的要求。

图5. 两台AllocSvr服务同个uid形成sequence回退。Client读取到的sequence序列为10一、20一、102优化

但也因为这个约束,多台AllocSvr同时服务同一个号段的多主机模型在这里就不适用了。咱们只能采用单点服务的模式,当某台AllocSvr发生服务不可用时,将该机服务的uid段切换到其它机器来实现容灾。这里须要引入一个仲裁服务,探测AllocSvr的服务状态,决定每一个uid段由哪台AllocSvr加载。出于可靠性的考虑,仲裁模块并不直接操做AllocSvr,而是将加载配置写到StoreSvr持久化,而后AllocSvr按期访问StoreSvr读取最新的加载配置,决定本身的加载状态。

图6. 号段迁移示意。经过更新加载配置把0~2号段从AllocSvrA迁移到AllocSvrBui

同时,为了不失联AllocSvr提供错误的服务,返回脏数据,AllocSvr须要跟StoreSvr保持租约。这个租约机制由如下两个条件组成:

  1. 租约失效:AllocSvr N秒内没法从StoreSvr读取加载配置时,AllocSvr中止服务

  2. 租约生效:AllocSvr读取到新的加载配置后,当即卸载须要卸载的号段,须要加载的新号段等待N秒后提供服务

    图7. 租约机制。AllocSvrB严格保证在AllocSvrA中止服务后提供服务

这两个条件保证了切换时,新AllocSvr确定在旧AllocSvr下线后才开始提供服务。但这种租约机制也会形成切换的号段存在小段时间的不可服务,不过因为微信后台逻辑层存在重试机制及异步重试队列,小段时间的不可服务是用户无感知的,并且出现租约失效、切换是小几率事件,总体上是能够接受的。

到此讲了AllocSvr容灾切换的基本原理,接下来会介绍整个seqsvr架构容灾架构的演变

容灾1.0架构:主备容灾

第一版本的seqsvr采用了主机+冷备机容灾模式:全量的uid空间均匀分红N个Section,连续的若干个Section组成了一个Set,每一个Set都有一主一备两台AllocSvr。正常状况下只有主机提供服务;在主机出故障时,仲裁服务切换主备,原来的主机下线变成备机,原备机变成主机后加载uid号段提供服务。

图8. 容灾1.0架构:主备容灾

可能看到前文的叙述,有些同窗已经想到这种容灾架构。一主机一备机的模型设计简单,而且具备不错的可用性——毕竟主备两台机器同时不可用的几率极低,相信不少后台系统也采用了相似的容灾策略。

设计权衡

主备容灾存在一些明显的缺陷,好比备机闲置致使有一半的空闲机器;好比主备切换的时候,备机在瞬间要接受主机全部的请求,容易致使备机过载。既然一主一备容灾存在这样的问题,为何一开始还要采用这种容灾模型?事实上,架构的选择每每跟当时的背景有关,seqsvr诞生于微信发展初期,也正是微信快速扩张的时候,选择一主一备容灾模型是出于如下的考虑:

  1. 架构简单,能够快速开发

  2. 机器数少,机器冗余不是主要问题

  3. Client端更新AllocSvr的路由状态很容易实现

前两点好懂,人力、机器都不如时间宝贵。而第三点比较有意思,下面展开讲下

微信后台绝大部分模块使用了一个自研的RPC框架,seqsvr也不例外。在这个RPC框架里,调用端读取本地机器的client配置文件,决定去哪台服务端调用。这种模型对于无状态的服务端,是很好用的,也很方便实现容灾。咱们能够在client配置文件里面写“对于号段x,能够去SvrA、SvrB、SvrC三台机器的任意一台访问”,实现三主机容灾。

但在seqsvr里,AllocSvr是预分配中间层,并非无状态的。而前面咱们提到,AllocSvr加载哪些uid号段,是由保存在StoreSvr的加载配置决定的。那么这时候就尴尬了,业务想要申请某个uid的sequence,Client端其实并不清楚具体去哪台AllocSvr访问,client配置文件只会跟它说“AllocSvrA、AllocSvrB…这堆机器的某一台会有你想要的sequence”。换句话讲,原来负责提供服务的AllocSvrA故障,仲裁服务决定由AllocSvrC来替代AllocSvrA提供服务,Client要如何获知这个路由信息的变动?

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

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

主备容灾的缺陷

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

  1. 扩容、缩容很是麻烦

  2. 一个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端内存缓存路由表以及路由版本号来解决,请求步骤以下:

  1. Client根据本地共享内存缓存的路由表,选择对应的AllocSvr;若是路由表不存在,随机选择一台AllocSvr

  2. 对选中的AllocSvr发起请求,请求带上本地路由表的版本号

  3. AllocSvr收到请求,除了处理sequence逻辑外,判断Client带上版本号是否最新,若是是旧版则在响应包中附上最新的路由表

  4. Client收到响应包,除了处理sequence逻辑外,判断响应包是否带有新路由表。若是有,更新本地路由表,并决策是否返回第1步重试

基于以上的请求步骤,在本地路由表失效的时候,使用少许的重试即可以拉到正确的路由,正常提供服务。

总结

到此把seqsvr的架构设计和演变基本讲完了,正是如此简单优雅的模型,为微信的其它模块提供了一种简单可靠的一致性解决方案,支撑着微信五年来的高速发展,相信在可预见的将来仍然会发挥着重要的做用。

相关文章
相关标签/搜索