原文:http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram
Instagram的存储量非常大,差不多每秒25-90张照片。为了保证我们的重要的数据能够合理的存储以便快速的提取应用,我们对数据进行了分片 -- 换句话说,将数据放到很多小的桶(buckets)中,每个桶存储一部分的数据。
我们的应用服务器跑的是Django ,后端数据库采用PostgreSQL 。我们决定采用分片后,第一个问题是我们是否还保留我们的主数据库,是否应该转换到其他的存储方案。我们评估了一些不同的NOSQL的解决方案,但是最后觉得最适合我们的需求的还是通过PostgreSQL server集群实现分片。
在将数据写入数据库集群前,我们必须解决如何分配数据唯一标识的问题(例如,上传上来的每一张照片)。在一个单数据的情况下,典型的解决方案是使用数据库原有的自增主键特性即可,但是这种方法在同时插入多个数据库的情况下不可行。这篇博客其他的部分将讲述我们如何解决这个问题的。
开始之前,我们列出了我们系统的基本特性:
生成的ID应该按时间排序(这样一个照片ID 的列表就足够了,并不需要照片的其他信息)
ID应该是64位的(为了更小的索引以及更好的存储在像Redis这样的系统中)
该系统应引入尽可能少的考虑新的“移动部件”(这里指额外的技术部件) --- 我们如何能够一直用很少的工程师扩展Instagram是因为选择简单、易于理解的解决方案。(The system should introduce as few new ‘moving parts’ as possible—a large part of how we’ve been able to scale Instagram with very few engineers is by choosing simple, easy-to-understand solutions that we trust. )
现成的解决方案
关于ID生成有很多现成的解决方案,这些是我们曾经考虑过的: 在web程序中生成ID
这个方案是将整个ID生成逻辑放到应用程序层而非数据库层。例如:MongoDB的ObjectId,采用12个字节的长度,并且将时间戳进行编码。另外一种流行的方案是使用UUIDs。
赞成者:
每个应用程序线程独立生成ID,这样最大限度减小了生成ID冲突的概率
如果你采用时间戳编码,得到的ID将保持按时间排序、
反对者:
通常需要更多的存储空间(96位或更高)以保证唯一性约束
一些UUID类型是完全随机的,没有自然顺序 通过专门的服务生成ID;
例如:Twitter的 Snowflake,一个Thrift 服务使用Apache ZooKeeper 去协调节点并生成64位的唯一标识ID
赞成者:
Snowflake ID是64位的,只有UUID的一般大小;
可以使用时间编码,保持有序性
适用于分布式系统(Distributed system that can survive nodes dying )
Finally, we take whatever the next value of our auto-increment sequence (this sequence is unique to each table in each schema) and fill out the remaining bits. Let’s say we’d generated 5,000 IDs for this table already; our next value is 5,001, which we take and mod by 1024 (so it fits in 10 bits) and include it too:
id |= (5001 % 1024)
最后,我们将通过自增长序列(这个序列的唯一性针对在每一个schema的每一个表,即每个schema的每个表有自己的序列)获取下一个值,并填入到余下的位中。假设我们已经为这个表生成了5000个序列ID,我们下一个序列ID应该是5001,我们用5001模1024,即为余下的ID值:
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;
And when creating the table, we do:
CREATE TABLE insta5.our_table (
"id" bigint NOT NULL DEFAULT insta5.next_id(),
...rest of table schema...
)
就这样!主键在我们的应用程序中是唯一的(额外的好处,包含在其中切分ID为更易于映射)。我们已经将这一方案应用到我们的产品中了,到目前为止效果良好。有兴趣帮助我们找出问题吗?我们正在招聘!
Mike Krieger, co-founder