淘宝网的困境

对于淘宝网这样的大型电子商务网站而言,图片服务的要求极高。对卖家而言,图片远胜于文字描述,因此卖家格外看重图片的显示质量与访问速度。根据淘宝网的流量分析,整个淘宝网流量中,图片访问流量占比超过 90%,而主站网页流量占比不到 10%。同时,大量的图片需要根据不同的应用位置,生成不同大小规格的缩略图。考虑到多种不同的应用场景以及改版的可能性,一张原图有可能需要生成 20 多个不同尺寸规格的缩略图。

淘宝整体图片存储系统容量为 1800TB(1.8PB),已占用空间 990TB(约 1PB)。保存的图片文件数量达到 286 亿多个,这些文件包括根据原图生成的缩略图。平均图片大小为 17.45KB;8KB 以下的图片占图片总量的 61%,占存储容量的 11%。对于如此大规模的小文件存储与读取,需要频繁的寻道和换道,在大量高并发访问的情况下,非常容易造成读取延迟。

2007 年之前,淘宝采用 NetApp 公司的文件存储系统。至 2006 年,NetApp 公司最高端的产品已不能满足淘宝存储的要求。首先是商用存储系统没有对小文件存储和读取环境进行针对性优化;其次,文件数量巨大,网络存储设备无法支撑;另外,整个系统所连接的服务器越来越多,网络连接数已到达网络存储设备的极限。此外,商用存储系统扩容成本高,10TB 的存储容量需要几百万元,而且存在单点故障,容灾和安全性无法得到很好的保证。

淘宝网自主开发的目的

  1. 商用软件的局限性:商用软件很难满足大规模系统的应用需求,无论是存储、CDN 还是负载均衡,因为在厂商实验室端,很难实现如此大的数据规模测试。
  2. 可控性与扩展性:研发过程中,将开源和自主开发相结合,会有更好的可控性。系统出现问题时,完全可以从底层解决,系统扩展性也更高。
  3. 规模效应:在一定规模效应基础上,研发的投入都是值得的。当规模超过交叉点后,自主研发才能收到较好的经济效果。实际上,淘宝网的规模已经远远超过了交叉点。
  4. 多层次优化:自主研发的系统可在软件和硬件多个层次不断地优化。

淘宝 TFS 的介绍

TFS 1.0 版本

从 2006 年开始,淘宝网决定自己开发一套针对海量小文件存储难题的文件系统,用于解决自身图片存储的难题。到 2007 年 6 月,TFS(Taobao File System,淘宝文件系统)正式上线运营。在生产环境中应用的集群规模达到了 200 台 PC Server(146G*6 SAS 15K Raid5),文件数量达到上亿级别。系统部署存储容量为 140TB,实际使用存储容量为 50TB;单台支持随机 IOPS 200+,流量 3MBps。

图为淘宝集群文件系统 TFS 1.0 第一版的逻辑架构:集群由一对 Name Server 和多台 Data Server 构成,Name Server 的两台服务器互为双机,即集群文件系统中管理节点的概念。

  • 每个 Data Server 运行在一台普通的 Linux 主机上。
  • 以 Block 文件的形式存放数据文件(一般 64MB 一个 Block)。
  • Block 存多份以保证数据安全。
  • 利用 ext3 文件系统存放数据文件。
  • 磁盘 Raid5 做数据冗余。
  • 文件名内置元数据信息,用户自己保存 TFS 文件名与实际文件的对照关系,使得元数据量特别小。

TFS 最大的特点就是将一部分元数据隐藏到图片的保存文件名上,大大简化了元数据,消除了管理节点对整体系统性能的制约。这一理念和目前业界流行的“对象存储”较为类似。传统的集群系统里面元数据只有 1 份,通常由管理节点来管理,因而很容易成为瓶颈。而对于淘宝网的用户来说,图片文件究竟用什么名字来保存实际上用户并不关心。因此,TFS 在设计规划上考虑在图片的保存文件名上暗藏了一些元数据信息,例如图片的大小、时间、访问频次等信息,包括所在的逻辑块号。而在元数据上,实际上保存的信息很少,因此元数据结构非常简单,仅仅只需要一个 File ID,能够准确定位文件在什么地方。由于大量的文件信息都隐藏在文件名中,整个系统完全抛弃了传统的目录树结构,因为目录树开销最大。拿掉后,整个集群的高可扩展性极大提高。

TFS 1.3 版本

