前文分别介绍了滴滴自研的ES强一致性多活是如何实现的、以及如何提升ES的性能潜力。由于滴滴ES日志场景每天写入量在5PB-10PB量级,写入压力和业务成本压力大,为了提升ES的写入性能,我们让ES支持ZSTD压缩算法,本篇文章详细展开滴滴在落地ZSTD压缩算法上的思考和实践。
// 背 景 //
ES通过索引(Index)对外提供数据检索能力,索引是用于组织和存储数据的逻辑单元。每个索引由若干个分片(shard)组成,每个分片就是一个Lucene索引,可以在不同的节点上进行分布式存储和并行处理,提高性能和可伸缩性。每个分片由一组段文件(segment)组成,段是分片中更小的存储和搜索单元,是一组物理文件,包含了检索需要的倒排索引(词项和文档ID的映射关系)和文档存储(字段值和其他元数据),如下图:
ES数据模型
Lucene作为ES的底层索引引擎,提供了灵活的数据检索能力,同时也导致CPU、存储占用较为严重。为实现降本增效,23年上半年,ES团队开启了Lucene压缩编码优化专项,通过改进存储层压缩算法,从而降低单位Document所占用的资源。本文概述了ES的底层索引文件,并介绍了Lucene存储压缩编码的优化。
// Lucene索引文件介绍 //
ES的压缩编码优化专项涉及到Lucene底层的文件存储,Lucene索引由一组Segment构成,每个Segment包含了一系列文件,重点文件类型如下图:
行存文件:包括原文存储文件和原文索引文件。原文存储文件,即.fdt文件。用户写入的原始数据都被存储于该文件中,因其占比大,为节约存储,Lucene在原文存储上支持LZ4压缩和ZIP压缩;原文索引文件,即.fdx文件,它存储了原文数据在原文存储文件中的位置信息,建立起了doc id和原文之间的联系,以支持快速访问和定位。
列存文件:即.dvd文件,常被应用于一些OLAP分析引擎中。列存文件按列组织数据,不同Document中的同一列数据(Field),相邻存放在一起,这样可以加速该列聚合分析性查询。同时,相邻每列类型相同,在存储的时候可以进行统一性的编码优化,提高压缩率,减少存储磁盘空间的占用。
索引相关文件:ES依靠分词产生倒排索引,使其具备强大的全文检索能力。索引相关文件中,重点文件包含:字典数据文件&倒排索引文件。字典数据文件,即.tim文件,通过用户配置的索引分词器,能够从用户数据中提取分词信息并存储在.tim文件中。同一列的分词信息,相邻存放,按块组织;倒排索引文件,即.doc文件,也被称为"倒排拉链表",它记录了每一个分词所关联的文档列表,能够实现快速的单词到文档的倒排查找。
// ZSTD压缩算法调研与分析 //
ES线上集群中资源比较紧张的主要是日志集群,集群写多读少,高峰期CPU使用率在85%左右,写入性能是它的主要瓶颈。通过调研可以发现原文存储文件的占比最大,基本都超过了30%,有些索引甚至超过了70%。由此,我们明确了索引文件压缩编码优化的重心。
目前滴滴ES线上采用的是7.6.0版本,对应的Lucene版本是8.4.0,该版本支持两种压缩策略:
BEST_SPEED,是ES索引默认的压缩算法,使用了LZ4压缩。压缩与解压速度快,CPU占用低,但压缩效果弱。
BEST_COMPRESSION,使用了ZIP压缩。压缩与解压速度慢,CPU占用高,但压缩效果好。
Lucene的压缩算法仅针对占比最大的行存文件生效,其他文件通过自定义编码优化来降低存储。目前滴滴ES日志集群采用BEST_COMPRESSION压缩算法,通过ES压缩比测试发现,日志场景下,同一个索引采用ZIP比LZ4低20% ~ 40%的磁盘存储占用空间。但通过分析日志集群的CPU使用情况可以发现,ES压缩模块的CPU占比较高,一些日志集群甚至超过30%,如下图:
CPU损耗占比
在上述背景下,我们调研了ZSTD压缩算法,ZSTD(Zstandard)底层基于FSE编码实现,具有出色的压缩和解压速度。ZSTD算法的实现经过了高度优化,通过SIMD等指令集能够充分利用硬件并行性,同时编码过程大量依赖位移运算来完成状态的切换,以此提高处理速度。ZSTD采用字典压缩算法,通过引用字典中的匹配项,能够大大减少重复数据的存储空间,提高压缩比。与此同时,ZSTD采用多级压缩策略,在不同的压缩级别中应用不同的压缩算法,能够在不同的应用场景中灵活地平衡速度和压缩比。
为了验证它的性能,采用bamai线上1GB的日志文件做压缩性能测试,测试发现,ZSTD的压缩速度是ZIP的4.5倍,解压缩速度是ZIP的1.5倍,压缩比几乎持平,如下图所示,ZSTD压缩算法兼顾了LZ4压缩的"快"及ZIP压缩的"效果好"。
压缩算法对比
// ZSTD压缩算法落地 //
为了实现ZSTD在滴滴ES的落地,我们从以下方面着手:
源码开发
1、ES setting和engine扩展
ES通过setting给每个索引配置压缩格式,需要在ES setting中支持ZSTD压缩格式。ES会为每个shard初始化一个engine,不同的分片类型或状态对应不同的engine,例如索引close对应的是noop engine,DCDR从索引对应的following engine,需要在不同类型的engine上抽象并扩展它的ZSTD压缩能力。
2、Lucene CompressionMode 扩展
Lucene是一个由Java编写的全文搜索引擎库,而ZSTD算法是基于C++实现的,因此在Lucene端引入了zstd-jni来扩展ZSTD压缩能力。通过扩展CompressionMode,自定义ZStandardDecompressor和ZStandardCompressor来实现数据的按块压缩、解压缩。
参数调优
1、Chunk Size调优
行存文件内部是以Chunk形式组织的,Chunk Size通常为数十KB级别。滴滴ES7.6.0版本采用的是Lucene 8.4版本, LZ4压缩算法设置的Chunk Size为16kb,而ZIP压缩算法设置的是60kb。将索引设置为ZSTD压缩格式并导入一批线上数据后,压缩结果如表所示。
Chunk Size压缩比对表
增大ChunkSize可以获得一个更大的数据区间内的共享字典数据,从而获得更好的压缩效果。但这也会导致随机访问时延变大、CPU消耗进一步增大。为保证后期索引压缩格式切换为ZSTD时不会出现数据膨胀问题,ChunkSize采用的是60kb。
2、ZSTD压缩等级调优
ZSTD采用多级压缩策略,它 提供了从 1 到 22 的压缩等级,数值越大表示压缩比越高,但压缩和解压缩速度越慢、CPU损耗越高。设置不同的压缩等级,导入测试数据,压缩结果如下表所示:
压缩等级性能比对表
通过增大压缩等级能够降低存储,例如将压缩等级调整为9,.fdt文件能够下降10%左右的存储,索引整体存储下降5%,此时CPU损耗和ZIP基本持平。
ES线上日志集群写多读少,采用的都是物理机(SSD硬盘),集群高峰期CPU使用率超过80%,集群整体磁盘水位在55%左右,CPU使用率是它的瓶颈。因此,采用的压缩等级为3,该等级在速度和压缩比之间取得了较好的平衡,并且能够尽可能地降低集群CPU使用率。
其他
1、解决Lucene打包部分依赖加载失败问题,比如:Lucene采用ivy进行依赖管理,通过引入repo解决Lucene打包过程中Maven主仓库中找不到 org.restlet.jee jar的问题,如下图:
ivy依赖导入图
2、通过前置初始化zstd模块,解决ES运行时动态加载zstd-jni-jar失败问题。
3、通过扩展noop engine的ZSTD压缩能力,解决索引close场景ZSTD类型解析失败问题。
// 上线效果 //
经过三个月的实践与优化,目前已在16个集群上线了ES-ZSTD版本,并将日志集群全量索引(6w+)以及部分公共集群索引的压缩格式均切换为ZSTD,上线后所有日志集群高峰期CPU使用率平均降幅达到15%,使ES可以提供更高性能、更低成本的检索服务,主要效果如下:
更高性能
1、某日志集群A上线效果
ES某日志集群A上线ES-ZSTD版本并将全量索引切换压缩切换为ZSTD格式后,集群高峰期CPU使用率下降18%,写入reject同比下降50%。
集群CPU Idle图(集群A)
DataNode写入reject图(集群A)
2、某超大日志索引M切换效果
ES某超大线上日志索引M压缩格式由ZIP切换为ZSTD后,写入条数不变的情况下,集群CPU使用率下降15%,写入性能提升25%。
集群CPU Idle图(集群B)
索引写入总耗时(索引M)
更低成本
1、LZ4压缩格式索引切换为ZSTD效果
ES日志集群还残留着部分LZ4压缩的日志索引,将这些日志索引切换为ZSTD压缩格式后,平均索引存储下降达到30%,如下图:
索引存储图
2、日志集群缩容
将索引压缩格式切换为ZSTD后,能够有效降低集群CPU,因此可以进行集群资源调整。目前已经缩容机器超过20台,仍在持续下线中。
// 总 结 //
ZSTD助力ES提供更高性能、更低成本的检索服务。之后也会陆续开启读写分离、ES大版本升级等项目,进一步助力业务发展。