
最近DevOpsという分野に興味があり、調べていたところ、印象的な映像があったので、これについてまとめてみたいと思います。

Tossでは2021年下半期に「
サーバーサイドレンダリング
」アーキテクチャの導入を開始しました。 これはユーザーが経験するロード時間を短縮するためには非常に効果的なアーキテクチャであり、これによりロード完了時間(LCP)が平均0.9秒短縮される経験をすることができたそうです。しかし、サーバーサイドレンダリングアーキテクチャを導入することで様々な問題も発生しました。
ビルドとデプロイ時間の増加
- モニタリングとメンテナンスコストの増加
- 必要なコンピューティングリソースの増加
今回の動画ではこのうち、
Webサービスのビルドとデプロイ
について詳しく説明しました。
ウェブサービスをビルドしてデプロイすることは、すべてのフロントエンドエンジニアがほぼ毎日行うプロセスであるため、Tossではビルド、デプロイ時間を短縮することが非常に重要な問題です。Tossで提供する小さなウェブサービスが100個以上あるので、これをテストするためのアルファ版へのデプロイを毎日150回以上行っており、ライブデプロイも35回以上行っています。
多数の人員が投入され、デプロイ回数が多いサービスの特性上、サービスビルド時間を短くすることが効率の面で非常に重要な問題です。
Tossではこのような問題を改善するために「
Frontend DevOps Engineer
」というポジションが存在します。SSRのようなアーキテクチャを導入することで、流れるようなユーザーエクスペリエンスを提供し、過程の複雑さによって開発者が経験する様々な不便さをシステム的に予防するポジションです。Tossではどのように配布時間を改善したのか?
一般的にSSRサービスをデプロイする過程は、次のようなパイプラインに基づいて行われます。

エンジニアがコード変更を
push
すると、CIマシンでコード修正をダウンロードして必要な依存関係をインストールします。ビルドする準備ができたら、サービスをビルドして実行します。その後、ユーザーはサービスにアクセスして最新のサービスを使うことができます。実装当初は「レポジトリの複製、依存関係のインストール」に70秒、「SSRサーバーの実行」に60秒かかっていましたが、改善後は「レポジトリの複製、依存関係のインストール」に6秒、「SSRサーバーの実行」に20秒と大幅に短縮されました。


リポジトリの複製と依存関係のインストールを最適化する方法

まず、tossサービスは
toss-frontend
という大きなリポジトリで管理されています。様々なウェブサービスが存在するため、依存するライブラリの量も多くなり、ソースコードも大きくなるという問題がありました。
このような状況で従来のNodeの方法で依存関係をインストールすると大きな問題が発生します。
node_modules
の容量のためI/Oも長くかかり、依存関係フォルダのキャッシュも不可能になります。
Tossチームではこのような問題を
Yarn Berry
の導入で解決しました。 Yarn Berryでは1つのパッケージが1つの圧縮ファイルで表示されるため、依存関係ファイルの数が少なく、サイズも小さいというメリットがあります。おかげでキャッシュも可能で、バージョン管理も可能になりました。Tossではレポジトリの複製だけで依存性のインストールが完了するZero-install機能を深く使っています。 他にも、レポジトリ全体が複数のバージョンの依存性を使わないようにdedupe, constraintsなどの機能も提供しているそうです。
一般的なGit cloneでレポジトリを複製すると全てのコミットオブジェクトと全てのファイルがネットワークからダウンロードされますが、容量が大きくなるとこの部分でも時間的な非効率が発生します。
これらの問題を解決するための最初の方法が「
Shallow Clone
」です。
Shallow Clone
は配布するブランチから一つまたは最大n個のコミットとその関連ファイルをダウンロードする方法です。Shallow Clone
はGit CloneのShallowオプション
で簡単に使用できますが、ファイル変更点の追跡が難しい、デプロイが必要なサービスだけデプロイできないという欠点があります。
Git 2.22.0以降
のバージョンからはFilter Spec機能
を使って修正内容を入れたコミットオブジェクトは全てダウンロードし、ファイルは最新バージョンのコミットに含まれるものだけダウンロードすることができます。
これにより、配布したいブランチの必要なファイルリストだけスマートに取り込むことができます。このようにすると、修正内容を入れたコミットオブジェクトは全てダウンロードし、ファイルは最新バージョンのコミットだけダウンロードすることが可能になります。
SSRサーバーの実行を最適化

TossでSSRサーバーを実行する順序は大きく2つありますが、最初のステップはCIビルドサーバーでSSRサーバーのDockerイメージをリポジトリにアップロードすること、そしてKubernetesクラスターにある各ノードがこのDockerイメージをダウンロードして実際に実行することです。上記の過程を見ると、ほとんどのボトルネックはCIサーバーがDockerイメージをアップロードし、Kubernetesのノードがこれをダウンロードするネットワーク時間であることが分かります。

これを最適化するために「
Dockerイメージはレイヤー化される
」という事実に注目したそうです。Node 16.14バージョンを使うレイヤーから始めて、その上にpackage.json、yarn.lockをコピーして新しいレイヤーを作ります。このように少しずつ新しいレイヤーを作成していきます。 各レイヤーはハッシュ値を持ち、同じハッシュを持つレイヤーはキャッシュされます。例えば、同じpackage.jsonとyarn.lockに対して以前にyarn installを実行した場合、再度yarn installを実行することはありません。
Dockerレイヤーに対するキャッシュは、複数のコンピュータ間でも維持することができます。例えば、Node 16.14の環境を既にダウンロードしたNodeであれば、次にまたダウンロードすることはありません。 自分がまだアップロードしていないレイヤーだけをアップロードし、受信していないレイヤーだけを再受信することになります。
これに基づいて考えると、Dockerイメージをアップロードする時間を短縮するためには、「
サービスごとに異なるレイヤーをできるだけ減らす必要がある
」という結論が導き出されます!
サービスごとに異なるレイヤーを減らすと、CIビルドサーバーがDockerイメージリポジトリに新しいビルドをアップロードする時、アップロードするサイズが小さくなります。

KubernetesでSSRサーバーを実際に実行する時も、すでにDockerレイヤーをダウンロードした状態であれば、SSRサーバーを実行するために受け取るレイヤーのサイズが小さくなるため、実行時間が速くなります。
Tossではレイヤーのサイズを最小限に抑えるために、サービスを実行するために必要な最小限のファイルだけを変更されるレイヤーに含めるようにしているそうです。
実行に必要な最小限のJavaScriptファイル
を実行するための作業を行うライブラリが存在します(== Vercelのnft、nodefile trace。正確にはどのJavaScriptファイルが必要かを計算してくれます)。
他にも様々な方法を活用してそれぞれリポジトリの全体サイズを削減し、トランスファイルとミニパイの時間を短縮し、複数のサービスのビルドとデプロイを同時に実行することが可能になったそうです。
感想とまとめ
「
サービスが継続的に最高のユーザーエクスペリエンスを提供するためには、開発者経験(DX)がサポートされなければなりません
」、「失われた開発者の時間を探して
」という二つのフレーズを通じて、DevOpsエンジニアの重要性と効果を感じることができたという点でとても印象的でした。DevOpsエンジニアという職種に興味を持つようになったのは最近のことなので、この動画を見ながら初めて接する概念が、今まで考えたことのない視点でプロジェクトを眺めているという点が難しくも興味深くなったような動画でした。😊
댓글