VPS 日志别只靠 tail:journald、logrotate 与轻量集中日志(Loki)实战

服务器日志怎么管才不丢也不爆盘?讲清 systemd journald 持久化、传统 /var/log 文本日志与暴力破解排查、logrotate 轮转,再用 Grafana Loki 做轻量多机集中日志。

刚拿到一台 VPS,很多人查问题的方式就是 SSH 上去敲一句 tail -f /var/log/nginx/access.log,盯着屏幕等出错。这套动作在机器只有一台、服务只有一个的时候确实够用,可一旦你开了第二台、第三台,或者日志量上去了,纯靠 tail 就会暴露三个老毛病:重启就丢(默认 journal 在内存里)、磁盘被吃满(没人轮转)、出事时要在四五台机器之间来回跳。本文想把日志管理这件小事一次说透——先让 journald 把日志稳稳存下来,再讲清传统 /var/log 文本日志怎么读、怎么用它揪出暴力破解,然后用 logrotate 兜底不让它爆盘,最后用 Grafana Loki 把多台 VPS 的日志汇到一处,顺带聊聊保留策略和那些绝对不该写进日志的敏感字段。

延伸阅读

更多相关攻略推荐:Ollama AI系列(2):VPS上的AI推理与API应用Ollama AI系列(2):VPS上的AI推理与API应用ARM / Ampere 席卷 VPS:性价比真香还是兼容陷阱?【VPS 硬件选型指南 (内存 篇) 02】大内存 VPS 能干嘛?2026 独立站 PCI-DSS 自查清单:SAQ A 还是 A-E

一、为什么只靠 tail -f 迟早会出事

tail 本身没有错,它只是个"看当前"的工具,并不负责"存历史"和"跨机器"。先说丢的问题:现代 Linux 大多用 systemd,日志默认走 journald,而 journald 的默认存储位置是 /run/log/journal,这块是 tmpfs,也就是内存里的临时文件系统。机器一重启,里面的日志瞬间清空,你想查"昨天半夜那次崩溃之前发生了什么"基本没戏。再说爆盘:传统文本日志如果你不轮转,Nginx 的访问日志、应用的 debug 输出会以肉眼可见的速度增长,一块 20GB 的小盘跑上几个月就可能被日志塞满,接着服务开始写不进数据、数据库报错、整台机器假死。最后说效率:真出问题的时候,你往往要在 web-01、db-01、cache-01 之间反复 SSH,每台各 tail 一遍,再靠脑子拼时间线,这种排查方式又慢又容易漏。

所以日志管理的目标其实就三条:不丢(持久化)、不爆(轮转+上限)、好查(集中+检索)。下面几节分别对应这三点。

二、journald:把日志收进二进制仓库并持久化

journald 是 systemd 自带的日志服务,它跟传统 syslog 最大的区别是把所有日志收成一个带结构元数据的二进制仓库——每条消息都带着精确时间戳、服务名、PID、UID、优先级等字段,搜索和过滤比纯文本快而且稳,不会因为某一行多了个换行就解析崩。它收集的来源很全:所有 systemd 服务的 stdout/stderr、内核日志(dmesg)、还有传统 syslog 套接字。

关键在存储模式。配置写在 /etc/systemd/journald.conf 的 [Journal] 段,核心参数是 Storage。默认在很多发行版是 auto:目录存在才持久,否则就只放内存。要彻底持久化,最省事的办法是建目录并改成 persistent:

# 启用持久化存储(二选一)
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal

# 或直接在配置里改
sudo nano /etc/systemd/journald.conf
# 把 Storage=auto 改成 Storage=persistent
sudo systemctl restart systemd-journald

光持久化还不够,你还得给日志设"天花板",否则它默认最多吃掉文件系统 10% 的空间。一台预算型 VPS 上我常用的配置是这样:

[Journal]
Storage=persistent
Compress=yes
SystemMaxUse=2G
SystemMaxFileSize=128M
SystemKeepFree=1G
MaxRetentionSec=6month
MaxFileSec=1week
ForwardToSyslog=no
RateLimitIntervalSec=30s
RateLimitBurst=10000

这里 SystemMaxUse=2G 是 journal 总占用上限,SystemKeepFree=1G 是永远给文件系统留 1GB 余量防止被日志挤爆,MaxRetentionSec=6month 表示超过半年的日志自动清掉,Compress=yes 会对大于 512 字节的日志对象压缩,省空间。ForwardToSyslog=no 是因为如果你同时跑着 rsyslog,两边都会记一份,属于重复。RateLimit 那条是给某个服务兜底:30 秒内最多允许 10000 条,超了就限流,避免一个疯狂打日志的 bug 把磁盘写穿。

三、journalctl 日常查询速查

日志存下来了,怎么查才是重点。journalctl 是统一的入口,下面这些是我在排障时最高频的命令,建议直接存进备忘:

