面试:你们是如何提升日志性能避免 IO 瓶颈?

2,561 阅读7分钟

对于上面的问题,相信大家都有答案。

  • 答案1:直接落库,查询日志文件,或者分布式的话就是直接落库,用日志收集组件去收集+展示。
  • 答案2:用ELK+Kafka收集日志之后做展示。 上面两个答案,其实大部分业务都是这么做的,但是现在很多人的简历上喜欢写高并发,多少多少qps,多少多少tps,那在高并发的情况下。我觉得上面的回答并不是最佳答案,也不是最优的做法。

秒杀日志面临的问题

对于并发不高的服务,我们可以把所有需要的日志写入到磁盘上的日志文件里。但是,在高峰期间,秒杀服务单节点需要处理的请求 QPS 可能达到 10 万以上。一个请求从进入秒杀服务到处理失败或者成功,至少会产生两条日志。也就是说,高峰期间,一个秒杀节点每秒产生的日志可能达到 30 万条以上。

这里由于我之前公司其中做过的一个项目的每秒QPS在150w以上,所以每秒的日志产生的量大概在450w以上。分给每台机器,其实最少最少要20w。

这是什么概念?

磁盘有个性能指标:IOPS,即每秒读写次数。一块性能比较好的固态硬盘,IOPS 大概在 3 万左右。也就是说,一个秒杀节点的每秒日志条数是固态硬盘 IOPS 的 10 倍!如果这些日志每次请求时都立即写入磁盘,磁盘根本扛不住,更别说通过网络写入到监控系统中。通过kafka+elk虽然可以避免上述的问题,但是首先kafka在常规的服务配置上,支持的吞吐量是10w级,所以也就是说,我们还需要几十台服务器用来支撑kafka+elk,这方案虽然可以,但是实际上确实比较费钱,而且如果项目不是持续性高并发的情况下,其实这种方案是比较坑的。

所以,秒杀日志会面临的第一个问题是,每秒日志量远高于磁盘 IOPS,直接写磁盘会影响服务性能和稳定性。

另外,服务在输出日志前,需要先分配内存对日志信息进行拼接。日志输出完,还需要释放该日志的内存。这将会导致什么问题呢?

对于那些有内存垃圾回收器的语言,如 Java 和 Golang ,频繁分配和释放内存,可能会导致内存垃圾回收器频繁回收内存,而回收内存的时候又会导致 CPU 占用率大幅升高,进而影响服务性能和稳定性。

那些没有内存垃圾回收器的语言,如 C++ ,又会受什么影响呢?它们通常是从堆内存中分配内存,而大量的分配、释放堆内存可能会导致内存碎片,影响服务性能。

所以,秒杀日志会面临的第二个问题是,大量日志导致服务频繁分配,频繁释放内存,影响服务性能。

最后,秒杀日志还会面临服务异常退出丢失大量日志的问题。

我们知道,由于秒杀服务处理的请求量太大,每秒都会有很多请求的日志未写入磁盘。如果秒杀服务突然出问题挂掉了,那这批日志可能就会丢失。

对于高并发系统,这在所难免,问题是如何把控好写入日志的时间窗口,将丢失的日志条数控制在一个很小的可接受范围内。

这就是秒杀日志面临的第三个问题。通过上面的介绍,想必你也明白了,像秒杀这种大流量业务场景下,日志收集是个大难题,也是个必须要解决的性能问题。

如何优化秒杀日志性能?

前面我们了解到,秒杀日志面临着磁盘 IO 高、内存压力大、大量丢失等风险,归根结底,还是因为日志量太大,常规日志保存手段已经无法发挥作用。怎么办呢?接下来我就对这几个问题一一介绍下。

磁盘IO性能优化

首先,我们来看下秒杀日志量超过磁盘 IOPS 的问题。

上一讲我给你介绍了多级缓存,你是否还记得内存性能和磁盘性能的差别呢?没错,内存性能远高于磁盘性能。那我们能否利用内存来降低磁盘压力,提升写日志的性能呢?答案是可以。

Linux 有一种特殊的文件系统:tmpfs,即临时文件系统,它是一种基于内存的文件系统。当使用临时文件系统时,你以为在程序中写文件是写入到磁盘,实际上是写入到了内存中。临时文件系统中的文件虽然在内存中,但不会随着应用程序退出而丢失,因为它是由操作系统管理的。

由于云架构保障了云主机的高可用,只要操作系统正常运行,也没有人删除文件,临时文件系统中的文件就不会丢失。所以,我们可以将秒杀服务写日志的文件放在临时文件系统中。相比直接写磁盘,在临时文件系统中写日志的性能至少能提升 100 倍。

当然,临时文件系统中的日志文件也不能无限制地写,否则临时文件系统的内存迟早被占满。那该怎么办呢?可以这样处理,比如,每当日志文件达到 20MB 的时候,就将日志文件转移到磁盘上,并将临时文件系统中的日志文件清空。 相比频繁的小数据写入,磁盘在顺序写入大文件的时候性能更高,也就降低了写入压力。

内存分配性能优化

不知道你学过 C 语言没?如果学过的话,你应该对 malloc 函数和 free 函数不陌生。malloc 函数主要用于从堆内存中分配内存,而 free 函数则是将使用完的内存归还到堆内存中。堆内存是由系统管理的,当堆内存中有大量碎片时,为了找到合适大小的存储空间,可能需要比对多次才能找到,这无疑让程序性能大打折扣。

而秒杀服务在输出大量日志的时候会存在频繁的内存分配和归还,如果使用常规方式分配内存,会导致高并发下性能下降。所以,我们需要使用高效的内存管理,既能快速分配内存,又能避免频繁触发垃圾回收器回收内存。

具体怎么实现?

对于秒杀系统来说,它的日志里需要附加一些信息,以便后面排查问题或者数据统计,这些附加信息有用户 ID、来源 IP、抢购的商品 ID、时间等。但日志文件是纯文本的,而附加信息中有的是整数,有的是字符串,这就需要统一拼接成字符串才能输出到文本文件中。然而,在像 Java、Golang 这类高级语言中,字符串是一个经过封装的对象,底层是字符数组。直接用字符串拼接的话,会导致程序分配新的字符串对象来保存拼接后的结果。

如何避免字符串内存分配呢?一般我们可以直接使用字符数组,基于字符数组做参数拼接。典型的例子是实现一个带字符数组缓冲区的日志对象。

总结:

一开始说的两个方案,其实都是可以的,如果是一个非高并发的服务,直接写日志文件就行,如果是高并发的项目,比如秒杀系统,用kafka+elk可以解决但是并不是最优选项。

如果面试中问到你这个了。普遍公司其实都是kafka+elk。就这么回答没问题,不过你可以后面接一句,但是我觉得有更好的解决方案,可以代替kafka+elk。之后把上面的tmpfs说一下和字符数组拼接说一下,这样面试官会对你有一个不一样的看法