ELK 组件部署(Elasticsearch / Logstash / Kibana / Filebeat)
这篇把 ELK 四个组件的单机/集群独立部署过程整理在一起:怎么装、配置文件里哪几项必须改、起不来时先看什么,以及 Logstash 各类插件的用法。四个组件装完之后就能拼出各种日志链路。
如果要看的是 Kubernetes 场景下的日志方案(sidecar 采集、基于 Helm 的 EFK、以及 Filebeat + Kafka + Logstash + ES + Kibana 的 EFLK 链路),那部分内容在 Kubernetes 日志收集 里,本文不重复。
总览与数据流向
先交代环境和四个组件各自的位置,后面每一节的配置都基于这套主机名。
环境信息
复用已有的 hadoop 完全分布式集群,三个节点:
1 | 192.168.2.241 hadoop01 |
组件版本统一用 8.2.0,安装目录统一放在 /opt/bigdata/<组件名>/,并用软链接 current 指向具体版本目录,后续升级只需要换软链接。
各组件的分工
Elasticsearch:实时的分布式搜索和分析引擎,用于全文搜索、结构化搜索及分析,是整条链路的存储与检索层。三个节点都装,组成集群Kibana:Elasticsearch 的可视化前端,负责检索、图表和索引管理,只需要装一台(这里放在 hadoop03)Logstash:负责接收数据、解析过滤转换、再输出数据,是链路中的重量级处理环节Filebeat:轻量级日志采集器,装在产生日志的机器上,只负责把日志读出来发走,资源占用远低于 Logstash
常见的组合方式有两种:日志量不大时 Filebeat → Logstash → Elasticsearch → Kibana;日志量大或者需要削峰时,中间加一层 Kafka,Filebeat → Kafka → Logstash → Elasticsearch → Kibana。本文按 Elasticsearch、Kibana、Logstash、Filebeat 的顺序逐个装,最后做一次最小链路的联调。
Elasticsearch
三个节点都要装。官网下载地址 https://www.elastic.co/downloads/elasticsearch 。
解压与用户准备
Elasticsearch 默认不允许用 root 启动,先建一个专用用户:
1 | useradd elasticsearch |
配置文件
/opt/bigdata/elasticsearch/current/config/elasticsearch.yml,其中 node.name 每个节点不同,discovery.seed_hosts 与 cluster.initial_master_nodes 三个节点写一样:
1 | cluster.name: my-application |
⚠️ 注:8.x 默认开启 xpack 安全认证,这里为了实验方便直接关掉了,仅适用于隔离的内网环境。生产集群应保留认证与传输加密,Kibana、Logstash、Filebeat 侧相应配置账号密码或 API Key。
系统调优
这一步不做,Elasticsearch 很可能直接启动失败。
/etc/sysctl.conf:
1 | fs.file-max=655360 |
写完文件记得让它生效,否则内核参数还是默认值,bootstrap check 照样会拦下启动:
1 | sysctl -p # 或者临时生效:sysctl -w vm.max_map_count=262144 |
fs.file-max是系统最大打开文件描述符数,建议 655360 或更高vm.max_map_count限制单个进程能持有的内存映射区(VMA)数量,跟线程没有关系。Elasticsearch 要抬高它是因为用 mmapfs 映射 Lucene 段文件,要求至少 262144
/etc/security/limits.conf 添加如下内容。其中 memlock unlimited 对应配置文件里的 bootstrap.memory_lock: true,不放开这一项,内存锁定会失败并导致启动中断:
1 | * soft nproc 20480 |
/etc/security/limits.d/20-nproc.conf 会覆盖上面的 nproc 设置,一并改掉,或者直接把这个文件删掉:
1 | * soft nproc 20480 |
堆和 page cache 怎么分
ES 的内存分配有一条和其他 Java 服务不太一样的原则:不要把内存都给堆。它底层是 Lucene,段文件靠 mmap 映射后由操作系统的 page cache 承载,检索性能很大程度上取决于有多少段文件能常驻内存。堆开得太大,留给 page cache 的就少了,反而更慢。
所以常规做法是一半给堆、一半留给操作系统,并且堆有一个硬上限:
- 堆必须小于 32GB 左右的压缩指针(compressed oops)阈值。堆一旦到达这个阈值,JVM 就关掉对象指针压缩,对象头变大,实际能装的对象反而比 31GB 还少。
- 官方的保守建议是不超过 26GB;确认开启了 zero-based oops(启动日志里能看到
heap address: ..., zero based Compressed Oops)才可以到 30~31GB。 -Xms和-Xmx必须设成相同值,避免运行期扩堆造成停顿,也避免和下面的内存锁定打架。
配套还有一项文中容易漏掉的:bootstrap.memory_lock: true 需要系统侧同时放开 memlock 限制,否则 ES 启动时锁定失败会直接中断。
1 | # /etc/security/limits.conf |
锁定内存的目的是防止堆被换出到 swap——堆页一旦进 swap,GC 扫描时要把它们逐页换回来,一次 Full GC 能卡到分钟级。这也是为什么前面要求关掉 swap。
堆内存在 /opt/bigdata/elasticsearch/current/config/jvm.options 里设置,一般取可用内存的一半,且必须小于 32G(堆一旦到 32G 附近就会关掉压缩指针,-Xmx32g 正好落在失效那一侧,此时对象头变大、实际能装的对象比 31G 还少)。官方保守值是不超过 26G,确认开启了 zero-based oops 才可以到 30~31G。剩下的内存会被 Lucene 用作文件系统缓存 —— Lucene 是一个开源全文检索工具包,Elasticsearch 底层就是基于它实现的:
1 | -Xms4g |
启动与集群验证
用 elasticsearch 用户启动,-d 表示后台运行,起不来就看 path.logs 下的日志:
1 | cd /opt/bigdata/elasticsearch/current |
访问任意节点的 9200 端口,能返回版本信息就算成功:
1 | curl hadoop01:9200 |
1 | { |
Kibana
Kibana 是 Elasticsearch 的检索与可视化界面,本身不存数据,只要能连上 ES 就行,所以只在 hadoop03 上装一台。官网下载地址 https://www.elastic.co/downloads/kibana 。
安装与配置
1 | wget https://artifacts.elastic.co/downloads/kibana/kibana-8.2.0-linux-x86_64.tar.gz |
修改 /opt/bigdata/kibana/current/config/kibana.yml:
1 | server.port: 5601 |
⚠️ 注:
elasticsearch.url是 6.x 的写法,7.x 起改成了列表形式并且必须带协议头,8.2 上应写作elasticsearch.hosts: ["http://hadoop01:9200"],同时建议把三个节点都列上以便故障时自动切换。
浏览器直连 ES 的场景下(例如一些自建面板),需要在 hadoop01 的 /opt/bigdata/elasticsearch/current/config/elasticsearch.yml 里放开跨域:
1 | http.cors.enabled: true |
启动
1 | cd /opt/bigdata/kibana/current/ |
启动后访问 http://hadoop03:5601 ,页面里的 Discover 用来查日志,Stack Management 里管理索引。
Logstash
Logstash 实现的功能分为接收数据、解析过滤并转换数据、输出数据三部分,分别对应三类插件:
input插件,必选filter插件,可选output插件,必选
配置文件就是这三段的组合:
1 | input { |
更准确地说,Logstash 不只是一个 input → filter → output 的数据流,而是一个 input → decode → filter → encode → output 的数据流,中间的编解码由 codec 插件完成。
安装与最小示例
官网下载地址 https://www.elastic.co/downloads/logstash 。所有节点用 root 用户安装:
1 | wget https://artifacts.elastic.co/downloads/logstash/logstash-8.2.0-linux-x86_64.tar.gz |
先用命令行参数跑一个读标准输入、打印到标准输出的最小例子:
1 | cd /opt/bigdata/logstash/current |
输入 123 # 输入 后得到:
1 | { |
三个要点:-e 表示直接执行后面的配置;input 选了 stdin;output 选了 stdout,其中 codec 是插件,用来指定输出格式,rubydebug 是专门用来做测试的格式,在终端输出的是 Ruby Hash 形式("key" => value)而不是 JSON——下面的示例本身就不是合法 JSON。真要输出 JSON 用 codec => json。
同样的内容写成配置文件 /opt/bigdata/logstash/current/logstash-simple.conf:
1 | input { |
用 -f 指定配置文件启动,输出与上面一致:
1 | bin/logstash -f logstash-simple.conf |
input 插件
从文件读取数据,start_position => "beginning" 表示从文件开头读:
1 | input { |
从标准输入读取,同时给事件加字段和标签:
1 | input{ |
输入 hello world,可以看到 type、tags、key 都进到事件里了:
1 | { |
从网络读取 TCP 数据,顺便用 grok 解析 syslog 行,配置文件 /opt/bigdata/logstash/current/logstash-tcp.conf:
1 | input { |
启动后在另一个终端用 nc 把日志灌进去:
1 | bin/logstash -f logstash-tcp.conf |
结果节选,可以看到 timestamp、process 等字段已经被 SYSLOGLINE 拆出来了:
1 | { |
编码插件(Codec)
编码插件用于在输入或输出时处理不同类型的数据,前面用到的 rubydebug 就是其中一个。常见的格式有 plain、json、json_lines 等。
plain 直接输出原始文本:
1 | input{ |
1 | hello world # 输入 |
json 输出压缩成一行的 JSON:
1 | input { |
1 | hello world # 输入 |
过滤器插件(Filter)
丰富的过滤器插件是 Logstash 功能强大的重要因素。名字叫过滤器,实际提供的不只是过滤能力,还可以对进入的原始数据做复杂的逻辑处理,甚至往后续流程里添加新的事件。
以这样一行访问日志为例:
1 | 192.168.2.241 [16/Jun/2021:16:24:19 +0800] "GET / HTTP/1.1" 403 5039 |
最常用的是三个插件配合:grok 做正则捕获拆字段、date 做时间处理、mutate 做字段改名与类型转换:
1 | input { |
转换后的结果,注意 @timestamp 已经变成日志里的时间而不是采集时间:
1 | { |
输出插件(Output)
常用的输出有这么几类:stdout 一般只用来调试;file 把日志写到磁盘文件;elasticsearch 把数据发给 ES,便于高效查询和长期保存;此外还支持 Nagios、HDFS、Email、Exec 等。
输出到标准输出,也就是前面一直在用的模式:
1 | input{ |
输出到文件,路径里可以直接用时间和字段做变量:
1 | input{ |
启动后输入 hello world,落盘内容如下。这里 %{host} 取到的是一个对象,所以文件名有点难看,实际使用时建议写成具体的子字段(如 %{[host][hostname]}):
1 | $ cat /data/log/2022-05-23/\{\"hostname\"\:\"hadoop03\"\}_02.log |
输出到 Elasticsearch 的写法见后面的「联调验证」。
Filebeat
Filebeat 装在需要采集日志的机器上,这里三个节点都用 root 用户安装。官网下载地址 https://www.elastic.co/cn/downloads/beats/filebeat 。
解压安装
1 | wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.2.0-linux-x86_64.tar.gz |
输出到 Kafka
集群里已经装了 Kafka,所以让 Filebeat 直接把日志投到 Kafka。
先想清楚一件事:你希望投到 Kafka 的消息长什么样。 下面这份配置里有几组选项其实是互相抵消的,先决定形态能省很多来回:
codec.format.string: '%{[message]}'的意思是”只发原始日志行”。一旦写了这个,前面所有add_*_metadataprocessor 生成的字段、以及drop_fields删掉的字段,统统作废——因为输出根本不带它们。想保留结构化字段就别配codec.format.string,让它默认发 JSON。- 反过来,如果确实只要原始行,那么
add_host_metadata、add_docker_metadata这些就该一并去掉,它们白白消耗 CPU 和内存。 drop_fields里删host、ecs、input之前想一下:Logstash 侧还需不需要靠host.hostname区分来源?删早了后面就补不回来。setup.template.settings和setup.kibana在 output 是 Kafka 时完全不生效。索引模板注册和 Kibana 加载走的是 ES output 那条路,Kafka 输出时这两段是空配置,留着只会让人误以为模板已经建好了。真要注册模板得临时切到 ES output 跑一次filebeat setup,或者干脆在 Logstash/ES 侧手工建。
一句话:要原始行就砍掉所有 processor;要结构化就砍掉 codec.format.string。 两者都留着,实际生效的永远是后者,前面的配置全是自我安慰。修改 /opt/bigdata/filebeat/current/filebeat.yml,采集登录日志 /var/log/secure:
1 | filebeat.inputs: |
几个容易踩的点:
topic写死成了my_test,fields.log_topic并没有生效。要按来源分 topic,就把它写成topic: '%{[fields][log_topic]}'drop_fields用来裁掉 Filebeat 自动附加的一大堆元数据字段,日志量大时能省不少带宽和存储codec.format.string决定投出去的是原始行还是 JSON,二者对下游 Logstash 的解析方式影响很大logging.level: debug只适合调试期,稳定后改回info
启动与在 Kafka 侧验证
用 root 用户启动,-e 表示日志打到标准错误、-c 指定配置文件:
1 | cd /opt/bigdata/filebeat/current |
改完配置要重启 Filebeat,然后 ssh 连一次主机制造新日志。在 Kafka 侧消费对应 topic:
1 | cd /opt/bigdata/kafka/current/bin |
默认配置下消息是完整的 JSON,字段非常多:
1 | {"@timestamp":"2022-05-22T08:20:32.262Z","@metadata":{"beat":"filebeat","type":"_doc","version":"8.2.0"},"ecs":{"version":"8.0.0"},"log":{"offset":3204,"file":{"path":"/var/log/secure"}},"message":"May 22 04:20:30 hadoop02 sshd[18047]: pam_systemd(sshd:session): Failed to release session: Interrupted system call","input":{"type":"log"},"host":{"containerized":false,"ip":["192.168.2.242","fe80::ec97:d991:4336:2e98","fe80::f0df:f765:7f99:9634"],"mac":["00:0c:29:68:79:09"],"hostname":"hadoop02","name":"hadoop02","architecture":"x86_64","os":{"type":"linux","platform":"centos","version":"7 (Core)","family":"redhat","name":"CentOS Linux","kernel":"3.10.0-1160.el7.x86_64","codename":"Core"},"id":"0988a88e747e428dbcf4fdc212a6c1ac"},"agent":{"ephemeral_id":"4f44629c-d5e9-4ae4-a5fc-6f96df866dfe","id":"5d7e5f81-16e5-4863-b736-89f6873105ec","name":"hadoop02","type":"filebeat","version":"8.2.0"}} |
加上 drop_fields 之后,只剩下关心的字段:
1 | {"@timestamp":"2022-05-22T08:32:49.556Z","@metadata":{"beat":"filebeat","type":"_doc","version":"8.2.0"},"log":{"file":{"path":"/var/log/secure"},"offset":5550},"message":"May 22 04:32:49 hadoop02 sshd[23225]: Received disconnect from 192.168.2.242 port 55948:11: disconnected by user","agent":{"name":"hadoop02"}} |
再加上 codec.format.string: '%{[message]}',投出去的就是原始日志行:
1 | May 22 04:43:41 hadoop01 sshd[3106]: Accepted publickey for root from 192.168.2.243 port 41710 ssh2: RSA SHA256:F1RBzp64noGdTdwWX8w+PYfi0zs8ifzkv+etLAOaCJQ |
联调验证
四个组件都起来之后,用最短的一条链路验证一遍:Logstash 读本机日志文件,处理后写入 Elasticsearch,再到 Kibana 里查。
新建 /opt/bigdata/logstash/current/secure_into_es.conf:
1 | input { |
这份配置里有三个地方是踩过坑之后才补上的,单独说一下。
一、date 过滤器不能省。 前面第 4 节专门强调过要把日志时间解析进 @timestamp,但落地示例里如果只有 grok 没有 date,@timestamp 就会是 Logstash 读到这一行的时刻。配上 start_position => "beginning" 之后,一个存了半年的 /var/log/secure 会被整批打上”导入那一分钟”的时间戳——在 Discover 里按时间轴看全糊成一根竖线,正是前文想避免的结果。
二、索引名用 %{+YYYY.MM.dd} 会在跨年那几天出错。 Joda 时间格式里大写 YYYY 是 week-year(ISO 周所属的年),不是日历年。12 月最后几天如果属于下一年的第 1 周,YYYY 就会给出下一年:
| 日期 | yyyy.MM.dd |
YYYY.MM.dd |
|---|---|---|
| 2025-12-28 | 2025.12.28 |
2025.12.28 |
| 2025-12-29 | 2025.12.29 |
2026.12.29 |
于是每年年底会凭空多出一批索引名跳到明年的数据,按 secure-2025.* 查就漏掉了。小写 yyyy 才是日历年。同一篇里第 522 行 file output 用的正是小写,两处本来就不一致——统一成小写即可。
三、%{SYSLOGLINE} 会把 message 变成数组。 这个内置 pattern 内部自己又捕获了一个名叫 message 的字段,而 grok 对已存在的字段是追加而不是覆盖。结果 message 从字符串变成了两元素数组(原始整行 + 解析出的正文),写进 ES 后成了多值字段,影响检索和高亮,而且报错信息里看不出原因,很容易以为是自己 pattern 写错了。加 overwrite => ["message"] 让它覆盖即可。
启动 Logstash:
1 | cd /opt/bigdata/logstash/current |
先在 ES 侧确认索引已经建出来、文档数在涨:
1 | curl "hadoop01:9200/_cat/indices?v" |
再到 Kibana(http://hadoop03:5601 )里为 secure-* 建一个 data view,就能在 Discover 中按时间和字段检索了。
需要中间加 Kafka 缓冲的完整链路(Filebeat → Kafka → Logstash → Elasticsearch → Kibana),包括 Logstash 侧的 kafka input 写法和 Kibana 建索引的界面步骤,见 Kubernetes 日志收集 一文的 EFLK 部分。