分布式ID作用

复杂系统中往往需要唯一标识做一些事情,比如全局唯一code码,比如分库分表用系统递增id不能满足需求,比如做IM消息id需要有个生成器。

业务上对于全局唯一ID有什么要求呢?

  1. 全局唯一:不能出现重复的ID;

  2. 趋势递增:MySql InnoDB引擎使用的是聚集索引,关系数据库使用b-tree数据结构组织存储数据,所以作为数据库表主键尽量保障其有序性以提高写性能;

  3. 单调递增:需要保障某个时间点后面生成的ID一定大于前面生成的ID,这样我们可以在业务上做一些事情,比如事务版本、增量消息、排序等要求;

  4. 数据安全:如果ID是连续递增了,其实是可以反解出对应的数据量增长的,比如订单id如果递增,竞对可以知道每日订单量是多少了,而且一旦数据id有规则,就容易被穷举,存在安全风险;

  5. 可靠性高:全局唯一ID生成服务往往是很多业务架构的底层服务,需要保障较高的可靠性,如果全局唯一ID服务不可用,上层业务系统可能会瘫痪;

总结下来一个支撑全局唯一ID生成的服务需要具备以下几点:

  1. TP999尽可能要低,不能因此提高整个上层业务时延;

  2. 可用性要求高,需要保证5个9;

  3. 高QPS支持,上层很多业务同一个高峰期间调用ID生成服务;

业界有以下几种常见唯一ID生成方式。

UUID方式

UUID是32个16进制数字,有基于“-”的连字号分成5端,共36字符。

8-4-4-4-12

优点:

  1. 性能高,本地生成,没有网络损耗

缺点:

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

  2. 信息安全不高:因为UUID生成需要基于MAC地址,可能造成MAC地址泄露,可能被黑客利用;

  3. 对于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是不同的。

优点:

  1. 毫秒数在高位,自增序列在低位,整个ID是趋势递增的;

  2. 不依赖数据库等三方系统,可以独立部署服务,稳定性高,生成ID的性能也非常好;

  3. 可以根据业务特点分配bit位,非常灵活;

缺点:

  1. 强制依赖机器时钟,如果机器上分配的时钟回拨,会导致发号重复或服务不可用;

数据库生成方式

就是利用Mysql的自增方式生成id,给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下面SQL读写MySql得到ID:

begin;

REPLACE INTO Tickets64 (stub) VALUES ('a');

SELECT LAST_INSERT_ID();

commit;

优点:

  1. 非常简单,利用现有数据库实现,成本小;

  2. ID号单调自增,可以实现一些对ID有特殊要求的业务场景;

缺点:

  1. 强依赖DB,当DB整体不可用时会导致服务不可用,配置主从复制可以增加DB可用性,但是数据一致性在主从延迟等情况下难以保证,主从切换时不一致可能导致重复发号;

  2. 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,发号逻辑为1,2,3,4,5。当想要扩容另一台机器时,考虑在扩容期间发号不会到14,则可以将第二台机器设置为从14开始发号,步长为2。然后摘除第一台,保留为奇数,步长设置为2。这样操作之后两台机器是可以产生符合预期的数字的。但是如果发号服务整体有100台机器,这个扩容操作就是噩梦了,复杂的难以实现。

  2. 两台机器整体对外提供的id是趋势递增的,但是对于大部分业务不是很重要,可以容忍。

  3. 数据库压力还是很大的,每次获取id都需要读一次数据库,只能靠堆机器提高性能。

那么LeafCode是如何实现的呢?

Leaf是基于上面第二种和第三种方案优点组合一起实现的。

基于数据库的Segment方案

之前基于数据库方案,每次获取ID都得读写一次数据库,对数据库造成非常大的压力。改为proxy方式后,可以批量获取,每次获取一个segment号段的值。用完之后再去数据库获取新的号段,可以大大降低数据库的压力。

不同业务通过biz_tag字段区分,每个biz_tag的ID获取相互隔离,互不影响。后续如果有性能需求可以对数据库进行扩容,不需要之前数据库方案复杂的扩容操作,只需要对biz_tag分库分表就行。

库表结构:

