Vector 日志采集管道实战:几十 MB 内存替代 ELK,多机日志汇聚、VRL 脱敏与 Loki 落地

日志采集,别一上来就上 ELK

每个自建站点的站长迟早会遇到一个需求:把多台服务器上的访问日志、错误日志、系统日志收拢到一处,能查、能统计、能告警。绝大多数教程会直接推荐 ELK(Elasticsearch + Logstash + Kibana)。但对一台 2G 内存的小 VPS 来说,光是 Elasticsearch 就吃掉 1G 起步,Logstash 基于 JVM,启动慢、内存高,往往还没开始分析日志,服务器先被压垮了。

Vector 是这几年崛起的一个日志与指标采集器,用 Rust 写成,单个二进制文件就能跑,常驻内存几十 MB。它的核心概念是“数据管道”:source(数据从哪来)→ transform(怎么处理)→ sink(发到哪去)。配置用 TOML 描述,比 Logstash 的配置清爽,性能却高出一个数量级。这篇文章用一个真实场景把 Vector 讲透:把三台 VPS 的 Nginx 访问日志、错误日志和 syslog 统一采集、结构化、脱敏,然后落到本地文件、推给 Loki,并对异常做一条告警。

在读之前请先明确一点:Vector 负责“采集与转换”,它不是存储也不是查询界面。查询那一步可以交给 Loki+Grafana,或者简单点直接落盘用 grep/lnav 看。理解这条边界,配置就不会跑偏。

为什么是 Vector 而不是 Filebeat

Filebeat 是 Elastic 官方出品,轻量、稳定,很多人以为它和 Vector 差不多。区别主要在三点。第一,转换能力:Filebeat 的处理能力(processors)比较弱,做字段提取、格式改写、条件过滤时常常得靠 ingest pipeline 后置;Vector 的 VRL(Vector Remap Language)是一门专门的数据转换小语言,能在采集端就把日志揉成你要的形状,省掉下游一大堆处理步骤。第二,多目标输出:Filebeat 主要面向 Elasticsearch/Logstash;Vector 一个 source 可以同时扇出到 file、Loki、ClickHouse、S3、Kafka 等几十种 sink,做“一份数据多路分发”非常自然。第三,资源占用:Vector 用 Rust 写,内存和 CPU 开销普遍比 Filebeat 更低,尤其在高吞吐场景差距明显。

对个人站长来说,只要你有“采集→转换→分发到多个地方”的需求,Vector 的性价比就很高。如果你的日志全部进 ES、场景极简,Filebeat 也够用,没有必要为了新潮而迁移。

安装:一个二进制搞定

Vector 官方提供各平台的安装脚本,Debian/Ubuntu 上:

bash -c "$(curl -L https://sh.vector.dev)" -- install vector
# 或直接用 deb 包
curl -1sLf 'https://packages.timber.io/vector/0.40/vector-0.40-1.x86_64.deb' -o vector.deb
dpkg -i vector.deb

装完确认版本:vector --version。Vector 的配置默认在 /etc/vector/vector.toml,也可以用 /etc/vector/vector.yaml 或 .json。它支持 TOML/YAML/JSON 三种写法,本文用 TOML。改完配置先 vector validate /etc/vector/vector.toml 校验,再 systemctl restart vector。校验这一步能救你很多次——VRL 语法错误、字段名拼错都会在这里报出来。

第一步:采集本机 Nginx 访问日志

Nginx 默认的 access_log 是 combine 格式,Vector 有一个内置的 nginx_ingress/demo_logs 之外,对 nginx 访问日志最省事的是先给 Nginx 换个 JSON 格式的日志,再直接当 JSON 解析。在 Nginx 里定义:

log_format json_combined escape=json
  '{"time":"$time_iso8601","remote_addr":"$remote_addr",'
  '"request":"$request","status":$status,"body_bytes":$body_bytes_sent,'
  '"referer":"$http_referer","ua":"$http_user_agent",'
  '"rt":$request_time,"upstream_rt":"$upstream_response_time"}';

access_log /var/log/nginx/access.log json_combined;

这样每行日志本身就是合法 JSON,Vector 的 file source 加上 decoding.codec = "json" 就能直接结构化,无需正则解析,速度快、不易错。Vector 配置:

[sources.nginx_access]
type = "file"
include = ["/var/log/nginx/access.log"]
read_from = "beginning"

[sources.nginx_access.decoding]
codec = "json"

