CAL PB 级的日志存储迁移到 Ceph 的实践

阅读数:5871 2019 年 7 月 29 日 08:00

CAL PB级的日志存储迁移到Ceph的实践

一、背景

CAL(Central Application Logging) 系统主要负责收集和处理 eBay 内部各个应用池的日志,日处理超过 3PB 的数据,提供包含原始日志、聚合报告、正则表达式搜索等在内的多种形式的结果供SRE(Site Reliability Engineer)和PD们(Product Developer,产品开发设计师)日常监控使用。从 21 世纪初上线至今,已经有10 多年的历史了。

早期后台存储日志使用的是NetApp公司提供的Filer,作为NFS(Network File System,网络文件系统) 的解决方案,所有 CAL 相关的模块都需要从 Filer 上读写数据。

CAL 使用的 Filer,是由图 1 所示的多个小的物理卷组成的,它有 2 个主要的问题

  1. 一旦需要增加一个新的卷,或者移除一个老的卷,都需要所有 CAL 组件做一次代码发布,效率很低
  2. 需要在写日志的时候选择最空闲的卷,来保证每个小卷都尽可能地使用均匀,但没有哪个算法能保证绝对均匀,空间使用效率不是很好

CAL PB级的日志存储迁移到Ceph的实践

图 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)) 都能通过只遍历目录来得到结果,不需要任何其他的依赖。

CAL PB级的日志存储迁移到Ceph的实践

图 2(点击可观看大图)

可惜随着日志容量的与日俱增,这种把文件系统当数据库来使用的用例,已经让文件系统苦不堪言。随之带来的影响就是,文件系统越来越频繁地出现读写文件十分缓慢的现象,极大地影响了用户体验。即使把Filer换成Ceph,这个问题也亟待解决。

考虑到 Ceph 后端MDS(Metadata Server,元数据服务器)集群的可扩展性,我们将目录结构简化成下面的格式,如图 3 所示,深度为 5(其中第三层为 0-1023 的数字,主要是为了方便分散文件到 MDS 集群的不同服务器上去);将大量原本占据目录名存放的信息,转移到了文件名上

CAL PB级的日志存储迁移到Ceph的实践

图 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 所示:

CAL PB级的日志存储迁移到Ceph的实践

图 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) 工具分析,发现里面有超过24KScheduledFutureTask 对象(如图 5 所示),每个持有了大概400K堆内存,总计保留了将近10G的堆内存。

这个ScheduledFutureTask里包含了一个rotateFuture(保留了一些堆外内存的引用),主要是负责定期地将缓存里保留的数据,刷到后端的 Filer 或者 Ceph 里。在 CAL 认为 Ceph 不可用的情况下,这部分 rotateFuture 没有能够进入正常的释放流程,导致越堆越多,最终用尽了 10G 堆内存的限制

CAL PB级的日志存储迁移到Ceph的实践

图 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 技术荟(ID:eBayTechRecruiting)

原文链接

https://mp.weixin.qq.com/s/6WuNy1GQY-e7qsXAjI5C7A

评论

发布