凌晨两点被磁盘告警电话叫醒,登录服务器一看,数据盘使用率已经飙到96%。不用多想,又是Nacos日志清理脚本这类需求该安排上了——尤其是“保留最近14天日志”这个指标,在微服务体系里几乎成了标配。先声明,这不是一篇“复制粘贴就能跑”的敷衍文章,我会把日志文件来源、为什么不能直接rm、find参数怎么定、定时任务怎么落地、出问题怎么排查串起来讲清楚,把这几年在Nacos日志清理上踩过的坑一次说透。
长话短说,如果你是运维或后端开发,正在为Nacos日志疯狂占磁盘发愁,这篇文章就是给你准备的。看完你不仅能写出一份能上生产环境的清理脚本,还能理解每条命令背后的原理,遇到诡异情况也能自己排。
1. 磁盘告警那晚:Nacos日志到底是怎么把空间吃掉的
很多人以为Nacos日志就是start.out,实际上Nacos的日志体系比想象中复杂。我见过最夸张的一台2C4G的测试机,跑了一个月Nacos集群,日志占了快30G,start.out连零头都算不上。真正的大头是Nacos内部组件各自输出的日志文件。
1.1 日志不是只有start.out,还有一堆你没注意过的文件
以Nacos 2.x为例,日志默认落在${nacos.home}/logs目录下,常见的文件有这几类:
| 文件或模式 | 来源组件 | 特点 |
|---|---|---|
start.out | 启动脚本stdout/stderr重定向 | 通常比较大,但不在logs目录下 |
access_log.yyyy-MM-dd.log | HTTP请求访问日志 | 按天滚动,文件名带日期 |
naming.log/naming.log.yyyy-MM-dd.log | 服务注册发现模块 | 当前活跃文件+历史滚动文件 |
config.log/config.log.yyyy-MM-dd.log | 配置中心模块 | 同上 |
cluster.log | 集群节点通信 | 有时会刷新很频繁 |
alipay-jraft.log | JRaft协议层 | 选举和复制日志,容易爆炸 |
embedded-derby.log | Derby嵌入式存储 | 仅当使用内嵌存储时存在 |
nacos_gc.log | JVM GC日志 | 滚动依赖HeapDump配置 |
这套文件基本对应了Nacos的注册中心、配置中心、JRaft共识协议三大模块。最气人的是alipay-jraft.log,它不像access_log那样按天切,而是单个文件增长,日志级别一开DEBUG,几天就能给你写满整个磁盘。所以写清理脚本时,别只盯.log通配符,文件名规则每个版本都可能不一样,要看清实际目录再动手。
1.2 哪些日志文件才是真正的“空间杀手”
实际生产环境里,我最怕的不是naming.log,而是access_log和alipay-jraft.log。
access_log每天都会记录所有HTTP请求,包括客户端注册、心跳、配置拉取、控制台操作。一个中型微服务集群如果有几百个服务实例,每30秒心跳一次,一天下来的access_log可以轻松到几百MB甚至上G。想象一下:每天一个几百MB的文件,14天就是10多个G,这还只是一个节点。如果集群有三台、五台节点,容量就成倍翻。
alipay-jraft.log比较特殊,它记录Raft协议层的状态变化。节点选举、leader切换、日志复制失败重试,都会写它。一旦网络抖动或节点频繁加入,这个文件会异常膨胀。我见过某个环境里它一天从200M涨到2G。为什么提它?因为很多人写清理脚本时只命中了access_log和naming.log,把alipay-jraft漏了,结果磁盘还在悄悄变小。
1.3 增长异常的信号:短时间内刷出几个G的场景
除了正常的按天滚动,有几个场景会让Nacos日志短时间暴涨。第一,控制台或客户端频繁发起配置变更,config.log会持续写入。第二,服务节点大批量上下线,naming.log和alipay-jraft会跟着刷。第三,权限校验失败反复报错,access_log会记录大量401响应。第四,最典型的是Nacos和客户端版本不兼容,导致心跳响应异常,客户端疯狂重连,access_log每秒几十行“request error, please try again later!”(这个报错也是官网常见热搜词之一,我后面会专门提排查思路)。
所以判断一个清理脚本合不合格,不是看它能不能删文件,而是看它能不能在日志暴涨时仍稳定工作。基于这个目标,我们设计的脚本必须只处理历史文件,绝不动正在被写入的活跃文件。
2. 直接执行rm不行的真实原因:被进程占用的文件删了也不释放
先说结论:动态删除了一个正在被Nacos写入的日志文件,磁盘空间不会立即释放。这不是脚本写得不对,而是Linux的文件删除机制在“骗”你。很多新手第一次遇到这个问题时,会下意识认为是服务器出故障了。
2.1 文件删除与空间释放的“两个时间点”
在Linux里,一个文件被删除其实涉及两个步骤:移除目录项(unlink)和释放inode与数据块。只有当文件的引用计数降为0时,数据块才会真正归还文件系统。如果某个进程已经打开这个文件,持有file descriptor,即使你执行rm -f,内核仍然认为这个文件还被使用中,数据块就不会释放。
可以把这个过程类比成酒店退房:你在前台把房卡退了(rm),但房客还住在里面(进程持有fd),酒店就不能把这个房间重新卖出去。磁盘空间对于正在运行的Nacos来说,就像那位还在房间里的住客,得等住客出门——也就是进程关闭fd或退出——房间才能腾出来。
2.2 验证实测:lsof +N +1能看到什么
判断是否有进程占用已删除的日志文件,最常用的命令是lsof。lsof +L1可以列出所有被删除但仍被进程打开的文件。我一般这样用:
lsof +L1 | grep nacos输出的文件中,你会看到naming.log路径后面跟着一个(deleted)标记。这说明这个文件已经被rm了,但因为应用进程还在写入,空间还没释放。如果想定位得更准,也可以这样:
lsof -p $(pgrep -f nacos | head -n 1) | grep deleted-p指定进程PID,grep deleted过滤被删除文件。看到结果的那一刻你就能理解:不是rm没用,是内核觉得文件还有“主人”。
2.3 为什么清理脚本反而不能重启Nacos来解决
有朋友会想:既然重启能释放所有被占用的fd,那我每天凌晨重启一次Nacos不就行了?这种方式确实能把空闲的空间还回来,但代价很大。Nacos作为注册中心和配置中心,重启意味着所有客户端断连,触发一遍重新注册和配置拉取,虽然阈值内影响可控,但生产环境这种操作终究是冒风险的。
更合理的方案是:脚本只删除已经不再被写入的历史日志。Nacos当前活跃日志是naming.log、config.log这类不带日期或带当日日期的最新文件,它们保留;带历史日期的旧文件即使还被某个fd引用,也不影响——因为进程不会再往那些文件里写数据了,文件内容已经定格。删除它们之后,只要对应fd一直没关闭,空间还是不会释放。这个问题怎么解决?答案很简单:别让活跃fd指向旧文件。Nacos自己的Logback滚动机制会在跨天时创建新文件并切换fd,所以历史文件此刻已经没有活跃fd了。换句话说,凌晨跑清理脚本删除的是“已经被Logback自己归档、不再使用的旧文件”,这时unlink之后空间能正常释放。如果担心某些组件没有按天滚动,比如alipay-jraft.log是单文件滚动,那判断方式就要看mtime,而不是只看文件名是否带日期。
3. 保留14天的清理脚本:从find参数到完整代码
脚本本身不复杂,难的是边界条件设计。以下是我在线上跑了两年多的版本,核心逻辑是:定位到Nacos日志目录,找出所有按日期滚动、且mtime超过14天的历史日志文件,逐一删除,并记录删除结果。
3.1 核心命令find -mtime +14拆解
find的-mtime参数是按“文件内容最后修改时间”来过滤的,单位是天。注意,-mtime +14表示“超过14天”,-mtime 14表示“正好14天”,-mtime -14表示“14天以内”。我们只清理超过14天的,也就是字面意思的“保留最近14天日志”。为什么用+14而不是14?因为+14是严格大于,更符合“超过14天就删”的语义。
使用-mtime有个容易被忽略的点:它受文件系统时间和系统时间影响。如果服务器时区错了或clock漂移,判断就会偏差。我的建议是,在脚本里先检查系统时间,再用date -d确认当前日期,确保cron执行环境里的时间正常。另外,find要配合-type f避免匹配到目录,配合-name限制文件后缀,避免误删非日志的元数据文件。
3.2 排除项设计:哪些文件千万不能动
清理脚本最容易翻车的地方就是“误删”。我总结过几类不能动的文件:
- 带当天日期或正在活跃写入的
access_log.当前日期.log、naming.log、config.log。这些是Nacos当前正在写的文件,删了会导致fd指向已删除文件,日志写入暂时记录不到磁盘,而且空间不释放,表面看磁盘没变,实际埋雷。 cluster.conf、raft.conf这类配置和快照文件,删了会导致集群元数据损坏,重启都起不来。- 数据库文件或Derby存储目录,如
derby-data、derby.log。如果你用的Nacos内嵌存储,这些文件承载了配置数据,绝不能按照“过期日志”逻辑清理。 - 目录本身和子目录。
logs目录下可能有config、naming等子目录,find -type f一般不会误删目录,但如果你加了-exec rm -rf且没写-type f,后果很严重。
所以我的脚本里用了双重保险:先按文件名正则匹配带日期的日志,再按mtime过滤,最后在删除前再确认一次扩展名和日期格式。宁可漏几个文件,也不能误删一个关键数据。
3.3 完整脚本与逐行说明
下面这段脚本就是我在生产环境中使用的精简版,去掉了一些内部加密密钥相关的部分,核心逻辑完整:
#!/bin/bash # Nacos 日志清理脚本 - 保留最近14天日志 # 适用环境: Linux + Nacos 2.x # 建议放到 nacos 用户 crontab 中,或在 root crontab 中以 nacos 用户身份执行 set -u NACOS_LOG_DIR="${NACOS_LOG_DIR:-/opt/nacos/logs}" RETENTION_DAYS="${RETENTION_DAYS:-14}" DRY_RUN="${DRY_RUN:-0}" if [ ! -d "$NACOS_LOG_DIR" ]; then echo "[ERROR] Nacos log dir not exist: $NACOS_LOG_DIR" exit 1 fi CURRENT_DATE=$(date +%Y-%m-%d) LOG_FILE="/var/log/nacos_log_clean.log" # 核心查找逻辑: # 1. 只匹配普通文件 (-type f) # 2. 文件名必须符合日志滚动规则(带日期后缀) # 3. mtime 超过 14 天 # 4. 排除当前活跃日志文件 FILES=$( find "$NACOS_LOG_DIR" -type f \ -regextype posix-awk \ -regex '.*(access_log|naming\.log|config\.log|cluster\.log|alipay-jraft\.log)(\.[0-9]{4}-[0-9]{2}-[0-9]{2})?.*\.log([0-9.-]*)?$' \ -mtime +"$RETENTION_DAYS" \ 2>/dev/null \ | grep -v "_${CURRENT_DATE}_" || true ) DELETED_COUNT=0 DELETED_SIZE=0 while IFS= read -r file; do [ -z "$file" ] && continue # 二次保护:跳过活跃日志文件 if [ "$(basename "$file")" = "naming.log" ] || \ [ "$(basename "$file")" = "config.log" ] || \ [ "$(basename "$file")" = "cluster.log" ]; then echo "[SKIP] active log: $file" continue fi size=$(stat -c%s "$file" 2>/dev/null || echo 0) if [ "$DRY_RUN" -eq 1 ]; then echo "[DRY_RUN] would delete $file (size=$size)" else if rm -f "$file"; then DELETED_COUNT=$((DELETED_COUNT+1)) DELETED_SIZE=$((DELETED_SIZE+size)) echo "[$(date '+%F %T')] deleted $file (size=$size)" >> "$LOG_FILE" else echo "[ERROR] failed to delete $file" >> "$LOG_FILE" fi fi done <<< "$FILES" echo "[$(date '+%F %T')] summary: deleted=$DELETED_COUNT size=$DELETED_SIZE bytes" >> "$LOG_FILE"解释几个关键点:
set -u会在引用未定义变量时直接报错,防止因为环境变量缺失导致路径变成空字符串,把根目录扫了。变量NACOS_LOG_DIR和RETENTION_DAYS支持从外部覆盖,这样一个脚本多套环境都能用。这两个是不同服务器环境差异最大的点,写成可配置能省很多事。
find的-regextype posix-awk改成了更通用的POSIX风格正则,匹配文件名中带yyyy-MM-dd日期的滚动日志。grep -v "_${CURRENT_DATE}_"排除了今天日期,防止删到当前活跃文件。最稳的办法其实是干脆把当前日期的文件名全部跳过,因为Nacos的滚动日志文件名里会带日期,比如access_log.2025-06-01.log。
为什么脚本里还单独加了一个“跳过naming.log/config.log/cluster.log”的判断?因为有些版本滚动机制并不按日期生成新文件,而是固定文件内部滚动。虽然正则已经匹配到了带日期的文件名,但二次判断永远不会嫌多,尤其是当别的工作人员手动复制了一个文件叫naming.log.bak时,这种保护能救你。
这里我也要专门说明:如果你使用的是Nacos较老版本(如1.x),日志文件名后缀可能是yyyy-MM-dd.HH甚至带小时。没关系,正则里[0-9]{4}-[0-9]{2}-[0-9]{2}已经覆盖日期部分。判断标准始终是“这文件是不是已经被Logback归档、不再参与写入”。
4. 把脚本安全地交给Cron:定时执行与落盘细节
脚本写出来永远不是终点,怎么把它稳定、安全地交给系统调度,才是生产环境的真正考验。我见过有人直接把脚本放在/root下,用root crontab执行,然后由于Nacos日志是nacos用户创建的,出现Permission denied。也见过有人用nacos用户跑,但log文件路径没有写权限,导致执行记录丢失。
4.1 Cron表达式与执行时间选择
我自己常用的计划是每天凌晨4点30分执行:
30 4 * * * /usr/local/bin/clean_nacos_logs.sh >> /var/log/nacos_log_clean_runtime.log 2>&1为什么要挑凌晨?因为凌晨通常是业务低峰期,Nacos日志写入量小,清理任务对IO的影响最小。虽然删除文件本身不占太多系统资源,但find会扫描整个日志目录,如果文件数量成千上万,IO还是有一点的。白天跑容易碰到突发请求,深夜跑最稳。另外建议大家给cron任务加上环境变量,因为cron子进程的环境变量极简:
SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin很多人踩过这个坑:脚本在终端里手动执行正常,放进cron就报错,找来找去发现是PATH里没有/usr/bin。明明find就在那里,却提示command not found。这种问题很蠢,但真的很常见。
4.2 加锁防止脚本并发重叠执行
如果某一天的执行特别慢,比如文件太多、磁盘变慢,下一次cron触发时上一次还没跑完,两个find同时扫描目录,有可能出现重复删除或IO抖动。为了避免这个问题,我建议用flock给脚本加一个文件锁:
#!/bin/bash exec 9>/var/lock/nacos_log_clean.lock if ! flock -n 9; then echo "[$(date '+%F %T')] another instance is running, skip." exit 0 fi把这个逻辑放在脚本开头。flock -n是非阻塞模式,拿不到锁就直接退出,绝对不会并发执行。文件锁文件本身很小,不占空间。这里的9是文件描述符编号,随便选一个不常用的就行。
4.3 与日志滚动策略(logrotate)配合的边界
很多服务器上默认有logrotate,它主要负责对系统日志进行滚动、压缩、删除。那么Nacos的日志能不能直接交给logrotate管?可以,但要注意边界。Nacos自身已经内置了Logback按天滚动,它滚动时会把当前文件改名为带日期的旧文件,再新建一个空白的当前文件。这个机制运行得很好,你不用重复设置按天滚动,logrotate如果也按天滚,反而可能把正在写的文件切掉,导致Nacos写入句柄出错。
所以我的建议是:Nacos的日志清理交给自己的脚本,logrotate只负责处理start.out这类由shell重定向生成、没有滚动能力的文件。分工清楚,两者不互相干扰。start.out有时也被称为Nacos服务的标准输出重定向文件,默认位置在/opt/nacos/start.out,可能不在logs目录里。你要么在清理脚本里单独加一段,要么用logrotate每天分割它,别指望一个脚本面面俱到把所有文件都管了。
5. 跑真实环境前先做三遍验证
新脚本上生产前,我会强制自己按照“dry-run、小范围试删、全量执行”三个步骤走。三个步骤都通过,才能交给cron长期运行。
5.1 先跑dry-run看输出清单
脚本里的DRY_RUN=1就是在这一步用的:
DRY_RUN=1 ./clean_nacos_logs.sh它会打印出每个“将要删除”的文件路径和大小,但实际上不做删除。这一步的价值是:你可以在删除前检查有没有误报。比如文件名正则会不会匹配到某个奇怪的快照,或者当天文件有没有被误判成过期文件。我建议你把输出保存下来,人工用ls -lh随机抽查几个文件,确认它们确实是历史日志,而且时间确实超过14天。
5.2 清理前后df对比与误删排查
确认清单没问题了就正式执行。执行前先记录目录占用:
du -sh /opt/nacos/logs df -h /opt/nacos/logs执行脚本后,再跑一遍这两条命令。正常情况下,目录占用会下降,磁盘使用率也会下降。有些人只关注磁盘总量,却忽略了du的输出。如果du下降但df没下降,说明有文件还被进程占着fd。这种场景多发生在当天日志上——如果脚本逻辑有问题,把刚滚动的活跃文件也删了,就会出现“ls看不到文件,但磁盘没释放”的诡异局面。这时用lsof +L1 | grep nacos查一下,基本能定位是哪个文件卡住了。
5.3 遇到“删不掉”和被重新生成的日志怎么办
实际执行中还有两种现象会让你怀疑脚本是不是坏了。
第一种是“删不掉”,rm -f返回失败。最常见的原因是文件属主和权限问题,比如Nacos以nacos用户运行,而脚本以root执行时由于SELinux或特殊挂载选项无法删除;或者日志目录被设置为只读挂载。这种情况看脚本日志里的[ERROR]输出,再用lsattr检查文件是否被加入了i属性(immutable)。chattr -i可以在确认安全后解除,但你得先搞清楚为什么文件会被加i,不能无脑解。
第二种是“删了又出现”。这是正常的:只要Nacos在运行,access_log当前日期文件每天都会生成。你删除的只是14天前的历史文件,当天和最近14天的文件会一直存在。如果某天你发现删除之后第二天文件又出现在logs目录里,而且文件名还是过期日期,那说明Nacos的日志时间有问题,比如服务器时钟跑慢了一天,导致Logback认为现在是过去的某一天,生成了旧日期文件。这时要检查系统时间和时区配置。我曾经在一个测试环境碰到过类似问题,最后发现是虚拟机休眠后时钟漂移导致的。
还有一个建议:保留一份模板文件在白名单里。比如在脚本里维护一个KEEP_FILES列表,每当看到新的日志文件名格式时,把它加到这个列表中。这样即使Nacos小版本升级导致日志文件命名规则变化,脚本也不会把新格式文件当成垃圾误删。
6. 容器化与多节点集群的额外提醒
如果你的Nacos部署在Docker或K8s环境里,清理脚本的逻辑不变,但注意几个差异。
容器里Nacos日志通常落在两个位置:一种是容器内部路径,默认一样是/home/nacos/logs或/opt/nacos/logs;另一种是你自己挂载的宿主机目录,比如NFS卷。如果日志目录是emptyDir,容器重启日志就没了,理论上不需要清理脚本。但实际生产上为了排查问题,很多人会挂载hostPath或PVC,日志是持久化的,磁盘增长问题就回来了。
对于容器场景,我建议直接在宿主机上跑清理脚本,因为容器内部往往没有cron,而且容器镜像精简到连find都不一定有。脚本里的目录路径改成宿主机挂载的实际路径即可。一个小细节:如果多个Nacos副本共享同一个日志目录,而每个副本都会写入access_log,那么文件名可能带有access_log.2025-06-01.log之外还带实例标识的后缀。这时候正则里要把实例标识也考虑进去,或者放宽为按-name "*access_log*"匹配。
集群模式下还有个问题容易被忽略:三节点Nacos集群的日志总量是单节点乘以节点数,磁盘规划和清理频率要按这个来算。我一般用这个粗略公式估算:单日日志大小×保留天数×节点数×1.3缓冲区。如果算出来空间紧,可以把保留天数从14天降到7天,但前提是确认没有审计合规方面的要求。access_log里记录了大量客户端访问信息,如果有安全审计需要,建议保留30天以上。运维决策不能只看磁盘,还得看业务规则。
另外,无论单机还是集群,我都会在监控面板上加一个磁盘使用率的告警阈值,通常是80%。清理脚本是“事后补救”,监控告警才是“事前发现”。脚本再稳,也有被人误删或cron异常停掉的意外,告警能让你在用户发现之前先发现问题。
Nacos控制台经常报错的“request error, please try again later!”也和日志清理有关。如果你清理日志时误删了当前活跃的access_log,会让控制台请求日志丢失,排查问题时看不到任何访问记录。我自己遇到过几次这种场景,后来学乖了:清理前先看一眼最新日志文件是否存在,清理后立刻确认控制台能正常打开。如果控制台打不开且日志里出现大量“request error, please try again later!”,优先检查磁盘是否满了,以及access_log文件是否存在。
总结一下我这两年多的运维体会:日志清理脚本的本质不是“删文件”,而是理解文件生命周期、认清进程与文件的引用关系、把定时任务做得足够安全。保留14天只是一个参数,真正值钱的是那几条保护规则和验证流程。你可以在我的脚本基础上改保留天数、改路径,但千万别删掉那些“跳过活跃文件”和“dry-run”的部分。磁盘满了能救回来,数据文件被误删了,真的就只能哭。