分布式ID生成服務,真的有必要搞一個

2021-01-11 娛影一鍋燴

目錄

闡述背景Leaf snowflake 模式介紹Leaf segment 模式介紹Leaf 改造支持 RPC闡述背景

不吹噓,不誇張,項目中用到 ID 生成的場景確實挺多。比如業務要做冪等的時候,如果沒有合適的業務欄位去做唯一標識,那就需要單獨生成一個唯一的標識,這個場景相信大家不陌生。

很多時候為了圖方便可能就是寫一個簡單的 ID 生成工具類,直接開用。做的好點的可能單獨出一個 Jar 包讓其他項目依賴,做的不好的很有可能就是 Copy 了 N 份一樣的代碼。

單獨搞一個獨立的 ID 生成服務非常有必要,當然我們也沒必要自己做造輪子,有現成開源的直接用就是了。如果人手夠,不差錢,自研也可以。

今天為大家介紹一款美團開源的 ID 生成框架 Leaf,在 Leaf 的基礎上稍微擴展下,增加 RPC 服務的暴露和調用,提高 ID 獲取的性能。

Leaf 介紹

Leaf 最早期需求是各個業務線的訂單 ID 生成需求。在美團早期,有的業務直接通過 DB 自增的方式生成 ID,有的業務通過 redis 緩存來生成 ID,也有的業務直接用 UUID 這種方式來生成 ID。以上的方式各自有各自的問題,因此我們決定實現一套分布式 ID 生成服務來滿足需求。

具體 Leaf 設計文檔見:https://tech.meituan.com/2017/04/21/mt-leaf.html[1]

目前 Leaf 覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅遊、貓眼電影等眾多業務線。在 4C8G VM 基礎上,通過公司 RPC 方式調用,QPS 壓測結果近 5w/s,TP999 1ms。

snowflake 模式

snowflake 是 Twitter 開源的分布式 ID 生成算法,被廣泛應用於各種生成 ID 的場景。Leaf 中也支持這種方式去生成 ID。

使用步驟如下:

修改配置 leaf.snowflake.enable=true 開啟 snowflake 模式。

修改配置 leaf.snowflake.zk.address 和 leaf.snowflake.port 為你自己的 Zookeeper 地址和埠。

想必大家很好奇,為什麼這裡依賴了 Zookeeper 呢?

那是因為 snowflake 的 ID 組成中有 10bit 的 workerId,如下圖:

snowflake組成一般如果服務數量不多的話手動設置也沒問題,還有一些框架中會採用約定基於配置的方式,比如基於 IP 生成 wokerID,基於 hostname 最後幾位生成 wokerID,手動在機器上配置,手動在程序啟動時傳入等等方式。

Leaf 中為了簡化 wokerID 的配置,所以採用了 Zookeeper 來生成 wokerID。就是用了 Zookeeper 持久順序節點的特性自動對 snowflake 節點配置 wokerID。

如果你公司沒有用 Zookeeper,又不想因為 Leaf 去單獨部署 Zookeeper 的話,你可以將源碼中這塊的邏輯改掉,比如自己提供一個生成順序 ID 的服務來替代 Zookeeper。

segment 模式

segment 是 Leaf 基於資料庫實現的 ID 生成方案,如果調用量不大,完全可以用 Mysql 的自增 ID 來實現 ID 的遞增。

Leaf 雖然也是基於 Mysql,但是做了很多的優化,下面簡單的介紹下 segment 模式的原理。

首先我們需要在資料庫中新增一張表用於存儲 ID 相關的信息。

