重複するイベントを削除したい


ことの背景

とある Add-on を使用しデータを取得していたところ、イベントデータの重複に気づきました。この Add-on は 10分毎に REST API からデータを取得するものなのですが、どうやらその 10分の境界付近のデータを二重に取得してしまっている模様です。

Splunk では delete コマンドでイベントの論理削除を行うことはできますが、さすがに 10分毎に確認して手動削除というわけにはいかないので、自動的に重複しているイベントを削除したいというのが今回の背景です。

解決方法

急いでいる人向け

長々読みたくない人向けに結論というか、汎用的に利用できそうなサーチ例を貼ります。

index="xxx" sourcetype="yyy"
| eval event_id=_cd
| search
    [search index="xxx" sourcetype="yyy"
    | eval hash=sha256(_raw)
    | streamstats count by hash
    | where count > 1
    | eval event_id=_cd
    | return 100 event_id] 
| delete

これを見て納得・理解できる方はご利用ください。
なぜこれでうまくいくのか、よくわからない方は続きを読んでください。

前提

上述しましたが、Splunk では一度取り込んでしまったデータは、インデックスごと消す以外ディスク上から消す術はありません。代わりに delete コマンドという論理的にデータを消す(サーチから見えなくする)が用意されています。

ただし、このコマンドはデフォルトではどのユーザ(ロール)から利用できないようになっています。間違って、簡単にデータを消してしまっては困るので、当たり前といえば当たり前です(論理的に削除と言っても、ディスク上にデータが残っているからといって、削除を取り消す方法はありません。少なくとも私は知りません)。

delete コマンドを利用できるようにするためには、can_delete という権限をユーザに付与しておく必要があります。この手順について、ここでは割愛します。

重複を探す

まず思いつきそうな方法

まず、何をもってイベントが重複していると言えるのかというと、_raw が全く同じであるということしかなさそうです。ログ種別によっては、各イベントにユニークな ID が付いているなどといったラッキーなケースもあるとは思いますが、ここではそういったことはないという前提で考えます。

_raw を比較するしかない以上、重複を見つけるに

index="xxx" sourcetype="yyy"
| stats count by _raw
| where count > 1

とかやりますか? 一般的な感覚はわかりませんが、個人的には by _raw というのが非常に気持ち悪いです。とはいえ、一応重複しているイベントは見つけられました。

で、次にこれを消そう、となるわけですが、大きく 2 つの問題があることに気づきます。それは、

  1. stats コマンドで集計してしまっているので、そのまま deleteコマンドに渡せない
  2. 重複が必ずしも 2 イベントとは限らない

2つ目はそれほど大きな大きな問題ではないかもしれませんが、1つ目は大問題です。せっかく重複を見つけたのに、消せないので。

次に思いつくのが、サブサーチです。上記のサーチをサブサーチとして、結果をメインのサーチに返すというものです。具体的には、次のような感じのサーチになります。

index="xxx" sourcetype="yyy"
    [search index="xxx" host="yyy"
    | stats count by _raw 
    | return 100 _raw]

return コマンドは返却する結果の最大値を決める必要があるので、とりあえず 100 としています。ちなみに、サブサーチ部分だけ実行していただくとわかるのですが、サブサーチはメインのサーチに

(_raw=...) OR (_raw=...) OR ...

のような検索条件を返しています。_raw を条件にするのもかなり気持ち悪いですね。

といったところあたりまで考えて、私はこの方法は筋が悪そうなので諦めることにしました。
(実は筋が悪いだけではなく、他にも問題があることが後からわかりましたが、ここでは省略します)

良さそうな方法

まず、_raw をそのまま扱うのは気持ち悪い(具体的な根拠があるわけではありません)ので、イベントを区別するためにハッシュ値を _raw に対して計算します。

| eval hash=sha256(_raw)

これで、_raw が同じ内容であれば同じハッシュ値が得られるはずです。

次に、上述の問題点でもあった stats で集計してしまうと、結果を delete コマンドに渡せない点について考えます。イベントはそのまま残したい、でもハッシュ値毎の集計がしたいということで、eventstats が良さそうです。

つまり、

| eval hash=sha256(hash)
| eventstats count by hash
| table hash count

とします。こうすると、同じハッシュ値をもつイベント数をカウントして、その値を各イベントに新しいフィールド(count)を追加してくれます。イメージ的には、こんな感じです。

hash ... count
hash_a ... 3
hash_b ... 1
hash_a ... 3
hash_c ... 1
hash_a ... 3

