Smartly.io博客

Juuso Mäyränen

我们是如何扩大架构以实现每秒渲染数千张图片的

By Juuso Mäyränen on 2017年12月27日

On-demand Image Processing

自动化正在在线广告领域变得越来越普遍。 一家在线购物网站的产品目录可能包含数千万个产品,而手动为这些产品制作广告根本不可行。 自动化关键便是基于商品信息的广告,这种方式能够根据产品信息文档(例如名称、描述、价格、图片等)来自动创建广告。 这篇博文介绍了我们如何构建基础架构来处理广告自动化所需的图像渲染请求。

 

基于商品信息的广告以及动态图片模板

我们开发了一个名为动态图片模板 (Dynamic Image Templates) 的功能,它可以将一直需要手工完成的广告图片创建过程自动化。 商品的图片被下载并合成到模板当中,并自动与其他商品信息(如描述,价格,公司logo以及其他数据)结合使用。 这其中涉及了许多密集的计算步骤,渲染一个广告的时间大概是几百毫秒。

Dynamic Image Template

图片模板案例

我们最初的图片模板处理方案首先通过任务队列处理商品信息库中的所有图片,然后将图片上传到Amazon S3(通过CloudFront服务),最后将Facebook指向包含处理后图片的商品信息库。 尽管这在客户的商品信息库包含上万个的商品时是一个不错的解决方案,但我们很快就发现,当有数以百万计的图片时,对它们进行预处理则不是一个好主意。尽管我们可以不断增加处理计算的基础设施,事实上我们也这么做了一段时间。 但是,并不是所有的商品能都会展示在广告当中,这种在部分图片被采用之前就处理所有图片的方式,变得又慢又浪费。

On-demand Image Processing—Before改动前的架构  

当我们开始寻找新的解决方案的时候,大型商品信息库的处理时间已经变得长达几个小时。 这是无法让人接受的,因为像价格更新这样的东西依赖于商品信息库的快速更新,而客户可能变成推广价格过时的商品或者已经缺货的商品。 

 

按需图片处理

仅在Facebook需要的情况下对图片进行渲染,这似乎是解决这一问题不错的开端。 根据这一想法我们开始制定计划,若采用按需处理的方式,我们必须为Facebook可能向我们提供的各种规模的流量做好准备,因为流量将取决于Facebook如何向用户展示广告。 因此,我们的可用性和容错要求必须提高到一个全新的水平。

简单而言,我们的想法是只有Facebook决定向用户展示商品广告,商品图片才会被Facebook抓取。 因此,就像任何其他大型网络平台一样,我们需要为负载峰值做好准备。 处理图片的过程大纲如下:

  1. Facebook通过HTTP请求商品图片。 商品特定的数据和要使用的模板ID都被编码在URL中。
  2. 我们从客户的服务器上下载商品图片。
  3. 我们将图片模板应用到商品图片中,并将结果返回到Facebook。

听起来很容易是不是?不过,还有一大堆需要考虑的地方:

  1. 易于扩展性:图片处理对CPU需求很大
  2. 高可用性:没有单点故障
  3. 避免不必要的工作:
    • 缓存商品图片,因为它们可能被多个图片模板使用,而届时我们不希望重新下载,也不希望对客户的服务器像拒绝服务攻击一样发送大量请求
    • 缓存处理后的图片,因为Facebook或其他方可能会再次需要它们
  4. 确保加载高峰不会导致其他系统的级联效应
  5. 确保与一个客户相关的负载高峰不会影响其他客户

 

起步

由于我们的预处理图片已经使用了CloudFront的服务,所以我们首先将流量从CloudFront导至轮训调度DNS后面少数的图片处理服务器。 初始阶段还涉及修改我们的应用程序代码,以通过HTTP API工作。

随着图片处理服务器数量的增加,我们很快就发现很难进一步提升DNS负载平衡。 如果某台服务器出现了故障或需要维护,则需要将其从DNS记录中删除。 由于DNS本身的性质,这将花费一些时间。而在添加新服务器的时候,同样的情形也会出现。

