写点什么

Twitter 如何做到每秒钟处理 3000 张图片上传

2016 年 5 月 30 日

本文翻译自 How Twitter Handles 3,000 Images Per Second ,作者是 Todd Hoff ,翻译已获得本人授权。

今天的 Twitter 每秒钟能够创建并保存 3000 张 (200GB) 图片。甚至早在 2015 年,Twitter 就通过改进媒体存储策略节约了 6 百万美元。

以前并不是这样的。Twitter 在 2012 年主要以提供文字信息为主,当时的用户也很少发布各种炫酷的动态图片。到现在 2016 年,Twitter 开始提供富媒体功能。这样的转变是通过 Twitter 自行开发的全新 _ 媒体平台 (Media Platform)_ 实现的,这个平台可以支持照片预览、多张照片发送、动态 Gif 图片、Vine 视频,以及内嵌视频等功能。

Twitter 软件开发工程师 Henna Kermani Mobile @Scale London 活动中,通过一场名为每秒 3000 张图片的有趣演讲介绍了 Twitter 的媒体平台。演讲主要侧重于图像处理方面,但她讲的大部分细节也同样适用于其他形式的媒体。

演讲中一些比较有趣的重点包括:

  • 因陋就简的处理方法注定将会失败。在原本不支持的情况下,不假思索通过简单的手段支持上传带图片的推文,这样的做法会造成某种形式的“套牢”。并且这种方式缺乏缩放能力,尤其是在网络状况不佳的时候,这使得 Twitter 很难增加新的功能。
  • 去耦合。通过将推文与所包含的媒体去耦合,Twitter 可以分别针对每个途径进行优化,在运营方面获得更大程度的灵活性。
  • 移动句柄,而非存储块。不要在您的系统内部移动过大的数据块。这种做法将消耗大量带宽,会导致所有需要访问这些数据的服务遭遇性能问题。更好的方法是单独存储数据,并通过句柄的方式进行引用。
  • 使用分块可续传方式上传可大幅降低媒体文件的上传失败率。
  • 持续的实验和研究。Twitter 研究发现,对于图片的不同变体(例如缩略图、小图、大图等),20 天的存活时间 (Time to live, TTL)是最佳甜区,这个值可以在存储和计算之间实现良好的平衡。图片内容发布 20 天之后的访问概率会大幅降低,因此可以删除图片的各种变体,这样的做法每天可以帮助 Twitter 节约 4TB 数据存储空间,并将需要的计算服务器数量减少几乎一半,同时每年可节约数百万美元。
  • 按需。老图片的不同变体可以放心删除,并在需要时重建,而无须预先创建好。这种按需执行服务的做法可改善灵活性,让您更清楚任务的执行方式,并对其进行集中的控制。
  • 渐显式 JPEG(Progressive JPEG)作为一种标准图片格式无疑是真正的赢家。这种格式在前端和后端都有良好的支持,在速度不快的网络中也能提供不错的表现。

Twitter 开发富媒体功能的过程中还发生了一些很棒的故事,一起来学学他们是如何做到的…

旧途 - 2012 年的 Twitter

