# 7. 设计YouTube或Netflix

**设计一个像 Youtube 这样的视频分享服务，用户可以上传/查看/搜索视频。 类似服务：netflix.com、vimeo.com、dailymotion.com、veoh.com**

> **难度级别：中等**

## 1.为什么选择 YouTube？

YouTube 是世界上最受欢迎的视频分享网站之一。 该服务的用户可以上传、查看、分享、评价和反馈视频以及添加对视频的评论。

## 2.系统的要求和目标

为了这个练习，我们计划设计一个更简单版本的 YouTube，包含如下需求：

* **功能性需求：**
  * 1.用户能够上传视频。
  * 2.用户能够分享和观看视频。
  * 3.用户能够根据视频标题进行搜索。
  * 4.服务能够记录视频的统计数据，例如点赞数/点踩数、总观看次数等等。
  * 5.用户能够添加和查看视频评论。
* **非功能性需求：**
  * 1.系统应该是高可用的，上传的视频不应该丢失。
  * 2.系统应该是高可用的。 一致性可能会受到影响（为了可用性）; 如果用户有一段时间没有观看视频，那应该没问题。
  * 3.用户在观看视频时应该有实时体验，不应有任何延迟。
* **不在范围内的需求：**
  * 视频推荐、最受欢迎的视频、频道、订阅、稍后观看、 收藏夹等

## 3.容量估算和约束

假设我们有 15亿总用户，其中 8 亿日活用户。如果平均而言一个用户每天观看5个视频，那么每秒的总视频观看量将是：

```java
800M * 5 / 86400 sec => 46K videos/sec
```

现在估算上传：观看比率为 1:200，即对于每个视频上传，有 200 个视频观看，每秒上传 230 个视频。

```java
46K / 200 => 230 videos/sec
```

**存储估算：**

假设每分钟有 500 小时的视频上传到YouTube。如果平均而言，一分钟的视频需要 50MB 的存储空间（视频需要存储成多种格式），一分钟内上传的视频所需的总存储空间为：

```java
500 hours * 60 min * 50MB => 1500 GB/min（25 GB/sec）
```

这些数字是在忽略视频压缩和复制的情况下估算出来的，如果算上的话，需要重新估算。

**带宽估算：**

每分钟上传 500 小时的视频并假设每个视频上传需要 10MB/分钟的带宽，每分钟将有 300GB 的上传。

```java
500 hours * 60 min * 10MB => 300GB/min（5GB/sec）
```

假设上传与观看比率为 1:200，将需要 1TB/s 的传出带宽。

## 4.系统API

可以使用 SOAP 或 REST API 来暴露服务的功能接口。以下是上传和搜索视频的API定义：

```java
uploadVideo(api_dev_key, video_title, vide_description, tags[], category_id, default_language,recording_details, video_contents)
```

**参数：**

* `api_dev_key(string)`：注册账号的API开发者密钥。用于根据分配的配额限制用户。
* `video_title（string）`：视频的标题。
* `vide_description（string）`：视频的可选描述。
* `tags (string[])`：视频的可选标签。
* `category_id（string）`：视频的类别，例如电影、歌曲、人物等。
* `default_language(string)`：例如英语、普通话、印度语等。
* `recording_details（string）`：录制视频的位置。
* `video_contents（stream）`：要上传的视频。

**返回：（string）**

成功上传将返回 HTTP 202（已接受请求）并且视频编码完成后，通过电子邮件通知用户，其中包含访问视频的链接。还可以暴露一个查询的 API让用户知道他们上传的视频的当前状态。

```java
searchVideo(api_dev_key, search_query, user_location, maximum_videos_to_return,page_token)
```

**参数：**

* `api_dev_key(string)`：服务注册账号的API开发者密钥。
* `search_query(string)`：包含搜索词的字符串。
* `user_location（string）`：执行搜索的用户的可选位置。
* `maximum_videos_to_return (number)`：一个请求中返回的最大结果数。
* `page_token（string）`：此令牌将指定结果集中应返回的页面。