journalctl -f                       # 实时跟随,等同 tail -f
journalctl -u nginx.service         # 只看某个服务的日志
journalctl -u nginx -f              # 实时跟随某个服务
journalctl --since "1 hour ago"     # 相对时间
journalctl --since "2026-08-10 09:00" --until "2026-08-10 18:00"
journalctl -p err                   # 只看 err 及以上(3~0 级)
journalctl -p warning..err          # 只看 warning 到 err 区间
journalctl -b                       # 本次启动以来的日志
journalctl -b -1                    # 上一次启动的日志
journalctl --list-boots             # 列出所有可查的启动记录
journalctl -k                       # 只看内核日志(等同 dmesg)
journalctl _UID=1000                # 按用户过滤
journalctl _PID=1234                # 按进程过滤
journalctl --grep="timeout"         # 在 journal 内直接搜关键词
journalctl --disk-usage             # 当前日志占了多少空间

优先级一共 8 级,从 0(emerg,系统不可用)到 7(debug)。日常排障先来一句 journalctl -p err -b 就能把本次启动后的所有错误捞出来,比 grep 全量文本快得多。磁盘快满想手动清理时,用 journalctl --vacuum-size=500M 把总量压到 500MB 以内,或 journalctl --vacuum-time=30d 删掉 30 天前的,再用 --disk-usage 确认效果。想做"变更前后对比"也很顺手:刚改了配置、重启了服务,用 journalctl -u 服务名 -p err -b 看本次启动以来的错误,再配合 --since "10 min ago" 锁定时间窗,比肉眼翻几千行快得多。排查的精髓就在于"给时间、给服务、给级别"三维同时收窄,噪音越小,真相越清楚。

四、传统 /var/log 文本日志与暴力破解排查

虽然 journald 很方便,但传统文本日志并没有消失。如果发行版保留了 rsyslog,/var/log 下会有一堆文本文件:Ubuntu 的 syslog(系统全局)、auth.log(认证/SSH/sudo)、kern.log(内核);RHEL 系对应的则是 messages 和 secure,没有 auth.log 这个文件名。Nginx 这类服务还会在自己的子目录写 access.log / error.log。看实时滚动用 tail -f,翻最后一百行用 tail -n 100,搜错误用 grep -i error。文本日志的好处是你可以用熟悉的 grep/awk 任意加工,这是 journalctl 早期版本不如它的地方。

sudo tail -f /var/log/syslog
sudo tail -n 100 /var/log/nginx/error.log
grep -i error /var/log/syslog

对 VPS 来说,auth.log(RHEL 是 /var/log/secure)是最该盯紧的文件,因为它记着每一次登录尝试。SSH 暴破是常态,用下面这条组合命令就能按次数排出最猖狂的攻击者:

sudo grep "Failed password" /var/log/auth.log
sudo grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -nr | head

揪出来后直接 ufw deny from 该IP,或用 firewalld 的 rich-rule 拒绝。这招比装工具还快,是每个运维的必备肌肉记忆。不过更治本的做法是配合 Fail2Ban:它能自动读取这些失败记录、按阈值临时封禁来源 IP,比手动 grep 省心,也避免了"人不在、机器被爆破"的窗口期。把"定期扫一眼失败登录"变成和"定期备份"一样自然的习惯,你会比故障快半步。

五、logrotate:别让应用文本日志把硬盘吃满

journald 管的是 systemd 服务的结构化日志,但 Nginx、MySQL 这些老牌应用还会往 /var/log 下写自己的文本日志,这部分 journald 不轮转,得靠 logrotate。它是几乎所有 Linux 发行版自带的服务,按天或按大小自动切割、压缩、保留历史,再通知进程重新打开日志文件。

以 Nginx 为例,在 /etc/logrotate.d/nginx 放一份配置即可,无需手写脚本:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 nginx adm
    sharedscripts
    postrotate
        systemctl reload nginx > /dev/null 2>&1 || true
    endscript
}

逐条说一下:daily 每天切一次;rotate 14 保留最近 14 份;compress 用 gzip 压旧日志;delaycompress 让"最新一份"先不压,等下次轮转再压,避免压到一个还在被进程写的文件上;notifempty 空文件不切;create 0640 用规定权限新建日志;postrotate 里 reload Nginx 很关键——不 reload 的话 Nginx 还握着旧文件的文件描述符,日志会一直写进已被改名的历史文件里,新 access.log 反而是空的。改完用 logrotate -d /etc/logrotate.d/nginx 做一遍模拟(不真切),确认无误再用 logrotate -f 强制跑一次。

如果某些日志是按大小涨的(比如某个疯狂打点的服务),把 daily 换成 size 100M 就能做到"到 100MB 才切"。要点就一句:凡是写到磁盘的文本日志,都得有人轮转,否则迟早爆盘。另外有个隐藏坑:有些自己写的服务(比如你写的 Python 脚本)是自己打开文件写日志,logrotate 把文件改名后它还在写那个已经被移走的旧 inode,导致磁盘不释放、新文件却是空的。解决办法是在 logrotate 配置里加 copytruncate(先拷贝再截断原文件),或在 postrotate 里让服务重新打开日志(如 kill -HUP)。这点不理解,你的磁盘清理可能只是自欺欺人。

