每秒上傳超過25張圖和90個「喜歡」,在Instagram咱們存了不少數據,爲了確保把重要的數據都扔到內存裏,達到快速響應用戶的請求,咱們已經開始把數據進行分片-換句話說,把數據放到更多的小桶子裏,每一個桶了裝一部分數據。mysql
咱們的應用服務器跑的是Django和後端是PostgreSQL,在決定要分片後的第一個問題是,是否還繼續用PostgreSQL做爲主要數據倉庫,或者換成別的?咱們評估了一些NoSQL的解決方案,但最終決定最好的解決方案是:把數據分片到不一樣的PostgreSQL數據庫。git
在寫數據到不一樣服務器以前,還須要解決一個問題,如何給在數據庫裏的每塊數據都標識上惟一的標識(如,發佈到咱們系統的每張圖)。單庫好解決,就是用自增主鍵-但若是數據同時寫到多個庫就不行了,本博客將回答若是解決這個問題。github
開始前,先列出系統的主要實現目標:web
生成的ID能夠按時間排序(如,一個圖片列表的id,能夠不用獲取更多信息便可直接排序)
sql
ID最好是64位的(這樣索引更小,存儲的也更好,像Redis)
mongodb
系統最好儘量地只有部分是「可變因素」-很大部分緣由爲什麼在不多工程師的狀況下能夠擴展Instagram,就是由於咱們相信簡單好用!shell
不少相似的ID解決方案都有些問題,下面是一小部分例子:數據庫
在web應用層生成ID
這類方法把生成ID的任務都扔到應用層實現,而不是數據庫層。如,MongoDB’s ObjectId,是一個12字節長的編碼的時間戳做爲第一部分,另一種流行的方法是用UUIDs。django
優勢:編程
每一個應用服務生成的ID是獨立的,生成時將失敗和競爭降到最小;
若是用時間戳做爲第一部分,就能夠按時間排序
劣勢:
須要更多存儲空間(96位或更多)才能保證惟一性;
一些UUID類型的徹底是隨機數,沒有排序特性;
由單獨的服務提供ID生成
如:Twitter的Snowflake,是一個Thrift服務用到Apache ZooKeeper協調各節點並生成一個惟一的64位ID。
優點:
Snowflake生成的ID是64位,只用UUID的一半大小;
能夠把時間排到前面,能夠排序;
分佈式系統能夠保證服務不會掛掉;
劣勢:
系統會變得更復雜和更多的「可變因素」(ZooKeeper, Snowflake 服務)加入到咱們的架構。
數據庫計數服務器
用數據庫自增字段的能力來保證惟一性(Flickr用了這個方法),但用了兩臺計數服務器(一臺是生成奇數,另一臺是偶數)才能避免單點失效。
優點:
數據庫好理解,擴展很容易預測要考慮的因素;
劣勢:
可能最終變成寫入是個瓶頸(儘管Flickr報告過這一點,但在高擴展下並非個問題);
新增了兩臺服務器要管理(或是EC2實例);
若是用單臺數據庫,會有單點失效問題,若是用多個庫,不能保證他們是可按時間排序的;
全部以上的方法中,Twitter的Snowflake最接近,但添加生成ID服務了複雜調用又衝突了,替換的方案是,咱們使用了概念相似的方法,可是從PostgreSQL內部特性實現的。
咱們的分片系統由幾千個邏輯分片組成,由代碼指向極少的幾個物理分片,用這個方法,咱們可用少數幾臺服務器就能夠實施起來,之後也能夠擴展到更多,只要簡單的將邏輯分片從一臺物理數據器移到另一臺,不須要從新聚合各分片的數據,咱們用PostgreSQL的schema特性很容易就作到實施和管理。
Schema(不要跟建單個表的SQL schema搞混了。相似oracle的tablespace表空間 --- 譯者)在PostgreSQL是一個邏輯分組的功能,每臺PostgreSQL有多個schema,每一個schema可包含一張或多張表,表名在每一個schema裏是惟一的,不是每一個庫,PostgreSQL默認把全部東西都放到一個叫public的schema裏。
咱們系統裏每一個邏輯分片就是一個schema,每一個分片的表(如,照片的「喜歡」功能)存在於每一個schema中。
咱們在每一個分片的每張表裏用PL/PGSQL(PostgreSQL內部編程語言)和自增特性來建立ID。
每一個ID包含有:
41位的毫秒時間(能夠用41年的ID);
13位表示邏輯ID;
10位自增序列,與1024取模,意味着每一個分片每毫秒能夠生成1024 個ID;
(譯者:上述設計若是要保證自增id可跟蹤的話,其設計不夠合理,由於最後10位自增序列與1024取模後將不能保持原來的自增id信息,參見pinterest的設計應該更合理,若是我在此ID能夠分析出自增id的話。請閱讀者本身判斷。)
假設如今是2011年9月9號下午5:00,系統的紀元開始是2011年9月1日,從紀元開始到如今已經通過了1387263000毫秒,爲生成ID,用左移方法填充最左邊41位值是:
id = 1387263000 << (64-41)
下一步,若是生成這個要插入數據的分片的ID呢?假設咱們用用戶ID(user ID)來分片,同時已經有2000個邏輯分片,若是用戶ID是31341,那麼分片ID是 31341 % 2000 -> 1341,用這個值也填充接下來的13位:
id |= 1341 << (64-41-13)
最後,來生成最後自增的序列值(這個序列對每一個schema每張表是惟一的)並填充完剩下的幾位,假設這張表已經生成了5000個ID,下一個值便是5001,跟1024取模(恰好10位),包含進來:
id |= (5001 % 1024)
ID生成了!用RETURNING返回給應用層用來做INSERT用。
下面是完整的PL/PGSQL代碼(例子中的schema是 insta5):
CREATE OR REPLACE FUNCTION insta5.next_id(OUT result bigint) AS $ DECLARE our_epoch bigint := 1314220021721; seq_id bigint; now_millis bigint; shard_id int := 5; BEGIN SELECT nextval('insta5.table_id_seq') %% 1024 INTO seq_id; SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) INTO now_millis; result := (now_millis - our_epoch) << 23; result := result | (shard_id << 10); result := result | (seq_id); END; $ LANGUAGE PLPGSQL;
用下面的代碼建立表:
CREATE TABLE insta5.our_table ( "id" bigint NOT NULL DEFAULT insta5.next_id(), ...rest of table schema... )
就這些!主鍵在全部應用層都是惟一的(另外的好處是,包含了分片ID這樣作映射就很容易),這個方法咱們已經用到生產環境了,結果到目前爲止使人滿意,若是您對擴展問題能幫助咱們,咱們正在招人!
Mike Krieger, co-founder
附:生成ID的MySQL 版本
CREATE DEFINER = `root`@`localhost` FUNCTION `next_id`(the_table_name varchar(255)) RETURNS bigint(64) LANGUAGE SQL DETERMINISTIC READS SQL DATA SQL SECURITY DEFINER COMMENT '' BEGIN DECLARE result bigint; /*next auto increment id of the_table_name*/ DECLARE seq_id bigint; DECLARE now_millis bigint; /*total shard number*/ DECLARE shard_total int; /*current shard amount*/ set shard_total = 1; if the_table_name IS NULL OR the_table_name = '' then return 0; end if; /*next autoincrement id*/ SELECT AUTO_INCREMENT INTO seq_id FROM information_schema.tables WHERE table_name = the_table_name AND table_schema = DATABASE( ) ; /*curremnt time - in seconds*/ SELECT UNIX_TIMESTAMP() INTO now_millis; /*generate 64bit ID */ /*1. 41 bits time. 64-41 */ set result = now_millis << 23; /*2. 13 bit logic sharding id. 64-41-13*/ set result = result | ((seq_id%shard_total) << 10); /*3. 10 bits auto increment id*/ set seq_id = seq_id % 1024; set result = result | seq_id; return result ; END
參考另一個mysql版本的實現
http://stackoverflow.com/questions/25677554/can-auto-increment-be-safely-used-in-a-before-trigger-in-mysql
2015.7.31 關於自增id的問題
由於分片後數據是分散的,分片的id若是是自增id,將致使肯定不了下一個自增Id是從哪臺分片庫表獲取,對於postgresql或oracle等,因其自增id是用sequence產生的,而sequence是獨立於表的,全部沒有這個問題,mysql就有問題,考慮的作法是,單首創建一個只產品自增id的計數器記錄最後一個id,代替sequence的做用。
第2個問題是,mysql的last_insert_id()函數是否穩定有效,還有待觀察。
第3個問題,在數據庫層生產shard id好像不太靠譜(如用trigger生成的方案),由於尋找分片實例是在進入數據庫層操做以前就要預先肯定的,而後再鏈接分片實例去建立shard的id信息。因此應該在應用層實現較靈活。
英文原文 http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram
<譯者:朱淦 350050183@qq.com 2015.7.29>