採用情報 お問い合わせ

BLOG

Zabbix 7.0 : 非特権ユーザーにおける障害画面の表示遅延と改善策

1. 概要

Web フロントエンドの「障害」画面を表示する際、画面の応答が極端に遅延する事象が確認されています。本事象は、非特権管理者(User / Admin)権限でのみ発生し、特権管理者(Super Admin)権限では発生しません。
本記事では、ユーザーの権限差異によって生じるパフォーマンス低下の内部メカニズムを解説し、運用上の回避策およびバージョンアップによる軽減策について解説します。

※ 本記事で使用する用語についてはこちらの用語集を参照してください。

2. 前提環境

Zabbix 7.0.18 および 7.0.19

3. 原因

本遅延事象の根本的な原因は、非特権管理者(User/Admin)に対するアクセス権限チェック処理に起因するデータベースへの相対的な負荷の増大です。

非特権管理者の場合、障害データを抽出・表示する際、トリガーに紐づくホスト群の中にユーザーが閲覧権限を持たないホストが混在していないかを判定・除外するクエリが実行されます。そのため、対象データが膨大に蓄積されている環境においては、特権管理者と比較して応答遅延として顕在化するケースがあります。

3.1. 実行される SQL クエリの差異

実際に特権ユーザーと非特権ユーザーで障害画面を表示する際に発行されるクエリの違いを比較します。

【特権ユーザー(Super Admin)のクエリ】

権限チェックの処理自体がないため、単一テーブルから条件に合致するレコードを抽出してソートするのみの軽量な処理となります。

SELECT
    p.eventid,
    p.objectid,
    p.clock,
    p.ns,
    p.name,
    p.severity,
    p.cause_eventid
FROM
    problem p
WHERE
    p.source = 0
    AND p.object = 0
    AND p.objectid = 25100
    AND p.cause_eventid IS NULL
    AND p.r_eventid IS NULL
ORDER BY
    p.eventid DESC
LIMIT 1001;
【非特権ユーザー(User / Admin)のクエリ】

ユーザーが対象の障害を閲覧する権限を持っているかを判定するため、複数のテーブルの結合や、サブクエリを用いたチェック処理が追加されます。

SELECT DISTINCT
    p.eventid,
    p.objectid,
    p.clock,
    p.ns,
    p.name,
    p.severity,
    p.cause_eventid
FROM
    problem p,
    functions f,
    items i,
    host_hgset hh,
    permission pp
WHERE
    p.source = 0
    AND p.object = 0
    AND p.objectid = f.triggerid
    AND f.itemid = i.itemid
    AND i.hostid = hh.hostid
    AND hh.hgsetid = pp.hgsetid
    AND pp.ugsetid = 2
    AND NOT EXISTS (
        SELECT NULL
        FROM
            functions f1
        JOIN items i1
            ON f1.itemid = i1.itemid
        JOIN host_hgset hh1
            ON i1.hostid = hh1.hostid
        LEFT JOIN permission pp1
            ON hh1.hgsetid = pp1.hgsetid 
            AND pp1.ugsetid = pp.ugsetid
        WHERE
            p.objectid = f1.triggerid
            AND pp1.permission IS NULL
    )
    AND p.objectid = 25100
    AND p.cause_eventid IS NULL
    AND p.r_eventid IS NULL
ORDER BY
    p.eventid DESC
LIMIT 1001;
【各項目の処理差異】
項目 Super Admin ( 特権管理者 ) User / Admin ( 非特権管理者 )
FROM 句 problem テーブル単体。権限を気にする必要がないため、障害データを直接取るだけです。 5 つのテーブルを結合。「どのトリガーの、どのアイテムの、どのホストグループか」を結合して辿る必要がある。
DISTINCT 句 不要。単一テーブルしか見ないため、1 つの障害が複数行に分裂することはない。 JOIN によって 1 つの障害がアイテム数分に増殖してしまうため、画面表示用に「1 行」にまとめる処理が必要。
AND NOT EXIST 句 存在しない。 存在し、総当たりチェックを行う。「紐づくアイテムの中に権限がないものが存在しないか」を判定する。

3.2 SQL クエリの実行計画(EXPLAIN)比較

未クローズイベント 33 万件、トリガー 10 アイテム構成の環境における検証結果です。
実行時間(actual time)を比較すると、特権ユーザーの約 2.8 秒に対し、非特権ユーザーは約 38.6 秒と、10 倍以上の遅延が発生していることがわかります。