六、用 Loki + Promtail 把多台 VPS 日志汇到一处

当你有 2 台以上的 VPS,最理想的状态是:在任何一台上都能搜全部机器的日志。Grafana Loki 就是干这个的轻量方案。它的思路和 ELK(Elasticsearch + Logstash + Kibana)相反——Loki 只给日志打"标签"(比如 job、host)做索引,日志正文以压缩块存储,因此内存占用极低,整套跑下来 Loki 本体大约 50MB、Promtail 约 30MB、Grafana 约 100MB,一台 1 核 2G 的小鸡都能扛住;而 Elasticsearch 光自己就要 2GB 起步。代价是全文搜索比 ES 慢一点,但对个人和小团队体量基本无感。

架构很简单:Loki + Grafana 只部署在"中央那台"服务器上,每台要收集日志的 VPS 上跑一个 Promtail 把日志推过去。中央节点的 docker-compose 大致是:

services:
  loki:
    image: grafana/loki:3.4
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yml:/etc/loki/loki-config.yml:ro
      - loki_data:/loki
    command: -config.file=/etc/loki/loki-config.yml
    restart: unless-stopped
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=changeme
    restart: unless-stopped
volumes:
  loki_data:

loki-config.yml 里把保留期打开,否则 Loki 默认不清日志:

auth_enabled: false
server:
  http_listen_port: 3100
common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: inmemory
schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h
limits_config:
  retention_period: 30d
  retention_size: 50GB
compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  retention_delete_delay: 2h

然后在每一台被收集的 VPS 上跑 Promtail(可以同样用容器,也可以直接跑二进制)。它的配置核心是 clients 指向中央 Loki 的地址,以及 scrape_configs 声明要采哪些日志、打什么标签:

server:
  http_listen_port: 9080
positions:
  filename: /tmp/positions.yaml
clients:
  - url: http://LOKI_SERVER_IP:3100/loki/api/v1/push
scrape_configs:
  - job_name: syslog
    static_configs:
      - targets: [localhost]
        labels:
          job: syslog
          host: web-01
          __path__: /var/log/syslog
  - job_name: auth
    static_configs:
      - targets: [localhost]
        labels:
          job: auth
          host: web-01
          __path__: /var/log/auth.log
  - job_name: docker
    static_configs:
      - targets: [localhost]
        labels:
          job: docker
          host: web-01
          __path__: /var/lib/docker/containers/*/*-json.log

注意 host: web-01 这种标签——它正是"跨机器检索"的关键。等所有机器都推上来,在 Grafana 里加一个 Loki 数据源(URL 填 http://loki:3100),到 Explore 里用 LogQL 查:{host="web-01"} 看单台,{job="docker"} |= "error" 搜所有机器的容器错误日志,count_over_time({job="docker"} |= "error" [1h]) 还能按小时统计报错数。把 LOKI_SERVER_IP 换成你中央机的公网或内网 IP 即可,多台机器就是复制同一份配置、改一下 host 标签。

七、保留策略与隐私:敏感字段别进日志

日志能查问题,也能泄密,这条在搭集中日志时特别容易被忽略。先说保留:journald 用 MaxRetentionSec 控时长,logrotate 用 rotate N 控份数,Loki 用 retention_period 控天数,三者各自独立,记得都设上限,别让任何一环无限增长。再说隐私,下面几类东西千万别写进日志:

  • 密码与令牌:登录接口的明文密码、API token、JWT、数据库连接的完整连接串。一旦集中日志被拖库,这些就是现成的提权凭证。
  • 个人身份信息(PII):身份证号、手机号、邮箱、完整银行卡号。很多地区的合规(如 GDPR)对这类数据有硬性留存限制。
  • 完整的会话与 Cookie:Authorization 头、Session ID 写到访问日志里,等于把别人的登录态公开了。

做法上,应用层就该从根上不记敏感字段;如果历史包袱改不动,可以用 Promtail 的 pipeline_stages 做脱敏——用 regex 把密码、token 那一段匹配出来,再用 replace 阶段替换成星号再推给 Loki。另外 labels(标签)虽然方便过滤,但不要拿高基数的值当标签,比如把"每个用户的 ID"设成 label,会导致 Loki 的索引爆炸;标签只放 host、job、level 这种有限集合。最后提醒一句:集中日志平台本身也要设访问密码(Grafana 默认 admin/changeme 一定改掉),它现在是全公司日志的汇总入口,价值极高也风险极高。日志真正的价值不是"事后诸葛亮",而是"事中雷达"——平时就盯着关键指标,在用户投诉之前先发现异常;把 /var/log/auth.log 的失败登录做成定时统计,能比 Fail2Ban 更早看出有人在对你撒网。

延伸阅读:如果你还没给 VPS 做过基础加固,建议先看 VPS 安全基础:从改端口到 Fail2ban;日志之外,定期快照同样重要,可参考 VPS 快照与备份:别等数据丢了才后悔;想看监控告警落地,见 用 Uptime Kuma 给 VPS 做免费监控