2

京东App秒级百G日志传输存储架构设计与实战

 2 years ago
source link: https://my.oschina.net/1Gk2fdm43/blog/5312538
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文作者:京东零售平台业务研发部-武伟峰,数据与智能部-李阳

在日常工作中,我们通常需要存储一些日志,譬如用户请求的出入参、系统运行时打印的一些info、error之类的日志,从而对系统在运行时出现的问题有排查的依据。

日志存储和检索是个很常见且简单的工作,市面也有很多关于日志搜集、存储、检索的框架可供使用。

譬如我们只有个位数机器时,可以通过登录服务器,查看log4j之类的框架打印到本地文件的日志。当日志多起来后,可以用elk三剑客处理日志。

当日志量进一步增多,我们可以上消息队列,譬如kafka之类来承接,然后消费入库。或者写本地文件,再采用filebeat之类上报再入库。

以上都是较为常见的日志传输和存储的方案,成本可控的情况下,可适用于绝大多数场景。

我们可以简单总结一下日志框架的功能,大概是暂存、传输、入库保存、快速检索。

量级上升,成本高昂

技术方案的设计和取舍,往往强受限于成本。当成本高企到难以承受时,将必须导致技术方案的升级换代。那么问题来了,我就是存个日志而已,怎么就成本难以承受了呢?

我们以一个常见的日志传输及存储方案来举例,入下图,暂存就是采用客户端写本地文件存日志,传输即是采用MQ,消费入库常见的如ES。下图方案,为了减少部分存储成本,将日志详情存储于压缩更好的Hbase,仅将查询时需要的一些索引字段放在了ES。

up-00275b840de750e4a7af20db10781cd3bac.png

以上作为一个常用的方案,为什么会成本高昂呢。

我们来简单计算一下,京东App某个模块(是一个模块,非整个App累计),单次用户请求,用户的入参+返回值+流程中打印的日志占用的大小在40k-2M之间,中位数在60k左右。该模块日常每秒约2-5万次访问,高峰时会翻10倍,极高峰可达百万。

以3万每秒来算,产生的日志大小为1.8G,也就是说即便是低负载时,这个日志框架要吞下1.8G的传输与存储。但这是远远不够的,因为我们即便放弃极高峰,仅仅支撑偶现的高峰,也需要该系统能支撑秒级15G以上的吞吐。但是这仅仅才是一个模块而已,算上前中台这样的模块还有很多。

那么我们就可以来算一算了,一秒1.5G,一个小时就是5.4TB。小高峰是肯定要支撑的,也就是秒级30万是要保的,那么我们的系统就要能支撑秒级15G单模块,算上各模块,200G秒级是跑不了了。

这只是各个机器所打印在各自本地的原始日志文件占用的大小,然后要发到MQ集群,大家都知道,MQ也是写磁盘的,这200G一点不少的在MQ机器上做了保存,并且MQ还有备份机制,就以最简陋的单备份来说,MQ每秒要承接400G的磁盘,并且离删除后释放磁盘还有挺长一段时间,哪怕只存1个小时,也是一个巨大的数字。

我们知道,一般服务器在磁盘还不错的情况下,单机秒级写入量200多M算比较不错的情况,通过上面的了解,我们仅仅做到日志的暂存和传输就需要2千台以上的服务器资源。

然后就到了worker消费集群,该集群只是纯粹的内存数据交换,不占磁盘,worker消费后写入数据库。大家基本可以想象到,数据库的占用是如何。OK,我们终于把数据存了进去,查询问题就成了另外一个必须面对的事情,如何快速从无数亿中找到你要查询的那个用户的链路日志。

到了此时,成本就成了非常要命的事情,尤其方案的设计,会导致原本就很庞大的数据,在链路上再次放大多倍,那么巨大的硬件成本如何解决。

缩短流程,缩减流量

通过上面的分析,我们已经发现,即便是市面上最通用的日志方案,在如此巨大的流量面前,也难以持续下去,高昂的硬件成本,将迫使我们去寻找更合适的技术方案。

世界上有一个著名的法则叫"奥卡姆剃刀定律",讲的是程序员该如何选择合适的剃刀,来让自己的秀发光滑柔顺有光泽。

其实不是的,该定律主要就是八个字"如无必要,勿增实体"。当一个流程难以支撑当前的业务时,我们就该审视一下,哪些步骤是不必要的。

up-6ed1a29a18b27425f5bd6eb2de50392a441.png

从这个通用流程中,其实我们很容易就能发现,我们经历了很多读写,每次读写都伴随着磁盘的读写(包括MQ也是写磁盘的),和频繁的序列化反序列化,以及翻倍的网络IO。

那么让我们挥舞起奥卡姆的剃刀,做一些删减,把非必要的部分给删掉,就变成了下图的流程:

up-433fdb02be43de5c481a250ac5ab920a267.png

我们发现,其实写本地磁盘、和MQ都是没有必要的,我们完全可以将日志数据写到本地内存,然后搞个线程,定时通过UDP将日志直接发送到worker端即可。

