overlayfs を用いたクラウドゲームのデータ管理

クラウド上でゲームを動かす場合、データ(ゲーム本体のデータ・セーブデータ)をどう管理するかは重要な課題のひとつだ。 そこで、自分が開発に携わっているOOParts のデータ管理を支える技術について紹介する。

ノベルゲームのセーブ事情

OOPartsで提供されているのは主にビジュアルノベルゲームで、セーブデータの形式や場所は実に多種多様である。 ゲーム本体と同じディレクトリにセーブデータを保存するゲームもあれば、ユーザーディレクトリ(マイドキュメント等)以下に保存するゲームもある。

とにかく様々な保存方式があるため複数メーカーのゲームを提供するとなると セーブデータの場所を機械的に特定できない。 唯一言えるのはほとんどのゲームはセーブデータをファイルに保存しているということぐらいだ。

ゲームタイトル毎に設定を持ち、「ゲームAのセーブデータは〇〇で、ゲームBのセーブデータは〇〇で…」とすれば不可能ではないが これでは新しいゲームタイトルを追加する度に工数がかかるので避けたい。

セーブデータは「ファイルの変更差分」と考える

そこで発想を少し変えみることにした。ノベルゲームは一度インストールするとすべてのゲームリソース(テキストや画像、音声など)がディスクに配置され、 ゲーム起動後に動的にダウンロードすることはほぼない。 よって、 ゲーム起動後に生じたファイルの差分は基本すべてセーブデータ関連 といえる。

このように「ファイルの変更差分はすべてセーブデータ」とすればセーブデータの場所を機械的に特定できるのではないかと考えた。

overlayfsによる差分検出

ファイルの変更差分を検出する方法としてはLinuxのinotifyを最初思い浮かべたが、他にも探してみたところ overlayfs というファイルシステムを見つけた。 overlayfs は「下層のファイルシステム」と「下層からの変更差分」を重ね合わせてひとつのファイルシステムのように見せるLinuxの機能である。 この特性がそのままノベルゲームのセーブデータ検出に使えるというわけだ。

図のように、下層のファイルシステムをインストール直後のゲームデータとすると、下層からの変更差分がセーブデータとして検出できる。

overlayfs は次のようにLinuxのmountコマンドで作成できる。 下層のディレクトリ (lowerdir)、変更差分のディレクトリ (upperdir)、そして両方を重ねあわせたディレクトリ(この例では /merged)を指定できるようになっている1

mount -t overlay overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work /merged

マウント後、基本的には重ね合わせ後の /merged で作業をする。 そこで下層の /lower にあったものはそのまま残っているし、新しくファイルを作ったり変更を加えたりすることもできるので普通のディレクトリと一見変わらないように感じる。

しかし、内部では面白い構造になっていて、もともとの下層 /lower 以下には何も変更は加わらず、新しく作ったファイルや修正したファイルは上層の /upper に保存されている。よって ゲーム終了後に /upper の中身を抽出するとセーブデータのみをきれいに取り出せる。 これでゲームごとに個別の設定を持たずに汎用的にセーブデータを抽出する機能として利用できるようになった。

Kubernetesによる動的なoverlayfsマウント

OOPartsのクラウドゲーミングは基本Kubernetes上で動いており、Linuxコンテナ上でWine を使ってゲームを起動している。 よってコンテナの内部でゲーム起動要求に応じて動的に overlayfs を作り出しゲームプレイ環境を準備している。

これをKubernetes上で実現するためにはいくつかポイントがある。

  • コンテナ内のoverlayfs
  • コンテナ内でのmountと別コンテナへの反映
  • 下層となるゲームデータをどうやって用意するか

コンテナ内のoverlayfs

overlayfsはDocker等のコンテナ技術で既によく使われている技術であり、コンテナの実行環境も実はoverlayfsのファイルシステムとなっている。 すでにoverlayfsの環境の上にさらにoverlayfsを被せようとするとdmesgに次のようなエラーが出る。

overlayfs: filesystem on '/upper' not supported as upperdir

overlayfsの変更差分を格納するディレクトリはoverlayfs以外のファイルシステムにしなければならない。 よってコンテナ上でoverlayfs mountをする場合はtmpfs等他のファイルシステム上にあるディレクトリを指定すればよい。

コンテナ内でのmountと別コンテナへの反映

コンテナ内から普通に mount システムコールを実行すると権限が足りずエラーになってしまう。 たとえば、Dockerコンテナ内でmountを実行するためには CAP_SYS_ADMIN と apparmor:unconfined というオプションが必要 となり、コンテナにかなり強めの権限が必要になる。

セキュリティを考えるとゲームを実行するコンテナに強い権限を与えるのは避けたい。 そこで、overlayfs のマウントは専用のコンテナに任せて、ゲーム実行のコンテナとは分離する手法を取った。 しかし、コンテナが分離するとファイルシステムも分離されてしまうので異なるコンテナ間でマウントを共有する仕組みが必要になる。 そこでKubernetesの機能にある Mount Propagation を利用した。

Mount Propagationはあまり聞き慣れない機能かもしれないが、リンク先のドキュメントにある通りコンテナ内でのマウントを他のコンテナにも反映させたり、逆にホスト側のマウントをコンテナ内に反映したりが可能になる機能。 overlayfsのマウントだけを特権コンテナで行い、それをゲーム実行側のコンテナに反映させるという今回の要件にはちょうど良い機能である。

しかし、図を見てもわかる通りコンテナの内側から外側に干渉するためトラブルが発生するとノード全体に悪影響を与えてしまう可能性があるので慎重に使う必要がある。 たとえばアンマウント処理を適切にせずにコンテナが終了するとPodがTerminatingから進まなくなる、なんてことが過去にあった(ノードに直接SSHで踏み込んで修正作業した…)。

下層となるゲームデータをどうやって用意するか

当然だがゲームを起動するためにはゲームの完全なデータが必要だ。 ただし、何のゲームが起動されるかはプレイヤーのゲームセッションが開始しないとわからない。よってコンテナイメージに事前にゲームを一緒に含めておくのは厳しい。 また、ゲームデータはサイズが大きく数百MB〜数GBの容量がある。よって起動時にゲームデータをダウンロードするのも厳しい。

そこで、NFS Serverにゲームデータを格納しそれを直接Kubernetesのホストマシンにvolume mountし、overlayfsの下層として指定する。 overlayfsと組み合わせることで保管元のNFS Serverを変更することなく、変更差分つまりセーブデータだけを抽出して別のストレージにアップロードできる。

まとめ

OOPartsではノベルゲームをクラウド上で動かすにあたり、下層のディレクトリとそこからの差分を重ね合わせるoverlayfsをセーブデータ検出に利用している。 また、Kubernetes上で高速なゲーム起動と一定のセキュリティを満たすためにVolume Mountの機能も活用している。

このようなLinuxやKubernetesの機能をノベルゲームのセーブデータ検出のために使っているというのはなかなか珍しい(?)例だと思うのでなにかの参考になれば幸いです。


  1. workdir というのも必要だが、これは作業用のディレクトリなので直接触る機会はない。適当にそれ用の一時ディレクトリを作っておけばよい