TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

938

はじめに こんにちは、計測プラットフォーム開発本部SREブロックの近藤です。普段はZOZOMATやZOZOGLASS、ZOZOFITなどの計測技術に関わるプロダクトの開発、運用に携わっています。計測プラットフォーム開発本部では、以前プロダクト単位でSLO(Service Level Objective) 1 を定めましたが、うまく活用できず、再度SLOについて運用方法を考え直すことになりました。本記事では、SLOの再導入から運用に向かう中で見つかった課題と、課題に対する対応策についてご紹介します。 目次 はじめに 目次 背景 要因分析 Problem Try Action Actionの実行 SLO設定時の段階分け 例:ZOZOMATの段階分け 課題の洗い出し 例:SLOがない事による課題(SRE視点) 目的の明確化 信頼性とはそもそも何か 一般的な信頼性 計測プロダクト UJの整理 SLOの定期的な見直しと改善 SLOダッシュボード まとめ 背景 前述したように、各プロダクト毎にSLOを導入しましたが、運用できているとは言えない状況でした。具体的には、SLOを活用した改善が進められておらず、また他チームやビジネスサイドに浸透できていませんでした。これらの課題について要因分析し、対応策を検討しました。 要因分析 要因分析としてSREチーム内でKPTAフレームワークを使った振り返りを実施しました。この場で挙げられたProblem、Try、Actionを以下に記載します。 Problem SLOの作成目的の抽象度が高く、解決したい課題が明確になっていなかった 過去に設定した目的:ステークホルダーがどの作業を優先すべきかを決定するため SLOの作成が優先され、何故作るのかが深掘りできていなかった SLOの啓蒙を十分に行わずに他のチームへ展開してしまった SLOが安定しており、課題が出てこない Try SLOによって何を得たいか具体化する 現状の課題とSLOの目的を明確にする 以下に例を載せるが、SLOがないことによる課題をあげ、SLOが運用されることでどういった状態を目指すか考える 例: 課題:サービスが信頼性を担保できているのかわからない 具体例:レイテンシが悪化傾向にあったとして、対応すべき問題かわからない 目指す状態:SLOがあることにより、サービスの信頼性について判断でき、信頼性の低下時に対応方法の議論/改善が行える 初期段階から他チームやビジネスチームを巻き込むのは難易度が高い SREだけでなく、他チームとSLOについて認識を合わせる 段階を分け、各段階毎に課題と目的を明確にする 上記で他チームと記載しましたが、ここで計測プラットフォーム開発本部の組織体制図を紹介します。体制図からわかる通り、職能毎にチームが分かれている状態となっています。ビジネスサイドはさらに別の組織となっているため、ここでは割愛します。 ※引用元スライド: https://speakerdeck.com/zozodevelopers/company-deck Action SLO設定時の段階分け 各段階の課題と目的を明確化 SLOの定期的な見直しと改善 Actionの実行 SLO設定時の段階分け 段階の分け方としては、計測システム部、計測プラットフォーム開発本部、と巻き込む範囲をまずは段階の要素として考え、次にSLOの成熟度も段階の要素として考えました。この2つの要素を縦軸を成熟度、横軸を巻き込む範囲としてグラフ化し、プロダクトのフェーズや状態によって調整しようと考えました。実際には、SLOの計測、可視化、目標の調整、目標値を下回った際の対応など細かいフェーズがありますが、図では簡略化しています。 実際に行った段階分けについてはプロダクト毎によって異なりますが、ここではZOZOMATを例として記載します。 縦軸がSLAの箇所に行くにはビジネスサイドを巻き込む必要があり必然的に横軸を進める必要がありますが、ZOZOMATはToB向けのサービスではないためSLOの運用までとし、横軸で段階分けを行いました。 例:ZOZOMATの段階分け 段階 状態 1 SREチーム内でSLOの運用が開始されている 2 計測システム部内でSLOの運用が開始されている 3 POを含め、SLOの運用が開始されている 課題の洗い出し まずはSLOがないことによる課題を洗い出すところから始めました。ここでは、段階1のSREチーム内で洗い出した課題を例として記載します。 例:SLOがない事による課題(SRE視点) サービスが信頼性を担保できているのかわからない 具体例:メトリクスに変化があっても問題か判断できない 仮に信頼性が低下傾向にあるとわかっても、信頼性を向上させることを他の事項よりも優先すべきか判断できない 具体例:システム振り返りの調査タスクが積もりがち、プランニングで優先度を判断できない 上記で記載したシステム振り返りは、以下のような形で行っています。 開催頻度:週次 参加者:計測システム部所属メンバー 対象システム:ZOZOMAT、ZOZOGLASS、ZOZOFIT 確認する項目:AWSコスト、Datadogコスト、アラート通知履歴、プロダクト毎のDatadogのダッシュボード 進め方:確認項目で気になる点を各自が記載した後、同期的に議論し、改善や調査の必要があればタスクを起票する 目的の明確化 課題について深掘りし、目的を明確化しました。ここでは、段階1のSREチーム内での目的を例として記載します。 信頼性とはそもそも何か 課題に記載した信頼性について言語化ができていない状態だったので、信頼性については定義することから始めました。信頼性が高い状態=ユーザに価値を継続して提供できている状態と考え、それはどういった状態かチーム内で議論しました。まずは、一般的な信頼性について考え、次に計測プロダクトに当てはめた場合について考えてみました。 一般的な信頼性 ユーザに価値を提供できていない状態とはどんな状態か ユーザが体験を損なわない速度で応答を返せているか 計測プロダクト 計測に時間がかかりすぎて、こんなに待てないとなっていないか ユーザが体験を損なわない成功率を出せているか 計測の成功率が低く、繰り返しの計測が求められ、離脱に繋がっていないか ここで計測プロダクトの特性を加味した信頼性について考えることで、SLIを立てる際にユーザージャーニー(以下、UJ)を整理して調整することになりました。 UJの整理 このように課題を洗い出して深堀りした上で、最初の目的は「SREチームがサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる」としました。 同じ流れで各段階毎の課題と目的を明確化し、最終的にZOZOMATについては以下のように整理されました。 段階 目標 目的 課題が解決された状態 1 SREチーム内でSLOの運用が開始されている SREチームがサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる プランニングで信頼性の向上施策について、優先度を判断できる 2 計測システム部内でSLOの運用が開始されている 計測システム部がサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる システム振り返りで調査対応すべきタスクを定量的な指標で判断できるようになる 3 POを含め、SLOの運用が開始されている POがサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる 信頼性が低下した場合に、他プロダクトの案件と比較して、優先度を判断できる SLOの定期的な見直しと改善 SLOの定期的な見直しと改善に関しては、チームでスクラムを採用している関係もあり、スクラムイベントの中に組み込みました。具体的には、SLOの確認はリファインメントで行い、課題が見つかった場合もその場で改善タスクを起票すると言った形です。当初は定期的な振り返りを想定していましたが、結果としてMTGの時間を増やす事なく、SLOの評価と改善が行えるようになりました。 SREチームのスクラムについてはブログ記事が公開されているので、気になる方はこちらの記事をご参照ください。 techblog.zozo.com SLOダッシュボード SLOの定期的な見直しと改善をするにあたってダッシュボードを整備しました。工夫したポイントとしては、1つのダッシュボードで全てのプロダクトのSLOが確認できる点と、SLOで問題があるものだけが表示される点です。情報量が多くなると見落としがちになるため、必要な情報のみ出すことで議論すべきSLOが明確になりました。なお、ZOZOFITのSLOは今後導入を予定しています。 まとめ 本記事では、SLOを導入し運用を進める中での課題と対応策について紹介しました。現状はSREチーム内での運用が開始され、次の段階である計測システム部での運用を目指している状態です。導入し運用に向かう中で見つかった課題と向き合うことで、本来解決したかった課題と目的を改めて考える機会を得られ、結果としてSLOの設計から見直すことができ、運用フローもチームに適した形となりました。SLOの作成や導入を急ぐと陥りがちな状況かもしれませんが、これからSLOの導入を進める方、運用に課題を感じている方の参考になれば幸いです。 余談になりますが、稼働中のPODに対して変更する上でエラーは発生するもののSLOに問題ない=サービスとして許容可能なリスクであると判断し、変更方法を決める際の後押しにもなりました。信頼できるSLOがあることで攻めの姿勢を選択しやすくなりました。これは予期せぬ副産物でした。 ZOZOでは一緒にサービスをより良い方向に改善して頂ける方を募集中です。計測プラットフォーム開発本部としては、既存サービスの改善を日々行いつつ、新規サービスの開発も行っています。このような環境を楽しみ、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com (引用: SLO:サービスの信頼性の目標レベル) ) ↩
アバター
はじめに こんにちは、ML・データ部MLOpsブロックの松岡です。 本記事では Cloud Composer のワークフローにおいて、GPUを使うタスクで発生した Google Cloud のGPU枯渇問題と、その解決のために行った対策を紹介します。 ZOZOが運営する ZOZOTOWN ・ WEAR では、特定の商品やコーディネート画像に含まれるアイテムの類似商品を検索する 類似アイテム検索機能 があります。本記事ではこの機能を画像検索と呼びます。 画像検索では類似商品の検索を高速に行うため、画像特徴量の近傍探索Indexを事前に作成しています。近傍探索Indexはワークフローを日次実行して作成しています。 このワークフローでは大きく次のように処理を行っています。 当日追加された商品の情報を取得し、商品情報をもとに商品画像を取得する。 物体検出器で商品画像から商品が存在する座標とカテゴリーを検出する。 検出した座標で、商品画像を切り抜き、画像の特徴量を抽出する。 特徴量からカテゴリーごとに、 Spotify が開発したPython製の近傍探索ライブラリである Annoy を使って近似最近傍探索Indexを作成する。 我々はこのワークフローをCloud Composer上に構築しています。Cloud ComposerとはGoogle Cloudにおける Apache Airflow のマネージドサービスです。Cloud Composerには大きく Cloud Composer 1 と Cloud Composer 2 があり、画像検索のワークフローはより新しいCloud Composer 2に移行済みです。 Apache Airflowでは個別の処理をタスクとして記載し、タスク同士の依存関係を有向非巡回グラフ( Directed Acyclic Graph : DAG)として記載することでワークフローを定義します。本記事の中でもワークフローのことはDAGと記載します。 画像検索の近傍探索Indexを作成するDAGのなかで、物体検出と特徴量抽出のタスクはMLモデルを利用しており高速化のためにGPUを使用しています。 Annoyにつきましては、弊社テックブログの 近傍探索ライブラリ「Annoy」のコード詳解 もご参照ください。 今回はこのDAGを運用する中で発生した、Google Cloud内部のGPUリソース枯渇による課題と、その解決のために行った対策について説明します。 目次 はじめに 目次 GPUを使用する構成 GPUを割り当てたNodeが起動しないことによるタスクの失敗 タスクが失敗した原因の調査 サポートチームの回答と提案された暫定対応 Google Cloud内部のGPUリソース枯渇への対策 対応1 GKEクラスタのロケーションタイプをゾーンからリージョンに変更する 対応2 Composerタスクのリトライ間隔を最適化する 対応3 各タスクが使用するGPU数を最適な値に調整する 対応4 同時に実行するタスク数を制御し必要なGPU数を更に下げる 対応5 前日のDAGが完了するまで翌日のDAG実行を遅延する 対応6 前日のDAGが正常に完了していることを確認する まとめ 終わりに GPUを使用する構成 まずDAGのタスクでGPUを使用する際のCloud Composer 2の環境構成について説明します。 Cloud Composer 2の環境は Google Kubernetes Engine (GKE)の Autopilot モードの VPCネイティブ クラスタを利用して構築されます。 Airflowはタスクのスケジュールを行うスケジューラ、管理画面を提供するウェブサーバー、各タスクを実行するワーカーのコンポーネントにより構成されます。 各コンポーネントはCloud Composerが動作するクラスタ内のPodでコンテナとして実行されます。ワークロードの構成でCPU、メモリ、ストレージ、スケーリング台数を指定可能ですが、GPUの指定はできません。このように2023年9月現在Cloud Composer 2は GPUの利用をサポートしていません。 Cloud Composer 2環境の詳細については次の公式ドキュメントを参照してください。 Cloud Composer 環境のアーキテクチャ Cloud Composer 環境を作成する そのためCloud Composer 2でGPUを使ったタスクを実行するには、ワーカーとは別にGPUが利用可能なインスタンスを用意する必要があります。画像検索では別途構築したクラスタにGPUが利用可能なNode poolを用意して、GPUが必要な処理をオフロードしています。 Cloud Composer 2の GKEStartPodOperator を使用することで、Cloud Composer 2からGKEクラスタのPodを起動できます。ワーカーはPodを起動、監視しGPUが必要な処理はワーカーが起動したPodにて行います。 Podを起動する先として、Composerが動作するGKEクラスタとは異なるGKEクラスタも指定可能です。画像検索においても、Cloud Composer2が動作するGKEクラスタとは異なるGKEクラスタを指定しています。 GPUを割り当てたNodeが起動しないことによるタスクの失敗 ここからは画像検索のDAGを運用する中で発生した具体的な問題について述べます。 プロダクション環境のDAGにおいて、物体検出のタスクが失敗し、DAGが正常に終了しない問題が発生しました。物体検出は上述したGPUを使用するタスクの1つです。本節ではこのタスクの失敗について、実際に行った原因調査と暫定対応についてご説明します。 タスクが失敗した原因の調査 まず失敗の原因が環境起因か切り分けるために、プロダクション環境以外でも同様の問題が発生していないか調査しました。画像検索ではプロダクション環境の他に開発環境とステージング環境が存在します。ステージング環境ではプロダクション環境と同じDAGを日次実行しています。ステージング環境を調べると同様の問題がプロダクションと近い時刻に発生していることがわかりました。また並列化した全カテゴリーで同様にタスクが失敗していることがわかりました。 AirflowのWeb UI上から失敗を起こした物体検出のタスクが出力したログを確認すると次のエラーログが確認できました。 {pod_manager.py:310} WARNING - Pod not yet started: detect-object-pants-v3-830zvw9r {pod_manager.py:310} WARNING - Pod not yet started: detect-object-pants-v3-830zvw9r {pod_manager.py:310} WARNING - Pod not yet started: detect-object-pants-v3-830zvw9r {pod.py:716} INFO - Deleting pod: detect-object-pants-v3-830zvw9r {taskinstance.py:1770} ERROR - Task failed with exception Traceback (most recent call last): File "/opt/python3.8/lib/python3.8/site-packages/airflow/providers/cncf/kubernetes/operators/pod.py", line 548, in execute_sync self.await_pod_start(pod=self.pod) File "/opt/python3.8/lib/python3.8/site-packages/airflow/providers/cncf/kubernetes/operators/pod.py", line 510, in await_pod_start self.pod_manager.await_pod_start(pod=pod, startup_timeout=self.startup_timeout_seconds) File "/opt/python3.8/lib/python3.8/site-packages/airflow/providers/cncf/kubernetes/utils/pod_manager.py", line 317, in await_pod_start raise PodLaunchFailedException(msg) airflow.providers.cncf.kubernetes.utils.pod_manager.PodLaunchFailedException: Pod took longer than 1200 seconds to start. Check the pod events in kubernetes to determine why. Pod took longer than 1200 seconds to start. のログからPodが起動時にタイムアウトしたことにより、タスクが失敗となった事がわかります。 PodのタイムアウトについてはCloud Composer 2の公式ドキュメントで KubernetesPodOperator のトラブルシューティングガイド に記載があります。 GKEStartPodOperator のPodが起動するデフォルトタイムアウト時間は120秒ですが、物体検出タスクではモデルのロードに時間がかかるため次のように1200秒を設定しています。 GKEStartPodOperator( startup_timeout_seconds= 1200 , ... ) Podが起動しなかった原因について、再現性があるものなのか確認するためタスクをリトライしました。 通常であればPodが起動するのに十分な時間が経過しても、 {pod_manager.py:310} WARNING - Pod not yet started: のログが出力され続けていました。 物体検出の処理を行うオフロード先のGKEクラスタに対して Kubectl get pods を実行してPodのステータスを確認したところ、次の結果が出力されました。 NAME READY STATUS RESTARTS AGE detect-tops 0/1 Pending 0 60m detect-pants 0/1 Pending 0 60m ... NodeのNodeAffinity、Toleration、コンテナに割り当てるResourceなど Pending の原因になりそうな項目は確認しましたが、問題ありませんでした。 Podのステータスについての詳細はKubernetes公式ドキュメントの Pod Lifecycle をご参照ください。 上記でステータスが Pending となっているPodの1つに対して kubectl describe pod コマンドを実行し、Podの詳細な情報を確認しました。実行中のPodのデバッグについては 実行中のPodのデバッグ をご参照ください。 kubectl describe pods の出力結果のうち Events フィールドに次のEventが確認できました。 Events フィールドにはPodの直近の Event が表示されています。Kubernetesの Event はスケジューラによって行われた決定や、PodがNodeからEvictされた原因など、クラスタ内部で起こっている情報を提供します。 Node scale up in zones asia-east1-a associated with this pod failed: GCE out of resources. Pod is at risk of not being scheduled. Node scale up in zones asia-east1-a associated with this pod failed: GCE out of resources, GCE quota exceeded. Pod is at risk of not being scheduled. 0/2 nodes are available: 2 node(s) didn't match Pod's node affinity/selector. GCE out of resources. との出力から、 Google Compute Engine (GCE)のリソースを超えているとわかります。続く Pod is at risk of not being scheduled. の出力から、Podはスケジュールされていない可能性があるとわかります。 続くログに GCE quota exceeded と出力されています。メッセージからはGCEの Quota (割り当て)を超過していると受け取れます。 さらに 2 node(s) didn't match Pod's node affinity/selector. が出力されています。このため、Podで設定した affinity/selector と一致するNodeのうち割り当て可能なものがなかったとわかります。 GCEでは割り当てによりGoogle Cloudプロジェクト単位で割り当てるリソースの上限が設定されています。割り当ての上限を超えるとGoogle Cloudプロジェクトへそれ以上のリソースが割り当てられなくなります。プロジェクトに割り当てられた割り当ての上限と、現在の使用量はナビゲーションメニューから IAMと管理 を選び 割り当て から確認可能です。 上述のログから割り当ての上限に達したことを想定していましたが、割り当ての状態を確認するとリソースの使用量は上限値まで余裕がある状態でした。またPodがデプロイされる Node pool のマシンタイプ等を変更したわけでもないため、Podはこれまで正常に動いていたスペックと同じスペックのNodeで実行されるはずです。 続いてGoogle CluodのWebコンソールからGKEのプロダクトページを開き、対象のPodが動作するクラスタの詳細を確認すると次の表示が出ていました。 表示の詳細を開くと表示の詳細を開くと次のメッセージを確認できました。 また詳細からログのリンク先に移動すると、 Cloud Logging のLogs Explorerから次のエラーメッセージを確認できました。 これらの調査から今回の問題の原因は、DAGの実装や構築したインフラの設定など開発者側の問題ではない可能性が高いと判断し、 Cloudカスタマーケア にてサポートケースを作成しました。 サポートチームの回答と提案された暫定対応 Google Cloudのサポートチームからの回答により、事象が発生していた時間帯において、オフロード先のGKEクラスタが存在する asia-east1-a ゾーン全体で nvidia-tesla-t4 インスタンスのリソースが一時的に枯渇していたことがわかりました。 対応方法として別の時間帯でタスクをリトライし、正常に動作するか確認することを提案されました。 提案に従ってリトライを繰り返すと、問題の発生から4時間ほど経って一部のカテゴリーで nvidia-tesla-t4 インスタンスを割り当てたNodeが起動しました。その後Podがスケジュールされステータスも Running となりました。 一方で引き続きPodがスケジュールされず Pending のままになっているカテゴリーもありました。このことから、 nvidia-tesla-t4 インスタンスのリソースはまだ完全に充実していないことが推測できます。 そこで同時に使用するGPUのリソース量を少しでも減らすため、先に Running となったタスクが完了してPodが終了するのを待ってから、 Pending となっているタスクをリトライしました。するとPodがスケジュールされ、 Running になりました。 タスクの状態をAirflowのWeb UIで監視し続け、タスクが完了するたびに失敗となっているカテゴリーのタスクをリトライすることで、全カテゴリーの物体検出を完了させることができました。 ところがGPUの枯渇はその後も頻発しました。上記の方法により、GPUが枯渇するたびに人力でタスクの状態を監視してリトライを行うのは運用コストが大きすぎます。また人力での対応では復旧までの時間もかかってしまいます。そこで割り当て可能なGPUが枯渇し物体検出タスクを実行できない問題について、恒久的な対応を実施しました。 次章では実施した恒久対応について説明します。 Google Cloud内部のGPUリソース枯渇への対策 GKEPodOperator はGPUが必要なタスクを実行するときに動的にNodeのリソースを確保します。タスクが終わるとNodeも終了するため確保されたリソースが開放されます。 一方で Compute Engine ゾーンリソースの予約 を利用することでリソースを確保し続けることもできます。 しかしながら日次で実行されるDAGにおいて物体検出は30分から2時間程度で完了します。また、商品カテゴリー単位で物体検出のタスクを並列に実行しているため、同時に多くのGPUを必要とします。画像検索では現在対応カテゴリーが13カテゴリーあり、各カテゴリーごとに4GPUを割り当てており計52GPUを使用しています。 もし、52GPUを一日中確保し続けると、タスクが実行されていないほとんどの時間帯ではGPUが使用されないため、無駄なコストが発生してしまいます。そこでリソースの予約は行わずGPUリソース枯渇問題に対応する必要がありました。 対応1 GKEクラスタのロケーションタイプをゾーンからリージョンに変更する GKEクラスタが動いているCompute Engineのリソースは世界中のロケーションごとにリージョンという単位で配置されています。リージョンにはゾーンというリソースの論理グループが3つから4つ用意されています、リージョン内のそれぞれのゾーンは広帯域のネットワークで接続されています。ゾーンごとに冷却インフラなどをグループ化することで、各ゾーンの障害が異なるゾーンに影響しづらくなるよう設計されています。 GKEクラスタのロケーションタイプには、リージョンタイプとゾーンタイプの2タイプがあります。ロケーションタイプにリージョンを指定することでクラスタは特定のゾーンに制限されなくなります。これによりCompute Engineはリージョン内の複数ゾーン間でリソースを適切に割り当てることができるようになります。 以前のバージョンである Cloud Composer 1 の GKEPodOperator ではロケーションにゾーンしか指定できませんでした。Cloud Composer 2になり、ロケーションにリージョンも指定できるようになりました。画像検索は構築時に Cloud Composer 1 を使用していたため、これまでオフロード先にゾーンタイプのGKEクラスタを使用していました。画像検索は昨年Cloud Composer 2へのバージョンアップを行ったことで、リージョンタイプのGKEクラスタも使用可能となっています。そこで、オフロード先のGKEクラスタをリージョンタイプに切り替えます。 GKEクラスタのロケーションは一度構築すると変更できません。そのためリージョンタイプのロケーションを持つGKEクラスタを用意するにはGKEクラスタの再作成が必要です。 GKEクラスタを作成時にリージョンを指定することで、ロケーションタイプがリージョンのGKEクラスタを作成できます。 GPUに関しては全ゾーンにすべてのGPUプラットフォームのリソースが存在するわけではなく 利用できるゾーンが限定されている ため注意が必要です。今回は NVIDIA Tesla T4 を使用するため デフォルトのノードのロケーション に asia-east1-a と asia-east1-c を指定します。 これによりオフロード先のGKEクラスタをリージョンタイプで再作成できました。 Cloud Composer 2からリージョンタイプのGKEクラスタへオフロードするには GKEStartPodOperator の location パラメータへゾーン名に変わってリージョン名を指定します。 GKEStartPodOperator( location= "asia-east1" , ... ) この対応により複数ゾーンのGPUを利用できるようになり、 asia-east1-a のGPUインスタンスリソースが枯渇しているときでも asia-east1-c のリソースを使用可能になりました。 対応2 Composerタスクのリトライ間隔を最適化する Airflowはタスクが失敗すると、一定期間をおいて自動でタスクをリトライさせます。今回の物体検出タスクが失敗した際にも、Airflowは自動的にタスクをリトライしていました。 しかしGPUの枯渇が長時間に及んだことで、リトライ回数の上限を超えてしまいタスクが失敗していました。失敗とマークされたタスクは、それ以降は自動的にリトライされなくなります。その場合手作業でタスクをリトライしなければなりません。 GPUの枯渇と回復は前触れなく発生するため、手作業での対応は手間と時間がかかり望ましくありません。タスクが失敗していることに気が付かなければリトライされない危険もあります。GPUの枯渇は週末に発生することが多く、タスクの失敗に気が付きにくいため問題はより深刻でした。 そこで、リソースが回復されるまでより幅広い時間に渡ってリトライするよう設定の見直しを行いました。それにはリトライ回数を増やすか、リトライ間隔を広げる方法が有効です。ただしこれらの対応にはそれぞれ弊害もあります。 リトライ回数を増やすと問題発生時、余計なコストを発生させる場合があります。一般的にタスクの失敗はリトライのみで解決するとは限りません。今回の原因以外にも入力データの不備により失敗することもあります。この場合データを整備して原因を解消してからリトライしなければ、タスクは失敗とリトライを繰り返してしまいGPUリソースを長時間使用し続けることになります。 リトライ間隔を広げるとタスクが失敗した時の復旧に時間がかかるようになります。一時的な問題が原因でタスクが失敗した場合など、直後にリトライすることで解決する問題も多くあります。長すぎるリトライ間隔はこのような場合に復旧が遅れる原因となってしまいます。 そのためまずは短い間隔でリトライし、リトライ時にもタスクが失敗した場合はリトライ間隔を広げていく Exponential backoff を行います。 タスクのリトライで Exponential backoff を有効にするには、Airflowの retry_exponential_backoff を設定します。 retry_exponential_backoff に True を設定すると、リトライ回数に応じてリトライ間隔が指数関数的に増加します。例えばリトライ間隔に60秒を設定した場合、最初のリトライは60秒、2回目は120秒、3回目は240秒となります。これにより、直後にリトライすれば解決する偶発的な問題では最初に短い間隔でリトライが行われます。最初のリトライで解決しなかった場合はリトライ間隔が広がることで、リトライ回数を増やすことなく、長期間に渡ってリトライが行われます。 retry_exponential_backoff の指定はDAGを定義するスクリプトに記述します。 retry_delay を60秒とし、 retry_exponential_backoff を True に設定するには次のように記述します。 dag = models.DAG( "YOUR_DAG_NAME" , default_args={ "retry_delay" : 60 , "retry_exponential_backoff" : True , ... ) 注意すべき点としてリトライ間隔が指数関数的に増加するため、リトライ回数が増えると間隔が必要以上に広がりすぎてしまうことがあります。例えばリトライ間隔を60秒に設定している場合、10回目のリトライではリトライ間隔が8時間以上まで広がってしまいます。これではリソースが空いてもすぐにリトライが行われず、再度割り当て可能なリソースがなくなってしまう恐れもあります。 このような事態を避けるために、 retry_exponential_backoff に合わせて max_retry_delay を設定します。これによりリトライ間隔に上限を設定できます。 dag = models.DAG( "YOUR_DAG_NAME" , default_args={ "retry_delay" : datetime.timedelta(minutes= 1 ), "max_retry_delay" : datetime.timedelta(minutes= 30 ), "retry_exponential_backoff" : True , ... ) 今回は max_retry_delay に30分を設定したことで6回目のリトライ以降はリトライ間隔が広がらず30分ごとにリトライされます。 これによりリトライ回数を大きく増やすことなく、リトライを幅広い時間帯で行えるようになり、GPUのリソースが枯渇した場合にもリトライで復旧できる可能性が高まります。 対応3 各タスクが使用するGPU数を最適な値に調整する タスク自体を見直し、同時に必要なGPU数を減らすことでGPUの枯渇が発生した場合の影響を低減します。 前述のとおり画像検索では13カテゴリーそれぞれで4GPUを使用し物体検出のタスクを行っており、一度に52GPUをリクエストしていました。このように大量のGPUを使用していたのは、インスタンスの使用コストがリソースのGPU数だけでなく使用時間にも比例するためです。1GPUで52時間かけて処理を行うのも、52GPUで1時間かけて処理を行うのも、どちらも利用するGPUのリソース量は52時間となります。一方で処理時間は52時間から1時間に短縮できます。このためリソースが充分に使用できる場合においては一度に大量のGPUを使用するのは妥当な方法と言えます。 しかしNodeに割り当て可能なGPUが枯渇した状態では、大量のGPUをリクエストするとタスク自体が動作しません。そのため上記の方法を見直し、同時に使用するGPU数を削減しました。 単純にGPUの数を減らすと、処理時間が犠牲になってしまいます。処理時間を犠牲にしないためカテゴリーごとの商品数に着目しました。ZOZOTOWNで取り扱う商品数はカテゴリーごとに差があり、必要な処理時間も異なります。商品数が少ないカテゴリーは物体検出が早く終わり、商品数が多いカテゴリーの物体検出が終わるまで待ち状態となっていました。 そこで商品数が少ないカテゴリーの物体検出で使用するGPU数を減らします。 直近の商品数を調べるとトップスカテゴリーの商品数が1番多く、続くパンツはその半分より少なく、それ以外の各カテゴリーはパンツのさらに半分以下でした。 そのためトップスにはこれまで通り4GPUを割り当て、パンツはその半分の2GPU、それ以外のカテゴリーは1GPUを割り当てることにします。カテゴリーによってはこれまでの4倍の時間がかかりますが、元々商品数が多いカテゴリーの物体検出が終わるまでは待ち状態となっていたため、トータルの処理時間は変わりません。 これにより全体の処理時間に影響なく、同時に割り当てるリソースを最大52GPUから17GPUにまで減少させることができました。 対応4 同時に実行するタスク数を制御し必要なGPU数を更に下げる GPU枯渇後の復旧時には全カテゴリーのタスクを同時に実行できず、一部カテゴリーのPodは Pending となっていました。このため先に実行されているタスクの終了を待機し、終了したらPodが Pending となっているタスクをリトライする必要がありました。この方法はGPU枯渇の対策に効果的でしたが、人力の作業であったため運用面での問題となっていました。 この問題を解決するため全カテゴリーのGPU使用量に応じて、自動的にタスクを実行する仕組みを構築します。対策3により使用するGPU数はカテゴリーごとで異なっているため、それも考慮して同時に実行するタスクを制限します。 タスクの並列数を自動的に監視し、制御するために、Airflowの Pool を利用します。PoolはAirflowのスケジューラがタスクの実行を管理するために使用する仮想的なリソースです。 Airflow全体で使用できるPoolとPoolが持つSlot数を設定し、タスクには使用するPoolと消費するSlot数を指定します。タスクが開始されると、指定されたPoolのSlotが消費されます。タスクが終了するとPoolのSlotが復活します。 スケジューラはタスクの消費するSlotがPoolに残っていない場合にはタスクの開始を待機します。その場合でもタスクは失敗となりません、そのためリトライ間隔が広がったり、リトライ回数を超えることはありません。他のタスクが終了し待機していたタスクの消費するSlotがPoolに確保されるとタスクは開始されます。 これにより17GPU未満しか割り当たらない場合でも、他タスクのGPU使用量を考慮してタスクをスケジュールできます。 今回は gpu_pool というPoolを設定し、同時に割り当て可能なGPU数をPoolのSlot数として設定します。 AirflowのWeb UIからメニューのAdminを選びプルダウンのPoolsを選ぶと、Poolsの設定画面が表示されます。 Composer 2の標準ではdefault_poolというPoolが設定されており、Slotsに100000が設定されています。+ボタンをクリックすると新しいPoolを追加できます。 PoolにPool名、Slotsに使用可能なSlot数を指定します。今回はgpu_poolというPoolを追加し、割り当て可能なGPU数をSlot数として指定します。 次に GKEStartPodOperator のパラメータ pool に使用するPool名を指定し、 pool_slots に使用するGPU数(トップスは4、パンツは2、それ以外は1)を指定します。 GKEStartPodOperator( ... pool= "gpu_pool" , pool_slots= 1 ) カテゴリーごとに使用するGPUの数に合わせて消費するSlot数を指定することで、タスクで使用するGPU数に応じて同時実行数が調整されます。 例えばSlotに4が指定されているときは、トップスの物体検出中は他カテゴリの処理は開始されません。トップスの処理が終わると、パンツとその他カテゴリが2カテゴリー同時に実行されます。 これにより人力による監視を必要とせず、使用可能なGPUを有効に使えるようタスクを順番にスケジュールし、全タスクを完了できるようになりました。 対応5 前日のDAGが完了するまで翌日のDAG実行を遅延する GPUがNodeへ割り当てられるのに時間がかかるようになったことで、DAGの総実行時間が1日を超える日が出てきました。 画像検索では前日のDAGが作成した結果のデータを利用して、翌日のDAGでは新たに追加された商品のみを差分で計算することで、処理量を減らす差分実行の仕組みを導入しています。そのため前日のDAGがデータを作成する前に翌日のDAGが実行されると、翌日のDAGは前日のDAGが作成したデータを利用できなくなってしまいます。この問題を防ぐために、前日のDAGが完走するまで、翌日のDAGが実行されないようにします。 これを実現するためにAirflowの max_active_runs_per_dag を使用します。 max_active_runs_per_dag は同一のDAGが同時に実行できる数を制限します。これに1を指定すると、同一のDAGが複数起動しなくなるため、前日のDAGが完了するまで翌日のDAGは開始されなくなります。 max_active_runs_per_dag を設定するにはコンソールのナビゲーションメニューから Composer を開き、名前を選択して Airflow構成のオーバーライド タブで編集ボタンを押下します。 AIRFLOW構成のオーバーライドを追加 を押下し、セクションで Core 、キーには max_active_runs_per_dag を選んで値を 1 に設定します。 これにより前日のDAGが完了する前に、翌日のDAGが実行されてしまう問題を防止できるようになりました。 対応6 前日のDAGが正常に完了していることを確認する max_active_runs_per_dag は同時に実行するDAGの数を制限するだけで、DAGの成否について考慮しないことに注意が必要です。もし前日のタスクが失敗し、DAGが Failed で終了しても、他に実行しているDAGがなければ翌日のDAGは実行されてしまいます。 これでは前日のデータが存在しない状態で翌日のDAGが動いてしまう問題を完全には防止できません。そこでDAGの冒頭で前日のDAGが Success で完了していることを確認するタスクを追加します。 DagRun.find() を使用することで他のDAGの状態を取得できます。自身のDAG_IDと一致するDAGの状態を取得し、実行日の降順でソート、今回の実行日より前に開始された最初のDAGを調べることで、前回実行されたDAGのステータスを取得できます。前回実行されたDAGのステータスが Success でない場合にはタスクを失敗としDAGの処理がそれ以上実行されないようにします。 def check_yesterday_state (**context): ds = datetime.datetime.strptime((context[ "ds" ]), "%Y-%m-%d" ).astimezone(datetime.timezone.utc) dagruns = DagRun.find(dag_id=<DAG_ID>) dagruns.sort(key= lambda x: x.execution_date, reverse= True ) for dagrun in dagruns: if dagrun.execution_date < ds: if dagrun.state == State.SUCCESS: return else : raise Exception ( "Previously ( Usually yesterday ) started DAG has not completed yet." ) これにより前日のDAGが失敗したときに翌日のDAGが実行されるのを防ぐことができました。 まとめ 本記事ではCloud Composer 2でGPUを使うタスクで発生したGPU枯渇問題とその対策について紹介しました。 リージョンタイプのGKEクラスタへの移行と、各タスクが使用するGPU数を最適な値に調整したことにより、GPU枯渇を大きく減らすことができました。 本番環境において発生したGPU枯渇は、対策前の30日間で24回発生していたのに対し、対策後の直近30日では6回に抑えられています。またその6回いずれの場合においても自動的にリトライが行われ手作業を必要とせずDAGを正常終了できています。 ステージング環境でDAGが正しく動作しなかった際には、翌日のDAGが実行前に停止しており、差分データを取得できない問題も未然に防ぐことができました。 今後も、さらなる改善により低コストで安定したシステムを構築していきたいと考えています。 終わりに 最後までお読みいただきありがとうございました。 ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、次のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
アバター
はじめに こんにちは! ZOZOTOWN開発本部のAndroid開発チームです。 2023年9月14日から16日にかけて「 DroidKaigi 2023 」が開催されましたね! 今年ZOZOはPLATINUM SPONSORSとして協賛し、オフライン会場にてスポンサーブースの出展をしました。 technote.zozo.com 今年は昨年を上回る多彩な企画や取り組みを展開しました。 クイズやアンケートなど、皆様にご参加いただく企画を実施しました。これらの集計結果や、ブース準備の様子など、ここでしか読めない裏話をご用意しました! ぜひ、最後までお楽しみに! 目次 はじめに 目次 公式アプリへのコントリビューション ZOZOエンジニアが1名登壇しました スポンサーブース運営までの裏側:DroidKaigi 2023の取り組み ブースコンセプトの策定 ブースを支える体制の紹介 情報共有(コミュニケーション) マインドマップ トークスクリプト ブース当番のオペレーションシート ノベルティの選定 ブース運営準備の振り返り アンケート結果 Androidでおすすめの開発便利ツールはありますか? 服やコスメの購入時に重要視するポイントは? DroidKaigiのブースで欲しいノベルティはどれですか? クイズの結果 来場者からの反応 今年も開催しました! After DroidKaigi セミナー・カンファレンス参加支援制度 まとめ 公式アプリへのコントリビューション 今年はAndroid版に6名、iOS版に1名と公式アプリへのコントリビューションを積極的に行いました。こちらに関しては 別記事 にてご紹介しておりますので、合わせてご覧ください! ZOZOエンジニアが1名登壇しました DevRelブロックの @wiroha が、「 よく見るあのUIをJetpack Composeで実装する方法〇選 」というタイトルで登壇しました! 「よく見るあのUIをJetpack Composeで実装するとどのくらいの時間がかかるのだろうか?」というお悩みを解決すべく、よく見るUIの実装方法についてアニメーション動画やコードを交えて詳しく解説しています。 docs.google.com このセッションでは初心者から経験者まで、幅広い層に対してわかりやすく知見が解説されました。資料もアップロードされているので、Jetpack ComposeでのUI実装を予定している方は是非、ご覧ください! スポンサーブース運営までの裏側:DroidKaigi 2023の取り組み 協賛ブース運営の全体進行を務めました高田です。ここからは協賛ブース運営を支えた体制や、得られた経験、そしてリードする立場での全体進行についてお伝えします。 ブースコンセプトの策定 まずはブースのコンセプトを策定しました。来場者の皆様に対して「何を届けたいのか?」というブース出展の根幹について今一度考えて整えることで、提供する価値の最大化を図りました。 本年度は、下記の3つを届けたい! という思いを、ブースのコンセプトとして策定しました。 ZOZOのサービス 社内制度 コスメ販売への取り組み ブースを支える体制の紹介 毎年、ZOZOではAndroidエンジニアが主だってブース準備を進めています。今年は3名で臨み、分業による効率化を図るために役割を分けました。その役割は下記のとおりです。 旗振り担当:全体の進行や方針決定、連携を主に担当 ブース担当:ブースの企画や、運営方法、展示物の検討を主に担当 ノベルティ担当:ノベルティ見積もりや発注を主に担当 また、Androidエンジニアだけでなく、様々な部署に協力を仰いで臨みました。今回一緒に協力してくれた部署は下記のとおりです。 技術戦略部、DevRel:DroidKaigi運営との連携、PR活動、イベント出展に関する物事全般(資材搬入、法務確認など)の調整を担当。 デザインチーム:ブースでのビジュアルコンテンツの制作、ノベルティのデザインを担当。 このようにたくさんの人達によって、今年のブース運営は支えられておりました! 情報共有(コミュニケーション) 弊社のエンジニアは基本的にリモートワークです。当日のオペレーションはどのように回すのか? ノベルティやコンテンツの制作はどこまで進んだか? など、共有すべき事項はたくさんあります。リアルなアウトプットが絡むイベントですから、いつものアプリ開発と同じコミュニケーションだけでは中々思うように進みません。 イベントの成功のためには、あらゆるコミュニケーションに工夫が必要でした。その一部をご紹介します。 マインドマップ イベントに対する認識は人によって異なります。ZOZOのブースを運営するためにはまず、やらなければいけないことを網羅する必要がありました。今回はその手段としてMiroを用いてマインドマップを作成しました。 ※かなり大きい図になってしまったため、一部のみのご紹介といたします。 複数人という体制でマインドマップを用いることで、短期間で網羅性の高い情報を書き出すことができました。そのおかげで、計画し損なった作業による手戻りは一切発生しませんでした。 もちろん一人でも有効な手段ではあるのですが、「最初のうちに複数人でやる」ということに大きなアドバンテージを感じました。 トークスクリプト イベント当日のブースは準備メンバー3名に加えて、当日参加するAndroidチームメンバー約10名が当番制で運営にあたります。例年、ブースに訪れた方々は弊社のサービスや開発に対する質問をしてくださいますが、全員が同じクオリティの回答をできておりませんでした。せっかく来ていただいたのに、詳しいお話をご提供できないという事態を問題として捉えました。そこで、ブース当日の会話を想定したZOZOのプロダクト、ノベルティやブース内容に関しての回答をまとめたトークスクリプトを用意しました。 このドキュメントを全メンバーが携えつつブース当番にあたることで、ご提供するお話のクオリティを一定以上の水準に保つことができました。 ブース当番のオペレーションシート 今年は例年と比較して多くの取り組みを企画したので、準備メンバー以外のAndroidチームメンバーが混乱することを予見していました。皆さんにお渡しするノベルティはいつも5種類くらいですが、今年は10種類。加えてそれぞれ異なるコンテンツでお渡しするというルール。コンテンツもそれぞれ濃い内容で3種類あります。 そこでトークスクリプトと同様の考え方で、ブース当番のやることが一目で分かるオペレーションシートをMiroで用意しました。 イベント当日、最初は混乱するメンバーもいましたが、こちらのオペレーションシートを使うことでスムーズにブース対応ができていました。予見していた事態を無事避けることができてよかったです! ノベルティの選定 今回は様々な趣向を凝らしたノベルティをご用意させていただきました。例年お渡ししているペンや付箋、ZOZOMAT、ZOZOGLASSに加えて以下のラインナップを追加しました。 一期一会という四字熟語をもじった「一合一会」をコンセプトにしたお米一合 ZOZOのコスメへの取り組みを象徴したハンドミラー ZOZOが本社を構える西千葉のコーヒー店「Eureka Coffee Roasters」とコラボしたブレンドコーヒー LINEスタンプとしても配信中の「箱猫マックスVol.6(エンジニア編)」をそのまま使ったステッカー ZOZOTOWNのロゴがプリントされたショッパー 来年もお楽しみに! ブース運営準備の振り返り DevRelやデザイン部、技術戦略部など、多岐にわたる部署のご協力のもと無事にブース運営を終えることができました。事前のリハーサルやプランの練り直しも含め、実際の協賛ブース運営のノウハウを学ぶことができました。また、外部への発信だけでなく社内の一体感を深める良い機会となりました。今回得られた経験・知識を今後に活かして参ります。 アンケート結果 ZOZOTOWNアプリ部Android1ブロックの池田です。ZOZOのスポンサーブースでは、時間別に3つのアンケートを実施しました。会場のみなさんに回答していただいたアンケート内容を見ていきたいと思います。 Androidでおすすめの開発便利ツールはありますか? Androidアプリを開発していく上でどのような便利ツールをみなさんが利用しているのかアンケートしました。やはり先日、日本でも利用可能となったStudio BotやChat GPT、GitHub CopilotなどのAI関連のツールに注目が集まっていますね。他には、HyperionやKotlin Fill ClassといったAndroid開発ならではのツールを回答してくれた方もいらっしゃいました。初めて聞いたツールもあったので非常に参考になりました。 アンケート「Androidでおすすめの開発便利ツールはありますか?」の回答結果 服やコスメの購入時に重要視するポイントは? 普段、服やコスメを買うときに何を重要視しているかエンジニアや人事の方、学生の方など多くの方に回答していただきました。「素材」や「価格」を重要視している方が多いという結果になりました。普段、みなさんが何を重要視していて、何に困っているのかといった洋服やコスメに関するお話をたくさんできました。 アンケート「服やコスメの購入時に重要視するポイントは?」の回答結果 DroidKaigiのブースで欲しいノベルティはどれですか? エコバッグやミニタオルが欲しいという意見が多かったです。企業ブースでのノベルティやDroidKaigiのオリジナルグッズがたくさんもらえることもあり、それらを持ち運べるエコバッグが欲しいという声が多かったです。今後のZOZOのノベルティ制作の参考にさせていただきますので楽しみにお待ちください! アンケート「DroidKaigiのブースで欲しいノベルティはどれですか?」の回答結果 クイズの結果 ZOZOのスポンサーブースでは、ZOZOについてより知ってもらおうというコンセプトで5問のクイズを実施しました。クイズは2日間で約200人の方に参加していただき、その中で全問正解した方が20人いらっしゃいました! 参加していただきありがとうございました! ここでは、正答率の一番高かったクイズと低かったクイズを見ていきたいと思います。 正答率の一番高かったクイズは「コーディングチャレンジ」の問題で、71.7%でした! ZOZOでは社内LT大会が行われており、そのときに題材となったコードをクイズとして出題しました。こちらのクイズではブースの前に設置したボードからコードを読み込み、議論をしながら何が描画されるか推察していただく方が多かった印象です。このコードでは drawB が花火の1つ1つの花びらを表しており、それが円状に配置されていることから正解は3番の花火になります。コードチャレンジの問題の正答率が一番高い結果となり、参加していただいた方々の技術力の高さを感じることができました。 一方で一番正答率の低かったクイズは「ネーミングセンスを鍛える会」の問題で、43.4%となりました! このクイズはZOZOTOWN Androidチームで行なっている取り組みをブースに来た方にも体験していただきたいといった目的で出題しました。このクイズのポイントは、お題の最後に書かれている「入力リストが空の場合、エラーメッセージを出力します」といった条件があり、戻り値はResult型を返す必要がありました。そのためこのクイズは2番が正解となります。 ただし、この問題は必ずしも一意に定まるものではないので、正解が1つにならなければいけないクイズとしては考え直す必要がありそうでした。取り組み自体は好評でしたので、今後はクイズではなく来場した皆様と議論できるようなコンテンツとして、再検討する予定です。 来場者からの反応 DroidKaigi 2023のZOZOブースに多くの来場者が訪れ、特に以下の点が好評でした。 Jetpack Composeのクイズ:Androidエンジニアの方々から、業務に関連する質問が盛り上がったり、社内LTトークやネーミングセンスを鍛える会の紹介を興味深く受け取ってもらえました。 ノベルティの選定:「一期一会」のお米や、ZOZOCOSMEを意識した手鏡、千葉のドリップコーヒーなどのノベルティが参加者から大変好評でした。特にお米への感想や、コンセプトに共感してくださる方が多かったです。 ARメイク体験:現地でのARメイク体験は、特に参加者からの反響が大きく、ZOZOの最先端の技術を間近で体験してもらえた点が好評でした。 これらのフィードバックをもとに、次回のイベントやブースの運営に活かしていきたいと考えています。 今年も開催しました! After DroidKaigi 昨年に引き続き今年もLINE、ヤフー、ZOZOの3社合同でAfter DroidKaigiをオンラインで開催しました! 詳細につきましては 別記事 にて紹介しておりますので、こちらもチェックをお願いいたします! zozotech-inc.connpass.com セミナー・カンファレンス参加支援制度 ZOZOにはエンジニアのイベント参加をサポートする制度があります。 ZOZOはPLATINUM SPONSORとして協賛しているのでスポンサーチケットがありますが、チケット費用をはじめとし、以下の補助が受けられます。 希望するエンジニアは業務として参加 チケット費用、交通費は経費精算、遠方の場合は出張扱い可 (イベントが休日開催の場合)休日出勤扱いで参加 これらの制度があることで、イベントに参加しやすい環境が整っています。我々開発メンバーとしても、今回のイベント参加ハードルは非常に低かったです。 まとめ オフラインで参加された方も、オンラインで参加された方も、お疲れさまでした! 今年も楽しかったですね! また、DroidKaigi 2023を運営してくださったスタッフの皆様のおかげで、我々も安心してイベントに参加できました。細かなフォローや丁寧な説明でサポートしていただきありがとうございました。これからもAndroidアプリ開発を盛り上げてまいりましょう! 最後に、ZOZOではAndroidエンジニアをはじめ、一緒に楽しく働く仲間を募集中です。ご興味のある方は下記採用ページをご覧ください! hrmos.co
アバター
はじめに こんにちは、生産プラットフォーム開発本部の stakme です。 本稿では、スプレッドシートの作業に「手続き的なアプローチ」と「宣言的なアプローチ」という観点を持ち込み、ふたつを対比しながら紹介します。Google Sheetsの多彩な関数を駆使して、日常的な問題に効率的に対応するための具体的なテクニックやヒントを提供します。また注意点やリスクを指摘し、スプレッドシートをより強力に活用するための知識を提供します。 目次 はじめに 目次 背景・課題 本稿の目的 規則的な処理を繰り返すケース 手続き的に構築された例 宣言的に記述された例 SEQUENCE ARRAYFORMULA 関数の組み合わせ なぜ「宣言的」なのか データが徐々に増えるケース 手続き的に構築された例 宣言的に記述された例 別の見せ方でデータを表示したいケース 手続き的に構築された例 宣言的に記述された例 やりすぎのケース 手続き的に構築された例 宣言的に記述された例 まとめ 背景・課題 筆者は生産プラットフォーム開発本部に所属するソフトウェアエンジニアです。弊本部は「 Made by ZOZO 」を支えるシステム開発に従事しています。筆者の役割は、BigQueryを中心とする事業データ利活用の基盤を整えることを通じて、目標達成に向かうビジネス上の意思決定を支援することです。 直近の案件において、「人間による入力」に対する「システムによる評価フィードバック」をすばやく得ることで意思決定を効率化したいというケースがありました。入力インタフェース(手での入力やコピーペースト)の強力さ、フィードバック反映のスピードなどから判断すると、Google Sheetsが最適と考えられるケースでした。 Google Sheetsは非常に強力なスプレッドシート製品であり、データの入力、蓄積、加工、表示という一連の流れを単体で処理できます。ただし今回のように不定件数の入力データを加工するときには、若干の「慣れ」が必要となります。素朴に関数をコピーペーストして実装すると、データ増加に伴って関数がコピーされていないセルが生まれ、処理が壊れてしまうのです。 振り返ってみると、ここで筆者が苦しんだ原因は「何を実現したいのか」ではなく「どのセルでどのような演算をするか」という抽象度の低い内容をそのまま記述してしまった点にあると思います。プログラミングでいうと、for文を使わずに何千回もメモリを直接操作しているような状態でしょうか。そう喩えてみると、いかにも壊れやすそうですね。プログラミングでは避けられることがスプレッドシートでは上手く回避できない、という部分にこの「慣れ」の要素があると思います。 本稿の目的 本稿は、そのようなスプレッドシートにおける「慣れ」の実体を叙述するために、プログラミングの領域からふたつの概念を借用します。 抽象度が低く、人間の管理に依存する、手続き的に構築されたスプレッドシート 抽象度が高く、データ構造に依存する、宣言的に記述されたスプレッドシート 両アプローチを比較しながら、具体的なGoogle Sheetsの利用テクニックを紹介します。この紹介を通じて、読者はGoogle Sheetsにおける実装の特徴について言語化する術を獲得し、場面によって適切なアプローチを意識的に採用できるようになるでしょう。 この目的を達成するため、手元で試して学べるサンプルを多く含めるようにしました。実際にGoogle Sheetsの動作を試しつつ、読み進めることをおすすめします! 規則的な処理を繰り返すケース 例えば以下のように、「A1の日付( 2023/01/01 )を基準として、その0日後、7日後、14日後… という日付を6回だけ生成して表示したい」とします。 手続き的に構築された例 このシートのB列は、下記のような関数で表現できるでしょう。 =A1 =B1+7 =B2+7 =B3+7 =B4+7 =B5+7 7日ずつ日付を進めたいので、 +7 を繰り返し記述しています。これを6回だけ行いたいので、6個のセルで関数を実行しています。とてもシンプルですが、誤って途中のセルを削除すると次の図のようになります。 削除したB3セルが空欄になるだけでなく、それ以降の日付は1900年1月になってしまいました 1 。あきらかに意図と異なる値ですが、日付データそのものは生成されてしまっており、問題に気づきにくい状態です。 宣言的に記述された例 最初に具体例を示します。B1セルにこのように入力してください。 =ARRAYFORMULA(A1 + SEQUENCE(6, 1, 0, 7)) このときB2からB6セルは空欄としてください。そうすると、B列に先ほどと同じ日付が表示されます。 この例に含まれている関数について、ひとつずつ説明します。 SEQUENCE まずは SEQUENCE(6, 1, 0, 7) という部分です。 SEQUENCE 関数 は、連続する数値を生成します。 SEQUENCE(6, 1, 0, 7) は、縦に1列で 0, 7, 14, 21, 28, 35 という6つの数値を生成します(下図のA列)。 設定を変えると、ゼロではない数値から始めたり、複数の列にわたる数値も生成できます。たとえば SEQUENCE(6, 3, 700, 7) を実行すると、下図のC列からE列のようになります。実際にさまざまな設定を試してみると、仕組みがよく分かると思います。 ARRAYFORMULA 次に ARRAYFORMULA です。 ARRAYFORMULA 関数 を理解するために、まずは下記の3つの例を確認してください。 ={10; 20; 30} =100 + {10; 20; 30} =ARRAYFORMULA(100 + {10; 20; 30}) {10; 20; 30} という記述は「縦に並んだ10、20、30という数値の組み合わせ」を表しています(1つのセルにこれを記述すると、そこから下に3つの数値が表示されます)。このような構造を配列(Array)といいます。配列と数値は異なるものであり、 100 + {10; 20; 30} というような足し算はできません。配列という特別な構造を踏まえつつ、その中身をひとつずつ処理するには ARRAYFORMULA 関数を利用します。 こちらも言葉で説明すると複雑ですが、実際の動作を比べると ARRAYFORMULA 関数の役割が分かると思います。 関数の組み合わせ このふたつを組み合わせた =ARRAYFORMULA(A1 + SEQUENCE(6, 1, 0, 7)) は、結局こんな処理を意味しています。 ①最初の状態 ARRAYFORMULA(A1 + SEQUENCE(6, 1, 0, 7)) ②SEQUENCE関数により、縦に並んだ6つの数値を生成 =ARRAYFORMULA(A1 + {0; 7; 14; 21; 28; 35}) ③ARRAYFORMULA関数により、縦並びの構造を保ちながらA1との足し算を実行 ={A1+0; A1+7; A1+14; A1+21; A1+28; A1+35} ④結果を表示 ちなみに、 MAP 関数 という別の関数を利用して =MAP(SEQUENCE(6, 1, 0, 7), LAMBDA(i, A1 + i)) などと書くこともできます(結果の見た目は同じです)。 なぜ「宣言的」なのか もともとこのケースは、「A1の日付を基準として、その0日後、7日後、14日後… という日付を6回だけ生成して表示したい」という明確な意図があると想定していました。手作業で =B2 + 7 と書いたり消したりしたのは、この意図をすばやく実現するための手続きを進めていたにすぎません。人間のミスは、このような手続きにおいて発生するものです。 だとすると、人間が手続きをやめれば、手続きのミスは問題しなくなるはずです。 人間は「こういう処理をしたい」と宣言だけして、あとの処理はスプレッドシートに任せてしまう。そうすれば、手続きでミスを起こして壊れる余地はなくなります。また、スプレッドシート作成者の意図がほかの人々にも理解しやすくなるでしょう。そこで本稿はこうしたアプローチを「宣言的な記述」と呼び、人間がデータ処理の流れをひとつずつ実装する「手続き的な構築」と対比することにしました。 データが徐々に増えるケース データが増えるたび、なんらかの処理を行いたいケースを考えます。 例えば、以下のように「処理済みのID」と「チェック対象のID」それぞれのリストがあります。あるIDが処理済みリストに含まれるかを VLOOKUP 関数 で調べたいとします。「チェック対象のID」はどんどん増えると想定します。 手続き的に構築された例 データが増えるたびにVLOOKUPをコピー&ペーストすることで対応できます。 =VLOOKUP(C2, $A$2:$A$4, 1, false) =VLOOKUP(C3, $A$2:$A$4, 1, false) =VLOOKUP(C4, $A$2:$A$4, 1, false) ... 宣言的に記述された例 手続き的なアプローチは、対象のIDが増えるたびに手作業が必要です。ただし、毎回まったく新しい作業をするわけでもありません。 C2, C3, C4, ... という部分だけ入れ替え、同じことを繰り返しています。 ということは、 {C2; C3; C4; ...} という無限に続く配列があれば、 ARRAYFORMULA 関数を利用して一気に処理できるはずです。Google Sheetsでは、このような配列を C2:C と表現できます。 C2:C は「C2からC列の最後まで」という意味です。ここでは、以下のように記述できます。 =ARRAYFORMULA(VLOOKUP(C2:C, $A$2:$A$4, 1, false)) これを実行すると、下図E列のようになります。 今はまだチェック対象のIDが存在しない行も含めて、C2より下にあるすべてのIDをチェックできています。 より読みやすい表示を実現するのであれば、 IF 関数 、 ISNA 関数 、 NOT 関数 を利用して改良できます。 =ARRAYFORMULA(IF(C2:C, NOT(ISNA(VLOOKUP(C2:C, $A$2:$A$4, 1, false))), "")) 別の見せ方でデータを表示したいケース すでにあるデータを、別の見せ方で表示したいことがあります。例えば、作成済みのテーブルの列を入れ替え、名前も変えて表示したいとします。 手続き的に構築された例 値をコピーして、手で入れ替え、「会員番号」「名前」と入力するだけです。今後のデータ更新を考慮しない場合であれば、この方法で十分です。 宣言的に記述された例 常に最新のデータを異なる見せ方で表示したい場合は、単純なコピーでは対応できません。 QUERY 関数 を利用することで「同じデータを、違う順番で、異なるラベルをつけて表示する」という処理を宣言的に記述します。 =QUERY(A1:B, "select B, A label A '名前', B '会員番号'") これをD1セル(D2ではないことに注意してください)に記述し、E1セルを空欄にすると、下図のようになります。 QUERY 関数を利用すれば、必要な列だけを再利用したり、特定条件のある行だけに絞り込んだり、データを加工したうえで表示したりできます。ただし、絞り込みや加工の自由度はあまり高くありません。 FILTER 関数 や SORT 関数 などの関数と使い分けるとよいでしょう。場合によっては、現在のデータをそのまま表示する ={A1:A} のような書き方が有効というケースもあるかもしれません。 なお QUERY 関数の生成する結果はそのまま ARRAYFORMULA 関数に渡すことができます。この組み合わせに慣れると、さまざまな処理を宣言的に記述できるようになります。 =ARRAYFORMULA(QUERY(A1:B, "select B where A = 'Alice'", 0) + 100) やりすぎのケース すでに明らかかもしれませんが、単純なコピー&ペーストで済む場面であれば、あえて複雑に実装する必要はありません。実装時のコストと必要性を天秤にかけて、適切な方法を選択することが重要です。その点に注意を促すため、実装・読解コストが高いと思われる例を紹介します。 数値の合計値を行ごとに求めて、右側のF列に表示したい場合を想定してください。 手続き的に構築された例 スプレッドシートに慣れた人であれば、迷うことはないと思います。 =sum(A1:E1) =sum(A2:E2) =sum(A3:E3) 意図が明瞭であり、非常にシンプルです。 宣言的に記述された例 宣言的に記述するなら、 このような書き方ができるでしょう。 ①SUMIFパターン =ARRAYFORMULA(SUMIF(IF(COLUMN(A1:E1), ROW(A1:A1000)), ROW(A1:A1000), A1:E1000)) ②MMULTパターン =ARRAYFORMULA(MMULT(A1:E3, SEQUENCE(COLUMNS(A1:E3), 1, 1, 0))) どちらも手続き的な記述より複雑です。なにより大きな問題は、実装の都合により SUM 関数が使われていないことです。 SUM 関数が存在しないため、「行の合計値を求める」という人間の意図が曖昧になっています。少なくともSUMIFパターンは設定すべきパラメータも多く、本当に必要なケースでしか利用すべきではありません。 このようなケースでは、シンプルさを重視して手続き的に構築することも合理的な選択肢です。宣言的に記述するとしても、MMULTパターンを 名前を付けた関数 として定義し、処理の意図をあきらかにしたうえで利用することをおすすめします(たとえば SumByEachRow など)。 まとめ 本記事では、Google Sheetsにおける処理アプローチを言語化するために「手続き的」「宣言的」というふたつの概念を持ち込み、それぞれの具体例や関数のリファレンスを示しました。これらが読者の皆さんの選択肢に加わり、スプレッドシートの利用がより効率的になることを願っています 2 。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。Made by ZOZOを支えるソフトウェアエンジニアのポジションも募集しています (Go, TypeScript)。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co hrmos.co Google Sheetsが内部的に「1899年12月30日=ゼロ」として管理していること、空白セルを数値として扱うとゼロになることが原因のようです。前者については、 Lotus 1-2-3互換に関連する挙動であるという説明 もあります。 ↩ 本稿における「手続き的・宣言的」という単語借用のアイデアについては、VisiCalcに対するテッド・ネルソンのコメントから刺激を受けました。“Where conventional programming was thought of as a sequence of steps, this new thing was no longer sequential in effect” ( Nelson, T. (1989). In S. Brand (Ed.), Whole Earth Software Catalog for 1986 (p. 66). Quantum Press/Doubleday. ) ↩
アバター
はじめに こんにちは。ブランドソリューション開発本部FAANSバックエンドブロックの佐野です。普段はサーバーサイドエンジニアとして、FAANSのバックエンドシステムを開発しています。 FAANSとは、弊社が2022年8月に正式ローンチした、アパレル店舗で働くショップスタッフの販売サポートツールです。例えば、コーディネート投稿機能や成果確認機能などを備えています。投稿されたコーディネートはZOZOTOWNやWEAR、Yahoo!ショッピング、ブランド様のECサイトへの連携が可能です。成果確認機能では、投稿されたコーディネート経由のEC売上やコーディネート閲覧数などの成果を可視化しています。 本記事では、成果データの集計処理におけるBigQueryのクエリ実行処理のユニットテストをGoで実装した取り組みと、その際の工夫についてご紹介します。 目次 はじめに 目次 成果データの集計処理とは 抱えていた課題 バグが発生しやすい 動作確認が煩雑になる 正しい動作を判断しづらい なぜSQLのテストをGoで書いたのか テストの実装 フィクスチャ テストケースの分割 悪い例 良い例 テストケースごとにテーブルを作成 Goのtemplateを使って接続先の差し替えを容易に 結果 開発効率が上がった バグが発生しにくくなった レビュアーの負担が減った QAの負担が減った 今後の展望 ボイラープレートの自動生成 QA用テストデータ作成の環境づくり さいごに 成果データの集計処理とは 本題へ入る前に、FAANSの成果データの集計処理について簡単にご説明します。 全社のデータ基盤のBigQueryには、例えばWEARやZOZOTOWNのユーザーが「コーディネート画像を閲覧した」といった様々な種類のビジネスイベントのデータが格納されています。それらのイベントデータから、コーディネート画像がどれくらい閲覧されたかや、コーディネート画像経由でどの商品がいくつ購入されたかといった様々な種類の成果のデータをバッチ処理で集計しています。 抱えていた課題 前述の通り、FAANSはZOZOTOWN・WEAR・Yahoo!ショッピング・ブランド様のECサイトと連携しているという特性上、複数のデータソースからデータを抽出する必要があります。 特に日次のバッチ処理で行っている成果データの集計処理では、複雑な条件によりSQLのクエリが長くなる傾向にあります。長いものだと10テーブルをJOINした上、WHERE句で8つほどの条件を指定しているため、100行以上になることもあります。 そのような複雑なロジックを持つクエリに関して、以下のような課題がありました。 バグが発生しやすい 長いクエリは読み解くのにコストがかかります。WHERE句ひとつとっても、背景を把握していないとなぜこの一文が必要なのかが伝わらない場合もあります。そのようなクエリに対して追加修正が必要となった際に、ロジックを読み違えて意図しない変更を加えてしまう可能性がありました。 動作確認が煩雑になる 動作確認の際にはその都度手作業でデータを用意しなくてはならず、また複数の条件に合致するデータを用意するだけでも確認事項が多いため、そこに多くの時間が取られてしまっていました。その結果、開発効率が落ちてしまったり属人化してしまうという状況にありました。 正しい動作を判断しづらい 100行にもわたるSQLのクエリは、どのような動きが正しいのかを判断しづらく、レビューコストが高いという課題もありました。SQL内のコメントで基本的なロジックについての説明ができても、様々なパターンのデータに対してどのように動作するかを詳細に伝えるのは困難でした。 なぜSQLのテストをGoで書いたのか FAANSでは、Web APIサーバーやバッチ処理といったバックエンドシステムの全てをGoで実装しています。また、今後も様々な分析データを提供するために、複雑なクエリを用いたデータ抽出処理は増えていくと考えられます。それらの理由から、チームの学習コストや開発生産性を考慮して、他言語のツールを導入するのではなくGoで書くことが適切と判断しました。 テストの実装 では、どのようにテストを書いたか説明していきます。 フィクスチャ 今回は、SQLを組み立ててBigQueryでクエリを実行する処理のユニットテストを実装しました。テストデータの投入はメルカリ社の記事( Goでテストのフィクスチャをいい感じに書く )を参考に用意したフィクスチャを使用しました。 テストケースの分割 テストケースの分割では、網羅性と凝集度を高めることを意識しました。例えば、売上の種類が3つ存在し、それぞれの合計額を取得する処理のテストを書くとします。 悪い例 func TestCalculateSalesAmount(t *testing.T) { f := fixture.Build(t, fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 1000 }), fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 2000 }), fixture.Sales( func (s *model.Sales) { s.Type = "B" s.Amount = 5000 }), fixture.Sales( func (s *model.Sales) { s.Type = "C" s.Amount = 3000 }), fixture.Sales... ) ... f.Setup(t) t.Run( "タイプA・タイプB・タイプCの売上データが取得でき、それぞれの合計額は3000円・5000円・10000円である" , func (t *testing.T) { ... if len (result) != 3 { t.Errorf( "売上データの件数に過不足がある" ) } if result[ 0 ].Type != "A" { t.Errorf( "タイプAの売上データが取得できていない" ) } if result[ 0 ].SalesAmount != 3000 { t.Errorf( "タイプAの売上データの合計額に誤りがある" ) } if result[ 1 ].Type != "B" { t.Errorf( "タイプBの売上データが取得できていない" ) } if ... }) } このように複数のパターンを一度にテストしようとすると、準備するべきデータや判定ロジックが増えてしまいます。すると、凝集度が低く見通しの悪いものとなってしまうため、ひとつのテストケースで確認する必要がないものは、以下のように分割しました。 良い例 func TestCalculateSalesAmount_TypeA(t *testing.T) { f := fixture.Build(t, fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 1000 }), fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 2000 }), fixture.Sales( func (s *model.Sales) { s.Type = "B" // 合算の対象にならないことを確認するために作成 s.Amount = 5000 }), ) ... f.Setup(t) t.Run( "タイプAの売上データが取得でき、合計額は3000円である" , func (t *testing.T) { ... if result[ 0 ].Type != "A" { t.Errorf( "タイプAの売上データが取得できていない" ) } if result[ 0 ].SalesAmount != 3000 { t.Errorf( "タイプAの売上データの合計額に誤りがある" ) } }) } 分割したことで準備するデータや確認項目も減り、テストケースの意図が明確になりました。このように分けることで、 TestXXX_TypeB 、 TestXXX_TypeC とテストコードが増えることにはなりますが、テストケースの凝集度を高めると、結果的にメンテナンス性も向上します。 テストケースごとにテーブルを作成 前項で示したように、複数のテストケースが存在していると、別のテストケースで用意したデータを参照してしまい、意図した結果とならないという問題が起きます。テストケース同士の関連やデータの競合について考慮しながらメンテナンスしていくのは難しいため、テスト用のヘルパーを用意してテストケースごとにテーブルを作成・削除するという方法をとりました。 まず、作成したいテーブルのスキーマをコピーして、テスト用のBigQueryのデータセットにテーブルを作成します。テーブル名はテストケース間で重複しないように、本来の名前のsuffixにランダムなIDを追加したものとしました。なお、必要なデータを投入してテストを実行した後、作成したテーブルを削除します。 テストケースごとにテーブルを分けたことにより、並列実行が可能になるというメリットもありました。実行時間の短縮のためにも、Goの標準パッケージである testing パッケージの t.Parallel() メソッドを使って、それぞれのテストケースを並列で動かすようにしました。 Goのtemplateを使って接続先の差し替えを容易に テスト対象の処理では、SQLはファイルに切り出し、Goの標準パッケージである text/template パッケージを用いて組み立てるようにしました。そうすることで、環境ごとに接続先のBigQueryのテーブルを切り替えることが可能になりました。また、指定するテーブルの情報はメソッドの外から渡すようにして、テスト時も差し替えがしやすい作りを意識しました。 SELECT ... FROM {{.project_id}}.{{.dataset_id}}.{{.table_id}} WHERE ... // templateを使ったSQLの組み立て func BuildSQL(path string , params any) ( string , error ) { body := & bytes.Buffer {} t, err := template.ParseFS(templates, path) if err != nil { ... } if err := t.Execute(body, params); err != nil { ... } return body.String(), nil } // 呼び出し側 sql, err := query.BuildSQL( "hoge.sql" , map [ string ] string { "project_id" : "hoge" , // GCPのプロジェクトID "dataset_id" : "fuga" , // BigQueryのデータセットID "table_id" : "piyo" , // BigQueryのテーブルID }) 結果 実際にテストを書いてみたところ、1つのクエリに対するテストで検証する項目が最大20パターン存在しました。これら全ての動作確認を手作業でデータを用意して行うのは非現実的です。また、テスト対象は一度しか実行されないクエリではなく定常的に実行されるもので、今後プロダクトが成長するにつれて、新たなカラムが追加されたり仕様が変わったりする可能性も大いにあります。そのような処理に対するテストの実装をしたことで、以下のような効果がありました。 開発効率が上がった フィクスチャの実装によって手動でデータを投入しなくてよくなり、煩雑な作業を無くすことができました。また、SQLに修正を加えてロジックが変更されても、テストデータの変更が容易になったため、すぐに動作確認ができました。テストを動かすための仕組みができるまでには一定のコストがかかりましたが、一度作ってしまえばその後の開発は進めやすくなるということを体感しました。 バグが発生しにくくなった 境界値のテストが簡単にできるようになり、細部までテストを書くことでバグの発生リスクを下げることができました。変更を加えた際に予期せぬ影響があっても、テストの失敗でそれに気付くことができ、心理的なハードルも下がりました。実際に、最近リリースした案件で新たに実装した成果データの集計処理では、QA(品質保証)でSQLの実装が原因のバグは見つかりませんでした。 レビュアーの負担が減った テストを書くまでは、レビュアーは仕様書と実際の処理を見比べながら、複雑なクエリを読み解く必要がありました。しかし、テストケース名から仕様の概要を把握できるようになり、効率よくレビューできるようになりました。テストケースの不足から考慮漏れに気付きやすくなり、レビューの質も向上しました。 QAの負担が減った 最大20パターンの検証項目があると、QAの際に必要となるデータのパターンも多岐に渡ります。ユニットテストでエッジケースを担保できるようになったため、QAの工数の削減に繋がりました。 今後の展望 今回の取り組みにより一定の効果が得られた一方で、技術的負債の影響でテストが書けていない箇所もまだ存在します。動作を確認しているとはいえ、テストを書けていない機能のリリースには不安が残るため、負債を解消しながらこの取り組みを継続的に行っていきたいと考えています。また、今後は以下の観点でも改善していきたいです。 ボイラープレートの自動生成 現状では、作成するテーブルのスキーマやフィクスチャのボイラープレートを手動で書いています。ある程度テストを書いてパターンが見えてきたため、今後はこれらを自動生成して、テスト作成の効率を上げていきたいです。 QA用テストデータ作成の環境づくり ユニットテストがあるとはいえ、QAではシナリオテストを実施したいと考えています。その場合、必要なデータの準備にはまだまだ改善の余地があります。データ投入を簡単に行えるツールを作成するなど、より良い方法を模索していければと考えています。 さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは、ブランドソリューション開発本部フロントエンド部の田中です。 普段はFAANSのWebフロントエンドの開発を行なっています。 FAANSとは「Fashion Advisors are Neighbors」がサービス名の由来で、ショップスタッフの効率的な販売をサポートするショップスタッフ専用ツールです。 ショップスタッフ向けにコーデ投稿・成果確認などの機能が存在し、2022年8月に正式ローンチしました。詳しくは以下のプレスリリースをご覧ください。 corp.zozo.com 現在FAANSは立ち上げから2年経過し、Webフロントエンドの開発現場において様々な組織的・技術的課題がありました。 今回はその課題と取り組みについて紹介したいと思います。 目次 目次 前提 FAANSの組織の特徴 FAANSのWebのプロダクトの特徴 変化の多い環境下で遭遇し続ける課題 組織的・技術的課題とその取り組み 課題1: UIコンポーネントの作成に時間がかかっていた 取り組み1: UIコンポーネントライブラリのChakra UIを導入した 課題2: FAANSのWebを開発しているメンバーが1人となり案件をさばけるような体制ではなかった 取り組み2: FAANS内の他職種の人に協力してもらう 課題3: 開発ドキュメントが少なく属人化していた 取り組み3: 開発ドキュメントを作成し、フロードキュメントとストックドキュメントを分けて運用 課題4: FAANSのWebにおいて何の課題を優先して取り組むべきか分からなかった 取り組み4: FAANSのWebチームで抱えている課題をJIRAで管理するようにした 課題5: 権限やフラグによってUIの表示や機能が異なり把握しづらかった 取り組み5: Storybookを使って多様なUIを管理した 課題6: フロントエンドとバックエンドの差異を吸収する層が存在していなかった 取り組み6: フロントエンドとバックエンドの差異を吸収するPresenters層を設けた 課題7: FAANSで使っていたCreate React Appにおいてメンテナンス状況に不安があった 取り組み7: Viteへ移行 課題8: OpenAPIのymlを手動でコピーして運用していた 取り組み8: submoduleを使ってOpenAPIのymlを参照し、自動でAPI Clientを生成するようにした 終わりに 前提 まず前提としてFAANSの組織とWebのプロダクトの特徴について紹介したいと思います。 FAANSの組織の特徴 FAANSはWeb、iOS、Androidのプラットフォームが存在します。立ち上がって2年ほどで、スタートアップのような小規模なチーム(チーム全体で約15名)で開発をしています。 FAANSのWebのプロダクトの特徴 FAANSは導入が決まった企業のみが利用でき、検索エンジンには載らないログイン必須な業務ツールです。 したがって、SEOは意識せず、ページ遷移やユーザーによる操作などのインタラクションが多いのが特徴です。その特徴のもとユーザービリティの高い体験を提供するために、以下のようにクライアントサイドレンダリングをベースにして開発しています。 アプリケーションのコードをViteを使ってビルドし、生成されたHTML,CSS,JSなどのファイルをFirebase Hostingからブラウザへ配信する構成になっています。 配信後クライアントサイドレンダリングでページ遷移し、データが必要な場合はバックエンドのAPIを叩いてデータを取得するシングルページアプリケーションです。 初回表示はFAANSの全体のフロントエンドのファイルをブラウザに配信するため時間がかかりますが、その後のページ遷移に関してはクライアント側でレンダリングするため速くなります。FAANSはSEOを意識しない業務ツールで頻繁にページ遷移が発生することから、初回表示よりもその後のページ遷移の速度を重視してこのような構成になっています。 また現時点(2023年10月時点)で採用している主な技術スタックは以下の通りです。 TypeScript React Vite Chakra UI React Hook Form Storybook 変化の多い環境下で遭遇し続ける課題 慎重にプロダクトに合わせたチームを形成・技術選定がされていたものの、運用していくにつれていくつか課題に遭遇しました。 後に紹介しますが、Webフロントエンドの開発チームが1人になり案件をさばける体制でない、立ち上げ当初は推奨されていた技術がメンテナンスに不安があるといった組織的・技術的にも取り組まないといけない課題がありました。 そのような変化がある環境下で、企業様が必要とする機能開発とバランスをとりながら、プロダクトチームと話し合い、優先度をつけてそれらの課題に取り組む必要がありました。 組織的・技術的課題とその取り組み 以降遭遇した組織的・技術的な課題とその取り組みについて時系順に紹介したいと思います。同じような課題感を持つ方の参考になれば幸いです。 課題1: UIコンポーネントの作成に時間がかかっていた 開発当初のFAANSのWebフロントエンドチームは2名体制で、styled-componentsを採用して1からCSSやJSを書いてUIコンポーネントを作成していました。 UIコンポーネントライブラリをFAANS用のUIにカスタマイズする案もありましたが、そのUIに引っ張られてカスタマイズに時間がかかる懸念から、当時は1から書く手法をとっていました。 しかし、モーダルのような複雑なUIコンポーネントを作成するときに時間がかかって、思うようにスピード感が出せてないと感じていました。少ないチームメンバーでスケジュール通りに機能を届けるためにも、そのスピード感を上げる必要がありました。 取り組み1: UIコンポーネントライブラリのChakra UIを導入した そこでCSSライブラリの変更の舵を切るのに不安がありましたが、プロダクトやチームの特性を考えて途中でChakra UIを導入することにしました。主な理由は以下の通りです。 機能豊富なコンポーネントが備わっていて、かつ、アクセシビリティが考慮されており、その土台がある状態からUIコンポーネントを作成できる。 UIの個性が強くなく簡単にスタイルを上書きできるのでFAANSのUIにカスタマイズしやすかった。 自作していたstyled-componentsのUIコンポーネントに対して、marginTopやfontSizeなどのスタイリングをするために、Styled Systemを使って拡張していた。Chakra UIもStyled Systemを参考にしているため、スタイリングの際のpropsのインタフェースがほとんど同じで移行しやすかった。 実際に運用してみるとstyled-componentsからの移行もスムーズで、UIコンポーネントの作成にかかる時間を短縮できました。これは少ないチームで開発をする上で大きなメリットとなりました。 また選定時は意識していなかったのですが、Inputのようなフォームのコンポーネントを非制御コンポーネントとして扱えるのもFAANSのWebにとってメリットがありました。 FAANSのWebでは店舗登録やスタッフ登録画面などのフォームが多く、フォームの管理のためReact Hook Formを採用しています。主な採用理由は非制御コンポーネントに対しても扱うことができ、その場合にフォームのデータをDOM自身が管理するため、再レンダリングの回数を減らせるからです。 以下のようにReact Hook Formのregisterを使って、フォームのコンポーネントに対してrefを渡すことで、データが更新されたとしても再レンダリングされないようにできます。 制御コンポーネントを扱うControllerを使うのに比べてregisterでの登録は記載が短くすみ、可読性の向上に繋がりました。 import { forwardRef , Input , InputProps } from '@chakra-ui/react' ; export const StyledInput = forwardRef < InputProps , 'input' >(( props , ref ) => ( < Input height = "42px" fontSize = "13px" borderColor = "gray.CCCCCC" borderRadius = "4px" _placeholder = {{ color: 'gray.999999' , }} ref = { ref } { ...props } / > )) < StyledInput { ...register ( 'externalUrl' ) } placeholder = "例:https://faans.jp" / > 課題2: FAANSのWebを開発しているメンバーが1人となり案件をさばけるような体制ではなかった FAANSのWebフロントエンドの開発は2人で開発していたのですが、一時的に1人のメンバーが開発から離れ、自分1人になった時期がありました。 その時期にとある企業様のFAANSの導入を進めるにあたって、スケジュール優先で案件を着地させる必要がありました。 メンバーが自分1人になる前、FAANSのWebフロントエンドの採用へ繋げるための土台づくり(テックブログを書く・登壇する)をしたものの、エンジニアの採用は難しく採用へ繋げることができませんでした。 そこで機能のスコープを優先度が高いものに絞ったり、開発効率を向上するために自動化などの施策を試みたものの、メンバー1人では案件をさばける状況ではありませんでした。 余裕をもった上で案件を着地させるためにも、この状況下で適切な手段を考える必要がありました。 取り組み2: FAANS内の他職種の人に協力してもらう そこでチームで解決策について話し合った結果、FAANSチームの他の職種からもWebフロントエンドの開発に協力してもらう手段を選択しました。というのも、Webフロントエンドの経験があったり、Webの最近の動向を知って自分の開発に活かしたいと興味のあるメンバーがいたからです。 実際にそのとき余力があったバックエンドのエンジニア2名とiOSのエンジニア1名に協力してもらい、自分を含む計4人体制で開発を進めました。 お願いする際にこの協力は評価されるかという点を気にしていましたが、会社の評価制度も柔軟で評価対象だと分かり安心してお願いできました。 協力の際にはJIRAでタスクを細かく切って、ガントチャートで進捗を可視化しつつ、その方のフロントエンドの経験度に合わせてタスクをお願いしました。 また新しく開発に関わる人にはどのディレクトリにどのファイルを置けば良いか、どういった作法でコードを書けば良いのか分からないという問題がありました。そこはHygenを使って指定したディレクトリにテンプレートのファイルを自動生成することで、開発時の迷いを軽減させるようにしました。 この協力体制のもと案件を切り抜けられました。この手法のメリットとして、再度Webのフロントエンドの開発の人手が足りない時に一度協力してもらった方にお願いできる余地ができました。 課題3: 開発ドキュメントが少なく属人化していた 取り組み2でFAANSの他職種の人にWebフロントエンドの開発の協力をしてもらい4人体制で開発を進めましたが、開発ドキュメントが充実していませんでした。質問の度に口頭で設計や運用ルールなどのWebフロントエンドの開発に必要な説明をしていて時間がかかっていました。 取り組み3: 開発ドキュメントを作成し、フロードキュメントとストックドキュメントを分けて運用 取り組み2の案件が終わった後、メンバーが1人仲間に加わり、FAANSのWebフロントエンドの開発メンバーが2人になりました。 そのタイミングで開発ドキュメントを作成し、それを見ればFAANSのWebフロントエンドの開発に必要な情報が分かるようにしました。 この結果、属人化が抑えられ開発の説明にかかる時間が短縮されました。 ただ、次の懸念としてそのドキュメントがメンテナンスされ続けるかという点を気にしていました。開発ドキュメントは一度作成して終わるというわけではなく、プロダクトの成長に伴ってドキュメントを追加したり更新する必要があります。 各々がバラバラにドキュメントを配置してしまったり、どのドキュメントを信頼すべきか分からなくなる可能性があり、それを避ける必要がありました。 そこでドキュメントを以下のように分けて運用しました。 ストックドキュメント: アーキテクチャーなど定期的にメンテナンスする必要があるドキュメント フロードキュメント: メンテナンスする必要がないメモのような一時的なドキュメント メモ書きでも良いのでチームとしてドキュメントは残す方針にし、一度フロードキュメントに入れてその中で良い内容はストックドキュメントにも記載、適切なタイミングで階層構造を整備する運用にしました。こうすることで、ドキュメントを残す文化が醸成し、ストックドキュメントにはメンテナンスされていて信頼できる情報が残るようになりました。 課題4: FAANSのWebにおいて何の課題を優先して取り組むべきか分からなかった 取り組み3で開発ドキュメントを作成したことによって、開発の説明にかかる時間は短縮されました。そのタイミングで一時的に離れていたメンバーも戻ってきて、FAANSのWebチームも3人になりました。ただ中途で入ったばかりの人や久しくFAANSの開発に携わる人にとって、案件とは別に現状どの課題が存在しているのか、何の課題を優先して解決すべきか分からないという声がありました。その結果優先度が低い課題に着手してしまう可能性がありました。 取り組み4: FAANSのWebチームで抱えている課題をJIRAで管理するようにした FAANSのWebチームの抱えている課題をJIRAのチケットとして作成して、管理するようにしました。その課題に対しては「FAANS_WEB_IMPROVEMENT」のラベルを貼るようにし、抱えている課題のチケットに絞って一覧で表示できるようにしました。それを元にチーム内で話し合い、課題に対してHigh、Medium、Lowなどの優先度付けをしました。これによって、メンバー全員に課題感の共有ができ、優先度が高い課題に取り組む体制となりました。 課題5: 権限やフラグによってUIの表示や機能が異なり把握しづらかった FAANSはショップスタッフ、ショップを管理する人、企業を管理する人などの権限や企業が持つ自社のECとの連携状況などのフラグが存在しています。そして、その権限やフラグによって表示されるUIや機能が異なるのもFAANSの特徴の1つです。 そのため各権限やフラグにおいて表示されるUIや機能が正しいか確認するために、アカウントを切り替えながら開発していて手間だと感じていました。 また、ある権限やフラグのアカウントで修正したときに他の条件のアカウントに影響が出ていないか自動的に担保する仕組みがありませんでした。 取り組み5: Storybookを使って多様なUIを管理した そこで解決策としてStorybookを導入し、権限やフラグの状態をMock Service Workerなどを使ってモックして、それぞれのページにおけるStoryを一覧で見られるようにしました。これでアカウントを都度切り替えずとも各UIのパターンを把握しながら開発できるようになりました。一例を挙げると以下のように自社EC連携の有無によって自社ECのカードが表示されるか確認できます。 また、StorybookではInteraction testsの機能が備わっており、表示されたUIに対して期待値と一致しているかのテストができます。以下では自社ECを連携している場合に自社ECのカードが表示されるかをテストしています。そのテストをCIに組み込むことができ、リグレッションが起きていないか自動的にテストできました。 export const IsLinkedOwnedEc: Story = { name: '自社EC連携をしている場合' , parameters: { msw: { handlers: { mockGetStaffMember: rest. get( ` ${ MSW_ORIGIN } /v1/staff_member` , ( req , res , ctx ) => { return res ( ctx.json ( produce ( mockGetStaffMemberBaseResponse , ( draftState ) => { draftState.company.is_linked_with_owned_ec = true ; } ), ), ); } , ), } , } , } , play: async ( { canvasElement , step } ) => { await step ( '自社ECのカードが表示されていること' , async () => { const title = '自社EC' ; await within ( canvasElement ) .findByRole ( 'region' , { name: title , } ); } ); } , } ; また、FAANSは業務ツールということもありユーザーによる操作などインタラクションが多いのも特徴です。例えばフォームの入力、ボタンのクリックなどの操作です。インタラクション後のUIも期待値通りか自動的に担保したく、それが可能で見やすい形でデバッグができるInteraction Testsの機能はFAANSのプロダクトの特性に合った選択肢でした。 Storybookの運用にあたって、Storybookの作成が目的化して、メンテナンスするためのモチベーションの低下を懸念していました。 しかしUIコンポーネントの管理の他に、以下のようにStorybookをメンテナンスするための目的を持たせたり、開発フローに取り込む事でモチベーション低下を防ぎました。 多様なUIパターンを把握しながら開発する その多様なUIやインタラクション後のUIも含めて自動テストをし、不具合を検知できるようにする 次の課題として、表示されたUIに対して担保したい項目が多く、その分テストコードも増えてメンテナンスコストが高くなっています。カバー率の高いビジュアルテストと組み合わせて、コストを下げることを検討しています。( 参考 ) 課題6: フロントエンドとバックエンドの差異を吸収する層が存在していなかった フロントエンドとバックエンドの差異を吸収する層が存在しないため、APIの変更の影響を受けやすいViewの実装がありました。 一例をあげると以下のフォームのように、APIのリクエストのあるパラメーターの型がbooleanなので、onChangeの際にbooleanへ変換するAPIを意識したViewの実装です。 この実装をするとAPIのリクエストのパラメーターの変更によって、Viewのコードの変更が必要になります。またこの変換や加工といった吸収処理をどこに書くかルールが決まっておらず、開発者によって書く場所が異なり、レビューコストが上がってしまいました。 // フォームのViewの実装例。ここではReact Hook FormのControllerでフォームのデータを管理。 < Controller control = { control } name = "parameter_name" render = { ( { field } ) => { return ( < RadioGroup onChange = { ( e ) => { field.onChange ( e === 'true' );   //APIのリクエストのパラメーターの型に合わせるために、stringをbooleanに変換している。このようにViewで書く開発者もいれば、onSubmit時に書く開発者もいて統一されていなかった。 }} value = { field.value ? 'true' : 'false' } > < Radio value = "false" > false < /Radio > < Radio value = "true" > true < /Radio > < /RadioGroup > ); }} / > 取り組み6: フロントエンドとバックエンドの差異を吸収するPresenters層を設けた フロントエンドとバックエンドの差異を吸収するPresenters層を設けてそこで吸収させることにしました。 これによって、APIのインタフェースの変更の影響範囲はそのPresenters層に限定でき、Viewにその影響を与えないようにしました。 またViewに書かれていた変換や加工といった吸収処理がPresenters層に統一されたことで、開発者がどこに書くか迷うことがなくなったり、Viewのコードもシンプルになりました。結果としてレビューコストも下がるようになりました。 // フォーム送信時に呼び出すPresenters層の関数。 export const xxxPresenter = async ( formData: FormData ) : Promise < Response > => { // formDataをAPIのインターフェースに合わせて変換・加工する。(例、stringをbooleanへと変換。) // 変換後にAPIを呼ぶ。 } ; // フォームのViewの実装例。ここではReact Hook FormのControllerでフォームのデータを管理。 < Controller control = { control } name = "name" render = { ( { field } ) => { return ( < RadioGroup //stringのまま管理。APIは意識せずにViewのコードを書ける。 onChange = { field.onChange } value = { field.value } > < Radio value = "false" > false < /Radio > < Radio value = "true" > true < /Radio > < /RadioGroup > ); }} / > 課題7: FAANSで使っていたCreate React Appにおいてメンテナンス状況に不安があった JIRAで管理している課題チケットの中にDependabotから通知されるセキュリティーアラートの数が多いという問題がありました。 そこでそのセキュリティアラートで指摘されているパッケージを見ると利用していたv4のCreate React App(以下CRA)が依存しているパッケージ起因であることが分かりました。 それを機にCRAの現在について調べてみると最近のv5へのアップデートが1年前だったり、メンテナンス状況に不安が残るissueがいくつかありました。またチーム内から開発サーバーの立ち上がりに時間がかかるといった声を聞くようになりました。 旧Reactの公式ドキュメントではSPA開発に CRAがお勧め されていて、その選定には違和感なく開発していたのですが、最近ではこのような状況になりフロントエンドの変化のスピードに驚きました。 取り組み7: Viteへ移行 そこで月1回開催されるフロントエンド勉強会で技術顧問やZOZOTOWNやWEARなどの他のチームの方に相談して、CRAに感謝しつつViteへの移行を決めました。 Next.jsでSPAを作るという候補も上がっていましたが、以下の理由でViteへ移行しました。 FAANSのWebがReact Routerに依存しておりファイルシステムベースのルーティングへの移行コストがかかりそう。 サーバーサイドレンダリングの予定がない。 移行作業もスムーズに行き、規模が大きいところでいくと環境変数の参照を process.env から import.meta.env へ変更することでした。なので環境変数を利用しているページに影響するとみて、そのページを中心にQAして頂いてリリースしました。 結果として抱えていた課題は解消され、以下のように開発サーバーの立ち上げや本番ビルドなどの速度が上がりました。 比較内容 CRA4 Vite dev cold start 約3分15秒 約13秒 dev warm start 約27秒 約2秒 hot reload 約2秒 保存直後 production build 約10分 約3分 課題8: OpenAPIのymlを手動でコピーして運用していた FAANSのWebではOpenAPI Generatorを使って、バックエンドのリポジトリに保存されているOpenAPIのymlファイルをフロントエンドのリポジトリに手動でコピーして、API Clientを生成していました。 定期的にこの作業は発生し、手動のためオペレーションミスを引き起こす可能性がありました。 取り組み8: submoduleを使ってOpenAPIのymlを参照し、自動でAPI Clientを生成するようにした そこでフロントエンドのリポジトリにsubmoduleを登録し、そこからバックエンドのリポジトリに保存されているOpenAPIのymlファイルを参照するようにしました。また、OpenAPIのymlの更新を検知して自動でAPI Clientを生成し、プルリクエストを作成するワークフローを組みました。この作業を自動化することで、オペレーションミスを防いだり、他のクライアントのチームにも展開できるようになりました。 終わりに 以上が組織的・技術的な課題とそれに対する取り組みになります。 全体的にまず課題の共有から始めて、WebフロントエンドのチームのみならずFAANSの他職種の方と会話を重ねるのも大事でした。 広い視点で、そのときの状況やプロダクトの特性に適した解決策が見つかったり、今何をやるべきかといった優先度も洗練されていくからです。 課題2のようにWebのフロントエンドの開発者が1人になり案件がさばける体制でない状況下で、他の職種の方に協力してもらう解決策はその1つの例でした。 当時は自動化して開発効率を上げる技術的なアプローチで解決することを考えていましたが、このように組織的なアプローチで解決に繋がるとは思わなかったです。 そうして話し合いながら、優先度が高い課題に対処して改善されていく日々を目の当たりにすることは、その課題の解消の効用以上にチームの雰囲気に良い影響を与えました。チームの雰囲気が良くなれば、チーム内で意見が言いやすくなったり、次なる課題に対して取り組みやすくなると思います。 この「課題の共有」→「プロダクトチームと会話」→「優先度づけして取り組む」→「チームの雰囲気が良くなる」→「次なる課題の共有」→(以降繰り返し)というポジティブなサイクルは、この変化の多い環境下で課題を解決し続けていく上で大事だと感じました。 現状FAANSのWebに関して以下のような課題を抱えており、これからも案件とバランスをとりながら取り組み続けていきたいと思っています。 Findyで開発生産性の可視化した上で施策が打ち続けられるチームづくり 開発効率を上げることを目的としたデザインシステムの作成 ZOZOではこのように課題を前向きに改善してくれるエンジニアを募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
アバター
こんにちは、技術本部SRE部ZOZOSREチームの斉藤です。普段はZOZOTOWNのオンプレミスとクラウドの構築・運用に携わっています。またDBREとしてZOZOTOWNのデータベース全般の運用・保守も兼務しております。 ZOZOTOWNではSQL Serverインスタンスが複数存在しており、サービスのメインデータベースとして稼働しています。その中で、1つのインスタンスを共用し、2つのデータベースが相乗りしている状態で運用されている環境が存在します。相乗りしているデータベースを検討したシステム構成の制限内で分離するには詳細な調査が必要でした。後述しておりますが、調査の過程で課題が見つかり、容易に分離はできませんでした。課題を解決し、分離を実現させるために日々邁進しております。 本記事では、SQL Serverインスタンスを共用し、2つのデータベースが相乗りしている環境からデータベースを分離させるための取り組みを紹介します。データベース分離に関して構成やスペック検討、課題として顕在化する項目の一例としてご参考になればと思います。 目次 目次 データベース分離を検討した背景と課題 構成検討 ソリューション選定 可用性 SQL Serverのエディション 必要なスペックを調査する CPUコア数を見積もる 並列処理の最大限度(MAXDOP)を検証する バッファキャッシュサイズを見積もる 必要スペックの調査結果と改善案 まとめ おわりに データベース分離を検討した背景と課題 現在、1つのインスタンス上にショッピングカート関連の機能を持ったデータベース(以下、カートDB)と履歴関連のデータを持つデータベース(以下、履歴DB)が共存している状態です。どちらかのデータベースが起因で障害が発生した場合、もう一方のデータベースにも影響が波及し、障害の範囲が広がってしまいます。特にカートDBが影響を受け、ショッピングカート機能に影響を受けるとZOZOのサービス継続に多大なインパクトを発生させてしまいます。障害の影響範囲を限定的にするため、カートDBと履歴DBを分離したいと考えました。ワークロードやサービスの継続性を考慮し履歴DBをインスタンスから分離させることにしました。 構成検討 履歴DBは、ZOZOTOWN内の一部機能やサイト表示に影響があり、社内の業務システムでも使用されていることから、可用性を担保したまま移行できることを必須の条件としました。他にも、クラウドとオンプレの比較、費用や機能面、アプリケーション改修コストを主な検討項目としました。 ソリューション選定 3つのデータベースソリューションを検討しました。コストは各ソリューションを5年間運用した場合を想定して比較しました。結論としては、アプリケーションの改修コストとライセンスコストを押さえられるオンプレミスのSQL Serverを選定しました。クラウドはコストメリットが出ませんでした。ハードウェアの運用については、ノウハウが蓄積されているので運用コストの大幅な増加は無いと判断しました。 製品 アプリケーション改修コスト 運用費用(5年) ハードウェアの運用 SQL Server(オンプレミス) 低 低 有 Amazon RDS for SQL Server 低 高 無 Amazon Aurora MySQL 高 中 無 可用性 Windows Serverの標準機能として利用できるWindows Server Failover Clustering(以下、WSFC)でクラスター化し、可用性を担保することにしました。WSFCは既存環境で採用されており、安定して運用できることが選定理由です。 WSFCについての情報は Windows Server フェールオーバー クラスタリングの概要 を参照してください。 概要図 SQL Serverのエディション 既存のインスタンスはEnterprise Editionで稼働しています。データベースが独立するので、サーバスペックを縮小できると想定しました。Enterprise EditionとStandard Editionではコストが大幅に変わります。可能ならば、分離先のインスタンスはStandard Editionで稼働させたいと考えましたが、エディション毎に機能制限があります。Enterprise EditionとStandard Editionの主な違いは以下の通りです。 ライセンス価格 Enterprise EditionはStandard Editionに比べ、約4倍の価格。 Enterprise Edition:USD$13,748 Standard Edition:USD$3,586 ライセンス価格についての情報は SQL Server 2019 の価格 を参照してください。 オンライン操作 Enterprise Editionはインデックスをオンラインで作成、再構築が可能だが、Standard Editionでは不可能。 リソース制限 Enterprise Editionはサーバに搭載されているリソースを最大限に使用できるが、Standard Editionでは制約がある。 CPU:4ソケットまたは、24コアのどちらか小さいほうに制限 バッファプール:最大サイズが128GB エディション間の機能差についての詳細情報は SQL Server 2019 の各エディションとサポートされている機能 を参照してください。 ライセンス価格の安いStandard Editionを選びたいところですが、各種制限の範囲で履歴DBを移行できるか懸念があります。次のステップとしてこれらの制限が移行にどの程度影響していくかを調査します。 必要なスペックを調査する データベースを分離するにあたって難しいと感じたのが、スペック検討です。共有しているサーバリソースから履歴DB部分で使用しているリソースのみを抜き出す必要があります。メトリクスがサーバやインスタンス単位で取得されており、データベース単位で数値化しなければなりません。更にStandard Editionのリソース制限内で運用が可能かという観点でも調査が必要でした。 調査項目 CPUコア数を見積もる 並列処理の最大限度(MAXDOP)を検証する バッファキャッシュサイズを見積もる CPUコア数を見積もる 必要なCPUコア数を見積もります。まずDBのCPU使用率が24時間中で最も高かった1時間に絞りました。弊社では動的管理ビュー(Dynamic Management View:以下、DMV)の情報をロギングしています。対象時間帯で実行されている全クエリから履歴DBのCPU時間を抽出し、以下の式に当てはめて必要なコア数を算出しました。DMVのロギングについては以前のテックブログで紹介しています。 techblog.zozo.com 計算式 (履歴DBのCPU時間 ÷ 全体のCPU時間) × 全体のCPU使用率 × サーバの論理コア数 結果 (279 ÷ 557) × 0.5 × 64 = 16.02 以上の結果から、必要なコア数は約16となり、Standard Editionの制限内である24コアで稼働できる見込みとなりました。SQL Server以外のOSなどが使用するCPUも考慮し、32コアのハードウェアスペックがあれば問題ないと見積もりました。 並列処理の最大限度(MAXDOP)を検証する SQL Serverは1つのステートメントで使用できるプロセッサの最大数を決めることができます。既存のインスタンスはMAXDOPが8で設定されており、Standard EditionでもMAXDOPを8に設定できます。しかし使用できるCPUコア数が24のため、MAXDOPが8のクエリが3セッションで実行されると、CPU使用率が100%に達してしまう可能性があります。Standard Editionの並列処理度を考慮するとMAXDOPを4程度に減らすことを検討する必要があります。並列処理の最大限度(MAXDOP)が半減した場合にどの程度の性能劣化が生じるのか検証しました。 並列処理の最大限度(MAXDOP)についての詳細情報は データベース エンジンの構成 -MAXDOP ページ を参照してください。 以下の調査クエリで履歴DBからMAXDOP8で実行されているクエリを抽出します。結果が比較しやすくなるので、調査クエリの結果から実行時間の長いクエリをピックアップしました。 select top 1000 last_execution_time ,execution_count ,last_elapsed_time ,last_logical_reads ,last_physical_reads ,max_dop ,text from sys.dm_exec_query_stats outer apply sys.dm_exec_query_plan (plan_handle) as qp outer apply sys.dm_exec_sql_text (sql_handle) as sql where qp.dbid = db_id( ' 履歴DB ' ) and max_dop = 8 MAXDOP4に設定したStandard Editionの環境でピックアップしたクエリを実行し、結果を確認します。 実行結果 実行時間:20秒 先行読み取り数:0 論理読み取り数:8112285(100%キャッシュに載っている状態) 実行したクエリのMAXDOPを確認します。 実行結果 MAXDOP4で実行されていることを確認 同様のクエリに「option (maxdop 8)」を指定して再度実行します。「option (maxdop 8)」を指定すると強制的にMAXDOP8で実行されます。 実行結果 実行時間:10秒 先行読み取り数:0 論理読み取り数:8112285(100%キャッシュに載っている状態) 以上の検証結果から同時実行性を考慮し、MAXDOPを4にした場合、MAXDOP8で実行されているクエリは性能劣化が想定される結果となりました。業務要件を精査してどうしても性能劣化が許容できないクエリにのみ「option (maxdop 8)」を指定するなどの対策が必要という結論に至りました。 バッファキャッシュサイズを見積もる 必要なバッファサイズを見積もります。現状の各データベース毎のバッファキャッシュサイズを調査しました。 各データベース毎のバッファサイズ調査クエリ select count (*)* 8 / 1024 / 1024 as buffer_cache_size , case database_id when 32767 then ' ResourceDb ' else db_name(database_id) end as database_name from sys.dm_os_buffer_descriptors with (nolock) group by db_name(database_id) ,database_id order by buffer_cache_size desc ; 各データベース毎のバッファサイズ buffer_cache_size database_name 496GB 履歴DB 149GB カートDB 履歴DBで使用されているバッファキャッシュのサイズは496GBでした。履歴データを保持しているテーブルはデータ量が多く、バッファキャッシュを想像以上に使用しており、Standard Editionの制限である128GBの約4倍のサイズでした。バッファキャッシュに載っているデータの中でアクセスの無い余剰なデータがあると考え、調査する方法を検討しました。 テスト環境を作成する 本番環境と同等のスペックを持つ周辺システムの構築とワークロードを再現することは現実的には難しい 本番環境のバッファキャッシュサイズに使用上限を設定して影響調査する 履歴DBのバッファキャッシュに限定して使用上限を設定できず、インスタンス全体に影響が波及してしまう インスタンスを共用しているデメリットが現れてしまいました。残念ながらバッファキャッシュ内の余剰なデータを調査できませんでした。しかし、下記の結果の通りディスクを読み込んでいる処理が存在するのを確認できました。現状、問題になっているわけではありませんが、現時点でキャッシュアウトが発生していることが想定され、余剰なデータはバッファキャッシュに載っていないと判断しました。 物理読み込み発生有無の調査クエリ select top 1000 last_logical_reads ,last_physical_reads ,max_dop ,text from sys.dm_exec_query_stats outer apply sys.dm_exec_query_plan (plan_handle) as qp outer apply sys.dm_exec_sql_text (sql_handle) as sql where qp.dbid = db_id( ' 履歴DB ' ) and last_physical_reads > 0 order by max_physical_reads desc 実行結果 last_physical_reads値が0ではないので、物理読み込みが発生していることが想定できる キャッシュアウトが増加した場合に備え、どの程度の性能劣化が起きるのか検証することにしました。まずはメモリを大量に使っているクエリを調査します。 メモリを大量に使用している調査クエリ select top 1 last_execution_time ,execution_count ,last_elapsed_time ,last_logical_reads ,last_physical_reads ,max_dop ,text from sys.dm_exec_query_stats outer apply sys.dm_exec_query_plan (plan_handle) as qp outer apply sys.dm_exec_sql_text (sql_handle) as sql where qp.dbid = db_id( ' 履歴DB ' ) order by max_logical_reads desc 該当したクエリ 実行時間:11秒 last_logical_reads:58GBをデータキャッシュから読み込んでいる 7494590ページx8KB=59956720KB=58GB last_physical_reads:ディスク読み込みは発生していない 該当クエリをテスト環境で実行し性能差を比較します。キャッシュをクリアし、該当クエリを実行します。 dbcc dropcleanbuffers 実行結果 実行時間:4分22秒 先行読み取り数:8112285 論理読み取り数:8112285 SQL Serverの仕様上physical_readsの値には反映されず、先行と論理の読み取り数が同じ場合、物理I/Oが発生していると判断します。キャッシュクリアをしたので、物理ディスクからの読み込みになっており想定通り実行時間が増加しました。 同様のクエリを再度実行します。 実行結果 実行時間:9秒 先行読み取り数:0 論理読み取り数:8112285 今回は100%キャッシュに載っている状態で実行され、本番と同じパフォーマンスで実行されました。バッファキャッシュに載っていればStandard Editionでも性能が出ることを確認できました。 以上の調査結果からStandard Editionの制限内でバッファキャッシュを128GBにした場合、キャッシュアウトの増加が予想されます。クエリの性能劣化が想定されるため、メモリを大量に使用しているクエリの改善や業務要件の見直し、性能劣化の許容範囲などを調整していく必要があるという結論に至りました。 必要スペックの調査結果と改善案 現状のまま履歴DBをStandard Editionに分離した場合、著しい性能劣化が想定される結果になりました。分離する前にデータベースを最適化する必要があることを確認し、改善案を検討しました。 CPUコア数 履歴DBのCPU使用率が高い時間帯のCPU時間から計算し、16コアが必要となりました。Standard Editionの制限内に収まりますが、なるべくCPU使用を抑える施策をしておくのがよさそうです。 改善案 CPUを非効率に使用しているクエリのチューニング 並列処理の最大限度(MAXDOP) Standard Editionは使用できるCPUコア数が24のため、MAXDOPを4程度に減らすことを検討する必要があります。クエリの性能劣化は避けられないので対策案を出しました。 対策案 MAXDOPを中間値の6に設定できるか検討する 業務要件を精査して性能劣化が許容できないクエリをピックアップ 対象のクエリにのみ「option (maxdop 8)」を指定する バッファキャッシュサイズ Standard Editionは使用できるバッファキャッシュサイズが128GBです。現状使用しているバッファキャッシュサイズの1/4程度になるので、キャッシュアウトが増加し、クエリの性能劣化が想定されます。メモリを大量に使用しているクエリを改善する必要があります。 改善案 大量のデータを処理している処理の改修 日時で全件selectをしている処理を改修する メモリ使用が多いクエリのチューニング 列ストアインデックスの検討 データを圧縮することで、ストレージの使用量が削減され、データの読み取り性能を向上させる 大量のデータを一括で処理するバッチ処理の性能が向上する 列ストアインデックスについての情報は 列ストア インデックス: 概要 を参照してください。 改善の余地があるので、上記の各改善案をもとに現状の状態で改善をし、Standard Editionの制限内で分離が実現できるか取り組んでいきたいと思います。 まとめ データベースの分離を検討した場合、スペックを縮小するのは難しい場合があります。コストや制限を考慮し、設けられた範囲内で分離するために施策を講じる必要があります。リソースを効率的に使用するために、1つのインスタンスに複数のデータベースを構築する場面は珍しくありません。しかし、分離の必要性が出てきた際にスペックを詳細に算出するのが難しいという課題があります。SQL Serverに関してはエディションによって制限があり、限られたリソースの範囲で分離を検討しなければならず、考慮するポイントが多いと感じました。「リソースに空きがある」という理由だけで1つのインスタンスを共用するのはリスクが高いと感じます。長期的な目線でデータベースの運用や障害リスクを考慮し、慎重に検討していくことが必要です。上記の通り、弊社では分離の前にリソースを最適化するという課題が現れました。今回考えた改善や改修案の実施結果については、また別の機会にお話できればと考えています。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。9月26日に「 Ask the Masters - 評価制度や組織設計 」と題して、ZOZO CTOの瀬尾とタイミー VPoTの山口さまによる対談イベントを開催しました! イベント内容 本イベントはオフライン・オンラインのハイブリッドで開催しました。オフライン会場にはタイミーさまのイベントスペースをお借りしました。 タイミーさまの広いイベントスペース 今回の企画は、タイミーの山口さまとZOZOの瀬尾がDeNA出身という共通点を持ち、親交があるという背景から実現しました。山口さまは2023年5月に株式会社タイミー執行役員VPoTに就任し、瀬尾は2023年6月に株式会社ZOZO執行役員CTOに就任しています。そこで、組織の作り方や技術戦略といった共通の関心事をディスカッションする場として設けることにしました。 パネルディスカッション「評価制度や組織設計」 ZOZOの瀬尾(左)とタイミーの山口さま(右) 「評価制度や組織設計」をテーマにいくつか質問を用意し、ディスカッションを行いました。当日の内容はYouTubeのアーカイブで視聴できます。 www.youtube.com 機材トラブルのため序盤はパネリストの音声が小さく聞き取りづらい状態ですが、 23:51 頃から解消しております。 用意した質問を一部抜粋します。回答が気になる方はぜひYouTubeをご覧ください! Q. EMは会社によって定義が微妙に違うので難しいロールだと感じておりますが、お二人が考えるEMの定義について教えてください Q. 開発組織作りにおいて参考にしている会社や書籍はありますか? また独自で工夫や意識していることを教えてください Q. お二人とも組織の急成長を経験されていると思いますが、開発組織のスケールのペインとそれをどう乗り越えたかを教えてください Q. 今まで多くの意思決定をされてきたと思いますが、組織レベルの意思決定で今振り返るとあの決断をして特に良かったと思う意思決定はありますか? Q&Aセッション Slidoを見ながら質問に回答 Q&Aセッションでは、Slidoで寄せられた質問に対して回答していきました。パネルディスカッションで話された内容の深掘りや、具体的な指標を教えてほしいといった質問が寄せられました。 「いつ頃から開発組織作りに興味を持っていたか」という質問に関して、おふたりともDeNA在職時はスペシャリスト寄りで、組織作りにはあまり意識を向けていなかったと話していました。そのような背景から徐々に組織作りへの関心が高まっていく変化は興味深いと感じました。 瀬尾がチームを持ちたいと思ったきっかけについて「ひとつの大きな成果を出そうと思うとひとりだけで全部やるのは無理がある。もっと会社に貢献していくために、チームをつくって同じミッションに向かって動かすことが必要だと思った」と語っていたのが印象的でした。 最後に 終了後、タイミーさまのマスコットキャラクターの前で撮影 Q&Aセッション終了後には懇親会を行いました。組織や評価に関心を持つ方々が集まっており、それぞれの持つ課題や今後やりたいことなどを話し合う場となっていました。会社の規模やカルチャーによって組織制度が異なるため、相互のコミュニケーションにより新しいヒントを得ることができたかと思います。今後もさまざまなイベントを開催していきますのでぜひご参加ください! ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。9月25日に After DroidKaigi 2023 を開催しました。9月14日〜16日に開催されたDroidKaigi 2023の協賛企業である株式会社ZOZO、ヤフー株式会社、LINE株式会社の3社合同での振り返りイベントです。オフラインとオンラインのハイブリッドで実施しました。 登壇内容まとめ 3社の社員による発表と、パネルディスカッションを行いました! コンテンツ 登壇者 ZOZOTOWNアプリでのJetpack Compose取り組み事例 内山 雅由 / 株式会社ZOZO Modifier.composedがプロダクトに与えている影響「Modifier.Nodeを使いましょう」を踏まえて 長濱 伶 / ヤフー株式会社 Code Review Challenge An example of a solution DroidKaigi 2023: コードレビューチャレンジの問題解説 安藤 祐貴 / LINE株式会社 パネルディスカッション 高田 真壽, 森 洋之, 玉木 英嗣 当日の発表はYouTubeのアーカイブでご覧ください。 ZOZOTOWNアプリでのJetpack Compose取り組み事例 内山 雅由 / 株式会社ZOZO speakerdeck.com 弊社ZOZOの内山からZOZOTOWNアプリのプロダクトコードにJetpack Composeを導入した事例を紹介しました。リニューアル前後でコードメトリクスの計測をしており、比較可能にしているのは良い取り組みだと感じました。 Modifier.composedがプロダクトに与えている影響「Modifier.Nodeを使いましょう」を踏まえて 長濱 伶 / ヤフー株式会社 speakerdeck.com 長濱さまからは、DroidKaigi 2023で発表されたセッション「 Modifier.Nodeを使いましょう 」を踏まえて、Recompositionについての詳細を解説しました。参考にされた「Modifier.Nodeを使いましょう」の発表動画はDroidKaigiの公式YouTubeで公開されています。ぜひ見てみてください! www.youtube.com Code Review Challenge An example of a solution DroidKaigi 2023: コードレビューチャレンジの問題解説 安藤 祐貴 / LINE株式会社 speakerdeck.com 安藤さまからはDroidKaigi 2023のブースで出題したコードレビューチャレンジの問題解説を行いました。例外のハンドリング漏れや複雑な関数など、アンチパターンがよくわかる発表でした。 パネルディスカッション パネルディスカッションの様子 パネルディスカッションでは今年最もホットだったトピックスは何だったか、おもしろかったセッション、やってみたいと思った事例などをそれぞれ語り合いました。Jetpack Composeに注目している方が多く、やってみたいといった話も多く出ました。 セッションについてだけではなく、各社ブースでどんな企画をしたかも紹介しました。今回のLTでも紹介されていたCode Review Challengeや、アプリの体験、クイズ、アンケートなど各社の個性が出ていました。 最後に オフライン会場ではノベルティの配布も実施しました パネルディスカッションの後、オフライン会場では交流会を実施しました。各社のノベルティを配布するお楽しみスペースを設け、DroidKaigi当日にもらって嬉しかったものなどの話が盛り上がりました。みなさまご参加ありがとうございました! ZOZOでは一緒にサービスを作り上げてくれるAndroidエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは、ML・データ部MLOpsブロックの 岡本 です。 MLOpsブロックでは日々複数の Google Cloudプロジェクト を管理しています。これらのプロジェクトでは、データサイエンティストやプロジェクトマネージャーなど別チームのメンバーが作業することもあり、必要に応じてメンバーの Googleアカウント へ権限を付与しています。 権限の付与はプロジェクトの管理者であるMLOpsブロックメンバーが行いますが、これは頻繁に発生する作業でありトイルとなっていました。 また権限付与後はこれらを継続的に管理し、定期的に棚卸しすることで不要になった権限を削除する必要があります。しかし当初の運用だと権限の棚卸しの対応コストが大きく、これが実施されずに不要な権限が残り続けるという課題もありました。 本記事ではMLOpsブロックで抱えていたGoogle Cloudプロジェクト内での権限管理における課題と、解決に至るまでの取り組みについて、実際の対応手順と運用後の所感を交えてご紹介します。手順の中には一部地道な手作業もありますが、組織のクラウド利用における権限管理に関する事例として参考にしていただければ幸いです。 目次 はじめに 目次 背景・課題 背景 課題と解決方針 既存権限のTerraform管理への移行 tfファイルの作成 既存IAMロールの一覧化 継続的な権限管理の方針とdriftctlによる省力化 継続的な権限管理の方針 driftctlの導入 driftctlの定期実行と自動化 権限付与・変更依頼の運用フローの見直し 運用後の所感 終わりに 背景・課題 背景 前提として、まずはZOZOにおける全社的な Google Cloud 利用の管理体制についてご説明します。 ZOZOでは多くのチームでGoogle Cloudを使用しており、これらの全社的な管理はワーキンググループという形でいくつかのチームから有志のメンバーが集まって行なっています。以下ではこちらのワーキンググループをGCP Adminと呼びます。 Google Cloudのリソース階層の中にはプロジェクトの上位リソースとしてフォルダ、組織という2つの階層が存在し、GCP Adminは組織レベルの権限を持っています。またGCP Adminでは組織配下にそれぞれのチームごとのフォルダを作成しています。プロジェクトの作成はGCP Adminが各チームから依頼を受けて対応し、プロジェクトはプロジェクト管理者が所属するチームのフォルダ配下に配置しています。 cloud.google.com この体制を取ることで、GCP Adminでは各Google Cloudプロジェクトがどのチームにより管理されているのか把握しやすくしています。加えて、GCP Adminでは組織レベルで主にセキュリティ・コスト観点について複数項目の監視・制限を作成することでガードレールを設け、ガバナンスを効かせています。 これらの取り組みの上で、プロジェクトレベルのリソースの管理については基本的に各チームの管理者に委ねており、ある程度自由度を持ってGoogle Cloudを利用できるようにしています。 ZOZOにおける全社的なGoogle Cloud管理の詳細については、TECH BLOGの「GCPの秩序を取り戻すための試み 〜新米GCP管理者の奮闘記〜」をご参照ください。 techblog.zozo.com MLOpsブロックでは、ZOZOTOWNの推薦・検索といった案件単位でGoogle Cloudプロジェクトを作成しています。またそれぞれの案件ごとにdev(開発環境)・stg(検証環境)・qa(本番環境と類似のテスト環境)・prd(本番環境)を作成しています。これらを合計すると、2023年9月時点では約40のGoogle CloudプロジェクトがMLOpsブロックの管理対象として存在しています。 前述の通りこれらのプロジェクトでは自チームのメンバーだけでなく、データサイエンティストやプロジェクトマネージャーなど他チームのメンバーが閲覧・分析・実験などの作業でリソースを操作します。 こういった中で、MLOpsブロックメンバーは自チームのGoogle Cloudプロジェクト内に存在するリソースを把握し、主にセキュリティ・コスト観点において管理する必要があります。 課題と解決方針 MLOpsブロックでは、 Terraform を利用してほぼ全てのGoogle Cloudリソースの作成・変更をコード化し、これらは全て Git ・ GitHub で管理されています。これにより手動作業によるミスの低減やリソースの変更点の追跡容易性といった恩恵を受けています。 Terraformとは、 HashiCorp, Inc. から提供されているツールであり、インフラリソースの構築をコードで行うInfrastructure as Code(IaC)を実現できます。 またGoogle Cloudでは、アクセス制御を管理する仕組みとして Identity and Access Management(IAM) システムが提供されています。リソースにアクセスするための複数の権限は IAMロール にまとめて、Googleアカウント・ サービスアカウント などのプリンシパルに付与します。 MLOpsブロックで権限付与の依頼に対応する際は、この仕組みで付与対象のGoogleアカウントへ必要なIAMロールを付与します。 しかし歴史的な経緯によりMLOpsブロックでは、Googleアカウントに付与するIAMロールが例外としてTerraform管理されていませんでした。そのため権限付与時はMLOpsブロックメンバーがGoogle CloudコンソールからIAMロールを付与・変更する運用が取られていました。 特に他チームのメンバーへの権限付与では次の定型作業が頻繁に発生していました。 Slack でMLOpsブロック宛に権限付与の依頼が来る 必要に応じて付与するIAMロール・付与先・用途についてヒアリングする MLOpsブロックメンバーがGoogle Cloudコンソールから手動でIAMロールを付与する こちらの運用における課題は主に次の2点です。 権限付与の履歴(誰が誰に何の権限を付与したのか・誰が承認したのか)を確認しづらく、セキュリティ観点での管理コストになる 権限付与・削除のたびにMLOpsブロックメンバーの工数が発生し、運用観点での対応コストになる これらの課題を解決するために、次の対応方針を立てました。 GoogleアカウントのIAMロールをTerraform管理に移行すること 権限付与の依頼・承認・付与の一連の流れをGitHubのPull Request上で行うようにすること 既存の運用に対して見込まれる改善点は次の2点です。 Gitのログや過去のPull Requestを追跡して権限付与の一連の流れを容易に確認できるため、管理コストが削減される MLOpsブロックメンバーの作業はPull RequestのレビューとApprove・Mergeのみになるため、対応コストが削減される MLOpsブロックで利用するTerraformのコードの変更はGitHubのPull Requestを通して行われ、Approve・MergeはMLOpsブロックメンバーのみが行えます。 GoogleアカウントのIAMロールについてもTerraformのコードとして定義することでGit・GitHubにより変更履歴を管理し、権限付与の一連の流れを容易に追跡可能にします。 またTerraformによるリソースの作成はCIにより自動化しています。IAMロールの変更反映時にMLOpsブロックメンバーの手動作業は発生せず、対応コストはPull RequestのレビューとApprove・Mergeのみと軽微なものになります。 次節では上記の方針をもとに具体的に行なった対応内容とつまづいたポイント、最後にしばらく運用を行なった所感をご説明します。 既存権限のTerraform管理への移行 既存権限をTerraform管理へ移行するにあたり、まずはプロジェクトに存在するIAMロールをTerraformのコードで定義し直しました。 MLOpsブロックでは、Googleアカウントに対して基本的に プロジェクトレベルのIAMロール を付与しています。今回はこれらを対象に作業しました。 次に作業手順の詳細についてご説明します。 tfファイルの作成 GoogleアカウントのIAMロールをTerraformのコードで定義するために、まずテンプレートファイル(.tf)を作成します。 ファイル構成については特に次の2点を意識しました。 付与対象のGoogleアカウントをIAMロールごとに一覧できる 利用用途(主にチーム単位)ごとにGoogleアカウントをまとめ、IAMロールの変更漏れを防止する 中心的なファイルは、IAMロールのリソースを定義する iam.tf 、対象のGoogleアカウントを配列にまとめてIAMロールと紐付ける role-binding.tf です。補助として利用用途でGoogleアカウントを配列にまとめているのが members.tf です。 ファイル構成は次の通りです。 . ├── iam.tf ├── members.tf └── role-binding.tf iam.tf は次のように記述します。説明のためプロバイダの指定は省いています。 resource " google_project_iam_member " " viewer " { project = " example-project " for_each = toset ( local.project_viewer_users ) role = " roles/viewer " member = each.value } resource " google_project_iam_member " " bigquery_jobuser " { project = " example-project " for_each = toset ( local.bigquery_jobuser_users ) role = " roles/bigquery.jobUser " member = each.value } role-binding.tf は次のように記述します。 locals { project_viewer_users = concat ( local.analysis_members, local.project_management_members, [ " user:user1@gmail.com ", ] ) bigquery_jobuser_users = local.analysis_members, } members.tf は次のように記述します。 locals { analysis_members = [ " user:analysis-member1@gmail.com ", " user:analysis-member2@gmail.com ", " user:analysis-member3@gmail.com ", ] project_management_members = [ " user:pm-member1@gmail.com ", " user:pm-member2@gmail.com ", ] } 上記のファイル構成をとることで次のメリットがあります。 例として、分析チームの新メンバーへ分析作業に必要な権限を付与する場合を考えます。この際 members.tf のanalysis_members配列に新メンバーのGoogleアカウントを追加することで、必要なIAMロールを一括で付与できます。 特に新メンバーが複数人いる場合や複数のIAMロールを付与する場合、コンソールでの手動作業はIAMロールの付与漏れが発生しやすくなります。 members.tf でGoogleアカウントをグループ化することで作業回数が減り、このようなケースでの作業漏れを低減できます。 また、 role-binding.tf ではIAMロールごとに付与の対象であるGoogleアカウントがグループ化されています。そのためMLOpsブロックメンバーは role-binding.tf を見ることで誰にどのIAMロールが付与されているのか容易に確認できます。 一方、こちらの構成では付与するIAMロールごとに iam.tf ・ role-binding.tf の記述が増えるという懸念があります。 しかしMLOpsブロック管理のプロジェクトでGoogleアカウントに付与するIAMロールは、 roles/viewer ・ roles/editor など基本のロールがほとんどです。その他のIAMロールについては必要に応じてアドホックに付与することが多く、量としては少ないため iam.tf ・ role-binding.tf の記述量は現状特に問題になっていません。 次節では、上記のテンプレートファイルを元に既存権限をTerraform管理化するため、プロジェクト内のIAMロールを一覧化する手順をご説明します。 既存IAMロールの一覧化 プロジェクト内のIAMロールをTerraformのコードで定義し直すには、MLOpsブロックが管理する約40のGoogle Cloudプロジェクトで、既存のIAMロールを一覧化する必要がありました。 プロジェクトのIAMロール一覧を取得するために gcloud CLI でCloud Asset Inventoryの search-all-iam-policies コマンド を利用しました。Cloud Asset Inventoryを用いるとプロジェクト・フォルダ・組織内のIAMポリシーを検索できます。 Cloud Asset Inventoryの詳細については次の公式ドキュメントを参照してください。 cloud.google.com コマンド実行時のscope引数に folder_number を指定することで、特定のフォルダ配下のプロジェクトにあるIAMロールの一覧を取得できます。 folder_number = 999999999 gcloud asset search-all-iam-policies --scope= " folders/ $folder_number " --query= " memberTypes:user " --asset-types= " cloudresourcemanager.googleapis.com/Project " --format= " json(resource,policy) " > iam_policies.json 上記コマンドを実行することで次の出力結果が得られます。 [ { " policy ": { " bindings ": [ { " members ": [ " user:user1@gmail.com ", " user:user2@gmail.com ", " serviceAccount:example@example-project.iam.gserviceaccount.com ", " group:example-group@gmail.com " ] , " role ": " roles/viewer " } , ] }, " resource ": " //cloudresourcemanager.googleapis.com/projects/example-project " }, { " policy ": { " bindings ": [ { " members ": [ " user:user3@gmail.com " ] , " role ": " roles/editor " } , ] }, " resource ": " //cloudresourcemanager.googleapis.com/projects/example-project-2 " } ] gcloudコマンドの出力では、 members フィールドにサービスアカウント・ Googleグループ が含まれます。フォルダ配下の全てのプロジェクトのIAMロールは出力内で配列の要素として数千行に渡り一覧化されています。 コマンドの出力をより見やすくするため、次のPythonスクリプトを作成しました。 サービスアカウントのIAMロールは既にTerraform管理されているためフィルタリングします。またIAMロールごとに付与対象のGoogleアカウントとGoogleグループをグループ化します。その後でプロジェクトごとにjsonファイルを作成して出力を分割しました。 import json import typer from pathlib import Path from typing import Optional def format_json (path: Optional[Path] = typer.Option( None )): output_dir_name = "outputs" Path(output_dir_name).mkdir(exist_ok= True ) with open (path, encoding= "utf-8" ) as f: folder_iam_list = json.load(f) for project_iam in folder_iam_list: project_id = project_iam[ "resource" ].split( '/' )[- 1 ] members_of_role = {} for role_binding in project_iam[ "policy" ][ "bindings" ]: role = role_binding[ "role" ] for member in role_binding[ "members" ]: if not (member.startswith( "user:" ) or member.startswith( "group:" )): continue members_of_role.setdefault(role, []) members_of_role[role].append(member) with open (f '{output_dir_name}/{project_id}.json' , encoding= "utf-8" , mode= "w" ) as f: json.dump(members_of_role, f) if __name__ == "__main__" : typer.run(format_json) 次のシェルスクリプトは上記のgcloudコマンドおよびPythonスクリプトを一度に実行します。 ./run.sh 999999999 のように対象の folder_number を指定してスクリプトを実行すると、 outputs ディレクトリ内にファイルが作成されます。これらはプロジェクトIDをファイル名として持ち、 folder_number で指定したGoogle Cloudフォルダ内のプロジェクトごとに example-project.json の形式で作成されます。それぞれのファイルにはプロジェクト内のIAMロールの一覧が出力されます。 #!/bin/bash set -eu folder_number = $1 gcloud asset search-all-iam-policies --scope= " folders/ $folder_number " --query= " memberTypes:user " --asset-types= " cloudresourcemanager.googleapis.com/Project " --format= " json(resource,policy) " > iam_policies.json poetry run python main.py --path iam_policies.json rm iam_policies.json outputs/example-project.json の出力例は次の通りです。IAMロールごとにGoogleアカウント・Googleグループを一覧化した結果を得られています。 { " roles/viewer ": [ " user:user1@gmail.com ", " user:user2@gmail.com ", " group:example-group@gmail.com " ] } 次にプロジェクトごとに生成されたjsonファイルを元に、既存のIAMロールをTerraformのコードとして定義し直しました。こちらの転記は地道に手作業で進めました。手作業による移行漏れのリスクについては、後述する差分検知の仕組みによりカバーが可能なため、ここでは問題としていません。 また、 role-binding.tf で共通の権限を持つGoogleアカウントについてはTerraformの配列としてまとめて members.tf に定義しています。 role-binding.tf では members.tf にまとめた配列を対象にIAMロールを紐付けました。 本節の手順によりプロジェクトのIAMロールをTerraformのコードとして定義し直し、既存の権限をTerraform管理下に置くことができました。 次節では、これらを継続的に管理するための方針と、ツールを導入することによる管理コストの省力化についてご説明します。 継続的な権限管理の方針とdriftctlによる省力化 継続的な権限管理の方針 上記の作業により、一時的にプロジェクト内のIAMロールがTerraform管理できている状態を作れましたが、それだけでは継続的にこの状態を維持できません。 IAMロールを変更できるロールを持つGoogleアカウントは、GitHubのPull Request上での権限付与の一連の流れを無視して、コンソール・CLIで既存のIAMロールを変更できてしまいます。 こうなるとTerraformのコードで管理されているIAMロールと実際にプロジェクト内に存在するIAMロールの間に差分が生じます。これでは権限付与の履歴を確認できない当初の課題が再発しています。 これはプロジェクトの権限管理の運用ルールとして、IAMロールを変更できるロールはCIのサービスアカウントのみに付与し、Googleアカウントへ付与しないことで一応回避できます。しかしこれでは緊急時にすぐ権限を付与できず、障害対応に支障が出ます。 MLOpsブロック管理のプロジェクトではセキュリティ・事故防止の観点からGoogleアカウントに対しdev環境を除いてはリソースの変更が可能なIAMロールを付与していません。特に本番環境では、プロジェクトの管理者である1人以外には基本的に閲覧以外のIAMロールを付与していません。 しかし本番稼働するAPI・バッチを運用・保守するMLOpsブロックメンバーについては、障害発生時に緊急対応のため手動でリソースを変更したいケースが発生し得ます。 このように緊急度の高いケースにおいては、Pull Requestを作成しての権限付与フローでは障害対応が遅れ、致命的な損失につながる可能性があります。これを避けるために、MLOpsブロックメンバーには本番環境においてプロジェクトのIAMロールを変更できるロールを例外的に付与しています。 そこでMLOpsブロックでは管理方針として、Terraform管理されているIAMロールとプロジェクト内に存在するIAMロールの差分を定期的に確認し、適宜修正するようにしました。 これにより全てのGoogleアカウントのIAMロールがTerraform管理された状態を継続的に維持できます。 この方針では定期的に差分の確認作業が発生しますが、ツールにより差分の確認を自動化することでMLOpsブロックメンバーの対応コストを最小限に抑えました。 次節ではこちらのツールと自動化による差分検知の仕組みについてご説明します。 driftctlの導入 Terraform管理されたIAMロールと、実際にプロジェクト内に存在するIAMロールの差分を確認するため、 driftctl というツールを採用しました。driftctlはApache-2.0 licenseで利用できますが、現在Beta版での提供となっている点にご注意ください。 driftctlは Snyk Ltd. により開発されたGo製のCLIツールで、IaCのコードが実際に存在するリソースをどの程度カバーできているか測定できます。またdriftctlではコンソール・CLIなど、Terraform以外の方法でGoogleアカウントに対してIAMロールが付与された場合の差分も検知できます。 Terraform管理されたリソースのみの差分検知であれば terraform plan コマンドで引数に -detailed-exitcode を指定し、コマンド終了時のexit codeにより差分の有無を判別することで対応できます。 一方でこの方法は、Terraform以外の方法でGoogleアカウントに対してIAMロールが付与された場合の差分は検知できません。 こちらの差分も検知できるという点で、driftctlはTerraform管理されているIAMロールとプロジェクト内に存在するIAMロールを完全一致させたいという今回の要件にマッチしました。 次にdriftctlの使い方を簡単にご説明します。 CLIは次のようにcurlやbrewでインストールできます。 # Linux # x64 $ curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_linux_amd64 -o driftctl # macOS $ curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_darwin_amd64 -o driftctl $ brew install driftctl # Windows # x64 $ curl https://github.com/snyk/driftctl/releases/latest/download/driftctl_windows_amd64.exe -o driftctl.exe Terraformと実際のリソースの差分は driftctl scan コマンドで確認でき、必要な環境変数を渡して実行することで差分を出力できます。 コマンドの詳細については次の公式ドキュメントを参照してください。 docs.driftctl.com Google Cloudを対象にする場合は Cloud Asset API の有効化と認証するアカウントに対してプロジェクトの roles/cloudasset.viewer ・ roles/viewer のIAMロールの付与が必要です。 Google Cloudでの認証の詳細については次の公式ドキュメントを参照してください。 docs.driftctl.com driftctl scan の実行時に参照するTerraformのStateは --from 引数で指定できます。特に指定しない場合はカレントディレクトリ配下のHCLを自動的に読み取り、使用するtfstateファイルを探します。 driftctl scan の実行と出力結果の例は次の通りです。 GOOGLE_APPLICATION_CREDENTIALS =your-creds.json \ CLOUDSDK_CORE_PROJECT =example-project \ driftctl scan --to gcp+tf { " options " : { " deep " : false , " only_managed " : false , " only_unmanaged " : false } , " summary " : { " total_resources " : 6 , " total_changed " : 0 , " total_unmanaged " : 0 , " total_missing " : 0 , " total_managed " : 6 , " total_iac_source_count " : 1 } , " managed " : [ { " id " : " example-project/roles/viewer/user:user1@gmail.com " , " type " : " google_project_iam_member " , " source " : { " source " : " tfstate+gs://bucket/terraform/terraform.tfstate " , " namespace " : "" , " internal_name " : " resourcemanager_projectiamadmin " } } , { " id " : " example-project/roles/bigquery.jobUser/user:analysis-member1@gmail.com " , " type " : " google_project_iam_member " , " source " : { " source " : " tfstate+gs://bucket/terraform/terraform.tfstate " , " namespace " : "" , " internal_name " : " bigquery_jobuser " } } , ... 省略 ] , " unmanaged " : null, " missing " : null, " differences " : null, " coverage " : 100 , " alerts " : null, " provider_name " : " gcp+tf " , " provider_version " : " 4.80.0 " , " scan_duration " : 1 , " date " : " 2023-09-14T00:00:00.000000+00:00 " } またdriftctlではコマンド実行時に --filter 引数を指定して、差分として検知する対象の絞り込み・除外ができます。 引数なしの場合、プロジェクト内のすべてのGoogle Cloudリソースを対象にTerraform管理されたリソースとの差分を出力します。今回差分を出力したいGoogle CloudリソースはプロジェクトのIAMロールのみです。そのため対象をプロジェクトのIAMロールに絞り込みました。加えてプロジェクトに存在するIAMロールにはサービスアカウントのロールも含まれるため、合わせて検知対象から除外しました。 上記の絞り込み・除外を考慮した driftctl scan の実行コマンドは次の通りです。 GOOGLE_APPLICATION_CREDENTIALS =your-creds.json \ CLOUDSDK_CORE_PROJECT =example-project \ driftctl scan --to gcp+tf --filter $' (Type== \' google_project_iam_member \' && contains(Id, \' user: \' )) ' 上記により、Googleアカウントに付与されたIAMロールを対象にTerraform管理されたリソースと実際のリソースの差分を出力できます。 しかし上記のコマンドではGoogle CloudのAPI呼び出しのRate Limitにより、 driftctl scan の実行時に次のエラーが発生しました。 rpc error: code = ResourceExhausted desc = Resource has been exhausted ( e.g. check quota ) . こちらについては 同様のエラーについてのIssue が報告されており、 コメントで提案されていた方法 を参考に対応しました。 Rate Limitに引っかかった原因はGoogle Cloud上のリソース数が多く、リソース取得のためにGoogle CloudのAPIを呼び出す回数が多くなっていることでした。これは driftctl scan で取得する対象のリソースを絞り込むことで解決できました。 この絞り込みには .driftignore を利用しました。 .driftignore に除外したいリソースを記述することで、 driftctl scan の取得対象から除外できます。前述の --filter 引数での除外は複雑な除外の用途で使用し、単に一連のリソースを除外するのみであれば .driftignore を使用することを公式ドキュメントでは推奨しています。 docs.driftctl.com 次の記述を .driftignore に追加し、 google_project_iam_member 以外のリソースを driftctl scan の対象から除外しました。 * はワイルドカードでの指定となるため、すべてのリソースを対象から除外し、その上で google_project_iam_member を除外対象から外すという記述になります。 # Ignore all drifts except for google_project_iam_member * !google_project_iam_member これにより、必要のないリソースが driftctl scan の取得対象となることを回避し、上述のRate Limitによるエラーを解消できました。また driftctl scan の実行時間も大幅に短縮されました。 次節ではdriftctlコマンドをCIで自動実行する手順についてご説明します。 driftctlの定期実行と自動化 MLOpsブロックではCIとして GitHub Actions を利用しています。 差分の確認を定期実行するため、GitHub Actionsの Scheduleトリガーイベント によりdriftctlコマンドを定期実行するワークフローを作成しました。このワークフローはTerraform管理されたリソースと実際のリソースに差分を確認するとSlackに通知します。 ワークフローを作成した後で気がつきましたが、GitHub Actionsでのdriftctlの実行については開発元であるSnyk Ltd.から driftctl-action が提供されています。詳細については次の公式ドキュメントを参照ください。 docs.driftctl.com ワークフローの大まかな流れは次の通りです。 GitHub Actionsで利用するGoogle Cloudのサービスアカウントを認証して、 driftctl scan 実行時に必要な認証情報を環境変数にセットする driftctlをインストールし、 driftctl scan コマンドにより差分を確認して結果を出力する 出力された結果の coverage の値を確認し、差分が見つかった場合は exit 1 で終了する 差分が見つかった場合のみSlackで通知する 上記の流れの2・3(driftctlのインストールから差分の確認まで)の実装は、driftctl-actionで代替できます。 以下では独自実装のワークフローについてご説明します。 ワークフローの定義は次の通りです。 name : check_user_account_project_iam_member_role_drift on : schedule : - cron : '0 1 * * 4' permissions : contents : 'read' id-token : 'write' jobs : check-user-account-project-iam-member-role-drift : runs-on : ubuntu-20.04 strategy : fail-fast : false matrix : cfg : - DIR : terraform_dir PROJECT_NUMBER : 202020202020 defaults : run : working-directory : ${{ matrix.cfg.DIR }} outputs : coverage : ${{ steps.run_driftctl.outputs.coverage }} steps : - uses : actions/checkout@v3.5.2 - name : authenticate to gcp uses : 'google-github-actions/auth@v1.1.1' with : workload_identity_provider : projects/${{ matrix.cfg.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/example-pool/providers/github-actions service_account : ci-service-account@example-project.iam.gserviceaccount.com create_credentials_file : true export_environment_variables : true - name : setup driftctl run : | curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_linux_amd64 -o driftctl chmod +x driftctl mv driftctl /usr/local/bin/ - name : run driftctl id : run_driftctl run : | output=$(driftctl scan --quiet --to gcp+tf --filter $'(Type==\'google_project_iam_member\' && contains(Id, \'user:\'))' --output json://stdout | jq 'del(.managed)' ) echo "coverage=$(echo " $output" | jq .coverage)" >> $GITHUB_OUTPUT echo $output | jq . - name : check coverage if : ${{ steps.run_driftctl.outputs.coverage != 100 }} run : | echo "The driftctl result coverage value is not 100. Please check run driftctl step output." && exit 1 slack-notice : needs : [ check-user-account-project-iam-member-role-drift ] runs-on : ubuntu-20.04 if : ${{ failure() }} steps : - uses : slackapi/slack-github-action@v1 with : payload : | { "blocks" : [ { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : "<!channel>" } } , { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : ":rotating_light: ユーザーアカウントのロールとterraformの構成に差分があります:rotating_light: \n 以下のURLからrun driftctlステップの差分を確認し、差分の解消を行って下さい。 \n\n *GitHub Actions URL*: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} \n\n *差分検知後の対応フロー*: 対応ドキュメントのリンク" } } ] } env : SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL_NOTICE }} SLACK_WEBHOOK_TYPE : INCOMING_WEBHOOK 続いてワークフローに含まれるJobごとの処理の流れをご説明します。 初めに check-user-account-project-iam-member-role-drift Jobについてご説明します。 認証処理の記述は次の箇所です。 まず、 authenticate to gcp StepではCIで利用するGoogle Cloudのサービスアカウントの認証を行なっています。 MLOpsブロックでは、サービスアカウントの権限を利用するすべてのリポジトリで認証に Workload Identity連携 を利用しています。ここでは google-github-actions/auth を利用しています。 前述の通り、 driftctl scan の実行時には GOOGLE_APPLICATION_CREDENTIALS (認証情報)と CLOUDSDK_CORE_PROJECT (プロジェクト名)を指定する必要があります。認証時の引数に create_credentials_file: true と export_environment_variables: true を指定し、環境変数として参照できるようにしています。 - name : authenticate to gcp uses : 'google-github-actions/auth@v1.1.1' with : create_credentials_file : true workload_identity_provider : projects/${{ matrix.cfg.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/example-pool/providers/github-actions service_account : ci-service-account@example-project-${{ matrix.cfg.ENV }}.iam.gserviceaccount.com export_environment_variables : true driftctlコマンドの実行処理の記述は次の箇所です。 setup driftctl Stepでは前述した手順に従ってdriftctl CLIをインストールして実行権限を付与しています。続く run driftctl Stepでは実際に driftctl scan を実行しています。 ここでJobにデフォルトで指定している working-directory には各IAMロールを定義したtfファイルが配置されているディレクトリを指定します。指定したディレクトリ配下には取得対象から除外するリソースを記載した .driftignore ファイルを置いています。 また --quiet 引数や jq によって出力結果を整形することで、差分検知のアラートがなった際にノイズとなる情報を取り除いています。 次に check coverage Stepで driftctl scan の出力結果を確認しています。 出力のうち coverage キーの値にはTerraform管理されたリソースが実際のリソースをどの程度カバーできているかを測定した値が入っています。こちらの値が100であれば、実際に存在するリソースがすべてTerraform管理された状態です。 if: ${{ steps.run_driftctl.outputs.coverage != 100}} により値を確認し、差分があった場合はログを出力して exit 1 で終了しています。 - name : setup driftctl run : | curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_linux_amd64 -o driftctl chmod +x driftctl mv driftctl /usr/local/bin/ - name : run driftctl id : run_driftctl run : | output=$(driftctl scan --quiet --to gcp+tf --filter $'(Type==\'google_project_iam_member\' && contains(Id, \'user:\'))' --output json://stdout | jq 'del(.managed)' ) echo "coverage=$(echo " $output" | jq .coverage)" >> $GITHUB_OUTPUT echo $output | jq . - name : check coverage if : ${{ steps.run_driftctl.outputs.coverage != 100 }} run : | echo "The driftctl result coverage value is not 100. Please check run driftctl step output." && exit 1 通知処理の記述は次の箇所です。 slack-notice Jobでは差分が検知された場合にのみSlack通知を送ります。差分があると前段の check coverage Stepは失敗します。この時 slack-notice Jobでは if: ${{ failure() }} を指定しているためJobがトリガされ、Slack通知処理が走ります。Slack通知処理には slackapi/slack-github-action を利用しています。 また送信するメッセージにはGitHub ActionsのRunのURL及び、差分解消の対応フローを記述したドキュメントのリンクを含めています。これによりMLOpsブロックメンバーであれば誰でも差分解消の作業ができるようにしています。 slack-notice : needs : [ check-user-account-project-iam-member-role-drift ] runs-on : ubuntu-20.04 if : ${{ failure() }} steps : - uses : slackapi/slack-github-action@v1 with : payload : | { "blocks" : [ { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : "<!channel>" } } , { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : ":rotating_light: ユーザーアカウントのロールとterraformの構成に差分があります:rotating_light: \n 以下のURLからrun driftctlステップの差分を確認し、差分の解消を行って下さい。 \n\n *GitHub Actions URL*: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} \n\n *差分検知後の対応フロー*: 対応ドキュメントのリンク" } } ] } env : SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL_NOTICE }} SLACK_WEBHOOK_TYPE : INCOMING_WEBHOOK 実際に差分が検知された場合のSlack通知は次の通りです。 通知があった場合は、メッセージに記述されたGitHub ActionsのRunのリンクに飛ぶことで検知された差分の内容を確認できます。 本節ではTerraform管理されているIAMロールと実際にプロジェクト内に存在するIAMロールについて、自動で差分を検知する仕組みを作成しました。これによりMLOpsメンバーの対応工数を抑えつつ、プロジェクト内に存在するGoogleアカウントのIAMロールをすべてTerraform管理できている状態を維持できるようになりました。 次節では、システム面以外での運用フローの改善についてご説明します。 権限付与・変更依頼の運用フローの見直し 既存の運用フローについて、Slackメッセージベースの依頼のフローから、GitHubのPull Requestベースでの依頼フローへの見直しについてご紹介します。 既存の運用フローでは権限の付与・変更時に、MLOpsブロックメンバーが主体となって作業する部分がほとんどでした。一方でGitHubのPull Requestベースのフローでは主な作業は依頼者が行なうようにしています。 依頼者は権限付与・変更のPull Requestを作成し、レビュアにMLOpsブロックの Team をアサインします。MLOpsブロックメンバーはPull Requestをレビューし、問題なければApprove・Mergeします。Terraformによるリソースの作成は元々CIにより自動化されているため、MLOpsブロックメンバーの作業は基本的にPull RequestのレビューとMergeのみになります。 実際の権限付与の依頼例は次の通りです。 権限付与が必要な背景はPull RequestのDescriptionに記載されており、MLOpsブロックメンバーはGit・GitHubの履歴を辿ることで、権限付与の経緯などを後でも確認できます。 一方で、GitHubのPull Requestベースのフローには次の2つの懸念点もあります。 Terraform経験が少ない依頼者にTerraformコードの記述を強制する必要がある 依頼者の対応工数が増える 1つ目について、権限付与の依頼者はほとんどがエンジニアですが、データサイエンティストなど普段Terraformを使わない方も多いです。そういった方に権限付与の依頼のためにわざわざTerraformの文法の履修を強制することは総工数を増やすだけでなく、依頼のハードルを上げることにもなります。 こちらについてはドキュメントを充実させ、Terraformの知識がない方でもコピー&ペーストで作業ができるようにすることで負荷を軽減しました。ドキュメントには権限の付与・変更・削除など想定される依頼パターンごとのPull Request作成例を記載するだけでなく、プロジェクトごとにリソースを定義したリポジトリの対応表など作業で必要な情報をまとめています。 これによりプログラミング経験、Gitの利用経験があればどなたでも作業可能な状態を作っています。 2つ目は、依頼者にとって権限付与の依頼は頻繁に発生する作業ではない点・基本コピー&ペーストで済む作業であれば対応工数も少なく済む点から許容できると判断しました。 最終的に、GitHubのPull Requestベースの新しい運用フローについて手順書を作成し、作成した手順書と運用フローの変更について関係者に周知しました。既存の運用では依頼時に利用する固定のSlackチャンネルはなかったため、MLOpsブロックのチャンネルで関連チーム向けにアナウンスし、同チャンネルにメッセージをピン留めする方法を取りました。 MLOpsブロックで抱えていたGoogle Cloudプロジェクト内でのGoogleアカウントの権限管理における課題について、解決のために行なった取り組みはこれで全てとなります。 運用後の所感 MLOpsブロックが管理するプロジェクト内での権限管理における課題解決、継続的な権限管理の仕組み導入と依頼時の運用フロー見直しについて、実際に約4か月運用した現在の所感をお話しします。 まずGoogleアカウントへのIAMロールの付与・変更をTerraform管理に移行した点では、権限の一覧性向上・コード上でGoogleアカウントをグループ化できる点で非常に管理しやすくなりました。特に後者については、Googleアカウントをチームごとに配列でまとめることで、チームメンバーが増えた際の権限付与や退職者の権限削除にかかる工数の削減と対応漏れの防止につながるというメリットがありました。 余談ですが、Googleアカウントをグループ化してIAMロールを付与することはGoogleグループでも対応可能です。ただしこの方法のデメリットとして、ユーザーは独自にグループへのメンバー追加ができるという点があります。これはMLOpsブロックメンバーが把握できないところで権限が付与されてしまうといった問題や、誰にどの権限が付与されているのかが分かりにくいといった権限管理上の問題を引き起こします。 こういった理由から、ZOZOのGoogle Cloud利用においてはGoogleグループへのIAMロールの付与を廃止し、個別のGoogleアカウントへの付与に移行しました。また、Googleグループに対するIAMロールの付与があればGCP Adminで検知できる監視の仕組みを導入しています。 次に、driftctlによる差分検知の導入についてです。dev・stg環境での軽微な作業の際にコンソールから一時的に付与したIAMロールの消し忘れなど、普段の運用の中での見落としを防ぐことにも非常に役立っています。また、差分検知の仕組みの運用・保守のコストは、Rate Limitエラーが発生した以外にほとんど発生していません。ワークフロー自体も非常にシンプルであるため、運用・保守のコストに対してチームのパフォーマンス改善に大きく貢献できています。 また、権限付与の運用フローを見直した点では、依頼者が権限付与のPull Requestを作成する運用にしたことで、MLOpsブロックメンバーの対応コストは大幅に低減されたと感じています。それ以外にも次のメリットを感じています。 変更がCIにより反映されるようになったことで、権限の付与など対応漏れの防止になる 依頼を見落としている場合に、GitHubのPull Requestレビューのリマインダーにより気がつける 一方で運用後に残っている課題として、権限付与の新しい運用フローが依頼者に浸透しきっていないという点があります。上述のように利用者への周知は行いましたが、たびたびSlackメッセージでの依頼は届いており、その都度MLOpsブロックメンバーがアナウンス時のメッセージのリンクを共有して再度周知しています。 また依頼者がTerraformのコードを書いてPull Requestを書くハードルについても、許容はできますが課題として残っています。 今後はこういった依頼時の認知・作業負荷の削減を考えており、 Slackワークフロー を利用したPull Request作成の自動化の仕組みによりこれを実現することを検討しています。 終わりに 最後までお読みいただきありがとうございました。 本記事ではMLOpsブロックが抱えていたGoogle Cloudプロジェクトの権限管理における課題とその解決方法について、実際の対応手順と運用後の所感を交えてご説明しました。本記事での事例の紹介が皆様のお役に立てば幸いです。 最後になりますが、ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用しているので、ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。9月20日に ZOZO Tech Talk #8 - Go を開催しました。ZOZOのエンジニアがGoを利用した開発事例を紹介する、ランチタイムのイベントです。 登壇内容まとめ 弊社から次の2名が登壇しました。 コンテンツ 登壇者 UseCaseの凝集度を高めるGoのpackage戦略 ブランドソリューション開発本部 バックエンド部 / 田村誠基 事業成長を加速させるGoのコード品質改善の取り組み ブランドソリューション開発本部 バックエンド部 / 田島太一 当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com UseCaseの凝集度を高めるGoのpackage戦略 ブランドソリューション開発本部 バックエンド部 / 田村誠基 speakerdeck.com 田村からはショップスタッフの販売サポートツール「FAANS」における改善を発表しました。これまでの構成は依存関係が増加しているという問題や、似た命名のstructがあることで見通しにくくなっているという課題がありました。パッケージを細かく分割することでこれらの問題点が解決されたそうです。実際試して良かったところ、気になるところも紹介しました。 事業成長を加速させるGoのコード品質改善の取り組み ブランドソリューション開発本部 バックエンド部 / 田島太一 speakerdeck.com 田島からはGoのコード品質改善のために取り組んだことを5つ紹介しました。徐々に厳しくするLinter設定、スタイルガイド「Google Go Style Guide」の導入、エラーハンドリングの改善、凝集度を高める実装パターン、ボーイスカウトルールによる既存コードの継続的改善の5つです。詳細はYouTubeやスライド資料をご覧ください。 最後に 質疑応答の様子 それぞれの発表の後には質疑応答も行いました。多くのご質問ありがとうございました! 皆さまの開発のヒントになっていれば幸いです。 ZOZOではGoを活用し、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは、SRE部 検索基盤SREブロックの花房と大澤です。普段はZOZOTOWNの検索関連マイクロサービスのインフラ運用を担当しています。 ZOZOTOWNの検索基盤では、商品検索に関わる大規模なデータを取り扱うためにElasticsearchを利用しています。Elasticsearchを運用していく中で、私たちはパフォーマンスとインフラコスト、運用トイルの問題に直面していました。本記事では、私たちが抱えていた問題と、それを解決したアプローチとしてシャーディング最適化とオートスケーラー開発の取り組みについてご紹介します。 目次 はじめに 目次 背景・課題 パフォーマンスの課題 インフラコストの課題 運用トイルの課題 解決策 シャーディング最適化 Elasticsearchのシャーディング ノードのインスタンスタイプ変更 負荷試験によるパフォーマンス検証 コスト見積 安全なリリース方法 導入効果 オートスケーラー開発 オートスケール方針の検討 オートスケーラーの設計と開発 夜間のスケールインと日中のスケールアウト 売り上げ予想に基づくノードサイズ(メモリサイズ)の算出 (1). 過去のデータの取得 (2). 最大リクエスト量の予測 (3). 必要ノード数(メモリサイズ)の算出 PRの自動生成とマージ ノード拡張(terraform applyの実行) 拡張監視 導入効果 おわりに 背景・課題 ZOZOTOWNでは、2017年からレガシーシステムのリプレイスを実施しています。レガシーシステムの刷新の中で、検索基盤においてはElasticsearchの導入・運用を開始しました。リプレイスの進展や、おすすめ順検索の利用拡大に伴い、検索基盤へのトラフィックは増大し、Elasticsearchへの負荷も増加していきました。しかし、Elasticsearchのインフラ運用の知見は十分に溜まっておらず、パフォーマンス・インフラコスト・運用トイルの3つの側面で課題が発生しました。 最初に、それら3つの側面の課題について紹介いたします。 パフォーマンスの課題 検索基盤では、ZOZOTOWNの商品情報の更新のために、Elasticsearchへのデータ投入を毎分実行しています。この高頻度のデータ投入と、マイクロサービス側での大量の検索リクエストにより、クラスタ全体のCPU負荷は常に高い状態でした。ノード数を増やしても処理速度の向上は見られず、さらに負荷が増大すると、レスポンスやインデキシングが遅れ、求めるパフォーマンスを維持できなくなります。そのため、負荷増加を招く可能性のある新たな施策には挑戦しにくい状態になっていました。 インフラコストの課題 検索基盤のElasticsearchは、Elastic社が提供するElastic Cloud上で稼働しています。フルマネージドなElastic Cloudではノードの稼働時間にコストが比例します。当然のように思えますが、自前で管理するElastic Cloud on Kubernetes (ECK) 1 の場合、利用量でなくライセンスの最大量で契約を結ぶ必要があるため、ノード数を下げてもライセンスの料金の節約には繋がりません。ZOZOTOWNの場合、時間帯や日別のイベントによりトラフィックが大きく変化します。ノード数の変動が大きい弊社では、コストカットができるようECKではなくElastic Cloudを採用しています。 リクエスト量は時間帯によって大きく異なるため、それに応じてリソースを最適化できれば余計なコストを減らせます。以下の図は、時間ごとの検索リクエスト数のグラフです。横軸が時間、縦軸が秒間リクエスト数を示しています。 Elastic Cloudにはディスク利用量トリガーによるオートスケール機能は存在しますが、リクエストやCPU負荷に応じてスケールする仕組みは存在しません。そのため、リクエストが少ない時間帯(深夜、日中など)でも、常にピークタイムのリクエストを処理できるノード数を準備して対応していました。これによりZOZOTOWNのインフラの中でも、特に膨大なインフラコストが発生しており、そのコスト削減が課題になっていました。 運用トイルの課題 ZOZOTOWNは、土日の午後9時頃にかけてリクエスト数が最も多くなる傾向を持っています。ピークである土日のリクエスト数を捌けるノード数を常時抱えておくのはコストが勿体ないため、週末のみスケールアウトする運用にしています。この運用では、土日の売上予算からリソース見積を算出する作業と、Elasticsearchのスケールアウト作業を手動で行っていました。作業はテンプレート化されていますが、運用トイルとなっている状況が問題でした。下記のスクリーンショットは、その作業のGitHub Issueであり、チームメンバー内の輪番により毎週対応していました。 解決策 パフォーマンス課題の解決案としては、シャーディングによる負荷分散を試しました。シャーディングの詳細については後述します。さらに、ノードをCPUコア数の多いマシンへ変更することも検証しました。これによりCPUの余力を作り、シャーディングの効果を一層引き出せると考えたためです。 運用トイルの課題に対しては、ZOZOTOWNの売上予測をベースに必要なリソース量を算出し、Elasticsearchを自動スケールさせるような独自の仕組みを開発しようと考えました。既存のツールでは、私たちの運用に合うオートスケーラーが存在しなかったためです。 パフォーマンス課題と運用トイル課題の解決は、結果的にインフラコストの課題解決にも繋がりました。以降では、3つの課題の解決策となったシャーディング最適化とオートスケーラー開発の詳細についてご紹介します。 シャーディング最適化 Elasticsearchのシャーディング シャーディングとは、インデックスを分割して複数のノードに保持させることです。分割されたデータをシャードと呼びます。下記にノードとシャード、レプリカの関係を図で示します。ノード数6、シャード数4、レプリカ数2の場合の例です。 CPUリソースに余裕がある場合、複数ノードでの並列処理により、クエリのレイテンシは改善します。各ノードで処理するデータがシャーディングにより分割されたデータ量に絞られるためです。 2 ただし、並列処理によりクラスタ全体のCPU利用は増加し、各ノードの処理結果をマージするオーバーヘッドも増加するため、スループットの悪化を招いたり、さらにレイテンシ悪化の可能性もあります。 インデックスのシャード数は、インデックス作成時に下記のパラメータを指定することで設定できます。 { " settings ": { " index " : { " number_of_shards ": 4 } } } このパラメータはインデックスごとに個別での設定が必要です。さらに、後からの変更は不可能であり、変更が必要な場合はインデックスを再作成する必要があります。今回の検証では、2シャードと4シャードの設定を試しました。 ノードのインスタンスタイプ変更 シャーディングの効果を引き出すためにはCPUリソースの余裕が必要です。そのため、ノードのインスタンスタイプをCPUコア数の多いマシンへ変更することにしました。 ノードのインスタンスタイプは、Elastic Cloudの管理画面から設定できます。管理画面には「hardware profile」という項目が存在し、そこでは「General purpose」や「CPU optimized」といった選択肢があります。この設定変更により、用途に合わせたリソースのインスタンスタイプの利用が可能です。私たちはAWSをクラスタのクラウドプロバイダとして選択しているため、ノードにはAWSのインスタンスタイプが適用されます。 今回のインスタンスタイプ変更では、「hardware profile」を「General purpose」から「CPU optimized」に変更しました。これにより、以前適用されていたm5dインスタンスから、c6gdインスタンスへの変更が行われます。私たちはTerraformコードにより適用しました。下記に、弊社で利用しているTerraformコードの例を示します。 resource "ec_deployment" "zozo_tech_blog" { region = "ap-northeast-1" version = "7.17.0" deployment_template_id = "aws-cpu-optimized-arm-v6" # この部分を変更 name = "ZOZO TECH BLOG" elasticsearch { autoscale = "false" topology { id = "hot_content" size = "60g" zone_count = "3" size_resource = "memory" } topology { id = "master" size = "8g" zone_count = "3" size_resource = "memory" } } } m5dとc6gdのCPUの比較は下記の通りです。 m5d c6gd vCPUコア数 16 32 プロセッサ Intel製 ArmベースのAWS Graviton2 実は、以前にもインスタンスタイプ変更によるパフォーマンス向上を検証したことがあります。その際もm5dからc6gdへの切り替えを検証しました。検証の結果、99パーセンタイルのレイテンシが100msほど悪化したため、この変更は本番環境にリリースできませんでした。当時はインデックスが1シャード構成であり、CPUコア数が多くても効果的な並列活用ができず、CPU1コアのパフォーマンスがレイテンシに直接影響したためと考えました。 最終的に、インスタンスタイプ変更によるCPUの2倍確保で書き込みと読み込みが相互影響しにくい状態にした上で、さらに余ったCPUを効率良く利用できるようにシャーディングを行う狙いになりました。 負荷試験によるパフォーマンス検証 負荷試験では、本番環境と同じ構成のステージング環境を利用します。今回はElasticsearchの検証を実施するため、ステージング環境に対して下記を設定しました。 ステージング環境のアプリケーションを検証対象のElasticsearchクラスタに接続 アプリケーション側のキャッシュを無効化し、Elasticsearchに負荷がかかるように設定 アプリケーションのリソースだけでなく、Elasticsearchへのインデキシングも本番環境と同様に動作する状態に変更 負荷試験におけるアプリケーションへのリクエスト数はElasticsearchのノード数に応じて変化させ、下記の条件の組み合わせで試験を実施しました。 3 負荷試験の実行では、弊社で開発しているOSSであるGatling Operatorを活用しています。Gatling Operatorについては こちらの記事 をご参照ください。 条件 内容 インスタンスタイプ c6gd インデックスのシャード数 2, 4 Elasticsearchのノード数 3 ~ 年間最大トラフィック想定の台数 リクエスト数(req/sec) 50 ~ 年間最大トラフィック 今回の負荷試験では、主にアプリケーション側の99パーセンタイルのレイテンシと、Elasticsearchのスループット、インデキシングバッチの処理時間の3つに焦点を当てています。また、4シャードの結果が2シャードよりも良好であったため、これまでの構成と4シャードの新構成で比較しました。下記は年間最大トラフィックでの比較結果です。 項目 1シャード x m5dインスタンス 4シャード x c6gdインスタンス 99パーセンタイルレイテンシ 670ms 370ms 1ノードあたりのスループット 40 query/sec 40 query/sec インデキシングバッチ処理時間 120分 60分 比較結果から、新構成はスループットを維持したまま、2つの項目では優れていることが確認できました。それぞれの項目について、考察を下記にまとめます。 99パーセンタイルレイテンシ シャーディングにより4つのノードに処理が分散されたことで、レスポンスを結合するオーバーヘッドを含めても、処理時間を短縮できた結果だと考えています。 1ノードあたりのスループット シャード数が4に増えた分、各シャードへのクエリ回数は4倍に増加するためスループットは減少しそうですが、今回はスループットを維持できています。これはインスタンスタイプ変更によるCPUコア数の増加の効果だと考えています。 インデキシングバッチ処理時間 新構成では旧構成と比較してCPUが2倍になったため、書き込みと読み込みが別々のCPUで処理されることが多くなり、書き込みでCPUを占有できるようになった結果、処理時間が短縮したと考えています。反対に、旧構成ではCPUの数が少なく、1つのCPUで書き込みと読み込みの両方を担うことが多かったため、処理に時間がかかっていたと考えています。また、シャードが分かれたことにより、インデキシングを並列で実行できるようなった効果も大きいと考えています。 コスト見積 リリースの判断を下すためには、インフラコストの見積が必要です。最終的なコストの詳細は記載できませんが、各要素について説明します。 1ノードのインスタンスサイズについては、メモリ容量で決定しています。Elastic Cloudのインスタンスは、基本的に他のユーザとリソースを共有する環境です。私たちのElasticsearchノードと、他のユーザのノードが同じインスタンス上に配置される可能性があります。その場合、リソースを取り合う形になってしまうため、安定した性能が発揮できないこともあります。しかし、一定以上のメモリサイズを指定することで、占有リソースのインスタンスが利用可能です。そのため、私たちはメモリサイズが60GBのインスタンスを「Hot data and Content tier」として使用しています。このサイズのノードについて、今回比較したインスタンスタイプの料金は2023年9月時点でそれぞれ以下の通りです。 m5d c6gd 1時間あたりのコスト $3.648 $4.452 コスト見積では、過去のリクエスト数データと負荷試験の結果を元に必要なノード数を計算し、Elastic Cloudの料金情報に基づいてコストを算出しました。1時間あたりのコストの計算方法は下記の通りです。 ( 検索APIのリクエストによって発生するElasticsearchへのクエリ量[query/sec] / ノード1台のスループット[query/sec] ) x ( ノード1台の1時間あたりのコスト[$] x ドル円為替[¥/$] ) x バッファ 下記2パターンについて、以前の構成と比較した際のコスト削減率を記載しています。 コスト算出パターン 以前の構成と比較した際のコスト削減率 最もリクエスト数が多い日のコスト -20% 年間コスト -35% 新構成では1台あたりのインスタンスコストが上がってしまうため、全体のコストも上がるように思います。しかし、レイテンシに関してはガードレール指標を定めており、レイテンシの改善分をガードレール指標まで落とすことでスループットを増やせました。その結果、ノード数を削減してコスト削減が実現できました。 安全なリリース方法 負荷試験結果から新構成のパフォーマンス、コストに問題がないことは分かりました。しかし、本番環境にリリースしてユーザトラフィックに晒した時、想定外のエラーやパフォーマンス悪化といったリスクが発生しないとは言い切れません。そのため、カナリアリリースによる安全なリリースを実施します。カナリアリリースは手動ではなく、 以前の記事 でご紹介した、Flaggerを用いたプログレッシブデリバリーにより実行しました。Flaggerはプログレッシブデリバリーを実現するKubernetes Operatorであり、検索基盤のマイクロサービスには導入済みです。 旧構成から新構成に切り替える方法は2つ考えられました。 同じクラスタ内でインデックスを作成し、エイリアスを切り替える案 旧クラスタと同じデータを持つ新クラスタを作成し、アプリケーション側から接続するElasticsearchのエンドポイントを切り替える案 シャーディングのみ適用すると切り替え期間は2つのインデックスへの書き込みが必要になり、CPUリソース不足になる可能性があります。また、インスタンス変更のみ適用するとレイテンシ悪化の可能性があります。そのため、今回は両方を同時に適用できる2つ目の方法を選択しました。 エンドポイントの切り替えは、Kubernetes上の検索APIの deployment リソースに記載している環境変数の更新により行います。また、新クラスタへの接続情報を格納する secret の作成と、それを参照するよう変更も行いました。これらの更新により、Flaggerが新クラスタへ接続する検索APIのカナリアバージョンを作成し、カナリアリリースを自動で進めてくれます。エラーの多発やレイテンシの悪化が発生した場合は、Flaggerにより自動的に元のクラスタを向いているバージョンに切り戻されるため、安全なリリースが実現できました。下記はプログレッシブデリバリーがKubernetes上でどのように実施されるかを表した図です。 リリースまでの手順は以下の通りです。まず検証環境で本手順通りにリリースの予行演習をし、問題がないことを確認しました。その後、本番環境で本手順通りにリリースしました。 新クラスタの構築 新旧2つのクラスタへのインデキシングを開始 プログレッシブデリバリーでのクラスタ切り替え 旧クラスタへのインデキシングの停止 導入効果 シャーディング最適化による導入効果を改めて下記にまとめます。 分散処理による、検索リクエストのレイテンシおよびインデキシング処理時間の改善 スループット向上によるコストカット 本番環境のレイテンシには変動があるため、1か月ほど様子を見た上で改善されたと判断しました。改善幅は負荷試験時ほどではありませんでしたが、99パーセンタイルレイテンシは平均で約150ms改善されました。本番環境と負荷試験との主な違いは、サイト内セールの影響や、それに伴う検索リクエスト種類の割合です。これらの要因や、コスト削減のためにレイテンシを落としてスループットを上げたことにより、負荷試験時とは異なる結果になったと考えています。インデキシングバッチについては、約4.5時間かかっていた処理が半分以下の約1.7時間で完了するようになりました。さらに、トイル削減にも繋がっており、セールイベント時に行っていたインデキシングバッチのチューニングが不要になりました。 シャーディング最適化によって、パフォーマンスとインフラコストの課題について解決できました。以前はCPUに余裕がなく断念せざるを得なかったパーソナライズに関する施策にも挑戦できるようになりました。 オートスケーラー開発 前述の通りインフラコスト・運用トイルを改善するためのアプローチとして、日中夜間・平日休日に適したリクエスト量を見積もり、かつ適切なノード数へ自動変更する手法を模索しました。この章ではSREチーム独自の仕組みとしてオートスケーラーを開発するに至った経緯と、その概要についてご紹介します。 オートスケール方針の検討 SREチームではElastic Cloud上でElasticsearchクラスタを運用しています。可能であれば公式に提供されている機能を利用したいところです。しかしながら、現在のところElastic Cloudよりスケジュール設定やCPU使用量、リクエスト量に応じたノード数変更の仕組みは提供されていません。そのため、実現方法としては下記の2つを検討しました。 既存のOSSを利用する 独自の仕組みとして開発する 既存のOSSを利用する方法として、 elastic-cloud-autoscaler を検討しました。このOSSは、Elastic CloudのAPIを通してスケジュールやクラスタ負荷に応じたノード数へ変更できます。導入にあたって検証を進めたところ、SREチームでの運用に不適ないくつかの課題が確認できました。 以前の記事 にて紹介のとおり、SREチームではTerraformによるコード管理にてElasticsearchクラスタを運用しています。これによりクラスタの状態とリリースブランチのコードの状態を常に一致させるという安定した運用基盤を構築しています。 OSSのオートスケーラーによる運用では、Terraformのコードとクラスタの状態との間に差分が生じてしまうことが最大の問題点でした。OSSのオートスケーラーはコードの状態を考慮せずにノードのスケーリングを実施します。この振る舞いによりコード差分が生じ、緊急時にTerraformでクラスタの構成を変更しようとすると、予期せぬ問題やエラー発生の可能性があります。また、本OSSで提供されているスケジュール設定についても、私たちの運用には合っていないことが分かりました。ZOZOTOWNでは、日によってセールなどの各種要因によりElasticsearchへ流入するリクエスト量が異なります。そのため、適切なノード数も日によって異なり、シンプルなスケジュール設定は合わなかったのです。 さらに、Elastic Cloudからは将来的にサーバーレスアーキテクチャが提供される予定もあり、オートスケーラーは一時的な運用となる可能性もあります。これらの点を加味し、SREチームの運用に適した、独自の仕組みを持つオートスケーラーを導入する方針が固まりました。以下がSREチームで検討した独自オートスケーラーの要件です。 クラスタの状態はTerraformのコードで管理する (Single Source of Truth) クーポンやイベント、広告により日々異なる売り上げ予想から、日々適切なノード数を自動で見積り、自動でスケールを行う 既存の仕組みを利用し、最小限の実装でオートスケールを実現する Elasticsearch以外に、Kubernetesのpod数変更や検索ドメイン以外でも利用できるような汎用的な作りにする オートスケーラーの設計と開発 SREチームの現在のクラスタ運用は、GitHub Actions (GHA) のCI/CDにより実施しています。リリースブランチへのPRマージをトリガーにGHAが起動し、 terraform apply を実行することでクラスタの状態とコードの状態を一致させています。この運用方法をベースに、最小限の実装でオートスケーリングを実現するため、GHAで新しいワークフローを構築しました。以下がワークフローの概要になります。左が今回新しく構築したオートスケール全体を担うワークフロー、右がPRマージ時にCI/CDで動作する既存のワークフローです。 大きく以下の要素で構成されています。 夜間のスケールインと日中の段階的なスケールアウト 売上予算に基づく必要ノード数(メモリサイズ)の算出 PRの自動生成とマージ ノード拡張(terraform applyの実行) 拡張監視 各要素のポイントを以降で説明します。 夜間のスケールインと日中のスケールアウト 時間帯で異なるトラフィック傾向に合わせて、以下のようにGHAのcronを設定しています。 9時 : 昼休みの時間帯のトラフィック増に備えてピーク時リソースの8割に増加 16時 : ゴールデンタイムのトラフィック増に備えてピーク時リソースの10割に増加 25時 : トラフィックのピークを過ぎたためピーク時リソースの6割に減少 売り上げ予想に基づくノードサイズ(メモリサイズ)の算出 Pythonスクリプトにて毎日の売上予算を基に、オートスケール実施日のリクエスト量を予測し最適なノード数を算出しています。以下は算出プロセスの詳細です。 (1). 過去のデータの取得 過去N日分の売上予算と最大リクエスト量を取得します。SREチームではElasticsearchの各種メトリクス情報をDatadogに送信しているため、DatadogのAPIを利用して過去N日分の最大リクエストを取得します。 (2). 最大リクエスト量の予測 取得したN日分の過去のデータをもとに、重回帰分析を実施し当日の売上予算から最大リクエスト量を予測します。 def calculate_trend (self, y_values, x_values, target): x = np.array(x_values) # x軸:過去の売上予算 y = np.array(y_values) # y軸:過去のリクエスト量 # 傾きと切片を計算 slope, intercept, _, _, _ = linregress(x, y) # 売上予算に対するリクエスト量を計算 trend_value = intercept + slope * target # target:当日の売上予算 return trend_value (3). 必要ノード数(メモリサイズ)の算出 terraformによるノード数の変更はメモリサイズ単位で行います。そのため、予測された最大リクエスト量をもとに、必要となるノード数を計算し必要メモリサイズへ変換します。 PRの自動生成とマージ クラスタを構成するterraformファイルには以下のようにタグ # {autoscaling} を設定しています。shellコマンドでこのタグが設定された行を検出し、算出したメモリサイズへファイルを更新します。 # タグを設定したterraformファイル topology { id = "hot_content" size = "900g" # {autoscaling} zone_count = "3" size_resource = "memory" } # sedコマンドによるタグ検出とファイル更新例 esfile=terraform/elastic_cloud/es-cluster.tf sed -i '/# {autoscaling}/s/size = "[0-9]*g"/size = "${{ env.ESTIMATE_SIZE }}g"/' ${esfile} ファイル更新後はリリースブランチへPRを作成し自動でマージしています。 ノード拡張(terraform applyの実行) リリースブランチへのPRマージを機に既存のCI/CDプロセスがトリガされ、クラスタの状態が最新のコードに一致するように更新されます。 拡張監視 監視方法として、Elasticsearchのヘルスチェックエンドポイントを叩き、クラスターステータス、Unassigned Shards数を監視します。なお、スケールインの失敗はサービスに最悪の影響を及ぼさないため許容できます。しかしながら、スケールアウトの失敗はピークタイム時のサービスに重大な影響を及ぼすため、万一の失敗時は即時対応が必要となります。そのため、スケールアウト時に一定時間経ってもクラスターステータスが正常にならない場合や、ワークフロー実行中にエラーが発生した場合にはPagerDutyでオンコールする仕組みを導入しています。 導入効果 オートスケーラーの導入により以下の効果が得られました。 インフラコスト削減:適切なリソース管理により、オートスケール導入前と比較し月額コストが20%改善しました。 運用トイル削減:輪番担当の際の負荷見積りやノード数調整、監視にかかる時間が無くなり、作業負荷が軽減されました。 これらの効果により、サービスの運用管理がスムーズになり、より価値のある開発や改善に時間とリソースを割くことが可能になりました。また、今回は記載を省略しましたが、Elasticsearchのノード数スケールと同様の方法で、検索APIのpod(minReplicas設定)についてもオートスケールの対象にしています。 おわりに 本記事では、Elasticsearchを運用していく中で私たちが直面した問題と、その問題をシャーディング最適化とオートスケーラー開発によって解決する過程をご紹介しました。 今回の取り組みによって、Elasticsearchのパフォーマンス、インフラコスト、運用トイルの改善を進めることができました。Elasticsearchの運用で同様の課題を抱えている方がいれば、ぜひ参考にしてみてください。 今後は、シャーディングのさらなる最適化とオートスケーラーのブラッシュアップ、その他にもワークロードに合わせたクラスタの分割などの施策を進め、改善を継続していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co https://github.com/elastic/cloud-on-k8s ↩ シャーディングにより、デフォルトでは単語頻度の計算がシャードごとになります。全てのシャードから収集した情報で、グローバルな単語頻度を計算させるには、 search_type として dfs_query_then_fetch を指定する必要があります。 https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-type ↩ クラスタの拡張縮退を繰り返す際はマスターノードのリソースを増やしておくことを推奨します。シャード再配置に失敗し、クラスタが利用不可になる可能性があるためです。 ↩
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。9月11日に After iOSDC Japan 2023 を開催しました。9月1日〜3日に開催されたiOSDC Japan 2023の協賛企業であるLINE株式会社、PayPay株式会社、株式会社ZOZO、ヤフー株式会社の4社合同での振り返りイベントです。 登壇内容まとめ 4社の社員による発表の後、パネルディスカッションを行いました! コンテンツ 登壇者 風レーダーを支える技術 冨田 悠斗 / ヤフー株式会社 15分でお伝え!iOSDC Japan 2023におけるZOZOの取り組み 加藤 祥真 / 株式会社ZOZO パフォーマンスモニタリングの取り組み かしはら / PayPay株式会社 LINEアプリのサポートバージョンの考え方 富家 将己 / LINE株式会社 Q&A & パネルディスカッション giginet, 長谷川 健, 中岡 黎, Shota Kashihara 当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com 風レーダーを支える技術 冨田 悠斗 / ヤフー株式会社(撮影:ヤフー株式会社 たなたつさま) www.docswell.com 冨田さまからは風が視覚的にわかる「風レーダー」を実装するための技術についての発表がされました。風レーダーについては動画がわかりやすいため、ぜひYouTubeのアーカイブをご覧ください。風レーダーはオーバーレイをしている情報が2つあったり、アニメーションをしていたりと複雑な画面でした。その実装にはMetalというグラフィックAPIを使っているそうです。難しそうな印象を持ちやすい部分ですが、意外と簡単に実装ができるそうでコードも含めて紹介していました。風レーダーの描画でのみMetalを使い、計算部分はSwiftで行うことで、低いコストで運用できるようにしているのは良い工夫だと感じました。 15分でお伝え!iOSDC Japan 2023におけるZOZOの取り組み 加藤 祥真 / 株式会社ZOZO 弊社の加藤は初のLTということで、会場からはあたたかい拍手が送られていました。スポンサーブースの取り組みの紹介と、登壇までの流れ・サポート体制を発表しました。現地には10名以上の社員が参加し、ブースに来てくださった皆さまとお話をしました。会場ではMiroを使ってアンケートを行い「Vision Pro買う?」「これ勉強してます!」「みんなと話したいこと」などをお聞きしました。回答の詳細は iOSDC Japan 2023参加レポートブログ をぜひ読んでみてください! CfPのネタだし&ネタレビュー会のドキュメントも一部紹介しました。チームでのレビューやDevRelブロックによるレビューを経て、採択されるようにプロポーザルを改善します。「サポートが手厚い」「自信を持って話せそう」とたくさんの反響をいただきました! パフォーマンスモニタリングの取り組み かしはら / PayPay株式会社(撮影:LINE株式会社 佐藤さま) かしはらさまからは、パフォーマンスの継続的なモニタリングについて発表がありました。ホーム画面のバーコード表示を例として、計測を導入するフローが丁寧に説明されました。実際に取り組む中でFirebase Performance Monitoringでは足りない部分に気付き、Looker Studioを使うようにしたのはリアルな知見だなと思いました。実際に改善したホーム画面のBefore・Afterの比較はとてもわかりやすかったです。 LINEアプリのサポートバージョンの考え方 富家 将己 / LINE株式会社(撮影:LINE株式会社 佐藤さま) speakerdeck.com 古いOSバージョンをサポートするメリット・デメリットや、対応方法などを整理して発表いただきました。どこまでサポートするかはみなさん悩む問題ですよね。LINEのサポートバージョンと、それぞれのユーザシェアを公開いただけるのは助かります。しかし、LINEは全世界のOSシェアを監視しており、サポート終了のタイミングは遅めなので参考にするのはおすすめしないそうです。日本のリージョンはアップデート率が高いので、自身のサービスのシェアを確認して判断するのが良いとのことでした。 Q&A & パネルディスカッション 質問1. どのセッションが面白かった? まずは乾杯をしてQ&A & パネルディスカッションがスタートしました。ひとつめの質問は「どのセッションが面白かった?」です。パネリストがフリップを使って紹介していきました。当日見切れなかった分の動画を見返すヒントになりますね。 質問2. オンライン or オフラインどちらで参加した?(撮影:ヤフー株式会社 水田さま) ふたつめの質問は「オンライン or オフラインどちらで参加した?」です。オンラインではセッションが見やすく、子育てをしながらでも見られるのが助かるという意見がありました。 質問3. イベントの企画で楽しかったこと 「イベントの企画で楽しかったこと」は3名から「LTのペンライト」という意見が出ており、とても好評でした。今回の新しい取り組みでインパクトがありましたね。「ご飯」「スポンサーブース」もあげられていました。ZOZOのブースでARメイクを試して楽しかった、ノベルティの手鏡が家族に喜ばれた、と言っていただきとても光栄です! 質問4. 自社の取り組み(撮影:ヤフー株式会社 水田さま) 「自社の取り組み」としてはCfPをたくさん出す、CfPレビュー、協賛、スポンサーセッションなどがあげられました。各人はじめての挑戦も見られ、カンファレンスはとても良い機会だなと感じました。最後にiOSDC Japan 2024への抱負を語り、パネルディスカッションは終了しました。パネルディスカッションの後、オフライン会場では交流会も行いました。 最後に オフライン・オンラインともに多くのご参加ありがとうございました。iOSDC Japan 2023の余韻を楽しみつつ、新しい知見も学べる場となったのではないでしょうか。今後もiOSに関するイベントを開催していきますのでご期待ください! ZOZOでは一緒にサービスを作り上げてくれるiOSエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、ECプラットフォーム部会員基盤ブロックの turbofish です。弊社ではモノリスのプログラムで動いているZOZOTOWNをマイクロサービス化する取り組みを行なっており、複数チームが1つの大きなオンプレシステムをマイクロサービスでリプレイスしています。その中で私が所属する会員基盤ブロックでは、ZOZOTOWNの会員情報を管理するマイクロサービスを開発しています。 本記事では、弊チームを含む複数のマイクロサービス開発チームにおいて、既存のアプリケーションの一部をマイクロサービスを使用する処理に置き換えた際、サービス無停止でオンプレ環境にあるDBからマイクロサービスが使用するクラウド環境のDBにデータを移行した戦略を紹介します。 ディスクレイマー 本記事で紹介するデータ移行方法には下記の制約があり、全ての状況に対応できるわけではありません。 DBへの書き込み処理と読み取りの処理の実装リリースタイミングを分けられない場合は実行できません プライマリキーを持たず、テーブル内のレコードを一意に識別できない場合はデータの正しさを保証できません また、この記事で紹介する方法は、手間をかけてでもデータ移行の際にサービス停止を発生させないことにフォーカスしています。同様の戦略を検討する際は、データ移行を実行する環境において全ての過程を実現できるか、サービス停止を回避するためにどの程度の手間をかけられるかなどを考慮の上、状況に応じた戦略を決定する必要があります。 本記事において、随所で「オンプレ」「クラウド」という単語を用いていますが、サーバーの場所は本記事で紹介する戦略に影響しません。あくまで本記事に登場する複数のDBを見分けるための名前だと考えてください。 目次 はじめに ディスクレイマー 目次 データ移行の背景と前提 データ移行の要件と課題 採用した戦略 戦略決定の背景 具体的な移行手順 1. データ調査と(必要に応じて)データ修正 2. DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正 3. データ移行手順のテスト実行 4. オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納 5. 一時DBに格納されたデータをクラウドDBへ格納 6. クラウドDBに保存されたデータの検証 7. データを参照する処理を、マイクロサービスAPIを使用するよう修正 工夫した点 移行元と移行先のDBのレコードのID(プライマリキー)が同じ値になるようにする データ移行前後でマイクロサービスAPIの実装を変える データ格納のロジック実装時に、データの削除方法を考慮する データ格納後のデータ検証のやり方 データ移行結果 データ移行にかかった時間 苦労した点 まとめ データ移行の背景と前提 ZOZOTOWNのマイクロサービス化の背景については、過去の記事をご覧ください。 techblog.zozo.com 弊社では各マイクロサービスが専用のDBを持ち、リプレイスを通じてDBMSとDBスキーマを変更したいと考えていました。そのため、マイクロサービスを使用する実装をリリースする際に、オンプレ環境で使用しているDBからマイクロサービスで使用するクラウド環境にあるDBへ、データをコピーする必要がありました。 以降、オンプレ環境で使用しているデータ移行元のDBを「オンプレDB」、クラウド環境にあるマイクロサービスで使用しているデータ移行先のDBを「クラウドDB」と表記します。また、オンプレDBからデータを抽出し、クラウドDBへ格納するETL処理のことを「データ移行」と呼びます。 本来、安全にデータを移行するためには、一旦サービスを停止してDBに書き込みがされない状態にしてからデータをコピーすることが最も確実です。特に会員基盤チームでは、ユーザーの個人情報を含むデータを扱うため、データの欠損や不整合は許されませんでした。しかし、ZOZOTOWNは非常にアクセスが多く、サービス停止による機会損失が大きいことから、多少の手間を許容してでもデータ移行に伴うサービス停止を無くす方法を考える必要がありました。 データ移行の要件と課題 弊社のマイクロサービス化のプロジェクトに共通する、データ移行の要件は下記の通りです。 移行元と移行先のDBMSの種類が異なる オンプレDBはMicrosoft SQL Server、クラウドDBはAmazon Aurora MySQLを使用 移行元と移行先のDBでスキーマが異なる 移行元のDBは長い歴史を経て最適な状態ではなくなっており、マイクロサービス側のDBでテーブルを新設する際にスキーマを再設計する必要がある データ移行1回につき、扱うデータ量は大体の場合多くても3千万件程度 ダウンタイムなしでデータを移行する必要がある 上記の要件を考慮し方針を検討したところ、下記の課題がありました。 DBの種類やテーブルスキーマが違うため、レプリケーションによって複製したDBをそのまま用いることができない 移行過程でテーブルのスキーマ変更に対応するため、柔軟なデータ変換を実現する必要がある マイクロサービス化を推進するためにも、移行元であるオンプレDB依存なアーキテクチャにはしたくない CDC(Change Data Capture)のような仕組みは極力採用したくない 採用した戦略 単純にDBやデータをコピーするだけでは、データの正しさとサービス無停止を両立することは難しいと考えられました。そこで、下記の通りアプリケーションレイヤーで工夫することで、上述した課題を全てクリアできると考えました。 オンプレDBに書き込みを行う処理を修正し、オンプレとクラウドの両DBにアトミックに書き込みを行うよう実装する データ移行でオンプレDBのデータをクラウドDBにコピー。この時、アプリケーションはオンプレDBのデータを参照している状態 オンプレDBのデータを参照している処理を修正し、マイクロサービスAPIからデータを取得するよう実装する 戦略決定の背景 データを移行してからマイクロサービスAPIを使用するアプリケーションの実装をリリースすると、データ抽出の開始時から実装リリースまでの間の時間に発生するデータ変更処理がクラウドDBに反映されません。つまり、サービス停止をせずにデータの正しさを担保するためには、アプリケーション実装リリースの後にデータを移行する必要がありました。 一方で、その場合アプリケーション実装リリース時にはクラウドDBに全くデータが存在しない状態となります。そのため、マイクロサービスのデータを参照せず、DBへの書き込み処理のみをオンプレとクラウド両方のシステムで行われるよう実装することで、サービスにクラウドDBの状態を表出させることなくデータ移行の時間を確保しようと考えました。正しくデータがDBに書き込まれる状態にしてからデータを移行することで、両方のDBに正しく全てのデータが揃っている状態を実現します。最後にクラウドDBのデータを参照する実装をリリースし、データ移行の過程は完了します。 具体的な移行手順 データ移行のために実行した手順をまとめると、下記の通りです。 データ調査と(必要に応じて)データ修正 DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正 データ移行手順のテスト実行 オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納 一時DBに格納されたデータをクラウドDBへ格納 クラウドDBに保存されたデータの検証 データを参照する処理を、マイクロサービスAPIを使用するよう修正 以下、それぞれの過程を具体的に説明します。 1. データ調査と(必要に応じて)データ修正 オンプレDBとクラウドDBのスキーマが異なる場合、もしくはクラウドDBに格納するデータが移行元のオンプレDBと異なる場合は、データを抽出した後にどうやって変換するかを検討する必要があります。オンプレDBの既存データが、マイクロサービスAPIで受け付けられる状態になっているかという観点で調査しました。例えば、マイクロサービスAPIが電話番号や郵便番号に全角文字や数字以外の文字を許容しない場合は、既存のデータにそれらが含まれていないか。enumのような特定の選択肢を期待しているデータに例外がないか、移行先のクラウドDBのカラムサイズを超えるデータがないかなどを確認しました。 データを変換する必要がある場合は、下記のどちらかを検討する必要があります。 オンプレDBに存在するデータをクラウドDBのスキーマに合わせて修正する 後述するデータ変換・データ格納のプロセスでデータを変換する 2. DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正 データを移行する前に、DBに書き込みを行う全ての処理においてオンプレとクラウドの両DBがアトミックに更新されることを担保します。アプリケーションコードにおいて、1つのトランザクション内でオンプレDBへの書き込み処理とマイクロサービスAPIの処理を行い、オンプレDBとクラウドDBの間でデータの整合性を取ります。この、「オンプレDBとクラウドDBに書き込む処理をアトミックに実行する」ことを、本記事では「ダブルライト」と呼びます。 ダブルライトの実装がリリースされた時点での、DB書き込みを伴うユーザー操作の処理を図示すると下記のようになります。 ダブルライトの具体的なフローの例として、会員情報を登録する処理は下図のようになります。フロー図はイメージが湧きやすいよう実際にチームで使用している図と近いものを使用していますが、マイクロサービスAPIを叩く際に必ず通過するAPI Gatewayについては、この記事の範疇を超えるため他の図では省略しています。 注意点として、この時クラウドDBに当たる会員基盤DBにはデータが何もないため、マイクロサービスAPIは、更新・削除APIにレコードが存在しないIDを指定したリクエストが来たとしても、オンプレDBと整合性のとれたデータが保存されるように実装する必要があります(詳細は「工夫した点」として後述します)。 3. データ移行手順のテスト実行 データ移行に失敗するとDBに何度も高い負荷をかけることになるので、本番環境での作業ミスを防止するため、本番環境での移行前に練習としてデータ移行のテスト実行を行いました。弊チームでは、実際にDBにどの程度の負荷がかかるのかを計測するため、より本番環境に近いデータを保持するステージング環境を使用しました。データ修正実行後に本番環境のデータを(個人情報をマスクして)ステージング環境にコピーし、ダブルライトの実装を全ての環境にリリースしたのち、ETL処理をテスト実行しました。 4. オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納 ダブルライトの実装をデータ移行に先立ってリリースすることにより、データが正しくDBに書き込まれることを担保したら、いよいよオンプレDBのデータをクラウドDBに移行するETL処理を実行します。本記事で紹介する手順では、データのETL処理をいくつかのツールを用いて実行します。データ抽出&変換(ET)にEmbulkというツールを使用し、データ格納(L)についてはGo言語でツールを自作しました。それら2つを、KubernetesのJobで実行しました。 データ移行中にもユーザー操作によるDBへの書き込み処理が継続していることを考えると、単純に移行元のオンプレDBのデータを全て抽出して移行先のクラウドDBに挿入するだけでは、要件を満たすことができません。そのため、まずはクラウド環境にデータ移行のための専用のDBを作成し(以降、このDBのことを「一時DB」と表記)、オンプレDBのデータを抽出して保存することにしました。 以降において、「データ抽出」はオンプレDBのデータを全て抽出すること、「データ変換」は抽出したデータをクラウドDB互換のスキーマに変換し、一時DBに保存することを指します。 データ抽出&変換には、社内での使用実績もある Embulk を使用しました。Embulkは、移行元のDBからselectしてデータを抽出し、YAMLで書かれた設定ファイルに沿ってテーブルを作成し、データを変換して挿入することが可能です。プラグインを用いて異なる種類のDB間での移行も可能で、弊チームの要件である、SQL ServerからMySQLへのETLに対応したプラグインもありました。Embulkの設定ファイルにデータ格納時のクエリを設定できるため、シンプルなロジックであれば、テーブル構造の差分を解決できました。例えば、移行元のDBにある日本語の文字列を移行先では英語のEnumで扱いたいといった場合には、Embulkが使用するクエリ中にswitch文を記載することでデータを変換してくれます。SQL文での実現が難しい、より複雑なロジックでデータを変換する必要がある場合は、この次に説明するデータ格納の際に使用する自作ツールでデータを変換するロジックを実装し、データ格納時に変換できます。 この時点の状況を整理すると、一時DBのデータはアプリケーションからアクセスされないため、データ抽出開始より後に発生したユーザー操作によるDBへの書き込み処理は反映されません。一方で、クラウドDBには常に書き込み処理が行われている状態です。 5. 一時DBに格納されたデータをクラウドDBへ格納 一時DBからクラウドDBへデータを格納する処理を、本記事では「データ格納」と呼びます。 既にダブルライトの処理が本番環境にリリースされているため、クラウドDBは随時ユーザー操作に伴いデータが更新される状態になっています。つまり、一時DBにはデータ移行プロセス開始以前の状態のデータが更新されない状態で保存されていて、クラウドDBにはデータ移行プロセス開始後の最新のデータだけが保存されている状態です。 そこで、すでにクラウドDBに存在する最新のデータは更新せずに、一時DBに保存されているデータをクラウドDBに格納するツールを自作することにしました。 データ格納ツールは、基本的には一時DBのレコードを1件ずつクラウドDBに挿入します。但し、既に最新のデータが存在するDBに更新されていない状態のDBのデータを格納するため、既存のツールではカバーできないやや柔軟な機能が必要でした。データ格納処理において、クラウドDBと一時DBに同じIDのレコードが存在した場合、データ抽出後にデータ更新処理が行われた可能性が高いと考えられます。そのため、アプリケーションからクラウドDBに直接書き込まれたデータは上書きせず、そのまま処理を継続しました。クラウドDBにレコードがすでに存在した場合は、念のためそのレコードがデータ抽出開始より後に作成または更新されていることを確認しました。 格納の処理は1,000レコードずつのバッチ処理で行いました。IDが重複した場合にエラーで処理が止まらないよう、複数レコードの同時挿入に失敗した時には重複したIDのレコードを避けて、成功するレコードのみ再度挿入するような実装にしました。 6. クラウドDBに保存されたデータの検証 移行元のDBと移行先のDBのデータに齟齬がないことを確認します。データ格納ツールの実行完了後に、データを検証する自作ツールを実行しました(詳細は「工夫した点」として後述します)。 7. データを参照する処理を、マイクロサービスAPIを使用するよう修正 クラウドDBがオンプレDBと同様のデータを保持した状態であることを確認したら、DBのデータを参照する処理にマイクロサービスAPIを使用する実装をリリースできます。リリース後の処理フローは下図のようになります。 工夫した点 移行元と移行先のDBのレコードのID(プライマリキー)が同じ値になるようにする データのETL処理が正しく行われていることを検証するため、プライマリキーでデータを識別し、移行元と移行先のDBに保存されているレコードを比較します。そのため、新規にレコードを追加する処理においては、オンプレDBで発行されたIDをマイクロサービスAPIに渡し、両方のDBでプライマリキーを一致させるようにしました。必然的に、いずれオンプレDBへの書き込みをせずにマイクロサービスAPIのみを使用するよう実装する際、レコードを追加するマイクロサービスAPIを修正する必要があります。 データ移行前後でマイクロサービスAPIの実装を変える クラウド本番DBにデータが存在しない状態でマイクロサービスAPIがリリースされるため、APIはデータ更新もしくは削除処理の際にデータが存在しなくても、404エラーを返さないようにしておく必要があります。つまり、APIの実装としては、更新処理でもレコードが存在しなければ登録する、UPSERTのような処理を行うようにしました。マイクロサービスAPIにおいてDBにデータが存在しない場合にエラーレスポンスを返すようにするためには、データ移行後にハンドリングを入れた実装をリリースする必要があります。 データ格納のロジック実装時に、データの削除方法を考慮する データ格納ロジックは、データのCRUDを行うアプリケーションロジック、テーブルを跨ぐデータの関係などに影響を受けます。特に注意する必要があるのは、DBへのデータ挿入、更新、削除のうち削除の処理です。データを物理削除するか論理削除するかによって、対応方法が変わります。 削除パターンが論理削除の場合 論理削除フラグをもつテーブルの場合は、データ格納において特別なロジックを追加する必要はありません。一方、削除済みデータ用のテーブルを持つタイプのデータの場合は、必要な全てのテーブルについてETL処理を行う必要があり、データ格納用のツールの仕様も複雑になります。メインのデータを保存するテーブルにあるレコードの状態が一時DBのレコードと異なる場合に、削除済みのデータが保存された別のテーブルをチェックするよう実装する必要があります。全てのデータ更新をクラウド環境内で検知できるため、オンプレ環境からの移行を考えている場合には物理削除のパターンよりもこちらの方が比較的実装は楽かもしれません。 削除パターンが物理削除の場合 物理削除されるデータの場合は、データ抽出開始後に削除されたデータのIDをDBから確認できません。そのため、一旦一時DBの全てのデータを格納してから、削除されるべきレコードのIDを特定してデータ格納完了後に削除する必要がありました。 データ移行期間中に発生する削除処理の回数が少ないケースでは、データ抽出開始〜データ格納完了の間に処理した削除リクエストのログからIDを抽出し、クラウドDBに直接SQLを流して削除しました。 短時間に大量に削除リクエストが発行されるアプリケーションの場合、一時的に削除されたレコードを保存する別テーブルを作成し、APIの削除処理を論理削除を行う実装にした上で、データ移行が完了したら物理削除を行う実装に修正するなどの方法も考えられます。 データ格納後のデータ検証のやり方 弊社では、移行元のオンプレDBへのアクセスを多くのマイクロサービスから行いたくなかったため、オンプレDBとクラウドDBのデータを直接突合させるツールを作成できませんでした。そこで、マイクロサービスが保持する一時DBとクラウドDBのデータに齟齬がないかをまずは確認し、齟齬があった場合はログを出力して後で調査しました。データ抽出開始より後に作成もしくは更新されたと考えられるデータの場合は、スプレッドシートなどを用いてオンプレDBのデータと突合しました。物理削除された可能性があるデータの場合は、一時DBに保存されているIDを出力し、アプリケーションログにて削除処理が行われたことを確認しました。もし検証プログラムが移行元と移行先の両DBにアクセスできるのであれば、それぞれのDBのレコードが全て一致していることを確認するだけでクラウドDBに正しくデータが移行されたことを確認できます。 データ移行結果 結果として、この戦略は要件を全て満たす状態で成功し、ノウハウが社内で共有され複数のチームで採用されるようになりました。会員基盤チーム以外の事例も含めて、移行にかかった時間や手間などについて紹介します。 データ移行にかかった時間 約3千万件のデータ移行で、データ抽出&変換は40分、データ格納は2時間程度で完了し、ETL処理の工程は1日あれば全て完了しました。一方で、イベントを記録する場合などデータ量がより多くなる場合には、数億件のデータを1週間かけて移行した事例もありました。データ移行にかかる時間は、DBのバージョンやスペック、データの複雑さ、レコードの件数と大きさ、移行中にDBにかかる負荷など、複数の要因に影響を受けます。 ※ 移行対象のデータの数はデータ移行の対象となったテーブルの行数を示しており、ZOZOTOWNの会員数もしくは会員情報の総数などを表すものではありません。 苦労した点 データ移行の前にダブルライトの処理をリリースすることにより、それぞれの過程で考えることが非常に多くなり、自作ツール開発などの追加作業が発生しました。加えて、マイクロサービスAPIを使用する実装のリリースタイミングを2段階設けたことにより、リリース前に行う必要があるテストを複数回実施する必要があり、これも工数が増えた要因でした。特にダブルライトのテストは画面上からデータを確認できないため、操作時に入力したデータをスプレッドシートに記録しておき、オンプレとクラウドそれぞれのDBのデータと突合する必要がありました。 ノウハウが溜まっていない時期には、考慮漏れにより全ての工程をやり直したこともありました。データ調査を怠ってデータ変換に失敗したり、データ検証の際にダブルライトの実装が漏れていたことに気づいたこともありました。前者のデータ修正の追加対応を行なったケースでは、オンプレDBのデータ調査、データ修正ののち、データ移行の工程を全てやり直し、結局データ移行を開始してから完了するまでに10時間弱かかりました。 また、データ移行のやり方がデータの性質によっても大きく変わってくるため、データの修正など準備にかける手間が非常に大きくなることもありました。例えば、プライマリキーを持たないテーブルに新たにプライマリキーを作成した上でデータを移行し、移行元のDBの修正処理を含めると合計5日ほど時間がかかったこともありました。 まとめ 本記事では、データ移行の前にデータ移行元と移行先の両方のDBにアトミックに書き込む実装をリリースすることで、サービスを停止させずにデータを移行する戦略について説明しました。本記事で紹介した戦略は、単純なデータコピーやデータレプリケーションと比較すると大幅に手間がかかります。しかし、サービス無停止でのデータ移行の成功例ができたことにより、機会損失を出すことなく複数のチームでマイクロサービス化のプロジェクトを進めることができるようになりました。マイクロサービス化の過程でサービス停止を伴わないデータ移行を検討している方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター
こんにちは。検索基盤部の橘です。検索基盤部では、ZOZOTOWNのおすすめ順検索の品質向上を目指し、機械学習モデル等を活用しフィルタリングやリランキングによる検索結果の並び順の改善に取り組んでいます。 最近行った並び順の精度改善の取り組みについては以下の記事をご参照ください。 techblog.zozo.com また、検索基盤部では新しい改善や機能を導入する前に、A/Bテストを行い効果を評価しています。A/Bテストの内容や分析の自動化への取り組みについては以下の記事をご覧ください。 techblog.zozo.com 検索基盤部ではA/Bテストの事前評価として、オフラインの定量評価と定性評価を実施しています。特に定量評価は、並び順の精度改善の仮説検証を迅速に行う手段として有効です。 しかし、ZOZOTOWNのおすすめ順検索の商品ランキングロジックの1つであるフィルタリング処理についてはこれまで明確な定量評価の指標がなく、評価が難しく改善を進めることが困難な状況でした。本記事では、この課題に焦点を当て、特に定量評価の指標を決定するアプローチについて詳細に記載していきます。 目次 目次 ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 フィルタリング処理における定量評価 A/Bテストにおける定量評価の位置付けと目的 検索結果の並び順のロジックを定量評価するには 導入した評価指標 『精度の維持度』の評価指標について 『多様性度』の評価指標について 『類似性』の評価指標について ケンドールの順位相関係数の難点 RBO(rank biased overlap) まとめ おわりに ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 ZOZOTOWNのおすすめ順検索の商品のランキングロジックは2つのフェーズに分けられています。 フィルタリング処理 :再現率を高めることを目的にルールベースのロジックや軽量な機械学習モデルを用いて商品のフィルタリングを行います。 リランキング処理 :フィルタ時のスコア結果トップN件に絞って「ランキング学習」と呼ばれる手法の機械学習モデルを用いた並び替え処理を行なっています。 以上のリランキング処理された並び順を検索結果に使用しています。 フィルタリング処理によりどのような商品群がリランキングされるのかが決まるので、フィルタリングの精度は最終的な検索結果に大きく影響を及ぼすことがわかります。続いて、このフィルタリング処理の精度の定量評価について記載していきます。 フィルタリング処理における定量評価 A/Bテストにおける定量評価の位置付けと目的 以下にA/Bテスト実施までのフローを示します。 定量評価及び定性評価は、事前評価としてA/Bテストの前に実施します。 事前評価を実施する目的として、以下が挙げられます。 『不要なA/Bテスト施策の排除』 :A/Bテストは社内の人的リソースを多く必要とします。明確に改善効果がない、または悪化する可能性がある施策をA/Bテスト前に排除することで、リソースの無駄を防ぎます。 『リスクの低減』 :致命的なエラーをA/Bテスト前に回避します。 『施策の内容理解』 :関係者が施策の内容や意図をより深く理解できるようにします。 この記事で取り上げる定量評価は主に1の不要なA/Bテスト施策の排除を目的としています。以下では、その精度の改善効果を確認するための評価方法や指標について詳細に記載していきます。 検索結果の並び順のロジックを定量評価するには 検索エンジンは、入力された検索キーワードに対して組み込まれたロジックに基づき商品を並び替えて出力します。定量評価では、入力された検索キーワードに対する新旧ロジックの検索結果の並び順を比較することによって新旧ロジックの良し悪しを評価・判断します。 以下に「リバーシブルパーカー」で検索した場合の新旧ロジックの比較イメージを示します。 導入した評価指標 フィルタリング処理における『良い検索結果の並び順のロジック』は、具体的に何が期待されるのでしょうか? 一般的な検索結果の並び順のロジックの良し悪しを評価する手法として、過去のユーザー行動ログを基に「正解データ」を作成し、その正解データとロジックの検索結果を比較してnDCGなどの指標で評価します。 しかし、ランキングの指標をそのままフィルタリング処理の評価指標として用いるのは必ずしも適切とは言えません。フィルタリング処理に関しては以下2点を考慮する必要があります。 正解データは旧ロジックによる検索結果が基になっていることにより、旧ロジックの精度が過大評価される可能性(historical bias)がある。 フィルタリング処理の検索結果は後のリランキング処理により並び替えられるので、正解データでフィルタリング処理時点での並び順の良し悪しを評価する必要性は低い。 これらの点を踏まえ、フィルタリング処理における良い検索結果の並び順のロジックを 新ロジックは旧ロジックの精度を保ちつつ、旧ロジックとは明確な差異を持ち、異なる商品を数多く表示できるもの と定義しました。 以上の定義を基に、以下の3つが確認できるような定量評価の指標を導入しました。 『精度の維持度』 :新ロジックは旧ロジックと同等の(もしくはそれ以上の)精度を有しているか。 『多様性度』 :新ロジックが、旧ロジックとは異なる商品を多く表示できるか。 『類似度』 :新ロジックと旧ロジックの検索結果の並び順がどの程度類似しているか。 以下にそれぞれの評価指標について詳細に説明していきます。 『精度の維持度』の評価指標について 精度の維持度の指標として コンバージョンカバー率 を導入しました。具体的には、新旧ロジック検索結果の上位N件の商品の過去のコンバージョン数を比較し、新ロジックが旧ロジックの数値をどれだけカバーしているかを割合で表します。 ※検索結果の上位N件は後のリランキング処理で並び替えを行う件数に該当します。 コンバージョンカバー率をベン図の部分で表すと以下のようになります。 本指標を使ってどのように新ロジックの良し悪しを判断するかを説明します。 ケース1ではコンバージョンカバー率が20%となっています。これは新ロジックの検索結果の上位N件のコンバージョン数が旧ロジックのそれの20%しかカバーしていないことを意味します。 ケース2ではコンバージョンカバー率が95%となっています。これは新ロジックの検索結果の上位N件のコンバージョン数が旧ロジックのそれの95%をカバーしていることを意味します。 ケース2はケース1と比較しコンバージョンカバー率が大きいので、新ロジックの検索結果の上位に過去ユーザーにとってコンバージョンしやすい商品が多く含まれていると解釈できます。つまり、コンバージョンカバー率が大きいほど新ロジックは旧ロジックと同等の検索精度を持っていると期待できます。 『多様性度』の評価指標について もし新ロジックの精度が旧ロジックの精度を維持していたとしても、新旧ロジックの検索結果が殆ど同等だった場合はA/Bテストをする必要性は低くなってしまいます。このようなロジックを除外できるように、旧ロジックとは異なる新しい検索結果をどれだけ取り入れているのかの指標が必要です。 これを踏まえ、多様性度の指標として 新表示商品率 を導入しました。具体的には、新ロジックで検索結果の上位N件に表示された商品の中で、旧ロジックで検索結果の上位N件に表示されない商品の割合で表します。 該当部分をベン図の部分で表すと以下のようになります。 先ほどの指標を使ってどのように新ロジックの良し悪しを判断するかを説明します。 ケース3では新表示商品率が50%になっています。これは新ロジックが旧ロジックに比べて新しい検索結果を50%も増やせていることを意味します。 ケース4では新表示商品率が5%になっています。これは新ロジックが旧ロジックに比べて新しい検索結果を5%しか増やせていないことを意味します。 ケース3はケース4と比較し新表示商品率が大きいので、新ロジックの検索結果の上位には旧ロジックとは異なる新しい検索結果が多く含まれていると解釈できます。 『類似性』の評価指標について 『多様性度』と同様に、新旧ロジックで検索結果の並び順がほぼ類似している場合、A/Bテストをする必要性は低くなってしまいます。このようなロジックも除外できるようにする必要があります。 検索結果の並び順の類似性を評価する際の代表的な指標としてスピアマンの順位相関係数、ケンドールの順位相関係数、RBO(rank biased overlap)などがあります。 指標を選定する際、おすすめ順検索のフィルタリング処理における検索結果の並び順は「2つの検索結果の並び順は互いに含まれていない商品を含んでいる場合がある」点を考慮する必要がありました。 以上を考慮する場合、RBOは使うことができますが、スピアマンの順位相関係数とケンドールの順位相関係数は扱いが難しいです。 以下にコードを踏まえながら、上記の難点を説明します。 ケンドールの順位相関係数の難点 まず、ケンドールの順位相関係数を挙げて上記の難点について説明します。ケンドールの基本的な説明や算出方法は Wikipediaの内容 をご参照ください。 簡単なPythonコードでケンドールの順位相関係数の計算例を示します。ここではscipyの kendalltau ライブラリを使います。 以下は両リストが同じ商品を含む場合のコードです。 !pip install scipy import numpy as np from scipy.stats import kendalltau # 2つの検索結果の並び順 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品E' ] search_result_2 = [ '商品A' , '商品B' , '商品E' , '商品C' , '商品D' ] # 2つの検索結果に含まれる商品をリスト化 all_goods = sorted ( list ( set (search_result_1 + search_result_2))) # 商品リスト内の商品に対して並び順の順位をつける関数 def assign_ranks (results, all_contents): return [results.index(c) for c in all_contents] ranked_1 = assign_ranks(search_result_1,all_goods) ranked_2 = assign_ranks(search_result_2,all_goods) # ケンドールの順位相関係数を計算 tau, _ = kendalltau(ranked_1, ranked_2) # 結果を出力 print ( "Kendall's tau:" , tau) >> Kendall 's tau: 0.6 ケンドールの順位相関係数をこのような場合に適用するときは、検索結果の並び順を基に商品リスト内の商品に対して並び順の順位をつけるようにし、それらを入力とし値を計算します。ケンドールの順位相関係数の値は-1から1の値をとり、値が低いほど負の相関、値が高いほど正の相関が高いことを示します。 この例の場合ケンドールの順位相関係数の値は0.6となります。 次に、異なる商品を含む場合のコードです。 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品hoge' ] search_result_2 = [ '商品A' , '商品B' , '商品fuga' , '商品C' , '商品D' ] 商品hogeと商品fugaという両リストに異なる商品が含まれています。この場合、これらの商品の順位をどのように扱えば良いのかという問題があります。 一般的には、含まれていない商品を無視する意図で順位をNaNに置き換える方法があります。 def assign_ranks (results, all_contents): return [results.index(c) if c in results else np.nan for c in all_contents] NaNを含む順位のデータを扱う場合、kendalltauの引数nan_policyを'omit'にすることで順位のデータ中のNaNを無視できます。 tau, _ = kendalltau(ranked_1, ranked_2, nan_policy = 'omit' ) print ( "Kendall's tau:" , tau) >> Kendall 's tau: 1.0 しかし、この場合のケンドールの順位相関係数の値は1.0と出力されてしまい、先ほどの互いに同じ商品を含んだ場合と比較しても違和感のある結果となってしまいます。 他にも様々な前処理方法が考えられますが、前処理方法により出力の値が変わってしまいます。よって、ケンドールの順位相関係数は、2つの検索結果の並び順において両リストに異なる商品を含んでいる場合に扱いが難しいと分かります。 RBO(rank biased overlap) 続いて、RBOを用いた場合について説明します。 RBOの値は0から1の値をとり、値が高いほど互いの並び順が類似していることを示します。RBOの特徴として、2つの検索結果の並び順の上位の商品が異なっていた場合に値を大きく減衰させます。 一般的に検索結果の並び順は上位の商品がより重視されるので、検索結果の並び順における類似性の評価指標として採用しやすい指標といえます。 RBOについての詳細はこれに関する論文である A Similarity Measure for Indefinite Rankings をご参照ください。 簡単なPythonコードでRBOの計算例を示します。ここでは rbo ライブラリを使います。 以下は2つの検索結果の並び順が互いに同じ商品を含んだ場合です。 !pip install rbo import rbo # 2つの検索結果の並び順 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品E' ] search_result_2 = [ '商品A' , '商品B' , '商品E' , '商品C' , '商品D' ] # RBOを計算する value_rbo = rbo.RankingSimilarity(search_result_1, search_result_2).rbo() print ( "RBO:" , value_rbo) >> RBO: 0.88 この例の場合、RBOの値は0.88となります。 次に、互いに含まれていない商品が存在する場合です。 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品hoge' ] search_result_2 = [ '商品A' , '商品B' , '商品fuga' , '商品C' , '商品D' ] ... print ( "RBO:" , value_rbo) >> RBO: 0.41 この例の場合、RBOの値は0.41となります。互いに同じ商品を含んだ場合と比較し値が低くなっているので違和感のない値になっています。 RBOは互いの並び順にあるリスト内の共通商品数をベースに算出する手法のため、互いに含まれていない商品が存在する場合でも違和感のない結果を出すことができます。 以上を踏まえて『類似性』の評価の指標としてはRBOを導入しました。 まとめ 本記事ではZOZOTOWNのおすすめ順検索の精度改善における定量評価及びその指標決定のアプローチを紹介しました。定量評価の指標が整ったことで、A/Bテスト前に精度改善の検証が迅速にできるようになり、よりA/Bテストの実施頻度の向上が期待されます。 今後は施策を重ねていく中で、これまで紹介した指標のブラッシュアップや新規指標を追加していく予定です。また、A/Bテストの負担軽減のため、これらの指標値をより迅速に算出できるような仕組みの構築も進めていく必要があります。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター
こんにちは、ZOZOTOWN開発本部ZOZOTOWNアプリ部の らぷらぷ です。先日9/1から9/3までの3日間、iOSDC Japan 2023が開催されました。弊社からは3名が登壇し、プラチナスポンサーとして協賛してブースを構え、10名以上がスタッフメンバーとして参加しました。 technote.zozo.com この記事では今年のiOSDCで登壇した3名の発表と弊社のスポンサーブースについてお伝えします。 登壇内容の紹介 マウスポインターを掴む?! 〜標準フレームワークで作る非接触でMacを操作する技術〜 WWDC Labsは怖くない。 Labsの準備とコツ、完全公開します 続・全力疾走中でも使えるストップウォッチアプリを作る 〜LiDARを使った精度への挑戦〜 スポンサーブース アンケート結果 Vision Pro買う? 今からアプリ作るなら何を選ぶ? これ勉強しています! チームは何人体制? これがないと始まらない!手放せない開発ツールは? 実はあれのコミッター、コントリビューターなんです 聞いてくれ!嬉しかった開発エピソード 掲示板 ~みんなと話したいこと~ 最後に 登壇内容の紹介 今年のiOSDCではDay1で1人、Day2で2人登壇しました。早速紹介していきましょう。 マウスポインターを掴む?! 〜標準フレームワークで作る非接触でMacを操作する技術〜 中岡黎 @rei_nakaoka / 計測プラットフォーム開発本部 計測アプリ部 iOSブロック speakerdeck.com 中岡からはVision Framework、Speech Framework、Core Motion Frameworkを駆使してMacの画面上の操作を実現する手法をデモを交えて発表しました。手や顔の動きでマウスカーソルを操作し、セリフからショートカットキーを入力する様子に、会場からは拍手や「おぉ〜」という声も上がりました。 WWDC Labsは怖くない。 Labsの準備とコツ、完全公開します 小松悟 @tosh_3 / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 iOS1ブロック speakerdeck.com 小松からは2020年から毎年参加し続けているWWDC Labsに臨む上での壁とその乗り越え方について発表しました。時差の壁に英語の壁、そしてもうひとつの壁のお話。この発表を聞いて来年はWWDC Labsに参加してみようかなと思っていただける方が増えると嬉しいです。 続・全力疾走中でも使えるストップウォッチアプリを作る 〜LiDARを使った精度への挑戦〜 荻野隼 @juginon / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 iOS2ブロック speakerdeck.com 荻野からは去年のiOSDC Japan 2022の続編として高精度なストップウォッチアプリの制作と実験結果について発表しました。ARKitとLiDARを活用したストップウォッチアプリは陸上競技歴16年の彼の全力疾走を精度高く計測できるのか。会場からの笑いもあり、テンポの良さもあって全力疾走感のある発表でした。 スポンサーブース ここからは弊社のスポンサーブースについてご紹介します。5月下旬から準備を進めて、各プロダクトのiOSエンジニアやDevRelブロックの方を始めとする多くの方にご協力頂いてブースのコンテンツやノベルティを用意できました。 ブース全体 ディスプレイにはアンケートや会社紹介をまとめたMiroを表示していました。アンケートは3日間で200人以上の方に答えていただきました。みなさまご回答頂きありがとうございました! 詳しいアンケート結果はこの後紹介します。 Miro全体図 今年のノベルティはステッカーやボールペンなど多種用意しました。 かわいいステッカーたち こちらはZOZOGLASSと手鏡になります。手鏡はZOZOTOWNの ARメイク を体験していただいた方に特別にお渡ししていました。 手鏡はスタッフの案から採用されました ARメイクデモ中 手書き広告で宣伝中 そして例年存在感のあるZOZOSUITを着たマネキンも健在です。 ZOZOGLASSもバッチリ アンケート結果 みなさんにお答え頂いたアンケート内容を見てみましょう。 Vision Pro買う? 今年のWWDCでお披露目されたVision Proですが、自腹でも買いたいという方が結構いらっしゃいました。会社や研究室で買っていただけると嬉しいと望まれてる方も多いですね。国内でVision Proがどのように流行っていくのか楽しみです。 今からアプリ作るなら何を選ぶ? iOSDCに来られるみなさまはSwiftで書く方が多いのか、はたまたReact NativeやFlutterに代表されるクロスプラットフォームやその他の方法を取られる方が多いのか聞いてみました。やはりSwiftを選ばれる方が多いですね。Flutterは個人開発やハッカソンなら使ってみたいという方もいらっしゃいました。 これ勉強しています! SwiftやSwiftUIにSwift Concurrency、Core MLやCore BluetoothなどのFrameworkといったiOSエンジニアなら気になるトピックが上がっていますね。他にもAndroidやFlutterにも興味を持っている方、英語をしっかり勉強したい方、ソフトウェアテストに関心のある方、マネジメントや会計にマーケティングと幅広くお答えいただきました。 チームは何人体制? 企業で開発されている方に個人開発の方、友達と開発している学生の方などにお答えいただきました。弊社のプロダクトチームも合わせて掲載しております。お答え頂いた中で1番大きいのは20人以上のチームという回答でした。 これがないと始まらない!手放せない開発ツールは? 普段の開発に欠かせないツールと聞かれて思い浮かべるものをお答えいただきました。無料ツールだとやはりXcodeが多いですね。Shiftltはウィンドウ位置やサイズをショートカットキーで調整できるツールです。こちらのツールとは別ですが私も似たようなツールを使っています。有料ツールだとChatGPTを挙げる方が多かったです。 お話いただいた方の中には、何か作りたいものがあるけど実現方法が分からないときはChatGPTに質問して回答を出してもらい、エラーなく動作するまで修正する方もいらっしゃいました。他の方は日々の開発にどう役立てているのか気になるところです。 実はあれのコミッター、コントリビューターなんです OSS活動をしている方はどれくらいいらっしゃるのか聞いてみました。弊社にもOSS活動をしている社員がおり、私もその一人です。ブースに訪れた方から「OSS活動って難しそう」というお話を頂き、そういったハードルの越え方をお話しさせていただきました。 聞いてくれ!嬉しかった開発エピソード こちらは質問ではなく、ブースに訪れた方から最近嬉しかった開発エピソードを披露していただきました。技術的な話だけではなくサービス改善やブログ発信など、聞いていて笑顔になるお話をたくさん頂けました。 掲示板 ~みんなと話したいこと~ こちらはブースのお客さんが会場の人に聞いてみたいことを挙げる場になっています。Vision Proで作ってみたいアプリを語って思いを馳せたり、SwiftUIとUIKitの併用の難しさを語って「そうだよね」と頷いたり、みなさん思い思いに話してくれました。 先の開発エピソードとこちらの掲示板は、「iOSDCに来ているお客さんと一緒に盛り上がりたいな」というスタッフの想いから生まれました。 最後に お客さんや他の企業ブースの方ともiOSの話で盛り上がり、オフラインの会場の体験を堪能できたiOSDC Japan 2023でした。このような素晴らしい場を提供してくれた運営さんに感謝しつつ、来年のiOSDCでも多くの方とワイワイ盛り上がれることを楽しみにしています。 ZOZOでは、来年のiOSDC Japanを一緒に盛り上げるエンジニアを募集しています。ご興味のある方はこちらからご応募ください。 hrmos.co
アバター
はじめに こんにちは、ZOZOMO部OMOバックエンドブロックの杉田です。普段は Fulfillment by ZOZO (以下、FBZ)が提供するAPIシステムを開発・運用しています。 FBZでは、昨年からビルドの高速化や自動デプロイをはじめとしたCI/CDパイプラインの最適化に取り組んできました。本記事では、それらの取り組みの詳細とその効果についてご紹介します。 目次 はじめに 目次 FBZにおけるCI/CDと構成管理の現状 リリースサイクルの見直し リリースまでの流れ 顕在化した課題 長時間のデプロイ ビルド環境のメンテナンス性の低さ 手動デプロイが抱える人為的なリスク CI/CDパイプラインの改善に向けて デプロイフローの見直し CodeBuildのバッチビルド タスク定義の実装例 個々のタスクを高速化 Direct deploymentsの有効化 devDependenciesの依存解決 デプロイフローを改善した効果 手動運用からの卒業 GitHub Actionsの活用 タグの生成 ビルド環境の自動アップデート 自動デプロイ 新生CI/CDパイプライン さいごに FBZにおけるCI/CDと構成管理の現状 FBZでは、 GitHub Actions (以下、GHA)と AWS CodeBuild (以下、CodeBuild)を用いてCI/CDパイプラインを構築しています。 利用用途などは、それぞれ以下の表の通りです。 GHA CodeBuild 主な利用用途 ・ユニットテスト ・静的コード解析 ・カバレッジ計測 ・アプリケーションのビルド ・AWSへのデプロイ ・E2Eテスト トリガー ・プルリクエストへのPush ・手動実行 設定管理 ・コードベース ・手動管理 また、FBZはサーバーレスアーキテクチャを採用しており、 AWS Lambda (以下、Lambda)を中心としたAWSが提供しているフルマネージドサービスを中心に構築されています。サービス構成やアーキテクチャ戦略の詳細については、以下の記事をご参照ください。 techblog.zozo.com 構成管理ツールとしては、サーバーレスアーキテクチャと相性の良い Serverless Framework を採用しています。 FBZでは、管理対象のリソースが多いことから関心事毎に定義ファイルを分割しています。そして、分割された定義ファイルはCodeBuildから直列実行されることで、デプロイ対象となるスタックの状態を最新化していきます。これらの構成はFBZの開発当初からあまり変わっておらず、大きな課題に直面することもなく、最近まで開発してきました。 一方で、リリースサイクルに関してはここ数年で大幅な見直しを行いました。 リリースサイクルの見直し 以前までは、隔週水曜日をリリース日としており、ある程度まとまった量の修正内容を一度にリリースするリリースサイクルを採用していました。 しかし、昨年からユーザーへの価値提供の速度を向上させることを目的として、リリースサイクルの見直しが行われました。その結果、リリース可能な状態になった修正は、可能な限り早いタイミングでリリースするという、リリース日を固定しないリリースサイクルへと変わりました。 この変化により開発者にも以下の利点がありました。 ビックバンリリースがほとんど行われなくなり、精神的な安定感が得られた 万が一リリースに伴う障害が発生しても、原因の調査や特定が容易になった リリースまでの流れ リリースサイクルを早く回していくという体制になりましたが、開発着手からリリースまでの流れは以前とあまり変わっていません。 以下の図は、FBZ開発におけるリリースまでの一連の流れになります。 この図で注目していただきたいのは、AWS上で実施される「検証・リリース」の2つです。 「設計・実装/テスト・レビュー」については、通常、開発者やレビュアのローカル環境で完結します。これらの3つのタスクにおいて待ち時間は、ユニットテストの実行中に数分程度という限定的なものです。 しかし、「検証・リリース」に関しては、AWS上へのデプロイを伴うため、デプロイが完了するまでに1時間以上もの待ち時間が発生します。検証にデプロイを伴う理由は、Lambdaベースのサーバーレスアーキテクチャを使用しており 1 、AWS上のLambdaに修正したコードを反映させないと動作確認できないことが挙げられます。 顕在化した課題 リリースサイクルを早くしたことでリリースする機会が増え、必然的にデプロイ回数も増えました。 その結果、次のような課題が見えてきました。 長時間のデプロイ ビルド環境のメンテナンス性の低さ 手動デプロイが抱える人為的なリスク 長時間のデプロイ 前述した通り、FBZでは開発当初に構築したCI/CDパイプラインを使ってきましたが、FBZは開発開始から6年以上が経過しています。日々、開発・保守を続けてきたことでアプリケーションコードをはじめ、サービス構成も複雑かつ肥大化してきました。その結果、開発時間のうち、リリースや検証作業といったデプロイを伴う作業にかかる時間の割合が増えてしまいました。 FBZではリリースで問題が発生した際に、再デプロイによって切り戻しを行うことがあります。そのため、デプロイに時間がかかってしまうと、それだけサービスの信頼性にも影響が出てしまいます。 ビルド環境のメンテナンス性の低さ CodeBuild上に作成するビルドプロジェクトと呼ばれる環境の中では、以下の項目をはじめとする様々な設定ができます。 メモリやCPUのスペック ビルド対象のソースコード 環境変数 しかし、これらの項目をメンテナンスする上で、次のような課題がありました。 設定が手動で追加・更新されていた 変更内容のレビューが困難であった 変更履歴が追跡できなかった これらの課題もあり、従来のビルド環境はメンテナンスし易いとは言いづらい状態でした。 手動デプロイが抱える人為的なリスク 手動によるリリースは手間がかかるだけでなく、人為的な問題を起こしてしまうリスクがあります。実際に、FBZではCodeBuild上でデプロイ対象のブランチ名の入力や環境変数を更新する場合など、リリース作業中は常にWチェックしながら操作に誤りが無いかを目視で確認していました。 CI/CDパイプラインの改善に向けて 先程までの課題を整理すると、いずれもCI/CDパイプラインを改善することで解決できることが分かりました。 以降は、それぞれの課題に対して取り組んだ内容を紹介していきます。 デプロイフローの見直し CodeBuildのバッチビルド ビルド定義の実装例 デプロイフローの見直し デプロイに時間がかかっていた要因を分析したところ、複数のServerless Framework定義ファイルを、単一のビルドプロジェクト内部で直列実行していたことが原因と分かりました。そこで、直列実行していたデプロイ処理を並列実行させる方法として、CodeBuildのバッチビルドという機能に注目しました。 CodeBuildのバッチビルド CodeBuildは、いくつかの種類のバッチビルドをサポートしています。 バッチビルドに関する詳細は、以下のドキュメントを御覧ください。 docs.aws.amazon.com FBZではビルドグラフという機能を利用しました。 ビルドグラフでは、タスク同士の依存関係を定義し、その定義された依存関係に基づいてビルドが実行できます。また、定義の仕方によって複数のタスクを並列実行させることもできます。 タスク定義の実装例 FBZのビルド構成を参考として、タスク定義の方法を紹介していきます。 今回は、以下の依存関係を持つタスク定義を実装していきます。 定義ファイル(buildspec)は以下のようになります。 # buildspec.yml version : 0.2 batch : build-graph : # PRE BUILD - identifier : PRE_BUILD_1 buildspec : buildspec_pre_build_1.yml # BUILD # 共通のbuildspecを使い、タスク毎に環境変数でデプロイ対象を制御 - identifier : API_BUILD_1 buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : api_a depend-on : - PRE_BUILD_1 - identifier : API_BUILD_2 buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : api_b depend-on : - PRE_BUILD_1 - identifier : BATCH_BUILD_1 buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : batch_a depend-on : - PRE_BUILD_1 # POST BUILD - identifier : POST_BUILD_1 buildspec : buildspec_post_build_1.yml depend-on : - API_BUILD_1 - API_BUILD_2 - identifier : POST_BUILD_2 buildspec : buildspec_post_build_2.yml depend-on : - API_BUILD_1 - API_BUILD_2 - BATCH_BUILD_1 特徴として、メインとなるタスク( API_BUILD_1 ・ API_BUILD_2 ・ BATCH_BUILD_1 )では buildspec を共通化していることが挙げられます。 タスクごとに環境変数を注入することで、ビルド処理を制御できるようにしています。 - identifier : BUILD_n buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : xxxxxxxx # 任意の値 こうすることで、今後さらにビルド対象が増えたとしても、環境変数を変えるだけでタスクの追加が行えるので、複雑になりがちなタスク定義が冗長にならず保守しやすくなります。 定義ファイルの詳細は以下のドキュメントを御覧ください。 docs.aws.amazon.com 個々のタスクを高速化 Serverless Frameworkのオプションを見直すことで、各タスクのデプロイ時間短縮を図りました。 Direct deploymentsの有効化 devDependenciesの依存解決 Direct deploymentsの有効化 CloudFormationのスタック作成時に変更セットを作成しないことで、デプロイ時間の高速化を実現する設定があります。以下のように定義します。 # serverless.yml provider : deploymentMethod : direct なお、以下のドキュメントにある通り、次期バージョンであるServerless Framework v4からは上記の設定がデフォルトとなるようです。 www.serverless.com 今後も変更セットを利用したい場合は、以下の定義を明示的に記述することで、Serverless Framework v3までと同じ設定でデプロイが可能です。 # serverless.yml provider : deploymentMethod : changesets deploymentMethod: direct とした場合のCloudFormationの挙動については、以下のドキュメントを御覧ください。 docs.aws.amazon.com devDependenciesの依存解決 デプロイパッケージ作成時に、devDependenciesの依存解決の除外処理に時間がかかっていたので、それらの処理を実行させないことで時間短縮を図りました。定義は以下のとおりです。 # serverless.yml package : excludeDevDependencies : false 詳細はドキュメントを御覧ください。 www.serverless.com デプロイフローを改善した効果 今まで紹介した改善策の実施前後で、ビルド時間にどれだけ変化があったかをキャプチャした結果が以下の図です。 改善前のビルド時間: 1時間14分 改善後のビルド時間: 24分 改善後はバッチビルドを利用しているため、改善前と比べるとフェーズの内訳が異なりますが、改善後の IN_PROGRESS で改善前の全フェーズを並列で実行しているイメージになります。 ビルドの並列化とServerless Frameworkのオプション見直しを行ったことで、およそ70%程度のデプロイ時間短縮を実現できました。 手動運用からの卒業 今まで手動で行ってきたCodeBuildのメンテナンスやデプロイですが、GitHubへの操作をトリガーに、各操作が自動で実行される仕組みを構築しました。 GitHub Actionsの活用 FBZではリリース毎にGitHub上でタグを付与しています。CodeBuildでのデプロイ時にはそのタグをデプロイ対象として使用しています。 タグの生成 タグの生成には、以下のアクションを利用しています。 github.com GHAのワークフローは以下の通りです。 name : Release Drafter on : push : branches : - main jobs : create-draft-release : name : Create Draft Release. runs-on : ubuntu-latest steps : - uses : release-drafter/release-drafter@v5 env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} 挙動を図化したものが以下になります。mainブランチへのpushをトリガーにアクションが起動してタグを生成します。 ビルド環境の自動アップデート CodeBuildで管理しているビルドプロジェクトに対して、変更履歴の追跡や事前のレビューを可能にするため、CodeBuildのリソース定義をコード化して管理することにしました。コード化するにあたり、CloudFormationのテンプレートを利用しました。 スタックの更新は、以下GHAのワークフローを用いて実現しています。 on : push : tags : - '*' env : STACK_NAME : your_stack_name AWS_REGION : your_aws_region jobs : update-build-project : name : Update CodeBuild Stack. runs-on : ubuntu-latest permissions : id-token : write contents : read issues : write pull-requests : write steps : - uses : actions/checkout@v3 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v2 with : role-to-assume : arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx aws-region : {{ env.AWS_REGION }} # テンプレートのデプロイと、削除保護の有効化 - name : Update Codebuild Build Project run : | aws cloudformation deploy \ --role-arn "arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx" \ --template "your_template_name.yml" \ --stack-name ${{ env.STACK_NAME }} \ --capabilities CAPABILITY_IAM aws cloudformation update-termination-protection \ --enable-termination-protection \ --stack-name ${{ env.STACK_NAME }} 上記ワークフローでは、CloudFormationのテンプレートに基づいて、対象のスタックに差分がある場合にのみ更新が行われます。 スタックに差分がない場合は、以下の画像のように更新は行われず処理が終わります。 これによって、CodeBuildの設定変更の履歴をGitHubで管理できるようになったほか、CI上で自動的にスタックの最新化が行われるようになりました。 自動デプロイ 先程までのワークフローに、デプロイを行うためのJobを追加します。 # (中略) env : STACK_NAME : your_stack_name AWS_REGION : your_aws_region # 追加 PROJECT_NAME : your_project_name jobs : # update-build-project: # (中略) deployment : name : Deploy. needs : update-build-project runs-on : ubuntu-latest permissions : id-token : write contents : read issues : write pull-requests : write steps : - uses : actions/checkout@v3 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v2 with : role-to-assume : arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx aws-region : {{ env.AWS_REGION }} - name : Start CodeBuild Batch Build run : | aws codebuild start-build-batch \ --project-name ${{ env.PROJECT_NAME }} \ --source-version ${{ github.sha }} ワークフロー内部でやっていることはシンプルで、AWS CLIを実行することでビルドプロジェクトのデプロイを開始させています。ここで追加したJobは、 needs: update-build-project を指定することで、ビルドプロジェクトの最新化が終わり次第起動します。 deployment : name : Deploy. needs : update-build-project ここまで、タグの生成をトリガーとした前提でワークフローの紹介をしてきましたが、タグの生成以外にも様々なイベントをトリガーにワークフローを実行させることができます。 詳細はドキュメントを御覧ください。 docs.github.com 新生CI/CDパイプライン ここまでの改善によって、今まで手動で行っていたデプロイに関係する操作が、タグの生成をトリガーとしてGHAのワークフローから自動実行されるようになりました。また、デプロイも直列実行から依存関係に従って並列実行されるようになりました。 改善前のデプロイフロー 改善後のデプロイフロー さいごに 本記事では、FBZにおけるCI/CDパイプラインの最適化させる取り組みと、それらの効果についてご紹介しました。 CI/CDパイプラインの最適化を実施した結果、以下の恩恵を得ることができました。 ビルドを並列化したことでデプロイ時間短縮 開発中の待ち時間が減って開発サイクルが高速化された 問題発生時の復旧にかかる時間が早くなった 手動で行っていた設定や操作の自動化 定義をコード化して管理できるようになったことで、変更追跡ができるようになった ビルド環境のメンテナンスが楽になった 変更内容がレビュー可能になった CodeBuildやServerless Frameworkを利用している方や、GHAを使ったCI環境の構築に興味がある方はぜひ参考にしてみてください。 ZOZOMO部では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co Serverless Frameworkには 任意のLambdaのみをデプロイする機能 がありますが、CloudFormation スタックの管理対象外となり、リソース管理が煩雑になるためFBZでは利用していません ↩
アバター
こんにちは、MA部MA開発1ブロックの齋藤( @kyoppii13 )です。 8/29-8/31に開催された Google Cloud Next '23 へ参加してきました。今年は4年ぶりとなるオフライン開催で、アメリカ・サンフランシスコで開催されました。弊社からはMA部の齋藤・松岡・中原の3名が参加しました。 今年は生成AIにフォーカスした内容がとても多く、それに関連する新サービスの発表も多くありました。本記事では、現地での様子と特に興味深かったセッションをピックアップして紹介します。 現地での様子 3日間に渡って開催されたGoogle Cloud Nextの会場はモスコーニ・センターという大きな展示施設で、メインルームではキーノート、他ルームでセッションが発表されるというものでした。発表以外にもワークショップやたくさんの企業ブースがあり大変賑わっていました。 Moscone Center Main Room Company Booth Google Cloud Nextは毎年開催されているイベントですが、今年は4年ぶりのオフライン開催ということもあり、非常に多くの参加者がいました。そのため、初日最初のキーノートは満席で入場制限が発生していました。セッションやワークショップは基本的には予約制で、予約なしで当日に並んで見ることも可能ですが、人気のものは長い待機列になっていました。 今年は生成AIに関連する発表がとても多かったです。GCPのマネージドなAIサービスであるVertex AIやGoogle Workspaceにおいて生成AIでユーザをサポートするDuet AIに関連する発表が多く見られました。 以降で現地参加したメンバーが気になったセッションについて紹介します。 セッション紹介 PostgreSQL the way only Google can deliver it: A deep dive into AlloyDB MA部MA開発1ブロックの齋藤です。 このセッションでは、昨年12月にGAとなったPostgreSQL互換のマネージドDBサービスであるAlloyDBの詳細と移行事例について紹介されました。 AlloyDBは標準のPostgreSQLと比較し、TPM(transaction per minute)が4倍以上、分析クエリにおいては最大100倍のパフォーマンス向上が期待できます。分析クエリのパフォーマンス向上により、従来のアプリケーションでは実行できなかったクエリも実行できるようになると紹介されていました。 パフォーマンス以外のメリットとして可用性やスケーラビリティ、オートパイロット機能・AI/ML機能についても述べられていました。また、OSSのPostgreSQLをベースとしているため、商用DBとは異なり、ベンダーロックインもなく、コストやライセンス管理も複雑になりません。 Linkより引用 移行事例としてCME Group社によるOracle Exadataからの移行について紹介されていました。移行に伴い従来のパフォーマンスを維持できるかのPoCを実施し、PoCの結果として、従来と同等またはそれ以上のパフォーマンスを実現できたと述べられていました。また、チューニングについても述べられていました。例えば、ORのUNIONへの置き換え、NOT EXISTSを使用しないなどのクエリ修正をすることでアプリケーションが要求するパフォーマンスを満たすことに繋がったと述べられていたのは興味深かったです。 ここからは高パフォーマンスがどのように実現されているかの紹介です。まずはレプリケーションとフェイルオーバーについてです。 アーキテクチャとして、コンピューティングとストレージでレイヤーが分かれていることで、それぞれでスケールでき、レプリケーションの遅延は通常のPostgreSQLと比較して25倍高速になります。フェイルオーバーにおいては、最近のリリースによってプライマリはダウンタイムが1秒未満、リードレプリカは0秒を実現しています。 Linkより引用 ダウンタイムが非常に短い点はとても気になりました。MA部では配信システムの開発・運用をしており、配信にはバッチによる配信とリアルタイムな配信があります。また、DBサービスとしてCloud SQLも一部システムで利用しています。Cloud SQLでは決められたメンテナンス時間帯に自動的にメンテナンスをしますが、数十秒のダウンタイムが発生します。特にリアルタイムな配信においてはダウンタイムが短いことは、機会損失を防げる点で非常に有益だと思いました。 参考:Cloud SQL インスタンスでのメンテナンスについて 次の高パフォーマンスの実現ではキャッシングと カラムナエンジン について述べられていました。AlloyDBでは複数のキャッシングを実装しています。RAMベースのキャッシングやUltra-fast Cacheと呼ばれるものです。RAMベースのキャッシュにおいては、通常のPostgreSQLでの行指向のキャッシングに加えて、列指向のキャッシングにも対応しています。このキャッシングに加えて、更にカラムナエンジンという列指向でのデータ処理を実施するエンジンも組み合わせることで、非常に高速な処理が可能となります。 それまでのRDBでは当たり前だった行指向でのデータの扱い方に加えて、BigQueryで培った列指向でのデータの扱い方を組み合わせるというのはGoogleだからこそできる技術だと思いました。 次にオートパイロット機能についてです。以下の機能が述べられていました。 Index Advisor(インデックスアドバイザー) Adaptive memory management(適応的メモリ管理) Adaptive vacuum management(適応的バキューム管理) Index Advisorはクエリパフォーマンス向上のために最適なインデックスを計算する機能です。Adaptive memory managementはOOMが発生しないようにバッファキャッシュを自動計算する機能です。Adaptive vacuum managementは現在のワークロードへの影響を最小限にしながらVACUUM処理を自動化するものです。 オートパイロット機能で特に気になったのは、Adaptive vacuum managementです。MA部では配信に利用するデータを生成するために、バッチ処理でクエリを実行しています。クエリパフォーマンスを保つためにAUTO VACUUMだけではなく、毎日深夜帯にVACUUM処理を実施しています。しかし、VACUUM処理が長引き、配信時間帯になっても終わらないことがあります。このような場合は配信影響を考慮し、VACUUMを停止するというオペレーションが発生します。このような問題を解決するために非常に有用な機能だと思いました。 最後に新機能について述べられました。新機能とは、この日最初のOpening Keynoteで新機能の発表であったAlloyDB AIについてです。 Linkより引用 SQL文の実行でエンベディング生成やVertex AIのモデルを呼び出せ、またそれらが高速に動作することで、DBのデータを利用した生成AIのアプリケーションを容易に作成できるようになります。現在はプレビュー版であり、本番利用はできません。詳しくは以下の公式ブログをご覧ください。 Google Cloud Blog - プレビュー版が提供開始された AlloyDB Omni で新たな生成 AI 機能を構築 MA部ではまだAlloyDBを利用していません。しかし今回の発表を聞いて非常に移行メリットは大きいと感じました。移行においてもオンプレやクラウドからの移行ツールである Database Migration Service が提供されています。 また、新機能であるAlloyDB AIもデータベースのデータを利用することで簡単にアプリケーションとして生成AIを組み込めるというのは大きなメリットだと思いました。私が今年もっとも衝撃を受けた技術である生成AI、今後の動向に注目したいです。 What's next for application developers MA部MA開発2ブロックの松岡( @pine0619 )です。 本セッションでは、アプリケーションエンジニアの開発者体験を変えるプロダクトとして以下2つの紹介がありました。 Jump Start Solutions Duet AI in Google Cloud Jump Start Solutions このプロダクトではユースケースに応じてアプリケーションとインフラのテンプレートが用意されており、それを使うことで簡単にアプリケーションをデプロイできます。新しくクラウドを使い始める際には様々なサービスを組み合わせ、ベストプラクティスを学習するなど障壁が高いという課題があります。このJump Start Solutionsを使うことで、最初のアプリケーション実行までを、より簡単かつセキュリティを担保した上で高速に実現できます。 現在は14種類の テンプレート が用意されています。 ユースケースとテンプレートが合致する場合という条件はあるものの、簡単にアプリケーション・インフラを構築し、初回デプロイまでの時間を短縮出来るのは便利だと感じました。今後テンプレートが増えていくとより使える場面が増えると思うので今後の動向に注目したいです。 Duet AI in Google Cloud 本イベントでは Google Cloud Duet AI in Google Cloudのプレビュー版 が発表されました。Duet AIとはGoogleが提供する生成AIを使った支援機能です。今回の発表でDuet AIによるアシスタント機能がコーディングやBigQuery、データベースなどGoogle Cloudの様々なプロダクト、サービスで利用できるようになりました。 本セッションは、そのDuet AIの機能の中でも開発者向けの機能であるコード補完、コード生成、チャットアシスタント、テスト生成などについての紹介です。 中でもチャットアシスタントはIDE上から呼び出すことができ、コーディングに関する質問やコード全体を要約するなど、効率的に開発を進める上で非常に便利な機能だと感じました。 セッションでは実際にデモも行われ、Duet AIを利用したコーディングの便利さを実感することが出来ました。 What's next for operations and platform builders 次も松岡から"What's next for operations and platform builders"のセッションについてご紹介します。 本セッションでは以下の機能についての紹介がありました。 Cloud TPU v4 on GKEのGAリリースおよびCloud TPU v5e on GKEのプレビュー提供 Cloud Run sidecars supportのGAリリース GKE Enterprise edition Interactive Troubleshooting PlaybooksのGAリリース 中でもCloud Run sidecars supportとInteractive Troubleshooting Playbooksが気になったので簡単に内容をまとめます。 Cloud Run sidecars support Cloud Run sidecars supportは今年の5月にプレビュー提供が開始され、今回GAリリースとなった機能で、Cloud Runでメインコンテナと並行して動作する独立したサイドカーコンテナが使えるようになります。使う場面としては、DatadogやOpenTelemetryといった監視ツールの動作や、nginxやEnvoyといったproxyとしての使用が挙げられます。サイドカーが使えるようになったことで今までに比べ選択肢が広がり、よりCloud Runの採用がしやすくなったのではないでしょうか? Cloud Run sidecarsについては以下のセッションで詳しく紹介されていましたので興味がある方はそちらもご覧ください。 Extend your Cloud Run containers’ capabilities using sidecars Interactive Troubleshooting Playbooks Interactive Troubleshooting PlaybooksはGKEのトラブルシューティングを行うための機能で、発生したトラブルに応じてログやメトリクスの出力、トラブルシューティングのための次のステップなどが表示され、解決への手引きをしてくれます。 現状は Pod unschedulable や CrashLoopBackOff などのいくつかのトラブルに関するplaybookが提供されていますが、今後他のトラブルに関するplaybookも提供される予定です。 詳しくは以下の公式ブログで紹介されていますので気になる方はご覧ください。 Simplify troubleshooting in Google Kubernetes Engine with new playbooks MA部ではメールやLINEなどへの配信のバッチ処理をGKE Autopilot上に構築したDigdagで行っています。そちらに関しては以下のテックブログで詳しく紹介しています。 techblog.zozo.com Interactive Troubleshooting Playbooksの機能を使うことでGKEの運用がより楽になることを期待したいです。また、本機能はGKEに限定されていますが、今後他のGCPサービスでも同様な機能が出て運用負荷の軽減が出来ることを期待しています。 What's new with BigQuery MA部MA開発1ブロックの中原です。 "What's new with BigQuery"について紹介します。 このセッションでは、 data clean rooms のプレビュー提供やBigQuery Omniの新機能、生成AIを使った新機能など多くのBigQueryに関する新しい発表がありました。その中でも生成AIを使った新機能に絞って紹介します。大きく以下3つの紹介がありました。 データエンジニアリング、アナリティクス、予測分析を1つに統合したインタフェースであるBigQuery Studioを発表 Vertex AI基盤モデルとの統合により、BigQueryのデータにAIを導入 Looker、BigQuery、Dataplexなどのサービスでデータ作業を再構築するために、Google CloudのDuet AIをプレビュー提供 それぞれについて簡単に内容をまとめます。 BigQuery Studioについて BigQuery Studioは現在プレビュー提供されています。これまでデータ分析や機械学習を活用するチームが、データウェアハウスやデータレイク、機械学習モデルなどを管理するために異なるツールを使用しており、生産性が低下してしまうという課題がありました。BigQuery Studioを利用することで1つのインタフェースでツールを切り替えることなく上記の作業をできるようになりました。 また、新しく発表されたPython向けノートブックの新サービス Colab Enterprise と統合されたUIでPythonによりBigQueryのデータを操作できるようになります。 Vertex AI基盤モデルとの統合について Googleが開発した言語モデルであるPaLM 2を含むVertex AI基盤モデルへのBigQueryからの直接アクセスできるようになり、以下の内容が発表されました。 BigQueryのコンソールでCREATE MODEL構文を使って、BigQuery MLモデルをVertex AI Model Registryに登録できる機能をGA提供 SQLのみで大規模言語モデル(LLM)のテキスト生成ができる機能をプレビュー提供 1つ目のVertex AI Model Registryへの登録については、機能の概要と詳細が 公式ドキュメント に記載されています。BigQueryのコンソール画面のみでモデルのバージョン管理、評価、デプロイを行い、オンライン予測ができるようになりました。 2つ目のLLMのテキスト生成については、使用例が 公式ブログ に記載されています。SQLのみで簡単な生成AIを実行でき、Cloud SQLやSpannerなどのデータベースに格納されているデータやCSVファイル、他の外部データソースのデータに対してもLLM分析が可能になりました。 Vertex AI基盤モデルとの統合と機能追加によって効率的にデータ分析ができるようになりました。 Duet AIについて 生成AIによる作業支援ツール「Duet AI」がLooker、BigQuery、Dataplexなど、Googleの様々なサービスでプレビュー提供すると発表されました。 BigQueryにおいては、自然言語での指示によるSQLの自動生成や、表やグラフを含むLookerStudioダッシュボードの自動生成などが可能になります。 詳しい内容は以下の公式ブログをご覧ください。 Reimagine data analytics for the era of AI MA部ではBigQueryのコンソールはよく使うため、Duet AIにより生産性が上がることを期待したいです。将来的にはAIがテーブル名やカラム、データの中身からどのようなテーブルなのかを理解し、どのようなデータを取りたいか指示すると自動でSQLの生成からデータの取得まで行えるようになるのでしょうか。今後の進化が楽しみです。 Go from idea to app with no coding using AppSheet 次も中原から"Go from idea to app with no coding using AppSheet"について紹介します。 AppSheetはノーコードで簡単にアプリケーションを作成できるサービスです。 このセッションでは、AppSheetに追加された新機能と機能強化によって、あらゆるスキルレベルのユーザにとってアプリ開発がより身近なものになったとデモも混じえながら紹介がありました。 発表された内容は、Duet AIがAppSheetで動作するようになり、自然言語で指示するとアプリを作成してくれるようになったということです。GoogleChatで「Duet AI in AppSheet」というアプリをスペースに追加すると、チャットの対話に基づいてアプリケーションを作成してくれます。また、アプリケーションの画面で変更したい箇所にマウスのカーソルを持ってきてクリックするだけで編集画面を開くことができ、直感的に画面をカスタマイズできるようになりました。 デモでは、オフィスのビルで働く人が管理者に対してメンテナンスが必要な箇所をモバイルで報告し、管理者にメールで通知が届きそれらを管理できるアプリを作成していました。データの登録や更新をするだけではなく、画像の添付やメール送信もあり少し複雑なものでしたが、チャットでAIと対話しながらテーブルの作成からアプリケーションの作成までできていました。 AppSheetはGmailやMeetなどのGoogle Workspaceと連携できるため、アイデアがあれば業務プロセスを効率化するようなアプリケーションを簡単に作れるようになりそうです。例えばMA部では、メルマガやLINE、アプリプッシュの配信システムを運用しているため、配信に関する問い合わせが様々な部署から来ます。今はSlack上で問い合わせを受け付けていますが、問い合わせの受け付けと管理をAppSheetのアプリケーションで行えば、問い合わせの管理がより効率的になるのではというアイデアが浮かびます。Duet AI機能を使えば、このアイデアも簡単にアプリケーションにできそうです。 AI-assisted collaborator and the changing workforce つづいても中原から"AI-assisted collaborator and the changing workforce"について紹介します。 このセッションは、AIと変化する職場についてのパネルディスカッションでした。 AIが職場と仕事に与える影響について以下のようなことを話されていました。 利点 AIは業務効率を向上させ、重要な業務プロセスを自動化する データの活用をサポートし、データから有用な情報を抽出できる メールの自動整理や適切なコミュニケーションのタイミングを提案することで、従業員間のコミュニケーションを助け、チームの連携が向上する トレーニングや教育プログラムをカスタマイズし、個々の従業員の成長をサポートする 課題 プライバシーとセキュリティの懸念が存在し、データの適切な保護が必要 従業員がAIを活用できるようにトレーニングとスキル向上のサポートが必要 AIの導入には文化の変化と変更管理が必要 AIからデータや情報を多く提供されると、有益な情報の抽出を難しくなる可能性があるため、AIが生成するデータのフィルタリングが必要 AI導入のベストプラクティス 明確なビジョンと戦略を持ち、段階的な導入を検討する 従業員の教育とスキル向上に投資し、外部の専門家を活用する データの品質とセキュリティに注意を払い、透明性を重視する 組織全体でのコミュニケーションを促進し、文化の変化をサポートする AIの導入と活用についてアドバイス AIは組織にとって貴重なツールであり、戦略的なパートナーとして活用すべき AIを受け入れ、学び、適応することが重要で、トレーニングとサポートを提供することが求められる 様々な場面でAIの導入の加速が予想されますが、それと同時にAIを活用するスキルが今後必要になってくると考えられます。ChatGPTをはじめとしたチャットAIやGitHub Copilotをはじめとするコード補完ツールなど様々なAIツールがありますが、最大限に活用できている人は多くないと思います。AI導入のベストプラクティスの「従業員の教育とスキル向上に投資」という観点では、ZOZOでは GitHub Copilotを全社導入 しています。私も業務で使用していますが、GitHub Copilotが得意なことや不得意なこと、うまく活用するコツを理解して活用するには至っていません。AIツールも進化していくので今のうちからAIツールを積極的に使い、AIツールをうまく最大限に活用する方法を模索していきたいです。 まとめ 参加メンバーがそれぞれ気になったセッションについて紹介いたしました。ここまで紹介したように今年はAI関連、特に生成AIに関する発表が多かったです。弊社でもすでにGitHub Copilotによる生成AIの開発支援は導入していますが、GCPのサービスでも生成AIを活用したものの導入が検討できそうだと思いました。 また、初めての海外カンファレンス参加でしたが、セッションでの学びはもちろんのこと、他社のエンジニアや企業ブースでの得られたコミュニケーションは、現地参加だからこその体験だと思いました。 紹介したセッション以外にもたくさんの興味深い発表がありました。全てのセッションは公式サイトの Session Library から見ることができます。ぜひご覧ください。 おまけ 会場やその周辺では様々なところにGoogle Cloud Nextの装飾やロゴがありました。 Entrance Next logo in San Francisco Certified Lounge ワークショップにも参加しました。参加したのはVertex AIでの生成AIとプロンプトデザインについてのワークショップです。 Vertex AIのGenerative AI Studio UIという画面上から生成AIモデルの作成や実行ができるツールとJupyter Notebookを操作しながら、生成AIについて学びました。課題をクリアしながらポイントをゲットしていくという形式で、ポイントランキング上位の人には景品があり、楽しく学ぶことが出来ました。 ランチは毎日メニューが異なり、複数のお弁当から選ぶことができました。これは1日目のチキンサンドイッチです。ビッグサイズでした。 Google Cloud Nextの開催前日には日本からの参加者向けに、ウェルカムパーティーを開催していただきました。Googleの方や他社のエンジニアの方と多くの方とお話して、とても有意義な時間を過ごすことができました。海近くのレストランのため、テラスからは心地よい風を感じられました。 Google Cloud Nextの終了翌日にはGoogleの新社屋であるベイ・ビューのオフィスツアーをしていただきました。とても大きく、オフィス間の移動には車や自転車を使うということでした。オフィスは独特な形をしています。これはソーラーパネルでの発電効率の向上や雨水を集めるための設計です。 社内には様々なGoogleロゴやオブジェがありました。 何箇所かマイクロキッチンと呼ばれる食べ物やドリンクが置かれているエリアがありました。 お昼は社内のカフェでランチをいただきました。いくつかの料理から選べるのですが、私はハンバーガーにしました。とても美味しかったです。 オフィスツアー後にはGoogle Merchandise StoreというGoogleグッズが売っているショップに連れて行っていただきました。アパレルグッズがとても多かったです。 ベイ・ビューのオフィス周辺はシリコンバレーエリアということで、GoogleplexやApple Parkも見てきました。スケールの大きさに驚くばかりでした。 最後に カンファレンス参加に伴う渡航費や宿泊費は 福利厚生 のひとつであるセミナー・カンファレンス参加支援制度によって全て会社負担です。 ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! corp.zozo.com
アバター
はじめに 検索基盤部の内田です。検索基盤部はZOZOTOWNの商品検索ロジックや検索動線上の各機能の改善に取り組んでいます。検索機能に関連したバックエンド実装にはJavaを使うことが多かったのですが、近年ではGo言語を採用することも増えてきました。 この記事は、Go言語で実装したWeb APIからElasticsearchへの検索処理を実装した際に調べたことをまとめたものです。Go言語でElasticsearchを取り扱うみなさまの助けとなれば幸いです。 2つのElasticsearchクライアント Go言語のElasticsearchクライアントについて調べると、主に以下の2つのライブラリが使われているのが見受けられます。 elastic/go-elasticsearchは、Elasticsearchを提供するElastic公式のクライアントです。公開されたのは2019年末と比較的最近で、サポートしているElasticsearchのバージョンも6から8系と新しめのものです。今後もElasticsearchのバージョンアップに合わせてアップデートされると思われるため、Go言語でElasticsearchを取り扱う際の有力な候補となるクライアントです。 一方のolivere/elasticはサードパーティ製クライアントであり、2012年に公開されて以来、Go言語でElasticsearchを扱う際の有力な選択肢として長い期間利用されてきました。サポートしているElasticsearchのバージョンは1から7系と幅広く、長い間コミュニティを支えてきたことが伺い知れます。しかし、後発の公式ライブラリの充実に伴い、olivere/elasticは2022年3月の更新を最後に開発が終了し、現在の利用は非推奨となっています。 現在、Go言語のElasticsearchクライアントを利用するならば、公式クライアントであるelastic/go-elasticsearchが最有力候補となります。しかし、公式クライアントの登場は比較的最近でolivere/elasticから主流が移ってまだ日が浅いため、参考となる資料があまり豊富ではありません。また、 Elasticが公開しているドキュメント も現時点ではあまり充実していません。 公式クライント elastic/go-elasticsearch についての知見 この節では、elastic/go-elasticsearchを利用して検索処理を実装した際に調べたことを紹介します。執筆時点でのelastic/go-elasticsearchの最新バージョンはv8.9.0です。 2種類のクライアント elastic/go-elasticsearchを用いて検索処理を実行するには、まずクライアントを生成する必要があります。クライアントを生成する関数には elasticsearch.Config 構造体を渡します。この構造体では、Elasticsearchへの接続や認証、通信に関する設定などを行うことができます。 認証については公式ドキュメントが詳しいのでご参照ください。通信に関する設定については本記事で後述します。 v8.9.0現在、クライアントには以下の2種類があります。基本的に提供されている機能は同じで通信処理なども共通ですが、機能の呼び出し方が異なります。 elasticsearch.Client elasticsearch.TypedClient elasticsearch.Client elasticsearch.Client は初期から存在するデフォルトのクライアントです。 elasticsearch.NewDefaultClient 関数はこちらのクライアントを生成します。提供されているAPIは esapi パッケージで確認できます。パッケージが巨大で、GoDocページが重めになっているのでご注意ください。 // type Client struct { // BaseClient // *esapi.API // } es, _ := elasticsearch.NewClient(elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く }) body := bytes.NewReader([] byte ( ` { "query": { "match_all":{} } } ` )) res, _ := es.Search( es.Search.WithContext(ctx), es.Search.WithIndex( "index-name" ), es.Search.WithBody(body), ) // ↓のように書いてもいい // req := esapi.SearchRequest{ // Index: []string{"index-name"}, // Body: body // } // res, _ := req.Do(ctx, es) defer res.Body.Close() body, _ := io.ReadAll(res.Body) // res.Bodyから読みだしたbyte列をjson.Unmarshalなどに渡す elasticsearch.Client には esapi.API 構造体へのポインタが埋め込まれているため、 esapi.API 構造体が持つフィールドや *esapi.API 型に紐づいたメソッドを呼び出すことができます。検索の実行に対応するメソッドは Search です。 Search メソッドを呼び出すと、内部では esapi.SearchRequest 構造体が生成され、その Do メソッドが呼び出されるようになっています。実装を確認したかぎり *esapi.API 型に紐づいたメソッドは基本的にすべて、対応するRequest構造体を生成してその Do メソッドを呼び出すようになっているようです。そのため、 *esapi.API 型に紐づいたメソッドの具体的な処理内容や設定可能な項目について知りたいときは、対応するRequest構造体のドキュメントや実装を調べるといいでしょう。埋め込まれた *esapi.API のメソッドを呼び出すのではなく、Request構造体を直接生成して Do メソッドを呼び出すように実装するのもシンプルでおすすめできます。 Elasticsearchの処理の実行結果は、呼び出したAPIの種類に関わらず *esapi.Response 型で返されます。 Body フィールドからバイト列を読み出し、呼び出したAPIに応じてJSONをパースし各要素にアクセスする必要があります。 elasticsearch.TypedClient elasticsearch.TypedClient はv8.4.0から追加された新しいクライアント実装です。提供されているAPIは typedapi パッケージで確認できます。こちらのクライアント実装は公式ドキュメントに 専用のページ が用意されていて、今後はこちらを推していきたいという雰囲気を感じます。 // type TypedClient struct { // BaseClient // *typedapi.API // } es, _ := elasticsearch.NewTypedClient(elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く }) res, _ := es.Search().Request(&search.Request{ Query: &types.Query{ MatchAll: &types.MatchAllQuery{}, }, }).Index( "index-name" ).Do(ctx) // resは*search.Response型 // 型付けされているため、res.Hits.Hits[0]のようにして各要素にアクセスできる elasticsearch.TypedClient 構造体には typedapi.API 構造体へのポインタが埋め込まれています。 elasticsearch.Client の場合と同様に埋め込まれた構造体のフィールドやメソッドにアクセスできます。こちらはメソッドチェーンの形でリクエスト内容の構築と実行ができるようになっています。 こちらのクライアントの特徴は、機能ごとにパッケージが切られて構造体や処理がまとめられていることです。例えば、検索の機能は searchパッケージ にまとめて定義されています。機能ごとに型付けされた構造体の操作でリクエスト内容の構築やレスポンス内容へのアクセスができるため、 elasticsearch.Client と比べるとJSON文字列の扱いを省略できる分シンプルかつ安全に取り扱うことができます。もちろん、 io.Reader 型を引数として受け取る Raw メソッドも用意されているので、従来どおりJSON文字列としてリクエストの内容を設定することもできます。 Raw メソッドを利用する場合は、 Request メソッドで渡した内容は無視されます( 該当箇所 )。 後発の実装なだけあり、 elasticsearch.Client と比べてよく整理されているので、 elasticsearch.TypedClient の利用から検討してみるといいと思います。ただし、歴史の浅いelastic/go-elasticsearchの中でも elasticsearch.TypedClient は新しいクライアントであるため、現時点では資料があまりありません。公式ドキュメントやGoDocを読んで自分のやりたい処理に対応するパッケージを調べましょう。 通信処理 通信に関する設定はelastic/go-elasticsearchの elasticsearch.Config で行うことができます。一方、通信処理の実装自体は別ライブラリ elastic/elastic-transport-go に切り出されています。 elasticsearch.Client もしくは elasticsearch.TypedClient を初期化すると、elastic/elastic-transport-goの elastictransport.New 関数が呼び出され、 elastictransport.Client が生成されます。生成された elastictransport.Client はクライアント構造体の中に格納され、すべての通信処理を担います。 elasticsearch.Config で設定したほとんど全ての項目はこの elastictransport.Client の生成時に利用されます。 elasticsearch.Config の GoDoc には、各フィールドがどのような設定項目なのかがまとめられており、何も設定しなかった場合のデフォルト値も記載されています。しかし、デフォルト値の記載がない一部のフィールドについては、elastic/elastic-transport-goを調べる必要があります。 例えば、コネクションや細かいタイムアウトの設定ができる Transport フィールドについてelastic/go-elasticsearchのドキュメントにはデフォルト値の記載がありません。しかし、elastic/elastic-transport-goを見ると、何も指定されなかった場合に elastictransport.New 関数内で http.DefaultTransport が使われるようになっていることが分かります( 該当箇所 )。そのため、デフォルトの挙動を踏襲しつつ一部の設定を変えたい場合は、以下のように http.DefaultTransport を元に生成したTransportの一部の設定を書き換えて利用するのがいいでしょう。 // http.DefaultTransportをコピーする tr, _ := http.DefaultTransport.(*http.Transport) t := tr.Clone() // DefaultTransportの値から変更したい項目を設定する t.MaxIdleConns = maxIdleConns t.MaxIdleConnsPerHost = maxIdleConnsPerHost t.MaxConnsPerHost = maxConnsPerHost t.IdleConnTimeout = idleConnTimeout cfg := elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く Transport: t, } Datadog APMとの連携 ZOZOTOWNではサービスの監視にDatadog APMを利用しています。Datadogからは公式のGoクライアントであるDataDog/dd-trace-goが公開されており、その中にelastic/go-elasticsearchと連携するための実装が含まれています。この実装を利用することで、クライアントの内部で行われているElasticsearchとの通信処理をトレースできます。ファイル名にv6と書かれていて不安になりますが、Elasticsearchの7系や8系でも利用可能です。 Elasticsearchとの通信処理のトレースは、 NewRoundTripper 関数で生成したオブジェクトを elasticsearch.Config の Transport フィールドに渡すことで実現できます。このRoundTripperもデフォルトでは http.DefaultTransport を元に生成されます( 該当箇所 )。先述したようなTransportのカスタマイズを行いたい場合は、下記のように WithTransport 関数を使って元となる http.RoundTripper インタフェースを満たす実装を渡す必要があります。 import ( elastictrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/elastic/go-elasticsearch.v6" ) t := MyRoundTripper() cfg := elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く Transport: elastictrace.NewRoundTripper( elastictrace.WithServiceName( "service-name" ), elastictrace.WithTransport(t), // http.DefaultTransportではなく、自分で用意したRoundTripperをベースにRoundTripperを生成させる ), } まとめ Go言語におけるElasticsearchクライアントについて紹介しました。改めて、本記事の概要を以下に列挙します。 メジャーなクライアントライブラリが2種類ありますが、elastic/go-elasticsearchの利用をおすすめします elastic/go-elasticsearchの中にさらに2種類のクライアント実装がありますが、 elasticsearch.TypedClient の利用をおすすめします 通信に関する処理は別ライブラリelastic/elastic-transport-goに切り出されているので、分からないことがあったらこちらの実装を調べると解決することがあります Datadog公式クライアントにはelastic/go-elasticsearchと連携するための実装が含まれているので、これを利用することでクライアント内部の通信処理をトレースできます おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、検索基盤部の伊澤です。検索基盤部では普段から、ZOZOTOWNの検索機能に関するデータ分析や、データ分析を踏まえた検索性能の改善に取り組んでいます。 検索に関するデータ分析では、検索クエリの傾向把握や課題のあるクエリの特定のために、検索クエリごとの検索結果のクリック率やコンバージョン率といったパフォーマンス指標を評価しています。 本記事では、検索クエリごとのデータ分析に関する情報共有を効率化するため、ウェブフレームワークの「 Dash 」で開発したダッシュボードを活用した事例を紹介します。 目次 はじめに 目次 検索クエリごとのデータ分析の重要性 分析結果のチーム内共有時の課題 Dashを用いたダッシュボードの開発 Dashとは Dashを選定した理由 検索クエリごとのパフォーマンス指標のダッシュボード 1. 検索クエリごとのパフォーマンス指標のテーブル 2. 検索クエリごとのパフォーマンス指標のグラフ 3. テーブルやグラフを操作するコントローラー ダッシュボードによる情報共有の効果 まとめ 検索クエリごとのデータ分析の重要性 ZOZOTOWNでは、ユーザーが欲しい商品を見つけやすくするために、以下のような検索機能を提供しています。 ユーザーが検索クエリを入力した際に、入力の続きを補完したキーワードを提示するサジェスト機能 検索結果の並び順を最適化するおすすめ順検索 検索の絞り込みクエリ提案機能 サジェスト機能の詳細は以下の記事をご参照ください。 techblog.zozo.com おすすめ順検索については以下の記事をご参照ください。 techblog.zozo.com ZOZOTOWNの検索の絞り込みクエリ提案機能は、以下の赤枠内に設置されています。ここで表示されているキーワードをクリックすることで、そのキーワードにより検索をさらに絞り込むことができます。 これら検索関連の機能の改善を進めるために、検索全体の商品クリック率やコンバージョン率だけではなく、検索クエリごとの指標を評価して効果的な検索クエリとそうでない検索クエリを把握することも必要です。これにより、適切な検索結果が表示されない検索クエリを特定し、改善の余地を見つけることができます。 例えば、おすすめ順検索においては、検索回数に対して商品クリック数やコンバージョン数が少ないような検索クエリを把握できれば、ランキングが効果的でない検索クエリを特定できます。 サジェスト機能や絞り込みクエリ提案機能では、検索クエリごとのパフォーマンス指標から、次に提案すべきクエリの示唆を得ることができます。 このように、「検索クエリごと」のパフォーマンス指標を把握することも重要となります。検索基盤部では、検索クエリごとのパフォーマンス指標を把握し、チーム内で共有して次の施策の改善案や優先順位を検討しています。 分析結果のチーム内共有時の課題 データ分析結果のチーム内共有時には、以下のような課題がありました。 膨大な検索クエリパターン :検索クエリごとの分析といってもそのパターンは膨大であり、それらを一覧で見て判断することは困難。そのため、検索回数の上位数十件に焦点を当てた限定的な範囲での結果の共有となってしまう。 ファッション業界の季節性 :ファッションの世界では、季節性によって検索されるクエリは日々更新されるため、検索クエリごとの分析結果も日々変化する。そのため分析も高頻度で行うことになるが、共有のためのグラフやテーブルをその都度作り直すことには非常に大きな手間がかかる。 チーム内での検索クエリへの関心の違い :チームメンバーの関心のある検索クエリが異なる場合、共有時のグラフに含まれない検索クエリはその場で確認できないことになる。 こうした課題を解決し、効率的な分析と共有を可能にするため、共有時にチームメンバーがインタラクティブに確認できるような状況を整える必要があります。 Dashを用いたダッシュボードの開発 上述の問題を解決するために、各検索クエリに関する商品クリック率やコンバージョン率をインタラクティブに可視化するダッシュボードを開発しました。 ダッシュボードの開発にあたっては、Pythonのウェブフレームワークである「Dash」を用いました。 Dashとは Dashは、Pythonでデータアプリケーションを開発できるウェブフレームワークです。少ないプログラムコード(Low-Code)で、様々なグラフや表を表示するダッシュボードを開発できます。 dash.plotly.com 以下にサンプルコードとサンプルアプリケーションのイメージを示します。サンプルコードは、公式の A Minimal Dash App を参考にしています。 from dash import Dash, dcc, html, callback, Output, Input import plotly.express as px import pandas as pd # サンプルデータを作成 data = { 'category' : [ 'tops' , 'tops' , 'tops' , 'bottoms' , 'bottoms' , 'bottoms' ], 'sub_category' : [ 't-shirt' , 'polo' , 'knit' , 'denim' , 'cargo' , 'chino' ], 'value' : [ 10 , 20 , 15 , 25 , 20 , 30 ] } df = pd.DataFrame(data) # Dashアプリを作成 app = Dash(__name__) # レイアウトを定義 app.layout = html.Div([ html.H1( 'Sample Dash App' ), dcc.Dropdown(df.category.unique(), 'tops' , id = 'dropdown' ), dcc.Graph( id = 'bar-chart' , figure=px.bar(df, x= 'sub_category' , y= 'value' , title= 'Sample Bar Chart' ) ) ]) # コールバックを定義 @ callback ( Output( 'bar-chart' , 'figure' ), Input( 'dropdown' , 'value' ) ) def update_bar_chart (value): dff = df[df.category == value] return px.bar(dff, x= 'sub_category' , y= 'value' , title= 'Sample Bar Chart' ) if __name__ == '__main__' : app.run_server(debug= True ) サンプルコードでは、レイアウトとコールバック関数を定義しています。レイアウト内のDropdownコンポーネントにidを付与し、コールバック関数内のInputにDropdownコンポーネントのidを指定しています。これにより、Dropdownコンポーネントの値が変化した時にコールバック関数が呼び出され、コールバック関数のOutputに指定したグラフの値を更新できます。 上記のコードだけで、以下のようなアプリケーションを立ち上げることができます。 その他、公式のサイトにサンプルアプリケーションが豊富にあります。Dashでどんなことができるか知りたい方は、以下を参照してください。 plotly.com Dashを選定した理由 今回、検索クエリごとのパフォーマンス指標のダッシュボードを作成するにあたっては、以下の理由からDashを選定しました。 Low-Codeでクイックにダッシュボードを開発できる 「 Plotly 」のグラフをダッシュボードに統合できる コンポーネントが豊富にありレイアウトのカスタマイズ性が高い Low-Codeでクイックに開発ができる点は、上記で見たように、レイアウトとコールバック関数を定義するだけでインタラクティブなダッシュボードを作成できることから明らかです。 Plotly は、様々なグラフを描画するPythonのライブラリです。Plotlyで作成したグラフに対しては、グラフ上でデータポイントをホバーして詳細を表示する、ズームする、グラフ内の範囲を選択するなどのインタラクティブな操作が可能です。Dashでは、 dcc.Graph モジュールを使って、Plotlyのグラフをアプリケーション内で表示できます。Plotlyのグラフに対するレイアウトやスタイルのカスタマイズも可能です。 さらに、Dashには、入力フォーム、ドロップダウン、ボタン、テーブルなど、オープンソースのコンポーネントが豊富にあります。これらのコンポーネントを組み合わせて、用途に応じたダッシュボードを作成できます。 Dashのほかにも、Low-Codeのウェブフレームワークとしては、 Streamlit があります。Streamlitを使っても、データ処理と可視化を一貫して行うことができます。しかし、Dashと比べてレイアウトのカスタマイズに制限があります。今回のダッシュボードについては、テーブルとグラフを水平方向に並べる際、レイアウトの微調整を必要としました。StreamlitではCSSを用いたスタイルの適用は困難ですが、Dashでは各コンポーネントモジュールにstyleを指定することでレイアウトの微調整が可能であるため、Dashを用いることとしました。 検索クエリごとのパフォーマンス指標のダッシュボード 今回作成した、検索クエリごとの商品クリック数、コンバージョン数(または率)を可視化したダッシュボードを紹介します。 ダッシュボードは、パラメータに分解した検索クエリを格納したテーブルと、各検索クエリに対応するパフォーマンス指標を表示するグラフで構成されています。 以下はダッシュボードのイメージです(グラフには適当な値を入れています)。 今回のダッシュボードには、以下の要素が含まれています。 検索クエリごとのパフォーマンス指標のテーブル(左下) 検索クエリごとのパフォーマンス指標のグラフ(右下) テーブルやグラフを操作するコントローラー(上部) これらの要素を順に説明します。 1. 検索クエリごとのパフォーマンス指標のテーブル ダッシュボードの左下にあるテーブル(下図)では、検索クエリを各パラメータ(ショップ、ブランド、カテゴリー等)をそれぞれのカラムに分割して表示しています。このテーブルは公式ドキュメントの「 DataTable Interactivity 」を参考に作成しました。 このテーブルには、ソート機能、フィルタ機能、ページング機能があります。ソート機能は、カラム名の横の三角形をクリックすることで使用でき、そのカラム内の値でテーブル全体をソートできます。また、フィルタ機能では、指定したフィルタの条件でデータを絞り込むことができます。紹介したイメージでは、platformが PC の検索クエリに絞って表示しています。さらに、ここでは1ページに20件の検索クエリを表示していますが、テーブル下部のページング機能から次の20件のデータを表示できます。 2. 検索クエリごとのパフォーマンス指標のグラフ 右下のグラフ(下図)は、テーブルの検索クエリごとの検索回数、商品クリック数、コンバージョン数を表示しています。テーブルの unique_id とグラフの unique_id によって、テーブル内の行とグラフ内のバーが対応しています。グラフの描画にはPlotlyを使っているため、ホバーやズーム等の操作ができます。 このグラフはテーブルの内容と連動しており、テーブルへの操作が行われるとコールバック関数が呼ばれてグラフも更新されるようになっています。 以下に、コールバック関数 update_graph の内容を説明します。 Input("table", "data") としており、これにより id が "table" であるコンポーネントの data が変わった時に update_graph 関数が呼ばれます。 update_graph 関数は、変更されたデータを受け取って新しいグラフを作成し、新しいグラフを格納したコンポーネントを返します。 update_graph 関数から返ってきた内容で、 Output に指定した id="graph-container" を持つ html.Div の中身が置き換えられ、表示されるグラフが更新されます。 # レイアウトを定義 app.layout = html.Div( [ ... html.Div( dash_table.DataTable( id = "table" , ... ) ), ... html.Div( id = "graph-container" ), ... ] ) @ app.callback ( Output( "graph-container" , "children" ), Input( "table" , "data" ), Input( "radio-button" , "value" ), ) def update_graph (queries: list [ dict ], radio-option: str ) -> html.Div: fig = go.Figure() # データの更新処理およびグラフの作成処理 ... # 更新したグラフを格納したコンポーネントを返す return dcc.Graph( id = "bar-chart" , figure=fig ) 3. テーブルやグラフを操作するコントローラー 上部のコントローラーには、データを検索クエリのパラメータでグルーピングするドロップダウン(下図左)と、グラフの表示データを切り替えるラジオボタン(下図右)を配置しています。 検索クエリのパラメータでのグルーピングには、先ほどのサンプルアプリケーションでも使用していたDropdownコンポーネントを使用しています。Dropdownコンポーネントで multi=True とすることで、複数のオプションから選択できるようになります。このドロップダウンからパラメータを選択すると、選択したパラメータでデータがグルーピングされ、その結果がテーブルに反映されます。 dcc.Dropdown( id = "multi-select-dropdown" , options=dropdown_options, value=dropdown_options, multi= True , ) ラジオボタンでは、グラフの数と率の表示を切り替えることができます。以下のラジオボタンのコードでは、 inputStyle でコンポーネントに適用するスタイルを指定しています。これはRadioButtonに限らず、そのほかのコンポーネントでも同様にスタイルを指定できます。 dcc.RadioItems( radio_options, "absolute" , id = "radio-button" , inline= True , inputStyle={ "margin-left" : "15px" , "margin-right" : "5px" , }, ) 今回作成したダッシュボードによって、表示するデータの範囲やパラメータの種類などをインタラクティブに操作しながら、検索クエリごとのパフォーマンス指標を可視化できるようになりました。 ダッシュボードによる情報共有の効果 今回のダッシュボードを用いるようになったことで、データ分析結果の情報共有時に以下の効果が得られました。 まず、効率的に共有が行えるようになりました。従来、データ分析のたびに、グラフやテーブルを作成してそれらをドキュメントにまとめる作業が必要でした。ファッション業界の季節性もありデータ分析結果の共有の頻度も高く、そのたびに新たなドキュメントを作成する手間が発生していました。今回のダッシュボードの導入により、共有のためのグラフやテーブルを手動で作り直す手間を軽減できました。これにより、定型作業から解放されたことで、分析や洞察に集中することが可能となりました。 さらに、インタラクティブな情報共有が可能になりました。これまでは、情報が一方的に共有されるだけであり、チームメンバーが共有された検索クエリ以外に関心がある場合はその場で確認することが困難でした。今回のダッシュボードにより、報告者以外のメンバーもデータを操作し、必要な情報を自由に探索できるようになりました。これにより、共有時のミーティングが対話的なものとなり、意見交換や意思決定が円滑に行えるようになりました。 まとめ 本記事では「Dash」を用いて検索クエリごとのパフォーマンス指標をインタラクティブに可視化した事例を紹介しました。 今後は、このダッシュボードを拡張させ、検索に関するそのほかのデータも可視化することで、さらに課題抽出や意思決定を加速化していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター