分享 | CAL PB级的日志存储迁移到Ceph的实践
供稿 | Unified Monitoring Platform
翻译&编辑 | 顾欣怡
本文3663字,预计阅读时间11分钟
更多干货请关注“eBay技术荟”公众号
导读
CAL(Centralized Application Logging)是 eBay的集中式应用日志框架 ,每日处理大量数据。为满足日趋增大的存储要求,eBay开始 将CAL上 PB级 的日志存储迁移到Ceph 。本文主要讲述 如何修改现有监控系统设计 以符合该项目的要求,以及在项目实施中遇到了哪些 具体问题。
一、背景
CAL(Central Application Logging) 系统主要负责收集和处理eBay内部各个应用池的日志,日处理超过 3PB 的数据 ,提供包含原始日志、聚合报告、正则表达式搜索等在内的多种形式的结果供 SRE (Site Reliability Engineer)和 PD 们(Product Developer,产品开发设计师)日常监控使用。 从20世纪初上线至今,已经有 10多年 的历史了。
早期后台存储日志使用的是 NetApp 公司提供的 Filer ,作为 NFS (Network File System,网络文件系统)的解决方案,所有CAL相关的模块都需要从Filer上读写数据。
CAL使用的Filer,是由图1所示的多个小的物理卷组成的,它有2个主要的问题:
1. 一旦需要增加一个新的卷,或者移除一个老的卷,都需要所有CAL组件做一次代码发布, 效率很低。
2. 需要在写日志的时候选择最空闲的卷,来保证每个小卷都尽可能地使用均匀,但没有哪个算法能保证绝对均匀, 空间使用效率不是很好。

