LogstashやFluentdで、正規表現の書き方による性能問題を防ぐには(Part 2)
こんにちは!
Airitechビッグデータ・AI活用グループのニャンリンアウンです。データ分析用Logstashの設定やメンテナンスなどを担当しております。
以前書いた「LogstashやFluentdで、正規表現の書き方による性能問題を防ぐには」という記事の続きです。
前回の記事で以下を紹介しました。
1. GREEDYDATA(.*)の使用で性能問題が発生する場合があること
2. DATA(.*?)を使うとその問題を防ぐことができること
3. ただし、DATA(.*?)を使っても、想定外の文字列が来た時に性能問題が発生する場合があること
1と2については、そのメカニズムの説明を書きましたが、3の問題がなぜ起こるのかを書いていなかったので、今回はそちらについて書きたいと思います。
前回の記事の計測結果の確認
aaa bbb ccc ddd eee
のような、スペース区切りでfield数が5個, 10個, 20個, 20個(1%だけfield数が19個しかない不正なデータを混ぜた)の4パターンのデータに対して、
GREEDYDATA(.*), DATA(.*?), NOTSPACE(\S+)の3つのパターンの正規表現で、fieldの抽出を行う処理の速さ(1秒間に何件処理できるか)を計測しました。
正規表現またはLogstashのgrok patternの書き方は以下のようになります。
GREEDYDATA
正規表現の場合:
^(?<field1>.*) (?<field2>.*) (?<field3>.*) (?<field4>.*) (?<field5>.*)$
grok patternを使った場合:
^%{GREEDYDATA:field1} %{GREEDYDATA:field2} %{GREEDYDATA:field3} %{GREEDYDATA:field4} %{GREEDYDATA:field5}$
DATA
正規表現の場合:
^(?<field1>.*?) (?<field2>.*?) (?<field3>.*?) (?<field4>.*?) (?<field5>.*?)$
grok patternを使った場合:
^%{DATA:field1} %{DATA:field2} %{DATA:field3} %{DATA:field4} %{DATA:field5}$
NOTSPACE
正規表現の場合:
^(?<field1>.*?) (?<field2>.*?) (?<field3>.*?) (?<field4>.*?) (?<field5>.*?)$
grok patternを使った場合:
^%{NOTSPACE:field1} %{NOTSPACE:field2} %{NOTSPACE:field3} %{NOTSPACE:field4} %{NOTSPACE:field5}$
計測結果
field数が20個で、1%だけfield数が19個しかない不正なデータが混ざっていた場合に、大幅に性能が悪くなっていることが確認できます。
DATA(.*?)の利用で性能問題が発生するメカニズム
前回の記事で、GREEDYDATA(.*)の利用で正規表現のバックトラックが発生して繰り返し処理の回数が指数関数的に増えてしまう様子をご紹介しました。
DATA(.*?)の場合も、仕組みはほとんど同じです。
field数が6個の想定の所に、field数が5個しかない文字列が来た時、マッチしないことが確定するまでの間に以下のようなパターンが試されます。
このように、field数が増えると、チェック処理で行ったり来たりする回数が指数関数的に増えてしまいます。GREEDYDATA(.*)と違うのは、チェックする順番だけで、マッチしない文字列の場合はあらゆるパターンを試そうとして結局同じことになってしまいます。
これに対し、NOTSPACE(\S+)であれば、マッチしない場合にもすぐに判定処理が終わるので、性能劣化が起こりません。
区切り文字がfieldの中に含まれる可能性がある場合はどうするか
様々な種類のログを扱っていると、区切り文字が要素の中に含まれてしまうケースに遭遇することがあります。
例えば、以下のように、「OK」と「Not OK」が入ることがあるような場合を考えます。
2021-01-01T12:11:35.123Z OK 10.0.0.1 abcde
2021-01-01T12:11:36.123Z OK 10.0.0.1 abcde
2021-01-01T12:11:37.123Z Not OK 10.0.0.2 ghijk
2021-01-01T12:11:38.123Z OK 10.0.0.1 abcde
区切り文字が要素の中に含まれてしまうケースがある場合、どこで区切っていいか分からず困ってしまいますが、「区切り文字が入る可能性があるfieldが1つしかない」ということが分かっているのであれば、DATA(.*?)を1箇所だけ使うことで対処可能です。
^%{NOTSPACE:timestamp} %{DATA:is_ok} %{NOTSPACE:ip} %{NOTSPACE:log_message}$
この書き方だと、区切り文字が要素の中に含まれる場合と、不正なフォーマットのログが来た場合にバックトラックが発生しますが、1箇所であれば処理量がそこまで極端に増えるわけではないので、許容範囲と言えるでしょう。
なお、区切り文字が入る可能性があるfieldが2つある場合には、DATA(.*?)を2箇所にしてしまうと、どちら要素に区切り文字が入ってしまったのか区別できないので、性能ではなく機能の観点で不適切です。
区切り文字がfieldの中に含まれる可能性がある場合のより好ましい書き方
より好ましい書き方は、バックトラックの発生を抑えるために、DATA(.*?)以降をできるだけ絞り込んだ形で記載することです。
「OK」「Not OK」の次の要素が必ずIPアドレスの形をしていることが決まっているのであれば、以下のように記載したほうがよいです。
^%{NOTSPACE:timestamp} %{DATA:is_ok} %{IP:ip} %{NOTSPACE:log_message}$
この書き方であれば、
2番目の要素が「Not」で、3番目の要素が「OK」
という可能性が、「O」まで読んだ段階ですぐに排除されます(「O」がIPアドレスの一部になり得ないため)。
区切り文字がfieldの中に含まれるときは引用符で囲まれる場合
例えば、以下のように、スペースが含まれる場合だけダブルクオーテーションで囲まれる仕様のログも見たことがあります。
2021-01-01T12:11:35.123Z OK 10.0.0.1 abcde
2021-01-01T12:11:36.123Z OK 10.0.0.1 abcde
2021-01-01T12:11:37.123Z "Not OK" 10.0.0.2 ghijk
2021-01-01T12:11:38.123Z OK 10.0.0.1 abcde
このような場合は、引用符で囲まれている場合と、そうでない場合の2パターンがあることを正規表現で表してやります。
^%{NOTSPACE:timestamp} (?:"%{NOTDOUBLEQUOTE:is_ok}"|%{NOTSPACE:is_ok}) %{IP:ip} %{NOTSPACE:log_message}$
ダブルクオーテーションで囲まれている場合は、「Not OK」の部分は、「ダブルクオーテーション以外の文字が1文字以上」とすることで、指定が明確になります。
なお、「ダブルクオーテーション以外」というgrok patternは別途定義する必要があります。もしくは、ここだけ正規表現で記載してもよいです。
^%{NOTSPACE:timestamp} (?:"(?<is_ok>[^"]+)"|%{NOTSPACE:is_ok}) %{IP:ip} %{NOTSPACE:log_message}$
dissect filterやcsv filterの利用
dissect filterやcsv filterが利用できるケースでは、grok filterを使わずにこれらのfilterを使うことも検討するとよいでしょう。
特に、CSVデータの場合は、
「区切り文字のカンマやダブルクオーテーションが含まれる場合はダブルクオーテーションで要素が囲まれるが、その中にダブルクオーテーションがある場合はエスケープされる」
というようなことになり、これを正規表現で表すのは大変です。
次の機会に、dissect filterやcsv filterとの性能比較についても記事にしてみたいと思います。
Airitechではビッグデータ解析ツール導入・支援のほか、トラブルシュートやシステム性能サービスなど、さまざまなサービスを提供しております。
UiPathユーザー向けElasticsearch・Kibanaのオンライントレーニングも行っています。Elasticsearch・Kibanaに興味のある方は、ぜひご参加ください。
Elasticsearch・Kibanaトレーニング(オンライン)
Airitechの採用情報はこちら
ホームページ