この記事は2023年後半の設計・構築プロジェクトをもとにしています。当時のバージョンは Elastic Stack 8.11系。設定項目や推奨値はその後変わっている部分があるので、最新の公式ドキュメントと併せて読んでください。
業務アプリ群のログを一元管理するため、Elastic Stack(Beats → Logstash → Elasticsearch → Kibana)のログ収集基盤をオンプレ中心の環境に設計・構築した。マネージドのElastic Cloudに乗れない事情がある環境で、サイジングも可用性も自前で決める必要があった。
この記事では「なんとなくデフォルト」や「なんとなく多め」で決めずに、公式ドキュメントの経験則に実測値を当てはめて設定値を導いた過程と、地味にハマった可用性まわりの設計を残す。
全体構成
- 2拠点2クラスタ (地理的に離れた拠点AとBにそれぞれ独立したElasticsearchクラスタを構築。拠点をまたぐ検索はクラスタ間検索(CCS: Cross-Cluster Search)で実現)
- Kibanaは各クラスタ1台 (検索UIはサービス提供上クリティカルではないと割り切ってシングル構成。片拠点のKibanaは緊急用とし、平常時は停止)
- 構成管理はAnsible (環境(prod/dev)×拠点×ノード種別の組み合わせをインベントリ変数で吸収し、設定ファイルはJinja2テンプレートで分岐)
「何を冗長化して何を割り切るか」を判断した。ログの受け口(Logstash)と保管(Elasticsearch)はSPOFを排除し、UI(Kibana)は落ちても業務が止まらないのでシングル。全部冗長化するのではなく、コンポーネントごとにクリティカル度を評価して濃淡をつける。
ノードロールは「削る」意識で
Elasticsearchのノードはロール設定で役割を分けた。
# マスター(適格)ノード
node.roles: [ master ]
# Hotデータノード
node.roles: [ data_hot, data_content, remote_cluster_client ]
# Warmデータノード
node.roles: [ data_warm ]
ポイントはロールを「付与する」のではなく「削る」意識で設計したこと。マスター適格ノードからdataロールを削ってクラスタ制御に専念させ、データノードからmasterロールを削ってデータ操作に専念させる。マスター適格ノードは奇数の3台構成で、1台障害時は残り2台から再選出、スプリットブレイン時は過半数側でクラスタ機能を維持する。
データノードはhot/warmの2ティア構成にして、ILM(インデックスライフサイクル管理)でこう流す。
- Hotフェーズ (インデックス作成から30日。書き込みと直近の検索を担当)
- Warmフェーズ (30日〜90日。参照頻度の下がったインデックスを移動)
- 削除 (90日経過後)
インデックスは「ログ出力元サーバ種別 × ログ種別」の組み合わせで日次に切っているので、ロールオーバーは使っていない。
サイジングは公式の経験則に実測値を当てる
Elasticsearchのサイジングは公式ドキュメントに経験則の数字が結構書いてあるので、自環境の実測値を当てはめると根拠つきで決められる。
マスターノードのヒープは、公式ドキュメントに「ヒープ1GBあたりインデックス3000未満」という経験則がある。実測のインデックス数は約450なので1GBで十分。結論としてヒープは明示設定せず、ロールと搭載メモリから決まる自動設定に任せた。拡張余地としては、手動設定の推奨上限である物理メモリの50%(このノードでは8GB)までヒープを引き上げると仮定した場合、24,000インデックスまで耐えられる試算になる。
データノードのヒープは「1インデックス・1フィールドあたり1KB」のオーバーヘッド見積もりを使う。実測(1ノードあたり約450インデックス)に増加率ざっくり2倍をかけて1000インデックス、代表的なログ(IISアクセスログ)のフィールド数から1インデックス100フィールドとして、
1000インデックス × 100フィールド × 1KB = 0.1GB
つまりヒープはごく小さくて足りる見込みが立つ。これもデフォルトの自動設定で初期構築した。
シャード数は経験則が「1シャードあたりドキュメント2億未満、サイズ10〜50GB」(参考: 公式ブログ)。実測は1インデックス最大約3,800万ドキュメント・最大50GB程度なので、全インデックスでプライマリシャード1+レプリカ1に決定。サイズが上限に近いインデックス種別が出てきたら、そこだけシャード分割を検討する方針にした。
シャードは細かすぎても大きすぎても効率が落ちるので、とりあえず多めに割っておくような決め方より、実測を経験則に当てはめて決めるほうが後で困らない。
Logstashの永続化キューは「3連休を耐える」を目標に
ログ収集パイプラインの設計方針は「ログ欠損の防止を最優先」。Logstashは永続化キュー(Persistent Queue)を使い、キューサイズは障害シナリオから逆算した。
想定シナリオは「休日にログ転送先の不具合が起きて、休み明けまで誰も対応できない」。土日+祝日の3連休、前日18:30から休日明け9:30までの87時間をキューで耐えることを目標にした。
1日の全体ログ量 約90GB ÷ 24 ÷ 3600 ≒ 秒間 0.00104GB
→ 1時間 約3.75GB
→ Logstash 27台で分散して 1台あたり 約0.139GB/時
→ 87時間分 = 約12.1GB
→ 安全係数1.1をかけて 約13.3GB
これにディスクサイズ(32GB)の半分までという拡張余力の目処を合わせて、queue.max_bytes: 16gb に決定した。
queue.type: persisted
queue.page_capacity: 64mb # デフォルト値。公式が「多くの場合デフォルトで良い」と言うので従う
queue.max_bytes: 16gb
queue.checkpoint.writes: 1
queue.checkpoint.writes: 1 は1イベント書き込みごとにチェックポイントを強制する設定で、プロセスがクラッシュしてもキュー内の欠損を最小化できる。代わりに書き込み性能とのトレードオフになるため、動作検証で支障が出たら緩める前提で入れた(現行運用と同等の設定だったため、致命的な影響は出ない見込みで採用)。
パースに失敗したイベントを取りこぼさないよう、DLQ(デッドレターキュー)も有効化している。
dead_letter_queue.enable: true
dead_letter_queue.max_bytes: 2048mb
syslogの受け口だけはkeepalivedでVIPにする
Logstashの冗長化で印象深かった点。送信元によって冗長化の方法を変える必要があった。
- Beats系 (送信側の設定に複数のLogstashを列挙でき、障害時は送信側が別のLogstashに切り替えてくれる(設定によってはロードバランスも可能)。受け側はLogstashを並べるだけでよい)
- syslog (送信先は基本1つのIP固定で、送信側での切り替えは基本期待できない。受け側でVIP(仮想IP)を持つ必要がある)
そこでsyslog受信用のLogstash群にはkeepalived(VRRP)でVIPを持たせた。ヘルスチェックはこの2段構え。
vrrp_script chk_logstash {
script "systemctl is-active logstash"
interval 3
fall 3
rise 2
}
vrrp_script chk_logstash_port {
script "< /dev/tcp/<自ホストIP>/10514"
interval 3
fall 3
rise 2
}
2つ目はbashの /dev/tcp 疑似デバイスでTCPポートの疎通を見る小技で、外部コマンド不要でポートチェックができる。ここでのハマりどころはUDP。/dev/udp も存在するが、UDPはコネクションレスなので戻り値が必ず0になり、ヘルスチェックとして機能しない。UDPの待ち受け確認をやるなら ss -anpu | grep 10514 のような自前スクリプトになる。今回はプロセス死活(systemctl)とTCP疎通の組み合わせで実用上十分と判断した。
VRRPの virtual_router_id と認証情報は環境×拠点ごとに変えて、同一セグメントでのVRRPインスタンス衝突を防止。マスター/バックアップの優先度はホスト名の連番から機械的に決まるようAnsibleテンプレートで生成していて、「01番機がデフォルトでマスター」という運用上の分かりやすさも確保した。
なお、Beats通信はTLS化しているが、Logstashのsyslog inputプラグインはTLS非対応のため、syslogだけは平文を許容した。
まとめ
- サイジングは実測値×公式の経験則 (「ヒープ1GBあたりインデックス3000」「1シャード50GBまで」など、公式が出している数字に自環境の実測を当てはめれば、根拠を説明できる設定値になる)
- 障害シナリオから逆算する (キューサイズ「16GB」は「3連休87時間を耐える」という具体的なシナリオの産物。数字の由来が残っていれば、前提が変わったときに再計算できる)
- 冗長化は濃淡をつける (全部冗長化ではなく、コンポーネントごとにクリティカル度を評価。Kibanaはシングル、syslogの受け口はVIP、と対象ごとに手段を変える)
- 送信プロトコルが可用性設計を決める (Beatsは送信側で冗長化、syslogは受け側でVIP。「何で送られてくるか」から逆算しないと受け口の設計はできない)