美團Leaf snowflake模式詳解


 

雪花算法概述

雪花算法簡單來說是這樣一個長整形數值。它64位,8個字節,剛好一個long。(為什么雪花算法ID是64位? 大概也是這個原因吧。理論上當然可以使用更多位,但是其實不是很有必要)
 

 

 

雪花算法,在單個節點上是有序的,如同 號段模式,但它也不是 全局嚴格有序,而是單個節點嚴格遞增。
 
雪花算法的問題
1 因為雪花算法 依賴於本地時鍾。所以存在時鍾回撥問題。那么,如何避免時鍾回撥?
2 雪花算法實現起來不復雜。但是問題是分布式場景之下,當需要啟動的leaf服務越來越多時,對其分配workerId是一件非常令人頭疼的事情。我們要做的是,盡量讓一件事情簡單化,讓用戶無感知。百度的UID做到了(文末有相關閱讀鏈接),leaf也做到了!

美團Leaf snowflake模式原理

leaf的Snowflake模式是怎么做到的呢?很簡單,通過zookeeper的PERSISTENT_SEQUENTIAL類型節點為每一個leaf實例生成一個遞增的workerId。以總計部署4個leaf實例為例:第1個leaf實例的workerId為0,且根據該實例的IP地址和配置的Port值,即使接下來重啟,workerId仍然為0;第2個leaf實例的workerId為1;第3個leaf實例的workerId為2;第4個leaf實例的workerId為3,以此類推。leaf持久化在zookeeper中的數據如下所示:
  1. /snowflake/afei/forever/
  2. |--192.168.0.1--0
  3. |--192.168.0.2--1
  4. |--192.168.0.3--2
我們可以看到這些數據的路徑是:/snowflake/${leaf.name}/forever/${ip}:${port}。如此一來,對於所有部署的leaf實例,其獲取到的workerId只跟它的ip和port有關。當然,由於其workerId占10位,所以,理論上Leaf服務實例數可以達到1024個(很明顯,這個實例數上限幾乎能夠滿足任何業務場景)。
 
實際情況, 默認會增加 - 00000 這樣的 序號。
 
 

美團Leaf snowflake的使用&配置

默認的配置是:

leaf.name=com.sankuai.leaf.opensource.test

leaf.snowflake.zk.address=localhost

leaf.snowflake.port=2181
 
說明:
leaf.snowflake.zk.address 確實是將要連接的zk的地址列表,形如192.168.56.1:2181;192.168.56.2:2181,它可以是localhost,這個時候leaf實際上連接zk使用的是2181;
 
需要注意的是上面leaf.snowflake.port為2181, 會讓人產生誤解。因為它並不是zk的端口,僅僅是用來標志當前leaf節點。用來拼接在節點名稱, 所以超過65536 也不會做檢查,它不管它是什么具體值(但需要是整數).. 實際上, 我們可以把它設置為web的端口,這樣就會出現沖突。
 
/**
 * @param zkAddress zk地址
 * @param port      snowflake監聽端口
 * @param twepoch   起始的時間戳
 */
public SnowflakeIDGenImpl(String zkAddress, int port, long twepoch) {
    this.twepoch = twepoch;
    Preconditions.checkArgument(timeGen() > twepoch, "Snowflake not support twepoch gt currentTime");
    final String ip = Utils.getIp(); // 實際將創建的節點的ip, 使用這個
    SnowflakeZookeeperHolder holder = new SnowflakeZookeeperHolder(ip, String.valueOf(port), zkAddress);

如上,實際將創建的節點的ip, 使用的是本機ip,然后拼接上面的leaf.snowflake.port端口,創建的節點是PERSISTENT_SEQUENTIAL類型節點,從名字上看,它會持久化,然后又是有序的(從父節點的角度來看,它是有序的。它會從0每次增加1,節點可以有自己的名字,但是zk會主動給節點添加一個序號的后綴,長度是10,類型是10進制)。所以它會一直保存在zk之上,也就是說zk因為異常等原因重啟之后,它依然存在,不會丟失。節點的內容是json,包括ip,port,時間戳timestamp;其中timestamp 是每3s更新一次。(點擊黃色的刷新按鈕,可以看到timestamp字段的變化)

  
leaf.name 配置項是做什么用的呢? 是用來確定將創建的zk節點的路徑,上圖因為沒有配置 leaf.name 所以 展現是null!雖然是null,不過好像也沒有什么關系,不影響程序正常運行。不過,一般情況下,對於一個分布式id,我們所有的leaf客戶端,需要設置為相同的leaf.name,這樣,workerID才會有序遞增,才不會出現沖突! 如果不同業務含義的分布式id,自然需要不同的leaf.name; 所以,可以理解為leaf.name是分布式id 的名字。注意到leaf.name 對於號段模式是沒有用的!!
  
因為將創建的zk節點使用的ip固定是本地ip,如果一個集群上部署兩個leaf呢?那leaf會重復利用那個節點,而不會新創建。就是說如果不改配置,一個機器只能部署一個相同的微服務。如果想兩個相同的微服務呢?那只能部署到不同的機器上去。 當然,對於不同的微服務,肯定需要不同leaf的配置,不然也不合理。那就只能修改 leaf.name 或leaf.snowflake.port; leaf會在本地創建緩存文件,名為workerID.properties,內容非常簡單,就是使用給雪花算法使用的workerID。
 
這樣下次重啟leaf,即使zk連接不上,也能夠正常運行。不過,就是后台會一直打印錯誤,有點煩人:
2021-05-01 14:43:35.450 INFO 13384 --- [.168.56.1:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server 192.168.56.1/192.168.56.1:2181. Will not attempt to authenticate using SASL (unknown error) 2021-05-01 14:43:37.452 WARN 13384 --- [.168.56.1:2181)] org.apache.zookeeper.ClientCnxn : Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect java.net.ConnectException: Connection refused: no further information at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) ~[na:1.8.0_231] at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717) ~[na:1.8.0_231] at org.apache.zookeeper.ClientCnxnSocketNIO.doTransport(ClientCnxnSocketNIO.java:361) ~[zookeeper-3.4.6.jar:3.4.6-1569965] at org.apache.zookeeper.ClientCnxn$SendThread.run(ClientCnxn.java:1081) ~[zookeeper-3.4.6.jar:3.4.6-1569965]
  
