1. 程式人生 > >分布式系統中生成全局ID的總結與思考

分布式系統中生成全局ID的總結與思考

ash you timestamp part 組成 我認 發的 映射 style

  世間萬物,都有自己唯一的標識,比如人,每個人都有自己的指紋(白夜追兇給我科普的,同卵雙胞胎DNA一樣,但指紋不一樣)。又如中國人,每個中國人有自己的身份證。對於計算機,很多時候,也需要為每一份數據生成唯一的標識。在這裏,數據的概念是非常寬泛的,比如數據量記錄、文件、消息,而唯一的標識我們稱之為id。

  本文地址:http://www.cnblogs.com/xybaby/p/7616272.html

自增ID

  使用過mysql的同學應該都知道,經常用自增id(auto increment)作為主鍵,這是一個為long的整數類型,每插入一條記錄,該值就會增加1,這樣每條記錄都有了唯一的id。自增id應該是使用最廣泛的id生成方式,其優點在於非常簡單、對數據庫索引友好、而且也能透露出一些信息,比如當前有多少條記錄(當然,用戶也可能通過id猜出總共有多少用戶,這就不太好)。但自增ID也有一些缺點:第一,id攜帶的信息太少,只能起到一個標識作用;第二,現在啥都是分布式的,如果多個mysql組成一個邏輯上的‘mysql’(比如水平分庫這種情況),每個物理mysql都使用自增id,局部來說是唯一的,但總體來說就不唯一了。

  於是乎,我們需要為分布式系統生成全局唯一的id。最簡單的辦法,部署一個單點,比如單獨的服務(mysql)專門負責生成id,所有需要id的應用都通過這個單點獲取一個唯一的id,這樣就能保證系統中id的全局唯一性。但是分布式系統中最怕的就是單點故障(single point of failure),單點故障是可靠性、可用性的頭號天敵,因此即使是中心化服務(centralized service)也會搞成一個集群,比如zookeeper。按照這個思路,就有了Flicker的解決方案。

  Flicker的解決辦法叫《Ticket Servers: Distributed Unique Primary Keys on the Cheap》,文章篇幅不長,而且通俗易懂,這裏也有中文翻譯。簡單來說,Flicker是用兩組(多組)mysql來提供全局id的生成,多組mysql避免了單點,那麽怎麽保證多組mysql生成的id全局唯一呢,這就利用了mysql的自增id以及replace into語法。

  大家都知道mysql的自增id,但是不一定知道其實可以設置自增id的初始值以及自增步長, Flicker中的示例中,兩個mysql(ticketserver)初始值分別是1和2,自增步長都是2(而不是默認值1),這樣,ticketserver1永遠生成奇數的id,而ticketserver2永遠生成偶數的id

TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1

TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2

  那麽怎麽獲取這個id呢,不可能每需要一個id的時候都插入一條記錄,這個時候就用到了replace into語法。 replace是insert、update的結合體,對於一條待插入的記錄,如果其主鍵或者唯一索引的值已經存在表中的話,那麽會刪除舊的那條記錄,然後插入新的記錄;如果不存在,那麽直接插入記錄。這個非常類似mongodb中的findandmodify語法。在Flicker中,是這麽使用的,首先schema如下:

CREATE TABLE `Tickets64` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`stub` char(1) NOT NULL default ‘‘,
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=MyISAM

  註意,id是主鍵,而stub是唯一索引,當需要產生一個id的時候,使用以下sql語句;

REPLACE INTO Tickets64 (stub) VALUES (‘a‘);
SELECT LAST_INSERT_ID();

  由於stub是唯一索引,當每次都插入‘a‘的時候,會產生新的記錄,而新記錄的id是自增的(則增步長為2)

  Flicker的解決辦法通俗易懂,但還是沒有解決id信息過少的問題,而且還是依賴單獨的一組服務(mysql)來生成全局id。如果全局id的生成不依賴額外的服務,而且包含豐富的信息那就最好了。

攜帶時間與空間信息的ID

UUID  

  提到全局id,首先想到的肯定是UUID(Universally unique identifier),從名字就能看出,這個是專門用來生成全局id的。而UUID又分為多個版本,不同的語言,廠家都有自己的實現。本文對uuid的介紹主要參考rfc4122,如下圖所示,一個uuid由一下部分組成:

  技術分享  

技術分享   可以看到,uuid包含128個bit、即16個字節,其中包含了時間信息、版本號信息、機器信息。uuid也不是說一定能保證不沖突,但其沖突的概率小到可以忽略不計。使用uuid就不用再使用額外的id生成服務了。但缺點也有明顯:太長,16個字節!太長有什麽問題呢,占用空間?問題不大。主要的問題,是太長且隨機的id對索引的不友好。在《Are you designing Primary Keys and ID’s???Well think twice..》一文中,作者也許需要用uuid來代替自增id,作者指出:
  So what do we do change ID’s to UUID as well. Well no, that’s not a good idea because we will simply increase work for our database server. It will now have to index a random string of 128 bit. The data will be more fragmented and bigger to fit in memory. This will definitely bring down the performance of our system.
  測試結果如下:

   技術分享  

  第一例是當前db中有多少條記錄,第二列是使用uuid作為key時插入1 million條記錄耗費的時間,第三列是使用64位的整形作為key時插入1 million條記錄耗費的時間。從結果可以看出,隨著數據規模增大,使用uuid時的插入速度遠小於使用整形的情況。

  既然uuid太長了,那後來者都是在uuid的基礎上盡量縮短id的長度,使之更加實用。我認為,如果使用時間信息、機器信息來生成id的話,那麽應該就是借鑒了uuid的做法,包含但不限於:twitter的snowflake,mongodb的ObjectId。

