BLOG
2022 年 05 月 11 日
Zabbix 6.0 の新機能:Zabbix サーバーの HA クラスター(機能解説編)
概要
前回の記事では Zabbix 6.0 で追加された Zabbix サーバーの HA クラスター機能について使い方などを簡単に紹介しました。本記事では、Zabbix サーバーの HA クラスター機能の実装について解説します。
機能解説
HA クラスター機能のため新しく HA manager プロセスが追加されています。まずはその HA manager プロセスについて説明してからノードの死活監視やハートビート、フェールオーバーの処理について順に見ていきます。
なお、本記事では MIRACLE ZBX 6.0.2-2 時点でのソースコードをもとに実装を解説しています。
HA manager プロセス
HA クラスター機能の追加にともない、Zabbix サーバーに HA manager プロセスが追加されました。HA manager プロセスは Zabbix サーバーのサービス起動時にメインプロセスに続いて起動する最初の子プロセスとなっています。
Zabbix サーバーの起動処理を見てみましょう。ソースコードは src/zabbix_server/server.c の MAIN_ZABBIX_ENTRY 関数です。
int MAIN_ZABBIX_ENTRY(int flags) { ... if (SUCCEED != zbx_ha_start(&rtc, ZBX_NODE_STATUS_UNKNOWN, &error)) // (1) HA manager プロセスの起動 { zabbix_log(LOG_LEVEL_CRIT, "cannot start HA manager: %s", error); zbx_free(error); exit(EXIT_FAILURE); } ... if (SUCCEED != zbx_ha_get_status(&ha_status, &ha_failover_delay, &error)) // (2) HA クラスターの状態取得 { zabbix_log(LOG_LEVEL_CRIT, "cannot start server: %s", error); zbx_free(error); sig_exiting = ZBX_EXIT_FAILURE; } if (ZBX_NODE_STATUS_ACTIVE == ha_status) { if (SUCCEED != server_startup(&listen_sock, &ha_status, &ha_failover_delay, &rtc)) // (3) アクティブノードの場合は他のプロセスを起動 { sig_exiting = ZBX_EXIT_FAILURE; ha_status = ZBX_NODE_STATUS_ERROR; } else { /* check if the HA status has not been changed during startup process */ if (ZBX_NODE_STATUS_ACTIVE != ha_status) server_teardown(&rtc, &listen_sock); } } ... }
サービスのメインループに入る前の初期化処理の部分です。まず (1) で zbx_ha_start 関数を実行して HA manager プロセスを起動します。(2) で HA クラスターの状態を取得して、アクティブノードの場合は (3) で server_startup 関数を実行して Zabbix サーバーの他のプロセスを起動するという流れになっています。
スタンバイノードでは (3) の server_startup 関数は実行されずにメインプロセスと HA manager プロセスの 2 つだけが起動している状態になります。
自身がアクティブノードなのかスタンバイノードなのかの判断は、HA manager プロセスの初期化処理の中で実行される ha_check_cluster_config 関数で実装されています。
static int ha_check_cluster_config(zbx_ha_info_t *info, zbx_vector_ha_node_t *nodes, int db_time, int *activate) { ... *activate = SUCCEED; // デフォルトはアクティブモード for (i = 0; i < nodes->values_num; i++) // DB から取得した全ノードを走査 { if (ZBX_NODE_STATUS_STOPPED == nodes->values[i]->status || SUCCEED != ha_is_available(info, nodes->values[i]->lastaccess, db_time)) // ノードが非稼働ならスキップ { continue; } ... if (ZBX_NODE_STATUS_ACTIVE == nodes->values[i]->status || ZBX_NODE_STATUS_STANDBY == nodes->values[i]->status) // HA モード判定 { *activate = FAIL; // 自身をスタンバイモードに設定 } } ... }
DB から取得したクラスターの全ノードについて走査し、すでにアクティブモードまたはスタンバイモードで稼働中のノードがあれば自身はスタンバイモードで稼働し、そうでないなら自身はアクティブモードで稼働するという処理になっているようです。
ここまで Zabbix サーバー起動時の初期化処理について見てきました。続いて、HA manager プロセスの起動後の処理を見てみます。ソースコードは src/zabbix_server/ha/ha_manager.c の ha_manager_thread 関数です。
ZBX_THREAD_ENTRY(ha_manager_thread, args) { ... while (SUCCEED != pause && ZBX_NODE_STATUS_ERROR != info.ha_status) // HA manager プロセスのメインループ { if (tick <= (now = zbx_time())) { ticks_num++; if (nextcheck <= ticks_num) { int old_status = info.ha_status, delay; if (ZBX_NODE_STATUS_UNKNOWN == info.ha_status) ha_db_register_node(&info); else ha_check_nodes(&info); // (1) ノードの死活監視 if (old_status != info.ha_status && ZBX_NODE_STATUS_UNKNOWN != info.ha_status) ha_update_parent(&rtc_socket, &info); // (2) 状態更新の通知 ... delay = ZBX_DB_OK <= info.db_status ? ZBX_HA_POLL_PERIOD : 1; while (nextcheck <= ticks_num) nextcheck += delay; } if (ZBX_DB_OK <= info.db_status) ha_send_heartbeat(&rtc_socket); // (3) ハートビート送信 while (tick <= now) tick++; } ... } ... }
HA manager プロセスでは、(1) ノードの死活監視、(2) ノードの状態変更の通知、(3) ハートビートメッセージの送信の処理をしています。
以下の節ではそれぞれの内容について見ていきます。
ノードの死活監視
ノードの死活監視処理について、ha_check_nodes 関数を見ていきます。
static void ha_check_nodes(zbx_ha_info_t *info) { ... if (ZBX_HA_IS_CLUSTER()) { if (ZBX_NODE_STATUS_ACTIVE == info->ha_status) { if (SUCCEED != ha_check_standby_nodes(info, &nodes, db_time)) // (1) アクティブノードからスタンバイノードの死活監視 goto out; } else { if (SUCCEED != ha_check_active_node(info, &nodes, &unavailable_index, &ha_status)) // (2) スタンバイノードからアクティブノードの死活監視 goto out; } } zbx_strcpy_alloc(&sql, &sql_alloc, &sql_offset, "update ha_node set lastaccess=" ZBX_DB_TIMESTAMP()); // (3) DB アクセス時刻の更新 ... if (ha_status != node->status) { zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, ",status=%d", ha_status); // ステータス更新 ... } zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " where ha_nodeid='%s'", info->ha_nodeid.str); if (SUCCEED == ha_db_execute(info, "%s", sql) && FAIL != unavailable_index) { zbx_ha_node_t *last_active = nodes.values[unavailable_index]; ha_db_execute(info, "update ha_node set status=%d where ha_nodeid='%s'", ZBX_NODE_STATUS_UNAVAILABLE, last_active->ha_nodeid.str); // 非稼働になったアクティブノードの無効化 ... } ... }
自身がアクティブノードの場合は (1) でスタンバイノードの死活監視を実施し、自身がスタンバイノードの場合は (2) でアクティブノードの死活監視を実施します。それぞれの内容は後で詳しく見ていきます。
その後、(3) で DB アクセス時刻を更新しています。DB アクセス時刻は DB の ha_node テーブルの lastaccess カラムに保存されており、この値を使って各ノードの稼働状態を判断しています。
その他にはスタンバイノードからアクティブノードに昇格するさいのステータスの更新や非稼働になったアクティブノードの無効化などの処理が実行されています。
それでは、アクティブノードからスタンバイノードの死活監視をしている部分を確認します。ha_check_standby_nodes 関数です。
static int ha_check_standby_nodes(zbx_ha_info_t *info, zbx_vector_ha_node_t *nodes, int db_time) { ... for (i = 0; i < nodes->values_num; i++) { if (nodes->values[i]->status != ZBX_NODE_STATUS_STANDBY) continue; if (db_time >= nodes->values[i]->lastaccess + info->failover_delay) // (1) スタンバイノードの稼働状態の判定 { zbx_vector_str_append(&unavailable_nodes, nodes->values[i]->ha_nodeid.str); ... } } if (0 != unavailable_nodes.values_num) // 非稼働になったスタンバイノードの無効化 { char *sql = NULL; size_t sql_alloc = 0, sql_offset = 0; zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "update ha_node set status=%d where", ZBX_NODE_STATUS_UNAVAILABLE); DBadd_str_condition_alloc(&sql, &sql_alloc, &sql_offset, "ha_nodeid", (const char **)unavailable_nodes.values, unavailable_nodes.values_num); if (SUCCEED != ha_db_execute(info, "%s", sql)) ret = FAIL; zbx_free(sql); } ... }
ポイントになるのは (1) のスタンバイノードの稼働状態判定の部分です。DB アクセス時刻と現在時刻を比べて差が failover_delay 時間よりも大きければそのノードを非稼働とみなします。failover_delay は Web フロントエンドでは「フェールオーバーの遅延」として表示されている値でデフォルト値は 1 分です。つまり、DB にアクセスできない時間が 1 分を超えるとそのノードは非稼働として扱われることになります。
また、関数の後半では非稼働になったスタンバイノードの無効化の処理が実施されています。
続いて、スタンバイノードからアクティブノードの死活監視の部分を確認しましょう。実装は ha_check_active_node 関数です。
static int ha_check_active_node(zbx_ha_info_t *info, zbx_vector_ha_node_t *nodes, int *unavailable_index, int *ha_status) { ... for (i = 0; i < nodes->values_num; i++) // 全ノードの中からアクティブノードを検索 { if (ZBX_NODE_STATUS_ACTIVE == nodes->values[i]->status) { ... break; } } if (i == nodes->values_num || SUCCEED == zbx_cuid_compare(nodes->values[i]->ha_nodeid, info->ha_nodeid)) // (1) アクティブノードがないときに自身をアクティブノードに昇格 { *ha_status = ZBX_NODE_STATUS_ACTIVE; } else { if (nodes->values[i]->lastaccess != info->lastaccess_active) { info->lastaccess_active = nodes->values[i]->lastaccess; info->offline_ticks_active = 0; } else info->offline_ticks_active++; if (info->failover_delay / ZBX_HA_POLL_PERIOD < info->offline_ticks_active) // (2) アクティブノードの稼働状態の判定 { *unavailable_index = i; *ha_status = ZBX_NODE_STATUS_ACTIVE; } } ... }
スタンバイノードからの死活監視では自身をアクティブノードに昇格する場合が 2 パターンあります。1 つはアクティブノードが存在していない場合で、もう 1 つがアクティブノードが非稼働になっていた場合です。
2 つ目のパターンのアクティブノードの稼働状態の判定部分を確認しましょう。この関数の呼び出しが ZBX_HA_POLL_PERIOD(5 秒と定義されている)ごとに実行されるため、大まかにはアクティブノードの DB アクセス時刻の更新が止まってから failover_delay だけ時間が過ぎると非稼働とみなされるようです。計算式は変わっていますが、スタンバイノードの稼働状態の判定とほぼ同じ条件と言えるでしょう。
ノードの死活監視の処理について見てきました。アクティブノードからスタンバイノードの死活監視もその逆の死活監視もどちらも DB アクセス時刻を使って監視をしており、DB アクセス時刻の更新が止まって failover_delay の時間を超えているノードが非稼働と判定されていることがわかりました。failover_delay はデフォルトでは 1 分に設定されています。
アクティブノードのハートビート
この節ではアクティブノードのハートビート処理について見ていきます。HA manager プロセスのメインループの中でハートビートメッセージを送信しているところがありました。不要な部分を除いてソースコードを再掲すると以下のようになっています。
ZBX_THREAD_ENTRY(ha_manager_thread, args) { ... while (SUCCEED != pause && ZBX_NODE_STATUS_ERROR != info.ha_status) { if (tick <= (now = zbx_time())) { ... if (ZBX_DB_OK <= info.db_status) ha_send_heartbeat(&rtc_socket); // ハートビートメッセージの送信 while (tick <= now) tick++; } ... } ... }
DB アクセスが正常である場合にハートビートメッセージを送信するようになっています。このハートビートメッセージは zbx_ha_dispatch_message 関数で処理されています。
int zbx_ha_dispatch_message(zbx_ipc_message_t *message, int *ha_status, int *ha_failover_delay, char **error) { ... if (NULL != message) { switch (message->code) { ... case ZBX_IPC_SERVICE_HA_HEARTBEAT: // ハートビートメッセージの処理 last_hb = now; break; } } if (ZBX_HA_IS_CLUSTER() && *ha_status == ZBX_NODE_STATUS_ACTIVE && 0 != last_hb) { if (last_hb + *ha_failover_delay - ZBX_HA_POLL_PERIOD <= now || now < last_hb) // アクティブノード自身の死活監視 *ha_status = ZBX_NODE_STATUS_STANDBY; // 自身をスタンバイモードに降格 } ... }
まずはハートビートメッセージの処理についてです。内容は簡単で、変数 last_hb に現在時刻を設定しているだけです。
この zbx_ha_dispatch_message 関数はハートビートの確認のためメインプロセスから定期的に実行されており、そこでアクティブノード自身の死活監視をしています。最後にハートビートを受け取った時刻から (failover_delay - 5) 秒以上経過している場合に自身をスタンバイノードに降格しています。failover_delay のデフォルト値は 1 分なので、アクティブノードは DB アクセスできない状態が 55 秒以上続くと自身をスタンバイノードに降格するようになっているようです。
フェールオーバー
最後にフェールオーバーの実装を見ていきます。HA manager プロセスのメインループのコードを再掲します。
ZBX_THREAD_ENTRY(ha_manager_thread, args) { ... while (SUCCEED != pause && ZBX_NODE_STATUS_ERROR != info.ha_status) // HA manager プロセスのメインループ { if (tick <= (now = zbx_time())) { ticks_num++; if (nextcheck <= ticks_num) { int old_status = info.ha_status, delay; if (ZBX_NODE_STATUS_UNKNOWN == info.ha_status) ha_db_register_node(&info); else ha_check_nodes(&info); // ノードの死活監視 if (old_status != info.ha_status && ZBX_NODE_STATUS_UNKNOWN != info.ha_status) ha_update_parent(&rtc_socket, &info); // 状態更新を通知 ... } ... } ... } ... }
ノードの死活監視の結果プロセスの状態が更新された場合、具体的にはスタンバイノードがアクティブノードの停止を検知して自身がアクティブノードに昇格した場合などにメインプロセスに状態更新を通知します。ha_update_parent 関数の詳細は割愛しますが、ランタイムコントロールを使ってメインプロセスに通知しています。
メインプロセスがこのメッセージを受信した後の処理を見てみましょう。
int MAIN_ZABBIX_ENTRY(int flags) { ... while (ZBX_IS_RUNNING()) // メインプロセスのメインループ { ... if (ZBX_NODE_STATUS_UNKNOWN != ha_status && ha_status != ha_status_old) // ノードの状態変化時の処理 { ha_status_old = ha_status; zabbix_log(LOG_LEVEL_INFORMATION, "\"%s\" node switched to \"%s\" mode", ZBX_NULL2EMPTY_STR(CONFIG_HA_NODE_NAME), zbx_ha_status_str(ha_status)); switch (ha_status) { case ZBX_NODE_STATUS_ACTIVE: if (SUCCEED != server_startup(&listen_sock, &ha_status, &ha_failover_delay, &rtc)) // アクティブノード昇格時のサーバー起動 { sig_exiting = ZBX_EXIT_FAILURE; ha_status = ZBX_NODE_STATUS_ERROR; continue; } if (ZBX_NODE_STATUS_ACTIVE != ha_status) { server_teardown(&rtc, &listen_sock); ha_status_old = ha_status; } break; case ZBX_NODE_STATUS_STANDBY: server_teardown(&rtc, &listen_sock); // スタンバイノード降格時のサーバー停止 standby_warning_time = now; break; ... } } ... } ... }
アクティブノードに昇格したときは server_startup 関数を実行して監視に必要な他のプロセスを起動しています。また、スタンバイノードに降格したときは server_teardown 関数を実行してメインプロセスと HA manager プロセス以外のすべてのプロセスを終了します。
まとめ
Zabbix サーバーの HA クラスター機能について、ソースコードを紹介しながら実装について簡単に説明しました。
新しく追加された HA manager プロセスについて簡単に紹介し、そのプロセスで処理されているノードの死活監視、ハートビート、フェールオーバーの処理について解説しました。