到 2009 年 6 月,TFS 1.3 版本上线,集群规模大大扩展,部署到淘宝的图片生产系统上。整个系统已经从原有 200 台 PC 服务器扩增至 440 台 PC Server(300G12 SAS 15K RPM)+ 30 台 PC Server(600G12 SAS 15K RPM)。支持文件数量也扩容至百亿级别;系统部署存储容量 1800TB(1.8PB);当前实际存储容量 995TB;单台 Data Server 支持随机 IOPS 900+,流量 15MB+;目前 Name Server 运行的物理内存是 217MB(服务器使用千兆网卡)。

tfs-2

图为 TFS 1.3 版本的逻辑结构图。在 TFS 1.3 版本中,淘宝网的软件工作组重点改善了心跳和同步的性能,最新版本的心跳和同步在几秒钟之内就可完成切换。同时进行了一些新的优化,包括元数据存内存上、清理磁盘空间,性能上也做了优化,具体包括:

  • 完全扁平化的数据组织结构,抛弃了传统文件系统的目录结构。
  • 在块设备基础上建立自有的文件系统,减少 EXT3 等文件系统数据碎片带来的性能损耗。
  • 单进程管理单块磁盘的方式,摒除 RAID5 机制。
  • 带有 HA 机制的中央控制节点,在安全稳定和性能复杂度之间取得平衡。
  • 尽量缩减元数据大小,将元数据全部加载入内存,提升访问速度。
  • 跨机架和 IDC 的负载均衡和冗余安全策略。
  • 完全平滑扩容。

TFS 主要的性能参数不是 IO 吞吐量,而是单台 PC Server 提供随机读写 IOPS。由于硬件型号不同,很难给出一个参考值来说明性能。但基本上可以达到单块磁盘随机 IOPS 理论最大值的 60% 左右,整机的输出随盘数增加而线性增加。

TFS 2.0 版本

TFS 2.0(下面简称 TFS,目前已经开源)是一个高可扩展、高可用、高性能、面向互联网服务的分布式文件系统。它主要针对海量的非结构化数据,构筑在普通的 Linux 机器集群上,可为外部提供高可靠和高并发的存储访问。TFS 为淘宝提供海量小文件存储,通常文件大小不超过 1MB,满足了淘宝对小文件存储的需求,被广泛地应用在淘宝各项应用中。它采用了 HA 架构和平滑扩容,保证了整个文件系统的可用性和扩展性。同时扁平化的数据组织结构,可将文件名映射到文件的物理地址,简化了文件的访问流程,一定程度上为 TFS 提供了良好的读写性能。

tfs-3-1

一个 TFS 集群由两个 NameServer 节点(一主一备)和多个 DataServer 节点组成。这些服务程序都是作为一个用户级的程序运行在普通 Linux 机器上的。在 TFS 中,将大量的小文件(实际数据文件)合并成为一个大文件,这个大文件称为块(Block)。每个 Block 拥有在集群内唯一的编号(Block Id),Block Id 在 NameServer 创建 Block 的时候分配,NameServer 维护 Block 与 DataServer 的关系。Block 中的实际数据都存储在 DataServer 上。而一台 DataServer 服务器一般会有多个独立 DataServer 进程存在,每个进程负责管理一个挂载点。这个挂载点一般是一个独立磁盘上的文件目录,以降低单个磁盘损坏带来的影响。正常情况下,一个块会在 DataServer 上存在,主 NameServer 负责 Block 的创建、删除、复制、均衡、整理。NameServer 不负责实际数据的读写,实际数据的读写由 DataServer 完成。

  • NameServer 主要功能:管理维护 Block 和 DataServer 相关信息,包括 DataServer 加入、退出、心跳信息,Block 和 DataServer 的对应关系建立、解除。
  • DataServer 主要功能:负责实际数据的存储和读写。

同时为了考虑容灾,NameServer 采用了 HA 结构,即两台机器互为热备,同时运行,一台为主,一台为备。主机绑定到对外 VIP,提供服务;当主机器宕机后,迅速将 VIP 绑定至备份 NameServer,将其切换为主机,对外提供服务。图中的 HeartAgent 就完成了此功能。

TFS 的块大小可以通过配置项来决定,通常使用的块大小为 64MB。TFS 的设计目标是海量小文件的存储,所以每个块中会存储许多不同的小文件。DataServer 进程会给 Block 中的每个文件分配一个 ID(File ID,该 ID 在每个 Block 中唯一),并将每个文件在 Block 中的信息存放在和 Block 对应的 Index 文件中。这个 Index 文件一般都会全部 load 在内存,除非出现 DataServer 服务器内存和集群中所存放文件平均大小不匹配的情况。

另外,还可以部署一个对等的 TFS 集群,作为当前集群的辅集群。辅集群不提供来自应用的写入,只接受来自主集群的写入。当前主集群的每个数据变更操作都会重放至辅集群。辅集群也可以提供对外的读,并且在主集群出现故障的时候,可以接管主集群的工作。

