🤦

New Relicの”ゼロNull”問題に僕らがハマる理由を考えてみた

2024/05/10に公開

こんにちは。イオンスマートテクノロジー株式会社(AST)でSREチームの林 aka もりはやです。

当社で利用しているNew Relicは”フルスタックオブザーバビリティ”を提供してくれる素晴らしいサービスです。本記事ではNew Relicが提供している機能の一つSyntheticを利用したアラートが自動復旧しなかった問題が、New Relicユーザの多くの方(?)が経験する”ゼロヌル問題”であり、私たちがなぜハマるのかを自分なりに考えてみました。(要因は複数あるため、シリーズ化するかもしれません)

TL;DR

はじめに要点を記載します。

  • New RelicのSyntheticのアラートが自動復旧しない状態に気づいた
  • 調べるとNew Relicでよくある質問”ゼロNull”が原因だった
  • filter によるサブ句を利用しないとWHERE句で要素に一致するデータがない場合、0ではなくNullの値が返される事象だった

New Relicの”ゼロNull”問題とは

New Relicの”ゼロNull”問題について簡単に説明します。詳しくはNew Relicの中の方が記載している”ゼロヌル問題”をみていただくのが早いですが、実際に回復しないアラートを体験したユーザ目線で記載します。

要点は以下です。

  • New Relicのアラートを設定する場合、Alert Conditionのthresholdsで閾値を定量的に設定する必要がある
  • Syntheticなどの監視の結果を定量化するために COUNT を利用するのが一般的
  • New Relicの COUNT はWHERE句でHitするものがないと 0 ではなく Null がリターンされる
  • WHERE句の条件で SyntheticがFAILEDした場合はCOUNTして1を返す 場合、テストが正常終了した際に not FAILED が永久的にNullがリターンされて復旧することがない

と箇条書きでまとめても、いまいちピンとこないかもしれません...(私のように)

COUNTの結果はゼロが返るものだという誤解

COUNT0 ではなくNullがリターンされることへの誤解の理由を説明するために、New Relicを離れて一般的なDBの話をします。(New Relicの仕様と異なるため、あえて誤解と表現しました)

例としての以下のテーブル及びデータが存在する場合に、MySQLやPostgreSQLがどのような動作をするかを確認します。

  • テーブル"CHECK_RESULT"は2つのカラムをもつ
  • カラム1は日付型の"datetime" *PostgreSQLの場合は"timestamp"
  • カラム2は文字列型の"result"
    • "result"には"FAILED"か"SUCCESS"のどちらかが入る

結果は以下に記した通り、MySQLとPostgreSQLは COUNT 関数で WHERE を利用したクエリがヒットしない場合ゼロ(0)を返します。これらのDBでの経験がNew Relicの仕様と異なることが私の勘違いに繋がりました。(そして多くのDB経験者にも同じことが起きるのではと考えています)

MySQLの場合

コンテナを使ってサクッと環境を用意します。私のPC環境ではコンテナ操作を podman を利用していますがdockerをお使いの方はそのまま docker へ置き換えても動作するはずです。

$ podman pull mysql:latest
$ podman run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest
$ podman exec -it some-mysql mysql -p
-> my-secret-pw を入れる

起動と接続に続いてテーブルを作成します。いきなり create table を実行すると以下のエラーが出たため use mysql でDBを選択しました。

ERROR 1046 (3D000): No database selected

> use mysql
> create table check_result(datetime datetime, result varchar(10));

さて、テーブルが作成されレコードが1行もない状態で count をしてみましょう。

mysql> select count(*) from check_result;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.01 sec)

結果はゼロです。Nullではありません。続いていくつかレコードを挿入します。イメージとして5分間隔のチェックで 10:10, 10:15 の2回だけFAILEDが記録された状況の再現です。

mysql> insert into check_result values ('2024-05-09 10:00:00', 'SUCCESS');
mysql> insert into check_result values ('2024-05-09 10:05:00', 'SUCCESS');
mysql> insert into check_result values ('2024-05-09 10:10:00', 'FAILED');
mysql> insert into check_result values ('2024-05-09 10:15:00', 'FAILED');
mysql> insert into check_result values ('2024-05-09 10:20:00', 'SUCCESS');
mysql> insert into check_result values ('2024-05-09 10:25:00', 'SUCCESS');
mysql> insert into check_result values ('2024-05-09 10:30:00', 'SUCCESS');

この状態でFAILEDをカウントすれば、当然2となります。

mysql> SELECT count(*) from check_result WHERE result = 'FAILED';
+----------+
| count(*) |
+----------+
|        2 |
+----------+
1 row in set (0.00 sec)

New RelicのWindow Durationが5minの場合を想定し、復旧後の 10:25-10:30の間をカウントします。

mysql> SELECT count(*) from check_result WHERE result = 'FAILED' AND datetime BETWEEN '2024-05-09 10:25:00' AND '2024-05-09 10:30:00';
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

