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(每秒操作次数)。
01 总体设计
通过对 CAL 现有业务逻辑和历史性能数据的分析,我们明确了设计目标:
1)文件系统元数据路径,支持100K OP/S ,包括:create / getattr / lookup / readdir / unlink等文件系统操作;并预留足够的升级空间满足业务发展。
2)文件系统的数据路径,支持150K IOPS(50%读,50%写),以及至少6GB/S(50%读,50%写)的吞吐。
3)单集群容量需求300TB 可用空间,满足性能需求的前提下尽可能降低存储成本。
这个略显激进的设计目标,其实在很大程度上已经决定了我们的方案。
基于我们之前针对Ceph MDS的性能研究,单MDS只能支持2K ~ 4K OP/S, 要支持100K OP/S,MDS多活负载均衡集群是唯一的路径。至少需要25个MDS Active-Active 组成集群。同时,业务负载还要能相对均衡地分布到每个MDS上。
150K IOPS的读写性能,如果单用机械硬盘HDD,需要至少1500块盘,可用容量接近3PB,又回到了用尽性能而浪费容量的老路。如果全部用SSD,1PB 裸容量的SSD 虽然能比商业存储省钱,但仍是一笔不小的数字。针对日志数据有明显冷热度的特点,使用分层存储,把频繁访问的数据放在SSD上,相对冷的数据放在HDD上,才能达到最佳费效比。
02 实现细节
1. MDS 多活负载均衡集群
要实现向外扩展(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 绑定关系,就能在客户不感知的情况下实现无缝扩容。
2. Cache Tier
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。
换句话说,通过 Cache Tier 我们减少了 94 的写 IOPS。
图 3 集群客户端写入性能(点击可查看大图)
而在读取侧,如图 4 所示,几乎所有的 IO 都在 Cache Tier 上发生,Base Tier 读流量近乎为 0。
图 4 集群客户端读取性能(点击可查看大图)
综合来看, 在 Cache Tier 上的总 IOPS 达到了 70K,而 Base Tier 只有 1.5K IOPS。通过深入理解业务的 IO 模型,合理配置 Cache Tier,我们实现了分层存储。
应用享受到了全闪存集群的性能,成本上却接近全机械盘的价格,实现了了性价比最大化。
03 遇到的问题
下面分享几个在实施日志存储方案中遇到的软硬件问题,附上我们的解决方案,以供大家参考。
1. Bluestore Allocator(空间分配器)
在上线过程中,我们很顺利地度过了 25%, 50%的流量。但在 75%流量时,遇到了诡异的性能问题。
如下图 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(点击可查看大图)
通过分析 Bluestore 的分配表,我们发现其实物理空间并没有碎片化,只是 StupidAllocator 在内存中的实现导致了碎片化。
StupidAllocator 是一个类似于内存管理中 Buddy 分配的一个实现。
通过下图 8 的例子,我们看看碎片是如何产生的:
一个 20K 的连续空间,经过 5 次 4K 分配,和乱序的 5 次对应的 4K 释放后,会变成 8k+4k+8k 三块空间。
其中[8K, 12K) 区域已经是个碎片,但因为和周边区块不是同样大小,落到不同的 bin 中,已经很难再被合并了,而类似的操作序列,在 Bluestore 日常面临的空间分配回收请求中并不鲜见。
OSD 重启过程中, 分配器的内存结构会根据磁盘上的位图重新建立,恰好是一次全局的合并,这也就解释了为什么重启服务能暂时缓解这个问题。
图 8(点击可查看大图)
我们首先在社区发现定位了这个问题,并与核心开发者一起验证了 Naultilus 中的位图分配器(Bitmap Allocator)没有类似问题,且性能表现更为稳定。因此也使得位图分配器被完整地反向移植到 Luminous 和 Mimic 版本中,并成为默认分配器。
2. 应对 SSD 稳态性能下降
关于 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 发生时延迟和吞吐都会受到很大的负面影响。
分析完这个可能原因后,在无法变更硬件的前提下,我们的解决办法是通过主动预留空间,来弥补厂商原有 OP 先天不足的“缺点”:
在 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 同样的性能和可靠性,但我们并不确定该经验是否在更大范围内具有通用性。这里仅供读者参考,也欢迎来自各厂商的专家提供更多的分享。
04 总结
在 eBay 存储团队和业务团队的无缝合作下,我们完美地将 eBay 应用的日志从商业存储迁移到了 CephFS 上,达到了非常高的性能和费效比(迁移给 eBay 带来的成本下降在单数据中心达数百万美金)。整个方案没有单点和瓶颈,各个部分均可以横向拓展,支撑更高的性能和吞吐。
CephFS 由于成熟得相对晚,在国内外的大规模商用案例有限,更多的被作为归档和冷存储使用,较少有大流量的线上业务应用。尽管我们比较激进的使用了 MultiMDS 多活,Cache Tier 等当前在业界还较少部署的技术,但 CephFS 在大流量的生产环境中证明了自己的价值和稳定性,为其他 eBay 内部应用迁移到 CephFS 打下了坚实的基础。
本文转载自公众号 eBay 技术荟(ID:eBayTechRecruiting)。
原文链接:
https://mp.weixin.qq.com/s/vUrp6Nyj06lsq8mZGR2n8g
评论 1 条评论