はじめに
はじめまして、機械学習エンジニアリングチームのsugasugaです。
今回の記事では、弊チームが管理するMLOps基盤の技術選定や構成内容を紹介させていただきます。
背景
ピクシブ株式会社はさまざまなサービスがありますが、イラスト投稿SNSであるpixivのサーバーはオンプレミスで動いています。
これまでは、GCP上でモデルの学習やバッチ推論を行い、データをオンプレミスに同期した上でレコメンドを提供していました。
しかし、最近になって新たにリアルタイムで推論する機能の必要性が生じました。
当初は、これまで通りオンプレミスでの推論基盤の構築を検討しましたが、マシンの新規調達が難しい状態でした。また、搭載されるアプリケーションの数が将来的に変動する可能性があったり、トラフィック数が予測できないという状態でした。
そのため、必要なリソースを必要なタイミングで確保できる形態の方が好ましいと判断しました。
そこで、この機会にGCP上で汎用的な推論基盤を作成することになりました。
アーキテクチャの概要
一部の細かいサービスは割愛していますが、アーキテクチャの概要は下記の図のようになっています。
機械学習アプリケーションはGKE上で稼働しており、推論を行います。
オンプレミスからの認証されたリクエストをCloud Load Balancingが受け、GKEに流しています。
機械学習アプリケーションに変更があれば、CIを通して、GKEへのデプロイが行われます。
定期的な再学習バッチをDigdag上で実行しており、半自動的に機械学習モデルを入れ替えることができます。
技術選定と構成
GKEの選定理由
アプリケーションを動かすサービスとしては、GCE・Cloud Function・Vertex AI prediction・GAE・Cloud Run・GKE等がありましたが、最終的にはGKEを選定しました。
選定理由をいくつか列挙します。
コンテナ開発ができReadiness Probe機能がある
コンテナでの開発を行いたいため、GKE・Cloud Run・GAEのFlexible Environmentなどが選択肢としてありました。
最初はCloud Runが有力な候補だったのですが、技術選定を進める上でデメリットが見つかりました。Cloud Runは、コンテナが立ち上がりポートが開かれたと同時に、トラフィックが流れ始めます。その際に、機械学習モデル読み込みに時間がかかりアプリケーションが立ち上がっていない場合、レスポンスが返すことができません。この問題は、kubernetesのReadiness Probeという機能を使って回避することができます。この機能によって、特定のエンドポイントへのヘルスチェックが通った場合のみ、トラフィックを流し始めることが可能です。
ポータブルである
コンテナかつkubernetesベースのシステムで構築しているため、ポータビリティを保てます。
社内ではサービスごとに採用されているインフラが異なり、オンプレミス環境・AWS環境・GCP環境を使い分けています。社の方針・料金・各ベンダーの提供するサービスなど、何かしらの事情で移行することもありえたため、ポータブルである必要がありました。
GKEが他の基盤ですでに運用されていた
チーム内の一部のメンバーによって、Digdag + GKEによる学習基盤が構築・運用されていました。推論基盤の構築に参加するメンバーが技術を習得し、チーム内でのトラック係数 (不意の事故などでメンバーに何人欠員が出るとプロジェクトが破綻するかを表す係数)を増やしてチームの耐障害性を強化できるという副次的な効果も狙いました。
また、チーム内だけでなく、他チームにもGKEの運用経験がある方がいました。
そのため、アドバイスを貰いやすい環境が整っていたのも採用を後押しした理由になります。
GKEと周辺のサービスのアーキテクチャ
GKE自体の設定
GKEでは、通常のStandard モードとマネージドな割合が大きいAutopilot モードがあり、後者を利用しています。Autopilotモードでは、ノートプールの管理が必要なく、またデプロイされたリソースに合わせて課金される体系のため、コストの最適化や、アプリケーションのスケーリングが容易というメリットがあります。また、限定公開クラスタにすることによりpodにpublic IPを持たせず、セキュリティ面を強化しています。
GKE周辺のサービス
アーキテクチャについて説明します。
GKEはアプリケーションにつかうDocker ImageをArtifact Registryから取得しています。
必要であれば、機械学習モデルやラベル情報などをアプリケーションのPod起動時にInit ContainerでGCSからダウンロードしてしています。
GKEの前段には、Cloud Load Balancingがありロードバランサーを設定しています。
Cloud DNSやマネージドSSL証明書の設定により、HTTPSによる通信を行なっています。
また、詳細は後述にて記載しますがCloud Load BalancingにてIdentity-Aware Proxyによる認証を有効にしています。これらのために、kubernetesのGKE ingressとBackendConfigの設定を行なっています。
kubernetesの管理ツール
kubernetesではマニフェストファイルと呼ばれる各種リソースの定義ファイルを準備し、GKE上でリソースの作成を行います。
それらのマニフェストファイルの管理を楽にするツールの選定をする必要がありました。
管理するツールとして有名なものには helm や kustomize がありますが、今回は kustomizeを利用することにしました。理由としては、helmに比べて可読性が高く、またkubernetesのコマンドラインツールであるkubectlから利用できることが挙げられます。
マニフェストファイルのディレクトリ構成は、下記のようになっており、開発環境と本番環境で共通の設定をbase以下、環境ごとの設定をoverlays以下に分けています。
└── アプリケーション名 ├── README.md ├── base │ ├── deployment.yaml │ ├── ingress.yaml │ ├── kustomization.yaml │ ├── scripts │ │ └── download-resources.bash │ └── service.yaml └── overlays ├── development │ ├── backendconfig.yaml │ ├── deployment.yaml │ ├── ingress.yaml │ ├── kustomization.yaml │ └── service.yaml └── production ├── backendconfig.yaml ├── deployment.yaml ├── ingress.yaml ├── kustomization.yaml └── service.yaml
オンプレミスサーバーとGKEの通信の認証
作成した基盤に乗せるアプリケーションは、オンプレミスのサーバー経由でユーザーから利用されます。そのため、一般公開はしておらず、直接利用されることは想定していません。
そして、通信はVPN等ではなくインターネット経由で行われるため、認証されていない通信の制限を行う必要がありました。そのため、Identity-Aware Proxyという、リバースプロキシとしてユーザーとアプリケーションの間に入り認証と認可を行ってくれるマネージドサービスを使用しています。
オンプレミスのサーバー側は、該当ロードバランサーの特定のIAM権限を付与されたサービスアカウントのクレデンシャルを使い、GKEと通信を行います。開発者にも同様の権限が付与されており、通信可能となっています。ただし、一般公開はされていない状態になっています。
ロギングのアーキテクチャ
アプリケーションのログ
アプリケーションから出力される機械学習モデルの推論結果などは、Cloud Loggingに送信されるようになっています。
GKEから出力された生ログをCloud Logging経由でBQに同期し、日毎のETL処理で必要な情報をテーブルに格納しています。
アプリケーションのエラー
アプリケーションのエラーログは、全社共通のSentry基盤に送信しています。エラーの確認・管理・通知などが簡単です。Sentryの設定のために、マニフェストファイル内に環境変数として、DSNを定義します。
spec: (割愛) containers: (割愛) env: - name: SENTRY_DSN value: "HOGEHOGEHOGE"
そして、アプリケーション側のコードで、sentry_sdk.init関数を呼び出してあげれば、ログを送信できます。自分達はsentry_sdk.set_tagをつけてアプリケーションを識別しています。
一定時間内に一定以上のエラーがあった場合、slackに通知する設定を作っています。
import sentry_sdk sentry_sdk.init(traces_sample_rate=1.0) sentry_sdk.set_tag("service_name", "アプリケーション名")
GKEクラスタのメトリクス
GKEクラスタのメトリクスはdatadogに送信しています。
クラスタに異常があれば通知がくるように設定しています。
クラスタにdatadogをインストールするために、マニフェストファイルにはkustomizeにてhelmチャートを使う設定を書いています。
apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component helmCharts: - name: datadog repo: https://helm.datadoghq.com version: 2.36.0 releaseName: datadog namespace: default valuesFile: values.yaml
継続的デリバリー
デプロイ
デプロイはCIを通して行われます。
アプリケーション側のリポジトリに変更を加えた場合、CIにより、
- アプリケーション用のDocker Imageのタグにコミットハッシュを付与し、Artifact Registryにpush
- 該当コミットハッシュのタグがついたDocker imageを使うようにマニフェストファイルを変更するMerge Request (Pull Request)が自動作成
されます。
MRの例:
その後、マニフェストファイルのMerge Request (Pull Request)が、developmentブランチにマージされれば開発環境のクラスタにCI経由でデプロイ、masterブランチにマージされれば本番環境のクラスタにCI経由でデプロイされます。
GitOpsのpush型採用の理由
マニフェストファイルのデプロイ引用手法としてはGitOpsがありますが、GitOpsにはpush型とpull型の2種類が存在します。
push型はCI上からkubectl applyコマンドを実行しデプロイする手法であり、pull型はGKEクラスタ内から、ツールがGitリポジトリの変更を検知して変更内容をデプロイする手法です。
一般的には、pull型が主流だと思いますが、今回はpush型を採用しています。
技術検証段階では、pull型を達成するためのArgoCDというツールを使っており、強く感じたメリットとして
- クラスタの状態がUI上からわかりやすい
- push型だと、CIが成功しても、コマンドが通っているだけでデプロイが成功しているわけではない。慣れていないチームメンバーが触った場合、勘違いしてしまう可能性がある。
- ArgoCDを利用した場合はUI上で簡単に確認できる
- UI上から緊急リバート可能
- アプリケーション・マニフェストのCIを待つ必要がない
という点があります。
しかし、実際にArgoCDで構築を行ったところ、クラスタ上にArgoCD関連のpodが乱立され、アプリケーションなどよりも高い金額がかかることが判明しました。また、マニフェストファイルの量がかなり増え、アプリケーション用のマニフェストファイルの量よりも、ArgoCDの用のマニフェストファイル量が多くなる状態となりました。さらに、ArgoCDは公式ドキュメント以外の情報が少なく、Git連携・認証・モニタリング連携の設定が難解のため、属人性の高い状態となりそうなことがわかりました。
今後アプリケーションの追加や人が増える場合、必要になることはあると思いますが、まずは小規模にスタートする予定だったため、採用を見送ることにしました。
継続的トレーニング
モデルの再学習等はdigdagの学習基盤で行なっています。
詳しい基盤については、こちらの記事をご確認ください
学習データは、BQやGCSから直接取得します。
学習内容に応じてオンデマンドインスタンスか、安いが最大でも24時間で強制終了されるプリエンプティブルインスタンスかを切り替えて学習を行なっています。また、学習が途中で中断しても再開できるように、checkpointをGCSに出力しています。
再学習が終わった後は、digdagからkubernetesのマニフェストファイルへ変更を行うMRを作成しています。MRがマージされると、アプリケーションがダウンロードしてくるメタデータ(ex. 機械学習モデル) の変更を行うことができます。
さいごに
今回作成した基盤で複数のアプリケーションを稼働させ、新機能の提供や既存機能の数値の改善などを行っています。今後もアプリケーションが追加されていく予定です。
ピクシブ株式会社の機械学習チームでは、自然言語処理、画像処理、テーブルデータなどの様々なデータで機械学習エンジニアリングが行えるだけでなく、インフラ、バックエンド、フロントエンドなどの幅広い技術領域での仕事をすることができます。
そんな弊チームでは、インターン・アルバイト・新卒採用・中途採用・業務委託(副業を含む)を問わず、採用を行っています。カジュアル面談も実施しておりますので、ぜひお気軽にお申し込みください。