【特権ユーザーの EXPLAIN 結果 (Zabbix 7.0.18)】
mysql>explain analyze SELECT DISTINCT p.eventid,p.objectid,p.clock,p.ns,p.name,p.severity,p.cause_eventid FROM problem p,functions f,items i WHERE p.source=0 AND p.object=0 AND p.objectid=25100 AND p.objectid=f.triggerid AND f.itemid=i.itemid AND i.hostid=10671 AND NOT EXISTS (SELECT NULL FROM event_suppress es WHERE es.eventid=p.eventid) AND p.cause_eventid IS  NULL AND (p.r_eventid IS NULL OR p.r_clock>1789693267) ORDER BY p.eventid DESC LIMIT 1001 \G
*************************** 1. row ***************************
EXPLAIN: ->Limit: 1001 row(s)  (actual time=2893..2893 rows=1001 loops=1)
【非特権ユーザーの EXPLAIN 結果 (Zabbix 7.0.18)】
mysql>explain analyze SELECT DISTINCT p.eventid,p.objectid,p.clock,p.ns,p.name,p.severity,p.cause_eventid FROM problem p,functions f,items i,host_hgset hh,permission pp WHERE p.source=0 AND p.object=0 AND p.objectid=f.triggerid AND f.itemid=i.itemid AND i.hostid=hh.hostid AND hh.hgsetid=pp.hgsetid AND pp.ugsetid=2 AND NOT EXISTS (SELECT NULL FROM functions f1 JOIN items i1 ON f1.itemid=i1.itemid JOIN host_hgset hh1 ON i1.hostid=hh1.hostid LEFT JOIN permission pp1 ON hh1.hgsetid=pp1.hgsetid AND pp1.ugsetid=pp.ugsetid WHERE p.objectid=f1.triggerid AND pp1.permission IS NULL) AND p.objectid=25100 AND NOT EXISTS (SELECT NULL FROM event_suppress es WHERE es.eventid=p.eventid) AND p.cause_eventid IS  NULL AND (p.r_eventid IS NULL OR p.r_clock>1789693048) ORDER BY p.eventid DESC LIMIT 1001 \G
*************************** 1. row ***************************
EXPLAIN: ->Limit: 1001 row(s)  (actual time=38653..38653 rows=1001 loops=1)

4. 対策

本事象への対策には、「運用面での回避」と「アップデートによる改善」の 2 つのアプローチがあります。
非特権ユーザーにおける権限チェック処理自体を排除することは設計上不可能ですが、ZBX-26419 の修正を適用することで、非効率なクエリによる遅延は改善可能です。その上で、日頃から未クローズ障害を溜め込まない運用を行うことが、システムの安定稼働の要となります。

4.1. 運用面での回避策

1. 未クローズ障害の適正化
  1. 一括クローズ:
    一定期間放置されている「対応不要だがクローズされていない障害」を、一括更新で手動クローズする等運用ルールを設けます。
  2. 自動クローズ :ログ監視や SNMP トラップ監視など、自然回復しないトリガーに対しては、グローバルイベント相関や nodata 関数の活用等により自動クローズを組み込み、イベントが蓄積しにくい設計に見直します。
フィルターの最適化
  1. 表示期間の絞り込み : 「過去 24 時間」「過去 7 日間」などに表示期間を限定します。
  2. 表示条件の絞り込み : 表示対象を特定のホストやトリガーに限定することで、クエリが評価する母数を物理的に削減します。

4.2. アップデートによる改善 (ZBX-26419)

Zabbix 7.0 のマイナーアップデート(ZBX-26419 の修正を含む Zabbix Server 7.0.19 以降)にて、この問題に対するクエリの最適化が実装されました。

※
参考情報 :ZBX-26419Improved performance of problem and event views by optimizing SQL queries to join only necessary triggers, eliminating redundant post-join filtering API

非特権ユーザーの権限チェックで実行される NOT EXISTS サブクエリの構造が見直され、外部クエリとの相関サブクエリを解消する修正が行われました。具体的には、以下の 3 つの変更によって処理の最適化が図られています

【修正前のコード抜粋 (ui/include/classes/api/services/CProblem.php)】
149         $sql_parts['where'][] = 'NOT EXISTS ('.
150           'SELECT NULL'.
151           ' FROM functions f1'.
152           ' JOIN items i1 ON f1.itemid=i1.itemid'.
153           ' JOIN host_hgset hh1 ON i1.hostid=hh1.hostid'.
154           ' LEFT JOIN permission pp1 ON hh1.hgsetid=pp1.hgsetid'.
155             ' AND pp1.ugsetid=pp.ugsetid'.
156           ' WHERE p.objectid=f1.triggerid'.
157             ' AND pp1.permission IS NULL'.
158         ')';
【修正後のコード抜粋 (ui/include/classes/api/services/CProblem.php)】
149         $sql_parts['where'][] = 'NOT EXISTS ('.
150           'SELECT NULL'.
151           ' FROM functions f1'.
152           ' JOIN items i1 ON f1.itemid=i1.itemid'.
153           ' JOIN host_hgset hh1 ON i1.hostid=hh1.hostid'.
154           ' LEFT JOIN permission pp1 ON hh1.hgsetid=pp1.hgsetid'.
155             ' AND pp1.ugsetid='.self::$userData['ugsetid'].
156           ' WHERE f.triggerid=f1.triggerid'.
157             ' AND i.itemid!=f1.itemid'.
158             ' AND pp1.hgsetid IS NULL'.
159         ')';
変更点 1: p.objectid から f.triggerid への変更

サブクエリ内での problem テーブル(p)への参照を取りやめ、より規模の小さい functions テーブル(f)の参照に差し替えています。修正前は、外側の problem テーブル(数万〜数十万行に及ぶケースがある)のレコード 1 件ごとにサブクエリが都度評価される状態でした。外側で既に p.objectid = f.triggerid の条件が成立していることを利用して参照先を変更し、不要なサブクエリの都度実行を解消しています。

