每秒上传超过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>