如今,无法想象基于 Kubernetes 的项目没有 ELK 堆栈,它保存集群的应用程序和系统组件的日志。 在我们的实践中,我们将 EFK 堆栈与 Fluentd 一起使用,而不是 Logstash。
Fluentd 是一个现代的通用日志收集器,越来越受欢迎,并已加入云原生计算基金会,这就是为什么它的开发向量专注于与 Kubernetes 结合使用。
使用 Fluentd 代替 Logstash 的事实并没有改变该软件包的一般本质,但是,Fluentd 的特点是其多功能性所带来的特定细微差别。
例如,当我们开始在一个日志记录强度较高的繁忙项目中使用 EFK 时,我们面临着这样的事实:在 Kibana 中,某些消息会重复显示多次。 在这篇文章中我们将告诉您为什么会出现这种现象以及如何解决问题。
文档重复的问题
在我们的项目中,Fluentd 部署为 DaemonSet(在 Kubernetes 集群的每个节点上的一个实例中自动启动)并监视 /var/log/containers 中的 stdout 容器日志。 收集和处理后,日志以JSON文档的形式发送到ElasticSearch,根据项目的规模以及性能和容错的要求,以集群或单机的形式提出。 Kibana用作图形界面。
当使用带有输出缓冲插件的 Fluentd 时,我们遇到了一种情况,ElasticSearch 中的某些文档内容完全相同,仅标识符不同。 您可以使用 Nginx 日志作为示例来验证这是否是消息重复。 在日志文件中,此消息存在于单个副本中:
127.0.0.1 192.168.0.1 - [28/Feb/2013:12:00:00 +0900] "GET / HTTP/1.1" 200 777 "-" "Opera/12.0" -
但是,ElasticSearch 中有多个文档包含此消息:
{
"_index": "test-custom-prod-example-2020.01.02",
"_type": "_doc",
"_id": "HgGl_nIBR8C-2_33RlQV",
"_version": 1,
"_score": 0,
"_source": {
"service": "test-custom-prod-example",
"container_name": "nginx",
"namespace": "test-prod",
"@timestamp": "2020-01-14T05:29:47.599052886 00:00",
"log": "127.0.0.1 192.168.0.1 - [28/Feb/2013:12:00:00 0900] "GET / HTTP/1.1" 200 777 "-" "Opera/12.0" -",
"tag": "custom-log"
}
}
{
"_index": "test-custom-prod-example-2020.01.02",
"_type": "_doc",
"_id": "IgGm_nIBR8C-2_33e2ST",
"_version": 1,
"_score": 0,
"_source": {
"service": "test-custom-prod-example",
"container_name": "nginx",
"namespace": "test-prod",
"@timestamp": "2020-01-14T05:29:47.599052886 00:00",
"log": "127.0.0.1 192.168.0.1 - [28/Feb/2013:12:00:00 0900] "GET / HTTP/1.1" 200 777 "-" "Opera/12.0" -",
"tag": "custom-log"
}
}
而且,可以有两次以上的重复。
在Fluentd日志中修复此问题时,您可以看到大量警告,内容如下:
2020-01-16 01:46:46 +0000 [warn]: [test-prod] failed to flush the buffer. retry_time=4 next_retry_seconds=2020-01-16 01:46:53 +0000 chunk="59c37fc3fb320608692c352802b973ce" error_class=Fluent::Plugin::ElasticsearchOutput::RecoverableRequestFailure error="could not push logs to Elasticsearch cluster ({:host=>"elasticsearch", :port=>9200, :scheme=>"http", :user=>"elastic", :password=>"obfuscated"}): read timeout reached"
当 ElasticSearch 无法在 request_timeout 参数指定的时间内返回对请求的响应时,就会出现这些警告,这就是无法清除转发的缓冲区片段的原因。 之后,Fluentd 再次尝试将缓冲区片段发送到 ElasticSearch,并在任意次数的尝试后,操作成功完成:
2020-01-16 01:47:05 +0000 [warn]: [test-prod] retry succeeded. chunk_id="59c37fc3fb320608692c352802b973ce"
2020-01-16 01:47:05 +0000 [warn]: [test-prod] retry succeeded. chunk_id="59c37fad241ab300518b936e27200747"
2020-01-16 01:47:05 +0000 [warn]: [test-dev] retry succeeded. chunk_id="59c37fc11f7ab707ca5de72a88321cc2"
2020-01-16 01:47:05 +0000 [warn]: [test-dev] retry succeeded. chunk_id="59c37fb5adb70c06e649d8c108318c9b"
2020-01-16 01:47:15 +0000 [warn]: [kube-system] retry succeeded. chunk_id="59c37f63a9046e6dff7e9987729be66f"
然而,ElasticSearch 将每个传输的缓冲区片段视为唯一,并在索引期间为它们分配唯一的 _id 字段值。 这就是邮件副本的显示方式。
在 Kibana 中它看起来像这样:
该解决方案
有多种选择可以解决这个问题。 其中之一是 Fluent-plugin-elasticsearch 插件中内置的机制,用于为每个文档生成唯一的哈希值。 如果使用此机制,ElasticSearch 将在转发阶段识别重复并防止重复文档。 但我们必须考虑到这种解决问题的方法与调查很困难,并且不能消除超时不足的错误,因此我们放弃了它的使用。
我们在 Fluentd 输出上使用缓冲插件,以防止在出现短期网络问题或日志记录强度增加时发生日志丢失。 如果由于某种原因 ElasticSearch 无法立即将文档写入索引,则该文档将排队并存储在磁盘上。 因此,在我们的例子中,为了消除导致上述错误的问题根源,有必要为缓冲参数设置正确的值,此时 Fluentd 输出缓冲区将具有足够的大小并且同时设法在规定的时间内清除。
值得注意的是,下面讨论的参数值在输出插件中使用缓冲的每种特定情况下都是单独的,因为它们取决于许多因素:服务将消息写入日志的强度、磁盘系统性能、网络信道负载及其带宽。 因此,为了获得适合每种情况的缓冲区设置,但又不冗余,避免盲目冗长的搜索,可以利用Fluentd在运行过程中写入日志的调试信息,相对快速地获得正确的值。
在记录问题时,配置如下所示:
<buffer>
@type file
path /var/log/fluentd-buffers/kubernetes.test.buffer
flush_mode interval
retry_type exponential_backoff
flush_thread_count 2
flush_interval 5s
retry_forever
retry_max_interval 30
chunk_limit_size 8M
queue_limit_length 8
overflow_action block
</buffer>
求解问题时,手动选择以下参数的值:
chunk_limit_size — 缓冲区中的消息被划分成的块的大小。
- lush_interval — 清除缓冲区的时间间隔。
- queue_limit_length — 队列中块的最大数量。
- request_timeout 是 Fluentd 和 ElasticSearch 之间建立连接的时间。
总缓冲区大小可以通过参数queue_limit_length和chunk_limit_size相乘来计算,它可以解释为“队列中块的最大数量,每个块都有给定的大小”。 如果缓冲区大小不足,日志中会出现以下警告:
2020-01-21 10:22:57 +0000 [warn]: [test-prod] failed to write data into buffer by buffer overflow action=:block
意味着缓冲区没有时间在规定的时间内被清除,进入满缓冲区的数据被阻塞,这会导致部分日志丢失。
您可以通过两种方式增加缓冲区:增加队列中每个块的大小,或增加队列中可以包含的块的数量。
如果您将块大小 chunk_limit_size 设置为超过 32 MB,则 ElasticSeacrh 将不会接受它,因为传入的数据包太大。 因此,如果需要进一步增加缓冲区,最好增加最大队列长度queue_limit_length。
当缓冲区停止溢出并且只剩下超时不足消息时,您可以开始增加 request_timeout 参数。 但是,如果您将该值设置为超过 20 秒,则 Fluentd 日志中将开始出现以下警告:
2020-01-21 09:55:33 +0000 [warn]: [test-dev] buffer flush took longer time than slow_flush_log_threshold: elapsed_time=20.85753920301795 slow_flush_log_threshold=20.0 plugin_id="postgresql-dev"
该消息不会以任何方式影响系统的操作,并且意味着缓冲区刷新时间比slow_flush_log_threshold参数设置的时间长。 这是调试信息,我们在选择 request_timeout 参数的值时使用它。
广义选择算法如下:
- 将 request_timeout 设置为保证大于所需值(数百秒)的值。 在设置过程中,正确设置该参数的主要标准是缺少超时的警告消失。
- 等待有关超过 Slow_flush_log_threshold 阈值的消息。 elapsed_time 字段中的警告文本将显示缓冲区被清除的实时时间。
- 将 request_timeout 设置为大于观察期间获得的最大 elapsed_time 值。 我们将 request_timeout 值计算为 elapsed_time + 50%。
- 要从日志中删除有关长缓冲区刷新的警告,可以提高 Slow_flush_log_threshold 的值。 我们将该值计算为 elapsed_time + 25%。
如前所述,这些参数的最终值是针对每种情况单独获得的。 通过遵循上述算法,我们可以保证消除导致重复消息的错误。
下表显示了在选择上述参数值的过程中每天导致消息重复的错误数量如何变化:
节点 1
节点 2
节点 3
节点 4
之前之后
之前之后
之前之后
之前之后
刷新缓冲区失败
1749/2
694/2
47/0
1121/2
重试成功
410/2
205/1
24/0
241/2
还值得注意的是,随着项目的增长,最终的设置可能会失去其相关性,相应地,日志数量也会增加。 超时不足的主要标志是向 Fluentd 日志返回有关长缓冲区刷新的消息,即超过 Slow_flush_log_threshold 阈值。 从此时开始,距离超过 request_timeout 参数还有很小的余量,因此需要及时响应这些消息并重复上述选择最佳设置的过程。
结论
微调 Fluentd 输出缓冲区是配置 EFK 堆栈的主要阶段之一,决定其操作的稳定性以及索引中文档的正确放置。 根据所描述的配置算法,您可以确保所有日志都将以正确的顺序写入ElasticSearch索引,而不会重复或丢失。
另请阅读我们博客上的其他文章:
Go 和 Zabbix 5.0 成为朋友 在不停机的情况下升级 Kubernetes 集群 Kubernetes:为什么设置系统资源管理如此重要? 缩小 Docker 镜像的三个简单技巧 大量异构Web项目的备份
来源: habr.com