**返回：（JSON）**

JSON包含有关与搜索查询匹配的视频资源列表的信息。每个视频资源包含视频标题、缩略图、视频创建日期和观看次数。

```java
streamVideo(api_dev_key, video_id, offset, codec, resolution)
```

**参数：**

* `api_dev_key(string)`：服务注册账号的API开发者密钥。
* `video_id（string）`：用于标识视频的字符串。
* `offset（number）`：我们应该能够从任何偏移量回到视频；这个偏移量将是一个从从视频开始时往后的时间。如果支持从多个设备播放/暂停视频，需要将偏移量存储在服务器上。这将使用户能够在离开任何一台设备后继续观看视频。
* `codec（string）&resolution（string）`：我们应该从 API 中发送编解码器和分辨率信息客户端支持从多个设备播放/暂停。想象一下，您正在电视的Netflix应用观看视频，暂停，然后开始在手机的 Netflix 应用上观看。在这种情况下，需要编解码器和分辨率，因为这两种设备具有不同的分辨率并使用不同的编解码器。

**返回: (STREAM)**

* 来自指定偏移量的流媒体（视频块）。

## 5.高阶设计

在高阶维度上，需要以下组件：

* 1.**处理队列**：每个上传的视频都会被推送到一个处理队列中去排队用于编码、缩略图生成和存储。
* 2.**编码器**：将每个上传的视频编码成多种格式。
* 3.**缩略图生成器**：为每个视频生成一些缩略图。
* 4.**视频和缩略图存储**：将视频和缩略图文件存储在一些分布式文件存储系统中。
* 5.**用户数据库**：用于存储用户的信息，例如姓名、电子邮件、地址等。
* 6.**视频元数据存储**：一个元数据数据库，用于存储有关视频的所有信息，例如标题、系统中的文件路径、上传用户、总浏览量、点赞数、点踩数等。也会用到存储所有视频的评论。