说明:

  1. biz_tag用来区分业务;

  2. max_id表示该biz_tag目前所被分配的id号段最大值;

  3. 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:

优点:

  1. Leaf服务可以线性扩展,性能完全支撑多数业务场景;

  2. ID号是趋势递增的8byte的64位数字,满足数据库主键要求;

  3. 容灾性高,leaf服务内部有号段缓存,即使DB宕机,短时间内leaf仍然可以对外提供服务;

  4. 可以自定义max_id大小,便于业务从原有id方式迁移过来;

缺点:

  1. ID号码不够随机,可以基于发号数量推演一些信息;

  2. TP999数据波动大,当号段使用完之后,会去db更新数据,这个过程可能会hang在这个IO上,tp999数据会出现偶尔尖刺;

  3. DB宕机,整个服务可能不可用;

解决IO尖刺问题

IO尖刺主要出现在Leaf服务内号段消耗完的时候,去db获取下一阶段临界点数据,可能会导致IO线程阻塞,如果DB查询网络抖动或是产生慢查询,整个系统会响应比较慢。

我们希望到DB取号的过程中可以做到无阻塞,可以在号段消费到某个时间点之后,异步的将下一号段加载到内存里面,这样就不需要等到用尽号之后才去更新号段,这样可以很大程度降低系统的TP999了。

可以采用双buffer方式,leaf内存服务有两个号段缓冲区(segment)。当号段已下发10%时,如果下一个号段未更新,则另起一个更新线程去更新号段。如果下个号段准备好了,则切换到下个号段为当前segment接着下发,循环往复:

  1. 每个biz-tag都有消费速度监控,通常推荐segment长度设置为高峰期发号qps的600倍(10分钟),这样即使宕机,leaf仍然可以持续发号1-~20分钟;

  2. 每次请求来临时,都会(多次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。启动步骤如下:

  1. 启动leaf-Snowflake服务,连接zk,在leaf-forever节点下检查自己是否已经注册过,判断依据是该顺序子节点;

  2. 如果有注册,则直接返回自己的workerId(zk顺序节点生成int类型id号),启动服务;

  3. 如果没有注册过,就在该父节点下创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerid,启动服务;

弱化zk依赖

除了每次都去zk拿数据外,也会在本机文件系统上缓存一个workerid文件。当zk出现问题时,恰好机器出现问题重启时,可以保障服务正常启动。这样做到了对于三方组件的弱依赖,一定程度上提高了SLA。

解决时钟问题

因为Snowflake方案对于时间强依赖,如果机器的时钟发生了回拨,那么可能会产生重复id,需要解决时钟回退问题。

参照整个启动流程图,服务启动时先检查自己是否写过zk的leaf_forever节点:

  1. 写过,则用自身系统时间与leaf_forever/${self}节点记录的时间比较,如果小于leaf_forever/${self}节点的时间,则认为机器时间发生了大步长回拨,服务启动失败并告警;

  2. 未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self},并写入自身系统时间,接下来综合对比其余leaf节点的系统时间判断自身系统时间是否准确,具体做法是取leaf_temporary下所有临时节点的服务ip:port,然后通过rpc请求得到对应节点的系统时间,计算sum(time)/nodeSize;

  3. 如果 (系统时间-sum(time)/nodeSize)<阈值,则认为当前系统时间正确(小于平均值),正常启动服务,同时写入临时节点leaf_temporary/${self}维持租约;

  4. 否则认为本机系统时间发生了大步长的便宜,启动失败告警;

  5. 每隔一段时间,比如3s上报自身服务时间,写入leaf_forever/${self};

因为Snowflake强依赖时钟,对时间比较敏感,在机器工作时NTP同步也会造成秒级回退,建议可以直接关闭NTP同步。或者在机器时钟回拨时,直接不对外提供服务,返回ERROR_CODE,等时钟追上之后再提供服务。

或做一层重试,等上报告警系统,或发现有时钟回拨之后自动摘除本身节点并告警:

监控

Leaf提供了web监控,映射的是内存数据情况,可以看到实时的发号状态,比如每个号段双buffer使用情况,当前id下发到哪个位置等等。

