Kubernetes上のMySQL:データを消失させないために —— StatefulSetの実践的な経験

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Kubernetesでデータベースを「間違った方法」で運用した時の痛み

Kubernetesを使い始めたばかりの頃、私はすべてのものをDeploymentにパッケージングすればいいと思っていました。Webアプリやマイクロサービスに関しては、それは非常にスムーズでした。しかし、MySQLをDeploymentで立ち上げた時、最初の苦い経験をしました。Podが一度再起動しただけで、すべてのデータが綺麗に「消失」してしまったのです。さらに、再起動のたびにPodのIPアドレスが変わり、アプリケーションとの接続が完全に切れてしまいました。データベースは明らかに、通常のアプリと同じようには運用できません。

MySQLはステートフル(Stateful)なアプリケーションです。大量にコピー&ペーストできるステートレスなアプリとは異なり、MySQLの各インスタンスには一意の識別子と、生涯を共にするディスクが必要です。ここでStatefulSetの出番です。これは、Podに mysql-0mysql-1 といった固定の名前を保証します。最も重要なのは、Podが削除されたり別のノードに移動したりしても、以前のPersistent Volume(PV)を自動的に再マウントしてくれることです。

私が現在管理しているプロダクションシステムでは、50GBのデータを保持したMySQL 8.0が稼働しており、秒間約200〜300トランザクションを処理しています。StatefulSetとSSDストレージを組み合わせることで、システムは非常に安定して動作するようになりました。ディスクエラーの対応で夜更かしすることも、ほとんどなくなりました。

データの「家」をセットアップする(ConfigMapとSecret)

マニフェストを書く前に、土台をしっかりと準備する必要があります。設定がずさんだと、高負荷時にDBのパフォーマンスが急激に低下します。

1. ConfigMapによる設定の分離

my.cnf ファイルをイメージ内に固定してはいけません。私は常にこれらをConfigMapに入れ、再ビルドなしで innodb_buffer_pool_sizemax_connections を簡単に微調整できるようにしています。

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  my.cnf: |
    [mysqld]
    # 私の50GBデータベース向けの最適化
    innodb_buffer_pool_size = 2G
    innodb_log_file_size = 512M
    max_connections = 500
    innodb_flush_log_at_trx_commit = 1
    character-set-server = utf8mb4
    collation-server = utf8mb4_unicode_ci

2. パスワードのセキュリティ

YAMLファイルにパスワードを直接書き込むのはやめましょう。Secretを使用してシステムの安全性を保ちます

kubectl create secret generic mysql-pass --from-literal=password='MatKhauSieuCap123'

Headless ServiceとStatefulSetのデプロイ

Kubernetesクラスター内のアプリケーションがMySQLを見つけられるようにするには、Serviceが必要です。StatefulSetの場合、私は Headless Service (clusterIP: None) を選択します。これにより、アプリはロードバランサーを経由する代わりに、固定のDNSを通じて各PodのIPに直接接続できます。

Headless Serviceの作成

apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
    - port: 3306
      name: mysql
  clusterIP: None
  selector:
    app: mysql

標準的なStatefulSetマニフェスト

以下は、私が実際のプロジェクトでよく使用する構成です。最も価値があるのは volumeClaimTemplates です。これにより、クラウドプロバイダーに対して、各Pod専用のディスクを自動的に要求します。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: "mysql"
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: config
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: config
        configMap:
          name: mysql-config
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 50Gi

オンプレミスで実行している場合は、Kubernetesがどこからディスクを取得すべきかを知るために、**StorageClass**(LonghornやNFSなど)をインストールするのを忘れないでください。

耐久性の検証とモニタリング

kubectl apply コマンドを実行して終わりではありません。データが本当に嵐を乗り越えられるかを確認する必要があります。

1. 「Pod削除」チャレンジ

私はよく精神的なテストを行います。テスト用のデータベースを作成し、その後、思い切ってそのPodを直接削除してみるのです。

# Podにアクセス
kubectl exec -it mysql-0 -- mysql -u root -p

# サンプルデータの作成
CREATE DATABASE test_persistence;

# 耐久性を試すためにPodを「斬る(削除)」
kubectl delete pod mysql-0

Kubernetesが新しいPodを復活させるまで数秒待ちます。もし test_persistence データベースがそこに残っていれば、Persistent Volumeの設定は正確です。

2. 生存指標の監視

クラッシュしてから気づくのでは遅すぎます。MySQLの場合、私は常にメトリクスをGrafanaに送り、以下のパラメータを監視しています。

  • InnoDB Buffer Pool Usage: キャッシュのためにいつRAMを追加すべきかを知るため。
  • Disk I/O Latency: ネットワークストレージを使用する場合に非常に重要です。レイテンシが20msを超えると、問題が発生し始めています。
  • Slow Queries: 開発チームがDBをハングさせるようなSQLを書いた時に、彼らに進言(あるいは少し自慢)するため。

最後にアドバイスを。Kubernetesがいかに優れていても、クラスター外へのバックアップは最後の「命綱」です。毎晩S3にバックアップをアップロードするCronJobを設定しましょう。皆様のDevOpsエンジニアとしてのMySQL運用が、安眠できるものでありますように!

Share: