Kubernetesでデータベースを「間違った方法」で運用した時の痛み
Kubernetesを使い始めたばかりの頃、私はすべてのものをDeploymentにパッケージングすればいいと思っていました。Webアプリやマイクロサービスに関しては、それは非常にスムーズでした。しかし、MySQLをDeploymentで立ち上げた時、最初の苦い経験をしました。Podが一度再起動しただけで、すべてのデータが綺麗に「消失」してしまったのです。さらに、再起動のたびにPodのIPアドレスが変わり、アプリケーションとの接続が完全に切れてしまいました。データベースは明らかに、通常のアプリと同じようには運用できません。
MySQLはステートフル(Stateful)なアプリケーションです。大量にコピー&ペーストできるステートレスなアプリとは異なり、MySQLの各インスタンスには一意の識別子と、生涯を共にするディスクが必要です。ここでStatefulSetの出番です。これは、Podに mysql-0、mysql-1 といった固定の名前を保証します。最も重要なのは、Podが削除されたり別のノードに移動したりしても、以前のPersistent Volume(PV)を自動的に再マウントしてくれることです。
私が現在管理しているプロダクションシステムでは、50GBのデータを保持したMySQL 8.0が稼働しており、秒間約200〜300トランザクションを処理しています。StatefulSetとSSDストレージを組み合わせることで、システムは非常に安定して動作するようになりました。ディスクエラーの対応で夜更かしすることも、ほとんどなくなりました。
データの「家」をセットアップする(ConfigMapとSecret)
マニフェストを書く前に、土台をしっかりと準備する必要があります。設定がずさんだと、高負荷時にDBのパフォーマンスが急激に低下します。
1. ConfigMapによる設定の分離
my.cnf ファイルをイメージ内に固定してはいけません。私は常にこれらをConfigMapに入れ、再ビルドなしで innodb_buffer_pool_size や max_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運用が、安眠できるものでありますように!

