当前位置: 首页 > 科技观察

日志系统成本暴涨上千万,吓得赶紧把ES换成ClickHouse..._1

时间:2023-03-19 17:03:13 科技观察

1.后台唯品会日志系统dragonfly1.0基于EFK。从2014年开始服役7年,支持物理机。日志采集、容器日志采集、特殊类日志综合采集等,极大方便了全公司日志的存储和查询。随着公司业务的发展,日志应用场景逐渐遇到了一些瓶颈,主要表现在应用数量和打印日志数量的增加上。开发需要打印更多日志,定位业务问题,进行运营数据分析;此外,外部攻击问题和审计要求需要上报更多安全相关的日志数据,并保存半年以上,以应对潜在的攻击,并在攻击发生时调查原因和影响范围。ELK的架构缺点很明显。ES集群规模为260台机器,所需硬件和维护成本高达千万级。如果通过扩容满足上述业务场景,会导致ES集群过大,不稳定。创建一个独立的集群也会需要更高的成本,这两者都会极大地增加成本和维护工作量。针对这些问题,去年6月我们开始探索一种新的日志系统架构,彻底解决上述问题。二、日志系统的演进1、标准的日志格式标准的日志格式有利于正确识别日志的关键元信息,满足查询、告警和聚合计算的需要。从以上格式的日志来看,filebeat转换后的结果如下:时间戳、日志级别、线程名、类名、eventName,自定义字段会被日志采集Agent解析,其他元数据如域名、容器名称或主机名以JSON格式一起报告。自定义字段由开发者根据业务需要打印到日志中。主要支持的功能有:查询时支持各种聚合分析场景。根据自定义字段进行聚合功能告警。2.ES存储解决问题1)ES日志存储模型EFK日志存储在elasticsearch中,每个domain的日志在ES中以天为粒度建立索引。索引大小是根据前几天的数据大小计算的。每个索引分片大小不超过30G,日志量越大的域分片越多。如果一个域的日志量过大或过长,会占用ES节点大量的CPU进行解析和段合并,影响其他域日志的正常写入,导致整体下降写入吞吐量。通常很难排查哪个域的哪个分片日志过大,面对这样的热点问题往往需要很长时间。我们的ES版本使用的是5.5,目前还不支持索引自动删除和冷热迁移。每天定时执行几个脚本,完成删除索引、关闭索引、移动冷索引、创建新索引等任务,其中移动索引和创建新索引是非常耗时的操作。整个生命周期每天循环执行。如果哪天某个步骤突然执行失败,或者执行时间过长,整个生命周期就会被拉长,甚至无法完成。第二天新数据的写入会受到严重影响甚至无法写入。另外,ES的倒排索引需要对日志进行分段,生成的索引文件较大,占用磁盘空间大。不过,ES也有它的优势。基于倒排索引的特点,查询ES时,一个分片只需要一个核就可以完成查询,因为通常查询速度更快,QPS更高。以下是ES在大规模(或海量)日志存储场景下的主要存储优缺点:3、日志系统2.0方案1)选择clickhouse的原因2019年,我们尝试了另一种HDFS存储方案。数据按照域名+toYYDDMMHH(timestamp)+host为key缓存在客户端。当大小或过期时间到了,提交给HDFS生成一个独立的文件。存储路径包括域、主机和时间信息,可以根据这些标签进行搜索过滤,这种存储方式有点类似于loki,它的缺点很明显,优点是吞吐量和压缩率非常高,可以解决我们吞吐量和压缩率不够的问题。如果在这个方案的基础上继续增强功能,比如添加标签、简单跳索引、查询功能、多节点并发查询、多字段存储等,开发的工作量和难度会非常大。我们对比了一些业界领先的存储方案,最终选择了clickhouse。其批量写入和列式存储方案完全满足我们的要求(基于HDFS存储)。此外,它还提供了主键索引和非常小的磁盘空间。与ES的全文索引相比,跳转计数索引具有明显的优势。将近26G的应用日志使用clickhouse的lz4、zstd和ES的lz4压缩算法进行对比:在实际生产环境中,zstd的日志压缩比更高,这与应用日志的相似度有关,最高可达15.8。Clickhouse有这么高的压缩率,但是没有索引,它的查询速度是多少?虽然没有索引,但是它的向量执行和SIMD结合多核CPU可以大大缓解没有全文索引的劣势。经过多次测试对比,其查询速度在绝大部分场景下与ES不相上下,在某些场景下甚至比ES更快。下图展示了实际生产环境中上千个应用的真实运行数据,以及查询24小时时间段内的日志和24小时时间段内的日志的耗时对比。通过对日志应用场景的分析,我们发现对于万亿级别的日志来说,可以查询的日志数量是非常非常少的,也就是说ES对所有日志的分词索引大部分都是失效的。日志越多,这个分词消耗的资源越多,越浪费。与clickhouse的MergeTree引擎相比,主要的资源消耗是日志排序、压缩和存储。另外,Clickhouse的MPP架构使得集群非常稳定,几乎不需要过多的运维工作。下面用一张图全面对比一下ES和Clickhouse的优缺点,来解释一下为什么我们选择clickhouse作为下一代日志存储数据库。3、技术详解EFK架构发展了这么多年,系统也成熟了很多。ES的默认参数和倒排索引,让你在对ES了解不多的情况下也能轻松上手。开源的kibana也提供了丰富的查询接口和图形面板。对于日志量较小的场景,EFK架构仍然是首选。Clickhouse是近几年OLAP领域比较流行的数据库。其成熟度和生态仍在快速发展。用于存储日志的开源解决方案并不多。要用好它,不仅需要深入了解Clickhouse,还需要做大量的开发工作。1、日志摄取——vfilebeat最初在dragonfly中使用logstash进行日志采集,但是logstash的配置比较复杂,无法支持配置文件的分发,不方便容器环境下的日志采集。当时用GO语言开发的另一个采集工具是vfilebeat。在性能和扩展性方面,我们在此基础上定制开发了自己的日志采集组件vfilebeat。vfilebeat在主机上运行。启动时可以通过参数指定采集的主机日志所属的域。如果不指定,安装时会读取CMDB配置文件的域名和主机名,主机收集的每条日志都会包含进来。域名和主机名作为标签。在容器环境中,vfilebeat也会监控容器的创建和销毁。容器在创建时,会读取容器的POD信息获取域名和主机名,然后从ETCD中拉取域的日志采集路径等配置参数,根据域名和POD名生成容器所属目录的日志文件采集路径,并在本地生成新的配置文件,vfilebeat重新加载配置文件开始滚动采集。现在我们环境中的大部分应用都是使用vfilebeat来采集,小部分场景保留使用logstash来采集。vfilebeat将采集到的日志附加应用、系统环境等标签,将配置的数据格式序列化,上报给kafka集群。应用程序日志是JSON,Accesslog是文本行。2.日志解析——flinkwriter收集的Kafka日志会被一个flinkwriter任务消费,然后写入clickhouse集群。writer首先将Kafka消费的数据转化为结构化数据。vfilebeat在上报的时候,可能会上报一些日期比较长的数据。太长的数据意义不大,会导致小部分较多,消耗clickhosuecpu资源。在这一步中,超过三天的日期将被丢弃,无法解析的数据或缺少必填字段的日志也会被丢弃。解析过滤后,数据通过转换步骤转化为表字段和clickhouse类型。转换操作从架构和元数据表中读取存储在域日志中的元数据。schema定义了clickhouse本地和全局的表名、字段信息,以及日志字段和表字段的默认映射关系。元数据定义了域日志具体使用的模式信息、日志存储的持续时间、域分区字段值以及域自定义字段映射到的表字段。通过这些域级别的配置信息,我们可以指定域存储表,将存储持续时间的超大日志域存储在独立的分区中,减少日志合并的CPU消耗。默认情况下,自定义字段存储在数组中。一些字段打印更多自定义日志字段。在日志量大的情况下,速度较慢。配置自定义映射物理字段存储可以提供比数组更快的查询速度和压缩比。1)Clickhousetableschema信息2)域自定义存储元数据信息转换后的数据携带了所有需要存储在CK表中的信息,会暂存到一个本地队列中。本地队列可能存储多个域中多个表的日志,在达到指定长度或时间后,提交到一个进程级的全局队列。因为writer进程是多线程的,消费了多个Kafka分区,全局队列将同一张表的多个线程的数据合并在一起,使得一次提交的batch更大,全局线程短暂缓冲。或者超时后,数据将作为写入提交到提交工作线程。submitworker负责数据写入、高可用、负载均衡、容错和重试逻辑。submit收到提交的batch数据后,随机搜索一个可用的clickhosueshard,提交并写入shard节点。clickhouse集群配置是双副本。当一个复制节点发生故障时,它会尝试切换并写入另一个节点。如果两者都失败,则分片将被暂时移除并重新写入一个健康的分片。向Clickhouse写入数据,我们使用的是clickhouse-jdbc,一开始会消耗大量的内存和CPU。分析jdbc源码后,我们发现jdbc在写数据的时候,会先把所有的数据转换成一个List对象。这个列表对象相当于提交数据的byte[]复制格式。为了减少这个占用,我们优化了数据转换步骤。将每条日志数据直接转化为jdbc可以直接使用的List数据,这样jdbc在构造生成SQL时,得到的数据实际上是对List的引用,这样的优化减少了三分之一左右的内存消耗。另外,在分析writer进程的火焰图时,我们发现jdbc在生成SQL的时候,会对提交数据的每个字符进行判断,识别特殊字符,比如'\','\n','\b',etc.意思是,这个escape操作使用了map函数,在数据量大的时候会消耗CPU17%左右。我们对此进行了优化。使用swtich后,内存大大减少,节省了13%的CPU消耗。clickhouse的弱集群理念保证了当单个节点宕机时,整个集群几乎不受影响。submit的高可用保证了当一个节点出现异常时,数据依然可以正常的写入到健康的节点中,这样整个日志的写入就非常的稳定,几乎不会出现节点宕机带来的延迟。关于Clickhouse的日志摄取方式,Shimo开源了另一种摄取方式,创建KafkaEngine表直接消费Clickhouse,然后将数据导入物化视图,最后通过物化视图将数据导入本地表。这种方式的优点是保存了一个writer组件,上报给Kafka的数据可以直接存储在clickhouse中,但是也有很多缺点:每个topic需要创建一个独立的KafkaEngine。如果您需要切换表格或添加主题,则必须更改它们。DDL,不能支持一个主题的不同域存储在不同的表中。另外,解析Kafka数据和物化视图会消耗节点CPU资源,而clickhouse的合并和查询操作对CPU资源的依赖性很强,会增加clickhouse的负载,从而限制clickhosue的整体吞吐量,影响查询性能。为了缓解这个问题,ClickHouse的单台服务器需要更多的核、SSD和大磁盘存储,因此扩容成本非常高。我选择将解析和编写组件分开,可以解决上面提到的很多问题,也为后面的很多扩展功能提供了很大的灵活性。好处很多,就不一一列举了。3.存储-Clickhouse1)提交给Clickhouse的高吞吐量写入数据以二维表的形式存储。对于二维表,我们使用Clickhouse中最常用的MergeTree引擎。关于MergeTree更详细的介绍,可以参考网上的这篇文章《MergeTree的存储结构》。https://developer.aliyun.com/article/761931spm=a2c6h.12873639.0.0.2ab34011q7pMZK数据在磁盘上的逻辑存储示意图MergeTree采用类似LSM-Tree的数据结构进行存储。每次提交的批量数据,根据表的partitionkey,分别保存在不同part目录下。part中的行数据按照sortkey排序后,按列压缩存储在不同的文件中。Clickhouse后台任务会不断合并这些smallpart,生成更多的Bigpart。MergeTree虽然没有ES的倒排索引,但是有更轻量级的分区键、主键索引、跳数索引。partitionkey可以保证在查找的时候快速过滤掉很多部分,比如按照时间查找的时候,只命中时间范围内的部分。主键索引不同于关系型数据库的主键,是一种轻量级的索引,用于快速查找已排序的数据块。跳数索引根据索引类型对字段值进行索引。比如minmax索引指定字段的最大最小值,set存储字段的唯一值用于索引,tokenbf_v1拆分字段,创建bloomfilter索引,查询可以直接计算日志是否在根据关键字对应的数据块。一部分的数据会按照sortkey排序,然后按照size分成更小的块(index_granularity)。block默认有8192行,主键索引对每个block的边界进行索引,hop索引根据索引的字段生成索引文件。通常,这三者生成的索引文件都很小,可以缓存在内存中,以加快查询速度。了解了MergeTree的实现原理后,我们可以发现,影响ClickHouse编写的一个关键因素是部件的数量。每次写入都会生成一个part。零件越多,后台合并任务就会越忙。除了这个因素之外,部分生成和合并都会消耗CPU和磁盘IO。所以总结一下,影响写入的因素有3个:部件数量-CPU核数少-磁盘IO多-高。要提高写吞吐量,需要从这三个因素入手,减少部件数量,增加CPU核心数,提高磁盘I/O。按照实现方式对图中的方法进行分类:硬件的CPU核数越多越好。我们的生产环境是40+,磁盘SSD是标配。由于SSD价格昂贵,容量小,所以采用SSD+HDD的冷热分离模式,表结构较长。日志量大的域使用bloomfilter索引加速查询,其他域可以使用普通的跳数索引。我们测试观察可以节省近一半的CPU。向Writer提交的数据写入数据,根据partitionkey分批提交,也可以使用部分partition字段,即单次提交的partitionkey的基数越小越好,理想情况下1、这种方法可以大大减少小零件的数量。在partitionkey的选择上,对于日志量大的domain,可以根据应用日志的数量选择独立的partitionkey。日志量大的应用通常会达到提交条目数的阈值,这可以使合并的部分都更大更高效;或混合分区键,将小型应用程序混合在一个分区中并提交。2)多次高速查询。我跟别人解释为什么没有(全文索引)日志系统还这么快的时候,直接把这张图扔了。图片来自商业产品Humio公司网站,也是我们老大推荐给我们学习参考的一款产品,2021年初被CrowdStrike以4亿美元收购,1PB的数据存储,没有全-文本索引和直接暴力检索关键字必须超时。如果使用time、tags、bloomfilter进行筛选过滤,然后再进行暴力搜索,则检索到的数据量会很小。许多。MergeTree引擎是一种高压缩率的列式存储。高压缩率有很多优点。从磁盘读取的数据量小,页缓存需要的内存少,高速内存可以缓存更多的文件。Clickhouse有和Humio一样的向量化执行和SIMD,在查询的时候,内存中的这些压缩数据块会被CPU分批执行SIMD指令,因为块足够小,压缩前一般1M,所以函数向量的数据execution和SIMD计算就够了放到cpucache中不仅减少了函数调用次数,而且大大降低了cpucache的未命中率。查询速度比没有向量执行和SIMD的查询速度快几倍。4.应用维度日志TTL一开始我们打算使用表级TTL来管理日志,将不同存储时长的日志放到不同的表中,但是这样会导致表和物化视图数量众多,不方便管理.后来,我们采用了一种改进方案,将TTL放在表分区字段中,开发了一个简单的定时任务,每天扫描删除所有超过TTL日期的部分。这样一张表就支持不同TTL的日志存储,灵活性非常高。应用您可以通过界面轻松查看和调整存储时长。5、自定义字段存储方案标准格式日志中的自定义字段名由业务输出,基数不确定。我们第一版的方案是创建上百个字符串、整数、浮点数的扩展字段,自己开发配置这个自定义映射,后来发现这个方案有严重的缺陷:开发需要手动配置每个字段日志到映射。随着日志的变化,这样的字段会越来越多,随着数量的增加,维护起来会很困难。,Clickhouse需要创建大量的列来保存这些字段。由于所有的应用都是混合在一起存储的,对于大多数应用来说,过多的列不仅浪费,而且会降低存储速度,占用大量的文件系统INODE节点。后来借鉴了Uberlogs的存储方案,为每个数据类型字段创建两个数组,一个保存字段名,一个保存字段值。名称和值在顺序上是一一对应的。查询时,使用clickhouse的数组检索功能,检索字段。此用法支持所有Clickhouse函数计算。[type]_names和[type]_values分别存储对应数据类型字段的名称和值。1)插入的多层嵌套的json字段会被压平存储,例如{"json":{"name":"tom"}}会被转化为json_name="tom"字段。不再支持数组存储,数组字段值会转为字符串存储,例如:{"json":[{"name":"tom","age":18}]},转为json=“[{\"姓名\":\"汤姆\",\"年龄\":18}]"。2)查询原始映射自定义字段。目前仍有10个保留。如果不够,可以随时添加。可以支持一些域的固定自定义字段,或者一些特殊类型的日志,比如审计日志,系统日志等,这些字段在查询的时候,用户可以使用原来的名字,会被替换成表字段名在访问Clickhouse之前。自定义字段的另一种选择是将它们存储在地图中,这样可以节省两个字段并使查询更容易。但是,经过我们的测试,查询性能不如数组:数组的存储压缩率略好于Map。数组查询速度比Map快1.7倍以上。Map的查询语法比数组简单,前端简化数组查询语法的时候可以忽略这个优势。4、前端日志查询系统第一版日志系统是基于kibana开发的,版本比较老。在2.0系统中,我们直接抛弃了旧版本,开发了查询系统。效果如下:新版查询会自动解析用户输入的查询语句,增加查询的应用域名和时间范围等,降低用户操作难度,支持多租户隔离.自定义字段的查询非常繁琐,我们也做了简化操作:string_values[indexOf(string_names,'name')]简化为:str.namenumber_values[indexOf(number_names,'height')]简化为:num。heightClickhouse一次执行一条语句。查询日志时,直方图和TOP示例日志是两条语句,会加倍查询时间范围。参考携程的优化方法。在查询详情时,我们会根据直方图的结果,将时间范围缩小到TOP记录所在的时间区间。丰富的查询用法Clickhouse丰富的查询语法使得我们新的日志系统的查询分析功能非常强大。从海量日志中提取关键词非常容易。下面是两种查询用法:从混合文本和JSON的日志数据中提取JSON字段从日志中计算分位数5.正确使用姿势。打印的日志不要太长,不要超过10K。查询条件标有跳数索引,或其他非日志详细信息字段。召回日志数量越少,查询速度越快。OLAP数据库Clickhouse是处理大规模数据密集型场景的强大工具。非常适合海量日志存储和查询分析。构建了低成本、无单点、高吞吐、高速查询的下一代日志系统。