當然,首次還是需要連zk的。另外,如果修改了leaf的配置,即leaf.properties ,那么會在zk上創建新節點;而之間創建的節點依然會留下來。
 
實際情況如何部署?
如同美團Leaf 號段模式,leaf-server 是可選的,我們只要創建自己的SnowflakeIDGenImpl實現的IDGen就可以了,然后當然要注意配置,要保證每個微服務實例使用相同leaf.name,然后要都能有各自的zk節點即可。任何兩個實例不能使用相同zk節點,相同則可能出現重復id!
 

美團Leaf snowflake的測試

leaf-sever提供了rest 獲取id 接口:
http://localhost:8010/api/snowflake/get/key
( 對於雪花算法,這里的key是沒有作用的)
結果是: 1388377407299268680
 
多次刷新,可見這個值不斷增長,也就是滿足單調遞增。
 
另外decodeSnowflakeId 接口提供了解碼雪花算法id 的功能:
http://localhost:8010/decodeSnowflakeId?snowflakeId=1388377012309078107
結果是:
{"workerId":"3","sequenceId":"91","timestamp":"1619849849189(2021-05-01 14:17:29.189)"}
 
sequenceId 是什么? 是id在當前毫秒內的 序列!它有12個bit,所以 一個毫秒內最多可以產生 4096個id 哦!但是因為美團leaf對id 起始值做了優化,
sequence = RANDOM.nextInt(100);
它是100內隨機的。
 
然后在不同的微服務實例上測試 /api/snowflake/get/key 接口,然后解析,發現是不同的workerId, 滿足分布式id 要求!
  

美團Leaf snowflake與時鍾回撥問題

時鍾回撥的原因在於 獲取雪花算法id 的時候,會去獲取本地時間。這個本地時間是可能被 有意無意修改,如果修改為過去的時間,就會可能導致id重復的問題,也就是回撥問題!
 
Segment模式有時鍾回撥問題嗎?很明顯沒有,因為通過這種模式獲取的ID沒有任何時間屬性,所以不存在時鍾回撥問題。美團Leaf-snowflake方案 是怎么解決這個問題的呢?我能夠猜到的是,如果它通過zk來獲取時間,那么就不會依賴本地時間了!進而解決問題!
 
然后,通過過程源碼發現,並沒有實現。 可能是這個時鍾回撥並不是很嚴重的問題吧!leaf的Snowflake模式並沒有徹底解決時鍾回撥的問題。當運行過程中,如果時鍾回撥超過5ms ( 准確說是兩次get 方法之間的回撥),依然會拋出異常。那么,Snowflake模式主要解決什么問題?很明顯,是snowflake中的workerId部分。最后,leaf會定期(間隔周期是3秒)上報更新timestamp。並且上報時,如果發現當前時間戳少於最后一次上報的時間戳,那么會放棄上報。之所以這么做的原因是,防止在leaf實例重啟過程中,由於時鍾回撥導致可能產生重復ID的問題。
 
也就是說,運行時,leaf允許最多5ms 的回撥;重啟時,允許最多3s 的回撥!
 
我們看源碼:
sequence = RANDOM.nextInt(100); 避免了 每次 容易獲取到的為 0 的問題。
 
它是怎么引起的? 手動修改時間? 我們的多個 linux機器上的時間不一致。。
 
—— 誰會這么無趣的去修改時間呢? 一般情況我們的linux機器上的時間還是可以保持一致的吧。所以,時鍾回撥 不是大問題, 而且到這個程度, 也已經差不多了!
 

全局有序問題!

美團Leaf-snowflake方案能夠全局有序嗎? 不能。 實際什么,任何算法都不能保證全局有序,除非 單點,集中部署;

是否有任何辦法保證全局有序遞增? 我們把節點id 字段設置為相同,是否可以? 不可以啊.. 因為可能出現相同id 的情況。那么改進一下, 使得每個節點的步長一致,然后起始值不同,如同 數據庫的主主模式, 好像也可以避免id 重復! (這樣做比較麻煩,但是 其實如果不在意是否全局有序,可以忽略這步..)

那么號段模式,是否可以也保證全局有序? 無法! 因為每個客戶端會獲取新的號段!號段之間是並發的!

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM