Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。現在本ブログは更新されておりません。新ブログ: https://medium.com/zeals-tech-blog

GKE環境のRedashでPostgreSQLサーバのディスクが溢れたので、Persistent Volumeをいじって何とかした

f:id:lead-zep0324teruhisa:20200202211533p:plain

こんにちは、分析基盤を担当している鍵本です。

本日は昨年の10月に GKE に移行した Redash の PostgreSQL サーバでディスクがいっぱいになってサービス停止してしまった 時のお話をします。

GKE に移行した話については2019年11月15日に公開したテックブログ Redash 分析環境のGKE移設&ver.3から7へのアップデート手順を公開 を御覧ください。

tech.zeals.co.jp

背景

ある朝出社すると事業部の人から「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 を定義する必要があります。

kubernetes.io

我々は 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で各テーブルの総サイズと平均サイズを知る で紹介されている以下のクエリを用いてテーブルの容量を確認してみます。

qiita.com

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開発を加速させるのに必要なデータ基盤の構築を手伝ってくれるエンジニアを募集しております。ご興味がある方は、ぜひ下記募集からご応募ください。お待ちしております。

hrmos.co