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

还在用ES?事实证明,ClickHouse更强大

时间:2023-03-19 10:07:57 科技观察

在日常工作中,我们通常需要存储一些日志,比如用户请求的输入输出参数,系统运行时打印的一些信息和错误日志,以便于分析系统运行时出现的问题是有排查依据的。图片来自宝途网日志的存储和检索是一个非常普通和简单的工作,市面上有很多日志采集、存储和检索的框架。比如我们只有个位数的机器时,可以登录服务器查看Log4j等框架打印到本地文件的日志。当日志较多时,可以使用ELK三剑客来处理日志。当日志量进一步增加时,我们可以去消息队列,比如Kafka,去承接,然后消费和存储。或者写一个本地文件,然后用Filebeat之类的上报,存入仓库。以上是比较常见的日志传输和存储方案,在成本可控的情况下可以适用于大部分场景。我们可以简单概括一下日志框架的功能,大概就是暂存、传输、入库和快速检索。高成本技术方案的设计和选择往往受到成本的强烈限制。当成本高到无法承受时,就会导致技术方案的升级。那么问题来了,我只是保存一个日志,为什么成本难以承受?我们以一个常见的日志传输和存储方案为例,如下图,暂存是使用客户端写本地文件来存储日志,传输是即时的,使用MQ,消费和入库是通用的,例如ES。在下图的方案中,为了降低一些存储成本,将日志明细存储在压缩较好的Hbase中,ES中只放置了一些查询需要的索引字段。以上是常用的方案,为什么成本高呢?我们来做个简单的计算,京东App的某个模块(一个模块,不是整个App的堆积),单个用户请求,用户输入+返回值+进程打印的日志大小在40k-2M之间,而中位数约为60k。该模块每秒约2万-5万次访问,高峰时会增加10倍,高峰时可达百万。以每秒30000条计算,产生的日志大小为1.8G,也就是说即使在低负载下,日志框架也要吞掉1.8G的传输和存储。但这还远远不够,因为即使我们放弃极端峰值,只支持偶尔的峰值,系统也需要能够支持每秒15G以上的吞吐量。但这只是一个模块,还有很多其他模块,包括前台和中台。然后我们可以算一下,每秒1.5G就是每小时5.4TB。小峰一定要支持,也就是要保证秒级30万,那我们的系统要能支持秒级15G的单模。算上所有模块,二级200G跑不了。这只是每台机器在本地打印出原始日志文件,然后发送到MQ集群所占用的大小。众所周知,MQ也是写磁盘的,200G一点点存到MQ机器上,而且MQ也有备份机制。以最简单的单机备份为例,MQ每秒需要承担400G的磁盘,删除后磁盘释放还有很长的时间。哪怕只是储存1个小时,也是一个巨大的数字。我们知道,在一般服务器的磁盘还不错的情况下,单机秒级写入量超过200M就已经算是比较好的情况了。基于以上理解,我们只需要2000多台服务器来临时存储和传输日志资源。然后是worker消费集群,它纯粹是为了内存数据交换,不占用磁盘。worker消费后写入数据库。你基本上可以想象数据库是如何被占用的。OK,我们终于把数据保存下来了,查询问题就成了另一个必须面对的事情。如何从数十亿中快速找到你要查询的用户的链接日志。至此,成本就成了一个很严重的问题,尤其是方案的设计会导致本就庞大的数据,在链路上会被放大数倍,那么庞大的硬件成本如何解决。缩短流程,减少流量通过以上分析,我们发现即使是市面上最常见的日志方案,面对如此庞大的流量也难以为继。高昂的硬件成本将迫使我们寻找更合适的技术方案。世界上有一条著名的定律叫“奥卡姆剃刀定律”,讲的是程序员应该如何选择合适的剃须刀,才能让头发光滑、柔顺、有光泽。其实不然,规律主要是“非必要不增实体”这八个字。当一个流程难以支撑当前业务时,我们应该检查哪些步骤是不必要的。从这个大概的过程中,我们不难发现,我们经历了很多次读写,而且每次读写都伴随着磁盘读写(包括MQ也是写入磁盘),以及频繁的序列化和反序列化。并加倍网络IO。那么我们来挥动奥卡姆剃刀,进行一些裁剪,把不需要的部分删掉,就变成了下图所示的流程:内存,然后设置一个线程,定时通过UDP将日志直接发送到worker端。worker收到后解析,写入自己的内存队列,同时启动多个异步线程,将队列数据批量写入ClickHouse数据库。大家可能已经看到了,在下面的流程图中,圆圈明显比上图中的圆圈要小。这是为什么?因为我做了压缩。前面说了,我们的单条消息是40k-2M,是一个非常大的消息,里面包含了一些用户请求的输入Json和输出Json,还有一些中途日志。出站传输不需要我们保持原文完整。使用主流的snappy、zstd等压缩工具,可以直接将字符串压缩成byte[],然后对外传输。压缩后的字符串在存储之前都是byte[],整个过程都没有解压大报文。那么这个压缩到底能压缩多少,80%-90%,对于60k的消息,发送出去还剩下6-8k。可以想象,仅仅对原始数据进行压缩,整个过程就节省了一笔巨大的开支。带宽,也大大提高了工人的吞吐量。这是一个小细节。最大的单个UDP数据包为64kb。如果我们压缩后超过64kb,UDP就发不出去了。您可以在这里选择向工作人员发送http请求。从上图我们可以看到,当流程中有些环节不是必须的时候,我们应该果断的砍掉,不要只是照搬网上的方案,而是选择更适合自己的方案。让我们详细谈谈系统是如何设计和运行的。一个更强大的日志采集系统下面用一个很短的链接来看一下这个日志采集系统:配置中心:用来存放worker的IP地址,供client获取自己模块分配的worker集群的ip.client:客户端启动后,从配置中心拉取分配给自己模块的worker集群的IP,对收集到的日志进行轮询压缩后通过UDP方式发送过来。Worker:每个模块会分配不同数量的worker机器,启动后将自己的IP地址上报给配置中心。收到客户端的日志后,解析相应字段,批量写入clickhouse数据库。clickhouse:强大的数据库,压缩比高,写入性能强,按天分片,查询速度好。非常适合写入量大,查询量少的日志系统。Dashboard:可视化界面,从clickhouse查询数据并展示给用户,具有多条件多维度查询功能。大家可以看到,这里面最关键的就是worker端。它的流量接受、消费性能、存储性能将决定整个链路能否正常运行。我们主要分别讲解client端和worker端的实现。在客户端聚合日志请求中,我们通常要保留的日志信息主要包括:筛选。在客户端的sdk中定义了一个filter,接入方可以通过配置filter来收集输入输出参数使其生效。如果是其他rpc应用的非http请求,也提供相应的filter拦截器获取输入输出参数。sdk获取输入输出参数后,对大包进行压缩,主要是输出参数,将整个数据定义为Java对象,用protobuf序列化,通过UDP轮询传输到对应的worker集群。②链接打印的一些关键信息,如调用其他系统的进出参数,自己打印的一些信息和错误信息。sdk为三种常用的日志框架:log4j、logback和log4j2提供了自定义的appender。用户可以在自己的日志配置文件(比如logback.xml)中定义我的自定义appender,然后用户在代码中打印的所有info、error等日志都会执行这个自定义appender。同样的,这个自定义的appender也是将日志临时存放在内存中,然后通过UDP异步发送给worker。这里有两个要点需要注意。一种是当压缩后的消息仍然超过udp的最大消息值时,会通过http发送。第二,对于这个请求,链路上可能会用到多线程和线程池技术。为了防止linktracer的唯一id在线程池中丢失,sdk使用TransmittableThreadLocal来保存linkID。您可以通过检查来了解这一点。总体来说,客户端的实现比较简单,省略了写入本地磁盘、消费文件、发送MQ等步骤,整体只有一次Protobuf序列化操作,对CPU性能影响极小和接入端。使用UDP发送,不需要worker的回复,不需要考虑tcp方式worker消耗慢导致自阻塞的问题。整体非常简洁高效。Worker端消费日志合并入库。worker端是调优的重点。由于需要从客户端接收大量的日志,解析并入库,因此worker需要有很强的缓冲能力。我们都可以看出,系统的瓶颈点一定是在存储阶段。解析日志和提取字段非常高效,可以通过控制线程数来控制,而存储会受到clickhouse写入性能的强烈限制。至于clickhouse是怎么优化的,后面clickhouse集群的负责人会讲优化。为了做好这个缓冲,即使接收到的日志量大于存储量,我们也要能够继续这些数据,尽量不丢失。首先在硬件方面,我们使用大内存的机器和8核32G的容器来存储尽可能多的数据。其次,采用双缓冲队列。首先将所有接收到的数据放在一个队列中,然后被多线程消费,解析成可以存储的行数据,然后放入一个队列存储,然后批量存储。那么我们做的这些操作可以支持什么样的数据量呢?通过在线应用和严格的压力测试,这样的单机docker容器每秒可以处理10-5000万行原始日志。比如一个用户请求,中间总共产生了1000多行日志,那么这样的一个worker机器每秒可以处理20000个客户端QPS。外部写入clickhouse数据库时,每秒可以写入160M-200M,比较稳定。通过上面的了解,我们知道这些数据是经过压缩的,直到库被压缩,只有终端用户查询的时候,才会解压。所以这200M基本相当于原始数据的1G多。也就是说,只要clickhouse的写入速度跟得上,这个系统只需要100个单元就可以极其高效地处理每秒数百G的原始日志。与写MQ的方案相比,中间所有的瓶颈点,比如MQ写磁盘速度、消费拉取速度等,都将不复存在。这是一个纯内存交换链接系统。强大的Clickhouse通过上面的理解,我们可以清楚的看出worker是一个纯内存计算组件,client端通过worker的数量将hash平均分配给每个worker。因此worker可以动态扩展,不存在性能瓶颈。唯一的限制是写入库的速度。如果写库的速度跟不上,worker就必须用有限的内存来存储大量发送的数据,一旦满了就会开始丢弃接收到的数据。所以整个系统的瓶颈点就是写库的速度。Clickhouse是面向海量数据实时、多维分析、高性能的新一代OLAP数据库管理系统。它实现了向量化执行和SIMD指令。对于内存中的列式数据,一个batch调用一次SIMD指令,大大减少了计算时间,导致多项性能改进。目前,它已成为驱动京东集团业务增长和创新的“超级引擎”。那么,在京东App的秒级100G日志传输存储架构中,Clickhouse是如何支持大吞吐数据的写入呢?主要在于以下两点:①集群高可用架构EasyOLAP部署CH集群是三层结构:域名+CHProxy+CH节点,域名将请求转发给CHProxy,然后CHProxy根据集群节点的状态。CHProxy的引入是为了让Query均匀分布在各个节点上,对CHProxy进行了一些改进,可以自动感知集群节点的状态变化。②当本地表向分布式表写入高吞吐数据时,分布式表会将接收到的插入数据拆分成多个部分,通过rand或shard_key转发给各个shard。这样会增加ch节点的网络带宽和merge的工作量,导致写入性能下降,同时会增加分块过多的风险,增加zookeeper的压力。另外,数据会暂时放在分布式表所在节点的磁盘上,然后异步发送到本地表所在节点进行物理存储。中间没有一致性检查。如果分布式表所在的节点出现故障,就会出现数据丢失。风险。因此,针对以上风险,可以将高吞吐量的数据直接写入本地表,与写入分布式表相比,可以大幅提升写入性能2-3倍。多条件查询控制台控制台比较简单,主要是做一些sql语句查询,在clickhouse中做好高效查询。下面是一些知识点。做好数据分片,比如按天分片。利用好prewhere查询功能可以提高性能。做好索引字段的设计,比如检索常用的time、pin等。细节很难描述。要对百亿级数据进行极速查询,还需要了解clickhouse的一些查询特性。下图所示的界面显示了一些索引项。点击查看详情,会从数据库中取出压缩后的数据,解压后展示给前端。查看链接是本次请求中整个链接用户(包括线程池中)打印的log。总结与比较我们可以简单的做一些比较,主要是硬件成本和软件性能的比较。由上可知,磁盘占用原方案占用磁盘(1份)、MQ(2份)、数据库(1份)。在新方案中,磁盘上只剩下clickhouse(0.8份),clickhouse本身对数据进行了压缩,所以实际占用的空间不到存储容量的80%。然后,仅磁盘一项的存储成本就可以节省75%以上。大家都知道秒级吞吐量是伴随着服务器CPU消耗的。并不是说一台服务器只用一块大硬盘就可以每秒处理1G。每台服务器的秒级吞吐量都有上限。磁盘使用率的秒级增长对应于CPU数量的增长。要支持每秒1G的磁盘写入,需要5台或更多的服务器。那么在磁盘的极大节省下,线性节省了大量的中间进程CPU服务器。根据实际粗略的统计效果,过程中服务器可以节省70%以上。在软件性能方面,这个过程很好理解。客户端的消费主要是序列化、写磁盘、读磁盘、反序列化的消费。Udp只有一步序列化。我们假设MQ集群具有无限写入能力,可以吞噬过去发送的所有日志,那么就是worker端的消费性能对比。从MQ拉取和消费。如果这个过程中MQ中没有积压,就会有零拷贝支持高速拉取。如果出现积压,可能会产生大量的MQ磁盘IO,拉取速度会明显下降。这个过程的效率会明显低于Udp发给worker的处理效率,而且会占用两倍的网络带宽。在实际性能上,worker的强大性能比之前单拉MQ集群提升了10倍以上。本文到此结束。主要简单介绍了一种新的日志采集系统的设计方案,以及该方案能够带来的巨大成本节约。作者:吴伟峰、李洋编辑:陶家龙来源:转载自公众号京东零售科技(ID:jd-sys)