変更点 2: pp.ugsetid' から '.self::$userData['ugsetid'] への変更

サブクエリ内から外側の permission テーブル(pp)を参照する外部依存を解消しています。ログインユーザーのグループ ID を PHP 側の変数から直接定数として SQL に組み込むことで、外側のテーブルとの結合を断ち切り、データベース側での処理効率を向上させています。

変更点 3: ' AND i.itemid!=f1.itemid' の追加

1 アイテムのみで構成されるトリガーの場合に、サブクエリ内の権限チェックをスキップするための条件です。単一アイテムのトリガーであれば、この条件(自分自身のアイテム ID とは異なる)を満たすデータが存在しないため、検索結果は即座に 0 件(NOT EXISTS が TRUE)となります。これにより、サブクエリ内でのテーブル結合(JOIN)処理自体が省略され、処理コストが軽減されます。

4.3 ZBX-26419 のパフォーマンス改善効果

アップデート前後での非特権ユーザーの EXPLAIN 結果を比較します。
(未クローズイベント 33 万件、トリガー 10 アイテム構成)

【Zabbix Server 7.0.18 ( 修正前 )】

mysql>explain analyze SELECT DISTINCT p.eventid,p.objectid,p.clock,p.ns,p.name,p.severity,p.cause_eventid FROM problem p,functions f,items i,host_hgset hh,permission pp WHERE p.source=0 AND p.object=0 AND p.objectid=f.triggerid AND f.itemid=i.itemid AND i.hostid=hh.hostid AND hh.hgsetid=pp.hgsetid AND pp.ugsetid=2 AND NOT EXISTS (SELECT NULL FROM functions f1 JOIN items i1 ON f1.itemid=i1.itemid JOIN host_hgset hh1 ON i1.hostid=hh1.hostid LEFT JOIN permission pp1 ON hh1.hgsetid=pp1.hgsetid AND pp1.ugsetid=pp.ugsetid WHERE p.objectid=f1.triggerid AND pp1.permission IS NULL) AND p.objectid=25100 AND NOT EXISTS (SELECT NULL FROM event_suppress es WHERE es.eventid=p.eventid) AND p.cause_eventid IS  NULL AND (p.r_eventid IS NULL OR p.r_clock>1789693048) ORDER BY p.eventid DESC LIMIT 1001 \G
*************************** 1. row ***************************
EXPLAIN: ->Limit: 1001 row(s)  (actual time=38653..38653 rows=1001 loops=1)

【Zabbix Server 7.0.19 ( 修正後 )】

mysql>explain analyze SELECT DISTINCT p.eventid,p.objectid,p.clock,p.ns,p.name,p.severity,p.cause_eventid FROM problem p,functions f,items i,host_hgset hh,permission pp WHERE p.source=0 AND p.object=0 AND p.objectid=f.triggerid AND f.itemid=i.itemid AND i.hostid=hh.hostid AND hh.hgsetid=pp.hgsetid AND pp.ugsetid=2 AND NOT EXISTS (SELECT NULL FROM functions f1 JOIN items i1 ON f1.itemid=i1.itemid JOIN host_hgset hh1 ON i1.hostid=hh1.hostid LEFT JOIN permission pp1 ON hh1.hgsetid=pp1.hgsetid AND pp1.ugsetid=2 WHERE f.triggerid=f1.triggerid AND i.itemid!=f1.itemid AND pp1.hgsetid IS NULL) AND p.objectid=25100 AND NOT EXISTS (SELECT NULL FROM event_suppress es WHERE es.eventid=p.eventid) AND p.cause_eventid IS  NULL AND (p.r_eventid IS NULL OR p.r_clock>1789696421) ORDER BY p.eventid DESC LIMIT 1001 \G
*************************** 1. row ***************************
EXPLAIN: ->Limit: 1001 row(s)  (actual time=2389..2389 rows=1001 loops=1)

5. まとめ

Zabbix 7.0 における非特権ユーザーの障害画面の遅延は、権限チェックに伴う SQL クエリの非効率性が引き金となっていました。しかし、この問題は Zabbix 7.0.19 以降のマイナーアップデート(ZBX-26419)で実装されたクエリの最適化により改善されています。
運用管理者におかれましては、該当バージョン以降へのアップデートを計画いただくとともに、ベストプラクティスとして日頃から未クローズ障害を溜め込まず、データベースへの負荷を最小限に抑える運用ルールを徹底することで、より快適で安定した運用を実現してください。

免責事項

本記事で紹介しているスクリプトや設定内容は、動作の仕組みを解説するためのサンプルです。実際の運用環境に適用する際は、ご自身の環境に合わせて十分にテストを行い、自己責任にてご活用ください。本記事の内容によって生じたいかなる損害についても、筆者は責任を負いかねます。

商標について

※ Zabbix は、ラトビア共和国およびその他の国における Zabbix LLC の登録商標です。
※ その他、本記事に記載されている会社名および製品名は、各社の商標または登録商標です。

本記事に関連するリンク