Instagram是什么
Instagram是一款社交服务软件,用户可以上传图片、分享图片以及视频。用户分享自己的信息可以设置为公开的或私有的,如果是公开分享的信息可以被其他所有人看见,如果是私有的则只能被一部分特点的人看见。用户也可以在其他社交平台如Facebook、Twitter、Flickr、Tumblr上分享自己的信息。
我们接下来实现一个简单版的Instagram,用户可以分享图片,可以关注其他人。每个用户都会有一个“News feed”,展示了用户关注的所有人的图片关注度排行榜。
完整系统设计面试资料大合集参考github,欢迎STAR:
大厂系统面试合集
系统需求和设计目标分析
功能性需求
- 用户可以上传、下载、查看图片/视频
- 用户可以通过标题来搜索图片/视频
- 用户之间可以相互关注
- 系统会生成用户的“News Feed”,包含了用户关注的所有人的图片排行榜
非功能性需求
- 服务高可用
- “News feed”生成的延时,例如200ms
- 关于系统一致性,不做强制要求。比如用户短时间内看不到另一个用户新上传的图片,是可接受的
- 系统可靠性。用户上传成功的图片和视频数据不会丢失
真实的产品可能包含其他更丰富的功能,在这里暂时不做考虑:
- 给图片打标
- 根据标搜索图片
- 图片评论
一些设计上需要重点考虑的点
系统读多写少,因此我们需要聚焦于如何快速读取图片信息。
- 用户可能上传的图片数非常多,那么对存储的有效管理将是系统设计的核心要素。
- 查看图片的延时需要尽可能低。
- 数据需要确保高可靠性(100%),只要图片被上传成功,就不会丢失。
容量估算
假设:
用户量:500M=5亿,日活100万。
新增图片量级:200万/天,23/秒。
图片大小:平均200KB。
每天新增存储空间:2M*200KB≈400GB。
十年新增存储空间:400GB36510≈1425TB。
概要设计
包含模块:
- 对象存储服务,负责存储图片
- 数据库,存储图片meta信息
- 支持用户进行2种操作:上传图片和查看搜索图片
数据库设计
数据库schema
需要存储的信息包含:用户、图片、用户之间的关注关系。
我们设计3张表:
(1)图片表:photo
| 字段名 | 字段类型 | 描述 |
|---|---|---|
| id | bigint | 自增ID,主键 |
| user_id | bigint | 用户信息id |
| photo_path | varchar | 图片在对象存储上的路径 |
| photo_lat | varchar | 图片拍摄纬度 |
| photo_lon | varchar | 图片拍摄经度 |
| user_lat | varchar | 用户所在纬度 |
| user_lon | varchar | 用户所在经度 |
| create_date | datatime | 创建时间 |
(2)用户表:user
| 字段名 | 字段类型 | 描述 |
|---|---|---|
| id | bigint | 自增ID,主键 |
| name | varchar | 用户姓名 |
| varchar | 用户邮箱 | |
| birth_date | datatime | 出生时间 |
| create_date | datatime | 创建时间 |
| last_login | datatime | 最近一次登录时间 |
(3)用户关注关系表:user_relation
| 字段名 | 字段类型 | 描述 |
|---|---|---|
| id | bigint | 自增ID,主键 |
| userid_follower | bigint | 关注人的id |
| userid_followee | bigint | 被关注人的id |
数据存储技术选型
由于我们需要进行多张表的join查询,最直接的存储方式是选择关系型数据库如MySQL。但是MySQL的伸缩性方面存在瓶颈,可以考虑使用NoSQL产品。
关于SQL VS NoSQL
我们可以将上述数据存储在k-v存储上,享受NoSQL带来的益处。对于图片表,key是photoId,value则是图片LBS、创建时间等信息。还需要用一张表存用户和图片之间的所属关系、用户之前的关注关系。常见的NoSQL产品有Cassandra。
\
数据量估算
如果采用上述关系数据库来存储,下面估算下3张表所需的存储空间。
用户表
int和datatime用4字节表示。那么用户表每行记录需占用空间是68bytes:
userid(4)+name(20)+email(32)+birth_date(4)+create_date(4)+last_login(4)=68bytes
用户量若是5亿,那么空间是:32GB:
500milion * 68≈32GB
图片表
每行是284bytes
每天新增上传图片量级是200万,那么需要0.5GB:
200万*284bytes≈0.5GB
用户关注关系表
如果每行记录是8bytes,用户5亿,平均每个用户关注500个其他用户,那么总共需要1.82TB:
5亿5008bytes≈1.82TB
总控件大概是3.7TB。
\
模块设计
系统需要支持2个主要功能:上传图片、查看图片。而上传图片由于需要写磁盘,响应较慢。而读如果使用缓存的话响应会非常快。在进行系统模块设计的时候,需要注意的是,web服务器的连接数是有限的,假设web服务器可同时支撑的连接数是500,那么同时进行的写和读并发数不能超过500。由于上传操作服务响应慢,可能占满连接数,因此我们可以考虑将上传操作和读拆分成2个单独的服务,这样可以确保上传操作不会占据整个系统宝贵的连接数资源。服务分开另一个好处是我们可以单独进行容量伸缩和优化。
可靠性和冗余性设计
在此系统中,文件必须保证不会丢失。因此必须将文件冗余存储多份,以确保部分存储服务宕机后可以从冗余存储的副本上正常服务图片数据。
其他模块也是一样,如果要保证高可用,服务必须设置多个副本,这样当其中部分服务宕机后可以快速切换以确保整个系统正常运行。冗余性设计消除了系统的单点失败风险。
数据分片设计(Data Sharding)
我们考虑几种可能的分片设计方案:
(1)基于Userid分片
同一个userid的所有图片存储在相同的分片。如果单个分片存储空间是1TB,那么要支持3.7TB的空间,我们可以设置分片数是10.
通过对userid mod 10结果,得出用户应该位于的分片。
由于每个分片的db有自己的自增ID,因此要保持photoId全局唯一,可以把shardId和photoId组装起来实现唯一。
基于Userid分片可能存在的问题:
- 怎么处理热点用户?热点用户被关注的量级大,其上传的图片查看的人量级也大
- 部分用户上传的图片量级可能比其他人大很多,这会导致分片之间的存储不均衡
- 如果某个用户上传的图片在一个分片无法存放怎么办?如果我们将一个用户上传的图片存放在不同的分片上会导致延迟飙升么?
- 一个分片宕机或者延迟高会导致对应用户的所有数据的操作都面临高延迟。
(2)基于PhotoId分片
如果基于photoId来分片呢?是不是就解决了上述的几个问题呢?
photoId的生成
由于我们是基于photoId做分片ID,需要首先知道photoId才能定位都分片,因此我们生成photoId必须是由一个全局的id生成器来完成的。其中一个方案是通过DB的自增ID来实现,每次往DB表中插入一条记录,生成自增id。
采用DB自增id的方式,为了防止DB存在单点失败隐患,我们可以设置N个DB来同时生成自增ID。如果N=2,可以一个生成偶数ID,一个生成奇数ID:
KeyGeneratingServer1:
auto-increment-increment = 2
auto-increment-offset = 1
KeyGeneratingServer2:
auto-increment-increment = 2
auto-increment-offset = 2
在DB前面增加一个负载均衡器,每次请求会被路由到一个DB。
我们也可以采用短链接服务中生成ID的方式。
如何应对数据量增长的问题?
未来数据量可能会逐步增长,那么,如何提前布局,当数据量增长后能从容应对呢?
我们可以在开始的时候就将逻辑分片数设置为一个较大的数值,实际可能一开始物理数据库服务器用不到那么多,可以将多个逻辑分片部署在一台物理服务器上。随着时间的增长当物理服务器上的数据达到容量阈值的时候,可以很方便得将逻辑分片迁移到其他服务器上。我们可以维护一个从逻辑分片到数据库服务器的映射关系的配置文件,以便于后面的迁移操作(只需要修改这个配置文件。)。
news feed生成
News feed需要展示出我们关注的哪些用户的图片值中新并且最受欢迎的那部分,并且需要做排序展示。
假设我们需要展示前100张图片,需要首先获取关注的那些用户每人的最近的100张图片,然后提交给排序算法模块,计算出前100张图片。因为这个过程张需要查询多张表,然后还需要对图片列表进行排序/合并/RANK操作,因此延迟可能比较高。为提高性能,我们可以考虑提前计算后面直接读已经计算好的结果的方式。
将已经计算好的结果发送给用户有几种可行的方案:
- 拉。需要客户端发起对服务端的请求,拉取最新的结果。这个方法弊端是在客户端拉取之前展示的都是之前的历史结果。
- 推送。由服务器主动将结果推送给客户端,此方法的弊端是客户端需要维持和服务端的长链接以便于接收结果。另外就是如果用户关注了哪些热点人物,服务器会频繁向客户端推送数据。
- 混合的方式。关注了大批用户的这些用户采用拉的方式,而其他的采用推送的方式。也可以通过控制推送的频率来解决这个问题,用户需要更新的时候可以手动刷新。
分片的处理
关键的一步是快速获取到用户关注的那些人的最近的图片,那么必须能够方便得对图片进行排序。因为数据是分片存储的,为了提高效率,可以将创建时间作为photoId的前缀,这样可以快速获取最近的photoId。