写入路径

  • 用户在某个应用中撰写了一条推文,并可能在推文中附加了一张图片。

    • 客户端将该推文发送至一个单体端点 (Monolithic endpoint)。图片会作为附带内容与推文的所有其他元数据一起上传,并传递至这一过程涉及到的每个服务。
    • 在原本的设计下,这个端点是造成很多问题的根源。
  • 问题 1:浪费大量网络带宽

    • 推文的创建和媒体的上传紧密耦合为一个操作。
    • 上传过程充满不确定性,或者完全成功或者彻底失败。失败可能出于任何原因,例如网络卡顿或传输错误等,如果失败就只能从头开始重新执行整个上传过程,包括上传图片。例如,假设上传操作在进行到 95% 后出错,也需要将所有内容重新上传一遍。
  • 问题 2:面对新出现更大体积的媒体无法很好地缩放

    • 这种方式无法通过缩放支持视频等更大体积的媒体文件。体积的增大会导致出错概率增加,尤其是在诸如巴西、印度、印度尼西亚这样网络速度慢并且不可靠的新兴市场,而 Twitter 非常迫切地希望提高这些地区用户上传推文的成功率。
  • 问题 3:内部带宽使用效率低下

    • 端点需要连接至负责处理用户身份验证和路由的 Twitter 前端 (TFE),随后用户会被路由至某个图片服务 (Image Service)。
    • 图片服务联系变体生成器 (Variant Generator),用不同尺寸(例如小、中、大、缩略图)为图片生成不同实例。这些变体会存储在 BlobStore 中,这是一种专为图片和视频等大型载荷进行过优化的键值 (key-value) 类型存储。随后图片将永远存储在这里。
    • 在创建和保存推文的过程中,还涉及到很多其他服务。因为端点是单体的,如果将媒体与推文元数据结合在一起,就需要在所有服务之间传输这种绑定在一起的数据。这样的大型载荷甚至会传递到原本在设计上并不用于直接处理图片的服务,这些服务甚至并不是媒体传递渠道的一部分,但依然需要针对大型载荷的处理进行优化。这样的做法导致内部带宽使用效率严重降低。
  • 问题 4:存储容量极度膨胀

    • 数月甚至数年前发布的推文中所包含的图片已无人问津,但依然需要永远存储在 BlobStore 中,这些内容耗费了宝贵的存储空间。因为没有垃圾回收机制,有时甚至在推文被删除后,图片依然会保存在 BlobStore 中。

读取路径

  • 用户看到一条包含图片的推文。图片来自哪里?

  • 客户端从 CDN 请求图片的变体。CDN 可能需要从原始位置或 TFE 处检索该图片。这一过程最终导致需要通直接查询 BlobStore 的方式,使用 URL 请求某一特定尺寸的图片。

  • 问题 5:无法引入新的变体

    • 这种设计不是非常灵活。如果需要添加新的变体,也就是说需要为图片创建一个新的尺寸,此时必须为 BlobStore 中的每个图片创建一个新尺寸的版本。这种方式缺乏按需创建变体的便利机制。
    • 缺乏灵活性意味着 Twitter 很难为客户端添加新的功能。

新法 - 2016 年的 Twitter

写入路径

将上传的媒体与推文去耦合。

  • 上传操作至此成为“一等公民”,并创建了专门用于将原始图片存储至 BlobStore 的上传端点。

  • 这种方法为上传操作的处理提供了极大的灵活性。

  • 客户端联系 TFE,TFE 随后联系图片服务,图片服务将图片保存至 BlobStore 并将相关数据添加至一个元数据存储。仅此而已。这一过程不会涉及任何隐藏的服务,不需要处理媒体,也不需要四处传递图片。

  • 随后图片服务会返回一个代表该媒体的唯一标识符,即 mediaId。当客户端需要创建推文,发布私信,或更新自己的头像照片时,将使用这个 mediaId 作为引用该媒体的句柄,而不需要提供媒体的原始文件。

  • 假设用户想要使用刚上传的图片发布推文,过程将会是这样:

    • 客户端联系更新端点,在推文中包含 mediaId,该请求将发送至 Twitter 前端,随后 TFE 会将请求路由至对于所创建内容来说最为恰当的服务。对推文本身,最适宜的服务是 TweetyPie,私信和用户资料信息的处理也由不同服务进行,所有这些服务都能与图片服务通信,图片服务器中包含处理面孔检测、儿童色情内容检测等功能所需的推文处理队列,当这些任务执行完毕后,图片服务会与处理图片的 ImageBird 或处理视频的 VideoBird 服务通信。ImageBird 负责生成变体,VideoBird 则对视频进行一定的转码,最终处理生成的媒体内容将保存至 BlobStore。
    • 不再需要将媒体内容四处传输,借此可节约大量本被浪费的带宽。

