TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

974

はじめに こんにちは、MA部の中原です。 MA部ではZOZOTOWNにおけるメルマガやLINE通知、アプリプッシュ通知、Web広告を配信するためのマーケティングオートメーションシステムを開発・運用しています。本記事では、Web広告について外部SaaSで利用していた機能の内製化と移行についてご紹介します。 目次 はじめに 目次 背景・課題 拡張コンバージョンとは? 拡張コンバージョンの実装 設定方法 環境構築 1. アクセス認証情報の作成 2. API認証ユーザーに付与するGoogle広告のアクセス権限 3. APIリクエストには個人アカウントのリフレッシュトークンを使用する 全体の流れ 1. 運用担当者の広告設定・広告出稿 2. ユーザが広告をクリックし購入 3. ファーストパーティ(自社)の購入データをGoogleに送信 実装 送信データ APIを使用したアップロード 並列処理の方法 1. 対象データ取得 2. バッチグループの作成 3. 並列処理 外部SaaSからの移行 1. テスト用のコンバージョンアクションの作成 2. テスト用のコンバージョンアクションに対して拡張コンバージョンをアップロード 3. テスト用のコンバージョンアクションで拡張コンバージョンが記録されることを確認 4. 本番適用と外部SaaSとの並行稼働 5. 外部SaaSの利用停止 移行前と移行後 まとめ さいごに 背景・課題 ZOZOTOWNでは、GoogleやYahoo!、Metaなどの様々な媒体に広告を出稿しています。広告の運用担当者は、配信した広告のクリック率やコンバージョン率などをみて効果検証し、広告の改善や最適化を行っています。Google広告においてはコンバージョンの計測を補完するために、外部SaaSの機能を活用して拡張コンバージョン(詳細は後述)を導入していました。しかし、外部SaaSに依存しているためエラーが発生した際にリカバリーができず、コンバージョンデータの欠損が発生し効果検証や分析に支障をきたすという課題がありました。そこで、Google Ads APIを用いた拡張コンバージョンの実装を内製化し、課題の解決を図りました。 拡張コンバージョンとは? 従来の方法では、コンバージョンは以下の流れで計測されます。 広告主がコンバージョンページ(注文完了ページなど)にコンバージョンタグを設定 ユーザが広告をクリックするとGCLID(Google Click Identifier)という広告クリックを一意に識別するIDや広告のキャンペーン情報をCookieに保存 ユーザがコンバージョンページに訪問すると、コンバージョンタグが発火し、Cookieの情報を読み取りGoogleに送信 しかし、近年のCookie規制の強化により従来の方法ではコンバージョンの欠損が起きやすくなります。今回紹介する拡張コンバージョンは、ファーストパーティデータ(自社データ)とGoogleアカウントを照合することで、この欠損を補完する機能です。具体的な計測の流れを以下に示します。 自社データのユーザ情報(ハッシュ化した電話番号やメールアドレスなど)やコンバージョン情報(注文日時)をGoogleに送信 Google側で送信されたデータとGoogleアカウントのデータと照合し、広告クリックとコンバージョンの関連付けが行われ計測 拡張コンバージョンの実装 設定方法 拡張コンバージョンの設定方法は3つあります。 table { border-collapse: collapse; width: auto; } th { border: solid 1px #666666; color: #000000; background-color: #FFFFFF; } td { border: solid 1px #666666; color: #000000; background-color: #FFFFFF; } 設定方法 メリット デメリット Googleタグマネージャー ユーザデータを収集するためのタグ設定を行うだけで非エンジニアでも設定可能 コンバージョンページに電話番号やメールアドレスの情報を持たせる必要があり、セキュリティリスクがある Googleタグ Google Ads API 自社から直接ユーザデータを送信するためセキュリティが高い 技術的な知識が不可欠でエンジニアによる開発が必要 今回はセキュリティの観点からGoogle Ads APIを採用しました。詳細は Google広告ヘルプページの拡張コンバージョンに関する説明 をご覧ください。 環境構築 インストールや認証等の設定は以下の公式ドキュメントに従えばスムーズに設定できました。この章では実装するにあたって特に重要なポイントについて説明します。 developers.google.com developers.google.com 1. アクセス認証情報の作成 OAuth 2.0クライアントにはウェブアプリケーションやデスクトップアプリケーションなど、さまざまな種類があります。今回の実装では、APIリクエストに必要なリフレッシュトークンを簡単に発行できるデスクトップアプリケーションとしてOAuth 2.0クライアントを作成しました。なお、サービスアカウントを使用する方法もありますが、これにはGoogle Workspaceドメイン全体の権限を委任する必要があります。この仕組みでは、Google広告の権限を持つユーザーになりすまして認可を受ける形となります。しかし、サービスアカウントに非常に強い権限を付与することになるためセキュリティリスクが高く、この方法は推奨されていません。アクセス認証情報の詳細については以下をご覧ください。 developers.google.com 2. API認証ユーザーに付与するGoogle広告のアクセス権限 APIリクエスト時の認証で使用するユーザは、Google広告にアカウントを追加およびアクセス権限の付与が必要です。主な権限とできることを以下に示します。 権限 できること 読み取り専用 閲覧のみ(広告、キャンペーン、レポートの確認など) 標準 広告運用の全般(キャンペーンや広告の作成、編集、コンバージョンデータのアップロードなど) 管理者 全ての操作 読み取り専用では拡張コンバージョンのアップロードで権限エラーが発生するため、標準以上の権限が必要です。上記以外にもアクセス権限の種類はありますが、詳細は以下をご覧下さい。 support.google.com 3. APIリクエストには個人アカウントのリフレッシュトークンを使用する Google Ads APIではサービスアカウント単体でのリクエストがサポートされていません。そのため個人アカウントの認証情報を使うしかありません。理由は以下の2点です。 Google広告には個人のアカウントしか登録できない Google Ads APIの認証時にAPI発行主がGoogle広告にアカウントと権限があるかが確認される APIのクライアントライブラリではリフレッシュトークンを認証情報として渡します。このリフレッシュトークンは、Google広告に権限があるアカウントで発行したものを使用します。APIリクエスト時にはリフレッシュトークンでアクセストークンを取得し、そのアクセストークンで認証・認可が行われます。リフレッシュトークンは個人アカウントに依存するため、システムをチーム全体で管理する際に不便な場合があります。たとえば、APIリクエストに使用していたメンバーのアカウントが退職などで削除されると、そのアカウントに紐づいたリフレッシュトークンを使用してAPIリクエストができなくなります。一方で、Google広告のアカウントを持つユーザーのリフレッシュトークンを使用することで、Google広告のアカウントを持たない他のメンバーやシステムからもAPIリクエストは可能です。そのためチーム全体で運用する際は、認証に使うアカウントの管理や引き継ぎを考慮する必要があります。個人に紐づくトークンを利用する設計は望ましくないため、将来的にはサービスアカウントをセキュアに利用できる仕組みが導入されることを期待しています。 全体の流れ 実装の説明の前に簡単な全体像を以下に示します。 大まかな流れは次の通りです。 1. 運用担当者の広告設定・広告出稿 運用担当者は広告を作成した後、コンバージョンを計測するためにコンバージョンアクションというものを作成し設定します。コンバージョンアクションとは、サイトでの商品購入やアプリのダウンロードなどユーザの特定の行動です。例えば、ウェブサイトの注文完了ページへのクリックで購入を測定する「購入コンバージョン」などです。サイト内にタグを設置することでクリック時に発火し測定されます。今回は購入コンバージョンの計測を補完しました。 2. ユーザが広告をクリックし購入 ユーザが広告をクリックし注文完了ページに到達すると、コンバージョンアクションのタグが発火し、Googleに以下の情報を送信します。 クリックID(GCLID):広告クリックを一意に識別するID 広告ID:クリックされた広告を特定するための情報 クリック時間データ:広告がユーザーに配信されてからクリックされた時間に関する情報 3. ファーストパーティ(自社)の購入データをGoogleに送信 自社のDBに蓄積される注文データ(ハッシュ化したメールアドレスなどのユーザデータや注文日時など)をGoogleに送信します。Google側でその情報を使用して広告のクリックとコンバージョンを関連付けます。ハッシュ化しているとはいえ、ユーザの個人情報を外部へ送信するため事前に法務部への確認が必要です。 実装 上図の赤の部分を実装しました。定期実行するために、 Digdag を使用しています。Digdagとはオープンソースのワークフローエンジンで、複数のタスクをワークフローとして定義しバッチ処理を行えるものです。自社の注文データをGoogle Ads APIを使用してGoogleに送信するシンプルなアプリケーションを作成しました。送信データとAPIを使用したアップロードの実装について説明します。 送信データ 送信するデータは 公式ドキュメント に記載されています。すべての項目を送信する必要はないため必要最低限の以下の項目を送信しました。 送信項目 説明 order_id コンバージョンのタグで指定する注文ID phone_number ハッシュ化したユーザの電話番号 country_code ISO 3166 の2文字の国名コード email ハッシュ化したユーザのメールアドレス conversion_date_time 注文日時 ハッシュ化する項目は、ハッシュ化する前に以下のように変換する必要があります。 先頭と末尾の空白を取り除く テキストを小文字に変換する E164規格に従って電話番号をフォーマットする メールアドレスのドメイン名の前にあるピリオド(.)をすべて削除する ハッシュ化はBigQueryのクエリで行ってテーブルに書き出し、アップロード時はテーブルのデータを参照するだけの状態にしました。以下にBigQueryでハッシュ化するクエリ例を示します。 SELECT MemberID, to_hex(sha256( lower ( replace ( replace (split(email, ' @ ' )[ 0 ], ' ' , '' ), ' . ' , '' )) || ' @ ' || lower ( replace (split(email, ' @ ' )[ 1 ], ' ' , '' )))) AS email_sha256, to_hex(sha256( ' +81 ' || ltrim ( replace (tel, ' - ' , '' ), ' 0 ' ))) AS tel_sha256 FROM `project.dataset.Member` -- 会員テーブル 送信するデータはファーストパーティのCookieの保管期限の関係で、 ドキュメント に記載の通り、コンバージョン発生から24時間以内のデータだけを送るように注意します。 APIを使用したアップロード Pythonのクライアントライブラリを使用しました。 サンプルコード が公式で用意されており、それを参考に実装しました。APIのリクエストは以下のドキュメントにある通り、1回につき2000件までという制約があります。直列で2000件ずつ処理すると時間がかかるため、今回はAPIリクエストを並列で処理するように工夫して実装しました。 developers.google.com 並列処理の方法 Digdagにはループ処理ができる for_each> オペレータと、並列処理できる _parallel: オプションがあります。 docs.digdag.io これらを使用し、対象データを固定の並列数で2000件ずつ処理するためのグループに分け、ループしながら効率的に処理することにしました。 1. 対象データ取得 DBから過去24時間以内の注文情報を取得して一時テーブルに書き出します。 2. バッチグループの作成 対象データからバッチ処理するグループのリストを作成します。以下はグループ作成のサンプルコードです。引数のcountには対象データの件数を渡します。 _BATCH_SIZE = 2000 # リクエスト1回あたりの最大件数 _PARALLEL_SIZE = 50 # 並列数 def generate_batch_group_list (count: int ) -> List: batch_size = int (count / _BATCH_SIZE) if int (count % _BATCH_SIZE) > 0 : batch_size += 1 batch_number_list = list ( range (batch_size)) return [batch_number_list[i:i + _PARALLEL_SIZE] for i in range ( 0 , len (batch_number_list), _PARALLEL_SIZE)] 例えば対象データ件数が100万件の場合、それぞれの変数とbatch_group_listの中身は以下の通りです。 count: 1000000 batch_size:(1000000 / 2000)= 500 batch_number_list: [0, 1, 2 ... 499] batch_group_list: [[0, 1, 2, ..., 49][50, 51, 52, ..., 99]...[450, 451, ... , 499]] batch_group_listは並列数の50個ずつに区切られた2次元配列となり、中身の数字は、BigQueryに書き出したテーブルから2000件ずつ取得するために基準となる数字です。 3. 並列処理 上記2で作成した batch_group_list をDigdagの変数にセットし、以下のようなDigdagのタスクを定義します。 +upload : for_each> : group_list : ${batch_group_list} _do : for_each> : batch_number : ${group_list} _parallel : true _do : _export : python : /usr/bin/pipenv_run_python docker : image : ${docker_python.image} pull_always : ${docker_python.pull_always} !include : k8s.dig py> : models.google_ads.upload_enhanced_conversions timestamp : ${moment(session_time).local().format("YYYYMMDD_HHmmss")} 外側のループで50個ずつにまとめたグループのリストを取り出し、内側のループでは _parallel : true オプションをつけることで50個を並列で処理します。ここで呼び出しているupload_enhanced_conversionsメソッドでは、batch_numberを基準として、1で書き出した一時テーブルから2000件取得してリクエストします。Google Ads APIのライブラリのクラスやメソッドを使う処理は省略しますが、以下にコード例を示します。 _BATCH_SIZE = 2000 def upload_enhanced_conversions (timestamp: str , batch_number: int ) -> None : start_index = batch_number * _BATCH_SIZE # 対象データをすべて書き出したテーブル table_id = f 'project.dataset.purchase_conversions_{timestamp}' conversions_iterator = bq.list_rows(table_id, start_index, _BATCH_SIZE) # 指定位置から2000件取得 conversions_list = list (conversions_iterator) """ conversions_listをGoogle Ads APIのライブラリのメソッドを使用してアップロード """ def list_rows (table_id: str , start_index: int , batch_size: int ) -> List[bigquery.Row]: client = bigquery.Client(project=config[ "gcp_project" ], credentials=gcp.get_credentials()) table = client.get_table(table_id) row_iterator = client.list_rows(table, start_index=start_index, max_results=batch_size) return list (row_iterator) これで50並列での処理が可能になりました。対象データが100万件の場合、2000件ずつのリクエストが500回となり、50並列で実行するため外側のループは500/50=10回まわることになります。 外部SaaSからの移行 ここまで説明した内容を冒頭でも説明したように外部SaaSを利用して行っていました。ここからは外部SaaSからどのように移行をしたのかについてお話しします。 1. テスト用のコンバージョンアクションの作成 Google広告には開発環境が用意されておらず、本番環境でテスト用のコンバージョンアクションを作成しました。本番運用で使用しているコンバージョンアクションと同じ設定で作成します。本番運用と同じページタグを設置することで、本番運用と同様のコンバージョンを計測でき、本番用のコンバージョンに影響を与えることなく確認できます。 2. テスト用のコンバージョンアクションに対して拡張コンバージョンをアップロード アップロードは、コンバージョンアクションID単位です。このIDはコンバージョンアクション単位で振られます。テスト用のコンバージョンアクションに対してアップロードします。 3. テスト用のコンバージョンアクションで拡張コンバージョンが記録されることを確認 Google広告の管理画面から確認します。以下の画面の「オフラインでのコンバージョン」の詳細ページでアップロードされたかが確認できます。 アップロードのデータに問題があると以下のようにアラートが表示されます。 この時は以下が原因でアラートが表示されました。 24時間より前に購入が発生したデータをアップロードしていた コンバージョン発生から24時間以内のデータが必要とされる テスト用ということで少量データしかアップロードしなかった タグが発火した回数(コンバージョン発生回数)分のコンバージョンデータをアップロードする必要がある 4. 本番適用と外部SaaSとの並行稼働 本番運用で使用しているコンバージョンアクションに対してもテスト用と同様にAPIを使用してアップロードします。アップロードが上手くいかずコンバージョンの計測を補完ができなかった場合、本番運用に影響するため外部SaaSと並行稼働します。 5. 外部SaaSの利用停止 問題なく安定的に本番運用で使用しているコンバージョンアクションに対してアップロードできていることを確認して、外部SaaSの利用を停止します。 移行前と移行後 外部SaaSからの移行前後で拡張コンバージョンのカバレッジが改善され安定するようになりました。コンバージョンが失われている可能性があるとカバレッジの割合は低く表示されるため、高い割合であることが好ましいです。 まとめ 本記事では、Google Ads APIを用いた拡張コンバージョンの実装と外部SaaSからの移行方法について紹介しました。外部SaaSの機能の内製化と移行によって、拡張コンバージョンのカバレッジが安定し、利用コストも削減できました。Google Ads APIを使用して拡張コンバージョンの実装を検討している方の参考になりましたら幸いです。 さいごに ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! corp.zozo.com
アバター
Developer Engagementブロックの @ikkou です。ZOZO開発組織の1か月の動向をMonthly Tech Reportとしてお伝えします。 ZOZO TECH BLOG 2024年11月度は7本の記事を公開しました(前月分のMonthly Tech Reportを含む)。中でも次の3つの記事は多くの方に読んでいただきました。 techblog.zozo.com techblog.zozo.com techblog.zozo.com まだお読みでない方はぜひご覧ください。 登壇 ちむぐくる!TOKYO 11月6日に開催されたgusuku Customineのユーザー交流会『 ちむぐくる 』にコーポレートエンジニアリング部の新井が登壇しました。 #CybozuDays 前日📅 サイボウズ東京オフィス・Factoryにて 「ちむぐくるTOKYO」はじまりました🌺🌿 今年の参加者は去年よりもめちゃくちゃ増えて、会場ほぼ🈵状態です👀💫 ありがたい〜〜🔥 #カスタマインちむぐくる #gusuku #Customine #kintone pic.twitter.com/zEhys05zlo — gusuku🎪DX MARKET〜あなたのkintoneに合うトッピングは? (@gusukuSupport) 2024年11月6日 Cybozu Days 2024 11月7日に開催された『 Cybozu Days 2024 』にコーポレートエンジニアリング部の新井が登壇しました。 【登壇のお知らせ】 明日から幕張メッセで開催される #CybozuDays 2024 の1日目にコーポレートエンジニアリング部の新井が『大企業では、kintoneをこう使う!ノーコードツール活用術を全部見せ』というテーマの座談会セッションに登壇します🎙️ https://t.co/AuCHgAARCt #CybozuDaysで会いましょう — ZOZO Developers (@zozotech) 2024年11月6日 経営者・リーダーのためのデータ活用実践フォーラム 11月13日に開催されたオンラインセミナー『 経営者・リーダーのためのデータ活用実践フォーラム 』の基調講演にデータサイエンス部の西山が登壇しました。 オンラインセミナー登壇のお知らせ🎙️👨‍💻 明後日11/13(水) 13時より開催される #日経クロステック 主催『経営者・リーダーのためのデータ活用実践フォーラム』の基調講演にデータサイエンス部の西山が登壇、ZOZOにおける #生成AI の活用事例をご紹介します! https://t.co/fnACYrPrTS — ZOZO Developers (@zozotech) 2024年11月11日 speakerdeck.com JSConf JP 11月23日に開催された『 JSConf JP 』にWEARフロントエンド部 テックリードの冨川( @ssssotaro )が登壇しました。 カンファレンス登壇のお知らせ🎙️ 来週末11/23(土)に開催されるJSConf JP 2024にてWEARフロントエンド部の冨川 @ssssotaro が11:40よりトラックBにて『React CompilerとFine Grained Reactivityと宣言的UIのこれから』というタイトルで登壇します! https://t.co/2UI0kCTq54 #jsconfjp #zozo_engineer — ZOZO Developers (@zozotech) 2024年11月13日 jsconf.jp speakerdeck.com アーキテクチャConference 2024 11月26日に開催された『 アーキテクチャConference 2024 』にCTOの瀬尾( @sonots )が会場限定の講演として登壇しました。 11/26(火) 開催!アーキテクチャConference 2024にCTOの瀬尾 @sonots が『ZOZOTOWNのアーキテクチャ変遷と意思決定の歴史をADRから振り返る』というテーマで登壇します🎙️ 会場限定講演!14:00~14:40に B会場で皆さんのご来場をお待ちしています! https://t.co/30XLLJuuan #アーキテクチャcon_findy — ZOZO Developers (@zozotech) 2024年11月20日 本日登壇した資料こちらに置いておきますね ^^ 『ZOZOTOWNのアーキテクチャ変遷と意思決定の歴史を ADRから振り返る』 https://t.co/sM9SSjOrKM #アーキテクチャcon_findy — そのっつ (Naotoshi Seo) (@sonots) 2024年11月26日 GitHub Universe Recap 東京 後述するGitHub × ZOZOTOWNコラボレーションアイテムの販売にあわせ、11月27日に開催された『 GitHub Universe Recap 東京 』に技術戦略部の諸星( @ikkou )と堀江( @Horie1024 )が登壇しました。 GitHub x ZOZOTOWNコラボグッズ着用時のすがた 先週のイベントで 「GitHub Copilot全社導入のその後とGitHub×ZOZOTOWNコラボレーションの舞台裏」 をお話しされた @Horie1024 さんの良いお写真が撮れたので、許可をいただき掲載。 カジュアルT、GitHub CopilotデザインのライトグレーLサイズです。 pic.twitter.com/Eydp1NF1w4 — GitHub Japan (@GitHubJapan) 2024年12月3日 異色とも言える今回のコラボレーションの裏話と、2023年にGitHub Copilotを全社導入した後の現状をお話ししました。全社導入時の記事とあわせてご覧ください。 speakerdeck.com techblog.zozo.com CloudNative Days Winter 2024 11月29日に開催された『 CloudNative Days Winter 2024 』にSRE部 カート決済SREブロックの横田と同SRE部 プラットフォームSREブロックの亀井が登壇しました。 📰ZOZOエンジニア登壇情報 CloudNative Days Winter 2024の2日目にカート決済SREの横田とプラットフォームSREの亀井が登壇します🎙️ 🖥️システムリプレイスプロジェクト発足から7年、改めてコスト最適化に向き合う 📅 2024/11/29 13:20-14:00 Track B https://t.co/44brxUeEG6 #CNDW2024 #CloudNative — ZOZO Developers (@zozotech) 2024年11月28日 event.cloudnativedays.jp speakerdeck.com 掲載 Software Design 2024年12月号 ZOZOTOWNリプレイスプロジェクトについて全8回で連載中の「 Software Design 2024年12月号 」が11月18日に発売されました。 最終回となる第8回のテーマは「 フロントエンドエンジニアから見るZOZOTOWNリプレイスとまとめ・今後の展望 」です。ぜひご覧ください。 ZOZOTOWNリプレイスプロジェクトについて連載中の「Software Design 2024年12月号」が本日11月18日(月)に発売されました! 第8回のテーマは「フロントエンドエンジニアから見るZOZOTOWNリプレイスとまとめ・今後の展望」です。連載がいよいよ最終回です。お見逃しなく! #zozo_engineer https://t.co/avn09O2zb4 — ZOZO Developers (@zozotech) 2024年11月18日 第7回までの連載は全文を公開しています 。あわせてご覧ください。 日経クロステック(xTECH) ZOZOTOWNのアイテムレビューにおける生成AI活用についての記事が日経クロステック(xTECH)に掲載されました。 xtech.nikkei.com 有料会員限定の記事となりますが、1ページ目はどなたでもご覧いただけます。 また、アイテムレビュー機能そのものについては、関連記事を公開しています。ご興味のある方はあわせてご覧ください。 techblog.zozo.com techblog.zozo.com 日本ネット経済新聞 ZOZOTOWNとWEAR by ZOZOにおける、気温別にアイテムやコーディネイトを提案するコンテンツ・機能について、日本ネット経済新聞に掲載されました。 netkeizai.com ZOZOTOWNは『 FASHION FOR WEATHER 』として特設ページを公開しています。WEAR by ZOZOはiOS/Androidアプリ左上のお天気アイコンから「 コーデ予報 」を確認できます。ぜひ日頃のコーディネートの参考としてください。 東洋経済education×ICT 山田進太郎D&I財団と42 Tokyoによる「 Girls Meet STEM〜ITのお仕事を体験しよう〜 」にZOZOが参画することについて東洋経済education×ICTに掲載されました。 toyokeizai.net 既に参加者の募集は締め切っていますが、「ZOZOのワークショップでITのお仕事を体験!」と題して12月15日に実施します。後日、レポート記事を公開するので、お楽しみに! www.shinfdn.org WAKE Career WAKE Careerの女性エンジニアインタビューとしてバックエンドエンジニアを務める半澤のインタビューが掲載されました。特に女性・ITエンジニアの活躍やキャリアに関心のある方は必見の内容となっています。 wake-career.jp 半澤の過去の取り組みについては、以下のページもあわせてご覧ください。 www.wantedly.com aws.amazon.com イベント案内 12月に開催するイベントをご紹介します。 ZOZO Advent Calendar 2024 ZOZO Advent Calendar 2024は過去最多となる全11シリーズ、275記事を公開予定 冬の風物詩とも言えるアドベントカレンダーに参加中です。今年は 過去最多となる全11シリーズ、275記事を公開予定 です。さまざまなジャンルの記事が公開されますので、ぜひ気になる記事を探してみてください。 qiita.com GitHub Universe 2024 Recap in ZOZO 10月29〜30日の2日間に渡ってサンフランシスコで開催された GitHub Universe 2024 を振り返るRecapイベントをオフラインで開催します。 ZOZOからは2名が登壇し、ゲストとしてニフティ株式会社さん、そして『コード×AIーソフトウェア開発者のための生成AI実践入門』の著者である服部さん( @yuhattor )も登壇します。GitHubに興味をお持ちの方はぜひご参加ください。 zozotech-inc.connpass.com AWS re:Invent 2024 Recap in ZOZO 12/2日から6日の5日間に渡ってラスベガスで開催される AWS re:Invent 2024 を振り返るRecapイベントを12月17日にオフラインで開催します。 ZOZOからはAWS re:Invent 2024参加者4名の他、ゲストとしてAWSの方も登壇します。AWS re:Invent 2024に参加した方も、参加できなかった方も、ぜひご参加ください。 zozotech-inc.connpass.com その他 GitHub × ZOZOTOWNコラボレーション 11月22日(金)12:00より、GitHubとZOZOTOWNのコラボレーションアイテムの受注販売が始まりました。GitHubに関するアイテムは GitHub公式のThe GitHub Shop でも購入できますが、本コラボレーションのアイテムはZOZOTOWNにて期間限定で販売されます。ぜひ 本コラボレーションの特設ページ をご覧ください。 🚨告知🚨 #GitHub × ZOZOTOWN コラボ 世界で最も広く採用されている AIが支援する開発者プラットフォーム 「GitHub」とのコラボレーションがついに...! 11/22㈮正午から発売スタート! お楽しみに👀 ▼特設ページURLはこちら https://t.co/uXzFmrtO36 pic.twitter.com/DVKS7GZMfv — ZOZOTOWN (@zozojp) 2024年11月21日 GitHub x ZOZOTOWNコラボに込めた想いを動画にしました🎥 どうぞご覧ください👇(字幕付き) pic.twitter.com/bz0DTPhbcZ — GitHub Japan (@GitHubJapan) 2024年11月26日 github.blog corp.zozo.com また、このコラボレーションにあわせて GitHub Universe Recap 東京 にてGitHub × ZOZOTOWNのコラボアイテムを展示するブースを設けました。 GitHub × ZOZOTOWN ブース イベント前日までに売り切れてしまい展示のみになったキーキャップセット 私も終日ブースに立っていましたが、まるでショップ店員になった気分でした。とても多くの方にお立ち寄りいただき、ありがとうございました! 受注期間は12月13日(金)11:59までの期間限定です。ぜひこの機会にお求めください! 現場からは以上です! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに 技術評論社様より発刊されている Software Design の2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。 ZOZOTOWNのリプレイスにあたり、検索機能に特化したマイクロサービスが構築されました。複雑に絡み合った既存機能からリプレイスすべき部分を見極め、どのように作業が進められたのでしょうか。その過程と成果を紹介します。 目次 はじめに 目次 はじめに Elasticsearchへのリプレイス 初期の検索機能 Elasticsearchへのリプレイス リプレイスの動機と技術選定 リプレイスに向けた検討事項 検索精度向上の取り組み Elasticserch導入後の課題と対処 リプレイスによる利点 検索機能の参照先をElasticsearchに一本化 課題 新インデクシングシステムの概要 データ基盤の整備 レガシーからモダンへ 検索機能に特化したマイクロサービスの構築 Web機能のリプレイス 既存システムが抱える課題 リプレイス後のアーキテクチャ リプレイス作業 1. 事前調査 2. PoC(Proof of Concept) 3. Step1:小規模なリプレイス 4. Step2:検索結果一覧機能のリプレイス リプレイスの過程で遭遇した困難 作業見積もりの難しさ 不要な機能の削除 二重開発の調整 膨大なビジネスロジック 極力リスクを取らないリリース リプレイスのバックエンド開発で得た教訓 専門チームによるリプレイスをすべき 段階的なリプレイスをすべき おわりに はじめに 本記事は連載の第7回としてZOZOTOWN検索機能のリプレイスについて紹介します。 ZOZOTOWNは2004年のサービス開始以降、IISとClassic ASP(VBScript)、SQL Serverを用いたモノリシックな構成で稼働してきました。初期の検索機能では検索エンジンとしてSQL Serverを採用していましたが、検索機能に対してより柔軟で高いパフォーマンスを求めElasticsearchへリプレイスしました。また検索機能に対する多様な要望とより高い検索精度を実現するため、モノリシックなシステムから機能を切り出し、検索に特化したマイクロサービスを構築しました。さらにWebサイト(IIS+Classic ASP)の検索機能はBFFとマイクロサービスの構成へリプレイスしました(図1)。 図1 検索機能のリプレイス年表 これらのリプレイスで解決した課題や得られた知見を順に紹介します。 Elasticsearchへのリプレイス 初期の検索機能 初期のZOZOTOWNの検索機能は、RDB(SQL Server)と全文検索エンジンであるGSA(Google Search Appliance)で構築されていました。GSAはキーワード検索に使用し、ヒットした商品IDを返却します。そのIDを基にRDBから商品情報を取得し、検索結果として表示していました。GSAの検索結果に対して再度RDBから商品情報を取得していた理由は、インデックス更新によるタイムラグを避け、在庫情報や価格情報などを最新の情報で検索結果として表示するためです。 Elasticsearchへのリプレイス リプレイスの動機と技術選定 2017年、GSAのサポート終了に伴い、RDBとElasticsearchの併用にリプレイスしました。選定の理由は次の2点です。 広範なコミュニティサポートと豊富なドキュメントを持つこと マネージドサービスを提供していること Elasticsearchは広範なコミュニティサポートがあり、問題解決や新しい機能の導入時に迅速に対応できる環境が整っています。また、ドキュメントが豊富で、導入や運用において詳細なガイドラインやベストプラクティスを参照できます。 さらに、Elasticsearchのマネージドサービス「Elastic Cloud」では、マルチAvailability Zone構成をサポートしているため、高可用性と災害復旧の対策が施されています。Elastic社から24時間365日のプラチナサポートが利用可能な点も魅力で、保守管理の負担を軽減できると考えました。 リプレイスに向けた検討事項 Elasticsearchへのリプレイスに向けた具体的な検討を開始しました。既存システムとの整合性を維持しつつ、新しい技術に適応するため、次のステップを実施しました。 Elasticsearchのスキーマ定義の設計 表記揺れ対策やシノニム(同義語)などの日本語処理 検索ログを用いたパフォーマンス試験の目標値設定とキーワードの傾向分析 リプレイス前後の検索結果比較の環境準備と実施 パフォーマンス、バックアップ、メンテナンス時間などの非機能要件、移行計画、運用保守体制の整備 検索精度向上の取り組み Elasticsearchの検索精度向上のために、Mappingの検討、Analyzerの選定、辞書登録を行いました。Mappingはドキュメント内の各フィールドのデータ構造やデータ型を定義するものです *1 。言語処理や検索の方法を細かく制御するために、フィールドごとに適切なAnalyzer(データをどう分割・処理するか)を選定しました。また検索時の表記揺れや同義語の対応のため辞書登録(シノニムやカスタム辞書)を行い精度の高い検索を目指しました。 精度を担保するためリプレイス前後の検索結果を比較し、悪化したキーワードについては、原因調査と改善を繰り返し検索精度の向上に取り組みました。 Elasticserch導入後の課題と対処 Elasticsearch導入後、2つの課題がありました。 1つ目は、バージョン更新対応に伴う作業の多さです。Elasticsearchは頻繁に新しいバージョンがリリースされます。これに対応するためには、既存の設定やプラグインの互換性を確認し、システム全体のテストが必要です。テストを実施する際には、バージョンアップ後の新しいクラスタを準備し、現行クラスタと新クラスタそれぞれのindexを同じ頻度で更新することで、同一の環境を構築し、リグレッションテストを行う方針を取りました。また、突発的な対応が難しいため、EOLから逆算してバージョンアップ計画を立て、必要な工数を見込んだうえで関係者とスケジュール調整を行っています。 2つ目は、シノニム定義とカスタム辞書のindex反映タイミングの課題です。運用中のindexにシノニム定義やカスタム辞書を反映させるためには、indexを再作成し、データを再投入する必要があります。この作業は検索精度に影響を与えるため、慎重に対応する必要がありました。search側ではシノニム定義を即時に反映できる一方、index側への反映は時間がかかるため、リアルタイムでの対応が困難でした。また、indexの再作成は運用負荷が高いため、現在は更新を控えています。その代わりに、シノニムやカスタム辞書を補うため、クエリの意図解釈を用いたしくみを導入し、対応を進めています *2 。 リプレイスによる利点 Elasticsearchへのリプレイスにより、次のような利点が得られました。 クラウドサービスを活用したインフラサポートの充実 スケーリングの柔軟性と効率化 まず、スケーリングやバックアップ、セキュリティパッチの適用などのインフラ運用に関わる重要なタスクがクラウドサービス側で自動化され、チームはアプリケーション開発に集中できるようになりました。また24時間体制のサポートが受けられ、障害時の対応を迅速に行える環境にもなりました。 加えて、Elasticsearchのクラスタは負荷状況に応じて柔軟にスケールを変更できるため、安定したパフォーマンスを維持できます。これにより、リソースの無駄を最小限に抑え、運用コストの削減にも成功しました。 検索機能の参照先をElasticsearchに一本化 2020年に、RDBとElasticsearchの併用からElasticsearchへの完全移行を進めました *3 。この移行の背景には、検索機能のパーソナライズ化を進める全社方針が確定していたことが挙げられます。 それまでZOZOTOWNでは、表示順として「人気順」を用い、データを集計して算出されたスコアを全ユーザーに対して一律に提供していました。しかし、ファッションは年代や性別に加えて、個人の趣向によっても好みが大きく異なる分野であるため、パーソナライズされた検索の実現が求められました。そこで、RDBから検索に特化したElasticsearchへ置き換えることによって、検索のパーソナライズ化を実現することとしました。 課題 既存のシステムには次の2つの課題がありました。 リードタイムの短縮 データの柔軟な受け入れ 既存のElasticsearchを用いた検索では、キーワードにマッチした商品IDのみを取得し、それをキーにRDBから最新の情報を引き当てていました。したがって、品切れや価格等のクリティカルな情報はRDB側で補完できたため、情報の更新は定期的なバッチで行っていました。一方で、RDBの併用を廃止しElasticsearchのみでの検索を実現するにあたり、RDB内マスタテーブルの更新からElasticsearchへ変更を反映するまでのリードタイムに制約が設けられました。この制約を満たすためには、既存のバッチのしくみでは対処が難しく、これに耐え得る新しいしくみが必要でした。 また、パーソナライズ化を進めるにあたり、機械学習に基づくデータやデータ分析に基づく商品特徴量など、複数チームがデータ追加の作業に関与することが想定されました。そのため、どのチームからでも任意のデータを受け入れられるようにして、柔軟に検索可能な状態を目指しました。 新インデクシングシステムの概要 データベースの変更を追跡し、Elasticsearchに反映するためにCDC(Change Data Capture)を使用しました。この機能はテーブル単位の設定が可能で、レコードの追加・変更・削除やテーブルに対するカラム追加・削除の履歴を取得できます。しかし、CDCだけでは変更されたレコードの情報しかなく、検索可能なデータにするためには別のテーブルと結合する必要があります。 当社では以前からDWH(Data Ware House)としてBigQueryを使用しており、本システムに要求される大規模データをすばやく処理するという要件に適していました。そのため、BigQuery上に構築されたデータ基盤のCDCを参照し、そこから得られた情報と、ある時点のスナップショットのテーブルを結合し、最新の情報を構築しています。定期的なデータ更新はGoogle CloudのApp Engine上でバッチ処理として行われ、Elasticsearchに反映されます。 このシステムを用いることで、商品がマスタテーブルに登録されてから、もしくは売り切れ等でステータスが変わった場合でも、平常時約10分以下のリードタイムで商品情報に反映することが可能となりました。 データ基盤の整備 BigQueryに構築されたデータ基盤にリアルタイムでDBデータを連携するしくみについて説明します。 従来のデータ基盤では1日1回の頻度でRDBからデータを抽出し、BigQueryにロードしていました。しかし、このデータをインデックス作成に使うと1日もの遅延が発生してしまうため、そのままでは使用できません。そのため、SQL ServerのChange Tracking機能を使い、変更の発生した行の差分情報のみを高速にBigQueryにロードするシステムを構築しました。なお、Change TrackingはCDCに似てはいるものの厳密にはCDCとは異なる機能ですが、説明の簡素化のためにここでは同一機能として扱います。 差分情報は最近変更のあった行の情報のみしか保持していないため、従来の日次でデータ連携しているデータとマージすることでRDBと同等の情報をBigQueryで再構成しています。当初はこの部分に有料のデータインテグレーションツールを使用していましたが、パフォーマンスが良くない・ソースコードが読めないことによりトラブルシューティングに限界があるなどの問題点があったため、OSSベースのシステムを構築し直しました *4 。OSSのデータコレクターとして有名なFluentd *5 を使いSQL ServerからCDCデータを読み出しています。 この用途で使用できるインプットプラグインは存在しなかったので、自社でインプットプラグインの開発も行いました。インプットプラグインが読み出したCDCデータはFluentdのPub/Subアウトプットプラグインに渡され、その後Pub/Sub→Dataflow→BigQueryという流れでストリーミングインサートされます。RDBでデータの変更が発生してから平均1分以内にそのデータがBigQueryで利用可能になります。 当初は検索のインデクシングのために構築したシステムでしたが、その便利さからか現在では検索以外のZOZOTOWNの機能を裏側から支えることも多くなりました。 レガシーからモダンへ 検索機能に特化したマイクロサービスの構築 RDB(SQL Server)とElasticsearchを併用していた2019年に、VBScriptで実装されていた検索機能は、ZOZOTOWNの参照系機能を提供するモノリスAPIとしてJavaへ一度リプレイスされました。その後、2020年にマイクロサービス化しながらリプレイスする全体方針に変更となり、検索機能のAPIを分離すると同時に、Elasticsearchに一本化し、検索専用のマイクロサービスを構築しました *6 。これは、2つの課題を解決することが目的でした。 1つは、複数チームで1つのコードベースのJava APIを開発しているため、リリースタイミングが調整しにくく、開発生産性が低下していたことです。もう1つは、異なる役割のAPIが同じコードベースに含まれ、トラフィック量にばらつきが生じていたことです。Java APIはSQL Serverを参照し商品やブランド・ショップなどの情報を取得しており、一方で検索機能のAPIはElasitcsearchを参照していました。 Web機能のリプレイス 連載第6回(本誌2024年10月号)で紹介しているBFF(Backends For Frontend)の実装と、Web機能に先行してZOZOTOWNホーム画面のリプレイスがリリース *7 されたことで、Web機能においてもモノリシックなシステムから脱却する道筋ができました。そのため2024年にWebの新品・古着の検索機能についても同様のアーキテクチャをベースにリプレイスを実施しました。 具体的には、Classic ASPでVBScriptを使って実装された検索機能の処理をフロントエンドとバックエンドで分割しました。フロントエンドはReact、バックエンドはNext.js+BFF+マイクロサービス+S3で再実装することで、開発サイクルを分離し、それぞれでデプロイやスケーリングできるようにしました。なお本記事ではバックエンド(マイクロサービス)のリプレイスを中心に紹介します。 既存システムが抱える課題 サービス開始から約20年の歴史を持つプログラムは、長年の機能改修と多数の開発者が関わったことで複雑になり、ほかの機能との依存関係も複雑になっていました。とくにスコープの広い変数やセッションを多用しているロジックの理解には時間がかかり、開発スピードの低下を招くことがありました。当時の設計意図や背景を知らない新しいメンバーにとっては、処理内容が不明瞭で、修正箇所の影響範囲も不透明なため、プログラムの改修や保守に苦労していました。 リプレイスにおける課題の1つは、担当メンバーの多くが既存のアーキテクチャやVBScriptに精通していなかったことです。また、リプレイス後の新しいアーキテクチャについても、経験が不足していました。さらに、ZOZOTOWNのサービスを停止せずに、膨大な検索パターンや多種多様な検索機能をリプレイスする必要がありました。 リプレイス後のアーキテクチャ リプレイス前のWebサーバには、検索条件の構築や検索結果描画用のHTML/JSONの生成をはじめ、リダイレクト処理、メタ情報の構築、関連コンテンツの取得など、さまざまな機能が実装されていました。リプレイス後のアーキテクチャでは、新規のマイクロサービスは構築せず、既存のマイクロサービスにエンドポイントを追加し、VBScriptで実装されていた処理を移行しました。また、Webサーバ上で参照していた設定ファイルについては、更新頻度を調査し、更新頻度が低いものはBFFに内包し、高いものはS3に配置するなど、適切な配置を行いました(図2)。 図2 検索機能のアーキテクチャ遷移 リプレイス作業 マイクロサービスへのリプレイスは、次の流れで実施しました。 事前調査 PoC(Proof of Concept) Step1:小規模なリプレイス Step2:検索結果一覧機能のリプレイス このリプレイスは非常に大規模であると予想されたため、リプレイスに専念できる体制と工期を確保しました。また、既存機能に改修が入ることで、既存システムとリプレイス後のシステムの両方に改修を加える必要が生じないよう、関係各所と事前に案件を調整しました。 1. 事前調査 リプレイス後のアーキテクチャの概観は見えていたため、まずはそのアーキテクチャに対してフィット&ギャップ分析を行い、既存の検索機能の要件が満たせない部分を調査しました。検索機能特有の処理を実装する必要があったため、設計意図や意思決定の経緯を記録するためにADR(Architecture Decision Record)を作成しました。 既存機能を分解するため、まずは既存機能の概要図と機能一覧を作成し、全体像を把握しました。より詳細に把握するため、プログラムを125個の処理単位に分類し、それぞれの実装内容を分析しました。これにより、リプレイス先の候補を選定し、内部の依存関係やロジックの複雑さを理解できました。また、開発メンバーと一緒に分析することで、複雑なロジックに対して理解が浅い部分を補い、プロジェクト全体の理解度を向上させることができました。 2. PoC(Proof of Concept) パフォーマンス要件に対する懸念があったため、リプレイス後のアーキテクチャに最低限の機能を実装し、負荷検証を行いました。これは、規模が大きく不確実性の高い状況で手戻りを防ぎ、プロジェクトのリスクを軽減するための有効な手段でした。また、リプレイス後の新しいアーキテクチャの開発プロセスと技術スタックを経験する貴重な機会となりました。 3. Step1:小規模なリプレイス 検索機能のビッグバンリプレイスを防ぐため、検索機能のリプレイスを2つのステップに分けて実施しました。まず、ユーザーへの影響を最小限に抑えるため、利用頻度が比較的少ない画面からリプレイスを始めました。PoCを経験していたため、このステップではスムーズに開発を進めることができました。またStep2に備え開発方針の整備とレビュー体制の見直しを行いました。 4. Step2:検索結果一覧機能のリプレイス ZOZOTOWNの検索結果一覧画面は機能が多く、リプレイスの規模が大きいため、作業を分割して進めることを検討しました。しかし、画面全体の依存関係が複雑で分割できませんでした。また従来の検索チームだけではリソース不足だったことから、中途採用や協力会社をチームに迎え入れ、開発をスタートしました。 リプレイスの過程で遭遇した困難 作業見積もりの難しさ 通常のシステム開発では、要件定義、設計、実装、テストと進めるのが一般的ですが、リプレイスでは既存システムと同等の要件を満たしつつ、リプレイス先のマイクロサービスアーキテクチャに合わせて設計する必要があります。このため、既存の要件を新しいアーキテクチャに適応させる作業が求められました。 長年にわたり進化してきたプログラムは、設計書が少なく、検索機能以外の機能とも複雑に絡み合っています。そのため、すべてを隅々まで把握するのは困難でした。とくに、検索機能が動作する過程でほかの機能も通過するため、本来リプレイスすべき部分と不要な部分を見極める必要がありました。単純な条件分岐はわかりやすいですが、共通処理として実装された機能で複数の条件が含まれる場合は、注意が必要です。 既存の処理を1つずつ詳細に調査し、リプレイスにかかる開発工数を見積もるのは時間がかかり過ぎるため現実的ではありませんでした。そこで、まずは検索結果一覧画面を6つのコンテンツに分けて作業範囲を明確にしました。コンテンツ単位でできる限り詳細に見積もり、複雑な処理や不確実な部分については課題として認識し、バッファを用意しました。それでも、実装完了までに複数回のスケジュール見直しが必要でした。 不要な機能の削除 検索機能の中には、利用されていない処理が多数存在していました。将来の利用可能性が低いレガシーな部分については、PMや関係者と相談のうえ、積極的にリプレイス作業から除外しました。 検索機能には、URL形式やカテゴリの構造変更に伴い互換性を維持するために多くのリダイレクトが実装されています。長年の運用の中で、リクエストされていないURL形式やクエリパラメータが増えていきました。こうしたケースでは、リクエスト状況を1つずつ確認し、リクエストが極端に少ないものについてはリプレイスしない判断をしました。 二重開発の調整 リプレイス作業に集中するためには、機能開発の一時停止やコードフリーズが理想です。しかし「事業を止めない」というポリシーに従い、優先度の高いビジネス要求については既存システムの改修を行い、その内容をリプレイス側に取り込むことにしました。 リプレイスの進行状況は全社に周知されていたため、検索機能のリプレイスが進行中であることを説明し、対応時期を調整できる機能開発は後回しにしました。これにより、できる限り二重開発を避けるよう努めました。 膨大なビジネスロジック まず、6つに分けたコンテンツごとにBFFと各マイクロサービスのAPI I/F(インターフェース)の設計に取り掛かりました。モノリシックなシステムを、まったく異なる設計思想を持つマイクロサービスとして再構築するには、検索機能に関する深いドメイン知識と、BFFおよび各マイクロサービスの知見が不可欠でした。とくにHTML組み立てに必要な情報が欠落するとバグに直結するため、フロントエンド開発者と密に連携し、API I/Fの各項目を丁寧に決定していきました。また、複雑な描画パターンを持つコンテンツに関しては、処理を整理し、描画パターンを設計書にまとめることで、フロントエンドとの意思疎通を図りました。 モノリシックなシステム内で複雑に絡み合ったVBScriptは、開発するうえでの高いハードルとなりましたが、これを乗り越えるために、デイリーミーティングで課題を共有・相談し、停滞を防ぎ早期解決を目指しました。 テスト工程では、テストの役割を整理し再定義しました。CIに組み込まれた単体テストに加えて、Karate注8を利用したエンドポイントテストを導入し、手動テストを自動化することでデグレを防ぎ、APIの品質を担保しました。結合テストでは、品質管理部門のQAチームと開発チームが協力して、ホワイトボックステストとブラックボックステストを本番同等のパイロット環境で実施し品質を確保しました。 極力リスクを取らないリリース 本番リリース時にはサイトの停止を行わず、ユーザーを既存システムとリプレイス後のシステムに段階的に振り分けるn%リリースを実施しました。1%、20%、50%、100%と徐々にユーザーの割合を増やす計画を立て、その過程で致命的な不具合が見つかった場合には、すぐに0%に戻す対応を取りました。また、事業への影響を最小限に抑えるため、大規模なセールなどのイベント開催時にはn%リリースを一時中断し、すべてのユーザーを既存システムに戻して安全にイベントを進めました。 リプレイスのバックエンド開発で得た教訓 専門チームによるリプレイスをすべき まずマイクロサービスアーキテクチャでは各サービスと結び付いたチームの責務範囲でシステムを構築するため、開発組織がその体制を受け入れられるかどうかはとても重要です。弊社ではマイクロサービスを意識した組織設計がされており、検索機能を専門とする検索チームが主軸になり集中してリプレイスすることで、難易度が高いリプレイスに取り組みました。 段階的なリプレイスをすべき 大規模でモノリシックなレガシーシステムを、設計思想がまったく違うマイクロサービスアーキテクチャで再実装することは、難易度が高く膨大な工数を伴います。そのためZOZOTOWNのような大規模システムのリプレイスにおいては、できる限り細かな単位でリプレイスするアプローチはとくに重要です。 残念ながら、検索機能のリプレイスにおいては段階的なアプローチが十分に取れず、リプレイス作業を2段階に分けるにとどまりました。とくに、Step2のリプレイスは大規模な作業となりました。振り返ると、段階的リプレイスの議論に十分な時間を確保できなかった点は反省点です。 おわりに 検索機能をよりモダンでスケーラブルなアーキテクチャへと進化させるために、機能全体をリプレイスしました。この取り組みにより、パフォーマンスの向上や機能拡張が容易になるなどの成果を上げることができました。リプレイス作業はまだ続きますが、筆者たちの経験が同様の課題に直面している方々の参考になれば幸いです。 本記事は、技術本部 データシステム部 検索技術ブロック ブロック長の可児 友裕と同 検索基盤ブロック ブロック長の渡 雄一郎、そして同 データ基盤ブロック ブロック長 テックリードの塩崎 健弘によって執筆されました。 本記事の初出は、 Software Design 2024年11月号 連載「レガシーシステム攻略のプロセス」の第7回「検索機能リプレイスの裏側」です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com *1 : Elasticsearchで日本語検索を扱うためのマッピング定義 *2 : ZOZOTOWNのクエリ解釈機能の改善に向けたAPIリプレイスの取り組み *3 : ZOZOTOWNの検索基盤におけるElasticsearch移行で得た知見 *4 : ZOZOTOWNを支えるリアルタイムデータ連携基盤 *5 : https://www.fluentd.org/ *6 : ZOZOTOWN検索機能のマイクロサービス化への取り組み *7 : ZOZOTOWNのWebホーム画面をNext.jsでリプレイスして得た知見
アバター
はじめに こんにちは、SRE部プラットフォームSREブロックの石田です。普段はZOZOTOWNのSREを担当しています。 Amazon Aurora MySQL(以降、Aurora MySQL)のv2系の標準サポートが2024年10月31日に終了しました。私たちのチームではZOZOTOWNのID基盤で使用するAurora MySQLをv2系からv3系へアップグレードしました。ユーザ影響を抑えたアップグレードの実現のため、Amazon Aurora Blue/Green Deployments(以降、Blue/Green Deployment)を社内で初めて採用しました。 本記事では、Blue/Green Deploymentを採用する上で直面した課題や、検証方法について具体的な内容をご紹介します。なお、本記事の内容は2024年10月9日時点の情報に基づいています。 目次 はじめに 目次 Amazon Aurora Blue/Green Deployments とは 背景・課題 アップグレードバージョンの決定 Blue/Green Deploymentによるアップグレード検証 アップグレード手順の検証 1. 検証用のAuroraクラスター作成 注意点 2. 新バージョンのDBパラメータグループ作成 3. Blue/Green Deploymentによるアップグレードを実施 4. 再びCloudFormationの管理化 注意点 5. 切り戻し 注意点 API利用確認 検証環境のアップグレード 検証結果 本番環境のアップグレード 注意点 まとめ Amazon Aurora Blue/Green Deployments とは この機能では、稼働中の環境(Blue環境)をもとに論理レプリケーションされた環境(Green環境)を作成できます。これにより稼働中の環境には影響を与えずに、Green環境でデータベースのバージョンアップやパラメータの変更が可能です。準備が整えば、Green環境を新しい稼働環境として昇格させることで、通常1分未満のダウンタイムで安全かつスムーズに移行できます。詳細な仕様や制約、考慮事項については、AWSの 公式ドキュメント をご確認ください。 背景・課題 Aurora MySQLのマイナーアップグレードにおいてはIn-placeアップグレードを採用していました。その際のダウンタイムは1分程度でしたが、今回のメジャーアップグレードにおいて検証した結果、10分程度のダウンタイムが発生する可能性を確認しました。ZOZOTOWNの本番環境において、ID基盤の機能が10分も停止することは、ユーザに大きな影響を与えることになります。そのため、ダウンタイムを最小限に抑える手段としてBlue/Green Deploymentの採用を検討しました。 しかし、Blue/Green Deploymentは 制限事項 に記載されている通り、CloudFormationに対応していません。このため、CloudFormationで管理しているAurora MySQLのバージョン管理に課題がありました。そこで、Blue/Green Deploymentの検証を通して、CloudFormationによるバージョン管理についても確認することにしました。 アップグレードバージョンの決定 はじめに、Aurora MySQL v2系からv3系にアップグレードするバージョンを決定します。ID基盤のAurora MySQLは 5.7.mysql_aurora.2.11.5 のバージョンを使用しており、アップグレードできるバージョンを下記コマンドで確認します。 - > % aws rds describe-db-engine-versions --engine aurora-mysql --engine-version 5 . 7 .mysql_aurora. 2 . 11 . 5 --query ' DBEngineVersions[].ValidUpgradeTarget[].EngineVersion ' [ " 5.7.mysql_aurora.2.11.6 " , " 5.7.mysql_aurora.2.12.0 " , " 5.7.mysql_aurora.2.12.1 " , " 5.7.mysql_aurora.2.12.2 " , " 5.7.mysql_aurora.2.12.3 " , " 8.0.mysql_aurora.3.03.1 " , " 8.0.mysql_aurora.3.03.2 " , " 8.0.mysql_aurora.3.03.3 " , " 8.0.mysql_aurora.3.04.0 " , " 8.0.mysql_aurora.3.04.1 " , " 8.0.mysql_aurora.3.04.2 " , " 8.0.mysql_aurora.3.04.3 " , " 8.0.mysql_aurora.3.05.2 " , " 8.0.mysql_aurora.3.06.0 " , " 8.0.mysql_aurora.3.06.1 " , " 8.0.mysql_aurora.3.07.1 " ] 弊チームでは長期サポート(LTS)が提供されるバージョンの採用を方針としています。当時のLTSバージョンである 3.04.* の中から、最新の 8.0.mysql_aurora.3.04.3 を選定しました。最新のLTSバージョンについてはAWSの 公式ドキュメント をご確認ください。 なお、以前は 2.07.9 を使用していましたが、このバージョンから直接v3系へのアップグレードはできませんでした。そのため、まず 2.11.5 へマイナーバージョンアップを実施し、その後に今回のv3系へのアップグレードを行っています。このように、直接アップグレードができない場合もあるため、事前にアップグレード可能なバージョンを確認しておくことが重要です。サポート期限が迫る前に計画的に対応することで、余裕を持ったアップグレードが可能になります。 Blue/Green Deploymentによるアップグレード検証 ここからはBlue/Green Deploymentによるアップグレード検証の手順をご紹介します。 アップグレード手順の検証 まずはアップグレードの手順を検証するため、CloudFormationで検証用のAuroraクラスターを作成し、想定通りにアップグレードが行えるかを確認します。また、切り戻しが可能かどうかも併せて確認します。 1. 検証用のAuroraクラスター作成 開発環境で使用しているCloudFromationテンプレートを参考にして、Aurora MySQL v2系のクラスターを作成します。下記に作成したテンプレートを記載します。必要なパラメータがわかりやすいように関連のないParametersやResourcesの記載は省略しています。 --- AWSTemplateFormatVersion : "2010-09-09" Description : "[upgrade-test] Aurora" Parameters : GlobalPrefix : Type : String Default : upgrade-test GlobalEnvironment : Type : String Default : dev ServiceName : Type : String Default : upgrade-test RDSEngine : Type : String Default : aurora-mysql RDSEngineVersion : Type : String Default : 5.7.mysql_aurora.2.11.5 RDSParameterGroupFamily : Type : String Default : aurora-mysql5.7 Resources : DBClusterParameterGroupZozoID : Type : "AWS::RDS::DBClusterParameterGroup" Properties : Description : !Sub "cluster parameter group for ${ServiceName}" Family : !Ref RDSParameterGroupFamily Parameters : binlog_format : ROW DBParameterGroupZozoID : Type : "AWS::RDS::DBParameterGroup" Properties : Description : !Sub "parameter group for ${ServiceName}" Family : !Ref RDSParameterGroupFamily DBClusterZozoID : Type : "AWS::RDS::DBCluster" Properties : DBClusterIdentifier : !Sub ${GlobalEnvironment}-${GlobalPrefix}-${ServiceName} DatabaseName : !Ref DBDatabaseName DBClusterParameterGroupName : !Ref DBClusterParameterGroupZozoID Engine : !Ref RDSEngine EngineVersion : !Ref RDSEngineVersion DBInstanceZozoID : Type : "AWS::RDS::DBInstance" Properties : DBClusterIdentifier : !Ref DBClusterZozoID DBInstanceClass : !Ref Instance1DBClass DBParameterGroupName : !Ref DBParameterGroupZozoID Engine : !Ref RDSEngine DBInstanceZozoIDSecond : Type : "AWS::RDS::DBInstance" Properties : DBClusterIdentifier : !Ref DBClusterZozoID DBInstanceClass : !Ref Instance2DBClass DBParameterGroupName : !Ref DBParameterGroupZozoID Engine : !Ref RDSEngine 注意点 Blue/Green DeploymentではBlue環境からGreen環境にレプリケーションするため、バイナリログを使用しています。そのため、実際の稼働環境では、事前にクラスターのDBパラメータグループのバイナリログ(binlog_format)を有効にする必要がある点ご注意ください。なお、複製の不整合のリスクを減らすために ROW の設定が推奨されています。詳細はAWSの 公式ドキュメント をご確認ください。 2. 新バージョンのDBパラメータグループ作成 1で作成したテンプレートに下記を追加して、Aurora MySQL v3系に対応したDBパラメータグループを作成します。作成したDBパラメータグループは次の手順で使用します。 --- AWSTemplateFormatVersion : "2010-09-09" Description : "[upgrade-test] Aurora" Parameters : ServiceName : Type : String Default : upgrade-test RDSParameterGroupFamilyMySQL80 : Type : String Default : aurora-mysql8.0 Resources : DBClusterParameterGroupMySql80ZozoID : Type : "AWS::RDS::DBClusterParameterGroup" Properties : Description : !Sub "cluster parameter group for ${ServiceName}" Family : !Ref RDSParameterGroupFamilyMySQL80 Parameters : binlog_format : ROW DBParameterGroupMySql80ZozoID : Type : "AWS::RDS::DBParameterGroup" Properties : Description : !Sub "parameter group for ${ServiceName}" Family : !Ref RDSParameterGroupFamilyMySQL80 3. Blue/Green Deploymentによるアップグレードを実施 前述した通りBlue/Green DeploymentはCloudFormationに対応していないため手動で作成します。Blue/Green Deploymentを作成する際、Green環境は 8.0.mysql_aurora.3.04.3 のバージョンで作成します。DBパラメータグループは2で作成したものを指定します。作成方法はAWSの 公式ドキュメント をご確認ください。 Blue/Green Deploymentが作成されると 8.0.mysql_aurora.3.04.3 の新バージョンでGreen環境が作成されます。 切り替えを実行し、新バージョンに切り替わることを確認します。 4. 再びCloudFormationの管理化 アップグレード後、CloudFormationで管理しているリソースに差分が生じるため、テンプレートを修正して再適用します。ただし、本来CloudFormation管理下のリソースは、スタック外で変更することを推奨しておらず、CloudFormationとして動作を保証していない点ご留意ください。詳細はAWSの 公式ドキュメント をご確認ください。 以下に修正後のテンプレートを記載します。変更した箇所にコメントを追加しています。 --- AWSTemplateFormatVersion : "2010-09-09" Description : "[upgrade-test] Aurora" Parameters : GlobalPrefix : Type : String Default : upgrade-test GlobalEnvironment : Type : String Default : dev ServiceName : Type : String Default : upgrade-test RDSEngine : Type : String Default : aurora-mysql RDSEngineVersion : Type : String Default : 8.0.mysql_aurora.3.04.3 # 新バージョンに変更 RDSParameterGroupFamilyMySQL80 : Type : String Default : aurora-mysql8.0 Resources : DBClusterParameterGroupMySql80ZozoID : Type : "AWS::RDS::DBClusterParameterGroup" Properties : Description : !Sub "cluster parameter group for ${ServiceName}" Family : !Ref RDSParameterGroupFamilyMySQL80 Parameters : binlog_format : ROW DBParameterGroupMySql80ZozoID : Type : "AWS::RDS::DBParameterGroup" Properties : Description : !Sub "parameter group for ${ServiceName}" Family : !Ref RDSParameterGroupFamilyMySQL80 DBClusterZozoID : Type : "AWS::RDS::DBCluster" Properties : DBClusterIdentifier : !Sub ${GlobalEnvironment}-${GlobalPrefix}-${ServiceName} DatabaseName : !Ref DBDatabaseName DBClusterParameterGroupName : !Ref DBClusterParameterGroupMySql80ZozoID # 新しいDBパラメータグループに変更 Engine : !Ref RDSEngine EngineVersion : !Ref RDSEngineVersion DBInstanceZozoID : Type : "AWS::RDS::DBInstance" Properties : DBClusterIdentifier : !Ref DBClusterZozoID DBInstanceClass : !Ref Instance1DBClass DBParameterGroupName : !Ref DBParameterGroupMySql80ZozoID # 新しいDBパラメータグループに変更 Engine : !Ref RDSEngine DBInstanceZozoIDSecond : Type : "AWS::RDS::DBInstance" Properties : DBClusterIdentifier : !Ref DBClusterZozoID DBInstanceClass : !Ref Instance2DBClass DBParameterGroupName : !Ref DBParameterGroupMySql80ZozoID # 新しいDBパラメータグループに変更 Engine : !Ref RDSEngine 変更セットを作成し、CloudFormationのコンソールよりプロパティレベルの変更の詳細を確認すると以下のような差分が確認できます。 クラスターのプロパティレベルの変更の詳細 インスタンスのプロパティレベルの変更の詳細 CloudFormation上は差分がある状態ですが、このテンプレートを適用することで問題なくCloudFormation管理下に戻すことができました。 なお、テンプレート適用後、ライターインスタンスのイベントログに Finished updating DB parameter group というメッセージが表示されていました。DBパラメータグループの更新が完了したというメッセージになりますが、このメッセージが出力されたからといって、DBパラメータグループの設定内容が実際に更新されたわけではありませんでした。 AWSサポートに問い合わせたところ、ログが検出された原因の特定はできませんでしたが、現在使用されているDBパラメータグループに再度変更するようなAPI呼び出しは実行可能とのことでした。また、変更前に違うDBパラメータグループが適用されていたことやパラメータが実際に変更されたことを直ちに示すものではないとのことです。ただし、テンプレート適用前後で、DBパラメータグループの設定内容に変更がないか確認しておくと安心です。 注意点 テンプレート適用前にインスタンスのDBパラメータグループのステータスを確認してください。ステータスが pending-reboot の場合はテンプレート適用時に再起動する可能性があります。ステータスが 同期中 であれば再起動が発生することはありません。 5. 切り戻し Blue/Green Deploymentには自動的な切り戻し機能はありません。Blue/Green Deployment作成時にはBlue環境からGreen環境にレプリケーションされますが、切り替えたタイミングでレプリケーションは切断されます。そのため、切り戻しは手動でエンドポイントを操作し、新バージョンのクラスターエンドポイントを別の名前に変更後、旧バージョンのクラスターエンドポイントを元に戻す流れで行います。イメージ図のクラスタ名およびインスタンス名はわかりやすくするために簡略化しています。 Blue/Green Deploymentの削除 まず、Blue/Green Deploymentを削除します。これにより、新バージョンと旧バージョンのクラスターの紐付きが解消されます。 新バージョンのクラスター名を変更 新バージョンのクラスター名を別の名前に変更します。ここでは末尾に -new をつけます。これによりエンドポイントが変更されるため、リクエストが全断します。 新バージョンのインスタンス名を変更 新バージョンのクラスター名の変更が完了したら、新バージョンのインスタンス名も別の名前に変更します。ここでは末尾に -new をつけます。 旧バージョンのインスタンス名を変更 次に、旧バージョンのインスタンス名を正規の名前に変更します。 旧バージョンのクラスター名を変更 最後に、旧バージョンのクラスター名を正規の名前に変更します。この変更が完了するとエンドポイントが元に戻ります。そして、旧バージョンのクラスターに対してリクエストが届くようになります。 こちらの手順で実施し、15分ほどで問題なく切り戻すことができました。 注意点 本番環境での実施時には、アップグレード中に問題が発生した場合、データに不整合が生じているかどうかの懸念があります。例えば、部分的なINSERT処理が失敗するなどの不具合が考えられます。そのため、切り戻し後にはデータの整合性を確認し、補正が必要となる場合があります。 API利用確認 開発環境のアップグレードを実施し、新バージョンのAurora MySQLにてID基盤で使用しているAPIが問題なく動作することを確認します。アップグレードは前述の通りの手順で実施します。結果としてアップグレードは問題なく完了し、ID基盤で使用しているAPIも正常に動作しました。 検証環境のアップグレード 検証環境のアップグレードは、商用相当のリクエスト負荷をかけて実施し、エラー発生状況やダウンタイムを確認します。商用相当のリクエストをかける理由は、 切り替えのベストプラクティス で記載されている通り、切り替え時のダウンタイムはリクエスト量に依存するためです。また、負荷をかけるツールは弊チームで開発したOSSツールであるGatling Operatorを使用しています。詳細は以前TECH BLOGに公開された下記記事をご参照ください。 techblog.zozo.com アップグレードは前述の通りの手順で実施します。結果としては想定通りのエラーが発生し、ダウンタイムとしては1分程度でした。なお、ダウンタイムの計測には、Datadog APMのトレース機能を利用しています。アプリケーションエラーを示す trace.http.request.errors メトリクスを可視化することでエラーが発生していた時間を計測しています。以下はDatadog Dashboard上で可視化したグラフです。これより、エラーの発生時間が1分程度であったことを確認できます。 検証結果 ここまでの検証を通して、以下の点を確認できました。 Blue/Green Deploymentを利用して、Aurora MySQL v2系からv3系へのアップグレードが可能であること v3系からv2系への切り戻しが可能であること アップグレード後、再びCloudFormationによる管理が可能であること v3系において、ID基盤としてのアプリケーションが問題なく動作すること ダウンタイムは1分程度であり、許容範囲内であること 本番環境のアップグレード 本番環境でのアップグレードは、ユーザへの影響を最小限に抑えるため、早朝の時間帯に実施します。アップグレードは前述の通りの手順で実施します。結果として、検証時の予測通りにエラーが発生し、ダウンタイムも約50秒と想定内に収まりました。これにより、問題なくアップグレードを完了できました。 注意点 Blue/Green Deploymentは新しいクラスターに切り替わるため、バイナリログを使用したデータの連携をしている場合は注意が必要です。実際に、ID基盤のデータベースではGCPのDatastreamを介してBigQueryにデータを連携していますが、この点を見落としていました。 公式ドキュメント の コンシューマーの親ノードの更新 の項目では、バイナリログコンシューマーがある場合はレプリケーションの継続性を維持するため、切り替え後に親ノードを更新する必要があると記載されています。今回のケースでは、バイナリログコンシューマーがDatastreamにあたります。 切り替え後のライターインスタンスでは、以下のようなバイナリログファイル名( mysql-bin-changelog.000008 )と位置( 536 )を含む情報がイベントログに出力されます。この情報を使用してDatastreamを更新することでデータ連携を再開できます。 この点を見落としていたため、切り替え後にデータ連携が停止し、復旧対応としてbackfill(全量転送)を実施せざるを得ませんでした。こうした事態を防ぐためにも、事前にバイナリログを利用するコンシューマーの存在を確認し、切り替え後に必要となる対応を計画的に準備しておくことが重要です。 まとめ 本記事では、Blue/Green Deploymentによるアップグレードの検証方法や移行に際して直面したコード管理の課題に対する対応について、具体的な取り組みを紹介しました。 入念なアップグレード検証により、ユーザへの影響を最小限に抑えつつ、確実なアップグレードを実現できました。また、CloudFormationで管理しているAmazon Aurora MySQLのバージョン管理においても、スタック外での変更は推奨されないものの、問題なく継続できることを確認できました。 さらに、この取り組みで得られた知見やノウハウは、社内の別マイクロサービスにおけるAuroraのメジャーアップグレードにも活用され、スムーズな移行を支援する結果につながりました。Blue/Green Deploymentによるアップグレードを検討されている方にとって、この記事が参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
アバター
はじめに こんにちは、FAANS部バックエンドブロックでFAANSのバックエンドシステムの開発と運用をしている 田島 です。 2021年11月にZOZOTOWNとアパレルのブランド実店舗をつなぐOMOプラットフォーム「ZOZOMO」が始動しました。FAANSは、ZOZOMOで展開するサービスの1つで、ブランド実店舗で働くショップスタッフ専用の販売サポートツールです。FAANSは2022年8月の正式版リリース以来、これまで様々な機能をリリースしてきました。以下はその一部です。 投稿機能 : ショップスタッフが自身で自社のアイテムを着て撮ったコーディネート画像やコーディネート動画といったコンテンツを複数チャネルに同時投稿できる機能。投稿先チャネルとしては、ZOZOTOWNやWEAR、Yahoo!ショッピングといった弊社並びに弊社のグループ会社のWebサイトに加え、ブランド企業の自社ECサイトへの同時投稿も可能。 成果確認機能 : 投稿機能で投稿したコンテンツがどのくらい閲覧されたのか、それをきっかけにEC上の売上にどのくらい貢献できたか、といった様々な切り口の『成果』をアプリ上で確認できる機能。 顧客直送機能 : ブランド実店舗で欠品している商品の在庫が弊社の物流拠点「ZOZOBASE」にある場合、お客様は店頭で決済し、ZOZOBASEからお客様の自宅へ商品を直送できる機能。 ブランド実店舗の在庫取り置き機能 : ZOZOTOWN上で実店舗の在庫取り置きを希望したお客様への対応を、ショップスタッフがFAANS上の簡単操作で完結できる機能。 FAANSではリリースから1年も経っていない段階でSLO(サービス品質目標)を導入し、ここまで試行錯誤を重ねて運用してきました。本記事ではFAANSにおけるSLO運用の実践とそこから得られた知見について紹介します。 目次 はじめに 目次 1 SLO導入の背景 2 FAANSのシステムとそれを支える開発組織 2.1 システムの特徴 2.2 開発組織の体制 3 最初のゴール設定 3.1 バックエンドシステムのみを対象としたSLOを運用に乗せる 3.2 SLOアラートは導入せず、SLOの達成状況の定期的な確認に留める 4 最初のSLI/SLOの選定 4.1 選定方針 4.1.1 対象システムはWeb APIサーバーのみとする 4.1.2 重要機能の単位とシステムコンポーネントの単位の2軸でSLOを定める 4.1.3 レイテンシーと可用性の2つのSLIを選定する 4.1.4 クラウドインフラのSLA未満の水準のSLOを定める 4.1.5 依存している社内の別プロダクトのサービス品質やSLOは一旦考慮しない 4.2 ステークホルダーとの合意 5 「SLO定例」による信頼性チェック 5.1 直近のリリース内容の確認 5.2 直近のアプリケーションエラーやシステム障害の確認 5.3 SLOの達成状況の確認 5.4 データベースのシステムメトリクスの確認 6 SLO運用開始後の工夫と取り組み 6.1 SLI/SLOの設定変更の記録をADR形式で残す「SLO-DR」 6.2 チームで行う継続的学習「SLO Study」 6.3 フルサイクルエンジニアリングチームへの体制移行 6.4 開発組織への定期報告 7 SLOを導入して得られた効果 7.1 明確な判断基準による会議の充実化 7.2 信頼性維持のためのアクション促進 7.3 他チームへの改善依頼の円滑化 7.4 技術的な意思決定の改善 8 SLOの導入は早ければ早いほどよい 8.1 SLO文化を浸透させるハードルが低い 8.2 SLOはソフトウェア設計の意思決定を支援する 8.3 信頼性は事業にとって最初から重要な指標である おわりに 1 SLO導入の背景 スタートアップ事業として立ち上がったFAANSはサービスローンチに向けた立ち上げフェーズを経て、現在は成長フェーズに移行しています。私達はさらなる顧客価値の提供のために、新機能のリリースや既存機能を強化するためのスピーディーな開発を日々行っています。一方で、FAANSではサービス企画段階から「当たり前品質の追求」を掲げてきました。ブランド実店舗で働くショップスタッフは、忙しい実店舗業務の傍らでFAANSを通じてコーディネート画像を投稿したり、FAANSを使ってお客様を接客したりします。そのため、アパレルブランド企業やそのショップスタッフがFAANSを利用する業務において、不満やストレスを感じることのないよう、サービス品質を維持することは極めて重要です。とはいえ限られたエンジニアリングリソースの中で、機能開発によるさらなる価値提供の取り組みとユーザーが不満を抱かないサービス品質維持の取り組みを両立することは、工夫なしでは実現できません。その両立を図るためにSLOを導入するに至りました。 2 FAANSのシステムとそれを支える開発組織 具体的なSLOの導入について話をする前に、まずはその前提知識となるFAANSのシステムの特徴とFAANSの開発組織の体制を概説します。 2.1 システムの特徴 FAANSのシステムは以下のような特徴を有しています。 FAANSのユーザーが利用するクライアントアプリとしてWeb/iOS/Androidに対応しています。 バックエンドシステムのインフラリソースは全てパブリッククラウド環境に存在します。そのほとんどがGoogle Cloudで構築されており、メール配信処理などで一部AWS(Amazon Web Services)を使用しています。 Web APIサーバーは役割の異なる複数種類のシステムコンポーネントが存在します。それらは全てGKE(Google Kubernetes Engine) *1 の1つのKubernetesクラスタ上でPodとしてサービングされています。以下はその一部です。 FAANSのiOS/Android/Webアプリからのリクエストを直接捌く クライアントアプリ用API 。 他のWeb APIサーバーから非同期でオフロードされたジョブを実行する 非同期ジョブ用API 。 アパレルブランド企業の自社ECサイトのシステムから直接アクセスされる 外部システム連携用API 。 ワークフローエンジンとしてKubernetesネイティブなArgo Workflows *2 を採用しており、Web APIサーバーが稼働するGKEクラスタに相乗りしています。スケジュールやイベント駆動で実行されるバッチ処理は基本的には全てArgo Workflowsのワークフローとして稼働しています。 ユーザーのマスタデータなどが管理されているオンライントランザクション向けのデータベースとしてGoogle CloudのCloud SQL(PostgreSQL) *3 を使用しています。このデータベースはWeb APIサーバーやバッチ処理からアクセスされています。 WEARやFulfillment by ZOZOといった弊社の別プロダクトのWeb APIにアクセスするWeb APIやバッチ処理も多く、社内の別チーム管轄の別プロダクトに依存したプロダクトです。 2.2 開発組織の体制 続いて、FAANSの開発組織についてです。以下はFAANSのプロダクト開発・運用に関わるメンバーが所属するブランドソリューション開発本部の組織図です。 ※引用元スライド: https://speakerdeck.com/zozodevelopers/company-deck 弊社ではチームをブロックという単位で編成しています。この中でFAANSのプロダクト開発と運用に中心的に関わるブロックを簡単に説明します。まず、プロダクト戦略部FAANSプロダクトマネジメントブロックにはFAANSのPO(プロダクトオーナー)やPM(プロジェクトマネージャー)といったロールのメンバーが所属しています。FAANSの開発と運用に携わるエンジニアはFAANS部に所属しています。フロントエンドブロックはFAANSのWeb/iOS/Android用のクライアントアプリの開発を、バックエンドブロックがFAANSのバックエンドシステム全体の開発と運用をそれぞれ担当しています。なお、以前はWEARバックエンド部のSREブロックがFAANSとWEARの2つのプロダクトのSRE業務を兼務していました。しかし、現在ではFAANS部バックエンドブロックにその責務は委譲されています。この体制移行の意思決定については後述の「6.3 フルサイクルエンジニアリングチームへの体制移行」で説明します。また、この図には記載していませんが、別の本部に所属するBizDev職メンバーやQA(品質保証)エンジニアもFAANSのプロダクトの開発や運用に深く関わっています。 3 最初のゴール設定 SLOのコアとなるプロセスは極端に単純化すれば以下に集約されます。 ユーザーの満足度に強く関連しているであろう代表指標= SLI(サービス品質指標) を選定し、ユーザーの信頼性を測定可能なものとする。 選定したSLIに対してユーザーが満足しているか、していないかに対応する目標値= SLO を定める。 SLOを満たしていない、つまり エラーバジェット が尽きた場合はSLOを満たすようにする改善を開発タスクより優先して実施する。 一方で、SLOは組織的な取り組みであるため、実用的な運用に乗せるためには戦略的な導入計画を考える必要があります。SLOを導入するにあたって、私達は慎重に「最初のゴール」を設定しました。 3.1 バックエンドシステムのみを対象としたSLOを運用に乗せる 理想的には、SLOはシステム全体、すなわちバックエンドシステムに限らず、クライアントアプリを含めたエンドツーエンドでの監視と運用をすることが望ましいです。ユーザーはクライアントアプリを介してバックエンドシステムを利用するので、バックエンドシステムのサービス品質がいくら高くても、クライアントアプリのサービス品質が悪ければユーザーハピネスに繋がりません。しかし、私達は導入段階で小さく始めることを選び、敢えてバックエンドシステムのみを対象にすることとしました。クライアントアプリは、今後の拡張フェーズでSLOの対象に加える予定です。このように小さなステップから始める理由は、SLOの運用が初期段階では試行錯誤の連続であり、最初からシステム全体に適用すると運用が複雑になりすぎる可能性があるためです。初期段階では対象範囲を絞ることで、SLOの設定や運用フローを確立し、チーム全体がその運用に慣れることを優先しました。この段階での成功体験をもとに、次のステップとして対象範囲を拡大していく方が、最終的な全体最適を図るためにも有効だと判断しました。 3.2 SLOアラートは導入せず、SLOの達成状況の定期的な確認に留める もう1つの重要な決定として、SLOアラートの導入を見送りました。SLOアラートは本来、サービスの品質が低下した際に迅速に対応するための仕組みですが、初期段階でこれを運用に組み込むと、試行錯誤が多い中でアラートが頻発し、かえってノイズとなる恐れがあります。これを防ぐため、まずはアラートを設けずに定期的なSLOの達成状況のチェックに留め、運用に慣れるまで柔軟に改善を重ねる体制を取りました。具体的には、SLOの達成状況を関係者で確認し、現状の目標の達成状況や運用課題を共有する形で進めています。その詳細は「5 『SLO定例』による信頼性チェック」で説明します。 このように、 最初のゴールをあえて小さく設定し、段階的に運用を確立していくことで、無理なく改善を重ねることが可能となる と考えました。 4 最初のSLI/SLOの選定 SLOは、SREのコアとなるプラクティスです。最初のSLI/SLOの設定を考える上で、まずはGoogleのSREチームによって執筆された2冊の書籍の該当章に目を通し、理解を深めました。それが 「SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム」 と、その副読本である 「サイトリライアビリティワークブック ―SREの実践方法」 です。これらの書籍では、SLI/SLOの概念から実際の運用方法までが体系的に解説されています。なお、私達がSLOを導入したタイミングでは和訳本がまだ存在していなかった 「SLO サービスレベル目標 ―SLI、SLO、エラーバジェット導入の実践ガイド」 という書籍が昨年発刊されました。SLOにまつわるトピックで1冊書かれた貴重な書籍です。これからSLOの導入を検討する方々には、参考文献としてぜひおすすめしたいです。また、これらに加えて各社のSLO導入事例を取り上げたWeb上の記事にも目を通し、他の企業がどのようにSLOを運用しているか、どのような課題に直面したかを学びました。これにより、理論だけでなく現実世界でのSLO導入に伴う具体的な実践方法や成功要因についても理解を深めることができました。 4.1 選定方針 書籍や記事で得たSLOに関する知識をベースにFAANSのシステムの特性を踏まえて考え、FAANSの最初のSLI/SLOを選定する上での方針を以下のように整理しました。 4.1.1 対象システムはWeb APIサーバーのみとする 最初のSLOを選定するにあたって、まずは小さく始めることを意識し、バックエンドシステムの中でもWeb APIサーバーのみを対象とする方針を立てました。この選定には、Web APIサーバーがFAANSにおけるサービスの中核を成しているという理由があります。FAANSのユーザーは、日常的にFAANSのクライアントアプリを通じてサービスを利用します。そして、ユーザーが行うアクションは、すべてWeb APIを介して処理されています。Web APIはユーザー体験の根幹を担っており、可用性やパフォーマンスが低下すれば、即座にユーザー体験へ悪影響を及ぼします。ユーザーであるアパレルブランド企業とそのショップスタッフにとって、Web APIサーバーはFAANSを利用する上で最も重要なシステムコンポーネントです。これを効果的に監視・管理することがSLO運用の初期段階で不可欠だと判断しました。なお、定時実行されるバッチ処理といったその他のシステムコンポーネントの信頼性の追跡に関してはSLO運用の拡大フェーズで検討することとしました。 4.1.2 重要機能の単位とシステムコンポーネントの単位の2軸でSLOを定める SLOを効果的に運用するためには、 システムの属性をうまくカバーできる必要最小限の選択をする ことが重要です。私達は、そのためのアプローチとして、 重要機能の単位 と システムコンポーネントの単位 の2軸でSLI/SLOを定めることにしました。 まず、重要機能の単位のSLO設定において、ユーザー体験の中で最も重要な操作やシナリオを把握するために、簡易的なCUJ(クリティカルユーザージャーニー)を実施しました。CUJとは、ユーザーがサービスを利用する際の主要なステップやアクションを追跡し、業務やサービスの成功に直結するポイントを特定するプロセスです。このプロセスを通じて、どの機能がユーザーにとって欠かせないのか、またその機能に関係するAPIの中で重要なものはどれなのか精査しました。例えば、ECサイトであれば「商品検索」「カートへの追加」「購入手続き」の各ステップがCUJの重要なポイントとなり、それに対応するAPIが重視されるでしょう。このCUJの結果をもとに、具体的な測定対象APIを選定しました。これにより、ユーザー体験の中で本質的な価値を持つ操作に焦点を当てることができ、SLOがユーザーにとって最大限の価値を提供できるような形となると考えています。 次に、システムコンポーネントの単位のSLO設定です。前述の通り、対象システムはWeb APIのみとするため、システムコンポーネントとしてはWeb APIサーバーの種別の単位となります。具体的には、前述のクライアントアプリ用Web APIサーバーや外部システム連携用Web APIサーバーなどです。そして、Web APIサーバーには複数のAPIが実装されていますが、それら全てのAPIを対象としたリクエスト処理に対する総合的な品質に対してSLOを定めることを指します。ただし、ヘルスチェックエンドポイントのようにユーザー体験に直接的な影響を与えないAPIは、サービス品質の測定結果に不要なノイズを生じさせる可能性があるため対象外としています。 このように、重要機能の単位とシステムコンポーネントの単位という2軸でSLOを定めた理由は、 業務の優先度と技術的な健全性の両面をカバーする ためです。システム全体の可用性やパフォーマンスを考慮しつつ、ビジネスにとって重要なユーザージャーニーの品質を確保することが、このアプローチの根幹にあります。SLOは、その設定が業務やサービスに与えるインパクトによって初めて価値を持ちます。もし、設定されたSLOがビジネスの優先度を反映しておらず、業務に影響を与えない指標にばかり焦点を当てていた場合、そのSLOは十分な価値を提供できないでしょう。この点を踏まえ、システムの技術的要素とビジネスの主要な機能の両方をカバーできるバランスを保つことが実用的で効率的なSLO運用のためには重要だと考えました。 4.1.3 レイテンシーと可用性の2つのSLIを選定する 私達は、Web APIサーバーというシステムのユーザーの信頼性を測る指標として レイテンシー と 可用性 の2つにフォーカスすることとしました。 まず、Web APIのレイテンシー、つまり応答時間は、ユーザー体験に直結します。APIのレスポンスがわずかに遅れるだけでも、特にリアルタイム性が求められるアプリケーションでは、ユーザーはフラストレーションを感じ、場合によってはサービスを離脱してしまいます。例えば、モバイルアプリやWebサービスでの検索やデータの読み込みが数秒以上かかると、ユーザーは「遅い」と感じ、その印象がサービス全体の評価に影響を与えることになります。ただし、前述の非同期ジョブ用APIは、そもそも処理に時間がかかることを前提として非同期で行われるためレイテンシーがそれほど重要ではないことから、今回はSLIの対象から除外しました。なお、レイテンシーのSLOは例えば『30日間のローリングウィンドウで99パーセンタイル値のレイテンシーが500ミリ秒以下』といった形で具体的に定義されます。 続いて、可用性(Availability)は、サービスがユーザーに常にアクセス可能な状態であることを示す指標です。可用性が低下するとユーザーはサービスにアクセスできなくなり、エラーやダウンタイムが発生すれば顧客に対する信頼性が著しく損なわれます。現代の多くのサービスは、24時間365日の稼働が当然とされています。Web APIサーバーは、他のシステムやクライアントアプリとの連携を担う中心的な役割を果たしているため、その可用性が低下すれば単にAPI自体の問題に留まらず、サービス全体へ広範な影響が及びます。なお、私達の場合は、Web APIサーバーや各APIの可用性を 成功したリクエスト数/全体のリクエスト数 というイベントベースの稼働率として算出しています。また、可用性のSLOは例えば『30日間のローリングウィンドウで99.5%以上の稼働率』といった形で具体的に定義されます。 私達がSLO運用の初期段階でレイテンシーと可用性にフォーカスしたのは、これらの指標がユーザー体験とサービスの信頼性に最も大きく影響を与えるからです。複雑な指標を多数導入するよりも、まずはWeb APIのようなリクエスト処理システムのSLIにおける2つの基本指標に絞り、運用フローをシンプルかつ効果的にスタートさせることが重要だと考えました。これにより、チーム全体がSLIの監視と改善サイクルに慣れ、徐々に運用の精度を上げていく狙いです。 4.1.4 クラウドインフラのSLA未満の水準のSLOを定める FAANSのバックエンドシステムは前述の通りGoogle CloudやAWSといったパブリッククラウド上に構築されています。そのため、そのシステムのサービス品質はパブリッククラウドのサービス品質に依存することとなります。例えば、FAANSのWeb APIのシステムはDNS、ロードバランサー、サーバーインスタンスやデータベースインスタンスなどのインフラコンポーネントで構成されています。Web APIへのHTTPリクエストがエラーを返すことなく正常に処理されるためには、これらのインフラコンポーネントが正常に稼働している必要があります。パブリッククラウドが提供するプロダクトの中には SLA(サービス品質保証) が定められているものも多いです。これら個々のインフラコンポーネントの役割を担うプロダクトの可用性SLAを調べたところ、最も低い水準だったものはGKE AutopilotのPodの『99.9%以上の稼働率』というSLA *4 でした。よって、FAANSのWeb APIサーバーの可用性SLOを定義する上で、『99.9%以上の稼働率』というインフラのSLA未満の水準で定義する必要があると考えました。 その理由を具体例を用いて説明します。例えば、Web APIのシステムを構成するクラウドインフラの可用性SLAが『99.9%以上の稼働率』だったとします。しかし、実際にしばらく計測してみると安定して『99.99%以上の稼働率』でした。そこで、Web APIの可用性SLOを「30日間のローリングウィンドウで『99.95%以上の稼働率』」と定義しSLOを運用し始めたとします。すると、しばらくの間はSLOを満たした状態が続いていたのですが、ある時Web APIの可用性SLOが未達状態に陥ってしまいました。調べてみると原因はクラウドインフラの可用性の低下であることが判明し、低い時では『99.90%の稼働率』にまで下がっていました。SLOの目標値である『99.95%以上の稼働率』を下回る品質が一定期間続きエラーバジェットを使い切ったので、SLOを満たす状態に戻す改善を早急に実施する必要があります。ところが、この時原因はクラウドインフラの可用性が低下したことなので提供元のクラウドベンダーに相談しても、可用性が改善されるとは限りませんし一般的には期待できません。なぜならば、『99.90%の稼働率』にまで下がっていたとしても『99.9%以上の稼働率』というSLAを満たせているからです。クラウドベンダーは顧客との間で合意済みのサービス品質で依然として提供できているため、顧客のための改善のアクションを取る必要性は基本的にはないのです。 この例からも分かる通り、クラウドインフラのSLAを超える水準のSLOを定めて運用することは現実的とはいえません。ただし、ここで重要なことは、 あくまで現状のインフラ構成のままの場合の制約である ことです。SLOはユーザーの満足度に基づいて決めることが重要です。もし、クラウドインフラのSLAがあるべきSLOの基準を超えていなかった場合、インフラ構成そのものを変えるという選択肢も検討すべきでしょう。SLAの水準が高くなるようにインフラ構成を変えることで、ユーザーの満足度に基づいたSLOがインフラリソースのSLAの水準を超えないように調整できる可能性があるからです。 4.1.5 依存している社内の別プロダクトのサービス品質やSLOは一旦考慮しない 前述の通り、FAANSのバックエンドシステムはWEARなどの社内の別プロダクトのシステムに依存しています。そのため、FAANSのサービス品質は依存先の別プロダクトのサービス品質の影響を受けます。例えば、FAANSのあるWeb APIエンドポイントはAPIの内部処理で社内の別プロダクトのWeb APIに同期的に1回アクセスしているとします。この場合、このFAANSのAPIのレイテンシーの品質は社内の別プロダクトのAPIのレイテンシーの品質を上回ることはありません。そして、別プロダクトのAPIのレイテンシーの品質が悪化すれば、それに引きずられる形でFAANSのAPIのレイテンシーも悪化します。このような依存関係が存在しますが、私達は 依存先プロダクトのサービス品質やSLOを一旦は考慮せず、ユーザー満足度に基づいたSLOを定めて運用し始めてみる こととしました。そのように判断した理由を説明します。ユーザーが満足するか、しないかの基準でFAANSのあるAPIのレイテンシーSLOを考えたときに、例えば『99パーセンタイル値で500ミリ秒以内』という目標値が妥当だという結論に至ったとします。一方で、このAPIの内部処理で同期的に1回アクセスするWEARのAPIのレイテンシー品質が『99パーセンタイル値で700ミリ秒』という実測でした。つまり、依存するWEARのサービス品質の実測に対してこのSLOは無理があるということになります。とはいえ、必ずしもこのSLOを達成まで持っていけないとは限りません。私達が管理していない別プロダクトであっても、同じ会社のその別プロダクトの開発チームに品質改善の相談や依頼をするというアクションはとれます。そして、その際にSLOは他チームに品質改善の必要性を理解をしてもらう上で説得力のある論拠となり得ます。まずは、そのようなアクションをとってみることが重要だと考えました。FAANSのSLOを踏まえて別プロダクトのサービス品質の改善やSLOの見直しを行うといった結果につながるかもしれません。もちろん、このやり方で全てが滞りなく解決されるとは限らず、難しい場面もあるかもしれませんが、その場合はチーム間で協議して現実的な落とし所を探っていけばよいと考えました。ただし、この時 私達が定めたSLOは絶対的に正しい閾値ではない 点には注意する必要があるでしょう。SLOは初期フェーズで適切な閾値を設定することは難しく、運用の中で適切な閾値となるようにイテレーティブに改善していくものです。また、いくら閾値を改善しても、絶対的に正しい閾値には到達し得ないものでもあります。私達はこのような前提を理解した上で、FAANSのSLOを絶対的な論拠とはせず、他チームと協調的に話を進めるべきだと考えています。 4.2 ステークホルダーとの合意 前述の選定方針を踏まえて選定した最初のSLI/SLOを実際に設定して運用を開始するには、関係者との合意が必要です。まず、関係者全員にSLOとは何か、そのメリットや基本的な考え方を丁寧に説明することから始めました。特に非エンジニア向けにSLOの理解を深めてもらうために、山口能迪さんによる 「SLOをもっとカジュアルに活用しよう」 という記事を事前に読んでもらうことを意識しました。この記事は、SLOの要点を非エンジニアにもわかりやすく簡潔に説明しており、初めてSLOに触れる人にとって理解しやすく、導入への前向きな姿勢を促す素晴らしい内容のため重宝させていただいています。なお、関係者ごとにどのような点で合意を取るべきかを明確にすることも重要です。POやPMとは、SLIの選定とSLOの目標値が適切であるかを丁寧にすり合わせ、その基準を下回った場合にエンジニアリングリソースを優先的に修正や改善に投入する必要があることを理解してもらいました。SLOがビジネス価値やユーザー体験に直結することを納得してもらい、リソース配分の重要性を認識してもらうことが大事です。また、エンジニアリングチームとは、エラーバジェットが尽きた際にサービスの安定性回復を優先し、機能開発を一時停止することに合意します。これにより、システムの信頼性がビジネスやユーザー体験にどれほど重要かを共有し、リソース投入のタイミングについて共通認識を持つことができます。 5 「SLO定例」による信頼性チェック 「3 最初のゴール設定」にも設定したように、私達はSLOの運用開始に伴い、SLOの達成状況や運用上の課題を定期的に確認し、必要な改善アクションを迅速に実行するための体制を整える必要があります。そこで、バックエンドシステムの開発・運用に関わるバックエンドブロックとSREブロックの2チームによる SLO定例 という定例会議を立ち上げました。SLO定例は、 FAANSに必要な信頼性を維持すること を目的としており、SLO運用に関連する認識合わせや議論だけでなく、信頼性維持に寄与するその他の情報共有や意思決定も行う場です。ただし、後述の「6.3 フルサイクルエンジニアリングチームへの体制移行」により、SREブロックはFAANSの業務から離れたため、現在はバックエンドブロックのみでこの定例会議を行っています。そのような経緯や試行錯誤を経た最新の会議形式を説明します。 会議の流れは以下のようになっています。 前回のNext Actionの対応状況の確認 直近のリリース内容の確認 直近のアプリケーションエラーやシステム障害の確認 SLOの達成状況の確認 データベースのシステムメトリクスの確認 Next Actionの整理 続いて、これらを個別に説明します。 5.1 直近のリリース内容の確認 GitHubで管理されている前回のSLO定例以降にリリースされたプルリクエスト一覧をチームで確認し、信頼性に影響を与える可能性があるリリースを特定します。信頼性に関わるリリースとは、信頼性向上を目的とした修正だけでなく、大規模な新機能のリリースなど、システム全体のパフォーマンスや可用性に影響を及ぼす可能性のある変更も含まれます。この確認を最初に行うことで、リリースがシステムに与えた影響を理解しやすくなり、後続のログやメトリクスの分析がより効果的なものとなります。 5.2 直近のアプリケーションエラーやシステム障害の確認 私達は、アプリケーションエラーを監視するツールとしてSentryを利用しており、この場では前回のSLO定例から現在までに発生したアプリケーションエラーの一覧をSentryで確認します。エラーが発生すると即座に通知され、迅速にエラー内容の把握と必要なアクションをとる体制を築いています。そのため、この会議の場では原因の深掘りや解決策の策定は行わず、どのエラーがどれだけの頻度で発生したかとそれぞれの対応状況を俯瞰的に確認し、全員で状況を共有するのみに留めています。また、稀に発生する中規模以上のシステム障害に関しては、その発生時期や進行中の再発防止策の状況も確認します。これにより、全員が現状をしっかりと把握し、迅速な対応や適切な判断ができる体制を整えています。さらに、この確認をSLOの達成状況の確認前に行うことで、SLOの改善や悪化時の要因分析がしやすくなります。 5.3 SLOの達成状況の確認 私達は、システム監視ツールとしてDatadogを利用しており、各種システムメトリクスがDatadogで確認できるようになっています。そのため、SLOの達成状況はDatadogのダッシュボードを活用してリアルタイムに監視できるようにしました。このダッシュボードには、例えばAPIごとのエラーレートやレイテンシーが一目で確認できるウィジェットなども配置されており、SLOに変化が生じた際にはその原因を特定しやすい設計となっています。なお、このダッシュボードは 「WEARにおけるSLOを用いた信頼性改善の取り組み」 で紹介されているDatadogのダッシュボードを参考に作っています。エラーバジェットの枯渇状況を確認し、原因を特定して適切な対応策を議論することで、SLOのプロセスが回るようになっています。 5.4 データベースのシステムメトリクスの確認 私達が利用しているCloud SQL(PostgreSQL)の監視として、アラート監視と定期的なメトリクス監視の二重体制を構築しています。まず、アラート監視に関して、データベースのログやメトリクスを対象とした適切なアラート設定を入れて、問題を迅速に検知して対応できるようにすることはデータベースの監視において重要です。また、それとは別で、状況の変化を時系列のメトリクスで管理しキャパシティプランニングや障害の予兆の把握に役立てることを目的とした、定期的なメトリクスの確認も重要です。このようなメトリクス監視もこの会議で行っています。なお、このメトリクス監視で見るべきメトリクスの一覧もダッシュボード化して、容易に確認できるようにしています。 6 SLO運用開始後の工夫と取り組み SLOの運用開始後、運用の過程でさまざまな工夫や取り組みを行ってきました。本章ではその一部をご紹介します。 6.1 SLI/SLOの設定変更の記録をADR形式で残す「SLO-DR」 SLI/SLOは最初に定義した設定を一切変えずに恒久的に使い続けるわけではなく、その設定自体もイテレーティブに見直すものです。例えば、既に運用しているSLOの設定をより適切な閾値となるように調整したり、運用していく中で見出したより適切なSLIに差し替えたり、新機能のリリースに伴い新たなSLI/SLOを盛り込むことがあります。FAANSではSLI/SLOの設定を変更する際にADRの形式で設定変更の意思決定の内容とそのコンテキストをドキュメントに残す規約とし、そのドキュメントを私達は SLO-DR と名付けました。 ADR *5 とは、Architecture Decision Recordの略で、ソフトウェアアーキテクチャに関する意思決定とそのコンテキストを残すドキュメント手法の一種です。FAANSの開発チームでは以前からADRによるドキュメント文化が浸透しており、ADRの形式は慣れ親しんだものでした。また、ADRは個々の意思決定を独立したドキュメントとして作成しますが、一般的には一度作成したドキュメントを廃止することはあっても、内容を更新することはない追記型のスタイルです。この追記型のスタイルはSLOの設定に関する意思決定がどのような変遷を辿ったかというコンテキストが追いやすく、今回の要件に適していました。 例えば、新規で構築したAという名前のWeb APIサーバーがあり、Aの可用性SLOの設定に関して、ある期間に時系列順で以下のタイトルで3つのSLO-DRのドキュメントが作成されたとします。 2024/08/01 新規構築したAのWeb APIサーバー全体の可用性SLOを30日間のローリングウィンドウで『99.9%以上』の稼働率として定義した 2024/10/01 Aの全体の可用性SLOを30日間のローリングウィンドウで『99.9%以上』→『99.5%以上』の稼働率に変更した 2024/12/01 Aの全体の可用性SLOを30日間のローリングウィンドウで『99.5%以上』→『99.8%以上』の稼働率に変更した 最初と最新の状態だけを比べれば『99.9%以上』から『99.8%以上』に目標値を下げたという変化としてまとめられますが、実際にはその過程で一度『99.5%以上』という設定を経由しています。一度『99.5以上』に下げてその後『99.8%以上』に上げたという個々の意思決定とそのコンテキスト、そしてそれらの時系列の変遷という情報には将来の意思決定の際に有用な情報が隠れていることがあります。この例では比較的単純な意思決定の流れを示していますが、実際のより複雑なケースでは、個々の意思決定とそのコンテキストがさらに重要な要素となります。SLO-DRでは、そのような情報が喪失せず、かつ追いやすい形式になっています。一方で、SLI/SLOの設定は頻繁に見直されることがあるわけではないことから、追記型がゆえのドキュメント数の増大に伴う認知負荷に関しては長期的な視点に立っても懸念がないと判断しました。また、最新のSLI/SLO設定の全体像は前述のダッシュボードでも容易に把握できます。私達は、SLI/SLO設定や運用開始日、エラーバジェットポリシーなどの決定内容と、そのコンテキストであるCUJや関係者との協議と合意の記録をまとめたドキュメントとして作成し一覧化して管理しています。 6.2 チームで行う継続的学習「SLO Study」 私達のチームでは、SLOに関する知識を共有し、運用の知見を深めるために SLO Study と名付けた継続的な学習の場を設けています。FAANSにはSLOの運用経験があるメンバーは少なく、SLOに対する関心や知識レベルにもばらつきがあります。そこで、全員がSLOに関する共通の知識を身に付け、チーム全体でSLOの効果的な運用を考えられる体制を築くことを目指して、この取り組みを開始しました。具体的には、事前にSLO Study用のドキュメントに記載の表にSLOに関するWeb上の記事のURLとそこから得た学びや議論したいことを記入しておきます。そして、SLO定例の最後の余った時間を使って記入者がその内容を発表し、定例の参加者で議論します。 SLO運用のあるべき姿は、事業やシステム、さらには組織の特性によって異なるため、一般化された知識だけではすべての現場に適用するのは難しいと感じています。よって、各々の開発組織が自分たちに合ったSLOのあり方を見つけ出すために、独自の試行錯誤を積み重ねていく必要があるでしょう。Google社がSLOのプラクティスを提唱して以来、多くの企業やサービスでSLOの取り組みが行われ、試行錯誤を経てきました。それぞれの現場で得られた実践的な知見を取り入れることは、私達にとって最適な運用方法を見つける上で非常に参考になります。SLO Studyの場では、SLO関連の資料や実際の運用事例をキャッチアップし、学んだことを「FAANSに活かせるか」「FAANSの場合どう適用するか」という視点で議論しています。このような負担の小さく無理のない継続的学習の取り組みを通じて、SLOに対する共通理解を深め、チーム全体での一体感を持ってSLO運用の改善を進めることが目標です。特に、書籍に記載されている一般的な知識だけでなく、各社の具体的な事例を学ぶことで、私達の現場に合った最適なSLOのあり方を模索していきたいと考えています。 6.3 フルサイクルエンジニアリングチームへの体制移行 SLOの運用を進める中で、SREブロックにアサインされていたFAANSの信頼性維持に関するタスクが思うように進まないという課題が浮上しました。この課題に対し、チームトポロジーの考え方を用いることで、状況を客観的に整理できました。 チームトポロジーとは、書籍 「チームトポロジー 価値あるソフトウェアをすばやく届ける適応型組織設計」 で紹介されている、組織のチーム設計において適応型のフレームワークを提供するモデルです。特にチームが担う認知負荷を最適化することを重視しています。この認知負荷が過剰になると、チームのパフォーマンスが低下し、システムの安定性に影響を及ぼすことが指摘されています。 FAANSとWEARは、それぞれ異なるビジネスドメインに属しており、どちらも複雑な要件を持っています。FAANSは新しいサービスながらも急速に成長しており、独自のビジネスニーズと技術的な要件が求められます。一方、WEARは長い歴史を持つ大規模なシステムであり、その運用には深い知識が必要です。これら2つの異なるビジネスドメインに対応することは、SREブロックに大きな認知負荷を強いる要因となっており、これは構造的な課題でもありました。さらに、FAANSとWEARは歴史的な経緯で両者の技術スタックの統一性が低いという事情もあります。そのため、SREブロックは異なる技術基盤の両システムに対応する必要があり、さらに認知負荷が増大していました。現に、SREブロックからは「WEARに手一杯で、FAANSに十分なアテンションを張れない」という声が上がっていました。このアテンションという言葉は、チームが持つ認知負荷に関連する概念で、適切な注意力を割けないことが認知的な過負荷の表れです。このような状況から明らかなのは、問題の本質が単なるリソース不足ではなく、過剰な認知負荷にあるということです。たとえSREブロックの人員を増やしても、認知負荷が軽減されない限り、この問題は解決しません。そこで、私達は チーム間の責任境界の見直し を行い、バックエンドブロックの責任範囲を広げてSREブロックの負担を軽減することによって、両者のフロー効率を改善する道を選びました。 歴史の浅いFAANSのバックエンドシステムは、WEARに比べて小規模なシステムで運用負荷が低いため、バックエンドブロックがSREブロックの責務を引き継いでも認知負荷的に無理はありません。さらに、バックエンドブロックはこれまでもインフラ構成や監視設定の変更、インフラ起因の問題解決に積極的に取り組んできた経験があります。また、バックエンドブロックにはSREブロックが担当していた運用業務の知識やスキルを持つメンバーが複数名いることもあり、体制移行は円滑に進行しました。この結果、現在ではSREブロックがFAANSの運用から離れ、バックエンドシステムの開発と運用はバックエンドブロックで自己完結する体制に移行しました。 バックエンドブロックにとっては責務が拡大したことでやるべき業務が増える一方で、大きなメリットもあります。運用を含む開発ライフサイクル全体を一貫して管理できるフルサイクルエンジニアリングチーム *6 として機能し始め、分業体制でのコミュニケーションコストやサイロ化の問題が解消されます。その結果として、開発ライクサイクル全体へのフィードバックループがより効率的に回りやすくなるのです。ただし、開発ライフサイクルの中でQAに関しては、高い専門性を有したQAエンジニアが複数名おり開発ライフサイクルのボトルネックにはなっていないため、引き続き外部化されたままの体制を維持しています。 6.4 開発組織への定期報告 SLOは、開発組織の全体に共有されるべき重要な指標です。バックエンドシステムを中心にSLO運用を開始していますが、SLOの設定や達成状況、そして取り組んでいる改善アクションは、プロダクト開発・運用に携わる全ての関係者によって把握されている状態が理想です。そこで、私達はFAANSのプロダクト開発のための情報共有や相談の場である週次の定例会議で、SLO運用に関する報告をするようにしました。この場でSLOの状況を定期的に共有することで、機能拡充や新機能開発に偏りがちな意識を、信頼性という側面にも広げ、バランスの取れた価値提供を意識し続けることができると考えています。これは、私達が目指す組織全体へのSLO文化の浸透における重要なステップの1つだと捉えています。特に、信頼性維持に関するタスクを行う際に、そのタスクが今なぜ必要で、どのように全体の価値提供に寄与するのかを組織全体で理解することが重要だと考えています。SLOの定期報告を通じて、信頼性維持の取り組みが単なる技術的メンテナンスにとどまらず、ユーザーやビジネスにとって直結した価値提供の一環であることが明確になります。結果として、組織全体で信頼性に対する意識が強化され、持続的な改善活動につながっていくと考えています。 限られた時間の中で効率的にSLOの情報を共有するため、報告のフォーマットにも工夫を凝らしました。この定例会議は、プロダクト開発に関わる全てのメンバーが集まる唯一の場です。エンジニアだけでなくプロジェクトマネージャーやデザイナー、ビジネスサイドのメンバーも参加しています。そのため、技術的な背景を持たないメンバーにも理解できるシンプルかつ視覚的に分かりやすい形式が求められます。SLOの詳細な分析やアクションの議論は、専用のSLO定例で行うため、定例会議では現状のSLOの達成状況と必要なアクションを簡潔に示すことに集中しています。各SLOの達成状況は 『達成』(=エラーバジェットに余裕がある状態) または 『未達』(=エラーバジェットが枯渇している状態) として色分けし、一目で判断できるようにしました。また、前回の報告からの変化は赤色の文字で記載し、未達状態のSLOに関するアクションプランも簡潔に記載することで、状況の変化や必要な対応を迅速に把握できるようにしています。 具体的には以下の項目を含めるようにしました。 測定日 : 各SLOの測定時点を明記する。測定日はSLO定例の開催日に対応。 SLOの設定内容とその達成状況 : SLOの設定内容と各SLOの『達成』または『未達』という達成状況を色分けして記載。 前回測定時との比較 : 前回測定時の達成状況も併記することで改善や悪化といった変化が読み取れるようにする。 品質改善アクション :『未達』状態のSLOに関しては、『達成』に持っていくためのアクションプランを記載する。 補足情報 : システムコンポーネントやSLO関連の用語の解説を添えることで、背景を理解しやすいようにする。 一方で、具体的なSLIの数値や過去の傾向分析といった細かなデータは報告には敢えて含めていません。それらは前述のSLOダッシュボードで確認でき、SLO定例で議論されるため、開発定例では最小限の情報に絞り込み、報告のスムーズな進行を心掛けています。 具体的には以下のようなフォーマットの資料で報告しています。 7 SLOを導入して得られた効果 SLOの導入によって、私達の開発・運用プロセスにおける意思決定が大きく改善されました。 7.1 明確な判断基準による会議の充実化 SLO導入以前も、バックエンドシステムの運用に関わるエンジニアが集う定例会議の中で、APIのレイテンシーやエラーレートなどのメトリクスを確認してはいました。しかし、それらがどの程度問題であるのか、具体的にどのようなアクションが必要なのか判断することが難しく、会議の進行が散漫になることがありました。メトリクスは収集できていても、問題の有無やアクションの必要性が不明確だったため、何となく会議を終えてしまうケースも少なくありませんでした。 SLO導入後は、その定例会議が前述のSLO定例として生まれ変わりました。各サービスにおいて設定されたSLOに基づいて判断できるようになり、会議でのディスカッションがより具体的かつ建設的なものとなりました。これにより、サービス品質に関する合意形成がスムーズに進み、どのタイミングでアクションを取るべきかが明確になりました。結果として、会議が締まりのあるものに変わり、効果的な意思決定を行う場としての役割を果たせるようになっています。 7.2 信頼性維持のためのアクション促進 SLOを満たせておらずエラーバジェットが尽きた場合には、即座に対策を講じるための具体的な行動に移る習慣が根付いてきました。もちろん、全ての問題が一度に解決されるわけではありませんが、少なくともSLOを基準に優先順位をつけ、後回しにされがちなサービス品質の課題に対しても確実に対応が取られるようになりました。これにより、従来は見過ごされていたような品質問題に対しても、早期に改善のためのアクションが取られるようになり、ユーザー体験の向上につながっています。例えば、APIのエラーレートがSLOを下回ることがあれば、その原因を特定し、必要な修正や最適化を実施する体制が整っています。以前であれば、「重大な問題でなければ後回し」という姿勢が取られていた場面でも、SLOに照らし合わせることで緊急性が明確となり、速やかに改善に取り組むことが可能になりました。 7.3 他チームへの改善依頼の円滑化 SLOの導入により、私達が依存している社内の他プロダクトの開発チームに対して、品質改善を依頼する際の根拠も強化されました。以前は、依存先の他プロダクトのAPIのレイテンシーがなんとなく遅いとは感じつつもどこまで改善すべきかも不明確で、他チームに働きかける具体的なアクションへ繋がりにくい状態でした。しかし、SLOによってその判断基準が明確になったことで、そのようなアクションに繋がりやすくなりました。また、「SLOを満たしていない」という明確な基準を提示できるため、依頼内容が具体的かつ論拠のあるものになり、依頼先にとっても協力してもらいやすいものとなりました。この結果、プロダクト間での協力体制も強化され、全体のサービス品質向上に寄与しています。 7.4 技術的な意思決定の改善 SLOの導入により、技術的な意思決定の際に信頼性への意識が高まりました。システムアーキテクチャの設計やパフォーマンス最適化には、トレードオフを踏まえた判断が求められます。SLOという信頼性基準が明確に定義されたことで、どの程度の信頼性が必要かという具体的な指標が提供され、意思決定の根拠がより明確になりました。例えば、レイテンシーに関するSLOが設定されていることで、負荷テスト実施の際にも目標とすべき性能値の意思決定がスムーズになり、試験結果に基づく判断がより合理的に行えるようになりました。また、SLOが信頼性の指標として組み込まれたことで、アーキテクチャ設計の際にも信頼性を意識した選択がより自然に行われるようになりました。 8 SLOの導入は早ければ早いほどよい SLOを運用していく中で感じたこととして、本記事のタイトルにもなっている SLOの導入は早ければ早いほどよい ということがあります。もちろん、早ければ何でもよいわけではなく、導入のスピードを重視するあまり重要な要素を見落としては本末転倒です。しかし、早いタイミングでSLOを導入することには多くの利点があると感じました。 8.1 SLO文化を浸透させるハードルが低い SLOを効果的に運用するためには、開発組織にその文化を浸透させることが不可欠です。一般的に、組織が大きくなるほど新しい文化を浸透させるのは難しくなります。開発組織が拡大するにつれて、SLO文化の浸透には労力と時間が必要となるでしょう。FAANSは、まだ成長段階にあり、開発組織も比較的小さいです。その点において、SLOの概念を関係者に伝える際の障壁が低く、皆で同じ目標に向かいやすい環境でした。これは信頼性への意識向上とSLOに基づいた意思決定が迅速かつ効果的に行えるようになった一因だと考えています。 8.2 SLOはソフトウェア設計の意思決定を支援する 前述の「7.4 技術的な意思決定の改善」からも明らかなように、サービス品質の基準とはソフトウェア開発の非機能要件の一部であり、ソフトウェア設計において重要な役割を果たします。SLOが早期に設定されていることで、開発プロセスの初期段階から信頼性の基準を考慮した意思決定が促進されます。後から信頼性を確保するよりも、最初からその基準を意識した設計をする方が遥かに効率的です。また、設計や実装のトレードオフを明確に理解し、最適なバランスを追求できるようになります。そして、システム全体の安定性が向上し、開発チームはより自信を持ってプロダクトの成長に貢献できるようになると考えています。 8.3 信頼性は事業にとって最初から重要な指標である そもそもユーザーの信頼性とは事業にとって初期段階から重要な指標です。サービスがローンチされ、ユーザーが実際に利用し始めた瞬間から、信頼性は欠かせない要素となります。ユーザー体験の質を維持するためには、信頼性を継続的に追跡し、改善し続けることが不可欠です。これを怠り、後回しにすることは、事業にとって大きなリスクとなります。 おわりに 本記事では、FAANSにおけるSLOの導入事例と、それによって得られた効果や運用の中での工夫と気付きを紹介しました。SLOの導入は、サービス品質を継続的に改善し、信頼性を維持するための重要なステップでした。導入初期の段階では、SLOに基づくフィードバックループを確立し、運用を通じて改善を重ねることで、一定の成果を達成できたと感じています。 今後の目標は、現在のSLO運用をさらに洗練させ、より効果的なフィードバックループを回し続けることです。その一例として、バーンレートアラートを導入し、エラーバジェットの消費速度をリアルタイムで把握し問題を早期に検知・対応する体制を整備することを検討しています。このような取り組みを通じて、信頼性に対する迅速なアクションをさらに強化し、SLOの達成をより確実にすることを目指します。また、このフェーズで得られた成功体験を基にSLOを定めるシステムの範囲を拡大し、組織全体でSLO文化を深く醸成していきたいと考えています。 本記事が読者の皆さんのSLO導入のきっかけや、導入手順と運用方法の参考となれば幸いです。また、FAANSでは、機能開発と信頼性維持の両方にコミットし、フルサイクルな開発プロセスを実現できるエンジニアを募集しています。私達と共に、信頼性を高めながらプロダクトの価値を最大化していく挑戦をしませんか。 corp.zozo.com *1 : 関連記事 FAANSにおけるCloud RunからGKE Autopilotへのリプレイス事例 *2 : 関連記事 Kubernetesネイティブなワークフローエンジンとは!FAANSでArgo Workflowsを導入した話 *3 : 関連記事 Cloud FirestoreからPostgreSQLへ移行したお話 *4 : なお、このSLAは私達のSLO導入時点におけるものですが、本記事の執筆時点においてもそのSLAに変わりはありません。 Google Cloudの公式ドキュメント *5 : 関連記事 ZOZOFITにおけるADRを利用した意思決定を残す文化作り *6 : 関連記事 Full Cycle Developers at Netflix — Operate What You Build
アバター
はじめに こんにちは、SRE部カート決済SREブロックの伊藤( @_itito_ )です。普段はZOZOTOWNのカート決済機能のリプレイス・運用・保守に携わっています。また、DB領域でのテックリードを担っており、データベース(以下DB)周りの運用・保守・構築に関わっています。 現在、ZOZOには DBを専門で扱う部署はありません 。一部メンバーでDatabase Reliability Engineeringのワーキンググループ(以下DBRE-WG)を構成して、DBの信頼性を高めるための活動をしています。 本記事ではZOZOにおけるDBRE-WGの概要と発生していた課題と、いかにして開発メンバーを招き、その結果どのような効果があったかの事例をご紹介します。 はじめに ZOZOにおけるDBRE活動について SRE部で行っているワーキンググループについて DBRE-WGで行っていること DB関連での問い合わせ・レビュー対応 各種案件におけるDB周りのサポート イベント時の監視対応 DB周りでの自動化やパフォーマンス可視化 技術共有会の実施 DBRE活動を進める上で発生した課題 カート決済SREとしてDBと関わることとの違い 課題解決に向けた取り組み 開発チームへの提案 DBAとDBREについて 開発チームとの話し合い メンバー参加後の活動 DBRE-WGに参加してもらった結果 問題となっていたDBのパフォーマンス改善 参加メンバーの自主的な動きによる効果 振り返ってみると 展望 DBのパフォーマンス改善 体制について 最後に ZOZOにおけるDBRE活動について SRE部で行っているワーキンググループについて 現在私が所属しているEC基盤開発本部SRE部は複数のブロック(チーム)で構成されています。このSRE部内の、ブロックを跨いだ活動を促進するための横断組織として複数のWGが存在しており、この中の1つとしてDBRE-WGが存在します。 SRE部内のワーキンググループ構成例 DBRE-WGで行っていること DBRE-WGは「各プロダクトのデータベース健全性を維持向上させる」ことを目的として、以下のような取り組みを実施しています。 DB関連での問い合わせ・レビュー対応 DBの設計や運用に関する質問や、パフォーマンス悪化や予期せぬエラーの発生といった緊急性の高い問い合わせなど、様々な問い合わせに対応しています。作成予定のテーブルの設計が問題ないか、ZOZO内でのDBの開発ガイドラインに沿っているかなどのレビューも行っています。 各種案件におけるDB周りのサポート DB設計が重要な役割を担う場合や、保守期限が迫っていてDB更改をする必要がある場合など状況に応じてDBREが参画します。参考までに、過去にDBREメンバーが主導してリプレイスを実施した記事は以下のとおりです。 techblog.zozo.com イベント時の監視対応 ZOZOTOWNでは1年を通じて様々なセールイベントを開催しており、大きな負荷が想定されるイベントではリアルタイムで監視しています。特に最大級のアクセスが発生する新春セールには最も力を入れており、当日も監視しますが、それに備えた負荷試験においてのDB負荷状況の確認も行っています。 techblog.zozo.com DB周りでの自動化やパフォーマンス可視化 手動運用していた作業の自動化やパフォーマンスの可視化なども進めています。可視化については具体的には以下の記事で紹介しています。 techblog.zozo.com 技術共有会の実施 不定期ではありますが、全社的な技術力向上を目的としてDBに関わる技術共有会を開催しています。 DBRE活動を進める上で発生した課題 上記の活動を進める上でいくつかの課題が生じてきました。その状況を図示したのが次の画像です。 DBRE活動における課題 特に、特定のDBにおいて、パフォーマンスの悪化やトラブル・問い合わせが増加傾向にあり、改善したい課題でした。 クエリタイムアウト起因のエラー数が増加傾向 しかし専任というわけではないので大きく時間を割くことが難しく、問題の原因となるクエリを見つけたとしても、そのサービスに詳しくないため改善のための手順が増えてしまい、改善を進めづらい状況でした。 カート決済SREとしてDBと関わることとの違い 冒頭でも述べた通り、私はカート決済SREとしても活動しています。いわゆるEmbedded SREのような立場であり、開発側を担当しているカート決済部と連携を取りながらDBの運用・保守をしています。 カート決済SREとして活動しているときは前述のような課題を感じず、振り返ってみると立ち位置の違いが大きいように感じました。 カート決済SRE兼DBREとしての立ち位置 問題が起きているDBに対しても、同じような立場で動ける人を増やすことで効率良く改善が進むと考え、そのためのアクションを取ることにしました。 課題解決に向けた取り組み 開発チームへの提案 上記から、パフォーマンス問題が発生しているDBに関わる 開発チームのメンバーから数人、DBRE-WGの活動に参加してもらえないか 提案することを決めました。 SRE側から増やすことも考えましたが、SQLを自在に書ける人材が少なく、1から学習する必要があるため、あまり現実的ではないと判断しました。(組織変更などもあり、2024年11月現在は問題となったDBに大きく関わるDBREメンバーも存在) また、開発側から参画してもらうことで初期学習コストが低い以外にも、下記のようなメリットが考えられました。 開発側の方がサービスの実装に詳しく、直近の問題にも対応できること 開発側でノウハウが蓄積されれば根本から解決できる可能性があること DBREと開発側との連携不足の解消 DBRE-WGの活動へと参加してもらう理由については、双方の連携を向上させるとともに、実際に発生している問題を題材にペアプロすることでスキルの伝達を速やかにできると考えたためです。 DBAとDBREについて 開発側に打診するにあたって、DBAの役割を担える人材を増やしたいという目的を話しています。ここでDBAとDBREの違いについて明確に整理するために、ChatGPTからの回答を引用します。 DBA(Database Administrator) ・役割: データベースの管理、設定、保守を行う。 ・主な業務: データベースのインストール、バックアップ、リカバリ、パフォーマンスのチューニング、セキュリティの管理など。 ・目的: データベースが正常に機能し続けることを保証する。 DBRE(Database Reliability Engineering) ・役割: データベースの信頼性、可用性、スケーラビリティを向上させることに焦点を当てる。 ・主な業務: システム全体のアーキテクチャを考慮し、障害時の復旧手法の設計や、自動化を推進することなど。 ・目的: 高可用性のシステムを実現し、ビジネスニーズに応じたパフォーマンスを維持する。 今回大きく問題となっていたのはパフォーマンス面であり、SRE的な部分までお願いする予定はないため、DBREではなくDBAを増やすことを目的としています。また、元々DBRE-WGはSRE部の中の1つの活動であり、「DBREに参加 ≒ SRE側への引き抜き」のような誤った形で捉えられないようにする意味でもこの辺りを明示しています。 開発チームとの話し合い 実際の話し合いには以下のような内容をまとめた資料を用いてプレゼンを実施しました。 DBRE-WGとは 現状のDBREにおける課題感 開発側メンバーを誘いたい理由 DBのパフォーマンス状況 悪化傾向にあり、エラー数も増加傾向にあることを示すグラフの提示 期待する人物像 懸念点 メンバーjoin時の双方の工数が具体的にどのくらい増えるか DBRE業務をどこまでお願いできるか 結果として、DBに関して同じ課題感を抱えていると認識し合い、ぜひ協力したいという意見をいただきました。 メンバー参加後の活動 上記を経て2名の開発メンバーに参加してもらうことになりました。 参加に伴って問い合わせやテーブル設計のレビューなどをペアプロ形式で行うとともに、DBのパフォーマンス改善に向けた定例会を設けました。 定例会では、以下の例のように最近のDBの状況を確認し、ボトルネックを特定したうえで、それに対する改善策を検討・実施しました。 エラー状況確認からのアクション例 DBRE-WGに参加してもらった結果 問題となっていたDBのパフォーマンス改善 改善活動を行ったことでクエリタイムアウト起因のエラー数が減少し、それに伴うレスポンスタイムの改善を確認できました。 クエリタイムアウト数の減少とレスポンスタイムの改善 参加メンバーの自主的な動きによる効果 上記以外にも、参加してもらったメンバーによる効果が以下のようにありました。 エラー発生時の初動対応の迅速化 背景:エラーが多数発生するとSlack通知が行われ、原因を調査するためのスレッドが立ち上がる 以前まで:調査が行われ、DBが関係していそうならばDBREに問い合わせが行われる メンバー参加後:参加メンバーがエラー発生時から積極的に見てくれるため、早期段階でDBの関係有無の周知とDB側調査が開始される 既存のアプリ改修による運用改善 背景:処理が失敗してしまうとDB全体の動きに影響を与えてしまうバッチアプリが存在する 以前まで:成否が重要なものの、既存アプリだとわかりづらく、運用でのカバーとなっていた メンバー参加後:アプリ自体の改修を実施し、万が一失敗した場合には、重要なアラートが通知されるSlackチャンネルに通知が届くようになった 振り返ってみると 書籍、 データベースリライアビリティエンジニアリング において、DBREの基本原則として以下の記述があります。 1.1.2 周囲を巻き込め 才能あるDBREは、サイトリライアビリティエンジニア(SRE)よりもはるかに希少な存在です。DBREを雇えたとしても、せいぜい1人か2人が関の山でしょう。これはつまり、多くのことを自分たちで、つまり限りあるリソースを最大限に活用して、自給自足で成し遂げる必要があるということです。 意図していたわけではありませんでしたが、開発チームも一緒になって改善を進めるというアプローチで、上記を実現できており、相乗効果を生んでいるように感じています。 展望 DBのパフォーマンス改善 前述のように改善効果があったものの、まだまだ改善すべき点は多くあります。そのため、引き続き改善に向けた活動を継続していきます。 体制について 理想としては次の画像のような体制が望ましいと考えています。 各サービスと近い位置にDBAを担える人がいる体制 サービスの実装に精通したDBAがいることで、問題が発生しても迅速に対応できるほか、有識者によるDB設計やクエリのレビューが行われることで、サービスの品質向上が期待できると考えています。 また、弊社はマイクロサービス化を進めており、それぞれのチームでクラウド管理しているDBの数も増えています。各チームのノウハウや、Aurora MySQLのアップグレードのような定期的な作業なども、個別に調査するのではなく、情報を共有し横展開することで全体の効率化が進むと考えています。 このような体制を目指し、これからもDBの信頼性を高めるための活動を続けていきます。 最後に ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
はじめに 2024年10月15日に『 ZOZOMETRY 』という計測技術を活用したサービスを正式ローンチしました。今回はZOZOMETRYのサービス概要、計測技術および計測精度について紹介します。 ZOZOMETRYとは ZOZOMETRYとは、事業者の採寸業務を効率化し、採寸が必要な服の売上拡大やコスト削減に貢献する法人向けのサービスです。以前、ZOZOTOWNで提供していた個人向けのサービスでは、ZOZOSUITを着用しての計測が必須でしたが、ZOZOMETRYではZOZOSUITあり、ZOZOSUITなしの異なる計測方法が提供されています。 biz.zozometry.com 計測技術について ZOZO New Zealandのノースと新事業推進本部の西澤です。ZOZOMETRYの計測技術を説明するにあたり、まずはZOZOSUITのローンチ以来、ZOZOの計測サービスがどのように進化してきたかを解説します。 計測サービスの進化 ZOZOSUITは当初、ZOZOプライベートブランドのサイズ推奨用技術として2018年にローンチしました。2020年にはデザインとアルゴリズムをアップグレードしたZOZOSUIT2(現在はZOZOSUITと名称変更)として、パートナー企業募集を実施しました。ZOZOSUITに続き、2019年にはネットでシューズ購入時のサイズ課題を解消させるZOZOMATをローンチしました。その後、ZOZOSUITで使用されている計測技術と、ZOZOMATで足形状の特定に使われるシルエットフィッティング技術の延長として開発されたのが、「ZOZOSUITなし計測」と呼ばれる最新の計測技術となっています。 ZOZOSUITを使った計測は幅広い分野で役立つことが証明され、医療、アパレル、フィットネス、ゲームなど、様々な業界におけるZOZOSUITの応用に関して、多くのクライアントと関わってきました。 また、外部の研究機関との取り組みにより、 脊柱側弯症の検出 や リンパ浮腫の評価 など、今までになかったZOZOSUIT関連プロジェクトの出発点となりました。そのような様々な業界との取り組みの中で、多くの事業者が単に顧客の体をスキャンするだけにとどまらない多様なニーズを持っていることが明らかになりました。 この課題に対して、ZOZOMETRYというBtoBプラットフォームを通じて、事業者が独自の測定ポイントを定義できるようにしました。これにより、業界ごとの専門知識に支えられながら、ビジネスニーズに合わせてカスタマイズできる測定技術を体験することが可能となります。 ZOZOMETRYの計測技術の概要 ZOZOMETRYでは、用途に合わせて「ZOZOSUIT着用あり」または「ZOZOSUIT着用なし」の2つの測定モードを専用アプリで選択できます。 ZOZOSUITは12種類のサイズ展開があり、約2万個の特徴的なフィデューシャルマーカーを使用して体型の3Dモデルを生成します。特殊なアルゴリズムによって高精度な身体測定が可能です。 ZOZOSUITを用いた計測は高精度であるものの、計測方法の手間とZOZOSUIT自体の配送までの待ち時間が課題点となっていました。そのため、ハンズフリーの計測技術である「ZOZOSUITSなし計測」を開発しました。2年間に及んだ開発期間中には、最大450件のZOZOSUITデータと、近年蓄積されたZOZOSUITなし計測のプロトタイプからの追加データ200件の分析が行われました。 ZOZOSUITなし計測を実現させる上で特徴的な問題となったのは、身体上のフィデューシャルマーカーを検出できるZOZOSUITとは異なり、身体を特定するための基準点がなかったことでした。解決策となったのは、ZOZOMATのシルエットフィッティング技術の応用でした。ZOZOSUITの計測技術を活用しながら、ZOZOSUITがない状態を補正します。 様々な背景や肌の色調を考慮できるシルエット検出機能を取り入れることで、ZOZOSUITなしの場合も体型の3Dモデルを構築することが可能となりました。もちろん、計測を成功させるには、iPhoneの前面カメラにアクセスできることと、(正確な測定を確実にするため)ぴったりとした服装を着用することが条件となっています。 ZOZOSUITなし計測の機能は2024年1月に初めてボディマネジメントサービス「ZOZOFIT」に統合されました(現在は米国でのみ利用可能)。以来、ZOZOFITは毎日約800件の計測データを処理しており、これまでに実施された計測データの総数は約5万件となっています。このサービスは、独自のビジネスニーズに合わせて139箇所から測定ポイントを定義できます。今後はZOZOMETRYの高精度なZOZOSUIT計測、または手軽なZOZOSUITなし計測を通して、カスタマイズ可能な身体測定サービスをぜひ世の中の企業様にご体験いただければと考えています。 このサービスは、各企業の特定のニーズに応じて柔軟に対応可能です。ぜひ、これらのサービスを世の中の企業様にご体験いただければと考えています。 ZOZOMETRYの計測精度について 計測プラットフォーム開発本部データ開発ブロックの嶺村と松山です。このセクションではZOZOMETRYのZOZOSUITなし計測およびZOZOSUITあり計測の精度についてご説明します。スキャン後に生成される人体の3Dモデルの精度と、他社の類似サービスとの比較結果についてご紹介します。 3Dモデルの生成精度について 検証内容 3Dモデルの生成精度については、ベンチマークと比較した正確性の精度(accuracy)と、同じ計測対象を複数回計測した場合のばらつきの精度(precision)の検証観点があります。今回の記事では前者の観点での検証結果についてご紹介します。 検証方法としては、第三者メーカー製の3Dボディスキャナー(以下、3Dボディスキャナー)の3Dモデルをベンチマークとし、ZOZOSUITなし計測またはZOZOSUITあり計測で生成された3Dモデルとの比較誤差を算出しています。3Dモデル同士の比較誤差の算出方法は以下の通りです。 人間型のマネキンや人体など、共通の計測対象をZOZOSUITなし計測、ZOZOSUITあり計測および3Dボディスキャナーで計測し、3Dモデルを生成します。 3Dモデル同士を直接比較する前に、ZOZOSUITなし計測、ZOZOSUITあり計測および3Dボディスキャナーの各モデルを移動および回転させ位置合わせします。各計測ツールによって生成された3Dモデルで姿勢(背中の曲がり方や腕、脚の開き具合など)が微妙に異なる可能性があるため、事前にこの方法で3Dモデル同士の姿勢を可能な限り一致させます。 ZOZOSUITなし計測またはZOZOSUITあり計測で生成された3Dモデルの各頂点に対して、3Dボディスキャナーで生成された3Dモデルへの最短の垂直距離を計測します。両者の距離は正の値になる(頂点が外側に位置する場合)ことも、負の値になる(頂点が内側に位置する場合)こともあります。弊社の検証ではいずれのケースでも距離の絶対値を『誤差』として算出しています。この方法で3Dメッシュ全体の頂点の距離を計算し、その平均値を『平均誤差』として計算します。 上記の計算方法で計算したZOZOSUITあり計測およびZOZOSUITなし計測の平均誤差は以下の通りです。 ZOZOSUITなし計測:平均誤差10mm以内 ZOZOSUITあり計測:平均誤差3.7mm以内 スマートフォンアプリを活用したツールでありながら、ZOZOSUITなし計測、ZOZOSUITあり計測のいずれも3Dボディスキャナーの計測結果に近い精度で3Dモデルを生成できています 1 。また、ZOZOSUITあり計測ではより3Dボディスキャナーに近い高精度な3Dモデル生成が可能です。 他社サービスとの比較 検証の概要 ZOZOMETRYと類似した人体計測を提供している他社サービスとの精度比較の結果についてもご紹介します。他社サービスからは3Dモデルへのアクセスが難しかったため、先述の3D同士の比較とは異なり、手計測(以下、ハンドメジャーと呼称)との比較検証を実施しました。検証内容の詳細と検証結果について記載していきます。 ハンドメジャーについて 弊社には、外部の研究機関にて身体計測を専門とする研究者の先生から、ISO規格で定められた国際標準のハンドメジャーの計測方法を学んだ専任のチームが存在します。この専任チームは、プロダクトの精度検証のために定期的にハンドメジャーを実施しているプロフェッショナルチームです。各計測ツールで算出された計測値と比較する正解値としては、このチームで行ったハンドメジャーの値を利用しています。 他社サービスについて 本記事では比較対象として、ZOZOMETRYと同様に携帯端末によりスキャンを行い身体の各箇所の計測値を算出しているA社(仮称)のサービスを取り上げます。A社の公開している計測箇所の一部の定義がZOZOMETRYと同様にISO規格に準拠しているため、同じ定義の計測箇所で比較検証を実施しました。 検証対象データについて 本検証については、以下のデータを収集して比較しています。 データ数 42件の計測データ(ZOZOSUITなし計測、ZOZOSUITあり計測、A社サービスで計測したデータ数がそれぞれ42件あります)。いずれも日本国内に在住している被検者を対象に計測。 対象者の体形レンジ 被験者の身長レンジ: 148cm ~ 186cm 被験者のBMIレンジ: 17.3 ~ 41 計測環境 ZOZOSUITなし計測、ZOZOSUITあり計測ともに照明、背景について当社の想定する理想的な撮影環境下において計測を実施しています。 A社サービスについては、サービス内で表示される注意事項に遵守した環境で計測を実施しています。 計測誤差の算出方法 ZOZOスタッフの計測した各部位のハンドメジャーの値と比較して、ZOZOSUITなし計測、ZOZOSUITあり計測、A社サービスの計測値が相対値で何パーセントずれているかを計算しています。今回対象としたすべてのスキャンデータにおいて、各計測箇所における誤差の平均値および標準偏差を集計しました。 計測精度の比較結果 ※計測箇所の名称はZOZOMETRYサービス内の呼称に準拠 傾向としては以下のようになりました。 平均誤差で見ると、どの計測箇所もZOZOSUITあり計測の精度が最も誤差が小さい結果となりました。特にA社サービスと比較すると、計測箇所によっては半分または三分の一以下に誤差を抑えられています。標準偏差の値もZOZOSUITあり計測の結果が最も小さくなるケースが多く、全体的に誤差が小さく抑えられていることがわかります。 ZOZOSUITなし計測での結果については、ZOZOSUITあり計測の精度には及ばないものの、A社サービスと比較して平均誤差を小さく抑えられています。ZOZOSUITなし計測の標準偏差についても同様に、ZOZOSUITあり計測よりはばらつきが出るものの、A社サービスとの比較ではどの計測箇所においてもばらつきが少ない結果でした。 以上の傾向から、ZOZOSUITあり計測、ZOZOSUITなし計測ともに今回取り上げた計測箇所についてはA社サービスより高精度な計測結果が期待できると考えられます。また、より高い精度が求められるケースにおいては、ZOZOSUITあり計測での計測が望ましいといえます。 なお、正式ローンチ以前よりオーダーメイドバイクスーツ制作のためZOZOMETRYを導入いただいていた南海部品株式会社様からも、計測精度の高さについてはコメントをいただいています。ZOZOMETRY申し込みサイトに掲載されている導入事例の記事もぜひご参照ください。 biz.zozometry.com 今回の検証では、A社のサービスと定義が共通している計測箇所4か所での比較をしました。ZOZOMETRYの正式ローンチ時点では、この4か所を含めて最大139箇所の計測が可能(10月15日現在)で、今後も新たな計測箇所が随時追加されていく予定です。 まとめ 本記事ではZOZOSUITがこれまでどのように使用されZOZOMETRYの開発に至ったのか、またその計測精度について紹介しました。また他にもZOZOMETRYのシステムについていくつか記事を執筆しています。バックエンドなどのシステムについて詳しく知りたい方は、ぜひそちらも合わせてご確認ください。 techblog.zozo.com ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 本検証に利用した3Dボディスキャナーの計測精度は±0.5%の公称誤差(1000mmの周囲長に対し±5mmの計測誤差が出る精度) ↩
アバター
DevRelブロック改めDeveloper Engagementブロックの @ikkou です。ZOZO開発組織の1か月の動向をMonthly Tech Reportとしてお伝えします。 ZOZO TECH BLOG 2024年10月度は11本の記事を公開しました。10月度は 10月15日に正式ローンチを迎えたZOZOMETRY の関連記事を集中的に4本公開しています。11月中にもう1本ZOZOMETRY関連記事を公開する予定です。 techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com 登壇 ZOZO Tech Meetup ~データガバナンス / データマネジメント~ 10月22日に自社主催のオフラインイベントとして『 ZOZO Tech Meetup ~データガバナンス / データマネジメント~ 』を開催し、データシステム部と事業推進部に所属する計5名が登壇しました。 techblog.zozo.com 本イベントはオフライン限定としてオンライン配信は実施しませんでした。イベントレポートには当日の様子とともに全登壇者の登壇資料を掲載しています。特にデータガバナンスやデータマネジメントに興味や課題をお持ちの方はぜひご覧ください。 エンジニアのキャリアランチ - スタッフエンジニア編 by Forkwell 10月24日に開催された『 エンジニアのキャリアランチ - スタッフエンジニア編 by Forkwell 』に技術戦略部の堀江( @Horie1024 )が登壇しました。 現在配信中📡『エンジニアのキャリアランチ - スタッフエンジニア編 by Forkwell』に技術本部 テックリードの堀江 @Horie1024 が登壇しています🎙️ ランチにあわせてぜひご視聴ください! https://t.co/4OEeocEyR6 #Forkwell_キャリアランチ — ZOZO Developers (@zozotech) 2024年10月24日 Forkwellのアカウントをお持ちの方はアーカイブを視聴できます。ぜひご覧ください。 AIレコメンドシステムの最前線を語る 10月28日に開催されたオンラインイベント『 AIレコメンドシステムの最前線を語る 』にデータシステム部の寺崎( @f6wbl6 )が登壇しました。登壇資料を公開しているので、当日の配信を見逃した方はぜひご覧ください。 本日12-13時にオンラインで開催される『AIレコメンドシステムの最前線を語る』に推薦基盤ブロック長 兼 推薦研究ブロック長の寺崎 @f6wbl6 が『ZOZOTOWNでの推薦システム活用事例の紹介』というタイトルで登壇します🎙️ ランチにあわせてお気軽にご視聴ください! https://t.co/1KXbIvYvMI #gen_ai_conf — ZOZO Developers (@zozotech) 2024年10月28日 speakerdeck.com Recommendation Industry Talks #4 10月30日に開催されたオフラインイベント『 Recommendation Industry Talks #4 』にデータシステム部の寺崎( @f6wbl6 )が登壇しました。登壇資料を公開しているので、当日参加できなかった方はぜひご覧ください。 【満員御礼】今夜ウォンテッドリー株式会社様で開催される『Recommendation Industry Talks #4』に推薦基盤ブロック 兼 推薦研究ブロックの寺崎 @f6wbl6 が『ZOZOTOWN のホーム画面をパーソナライズすることの難しさと裏話を語る』というタイトルで登壇します🎙️ https://t.co/CMeETydq5x #RecIndTalks — ZOZO Developers (@zozotech) 2024年10月30日 speakerdeck.com 掲載 Software Design 2024年11月号 ZOZOTOWNリプレイスプロジェクトについて全8回で連載中の「 Software Design 2024年11月号 」が10月18日に発売されました。第7回のテーマは「検索機能リプレイスの裏側」です。ぜひご覧ください。 ZOZOTOWNリプレイスプロジェクトについて連載中の「Software Design 2024年11月号」が本日10月18日(金)に発売されました! 第7回のテーマは「検索機能リプレイスの裏側」です。今回もぜひお楽しみください! #zozo_engineer https://t.co/BUG17iWT19 — ZOZO Developers (@zozotech) 2024年10月18日 第6回までの連載は全文を公開しています 。あわせてご覧ください。 Software Design総集編【2018~2023】 10月12日に発売された『 Software Design総集編【2018~2023】 』に、データシステム部の塩崎が『 Software Design 2021年9月号 』に寄稿した記事『BigQueryによるデータ基盤構築の舞台裏 失敗から学んだ健全な運用とは』が掲載されています。 『Software Design総集編【2018~2023】』は本日(10月12日)発売です! 付属のDVD-ROMおよび同梱の電子版ダウンロードコードから、6年分のバックナンバーPDFを入手できます。 ITの基礎情報のほか、多くのITエンジニアやIT現場の知見の宝庫です。ぜひお手元に1冊置いておいてください。… pic.twitter.com/9mOnn9V1hm — SoftwareDesign (@gihyosd) 2024年10月12日 エンジニアtype 手前味噌ですが、私がエンジニアtypeさんから取材を受けた記事『 「DevRelの目的は、採用ではなくブランディング」ZOZOのDevRelが目指すのは、業界への恩返し 』が10月30日に公開されました。 type.jp ZOZOにおけるDevRelや技術広報の考え方について語っています。技術情報の発信については過去にZOZO DEVELOPERS BLOGやマイナビニュースのTECH+(テックプラス)にも掲載されています。あわせてご覧ください。 technote.zozo.com news.mynavi.jp その他 2026年度エンジニア新卒採用本選考の応募受付開始 10月1日よりエンジニア新卒採用本選考の応募受付を開始しました。あわせてYouTubeに『 2026年度エンジニア向け新卒会社説明動画 』も公開しています。ご応募をお待ちしています! \応募受付開始!2026年度エンジニア職 新卒採用🔥/ 本日よりエンジニア新卒採用本選考の応募受付が開始🤝❤️‍🔥 面接以外にもパネルトークや希望エンジニアとのコーヒーチャットなど幅広いコンテンツを通して、会社理解を深めながら選考を進めることができます☕ みなさまのご応募をお待ちしています! pic.twitter.com/LPuw7TvtRU — ZOZO Developers (@zozotech) 2024年10月1日 www.youtube.com Findy Team+ Award 2024 受賞 ZOZOは開発生産性が優れたエンジニア企業を称える式典『 Findy Team+ Award 2024 』にてTeam AwardのSequential Approach Div.部門で受賞いたしました。 ZOZOは『Findy Team+ Award 2024』にてTeam AwardのSequential Approach Div.部門で受賞いたしました! 開発生産性が優れたエンジニア組織を表彰「Findy Team+ Award 2024」〜約450社・20,000チームから、多様な観点で優れた48社を選出〜 https://t.co/HvdqEoyz6k #FindyTeamAward — ZOZO Developers (@zozotech) 2024年10月31日 表彰式にはデータシステム部の寺崎( @f6wbl6 )が出席しました。 Team Award〜Sequential Approach Division〜受賞企業様です!👏 #FindyTeamAward pic.twitter.com/FClN5QguHr — Findy Team+ファインディ【公式】 (@FindyTeamPlus) 2024年10月31日 現場からは以上です! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、WEARフロントエンド部Androidブロックの酒井柊輔です。普段はファッションコーディネートアプリWEARのAndroidアプリを開発しています。 WEARアプリは2024年5月に大規模なリニューアルをしました。そのため新たに多くの画面やUIを開発する必要がありました。しかしWEARアプリはビルド時間が長く、少しの変更を確認するだけでも数分かかるため、新規のUIの作成やUIの変更と確認に多大な時間を要していました。 このような課題を弊チームでは、UIの開発と確認をするための、ビルド時間の短い簡易アプリを作成することで解決しました。本投稿ではそのUI確認用簡易アプリを用いた課題解決アプローチの詳細と、その成果についてご紹介します。 目次 はじめに 目次 WEARプロジェクトのモジュール構成 ビルド時間が大幅にかかるという課題 解決への取り組み 具体的なアプローチ 作成したUI開発用アプリの概要 各開発者専用のUI確認用モジュールの構成 UiApplication UiActivity UiFragment build.gradle UI開発用アプリで得られた成果 依存モジュールの制限によるビルド時間の短縮 既存画面やUI共通部品を利用できる 各開発者が独自の環境を運用できる おわりに WEARプロジェクトのモジュール構成 弊チームでは現在マルチモジュール化を進めており、以下の図のようなモジュール構成(イメージ)で当時は開発をしていました。 ├── Project │ ├── :app │ ├── :feature │ │ ├── :home │ │ │ ├── HomeFragment │ │ │ ├── HomeFragmentViewModel │ │ │ └── HomeScreen │ │ ├── :search │ │ │ ├── SearchFragment │ │ │ ├── SearchFragmentViewModel │ │ │ └── SearchScreen │ │ └── :mypage │ │ ├── MyPageFragment │ │ ├── MyPageFragmentViewModel │ │ └── MyPageScreen │ ├── :core │ │ └── :ui │ │ ├── CommonButton.kt │ │ ├── CommonCard.kt │ │ └── etc... │ ├── :infrastructure │ └── :domain :app :元々利用していたモジュール。各モジュールに分割しきれていないファイル群(navigation関連ファイル、ネットワークアクセス関連ファイル等)が格納されている :feature :アプリの画面や機能毎に分割されたモジュール群 :home , :search , :mypage :各画面に関連するファイル群を格納するモジュール :core :アプリ共通のファイルを格納するモジュール群 :ui :UI共通部品を格納するモジュール :infrastructure :ネットワークアクセスロジックがまとめられているモジュール群 :domain :ビジネスロジックや共通のモデルがまとめられているモジュール群 ビルド時間が大幅にかかるという課題 些細な変更を確認するにも、多大なビルド時間を要することが課題でした。 当時はWEARの大規模リニューアル開発の真っ最中だったので、多くの新たなUIをJetpack Composeで作成する必要がありました。しかしアプリの長いビルド時間によって、作成したUIの確認と修正のサイクルを効率よく回せず、開発が滞ってしまう問題を抱えていました。 解決への取り組み ビルドが遅い原因を調査したところ、モジュール分割しきれていないファイル群が入っている:appのビルドに大幅な時間を要していることが分かりました。 幸い、UIに関するファイルはほとんど:appから別モジュールへ分割できている状態でした。そのため弊チームでは課題を、 :appをはじめとする不要なモジュールを抜いた、必要最低限の依存関係を持つ簡易アプリを、UI開発用に作ることで解決しました。 具体的なアプローチ 作成したUI開発用アプリの概要 UI開発用アプリの開発環境として、Project配下にui-appというディレクトリを作成しました。ui-appの中には各開発者のUI確認用モジュールが格納されています。 ├── Project │ ├── ui-app │ │ ├── :developer1 │ │ ├── :developer2 │ │ └── :developer3 │ ├── :app │ ├── :feature │ │ ├── :home │ │ │ ├── HomeFragment │ │ │ ├── HomeFragmentViewModel │ │ │ └── HomeScreen │ │ ├── :search │ │ │ ├── SearchFragment │ │ │ ├── SearchFragmentViewModel │ │ │ └── SearchScreen │ │ └── :mypage │ │ ├── MyPageFragment │ │ ├── MyPageFragmentViewModel │ │ └── MyPageScreen │ ├── :core │ │ └── :ui │ │ ├── CommonButton.kt │ │ ├── CommonCard.kt │ │ └── etc... │ ├── :infrastructure │ └── :domain 各開発者専用のUI確認用モジュールの構成 各開発者のモジュールの構成は以下のようにしました。 ├── :developer1 │ ├── src │ │ └── main │ │ ├── java │ │ │ └── com.xxx │ │ │ ├── UiApplication │ │ │ ├── UiActivity │ │ │ └── UiFragment │ │ └── res │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.webp │ │ └── AndroidManifest.xml │ ├── .gitignore │ └── build.gradle.kts UiApplication Applicationです。最小構成での実装です。 class UiApplication : Application() { override fun onCreate() { super .onCreate() // サードパーティライブラリの初期化処理等 } } UiActivity Activityです。replaceメソッドの第二引数に任意のFragmentを渡すことで、好きな画面を表示できるようにしています。 class UiActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) val containerId by lazy { View.generateViewId() } setContent { AndroidView( factory = { context -> FragmentContainerView(context).apply { id = containerId } }, update = { supportFragmentManager.commit { replace(containerId, UiFragment()) } }, ) } } } 例えば:feature:homeをこのモジュールでimplementしていたとしたら、 replace(containerId, HomeFragment()) とすればホーム画面を表示できます。 UiFragment Fragmentです。主にJetpack Composeで作成したUIを確認する用途で利用していました。 単純にsetContent内に作成したUI配置し、端末上で表示して確認することを行なっていました。例えば:core:uiをこのモジュールでimplementしていたとしたら、作成したUI共通部品をsetContent内から参照し確認できます。 class UiFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View = ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner), ) setContent { Surface( modifier = Modifier.fillMaxSize(), ) { // 作成したComposeのUI CommonButton() } } } } build.gradle UI開発用アプリモジュールのbuild.gradleです。 plugins { apply( "com.android.application" ) apply( "org.jetbrains.kotlin.android" ) apply( "org.jetbrains.kotlin.plugin.compose" ) } android { // 既存プロジェクトの設定と同じものを記述 } dependencies { // UI確認に最低限必要な依存関係 implementation xxx implementation yyy implementation zzz // 後から各開発者が必要に応じて追加する依存関係 implementation project( ":feature:home" ) implementation project( ":feature:search" ) implementation project( ":feature:mypage" ) implementation project( ":core:ui" ) } pluginsにはモジュールのビルドに必要な com.android.application と、Kotlinを扱うのに必要な2つのプラグインを記述しました。 dependenciesには最低限必要な依存関係のみ予め記述しておき、その他の依存関係は各開発者が必要に応じて自身のモジュールのbuild.gradleに記述する運用としました。 WEARでは、:feature:homeや:core:ui等のUI開発に必要な依存関係のみをimplementすることで、ビルド時間のかかる不要な依存関係(:app等)を省いたアプリを実現しました。 UI開発用アプリで得られた成果 このようなアプリを作成することで、以下のような成果を得られました。 依存モジュールの制限によるビルド時間の短縮 既存画面やUI共通部品を利用できる 各開発者が独自の環境を運用できる 依存モジュールの制限によるビルド時間の短縮 ビルド時間が5〜10分かかっていたのを10秒程度に短縮でき、チームの開発効率が上がりました。 また、UI作成のトライアンドエラーのサイクルを回しやすくなったので、Android開発にまだ慣れていないチームメンバーの技術キャッチアップの手助けにもなりました。 既存画面やUI共通部品を利用できる 別Projectではなく同Project内にUI開発用アプリを作ることで、既存画面に新規作成UIを組み込みながら開発できたり、アプリ内で利用されるUI共通部品を利用した開発も行えたりしました。 WEARでの事例だと、:feature:homeをimplementして既存のホーム画面を利用した開発をしたり、:core:uiをimplementして共通UI部品を利用した開発をしたりしました。 各開発者が独自の環境を運用できる developer1, developer2のように、各開発者のモジュールを作成することによって各々が独自のUI開発用アプリの環境を作ることができました。 WEARチームではこれらの各開発者のモジュールを、他のコードと同様にGitで管理していました。そのため作成したUIを確認できるようなコードを開発者専用のモジュールに記述しておけば、レビューする人がPR確認時にそのモジュールを手元でビルドしUIを確認することにも利用できました。 おわりに 本記事では、ビルド時間の短いUI開発用アプリの作成方法とその運用方法、得られた成果をご紹介しました。 UIに関するファイルのモジュール分割が既にできていれば、どの開発現場でも適用できる事例であると思います。もし同様の問題を抱えていれば、アプリの作成を検討してみてはいかがでしょうか。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに 技術評論社様より発刊されている Software Design の2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。 3年前に行われたZOZOTOWNの大規模なリニューアルを行う際、リプレイスプロジェクトと関連する課題を解決するためにBFF(Backends For Frontends)の導入が行われました。今回は、その経緯と効果を紹介します。 目次 はじめに 目次 はじめに ZOZOTOWNの課題とBFFによる解決 通信量の増大 パーソナライズ機能の追加 BFFによる解決 アーキテクチャの説明 BFFをマイクロサービスとして構築 BFF構成にて見えてきた課題 キャッシュの導入 Redisをキャッシュに使う キャッシュを導入したことによる新たな問題 キャッシュスタンピードの対策 サービスの可用性 BFFは可用性が大事 各マイクロサービスに依存しないしくみ 運用時に見つけた課題 サーキットブレーカーの導入 意図しているエラー条件 BFFにおける障害試験の重要性 BFFのこれまでとこれから おわりに はじめに こんにちは。株式会社ZOZO技術本部SRE部の三神と技術本部ECプラットフォーム部の藤本です。ZOZOTOWNでは約3年前に大規模なリニューアルを実施し、BFF(Backends For Frontends)を導入しました。第6回ではBFFの導入に至った経緯やそのしくみ、そして導入後にどのような変化があったのかについて紹介します。 ZOZOTOWNの課題とBFFによる解決 2021年3月に実施したZOZOTOWNの大規模リニューアルの一環として、BFFを導入するという判断をしました。BFFはアーキテクチャ設計パターンのひとつで、クライアントからのリクエストを一元管理し、フロントエンドとバックエンドの間で双方の複雑さを吸収して処理を効率化するためのものです。BFF導入の背景には、ZOZOTOWNリプレイスプロジェクトやリニューアル時にやりたかったことに関連して、いくつか解決したい課題がありました。その中でもとくに大きなものとして次の2つがありました。 通信量の増大 1つ目はクライアントからの通信量の増大です。連載第1回(本誌2024年5月号)でお伝えしているとおり、ZOZOTOWNリプレイスプロジェクトでは、VBScriptのモノリスなアプリケーションからGoやJavaを使ったマイクロサービスへ移行を進めていました。これまでのモノリスなアプリケーションとして動いていたところからマイクロサービスへ機能を切り出していくと、内部の機能の呼び出しだったところが各マイクロサービスに対しての通信へ置き換わることとなります。 このマイクロサービスの呼び出しに伴う通信を、そのままブラウザやスマートフォンアプリといったクライアントから直接行おうとすると、サーバとの通信回数が何倍にも増えてしまいます。もし1回のサーバとの通信で必要な情報が集められない場合は、複数回同じサーバに通信する必要が出てきてしまいます。とくにスマートフォンの場合は、通信回数が増えることで電池の消費も増えてしまうのでより大きな問題となります。 パーソナライズ機能の追加 課題の2つ目は、ユーザーの性別や年齢、お気に入り情報などからコンテンツの内容を変化させるパーソナライズ機能の追加です。ZOZOTOWNを訪れるユーザーの興味や関心はさまざまなので、それぞれのユーザーの好みに合わせたコンテンツを表示することで、より便利に使ってもらえることを目指しています。 リニューアル時のパーソナライズ機能の追加には、表示する条件の変更が柔軟に行えるようにしくみを整えることも含まれていました。日常的なサービスの運営として、パーソナライズ機能で表示するコンテンツの種類や数を、キャンペーンやセール、季節などに合わせて調整できるようにする計画だったため、処理をクライアント側に実装するわけにはいきませんでした。もしクライアント側に実装すると、変更のたびにリリースが必要になってしまい、柔軟な変更という部分が損なわれてしまいます。こちらも1つ目の課題と同様に、パーソナライズの条件の変更のたびにスマートフォンのアプリをアップデートするのは現実的ではないので、避けなければなりませんでした。 BFFによる解決 1つ目の課題は、クライアントと各マイクロサービスの間にZOZO Aggregation APIというBFFを配置して、通信量を抑えることで解決しました(図1)。クライアントへのレスポンスも、重複した内容を削りながら必要としている情報を整理してレスポンスできるようになっています。また今後さらに必要なマイクロサービスが増えたとしても、ZOZO Aggregation API内でレスポンスを1つにまとめられるので、クライアント側の通信に大きな影響を与えずに済みます。もちろん表示するコンテンツを増やした場合はレスポンスのサイズも増えますが、クライアントの通信回数を増やした場合よりも抑えられると考えています。 図1 クライアントとマイクロサービスの間にZOZO Aggregation APIを配置 そして2つ目の課題は、サービスを「表示するコンテンツを選択するサービス(推薦サービス)」と「コンテンツの中身を提供するサービス」に分けて、ZOZO Aggregation APIからそれぞれのサービスを順に呼び出すことで解決しました。推薦サービスを独立させることによってクライアントの中に処理を持たせないという仕様はクリアできました。しかし、各サービスをクライアントから直接呼び出してしまうと通信量の増大という課題が残ってしまうことになるので、リニューアルのタイミングでBFFを入れる判断をしました。追加したパーソナライズ機能は、ユーザーごとに表示するコンテンツの種類や数の調整を推薦サービスが行い、その結果をもとにZOZO Aggregation APIがコンテンツに必要な情報を各マイクロサービスから集めて、最後にクライアントが必要としている形に整形してレスポンスする流れになっています(図2)。表示するコンテンツを変更したい場合も、ZOZO Aggregation APIと推薦サービスの間で調整すればよく、クライアント側が意識する必要はほぼなくなっています。 図2 ZOZO Aggregation APIによるパーソナライズ機能の実現 アーキテクチャの説明 BFFをマイクロサービスとして構築 ZOZOTOWNではAPIゲートウェイパターンのアーキテクチャを採用しており、認証認可やカナリアリリース機能を備える高機能なZOZO API Gatewayを内製しています。このZOZO API Gatewayを軸にZOZOTOWNのシステムは構成されています。 そこで、ZOZOTOWNトップページの表示内容を生成するAPIとしてZOZO Aggregation APIをZOZO API Gateway配下の1マイクロサービスとして設置し、リクエストもZOZO API Gatewayを経由してルーティングする形で構築しました。 BFF構成にて見えてきた課題 ZOZO Aggregation API導入後のシステム要件を整理する中で、各マイクロサービスの最大負荷が設計当初の想定以上に高いことが判明しました。 リニューアル後のトップページでは、ZOZOTOWNを利用する各ユーザーに対して趣味、嗜好に合わせた魅力的な商品をリーチするために今まで以上に多くのデータを使ってパーソナライズを行っています。そのため各マイクロサービスへのリクエストがリニューアル前に比べて格段に増加していました。それに加えて「ZOZOWEEK」等の大規模セール時は通常時と比較して圧倒的にユニークユーザー数が多くなるので、スパイクを考慮すると各マイクロサービスへのリクエストがリニューアル前の数倍以上になる可能性が出てきました。 リニューアル後の大規模セール時に発生するスパイクをシミュレーションすると、既存の各マイクロサービス構成では負荷に耐えられないことがわかりました。各マイクロサービスが耐えきれずレスポンスが遅延して、ZOZO Aggregation APIのレスポンスも遅延すると、トップページ生成時間が長くなるのでZOZOTOWNでの体験を著しく損なってしまいます。 キャッシュの導入 この問題の対策として、各マイクロサービスの増設、もしくはキャッシュの導入を検討しました。前者の増設による対策の場合、すべてのマイクロサービスをイベントごとに増設する必要があり、そのための工数や維持費用が膨れ上がり現実的な解決策とは言えませんでした。そこで、後者のキャッシュによる解決策を中心に検討を進めていきました。 レスポンスの遅延はトップページにおけるアクセス増が原因なので、AkamaiやFastlyといったCDNを用いたキャッシュによる負荷軽減策を模索しました。しかし、パーソナライズを実現するにあたり、多種多様なデータの組み合わせを想定しているため、ユーザーごとに表示される内容に差異が多くなる仕様になっていました。したがって、ZOZO Aggregation APIにて集約した後のページをキャッシュするCDNのような方式は負荷対策として効果的ではありませんでした。 Redisをキャッシュに使う そこで、トップページ生成に必要なデータを細かくキャッシュする方式を検討しました。ZOZO Aggregation APIではパーソナライズ条件に基づいて取得したデータをモジュール(部品)として扱っており、モジュールを組み合わせてトップページを生成しています。モジュール単位であれば、同一条件下でのレスポンスデータ生成においてキャッシュが利用できます。そのため、必要なリクエストをすべてマイクロサービスへ送るのではなく、マイクロサービスから取得したデータをモジュール単位でキャッシュするシステム構成に変更しました。 具体的には、マイクロサービスへ接続するときのURLとパラメーターをキーに、マイクロサービスから実際に取得できるレスポンスを値としてAmazon ElastiCache(Redis)に保存できるようにZOZO Aggregation APIを改修しました(図3)。マイクロサービスにリクエストを送る代わりにRedisからキャッシュを取得することで、直接リクエストする回数を減らして負荷の軽減を図る目的です。 図3 モジュール単位のキャッシュ これにより、もう一度同じ条件のモジュールを取得する場合は先にRedisを参照することで、マイクロサービスに接続する回数を減らせました。キャッシュを導入した効果はすばらしく、インフラの増強を最小限に抑えることができました。 キャッシュを導入したことによる新たな問題 キャッシュを使うことでコストの問題は解決できましたが、ZOZOTOWNの商品情報は随時更新されていくので、いつまでも同じキャッシュを使い続けることはできません。ZOZOTOWNでは毎日午前0時にクーポンを切り替えているため、少なくとも1日に1回はキャッシュに保存した商品情報を更新する必要があります。実際はクーポンの切り替え以外でも、価格や説明など商品の情報は1日に複数回更新される場合があります。 一般的に、キャッシュを保存するときは有効期限を設けて、期限が来たら自然に消えていくように設計することが多いと思います。この場合、キャッシュの有効期限が切れたタイミングで、瞬間的にマイクロサービスへリクエストが殺到することになります(図4)。せっかく負荷を減らしたにもかかわらず、マイクロサービスへの負荷が一気に増大してしまうということです。この現象は一般的にCache Stampede(キャッシュスタンピード)、Dog piling(ドッグパイル)などの名称で呼ばれています。本記事では当時社内で利用していたキャッシュスタンピードの名称を使用します。 図4 キャッシュが参照できないときにマイクロサービスヘリクエストが殺到する キャッシュスタンピードの対策 キャッシュスタンピードを防ぐ代表的な方法は3つあります。 次の有効期限に参照するキャッシュを事前に作る 有効期限が切れる前に延長する ロックを使って1プロセスだけオリジンから取得する 当時は未来の商品公開情報を生成するしくみが存在していなかったため、1の方法は採用できませんでした。また2の方法は、キャッシュ有効期限は延長されるものの保存している内容がそのまま残るため、商品情報を更新したいという要件には合いませんでした。結果として、残った3の方法を選択しました。 ZOZO Aggregation APIはKubernetes上で動作しているので、単純にアプリケーション内部でロックを取得しただけではほかのPodとはロックを共有できません。そのため、RedisのSETコマンドにNXオプションを付与して、Redis上でロックを取得することにしました。 NXオプションはキーが存在しない場合のみ値を設定して、キーが存在する場合は何もせず失敗します。ロックを取得できた場合はオリジンから商品情報を取得してキャッシュの更新処理を行い、ロックを取得できなかった場合はキャッシュが更新されるのを一定時間待つようにしています。これによりキャッシュスタンピードを防ぎつつ、キャッシュ更新時の遅延も抑えながら安定してレスポンスを返すことができています。 サービスの可用性 BFFは可用性が大事 BFFはフロントエンドからリクエストを受け付けるため、BFFに障害が発生するとサービス全体に直結しやすい傾向にあります。つまり、BFFはサービス可用性を考えるうえで重要なポイントです。 ZOZO Aggregation APIに関しても、BFFとして設計を進めていくうえで障害時のシナリオをシミュレーションしたところ、大きな課題を発見しました。ZOZO Aggregation APIは複数のマイクロサービスからモジュールとして商品のデータを取得する必要があるため、初期の設計では、いずれかのマイクロサービスに障害が発生した際に引きずられてカスケード障害が発生することが懸念されました。しかし、ZOZO Aggregation APIは初期の設計でも3つ以上のマイクロサービスと通信してトップページに必要なモジュールを生成していたので、ZOZO Aggregation APIとその3つのマイクロサービスがすべて正常に動作することが、正常にトップページを生成する条件となっています。そのため、可用性の低いシステムになっていました。 BFF導入後のアーキテクチャでZOZOTOWNの可用性を担保するにはこの課題の対策が必須となりました。 各マイクロサービスに依存しないしくみ そこでZOZO Aggregation APIでは、いずれかのマイクロサービスにて障害が発生した場合は、取得できた情報とデフォルトとして定義された情報を組み合わせたモジュールを生成してレスポンスを行う仕様にしました。タイムアウトとリトライ制御を各マイクロサービスに設定しておき、マイクロサービスが規定の時間内に正常なレスポンスを返さない場合はほかのマイクロサービスから取得できたデータとデフォルト定義されたデータにてモジュールを生成します。 実際に運用が始まると、障害の際にマイクロサービスに障害が発生して一部のデータを取得できない状態になりました。しかし本仕様のおかげでZOZO Aggregation APIは障害にならず、ZOZOTOWNのトップページを表示し続けることができました。 運用時に見つけた課題 ZOZO Aggregation APIにはリリース後もさまざまな機能が追加されており、マイクロサービスの通信先もリリース時と比べて増えている状態でした。リリース初期は同じKubernetesクラスター内のマイクロサービスとの通信がほとんどでしたが、社内の別環境にあるAPIや社外のAPIからデータを取得して生成するモジュールも出てきました。通信先が増えてもZOZO Aggregation APIにて各マイクロサービスの障害に引きずられないしくみを導入しているので安心していましたが、障害発生時に挙動を確認した際に気になる点がありました。 先の仕様ではZOZO Aggregation APIから各マイクロサービスに対してタイムアウトとリトライ制御を使って障害判定をしていたため、障害発生時に200を返すことによりレスポンスタイムの悪化が発生していました。仮にマイクロサービスにて10分間障害が発生するとZOZO Aggregation APIは「レスポンスは遅延しているが200を返す」状態で10分間動作し続けていることになります。マイクロサービスの障害に引きずられないしくみを導入したのはZOZOTOWNのユーザー体験を損なわないことが目的ですが、この状態はユーザー体験が良いとは言えないので対策することになりました。 ZOZO Aggregation APIでは各マイクロサービス間との通信におけるタイムアウトとリトライ制御にIstioを利用しているため、Istioを活用して対応する方法がないかを検討しました。Istioを調査する中でサーキットブレーカー機能があるとわかり、ZOZO Aggregation APIと各マイクロサービスとの通信にサーキットブレーカーを導入することで障害発生時のレスポンスを改善できるのではと考えました。 サーキットブレーカーの導入 サーキットブレーカーとは、あるサービスの障害を検知した場合には通信を遮断、その後サービスの復旧を検知すると通信を復旧させるしくみです。サーキットブレーカーを導入することで、各マイクロサービスに障害が発生した際にサーキットブレーカーがそれを検知し、ZOZO Aggregation APIと該当マイクロサービスとの通信を即座に遮断します。遮断されている状態ではZOZO Aggregation APIが該当マイクロサービスに通信をすると即座にエラーレスポンスが返るので、先のしくみにより取得できたデータからのレスポンスデータをもとにモジュールを生成します。マイクロサービスの障害が収束するとサーキットブレーカーがそれを検知してZOZO Aggregation APIとの通信を復旧させます(図5)。 図5 サーキットプレーカー導入による変化 サーキットブレーカーを導入したことで、ZOZO Aggregation APIは障害発生時でも都度タイムアウトを待たずにレスポンスを返せるようになりました。また先のしくみと合わせることで、特定のマイクロサービスに障害が発生したとしてもユーザー体験を損なわないシステムになり、BFFとして信頼性が高い状態となりました。 意図しているエラー条件 このように可用性担保のためさまざまな対策が行われたZOZO Aggregation APIですが、1つだけ可用性を考慮せず、意図して500エラーをレスポンスする条件があります。それは「キャッシュから正常なデータを取得できない状態」です。 ZOZO Aggregation APIはキャッシュを導入することで各マイクロサービスの負荷軽減を行っています。キャッシュからデータが取得できない場合に、各マイクロサービスから直接データを取得する挙動だと、セールなどの高負荷時に対象マイクロサービスがダウンする可能性があります。ZOZO Aggregation APIのキャッシュに障害が発生したことで各マイクロサービスが高負荷になり、ZOZOTOWNの別機能に影響が出るという事態は防がなければいけません。そこで、キャッシュにて障害が発生した場合は、ZOZO Aggregation APIにて500エラーを返して各マイクロサービスと通信をしない仕様にしています。 なおキャッシュ障害を検知した場合は、予備で用意しているキャッシュに通信先を変更することで迅速な復旧ができるようにしています。 BFFにおける障害試験の重要性 前述のとおりBFFは可用性がとても重要なので、障害時の動作を把握するために、障害試験にはかなり注力しています。サーキットブレーカー導入時はもちろんのこと、新たな通信先が追加されるたびに、さまざまなエンドポイントにて障害発生時の動作を確認しています。 ZOZO Aggregation API自体の障害発生シミュレーションは当然行いますが、外部サービスも含めて多種多様なマイクロサービスと通信するため「各マイクロサービスがダウンした場合の挙動」を定義しておき、障害試験で想定通りのレスポンスが返答されるか確認することが大事です。Istioを使ってZOZO Aggregation APIと各マイクロサービス間の通信に遅延を発生させて、サーキットブレーカーの発動と発動後のレスポンス内容が想定どおりになっているかはリリース前にチェックしています。 これらのチェックを行っているため、ZOZO Aggregation APIはリリースから今年で3年が経過しているにもかかわらず、安定した運用を続けられています。 BFFのこれまでとこれから ZOZOでは、BFFアーキテクチャの国内での実例がまだ少なかった2021年から、ZOZO Aggregation APIを構築して運用を続けてきました。運用していく中でキャッシュスタンピードをはじめとしたさまざまな課題が見つかりましたが、開発者とSREが一丸となって改善を続けてきました。結果を見れば、この3年間におけるZOZOTOWNの安定性にZOZO Aggregation APIは大きく貢献しており、当初想定していたアーキテクチャのメリットを享受できています。 リリース当初はZOZOTOWNトップページの表示内容を生成するAPIでしたが、現在はトップページだけではなくカート画面や検索画面等に表示するデータも扱う、ZOZOTOWNにおける中核を担うAPIとなりました。req/sやキャッシュの使用量も含めて右肩上がりになっており、用途はこれからも増えていく予定のため、今後の増強も予定しています。 また、3年の運用でZOZO Aggregation APIに機能が増えてきたことで、さまざまな課題が見えてきました。たとえば、さまざまな機能が追加されてロジックに複雑さが出てきたことや、関係者が増えたことによりコミュニケーションコストも増えてきたこと、マイクロサービスとBFFの責務があいまいになっている部分があることなどが挙げられます。これらの問題に対応するために、機能ごとにBFFとしての機能を分割する案や、デバイス別に分割する案といったさまざまな角度からこれからのZOZO Aggregation APIについて議論を進めています。今後のZOZOにおけるBFFの方針が決まった際には テックブログ 等で公開したいと思っています。 おわりに 連載第6回では2021年に導入したZOZOのBFFであるZOZO Aggregation APIについて、導入により発生したメリットや、運用上の課題、今後の展望について紹介しました。 BFFアーキテクチャのひとつの形としてBFFの導入を検討している方の参考になればうれしく思います。 本記事は、技術本部 SRE部フロントSREブロック ブロック長の三神 拓哉と同 ECプラットフォーム部マイグレーションブロックの藤本 拓也によって執筆されました。 本記事の初出は、 Software Design 2024年10月号 連載「レガシーシステム攻略のプロセス」の第6回「ZOZOTOWNにおけるBFFアーキテクチャ実装」です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。Developer Engagementブロックの @wiroha です。10月22日に「 ZOZO Tech Meetup ~データガバナンス / データマネジメント~ 」を開催しました。ZOZOTOWNを支える開発において「データガバナンス / データマネジメント」にフォーカスして、弊社エンジニアが具体的な事例を交えながら紹介するイベントです。 登壇内容まとめ 弊社から次の5名が登壇しました。 コンテンツ 登壇者 #1 データガバナンスチームの結成で得た学び 事業推進部 田中 #2 ZOZOのデータマネジメントの取り組み:これまでとこれから データ推進ブロック 髙木 #3 dbt-coreで実現するCore DataMartsのデータモデリング〜dbt編〜 推薦基盤ブロック 栁澤 #4 dbt-coreで実現するCore DataMartsのデータモデリング〜Cloud Composer編〜 データ基盤ブロック 奥山 #5 全部見せます!BigQueryコスト削減の手法とその効果 データ基盤ブロック 塩崎 #1 データガバナンスチームの結成で得た学び 事業推進部 田中による発表 speakerdeck.com 田中からのコメント 抽象度の高い学びの共有で恐縮ですが、データガバナンスに取り組んでいる/これから取り組むみなさまのお役に立てれば幸いです。これから迎えるZOZOのデータ活用の成熟期を、今回ご紹介したデータエンジニア、アナリティクスエンジニア、データマネージャに加え、ビジネス職のあらゆるスタッフやMLエンジニア、データアナリスト、データサイエンティストといった専門家たちと楽しんで行きたいと思います。 #2 ZOZOのデータマネジメントの取り組み:これまでとこれから データ推進ブロック 髙木による発表 speakerdeck.com 髙木からのコメント ZOZOで行っているデータガバナンス / データマネジメントについて一部ですがお話させていただきました。多くの方へ届けるためにオンライン開催も検討したのですが、オフラインならではの意見交換も活発に行うことができ、個人的にもとても楽しい時間を過ごすことができました! まだまだデータマネージャーとして働いている方は少ないかと思いますが、これからどんどん発展していく領域でもあるので、今回のイベントをきっかけにZOZO含めて興味を持っていただければとても嬉しいです。絶賛採用中なので、気軽にカジュアル面談をお申込みいただければと思います! #3 dbt-coreで実現するCore DataMartsのデータモデリング〜dbt編〜 推薦基盤ブロック 栁澤による発表 speakerdeck.com 栁澤からのコメント dbt-coreを使ったデータモデリングの実践についてお話しさせていただきました。会場では多くの方々にご質問をいただき、またデータモデリングに関する議論もできて、とても楽しかったです! 限られた時間の中で全てをお伝えできませんでしたが、スライドをご覧いただくことで、少しでも同じ悩みをお持ちの方々のお役に立てると嬉しいです。 #4 dbt-coreで実現するCore DataMartsのデータモデリング〜Cloud Composer編〜 データ基盤ブロック 奥山による発表 speakerdeck.com 奥山からのコメント Cloud Composer(Airflow)からdbtモデルの更新・データ品質チェックを依存関係を保ちながら行うためのポイントについて発表しました。dbt導入を検討しているものの、実行基盤・インフラ部分の実装に悩んでいる方々の一助になれば幸いです! 参加者にはデータエンジニアの方も多く、データパイプラインの設計やツールの選定などたくさんの知見を交換できて嬉しかったです! #5 全部見せます!BigQueryコスト削減の手法とその効果 データ基盤ブロック 塩崎による発表 speakerdeck.com 塩崎からのコメント BigQueryの裏技めいたコスト削減手法などを紹介しました。昨今の様々な事情によりコスト削減熱の高まりを観測することが多くなってますので、コスト削減に取り組んでいる方々の一助になれば幸いです。 いただいた質問への回答 当日は進行の都合上、発表ごとに質疑応答の時間を取れなかったため、後日回答するという形で質問を募りました。一部の質問についてテキストにて回答いたします。 Q. 聞き洩らしたかもしれませんが、データマネジメントチームの影響がつかめていないです。プロダクトに依存せず横ぐしですべてのデータを見ている印象を受けましたが合っていますでしょうか...? A. プロダクトに依存せず横串で対応するチームの認識で間違いありません。データマートで言えば全社で使える汎用分析データマートをデータマネジメントチームから提供し、その先のより細かい分析やBI用途のデータはデータアナリストや事業部のメンバーが作成しています。ただ、プロダクトによっては内部で完結しているケースもあるので、新たに領域を拡大していくのも課題としてはあります。 Q. LookMLを運用するうえでのツラいという話の詳細が気になりました。 A. 前提として、LookMLは優れたデータモデリング言語であり、今後もLookerをBIツールとして利用する予定です。ただ、データモデリングの全てをLookMLで行うと、管理の複雑化やパフォーマンス最適化の難しさという課題がありました。また、一時的にLookMLに詳しい人材が不足したこともあり、運用面での問題が浮き彫りになりました。そこで、重要なビジネスロジックはdbtに集約してデータモデルの安定性を確保しつつ、柔軟な表現が求められる部分はLookerのセマンティックレイヤーで対応することで、効率的な運用を目指しています。これにより、一貫性を保ちながらも、ビジネスニーズに柔軟に対応できる体制を整えたいと考えています。 Q. dbt docsがビジネスユーザーにツラいという話の詳細が気になりました。 A. 言葉足らずでしたが、奥山からあったように弊社ではSQLデータマートとdbtデータマートをBigQuery上で共存させています。dbt docsではdbtデータマートのみしか対応できないため、処理の方法に関わらず全体を俯瞰できるデータカタログが必要となっています。 Q. 誰でもSQLを使える点に驚きました。アクセスコントロールをどうやっているのか気になりました。 A. 個人情報や広く公開すべきではない営業秘密などに対してはBigQueryの列レベルセキュリティ機能を使っており、一部の限られた人のみが閲覧可能です。一方でそれ以外のほぼすべての情報は全社員が活用できるようにアクセス権限を設定しています。 最後に 登壇者のみなさん 非常にたくさんの方にご参加いただき、ありがとうございました。懇親会では登壇者への質問・意見交換が活発に行われ、有意義な時間を過ごすことができました。今後もイベントを開催していきますので、ぜひご参加ください! ZOZOではデータアナリスト、データマネージャー等のさまざまなポジションで一緒に働く仲間を募集中です。カジュアル面談も実施しておりますので、ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com www.wantedly.com
アバター
はじめに こんにちは、データサイエンス部の広渡です。データサイエンス部では、取り組みの一環として検索クエリのサジェスト(以下、サジェスト)の改善に力を入れています。 ここでサジェストは一般的に「Query Auto Completion」と呼ばれる、検索窓にキーワードが入力された際に続きを補完したキーワードを提示する機能を指します。 弊チームではサジェスト改善の取り組みとして、パーソナライズ化を進めています。本記事では、パーソナライズ化の一環として、ユーザーの性年代に適したサジェスト(以下、性年代別サジェスト)を実現した事例について紹介します。 参考として、近年のサジェスト改善事例に関する記事もご覧ください。 techblog.zozo.com 目次 はじめに 目次 背景・課題 性年代別サジェストとは 性年代別サジェストの実現方法 方針 Elasticsearchでの実現方法 結果 定性評価 A/Bテスト まとめ おわりに 背景・課題 ZOZOTOWNでは、ユーザーログを活用してサジェストを実現しています。なお、本記事ではサジェストに候補として表示された検索クエリをサジェストクエリと呼ぶことにします。 ユーザーログはサジェストクエリの作成と並び順に活用されています。並び順は、過去の検索クエリのクリック数やクリック後の商品の購入数などをベースにしたスコアを算出し、そのスコアに基づいて決定されています。ユーザーログを活用した詳しい改善事例は以下の記事を参照してください。 techblog.zozo.com 以前のサジェストでは、ユーザーログを一括りにしてサジェストクエリを作成しており、どのユーザーにもZOZOTOWNのメインユーザーである20〜30代女性向けのサジェストクエリが多く表示されていました。 サジェストクエリ作成時にユーザーの性別や年代を考慮していないため、例えば、男性ユーザーがキーワードを入力中に「Tシャツワンピース」など女性向けと考えられるサジェストクエリも表示されることになります。以下は実際のサジェストクエリの例です。 そこで、ユーザーログを性別や年代ごとに活用することで、ユーザーの性年代に適したサジェストクエリを提供する性年代別サジェストに取り組みました。 性年代別サジェストとは 性年代別サジェストとは、ユーザーの性別と年代の組み合わせごとにサジェストクエリを作成し、ユーザーの性年代ごとにそれぞれ異なるサジェストクエリを表示するものです。 性別と年代の組み合わせは以下のようなイメージです。 性別:男性、女性 年代:10代、20代、... 男性x10代、男性x20代のような性別と年代の組み合わせごとにサジェストを作成します。それを用いて以下の図に示すようにユーザーの性年代に応じたサジェストクエリを表示します。 性年代別サジェストの実現方法 ここでは、性年代別サジェストを実現した方法について紹介します。 方針 まず、性年代別サジェストを実現するために、2つの方針を考えました。 性年代別でサジェストクエリとなるキーワードごと変える。 ユーザーログを性年代別にフィルタリングし、サジェストクエリとなるキーワードを作成する。 例えば、20代男性ユーザーが「Tシャツ レディース」を検索したログがなければ、他の性年代のユーザーが検索していたとしてもサジェストクエリとならない。 サジェストクエリとなるキーワードは同じだが性年代別でスコアを変える。 全てのユーザーログでまとめてサジェストクエリとなるキーワードを作成した上で、その性年代が検索したキーワードでないサジェストクエリは削除せず、表示順が下位になるようにスコアを小さくする。 例えば、20代男性ユーザーが「Tシャツ レディース」を検索したログがなくても、他の性年代のユーザーが検索していたらサジェストクエリとなる。 各方針のイメージです。20代男性が検索窓に「Tシャツ」を入力した場合を想定しています。 今回は方針2の「サジェストクエリとなるキーワードは同じだが性年代別でスコアを変える」方針に定めました。 前者は、その性年代でクリックされにくいサジェストクエリを取り除けるかもしれませんが、サジェストクエリの数は少なくなる懸念があります。後者は、前者のデメリットをカバーし、改善前のサジェストクエリの数に保つことができるというメリットがあると考えました。 スコア算出のロジックは既存のものを使用し、性年代ごとのクリック数やクリック後の商品の購入数をもとにそれぞれのスコアを算出しました。これは、性別や年代ごとのクリック数や購入数をスコア算出に活用することで、性年代に適したサジェストクエリが表示されているかどうかを、効果の測定を通じて判断するためです。 Elasticsearchでの実現方法 ZOZOTOWNでは、ユーザーが入力したキーワードからサジェストクエリを抽出する検索エンジンとしてElasticsearchを採用しています。サジェストクエリとしたい文字列をインデクシングし、その文字列に対して前方一致させることで実現しています。 ここではElasticsearchを用いた性年代別サジェストの実現方法について紹介します。 まず、Elasticsearchで Mapping と言われるドキュメント設定は以下のようにしました。この設定はインデクシング時に活用されます。 { " mappings ": { " dynamic ": " false ", " properties ": { " suggest ": { " type ": " keyword " } , ... " score ": { " properties ": { " mens_gene_20_29 ": { " type ": " float ", " index ": false } , ... " mens_all ": { " type ": " float ", " index ": false } , ... " all_gene_20_29 ": { " type ": " float ", " index ": false } , ... " all ": { " type ": " float ", " index ": false } } } } } } 主なポイントはサジェストの並び順を決定するための「score」フィールドにサブフィールドを追加し、性別と年代に応じたスコアを持たせた点です。 性別と年代に応じたスコアの詳細は以下の通りです。 性年代別でのスコア(上記mens_gene_20_29に該当) 性別でのスコア(上記mens_allに該当) 年代別でのスコア(上記all_gene_20_29に該当) 全体でのスコア(上記allに該当) 性年代別だけでなく、性別、年代別、全体でのスコアを持たせた理由は2つあります。1つ目はユーザーの性別と年代の両方の情報が取得できなかった場合に対応させるためで、2つ目はサジェストクエリのソートに活用するためです(後述)。 次に、Elasticsearchへのリクエストクエリは以下のようにしました。ここでは、検索窓にキーワードが入力された際に、サジェストクエリ候補に前方一致させフィルタリングしソートします。この例では、男性20代ユーザーが「Tシャツ」を入力した場合を想定しています。なお、下記のクエリは実際のクエリを簡略化しています。 { " query ": { " prefix ": { " suggest ": " Tシャツ " } } , " sort ": [ { " score.mens_gene_20_29 ": { " order ": " DESC " } , " score.mens_all ": { " order ": " DESC " } , " score.all_gene_20_29 ": { " order ": " DESC " } , " score.all ": { " order ": " DESC " } , } ], " size ": 10 } ここで工夫した点は、多段ソートをするようにした点です。上記の例では、20代男性 → 男性 → 20代 → 全ての順にソートしています。 以下はこの時の動作イメージです。 複数のサジェストクエリ候補間でユーザーの性年代でのスコアが等しい場合は、ユーザーの性でのスコアの高い方が上位になります。ユーザーの性年代と性でのスコアも等しい場合は、ユーザーの年代でのスコアの高い方が上位になります。こうすることで、なるべくユーザーに好まれやすいサジェストクエリが表示されるようにしました。 スコア算出に用いる性年代別のログは、性年代ごとにデータをフィルタリングするため、全体のログと比べてデータが少なくなるという欠点があります。多段ソートによって、性年代だけでなく、複数のフィルタリング条件を組み合わせたソートが可能になります。これにより、性年代でのスコアではクリックされやすさを十分に反映できなかったサジェストクエリに対しても、より多くのログから算出されたスコアを基に並び替えることができ、この欠点を補うことが可能です。 結果 ここでは定性的な結果とABテストの結果について紹介します。 定性評価 以下が改善前後でのサジェストの比較です。 20代男性 が検索窓に「Tシャツ」を入力した場合を想定しています。左側が改善前で右側が改善後のサジェストを示しています。 改善前は「Tシャツ レディース」などの女性向きのサジェストクエリが表示されていましたが、改善後では「Tシャツ メンズ」などの男性向きのサジェストクエリがより上位に表示されていることがわかります。 性別や年代を入れ替えて様々な条件でチーム内定性評価をしたところ、既存のサジェストよりも良い評価が得られました。 A/Bテスト 性年代別サジェストを評価するためにZOZOTOWNのユーザーに対して2週間A/Bテストを行いました。 以下が結果のサマリです。計測した指標は他にもありますが抜粋しています。 指標 結果 1ユーザー当たりの受注金額 100.20 % サジェストクリック率 100.21 % GMV相当の1ユーザー当たりの受注金額は有意差なしとなりましたが、サジェスト機能指標であるサジェストクリック率は有意差あり勝ちとなりました。 まとめ 本記事では、性年代別のサジェストを実現した事例を紹介しました。性年代別のサジェストを実現することで、サジェストを改善できました。 今後の展望として、性年代別でスコア算出ロジックを変更すること、さらにはユーザー一人ひとりにパーソナライズ化したサジェストを実現することを考えています。 おわりに ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
目次 目次 はじめに 我々のチームについて ZOZOMETRYについて ZOZOMETRYでのBtoB開発で取り入れたこと プールモデルによるマルチテナント管理 Cognito+DBによるユーザー情報の管理 RLSによる行単位でのデータアクセス制御 RLSの利用を見送った理由 理由1 : コネクションプールの管理 理由2 : O/RマッパーでのRLSの利用 DDDにおけるテナントのアクセス制御 MySQLを採用した理由 AWS Auroraとの互換性 PostgreSQL独自の機能の不使用 チームの経験と学習コスト 計測プロダクトとの整合性 PostgreSQLを採用したいケース Gitの運用フロー まとめ 最後に はじめに こんにちは。計測プラットフォーム開発本部バックエンドブロックの髙橋です。 先日、ZOZOMETRYという新規サービスをローンチしました。 corp.zozo.com 本記事ではZOZOMETRYをローンチするにあたり発生したBtoB開発における考慮すべきポイントと対応について解説します。 我々のチームについて 計測プラットフォーム開発本部バックエンドブロックでは、「世界中に計測技術を通じて、新しい価値をプラスする」をミッションとして掲げています。このミッションのもとZOZOMAT、ZOZOGLASS、ZOZOMETRYなどの計測プロダクトのバックエンド開発・運用をしています。主にScalaを使用し、堅牢で拡張性の高いシステムを目指しています。 ZOZOMETRYについて ZOZOMETRY とは、事業者の採寸業務を効率化し、採寸が必要な服の売上拡大やコスト削減に貢献するサービスです。以前、ZOZOTOWNで提供していたサービスでは、ZOZOSUITを着用しての計測が必須でしたが、ZOZOMETRYではZOZOSUIT着用あり、ZOZOSUIT着用なしの異なる計測方法が提供されています。 ZOZOMETRYでのBtoB開発で取り入れたこと 我々のチームでは、過去にBtoCサービスを開発・ローンチしており、BtoBサービスの開発経験はありませんでした。しかしZOZOMETRYは法人向けのBtoBサービスであり、BtoCサービスとは異なる課題がありました。特に契約企業ごとにマルチテナンシーなサービスを提供する必要のあるユースケースで我々が取り組んだ方法をご紹介します。 プールモデルによるマルチテナント管理 SaaS型のBtoBサービスでは、多くの場合、契約企業ごとにデータを分離する必要があります。これらの分離モデルとして、 サイロモデル・プールモデル・ブリッジモデル が知られています。 ZOZOMETRYのバックエンドシステムではローンチしたばかりのサービスである点からも、テナントごとに専用のリソースを用意することに関しては管理コストの増大につながる懸念がありました。そのためZOZOMETRYではテナントごとにAPIを用意することはせず、共通の1つのリソースでサービスを提供するためにプールモデルを採用しました。これにより、テナントごとに独立したリソースを用意することなく、複数のテナントを1つのリソースで運用できます。 Cognito+DBによるユーザー情報の管理 ユーザー認証はAWSが提供するCognitoを採用しています。外部サービスであるCognitoには認証に必要な最小限の情報(アカウントID、メールアドレスなど)のみを定義しています。各組織を区別するために必要なテナントIDも含めた、ユーザーの氏名などのmetadataはRDSに紐づける形で管理する方針としています。これにより、metadataの参照や更新が行われた場合にCognitoを経由する必要がなく、パフォーマンスや拡張性の観点から管理が容易となります。 CognitoのユーザープールとDBのテーブルの関係は以下のようになっています。CognitoのアカウントIDと、DB上のアカウントIDを紐づけることで、Cognitoのユーザー情報とDBのテナント情報を紐づけ、DB上のmetadataを取得しています。 また、この構造をユーザー認証にも用いています。Cognitoから発行されたアクセストークンのペイロードには、CognitoのアカウントIDがsub属性として含まれています。アプリケーション側では、アクセストークンからCognitoのアカウントIDを取得し、DB上のアカウントIDを取得してユーザーを特定します。 しかしプールモデルでは、上段で説明したように1つのリソースで全ての顧客のデータを保持し、1つのサーバーで運用するような形になります。そのため、リソースごとにテナントごとのデータ分離をアプリケーション側で意識する必要があります。我々はアプリケーション側でテナントIDを保持することで、テナント間のデータを分離した状態でサービスを提供することが可能になりました。 RLSによる行単位でのデータアクセス制御 プールモデルにおけるマルチテナント管理の手段としては一般的にいくつか存在しています。我々は当初、PostgreSQLが提供するRLS (Row Level Security)を利用する想定で設計を進めていました。 RLSはテーブルに対して行単位でアクセスを制御できる機能です。詳細は PostgreSQLのドキュメント をご覧ください。私たちのユースケースでは、RLSを利用して、テナントIDをセッション変数として保持し、テナントIDに紐づくデータのみを参照することを想定しました。これにより、以下のコードのようにテナント間のデータをアプリケーション側で意識することなく分離できると考えました。 -- テーブルの作成 CREATE TABLE users ( user_id SERIAL PRIMARY KEY, tenant_id INT , first_name VARCHAR ( 50 ), last_name VARCHAR ( 50 ) ); -- サンプルデータの挿入 INSERT INTO users (tenant_id, first_name, last_name) VALUES ( 1 , ' Alice ' , ' Smith ' ); INSERT INTO users (tenant_id, first_name, last_name) VALUES ( 2 , ' Bob ' , ' Jones ' ); SELECT * FROM users; /* user_id | tenant_id | first_name | last_name --------+-----------+------------+----------- 1 | 1 | Alice | Smith 2 | 2 | Bob | Jones */ -- テーブルにRLSを適用 ALTER TABLE users ENABLE ROW LEVEL SECURITY; -- ポリシーの作成 CREATE POLICY users_rls_policy ON users FOR SELECT USING (tenant_id = current_setting( ' service.tenant_id ' ):: int ); -- RLSを用いたSELECT SET LOCAL service.tenant_id = 1 ; SELECT * FROM users; /* user_id | tenant_id | first_name | last_name --------+-----------+------------+----------- 1 | 1 | Alice | Smith */ RLSの利用を見送った理由 しかし、我々は以下の理由からRLSを使ったテナントごとのデータ分離手法の採用を断念することにしました。以下に、その理由について説明します。 理由1 : コネクションプールの管理 PostgreSQLのRLSは、ポリシーでパラメーターから得られるテナントIDが一致する行を条件にしています。DBセッション内でテナントIDを設定することで、そのテナントに対するデータにアクセスが許可されます。しかし、コネクションプールを利用する場合、同じコネクションを使いまわすことで違うテナントのデータを参照してしまう可能性があります。 そのため、セッションごとにコネクションを張ることになり、コネクションプールの効率を悪くする懸念がありました。また、プールモデルでシステムを構成しているため、コネクションが枯渇した際に全てのテナントに影響が出る可能性もありました。 理由2 : O/RマッパーでのRLSの利用 RLSを用いる場合はO/Rマッパー側でセッション変数を設定する必要があります。しかしながらユーザーごとにポリシーを分けておらず、コネクションのポリシーを都度切り替える必要がありました。結果的にコネクションにテナントIDをアタッチするための生クエリを発行するコンポーネントを作らなければならず、カスタム実装が必要でした。 我々はプール型のシステムであり、共通のAPIサーバーを利用しています。そのため、ユーザーごとにポリシーをアタッチするような手法を用いるのが難しく、結果としてWHERE句にテナントIDを付加する手法と比較し、コストとリスクがあまり変わらないと判断しました。下図のように、テナントごとに異なるAPIサーバーを持っている場合であれば、RLSの効果的な利用が可能であったと考えています。 上記で触れたように、RLSを利用しない場合、内部的にはSQLクエリ上でテナントIDを指定する必要があります。これをエンドポイントごとに実装することなく、我々はアプリケーション側、実際はDDDにおけるユースケース層で定義し、実装することにしました。DDDにおけるテナントIDの取り扱いについては、以下に詳しく説明します。 DDDにおけるテナントのアクセス制御 DDDとは「Domain Driven Design」の略で、日本語では「ドメイン駆動設計」と呼ばれる設計手法です。DDDは、主にそのソフトウェアが対象とする領域(ドメイン)に焦点を当てて、それをソフトウェアに対して抽象化して適用し、その領域における問題を解決するための設計手法です。業務システムにおいて、ドメインとは主にビジネスロジックを表現するための概念です。バックエンドにおいては、システムのビジネスロジックを処理する主要な場所であることが多いことから、我々もシステムの設計にDDDを取り入れています。 そのため、我々はRLSが担うはずだった特定のテナントIDが割り当てられたデータのみのアクセス制限を行う処理を、共通に呼び出すビジネスロジックを集約するユースケース層に実装しました。これにより、実装者による実装漏れの懸念をほぼなくすことができます。ユースケース層に集約することにより、以下のメリットがあります。 コードの可読性と保守性の向上 ビジネスロジックが明確に分離されるため、テストの容易さが向上。コードの重複を避けることができる ユースケース層に集約されたビジネスロジックは、他のユースケースやアプリケーション層からも再利用可能 これらの点を考慮しながら、ユースケース層にビジネスロジックを集約し、DDDの原則に従った堅牢なアプリケーションを構築できます。 以下に、ユースケース層におけるテナントIDの取り扱いについてのコード例を示します。このコードでは、ユースケース層においてテナントIDを保持し、テナントIDに紐づくデータのみを参照する処理を実装しています。全てのユースケースがこのtraitをmixinすることで、チェック処理が漏れる懸念もありません。 trait UserUseCaseProtocol[Req <: UseCaseRequest[?], Res <: UseCaseResponse[?, ?]] { val userRepository: Repository[User] protected def execute(request: Req)(implicit ec: ExecutionContext): Future[Res] private def response(request: Req): Future[Res] = { for { _ <- ensureBelongingTo(request.userId, request.tenantId) res <- execute(request) } yield res } private def ensureBelongingTo( userId: Id[User], tenantId: Id[Tenant] )(implicit ec: ExecutionContext): Future[User] = { userRepository .resolveById(userId) .flatMap(ensureSameTenantId(_, tenantId)) } private ensureSameTenantId(user: User, tenantId: Id[Tenant]): Future[User] { if (user.tenantId == tenantId) Future.successful(user) else Future.failed(NotBelongingToTenantException(entity.id)) } } // --- Exception --- class NotBelongingToTenantException(userId: Id[User]) extends Exception このように、我々はPostgreSQLのRLSを使ったデータ分離を見送ることにしました。 MySQLを採用した理由 我々はZOZOMETRYでRLSを使わない意思決定をしたに過ぎず、このままPostgreSQLを使い続ける余地もありました。しかし、最終的に我々はPostgreSQLでの開発を断念し、MySQLを採用することに決定しました。その理由は以下の通りです。 AWS Auroraとの互換性 まず、我々はデータベースにAWSのAuroraを使用しています。AWSがAuroraの新機能をリリースする際、基本的にはMySQLファーストで行われます。現時点でも、クロスリージョンリードレプリカやマルチマスタークラスターなどはMySQLでしか対応されていません。 PostgreSQL独自の機能の不使用 次に、我々はhstore型などのPostgreSQL独自の型は使用しません。json型やhstore型については、RDBではなくドキュメントDBやKVSなど、本質的にそれらを扱うことに適したデータベースへ永続化するように分離します。また、今まで説明していたようにRLSも利用しないため、PostgreSQLの独自機能を使うメリットが少ないと判断しました。 チームの経験と学習コスト さらに、チームはこれまでのMySQLでの開発・運用経験を再利用可能です。新たにチームにジョインする人にはMySQLの知識が期待されます。MySQLを初めて扱う人には、例としてMySQLの固有なネクストキーロックなど、学習コストがかかりますが、チームとしての技術スタックを統一できます。 計測プロダクトとの整合性 最後に、我々のチームでは計測プロダクトの開発・運用にMySQLを採用しています。PostgreSQLの採用は計測プロダクトの中でもイレギュラーな技術スタックの決定となります。また、Auroraを使う上であってもMySQLを利用するメリットが大きいため、ZOZOMETRYでもMySQLを他の計測プロダクト同様に使用することにしました。 PostgreSQLを採用したいケース 一方で、PostgreSQLを採用したいケースもあります。例えば、複雑なクエリのパフォーマンス向上を図りたい場合や、hstore型などPostgreSQLにのみ存在する独自の型を使いたい場合です。しかし、我々のチームではこれらに遭遇するケースが少ないため、最終的にMySQLを採用することに決定しました。Auroraを使用する以上、PostgreSQLで機能の制限を受けることは避けたいと考えておりました。また、バックエンド側ではデータベースでJOINを行わない方針で開発を進めています。これは、データベースはアプリケーション層に比べてスケールが難しく、データベースに多くの仕事をさせないためです。 Gitの運用フロー ZOZOMETRYでは、リリースのタイミングや内容の管理が重要です。以前記事として出したように、チームで開発・運用しているプロダクトではGitHub Flowを採用し、以下のように運用しています。 techblog.zozo.com ZOZOMETRYでは定期リリースを採用することが決まったこと、BtoB向けのプロダクトであり変更に伴う顧客説明が必要となることから、定期リリースにおける最適なブランチ戦略を見直す必要があります。我々はGitHub Flowをベースに、releaseブランチを付加した運用フローを採用しました。これにより、リリースのタイミングや内容を管理し、リリース前に必要な調整ができます。 まとめ 本記事では、ZOZOMETRYのBtoBサービスにおける課題とその解決策について紹介しました。BtoBサービスではBtoCサービスとは異なる課題があり、それに対応することが求められますが、マルチテナンシーなサービスをプールモデルで提供し効率的なサービス提供を実現しました。また、アプリケーション側でテナントIDを保持することで、テナント間のデータを分離した状態でサービスの提供が可能になりました。引き続き、計測技術を用いて新しい価値を提供するために、技術的な課題に立ち向かいながらサービスの開発を進めていきます。 最後に 計測プラットフォーム開発本部バックエンドチームでは、グローバルに計測技術を開発していくバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co
アバター
はじめに こんにちは、データシステム部推薦基盤ブロックの寺崎( @f6wbl6 )と佐藤( @rayuron )です。 私たちは2024年10月14〜18日にイタリアのバーリにて開催されたRecSys 2024(18th ACM Conference on Recommender Systems)に現地参加しました。本記事では現地でのワークショップやセッションの様子をお伝えすると共に、気になったトピックをいくつか取り上げてご紹介します。 RecSysとは RecSysとは米国計算機学会(ACM)が主催する推薦システムに関する国際的なカンファレンスです。今回で18回目の開催となるRecSys 2024は2024年10月14〜18日にイタリアのバーリで開催されました。 recsys.acm.org RecSysでは推薦システムに関わる各国の大学の研究チームや、Google、Amazon、Netflix、Spotifyをはじめとする推薦の関連分野で活動する世界有数の企業を集め、推薦システムの幅広い分野における新しい研究成果を発表します。今回は、アメリカや中国などの53カ国から1,123人の研究者や開発者がRecSysに参加しました。 初日と最終日にチュートリアルとワークショップがバーリ工科大学で行われ、その他の日付で ペトゥルッツェッリ劇場(Teatro Petruzzelli) を会場としてメインカンファレンスが行われました。全日程を通して発表は全て英語で行われます。どの日も9:00頃からカンファレンスが開始され、朝夕2回のコーヒーブレークやランチを挟んで18:30頃に終了します。夜にはレセプションパーティーやソーシャルディナーが実施されたりと多くの人と交流できます。また今回は、メインカンファレンス終了後にオーケストラコンサートにも参加できました。 開催地のバーリについて RecSys 2024の開催地であるバーリは、アドリア海に面した南イタリアの美しい港町 今回の開催地であるバーリは、アドリア海に面した南イタリアの美しい港町です。街には白を基調とした外壁の建物が多く、青い海や空とのコントラストが印象的で魅力的でした。コーヒーブレークの合間に沿岸沿いを散策する参加者の姿が多く見られ、気分をリフレッシュしている様でした。 会場の様子 チュートリアルとワークショップの会場となったバーリ工科大学 初日と最終日には、チュートリアルとワークショップがバーリ工科大学で行われました。発表テーマは教室によって分かれているため、聞きたいプログラムを自分で選択し聴講しました。特にワークショップでは MuRS や Video Recsys の様に音楽や動画という特定のドメインに特化したテーマの他、 FAccTRec や CARS のように推薦を考える上で重要な観点に重きをおいたテーマでの発表がありました。 メインカンファレンスの会場は荘厳な雰囲気のペトゥルッツェッリ劇場 10/15〜10/17にメインカンファレンスがペトゥルッツェッリ劇場で行われました。荘厳な雰囲気が魅力的な会場でした。劇場の椅子に座り全員が同じ基調講演とセッションを聴講しました。 Michael I. Jordan 氏をはじめとする研究者が基調講演し、セッションでは大学の研究チームの発表やGoogleをはじめとする会社の研究チームの発表が行われました。セッションは以下のテーマで構成され、全体を通してLLMとSequential Recommendationに関する発表が多い印象でした。 Large Language Models Bias and Fairness Collaborative Filtering Cross-domain and Cross-modal Learning Multi-Task Learning Cold Start Sequential Recommendation Graph Learning Optimisation and Evaluation Robust RecSys Off-Policy Learning Women in RecSys 研究内容の紹介 ここからは、カンファレンスを通して特に気になった論文について取り上げてご紹介します。 Bootstrapping Conditional Retrieval for User-to-Item Recommendations Hongtao Lin, Haoyu Chen, Jaewon Yang, Jiajing Xu Hongtao Lin氏らによる Bootstrapping Conditional Retrieval for User-to-Item Recommendations のFigure 1より引用 この発表はPinterestの研究で、Two-Towerモデルによるretrievalタスクにおいて条件付けして取得するアイテムを制御する方法を提案しています。以降、条件付けしたretrievalのことを”conditional retrieval”と記載します。conditional retrievalの実現方法として大量のアイテムを取得して後処理で条件に一致するアイテムのみを抽出する方法や、近似近傍探索時にフィルタ条件を考慮する方法が考えられます。これらの方法だとフィルタリング項目に対してモデルが最適化されているわけではないため、フィルタリング項目との関連度は高いがユーザーとの関連度は低い、といった状況が生じると考えられます。 この研究ではアイテムのメタデータを特定のフィルタリング項目にマッピングするCondition Extraction Moduleという機構を設け、そこから得られたembeddingをユーザータワーで利用する手法を紹介しています。 提案手法は特定のトピックで抽出したアイテムをメール通知またはプッシュ通知するタスクで評価しており、複数の手法と比較しています。 手法 概要 index 指定されたトピックのアイテムを全件抽出して人気順の上位N件を取得するもの LR: Learned Retrieval 通常のTwo-Towerモデル + 内製のストリーミングトピックフィルタ*を適用したもの CR1: Conditional Retrieval(提案手法 A) 内製のストリーミングトピックフィルタを適用しないもの CR2: Conditional Retrieval(提案手法 B) 内製のストリーミングトピックフィルタを適用したもの *指定したトピックのアイテムを一定数取得または時間予算に達するまで取得するトピックフィルタの機構。 オンラインテストの結果、提案手法がCTR・コストの両方で優れていることが示されています。注目すべきポイントはインフラコスト面で、LRにストリーミングトピックフィルタを適用することでコストが大幅に増加している点に対し、CRのコスト増はLRの4〜6分の1程度に抑えられていました。これはLRの場合だと条件に合致するアイテムを大量にフェッチして処理する必要があるためで、この点から提案手法では条件に合致するアイテムを効率的に取得できていることがわかります。 発表時にはlimitationとして複数の条件によるconditional retrievalが行えないことや、必ずしも条件に合致したアイテムが取得できるわけではない点を挙げており、検索機能の代替にはならない点に言及していました。 感想・考察 こちらの手法はサービスのユースケース次第では簡単にconditional retrievalを実現できるため、非常に参考になる発表でした。クエリ実行時にアイテムのconditionにあたるembeddingをユーザーのfeatureとして入力するという発想はシンプルなので、これでconditionに沿ったアイテムが取得できるようになるのは意外な結果です。比較対象として用いているTwo-Tower+トピックフィルタはよくあるconditional retrievalの構成なので、似たような構成のシステムを運用している方は参考にしてみると良いでしょう。 Short-form Video Needs Long-term Interests: An Industrial Solution for Serving Large User Sequence Models Yuening Li, Diego Uribe, Chuan He, Jiaxi Tang, Qingyun Liu, Junjie Shan, Ben Most, Kaushik Kalyan, Shuchao Bi, Xinyang Yi, Lichan Hong, Ed Chi, Liang Liu この発表はGoogleとDeep Mindの研究で、YouTubeのショート動画など尺の短い動画コンテンツ(Short-Form Videos, SFVs)を大量に消費するサービスにおいてユーザーシーケンスを効率的に扱う方法を提案しています。SFVsでは尺の長い動画(Long-Form Videos, LFVs)と異なりユーザーシーケンスが長くなる傾向にあるため、モデルをサービングする際にどの程度のシーケンス長を考慮するかがポイントになります。シーケンス長をできるだけ長く扱うようにしたところオンラインメトリクスは大幅に向上したものの、サービングコストの増加が確認されたため、できるだけこれらの影響を小さくすることがこの研究のモチベーションです。なお論文中には記載がありませんが、使用するユーザーシーケンスを長くすることでモデルサービング時のレイテンシも悪化した、と発表中に言及されていました。 Online Metric Serving Cost (naive) User Model (sequence length 200) +0.14% +5.6% User Model (sequence length 1000) +0.38% +28.7% 使用するユーザーシーケンスを長くするとメトリクスは改善するがコストやレイテンシが悪化している。 提案手法はユーザーシーケンスをembeddingにするモデルの推論処理をサービングから切り離して非同期に行うというもので、embeddingのキャッシュと更新手続きの手順をフレームワークとして提案しています。 Yuening Li氏らによる Short-form Video Needs Long-term Interests: An Industrial Solution for Serving Large User Sequence Models のFigure 1より引用 このような構成を取るメリットとしてサービング時のインフラコスト改善だけでなく、ユーザーシーケンスをembeddingにするモデルをLLMなどの大規模モデルにできる点を挙げています。特にユーザーシーケンス長の限界を意識する必要がなくなるので、ユーザーの長期的な嗜好を捉えられるようになる点が大きなメリットと言えるでしょう。 embeddingのキャッシュと更新の手続きは以下のような流れで行われます。 キーバリューストアからユーザーIDに対応するembeddingを取得する embeddingが有効な場合はそのまま返却 embeddingが無効や期限切れだった場合はembeddingのリフレッシュ処理をトリガー 最新のユーザーシーケンスを取得してユーザーモデルでembeddingを計算しキーバリューストアに格納・embeddingを返却 Yuening Li氏らによる Short-form Video Needs Long-term Interests: An Industrial Solution for Serving Large User Sequence Models のFigure 2より引用 提案手法の評価としてSFVsの推薦タスクとLFVsの推薦タスクでA/Bテストをしており、SFVsではインフラコストの大幅な削減を達成し、LFVsでは一部のメトリクス改善につながったと報告しています。この発表の質疑では「ユーザーのembeddingが無効であるとどのように判断しているのか」という質問が出ており、一定期間で無効にする方法とユーザーの属性やコンテキストの変化で無効にする2つのパターンがあるとのことでした。 感想・考察 まず、YouTubeのような巨大サービスにおけるembeddingの扱いに関する方法などの詳細を直接聞けるのがRecSysに参加する大きな意義だと感じました。ZOZOTOWNでも似たようなシステム構成を採っている推薦システムは存在しますが、本発表でのembeddingの管理方法はさすが痒いところまで手が届いている、という印象です。YouTubeほどのサービス規模になるとインフラコストの削減やレイテンシの改善によるインパクトは計り知れず、メトリクスが改善していてもインフラコストがかかりすぎてリリースができないというのはこの規模のサービスならではの課題だと思います。 MARec: Metadata Alignment for cold-start Recommendation Julien Monteil, Volodymyr Vaskovych, Wentao Lu, Anirban Majumder, Anton van den Hengel この発表はAmazon Machine Learningによる研究で、ウォームアイテムに対する精度を保ちつつコールドアイテムの精度を改善する手法を提案しています。推薦システムにおけるコールドアイテムとは「ユーザーのインタラクションが少ない・もしくは全くないアイテム」を指しており、こうしたアイテムへのアプローチとして以下が挙げられています。 協調フィルタリングとコンテンツベース推薦のハイブリッド メタラーニングアプローチ ニューラル埋め込みアプローチ retrieval拡張 よく使われるのはひとつ目の手法ですが、コンテンツベース推薦のアプローチでコールドアイテム、協調フィルタリングでウォームアイテムへの推薦に対応しているため、モデルアーキテクチャが複雑かつ学習が収束しにくくなるという問題に繋がります。またそれぞれのモデルに対する影響もあり、ウォームアイテムへの推薦精度を悪化させるケースもあります。この研究ではウォームアイテムへの精度を変えずにコールドアイテムへの推論精度を高めつつ、よりスケーラブルなアプローチを提案しています。 提案手法は M etadata A lignment for cold-start Rec ommendation (MARec)と呼ばれるもので、バックボーンモデル、embeddingモデル、アライメントモデルの3つのモデルを組み合わせたアーキテクチャを採っており、それぞれのモデルの出力を使って損失を計算する構成となっています。 Julien Monteil氏らによる MARec: Metadata Alignment for cold-start Recommendation のFigure 1より引用 バックボーンモデルは図中のfBで表されているモデルでクリックデータを入力としており、図中のfEで表されているembeddingモデルはアイテムのメタデータをembeddingに変換します。そしてfAで表されているアライメントモデルはfBの学習で使用するクリックデータとfEから得られるembeddingのバランスをとるためのもので、ここが提案手法のキモになっています。 提案手法の評価は公開データセットに対するオフライン評価のみですが、コールドアイテムでの評価指標が最大47.9%アップリフトしており、一方ウォームアイテムでは最大マイナス1.5%の精度低下に抑えられていることが確認されました。 感想・考察 今回のRecSysではコールドスタート問題に関する発表が数多くありましたが、手法が最もシンプルかつ広範に渡る評価を行っている発表だったと思います。「コールドアイテムへの対策をしたモデルはウォームアイテムへの精度を悪化させる懸念がある」という観点も説明されると納得ですが自分たちのプロダクトで考慮できていなかった点なので、新たな観点として得られたのが個人的に良かったポイントです。課題として挙げていた「アーキテクチャが複雑化する」という点について提案手法も複雑そうには見えますが、中身としてはアライメントモデルを追加しているのみなので比較的簡単にこの手法を試せそうな印象です。今後、プロダクトへの導入結果が報告されるのを楽しみにしている研究のひとつです。 Building a Scalable, Real-time Sequence and Context-Aware Ranking Marjan Celikik, Jacek Wasilewski*, Ana Peleteiro Ramallo, Alexey Kurennoy, Evgeny Labzin, Danilo Ascione, Tural Gurbanov, Géraud Le Falher, Andrii Dzhoha and Ian Harris Marjan Celikik氏らによる Building a Scalable, Real-time Sequence and Context-Aware Ranking のFigure 1より引用 こちらの研究は、 CARS というワークショップで取り上げられた、ファッションECサイト「Zalando」の発表内容です。 オンラインショッピングサイトでは、膨大な商品データベースから、ユーザーの行動やコンテキストに基づきリアルタイムでパーソナライズされた商品を効率的に推薦することが重要です。本研究では、2ステージの推薦モデルを採用し、推薦精度とレイテンシの両方を向上させるアプローチが取られています。 候補生成フェーズでは、Two-Towerモデルが使用されています。このモデルは、ユーザーとアイテムの特徴を独立して処理し、ユーザーの行動履歴やコンテキストを基にユーザーembeddingを生成し、アイテムのembeddingと組み合わせて商品候補を生成します。embeddingの生成方法としては、以下の3つが提案されています。 RCGntr : 事前学習されたembeddingを使用するモデル RCGtr : 事前学習されたembeddingを初期値として、学習されたembeddingを使用するモデル RCGtr+ctx : RCGtrに加え、検索クエリや閲覧カテゴリなどのコンテキスト情報を組み込んだモデル RCGtrは、従来のGradient Boosting Treesを使用した候補生成と比較して4.48%のエンゲージメント向上を達成しています。 ランキングフェーズでは、ユーザーの行動履歴とコンテキストに基づいて、クリック、カート追加、購入といった複数のアクションを予測するポイントワイズのマルチタスク学習が採用されています。学習時にはすべてのターゲットアクションに対して等しい重み付けが行われますが、サービング時にはユースケースに応じて各アクションのスコアに動的な重み付けが適用され、ビジネスニーズに合わせて最適化されています。 上記のモデルは、従来のWide & Deep Learningモデルと比較して4.04%のエンゲージメント向上を実現しました。また、ランキングモデルの候補生成にRCGtr+ctxを使うと、RCGtrと比較して+2.40%のエンゲージメント向上を実現しました。そして、システム全体では、リアルタイムで約200ミリ秒のレイテンシを維持しています。 感想・考察 ランキングフェーズにポイントワイズなマルチタスク学習を適用しており、学習とサービング時に異なるタスクごとに異なる重みを使用する点がユニークでした。重みはユースケースに応じて動的に設定されると言及されていたので具体的な決め方について知りたいと思いました。 Dynamic Stage-aware User Interest Learning for Heterogeneous Sequential Recommendation Weixin Li, Xiaolin Lin, Weike Pan and Zhong Ming Weixin Li氏らによる Dynamic Stage-aware User Interest Learning for Heterogeneous Sequential Recommendation のFigure 2より引用 こちらは、Session 8: Sequential Recommendation 1で発表された、深圳大学の研究です。従来の推薦システムは、ユーザーの行動履歴に基づいて商品を提案していましたが、ユーザーの興味が時間の経過や特定の行動によって段階的に変化することを十分に反映できていませんでした。この課題に対処するため、DSUIL(Dynamic Stage-aware User Interest Learning)という新しいモデルを提案しました。DSUILは、ユーザーの行動シーケンスを「購入」などの重要な行動を基準に複数の段階に分割し、各段階でユーザーの興味がどう変化するかを学習します。 DSUILは、以下の4つの主要なモジュールから成り立ちます。 Dynamic Graph Construction : ユーザーの過去の行動に対し購入行動を境界としてサブグラフを作成する Dynamic Graph Convolution : 各サブグラフ内のアイテム間の依存関係を学習する Behavior-aware Subgraph Representation Learning : 閲覧や購入など異なる行動間の依存関係を捉え、サブグラフ内のユーザーの興味を表現する Interest Evolving Pattern Extractor : 複数のサブグラフを結合し最終的なアイテムを予測する 実験結果から、DSUILは既存の最先端手法と比較して優れた性能を示し、特に異なる段階間の依存性をモデル化することが、推薦精度の向上につながることを示しています。 感想・考察 消費者行動モデルのAISAS(Attention, Interest, Search, Action, Share)で説明される様に、消費者行動が段階的に変化することは明らかだと思います。この研究は従来のSequential Recommendationで考慮し切れていなかったユーザーの段階的な行動をモデリングする点で筋が良いなと思いました。 Self-Auxiliary Distillation for Sample Efficient Learning in Google-Scale Recommenders Yin Zhang, Ruoxi Wang, Xiang Li, Tiansheng Yao, Andrew Evdokimov, Jonathan Valverde, Yuan Gao, Jerry Zhang, Evan Ettinger, Ed H. Chi and Derek Zhiyuan Cheng Yin Zhang氏らによる Self-Auxiliary Distillation for Sample Efficient Learning in Google-Scale Recommenders のFigure 1より引用 こちらは、Session 11: Optimisation and Evaluation 1で発表されたGoogle DeepMindによる研究です。こちらの研究は、Googleの大規模な推薦システムにおいて、限られたデータから効率よく学習を進めるSelf-Auxiliary Distillationという手法を提案しています。 推薦システムでは、フィードバックデータをそのまま使用してモデルをトレーニングしますが、これらのラベルの情報価値は均一ではありません。例えば、クリック予測モデルでクリックされずにラベルを0とした中でも、ポジティブに近いネガティブや、完全なネガティブが存在します。そのため、否定的なラベルを単純に0とするのではなく、もっと細かく評価することが有効とされました。 Self-Auxiliary Distillationは、信頼性の高いポジティブラベルに重点を置いて学習を進めつつ、信頼性の低いネガティブラベルに対しては蒸留を通じて解像度を高め、モデル全体の精度を向上させる手法です。この方法では、次の2つのタスクを同時に処理します。 Main Task : ground-truthに基づいてモデルをトレーニングし、教師として確率的なソフトラベルを生成する Auxiliary Task : 教師モデルから生成されたソフトラベルと正解ラベルの両方を学習する この手法を使用することで、あるGoogle Appsの推薦システムではオフラインでのAUCが+17%向上し、オンラインの主要ビジネスメトリクスにおいても大きな成果が見られました。また、補助タスクを追加することで、モデルのサービング時のコストが増大することはなく、トレーニングにかかるコストもほとんど増加しません。 AppleのiOSのプライバシーポリシーによってラベルデータの取得が難しくなった環境でも、この手法が有効に機能しています。同意を得られないユーザーのデータが「真のネガティブラベル」と区別できない問題に対して、補助タスクで生成されたソフトラベルを使用することで、推薦システムのパフォーマンスを維持することに成功しました。 感想・考察 pseudo-labelを使用することで、ユーザーの潜在的な関心度を学習に取り入れている点は、純粋に賢いアイデアだと感じました。プラットフォームのプライバシー規制に対して適応力があるという点は面白い観点だと思いました。他にもbotアクセスによる推薦精度の低下への対応策にもなり得るのではないかと考えました。 おわりに RecSys 2024に参加して、豊富なインスピレーションを得ると共に自社の推薦機能に改善の余地があることを再確認しました。前述の通り今年のRecSysはSequential Recommendationの発表が多く、発表中の課題設定としてECでの購買行動を扱っているものも多かったためZOZOTOWNにおける推薦機能の改善にそのまま活かせそうな内容ばかりでワクワクしました。Industrial Paperの発表はサービス特有の課題設定を解くものでしたが、課題設定の観点とそのアプローチは自分たちがプロダクトを改善していく上でとても参考になります。 RecSys 2025はチェコのプラハで開催! RecSys 2025はチェコのプラハで開催されるとのことで、自分たちも発表者の立場として参加できるように推薦システムをアップデートしていきたいと思います。 ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
はじめに こんにちは、計測システム部SREブロックの @TAKAyuki_atkwsk です。普段は ZOZOMAT や ZOZOGLASS 、 ZOZOMETRY などの身体計測サービスの開発・運用に携わっています。 最近公開されたZOZOMETRYですが、正式ローンチに至るまでにチーム間のサイロ化によるデリバリー速度の低下という課題が見えてきました。そこで、モブプログラミング(モブプロ)を通してチーム間のコラボレーションを促進し、課題の解決を試みている事例をご紹介します。 目次 はじめに 目次 背景・課題 モブプロを試してみる 私たちのモブプロのやり方 事前準備 モブプロの流れ モブプロをやってみて 改善点 まとめ 背景・課題 私の所属する計測システム部では以下の組織図の通り、SRE、バックエンド、フロントエンド、研究開発の4つのチームが存在しています 1 。さらに、複数のプロジェクトが並行して進むことが多く、一人がプロジェクトを兼任することもあります。 ( ZOZOエンジニア向け会社説明資料 より引用) 各チームは役割によって責務が分けられていますが、SREチームとバックエンドチームの間で領域が重なる部分や依存する部分を進める際に上手くいかないことがありました。例えば、私たちはアプリケーションのソースコードとCloudFormationテンプレートをそれぞれ別のリポジトリで管理しています。前者はバックエンドチームが管理し、後者はSREチームが管理しています。ここで、バックエンドのメンバーがちょっとしたAWSリソースの追加や修正する場合にSREのメンバーに依頼する状況となっていました。また、逆のパターンでSREのメンバーからバックエンドのメンバーにちょっとしたソースコードの修正を依頼することもありました。 これらの出来事は、各チームが責務通りに作業しているとも捉えられますが、メンバー自身が作業したいにもかかわらずお作法や考え方が分からないために依頼した方が速そうという判断が一因になっていました。依頼することで結果的に作業待ちが発生し、ちょっと試して確認したいだけなのに時間が掛かってしまうことがありました。 冒頭で書いた「サイロ化によるデリバリー速度の低下という課題」の一例を取り上げました。サイロと言うには大げさかもしれませんが、両チーム間で実装・設計方針が揃っておらず大きな手戻りが発生したこともあり、コミュニケーションとコラボレーションの不足が要因としてあったと考えられます。 モブプロを試してみる チーム間のコミュニケーションとコラボレーションを促進するための方策の1つとして私たちはモブプロを実施することにしました。実はSREチーム内では以前からモブプロを取り入れていたので私自身は経験がありました。SREチームのモブプロでは、なかなか取り組めなかった運用改善系のタスクを複数人で一気に取り組むことや、メンバーの技術力の底上げや新規参画者に対するナレッジの共有を目的にしていました。これらについてはある程度効果が出ていました。 ただ、チームをまたいだモブプロの経験はほとんど無かったため、まずは特定のプロジェクトもしくはある程度の機能で試したいと考えました。その頃、ZOZOMETRYではクローズドローンチ期間を設けていくつかの企業にサービスを提供していました。この運用の中で、頻度は低いものの開発者がSQLを実行する必要のある作業が顕在化してきました。この一部はスクリプト化されていますが、手動でSQLを実行するのは属人性が高く人為的なミスの起きやすいものとなっています。そこで、権限を持つメンバーなら誰でも安全に作業できるよう、管理画面を作ることになりました。 さらに、メンバーと話して以下のことからモブプロの題材として管理画面が適していそうだと判断しました。 緊急性の高い要件が発生しにくい 並行するプロジェクトがある中で着実にタスクを進めたい 複数のチームで協調して作業する必要がある これでチームをまたいだモブプロを試していく準備が整いました。 私たちのモブプロのやり方 管理画面はいくつかのコンポーネントに別れていて、大まかにはフロントエンド、バックエンド(API)、これらを動かす基盤(AWSやKubernetesリソース)となります。コラボレーションを意識してAWSやKubernetesリソースの作成やCI/CDの整備、認証などの機能開発を中心にモブプロを行いました。 参加メンバーはSREメンバー1名(私)、バックエンドメンバー2〜3名、フロントエンドメンバー1名です。各メンバーはリモートワークしているので、以下のように音声と画面を共有しながら作業します。 SlackのhuddleもしくはGoogle Meetで音声・映像を同期、画面共有 Visual Studio Codeの Live Share でソースコードを共有 コミットに Co-authored-byトレーラー を付けてモブプロ参加者を共同作成者とする モブプロは1回1時間の枠で週2回のペースで実施しています。この後の「改善点」のセクションで触れますが、複数のプロジェクトに参画しているメンバーもいるためなかなか空いている時間が見つけられず、このような時間枠としています。 事前準備 モブプロの事前準備として取り組むテーマとオーナーを決めておきます。実際に「オーナー」と呼ぶことはありませんが、説明のため便宜的にそう呼ぶことにします。オーナーはテーマ、ゴール、具体的にやること、参考資料などを社内ドキュメントツールとして利用するコンフルエンスに簡単にまとめておきます。内容の例を以下に示します。 ## テーマ - ZOZOMETRY管理画面 フロントエンドのCI整備 ## ゴール - ECRリポジトリとIAM Roleが存在すること - GitHub ActionsワークフローでWebサーバーのイメージがビルドされ、ECRリポジトリにpushされていること ## 前回のおさらい - 前回のモブプロまとめページのリンク - 前回作成したプルリクエストのリンク ## やること - ECRリポジトリとIAM Roleの作成 - 作業するGitHubリポジトリ名: xxx - ECRリポジトリの名前: xxx - IAM Role: 既存のIAMRoleXxxを参考にする - GitHub Actionsワークフローの作成 - 作業するGitHubリポジトリ名: xxx - mainブランチへのマージ(push)をトリガーにする - 注意事項 - xxx - yyy - 別リポジトリの参考になるワークフローのリンク このように作業についてまとめておくことで、メンバーには効率的に共有できて認識も揃いやすくなると考えています。あまり時間を確保できない中でメンバーの理解を深めることにおいて、私たちなりの工夫したポイントになります。 モブプロの流れ ここからはモブプロ1回分の流れを説明します。 まず、冒頭の5〜10分でオーナーが先ほど紹介した事前準備のコンフルエンスを使い、今回のテーマ、ゴール、前回のおさらい、今回やることを共有し、参加メンバーの認識を揃えておきます。 このあと40分程度を実装の時間に費やします。一般的なモブプロと同様にタイピストとモブに分かれて作業を進めていきます。その日のゴールによっては慣れていないメンバーがタイピストになるようオーナーが調整することもあります。 最後の5分でふりかえりを行い、良かった点と改善できそうな点をメンバーで話し合います。以上がモブプロ1回分の流れとなります。 モブプロの実施スタイルはチームによって細かい部分が異なるので、社内の他のチームの事例も参考にしてみてください。 techblog.zozo.com モブプロをやってみて モブプロ内でのふりかえりの他に、SREとバックエンド互いの領域に対する習熟度についてのアンケートを実施しました。これらの内容を元にモブプロの効果について述べていきます。 モブプロに参加したメンバーの感想をいくつか紹介します。 互いの守備範囲の技術スタックの理解につながっていると思います Kubernetesの操作やインフラリソースの作成などで出来ることが増えたことが一番のグッドポイントでした! (管理画面の認証における)SSO設計についてみんなで話すのが楽しかったです まず、お互いの領域に対する技術的な理解が増したというような感想が寄せられました。また、以下のアンケート回答結果においてもできることが増えているのが分かります。A-1からB-7までの項目はお互いの領域における作業に対応しており、Aがつくものはバックエンドメンバーに対して、BがつくものはSREメンバーに対する質問です。 2 例えば、「Kubernetesリポジトリでリソースの追加や修正を環境に応じて実施できる」「ローカル環境でAPIサーバーを起動できる」といった質問です。 それから、2つ目の感想を残してくれたバックエンドメンバーは、モブプロ開始後に別のプロジェクトでKubernetesリソースの作成や変更のプルリクエストを出していました。これは、背景・課題の部分で述べた、ちょっとした修正でも依頼して作業待ちになってしまうことへの解消に繋がると考えています。 また、私にとっては不慣れな領域でも参加メンバーの知識を組み合わせながら実装できて達成感を得られましたし、実装中に質問されることで自分自身の理解しきれていない部分が改めて分かり学びになりました。 さらに、このモブプロの様子を聞いた他のメンバーが別の組み合わせでモブプロやモブ作業を実施するようになり、コラボレーションの輪が広がったと感じました。また、障害対応の場面で、互いにシステムの解像度が高まったことやメンバーの得意を知れたことで以前よりも協力できるようになりました。 改善点 概ねうまく実施できていますが、改善したいこともあります。それは、モブプロの時間の長さについてです。現在は1回の枠は1時間で週に2回実施しています。しかし、1回あたりの時間枠が短いことで、あともう少しで実装しきれたのに時間切れ、リズムが出てきて盛り上がってきたところで時間切れ、となることがあります。ですので、今後は1回あたり2時間程度 3 の時間枠を用意できるようにしたいと考えています。 まとめ 本記事ではモブプロを通してチーム間のコラボレーションを促進させた事例を紹介しました。どの組織にもあてはまるものではありませんが、チーム間でコミュニケーションを改善したい、協力体制を築いていきたいと検討している方がいれば、ぜひ参考にしてみてください。今後もモブプロをきっかけとしてメンバーや他のチームとコラボレーションして、ZOZOMETRYを始めとしたサービスを改善していきたいと考えています。 また、この記事以外にもZOZOMETRYに関する記事を連載しておりますので、興味のある方はぜひご覧ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com この記事を書いている2024年9月末時点 ↩ A-4、A-5、B-7の質問はモブプロ実施後に行ったアンケートで追加されたものです。 ↩ 『 モブプログラミング・ベストプラクティス ソフトウェアの品質と生産性をチームで高める 』(2.4モビングインターバル)にはモビングセッションと呼ばれる、人々が1つのグループとしてモビングする継続的な時間は通常数時間に及ぶと書かれています。 ↩
アバター
はじめに こんにちは。計測プラットフォーム開発本部SREの纐纈です。最近はZOZOMETRYという法人向け計測業務効率化サービスの開発・運用に携わっています。今回ZOZOMETRYが正式に公開されることとなったので、合わせてこの記事を書くことになりました。 biz.zozometry.com 現在はパフォーマンスも良好なZOZOMETRYですが、ローンチ当初はパフォーマンスが良くなく、改善のために様々な検証をしました。今回はその経緯と検証内容を中心に、ZOZOMETRYのパフォーマンス改善についてお話ししたいと思います。 一般的にも、Lambdaを使ったサーバーレスアーキテクチャの構築やSQSを使った非同期処理の設計など、参考になる部分があるかもしれません。ぜひ最後までお付き合い頂ければ幸いです。 ZOZOMETRYの機能と構成紹介 まずは、ZOZOMETRYの機能と構成について簡単に紹介します。 ZOZOMETRYは、アプリでスキャンした体型データを元に、様々な体の部位の計測を可能にしています。計測部位に関しては、ユーザーの希望によって任意の組み合わせが可能です。部位の計測は、ZOZOグループの海外子会社が開発するSDKによって行われます。 これまでの計測サービスでは、このSDKをアプリに組み込んで計測していました。しかし、計測したい部位が企業によって異なるため、計測箇所を柔軟に変更できるようにする必要がありました。そのため、アプリで計測して、計測データをサーバーにアップロードし、サーバー側で計算する仕組みを導入しました。以降、このSDKを内包したサーバーのことをSDKサーバーと呼びます。以下、構成の簡略図です。 この図でのAPIサーバーは組織に登録されている計測部位のリストを取得し、計測値計算リクエスト後データベースに保存しています。これを計測値保存APIと呼びます。また、初期段階ではこれらの計測値保存APIやSDKサーバーは、K8s上のPodとして動作していました。SDKサーバーには計測値計算APIからのPod間通信でリクエストが送られる想定でした。 データの流れも補足すると、アプリで計測したデータはS3にアップロードされます。この計測データと前もってS3に格納されている計測箇所ごとのデータを元に、SDKサーバーによってそれぞれの計測箇所ごとの計測値を計算します。その後、その計測値データはデータベースに保存され、APIを通じて取得可能となります。 この構成を取ったことで、選択対象となる計測箇所の追加や更新が容易になり、複数の組織に対して異なる計測部位の提供がしやすくなりました。しかしながら、この構成によって計測値を計算するというサービスのコア機能にパフォーマンスの問題が発生してしまいました。次に、その経緯についてお話しします。 開発段階でのパフォーマンス ZOZOMETRYは正式に公開する以前に、クローズドローンチ期間を設け、いくつかの企業に提供していました。しかし、その限定的ローンチの直前に、パフォーマンスの問題が発覚しました。具体的には、SDKサーバーにかかる負荷がそこまで高くなくても(~10rps)、タイムアウトやクラッシュが発生してしまう状況でした。 なお、SDKサーバーは1つの計測箇所に対して1つのAPIを叩く形になっており、計測された際には、計測箇所数のリクエストが走ることになります。このため、数十箇所の計測箇所を計測したい組織の場合、SDKサーバーに対して一度に数十リクエストが走ることになります。 この仕様によって、少ない計測数でもリクエストが集中しやすくなっており、負荷によるクラッシュが発生しやすい状況でした。この問題を解決するために、SDKサーバーに負荷をかけるリクエストを直列にすることで、一時的な対応をしました。結果として、計測値結果の算出完了までのリードタイムは伸びてしまいましたが、幸いなことにローンチ時点では提供していた企業が少数だったため、これらの企業でのユースケースでは許容される範囲でした。というのも、仕様上スキャンアプリで計測直後に計測値を確認するわけではなく、スキャン後に別途、企業担当者がWebから計測結果を確認するという流れでした。そのため、計測値の保存に時間がかかってもそこまで支障がありませんでした。 しかしながら、今後の展開やクライアント数の増加を考えると、このままでは問題の発生する可能性が高いと判断し、ローンチ後に本格的なパフォーマンス改善をすることにしました。 ローンチ前の緩和策 計測値保存APIのLambda化とSQSの導入 ローンチ前の緩和策について、まずは紹介します。SDKサーバーが負荷に対して不安定かつ遅延も大きいという問題から、ひとまず同期的にリクエストを処理するのではなく、呼び出し元の計測値保存APIをLambdaに切り替えました。これには、SDKサーバーとの通信で起こるスレッドの占有によって、APIサーバーの他APIに影響を出さないという意図もありました。また、S3のイベントトリガーを使って、S3にアップロードされた計測データをトリガーにしてLambdaを起動し、SDKサーバーにリクエストを送るようにしました。さらに、失敗時の再処理をかけやすくできるように、SQSを挟みました。 この変更によって、スキャン後の計測値計算が非同期で行われることになったので、待ち時間がなくなり、計測体験も向上しました。また、Lambdaの同時実行数を設定することでSDKサーバーの最大負荷も調整できるようになりました。さらに、SQSを挟むことでリトライやDLQの設定が容易になり、計測処理が失敗した場合でも、再処理が可能になりました。 これによってSDKサーバーが安定稼働はするものの、計測箇所の数に応じて直列にSDKサーバーへリクエストを送るので、計測箇所の処理が終わるまでに2〜3分はかかるようになってしまいました。また、スループットに関しても30分の間に30件の計測を処理するのがやっとという状態でした。 スループットが低い理由としては、コストを抑えるためSDKサーバーのPodの台数が少なかったこともあります。HPAを設定していたものの、Podの起動に時間がかかるため、リクエストが集中するとPodの起動が追いつかず、再実行が必要になることがありました。 項目 値 計測値保存Lambdaのバッチ処理時間 120~180s 30分で処理可能な計測数 30 LambdaのSnapStart有効化 さらに、LambdaのSnapStartを有効化することで、Lambdaの起動時間を短縮しました。SnapStartは、Lambdaのコンテナを再利用することで、Lambdaの起動時間を短縮する機能です。SnapStartを有効化することで、Lambdaの起動時間が若干ではありますが短縮され、SDKサーバーへのリクエストが早く処理されるようになりました。 項目   値 計測値保存Lambdaのバッチ処理時間 90~180s 30分で処理可能な計測数 50~60 この時点で、1件の計測では1.5〜3分、50件の計測を処理するのに25分要する状態でした。ただし、失敗した場合でも自動でLambdaによって計測の再実行が可能になりました。それだけではなく、一定数リトライが失敗した場合でもSQSのDLQにメッセージが移動するように設定していたため、その失敗した計測を検知して再実行できるようになりました。 立て直し ここからは、このSDKサーバーの安定性とパフォーマンス改善のために行ったことを紹介します。 ローンチ後の改善 SDKサーバーのLambda化 ローンチ後の改善施策として、まずはSDKサーバーのLambda化を行いました。SDKサーバーの処理をLambdaに切り出すことで、SDKに負荷がかかってクラッシュすることを防ぎ、計測値計算APIの安定稼働を実現しました。なお、この時点ではまだ計測値の並列計算はしていない状態です。 この時点でのパフォーマンスは以下のとおりです。 項目 値 計測値保存Lambdaのバッチ処理時間 90~180s 30分で処理可能な計測数 165 設定値の調整 また、各種インフラの設定値も調整しました。例えば、Lambdaの同時実行数やタイムアウト時間、SQSのバッチサイズやリトライ回数など、パフォーマンスに影響を与える設定値を調整し、最適な値を探りました。 Lambda 同時実行数 Lambdaの同時実行数は、ReservedConcurrentExecutionsという設定値で制御されます。この値を調整することで、Lambdaの同時実行数をクォータ内で確保また制限できます。ただし、この数はアカウント全体で共有されているため、他のLambdaに影響する可能性があります。 また、Lambda化したSDKサーバーを並列で呼び出すため、計測値保存Lambdaの同時実行数と組織で登録可能な計測箇所数の乗数までは、Lambdaの起動数が増えます。そのため、この最大値がクォータに引っかからないように設定する、もしくは事前にクォータ(デフォルトでは1000)の引き上げ申請をする必要があります。 こちらに関しては、現段階では、同時に計測されるようなケースは少ないため、計測値保存Lambdaの同時実行数は小さい値で十分でした。ただし、このLambdaの起動数がクォータに引っかからないように、クォータの割合に応じた監視を入れており、引き上げのタイミングがわかるようにしています。 docs.aws.amazon.com タイムアウト時間 Lambdaのタイムアウト時間は、1リクエストの処理時間x SQSのバッチサイズに余裕を持たせた値を設定する必要があります。これは、最大SQSのバッチサイズ分のメッセージ数が1つのLambdaで処理されるためです。 また、SQSのメッセージ再送信までの時間もこのタイムアウト時間よりも長い値に設定する必要があります。これによって、Lambdaの処理がタイムアウトすることなく、正常に処理を終えることができます。 docs.aws.amazon.com SQS バッチサイズ SQSのバッチサイズは、1つのLambdaで処理させるメッセージの量を制御する設定値です。この値を大きくすると、Lambdaのタイムアウトに引っかかりやすくなります。逆に、小さくすると、Lambdaの並列数が上がり、キューの中で待ちになるメッセージが増えます。 docs.aws.amazon.com リトライ回数 SQSのリトライ回数は、Lambdaへ再送信する回数を制御する設定値です。この値を大きくすると、再実行の試行回数が増えるため、DLQでの検知が遅れます。逆に、小さくすると、リトライによって成功した可能性のあるメッセージもDLQに入れられてしまう可能性が高くなります。 こちらは今回の検証項目には含まれていませんが、参考までに記載しておきます。 docs.aws.amazon.com これらの設定値を調整した際のパフォーマンスは以下のようになりました。 検証した組み合わせは以下の通りです。 パターン1 Lambdaタイムアウト:180s SQSバッチサイズ:10 Lambda同時実行数:10 パターン2 Lambdaタイムアウト:300s SQSバッチサイズ:10 Lambda同時実行数:10 パターン3 Lambdaタイムアウト:300s SQSバッチサイズ:4 Lambda同時実行数:10 パターン4 Lambdaタイムアウト:300s SQSバッチサイズ:4 Lambda同時実行数:20 パターン 1 2 3 4 計測値保存Lambdaのバッチ処理時間 60~180s 60~300s 60~300s 60~300s 30分で処理可能な計測数 165 200 200 500 SQSメッセージの再送信数 3 3 2 2 計測値の並列計算の導入 最後に、計測値計算の並列処理を導入しました。これによって、複数の計測箇所を同時に計算できるようになり、計測値の計算処理が高速化されました。具体的には、計測値保存Lambdaのコードを改修し、複数の計測箇所を並行してリクエストするようにしました。これによって、すべてのメッセージを再送信することなく捌き切ることができるようになりました。 項目 値 計測値保存Lambdaのバッチ処理時間 ~60s 30分で処理可能な計測数 850 SQSメッセージの再送信数 0 結果 これらの構成変更や設定の調整によって、現在は計測値の計算処理は5秒程度、3分以内に最低でも160件は捌けるようになりました。SDKサーバーのクラッシュの懸念もなくなり、計測値の計算処理も安定しています。また、スケールアウトも容易になり、今後のクライアント数の増加にも対応しやすくなりました。 項目 改善前 改善後 計測1件あたりの処理時間 120~180s 5~10s 30分で処理可能な計測数 30 850 余談 さて、これらのパフォーマンス改善ですが、実はこれらを改善する前までビジネス的な優先度が低いとされていました。なぜなら、ローンチ直後には、クライアントからのフィードバックも特になく、負荷試験の結果から見ても先1年はビジネスチームからも問題がないとされていたからです。 とはいえ、以前のままでは将来的に問題の発生する可能性が高く、SREとしては喫緊のタスクがなかったこともあり、機能開発の裏でパフォーマンス検証を進めていきました。その後、検証結果を提示することで改善の効果とその工数を確認してもらいました。その結果、ビジネスチームからの期待値が上がり、本格的なパフォーマンス改善を進めることができました。コストに関しても、イベント駆動型の設計に切り替えたことで、インフラコストを抑えることができました。 学んだこと 最後に、今回のパフォーマンス改善を通じて学んだことをまとめます。 もちろん、パフォーマンスがあまり優れないAPIをLambdaに載せ替え、並列化することでレイテンシや安定性を向上させることができたのは、あまり新鮮ではなかったかもしれません。しかし、今回の改善を通じて、以下のようなことを学びました。 不確定要素が多い機能の実装は早めに設計を済ませる 今回、この計測値計算の機能はローンチ間際まで設計が進んでいませんでした。そのため、ローンチ後にパフォーマンスの問題が発生し、改善に時間がかかってしまいました。SDKの仕様や計測値計算の仕組みを早い段階で把握していれば、より適切なインフラ設計をできたと思います。 PoC段階でパフォーマンス試験を実施する 今回の計測値を計算する機能ですが、PoC段階では複数企業に対応したものではなく、単一企業を想定した実装となっていました。そのため、複数の企業に対応した際のパフォーマンスの問題がローンチ間際に発生しました。PoC段階で複数企業に対応したパフォーマンス試験を取り入れていれば、この問題を事前に発見し、改善できたかもしれません。 コミュニケーションコストが高い開発では、早い段階で認識を合わせる 今回のSDKの改善に取り組みづらかった理由として、SDKの開発をZOZOグループの海外子会社であるZOZO NEW ZEALANDが担っていたことが挙げられます。そのため、SDKの仕組みやコードの詳細を把握しきれていませんでしたが、SDKの仕組みを理解していれば、改善のアプローチも取りやすかったかもしれません。実装する上でボトルネックになりそうな箇所がある場合、早い段階で認識を合わせることが重要であると感じました。 終わりに 今回は、ZOZOMETRYのパフォーマンス改善についてお話ししました。ローンチ直後は問題があったものの、今では安定して計測値の処理も行えるようになりました。今後も引き続き、ZOZOMETRYの改善に取り組んでいきます。 また、この記事以外にもZOZOMETRYに関する記事を連載しておりますので、興味のある方はぜひご覧ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。計測プラットフォーム開発本部バックエンドチームの佐次田です。普段はZOZOMATやZOZOMETRYなどの計測技術に関わるシステムの開発、運用に携わっています。 本記事では計測システムにおける計測データの管理方法を進化させた点についてご紹介します。 目次 はじめに 目次 計測プラットフォーム開発本部 バックエンドチームとは ユビキタス言語 3Dデータ 計測箇所 計測値 ZOZOMETRYとは ZOZOMETRYのコア機能 計測箇所のカスタマイズに関する課題 計測処理をサーバーが担当する解決アプローチ システム構成図 既存プロダクトのシステム構成 ZOZOMETRYにおけるシステム構成 システム構成の変更点 計測セットを元に計測箇所をカスタマイズする 計測セットのドメインモデル 計測シーケンスの概要 3Dデータと計測値の保存先を分離する 3DデータをAPIサーバーで管理する課題 ペイロードのサイズが大きい GC(Garbage Collection)によるパフォーマンスの低下 署名付きURLによる解決アプローチ 署名付きURLとは APIサーバーを経由せずに3Dデータのやり取りを可能とする 署名付きURLを利用した3Dデータのアップロード APIサーバーを経由する場合 署名付きURLを利用した場合 署名付きURLを利用した3Dデータのダウンロード APIサーバーを経由する場合 署名付きURLを利用した場合 署名付きURLを利用したメリット まとめ 最後に 計測プラットフォーム開発本部 バックエンドチームとは 計測プラットフォーム開発本部バックエンドチームは、ZOZOMAT、ZOZOGLASS、ZOZOSUITによって採集される計測データにまつわるバックエンド開発を担うチームです。アプリやブラウザに対するクライアントAPIや、ZOZOTOWN内部のマイクロサービスのAPIにおいて、徹底的に低レイテンシにこだわりを持つことと、高可用性を保つことを目指しています。 ユビキタス言語 本記事を読む上で必要なユビキタス言語は以下の通りです。 3Dデータ 3Dデータは、被計測者の体型などを3次元で表現するデータです。ZOZOSUITやZOZOMATなどで共通の計測技術を利用して生成されるデータであり、計測時点での身体情報の3D表示を可能とします。ZOZOSUITの場合は被計測者の体型を、ZOZOMATの場合は被計測者の足型を3Dで表現します。 計測箇所 計測箇所は計測の対象となる各部位のことを指します。ZOZOSUITの場合は腕周りや首周り、ZOZOMATの場合は足幅や足長など複数箇所を計測箇所として定義しています。 計測値 計測箇所ごとに計測された数値データを計測値として扱います。 ZOZOMETRYとは ZOZOMETRY とは、事業者の採寸業務を効率化し、採寸が必要な服の売上拡大やコスト削減に貢献するサービスです。多様な業種のビジネスニーズに応えられるよう、計測箇所を柔軟に選択できます。ファッションに限らず、フィットネスや医療、スポーツなど幅広い業種に対応できるように設計されています。 corp.zozo.com ZOZOMETRYのコア機能 ZOZOMETRYのコア機能として、身体計測を実現する機能と、計測データの管理機能があります。身体計測の機能は計測技術により手軽に高精度な計測を実現する機能であり、直近のリリースによりZOZOSUITの着用無しでも計測が可能となりました。計測データの管理機能は計測データの保存、参照、エクスポートなどを行う機能で、計測データの管理を効率化し、より重要な業務に集中できるように設計されています。しかし、身体計測の機能を顧客企業向けに提供するには計測箇所の柔軟なカスタマイズが必要であり、これまでの構成では難しい問題がありました。初めに既存プロダクトにおいてどのような構成で身体計測の機能を提供していたかと、ZOZOMETRYにおいて計測箇所をカスタマイズするために必要だった構成についてご紹介します。 計測箇所のカスタマイズに関する課題 ZOZOMETRYではビジネスニーズに合わせて顧客企業ごとに計測箇所の柔軟なカスタマイズを行えることが重要でした。既存プロダクトでは固定の計測箇所を定義していたため、顧客企業ごとの要望に対応することが難しい状況でした。例えば、ウエストという計測箇所は顧客企業によってはウエストを腰骨の位置から何センチ上で定義しているなど細かな差異があり、1つのウエストという定義では個別の要求に対応できない課題がありました。 計測処理はアプリ内に内包されているSDKにより行われており、内部的には身体をスキャンする処理と計測値を算出する処理が行われています。この構成の場合、計測箇所を追加・編集するにはアプリの更新が必要でした。 計測処理をサーバーが担当する解決アプローチ 計測箇所をカスタマイズ可能とするため、3Dデータの生成と計測値を算出する責務をアプリとサーバーで分離するアプローチを取りました。具体的な方法は割愛しますが、計測値の算出処理をサーバー側に移動することで、計測箇所の追加や更新によるアプリの更新が不要になる想定でした。 システム構成図 既存プロダクトとZOZOMETRYでのシステム構成図の比較は以下の通りです。 既存プロダクトのシステム構成 ZOZOMETRYにおけるシステム構成 システム構成の変更点 アプリ内で行っていた計測値の算出処理をサーバーが担当するように変更しました。また、3Dデータと計測値の保存先はユースケースに合わせて分離する構成を取りました。 計測セットを元に計測箇所をカスタマイズする 計測セットは先述のカスタマイズした計測箇所をさらに顧客企業ごとに必要な計測箇所のまとまりとして定義する機能であり、これにより計測ごとに必要な計測箇所を顧客企業が自由に編集できるようにする機能です。ドメインモデルとしては、以下のような構成となっています。 計測セットのドメインモデル 計測シーケンスの概要 サーバーは計測セットに含まれる計測箇所を元に計測値の算出処理を行います。このアプローチにより、顧客機能のビジネスニーズに合わせた計測処理の提供が可能となりました。 3Dデータと計測値の保存先を分離する 既存プロダクトでは3Dデータと計測値を単一の保存先に格納、管理していましたが、3Dデータと計測値はそれぞれ異なる特性を持っており、それぞれの特性に合わせた最適な保存先を検討する必要がありました。3Dデータは不変である特性を持ち、一度作成されると変更されることがありません。扱いとしてはファイルや画像などと同様に管理することが良いと考え、管理にはAWSが提供するS3を利用しました。S3については多くの記事で紹介されているためここでは割愛しますがパフォーマンスとセキュリティ、可用性の面で優れており、3Dデータの管理に適していると考えました。計測値は3Dデータとは異なり、計測データを管理するコア機能において参照されるユースケースが多くあります。計測を一覧表示する機能や、計測データをCSVエクスポートする機能など、計測値を参照し結合して返す必要がありました。そのため、計測値の管理にはリレーショナルデータベースであるRDSを採用することにしました。 3DデータをAPIサーバーで管理する課題 3Dデータの管理にS3を利用することを決定しましたが、3DデータのS3へのアップロード、ダウンロードをAPIサーバーを経由して行うことに課題がありました。 ペイロードのサイズが大きい 3Dデータはサイズが大きいため、被計測者に計測データを返す際のパフォーマンスが課題でした。ZOZOMATについては3Dデータの送信に対して通信プロトコルを変更するアプローチで対応しました。詳細については下記の記事をご参照ください。 techblog.zozo.com 通信プロトコルを変更することで一定の効果を得ましたが、API経由のスキャンデータのアップロードに時間がかかるなどサイズが大きいことによる課題は残り続けていました。 GC(Garbage Collection)によるパフォーマンスの低下 私たちバックエンドチームではプログラミング言語にScalaを採用しています。技術戦略については下記の記事をご参照ください。 techblog.zozo.com 既存プロダクトでは3Dデータを参照するたびにAPIサーバーを経由してデータを取得します。もちろん、計測データを専用に扱うサーバーを別で建てるように分離することも検討しましたが、初期の設計では立ち上げ期という事情も働き、モノリシックな構成でローンチしています。ScalaはJVM上で動作するためメモリの解放時にGCが発生します。サイズの大きな3Dデータをメモリに繰り返しロードすることでGCも頻発し、パフォーマンスに影響を与える問題がありました。 署名付きURLによる解決アプローチ 署名付きURLとは 署名付きURL はAWS S3バケットに対して一時的なアクセス権限を付与するURLです。これにより、特定のオブジェクトに対して指定された時間内に限り、読み取りまたは書き込みの操作が可能です。 APIサーバーを経由せずに3Dデータのやり取りを可能とする 3Dデータはサイズが大きいためAPIサーバーを経由せずにクライアントとデータのやり取りをすることが重要となりました。署名付きURLを利用することで、APIサーバーに負荷をかけずセキュアにS3へのアップロードを実現できます。 署名付きURLを利用した3Dデータのアップロード APIサーバーを経由する場合 署名付きURLを利用した場合 クライアントとサーバーのやり取りは署名付きURLの発行手続きのみとなり、3DデータのアップロードはクライアントとS3の間で直接行われます。このアプローチによりAPIサーバーの負荷を軽減し、APIサーバーのスケーリングに依存せずデータのやり取りが可能となりました。 署名付きURLを利用した3Dデータのダウンロード APIサーバーを経由する場合 署名付きURLを利用した場合 アップロードと同様に、クライアントとサーバーのやり取りは署名付きURLの発行手続きのみとなり、3DデータのダウンロードはクライアントとS3の間で直接行われます。ダウンロードの場合は署名付きURLをstatus code 302で返し、ダウンロード用のリンクにリダイレクトするようにクライアントに返します。これにより、ブラウザが自動的にリダイレクトを処理するため、クライアント側での実装負担を削減できます。 署名付きURLを利用したメリット 署名付きURLを利用することで以下のメリットがありました。 パフォーマンスの向上 3DデータをAPIサーバーを経由せずにやり取りすることで、APIサーバーの負荷が軽減されました コストの削減 APIサーバーを経由せずに3Dデータがやり取りできるため、サーバーリソースの節約が可能となりました APIサーバーの実装をシンプルに 署名付きURLを利用することでAPIサーバーの実装がシンプルになり、開発やメンテナンスが容易になりました 一方で、署名付きURLを用いたデメリットとして、クライアントでは1回のアップロード操作に対して、署名付きURLの発行リクエストと実際のデータアップロードリクエストの2回のリクエストを処理する必要があります。署名付きURLを多く使用する場合はクライアントの処理が煩雑になるため、実装に注意が必要です。 まとめ ビジネスニーズに合わせて計測処理をサーバーで管理することで、計測箇所の追加や変更のニーズに対応可能となりました。3Dデータと計測値を分離し、それぞれの特性に応じた最適な管理方法を検討しました。また、クライアントとサーバー間の3Dデータの受け渡しに署名付きURLを活用することでAPIサーバーの負荷を軽減しました。 ZOZOMETRYはローンチ後も引き続き機能追加をしており、計測データの管理方法について検討を続けていきたいと考えています。 最後に 計測プラットフォーム開発本部バックエンドチームでは、グローバルに計測技術を開発していくバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co
アバター
はじめに こんにちは、ZOZOTOWN企画開発部・企画フロントエンド1ブロックの ゾイ です。 ZOZOTOWNトップでは、セール訴求や新作アイテム訴求、未出店ブランドの期間限定ポップアップ、著名人コラボなどの企画イベントが毎日何かしら打ち出されています。私はそのプラットフォームとなる企画LPをメインに実装するチームに在籍しています。 今まで実装した企画LPをアーカイブするプロジェクトにも参加しているので、チームの特性や企画LPの詳細は下記の記事をご覧いただければと思います! techblog.zozo.com 目次 はじめに 目次 背景・課題 仕様・技術選定 1. 型の定義と入力データのバリデーション 2. 入力フォームの実装 3. URLの生成 ユーザーの反応 終わりに 背景・課題 ZOZOTOWNではカルセールバナーなど、アプリとWeb共通で表示させるコンテンツにはURLを分ける必要があります。 例えば、レディーズのトップス商品一覧がある検索結果に遷移したい場合、Webでは以下のようなURLを指定する必要があります。 https://zozo.jp/women-category/tops/ 対して、アプリでは以下のようなスキームを指定する必要があります。 zozotown://index/search?view=result&p_cutyid=2&p_tycid=101 上記のようなURLは施策担当の方が下記の方法で作成し、カルセールバナーやLP内の遷移先に利用することが一般的なフローです。 Web:検索結果のページで絞り込み条件を手動で入れて発行する アプリ:既存のアプリスキーム生成ツールで作る 今まではそれぞれ違う方法でURLを作成していたため、下記のような問題がありました。 WebのURLでの絞り込み条件とアプリのURLでの絞り込み条件が一致しない URL生成に慣れてない部署やチームは作り方に迷うため、PMの負担が高まる 課題を解決するためPMと相談した結果、アプリとWeb用のURLを同時に生成できる「URL生成ツール」を実装することになりました。 仕様・技術選定 検索ページ用のURL生成 商品詳細ページ用のURL生成 アプリ上でLPを確認できるQRコード生成 外部URL遷移用のアプリスキーム生成 上記の機能をミニマムで盛り込み、デザイナー・PM・事業部・エンジニア等、企画に関わる多方面のスタッフにとって嬉しいツールを目指します。今回の記事では主に「1. 検索ページ用のURL生成」の実装方法について紹介したいと思います。 アプリのようにサクサク動くPWA的なサイトにしたかったため、全ての処理がCSRで実現できることを意識しました。そのため、 Next.js 、 MUI 、 Emotion 、 zod などを採用しました。 Googleなどで利用されておりUI的に一番親近感もあってEmotionとも相性が良いため、MUIを採用しました。また、検索ページ用のURL生成には入力フォームが多いため、ユーザーが誤って間違えた値を入力できないよう、エラー検知用にzodを採用しました。 他にも qrcode.react や encoding-japanese などのようなヘルパーパッケージを導入しました。 1. 型の定義と入力データのバリデーション まず、検索ページで利用しているパラメーターを調査し、許可されない値の入力を防ぐ対応を入れます。 下記は「価格(から)」が必ず「価格(まで)」より小さい数字にさせる対応の例です。 const priceSchema = z . object ({ priceFrom : z . union ([ z . literal ( '' ) , z . number () . min ( 0 )]) , priceTo : z . union ([ z . literal ( '' ) , z . number () . min ( 0 )]) , }) . refine (( schema ) => { return ( schema . priceFrom === '' || schema . priceTo === '' || ( schema . priceFrom && schema . priceTo && schema . priceFrom < schema . priceTo ) ) } , 'PriceFrom must be less than PriceTo' ) 上記のような制限をすべて含めて、以下のような型をzodで定義します。 export const schema = z . object ({ // ...省略 gender : z . number () , couponFilter : z . boolean () , discountRate : z . number () . min ( 1 ) . max ( 100 ) . optional () , }) . merge ( priceSchema . innerType ()) 2. 入力フォームの実装 次に、入力フォームを実装します。CSRでURLを生成できるよう、入力フォームの状態をReactのContextを共有する形で実装していきます。入力フォームにはMUIの AutoComplete 、 FormControl 、 TextField を採用しました。特に AutoComplete はアルファベット順のソートや、検索機能もあるためブランド一覧の表示にとても便利です。 入力フォームは以下を考慮して実装しました。 onChange時(ユーザーが何かを入力した時)に、Contextを更新する 入力項目に誤りがあったらエラーUIにさせる ショップなど、Webでは名前、アプリではIDを利用する項目が多いため、データ属性に両方指定する 同時に絞り込みきない項目や必要な項目がない場合、非アクティブ状態にする 以下は「価格(から)」のフォームの実装例です。 export const PriceFrom = () => { // コンテキスト const { searchParamsValue , setSearchParamsValue } = useContext ( SearchContext ) const { priceFrom , ... rest } = searchParamsValue const [ hasError , setHasError ] = useState ( false ) // ユーザーが入力したら値を確認し、問題なかったらコンテキストを更新する const handleChange = ( e: React . ChangeEvent < HTMLInputElement > ) => { const input = parseInt ( e . target . value ) const result = priceSchema . safeParse ({ priceFrom : input , priceTo : searchParamsValue . priceTo , }) if ( ! result . success ) setHasError ( true ) else { setHasError ( false ) setSearchParamsValue ({ priceFrom : input , ... rest }) } } return ( < TextField label = { hasError ? `¥ ${ searchParamsValue . priceTo } 以下の数字を入力してください` : '価格(から)' } type = "number" variant = "standard" onChange = { handleChange } autoComplete = "off" error = { hasError } sx = {{ width : 170 }} InputProps = {{ startAdornment : < InputAdornment position = "start" > ¥ </ InputAdornment > , }} /> ) } 3. URLの生成 最後に、URLを生成します。 まず、アプリとWebによって違う値を利用する項目があるため値を変換する対応を入れます。例えば、キーワード絞り込みはアプリではUTF-8でエンコードされた値を、WebではShift_JISにエンコードされた値を利用します。 export const transformValues = ( searchParameters: SearchParameters , platform: 'web' | 'app' , ) : TransformedValues => { const isApp = platform === 'app' const { keyword } = searchParameters const transformedValues: TransformedValues = { // ...省略 keyword : isApp ? encodeURI ( String ( keyword )) : encodeToShitJis ( String ( keyword )) , } return transformedValues } 次に、アプリとWebのURLを生成する関数を実装します。アプリのURLは全てのパラメーターを繋ぎこむだけで済みますが、Webの場合はZOZOTOWNのドメイン知識が必要なため、もう少し複雑な処理が入っています。そのため、今回の記事では一般的に利用できる、アプリのURLを生成する関数だけ紹介したいと思います。 export const composeAppUrl = ( value: SearchParameters ) => { if ( value === DEFAULT_SEARCH_PARAMS ) return '検索条件を入力してください' // コンテキストの値をアプリ用に変換する const transformedValues = transformValues ( value , 'app' ) const params = { ... value , ... transformedValues , } // queryStringの作成 const queryString = Object . entries ( params ) . reduce (( acc , [ key , val ]) => { if ( ! val ) return acc return acc } , [] as string []) . filter ( Boolean ) . join ( '&' ) // queryStringとpathを繋ぐ return appendQueryStringToPath ( ZOZOTOWN_APP_URL . search , queryString ) } 上記で生成したURLを表示させたら完成です! ユーザーの反応 URL生成ツールを利用していただいている、事業部やPMの方々から以下のようなご意見をいただきました! 複数人で同時に使用できるようになった アプリスキームだけではなくPC/SPのURLも確認できるのがありがたい プルダウン式で項目を選べるのがとても助かるし使いやすい 既存のアプリスキーム作成ツールでは、条件を入力し作成ボタンを押した後、数秒間待つ時間があるのに対し、ZOZO URL GENERATORでは作成結果が即時反映されて嬉しい 各項目のショップIDやブランドIDなどIDを逐一調べず抽出でき工数削減につながった URL生成ツールを通して様々な部署の工数を削減できてよかったと思います。 終わりに 本記事ではURL生成ツールを紹介しました。URL生成ツールの導入によってサイトバナーの管理プロセスや、LPの開発フローを改善できて良かったと思います。 株式会社ZOZOでは、アイデア次第でこんなふうに自由度の高い開発を経験できる環境が整っています! ご興味のある方はぜひ、ご応募お待ちしております! corp.zozo.com
アバター
はじめに こんにちは、WEARバックエンド部バックエンドブロックの塩足です。普段は弊社サービスであるWEARのバックエンド開発・保守を担当しています。 WEARのバックエンドでは、これまで自動テスト環境としてCircleCIを使用していましたが、運用保守の改善を目的にGitHub Actionsへ移行しました。 今回は、GitHub Actionsへ移行する際に取り組んだ以下の3点について紹介します。 効率的にテストを分割してテストを並列実行する方法 失敗したテストのみを再実行する仕組みの構築 GitHubのCheck annotationsを活用して、失敗したテスト情報を表示 また、最後に今回行ったテストカバレッジのレポーティングとGitHub Pagesでのホスティングの方法について紹介します。 目次 はじめに 目次 背景 なぜ自動テスト環境をCircleCIからGitHub Actionsに移行したのか テストカバレッジのレポーティングやGitHub Pagesでのホスティングがシンプルに実現可能 octocovの導入が容易 ワークフローをトリガーする豊富なイベント 全社的にGitHub Actionsを推奨 課題 1. CircleCIのタイミングベースでテスト分割する仕組みがGitHub Actionsにない 2. CircleCIの失敗したテストのみを再実行する機能がGitHub Actionsにない 3. GitHub Actionsで失敗したテスト詳細情報へのアクセシビリティが低い 課題解決の方法 1. r7kamura/split-tests-by-timingsアクションを使用したタイミングベースのテスト分割 Download all test results for default branchステップ Split tests by timingsステップ 2. RSpecの--only-failuresオプションで失敗したテストのみ再実行 Download previous test resultステップ Place previous test resultステップ spec/examples.txt JUnit XMLファイル カバレッジデータ Re-run rspec only failuresステップ 3. JUnit XMLファイルとreviewdogを用いて、GitHubのCheck annotationsで失敗したテスト情報を表示 テストカバレッジを活用できる環境整備の方法 octocovを使用してテストカバレッジをPull Requestにレポートする テストカバレッジの結果をGitHub Pagesでホスティングする 今後の展望 Flaky testを検出する機能がない 追加・変更した実装コードのソースコードカバレッジ まとめ 背景 前述の通り、WEARのバックエンドではこれまで自動テスト環境としてCircleCIを使用していました。 なぜ自動テスト環境をCircleCIからGitHub Actionsに移行したのか では、なぜ自動テスト環境をCircleCIからGitHub Actionsに移行したのか、その理由を説明します。 テストカバレッジのレポーティングやGitHub Pagesでのホスティングがシンプルに実現可能 octocov の導入が容易 octocov はコードメトリクスを収集するツールキットです。 octocov には前回のテストカバレッジと比較するdiff機能があります。この機能を使うにはデータストレージが必要です。 CircleCIで利用する際は、外部ストレージを利用する必要があります。一方、GitHub Actionsでは、 octocov がGitHub Actionsアーティファクトをデータストレージとしてサポートしています。また、 k1LoW/octocov-action@v1 アクションが用意されているため、簡単に導入できます。 ワークフローをトリガーする豊富なイベント これまで、CircleCIでは push をトリガーにワークフローを実行していました。しかし、 octocov を利用するには pull_request をトリガーにワークフローを実行する必要があります。 pull_request をトリガーにすると、Pull Requestを作成するまでテスト結果を確認できないため、開発生産性が落ちてしまいます。 そこで、CircleCIで push と pull_request の両方をトリガーする方法を検討しました。しかしながら、両方をトリガーするにはGitHub ActionsからCircleCIのAPIでトリガーする必要があり、全体のワークフローが複雑になってしまいます。一方、GitHub Actionsであれば柔軟なトリガーが可能なため、シンプルに解決できます。 全社的にGitHub Actionsを推奨 全社的に新規のプロジェクトに関しては、基本的にGitHub Actionsの利用を推奨しています。そのため、今後はGitHub Actionsに揃えることで、ナレッジの共有やメンテナンスが容易になると考えました。 上記2つの理由から、自動テスト環境をCircleCIからGitHub Actionsへ移行することを決定しました。 課題 CircleCIからGitHub Actionsに移行するにあたって、以下の3つの課題がありました。 CircleCIのタイミングベースでテスト分割する仕組みがGitHub Actionsにない CircleCIの失敗したテストのみを再実行する機能がGitHub Actionsにない GitHub Actionsで失敗したテスト詳細情報へのアクセシビリティが低い 1. CircleCIのタイミングベースでテスト分割する仕組みがGitHub Actionsにない CircleCIは標準で タイミングデータに基づいたテストの分割 が可能です。GitHub Actionsには同様の機能が備わっていないので、別途仕組みを考える必要があります。 2. CircleCIの失敗したテストのみを再実行する機能がGitHub Actionsにない CircleCIには標準で 失敗したテストのみを再実行 する機能を提供しています。GitHub Actionsには失敗したテストのみ再実行する機能がないため、Flaky test等でテストが失敗した際、全テストを実行する必要があり開発生産性に影響します。 3. GitHub Actionsで失敗したテスト詳細情報へのアクセシビリティが低い CircleCIはJob詳細画面のTESTSタブで失敗したテストを一覧で閲覧できます。 一方、GitHub Actionsには失敗したテストを一覧で閲覧する機能はありません。並列で実行して複数のtestジョブでテストが失敗している場合、失敗しているtestジョブの数だけログを確認する必要があります。 課題解決の方法 まずは以下のようなTestワークフロー( .github/workflows/test.yml )を用意し、これを拡張することで3つの課題を解決していきます。 name : Test on : push : concurrency : group : ${{ github.workflow }}-${{ github.ref }} cancel-in-progress : true env : TEST_JOB_PARALLEL_COUNT : 2 RAILS_ENV : test defaults : run : shell : bash jobs : test : runs-on : ubuntu-latest timeout-minutes : 20 permissions : contents : read actions : read strategy : fail-fast : false matrix : group_index : [ '0,1' , '2,3' ] steps : - name : Checkout uses : actions/checkout@v4 - name : Setup Ruby uses : ruby/setup-ruby@v1 with : bundler-cache : true - name : Setup DB run : bundle exec rails "parallel:setup[`nproc`]" - name : Run rspec in parallel run : bundle exec parallel_rspec -n $((${TEST_JOB_PARALLEL_COUNT} * `nproc`)) --only-group ${{ matrix.group_index }} - name : Upload test result if : ${{ success() || failure() }} uses : actions/upload-artifact@v4 with : name : test-result-${{ matrix.group_index }} path : | test_results/ coverage/.resultset*.json include-hidden-files : true if-no-files-found : ignore このワークフローは matrix strategy を使用して2つ( TEST_JOB_PARALLEL_COUNT )のtestジョブを並列実行します。さらに、各testジョブは parallel_rspec で2つ(= nproc から取得したCPU数)のrspecプロセスを実行します。テストは parallel_rspec によってファイルサイズをベースに4つ(= testジョブ並列数 × CPU数)のグループに分割され、各testジョブで2グループずつ実行されます。テスト結果は test-result-${{ matrix.group_index }} という名前でアーティファクトに保存します。 testジョブのワークフローのフローチャートは以下のようになります。 1. r7kamura/split-tests-by-timings アクションを使用したタイミングベースのテスト分割 課題1を解決するための、タイミングベースのテスト分割について説明します。 parallel_rspec のオプション --group-by に runtime を指定することで、タイミングベースでテスト分割できます。しかし、今回はより汎用的に利用できる r7kamura/split-tests-by-timings アクションを採用しました。 r7kamura/split-tests-by-timings アクションはJUnit XMLファイルを元にタイミングベースでテスト分割するアクションです。つまり、過去のテスト結果からJUnit XMLファイルを抽出し、 r7kamura/split-tests-by-timings アクションに渡す必要があります。 タイミングベースでテスト分割するために、testジョブを以下のように修正します。 @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - group_index: ['0,1', '2,3'] + test_job_index: [0, 1] steps: - name: Checkout uses: actions/checkout@v4 @@ -35,13 +35,30 @@ jobs: bundler-cache: true - name: Setup DB run: bundle exec rails "parallel:setup[`nproc`]" + - name: Download all test results for default branch + uses: dawidd6/action-download-artifact@v6 + with: + name: test-result-* + name_is_regexp: true + path: ${{ runner.temp }}/default-branch-test-results + branch: ${{ github.event.repository.default_branch }} + workflow_conclusion: success + if_no_artifact_found: warn + - name: Split tests by timings + uses: r7kamura/split-tests-by-timings@v0 + id: split-tests + with: + reports: ${{ runner.temp }}/default-branch-test-results/**/test_results + glob: spec/**/*_spec.rb + index: ${{ matrix.test_job_index }} + total: ${{ env.TEST_JOB_PARALLEL_COUNT }} - name: Run rspec in parallel - run: bundle exec parallel_rspec -n $((${TEST_JOB_PARALLEL_COUNT} * `nproc`)) --only-group ${{ matrix.group_index }} + run: bundle exec parallel_rspec -n `nproc` ${{ steps.split-tests.outputs.paths }} - name: Upload test result if: ${{ success() || failure() }} uses: actions/upload-artifact@v4 with: - name: test-result-${{ matrix.group_index }} + name: test-result-${{ matrix.test_job_index }} path: | test_results/ coverage/.resultset*.json testジョブにおいて以下の点線で囲った2つのステップが追加されます。 追加した2つのステップについて説明します。 Download all test results for default branch ステップ このステップではデフォルトブランチで成功している最新のテスト結果をダウンロードします。テストは複数のジョブで分散して実行されるため、その全てのテスト結果を取得する必要があります。 公式で用意されている actions/download-artifact アクションでは、ブランチやワークフロー実行のステータス等を指定してダウンロードできません。そこで、より柔軟に指定できる dawidd6/action-download-artifact アクションを使用することにしました。 dawidd6/action-download-artifact アクションの各入力パラメータについては以下の表で説明します。 キー 値 name ダウンロードするアーティファクト名のパターンを指定します。 name_is_regexp nameで正規表現を利用できるように、 true を設定します。 path ダウンロードするファイルパスを指定します。 branch ブランチでアーティファクトを検索します。今回はデフォルトブランチを指定します。 workflow_conclusion ワークフローのステータスでアーティファクトを検索します。今回は成功しているワークフローのみに絞り込むため、 success を指定します。 if_no_artifact_found アーティファクトが見つからない場合の挙動を定義します。 初回実行を考慮してアーティファクトが存在しない場合でも動作するように warn を指定しています。 Split tests by timings ステップ r7kamura/split-tests-by-timings アクションはJUnit XMLファイルを元にタイミングベースでテストを分割します。 r7kamura/split-tests-by-timings アクションの各入力パラメータについては以下の表で説明します。 キー 値 reports JUnit XMLのファイルパスを指定します。 指定したパスにある全てのXMLファイルからタイミングデータを取得します。 glob テストファイルのglobパターンを指定します。 index 分割したグループのインデックスを指定します。 total 分割するグループ数を指定します。 今回はtestジョブの数だけグループを作成します。 分割されたファイルパスのリストは ${{ steps.split-tests.outputs.paths }} でアクセスできます。 Split tests by timings ステップで分割したファイルパスのリストを Run rspec in parallel ステップで使用することで、タイミングベースのテスト分割ができます。タイミングベースのテスト分割によりtestジョブは約30秒高速化しました。 2. RSpecの --only-failures オプションで失敗したテストのみ再実行 課題2を解決するため、失敗したテストのみを再実行する方法を説明します。 CircleCIでは circleci tests run を使用することで、失敗したテストのみ再実行できましたが、GitHub Actionsにそういった機能は用意されていません。一方で、RSpecには --only-failures オプションが用意されています。 --only-failures オプションは、実行されるテストをフィルタリングして、前回実行時に失敗したテストだけが実行されるようにします。 失敗したテストのみを再実行するために、testジョブを以下のように修正します。 @@ -33,9 +33,35 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true + - name: Download previous test result + uses: actions/download-artifact@v4 + with: + pattern: test-result-${{ matrix.test_job_index }} + path: ${{ runner.temp }} + - name: Place previous test result + id: previous-test-result + env: + TEST_RESULT_DIR: ${{ runner.temp }}/test-result-${{ matrix.test_job_index }} + run: | + if [ -f ${TEST_RESULT_DIR}/spec/examples.txt ]; then + mv ${TEST_RESULT_DIR}/spec/examples.txt spec/examples.txt + echo "failed-tests-only=true" >> $GITHUB_OUTPUT + fi + suffix="_`date +%s`" + mkdir -p test_results coverage + if [ -e ${TEST_RESULT_DIR}/test_results ]; then + mv ${TEST_RESULT_DIR}/test_results/* test_results/ + find test_results -type f -name "*.xml" | sed "p;s/.xml/${suffix}.xml/" | xargs -n2 mv + bundle exec rails runner "Dir['test_results/**/*.xml'].each { |path| File.write(path, Nokogiri(File.read(path)).tap { _1.css('testcase:has(failure)').remove }.to_s) }" + fi + if [ -e ${TEST_RESULT_DIR}/coverage ]; then + mv ${TEST_RESULT_DIR}/coverage/.resultset*.json coverage/ + find coverage -type f -name "*.json" | sed "p;s/.json/${suffix}.json/" | xargs -n2 mv + fi - name: Setup DB run: bundle exec rails "parallel:setup[`nproc`]" - name: Download all test results for default branch + if: ${{ !steps.previous-test-result.outputs.failed-tests-only }} uses: dawidd6/action-download-artifact@v6 with: name: test-result-* @@ -45,6 +71,7 @@ jobs: workflow_conclusion: success if_no_artifact_found: warn - name: Split tests by timings + if: ${{ !steps.previous-test-result.outputs.failed-tests-only }} uses: r7kamura/split-tests-by-timings@v0 id: split-tests with: @@ -53,7 +80,11 @@ jobs: index: ${{ matrix.test_job_index }} total: ${{ env.TEST_JOB_PARALLEL_COUNT }} - name: Run rspec in parallel + if: ${{ !steps.previous-test-result.outputs.failed-tests-only }} run: bundle exec parallel_rspec -n `nproc` ${{ steps.split-tests.outputs.paths }} + - name: Re-run rspec only failures + if: ${{ steps.previous-test-result.outputs.failed-tests-only }} + run: bundle exec rspec --only-failures - name: Upload test result if: ${{ success() || failure() }} uses: actions/upload-artifact@v4 @@ -61,6 +92,7 @@ jobs: name: test-result-${{ matrix.test_job_index }} path: | test_results/ + spec/examples.txt coverage/.resultset*.json include-hidden-files: true if-no-files-found: ignore testジョブにおいて以下の点線で囲った3つのステップが追加されます。 追加した3つのステップについて説明します。 Download previous test result ステップ actions/download-artifact アクションで前回のテスト結果のアーティファクトをダウンロードします。 GitHub Actionsのジョブの実行はいくつか方法がありますが、通常はイベントトリガーで実行されます。また、再実行はGitHub Actionsの実行詳細ページにある以下のボタンから「Re-run all jobs」と「Re-run failed jobs」を選択して実行できます。 このジョブの実行方法の違いによりアーティファクト取得の挙動が変化します。 実行方法 アーティファクト取得の挙動 イベントトリガー アーティファクトがまだ存在しないため、取得できません。 Re-run all jobs すべてのジョブのアーティファクトがリセットされます。 つまり、挙動としては イベントトリガー と同じになります。 Re-run failed jobs 成功したジョブのアーティファクトは変更されず、そのまま保存されます。 失敗したジョブのアーティファクトは 再利用 でき、再実行後にそのジョブの新しいアーティファクトで 上書き できます。 Re-run failed jobs の場合、失敗したジョブのアーティファクトは再利用できるため、前回のテスト結果を利用できることになります。 Place previous test result ステップ Download previous test result ステップで前回のテスト結果をダウンロードできた場合、以下の3つのファイルを適切に配置する必要があります。 spec/examples.txt JUnit XMLファイル カバレッジデータ それでは1つずつ説明します。 spec/examples.txt spec/examples.txt は、RSpecの実行結果に関する情報を記録するためのファイルです。このファイルには、前回のRSpec実行時にどのテストが成功したか、失敗したかなどの情報が保存されます。 また、 --only-failures オプションを使用するには以下のように、 spec/examples.txt を spec/spec_helper.rb に設定する必要があります。 RSpec .configure do |config| config.example_status_persistence_file_path = ' spec/examples.txt ' end JUnit XMLファイル JUnit XMLファイルは以下の2箇所で利用します。 r7kamura/split-tests-by-timings アクションを使用したタイミングベースのテスト分割 後述するGitHubのCheck annotationsで失敗したテスト結果を表示 bundle exec rspec --only-failures を実行すると、JUnit XMLのファイルは前回失敗したテストだけの結果に上書きされます。タイミングベースでテスト分割する際に、全体のテスト結果が必要になるため、別の名前にリネームして残しておく必要があります。 当然、このファイルには前回失敗したテスト結果が含まれています。前回失敗したテストは再実行されるため、前回失敗したテスト結果だけ事前に削除しておく必要があります。 path = ' test_results/rspec.xml ' File .write(path, Nokogiri( File .read(path)).tap { _1.css( ' testcase:has(failure) ' ).remove }.to_s) カバレッジデータ このファイルはテストカバレッジをレポートする際に利用します。このファイルもJUnit XMLファイルと同様に bundle exec rspec --only-failures を実行すると、上書きされてしまいます。そのため、カバレッジデータも別のファイル名にリネームして残しておく必要があります。 前回のテスト結果がある場合は ${{ steps.previous-test-result.outputs.failed-tests-only }} に true が設定され、後続する処理で利用されます。 Re-run rspec only failures ステップ 前回のテスト結果がある場合のみ bundle exec rspec --only-failures を実行します。 これで Re-run failed jobs から再実行した場合に、失敗したテストのみを再実行する方法が実現できました。 3. JUnit XMLファイルとreviewdogを用いて、GitHubのCheck annotationsで失敗したテスト情報を表示 課題3を解決するために、GitHubのCheck annotationsで失敗したテスト情報を表示する方法について説明します。 reviewdog は Reviewdog Diagnostic Format(RDFormat) という独自のフォーマットを利用して任意のlinterと連携できます。 JUnit XMLのデータは、エラーが発生したテストのファイルパス、テスト名(full_description)、エラーメッセージを保持しています。 reviewdog を使用し、JUnit XMLのデータだけでエラー情報を表示すると以下のようになります。 対象ファイルの1行目にエラー情報が全て表示されるため、どのテストの情報なのか分かりにくくなってしまいます。 そこで、各テスト名の行番号を取得してエラー情報を見やすくする方法を検討しました。今回は RSpec::Core::ExampleGroup オブジェクトの metadata から行番号を取得する方法を採用しました。 以下のファイル( scripts/generate_rspec_reviewdog_json.rb )はJUnit XMLファイルからRDFormatファイルを生成するスクリプトとなります。 require ' nokogiri ' $LOAD_PATH .unshift ' spec ' require ' rails_helper ' # RSpecのExampleGroupを再帰的に辿り、全てのExampleを取得する # @param example_group [RSpec::Core::ExampleGroup] # @return [Array<RSpec::Core::Example>] def collect_all_examples (example_group) example_group.examples + example_group.children.flat_map { collect_all_examples(_1) } end # ExampleGroupからfull_descriptionとline_numberの対応関係を生成する # @param example_group [RSpec::Core::ExampleGroup] # @return [Hash{String => Integer}] def map_description_to_line_number (example_group) collect_all_examples(example_group) .each_with_object({}) { |example, obj| obj[example.metadata[ :full_description ]] = example.metadata[ :line_number ] } end # キャッシュを利用して、ファイルパスに対応するfull_descriptionとline_numberの対応関係を生成する # @param path [String] # @return [Proc] def cached_description_to_line_number cache = {} ->(path) { cache[path] ||= begin example_group = eval ( File .read(path)) # rubocop:disable Security/Eval map_description_to_line_number(example_group) end } end # JUnit XMLファイルからRDFormatのデータを生成する # @param junit_xml_file_path [String] # @return [Array<Hash>] def parse_junit_failures (junit_xml_file_path) description_mapper = cached_description_to_line_number Nokogiri( File .open(junit_xml_file_path)).css( ' testsuite testcase failure ' ).map do |failure_elem| elem = failure_elem.parent path = elem.attr( ' file ' ) description_to_line = description_mapper.call(path) { message : failure_elem.text, location : { path : path, range : { start : { line : description_to_line[elem.attr( ' name ' )] } } } } end end File .open( ENV .fetch( ' REVIEWDOG_JSON_FILE_PATH ' ), ' w ' ) do |f| Dir [ ENV .fetch( ' JUNIT_XML_FILE_PATH_PATTERN ' )].each do |junit_xml_file_path| rows = parse_junit_failures(junit_xml_file_path) f.puts(rows.map(& :to_json ).join( "\n" )) if rows.present? end end 上記スクリプトを利用して、TestワークフローにGitHubのCheck annotationsで失敗したテスト情報を表示する report-failed-tests ジョブを追加しました。 report-failed-tests : needs : test runs-on : ubuntu-latest timeout-minutes : 5 continue-on-error : true permissions : contents : read pull-requests : write if : ${{ success() || failure() }} env : REVIEWDOG_JSON_FILE_NAME : rspec_reviewdog.jsonl steps : - name : Checkout uses : actions/checkout@v4 - name : Setup Ruby uses : ruby/setup-ruby@v1 with : bundler-cache : true - name : Setup reviewdog uses : reviewdog/action-setup@v1 with : reviewdog_version : v0.20.0 - name : Download all test results uses : actions/download-artifact@v4 with : pattern : test-result-* path : ${{ runner.temp }}/test-results - name : Generate RSspec reviewdog json env : JUNIT_XML_FILE_PATH_PATTERN : ${{ runner.temp }}/test-results/**/test_results/*.xml REVIEWDOG_JSON_FILE_PATH : ${{ runner.temp }}/${{ env.REVIEWDOG_JSON_FILE_NAME }} run : bundle exec ruby scripts/generate_rspec_reviewdog_json.rb - name : Run rspec reviewdog env : REVIEWDOG_GITHUB_API_TOKEN : ${{ github.token }} REVIEWDOG_JSON_FILE_PATH : ${{ runner.temp }}/${{ env.REVIEWDOG_JSON_FILE_NAME }} run : | cat $REVIEWDOG_JSON_FILE_PATH | reviewdog -f=rdjsonl -reporter=github-check report-failed-tests ジョブでは scripts/generate_rspec_reviewdog_json.rb を実行してRDFormatのファイルを作成します。作成されたRDFormatファイルは reviewdog に渡されて、 reviewdog 内でGitHub APIの Update a check run を実行します。 report-failed-tests ジョブを実行した結果、以下のように各テストの行にエラー情報が表示されるようになりました。 テストカバレッジを活用できる環境整備の方法 最後に、テストカバレッジを活用できる環境整備の方法について説明します。 octocov を使用してテストカバレッジをPull Requestにレポートする まずアプリケーションに以下の .octocov.yml ファイルを用意します。 coverage : paths : - coverage/.resultset.json acceptable : 60% codeToTestRatio : acceptable : 1:1.2 code : - "app/**/*.rb" - "lib/**/*.rb" test : - "spec/**/*_spec.rb" diff : datastores : - artifact://${GITHUB_REPOSITORY} comment : if : is_pull_request && !is_default_branch hideFooterLink : false deletePrevious : true report : if : is_default_branch datastores : - artifact://${GITHUB_REPOSITORY} .octocov.yml ファイルは以下のような設定になっています。 テストカバレッジは60%未満の時、exist status 1で終了する コードとテストの割合が 1 : 1.2 未満の時、exist status 1で終了する デフォルトブランチとのテストカバレッジの差分を表示する コメントはデフォルトブランチ以外のPull Requestの場合に行う 次にTestワークフローに以下の report-coverage ジョブを追加します。 report-coverage : needs : test runs-on : ubuntu-latest timeout-minutes : 5 permissions : contents : read pull-requests : write if : ${{ success() || failure() }} steps : - name : Checkout uses : actions/checkout@v4 - name : Setup Ruby uses : ruby/setup-ruby@v1 with : bundler-cache : true - name : Download all test results uses : actions/download-artifact@v4 with : pattern : test-result-* path : ${{ runner.temp }}/test-results - name : Aggregate all coverage resultsets run : bundle exec rails runner "require 'simplecov'; SimpleCov.collate(Dir['${{ runner.temp }}/test-results/**/coverage/.resultset*.json'], 'rails')" - name : Report coverage by octocov uses : k1LoW/octocov-action@v1 - name : Upload coverage uses : actions/upload-artifact@v4 with : name : coverage path : coverage include-hidden-files : true report-coverage ジョブはテスト結果をダウンロードし、複数 test ジョブで生成されたカバレッジデータを SimpleCov.collate で集計します。集計したカバレッジデータを k1LoW/octocov-action@v1 に与えることでPull Requestにテストカバレッジをレポートすることが出来ます。 しかし、この方法ではテストカバレッジをレポートできないケースがあります。Testワークフローは push をトリガーに実行されます。つまり、このTestワークフローの report-coverage ジョブが実行している時点でPull Requestが存在しない場合、テストカバレッジをレポートできません。そこで、 pull_request をトリガーに実行する Report coverageワークフロー ( .github/workflows/report-coverage.yml )を用意します。 name : Report coverage on : pull_request : types : opened concurrency : group : ${{ github.workflow }}-${{ github.ref }} cancel-in-progress : true env : TEST_WORKFLOW_FILE_NAME : test.yml COVERAGE_ARTIFACT_NAME : coverage defaults : run : shell : bash jobs : report-coverage : runs-on : ubuntu-latest timeout-minutes : 5 permissions : contents : read pull-requests : write actions : read env : GH_TOKEN : ${{ github.token }} steps : - name : Checkout uses : actions/checkout@v4 - name : Get run_id of test workflow id : get-run-id run : | gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "/repos/${GITHUB_REPOSITORY}/actions/workflows/${TEST_WORKFLOW_FILE_NAME}/runs?head_sha=${{ github.event.pull_request.head.sha }}&status=completed" | \ jq '.workflow_runs | sort_by(.id)[] | select(.conclusion == "success" or .conclusion == "failure") | .id' | \ jq -sr '"test-run-id=\(last)"' >> $GITHUB_OUTPUT - name : Download coverage if : ${{ steps.get-run-id.outputs.test-run-id != 'null' }} uses : actions/download-artifact@v4 with : name : ${{ env.COVERAGE_ARTIFACT_NAME }} path : coverage run-id : ${{ steps.get-run-id.outputs.test-run-id }} github-token : ${{ github.token }} - name : Coverage Report by octocov if : ${{ hashFiles('coverage/.resultset.json') }} uses : k1LoW/octocov-action@v1 Report coverageワークフローの report-coverage ジョブはTestワークフローの report-coverage ジョブで保存したcoverageアーティファクトを利用します。ただし、Report coverageワークフローがトリガーされた時点でTestワークフローのcoverageアーティファクトが存在しない場合は処理をスキップします。 このように、 push と pull_request の両方のトリガーを利用することで、テストカバレッジをPull Requestにレポートできました。 テストカバレッジの結果をGitHub Pagesでホスティングする 続いて、テストカバレッジの結果をGitHub Pagesでホスティングする方法を説明します。 Testワークフローに以下の build-github-pages ジョブと deploy-github-pages ジョブを追加します。どちらのジョブもデフォルトブランチの場合のみ実行します。 build-github-pages : needs : report-coverage runs-on : ubuntu-latest timeout-minutes : 5 if : ${{ format('refs/heads/{ 0 }', github.event.repository.default_branch) == github.ref }} steps : - name : Download coverage uses : actions/download-artifact@v4 with : name : coverage path : coverage - name : Upload pages artifact uses : actions/upload-pages-artifact@v3 with : path : coverage deploy-github-pages : needs : build-github-pages runs-on : ubuntu-latest timeout-minutes : 5 if : ${{ format('refs/heads/{ 0 }', github.event.repository.default_branch) == github.ref }} permissions : pages : write id-token : write environment : name : github-pages url : ${{ steps.deployment.outputs.page_url }} steps : - name : Deploy to GitHub Pages id : deployment uses : actions/deploy-pages@v4 build-github-pages ジョブは actions/upload-pages-artifact@v3 アクションでcoverageをアーカイブします。そして、アーカイブしたファイルを github-pages というアーティファクト名でアップロードします。 deploy-github-pages ジョブは actions/deploy-pages@v4 アクションで github-pages アーティファクトをGitHub Pagesにデプロイします。 これでテストカバレッジを活用できる環境整備ができました。 今後の展望 以上の取り組みによって、テストカバレッジを活用できる環境を整えつつ、CircleCIからGitHub Actionsへ移行出来ました。 しかし、まだ改善余地があり、以下のような課題があります。 Flaky testを検出する機能がない CircleCIにはFlaky testを検出できる テスト インサイト機能 があります。GitHub ActionsにはFlaky testを検出できる機能がないため、別途用意する必要があります。 追加・変更した実装コードのソースコードカバレッジ テストカバレッジの結果をGitHub Pagesでホスティングすることで、デフォルトブランチのテストカバレッジをソースファイル単位で確認できるようになりました。しかし、Pull Requestのテストカバレッジを確認するにはアーティファクトからダウンロードする必要があるため、Pull Request上で確認するには仕組みを考える必要があります。 今後は、これらの課題を解決する方法を検討したいと思います。 まとめ 本記事ではRailsアプリケーションの自動テスト環境をCircleCIからGitHub Actionsへ移行した際に発生した課題とその解決方法に関して紹介しました。Railsアプリケーションの自動テスト環境をCircleCIからGitHub Actionsへ移行を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、データシステム部MLOpsブロックの薄田( @udus122 )です。 この記事ではFour Keysなどの指標を活用して、定量的な根拠に基づきチームの開発生産性を改善する考え方とふりかえり手法を紹介します。 Four Keysとはデプロイ頻度、変更のリードタイム、変更障害率、平均修復時間の4つの指標からなるソフトウェアデリバリーや開発生産性の指標です。 Four Keysなど開発生産性の指標を計測し、定期的にふりかえっているけれど、なかなか具体的な改善につながらない。 そんな悩みはないでしょうか? 実際に私たちのチームで抱えていた開発生産性の改善に関する課題と解決策を紹介します。皆さんのチームで開発生産性を改善する際のご参考になれば幸いです。 目次 はじめに 目次 開発生産性の改善に取り組んだ背景 チームの改善に取り組む上での課題 Four Keysの考え方に対する理解不足 指標に含まれるノイズ ステージング環境を含めた変更障害の集計定義 変更障害の修正以外のrevert活用 幅広い業務内容による指標の上振れ ふりかえりプロセスから「要因の把握」が漏れていた チームの改善サイクルを回すために行った工夫 Four Keysの考え方についてチーム内で認識を合わせた 指標の定義と集計対象を明確化し外れ値を除外した ブランチ名でふりかえるべき失敗と些細なミスを区別した ふりかえりの準備で外れ値を除外する運用を行った 「プルリク深掘り」という手法でふりかえりを実施し、改善に繋がりやすい工夫をした 役割分担 タイムテーブル 進行の流れ 前回のふりかえり Four Keys指標の確認と変更障害プルリクの深掘り サイクルタイム分析の確認と個別のプルリクの深掘り 改善アクションの優先順位の投票 ネクストアクションの明確化 ふりかえりから生まれた改善施策と効果 まとめ 開発生産性の改善に取り組んだ背景 MLOpsブロックがFour Keysを活用した開発生産性の改善に取り組んだ背景ときっかけを紹介します。 MLOpsブロックは ZOZOTOWN や WEAR の推薦、検索といった機械学習系のマイクロサービスを開発・運用するチームです。MLプロダクトを世に出すために必要となるモデル作成以外の全てのエンジニアリングを担当しています。API開発からインフラの設計構築、CI/CDパイプラインの構築、負荷試験、アラート対応、実験基盤の整備など業務は多岐に渡ります。 MLOpsブロックでは週に1度業務のふりかえりを実施しています。ふりかえりはKPTの形式で実施していました。KPTは「Keep、Problem、Try」の3要素を検討するふりかえりのフレームワークです。 ZOZOでは開発生産性の指標を可視化するため全社的に Findy Team+ を導入しています。Findy Team+は GitHub Pull Request(以下プルリク)のステータスの変化などから開発生産性の指標を集計・可視化してくれるツールです。MLOpsブロックでもFindy Team+を使ってFour Keysなど開発生産性の指標を確認できる状態でした。 従来はふりかえりを始める前にDevOps分析(Four Keys)を確認する流れでFindy Team+を活用していました。 しかし指標を観測してもこれを活用した具体的な開発生産性の改善は滞っていました。 そんな中、2024年度の全社目標で生産性の向上が掲げられました。これをきっかけに改めて開発生産性の指標を活用して、チームの開発生産性を改善する取り組みを始めました。 チームの改善に取り組む上での課題 MLOpsブロックの開発生産性の改善における課題は、観測した指標を具体的なチームの改善行動に活かせていないことでした。 特に開発生産性の指標が変化する要因を把握できていないことが大きな問題でした。 これまでの経験から、開発生産性の指標を活用したチーム改善のプロセスは次の流れで進めるのが理想だと考えています。 開発生産性の指標の変化を観測する 指標の変化要因となった事実を特定する 事実から横展開可能な成功や改善すべき失敗を発見する 成功や失敗から具体的な改善点を決め、実行する 改善行動の結果、指標が変化したかどうかを確認する(ステップ1に戻る) 開発生産性の指標を活用してチームを改善するには、ステップ2で変化要因となった事実を把握することが重要です。ステップ3での成功や失敗原因の分析はステップ2で変化の要因となった事実を起点に考えます。以前のMLOpsブロックで行っていたように指標を見て良かった点や悪かった点を考えるだけでは不十分です。 実際MLOpsブロックでもこの「指標が変化した要因の把握」がうまくいっていませんでした。原因は大きく3つありました。 Four Keysの考え方に対する理解不足 指標に含まれるノイズ ふりかえりプロセスでの「要因の把握」の漏れ 次にそれぞれの課題について詳細を説明します。 Four Keysの考え方に対する理解不足 Four Keysの可視化はFindy Team+を利用することで実現できていました。しかしその背後にある考え方に対する理解度は浅く、指標の背後にある原因を考えたり、指標の変化から改善策を検討したりする材料が少ない状態でした。 Four Keysは単独でみるとソフトウェアデリバリーのパフォーマンスを示す指標です。チームの生産性を示す指標ではありません。Four Keysはチームのデリバリーパフォーマンスを結果としてのみ表します。そのため変化の要因やその他の影響については、Four Keysの指標のみでは判断できません。 変化の要因やその他の影響を無視すると、例えば目先のデリバリーを優先して長期的なコードの保守性を疎かにしてしまうリスクがあります。デプロイ頻度・変更リードタイムの改善を重視し過ぎて他のことが疎かになってしまうと本末転倒です。 Four Keysを提唱した DORA の研究では、背後にあるチームのCapability 1 とセットで分析し、Four Keysが生産性などの組織全体のOutcome 2 と関連があることを示しています。つまり、Four Keysを開発生産性の指標として扱う場合は単独で見るのではなく、背後に存在するCapabilityとセットで見る必要があります。 以下が最新版(2024年9月23日時点)のDORA Core Modelです( DORA | Research より引用)。 こちらの図からチームのCapability、Performance、Outcomeの関係が読み取れます。チームのCapabilityがFour Keysなどのパフォーマンス指標を説明し、パフォーマンス指標から組織のOutcomeを予測できることを示しています。大元の出発点としてチームのCapabilityがあり、Four Keysを変化させる要因であることが読み取れます。 このことから改善すべき対象はチームのCapabilityであり、Four KeysはCapabilityの改善結果を確認するための指標であるとわかります。 以前はこの考え方が念頭になかったため、指標を見ても何を改善すればよいか分からず具体的な改善施策につなげることができていませんでした。 指標に含まれるノイズ 指標に含まれるノイズが原因で、指標の変化からチームのCapabilityの改善につながる事実を抽出することが困難という問題もありました。 ここで言うノイズ 3 とは、チームのCapability以外に由来する指標の変化です。指標の集計定義や集計の仕組みの不完全さに由来するリードタイムの外れ値や例外的な変更障害などが該当します。 指標の変化からチームのCapabilityを改善するには、指標の変化がチームのCapabilityに紐づいていることが大切です。しかしこのようなノイズが指標に含まれることでチームのCapabilityに関わる事実が見えにくくなっていました。 MLOpsブロックの指標におけるノイズの原因は次の3つでした。 ステージング環境を含めた変更障害の集計定義 変更障害の切り戻し以外のRevert活用 幅広い業務内容による指標の上振れ 順番に説明します。 ステージング環境を含めた変更障害の集計定義 変更障害率の一般的な定義は「デプロイが原因で本番環境で障害が発生する割合(%)」です。cf. エリート DevOps チームであることを Four Keys プロジェクトで確認する Findy Team+の定義では不具合を含む変更が本番環境にデプロイされた時、変更障害としてカウントします。逆に言えば不具合を含む変更であっても本番環境へデプロイされていなければ変更障害としてカウントしません。 一方で私たちのチームでは本番環境の前段のステージング環境への反映においても、不具合が発生した場合は変更障害としてカウントしています。我々の変更障害の定義は「本番/ステージング環境において変更をデプロイにより意図しない不具合が発生すること」です。 なぜ通常よりも厳しい定義を採用しているのか説明します。 MLOpsブロックでは次のブランチ戦略を採用しています。 メインブランチ( main )の内容がステージング環境に反映される リリースブランチ( release )の内容が本番環境に反映される 開発作業は、メインブランチからフィーチャーブランチ(ブランチ名は任意)を作成する メインブランチに変更が加わる度に、その内容をリリースブランチに反映するリリースプルリクが自動で作成・更新される リリースする際は、メインブランチから発行されるリリースプルリクをマージする 本番環境に対する変更は常にリリースプルリクを経由して反映されます。リリースプルリクのブランチ名も常に固定です。しかしFindy Team+はブランチ名から変更障害を判定する仕組みになっているため、本番環境に対する変更が通常のリリースなのか変更障害を修復するものなのか区別できません。そのため本番環境における変更障害を集計対象に含めることができませんでした。 またリリースプルリクはメインブランチに変更が反映されると自動で作成されます。リリースプルリクにはこれからステージング環境で検証する変更も含まれます。そのためタイミングによっては検証中の変更を他の人が誤って本番リリースしてしまう危険もあります。このように、ブランチの運用ルール上ステージング環境に反映されている内容は常に本番環境にも反映される可能性がありました。そのためチームの理想はステージング環境にも変更障害がないことでした。 MLOpsブロックの業務にはCI/CD整備やインフラ構築などが含まれます。問題があると本番環境への適用前にリリースが失敗する、または問題があってもユーザー影響のある障害にはつながらない作業も少なくありません。ステージング環境での変更障害も集計に含めることで、これらの作業で発生した問題も指標に反映されてふりかえりが可能になるメリットもありました。 まとめると次の3つの理由から集計の定義にステージング環境を含める判断をしました。 Findy Team+の集計の仕組みとリリースプルリクの運用ルールの兼ね合いに問題があったため ステージング環境の変更はそのまま本番環境への影響につながるリスクがあるため ステージング環境における問題をふりかえりの俎上にのせるため 一方でこの定義変更により、本番環境に影響がなかった些細なミスも集計対象に含まれ、ノイズとなっていました。ステージング環境で発見できた小さな失敗(タイプミスなど)まで含めてふりかえりで会話し対策を検討しているとキリがありません。ふりかえりの時間は有限です。 変更障害の修正以外のrevert活用 Findy Team+では変更障害の発生を判定する際に直接的な判定は難しいため、変更障害の修正プルリクの有無で判定します。MLOpsブロックでは名前が"hotfix"または"revert"から始まるブランチを変更障害の修正プルリクのブランチ名としています。これに一致するブランチがメインブランチにマージされたことを持って変更障害の発生を集計していました。 2種類のブランチ名は、次の2つの方法で作られた修正プルリクを変更障害として集計するために使い分けています。 "hotfix": 手動で発行された変更障害の修正プルリクを判定する "revert": GitHubのPull Request revert機能 を利用した変更障害の修正プルリクを判定する Findy Team+での集計でノイズとなったのは"revert"のプルリクでした。 GitHubのPull Request revert機能で作られる"revert"プルリクの変更内容は大きく分けて2種類ありました。 変更障害の修復 一時的な設定変更などの切り戻し 前者は変更障害ですが、後者は変更障害として含めたくありません。一時的な設定変更などを切り戻す際にrevert機能を使わないことでこの問題は回避可能です。しかし、GitHubのPull Request revert機能は便利です。GitHubのUIからワンクリックで修正できる変更を手作業で行うのは無駄が多くミスのリスクも高まります。そのため他の方法で課題を解決する必要がありました。 幅広い業務内容による指標の上振れ MLOpsブロックの業務は多岐に渡るためプルリクの種類も様々です。中には簡単な設定変更や権限付与などリードタイムが極端に短く済むプルリクも多くあります。 それらはデプロイ頻度やリードタイムの指標を底上げし、結果として全体の開発生産性スコアも業界のベンチマークと比較して上振れていました。 見た目上のスコアが良くても、チームに課題がないのかというとそうではありません。これまで指標のスコアが見かけ上高くなっていたことにより、課題のあるプルリクを見逃してしまうケースがありました。 ふりかえりプロセスから「要因の把握」が漏れていた 従来のふりかえりプロセスには、指標の変化のみを見て要因となった事実を確認するステップがないという問題がありました。 従来のふりかえりの流れでは最初にFindy Team+のDevOps分析(Four Keys)を確認し、すぐにKeepとProblemの検討を始めていました。 この流れでは、指標を見てもなぜそのように変化したのか曖昧なまま改善点を出すことになります。結果として、チームの改善までつなげづらい状況となっていました。 下の図は従来のプロセスで進めたある日のふりかえりのKPTです。各人のKPTがバラバラに配置されており、具体的な問題ではなく個人のアイデアがベースとなってふりかえりが進んでいることが見て取れます。 このように指標の変化の要因が曖昧になることで起こる問題は次の通りです。 ふりかえりの主語がチームではなく個人に寄ってしまい、チームの改善に繋がるアイデアが出づらい 他のメンバーが課題の具体的な内容や大きさを把握しづらく、改善の優先度を決めにくい 問題の重要性に関わらず直近起きた問題の方が議論に上がりやすい チームの改善サイクルを回すために行った工夫 本章ではチームの開発生産性の改善に取り組む上で存在していた課題に対して、どのような解決策を取ったのか紹介します。 MLOpsブロックで開発生産性の指標を活用したチームの改善がうまくいかなかったのは、次の3つの原因から指標が変化した要因を把握できていなかったためでした。 Four Keysの考え方に対する理解不足 指標に含まれるノイズ ふりかえりプロセスから「要因の把握」が漏れていた これらの課題を解決するために、次の3つの対策を実施しました。 Four Keysの考え方についてチーム内で認識を合わせた 指標の定義と集計対象を明確化し外れ値を除外した 「プルリク深掘り」という手法でふりかえりを実施し、改善に繋がりやすい工夫をした それぞれ具体的にどのようなことを行ったのか説明します。 Four Keysの考え方についてチーム内で認識を合わせた 指標を見るだけで終わってしまっていた要因の1つは、チームのCapabilityとセットでFour Keys指標を活用できていなかったことでした。Four KeysをチームのCapabilityに絡めて評価する考え方がチーム内に浸透していなかったため、指標を見てもどの部分を改善すべきか手がかりがなく具体的な改善施策につなげられていませんでした。 対策としてFour Keysの指標と背後にあるチームのCapabilityの関係についてチーム内で認識合わせを行いました。またFour Keysのふりかえりの際に単に指標のみを評価するのではなく、チームのCapabilityの課題について合わせて議論しました。 この考えのきっかけやチームで会話をする材料として、以下の資料がとても参考になりました。 speakerdeck.com Four Keysの考え方を共有したことで、次の共通認識を得ることができました。 全体の指標の数値が基準より高くても問題が隠れている場合がある 指標のスコアを変化とその要因と合わせて把握し、それらを起点にチームのCapabilityの問題を検討することが大切 指標の定義と集計対象を明確化し外れ値を除外した 続いて、指標に含まれるノイズの問題をどのように解決したか説明します。 変更障害の数値がばらつく原因は3つありました。 ステージング環境を含めた変更障害の集計定義 変更障害の切り戻し以外のrevert活用 幅広い業務内容により、指標が上振れする Four Keys指標の考え方を共有し前回からの変化に着目すること、後述する「プルリク深掘り」の手法で変化要因を深掘ることで指標の上振れにより課題を見逃す問題は解決できました。 残る2つの課題をどのように解決したか紹介します。 ブランチ名でふりかえるべき失敗と些細なミスを区別した 変更障害の定義にステージング環境を含めることによる弊害は、本番環境に影響がなかった些細なミスまで集計対象になってしまい、ふりかえりの時間を無駄にしてしまうことでした。 この問題を解決するために、ブランチの命名ルールを分けることにしました。Findy Team+はブランチ名を元に集計対象をフィルタリングできます。MLOpsブロックではふりかえる必要がない些細なミスの修正には"fix"で始まるブランチ名を、変更障害の修正としてふりかえるべきプルリクには"hotfix"で始まるブランチ名を使用しました。 些細なミスの基準については全てのプルリクを分類できるほど明確化できておらず迷うケースは多少存在します。迷った場合に"fix"を付与すると問題を見逃すリスクがあります。そのため迷う場合や繰り返し同じ問題が発生する場合は積極的に"hotfix"のブランチ名を付与し議論の俎上に載せることを推奨しています。 ふりかえりの準備で外れ値を除外する運用を行った 前述の通り、GitHubのPull Request revert機能を利用することで本来は変更障害ではない変更が変更障害として集計され、ふりかえりのノイズとなってしまう問題がありました。 設定の切り戻しなど変更障害の修正ではない例外的なrevertプルリクには特定のラベルを付与して集計対象から除外することにしました。これにはFindy Team+のラベルフィルター機能を使っています。 一方で上記のようなブランチやラベルの運用は人に依存するためこれだけでは課題が残ります。 ブランチやラベルの運用は明確な基準を設けることが難しく個々人の判断では多少のブレが出ます。ブレを最小限に抑えるためふりかえりの前日にふりかえりのファシリテーションの担当者が集計されたプルリクを簡単に確認し、除外すべきプルリクがあるかどうかをチェックしています。 手間にはなりますが5分程度実施することで個々人の判断によるブレを防止し、ふりかえりの質を向上させています。 「プルリク深掘り」という手法でふりかえりを実施し、改善に繋がりやすい工夫をした 続いてふりかえりの具体的な進め方についてご説明します。 従来のふりかえりの問題点は、指標の変化を見てその要因となった事実を確認するステップがないことでした。これにより指標の観測から具体的なチームの改善に繋げづらくなっていました。 この課題を改善するために、指標の観測からチームの改善に繋げやすい「プルリク深掘り」という手法を考えました。 具体的には指標の変化を見てその原因となったプルリクについて深掘りをするふりかえり手法です。 プルリク深掘りの進め方について説明します。プルリク深掘りは、次のような役割分担とタイムテーブルで進めます。ツールは Miro を使っており、Miroの画面を共有しながら進めています。私たちのチームでは隔週でプルリク深掘りを実施しています。 役割分担 ファシリテーター: 1名 全体の進行を担当する 書記: 1名 ふりかえりの中で出た気づき・アイデアなどをMiro上の付箋にメモとして残す アイデア出し: 残りの全員 指標の変化やプルリクを見て、課題や改善点のアイデア出しをする タイムテーブル 時間 タイトル 概要 5分 前回のふりかえり 前回出たネクストアクションを見て、改善できているか確認する 15分 DevOps分析の確認と変更障害プルリクの深掘り ・指標の変化を確認する ・障害対応のプルリクを1つ1つ確認し、気付きをMiroにメモする ・プルリクの担当者にプルリクの概要や背景を説明してもらう ・口頭で出たものは書記役がメモし、残りのメンバーも気づいたことは積極的にメモする 15分 サイクルタイム分析の確認と個別のプルリクの深掘り ・サイクルタイムが長いプルリクを全員で見ながら、気付きや改善点をメモしていく ・プルリクの担当者にプルリクの概要や背景を説明してもらう ・口頭で出たものは書記役がメモし、残りのメンバーも気づいたことは積極的にメモする 5分 優先順位の投票 Miroにメモしたアイデアに対して投票を行い、改善アクションを検討する優先順位をつける 10分 アクションを明確化する 投票数の多いものから順に具体的なネクストアクションに落とし込み、担当者を決める 進行の流れ 前回のふりかえり プルリク深掘りは前回のネクストアクションのふりかえりから始まります。前回のネクストアクションが適切に実行されているか確認し、実行漏れがあればリマインドします。 Four Keys指標の確認と変更障害プルリクの深掘り 次にFindy Team+の「DevOps分析」を参照し、チームのFour Keys指標を確認します。 1つずつ指標を確認して改善、悪化といった変化を確認します。変更障害率のタブを確認し変更障害を修正したプルリクを一覧し、1つずつ深掘ります。 次の流れでプルリクを深掘ります。 プルリクを開き、プルリクの作成者に変更の概要を説明してもらう 他のメンバーは気になる点があれば随時質問し、課題点や改善点があれば発言するかMiroに書く 書記はメンバーの発言を都度メモする メモを整理する際のコツは、トピックごとに樹形図形式でポイントを羅列することです。改善の優先順位を決める際に、分かりやすくなります。 サイクルタイム分析の確認と個別のプルリクの深掘り サイクルタイム分析はプルリクのリードタイムを細分化する機能です。 サイクルタイム分析についてもDevOps分析と同様にまず全体の指標を見ます。次にリードタイムが長いなど他と比較して目立ったプルリクを個別で深掘ります。 以上でプルリクの深掘りは終わりです。 改善アクションの優先順位の投票 次に深掘り中に出てきたアイデアについて、 Miroの投票機能 を用いてチーム全員で投票します。 トピックや関連する発言などが書かれた全ての付箋を対象にして、1人3票でネクストアクションとしての優先度が高いと思う付箋に投票します。 得票数が高い順に詳細な議論をして、後述するネクストアクションの明確化の材料にします。 投票が終わった後で付箋をまとめて得票数トピックごとの合計で優先順位を決めます。 ネクストアクションの明確化 最後に、得票数の多い付箋から順に詳細について議論します。 この議論は課題を改善するために実施するネクストアクションを明確化することを目的としています。この時の議論のやり方は課題によって様々です。 大事なポイントは、時間内にネクストアクションの明確化と担当者の決定までをやり切ることです。ネクストアクションは、「あえて何もしない」や「改めて時間を取って話す」といったものでも問題ありません。 課題が明確な場合はすぐにネクストアクションのブレストへと移りますし、まだ課題が抽象的な場合は、改めて何が問題なのか明確にするところから始めるケースもあります。 後からふりかえりやすいようにネクストアクションの付箋の色を変えて完了です。 ふりかえりから生まれた改善施策と効果 このふりかえり手法の導入によって、滞り気味だった開発生産性の改善サイクルが再び回り始めました。 ふりかえりから生まれた開発生産性の改善施策の例は次の通りです。 他チームが関わるプルリクのレビューやマージのルールの明確化 共通のプルリクテンプレートを用意 ブランチ名からのカテゴリラベルを自動付与する仕組みを作成 これらの改善成果として、リードタイムやアウトプット数に関する主要な指標を改善できました。オープンからマージまでの時間を約半分に短縮しながら、プルリク作成数を増やせていることが分かります。 一方で変更障害率は維持できています。これはふりかえりの内容が指標の改善ではなく、Capabilityの改善にチームの意識が向いた結果です。 実際ふりかえりから生まれた改善施策はプルリクに関わるものだけではありません。トイルの削減やコミュニケーション改善、ヒヤリハット予防など幅広い施策につながっています。実際の例は次の通りです。 社内問合せの対応ルールを明確化 業務を効率化するツールの作成 バッチ処理の失敗アラートにチームメンションを付与 他チームとのコミュニケーションの場作り 今後は現状維持だった変更障害率を重点的に改善予定です。またもう1つのパフォーマンス指標である信頼性(SLOs)もふりかえりに組み込むことで多角的な視点から改善できるプロセスも検討しています。 まとめ 本記事ではチームの開発生産性を高めるために行ったふりかえり手法とその考え方について紹介しました。 ポイントはただ指標の変化を見るのではなく、その変化要因となった事実を把握しチームのCapabilityと紐づけて改善を考えることです。そのコツはノイズとなる外れ値を取り除き、ふりかえりの中でプルリクを深堀ることでした。 Four Keys指標を使ったチームの開発生産性を向上させようと考えている方は、是非参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co Capability:コードの保守性や自動テストなど技術的なものから仕事のプロセスや組織文化に関するものまで、チームの能力や機能のこと ↩ Outcome:事業の収益性や生産性、仕事の満足度などの組織全体にとって望ましい成果のこと ↩ 一般的には指標のバラツキ(分散)を指しますが、ここでは指標に含まれるバイアス(偏り)も含めてノイズと呼んでいます ↩
アバター