首页 / 资讯中心 / 文章详情

从filebeat到Kibana:日志采集、Logstash解析与Elasticsearch索引链路实践

从filebeat到Kibana:日志采集、Logstash解析与Elasticsearch索引链路实践 ★ FEATURED ARTICLE
1. 从基础环境到链路打通我们究竟在补什么在上一篇里我们完成了基础环境的搭建Elasticsearch 和 Logstash 能正常启动Kibana 也能连上集群。但说实话那只是万里长征第一步——环境能跑起来和日志链路真正“通”起来中间还隔着不少坑。我见过很多人卡在这一步明明 Elasticsearch 和 Logstash 都启动了日志就是不见踪影查了半天发现是 filebeat 的输出配置里写错了端口或者 Logstash 的 pipeline 根本没有正确加载配置。所以这一篇我不打算再重复环境搭建那些事直接讲链路打通过程中最关键的三个环节数据源采集、Logstash 侧解析清洗、Elasticsearch 侧索引落地。先说我自己的一个体会。很多教程喜欢把 Logstash 的 pipeline 配置写得特别复杂一上来就是几十行 grok 正则看着很唬人。但实际生产环境里真正稳定跑了一年半载的链路配置往往简单直接复杂的那部分都沉淀到了具体的解析规则里。这篇的核心思路就是先把最朴素的链路跑通再逐层加复杂度。如果你能把一条 nginx 访问日志从 filebeat 采集开始完整地走到 Elasticsearch 并能在 Kibana 里查出来那这套方法迁移到其他日志类型应用日志、系统日志、容器日志就只是个规则适配问题架构上完全不用动。需要说明一下以下涉及的 IP、域名、项目名称均为模拟数据仅用于演示链路配置逻辑你可以按自己的实际环境替换。2. 日志从哪里来采集端的配置细节跟随文件还是轮询目录filebeat 的 input 选择很多第一次搭日志链路的人会在采集端纠结一个问题到底用 filebeat 还是直接让 Logstash 去读日志文件。我的建议很清楚采集端用 filebeatLogstash 专心做解析各干各的活儿。理由有两点第一Logstash 的 file input 插件在处理日志文件轮转时的表现不如 filebeat 稳定。filebeat 自带 harvester 机制能记住每个文件读取到的 offset就算 filebeat 重启它也能从上次的位置继续读不会重复采集也不会丢数据。Logstash 的 file input 虽然也有 sincedb 机制但在多文件、多路径的复杂场景下配置和维护成本明显更高。第二filebeat 在采集端能做第一层轻量过滤比如只采集 .log 后缀的文件排除临时文件这样能显著减少进入 Kafka 或 Logstash 的无用数据量。我用的采集配置大致长这样filebeat.inputs: - type: filestream enabled: true id: access-log paths: - /data/logs/nginx/access/*.log fields: log_type: nginx-access fields_under_root: true output.logstash: hosts: [10.0.0.11:5044]这里有一个版本差异要提醒大家。filebeat 7.x 之后主推 filestream 类型替代了原来的 log 类型。filestream 的优势在于处理日志轮转更干净但听一些用 7.10 之前版本的老用户说老版本的 log 类型反而更符合他们的习惯。我自己在 8.x 版本下用 filestream配合 keep_file_open 参数倒是没出过什么问题但这个参数在文件轮转频繁的场景下要谨慎开启容易导致句柄泄漏。标签是链路的灵魂fields 字段的妙用在 filebeat 的配置里很多人忽略fields这个配置实际上这是整条链路的灵魂。生产环境里一台服务器上可能同时跑着 nginx、业务应用、定时任务脚本日志都往同一个目录堆。如果不打标签Logstash 侧就只能靠路径去猜日志类型猜错了就解析失败。我用log_type这个自定义字段标记日志来源然后在 Logstash 里用 if 条件判断走不同的解析分支。没有这个标签的话后面写 grok 正则得多绕很多路。这个经验是实打实踩坑踩出来的——第一次搭链路时没打标签几百台机器的日志混在一起日志类型猜错之后解析乱成一锅粥最后只能回退到重新打标签再采集。为什么一定要加 hosts 而不是直接用 IPfilebeat 的 output 配置里hosts 可以写 IP也可以写域名我建议写域名。原因很简单Logstash 实例如果要做水平扩展IP 可能会变域名指向不变上游 filebeat 不用动。我们在测试环境里用了一个固定的内网域名指向 Logstash 的负载均衡这个设计在后期扩容时省了太多事。3. Logstash pipeline 的分层设计不把鸡蛋放一个篮子里input 插件的端口规划与负载均衡Logstash 的端口规划是个值得提前设计的事情。一个生产环境的 Logstash 实例通常不会只服务一条日志链路而是同时接入 nginx 日志、应用日志、数据库慢查询日志。我给每个日志类型分配独立的端口日志类型Beats 输入端口说明nginx-access5044访问日志量大单独端口隔离压力app-json5045业务应用 JSON 日志slow-query5046数据库慢查询量小但重要端口隔离不只是为了清晰更重要的是某条链路的 Logstash pipeline 挂了不会影响其他链路的数据接入。这个设计在我实际运维中救过我一命有一次 nginx 访问日志的 grok 正则写错了导致大批量解析失败但因为端口隔离应用日志那条链路完全没受影响。下面是 Logstash 管道配置的开头部分用的是 beats 输入插件input { beats { port 5044 client_inactivity_timeout 90 # 关闭空闲连接检测避免 filebeat 频繁重连 } }不要小看client_inactivity_timeout这个参数。默认值 60 秒那是给公网环境准备的内网传输速度快日志量大的时候 filebeat 和 Logstash 之间保持长连接更稳定。每次重连都有握手开销高频重连还会让 Logstash 报 connection reset 的异常日志看着心烦实际影响倒是不大但会让排查其他问题时分心。filter 阶段的架构if 分支与统一出口filter 阶段的配置我强烈建议关注一下这个原则先分叉再汇聚。意思是用 log_type 字段做 if 分支每个分支处理一种日志类型的解析解析完统一往 Elasticsearch 输出。这样设计的优势在于日志类型越来越多的时候只需要新增一个 if 分支不用动其他分支的逻辑。坏处是配置会越来越长所以我会把每种日志类型的解析规则放在单独的文件里用 Logstash 的管道配置引入外部文件。Logstash 7.x 之后的版本支持在 pipeline 配置里引用别的文件这样主配置文件就很清爽filter { if [log_type] nginx-access { grok { match { message %{COMBINEDAPACHELOG} } } date { match [ timestamp, dd/MMM/yyyy:HH:mm:ss Z ] target timestamp } mutate { remove_field [message, timestamp] } } }看到一个细节没我在 grok 和 date 之后用 mutate 把 message 和原始 timestamp 字段删掉了。理由很简单原始日志内容已经解析成结构化字段message 留着对 Elasticsearch 的存储是负担对查询也没帮助。有人会担心解析失败的时候删掉 message 就查不到原始日志了其实不用担心grok 解析失败时 tag 里会加上_grokparsefailuremessage 字段不会被 mutate 删掉因为 grok 分支没匹配上后续也不会走到 mutate。只有在解析成功的情况下 message 才被清理。goroutine 不够用worker 数量的经验值Logstash 的 pipeline 配置里有pipeline.workers这个参数很多人按照默认值跑并发一高才发现吞吐跟不上。这个参数的意义在于Logstash 的 filter 阶段默认是单线程处理的如果不设置 workers默认等于 CPU 核数但对于纯 CPU 密集的正则解析场景来说适当调大 workers 数值能明显提升吞吐。我自己的对比测试数据是这样的在 8 核 16G 的机器上把pipeline.workers从默认的 8 调到 24logstash 处理 nginx 日志的吞吐量从大约 2 万条/秒提升到了 4.5 万条/秒。继续往上加到 32吞吐提升就不明显了反而内存占用明显增加。所以 24 这个值对我来说是个甜点但机器配置不同甜点也不一样建议你从 16 开始往上加做一个简单的压测找到自己的最优值。4. 动手搭一条完整链路从 filebeat 到 Kibana第一步准备一份模拟 nginx 访问日志为了演示我准备了一份模拟访问日志放在/data/logs/nginx/access/demo.log10.0.0.25 - - [15/May/2025:14:23:45 0800] GET /api/user/info?userId1024 HTTP/1.1 200 532 - Mozilla/5.0 (Windows NT 10.0; Win64; x64) 10.0.0.31 0.045 10.0.0.26 - - [15/May/2025:14:23:46 0800] POST /api/order/create HTTP/1.1 201 1203 - curl/7.68.0 10.0.0.32 0.128 10.0.0.27 - - [15/May/2025:14:23:47 0800] GET /api/product/detail?id7788 HTTP/1.1 200 982 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) 10.0.0.33 0.032注意这里的日志格式不是标准的 Combined Log Format我在最后加了一个响应时间字段后面就是要通过 grok 把响应时间解析出来。第二步为日志格式定制 grok 正则COMBINEDAPACHELOG 能解析标准的 nginx 日志但加了自定义字段就得自己写正则。我的处理方式是把日志格式拆成两段前半段用内置模式匹配后半段自己加正则。grok { match { message %{IPORHOST:client_ip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] \%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}\ %{NUMBER:response_code} %{NUMBER:bytes} \%{DATA:referrer}\ \%{DATA:user_agent}\ \%{IPORHOST:upstream_ip}\ \%{NUMBER:response_time}\ } }这个正则在 Grok Debugger 里调试过可以正常解析上面的测试日志。几点说明USER:ident和USER:auth对应日志里的两个-nginx 默认没开认证所以都是-HTTPDATE能解析[15/May/2025:14:23:45 0800]这种带时区的格式不用自己写时间正则URIPATHPARAM会把查询参数一并匹配这比单独用NOTSPACE强grok 正则调试是个体力活我习惯先在 Kibana 自带的 Grok Debugger 里调调好了再贴到 Logstash 配置里。如果不想开 Kibana也可以用在线grok调试这种工具原理相同正则语法就是 Ruby 的正则风格。第三步设置 timezone 与时区陷阱date 插件解析时间的时候有个特别容易踩的坑默认时区是 UTC。如果日志时间是东八区直接用 date 解析后存到 Elasticsearch 里时间会整整差 8 小时。kibana 显示的时候如果用的是浏览器本地时区看起来时间能对上但实际存储已经出错了后面做时间聚合查询的时候数据会凭空少一块。我的处理方式是在 date 插件里显式指定时区date { match [ timestamp, dd/MMM/yyyy:HH:mm:ss Z ] target timestamp timezone Asia/Shanghai }另一个坑是如果日志里的时间格式带时区偏移比如0800date 插件解析时会自动转成 UTC 时间存储timezone参数其实不影响这个转换。这个参数主要作用于不带时区的日志时间。所以上面这份日志里带了0800配置里写不写 timezone 其实都能解析正确。但如果你的日志时间格式是15/May/2025:14:23:45这种不带时区的那 timezone 就必须写不写就默认当 UTC 处理。第四步Elasticsearch output 与索引模板Logstash 往 Elasticsearch 输出的时候有几种思路。一个是让 Logstash 自动创建索引默认索引名格式是logstash-%{yyyy.MM.dd}。另一个是用 Elasticsearch 的索引模板控制 mapping让 Logstash 只管写数据索引结构和字段类型由模板统一管。我建议使用自定义索引名加索引模板的组合output { elasticsearch { hosts [http://10.0.0.10:9200] index nginx-access-%{yyyy.MM.dd} template_name nginx-access-template template /etc/logstash/templates/nginx-access.json template_overwrite true } }这样设置的好处是索引模板可以控制 response_time 字段按 float 类型存储而不是默认的 text 类型。否则 Elasticsearch 对数字字符串做自动映射时会生成 text 加 keyword 的子字段查询性能不算最优聚合也很难用。把模板文件交给 Logstash 管理每次推配置的时候同步更新模板模板版本和解析规则一起走版本控制比在 Kibana 里手动建模板靠谱得多。模板文件的主要内容大概是这样的{ index_patterns: [nginx-access-*], settings: { number_of_shards: 3, number_of_replicas: 1, refresh_interval: 5s }, mappings: { properties: { response_time: { type: float }, response_code: { type: integer }, bytes: { type: long }, timestamp: { type: date }, request: { type: keyword } } } }refresh_interval设成 5 秒是折中方案实时性要求不高的日志场景不需要 1 秒刷新5 秒刷新对写入性能的影响小很多。如果你追求日志秒级可见可以设回 1s但写入吞吐会下降具体取舍看你的业务。第五步启动与验证前几步配置都调整完之后启动 filebeat 和 Logstash检查几个关键点Logstash 启动日志里有没有报配置错误使用bin/logstash -f /etc/logstash/pipeline/nginx.conf --config.test_and_exit可以先做配置校验filebeat 日志里有没有 output 报错filebeat 默认日志在/var/log/filebeat/filebeat下看到acknowledged字样说明数据已经送达Elasticsearch 里有没有生成索引用curl -XGET http://10.0.0.10:9200/_cat/indices/nginx-access-*?v看索引是否存在Kibana 里能看到日志条目创建索引模式nginx-access-*时间字段选timestamp就能搜索了我踩过的一个坑是filebeat 和 Logstash 都用默认配置跑结果 Logstash 的 output 连不上 Elasticsearch报connection refused。检查发现 Elasticsearch 绑定的是 127.0.0.1Logstash 在另一台机器上当然连不上。你得确认 Elasticsearch 的network.host配置监听在内网 IP 上或者至少监听在 0.0.0.0。这个比较基础但犯过这个错的人真不少。5. 常见报错与排障记录grokparsefailure 满天飞这是 Logstash 新手遇得最多的问题。日志进到 Logstash 里解析失败tags字段里有_grokparsefailure。查这种问题我有一套固定思路先确认日志格式是不是预期的格式有时候 nginx 配置改了 log_format 但忘了同步 Logstash 的 grok 正则把报错的原始日志复制出来在 Grok Debugger 里重新匹配逐段测试定位到是哪个字段匹配不上确认正则里有没有用错转义\[必须转义成\\[这在配置里很容易漏有一次问题出在 user_agent 字段上某个爬虫的 UA 里带了引号%{DATA:user_agent}匹配到引号就停住了后面的 upstream_ip 完全解析不出来。后来我把 user_agent 的正则从%{DATA:user_agent}改成了%{QUOTEDSTRING:user_agent}之类更严格一点的模式配合%{GREEDYDATA:user_agent}做兜底。如果你遇到 UA 不规律的情况建议专门跑一个 UA 解析 Java 库或者 ESRuby 规则。date 解析失败导致 timestamp 是当前时间date 解析失败不会让整个 pipeline 挂掉但后果很隐蔽timestamp默认取的是 Logstash 处理日志的当前时间而不是日志里的实际时间。这种错位一时半会看不出来等到凌晨看报表的时候发现某个小时的日志计数是负数上一小时的日志被算到了下一小时才知道时间字段错位了。这个问题的排查方式是在 Elasticsearch 里随意取一条日志看timestamp和日志里原始时间的差值。如果两者相差超过几秒基本就是 date 解析没生效。date 配置里match的格式一定要和日志严格一致尤其是dd/MMM/yyyy:HH:mm:ss Z这种大小写和斜杠都不能错。output 到 Elasticsearch 的 mapping 冲突Elasticsearch 7.x 之后自动创建索引时字段类型靠动态映射。同一个字段第一天写入的是字符串第二天写入的是数字Elasticsearch 会拒绝写入报mapper_parsing_exception。典型场景就是response_code某天日志格式变了写了个200字符串进去第二天又写了个数字200冲突就来了。用索引模板可以提前把字段类型定死从根源上避免这类问题。如果你已经有一批索引因为映射问题写不进数据了只能重建索引或者用 reindex。提前用模板控制是最优解。6. 后续可以怎么玩链路基础上的延展思路日志链路打通之后能玩的花样就多了。简单列几个方向这些是我在实际项目里验证过的日志内容告警在 Logstash 的 filter 里加一个 if 判断当 response_code 大于 500 或者 response_time 大于 3 秒时通过 email 或者 webhook 插件推一条告警。这种方式比纯靠 Kibana 告警更实时因为 Logstash 在数据刚进来的时候就能拦截到。字段归类与脱敏日志里有些敏感字段比如用户 ID、手机号可以在 mutate 里做正则替换把关键信息打码。不要把脱敏放在应用侧做应用改代码的成本高Logstash 侧配置一次全局生效。冷热数据分层索引模板里配置生命周期策略把 7 天前的索引自动转成只读30 天前的索引删掉或迁移到冷存储。这个策略在数据量上来之后很管用不然磁盘很快就满了。我个人在实际操作中的一个体会是日志链路这条线70% 的功夫在日志格式规范和正则解析上真正搭建 Elasticsearch 和 Logstash 的架构只占 30%。很多人一开始就把精力放在组件选型和集群规模上等日志真正进来了才发现解析规则写得不好数据质量差后面做分析挖掘的时候全靠清洗越到后面越被动。所以如果你正准备搭第二条、第三条日志链路先把日志格式规范定清楚再动手写配置省下来的时间远比想象中多。哦对了最后再分享一个小技巧。Logstash 配置改完不要直接重启生产实例先用--config.test_and_exit校验再用--config.reload.automatic的方式让它自动加载。这样改错配置不会导致数据中断顶多是一次短暂的管道重建比手动 kill 进程安全得多。
阅读完成 · 觉得有帮助?
咨询建站