做可观测性这行,绕不开日志采集。最近我把一套夜莺(Nightingale)监控环境的日志全部接到VictoriaLogs里,采集管道用的是Vector。这套组合不算热门,但实测下来很稳,资源占用比那些“全家桶”方案低一截。如果你也在维护夜莺,或者正在纠结日志该往哪送,这篇内容可以直接照着抄。
夜莺本身是个监控系统,但它自己也产生日志,比如服务端n9e的运行日志、告警事件、登录审计。以前排查问题只能ssh到机器上tail,几台机器轮着看,效率很低。我的目标很明确:把夜莺所在机器的日志统一采集,做成一个可检索的集中式日志平台,同时尽量少占资源、少引入额外维护成本。最后定下的方案就是Vector采集 -> VictoriaLogs存储查询。
这篇内容适合正在使用夜莺监控、又想做日志集中管理的运维或SRE同学。里面会讲方案对比、组件原理解读、从零到一的部署步骤、VRL处理细节,还有我实测遇到的坑和排查思路。如果你只是想快速搭一套夜莺日志采集,直接跳到第三章把配置文件复制过去改改路径就行,但我建议还是把原理看一遍,遇到问题才知道往哪个方向排查。
1. 项目背景与整体方案拆解
1.1 夜莺日志为什么需要集中采集
夜莺监控系统本身承担着指标采集、告警判定、通知发送等工作,它的运行状态直接影响整个监控体系的可信度,但大家往往只关注夜莺“监控了谁”,忽略了夜莺自己被监控的需求。我接手这套环境时,夜莺是标准的二进制多节点部署,一台server、一台边缘节点,日志散落在各自机器的部署目录里。真要排查一次告警丢失或者webhook发送异常,得先回忆日志在哪个路径,再挨个节点登录、tail、翻找,效率非常低。
更麻烦的是日志轮转。夜莺二进制部署的日志轮转主要靠logrotate或者运维脚本,轮转之后旧文件被改名,如果不做归档,时间一长就查不到历史记录。告警事件虽然在数据库里有记录,但具体到某条告警的通知响应过程、回调结果、失败原因,数据库里根本不体现,这些细节只存在于日志文件里。于是集中日志采集就变成了刚需。
1.2 方案选型:为什么是Vector加VictoriaLogs
可能有人会问“ELK那么成熟,为什么不用?”我在方案评审时也认真对比过几个主流选择。先明确一点,我们的目标是采集夜莺的日志,而不是搞一个公司级的大数据日志平台,资源占用和运维复杂度是首要考量。
| 采集端 | 资源占用 | 处理能力 | 综合感受 |
|---|---|---|---|
| Logstash | JVM系,内存至少1GB起步 | 插件丰富,处理能力强 | 太重,在多节点部署成本高 |
| Filebeat | 较轻 | 管道处理能力有限,复杂解析别扭 | 适合轻量采集,复杂逻辑不好写 |
| Fluent Bit | 轻 | 中等,插件不少 | 勉强够用,但稍复杂逻辑配置较绕 |
| Vector | 单二进制,内存几十到几百MB | VRL脚本表达能力很强 | 轻量且灵活,明显匹配这个场景 |
我选Vector有三个具体原因。一是它的Source、Transform、Sink三件套抽象非常直观,配置是TOML格式,比Logstash的管道配置容易读,也比Fluent Bit的插件链更结构化。二是VRL这个脚本语言处理日志真的很顺手,解析JSON、正则提取、时间格式转换、条件分支都能在一个transform里完成。三是Vector本身也支持指标采集,后续如果想把夜莺的性能数据也走同一条管道送出去,不需要再引入新组件。
存储端没有选Elasticsearch,原因也很现实。ES需要集群、需要单独调JVM堆、要管理索引模板和mapping,对一个小团队来说维护成本划不来。Loki用起来确实简单,但日志内容检索能力偏弱,而且需要follow组件或者promtail配套。VictoriaLogs是VictoriaMetrics团队做的日志数据库,单文件二进制,默认端口9428,插入和查询都是HTTP接口,语法走Lucene风格,全文检索体验很接近ES,但安装维护简单得多。对夜莺日志这种“类型不固定、格式多变”的场景,它不强求schema,收到JSON lines就直接建立倒排索引,非常灵活。
1.3 最终架构与数据链路
整个数据链路是这样跑的:
夜莺节点上的日志文件 -> Vector的file source读取 -> remap transform做解析和字段规整 -> http sink发送 -> VictoriaLogs的/insert/jsonline接口写入 -> 用户通过VictoriaLogs的/select/lucene查询
这套架构里需要长期维护的可执行文件只有两个:Vector单二进制和VictoriaLogs单二进制。中间没有消息队列、没有Redis、没有协调服务,故障点非常少。夜莺节点上只需要放一个Vector,VictoriaLogs单独部署在一台机器上,数据集中存储、集中查询。后面所有配置和踩坑记录都是基于这条链路展开的。
2. 核心组件原理与关键细节
2.1 Vector的source/transform/sink体系
Vector把采集链路抽象成三件事:数据从哪来、数据怎么改、数据往哪去。这就像一个净水系统:source是进水口的水龙头,transform是中间的滤芯,sink是出水口接的水桶。理解了这个抽象,后面配置无论多复杂都能快速归类。
这个项目里我用了三个组件。第一个是file类型的source,负责读本地日志文件。file source有一个很关键的设计:它会把当前读取位置记录在磁盘上,进程重启后能接着读,不会因为重启而重复或丢失。它还支持glob匹配文件、忽略旧文件、处理文件轮转。第二个是remap类型的transform,负责执行VRL脚本,所有日志解析和字段规整都在这里做。第三个是http类型的sink,负责把处理后的日志事件编码成JSON lines,批量POST到VictoriaLogs。
这里有个容易忽略的点:http sink本身没有太多业务逻辑,它的核心复杂度在请求参数上。批量大小、提交间隔、并发数、超时时间都会影响写入吞吐和VictoriaLogs的负载。网上很多教程只给个最小配置,上线后发现日志量大了推送不过来,就是这个原因。
2.2 夜莺日志的常见格式与位置
夜莺的官方文档在监控官网上有完整说明,其中数据模型和告警规则章节最值得先读。二进制部署的夜莺,解压后的目录通常在/opt/n9e或者/home/n9e,日志目录一般叫log或者logs。比较常见的日志文件包括n9e.log(服务端主日志)、webapi.log(API层日志)、collector.log(采集器日志)等。
夜莺的日志格式在不同版本里略有差异。老版本大多是纯文本,典型的一行长这样:
2024/07/19 10:00:00 [INFO] AlertRuleManager run ok, ruleCount=128新版本部分模块会输出JSON格式,字段类似{"ts":"2024-07-19T10:00:00+08:00","level":"info","msg":"...","module":"..."}。如果部署方式是docker-compose,容器内的日志目录需要提前用volume挂载宿主机目录,Vector直接采集宿主机的挂载目录就行。另外,夜莺日志的轮转策略不统一,有的机器配了logrotate按天轮转,有的只是按大小切割,Vector的file source对轮转有专门检测逻辑,后面配置时会说到。
2.3 VictoriaLogs的接口与数据模型
VictoriaLogs和ES最大的区别是不需要预建索引模板,也不强制mapping。它的写入接口是POST到/insert/jsonline,请求体是JSON lines格式,每行一条日志。它有几个保留字段需要知道:
- _msg:日志正文,全文搜索时默认查这个字段
- _time:RFC3339格式的时间戳,如果不传就用服务端接收时间
- _stream_id:后台用来标识数据流,如果两条日志内容完全一样会被去重
查询接口是/select/lucene,语法是Lucene风格。比如查包含error关键字的日志就写query=error,查指定级别就写level:"error",时间范围可以用_time:[now-1h, now]过滤。字段级的精确匹配和全文检索可以混用,这对夜莺场景很实用,比如查某条告警事件,用alert_name:"内存告警"把范围缩小,再搜body里的关键内容。
3. 从零搭建采集链路的完整实操
3.1 环境规划与版本选择
我先交代一下实测环境的规模。夜莺集群有三台节点,一台server、两台edge节点,日志量不大,峰值每秒几百条,一天大概几十GB。VictoriaLogs单独部署在一台4核8G的虚拟机上,Vector直接装在夜莺所在的三台节点上。这样数据尽量本地采集,网络传输链路短,VictoriaLogs挂了也不会影响夜莺节点上Vector的采集缓存。
版本方面,Vector当时用0.36.x稳定版,VictoriaLogs用v0.39.x。安装方式我这边全部用二进制加systemd管理,没有用docker。原因是二进制文件只有一个,systemd的unit文件写起来也简单,底层文件读取的权限控制更加直观。日志文件目录权限如果不对,Vector很容易启动成功却什么都读不到。
3.2 部署VictoriaLogs
VictoriaLogs部署非常简单,下载压缩包解压就能跑:
mkdir -p /opt/victoria-logs && cd /opt/victoria-logs wget https://github.com/VictoriaMetrics/VictoriaLogs/releases/download/v0.39.3-victorialogs/victoria-logs-linux-amd64-v0.39.3-victorialogs.tar.gz tar -xzf victoria-logs-linux-amd64-*.tar.gz启动命令要指定数据目录和监听端口:
./victoria-logs-prod -storageDataPath=/data/victoria-logs-data -httpListenAddr=:9428 -retentionPeriod=30d我加了一个-retentionPeriod=30d参数,意思是日志保留30天。这个参数不是必须的,但日志平台如果不设保留时间,磁盘总有一天会被填满。数据目录/data/victoria-logs-data要提前建好并授予运行用户写权限。我用systemd管理,unit文件里指定User=vlogs:
[Unit] Description=VictoriaLogs service After=network.target [Service] ExecStart=/opt/victoria-logs/victoria-logs-prod -storageDataPath=/data/victoria-logs-data -httpListenAddr=:9428 -retentionPeriod=30d Restart=on-failure User=vlogs [Install] WantedBy=multi-user.target启动后验证接口:
curl http://localhost:9428/select/lucene?query=*返回一个空结果JSON列表就说明服务正常。
3.3 安装Vector
Vector的官方安装脚本会自动配置好apt或yum源:
curl -1sLf 'https://repositories.timber.io/public/vector/cfg/setup.sh' | bash -s -- -y安装完成后,默认配置目录在/etc/vector,配置文件是/etc/vector/vector.toml。如果不想用官方源,也可以下载deb包,或者直接用docker跑:
docker run -d --name vector \ -v /home/n9e/log:/home/n9e/log:ro \ -v /etc/vector:/etc/vector:ro \ timberio/vector:latest-alpine我这边用systemd方式管理,直接使用二进制安装后的服务即可。装好后可以执行vector --version确认版本。顺便说一句,搜Vector资料的时候很容易搜到C++的std::vector,还有汽车电子领域的Vector CANoe工具链,这完全是两码事。看文档时认准“timberio/vector”和“Vector Remap Language”,避免浪费时间。
3.4 编写Vector配置
现在到核心环节。我在/etc/vector/vector.toml里写了这样一个完整配置:
[sources.n9e_file] type = "file" include = ["/home/n9e/log/n9e.log", "/home/n9e/log/webapi.log"] read_from = "beginning" ignore_older_secs = 600 [transforms.n9e_meta] type = "remap" inputs = ["n9e_file"] source = ''' .meta.host = get_hostname!() .meta.file = .file ?? "unknown" .meta.service = "n9e-server" ''' [transforms.n9e_parse] type = "remap" inputs = ["n9e_meta"] source = ''' if starts_with!(.message, "{") { parsed = parse_json(.message) ?? {} if parsed != {} { .msg = parsed.msg ?? parsed.message ?? .message .level = parsed.level ?? "info" ._time = parse_timestamp!(parsed.ts, format: "%+") ?? now() del(.message) } } else { parsed_re = parse_regex!(.message, r'^(?P<ts>[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}) [.*?]*(?P<level>[A-Z]+) (?P<content>.*)') if length(parsed_re) == 4 { ._time = parse_timestamp!(parsed_re[1], format: "%Y/%m/%d %H:%M:%S") ?? now() .level = downcase(parsed_re[2]) .message = parsed_re[3] } } ''' [sinks.vlogs] type = "http" inputs = ["n9e_parse"] uri = "http://192.168.1.10:9428/insert/jsonline" [sinks.vlogs.encoding] codec = "json_lines" [sinks.vlogs.batch] max_events = 1000 timeout_secs = 5 [sinks.vlogs.request] concurrency = 2逐个说明关键点。
source部分,include可以写多个文件或glob匹配,比如include = ["/home/n9e/log/*.log"]。read_from = "beginning"表示Vector第一次启动时从文件头开始读,能把已有日志回补进VictoriaLogs;如果改为"end"则只采集新日志。线上环境如果日志文件很大,full回补会占用带宽,建议第一次跑用beginning,确认历史数据不需要后再改成end。ignore_older_secs = 600表示超过600秒未被修改的文件不再读取,避免长时间不动的旧文件浪费系统资源。
meta这个transform的作用是给每条日志打上主机、文件和服务的标识。因为最终日志会汇总到VictoriaLogs,多台夜莺节点混在一起时,必须知道某条日志来自哪台机器、是哪个服务产生的,否则排查问题依然靠猜。
parse这个transform里的VRL脚本分两个分支。第一个分支处理JSON格式的日志,用parse_json解析原始行,遇到解析失败就用空对象兜底,然后提取msg、level字段并转换成统一的_time。第二个分支处理纯文本,用parse_regex正则把时间、级别、内容拆出来,正则的捕获组在上文代码中我用数组下标访问,下标0是完整匹配,1、2、3分别对应时间、级别和内容。VRL脚本默认遇到错误会丢弃事件,所以时间解析我用了非感叹号版本并且用?? now()兜底,宁可时间不准确也尽量不让日志丢失。
sink部分,uri指向VictoriaLogs的/insert/jsonline接口,encoding.codec = "json_lines"表示每次请求按JSON lines编码发送。batch里的max_events和timeout_secs控制攒批逻辑,设置1000条或5秒满足一个就提交一次。concurrency = 2控制并发连接数,对当前日志量来说已经足够,如果想压吞吐可以提高到4或者8。
3.5 写入验证与查询演示
写完配置后,先重启Vector并查看状态:
systemctl restart vector systemctl status vector手动往夜莺日志文件里追加一条测试日志:
echo '2024/07/19 12:00:00 [ERROR] test alert event' >> /home/n9e/log/n9e.log等几秒后到VictoriaLogs查询:
curl 'http://192.168.1.10:9428/select/lucene?query=test+AND+level%3Aerror&start=now-1h&end=now&limit=10'如果返回结果里有刚才那条测试内容,链路就通了。这里注意查询拼接时level:error要转码成level%3Aerror,否则冒号可能被shell或代理解析出错。
4. 用VRL把夜莺日志处理成干净结构
4.1 针对文本和JSON两种格式的解析
夜莺日志格式不统一,我在VRL脚本里用了一个很实用的分支结构:先看message字段是否以{开头。如果以{开头,走parse_json逻辑;否则走正则逻辑。这种方式比单纯分两个source文件更省事,因为很多情况下同一个文件的日志格式都会混着来,尤其新版夜莺部分模块在改造过程中会同时输出JSON和文本。
JSON分支里要小心字段名。夜莺的JSON日志字段最常见的是ts、level、msg,但也可能出现message、time、lvl。我的脚本用了parsed.msg ?? parsed.message ?? .message这种链式兜底,左边取不到就取右边,最终保证msg字段有值。级别字段也一样,默认兜底为info,避免字段缺失导致查询时筛选不完整。
文本分支里正则表达式的写法是重点。夜莺老日志格式是"时间 [级别] 内容"或者"时间 [模块] 级别 内容",具体在不同版本不完全一样。我在正则里写成^日期时间 .*?(Level) 内容,用非贪婪匹配跳过中间可能出现的模块名,这样能兼容两种常见格式。千万不要一次性想写一个兼容所有场景的正则,而是先跑几个真实日志样本,看输出效果再逐步优化。
4.2 字段统一与收敛
日志采集到VictoriaLogs之后,最重要的不是“能搜到”,而是“搜的时候好用”。我的做法是强制统一三个字段。
时间字段统一为_time,并且格式必须是RFC3339。VictoriaLogs对时间字段校验很严格,不是RFC3339格式就会忽略,所以我在脚本里用parse_timestamp转换老日志里那种"2024/07/19 10:00:00"的格式,转换失败则用当前时间兜底。级别字段统一为小写的level,查询时直接level:error,不用记原始日志是大写还是小写。服务字段统一叫service,文件路径统一叫file,多套环境混接时只要在查询条件里加service字段就能隔离。
字段收敛还有一个重要操作:及时删除原始字段。JSON日志被解析后,danese原始message字段如果还保留,事件里会同时有message、msg两个字段,查询时容易分不清。我在解析成功的分支里执行了del(.message),确保最后只留一个标准字段。字段越多索引越大,查询体验也会下降,不是所有原始字段都值得保留。
4.3 多行日志的合并处理
夜莺在panic或者告警回执时,堆栈信息经常跨多行。file source默认每一行都是一个独立事件,堆栈会被拆成好几条记录,查询起来乱成一团。解决方式是在file source里配置multiline合并。
Vector的multiline配置主要看两个参数:mode和start_pattern。mode = "continue"表示后续不以start_pattern开头的行都合并到前一条事件里。我用的配置如下:
[sources.n9e_file] type = "file" include = ["/home/n9e/log/n9e.log"] multiline = { mode = "continue", start_pattern = "^[0-9]{4}/" }start_pattern用正则"^[0-9]{4}/"匹配以年份开头的行,也就是正常日志的起始行。这样堆栈里那些缩进、纯文本代码片段就不会被当成独立事件。需要注意,启用multiline后,file source会缓存一部分事件来判断是否结束,对日志量很大的场景会额外占一些内存,但夜莺这种量级基本没有影响。
5. 常见问题与排查实录
5.1 Vector启动后没有任何日志被采集
我遇到过好多次这种情况:配置看起来没问题,systemctl状态也是active,但VictoriaLogs就是查不到数据。排查顺序我建议按照下面这个列表走。
第一,看Vector自带指标。Vector启动后默认监听localhost:8686,执行curl -s localhost:8686/metrics | grep -E 'file_|http_|received|sent'。如果file_type里的received_events为0,说明文件读取就没成功,问题在source;如果received有值但sent为0,问题在sink或者transform。
第二,检查运行用户权限。Vector如果以非root用户运行,而夜莺日志目录权限是700,文件根本没权限读取,但服务不会报错。可以直接sudo -u vector tail -f /home/n9e/log/n9e.log验证。
第三,检查include路径。我踩过一个很典型的坑:include写的是某个具体文件路径,没有用glob匹配,logrotate把文件改名后,新文件不在include列表里,Vector就停采了。改成include = ["/home/n9e/log/*.log"]之后问题才解决。
5.2 VRL时间解析报错导致整条事件丢失
VRL脚本中带感叹号的函数解析失败时会直接丢弃事件。我在调试阶段写parse_timestamp!时遇到一条非标准时间格式的日志,结果这条日志永远进不了VictoriaLogs,排查半天才发现是这里丢的。
解决办法很简单:把带感叹号的函数换成不带感叹号的版本,再配合??兜底值。比如:
._time = parse_timestamp(parsed_re[1], format: "%Y/%m/%d %H:%M:%S") ?? now()这样即使时间解析失败,也会用当前时间顶上,日志内容不会丢。我的原则是:对可降级的字段用非报错版本,对主机名这种必需字段才用get_hostname!()这类带感叹号的版本,让问题尽早暴露。
5.3 VictoriaLogs返回400 Bad Request
这种问题基本都出在发送格式上。VictoriaLogs的/insert/jsonline要求每行是一个独立的JSON对象,不能带数组包裹,也不能一行里放逗号分隔的多个对象。排查时可以先手动POST一条数据:
curl -X POST 'http://192.168.1.10:9428/insert/jsonline' \ -H 'Content-Type: application/json' \ -d '{"_msg":"test","_time":"2024-07-19T12:00:00.000Z"}'如果手动发送成功,再看Vector sink配置。确认encoding.codec必须是json_lines而不是text。还有一个容易忽略的点:如果uri写错了,比如多写了路径,请求可能到不了insert接口就会返回404而不是400,这时要重点检查uri。
5.4 字段冲突与格式混乱
日志平台跑久了,最怕字段名混乱。我有一次看到一个事件里同时有ts、time、_time、timestamp四个跟时间相关的字段,查询时不知道到底该用哪个,非常影响效率。后来在VRL里做了严格收敛:凡是解析出来的字段,保留统一规范名,原始字段一律删除或者改名。
所以这里给一个建议:每次修改VRL脚本后,可以发送一条真实样本日志,再到VictoriaLogs查询这条日志,检查返回的JSON里字段是否干净。不要靠肉眼去猜,直接看最终落库的结果。
5.5 性能观察与调参
这套方案部署完成后我持续观察了两周。Vector单实例内存占用大概80到150MB,CPU基本稳定在1%以内,在夜莺节点上完全没什么存在感。VictoriaLogs在几十GB数据量下内存约1GB,查询响应基本秒级,全文检索体验比想象中好。
如果后续日志量翻倍,优先调Vector sink里的batch.max_events和request.concurrency。batch.max_events调大能减少HTTP请求次数,concurrency调大能增加并行写入。VictoriaLogs本身的并发参数不建议乱动,默认已经能支撑很高的写入吞吐,瓶颈一般只会在磁盘IO和网络带宽上,调大单side并发反而可能把磁盘打满。
6. 我的实战体会与后续扩展
这套方案我已经在维护环境里跑了两个多月,最大的感受是日志采集不应该一开始就设计得太重,先跑通最小链路,再逐步丰富解析逻辑和字段规范化,远比一开始就想做一个完美平台靠谱。Vector单二进制、VictoriaLogs单二进制的组合,部署和升级都非常省心,夜莺集群每个节点加一个Vector就够了。
最后再分享一个小细节:日志采集链路跑起来后,一定要把这条链路本身也纳入监控。我现在会给每台夜莺节点的磁盘空间、VictoriaLogs所在机器的磁盘使用率、Vector进程存活状态都配上告警。原因很简单,日志平台一旦挂了,你连排查它为什么挂的日志都看不到,那时候就只能对着冷冰冰的进程列表发呆了。如果你也在为夜莺的日志发愁,可以先照着这份配置搭起来,跑一段时间再根据实际查询习惯去调字段和保留策略。后续我打算把这套链路的指标采集也扩展一下,让夜莺主机指标和日志走同一条Vector管道统一管理,等实践得差不多了再来补充。