記事のサマリー(TL;DR)
- サイボウズが20年超稼働の C++ 製 CGI アプリを新インフラ「Neco(Kubernetes)」へ移行中
- fcntl(2) ロックを Pod 間で機能させるため、Pod Affinity + ReadWriteOnce PV の組み合わせでダウンタイムゼロのローリングアップデートを実現
- 数億ファイル規模のデータ移行は「事前転送(rsync)+最終転送(mtime 差分)」の2段階で約300万ファイルを15分で転送完了
国内 kintone・Garoon・SaaS 事業者が注目すべきレガシー移行設計の論点
サイボウズの事例は、SQLite やローカルファイルを前提としたステートフル CGI アーキテクチャを Kubernetes へ移行する際の設計判断を詳細に開示した、国内では珍しい実践報告です。同様の課題は、EC-CUBE など「ファイルシステムを直接操作する PHP/CGI 系アプリ」を Kubernetes やコンテナ環境へ移行しようとしている事業者にも共通します。
特に注目すべき点は3つです。①ファイルロックの排他制御が「同一カーネルのプロセス間」でしか機能しないという Linux の制約を、Pod Affinity で設計上吸収していること、②mtime ベースの差分転送の前提を bpftrace で本番相当環境から実証的に検証したこと、③コマンドライン操作を gRPC サイドカーに置き換えることで運用操作の API 化を達成したこと。これらは「動いているアプリをそのまま K8s に乗せる」際の典型的な落とし穴を丁寧に潰した好例です。
kintone のように Neco への移行が先行したプロダクトと、今回の Office・メールワイズのようにステートフル性の強い製品では移行の難易度が大きく異なることも、本事例で改めて浮き彫りになっています。
詳細
なぜ Kubernetes に移行するのか
サイボウズでは kintone や Garoon など各プロダクトを新インフラ基盤「Neco」へ移行する取り組みを進めています。Neco への統合によって、テナント管理やリリースの仕組みを全社共通基盤に集約でき、ハードウェアの世代交代やスケーリングも Kubernetes の仕組みに乗せられるようになります。
一方で、サイボウズ Office とメールワイズは20年以上稼働し続ける C++ 製 CGI アプリケーションであり、テナントごとのデータを SQLite で保持しているという独自の前提を持ちます。このアーキテクチャを Kubernetes 上で安全に動かすには、製品固有の前提に合わせた設計が不可欠でした。
SQLite を利用する CGI を Kubernetes で動かす
ステートフルワークロードと CGI の性質
Kubernetes 上でのステートレスなプロセスは、どのノードに Pod が配置されても問題ありません。しかし、MySQL のように「1つのデータディレクトリを1プロセスが占有する」アプリケーションでは、複数の Pod から同じボリュームに同時書き込みをするとデータ破損のリスクがあります。
CGI アプリケーションの場合、リクエストごとにプロセスが起動するため、複数プロセスが同じファイルを操作することが通常想定されています。その排他制御には fcntl(2) や flock(2) などのファイルロックが使われます。
Pod とボリュームの配置に関する制約
SQLite の排他制御に使われている fcntl(2) のロックは、同じカーネルを共有するプロセス間でのみ機能します。したがって、同じボリュームをマウントする複数の Pod 間でロックを機能させるには、それらの Pod が同一 Kubernetes ノード上に配置されている必要があります。
この制約を満たすために、次の2案を検討しました。
| 方式 | ダウンタイム | |
|---|---|---|
| 案1 | StatefulSet を利用 | 更新のたびに発生(旧 Pod 削除 → 新 Pod 作成) |
| 案2 | Deployment + Pod Affinity + ReadWriteOnce PV | ローリングアップデートによりゼロ |
月1回の定期メンテナンス以外でも安全にリリースしたいという要件から、案2を採用しました。Pod Affinity により新しい Pod は古い Pod と同じノードに配置され、ReadWriteOnce のボリュームは同時に1ノードにしかアタッチされないため、万一 Pod が別ノードに配置されてもデータ破損を防げます。
ローリングアップデートの安全性を検証する
案2ではローリングアップデート中に新旧 Pod が同じボリュームを同時マウントする瞬間があります。そのため次の2点を Linux カーネルの挙動で実際に検証しました。
fcntl(2) のロックが Pod 間で機能することを確認する
同じボリュームをマウントする2つの Pod で、SQLite が使う fcntl(2) の排他ロックを取得する Go プログラムを用意し、順に実行しました。後発プロセスのロック取得待ち時間が先行プロセスの待機時間ぶん長くなることで、Pod 間でもロックが正しく機能することを確認しています。Pod はプロセス空間こそ名前空間で隔離されていますが、同一カーネル上のプロセスであるため、これは自然な挙動です。
lock := unix.Flock_t{
Type: unix.F_WRLCK,
Whence: io.SeekStart,
Start: 0,
Len: 0,
}
unix.FcntlFlock(file.Fd(), unix.F_SETLKW, &lock)
ページキャッシュが Pod をまたいで残ることを確認する
製品の応答速度はページキャッシュに大きく依存しているため、Pod 入れ替え後もキャッシュが引き継がれるかを検証しました。memory limit 1Gi の2つの Pod で、一方が書き込んだ 5GB のファイルに対して fincore でページキャッシュを確認すると、もう一方の Pod からも同じ約 977MB のキャッシュが見えました。また kubectl rollout restart で Pod を入れ替えた後も、このキャッシュは残留しており、ローリングアップデートのたびにキャッシュがクリアされて応答速度が低下するという問題を回避できることが分かりました。
CGI のコマンドライン呼び出しを gRPC の API でラップする
Web リクエスト以外にも CGI を呼び出す処理がある
サイボウズ Office とメールワイズには、テナントの初期化・メンテナンスモード切り替え・ディスク使用量集計といった処理があり、これらは従来 VM 上のデーモンや SSH 経由で CGI をコマンドライン実行していました。Kubernetes では kubectl exec で代替できますが、操作の粒度が合わないため、サイドカーコンテナとして gRPC サーバーを追加し、CGI のコマンドライン呼び出しを API としてラップしました。
Pod 内の構成
Pod 内では2つのコンテナを稼働させています。
- メインコンテナ: Apache + CGI で Web リクエストを処理
- サイドカーコンテナ: gRPC サーバーとして動作し、CGI コマンドライン実行 API を提供
両コンテナは同じ製品イメージを使用し、PersistentVolume を共有マウントします。サイドカー化により、コマンド実行ログと Apache ログを分離しながらデータは共有できます。
また、gRPC サーバーの実行ファイルは製品イメージには含めず、Kubernetes の Image Volume で別の OCI イメージから読み取り専用マウントする構成を採用しました。これにより、gRPC サーバーだけを修正したい場合でも製品本体のリリースが不要になり、ビルドと配布を独立させています。
大量のファイルを短い停止時間で移行する
事前転送と最終転送の 2 段階の構成
対象ファイル数が数億規模になるため、月1回の定期メンテナンス時間内にすべてを転送することは不可能です。そこで転送を2段階に分けました。
- 事前転送: サービス稼働中に rsync で大半のデータを転送
- 最終転送: メンテナンス中に mtime ベースの差分だけを転送
実測では約300万ファイルの転送が15分程度で完了し、数億ファイル規模でも事前転送は1日程度で収まるという見積もりが立ちました。
mtime ベースの差分転送の安全性を検証する
rsync はデフォルトでファイルのサイズと mtime(最終更新時刻)だけを比較して転送対象を決めます。「データが書き換わったのに mtime が更新されないファイルは存在しない」という前提が崩れると、古いデータのまま移行されてしまいます。
POSIX の仕様では write() 成功時に mtime を更新対象としてマークすることが要求されており(POSIX.1-2017)、Linux (ext4) の通常書き込みでも一貫性は保たれます。ただし例外となりうる経路として次の2つが特定されました。
- O_DIRECT での書き込み: ページキャッシュをバイパスし、buffered I/O との混在時に一貫性が保証されない場合がある
- タイムスタンプを書き換えるシステムコール:
utime/utimes/utimensatなどで mtime を過去の値に設定できてしまう
これらについては bpftrace で本番相当環境を監視し、製品から使われていないことを確認しました。security_file_open(ファイルオープン経路)と vfs_utimes をそれぞれフックして対象デバイスへの呼び出しを検出しています。
移行先でもバックアップとリストアをできるようにする
サイボウズ Office とメールワイズのデータは Ceph の RBD(ブロックデバイス) 上に置かれます。バックアップにはストレージ基盤チームが実装した Mantle を使用し、日次バックアップ・リージョン間レプリケーション・リストアを構成しています。
Mantle のバックアップは2段階です。
- クラスタ内バックアップ: RBD スナップショットを日次で取得
- リージョン間レプリケーション: スナップショット間の増分をオブジェクトストレージ経由で別リージョンへ転送し、同じイメージを再構成
外形監視を再設計する
移行前は専用デーモンが監視用テナントに毎秒アクセスし、1秒以内の応答を確認する構成でしたが、監視実行からアラート送信まで1コンポーネントが担っており、アラート条件の変更も実装変更を伴う課題がありました。
移行を機に、アプリケーションに監視用エンドポイントを追加しました。このエンドポイントは特定ファイルへの書き込みと fsync を実行し、成功すれば HTTP 200、失敗すれば 503 を返します。ディスクへの書き込みまで確認することで、アプリケーションとボリューム両方を同時に監視できます。
監視コンポーネントはこのエンドポイントにアクセスして結果をメトリクスとして公開する exporter に変更し、可視化とアラート判定は Grafana とアラートルールに委譲しました。監視の実行とアラートの判定が分離され、条件変更が柔軟になっています。
まとめ
本事例の核心は「アプリケーションをコンテナで動かすこと以上に、製品が暗黙に依存してきた OS やストレージの挙動を明らかにすることが重要だった」という点です。fcntl ロック・ページキャッシュ・mtime についてはカーネルの挙動まで実証的に確認したうえで既存の仕組みを活かし、コマンドライン操作の API 化と監視設計については Kubernetes 上で運用しやすい形に刷新する——という「活かすところと変えるところの切り分け」が、20年超の製品を安全に移行するための基盤となりました。