高性能日志:如何提升日志性能避免 IO 瓶颈?_日志性能优化-程序员宅基地

技术标签: php-小记  架构  日志  高性能  

是当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 “iowait”。

iowait 怎么查看呢?

如果你用的是 Linux 系统或者 Mac 系统,当你在执行一项很耗费磁盘 IO 的操作时,比如读写大文件,通过 top 命令便可以看到。如下图所示:

在这里插入图片描述

CPU 开销示意图

其中的 2.6 wa 便是 iowait 占用了 2.6% CPU。

那么,这种 CPU 开销对性能会有什么影响呢?特别是像秒杀这样的高并发系统,当秒杀服务运行的时候,会输出大量信息到日志文件,比如程序报错信息、请求参数的调试信息等,而这些写日志文件无疑会给磁盘带来更大的压力,导致更多的 CPU 开销。所以,这一讲我们就主要来探讨下这个问题。

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

这是什么概念?

磁盘有个性能指标:IOPS,即每秒读写次数。一块性能比较好的固态硬盘,IOPS 大概在 3 万左右。也就是说,一个秒杀节点的每秒日志条数是固态硬盘 IOPS 的 10 倍!如果这些日志每次请求时都立即写入磁盘,磁盘根本扛不住,更别说通过网络写入到监控系统中。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

怎么做呢?

我们可以参考共享单车运营方的做法。像摩拜、哈罗、青桔等,单车的起步和归还都在人流量大的投放点,而不是运营方仓库。假如在程序中,我们也能像共享单车一样,根据实际业务自己管理内存的分配和归还,就能避免譬如内存碎片和内存垃圾回收,导致性能降低的问题。

具体怎么实现?

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

比如下面的代码就会触发内存分配。

str := "hello " + userName

如何避免字符串内存分配呢?一般我们可以直接使用字符数组,基于字符数组做参数拼接。典型的例子是实现一个带字符数组缓冲区的日志对象,提供类似 AppendInt、AppendString 这样的方法拼接参数。比如下面这部分。

type Logger struct{
  data []byte
}
const maxDataSize = 65536
func NewLogger() *Logger {
  l := &Logger{
    data: make([]byte, 0, maxDataSize)
  }
}
// 整数转成字符数组并追加到缓冲区
func (l *Logger)AppendInt(data int){
  d := strconv.Itoa(data)
  l.data = append(l.data, d...)
  l.tryFlush()
}
// 字符串转成字符数组并追加到缓冲区
func (l *Logger)AppendString(data string){
  l.data = append(l.data, []byte(data)...)
  l.tryFlush()
}
// 关闭 Logger,将缓冲区中数据写入到日志文件中。通常在程序退出前调用该函数。
func (l *Logger)Close(){
  l.Flush()
}
func (l *Logger)Flush(){
  // 此处省略具体写文件的代码,大家可以自行练习
  // 将字符切片指向 l.data 的头部,清空缓冲区
  l.data = l.data[0:0]
}
func (l *Logger)tryFlush(){
  // 超过 64KB 则写入到磁盘
  if len(l.data) >= maxDataSize {
    l.Flush()
  }
}

在上面的代码实现中,每个 Append 函数中采用追加的方式拼接参数,在缓冲区足够用的情况下,不会为拼接后的数据重新分配内存。

怎么确保缓冲区足够用呢?
答案是最后面的 tryFlush 函数,它能控制缓冲区中的内容不会过大。 当 tryFlush 函数发现数据长度超过设定的最大值时,会将数据写入到日志文件中并清空缓冲区。在这个过程中,Logger 不需要归还、再分配缓冲区。

当然,以上只是个简单的示例,真正生产环境中用的 Logger 要强大很多。感兴趣的可以看看 zap、logrus 等 Logger 的实现。

如何减小丢日志的风险
前面我们了解到,秒杀服务在高并发下发生异常的时候可能导致部分日志丢失。我们还了解到,秒杀服务日志不能实时写入到日志文件。有没有发现,这两件事情是互相矛盾的?实际上,在高并发下,我们无法彻底解决丢日志的风险,只能减小丢日志的概率。为啥呢?

在高并发下,我们需要尽可能将日志先缓存到程序本地内存中,也就是 Logger 的缓冲区中。当日志到一定量后,批量写入日志文件,以便达到良好的写入性能。但是,假如程序异常退出,而缓冲区中日志大小又没达到批量写入的条件,这部分日志就可能丢弃了。

怎么办呢?

程序异常有两种:一种是能捕获的可控异常,比如 Golang 中数组越界触发 panic;一种是无法捕获的不可控异常,比如 Golang 中并发读写未加锁的 map。

这两种异常下,如何尽可能将缓冲区中的日志写入日志文件呢?

对于第一种情况,通常是捕获异常,在退出程序前执行实例代码中的 Close 函数将日志写入到日志文件。对于第二种情况,我们可以采用定时器,定时将缓冲区中的数据写入到日志文件中,比如定时 100 毫秒执行 Flush 函数
在这里插入图片描述

本文章内容为转载,如有侵权请联系作者下架。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16142851/article/details/112511354

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签