• 欢迎光临~

微信的消息序列号生成实践

开发技术 开发技术 2022-08-18 次浏览

学习一下微信是怎么搞定序列号的。

从seqsvr申请的、用作数据版本号的sequence,具有两种基本的性质:

1.  递增的64位整型变量

2. 每给用户有自己独立的64位sequence 空间。 (每个用户哦)

 

具体的技术架构原型:

  不考虑seqsvr具体架构的话,它应该是一个巨大的64位数组,每个用户独占一个, 用户来申请的话,只需要将用户当前的seq+1,存回数组,并返回给用户。

  简单么? 简单,那如果千万级的访问量呢。

  微信的消息序列号生成实践

 

 

此时放低要求,只要求递增,不要求连续了。

实现方案:

  1. 内存中储存最近一个分配出去的sequence:cur_seq,以及分配上限:max_seq;

  2. 分配sequence时,将cur_seq++,并与分配上限max_seq比较:如果cur_seq > max_seq, 将分配上限提升一个步长 max_seq + = step,并持久化max_seq;

  3. 重启时,读出持久化的max_seq,赋值给cur_seq。

  通过一个预分配sequence的中间层,在保证sequence不回退的前提下,大幅提升了sequence的性能。实际应用中每次提升的步长为10000,那么持久化的次数

  降低10000倍,处于可接受范围。在正常运作时分配出去的sequence是顺序递增的,只有机器重启时,跳跃大小取决于步长。

  

  重启时需要读取大量的max_seq数据加载到内存中。  以目前uid上线 2的32个,一个max_seq 8bytes的空间,数据大小一共为32GB,从硬盘加载需要不少时间。

  处于可靠性的考虑,需要一个可靠存储系统来保存max_seq数据,重启时通过网络从该可靠存储系统加载数据。如果max_seq数据过大的话,会导致重启时在数据传输花费时间过长,造成一段时间服务不可用。

  分号段共享存储:

  uid相邻一段用户属于一个号段,同个号段内的用户共享一个max_seq, 这样大幅减少了max_seq数据的大小,降低了IO次数。

  微信的消息序列号生成实践

 

 

   一个sequsvr 中一个Section 包含10万个uid,max_seq 数据只有300+KB。

 

  工程实现:

  1. 把存储层和缓存中间层分成两个模块StoreSvr及AllocSvr。 

      StoreSvr 为存储层,利用多机NRW策略来保证数据持久化不丢失;(不丢失,NRW)

      AllocSvr 则是缓存中间层,部署于多台机器,每台AllocSvr负责若干号段sequence分配

  2. 整个系统按uid 范围进行分Set, 每个Set都是一个完整的、独立的StoreSvr + AllocSvr子系统。

      分Set 目的是为了做灾难管理,一个Set出现故障只会影响该Set内的用户。

 

seqsvr最核心的点是什么?  每个uid的sequence申请要递增不回退。  如果seqsvr满足这么一个约束:

  任意时刻只有一台AllocSvr提供服务,就可以比较容易地实现sequence递增不回退的要求。只能采用单点服务的模式

  微信的消息序列号生成实践

  当某台AllocSvr不可用时,需要将该机服务的uid段切换到其他机器来实现容灾。这里需要引入一个仲裁服务,探测AllocSvr的服务状态,决定每个uid段由哪台AllocSvr加载。

  出于可靠性的考虑,仲裁模块并不直接操作AllocSvr ,而是将加载配置写到StoreSvr持久化,然后AllocSvr定期访问StoreSvr读取最新的加载配置,决定自己的加载状态。

  微信的消息序列号生成实践

 

   为了失去联系的AllocSvr提供错误服务,返回脏数据,AllocSvr 需要跟StoreSvr保持租约。

  

  这个租约由两个条件组成:

    租约失效:AllocSvr N秒内无法从Store Svr读取加载配置时,AllocSvr停止服务

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

    微信的消息序列号生成实践

 

     这种机制会造成切换的号段存在小段时间不可服务,不过因为重试和异步重试队列,小段时间的不可用服务用户无感知的。并且是小概率事件。

 

接下来看看它的容灾:

  1.0

  初版本的seqsvr采用了主机+冷备机容灾模式。

  全量UID分为 N个Section,连续的若干个Section 组成了一个Set,每个Set中都有一主一备,两台AllocSvr。正常情况下,主出故障时,仲裁发现了,切换主备,原来的主机下线变为备机,备机变成主机后加载uid号段提供服务。

  微信的消息序列号生成实践

 

   明显缺陷: 有一半机器闲置,主备切换时候,瞬间要接收主机所有请求,容易导致备机过载。

  当年选主备容灾的原因: 调用段读取本地机器的client配置,决定去哪台服务端调用,可以写上 去 A,B,C服务找,有你想要的。如果不是主备的话,就要问过来个遍。但是主备的话,就最多一次就试出来了,额外消耗少,编码简单。   因为client是静态配置的,Svr发生状态变更,无法确定。

 

  缺陷:

  1. 扩容、缩容非常麻烦

  2. 一个Set的主备机都过载,无法使用其他Set机器进行容灾。(可以使用一致性Hash算法,找某个uid,仲裁服务优先分给A,A挂掉分配给B,再不行分配给C),然后就沿着这条路线去访问(流量没有叠加哦),但是仍然改变不了客户端配置文件变更的问题。

 

容灾2.0:嵌入式路由表容灾

  既然是Client 和AllocSvr 路由状态不一致的问题,那么让AllocSvr把当前路由状态传递给Client不就行了。

  然后2.0就把AllocSvr的路由状态嵌入到Client 请求sequence的相应包中。

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

  它解决了路由状态不一致的问题,可以所有机器主备,也可以负载均衡。

  

  路由同步优化:

    把路由表嵌入到取sequenece的请求,没有路由表怎么知道去哪台AllocSvr取呢?(随机选一台取)  另外Sequence 高频操作,如何避免嵌入路由表带来的带宽呢?(版本号是否更新)。

 

    1. 如果有缓存,选择对应的AllocSvr,如果不存再随机选一台

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

    3. Alloc收到请求,除了sequence逻辑,判断Client 带上版本号是否更新,如果旧,就附上最新

    4. client收到请求,除了处理sequence逻辑外,看是否有新的,如果有,更新本地路由表。

 

  

 

 

程序员灯塔
转载请注明原文链接:微信的消息序列号生成实践
喜欢 (0)