注意 read_from = "beginning" 只在首次采集时读历史,之后 Vector 会记住文件偏移(checkpoint),重启不会重复读。file source 还支持 ignore_older_secs、max_line_bytes 等参数,日志被 logrotate 切割后它会自动跟上新文件,这点比手写脚本稳妥得多。

第二步:用 VRL 做转换与脱敏

采集只是第一步,真正体现 Vector 价值的是 transform。我们把访问日志里几个字段改写一下:把 status 转成整数并打上“是否错误”的标记,把时间戳的字符串转成标准时间,去掉 referer 里的查询串避免泄漏隐私,并对 UA 做一个粗略归类。

[transforms.clean]
type = "remap"
inputs = ["nginx_access"]
source = '''
  .http.status = to_int!(.status)
  del(.status)

  .is_error = .http.status >= 500
  .is_slow  = to_float!(.rt) > 1.0

  # 统一时间字段
  .ts = parse_timestamp!(.time, format: "%+")

  # referer 去掉 query,只看来源域
  if exists(.referer) {
    .referer_host = parse_url(.referer).host || ""
  }
  del(.referer)

  # 简单 UA 归类
  .client = "other"
  if contains(string!(.ua), "bot") || contains(string!(.ua), "spider") {
    .client = "bot"
  } else if contains(string!(.ua), "Mobile") {
    .client = "mobile"
  } else {
    .client = "desktop"
  }
  del(.ua)
'''

VRL 的语法要点:. 代表当前事件(一条日志),.foo 读写字段,del(.x) 删除,! 表示“出错就终止这条日志的处理”(不加 ! 则出错返回 null)。上面用了 to_int! 等函数,注意 VRL 里数字和字符串要显式转换,"$status" 如果没转格式,Nginx 的 JSON 里 status 是数字类型,我们这里为稳妥仍做了 to_int。

脱敏的关键在 .referer_host:只保留来源域名,把可能含用户信息的 query string 丢掉。同理,如果日志里有 IP,行业合规上一般建议做掩码,可以加 .remote_addr = replace!(.remote_addr, r'\.\d+$', ".0") 把最后一段打码。

第三步:多路输出——落盘 + 推 Loki

Vector 最舒服的地方是一个 remap 结果可以被多个 sink 同时消费。我们做两路:一路按日期落本地文件做归档,一路推给 Loki 做检索。

[sinks.to_file]
type = "file"
inputs = ["clean"]
path = "/var/log/aggregated/access-%Y-%m-%d.log"
encoding.codec = "json"

[sinks.to_loki]
type = "loki"
inputs = ["clean"]
endpoint = "http://127.0.0.1:3100"
labels.source = "nginx"
labels.host = "{{ host }}"
encoding.codec = "json"

Loki 侧只需要一个最简的 loki-local-config.yaml,用 docker 跑起来:

docker run -d --name loki -p 3100:3100 \
  -v /opt/loki:/etc/loki grafana/loki:2.9 \
  -config.file=/etc/loki/loki-config.yaml

Grafana 里添加 Loki 数据源,就能用 LogQL 查询,比如 {source="nginx"} | json | http_status >= 500 查所有 5xx。注意上面的 labels 里用了 {{ host }} 模板变量,这是 Vector 的内置元数据,能自动填当前主机名——这样多台机器推同一个 Loki 时可以按主机区分。

第四步:多台 VPS 的日志怎么汇聚

上面是本机采集。如果你有三台机器,有两种汇聚方式。一是让每台机器上的 Vector 各自推 Loki(endpoint 指向中心 Loki 的公网地址,加 TLS 与 basic auth)。二是让中心机器跑一个 Vector 做 vector source 接收其他机器发来的数据,做二次分发。第二种更适合要做全局关联分析的场景:

# 边缘机器:发到中心
[sinks.to_center]
type = "vector"
inputs = ["clean"]
address = "center.example.com:6000"
compression = true

# 中心机器:接收
[sources.from_edges]
type = "vector"
address = "0.0.0.0:6000"

传日志同样要加密。Vector 的 vector source/sink 支持 TLS,在中心机器配好证书,边缘配 tls.enabled = true 与 CA 路径即可。这样跨公网传输的日志不会裸奔。压缩用 compression = true(gzip)能显著降低带宽。

第五步:磁盘队列防丢 + 一条告警

