Instagram架构的分片和ID设计

前言

每秒上传超过25张图和90个“喜欢”,在Instagram咱们存了不少数据,为了确保把重要的数据都扔到内存里,达到快速响应用户的请求,咱们已经开始把数据进行分片-换句话说,把数据放到更多的小桶子里,每一个桶了装一部分数据。mysql

咱们的应用服务器跑的是Django和后端是PostgreSQL,在决定要分片后的第一个问题是,是否还继续用PostgreSQL做为主要数据仓库,或者换成别的?咱们评估了一些NoSQL的解决方案,但最终决定最好的解决方案是:把数据分片到不一样的PostgreSQL数据库。git

在写数据到不一样服务器以前,还须要解决一个问题,如何给在数据库里的每块数据都标识上惟一的标识(如,发布到咱们系统的每张图)。单库好解决,就是用自增主键-但若是数据同时写到多个库就不行了,本博客将回答若是解决这个问题。github

开始前,先列出系统的主要实现目标:web

  1. 生成的ID能够按时间排序(如,一个图片列表的id,能够不用获取更多信息便可直接排序)
    sql

  2. ID最好是64位的(这样索引更小,存储的也更好,像Redis)
    mongodb

  3. 系统最好尽量地只有部分是“可变因素”-很大部分缘由为什么在不多工程师的状况下能够扩展Instagram,就是由于咱们相信简单好用!shell

现有的解决方案

不少相似的ID解决方案都有些问题,下面是一小部分例子:数据库

在web应用层生成ID
这类方法把生成ID的任务都扔到应用层实现,而不是数据库层。如,MongoDB’s ObjectId,是一个12字节长的编码的时间戳做为第一部分,另一种流行的方法是用UUIDs。django

优势:编程

  1. 每一个应用服务生成的ID是独立的,生成时将失败和竞争降到最小;

  2. 若是用时间戳做为第一部分,就能够按时间排序

劣势:

  1. 须要更多存储空间(96位或更多)才能保证惟一性;

  2. 一些UUID类型的彻底是随机数,没有排序特性;

由单独的服务提供ID生成

如:Twitter的Snowflake,是一个Thrift服务用到Apache ZooKeeper协调各节点并生成一个惟一的64位ID。

优点:

  1. Snowflake生成的ID是64位,只用UUID的一半大小;

  2. 能够把时间排到前面,能够排序;

  3. 分布式系统能够保证服务不会挂掉;

劣势:

  1. 系统会变得更复杂和更多的“可变因素”(ZooKeeper, Snowflake 服务)加入到咱们的架构。

数据库计数服务器

用数据库自增字段的能力来保证惟一性(Flickr用了这个方法),但用了两台计数服务器(一台是生成奇数,另一台是偶数)才能避免单点失效。

优点:

  1. 数据库好理解,扩展很容易预测要考虑的因素;

劣势:

  1. 可能最终变成写入是个瓶颈(尽管Flickr报告过这一点,但在高扩展下并非个问题);

  2. 新增了两台服务器要管理(或是EC2实例);

  3. 若是用单台数据库,会有单点失效问题,若是用多个库,不能保证他们是可按时间排序的;

全部以上的方法中,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>

相关文章
相关标签/搜索