磁盘满了对日志打印(Logback)的影响

news2024/11/15 16:25:24

背景

我们生产环境有一个服务半夜报警:磁盘剩余空间不足10%,请及时处理。排查后发现是新上线的一个功能,日志打太多导致的,解决方法有很多,就不赘述了。领导担心报警不及时、或者报警遗漏,担心磁盘满了对线上业务有负面影响,甚至不可用,令我研究一下该情况下对服务的影响。

正文

注:我们线上的应用使用Logback日志框架进行打印,所以log4j、log4j2或其它日志框架不在本次研究的范围。

实验研究

实验步骤

  1. 启动服务,观察各项指标正常(模拟正常工作的服务以及环境状态)
  2. 将磁盘写满
  3. 观察应用服务、宿主机各项指标,查看服务响应是否正常(包括功能以及性能)
  4. 将大文件删除(恢复)
  5. 重复第3步

实验过程

第1、3、5步重在观测,不赘述。

第2步的模拟磁盘写满,使用linux的fallocate命令。

将磁盘写满的方式如下有几种:

  1. 自己写文件,写满为止,速度较慢,有开发(写脚本)成本
  2. 使用dd命令,但是比较慢,取决于磁盘的速度
  3. 使用truncate命令,但是该命令操作的结果并不占用实际的磁盘空间
  4. 使用fallocate命令,fallocate -l {size} {fileName},如fallocate -l 20G text1

我选择了方式4,很快就写满了磁盘

验证方式:vim {xxx},进入文件编辑,写入任意内容,保存失败并提示:Can’t open file for writing(No space left on device)

实验结果

  1. 磁盘写满前、后,应用服务各项指标均正常(功能、性能)
  2. 磁盘写满后,服务器磁盘空间报警(无可用空间),删除文件后报警恢复
  3. 磁盘写满后,应用日志停止打印,删除文件后应用日志恢复打印

即,磁盘满了对于使用logback日志框架的应用,并不会造成影响。

实验结果令人诧异,按正常理解,磁盘满了之后再写入,会报No space left on device异常,进而影响到应用程序的功能。

原理分析

logback配置文件里的文件appender如下(RollingFileAppender同理):

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${LOG_HOME}/${APP_NAME}.log</file>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

把关注点放到appender即可,因为最终的日志输出是由appender控制的:它控制写在哪个文件里,格式是怎样的,滚动策略又是怎样的。

通过IDEA查看RollingFileAppender的继承关系如下下示:

在这里插入图片描述

将视线聚焦到ch.qos.logback.core.OutputStreamAppender#writeBytes,代码如下:

private void writeBytes(byte[] byteArray) throws IOException {
    if(byteArray == null || byteArray.length == 0)
        return;
    
    lock.lock();
    try {
        this.outputStream.write(byteArray);
        if (immediateFlush) {
            this.outputStream.flush();
        }
    } finally {
        lock.unlock();
    }
}

由上可知,写日志时会加锁,避免资源竞争,然后通过outputStream写出,接着马上刷盘(默认情况)。

这里的outputStream可了不得,是个ResilientFileOutputStream实例,顾名思议,【弹性的】FileOutputStream。它是个子类,核心逻辑在父类ResilientOutputStreamBase上,看看write方法:

public void write(byte b[], int off, int len) {
    if (isPresumedInError()) {
        if (!recoveryCoordinator.isTooSoon()) {
            attemptRecovery();
        }
        return; // return regardless of the success of the recovery attempt
    }

    try {
        os.write(b, off, len);
        postSuccessfulWrite();
    } catch (IOException e) {
        postIOFailure(e);
    }
}

这个write方法隐藏着【即使磁盘满了也不影响业务】的秘密!它的核心逻辑是:如果能正常写文件,就写;如果写入失败(出现了IO异常),就抓住异常并切换至失败状态,接下来的一段时间都不再继续写文件,直接返回。

而此处所有的IO异常,就包含了No space left on device,因此,此处便是磁盘满了之后继续日志写入,却不会抛出No space left on device异常进而影响业务的原因。

还有一个问题,待磁盘有剩余空间后,如何恢复日志写入?上面实验结果提到,磁盘写满后,应用日志停止打印,删除文件后应用日志恢复打印。因此必然会有一个策略,能够在磁盘空间恢复后,继续进行日志写入。

再仔细查看看write方法,方法首先判断当前状态是否为“失败”。若是,接着检查自上次失败至今的时间。如果已经经过了很长时间,就【尝试恢复】。

void attemptRecovery() {
    try {
        close();
    } catch (IOException e) {
    }

    addStatusIfCountNotOverLimit(new InfoStatus("Attempting to recover from IO failure on " + getDescription(), this));

    // subsequent writes must always be in append mode
    try {
        os = openNewOutputStream();
        presumedClean = true;
    } catch (IOException e) {
        addStatusIfCountNotOverLimit(new ErrorStatus("Failed to open " + getDescription(), this, e));
    }
}

