こんにちは、分析基盤を担当している鍵本です。
本日は昨年の10月に GKE に移行した Redash の PostgreSQL サーバでディスクがいっぱいになってサービス停止してしまった 時のお話をします。
GKE に移行した話については2019年11月15日に公開したテックブログ Redash 分析環境のGKE移設&ver.3から7へのアップデート手順を公開 を御覧ください。
背景
ある朝出社すると事業部の人から「Redash にアクセスすると 500 Internal Server Error となります」という報告がありました。Kubernetes上のRedashサーバーのPodが再起動したからだと思い「数分待ってから再度アクセスしてみてください」と答えました。しかし数分後も同じ状況とのことでした。調べてみると PostgreSQL サーバの Pod は起動しておらず、ログを見たところ No space left on device とのことでした。
Persistent Volume の容量追加
PostgreSQL 用のデータディスクは永続化をしなければいけないので、Kubernetes のリソースである Persistent Volume を使用しています。今回はファイルシステムの使用率が 100% になってしまったので、この領域を広げることにします。
StorageClass の修正
Persistent Volume をプロビジョニングするためには StorageClass を定義する必要があります。
我々は GKE のデフォルトとして用意されている Standard StorageClass を PostgreSQL 用として使用しました。これは動的な容量拡張を許可していません。そこで容量拡張許可の設定を最初に行います。
kubectl edit sc standard
以下を追記して保存します。
allowVolumeExpansion: true
本来であれば専用の StorageClass を作成するべきなのですが、容量拡張の設定だけならば特段問題にならないだろうと思ってこの方法を採用します。
なおボリュームの容量拡張の可否はお使いのボリュームタイプによって異なります。詳細については公式ドキュメント Storage Classes の Allow Volume Expansion の項 を御覧ください。
PersistentVolumeClaim のマニフェスト修正
割り当てるディスク容量は PersistentVolumeClaim のマニフェストに記載されております。以下のように、spec.resources.requests.storage の値を 2Gi から 10Gi に変更します。
vi redash/base/persistentvolumeclaim/postgres-persistentvolumeclaim.yaml
apiVersion: v1 kind: PersistentVolumeClaim metadata: labels: io.kompose.service: postgres name: postgres spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi
マニフェストの適用
あとはマニフェストを適用するだけで Persistent Volume の容量が増加します。とても簡単ですね!
kustomize build redash/overlays/prd | kubectl apply -f -
Redash 用の PostgreSQL のディスク使用量で気をつけること
クエリ結果を保存するテーブルに TOAST が使用されてること
Redash はクエリ結果を PostgreSQL に一定期間保存しています(既定値は 7 日間)。保存先は query_results
テーブルですが、クエリ結果は一般的に大きなデータになるため、直接該当フィールドに格納することができません。そこで TOAST(過大属性格納技法:The Oversized-Attribute Storage Technique) を用いて、大規模フィールドの値を格納します。この制限の詳細については公式ドキュメントの 59.2 TOAST を御覧ください。
まずは現状把握ということで @awakia さんの PostgreSQLで各テーブルの総サイズと平均サイズを知る で紹介されている以下のクエリを用いてテーブルの容量を確認してみます。
SELECT pgn.nspname, relname, pg_size_pretty(relpages::bigint * 8 * 1024) AS size, CASE WHEN relkind = 't' THEN ( SELECT pgd.relname FROM pg_class pgd WHERE pgd.reltoastrelid = pg.oid) WHEN nspname = 'pg_toast' AND relkind = 'i' THEN ( SELECT pgt.relname FROM pg_class pgt WHERE SUBSTRING(pgt.relname FROM 10) = REPLACE(SUBSTRING(pg.relname FROM 10), '_index', '')) ELSE ( SELECT pgc.relname FROM pg_class pgc WHERE pg.reltoastrelid = pgc.oid) END::varchar AS refrelname, CASE WHEN nspname = 'pg_toast' AND relkind = 'i' THEN ( SELECT pgts.relname FROM pg_class pgts WHERE pgts.reltoastrelid = ( SELECT pgt.oid FROM pg_class pgt WHERE SUBSTRING(pgt.relname FROM 10) = REPLACE(SUBSTRING(pg.relname FROM 10), '_index', ''))) END AS relidxrefrelname, relfilenode, relkind, reltuples::bigint, relpages FROM pg_class pg, pg_namespace pgn WHERE pg.relnamespace = pgn.oid AND pgn.nspname NOT IN ('information_schema', 'pg_catalog') ORDER BY relpages DESC;
使用しているページ数の多い順に10件だけ抽出した結果が以下の通りです。
nspname | relname | size | refrelname | relidxrefrelname | relfilenode | relkind | reltuples | relpages ----------+---------------------------------------------+------------+---------------------------+---------------------------+-------------+---------+-----------+---------- pg_toast | pg_toast_16491 | 4062 MB | query_results | | 16494 | t | 658109 | 519947 public | events | 215 MB | pg_toast_16446 | | 16446 | r | 545862 | 27550 pg_toast | pg_toast_16491_index | 68 MB | pg_toast_16491 | query_results | 16496 | i | 85737 | 8706 public | events_pkey | 12 MB | | | 21067 | i | 545862 | 1500 public | query_results | 7568 kB | pg_toast_16491 | | 16491 | r | 2207 | 946 pg_toast | pg_toast_16483 | 1880 kB | queries | | 16486 | t | 1008 | 235 public | queries | 1848 kB | pg_toast_16483 | | 16483 | r | 1131 | 231 public | ix_queries_search_vector | 1384 kB | | | 21106 | i | 1131 | 173 public | changes | 1160 kB | pg_toast_16417 | | 16417 | r | 1138 | 145 public | ix_query_results_query_hash | 616 kB | | | 21107 | i | 2207 | 77
pg_toast_16491 テーブルは query_results の結果を分割して格納している TOAST テーブルです。容量が 4GiB と最も多いことがわかります。Google で postgresql toast grow を検索するとわかるように TOAST テーブルが数十 GiB 以上に増大してディスクを圧迫している という事例が多くあります。つまり PostgreSQL に大規模データをフィールドに格納する必要がある場合には TOAST テーブルのサイズに注意を払う必要があります。
クエリ結果の保存期間
先述の通り、Redash ではクエリ結果を 7 日間保存しています。もし 7 日間保存する必要があるのであれば、その分のディスク容量を確保しておく必要があります。我々は以下のような環境変数を設定して 3 日 に変更しました。
REDASH_QUERY_RESULTS_CLEANUP_MAX_AGE=3
上記の pg_toast_16491 の容量は 3 日保存に設定して落ち着いた結果になります。
まとめ
今回は Redash を GKE 環境に移行して初めて遭遇した問題、つまり PostgreSQL サーバのディスクが溢れた話をご紹介しました。移行前にきちんと必要なリソースを見積もり、かつ運用中にしっかり監視しておけば事前に対応できたのではないかと反省しております。
最後に
Zeals ではAI開発を加速させるのに必要なデータ基盤の構築を手伝ってくれるエンジニアを募集しております。ご興味がある方は、ぜひ下記募集からご応募ください。お待ちしております。