记一次 Minecraft 服务器运维:脚本编写漏洞导致的日报数据不全

问题现象:日报只记录了“半天”的活动

我搭建的 Minecraft Fabric 1.20.1 服务器每天重启四次(0、6、12、18 点),并在 23:55 生成一份 Markdown 日报。运行一段时间后,我不幸的发现:日报里的“最近聊天记录”只显示最后几小时的消息,而“活跃玩家列表”中的“最后活动”时间其实是玩家最后一次上线时间,根本不是真正的退出时间。简单来说,日报数据严重不全,几乎失去了参考价值。这令人沮丧,本来我就被脚本妙妙小 bug 折磨,现在又要来修了,于是在精心编写了提示词后与 AI 好好学习了一下,现在总结问题发出来。

为什么会出现数据不全?

要搞清原因,得先了解 Minecraft 服务器的日志机制。

1. 服务器重启会覆盖 latest.log

Minecraft 服务端(包括 Fabric)运行时,会把所有日志实时写入 logs/latest.log。但当服务器重启(无论是正常关闭还是崩溃重启),latest.log 通常会被清空或覆盖。新启动的服务器会创建一个全新的 latest.log,只记录本次启动后的日志。

我之前是打算每天 23:55 生成报告,服务器这之前不重启。但后来为了保证服务器稳定(对钱包的妥协),设置了每天四次定时重启(通过维护脚本)。这就导致:23:55 生成日报时,latest.log 里只保存了最后一次重启(18 点)之后到现在的日志,而 0 点到 18 点之间的所有日志都丢失了。

2. 旧脚本只盯着 latest.log

看看旧的日报脚本是怎么获取日志的(简化版):

LOG_FILE="$SERVER_DIR/logs/latest.log"
TODAY_LOG=$(grep "$TODAY" "$LOG_FILE" 2>/dev/null)
if [ -z "$TODAY_LOG" ]; then
    TODAY_LOG=$(tail -1000 "$LOG_FILE")
fi

这段代码试图从 latest.log 中提取当天的日志行(按日期字符串过滤),如果找不到(比如日志里没有日期前缀,或者重启后日期已变),就退而取最后 1000 行(至少想到了这个备案)。但是显然,无论哪种方式,都只能得到部分日志。

3. 三步之内必有解药:日志归档目录 logs/

实际上,Minecraft 服务端在日志轮转方面做得挺周到(屎山代码居然还有小巧思)——它会将旧的日志自动压缩并存放到 logs/ 目录下(缺点就是积压一堆不大但是碍眼的压缩包),文件名通常像这样:

lllooogggsss///222000222555---000222---222334---121...llloooggg...gggzzz

这些压缩文件里保存着全天的日志,按时间分段。如果日报脚本能读取这些文件,就能拼凑出完整的日志历史。

解决方案:合并所有当天的日志源

新脚本的核心思路是:不再只依赖 latest.log,而是收集当天产生的所有日志文件(包括压缩的归档文件),解压后合并,再按时间排序。这样就能得到从 00:00 到现在的一条完整时间线。

关键代码:fetch_all_logs 函数

下面是从新脚本中摘出的核心函数,这里逐行解释它的作用。

