Promtail 采集宿主机 Nginx 日志重复采集与时间不准问题排查
Promtail 采集宿主机 Nginx 日志重复采集与时间不准问题排查
背景
有 2 台 Nginx 服务器不在 k8s 集群内,并且由于网络原因不方便与 k8s 集群的日志采集服务打通。
现在采取的收集 Nginx 日志的方案是:在 k8s 一个节点上通过 rsync 将 Nginx 服务器的 Nginx 日志同步到 k8s 节点,Promtail 收集 k8s 节点上的日志。
rsync 通过 k8s DaemonSet 方式运行在一个节点,存放 Nginx 日志路径通过 hostPath 挂载路径到宿主节点
现象
- 重复采集:日志并非增量收集,Promtail 每次都是全量重新读取,导致 Loki 中出现大量重复日志。
- 时间筛选不准:按时间范围查询日志时结果不准确(也有现象1的原因)。
配置
宿主机 Nginx 日志通过 rsync 每 30s 同步一次到本地目录(供 Promtail 采集):
原始rsync命令:
rsync -azvh --delete \
-e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /app/id_rsa -p 22' \
root@<ip>:/var/log/nginx/ data/nginx-log/<ip>/
原始 Promtail 抓取 job 配置:
- job_name: nginx-host-logs
static_configs:
- targets:
- localhost
labels:
__path__: /data/nginx-log/*/*.log
job: nginx-host-logs
app: nginx
log_type: access
pipeline_stages:
- regex:
source: filename
expression: '/data/nginx-log/(?P<host>[^/]+)/(?P<log_file>[^/]+\.log)'
- regex:
source: filename
expression: '/data/nginx-log/[^/]+/(?P<app>[^.]+)\.(?P<log_type>access|error)\.log'
- regex:
source: filename
expression: '/data/nginx-log/[^/]+/(?P<log_type>error)\.log$'
- labels:
host:
log_file:
app:
log_type:
根因
1. 重复采集
rsync 默认更新已存在文件的方式是:写一个临时文件,传输完成后 rename() 覆盖到目标路径。
- 目标文件路径不变,但每次同步都会产生一个新的 inode。
- Promtail 的文件 tailer 依赖 inotify 监听 inode;inode 被替换后,Promtail 认为是「新文件」,从头读取,而不是从
positions.yaml记录的偏移量继续读。
2. 时间筛选不准
nginx-host-logs job 原来的 pipeline_stages 只做了文件名正则提取 host/app/log_type 标签,没有解析日志行内容里的真实时间,Loki 存储的 entry 时间戳是 Promtail 读到该行的时间(ingestion time),而不是 Nginx 记录请求/错误发生的真实时间。叠加问题 1 的重复读取,历史日志会被打上「当前」时间,时间筛选完全失真。
修复方案
rsync 改为原地更新
在 rsync 命令中增加 --inplace:
rsync -azvh --inplace --delete \
-e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /app/id_rsa -p 22' \
root@<ip>:/var/log/nginx/ data/nginx-log/<ip>/
- 原地写入,inode 不变:rsync 直接在目标文件上更新,不再创建临时文件 + rename,每次同步目标文件 inode 保持不变,Promtail 能从
positions.yaml记录的偏移量继续读,不会误判为新文件。 - 源端发生截断(如日志轮替):
--inplace会将目标文件截断到与源端一致的大小,仍是同一 inode(文件变小),Promtail 通过「当前大小 < 已记录偏移量」判定截断,从 0 重新读取,不会造成大规模重复。
关于日志轮替:源端 Nginx 按天轮替产生的备份文件命名为 access.log.1 / error.log.1 这类(结尾非 .log)。Promtail 的采集路径 __path__: /data/nginx-log/*/*.log 是标准 glob,只匹配以 .log 结尾的文件,access.log.1 不会被匹配采集,因此这些历史备份文件不会被重复读取,无需额外增加 rsync exclude 规则。
pipeline_stages 增加 timestamp 解析
在 nginx-host-logs job 中,按 log_type 分别解析真实日志时间:
- access 日志:格式
... [$time_local] ...,如[11/Jul/2026:10:57:55 +0800](含时区偏移,Go format:02/Jan/2006:15:04:05 -0700)。 - error 日志:行首格式
2026/07/13 01:42:48 [crit] ...(不含时区,需显式指定location,当前配置为Asia/Shanghai,若宿主机时区不同需同步修改)。
- job_name: nginx-host-logs
static_configs:
- targets:
- localhost
labels:
__path__: /data/nginx-log/*/*.log
job: nginx-host-logs
app: nginx
log_type: access
pipeline_stages:
- regex:
source: filename
expression: '/data/nginx-log/(?P<host>[^/]+)/(?P<log_file>[^/]+\.log)'
- regex:
source: filename
expression: '/data/nginx-log/[^/]+/(?P<app>[^.]+)\.(?P<log_type>access|error)\.log'
- regex:
source: filename
expression: '/data/nginx-log/[^/]+/(?P<log_type>error)\.log$'
- labels:
host:
log_file:
app:
log_type:
- match:
selector: '{log_type="access"}'
stages:
- regex:
expression: '\[(?P<timestamp>\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} [+-]\d{4})\]'
- timestamp:
source: timestamp
format: '02/Jan/2006:15:04:05 -0700'
- match:
selector: '{log_type="error"}'
stages:
- regex:
expression: '^(?P<timestamp>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})'
- timestamp:
source: timestamp
format: '2006/01/02 15:04:05'
location: 'Asia/Shanghai'
修复后,Loki 存储的 entry 时间 = 日志行内真实时间,按时间范围查询结果准确。
两个方案叠加的说明
- 两个方案作用在不同环节(rsync 同步方式 / Promtail 解析逻辑),互不冲突,可同时生效。
- 历史已入库的重复数据 / 错误时间戳数据不会被自动清理,仅能防止修复后的新数据继续出问题;如需清理历史脏数据,需走 Loki compactor 的 delete API。
- 切换到
--inplace时,如果目标端文件内容与源端已完全一致,rsync 校验和比对后不会写入任何数据,positions.yaml偏移量不受影响,可平滑过渡,不会引发一次性全量重读。