Leaf分布式ID生成方案

简介

Leaf是美团自主研发设计的分布式ID生成方案,经历了美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的大流量考验,让我们看看它是如何做到。

Leaf这个名字是来自德国哲学家、数学家莱布尼茨的一句话: >There are no two identical leaves in the world > “世界上没有两片相同的树叶”,以下是美团对该系统的要求:

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

Leaf方案实现

Leaf基于数据库生成雪花算法实现了两种分布式ID方案:Leaf-segmentLeaf-snowflake

Leaf-segment数据库方案

通过缓存加数据库的方式生成ID,数据库中存储ID的业务类型、已使用最大ID、每次获取的步长,Leaf服务通过业务类型向数据库获取固定步长的ID,存储在缓存中,使用完再循环向数据库获取。

数据库表设计如下:

+-------------+--------------+------+-----+-------------------+-----------------------------+
| Field       | Type         | Null | Key | Default           | Extra                       |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| biz_tag     | varchar(128) | NO   | PRI |                   |  业务类型                    |
| max_id      | bigint(20)   | NO   |     | 1                 |  已使用id                    |
| step        | int(11)      | NO   |     | NULL              |  每次获取步长                 |
| desc        | varchar(256) | YES  |     | NULL              |                             |
| update_time | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+
高可用

同时为了保障高可用,分布式部署Leaf服务,为了降低对数据库的依赖,增加缓存,每次向数据库获取指定步长的ID,存储在缓存中,步长内的ID未消耗完不会依赖到数据库,步长的大小设置为业务最高TPS的600倍,保证在数据库挂掉后,在10分钟内仍能持续提供ID。

双缓存

持续压测下,TP999(99.9%的请求最大响应时间)数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,TP999数据会出现偶尔的尖刺,为了解决该问题使用双缓存的策略,即第一个缓存消耗到某个阈值,如号段的80%,则开始向数据库获取下个号段的ID存入第二个缓存中,第一个缓存消耗完后,则使用第二个缓存,以此反复,这样就能保证数据库网络抖动时能把请求’尖刺‘抹平,这个设计让我想到JVM垃圾回收机制的from区和to区,当然解决的问题不一样。

优缺点

设计其实是通俗易懂的,这样的设计有以下优势

  • 拓展容易 根据业务类型区分id,每次新增id只需数据库中加条数据即可。
  • 高可用、高响应,通过分布式实现高可用,使用缓存实现高响应。
  • ID单调递增、长度可控,满足数据库存储的主键要求。

缺点:

  • ID号码不够随机,能够泄露发号数量的信息,不太安全。
  • DB宕机会造成整个系统不可用。

Leaf-snowflake 雪花算法

0 - 0000000000000000000000000000000000000000 - 00000 00000 - 000000000000
(1位)固定值  - (41位)时间戳 - (10位)服务id - (12位)序号

Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。对于服务id的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置服务id。Leaf-snowflake是按照下面几个步骤启动的:

  1. 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
  2. 如果有注册过直接取回自己的服务id(zk顺序节点生成的int类型ID号),启动服务。
  3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的服务id号,启动服务。
弱依赖ZooKeeper

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

解决时钟问题

因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。

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

  1. 若写过,则用自身系统时间与leaf_forever/Leaf分布式ID生成方案_分布式id{self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。
  2. 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
  3. 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。
  4. 否则认为本机系统时间发生大步长偏移,启动失败并报警。
  5. 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警,如下:

//发生了回拨,此刻时间小于上次发号时间
 if (timestamp < lastTimestamp) {
  			  
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                	//时间偏差大小小于5ms,则等待两倍时间
                    wait(offset << 1);//wait
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                       //还是小于,抛异常并上报
                        throwClockBackwardsEx(timestamp);
                      }    
                } catch (InterruptedException e) {  
                   throw  e;
                }
            } else {
                //throw
                throwClockBackwardsEx(timestamp);
            }
        }
 //分配ID

从上线情况来看,在2017年闰秒出现那一次出现过部分机器回拨,由于Leaf-snowflake的策略保证,成功避免了对业务造成的影响。