CREATE TABLE `leaf_alloc` ( `biz_tag` varchar(128) NOT NULL DEFAULT '', `max_id` bigint(20) NOT NULL DEFAULT '1', `step` int(11) NOT NULL, `description` varchar(256) DEFAULT NULL, `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`biz_tag`)) ENGINE=InnoDB;biz_tag 用於區分業務類型,比如下單,支付等。如果以後有性能需求需要對資料庫擴容,只需要對 biz_tag 分庫分表就行。

max_id 表示該 biz_tag 目前所被分配的 ID 號段的最大值。

step 表示每次分配的號段長度。

下圖是 segment 的架構圖:

segment架構從上圖我們可以看出,當多個服務同時對 Leaf 進行 ID 獲取時,會傳入對應的 biz_tag,biz_tag 之間是相互隔離的,互不影響。

比如 Leaf 有三個節點,當 test_tag 第一次請求到 Leaf1 的時候,此時 Leaf1 的 ID 範圍就是 1~1000。

當 test_tag 第二次請求到 Leaf2 的時候,此時 Leaf2 的 ID 範圍就是 1001~2000。

當 test_tag 第三次請求到 Leaf3 的時候,此時 Leaf3 的 ID 範圍就是 2001~3000。

比如 Leaf1 已經知道自己的 test_tag 的 ID 範圍是 1~1000,那麼後續請求過來獲取 test_tag 對應 ID 時候,就會從 1 開始依次遞增,這個過程是在內存中進行的,性能高。不用每次獲取 ID 都去訪問一次資料庫。

問題一

這個時候又有人說了,如果並發量很大的話,1000 的號段長度一下就被用完了啊,此時就得去申請下一個範圍,這期間進來的請求也會因為 DB 號段沒有取回來,導致線程阻塞。

放心,Leaf 中已經對這種情況做了優化,不會等到 ID 消耗完了才去重新申請,會在還沒用完之前就去申請下一個範圍段。並發量大的問題你可以直接將 step 調大即可。

問題二

這個時候又有人說了,如果 Leaf 服務掛掉某個節點會不會有影響呢?

首先 Leaf 服務是集群部署,一般都會註冊到註冊中心讓其他服務發現。掛掉一個沒關係,還有其他的 N 個服務。問題是對 ID 的獲取有問題嗎? 會不會出現重複的 ID 呢?

答案是沒問題的,如果 Leaf1 掛了的話,它的範圍是 1~1000,假如它當前正獲取到了 100 這個階段,然後服務掛了。服務重啟後,就會去申請下一個範圍段了,不會再使用 1~1000。所以不會有重複 ID 出現。

Leaf 改造支持 RPC

如果你們的調用量很大,為了追求更高的性能,可以自己擴展一下,將 Leaf 改造成 Rpc 協議暴露出去。

首先將 Leaf 的 Spring 版本升級到 5.1.8.RELEASE,修改父 pom.xml 即可。

<spring.version>5.1.8.RELEASE</spring.version>然後將 Spring Boot 的版本升級到 2.1.6.RELEASE,修改 leaf-server 的 pom.xml。

<spring-boot-dependencies.version>2.1.6.RELEASE</spring-boot-dependencies.version>還需要在 leaf-server 的 pom 中增加 nacos 相關的依賴,因為我們 kitty-cloud 是用的 nacos。同時還需要依賴 dubbo,才可以暴露 rpc 服務。

<dependency> <groupId>com.cxytiandi</groupId> <artifactId>kitty-spring-cloud-starter-nacos</artifactId> <version>1.0-SNAPSHOT</version></dependency><dependency> <groupId>com.cxytiandi</groupId> <artifactId>kitty-spring-cloud-starter-dubbo</artifactId> <version>1.0-SNAPSHOT</version></dependency><dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId></dependency>在 resource 下創建 bootstrap.properties 文件,增加 nacos 相關的配置信息。

spring.application.name=LeafSnowflakedubbo.scan.base-packages=com.sankuai.inf.leaf.server.controllerdubbo.protocol.name=dubbodubbo.protocol.port=20086dubbo.registry.address=spring-cloud://localhostspring.cloud.nacos.discovery.server-addr=47.105.66.210:8848spring.cloud.nacos.config.server-addr=${spring.cloud.nacos.discovery.server-addr}Leaf 默認暴露的 Rest 服務是 LeafController 中,現在的需求是既要暴露 Rest 又要暴露 RPC 服務,所以我們抽出兩個接口。一個是 Segment 模式,一個是 Snowflake 模式。

Segment 模式調用客戶端

/** * 分布式ID服務客戶端-Segment模式 * * @作者 尹吉歡 * @個人微信 jihuan900 * @微信公眾號 猿天地 * @GitHub https://github.com/yinjihuan * @作者介紹 http://cxytiandi.com/about * @時間 2020-04-06 16:20 */@FeignClient("${kitty.id.segment.name:LeafSegment}")public interface DistributedIdLeafSegmentRemoteService { @RequestMapping(value = "/api/segment/get/{key}") String getSegmentId(@PathVariable("key") String key);}Snowflake 模式調用客戶端

/** * 分布式ID服務客戶端-Snowflake模式 * * @作者 尹吉歡 * @個人微信 jihuan900 * @微信公眾號 猿天地 * @GitHub https://github.com/yinjihuan * @作者介紹 http://cxytiandi.com/about * @時間 2020-04-06 16:20 */@FeignClient("${kitty.id.snowflake.name:LeafSnowflake}")public interface DistributedIdLeafSnowflakeRemoteService { @RequestMapping(value = "/api/snowflake/get/{key}") String getSnowflakeId(@PathVariable("key") String key);}使用方可以根據使用場景來決定用 RPC 還是 Http 進行調用,如果用 RPC 就@Reference 注入 Client,如果要用 Http 就用@Autowired 注入 Client。

最後改造 LeafController 同時暴露兩種協議即可。

@Service(version = "1.0.0", group = "default")@RestControllerpublic class LeafController implements DistributedIdLeafSnowflakeRemoteService, DistributedIdLeafSegmentRemoteService { private Logger logger = LoggerFactory.getLogger(LeafController.class); @Autowired private SegmentService segmentService; @Autowired private SnowflakeService snowflakeService; @Override public String getSegmentId(@PathVariable("key") String key) { return get(key, segmentService.getId(key)); } @Override public String getSnowflakeId(@PathVariable("key") String key) { return get(key, snowflakeService.getId(key)); } private String get(@PathVariable("key") String key, Result id) { Result result; if (key == null || key.isEmpty()) { throw new NoKeyException(); } result = id; if (result.getStatus().equals(Status.EXCEPTION)) { throw new LeafServerException(result.toString()); } return String.valueOf(result.getId()); }}擴展後的源碼參考:https://github.com/yinjihuan/Leaf/tree/rpc_support[2]

感興趣的 Star 下唄:https://github.com/yinjihuan/kitty[3]

關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud 微服務-全棧技術與案例解析》, 《Spring Cloud 微服務 入門 實戰與進階》作者, 公眾號 猿天地發起人。個人微信 jihuan900,歡迎勾搭。

參考資料

[1]mt-leaf.html: https://tech.meituan.com/2017/04/21/mt-leaf.html

[2]Leaf/tree/rpc_support: https://github.com/yinjihuan/Leaf/tree/rpc_support

[3]kitty: https://github.com/yinjihuan/kitty

相關焦點

  • 8種分布式ID生成方法
    前言業務量小於500W或數據容量小於2G的時候單獨一個mysql即可提供服務,再大點的時候就進行讀寫分離也可以應付過來。但當主從同步也扛不住的時候就需要分表分庫了,但分庫分表後需要有一個唯一ID來標識一條數據,且這個唯一ID還必須有規則,能輔助我們解決分庫分表的一些問題。
  • 分布式ID | 這六種分布式ID生成方法,總有一款適合你
    我是小小,我們又見面了,我們今天的話題是六種分布式ID生成算法。分布式ID簡介什麼是分布式ID在數據量不大的時候,單庫單表完全可以支撐現有業務,數據量再大一點搞個MySql主從同步也可以。數據量增長,到後期,需要進行分庫分表,顯然,這個時候需要一個全局唯一ID,而這個訂單號就是分布式ID。
  • 最常用的分布式 ID 解決方案,都在這裡了!
    在複雜的分布式系統中,往往也需要對大量的數據和消息進行唯一標識。舉個例子,資料庫的ID欄位在單體的情況下可以使用自增來作為ID,但是對數據分庫分表後一定需要一個唯一的ID來標識一條數據,這個ID就是分布式ID。對於分布式ID而言,也需要具備分布式系統的特點:高並發,高可用,高性能等特點。
  • 最常用的分布式ID解決方案,你知道幾個?
    二、分布式ID實現方案下表為一些常用方案對比:描述優點缺點UUIDUUID是通用唯一標識碼的縮寫,其目的是上分布式系統中的所有元素都有唯一的辨識信息,而不需要通過中央控制器來指定唯一標識。1. 降低全局節點的壓力,使得主鍵生成速度更快;2.
  • 40張圖帶你看懂分布式追蹤系統原理及實踐
    全局 trace_id:這是顯然的,這樣才能把每一個子調用與最初的請求關聯起來span_id: 圖中的 0,1,1.1,2,這樣就能標識是哪一個調用parent_span_id:比如 b 調用 d 的  span_id 是 1.1,那麼它的 parent_span_id 即為 a 調用 b 的 span_id 即 1,這樣才能把兩個緊鄰的調用關聯起來。
  • 分庫分表之後,id 主鍵如何處理?
    這個方案的好處就是方便簡單,誰都會用;缺點就是單庫生成自增 id,要是高並發的話,就會有瓶頸的;如果你硬是要改進一下,那麼就專門開一個服務出來,這個服務每次就拿到當前 id 最大值,然後自己遞增幾個 id,一次性返回一批 id,然後再把當前最大 id 值修改成遞增幾個 id 之後的一個值;但是無論如何都是基於單個資料庫。
  • 雲時代的分布式資料庫:阿里分布式資料庫服務DRDS
    然而,當我們服務的應用從十幾個增長到幾百個的時候,大量的中小應用加入,大家紛紛表示,原來的方案限制太大,很多應用其實只是希望做個讀寫分離,希望能有更好的SQL兼容性。於是,我們做了第一次重大升級,在這次升級裡,我們提出了一個重要的概念就是三層架構,Matrix對應資料庫切分場景,對SQL有一定限制,Group對應讀寫分離和高可用場景,對SQL幾乎沒有限制。如圖2所示。
  • xsequence 1.5 發布,分布式序列號生成組件
    項目介紹微服務時代,我們需要生產一個連續的友好的序列號,例如訂單號等。變得比較麻煩。
  • 「技術貼」微服務中臺技術解析之分布式事務方案和實踐
    但是某個消費者組對一個partition上的消息,有可能並不需要全部串行消費。比如某個服務認為消息A、B和C雖然都被劃分到了partition 0,但是只有A和C之間存在次序關係(比如更新的是同一條數據),B可以與A、C並行消費。
  • 微服務 SpringCloud Alibaba Seata處理分布式事務
    SpringCloud Alibaba Seata處理分布式事務一、分布式事務問題什麼是分布式事務?一次業務操作需要跨多個數據源或需要跨多個系統進行遠程調用,就會產生分布式事務問題。例如,在微服務分布式架構中,一次網上購買操作涉及到,訂單系統,支付系統,積分系統,庫存系統,物流系統。一個業務邏輯,分別對應不同的系統,不同的數據源,其中一環出現問題,需要全部回退,這就是分布式事務要解決的問題。
  • 面試必備的分布式事物方案
    在下單邏輯裡面(Producer 端),我們先生成一個訂單的數據,比如訂單號,數量等關鍵的信息,先包裝成一條消息,並把消息的狀態置為 init ,然後發送到 獨立消息服務中,並且入庫。接下來繼續處理 下單的其他本地的邏輯。
  • 使用Knockout和IPFS來構建一個分布式的聊天應用程式
    他們最大的優點之一是避免任何一點失敗,不像傳統的APP一樣,沒有一個實體能夠完全控制他們的運作。DApp是一個相對較新的概念(所以仍然難以給出標準定義),但是最廣泛的一組例子就是在以太坊上運行的智能合約。隨著新的分布式技術,例如區塊鏈和諸如星際文件系統(IPFS)之類的項目受到越來越多關注,勢頭越來越強勁,DApps也隨之越來越受歡迎。
  • Dapper: 大規模分布式系統鏈路追蹤基礎設施
    現代網際網路通常被實現為複雜的大規模分布式微服務系統。這些應用可能是由不同團隊使用不同程式語言開發的軟體模塊集合構建的,並且可能跨越數千臺計算機及多個物理設備。在這樣的環境中,有一個幫助理解系統行為和關於性能問題推理的工具顯得非常寶貴。
  • 分布式SQL查詢引擎之Presto
    Presto 是一個開放原始碼的分布式SQL 查詢引擎,用於對大小從GB 到PB 的各種數據源運行交互式分析查詢。Presto 是專為交互式分析而設計和編寫的,可在擴展到Facebook 之類的組織規模的同時,實現商業數據倉庫的速度。Presto 能做什麼?
  • Node.js 中實踐基於 Redis 的分布式鎖實現
    作者簡介:五月君,Nodejs Developer,慕課網認證作者,熱愛技術、喜歡分享的 90 後青年,歡迎關注 Nodejs技術棧 和 Github 開源項目 https://www.nodejs.red認識線程、進程、分布式鎖線程鎖:單線程編程模式下請求是順序的,一個好處是不需要考慮線程安全、資源競爭問題
  • PHP唯一ID生成模塊 Ukey V0.1 發布
    Ukey是一個生成唯一ID的PHP擴展模塊, 其安裝Twitter的 Snowflake算法來生成ID, 所以效率非常高, 而且唯一性非常好
  • 分布式系列之一:架構的演進過程
    節點是指一個可以獨立按照分布式協議完成一組邏輯的程序個體。在具體的項目中,一個節點表示的是一個作業系統上的進程。如上面的切菜是一個節點,洗菜也是一個節點 副本機制 副本(replica/copy)指在分布式系統中為數據或服務提供的冗餘。數據副本指在不同的節點上持久化同一份數據,當出現某一個節點的數據丟失時, 可以從副本上讀取到數據。數據副本是分布式系統中解決數據丟失問題的唯一手段。
  • 分布式事務(一) 兩階段提交及JTA
    分布式事務簡介分布式事務是指會涉及到操作多個資料庫(或者提供事務語義的系統,如JMS)的事務。其實就是將對同一資料庫事務的概念擴大到了對多個資料庫的事務。目的是為了保證分布式系統中事務操作的原子性。分布式事務處理的關鍵是必須有一種方法可以知道事務在任何地方所做的所有動作,提交或回滾事務的決定必須產生統一的結果(全部提交或全部回滾)。
  • 分布式深度學習最佳入門(踩坑)指南
    DDP提供了數據並行相關的分布式訓練接口;RPC提供了數據並行之外,其他類型的分布式訓練如參數伺服器模式、pipeline並行模式,使用的是P2P點對點通信;而c10d是一個用於集合通信的庫,作為DDP的組件為其提供服務。
  • PHP唯一ID生成模塊 Ukey V0.2 發布
    Ukey是一個生成唯一ID的PHP擴展模塊, 其按照Twitter的 Snowflake算法來生成ID, 所以效率非常高, 而且唯一性非常好.