美团分布式ID生成服务LeafCode
分布式ID作用
复杂系统中往往需要唯一标识做一些事情,比如全局唯一code码,比如分库分表用系统递增id不能满足需求,比如做IM消息id需要有个生成器。
业务上对于全局唯一ID有什么要求呢?
全局唯一:不能出现重复的ID;
趋势递增:MySql InnoDB引擎使用的是聚集索引,关系数据库使用b-tree数据结构组织存储数据,所以作为数据库表主键尽量保障其有序性以提高写性能;
单调递增:需要保障某个时间点后面生成的ID一定大于前面生成的ID,这样我们可以在业务上做一些事情,比如事务版本、增量消息、排序等要求;
数据安全:如果ID是连续递增了,其实是可以反解出对应的数据量增长的,比如订单id如果递增,竞对可以知道每日订单量是多少了,而且一旦数据id有规则,就容易被穷举,存在安全风险;
可靠性高:全局唯一ID生成服务往往是很多业务架构的底层服务,需要保障较高的可靠性,如果全局唯一ID服务不可用,上层业务系统可能会瘫痪;
总结下来一个支撑全局唯一ID生成的服务需要具备以下几点:
TP999尽可能要低,不能因此提高整个上层业务时延;
可用性要求高,需要保证5个9;
高QPS支持,上层很多业务同一个高峰期间调用ID生成服务;
业界有以下几种常见唯一ID生成方式。
UUID方式
UUID是32个16进制数字,有基于“-”的连字号分成5端,共36字符。
8-4-4-4-12
优点:
性能高,本地生成,没有网络损耗
缺点:
不易存储:UUID太长,16字节128位,通常以36长度字符串表示,很多场景不适用;
信息安全不高:因为UUID生成需要基于MAC地址,可能造成MAC地址泄露,可能被黑客利用;
对于DB主键场景非常不适用:MySql官方建议主键尽量越短越好,36字符长度的UUID显然不符合,同时这种无序生成的ID对mysql索引非常不利,在InnoDB引擎下,UUID的主键会引起数据位置频繁变动,严重影响性能,如果存储介质不是MySql可以忽略;
Snowflake方式
之前的文章介绍过手写一个Snowflake方案,大致的思路是类似于UUID的划分命名空间的方法来生成唯一ID,把64-bit分成多段,用来区分机器标识、时间、自增序列等,具体如图所示:
41-bit时间可以表示(1L<<41)/(1000L*3600*24*365)=69年,10-bit可以表示1024台机器。如果对于IDC有要求,可以将10-bit分5个给到IDC,另外5个给工作机器。这样可以标识32个IDC,每个IDC可以有32台机器,可以根据自身需求定义。
12个自增序列号可以标识2^12个ID,理论上snowflake的qps可以支持409.6w/s,这种分配方式可以保证在任何一个IDC任何一台机器在毫秒内生成ID是不同的。
优点:
毫秒数在高位,自增序列在低位,整个ID是趋势递增的;
不依赖数据库等三方系统,可以独立部署服务,稳定性高,生成ID的性能也非常好;
可以根据业务特点分配bit位,非常灵活;
缺点:
强制依赖机器时钟,如果机器上分配的时钟回拨,会导致发号重复或服务不可用;
数据库生成方式
就是利用Mysql的自增方式生成id,给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下面SQL读写MySql得到ID:
begin;
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;
优点:
非常简单,利用现有数据库实现,成本小;
ID号单调自增,可以实现一些对ID有特殊要求的业务场景;
缺点:
强依赖DB,当DB整体不可用时会导致服务不可用,配置主从复制可以增加DB可用性,但是数据一致性在主从延迟等情况下难以保证,主从切换时不一致可能导致重复发号;
ID发号器的瓶颈在DB本身;
对于提高Mysql性能上可以采用这种方案:
在分布式系统中可以部署多台机器,每台机器提供不同的初始值,且步长相等。
比如两台机器,step为2,TicketServer1初始值为1,取值过程为:1,3,5,7,9,11...。TicketServer2初始值为2,取值过程为:2,4,6,8,10...。
这是Flicker在2010年介绍的一种主键生成策略,上述方案对应参数如下:
TicketServer1:(TicketServer)
auto-increment-increment = 2
auto-increment-offset = 1
TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2
如果我们部署N台机器,步长需要设置为N,每台初始值依次为0,1,2...,n-1:
这种架构看起来可以满足性能需求,但也存在几个缺点:
系统水平扩展起来比较麻烦,比如定义好步长和机器台数之后,后续添加机器应该怎么操作?可以这样搞,比如之前有一台机器步长为1,发号逻辑为1,2,3,4,5。当想要扩容另一台机器时,考虑在扩容期间发号不会到14,则可以将第二台机器设置为从14开始发号,步长为2。然后摘除第一台,保留为奇数,步长设置为2。这样操作之后两台机器是可以产生符合预期的数字的。但是如果发号服务整体有100台机器,这个扩容操作就是噩梦了,复杂的难以实现。
两台机器整体对外提供的id是趋势递增的,但是对于大部分业务不是很重要,可以容忍。
数据库压力还是很大的,每次获取id都需要读一次数据库,只能靠堆机器提高性能。
那么LeafCode是如何实现的呢?
Leaf是基于上面第二种和第三种方案优点组合一起实现的。
基于数据库的Segment方案
之前基于数据库方案,每次获取ID都得读写一次数据库,对数据库造成非常大的压力。改为proxy方式后,可以批量获取,每次获取一个segment号段的值。用完之后再去数据库获取新的号段,可以大大降低数据库的压力。
不同业务通过biz_tag字段区分,每个biz_tag的ID获取相互隔离,互不影响。后续如果有性能需求可以对数据库进行扩容,不需要之前数据库方案复杂的扩容操作,只需要对biz_tag分库分表就行。
库表结构:
说明:
biz_tag用来区分业务;
max_id表示该biz_tag目前所被分配的id号段最大值;
step表示每次分配号段长度;
之前每次获取id都需要读写数据库,现在只需要把step设置足够大,比如1000,那么只有当1000个号被消耗完之后才重新读写一次数据库。读写频率从之前的1减少到了1/step。
比如test_tag在第一台leaf机器上是1~1000的号段,当这个号段用完时,会去加载另一个长度为step=1000的号段,比如现在有三个leaf服务,每个1000,如果考虑另外两个服务的id没有变更的情况下,新加载的1000号会到第一台机器,号段变成了3001~4000。同时数据库对应的biz_tag数据的max_id会从3000变更为4000,对应sql:
优点:
Leaf服务可以线性扩展,性能完全支撑多数业务场景;
ID号是趋势递增的8byte的64位数字,满足数据库主键要求;
容灾性高,leaf服务内部有号段缓存,即使DB宕机,短时间内leaf仍然可以对外提供服务;
可以自定义max_id大小,便于业务从原有id方式迁移过来;
缺点:
ID号码不够随机,可以基于发号数量推演一些信息;
TP999数据波动大,当号段使用完之后,会去db更新数据,这个过程可能会hang在这个IO上,tp999数据会出现偶尔尖刺;
DB宕机,整个服务可能不可用;
解决IO尖刺问题
IO尖刺主要出现在Leaf服务内号段消耗完的时候,去db获取下一阶段临界点数据,可能会导致IO线程阻塞,如果DB查询网络抖动或是产生慢查询,整个系统会响应比较慢。
我们希望到DB取号的过程中可以做到无阻塞,可以在号段消费到某个时间点之后,异步的将下一号段加载到内存里面,这样就不需要等到用尽号之后才去更新号段,这样可以很大程度降低系统的TP999了。
可以采用双buffer方式,leaf内存服务有两个号段缓冲区(segment)。当号段已下发10%时,如果下一个号段未更新,则另起一个更新线程去更新号段。如果下个号段准备好了,则切换到下个号段为当前segment接着下发,循环往复:
每个biz-tag都有消费速度监控,通常推荐segment长度设置为高峰期发号qps的600倍(10分钟),这样即使宕机,leaf仍然可以持续发号1-~20分钟;
每次请求来临时,都会(多次check)判断下个号段状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新;
高可用容灾
对于DB可用性问题,可以采用一主两从方式,同时分机房部署,Master和Slave之间采用半同步方式同步数据。引入高可用中间件做故障转移与主从切换。当然极端情况下,这些还会退化为异步模式,导致数据不一致,但概率很小。
所以如果系统要保证100%的数据强一致,可以选择类似于Paxos算法,做到数据主从的强一致MySql方案。
同时Leaf服务分IDC部署,内部服务基于RPC调用,服务调用时根据负载均衡算法优先调用同机房的leaf服务。在该IDC内leaf服务不可用时才选择其他机房Leaf服务。同时基于治理平台提供针对于服务过载保护、一键截流、动态流量分配等对服务进行保护。
Snowflake的方案
Leaf的另一种方案是基于Snowflake的方案,产生趋势递增的id,由于ID是可以计算的,不适用于订单id生成场景。
leaf-Snowflake完全沿用了Snowflake方案的bit设计,1+41+10+12方式生成id。
针对于workerId的分配,当服务集群数量较少时,完全可以手动配置。leaf服务规模较大时,动手配置成本太高。可以适用zk持久顺序节点特性自动对Snowflake节点配置workerid。启动步骤如下:
启动leaf-Snowflake服务,连接zk,在leaf-forever节点下检查自己是否已经注册过,判断依据是该顺序子节点;
如果有注册,则直接返回自己的workerId(zk顺序节点生成int类型id号),启动服务;
如果没有注册过,就在该父节点下创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerid,启动服务;
弱化zk依赖
除了每次都去zk拿数据外,也会在本机文件系统上缓存一个workerid文件。当zk出现问题时,恰好机器出现问题重启时,可以保障服务正常启动。这样做到了对于三方组件的弱依赖,一定程度上提高了SLA。
解决时钟问题
因为Snowflake方案对于时间强依赖,如果机器的时钟发生了回拨,那么可能会产生重复id,需要解决时钟回退问题。
参照整个启动流程图,服务启动时先检查自己是否写过zk的leaf_forever节点:
写过,则用自身系统时间与leaf_forever/${self}节点记录的时间比较,如果小于leaf_forever/${self}节点的时间,则认为机器时间发生了大步长回拨,服务启动失败并告警;
未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self},并写入自身系统时间,接下来综合对比其余leaf节点的系统时间判断自身系统时间是否准确,具体做法是取leaf_temporary下所有临时节点的服务ip:port,然后通过rpc请求得到对应节点的系统时间,计算sum(time)/nodeSize;
如果 (系统时间-sum(time)/nodeSize)<阈值,则认为当前系统时间正确(小于平均值),正常启动服务,同时写入临时节点leaf_temporary/${self}维持租约;
否则认为本机系统时间发生了大步长的便宜,启动失败告警;
每隔一段时间,比如3s上报自身服务时间,写入leaf_forever/${self};
因为Snowflake强依赖时钟,对时间比较敏感,在机器工作时NTP同步也会造成秒级回退,建议可以直接关闭NTP同步。或者在机器时钟回拨时,直接不对外提供服务,返回ERROR_CODE,等时钟追上之后再提供服务。
或做一层重试,等上报告警系统,或发现有时钟回拨之后自动摘除本身节点并告警:
监控
Leaf提供了web监控,映射的是内存数据情况,可以看到实时的发号状态,比如每个号段双buffer使用情况,当前id下发到哪个位置等等。
美团分布式ID生成服务LeafCode相关推荐
- Leaf:美团分布式ID生成服务开源
Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家.数学家莱布尼茨的一句话:"There are no two identical leaves in the wor ...
- Leaf:美团分布式ID生成服务开源 1
Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家.数学家莱布尼茨的一句话:"There are no two identical leaves in the wor ...
- 美团分布式ID生成服务开源!
上一篇:突然!VS Code 杀死 IDEA?! 简介 Leaf 最早期需求是各个业务线的订单ID生成需求.在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也 ...
- Leaf(美团分布式ID生成服务)核心代码分析
上一篇文章我们讲了几种常见的分布式唯一ID生成方案,今天我们再来讲一下由美团开源的Leaf框架,这个框架集成了两种最适合生产环境使用的方式 第一种方式是:Leaf Segment 这种方式其实跟我们之 ...
- Leaf-美团分布式ID生成服务
Leaf : 美团分布式ID生成服务 There are no two identical leaves in the world.(世界上没有两片相同的树叶.) - 莱布尼茨 现有分布式ID生成方案 ...
- 深度解析leaf分布式id生成服务源码(号段模式)
原创不易,转载请注明出处 文章目录 前言 1.实现原理推演 1.1 基于mysql最简单分布式ID实现 1.2 flickr分布式id解决方案 1.3 号段+mysql 2.源码剖析 2.1初始化 2 ...
- 美团开源分布式ID生成系统——Leaf源码阅读笔记(Leaf的号段模式)
Leaf 最早期需求是各个业务线的订单ID生成需求.在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也有的业务直接用UUID这种方式来生成ID.以上的方式各自 ...
- easyui treegrid获取父节点的id_超简单的分布式ID生成方案!美团开源框架介绍
目录 阐述背景 Leaf snowflake 模式介绍 Leaf segment 模式介绍 Leaf 改造支持 RPC 阐述背景 不吹嘘,不夸张,项目中用到 ID 生成的场景确实挺多.比如业务要做幂等 ...
- 超简单的分布式ID生成方案!美团开源框架介绍
目录 阐述背景 Leaf snowflake 模式介绍 Leaf segment 模式介绍 Leaf 改造支持 RPC 阐述背景 不吹嘘,不夸张,项目中用到 ID 生成的场景确实挺多.比如业务要做幂等 ...
最新文章
- NR 5G 网络切片
- 算法经典书籍--计算机算法的设计与分析
- OpenGL ES 3.0之Texturing纹理详解(二)
- 分布式锁的三种实现方式_分布式锁的几种实现方式~
- qpython执行手机脚本精灵使用教程_Android上执行python脚本-QPython
- 单例模式的七种实现方法(java版)
- html%2b怎么转换成加号,Apache mod_rewrite%2B和加号(+)符号
- 基金投资需要注意什么?
- UVA1276 Network
- mstar Android解锁,液晶电视维修之Mstar方案从强刷包提取引导的方法
- 谷歌账号--手机号无法验证的问题
- R语言文本挖掘展示:画词云图
- 阿里达摩院最新FEDformer,长程时序预测全面超越SOTA | ICML 2022
- 夜深人静写算法(三十二)- 费马小定理
- random.seed()的作用(python)
- 滴水三期:day04.3-标志寄存器
- 基于MATLAB的说话人识别系统
- 【知乎问题】如何让不懂编程的人感受到编程的魅力
- MPI 易懂笔记
- 共享一个科学计算器(js版本)