网络抖动、Loki 重启都可能让日志在内存 buffer 里丢失。Vector 提供磁盘持久化缓冲,遇到下游不可用就先落盘,恢复后重放。给 sink 加:

[sinks.to_loki.buffer]
type = "disk"
max_size = 268435488   # 256MB
when_full = "block"

when_full = "block" 表示缓冲区满时阻塞上游而非丢弃,是“宁可慢也不丢”的选择。max_size 别设太大,注意给磁盘留余量。

再加一条基于错误的告警。Vector 本身可以发到 http sink 触发 webhook,配合 filter transform 只放行错误日志:

[transforms.only_5xx]
type = "filter"
inputs = ["clean"]
condition = '.is_error == true'

[sinks.alert]
type = "http"
inputs = ["only_5xx"]
uri = "https://hooks.example.com/alert"
method = "post"
encoding.codec = "json"

这样只有 5xx 日志会被推给告警端点,不会淹没在正常流量里。如果想做“同一条错误 5 分钟内不重复告警”,更专业的做法是把告警交给 Loki 的 ruler 或 Alertmanager,Vector 只负责把 5xx 单独送出去。

第六步:错误日志与系统日志一并收进来

访问日志只是半边天,真正出问题时救命的往往是错误日志和系统日志。Vector 的 file source 可以一次声明多个路径,把 Nginx error.log 与 fastcgi 的错误输出一起采进来:

[sources.app_errors]
type = "file"
include = ["/var/log/nginx/error.log", "/var/log/php*-fpm.log"]

[transforms.tag_errors]
type = "remap"
inputs = ["app_errors"]
source = '''
  .log_type = "app_error"
  # 提取日志级别,方便后续过滤
  .level = "info"
  if contains(string!(.message), "error") { .level = "error" }
  if contains(string!(.message), "warn")  { .level = "warn"  }
'''

系统日志则用 journald source 直接读 systemd 的日志,不需要碰文件路径,也无需 root 单独授权:

[sources.syslog]
type = "journald"
units = ["sshd.service", "nginx.service", "php8.2-fpm.service"]
current_boot_only = true

把这三类日志(访问、应用错误、系统)用同一个 labels.source 之外再加 log_type 字段区分,推给 Loki 后在 Grafana 里就能用一个查询面板分别统计。SSH 的登录失败、Nginx 的 worker 异常、PHP-FPM 的池满警告,全都汇进同一条时间线,排查时不用再一台台机器 tail -f。

几个容易踩的坑

1. 时区。Nginx 的 $time_iso8601 默认是服务器本地时区,解析成 timestamp 时没问题,但推给 Loki 后 Grafana 展示会按 UTC 对齐,看着像差了 8 小时。统一在日志里输出带时区偏移的时间,或在 VRL 里用 parse_timestamp 明确指定时区。

2. 字段类型。VRL 是强类型的,to_int!(.status) 遇到空值会终止整条记录。字段可能缺失时,要么用 to_int(.status) ?? 0,要么先判断 exists。上线前用 vector test 跑一遍样例数据。

3. 内存与批处理。transform 会在内存里批量处理,默认 batch 有上限。日志量特别大时,调小 batch.max_events、调大磁盘缓冲,避免内存飙升触发 OOM Killer。

4. 权限。Vector 默认以 vector 用户运行,读不到 /var/log/nginx/(通常只有 root 和 adm 组可读)。要么把 vector 加进 adm 组,要么放宽目录权限,否则会出现“配置没错但一条日志也收不到”的诡异现象。

5. checkpoint 路径。file source 的读取位置记录在 /var/lib/vector/。如果你迁移或重建了 Vector 容器但保留了这个目录,重启后会从旧偏移继续,可能漏掉期间的日志——排查“日志断档”时要想到这里。

小结

Vector 把“采集日志”这件事做成了一个描述清晰的数据管道:source 接源头,remap 用 VRL 揉形状,sink 分发到任意目的地,外加持久化缓冲兜住网络抖动。对个人站长来说,它最大的价值是用几十 MB 内存就替代了动辄吃 1G+ 的 ELK 采集链路,而且配置比 Logstash 直观得多。真正上手只需要记住三件事:源、转换、汇;配置写完先 validate;日志字段能输出成 JSON 就别用正则硬解。把这套管道搭好之后,服务器上再出什么幺蛾子,你至少有据可查,而不是对着一堆分散的日志文件干瞪眼。

Last modification:October 5th, 2026 at 12:28 pm

Leave a Comment