平滑扩容

原有 TFS 集群运行一定时间后,集群容量不足,此时需要对 TFS 集群扩容。由于 DataServer 与 NameServer 之间使用心跳机制通信,如果系统扩容,只需要将相应数量的新 DataServer 服务器部署好应用程序后启动即可。这些 DataServer 服务器会向 NameServer 进行心跳汇报。NameServer 会根据 DataServer 容量的比率和 DataServer 的负载,决定新数据写往哪台 DataServer 服务器。根据写入策略,容量较小、负载较轻的服务器新数据写入的概率会比较高。同时,在集群负载比较轻的时候,NameServer 会对 DataServer 上的 Block 进行均衡,使所有 DataServer 的容量尽早达到均衡。

进行均衡计划时,首先计算每台机器应拥有的 Blocks 平均数量,然后将机器划分为两堆:一堆是超过平均数量的,作为移动源;一类是低于平均数量的,作为移动目的。

移动目的的选择:首先一个 Block 的移动的源和目的,应该保持在同一网段内,也就是要与另外的 Block 不同网段;另外,在作为目的的一定机器内,优先选择同机器的源到目的之间移动,也就是同台 DataServer 服务器中的不同 DataServer 进程。

当有服务器故障或者下线退出时(单个集群内的不同网段机器不能同时退出),不影响 TFS 的服务。此时 NameServer 会检测到备份数减少的 Block,对这些 Block 重新进行数据复制。

在创建复制计划时,一次要复制多个 Block,每个 Block 的复制源和目的都要尽可能的不同,并且保证每个 Block 在不同的子网段内。因此采用轮换选择(Round Robin)算法,并结合加权平均。

由于 DataServer 之间的通信主要发生在数据写入转发的时候和数据复制的时候,集群扩容基本没有影响。假设一个 Block 为 64MB,数量级为 1PB。那么 NameServer 上会有 1 * 1024 * 1024 * 1024 / 64 = 16.7M 个 Block。假设每个 Block 的元数据大小为 0.1KB,则占用内存不到 2GB。

存储机制

在 TFS 中,将大量的小文件(实际用户文件)合并成为一个大文件,这个大文件称为块(Block)。TFS 以 Block 的方式组织文件的存储。每一个 Block 在整个集群内拥有唯一的编号,这个编号是由 NameServer 进行分配的,而 DataServer 上实际存储了该 Block。在 NameServer 节点中存储了所有的 Block 的信息,一个 Block 存储于多个 DataServer 中以保证数据的冗余。对于数据读写请求,均先由 NameServer 选择合适的 DataServer 节点返回给客户端,再在对应的 DataServer 节点上进行数据操作。NameServer 需要维护 Block 信息列表,以及 Block 与 DataServer 之间的映射关系,其存储的元数据结构如下:

tfs-3-2

在 DataServer 节点上,在挂载目录上会有很多物理块,物理块以文件的形式存在磁盘上,并在 DataServer 部署前预先分配,以保证后续的访问速度和减少碎片产生。为了满足这个特性,DataServer 现一般在 EXT4 文件系统上运行。物理块分为主块和扩展块,一般主块的大小会远大于扩展块,使用扩展块是为了满足文件更新操作时文件大小的变化。每个 Block 在文件系统上以“主块 + 扩展块”的方式存储。每一个 Block 可能对应于多个物理块,其中包括一个主块,多个扩展块。

在 DataServer 端,每个 Block 可能会有多个实际的物理文件组成:一个主 Physical Block 文件,N 个扩展 Physical Block 文件和一个与该 Block 对应的索引文件。Block 中的每个小文件会用一个 Block 内唯一的 File ID 来标识。DataServer 会在启动的时候把自身所拥有的 Block 和对应的 Index 加载进来。

容错机制

  • 集群容错:TFS 可以配置主辅集群,一般主辅集群会存放在两个不同的机房。主集群提供所有功能,辅集群只提供读。主集群会把所有操作重放到辅集群。这样既提供了负载均衡,又可以在主集群机房出现异常的情况不会中断服务或者丢失数据。
  • NameServer 容错:NameServer 主要管理了 DataServer 和 Block 之间的关系,如每个 DataServer 拥有哪些 Block,每个 Block 存放在哪些 DataServer 上等。同时,NameServer 采用了 HA 结构,一主一备,主 NameServer 上的操作会重放至备 NameServer。如果主 NameServer 出现问题,可以实时切换到备 NameServer。另外 NameServer 和 DataServer 之间也会有定时的 Heartbeat,DataServer 会把自己拥有的 Block 发送给 NameServer。NameServer 会根据这些信息重建 DataServer 和 Block 的关系。
  • DataServer 容错:TFS 采用 Block 存储多份的方式来实现 DataServer 的容错。每一个 Block 会在 TFS 中存在多份,一般为 3 份,并且分布在不同网段的不同 DataServer 上。对于每一个写入请求,必须在所有的 Block 写入成功才算成功。当出现磁盘损坏、DataServer 宕机的时候,TFS 启动复制流程,把备份数未达到最小备份数的 Block 尽快复制到其他 DataServer 上去。TFS 对每一个文件会记录校验 CRC,当客户端发现 CRC 和文件内容不匹配时,会自动切换到一个好的 Block 上读取。此后客户端将会实现自动修复单个文件损坏的情况。

