概述
一般我们需要进行日志分析场景:直接在日志文件中 grep、awk 就可以获得自己想要的信息。但在规模较大的场景中,此方法效率低下,面临问题包括日志量太大如何归档、文本搜索太慢怎么办、如何多维度查询。需要集中化的日志管理,所有服务器上的日志收集汇总。常见解决思路是建立集中式日志收集系统,将所有节点上的日志统一收集,管理,访问。
ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是开源软件。新增了一个FileBeat,它是一个轻量级的日志收集处理工具(Agent),Filebeat占用资源少,适合于在各个服务器上搜集日志后传输给Logstash,官方也推荐此工具。
Elasticsearch是个开源分布式搜索引擎,提供搜集、分析、存储数据三大功能。它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。
Logstash 主要是用来日志的搜集、分析、过滤日志的工具,支持大量的数据获取方式。一般工作方式为c/s架构,client端安装在需要收集日志的主机上,server端负责将收到的各节点日志进行过滤、修改等操作在一并发往elasticsearch上去。
Kibana 也是一个开源和免费的工具,Kibana可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助汇总、分析和搜索重要数据日志。
日志收集流程
-
从文件收集
此种方式主要是使用Filebeat对服务运行过程中产生的log文件进行拆解分析,然后发往Kakfa。具体路线为:
优点:
- 和服务完全解耦,不会影响服务的吞吐量
缺点:
- 实时性略差
- 增加了额外的组件(Filebeat)
-
服务直接发往kafka
此种方式主要由服务直接将log发往Kafka,具体路线为:
优点:
- 实时性强
- 使用的组件略少于上述方式
缺点:
- 可能会影响服务的吞吐量
这次我们采用的是第一种方式实现日志采集
如何将服务日志发往Kafka
对于上一章节描述的第一种日志采集方式,我们如何做到将日志直接发往Kafka呢?
logback-kafka-appender
首先肯定不能自己每次都使用Kafka的API手动发给Kafka,这样实在太麻烦。毕竟有时候服务并不是你从头写的,此外这样耦合性太高。所以这里这里我们采用logback-kafka-appender
,它扩展了Logback可以直接将日志发往kakfa而不需要额外的编码,只需要再XML文件种配置即可。
<dependency>
<groupId>com.github.danielwegener</groupId>
<artifactId>logback-kafka-appender</artifactId>
<version>0.2.0-RC2</version>
</dependency>
配置如下:
<appender name="kafkaAppender" class="com.github.danielwegener.logback.kafka.KafkaAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -[%jobName]- %msg %n</pattern>
</encoder>
// 发往的topic
<topic>logs</topic>
<!-- we don't care how the log messages will be partitioned -->
<keyingStrategy class="com.github.danielwegener.logback.kafka.keying.NoKeyKeyingStrategy" />
<!-- use async delivery. the application threads are not blocked by logging -->
<deliveryStrategy class="com.github.danielwegener.logback.kafka.delivery.AsynchronousDeliveryStrategy" />
<!-- each <producerConfig> translates to regular kafka-client config (format: key=value) -->
<!-- producer configs are documented here: https://kafka.apache.org/documentation.html#newproducerconfigs -->
<!-- bootstrap.servers is the only mandatory producerConfig -->
//kafka的服务地址
<producerConfig>bootstrap.servers=192.168.1.203:6667</producerConfig>
<producerConfig>buffer.memory=8388608</producerConfig>
<!-- If the kafka broker is not online when we try to log, just block until it becomes available -->
<producerConfig>metadata.fetch.timeout.ms=5000</producerConfig>
<!-- use gzip to compress each batch of log messages. valid values: none, gzip, snappy -->
<producerConfig>compression.type=gzip</producerConfig>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- if neverBlock is set to true, the async appender discards messages when its internal queue is full -->
<neverBlock>true</neverBlock>
<appender-ref ref="kafkaAppender" />
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
<appender-ref ref="ASYNC" />
</root>
如上配置都是基于该项目Github地址改造而来的。
注意!这种配置不能完全保证日志不丢失,如果想要不丢失,在github的readme里面有相关配置。这里为了尽量保证程序的吞吐量做了取舍。
配置解释
你需要做一个重要的决定:是将所有日志发送到远程Kafka更重要,还是让应用程序平稳运行更重要?这两种决策都允许您调优该appender以提高吞吐量。
发送策略 | 详细 |
---|---|
AsynchronousDeliveryStrategy | 将每条日志消息发到Kafka Producer。如果由于某些原因传递失败,则将消息发送到fallback appenders。但是,如果生产者发送的缓冲区已满,这个DeliveryStrategy会阻塞(如果与代理的连接丢失,就会发生这种情况)。为了避免这种阻塞,可以启用producerConfig block.on.buffer.full=false。所有不能以足够快的速度传递的日志消息将立即转到fallback appenders。 |
BlockingDeliveryStrategy | 阻塞每个调用线程,直到实际交付日志消息。通常不鼓励使用这种策略,因为它对吞吐量有巨大的负面影响。警告:此策略不应与producerConfig linger.ms一起使用 |
你可以使用partition属性为kafka appender提供一个固定的分区,或者让生产者使用message key来分区消息。因此logback-kafka-appender支持以下关键策略:
key生成策略 | 详细 |
---|---|
NoKeyKeyingStrategy (default) | Does not generate a message key. Results in round robin distribution across partition if no fixed partition is provided. |
HostNameKeyingStrategy | This strategy uses the HOSTNAME as message key. This is useful because it ensures that all log messages issued by this host will remain in the correct order for any consumer. But this strategy can lead to uneven log distribution for a small number of hosts (compared to the number of partitions). |
ContextNameKeyingStrategy | This strategy uses logback's CONTEXT_NAME as message key. This is ensures that all log messages logged by the same logging context will remain in the correct order for any consumer. But this strategy can lead to uneven log distribution for a small number of hosts (compared to the number of partitions). This strategy only works for ILoggingEvents . |
ThreadNameKeyingStrategy | This strategy uses the calling threads name as message key. This ensures that all messages logged by the same thread will remain in the correct order for any consumer. But this strategy can lead to uneven log distribution for a small number of thread(-names) (compared to the number of partitions). This strategy only works for ILoggingEvents . |
LoggerNameKeyingStrategy | * This strategy uses the logger name as message key. This ensures that all messages logged by the same logger will remain in the correct order for any consumer. But this strategy can lead to uneven log distribution for a small number of distinct loggers (compared to the number of partitions). This strategy only works for ILoggingEvents . |
如果你想确保你的appender绝对不会阻塞你的服务,你可以使用logback的AsyncAppender再次包裹你的KafkaAppender。配置如下:
<configuration>
<!-- This is the kafkaAppender -->
<appender name="kafkaAppender" class="com.github.danielwegener.logback.kafka.KafkaAppender">
<!-- Kafka Appender configuration -->
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- if neverBlock is set to true, the async appender discards messages when its internal queue is full -->
<neverBlock>true</neverBlock>
<appender-ref ref="kafkaAppender" />
</appender>
<root level="info">
<appender-ref ref="ASYNC" />
</root>
</configuration>
配置如上xml之后,启动程序就可以发现所有的info日志都被发往kafka名为logs的topic里面了。
总结
这种方式对代码的侵入性很小,且定制化程度也不低。因为本身logback就提供了很多扩展方案,其次logback-kafka-appender源码相对比较简单,修改起来了还是比较容易的。
Logstash配置
配置如下:
input {
kafka{
bootstrap_servers => "192.168.1.203:6667"
topics=>["logs"]
client_id => "addax"
type=>"addax-log"
}
kafka{
bootstrap_servers => "192.168.1.203:6667"
topics=>["dolphin-log"]
client_id => "dolphin"
type=>"dolphin-log"
}
}
filter {
if[type]=="addax-log"{
mutate{
split=>["message","#"]
add_field=>{
"timestamp"=>"%{[message][0]}"
"thread" => "%{[message][1]}"
"logLevel" => "%{[message][2]}"
"class" => "%{[message][3]}"
"jobName" => "%{[message][4]}"
"msg" => "%{[message][5]}"
"n" => "%{[message][6]}"
}
}
}
if[type]=="dolphin-log"{
mutate{
split=>["message",";"]
add_field=>{
"jobName" => "%{[message][0]}"
"msg" => "%{[message][1]}"
"timestamp"=>"%{[message][2]}"
}
}
}
}
output {
elasticsearch {
index=>"addax-log"
hosts =>["192.168.1.204:9200"]
}
}
Logstash拉去Kafka日志内容
kafka{
bootstrap_servers => "192.168.1.203:6667"
topics=>["logs"]
client_id => "addax"
type=>"addax-log"
}
参数 | 解释 |
---|---|
bootstrap_servers | kafka地址 |
topics | kafka topics |
client_id | 配置多个kafka数据源时需要配置,且必须不同 |
type | 类型,方便后去过滤 |
Logstash过滤日志内容
if[type]=="addax-log"{
mutate{
split=>["message","#"]
add_field=>{
"timestamp"=>"%{[message][0]}"
"thread" => "%{[message][1]}"
"logLevel" => "%{[message][2]}"
"class" => "%{[message][3]}"
"jobName" => "%{[message][4]}"
"msg" => "%{[message][5]}"
"n" => "%{[message][6]}"
}
}
}
if[type]=="dolphin-log"{
mutate{
split=>["message",";"]
add_field=>{
"jobName" => "%{[message][0]}"
"msg" => "%{[message][1]}"
"timestamp"=>"%{[message][2]}"
}
}
}
if[type]=="addax-log"
根据不同类型做不同过滤处理,我们先前已经通过特殊符号分割好了日志内容。比如addax-log日志就是动过#号分割。
split=>["message","#"]
分割日志
add_field=>{
"timestamp"=>"%{[message][0]}"
"thread" => "%{[message][1]}"
"logLevel" => "%{[message][2]}"
"class" => "%{[message][3]}"
"jobName" => "%{[message][4]}"
"msg" => "%{[message][5]}"
"n" => "%{[message][6]}"
}
}
"timestamp"=>"%{[message][0]}"
其实就是简单的取数组值然后赋值到timestamp,最后在elastic search
中就会有这个字段。
Logstash存储日志内容
output {
elasticsearch {
index=>"addax-log"
hosts =>["192.168.1.204:9200"]
}
}
看配置应该就能明白,如上配置存储到es中。
参数 | 解释 |
---|---|
index | es索引 |
hosts | es部署地址和端口号 |
Kibana
Kibana是一个开源的分析与可视化平台,设计出来用于和Elasticsearch一起使用的。相对来说只要当个可视化工具既可因此没啥好说的。只说明如何查看数据:
输入http://192.168.1.204:5601/登录kibana
点击1处,然后点击2处进入索引模式页面。
索引模式页面左上角,点击创建索引模式
然后输入我们刚刚在logstash配置文件中output中的index参数。
点击下一步,然后点击创建索引模式即可。
Elastic Search
本文档主题不是如何使用ES,因此就不说ES具体使用方法。只需要知道通过上面的步骤我们已经可以将格式化好的日志内容存储近ES中,并且可以实现各种查询(聚合,分组,排序等等)。
ELK系统docker compose配置文件
version: '3'
services:
# search engine
elasticsearch:
image: elasticsearch:7.14.1
# image: elasticsearch:7.9.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- http.port=9200
- http.cors.enabled=true
# - http.cors.allow-origin=http://192.168.93.139:1358
- http.cors.allow-origin=*
# - http.cors.allow-origin=http://localhost:1358,http://127.0.0.1:1358
- http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization
- http.cors.allow-credentials=false
- bootstrap.memory_lock=true
- 'ES_JAVA_OPTS=-Xms512m -Xmx512m'
volumes:
- /data/elk/data/es/data:/usr/share/elasticsearch/data:rw
- /data/elk/data/es/logs:/usr/share/elasticsearch/logs:rw
ports:
- '9200:9200'
- '9300:9300'
# elasticsearch browser
kibana:
image: kibana:7.14.1
container_name: kibana
volumes:
- /data/elk/conf/kibana.yml:/usr/share/kibana/config/kibana.yml
ports:
- '5601:5601'
logstash:
image: logstash:7.14.1
container_name: logstash
volumes:
- /data/elk/conf/logstash.yml:/usr/share/logstash/config/logstash.yml
- /data/elk/conf/pipeline:/usr/share/logstash/pipeline
- /data/elk/data/logstash/logs:/var/log/logstash
ports:
- 5044:5044
environment:
LS_JAVA_OPTS: "-Xmx256m -Xms256m"
depends_on:
- elasticsearch