Instagram架构的分片和ID设计

前言

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

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

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

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

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

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

  3. 系统最好尽可能地只有部分是“可变因素”-很大部分原因为何在很少工程师的情况下可以扩展Instagram,就是因为我们相信简单好用!

现有的解决方案

很多类似的ID解决方案都有些问题,下面是一小部分例子:

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

优点:

  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>





转载于:https://my.oschina.net/swingcoder/blog/484947

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值