hash_a は 3 回現れるので、hash_a を持つイベントにはそれぞれ count に 3 が入ります。同様に hash_b, hash_c は 1 回ずつしか現れないので、count は 1 です。

この結果から count が 1 より大きい2以上)イベントが重複しており、削除対象とすればいいと判断できます。

これで全て解決と言いたいところですが、次の課題があります。それは、count > 1 に該当するイベントを全て削除してはダメだということです。つまり、

| eval hash=sha256(hash)
| eventstats count by hash
| where count > 1
| delete

としてしまうと、重複していたイベントが全て消えてしまい、残ってほしい最後の 1 イベントもなくなってしまいます。

それではどうしたらいいか。重複しているイベントが何個重複しているかはわかっているわけなので(count)、それらに通し番号を振ってみればいいのでは、と考えました。ハッシュ値毎に通し番号を振るには streamstats が良さそうです。つまり、次のようなサーチになります。

| eval hash=sha256(hash)
| eventstats ev_count by hash
| streamstats st_count by hash
| table hash ev_count st_count

こうすると、結果は、

hash ... ev_count st_count
hash_a ... 3 1
hash_b ... 1 1
hash_a ... 3 2
hash_c ... 1 1
hash_a ... 3 3

のような形になっているはずです。
各ハッシュ値をもったイベントを 1 つだけ残して他のイベントを delete コマンドに渡すには ev_count と st_count を比較してフィルタしてあげればいいです。

| eval hash=sha256(hash)
| eventstats count as ev_count by hash
| streamstats count as st_count by hash
| where ev_count > st_count

st_count の最大値は ev_count に一致しますから、where 文には引っかかりません。つまり 1 イベントだけを残すことができます。

もっと良さそうな方法

ここまでやってきて、ふと気づきました 「eventstats いらなくね?」。

hash ... ev_count st_count
hash_a ... 3 1
hash_b ... 1 1
hash_a ... 3 2
hash_c ... 1 1
hash_a ... 3 3

先程の表を見返します。重複があるイベントは st_count が 2 以上のものがあるわけなので、ev_count とか関係なく st_count > 1 で十分じゃんと。

つまり、こんなサーチで良いのではと。

| eval hash=sha256(hash)
| streamstats count by hash
| where count > 1

思いのほかシンプルなサーチになってしまいました。

削除するできなかった

誤算

ようやく、これで delete コマンドへ渡すことができそうです。
先程のサーチ結果を delete コマンドに流し込みます。

| eval hash=sha256(hash)
| streamstats count by hash
| where count > 1
| delete

これで重複したイベントが華麗に消えてくれる、と思ったのですが、、、

知りませんでした。delete コマンドは分散ストリーミングコマンドの後にしかもってこれないんですね…
分散ストリーミングコマンドというのは、シンプルにいうとインデクサ上で実行可能なコマンドだと捉えていただくと大きく間違っていないと思います。イベントを集計しなくてもできる、eval 等の個々のイベントに対して操作するコマンドです。正確なところはこちらをご確認ください。

対象となるイベント全部を調べて同じ内容のイベントを探すわけですから、分散ストリーミングコマンドだけで書くのは無理と断念。

妥協案

上記のサーチで実現できれば非常にシンプルで良かったのですが、致し方ないのでサブサーチを使う方針に変更。次のようなサーチを書きました。

index="xxx" sourcetype="yyy"
| eval event_id=_cd
| search
    [search index="xxx" sourcetype="yyy"
    | eval hash=sha256(_raw)
    | streamstats count by hash
    | where count > 1
    | eval event_id=_cd
    | return 100 event_id] 
| delete

基本的には、サブサーチで重複しているイベントを見つけて、それをメインのサーチで消すということをしているだけなのですが、重複イベントのハッシュ値をメインサーチに返すと、メインサーチで重複イベントを全て消してしまうことになりうれしくありません。

ということで、ここでは同一ハッシュでもイベントを区別するために、イベントを一意に区別する隠しフィールド "_cd" を使うことにしました。
(補足: マニュアルによると隠しフィールドではなく内部フィールドと呼ぶのが正しいようです。)

これでようやく完成です。初めはそんなに難しくないだろうと思っていたのですが、思いのほか苦労することとなりました。

繰り返しになりますが、delete コマンドで消してしまったイベントは二度と元には戻りませんから、ご利用は慎重にお願いします。このサーチを saved search にして定期実行すれば自動的に重複排除することもできそうですが、十分検証してからやったほうが良さそうですね。