worker接收到之后,解析一下,写入自己的内存队列,再起数个异步线程,批量将队列的数据写入ClickHouse数据库即可。

大家可能看到了,下图的流程中,那个圆圈明显比上图的圆圈要小,这是为什么呢?因为我做了压缩。

前文讲过,我们单条报文40k-2M,这是一个非常大的报文,这里面都是一些用户请求的入参Json和出参Json以及一些中途日志,我们完全没有必要将原文原封不动往外传输。

通过采用主流的snappy、zstd等压缩工具类,可以直接将字符串压缩成byte[]再往外传输,这个被压缩后的字符串,直至入库都是byte[],全程不对大报文解压。

那么这个压缩能压多少呢,80%-90%,一个60k的报文,往外送时就剩6-8k了,可想而知,仅仅压缩一下原始数据,就在整个流程中,节省了巨大的带宽,同时也大幅提升了worker的吞吐量。

这里有个小细节,udp单个最大报文是64kb,如果我们压缩后,还是超过了64kb的话,udp是送不出去的,这里可以选择发个http请求送到worker即可。

通过上图,我们可以看到,当流程中的某些环节并不是必需的时,我们应该果断砍掉,不要轻易照搬网上的方案,而应该选择更适合自己的方案。下面我们详细讲一下系统是如何设计、运转的。

更强悍的日志搜集系统

up-145416d48409c1b2db238a77f92fa8b727b.png

我们来审视一下这个链路极短的日志搜集系统。

配置中心:用来存储worker的IP地址,供客户端获取自己模块所分配的worker集群的ip。

client:客户端启动后,从配置中心拉取分配给自己这个模块的worker集群的IP,并轮询将搜集的日志压缩后发送过去,通过UDP的方式。

worker:每个模块会分配数量不等的worker机器,启动后上报自己的IP地址到配置中心。接受到客户端发来的日志后,解析相应的字段,批量写入clickhouse数据库。

clickhouse:一个强大的数据库,压缩比很高,写入性能极强,按天分片,查询速度佳。非常适合应用于日志系统这种写入极大,查询较少的系统。

dashboard:可视化界面,从clickhouse查询数据展示给用户,具有多条件多维度查询功能。

大家都能看出来,这其中最关键的地方是worker端,它的承接流量、消费性能、入库性能将决定着整个链路能否良好地运转。

我们主要分别讲解一下client端和worker端的实现。

up-4c6ddc2bdb3f7a82029bbb84099ec570109.png

Client端聚合日志

一次请求中,我们通常要保留的日志信息主要有:

(1)请求的出入参>

  如果是http web应用,要获取出入参比较简单的方式就是通过自定义filter即可。client的sdk里定义了一个filter,接入方通过配置该filter生效即可搜集到出入参。

  如果是其他rpc应用非http请求的,也提供了对应的filter拦截器来获取出入参。

  在获取到出入参后,sdk对其中大报文,主要是出参进行了压缩,并将整个数据定义为一个JAVA对象,做protobuf序列化,通过UDP方式往自己对应的worker集群轮询传输。

(2)链路上自己打印的一些关键信息,如调用其他系统的的出入参,自己打印的一些info、error信息>

  sdk分别提供了log4j、logback、log4j2三个常用日志框架的自定义appender,用户可以通过在自己的日志配置文件(如logback.xml)中,将我自定义的appender定义出来即可,那么后续用户在代码里所有打印的info、error等日志都会执行这个自定义appender。

  同样,这个自定义appender也是将日志暂存内存,然后异步UDP外送到worker。

这里主要有两个地方需要注意,一是当压缩后的报文依旧超出udp最大报文值时,即通过http送出。二是这一次请求,链路中可能会使用多线程、线程池技术,为避免链路tracer的唯一id在线程池丢失,sdk采用了TransmittableThreadLocal来保持链路的ID,这个查一下就懂。

总体来说,client端实现较为简单,省略了写本地磁盘、消费文件发MQ等等步骤,整体只有一次Protobuf序列化操作,对CPU、接入方性能影响极小,采用UDP外送,不需要worker的任何回复,也不用考虑tcp模式下worker消费慢导致自己阻塞的问题。整体非常简洁高效。

Worker端消费日志并入库

worker端是调优的重点,由于要接收海量客户端发来的日志,解析后入库,所以worker需要具备很强的缓冲能力。

我们都能看出来,系统的瓶颈点肯定在入库这个阶段,解析日志,抽取字段都是效率很高的,而且完全可以通过控制线程的数量来控制住,而入库将强受限于clickhouse的写入性能。至于clickhouse是如何做的优化,后面会有clickhouse集群负责人来讲一下做了哪些优化。

为了做好这个缓冲,即便日志接收量大于入库量,我们也要能接下来这些数据,尽量不丢失。首先硬件上,采用大内存机器,8核32G的容器,来尽量多屯一些数据。其次,采用了双缓冲队列,先将所有接收的数据放一个队列,然后多线程消费、解析成可供入库的行数据,再放入一个待入库队列,然后批量入库。

那么我们做的这些操作,能支撑什么样的数据量呢?

