摘要:AccessLogWriter是唯品会Dragonfly日志系统众多组件中的一个新成员,用于将Access Log写入HDFS以提供准实时的文件下载和快速查询服务。AccessLogWriter能达到单机150万QPS的处理效率,仅使用6台服务器就支撑了今年616大促时全网包括Nginx, Janus, Osp, Osp-proxy的Access Log流量。
使用Golang实现高吞吐量的Hadoop应用,以及在HDFS上并发写入数万个文件,在业界并没有很好的参考案例。本文介绍了唯品会Access Log存储方案,以及AccessLogWriter的实现思路,或许能给你带来一些新的想法。
唯品会技术平台有多种组件会生成访问日志,包括:
Nginx
Janus网关
Osp服务组件及其proxy agent
这些访问日志数量巨大,使用频率又相对较低(通常是出现问题才需要使用),由于公司Dragonfly日志系统的资源限制,一直没有允许访问日志接入。以往开发人员在排查问题要用到访问日志时,只能通过运维同事到服务器拉取,效率很低,特别是不确定需要的log在哪一台服务器的情况下。因此,我们需要一个可以对Access Log提供集中存储和方便访问的低成本方案。
方案设计
总体方案
Access Log在服务器上通过采集客户端上传到Kafka。为最大程度保留用户对Access Log的使用习惯,我们将上传上来的Access Log按一定方式切分成文件,压缩后存储在HDFS,并提供下载和查询界面。
压缩方式选择的是gzip,它相比其它压缩方式有两大优点:
可以不断拼接新的数据块,特别适合流式写入;
用户操作系统普遍支持解压(唯品会的Windows系统安装了7zip,Linux/Mac则更加方便,不但自带gzip支持,还有zcat/zless等好用的命令)。
缺点则是性能比不上新一代的压缩算法如snappy, lz4等。
文件按照以下规则进行切分并保存:
类型
应用名
主机名
日期
小时
大小(目前按压缩后每100MB滚动)
这里会出现一个问题,以唯品会目前的服务器规模,按照上述规则每小时会生成20000个以上的Access Log文件。众所周知,HDFS擅长处理大文件读写。但是如果缓存一小时的数据再一次写入,需要使用非常大量的内存,而且会带来至少一小时的延迟,这是不可接受的。而要实时写入上万个文件,性能又不能满足。所以我们的方案就是适当延迟的批量写入。似乎简单明了,但为了达到很高的性能,还是需要良好的设计。
语言选型
Java系语言向来是开发Hadoop应用的绝对主流。但对于处理Access Log写入这类高吞吐量场景,从以往项目经验看来,Java的GC问题会是很大的制约。
于是,我们尝试了使用相对偏门的Golang来实现。在本项目实践中,Golang本身语言的简洁、实现并发的优雅、对内存使用的高效、以及编译和调试的便利,这些优势都发挥地淋漓尽致。
最终的实现,大量使用了goroutine和channel,通过有效的设计,总体代码量并不大,1千行左右。
值得一提的是,Dragonfly上提供对全应用access log文件的在线查询及高级统计功能(支持以管道符拼接的grep, awk, sed, wc等操作),其调用API也是通过Golang实现的。
AccessLogWriter设计
AccessLogWriter就是用来处理写入HDFS的组件。下面是它的设计和介绍:
Consumer
负责消费Kafka数据,使用github.com/Shopify/sarama实现。经过测试,单个实例处理能力大约为50万QPS。如果要达到更高性能,则在一个AccessLogWriter上需要启动多个Consumer实例。
我们在日志上传到Kafka时已经设置了以主机名作为partitioning hash key,也就是同一台主机的日志会顺序写入同一个kafka partition,所以我们将收到日志也按照partition id取模输出到对应编号的Parser,就可以保证最终每个主机对应的文件中日志的顺序。
Parser
负责解析日志内容,提取出应用名(在Nginx Access Log中则是host字段,即要访问的域名),主机名,和时间戳。这些信息用于组合生成写入HDFS的文件路径。
我们维护一个全局的map,以写入HDFS的文件路径为key,它的值为一个HdfsTarget对象:
Parser从map中找到或创建HdfsTarget对象,将日志发送到它的Compressor channel中。
在工作模式上,Parser相当于Nginx中的worker process,无状态的处理,输出到下游不做等待立即返回。
Compressor
负责日志压缩,每个文件对应一个Compressor。Compressor中包含了第一级的缓存,默认每1000条日志或者每2分钟,执行批量压缩后输出到Writer。
批量压缩可以提高数据压缩率。一次压缩的日志数越多,压缩率越高,但两者不是线性关系,而且会增加内存使用以及增加延迟,所以需要有一个权衡。
Writer
负责缓存压缩数据并写入HDFS,每个文件对应一个Writer。Writer中包含了第二级的缓存,默认每5MB或每10分钟,将缓存数据flush到HDFS中,并判断是否需要滚动成新的文件。对HDFS写失败会进行重试。
为了避免每隔10分钟出现大量文件同时写HDFS,每个Writer第一次写有一个随机的时间。
小结
AccessLogWriter设计的关键之处,在于二级缓存以及gzip压缩格式的使用。
二级缓存的设计,使数据在等待写入HDFS之前是以压缩格式缓存的,大大减少了内存的占用,而压缩率又比逐条压缩高的多。
而gzip压缩格式的使用,是压缩数据块可以流式写入的前提。并且压缩数据分块写入,可以在由于异常原因导致个别数据块损坏的情况下,仍能通过头部检索恢复绝大部份的数据。
一些优化
最终效果
经过一些参数调整,最终对HDFS的写入频率平日可以控制在50QPS左右,HDFS集群的性能完全可以满足。而文件更新延迟可以控制在15分钟以下(只对日志量少的应用,日志量大的当buffer满了就会flush,延迟在分钟级。最大延迟可以配置得更低,但似乎没有需要)。
在单台24核CPU,128GB内存的服务器上,AccessLogWriter最高可以达到每秒处理150万条日志的速率。这时CPU接近跑满,主要被压缩计算占用,是比较理想的结果。内存占用通常在64GB以下,预留了空间给万一出现处理延迟需要追赶的情况下需要使用更多的内存。
在今年616大促时,仅使用6台AccessLogWriter服务器和10台HDFS服务器,就支撑了全网各种Access Log的写入,最大延迟不超过半小时。
篇外话
Access Log下载和查询是Dragonfly日志系统的一个新功能。唯品会内部数坊平台提供了全网的Nginx Access Log数据,但数据延迟时间比较长、查询速度稍慢。Dragonfly的Access Log服务可以提供准实时数据和快速查询,但目前还没有离线处理和报表生成能力。所以对于Nginx Access Log数据,数坊和Dragonfly可以很好地互补。
除此之外,内部Osp Access Log,Osp-Proxy Access Log和Janus Access Log也可以在Dragonfly上查到。
领取专属 10元无门槛券
私享最新 技术干货