下一步是添加一些HAproxy负载平衡器来处理流量。 例如,通过专用负载均衡器,实现适当的容错能力要容易得多。 它们可以检测到服务器负载过重或出现故障,并停止向它们传递请求。 同样,我们可以轻松禁用负载均衡器中的任何服务器,并在需要执行维护时确保不丢失任何请求。 将流量从故障负载均衡器的IP地址重定向到正常负载均衡器这一机制保证了容错能力,不过这种机制需要我们对数据中心的流量路由进行一些控制。

在这个阶段,我们的系统已经具有很好的可扩展性和容错性,也具备了一定的缓存,但这还不够。 我们还想缓存原始的商品图片,以免访问流量冲击客户的服务器,并尽量缩短图片下载的时间。 为此,我们建立了一些缓存转发代理(由Squid提供),以此来引导所有的图片下载。

除了缓存之外,Squid还有压缩转发等不错的功能。 它确保当我们同时收到多个对同一商品图片URL的请求时,我们只向客户的服务器发出一个请 ——这在同时使用不同图片模板的时候是非常常见的。所以,这样的做法有助于大大减轻客户服务器的负担。

图片处理器通常至少有几百GB的硬盘空间,所以我们决定将原始图片缓存在这些节点上。在代理设置中所有图片处理器都在本地运行HAproxy,这便保证了较高的可用性——如果Squid代理服务器关闭,所有流量都将导至其他服务器。

 

完成

在我们开始将全新的基础架构推广给所有的客户时,我们发现了CloudFront的成本远远超过了其带来的收益。尽管它作为缓存很有用,但大多数其他功能(如地理分配)与我们并不是很相关,因为我们都是向Facebook提供图片。 面对不断增长的成本,我们在负载平衡器和图片处理器之间建立了一些Vanish缓存来替代CloudFront。 现在(2016年10月),即使我们服务的请求数量几乎增加了三倍,每月的度缓存和负载平衡成本不到2016年1月使用CloudFront时的十分之一。

On-demand Image Processing-After改动后的架构

为了进一步提高缓存命中率并更好地利用资源,我们在图片处理器中运行的Nginx网络服务器中设置了缓存。 所有的负载平衡都是基于URL上的一致性哈希完成的 ——同一个请求应总是到达相同的Varnish以及相同的图片处理器。 因此,很多通过Varnish缓存传递后没有命中的请求会成为图片处理器的缓存命中,这进一步减少了重新处理图片的需求。

在应用层面,也有一些机制与基础架构和可伸缩性有关。 如果有一个图片模板不断地与下载时间过长的商品图片一起使用,则该图片模板将被限制以免影响其他模板。 另一个例子是,图片处理器会缓存它们需要从数据库中获取的资源,以确保数据库在负载峰值时不会崩溃。

 

结果

在9月的最后一周,我们的图片处理系统平均每秒处理约1000个请求。 就带宽而言,我们通常是500 - 1000 Mbps的恒定负载,峰值大约在3Gbps。 大约65%的请求是Varnish缓存命中,加上nginx缓存,总缓存命中率大约为75%。

用于下载原始商品图片的Squid缓存也被证明是非常有效的,其命中率大约在50-80%之间。 当然这是平均数字,峰值则可能高达10倍以上,但最近我们已经能够摆脱瓶颈,这并没有引起很多问题。

On-demand Image Processing

一个负载均衡器的出站流量的一周统计

按需图片处理设置在2016年初推出,在半年之后现在看来,它在可预见的未来应该还能很好地扩展。 在软件开发、尤其是快速发展的在线广告界当中,用例的不断变化和新瓶颈的不断出现正在成为家常便饭,而我们将继续面对它们、不断前进。

 

您想加入我们的大规模产品开发项目么?

我们正在招聘优秀的软件开发工程师加入我们的团队。请通过这个链接查看在Smartly.io当一名工程师的是什么样的体验?smartly.io/developer 

返回博客首页