1. 为什么我们需要Flink+ELK:告别“日志迷宫”
不知道你有没有过这样的经历:线上一个Flink作业突然报错,你火急火燎地登录到Yarn集群,想找到那个出错的TaskManager日志。结果发现,因为任务已经失败,Yarn早就把对应的Container给回收了,日志文件也消失得无影无踪。或者,面对一个按500MB大小滚动生成的几十个日志文件,为了定位一个跨了多个文件的错误,你得用grep、tail -f、less来回切换,看得眼花缭乱,效率极低。
这就是传统基于文件的日志管理方式在分布式、云原生环境下的典型痛点。日志分散在各个节点,生命周期与计算资源绑定,一旦任务结束或节点重启,宝贵的调试信息就可能丢失。对于Flink on Yarn这类资源动态调度的场景,这个问题尤其突出。
我踩过几次坑之后,决定把日志管理升级一下。我们的目标很明确:集中、实时、可查。简单说,就是把所有Flink作业(不管是Standalone还是on Yarn)产生的日志,实时地收集到一个中心化的地方,并且能像用搜索引擎一样,快速、灵活地查询和分析。
这时候,Flink + ELK的组合就闪亮登场了。这个方案的核心思路是:让Flink把日志“流式”地写出来,而不是死守在本地文件里。我们利用Flink的Log4j或Logback框架,配置一个KafkaAppender,把日志事件当成一条条消息,实时发送到Kafka消息队列。然后,由ELK栈(Elasticsearch, Logstash, Kibana)接手:Logstash从Kafka消费日志,进行必要的解析和清洗,然后存入Elasticsearch这个强大的搜索引擎,最后通过Kibana这个可视化工具,我们可以轻松地搜索、过滤、聚合,甚至制作实时监控仪表盘。
这个方案的好处是显而易见的:
- 日志不丢失:日志发到了Kafka,就有了持久化保障,即使Flink任务挂了、Yarn回收了容器,日志也安然无恙。
- 集中管理:所有日志都在Elasticsearch里,一个入口就能查所有作业、所有实例的日志。
- 实时性强:从日志产生到能被搜索到,延迟通常在几秒内,对于排查线上问题至关重要。
- 查询强大:告别
grep。你可以用Kibana的查询语法,轻松实现类似“查找过去5分钟内,所有ERROR级别、包含NullPointerException、且来自订单处理作业的日志”这样的复杂查询。 - 便于分析:可以对日志进行字段化解析(比如提取出错误码、用户ID、响应时间),然后做统计图表,比如统计错误率趋势、高频错误类型等。
下面,我就手把手带你搭建这套系统,从Flink配置开始,一直到在Kibana里看到漂亮的日志图表。
2. 第一步:让Flink日志“流”入Kafka
要让Flink把日志写到Kafka,核心是修改它的日志配置文件。Flink默认使用Log4j2,我们需要配置一个Kafka Appender。
2.1 基础环境与版本选择
首先,确保你的组件版本兼容。我这里用一个经过验证的组合,你可以根据实际情况调整:
| 组件 | 版本 | 说明 |
|---|---|---|
| Flink | 1.16.1 | 核心计算引擎 |
| Kafka | 2.0.0 (或 3.x) | 消息队列,做日志缓冲和传输 |
| Logstash | 6.5.4 | 日志收集与处理管道 |
| Elasticsearch | 6.3.1 | 日志存储与检索引擎 |
| Kibana | 6.3.1 | 日志可视化与分析界面 |
注意:版本兼容性很重要,特别是Logstash的
logstash-input-kafka插件与Kafka客户端的版本。如果Kafka版本较新(如3.x),可能需要使用更高版本的Logstash(如7.x+)或手动替换Logstash自带的Kafka客户端JAR包。
2.2 关键配置:修改log4j.properties
Flink的日志配置文件位于conf/log4j.properties。我们需要在其中添加一个Kafka Appender。这里我给出一个详细配置并解释每个关键参数。
# 1. 定义Kafka Appender
appender.kafka.type = Kafka
appender.kafka.name = Kafka
# 同步发送,确保日志不丢失,但性能略有影响。对于日志场景,可靠性优先。
appender.kafka.syncSend = true
# 是否忽略发送异常,设为false,让日志系统感知发送失败。
appender.kafka.ignoreExceptions = false
# 日志要发送到的Kafka主题,可以按环境或集群命名,如 `flink-prod-logs`
appender.kafka.topic = flink_logs
# 配置Kafka Broker地址
appender.kafka.property.type = Property
appender.kafka.property.name = bootstrap.servers
appender.kafka.property.value = kafka-broker1:9092,kafka-broker2:9092,kafka-broker3:9092
# 2. 配置日志格式(Layout)
# 使用JSON格式,便于后续Logstash和Elasticsearch解析
appender.kafka.layout.type = JSONLayout
# 使用Logstash兼容的JSON布局,这样Logstash可以直接用`codec => json`解析
appender.kafka.layout.value = net.logstash.log4j.JSONEventLayoutV1
# 紧凑模式,减少输出体积
appender.kafka.layout.compact = true
# 不输出完整的日志事件元信息(如位置信息等),可根据需要开启
appender.kafka.layout.complete = false
# 3. 添加自定义字段(非常有用!)
# 在日志JSON中增加我们关心的上下文信息,方便后续过滤和聚合。
# 例如,添加日志文件路径
appender.kafka.layout.additionalField1.type = KeyValuePair
appender.kafka.layout.additionalField1.key = log_path
appender.kafka.layout.additionalField1.value = ${sys:log.file}
# 添加Flink作业名称(这个需要我们在提交作业时传递)
appender.kafka.layout.additionalField2.type = KeyValuePair
appender.kafka.layout.additionalField2.key = flink_job_name
appender.kafka.layout.additionalField2.value = ${sys:flinkJobName}
# 添加Yarn Container ID(仅Flink on Yarn模式需要,用于定位容器)
appender.kafka.layout.additionalField3.type = KeyValuePair
appender.kafka.layout.additionalField3.key = yarn_container_id
appender.kafka.layout.additionalField3.value = ${sys:yarnContainerId}
# 4. 将Kafka Appender挂载到根Logger
rootLogger.appenderRef.kafka.ref = Kafka
# 通常我们同时保留文件输出,作为本地备份
rootLogger.appenderRef.file.ref = RollingFileAppender
rootLogger.level = INFO
配置要点解析:
syncSend = true:对于日志这种“可接受少量延迟但绝不能丢”的数据,建议设为true,保证每条日志都成功发送到Kafka。如果日志量极大且对性能敏感,可以评估设为false(异步发送)的风险。- JSON格式:使用
JSONLayout或JSONEventLayoutV1能让日志结构化。到了Elasticsearch里,每个字段(如level,message,thread)都会被自动索引,查询效率极高。 - 自定义字段:这是把日志从“文本”变成“数据”的关键一步。通过
${sys:...}可以获取系统属性。比如,我们通过-DflinkJobName=MyJob在启动时传入作业名,这样所有该作业的日志都会带上flink_job_name字段,在Kibana里就可以轻松按作业名筛选了。
2.3 为Flink on Yarn模式传递自定义参数
在Yarn模式下,我们需要把yarnContainerId和flinkJobName这些信息传递给日志框架。这需要修改flink-conf.yaml和作业提交脚本。
首先,在flink-conf.yaml中,添加以下配置,让Flink将YARN的Container ID设置为Java系统属性:
env.java.opts.taskmanager: -DyarnContainerId=$CONTAINER_ID
env.java.opts.jobmanager: -DyarnContainerId=$CONTAINER_ID
然后,在提交Flink on Yarn作业时,通过-D参数传递作业名:
./bin/flink run-application \
-t yarn-application \
-D yarn.application.name=MyFlinkJob \
-D env.java.opts="-DflinkJobName=MyFlinkJob" \
-c com.example.YourMainClass \
/path/to/your-job.jar
注意:-D env.java.opts="-DflinkJobName=xxx" 这个参数是必须的,否则上一步配置中的${sys:flinkJobName}就获取不到值,flink_job_name字段会是空的。
2.4 依赖与验证
添加依赖:确保Flink的lib目录下有对应版本的Kafka客户端JAR包,例如kafka-clients-3.1.0.jar。如果使用JSONLayout,可能还需要Jackson相关的JAR包(jackson-core, jackson-databind, jackson-annotations)。这些包通常Log4j2的Kafka appender依赖中会包含,但最好检查一下。
验证日志是否成功写入Kafka:配置完成后,启动你的Flink作业(Standalone或Yarn模式)。然后,用Kafka自带的控制台消费者工具检查flink_logs主题:
./bin/kafka-console-consumer.sh --bootstrap-server kafka-broker1:9092 --topic flink_logs --from-beginning
你应该能看到一串串JSON格式的日志输出,类似下面这样:
{
"instant": { "epochSecond": 1698723428, "nanoOfSecond": 544000000 },
"thread": "flink-akka.actor.default-dispatcher-17",
"level": "INFO",
"loggerName": "org.apache.flink.runtime.rpc.akka.AkkaRpcService",
"message": "Stopped Akka RPC service.",
"endOfBatch": false,
"loggerFqcn": "org.apache.logging.slf4j.Log4jLogger",
"threadId": 68,
"threadPriority": 5,
"log_path": "/yarn/container-logs/application_1697779774806_0046/container_1697779774806_0046_01_000002/taskmanager.log",
"flink_job_name": "MyFlinkJob",
"yarn_container_id": "container_1697779774806_0046_01_000002"
}
看到这样的数据,恭喜你,Flink到Kafka的桥梁已经打通了!
3. 第二步:用Logstash搭建数据处理管道
Logstash的角色是“数据搬运工”兼“初级清洗工”。它从Kafka拉取原始的JSON日志,可以进行一些简单的处理(比如解析时间戳、过滤字段),然后批量写入Elasticsearch。
3.1 编写Logstash配置文件
创建一个配置文件,比如logstash-flink.conf。它的结构通常是input -> filter (可选) -> output。
input {
kafka {
# 你的Kafka集群地址
bootstrap_servers => "kafka-broker1:9092,kafka-broker2:9092,kafka-broker3:9092"
# 消费者组ID,可以任意命名,用于偏移量管理
group_id => "logstash-flink"
# 要消费的主题,就是Flink日志写入的那个
topics => ["flink_logs"]
# 消费线程数,根据吞吐量调整
consumer_threads => 3
# 为事件添加一个类型字段,便于后续区分
type => "flink-logs"
# 因为Flink输出的是JSON字符串,这里用json解码器
codec => json
# 如果没有保存偏移量,从最新的消息开始消费
auto_offset_reset => "latest"
# 可选:增加一些安全性或性能配置
# security_protocol => "SASL_PLAINTEXT"
# sasl_mechanism => "PLAIN"
# jaas_path => "/path/to/jaas.conf"
}
}
# 这里可以添加filter段对数据进行处理
filter {
# 示例1:将日志中的时间戳字段(@timestamp)解析为Logstash的@timestamp
# 因为JSONLayout默认的timestamp字段可能在`instant`里,我们需要提取它
date {
match => [ "[instant][epochSecond]", "UNIX" ]
target => "@timestamp"
}
# 示例2:移除一些不需要的字段,让ES记录更简洁
mutate {
remove_field => [ "instant", "endOfBatch", "loggerFqcn", "threadPriority" ]
}
# 示例3:增加一个标识来源的字段
mutate {
add_field => { "data_source" => "flink_kafka_appender" }
}
}
output {
# 输出到Elasticsearch
elasticsearch {
# Elasticsearch集群节点
hosts => ["es-node1:9200", "es-node2:9200"]
# 索引名。按天创建索引,便于管理(滚动、删除过期数据)
index => "flink-logs-%{+YYYY.MM.dd}"
# 如果ES开启了安全认证
# user => "elastic"
# password => "your_password"
# 设置文档ID,可选。使用日志的唯一标识可以避免重复(如果at-least-once的话)
# document_id => "%{thread}-%{@timestamp}"
}
# 可选:同时输出到标准输出,用于调试
stdout {
codec => rubydebug
}
}
配置详解:
input.kafka.codec => json:这是关键!因为Flink的KafkaAppender输出的是JSON字符串,这个配置告诉Logstash直接将其解析为结构化的Event,这样在filter和output中就可以直接使用level、message这些字段了。filter.date:Logstash的@timestamp是内置的时间字段,很多功能(如Kibana的时间筛选)依赖它。我们需要从原始日志的instant.epochSecond(Unix时间戳)解析出正确的时间。output.elasticsearch.index:使用带日期的索引模式是最佳实践。这样每天都会生成一个新索引(如flink-logs-2023.11.05),非常方便:- 按时间范围查询更快:直接查询某个日期的索引即可。
- 易于生命周期管理:可以方便地设置策略,自动删除或归档N天前的旧索引(通过Elasticsearch的ILM功能)。
- 避免单个索引过大。
3.2 启动Logstash
保存好配置文件后,就可以启动Logstash了。建议以后台方式运行,并记录日志:
cd /path/to/logstash-6.5.4
nohup ./bin/logstash -f config/logstash-flink.conf --config.reload.automatic > logs/logstash.out 2>&1 &
参数--config.reload.automatic允许你在修改配置文件后,Logstash自动重新加载,无需重启。
检查Logstash运行状态:
- 查看进程:
ps aux | grep logstash - 查看日志:
tail -f logs/logstash.out,观察是否有连接Kafka和ES成功的日志,以及是否有错误信息。
4. 第三步:在Elasticsearch中存储与索引日志
Elasticsearch是整个系统的“大脑”,负责存储和索引日志数据。它的配置和优化是一个很深的话题,这里我们聚焦在日志场景下的基本设置。
4.1 基本部署与配置
- 下载与解压:从官网下载对应版本的Elasticsearch。
- 配置
config/elasticsearch.yml:# 集群名称,节点间通过此名称发现彼此 cluster.name: flink-log-cluster # 节点名称 node.name: node-1 # 数据存储路径 path.data: /path/to/elasticsearch/data # 日志存储路径 path.logs: /path/to/elasticsearch/logs # 绑定地址,设为0.0.0.0允许其他机器访问 network.host: 0.0.0.0 # HTTP端口 http.port: 9200 # 集群初始主节点列表 discovery.seed_hosts: ["host1", "host2"] cluster.initial_master_nodes: ["node-1"] - 系统参数调整:Elasticsearch对系统资源有要求,通常需要调整:
- 最大文件描述符:
ulimit -n 65535 - 虚拟内存映射:
sysctl -w vm.max_map_count=262144(永久修改需写/etc/sysctl.conf)
- 最大文件描述符:
- 以非root用户启动:出于安全考虑,务必创建一个专用用户来运行ES。
useradd elasticsearch chown -R elasticsearch:elasticsearch /path/to/elasticsearch su elasticsearch ./bin/elasticsearch -d
4.2 针对日志场景的索引模板
为了让每天自动创建的索引(flink-logs-*)拥有合适的设置(如分片数、字段类型、分词器),我们需要创建一个索引模板。
通过Kibana的Dev Tools或直接调用ES API来创建:
PUT _template/flink-logs-template
{
"index_patterns": ["flink-logs-*"],
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index": {
"codec": "best_compression"
}
},
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"level": {
"type": "keyword" // 使用keyword类型,便于精确聚合和筛选
},
"message": {
"type": "text", // 使用text类型,支持全文搜索
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256 // 同时提供一个keyword子字段用于精确匹配
}
}
},
"thread": {
"type": "keyword"
},
"loggerName": {
"type": "keyword"
},
"flink_job_name": {
"type": "keyword"
},
"yarn_container_id": {
"type": "keyword"
},
"log_path": {
"type": "keyword"
}
}
}
}
模板关键点:
index_patterns:匹配所有以flink-logs-开头的索引,模板自动生效。number_of_shards:主分片数。根据你的日志量预估和集群规模设置。对于日志,通常不需要太多分片,3-5个起步即可。分片数一旦设定,索引创建后无法修改。number_of_replicas:副本数,提供高可用和读性能。生产环境至少设为1。refresh_interval:索引刷新间隔。默认1秒,对于日志这种写入量大、实时性要求稍低的场景,可以适当调大(如30秒)以提升写入性能。- 字段类型:
keyword:用于精确匹配、聚合、排序的字段,如level,flink_job_name。不会分词。text:用于全文搜索的字段,如message。会被分词。我们同时为message定义了一个keyword子字段,以便在某些场景下进行精确匹配(如匹配完整错误信息)。
创建模板后,当天首次有日志写入,ES会自动创建flink-logs-2023.11.05索引,并应用此模板的设置。
5. 第四步:在Kibana中可视化与分析日志
Kibana是我们的“驾驶舱”,所有日志的查询、分析、监控都在这里进行。
5.1 配置索引模式
启动Kibana(./bin/kibana)并访问其Web界面(默认http://localhost:5601)。
第一步是告诉Kibana去哪里找数据,即创建“索引模式”(Index Pattern)。
- 进入 Management -> Stack Management -> Kibana -> Index Patterns。
- 点击 Create index pattern。
- 在索引模式名称中输入
flink-logs-*。Kibana会自动检测匹配的索引。 - 在“时间字段”下拉框中,选择
@timestamp。这是最关键的一步,这样Kibana才能基于时间进行筛选和展示。 - 点击 Create index pattern。
5.2 探索与查询日志
创建好索引模式后,就可以在 Discover 页面查看日志了。
- 时间范围选择器:在右上角,你可以选择查看“最近15分钟”、“最近1小时”、“今天”等,或者自定义绝对时间范围。
- 字段筛选:左侧列出了索引中的所有字段(
level,message,flink_job_name等)。点击任何一个字段,可以看到其值分布(如INFO,WARN,ERROR各有多少条)。你可以点击字段值旁边的“加号”或“减号”来快速添加正向或反向筛选条件。 - 查询栏:在顶部输入框,可以使用Kibana Query Language (KQL) 进行查询,语法非常直观:
level: ERROR查找所有错误日志。flink_job_name: "MyFlinkJob" and level: WARN查找MyFlinkJob作业的所有警告日志。message: "NullPointerException"在message字段中全文搜索包含该异常的日志。yarn_container_id: "container_123456"精确查找某个容器的所有日志。
- 查看日志详情:点击任意一条日志记录左边的箭头,可以展开查看该条日志完整的JSON结构,所有字段一目了然。
5.3 构建监控仪表盘
在 Visualize 和 Dashboard 页面,你可以创建丰富的图表来监控日志。
场景示例:监控各Flink作业的错误率
- 进入 Visualize -> Create new visualization。
- 选择 Lens(推荐,更易用)或 Vertical bar chart。
- 选择数据源为
flink-logs-*。 - 配置图表:
- X轴:选择
Terms,字段选flink_job_name.keyword。 - Y轴:选择
Count。 - 拆分切片:选择
Terms,字段选level.keyword。 - 添加筛选器:在顶部的筛选器栏添加
level: ERROR。
- X轴:选择
- 这样你就得到了一个柱状图,显示了每个Flink作业产生的
ERROR日志数量。你可以保存这个可视化图表。 - 进入 Dashboard -> Create new dashboard,将刚才保存的图表添加进去。你还可以添加其他图表,比如“随时间变化的日志总量(面积图)”、“日志级别分布(饼图)”、“包含特定关键词的日志趋势(折线图)”等。
5.4 设置告警
Kibana集成了Alerting功能(在较新版本中),可以基于日志条件触发告警。
例如,你可以创建一个规则:“如果在过去5分钟内,flink_job_name为CriticalPaymentJob的日志中,出现ERROR级别的次数超过10次,则发送邮件通知”。
- 进入 Management -> Stack Management -> Kibana -> Alerting。
- 点击 Create rule。
- 选择规则类型,对于日志计数,常用的是“Threshold”。
- 定义条件:索引
flink-logs-*,查询语句flink_job_name: "CriticalPaymentJob" AND level: ERROR,时间窗口5m,阈值> 10。 - 配置动作(Action):比如连接到邮件服务器、Webhook(触发钉钉/企业微信机器人)、或Slack等。
这样,你就建立了一个从日志采集、传输、存储到可视化、监控、告警的完整闭环。
6. 进阶技巧与避坑指南
在实际使用中,你可能会遇到一些挑战。这里分享几个我总结的经验和技巧。
6.1 性能优化与稳定性
- Kafka分区与消费者:如果Flink集群规模大、日志量巨大,确保Kafka的
flink_logs主题有足够的分区数(比如和Flink的TaskManager数量成一定比例)。同时,调整Logstash的consumer_threads参数(不超过分区数),让多个线程并行消费,提高吞吐。 - Elasticsearch批量写入:Logstash的
elasticsearch输出插件默认是批量写入的。你可以调整flush_size(每批文档数,默认500)和idle_flush_time(空闲刷新时间,默认1秒)来优化写入性能,在吞吐量和延迟之间取得平衡。 - Elasticsearch索引生命周期管理(ILM):日志数据会不断增长。使用ILM策略自动管理索引的生命周期,例如:热阶段(7天,在SSD上)、温阶段(30天,在HDD上)、冷阶段(归档)、删除阶段(超过90天自动删除)。这能显著降低存储成本。
- 监控ELK栈自身:用ELK监控ELK自己。为Logstash、Elasticsearch、Kibana也配置日志采集(发到另一个Kafka Topic),这样当管道出现问题时,你也有迹可循。
6.2 日志解析与字段增强
原始的JSON日志可能信息不够。Logstash的filter部分非常强大,可以用来解析和丰富日志。
- 解析复杂Message:如果
message字段是结构化的字符串(比如用特定分隔符),可以用grok或dissect过滤器将其拆分成多个字段。filter { grok { match => { "message" => "\[%{TIMESTAMP_ISO8601:log_timestamp}\] %{LOGLEVEL:log_level} %{GREEDYDATA:log_content}" } } } - 地理信息查询:如果日志中包含IP地址,可以用
geoip过滤器查询其地理位置信息。 - 字典查询:用
translate过滤器,根据日志中的错误码,映射成更易懂的错误描述。
6.3 处理多行日志(堆栈跟踪)
一个Java异常堆栈跟踪会被拆分成多行。如果直接发送,在Kibana中会显示为多条独立的日志,很难阅读。这需要在源头(Flink的Log4j配置)或采集端(Logstash)处理。
更推荐在Log4j端处理:使用Log4j的PatternLayout配合encodeThreadContextAsList等配置,或者使用专门的编码器(如LogstashLayout),确保一个异常事件被输出为单个JSON对象,堆栈信息放在一个字段内(如stack_trace)。
如果必须在Logstash处理,可以使用multiline codec,但配置较为复杂,且容易出错。
6.4 安全与权限
- Kafka/Elasticsearch认证:生产环境务必开启安全认证。在Logstash配置中配置SASL/SSL连接Kafka,在Elasticsearch output中配置用户名密码。
- Kibana访问控制:使用Kibana Spaces和Role-Based Access Control (RBAC) 来限制不同团队或用户只能访问其负责的Flink作业的日志索引(例如,通过索引模式
flink-logs-*-teamA)。
这套Flink+ELK的日志实时采集与分析方案,从我们团队上线以来,已经成为了运维和开发排查问题的利器。它把原本繁琐、被动的日志查找,变成了主动、高效的监控和分析。当你第一次在Kibana里,用几个点击就定位到凌晨3点那个诡异错误的完整上下文时,你会觉得这一切的配置都是值得的。
1089



被折叠的 条评论
为什么被折叠?