MongoDB ObjectId

ObjectId is a 12-byte BSON type, constructed using:
  • a 4-byte value representing the seconds since the Unix epoch,
  • a 3-byte machine identifier,
  • a 2-byte process id, and
  • a 3-byte counter, starting with a random value.
  objectid有12個字節,包含時間信息、機器表示、進程id、計數器。在mongo.exe中,通過ObjectId.getTimestamp可以獲取時間信息
mongos> x = ObjectId() ObjectId("59cf6033858d9d5a85caac02") mongos> x.getTimestamp() ISODate("2017-09-30T09:13:23Z")
  MongoDb的各語言驅動都實現了ObjectId的生成算法,比如PyMongo,在bson.objectid.py裏面。通過ObjectId的生成算法以及mongo shell、pymongo的例子,我們可以看到,objectid的生成是由驅動負責的,而不是MongoDB負責,這樣減輕了MongoDB負擔,也達到了去中心化服務的目的。

結構化ID思考

  這裏的結構化ID,就是指按一定規則,用時間、空間(機器)信息生成的ID,上面介紹的UUID以及各種變種都屬於結構化id。

  結構化ID的優點在於充足的信息,最有用的肯定是時間信息,通過ID就能直接拿到數據的創建時間了;另外,天然起到了冷熱數據的分離。當然,有利必有弊,比如在ID作為分片鍵的分片環境中,如果ID包含時間信息,那麽很可能在短時間內生成的數據會落在同一個分片。在《帶著問題學習分布式系統之數據分片》一文中,介紹了MongoDB分片的兩種方式:“hash partition”與“range partition“,如果使用ObjectId作為sharding key,且sharding方式為range partition,那麽批量導入數據的時候就會導致數據落在同一個shard,結果就是大量chunk的split和migration,這是不太好的。

TFS文件名

  如果結構化ID中包含分片信息,那就更好了,這樣就不會再維護數據與分片的信息,而是直接通過id找出對應的分片。我們來看看TFS的例子

  TFS是淘寶研發的分布式文件存儲系,其的結構一定程度上參考了GFS(HDFS),元數據服務器稱之為Nameserver,實際的數據存儲服務器稱之為Dataserver。TFS將多個小文件合並成一個大文件,稱之為block,block是真實的物理存儲單元。因此,DataServer負責存儲Block,而NameServer維護block與DataServer的映射。那麽小文件與block的映射關系在哪裏維護呢?要知道小文件的量是很大的

  TFS的文件名由塊號和文件號通過某種對應關系組成,最大長度為18字節。文件名固定以T開始,第二字節為該集群的編號(可以在配置項中指定,取值範圍 1~9)。余下的字節由Block ID和File ID通過一定的編碼方式得到。文件名由客戶端程序進行編碼和解碼

  如圖所示:

     技術分享

  從上圖可以看到,最終的文件名是包含了block id信息的的,那麽如何利用這個blockid信息呢,如下圖所示:

    技術分享

  當需要根據文件名獲取文件內容的時候,TFS的客戶端,首先通過文件名解析出Block id與File id,然後從NameServer上根據Block id查詢block所在的DataServer。然後從DataServer上根據Block id拿到對應的block,在根據file id從block中找到對應的文件。

  

  TFS用於存儲淘寶大量的小文件,比如商品的各種尺寸的小圖片,這個數量是非常大的,如果用單獨的元數據服務器維護文件名與文件信息的映射,這個量是非常大的。而使用攜帶block id信息的文件名,很好規避了這個問題。但使用這種攜帶分區信息的ID時,需要考慮數據在分區之間的遷移情況,ID一般來說使不能變的,因此ID映射的應該是一個邏輯分區,而不是真正的物理分區。

總結

   本文介紹了分布式系統中,全局唯一ID的生成方法。ID主要有兩種類型,一種是數字自增ID,如flicker的解決方案;另一種是攜帶時間、機器信息的組合ID,如uuid。分布式系統中,好的全局ID生成算法首先是需要避免單點,如果不需要中心化服務的話更好;另外,攜帶時間信息、分片信息的ID更加實用。

references

Ticket Servers: Distributed Unique Primary Keys on the Cheap

UUID(Universally unique identifier)

rfc4122

Are you designing Primary Keys and ID’s???Well think twice..

TFS

分布式系統中生成全局ID的總結與思考