百亿级日志系统架构设计及优化
点击上方“民工哥Linux运维”,选择“置顶公众号”
有趣有内涵的文章第一时间送达!
日志数据是最常见的一种海量数据,以拥有大量用户群体的电商平台为例,双 11 大促活动期间,它们可能每小时的日志数量达到百亿规模,海量的日志数据暴增,随之给技术团队带来严峻的挑战。
本文将从海量日志系统在优化、部署、监控方向如何更适应业务的需求入手,重点从多种日志系统的架构设计对比;后续调优过程:横向扩展与纵向扩展,分集群,数据分治,重写数据链路等实际现象与问题展开。
日志系统架构基准
有过项目开发经验的朋友都知道:从平台的最初搭建到实现核心业务,都需要有日志平台为各种业务保驾护航。
如上图所示,对于一个简单的日志应用场景,通常会准备 master/slave 两个应用。我们只需运行一个 Shell 脚本,便可查看是否存在错误信息。
随着业务复杂度的增加,应用场景也会变得复杂。虽然监控系统能够显示某台机器或者某个应用的错误。
然而在实际的生产环境中,由于实施了隔离,一旦在上图下侧的红框内某个应用出现了 Bug,则无法访问到其对应的日志,也就谈不上将日志取出了。
另外,有些深度依赖日志平台的应用,也可能在日志产生的时候就直接采集走,进而删除掉原始的日志文件。这些场景给我们日志系统的维护都带来了难度。
参考 Logstash,一般会有两种日志业务流程:
正常情况下的简单流程为:应用产生日志→根据预定义的日志文件大小或时间间隔,通过执行 Logrotation,不断刷新出新的文件→定期查看→定期删除。
复杂应用场景的流程为:应用产生日志→采集→传输→按需过滤与转换→存储→分析与查看。
我们可以从实时性和错误分析两个维度来区分不同的日志数据场景:
实时,一般适用于我们常说的一级应用,如:直接面向用户的应用。我们可以自定义各类关键字,以方便在出现各种 error 或 exception 时,相关业务人员能够在第一时间被通知到。
准实时,一般适用于一些项目管理的平台,如:在需要填写工时的时候出现了宕机,但这并不影响工资的发放。
平台在几分钟后完成重启,我们可以再登录填写,该情况并不造成原则性的影响。因此,我们可以将其列为准实时的级别。
除了直接采集错误与异常,我们还需要进行分析。例如:仅知道某人的体重是没什么意义的,但是如果增加了性别和身高两个指标,那么我们就可以判断出此人的体重是否为标准体重。
也就是说:如果能给出多个指标,就可以对庞大的数据进行去噪,然后通过回归分析,让采集到的数据更有意义。
此外,我们还要不断地去还原数字的真实性。特别是对于实时的一级应用,我们要能快速地让用户明白他们所碰到现象的真实含义。
例如:商家在上架时错把商品的价格标签 100 元标成了 10 元。这会导致商品马上被抢购一空。
但是这种现象并非是业务的问题,很难被发现,因此我们只能通过日志数据进行逻辑分析,及时反馈以保证在几十秒之后将库存修改为零,从而有效地解决此问题。可见,在此应用场景中,实时分析就显得非常有用。
最后是追溯,我们需要在获取历史信息的同时,实现跨时间维度的对比与总结,那么追溯就能够在各种应用中发挥其关联性作用了。
上述提及的各个要素都是我们管理日志的基准。如上图所示,我们的日志系统采用的是开源的 ELK 模式:
ElasticSearch(后简称 ES),负责后端集中存储与查询工作。
单独的 Beats 负责日志的搜集。FileBeat 则改进了 Logstash 的资源占用问题;TopBeat 负责搜集监控资源,类似系统命令 top 去获取 CPU 的性能。
由于日志服务对于业务来说仅起到了维稳和保障的作用,而且我们需要实现快速、轻量的数据采集与传输,因此不应占用服务器太多资源。
在方式上我们采用的是插件模式,包括:input 插件、output 插件、以及中间负责传输过滤的插件。这些插件有着不同的规则和自己的格式,支持着各种安全性的传输。
日志系统优化思路
有了上述日志的架构,我们针对各种实际的应用场景,进一步提出了四个方面的优化思路:
基础优化
内存:如何分配内存、垃圾回收、增加缓存和锁。
网络:网络传输序列化、增加压缩、策略、散列、不同协议与格式。
CPU:用多线程提高利用率和负载。
此处利用率和负载是两个不同的概念:
利用率:在用满一个核后再用下一个内核,利用率是逐步升高的。
负载:一下子把八个核全用上了,则负载虽然是满的,但是利用率很低。即,每核都被占用了,但是所占用的资源却不多,计算率比较低下。
磁盘:尝试通过文件合并,减少碎片文件的产生,并减少寻道次数。同时在系统级别,通过修改设置,关闭各种无用的服务。
平台扩展
做加减法,或称替代方案:无论是互联网应用,还是日常应用,我们在查询时都增加了分布式缓存,以有效提升查询的效率。另外,我们将不被平台使用到的地方直接关闭或去除。
纵向扩展:如增加扩展磁盘和内存。
横向扩展:加减/平行扩展,使用分布式集群。
数据分治
根据数据的不同维度,对数据进行分类、分级。例如:我们从日志中区分error、info、和 debug,甚至将 info 和 debug 级别的日志直接过滤掉。
数据热点:例如:某种日志数据在白天的某个时间段内呈现暴涨趋势,而晚上只是平稳产生。我们就可以根据此热点情况将它们取出来单独处理,以打散热点。
系统降级
我们在对整体业务进行有效区分的基础上,通过制定一些降级方案,将部分不重要的功能停掉,以满足核心业务。
日志系统优化实践
面对持续增长的数据量,我们虽然增加了许多资源,但是并不能从根本上解决问题。
特别体现在如下三方面:
日志产生量庞大,每天有几百亿条。
由于生产环境隔离,我们无法直接查看到数据。
代理资源限制,我们的各种日志采集和系统资源采集操作,不可超过业务资源的一个核。
一级业务架构
我们日志系统的层次相对比较清晰,可简单分为数据接入、数据存储和数据可视化三大块。
具体包括:
Rsyslog,是目前我们所接触到的采集工具中最节省性能的一种。
Kafka,具有持久化的作用。当然它在使用到达一定数据量级时,会出现 Bug。
Fluentd,它与 Rsyslog 类似,也是一种日志的传输工具,但是它更偏向传输服务。
ES 和 Kibana。
该架构在实现上会用到 Golang、Ruby、Java、JS 等不同的语言。在后期改造时,我们会将符合 Key-Value 模式的数据快速地导入 HBase 之中。
基于 HBase 的自身特点,我们实现了它在内存层的 B+ 树,并且持久化到我们的磁盘之上,从而达到了理想的快速插入的速度。这也正是我们愿意选择 HBase 作为日志方案的原因。
二级业务架构
我们直接来看二级业务架构的功能图,它是由如下流程串联而成的:
在完成了数据采集之后,为了节省自己占用磁盘的空间,许多应用会完全依赖于我们的日志系统。因此在数据采集完以后,我们增加了一个持久缓存。
完成缓存之后系统执行传输。传输的过程包括:过滤和转换,这个过程可以进行数据抽稀。值得强调的是:如果业务方尽早合作并给予我们一些约定的话,我们就能够通过格式化来实现结构化的数据。
随后执行的是分流,其主要包括两大块:一种是 A 来源的数据走 A 通道,B 来源的数据走 B 通道。另一种是让 A 数据流入到我们的存储设备,并触发保护机制。即为了保障存储系统,我们额外增加了一个队列。
例如:队列为 100,里面的一个 chunk 为 256 兆,我们现在设置高水位为 0.7、低水位为 0.3。
在写操作的堆积时,由于我们设置了 0.7,即 100 兆赫。那么在一个 256 兆会堆积到 70 个 chunk 时,我们往该存储平台的写速度就已经跟不上了。
此时高水位点会被触发,不允许继续写入,直到整个写入过程把该 chunk 消化掉,并降至 30 个时,方可继续往里写入。我们就是用该保护机制来保护后台以及存储设备的。
接着是存储,由于整个数据流的量会比较大,因此在存储环节主要执行的是存储的索引、压缩、和查询。
最后是 UI 的一些分析算法,运用 SQL 的一些查询语句进行简单、快速地查询。
通常从采集(logstash/rsyslog/heka/filebeat)到面向缓存的 Kafka 是一种典型的宽依赖。
所谓宽依赖,是指每个 App 都可能跟每个 Broker 相关联。在 Kafka 处,每次传输都要在哈希之后,再把数据写到每个 Broker 上。
而窄依赖,则是其每一个 Fluentd 进程都只对应一个 Broker 的过程。最终通过宽依赖过程写入到 ES。
采集
如 Rsyslog 不但占用资源最少,而且可以添加各种规则,它还能支持像 TSL、SSL 之类的安全协议。
Filebeat 轻量,在版本 5.x 中,Elasticsearch 具有解析的能力(像 Logstash 过滤器)— Ingest。
这也就意味着可以将数据直接用 Filebeat 推送到 Elasticsearch,并让 Elasticsearch 既做解析的事情,又做存储的事情。
Kafka
接着是 Kafka,Kafka 主要实现的是顺序存储,它通过 topic 和消息队列的机制,实现了快速地数据存储。
而它的缺点:由于所有的数据都向 Kafka 写入,会导致 topic 过多,引发磁盘竞争,进而严重拖累 Kafka 的性能。
另外,如果所有的数据都使用统一标签的话,由于不知道所采集到的数据具体类别,我们将很难实现对数据的分治。
因此,在后面的优化传输机制方面,我们改造并自己实现了顺序存储的过程,进而解决了一定要做持久化这一安全保障的需求。
Fluentd
Fluentd 有点类似于 Logstash,它的文档和插件非常齐全。其多种插件可保证直接对接到 Hadoop 或 ES。
就接入而言,我们可以采用 Fluentd 到 Fluentd 的方式。即在原有一层数据接入的基础上,再接一次 Fluentd。同时它也支持安全传输。当然我们在后面也对它进行了重点优化。
ES+Kibana
最后我们用到了 ES 和 Kibana。ES 的优势在于通过 Lucene 实现了快速的倒排索引。
由于大量的日志是非结构化的,因此我们使用 ES 的 Lucene 进行包装,以满足普通用户执行非结构化日志的搜索。而 Kibana 则基于 Lucene 提供可视化显示工具。
问题定位与解决
下面介绍一下我们碰到过的问题和现象,如下这些都是我们着手优化的出发点:
传输服务器的 CPU 利用率低下,每个核的负载不饱满。
传输服务器 Full gc 的频次过高。由于我们是使用 Ruby 来实现的过程,其内存默认设置的数据量有时会过大。
存储服务器出现单波峰现象,即存储服务器磁盘有时会突然出现性能直线骤升或骤降。
频繁触发高水位。如前所述的高水位保护机制,一旦存储磁盘触发了高水位,则不再提供服务,只能等待人工进行磁盘“清洗”。
如果 ES 的一台机器“挂”了,则集群就 hang 住了。即当发现某台机器无法通讯时,集群会认为它“挂”了,则快速启动数据恢复。而如果正值系统繁忙之时,则此类数据恢复的操作会更加拖累系统的整体性能。
由于所有数据都被写入 Kafka,而我们只用到了一个 topic,这就造成了每一类数据都要经过不一定与之相关的规则链,并进行不一定适用的规则判断,因此数据的传输效率整体被降低了。
Fluentd 的 host 轮询机制造成高水位频发。由于 Fluentd 在与 ES 对接时遵循一个默认策略:首选前五台进行数据写入,即与前五台的前五个接口交互。
在我们的生产环境中,Fluentd 是用 CRuby 写的。每一个进程属于一个 Fluentd 进程,且每一个进程都会对应一个 host 文件。
而该 host 文件的前五个默认值即为 ES 的写入入口,因此所有机器都会去找这五个入口。
倘若有一台机器宕机,则会轮询到下一台。如此直接造成了高水位的频繁出现、和写入速度的下降。
众所周知,对日志的查询是一种低频次的查询,即只有在出现问题时才会去查看。但是在实际操作中,我们往往通过检索的方式全部取出,因此意义不大。
另外 ES 为了达到较好的性能,会将数据存储在 raid0 中,存储的时间跨度往往会超过 7 天,因此其成本也比较高。
通过对数据的实时线分析,我们发现并未达到写入/写出的平衡状态。
为了提高 Fluentd 的利用率,我们用 Kafka 去数据的时候提高了量,原来是 5 兆,现在我们改到了 6 兆。
如果只是单纯传输,不论计算的话,其实可以改更高。只不过因为我们考虑到这里包含了计算的一些东西,所以只提到了 6 兆。
我们的 Fluentd 是基于 JRuby 的,因为 JRuby 可以多线程,但是我们的 CRuby 没有任何意义。
为了提高内存,我把 Ruby 所有的内存机制了解了一下,就是散列的一些 host 文件,因为我们每个进程都选前五列就可以了,我多开了几个口。ES 的优化这一块,在上 ES 之前,我们已经有人做过一次优化了。
因为基于我刚才说的有时候日志量很高,有时候日志量很少。我们会考虑做动态配置。
因为 ES 就是支持动态配置的,所以它动态配置的时候,我们在某些场景下可以提高它的写入速度,某些场景下可以支持它的这种查询效率。我们可以尝试去做一些动态配置负载。
改造一:存储降低
降低存储在整体架构上并没有太大变化,我们只是在传输到 Fluentd 时把天数降下来,改成了一天。
同时,我们直接进行了分流,把数据往 Hadoop 里写,而把一些符合 Kibana 的数据直接放入 ES。
上面提过,日志查询是低频次的,一般需要查询两天以上数据的可能性很小,因此我们降低存储是非常有意义的。
改造二:数据分治
我们在日志文件节点数较少(机器数量小于 5 台)的情况下,去掉了 Kafka 层。由于 Fluentd 可以支持数据和大文件存储,因此数据能够被持久化地存入磁盘。
我们给每个应用都直接对应了一个 tag,以方便各个应用对应到自己的 tag、遵循自己的固定规则、并最终写入 ES,这样就方便了出现问题的各自定位。
另外,我们运用延迟计算和文件切分也能快速地找到问题的根源。因此我们节约了 Kafka 和 ES 各种计算资源。
在实际操作中,由于 HBase 不用去做 raid,它自己完全能够控制磁盘的写入,因此我们进行了数据压缩。就其效果而言,ES 的存储开销大幅降低。
在后期,我们也尝试过一种更为极端的方案:让用户直接通过客户端的 Shell 去查询数据,并采用本地缓存的留存机制。
优化效果
优化的效果如下:
服务器资源的有效利用。在实施了新的方案之后,我们省了很多服务器,而且单台服务器的存储资源也节省了 15%。
单核处理每秒原来能够传输 3000 条,实施后提升到了 1.5~1.8 万条。而且,在服务器单独空跑,即不加任何计算时,单核每秒能传输近 3 万条。
很少触发 ES 保护机制。原因就是我们已把数据分流出来了。
以前历史数据只能存 7 天,由于我们节省了服务器,因此我们现在可以存储更长时间的数据。而且,对于一些他人查询过的日志,我们也会根据最初的策略,有选择性地保留下来,以便追溯。
日志系统优化总结
关于日志平台优化,我总结了如下几点:
由于日志是低频次的,我们把历史数据存入了廉价存储之中,普通用户需要的时候,我们再导到 ES 里,通过 Kibana 的前端界面便可快速查询到。而对于程序员来说,则不需要到 ES 便可直接查询到。
数据存在的时间越长,则意义越小。我们根据实际情况制定了有效的、留存有意义数据的策略。
顺序写盘替代内存。例如:区别于平常的随机写盘,我们在操作读写一个流文件时采取的是按顺序写数据的模式。
而在存储量大的时候,则应当考虑 SSD。特别是在 ES 遇到限流时,使用 SSD 可以提升 ES 的性能。
提前定制规范,从而能够有效解决后期分析等工作。
日志格式
如上图所示,常用的日志格式类型包括:uuid、timestamp、host 等。
特别是 host,由于日志会涉及到几百个节点,有了 host 类型,我们就能判定是哪台机器上的标准。而图中其他的环境变量类型,则能够有效地追溯到一些历史的信息。
日志方案
如上图所示,我们通过 Rsyslog 可以直接将采集端的数据写入文件或数据库之中。
当然,对于一些暂时用不上的日志,我们不一定非要实施过滤传输的规则。
如上图,Fluentd 也有一些传输的规则,包括:Fluentd 可以直接对接 Fluentd,也可以直接对接 MongoDB、MySQL 等。
另外,我们也有一些组件可以快速地对接插件和系统,例如让 Fluentd 和 Rsyslog 能够直接连到 ES 上。
这是我个人给大家定制的一些最基本的基线,我认为日志从采集、缓存、传输、存储,到最终可视化,分成了三套基线。
采集到存储是最简单的一个,像 Rsyslog 到 hdfs 或者其他 filesystem,我们有这种情况。
比较常见的情况,就是从采集、传输、到存储可视化,然后形成最终我们现在最复杂的一套系统,大家可以根据实际情况取舍。
最后是我考虑到一个实际情况,假如这个案例,我们尽可能少的占有服务器,然后传输需要过滤转换,日志可以比较简单,符合这种 Key value(KV)格式。
我们可以按照取了一个 Rsyslog、取了一个 Fluentd、取了一个 Hbase,取了一个 echars 等这么一个方式做一个方案就可以了。
我觉得 Rsyslog、Fluentd、heka 这些都可以做采集。然后传输这块有 Fluentd 传输,因为 Fluentd 和 Kafka 到插件非常灵活可以直接对接我们很多存储设备,也可以对应很多的文件、连 ES 都可以。
可视化可以用 Kibana,主要是跟 ES 结合得比较紧密,它们结合在一起需要一点学习成本。
杨津萍,大数据架构师,从业十余年,专攻 Web 架构及大数据架构。开源的热衷人员,对大数据类项目,如 Hadoop、Hive、Shark 等,有过开源贡献。 目前在凡普金科担任大数据架构师职位。