カラーミーショップ では、近年急激に増え続けるEC需要に対応するため、システム全体の可用性向上を目指す「Boostプロジェクト」が組織されています。
自分はそのプロジェクトで主に既存アプリケーションを Kubernetes 上で稼働させるための移行作業として、アプリケーションのコンテナ化や、それらに伴うアーキテクチャの変更などを主に担当しています。
カラーミーショップはサービス開始から15年以上経っているため、古くから稼働しているアプリケーションも多くあり、それらを Kubernetes という新しいシステム上へ載せ替えようとすると、大抵一筋縄では行かないものです。今回は、その中でもレガシーな「ファイルキャッシュ」という仕組みを Kubernetes 上に移行する際に出た課題とその解決方法についてお話します。
なおこの記事の内容は、去る2021年12月9日開催された ペパボECテックカンファレンス カラーミーショップの裏側を大公開!15年以上続くサービスが今取り組む技術課題と2022年の抱負 内で「15年以上動くECシステムをクラウドネイティブにするためにやっていること」として発表したスライドの詳説版になります。
カラーミーショップの自由なテンプレートシステム
カラーミーショップには、ショップオーナーがページごとに表示内容を自由に設定できる「テンプレート機能」があり、ショップページの表示に関しての自由度が高いことが特徴です。このときテンプレートはカラーミーショップが予め用意しているものを選択したり、ショップオーナー自身が専用のマークアップ用構文を使って独自に製作したりすることができます。それらのテンプレートを、ショップページ用アプリケーション(以下、colorme-entrance)のテンプレートエンジンが読み込み、そこにデータベースから取得したショップ情報や商品情報を流し込むことでページのレンダリングが行われているというわけです。このときテンプレートエンジンは、一度読み込んだテンプレートファイルから「キャッシュファイル(以下、キャッシュ)」を作成し、次回以降はそのキャッシュを元にしてレンダリングを行います。このキャッシュはいわゆる「中間ファイル」的なもので、商品情報など可変部分については含まれていません。このキャッシュの仕組みによってイチからテンプレートを読み込んでデータを流し込むよりも幾らか高速にレンダリング結果を返却することができるようになっています。
この仕組みによって生まれるキャッシュの数は、ショップ×ページ種類(トップページ・商品ページ・カテゴリ一覧ページなど)になります。カラーミーショップでは、2021年末現在で4.5万件のショップが運営されているので、単純計算でも 4.5万×ページ種類 のキャッシュが生成されていることになります。実際には複数台のサーバーでラウンドロビンされる構成になっていたり、定期的にキャッシュの削除を行っていたりするため、正確な数ではありませんが、それでもサーバー上には相当な量のキャッシュが保持されています。
Kubernetes での Pod のライフサイクルとファイルキャッシュの関係
今回可用性向上プロジェクトでは既存システムを Kubernetes 上で稼働させることを目標としています。その重要な核とも言える Kubernetes では、基本的にアプリケーションを「Pod」という単位で稼働させます。これは Kubernetes 上で1つのアプリケーションを構成するための最小単位で、専ら複数のコンテナの集合によって表現されます。この Pod 内のコンテナ同士であれば、ネットワーク通信や Volume によるファイルの共有などができるようになっており、アプリケーションのコアを成すコンテナの他に Fluentd といったロギングシステムや、 Nginx などのリバースプロキシなんかを起動させることで、さながら通常のサーバーマシンのように振る舞うことができます。
一方で、 Pod は論理的な概念であり、ほとんどの場合で動作する物理マシンなどには制約されることがなく、 Pod は Kubernetes の非常に柔軟なスケジューリングによって新規作成・削除が行われることになっています。以下はその一例です。
- アプリケーションのアップデートによる Docker イメージの差し替え
- 新しいアプリケーションコードを含む Docker イメージの Pod が新たに作成され、古い Docker イメージの Pod は Pod ごと削除される
- スケールアップ・ダウンによる Pod 数の調整
- CPU負荷やメモリ使用量がしきい値を超えた際のオートスケールによって新規追加されたり不要になったら削除されたりする
最も重要なのが、 Pod が新たに作られたとき、それまで使われていた Pod 上で作成されたファイルが引き継がれることがないということです。そのため今回取り上げたテンプレートエンジンのようにキャッシュをファイルとして出力するようなシステムがある場合、 Pod が再作成される度にキャッシュがクリアされてしまう ということになってしまいます。
キャッシュのライフサイクルが変更されると実際どれぐらい影響を受けるのか
ここまでの話で、そのまま colorme-entrance を Kubernetes へ移行すると、事あるごとにレンダリングエンジンのキャッシュが失われてしまうということがわかりました。とはいえ「テンプレートエンジンによってキャッシュが出力される」という事実は把握していますが、そのキャッシュによってどれぐらいの効果があるものなのかはわかっていません。現在の運用でも定期的に長期間アクセスがないと思われるキャッシュについては削除するオペレーションも実施しており、その中でショップの訪問者への直接的な影響は確認できていないという事実があります。
そこで、我々のチームはキャッシュが無いとき、実際のレスポンスタイムにどのような影響が出るのかを調査をすることにしました。測定には現行アーキテクチャである Virtual Machine (以下、VM) を新たに用意し、これを本番環境のロードバランス先の1台として投入することにしました。新しい VM は「キャッシュがない状態」でショップの訪問者へレスポンスを生成することになるので、これと現在稼働中のキャッシュが十分に溜まっている VM とのレスポンスタイムの比較によって、キャッシュの有無による影響を計測することができます。
この計測は平日の日中に2時間ほどを使って行いました。計測結果は下図のとおりで、既存の VM たちのグラフの線はだいたい一定のレスポンスタイムでまとまっているように見える中、新規に投入した VM のレスポンスタイム(赤線)だけが目立って遅延しており、既存の VM の平均レスポンスタイムと比較すると 20〜30% ほど悪化することが分かりました。
ただしこの悪化の傾向は、投入されてから 40〜50分 ほどの間続くものの、それ以降は既存の VM と同程度まで改善されます。これはある程度のキャッシュが溜まる(=使用頻度の高いキャッシュが揃ってくる)とそれ以降は全体へのレスポンスタイムの影響は少ないということでもあり、今回の計測ではこれがわかったことも非常に大きな収穫でした。
この VM での計測の結果を元に、 colorme-entrance を Kubernetes 上で稼働する Pod にしたときに、キャッシュがないことでどういった影響が出るのかをチーム内で考えた結果、以下のような意見が出ました。
- リリース直後 40〜50 分の影響であれば長期的なスパンで見るとそこまでの悪化にならないのではないか
- colorme-entrance のリリース頻度はそこまで頻繁ではないため
- 一方で今後 Kubernetes のオートスケールや Pod のスケジュールなどを考慮した場合に影響が出てしまいかねない
- Pod の増減はリリースだけではなく Node の追加・削除やオートスケールなどでも起こりうるため
- Pod だと VM より更に台数が増えるのでさらにキャッシュが揃うまでに必要な時間がかかることが想像できる
実際に colorme-entrance を本番導入したとき、 Pod のスケジューリングがどうなるかについて現時点で定まってはいませんが、現行の VM でのホスティングよりかは柔軟に行われることは間違いないでしょう。そしてその全てで今回のようなレスポンスタイムの悪化を招くと、ショップの訪問者の体験が悪化し、 CVR の悪化につながりかねないことも想像できます。これ以外にもチーム内で色々な検討をしましたが、最終的に我々がチームの目標として「可用性向上」を掲げている以上は、やはり何らかの方法でキャッシュを利用できるようにしたほうがよいという結論に至りました。
キャッシュを Kubernetes 上で保持するのためのアプローチ
というわけで colorme-entrance は Pod が差し替わっても、何らかの形でキャッシュを保持する仕組みを考えなければならなくなりました。
ここで一般的な Kubernetes の利用者が「ファイルを永続化するための解決案」としてまず最初に考えつくものとしては、 Kubernetes の基本的な機能である Presistent Volume (永続ボリューム) を使って解決するという案です。これは大容量のストレージを持った Node (実際に Pod が稼働するための VM または物理マシン) などに Volume を作成し、そこにファイルを貯めてゆくという非常に単純な仕組みです。もし Pod が再作成された際は、その前に稼働していた Pod と同じ Volume をマウントすることで、前の Pod が作ったファイルをそのまま参照できるようになります。
一見何も問題ないように見えますが、この方式を採った場合のデメリットは挙げるときりがありません。
- Kubernetes ならではのオートスケールといったメリットを最大限享受できなくなる
- 複数の Pod が同じ Volume のファイルを読み書きすることはできないため、 Pod のワークロードを Deployment から StatefulSet に変更する必要がある
- これによりスケーリングやローリングアップデートの速度に悪い影響が出る
- この方式をとったとしてもそれまで稼働していた Pod の台数より多い Pod を稼働させると結局キャッシュなしの状態からスタートすることとなる
- Pod のデプロイ先が永続ボリュームを作成できる Node に依存するため Pod の可搬性が下がる
- ペパボのプライベートクラウドの関係で特定 Node でしか永続ボリュームを利用することができません
- 現状の VM でもキャッシュの溜め込み量が問題となっており低頻度アクセスのキャッシュを削除するサービスが必要
- 結局なんらかのアプリケーションの開発は避けられない
そこで Kubernetes のメリットを活かしつつ、クラウドネイティブな解決案をチーム内で考えた結果、以下のように実現することとなりました。
- キャッシュを定期的にオンラインストレージにアップロードするアプリケーションを作成
- オンラインストレージにあるキャッシュを Pod の起動時にダウンロードしてくる
キャッシュマネージャの作成
「キャッシュをオンラインストレージにホストする」を実現するためには、新たなアプリケーションを作成することになります。このアプリケーションについてもチーム内で仕様を検討した結果、以下の要件を定義することにしました。
- オンラインストレージにホストするキャッシュはカラーミーショップ全体でのアクセス数が上位のショップのものとする
- 計測の結果からアクセスされる頻度の高いショップのキャッシュが存在すれば高速化の効果が期待できるため
- 絞り込みにより現在 VM で行っている定期的なキャッシュ削除作業の代替になるため
- 使用するオンラインストレージの容量削減も行えるため
- キャッシュのアップロードは定期的に実行されるようにする
- テンプレートの書き換えやアクセス集中などにより必要なキャッシュも時間とともに変わってくるため
- Pod の終了シグナルを受け取ってからでは間に合わないので Pod のライフサイクルより短い範囲で行われるようにすることで回避するため
- オンラインストレージにホストされたキャッシュはアプリケーションに依存しない形で取り込めるようにする
- キャッシュマネージャーを持たない通常の colorme-entrance の Pod でもダウンロードできるようにするため
まず 1. と 2. の要件を実現するためには、実行時点でのアクセス上位のショップを求める必要があり、これには Google の BigQuery を使うことにしました。カラーミーショップで使っている BigQuery 上には既存の VM 環境のアクセスログもあるため、 Kubernetes と並行運用しても効率よくデータを収集できるというメリットもあります。また、キャッシュをホストするためのストレージは事業部内でも利用実績のあった Amazon S3 を利用することにしました。
要件の 2. については colorme-entrance の Pod 内に新たなコンテナを立ち上げることで実現でき、 3. についても Kubernetes の基本的な機能である InitContainers を使って Amazon S3 から Pod のボリューム上にダウンロードできる形にファイルを配置すればよいというだけなので、今回実際にアプリケーションとして実装したのは要件 1. の部分がメインとなります。
このアプリケーションで使用する言語は、ペパボ内でも利用者が多く関連ライブラリも十分に揃っている Ruby を選択して開発を行いました。最終的に Gem として Docker イメージ内にインストールすることで、シェルコマンドからサービスを起動でき、 Deployment の Manifests (稼働させる Pod の構成を書いた定義ファイルのこと) 上で可変パラメータの設定を行えるようになっています。
キャッシュマネージャのデプロイ
完成したキャッシュマネージャは、既に出来上がっていた colorme-entrance の Pod に含まれる新たなコンテナとして稼働させることになります。しかしながら、キャッシュマネージャをすべての Pod で稼働させてしまうとオンラインストレージ上のキャッシュの更新合戦が起こってしまうため、選ばれた1台のみがこのコンテナを含むように調整する必要があります。これには既にプロジェクト内で導入されていた Kustomize の Manifests の継承機能を利用することで colorme-entrance の Pod の機能を引き継ぎつつも、別の Deployment として定義されるように構成しました。
これによって、キャッシュマネージャを持つ Pod もそうでない Pod も同じ colorme-entrance の機能を持たせることができたので、両者に同じラベルを付与した上でそのラベルを持つ Pod を対象に Service のラウンドロビン先として定義すれば、キャッシュマネージャを持つ Pod 上にもキャッシュが溜め込まれてゆくという構成になっています。
まとめ
今回はレガシーなアーキテクチャの資産を生かしつつもクラウドネイティブなプラットフォームへ移行するための取り組みの一例をご紹介しました。カラーミーショップの可用性向上を目指す「Boostプロジェクト」では今回紹介した内容以外にも、様々な課題や問題を解決するために日々開発をしています。決して派手ではありませんが、こういった地道な努力によってカラーミーショップをより多くの方々に使っていただけるよう今後も頑張っていきますのでよろしくおねがいします。