fetch_all_logs() {
    local log_dir="$LOG_DIR"          # logs 目录
    local today="$TODAY"               # 今天的日期,如 2025-02-24
    local temp_file="$1"               # 输出到临时文件

    # 清空临时文件
    > "$temp_file"

    local files=()
    # 添加最新的未压缩日志(如果存在)
    if [ -f "$log_dir/latest.log" ]; then
        files+=("$log_dir/latest.log")
    fi
    # 添加当天日期的压缩文件(格式如 2025-02-23-1.log.gz)
    for gz in "$log_dir"/$today-*.log.gz; do
        if [ -f "$gz" ]; then
            files+=("$gz")
        fi
    done

    if [ ${#files[@]} -eq 0 ]; then
        # 如果没有任何当天日志,用 latest.log 最后 1000 行兜底
        if [ -f "$log_dir/latest.log" ]; then
            tail -1000 "$log_dir/latest.log" > "$temp_file"
        fi
    else
        # 使用 zcat -f 解压所有文件并输出,然后按时间排序
        zcat -f "${files[@]}" 2>/dev/null | sort -t '[' -k2 > "$temp_file"
    fi
}

解释:

  • files=():建立一个数组,准备存放所有需要读取的日志文件。
  • 先加入 latest.log(如果存在)。
  • 再用通配符 $today-*.log.gz 匹配所有以今天日期开头的压缩日志文件,逐一加入数组。
  • 如果数组为空(可能服务器刚装好,还没产生归档日志),就回退到 tail -1000 latest.log 做保底。
  • 核心魔法zcat -f 可以同时处理普通文本文件和 gzip 压缩文件(-f 强制让 zcat 对非压缩文件也直接输出)。"${files[@]}" 把所有文件路径传给 zcat,它会将所有文件内容解压后输出到标准输出。
  • 由于每个日志行都以 [HH:MM:SS] 开头,我们可以用 sort -t '[' -k2 按时间排序(-t '[' 表示以 [ 作为分隔符,-k2 表示按第二个字段排序)。这样就能得到严格按时间顺序排列的完整日志流。
  • 最后将排序后的内容存入临时文件 $temp_file

如何使用这个完整日志流?

有了 $TEMP_LOG(临时文件),后续的分析就可以基于它进行。例如,在分析玩家上线区间时,我们遍历每一行,用状态机匹配 joined the gameleft the game,就能准确得到每次登录和登出的时间,计算在线时长。聊天记录提取也直接从这个完整日志流中取最后 50 条,自然覆盖全天。

一点小巧思:处理未退出的玩家

在遍历完整日志时,如果到文件末尾还有玩家未登出(比如日志截止时他还在线),我们用当前时间作为“登出时间”,并标记“未退出”。这保证了数据的完整性。

为什么这样就能解决“日报数据不全”?

  • 覆盖全天logs/ 下的压缩文件保存了从当天第一次启动到现在的所有日志段,合并后就能还原全天的历史。
  • 不怕重启:即使服务器重启多次,每次重启前的日志都已被归档,不会丢失。
  • 时间顺序正确sort 保证了日志行的顺序,即使文件被分段,也能拼接成正确的时间线。

总结与建议

  1. 别只盯着 latest.log:对于会定期重启的服务器,一定要考虑日志归档文件。Minecraft 默认会将旧日志压缩,善用这些资源。
  2. 善用 zcatsort:这两个命令组合可以轻松处理多个压缩日志文件,并保证时间顺序。
  3. 研究一下日志轮转策略:如果你的服务器日志轮转方式不同(比如自定义了 log 4 j 配置),可能需要调整匹配模式。但大多数默认配置都适用上述方法。

通过这次优化,日报终于能完整反映全天的玩家活动了(大概,反正我也没仔细检查)。当然,如果读者也遇到类似问题,希望这篇文章能给以启发。

运维无小事,每一个细节都可能影响最终结果的准确性。多动手、多思考,脚本就会越来越健壮。

杂谈:关于 AI 的使用

AI 确实是个好东西,但不是万能的,而且对他的结果也取决于使用者,我可以做一个比喻:想要达到目的,就相当于从一个地方到另一个地方,我们使用者应该做的首先是具有一张地图(充分了解技术路径),告诉 AI 自己预想的路线,或者自己需要避开的东西,比如不走高速。

然后精心编写一份提示词,(也可以用自然语言书写,再喂给 AI 让他转换格式,更加条理分明)并且尽量提供自己本身的精细信息,越精细越好,比如自己出发时间或者能接受的通勤时间等等。

这样一股脑喂给 AI,AI 才能发挥最大作用:为你细化整个路径。

而很多初学者使用 AI 就只是单纯的告诉 AI 自己在哪,自己想去哪个地方,那 AI 就会凭借他的主观给你找出一条路线出来。这条路线不一定会是最近的,而且可能会跑到高速上去(如果你不想上的话那就是大麻烦)。同时如果提供信息不详细,他还会错估你的形式,造成完全错误的答案。

这就是 AI 的使用哲学,它不是一个十分优秀的引路人,他应当是一个细化者或者拓展者,你必须要指明方向,甚至规划路径,用最细节的语言表述自己,这个时候 AI 才会成为一个趁手的工具。

就比如这一次问题,实际上就是因为我本人对于我的世界日志轮转不了解,只是干巴巴的命令 AI 去搞一个每日报告系统出来,于是他就一臆想了一份不会由于服务器重启删除的日志,从而导致了这一次错误。如果我知道这一运转逻辑,在提示词里面提到,那么他就会很自然的避开这一路径。

所以说 AI 永远只是一个好用的工具,至少现在是。

他的水平完全取决于他的使用者。


如果你有任何问题或更好的思路,欢迎给我发邮件进行交流。