# Twitter

twitter 是社交网络服务之一，用户可以在上面发布、阅读 140 字符长度的消息，即 tweets。系统的主要功能罗列如下：

* 核心功能
  * 用户可以发布新的 tweets
  * 用户可以互相关注
  * 用户可以将它喜欢的 tweets 标记为喜爱（favorites）
  * 用户可以查看其关注的其它用户最近发布的 tweets，称为 timeline
  * 每个 tweets 可以包含图片和视频
  * 用户可以转发 tweet，即 re-tweet
  * 用户可以按关键词检索 tweets
* 扩展功能
  * 回复 tweet
  * 热搜话题
  * tweet push：新消息提示
* 系统要求
  * 服务高可用
  * timeline 服务的延迟 <= 200ms
  * 一致性要求不高，tweet 发布后一段时间内其他用户无法访问可以容忍

## System APIs

tweeter 的 api 总体上可分为 read 和 write，其中 read 可以从以下两个维度来看：

|          | Pull           | Push                                       |
| -------- | -------------- | ------------------------------------------ |
| Targeted | home\_timeline | User/Site Streams, Mobile Push (SMS, etc.) |
| Queried  | search         | track/follow streams                       |

而 write api 只有一个，即

```bash
post_tweet(api_dev_key, tweet_data, tweet_metadata)
```

其中：

* api\_dev\_key 用于控制用户的资源配额
* tweet\_data 用于记录 tweet 数据本身，如文字以及图片、音频、视频的地址
* tweet\_metadata 用于记录 tweet 的元数据，包括发送地点、用户信息等等

## High Level System Design

twitter 服务请求是典型的读多写少型，但即使是少，量级也很大，因此我们需要多个 application servers 来服务这些请求，并通过 load balancer 来分发请求，后端需要高性能的数据库来存储大量 tweets 数据，同时承载更大量集的读压力，此外我们还需要一些文件存储服务来放置用户上传的图片与视频：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-L_qPiIeJHRRM1pb6E_-%2F-L_pvpqZBeFhDSnV14BR%2FScreen%20Shot%202019-03-13%20at%203.23.26%20PM.jpg?alt=media\&token=c0b6b1e4-472f-4563-b259-921779c38aa4)

在设计的时候，需要考虑到峰值请求量，这里使用 auto-scaling，能够在必要时扩容以抗住压力。

考虑到数据库承载的读写量巨大，需要对其进行分片，因此中间需要增加一组 aggregator servers 负责从多个数据库分片中查询并聚合结果，因此可进一步细化为：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-L_qPiIeJHRRM1pb6E_-%2F-L_qIWHQRCj9js9KjXWD%2FScreen%20Shot%202019-03-13%20at%205.06.45%20PM.jpg?alt=media\&token=d2fae426-07b5-47d4-a865-47855e4b2d42)

## Low Level System Design

### Data Model & Database

在 database 中，我们需要存储用户、tweets、关注以及喜欢的关系，因此至少需要 Tweet、User、Follow、Favorite 数据表：

#### Tweet

```
TweetID:        int
UserID:         int
Content:        varchar(140)
TweetLatitude:  int
TweetLongitude: int
UserLatitude:   int
UserLongitude:  int
CreationDate:   datetime
NumFavorites:   int
PK on TweetID
```

#### User

```
UserID:       int
Name:         varchar(30)
Email:        varchar(32)
DateOfBirth:  datetime
CreationDate: datetime
LastLogin:    datetime
PK on UserID
```

#### Follow

```
UserID1: int
UserID2: int
PK on (UserID1, UserID2)
```

#### Favorite

```
TweetID:      int
UserID:       int
CreationDate: datetime
PK on (TweetID, UserID)
```

那么用什么方式来存这些数据就是下一步需要做的决定，是 SQL 还是 NoSQL？RDBMS 在扩展性上，横向扩展技术不算成熟，通常会带来较大的维护成本，由于 twitter 对扩展性、可用性的要求较高，对一致性的要求反而不高（tweet 发布一段时间后，相关的用户看不到是可以接受的），因此这里选择 NoSQL 数据库更加合理。

#### Data Sharding

由于每天都有大量的 tweets 读写，我们需要将数据分布到不同的机器上，这样能够达到更高效地 read/write，如何对数据划分是下一步要确定的问题。在此之前，需要明确 sharding 的目的：提高 timeline 查询的效率，尽量分散读写请求，注意处理热点场景（部分 user 或者 tweet 可能出现大量读写，产生热点）

回顾 Tweet 数据模型，可能的方案有：