尝试恢复的逻辑是:关闭旧文件流并重新打开文件流,将【失败】状态切换至【半恢复】状态,此处并未完全进入【正常】状态,而是立即返回,即是说当次的日志记录请求并不会写文件。

下次的日志写入,才会尝试将日志写入文件

  • 若日志成功写入文件,就将状态切换至【正常】状态。
  • 若日志写入文件失败,就将状态切换至【失败】状态。

此处的失败以及恢复逻辑非常像Hystrix断路器,即经历一个正常→失败→半通路状态,通过下次请求的结果来决定是恢复正常还是保持失败。

上面提到若经过很长时间,就尝试恢复,这里的【很长时间】其实是一个泛指,它有自己的时间计算逻辑,此处采用了类似于RocketMQ在消费失败时的重试策略,即采用指数退避逻辑来控制重试时间间隔。

每次【半恢复】状态下写文件失败时,会保持【失败】状态,且每一次的失败写入都会指数级延长【失败】状态保持的时间。

public boolean isTooSoon() {
    long now = getCurrentTime();
    if (now > next) {
        next = now + getBackoffCoefficient();
        return false;
    } else {
        return true;
    }
}
private long getBackoffCoefficient() {
    long currentCoeff = backOffCoefficient;
    if (backOffCoefficient < BACKOFF_COEFFICIENT_MAX) {
        backOffCoefficient *= BACKOFF_MULTIPLIER;
    }
    return currentCoeff;
}

backOffCoefficient初始值:20

BACKOFF_COEFFICIENT_MAX:327680

BACKOFF_MULTIPLIER:4

总结

使用Logback日志框架,当磁盘满了后不必惊慌,它不会对应用程序产生太负面影响。仔细考虑,做为一个日志框架本该如此,不能因为写不了日志就抛出异常进而影响应用本身,毕竟,日志终究是个辅助的旁路逻辑,没有它应用也应该work well。

底层知识的掌握有助于迅速理解上层应用。在阅读源码的过程中,我发现了非常熟悉的设计逻辑:即类似于Hystrix的断路器和RocketMQ消费失败重试的策略。由于之前有相关经验,因此我很快就能够理解作者的设计意图。

注:事后我测试了一下Log4j2日志框架,表现也差不多,磁盘满了也不会对应用程序本身产生影响。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1074592.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OSPF的7大状态和5大报文详讲

- Down OSPF的初始状态 - Init 初始化——我刚刚给别人发Hello报文 我们可以将OSPF邻居建立的过程理解为&#xff1a;我和你打招呼&#xff0c;你和我打招呼&#xff0c;然后咱俩成了邻居 比如&#xff1a; R1和R2要建立OSPF邻居 R1给R2发送了Hello报文&#xff0c;但是R1此时…

如何提升和扩展 PostgreSQL — 从共享缓冲区到内存数据网格

利用共享缓存和操作系统缓存利用 RAM Postgres 是一个基于磁盘的数据库&#xff0c;即使您的整个架构是围绕磁盘访问设计的&#xff0c;利用 RAM 也很重要。如果按照人类规模的延迟来判断&#xff0c;这可以将延迟从几天缩短到几分钟&#xff08;图 1&#xff09;。只需看一下…

【SV中的多线程fork...join/join_any/join_none】

SV中fork_join/fork_join_any/fork_join_none 1 一目了然1.1 fork...join1.2 fork...join_any1.3 fork...join_none 2 总结 SV中fork_join和fork_join_any和fork_join_none; Note: fork_join在Verilog中也有&#xff0c;只有其他的两个是SV中独有的&#xff1b; 1 一目了然 1.…

CCF CSP认证 历年题目自练Day26

题目一 试题编号&#xff1a; 202012-1 试题名称&#xff1a; 期末预测之安全指数 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 512.0MB 题目分析&#xff08;个人理解&#xff09; 还是先看输出&#xff0c;此题简单的离谱&#xff0c;第一行输入小菜有几个测评依据…

1.3 第一个JAVA程序

1.3 第一个JAVA程序 **1.3 第一个Java程序** **前言:** 在1.2节中&#xff0c;我们学习了如何搭建Java开发环境。本节将带你编写并执行你的第一个Java程序。 **1. 编写Java源文件** - 在JDK的bin文件夹下新建文本文档&#xff0c;并重命名为HelloWorld.java。 - 使用记…

Linux命令笔记

终端命令格式&#xff1a; bash command [-options] [parameter] 7个常见Linux命令&#xff1a; 01 ls | list | 查看当前文件夹下的内容 02 pwd | print work directory | 查看当前所在文件夹 03 cd [目录名] | change directory | 切换文件夹 04 touch [文件名] | touc…

使用ChatGPT和MindShow一分钟生成PPT模板

对于最近学校组织的实习答辩&#xff0c;由于时间太短了&#xff0c;而且小编也特别的忙&#xff0c;于是就用ChatGPT结合MindShow一分钟快速生成PPT&#xff0c;确实很实用。只要你跟着小编后面&#xff0c;你也可以快速制作出这个PPT&#xff0c;下面小编就来详细介绍一下&am…

