生产事故-误删文件开发运维险被同时开除

· 浏览次数 : 0

小编点评

## 问题分析 该问题是由于系统清理操作导致临时文件目录被清理,但 POI 的 DefaultTempFileCreationStrategy 保留了对该目录的引用,导致后续的读取操作失败。 **主要原因:** 1. 系统清理操作默认只清理 /tmp 目录,而 POI 的 DefaultTempFileCreationStrategy 在创建临时目录时会使用 tempfiles.d 文件作为参考。 2. 由于 tempfiles.d 文件可能长期没有读写,导致 POI 无法正常创建临时目录。 3. 开发人员在开发模块时可能忘记创建临时目录或者使用完临时目录后误删除了。 **解决方案:** 1. 更改 tempfiles.d 的清理规则,指定要清理的目录,例如 /tmp 目录。 2. 使用自定义的 TempFileCreationStrategy,确保临时目录创建时会重新创建。 3. 修改 POI 的 DefaultTempFileCreationStrategy,指定临时目录的创建路径。 4. 改善代码,避免在创建临时目录时误删除临时目录。 5. 与运维人员沟通并提供详细的错误日志,帮助定位问题。

正文

入职多年,面对生产环境,尽管都是小心翼翼,慎之又慎,还是难免捅出篓子。轻则满头大汗,面红耳赤。重则系统停摆,损失资金。每一个生产事故的背后,都是宝贵的经验和教训,都是项目成员的血泪史。为了更好地防范和遏制今后的各类事故,特开此专题,长期更新和记录大大小小的各类事故。有些是亲身经历,有些是经人耳传口授,但无一例外都是真实案例。

注意:为了避免不必要的麻烦和商密问题,文中提到的特定名称都将是化名、代称。

0x00 大纲

0x01 事故背景

2024年5月10日下午临近下班时分,同事Y正准备收拾东西下班回家。正所谓“麻绳总挑细处断,厄运专找苦命人”,说时迟那时巧,突然一旁的座机在不恰当的时间恰当地响起,同事Y极不情愿地拿起听筒,电话那头是项目经理的声音,音量MAX,听起来情绪不算稳定,看戏的群人心想:多半是出事了。原来是业务人员收到客户投诉,某个导出数据的功能无法使用,由于时间紧任务重,必须尽快(加班加点)修复。

0x02 事故分析

首先按照标准流程,先让运维拷贝和固定了事故系统日志及生产版本应用包。发现该功能已经在两个基线版本没有变更更过了,这么说来,不是变更升级引起的问题了。屋漏偏逢连夜雨,麻烦总比办法多。看来今晚不只是同事Y不能准时下班了……

故障的现象很简单,就是前端点击页面导出按钮之后,后端查询数据并生成Excel文件时报错,据客户说分时段尝试了几次,都不成功。我们在日志上也确实看到了几个相关的异常日志,与客户描述的时点基本一致。让人费解的是异常信息全是类似这样的:

Exception in thread "Thread-1" java.lang.IllegalStateException: java.nio.file.NoSuchFileException: /tmp/42376e0c-b088-4816-9d9d-8fac49614625/poifiles/poi-sxssf-sheet8012028719757675239.xml
	at org.apache.poi.xssf.streaming.SXSSFWorkbook.createAndRegisterSXSSFSheet(SXSSFWorkbook.java:696)
	at org.apache.poi.xssf.streaming.SXSSFWorkbook.createSheet(SXSSFWorkbook.java:712)
	at org.apache.poi.xssf.streaming.SXSSFWorkbook.createSheet(SXSSFWorkbook.java:104)
	at com.alibaba.excel.util.WorkBookUtil.createSheet(WorkBookUtil.java:86)
	at com.alibaba.excel.context.WriteContextImpl.createSheet(WriteContextImpl.java:223)
	at com.alibaba.excel.context.WriteContextImpl.initSheet(WriteContextImpl.java:182)
	at com.alibaba.excel.context.WriteContextImpl.currentSheet(WriteContextImpl.java:135)
	at com.alibaba.excel.write.ExcelBuilderImpl.addContent(ExcelBuilderImpl.java:54)
	at com.alibaba.excel.ExcelWriter.write(ExcelWriter.java:73)
	at com.alibaba.excel.ExcelWriter.write(ExcelWriter.java:50)
	at com.alibaba.excel.write.builder.ExcelWriterSheetBuilder.doWrite(ExcelWriterSheetBuilder.java:62)
	(这里省略10086行堆栈信息)

分析该日志,可以知道该服务使用的Excel导出组件为EasyExcel,其底层实现是POI 4以上(使用了SXSSFWorkbook模型),目录名称使用了UUID,文件名则存在类随机数的组成。

