上周末在家闲来无事,于是乎动手帮项目组搭建日志收集的EFK环境,最终目标的部署是这个样子的:
在每个应用机器上部一个Fluentd做为代理端,以tail方式读取指定的应用日志文件,然后Forward到做为汇聚端的Fluentd,汇聚端对日志内容加工、分解成结构化内容,再存储到ElasticSearch。日志内容的展现由Kinana实现。
Fluentd是什么? Fluentd是一个完全免费的、完全开源的日志收集器,可以与125多种系统相对接,实现“日志一切”架构。
由于前面已经在Kubernetes容器平台上部署好了ElasticSearch与Kibana,周末只是忙乎Fluentd的安装配置,以前没有使用过,现学再卖,结果折腾过程中出现了几个问题,在这里念叨记录一下。
在我们的部署中,只使用了Fluentd两种Output插件,一个是代理端用于向后传递日志内容的Ouput-Forward,一个是用于向ES中存储数据的Output-ElasticSearch。
@type forward
require_ack_response ture
host 10.xxx.xx.xx
port 24224
<buffer tag>
@type file
path /xxx/xxx/buffer
flush_interval 10s
total_limit_size 1G
buffer>
<match xxxxx.xxxxx.**>
@type elasticsearch
host 10.xxx.xx.xxx
port 30172
logstash_format true
logstash_prefix log.xxxxx.xxxxx.xxxxx-
@type file
path "/xxx/xxx/fluentd/aggregator/xxxx/buffer"
total_limit_size 20G
flush_interval 10s
match>
配置完以后,简单测试跑通后,满心欢喜地拿应用某台机器上1-3月产生的日志灌数。从Kibana界面上代表日志数量的绿柱向上增长,几千…几万..百万…两百万,当时还发图片给项目组的显摆了一番。后面想着不可能有这么多日志记录吧!? 登录到应用服务器上去日志目录里wc了一把,所有文件行数总和才150万! 晕死!
跑进Kibana界面细观瞧,才发现入库的日志记录有重复内容,比如:某条记录就重复了233次。 当时也不知道具体原因,猜测着可能是Output-ElasticSearch插件因接收响应超时,叕重发了内容到ElasticSearch,所以试着查到相应参数:request_timeout
加入到汇聚端配置中,参数默认值是:5s
@type elasticsearch
...
request_timeout 30s
...
发信号HUP
给汇聚端Fluentd进程,让它重新载入修改后的配置,再试,没再出现重复日志记录。但,超时时间只是表象,真正的原因是buffer配置操成,请见下一条内容。
在说明这个问题前,先看看Fluentd的buffer机制,buffer做为Output插件的缓冲区,当输出端(比如:ElasticSearch)服务不可用时,Fluentd暂时缓存需要输出的内容到文件或者内存,然后再重试向输出端发送。
What? 那输出端服务总是不恢复,光进不出,企不是要将Fluend缓冲区撑爆后丢失日志数据?
当缓冲区Full后,默认产生BufferOverflowError
异常,输入插件自行如何处理此异常。我们使用的in_tail插件会停止读取日志文件内容,in_forward插件会向上一级ouput_forward插件返回错误。除产生异常外,还可通过Output插件的配置参数overflow_action
(>=1.0)或者buffer_queue_full_action
(<1.0)控制缓冲区满后的行为:
具体说明请见:官网Output Plugin Overview (>=1.0) 或者Buffer Plugin Overview
为什么扯到buffer?因为遇到的这个问题与它有关。在修改完request_timeout
参数后,又经常看到错误信息:
2018-03-04 15:10:12 +0800 [warn]: #0 fluent/log.rb:336:warn: Could not push logs to Elasticsearch, resetting connection and trying again. Connection reset by peer (Errno::ECONNRESET)
显示被ElasticSearch端强制断掉连接,什么情况? 在命令行启动fluentd时强制trace日志级别,满屏的输出中找不见错误原因,除了ECCONNRESET。 ElasticSearch端日志也未见异常信息。 只好求助于另一杀器:tcpdump,抓取与ES之间的通讯包:
sudo tcpdump -A -nn -s 0 'tcp port 9200 ' -i eth1 #9200是ES接受请求的端口
得到,请求包头:
User-Agent: Faraday v0.13.1
Content-Type: application/json
Host: 10.210.39.136:30172
Content-Length: 129371575
响应包头:
HTTP/1.1 413 Request Entity Too Large
content-length: 0
Request Entity Too Large! 查了下ElasticSearch文档,ES默认能接受的数据包大小是100MB,由`http.max_content_length
参数控制。而我们上送的数据包长度明显大于这一限制Content-Length: 129371575
。
是什么造成了这种结果? 从上面关于Fluentd Buffer的示意图中,可以看出Output插件是以Chunk为单位向输出端发送数据包,Chunk的大小由参数chunk_limit_size
设置(说明文档here),默认值:内存缓冲8MB/文件缓冲256MB。 在我们初始配置中并没有设定,采用的是默认值,所以出现Request Entity Too Large! 错误也就不稀奇。 分别在代理端与汇聚端的buffer相关配置里增加chunk_limit_size参数设置:
...
chunk_limit_size 10M
...
...
chunk_limit_size 15M
...
想想前面一节说明到与ES间请求超时,与采用默认chunk大小设置是有关的,文件型缓冲区默认256M的chunk大小,送给ES端就是大小不超过其限制的100MB,也会让ES处理较长的时间,所以会有请求超时出现。
在配置过程中,看错了老版本的文档,使用参数buffer_chunk_limit
设置,没起作用,迷惑一小会儿!
另外,是否可以将汇聚端的chunk大小设置得比代理端小? 是可以的,但汇聚端会有警告日志产生。大致看了下代码,说说我自己的理解(不见得对哟):Flunetd后一级收到前一级发送来的chunk数据时,先不做大小上的处理,在向再后一级发送时,根据后一级类型进行chunk的编码,完成后再与设置的chunk大小做比对,如果大于设置再重新分拆成多个后再进行编码发送,期间会产生警告日志 。
为防止再出现日志记录重复的现象,除上述配置上的处理外,最好在发给ElasticSearch前,给每条日志生成一个主键值,这样ES在收到重复记录后,如果发现主键对应的记录已存在则Update,否则才Insert。
@type elasticsearch_genid
hash_id_key _hash # storing generated hash id key (default is _hash)
@type elasticsearch
...
id_key _hash # specify same key name which is specified in hash_id_key
remove_keys _hash # Elasticsearch doesn't like keys that start with _
...
因为上送ElasticSearc的日志记录中,标明日志时间的域值是拼接出来的再转换成日期类型的:
${ DateTime.parse(record["systemDate"] + " " + record["systemTime"] + " +0800").to_time.to_i }
注意,一定要在最后使用to_i
方法,将其转换成Unix Time格式。否则,会在编码送往ES数据时报错,大到错误信息是“未发现Time类型上的msgPack方法”。
另:Output-ElasticSearch插件的说明文档,官网内容不全面,完全说明在这里。
附上全部配置文件内容:
<system>
worker 2
root_dir /xxxx/xxxxx/fluentd/agent
log_level info
system>
<source>
@type tail
tag xxxxx.xxxxx.agent.log
path_key path
path /xxxx/xxxxx/agent/logs/xxxxx/*.batch*.log
pos_file /xxxx/xxxxx/fluentd/agent/pos/batch_agent.db
read_from_head true
<parse>
@type multiline
format_firstline /^(\d+-\d+-\d+\s+)?\d+:\d+:\d+\|/
format1 /^(?<log>(\d+-\d+-\d+\s+)?\d+:\d+:\d+\|.*)$/
parse>
#multiline_flush_interval 5s
source>
<filter foo.bar>
@type record_transformer
<record>
hostname "#{Socket.gethostname}"
record>
filter>
<match **>
@type forward
require_ack_response ture
<server>
host xx.xxx.xx.xx
port 24224
server>
<buffer tag>
@type file
path /xxxx/xxxxx/fluentd/agent/buffer
flush_interval 10s
total_limit_size 1G
chunk_limit_size 10M
buffer>
match>
@type parser
reserve_data true
key_name log
@type regexp
expression /(?mx)^(?\d+-\d+-\d+)?\s?(?\d+:\d+:\d+)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)\|(?.*)$/
@type record_transformer
enable_ruby true
systemDate ${ if !(o=record["systemDate"]).nil? and o.length!=0 then o else if (d=record["path"].scan(/\d{4}-\d{2}-\d{2}/).last).nil? then Time.new.strftime("%Y-%m-%d") else d end end }
@type record_transformer
enable_ruby true
logtime ${ DateTime.parse(record["systemDate"] + " " + record["systemTime"] + " +0800").to_time.to_i }
renew_time_key logtime
remove_keys logtime,systemDate,systemTime
@type elasticsearch_genid
hash_id_key _hash # storing generated hash id key (default is _hash)
<match aplus.batch.**>
@type elasticsearch
host xx.xxx.xx.xxxx
port 9200
id_key _hash # specify same key name which is specified in hash_id_key
remove_keys _hash # Elasticsearch doesn't like keys that start with _
logstash_format true
logstash_prefix log.xxxx.xxxxx.agent-
request_timeout 30s
# reload_connections false
slow_flush_log_threshold 30s
@type file
path "/xxxx/xxxxx/fluentd/aggregator/#{ENV['HOSTNAME']}/buffer"
total_limit_size 20G
chunk_limit_size 15M
flush_interval 10s
retry_wait 10.0
match>