このようにMySQLではFAILEDなレコードがHITしない場合は0になるのが仕様です。

PostgreSQLの場合

こちらもコンテナを使ってサクッと環境を用意します。繰り返しになりますが私のPC環境ではコンテナ操作を podman を利用していますが docker へ置き換えても動作するはずです。

$ podman pull postgres:latest
$ podman run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres:latest
$ podman exec -it some-postgres psql -U postgres

起動と接続に続いてテーブルを作成します。

postgres=# create table check_result(datetime timestamp, result varchar(10));

さて、テーブルが作成されレコードが1行もない状態で count をしてみましょう。

postgres=# select count(*) from check_result;
 count
-------
     0
(1 row)

結果はゼロです。Nullではありません。続いていくつかレコードを挿入します。イメージとして5分間隔のチェックで 10:10, 10:15 の2回だけFAILEDが記録された状況の再現です。

postgres=# insert into check_result values ('2024-05-09 10:05:00', 'SUCCESS');
postgres=# insert into check_result values ('2024-05-09 10:10:00', 'FAILED');
postgres=# insert into check_result values ('2024-05-09 10:15:00', 'FAILED');
postgres=# insert into check_result values ('2024-05-09 10:20:00', 'SUCCESS');
postgres=# insert into check_result values ('2024-05-09 10:25:00', 'SUCCESS');
postgres=# insert into check_result values ('2024-05-09 10:30:00', 'SUCCESS');

この状態でFAILEDをカウントすれば、当然2となります。

postgres=# SELECT count(*) from check_result WHERE result = 'FAILED';
 count
-------
     2
(1 row)

New RelicのWindow Durationが5minの場合を想定し、復旧後の 10:25-10:30の間をカウントします。

postgres=# SELECT count(*) from check_result WHERE result = 'FAILED' AND datetime BETWEEN '2024-05-09 10:25:00' AND '2024-05-09 10:30:00';
 count
-------
     0
(1 row)

このようにMySQLと同様にPostgreSQLではFAILEDなレコードがHITしない場合は0になるのが仕様です。

New Relicのクエリ順序によるNullの動作

上述したように過去のRDBMSでの経験からCOUNTの結果は 0 だと考えてしまいましたが、New Relicは仕様が異なります。

New Relicでは、アラートのためのNRQLの COUNT(*)WHERE result = 'FAILED' の場合 FAILED がなければ 0 ではなく Null が返されます。

この動作は以下の説明を読むことでわかりやすくなります。

NRQLアラート条件を作成する - NRQL条件および演算のクエリ順序

該当部分を引用します。

NRQL条件および演算のクエリ順序

デフォルトで、集計ウィンドウの期間は1分ですが、必要に応じてウィンドウは変更できます。集計ウィンドウが何であろうと、New RelicはNRQL条件のクエリの関数を使用して、そのウィンドウのデータを集計します。クエリは構文解析され、以下の順序でシステムによって実行されます。

  1. FROM 句。どのイベントタイプを取り込む必要があるのか?
  2. WHERE 句。何を除去できるのか?
  3. SELECT 句。今、フィルタリングしたデータセットから何の情報を返す必要があるのか?

NRQLの動作として 2. WHERE句 の段階で全てのデータが除去された場合 3. SELECT句 は実行されず Null が返されることとなります。

そうです、ここで 0 ではなく Null が返されるのです。(大事なことなため2回書いた)

対策としての filter 関数

この(個人的に)直感的ではないNullが返却される動作に対して、New Relicはもちろん対策を用意してくれています。

”ゼロヌル問題”より以下に引用します。

また、別の方法としては、SELECT 句で filter 関数を用いる方法が挙げられます。NRQL の WHERE 句で条件を儲けるのでなく、SELECT 句の filter 関数でフィルタリングした結果を count することで 数値としてのゼロ (0) が記録できます。

SELECT filter(count(*), where result = 'FAILED') FROM SyntheticCheck WHERE monitorName = 'My Cool Monitor'

つまり、Synthethicを活用する私たちのアラートのNRQLは filter を用いた以下のような形にすることでNullではなく確実に 01 を結果にとることが出来るようになりました。

SELECT filter(count(*), WHERE result = 'FAILED') AS 'Failures'
FROM SyntheticCheck
WHERE entityGuid IN
  (<Synthetic monitorsのGuIDその1>,<Synthetic monitorsのGuIDその2>,<Synthetic monitorsのGuIDそのN...>)
FACET monitorName

まとめ

以上、New Relicの”ゼロNull”問題にハマる理由を、一般的なDBの例も踏まえて理由づけしてみました。違いを理解することで少しでもNew Relicユーザの皆様の誤解(あえて強い表現)が減らせると幸いです。

PS: filter を使ってNullを回避したはずが、Window Durationの兼ね合いでやっぱりNullを得ていて、閾値を下回らなかった話もあるため気が向いたら連載予定です。

AEON TECH HUB

Discussion