ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ELK 日志查询实战:从 traceid 到 Kibana 跨服务排障

ELK 日志查询实战:从 traceid 到 Kibana 跨服务排障 凌晨两点工作群里甩过来一条消息附了一个 traceid6aa2526590ad07346b76e2b8d8d80384后面跟一句这个单子状态没更新帮忙看下。没有任何上下文没有用户ID没有时间点。这时候你能做的第一件事就是打开 Kibana把这个 traceid 丢进去。ELK 这套东西存在的意义很大程度上就是为了应对这种什么都不知道、只知道一个线索的时刻——Elasticsearch 负责把海量日志存下来并建好索引Kibana 负责让你用几秒钟从几亿条日志里把那条相关的捞出来。这篇内容聊的就是 ELK 查日志这件事从怎么把日志弄进 Elasticsearch到 Kibana 里怎么写出真正管用的查询再到用 traceid 把跨服务的调用串成一条链。它既适合刚接手日志平台、还在对着 Kibana 界面发懵的运维和开发也适合已经在用、但每次查问题都靠全文搜关键词肉眼翻的老手。我会尽量把每个选择的理由讲清楚而不是只丢一段配置文件给你抄——因为同样的配置换个业务场景可能就是坑。1. 从一个 traceid 出发ELK 查日志的真实工作场景线上排障最有意思的一点是你手里往往只有一个碎片。可能是一个 traceid可能是用户截图里的一个订单号也可能是大概下午三点左右这么一个模糊的时间范围。ELK 的价值就在于它把这三类碎片都变成了可索引、可检索的维度。但前提是你的日志体系得先把这些维度采集下来并且存对地方。1.1 日志查不动问题通常不在 Kibana很多人第一次用 Kibana 觉得难用以为是查询语法不熟。实际上大部分查不动的根因都发生在日志写进 Elasticsearch 之前。比如日志是纯文本一行行塞进去的traceid 和业务字段全糊在 message 里那你只能靠全文检索去撞再比如时间戳字段解析错了字段类型变成了text而不是dateKibana 的时间筛选就完全失效你在界面上选的时间范围形同虚设。我见过最典型的一个案例某个服务的日志时间戳用的是容器本地时间而容器时区是 UTC宿主机是东八区结果 Kibana 里看到的时间永远差 8 小时。排查的人以为是 Kibana 抽风折腾了一下午才发现是采集端没统一时区。所以第一件事得想明白日志从产生到能被查询中间要经过产生、采集、解析、写入、索引五个环节任何一个环节的信息丢失都会让最终查询少一个维度。1.2 Elasticsearch 和 Kibana 各自负责什么把 ELK 拆开看其实很朴素。Elasticsearch 是一个分布式的文档数据库它管存储、管索引、管检索。你写进去的每一条日志在它眼里就是一个 JSON 文档只不过这个文档的字段会被倒排索引处理过所以检索速度极快。Kibana 则是架在 Elasticsearch 前面的一个可视化壳子你点鼠标、敲查询语句它翻译成 Elasticsearch 的查询 DSL 发过去再把结果渲染出来。两者之间的边界很重要Kibana 本身不存任何日志数据。你换了 Kibana 依然查不到数据那问题一定在 Elasticsearch 侧或者更上游的采集链路反过来如果你用curl直接查 Elasticsearch 能查到、Kibana 却查不到那大概率是索引模式Index Pattern配错了或者时间字段没选对。搞清楚这条分界线能省掉一大半到底怪谁的纠结。1.3 一套查得动的日志体系长什么样我心目中一套合格的日志体系至少要满足几个条件。第一每条日志都带统一的 traceid这样跨服务的问题能串起来。第二关键业务字段用户ID、订单号、接口名、耗时、状态码都从 message 里拆出来成为独立字段别让它们埋在文本里。第三时间字段是真正的 date 类型且全局统一时区。第四有一个合理的数据保留策略别让磁盘写爆了才想起来清理。这四条听起来简单但真正落地的时候每一条都会牵扯到采集配置、日志框架、索引模板的设计。后面几节我会逐条拆开讲包括为什么这么设计、配置该怎么写、以及我踩过哪些坑。2. 日志进 ES 之前采集、解析与字段设计决定查询上限日志能不能查得爽八成取决于它进 Elasticsearch 之前的加工质量。采集这一层容易被当成配一下就跑的体力活但它其实是整个日志平台的地基。地基没打平后面 Kibana 里怎么写查询都别扭。2.1 Filebeat 还是 Logstash先分清职责再选型常见组合是 Filebeat 采集 Logstash 清洗。Filebeat 轻量部署在业务机器上负责读文件、做最基础的字段添加、把数据推给下游。Logstash 重但它有强大的 filter 插件能做 grok 正则解析、字段改名、时间戳覆盖、条件分支。两者不是非此即彼而是分工。如果你的日志本身就是结构化 JSON那其实 Filebeat 直接把message里的 JSON 展开就够了不一定非要 Logstash。Filebeat 的配置大概是这个感觉filebeat.inputs: - type: filestream paths: - /var/log/app/*.log json.keys_under_root: true json.overwrite_keys: true fields: service: order-service env: prodjson.keys_under_root: true是把日志行里的 JSON 提升到文档顶层而不是塞在 message 字段里。json.overwrite_keys: true是允许日志自己的字段覆盖掉 Filebeat 默认加的字段比如时间。如果日志是非结构化的比如那种2024-01-01 12:00:00 INFO [order] user123 actionpay cost45ms的纯文本那就需要 Logstash 的 grokfilter { grok { match { message %{TIMESTAMP_ISO8601:log_time} %{LOGLEVEL:level} \[%{DATA:service}\] user%{NUMBER:user_id} action%{WORD:action} cost%{NUMBER:cost_ms}ms } } date { match [log_time, yyyy-MM-dd HH:mm:ss] timezone Asia/Shanghai target timestamp } }选型的判断标准很简单日志是不是已经是 JSON。是就尽量让采集端轻量别引入 Logstash 增加运维成本不是那就老老实实上 Logstash 或者把应用日志改造成 JSON 输出。我个人的偏好是推动应用侧直接输出 JSON因为解析正则是很容易出错的业务日志格式一改grok 就崩了而且 grok 正则匹配是 CPU 消耗大户。2.2 为什么我坚持让应用输出结构化 JSON纯文本日志最大的问题是字段不可控。你写正则去拆就得假设日志格式永远不变。但现实里今天加个字段明天改个措辞正则就失效了日志采集就静默丢字段。而结构化 JSON 是把字段定义这件事前移到应用代码里谁写日志谁负责字段反而稳定。一个典型的 JSON 日志长这样{ timestamp: 2024-06-18T14:32:11.234Z, level: INFO, service: order-service, trace_id: 6aa2526590ad07346b76e2b8d8d80384, span_id: a1b2c3d4, user_id: 10086, order_id: SO202406180001, action: update_status, cost_ms: 45, message: 订单状态更新完成 }这种日志进 Elasticsearch 之后每个字段都是独立可查的。你想查trace_id等于某值、cost_ms大于 500 的记录就是一个精确匹配加一个范围查询速度极快。相比之下如果这些都埋在 message 里就只能全文检索还得担心分词把 traceid 切碎。2.3 Mapping 与字段类型定错一次后悔很久Elasticsearch 默认会对新字段做动态映射dynamic mapping。规则大概是字符串默认映射成text加一个keyword子字段数字映射成long或float能被解析成日期的字符串映射成date。这个默认行为在测试环境很方便但在生产环境是灾难的开始。因为动态映射一旦给某个字段定了类型后续这个字段再写入不兼容的值就会报错而且字段类型在同一个索引里无法修改只能重建索引。所以生产环境一定要用索引模板index template显式定义关键字段类型{ index_patterns: [app-logs-*], template: { mappings: { properties: { timestamp: { type: date }, trace_id: { type: keyword }, user_id: { type: long }, order_id: { type: keyword }, cost_ms: { type: long }, message: { type: text }, level: { type: keyword }, service: { type: keyword } } } } }这里有个关键区别text会分词keyword不分词。traceid、订单号、用户标识这类需要精确匹配的字段必须是keyword。如果你把它映射成text写入时会被分词器切成一堆片段你按完整 traceid 查询就可能查不到——这是新手最常踩的坑之一后面第 6 节会专门讲。2.4 时间戳字段多来源日志的第一杀手多来源日志汇聚到一个 Elasticsearch 里最容易乱的就是时间。每个服务、每台机器、每个容器的时区都可能不一样。如果采集时不做统一处理timestamp就会七零八落Kibana 的时间范围筛选就会漏掉一堆数据。我的处理原则是所有日志在采集端统一转换成 UTC 写入timestamp展示时由 Kibana 按浏览器时区渲染。Logstash 的 date filter 里显式指定timezoneFilebeat 则尽量让它读取日志自带的 ISO8601 时间。如果应用日志里的时间是东八区就必须在解析时告诉解析器这是东八区否则 Elasticsearch 会按 UTC 解释结果整体偏移 8 小时。这个偏移很隐蔽因为数据看着是有的只是跑到别的时间段去了。你在界面选今天 14:00-15:00恰恰什么也查不到。所以只要发现查不到日志第一反应就该去核对时间字段别急着怀疑查询语句。3. Kibana 查询语法实战KQL、Lucene 与 DSL 的选择Kibana 的 Discover 页面提供两种查询语言KQLKibana Query Language和 Lucene。很多人不知道自己在用哪种也没搞清楚两者区别导致查询时灵时不灵。搞清楚这一层查询效率会有明显提升。3.1 KQL 和 Lucene 到底用哪个KQL 是 Kibana 后来主推的语法更贴近自然语言对新手友好。比如查订单服务里耗时超过 500 毫秒的日志service: order-service and cost_ms 500Lucene 语法更老派但表达能力强一些尤其是涉及正则、模糊匹配和复杂布尔组合的时候service:order-service AND cost_ms:{500 TO *}我的一般建议是日常排查用 KQL够用且不易写错需要复杂正则或者字段级的高级匹配时切到 Lucene。Kibana 界面上有个开关可以在两者间切换切换的时候注意语法不通用换了语言原来那条查询要重写。这里给个选择对照方便你判断场景推荐语言示例精确匹配字段值KQLlevel: ERROR数值/时间范围KQLcost_ms 500字段存在性判断KQLtrace_id: *复杂正则Luceneorder_id: /SO2024061.*/多字段模糊匹配Lucenemessage:fail~补充一句KQL 里的and、or、not也可以写成AND、OR、NOT大小写都认但建议统一用小写看起来更清爽。3.2 通配符、短语和模糊查询的正确姿势几种容易混的匹配方式得说清楚。第一种是短语匹配message: 订单状态更新加了引号表示整段短语匹配不会把词拆开。第二种是通配符order_id: SO2024*星号表示前缀匹配。第三种是模糊查询message: error~波浪号后面还能跟数字表示允许的编辑距离。要注意的是通配符查询尤其是前置通配*abc非常吃性能因为它没法利用倒排索引的前缀优化会扫大量词项。生产环境建议避免前置通配能用精确匹配就别用通配。如果你经常需要按订单号前缀查不如把订单号做成keyword字段直接精确查或者额外建一个专门的前缀字段。模糊查询同理编辑距离查询在数据量大时开销很高一般只用于兜底场景比如你不确定用户输入的业务关键字拼写才用~去碰运气。3.3 traceid 查询从全文检索到精确命中回到最开始那条 traceid。如果trace_id是keyword字段查询就一句话trace_id: 6aa2526590ad07346b76e2b8d8d80384命中速度是毫秒级因为它走的是精确索引。但如果trace_id错误地映射成了text写入时会被分词器切开你按完整值查就可能一条都查不到这时候你只能用match或者全文检索去撞效率低不说还可能因为分词把无关日志也带出来。我建议的做法是trace_id用keyword并且如果你还需要在全文检索里按它搜索可以额外加一个text子字段trace_id: { type: keyword, fields: { text: { type: text } } }这样一个字段两用trace_id精确查trace_id.text供全文匹配。这是个很实用的技巧代价是索引略微变大但排查体验提升明显。3.4 把常用查询固化成保存搜索排障时你可能会反复用同一类查询比如某服务的所有 ERROR、耗时超过 1 秒的接口调用、某个业务动作的完整链路。Kibana 允许把这些查询保存成 Saved Search下次一键加载还能直接生成分享链接丢到群里。我团队的实践是维护一份排障查询手册把每个核心服务最常用的几条查询存好新同事上手时直接照着用不用每次从零开始想语法。这看似是个小动作但它把谁能查日志从少数熟悉语法的人扩展到整个团队排障响应速度能提升不少。4. 用 traceid 把一次请求串起来跨服务日志的关联单个服务的日志好查难的是把一个用户请求在微服务之间流转的全过程拼起来。这正是 traceid 存在的意义。它是一条贯穿整条调用链的标识只要每个服务在打日志时都带上它你就能在 Kibana 里拼出完整链路。4.1 traceid 是怎么一路透传下来的traceid 的传递通常靠两个途径HTTP 请求头和服务间调用的上下文。用户请求进入网关时网关生成一个 traceid或者从上游取现成的塞进请求头比如X-Trace-Id。后面的服务在收到请求时从请求头里读出来放进当前线程的上下文比如 MDC之后所有日志自动带上它。向下游发起调用时再把这个 traceid 放进新的请求头传给下一个服务。关键点是上下文透传。如果服务 A 调服务 B 的时候忘了传 traceid那 B 里产生的日志就跟这条链断了你在 Kibana 里只能看到半截链路。所以我一般会在统一的 HTTP 客户端拦截器里做这件事而不是靠每个业务方法手动传避免遗漏。4.2 日志框架和 MDC 的配合Java 生态里最常见的做法是 MDCMapped Diagnostic Context。请求进来时把 traceid 放进 MDC日志框架输出时会自动带上。配合 logback 的配置pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{trace_id}] %logger{36} - %msg%n/pattern%X{trace_id}就是取 MDC 里的 traceid。这样同一条链上的所有日志都会带上同一个值串联就有了基础。如果你用的是结构化 JSON 日志那就更直接把 traceid 作为一个字段塞进日志 JSON 里。需要注意的是MDC 是线程绑定的如果你用了线程池或者异步任务子线程里拿不到父线程的 MDC日志就会丢 traceid。这种情况需要手动把上下文传给子线程或者用支持上下文透传的线程池包装。4.3 在 Kibana 里拼出完整链路有了统一字段之后在 Kibana 里查链路就很直接。用 traceid 精确过滤然后按timestamp排序就能看到这条请求流经的每个服务的日志。更规范的做法是引入 OpenTelemetry 那套 trace 体系字段命名标准化trace.id、span.idKibana 里还能用 APM 视图直接展示调用链。如果只是用自建的 traceid 字段也没问题就是自己拼。我的做法是在 Discover 里按时间排序看同时把关键字段service、level、message、cost_ms选进列显示一条条往下看哪一步慢了、哪一步报错了。如果服务数量多、日志量大还可以先按service做聚合看哪个服务贡献的日志最多快速缩小怀疑范围。4.4 没有 traceid 时的补救思路理想情况当然是全链路都有 traceid但现实里总有些老服务没接。这时候只能退而求其次靠时间窗加业务主键去关联。比如你知道订单号就用订单号在所有服务里查把时间范围收窄到问题的那个时间点前后几分钟再看哪些日志时间上对得上。这种补救方式效率明显低而且容易误判因为同一时间段可能有多个请求在跑。所以只要有条件我还是建议把 traceid 铺全这是一次性投入、长期受益的基础设施。5. 索引治理分片、ILM 与查询性能日志数据是持续增长的如果不管不顾地往一个索引里堆早晚会因为分片过大或者磁盘写满而出事。索引治理这件事平时不起眼出事的时候很要命。5.1 按月建索引还是按天最直观的选择是按时段切索引比如app-logs-2024.06.18一天一个。好处是管理简单过期直接删整个索引比在索引里按时间删文档快得多。坏处是如果日志量小会产生大量小索引每个索引都有固定的分片开销反而不划算。我的经验是日志量大每天几十 GB 以上就按天切量小可以按周或按月。判断依据是单索引的大小一般建议单个分片控制在几十 GB 以内太小浪费资源太大不利于均衡和恢复。按天切最省心的一点是配合 ILM 做生命周期管理非常自然。5.2 分片数量和副本的取舍分片数不是越多越好。分片越多集群元数据开销越大查询时协调节点要合并的分片也越多。对于日志场景我一般建议单分片大小控制在 30-50GB既不会太大导致恢复慢也不会太小导致分片爆炸。副本replica主要作用是高可用和分担查询压力。日志数据通常对一致性要求不高如果集群资源紧张副本可以设为 0 或 1如果有查询压力并且不差磁盘设 1 就够了。要注意副本数不能超过节点数减一否则副本分配不出去集群会一直处于 yellow 状态。这里给个参考日志日增量索引周期主分片数副本数 5GB按月115-30GB按周3130-100GB按天3-51 100GB按天按节点数定15.3 ILM 冷热分层与保留策略ILMIndex Lifecycle Management是管理日志生命周期的标准工具。它把索引的生命周期分成 hot、warm、cold、delete 几个阶段你可以定义写入阶段放在热节点数据变旧后迁移到便宜的大盘节点再老就直接删。一个典型的策略是热阶段保留 3 天温阶段保留到 15 天之后删除。这样既保证了最近几天的查询性能又控制了磁盘成本。配置大概是{ policy: { phases: { hot: { actions: { rollover: { max_size: 30gb, max_age: 1d } } }, warm: { min_age: 3d, actions: { forcemerge: { max_num_segments: 1 } } }, delete: { min_age: 15d, actions: { delete: {} } } } } }forcemerge把多个小段合并成一个能显著提升查询性能但它是 IO 密集操作建议放在低峰期。5.4 查询慢的常见原因查询慢通常有几个来源。第一是时间范围太大你让它扫半年数据肯定慢正确做法是尽量收窄时间范围让 Elasticsearch 只查相关的少量索引。第二是通配符和模糊查询尤其是前置通配。第三是聚合操作太重比如对超大字段做 terms 聚合。第四是字段类型设计不合理本可以精确匹配的字段做成了 text导致走全文检索路径。优化思路是先看 Kibana 里的查询语句有没有明显问题再考虑从索引设计层面调整比如给高频查询字段多做keyword避免不必要的高基数字段聚合。很多时候只要把时间范围收窄查询速度就能从几十秒降到几百毫秒。6. 查不到日志的排查清单一次完整的定位链路最让人抓狂的不是查询慢而是明明应该有日志却什么都查不到。这时候别乱试按链路逐层往下排查效率最高。6.1 先确认数据到底有没有进 ES第一步永远是用命令行直接查 Elasticsearch绕过 Kibanacurl -s http://es-host:9200/app-logs-*/_count -H Content-Type: application/json -d { query: { match_all: {} } }如果这个 count 是 0说明数据根本没进来问题在采集链路或写入端Kibana 怎么配都没用。如果 count 大于 0 但 Kibana 查不到那就是 Kibana 侧的索引模式、时间字段或者查询语句问题。这一步能帮你迅速定位问题出在哪一半。6.2 时间与时区最冤的一种查不到确认数据存在之后最常见的坑是时间。Kibana 的时间选择器默认按你浏览器时区展示但如果日志写入时timestamp用的是错误的时区数据就会跑到别的时间段。你在今天这个范围里查不到不代表数据不存在只是它被记到了别的时间。验证方法很直接把 Kibana 的时间范围放大到最近 30 天看数据是否出现。如果放大就出来了那就是时间字段的问题。这时候要回头检查采集端的时间解析配置确认timestamp写入的是 UTC。6.3 字段类型与分词全文能搜到、精确查不到另一个高频坑是字段类型。如果你用trace_id: 完整值查不到但用message: traceid的某一段能搜到基本可以确定trace_id被映射成了text且被分词了。解决方案是把它改成keyword但字段类型不能直接改需要重建索引或者用 reindex 迁移数据。临时救急的话可以用trace_id.text或者直接对message做全文匹配把那个 traceid 当普通文本搜但这样没法精确也可能带出无关记录。长期的方案还是修正索引模板让新数据用正确的类型。6.4 采集端和写入端的常见故障如果数据压根没进来往上游查。Filebeat 有没有在正常读文件看它的 registry 状态和日志、Logstash 有没有解析报错、Elasticsearch 有没有因为磁盘水位满了而拒绝写入cluster.routing.allocation.disk.watermark。磁盘水位触发后Elasticsearch 会把索引切成只读这时候数据会写不进去表现就是日志突然断了。我还遇到过一类很隐蔽的问题采集端配置里的paths写错了日志文件确实在产生但 Filebeat 通配符没覆盖到结果就是静默无数据。所以排查采集端时第一件事是确认路径通配符能匹配到目标文件第二件事是看采集组件自己的日志有没有报错别只盯着 Elasticsearch。我在实际运维里最大的体会是日志平台的问题八成能在数据到底进没进 ES这一步定性。养成先用 curl 确认数据存在性的习惯比在 Kibana 界面里反复调查询语句要省太多时间。另外别指望一套配置一劳永逸业务日志格式、服务数量、数据量都在变索引模板和 ILM 策略也需要定期回顾。我通常每个季度会把保留策略和高频查询重新过一遍看看有没有字段类型定错、有没有索引膨胀、有没有查询别名过时。这些小事平时不做等到磁盘告警或者排障卡壳的时候代价就大了。
返回列表