为 Ubuntu 设计快速缩略图服务

  • 金灵杰

2015 年 8 月 19 日

话题:架构语言 & 开发

最近,James HenstridgeXavi Garcia MenaMichi Henning为 Ubuntu 和 Ubuntu Touch 实现了一个快速、可伸缩的缩略图服务

简介

手机和桌面应用都有很多场景需要使用缩略图服务。同时,有许多媒体类型需要生成缩略图,比如图片、音乐、视频等。为每种媒体类型设计独立的 API 会增加开发成本,且生成这些缩略图也需要消耗大量 CPU 资源,网络传输缩略图会消耗不少带宽。本文主要介绍一种通用的缩略图服务,它为开发者屏蔽了上述这些复杂内容,通过缓存等措施大大提升了缩略图服务的性能。

系统架构

外部 API

缩略图服务对外提供了三种 API。

  • QML API:通过注册为 QQuickAsyncImageProvider 的提供者,使得调用者能够通过传入特定的 URI 和参数,获取特定大小的本地或者远程缩略图文件
  • Qt API:提供了三个函数获取特定类型的缩略图

    • QSharedPointer getThumbnail(QString const& filePath, QSize const& requestedSize);
    • QSharedPointer getAlbumArt(QString const& artist, QString const& album, QSize const& requestedSize);
    • QSharedPointer getArtistArt(QString const& artist, QString const& album, QSize const& requestedSize);

    其中 getThumbnail 函数从本地媒体文件提取缩略图,getAlbumArt 和 getArtistArt 函数从远程图片服务器获取。这几个函数返回的 Request 对象,提供 downloadFinished 信号,调用者可以连接该信号异步获取缩略图数据。

  • DBus API:缩略图服务通过 DBus 注册了两个接口,分别为 com.canonical.ThumbnailerAdmin 和 com.canonical.Thumbnailer。前者提供了对缓存的操作和缓存状态查询,后者提供了 GetAlbumArt、GetArtistArt 和 GetThumbnail 三个函数,对应 Qt API 中的三个函数,获取不同类型的缩略图。

同时,缩略图服务还提供了命令行工具thumbnailer-admin,是得能够通过命令行操作上述两个 DBus 接口。

为了节省资源,DBus 接口由 DBus 服务启动,在 30 秒空闲后关闭。

图片提取模块

图片提取模块包括图中的音频、视频提取器,下载器和图片提取器。

音频、视频提取器使用GStreamer来解码音频和视频文件。由于 GStreamer 自身的稳定性问题,部分解码器可能会导致程序挂起失去响应,因此将和 GStreamer 交互部分单独封装成独立的可执行程序vs-thumb。主服务通过管道的形式和它交互,很好的避免了因为解码器崩溃导致的稳定性问题。

下载器提供 download_album 和 download_artist 两个异步函数,通过 Qt 的QNetworkAccessManager组件从 dash.ubuntu.com 下载图片。

图片提取器使用图片转换模块从本地图片中提取缩略图图片。

图片缩放和转换模块

图片缩放和转换模块主要负责将图片转换和缩放成 JPEG 格式的最终图片文件。该模块使用Gdk-Pixbuf库进行转换。

针对 JPEG 图片,图片缩放模块会通过 libexif 库试图读取图片的 EXIF 信息。如果图片的 EXIF 信息包含缩略图,且该缩略图的大小不小于目标缩略图大小,则图片缩放模块会使用 EXIF 中的缩略图进行缩放,以提高性能。

磁盘缓存模块

磁盘缓存包括三部分,全尺寸图片缓存、缩略图缓存、失败缓存。

  • 全尺寸缓存:主要保存从远程图片服务器获取的图片和从音频、视频文件中提取的图片。由于这些来源的图片获取成本比较高,这些图片以原始尺寸进行保存。默认该缓存大小为 50MB,采用最近最少使用方式进行替换,可以通过修改 data/com.canonical.Unity.Thumbnailer.gschema.xml 文件中的 full-size-cache-size 节点数据来重新设置缓存大小。
  • 缩略图缓存:保存为调用者生成的指定尺寸的缩略图。该缓存大小默认为 100MB,同样可以通过修改 thumbnail-cache-size 节点数据来重新设置缩略图缓存大小。
  • 失败缓存:保存由于异常导致缩略图提取失败的项。对于远程文件,可能是因为无法获取远程文件(文件不存在、授权失败等);对于本地文件,可能是因为文件损坏或者音频文件不包含插图等。失败缓存主要用于减少对已知错误的重复尝试,对于该缓存中的内容,采用最近最少使用和指定过期时间的方式控制缓存失效。

由于使用了三个缓存,缩略图服务在返回指定大小缩略图时的查找流程大致为:

  1. 检查缩略图缓存中是否已经存在指定大小的图片。如果存在则直接返回。
  2. 检查全尺寸缓存中是否有该图片的全尺寸副本。如果存在则使用全尺寸图片进行缩放,将缩放完成的缩略图添加到缩略图缓存后返回。
  3. 检查失败缓存中是否有对应的项,如果有则直接返回错误。
  4. 尝试从远程下载或者从原始文件提取缩略图。如果失败,则加入到失败缓存中,并返回错误。
  5. 如果原始文件是从远程下载,或者从音频、视频流中提取,将源文件加入到全尺寸缓存中。
  6. 缩放图片文件到指定大小,加入到缩略图缓存中,并返回。

性能提升

缩略图服务的主要耗时在基于网络的下载和基于 CPU 的图片提取。因此对性能的提升,主要考虑在网络 IO 和 CPU 利用率上。为了避免这两个耗时的操作阻塞其他请求,特别是能够可以从缓存模块中快速响应的请求,下载和图片抽取被放在独立的事件循环中,利用 Qt 的信号和槽机制,将耗时请求异步化。

对于缓存的测试,使用配置为 Intel Ivy Bridge i7-3770k 3.5 GHz 处理器和 256GB 固态硬盘的机器,测试数据为使用 60 字节长度字符串作为键,使用平均大小为 20KB 的随机二进制数据作为值,缓存大小为 100MB。

缓存写入时间为大约 2.8 秒,然后测试场景为 80% 的缓存命中率,当缓存未命中时,在缓存中插入新的数据,触发缓存按照最近最少使用模式进行数据替换。在 10 万次循环中,缓存每秒返回约 4800 个“快照”,聚合读写吞吐率在每秒 93MB。如果将缓存命中率提升到 90%,每秒返回的记录数接近翻倍,达到了每秒 7100 条记录。[1]

总结

缩略图服务中的每个模块都有清晰的接口定义。支持新的媒体类型或者新的远程图片服务扩展非常容易,不会影响到现有代码。

为了尽可能的利用硬件资源,缩略图服务在针对长时间操作的异步接口使用了多线程的方式,提高系统 IO 利用率。

缩略图服务目前使用在 Ubuntu Touch 系统上,为图库、相机、音乐和其他使用媒体缩略图的应用提供缩略图服务。


感谢徐川对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

架构语言 & 开发