CAL(Central Application Logging)系统主要负责收集和处理 eBay 内部各个应用程序池的日志,日处理超过 3PB 的数据,供运维团队和开发团队日常监控使用。
CAL 系统通 过 HTTP 接口接受应用产生的日志,将日志持久化到经 NFS 挂载 的网络存储上, 用户(运维团队和开发团队)可以通过 CAL 系统方便地 查找、查看日志 。同时,日志也会被导入 Hadoop 进行进一步的分析形成报告。该系统自 21 世纪初至今,已经有 10 多年 的历史了。
CAL 从第一天起就运行在 Netapp 的商业存储 上。随着业务的发展,业务产生的数据量急剧增加,单个存储集群已经无法承载 CAL 的流量和性能需求,存储团队和业务团队通过增加集群来解决性能问题。一直到 2018 年 ,CAL 在每个数据中心已经需要 25 个 集群才能支撑起其性能需求。
虽然统计上,CAL 只使用了 eBay 5% 的 NFS Filer 总容量,但实际上却消耗了 50% 的总性能。性能和容量的巨大偏离,使得实际成本已经比该存储方案的 裸 $/GB 成本 高了一个数量级。
高成本,叠加上由 eBay 业务驱动的每年 30% 自然增长,这套架构亟需重构优化。
2014 年,eBay 存储团队开始在公司内将 Ceph 应用于生产环境。第一阶段主要是 RBD 块设备服务。
2017 年,Jewel 版本发布后,我们开始尝试提供 CephFS 分布式文件系统服务,多个天使应用从商业存储迁移到 CephFS 上,从中我们发现修复了 MDS 的多个 bug 并贡献回社区,也在社区首次完成了 MDS 的性能分析。
2018 年,随着 MDS 多活功能的初步成熟,我们也和业务团队一起,开始了把 CAL 迁移到 CephFS 上的征程。
2019 年初上线至今,Ceph 在大流量下稳定运行。平均读写带宽同时超过了 3GB/S ,读写请求数合计超过 100K IOPS(每秒进行读写操作的次数), 文件系统元数据操作数超过 30K OP/S(每秒操作次数)。
通过对 CAL 现有业务逻辑和历史性能数据的分析,我们明确了 设计目标 :
1)文件系统元数据路径,支持 100K OP/S ,包括:create / getattr / lookup / readdir / unlink 等文件系统操作;并预留足够的升级空间满足业务发展。
2)文件系统的数据路径,支持 150K IOPS(50% 读,50% 写),以及至少 6GB/S(50% 读,50% 写)的吞吐。
3)单集群容量需求300TB 可用空间,满足性能需求的前提下尽可能降低存储成本。
这个略显激进的设计目标,其实在很大程度上已经决定了我们的方案。
要实现向外扩展(scale-out)的架构,核心是任务分配,把负载相对均等地分配到每个 MDS 上。对于文件系统来说,整个目录结构是一棵树。任务分配的本质,是通过设计目录树的结构,使得 叶子(文件) 平均地长在这颗树的每个 枝干(目录) 上。同时,每个层次的文件 / 文件夹个数必须是 可控且相对均衡的 ,这样才能尽量降低树的深度,避免对一个巨大文件夹进行 ListDir 操作 带来的延迟和开销。
CAL 原先的目录层次如下图 1 所示,一共有 15 层,100 多万 个文件夹, 几乎违反了上面讨论的所有原则,目录过深且不均衡 。eBay 每个 pool 承担一个服务,每个服务的流量,服务器数量差异显著。更糟糕的是整个目录树,在每个小时开始的时候,几乎完全重建(虚线部分),导致每小时开始时元数据操作风暴。
图 1(点击可查看大图)
针对这个问题,我们和 CAL 团队合作设计并实现了 一套新的目录层次 ,如图 2 所示:
图 2(点击可查看大图)
1)在根目录下建立 1024 个 Bucket, Client (CAL 的 Client,其实是业务 APP 服务器) 通过哈希映射到其中一个 bucket 下 。由于哈希的(统计意义)均衡性,每个 Bucket 里的 Client 文件夹数量是均衡的。而 Client 文件夹的总数,和业务 APP 服务器的总数对应。
虽然虚拟化、容器化使得 Client 越来越小,越来越多,但因为 Bucket 数量足够多, 每个 Bucket 下的文件夹数依旧是可控的 。而对于每个 Client 文件夹里的文件数,根据配置,每小时产生 5-20 个 不等的文件,文件总数 = 5~20 /hour * rotation_hours。 可见,其文件数也是可控的 。
2)Bucket 通过 auth_pin , 绑定到 MDS_rank 上, 简单来说:
auth_pin = bucket_id % NUM_MDSs
基于之前讨论的 Bucket 均衡性,以及所有 Bucket 被均衡地分布到各个 MDS。 可见,MDS 的负载也是均衡的 。
除此之外,我们也预留了拓展性。当前我们用了 33 个 MDS 做了 AA,其中 32 个 MDS 分别绑定了 32 个 Bucket,rank 为 0 的 MDS 绑定到了根目录。日后,只需要增加 MDS 的数量,例如从 33(32+1) 增加到 65(64+1) ,修改 auth_pin 绑定关系,就能在客户不感知的情况下实现无缝扩容。
Ceph Cache Tier 是一个在诞生之初被抱以巨大期望,而在现实中让不少部署踩坑的技术,尤其是在 RBD 和 CephFS 上。在社区邮件组和 IRC 里最经常出现的问题就是应用了 Cache Tier,性能反而下降了。
其实,那些场景多数是误读了 Cache Tier 的应用领域。Cache Tier 的管理粒度是 Object(默认 4MB) , 而在 RBD/CephFS 上的用户 IO 通常明显 小于 4MB 。当用户访问未命中,Cache Tier 决定将这个 Object 从 Base Tier 缓存到 Cache Tier 时, 付出了 OBJ_SIZE 大小的 慢速读 (Base Tier)加上 OBJ_SIZE 大小的 快速写 (Cache Tier)。
只有后续用户对同一个 Object 的重复访问达到足够多的次数,才能体现 Cache Tier 的性能和成本优势。例如,业务负载是随机 IO,平均请求大小 4KB ,则命中率必须达到 **99.9%** 以上。
现实是许多业务负载并没有这样的特性。
在 CAL 的业务模型里,典型的 IO 负载是应用日志每隔一分钟或者 128KB 触发一次写入。同时,读取端不断地追踪日志,以同样的间隔把数据读走。 针对这个特点,我们决定将 Cache Tier 配置成为一个 Writeback Cache(回写式缓存), 这样就能实现多个目的 :
1)写入缓冲与合并。Cache Tier 的设计容量足够缓存一小时的写入量,不论业务的写入请求大小是多少,都会被 Cache Tier 吸收,以 OBJ_SIZE(4M)为单位刷回 Base Tier。4M 的大块写,是对 HDD 最友好的访问行为,可以最大化 Base Tier 的吞吐量。
2)读取缓存。CAL 的读取行为也大多集中在对最近一个小时写入数据的读取。这部分数据在 Cache Tier 里,所以读请求也能基本全部命中 Cache Tier。
3)故障隔离。机械硬盘的故障率比 SSD 高几个数量级,并且更容易因坏道导致响应时间增长,从而使得 Ceph 集群出现 慢请求(slow request) 。表现在服务质量上,就是延迟相对不稳定且 尾延迟(tail latency) 特别长。借助 Cache Tier 的缓冲,业务并不会直接感受到由全机械硬盘构成的 Base Tier 的性能抖动,从而达到更好的性能一致性。
值得一提的一个优化点是:我们通过禁用 Hitset 关闭了 Proxy Write 这个功能。Ceph Cache Tier 的默认行为是 Proxy Write——即若待写入数据在 Cache Tier 不命中,Cache Tier 会直接把请求写入 Base Tier,并在写入完成后才将结果返回给应用。
这是一个正确的设计,主要是为了避免写请求不命中时 promote 带来的额外延迟,并解耦数据写入逻辑和 Cache Promote 逻辑。
但在 eBay 日志的应用场景中,因为写入行为的顺序性和日志不可变更的特性,可以明确知道 写入不命中一定是因为写入了一个新的 Ceph Object,而非对老 Object 的改写,因此 proxy write 的逻辑就带来了额外的开销 。
例如,对 obj x 的第一次写入, write(obj_x, 128kb), 在默认的 Proxy Write 行为下 ,这个写被 proxy 到了 Base Tier 。写入成功后,在 Base Tier 留下 128KB 的 object,然后 Cache Tier 再把 obj_x 从 Base_tier promote 上来。
而关闭 proxy write 后,则先对 obj_x 做一次 promote,在 Cache Tier 中产生一个空对象,再直接写入 128KB 数据即可。
相比之下,后者节约了 Base Tier 一次 128KB 写和一次 128KB 读,对于由全机械硬盘构成的 Base Tier 来说,这样的节约意义重大,并且应用的写入延迟大大降低了。这部分 Ceph 具体代码,可以阅读
PrimaryLogPG::maybe_handle_cache_detail 的实现。
其他的配置优化包括设置 min_flush_age 和 min_evict_age 来保证最近一小时的数据不会被刷出 Cache,以及对 target_max_byte, target_dirty_ratio, target_dirty_ratio_high 的调整,在此不一一赘述。
通过上述一系列优化, 如图 3 所示,在写入侧, Cache Tier 非常好地完成了写入缓冲与合并,来自应用的 25K IOPS 经由 Cache Tier 缓冲与合并之后到 Base Tier(fs_data) 写入请求只有 1.5K IOPS。
图 3 集群客户端写入性能(点击可查看大图)
而在读取侧,如图 4 所示,几乎所有的 IO 都在 Cache Tier 上发生, Base Tier 读流量近乎为 0 。
图 4 集群客户端读取性能(点击可查看大图)
综合来看, 在 Cache Tier 上的总 IOPS 达到了 70K ,而 Base Tier 只有 1.5K IOPS。通过深入理解业务的 IO 模型,合理配置 Cache Tier,我们实现了 分层存储 。
下面分享几个在实施日志存储方案中遇到的 软硬件问题 ,附上我们的 解决方案 ,以供大家参考。
如下图 5 所示, Cache Tier 的写入延迟暴增 ,甚至能达到分钟级别,通过 OSD Performance Counter 很快把问题缩小到 Bluestore 内部, 结合日志发现延迟主由
STATE_KV_COMMITING_LATSTATE_KV_COMMITING_LAT 约等于 commit_lat。
我们第一个怀疑的对象是 RocksDB 的性能,尤其是 compaction 带来的影响,并在此做了大量的调优,然而一无所获。
但我们发现,在调优的过程中,为了修改参数重启 OSD 服务后,被重启的 OSD 能保持 20 小时 良好性能,之后延迟慢慢衰减到重启以前。 既然重启 OSD 能暂时缓解问题,那 RocksDB 就没有嫌疑了 。
图 5 Bluestore commit latency 性能监控(点击可查看大图)
如下图 6 所示,进一步观察分析 性能计数器(performance counter) 的数据并结合代码,我们注意到: STATE_KV_DONE_LAT 虽然平均值只有 STATE_KV_COMMITING_LAT 的 3% ,但与 STATE_KV_COMMITING_LAT 有明显的相关性。
这个发现大大缩小了问题的范围,因为 STATE_KV_DONE_LAT 所覆盖的代码范围基本只包含 _txc_finish 这一个函数。在某些时候,_txc_finish 释放空间后,需要等待超过 100 毫秒 才能获得分配器的锁。同时, perf 的结果也指向了 StupidAllocator 这个循环 。
图 6(点击可查看大图)
如下图 7 所示,进一步分析日志,我们发现在 StupidAllocator 中的空间碎片非常严重 。虽然磁盘利用率不超过 50% ,但分配器里已经没有超过 256K 的 数据段(segment) 可供分配。这就解释了为什么 StupidAllocator 会在上述的分配空间循环中耗费大量时间。因为分配时持有了分配器锁,所以释放空间时需要等待很长时间来获得锁。
图 7(点击可查看大图)
StupidAllocator 是一个类似于内存管理中 Buddy 分配的一个实现。
一个 20K 的连续空间,经过 5 次 4K 分配,和乱序的 5 次 对应的 4K 释放后,会变成 8k+4k+8k 三块空间。
其中 [8K, 12K) 区域已经是个碎片,但因为和周边区块不是同样大小,落到不同的 bin 中,已经很难再被合并了,而类似的操作序列,在 Bluestore 日常面临的空间分配回收请求中并不鲜见。
图 8(点击可查看大图)
我们首先在社区发现定位了这个问题,并与核心开发者一起验证了 Naultilus 中的 位图分配器(Bitmap Allocator) 没有类似问题,且性能表现更为稳定。因此也使得位图分配器被完整地反向移植到 Luminous 和 Mimic 版本中,并成为默认分配器。
关于 SSD 性能有一个并不冷门的常识——SSD 的稳态性能与标称性能不一定是一致的。对于普通数据中心级别的 SSD 来说, 稳态性能 通常比 非稳态性能 低(相应的,非稳态可以认为 SSD 尚有足够空间供分配,或者后台维护行为对业务 IO 无干扰),非写入密集型的型号差距会尤其明显。
SSD 进入稳态是需要一定写入量和时间的,因此在做硬件性能和软件性能调优时需要将这个因素充分考虑进去,否则实际产品运行起来以后就会出现意料不到的问题。
图 9 是我们实测某 DWPD 为 0.7 的产品稳态与非稳态性能比较。 蓝色线 是稳态性能, 橘色线 是非稳态性能(对 SSD 做了全盘 TRIM)。
图 9 SSD 稳态与非稳态性能对比(点击可查看大图)
受限于成本以及当前可用服务器选择,我们所采用的 SSD 即属于 非写入密集型 (DWPD 较低),同机型同容量,来自 3 个 不同厂商的 SSD 的 DWPD 分布在 0.7~1 之间。而 eBay 日志存储 Cache Tier 恰好是写入读取都很密集的类型。这下,我们似乎面临了巧妇难为无米之炊的困境。
基于我们对 SSD 实现原理的理解,非写入密集型 SSD(DWPD 指标低)稳态与非稳态性能差距大的主要原因之一是厂商针对这类 SSD 的产品设计上,出厂预留的 Over Provision 空间 较小。
当合理的产品设计面临“不合理”的业务需求时,大写入压力持续一段时间以后,SSD 固件做 空间整理 和 垃圾回收(GC) 的效率会变低。应用端的表现是在持续的大负载写入下,GC 发生时延迟和吞吐都会受到很大的负面影响。
在 SSD 被加入集群前,我们对全盘 TRIM 之后,分区时只使用 80% 的空间;留下 20% 被 TRIM 过的区域因为在固件 FTL 中标记为空闲,自然会在 GC 等操作的时候当作缓冲来使用。加上这额外的 20% OP 后,我们 SSD 的写入延迟和带宽都能稳定在最佳状态(图 9 中 橘色线 的性能)。
这套实践应用在同机型、多批次来自 3 个不同厂商的 SSD 上都获得同样好的效果,上线至今并未观察到衰减。
那么 20% 的空间预留是目前的经验值,当应用行为有所改变时 SSD 性能再次衰减又该怎么应对呢?需要停下服务重新修改预留空间大小吗?
其实不然, 对于已经衰减的 SSD, 只需要 TRIM 20 的区域提供 OP,再经过一个完整的写入周期后,性能就可以回升到最佳状态 。
图 10 展示的就是这样一个过程,在模拟测试开始约 600 秒 时,性能开始大幅下降,此时对预留的 20% 区域做 TRIM,经过一个多小时,性能逐步回升,最终回到峰值并持续稳定下去。
图 10 对预留区域 TRIM 恢复全盘性能测试(点击可查看大图)
在这里需要指出:上述经验是我们基于存储产品的理解和合理猜测基础上,经测试验证通过,并在生产中应用的。 限于条件和知识所限,我们无法进一步探究 OP 是否是同系列不同定位的 SSD 产品之间的主要差别 。
通过我们所接触的有限样本中的数据,可得出以下观点: 读密集型 SSD 加上大的 OP,能达到同系列写入密集型 SSD 同样的性能和可靠性,但我们并不确定该经验是否在更大范围内具有通用性 。这里仅供读者参考,也欢迎来自各厂商的专家提供更多的分享。
在 eBay 存储团队和业务团队的无缝合作下,我们完美地将 eBay 应用的日志从商业存储迁移到了 CephFS 上,达到了非常高的性能和费效比(迁移给 eBay 带来的成本下降在单数据中心达 数百万美金 )。整个方案没有单点和瓶颈,各个部分均可以横向拓展,支撑更高的性能和吞吐。
CephFS由于成熟得相对晚,在国内外的大规模商用案例有限,更多的被作为归档和冷存储使用,较少有大流量的线上业务应用。尽管我们比较激进的使用了 MultiMDS 多活 , Cache Tier 等当前在业界还较少部署的技术, 但 CephFS 在大流量的生产环境中证明了自己的价值和稳定性,为其他 eBay 内部应用迁移到 CephFS 打下了坚实的基础 。