专栏 | 九章算法
网址 | www.jiuzhang.com
原文标题:Sharding & IDs at Instagram
原文连接:Sharding & IDs at Instagram
摘要:Instagram 官方技术文档,关于Instagram的系统设计,经典系统设计文章。
翻译:金秋javascript
Instagram上有大量的数据,每分钟就有超过25张的图片和90个点赞。为了确保全部重要的数据都能被合理存储而且及时得被提取应用,咱们对数据进行了分片(sharding)——也就是说,咱们把数据放到多个桶(bucket)中,每一个桶里都有一部分数据。java
咱们的应用服务器上运行的是Django, 后端数据库是PostgreSQL。对数据分片首先要决定是否要保留PostgreSQL做为主要的数据存储库,是否要采用其余的数据库。通过评估一些不一样的数据库解决方案,咱们最终肯定最适合的方案是在PostgreSQL数据库集群上实现数据分片。程序员
然而在把数据写到数据库以前,咱们还要解决如何给数据(例如Instagram上发布的没一张图片)加上惟一标识符的问题。在单一数据库上的典型解法——使用数据库自带的自增主键功能——在当数据须要被同时插入到多个数据库时就不适用了。文章的下面就来说讲咱们是如何解决这个问题的。web
开始前,咱们先列出系统所须要的全部重要的功能。面试
1. 产生的数据ID需是能够按时间排序的。(好比对一列图片数据的ID进行排序,能够不须要提取太多图片自己的信息)
2. 理想的ID是64位的。(这样索引更小,存储也更优,像Redis)
3. 系统要尽可能少的引用“可变更因素”——在不多工程师的状况下还能够扩张Instagram的很大一部分缘由就是,咱们相信简单易懂的方案。复制代码
现有不少产生ID的解决方案,咱们考虑的有如下这些:算法
这种方案将ID的生成彻底交到应用层,而不是数据库。例如,MongoDB的ObjectId,就是12字节长而且在最前面加上时间戳进行编码。另外一个流行的方案是使用UUIDs。数据库
优势:编程
- 每一个应用线程独立生成ID,最小的下降ID生成的失败和竞争。
- 若是用时间戳做为ID的起始部分,那么ID能够按时间排序。
缺点:后端
- 一般须要更多的存储空间(96位或者更多)来确保ID的合理惟一性。
- 一些UUID类型彻底是随机的,没法排序。
例如:Twitter的Snowflake,是一个Thrift服务,使用了Apache Zookeeper来协调各个结点而且产生64为的惟一ID。服务器
优势:
- Snowflake的ID只有64位,是UUID的一半。
- 能够放时间戳到ID头,从而能够按时间排序。
- 分布式系统保证了系统结点不会挂掉。
缺点:
增长了复杂性,并且引入了更多的“可变更因素”(如ZooKeeper, Snowflake服务器)到系统构架中。
利用数据库自带的自增特性来确保惟一性。Flicker采用这一方法,不过还用了两台ticket数据库(一个生成偶数,一个生成奇数)来避免单点失败。
优势:
数据库好理解,带有易预测的可扩张功能。
缺点:
- 最终会出现数据写入的瓶颈(尽管Flicker称在高扩展下没有问题)。
- 须要管理多的两台服务器(或者EC2实例)。
- 若是单独使用数据库,会出现单点失效。若是使用多个数据库,则不能保证ID能够按时间排序。
在全部这些方案中,Twitter的snowflake是最接近的,可是生成ID所需的添加复杂性又和咱们的目标冲突。咱们的替换方案是采用概念上相近的方法,可是带到PostgreSQL内部实现。
咱们的分片系统是由上千个“逻辑”分片组成的,由代码映射到少许的物理分片。
经过这个方法,咱们一开始用少数数据库实现,慢慢扩展到更多个数据库,只须要把部分逻辑分片从一台数据库转移到另外一台数据库里,不须要从新把数据从新聚合。咱们用到的PostgreSQL的schema的特性能够轻松的实现计划和管理。
Schema(不要和SQL单个表的schema搞混了)是PostgreSQL里的一个逻辑分组功能。每个PostgreSQL数据库都有好几个schema,每个schema都有一到多个表。表名在没个schema中都是惟一的,而不是每一个数据库,默认状况下,PostgreSQL会把全部数据都放在一个叫“public”的schema中。
在咱们的系统中每一个“逻辑”分片都是一个PostgreSQL schema, 每一个分片的表(好比照片的“点赞”功能)都存在每一个schema中。
咱们经过使用PL/PGSQL, PostgreSQL内部的编程语言,和PostgreSQL现有的自增功能来生成ID。
每一个ID都由下面几个部分组成:
41位的毫秒级时间(用于产生41年的ID)
13位用来表示逻辑分片的ID
10位的自增序列,模上1024, 意味着每一个分片每毫秒能够产生1024个ID复制代码
下面经过一个例子说明:好比说如今是2011年的九月九日,咱们的纪元是从2011年的一月一号开始。重新纪元开始到此有1387263000毫秒,那么咱们把这个数字左移41位来填满ID的头。
id = 1387263000 <<(64 – 41)
接下来, 咱们拿来咱们准备把这个数据插入的分片ID;若是咱们用户ID是31341, 这个分片的ID是 31341%2000 → 1341。 咱们接下来把下面13为填满:
id |= 1341 << (64-41-13)
最后, 咱们用所剩的自增序列(每一个schema中每一个表中这个序列都是惟一的)来填满的后面的位数。 假设这张表中已经有了5000个ID; 咱们下一个数据就是5001,咱们把它模上1024获得:
id |= (5001%1024)
咱们就获得了咱们的ID, 咱们把这个id做为insert中的RETURNING返回给应用层。
下面是是实现以上过程的PL/PGSQL代码(这里用的schema是insta5)