并发机制

对于同一个文件来说,多个用户可以并发读。现有 TFS 并不支持并发写一个文件,一个文件只会有一个用户在写。这在 TFS 的设计里面对应着是一个 Block 同时只能有一个写或者更新操作。

TFS 文件名的结构

TFS 的文件名由块号和文件号通过某种对应关系组成,最大长度为 18 字节。文件名固定以 T 开始,第二字节为该集群的编号(可以在配置项中指定,取值范围 1~9)。余下的字节由 Block ID 和 File ID 通过一定的编码方式得到。文件名由客户端程序进行编码和解码,它映射方式如下图:

tfs-3-3

TFS 客户程序在读文件的时候,通过将文件名转换为 Block ID 和 File ID 信息,然后可以在 NameServer 取得该块所在 DataServer 信息(如果客户端有该 Block 与 DataServer 的缓存,则直接从缓存中取),然后与 DataServer 进行读取操作。

图片服务器部署与缓存

下图为淘宝网整体系统的拓扑图结构。整个系统就像一个庞大的服务器一样,有处理单元、缓存单元和存储单元。前面已经详细介绍过了后台的 TFS 集群文件存储系统,在 TFS 前端,还部署着 200 多台图片文件服务器,用 Apache 实现,用于生成缩略图的运算。

根据淘宝网的缩略图生成规则,缩略图都是实时生成的。这样做的好处有两点:一是为了避免后端图片服务器上存储的图片数量过多,大大节约后台存储空间的需求。淘宝网计算,采用实时生成缩略图的模式比提前全部生成好缩略图的模式节约 90% 的存储空间,也就是说,存储空间只需要后一种模式的 10%;二是,缩略图可根据需要实时生成出来,更为灵活。

tfs-4-4

淘宝网图片存储与处理系统全局拓扑,图片服务器前端还有一级和二级缓存服务器,尽量让图片在缓存中命中,最大程度的避免图片热点。实际上,后端到达 TFS 的流量已经非常离散和平均。

图片文件服务器的前端则是一级缓存和二级缓存,前面还有全局负载均衡的设置,解决图片的访问热点问题。图片的访问热点一定存在,重要的是,让图片尽量在缓存中命中。目前淘宝网在各个运营商的中心点设有二级缓存,整体系统中心店设有一级缓存,加上全局负载均衡,传递到后端 TFS 的流量就已经非常均衡和分散了,对前端的响应性能也大大提高。

根据淘宝的缓存策略,大部分图片都尽量在缓存中命中。如果缓存中无法命中,则会在本地服务器上查找是否存有原图,并根据原图生成缩略图。如果都没有命中,则会考虑去后台 TFS 集群文件存储系统上调取。因此,最终反馈到 TFS 集群文件存储系统上的流量已经被大大优化了。

淘宝网将图片处理与缓存编写成基于 Nginx 的模块(Nginx-tfs)。淘宝认为 Nginx 是目前性能最高的 HTTP 服务器(用户空间),代码清晰,模块化非常好。淘宝使用 GraphicsMagick 进行图片处理,采用了面向小对象的缓存文件系统。前端有 LVS+Haproxy 将原图和其所有缩略图请求都调度到同一台 Image Server。

文件定位上,内存用 Hash 算法做索引,最多一次读盘。写盘方式则采用 Append 方式写,并采用了淘汰策略 FIFO。主要考虑降低硬盘的写操作,没有必要进一步提高 Cache 命中率,因为 Image Server 和 TFS 在同一个数据中心,读盘效率还是非常高的。

说明:本文内容主要基于 2006 年至 2013 年期间的淘宝图片服务架构与技术资料整理,部分数据(如存储容量、服务器规模)及版本特性(TFS 1.0/1.3/2.0)反映的是当时的历史状态。随着技术发展,当前实际生产环境可能已采用更新的架构或云原生解决方案,请以官方最新文档为准。