图1
另一方面,随着日志数据量的逐年增长, CAL Filer的容量(Volume )也增大到每个数据中心大约 300多T ,但 日志的有效时间 却从原先的几天下降到现在的 20个小时 。同时,鉴于NetApp Filer提供的支持和监控都很有限,已经越来越不能满足eBay CAL团队对于存储的要求。再者,CAL团队也在寻求 更快更稳定更便宜 的NFS存储方案。
鉴于以上几点原因,加上eBay内部也有团队深耕新式存储Ceph多年,很自然地,CAL团队着手和Ceph团队一起开始了CAL-on-Ceph这样一个跨团队的合作项目。
二、系统设计的修改
对于这样一个大规模的CAL系统,考虑到对Filer的使用经验和现状以及实时迁移,我们对现有的监控系统设计做了以下几个方面的修改。
1. 目录结构简化
在CAL系统设计之初,出于易用性考虑,加之当时日志的容量并不是很大,我们设计了 一种特殊的文件存放的目录结构, 如图2所示,深度 15+ 。这个目录结构包含了原始日志文件的 元数据信息(环境,日期,应用池,客户端IP等) 。因此几类使用广泛的 API ,例如 getPools (指定日期和环境,获取所有符合条件的应用池)和 getMachines (指定日期,环境,应用池,获取所有符合条件的机器(machine))都能通过 只遍历目录来得到结果 ,不需要任何其他的依赖。
图2(点击可观看大图)
可惜随着日志容量的与日俱增,这种把文件系统当数据库来使用的用例,已经让文件系统苦不堪言。随之带来的影响就是,文件系统越来越频繁地出现 读写文件十分缓慢 的现象,极大地影响了用户体验。即使把 Filer 换成 Ceph ,这个问题也亟待解决。
考虑到Ceph后端 MDS(Metadata Server,元数据服务器) 集群的可扩展性,我们将目录结构简化成下面的格式,如图3所示,深度为 5 (其中第三层为0-1023的数字,主要是为了方便分散文件到MDS集群的不同服务器上去); 将大量原本占据目录名存放的信息,转移到了文件名上。
图3(点击可观看大图)
2. 基于数据库的日志元数据存储和查询
目录结构简化只是解决了文件系统的问题,上面提到的很多API还是最适宜由数据库来服务,一条select语句就能搞定。因此,我们重新梳理了所有使用到文件目录的API,将最重要的 四个元组(日期,环境,应用池,客户端IP) 提取出来,作为创建数据库表的基本元素;并设计了一个包含 24张 表(每小时一张),且每张表包含四个列的数据库。考虑到eBay DBA的可用支持,最终选择了 MySQL 。
鉴于eBay拥有 超过百万台 的服务器,每个服务器又通过多个连接同时将日志写进CAL系统,可以预见这张表规模不会小。如果日志进入CAL系统后,上述API能在很短的 SLA 内被用户所消费,数据库的 最大TPS 可能会达到 50k (主要是写的TPS)。如果不采取优化,只是一条一条写的话,性能上会有问题。
我们调研了几种方案,最终选用了“LOAD DATA LOCAL INFILE”的方式来做批量写。实验表明,这种将客户端多个插入请求合并到本地文件,再传输到 MySQL server 端 去做批量插入的方法,最大能支持 322k TPS ,足以满足当前以及未来很长一段时间内的需求。
3. 文件IO操作异步化
在迁移的过程中,为了验证Ceph的性能,需要先做双写。在不影响当前数据流到Filer的前提下,克隆数据,导入Ceph。在 预发布(Staging)环境 做测试的时候,就已经发现Ceph相比于Filer,一个劣势就是会出现一定频率的 “卡死”现象 ,即某些CAL 服务器会突然连不上Ceph,所有对于Ceph的文件操作都直接没有响应了。同时还需要考虑到Ceph可能会有 宕机 等其他不可用的情况,在这些情况下,都不能影响到Filer的数据流。
CAL原先是同步处理一条一条的日志,一旦碰到这种现象将会直接影响本该正常写到Filer的日志。现在我们将 所有文件IO类的操作都从主处理流程上解耦 ,放到 后台线程池 中进行处理, 辅之以线程超时以避免“卡死”的情况。
同时,考虑到 快速失败机制(fast fail) ,我们在后台有个 额外线程 ,每隔一段时间主动检查Ceph的可用性。 如果不可用,涉及到Ceph的文件IO操作会 直接跳过 ,进一步减少不必要的内存堆积。
三、项目实施中碰到的问题
尽管事先已经考虑了诸多可能会出现的问题,并在预发布(Staging)环境进行了一定规模的测试,但是在生产(Production)环境实施的时候,我们仍然碰到了几个问题。 下面分享几个主要的有代表性的问题。
1. MySQL查询性能急剧下降
首次在生产环境上线大约半小时后,我们就观测到几个主要API访问MySQL时出现大规模的 读取困难 , 原先的查询秒级返回,变成现在的需要 超过 1分钟 才能返回 。查看MySQL的监控后,发现负载急剧增长,于是不得不先回滚。如图4所示:
图4
通过排查日志,并且和DBA一起研究过后,我们将可能的原因缩小到以下几方面,并做了相应的优化:
01
MySQL里产生了大量碎片
现有的 DB 数据保留(retention) 方法是采用 “DELETE * from TABLE xx where date < ${date}”,定期删除某个时间点之前的所有条目。但这并不是MySQL友好的方式,在目前单表几百万数据的情况下,随着时间的推移,会产生 大量的数据碎片, 进而造成MySQL主、从节点之间同步困难,最终影响数据库的性能。
咨询了DBA后,考虑到记录有效使用时间, 我们将数据保留方法调整为若干个小时(< 24小时 )后,直接“truncate”掉整张表,既快又不会产生碎片。
02
MySQL表中有大量重复数据
现有的写数据库操作逻辑是:当客户端同CAL 服务器建立连接后,会将一条记录放在CAL 服务器的本地缓存里,然后 周期性地 将缓存里的 所有记录 都 一次性 通过上面提到的”LOAD DATA LOCAL INFILE”的方式写到MySQL里。但每个客户端会建立若干个(一般为 5个 )连接到CAL 服务器。 因此,可能来自同一个客户端的多个连接会落到同一个CAL 服务器里。
考虑到这种情况,在略微降低SLA的前提下,将周期性刷新缓存到MySQL的时间间隔 从原来的 一分 钟 ,增大到了 五分钟 。 同时在刷新前,再对缓存做一次去重。 最终将单表的条目数量从原先的最多 900多万条 ,减少到现在的 500万条 左右。
03
MySQL表的设计没有索引
由于设计之初并没有考虑做表的索引, 通过上面发生的问题,我们开始思考从索引的角度来进一步优化表的查询 。考虑到现在的4列 (日期,应用池,环境,客户端IP),其中日期的 基数(cardinality) 不高,因为一张表里存放的都是某一个小时的记录;环境变量也只有prod,sandbox等几个,显然大部分记录都集中在prod里,因此也不适合索引;客户端IP就更别说了,基数最高,索引没什么意义。
因此只有应用池这一列了,其索引基数大致几千,上百万条记录分布在这几千个应用池里,比较适合做索引 。在 对应用池这一列做了索引后, 50个 线程同时使用getMachines API,其平均响应时间从原先的 24秒 下降到现在的 1秒 左右。
以上3个方面的优化都完成后,在生产环境实施该 项目 时就再也没出过MySQL方面的问题了。
2.频繁full GC
在生产环境做双写的时候,我们发现了一个现象:某些CAL进程运行了大概 30分钟 后,整个进程就没有任何响应了,也不写日志了。翻阅 GC (Garbage Collection, 垃圾回收)日志,发现它在运行 20分钟 后,就开始 频繁做full GC ,不久之后每次GC都释放不掉任何内存了,总是 10G->10G 。
经过数次失败后,终于在进程开始full GC时,成功地完成了一次 heapdump 。通过 MAT (Memory Analyzer Tool)工具分析,发现里面有超过 24K 个 ScheduledFutureTask对象 (如图5所示),每个持有了大概 400K 堆内存,总计保留了将近 10G 的堆内存。
这个 ScheduledFutureTask 里包含了一个 rotateFuture (保留了一些堆外内存的引用),主要是负责定期地将缓存里保留的数据,刷到后端的Filer或者Ceph里。 在CAL认为Ceph不可用的情况下,这部分rotateFuture没有能够进入正常的释放流程,导致越堆越多,最终用尽了10G堆内存的限制。
图5
发现了问题后解决方案并不复杂,添加在Ceph不可用情况下的rotateFuture释放即可,之后的模拟测试和最终上线后便再也没碰到过这个问题了。
3.存储端同时读写同一文件时性能急剧下降
当只往Ceph写,但是不从Ceph里读数据的时候,Ceph端显示的 Write IOPS (每秒进行读写操作的次数)只有大约 40K 左右。但是,当从Ceph端开始大规模读数据的时候,其 Write IOPS 突然飙升到了 170K 左右,然后整个集群的延迟开始显著增加。
对于 CAL 端 来说,我们观测到的现象是:刷数据到Ceph的 速度明显下降 ,经常出现写的速度跟不上从客户端读数据的速度, 导致本地缓冲数据的队列迅速堆满,进而开始丢数据。
和Ceph团队一起研究过后,发现Ceph为了性能考虑,在真正刷数据到持久化层之前,做了一层缓存。Ceph会根据一定逻辑将收到的若干条数据做批量写。 但是,这有个前提,即缓存里的数据没有别人需要消费。 当CAL从Ceph端读取数据之后,每个文件都被多个客户端同时打开,同时进行读和写的操作,此时Ceph所做的缓存就失效了,IOPS也就回归到了原本的真实水平。
针对这一现状,CAL 团队的应对方法是自己实现批量写,来减少刷数据的次数。原来是将一个个包含多条日志的数据块 直接压缩 ,然后写到后端存储去。现在是将多个数据块压缩好后, 合并成一个大的数据块 ,再写到后端存储去,相比之前,现在的方法进一步减少了刷数据的次数,降低了Write IOPS。Ceph端的监控显示:Write IOPS从 170k 降到了 30k 左右,整体集群工作状态良好。
四、总结
虽然初衷只是替换掉日处理超过 3PB 数据的CAL系统所使用的后端存储,但是我们没有满足于此。 通过一系列的设计优化,不仅成功地将数据迁移到了Ceph平台上,更借此机会优化了CAL系统的日志存储结构,获得了更好的读写性能,还降低了成本。
截止2019年5月底,eBay已将一整个数据中心的CAL日志搬迁到了Ceph上,相比以前的Filer,共节省了大约 50万 美金 ,未来随着更多数据中心的日志迁移,相信还会节省更多。

↓点击 阅读原文 ,eBay大量优质职位虚席以待。
我们的身边,还缺一个你!