當前位置:
首頁 > 知識 > 分散式系統中生成全局ID的總結與思考

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


自增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由一下部分組成:

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

可以看到,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.

測試結果如下:

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

第一例是當前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通過一定的編碼方式得到。文件名由客戶端程序進行編碼和解碼

如圖所示:

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

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

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

當需要根據文件名獲取文件內容的時候,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更加實用。



更多優質內容推薦:

2017優就業就業促進計劃:http://www.ujiuye.com/zt/jycj/?wt.bd=zdy35845tt

中公教育「勤工儉學計劃」,給你一個真正0元學習IT的機會!

http://www.ujiuye.com/zt/qgjx/?wt.bd=zdy35845tt

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 IT優就業 的精彩文章:

演算法的複雜度學習筆記
實例說明MVC,MVP,MVVM架構
Eclipse連接SQL Server 2008資料庫以及問題總結
JPEG流封裝AVI視頻

TAG:IT優就業 |