TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

938

はじめに こんにちは! WEARバックエンドブロックの高久です。 WEAR ではOpenAPI(Swagger)を使って、アプリやWebのクライアントが利用するAPIを定義しています。そして先日、開発効率化のために OpenAPI Generator でOpenAPIからAPIクライアントコードを自動生成、活用できるように整備をしました。その中でOpenAPI Generatorに適したOpenAPIの書き方のポイントがいくつかあったので、内容を紹介していきます。 想定読者 OpenAPIを現在利用している、またはこれから利用する予定の方 OpenAPI Generatorを利用したコード自動生成を検討している方 背景 当初WEARではAPIクライアントコードはOpenAPIでのAPI定義を基に各クライアントが手動で実装していました。しかし手動で実装すると初期の実装コストや変更時の追従コストがかかるため、開発効率化のためにOpenAPI Generatorを利用してAPIクライアントコードを自動生成することにしました。 ただ、そのままのOpenAPIでコードを自動生成しようとすると、エラーが発生したり、自動で名付けられたクラス名が不相応だったりと実用できるコードではありませんでした。そのためTry&Errorで実用可能なコードになるまで改善を繰り返しました。今回はその改善から得たOpenAPI GeneratorフレンドリーなOpenAPIの書き方を紹介します。 前提 OpenAPIのバージョンは3.0.0です。 言語によってOpenAPI Generatorの挙動が変わるため、本記事に記載する事象が全ての言語に当てはまる訳ではありません。WEARではクライアントの言語としてSwift, Kotlin, Go, TypeScriptを利用しているため、今回はそのいずれかの言語で発生した内容となります。 書き方のポイント tags、operationIdを1エンドポイントにつき1つ設定する レスポンススキーマでenumを使わない anyOf、oneOfを使わない type:object単位で/components/schemas配下にスキーマ化する enumに各言語の予約語を使用しない tags、operationIdを1エンドポイントにつき1つ設定する paths: /pet/findByStatus: get: tags: - pet operationId: findPetsByStatus 理由 tags 、 operationId は自動生成されたコードではそれぞれクラス名、メソッド名になります。 それぞれ設定しないと自動で名前が付与されてしまいます。意図しない名前が付与されてしまうことを防ぐため、1エンドポイントにつき1つ設定して適切なクラス名、メソッド名を付与するようにしています。 (以降Rubyでの生成結果) 以下、tags、operationIdを設定した例。 class PetApi ・・・ def find_pets_by_status(opts = {}) ・・・ end end 以下、tags、operationIdを設定しなかった例。このように自動で名付けられます。 class DefaultApi ・・・ def pet_find_by_status_get(opts = {}) ・・・ end end また tags はOpenAPIでは配列形式で設定が可能ですが、タグを2つ以上設定すると設定したタグのクラスに同じメソッドが重複して定義されてしまいます。そのため1エンドポイントにつきタグは1つだけ付与することを推奨します。 レスポンススキーマでenumを使わない いい例。 components: schemas: status: type: string 悪い例。 components: schemas: status: type: string enum: - placed - approved - delivered 理由 enumに値を追加したい場合に考慮することが増えるためです。 APIレスポンスのenumに値を追加したい場合、クライアントコード側でもenumが追加された状態でないと、自動生成コードでパースエラーになることがわかりました 1 。 APIとクライアントでenumを追加するタイミングを合わせる必要があるのですが、そのためにはアプリの強制アップデート等の対応が必要になってきます。その考慮が必要など対応工数が大きくかかるため、WEARではレスポンススキーマからはenumを削除し、どの値でも受け付けられるようにしました。 なお、リクエストパラメータのenumに関しては上記の課題は影響しないため、使用しても特に問題ありません。 anyOf、oneOfを使わない 理由 anyOf、oneOf はまだ完全にサポートされておらず、動作が不安定なためです。2023年3月現在、こちらの issue でまだ議論が行われております。 幸いWEARではanyOf, oneOfの使用箇所が少なかったため、使用箇所は排除し今後使わない方針としました。 type:object 単位で /components/schemas 配下にスキーマを作成し、ref参照する いい例。 components: schemas: Pet: type: object properties: id: type: integer category: $ref: '#/components/schemas/Category' Category: type: object properties: id: type: integer name: type: string 悪い例(Petオブジェクトのなかに、categoryオブジェクトを定義している) components: schemas: Pet: type: object properties: id: type: integer category: type: object properties: id: type: integer name: type: string 理由 type:object の中に更にobjectを定義すると、そのobjectのモデル名が自動で付与されてしまうためです。 以下は「悪い例」の定義で自動生成した際のソースコードです。 module OpenapiClient class PetCategory ・・・ end end 自動的に PetCategory というモデル名が名付けされています。言語によっては InlineObject{連番} というような名付けがされることもあり、可読性、保守性の面で実用的ではありません。 type:object の中にobjectが必要になったら、 /components/schemas 配下にスキーマを1つ作りref参照することで、自動で名付けされることを防げます。 また以下のようなレスポンスボディの定義も同様です。 悪い例。 paths: /pet/{petId}: get: parameters: - name: petId in: path schema: type: integer responses: '200': content: application/json: schema: type: object properties: id: type: integer name: type: string category: $ref: '#/components/schemas/Category' いい例。 paths: /pet/{petId}: get: parameters: - name: petId in: path schema: type: integer responses: '200': content: application/json: schema: $ref: '#/components/schemas/GetPetResponse' enumに各言語の予約語を使用しない 理由 自動生成したコードでシンタックスエラーになるためです。 WEARで発生した例を紹介します。あるパラメータでenumに open を使っていました。しかしKotlinでは open が修飾子に当たるため、自動生成したコードでシンタックスエラーになることがありました。各言語で予約語が異なるので、全てを考慮して命名することは難しいのですが、意識しておく必要があります。 まとめ OpenAPI GeneratorフレンドリーなOpenAPIの書き方をいくつかご紹介しました。 WEARでは修正箇所のボリュームが多かったため、問題度合いやエンドポイントから優先度を立てて修正していきました。修正コストはかかったものの、それを上回るメリットがありました。 OpenAPIを使っていてOpenAPI Generatorでのコード自動生成を検討している方がいれば、是非参考にしてみてください。 最後に WEARでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com ただしこの挙動は自動生成する言語によって変わる可能性があります。WEARではGoでパースエラーになったことを確認しています。 ↩
アバター
はじめに こんにちは。ブランドソリューション開発本部FAANSバックエンドブロックの田村です。普段はサーバサイドエンジニアとしてFAANSのバックエンドシステムの開発をしています。 FAANSとは、弊社が2022年8月に正式ローンチした、アパレル店舗のショップスタッフの販売サポートツールです。FAANSでは、データベースとしてGCPのサーバレスでドキュメント指向のNoSQLデータベースである Cloud Firestore を当初採用していました。Cloud Firestoreはサーバレスなので運用負荷が掛からず、また安価でスケーラビリティにも優れたハイパフォーマンスなデータベースです。 しかし、Cloud Firestoreを使用して開発・運用していく中で直面した様々な課題からGCPのフルマネージドのリレーショナルデータベースである Cloud SQL のPostgreSQLにデータベースのリプレイスを実施しました。 本記事では、Cloud FirestoreからPostgreSQLへのリプレイス過程を紹介します。 Cloud Firestoreを採用していた理由 FAANSのバックエンドのシステムで利用するデータベースとしてCloud Firestoreを当初採用していました。FAANSのシステムは、アパレル企業毎にその企業と紐付いたスタッフ情報やショップ情報をデータベースで管理します。ユーザーが自分の所属企業とは別の企業のデータにアクセスできてしまわないように、データベース内で企業をテナントの単位としたマルチテナンシーを実現できるデータベースが必要でした。 Cloud Firestoreにはサブコレクションという概念があり、階層構造でデータを保持できます。トップレベルのコレクションとして企業データを管理し、その配下のサブコレクションとしてその企業と紐づくデータを保持することによって、企業単位でデータを完全に分離しマルチテナンシーを安全に運用できます。 Cloud Firestore時代の開発と直面した課題 Cloud Firestoreはドキュメント指向のNoSQLデータベースであるにも関わらず、普段使い慣れているリレーショナルデータベースで求められるようなリレーションモデルの正規化思考が抜けずにデータモデリングしてしまっていて反省すべき点でもあるのですが、データを取得するために仕方なくN+1問題が発生するようなクエリを発行していました。 その結果、Web APIサーバの一部のエンドポイントではリクエストあたりのCloud Firestoreへのクエリ発行回数が極めて多い実装となり、Web APIサーバへのリクエストのレスポンスタイムが大変遅くなってしまうという問題が発生しました。 この問題を解決するために、画面仕様に合わせて適切に非正規化されたデータモデルを設計する必要があると考えました。具体的にどういうことかを、スタッフ一覧の画面を例に説明していきます。 画面仕様を「スタッフの名前とスタッフが所属している店舗の名前をリストで表示する」とします。Cloud Firestoreのデータベース設計を下記のように仮定します。 StaffMemberは、とある企業に所属しているスタッフを表現しています。Shopは、店舗ショップ情報を表現しており、スタッフはいずれかのショップに所属しているとします。必要な情報を取得する場合、StaffMemberコレクションから取得した後に、ShopIDを基にShopコレクションからデータを取得する必要があります。パフォーマンス最適化のことを考えると、1コレクションのみから取得できるのが理想です。 そのため、画面仕様に合わせてスタッフ名や所属ショップ名といったデータを適切に非正規化されたデータモデルで保持するために、そのための新しいコレクションを用意するのが良いと考えられます。以下の例では、StaffMemberInfoという新規コレクションを作成しています。 そうすることでN+1問題は回避できますが、新たな問題が生まれてしまいます。画面仕様が「スタッフ名、所属ショップ名、所属事業名のリストを表示する」に変更された場合、新たなフィールドの追加が必要となります。仕様変更の度に変更する必要があり、仕様変更に弱い作りとなります。 さらに、ショップ名等更新があるたびに非正規化している値も更新する必要があります。更新処理が複雑になったり、実装漏れによる更新忘れが発生してしまうことも考えられます。また、非正規化された設計がゆえに、依然として1リクエストあたりのクエリの発行回数が多いためWeb APIサーバーのレスポンスタイムが遅いという問題も残りました。 今後FAANSは事業や組織としても大きくなり、仕様変更の頻度も増えていくと考えられます。そのため、データはできる限り正規化して正しく保持して仕様変更に強くしておくべきだと考えていました。 PostgreSQL採用理由 マルチテナンシーの要件に対応できるデータベースを改めて技術調査したところ、PostgreSQLでRLS(Row Level Security)の機能を利用して実現できることが分かりました。Row Level Securityとは、行単位へのアクセス制御を可能にする仕組みで、 PostgreSQL Version 9.5から利用できます 。 例えば、企業情報を保持しているcompaniesというテーブルがあるとします。下記のように設定することで、特定のcompany_idに一致するデータのみ取得可能となります。 ALTER TABLE companies ENABLE ROW LEVEL SECURITY; CREATE POLICY multi_tenant_policy ON companies AS PERMISSIVE FOR ALL TO public USING ((id)::text = current_setting( ' app.company_id ' ::text)); データ取得の際は、下記のようにクエリを発行することで、company_idが 123 である企業情報を取得することが可能となります。 BEGIN ; SET LOCAL app.company_id = ' 123 ' ; SELECT * FROM companies; COMMIT ; SET LOCALにすることにより、このトランザクション内で有効なcompany_idを指定できます。ここでapp.company_idを指定せずにcompaniesを取得しようとするとエラーが出てクエリの実行に失敗します。これにより、誤って別企業のデータを閲覧してしまうことは無くなります。また、適切に正規化できていれば、画面仕様を変更する際は取得するクエリを変更するだけで済みます。 Row Level Securityで安全にマルチテナンシーを運用できる点と、 リレーショナルデータベースで採用されているリレーショナルモデルの方が画面仕様の変更に強い作りとしやすい点からPostgreSQLを採用しました。 Row Level Securityの適用方法 Row Level Securityを利用することになり、安全に運用するために必ずトランザクション内でCRUD処理を行うようにしています。指定された企業ID(companyID)で企業情報を取得する処理のGo言語による実装例を以下に示します。RunTransaction内では、前述の SET LOCAL app.company_id が実行され、必ず企業IDが設定されている状態にしています。もし、RunTransactionを忘れて取得処理を記述してしまっても、クエリの実行時にエラーで失敗するように実装していますのでそのようなミスは発生しません。 err = txm.RunTransaction(ctx, companyID, func (ctx context.Context) error { // 企業情報取得処理... }) if err != nil { return err } PostgreSQL移行実装 PostgreSQL移行の実装をするにあたり、下記の項目を決定しつつ行いました。 スキーマ管理方法 開発環境、本番環境へのDDL(Data Definition Language)適用方法 データベースアクセスライブラリの選定 既存開発と並行して移行を実装する方針 データベースマイグレーション リリース作業 スキーマ管理方法 開発時のDDLの定義方法は、自分でSQLを実行するかデータベース管理ツールでテーブル定義をするかは、個人の選択に任されています。ただし、最終的なテーブル定義は、チーム全体で揃える必要があります。 そこで利用したのが、 sqldef というツールです。sqldefは、適用先のデータベーススキーマとの差分を検出して、必要なDDLを自動で生成や実行してくれる、データベース変更管理ツールです。その中の機能で、指定のデータベースに存在するテーブルのDDLをSQLファイルとして出力できます。生成されたSQLファイルをコミットして管理することで、開発時の定義方法は異なっていたとしても、最終的な成果物は同じ形式で出力されます。 開発環境、本番環境へのDDL適用方法 適用時もsqldefを用いて、コミットされているsqlファイルと適用先のデータベースのスキーマとの間で差分を検出し、その差分のDDLを適用する形で運用しています。テーブル定義に変更が入るような改修をする場合は、差分DDLの結果をPull Requestのコメントとして自動出力されるようにしました。そうすることで、意図しない変更をしようとしていないかを確認できます。レビューで問題なくマージされた場合、CI/CDにより自動でDDL適用がされます。 まとめると、下記の流れとなります。 ローカル環境のデータベースに新規テーブル定義を追加する sqldefで生成されるSQLファイルを git commit する Pull Requestを作成して、開発環境で実行されるDDLがPull Requestのコメントに自動で出力される 問題なければ、mainブランチにマージして開発環境に自動的に適用される データベースアクセスライブラリの選定 Go言語でPostgreSQLにクエリを発行する際に使用するライブラリとして、データベースのテーブル定義を元にコードの自動生成が可能で、型安全で記述ができる SQLBoiler を採用しました。マイグレーション機能はありませんが、前述の通りマイグレーションに関してはsqldefを利用しています。 下記にデータ投入時と取得時のコード例を記載します。ここでは、企業データのモデルをCompanyとします。 データ投入例 company := &model.Company{ Name: company.Name, } if err := company.Insert(ctx, db, boil.Infer()); err != nil { return nil , err } データ取得例 企業名が、「株式会社ZOZO」のデータを取得する際を例にすると、次のように型安全で記述できます。 companies, err := model.Companies( model.CompanyWhere.Name.EQ( "株式会社ZOZO" ), ).All(ctx, db) if err != nil { return nil , err } 既存開発と並行して移行を実装する方針 FAANSのバックエンドで動作しているWeb APIサーバーのアーキテクチャとして、クリーンアーキテクチャとRepositoryパターンを採用しています。省略している箇所もありますが、アーキテクチャ図は下記のようになります。 PostgreSQLを実装するに当たり、変更箇所は下記の赤い部分となります。 PostgreSQLの実装には、Use Case層とRepository層の箇所のみ変更が必要でした。Use Case層は、RLSを適用するために若干の変更が必要でしたが、ほとんどは既存コードのままで実装できました。Repository層の部分をPostgreSQL接続に変更することで、実装が完了する流れでした。 データベースの移行を決断した当時、FAANSは既にCloud Firestoreで本番運用されている状態にあり、また既存機能の改修や新機能の開発が日々進行していました。そのため、既存機能に影響がないようにデータベース移行の開発をする必要があったので、PostgreSQL用のUse Case層とRepository層の実装は、別ファイルで作成することにしました。つまり、Cloud Firestore用のUse Case, RepositoryとPostgreSQL用のUse Case2, Repository2の、どちらのコードも混在している状態です。 ただし、Use Case層とRepository層をinterfaceで抽象化し、 wire というGo言語の依存性注入ライブラリでレイヤー間の依存性を管理するようにしていました。そうすることで、本番稼働しているCloud Firestore版の既存機能に影響を出さずに並行してPostgreSQL版の開発を進められるようにしました。 もちろん、Cloud Firestore版の実装の仕様に修正が入った場合は、PostgreSQL版の実装も同様の修正が必要になります。こちらに関しては、定期的に差分を確認して取り込むということを行っておりました。しかし、定期的に差分を取り込むといっても、手作業となるため漏れが発生していました。そこで、移行コード開発終盤時に最終確認のため、全差分チェックを行うことで漏れを防ぐように気をつけました。 PostgreSQL版の開発の途中からは、Cloud Firestore版のコードを改修する際にPostgreSQL版の実装との間に差分が発生しないようにPostgreSQL版の改修も併せて行う決まりにしたので、それ以降の実装漏れは最小限に抑えられたかと思います。反省点としては、開発速度は少し落ちてしまいますが、もう少し早い段階からこのような方針で進めておくべきだったと思います。 データベースマイグレーション Cloud FirestoreからPostgreSQLへのデータ移行では、データをマイグレーションする必要があります。そのため、Cloud Firestoreからデータを取得してPostgreSQLにデータを投入する処理を作成しました。 この処理の流れは、下記のシーケンス図で示します。 ただし、データ投入だけでは正しいデータで登録されているか不安になります。そこで、Cloud Firestoreから取得したデータとPostgreSQLから取得したデータを比較して検証し、移行の安全性を高めました。 30個ほどのテーブルを、依存関係に応じてグループ分けし、マイグレーションを実施するように実装しました。例えば、事業データは企業データに依存しているので、企業データが作成された後に事業データを作成する必要があります。このような依存関係を元に、企業はAグループ、事業はBグループのように割り振っていきました。その結果、4つのグループに絞られました。 SQLBoilerにはBulk Insertの処理が実装されておらず、また1件ずつ書き込む場合は許容できないほどの時間が掛かってしまうことが想定されました。そこで、Bulk Insertを行うSQLBoilerのテンプレートファイルを作成することで、効率的にデータ投入しました。Bulk Insertテンプレートファイルは、 こちらの記事 を参考にさせていただきました。 リリース作業 リリース作業は、以下のような流れでした。 メンテナンス開始 データベースマイグレーション実施 Web APIサーバのサービス切り替え QA(品質保証)テスト メンテナンス解除 Web APIサーバのサービスの切り替えは、下記の図のようにドメイン先のサービスをPostgreSQL版に切り替えるようなイメージです。 まずは、開発環境で実際にリプレイス実施してみて、手順等に問題がないかリハーサルを行いました。リハーサル時には、表示言語の差によって付与したい権限を探すのに手間取ったり、意図したサービスの方にトラフィックが流れているかの確認方法が少し手間だったり、細かいところに気づけました。本番作業までに、それらの手間を無くすようにして、万全の状態で本番リリース作業を行ったため、スムーズに切り替えを完了できました。 リプレイスの結果 Cloud Firestore利用時は、パフォーマンス最適化のためではありましたが、別コレクションに同じ情報を保持するように非正規化を行っていました。PostgreSQLにリプレイスすることにより、正規化されたデータとして全てのデータを持つことができるようになりました。これにより、非正規化されていたデータが故の複雑な処理を実装する必要もなくなり、処理がシンプルで理解しやすくなったように思います。今後の新機能開発は、特別な場合を除いて非正規化のことを考える必要がなくなりましたので、よりスムーズに開発できそうです。 さらに、パフォーマンス最適化が出来ていなかったAPIのレスポンスタイムが、劇的に改善されました。下図から、リプレイスを実施した2022年12月15日以降でWeb APIのレイテンシーが全体的に改善されていることが分かります。 また、リリース後から一ヶ月ほどシステムの様子をみましたが、リプレイスによる目立ったシステム障害は発生しませんでした。データの表示が速くなり快適になったとのお声もいただき、リプレイスは大成功に終わりました。 さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、ブランドソリューション開発本部フロントエンド部WEAR Androidブロックの安土琢朗です。普段はファッションコーディネートアプリWEARのAndroidアプリを開発しています。 WEARではすでにXMLで書かれたレイアウトをJetpack Composeにリファクタリングする作業を進めています。作業を進める中で、Jetpack ComposeのLazyColumn利用箇所でスクロールが以前よりスムーズに動かない、初回起動時にスクロールが遅いなどのパフォーマンス問題に直面しました。 本投稿では、最適なパフォーマンスを実現する方法の1つであるベースラインプロファイルの導入の仕方について説明します。 ベースラインプロファイルとは ベースラインプロファイルはAndroid Runtime(ART)がプリコンパイルする時に使うクラスやメソッドをリスト化してあるものです。ベースラインプロファイルを使うことで起動時間とジャンクの削減、全体的なランタイムパフォーマンスの向上ができます。 それでは実際に導入をみていきましょう。 導入方法 benchmarkモジュールをアプリに追加する まずAndroid Studioの"Create New Module" でベンチマーク用のモジュールを追加するテンプレートが用意されているのでモジュールを追加します。 ベースライン プロファイルの難読化を無効にする ベンチマークに対して難読化を無効にする必要があります。appモジュール内にbenchmark-rules.proというファイルを作成します。 benchmark-rules.pro # Disables obfuscation for benchmark builds. -dontobfuscate 次に、appモジュールのbuild.gradle.ktsでbenchmark buildTypeを変更し、作成したファイルを追加します。 app/build.gradle.kts buildTypes { create("benchmark") { signingConfig = signingConfigs.getByName("debug") matchingFallbacks += listOf("release") proguardFiles("benchmark-rules.pro") } } ベースラインプロファイルのジェネレータを作成する ベースラインプロファイルを生成するために、benchmarkモジュールにテストクラスBaselineProfileGeneratorを作成します。 BaselineProfileGenerator.kt @ExperimentalBaselineProfilesApi class BaselineProfileGenerator { companion object { const val PACKAGE_NAME = "com.example.myapplication" } @get:Rule val baselineProfileRule = BaselineProfileRule() @Test fun generate() = baselineProfileRule.collectBaselineProfile(PACKAGE_NAME) { startActivityAndWait() startApplication() scrollScreen() } // アプリを起動する関数 private fun MacrobenchmarkScope.startApplication() { pressHome() startActivityAndWait() device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth( 0 )), 5_000 ) val suggestions = device.findObject(By.res( "suggestions" )) val searchCondition = Until.hasObject(By.res( "coordinateTop" )) suggestions.wait(searchCondition, 5_000 ) } // リストをスクロールする関数 private fun MacrobenchmarkScope.scrollScreen() { val suggestions = device.findObject(By.res( "suggestions" )) suggestions.setGestureMargin(device.displayWidth / 5 ) suggestions.fling(Direction.DOWN) device.waitForIdle() } } startApplication() 関数では次の3点を行います。 アプリの状態が再起動になったことを確認。 デフォルトのアクティビティを開始し、最初のフレームがレンダリングされるのを待つ。 コンテンツが読み込まれてレンダリングされ、ユーザー操作が可能になるまで待つ。 scrollScreen()関数では次の2点を行います。 LazyColumnのmodifierにtestTagを追加してtagを元にスクロールできるUI要素を見つける。 リストをスクロールする。 ベースラインプロファイルのジェネレータを実行する ベースラインプロファイルを生成するには、root権限のあるAndroid9(API 28)以上のデバイスを使用する必要があります。benchmarkモジュールのbuild.gradle.ktsファイルで、Gradleで管理されているデバイスを定義します。 benchmark/build.gradle.kts testOptions { managedDevices { devices { create<ManagedVirtualDevice>("pixel2Api31") { device = "Pixel 2" apiLevel = 31 systemImageSource = "aosp" } } } } 生成されたベースラインプロファイルをアプリに適用する テストが正常に終了したら、アプリにベースラインプロファイルを適用します。生成されたファイルは /benchmark/build/outputs/ の中の managed_device_android_test_additional_output/ フォルダ内にあります。そのファイルをappモジュールにbaseline-prof.txtでコピーします。 続いて、appモジュールにprofileinstallerの依存関係を追加します。 app/build.gradle.kts dependencies { implementation("androidx.profileinstaller:profileinstaller:1.2.0") } ここまでがベースラインプロファイルを生成してアプリに適用するまでの手順です。 次に実際にアプリのパフォーマンスについて測定してみます。使うライブラリはMacrobenchmarkです。 Macrobenchmarkとは 「Jetpack Macrobenchmark」は、パフォーマンスを測定するために導入されます。起動やUIの操作、アニメーションなどのパフォーマンスを測定できます。このライブラリを使用すると、以下のことができます。 アプリを複数回測定し、起動パターンやスクロール速度で測定できます。 複数のテスト実行結果を平均化し、パフォーマンスのばらつきを抑えることができます。 アプリのコンパイル状態を制御することで、パフォーマンスの安定性に影響を与える要因を制御できます。 Google Playストアで行われるインストール時の最適化をローカルで再現して、実際のパフォーマンスを確認できます。 Macrobenchmark導入方法 Macrobenchmarkのライブラリを追加 まずbenchmarkモジュールにMacrobenchmarkのライブラリを追加します。 benchmark/build.gradle.kts dependencies { implementation "androidx.benchmark:benchmark-macro-junit4:1.1.1" } アプリのパフォーマンスを測定する アプリのパフォーマンスを測定するために以下のテストクラスを作成します。 StartupBenchmark.kt class StartupBenchmark { companion object { const val PACKAGE_NAME = "com.example.myapplication" } @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test fun startupCompilationNone() = startup(CompilationMode.None()) @Test fun startupCompilationPartial() = startup(CompilationMode.Partial()) @Test fun startupCompilationFull() = startup(CompilationMode.Full()) private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( packageName = PACKAGE_NAME, metrics = listOf(StartupTimingMetric()), iterations = 5 , compilationMode = compilationMode, startupMode = StartupMode.COLD ) { pressHome() startActivityAndWait() device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth( 0 )), 5_000 ) val suggestions = device.findObject(By.res( "suggestions" )) val searchCondition = Until.hasObject(By.res( "coordinateTop" )) suggestions.wait(searchCondition, 5_000 ) suggestions.setGestureMargin(device.displayWidth / 5 ) suggestions.fling(Direction.DOWN) device.waitForIdle() } } benchmarkRule.measureRepeated関数に以下パラメータを指定します。 packageNameは測定するアプリのパッケージ名を指定します。 metricsは測定する情報の種類を指定します。 iterationsはベンチマークを繰り返す回数を指定します。回数が多いほど結果は正確になります。 startupModeはアプリの起動方法を指定します。指定可能な値はCOLD、WARM、HOTです。 measureBlockは測定するアクション(例えば、アクティビティの開始、ボタンのクリック、スクロール、スワイプなど)を定義します。 CompilationModeを使って異なる3つのテスト関数を追加します。それぞれのテストの役割は以下です。 CompolationMode.Noneはアプリのコードをプリコンパイルしません。 CompolationMode.Partialはベンチマークを実行する前にベースラインプロファイルを読み込みプロファイルで指定されたクラス、関数をプリコンパイルします。 CompolationMode.Fullはアプリ全体をプリコンパイルします。 測定結果 次の表に中央値を示します。 timeToInitialDisplay(ms) None 394.5 Full 454.1 Partial 348.7 Noneモードの場合は全くプリコンパイルしないのでパフォーマンスが悪くなりPartialより数値は大きくなります。 Fullモードの場合はコード全体をプリコンパイルします。なので起動時にディスクの読み込みにかかるコストと命令キャッシュの負荷が増加するため、パフォーマンスは1番低くなります。 Partialモードの場合はベースラインプロファイルを使用していてユーザーがよく使うコードを優先的にプリコンパイルし、あまり重要でないコードは一時的に読み込まないようにしています。結果としてベースラインプロファイルを使用しているPartialはパフォーマンスが高い結果となりました。 まとめ 今回ベースラインプロファイルを導入することによってパフォーマンスを改善できました。ただライブラリ更新や実装が変わった時にベースラインプロファイルを都度作成する必要があるので、ワークフローを作成するようにした方が良いです。またMacrobenchmarkを使って実際にパフォーマンスを測定でき、数値で比較できました。今後その数値を元にパフォーマンスをより向上させたいと思います。 最後に ZOZOではAndroidエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください。 corp.zozo.com
アバター
はじめまして。ZOZO DevRelブロックの @wiroha です。2月1日に入社し技術広報などを担当していくことなりました。皆さまどうぞよろしくお願いいたします。 はじめに 2/22に AWSコスト削減事例祭り をAutifyさん、dipさんと共催しました。AWSを活用する3社が集まりAWSコスト削減についての事例を発表するオンラインイベントです。皆さまの関心が高いテーマのようで、約400名ものお申込をいただきました! zozotech-inc.connpass.com 素敵な配信会場はAutifyさんにご提供いただきました。ありがとうございました。 モデレータはZOZOの笹沢とdipの石川さん ※登壇者及び運営者は感染症予防をした上で、登壇時のみマスクを着用せずお話ししています 登壇内容まとめ 各社から合わせて次の4名が登壇しました。 塵も積もれば山となるコスト削減の話 (Autify 松浦) WEARのEKSコストを救いたい (ZOZO 小林) AWSコスト分析を利用したコスト最適化 (dip ジョンフンモ) プロダクト間のデータ連携をイベント駆動で作り直した話 (dip 藤中) 当日の発表はYouTubeのアーカイブでご覧下さい。 www.youtube.com 塵も積もれば山となるコスト削減の話 トップバッターのAutify 松浦さん speakerdeck.com YouTube 2:54〜 Autifyの松浦さんからは、AWSコストが急増した中で行った見直しの話がされました。スプレッドシートを作ってサービスとリージョンごとに1つ1つ優先順位をつけて改善策を検討していったそうで、地道な努力を感じました。トラフィックの削減、S3 Intelligent-Tieringの有効化、CloudWatchに送るログの精査などによってコストが劇的に減ったとのことでした。着手中の削減施策については質問も活発に行われました。 WEARのEKSコストを救いたい 2人目は弊社ZOZOの小林 speakerdeck.com YouTube 24:18〜 ZOZOの小林からは、EKSコストが10倍になったという驚きの話がありました。Fargate specのoptimize、Fargate実行時間の削減を実施し、またEKS on FargateからEKS on EC2への移行は検証中とのことです。EC2(EKS)への移行はAutify松浦さんも着手中として同じ話をされており、結果が気になりますね。 AWSコスト分析を利用したコスト最適化 3人目はdip ジョンフンモさん speakerdeck.com YouTube 49:26〜 dipのジョンフンモさんからはAWS Compute Optimizerを用いた最適化の発表がされました。AWS Compute Optimizerはリソースの最適なタイプを推奨するだけでなく、リスクも合わせて見積もってくれるのが便利だと感じました。バイトル、バイトルPROなど複数のサービスで段階的に最適化を行い、年間料金の約10%の削減に成功したそうです。 プロダクト間のデータ連携をイベント駆動で作り直した話 最後はdip 藤中さん speakerdeck.com YouTube 59:45〜 dipの藤中さんからは、コボットとバイトルという2つのプロダクトのデータ連携の発表がされました。データベースからAPIによってデータを取得するシステムから、AWS Lambdaを使ったイベント駆動のシステムに作り直すことでインフラコストを10分の1にでき、タイムラグがなくなったそうです。LambdaのCI/CD環境設計に手間取るなど、大変だったことも聞けるのは実例のありがたい点でした。 最後に 登壇者の皆さまはとても緊張されていましたが、無事に終わると笑顔で「次回も開催したい」と盛り上がっていました。それぞれ検証中の内容や今後の展望も発表いただいたので、結果が出る頃にまた開催できればと思います! ご登壇いただいた皆さま、ありがとうございました ZOZOではAWSを活用し、一緒にサービスを作り上げてくれる仲間を募集中です。今回発表のあったWEARだけでなくZOZOTOWN、オープンポジションでもSREエンジニアを募集しておりますので、ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、技術本部ML・データ部MLOpsブロックの鹿山( @Ash_Kayamin )です。MLOpsブロックではバッチ実行環境として Vertex AI Pipelines を用いています。Vertex AI PipelinesはGCPマネージドなKubeflow Pipelinesを提供するサービスで、コンテナ化した処理に依存関係をもたせたパイプラインを定義し実行できます。この記事ではVertex AI Pipelinesで起動するノードからIPアドレス制限があるエンドポイントへ通信するために、NATを利用して通信元IPアドレスを固定した方法と実装のはまりどころについてご紹介します。 Vertex AI Pipelinesの利用例については過去の記事で紹介していますので、併せてご覧ください。 techblog.zozo.com 目次 はじめに 目次 課題:Vertex AI Pipelinesで起動するノードからIPアドレス制限があるエンドポイントと通信したい 解決策:ピアリングしたVPCにNATインスタンスを作成し、NATインスタンス経由で外部通信させる 通信元IPアドレスを固定するための最小構成 手順1. プライベートサービスアクセスを用いてユーザー管理のVPCとGoogleの共有VPCをピアリングする 手順2. 外部通信用VPC、サブネットおよびNATインスタンスを作成する 手順3. カスタムルートを作成してGoogleの共有VPCにエクスポートする NATインスタンスを冗長化した構成 差分1. Managed Instance Groupを用いてNATインスタンスを2台作成する 差分2. ロードバランサーのバックエンドにManaged Instance Groupを設定し、ロードバランサー経由でNATインスタンスに接続する 差分3. Cloud NATを用いて、NATインスタンスからの外部通信の通信元IPアドレスを固定する 実際に冗長構成で通信する例 終わりに 課題:Vertex AI Pipelinesで起動するノードからIPアドレス制限があるエンドポイントと通信したい 先日、Vertex AI Pipelinesで実行するバッチの中で、IPアドレス制限を課しているエンドポイントと通信することが必要になりました。しかしながら、Vertex AI Pipelinesで起動するノードからの通信の通信元IPアドレスは固定されず、またパイプラインの実行パラメータ等で指定もできません。そのため、デフォルトではIPアドレス制限を課しているエンドポイントと通信できません。通信先のIPアドレス制限に対応するため、通信元IPアドレスを固定する方法を検討する必要がありました。 解決策:ピアリングしたVPCにNATインスタンスを作成し、NATインスタンス経由で外部通信させる GCP公式ブログ でVertex AI Pipelinesを様々なネットワーク構成で利用する方法が紹介されています。今回作成した構成はこちらを参考にさせていただきました。公式ブログで紹介されている構成のおおまかな流れは以下になります。 ユーザーが管理するVPCとGoogleの共有VPCをピアリングする。 ユーザーが管理するVPCにNATインスタンスを作成する。 Googleの共有VPCで起動するノードからの特定の宛先への外部通信がNATインスタンスを経由するようにカスタムルートを作成する。 Vertex AI pipelinesのパイプラインパラメータ VPC network に、ピアリングしたユーザー管理のVPCを指定してパイプラインを実行する。 パイプラインパラメータ VPC network にピアリングしたユーザー管理のVPCを指定することで、ピアリング先のGoogleの共有VPCでノードを起動できます。このGoogleの共有VPC内で起動したノードから特定の宛先へ外部通信する際にNATインスタンスを経由させることで、通信元IPアドレスをNATインスタンスに付与したIPアドレスに固定します。 ネットワーク構成の概略図は以下になります。こちらの詳細については後ほどご説明します。 公式ブログの構成例ではNATインスタンスの耐障害性が考慮されていません。そこで、 Managed Insntance Group(MIG) と Cloud NAT を利用することでNATインスタンスが担う機能に冗長性を持たせました。MIGはインスタンスの設定を記載したテンプレートからインスタンスを起動した上で、インスタンスのヘルスチェック・再起動・負荷に応じたスケーリング等を自動的に行ってくれます。 リージョンMIG を使用すると、インスタンスを複数のゾーンに配置できます。また、Cloud NATはGCPマネージドなNATを提供するサービスになります。まず、Vertex AI Pipelinesで起動したノードからの外部通信は、MIGで起動したNATインスタンスを経由するようにしました。加えて、NATインスタンスからの外部通信はCloud NATに付与したIPアドレスを通信元IPアドレスとするようにCloud NATを設定しました。MIGとCloud NATを合わせて利用することで、NATインスタンスに冗長性を持たせた通信元IPアドレスの固定を実現しました。 ネットワーク構成の概略図は以下になります。こちらの詳細についても後ほどご説明します。 通信元IPアドレスを固定するための最小構成 この章では、NATインスタンスを用いて通信元IPアドレス固定を実現する最小構成の構築手順と利用方法についてご説明します。 図の例では、Vertex AI Pipelinesで起動したノードから、カスタムルートを設定しているIPアドレス x.x.x.x へ通信した場合、最終的にNATインスタンスから宛先アドレス x.x.x.x へ通信元アドレス y.y.y.y で通信が行われます。ルーティングの例を以下に示します。 手順1. プライベートサービスアクセスを用いてユーザー管理のVPCとGoogleの共有VPCをピアリングする まずVPC Aを作成し、 公式ドキュメント の手順に従ってGoogleの共有VPCとピアリングします。ピアリングをすることで、VPC間で内部通信できるようになります。ピアリングにあたっては、CIDRを指定する必要があります。このCIDRはユーザー管理のVPC内の他のサブネットで利用しているCIDRと重複してはいけません。Vertex AI Pipelinesでパイプラインを実行する際のパラメータ VPC network にピアリング済みのVPC Aを指定すると、パイプラインで利用するノードのIPアドレスはここで指定するCIDRから割り当てられるようになります。 Googleの共有VPCとピアリングした際のGCPマネジメントコンソールでのVPC Peeringの表示は以下のようになります。 ここで指定するCIDRのレンジが小さいと、同じネットワークを指定して複数のパイプラインを実行した際に、IPアドレスが枯渇してパイプラインを実行できなくなる可能性があります。そのため十分な大きさのレンジを割り当てるように注意します。Vertex AI Pipelinesで実行するパイプラインに含まれる各コンポーネントの処理はVertex AI TrainingのCustom Jobとして実行されます。割り当てるレンジと、Custom Jobで起動できるノードの数の関係は 公式ドキュメント に記載があります。例えば/16を割り当てると最大63ジョブ(1ジョブ当たり8ノードと仮定)を並列実行できます。つまり、並列で63個のコンポーネントを同時に実行できますが、これ以上のコンポーネントを実行しようとするとIPアドレス不足でエラーが発生します。また、 公式ドキュメント の通り、同じGCPプロジェクト内で特定のネットワークを指定して実行しているパイプラインがある場合、そのパイプラインを実行中は別のネットワークを指定したパイプラインは実行できないことにも注意が必要です。 加えて、Vertex AI Pipelinesで起動するノードから、VPC A内のサブネットに作成するNATインスタンスへの通信を許可するため、ここで指定するCIDRからの通信を許可するファイアウォールルールをVPC Aに作成します。 手順2. 外部通信用VPC、サブネットおよびNATインスタンスを作成する 次にVPC AのサブネットA、VPC BならびにVPC BのサブネットBを作成します。そして、サブネットA、Bそれぞれに接続した2つのネットワークインタフェースを持つCompute Engineインスタンスを作成します。サブネットBに接続するネットワークインタフェースにはパブリックIPを割り当てます。Compute EngineのインスタンスをNATとして機能させるため、以下コマンドをインスタンスの起動時に実行されるスクリプトとして設定します。 # サブネットAに10.95.0.0/16, サブネットBに10.97.0.0/16を割り当てている前提で # それぞれのサブネットに接続しているネットワークインタフェース名を取得する private_interface = $( ifconfig | grep 10 . 95 -B 1 | head -n 1 | awk -F: { ' print $1 ' } ) public_interface = $( ifconfig | grep 10 . 97 -B 1 | head -n 1 | awk -F: { ' print $1 ' } ) # フォワーディングとマスカレードを有効化 sysctl -w net.ipv4.ip_forward = 1 sudo iptables -t nat -A POSTROUTING -o " $public_interface " -j MASQUERADE # サブネットBのデフォルトゲートウェイをネクストホップとするデフォルトルートを作成 sudo ip route add default via 10 . 97 . 0 . 1 dev " $public_interface " # Googleの共有VPCとのピアリングで10.98.0.0/21を割り当てている前提で # Vertex AI Pipelinesで起動したノードへの戻り通信(=宛先が10.98.0.0/21)の場合はサブネットAのデフォルトゲートウェイをネクストホップとする sudo ip route add 10 . 98 . 0 . 0 / 21 via 10 . 95 . 0 . 1 dev " $private_interface " # Cloud IAPの戻り通信(=宛先が35.235.240.0/20)の場合はサブネットAのデフォルトゲートウェイをネクストホップとする. インスタンスへIAPを用いてSSH接続したい場合に必要 sudo ip route add 35 . 235 . 240 . 0 / 20 via 10 . 95 . 0 . 1 dev " $private_interface " このスクリプトではサブネットAに属するネットワークインタフェースで受けた通信を、サブネットBに属するネットワークインタフェースからVPC Bのデフォルトゲートウェイへ送るデフォルトルートを作成します。IPマスカレードの設定とこのデフォルトルートにより、NATインスタンスのサブネットAのネットワークインタフェースで受けた通信は、サブネットBのネットワークインタフェースに割り当てたパブリックIPアドレスを通信元IPアドレスとして、サブネットBのデフォルトゲートウェイ経由で外部と通信します。 また、Vertex AI Pipelinesで起動したノードへの戻り通信を考慮したルートを追加していることに注意してください。戻り通信に対しては、行きの通信を受けたサブネットAのデフォルトゲートウェイをネクストホップとしています。このルートがないと、デフォルトルートによって戻り通信がサブネットBのデフォルトゲートウェイにルーティングされてしまい、Vertex AI Pipelinesで起動したノードと通信ができません。 手順3. カスタムルートを作成してGoogleの共有VPCにエクスポートする 最後にNATインスタンス経由で通信させたい外部IPアドレスを定め、このIPアドレスへの通信を手順2で作成したNATインタンスへルーティングするカスタムルートをVPC Aに作成します。そしてVPC Peeringの設定でカスタムルートのエクスポートを有効にし、このルートをVertex AI Pipelinesのノードが起動するGoogleの共有VPCへエクスポートします。 カスタムルートのエクスポートを有効にした際の、GCPマネジメントコンソールでのVPC Peeringの表示は以下のようになります。 ここで注意して欲しいのが、VPC Peeringではデフォルトルートならびに、通信元のIPアドレスをベースにしたルーティングを行うルートのエクスポートはサポートされていないことです。デフォルトルート(宛先IPアドレス 0.0.0.0/0 に対するカスタムルート)のエクスポートはサポートされていないので、Googleの共有VPCからの全ての通信をNATインスタンス経由にするようなルートを作成しても反映されません。デフォルトルートを作成するとGCPマネジメントコンソール上はピアリング先へのルートのエクスポートが成功した表示になります。しかしながら、実際にはこのルートは適用されないので注意してください。また、2023年1月には通信元のIPアドレスをベースにしたルーティング( Policy-based routes )がプレビュー機能として提供され始めましたが、こちらはピアリング先にはエクスポートされません。したがって、NATインスタンス経由にしたい宛先ごとにカスタムルートを作成してエクスポートする必要があります。 今回の構成において2つのVPCを作成しているのはこの理由からです。VPC Aに、通信したい特定の宛先への通信をNATインスタンスにルーティングするルートを作成するため、VPC A内からは通信したい特定の宛先への外部通信はできません。NATインスタンスから出た通信が再びNATインスタンスにルーティングされて戻ってきてしまうためです。この問題を解消するためにVPC Bを作成しVPC Aで受けた通信をVPC Bから外部通信するようにしています。 NATインスタンスを冗長化した構成 ここまで説明した最小構成ではNATインスタンスの部分が冗長化されていません。そのためNATインスタンスに問題が発生した場合、Vertex AI Pipelinesで起動したノードからカスタムルートを設定している特定の宛先への外部通信が一切行えなくなってしまいます。そこでNATインスタンスを冗長化した構成を作成しました。最小構成との差分をこの章で解説します。 再掲ですが、構築した冗長構成を以下の図に示します。 図の例では、Vertex AI Pipelinesで起動したノードから、カスタムルートを設定しているIPアドレス x.x.x.x へ通信をした場合、最終的にCloud NATから宛先アドレス x.x.x.x へ通信元アドレス y.y.y.y で通信が行われます。ルーティングの例を以下に示します。 差分1. Managed Instance Groupを用いてNATインスタンスを2台作成する まず、リージョンMIGを用いて2つの異なるゾーンでインスタンスを起動するように変更しました。MIGはインスタンスの設定を記載したテンプレートからインスタンスを起動した上で、インスタンスのヘルスチェック、再起動ならびに負荷に応じたスケーリング等を自動で行います。MIGからのヘルスチェックに対応するため、起動時に実行するスクリプトの末尾に以下のコマンドを追加しました。 # MIGからのヘルスチェックの戻り通信(=宛先が35.191.0.0/16、 130.211.0.0/22) の場合はサブネットAのデフォルトゲートウェイをネクストホップとする. MIGのヘルスチェックでの通信に必要 sudo ip route add 35 . 191 . 0 . 0 / 16 via 10 . 95 . 0 . 1 dev " $private_interface " sudo ip route add 130 . 211 . 0 . 0 / 22 via 10 . 95 . 0 . 1 dev " $private_interface " # 外部への接続が可能なことを確認するヘルスチェックエンドポイントをPythonで作成 cat <<EOF > /usr/local/sbin/health-check-server.py #!/usr/bin/env python from http.server import BaseHTTPRequestHandler、 HTTPServer import subprocess PORT_NUMBER = 8080 PING_HOST = "example.com" def connectivityCheck(): try: subprocess.check_call(["ping", "-c", "1", PING_HOST]) return True except subprocess.CalledProcessError as e: return False class MyHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == '/health-check': if connectivityCheck(): self.send_response(200) self.end_headers() self.wfile.write(b"OK") else: self.send_response(503) else: self.send_response(404) try: server = HTTPServer(("", PORT_NUMBER), MyHandler) print(f"Started httpserver on port {PORT_NUMBER}") #Wait forever for incoming http requests server.serve_forever() except KeyboardInterrupt: print("^C received, shutting down the web server") server.socket.close() EOF # ヘルスチェックエンドポイントを起動 nohup sudo python3 /usr/local/sbin/health-check-server.py > /dev/null 2 >&1 & 加えて、MIGからのヘルスチェックの通信を許可するファイアウォールルールをVPC Aに追加しました。ヘルスチェックエンドポイントではリクエストを受けると外部通信し、通信に成功した場合には200 OK、失敗した場合には503 Service Unavailableを返します。こうすることで、何らかの問題が発生してインスタンスから外部通信が行えなくなりNATインスタンスとしての役割を果たせなくなった際に、MIGで障害を検出して自動的にインスタンスを再起動できます。 差分2. ロードバランサーのバックエンドにManaged Instance Groupを設定し、ロードバランサー経由でNATインスタンスに接続する 次に、内部ロードバランサーを作成し、ロードバランサーのバックエンドに差分1で作成したMIGを設定します。そして、この内部ロードバランサーをカスタムルートでのルーティング先に変更します。ロードバランサーのヘルスチェック機能で、MIGのいずれかのインスタンスで障害が発生した場合、障害が発生したインスタンスはロードバランサーのルーティング対象から自動的に除外されます。NAT経由にしたい通信をLBを介してNATインスタンスにルーティングすることで、特定のNATインスタンスに障害が発生した場合でも、他の正常なNATインスタンスを利用して外部通信を継続できます。 差分3. Cloud NATを用いて、NATインスタンスからの外部通信の通信元IPアドレスを固定する MIG管理のインスタンスに、事前に作成した固定のパブリックIPアドレスを付与する場合、インスタンステンプレートのネットワークインタフェース設定に固定のパブリックIPアドレスを記載することになります。この場合、同じパブリックIPアドレスは1つのインスタンスにしか付与できないので、同じテンプレートを利用するMIGでは1台しかインスタンスを起動できないという問題が発生します。この問題を解決するため、サブネットBからの外部通信は全てCloud NAT経由で通信をするように変更し、NATインスタンスのネットワークインタフェースに付与していたパブリックIPアドレスは削除します。こうすることで、MIGで作成する個別のインスタンスに固定のパブリックIPアドレスを付与する必要がなくなり、MIGで複数のインスタンスを作成できるようになります。 実際に冗長構成で通信する例 冗長構成を実際に構築して通信をする例を示します。Vertex AI Pipelinesでパイプラインを実行する際のパラメータ VPC network に手順1で作成したVPC Aを指定します。手順2で予めカスタムルートを作成した宛先へ外部通信する処理をパイプライン内で行うと、Clound NAT経由での外部通信になります。 実際に通信した際のカスタムルートの設定、Cloud NATの設定、Vertex AI Pipelinesのコンテナログを以下に示します。 Vertex AI Pipelinesのコンテナログに出力されている通り、Vertex AI Pipelinesが起動したノードで実行したコンテナからifconfig.ioへcurlしています。curlの出力をみると、カスタムルートを設定しているIPアドレス 172.64.110.32 への通信をしています。そしてcurlのレスポンスを見ると、通信の通信元IPアドレスがCloudNATへ割り当てたパブリックIPアドレス 34.37.88.80 になっていることがわかります。 終わりに 今回はNATを利用し、Vertex AI Pipelinesで起動するノードから通信する際の通信元IPアドレスの固定を実現しました。これにより、Vertex AI Pipelinesで起動したノードからIPアドレス制限があるエンドポイントへ通信できるようになりました。 今後の活用方針としては例えば、複数の異なるGCPプロジェクトで実行するパイプラインから利用する共通機能を作成する際の活用が考えられます。1つのGCPプロジェクトに作成した共通機能のエンドポイントにIPアドレス制限を設けることで、異なるGCPプロジェクトで実行する通信元IPアドレスを固定したパイプラインからの外部通信は許可しつつ、他の管理されていない環境からの通信を弾くことができます。 なお複数のGCPプロジェクト間のネットワークをつなぐサービスとしてShared VPCがありますが、Vertex AI Pipelinesではその恩恵にあずかれないので注意してください。Shared VPCを用いてGCPプロジェクト間で通信可能なネットワークを構築していても、Vertex AI Pipelinesで起動するノードから他のGCPプロジェクトのリソースへは内部通信はできません。 公式ドキュメント に記載の通り、VPC Peeringでは推移的なルーティングはサポートされていません。そのため、Googleの共有VPCとShared VPCの1つのVPCをピアリングしても、直接ピアリングしたVPC間でしか通信できず、Shared VPCに含まれる他のVPCとGoogleの共有VPCは通信できません。またVertex AI Pipelinesで必要とするCIDRは比較的大きいので、Shared VPCに属するVPCをピアリング対象とするとShared VPC全体のCIDRを逼迫してしまう可能性もありおすすめできません。 今後は今回作成した仕組みを用いて、より運用負荷とセキュリティリスクを低減したMLバッチの実行環境を構築していく予定です! ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com hrmos.co
アバター
こんにちは。WEAR部iOSチームの小野寺です。 先日CollectionViewで実装しているトップページを改修しました。 改修はトップページに並べていたコンテンツを1つにまとめて、横スクロール(手動 / 自動)によってコンテンツを切り替え可能にしました。 横スクロールによってコンテンツを切り替える仕様なので、CompositionalLayoutで実装しました。 上記の方針で進めていく中で、困難な実装に直面したので紹介します。 セクション全体への装飾 最初に直面した問題が、セクション全体に対するViewの装飾です。今回画像の赤枠部分について、次のように改修が必要になりました。 トップページ改修の要件 横スクロールで、コンテンツを切り替えられるレイアウトに変更 その上に固定で表示され続けるViewを被せる(画像の赤枠) 固定で表示するViewの高さはタグ名の長さによって変わる 改修前 改修後 レイアウト調整にコストがかかる 固定で表示され続けるViewは、UICollectionReusableView, NSCollectionLayoutBoundarySupplementaryItemを使用しました。 func createLayout() -> UICollectionViewCompositionalLayout { ~~ 省略:横スクロール用のレイアウト設定 ~~ let decorationViewHeight = 150 let decorationViewSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(decorationViewHeight)) let decorationView = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: tagContainerViewSize, elementKind: “tag-container-element-kind”, alignment: .bottom, absoluteOffset: .init(x: 0, y: -decorationViewHeight)) section.boundarySupplementaryItems = [decorationView] ~~ 省略 ~~ } 追加したViewのy座標を、セクションの乗せたい位置(decorationViewHeight)までずらすことで、セクション全体へかかるようにしています。 ここではまだ、高さが変わることを考慮していないので、内容によってはレイアウトが崩れてしまいます。 Viewの高さを計算する対応を追加し、コンテンツの表示に必要な高さを確保します。 class DecorationView: UICollectionReusableView { ~~ 省略 ~~ static func calcViewHeight(title: String, containerSizeOfWidth: CGFloat) -> CGFloat { let decorationView = calculationBaseView decorationView.label.text = title decorationView.setNeedsDisplay() decorationView.layoutIfNeeded() let layoutViewSize = layoutView.systemLayoutSizeFitting(CGSize(width: containerSizeOfWidth, height: 0), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) return layoutViewSize.height } ~~ 省略 ~~ } 追加したcalcViewHeight()をdecorationViewHeightへ反映させます。 func createLayout() -> UICollectionViewCompositionalLayout { ~~ 省略:横スクロール用のレイアウト設定 ~~ // let decorationViewHeight = 150 let title = "冬がはじまるよ" let layout = collectionView.layoutAttributesForItem(at: IndexPath(item: 0, section: 0))! let decorationViewHeight = DecorationView.calcViewHeight(title: title, containerSizeOfWidth: layout.frame.width) let decorationViewSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(decorationViewHeight)) let decorationView = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: tagContainerViewSize, elementKind: “tag-container-element-kind”, alignment: .bottom, absoluteOffset: .init(x: 0, y: -decorationViewHeight)) section.boundarySupplementaryItems = [decorationView] ~~ 省略 ~~ } コンテンツの内容によって高さを取得する必要があるのでレイアウト調整にコストがかかっています。 OSによってはフッターが非表示になる さらに、iOS14.5未満のOSバージョンで、フッターがセルの裏に隠れてしまう問題もありました。 この事象に対しては、UICollectionReusableViewを使った対応が取れませんでした。 今回は後にターゲットOSを上げる予定があった為、暫定対応としてUICollectionReusableViewに直接実装したコンテンツをカスタムUIViewとして切り出しました。 事象が発生するバージョンではUICollectionReusableViewは使用せず、このカスタムUIViewをCollectionViewのSubViewとして扱う改修で対応しました。 コンテンツの自動スクロール レイアウトの改修の次は、自動でコンテンツをスクロールさせる機能の追加です。 自動スクロール機能の要件 一定時間に画面の操作がない場合に、次のセルを表示 最後のセルまで表示させたら、先頭に戻る 自動スクロール中に意図しないスクロールの発生 自動スクロールの対応は、はじめに以下の方針で検討しました。 一定時間でスクロールできるように、Timerを追加して定周期でスクロール処理を呼び出す。 スクロール処理は、scrollToItem(at:at:animated:)をデフォルトのアニメーションを有効にして、コンテンツをスクロールさせる。 Timer側からの呼び出し Timer.scheduledTimer( withTimeInterval: 3.0, repeats: true, block: { [weak self] _ in guard let self = self else { return } let toItem = displayContentIndexPathItem + 1 self.scrollToItemForCarousel(at: .init(item: toItem, section: 0)) } ) 定周期で呼び出すスクロール処理 func scrollToItemForCarousel(at indexPath: IndexPath) { collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } 実際動作させてみたところcollectionView.contentOffset.yが、アニメーションの度に改修したセクションのデフォルトの位置へと、引き戻される問題が発生しました。 原因は、scrollToItem(at:at:animated:) へ第1引数で渡しているindexPathにありました。スクロール先の指定にindexPathが使用されることで、x座標、y座標それぞれに対してscrollToItemが作用してしまいます。 これによりCollectionViewの垂直方向が、少しでもスクロールされた状況下の場合に、事象が発生してしまいました。 自作の自動スクロールのアニメーションを追加 問題を解消するために、今回はscrollToItem(at:at:animated:)のデフォルトのアニメーションは使わず、次のような自作アニメーションを実装しました。 自動スクロール前のcontentOffsetの位置を保持する scrollToItem(at:at:animated:)でスクロール位置を更新 y座標を調整前の値で更新する 2と3を1つのアニメーションとして扱う func scrollToItemForCarousel(at indexPath: IndexPath) { let currentOffset = collectionView.contentOffset UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { [weak self] in guard let collectionView = self?.collectionView else { return } collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) collectionView.contentOffset.y = currentOffset.y } } scrollToItemでのindexPathの更新とy座標をもとに戻す対応を1つのアニメーションとして実装することで、引き戻される問題を回避しています。 先頭へ戻るアニメーションにコストがかかる ここまでの対応で、自動で次のコンテンツへ切り替える対応ができました。次に最後のセルまで表示させたら、先頭に戻るアニメーションが必要です。 戻る処理についてもscrollToItem(at:at:animated:)のアニメーションをそのまま使用できれば、簡潔に対応可能です。 (第1引数へ先頭のIndexPathを指定することで、コンテンツが最終位置にいる状態から、アニメーション付きで一気に先頭へ戻す挙動を実現できます) しかし自作アニメーションを使用する場合は、次のような変更が必要になりました。 UIView.animateのcompletion内で再度自作アニメーションを呼ぶ処理を追加 引数needsRepeateを追加し、先頭のセルに戻ってくるまでこの値を有効にする needsRepeateの値が有効の間、自作アニメーションを繰り返す アニメーション処理を繰り返すことで、最後尾から先頭までのコンテンツの移動を、1つのアニメーションのように見せます。 この際durationの値を調整して、scrollToItem(at:at:animated:)のアニメーションを使った場合の挙動に近づけています。 func scrollToItemForCarousel(at indexPath: IndexPath, needsRepeate: Bool) { let currentOffset = collectionView.contentOffse let duration: TimeInterval = needsRepeate ? 0.05 : 0.3 UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut) { [weak self] in guard let collectionView = self?.collectionView else { return } collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) collectionView.contentOffset.y = currentOffset.y } completion: { [weak self] _ in let previousIndex = indexPath.item - 1 if needsRepeate { self?.scrollToItemForCarousel(at: .init(item: previousIndex, section: indexPath.section), needsRepeate: previousIndex >= 0) } } } CompostionalLayoutを使った実装は、レイアウトを組むまでは容易でしたが「セクション全体への装飾」と「コンテンツの自動スクロール」の実装が複雑になり大変でした。 CompositionalLayoutを使わない解決策 これらの実装をCompostionalLayoutを使わず、CollectionViewを中に入れたセル(ContentCollectionViewInCell)を使う方法で考えてみます。 ContentCollectionViewInCellは、横スクロールで切り替え可能なコンテンツの表示に使用するCollectionViewと装飾に使用するViewを配置したもので考えてみます。 ContentCollectionViewInCell.xib ContentCollectionViewInCell.swift class ContentCollectionViewInCell: UICollectionViewCell, UICollectionViewDataSource { @IBOutlet var collectionView: UICollectionView! @IBOutlet var decorationView: DecorationView! var contentImages: [UIImage] = [] override func awakeFromNib() { super.awakeFromNib() configureCollectionView() } func configure(contentImages: [UIImage], labelText: String) { self.contentImages = contentImages collectionView.reloadData() } private func configureCollectionView() { collectionView.register(ContentImageCell.self, forCellWithReuseIdentifier: ContentImageCell.identifier) collectionView.dataSource = self collectionView.collectionViewLayout = createPagingContentLayout() } private func createPagingContentLayout() -> UICollectionViewLayout { UICollectionViewCompositionalLayout { _, _ -> NSCollectionLayoutSection? in ~~ 省略:横スクロール用のレイアウト設定 ~~ return section } } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return contentImages.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ContentImageCell.identifier, for: indexPath) ~~ 省略:cellの中身の更新 ~~ return cell } } セクション全体を装飾するViewの調整 ContentCollectionViewInCellを使用した場合、CompostionalLayoutで苦労したセクション全体を装飾するViewの高さの調整は、オートレイアウトで解決できます。 実際に「ComposionalLayoutを使用した場合」と「ContentCollectionViewInCellを使用した場合」で実装を比較してみます。 ComposionalLayoutを使用した場合 class ViewController: UIViewController { ~~ 省略 ~~ func createLayout() -> UICollectionViewCompositionalLayout { ~~ 省略:横スクロール用のレイアウト設定 ~~ let title = "冬がはじまるよ\nアウターはMAST\n見れたらいいねスターダスト" let layout = collectionView.layoutAttributesForItem(at: IndexPath(item: 0, section: 0))! // 最前面に配置したViewの高さを計算 let decorationViewHeight = DecorationView.calcViewHeight(title: title, containerSizeOfWidth: layout.frame.width) // 最前面に配置したViewの高さの反映と位置を調整する let decorationViewSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(decorationViewHeight)) let decorationView = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: tagContainerViewSize, elementKind: “tag-container-element-kind”, alignment: .bottom, absoluteOffset: .init(x: 0, y: -decorationViewHeight)) section.boundarySupplementaryItems = [decorationView] return section } ~~ 省略 ~~ } ContentCollectionViewInCellを使用した場合 class ContentCollectionViewInCell: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate { ~~ 省略 ~~ func updateDecorationViewLabel() { decorationView.contentLabel.text = "冬がはじまるよ\nアウターはMAST\n見れたらいいねスターダスト" collectionView.reloadData() } } ContentCollectionViewInCellを使用した場合、decorationViewが持つラベルの設定とcollectionViewの更新のみで、期待した高さが反映されます。 OS差分の問題 ComposionalLayoutを使用せずにContentCollectionViewInCellを使用した場合は、iOS14.5未満とそうでないOSの差分による影響も無くなります。 コンテンツの自動スクロール機能 コンテンツの自動スクロール機能もContentCollectionViewInCellを使用した場合は、実装が容易になります。 y座標は常に固定で扱うことが可能なことから、scrollToItem(at:at:animated:)の呼び出しのみで対応することが可能になります。 実際に「ComposionalLayoutを使用した場合」と「ContentCollectionViewInCellを使用した場合」で比較すると次のようになります。 ComposionalLayoutを使用した場合 class ViewController: UIViewController { ~~ 省略 ~~ func scrollToItemForCarousel(at indexPath: IndexPath, needsRepeate: Bool) { let currentOffset = collectionView.contentOffse let duration: TimeInterval = needsRepeate ? 0.05 : 0.3 UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut) { [weak self] in guard let collectionView = self?.collectionView else { return } // 次のセルに進めるアニメーション collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) collectionView.contentOffset.y = currentOffset.y } completion: { [weak self] _ in let previousIndex = indexPath.item - 1 if needsRepeate { // 先頭のセルに戻すアニメーション self?.scrollToItemForCarousel(at: .init(item: previousIndex, section: indexPath.section), needsRepeate: previousIndex >= 0) } } } ~~ 省略 ~~ } ContentCollectionViewInCellを使用した場合 class ContentCollectionViewInCell: UICollectionViewCell, UICollectionViewDataSource { ~~ 省略 ~~ func autoScroll(toIndex: Int) { let totalContents = 5 if indexPath.item < totalContents { // 次のセルに進めるアニメーション collectionView.scrollToItem(at: .init(item: toIndex, section: 0), at: .centeredHorizontally, animated: true) } else { // 先頭のセルに戻すアニメーション collectionView.scrollToItem(at: .init(item: 0, section: 0), at: .centeredHorizontally, animated: true) } } ~~ 省略 ~~ } このように今回紹介したケース単体で見ると、CollectionViewを中に入れたセルを使う対応の方がより簡潔に済みます。 CollectionViewInCellの問題点 CollectionViewInCellにも仕様によっては、かえってコード量の増加や実装の複雑度が上がってしまう問題が考えられます。 考えられる問題 セル毎にCollectionViewの設定が必要になる 親のセルが表示するセルの状態の管理をするようになる セルの階層に比例してタップアクションのハンドリングが複雑になる 実現したいUIに対する実装が容易になる一方で、アクションに対する実装やその後の運用が困難になるといったトレードオフがあることを理解しておく必要があります。 おわりに 今回は複数のレイアウトが混在する画面の改修だったため、CompositionalLayoutを使う方針を選択しました。 これにより、横スクロールのような複雑なレイアウトに対しても簡潔に取り入れることができました。 その一方で、凝ったUIの仕様では実装の複雑度が上がってしまいました。 今回は最初に検討していた、CompositionalLayoutで実現する方法を選択しましたが、メリット・デメリットを把握して現状取れる最適な選択をすることが大事だと感じました。 WEARでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は次のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは。ML・データ部/推薦基盤ブロックの佐藤( @rayuron )です。私たちは、ZOZOTOWNのパーソナライズを実現する機械学習を用いた推薦システムを開発・運用しています。また、推薦システムの実績を定常的に確認するためのシステムも開発しています。本記事では、Lookerを用いて推薦システムの実績をモニタリングするシステムの改善に取り組んだ件についてご紹介します。 はじめに 改善の背景と課題 背景 課題 課題解決のために 要件1. 指標異常時の自動アラート 要件2. サマリの定期配信 要件3. 上記2つをSlack通知できること ダッシュボードの候補の比較 要件を満たすための設計 要件の実現方法 開発環境と本番環境 実装 ディレクトリ構成 ダッシュボード ダッシュボード構築の流れ 配信実績に関して 推薦結果に関して GitHub Actions 1. 指標異常時の自動アラート 2. サマリの定期配信 工夫した点と苦労した点 工夫した点 サマリの定期配信のフォーマット 苦労した点 アラートの閾値の決め方 改善の効果 今後の展望 おわりに 改善の背景と課題 背景 運用しているシステムの1つにメール配信を利用してシューズアイテムを訴求するシステムがあり、私たちのチームではユーザーが興味を惹くアイテムを推薦するための機械学習システムを開発・運用しています。この推薦システムの実績を定常的にモニタリングするために、 Looker Studio (旧 Data Portal)を用いてダッシュボードを構築していました。さらに、このダッシュボードに連携するためのデータを集計するシステム「モニタリングシステム」を運用しており、以下の図で構成されます。 Vertex AI PipelinesはCloud SchedulerとCloud Functionsによって1日1回定期実行されます。Vertex AI PipelinesではBigQueryのジョブを実行し、Looker Studioのダッシュボードに表示しやすい形式でデータを整形してその結果を連携用のテーブルとして保存します。Looker Studioではこの中間テーブルからデータを取得してダッシュボードを表示しています。また、週に1度、指標の変化率を以下の様にSlackで通知していました。 メール配信数: N 件(前週比:N %) 週間売上: N 円(前週比:N %) 1配信あたり流入数: N 件/配信(前週差:N pt) 1配信あたり注文数: N 件/配信(前週差:N pt) 1配信あたり売上: N 円/配信(前週差:N pt) Vertex AI Pipelinesは一般的に機械学習システムのワークフロー管理ツールとして使用されますが、私たちのチームでは推薦システムの実績をモニタリングする用途でも使用しています。Vertex AI Pipelinesの導入事例については過去のテックブログでも紹介していますのでご参照ください。 techblog.zozo.com 課題 モニタリングシステムを運用してみて課題となったのは、 指標の異常に気付くのが遅れたり、そもそも気付かないことでした。 これまでは人が能動的にダッシュボードを見に行かなければ指標の異常に気づけないという状況でした。 課題解決のために 結論を申し上げると、上記課題を解決するためにVertex AI PipelinesとLooker Studioで構築していたモニタリングシステムをLookerを使用したシステムに置換しました。ここからは代替となるシステムを検討する際の3つの要件をご紹介します。 要件1. 指標異常時の自動アラート 課題の通り、指標の異常に気づくためには人がダッシュボードを見る工程が必要でした。この工程を自動化するため、指標異常時にはアラートを自動的に通知する仕組みを実現する必要があります。異常値の定義の方法には統計学や機械学習を用いる方法がありますが、今回は簡単な閾値の判定で異常値の検知をします。 要件2. サマリの定期配信 要件1を満たすことで明らかな異常値に気づくことはできるものの、こうした閾値判定だけでは中長期的な変化を捉えることができないため、少なからず人の目での定期的なチェックが必要です。また、プロダクトを管理するという観点で指標のトレンドを把握する必要があります。 要件3. 上記2つをSlack通知できること また、私たちのチームでは通知をSlackで受け取るため上記2つをSlackに通知することが要件となります。 ダッシュボードの候補の比較 部署内では、ダッシュボードとしてLooker、Looker Studio、スプレッドシートを使用しています。そこで、活用事例があるこれらのサービスを使用して要件を満たすシステムが実現できないかを考えました。 要件 Looker Looker Studio スプレッドシート 1. 指標異常時の自動アラート ○ x x 2. サマリの定期配信 ○ ○ x 3. 上記2つをSlack通知できること ○ x x スプレッドシートは標準機能で要件を満たさず、Looker Studioは要件2のサマリをメールで送ることができましたが、Slackには通知できませんでした。そのため、全ての要件を満たした Lookerを採用することに決めました 。 要件を満たすための設計 上記の要件を実現するためにシステム設計をしました。Lookerのダッシュボードやアラートの細かな設定をすべてGitHubでバージョン管理できるようにすることと設定追加の拡張性を重視しています。 要件の実現方法 Lookerの標準機能を用いて指標異常時の自動アラートとサマリの定期配信のSlack通知を実現します。以下の図のシステム構成を考えました。 アラートと定期配信に関するyamlの設定ファイルを作成し、GitHub Actions上でLooker APIを使ってそれらの情報を設定します。アラートとサマリの定期配信の設定はLookerのUIからできますが、設定数の増大を想定してLooker APIを用います。 開発環境と本番環境 動作確認のために開発環境と本番環境に分け実装します。Lookerのインスタンスを2つ使用し、以下の図のような構成にしました。 Looker IDEからアラートと定期配信の対象となるダッシュボードのために必要なファイルを実装します。アラートと定期配信に関するyamlの設定ファイルに関しては、Looker IDE上でyamlが編集できないため、ローカルPCからPull Requestを作成します。GitHubとLookerのWebhookの設定とブランチの設定をすることで、それぞれのLookerインスタンスがGitHubの変更を反映するようにします。環境毎のブランチとGitHub Actionsの設定についてまとめると以下の様になります。 環境 ブランチ トリガー 実行内容 開発環境 main feature branchを main branchにmerge 設定ファイル記載のアラートの登録 設定ファイル記載の定期配信の登録 本番環境 release main branchを release branchにmerge 設定ファイル記載のアラートの登録 設定ファイル記載の定期配信の登録 実装 このセクションでは上記で説明した設計の具体的な実装について説明します。 ディレクトリ構成 ダッシュボード GitHub Actions ディレクトリ構成 ディレクトリ構成は以下です。異なるダッシュボードで同じLookMLファイルを参照する場合を考え、LookMLファイルを再利用しやすくするようにディレクトリを分けています。 . ├── .github │ └── workflows │ ├── main-push.yaml │ ├── main-merge.yaml │ └── release.yaml ├── README.md ├── config │   └── project_name │   ├── alert.yaml │   └── scheduled_plan.yaml ├── dashboards │   ├── *.dashboard.lookml ├── explores │   ├── *.explore.lkml ├── models │   └── *.model.lkml ├── scripts │   ├── compare_dashboards.py │   ├── set_alert.py │   └── set_scheduled_plan.py ├── tests │ └── *.test.lkml └── views └── *.view.lkml ディレクトリ名と詳細は以下です。 ディレクトリ名 詳細 .github GitHub Actionsの設定ファイル README.md READMEファイル config アラートと定期配信の設定ファイル dashboards LookML Dashboardファイル explores Lookerのexploreファイル models Lookerのmodelファイル scripts CIのスクリプト tests Lookerのtestファイル views Lookerのviewファイル ダッシュボード 指標異常時の自動アラートとサマリの定期配信は指定されたダッシュボードに対して行われるため、ダッシュボード構築が必要となります。 ダッシュボード構築の流れ Looker内で定義できるダッシュボードには ユーザー定義ダッシュボード と LookML Dashboard の2種類があります。以下の表でそれぞれを簡単に比較します。2つのダッシュボードについてより詳細な比較は 公式ドキュメント をご参照ください。 種類 ダッシュボードの作成方法 編集可能なユーザー ユーザー定義ダッシュボード UIから作成 ビジネスユーザーとLookerデベロッパー LookML Dashboard ファイルで定義 Lookerデベロッパーの選択したグループ ユーザー定義ダッシュボードはビジネスユーザーにも編集権限があります。ビジネスユーザーが意図せずダッシュボードに変更を加えてしまうことを回避するために最終的にLookML Dashboardを使用します。 以下の流れでLookML Dashboardを構築します。 Looker IDEからview、model、exploreファイルを定義 Explore UIからユーザー定義ダッシュボードを作成 ユーザー定義ダッシュボードをLookML Dashboardに変換 詳細については以下の公式ドキュメントをご参照ください。 Lookerの用語と概念 ユーザー定義ダッシュボードの作成 ユーザー定義ダッシュボードからLookML Dashboardに変換 配信実績に関して メール経由の注文率などの配信実績に関しては実績値と変化率を折れ線グラフで表示します。さらにそれらの指標を性別と年代別に分けてプロットしました。以下はダッシュボードの一例です。 メール流入率 世代別のメール流入率 推薦結果に関して 今回の改善に伴い、推薦を作成したユーザーの世代別割合やユーザーが推薦される商品画像の一部など推薦結果に関わる指標もモニタリング対象としました。 推薦を作成したユーザーの世代別割合 推薦される商品画像の一部 GitHub Actions 指標異常時の自動アラートとサマリの定期配信の設定についてGitHub Actionsを使用して実装します。 1. 指標異常時の自動アラート アラートの設定用に以下のようなyamlファイルを作成し、CI実行時に内容をLookerへ登録します。 - lookml_dashboard_id : kpi_monitoring_shoes::shoes_recommendation alerts : - lookml_link_id : visit/open cron : "0 10 * * *" name : mail_result.visit_per_open lower : 0.1 upper : 0.5 - lookml_link_id : recommend_generation cron : "0 10 * * *" name : generation lower : 0.01 upper : 0.1 field_name : member.age_tier field_value : 19 to 22 ... それぞれのパラメーターで指定できる内容は以下です。 パラメーター 内容 lookml_dashboard_id ダッシュボードのID alerts -- (alerts配下に情報を記載) lookml_link_id ダッシュボードの要素のID cron アラートのスケジュール name モニタリングする指標の列 lower 下限閾値 upper 上限閾値 field_name ピボットテーブルを使用した場合のモニタリングする指標の列 field_value ピボットテーブルを使用した場合のモニタリングする指標の値 上記のパラメーターにモニタリングしたい指標の情報を記述すると、CI実行時に設定が登録され同じタイミングですでに登録されていた設定を削除します。アラートのSlack通知のイメージは以下です。 2. サマリの定期配信 サマリの定期配信の設定用に以下のようなyamlファイルを作成し、CI実行時に内容をLookerへ登録します。 - lookml_dashboard_id : kpi_monitoring_shoes::shoes_recommendation_weekly cron : 0 10 * * thu title : shoes_recommendation_weekly slack_message : shoes_recommendation_weekly format : wysiwyg_png - lookml_dashboard_id : kpi_monitoring_shoes::shoes_recommendation_monthly cron : 0 10 4 * * title : shoes_recommendation_monthly slack_message : shoes_recommendation_monthly format : wysiwyg_png ... それぞれのパラメーターで指定できる内容は以下です。 パラメーター 内容 lookml_dashboard_id ダッシュボードのID cron 定期配信のスケジュール title 定期配信のタイトル slack_message 定期配信時のメッセージ format 通知時のファイル形式 上記のパラメーターに定期配信したいダッシュボードの情報を記述すると、CI実行時に設定が登録され同じタイミングですでに登録されていた設定を削除します。定期配信のSlack通知のイメージは以下です。 工夫した点と苦労した点 全体を通して工夫した点と苦労した点を以下で説明します。 工夫した点 サマリの定期配信のフォーマット 当初は時系列のグラフをSlackに通知することを考えていましたが、視認性の向上のため最低限の指標を載せたシンプルなダッシュボードを定期配信するようにしました。PDFよりもロードが早いPNG形式で表示し、Slackの画像をワンクリックすることで指標を確認できるようにしました。具体的には変更前と変更後でSlack通知の様子は以下の様に異なります。 変更前 変更後 苦労した点 アラートの閾値の決め方 アラートの閾値の決め方に現状も苦労しています。配信実績の閾値はプロジェクトメンバーと議論することにより共通認識を持つことができます。一方、機械学習タスクは問題設定が様々であったり、モデルが時として人間の直感に反した結果を返したりするため、閾値決めは難しいものと考えています。この点は今後の検討事項としています。 改善の効果 これまで説明してきたシステムを構築することで、以下の2つの効果を得られました。 1つ目は、指標の異常時に自動で気づく体制ができたことです。以前は、指標の異常に気付くためには人による指標のモニタリングというオペレーションが必要でした。アラート機能を実装したことによって、指標の異常時に自動的に気付く体制ができました。 2つ目は推薦を以前より高い解像度で説明できる様になったことです。思わぬ効果でしたが、モニタリングする指標が増えたことでどのようなユーザーにどのようなアイテムが推薦されているかを以前より高い解像度で認識できました。これによりユーザーやアイテムの簡単な傾向をアドホック分析なしに定常的に確認できる様になりました。 今後の展望 実装を終えてみて、今後の展望は大きく以下の4つです。 1つ目は異常値のアラートに関してです。季節の影響を強く受けるファッションドメインで、常に同じ閾値で異常値を判定することは正しい状態かを考える余地があります。また、アラートが通知された後の意思決定フローについても考える必要があります。 2つ目はサマリの定期配信に関してです。定期配信は形骸化してしまう可能性が大いにあるので、今後形骸化しない方法を考える必要があります。 3つ目はダッシュボードに載せる指標に関してです。今回は配信実績と推薦結果に関わる指標をメインでダッシュボードに載せました。一方で、今回の改善を通してLookerで機械学習モデルに関わる指標をモニタリングできると感じたので、今後検証していきたいと考えています。 4つ目はOSSとしての公開です。本記事で紹介したモニタリングシステムは将来的にOSSとして公開することを考えています。 おわりに 本記事では機械学習を用いた推薦に関するモニタリングシステムの改善の話をしてきました。現在ZOZOではパーソナライズを強化している最中で、今回の話で挙がった推薦をモニタリングするシステムの構築もその一環です。推薦に関わるエンジニアを募集しているので、ご興味がある方は是非以下のリンクからご応募ください! hrmos.co hrmos.co
アバター
こんにちは。ML・データ部データサイエンス1ブロックの尾崎です。データサイエンス1ブロックでは機械学習モデルや、データ分析によって得られたルールベースのモデルの開発をしています。特に、ZOZOTOWNやWEARの画像データを扱っています。 本記事では、教師データがないPoC特有の「モデルの評価をどうするか」という課題への対策を商品画像の色抽出の事例とともに紹介します。教師データが無いという同じ境遇に置かれた方々の一助となれば幸いです。 目次 目次 事業上の課題 どのようなモデルを作ったか モデルの評価をどうしたか 何を正解ラベルとするか アノテーションを外注するか、内製するか 評価指標の設計をどうしたか まとめ 参考 事業上の課題 アパレル商品の検索において、カラーは重要な要素の1つです。ZOZOTOWNでは15色のカラー(図1)を指定して検索できますが、より細かな粒度で商品を検索したいユースケースもあります。最近ではメイクやファッションにパーソナルカラーを取り入れることが流行しています。自分のパーソナルカラーに合う色で検索したいユーザもいることでしょう。ただ、現在のZOZOTOWNの検索だと「黄緑色」の商品に細かく絞り込みたくても、検索結果に「濃い緑色」の商品などが含まれてしまいます(図2)。 図1 ZOZOTOWNのカラー検索で使える色 図2 「緑色」の条件で絞り込んだ検索結果 詳しいカラーで検索できる機能があるとUX向上にも寄与します。そのためには、これまでのカラーデータよりも詳細なカラーデータを得る必要があります。そこで、データサイエンス1ブロックでは 教師データがない状況 から、商品画像のカラーを抽出するモデルを開発しました。 どのようなモデルを作ったか 図3が、作ったモデルによる抽出・検索結果です。カラーコードの色で検索した結果を列ごとに表示しています。教師データがない状況でここまでの精度を出すことができました。 図3 モデルによる出力結果 モデルの概要図は図4です。商品画像からカラーと比率を抽出するモデルです。 図4 カラー抽出モデル概要図 以下のステップでカラー抽出を実現しました。 商品画像からセマンティック・セグメンテーションによってファッションアイテムのマスクを抽出する 商品のカテゴリ情報から最適なマスクを選択する マスク内のピクセルのカラーをクラスタリングし、代表色にまとめる カラーはL*a*b*(以下、簡単のためLabと表記する)色空間上の3次元のベクトルで表現しています。Lab色空間は人間の視覚に近くなるよう設計されており、人間が知覚する色差をユークリッド距離で表せます。 モデルの評価をどうしたか 教師データがないので、工夫して定量評価できるようにしました。定量評価をわざわざ行えるようにした理由は実験サイクルを早く、正確に回すためです。定性評価では実験のたびに関係者へ評価をお願いするので時間がかかりますし、評価に主観が入り込んでしまいます。 次の節からその工夫を解説します。主に正解ラベル、アノテーションを外注/内製するか、評価指標の決め方を紹介します。 何を正解ラベルとするか 教師データがなくても、定量評価には正解ラベルが必要です。正解ラベルは、商品画像に含まれる「カラーのみ」としました。他にも以下の表1の案がありました。 表1 正解ラベル案の比較 「カラーのみ」を選んだ理由はアノテーションコストが最も低く、かつ検索というユースケースにおいては「カラー」と「カラー数」が評価できれば十分だったからです。 アノテーションを外注するか、内製するか 今回は内製にしました。理由は「1件あたりのアノテーション時間」を計測し、最終的にかかる時間を見積もったところ、コア業務に支障がでない時間で終わりそうだったからです。この判断により、外注費用の節約ができましたし、アノテーションがまったく終わらないという事態も回避できました。 評価指標の設計をどうしたか IoU (Intersection over Union) など、セマンティック・セグメンテーションの指標ではなく、評価指標を独自に定義する必要がありました。なぜなら、正解ラベルとして「ピクセル単位のラベル」ではなく、商品に含まれる「カラーのみ」を採用したからです。 まず、評価したい観点として以下の2つを洗い出しました。 カラーが近いか? カラー数が同じか? カラー数も評価する理由は今回のユースケースである検索において、どんなに色が近くても色数が異なればユーザ体験が損なわれると判断したからです。 この2つの観点を測るため、以下の図5のように色の近いペアごとに類似度を計算し、その和を正解と予測のうち多い方の色数で割った値を評価指標としました。 図5 独自に定義した評価指標の概要 これを数式で表現すると以下になります。 :正解カラーの集合( :Lab色空間上のベクトル) :予測カラーの集合( :Lab色空間上のベクトル) :予測カラーと正解カラーの類似度(詳しい定義は後述) なぜ、この式で2つの観点を測れるのかを解説していきます。まず、1つ目の観点である「カラーが近いか?」は、分子(類似度 を足し合わせること)で測れます。ただし、 近い色のペアのみ に絞ります。なぜなら、すべてのペアだと以下の問題があるからです。 同系色間の類似度が強く反映されてしまう 近くない色のペアによって平均化されてしまう 例えば、以下の図6の左の類似度行列の予測には緑の同系色 が含まれています。影の色を異なる同系色として抽出してしまうことがよくあります。こうなると白系のペア と比べて、緑系のペア の占める割合が高くなり、緑系が強く評価指標に反映されてしまいます。また、図6の左の方が右よりも上手く抽出できていますが、類似度の和は同じ3.3になってしまいます。これは、近くない色のペア も含めているからです。 図6 2つの類似度行列の例(説明の簡単のため、正確な値ではありません) 近い色のペアのみ に絞ることで、緑は のみが使われ、緑と白が対等に評価されます。また、左右の類似度の和は、それぞれ1.9、1.5となり「左の方が右よりも上手く抽出できている」ということを表せます。 の漸化式(以下に再掲)は、正解・予測カラーの集合から「近い色のペア=類似度が最大のペア」を順に取り出していくことを表しています。 :正解カラーの集合( :Lab色空間上のベクトル) :予測カラーの集合( :Lab色空間上のベクトル) この漸化式を図解したものが以下の図7になります。赤字が最も近いペアとして取り出されていきます。この赤字の類似度の和が分子になります。 図7 漸化式を類似度行列で図解 ところで、類似度 の定義は以下になります。 :予測・正解カラー間のユークリッド距離。Lab空間上の2点間のユークリッド距離は人間が知覚する色差に近しい 1 :関係者間で定性的に決めた「これ以上離れたら、全く異なる色に感じる距離」。データセット内の距離の最大値を使ってしまうと、ほとんどの色のペアの類似度が高くなってしまうため。 わざわざ距離を類似度に変換した理由は、最終的な指標を「何色中、何色あってるか」と解釈しやすくするためです。例えば、2色のうち1色は完全一致(類似度1)、もう1色は半分くらい似ている色(類似度0.5)のとき「2色のうち1.5色あっている」と解釈できるようにするためです。 2つ目の観点である「カラー数が同じか?」は分母(正解と予測のうち、多い方の色数 で割ること)によって測れます。他の案として類似度の平均にしてしまう、つまり、少ない方の色数 で割ってしまうと「カラー数が同じか?」を評価できません。例えば、正解が2色のとき、予測が2色でも100色でも分母は2となり同じ評価値になってしまいます。多い方の色数で割ることによって、100色より2色の方が良いことを表現できます。 以上より、今回の評価指標(以下に再掲)は、2つの観点「カラーが近いか?」と「カラー数が同じか?」を測れていると言えます。 最後に「予測の良し悪しと、評価値の良し悪しが連動するか」を具体的なデータで確認します。 図8 評価値が高い/低い例 図8の評価値が高い例では、カラーの近さとカラー数が両方とも合っています。一方、評価値が低い例では、全然違うカラーだったり、カラー数が間違っています。つまり、予測の良し悪しが評価値の良し悪しに連動していることを確認できました。このように評価値が高い/低い例を確認することで、評価指標に欠陥がないかを確認できます。この確認のおかげで「近い色のペアのみに絞る」や「多い方の色数で割る」という改善を思いつくことができました。 まとめ 本記事では、教師データがないPoC特有の「モデルの評価をどうするか」という課題に対して、商品画像の色抽出の事例とともに以下の解決策を紹介しました。 教師データが無くても、正解ラベルを用意して定量評価の方法を確立することで、実験サイクルを早く・正確に回せるようにしました。 アノテーションの外注か内製かを選ぶにあたり「1件あたりのアノテーション時間」を計測し、最終的にかかる時間を見積もりました。この判断により、外注費用の節約ができましたし、アノテーションがまったく終わらないという事態も回避できました。 独自の評価指標や正解ラベルを定義する際は「ユースケースに必要十分な評価観点を明らかにすること」と「予測の良し悪しと、指標の良し悪しが連動するかを具体的なデータで確認すること」で適切な定量評価方法を設計できました。 ZOZOではデータサイエンティスト・MLエンジニアのメンバーを募集しています。今回紹介した画像タスクに興味ある方はもちろん、幅広い分野で一緒に研究や開発を進めていけるメンバーも募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co 参考 Jain, Anil K. (1989). Fundamentals of Digital Image Processing. New Jersey, United States of America: Prentice Hall. pp. 68, 71, 73. ISBN 0-13-336165-9. ↩
アバター
はじめに こんにちは。計測プラットフォーム開発本部 計測プロデュース部の井上です。 私たちは ZOZOFIT 、 ZOZOMAT 、 ZOZOMAT for Hands や ZOZOGLASS などの計測技術に関わるプロダクトのサービス開発をしています。先日ローンチしたZOZOFITではGoogle Analytics 4(以下、GA4)を導入しました。本記事ではGA4を導入する際に工夫した点と注意点について紹介します。 目次 はじめに 目次 計測プラットフォーム開発本部 計測プロデュース部とは 計測プロダクトとデータ分析 ZOZOFITとは GA4の導入 自動計測screen_viewイベントの無効化 手動計測screen_viewイベントの実装 GA4のDebugView設定 GA4とBigQueryの連携 Looker Studio Looker Studioのフィルタ機能 Looker Studioの関数 Looker Studioの正規表現 まとめ おわりに 計測プラットフォーム開発本部 計測プロデュース部とは 計測プラットフォーム開発本部は、計測技術の活用を通して、「世界中に計測技術を通じて、新しい価値をプラスする」ことをミッションとしています。その中で計測プロデュース部は、計測サービスを企画、開発するチームです。 計測プロダクトとデータ分析 私たちが提供している計測プロダクトではGoogle Analytics(以下、GA)を活用してUI/UX向上に役立てています。例えば、GAから取得する指標に離脱数があります。計測フローの中でユーザーの離脱数が多い箇所に関して、チュートリアル動画の内容の見直すなどサービスをより使いやすくする施策を日々行っています。その他にも地域、年齢、性別、端末の種類など様々な角度から取得したデータを切り取ることで属性の違いによる傾向を分析し、サービスの強みと弱みを見出す努力をしています。 これらの分析の積み重ねが最終的にサービス全体の今後の方向性や優先順位を決める意思決定につながっています。また、データの全社的な使用性と加工性を考えて、GAで取得したデータは会社アカウントのBigQueryに連携しています。その結果、他部署のスタッフであってもデータにアクセスできる上、好きなツールでシームレスに連携が可能なので、部を跨いだ活用がなされています。最終的に収集したデータはLooker Studio(旧GoogleDataStudio)を用いて可視化し、データから得られた気付きを分かりやすい形でチームに共有しています。 ZOZOFITとは ZOZOFIT は2022年に発表した体型管理を目的としたフィットネスアプリです。現在は北米のみのローンチ展開となっています。 これまで計測プロダクトではUniversal Analytics(以下、UA)を用いて計測していましたが、Google社から2023年7月にUA クローズのアナウンス がされていることから、GA4でZOZOFITを設計する必要がありました。 GA4はGoogle社が提供するアクセス解析ツールで第4世代のものになります。特徴としてはウェブとアプリを統合したイベントを計測単位とし、プライバシーに配慮した分析(Cookieレス)ができます。以下では私たちがGA4を導入する際に工夫した点と注意点について紹介します。GA4は 公式ドキュメント を参考に導入しました。 GA4の導入 ここからはGA4の導入に際して、躓いた点や最初から知っていれば作業が捗った点などを紹介します。 自動計測screen_viewイベントの無効化 GA4では初めから 自動収集イベント が設定されています。自動収集イベントのscreen_viewイベントはデフォルトでパラメータ値(スクリーン名)が送られないため、後で分析する際にスクリーンを特定することが難しくなってしまいます。そのため、screen_viewイベントの自動計測を無効化し、任意のスクリーン名が送られるように各スクリーンにコードを実装しました。以下は自動計測screen_viewイベントの無効化の設定方法になります。 iOSの場合は、 Info.plist で FirebaseAutomaticScreenReportingEnabled を false に設定します。 <key>FirebaseAutomaticScreenReportingEnabled</key> <false/> Androidの場合は、 AndroidManifest.xml で <application> タグ内の FirebaseAutomaticScreenReportingEnabled を false にします。 <meta-data android : name = "google_analytics_automatic_screen_reporting_enabled" android : value = "false" /> 手動計測screen_viewイベントの実装 UAではページビュー、イベント(ユーザーのアクション)という概念が存在しましたが、GA4からは全てイベントを指標とする計測に変わりました。そのためUA時代のスクリーンビューは スクリーンビューイベント 、イベントは アクションイベント として設計する対応としました。例えばZOZOFITの計測結果では、計測結果の画面が表示された時は ページビューイベント 、ユーザーによるトレンドチャート 1 のタッチ時は アクションイベント が発火されるよう実装しました。 ZOZOFITの肩の計測結果の画面。画面下のトレンドチャートをタッチするとアクションイベントが発火される。 以下はコードの実装例となります。 iOS(Swift) ページビューイベント Analytics.logEvent("ScreenView", parameters: [ AnalyticsParameterScreenName: "ShoulderScreen", "ScreenCategory": "Results"]) アクションイベント Analytics.logEvent("Action", parameters: [ "ActionName": "ShoulderTrendChartTouchAction"]) Android(Kotlin) ページビューイベント firebaseAnalytics.logEvent("ScreenView") { param(FirebaseAnalytics.Param.SCREEN_NAME, "ShoulderScreen") param("ScreenCategory", "Results")} アクションイベント firebaseAnalytics.logEvent("Action") { param("ActionName", "ShoulderTrendChartTouchAction")} これらをiOSの場合は onAppear (SwiftUI)もしくは viewDidAppear (UIKit)メソッド、Androidの場合は onResume メソッドで呼び出せばイベントが発火されます。また、パラメータはGA4のデフォルトパラメータと ScreenCategory のようなカスタムパラメータを設定できます。 GA4のDebugView設定 DebugViewはGA4から新しく追加された機能で、端末を絞ってリアルタイムにトラッキング情報を確認できます。私たちは設定した内容が正しくトラッキングできるかテストするためにDebugViewを使っています。 これまでUAではリアルタイムレポートの項目からページやイベントの確認はできましたが、端末(ユーザー)を絞って確認することができませんでした。そのため、テストの際は確認用の環境を作って、確認中はなるべくテスターだけがアクセスするなどの配慮が必要でした。 また、GA4にはリアルタイムレポートの機能が引き続き存在し、その中にユーザースナップショットというDebugViewと似た機能があります。 ユーザースナップショットは候補となるユーザーがランダムに選ばれる仕様で確認したい端末を選択できない場合がありました。 以上の2つの理由からトラッキング確認にはDebugViewを採用しました。 以下はDebugViewの設定方法になります。iOSの場合は、以下のコードでデバッグモードを有効にしてDebugViewを見れるようにします。 var args = ProcessInfo.processInfo.arguments args.append("-FIRDebugEnabled") ProcessInfo.processInfo.setValue(args, forKey: "arguments") Androidの場合は、以下の設定をします。 Android検証端末の 開発者向けオプションとUSBデバッグを有効化 します。 Android検証端末を繋ぐPCで Android Debug Bridge (以下、adb)コマンドのパスを通します。 Android検証端末とPCを接続し、PCで以下のコマンドを実行します。 $ adb shell setprop debug.firebase.analytics.app PACKAGE_NAME 設定ができたら、GA4プロパティからDebugViewを確認します。 DebugViewから自身が設定したイベント名、パラメータが確認できればOK。 GA4とBigQueryの連携 先に述べた全社的な使用性に加え、GA4の データ保持期間 は最長で14カ月(年齢、性別などは2カ月)であり、14カ月以上データを保持することが考えられるためBigQueryに連携しました。BigQueryは他にも生データのクエリ分析や中間テーブルの作成、他サービスとデータの統合ができるなどのメリットがあります。デメリットとしては、1日上限の100万イベントを超えると超過料金が発生します。ちなみにUA時代にも同じ理由からBigQueryと連携していましたが、UAのデータ保持期間は最長で50カ月であったので、GA4とBigQuery連携のメリットがより高まったと言えます。 GA4の連携したいプロパティの管理画面から BigQueryのリンク を選択して連携できます。初回データの反映は連携完了後 24時間以内 でBigQueryプロジェクトにエクスポートされます。 event_params(とuser_proparties)はネストされた状態で入る。データをクエリで取り出す際はアンネストする必要があるので注意。 Looker Studio BigQueryからクエリを用いてデータ抽出できますが、クエリに不慣れな場合、データ抽出できなかったり、時間がかかってしまうことがあります。その点、Looker Studioを用いれば比較的、手軽にデータ分析できます。データの流れは下図のようになります。 Looker Studioのフィルタ機能 ここからは私たちが使っているLooker Studioの有効な機能を紹介します。Looker Studioにはデータのフィルタ機能があります。フィルタを用いれば欲しいデータに絞った分析ができます。例えば、 screen_name が StartingScreen のイベント数を集計したい場合はフィルタを以下のように設定します。 条件 パラメータ 不等号 値 一致条件 Event Param Value(String) 次に等しい(=) StartingScreen フィルタをかけないと他のパラメータが混在してしまう。 フィルタをかけることにより、StartingScreenだけに絞ったイベント数を集計できる。 Looker Studioの関数 Looker Studioでは 関数 を用いて様々な計算を行うことができます。 例えば、CASE関数を用いれば、パラメータの表示名を他の名称に置き換えることができます。この方法は報告する相手に合わせて英語名のパラメータを日本語名に変更したい場合などに有効です。ディメンションのフィールド作成から新規フィールドを開いて、以下の式を入力します。 case when Event Param Value (String)="StartingScreen" then "開始画面" else null end StartingScreen を 開始画面 に置き換える。 when以降の式を足していけば、画面が増えた時にも対応できる。 Looker Studioの正規表現 Looker Studioでは計算フィールド内で正規表現を使うことができます。そのため例えば、年代別の分析がしたい場合、以下の式を計算フィールドに入力します。 case when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^1[0-9]’) then ‘10代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^2[0-9]’) then ‘20代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^3[0-9]’) then ‘30代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^4[0-9]’) then ‘40代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^5[0-9]’) then ‘50代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^6[0-9]’) then ‘60代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^7[0-9]’) then ‘70代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^8[0-9]’) then ‘80代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^9[0-9]’) then ‘90代’ when REGEXP_MATCH(DATETIME_DIFF(TODAY(),Birth Date , YEAR),‘^1[0-9][0-9]’) then ‘100代’ else null end Birth Date(YYYYMMDD) を変数として算出する。 まとめ ここまで書いた内容はGA4の導入を始めた初期の私が知りたかった、もしくは知っていれば幾分か作業が捗ったと思う内容です。例えば、GA4から変わったトラッキングの概念や、DebugViewの新しい機能の使い方、他にもLooker Studioのフィルタや関数を用いた分析などがあります。まだまだアップデートの多いGA4ですが、今後もキャッチアップを続けていきます。今回書いた内容の他にも方法はあると思いますが、本記事が少しでも導入の手助けになれば幸いです。 おわりに 計測プラットフォーム開発本部では、今後も新しいサービスのローンチを予定しています。更にスピード感を持った開発が求められますが、このような課題に対して楽しんで取り組み、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。また、カジュアル面談も随時実施中ですのでお気軽にご応募ください。 テクニカルプロダクトマネージャー(ZOZOMAT/ZOZOFIT等) | ZOZOグループ ZOZOFITの計測結果の推移を時系列に確認できるチャート。 ↩
アバター
こんにちは。検索基盤部の倉澤です。 ZOZOTOWNには、ユーザーが検索クエリを入力した際に、入力の続きを補完したキーワードを提示するサジェスト機能があります。この機能は一般に「Query Auto Completion」と呼ばれ、素早くユーザーの検索を完了させることを目的としています。 検索基盤部では、ZOZOTOWNの商品検索だけではなくサジェスト機能の改善にも取り組んでいます。今回は近年実施したサジェスト機能の改善事例を紹介します。2年程前にまとめたサジェスト改善事例の記事も併せてご覧ください。 techblog.zozo.com 目次 目次 システム概要 インデキシングフェーズ 検索フェーズ 改善事例 ユーザーインタフェース サジェスト候補のハイライト サジェスト機能の視覚的な奥行き サジェスト候補のキーワード ブランド名やショップ名の表記揺れ 検索クエリの読み仮名と異なるサジェスト候補のキーワード 今後の展望 スペル修正 ドキュメントの生成ロジック おわりに 改善事例を紹介する前にサジェスト機能のシステム概要を紹介します。 システム概要 システム概要をインデキシングフェーズと検索フェーズに分けて簡単に説明します。 インデキシングフェーズ インデキシングフェーズでは、サジェスト候補のキーワード生成、Elasticsearchへのインデックス生成、エイリアスの更新をするバッチジョブを定期的に実行しています。実行基盤にはGoogle Kubernetes Engineを採用し、CronJobによりバッチジョブをスケジューリングしています。 サジェスト候補となるキーワードは、ユーザーの検索クエリのログから生成しています。検索クエリのログはGoogle BigQueryに日々格納しており、インデキシングフェーズではGoogle BigQueryから過去数日分の検索ログを取得しサジェスト候補のキーワードを生成しています。 また、Google BigQueryから取得する検索クエリのログには、1つも商品が表示されなかった検索クエリ(0件ヒットクエリ)も含まれています。ユーザーに0件ヒットクエリを提示することは再検索の手間を発生させ、ユーザーの離脱に繋がってしまう可能性があります。そのため、サジェスト候補を生成する際には0件ヒットクエリを除外しています。 Elasticsearchでのクエリとドキュメントのマッチングに用いられるスコアは デフォルト でBM25をベースに計算されます。私たちはデフォルトのスコアを利用せず、過去のサジェスト候補へのクリック率やクリック後の商品購入率などを利用しスコア計算しています。 検索フェーズ 検索フェーズでは、ユーザーから入力されたクエリを受け取り、ZOZOTOWNの検索機能を提供するAPIにてElasticsearchのクエリを生成します。Elasticsearchの検索クエリでは、入力されたクエリと表示されるサジェスト候補の対応をわかりやすくするため prefix match を用いています。Elasticsearchにリクエストを送信するとサジェスト候補のキーワードが返されます。 また、サジェスト候補クリック後の商品検索ロジックについてはZOZOTOWN検索の精度改善の取り組み紹介の記事で説明しています。 techblog.zozo.com 改善事例 ユーザーインタフェースやサジェスト候補のキーワードに対する改善例をいくつか紹介します。各事例に対する課題やアプローチ方法を簡単にまとめた表が以下になります。 カテゴリー 事例 概要 課題 アプローチ方法 ユーザーインタフェース サジェスト候補のハイライト 検索クエリに対するサジェスト候補のハイライト箇所について 各サジェスト候補を比較する際の視覚的負荷 検索クエリとサジェスト候補の差分箇所をハイライト ユーザーインタフェース サジェスト機能の視覚的な奥行き 検索クエリ入力時のサジェスト機能の見せ方について サジェスト機能以外のコンテンツによる検索行動への影響 ページ背景を暗くし奥行きをつける事によるサジェスト機能の強調 サジェスト候補のキーワード生成 ブランド名やショップ名の表記揺れ 検索クエリとサジェスト候補のキーワード間に発生する表記揺れについて 表記揺れしたブランド名やショップ名がサジェスト候補に表示される サジェスト候補のキーワードに対する読み仮名の取得やElasticsearchのクエリ改修 サジェスト候補のキーワード生成 検索クエリの読み仮名と異なるサジェスト候補のキーワード 日本語検索クエリの読みに対して表示すべきサジェスト候補について 日本語検索クエリの読み仮名と異なるサジェスト候補のキーワードが表示される インデキシング時や検索時に適用されるnormalizerの修正 ユーザーインタフェース 検索基盤部では、 Baymard Institute社 が提供する検索体験のガイドラインレポートを改善方針として活用していました。 Baymard Institute社のガイドラインレポートは有償ですが、オートコンプリート機能にまつわるレポート「 9 UX Best Practice Design Patterns for Autocomplete Suggestions(Only 19% Get Everything Right) 」は無償で公開されているので、こちらを交えて改善例を紹介します。 サジェスト候補のハイライト ガイドラインレポートによると、ユーザーが入力したクエリとサジェスト候補のキーワードの差分を強調することで各候補との比較が容易になり、ユーザーの視覚的な負担を低減できるとされています。 良い例と悪い例のイメージ図は以下の通りです。 サジェスト機能の視覚的な奥行き ガイドラインレポートによると、ウェブサイト・アプリ内に配置されている数多くのコンテンツはユーザーがサジェスト機能へ集中する妨げになる可能性があるとされています。 そのため私たちは、他のコンテンツとの間に境界線を設けるだけではなく、ユーザーが検索窓に入力している間はページの背景を暗くし視覚的な奥行きを付けることでサジェスト機能を強調しました。 良い例と悪い例のイメージ図は以下の通りです。 ZOZOTOWNのサジェスト機能では、以下のようにサジェスト候補のハイライトや視覚的な奥行きを持たせたユーザーインタフェースが実現されています。 サジェスト候補のキーワード インデキシングするドキュメントやElasticsearchの検索クエリに対する改善例を紹介します。 ブランド名やショップ名の表記揺れ ユーザーが入力したクエリに含まれる語とサジェスト候補のキーワードに含まれる語との間に発生する表記揺れにより、適切なサジェスト候補が取得できないという問題がありました。例えば、ユーザーが検索窓に「ゾゾタウン」と入力した場合、「ZOZOTOWN」から始まるサジェスト候補がヒットしませんでした。 この事象は特にブランドやショップに関するキーワードにおいて多く発生していたため、私たちはブランドやショップの読み仮名のデータを利用して対処しました。 サジェスト候補のキーワードの読み仮名をドキュメントの"furigana"フィールドとして追加しました。検索クエリにブランド名やショップ名の読み仮名が入力された場合、"furigana"フィールドによりドキュメントをヒットさせます。 インデキシング時にブランド名やショップ名に対する読み仮名を管理している辞書データを参照し、"furigana"フィールドへブランド名やショップ名の読み仮名を追加しています。 以下が改善後のドキュメントの一例です。"suggest_candidate"フィールドがサジェスト候補のキーワードです。 { " suggest_candidate ": " ZOZOTOWN限定 ", " furigana ": " ゾゾタウンゲンテイ " , ... } 以下が"furigana"フィールドを用いたElasticsearchの検索クエリの一部です。検索クエリ「ゾゾタウン」が上記のドキュメントへヒットするようになりました。 { " query ": { " bool ": { " should ": [ { " prefix ": { " suggest_candidate ": " ゾゾタウン " } } , { " prefix ": { " furigana ": " ゾゾタウン " } } ] } } } 検索クエリ「ゾゾタウン」を入力し「ZOZOTOWN」を含むサジェスト候補が表示されていることがわかります。 冒頭で述べたようにユーザーの検索クエリのログを用いてサジェスト候補を生成しています。そのため、サジェスト候補のキーワード生成時にブランド名やショップ名の表記が揺れている問題がありました。 例えば、正式なブランド表記は「ZOZOTOWN」だがサジェスト候補のキーワードとして「zozotown」が定義されてしまうことです。 この事象はブランド名やショップ名に対する別称や読み仮名を管理している辞書によって解決しました。サジェスト候補のキーワード生成時に行っている大まかな処理内容は以下の通りです。 検索クエリをターム分割 ターム毎に辞書を参照 参照したタームを正式なブランド名やショップ名に変換 検索クエリの読み仮名と異なるサジェスト候補のキーワード ユーザーから日本語の検索クエリ、例えば「あい」が入力された場合に「Airplane」(エアプレーン)など日本語の読み仮名と異なるサジェスト候補が表示されてしまう課題がありました。日本語の場合、読み仮名にマッチするドキュメントをヒットさせるのが自然と考えたため、この課題に取り組みました。 一方、ローマ字の検索クエリ、例えば「ai」が入力された場合に「アイシャドウ」など入力されたローマ字表記と異なるサジェスト候補が表示されていました。しかし、PCからキーボードを操作する際に意図せずローマ字表記となってしまうケースが考えられたため、日本語入力のみ対応する方針を取りました。 なぜ検索クエリ「あい」に対してドキュメント「Airplane」がヒットしていたのかを説明します。 ドキュメント「Airplane」のインデキシング時に独自定義している normalizer により最終的にローマ字変換され「ai」として登録される 入力されたクエリ「あい」も最終的にローマ字変換され「ai」となる 「ai」に対して「airplane」がprefix matchする normalizerによる変換処理を模式的に表しました。 入力 正規化 ローマ字変換 検索クエリ あい あい ai ドキュメント Airplane airplane airplane ローマ字変換を削除することで検索クエリ「あい」はドキュメント「Airplane」にヒットしなくなります。 一方、ローマ字変換を削除すると検索クエリ「ai」に対してドキュメント「アイシャドウ」はヒットしなくなります。 そのため、サジェスト候補のキーワードの振り仮名をローマ字変換した値をドキュメントの"furigana_romaji"フィールドとして追加しました。こうすることでローマ字の検索クエリに対して、"furigana_romaji"フィールドを用いてマッチさせることができ、今まで通りドキュメントをヒットさせています。 以下が"furigana_romaji"フィールドを追加したドキュメントの一例です。 { " suggest_candidate ": " アイシャドウ ", " furigana ": " あいしゃどう " , " furigana_romaji ": " aishadou ", ... } 以下が"furigana_romaji"フィールドを用いたElasticsearchの検索クエリの一部です。 { " query ": { " bool ": { " should ": [ { " prefix ": { " suggest_candidate ": " ai " } } , { " prefix ": { " furigana ": " ai " } } , { " prefix ": { " furigana_romaji ": " ai " } } ] } } } 以下の通り、検索クエリ「ai」に対して今まで通り「アイシャドウ」が表示されています。具体的なブランド名や商品名には加工処理をしています。 今後の展望 今回は主にユーザーインタフェースやサジェスト候補のキーワードを精度高く表示する改善例を中心に紹介しました。今後については以下のような視点でもサジェスト機能をさらに改善していきたいと考えています。 スペル修正 ユーザーから入力されたクエリにスペルミスがあった場合、ユーザーが意図したサジェスト候補は返されずユーザー側でクエリを修正する必要があります。ユーザーにより早く探している商品に辿り着いてもらうため、ユーザーの検索意図を理解し入力されたクエリにスペルミスがあった場合でも意図した結果を返すように改善していきたいと考えています。 ドキュメントの生成ロジック 冒頭で述べたように現状サジェスト候補のドキュメントは過去のユーザー検索ログから生成しておりますが、検索頻度が低いクエリにヒットするようなドキュメントは生成しておりません。より多くの検索クエリに対応できるようにサジェスト候補の生成ロジックの改善に取り組んでいきたいと考えています。 おわりに ZOZOでは検索エンジニア・MLエンジニアを募集しています。今回紹介した検索技術に興味ある方はもちろん、幅広い分野で一緒に研究や開発を進めていけるメンバーも募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co hrmos.co
アバター
こんにちは、MLデータ部データ基盤ブロックの奥山( @pokoyakazan )です。趣味の範疇ですが、「ぽこやかざん」という名前でラジオ投稿や大喜利の大会に出たり、「下町モルモット」というコンビで週末に漫才をしたりしています。私は普段、全社データ基盤の開発・運用を担当しており、このデータ基盤はGCPのBigQuery上に構築されています。そして、データ基盤内の各テーブルは、大きく分けて以下の2種類に分類されます。 システムDBのデータやログデータなどが、特に加工されることなく連携されている一次テーブル 一次テーブルから必要なデータを使いやすい形に集計したデータマート 本記事では、後者のデータマートを集計するジョブを制御するワークフローエンジンを、DigdagからCloud Composerに移行した事例について紹介します。Cloud Composerとは、GCPにて Apache Airflow をマネージドに提供するサービスです。 cloud.google.com なお、本記事では、Cloud Composer・Apache Airflowそれぞれのバージョンは以下のものとして話を進めます。 Cloud Composer: composer-2.0.24-airflow-2.2.5 Apache Airflow: 2.2.5 そのため、記事内で参考情報リンクとして貼っている公式ドキュメントについても、こちらのバージョンのものとなります。 目次 目次 データマート集計ジョブの仕組み 各データマートの依存関係について 移行前のシステムのデータマート集計方法 各マートのSQLファイルからマート間の依存関係グラフの作成 並列に処理しても問題ないマートをまとめた集計グループを作成 各集計グループごとにマート集計を並列実行 データマート集計ジョブの課題 1つのマートの集計が失敗すると後続のグループに属する全てのマートの集計が停止する 一次テーブルの更新遅延がデータマート集計全体の遅延に繋がる 集計グループの増加に伴いデータマート集計ジョブの実行時間が長くなる DigdagからCloud Composer(Airflow)への移行 移行の契機 Airflowでのデータマート集計方法 タスクの定義 タスク間の依存関係の設定 一次テーブルの更新待ち処理追加 Cloud Composer移行によって得られた効果 Cloud Composerの運用Tips Tips1: メタデータの読み込み方法は「読み込まれるタイミング」によって使い分ける SchedulerによるDAG解析 Variablesの読み込みはTop level codeで行わない メタデータの読み込み方法の使い分け Tips2: DAG・タスクのエラーハンドリングは目的に応じてパラメータを使い分ける 1つでもタスクがエラー終了したら保守担当者に架電 エラー終了したタスクの分だけSlack通知 Tips3: Composer環境自体の外形監視を設定する Tips4: 集計遅延の検知の仕組み 採用しなかった方法1: タスクにslaパラメータを指定 採用しなかった方法2: DAG・タスクのいずれかにタイムアウト値を設定 Tips5: プライベートIP環境で構築 まとめ データマート集計ジョブの仕組み 以下の記事でもご紹介した通り、マート集計処理の実体はデータ基盤利用者が作成したSQLファイルで、全てGitHubで管理されています。 techblog.zozo.com SQLファイルにはSELECT文のみが記述されており、UPDATEやDELETEといったDMLは記載されていません。 各データマートの依存関係について あるマートが他のマートを参照している(依存関係がある)場合、集計の順番を間違えるとデータに不整合が発生してしまいます。例えば、 existing_table1 , existing_table2 という一次テーブルが存在するとし、以下のような集計クエリを持つ5つのマートを構築したい場合を考えます。 table1.sql SELECT * FROM `project.dataset.existing_table1`; table2.sql SELECT * FROM `project.dataset.existing_table2`; table3.sql SELECT * FROM `project.dataset.table2`; table4.sql SELECT * FROM `project.dataset.table1` UNION ALL SELECT * FROM `project.dataset.table3`; table5.sql SELECT * FROM `project.dataset.table3`; この場合、「 table3 の前に table2 」「 table4 の前に table1 と table3 」「 table5 の前に table3 」が集計されている必要があります。 移行前のシステムのデータマート集計方法 Digdagでマート集計する場合、以下の流れで行います。 各マートのSQLファイルからマート間の依存関係グラフの作成 並列に処理しても問題ないマートをまとめた集計グループを作成 各集計グループごとにマート集計を並列実行 各マートのSQLファイルからマート間の依存関係グラフの作成 マート間の依存関係は、各マートのSQLファイル内の、 FROM もしくは JOIN の直後にくるマート(自己参照は除く)を調べるとわかります。 FROM , JOIN の後ろに書かれているマートは、SQLを実行するマートよりも前に集計しなければなりません。そのため、「 FROM , JOIN の後ろのマート」→「SQLを実行するマート」というように依存関係グラフを作成していきます。例えば、上記の5つのSQLからマート間の依存関係グラフを作成すると以下のようになります。 これをPythonコードで実装していきます。まず、各マートのSQLファイルから以下の正規表現を使って参照先となるマートを抽出します。 (?i)(?<=FROM|JOIN)[\s \n]*`(.+?)` そして、 参照元: 参照先 という形のDictを作成します。 { 'table1' : [], # table1の依存先 'table2' : [], # table2の依存先 'table3' : [ 'table2' ], # table3の依存先 'table4' : [ 'table1' , 'table3' ], # table4の依存先 'table5' : [ 'table3' ] # table5の依存先 } 並列に処理しても問題ないマートをまとめた集計グループを作成 作成した依存関係グラフを利用し、マートの集計順序を担保したまま、可能な限り処理を並列化していきます。具体的には、並列実行しても問題のないマート同士をグループ化します。まず、親ノードがないマート群をリストに追加し、追加したマートをグラフから削除します。そして、もう一度親ノードがないマート群をリストに追加し、追加したマートをグラフから削除…というのを繰り返していきます。 結果として、以下のような集計グループのリストができあがります。 [[table1, table2], [table3], [table4, table5]] 各集計グループごとにマート集計を並列実行 各集計グループ(マートのリスト)は、リストの先頭から順番に実行可能で、同じ集計グループ内のマートの集計は並列化できます。結果として、集計の流れは以下のようになります。 [table1, table2] を並列実行 [table3] を実行 [table4, table5] を並列実行 データマート集計ジョブの課題 上記の方法で集計すると、依存関係に沿った集計順序が担保され、同じ集計グループ内では処理を並列化できます。ただし、この方法にはいくつか課題も存在します。 1つのマートの集計が失敗すると後続のグループに属する全てのマートの集計が停止する あるマートの集計ジョブがエラー終了した場合、このマートと同じ集計グループに属するマートについては、処理が並列化されているため影響を受けません。しかし、失敗したマートが属する集計グループより後のグループは、全て処理が停止してしまいます。例えば、 table1 の集計がこけた場合、以下のようになります。 [table1, table2] → table2 は実行される [table3] → 実行されない [table4, table5] → 実行されない 集計グループ単位で見ると、先頭の集計グループの処理が失敗しているので、2番目と3番目の集計グループの処理は開始されません。そのため、 table1 に依存しない table[3, 5] の集計は実行されてほしいところですが、これらのマートの集計も停止してしまいます。 一次テーブルの更新遅延がデータマート集計全体の遅延に繋がる データマートは、一次テーブルから必要なデータを使いやすい形に加工し抽出したテーブルです。そのため、一次テーブルが更新される前に、一次テーブルを参照しているマートの集計が行われるとデータの不整合が発生します。そこで、一次テーブルを参照するマートは、一次テーブルが正常に更新されるまで集計開始を待つ必要があります。さらに、集計グループ内の1つのマートのみ集計を停止させることはできないため、その場合は集計グループ自体の実行を停止(グループ内の全マートの集計を停止)させる必要があります。つまり、なんらかの理由で一次テーブルの更新が失敗・遅延すると、この一次テーブルを参照するマートが属する集計グループ内全てのマートの集計タスクが実行されません。また、一次テーブルに依存するマートの集計タスクは先頭の集計グループに属することが多いため、一次テーブルの更新遅延はマート集計ジョブ全体の大幅な遅延に繋がります。例えば、一次テーブル existing_table1 の更新が遅延している場合、以下のようになります。 [table1, table2] → 実行されず待機 [table3] → 実行されず待機 [table4, table5] → 実行されず待機 集計グループ単位で見ると、先頭の集計グループの処理が開始されないため、2番目と3番目の集計グループの処理も開始されません。そのため、 existing_table1 に依存しない table[2, 3, 5] の集計は実行されてほしいところですが、これらのマートの集計も停止してしまいます。 集計グループの増加に伴いデータマート集計ジョブの実行時間が長くなる 現在、データマートの数は900を超えており、今も日々増え続けています。さらに、各マートは複雑に依存しあっているため、マート数の増加に伴い集計グループが増えることもあります。各集計グループごとの処理は直列に実行されるため、集計グループが増加すると、マート集計ジョブ全体の実行時間も一気に増加してしまいます。 DigdagからCloud Composer(Airflow)への移行 移行の契機 Digdagではあるマートの集計が失敗した場合、失敗した集計クエリの修正対応などを行った後に、停止していた集計グループからジョブをリトライします(RETRY FAILED)。そして、リトライしたジョブの完了時間と、エラーが発生しなかった場合の普段のジョブ完了時間との差分が遅延時間となります。マート数が少ないうちは、Digdagでも特に遅延時間が大きくなることはありませんでした。むしろ、DAG(Directed Acyclic Graph)と呼ばれるタスク間に依存関係があるジョブを、YAML書式で簡潔に定義できる点でDigdagは非常に優れています。しかし、マート数が1000近くにまで増えたため、エラーが発生した際の遅延時間がとても大きくなり、上記課題の解決が急務となりました。これらの課題は、マート1つ1つに対して依存関係を定義して集計グループを作らずに集計順序を制御できれば解決が可能ですが、現状Digdagではこういった柔軟な依存関係の定義が難しいです。そこで、より柔軟にタスク間の依存関係を定義できるAirflowへの移行を検討し始めました。Airflowを実行するインフラについては、データ基盤がGCPにあるため、他GCPサービスとの連携のしやすさを考慮しCloud Composerを利用することにしました。マネージドサービスを利用することにより、運用負荷を低減することも狙いの1つです。 Airflowでのデータマート集計方法 Airflowでも、DAGと呼ばれるジョブに、タスクと呼ばれる実際の処理の内容を定義していきます。さらに、タスクの実行設定・タスク間の依存関係を追加で設定していくことで、あとは Airflow Scheduler が設定に従ってDAGを実行してくれます。実際にマート集計DAGを記述していきます。 タスクの定義 こちらのPythonコードは、毎日7:00に実行されるマート集計DAGの一部です。 import pendulum from airflow import DAG from airflow.operators.python import PythonOperator from airflow.utils.task_group import TaskGroup def _update_datamart (**kwargs): datamart_id = kwargs[ 'datamart_id' ] ''' データマート更新処理 ''' with DAG( dag_id= 'dailybatch_datamart' , start_date=pendulum.datetime( 2023 , 1 , 1 , 7 , 0 , tz= 'Asia/Tokyo' ), schedule_interval= '0 7 * * *' , catchup= False , ) as dag: datamart_ids = [ 'table1' , 'table2' , 'table3' , 'table4' , 'table5' , ] with TaskGroup(group_id= 'mart' ) as mart: for datamart_id in datamart_ids: globals ()[datamart_id] = PythonOperator( task_id=datamart_id, on_failure_callback=_failure_notify, python_callable=_update_datamart, op_kwargs={ 'datamart_id' : datamart_id}, ) datamart_ids というマート名が格納されたリストを作成し、ループで回してタスクを定義しています。マート更新処理(SQLの実行)は、全マート共通のため、全タスクで同じ関数 _update_datamart を呼び出しています(※ on_failure_callback については後述)。 タスク間の依存関係の設定 タスク定義の次は、タスク間の依存関係を設定していきます。こちらは上記「 移行前のシステムのデータマート集計方法 」内の「 各マートのSQLファイルからマート間の依存関係グラフの作成 」までは同じ手順となります。Airflowではタスク間の依存関係を >> で定義するため、正規表現を使ってSQLファイルから参照先のマートを抽出した後に、依存関係を以下のような文字列型で定義します。 'table1 >> table4' そしてこの文字列を2次元配列に格納していきます。 datamart_dependencies = [ [], # table1の依存先 [], # table2の依存先 [ 'table2 >> table3' ], # table3の依存先 [ 'table3 >> table4' , # table4の依存先 'table1 >> table4' , # table4の依存先 ], ] 最終的に、データマート集計DAGは以下のようになります(※ on_failure_callback については後述)。 import pendulum from airflow import DAG from airflow.operators.python import PythonOperator from airflow.utils.task_group import TaskGroup def _update_datamart (**kwargs): datamart_id = kwargs[ 'datamart_id' ] ''' データマート更新処理 ''' with DAG( dag_id= 'dailybatch_datamart' , start_date=pendulum.datetime( 2023 , 1 , 1 , 7 , 0 , tz= 'Asia/Tokyo' ), schedule_interval= '0 7 * * *' , catchup= False , ) as dag: datamart_ids = [ 'table1' , 'table2' , 'table3' , 'table4' , 'table5' , ] with TaskGroup(group_id= 'mart' ) as mart: for datamart_id in datamart_ids: globals ()[datamart_id] = PythonOperator( task_id=datamart_id, on_failure_callback=_failure_notify, python_callable=_update_datamart, op_kwargs={ 'datamart_id' : datamart_id}, ) # タスク間の依存関係の設定 for dependenies_of_one_mart in datamart_dependencies: for dependency in dependenies_of_one_mart: eval (dependency) other_task1 >> mart >> other_task2 また、AirflowのWeb UI上から確認できるタスク間の依存関係グラフは以下のようになります。 一次テーブルの更新待ち処理追加 Airflowを使うことで、一次テーブルが更新途中であっても「一次テーブルに依存するマートのみ待機し、依存しないマートについては影響を受けることなく集計を進める」といったことが可能になりました。これにより、一次テーブルの更新遅延がマート集計ジョブ全体の遅延に繋がる問題を解決できます。具体的には、まず一次テーブルの更新を待機する「更新待ちタスク」を定義します。更新待ちタスクは、 existing_table[1, 2] の更新時間チェックを行うBigQueryのクエリを実行し続け、更新されていることが確認できたらタスクを完了させるような内容にしています。そして、一次テーブルを参照するマートの集計タスクが、この更新待ちタスクの後にくるよう依存関係を設定します。 [ [ 'wait_existing_table1 >> table1' ], [ 'wait_existing_table2 >> table2' ], ] 結果として、AirflowのWeb UI上から確認できるタスク間の依存関係グラフはこのようになります。 これにより、一次テーブルの更新遅延による影響を極限まで小さくできました。 Cloud Composer移行によって得られた効果 Composer(Airflow)に移行することで、タスク間の依存関係を柔軟に設定できるようになりました。結果として、あるマートの集計でエラーが発生しても、そのマートと依存関係のないマートは影響を受けずに集計を進められるようになりました。例えば、 table1 の集計がエラー終了した場合、 table1 を参照する table4 のみ集計がストップし、他の table[2, 3, 5] については影響を受けることなく集計が行われます。 さらに、一次テーブルの更新待ちタスクを定義し、一次テーブルを参照するマートの集計タスクとの依存関係を設定しました。結果として、一次テーブルに依存するマートのみ更新を待ち、それ以外のマートは一次テーブルの更新タイミングに影響されることなく集計を進められるようになりました。例えば、 existing_table1 の更新処理が遅延し完了していない場合、 table1 と table4 のみ更新を待ち、他の table[2, 3, 5] については影響を受けることなく集計が行われます。 また、以下の記事で紹介した通り、DigdagではAWSのEC2インスタンス・Aurora DBを組み合わせてマート集計基盤を構築していました。 techblog.zozo.com 対して、ComposerではAirflowの環境クラスタがGKEのAutopilotモードによってマネージドに構築されるため、インフラ管理の運用負荷を下げることができました。 Cloud Composerの運用Tips 最後に、タスクの依存関係とは関係ありませんが、Composerを運用していくにあたって得た知見を記載します。Composerを運用するにあたっての参考になれば幸いです。 Tips1: メタデータの読み込み方法は「読み込まれるタイミング」によって使い分ける Airflowには、DAGの実行中に読み込みたいメタデータを、 key/value の形で、AirflowのメタデータDBに保存しておくことができる Variables という機能があります。DAG内に直接記載したくない機密情報、アクセス情報などを Variables に保存しておき、実行中のタスクから読み込むといったことが可能です。しかし、この Variables は「読み込まれるタイミング」に注意して利用する必要があります。 SchedulerによるDAG解析 Variables の注意点の前に、 Airflowのアーキテクチャ について触れておきます。Airflowのアーキテクチャは、以下のコンポーネントから成り立っています。 ジョブ実行のスケジュールを管理する Scheduler ジョブを実際に実行する Worker Web UIを提供する Web Server ここで重要なのが、 Scheduler がDAGファイル(Pythonコード)の解析を行い、 Worker がタスクを実行するということです。 Scheduler には、ジョブ実行のスケジュール管理以外にも DAGs folder と呼ばれるフォルダ内にあるPythonファイルを読み込み、DAGやタスクの設定・依存関係を解析する役割があります。そして、このDAG解析は頻繁(デフォルトでは1分に1度)に行われます。詳細についてはこちらの公式ドキュメントを参照ください。 airflow.apache.org つまり、Airflow環境で動くコードは以下の2つに大別されます。 Scheduler によって解析されるコード: Top level code Worker によって実行されるタスクのコード: Operator 内のコード 例として、以下のようなDAGについて考えてみます。 def _task1 : ''' task1の処理 ''' def _task2 : ''' task2の処理 ''' with DAG( # ~~~ ) as dag: task1 = PythonOperator( task_id= 'task1' , python_callable=_task1, ) task2 = PythonOperator( task_id= 'task2' , python_callable=_task2, ) task1 >> task2 関数 _task1 , _task2 の外のコードは、 Scheduler によって解析されるため Top level code となります。一方で、関数 _task1 , _task2 内のコードは、 Worker が実行する PythonOperator によってDAG実行時に初めて呼び出される Operator内のコード となります。ここでの大事なポイントは、 Top level code 内でサイズが大きいライブラリのインポートやDB接続といった重たい処理を行うと、DAGの解析時間が著しく遅くなってしまうという点です。 Variablesの読み込みはTop level codeで行わない Variables はAirflowのメタデータDBに保存されています。そのため、 Top level code で Variables を読み込むと、DAG解析の度に Scheduler によるメタデータDBへの接続が作成されます。結果として、DAGの解析が著しく遅くなり、 Scheduler 全体のパフォーマンスが劣化してしまいます。Composerの検証時、DAGを実行しても、タスクがQueueに停滞して実行されないという問題が発生しました。これは、DAG解析時に読み込んでいるPythonモジュール内に、 Variables を読み込む処理を記述していたことが原因と考えられます。DAGが定義されているPythonファイルだけでなく、DAG解析時に呼び出される処理も Top level code となります。そのため、DAG解析時に Variables の読み込み処理が Scheduler によって行われてしまい、パフォーマンスが劣化していました。 Top level code 内でメタデータを読み込みたい場合には2つの方法が考えられます。1つ目が、事前に環境変数として定義しておき、以下のようにDAG解析時に取得する方法です。 ENVIRONMENT = os.environ.get( 'ENVIRONMENT' ) 2つ目が、メタデータをYAMLファイルなどに保存しておき、DAG解析時に動的に読み込むようにする方法です。これらの方法で、DAG解析にかかる時間を抑えられ、 Scheduler のパフォーマンス劣化を防ぐことができます。 メタデータの読み込み方法の使い分け 以上より、メタデータの読み込み方法の方針は以下のようにしました。 Scheduler によって解析される Top level code では、環境変数やYAMLから動的に読み込む Worker によって実行される Operator内のコード では、 Variables から読み込む 具体的には、 config ディレクトリ配下に dag_parse , variables という2つのディレクトリを作成し、それぞれにメタデータが記載されたYAMLファイルを配置しています。※実際は prd , stg , dev など環境ごとに設定が分かれるため、ディレクトリ階層はもう一段深くなりますが、ここでは簡略化して記載します。 config ├── dag_parse │ ├── datamart.yml │ ├── ... └── variables ├── database.yml ├── tables.yml ├── gcp.yml ├── ... Composerでは、環境ごとにGCSバケットを持ち、このバケット内に dags , plugins , data といったフォルダが配置されています。そして、それぞれのフォルダが、各Airflowコンポーネント( data は Worker のみ)のローカル環境と同期されています。 cloud.google.com そこで、 dag_parse 配下のYAMLファイルはGCSバケットの dags フォルダへアップロードし、DAG解析時に読み込みます。一方、 variables ディレクトリ配下のYAMLファイルは、以下の流れでAirflowの Variables として登録していきます。 YAMLをJSONに変換 変換したJSONをGCSバケットの data フォルダにアップロード data フォルダと同期されている Worker 内の data フォルダから Variables を登録 2と3について補足します。Composerでは、以下のコマンドで Variables を登録できます。 gcloud composer environments run ${Composer 環境名 } variables -- import ${ 登録したいJSON } しかし、この ${登録したいJSON} は、Airflowコンポーネントのローカル環境に配置されている必要があります。そこで、GCSバケットの data フォルダにJSONをアップロードし、 Worker 内の data フォルダと同期させてから Variables として登録しています。 Variables 登録箇所のコードはこちらです。 # VARIABLES_YAML_DIR_PATH: config/variablesディレクトリのパス # VARIABLES_JSON_PATH: Variablesに登録するJSON # VARIABLES_YAML_DIR_PATH内のYAMLを1つのJSONに変換 python load_variables_from_yaml.py \ -c ${VARIABLES_YAML_DIR_PATH} \ -o ${VARIABLES_JSON_PATH} # 作成したJSONをGCSのデータフォルダにアップロード gcloud --project ${PROJECT} composer environments storage data import \ --environment ${COMPOSER_ENVIRONMENT} \ --location ${REGION} \ --source= ${VARIABLES_JSON_PATH} # データフォルダに置かれたJSONの内容をAirflow Variablesとして登録 gcloud --project ${PROJECT} composer environments run \ ${COMPOSER_ENVIRONMENT} \ --location ${REGION} \ variables -- import /home/airflow/gcs/data/variables.json load_variables_from_yaml.py の中身はこちらです。単純に YAML→PythonのDict→JSON の順で変換しているだけです。 import yaml import json import glob import argparse def load_environment_yaml_to_variables (yamls): variables_dict = {} for yaml_path in yamls: with open (yaml_path, 'r' ) as rf: loaded_variables = yaml.load(rf, Loader=yaml.SafeLoader)[project] variables_dict = dict (**variables_dict, **loaded_variables) return variables_dict if __name__ == '__main__' : parser = argparse.ArgumentParser(description= 'Argument to load variables' ) parser.add_argument( '-c' , '--conf' , required= True , help = 'Input config dir path' ) parser.add_argument( '-o' , '--output' , required= True , help = 'Output json path' ) args = parser.parse_args() config_path = args.conf output_path = args.output yamls = glob.glob(f '{config_path}/*.yml' ) variables_dict = load_environment_yaml_to_variables(yamls) with open (output_path, 'w' ) as wf: json.dump(variables_dict, wf, indent= 2 ) 上記の全ての処理を、GitHub Actionsから行うことで自動化しています。 Tips2: DAG・タスクのエラーハンドリングは目的に応じてパラメータを使い分ける 各マートの集計タスクがエラー終了した場合、目的に応じて以下2つのエラーハンドリングを行っています。 1つでもタスクがエラー終了したら保守担当者に架電 エラー終了したタスクの分だけSlack通知 1つでもタスクがエラー終了したら保守担当者に架電 マート集計ジョブ自体にエラーが発生した場合、保守担当者へ架電されるようにしています。架電の目的は「問題が起きていることを知らせること」なので、エラー終了したタスクの数によらず架電される回数は1回で十分です。そんな時は、 trigger_rule パラメータを使ったタスクを新たに定義します。デフォルトでは、タスクは上流(upstream)にある全てのタスクが成功しないと実行されません( trigger_rule=all_success )。ただし、この trigger_rule パラメータの値を変えることで、タスクの起動条件を変更できます。今回は trigger_rule に one_failed を指定することで目的が達成できます。 one_failed: At least one upstream task has failed (does not wait for all upstream tasks to be done) # 具体的な架電処理はfailure_on_call.shに記述 failure_on_call = BashOperator( task_id= 'failure_on_call' , trigger_rule= 'one_failed' , bash_command= 'failure_on_call.sh' , env={ 'message' : '[ERROR] {{ task_instance.dag_id }}' }, ) other_task1 >> mart >> other_task2 >> failure_on_call エラー終了したタスクの分だけSlack通知 一方Slack通知に関しては、エラー終了したタスクの分だけ通知が飛ぶようにしました。Slackのメッセージ内容にタスク名、エラー内容、ログへのリンクを載せることで、調査・対応をやりやすくすることが目的です。そんな時は、 on_failure_callback パラメータを使って各マートの集計タスクを定義します。こうすることで、タスクが失敗した際に、 on_failure_callback に指定した関数が呼び出されます。 def _failure_notify (context): ''' Slack通知処理 ''' with DAG( # ~~~ ) as dag: with TaskGroup(group_id= 'mart' ) as mart: for datamart_id in datamart_ids: globals ()[datamart_id] = PythonOperator( task_id=datamart_id, on_failure_callback=_failure_notify, # タスク失敗時に_failure_notifyを呼び出す python_callable=_update_datamart, op_kwargs={ 'datamart_id' : datamart_id}, ) Tips3: Composer環境自体の外形監視を設定する DAGのエラーハンドリングを設定することで、DAGの実行中の発生したエラーを検知できるようになりました。しかし、このままではComposerの環境自体に問題が発生し、そもそもDAGが実行されなくなった場合に検知できません。そこで、GCPのCloud Monitoringを使って、Composer環境自体の外型監視を入れています。Composerには、 airflow_monitoring という、環境が正常に動作しているかを監視するためのDAGが最初から用意されています。 cloud.google.com そのため、この airflow_monitoring が無事動作しているかを監視するAlert PolicyをCloud Monitoringに設定すれば、外型監視が可能となります。外型監視のAlert PolicyはTerraformで作成しており、そのTerraform定義は以下のようにしています。 resource "google_monitoring_alert_policy" "composer_healthy" { display_name = "Cloud Composer Environment Healthy" combiner = "OR" conditions { display_name = "Composer Environment Healthy" condition_threshold { aggregations { alignment_period = "60s" per_series_aligner = "ALIGN_COUNT_TRUE" cross_series_reducer = "REDUCE_SUM" group_by_fields = [ "resource.label.environment_name" , ] } comparison = "COMPARISON_LT" duration = "0s" filter = <<EOT resource.type="cloud_composer_environment" AND ( resource.labels.environment_name=$ { Composer環境名 } ) AND metric.type="composer.googleapis.com/environment/healthy" EOT threshold_value = 1 trigger { count = 1 percent = 0 } } } documentation { # ~~~ } notification_channels = [ # ~~~ ] } このように、他のGCPのマネージドサービスと組み合わせることができるのもComposerのメリットです。 Tips4: 集計遅延の検知の仕組み DAGの実行中のエラー、Composer環境自体のヘルス不良は検知できるようになりました。しかし、まだ「普段3時間で終わるマート集計DAGが6時間経っても終わっていない」といったような、集計遅延は検知できません。そこで、DAGの実行時間のSLAを定め、その時間を超えた場合にアラートを飛ばす仕組みを入れました。具体的には、監視用のDAG( sla_check_dailybatch_datamart )を新たに作成しました。この監視用DAGでは、 ExternalTaskSensor を使って、マート集計DAGを監視しています。 ExternalTaskSensor を利用するタスクでは、以下のパラメータを指定します。 external_dag_id external_task_id allowed_status そして、 external_task_id に指定したタスクの状態が allowed_status の状態へ遷移すると、 ExternalTaskSensor を利用するタスクがSuccessとなります。また、 ExternalTaskSensor は、 BaseSensorOperator というクラスを継承したクラスです。 BaseSensorOperator クラスでは、 timeout を設定でき、タスクの実行時間が指定した時間を過ぎるとエラー終了させることが可能です。マート集計DAGでは、全マートの集計完了後にSlack通知をする success_notify というタスクを定義しています。そこで、 ExternalTaskSensor から、この success_notify タスクを監視し、 timeout パラメータにSLA時間を指定しています。そうすることで、マート集計DAGの実行時間がSLA時間を超えた場合に、 ExternalTaskSensor タスクが失敗するようになります。最後に、この ExternalTaskSensor タスクが失敗した時に起動しアラートを飛ばす sla_violation_alert タスクを定義することで、集計遅延の検知が可能となります。 CHECK_DAG_ID = 'dailybatch_datamart' CHECK_TASK_ID = 'success_notify' # 対象DAGのSLA違反となる実行時間(秒) SLA_VIOLATION_TIMEOUT = 60 * 60 * 3 def _sla_violation_alert (**kwargs): ''' アラート発報処理 ''' with DAG( # ~~~ ) as dag: sla_check_dailybatch_datamart = ExternalTaskSensor( task_id= 'sla_check_dailybatch_datamart' , external_dag_id=CHECK_DAG_ID, external_task_id=CHECK_TASK_ID, timeout=SLA_VIOLATION_TIMEOUT, allowed_states=[ 'success' ], failed_states=[ 'failed' , 'skipped' ], # poke or reschedule: センサーの待機時間が長いのでスロットを解放するrescheduleを選択 mode= "reschedule" , ) sla_violation_alert = PythonOperator( task_id= 'sla_violation_alert' , trigger_rule= 'one_failed' , python_callable=_sla_violation_alert, ) sla_check_dailybatch_datamart >> sla_violation_alert 今回は ExternalTaskSensor を使う方法を採用しましたが、他に検討した集計遅延の検知方法についても記載します。 採用しなかった方法1: タスクに sla パラメータを指定 Airflowでは、各タスクのSLA時間を、 sla パラメータを使って設定できます。 airflow.apache.org 例えば、SLA時間を30秒にしたい場合、以下のように PythonOperator の引数に sla=timedelta(seconds=30) を追加します。 def sla_callback (): ''' SLA違反の際の処理 ''' with DAG( # ~~~ ) as dag: task = PythonOperator( task_id= 'task' , pythonc_collable=_collable, sla=timedelta(seconds= 30 ), sla_miss_callback=sla_callback, ) すると、30秒以上タスクが実行されるとSLA違反となり、 sla_miss_callback に設定している関数 sla_callback が呼び出されます。最後に設定した sla_callback 内からSlack通知なり架電を行うことで、集計遅延を検知できます。しかし、 sla パラメータによるSLA違反のチェックタイミングは、SLA違反したタスクの次のタスクの実行前です。そのため、遅延しているマート集計タスクが実行中の間(完了しない限り)は、遅延を検知できません。遅延しているタスクが実行中であっても、即座に遅延を検知したかったため、この sla パラメータと sla_miss_callback を組み合わせる方法は見送りました。 採用しなかった方法2: DAG・タスクのいずれかにタイムアウト値を設定 Airflowでは、DAG・タスクそれぞれに対して dagrun_timeout ・ execution_timeout といったパラメータを指定することで、実行時間のタイムアウト値を設定できます。 cloud.google.com これらのパラメータを使うと、指定した時間内にDAGまたはタスクが終わらなかった場合、強制的にエラー終了させられます。しかし、今回は集計遅延の検知さえできれば良く、実行中のタスクを強制的にエラー終了させる必要はありませんでした。また、集計遅延の原因はBigQueryジョブに時間がかかっているケースがほとんどです。遅延しているマートの集計クエリの調査・BigQueryジョブのプロファイリングを行った上で、タスクを実行させたままにするか、エラー終了させるかを運用者側で判断したいという要望もありました。そのため、この dagrun_timeout ・ execution_timeout を使う方法も見送りました。 Tips5: プライベートIP環境で構築 公共ネットワークや外部サービスからComposer環境(GKEクラスタ)へインバウンドアクセスする用途はなかったため、よりセキュアな プライベートIP環境 としてComposer環境を構築しました。また、 Cloud NAT を利用することで、外部からはアクセスできないが、外部へはアクセスできるようにしています。構築手順は以下の公式ドキュメントに沿って行っています(詳細は割愛します)。 cloud.google.com まとめ データマートの集計ジョブを制御するワークフローエンジンを、DigdagからCloud Composerに移行した事例について紹介しました。移行により、タスク間の依存関係を柔軟に設定できるようになり、1つのマートの集計エラーがマート集計ジョブ全体に及ぼす影響を小さくできました。ZOZOでは、一緒にデータ基盤を作ってくれる方を大募集しています。ご興味がある方は以下のリンクから是非ご応募ください! hrmos.co
アバター
こんにちは。ZOZO研究所の平川とML・データ部のデータサイエンスブロック2の荒木です。私たち2022年度の新卒入社メンバーは有志で社内マッチングアプリ「CLUB ZOZO」を運営しています。この記事では、興味関心が近い社員同士を自動でマッチングするアルゴリズムについてご紹介します。マッチング時のバッチ処理については推薦基盤ブロックの関口が解説していますので、興味のある方は併せてご覧ください。 qiita.com 目次 目次 CLUB ZOZOとは CLUB ZOZOを運営するにあたり解決すべき課題 ユーザ間の類似度を計るアプローチ 数理最適化を用いた偏りのないマッチング生成 ダミーデータでの推論結果 まとめ 最後に CLUB ZOZOとは CLUB ZOZOは、興味関心が近い社員同士をマッチングし、週に1回15分間のChat Timeをセッティングするサービスです。Chat Timeとは「上司」と「部下」の関係で実施される1on1ではなく、同じ興味関心を持つもの同士で純粋に会話を楽しんで欲しいという願いを込めて作った造語です。ユーザはSlackアプリ上で自分の興味関心を登録し(以下では「趣味タグ」と呼びます)待つだけでChat Timeがセッティングされるため、気軽に新しい仲間との出会いを楽しむことができます。 CLUB ZOZOは、新卒チーム開発研修で社内コミュニケーションを促進するためのツールを開発したことがきっかけとなり誕生したサービスです。2022年11月に社内リリースされ、2023年1月時点で約350名の社員が利用しています。 CLUB ZOZOを運営するにあたり解決すべき課題 CLUB ZOZOを運営する上でボトルネックとなるのがマッチングの生成です。サービスの性質上、以下の要件を満たす必要があるため手動での実施は運営側の負担が大きくなります。 共通の話題がある 同じ人とばかりマッチングしない マッチング機会が特定のユーザに偏らない そこで、私たちは機械学習と数理最適化を組み合わせたマッチングアルゴリズムを開発し、CLUB ZOZOの運営コストを大幅に削減することに成功しました。開発したアルゴリズムは、「word2vecを用いた趣味タグの類似度計算」と「数理最適化を用いた偏りのないマッチング生成」の2つの工程から成ります。 このアプローチは、ユーザのマッチング度合いに関する教師データが不要であるため、サービス立ち上げ段階で教師データが存在しない状況においても適用可能であるという利点があります。 以降では「ユーザ間の類似度を計るアプローチ」と「数理最適化を用いた偏りのないマッチング生成」について詳細を説明します。 ユーザ間の類似度を計るアプローチ 今回は、ユーザ間の類似度を「ユーザが登録しているタグの類似度」として計るアプローチを採用しました。具体的には、各ユーザのタグのすべてのペアに対して単語間の類似度を計算して、それを平均しています。 こちらの図の例では、以下の3名がそれぞれ次のタグを持っているとします。 Aさん:テニス、君の名は。 Bさん:テニス、映画 Cさん:テニス、ママ このとき、AさんとBさんは「テニス」という共通のタグを持っており、かつ「君の名は。」と「映画」という類似の趣味タグを持っているためユーザ間の類似度は55%となりました。一方で、AさんとCさんは「テニス」という共通の趣味タグを持っていますが、もう1つの「君の名は。」と「ママ」というタグはあまり似ていないためユーザ間の類似度は33.75%となります。つまり、AさんとBさんの方がより類似しているという直感をうまく反映できていると言えます。 また、単語間の類似度を計算する手法として「word2vec」を用います。word2vecは単語の意味をベクトルとして表現するモデルであり、自然言語を扱う機械学習モデルで広く用いられています。word2vecの学習には、 Wikipediaが提供している全文データ に対して、日本語用の形態素解析システムである MeCab による分かち書きを行ったデータを利用します。 しかし、このままでは辞書にない語句(=学習データに含まれない語句)や節はベクトル化できません。辞書にない語句や節の登録を禁止することも考えられますが、それではUX的にあまり嬉しくありません。そこで、もし趣味タグとして登録された文字列が辞書にない場合は、形態素解析で抽出した名詞のみを用いてベクトル化を行います。 画像のように、辞書にない文字列のペアに対して類似度の計算を実現しています。例えば、「サッカー」と「スポーツ観戦」はどちらも辞書に存在するため、そのまま類似度を計算できます。一方で、「サッカー観戦」は辞書にないタグなので、一度分かち書きをして「サッカー」と「観戦」に分割します。そして、それぞれ「スポーツ観戦」とのスコアを計算し、その平均値を類似度とします。「サッカー観戦」と「サッカー実況」のようにタグのペアのうちどちらも辞書にない語句の場合は、それぞれ分かち書きを行い、各スコアを求めて平均した結果を類似度とします。 では、もしタグに少し長めの文章が入力された場合はどうなるのでしょうか。例えば「公園で子供とサッカーをする」と「縁側で猫と日光浴をする」というペアを考えてみましょう。分かち書きを行うと、それぞれ「公園・で・子供・と・サッカー・を・する」「縁側・で・猫・と・日光浴・を・する」になります。もし、このまま各単語のベクトルを平均して類似度を計算する場合、「で・と・を」といった助詞が類似度を底上げしてしまい不自然に高いスコアが出てしまいます。実際このペアの類似度は87.5%となります。そこで、今回は形態素解析をして、品詞が名詞である単語のみを用いてベクトルを平均します。名詞だけを用いる場合、今回のペアは59.5%となり少し低めに計算されました。また、「サッカーを観戦する」「サッカー観戦」という一見すると類似度が高めに出るはずのペアも、名詞以外を用いた場合は類似度が45.5%とかなり低めに計算されてしまいます。もちろん、名詞のみを用いた場合は100%となります。 ただし、分かち書きをしても単語が辞書に存在しない場合は、スコアを0%としています。この他にも、類似度が55%未満のタグのペアは経験的にあまりふさわしいペアでないことがわかっていますので、そのペアのスコアを0%にしてペアを無効化するなど細かな調整を入れています。 数理最適化を用いた偏りのないマッチング生成 上記の方法で全ユーザの組に対してユーザの類似度が計算できたとします。ユーザの類似度が高いペアから貪欲にマッチングを成立させた場合(図の「偏り制約なし」)、「マッチング機会が特定のユーザに偏らない」という要件を満たさないマッチング結果になる場合があります。 別の方針として、一度のマッチング機会で各ユーザが1人のユーザとだけマッチングするという制約を満たしつつ、マッチングスコアの総和を最大化する解(図の「偏り制約あり」)を見つけるということが考えられます。 この問題は以下の数理最適化問題として定式化できます。 ここで、 は生成するマッチング数、 はユーザ数、 は 番目のユーザと 番目のユーザの類似度、 は 番目のユーザと 番目のユーザがマッチングするか否かを表すバイナリ変数です(以下ではマッチング変数と呼びます)。この問題は「整数線型計画問題」と呼ばれるクラスの問題であり、PuLPなどの最適化ソルバーを用いて最適解を求めることができます。 この問題の最適化対象は、マッチングが成立した組のユーザ類似度の総和です。この値は全体の効用を表現しており、値が大きいほど良いマッチングを生成できていると考えられます。1つ目の制約条件は、生成するマッチング数が指定の数と一致することを保証するための条件です。例えば、 を入力すると、10組のマッチングが生成されます。2つ目の制約条件は、一度のマッチング機会で各ユーザが1人のユーザのみとマッチングすることを保証するための条件です(以下では重複制約と呼びます)。全ユーザが同時に重複制約を満たす条件は、任意のユーザが重複制約を個別に満たすことと同値です。 以下の図では、4番目のユーザ( / Dさん)に着目して重複制約の直感的なイメージを説明します。 のユーザがマッチングするか否かを決定する際に着目すべき領域は、図中の黄色に着色されたセル(以下では対象領域と呼びます)です。マッチング変数が赤色のセルはマッチング変数の値が1になり、黒色のセルはマッチング変数の値が0になることを表すものとします。この時、4番目のユーザが重複制約を満たすための条件は、対象領域に含まれるマッチング変数の中で値が1になるものが1個(DさんはEさんとだけマッチングする / 図の中央の例)もしくは0個(Dさんは誰ともマッチングしない / 図の右端の例)の場合に限ります。実際、図の左端の例のように対象領域に含まれるマッチング変数の中で値が1になるものが2つ以上存在する場合は、Dさんは2人以上のユーザとマッチングしてしまい不適となります。 さらに、運用上は重複制約に加えて、同じユーザとばかり連続でマッチングしない制約(以下では連続制約と呼びます)を加えることも重要です。連続制約については、上記の最適化問題をソルバーに入力する前段で、対応するユーザ同士のマッチングスコア に最低値である0を代入しておくことで達成できます。同様の原理を用いると、「部署指定マッチング」などの拘束条件付きマッチングについても実現できます。 ダミーデータでの推論結果 開発したアルゴリズムがうまくマッチングできるかを確認するために、ダミーのユーザデータを用いて実際にマッチングを行いました。 左側の表は完全にランダムでマッチングされた結果であり、ほとんどのペアが出鱈目で、良いマッチング結果とは言えません。対して右側の表はword2vecによるユーザの類似度を用いたマッチング結果であり、似ている趣味タグを持っているユーザ同士のマッチングを実現できたことがわかります。各表の中央にある「Score」は、各タグの類似度を示します。 続いて、マッチングの偏りを抑止する制約の有無による結果の比較です。左側の表は先程のword2vecの結果と同じですが、同じユーザIDにそれぞれ色をつけています。ユーザID: 4, 7, 14のユーザがそれぞれ2回マッチングしており、ユーザに偏りができてしまっていることがわかります。対して右側の表はマッチングの偏りを抑止する制約を入れた結果であり、どのペアも必ず違うユーザが選ばれており、特定ユーザに偏らないマッチングを実現できたことがわかります。 まとめ 本記事では、新卒研修で開発した社内マッチングアプリ「CLUB ZOZO」のうち、ユーザ間のマッチングを行うアルゴリズムについてご紹介しました。word2vecと数理最適化の組み合わせで、興味・関心が近いユーザ同士を偏りなくマッチングするアルゴリズムを実現しました。今後は、実際にツールを利用している方から得られたフィードバックを教師データとして活用するほか、部署指定といった新機能の追加を考えています。 最後に ZOZOではファッションに関する様々な分野で、研究や開発を一緒に進めていくデータサイエンティスト・MLエンジニアを募集しています。ご興味を持たれた方は、以下のリンクからぜひご応募ください。 corp.zozo.com zozonext.com
アバター
はじめに こんにちは。ML、データ部データサイエンス2ブロックの吉本です。 ZOZOTOWNの商品には「長袖」「クルーネック」「花柄」といった、アイテムの特徴を示すタグ(アイテム特徴タグ)や「ベーシック」「モード」「結婚式」といった、アイテムに合うシーンやスタイルを表すタグ(シーン・スタイルタグ)が付与されています。これらは商品情報の登録時、ブランドさんに付与していただいているものです。 これらタグに関する課題として、タグ付与の手間、シーン・スタイルタグのタグ付与率の低さがあります。アイテム特徴タグは例えばTシャツ/カットソーカテゴリでは約50種類、シーン・スタイルタグは約130種類のタグがあり、一つ一つの商品に対してこれらの中から該当するものを選んで付与することは手間のかかる作業となります。またシーン・スタイルタグについてはZOZOTOWNに導入されてから2年弱とまだ日が浅いことから、認知度が低くタグが付与される商品の割合が小さくなっています。 そこでZOZOではタグ登録の手間の軽減、タグ付与率の向上を目標に画像認識によるタグ予測に取り組んでいます。2023年1月からは、ブランドさんが商品にタグを登録する際に、予測したタグを推薦するという仕組みとして導入されています。 また現在稼働しているシーン・スタイルタグ推薦のモデルでは、予測精度が高いタグに限って推薦を行っていますが、モデル開発では引き続き対応タグを増やすための精度の向上に取り組んでいます。こちらの取り組みでは、スタイルタグを人手でアノテーションすることによって、学習データを改善し精度の向上を目指しました。 本記事では、はじめに現時点のタグ推薦で用いられているアイテム特徴タグ、シーン・スタイルタグのモデル(商品タグによるモデル)に関して簡単に紹介したのち、スタイルタグの対応タグ数増加のための取り組み(アノテーションデータによるモデル)に関して紹介します。 はじめに ZOZOTOWNの商品タグ 商品タグによるモデル アイテム特徴タグ シーン・スタイルタグ アノテーションデータによるモデル アノテーション 予測スコアを用いた抽出 結果分析 モデル作成 評価 アノテーションデータ上での評価 リリースに向けた分析項目 分析結果 対応タグの増加、タグ推薦対象の増加 さいごに ZOZOTOWNの商品タグ ZOZOTOWNでは、見た目の特徴や素材などに関するアイテム特徴タグや、活用シーンや合うスタイルを表すシーン・スタイルタグが商品に対して付与されています。 アイテム特徴タグは「着丈」「袖丈」「柄、デザイン」といったタググループに分かれていて、その中に例えばシャツ/ブラウスカテゴリなら「着丈」の「ショート丈」「ミドル丈」「ロング丈」といったタグがあります(上図左)。アイテムのカテゴリ(シャツ/ブラウス、ニット/セーターなど)ごとに対象のタググループが決まっていて、ブランドさんはそこから選んでタグを付与できるようになっています。ユーザーさんは各タググループごとにタグを指定することで、求める特徴を持つ商品に絞り込んで検索できます。アイテム特徴タグの付与率は比較的高く、Tシャツ/カットソーの「ネック」だと約84%の商品にタグが付与されています。 シーン・スタイルタグに関しては130個ほどあり、商品ページでは「このアイテムの関連キーワード」の欄に表示されています(上図中央)。商品ページからこれらのタグをクリックすることで、当てはまる商品を検索できるようになっています(上図右)。導入されてからまだ2年弱しか経過していないということもあり、何らかのタグが1つ以上付与されている商品は全体の10%程度であり、タグがついてる商品は未だ多いとは言えない状態です。 商品タグによるモデル はじめに現在タグ推薦で利用されている、商品タグを用いて作成したモデルに関して簡単に紹介します。 アイテム特徴タグ アイテム特徴タグは多くのタグで学習に十分なデータ量がありました。このためZOZOTOWNの商品画像と、商品に付与されたアイテム特徴タグを学習データとして用いました。 モデルとしてはベースネットワークの上に、タググループごとにタグを1つ予測する層を載せたモデルを学習しました。損失関数にはSoftmax Cross Entropy Lossを用いました。ベースネットワークとしては、速度と精度ともに良い数値が報告されていた EfficientNetV2-S を用いました。 プロダクト導入に際しては社内の他部署に評価を依頼しました。各カテゴリごとに約100画像を用意し、各タググループに関して予想したタグが正しいかどうかを評価しました。この評価において、正解率が事前に定めた閾値を上回ったカテゴリ×タググループを導入することに決定しました。 シーン・スタイルタグ シーン・スタイルタグは前述の通りタグ付与率が低い状態でした。そのため商品に付与されたシーン・スタイルタグだけでは十分なデータ量を確保できないタグが多くありました。データ量の確保のため、商品説明にシーン・スタイルタグと一致する文字列が含まれる場合、そのタグが付与されているとみなして、商品に付与されたシーン・スタイルタグに加えて用いました(以後これも合わせて商品タグと呼びます)。 モデルとしてはベースネットワークの上に、各タグを付与するべきかどうかを予測する層を載せたモデルを学習しました。損失関数には負例を多く含むデータセットで高い精度が報告されている Asymmetric Loss を用いました。ベースネットワークとしてはアイテム特徴と同じくEfficientNetV2-Sを用いました。 画像認識を用いるともに、人手によりカテゴリごとに対象タグを洗い出しました。これにより精度を細かく評価できるようになるとともに、評価作業や後ほどお話するアノテーション作業の手間を軽減できました。 アイテム特徴と同じく、プロダクト導入に際しては社内の他部署に評価を依頼しました。訓練画像が十分に存在するカテゴリ×タグを評価対象とし、対象画像は各カテゴリ×タグ中で予測スコアが上位5%に入るものから50枚ずつ抽出しました。アイテム特徴と異なり、シーン・スタイルタグはそのシーンやスタイルに当てはまるか、当てはまらないかではっきりと分けることができないため、タグとしてどのくらい妥当かを4段階で評価しました。 評価の結果、評価対象としたカテゴリ×タグの約91%に当たる約300個に関して、社内で定めた閾値を上回ったため導入を決定しました。ただ評価対象としなかったものを中心に、導入を見送ったカテゴリ×タグも多く残りました。 アノテーションデータによるモデル 商品タグによるシーン・スタイルタグのモデルでは、上述のとおり精度が足りないことから導入を見送ったタグが多くありました。これらの多くは、付与数が少ないタグや商品説明文の中であまり使われない用語に関するタグでした。例えばTシャツ/カットソーのスタイルタグでは「マリン」「ロック」「セクシー」「カレッジ」「ギャル」「リゾート」の6種類のタグが該当しました。 また商品タグによるデータセットの問題点として、タグが付与されるべき商品にタグが付与されているとは限らないという問題がありました。これは精度への悪影響に加え、このデータセットを用いて信頼のできる定量評価ができない、本来タグが付与されるべき商品の割合を知ることができないといった問題につながっていました。 そこで次のステップとして、人手によるスタイルタグのアノテーションを行ったデータセットを作成することで、上記の問題を解決し対応タグ数の増加を目指しました。 アノテーション アノテーションは過去に実績のある FastLabel 社に依頼しました。65のカテゴリに対して、前述の人手で作成したカテゴリごとの対応タグリストを用いて、アノテーション対象のタグを設定しました。より主観的な要素が強いシーンタグは、アノテーション作業の効率や導入時の効果の観点から今回の対象から外し、スタイルタグのみを対象としました。 また、今回1つの画像に対して3人のアノテータにタグを付与していただきました。これは人によって各スタイルのイメージが異なり、社内で行った少量のアノテーションにおいても、人によってタグ付与の基準がばらつくという結果が出ていたためです。 アノテーション対象画像としては、約1年間分のZOZOTOWNの商品から約35万画像を抽出しました。この際に多くの商品で、1つの商品からは1カラーバリエーションの画像のみを選定するようにしました。アノテーション後、同じ商品の異なるカラーバリエーションの画像にも自動的に同じタグを付与することで、約70万の画像からなるデータセットとなりました。 予測スコアを用いた抽出 このアノテーション対象商品の選定方法ですが、今回の主目的が対応タグ数の増加にあるため、新たに対応を目指すタグが付与されそうな商品を重点的に抽出することを考えました。このために、新たに対応したいタグを対象に上述のモデルを用いて予測を行い、カテゴリ×タグごとに予測スコアが上位20%に入る商品からランダムに商品を抽出しました。トレーニングデータにおいて、この予測スコアを用いて集めた商品と、全商品からランダムに収集した画像をあわせて用いました。具体的なデータ数の目安としてはランダムに2500個(カテゴリごとにばらつきはありますが)、予測スコアを用いた分に関してはカテゴリ×タグごとに500個と設定しました。バリデーションデータ、テストデータに関しては、ZOZOTOWN上でのタグの分布と同じ分布のデータで評価するために、全量を全商品からランダムに集めたものとしました。4カテゴリに関してテスト発注、分析を行い効果が期待できそうであったため、残りの分もこの方法で収集しアノテーションを依頼しました。 結果分析 まずアノテーションデータと商品タグデータでタグ付与率を比較しました。この結果アノテーションデータでのタグ付与率の方が、商品タグデータでの付与率より4.14倍大きいことがわかりました。 3人のアノテータ間のタグの一貫性に関しても集計を行いました。カテゴリ×タグ内でタグが1人、2人、3人に付与された画像の割合を横軸に、各範囲に該当するカテゴリ×タグ数を縦軸にプロットしたヒストグラムを見ると以下のようになっています。 予測スコアを用いた収集方法の効果検証として、予測スコアを用いて収集された画像とランダムに収集された画像のそれぞれで、別々にタグ付与率を算出し比較しました。この結果、予測スコアを用いたカテゴリ×タグの30/31個において、予測スコア収集分の付与率の方が高いことがわかりました。また平均では1.7倍高いという結果になりました。予測スコア収集の対象外のタグに関しては、予測スコア収集分ではタグ付与率が下がる傾向があり、最も下がっていたTシャツ/カットソーの「エレガント」「フェミニン」「キレイめ」「ガーリー」「ナチュラル」タグでは0.65-0.77倍となっていました。 モデル作成 アノテーションデータを用いた検証をする前に、商品タグを用いたデータセットでベースネットの精度検証を行いました。前述のEfficientNetV2-Sに加えて、CoAtNet-0、CoAtNet-2、VOLO-D2を試しました。新しく試したネットワークについては keras_cv_attention_models を用いて検証しました。報告されているImageNet上での精度と同様、順当にパラメータ数が多いものほど高い精度が出るという結果になり、最も精度が高かったVOLO-D2を採用しました。 データセットはアノテーション対象データの抽出元となった約1年分の商品から作成しました。ラベルとしてはアノテーションと商品タグを別に使えるように用意しました。モデルの方でもベースネットワークの上にアノテーションタグ、商品タグそれぞれに対して別に層を用意して、別に予測、学習を行うようにしました。プロダクトで用いる際には、アノテーションで十分なラベル量があるタグはアノテーションタグの方の予測(以下アノテーションタグ予測と呼びます)を用いることにし、そうでないタグ(主にアノテーションの対象としなかったシーンタグ)に関しては商品タグの方の予測(商品タグ予測と呼びます)を用いることにしました。 損失関数には前回と同様にAssymmetric Lossを用いました。またタグを付与したアノテータ数の情報を活かすため、付与した人数×0.5で正例を重み付けしました。 評価 アノテーションデータ上での評価 評価指標としては、Precision(モデルがそのタグと予測したもののうち、正解データでアノテータ3人中2人以上がそのタグをつけた割合)、Recall(3人中2人以上がそのタグをつけたもののうち、予測できた割合)をカテゴリ×タグごとに算出し用いました。 タグを付与する/しないを決定する閾値には、タグ付与率(そのカテゴリにおいて3人中2人以上がそのタグを付与した割合)に応じた値を設定しました。 結果、Precisionは対象カテゴリ×タグの中央値では0.31、Recallは0.42でした。またPrecision算出時の正解データの条件を変更し「1人以上がそのタグを付けた割合」とすると0.72でした。 リリースに向けた分析項目 リリースに向けて以下の項目を確認しました。 タグ付与率とPrecisionの関係 アノテーションデータ上での評価と社内評価の相関 アノテーションタグ予測と商品タグ予測の相関 1はタグによる絞り込み検索の効果の参考として用いました。仮に検索対象の全商品に予測したタグを付与し、タグを指定して絞り込み検索をしたとすると、Precisionは絞り込み結果に正しくそのタグが該当する商品が含まれる割合を表します。絞り込まない場合にそのタグが含まれる割合はタグ付与率となります。タグ付与率とPrecisionを比較することで、絞り込み検索をした際にどの程度そのタグに該当する商品が増えるかを知ることができます。 2に関しては、一部のカテゴリ×タグについて商品タグを用いたモデルのときと同じく他部署の人に4段階で評価していただき、アノテーションデータでの評価が社内での評価と乖離していないかを確認しました。 3では、これまで用いてきた商品タグ予測とアノテーションタグ予測の間に大きな乖離がないかを見るため、ランキング指標を用いて確認しました。 分析結果 以下の左図が1のタグ付与率とPrecisionの関係を、カテゴリ×タグを1サンプルとしてプロットしたものになります。タグ付与率に比べてPrecisionが中央値で2.3倍となり、さらに付与率0.15以下のものに限ると4.6倍という結果になりました。絞り込みにより対象商品が数倍増えるという結果を得ることができました。 絞り込みにより対象タグの割合が増えても、タグと全く関係のない商品が検索結果の大半を占めると使われづらいことが予想されます。そこで正解データの条件を緩め、3人中1人以上がタグをつけたものとした場合のPrecisionも見てみました(下図右)。タグ付与率が0.01から0.05のものに限ると0.5、0.01以下だと0.26となりました。タグ付与率が低いカテゴリ×タグにおいても、絞り込み結果中のタグに関連する商品の割合を高くできそうなことがわかりました。 2のアノテーションデータ上での評価と社内評価の相関に関しては下図右になります。アノテーションデータでのPrecisionが高いほど、定量評価の点数が高いという傾向が見て取れます。 3の確認には、商品タグ予測とアノテーションタグ予測の間のケンドールの順位相関係数を用いました。全カテゴリ×タグのうち約97%で正の相関があることが確認できました。 対応タグの増加、タグ推薦対象の増加 これらの結果と人手でのタグリストのチェックの結果、217個のカテゴリ×タグについて、アノテーションタグ予測を用いて追加で対応することが決定しました。また現在稼働中のタグ推薦において既に対応済みのカテゴリ×タグのうち、スタイルタグの大部分になる164個に関してアノテーションタグ予測を用いることになりました。 このモデルでの改善点として、対応タグの増加の他に、推薦対象商品の増加があります。前節の商品タグを用いたデータセットでは、正確なタグ付与率を計算できませんでした。そのため社内で行った定性評価の結果などを元に、各カテゴリ×タグの中で上位10%に当たるスコアを持つ商品に対して、タグを推薦することにしていました。この方法だと、大部分の商品にそのタグが当てはまるようなカテゴリ×タグにおいては、そのタグが推薦される商品の割合が理想より小さくなってしまうといった問題がありました。今回のモデルでは、アノテーションされたタグの割合に応じて推薦のための閾値を定めたため、上記の問題を解決しタグ推薦対象の商品を増やすことができました。 さいごに 本記事では画像認識を用いた商品タグ予測に関する取り組み、特にアノテーションデータを用いた精度改善について紹介しました。今後も検索体験の向上や商品登録の手間の軽減に向けて、機械学習、システム面ともに改善を進めていきます。 ZOZOではMLエンジニアを募集しています。以下のリンクからご応募ください。 hrmos.co
アバター
こんにちは、ARやVRといったXR領域やNFTなどのWeb3領域を推進している創造開発ブロックの @ikkou です。 ZOZOCOSMEのARメイク などを担当しています。 2023年1月5日から8日の4日間にかけてラスベガスで開催された「CES 2023」に参加してきたので現地の様子をお伝えします。2020年以来、3年ぶりの現地参加となりました。 techblog.zozo.com CESとは Tech East, LVCC, Central Hall CESはCTA(Consumer Technology Association)が主催する、毎年1月にラスベガスで開催される世界最大級と言える「テクノロジーのショーケース」です。読み方は「せす」と呼ぶ方もいますが、正しくは「しーいーえす」です。 https://www.ces.tech/ 一昨年の「CES 2021」は新型コロナウイルス感染症の影響でCES史上初の完全オンライン開催となりました。そして昨年の「CES 2022」はオンラインとオフラインのハイブリッド開催ではあったものの、会期直前にオミクロン株がまん延した影響で出展や来場を見合わせる動きが目立ち、出展社数も来場者数も大幅に激減しました。さらに通常4日間の会期は1日短縮されていました。そして今年の「CES 2023」はハイブリッド開催を維持したまま、参加者数は「CES 2020」相当の活気を取り戻し、会期も4日間に戻りました。しかし、出展社数は「CES 2020」の5割程度と、まだ完全復活には時間がかかるかもしれません。 展示会場について LVCC Venetian ExpoのCES看板 CESの展示はTech East・Tech West・Tech Southという3つのエリアに大別されます。 特にLAS VEGAS CONVENTION CENTER(LVCC)から成るTech EastとVenetian Expoを中心とするTech Westにブースが多く集まっています。さらにTech Eastの中心となるLVCCはWest Hall・North Hall・Central Hall・South Hallに大別されます。 LVCC West会場の様子 「CES 2020」参加当時はまだWest Hallが建設中でしたが、「CES 2022」からWest Hallが追加されています。また、「CES 2023」ではSouth Hallが改修工事中だったため、LVCCに関してはWest Hall・North Hall・Central Hallの3エリアをまわる形となっていました。 https://www.ces.tech/exhibits/official-show-locations.aspx 今年は諸事情により会期4日目の早朝には現地を発つ必要があったため、3日間を効率よく使うためにTech EastのLVCC、Tech WestのVenetian ExpoとWynn Las Vegasに絞って朝から夜まで歩き回りました。 LVCC LoopによるLVCC各会場間の移動 LVCC West HallとLVCC West Hall Station West Hall・North Hall・Central Hallの3ホールをまわることになるLVCCですが、各会場間は徒歩でも移動できるものの、実際に歩くことなると時間がかかります。「CES 2020」当時はWest Hallが存在していなかったとはいえ、North Hall・Central Hall・South Hallを移動するだけで体力を消費していました。しかし「CES 2022」からVegas LoopのLVCCルートが開通したことで状況は大きく変わりました。 Vegas Loopはイーロン・マスク氏率いるThe Boring Company社が運営するLVCCの会場間を繋ぐ地下トンネルです。 https://www.lvcva.com/loop/ LVCC Central Station 「CES 2023」ではコンラッド系列のリゾートワールドからLVCC West Hallの北側を繋ぐ路線と、LVCC West Hallの南側、Central Hall、South Hallを繋ぐ路線の2路線が運用されていました。前者のリゾートワールドとLVCC West Hallを繋ぐ路線は1日$4.50の有料でしたが、LVCC間を繋ぐ路線は無料でした。 Vegas Loop内を走行している様子 何度かLVCC West HallとCentral Hallの移動に使いましたが、歩くと15分程度かかる道のりがわずか2分足らずで到着するので、とても便利でした。ちなみに、テスラと言えば「オートパイロット」のイメージがあるかもしれませんが、LVCC Loopでは人間のドライバーがハンドルを持っていました。ハンドルを「握る」というよりは「持っていた」ようだったので、Level 2相当のADASかもしれません。 このVegas Loopは将来的にはラスベガスの主要エリアを結ぶ交通システムとして開発されることが予定されています。同じラスベガスで毎年開催されている「AWS re:Invent」の参加者が利用する未来もあるかもしれないですね。 AR Smart GlassesとVR HMDの状況 Tech East, LVCC, Central Hall 入場待ちの様子 LVCC Central Hallには「Gaming | Metaverse | XR」とカテゴライズされた一画があります。このカテゴリー名は業界の世相を表していて、「CES 2020」当時は「AR/VR & Gaming」でした。「CES 2023」では、昨今の「メタバース」ムーブメントもあり「Metaverse」が追加され、「AR/VR」はその総称である「XR」に置き換わっていました。 私自身がCES 2023で体験したAR Smart GlassesとVR HMDの一部 「CES 2020」当時もたくさんのXRデバイスを試してきましたが、もちろん「CES 2023」でもXRデバイスを第一の目的として各社のブースをまわってきました。 「CES 2020」当時と比較するとAR Smart Glassesの勢いは少し落ち着いたように感じました。一口にAR Smart Glassesと言っても、レンズ越しに様々な情報を見せるものから、HDMIケーブルでスマートフォンやコンソールデバイスと接続して大画面で見せるもののふたつに大別できます。「CES 2022」では、後者の大画面で映像を見ることを目的としたデバイスが目立っていたように感じました。 VR HMDについては「CES 2023」で実物が初お披露目となった「Lynx R-1」や、「CES 2023」で発表された「VIVE XR Elite」などが特に注目を集めて賑わっていました。昨今のVR HMDは視界を100%バーチャルなものに置き換える、これまでのVRデバイスから、デバイス前面に搭載されているカメラで「現実世界をビデオパルスルーで見せる機能」が主流になりつつあります。これはMixed Realityの考え方です。 Lynx R-1の光学系を担当しているエンジニアの方と 「Lynx R-1」については先行してクラウドファンディングでプレッジしていることもあり、光学系を担当しているエンジニアの方と性能面などについてやり取りしました。 シャープの「VR GLASS」のプロトタイプ 日本からは「CES 2020」でパナソニックが「眼鏡型VRグラス」として発表し話題になったものが、パナソニックの100%子会社となったShiftallより「MeganeX(メガーヌエックス)」の量産機として展示されていました。また、シャープからはスマートフォンに繋ぐタイプの「VR GLASS」のプロトタイプが展示されていました。 CES 2023で見かけた触覚系デバイスの一部 VRと言うと「ゴーグルのような被るもの」を想像する方も多いかと思いますが、VRは五感に対して作用するものなので、例えば「触覚」を提示するものもVRの一部です。「CES 2023」ではこの触覚提示デバイスに相当するものが「CES 2020」当時よりも多く感じました。特に先日クラウドファンディングを終えたDiver-Xの「ContactGlove」は要注目です。 衣類の購入に際して、店頭での購入とオンラインECでの購入の違いのひとつとして生地に触れられる、という観点があります。将来的にこういった「触覚」を提示するデバイスが一般化、普及することで、メタバース空間における買い物体験が変わるかもしれません。 本記事では取り上げていないXR関連デバイスを含め、先日オンライン開催した「CES2023 オンライン報告会 ~XR関係者による本音トーク!~」のアーカイブが残っています。興味のある方はぜひご覧ください。「CES 2023」現地での交流もあり、Lynx R-1 CEOの方がフランスよりライブで繋いで日本からの質問に答えているのは貴重かもしれません。 Fashion TechとBeauty Tech XR関連以外にも面白そうなもの、業務に活かせそうなものは見てまわりましたが、その中でも特にFashion TechとBeauty Techは注視するようにしていました。これらのブースの多くはTech West, Venetian Expo 2FのLIFE STYLEエリアに出展していました。「CES 2023」では、ZOZOCOSMEのARメイクでも利用している「YouCam メイク」のパーフェクトが出展していなかった他、「CES 2020」と比べミラー系のデバイスが減ったようでした。 インスタントタトゥーマシンを提供するPrinker Fashion Tech Newsの記事でも取り上げている 「CES 2020」で人気を博していたPrinkerは「CES 2023」でも同じように大盛況でした。 Prinkerとジャグアタトゥーの比較 左の写真がPrinkerのブースで試しに印刷してもらったもので、右の写真が比較対象として現地で描いてもらったジャグアタトゥーです。どちらもインスタントタトゥーですが、Prinkerはプリントしてもらった黒以外にカラーも可能です。また、デバイスの大きさに依存するためサイズはやや小さく、耐久性は2-3日程度です。対するジャグアタトゥーは青寄りの色で、手書きのため大きさに制限はなく、ベストな発色は1週間程度です。類似のデバイスはまだそう多くありませんが、こういった可逆性のあるファッションの楽しみ方が今後どうなっていくのか楽しみです。 Koséのブース 日本からはKoséが「Maison KOSÉ銀座」で体験できる、デジタルパーソナルカラー体験「COLOR MACHINE」と関連する技術を出展していました。KoséのコスメブランドのひとつであるESPRIQUEがZOZOCOSMEのARメイクに対応しているという繋がりもあり、「COLOR MACHINE」の存在は認識していました。しかし、なかなか足を運ぶ機会がなく、奇しくも異国の地で体験することになりました。「Maison KOSÉ銀座」では体験後にパーソナルアドバイスシートを頂けるようなので、興味のある方は足を運んでみてください。 Fashion Show Las Vegas ファッション文脈ではWynn、Venetian Expoの近くにあるラスベガス最大級のショッピングモール「 Fashion Show Las Vegas 」も覗いてきました。ここでは週末にライブやファッションショーが催されるのですが、タイミングがあわず、それらは見れませんでした。 おわりに 前回同様、今回のCESは開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 世界情勢に伴うフライト価格の高騰や円安の影響を受け、海外カンファレンスへの参加コストは確実に上昇しています。実際、今回も2020年参加時と同じ航空会社・宿泊施設を利用しましたが、およそ1.5倍の金銭的コストが発生しました。CESに限らず、海外カンファレンスの参加におけるこうした金銭的コストの妥当性を説明するのは難しい側面もありますが、ことXR領域に関しては文字通り「百聞は一体験に如かず」です。現地に足を運び、その目その手で体験することに価値があると考えています。 CESの特性上、本来であれば現地でのビジネスミーティングの実施や、会食などを通してより深いやり取りをするべきですが、今回に関しては新型コロナウイルス オミクロン株の派生型「XBB.1.5」がアメリカで急速に広がっている状況を鑑みて泣く泣くそういったコミュニケーションを見送りました。そういった事情もあり、例年よりも直接的に得られているものが少ない分、より意識的に業務に活かしていきたいと考えています。 最後までご覧いただきありがとうございました。ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 現場からは以上です!
アバター
こんにちは。検索基盤部の山﨑です。検索基盤部では、検索基盤の速度改善やシステム改善だけではなく検索の精度改善にも力を入れて取り組んでいます。 検索システム改善についての過去の取り組み事例は、 こちら のリンクをご参照ください。 techblog.zozo.com また、ZOZOTOWNの検索ではElasticsearchを活用しています。Elasticsearchに関する取り組み事例は こちら のリンクをご参照ください。 techblog.zozo.com 本記事では、ZOZOTOWNで近年実施した検索の精度改善の取り組み事例を紹介します。 目次 目次 はじめに ZOZOTOWN検索の処理フロー ZOZOTOWN検索改善の方針について 商品のリランキングロジックについて 商品のリランキングロジックの概要 特徴量ロギングの導入について 今後のZOZOTOWN検索の展望 おわりに はじめに ZOZOTOWN検索の処理フロー ZOZOTOWN検索では、ユーザーが検索クエリを入力してから検索結果を出力するまで大きく3つのステップに分けられます。 Step 1: ユーザーから入力された検索クエリを受け取る。 検索クエリの支援のために検索クエリサジェスト機能があります。 サジェスト改善の取り組みについては、 ユーザーログを活用したZOZOTOWNの検索サジェスト改善 をご覧ください。 Step 2: 検索クエリの意図を解釈した結果を基にElasticsearchのクエリを作成する。 クエリ分割をQuery Segmentation、クエリの属性の引当をEntity Recognitionと呼ぶことがあります。 Step 3: 作成されたElasticsearchのクエリを基に、検索結果に表示される商品の絞り込みと並べ替えを行う。 本記事では特に以下の内容について紹介します。 全ステップに共通するZOZOTOWN検索改善の方針について Step 3の仕組みの概要と特徴量ロギングの導入について Step 2の現状と今後の展望について 次節では、全ステップに共通する改善方針について説明します。 ZOZOTOWN検索改善の方針について プロダクトを成長させるためには、良い評価指標の設計と施策の試行回数が必要です。全ての施策はA/Bテストで検証するため、各ステップでA/Bテストが容易に実施可能な状態となるようにシステム改修を進めました。 各ステップでA/Bテストが実施できる状態になった後は、評価指標の設計を進めました。評価指標の設計はZOZOTOWN全体のKGI/KPIまで関わるため、非常に難しい内容です。 私たちは、まず「良いZOZOTOWN検索とはなにか?」を議論してKPIツリーを作成しました。このKPIツリーをベースに、ガードレール指標を定めました。ガードレール指標とは、ビジネス上毀損したくない指標で、例えばZOZOTOWN全体の売上などが該当します。ガードレール指標をA/Bテストの度に計測することで、思わぬ事故を未然に防ぐ効果があります。こちらは、 A/B Testing at Scale Tutorial などで紹介されています。 ガードレール指標を決めた後は各ステップでのA/Bテストの評価指標(Overall Evaluation Criterionを略してOEC指標とも呼ばれます)の設計を進めました。 評価指標の設計に際して、 [2019 KDD-tutorial] Challenges, Best Practices and Pitfalls in Evaluating Results of Online Controlled Experiments で紹介されている下記5つの特性を参考にしています。 Sensitivity: 変更に対してどれだけ指標が変化するか Trustworthiness: 得られた指標がどれだけ信頼できるか Efficiency: 指標の計測がどれだけ効率的か Debuggability and Actionability: 指標の変化を説明できる状態か Interpretability and Directionality: 指標を改善したとき、最終的な目標値が改善するか A/Bテストの基盤と運用を整備すると、1回当たりのA/Bテストの実行コストが小さくなるため、小さな改善でも気軽にA/Bテストを実施できるようになります。また、Sensitivityの高い指標を定義できていれば、1回当たりのA/Bテスト期間も短くできます。そのため、Sensitivityが高い指標を見つけることは試行回数を増やす上でも大切です。 小さな改修を重ねてA/Bテストを繰り返すと、Sensitivityの高い指標については有意な差が計測され、機能に変化が出ていることが確認できました。 一方で、Sensitivityの低いKPIについては一度のA/Bテストで有意な差が計測されることが少ないため、小さな改善の積み重ねがKPIに正しく影響を与えているのか疑問視されていました。 この疑問に対応するため、ZOZOTOWN検索では、改善を積み重ねた現行ロジックと1年前のロジックをA/Bテストで比較するネガティブテストを行いました。結果として、現行のロジックと1年前のロジックではKPIに大きな差が観測され、小さな改善の積み重ねがKPIの改善に有効であったことが分かりました。 以上をまとめると、各ステップにおいて下記のサイクルを回しながら改善を進めています。 A/Bテストが可能な状態にシステムを改修する。 A/Bテスト後のリリース判断に用いるガードレール指標と評価指標を設計する。 A/Bテストを何度か実施してリリースを進める。 複数の改修をリリースした後に、1年前のロジックと比較してKPIの改善を確認する。 KPIと評価指標の見直しを行う。 次章では、近年特に改善が進んだStep 3の商品の並べ替え(以下リランキングと呼ぶ)について紹介します。 商品のリランキングロジックについて ZOZOTOWNのおすすめ順検索では、機械学習モデルを活用することでユーザー1人当たりの購入率を大幅に改善しました。 商品のリランキングロジックの概要 ZOZOTOWN検索では、検索したユーザーの情報と検索クエリを入力として、パーソナライズされた商品一覧の結果を返します。毎回全ての商品を機械学習モデルでランキングすると、計算コストが膨大になります。そこで、商品のランキングロジックを2つのフェーズに分けることで、計算コストを軽量化しました。 最初のフェーズでは、再現率を高めることを目的にルールベースのロジックや軽量な機械学習モデルを用いて、商品のフィルタリングを行います。 Elasticsearchでは、入力されたクエリと商品indexの各フィールドとのマッチスコアの重みを設定できます。下記のクエリ例の "brand_name^0.1" の 0.1 部分です。この重み付けを線形モデルで学習する手法を採用しています。 { " query ": { " bool ": { " must ": [ " multi_match ": { " query ": " クエリ1 ", " fields ": [ " brand_name^0.1 ", " shop_name^0.2 ", ... ] } , " multi_match ": { " query ": " クエリ2 ", " fields ": [ " brand_name^0.1 ", " shop_name^0.2 ", ... ] } ] } } , " score_mode ": " sum ", " boost_mode ": " sum " } 次のフェーズでは、適合率を高めることを目的に商品間の順序関係を学習する「ランキング学習」と呼ばれる手法の機械学習モデルを用いて、商品を並べ替えます。 フィルタリングされた全商品に対してランキング学習を走らせるのではなく、フィルタリング時のElasticsearchのスコア結果トップN件に絞ってリランキング処理を行います。リランキング処理でのランキング学習では、ElasticsearchのLearning to Rankプラグインを使用しています。詳しくは Elasticsearch Learning to Rankプラグインの使い方とポイント をご覧ください。 techblog.zozo.com また、機械学習モデルの開発にはVertex AI Pipelinesを利用しています。詳しくは Vertex AI Pipelinesによる機械学習ワークフローの自動化 をご覧ください。 techblog.zozo.com 機械学習モデルの導入前後でネガティブテストを実施することで、ユーザー当たりの商品購入率が大幅に改善することを確認しました。 次節では、機械学習モデル改善の取り組みの中でも評価指標が大きく改善した、特徴量ロギングの導入について説明します。 特徴量ロギングの導入について ZOZOTOWN検索の処理フローの概要は下記のとおりです。 Elasticsearchにリクエストした時点の特徴量をロギングしておくことは、機械学習モデルを構築する上での訓練データの作成の観点で重要です。 機械学習モデル構築の最初期は、ユーザーの行動ログ(ユーザーの商品インプレッションとクリックのログ)と訓練時点でElasticsearchに格納されている商品情報を紐付けることで訓練データを作成していました。 しかし、ZOZOTOWNの商品情報は高頻度で更新されているため、この訓練データの作成方法だと行動ログが記録された時点の商品情報と現在の商品情報が一致しないという課題がありました。そこで、ユーザーが検索するタイミングでの商品情報に基づいて計算された特徴量をログとして記録し、モデル学習の際に活用するように仕組みを整えました。結果として、上記の課題は解決され、さらに評価指標も改善しました。 なお、ZOZOTOWN検索では、特徴量ログを出力するためにLearning to rankプラグインを活用しています。しかし、特徴量ログを落とすとスコアの計算時とログの出力時に二重で特徴量の計算が走ってしまい、CPUに負荷がかかり検索のパフォーマンスが劣化してしまう課題がありました。私たちはプラグインに特徴量キャッシュ機能を実装することで、この課題に取り組みました。こちらの取り組みの詳細は、以下の記事をご覧ください。 techblog.zozo.com 今後のZOZOTOWN検索の展望 現行のZOZOTOWNの検索では、Step 2のクエリの意図解釈は辞書の引当てで実現しています。クエリの意図解釈に関しては、下記に記すような現行のZOZOTOWN検索には実装されていない機能が既にいくつも提案されています。 スペル修正 検索クエリのスペル等を修正することで、ユーザーが意図した結果を返す 例: Tシャル で検索 -> Tシャツ で検索 クエリ拡張 ユーザーが入力した検索クエリに関連するクエリを追加して検索することで、ユーザーの検索クエリに類似した結果も併せて返す 例: Tシャツ で検索 -> Tシャツ OR シャツ で検索 クエリ緩和 ユーザーが入力した検索クエリから一部のキーワードを削除して検索することで、より広範な結果を返す 例: Tシャツ 白 で検索 -> Tシャツ で検索 クエリ解釈については、下記の外部記事などで詳しく解説されています。 Daniel Tunkelang: Query Understanding 検索体験を向上する Query Understanding とは 私たちは、上記に紹介した機能を追加した下記のような処理フローを実現することで、一層ユーザーの意図に沿った検索結果を返すことを目指しています。 おわりに ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。今回紹介した検索技術に興味ある方はもちろん、幅広い分野で一緒に研究や開発を進めていけるメンバーも募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co hrmos.co
アバター
はじめに こんにちは、ブランドソリューション開発本部 フロントエンド部 WEAR Androidブロックの武永です。普段はファッションコーディネートアプリWEARのAndroidアプリを開発しています。 リリースノートを手動で毎回入力するのが面倒 WEARは多言語対応をしています。Google Play Consoleへアップロード後、Google Sheetsからテキストを4言語分コピーしたのち、申請画面でテキストを貼り付ける作業が面倒でした。誤って違う言語のリリース文言を記述してしまうリスクもあったので、検討した結果リリースノートもアプリと同じタイミングでアップロードすることにしました。 導入方法 今回はGitHub ActionsでRubyをセットアップし、Rubyのファイルを実行してリリースノートのテキストを取得します。それをテキストファイルとしてダウンロードします。使用するライブラリは2つあります。 1つ目はGoogle Sheetsからテキストを取得するRubyライブラリの「 google-drive-ruby 」です。Google Sheetsを操作できるライブラリはいくつかあったのですが、スター数もそれなりにあり信頼できると思い選定しました。 2つ目はGoogle Play Consoleにアプリのパッケージをアップロードするライブラリの「 gradle-play-publisher 」です。非公式ではありますが、できることの柔軟さが魅力的で選定に至りました。それでは実際に見ていきましょう。 Rubyファイルのセットアップ プロジェクト直下にGoogle Sheetsからテキストを取得する下記のGemfileとRubyファイルを追加します。 Gemfile source "https://rubygems.org" gem 'google_drive' download_release_note_text.rb require ' google_drive ' def fetch_google_sheets service_acount_key_json = { type : ' service_account ' , project_id : ENV [ " SERVICE_ACCOUNT_PROJECT_ID " ], private_key_id : ENV [ " SERVICE_ACCOUNT_PRIVATE_KEY_ID " ], private_key : ENV [ " SERVICE_ACCOUNT_PRIVATE_KEY " ].gsub( /\\ n / , "\n" ), client_email : ENV [ " SERVICE_ACCOUNT_CLIENT_EMAIL " ], client_id : " CLIENT_ID " , auth_uri : ' https://accounts.google.com/o/oauth2/auth ' , token_uri : ' https://oauth2.googleapis.com/token ' , auth_provider_x509_cert_url : ' https://www.googleapis.com/oauth2/v1/certs ' , client_x509_cert_url : ENV [ " SERVICE_ACCOUNT_CLIENT_X509_CERT_URL " ] }.to_json service_acount_key_io = StringIO .new(service_acount_key_json) session = GoogleDrive :: Session .from_service_account_key(service_acount_key_io) spreadsheet = session.spreadsheet_by_key( ENV [ " GOOGLE_SPREAD_SHEET_ID " ]) return spreadsheet end def save_metadata (spreadsheet) branchName = ENV [ " GITHUB_BRANCH_NAME " ].dup versionName = branchName.delete( " qa/ " ) LANGUAGES .each do |key, value| row = spreadsheet.worksheet_by_title(key).rows.find { |row| row[ 0 ] == versionName } path = " ./app/src/main/play/release-notes/ #{ value } /default.txt " File .open(path, mode = ' wb ' ) do |f| f.write(row[ RELEASE_NOTES_COLUMN ]) end end end LANGUAGES = { ' ja ' => ' ja-JP ' , ' zh-Hans ' => ' zh-CN ' , ' zh-Hant ' => ' zh-TW ' , ' en-US ' => ' en-US ' } RELEASE_NOTES_COLUMN = 7 spreadsheet = fetch_google_sheets save_metadata(spreadsheet) Rubyファイル上でGoogle Sheetsを認証します。その後リリースノートファイル作成の関数を呼び出しテキストファイルとして保存しています。LANGUAGESのdictionary型はGoogle Play ConsoleにアップロードできるようにGoogle Sheetsのシート名を置き換えています。ちなみにサービスアカウントで .gsub(/\\n/, "\n"), の処理を行なっている理由は、環境変数から改行コードを読み込んだ場合 \\n になるので置換しています。詳しい説明は後ほど行ないます。 次にGitHub Actions上で上記のRubyファイルを実行できるようにセットアップします。 download_release_note.yml build : runs-on : ubuntu-latest steps : - uses : actions/checkout@v3.2.0 with : ref : ${{ github.head_ref }} - uses : actions/setup-ruby@v1 with : ruby-version : 3.1 - name : Prepare bundler run : | gem install bundler bundle install --jobs 4 --retry 3 - name : Deploy Metadata run : bundle exec ruby get_release_note_text.rb env : GITHUB_BRANCH_NAME : ${{ github.head_ref }} GOOGLE_SPREAD_SHEET_ID : ${{ secrets.GOOGLE_SPREAD_SHEET_ID }} SERVICE_ACCOUNT_PROJECT_ID : ${{ secrets.SERVICE_ACCOUNT_PROJECT_ID }} SERVICE_ACCOUNT_PRIVATE_KEY_ID : ${{ secrets.SERVICE_ACCOUNT_PRIVATE_KEY_ID }} SERVICE_ACCOUNT_PRIVATE_KEY : ${{ secrets.SERVICE_ACCOUNT_PRIVATE_KEY }} SERVICE_ACCOUNT_CLIENT_EMAIL : ${{ secrets.SERVICE_ACCOUNT_CLIENT_EMAIL }} SERVICE_ACCOUNT_CLIENT_ID : ${{ secrets.SERVICE_ACCOUNT_CLIENT_ID }} SERVICE_ACCOUNT_CLIENT_X509_CERT_URL : ${{ secrets.SERVICE_ACCOUNT_CLIENT_X509_CERT_URL }} - name : Commit and Push run : | git add ./app/src/main/play/* git diff --name-only set -x git config user.name github-actions[bot] git config user.email github-actions[bot]@users.noreply.github.com git add . git commit --author=. -m 'add release note text' git push こちらはプルリクエストが作成されたタイミングで実行されるワークフローです。リリースノートを取得した後にその変更をコミットし、originにプッシュします。セキュアな情報が多いのでシークレットに変数を追加することを推奨します。 上記のプルリクエストがクローズされたらGoogle Play Consoleにパッケージをアップロードします。 internal_test_deploy.yml on : pull_request : branches : - main types : [ closed ] jobs : build-and-deploy : runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v3 - name : set up JDK 11 uses : actions/setup-java@v3 with : distribution : "zulu" java-version : 11 - name : Create Empty local.properties for ci run : echo > local.properties - name : Copy CI gradle.properties run : mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name : Create Credential File run : echo '${{secrets.SERVICE_ACCOUNT_JSON}}' > ./app/service_account.json - name : Upload to Play Console run : ./gradlew publishReleaseBundle app/build.gradle //細かい導入方法は割愛しています import com.github.triplet.gradle.androidpublisher.ReleaseStatus play { serviceAccountCredentials.set(file('service_account.json')) track.set("internal") releaseStatus.set(ReleaseStatus.COMPLETED) } gradle-play-publisherはbuild.gradleに記述しアップロードをします。GitHub Actionsの実行ファイルはシンプルになります。internalは内部テスト配布です。内部テスト配布にしている理由は審査が入らないトラックなので即時にGoogle Play Consoleへ反映され確認がしやすいからです。alpha,betaは審査が入るので注意が必要です。 困ったこと Google Sheetsでリリースノートのバージョン管理を行なっているのですが、そのバージョンとトリガーになるブランチ名を一致させないといけませんでした。そこでGitHub Actionsでブランチ名を取得できる変数を使う方法を採用しました。実際に見ていきましょう。 download_release_note_text.rb def save_metadata (spreadsheet) branchName = ENV [ " GITHUB_BRANCH_NAME " ].dup versionName = branchName.delete( " qa/ " ) LANGUAGES .each do |key, value| row = spreadsheet.worksheet_by_title(key).rows.find { |row| row[ 0 ] == versionName } path = " ./app/src/main/play/release-notes/ #{ value } /default.txt " File .open(path, mode = ' wb ' ) do |f| f.write(row[ RELEASE_NOTES_COLUMN ]) end end end LANGUAGES = { ' ja ' => ' ja-JP ' , ' zh-Hans ' => ' zh-CN ' , ' zh-Hant ' => ' zh-TW ' , ' en-US ' => ' en-US ' } RELEASE_NOTES_COLUMN = 7 今回は github.head_ref を使用しています。運用としてはQA期間があり、テストが完了したらmainブランチへのマージを行ないます。そしてプルリクエストがオープンされたタイミングでワークフローが発火します。 github.head_ref を指定すると qa/1.0.0 のテキストが出力されます。そのテキストをRubyファイルに渡し、環境変数から値を取り出した後に qa/ の部分を削除し、バージョンのみを取得します。 まとめ 実際にブランチのトリガー1つでGoogle Play Consoleまでのアップロードを自動化してみたところ手動で行うことが減りました。人為的ミスも起こりにくくなり、導入して恩恵を受けました。ちなみに該当するバージョンのリリースノートが記載されていない場合はCIがエラーを起こして検知してくれるかつ、マージできないので気付けて便利です。GitHub Actions上で完結できるライブラリがあればもっとシンプルなワークフローになるので、改修コストもかからなくて良いと思いました。 最後までご覧いただきありがとうございました。ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com
アバター
ZOZO CTOブロックの @ikkou です。もうすぐ2022年も終わりますね。皆さんは師走のイベント、「アドベントカレンダー」に参加しましたか? ZOZOは例年アドベントカレンダーに参加していて、2020年は計100本、2021年は計125本、そして今年は昨年以上となる計175本の記事公開を“完走”しました。本記事ではその概要をお伝えします。 ZOZO Advent Calendar 2022 今年は計7個のカレンダーを実施したため、12/1-25の期間中、合計175本の記事を公開しました。 qiita.com 実施概要 アドベントカレンダーは「任意参加」で実施しています。エンジニア向けのSlackチャンネルで実施と参加を呼びかけ、各自で空いている日に登録する運用となっています。 公開する先はZOZO TECH BLOGだけでなく、QiitaやZenn、noteや個人のブログなど自由です。ZOZO TECH BLOGよりも気軽に書けるという観点から、QiitaやZennに書く方が多いです。 1人で複数の記事を書くこともあるので、今年は175本の記事に対して112名 *1 が参加しました。昨年は125本の記事に対して75名が参加していましたが、今年はいよいよ100名を超える参加人数となりました。アドベントカレンダーに参加するモチベーションは人それぞれですが、この参加人数の多さは特筆すべき点だと感じています。 アドベントカレンダーはアウトプットの練習に適したイベントです。ZOZOではテックブログをアウトプットの主軸に置いていますが、「まだテックブログを書く自信が無い」「テックブログに書くにはネタが小粒」のような場合に、アドベントカレンダーは良い機会です。 2022年の振り返り ZOZOのアドベントカレンダーでは例年その年を振り返る記事を公開しています。 開発組織については昨年同様、技術本部 本部長 兼 VPoEの @sonots が執筆しています。人事制度の見直しなど、昨年のZOZOとZOZOテクノロジーズの会社統合を経た「開発組織の今」を知れます。 qiita.com また、米国での「ZOZOFIT」の提供開始をはじめとする「プロダクト面の進歩」については、昨年同様「ファッションテックハイライト」を公開しています。あわせてご覧ください。 technote.zozo.com 過去のアドベントカレンダー ZOZOでは、2018年から毎年アドベントカレンダーに参加しています。 ZOZO Advent Calendar 2021 qiita.com ZOZOテクノロジーズ Advent Calendar 2020 qiita.com qiita.com qiita.com qiita.com ZOZOテクノロジーズ Advent Calendar 2019 qiita.com qiita.com qiita.com qiita.com qiita.com ZOZOテクノロジーズ Advent Calendar 2018 qiita.com qiita.com qiita.com 最後に ZOZOでは、プロダクト開発以外にも、今回のような外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com *1 : Qiitaアドベントカレンダーに登録している110名+2名の代理投稿
アバター
こんにちは。検索基盤部 検索基盤ブロックの佐藤( @satto_sann )です。 11月30日に ElasticOn Tokyo 2022 が行われました。今回弊社からは検索システムに関わるメンバー10名で参加して、そのうち2名が登壇しました。本記事では弊社エンジニアによる登壇の様子や気になったセッションについて紹介していきます。 目次 目次 ElasticOn Tokyoについて プログラム 全体聴講 ユーザ分科会 テクニカル分科会 ZOZOエンジニアが2名登壇しました 参加メンバーによるセッション紹介 ベクトルサーチによる関連性の追求 ベクトル入門、類似性について Elasticsearchでのベクトル検索について サーバレスアーキテクチャへの道 闇の魔術から身を守る。スピードが肝心 | Elasticでスピードを加速させるには? 最後に 番外編:会場の様子をお届け ElasticOn Tokyoについて ElasticOn Tokyo 2022は2022年11月30日に開催されました。コロナ禍の影響で、オフラインでの開催は3年ぶりでした。開催場所は恵比寿駅から徒歩5分ほどに位置するウェスティンホテル東京です。 会場の詳しい様子は、記事の最後に紹介します。 プログラム 本カンファレンスのプログラムは、午前は全体聴講、午後は分科会形式で行われました。 プログラムの詳細は下記を参照ください。 www.elasticon.com 全体聴講 午前は、Elasticsearch株式会社の代表である山賀氏の挨拶に始まり、基調講演や落合陽一氏による特別講演が目玉になっていました。Elastic社のGeneral ManagerであるMatt Riley氏の基調講演では、Elastic社のこれまでの歴史や今後の展望についてなど話がありました。 今後の展望に関しては、特にセキュリティ・オブザーバビリティ・AIという3つの分野がキーワードになっており、ただデータを蓄積するだけでなくよりパワフルにデータ活用できる環境を目指しているようでした。また、Elastic社のアジアVPであるBarrie Sheers氏によるアジア市場での展望の話では、アジアの各国にElastic社の拠点を作るほどアジアを重要視していることが紹介されていました。 ユーザ分科会 午後の部は、「ユーザー分科会」と「テクニカル分科会」の2つのブースに分かれます。参加者は気になるセッションのブースへ参加して聴講するという形式でした。 ユーザー分科会では、各ユーザー企業がElastic Stackの活用事例を発表し、参加者に知見が共有されました。 Elastic Stackの活用方法は企業によって様々でした。いずれの企業においてもElastic Stackを利用することでシステムが抱えていた大きな課題が解消されており、ソリューションのひとつとして大きな役割を担っていることが分かりました。 また、今後Elastic Stackの活用の幅をより広げていこうとしている企業も見受けられました。 テクニカル分科会 テクニカル分科会では、Elastic Stackの最新機能や実践的な利用方法が紹介されていました。ユーザー分科会と比べ、より技術的な知識の共有が際立っていました。 共通するテーマとして、膨大で複雑なデータを集約していかにそれらを活用するかが掲げられていました。また、オブザーバビリティ(可観測性)を強調するセッションも多く見受けられました。こちらのセッションについては、後ほどエンジニアがいくつか紹介するので詳細はそちらをご確認ください。 カンファレンスの後には、ネットワーキングの時間もあり、Elastic Stackを利用する様々なユーザー企業とコミュニケーションを取ることもできました。 ZOZOエンジニアが2名登壇しました ZOZOも午後のユーザー分科会に「ZOZOTOWNの商品検索におけるElasticsearch活用事例」というタイトルで登壇しました。 2名で登壇し、前半パートは検索基盤部 検索基盤ブロックの池田より発表しました。Elasticsearchを用いた商品検索のシステム構成や、ElasticsearchLTRプラグインを用いた「おすすめ順」の仕組みなどについて話をしました。 後半パートは、SRE部ECプラットフォーム基盤SREブロックの立花より「検索APIが利用するElasticsearchの運用をプロダクションレディにするための取り組み」について説明しました。 登壇後には、Elastic CloudのTerraform管理や運用面の課題に興味を持ってくださったユーザー企業様とネットワークを形成でき、ユーザー分科会の目的を達成できました。 登壇資料は以下にアップしておりますので、詳細はこちらをご参照ください。 speakerdeck.com 参加メンバーによるセッション紹介 ここからは参加メンバーから、聴講したテクニカル分科会やユーザ分科会の内容を一部紹介していきます。 ベクトルサーチによる関連性の追求 検索基盤部 検索基盤ブロックの今井です。 このセッションでは、ベクトル検索入門の話やElasticsearchでのベクトル検索の実現方法について紹介がありました。 ある単語を検索したとき、全文検索ではインデックス内に存在する文章の中から、その単語が含まれる文書を見つけます。言い換えると、文章内に単語が含まれていないと検索できないことを意味します。 一方、ベクトル検索を利用することで文章内に検索対象の単語が含まれていなくとも、単語の意図や意味を考慮して検索出来るようになります。そのほか、入力としてテキストだけでなく音声や画像も扱うことが出来たり、ドメインの特異性(ECのドメイン知識など)を学習して活用出来たりと、今まで出来ていなかった検索をすることが出来るようになります。 ベクトル入門、類似性について クエリと検索対象の文書をそれぞれベクトル化することで、ベクトルの距離が近いほど類似しているというように数学的な比較が出来るようになります。 また、ベクトルの次元数が多ければ多いほど、要素をより多く表現/比較することが出来るため精度は高くなります。ただ、その分計算量が増えてしまい処理時間がかかるようになってしまいます。次元数はElasticsearchでは最大2048次元まで対応しており、現実的なところでは300次元ほどではという話がありました。次元の上限に関しては、以下公式リファレンスにdimsパラメータの記載がありますので参照ください。 www.elastic.co ベクトルを用いた検索のために、ElasticsearchではANN(近似最近傍探索)がサポートされています。ANNではHNSWという複数レイヤーを使って近似値を出すアプローチをとっています。これにより、多少の精度とは引き換えに速度優先で検索することが出来て、大規模インデックスにおいても優れた処理速度で処理することが出来るとのことでした。 Elasticsearchでのベクトル検索について 実際にどう実装するかの話ですが、セッションでは以下の手順の紹介がありました。 学習モデルの準備 PyTorch(目的に沿ったモデル選び)→ クラスターへのモデルアップロード(eland_import_hub_model)→ KibanaのTrained Modelでアップロードモデル確認可能 ※モデルアップロードにはML機能を利用可能なライセンスが必要 データの取り込みと埋め込み(embedding)生成 ML inference Processorを用いて埋め込み ベクトルクエリ発行 クエリの埋め込みを生成(/ ml/trained_models/<model_id>/ infer)→ 生成された埋め込みをESLのknn項目に指定して_searchエンドポイントにリクエストを発行 knn項目で指定する内容は以下公式リファレンスを参照ください www.elastic.co 手順の紹介の最後には、全文検索でのスコアとベクトル近似スコアを組み合わせて利用することも可能という説明もありました。 このセッションを聴講して、テキストマッチングと組み合わせて利用することでテキストと画像を一緒に検索してスコアリングするなど検索の幅が広がり、可能性を感じました。知見はまだまだ貯まっていないと思いますので、実際に導入を検討する場合はベクトルの次元や要素の選定やベクトル検索の処理速度など実用レベルで利用するためによく検証する必要はあると思いました。 サーバレスアーキテクチャへの道 SRE部 ECプラットフォーム基盤SREブロックの大澤です。 このセッションではElasticsearchが現在アーキテクチャに至る経緯と課題から、今後予定されているアーキテクチャについて紹介されました。その中でもSREチームとして気になった内容についてピックアップして紹介します。 Elasticsearchは拡張性・耐障害性のためのShard分割、効率的なデータ管理のためのデータ階層化を経て現在のアーキテクチャに至りました。しかしながら様々な課題を抱えており、それら課題を解決するための新しいアーキテクチャが「ステートレス」です。 ここで特に注目したトピックは「データ投入と検索の分離」「オブジェクトストア」です。現在のアーキテクチャはデータ投入と検索でCPUを共有していますが、ステートレスではそれぞれ独立したCPUを使用します。それぞれの機能がCPUを占有して使えるようになることから、データ投入時のスループット向上やCPUコストの低減が期待できるようです。 SREチームでも負荷試験を行う際、データ投入処理にCPUが使われることで検索性能が低下する状況を観測しています。こうした状況に改善が見られるのではと非常に期待しています。 また、各ノードのローカルディスクに保持していたデータがデータオブジェクトになりレプリカデータを保持しなくなるようです。現在のアーキテクチャではノード拡張時にレプリカデータの作成完了を待つ必要があります。ステートレスアーキテクチャではこのようなアイドルタイムが軽減され、ノード拡張したら検索機能を直ちに使える状態になるのではとこちらも期待してます。 さらに将来的なアーキテクチャとしてサーバレスが紹介されました。定期的なバージョンアップ作業を抱えるSREチームとしては、バージョンレスといったキーワードは非常に魅力的です。 性能面・運用面共に新アーキテクチャはSREチームが抱える課題を解決してくれる可能性を感じました。機能提供された際には機能検証を行い、クラスタの乗換えを検討していきたいです。 闇の魔術から身を守る。スピードが肝心 | Elasticでスピードを加速させるには? 検索基盤部 検索基盤ブロックの佐藤( @satto_sann )です。 こちらのセッションでは、昨今のサイバー攻撃の脅威からサービスを防衛するための効率的なElastic Securityの利用方法について紹介されています。 とにかくタイトルが強烈だったので気になり参加しました。また、本カンファレンスの最後のセッションということもあり、私同様に気になって参加された方も多いのではないでしょうか? 昨今の働き方の多様化やシステムのクラウド化に伴って、社内だけ守る時代は終わり、ネットワークに混在する様々な自社のデバイスやシステムを脅威から守る必要があります。しかし、セキュリティ対策や監視に必要なログや関連するデータは複雑かつ指数関数的に増加する一方です。 インシデントが発生した際に、原因究明のためこれらのデータをシステムやデバイスごとに手作業で分析していては、被害がさらに拡大する恐れもあります。また、効率化のため個人で作成したツールやクエリで分析していては、スキルの属人化を招いてしまいます。そこで紹介されているのが、Elastic Securityです。 Elastic Securityは以下の機能を提供しています。 オープン&統合 セキュリティソリューションを1つの環境に閉じ込めず、再利用できるものは他の環境でも利用可能 セキュリティソリューションをブラックボックス化しない 検知ルールもオープンになっているので、アラート発生理由がわかる ネイティブプロテクション MITRE ATT&CKというサイバー攻撃の流れと手法を体系化したフレームワークやMLモデルを利用 どんなインシデントが起きていて、その原因や発生場所といった断片的なイベントを繋げた一貫した情報を提供 未来のスケールに他対応する設計 大量のデータ登録や分析が可能 今後Elastic Cloudに導入が検討されているサーバーレスの新アーキテクチャが利用できれば、よりインフラ側を意識しなくても良くなる インサイトの自動化 インシデントが起きた際の情報収集やトリアージ等の作業を自動化 分析者の負担を軽減 ハイブリッドクラウド マルチクラウドの利用が可能 マルチクラウドでデータを貯めていてもKibanaのフェデレーション機能で横断的に検索可能 発表を聞く限りElastic Cloudにデータを集約することで、オブザーバビリティが向上して、インシデント発生時に検知から解決までを一貫して効率よく対応できそうでした。 今回のカンファレンスでは特に、オブザーバビリティが強調されていたので、このセッションでもその有用性が紹介された形です。 最後にデモでは、Kibana上からElastic Securityを操作しながら上記機能を具体例と共に紹介していました。 例えば、クラウド上にあるKubernetesの設定が不足していてセキュリティの脆弱性があった場合、Elastic Securityではそれらを分析して、87%良いといったように良し悪しを可視化します。 他にもホストごとにセキュリティスコアを算出して可視化するEntity Analyticsダッシュボード。時系列ごとにどのようなアクションがされたか可視化、またその詳細の深掘りやアラート原因の可視化などをデモで紹介していました。 これらダッシュボードの詳細については下記公式ドキュメントを参照ください。 www.elastic.co セッションを聞いた感想としては、インシデント発生時の原因の分析やトリアージといった分析者の負担が特に大きい部分をElastic Securityでサポートしてもらう。これにより、迅速な対応が可能となり、被害も従来より抑えられそうだなと感じました。 また、このようなセキュリティ対策や対応は属人化も問題になるので、1つのサービスで完結できるとそういった課題も解決でき便利に感じました。 最後に 数年ぶりのオフラインイベント参加でしたが、現地でしか得られない最新情報を聞けただけではなく、合間の休憩時間でユーザー企業様と検索について意見交換できたのでとても有意義な時間を過ごせました。オンラインにはない良さを改めて感じました。 また、ZOZOではセミナー・カンファレンスへの参加を支援する福利厚生があり、カンファレンス参加チケット・宿泊費・交通費などは全て会社に負担してもらっています。今回参加メンバーが10人でカンファレンスの中でもトップレベルの大所帯だったと思いますが、例外なく福利厚生を受けられました。 ZOZOでは一緒に働く検索エンジニアを募集していますので、興味のある方は以下リンクからぜひご応募ください。 hrmos.co hrmos.co 番外編:会場の様子をお届け 検索基盤部 検索基盤ブロックの可児( @KanixT )です。 最後に会場の様子を簡単にご紹介いたします。 朝9時から開場だったため、朝早く遠方から参加された方や朝食を食べ忘れた方向けに軽食が用意されていました。 続いてElastic製品に触れられるブースです。 Kibanaなどのアイコンと一緒に写真が撮れるフォトブースもありました。 お昼はビュッフェ形式でお寿司やカレーなどさまざまなお料理が並びました。その中でもデザートのスイーツはとても美味しかったです。 セッション終了後には懇親会が催され、登壇者の方やElastic社の様々な方とお話させていただきました。
アバター
こんにちは。SRE部 ECプラットフォーム基盤SREブロックの高塚です。 11/28〜12/2に開催されたAWS re:Invent 2022に、ZOZOのエンジニア10名が参加しました。 アドベントカレンダーの1日目 ではHave Funイベントなどを紹介しましたが、この記事では現地の様子とセッションについてレポートします! AWS re:Invent 2022とは 現地の様子 セッション紹介 おわりに AWS re:Invent 2022とは re:Invent はAWS最大のカンファレンスです。2012年から毎年ラスベガスで開催されており、今年で11回目を迎えます。 2020年はオンライン開催のみで、2021年はマスク着用での開催でしたが、今年はマスクの着用義務がなくなり、いわば3年ぶりの通常開催でした。 今年も多くの新サービスや新機能が発表され、5日間で1500以上のセッションが行われています。セッションにはいくつかの形式があります。 形式 説明 Keynote 基調講演 Breakout Session 講義形式。登壇者はAWSの中の人、カスタマー、パートナーなど様々 Chalk Talk 登壇者と参加者がディスカッションする大学の講義のような形式 Workshop 参加者で少人数のグループを組み、テーマに沿って手を動かす形式 Gamified Learning ゲーム形式の参加型コンテンツ。GameDayやJamなど 現地の様子 キャンパス(会場)は全部で6つあります。地図の 黄色 がキャンパスです。南北で4km以上離れているため、無料のシャトルバスが運行されています。 引用: https://reinvent.awsevents.com/campus/ メインのキャンパスはベネチアン(地図の3)です。その隣のミラージュ(地図の8)に私たちは宿泊していましたが、ホテルの部屋からKeynote会場までは徒歩15分ほどかかりました。それほど各ホテルが巨大です。 Googleストリートビュー。正面がベネチアン、右後ろがミラージュです。 Keynote会場。1日目のMonday Night LIVEの様子です。 撮影:児島 こちらはExpo。企業のブースがたくさん並びます。 撮影:高塚 Self-paced labs。自分のペースでAWSを学習できるコーナーです。 撮影:高塚 期間中は5つのキャンパスで朝食と昼食が提供されます。ビュッフェのほか、テイクアウトできるランチボックスも用意されていました。 撮影:高塚 撮影:佐藤 撮影:纐纈 撮影:笹沢 食後のデザートもあります。 撮影:佐藤 また、食事とは別に各所でコーヒーや軽食が配られていました。 撮影:鈴木 撮影:佐藤 こちらはAWS公式グッズのストア。 撮影:高塚 4日目の夜にはre:Inventを締めくくるパーティー「re:Play」があります。キャンパスから少し離れた特設会場で行われ、DJやバンドの演奏と、ドッジボールやアーチェリータグなどのアクティビティを楽しめます。下記は全員の集合写真。 セッション紹介 ここからは各メンバーが気になったセッションを1つずつ紹介します。 Workshop From serverful from serverless Java with AWS Lambda (SVS310) Data analysis with Amazon EKS and AWS Batch (CMP355-R) Chalk Talk Prevent unintended access with AWS IAM Access Analyzer policy validation (SEC319) Breakout Session What's new and what's next with Amazon EKS (CON205) Architecting sustainably and reducing your AWS carbon footprint (SUS205) A close look at AWS Fargate and AWS App Runner (CON406) The future of sport: How Riot Games is reinventing remote esports broadcasts (CMP311) How to save costs and optimize Microsoft workloads on AWS (ENT205) Optimizing Amazon EKS for performance and cost on AWS (CON324) Workshop From serverful from serverless Java with AWS Lambda (SVS310) 計測プラットフォーム開発本部 計測システム部の児島です。私の所属する計測システム部は、今夏アメリカに向けてフィットネスアプリ ZOZOFIT をリリースしました。その認証機構にはCognitoを採用したのですが、カスタム認証フローを利用するため、Lambdaのトリガー処理を書く必要がありました。 私たちのチームは、開発言語にScalaを採用しています。一方で、LambdaとJVMの組み合わせはコールドスタートの問題があり、相性はよくありません。かつて、Lambdaを使いたいケースでPythonを採用したこともありました。最近では、より軽量なGoなどの言語もあり、多くの開発の現場でLambdaのために開発言語を分けるという選択がとても多く見られる様になりました。しかし、私たちのチームは2つ以上の言語を維持運用するほどのチーム規模もなく、言語としてはScalaのまま、LambdaのカスタムランタイムでGraalVMを選択する意思決定をしました。 In this workshop, learn how to bring traditional Java Spring applications to AWS Lambda with minimal effort and iteratively apply optimizations to enhance your serverless Java experience. Hear about best practices, performance trade-offs, and design considerations for each step to help you make well-informed decisions when bringing enterprise Java applications to AWS Lambda. 今回のWorkshopに参加したきっかけは、このWorkshopの説明欄にあったこの一節がきっかけでした。私たちは、多くの世に出ている知見からGraalVMを採用する意思決定をしたわけですが、その他に取り得た手法の検証に十分な時間がありませんでした。そのため、改めてLambdaでJVMを採用する際の選択肢と、それらの比較、それぞれの選択に潜在するトレードオフに関する知見を得たいというモチベーションがありました。 ワークショップでは、まず初めに暫定的なAWSアカウントが与えられ、Cloud9に既に必要なコード群が事前に用意された状態で、Lambda等の必要なリソースのセットアップを済ませます。そして、Spring Bootをベースとした何も最適化されてないサンプルアプリケーションをLambdaで動かすことからはじめます。そこから、TieredCompilationの有効化、依存の軽量化などを経て、GraalVMの採用まで、様々な最適化を施していきます。その過程で、実際のコード変化を確認し、ビルドと実行を繰り返し、ベンチマークを確認していきます。 結果です。既知のものも含まれていましたが、今回のWorkshopを通してあらかじめ用意されたLambdaのパフォーマンス改善の追体験ができたことは、とても有意義な時間でした。 さて、この前日のKeynoteでは、Lambda Snapshotという更なるLambdaにJVM系言語の採用を後押しする新機能の発表がありました。今回のre:Inventの他のセッションを通しても、毎年AWSがサーバーレスへの移行をサポートする機能を強化していく実感があります。今後も、こうしたアプリケーション開発者が開発に集中しやすくなる技術のリリースに期待するとともに、引き続き注視していきたいです。 Data analysis with Amazon EKS and AWS Batch (CMP355-R) 計測システム部 SREブロックの纐纈です。 基調講演では今年新しくリリースされたサービスや機能が紹介されます。その中で、AWS BatchをEKS上で利用できるようになったという発表がありました。 まず、AWS Batchは2016年のre:Inventで発表された、バッチ処理の実行基盤のマネージドサービスです。処理すべきジョブに合わせてコンピューティングリソースの配分を自動で最適化するという特徴を持っています。今まではECSでの実行環境のみだったのですが、2022年10月から新しくEKSにも対応したとのことでした。ただ、2022年12月時点ではEKS Fargateには未対応なので注意が必要です。 今回私が参加したワークショップでは、AWS Batchを使ってAmazon EKS上でデータ分析のサンプルを動かしてみるという内容でした。2時間のワークショップの中で、前半はAWS BatchのEKS対応について概要や内部構造などの説明があり、後半はAWS Workshop Studioを使って手を動かします。EKSクラスタを立てて、PrometheusやGraphanaをデプロイし、円周率の計算をするバッチ処理でクラスタに負荷をかけながらリソースの変動を見るということもできました。 ワークショップの内容はこちらからも見ることができます。 catalog.us-east-1.prod.workshops.aws Chalk Talk Prevent unintended access with AWS IAM Access Analyzer policy validation (SEC319) SRE部 ZOZO-SREブロックの鈴木です。 普段はZOZOTOWNの裏で動いているオンプレ、AWSにあるインフラの管理をするとともに社内のAWS管理者としてセキュリティ周りの対応や各種運用をしています。 今回私が参加したSEC319は「 Chalk Talk 」という形式のセッションです。スピーカの方がスライドを発表するだけでなく、横にホワイトボードが置かれており参加者も質問したりディスカッションしたりしながら、さながら大学の講義のように進んでいきます。動画配信がなく現地に行ったメンバーのみが参加できるセッションであり、現地へ行った優越感に浸れるおすすめのセッションです。発表頂いたスライドは資料欄にリンクを掲載しています。 本セッションではIAMポリシーのライフサイクルを定義し、その過程で安全なIAMポリシーを作成、メンテナンスしていくにはどうしたらいいかを議論しました。IAMポリシー作成過程の1つに「Validation」のステップがあり、ここで正しくポリシーを検証することが安全性を高める1つの手段となることを確認しました。そのValidationのツールとして「 IAM Access Analyzer 」が紹介され、実際にどのように使うかのデモをいただきました。 IAM Access Analyzerを利用する際のCI/CD構成例の1つに「CloudFormation hooks」を利用した例があげられました。参加者にはCloudFormation hooksを使っているか、どんなところで使っているのかという質問が投げかけられ、各社の真似したくなるような事例をいくつか聞くことができました。 資料 Prevent unintended access with AWS IAM Access Analyzer policy validation awslabs/aws-cloudformation-iam-policy-validator Breakout Session What's new and what's next with Amazon EKS (CON205) 計測システム部 SREブロックの西郷です。 普段は ZOZOMAT や ZOZOGLASS 、 ZOZOFIT といった計測サービスの運用に携わっています。これらの基盤にはEKSを採用しているので、業務に活かせるものがあればと思い参加しました。 今回のセッションの中で気になったトピックとしては以下です。 EKSクラスタのKubernetesバージョンアップデートにかかる平均時間の短縮 40分以上かかっていたものが10分少々にまで短縮された VPC Lattice のサポート VPC Latticeはre:Invent期間中に発表された新機能で、VPCの1機能 L7のサービス間ネットワーク通信機能を提供するとのことで、サービスメッシュに近いものの印象 サイドカーなしで利用でき、プロトコルはHTTP、HTTPSに加えgRPCもサポートしている VPCPeeringなしでアカウントおよびVPCを跨いだ接続が可能 MarketplaceのソフトウェアとAWSによって開発されている主要なOSSのプロダクトをEKS add-onsがサポート re:Invent期間中に発表された新機能、EKS add-onsを通してEKSクラスタ内にデプロイ可能 DatadogやNewRelic、KubernetesのポリシーエンジンであるKyvernoを提供するNirmata等がComing Soonなものとして上がっていた これまでにも計測ではEKSクラスタのバージョンアップの省力化を進めてきたこともあり、Kubernetesバージョンアップ自体にかかる時間を短縮できることは大きなメリットになりそうです。 また、VPC Latticeはサイドカーなしで利用できるという点が魅力的です。現時点でサービスメッシュの利用はないものの、システムの成長に伴い検討していた背景もあるため、機会があれば検証してみたいと思っています。 ※2022/12時点ではVPC Latticeはプレビュー機能です。 Architecting sustainably and reducing your AWS carbon footprint (SUS205) SRE部 ECプラットフォームサービスSREブロックの佐藤です。普段はZOZOTOWNで使用されるマイクロサービスのインフラ構築と運用を担当しています。それ以外にも、チームをまたいだ活動としてクラウドサービスのコスト可視化・最適化にも取り組んでいます。 アダム・セリプスキーCEOのキーノート でもふれられていたようにサステナビリティは今年のre:Inventでも重要なテーマの1つでした。 本セッションは、AWSにおけるサステナビリティへの取り組みと、Amazon Prime Videoでの実例を紹介する内容になっています。コスト効率の良いシステムを考える上で、今後必須の知識になるであろうと思い、このセッションに参加しました。 セッション前半では、まずAmazonのサステナビリティに対する考えが述べられました。それから気候変動の対策に関する誓約である The Climate Pledge や、データセンターで使用される100%再生エネルギーについて、ウォーターポジティブへの取り組みなどが紹介されました。Amazonのサステナビリティに対する取り組みは、下記のサイトで確認できます。 sustainability.aboutamazon.com その中で、サステナビリティとは持続可能な未来というゴールへ向かうものではなく、永続的に繰り返す活動(セッションの中ではジャーニーと呼んでいました)であると解説していました。そのために適切なリソースを適切なタイミングで使用することが重要で、つまり効率化が中核をなす要素だと説明しており、日常的に自動化・効率化に励むSREにとって刺さる話でした。 また、 Well-Architectedフレームワーク から、 リージョンを選ぶ基準としてレイテンシやコストに加えAmazonの再生エネルギープロジェクトに近い地域、または二酸化炭素排出量の少ない地域であるか検討するプラクティス や、 よりエネルギー効率の良いインスタンスを継続的に使用するためのプラクティス の解説もありました。 後半ではAmazon Prime Videoにどのようにサステナビリティのカルチャーを取り入れ、システムの効率化を行ったのか、そのプロセスの紹介がありました。プロセスの概要は下記です。 目標を設定する 二酸化炭素の排出量削減に繋がる具体的なゴールを決める。例えばネットワーク負荷を減らすようなエンコードアルゴリズムやインフラリソースの最適化など。 この時点で、この活動に必要なリソース確保をマネージャーレベルで調整する。 指標を設定する 自分たちのサービスがどれくらい二酸化炭素を排出しているかは、 Customer Carbon Footprint Tool で確認できるが、より自分たちがコントロールしやすい代替指標(プロキシメトリクス)を定義する。例えば特定のワークロードにかかるインスタンス時間など。 反復的な改善プロセスを実行する ステップ1: 改善対象を特定する。例えば適切なインスタンスのサイジングなど。 ステップ2: 改善点を評価する。 AWS Compute Optimizer などのツールを利用する。 ステップ3: 優先順位をつけて計画を立てる。得られる成果をビジネス価値に置き換え判断材料とする。同時に、成果に関心を持つ財務や利害関係者への窓口を設置する。 ステップ4: テストし検証する。単一ホストのみで動作させるなどして小規模に始める。本番トラフィックが流れない環境を用意してスケーリングもテストする。 ステップ5: 本番環境にデプロイする。 ステップ6: 結果を測定し、成功事例を別チームに展開する。 さらに、NFLの中継を独占配信した際に行われた取り組みが取り上げられていました。それは社内から効率化の専門家を集め、各サービスのコードとスタックについてパフォーマンスレビューをするというものです。これは現在弊社で行っている正月セールに向けた取り組みと重なる部分があり、案外こういう社内の関心が高いイベントのときこそ、周りを巻き込みやすいので新しい挑戦がしやすいのではと思いました。 またスパイク対策として、重要度の低い機能をオフにするコンティンジェンシースイッチと呼ばれる仕組みの導入や、クライアントサイドでのプリキャッシュについて説明がありました。これらは持続可能な方法でスケールできるようにデザインパターン化しているとのことでした。 セッション全体を通しての気づきは、我々が普段行っているパフォーマンスチューニングやコスト最適化は、実はサステナビリティな活動そのものだということです。そして業務にはコストやビジネス要件、コンプライアンスなど優先すべきことが多いですが、まずは同じ天秤に乗せるのが大切であると感じました。そうすることで、今後の設計や技術選定などにおいて別の観点が加わり、組織や社会にとってより良い判断ができると思いました。 A close look at AWS Fargate and AWS App Runner (CON406) ブランドソリューション開発本部 バックエンド部の笹沢( @sasamuku )です。 Archana SrikantaさんよりAWS Fargateの裏側の仕組みが紹介されました。re:Invent 2019における 同氏らの発表 と重複する箇所もありましたが、今回はよりセキュリティと可用性に焦点を当てた内容となっていました。以下、抜粋してご紹介します。 前半はコンピューティングサービスの進化について説明がありました。Amazon EC2が主役だった頃と比較するとAWSの責任領域が次第に拡大しています。直近にリリースされたAWS App Runnerでは、ソースコードあるいはDockerイメージを用意して基本的な設定をするだけでスケーラブルなサービスを公開できます。 後半はセキュリティに関する興味深い話がありました。AWS Fargateはマルチテナントなサービスです。悪意あるユーザによって脆弱性を突かれれば他のユーザにも影響が出かねません。AWSは仮想的にハードウェアを分離することでこれを防止しています。具体的には、Firecrackerと呼ばれるMicro VMをタスク1つにつき1台用意しています。また、タスク間やベアメタルサーバ間の意図しない通信を許可していません。 他にもセルラーアーキテクチャと呼ばれるAWS Fargateの可用性に関する仕組みも紹介されていました。Amazon ECS on AWS FargateやAWS App Runnerを利用している方にはぜひおすすめしたいセッションの1つです。 The future of sport: How Riot Games is reinventing remote esports broadcasts (CMP311) SRE部 ZOZOSREブロックの秋田です。オンプレとクラウドのハイブリットな環境を運用保守するチームに所属しています。 私は普段からesports観戦が好きで、特にRiot Gamesが出しているLegue of LegendsやVALORANTのタイトルをよく観戦しています。大会の裏側で全世界へどのようにして配信をしているか興味がありこのセッションを聴講することにしました。 このセッションでは、Riot Gamesが従来の配信方法からどのようにeスポーツの配信方法を進化させたか、どのように配信基盤を作成しているかという話がされました。 特に興味を引いたものがRiot Directです。世界中に専用のネットワークを有しており、オンライン対戦で使われるほか、配信でもこれが使われています。 Riot Directは以下のようなものになります。 Riotのパケットに特化したケーブルとルーターを世界中に展開 Cold-potato routing 世界中で数Tbpsの帯域が利用可能 IXPやPNIを経由して世界の主要ISPにピアリング可能 独自のDDoS軽減ソリューションと既製品のDDoS軽減ソリューションの混在 また、このRiot Directですが3500以上ものBGPセッションが管理されており、様々な地域と拠点で相互接続することが可能になっています。 ZOZOTOWNのサービスとはかけ離れているセッションでしたが、普段は聞くことが出来ない世界トップゲームの話をリアルで聞けてとてもいい経験になりました。 How to save costs and optimize Microsoft workloads on AWS (ENT205) SRE部 カート決済SREブロックの伊藤です。ZOZOTOWNはWindowsサーバーやSQL Serverを使用して稼働している箇所が多くあります。それらのコスト最適化の参考になればと本セッションを受講しました。 techblog.zozo.com techblog.zozo.com 一部を抜粋してご紹介させていただきます。 FCI(Failover Cluster Instance)を構築する SQL Serverで高可用性を実現する方法としてAlwaysOnが使われがちではあるが、FCIを構築するという方法がある FCIの場合必要なSQL Serverのライセンスは1つだけであり、コストを抑えることができる aws.amazon.com EC2のCPUオプションを指定する メモリを多く使用したいが、CPUはそこまで必要ないなど一致するインスタンスが存在しない場合にvCPUの数を抑えることができる インスタンス自体のコストは変わらないが、SQL Serverなどコア単位でライセンス量が変わってくる場合にコストを抑えることができる 具体的にはr5.2xlarge(8core, 64GB)を選んでvCPUを4coreに抑えると40%の節約となる docs.aws.amazon.com その他、NitroSystemであるが故にリソースを効率良く利用できることや、既存環境を分析してレポートしてくれる OLA(Optimization and Licensing Assessment) に関しての説明などがありました。 Optimizing Amazon EKS for performance and cost on AWS (CON324) SRE部 ECプラットフォームサービスSREブロックの三品です。普段の業務でEKSを利用しているので、EKSのコスト最適化に関するこちらのセッションに参加しました。 前半はKubernetesのコスト監視の難しさと、どのようなところでコストが発生しやすいかについて解説されていました。前半の最後には、Kubernetesのコストをどのように可視化したら良いかが述べられていました。 後半では、コストを最適化するためにはどうしたら良いかについて解説されていました。具体的にはAWS Gravitonインスタンス、EC2 Spot、Karpenterについて実際の企業事例を踏まえて紹介されていました。 AWS Gravitonインスタンス AWS Gravitonインスタンスの特徴(一部抜粋) C7gインスタンスは、計算集約型のワークフローに最適な価格性能を提供しています 同クラスのEC2インスタンスと比較して60%エネルギー効率化されています (注意)AWS Gravitonインスタンスを利用するためには、ARM互換のイメージを作成する必要があります EC2 Spot EC2 Spotの特徴(一部抜粋) インスタンスタイプの種類やAZを増やすことでSpotの可用性が向上し、中断が少なくなくなります 時間に融通の効くワークロードは、空き容量を待つことができるのでEC2 Spotは最適です EC2 Spotは短期間の中断可能なワークロードに適しており、逆に長時間稼働するようなJobやstatefulなワークロードには適していません Karpenter Karpenterの特徴(一部抜粋) EC2 SpotやAWS GravitonインスタンスなどをKubernetesネイティブに統合しています ノードの起動を高速化することでコストを最小限に抑えることができます オペレーションのオーバーヘッドを削減することによって開発者はサービス開発/運用に集中できます 個人的には、前半で紹介されたkubecostに興味を持ちました。まずはkubecostを導入して適切なリソースを割り当てられているかを確認することが、コスト最適化の糸口を探すことになるはずなので、検証してみたいと思います。 おわりに re:Inventは多くのセッションをオンラインでも視聴できます。それにも関わらず、わざわざ現地参加する価値とはなんでしょうか? 会場をキャンパスと呼ぶとおり、re:Inventは学習型のカンファレンスです。しかし、その雰囲気はまるで「お祭り」です。AWSの大好きな人々が世界中から集まり、皆で学び合い、熱く語らう「お祭り」です。この盛り上がりは現地でしか感じられません。 また、WorkshopやChalk Talkなどの体験型セッションが豊富で、そこに集まる参加者のレベルも高く、濃密な知見を得られます。AWSやパートナー企業のエンジニアと直接話せる貴重な機会でもあります。 もちろん、オンライン参加でも多くの学びを得られると思います。KeynoteやBreakout Sessionは1つ1つのクオリティがとても高く、たくさんの新情報を含んでいます。 でも、この刺激、特に 他のエンジニアから受ける刺激 は、ラスベガス現地でしか味わえないと確信します。この刺激を求め、私たちは来年もre:Inventに参加するはずです。 今回得た知見と刺激を社内外に共有しながら、これからもAWSを活用してプロダクトとビジネスの成長を支えていきます! ZOZOではAWSが大好きなエンジニアを募集しています。奮ってご応募ください。 corp.zozo.com corp.zozo.com 最後に、ラスベガスでお世話になった方々へ御礼申し上げます。ありがとうございました。
アバター
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの渡辺です。普段はZOZOMATやZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。 先日私達のチームでは、リリースフローにステージング環境での負荷試験を自動化する取り組みを行いました。今回説明する「負荷試験の自動化」が何を表すのかを定義すると、ここではステージング環境のアプリケーションバージョンを変更した際に、人の手を介さずに負荷試験が行われることを指します。 Kubernetes環境における負荷試験の自動化を検討している方は是非参考にしてください。 目次 はじめに 目次 負荷試験を自動化する前の課題 負荷試験のシナリオ設計 目的設定 現状調査 目標値設定 シナリオ設計 負荷試験を自動化する取り組み 構成 処理の流れ シナリオに沿ったリクエストを送る 実行結果をS3に保存してSlackに通知する 負荷試験で作成したデータを削除する 負荷試験の実行トリガー 今後の改善ポイント 自動化の精度向上 リリース判断の自動化 まとめ 終わりに 負荷試験を自動化する前の課題 私達のチームではEKS環境でアプリケーションを管理しており、これまでArgo CDやArgo Rolloutsなどのツールを導入することで、リリース安定性を向上させてきました。 techblog.zozo.com techblog.zozo.com 特にArgo Rolloutsを導入したカナリアリリースにより、アプリケーションエラーやレイテンシ悪化などによるユーザへの影響を最小限にできました。 しかしながら、ステージング環境での動作確認だけでは見落とされる問題については、カナリアリリース後にアラート通知がくるなど未然に防げませんでした。例えば、ライブラリ更新によるメモリリークの発生がリリース後に発覚する事象などが挙げられます。 負荷試験のシナリオ設計 そこで、上記の問題を解決すべく、ステージング環境で負荷試験を自動化する取り組みを行いました。 まずは、負荷試験のシナリオを設計するために行ったことを紹介します。 目的設定 負荷試験はあくまで手段であるため、目的を見失わないようにあらかじめ「アプリケーションの特性」「現在の課題」「負荷試験の目的」などをドキュメント化しました。 負荷試験と一口に言っても、目的によってシナリオが変わってくるので、多少面倒でも目的をまとめておくことをオススメします。 自分達が負荷試験で何を実現したいのかを検討した結果、「負荷試験を自動化する前の課題」でも説明しているようにリリース後の性能悪化を事前に検知することが一番の目的となりました。 ピーク時の性能テスト ロングランテスト テスト目的 在庫数よりも需要が大きく上回る商品の発売やメディア露出によるリクエストピーク時に十分なパフォーマンスが得られるか確認する 通常時に十分なパフォーマンスが得られるか確認する(メモリリークの発生など長時間テストしないと表面化しない問題の確認) 「ピーク時の性能テスト」を基本として、「ロングランテスト」はオプションとして設定しています。メモリリークなどロングランテストを行うことで検知の精度が高まるものもありますが、リリース作業に数時間の負荷試験を実行することは現実的でないと判断しました。 なお、今回は見送りましたが、今後は以下の観点でも負荷試験を活用したいと考えています。 システム限界点を見極める 予想外のスパイク発生を想定したテストを行い、短時間に処理が集中した場合に発生する現象を確認する 限界を超えた場合にどのような現象が発生するか確認する 現状調査 テスト目的から、ピーク時の本番トラフィックを基にシナリオを作成するため、各APIごとリクエストの実績を調査しました。時間帯や曜日ごとに数が変化しているため、現状調査を通じてサービスの理解やユーザ行動などを深く認識できました。 負荷試験とは直接関係ありませんが、サービスのドメイン知識が少ないエンジニアにとって、現状調査をすることで得られるものは大きいと実感しました。このため、入社間もないドメイン知識が少ない方こそ負荷試験のタスクに取り組むことをオススメします。 目標値設定 負荷試験の実行結果と比較するための目標値を設定しました。 「目標値をクリアできないとリリースしない」などの指標にするため、既にSLOやSLAを策定している場合はその数値を利用してもいいと思います。しかし、私たちのチームでは各APIごとのSLOを策定していないため、導入初期の段階として「現状のレイテンシやエラー率を上回らない」ことを目標とする値を設定しました。 今後、各APIごとのSLOを策定した際には、こちらの目標値も更新していくつもりです。サービスの成長と共に目標値は変わってくるため、一度設定して終わりではなく定期的に見直すことが大切です。 シナリオ設計 上記の調査で得た情報を基にシナリオを設計します。 計測プロダクトを利用するユーザの行動は、大きく2つに分けられます。ユーザ行動はとてもシンプルなので、シナリオもシンプルになります。 計測ユーザ(計測行動) 商品閲覧ユーザ(計測後の行動) 例えばZOZOGLASSの計測ユーザ行動は以下のようになります。 2パターンのユーザ行動のシナリオを設計し、目的設定や現状調査で積算したパラメータ(テスト時間やリクエスト毎秒など)を割り当てていきます。計測ユーザよりも商品閲覧ユーザの方が多いため、以下のようなイメージでシナリオを実行していきます。 なお、先ほどオプションとしてロングランテストを設定していると説明しましたが、パラメータを環境変数で設定することによりテスト目的に合わせて簡単に切り替えられるようにしています。 負荷試験を自動化する取り組み それでは、負荷試験を自動化する取り組みについて紹介します。 私たちはKubernetesマニフェストをGitHubで管理しており、Argo CDによるGitOpsを実現しています。 Podのコンテナイメージのタグ更新PRをmainブランチにマージした時、Argo CDによりステージング環境のPodが入れ替わります。 リリース担当者はPRのマージを行うまでを担当し、Podの入れ替えから負荷試験の実行までを自動的に行うような仕組みを構築しました。 構成 負荷試験ツールとしては、社内でも利用実績のあるGatlingを採用しました。 GatlingはテストシナリオをScalaで記述でき、結果レポートをHTMLで自動生成してくれます。計測プロダクトではバックエンドにScalaを採用していることもあり、馴染みのある言語でシナリオを作成できました。 処理の流れ 負荷試験を実行するPodが行うことは以下になります。 シナリオに沿ったリクエストを送る 実行結果をS3に保存してSlackに通知する 負荷試験で作成したデータを削除する シナリオに沿ったリクエストを送る 負荷試験を実行するコンテナをアプリケーションのサイドカーに設置してリクエストを流すこともできます。 しかし、計測サービスではレイテンシを重要視しているため、より本番トラフィックに近いネットワークでリクエストを送信したいという要求がありました。そこで、負荷試験を実行するPodからNAT Gatewayを経由する設計にしました。 ステージング環境は、ALBのルールで限られたIPからのリクエストのみ受け付ける設定をしています。NAT Gatewayに紐付けたElastic IPを許可することでリクエストを受け付けるようにしました。 実行結果をS3に保存してSlackに通知する リリース担当者が負荷試験を待っている間に別の作業をしていると、つい負荷試験の完了に気づくのが遅れてしまいます。結果的にリリース作業も遅れるため、負荷試験の完了時にSlackへ結果レポートのリンク先(S3)を通知します。リンクをクリックするだけで結果を確認できるので、リリース担当者の手間が省けます。 負荷試験で作成したデータを削除する 負荷試験ではダミーデータで計測するため、ダミーデータがDBやS3に蓄積されていきます。塵も積もれば山となるため、負荷試験の実行後に該当データを削除しコスト負担が大きくならないようにしています。 負荷試験の実行トリガー 私達のアプリケーションはJVM環境で動作しており、アプリケーションコンテナがデプロイされる際は、リクエストを受け付ける前に暖機運転を行っています。 techblog.zozo.com また、起動中のPod数をコントロールするためのPodDisruptionBudget設定やカナリアリリースを採用していることもあり、すべてのPodが入れ替わるまでにタイムラグが生じます。 このため、アプリケーションのデプロイと同じトリガーで負荷試験用のPodを起動すると、全てのPodが新しいバージョンへ入れ替わる前に実行される場合があります。 これでは、正しい試験結果を得ることはできません。負荷試験の開始をsleepなどで調整することも考えられますが、確実にPodが入れ替わった保証にはなりません。 そこで、全てのPodが新バージョンに入れ替わったことを負荷試験のトリガーにするため、当初はKubernetesのカスタムリソースを作成することを検討しました。 しかし、コントローラのメンテナンスなどを考慮すると負担も大きく感じたので、他の方法がないかネットの海を探索していたところ、Argo CDの Resource Hook が目に留まりました。 Jobリソースのannotationsに argocd.argoproj.io/hook: PostSync を設定するだけでデプロイ後のヘルスチェックが成功した後でJobを起動できます。たった一行追加するだけで要件を満たすことができたので、Argo CDを導入した恩恵を受けました。 今後の改善ポイント 自動化の精度向上 Argo CDのResource Hookはとても便利なのですが、Pod関連以外のリソース変更時もトリガーになるところがコントロールできません。 このため、Kubernetesリソースの変更があった場合(例えばServiceAccountを作成するなど)、アプリケーションは変更していないのですが負荷試験が実行されてしまいます。 現状、Kubernetesリソースの変更や追加は頻繁に発生していないので、意味のない負荷試験が実行される回数は少ないです。しかし、開発が盛んなプロダクトへ導入する際は、この辺を工夫する必要がありそうです。 リリース判断の自動化 今回、負荷試験の自動化は実現できましたが、リリースの判断の自動化までは実現できていません。 現状は、Slackに通知された結果レポートと目標値設定で策定した値を比べて、リリース担当者がリリースの可否を判断しています。 ヒューマンエラーを防ぐことはできていないため、レイテンシの悪化に気づかずリリース作業を進める可能性もゼロではありません。 また、負荷試験の結果を待たずとも本番リリースはできてしまうので、今後は負荷試験の結果でリリース判断を機械的に判断する仕組みを構築していきたいです。 まとめ 今回は計測システムのリリース業務の安定性向上の一例として、負荷試験を導入する方法についてご紹介しました。ステージング環境のデプロイ時に自動で負荷試験を実行する仕組みを構築したことで、より安全に定期リリースを行えるようになりました。 負荷試験は導入して終わりではないので、今後はさらなるサービスの安定性に役立てるため積極的に活用していきたいと思います。 終わりに 計測プラットフォーム開発本部では、ZOZOMATやZOZOGLASS、ZOZOFITなど様々なサービスを展開しています。今後も新しいサービスや機能を開発する予定であるため、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
アバター