看到NoSuchFileException或者FileNotFoundException这两兄弟就知道应该是引用了不存在的文件路径。服务器上尝试ls查看该目录,发现除了/tmp/根目录,另外两个子目录居然都不存在,难道是哪个可爱的同事忘了创建临时目录或者不小心删除了临时目录。经过观察,所有的报错都指向相同的目录前缀/tmp/42376e0c-b088-4816-9d9d-8fac49614625/poifiles/, 本着先恢复再查错的原则,决定先手工创建临时文件目录尝试解决该故障,重建目录后,客户反馈数据导出正常。

0x03 事故原因

前面怀疑是同事Y在开发相关模块时,忘记创建临时目录或者使用完临时目录后误删除了。然而事实并非如此,项目中使用组件准确版本号为EasyExcel 3.3.4POI 5.2.5,查看对应的代码,发现临时目录是由组件自动维护的,根本不需要开发人员进行维护(这里是个坑,为后面埋下了伏笔)。其中有两个地方的代码很关键,一段位于POIDefaultTempFileCreationStrategy中:

    public static final String POIFILES = "poifiles";
private volatile File dir;

private void createPOIFilesDirectory() throws IOException {
    if (dir == null) {
        dirLock.lock();
        try {
            if (dir == null) {
                String tmpDir = System.getProperty(JAVA_IO_TMPDIR);
                if (tmpDir == null) {
                    throw new IOException("System's temporary directory not defined - set the -D" + JAVA_IO_TMPDIR + " jvm property!");
                }
                Path dirPath = Paths.get(tmpDir, POIFILES);
                dir = Files.createDirectories(dirPath).toFile();
            }
        } finally {
            dirLock.unlock();
        }
    }
}

@Override
public File createTempFile(String prefix, String suffix) throws IOException {
    createPOIFilesDirectory();
    File newFile = Files.createTempFile(dir.toPath(), prefix, suffix).toFile();
    if (System.getProperty(DELETE_FILES_ON_EXIT) != null) {
        newFile.deleteOnExit();
    }
    return newFile;
}

可以看到POI的管理策略为创建临时文件时会先去检查一遍临时目录的创建情况,没有创建则自动创建,并且不会删除这个目录。看到这里可能有人会有疑问,前面提到的带UUID的目录是哪里来的,没错,UUIDEasyExcel干的,目的是为了防止一台机器运行多个应用时,共用一个临时文件夹可能存在读写权限的问题(大部分时候创建目录默认权限是07440755,除非你用root用户启动应用服务……)。在EasyExcelFileUtils中实现了该优化:

    private static String tempFilePrefix =
        System.getProperty(TempFile.JAVA_IO_TMPDIR) + File.separator + UUID.randomUUID().toString() + File.separator;

    public static void createPoiFilesDirectory() {
        File poiFilesPathFile = new File(poiFilesPath);
        createDirectory(poiFilesPathFile);
        TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(poiFilesPathFile));
    }

排除了开发和组件的问题,接下来就不得不怀疑是不是运维人员背刺了。由于临时文件位于系统临时目录,如果目录长时间没有读写,可能会被tmpfiles.d服务清理,事故服务器上执行cat /usr/lib/tmpfiles.d/tmp.conf查看:

#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

# See tmpfiles.d(5) for details

# Clear tmp directories separately, to make them easier to override
q /tmp 1777 root root 10d
q /var/tmp 1777 root root 30d

这下问题就定位清楚了,长时间没有读写的临时目录被系统清理了,但是POIDefaultTempFileCreationStrategy还保持了对该目录的引用,以至于后续不会再自动创建临时目录,等再次使用时就会触发FileNotFoundException或者NoSuchFileException,定位了问题,解决方案也简单:

  • 更换临时目录的位置。通过修改java.io.tmpdir属性来修改临时目录路径,影响的代码范围太大,不推荐。
  • 修改tmpfiles.d的清理规则。通过修改tmpfiles.d的清理周期或添加白名单,影响的机器数量太多,不推荐。
  • 自定义TempFileCreationStrategy。利用POI提供的扩展点,注册自定义的Strategy,每次使用临时目录前检查是否存在,影响最小,果然还得是开发,命够苦。

扩展点位于POITempFile工具类中:

public final class TempFile {
    private static TempFileCreationStrategy strategy = new DefaultTempFileCreationStrategy();

    public static void setTempFileCreationStrategy(TempFileCreationStrategy strategy) {
        if (strategy == null) {
            throw new IllegalArgumentException("strategy == null");
        }
        TempFile.strategy = strategy;
    }

    // 这里省略部分代码
}

通过调用TempFile.setTempFileCreationStrategy,即可替换为自定义的临时文件创建策略。

0x04 事故复盘