![](https://3100185414-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FgozhSDAYrrQ6G5G7iYxs%2Fuploads%2Fgit-blob-afd86989021b33a81f455a27e6fe8d9fae3e7600%2Fch7_1.png?alt=media)

​ ***fig1.Youtube的高阶设计***

## 6.数据库架构

**视频元数据存储 -MySql**

* 视频元数据可以存储在 SQL 数据库中。应保持视频的以下信息：
  * 视频ID
  * 标题
  * 描述
  * 视频大小
  * 缩略图
  * 上传者/用户
  * 点赞总数
  * 点踩总数
  * 观看总数
* 对于每条视频评论，需要存储以下信息：
  * 评论ID
  * 视频ID
  * 用户ID
  * 评论
  * 创建时间

**用户数据存储 -MySql**

* 用户ID、姓名、电子邮件、地址、年龄、注册详细信息等。

## 7.详细的组件设计

该服务是个读取量大的服务，因此需要关注构建一个可以快速检索视频的系统。可以预期读写比率为 200:1，这意味着对于每个视频上传都有200次观看。

**视频将存储在哪里？**

视频可以存储在分布式文件存储系统中，如[HDFS](https://en.wikipedia.org/wiki/Apache_Hadoop#HDFS)或[GlusterFS](https://en.wikipedia.org/wiki/GlusterFS)。

**应该如何有效地管理读取流量？**

我们应该将读取流量与写入流量分开。由于有每个视频的多个副本，因此我们可以将读取流量分布在不同的服务器。对于元数据，我们可以有主从配置，其中写入的数据将首先进入主节点，然后同步到其它的从节点。此类配置可能会导致数据过期，例如，当添加新视频后，其元数据将首先插入到主节点中，然后再同步到从节点，这时候从节点访问不到这些数据；因此将向用户返回过期的数据。这种过期的特征在系统中可能是可以接受的，因为它的生命周期非常短暂，并且用户将在几毫秒后就能看到新的视频。

**缩略图将存储在哪里？**

缩略图将比视频多得多。如果假设每个视频都有五个缩略图，需要一个非常高效的存储系统，可以承载这么巨大的读取流量。在决定应该使用哪种存储系统之前，对于缩略图有两个考虑因素：

* 1.缩略图是小文件，例如说每个最大 5KB。
* 2.缩略图的读取流量比视频大。用户将观看一个视频一次，但他们可能正在查看包含20个其他视频缩略图的页面。

让我们假设将所有缩略图存储在磁盘上。鉴于有大量的文件，我们对磁盘上的不同目录执行大量搜索以读取这些文件。这是非常低效的，并且会导致很高的延迟。

[Bigtable](https://en.wikipedia.org/wiki/Bigtable) 在这里可能是一个合理的选择，因为它将多个文件组合到一个块中，存储在磁盘上，并且在读取少量数据时非常高效。这两个设计的服务中最重要的需求。将热门缩略图保存在缓存中也将有助于减缓延迟，并且鉴于缩略图文件很小，可以很容易地缓存大量此类文件到内存中。

**视频上传**

由于视频可能很大，如果在上传连接断开时，能从断开时开始继续上传（断点续传）。

**视频编码**

新上传的视频存储在服务器上，并添加一个新任务到处理队列以将视频编码为多种格式。 一旦所有编码完成，上传者将收到通知，视频可以查看/分享。

![](https://3100185414-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FgozhSDAYrrQ6G5G7iYxs%2Fuploads%2Fgit-blob-9805a46d0710274f7ef868145aee7836a6b7e13c%2Fch7_2.jpg?alt=media)

​ ***fig2.YouTube的详细组件设计***

## 8.元数据分片

由于每天都有大量的新视频，而且读取流量非常大，因此，需要将数据分布到多台机器上，以便可以实现高效的读/写操作。有很多选择来对数据进行分片。让我们一个个地来看看不同的分片策略：

**基于 UserID 的分片**

可以尝试将特定用户的所有数据存储在一台服务器上。当存储时，可以将 UserID调用哈希函数，它将用户映射到数据库服务器，存储该用户视频的所有元数据。在查询用户的视频时，可以先访问哈希函数找到保存用户数据的服务器，然后从这个服务器读取。按标题搜索的视频，将不得不查询所有服务器，每个服务器将返回一组视频。中心服务器将汇总这些结果并对其进行排名，然后再将它们返回给用户。

这种方法有几个问题：

* 1.如果用户变得热门怎么办？存有该用户数据的服务器上可能涌入很多查询；这可能会造成性能瓶颈。这也会影响整体服务。
* 2.随着时间的推移，一些用户最终可能会比其他用户存储大量视频。保持一个不断增长的用户数据的均匀分布是相当棘手的问题。

要解决这些问题，必须重新分区/重新分配数据或使用一致哈希以平衡服务器之间的负载。

**基于 VideoID 的分片**

哈希函数将每个 VideoID 映射到存储有视频元数据随机服务器上。 要查找用户的视频，将不得不查询所有服务器，每个服务器将返回一组视频。中心服务器将汇总这些结果并对其进行排名，然后再将它们返回给用户。这种方法解决了热门用户问题，但将其转移到了热门视频。

可以在数据库服务器的前一层，通过引入缓存来存储热门视频，从而进一步提高性能。

## 9.视频去重

随着大量用户上传海量视频数据，我们的服务将不得不处理具有大范围的视频复制问题。重复的视频通常在宽高比或编码方面有所不同，可以包含叠加层或附加边框，或者可以是较长的原始视频的摘录。重复视频的泛滥可能会在多个层面产生影响：

* 1.**数据存储**：保存同一视频的多个副本可能会浪费存储空间。
* 2.**缓存**：重复的视频会占用空间，导致缓存效率下降，而这些存储本来应该是存储唯一的内容的。
* 3.**网络使用**：重复的视频也会增加通过网络传输到内网缓存系统的数据量。
* 4.**资源消耗**：更高的存储、低效的缓存和网络使用可能导致资源浪费。

对于终端用户来说，这些低效会带来：重复的搜索结果，时间更长视频启动和中断流媒体。

对于我们的服务，删除重复数据在早期是最有意义的；当用户上传视频时，对其进行后处理以查找重复的视频。内嵌的删除重复数据将节省大量资源，这些资源可用于编码、传输和存储视频的副本。只要任何用户开始上传视频，我们的服务可以运行视频匹配算法（例如，[块匹配](https://en.wikipedia.org/wiki/Block-matching_algorithm)，[相位相关性](https://en.wikipedia.org/wiki/Phase_correlation)等）以查找重复项。如果我们已经有正在上传的视频的副本，我们可以要么停止上传并使用现有副本，要么继续上传并使用新上传的高质量视频。如果新上传的视频是现有视频的一部分，或者，反之，反之亦然，我们可以智能地将视频分成更小的块，这样我们只上传那些 丢失的部分。

## 10.负载均衡

应该在缓存服务器之间使用[一致性哈希](https://www.educative.io/collection/page/5668639101419520/5649050225344512/5709068098338816/)，这也有助于平衡负载缓存服务器之间的压力。因为我们将使用基于静态哈希的方案将视频映射到主机名，由于每个视频的受欢迎程度不同，它可能导致逻辑副本上的负载不均衡。例如，如果一个视频变得热门，则与该视频对应的逻辑副本将遭受比其他服务器更多的流量。逻辑副本的这些不均衡负载可能会转化为相应的物理服务器上分配负载的不均衡。要解决此问题，一个位置中的任何繁忙服务器都可以将客户端重定向到同一缓存位置中不太繁忙的服务器。此场景中，可以使用动态的 HTTP 重定向。

然而，使用重定向也有它的缺点。首先，由于我们的服务尝试负载均衡在本地，如果接收重定向的主机无法提供视频，则会导致多次重定向。此外，每次重定向都需要客户端发出额外的 HTTP 请求，在视频开始播放之前会也导致更高的延迟。此外，中间层（或跨数据中心）重定向会导致一个客户端到一个比较远的缓存位置，因为更高层的缓存只存在于少数地点。

## 11.缓存

为了服务全球分布的用户，我们的服务需要一个大规模的视频传输系统。使用大量地理分布的视频缓存服务器，我们的服务应该将其内容推向更靠近用户的地方。我们需要制定一个策略，最大限度地提高用户体验，并且均匀地 将负载分布在缓存服务器上。

我们可以为元数据服务器引入缓存来缓存热点行数据库。使用 Memcache 进行缓存数据，访问数据库之前的数据和应用程序服务器可以快速检查缓存是否具有所需的行数据。LRU可能是我们系统的合理缓存清除策略。在此之下 策略，我们最先丢弃最近最少查看的行。

**如何构建更智能的缓存？**

如果我们遵循80-20法则，即每天 20%的访问量产生了80%的流量，这意味着某些视频非常受欢迎，以至于大多数 人们观看它们；因此，我们可以尝试缓存 20% 的每日视频和元数据读取量。

## 12.内容交付网络 (CDN)

CDN 是一种基于用户的地理区域、网页的来源，向用户提供 Web 内容的分布式服务器系统，即内容投送服务器。CDN的更多内容，查看缓存章节。

我们的服务可以将热门视频移至 CDN：

* CDN 在多处复制内容。 视频更接近于用户，并且跳数更少，视频将以更流畅网络流式传输。
* CDN 机器大量使用缓存，并且大部分可以提供超出内存的视频。

不被 CDN 缓存的即不太热门的视频（每天1-20 次观看），可以由我们的服务器提供各种数据中心。

## 13.容错

应该使用[一致性哈希](https://www.educative.io/courses/grokking-the-system-design-interview/B81vnyp0GpY)在数据库服务器之间进行分配。 一致性哈希不仅有助于替换宕机服务器，还有助于在服务器之间分配负载。