分块可续传的上传。

  • 进入地铁站,10 分钟后出来,上传过程可从上次中断的地方恢复进行。对用户来说该过程是完全无缝的。
  • 客户端使用上传 API 发起上传会话,后端会为用户提供一个 mediaId,这个 mediaId 将在整个上传会话中充当标识符。
  • 图片被拆分为多个小块,例如拆分成三块。这些文件块可使用 API 附加到一起,每次调用的附加操作可提供必要的片段索引,所有附加操作都可作用于同一个 mediaId。上传完成后对上传内容进行“定稿”,随后这个媒体就可以使用了。
  • 这种方法更容易适应网络故障。每个独立小块可以重试,如果网络因为任何原因中断,用户可以暂停并在网络恢复后从暂停的位置继续上传。
  • 方法简单,效益巨大。对于体积超过 50KB 的文件,上文提到的三个国家中图片上传失败率降低幅度分别为:巴西,33%;印度,30%;印度尼西亚,19%。

读取路径

此处用到了一种名为 MinaBird 的 CDN 源服务器。

  • MinaBird 可与 ImageBird 和 VideoBird 通信,这样就算不存在,也可以即时生成不同尺寸图片和不同格式视频的变体。

  • 在处理客户端请求方面,MinaBird 更流畅也更动态。举例来说,假设有内容由于 DMCA(数字千年版权法)的要求需要删除,此时很容易便可阻止对相关内容的访问,或重新允许对媒体特定片段的访问。

  • 按需即时生成变体和转码的方式使得 Twitter 在存储容量的使用方面更为高效。

    • 按需生成变体,意味着不需要将所有变体都存储在 BlobStore 中,这是一个巨大的进步。
    • 原始图片在删除前将一直保留,而变体只保留 20 天。媒体平台团队针对最佳过期时限进行了大量研究,发现在所有请求的图片中,有大约 50% 的图片都是在最多 15 天(左右)的时间内上传的。继续保留更早前上传的图片可以获得的收益在逐渐下降。更老的媒体文件也有可能就此无人问津。15 天后存在一条很长的长尾。
    • 不设置存活时间 (TTL) 并且不过期的情况下,媒体文件的存储导致数据存储总量每天增加 6TB,按需生成所有变体的“偷懒”做法会让数据存储总量每天增加 1.5TB。20 天存活时间所用的存储空间并不像“偷懒”做法那么多,因此在存储方面的成本并不高,但对计算的要求更高了。对于“偷懒”的做法,在读取的同时生成所有变体,需要为每个数据中心提供 150 台 ImageBird 服务器,而 20 天存活时间的做法只需要投入 75 台。因此 20 天存活时间是一个甜区,可以在存储和计算方面实现平衡。
    • 由于节约存储和计算资源等同于省钱,通过采取 20 天存活时间的做法,Twitter 在 2015 年节约了 6 百万美元。

客户端的改进 (Android)

  • 针对 Google 创建的图像格式 WebP 执行了为期 6 个月的实验。

    • 相比 PNG 或 JPEG 图片,这种格式的图片体积平均减小 25%。
    • 用户参与积极性有所提高,尤其是在减小图片体积可以帮助网络减压的新兴市场。
    • iOS 不支持该格式。
    • 仅 Android 4.0 以上系统可支持。
    • 平台支持的缺乏使得 WebP 的支持代价不菲。
  • Twitter 还尝试过渐显式 JPEG。这种格式可以使用逐行扫描的方式进行渲染,首次扫描的图片可能显得斑驳不匀,但可通过逐行扫描的方式逐渐进行完善。

    • 性能更好。
    • 后端易于支持。
    • 相比传统 JPEG 编码速度慢 60%。但由于编码工作只需要进行一次,随后所有用户都可从中受益,因此这不算什么大问题。
    • 不支持透明,因此还需保留透明 PNG,但渐显式 JPEG 其他方面都很出色。
    • 客户端对该格式的支持是通过 Facebook 的 Fresco 库实现的。Fresco 库的价值很值得大书特书,就算在 2G 网络中也能实现让人印象深刻的效果。PJPEG 的首次扫描只产生 10kb 流量,因此很快就可以加载完成。当原生渠道还在等待加载,无法显示任何内容的时候,PJPEG 已经可以提供可分辨的图片。
    • 通过对推文详细信息视图的加载进行持续的实验发现,p50 加载时间降低 9%,p95 加载时间降低 27%,出错率降低了 74%。网络速度缓慢的用户无疑能从中获得不菲的收益。

