【阅读】Leaf——美团点评分布式ID生成系统

背景

阅读了美团的ID生成器架构设计,记个笔记,同时强烈推荐阅读原文;
业务系统对ID号的要求有:

  1. 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  2. 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  3. 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  4. 信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。

3和4需求还是互斥的,无法使用同一个方案满足。

业务还对ID号生成系统的可用性要求极高,由此总结下一个ID生成系统应该做到如下几点:

  1. 平均延迟和TP999延迟都要尽可能低;
  2. 可用性5个9;
  3. 高QPS。

常见实现方案

UUID

UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符;

优势

性能高,本地生成,没有网络消耗;

缺点

不易于存储;UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用;

信息不安全;基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置;

ID作为主键时在某些场景不适用;比如做DB主键的场景下,UUID就非常不适用;而且UUID的无序性会导致数据位置在MySQL索引中频繁变动;

应用

类SnowFlake方案

是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等;理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

在这里插入图片描述

第一bit不用,41-bit的时间可以表示(1L<<41)/(1000L360024*365)=69年的时间,10-bit机器可以分别表示1024台机器【如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器,这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义】,12个自增序列号可以表示2^12个ID;

优点

ID趋势递增,毫秒数在高位,自增序列在低位;

不依赖数据库等第三方系统,本地生成,稳定性高,性能也高;

分配灵活,根据自身业务特性分配bit位;

缺点

强依赖机器时钟,机器上的时钟回拨会导致ID重复或者服务不可用;

应用

MongoDB的objectID,通过“时间+机器码+pid+inc”共12个字节,通过4+3+2+3的方式最终标识成一个24长度的十六进制字符。

数据库生成

以MySQL举例;利用给字段设置auto_increment_incrementauto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号
在这里插入图片描述

begin;
REPLACE INTO Tickets64 (stub) VALUES (‘a’);
SELECT LAST_INSERT_ID();
commit;

replace into 跟 insert 功能类似,不同点在于:replace into 首先尝试插入数据到表中, 1. 如果发现表中已经有此行数据(根据主键或者唯一索引判断)则先删除此行数据,然后插入新的数据。 2. 否则,直接插入新数据。

要注意的是:插入数据的表必须有主键或者是唯一索引!否则的话,replace into 会直接插入数据,这将导致表中出现重复的数据。

优点

成本小,简单,通过MySQL就能实现;

ID号自增,在特殊场景下这也是优势;

缺点

强依赖DB,当DB异常时整个系统不可用;

DB主从切换时可能导致数据不一致,ID重复;

性能瓶颈在单台MySQL;

MySQL 增强版

N台机器,每台机器的步长需设置为N,每台的初始值依次为0,1,2…N-1;
在这里插入图片描述

优点
缺点

系统水平扩展很难;机器台数和步长确定后,扩容麻烦;

ID不能单调递增,只能趋势递增;

数据库压力依旧很大,每次获取ID都需要读写一次数据库;

或许可以通过预生成来减少数据库的压力;比如数据库预先生产千万级的数据到缓存中,业务先从缓存读取ID,缓存没有了再去数据库中读取;数据库中数据批量追加到缓存中;

Leaf -segment方案

Leaf分别在上述第二种和第三种方案上做了相应的优化,实现了Leaf-segment和Leaf-snowflake方案。

通过proxy server批量获取,每次获取一个segment(step决定大小)号段的值,用完后再去数据库中取;同时不同业务通过biz_tag字段区分,每个biz_tag的ID获取,互相隔离;

扩容通过biz_tag分库分表即可;

±------------±-------------±-----±----±------------------±----------------------------+
| Field | Type | Null | Key | Default | Extra |
±------------±-------------±-----±----±------------------±----------------------------+
| biz_tag | varchar(128) | NO | PRI | | |
| max_id | bigint(20) | NO | | 1 | |
| step | int(11) | NO | | NULL | |
| desc | varchar(256) | YES | | NULL | |
| update_time | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
±------------±-------------±-----±----±------------------±----------------------------+

biz_tag用来区分业务,max_id表示biz_tag目前被分配的ID号段的最大值,step表示每次分配的号段长度;(比如step为1000,那么当1000取完了才去数据库中读取)

架构设计:
在这里插入图片描述

Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit

优点

leaf服务扩容方便,性能能支撑大多数业务场景;

ID号码是趋势递增的8bytes的64位数字,用MySQL就能满足;

容灾性高,leaf内部有号段缓存,即使DB宕机段时间内服务仍然可用;

迁移性好,max_id可变,迁移方便;

缺点

ID号码不够随机,不太安全;

TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,所以TP999会出现尖刺;

DB宕机会导致系统不可用;

Leaf -segment方案-双Buffer优化

leaf取号段的时机是在号段消耗完才进行的;也就是当前号段的上界点的ID下发时间是下一号段的取回号段时间,过程中新的拉取ID请求需要等待(线程阻塞),特别是取DB的时候网络的抖动或者DB的慢查询会导致整个系统的响应时间变慢;

本架构设计在号段消耗完前,提前预拉取下一号段的数据;

内存中同时存在2个号段数据,号段通过异步线程滚动前进;

在这里插入图片描述

优点

每个biz_tag都有消费进度监控,比如号段长度设置为高峰期QPS的600倍;

每次请求都会判断下一个号段的状态,从而更新下一个号段;

缺点

Leaf -snowflake方案

Leaf分别在上述第二种和第三种方案上做了相应的优化,实现了Leaf-segment和Leaf-snowflake方案。

snowflake方案适合订单类,不需要趋势递增的场景;

leaf-snowflake方案仍然使用snowflake方案的bit位设计,“1+42+10+12”;

通过zookeeper服务注册管理每个leaf服务,并且zk负责下发每个leaf服务的work id,作为标识启动后续ID生成程序;

在这里插入图片描述
在这里插入图片描述

弱依赖ZK

在本机缓存一个worker ID文件,当zk出现问题时,通过读取文件实现对zk的弱依赖;

时钟同步问题

如果机器时钟发生了回拨,那么就会发生重复ID号;启动顺序如下:
在这里插入图片描述

参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

若写过,则用自身系统时间与leaf_forever/ s e l f 节点记录时间做比较,若小于 l e a f f o r e v e r / {self}节点记录时间做比较,若小于leaf_forever/ self节点记录时间做比较,若小于leafforever/{self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。

若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。

若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。

否则认为本机系统时间发生大步长偏移,启动失败并报警。

每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

由于强依赖时钟,对时间的要求敏感,需要关闭机器的NTP同步;

并且在时钟回拨时不提供服务或者返回Error_Code,或者摘除节点并报警;

// 当前时间小于上次的发号时间

if (timestamp < lastTimeStamp) {

// 发生时钟回拨

}

优点

目前Leaf的性能在4C8G的机器上QPS能压测到近5万/s,TP999 1ms

缺点

参考文档

leaf 美团的分布式ID生成器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值