美团分布式ID生成服务LeafCode相关推荐

  1. Leaf:美团分布式ID生成服务开源

    Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家.数学家莱布尼茨的一句话:"There are no two identical leaves in the wor ...

  2. Leaf:美团分布式ID生成服务开源 1

    Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家.数学家莱布尼茨的一句话:"There are no two identical leaves in the wor ...

  3. 美团分布式ID生成服务开源!

    上一篇:突然!VS Code 杀死 IDEA?! 简介 Leaf 最早期需求是各个业务线的订单ID生成需求.在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也 ...

  4. Leaf(美团分布式ID生成服务)核心代码分析

    上一篇文章我们讲了几种常见的分布式唯一ID生成方案,今天我们再来讲一下由美团开源的Leaf框架,这个框架集成了两种最适合生产环境使用的方式 第一种方式是:Leaf Segment 这种方式其实跟我们之 ...

  5. Leaf-美团分布式ID生成服务

    Leaf : 美团分布式ID生成服务 There are no two identical leaves in the world.(世界上没有两片相同的树叶.) - 莱布尼茨 现有分布式ID生成方案 ...

  6. 深度解析leaf分布式id生成服务源码(号段模式)

    原创不易,转载请注明出处 文章目录 前言 1.实现原理推演 1.1 基于mysql最简单分布式ID实现 1.2 flickr分布式id解决方案 1.3 号段+mysql 2.源码剖析 2.1初始化 2 ...

  7. 美团开源分布式ID生成系统——Leaf源码阅读笔记(Leaf的号段模式)

    Leaf 最早期需求是各个业务线的订单ID生成需求.在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也有的业务直接用UUID这种方式来生成ID.以上的方式各自 ...

  8. easyui treegrid获取父节点的id_超简单的分布式ID生成方案!美团开源框架介绍

    目录 阐述背景 Leaf snowflake 模式介绍 Leaf segment 模式介绍 Leaf 改造支持 RPC 阐述背景 不吹嘘,不夸张,项目中用到 ID 生成的场景确实挺多.比如业务要做幂等 ...

  9. 超简单的分布式ID生成方案!美团开源框架介绍

    目录 阐述背景 Leaf snowflake 模式介绍 Leaf segment 模式介绍 Leaf 改造支持 RPC 阐述背景 不吹嘘,不夸张,项目中用到 ID 生成的场景确实挺多.比如业务要做幂等 ...

最新文章

  1. NR 5G 网络切片
  2. 算法经典书籍--计算机算法的设计与分析
  3. OpenGL ES 3.0之Texturing纹理详解(二)
  4. 分布式锁的三种实现方式_分布式锁的几种实现方式~
  5. qpython执行手机脚本精灵使用教程_Android上执行python脚本-QPython
  6. 单例模式的七种实现方法(java版)
  7. html%2b怎么转换成加号,Apache mod_rewrite%2B和加号(+)符号
  8. 基金投资需要注意什么?
  9. UVA1276 Network
  10. mstar Android解锁,液晶电视维修之Mstar方案从强刷包提取引导的方法
  11. 谷歌账号--手机号无法验证的问题
  12. R语言文本挖掘展示:画词云图
  13. 阿里达摩院最新FEDformer,长程时序预测全面超越SOTA | ICML 2022
  14. 夜深人静写算法(三十二)- 费马小定理
  15. random.seed()的作用(python)
  16. 滴水三期:day04.3-标志寄存器
  17. 基于MATLAB的说话人识别系统
  18. 【知乎问题】如何让不懂编程的人感受到编程的魅力
  19. MPI 易懂笔记
  20. 共享一个科学计算器(js版本)

热门文章

  1. CF1407D Discrete Centrifugal Jumps(单调队列+DP)
  2. 模板 - 树上问题(树的直径、动态查询树的直径、树的重心)
  3. 【网络流24题】I、 方格取数问题(二分图的最大独立集/最小割)
  4. MySQL第九章索引_MySQL高级(索引优化+慢查询定位)
  5. 配置TS + node 的开发环境
  6. 基础搭建Hadoop大数据处理-编程
  7. Fortinet网络接入及安全方案配置步骤
  8. effective stl 条款15 小心string实现的多样性
  9. 系统进程管理及用户登陆监控
  10. Java_JAVA6动态编译的问题