idea(添加jsp文件模板)

代码模板 初始化&#xff1a; <%-- Created by IntelliJ IDEA. User: ${USER} Date: ${DATE} Time: ${TIME} To change this template use File | Settings | File Templates. --%> <% page contentType"text/html;charsetUTF-8" language&…

微服务09-Sentinel的入门

微服务中的雪崩现象 首先&#xff0c;我们介绍一下微服务中雪崩现象&#xff1a;因为微服务中服务是互相调用的&#xff0c;错综复杂&#xff0c;当一个服务D出现问题时&#xff0c;那么调用D的服务请求就会失败&#xff0c;当请求累积到一定的量时&#xff0c;请求D的服务也会…

常见排序算法Java版(待续)

冒泡排序O(n^2) public class Main {public static void main(String[] args) {Random random new Random();int[] nums new int[]{random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100)};for (i…

抽象轻松的java-mybatis简单入门

第一步&#xff1a;用IDEA新建一个java包 第二步&#xff1a;在IDEA中添加数据库&#xff08;ps&#xff1a;自己百度&#xff09; 点击数据库 第二步&#xff0c;新建数据库 选择你使用的数据库 用户与密码根据自己的设置进行配置 为了更方便的查看数据库&#xff0c;可以像图…

一篇短小精悍的文章让你彻底明白KMP算法中next数组的原理

以后保持每日一更&#xff0c;由于兴趣较多&#xff0c;更新内容不限于数据结构&#xff0c;计算机组成原理&#xff0c;数论&#xff0c;拓扑学......&#xff0c;所谓&#xff1a;深度围绕职业发展&#xff0c;广度围绕兴趣爱好。往下看今日内容 一.什么是KMP算法 KMP&#x…

C#对字典容器Dictionary<TKey, TValue>内容进行XML序列化或反序列化报错解决方法

一、问题描述 在使用C#对字典容器Dictionary<TKey, TValue>内容进行XML序列化报错【System.Exception:“不支持类型 System.Collections.Generic.Dictionary2[[System.String, mscorlib, Version2.0.0.0, Cultureneutral, PublicKeyTokenb77a5c561934e089],[System.Strin…

力扣刷题 day39:10-09

1.统计有序矩阵中的负数 给你一个 m * n 的矩阵 grid&#xff0c;矩阵中的元素无论是按行还是按列&#xff0c;都以非递增顺序排列。 请你统计并返回 grid 中 负数 的数目。 方法一&#xff1a;二分法 #方法一&#xff1a;二分法 def countNegatives(grid):res0for nums in…

Altium Designer实用系列(二)----PCB绘图小技巧

一、技巧总结 1.1 丝印大小 在导入PCB之后&#xff0c;元器件的丝印一般都是strock font&#xff0c;个人感觉比较大&#xff0c;也不美观&#xff0c;但是一个个修改成true type又比较麻烦。简便方法是使用相似查找全部修改:   此时会选中所有stroke 类型的丝印&#xff…

提高工作效率!本地部署Stackedit Markdown编辑器,并实现远程访问

文章目录 1. docker部署Stackedit2. 本地访问3. Linux 安装cpolar4. 配置Stackedit公网访问地址5. 公网远程访问Stackedit6. 固定Stackedit公网地址 StackEdit是一个受欢迎的Markdown编辑器&#xff0c;在GitHub上拥有20.7k Star&#xff01;&#xff0c;它支持将Markdown笔记保…

git命令笔记

git命令笔记 前言&#xff1a;git对于软件开发和协作的重要性不言而喻&#xff0c;在企业开发中&#xff0c;git命令和linux命令的使用同样重要。作为开发者&#xff0c;需要牢记并熟练使用常见的git命令 git工作流程图 命令如下&#xff1a; clone&#xff08;克隆&#xf…

论文解析——异构多芯粒神经网络加速器

作者 朱郭益, 马胜&#xff0c;张春元, 王波&#xff08;国防科技大学计算机学院&#xff09; 摘要 随着神经网络技术的快速发展, 出于安全性等方面考虑, 大量边缘计算设备被应用于智能计算领域。首先&#xff0c;设计了可应用于边缘计算的异构多芯粒神经网络加速器其基本结构…

C++ - 可变模版参数 - emplace相关接口函数 - 移动构造函数 和 移动赋值运算符重载 的 默认成员函数

可变模版参数 我们先来了解一下&#xff0c;可变参数。可变参数就是在定义函数的时候&#xff0c;某一个参数位置使用 "..." 的方式来写的&#xff0c;在库当中有一个经典的函数系列就是用的 可变参数&#xff1a;printf&#xff08;&#xff09;系列就是用的可变参…

基于SSM的个人博客系统

实现内容 本系统为用户提供实现了以下功能&#xff1a; 1.登录功能&#xff1a; 系统为单用户系统&#xff0c;为用户分配了用户名和密码。用户必须先登录&#xff0c;进入操作界面。用户输入ID和密码&#xff0c;通过服务器验证方可运行&#xff0c;否则显示消息提示。 2.…