感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 5 月 30 日 17:483694
用户头像

发布了 283 篇内容, 共 84.6 次阅读, 收获喜欢 34 次。

关注

评论

发布
暂无评论
发现更多内容

架构师训练营第 1 期 -- 第六周学习总结

发酵的死神

极客大学架构师训练营

当AI入职FBI,克格勃直呼内行

脑极体

目标检测学习-比赛路线

Dreamer

【面经】面试官:讲讲类的加载、链接和初始化?

冰河

架构 JVM 类加载 优化 性能调试

TarsCpp 组件之 MySQL 操作

TARS基金会

c++ MySQL 数据库 微服务 TARS

甲方日常42

句子

工作 随笔杂谈 日常

架构师训练营第 1 期 -- 第六周作业

发酵的死神

极客大学架构师训练营

老板下了死命令,要把日志系统切换到Logback

沉默王二

Java logback 日志系统

「架构师训练营第 1 期」第六周作业

张国荣

美国半导体十年计划中的NO.1,模拟硬件究竟有什么价值?

脑极体

Scrapy 源码剖析(二)Scrapy是如何运行起来的?

Kaito

Python 爬虫 Scrapy 源码剖析

如何搭建一个爬虫代理服务?

Kaito

爬虫 代理

架构师训练营第 1 期 第 6 周作业

李循律

极客大学架构师训练营

CICD实战——服务自动测试

TARS基金会

微服务 单元测试 CI/CD

TarsCpp 组件 之 智能指针详解

TARS基金会

c++ 微服务 智能指针 TARS

架构师训练营第二周总结

lakers

极客大学架构师训练营

元模型驱动(二)构建元模型ーGME构建分层模型

KaYa

DDD Kaya MDA GME MDD

SpringCloud-初步介绍

hepingfly

微服务 springboot SpringCloud

Scrapy 源码剖析(四)Scrapy如何完成抓取任务?

Kaito

Python 爬虫 Scrapy 源码剖析

元模型驱动(一)构建元模型ーGME入门

KaYa

DDD Kaya MDA GME MDD

Java9新特性-上篇

hepingfly

Java Java新特性

如何构建一个通用的垂直爬虫平台?

Kaito

Python 爬虫 代理

Scrapy 源码剖析(三)Scrapy有哪些核心组件?

Kaito

Python 爬虫 Scrapy 源码剖析

架构师训练营第二周

M.

元模型驱动(三):构建我们自己的元模型-KAYA

KaYa

DDD Kaya MDA MDD 元建模

Scrapy源码剖析(一)架构概览

Kaito

Python 爬虫 Scrapy 源码剖析

【架构师训练营 1 期】第六周作业

诺乐

队列实现栈的3种方法,全都击败了100%的用户!

王磊

Java 算法和数据结构

架构师训练营第二周作业

lakers

极客大学架构师训练营

嵌入式面试之《Linux系统编程100问》

哒宰的自我修养

Linux 线程 网络编程 进程

有状态软件如何在k8s上快速扩容甚至自动扩容

东风微鸣

Kubernetes DevOps openshift

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

Twitter如何做到每秒钟处理3000张图片上传-InfoQ