1. 日志系统搭建前的需求拆解与整体思路
先说个背景。我做代购系统后端也有几年了,一开始根本没把日志当回事,就是用print和框架自带的logger随便打打,出问题了直接上服务器tail -f看。等到系统上线跑了一段时间,用户量上来,渠道变多,才发现这套土办法彻底撑不住了。
代购这个业务有个特点,就是链路特别长。用户下单之后,要经过采购员接单、海外买手采购、国际物流、清关、国内转运、签收这么一大串环节。每个环节都分布在不同的服务里,甚至不同的服务器上。订单状态出问题的时候,最痛苦的排查方式就是一台一台服务器翻日志,一条一条对时间戳。有一次排查一个订单卡在“清关中”两天没动的问题,我愣是手工翻了四台机器的日志,最后发现是某个回调服务凌晨三点因为内存溢出自动重启了,请求丢了。
也就是从那一次之后,我下定决心要把日志系统正经做起来。
做日志系统之前,我先把自己想要的几个核心能力理清楚了,这里分享下我的需求拆解思路:
- 全链路追踪:一个订单从下单到签收,所有关键状态变更都要能串起来,不能散落在各个服务的日志文件里找不着。
- 异常主动发现:系统报错、接口超时、外部渠道回调失败这些事,最好能在监控页面直接看到,而不是等用户投诉了才发现。
- 业务对账辅助:代购系统经常涉及多级分销、渠道返佣、采购对账,日志里得有足够详细的业务字段,方便出问题的时候往前追溯。
- 检索必须快:日志量上去之后,
grep已经没法用了。我要支持按订单号、用户ID、时间段这些维度秒级查询。
基于以上需求,市面上的方案其实就那么几个。要么是用云厂商自带的日志服务,要么自建 ELK 这套经典组合,要么用 Loki 这种轻量方案。我当时权衡了一下,因为服务器已经有不少了,而且数据敏感度比较高,不想把日志传到云上,所以自建是更合适的选择。
在 ELK 和 Loki 之间,我选了 ELK 而不是 Loki,原因也很简单:
| 对比项 | ELK | Loki |
|---|---|---|
| 索引能力 | Elasticsearch 全文检索能力强,支持复杂聚合 | 基于标签索引,全文检索能力弱一些 |
| 生态成熟度 | 组件多、文档多、踩坑资料多 | 相对年轻,部分功能还在完善 |
| 资源占用 | ES 比较吃内存 | 整体更轻量 |
| 适用场景 | 业务日志需要深度分析、聚合统计 | 纯日志排查、轻量监控为主 |
代购系统的日志不只是拿来“看”的,我还要做错误率统计、接口耗时分析、用户操作行为分析这些事,ES 的聚合能力刚好能覆盖。所以虽然 ELK 部署更重,我最后选了它。
整体架构也定下来了,简单清晰:
应用日志 -> Filebeat(采集)-> Logstash(清洗/解析)-> Elasticsearch(存储/索引)-> Kibana(展示/分析)这套架构里有个细节,为什么采集端用 Filebeat 不用 Logstash?因为 Logstash 是 Java 写的,每台业务服务器上部署一个,内存开销太大,动不动就占掉 1G 以上。Filebeat 是 Go 写的,资源占用小一个量级,跑在业务服务器上非常轻量,内存占用大概在 20MB 到 50MB 之间,对业务的影响可以忽略。
从拆解需求到确认方案,我认为这是整个项目里最关键的一个阶段。先想清楚要解决什么问题,再去选工具,思路才不会跑偏。
2. 日志规范与字段设计,一切分析功能的基础
日志分析做得好不好,七分靠规范,三分靠工具。这句话是我的切身感受。很多团队搭好了 ELK,结果发现日志格式乱七八糟,统计的时候根本没法用,然后回过头来改业务代码里的日志输出,这个成本就太大了。
2.1 日志分级与格式统一
我梳理了代购系统里需要的日志级别,参照的是大厂常用的分级规范,简单聊聊我的使用习惯。
- ERROR:系统跑不通了、外部接口返回明确失败、数据一致性出问题。这种日志必须打,而且要带完整的上下文,比如订单号、用户ID、报错堆栈。
- WARN:不该发生但没影响核心流程的事。比如某个商品库存查询超时走了降级逻辑、某个渠道回调重复通知了三次。这类日志的价值在于发现隐患。
- INFO:关键业务节点。用户下单成功、支付回调接收到、采购单创建、物流状态更新。这类日志不要打太多细节,否则量太大而且没意义。
- DEBUG:开发排查用,生产环境一般不开启。
有了分级还不够,格式也必须统一。我的做法是定义了一个日志输出的标准模板,所有服务的日志都按这个模板打:
时间戳 | 日志级别 | 服务名 | 追踪ID | 业务类型 | 业务ID | 用户ID | 具体信息举例说明:
2024-11-20 14:23:45.678 | INFO | order-service | 9f8e7d6c5b4a | order | 20241120142345001 | 10086 | order created, sku=SKU12345, qty=2, amount=188.00这个格式看起来简单,实际操作时想让每个开发都严格照做其实不容易。我的经验是没靠自觉,而是统一封装了一个日志工具包。团队里所有人打日志都用这个工具包提供的方法,比如LogUtil.info(业务类型, 业务ID, 用户ID, "描述信息"),底层统一拼格式。这样就算有人偷懒不传用户ID,模板也不会乱。
2.2 全链路追踪ID的传递
日志分析里最深的一个坑,就是没有链路追踪ID。单看一条日志觉得没问题,但想把一个请求经过的所有服务日志串起来,就发现根本串不动。
代购系统的调用链相当复杂,用户下单这个动作,前端会调网关,网关调订单服务,订单服务要调库存服务,还要发消息给采购系统,采购系统又要回调。链路长,节点多,没有统一的追踪ID,排查一次问题等于大海捞针。
我采用的方案是自己封装了一个轻量级的链路追踪组件,基于SLF4J的MDC机制来实现。核心逻辑其实不难:
- 请求进入网关时生成一个全局唯一的
traceId,用 UUID 就行。 - 通过 HTTP 头向下游传递,约定好的 Header 名是
X-Trace-Id。 - 服务收到请求后,从 Header 里取
traceId,放到日志框架的 MDC 里。 - 日志输出模板里自动带上 MDC 中的
traceId。
如果是走消息队列的场景,比如订单服务发消息给采购服务,那traceId就塞进消息头里,消费方拿到消息后先从消息头取traceId再设置到 MDC 里。
这样一来,不管是同步调用还是异步消息,整条链路的所有日志都带着同一个traceId。在 Kibana 里按traceId一搜,这个订单从创建到状态流转再到异常堆栈,全部日志按时间线排得整整齐齐,排查效率直接翻倍。
这里有个小细节值得提醒:HTTP 头里的traceId一定要过滤掉用户传入的值,不能在入口处直接信任客户端传上来的 Header 直接拿来用,而是生成新的或者当不存在时再生成。不然有人恶意传一个超长的字符串,会影响 ES 索引效率,也不安全。
2.3 关键业务埋点:日志不只是给排障用的
日志如果只用来排障,那就浪费了一大半价值。我还在关键业务节点上做了埋点,把日志直接变成数据源。
举几个实际例子:
- 下单接口耗时:在所有 Controller 入口和出口打 INFO 日志,记录耗时,拿来做接口性能分析。
- 外部渠道回调监控:代购系统对接了不少海外支付渠道,每次回调都记录渠道名称、回调内容、处理结果、耗时。哪个渠道最近回调成功率下降了,一查日志就明白。
- 采购员抢单行为:记录采购员ID、抢单时间、订单金额,这个数据在分析采购员活跃度和接单策略时非常有用。
- 物流状态推送:清关、起飞、落地、派送、签收这些节点都记录,后面做时效分析全靠它。
这些设计在做的时候多花不了多少时间,但后面受益很大。不过提醒一句:埋点要克制,一条日志不是打得越多越好,每一条都要想清楚“将来谁会用它,解决什么问题”。只为了自己调试方便打的日志,该删就得删,留着只会增加存储成本。
3. 基于Filebeat + Logstash + Elasticsearch + Kibana的落地实践
架构定好了,日志规范也统一了,接下来就是最硬核的部署和实施阶段。我用的这套组合是非常经典的 ELK 技术栈,网上资料虽然多,但真正能拿来就用、少走弯路的细节,还是得自己踩过坑才知道。
3.1 Docker Compose快速搭建ELK环境
我们大部分业务服务已经容器化部署了,所以 ELK 这套也直接用 Docker Compose 跑,维护起来省心很多。我把 compose 文件放出来,基本就是可用的版本。
version: '3' services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9 container_name: es-node environment: - cluster.name=es-docker-cluster - node.name=es-node1 - discovery.type=single-node - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms4g -Xmx4g" - TZ=Asia/Shanghai ulimits: memlock: soft: -1 hard: -1 volumes: - /data/es-data:/usr/share/elasticsearch/data ports: - "9200:9200" networks: - elk-net - app-net restart: always logstash: image: docker.elastic.co/logstash/logstash:7.17.9 container_name: logstash environment: - TZ=Asia/Shanghai - LS_JAVA_OPTS=-Xms2g -Xmx2g volumes: - /data/logstash/pipeline/:/usr/share/logstash/pipeline/ - /data/logstash/patterns/:/usr/share/logstash/patterns/ ports: - "5044:5044" depends_on: - elasticsearch networks: - elk-net - app-net restart: always kibana: image: docker.elastic.co/kibana/kibana:7.17.9 container_name: kibana environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 - TZ=Asia/Shanghai - I18N_LOCALE=zh-CN ports: - "5601:5601" depends_on: - elasticsearch networks: - elk-net restart: always networks: elk-net: driver: bridge app-net: external: true部署的时候有两个细节值得展开说说。
第一个是内存设置。ES的ES_JAVA_OPTS给的4g不是随便写的。ES 官方建议是把堆内存设置为物理内存的一半,但是不能超过 31GB(超过了会有压缩指针失效的问题)。如果服务器总共 16G 内存,分配 4G 给 ES、2G 给 Logstash 是合理的,剩下的要给 Filebeat、Kibana 和操作系统缓存留余地。ES 的bootstrap.memory_lock=true是为了锁定内存,防止发生 swap 导致性能剧烈抖动,这一步强烈建议开启。
第二个是网络规划。我把 ELK 组件放一个独立网段elk-net,业务服务在app-net网段,两边通过external: true挂载同一个业务网络。这样 Filebeat 在业务容器里可以直接用logstash:5044这个服务名连上 Logstash,不需要暴露公网端口,安全很多。
3.2 模板化Logstash解析配置,多服务日志一招通吃
Logstash 的 pipeline 配置是整个链路里最值得花心思的地方。如果每个服务的日志格式不一样,就要写一堆不同的解析规则,维护成本会爆炸。我前面提到“日志格式统一”的价值就在这里体现出来了。
我的 Logstash pipeline 是按服务名做分发,每个服务一套 grok 正则,但配置结构高度模板化。看一眼其中一个配置就明白套路了:
input { beats { port => 5044 } } filter { if [service] == "order-service" { grok { match => { "message" => "%{TIMESTAMP_ISO8601:log_time}\|%{LOGLEVEL:level}\|order-service\|%{UUID:trace_id}\|order\|%{DATA:business_id}\|%{NUMBER:user_id}\|%{GREEDYDATA:log_content}" } } date { match => ["log_time", "yyyy-MM-dd HH:mm:ss.SSS"] target => "@timestamp" } } else if [service] == "purchase-service" { // 类似的正则规则 } mutate { remove_field => ["message", "path", "host", "beat"] } } output { elasticsearch { hosts => ["elasticsearch:9200"] index => "app-logs-%{service}-%{+YYYY.MM.dd}" template => "/usr/share/logstash/templates/logstash.json" template_name => "app-logs" template_overwrite => true } }这套配置里有三个关键设计想跟大家分享。
第一是字段精简。我在mutate里把message、path、host这些原始字段全部删掉了。减少存储压力是次要的,更主要的原因是 ES 索引字段越简洁,聚合查询的时候越不容易踩字段映射冲突的坑。
第二是索引按天滚动。索引名带上了日期,配合 ES 的索引生命周期管理策略,30 天前的索引可以自动关闭甚至删除,这样就能控制好磁盘占用。
第三是自定义模板。日志里如果带纯数字的 JSON 字段,ES 有可能发生类型映射冲突。比如某个字段这次是数字,下次是字符串,就会导致索引 mapping 报错。提前在模板里把字段类型固定下来,就从根上避免了这个痛苦。
3.3 服务端Filebeat配置与常见翻车点
业务服务器上跑 Filebeat,我一开始踩了不少坑。直接把我调好的配置放出来:
filebeat.inputs: - type: container enabled: true paths: - /var/lib/docker/containers/*/*.log json.keys_under_root: true json.add_error_key: true filebeat.config.modules: path: ${path.config}/modules.d/*.yml reload.enabled: true processors: - add_docker_metadata: host: "unix:///var/run/docker.sock" - decode_json_fields: fields: ["message"] target: "json" - drop_fields: fields: ["container.image.name", "stream"] output.logstash: hosts: ["logstash:5044"] loadbalance: true logging.level: warning使用 Docker 容器日志有个特别关键的问题,容器输出的 JSON 日志和代码里设置的多行堆栈是对不上的。比如 Java 应用打一个异常堆栈,原本是一条日志里有很多行,但 Docker 默认会把每一行都当成一个独立的日志消息存下来。Filebeat 采集的时候如果不知道要合并,一条完整堆栈就会被拆成五六条,在 Kibana 里看到的报错就是支离破碎的,没法完整分析异常原因。
解决办法是使用multiline多行配置。Filebeat 里提供了multiline.type和multiline.pattern配置项来合并多行日志:
filebeat.inputs: - type: container enabled: true paths: - /var/lib/docker/containers/*/*.log json.keys_under_root: true multiline.type: pattern multiline.pattern: '^\d{4}-\d{2}-\d{2}' multiline.negate: true multiline.match: after这个配置的意图是:如果一行的开头不是以2024-11-20这种日期格式开头,就说明它是上一条日志的延续,应该合并到上一条里去。multiline.negate: true表示“不匹配这个规则的都视为延续行”,multiline.match: after表示“把延续行拼接到前一条后面”。
使用 Java 的 logback 日志框架时,如果配置了pattern输出时间戳格式不一致,需要你把正则改成和实际格式匹配的。我见过有些同事卡在这个环节一晚上,就是因为正则和日志开头的时间格式对不上,合并始终不生效,白白浪费好几个小时。
另外再分享一个 Filebeat 的细节。add_docker_metadata这个 processor 会从 Docker 读取容器信息,把服务的容器名、镜像名都加到日志字段里。这个很有用,因为不同的环境可能跑同一个服务名,区分环境和版本都得靠容器的元数据。但要注意给 Filebeat 挂载/var/run/docker.sock,不然它取不到 Docker 的信息,这个 processor 就静默失效了。
4. Elasticsearch索引生命周期与性能调优实践
ELK 部署完成之后,日志不断往 ES 里灌入,很快就会遇到存储和性能的问题。代购系统到了大促节点,采购单和订单量能翻几倍,日志写入量更是水涨船高。如果不对 ES 做生命周期管理和调优策略,集群很容易变成定时炸弹。
4.1 索引生命周期管理,磁盘不是无限大的
日志这种数据有个特点:越新的越值钱,越老的价值越低。一周前的日志偶尔查查还能接受,一个多月前的日志基本就没什么人会去碰了。既然这样,没必要让所有索引都保持同样的副本数和存储状态。
我配置的是经典的 30 天生命周期策略:
| 阶段 | 触发条件 | 动作 |
|---|---|---|
| Hot | 写入中 | 默认配置,索引持续写入 |
| Warm | 7天 | 合并分段,下调副本数为0 |
| Delete | 30天 | 删除索引,释放磁盘空间 |
配置这个策略用 ES 的 Index Lifecycle Management 来做,一段简单配置是这样的:
PUT _ilm/policy/log-policy { "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_size": "30gb", "max_age": "1d" } } }, "warm": { "min_age": "7d", "actions": { "forcemerge": { "max_num_segments": 1 } } }, "delete": { "min_age": "30d", "actions": { "delete": {} } } } } }这份策略里我用了rollover配置。意思是当索引超过 30GB 或者超过 1 天还没滚动,就会自动创建新索引。好处有两个:单个索引的碎片数量不会失控,被删掉的历史索引数据量也适中。
每天大概产生几 GB 日志的时候,磁盘规划很简单,500G 基本可以稳定跑两个月。但如果是日志量特别大的业务,建议升级到冷热架构或者对象存储归档,成本会更优。
4.2 索引mapping优化,预防字段映射冲突
日志场景里最常见的 mapping 坑就是 Es 动态映射把字段猜错了类型。我之前踩过一个特别典型的坑:订单号的 JSON 里有个字段叫status,有时候是字符串"SUCCESS",有时候是数字1,ES 动态映射先见到字符串就把它设为text,之后来了数字字段就拒绝写入,整个 logstash 管道报错,后面日志全都堵住了。
这个问题的标准解决方案是在索引模板里把字段类型全部锁死。对于不确定的数据,直接放弃自动映射,全部设置为keyword类型最保险:
PUT _index_template/app-logs-template { "index_patterns": ["app-logs-*"], "template": { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "index.lifecycle.name": "log-policy" }, "mappings": { "dynamic": "strict", "properties": { "@timestamp": { "type": "date" }, "service": { "type": "keyword" }, "level": { "type": "keyword" }, "trace_id": { "type": "keyword" }, "business_id": { "type": "keyword" }, "user_id": { "type": "keyword" }, "log_content": { "type": "text", "analyzer": "ik_max_word" } } } } }这种设置下,如果来了一个模板里没定义的新字段,写入会直接报错而不是自动创建。有些人会觉得太死板了,不灵活,但我觉得日志场景里灵活反而是灾难。新增字段时模板加一条配置就行了,几分钟的事,总比线上写入被中断要好得多。
需要特别说明一下,ik_max_word这个分词器是为中文内容准备的,需要你自己给 ES 装 IK 插件。日志里的中文内容很多时候是提示信息,比如“订单创建失败:库存不足”,我们想按“库存不足”搜到它,就必须有中文分词能力。如果不做这一步,中文搜索体验会非常差,搜一个完整句子可能匹配不到,搜单个字又可能匹配出一堆无关结果。
4.3 Filebeat到Kafka的缓冲架构升级(可选)
如果日志量只有每天几十 GB,Logstash 直接消费 Filebeat 的数据,顶多偶尔有点延迟,完全够用了。但如果到了大促节点,日志量瞬间飙升到每秒几千条,Logstash 稍有不慎就会成为瓶颈,然后 Filebeat 的积压队列开始涨,最后丢日志。
这时候就要在 Filebeat 和 Logstash 之间加一个 Kafka 做消息缓冲。日志进 Kafka,Logstash 以自己吃掉的速度去消费,谁慢谁调整,谁挂了也不影响上游。
我实测下来的体感是,Kafka 的引入让日志链路的稳定性上升了一个大台阶。代价是运维复杂度提高了,Kafka 本身也需要部署和监控。所以这个方案适合日志量确实大、或者对日志不丢失有强要求的场景,如果量不大,完全没必要增加维护负担。
5. 日志驱动的业务价值挖掘:从排障到监控预警
日志系统真正跑起来之后,价值很快就不只停留在“出问题能查到日志”这个层面了。基于 ES 聚合出来的数据做业务分析和监控,才是这个系统最香的部分。
5.1 基于Kibana做实时业务监控面板
Kibana 的 Dashboard 功能非常实用。我建了几个监控面板,跟大家分享下思路。
第一个是支付网关异常监控。这张面板按照支付渠道分组展示回调失败数量、平均回调延迟、最近 30 分钟失败趋势。每天只要扫一眼面板,就能知道早上那批“渠道回调 Timeout”是不是上升了。
构建方法是找到service=gateway AND level=ERROR的索引,做terms聚合按channel分组。把结果可视化成一个柱状图,右上角再叠一个折线图看时间趋势。
第二个是订单链路耗时分析。这里用关键的埋点数据,找出每笔订单从创建到支付成功再到采购接单,每个环节消耗了多久。在 RabbitMQ 消费线程池拥堵的时候,这个图看得特别明显,从“支付成功”到“采购接单”之间的时间差突然拉大,一查就能定位到是 MQ 消费出了问题还是采购端应用崩了。
第三个是接口 TOP 慢请求列表。用percentiles聚合查出耗时超过 95 分位的接口请求,每天列出 Top 20。经常能看到一些平时不太注意的接口,因为数据库 SQL 没加索引,单次耗时从 50ms 涨到 2 秒,这种问题如果没有日志分析,靠用户投诉才能发现,那就晚了。
5.2 MySQL日志分析与慢SQL发现
代购系统后端重度依赖 MySQL,慢查询基本属于家常便饭。ELK 里我只收集了应用日志,MySQL 的慢日志和错误日志也是分析的重点。我的做法是在装了 MySQL 的服务器上,通过 Filebeat 采集 MySQL 慢查询日志,经过 Logstash 解析后写入独立的索引。
MySQL 慢查询日志每行格式长这样就够 Logstash 解析了:
# Time: 2024-11-20T14:23:45.678901Z # User@Host: order_rw[order_rw] @ [10.0.0.12] Id: 882313 # Query_time: 3.210000 Lock_time: 0.000213 Rows_sent: 1 Rows_examined: 89032 SET timestamp=1732098225; SELECT * FROM order_item WHERE user_id = 10086 AND order_id = '20241120142345001' AND status = 1 ORDER BY created_at DESC;Logstash 里用 grok 正则把Query_time、Rows_examined、SQL 文本拆出来,然后写入 ES 专门的mysql-slowlog-*索引。之后我在 Kibana 上做了一张“慢 SQL 排行榜”,按Query_time降序排列,一眼就能看出哪些 SQL 需要优化。
这套机制上线后的第一个星期就抓到了一个非常典型的低级错误。采购员批量创建采购单时执行的一个 UPDATE 语句,因为联合索引的字段顺序建反了,导致全表扫描,单次执行耗时高达 800 毫秒。在慢日志里它的出现频率极高,占掉了前十名里的五个位置。后来调整了索引字段顺序,耗时直接降到了 30 毫秒以下,采购员那边反馈操作明显跟手了很多。
MySQL 错误日志也值得收一份。主从同步断掉、连接数打满、死锁这类严重问题,都会先出现在错误日志里,如果被动等监控报警,往往已经影响用户了,主动盯着错误日志的变化趋势,能提早发现苗头。
5.3 从“玄机”到确定性:玄学问题这样变透明
我琢磨着热搜词里“mysql日志分析 玄机”这个表述挺有意思。确实,很多时候日志分析做得不到位,系统问题就变成了玄学:某个订单数据丢失了,谁都不知道怎么回事;某个接口偶尔超时,重启就好,过了三天又犯。
有一个最典型的例子让我印象很深。上线了日志系统之后,有个售后客服反复反馈“偶尔有订单显示已签收,但用户实际没收到货”。因为物流状态回调是外部合作的物流公司推送的,没日志的时候我们只能反馈“查一下物流官网”,完全无从下手。
后来在日志里加了物流签收回调的完整记录,问题很快就水落石出。有一条签收记录是在凌晨 2 点 47 分推送的,而实际上快递员扫描签收的时间也是凌晨 2 点 47 分。但通过分析同一批签收记录发现了一个规律,部分异常单都是相同网点同一批次推过来的,而且推出时间与真正签收时间相差接近 20 个小时。
最后的结论是物流公司的某个网点扫描枪的时间设置错了,导致签收时间严重超前。虽然根本原因在外部,但我们的日志分析为这个判断提供了确凿的证据链,如果没有完整日志,这就是一个让人抓狂的“灵异事件”。
日志的价值就在这里,把玄学问题变成确定性结论,把“我觉得好像是”变成“日志里明确记录了”。
6. 实战中的血泪总结与操作技巧
这套系统从搭建到现在稳定运行,中间踩了不少坑,积累了不少心得。挑几个最值得说的放到这里,希望兄弟们少走点弯路。
6.1 排查链路故障的黄金指令与高频问题速查
日志系统也会出故障,关键是出了故障怎么快速定位。我整理了一些高频问题,按“症状-原因-解法”列个表,方便自己人快速排查:
| 症状 | 常见原因 | 排查命令/思路 |
|---|---|---|
| Kibana 搜不到新日志 | Filebeat 没起来或连不上 Logstash | 先看 Filebeat 日志:journalctl -u filebeat -f |
| Logstash 日志大量报错 | grok 正则没匹配上 | 打开 Logstash 的--debug,看是否是_grokparsefailure标签 |
| ES 写入拒绝 | 磁盘水位线满了 | 检查GET _cluster/allocation/explain,再清理旧索引 |
| 时区差了 8 小时 | 默认用了 UTC 时区 | ES 容器环境变量增加TZ=Asia/Shanghai,Logstash date 插件匹配后也要转本地时区 |
| 索引字段变成 float 导致精度丢失 | ES 动态映射猜测错误 | 修改索引 template 的 mapping 字段为long/keyword并重建索引 |
6.2 磁盘写满的未雨绸缪
日志服务本身比业务还吃磁盘,这是很多人低估的坑。ELK 组件全家桶跑起来,ES 数据、Logstash 队列、Kibana 缓存,一天下来几个 GB 非常正常。如果忘了做索引清理策略,磁盘写满是迟早的事,而磁盘写满后 ES 会变成只读,所有业务日志都写不进去,整个监控体系直接瘫痪。
除了前边说的 ILM 策略之外,我还会定时用 curl 写上一条 cron 清理任务,双保险:
# 每天凌晨2点执行,删除7天前的旧索引 0 2 * * * curl -X DELETE "http://localhost:9200/app-logs-$(date -d '7 days ago' +%Y.%m.%d)" >/dev/null 2>&1同时要监控 ES 和 Logstash 所在节点的磁盘使用率,建议超过 75% 就告警,85% 就要强制清理了。我在 Prometheus 里配过这类节点的磁盘监控,这块我建议你也有意识地给日志服务器做一个单独的磁盘告警,而不是和普通业务机混在一起设 90% 告警。
6.3 Filebeat加载均衡与并发上的一些配置细节
Filebeat 连接多个 Logstash 节点,默认会轮询负载均衡。但有个细节,loadbalance: true打开时,Filebeat 会把不同的事件发给不同的 Logstash 实例。这要求所有 Logstash 的配置保持一致,否则会出现同一个服务的日志,有的被解析了有的没被解析,混乱无比。
另外一个我踩过比较惨的坑是 Filebeat 的harvester_buffer_size。默认值是 16384 字节,如果某条日志非常大,比如一个异常堆栈带了很长的 SQL 或者 HTTP body,超过了缓冲大小,就会被截断。这一类问题不好查,因为日志看起来格式都对,只有最后的内容少了半截。有需要的话把它调到 1MB:
filebeat.inputs: - type: container enabled: true harvester_buffer_size: 10485766.4 日志采集对业务性能的影响到底多大
聊一下大家最关心的性能影响问题。在 Filebeat 刚上线时,我也担心过它对业务服务器的资源占用,实际情况反而很让人放心。
Filebeat 在日志量不大时,CPU 占用基本可以忽略,内存稳定保持在 30MB 上下。Logstash 解析是唯一比较吃资源的组件,因为 grok 正则的解析是 CPU 密集操作。但我们在 pipeline 里已经按服务做了 if 分支,每次只跑一条匹配规则而不是把正则全部试一遍,实际压测时,单台 8 核机器跑 Logstash 每秒处理 2 万条日志问题不大。
对于容器日志,如果 Filebeat 直接把整个容器的 stdout 日志采集走,业务容器完全不需要额外做任何事,这对性能影响为零。真正要注意的是日志在容器内的落盘方式,建议让日志直接打到 stdout 由 Docker 接管,还是直接重定向到文件再收集,这两种方式在 Docker 环境下性能差异很大,我一贯沿用 stdout 方案即可。
7. 日志系统的安全与权限控制不可忽视
日志里有很多敏感数据,可能有用户手机号、订单金额、收货地址、支付流水单号。整个系统使用越深入,越意识到权限控制是绝对不能跳过的环节。
7.1 日志脱敏处理
我这边在日志输出层面就做了脱敏。工具包里定义专门的日志输出方法,在拼字符串之前先对手机号、身份证号、真实姓名做脱敏处理。手机号输出138****8000,姓名输出张**。虽然在 Logstash 阶段也可以用正则再脱敏一次,但源头不输出更安全,双保险更安心。
7.2 Kibana 多租户与权限隔离
不同角色的人应该只能看到自己需要的数据。我给运营看订单业务面板,给开发看应用错误日志,给财务看对账报表。Kibana 基于 ES 的 RBAC 来做,创建几个角色然后绑定索引级权限,实测相当方便:
PUT _security/role/order_log_viewer { "cluster": [], "indices": [ { "names": ["app-logs-order-*"], "privileges": ["read", "view_index_metadata"] } ] }再创建响应用户并绑定角色:
PUT _security/user/order_ops { "password" : "some-strong-password", "roles" : [ "order_log_viewer" ], "full_name" : "Order Ops" }这样运营同事登录 Kibana 后只能看到订单服务的索引,连其他服务日志的存在都感知不到。这个权限隔离的效果很直接:之前有业务方会去日志里扒用户的完整收货信息,一旦收紧了权限,事故概率基本降为零。
8. 几个独家的排查脚本与实用技巧,直接能用
最后分享几个我在实际运维中天天用的小脚本。ELK 的运维不太适合天天在页面上点来点去,很多操作命令行更快更直观。
查看各索引大小排行,最实用的磁盘管理命令:
curl -s -X GET "http://localhost:9200/_cat/indices?v&s=store.size:desc" | head -20查看字段映射冲突,很多写入报错都能从这里找出原因:
curl -s -X GET "http://localhost:9200/app-logs-*/_mapping/field/status?pretty"清理持有大量删除标记的分段,磁盘空间明明删了很多文档但没释放时的首选操作:
curl -s -X POST "http://localhost:9200/app-logs-2024.11.01/_forcemerge?max_num_segments=1"Kibana 里排查慢加载时,ES 慢日志的查看命令也很重要。
curl -s -X GET "http://localhost:9200/app-logs-*/_settings?pretty" | grep -A3 indexing如果以前开启了 ES 的 slowlog 但没有正常输出,需要在 ES 的 log4j2.properties 里检查一下相关配置,这个问题当时也折腾过我不少时间。
9. 写在最后的实操建议
日志系统从零搭到现在,给我最深的体验是:它不是一个“一次性搭建完就结束”的项目,它是会持续演进的基础设施。从最初每天几个 G 日志,到后面大促单日接近 100G,架构从简单 Filebeat + Logstash 到中间加 Kafka,再到 ILM 策略、索引模板的多次调整,一直是边跑边优化的过程。
如果你是刚开始搞这套,我的建议是从最简单的架构起步,不要一上来就上 Kafka 加一堆组件。先统一日志格式,把链路追踪 ID 带上,用 Filebeat + Logstash + ES + Kibana 跑起来,感受一下日志分析和排查问题的工作方式变化,等你真正体验到了每天点几个按钮就能完成以前两小时排查工作的感觉,自然会理解接下来该优化什么、扩展什么。
最后分享一个我一直保留的排查习惯:遇到棘手的问题先不加猜测,先去 Kibana 把日志拉出来看,完整看一遍时间线再下结论。日志不会骗人,就看你想不想得到所有应该被记录下来的东西。日志系统不只是排查工具,更是让业务变得透明、让问题不再玄学的底气。