如果开发和运维沟通到位,或者运维手册严格按照规范来书写,这本是一次可以避免的生产事故。在双方无法直接沟通或者只能有限沟通的情况下,或许可以进一步强化配置测试和引入待机测试?来发现此类问题,减少此类事故。

0x05 事故影响

事故影响较小,同事Y和相关人员被迫延迟两小时下班。第二天被老板知道了,差点把运维和开发都开除,项目负责人和运维负责人连夜编写事故报告一份。

与生产事故-误删文件开发运维险被同时开除相似的内容:

生产事故-误删文件开发运维险被同时开除

入职多年,面对生产环境,尽管都是小心翼翼,慎之又慎,还是难免捅出篓子。轻则满头大汗,面红耳赤。重则系统停摆,损失资金。每一个生产事故的背后,都是宝贵的经验和教训,都是项目成员的血泪史。为了更好地防范和遏制今后的各类事故,特开此专题,长期更新和记录大大小小的各类事故。有些是亲身经历,有些是经人耳传口授

[转帖]炸了~Redis bigkey导致生产事故-bigkey问题全面分析

文章首发于公众号:BiggerBoy 原文链接 一个Redis生产事故的复盘,整理这篇文章分享给大家。本期文章分析Redis中的bigkey相关问题,主要从以下几个点入手: 文章目录 什么是bigkey?bigkey的危害bigkey的产生如何发现bigkey实际生产的操作方式 如何优化bigkey

[转帖]炸了~Redis bigkey导致生产事故-bigkey问题全面分析

https://blog.csdn.net/ibigboy/article/details/124216874 文章首发于公众号:BiggerBoy 原文链接 一个Redis生产事故的复盘,整理这篇文章分享给大家。本期文章分析Redis中的bigkey相关问题,主要从以下几个点入手: 文章目录 什么

记一次 .NET 某拍摄监控软件 卡死分析

一:背景 1. 讲故事 今天本来想写一篇 非托管泄露 的生产事故分析,但想着昨天就上了一篇非托管文章,连着写也没什么意思,换个口味吧,刚好前些天有位朋友也找到我,说他们的拍摄监控软件卡死了,让我帮忙分析下为什么会卡死,听到这种软件,让我不禁想起了前些天 在程序员桌子上安装监控 的新闻,参考如下: 我

[转帖]线程池使用的10个坑

https://juejin.cn/post/7132263894801711117 前言 大家好,我是捡田螺的小男孩。 日常开发中,为了更好管理线程资源,减少创建线程和销毁线程的资源损耗,我们会使用线程池来执行一些异步任务。但是线程池使用不当,就可能会引发生产事故。今天田螺哥跟大家聊聊线程池的10

记一次 .NET某账本软件 非托管泄露分析

一:背景 1. 讲故事 中秋国庆长假结束,哈哈,在老家拍了很多的短视频,有兴趣的可以上B站观看:https://space.bilibili.com/409524162 ,今天继续给大家分享各种奇奇怪怪的.NET生产事故,希望能帮助大家在未来的编程之路上少踩坑。 话不多说,这篇看一个.NET程序集泄

煤矿安全大模型:微调internlm2模型实现针对煤矿事故和煤矿安全知识的智能问答

煤矿安全大模型————矿途智护者 使用煤矿历史事故案例,事故处理报告、安全规程规章制度、技术文档、煤矿从业人员入职考试题库等数据,微调internlm2模型实现针对煤矿事故和煤矿安全知识的智能问答。 本项目简介: 近年来,国家对煤矿安全生产的重视程度不断提升。为了确保煤矿作业的安全,提高从业人员的安

【Azure 事件中心】Event Hubs如何获取其中存放的历史消息

问题描述 使用Azure Event Hub服务,除了正常的生产,消费消息以外,如果想拿到Event Hub中存储的历史消息?有什么方法呢? 问题解答 获取 Event Hubs 存储的历史消息,首先需要确保消息进入Event Hub的时间处于保留期限(Retention Days)内,因为超过这个

抽丝剥茧:详述一次DevServer Proxy配置无效问题的细致排查过程

事情的起因是这样的,在一个已上线的项目中,其中一个包含登录和获取菜单的接口因响应时间较长,后端让我尝试未经服务转发的另一域名下的新接口,旧接口允许跨域请求,但新接口不允许本地访问(只允许发布测试/生产的域名访问)。 问题 那么问题来了,本地环境该如何成功访问到新的接口并验证业务功能是否生效呢? 尝试

公有云降本增效最佳实践

前言 最近看到了几个事情,一个是某保险系统,为了快速上线,全量上云,结果生产正式运行后每月账单高达几十万。相关业务总扛不住这个支出,又劳师动众,让下面的项目经理、开发、运维、架构师花了3个月把业务全量从公有云迁移下来。相关人员被折磨的半死不活,而且大大拖慢了系统的迭代速度。 另一个是某个电商的案例,