* 根据 TweetID 分库
* 根据 UserID 分库
* 根据 CreationDate 分库

_TweetID_

timeline：

1. application server 将请求发到 aggregator server
2. aggregator server 找到该用户关注的所有其它用户
3. aggregator server 发送相应的查询请求到每个数据库分片上，查询对应用户的按时间倒排的 tweets
4. aggregator server 搜集每个分片查询的结果，聚合后返回给 application server

这个方案解决了热点场景，分散了读写请求，但由于需要查询每个数据库分片，延迟较高，但这方面的延迟可以通过缓存层来缓解。

_UserID_

timeline：

1. application server 将请求发到 aggregator server
2. aggregator server 找到该用户关注的所有其它用户
3. aggregator server 发送相应的查询请求到用户所在的数据库分片上，查询对应用户的按时间倒排的 tweets
4. aggregator server 搜集每个分片查询的结果，聚合后返回给 application server

这个方案使得 aggregator server 不需要去访问每个数据库分片，而是从特定的几个分片上查询，减少查询延迟，但缺点在于无法解决热点场景问题，热点用户所在的数据库分片将承受更大的请求压力。本质问题在于数据并没有平均分散到每个数据库分片，且每个分片的数据增减速度也不一样，尽管可以通过重新分片或一致性哈希缓解问题，但也许有更好的设计存在。

_CreationDate_

按 creationDate 分片从单个查询请求 timeline 的角度来说，能够减少查询的分片个数从而减小延迟，但与之伴随的问题就是，读写都会集中到某些分片上，导致这些分片的读写压力增大，反而有可能增加延迟。

TweetID + CreationDate

将 TweetID 与 CreationDate 结合，获得两种方案的长处：

* 快速找到最近发布的 tweets
* 将读写压力分散到各个数据库分片上

但这需要一点黑科技：将 CreationDate 塞进 TweetID 中！

将 tweetID 看作是二进制整数，分为高位 (significant) 区域和低位区域，高位区域负责记录时间戳，即 UNIX Epoch time，低位区域使用计数器来表示该 tweet 为同一 epoch time 下发送的 tweet 序号。如此一来：

* 不需要使用 creationDate 的二级索引，减少写操作的延迟（更新索引）
* 读取的时候不需要根据 creationDate 过滤数据，因为相关信息已经被塞进 TweetID 中，使用 range query 即可。

### Cache

为了减少 DB 的读写压力，我们可以在 aggregators servers 下层再引入 cache，负责缓存热点 tweets 和 users，这样 aggregator servers 在查询 DB 之前可以先看一下所需的数据是否已经在 cache 中，从而挡住大量读写请求。

* replacement policy：使用 LRU cache，或者做一些更细致的策略
* what data to cache：可以只 cache 最近 N 天的数据

如此一来，架构又进一步更新为：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-L_qPiIeJHRRM1pb6E_-%2F-L_qM1SReXOjrkMspa76%2FScreen%20Shot%202019-03-13%20at%205.22.12%20PM.jpg?alt=media\&token=d43137e9-8bd8-4cb1-a887-04c3cea3c6f4)

## Real Time Delivery

本节讨论：如何利用 cache 层使得 twitter 服务能够做到 real-time delivery。

核心思想：写操作的时间换读操作的时间

* 在 cache 中记录当前活跃的所有用户订阅的 tweets，即它们各自的 timeline
* 每当用户 A 发布 tweet 时，aggregator servers 需要
  * 将 tweet 写入 database 中，得到 tweetID
  * 查询所有关注用户 A 的用户
  * 找到所有相关用户 timeline 所在的 cache servers，往它们的 timeline 插入相关 tweetID

整个过程如下图所示：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-L_qPiIeJHRRM1pb6E_-%2F-L_qOs5a_FO71TJHnIen%2FScreen%20Shot%202019-03-13%20at%205.34.34%20PM.jpg?alt=media\&token=f2a024af-2c9e-4f76-a71c-7eb1da3546c7)

该方案能将用户查询 timeline 的时间大大缩小，使得 real time 变成现实。

为了缓存更多用户的 timeline 数据，在 cache 中仅存储必要数据的 ID，如下图所示：

![](https://1008303647-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-L_qPiIeJHRRM1pb6E_-%2F-L_qPMRsgJ-UKT33cXSZ%2FScreen%20Shot%202019-03-13%20at%205.36.49%20PM.jpg?alt=media\&token=39471d23-181c-4c96-a7a3-44497ff2f99a)

其中，多余的 tweetID 可用来表示转发的 tweet。