通过线上的应用和严苛的压测,这样一台单机docker容器,每秒可以处理原始日志1-5千万行,譬如一条用户请求,中途产生了共计1千多行日志,那么这样的一台worker单机,每秒可以处理2万客户端QPS。对外写clickhouse数据库,每秒可以写200多M比较稳定。

通过对上文的了解,我们知道,这些数据都是被压缩过的,直至库里面的都是压缩过的,只有当最终用户查询时,才会进行解压。所以,这200M,基本相当于原始数据1G多的大小。

也就是说,只要clickhouse写入速度跟的上,这个系统仅需100台就可以极其高效地处理原始秒级百G的日志。对比写MQ的方案,中途所有会出现瓶颈的点如MQ写磁盘速度、消费拉取速度等,都将不复存在。这是一个纯内存交换的链路系统。

强悍的Clickhouse

通过以上的了解,我们可以清楚的看到,worker作为一个纯内存计算的组件,client端通过worker的数量进行hash均匀分发到各个worker,所以worker可以动态扩容而且不存在性能瓶颈,其唯一受限制的就是写入库的速度。

倘若写库速度跟不上,则worker必须要拿有限的内存去屯下发来的大量数据,一旦写满则就会开始丢弃接收到的数据。所以整个系统的瓶颈点,就是写库的速度。

Clickhouse是面向海量数据实时、多维分析、高性能的新一代OLAP数据库管理系统,实现了向量化执行和SIMD指令,对内存中的列式数据,一个batch调用一次SIMD指令,大幅缩短了计算耗时,带来数倍的性能提升。目前已成为驱动京东集团业务增长、创新的“超级引擎”。那么在京东App秒级百G日志传输存储架构中,Clickhouse如何支撑大吞吐量数据的写入,主要在于两点

 1)集群高可用架构

         EasyOLAP部署CH集群是三层结构:域名 + CHProxy + CH节点,域名转发请求到CHProxy,再由CHProxy根据集群节点状态来转发。CHProxy的引入是为了让Query均匀分布在每个节点上,,并对CHProxy做了一定的改进,自动感知集群节点的状态变化。

up-752e9eed04a7180d816ccfd4e6d97c6a0c7.png

多条件查询控制台

控制台比较简单,主要就是做一些sql语句查询,做好clickhouse的高效查询,这里简单提一些知识点。

做好数据的分片,如按天分片。用好prewhere查询功能,可以带来性能的提升。做好索引字段的设计,譬如检索常用的时间、pin。

细节难以尽述,要从百亿千亿数据中,做好极速的查询,还需要对clickhouse的一些查询特性有所了解。

下图界面展示的即为一些索引项,点击查看详情,则从数据库捞出压缩过的数据,此时才解压并展示给前端。查看链路,则是该次请求中,整个链路用户打印的log(包括线程池内的)。

up-7aea613cef55f035728f80cde4fa3704b9e.png

up-8c9ac94e4c5e58db08ccefb90abb702bcf8.png

总结与对比

我们可以简单的做一些对比,主要在于硬件成本和软件性能的对比。

up-bdb3a090866bd7fe17dd2d0dfd9191ecc05.png

从上文可知,磁盘的占用原始方案占用了磁盘(1份),MQ(2份),数据库(1份)。而在新的方案中,磁盘的占用仅剩下clickhouse的(0.8份),clickhouse自身又对数据做了压缩,实际占用空间不到入库容量的80%。

那么仅磁盘即可节省75%以上的存储成本。

大家都知道,秒级的吞吐量,是伴随着服务器Cpu的耗费的,并不是说只给个大硬盘,即可一台服务器每秒吞吐1个G的。每台服务器秒级的吞吐量是有上限的,秒级占用磁盘的上升,即对应Cpu数量的上升,要支撑一秒1G的磁盘写入,需要5台或以上的服务器。

up-934206a849391909dedc0c24d3fc79f9cbf.png

那么在磁盘的大幅节约下,线性地节省了大量的中间过程Cpu服务器。实际粗略统计效果,流程中服务器可节约70%以上。

在软件性能上,过程很好理解。对Client端的消耗主要就是序列化、写磁盘、读磁盘、反序列化这几步的消耗,Udp则仅有一步序列化。我们假设MQ集群是有无限的写入能力,可以吞下所有的发过去的日志,那么就是worker端的消费性能对比。

从MQ拉取并消费,这个过程如果MQ没有积压,则有零拷贝在支撑高速的拉取,如果积压了,则可能产生大量的MQ磁盘IO,拉取速度会大幅下降。这个过程效率会明显低于Udp发送到worker的处理效率,而且占用双份的网络带宽。实际表现上,worker表现出的强劲性能,较之前单条拉取MQ集群时,消费性能提升在10倍以上。

本文到此就结束了,主要简略介绍了一个新的日志搜集系统(用户跟踪框架)的设计方案,以及该方案能带来的巨大的成本节省。相关代码日后会开源于"gitee.com"的京东零售账号下,届时有相关需求的同学可以加以关注。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK