杀死集群的IngestNode和Grok
这篇文章是2018年Elastic stack(Elasticsearch)圣诞日历的第八天的文章。
首先
你好,我叫tshohe。
Elasticsearch是一种非常重要的软件,经常在日志和度量的收集与可视化中使用。在本文中,我想深入研究我今年遇到的Elasticsearch集群停止故障情况。
环境
-
- Elasticsearch6.1.1 + Filebeat6.1.1(PostgreSQL Module)
- 各ノードはmaster/data/ingestを兼任
我今年年初开始使用PostgreSQL10,当时pg_stats_reporter还没有配套,我在考虑如何进行监控。正好在那个时候,Beats新增了PostgreSQL模块,所以我决定试用一下。
Filebeat 的 PostgreSQL 模块在当时仍处于实验性阶段,但它只是通过 Filebeat 将日志转发并在 IngestNode 的 Pipeline(Grok)中进行处理,所以我并没有太担心会发生什么奇怪的事情。(自负责任…)
※ 现在似乎已经正式取消了实验性质
https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-postgresql.html
有一天,当我看到配备了可视化/仪表盘的时候,我觉得它可以很好地进行可视化,感觉很不错。然而,发生了一场悲剧。
我突然陷入了这样的困境…!
-
- write処理が溜まってキューが枯渇
-
- リクエストがrejectされまくりDocumentが全く追加されない状態
- リクエストを受けるノードのCPU使用率(user)が100%に張り付き
发生了什么事?
在停止各种日志传输的同时,我们发现问题的原因是由filebeat添加的名为”filebeat-6.1.1-postgresql-log-pipeline”的Pipeline。
这个Pipeline的grok processor在处理特定日志时花费了很长时间,导致队列堵塞。
让我们来看一下导致问题的Grok设置。
Filebeat-6.1.1-postgresql-log-pipeline是一个链接到GitHub上的文件,它包含了一个路径为filebeat/module/postgresql/log/ingest/pipeline.json的文件的引用,我们可以从这个文件的第18行中找到所需信息。
"grok": {
"field": "message",
"ignore_missing": true,
"patterns": [
"%{LOCALDATETIME:postgresql.log.timestamp} %{WORD:postgresql.log.timezone} \\[%{NUMBER:postgresql.log.thread_id}\\] %{USERNAME:postgresql.log.user}@%{HOSTNAME:postgresql.log.database} %{WORD:postgresql.log.level}: duration: %{NUMBER:postgresql.log.duration} ms statement: %{MULTILINEQUERY:postgresql.log.query}",
"%{LOCALDATETIME:postgresql.log.timestamp} %{WORD:postgresql.log.timezone} \\[%{NUMBER:postgresql.log.thread_id}\\] \\[%{USERNAME:postgresql.log.user}\\]@\\[%{HOSTNAME:postgresql.log.database}\\] %{WORD:postgresql.log.level}: duration: %{NUMBER:postgresql.log.duration} ms statement: %{MULTILINEQUERY:postgresql.log.query}",
"%{LOCALDATETIME:postgresql.log.timestamp} %{WORD:postgresql.log.timezone} \\[%{NUMBER:postgresql.log.thread_id}\\] %{USERNAME:postgresql.log.user}@%{HOSTNAME:postgresql.log.database} %{WORD:postgresql.log.level}: ?%{GREEDYDATA:postgresql.log.message}",
"%{LOCALDATETIME:postgresql.log.timestamp} %{WORD:postgresql.log.timezone} \\[%{NUMBER:postgresql.log.thread_id}\\] \\[%{USERNAME:postgresql.log.user}\\]@\\[%{HOSTNAME:postgresql.log.database}\\] %{WORD:postgresql.log.level}: ?%{GREEDYDATA:postgresql.log.message}",
"%{LOCALDATETIME:postgresql.log.timestamp} %{WORD:postgresql.log.timezone} \\[%{NUMBER:postgresql.log.thread_id}\\] %{WORD:postgresql.log.level}: ?%{GREEDYDATA:postgresql.log.message}"
],
"pattern_definitions": {
"LOCALDATETIME": "[-0-9]+ %{TIME}",
"GREEDYDATA": ".*",
"MULTILINEQUERY" : "(.|\n|\t)*?;$"
}
}
如果MULTILINEQUERY不匹配,则使用GREEDYDATA进行处理。
顺便提一下,这个正则表达式已经不太好,已经被修改成了以下的样式。
6.5.1版本的Elastic Beats的filebeat模块中的postgresql/log/ingest/pipeline.json文件,可以在以下链接找到:
https://github.com/elastic/beats/blob/v6.5.1/filebeat/module/postgresql/log/ingest/pipeline.json
- "GREEDYDATA": ".*",
- "MULTILINEQUERY" : "(.|\n|\t)*?;$",
+ "GREEDYDATA": "(.|\n|\t)*",
让我们检查一下,MULTILINEQUERY和GREEDYDATA这两个表达式是否需要以分号结束,并且找出正则表达式(.*)?;$的不正常之处。
由于不是org.joni.Regex,所以准确的操作可能会有所不同,但我们可以在一个能够简单跟踪步骤的网站(https://regex101.com)上进行确认。
如果假设在MULTILINEQUERY的地方输入的查询是以下这种情况的话…
select a.b,
a,
b,
c,
d
from t1
join t3
on a = a
and b = b
where aaaaaaaaa between '2018-12-07 00:00:00' and '2018-12-08 00:00:00'
union all
select a.b,
a,
b,
c,
d
from t2
join t4
on a = a
and b = b
where bbbbbbbb between '2018-12-07 00:00:00' and '2018-12-08 00:00:00';
大约需要1788步处理(耗时187毫秒)。
由于对任何文本的后面进行了”;”的检查,所以与普通的GREEDYDATA相比,步骤数似乎更多。
然而,真正糟糕的是当后面出现意外的字符串时。
这次问题涉及的日志是指当PostgreSQL的log_min_duration_statement超过设定时间时输出的慢查询日志,因此查询会被直接输出。因此可能会在下面的注释中提到。
...
where bbbbbbbb between '2018-12-07 00:00:00' and '2018-12-08 00:00:00'; -- hogehoge piyopiyo
一下子踏出的步数飚升至934,154步(1.06秒)。
由于字符数量的增加,步数会急剧增加,所以如果SQL语句的缩进和空格有很多的话会出大问题。(就像这个PostgreSQL的例子中的慢查询一样,非常长…)
-
- .*があることでバックトラックが発生(最大限マッチするパターンを探すため)
- ;で終わっていればよいが、終わっていないと1~n文字目の地点から開始した場合の文字列全てのパターンを確認するのでとんでもない計算量になる
总结
只要输入像hogehogepiyopiyoA hogehogepiyopiyo……这样的数据到定义了类似于 (.*)A$ 的grok模式的地方,就可以给集群带来巨大负荷,甚至导致集群崩溃。
刚刚在讨论Elasticsearch,或者说是正则表达式的问题。。。
教训切勿忽视
-
- 应该写入能够容忍日志的混乱的程度。
当将Pipeline或Grok设置专门用于特定字符串时,对于意外值的行为是不可预料的,这令人担忧。应该在测试环境中检查各种模式。
应该将IngestNode与MasterNode/DataNode分离。
至少应该能够进行搜索。但只有这样的话,IngestNode的优点就消失了,就算没有Fluentd/Logstash,也可以插入处理。