TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

987

はじめに こんにちは、ZOZOMO部OMOバックエンドブロックの長野です。普段はZOZOMOのサービスである ブランド実店舗の在庫確認・在庫取り置き (以下、店舗在庫連携)の開発・保守を担当しています。 店舗在庫連携はAWS上にシステムを構築しており、システムにはAWSの各サービスを利用しています。AWS上で構築するシステムは、マルチAZなどの冗長構成をとることで可用性を高めることができます。しかし、実際に障害が起こった際に、意図していなかった箇所でシステムが停止してしまう可能性は否定しきれません。 OMOバックエンドブロックでは、このような未知の障害を防ぐためのアプローチとしてカオスエンジニアリングを実施しました。本記事ではカオスエンジニアリングの説明とチームで行った結果を紹介します。 目次 はじめに 目次 カオスエンジニアリングとは 1. 定常状態を定義する 2. 仮説を立てる 3. 実験する 4. 検証する カオスエンジニアリングの目的 チームで実施したこと 仮説を立てる 実験する 検証する カオスエンジニアリングを実施して良かったこと まとめ カオスエンジニアリングとは カオスエンジニアリングは、稼働しているシステムに対して意図的に障害を発生させることでシステムの堅牢性・回復力を高める手法です。特にクラウド上で構築されるシステムは、各コンポーネント同士が疎になっていて、障害時の挙動の予想がより困難になっています。そこで、意図的に障害を発生させることで、システムの未知の挙動を発見することが期待できます。 カオスエンジニアリングは主に4つのステップで構成されます。 定常状態を定義する 仮説を立てる 実験する 検証する それぞれのステップについて説明します。 1. 定常状態を定義する 障害を注入する前にシステムの通常時の振る舞いを定義します。 定常状態は、障害発生中にシステムが問題なく稼働していることの確認や、停止状態からの復帰の判断に利用します。スループット・エラーレート等のメトリクスやSLOの値などが用いられます。 2. 仮説を立てる 定常状態を破壊できる事象を考え、その事象が発生してもシステムが停止しないという仮説を立てます。 いきなり仮説を立てることが難しい場合には、先ず起こりうる事象を考えるところから始めるのがやりやすいと思います。仮説を立てる中で課題を発見した場合は、実験の対象にはせずに改善します。 カオスエンジニアリングは未知の発見が目的になるので、既知となった問題は実験対象として扱いません。 3. 実験する 定常状態を破壊できる事象を実際に発生させます。 カオスエンジニアリングによる実験は、本番環境で実施することが推奨されますが、まずは開発環境で実施することをおすすめします。仮説が正しいことを開発環境で確認した後に、本番環境で実施するようにした方が安心出来ますね。 4. 検証する 仮説が正しかったか確認します。 もし仮説が誤っていた場合は、未知を発見できたということになるので、システムのどの部分が停止したかを確認して改善していきます。 カオスエンジニアリングの目的 前述のとおり、カオスエンジニアリングは未知を発見することが目的ですが、それだけではありません。定常状態を破壊できる事象を考えることや検証することは、開発者のシステムへの理解を高めることにもなります。 例えば、定常状態を破壊できる事象を考えるためにシステムのアーキテクチャ図を眺めることや、仮説が誤っていた場合に実際にはどうだったのかを調査する必要があります。これらを繰り返すことでチームはシステムに対する理解を深め、システムはより堅牢なものになっていきます。 チームで実施したこと チーム内にカオスエンジニアリングの実施経験のあるメンバーがいなかったため、まずはカオスエンジニアリングの目的や期待できる効果について、前述した内容をチーム内で会話して共通認識にしました。その後、仮説をもとに実験と検証を実施したのでそれぞれお話しします。 仮説を立てる 仮説を立てるにあたり、まず発生し得る事象を洗い出しました。下記は洗い出した事象の一例です。 ECSのタスクが落ちる 特定AZで障害が発生してAZ内のリソースが利用不可になる 次に、事象発生の際にどのような影響があるかを考えました。例えば、ECSのタスクが落ちた場合、サービススケジューラで設定してある必要タスク数まで自動復旧する等です。 このような仮説を立てた後はサービスへの影響度を考えます。その仮説が間違っていて問題が発生してしまった場合の影響度を考えることで、カオスエンジニアリングによる実験の優先順位を決定します。 最終的には、すべての仮説を検証して、システムの安全性に自信をもてる状態を目指しますが、まずは優先順位の高いものから手を付けていきます。仮説は上記の例以外にも挙げられましたが、今回は影響度が高いAZ障害への耐障害性を実験することに決定しました。 実験する AZ障害を発生させるにあたり、障害を注入するツールとしてAWS Fault Injection Service(以下、FIS)を利用しました。FISの詳細は AWSのドキュメント をご確認ください。 FISでは、VPCサブネットを出入りする通信を妨害する障害を注入できます。特定AZのサブネットのみ通信を妨害することで、各AWSサービス間の通信を失敗させ、AZ障害を再現しました。 店舗在庫連携では、主にALB、ECS、RDSを用いてシステムを構築しているため、この3つのサービスでAZ障害を起こし、耐障害性を検証します。 下図に各リソースがどのように配置されているかの簡単な図を示します。画像内の赤い枠線で囲まれたサブネットが今回障害を発生させる対象になります。 下の画像のようにFISは実施したいアクションと、そのターゲットを組み合わせることで障害を発生させます。画像では、サブネット通信を妨害するアクションを disrupt-connectivity に、特定AZのサブネットを選択したグループのターゲットを Subnets-Target-1 に設定しています。このアクションとターゲットを紐づけることで、障害を発生させることが出来ます。 FISでは、複数のアクションやターゲットを組み合わせることも可能です。その他、適切なIAMロールを設定することで、不要なリソースへのアクセスを拒否することや、CloudWatch Alarmを用いてメトリクスをもとに実験を停止させる等、安全面も考慮されています。 今回、店舗在庫連携のAPIでは、下記内容で実験しました。 前提 実験環境:検証環境 AWSの各リソースは3AZに配置 10req/sで店舗在庫連携のAPIを7分間実行する 実験 APIを実行中に特定AZのサブネット通信を妨害し、AZ障害を発生させる 仮説 各リソースでマルチAZの冗長構成にしているためサービス提供は止まらない 下図はAPIの実行結果になります。15:59:20から16:01:50の2分30秒間で504エラーが発生していることがわかりました。 実験結果:15:59:20から16:01:50の2分30秒間で504エラーが発生していることがわかる 検証する 実験結果が仮説通りだったか、もし仮説通りでなければ実際には何がおこっていたのかを確認していきます。 今回の実験では「各リソースでマルチAZの冗長構成にしているためサービス提供は止まらない」でした。これ自体は間違いではありませんでしたが、詳しく見ると一部のリクエストが数分間エラーになる状態でした。 カオスエンジニアリングは実験してシステムの安全性を確認して終わりではなく、何が起こっていたのかを確認して開発者の知識を増やすことも目的の1つです。 今回の実験で2分30秒間エラーが発生した理由を調査すると、ALBのターゲットグループに登録してあるECSタスクのうち、障害が発生中のECSタスクにもリクエストが割り振られているためでした。これは考えれば分かる事だと思いますが、ではなぜ2分30秒なのかということも確認してみます。 これは、ターゲットグループのヘルスチェックで異常を検知するための設定に依存していました。店舗在庫連携では、その設定が2分30秒だったためで、2分30秒という時間の元となる設定内容は、ヘルスチェックの回数や間隔です。 カオスエンジニアリングを実施して良かったこと 今回の実験で分かったことは、AZ障害が発生してもサービス提供は停止しないが、2分30秒の間で一部エラーが発生してその後自動復旧するということでした。実験前は「マルチAZでやっているから大丈夫」という軽い認識でしたが、「2分30秒間は一部エラーが発生するものの、その後自動復旧するから大丈夫」という根拠を持った自信になりました。 また、インフラ構築の経験者や知識がある人は最初から結果の想像が付いていましたが、普段インフラを触る機会が少ない開発メンバーにとっては、新しい知識が増える機会の多い良い学びの場になりました。カオスエンジニアリングを継続することで、システムと開発者が共に強くなっていくということも実感しました。 まとめ 本記事では、弊チームのカオスエンジニアリングへの取り組みを紹介しました。カオスエンジニアリングを実施することで、システムの安全性の確認と開発者のシステムへの理解を深めることができました。カオスエンジニアリングの実施を検討している方がいれば、ぜひ参考にしてみてください。今後は洗い出した仮説をすべて検証することと、本番環境で実験していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZOMO部FBZブロックの山村です。普段は Fulfillment by ZOZO (以下、FBZ)が提供するAPIシステムを開発・運用しています。 FBZは、 AWS Lambda (以下、Lambda)を中心にAWSが提供するフルマネージドサービスを活用したサーバーレスアーキテクチャを採用しています。 以下の記事にてサービス構成やアーキテクチャ戦略の詳細を説明しています。 techblog.zozo.com 今回は、イベント駆動型のアーキテクチャを開発するうえで直面した課題と、その開発経験をもとにどのようにアプローチしたかをご紹介します。 目次 はじめに 目次 背景・課題 インフラリソース定義のための知識が必要であり、プロジェクトの参入難度を上げていた イベント駆動ではアプリケーションコードに合わせて関連リソース定義も必要になる 記述時点では正しく記述できているか気づくことができず、試行錯誤の回数が多くなっていた 構成管理ツールの見直し アプリケーション側と同様の言語で記述できる 静的型付け言語での記述 コードの再利用性 さいごに 背景・課題 FBZの新機能を開発するため、新たにデータ連携用のマイクロサービスを作ることになりました。 今回の開発では、初期ローンチ時のインフラコストをなるべく抑え、かつサービスのスケールにも対応できるよう、FBZと同じくLambdaを中心としたイベント駆動型のアーキテクチャを採用しました。 しかし、新規開発にあたっては、FBZを開発、運用する上で存在していた以下の課題と向き合う必要がありました。 インフラリソース定義のための知識が必要であり、プロジェクトの参入難度を上げていた FBZでは、構成管理ツールとして Serverless Framework を使用しています。 アプリケーションの構成やインフラリソースは、YAML形式の設定ファイルに記述しています。 コード管理できることはメリットですが、500以上のLambda関数やそれに関連するリソースの多いFBZの開発では、この設定ファイルへの記述を煩雑に感じることが多々ありました。 アプリケーション側のコードとは異なる言語での設定を必要とするため、これに慣れていない開発者にとっては新たな技術スタックに馴染むまでの一定の時間と労力が必要です。 特にリソースの定義においては、正確なパラメータや設定を把握しなければならないため、プロジェクトへの新規参入が一定の学習コストを伴っている状態でした。 イベント駆動ではアプリケーションコードに合わせて関連リソース定義も必要になる 例えば、API GatewayへのリクエストをトリガーにLambdaが起動し、DynamoDBへアクセスする処理があるとします。 下記はLambdaとDynamoDBをそれぞれ2つずつ定義していますが、同じような記述が必要になり、設定ファイル自体が肥大化していきます。 service : my-serverless-app provider : name : aws runtime : python3.11 # Lambda関数の定義 functions : writeDataToDynamoDB : handler : src/write_data_to_dynamodb.main events : - http : path : writeData method : post environment : DYNAMODB_TABLE : MyDynamoDBTable getDataFromDynamoDB : handler : src/get_data_from_dynamodb.main events : - http : path : getData method : get environment : DYNAMODB_TABLE : MyDynamoDBTableWithGSI # DynamoDBの定義 resources : Resources : MyDynamoDBTable : Type : AWS::DynamoDB::Table Properties : TableName : myDataTable AttributeDefinitions : - AttributeName : id AttributeType : N KeySchema : - AttributeName : id KeyType : HASH BillingMode : PAY_PER_REQUEST MyDynamoDBTableWithGSI : Type : AWS::DynamoDB::Table Properties : TableName : myGSITable AttributeDefinitions : - AttributeName : id AttributeType : N KeySchema : - AttributeName : id KeyType : HASH - AttributeName : id KeyType : HASH BillingMode : PAY_PER_REQUEST # セカンダリインデックスの設定 GlobalSecondaryIndexes : - IndexName : MyGlobalSecondaryIndex KeySchema : - AttributeName : name KeyType : HASH Projection : ProjectionType : ALL 上記は簡単な例ですが、実際には関連リソース間のIAMロールやポリシーを紐づける記述も必要になってきます。そして数多くのリソースが存在する場合、冗長な記述の設定ファイルであることが想像できます。 リソースや開発者が増えていくにつれ、記述の際にはリソースの命名規則にも揺らぎが生じることもありました。 記述時点では正しく記述できているか気づくことができず、試行錯誤の回数が多くなっていた また、記述の不足や間違いがあると、期待されるリソースを作成できません。記述時点では気づくことができず、デプロイ時のエラーにより発覚するケースも発生しました。 例えば、API Gatewayを使用してLambda関数をトリガーする場合、YAMLファイル内でのAPI Gatewayの設定が不十分だと、リソースは正しく構築されません。 functions : writeDataToDynamoDB : handler : src/write_data_to_dynamodb.main events : # メソッドの定義が不足している - http : path : writeData environment : DYNAMODB_TABLE : MyDynamoDBTable また、リソース間の依存関係が適切に定義されていない場合、デプロイは成功してもデプロイ後のLambda関数は期待通りに動作しません。 下記の例は、Lambda関数がDynamoDBテーブルに依存している場合、その依存関係がYAMLファイルに正しく記述されていないため、実行時にエラーとなります。 functions : writeDataToDynamoDB : handler : src/write_data_to_dynamodb.main events : - http : path : writeData method : post # 存在しないテーブルに依存している environment : DYNAMODB_TABLE : myTable このような状態に気づかないままデプロイをしてしまうと、下記のような手順が必要になります。 デプロイ時もしくはLamda関数実行時にエラー発生 切り戻しが必要な状態であれば変更前の正常な状態のコードを再デプロイ 原因調査、修正 変更後のコードを再度デプロイ リソースが増えていくとともにデプロイにも時間がかかり、原因調査なども含めると多くの時間を要してしまいます。 デプロイ過程で発生するこのような手戻りが、効率的に開発するうえでのネックになっていました。 構成管理ツールの見直し 上記の課題を受け、新規開発するマイクロサービスでは構成管理ツールの見直しを行いました。 煩雑であったYAMLへの記述を回避するため、プログラミング言語で記述できる AWS Cloud Development Kit (以下、AWS CDK)を利用することにしました。 実際にAWS CDKを利用して感じたメリットは大きく以下の3つが挙げられます。 アプリケーションで利用している言語をCDKがサポートしていれば、同様の言語で記述できる 静的型付け言語を選択することで、型安全性が高まり信頼性と効率が向上した プログラマブルに書けるため、コードの再利用でリソースを追加できるようになった アプリケーション側と同様の言語で記述できる 今回開発するマイクロサービスでは、アプリケーションの実装にGoを採用しています。 Goを使用してリソースを定義できるAWS CDKでは、通常のコードと同様にインフラストラクチャを記述でき、煩雑に感じていたYAMLファイルへの記述を回避できます。 静的型付け言語での記述 静的型付け言語であるGoで記述することで、型安全性が高まり信頼性と効率の向上を実感しました。AWS CDKのコードが正確に記述されているかをデプロイ前に確認できるため、デプロイ時に発覚する設定ミス等の問題を最小限に抑えられます。 以下は、AWS CDKをGoで記述してAPI GatewayとLambda関数をデプロイする例です。 package main import ( "github.com/aws/aws-cdk-go/awscdk" "github.com/aws/aws-cdk-go/awscdk/awsapigateway" "github.com/aws/aws-cdk-go/awscdk/awslambda" ) func main() { app := awscdk.NewApp( nil ) stack := awscdk.NewStack(app, "MyStack" , nil ) // Lambda関数 lambdaFn := awslambda.NewFunction(stack, "MyLambda" , &awslambda.FunctionProps{ Runtime: awslambda.Runtime_GO_1_X(), Handler: "main" , Code: awslambda.AssetCode_FromAsset( "path/to/your/code" ), }) // API Gateway api := awsapigateway.NewRestApi(stack, "MyApi" , &awsapigateway.RestApiProps{ RestApiName: "MyApi" , Description: "My API" , }) apiResource := api.Root().AddResource( "myresource" ) apiResource.AddMethod( "GET" , &awsapigateway.LambdaIntegrationProps{ Handler: lambdaFn, }) app.Synth( nil ) } このように、設定の記述はよりプログラマブルであることがわかります。 さらに静的型付け言語であるGoで記述することで、開発者はコンパイル時に型エラーを検出し、リソースの構築や変更において信頼性と効率を向上させることができるようになりました。 また、アプリケーション側の言語とインフラ設定の言語が統一されることで、開発時のコンテキストスイッチの切り替えが不要となり、抱えていたストレスから開放されました。 コードの再利用性 プログラマブルに記述できるため、コードの再利用でリソースを追加できるようになりました。 GoでLambda関数等のリソースを作成する際、基本となる処理をメソッド化することで、リソースの一貫性を確保できます。 package main import ( "fmt" "os" "github.com/aws/aws-cdk-go/awscdk" "github.com/aws/aws-cdk-go/awscdk/awslambda" // リソース名を定義しているパッケージ name "github.com/st-tech/example-resource-name" ) func main() { app := awscdk.NewApp( nil ) stack := awscdk.NewStack(app, "MyLambdaStack" , nil ) // Lambda関数1 // ハンドラ名はnameパッケージを参照 createLambdaFunction(stack, name.Handler1, "path/to/code1" ) // Lambda関数2 createLambdaFunction(stack, name.Handler2, "path/to/code2" ) app.Synth( nil ) } // Lambda関数を作成するメソッド func createLambdaFunction(stack awscdk.Stack, handler, codePath string ) { // リソース名の生成 resourceName := fmt.Sprintf( "%s%s" , os.Getenv( "stageName" ), handler) awslambda.NewFunction(&stack, resourceName, &awslambda.FunctionProps{ Runtime: awslambda.Runtime_GO_1_X(), Handler: handler, Code: awslambda.AssetCode_FromAsset(codePath), }) } 上記の例では、createLambdaFunctionメソッドにLambda関数の作成ロジックを切り出すことで、同じ構造のLambda関数を作成する際にコードを再利用できます。新しいLambda関数2が追加された場合も、メソッドの引数としてLambda関数の異なるパラメータを指定できるため、簡単に作成できます。 また、リソース名は明示的に指定しなければ自動で生成されますが、この例ではメソッド内でhandler引数(ハンドラの名前)を元にリソース名を生成しています。生成されたリソース名は一貫性を持ち、Lambda関数が異なるハンドラに対しても名前が適切に付与されます。このようにして、関数名とリソース名を結びつけることで一貫性を確保できます。 さらに、ハンドラ名は別のパッケージに定義したものを参照しています。アプリケーション側のコードからも同様のパッケージを参照できるので、インフラ側で定義している名前との揺らぎが無くなります。 このように、プログラミング言語で記述することの強みを活かし、より柔軟で効率的なインフラストラクチャの構築が可能になりました。 さいごに 本記事では、イベント駆動型アーキテクチャの構成管理において生じていた課題と、AWS CDKを利用したよりプログラマブルな課題解決の例を紹介しました。 構成管理ツールの利用を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、ML・データ部推薦基盤ブロックの栁澤( @i_125 )です。私はZOZOのデータ基盤におけるデータガバナンス強化を実現するために、Analytics Engineerとして複数の部門を跨ぐプロジェクトチームに参加しています。本記事ではZOZOにおけるデータガバナンス上の課題と、その課題の解決策の1つとしてdbtを導入した話をご紹介します。 目次 はじめに 目次 背景 課題 データマートの乱立 集計定義のばらつき 依存関係の洗い出しが困難 データモデリングツールの比較検討 データ変換に関する要件 データモデリングツールの選定 レイヤリングによる責務の分離 実装方針 今後の展望 dbtモデルを開発する上で工夫したこと 環境の分離 背景 工夫したこと ダミーデータセットの生成 背景 工夫したこと SQLFluffを使ったフォーマット統一 依存モデルを含むテスト dbt Docsを使ったドキュメント生成 工夫したこと 今後の展望 dbt導入に期待する効果 データマートの乱立が抑制される バラついていた集計定義の統一 影響範囲の特定がスムーズになる 後続クエリの記述量が減る 品質担保されたデータマートを参照した開発が可能 まとめ 背景 弊社のデータ基盤はGoogle CloudのBigQuery上に構築されています。データマート開発者はSQLファイルを用意しGitHubリポジトリにPull Requestを送ります。これによってCI/CDおよびCloud Composerのワークフローを通じてBigQuery上にテーブルを作成できます。このワークフローを実現しているCloud Composerの導入事例については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com このような仕組みによってデータ利活用が促進され、アドホックな分析目的だけではなく、推薦システムやMarketing Automationなど様々な後続システムで利用されるようになりました。また各部署でダッシュボードによる可視化も積極的に行われ、現在は様々なBIツールから参照されています。主に Looker 、 Looker Studio 、 Power BI を利用しており、Looker(LookML)では一部データモデリングも行われています。 課題 データ利活用が進む中で、データ基盤におけるデータガバナンス上の課題が徐々に顕在化してきました。 データマートの乱立 前述の仕組みによってテーブル単体では一定のレビュープロセスを経たものがデータ基盤上に作成される一方で、以下の理由から徐々に似たような目的のデータマートが作成されるようになりました。 後続システム利用を想定した上で、体系的にモデリングされたデータが存在していない テーブルの数が多く似たようなテーブルの存在に気づかない 可視化・分析での利用を想定したデータマートとその利用方法を示したドキュメントは存在していましたが、後続システム利用を想定したデータマートは存在していませんでした。そのため後続システムの開発者は必要なデータマートをそれぞれ作成していました。 集計定義のばらつき データマートの数に比例してクエリがDRYにならず、同じ指標のはずなのにテーブルによって集計定義が微妙に異なったり、集計定義を変更する際に変更漏れが発生したりしていました。 依存関係の洗い出しが困難 データマートの数の多さに加えて、各後続システム側で必要なワークフローを開発したことでより複雑な依存関係が生まれていました。それによってデータ連携やデータマート生成で障害が発生したり、データマート生成用のクエリを変更したりした場合に以下のような影響が発生していました。 データマートを利用している後続システムが正常に稼働しない 正しい依存関係を全て洗い出すのに多大な工数を割いている データモデリングツールの比較検討 これらの課題を解決するためには体系的なモデリングに則り品質担保されたデータの提供が必要です。この開発をスムーズに行うために、必要な要件を明らかにした上でツールを比較検討しました。 データ変換に関する要件 プロジェクトチームで議論した結果、データ変換に関する要件は以下となりました。 集計定義の集約 集計定義の変更履歴の追跡 実際に実行するクエリが分かる 依存関係や待機を考慮したワークフロー実行 ワークフローのエラー検知 ワークフローの部分的な自動リトライ 「集計定義の集約」「集計定義の変更履歴の追跡」という要件はデータモデリングを実践する上で一般的なものかと思います。一方、それ以外の要件はZOZOの事情に依るものです。 「実際に実行するクエリが分かる」については、データマートのモデリングを行うチームがクエリに書き慣れていた事もあり、実際に実行するクエリをイメージできる方がモデリングしやすかったためです。 残り3つの要件はデータマートの数の多さに関係しています。弊社のデータ基盤には20以上のソースシステムと1000以上のデータマートが存在しています。さらに後続システムでそのデータマートを参照しているので、データマート更新が遅延すると後続システムに影響します。そのため複雑な依存関係を解決しつつ効率的に更新するために、以下のような制御が必要です。 複数のソースシステムからの連携が完了次第、データマート更新を開始する データマートが依存しているソースシステムの連携が完了次第、データマート更新を開始する 失敗したデータマート更新のみ自動で指定回数リトライする 失敗したデータマートに依存しないデータマートは更新を続ける ワークフローの実行イメージは以下のようになります。 データリネージュの可視化、データ品質監視、データ仕様のカタログ化など一見重要そうな要件が入っていないのは、既に Dataplex の導入を検討していたためそちらでもカバーが可能と判断したためです。 データモデリングツールの選定 まずは集計定義の集約、集計定義の変更履歴の追跡という要件にフォーカスして以下の候補からデータモデリングツールを選定しました。 Looker(LookML) Dataform dbt Cloud dbt Core Dataformとdbtは一般的な選択肢です。また弊社ではこれまでも一部でLooker(LookML)を使ってディメンショナルモデリングを行っていたので候補に含めました。その際の導入事例については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com 要件を元に各ツールを比較すると以下のようになります。 要件 Looker(LookML) Dataform dbt Cloud dbt Core 集計定義の集約 ○ ○ ○ ○ 集計定義の変更履歴の追跡 ○ ○ ○ ○ 実際に実行するクエリが分かる △ ※ ○ ○ ○ 依存関係や待機を考慮したワークフロー実行 △ △ △ △ ワークフローのエラー検知 △ △ △ △ ワークフローの部分的な自動リトライ △ △ △ △ ※ ジョブ履歴から辿る必要がある。 既に利用していたLooker(LookML)を継続して利用することも考えられます。しかしLooker(LookML)は候補ツールの中で唯一、記法がYAMLベースとなります。そのため実際に実行するクエリが分かるかという点では他ツールに劣ります。 SQLブロック を使えば部分的にSQLベースの記法を実現できますが、ExploreやViewなどパーツ同士をJOINした結果をBigQueryに投げることもできます。そのため最終的にBigQuery上にどのようなクエリが投げられているのかは、その上層の設計に依ります。また、BigQueryのジョブ履歴を参照すれば実際に実行したクエリは分かります。しかしLooker(LookML)のUI上から簡単に遷移できなかったり、ジョブ履歴のクエリも改行等が挿入されていなかったりと人間にとって読みづらいなどの問題があります。 Looker(LookML)自体は、使いこなせばガバナンスを効かせやすいツールです。しかし今後データマートのモデリングを行うチームにとっては実際に実行するクエリをイメージできる記法が良いという意見が多く、他ツールが有力候補となりました。 問題は最後の3つの要件です。これらはどのデータモデリングツールでも 単体では 満たすことはできませんでした。そこで既に運用中のCloud Composerとの組み合わせで実現することにしました。 それではどのツールが最適でしょうか。実行制御を担うのはCloud Composer、集計定義や依存関係を管理するのはデータモデリングツールといった形で責務の分離をしたいです。しかしDataformには 集計定義と依存関係だけをファイル出力する機能が存在しません でした。またdbt Cloudはメリットとして実行環境や実行制御とセットになっている、IDEが付属している点が挙げられますが、今回はそれらを必要としなかったのと、デメリットとして利用料金がかかります。 そこでシンプルに 集計定義と依存関係をファイル出力できるdbt Core を採用しました。 レイヤリングによる責務の分離 実装方針 集計定義をDRYに集約するためレイヤリングによって責務を分離しました。メンテナンスができるだけ属人化しないように、dbt公式ドキュメントの best-practices をベースに以下の構成を取ることにしました。 Sources:ソースシステムから連携された一次テーブル Staging:退避テーブル等同じスキーマ同士のUNIONおよび重複排除を実施 Marts Core Marts:同じエンティティで、1対1または多対1の多重度のテーブル同士をJOINしたワイドテーブルを定義 Intermediate:Core MartsとDomain Martsの中間層として複数のエンティティにまたがる集計ロジックを定義 Domain Marts:各後続システムで必要なデータマートを定義 ベストプラクティスに則ると型変換やTimeZoneの統一などシンプルな変換処理はStagingで行うべきです。しかし弊社のデータ基盤ではStagingより前段のBigQuery格納時にこれらの処理を行っているため、Stagingでの実装は不要でした。また、今回はまず必要と思われる最低限のレイヤーを実装し、必要に応じて後からレイヤーを追加する等のリファクタリングをすることにしました。 今後の展望 データマートの数が非常に多いため、レイヤーによってはディメンショナルモデルを採用した方がよいかもしれません。またMartsの下層に位置する セマンティックレイヤー を導入するべきか、どのツールでどのように管理するのかについても決めていく必要があります。このあたりは今後の整理や運用する中で検討すべき事項としています。 dbtモデルを開発する上で工夫したこと dbtモデルの開発効率化や品質担保のために以下の工夫をしました。 環境の分離 背景 Cloud Composerを使ったワークフローを開発するため、弊社には元々Dev/Stg/Prdの3環境が存在し、それぞれGoogle Cloudプロジェクトを分けていました。またdbtのモデル開発者とCloud Composerのワークフロー開発者は異なるロールなので、それぞれの環境やGitHubリポジトリを分けることで責務を明確にしたいと考えました。 工夫したこと 環境の分離については以下の方針としました。 dbt開発者が自由にデプロイできるSandbox環境として、Google Cloudプロジェクトを新規作成 dbt開発用のGitHubリポジトリを新規作成 dbt開発用のリポジトリでPull Requestをmain branchにマージすると、Cloud Composer開発用のリポジトリに対して自動でPull Requestが作成されます。Cloud Composer開発者は作成されたPull Requestをmain branchにマージすることで、dbtで定義した集計ロジックや依存関係をCloud Composerに適用します。 ダミーデータセットの生成 背景 dbtモデルを dbt run で実体化したり、 dbt test でテストしたりする際に、Sources層として参照元のデータセットやテーブルが必要です。dbtの Source定義 では本番環境のプロジェクトを直接参照できます。しかし弊社のデータ基盤ではガイドライン上、本番環境のデータを直接参照できません。そこで開発環境(Sandbox環境とDev環境)内に何らかの方法でデータセットやテーブルを生成する必要があります。 さらに弊社のデータ基盤の一部のテーブルでは会員の住所やメールアドレスといった秘密情報にあたるカラムに ポリシータグ を付与した上で保有しており、これをダミーデータに置き換える必要があります。ポリシータグの導入事例については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com 工夫したこと 開発環境(Sandbox環境とDev環境)内にビューを生成することで本番環境を間接的に参照するようにしました。その際にポリシータグ付きカラムはダミーデータに置換しました。また環境内で開発者同士の競合が起きないように、GitHubのbranchごとにデータセット単位でダミーデータを生成する dbt macros を用意しました。 このdbt macrosはGitHub上でbranchを新規作成後、GitHub Actionsの workflow_dispatch イベントで手動実行します。BigQueryではビューの生成にはコストがかかりませんが、ダミーデータ生成が不要な対応の場合はCIの実行リソースが勿体無いので自動実行は避けました。 開発環境(Sandbox環境とDev環境)では参照するデータセットとデプロイ先となるデータセットをbranchごと・レイヤーごとに分岐させる必要があります。具体的には以下のように実装しました。 dbt_project.yml ファイルでbranch名をプロジェクト変数として宣言します。 vars : branch_name : "development" 参照するデータセットは sources ファイルで Jinja を用いて動的に定義しました。 version : 2 sources : - name : source_name database : | { %- if target.name == 'sandbox' -% } sandbox-project-name { %- elif target.name == 'dev' -% } dev-project-name { %- elif target.name == 'stg' -% } stg-project-name { %- elif target.name == 'prd' -% } prd-project-name { %- endif -% } schema : | { %- if target.name == 'sandbox' or target.name == 'dev' -% } dataset_name_{{ var('branch_name') }} { %- else -% } dataset_name { %- endif -% } デプロイ先となるデータセットはdbt公式ドキュメントの Advanced custom schema configuration を参考にハンドリングを実装しました。dbtで提供されているdbt macros generate_schema_name をオーバーライドしています。 {% macro generate_schema_name(custom_schema_name, node) -%} {%- set default_schema = target.schema -%} {%- if custom_schema_name is none -%} {{ default_schema }} {%- else -%} {%- if target.name == ' sandbox ' or target.name == ' dev ' -%} {{ default_schema }} _ {{ custom_schema_name | trim }} _ {{ var( " branch_name " ) | trim }} {%- else -%} {{ default_schema }} _ {{ custom_schema_name | trim }} {%- endif -%} {%- endif -%} {%- endmacro %} さらにデプロイ先となるデータセットにレイヤー名を付与するため、 dbt_project.yml ファイルでディレクトリ構造に従ってカスタムスキーマを定義しました。この値は前述のdbt macros generate_schema_name に対して custom_schema_name として渡されます。 models : dbt_project_name : staging : +schema : stg marts : core : +schema : core domain : intermediate : +schema : int dbtコマンドを実行する時は以下のように引数としてbranch名を渡します。 dbt run --vars " {'branch_name': 'issue_95'} " --target sandbox これによってbranchごと・レイヤーごとにデータセットが生成されます。 SQLFluffを使ったフォーマット統一 可読性向上のために SQLFluff を使ってフォーマットを統一するようにしました。main branchに対するPull Requestを作成すると、CIでSQLFluffを実行し、フォーマットに問題がある場合はエラーとなります。ちなみにSQLFluffを選択した理由はルールの細かなカスタマイズが可能なのと、もし将来的にdbt Cloudと併用する場合も同じルールの適用が可能なためです。 依存モデルを含むテスト データマートを変更する際には、依存関係のあるダウンストリームのデータマートのテストも実行するようにしました。main branchに対するPull Request作成により、CIにて dbt test を実行します。Warningレベルのログを含めたテスト結果がPull Request内にコメントで通知されます。 dbt Docsを使ったドキュメント生成 工夫したこと 各dbtモデルにはYAMLファイルにテーブルレベル、カラムレベルのdescriptionを記述できます。さらに dbt docs コマンドによって、dbtモデルのdescriptionだけでなく、依存関係も可視化された ドキュメントサイト を自動生成できます。 今回Core Marts層およびIntermediate層の実装にあたって、バラついていた集計定義を統一するため利用者にヒアリングの上、調整をしたところもありました。そこで利用者が簡単に集計定義を確認できる様に以下の工夫をしました。 persist_docs オプションを有効化することで、YAMLファイルで定義した内容をBigQueryのdescrptionにも反映 main branchに対するPull Requestをマージすると、GitHub Pagesでホスティングしているdbt Docsサイトを更新 今後の展望 集計定義そのものはdescriptionに記述できても、その経緯や背景の詳細までは記述できません。現在そういった情報は社内ドキュメント管理ツールで管理しています。今後はdbtのドキュメント生成機能を拡張していくのか、それとも全く別のデータカタログツールを導入するのか、検討していきたいと考えています。 dbt導入に期待する効果 dbt導入はこれから実運用に入る段階のため期待する効果を以下に示します。 データマートの乱立が抑制される 後続システムでの利用も想定した上で体系的にモデリングしたデータと、dbt Docsによるドキュメントを提供することで、データマートの乱立が抑制されることを期待しています。ただし、もし今後データマートの整理を進める中でデータマート数をさらに抑制した方がよい場合は、レイヤーの追加や異なるモデリング手法などを検討します。 バラついていた集計定義の統一 Core Marts層またはIntermediate層で重要な指標や共通した指標を定義することで、下層ではそれらを参照すればよいため、集計定義を統一しやすい環境が整いました。今後は定期的に集計定義の見直しを行い、必要に応じてCore Marts層やIntermediate層を拡充していく予定です。 影響範囲の特定がスムーズになる dbt Docsによって依存関係が可視化されるため、どのデータマートがどのデータマートを参照しているのかが明らかになります。これによって、データ連携やデータマート生成で障害が発生した場合やクエリを変更した場合の影響範囲の特定がスムーズになることを期待しています。 後続クエリの記述量が減る これまで各データマートで似たようなJOINを繰り返し記述していましたが、Core Marts層またはIntermediate層を参照することで後続クエリの記述量が減ります。また後続クエリ側はJOIN数が減るのでクエリパフォーマンスの改善、ひいては実行時間や消費スロット削減等のコストメリットも見込めます。 品質担保されたデータマートを参照した開発が可能 各dbtモデルのテスト実装およびCI上での自動テストによって、後続のデータマート開発者は品質が担保されたデータマートを参照して開発できるようになります。 まとめ 本記事ではZOZOのデータ基盤でdbtを導入した話を紹介しました。ただdbtを導入しただけで全てが解決するわけではありませんが、dbtはサードパーティツールが充実していることもあり、ベストプラクティスを適用しやすくなったと感じます。既にデータ基盤を運用している組織でdbt導入を検討している方の手助けになれば幸いです。今後は社内のさまざまなプロダクトに対して品質が担保されたデータを提供できるよう、Marts層の拡充やモニタリングをはじめとする運用の最適化・自動化を進めていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。検索基盤部の山﨑です。検索基盤部では、ZOZOTOWNの検索機能の改善を目的とした施策の有効性をA/Bテストで検証しています。 A/Bテストは、新たな施策の有効性を評価する手法として信頼性の高い手法ではあるものの、下記のような制約があります。 統計的に有意な差が出るためには、多くのユーザーからのフィードバックが必要である 比較手法を実際のユーザーに提示するため、ユーザー体験に悪影響を与えるリスクがある これらの制約から、実験したい全ての施策をA/Bテストで検証することは困難なため、事前に有効な可能性が高い施策に絞った上でA/Bテストに臨むことが大切です。 事前に有効な可能性が高いことを示すためには、オフラインでの評価結果を活用します。しかし、オフライン評価とA/Bテストの結果は必ずしも一致しないことが知られており、ZOZOTOWNにおいても同様の問題が発生しています。 このような課題に対しては、反実仮想機械学習の分野で研究されているOff-Policy Evaluation(OPE: オフ方策評価)が有効であると考えられます。OPEについての詳細は、下記の記事を参照してください。 techblog.zozo.com 本記事ではOPEの詳細を説明しませんが、OPEの考え方を踏まえながら、オフライン評価とA/Bテストの結果が一致しない問題に対しての実践的な対応策について紹介します。 まず、ZOZOTOWN検索においてオフライン評価とA/Bテストの結果が一致しない問題と、その実践的な対応策を解説します。そしてデータの選択バイアスを中心に、この問題の解決策をいくつか紹介します。 目次 目次 オフライン評価とA/Bテストの結果が一致しない問題 オフライン評価でのnDCGの弱点とそれを補う評価指標 ヘルスチェックとしてのオフライン評価指標の活用 データの選択バイアスを考慮したオフライン評価手法 一般的な情報検索の評価指標 選択バイアスを考慮した情報検索の評価手法 インターリービングを活用した情報検索の評価手法 まとめ おわりに 参考文献 オフライン評価とA/Bテストの結果が一致しない問題 オフライン評価結果とKPI(ここでは、A/Bテストの結果とKPIを同義に扱います)が一致しない問題は、多くの研究で報告されています。 例えば、 Bernardi et al, KDD 2019 1 ではオフライン評価の改善率とA/Bテストでのコンバージョン率の改善率を比較したところ、それらの結果が相関しないことを報告しています。 相関しない理由として、KPIの代理変数を過剰最適してしまうことやA/Bテストという短期間でビジネスインパクトに影響のある変更の検知が難しいことなど、幾つかの要因が挙げられています。 同様にZOZOTOWNにおける検索改善の施策でのランキングベースのオフライン評価とA/Bテストを比較した結果を下図に示します。x軸がオフライン実験での購入ベースのnDCG(nDCGの詳細は次章の説明を参照してください)の改善率、y軸がA/Bテストでの商品購入率の改善率です。 この図の示す通り、1つのオフライン評価とA/Bテスト結果が必ずしも相関しない問題はZOZOTOWNの検索改善においても発生しています。 この問題を解決するための研究は今も盛んに研究されていますが、ユーザーの行動ログの様々なバイアスの問題ゆえに、根本解決が難しい問題です。そのため、まずはオフライン評価とA/Bテストの結果は一致しないという前提で、それぞれの評価手法の特性を活用する方針で進めました。 オフライン評価でのnDCGの弱点とそれを補う評価指標 nDCGは情報検索の評価指標として広く使われている指標ですが、過去に一度でもクリック/購入されている商品や検索クエリしか評価できないという弱点があります。この問題は D. Turnbull, BSM 2023 2 でも取り上げられています。 実際にZOZOTOWNでも数%程度の検索クエリにしか購入のイベントが発生していないため、前章で紹介した「購入ベースのnDCG」では、この数%の検索クエリしか評価できていないことが分かりました。 この弱点を補うためには、購入イベントが発生していない検索クエリに対しても評価できるような評価指標が必要です。 我々はこの評価指標として、nDCGのような順序を評価するランクベースの指標の他に、検索結果を集合として扱う集合ベースの評価指標を採用しました。詳細は ZOZOTOWN検索の精度評価への取り組み の記事で紹介していますが、概括として下記2つの指標を使用します。 購入数カバー率 : 購入イベントが発生している商品をどれだけカバーできているか(通常の再現率を購入数に重み付けした指標) 商品一覧の類似度 : 旧ロジックと新ロジックが表示する商品の一覧がどれだけ類似しているか 購入数カバー率が高く商品一覧の類似度が低い場合、新ロジックが旧ロジックではカバーできていない、かつ購入確率の高い商品をカバーできる可能性が高いことを示しています。 商品の絞り込みロジックを改善した場合に、これらの指標を活用することで有効な可能性が高い施策を絞れます。 ヘルスチェックとしてのオフライン評価指標の活用 前節で紹介した各オフライン評価指標の考え方を整理すると、下記のようになります。 評価指標 悪化した場合 nDCG 機械学習モデルが適切に学習できていない可能性がある 購入数カバー率 検索結果が悪化する可能性がある 商品一覧の類似度(類似していないほど良い) 検索結果がほぼ同じ結果となる可能性がある ZOZOTOWNの検索改善では、これらの評価結果をA/Bテストに進むかの判断軸として活用しています。 ただし、オフライン評価はあくまでヘルスチェックとして活用し、実際のビジネスインパクトはA/Bテストで検証する方針を採用しています。 Bernardi et al, KDD 2019 や D. Turnbull, BSM 2023 と同様の考え方を採用しています。 つまり、これらの指標はあくまで大幅な悪化が無いことを確認することで、想定外の施策内容をA/Bテストに進めることを防ぐことを目的としています。 本章では、オフライン評価とA/Bテストの結果が必ずしも一致しない問題に対しての実践的な対応策について紹介しました。次章では、結果の乖離が発生するのかを分析する目的で、特にデータの選択バイアスに着目した評価手法と実験結果について紹介します。 データの選択バイアスを考慮したオフライン評価手法 ランキング学習の機械学習モデルの学習方法は、人手でラベルづけしたデータを用いる方法と、ユーザーの行動ログを用いる方法があります。 人手でラベルづけしたデータを用いて学習する場合、データの選択バイアスを考慮する必要がありませんが、データの収集にコストがかかります。 ユーザーの行動ログを使用することでデータ収集コストを下げられますが、そのデータは観測・未観測の傾向が異なる、いわゆるMNAR(Missing Not At Random)と呼ばれるデータです。 MNARなデータでは、例えばポジションバイアスと呼ばれる、検索結果の上位に表示された商品の方が観測されやすいという選択バイアスが存在します。 本章では、このようなユーザーの行動ログに含まれるデータの選択バイアスを考慮したオフライン評価手法について紹介します。 一般的な情報検索の評価指標 まず、情報検索の評価指標で広く使われているnDCG(normalized Discounted Cumulative Gain)と MRR(Mean Reciprocal Rank)について説明します。 検索クエリ に対して商品の一覧が返されるとき、 を に対する 番目の商品の関連性スコアと定義します。 関連性スコアは任意に定義でき、例えばユーザーがその商品を購入したら1で、クリックしなかったら0などを指定できます。 このとき、 に対する 番目までの検索結果を評価するための指標としてDCG (Discounted Cumulative Gain) を求めます。DCGは C. Burges et al, ICML 2005 3 の定義に従うと、以下のように定義されます。 さらに、関連性スコアが高い商品から順番に並べた理想的なランキングにおけるDCGをmax DCGとします。このとき、nDCGは全てのクエリ におけるmax DCGで正規化済みのDCGを合算することで、下記のように定義されます。 次に、検索クエリ に対して、最初に関連性スコアが1となる商品のランクを とすると、MRRは以下のように定義されます。 MRRのような逆数の平均値を計算する手法は、順序尺度の平均値を計算しているため、情報検索の評価指標として適切な結果とならないケースがあることに注意してください。 直感的な理由として、ランキングの順位が1番目と2番目の差は である一方で、2番目と3番目の差は となり、MRRは上位のランクを過剰に評価してしまうためです。 平均値を計算するためには、順序尺度ではなく間隔尺度などの数量データを使用する必要があります。 例えば、 M. Ferrante et al, IEEE 2021 4 では、MRRを適切な間隔尺度にマッピングした上で評価した場合と比較して、30%以上の結果が変わることを報告しています。nDCGも同様の問題があるもののMRRほど深刻ではないことが報告されています。 本記事ではこの問題についての詳細を割愛しますが、興味のある方は上記の論文や N. Fuhr, SIGIR 2020 5 を参照してください。 選択バイアスを考慮した情報検索の評価手法 近年、前節で紹介したnDCGやMRRに対して選択バイアスを考慮するように拡張した評価手法が提案されています。 X. Wang et al, SIGIR 2016 6 では、選択バイアスを考慮したMRRの拡張手法を提案しています。また、 L. Yan et al, SIGIR 2022 7 や Y. Zhang, KDD 2023 8 では、選択バイアスを考慮したnDCGの拡張手法を提案しています。 どちらも、Inverse Propensity Score(IPS: 逆傾向スコア)と呼ばれる推定量を用いて、バイアスを軽減した評価指標を定義しています。IPSは傾向スコア(Propensity Score)と呼ばれる推定量を活用します。 検索の文脈において傾向スコアは、例えば、ポジションごとの商品の見られ易さと定義できます。この傾向スコアの逆数を使用することで、ポジションによる見られ易さ、すなわちポジションバイアスを軽減できます。 ここで、 を の観測頻度、傾向スコア を が実際に表示されたときのクリック率と定義します。つまり、 のポジションが上位なほど、高い値を取る傾向があります。 傾向スコアを用いると、IPS推定量は と表せます。 このIPS推定量 を用いて、選択バイアスを考慮したIPSMRRは以下のように拡張されます。 また、ポジション ごとのIPS推定量を と定義すると、選択バイアスを考慮したIPSDCGは以下のように拡張されます。 IPSDCGをnDCGのDCGに適用することで、選択バイアスを考慮したIPSnDCGを定義できます。 実際に過去にZOZOTOWN上でA/Bテストを行ったモデルの評価をIPSMRRで再評価しました。特にポジションバイアスを軽減したモデルとそうでないモデルの比較に対して、通常のnDCGよりもA/Bテストの結果に相関する傾向を観測できました。 インターリービングを活用した情報検索の評価手法 最後に、オフライン評価とA/Bテストの結果が一致しない問題に対して、インターリービングを活用している事例を紹介します。 インターリービングは、2つのランキング結果を混ぜ合わせて1つのランキング結果を生成する手法です。インターリービングについての詳細は Effective Online Evaluation for Web Search 9 で詳しく解説されています。 F. Radlinski and N. Craswell, SIGIR 2010 10 では、nDCGとインターリービングでのモデルの良し悪しの傾向が強く相関することを示しています。 また、 Wang et al, SIGIR 2023 11 でも、nDCGとインターリービングの結果がほぼ一致していることを示しています。 このことから、インターリービングを活用することで、オフライン評価とA/Bテストの結果が一致しない問題に対して、より信頼性の高い評価手法を構築できる可能性があります。 まとめ 本記事では、オフライン評価とA/Bテストの結果が一致しない問題に対しての実践的な対応策と、特にデータの選択バイアスを考慮することでこの問題を軽減する方法について紹介しました。 データの選択バイアス削減や反実仮想な機械学習を導入することで、より性能の高いモデルや事前のオフライン検証を進めることを期待できます。 おわりに ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。今回紹介した検索技術に興味ある方はもちろん、幅広い分野で一緒に研究や開発を進めていけるメンバーも募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com 参考文献 Bernardi, L., Mavridis, T., & Estevez, P. (2019, July). 150 successful machine learning models: 6 lessons learned at booking. com. In Proceedings of the 25th ACM SIGKDD international conference on knowledge discovery & data mining (pp. 1743-1751). ↩ Doug Turnbull. (2023, October). NDCG IS OVERRATED. Berlin Search Meetup. ↩ Burges, C., Shaked, T., Renshaw, E., Lazier, A., Deeds, M., Hamilton, N., & Hullender, G. (2005, August). Learning to rank using gradient descent. In Proceedings of the 22nd international conference on Machine learning (pp. 89-96). ↩ Ferrante, M., Ferro, N., & Fuhr, N. (2021). Towards meaningful statements in IR evaluation: Mapping evaluation measures to interval scales. IEEE Access, 9, 136182-136216. ↩ Breuer, T., Ferro, N., Fuhr, N., Maistro, M., Sakai, T., Schaer, P., & Soboroff, I. (2020, July). How to measure the reproducibility of system-oriented IR experiments. In Proceedings of the 43rd International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 349-358). ↩ Wang, X., Bendersky, M., Metzler, D., & Najork, M. (2016, July). Learning to rank with selection bias in personal search. In Proceedings of the 39th International ACM SIGIR conference on Research and Development in Information Retrieval (pp. 115-124). ↩ Yan, L., Qin, Z., Zhuang, H., Wang, X., Bendersky, M., & Najork, M. (2022, July). Revisiting two-tower models for unbiased learning to rank. In Proceedings of the 45th International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 2410-2414). ↩ Zhang, Y., Yan, L., Qin, Z., Zhuang, H., Shen, J., Wang, X., ... & Najork, M. (2023, August). Towards Disentangling Relevance and Bias in Unbiased Learning to Rank. In Proceedings of the 29th ACM SIGKDD Conference on Knowledge Discovery and Data Mining (pp. 5618-5627). ↩ Drutsa, A., Gusev, G., Kharitonov, E., Kulemyakin, D., Serdyukov, P., & Yashkov, I. (2019, July). Effective online evaluation for web search. In Proceedings of the 42nd International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 1399-1400). ↩ Radlinski, F., & Craswell, N. (2010, July). Comparing the sensitivity of information retrieval metrics. In Proceedings of the 33rd international ACM SIGIR conference on Research and development in information retrieval (pp. 667-674). ↩ Xiaojie Wang, Ruoyuan Gao, Anoop Jain, Graham Edge, and Sachin Ahuja. 2023. How well do offline metrics predict online performance of product ranking models? In Proceedings of the 46th International ACM SIGIR conference on Research and Development in Information Retrieval. ↩
はじめに こんにちは、ZOZOMO部OMOバックエンドブロックの中島です。普段は ZOZOMO店舗在庫取り置き というサービスの開発を担当しています。 2024年1月14日から16日の3日間にかけてニューヨークで開催された「NRF 2024: Retail's Big Show」に初めて現地参加してきました。 前半はNRF Retail's Big Showの概要と関連する情報、後半はFashion TechやRetail Techを中心にお伝えします。NRF 2024全体の概要については、 NRF 2024 Event Recap などをご覧ください。 目次 はじめに 目次 NRF Retail's Big Showとは 会場の概要 セッション Expo Fashion Tech サイズ計測 ARミラー ロレアル社のパーソナライズリップスティック Retail Tech スマートカート RFID AmazonのJust Walk OutとAmazon One おわりに NRF Retail's Big Showとは NRF Retail's Big Show は、毎年1月頃にNRF(National Retail Federation)が主催する世界最大級の小売産業向け展示会です。 2024年1月14日から16日までニューヨークで開催され、190以上のセッションが行われ、1,000以上の企業が出展しました。 NRF Retail's Big Showは毎年テーマが掲げられており、NRF 2024では「Make it Matter」というテーマでした。 NRF 2024のサイト上では、おすすめホテルの紹介もされます。初参加で土地勘もなかったので、会場近くのホテル情報を得られて助かりました(ここから直接予約もできます)。 カンファレンスが近づくと、NRF 2024のアプリも公開されました。アプリを使うと、見たいセッションのチェックができたので、カンファレンス中のスケジュールを管理するのに便利でした。 NRF 2024アプリ 会場の概要 NRF 2024は Jacob K. Javits Convention Center で開催されました。 Jacob K. Javits Convention Centerの外観 2階がメインエントランスになっており、カンファレンスパス購入時に発行されるQRコードを使って受付をします。 NRF 2024に参加するためのカンファレンスパスは、All-Access PassとExpo Passの2種類があります。Expo PassだとExpo会場のみ参加可能で、Keynoteセッションなどが開催される会場に入るには、All-Access Passが必要になります。 All-Access Passは8月頃から販売がはじまり、早ければ早いほど安く購入できます。来年参加する予定の方は、夏くらいから動き始めることをおすすめします。 受付の近くには、Amazon Goの店舗もありました。クレジットカードがあれば利用できる仕組みだったので、実際に商品を手に取り購入してみました。ゲートを通る前に、レシート送付先のメールアドレスを登録すると、ゲート通過後にメールで明細の確認もできます。ゲートを通るだけで商品購入ができるという体験は少し不思議な感じでした。 Amazon Goの店舗 早朝はDonuts Dunkというものがあり、ドーナツなど軽食を食べながらネットワーキングできる時間が設けられています。 Donuts Dunk、ドーナツ以外にヨーグルトやコーヒーもありました 世界的なイベントということもあり、日本企業の方も多く参加されていたので、そこでコミュニケーションを取ることができました(アメリカで名刺交換するとは思いませんでしたが)。 セッション Javits Center内の4階5階が主なセッション会場になっていました。その中で最も大きなステージが、Keynoteセッションが行われるSAP Theatreステージになります(ステージごとにスポンサーの名前がついていました)。 Keynoteセッションが行われるSAP Theatre オープニングセッションでは、NRFのCEOとLevi'sのPresidentのセッションでした。Levi'sが2023年に実施した取り組みの紹介などを対談形式で進行していました。 NRFの各種セッションは、基本的に複数人での対談形式で進行します。技術系のカンファレンスだとプレゼン主体で技術内容の説明をする形式が多いので、このようなセッションは個人的に新鮮でした。 いくつかセッションを聞いたり、Expoをまわったりする中で、やはりAIに関連する話題が非常に多かったです。その中でもWalmart CEOとSalesforce CEOが対談するKeynoteの中で、GUCCIがカスタマーサポートに生成AIを導入したことで一夜にして収益が30%増加した話は象徴的でした。 Expo Javits Center内の1階3階が主なExpo会場になっていました。 主に大手企業が展示されている3階のExpo会場 Expo会場内は、各企業のブースがありますが、その中で、4つ特徴的なエリアがあります。 今回、Fashion TechやRetail Techで先進的な取り組みの内容を知りたかったので、ExpoではInnovation LabとStartup Hubを中心に回りました。 ZOZOTOWNは食品の取り扱いがないため、Foodservice Innovation Zoneは軽く回った程度ですが、ドライブスルーのエリアがあったところにアフターコロナを感じました。 また、Exhibitor Big IdeasはExpo Passでも参加できるExpo出展企業のセッション会場です。Exhibitor Big Ideasは NRF 2024のAgenda からセッション動画にアクセスできるので、気になる人はそちらから確認してください。 Fashion Tech Fashion Tech関連の展示では、サイズ計測のサービスやAR関連のサービスを見ることができました。 サイズ計測 3D Foot Scanの機器(左がAetrex社、右がVolumental社) Aetrex社の機器でScanした結果 足を3Dスキャンすることで、サイズがフィットするかを判別するソリューションがいくつかありました。ただ、その計測機器が高価だったので、一般化していくにはまだ時間がかかりそうな印象を受けました。 ARミラー ZERO10社のARミラー AR関連でバーチャル試着系のサービスもありました。実際に試して見ましたが、人の周りにエフェクトがかかったり、雨が降ったりなど、エンタメ要素が強いものでした。そのため、いろいろな服をデジタルで試着するサービスというよりは、人の足を止める集客ツールという印象でした。試着用の洋服データを準備することも必要になるので、精度の高いバーチャル試着の仕組みを作るのはまだ難しいのかもしれません。 ロレアル社のパーソナライズリップスティック AR関連では、ロレアル社の展示が目を惹きました。 ロレアル社のパーソナライズリップスティック 左側のアプリで口紅の色を決め、右側のデバイスにその色の配合になる口紅が出てきます。決まった口紅をお店で買うのではなく、その日にあった口紅を作るという発想に驚きました。また、このプロダクトはOEMなどではなく、ロレアル社の自社開発プロダクトという点も驚きでした。 サービスの詳細は以下の動画を見てもらうのがわかりやすいと思います。 www.youtube.com このとき初めて知りましたがすでに販売開始しており、 2022年に日本上陸もしています 。国内では 表参道フラッグシップ ブティック で体験できるので、興味のある方は確認してみてください。 Retail Tech Retail Techでは、スマートカートとRFIDに関する展示が多かったように感じました。 スマートカート 展示されていたスマートカートの例 展示されていたスマートカートは、カートとPOSレジが合体したようなものを多く見ました。カートで商品の場所を教えてくれたり、カートに商品をいれると商品判別と価格の自動計算をしてくれたりします。日本でもスーパーなどで、スマホでバーコードスキャンするようなものがありますが、それがショッピングカートと合体してより高精度になったイメージです。数年後は日本でもこのようなカートが当たり前にある世界が来るのかもしれません。 Amazon Dash Cart、レコメンドを表示したり、商品を自動判別したりする ECでの買い物の場合、カートに商品を入れたりすると、おすすめ商品のようにパーソナライズされた情報提供があります。今回展示されていたようなショッピングカートが一般化すれば、リアルの世界でも、同じようにパーソナライズされた情報提供ができるようになっていくと思います。 RFID RFIDは、国内アパレルだと棚卸しなどで使われるイメージでしたが、生鮮食品の生産から店頭に並ぶまで、すべてトラッキングしてデータ化されているものを見ることができました。どこでいつ作られたものかだけではなく、どこからどこに移動したかがわかるため、CO2排出量の管理ができるようになっていました。サステナビリティに対する取り組みにはこういう技術的なアプローチがあることを知ることができました。 RFIDリーダーで商品を特定する様子 RFIDタグが付いた商品のトラッキングができる アパレル関連で、RFID関連を使ったテクノロジーでNexite社のサービスも目に留まりました。商品の位置情報を把握して、商品の移動情報がわかるものでした。例えば商品が何回試着室に持っていかれたかがわかるので、商品の購買前のデータで分析が可能になります。 Nexite社のブース AmazonのJust Walk OutとAmazon One 実店舗での売上の最大化をしたいと考えたときに、どうしてもレジがボトルネックになります。レジの効率化のために、最近だとセルフレジやスマホをレジ代わりにするサービスがよく使われるようになってきています。そんな中で、AWSのブースにあったJust Walk OutとAmazon Oneの組み合わせは新しい解決策に見えました。 AmazonのJust Walk OutとAmazon One 簡単に説明すると、以下のようなもので、すでにアメリカのスタジアムなどで導入されているそうです。 商品判別はRFIDで実施 = レジ打ちが不要 手のひら認証で決済 = 財布・スマホが不要 最近だと、財布がなくてもスマホがあれば買い物に困らないことが増えましたが、スマホすらなくても商品購入できる世界を作ろうとしているのには驚きました。日本でもどこかで使えるようになるのを楽しみにしています。 また、AWSのブースでは、日本語でアテンドをしていただけたことで、Expoをより楽しむことができました。この場を借りて御礼申し上げます。 おわりに ホテルから歩いていけたタイムズスクエア OMO関連の新サービスが出てきているか気になっていましたが、まったく新しいサービスというものはなかったように思います。ただ、日本との違いとして、すでにBOPISやBORISなどは普通にできる環境が整っていて、その先でよりオンラインとオフラインをシームレスにつなげることを考えているように感じました。 今回のNRF視察は開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 NRF 2024は技術カンファレンスではないですが、小売の中でテクノロジーが介在しない事はありえないので、技術者がこのようなイベントに参加することで得られるものも確実にあります。また、Expoを回る中で、ZOZOFITやZOZO CHAMPIONSHIPでZOZOを知っている企業がいくつかあったのは嬉しかったです。 NRF Retail's Big Showはこれまでニューヨークでのみ開催されていましたが、 2024年6月にはじめてシンガポールで開催されます 。アジア系の企業が多く出店されるようですので、日本からだとこちらのほうが参加しやすいかもしれません。 ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 最後までご覧いただきありがとうございました!
はじめに こんにちは、計測プロデュース部の歌代です。 私たちはZOZOFITやZOZOMATといった計測系プロダクトの開発PM、データ収集、精度検証などサービス構築から、UI/UXの分析・評価など幅広く業務を行っております。 今回は私たちのチームが抱えていた課題と、対応策として行った工夫についてご紹介します。 目次 はじめに 目次 直面した課題 目的の重要性 概要とデータを分ける 概要をまとめた効果 文書フォーマットの効果 最後に 直面した課題 私たちのチームが直面した課題は「QAスタッフの入れ替えが発生する度に、引き継ぎがうまくできず、検証のクオリティが下がる」というものでした。私たちの業務は、計測に関する独自のノウハウや技術が多くあります。また過去に実施した検証の内容を定期的に振り返り、過去の検証手法や検証結果を参考にして、現在の検証を決定する場面が多く発生しています。 スプレッドシートでまとめたメモには検証結果や分析データが精緻に記載されており、検証当時の環境や条件・結果を確認できるようになっていました。 その後、新しいスタッフを迎え、プロジェクトを振り返るタイミングで今回の課題が発覚しました。スプレッドシートを開き、当時の検証の内容を説明しようとすると難しい点に気がつきます。一つ一つの検証データは詳細に記載されていますが、検証の目的や概要などの記載はなく、日付とデータを見て、当時の状況を思い出しながら説明することになりました。結果として、説明をする側、説明を受ける側双方にとって、多くの時間を費やすことになりました。 目的の重要性 後任のスタッフに過去の検証実績をスムーズに引き継ぎできないのはなぜでしょうか。 当時の細かなデータは確かにスプレッドシートに記載されており、結果についてもしっかりまとめられており、検証結果を示すものとしては十分な資料になっていました。 実際、自分で資料を見直してみたところ、あることに気がつきました。それはそれぞれの検証の目的が具体的に記載されていないことでした。 それぞれの検証で「何のために」「何を調べたかったのか」などの情報が記載されていませんでした。その結果、細かなデータの記載はあるものの、そもそもこの検証を行った背景を理解しないまま、検証結果だけを共有されていました。後任のスタッフはゴールが見えないまま、データだけを読み解いていきながら、検証目的をぼんやりと理解しているのではないかと仮説を立てました。 概要とデータを分ける そこで私たちは、検証の概要はドキュメントWebサービス(Confluence)、データはスプレッドシートで運用することにしました。 改善前までは検証に関する概要などのメモをフリーフォーマットでスプレッドシート上にまとめていました。しかしその状態では、ドキュメントごとに記載されている項目にばらつきがあり、必要な情報を探すのにも時間がかかり、ストレスがかかるものでした。 検証の「概要」と「データ」をそれぞれ得意とするドキュメントにまとめる運用を始めるにあたって、私たちは以下のポイントを重視しました。 概要はできるだけシンプルなフォーマットにすること 概要はできるだけ情報量を少なくし、初めて読む人がうんざりしない量に留めること データは今まで通り、細かなデータを記載すること 基本的に過去の検証結果を振り返る際、まず概要の情報だけで「当時の検証が何を目的としており、結果どうだったのかサマリーで理解できること」を「概要」の役割としました。そして「概要」を読んで、もう少し詳しく知りたいと思う人が、さらに精緻な情報として「データ」を閲覧します。これにより、検証の目的を念頭においた状態で、より詳しく検証の理解を深めていくという運用を想定しました。 概要をまとめた効果 検証概要を情報共有ドキュメントWebサービスにまとめるにあたって、以下の項目に絞り込んで記載することにしました。 目的(検証の背景やゴール) 概要(具体的な環境・端末の情報) 結果(検証結果のサマリー:結論から書く) 課題(発覚した課題や残課題) 関連リンク(関連する過去検証の情報) 「後任スタッフへの引き継ぎがうまくいかなかった」私たちは特に「検証目的を明示すること」を重視しました。 目的を資料に書き起こすメリットは大きく3つあります。 資料を作成する人の頭を整理できる 資料を読む人の情報量を処理する負担が減る 明文化することで、目的を客観視・共有できる 特に「目的を客観視・共有できる」ことは、私たちのチームではとても効果的でした。「過去検証の振り返り」の対応策として実施した運用でしたが、現在進行形で検証を行う時にも非常に効果的でした。作業を依頼する人、作業を引き受ける人が口頭のみで共有するよりも、まず資料に文章で目的を明文化します。その後、文書と口頭で説明を補足することで、認識の齟齬を減らす事ができます。検証時に本来の目的以外で発見されたバグなどについては、スケジュールや発生頻度を考慮して、バグ修正を次回アップデートに回すといった柔軟な対応が取れるようになりました。 文書フォーマットの効果 私たちのチームでは検証の概要を、基本的には5つの項目でまとめました。ここで決められた項目(目的・概要・結果・課題・関連リンク)というフォーマットととても相性が良かったです。 自由なフォーマットに落とし込んで書いた場合、資料作成者が「伝えたいことを伝えたいだけ、細かく記載してしまう」ことが発生します。必要項目に記入していく形式にしたことで、資料を読む人やこのあと引き継ぎする人が、知りたい目的や概要、結果などをまとまった状態で読む事ができるようになりました。さらに文書共有ドキュメントWebサービスを使うことで誰が記載してもフォーマットさえ決めておけば、資料のクオリティを下げる事なく、情報をまとめる事ができます。書式なども決められたものを使うので、資料としてのまとまりがよく読みやすいという利点があります。このようなWebサービスでは資料をツリー管理しているため、前後の検証の資料と紐づいた形で資料を閲覧・管理できます。 私たちのチームでは概要ページのタイトルを「yyyymmdd_検証概要」という命名規則で、管理しています。単純に過去資料の振り返りをしやすくなったという効果の他に、半年に一度、自分が行ってきたタスクや業務を振り返る時にも役に立ちました。まとめた資料は見やすく、リンクを一覧化することで、同僚や上司にそのまま報告ができるといった効果がありました。また自身の実績を整理する際に、さまざまなファイルやドライブを探しまわる必要がなくなりました。ドキュメントWebサービスにこれまでの検証内容が概要としてまとまっていることで、作業効率が上がり、管理しやすいという効果を感じる事ができました。 最後に 私たちのチームではドキュメントWebサービスとスプレッドシートのそれぞれの得意分野に「概要」と「データ」に分けて管理することで、とても運用しやすくなりました。 「概要」に「目的」を記載することで、引き継ぎの際、当時何を目指したのかわかりやすくなった 「目的」を理解して、データを読むと引き継ぎの理解度が上がった 「目的の明文化」は、現在進行形の検証でも有効だった ドキュメントWebサービスはシステム的に文書を管理するため、検索や振り返りが容易になった もし過去実績の引き継ぎがうまくいかないなどでお悩みでしたら、「概要」と「データ」を分けるところから始めてみるのはいかがでしょうか。 ありがとうございました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、ZOZO NEXTでFashion Tech Newsの開発を担当している 木下 です。先日、弊社が運営するオウンドメディアのFashion Tech Newsにおいて英語版が公開されました。本記事では、機械翻訳サービスの比較検討、翻訳精度を向上するための調整、スムーズな翻訳を実現する仕組みについてご紹介します。比較検討の結果GPT-4を採用したため、GPT-4の本番運用を検討されている方の参考にもなるかと思います。 fashiontechnews.zozo.com 背景 翻訳の全体像 機械翻訳サービスの比較検討 翻訳精度を向上するための調整 プロンプトの調整 ルールベースでの前後処理 人によるチェック スムーズな翻訳を実現する仕組みの導入 日本語記事の公開をトリガーに翻訳 ネイティブチェックの完了をトリガーにチェック内容を可視化、Slackに通知 まとめ 背景 「 Fashion Tech News 」とは、2018年に運用を開始したZOZO NEXTのオウンドメディアです。ファッションテック領域へ挑戦を続けるZOZO NEXTが、独自の視点でファッション × テクノロジーのニュースを提供しています。 Fashion Tech Newsは記事本数の増加やPV数の増加で、メディアとしての価値が向上していました。そのような状況の中、より多くの方々に読んでいただけるように、また日本のファッションテックの情報を世界に発信することを目的に、記事を英語翻訳して公開することとなりました。限られたリソースの中で実現するために、機械翻訳を行なった上で人によるチェックを行う体制をとることとしました。 本番運用までに以下のステップで進めました。 機械翻訳サービスの比較検討 翻訳精度を向上するための調整 スムーズな翻訳を実現する仕組みの導入 翻訳の全体像 結論としては、以下のような流れで翻訳が行われます。記事は microCMS と呼ばれるCMSで管理されています。 日本語の記事の公開をトリガーに翻訳し、英語の記事を作成 CMS上でネイティブチェックを行う 編集者による最終チェックを行い公開する 機械翻訳サービスの比較検討 技術選定に当たっては一般的な機械翻訳サービスに加えて、大規模言語モデル(LLM: Large Language Model)も話題になっていたため比較に加えました。機械翻訳サービスの選定に当たって比較したポイントは以下です。 文意が正しいか 翻訳抜けがないか 固有名詞が正しいか 自然な英語表現か(面白い文章か) 料金 以下に比較表を示します。実際に翻訳結果を比較し、候補を絞り込んだ上で、ネイティブの方々からの評価をまとめたものです。表以外のサービスとして、GoogleのCloud Translation, Amazon Translate, Microsoft Tranlator, GPT-3.5も試しました。 比較項目 DeepL GPT-4 DeepLで翻訳し、GPT-4で編集 文意が正しいか △ ○ × 翻訳抜けがないか △ ○ △ 固有名詞が正しいか ○ △ ○ 自然な英語表現か(面白い文章か) △ ○ ○ 1年分(400万文字)の料金 (¥150/$1換算) ¥18,000 ¥41,000 ¥51,000 その他の特徴 - 用語集 - 語調切り替え - 専門用語等の翻訳が正確 - 漢字の読みが比較的正確 総合評価 △ ○ △ ※翻訳抜けとは、文章の一部や一文が翻訳されず、すっぽりと抜けてしまうことを指します。一度に長い文章を翻訳すると翻訳抜けが発生しやすいため、短い文章に分割して翻訳する工夫が必要です。ただ一方で短い文章ごとの翻訳では、表記揺れが発生しやすいという問題があります。 メディアとして正しい情報を伝える必要があるため、文意が正しく翻訳されなければいけません。その上で、読み物として文章が面白いことも必要になってきます。ネイティブチェックでは文章の面白さに重点が置かれており、ネイティブではない自分にとって印象的な視点でした。ネイティブチェックの結果、GPT-4が高く評価されました。 DeepLの強みである専門用語の正確な翻訳と、GPT-4の強みである自然な英語表現という2つの長所を活かし、DeepLで翻訳した文章をGPT-4で編集する方法も試しました。しかしDeepLで正しく翻訳できなかった箇所を、GPT-4でより異なる意味に変換してしまう事象が発生しました。そのため、GPT-4のみを採用しました。 カジュアルな内容から学術的な内容までを広く含むメディアであったこと、またネイティブチェックを行う体制をとることを踏まえてGPT-4を採用しました。今回は採用を見送りましたが、DeepLは専門用語の翻訳に定評があり、語調の切り替えなど便利な機能が存在します。翻訳の目的、チェック体制などを考慮して選定するのが良いと感じました。参考にした記事は以下です。 4大自動翻訳サービスの実力比較と活用ポイント DeepLの弱点が明らかに AI翻訳を比較してみました。Google, DeepL, OpenAI DeepL の翻訳精度は? ビジネスメールでの Google、Microsoft との比較結果 翻訳精度を向上するための調整 GPT-4を採用した上で、翻訳精度を向上するために以下の調整をしました。 プロンプトの調整 ルールベースでの前後処理 人によるチェック プロンプトの調整 最終的なプロンプトの抜粋を以下に示し、それぞれの意味を解説します。プロンプトの調整は、OpenAIのPlaygroundを利用しました。 記事IDには取り上げるテーマや人名の英語が入っていることがあり、それが翻訳の参考となるためプロンプトに含めました。 This article's id is ${id}. 書籍名は、日本語のみのものも多いため、日本語のまま表記するというルールがチーム内で決められました。プロンプトでは指示文のみの場合、意図した通りに動作しなかったため、以下のように具体例を含める必要がありました。 Translate the text into English, but all titles, including academic papers, reports, and literature should be left in Japanese. (ex. 『書籍のタイトル』(2023年、xx出版)-> "書籍のタイトル" (2023, xx Publishing)) メディアとしての文体で、読みやすくするための指示を追加します。こうすることで文体もある程度統一されます。 To enhance the web media tone of the text, replace any words. ルールベースでの前後処理 その他の必要な処理は、ルールベースで行いました。具体的には、英語の敬称(Mr., Ms.など)や「〜さん」といった表現は翻訳に含めないことになったため、削除する処理を入れました。 人によるチェック 上記のような調整を行なっても、正しい翻訳結果が得られないこともあります。具体的には以下のような場合です。 珍しい、もしくは特殊な読み方をする漢字 直訳ではない、会社名や人名の英語表記 専門用語の訳 文脈内で特殊な使い方をされている用語 これらはここまでの工程では対応できないため、人によるチェックが必要です。そのためチェックする方と認識を揃えるために、チェックリストを作成しました。 この部分はまだ改善の余地はあり、GPT-4を用いるなどして最初に文章から固有名詞を抜き出し、固有名詞の辞書を用いる、もしくは検索を利用して予め翻訳しておくなどの方法が考えられます。辞書としては、 ENAMDICT/JMnedict などが挙げられます。 スムーズな翻訳を実現する仕組みの導入 ここまで説明した内容は翻訳の質を担保するものでした。それに加えて、スムーズな翻訳を実現する仕組みを導入しました。 日本語記事の公開をトリガーに翻訳 ネイティブチェックの完了をトリガーにチェック内容を可視化、Slackに通知 冒頭でも紹介したように、記事の管理はmicroCMSを用いています。英語記事の公開に当たり、日本語とは別の環境(API)を作成しました。日本語記事をmicroCMSから取得し、翻訳結果を英語の環境に保存します。その後のチェックではmicroCMSを直接編集することになります。 日本語記事の公開をトリガーに翻訳 日本語記事が公開されると、microCMSの Webhookの機能 を利用し、それをトリガーに翻訳が開始されます。翻訳結果はmicroCMSの英語の環境に保存されます。またネイティブチェックの差分を可視化する際に必要となるため、AWS S3にも保存されます。 ネイティブチェックの完了をトリガーにチェック内容を可視化、Slackに通知 その後ネイティブチェックが行われます。ネイティブチェックが完了したら、チェックを担当した人はmicroCMSの カスタムステータス を変更します。これをトリガーに、現状のmicroCMSのデータと、S3に保存されている当初の翻訳との差分を可視化するコードが実行されます。このコードの実装にはGoogleの Diff Match Patch を利用し、Googleドキュメントに結果を保存しました。 続いて、ネイティブチェックとその差分の可視化が完了したことをSlackに通知し、編集者による最終チェック、公開へと進みます。2段階のチェックとすることで、英語と情報、両方の正しさを担保しています。 まとめ Fashion Tech Newsの翻訳に当たり、様々な機械翻訳サービスを比較しGPT-4を採用しました。採用理由として挙げられるのは、様々なジャンルの記事に対応できたこと、チェック体制との親和性が高かったことです。そして翻訳精度の向上を目指し、プロンプトの調整、ルールベースでの前後処理、人によるチェックを導入しました。さらに翻訳をスムーズに進めるため、microCMSの機能やチェックの差分を可視化する仕組みを活用しました。よりチェックの負担を減らすには、改善の余地はまだありますが、今回の手法により効率的で安定した翻訳体制を実現できました。 ZOZO NEXTでは、最先端を含む様々な技術を取り入れプロダクト開発に取り組んでいます。絶賛仲間を募集しておりますので、興味を持ってくださった方は以下をご確認ください。 カジュアル面談はこちらからご応募ください。 hrmos.co 募集している職種はこちらからご確認ください。 https://hrmos.co/pages/zozo/jobs/0000209 hrmos.co https://hrmos.co/pages/zozo/jobs/1809846973241688076 hrmos.co
はじめに こんにちは、ZOZOTOWN開発1部Android2ブロックの井上晃平( @ねも )です。普段はZOZOTOWN Androidアプリの開発を担当しています。ZOZOTOWN Androidチームでは、以前から商品に対して口コミや評価を投稿・閲覧できる、アイテムレビュー機能を開発していました。そして、2023年11月29日に晴れてアイテムレビュー機能がリリースされました。 アイテムレビュー機能を設計・開発していく中で見えてきた課題を、解決策とともにご紹介します。 そもそもアイテムレビュー機能のことを知りたいという方は、 プレスリリース で機能紹介をしているので、あわせてご覧ください。 目次 はじめに 目次 課題 解決策 マイルストーン方式 モジュール構成の工夫 FeatureFlag まとめ 課題 アイテムレビュー機能の開発における最大の課題はビッグバンリリース問題です。アイテムレビュー機能は開発に長い時間をかけていて、弊社の中ではかなりの大型案件でした。弊社の案件では基本的に1回のリリースで全コード差分を本番コードに取り込みます。しかし、今回のアイテムレビュー機能では機能要件が他と比べて複雑かつ巨大なため、それを実現するコードの量も多いです。そのため普段の機能開発と同じリリース方式を採ってしまうと、とても大きなコンフリクトが発生してしまったり、万が一リリース後に不具合が発生した際は、原因究明が難しくなったりするなどの問題がありました。 解決策 マイルストーン方式 この問題の解決策として、弊チームはマイルストーン方式のリリースを採用しました。開発期間を6分割し、マイルストーン毎にリリースする要件やタスクを決めます。そして、マイルストーンが終わるタイミングでQAを行い実際にプロダクションコードに差分を取り込んでいくというリリース手法です。このマイルストーン方式によって主に3つの成果を得ることができました。 大規模なコンフリクトの防止 アイテムレビュー機能の開発を6分割した小さい粒度でプロダクションコードに差分を取り込むので、大規模なコンフリクトが起きにくいです。 アプリ品質の向上 マイルストーン毎にQAを実施するので、不具合を早期発見できて原因も特定しやすいです。そのため結果的にほとんどの不具合が解消された状態で、アイテムレビュー機能の本番リリース日を迎えることができました。 チームメンバーモチベーションの向上 マイルストーン毎にリリース時の達成感を味わえるため、長い開発期間の中でチームの士気が落ちてしまうことを防げました。 マイルストーン方式について1つ考えるべきことがあります。それは開発中のアイテムレビュー機能を、リリースビルド上でいかに隠すかという点です。リリースしていない機能のプログラムがリリースビルドで動いてしまうのは問題があります。マイルストーン毎にコードをリリースしますが、実際にアイテムレビュー機能を世に出すのは6回目のマイルストーンが終わったタイミングです。それまでの間はコード差分をプロダクションコードに取り込むものの、実際のリリースビルド上ではそのコードが含まれないもしくは実行されない状態になっている必要がありました。 この問題についてZOZOTOWN Androidチームではモジュール構成の工夫とFeatureFlagを解決策として採用しました。 アイテムレビュー機能周りのコードは以下のように2つに分けることができます。 アイテムレビュー機能のモジュールの内側で閉じているコード アイテムレビュー機能のモジュールの外側に出ているコード モジュール構成の工夫 まず1のコードをリリースビルドに含めない方法を紹介します。これを実現するためにモジュール構成を工夫しました。ZOZOTOWN Androidアプリではマルチモジュール構成を採用しています。アイテムレビュー機能の実装においては、このモジュール同士の依存関係を工夫しました。具体的には以下の画像の通りです。 これらのモジュールの役割を1つずつ解説していきます。 まずinterfaceモジュールは、その名の通り interface を保持するモジュールです。具体的に言うと以下の内容を interface として抽象化して保持しています。 アイテムレビュー機能のUI モーダルを表示させるための処理群 アイテムレビュー機能のCRUD処理 次にcoreとnoopモジュールについてです。これらは両方ともinterfaceモジュールの処理の実装を保持しています。interfaceモジュールにおいて依存関係を逆転しているので、それらの処理の実装をcoreとnoopの2パターン用意できます。具体的なコード例を次に示します。 まずinterfaceモジュールに以下のような interface を定義します。 interface GetTopItemReviewUseCase { suspend operator fun invoke(parameter: Parameter): Result <TopItemReview> data class Parameter( val goodsId: GoodsId, ) } interface TopItemReview { fun findUserReviewById(id: Long ): Serializable? } そしてその実装をcoreとnoopでそれぞれ用意します。 // coreモジュール class GetTopItemReviewUseCaseImpl @Inject constructor ( private val itemReviewRepository: ItemReviewRepository, ) : GetTopItemReviewUseCase { override suspend fun invoke(parameter: GetTopItemReviewUseCase.Parameter): Result <TopItemReview> { return itemReviewRepository.getTopItemReview(parameter.goodsId) } } // noopモジュール class GetTopItemReviewUseCaseImpl : GetTopItemReviewUseCase { override suspend fun invoke(parameter: GetTopItemReviewUseCase.Parameter): Result <TopItemReview> { return Result .success( object : TopItemReview { override fun findUserReviewById(id: Long ): Serializable? { return null } }, ) } } noopの invoke メソッドの処理で return している TopItemReview#findUserReviewById は、 null を返すだけの空の実装になっています。逆にcoreモジュールの方は ItemReviewRepository#getTopItemReview の処理を呼び出しています。 最後のintergrationモジュールはcoreとnoopモジュールをスイッチするためのモジュールです。プロジェクト内の他のモジュールがアイテムレビュー機能にアクセスしたいときは、このintegrationモジュールに依存させます。integrationモジュールに対して、noopは releaseImplementation として、coreは debugImplementation として依存させます。そうすることでビルドバリアントを変更するだけで実際のビルドさせるモジュールが切り替わるようになっています。 以上のようにモジュール構成を工夫することによって、リリースビルドでは空の実装が使用され、開発中のコードがリリースビルドに含まれてしまうことを防いでいます。 FeatureFlag 次に2のアイテムレビュー機能のモジュールの外側に出ているコードをリリースビルド上で動作させないための仕組みを解説します。そもそもモジュールの外側に出ているコードとは以下のようなものがあります。 アイテムレビュー機能のUI表示処理 既存画面に追加されるアイテムレビュー機能の初期データ取得処理 Google Analyticsなどのログ送信処理 これらのコードがリリースビルド上で動かないようにするため、FeatureFlagを導入しました。今回の案件ではサーバーサイドのAPIやFirebase RemoteConfigでフラグを管理する方法ではなく、ローカルで管理する方法を採用しました。実運用されているコードを例に解説します。 まずFeatureFlagのモジュールはfeatureFlag:flag、featureFlag:core、featureFlag:core-noopの3種類存在します。featureFlag:core-noopをリリースビルドで、featureFlag:coreをデバッグビルドでそれぞれ使用します。featureFlag:flagモジュールはビルドバリアント関係なく使用します。 featureFlag:flagモジュールにFeatureFlagを以下のような形で宣言します。 title と description を保持することで、そのFeatureFlagがどのような意味を持ち、どのような働きをするのかという内容を明確にします。 // featureFlag:flag sealed class FeatureFlag( val title: String , val description: String , ) { object ItemReview : FeatureFlag( "ItemReview有効化" , "ItemReviewの有効/無効を切り替える" ) // 他のFeatureFlag } そしてfeatureFlag:core-noopとfeatureFlag:coreのそれぞれに必要なコードを追加します。 // featureFlag:core-noop // in FeatureFlagEx.kt fun FeatureFlag.isEnable(): Boolean = false // in FeatureFlagContainer.kt object FeatureFlagContainer { @JvmStatic fun init (context: Context) { // no-op } } // featureFlag:core // in FeatureFlagEx.kt fun FeatureFlag.isEnable(): Boolean = FeatureFlagContainer.isEnable( this ) // in FeatureFlagContainer.kt object FeatureFlagContainer { private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "feature_flag" ) private var dataStore: DataStore<Preferences>? = null private fun FeatureFlag.toPreferencesKey() = booleanPreferencesKey(javaClass.name) private fun store(): DataStore<Preferences> = checkNotNull(dataStore) @JvmStatic fun init (context: Context) { if (dataStore != null ) return dataStore = context.dataStore } internal fun isEnable(feature: FeatureFlag): Boolean = runBlocking { store().data.first()[feature.toPreferencesKey()] ?: false } suspend fun set (feature: FeatureFlag, enable: Boolean ) { store().edit { setting -> setting[feature.toPreferencesKey()] = enable } } } リリースビルドでは isEnable が常に false です。デバッグビルドではJetpackのDataStoreを利用してFeatureFlagを保存しています。そのため必要に応じて isEnable の true/false を切り替えることができます。 また、 isEnable の処理だけをFeatureFlagの拡張関数として別ファイルに切り出している理由は以下です。 FeatureFlag. と入力し、sealed classで定義したFeatureFlagをIDEの一覧サジェストから選択して、 .isEnable とスムーズに続けることができるため。 isEnable の true/false のチェックのみが必要な箇所に対して、不必要に保存処理等までを公開しないようにするため。 FeatureFlagを利用する側では isEnable を呼び出しその戻り値によって実行する処理を分岐します。今回はアイテム詳細画面において、レビューの平均値を表示する星を出し分けるコードを例に紹介します。下記のように onViewCreated 内部でFeatureFlagを確認し、 ItemReview#isEnable が true になっていれば星を表示して、 false であれば何もしません。結果的にアイテムレビュー機能のON/OFFを切り替えることができるようになります。 override fun onViewCreated(view: View, savedInstanceState: Bundle) { super .onViewCreated(view, savedInstanceState) if (FeatureFlag.ItemReview.isEnable())) { // 星を表示させる処理 } } レビュー機能ON レビュー機能OFF 以上のようなFeatureFlagを用いて、アイテムレビュー機能のモジュールの外側に出ているコードがリリースビルド上で動かないようにできました。 まとめ 本記事ではZOZOTOWN Androidチームのアイテムレビュー機能実装における取り組みを紹介しました。比較的大きな案件でビッグバンリリースを未然に防いだ実績を作ることができました。今回学んだことを活かして次回以降の案件の遂行をより円滑にしようと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、ZOZO研究所AppliedMLチームの古澤です。私たちは商品画像の検索の基礎として、深層距離学習という技術を研究しています。本記事では、本研究所からICLR2024に採択された「Mean Field Theory in Deep Metric Learning」という研究について紹介します。対象読者としては、機械学習系のエンジニアや学生を想定しています。 目次 はじめに 目次 Notation 深層距離学習 磁性体と平均場理論 磁性体 平均場理論 磁性体と深層距離学習のアナロジー 深層学習への応用 Contrastive Loss ClassWiseMultiSimilarity Loss 実験 評価指標 データセット 実装の詳細 比較手法 定量評価 まとめ 参考文献・注釈 Notation この記事では以下のnotationを使用します。 訓練データ: 画像などのデータ: クラスラベル: クラス に属する訓練データ: データ を長さ1のd次元ベクトルに写す特徴抽出器: 特徴空間の距離: 深層距離学習 深層距離学習は、データ間の距離や類似度を学習するタスクであり、画像認識、顔認証、推薦システムなど、多くのアプリケーションで利用されています。 深層距離学習では、画像やその他の複雑なデータから、データの類似度を反映した特徴ベクトルを抽出することを目指しています。これを実現するためには、ベクトル空間内で類似するデータ点が近く、異なるデータ点が遠くなるように損失関数を設計することが重要です。 深層距離学習の損失関数の典型例として、Contrastive Loss 1 が知られています。 ここで、 です。第一項は同じクラス内のデータ点(正例ペア)の相互作用を、第二項は異なるクラスのデータ点(負例ペア)の相互作用を表しています。 ( )は正例(負例)ペア間の距離を制御するハイパーパラメータです。正例(負例)ペアの距離が ( )よりも小さく(大きく)なるように学習が進むことを意味しています。 Contrastive Lossのようなペアに基づく損失関数の場合、可能なペアの組み合わせはデータ数が増えるに従って多項式的に増加します。このペアのうちの多くは学習に寄与しない簡単なペアであるため、学習が遅くなってしまうという課題があります。 一方で、深層距離学習の損失関数としてはペアを使用せず、分類問題と同じように訓練できる損失関数も存在します。 今回の研究では、統計物理学における解析手法である平均場理論を使用することで、ペアに基づく損失関数から、分類問題と同じように訓練できる損失関数を導出する方法を確立することを目的としています。 磁性体と平均場理論 今回の研究は、統計物理学における磁性体のモデルと深層距離学習とのアナロジーに基づいています。磁性体、特に強磁性体は、その物質の微視的な磁気モーメント(スピン)が互いに相互作用し、同じ方向を向くことで巨視的な磁化を示します。磁性体の相転移現象は、統計力学の典型的な問題として知られています。 2 磁性体 磁性体の簡単なモデルとして、以下のような無限レンジモデルを考えてみます。無限レンジモデルのエネルギー関数は次のように表されます。 ここで、Nはスピンの総数で、 とします。 は半径1の球面上に値をとるベクトルで、i番目のスピンを表します。この は、スピンが同じ方向を指している状態がエネルギー的に好まれることを示しています。 統計力学によると、温度Tでのスピン配位の確率分布はギブス分布に従います。 このモデルの熱力学的に重要な特性は、分配関数 から計算できます。 しかし、スピン間の相互作用のため、分配関数 の積分の解析的、数値的な計算は困難に見えます。 3 平均場理論 次に平均場理論について説明します。平均場理論の基本的なアイデアは、各スピンが他のスピンと相互作用する際に、他のスピンをその「平均場」で近似し、揺らぎを無視することです。これにより、各スピンが他のスピンと直接相互作用するのではなく、平均場と相互作用するようにハミルトニアンを近似できます。 具体的には、 という恒等式を使用して を の揺らぎに関してTaylor展開し、スピン間の揺らぎの交差項を無視します。この操作によりエネルギー関数と分配関数は次のようになります。 ここで、平均場 は を最小化することで決定されます。この条件は次の自己整合方程式に帰着します。 この式は、平均場 が実際に他のスピンの平均を表しており、実際に平均値周りでの揺らぎに対して展開が行われていたことを示しています。 平均場理論により、新しく最適化パラメータが導入され、スピン間の相互作用が単純化されます。これにより、分配関数に含まれる積分は独立に実行可能となります。 磁性体と深層距離学習のアナロジー 次に、分配関数のT=0の極限を考えてみましょう。この極限では、分配関数に最も寄与するスピン配位はエネルギー関数 を最小化する配位となります。これは、T=0の極限で、元の問題が を最小化するスピン配位を見つける問題と同等であることを意味します。 平均場理論を適用すると、この問題はハミルトニアン を最小化する問題に変換されます。そして、この極限では、 は に対して最小化された に比例します。したがって、 を と について最小化する問題となることがわかります。すなわち、以下のように問題が変換されたことになります。 一方で、深層距離学習では、損失関数を最小化する最適な学習パラメータ を見つけることが目標であり、次のように書くことができます。 この形式は磁性体の問題と同様であり、どちらもペア間の相互作用の関数として記述されています。このアナロジーは、平均場理論を深層距離学習に適用することで、ペア間の相互作用を平均場との相互作用に置き換えることが可能であることを示唆しています。 深層学習への応用 次に、深層距離学習の損失関数に平均場理論を適用してみます。簡単のため、Contrastive Lossに平均場理論を適用し、その後、より複雑な損失関数への適用について議論します。 Contrastive Loss 平均場理論を適用するために、各クラスの平均場 を導入し、 をこれらの平均場周りの揺らぎに関して展開します。この際、平均場間の相対距離を制約する条件を課します。 この条件は、0次の展開で を最小化する平均場のみを探索することを意味します。 磁性体の場合と同様、各特徴ベクトル について、同じクラスの平均場 の周りで展開します。 展開の際にはやはり交差項は無視しますが、揺らぎの自己相互作用に関しては残しつつ、resummationを行います。結果として、以下のMeanFieldContrastive(MFCont)Lossが得られます。 ここで、平均場に対する制約条件は を用いてソフトに取り入れられています。以上の議論から、深層距離学習の場合でも、平均場を導入することでペアの相互作用を取り除き、通常の分類問題の形に帰着できることがわかります。 ClassWiseMultiSimilarity Loss より発展的な損失関数として、MultiSimilarity Loss 4 のようなミニバッチ内のペア間の相互作用を考慮した損失関数の平均場理論について考察します。 Taylor展開を簡単にするためには、 と に関して対称な損失関数が望ましいです。次のような新しい損失関数を導入しましょう。 ここで、 、 、 はハイパーパラメータです。この損失関数はMultiSimilarity Lossに似ていますが、クラス別の負のサンプル間の相互作用を含むため、ClassWiseMultiSimilarity(CWMS)Lossと呼びます。 次に、この損失関数に平均場理論を適用してみましょう。最初と2番目の項のlogの中身は、Contrastive損失の正と負の相互作用に似た形をとります。このため、Contrastive Lossの議論を繰り返すことで、MeanFieldClassWiseMultiSimilarity(MFCWMS)Lossを導くことができます。 ここで、平均場の制約条件もソフトに導入しています。この損失関数は、各サンプル間の相互作用も取り入れており、より複雑な関係性まで取り入れられていることがわかります。 実験 今回は従来のMetric learningの評価手法と A Metric Learning Reality Check (MLRC) 5 で導入されたより公平なベンチマーク手法の両方を使用して評価しました。 評価指標 深層距離学習の単純な評価指標としてはPrecision@K(P@K)やRecall@K(R@K)などが考えられます。しかし、今回はMLRCに従い、Mean Average Precision at R(MAP@R)と呼ばれる指標を使用します。 各クエリに対し、 はk番目に近いデータ点が同じクラスの場合はP@k、そうでなければ0となる関数です。また、Rはクエリと同じクラスに属するデータ点の数を表します。 MAP@Rは最近傍のデータ点が同じクラスかという情報だけではなく、順位の情報も反映しています。このため、データ点がよりよくクラスタリングされているかを評価できます。 評価指標の比較 (figure from A Metric Learning Reality Check ) データセット 実験では、以下の4つの画像検索用データセットを使用しました。 CUB-200-2011 (CUB):200クラス・11788枚の鳥の画像データセット 6 Cars-196 (Cars):196クラス・16185枚の車の画像データセット 7 Stanford Online Products (SOP):22634クラス・120053枚の商品画像データセット 8 InShop:7982クラス・52712枚のファッション商品画像データセット 9 CUBとCarsと比較するとSOPとInShopは規模が大きいデータセットです。 MLRCのベンチマークではCUB、Cars、SOPを、通常の評価手法ではすべてのデータセットを使用しました。 実装の詳細 ベースとなる埋め込みモデル には、ImageNetで事前学習されたBN-Inceptionネットワークを使用し、最後の線形層を所望の次元の特徴ベクトルを得られるように置き換えました。 MLRCの評価手法では、損失関数のハイパーパラメータ最適化のため、ベイズ最適化を50回繰り返します。データセットはtrain-valid(最初の半分のクラス)とtestデータセット(残り)に分割しました。さらに、train-validセットをクラスが重複しないように4分割し、4-foldのクロスバリデーションを実施しました。クロスバリデーションにおけるMAP@Rの平均をベイズ最適化の目的関数として使用しました。また、特徴ベクトルの次元は128次元に、バッチサイズは32に設定しました。 テスト段階では、最適なハイパーパラメータで同様にクロスバリデーションを実行し、4つの埋め込みモデルを得ました。それぞれの特徴ベクトルを独立に使用した場合のMAP@Rの平均と、4つの特徴ベクトルを結合して新しい512次元の特徴ベクトルを作成し、これをもとに計算したMAP@Rを評価に使用しました。テストでは上記の施行を10回繰り返し、各指標の平均値と95%信頼区間を報告しました。 一方、通常の評価手法では、データセットをクラス間が重複しないようにtrainとevaluationの2つに分割しました。各エポックでevaluationスコアを計算し、その最大値を最終的なevaluationスコアとして採用しました。また、特徴ベクトルの次元は512次元に、バッチサイズは128に設定しました。こちらも同様に10回繰り返し、その平均値と95%信頼区間を報告しました。 比較手法 比較手法としては、以下の損失関数を採用しました。 Contrastive (Cont.) ClassWiseMultiSimilarity (CWMS) MultiSimilarity (MS) MultiSimilarity + Miner (MS+Miner) ArcFace 10 CosFace 11 ProxyNCA 12 ProxyAnchor (ProxyAnch.) 13 最初の4つはペアに基づく損失関数で、残りの4つは分類問題と同様に訓練できる損失関数です。特にProxyAnchor Lossは非常に性能の良い損失関数と考えられています。 定量評価 MLRCベンチマークでは、MFContとMFCWMSは、ほとんどの場合で元の損失関数よりも優れたスコアを示しました。これは、平均場理論を適用することで、学習を単純化するだけでなく、より汎化性能が高い特徴空間を学習できることを意味しています。 CarsデータセットではProxyAnchorおよびCWMSがMFContとMFCWMSよりも優れたパフォーマンスを示しています。しかし、CUBおよびSOPデータセットでは、分離されたMAP@Rおよび連結されたMAP@Rの両方で他のベースライン手法を一貫して上回りました。 ベンチマーク結果 また、従来の評価方法では、Carsデータセットを除く全てのデータセットにおいて、MFContとMFCWMSはProxyAnchor Lossおよび元の損失関数の性能を上回りました。これはMLRCベンチマークとも一致する結果です。精度の向上は特に大きなデータセットで顕著でした。さらに、全てのデータセットにおいて、MFContとMFCWMSは他の損失関数よりも早く収束することが確認されました。 評価結果 まとめ この記事では、統計物理学における解析手法である平均場理論を深層距離学習に適用した研究を紹介しました。特に、平均場理論を用いることで、学習が難しいペアに基づく損失関数を、分類問題のような損失関数に帰着させることができることを示しました。さらに、平均場理論をContrastive LossとCWMS Lossに適用し、新しい損失関数としてMFCont LossとMFCWM Lossを提案しました。 導出された損失関数の評価した結果、これらは比較手法と比較して、多くのデータセットで優れたパフォーマンスを示すことが確認されました。この結果は、深層距離学習における平均場理論の有効性を示唆しています。 ZOZO研究所では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co hrmos.co 参考文献・注釈 Raia Hadsell, Sumit Chopra, and Yann LeCun, "Dimensionality reduction by learning an invariant mapping", In 2006 IEEE Computer Society Conference on Computer Vision and Pattern Recognition (CVPR’06), volume 2, pages 1735–1742. IEEE, 2006. ↩ Nishimori, Hidetoshi, and Gerardo Ortiz, "Elements of Phase Transitions and Critical Phenomena", (Oxford, 2010; online edn, Oxford Academic, 1 Jan. 2011), ↩ 無限レンジモデルの場合は、実は解析的に分配関数を計算可能です。しかし、より現実的なスピンが格子上にある場合などについては、通常、解析的な計算は困難です。 ↩ Xun Wang, Xintong Han, Weilin Huang, Dengke Dong, and Matthew R Scott, "Multi-similarity loss with general pair weighting for deep metric learning", In Proceedings of the IEEE/CVF conference on computer vision and pattern recognition, pages 5022–5030, 2019. ↩ Kevin Musgrave, Serge Belongie, and Ser-Nam Lim, "A metric learning reality check", In Computer Vision–ECCV 2020: 16th European Conference, Glasgow, UK, August 23–28, 2020, Proceedings, Part XXV 16, pages 681–699. Springer, 2020. ↩ Catherine Wah, Steve Branson, Peter Welinder, Pietro Perona, and Serge Belongie, "The caltech-ucsd birds-200-2011 dataset", 2011. ↩ Jonathan Krause, Michael Stark, Jia Deng, and Li Fei-Fei, "3d object representations for finegrained categorization", In Proceedings of the IEEE international conference on computer vision workshops, pages 554–561, 2013. ↩ Hyun Oh Song, Yu Xiang, Stefanie Jegelka, and Silvio Savarese, "Deep metric learning via lifted structured feature embedding", In Proceedings of the IEEE conference on computer vision and pattern recognition, pp. 4004–4012, 2016. ↩ Ziwei Liu, Ping Luo, Shi Qiu, Xiaogang Wang, and Xiaoou Tang, "Deepfashion: Powering robust clothes recognition and retrieval with rich annotations", In Proceedings of the IEEE conference on computer vision and pattern recognition, pp. 1096–1104, 2016. ↩ Jiankang Deng, Jia Guo, Niannan Xue, and Stefanos Zafeiriou, "Arcface: Additive angular margin loss for deep face recognition", In Proceedings of the IEEE/CVF conference on computer vision and pattern recognition, pp. 4690–4699, 2019. ↩ Feng Wang, Jian Cheng, Weiyang Liu, and Haijun Liu, "Additive margin softmax for face verification", IEEE Signal Processing Letters, 25(7):926–930, 2018a. ↩ Yair Movshovitz-Attias, Alexander Toshev, Thomas K Leung, Sergey Ioffe, and Saurabh Singh, "No fuss distance metric learning using proxies", In Proceedings of the IEEE international conference on computer vision, pp. 360–368, 2017. ↩ Sungyeon Kim, Dongwon Kim, Minsu Cho, and Suha Kwak, "Proxy anchor loss for deep metric learning", In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, pp. 3238–3247, 2020. ↩
はじめに こんにちは。ブランドソリューション開発本部WEARバックエンド部SREブロックの山岡( @ymktmk )です。 2024年1月25日にFindy社によるオンラインイベント「Kubernetes活用の手引き 私たちの基盤構築・運用事例 Lunch LT」が開催されました。このイベントでは、株式会社メルカリさん、株式会社MIXIさん、LINEヤフー株式会社さんから一人ずつ、弊社からも私がLTをしてきましたので、こちらのブログでも報告いたします。 findy.connpass.com 登壇内容 今回のイベントでは、以下のような方をターゲットとして、各社からKubernetesに関する取り組みを紹介しました。 他社のKubernetes活用事例を知りたい方 Kubernetesを運用するための工夫点や検討すべき点を知りたい方 私からは、「Kubernetesを活用した開発者体験向上の取り組み」というタイトルで発表いたしました。 speakerdeck.com 発表内では、弊チームがこれまで取り組んできた開発者体験の向上施策のうち、「負荷試験基盤」と「Pull Request毎のPreview環境」の2つの事例を取り上げてお話ししました。以下は今回の発表の要約です。 負荷試験基盤 弊チームの負荷試験では各々が異なる負荷試験ツールを使用しており、統一されていませんでした。そのため、チームにおける負荷試験のノウハウの蓄積が難しく、特に新規メンバーは負荷試験の実施が容易ではありませんでした。 そこで、使用するツールをK6に統一し、 k6-operator とGitHub Actionsを活用した負荷試験基盤を作成しました。これにより、負荷試験の実施者は、テストシナリオをGitHubにPushし、実施したいタイミングでGitHub Actionsのワークフローを手動実行するだけで負荷試験が可能になりました。 導入後の効果としては、負荷試験を手軽に実施できるようになり、負荷試験のハードルが下がりました。また、+αの効果として、GitHubでテストシナリオを管理するようになったため、過去のシナリオを再利用できるようになり、負荷試験の準備やレビューが容易になりました。それにより、負荷試験の妥当性が向上し、心理的な負担も軽減しました。 Pull Request毎のPreview環境 私たちが担当する WEAR はローンチから10周年を迎えており、10年間の技術負債を解消すべく、Webフロントエンドをリプレイス中です。リプレイスにおいて、旧環境のUIと比較しながらレビューすることが多いため、負担が大きいことや、デザイナーを巻き込む場合にはStaging環境を使うため、他の機能のリリースを妨げていました。また、VercelやHerokuなどのモダンなPaaSに備わっているPreview Deployment機能を使った開発者体験を求める声が多く寄せられていました。 そこで、 Argo CD Pull Request Generator を活用し、Pull Request毎に Virtual Service 、Service、DeploymentのセットをKubernetesクラスターに適用することでPreview環境を実現しました。 導入後の効果としては、「Pull Requestのレビューがしやすくなりました」「開発スピードが爆上がりしました」などの嬉しいフィードバックをいただきました。現在では、フロントエンドチームが作成するPull Requestの半分以上はPreview環境が活用されています。 まとめ Kubernetesは高い拡張性を活かして様々な機能開発ができます。弊チームでは、k6-operatorを使って負荷試験基盤、Argo CD Pull Request Generatorを使ってPull Request毎のPreview環境を実現しています。Kubernetesと開発者体験の向上はとても相性が良いです。運用において発生した課題を解決するためOperatorの導入や開発に取り組んでみてはいかがでしょうか。 10分という短い時間でしたので詳細なことは話せませんでしたが、これらの事例の技術面にフォーカスした話は、弊社のTechBlogやイベントで発表していますので、ぜひご覧ください。 techblog.zozo.com techblog.zozo.com 登壇後の所感 登壇後のフィードバックやX( #k8s_findy )の実況を拝見すると、「k6-operator」や「Argo CD Pull Request Generator」といったKubernetes Operatorに対する関心が高いようでした。弊社での活用事例が参考になったとの声もいただき、大変嬉しく思います。 普段、社外からチームの活動に対する評価をいただく機会が少ない中、このような登壇の機会を頂けたことをとても感謝しています。今後も機会がありましたら、新たな情報発信ができればと思います。 謝辞 本イベントはFindy社の主催でしたので、Findyの方々には様々なサポートをしていただきました。そして、登壇に至るまでサポートしてくれた社内のDevRelチームや弊チームにもこの場を借りて感謝申し上げます。また、ご視聴いただいた皆様ありがとうございました。 おわりに ZOZOでは、プロダクト開発以外にも、今回のようなイベントの開催や登壇など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com corp.zozo.com
初めまして。MLデータ部データ基盤ブロックの小泉です。 本記事ではGartner社から提唱されたActive Metadata Managementに着目し、BigQueryのCompute費用を削減した方法についてご紹介します。 目次 目次 Active Metadata Managementとは 結局どんなことを行なったのか、ざっくりまとめ Compute費用のpricing modelとReservationについて pricing model Reservation Metadataを使用して最安値のpricing modelを求める方法 マート集計クエリ実行時、pricing modelを切り替える方法 まとめ Active Metadata Managementとは Active Metadata Managementとは、Gartner社が提唱するメタデータ管理の新しい考え方です。 簡単に一言で説明すると、 システムが自らメタデータを収集、分析、洞察し、具体的なアクションを起こすこと です。この 一連のプロセスが継続的に行われること もポイントです。これにより、データの品質管理、セキュリティ、コストなどの最適化が期待される考え方になります。 詳細な解説がされているGartner社の記事はこちらです。 towardsdatascience.com 上記記事を要約すると、Active Metadata Managementには以下4つの特徴があります。 Active metadata platforms are always on. 人間が手作業でメタデータを入力することなく、常にあらゆるメタデータを自動収集する。 Active metadata platforms don’t just collect metadata. They create intelligence from metadata. メタデータを収集するだけでなく、収集したメタデータを分析して洞察する。加えて時間と共に洞察の精度を向上させる。 (例)クエリログからSQLコードを解析して自動的にカラムレベルのリネージを作成し、どのテーブルが最もクエリされているかを推察する。 Active metadata platforms don’t just stop at intelligence. They drive action. 収集したメタデータを分析して洞察した結果を、自ら活用する。 (例)過去のログを使用して、どのデータセットが最も利用されているかを分析。その分析結果をデータパイプラインシステムに送り、データパイプラインの実行スケジュールを自動的に最適化する。 Active metadata platforms are API-driven, enabling embedded collaboration. 外部ツールとAPI連携が可能。 (例)あるデータへのアクセスをリクエストされた際、データ所有者はSlack上でリクエストを受け取り、承認または拒否ができる。 今回私たちは、3つ目の特徴(Active metadata platforms don’t just stop at intelligence. They drive action.)に着目しCompute費用削減を行ないました。 3つ目の特徴についてもう少し詳しく説明します。 この特徴を言い換えると「メタデータを使用し、システムに対して自動的に何かをする」ということです。この「自動的に何かをする」というところが重要になります。先ほど挙げた例においては、「データパイプラインの実行スケジュールを自動的に最適化する」という部分が「自動的に何かをする」を指しています。また、データパイプラインの実行スケジュールが最適化されるまでに、人間が手を加える必要がないことも重要です。 つまり、 人間が介入することなく、システムが自動的にメタデータを分析し、アクションを起こすこと が3つ目の特徴となります。 以上がActive Metadata Managementの特徴です。本記事がActive Metadata Management活用のきっかけとなれば幸いです。 では、本題に入ります。 結局どんなことを行なったのか、ざっくりまとめ 費用削減までの工程をざっくりまとめると以下の通りです。 各工程における詳細な情報は後述します 。 各種pricing modelを設定したReservationを用意し、各々専用のprojectを割り当てる INFORMATION_SCHEMA.JOBS_BY_PROJECTから取得した過去の集計実績を元に、データマート毎に最安値のpricing modelを求める マート集計クエリ実行時、クエリを実行するprojectを最安値のpricing modelを設定しているprojectに切り替えて集計する 改めまして、ここからCompute費用の削減方法について詳しくご紹介していきます。 Compute費用のpricing modelとReservationについて 今回のCompute費用削減においては、Compute費用のpricing modelとReservationへの理解が重要になります。特に後述するReservationへprojectを割り当てる「Assignment」は、Compute費用削減に利用する機能です。それでは、Compute費用のpricing modelとReservationについて説明していきます。 pricing model まず、Compute費用のpricing modelについて簡単に説明します。 ※私たちはUSリージョンでBigQueryを使用しているので、USリージョン版単価での説明となります。 Compute費用のpricing modelを簡単な図にまとめました。 cloud.google.com 順を追って説明していきます。 まず、Compute費用のpricing modelは On-demand compute pricing と Capacity compute pricing の2種類に分類されます。違いは以下の通りです。 On-demand compute pricing:クエリ実行時にスキャンされたデータ量に対して課金される Capacity compute pricing:クエリ実行時に使用された計算リソース(Slot)に対して課金される それぞれのpricing modelについてもう少し詳しく解説していきます。 On-demand compute pricingは クエリ実行時にスキャンされたデータ量に対して1TBあたり6.25USD が課金されます。従って、クエリ実行時にスキャンされるデータ量が多いほど費用が高くなります。そしてCompute費用削減の観点からは、クエリ実行時にスキャンされるデータ量を少なくすることが重要になります。以下がOn-demand compute pricingを適用した方がお得になるクエリのイメージです。 (例)On-demand compute pricingを適用した方がお得になるクエリのイメージ SELECT Window関数等の複雑な計算 FROM 小さいテーブル_1 CROSS JOIN 小さいテーブル_2 CROSS JOIN 小さいテーブル_3 上記クエリは、複雑な計算を行なっているのでSlot使用量は多くなりますが、小さいテーブル_1,2,3のデータ量が少ないため、クエリ実行時にスキャンされるデータ量が少なくなります。従ってOn-demand compute pricingを適用した方がお得になります。 一方、Capacity compute pricingは クエリ実行時に使用された計算リソース、いわゆるSlot に対して課金されます。つまり、クエリ実行時に使用されるSlot数が多いほど費用が高くなります。Compute費用削減の観点からは、クエリ実行時に使用されるSlot数を少なくすることが重要になります。以下がCapacity compute pricingを適用した方がお得になるクエリのイメージです。 (例)Capacity compute pricingを適用した方がお得になるクエリのイメージ SELECT * FROM 巨大なテーブル LIMIT 10 上記クエリは、処理がシンプルなのでSlot使用量は少なくなりますが、巨大なテーブルのデータ量が多いため、クエリ実行時にスキャンされるデータ量が多くなります。従ってCapacity compute pricingを適用した方がお得になります。 また、Capacity compute pricingは、 BigQuery editions という、利用可能な機能・単価に違いがある3種類のplanから選択をする必要があります。以下がBigQuery editionsの一覧です。 pricing model 課金単位 単価 Standard Edition Slot(hour) 0.04USD Enterprise Edition Slot(hour) 0.06USD Enterprise Plus Edition Slot(hour) 0.1USD 料金の傾向として、Standard Editionsが最安値のpricing modelになります。 各Editionの違いに関する詳細は以下をご確認下さい。 cloud.google.com 各Editionで使用できる詳細な機能の違いについては上記ドキュメントをご確認いただければと思いますが、ここでは今回のCompute費用削減に関係する2点をご紹介します。 まず1点目は、3つのEditionでは費用の計算方法が異なることです。詳細は Reservation でご紹介しますが、以下が各Editionでの課金方法です。 Standard Edition:Autoscale Slot Enterprise Edition:Autoscale Slot + Baseline Slot Enterprise Plus Edition:Autoscale Slot + Baseline Slot 2点目は、 Fine-grained security controls の使用可否です。Fine-grained security controlsとは、BigQueryテーブルのカラムに対して、以下のようなことができる機能(他にもいくつかあります)です。 policy_tag というタグを付与することで特定カラムのデータへのアクセス権限を制限すること 特定カラムのデータをマスキングすること この機能についても3つのEditionで使用できるかどうかが異なります。今回、このFine-grained security controlsの使用可否は重要なポイントになりますので、念頭に置いておいていただけると幸いです。 Standard Edition:使用不可 Enterprise Edition:使用可能 Enterprise Plus Edition:使用可能 Fine-grained security controlsの詳細は以下をご確認下さい。 cloud.google.com ここからは私たちが採用している3つのpricing modelについてご紹介します。 Compute費用削減前の私たちは、全てのデータマートをEnterprise Editionのみで集計していましたが、今回新たにOn-demandとStandard Editionを導入しました。現在はこの3つのpricing modelからデータマート毎に最安値のpricing modelを求め、Compute費用を削減しています。 On-demand Standard Edition Enterprise Edition 以上がCompute費用のpricing modelについての説明です。続いてReservationについて説明します。 Reservation まず、Compute費用削減においてなぜReservationが関係するのか説明します。 現状(2024年2月時点)では クエリ実行時に使用するpricing modelをダイレクトに切り替える機能がありません 。そのため、今回のCompute費用削減ではReservationのAssignment機能を利用してpricing modelを切り替えることにしました。では、最初にReservationの解説に入ります。 Reservationとはクエリ実行に使用するSlot数をあらかじめ予約できるBigQueryの機能です。作成時は以下の設定をします。On-demand用のReservationに関しては、 Explicit On Demand Resources というReservationがデフォルトで用意されていますのでこちらを使用しています。 Reservation:Reservationの名前 Location:リージョン Editions:Reservationへ適用するBigQuery editions Max Reservation size:クエリ実行中、必要に応じて自動的に追加されるSlot(Autoscale Slot)の上限 Baseline Slot:常時Reservationへ割り当てるSlot数 以下がOn-demand用のReservation、Explicit On Demand Resourcesです。 詳細なReservationの設定方法は以下をご確認下さい。 cloud.google.com また pricing model でも触れましたが、Reservation作成時に選択した BigQuery editionsによって課金されるCompute費用の計算方法が異なります のでご注意ください。 Standard Editionを選択した場合は、以下の計算方法で課金されます。加えて設定できるMax Reservation sizeの上限は 1600Slot です。 クエリ実行中、自動的に追加されるSlot数(Autoscale Slot)のみ Enterprise EditionとEnterprise Plus Editionは以下の計算方法で課金されます。こちらはMax Reservation sizeの上限がありません。 Baseline Slot + クエリ実行中、自動的に追加されるSlot数(Autoscale Slot) 加えてこの2つのEditionは クエリ実行をしていない場合でも、設定したBaseline Slot分の料金が常に課金される ので注意してください。 以上がBigQuery editionsにおける費用の計算方法の違いです。Baseline SlotやAutoscale Slotの詳細については以下をご確認下さい。 cloud.google.com 続いてAssignmentについて説明します。AssignmentとはReservation作成後、設定したSlotを使用するため、project, folder, organizationのいずれかを割り当てる機能のことです。今回のCompute費用削減においては、projectをAssignmentするパターンを採用しているので、projectをAssignmentする方法について説明します。 まず、ReservationへprojectをAssignmentする効果は 設定したprojectで実行されるクエリがReservationのSlotを確保できる ということです。 そして最大のポイントは、クエリ実行する際にAssignmentしたprojectを選択すると、 Reservationで設定したpricing modelを使用して費用課金される ということです。今回はこのポイントを利用してCompute費用削減を行なっています。 具体的なAssignmentの方法ですが、BigQuery editionsを採用しているReservationの場合は、コンソールから操作が可能です。 注意点は、On-demand用Reservation(Explicit On Demand Resources)にはコンソールからprojectのAssignmentができないことです。On-demand用Reservation(Explicit On Demand Resources)へprojectをAssignmentする方法は2種類あります。 bqコマンドを実行する CREATE ASSIGNMENT DDLステートメントをコンソールから実行する 今回私たちは、bqコマンドを実行する方法を採用しました。 bq mk \ --location = LOCATION \ --reservation_assignment \ --reservation_id = none \ --job_type = QUERY \ --assignee_id = PROJECT_ID \ --assignee_type = PROJECT reservation_id=noneに設定する ことで、指定したprojectはOn-demand用Reservationを使用したクエリ実行が可能になります。加えて、On-demandはQUERYジョブ(job_type=QUERY)のみのサポートとなりますのでご注意下さい。 CREATE ASSIGNMENT DDLステートメントをコンソールから実行する方法を含む詳細なAssignment設定方法は以下をご確認下さい。 cloud.google.com ここからは私たちのReservation設定についてご紹介します。 1つのReservationに複数のprojectを割り当てることも可能ですが、今回私たちは各price modelを設定したReservationに対し、1つずつ専用projectを割り当てました。 以上がReservationについての説明です。続いて、Compute費用削減における各工程について説明します。 Metadataを使用して最安値のpricing modelを求める方法 まず、私たちが採用している3つのpricing modelを使用する際の注意点について説明します。 pricing model でご紹介した通り、今回のCompute費用の削減に伴い新たにOn-demandとStandard Editionを導入しました。3つのpricing modelからデータマート毎に最安値のpricing modelを求め、Compute費用削減をしています。 On-demand Standard Edition Enterprise Edition しかし、 pricing model でも触れた通り、Standard Editionは Fine-grained security controlsが使用できません 。つまり、policy_tagが付与されているテーブルを使用したクエリには適用不可能ということになります。従って、Standard Editionを適用するには以下の注意点を考慮する必要があります。 実行クエリ内でpolicy_tagが付与されているテーブルを参照していないこと この条件を判定するには policy_tagが付与されているテーブル情報 が必要になりますが、現状(2024年2月時点)では INFORMATION_SCHEMA からこの情報を取得できません。そのため、私たちはpolicy_tagが付与されているテーブルの一覧情報を管理するテーブルを自作しています。こちらの管理テーブルからpolicy_tagが付与されているテーブルの一覧を取得することで Standard Edition を適用する条件を満たしているかどうかを判定できます。 以下が最安値のpricing modelを求めるクエリです。このクエリを実行していただければ、全データマートの最安値pricing model情報を一括で求めることが可能です。 WITH -- policy_tagが付与されているテーブル一覧を全て取得 GetSensitive AS ( SELECT DISTINCT TableName FROM `プロジェクトID.データセットID.policy_tag管理テーブル` ), -- 集計されているデータマート情報を各3種類の料金プランが設定されているプロジェクトから取得 --(On-demand, Standard Edition, Enterprise Edition) JOBS_BY_PROJECT AS ( SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM ` On -demand設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` UNION ALL SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM `Standard Edition設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` UNION ALL SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM `Enterprise Edition設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` ) --データマート毎に過去7間の平均価格を各3種類の料金プランで算出(On-demand, Standard Edition, Enterprise Edition) SELECT destination, ondemand_avg_price, enterprise_avg_price, standard_avg_price, policy_tag_flag, IF (policy_tag_flag , CASE WHEN enterprise_avg_price > ondemand_avg_price THEN ' On-demand設定のプロジェクトID ' ELSE ' Enterprise Edition設定のプロジェクトID ' END , CASE WHEN standard_avg_price > ondemand_avg_price THEN ' On-demand設定のプロジェクトID ' ELSE ' Standard Edition設定のプロジェクトID ' END ) AS lowest_project FROM ( SELECT CONCAT (destination_table.project_id, ' . ' ,destination_table.dataset_id, ' . ' ,destination_table.table_id) AS destination, SUM ((total_bytes_processed / 1024 / 1024 / 1024 / 1024 ) * 6 . 25 ) / 7 AS ondemand_avg_price, SUM ((total_slot_ms / 1000 / 60 / 60 ) * 0 . 06 ) / 7 AS enterprise_avg_price, SUM ((total_slot_ms / 1000 / 60 / 60 ) * 0 . 04 ) / 7 AS standard_avg_price, -- policy_tagが付与されているテーブルを参照しているかどうかを判定 LOGICAL_OR(ARRAY_LENGTH(ARRAY( SELECT referenced_table.table_id FROM UNNEST(referenced_tables) AS referenced_table INNER JOIN GetSensitive ON referenced_table.table_id = TableName)) > 0 ) AS policy_tag_flag FROM JOBS_BY_PROJECT WHERE DATE (creation_time, " Asia/Tokyo " ) BETWEEN DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 8 DAY) AND DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 1 DAY) AND user_email = ' データマート集計に使用するサービスアカウント ' AND job_type = ' QUERY ' GROUP BY destination ) クエリをご覧いただければわかる通り、データマート毎に過去7間の平均価格を各3種類の料金プランで算出及び比較をして最安値のpricing model( lowest_project カラム)を求めています。特に lowest_project カラムは、pricing modelを切り替える際に使用する情報ですのでご注目ください。 INFORMATION_SCHEMA.JOBS_BY_PROJECTの詳細については以下をご確認下さい。 cloud.google.com 以上がMetadataを使用して最安値のpricing modelを求める方法の説明です。 マート集計クエリ実行時、pricing modelを切り替える方法 続いて、マート集計クエリ実行時にpricing modelを切り替える方法について説明します。流れとしては、データマート集計クエリ実行時、取得した最安値のpricing modelへ切り替えをしてから、集計・更新するというものなっています。該当部分のコードを抜粋します。 from google.cloud import bigquery client = bigquery.Client(project= 'Enterprise Edition設定のプロジェクトID' ) query_job = client.query(project= '最安値のプロジェクトID' , query= 'データマート集計クエリ' , job_config= '集計結果を格納するテーブル設定など' ) 一番下の query_job に注目して下さい。ここで、 bigquery.Client のprojectパラメーターに先程の lowest_project カラム情報を指定しています。この設定をすることで、最安値のpricing modelを使用して集計ができます。つまり、以下3つのprojectの内、最安値のReservationにAssignmentされているprojectがパラメーターに設定されます。現状(2024年2月時点)では bigquery.Client にpricing modelを切り替えるパラメータが存在しないため、このような手法を採用しています。 また、 client にて bigquery.Client を定義する際、projectパラメータへ初期値として Enterprise Edition専用project を設定しています。これにより、最安値のpricing model情報が取得できなかったデータマートは、初期値に設定したproject(Enterprise Edition)で集計が行われます。 以上がマート集計クエリ実行時、pricing modelを切り替える方法です。 まとめ 本記事では、Active Metadata Managementに着目し、BigQueryのCompute費用を削減する方法についてご紹介しました。Compute費用の削減前は、全てEnterprise Editionでデータマート集計しておりましたが、この方法を採用した結果、以下のような割合でデータマート集計がされています。 On-demand:約20% Enterprise Edition:約30% Standard Edition:約50% Compute費用削減後、約50%のデータマートがStandard Editionを使用しています。Standard EditionはMax Reservation sizeの上限が1600Slotとなっています。そのため、集計遅延が発生する懸念をしておりましたが、Compute費用削減後も集計遅延は起きておらず削減前と変わらない速度で集計が完了しています。 そして本記事のタイトルにもなっていますが、トータルのCompute費用は約40%削減ができました。 比較的お手軽にCompute費用削減ができる方法ですので、是非お試しください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。カート決済部カート決済基盤ブロックの斉藤とSRE部カート決済SREブロックの飯島です。普段はZOZOTOWNカート決済サービスのリプレイスに携わっています。 弊社はモノリスからマイクロサービスへのリプレイスを進めており、カート決済サービスもその一環としてリプレイスを進めています。 よろしければ、カートリプレイスPhase1としてカート投入リクエストのキャパシティコントロールを実現させた事例もご覧ください。 techblog.zozo.com 本記事ではリプレイスの中でも、カートリプレイスPhase2としてZOZOTOWNで扱う在庫データをクラウドリフトした事例を紹介します。 はじめに 背景・課題 解決へのアプローチ アプリケーションの説明 DynamoDB API その他、AWSリソース 在庫の同期・補正を行うバッチ リリースまでの道のり 負荷試験 リリース 1. 初期データの移行 DynamoDBのキャパシティ 整合性の担保 2. リリース 効果 他の施策との相乗効果 さいごに はじめに 本章ではまず、リプレイス前のZOZOTOWNの在庫データの概要を説明します。 ZOZOTOWNの在庫データはオンプレミス環境で稼働しているSQL ServerのDB・テーブルで管理されています。CartDBとFrontDBという2つのDBで管理しており、CartDBはユーザのカート操作に関わる在庫データ、FrontDBはユーザの注文確定に関わる在庫データを管理しています。CartDBとFrontDBの在庫データの概要を以下に示します。 CartDBの在庫テーブル 販売可能数を管理している メタデータとして販売種別や販売開始日、販売価格等も管理している カート投入操作で更新される CartDBのカートテーブル ユーザがカートに確保している販売可能数を管理している カート投入・カート削除操作で更新される FrontDBの在庫テーブル 販売可能数を管理している メタデータとして販売種別や販売開始日、販売価格等も管理している 注文確定で更新される 上記テーブルのデータの流れを以下の図に示します。 CartDBとFrontDBの在庫テーブルは過去1つのDBで管理されていました。しかしZOZOTOWNの成長に合わせてカート投入と注文確定という在庫データへの主要なユーザアクションによるDBへの負荷を分散させるために分離されました。在庫テーブル・カートテーブルはSQL Serverのトランザクションで更新することによって販売可能数の整合性を担保していました。 背景・課題 昨今のZOZOTOWNの成長やカートリプレイスPhase1でも触れていた特定商品へのカート投入リクエストの集中によって在庫データへのアクセスが増加しており、負荷が上昇していました。また、上述した通り在庫データの管理にオンプレミス環境で稼働しているSQL Serverを使用しているため、耐障害性やスケーラビリティに課題がありました。このような課題から現在の在庫データの構成だと数年後には限界を迎えると考えられます。 具体的な負荷上昇につながっているボトルネックについて説明します。 CartDBで管理している在庫テーブルはカート投入時に以下のようなクエリが実行されます。 update 在庫テーブル set 販売可能数 = 販売可能数 - 1 where PK = *** 上記のクエリが同一の特定商品へ大量に実行されると、在庫テーブルへの読み取り要求・更新要求の競合が発生し、クエリの実行時間の大幅な遅延やタイムアウトが発生します。最悪のケースでは、クエリの遅延によりワーカースレッドが枯渇しCartDB全体のスループットが著しく下がるという障害が発生することもあります。SQL Serverの読み取り要求・更新要求の競合については以下に詳しく記載されていますので気になる方はご覧ください。 techblog.zozo.com 解決へのアプローチ この問題を解決するためにCartDBで管理していた販売可能数をAmazon DynamoDB(以下、DynamoDB)へ移行することにしました。 DynamoDBはオンプレミス環境で稼働しているSQL Serverと比較して以下のようなメリットがあります。 耐障害性が高い スケーラビリティが高い 自動でキャパシティがスケールイン・スケールアウトされる また、カートリプレイスPhase1でも採用されているため、既に運用ノウハウがある点も採用に至った理由です。弊社の半澤がカートリプレイスプロジェクトでDynamoDBを採用した背景について説明している記事がAmazon Web Serviceブログに掲載されていますので、そちらもぜひご覧ください。 aws.amazon.com アプリケーションの説明 ここからカートリプレイスPhase2のシステム構成について説明します。 システム構成図は以下のようになっています。 DynamoDB SQL Serverから移行された在庫データを持ちます。テーブル定義の例を以下に示します。 AttributeName Type KeySchema Description stock_id N Hash 商品のサイズ・カラーを特定できるID goods_id N 商品を特定できるID stock_quantity N 販売可能数 created_at S 作成日時 updated_at S 更新日時 上記のようにZOZOTOWN上で更新頻度が高い販売可能数をDynamoDBへ移行しました。これによって同一の特定商品へカート投入リクエストが集中した場合でも負荷に耐えられることが可能となりました。 上記のテーブルが生まれたことにより、ZOZOTOWNの在庫データの流れは以下のようになりました。 在庫同期については後述します。 今回、読み取り/書き込みスループットの課金方法と管理方法についてはオンデマンドモードを採用しています。その理由としては、ZOZOTOWNはカート投入リクエスト数がイベントや時間帯によって大きく異なり、また人気商品の発売による突発的なリクエスト増加もあるためキャパシティを予測することが困難なためです。リクエストが少ない時はキャパシティが縮小されてコストが抑えられ、逆にリクエストが増加した際には自動で拡張されるため柔軟性と拡張性を担保できます。 バックアップについてはPoint-in-time recovery(以下、PITR)を採用しています。PITRとは自動で継続的な増分バックアップを作成し、直近5分より以前の特定の状態に復元できるDynamoDBの機能です。バックアップの保持は35日間であるためそれ以前の状態には復元できません。他にも定期的にフルバックアップを作成するオンデマンドバックアップ機能もありますが、今回は以下の理由からPITRを採用しました。 在庫データは常に変動するため、何日前の何時いつの状態にリカバリするといった要件がない 運用ミスでテーブルを削除してしまった時の念のためのバックアッププランがほしい 復旧時間を短縮するためにできるだけ直近のデータにリカバリしたい API 在庫データをDynamoDBに移行するとともに、在庫データを操作する機能をCartAPIという名称のAPIとして切り出しています。元々はオンプレミスのWebサーバで稼働していたClassic ASPをJavaにリプレイスし、Amazon EKS上(以下、EKS)でマイクロサービス化しました。 CartAPIが持つエンドポイントは以下のものがあります。 販売可能数を取得する参照系 販売可能数を減少、増加する更新系 またSQL ServerとDynamoDBの在庫同期や不整合を防ぐためのバッチから呼び出されるCartBatchAPIも誕生しています。CartAPIと同様にClassic ASPからJavaにリプレイスし、EKS上でマイクロサービス化しました。バッチについては後述します。 CartBatchAPIが持つエンドポイントは以下のものがあります。 販売可能数を取得する参照系 販売可能数を同期、補正する更新系 その他、AWSリソース DynamoDBの更新内容はAmazon Kinesis Data Streams(以下、KDS)を用いてキャプチャし、Amazon Aurora MySQL(以下、MySQL)に保存しています。KDSにキャプチャされたデータはKinesis Client Library (以下、KCL)アプリケーションによって取り出し、MySQLに書き込みます。このMySQLのデータは後述する在庫を補正するバッチで参照されます。KCLアプリケーションの仕様の詳細についてはカートリプレイスPhase1のテックブログで触れているのでご参照ください。 techblog.zozo.com 在庫の同期・補正を行うバッチ 今まではFrontDBの在庫データをCartDBに同期していましたが、リプレイス後はFrontDBの在庫データ内の販売可能数をDynamoDBに同期するように変更しました。 同期のフローは以下のとおりです。 上述のバッチが常に稼働しているためニアリアルタイムでSQL Serverの販売可能数の変更がDynamoDBに反映されます。 しかし、DynamoDBとSQL Serverの更新処理は別トランザクションであるため、ユーザのカート操作によって以下のような不整合が考えられます。 カート投入時 DynamoDBの更新が成功したがCartDB(カートテーブル)の更新に失敗する カート削除時 CartDB(カートテーブル)の更新が成功したがDynamoDBの更新に失敗する 上記のような不整合を防ぐために不整合を検知して補正するバッチを作成しました。 バッチの仕組みとしては以下の2つの履歴から在庫変動があったデータをターゲットとします。ターゲットとなる在庫に対して、オンプレミスの販売可能数とユーザのカートに確保されている販売可能数を用いてDynamoDBの販売可能数を補正するというものです。 ユーザのカート操作の履歴(SQL Server) DynamoDBの操作履歴(MySQL) 上記2つのバッチ処理によって常にDynamoDBの販売可能数はSQL Serverの販売可能数と一致するようになります。 リリースまでの道のり 負荷試験 在庫を同期するバッチや在庫を補正するバッチの負荷試験を行いました。在庫データの更新される件数をもとに同期の件数を処理し切れるか、また同期の処理時間がどの程度かを検証しました。検証結果に応じてチューニングを行い、バッチの並列度・処理間隔を決定しました。 リリース 本番環境におけるSQL ServerからDynamoDBへの在庫データの移行はN%リリースで行いました。以下がN%リリースの手順です。 初期データの移行 ZOZOTOWN内で扱う販売可能数のN%がDynamoDBの販売可能数を参照するように変更するリリース 各ステップの詳細を以下に説明します。 1. 初期データの移行 最初のステップとして、本番環境のSQL Serverの在庫データをDynamoDBに同期しました。SQL Serverの在庫データをCSV形式で出力し、AWSのブログで紹介されているソリューションを参考にしたスクリプトを使用してDynamoDBにインポートしました。 aws.amazon.com スクリプトではBoto3(AWS SDK for Python)というPythonからAWSリソースを操作できるライブラリを使用しています。DynamoDBへの書き込みは batch_writer を使用し、1回の書き込みリクエストの上限を気にすることなく、大量のデータを効率良く処理させています。 以下はスクリプトから一部抜粋、加工したもので、動作は保証しませんのでご了承ください。 import boto3 import csv s3 = boto3.resource( 's3' ) dynamodb = boto3.resource( 'dynamodb' ) def main (s3_bucket=s3_bucket, csv_file=csv_file, dynamodb_table=dynamodb_table): obj = s3.Object(s3_bucket, csv_file).get()[ 'Body' ] table = dynamodb.Table(dynamodb_table) batch_size = 100 batch = [] for row in csv.DictReader(codecs.getreader( 'utf-8' )(obj)): if len (batch) >= batch_size: write_to_dynamo(batch, table) batch.clear() batch.append(row) if batch: write_to_dynamo(batch, table) def write_to_dynamo (rows, table): with table.batch_writer() as batch: for i in range ( len (rows)): batch.put_item( Item=rows[i] ) 今回Lambdaには以下の制約があるためスクリプトの実行環境はEC2を選択しました。 Lambdaのタイムアウトが15分 15分で処理できる件数は約100万件 対象の在庫データの件数は1500万件を超えており、また日々増え続けていました。Lambdaのタイムアウト値や並列数の計算せずにシンプルに考えるためそのような制約がないEC2を選択しました。以下が手順と実行環境の全体図です。 CartDBから在庫データをCSV形式でエクスポート CSVをS3にアップロード EC2にS3のCSVをダウンロード EC2上でスクリプトを実行してCSVの中身をDynamoDBにインポート 次に、インポート時に発生した問題とその解決方法、またインポート後に整合性を確認するために行なった作業をご紹介します。 DynamoDBのキャパシティ 対象の在庫データは1500万件以上で、SQL Serverとの差分を小さくするためできる限り短時間での同期が望まれました。そのため複数台のEC2上でスクリプトを並列実行したところ、オンデマンドモードでの書き込みキャパシティの拡張が間に合わず、スロットリングエラーが多発しました。この問題に対して以下のように解決しました。 同期前にオンデマンドモードからプロビジョニングモードに切り替える 事前にキャパシティを手動で拡張する DynamoDBはオンデマンドモードからプロビジョニングモードに切り替えた場合、24時間経過しないと再度オンデマンドモードに戻せませんのでご注意ください。 整合性の担保 同期完了後に、SQL Serverから出力したCSVファイルとDynamoDBのデータにズレが生じていないか確認しました。スクリプトでログ出力やリトライ設定などは行っているので異常時には検知やリカバリができますが、より厳密に整合性を担保するためです。まずはデータの件数が一致しているかAWS CLIで確認しました。 $ aws dynamodb scan --table-name < table-name > --select COUNT --return-consumed-capacity TOTAL 補足ですが、上記コマンドでDynamoDBの読み込みキャパシティがある程度必要になります。そのため同期前に書き込みだけでなく読み込みのキャパシティも拡張しています。 次にSQL Serverから出力したCSVファイルを正として、データ1件1件をDynamoDBのデータと照らし合わせる作業を実施しました。先述の在庫同期と同じ環境下で、こちらもスクリプトを使用しました。同期完了後はすぐにDynamoDBのテーブルとSQL Serverとの差分を補正するためのバッチ処理を実行する必要がありました。そのため照合作業は以下のように非同期で行いました。 同期した直後のDynamoDBをPITRで復元する 復元元DynamoDBはバッチ処理の対象となりデータが更新されていくためバッチ処理開始前に復元する 復元先のDynamoDBに対してCSVと差分がないか確認する 同期後のデータの件数の一致と、データ内容に差分がないことを確認し、在庫同期において整合性を担保できました。 2. リリース 当然、在庫データはZOZOTOWNでも重要なデータなので影響範囲を限定するためにN%リリースを実施しました。その際、ZOZOTOWNで扱っている全在庫をN%に区切りながらリリースすることが難しかったため、以下のように決めました。 ZOZOTOWNで扱っている特定の1ショップだけクラウド対象にする ZOZOTOWNで扱っている特定の10ショップだけクラウド対象にする ZOZOTOWNで扱っているショップのN%をクラウド対象にする ZOZOTOWNで扱っているショップの100%をクラウド対象にする というように、段階的にクラウド対象を増やしていきました。最終的にはZOZOTOWNで扱っているすべてのショップをクラウド対象にしました。 効果 本リプレイスの効果について説明します。 リプレイス前は同一の特定商品へ大量のカート投入リクエストが実行されると、在庫テーブルにおいて読み取り要求・更新要求の競合が発生していました。その結果CartDBの負荷が上昇していました。しかしリプレイス後では競合の発生を防ぎ、CartDBの負荷を抑えられました。 下図はリプレイス前後で、同一の特定商品へ大量のカート投入リクエストが行われた某日を比較しています。リプレイス後はリプレイス前より5倍近いカート投入リクエストが実行されているにもかかわらず、競合は発生せずにCartDBの負荷も上昇していないことがわかります。 他の施策との相乗効果 カート決済チームではリプレイス以外にもカート機能の改善に取り組んでいます。本リプレイスと同時期にCartDBの負荷を下げる施策として、Istio Rate Limitを用いた商品単位でのカート投入リクエスト制限を導入しています。先述した本リプレイス後の結果はこの施策との相乗効果といえます。 Istio Rate Limitについては注文リクエストの制限に導入した事例が公開されているのでご覧ください。 techblog.zozo.com さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに ML・データ部推薦基盤ブロックの佐藤( @rayuron )です。私たちはZOZOTOWNのパーソナライズを実現するために、機械学習モデルとシステムを開発・運用しています。本記事ではクーポン推薦のための機械学習モデルとシステム改善に取り組んだ話を紹介します。 はじめに 背景 課題 1. 古い基盤でシステムが運用されている 2. KPIに改善の余地がある 3. 機械学習モデルの評価体制がない 課題解決のために 1. Vertex AI Pipelinesへの移行 2. Two-Stage Recommenderの導入 プロジェクトへの導入 Candidate Generation 1. 過去の実績 2. 人気ブランド 3. 興味を持っているブランドの類似ブランド 評価方法 Reranking 学習データの作成 アンダーサンプリング 特徴量エンジニアリング 学習 バリデーション 推論 3. 機械学習モデルの評価と体制整備 オフライン評価 メトリクスのオフライン評価 過去の施策を活用したオフライン評価 定性評価 実験管理の導入 結果 運用業務の簡易化 KPIの改善 リリースサイクルの改善 今後の展望 バイアスの考慮 評価体制の改善 評価指標の探求 定性評価の体制整備 おわりに 背景 ZOZOTOWNでは一部の提携するショップで使用可能なクーポンが発行されます。クーポンが発行されていることをユーザーに知らせるため、メールやPush通知のチャネルを通してクーポン対象のショップとアイテムの訴求を行なっています。以下はメール配信のイメージ画像です。 クーポン対象となるショップおよびアイテムが決まっている条件下において、メール配信の対象者と推薦ショップ、推薦アイテムを決定するために機械学習モデルとシステムを運用しています。以降、この機械学習モデルをクーポン推薦モデルと表現して紹介します。 課題 既存のクーポン推薦モデルとシステムには以下の課題がありました。 1. 古い基盤でシステムが運用されている 弊社の機械学習パイプラインはVertex AI Pipelinesを標準としているのですが、既存のクーポン推薦のパイプラインは標準化前のCloud Composerで運用されていました。Vertex AI Pipelinesは運用を簡易化するために内製の整備が進んでいる一方、Cloud Composerは整備されていないため運用業務が煩雑で時間を要します。さらにCloud Composerで機械学習パイプラインを作成している社員がほぼいないので属人性が高いという課題がありました。 2. KPIに改善の余地がある 前述の様にクーポン推薦モデルでは配信対象者の選定、ショップ推薦、アイテム推薦という3つの観点を考えます。 各観点に対する既存のアプローチは以下の通り大部分がルールベースのロジックで構成されているため、機械学習モデルに置き換えることでKPIを改善できると考えられます。 推薦の観点 既存のアプローチ 配信対象者の選定 Matrix Factorizationとルールベース ショップ推薦 ルールベース アイテム推薦 ルールベース 3. 機械学習モデルの評価体制がない 既存システムでも配信対象者の選定で機械学習モデルが利用されていますが、その当時は現在ほどMLOpsが整備されていなかったため、機械学習モデルを評価する体制がありませんでした。モデルやデータに変更がある際に精度の変化を確認できないためモデルの改善サイクルを回しにくく、意図せず低品質なモデルをデプロイしてしまう危険性がありました。 課題解決のために 上記の課題解決のために以下の3つに取り組みました。 Vertex AI Pipelinesへの移行 Two-Stage Recommenderの導入 機械学習モデルの評価と体制整備 1. Vertex AI Pipelinesへの移行 既存のシステムはCloud Composer上で機械学習パイプラインを構築していたため、現在弊社で機械学習基盤として利用されているVertex AI Pipelinesへ移行しました。移行に際し、 2021年に公開した「Kubeflow PipelinesからVertex Pipelinesへの移行による運用コスト削減」という記事 で今後の展望として挙げられていたパイプラインテンプレートを利用しています。パイプラインテンプレートは GitHubのテンプレートリポジトリ機能 を利用し、Vertex AI Pipelinesの実行・スケジュール登録・CI/CD・実行監視等をテンプレート化したものです。以下のテックブログでもVertex AI Pipelinesの導入事例についてご紹介しているので是非ご覧ください。 techblog.zozo.com techblog.zozo.com 2. Two-Stage Recommenderの導入 KPIを改善するために、推薦分野で広く利用されている Two-Stage Recommender という推薦手法を導入します。このモデルアーキテクチャは推薦に関するコンペティションの上位解法でよく使用されており、最近だとKaggleの H&M Personalized Fashion Recommendations などで使用されていました。その名の通りCandidate GenerationとRerankingという2つのステージで構成されています。ユーザーとアイテムの全ての組み合わせに対するスコア計算が困難な場合に、ユーザーとアイテムに対するスコアの計算を一部の組み合わせに限定できます。 Two-Stage Recommenderは以下の工程で推薦を実現します。 Candidate Generationステージで、ユーザーと関連度の高いアイテムの集合を取得する Rerankingステージで、取得したアイテムをユーザーの関心度に基づいて並べ替える プロジェクトへの導入 本プロジェクトでは前述の3つの推薦の観点を以下の様に変更します。 推薦の観点 変更前のアプローチ 変更後のアプローチ 配信対象者の選定 Matrix Factorizationとルールベース Two-Stage Recommender ショップ推薦 ルールベース Two-Stage Recommender アイテム推薦 ルールベース ショップ推薦を考慮したルールベース 配信対象者の選定とショップ推薦では同一のTwo-Stage Recommenderを使用しています。配信対象者の選定ではユーザー毎にTwo-Stage Recommenderのスコアの最大値を集計し、集計値が高い上位Nユーザーを配信対象とします。ショップ推薦ではTwo-Stage Recommenderのスコアが大きい順にショップを掲載します。アイテム推薦ではショップ推薦を考慮したルールベースのロジックを使用しますが、本記事では取り上げません。 ZOZOTOWNに出店しているショップは複数のブランドを取り扱うことがあるため、同じショップ内でもブランドによってユーザーの興味は異なるという仮説をモデルに反映します。以上を踏まえて、以下では本プロジェクトのTwo-Stage Recommenderの詳細について説明します。 Candidate Generation Candidate Generationステージではユーザー、クーポン対象ショップ、取り扱いブランド、日付の組み合わせを取得します。 以下の3つの軸を考慮してCandidate Generationを行いました。 過去の実績 人気ブランド 興味を持っているブランドの類似ブランド 1. 過去の実績 リターゲティング施策はコンバージョンにつながりやすいことが分かっているため、ユーザーのお気に入りブランドと過去N日間で閲覧したアイテムのブランドを推薦候補とします。 2. 人気ブランド トレンドを考慮した推薦をするため、性年代別の人気ブランドの上位N件を取得しユーザーに紐付けます。 3. 興味を持っているブランドの類似ブランド 多様性のある推薦をするため、ユーザーが閲覧したブランドを元に以下の手順で類似ブランドを取得します。 ユーザーのブランドに対する行動を学習データとしたMatrix Factorizationモデルを学習 学習の中間生成物であるブランドのEmbeddingを取得 Embeddingから全ブランド同士のコサイン類似度を計算 ライブラリは LightFM を使用しています。指定可能なLogistic、BPR、WARPのロスを変えた時の精度を比較し、最終的には次項で説明する評価指標が最も良かった WARPロス を使用しました。 評価方法 Candidate Generationの評価は後述する 全レコード数に対するRerankingモデルの正例の割合 を使用します。使用データの日数や人気ブランドの取得件数などのパラメータは正例の割合と推薦に必要な最低ユーザー数を考慮して決定します。 Reranking RerankingステージではCandidate Generationステージで取得した組み合わせを使用して、ユーザーが興味を持つ順番でクーポン対象ショップ、取り扱いブランドを並び替えます。 学習データの作成 ユーザーのブランドへの興味を数値化しランク付けをしたいのですが、興味を直接数値化することは困難です。そこで、ユーザーのブランドへの興味を アイテム閲覧データを用いて表現 します。閲覧アイテムに紐づくブランドを取得し、アイテム閲覧データをブランド閲覧データに変換します。これをCandidate Generationで取得したデータに紐付けることでZOZOTOWN会員が次の日にあるブランドを閲覧することを正例とした学習データを作成します。ブランド推薦をメール配信以外でも展開したいという要件があったため、クーポン施策に関わるユーザー行動を正例とするのではなくZOZOTOWN全体のユーザー行動を正例としています。 アンダーサンプリング 前述の通りに学習データを作成するとCandidate Generationで取得したレコードでは正例の割合が1〜2%程度となります。 学習時間の短縮と不均衡データへの対処を目的 とし、学習と検証データに負例のアンダーサンプリングを行いました。後述するオフライン評価指標が低下しないことを確認し、最終的に正例:負例 = 1:1を採用します。 特徴量エンジニアリング 特徴量としてユーザーとアイテムのマスタデータに加えてユーザーの行動データを用います。行動データとはZOZOTOWN上での注文、検索、カートイン、お気に入り、閲覧などのデータです。特徴量は全てBigQueryを使用して作成します。 ユーザー軸 デモグラフィック属性 価格に関わる指標 ブランド軸 ユーザー行動を集計した指標 価格に関わる指標 クーポン付与ポイント Candidate Generationで使用したモデルのEmbeddingを主成分分析した第N主成分 ユーザーとブランド軸 ユーザー行動を集計した指標 価格に関わる指標 ブランドロイヤルティを表す指標 特に特徴重要度が高かった特徴量は以下です。 価格に関わる特徴量 Embeddingを主成分分析した特徴量 学習 推薦モデルの学習には LightGBM を使用し、スコアが0〜1の範囲に収まる様に二値分類をします。これはスコアが閾値以上の時に配信対象者とすることで将来的にはクーポンに興味があるユーザーのみに配信を送りたいという要件のためです。 バリデーション Time Series Hold outによって検証データを作成します。最新のN日間のデータを取得し、約20〜30%が検証データとなります。アーリーストッピングを設定し検証データに対するロスが最善の時点で学習を終了します。 推論 Dataflow を使用して並列推論を行います。実験中に推論の実行時間に課題感を感じていましたが、導入後はテストデータと推薦対象データを対象に100GB超のデータに対して約20分で推論が完了します。 3. 機械学習モデルの評価と体制整備 上記の機械学習モデルを評価するためにオフライン評価の体制を整備します。具体的にはオフライン評価と実験管理の導入について紹介します。 オフライン評価 オフライン評価は定量評価と定性評価の2軸で評価をし、特に定量評価ではメトリクスの評価と過去の施策を活用したオフライン評価をします。 メトリクスのオフライン評価 テストデータに対するモデルメトリクスが向上していることを確認すると同時にvalidation lossやユーザーユニーク数などのメトリクスが異常ではないかを確認します。具体的には以下のメトリクスを確認します。 テストデータに対するprecision@k テストデータに対するrecall@k train loss validation loss 学習/推薦ユーザーのユニーク数 学習/推薦ブランドのユニーク数 過去の施策を活用したオフライン評価 過去の施策によって変更前、変更後のモデルに関わらないメール配信者やメール経由サイト流入者のデータが手に入っていました。このデータを活用することで以下の指標を擬似的に再現し、変更前のモデルと変更後のモデルを評価します。 N万位以内のユーザーの配信流入率 メール経由流入者がN万位以内である割合 定性評価 定量評価に加えて、定性評価をチームメンバーに行なってもらいます。具体的には各自のZOZOTOWNアカウントを用いて推薦結果に対するフィードバックをしてもらいます。以下の様なフィードバックをもらい、モデル改善の方向性を決定する際の参考にしました。 変更前の推薦は人気ブランドに偏っているが変更後の推薦はブランドのバリエーションが増えていて良い ライトユーザーは変更前の推薦を好むが、ミドル/ヘビーユーザーは変更後の推薦を好みそう 変更後の推薦は未知のブランドが多いが、その中に興味を持っているブランドがある時はかなり嬉しい 実験管理の導入 変更前の機械学習モデルは評価指標を保存する場所がなく他のモデルとの比較ができませんでした。対策として、オフライン評価指標の保存と参照を簡易化するために実験管理ツールである MLflow を導入します。パラメータはパイプラインの実行時に保存され、メトリクスは学習や評価の際にMLflowへ保存します。保存したデータは以下の画像のように確認できます。メトリクスにはダミーの値を入れています。 ▼ パラメーター ▼ メトリクス ▼ メトリクスの推移 結果 運用業務の簡易化 今回のシステム移行によってVertex AI Pipelinesを利用した他システムと同様の方法で 簡単に運用業務が行える ようになりました。また、Cloud Composerを使用した機械学習モデルの運用が全社的に終了し Vertex AI Pipelinesへと完全移行 できました。 KPIの改善 配信対象者の変更とショップ推薦の変更に対して実施したA/Bテストではcontrolと比較して KPIが以下の様に改善 しました。絶対数を表す指標は比率を、割合を表す指標は差を示しています。 KPI 配信対象者の変更 ショップ推薦変更 売上 124.69 % 104.02 % 1配信あたりの売上 123.81 % 104.40 % 注文者数 120.08 % 103.76 % 配信流入率 + 0.12 pt + 0.01 pt 配信注文率 + 0.004 pt + 0.001 pt リリースサイクルの改善 MLflowの導入によってモデルの精度比較が簡単にできるようになったため、 モデル改善のサイクルが早まりました 。また、モデルに関わらない変更をリリースする際も精度が低下していないことを確認できるため リリースの安全性が向上しました 。 今後の展望 バイアスの考慮 現在はアイテムを閲覧したブランドを学習データとしてショップ推薦をしています。このデータは推薦経由であっても記録されるため推薦モデルが自己の学習データに影響を与えることになります。推薦したブランドは再び推薦されやすくなるというバイアスを生み出しているため、このバイアスを考慮した設計をする必要があります。 評価体制の改善 評価指標の探求 現在の評価指標は推薦の精度に着目したものが多いです。一方で推薦には多様性、新規性、意外性等の精度以外にも考慮すべき指標が提案されています。弊社でも長期的な推薦の価値を捉えた指標を明らかにし、モニタリングする必要があると考えています。 定性評価の体制整備 今回のプロジェクトでは定量評価の体制を整備しましたが、定性評価がモデル改善の方針の参考になることが多かったため、定性評価の方法も標準化することでモデル改善を加速させたいと考えています。 おわりに 本記事ではクーポン推薦モデルとシステムの改善について紹介しました。現在ZOZOでは 「ワクワクできる『似合う』を届ける」 という経営戦略のもとパーソナライズを強化している最中です。推薦を含め機械学習に関わるエンジニアを募集しているので、ご興味がある方は是非以下のリンクからご応募ください。 hrmos.co hrmos.co
はじめに こんにちは。DevRelブロックの @wiroha です。1月23日に「 ZOZO Tech Meetup - Android 」を開催しました。ZOZOのモバイルアプリエンジニアがAndroidの開発話をするオンラインイベントです。 登壇内容まとめ 弊社から次の5名が登壇しました。 コンテンツ 登壇者 CoroutineExceptionHandlerと仲良くなる 愛川功樹 Compose Multiplatform for iOS開発でぶつかった壁 井上晃平( @nemo-855 ) Gradle超入門 財部彰太( @rabe_hamuyatti ) Jetpack Composeで様々な画面サイズに対応する 田中崇裕 Jetpack ComposeへのリファクタリングのTIPS: CustomViewの便利な使い方 山田尚吾( @yshogo87 ) 当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com CoroutineExceptionHandlerと仲良くなる speakerdeck.com 愛川からはCoroutineExceptionHandlerについて紹介しました。Coroutinesの例外伝播やCoroutineExceptionHandlerの実体・呼ばれるまでの流れなどを説明しており、詳しい理解を助ける内容でした。Coroutineの実装を見ていくことで、普段コードを書く上でも内部のイメージをしやすくなったとのことです。 Compose Multiplatform for iOS開発でぶつかった壁 speakerdeck.com 井上はCompose Multiplatform for iOSを用いて、個人でiOSアプリとAndroidアプリを作成したそうです。開発する中でぶつかった課題とその解決法を紹介しました。TextField、Context、権限リクエストといったOS間の差異を吸収する工夫はとても良い知見だと感じました。 Gradle超入門 speakerdeck.com 財部からはGradle入門を発表しました。GradleはAndroid開発を行う上でほぼ必ず扱うことになるので、改めて理解を深める良い機会だと感じました。応用すると依存関係の問題の回避や、ビルド時間の短縮にも結びつくので、継続して学んでいきたいですね。 Jetpack Composeで様々な画面サイズに対応する speakerdeck.com 田中からは、さまざまな画面サイズへ対応するハードルを下げるべく、理解を深めていくための発表を行いました。ナビゲーションとペインの出し分けを実装することで、さまざまなサイズに対応します。具体的なコードが多く出てきて、対応時のイメージがわきやすくなっていました。 Jetpack ComposeへのリファクタリングのTIPS: CustomViewの便利な使い方 speakerdeck.com 山田からはリファクタリングを進めるためのTIPSを発表しました。複雑なレイアウトをいきなりJetpack Composeへリファクタリングするのではなく、少しずつ手順を踏みます。まずはレイアウトを分割し、分割したレイアウトのCustomViewを作り、CustomViewの中だけをJetpack Composeにリファクタリングします。これにより差分が小さくなるため細切れの時間を活用でき、不具合に気づきやすくなるメリットもあるとのことでした。 最後に 今回はAndroidに関するさまざまな知見を発表しました。みなさまご参加ありがとうございました。各発表の質疑応答の時間で多数の質問をいただきましたので、ぜひアーカイブをご覧ください。 ZOZOではAndroidを活用し、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、ZOZOTOWN開発2部Androidブロックの大江です。普段はZOZOTOWN Androidの開発を担当しています。 ZOZOTOWN Androidはリリースから10年以上経過し、現在のソースコードは9年近く開発されています。そのため、複数のアーキテクチャが混ざった状態になっていて、開発速度の向上を妨げる要因になっていました。 そこで今ZOZOTOWNにある3つのAndroidブロックから、それぞれテックリードなどの代表者を立て、アーキテクチャを統一するための座談会を週に1度開催しています。その座談会の成果としてまずはViewModel部分の実装方針を決めることができました。 本記事では座談会で決められたViewModelの実装方針と狙いをご紹介します。 目次 はじめに 目次 ViewModelの実装方針を決めるための要件 サンプルの説明 ViewModelの実装方針の紹介 UI StateとEffect Event ViewModelに実行してほしい処理の内容を表すようにEventを定義する案 UIの操作をそのまま表すようにEventを定義する案 Android ViewからJetpack Composeへの移行 まとめ ViewModelの実装方針を決めるための要件 ViewModelの詳細な実装方針を検討するにあたって、まずは現状のZOZOTOWN Androidが抱えている課題を整理し、それらを解決するための要件を決定しました。 具体的には以下の要件に基づいてViewModelの実装方針を決めることにしました。 UIの実装をシンプルにする 一般的にUIのテストを実装するコストは大きく、ZOZOTOWN AndroidでもUIのテストは十分に実装できていない UIの実装をシンプルにすることでテストが実装でき、それによって実装ミスを減らしたい 機能やレイアウトの改修を簡単に行える ZOZOTOWN Androidは大人数かつ複数のチームが同じ機能や画面を改修することがある その際のコンフリクトの発生や改修自体のコストを減らしたい UIのFrameworkに依存しない ZOZOTOWN AndroidはAndroid ViewとJetpack Composeが混在している Android ViewをJetpack Composeに置き換えていく予定だが、その際にViewModelは変更しなくて済むようにしたい サンプルの説明 今回のViewModelの実装方針を紹介するため、サンプルのお知らせ一覧画面を用意しました。 お知らせをタップしたらお知らせ詳細画面に遷移または外部ブラウザを起動するような仕様を想定します。 ViewModelの実装方針の紹介 ViewModelとUI双方向の処理の流れを明確にして、ViewModelの実装方針を決めました。 ViewModelからUIに向かう処理はUI StateとEffectとして実装する UIからViewModelに向かう処理はEventとして実装する それぞれについて解説します。 UI StateとEffect UI StateはUIに表示する内容を1つにまとめた値を表すdata classであり、StateFlowで管理します。 この実装は Android Developersのアプリ アーキテクチャ ガイドのUI レイヤのページ で紹介されている内容に則っています。 しかし、アプリ アーキテクチャ ガイドで紹介されている画面遷移などのイベントをUI Stateで管理するという部分は採用しませんでした。 ZOZOTOWN Androidでは画面遷移などのイベントはUI Stateとは別にEffectというsealed classを定義してSharedFlowで管理する方針を採用しました。 画面遷移などのイベントはSharedFlowで管理した方がシンプルな実装になるためです。 例えば、次のような場合にStateFlowを用いると値をクリアする必要がありますが、SharedFlowを用いると値をクリアする必要はありません。 同じイベントを連続で実行したい場合 画面の再生成時にイベントを再発火させたくない場合 お知らせの一覧を表示する画面のAndroid Viewを使ったサンプルコードを載せておきます。 data class InformationListUiState( val informationList: List <Information> ) sealed class InformationListEffect { data object ShowInformationDetail : InformationListEffect() data object OpenExternalBrowser : InformationListEffect() } class InformationListViewModel : ViewModel() { private val _uiState = MutableStateFlow(InformationListUiState(emptyList())) private val _effect = MutableSharedFlow<InformationListEffect>() val uiState = _uiState.asStateFlow() val effect = _effect.asSharedFlow() } class InformationListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { uiState -> // Adapterを更新してRecyclerViewでお知らせの一覧を表示する adapter.update(uiState.informationList) } } } viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.effect.collect { effect -> when (effect) { InformationListEffect.ShowInformationDetail -> { // 詳細画面に遷移する } InformationListEffect.OpenExternalBrowser -> { // 外部ブラウザを起動する } } } } } } } Event EventはUIの操作をViewModelに伝えるためのinterfaceです。 ViewModelにはEventを受け取るための dispatch() を定義します。 EventにはViewModelの拡張関数として consume() を定義して、 dispatch() の内部では consume() を実行するだけにしました。 class InformationListViewModel : ViewModel() { fun dispatch(event: Event) { event.run { this @InformationListViewModel.consume() } } interface Event { fun InformationListViewModel.consume() } data object Event1 : Event { override fun InformationListViewModel.consume() { // Event1がdispatchされたときに実行したい処理 } } data object Event2 : Event { override fun InformationListViewModel.consume() { // Event2がdispatchされたときに実行したい処理 } } } このような実装にすることでレイアウトや機能を改修する際には、そのレイアウトや機能に対応するEventのみを変更すればよく、他のEventに干渉することなく安全に作業できます。 次にEventの定義の粒度について説明します。 Eventをどのような粒度で定義するかについて、次の2つの案が挙がりました。 ViewModelに実行してほしい処理の内容を表す UIの操作をそのまま表す この2つの案について検討した結果、UIの実装をシンプルにするという観点からUIの操作をそのまま表す案を採用することにしました。 ViewModelに実行してほしい処理の内容を表すようにEventを定義する案 class InformationListViewModel : ViewModel() { fun dispatch(event: Event) { event.run { this @InformationListViewModel.consume() } } interface Event { fun InformationListViewModel.consume() } data class ShowInformationDetail( val informationID: InformationID) : Event { override fun InformationListViewModel.consume() { // お知らせ詳細画面に遷移する viewModelScope.launch { _effect.emit(InformationListEffect.ShowInformationDetail) } } } data class OpenBrowser( val url: String ) : Event { override fun InformationListViewModel.consume() { // 外部ブラウザを起動する viewModelScope.launch { _effect.emit(InformationListEffect.OpenExternalBrowser) } } } } class InformationListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { adapter.setOnItemClickListener { index -> val information = _uiState.value.informationList[index] if (information.isExternalLink) { viewModel.dispatch(InformationListViewModel.OpenBrowser(information.url)) } else { viewModel.dispatch(InformationListViewModel.ShowInformationDetail(information.informationID)) } } } } UIの操作をそのまま表すようにEventを定義する案 class InformationListViewModel : ViewModel() { fun dispatch(event: Event) { event.run { this @InformationListViewModel.consume() } } interface Event { fun InformationListViewModel.consume() } data class OnItemClick( val index: Int ) : Event { override fun InformationListViewModel.consume() { val information = _uiState.value.informationList[index] if (information.isExternalLink) { // 外部ブラウザを起動する viewModelScope.launch { _effect.emit(InformationListEffect.OpenExternalBrowser) } } else { // お知らせ詳細画面に遷移する viewModelScope.launch { _effect.emit(InformationListEffect.ShowInformationDetail) } } } } } class InformationListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { adapter.setOnItemClickListener { index -> viewModel.dispatch(InformationListViewModel.OnItemClick(index)) } } } それぞれの案の実装を比較してもらうと後者のUIの操作をそのまま表す案の方がFragmentの実装がシンプルになっていることがわかると思います。 アプリ アーキテクチャ ガイドではViewModelのfunの名前が前者の案であるViewModelに実行してほしい処理の内容を表しています。しかしZOZOTOWN AndroidではUIの実装をシンプルにするため、後者のUIの操作をそのまま表す案を採用しました。 Android ViewからJetpack Composeへの移行 Android ViewからJetpack Composeへ移行する場合は以下のようにUI部分を変更するだけです。 以下の理由でViewModelの実装を変更する必要はありません。 UI State、Effect、EventがAndroid ViewやJetpack Composeに関連するクラスを使用していない UI State、Effect、EventがAndroid ViewやJetpack Composeを連想させる命名になっていない @Composable fun InformationListScreen( viewModel: InformationListViewModel, ) { val uiState by viewModel.uiState.collectAsState() val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(viewModel, lifecycleOwner) { viewModel.effect .flowWithLifecycle(lifecycleOwner.lifecycle) .onEach { effect -> when (effect) { InformationListEffect.ShowInformationDetail -> { // 詳細画面に遷移する } InformationListEffect.OpenExternalBrowser -> { // 外部ブラウザを起動する } } } .launchIn( this ) } InformationListContent( uiState = uiState, dispatch = viewModel :: dispatch, ) } @Composable fun InformationListContent( uiState: InformationListUiState, dispatch: (InformationListViewModel.Event) -> Unit , ) { // LazyColumnでお知らせの一覧を表示する LazyColumn { itemsIndexed(uiState.informationList) { index, information -> // お知らせの項目を表示する InformationItem( information = information, onClick = { dispatch(InformationListViewModel.OnItemClick(index)) } ) } } } まとめ 本記事ではZOZOTOWN Androidで採用することになったViewModelの実装方針を紹介しました。このViewModelの実装方針をチーム全体に浸透させることで開発速度の向上を期待しています。AndroidでViewModelの実装方針を検討している方がいれば、ぜひ参考にしてみてください。今後はUseCaseやRepositoryについても実装方針を決めていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、ZOZOTOWN開発1部iOSブロックの荻野です( @juginon )。 WWDC19でSwiftUIが発表されてから今年で5年になりますが、みなさんの携わっているプロジェクトではSwiftUIを使っていますか。ZOZOTOWN iOSチームでは、2023年11月にリリースしたアイテムレビュー機能 1 へ向けSwiftUIを積極的に導入する方針を定め、新規画面及びUI要素ではSwiftUIでの実装に取り組みました。 巨大なプロジェクトであるZOZOTOWN iOSにSwiftUIをどのように導入していったのか? 本記事では、実際にプロダクトコードに導入したことで見えた問題点、そこで得た知見を踏まえた上でのSwiftUIの導入方針についてご紹介します。 ZOZOTOWN iOSへのSwiftUI導入の背景 旧方針:限定的な範囲で小さく導入する UIViewで書いている部分はSwiftUIで実装する SwiftUIの標準APIで実現できないことがあるケースではUIKitを使用する 実際に導入した結果うまくいかなかった どう解決したか 新方針:小ささの粒度を柔軟に変えながら導入する まとめ ZOZOTOWN iOSへのSwiftUI導入の背景 ZOZOTOWN iOSへSwiftUIを導入した主な目的は、チーム全体でSwiftUIへの知見を増やすためです。今までもZOZOTOWN iOSにSwiftUIを導入した前例はありましたが、メンバー個人での挑戦に留まっていました。取り組んだメンバー同士で振り返ってみて、巨大で複雑なZOZOTOWN iOSのコードの上でSwiftUIの知見を増やすには、メンバー個人ではなくメンバー全員で取り組んだ方が良いと判断しました。そこで、ZOZOTOWN iOSにSwiftUIを導入するチームとしての方針を決めることにしました。SwiftUIへの挑戦や実際に発生した問題の共有をチームメンバー全員で取り組むことで、チームとして実践的な知見を増やせると期待しました。 また副次的な効果として、Previews in Xcodeを積極的に使うことでデザイン仕様の実装漏れを防止する期待もあります。機能要件の複雑さゆえにデザイン仕様の見落としが少なからずあるので、デザイン仕様から考えられる複数のユースケースをPreviewsで確認しながら実装できると仕様の見落としに気づきやすくなるでしょう。 これらの背景から、ZOZOTOWN iOSではSwiftUIの導入が検討されるようになりました。まずは導入方針を決めるにあたってどのような意思決定がされたのかについてご紹介します。 旧方針:限定的な範囲で小さく導入する 導入の方針を決める際は、画面単位で一気にSwiftUIを導入していくのか、コンポーネントにわけて小さく導入していくのか、大きく分けて2つの選択肢を考えました。チームの議論では以下のような意見が出ました。 一気に画面全体をSwiftUI化することはコードの規模や時間的に難しい ZOZOTOWN iOSでは複数の案件が同時並行で走っており、影響範囲が大きい改修を入れるのは現実的でない。 もし新規で画面を作る場合でも、問題が発生した際にUIKitの実装へフォールバックさせる判断をしたときのダメージが大きい。 ユーザーに価値を届けることを最優先にするが、技術的な挑戦もないがしろにするべきではない まずは小さく始めて導入に際しての懸念点を探し、チーム内で知見を増やすべき。 上記のように、最初は限定的な範囲で小さくSwiftUIを導入し、徐々にその適用範囲を広げていくことでチームのSwiftUIに対する理解と経験を速いサイクルで段階的に深めていこう、と決まりました。 具体的には次のアクションに沿ってSwiftUIをチームで導入することにしました。 UIViewで書いている部分はSwiftUIで実装する UIKitで実装することを考えたとき、UIViewとして書くであろう部分はSwiftUIのViewで実装します。具体的には以下のような例が考えられます。 UIViewController内で表示するビューをSwiftUIのViewで実装する UIViewController内で表示するビューはUIViewのままで、ビューを構成する各コンポーネントをSwiftUIのViewで実装する UIViewControllerの中でSwiftUIのViewを表示するときは UIHostingController を使用します。親のUIViewControllerにSwiftUIのViewを持つUIViewControllerを追加する形で表示します。 とはいえ、SwiftUIの標準APIだけでは実現できないケースもありえることはチームでも話に上がっていたため、次のアクションも合わせて決めました。 SwiftUIの標準APIで実現できないことがあるケースではUIKitを使用する 例として、リスト表示におけるSwiftUIとUIKitの実装を考えます。ZOZOTOWN iOSのUIデザインはSwiftUIのGridでは実装が難しく、UICollectionViewでないと表現しづらい複雑なレイアウトやデザインになっていることもあります。 この例に限らず、現状ZOZOTOWN iOSにおける様々なデザイン仕様に対してSwiftUIの標準APIだけで実現するには難しい状況です。 そのため、SwiftUIだけでは実現できない画面やコンポーネントに関してはUIViewを利用します。 実際に導入した結果うまくいかなかった アイテムレビュー機能では、上記の方針をもとに実際にいくつかの画面でSwiftUIのビューを作成しました。 最初に、新規作成されたレビュー詳細画面におけるSwiftUIでの実装予定と最終的なリリース時点での実装を比較してみましょう。図中の赤枠部分がSwiftUIでの実装です。 実装予定では4つのコンポーネントをSwiftUIで実装する計画を立てていましたが、実際に計画通り実装できたのは上2つの赤枠のコンポーネントのみとなりました。 実装予定 リリース時点での実装 計画通りにSwiftUIで実装できなかったコンポーネントは次の3つです。1つ目はユーザーの投稿を表示しているセル、2つ目はユーザーのリアクションを表示するビュー、3つ目は星評価のビューです。 技術的な課題の複雑さとプロジェクトのスケジュールの両方を考慮し、1つ目と2つ目についてはやむを得ずUIKitの実装へと戻しました。3つ目の星評価のビューについては不具合の発生条件があったため、原因を調査して対応した結果SwiftUIのまま実装できました。 この星評価のビューの対応について詳しくご紹介します。星評価のビューは以下の図の赤枠で使用されています。 赤枠のうち上から2つのビューは正しく表示されましたが、一番下の赤枠のみ星が表示されなくなる不具合を発見しました。 なぜ他の部分ではうまく表示できてこの箇所のみうまく表示できないのか、詳しく原因を調査することにしました。 左図: レビューセルの構成 右図: 不具合のイメージ この画面は UICollectionView がベースとなっており、そこに各セクションのビューを表示しています。 アイテムレビューのユーザー投稿は UICollectionViewCell で実装されています。セルの中で表示している星評価はSwiftUIのViewで作成したため、 UIHostingController を使用して表示していました。 このとき、以下の特定の状況において星評価のビュー自体が表示されませんでした。 レビューの投稿一覧のうち、上から2番目のレビューの星のみが表示されない iOS 14系、 iOS 16系では再現せず、iOS 15系でのみ再現が確認できる ファーストビューに表示されるセルの位置関係によって表示されない場合がある どう解決したか 不具合が起きているビューの特徴として、 UICollectionViewCell の内部で表示していることに着目しました。 同様の問題について言及している Apple Developer Forums を確認しました。公式の回答によると、 UIHostingController を UICollectionViewCell の内部での使用はサポートされてないようです。 Embedding a UIHostingController inside of cells is not officially supported, and you may run into various issues doing this. 本来であればこの段階でUIKitでの実装へ戻すアクションを取るべきです。 しかし、チーム内で調査を進めていった結果、今回の問題は safeAreaInsets がSwiftUIによって意図せず設定され、レイアウト崩れが発生していることが原因であるとわかりました。 ビュー自体は正常に追加されていたものの、 safeAreaInsets が設定されていたことで星を描画するサイズを確保できていなかったようです。 safeAreaInsets をゼロにすることで解決できました。 表示されない場合 表示される場合 iOS 16.4以降の場合、 UIHostingController の safeAreaRegions 2 を remove(.all) することでこの問題を解決できました。しかし、アイテムレビューリリース時のZOZOTOWN iOSはiOS 14以降をサポート対象OSとしていたため、iOS 16.4未満でも safeAreaInsets をゼロにする必要がありました。 iOS 16.4未満の場合には、独自で作成した SafeAreaRemovedHostingController を使用しました。 SafeAreaRemovedHostingController は safeAreaInsets をゼロにしたUIViewを持つ UIHostingController です。 import SwiftUI import UIKit final class SafeAreaRemovedHostingController < View : SwiftUI.View >: UIHostingController < View > { private class WrapperView : UIView { override var safeAreaInsets : UIEdgeInsets { .zero } } private let wrapperView = WrapperView() override func loadView () { super .loadView() view.backgroundColor = .clear view.translatesAutoresizingMaskIntoConstraints = false wrapperView.addSubview(view) NSLayoutConstraint.activate([ wrapperView.leadingAnchor.constraint(equalTo : view.leadingAnchor ), wrapperView.trailingAnchor.constraint(equalTo : view.trailingAnchor ), wrapperView.topAnchor.constraint(equalTo : view.topAnchor ), wrapperView.bottomAnchor.constraint(equalTo : view.bottomAnchor ) ]) view = wrapperView } } SafeAreaRemovedHostingController を用いて以下のようにOSバージョンによって出し分けました。これによってZOZOTOWN iOSでサポートしているすべてのOSバージョンで星評価のビューを問題なく表示できました。 let starView : UIView = { if #available(iOS 16.4 , * ) { let hostingController = UIHostingController(rootView : starRatingView ) hostingController.safeAreaRegions.remove(.all) return hostingController.view } else { return SafeAreaRemovedHostingController(rootView : starRatingView ).view } }() UICollectionView や UITableView の中でSwiftUIを表示する仕組みとしては、WWDC22で UIHostingConfiguration が紹介されています 3 。 UIHostingConfiguration の導入はiOS 16から可能となり、より簡潔に不具合なく実装できることが期待されます。ZOZOTOWN iOSのサポートバージョンがiOS 16以上へと上がったタイミングで上記の出し分けは消え、 UIHostingConfiguration へと移行する予定です。 新方針:小ささの粒度を柔軟に変えながら導入する 小さく導入するという旧方針に従って実際にSwiftUIで実装した結果、場合によって小ささの粒度を柔軟に変える必要がありました。これを踏まえて、ZOZOTOWN iOSにおけるSwiftUI導入のアクションは以下のように変更しました。 UIViewのサブビューにコンポーネントが複数ある場合、コンポーネントはSwiftUIかUIKitのどちらかに統一する UIHostingConfiguration を使えるようになるまでは UICollectionViewCell や UITableViewCell でSwiftUIを使わない SwiftUIで実装する際には、UIKitの実装へと戻す可能性も考えたリソース配分・スケジューリングを行う すでに上記のアクションを基に画面単位でSwiftUIへのリファクタリングをおこなった部分もあり、これからもこの方針でSwiftUIの導入を進めていきます。 まとめ 本記事ではZOZOTOWN iOSにSwiftUIを小さく導入するに至った経緯、実際に導入したことで見えてきた問題点、そこで得た知見を踏まえた上でのSwiftUIの導入方針をご紹介しました。 本記事で具体例として挙げたような問題は他にも存在する可能性がありますが、チームとしては都度調査し、SwiftUIとの共存を目指しています。今回のアイテムレビュー機能で小さく始めた経験は、チームにとって重要な知見を得る機会となりました。 個人開発なら時間をかけさえすれば解決できるような問題も、実際に仕事として進めていく中で直面すると臨機応変に対応しなければなりません。この経験はチームの文化としてSwiftUIを導入しようとしなければわからなかったことで、個人的には貴重な経験でした。 そういった短い時間での意思決定と技術的挑戦のバランス感を今後も養っていき、よりお客様に価値を提供できるといいなと思っています。 ZOZOTOWN iOSチームは今回の例に限らず、技術導入に積極的に挑戦しつつも、常にお客様に価値を提供することを最優先に考えています。 ZOZOTOWN iOSチームでは、そのような価値観を持っている方と一緒にサービスを作り上げたいと思っています。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co https://corp.zozo.com/news/20231130-zozotown-itemreview/ ↩ https://developer.apple.com/documentation/swiftui/safearearegions ↩ https://developer.apple.com/videos/play/wwdc2022/10072/ ↩
こんにちは、XR × Fashion TechやXR × Beauty Techといった領域を推進している創造開発ブロックの @ikkou です。 2024年1月9日から12日の4日間にかけてラスベガスで開催された「CES 2024」に一般参加者として現地参加してきました。私個人としては通算5度目、ZOZO所属としては2020年、2023年に続き3度目の参加となります。 techblog.zozo.com techblog.zozo.com 前半はCESの概要と関連する情報、後半は私が注目したXR TechとFashion Tech、そしてBeauty Techについてお伝えします。CES 2024全体のトレンドについては、CES Daily Show Day 1 ・ Day 2 ・ Day 3 などをご覧ください。 CESとは 値上がりした参加費 会場の概要 Official Show Store 会場間の移動 徒歩 Vegas Loop Lyft Las Vegas Monorail XR Tech XR関連企業の出展動向 XRデバイスの動向 MetaのRay-BanスマートグラスとAmazonのEcho Frames CES 2024にあわせた発表 Apple Vision Pro XREAL Air 2 Ultra SonyのVR HMDを含む没入型空間コンテンツ制作システム ShiftallのMeganeX superlight Fashion TechとBeauty Tech Beauty Tech領域で強い存在感を示していたL'Oréal Group CES初出展の資生堂 キヤノンのスキャンソリューション おわりに CESとは 毎年Venetian Expoの2Fから1Fへ降りるエスカレーター付近に設置されているCESの吊り看板 CES はCTA(Consumer Technology Association)が主催する、毎年1月にラスベガスで開催される世界最大級と言える「テクノロジーのショーケース」です。 読み方は「せす」と呼ぶ方もいますが、正しくは「しーいーえす」です。また、CESは長らく「Consumer Electronics Show」という名称でしたが、現在この「Consumer Electronics Show」という表記は非推奨となっています。 CTAの100th Anniversaryを記念するモニュメント CESを主催しているCTAは、1924年にラジオ工業会として設立され、2024年は記念すべき100th Anniversaryの年でした。 CESには毎年テーマがあり、 2024/01/05に発表されたCTAのプレスリリース によると、今年のトップテーマは次の4つでした。 人工知能 / Artificial Intelligence 万人のための人間の安全保障 / Human Security for All モビリティー / Mobility 持続可能性 / Sustainability 毎年Venetian Expoの2Fに用意されている大きな看板 会場内の至るところに掲げられていた「ALL ON」の文字は「The Future is ALL ON」を意味し、「未来はすべてここにある」というCTAの強い意志を感じました。 INNOVATION AWARD SHOWCASEのエントランス 時勢もあり特に強く「AI推し」の側面が感じられ、会場の様々な場所で掲げられている各種ビジュアルも生成AIを利用して制作されているようでした。 ここ数年、CESは新型コロナウィルス感染症の影響を受け出展社数が減っていました。CES 2020の出展社数が4,400社以上だったところ、オフラインが完全復活を遂げた去年のCES 2023の出展社数が3,200社以上でした。しかし、 公式発表によれば今年のCES 2024の出展社数は4,300以上 と、概ねコロナ禍以前の水準に戻りつつあるように感じました。 値上がりした参加費 LVCC West Hallに設置されている#CES 2024のモニュメント CES 2024の参加登録は現地時刻2023/09/12にオープンし、チケットの価格は同12/05までの早期登録が$149(約¥22,000)、以降は$350(約¥52,000)でした。私は「貴重なCESの卒業生(英語表記は“valued CES alum”)」向けの特典によりRegistrationは$0で済みました。 ちなみに、CES 2023以前は早期登録が$100、通常登録が$300だったので、CES 2024から$50ほど値上がりしていることになります。USは何かと高騰が進んでいるので、これは仕方がないことかもしれません。 今回はCES参加5度目にして初めて公式のカンファレンスプログラムである「 AR/VR/XR 」を追加しました。オンサイトでXR領域に関する3つのセッションに参加できて$400(約¥60,000)でした。 このカンファレンスプログラムは参加費も含まれるため、特典を持っていない方は実質$50で3つのセッションに参加できるということです。 会場の概要 Image Source. https://www.ces.tech/exhibits/maps-and-locations.aspx CESの展示会場は例年通りTech East・Tech West・Tech Southという3つのエリアに大別されています。 一般の来場者向けの会期は4日間ありますが、1人で全ての会場・全てのブースを巡るのはあまりにも非現実的です。 XRエリアに近いCentral Hallの入口、初日の開場直前は多くの人が集まる。 私は例年、Tech EastのLas Vegas Convention(LVCC)のWest・North・Central HallとTech WestのVenetian Expoを中心に巡っています。LVCCにはSouth Hallもありますが、昨年に引き続き今年も改装中でした。 4日間ともその日の最初から最後までとにかく歩き回りました。見るべきものを絞って効率的に巡っているとはいえ、期間中の平均歩数は1日あたり25,000歩を超えていました。普段リモートワークであまり歩かない私にとってはかなりの運動量です。 Official Show Store オフィシャルCESストア 過去参加したCESでは見かけていませんでしたが、CES 2024ではCES公式のオフィシャルストアが会場内に5ヵ所設置されていました。CESの公式ウェブサイトには “We’re thrilled to bring an official store back to CES”と記載されている ので、過去にもあったのかもしれません。 CES 2024 Official Show Storeで購入したフーディー 私はフーディーを1着購入しましたが、このフーディーの図柄も生成AIを利用して制作されたもののように感じました。 会場間の移動 とにかく会場間の移動には時間がかかります。今回、会場間の移動には徒歩、Vegas Loop、Lyft、そしてLas Vegas Monorailを利用しました。 この他の選択肢として、会場間をつなぐ無料のシャトルバスや、有料の循環バス「DEUCE」、そして通常のタクシーなどが挙げられます。 徒歩 徒歩ルートを選ぶとVenetian Expoを出てすぐの歩道橋で話題のSphereを間近で見られる。 LVCCとVenetian Expoの間は何度か徒歩で移動しました。片道で30分程度かかります。 ライドシェアやシャトルバスの移動は乗車までに時間がかかることや、渋滞に巻き込まれることもあるため、実は徒歩の方が早いこともあります。日中であれば大通りの治安は悪くないので、時間と体力に余裕がある場合は徒歩での移動もおすすめです。 Vegas Loop Vegas LoopのLVCC WEST STATION Vegas Loop はイーロン・マスク氏が立ち上げたThe Boring Company社によるラスベガスの地下交通網です。ラスベガスの空港から北部のダウンタウンまで結ぶ計画が当局に承認されていますが、現在はLVCCのWest Hall・Central Hall・South Hallを結ぶ無料の路線(LVCC Loop)と、RESORT WORLDとLVCCを結ぶ有料の路線のみが運用されています。 Vegas LoopのRESORT WORLD路線の発着場 後者の有料の路線は、開通当時は1日乗り放題で$2.5でしたが、CES 2023時は1日乗り放題で$4.5に、そしてCES 2024では$5(約¥740)に値上がりしていました。ただし、この有料の路線はRESORT WORLD近辺の宿泊者以外がそう頻繁に利用することはないでしょう。私は確認するために1度だけ乗車しました。 CESの参加者はもっぱらLVCCの会場間を繋ぐ無料のLVCC Loopを利用します。LVCCのWest HallとCentral Hall間を徒歩で移動する場合は20分間程度かかりますが、LVCC Loopであればわずか2分間で移動でき、とても便利です。 Vegas Loopを走るファルコンウイングドアが特徴的なModel X 昨年同様、今年も何度か利用しました。車両はTeslaで統一され、車種はModel Yが多く、稀にファルコンウイングドアが特徴的なModel Xも走っていました。相乗りが前提になっていて、どの車両に乗れるかは運次第です。今回は運良く何度かModel Xに乗車できました。 The Boring Company社の公式Xによると、 将来的にはCybertruckが走る ようで、とても楽しみです。 Lyft ホテルと離れた会場間の移動にはライドシェアのLyftを多用しました。過去のCESではAptivと連携した「自動運転」のLyftが走っていましたが、今回は見かけませんでした。 ライドシェアの価格は変動相場制になっているので、同じ行き先でも時間帯によって価格が変わります。より安価に済ませたい場合はLyftとUberで比較することをおすすめします。 注意点として、まずライドシェアの乗り場は決まっています。乗り場から離れた場所にいる場合は移動しなければなりません。また、その日の閉場時刻になると非常に多くの人が乗ろうとするため混雑します。私はライドシェア乗り場で車両を手配してから40分待った末、車両が到着しなかったため結局徒歩で移動する羽目になりました。 Las Vegas Monorail LVCCとVenetianの移動に一度だけ Las Vegas Monorail を利用しました。区間距離は関係なく片道で$6(約¥890)です。1DAY PASSから7DAY PASSまで用意されているので、駅周辺に宿泊しているなどCESの期間中の移動をすべてLas Vegas Monorailで済ませる場合は$39(約¥5,800)の4-Day Passがおすすめです。なお、紙の切符ではなく、eTicketの場合は割引があります。 ライドシェアをはじめとする道路を走る交通手段と比較すると、目的地によっては駅を降りた後の徒歩移動が必要となりますが、渋滞に巻き込まれることもなく、安定した時間で移動できます。 XR Tech CESのProduct Categoriesに「AR/VR/XR」が存在するように、例年多くのXR関連企業が出展、あるいはCESにあわせて発表しています。また、HTCのように近くのホテルで独自に展示している企業もあります。 XR関連企業の出展動向 Image source. https://exhibitors.ces.tech/8_0/floorplan/?hallID=B LVCC Central Hallには「GAMING | METAVERSE | XR」とカテゴライズされた一画があります。CES 2020当時は「AR/VR & Gaming」でしたが、今年は昨年のCES 2023から変わらずこのカテゴリー名になっています。ただ、個人の感覚として、2024年においてもメタバースムーブメントが去年同様盛り上がっているかどうかについて疑義があります。実際のところこのエリアにメタバース推しのブースはほとんどありませんでした。 Venetian Expoの「Gaming/XR」エリアの地図 また、Venetian Expoのスタートアップ企業が集まるEureka Parkにも「Gaming/XR」エリアが設けられていましたが、7ブースしか出展していませんでした。参考としてCES 2023では「Gaming/Metaverse/XR」エリアとして16社が出展していました。2023年から2024年の変化としてはエリアとして「Metaverse」が削除され、「Gaming/XR」に取り組むスタートアップ企業が減ったということになります。 ただし、LVCC・Eureka Parkともに、このエリアに限らずVR HMDやAR Smart Glassesを含めたXR関連のブースは多くありました。例えばEureka Parkは国ごとのエリアが設けられているので、その中でXR関連のブースは「Gaming/XR」エリアの出展数以上にありました。 XRデバイスの動向 CES 2024で体験したXRデバイスの一部 今年もたくさんのXRデバイスを実際に試してきました。CES 2023では一旦落ち着きを見せていたXRデバイスですが、今年はCES 2020の頃のように盛り上がりを見せているように感じました。キヤノンのMREALやThinkのReality A3のように既に販売されているデバイスもありますが、まだ開発中のものも多く含まれています。 いわゆる眼鏡型のデバイスは、以前からあるスマートフォンに接続するものの他、Spatial Computerを謳う「 Nimo 1 」のようにデスクトップを拡張するものを増えてきました。これは後述するApple Vision Proと同じ流れと言えますし、CES 2024には出展していませんでしたが、 ImmersedのVisor も同様です。 MetaのRay-BanスマートグラスとAmazonのEcho Frames Metaは昨年の Meta Connect 2023 でMeta AIを組み込んだスマートグラスである「 Ray-Ban Metaスマートグラス 」を 発表しました 。また、昨年末にはこの Meta AIにマイクロソフトの「Bing」を組み込んだ物体認識機能が発表 されています。 Ray-Ban Metaスマートグラスを展示しているMetaブース CES 2024では、Metaブースでこの「Ray-Ban Metaスマートグラス」が展示され、多くの賑わいを見せていました。 Ray-Banショップで販売されていたRay-Ban Metaスマートグラス このスマートグラスは日本からの注目度が高いものの、現時点で日本からは注文できません。しかし、ラスベガスにはこの「Ray-Ban Metaスマートグラス」を取り扱うRay-Banの店舗やグラスショップが複数ありました。私の観測範囲内ではCESにあわせて渡米した日本からの参加者が一定の割合で購入していたようでした(私はマッチするフレームとレンズの組み合わせがなく、今回は見送りました)。 Amazonブースに展示されていたEcho Frames Amazonも昨年9月に第3世代にあたる「 Echo Frames 」を発表していて、AmazonのAlexa関連デバイスを紹介するブースで展示されていました。 Amazon.comで販売されていて、ブースではCES期間中の限定クーポンを配布していましたが、残念ながらこちらも現時点で日本からは注文できません。 CES 2024にあわせた発表 CES 2024にあわせてXR関連デバイスを発表した企業もあります。いくつか気になったものを紹介します。 Apple Vision Pro Appleは例年CESに“出展”していませんが、CESにあわせてOOHを掲出したり、CES 2020ではセッションに登壇したりしています。そのAppleはCES 2024の初日に重ねる形で「 Apple Vision Pro 」のUSにおける予約日と発売日を発表しました。 Apple CEOのTim Cook氏によるXのポスト を見かけたときは本当に興奮したことを今でも覚えています。 The era of spatial computing has arrived! Apple Vision Pro is available in the US on February 2. 本レポート記事の公開時点では、既にUSでの予約が始まっています。日本からの購入組も一定数いるように見えますが、いわゆる“技適”の関係で大手を振って国内で使えるようになるのは、日本での発売日を迎えてからになります。一刻も早く日本でも発売されることを願ってやみません。 XREAL Air 2 Ultra XREALブースの様子 いわゆる“ARグラス”の領域で特に勢いのあるXREAL社はCES 2024にあわせて「 XREAL Air 2 Ultra 」を発表、予約を開始しました。開発者が待ち望んだ「Nreal Light」の正統後継デバイスです。 赤いXの目立つブースでは、既に発売されているXREAL Air 2 / Air 2 Proの他、XREAL Air 2 Ultraの実機でハンドトラッキングのデモなどを体験できました。 6DoFのトラッキング性能を確認できるデモ 別途アポイントが必要となる個別のブースでは、6DoFのトラッキング性能を確認できるデモを体験できました。これはCES 2024の会期直前にメディア向け限定して開催される「CES Unveiled」で展示されていたものと同じです。自分だけしかいない落ち着いた個室での体験となり、Unveiledのように人が多くて混雑している状況よりもじっくりと体験できたと言えるかもしれません。 SonyのVR HMDを含む没入型空間コンテンツ制作システム CES 2024のプレスカンファレンスで突然登場したのが、SonyのVR HMDを含む没入型空間コンテンツ制作システムです。一般個人向けの製品ではなく、産業向けの製品として発表されました。 SonyのVR HMDを含む没入型空間コンテンツ制作システム Sonyブースには展示されていませんでしたが、 協業先となるSiemens のブースに実機が展示されていました。 www.youtube.com 残念ながら実機は体験できませんでしたが、YouTubeを見る限りでは、デジタル化が進んでいるファッション・アパレル領域でも使える可能性があると感じました。 ShiftallのMeganeX superlight Panasonicの100%子会社であるShiftallはMeganeXの軽量版にあたる「 MeganeX superlight 」、センサーを改良した「 HaritoraX ワイヤレス R 」、そして防音マイクmutalkの新モデル「 mutalk 2 」を発表しました。 MeganeXとMeganeX superlight CES 2020で発表された当時、その見た目から サイバーパンクやスチームパンクと話題になった VR HMDです。その後、改良を経て2023年夏に「MeganeX」として発売され、さらに実際の利用用途を考慮して軽量化されたのが「MeganeX superlight」です。 私は事前に日程をあわせた上でプライベートブースに伺い、現行品の「MeganeX」そして軽量版の「MeganeX superlight」をじっくりと試してきました。 Apple Vision Proの重さが600~650gと言われる中、約200gとされる「MeganeX superlight」は圧倒的に軽く感じました。将来的な方向性などもたくさんお話しいただき、とても有意義な時間を過ごすことができたとともに、今後が非常に楽しみになりました。 Fashion TechとBeauty Tech 昨年同様、XR領域と並行して、Fashion TechとBeauty Techも注目していました。 CES 2024のProduct Categories には記載されていませんが、Tech WestのVenetian Expo 2FにBeauty Techエリアが設けられていました。 Beauty Techエリアの地図 昨年はLife Styleエリアに配置されていたので、これはBeauty Techの注目度が高まっていることを示唆するのかもしれませんが、ブース数としては5社に留まっていました。 ただし、必ずしも関連するブースがこのBeauty Techエリアに配置されるわけではありません。昨年も賑わいを見せていた、肌に直接印刷するプリンターのPrinker社は別のエリアに配置されていたものの、今年も大盛況でした。 LG H&H社のIMPRINTUブース 肌に直接印刷するプリンター3種 また、同じジャンルでLG H&H社が IMPRINTU をCESにあわせて発表し、こちらも賑わっていました。ともにCES 2024で特に強い勢いを見せていた韓国企業です。 Beauty Tech領域で強い存在感を示していたL'Oréal Group Resort Worldに大きく展開されていたL'Oréal GroupのOOH 昨年のCES 2023では、『手や腕が不自由な人に向けたメイクアップアプリケーター「HAPTA」』と『世界初の家庭用電子アイブロウメイクアップアプリケーター「L'Oréal Brow Magic(現3D shu:brow)」』で CES 2023 Innovation Awardsを受賞していた L'Oréal Groupですが、今年もBeauty Tech領域で強い存在感を示していました。 Resort Worldに大きく展開されていたL'Oréal GroupのOOHもそのひとつですが、CES 2024ではBeauty系企業として初めてKeynoteを開催しました。 Image source. https://www.youtube.com/watch?v=pArGshMSoNo このKeynoteでは、L'Oréal GroupのCEOであるNicolas Hieronimus氏が登壇し、L'Oréal Groupのビジョンや、Beauty Tech領域における取り組みについて語りました。DE&IなBeauty Techの実現に向けて掲げていた「BEAUTY FOR EACH POWERED BY TECH」という表現が印象的でした。 また、L'Oréal Groupの持つ膨大なデータとLLMを利用したAIソリューションの「Beauty Genius」も発表されました。パーソナライズ・レコメンデーション・ARによるバーチャルトライオンなどを統合したソリューションです。 バーチャルトライオンついては2018年3月にModiFace社を買収し、その後さまざまな形で展開しているので、その一環だと考えられます。 https://www.loreal.com/ja-jp/japan/pages/group/our-purpose/discovering-modiface-jp/ www.loreal.com Image source. https://www.youtube.com/watch?v=pArGshMSoNo Beauty Geniusのデモとして、チャットUIでの会話・スマートフォンで自身の顔を撮影して送信・肌の状態を分析してスキンケア製品をレコメンドしてもらう一連の流れを披露していました。こういったソリューションが一般化することで、AIもよりいっそう自然なものになっていくのかもしれません。 L'Oréal Groupの取り組みに興味のある方はYouTubeに公開されているKeynoteのアーカイブ動画を参照してください。 www.youtube.com CES初出展の資生堂 CES初出展となった資生堂のブース 日本企業勢として、昨年のCES 2023ではKoséが初めてCESに出展していましたが、今年のCES 2024では資生堂が初めて出展していました。 資生堂がCESに出展していることに違和感を覚える方がいるかもしれませんが、近いところでは SXSW 2019でInvisible VRプロジェクト“caico”を出展 しています。他にもさまざまな形でBeayty Techに取り組んでいて、今回のCES 2024でもこの“caico”プロジェクトを引っ張っていた方が出展責任者を務めていました。 corp.shiseido.com 資生堂のブースでは、適切なスキンケア美容法の実践をサポートするアプリ「 Beauty AR Navigation 」と、2023年11月に発表された「 顔画像から将来の肌悩みを予測できるツール 」をベースにした「鼻の骨格から、“未来の肌悩み”を予測するツール」の2つを体験できました。 Beauty AR Navigationの様子 「Beauty AR Navigation」は、美容液などを肌に塗る際の資生堂が考える正しい動作を、DirectionとSpeedの2軸で評価するものです。ディスプレイの前に設置されているWebカメラに自身が映り、その動作をARで表示された手の動きと比較することで、正しい動作を学べます。私が体験したところ、Directionは良かったもののSpeedに難があり、結果は60点でした。 ゲーミフィケーション的な要素を取り入れることによって、正しい動作を習得するモチベーションを高められると感じました。Beauty TechにおけるARはバーチャルトライオンが目立ちがちですが、こうした正しい動作の習得を目的としたARの利用も、今後のBeauty Tech領域の発展に大きく貢献すると考えています。 鼻の骨格から、“未来の肌悩み”を予測するツール 「鼻の骨格から、“未来の肌悩み”を予測するツール」は、iPhoneのインカメラで顔を撮影するだけで、鼻の骨格から肌の状態や肌の特徴を推定するものです。この鼻骨格による肌分析は世界初とのことで、まさか鼻の形から将来的な肌予測に繋がるとは思いもせず、資生堂の研究開発力の高さを感じました。極めて簡単に診断できるので、今後の展開が非常に楽しみです。 キヤノンのスキャンソリューション キヤノンブース 技術カテゴリーとしてはImaging Technologyになりますが、キヤノンのブースではFashion Tech領域にも展開できるスキャンソリューションが初出展されていました。 デジタルツインなどの文脈で商品の3Dモデルを作成するソリューションは既に複数存在していますが、キヤノンのソリューションは1枚の画像からAIによってデプスを推定して立体に見せるものです。その仕組み上、360度すべてを見回すことはできませんが、ECにおいて必ずしも360度すべてを見せる必要があるわけではないので、十分に有用なソリューションだと感じました。サンプルとして服・靴・帽子が用意されていたのも非常にわかりやすかったです。 ちなみに、CES 2024では展示されていませんでしたが、同じカメラメーカーである ニコンもアバターファッションの文脈で服の3D化に取り組んでいます 。Fashion Tech領域において、カメラメーカーの持つ技術も活用されていくことは想像に難くなく、今後の動向が楽しみです。 おわりに 4日目の夜明けにホテルから見たSphere 例によって今回のCES視察は開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 今回はCES 2023時よりも1か月早くフライトとホテルを手配したことが功を奏したのか、CES 2023当時よりも15%ほど安価にフライトとホテルを手配できました。既にCES 2025の開催日程が発表されているので、参加意向のある方は、できるだけ早く手配すると良いかもしれません。特にホテルは多くの場合で一定期間まではキャンセルできます。 実際にはフライトとホテル以外にも一定の金銭的コストが発生していて、そのコストに対して得られたことの妥当性を説明するのは難しいでしょう。しかし、昨年も言っていますが、XR領域は「百聞は“一体験”に如かず」です。CESに関するニュース記事は日本でも多く目にしますが、現地に足を運び、その目その手で体験することに価値があると考えています。 また、CESはビジネスショーという性質上、個別に会話するプライベートブースが用意されています。いくつか参加しましたが、こういったオンサイトならでの対面コミュニケーションも、インターネットメディアの記事等からは得られない大きなメリットだと考えています。 最後までご覧いただきありがとうございました。帰国便の飛行機が大幅に遅延し、ひたすらロビーで待った末にフライトがキャンセルになり予定通り帰国できなくなるといったトラブルもありましたが、それはまた別の機会に。 ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 現場からは以上です!
はじめに こんにちは、ML・データ部MLOpsブロックの 岡本 です。 MLOpsブロックでは機械学習モデルの実験基盤の作成、機械学習モデルを組み込んだAPI・Batchの開発・運用・保守を行なっています。APIを開発する際には負荷試験を実施し、本番環境で運用する際に求められるスループット・レイテンシを達成できるか確認します。 MLOpsブロックでの従来の負荷試験実施には人手を要する定型的な作業が複数ありました。また頻繁に行う作業でもありトイルとなっていました。 本記事ではMLOpsブロックで抱えていた負荷試験実施の課題と、解決のために開発したOSSのCLIツール、Gatling Commanderについて紹介します。Gatling Commanderが負荷試験の実施におけるトイル削減の一助になれば幸いです。 github.com 目次 はじめに 目次 背景・課題 従来の負荷試験実施の方法 分散負荷試験ツール(Gatling Operator)の説明 従来の実施方法 シェルスクリプトを使ったKubernetesクラスタの操作 手作業での負荷試験結果の記録 負荷試験実施の課題 設定値の変更の手間 実行状況の確認の手間 結果の記録の手間 Go製CLIツール(Gatling Commander)による負荷試験実施の自動化 Gatling Commanderの機能 負荷試験用コンテナイメージを自動作成 負荷試験用Gatlingオブジェクトを自動作成 過負荷時に負荷試験を自動停止 負荷試験結果・コンテナメトリクスを自動記録 負荷試験の完了を通知 Gatling Commanderの実装 利用技術 処理の概要 Kubernetesオブジェクトの操作の実装 Goroutinesによる並列処理の実装 Gatling Commanderの導入効果 負荷試験シナリオ切り替えの手間が不要になった 負荷試験実行の状況確認の手間が不要になった 負荷試験結果の記録の手間が不要になった 今後の展望 APIパフォーマンス劣化検知への利用 GitHub Actionsとの連携による負荷試験の定期実行 機能追加・改善 Kubernetes以外で稼働するAPIへの対応 Google Cloud以外のプロバイダへの対応 取得するメトリクスの拡充 まとめ 背景・課題 従来の負荷試験実施の方法 本節ではMLOpsブロックで行なっていた従来の負荷試験実施の方法について、利用するツールと実施方法を説明します。 分散負荷試験ツール(Gatling Operator)の説明 MLOpsブロックでは負荷試験の実施に、 Gatling で分散負荷試験を行う Kubernetes Operator の Gatling Operator を利用します。 GatlingはWebアプリケーション向けのOSS負荷試験フレームワークです。負荷試験シナリオに基づき負荷試験を行い、HTML形式の結果レポートを自動で生成します。 Kubernetes Operatorは、Kubernetes APIの拡張である Custom Resource と、その状態管理を担う Custom Controller によりKubernetesを拡張するための仕組みです。 Workloads の目的の状態を定義したCustom ResourceをKubernetesクラスタにデプロイすると、Custom Controllerは制御ループを通じてその目的の状態に近づくように制御します。WorkloadsはKubernetes上で稼動するアプリケーションであり、制御ループはあるシステムの状態を制御する終了状態のないループです。 Kubernetes Operatorを利用するとKubernetes上で稼動するアプリケーションであるWorkloadsのライフサイクル管理を自動化できます。 Gatling Operatorは分散負荷試験の内容を定義したCustom ResourceであるGatling Resourceを扱うKubernetes Operatorです。Gatling ResourceがKubernetesクラスタにデプロイされると、Gatling Resourceを監視するCustom ControllerであるGatling Controllerが目的の状態に近づくように制御します。Gatling Operatorの仕組みにより、一連の分散負荷試験のタスクを自動化できます。 Gatling Operatorの詳細は、TECH BLOGの「Gatlingによる分散負荷試験を自動化するKubernetesオペレーターGatling Operatorの紹介」をご参照ください。 techblog.zozo.com 従来の実施方法 MLOpsブロックでは従来、次の流れでGatling Operatorを使った負荷試験を実施していました。 Gatlingの負荷試験シナリオを作成 負荷試験シナリオを含むコンテナイメージのBuild&Push(アップロード) Gatling ResourceのKubernetesマニフェストを作成 Gatling ResourceをKubernetesクラスタへデプロイし、負荷試験を開始 負荷試験対象APIが稼働するコンテナのメトリクスを取得 負荷試験の実行状況を確認 負荷試験結果(Gatling Report)を Google Sheets へ記録 これらの作業は作業者のローカル環境で行っていました。 Gatling Operatorを使った従来の負荷試験では、各作業で利用できるシェルスクリプトはいくつかあるものの、作業者の手作業が多く必要でした。 シェルスクリプトを使ったKubernetesクラスタの操作 Gatling ResourceのデプロイなどKubernetesクラスタへの操作は、 kubectl コマンドを利用します。 kubectl コマンドはKubernetes APIを使用してKubernetesクラスタと通信するためのCLIツールです。 負荷試験の作業では、 kubectl コマンドを使ってKubernetesクラスタを操作し、Gatling Resourceのオブジェクト作成と負荷試験対象コンテナのメトリクスを取得します。 kubectl コマンドでの操作では操作対象クラスタの切り替えなどの作業も行います。 負荷試験でGatlingが自動生成するHTML形式の結果レポートは、Gatling ResourceのKubernetesマニフェストで指定したクラウドストレージにアップロードされます。一方で負荷試験結果は負荷試験を行うGatlingコンテナのログにも出力されます。このログを kubectl logs コマンドで取得し、ログから抽出することで、クラウドストレージを確認せずとも簡易的な形式で負荷試験結果を確認できます。 負荷試験対象コンテナのメトリクスは kubectl top コマンドで取得します。 kubectl top コマンドはKubernetesクラスタ内に Metrics Server をインストールすると利用できます。 kubectl top コマンドによりPod・Node・コンテナのCPU・メモリの使用量を取得できます。 このように負荷試験用のシェルスクリプトでは kubectl コマンドを使って、Gatling Resourceのオブジェクトを作成していました。またGatlingコンテナのログから結果の抽出と、負荷試験対象コンテナのメトリクスを取得し、これらの値をまとめた負荷試験結果を出力していました。 負荷試験時には作業者がこれらのシェルスクリプトを実行していました。 手作業での負荷試験結果の記録 MLOpsブロックでは負荷試験結果の記録先としてGoogle Sheetsを利用しています。Google Sheetsは複数の人が同時に閲覧・編集できるため、チームでの作業に便利です。 Gatlingは負荷試験ごとに結果レポートを自動生成します。このレポートは負荷試験ごとに作成されるため、実施したすべての負荷試験結果を一覧し、比較するには不便でした。 Google Sheetsであれば負荷試験結果を表形式でまとめることができ、Googleアカウントへの権限付与で共有できるためチーム内への展開も簡単です。そのため負荷試験結果の最終的な記録先としてGoogle Sheetsを利用しています。 作業者はシェルスクリプトが出力する負荷試験結果をGoogle Sheetsにコピー&ペーストして記録していました。 負荷試験実施の課題 従来の負荷試験実施の方法は作業者の手間が多く、負荷試験全体でかかる作業工数が課題でした。作業者は各作業を負荷試験シナリオごとに行います。個々は小さな手間でも積み重なると大きくなり、負荷試験全体で1〜2日程度の作業工数を要していました。 負荷試験でAPIのパフォーマンスを計測する際、様々な条件を試すために設定パラメータの値が異なる複数の負荷試験シナリオを用意します。負荷試験シナリオごとに、前節( 従来の実施方法 )で説明した 3.Gatling ResourceのKubernetesマニフェストを作成 から 7.負荷試験結果(Gatling Report)をGoogle Sheetsへ記録 までの作業を繰り返します。これらは定型作業ですが作業数が多く、負荷試験シナリオの数に比例して作業者の手間も増えます。 このように従来の負荷試験実施では、手作業の項目が多いことで作業者の工数を要していました。本節ではこれらの項目の中でも、特に作業者の大きな手間になっていた項目について説明します。 設定値の変更の手間 従来の負荷試験実施の方法では、負荷試験シナリオごとに作業者が設定パラメータの値を変更していました。 負荷試験シナリオの設定パラメータはGatlingのテストシナリオを記述したファイルであるSimulationで定義します。SimulationはGatlingのコンテナイメージに含めます。設定パラメータの値はGatling ResourceのKubernetesマニフェストで指定します。Gatlingはこの値をGatlingコンテナの環境変数から読み込みます。 負荷試験シナリオ間でSimulationが共通する場合、同一のコンテナイメージを利用できるため、シナリオごとにコンテナイメージのBuild&Push作業は不要です。一方で設定パラメータの値については負荷試験シナリオ間で異なるため、シナリオごとにGatling ResourceのKubernetesマニフェストで値の変更が必要です。 例えば負荷試験で頻繁にある、APIの最大スループットを計測する場合を考えます。この場合秒間リクエスト数の設定パラメータ値を変更した、複数のシナリオで負荷試験を実施します。 作業者は秒間リクエスト数を変更するために、負荷試験シナリオごとにGatling ResourceのKubernetesマニフェストを変更する必要がありました。負荷試験シナリオごとに実行結果を確認し、レイテンシに問題がなければ秒間リクエスト数の値を増やして次のシナリオを実行していました。 リクエスト数の変更を繰り返してAPIの最大スループットを測るため、作業者が負荷試験の設定パラメータの値を変更する作業回数は多くなります。特に新規開発したAPIでは、どの程度のスループットを捌けるのか事前にわからず、作業工数がかかっていました。 このように負荷試験において設定パラメータの値の変更は頻繁に行われるにもかかわらず、スクリプトでの自動化ができていなかったため作業者の手間になっていました。 実行状況の確認の手間 同一のAPIを対象に負荷試験を行う場合、前のシナリオで負荷試験が完了してから、次のシナリオでの負荷試験を実行します。従来の方法では、前の負荷試験の実行状況をシナリオごとに、作業者が確認していました。 Gatling Operatorによって分散負荷試験の実行は自動化されているため、シナリオごとの負荷試験の実行中に作業者による作業は必要ありません。この間に作業者は他のタスクを進められます。しかしシナリオあたりの負荷試験の実行時間が短いと、実行状況の確認のために他のタスクを頻繁に中断するため、作業者は効率的に作業できていませんでした。 このように実行状況の確認は、作業者にとって頻繁に行う必要のある割り込み作業であり、手間になっていました。 結果の記録の手間 従来の負荷試験実施の方法では、シナリオごとに作業者が結果を記録していました。 前節( 手作業での負荷試験結果の記録 )の通り、作業者はシェルスクリプトが出力した、簡易的な負荷試験結果の値をGoogle Sheetsへ記録します。作業者はGoogle Sheetsへ手作業でコピー&ペーストして結果を記録していました。 また前節( 設定値の変更の手間 )で例として挙げたAPIの最大スループットを測る場合では、結果の記録に加えて結果の値の確認も必要です。作業者は負荷試験結果を確認し、目標レイテンシを超えていないか、エラーが発生していないかなど、事前に決められた閾値を元に次のシナリオを実行するかどうかを判断していました。 このように負荷試験結果の確認およびGoogle Sheetsへの結果の記録も、シナリオごとに作業者が手作業で行い、作業者の手間となっていました。 Go製CLIツール(Gatling Commander)による負荷試験実施の自動化 MLOpsブロックでは負荷試験実施にかかる作業者の工数削減のために、CLIツールGatling Commanderを開発し、OSSとして公開しました。 Gatling Commanderは、Gatling Operatorを利用した負荷試験実施における一連の作業を自動化します。 前節( 従来の実施方法 )で説明した従来の負荷試験の流れのうち、 3.Gatling ResourceのKubernetesマニフェストを作成 から 7.負荷試験結果(Gatling Report)をGoogle Sheetsへ記録 までの作業はGatling Commanderにより自動化されます。 本節ではGatling Commanderについて機能と実装の概要、そして実際にMLOpsブロックで利用して感じた効果を説明します。 Gatling Commanderの機能 Gatling Commanderは次の機能を持ちます。 負荷試験用コンテナイメージをBuild&Push 負荷試験シナリオごとにGatlingオブジェクトを作成 過負荷時に負荷試験を自動停止 負荷試験結果、コンテナメトリクスを記録 負荷試験の実行状況を確認 負荷試験の完了を通知 これらの機能に加えて、Gatling Commanderの設定ファイルには複数の負荷試験シナリオを記述可能です。この設定ファイルをYAML形式で記述し、コマンドを実行するとGatling Commanderによりすべての負荷試験シナリオが自動実行されます。 Gatling Commanderは、従来の負荷試験実施の方法でシナリオの数に比例して作業者の手間が増加していた課題を解消し、負荷試験実施にかかる全体の作業工数を削減しています。本節ではGatling Commanderが提供する機能について説明します。 負荷試験用コンテナイメージを自動作成 Gatling Commanderは負荷試験用コンテナイメージのBuild&Push作業を自動化します。 Gatling CommanderはGatlingのSimulation等のファイルを含めて負荷試験用のコンテナイメージをBuildします。またGatling Commanderの設定ファイルで指定したコンテナレジストリに、BuildしたコンテナイメージをPushします。 Gatling CommanderはBuild&Pushしたコンテナイメージを、複数の負荷試験シナリオ間で共通のコンテナイメージとして利用します。 事前にBuild&Pushしたコンテナイメージを使う場合は、コマンド実行時にオプションを指定してBuild&Pushを省略可能です。Build&Pushを省略する場合、Gatling Commanderは設定ファイルで指定したBuild&Push済みのコンテナイメージを利用します。 負荷試験用Gatlingオブジェクトを自動作成 コンテナイメージのBuild&Push後、Gatling CommanderはKubernetesクラスタ内にGatling Resourceのオブジェクトを作成します。Kubernetesクラスタ内にGatlingオブジェクトが作成されると、Gatling OperatorはGatlingコンテナを稼働させるPodを作成し分散負荷試験を実施します。 Gatling Commanderは、設定ファイルで指定したベースとなるGatling ResourceのKubernetesマニフェストを、GoのGatling構造体のオブジェクトに読み込みます。次に負荷試験シナリオごとに値が異なるフィールドの値を、設定ファイルに指定された値で上書きします。 ベースとなるGatling ResourceのKubernetesマニフェストの例は次のとおりです。 apiVersion : gatling-operator.tech.zozo.com/v1alpha1 kind : Gatling metadata : name : <config.yaml overrides this field> # will be overrided by services[].name field value in config.yaml. ex: sample-service namespace : gatling spec : generateReport : true generateLocalReport : true notifyReport : false cleanupAfterJobDone : false podSpec : gatlingImage : <config.yaml overrides this field> # will be overrided by built Gatling Image URL or imageURL field value in config.yaml. ex: asia-docker.pkg.dev/project_id/foo/bar/gatlinge-image-name-prefix-YYYYMMDD rcloneImage : rclone/rclone resources : requests : cpu : "7000m" memory : "4G" limits : cpu : "7000m" memory : "4G" serviceAccountName : "gatling-operator-worker-service-account" affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - "gatling-operator-worker-pool" tolerations : - key : "dedicated" operator : "Equal" value : "gatling-operator-worker-pool" effect : "NoSchedule" cloudStorageSpec : provider : "gcp" bucket : "report-storage-bucket-name" notificationServiceSpec : provider : "slack" secretName : "gatling-notification-slack-secrets" testScenarioSpec : parallelism : <config.yaml overrides this field> # will be overrided by services[].scenarioSpecs[].testScenarioSpec.parallelism field value. ex: 1 simulationClass : <config.yaml overrides this field> # will be overrided by services[].scenarioSpecs[].testScenarioSpec.simulationClass field value. ex: SampleSimulation env : # will be overrided by services[].scenarioSpecs[].testScenarioSpec.env[] field value. ex: `env: [{name: ENV, value: "dev"}, {name: CONCURRENCY, value: "20"}]` - name : <config.yaml overrides this field> value : <config.yaml overrides this field> 上記のGatling ResourceのKubernetesマニフェストの例で、 <config.yaml overrides this field> と記述があるフィールドの値は、Gatling Commanderの設定ファイルの値で負荷試験シナリオごとに上書きされます。 例えば負荷試験コンテナイメージを指定する spec.podSpec.gatlingImage フィールド・GatlingのSimulationのクラス名や環境変数など、負荷試験の具体的な設定値を記述するフィールドを配下に持つ spec.testScenarioSpec フィールドの値は上書き対象です。これらのフィールドの値はGatling Commanderの設定ファイルの値で負荷試験シナリオごとに上書きされます。 Gatling Commanderの設定ファイルの記述例は次のとおりです。 gatlingContextName : gatling-cluster-context-name imageRepository : gatling-image-stored-repository-url imagePrefix : gatlinge-image-name-prefix imageURL : "" # (Optional) specify image url when using pre build gatling container image baseManifest : config/base_manifest.yaml gatlingDockerfileDir : gatling startupTimeoutSec : 1800 # 30min execTimeoutSec : 10800 # 3h slackConfig : webhookURL : slack-webhook-url mentionText : <@targetMemberID> services : - name : sample-service spreadsheetID : sample-sheets-id failFast : true targetPercentile : targetLatency : targetPodConfig : contextName : target-pod-context-name namespace : sample-namespace labelKey : run labelValue : sample-api containerName : sample-api scenarioSpecs : - name : case-1 subName : 10req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "10" - name : ENV value : "dev" - name : DURATION value : "180" - name : case-1 subName : 20req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "20" - name : ENV value : "dev" - name : DURATION value : "180" Gatling Commanderの設定ファイルでは、負荷試験シナリオごとに上書きするフィールド値を1つの設定ファイル内に記述できます。複数の負荷試験シナリオごとの設定値を1つの設定ファイルに集約することで、実行するシナリオを一覧しやすくなります。 上記の設定ファイルの例で services[].scenarioSpecs[] フィールドには2つの要素が記述されています。このフィールドの各要素は負荷試験シナリオごとに固有の設定値を持ちます。 Gatling Commanderはコマンド実行時に、設定ファイルから services[].scenarioSpecs[].testScenarioSpec フィールドの値を読み取ります。こうして読み取った値で負荷試験シナリオごとに、ベースとなるGatling ResourceのKubernetesマニフェストの対象フィールド値を上書きします。この値の上書きは、ベースとなるGatling ResourceのKubernetesマニフェストを読み込んだGatling構造体のオブジェクトに対して行われます。 負荷試験シナリオごとに固有な設定値をGatling Commanderの設定ファイルに指定することで、Gatling ResourceのKubernetesマニフェストをシナリオごとに用意する必要がなくなります。Gatling ResourceのKubernetesマニフェストについて、複数の負荷試験シナリオ間で同一のフィールド値の指定が共通化されるため、作業者はシナリオごとに固有の設定値のみGatling Commanderの設定ファイルへ記述すれば良くなります。フィールドが共通化されることにより、値を変更するフィールドが絞られ、設定値の変更漏れを防ぐことができます。 このようにGatling Commanderは、ベースとなるGatling ResourceのKubernetesマニフェスト・Gatling Commanderの設定ファイルから、負荷試験シナリオごとにGoのGatling構造体のオブジェクトを作成します。こうして作成したGatling構造体のオブジェクトを元に、Kubernetesクラスタ内に負荷試験シナリオごとのGatlingオブジェクトを作成します。 Gatling Commanderはこれらの処理により、負荷試験用Gatlingオブジェクトを自動作成します。 過負荷時に負荷試験を自動停止 Gatling Commanderは設定ファイルに停止条件を指定し、ある負荷試験シナリオの結果が条件に一致した時、後続のシナリオの実行を自動停止できます。事前に停止条件を指定することで、負荷試験シナリオを余計に実行することを防ぎます。 前節( 設定値の変更の手間 )で挙げた、複数のシナリオを切り替えて秒間リクエスト数を増やしながら負荷試験を繰り返し、APIの最大スループットを測る場合を考えます。 この場合、ある負荷試験シナリオで目標レイテンシを達成できないと、そのシナリオよりもリクエスト数が多い後続のシナリオでは更なるレイテンシの悪化が予想でき、追加実行はあまり意味がないです。こうした場合にGatling Commanderによる負荷試験の自動停止が活用できます。 負荷試験の自動停止の条件には次の項目が利用可能です。 Failしたリクエストがある 特定のパーセンタイル値で指定した目標レイテンシの閾値を超過する 設定ファイルに停止条件を指定すると、Gatling Commanderは各シナリオで負荷試験の実行が完了した際に、負荷試験結果の値を確認します。確認した値が指定したこの条件に一致する場合、Gatling Commanderは後続のシナリオの負荷試験を自動実行せずに停止します。 前節( Gatling Commanderの機能 )の通り、Gatling Commanderの設定ファイルには複数の負荷試験シナリオを記述できます。これらの負荷試験シナリオはserviceという単位で記述します。 service単位で記述することで、同一の負荷試験対象APIに対する複数のシナリオの設定値をグループ化できます。同一のservice配下に複数の負荷試験シナリオの設定値を記述する場合、これらはGatling Commanderにより設定ファイル内での記載順で実行されます。 前節( 負荷試験用Gatlingオブジェクトを自動作成 )で説明したGatling Commanderの設定ファイルの例で、serviceごとの設定値は次の箇所です。 services : - name : sample-service spreadsheetID : sample-sheets-id failFast : true targetPercentile : targetLatency : targetPodConfig : contextName : target-pod-context-name namespace : sample-namespace labelKey : run labelValue : sample-api containerName : sample-api scenarioSpecs : - name : case-1 subName : 10req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "10" - name : ENV value : "dev" - name : DURATION value : "180" - name : case-1 subName : 20req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "20" - name : ENV value : "dev" - name : DURATION value : "180" Galing Commanderの設定ファイルでは、 services[] フィールドの配下の要素にserviceごとの設定値を指定します。負荷試験の自動停止の条件は services[].failFast フィールドと services[].targetPercentile ・ services[].targetLatency フィールドで指定します。 この例では services[].name フィールドの値に負荷試験対象のservice名として sample-service を指定しています。このserviceの services[].scenarioSpecs[] フィールドには2つの負荷試験シナリオの設定値を指定しています。同一service内の負荷試験シナリオは順次実行されます。この例だとGatling Commanderは name: case-1 の subName: 10req/sec 、 subName: 20req/sec の順番でシナリオごとの負荷試験を実行します。 この例で services[].failFast フィールドの値は true です。そのため name: case-1 の subName: 10req/sec のシナリオの結果でFailしたリクエストがあると、後続の name: case-1 の subName: 20req/sec のシナリオの負荷試験は実行しません。 負荷試験の自動停止の例としてGatling Commanderを利用し、前節( 設定値の変更の手間 )で挙げたAPIの最大スループットを測る場合を考えます。次のYAMLはこの場合のGatling Commanderの設定ファイルの記述例です。 gatlingContextName : gatling-cluster-context-name imageRepository : gatling-image-stored-repository-url imagePrefix : gatlinge-image-name-prefix imageURL : "" # (Optional) specify image url when using pre build gatling container image baseManifest : config/base_manifest.yaml gatlingDockerfileDir : gatling startupTimeoutSec : 1800 # 30min execTimeoutSec : 10800 # 3h slackConfig : webhookURL : slack-webhook-url mentionText : <@targetMemberID> services : - name : sample-service spreadsheetID : sample-sheets-id failFast : true targetPercentile : 99 targetLatency : 500 targetPodConfig : contextName : target-pod-context-name namespace : sample-namespace labelKey : run labelValue : sample-api containerName : sample-api scenarioSpecs : - name : check-max-throughput subName : 50req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "50" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 100req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "100" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 125req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "125" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 150req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "150" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 175req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "175" - name : ENV value : "dev" - name : DURATION value : "180" この例ではGatling Commanderの設定ファイルで停止条件として、 services[].targetPercentile に 99 、 services[].targetLatency に 500 を指定しています。これにより、あるシナリオの負荷試験結果で指定した目標レイテンシ(99%ile値で500ミリ秒)の閾値を超えると、後続のシナリオの負荷試験は実行されません。またGatling Commanderの設定ファイルの services[].scenarioSpecs[] フィールドの要素にはリクエスト数が50・100・125・150・175req/secに対応する複数の負荷試験シナリオの設定値を記述しています。 Gatling Commanderはこれらの負荷試験シナリオを設定ファイルの記載順で実行するため、小さいリクエスト数のシナリオを先に記述します。このように設定値を記述すると、Gatling Commanderはあるシナリオの負荷試験で目標レイテンシの閾値を超えるまで、順にリクエスト数を増やして負荷試験を実行します。もし125req/secのシナリオで負荷試験結果が目標レイテンシの閾値を超えると、Gatling Commanderは150req/sec以降のシナリオを実行せずに負荷試験を終了します。 このようにGatling Commanderでは事前に設定ファイルで停止条件を指定し、負荷試験結果が指定した条件と一致した場合に後続の負荷試験を自動停止できます。 負荷試験結果・コンテナメトリクスを自動記録 Gatling Commanderは設定ファイルで指定したGoogle Sheetsに、負荷試験結果と負荷試験対象コンテナのメトリクスを自動で記録します。 Gatlingコンテナが稼働するPodの実行が終了すると、Gatling OperatorはGatlingの結果レポートを指定したクラウドストレージにアップロードします。Gatling Commanderはこのクラウドストレージにアップロードされた結果レポートを取得します。 またGatling Commanderは負荷試験の実行中に、負荷試験対象コンテナのメトリクスを一定間隔で取得し続けます。このメトリクスはKubernetesクラスタのMetrics APIから取得します。そして負荷試験の実行完了後に、Gatling Commanderは取得したコンテナメトリクスの平均値を計算します。 続いてGatling Commanderは取得した結果レポートと、計算したコンテナメトリクスの平均値から負荷試験結果を生成します。生成した負荷試験結果を、Gatling Commanderの設定ファイルで指定されたGoogle Sheetsへ記録します。 次に示す画像が、Google Sheetsへの負荷試験結果の記録例です。 負荷試験結果の記録先となるGoogle SheetsはGatling Commanderの設定ファイルでserviceごとに指定できます。また同一service内では、 services[].scenarioSpecs[].name フィールドの値と実行日を基にしたシート名で、記録先シートを自動作成します。 負荷試験の完了を通知 Gatling Commanderでは設定ファイルに通知先を指定して、すべての負荷試験シナリオが完了したことを通知できます。通知先は Slack を利用でき、メンバーIDを指定することで特定メンバーへのメンションも可能です。 次に示す画像はSlack通知の例です。 この画像の例は、Gatling Commanderで実行した負荷試験が正常終了した場合の通知です。 Gatling Commanderは複数の負荷試験をまとめて実行可能であり、1回のコマンド実行あたりの負荷試験全体の実行時間は長くなります。このように事前にSlack通知を設定することで、作業者はGatling Commanderの実行完了時にSlackのメンションですぐに気が付けます。 Gatling Commanderの実装 本節ではGatling Commanderについて、利用技術、処理の概要、主な機能の実装として、Kubernetesオブジェクトの操作の実装・Goroutinesによる並列処理の実装を説明します。 利用技術 Gatling CommanderはGoで実装しています。 従来の負荷試験実施の方法では、負荷試験の際に必要な一部の操作をシェルスクリプトとして実装していました。そのため開発時に元々あったシェルスクリプトの機能を拡張し、課題を解決するアプローチも検討しました。しかし機能の拡張のしやすさを考えると、別の言語で新たに実装した方が良いと判断し、元々シェルスクリプトで行っていた処理も含めて新たにGoでCLIツールを実装しました。 Goは client-go ・ apimachinery ・ api などKubernetes周辺の開発で利用できるモジュールが充実しており、Kubernetes関連のツールの実装と相性が良いです。またGatling OperatorもGoで実装されており、Gatling Resourceに対応するGoの構造体はGatling Operatorのリポジトリで定義されています。これらの理由からGatling Commanderの開発言語にはGoを採用しました。 またGatling Commanderは、CLIを作成するためのインタフェースを提供するGoのモジュールである Cobra をベースに実装しています。Cobraを利用することでCLIインタフェースの実装に工数をかけずに開発ができました。 次にGoogle Sheetsへの結果の記録には Google Sheets API を利用しています。 Google Sheets APIへのリクエストには、Goのモジュールである Google APIs Client Library を使用しています。このモジュールは Google Cloud サービスへアクセスする機能を提供します。Gatling Commanderはこのモジュールの sheets パッケージを利用して、Google Sheets APIへリクエストを送り、Google Sheetsへ負荷試験結果を記録します。 処理の概要 Gatling Commanderは負荷試験実施の一連の作業を、次に示す各処理で実装しています。これらの一連の処理は次の流れで行います。 Gatling Commanderの設定ファイルの読み込み 負荷試験用コンテナイメージをBuild&Push 負荷試験シナリオに対応するGatling構造体のオブジェクトを作成 負荷試験対象APIが稼働するコンテナのリソース割り当ての値を取得 KubernetesクラスタにGatlingオブジェクトを作成 負荷試験の実行中に、負荷試験対象APIが稼働するコンテナのメトリクスを取得 負荷試験の実行後に、アップロードされた負荷試験の結果レポートをクラウドストレージから取得 負荷試験結果をGoogle Sheetsへ書き込み 次の負荷試験シナリオの負荷試験を実行 同一service内に複数のシナリオがある場合、 3.負荷試験シナリオに対応するGatling構造体のオブジェクトを作成 から 9.次の負荷試験シナリオの負荷試験を実行 までの処理をシナリオごとに繰り返します。またserviceごとの、 3.負荷試験シナリオに対応するGatling構造体のオブジェクトを作成 から 9.次の負荷試験シナリオの負荷試験を実行 までの処理は並列で実行します。 Kubernetesオブジェクトの操作の実装 Gatling CommanderはGoのモジュールを利用してKubernetesのオブジェクトを操作します。 具体的には次の操作をします。 Gatlingオブジェクトの取得・作成・削除 負荷試験対象APIが稼働するコンテナのメトリクスを取得 Kubernetesのオブジェクトを操作するためにはKubernetes APIへリクエストを送ります。GoでKubernetes APIへリクエストを送るために、各Kubernetes Resourceに対応するClientパッケージを利用します。Clientパッケージを利用することで、Goのオブジェクトのメソッドを通してAPIへのHTTPリクエストを送れます。 前述した client-go モジュールでは、PodやDeploymentなどKubernetesの標準Resourceを扱うClientを提供します。またKubernetesのControllerを開発する際に利用される controller-runtime モジュールでもClientを提供します。 Gatling Commanderでは controller-runtime の client パッケージでClientを初期化し、Kubernetesのオブジェクトを操作します。 client パッケージを用いて、Kubernetes Resourceを操作するには、操作対象のResourceのSchemeを追加する必要があります。SchemeはKubernetes Resourceについて、Kubernetes APIで扱うKubernetesオブジェクトと対応するGoの構造体をマッピングします。操作対象のKubernetes ResourceのSchemeを追加すると、 client パッケージで初期化したClientから対象のResourceを操作できます。 Gatling Commanderでは次の実装でClientを初期化します。簡略化のため一部処理の実装を省略しています。 import ( gatlingv1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" ctrlConfig "sigs.k8s.io/controller-runtime/pkg/client/config" ...省略 ) func InitClient(k8sCtxName string ) (ctrlClient.Client, error ) { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(gatlingv1alpha1.AddToScheme(scheme)) k8sConfig, err := ctrlConfig.GetConfigWithContext(k8sCtxName) if err != nil { return nil , err } cl, err := ctrlClient.New(k8sConfig, ctrlClient.Options{ Scheme: scheme, }) ...省略 } 各Kubernetes ResourceのSchemeは、各Resourceに対応する構造体を定義したGoのモジュールで提供されます。各モジュールが提供する AddToScheme() 関数にSchemeオブジェクトを渡すことで、Kubernetes ResourceごとのSchemeがSchemeオブジェクトに追加されます。 Clientは controller-runtime モジュールの client パッケージが提供する New() 関数で初期化できます。上記の実装では cl 変数が初期化したClientです。Schemeオブジェクトを New() 関数の引数に渡すことで、ClientはSchemeを追加したKubernete Resourceについて、メソッドを通してリクエストを送れるようになります。 初期化したClientの Create() メソッドを利用すると、次の実装でGatlingオブジェクトを作成できます。簡略化のため一部処理の実装を省略しています。 func CreateGatling(ctx context.Context, cl ctrlClient.Client, gatling *gatlingv1alpha1.Gatling) error { ...省略 if err := cl.Create(ctx, gatling); err != nil { return err } return nil } この実装の CreateGatling() 関数では引数 cl で、初期化したClientを受け取っています。引数 cl の Create() メソッドの引数には、Gatling構造体オブジェクトのポインタを渡して実行します。 Create() メソッドの実行によりKubernetes APIにリクエストを送り、KubernetesクラスタにGatlingオブジェクトを作成します。 このようにGoの controller-runtime 等のモジュールを活用して、簡単にKubernetes Resourceの操作を実装できました。 Goroutinesによる並列処理の実装 Gatling CommanderではGoが提供するGoroutinesを活用して、複数箇所で処理を並列実行しています。 GoroutinesはGoのランタイムにより管理される軽量なスレッドです。 go キーワードを関数の前につけることで、その関数を他のコードと別のスレッドで実行できます。 Gatling CommanderでのGoroutinesの主な利用用途は次の2つです。 serviceごとの負荷試験の並列実行 実行した負荷試験の終了の待機処理と、負荷試験対象コンテナのメトリクス取得処理の並列実行 Gatling Commanderでは次の実装でserviceごとの負荷試験を並列実行しています。簡略化のためここでは負荷試験の実行処理等の実装を省略しています。 wg := new (sync.WaitGroup) for _, service := range config.Services { wg.Add( 1 ) go func (ctx context.Context, s cfg.Service) { defer wg.Done() // 負荷試験の実行処理 ...省略 }(ctx, service) } wg.Wait() 上記の実装の中で config 変数はGatling Commanderの設定ファイルに対応する構造体のオブジェクトです。 config オブジェクトの Services フィールドでは各serviceの全ての負荷試験シナリオを含む設定値を持ちます。 for ブロックの中で go キーワードを無名関数の前につけて実行しています。この関数には go キーワードがついているため、serviceごとに呼び出し元のスレッドとは別のスレッドで実行されます。この関数の中では、Gatling Resourceオブジェクトの作成など負荷試験の根幹的な処理をします。 上記の実装では、呼び出し側のスレッドで実行したGoroutinesの完了を待つために wg 変数を作成しています。 wg 変数は sync パッケージの WaitGroup のポインタ型変数です。 WaitGroup 型が提供する Wait() メソッドを利用すると、実行したGoroutineの完了まで呼び出し側のスレッドで処理を待機できます。 Wait() メソッドは WaitGroup オブジェクトの持つカウンタが0になるまで処理をブロックします。また Add() メソッドはこのカウンタの値を増やし、 Done() メソッドはこの値を減らします。 上記の実装では、Goroutinesの起動時に呼び出し側で Add() メソッドを呼び、その後 Wait() メソッドを呼んでいます。それぞれのGoroutines内で defer キーワードをつけて、 Done() メソッドを呼んでいます。関数に defer キーワードをつけることで実行が遅延され、無名関数のブロックを抜ける前に必ず Done() メソッドが実行されます。 Wait() メソッドではカウンタが0になるまで処理をブロックするため、起動したすべてのGoroutines内で Done() メソッドが呼ばれるまで処理を待機します。 このようにGatling CommanderではGoroutinesを利用して、serviceごとの負荷試験を並列実行しています。 またGatling Commanderでは、実行した負荷試験の終了の待機処理と、負荷試験対象コンテナのメトリクス取得処理を並列で実行します。serviceごとの負荷試験の並列実行の実装と異なる点は、起動したGoroutinesを関数の呼び出し元のスレッドから終了する点です。 このようなGoroutines間の連携ではGoのChannelsを利用できます。Channelsを利用すると、異なるGoroutinesで実行している関数同士で、特定の型の値を送受信してやり取りできます。 実行した負荷試験の終了を待機する関数では、負荷試験の実行状況を一定間隔で確認し続けます。そして負荷試験の実行終了を確認すると、並列実行していた負荷試験対象コンテナのメトリクスを取得する関数に、負荷試験の実行終了を知らせます。 それぞれの関数は異なるGoroutinesとして実行されているため、関数間でのやり取りにChannelsを利用しています。 Gatling Commanderでは次の実装で、負荷試験の実行終了の待機と、負荷試験対象コンテナのメトリクス取得を並列実行しています。 wg := new (sync.WaitGroup) wg.Add( 1 ) informJobFinishCh := make ( chan bool , 1 ) metricsUsageCh := make ( chan kubeapiTools.MetricsField, 1 ) go kubeapiTools.FetchContainerMetricsMean(ctx, wg, metricsCl, metricsUsageCh, informJobFinishCh, targetPodConfig) err = gatlingTools.WaitGatlingJobRunning(ctx, k8sGatlingClient, gatling, waitExecTimeout, informJobFinishCh) if err != nil { return nil , fmt.Errorf( "failed to wait gatling job running, %v" , err) } close (informJobFinishCh) wg.Wait() close (metricsUsageCh) metricsUsageMean, ok := <-metricsUsageCh if !ok { fmt.Fprintf(os.Stderr, "metricsUsageCh value is empty, so each metricsUsage field value is 0" ) } この実装では、負荷試験対象コンテナのメトリクスを取得する関数が FetchContainerMetricsMean() 、実行した負荷試験の終了を待機する関数が WaitGatlingJobRunning() です。 FetchContainerMetricsMean() 関数の前には go キーワードがあり、 WaitGatlingJobRunning() 関数を実行するスレッドとは別のスレッドで実行されます。 WaitGatlingJobRunning() 関数を実行するスレッドから FetchContainerMetricsMean() 関数を実行するGoroutinesを終了するために利用するChannelsが informJobFinishCh 変数です。 FetchContainerMetricsMean() 関数と WaitGatlingJobRunning() 関数ではどちらも引数に informJobFinishCh 変数を受け取ります。 WaitGatlingJobRunning() 関数では負荷試験が終了した際にこの informJobFinishCh 変数へ値を送信します。一方で FetchContainerMetricsMean() 関数では informJobFinishCh 変数に書き込まれた値を受信します。 Gatling Commanderでは次の実装で、 WaitGatlingJobRunning() 関数内でChannelsへ値を送信します。簡略化のためここでは負荷試験の実行状況の確認処理を省略しています。 func WaitGatlingJobRunning( ctx context.Context, cl ctrlClient.Client, gatling *gatlingv1alpha1.Gatling, timeout int32 , jobFinishCh chan bool , ) error { defer func () { jobFinishCh <- true }() for { // 負荷試験の実行状況の確認処理 ...省略 } } WaitGatlingJobRunning() 関数では informJobFinishCh 変数の値を引数の jobFinishCh で受け取ります。そしてChannelsである jobFinishCh 引数へ値を送信します。 jobFinishCh 引数への値の送信は defer キーワードをつけた無名関数内で実行しています。 defer キーワードにより WaitGatlingJobRunning() 関数では、関数ブロックを抜ける前に必ず jobFinishCh 引数への値の書き込みが実行されます。 Gatling Commanderでは次の実装で、 FetchContainerMetricsMean() 関数内でChannelsから値を受信します。簡略化のためここではコンテナメトリクスの取得処理等を省略しています。 func FetchContainerMetricsMean( ctx context.Context, wg *sync.WaitGroup, cl metricsClientset.Interface, resultCh chan MetricsField, receiveGatlingFinishedCh chan bool , podConfig cfg.TargetPodConfig, ) { defer wg.Done() ...省略 for { select { case <-ctx.Done(): return case <-receiveGatlingFinishedCh: // 取得したコンテナメトリクスの平均値を計算 // 計算したコンテナメトリクスの値を呼び出し側に送信 ...省略 return default : // コンテナメトリクスの取得の処理 ...省略 } } } FetchContainerMetricsMean() 関数内で informJobFinishCh 変数の値を、引数の receiveGatlingFinishedCh で受け取ります。そしてChannelsである receiveGatlingFinishedCh 引数から値を受信します。 FetchContainerMetricsMean() 関数では、無限ループとなる for ブロックの中で select ブロックを使用して処理を条件分岐しています。 WaitGatlingJobRunning() 関数でChannelsに値が送信されると、 case <-receiveGatlingFinishedCh の条件に一致します。この条件に一致した場合、呼び出し側へ取得したコンテナメトリクスの値を渡して for ブロックを抜けます。 上記の実装では省略していますが、取得したコンテナメトリクスの値を呼び出し側に渡す際には、Channelsである引数の resultCh へ値を送信しています。 このようにGatling Commanderでは開発言語としてGoを採用したことで、GoroutinesとChannelsを活用し簡単に並列処理を実装できました。 Gatling Commanderの導入効果 Gatling Commanderの導入により、Gatling Operatorのみで行っていた従来の負荷試験において、作業者が手作業で実施していたほとんどの作業を自動化できました。 次に示すのが自動化できた作業の一覧です。 負荷試験シナリオを含むコンテナイメージのBuild&Push Gatling ResourceのKubernetesマニフェストを作成 負荷試験の開始(Gatling ResourceをKubernetesクラスタへデプロイ) 負荷試験対象APIが稼働するコンテナのメトリクスを取得 負荷試験の実行状況を確認 負荷試験結果(Gatling Report)をGoogle Sheetsへ記録 前節( 従来の実施方法 )で説明した作業の一覧と比較すると、 2.負荷試験シナリオを含むコンテナイメージのBuild&Push(アップロード) 以降の全ての作業が自動化できています。 またこれらの作業の中には作業者が負荷試験シナリオごとに設定を変えて繰り返す作業もありました。Gatling Commanderでは設定ファイルに複数の負荷試験シナリオに対応する値を指定できます。事前に複数のシナリオの値を指定することで、従来の負荷試験実施の方法で作業者が繰り返していた設定変更の作業は不要になりました。 このようにGatling Commanderを利用することで、負荷試験全体で作業者が要する工数を大幅に削減できました。作業者は一度Gatling Commanderのコマンドを実行すれば、全てのシナリオで負荷試験の実行が完了し、終了通知が来るまで負荷試験の実施を放置できるようになりました。 本節では、中でも特にGatling Commanderによる自動化の恩恵が大きかった部分について説明します。 負荷試験シナリオ切り替えの手間が不要になった 前節( 設定値の変更の手間 )の通り、従来の負荷試験実施ではシナリオごとの設定値の変更作業が作業者の大きな手間でした。 Gatling Commanderでは複数のシナリオごとの設定値を、1つの設定ファイルで記述できます。また負荷試験シナリオはservice単位でグループ化し、シナリオ間で共通する値の指定は共通化しています。serviceごとの負荷試験は並列で実行し、同一service内のシナリオごとの負荷試験は順次実行します。 事前に全ての負荷試験シナリオの設定値をGatling Commanderの設定ファイルに記述すると、Gatling Commanderは自動でシナリオごとの設定値を切り替えます。そのため従来発生していた作業者による設定値の変更作業は不要です。 負荷試験シナリオの切り替えの自動化により、作業者の工数を大きく削減できました。 負荷試験実行の状況確認の手間が不要になった 前節( 実行状況の確認の手間 )の通り、従来の負荷試験実施ではシナリオごとに作業者が実行状況を確認して、次のシナリオを実行していました。 Gatling Commanderは、作業者が確認していた次の点を自動で確認します。 前に実行した負荷試験が実行中か 実行した負荷試験の結果が閾値(目標レイテンシ、リクエストのFail数が0)を超えていないか 前節( Goroutinesによる並列処理の実装 )の通り、Gatling Commanderは実行中の負荷試験の終了を待機します。また前節( 過負荷時に負荷試験を自動停止 )の通り、事前に停止条件を指定することで過負荷時に負荷試験の順次実行を自動停止できます。Gatling Commanderが提供するこれらの機能により、従来の負荷試験で作業者が行っていた上記の点の確認作業は不要です。 例えばAPIの最大スループットを測る場合、従来の方法では作業者はAPIのレイテンシを見ながらその度にシナリオを変更して、負荷試験結果が閾値を超えないか確認していました。すべてのシナリオが完了するまでには最小でも 負荷試験の試行回数 x 負荷試験の実施時間 の時間が必要でした。試行回数が10回、実施時間が3分の場合、全ての負荷試験を合わせて最小でも30分かかります。実行状況の確認はこの間に頻繁に行う必要のある割り込み作業であり、作業者は全体で30分かかる待ち時間を効率的に使えていませんでした。 Gatling Commanderを利用すれば、従来の方法で作業者が行っていた実行状況の確認は不要になります。割り込み作業はなくなるため、作業者はすべての負荷試験シナリオが完了するまでの時間を最大限に活用し、他のタスクを進めることができます。 負荷試験の実行状況の確認作業の自動化により、作業者は複数のシナリオを持つ負荷試験の実行待ち時間を効率的に利用できるようになりました。 負荷試験結果の記録の手間が不要になった 前節( 結果の記録の手間 )の通り、従来の負荷試験実施ではシナリオごとに作業者が結果を記録していました。 Gatling Commanderは負荷試験の実行後に、負荷試験結果とコンテナメトリクスをGoogle Sheetsに自動記録します。この機能により、従来の負荷試験実施で作業者がシナリオごとに行なっていた記録作業は不要になります。 またGatling Commanderの負荷試験コマンド実行の終了後、作業者はすぐにシナリオごとの結果をGoogle Sheetsで一覧できるようになりました。 結果の記録作業は負荷試験実施の作業でも大きなトイルであったため、この点の自動化は作業者の工数削減に大きく貢献しました。 今後の展望 本節では今後の展望として、Gatling Commanderを応用したAPIパフォーマンスの劣化検知と、Gatling Commanderの機能追加・改善について説明します。 APIパフォーマンス劣化検知への利用 MLOpsブロックではGatling Commanderを利用したAPIパフォーマンスの劣化検知の仕組み作成を検討しています。 MLOpsブロックでは複数のAPIを本番運用していますが、短いスパンでの負荷試験実施はできていません。 新規開発や大きな機能変更の際には必ず負荷試験を実施し、日々の運用ではレスポンスタイムの監視も行なっています。しかしこれらの負荷試験の実施頻度は中・長期であり、依存ライブラリの更新など比較的小さな変更によるAPIパフォーマンスの変化は確認できていません。 劣化検知の仕組みを作成することで、APIのパフォーマンス悪化を早期に発見し、より安定して運用できるようにします。 劣化検知のためには負荷試験の定期的な実施が必要ですが、MLOpsブロックで運用するAPIは10以上あるため全てのAPIで負荷試験を実施する工数は大きいです。Gatling Commanderを利用することで、この工数を削減できるため、APIパフォーマンスの劣化検知の仕組みを作成できると考えています。 本節ではGatling Commanderを利用したAPIパフォーマンスの劣化検知の仕組みの構成案を簡単に説明します。 GitHub Actionsとの連携による負荷試験の定期実行 APIパフォーマンスの劣化検知の仕組みの構成案として、Gatling Commanderを利用した負荷試験を GitHub Actions でスケジュール実行することを考えています。 MLOpsブロックではCIツールとしてGitHub Actionsを利用しています。GitHub ActionsはGitHubが提供するCI/CDツールです。GitHub Actionsでは schedule イベントを利用することでスケジュールした時刻にGitHub Actionsのワークフローをトリガーできます。 Gatling CommanderはCLIツールであり、Goの実行環境があればインストール可能です。事前にGatlingのSimulation、Gatling Commanderの設定ファイルをリポジトリ内に用意します。GitHub Actionsのワークフローで、Gatling Commanderのコマンド実行時にこれらのファイルを指定して、負荷試験を実行できます。 このようにGatling CommanderのコマンドをCIでスケジュール実行することで、比較的簡単に定期的な負荷試験の実行が可能です。この構成案によりAPIパフォーマンスの劣化検知の仕組みを作成できると考えています。 機能追加・改善 本節では、Gatling Commanderで検討している今後の機能追加・改善について説明します。 Kubernetes以外で稼働するAPIへの対応 MLOpsブロックで運用するAPIはすべてKubernetes上で稼働しており、Gatling Commanderは負荷試験対象のAPIがKubernetes上で稼働する前提に実装されています。そのため本記事の公開時点でGatling Commanderは、Kubernetes以外の環境で動作するAPIの負荷試験に対応していません。 負荷試験ツールとしてより利用しやすくするために、任意のインフラで稼働するAPIを負荷試験対象にできるよう対応を検討中です。 Google Cloud以外のプロバイダへの対応 MLOpsブロックではGoogle Cloudのサービスを利用してインフラを構築しています。Gatling Commanderでは現状、Google Cloudが提供する Cloud Storage や Artifact Registry ・ Container Registry 以外のクラウドストレージ、レジストリサービスの利用に対応していません。そのため公開時点では Amazon Web Services (AWS)の S3 や Elastic Container Registry などGoogle Cloud以外のプロバイダで提供されるクラウドストレージ、レジストリサービスを利用できません。 今後の機能改善としてAWSなど他のプロバイダが提供するクラウドストレージ、レジストリサービスへの対応を考えています。 取得するメトリクスの拡充 現状Gatling Commanderが取得可能なメトリクスはコンテナのCPU・メモリ使用率のみです。自動で取得可能なメトリクスが増えることで、APIのパフォーマンス計測において作業者はより充実した情報を、工数をかけずに得ることができます。 既存のコンテナメトリクスに加えて、Google CloudのCloud MonitoringのようなリソースモニタリングやDatadogのようなアプリケーションのパフォーマンス監視の値を取得可能にする機能追加を検討中です。 まとめ 最後までお読みいただきありがとうございました。 本記事ではMLOpsブロックで抱えていた負荷試験実施の課題と、解決のために開発、公開したOSSのCLIツール、Gatling Commanderについてご紹介しました。 Gatling Commanderを利用して負荷試験を実施することで、従来の負荷試験で作業者が実施していた手作業を大幅に削減できました。本記事での事例の紹介が皆様のお役にたてば幸いです。またGatling CommanderではIssueやPull Requestを歓迎しています。ご興味のある方はぜひご利用ください。 最後になりますが、ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用しているので、ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
DevRelブロックの @ikkou です。もうすぐ2023年も終わりますね。皆さんは師走のイベント、「アドベントカレンダー」に参加しましたか? ZOZOは例年アドベントカレンダーに参加していて、2020年は計100本、2021年は計125本、2022年は175本と年々本数を増やしてきました。そして今年は昨年以上となる計225本の記事公開を“完走”しました。本記事ではその概要をお伝えします。 ZOZO Advent Calendar 2023 今年は計9個のカレンダーを完走し、12/1-25の期間中に合計225本の記事を公開しました。 ZOZO ADVENT CALENDAR 2023、無事完走しました! 今回はなんとシリーズ9まで、合計225件の記事を公開しました! 読んでくださったみなさま、ありがとうございました! https://t.co/2gxEcXIryx #zozo_engineer #Qiitaアドカレ #Qiita pic.twitter.com/4t9Njp4cEQ — ZOZO Developers (@zozotech) 2023年12月25日 qiita.com 実施概要 アドベントカレンダーは「任意参加」で実施しています。Slackチャンネルで実施と参加を呼びかけ、各自で空いている日に登録する運用となっています。 公開する先はZOZO TECH BLOGだけでなく、QiitaやZenn、noteや個人のブログなど自由です。ZOZO TECH BLOGよりも気軽に書けるという観点から、QiitaやZennに書く方が多いです。 1人で複数の記事を書くこともあるので、今年は225本の記事に対して123名が参加しました。昨年に続き100名以上が参加しています。ちなみにもっとも多くの記事を書いたのは @shiozaki で、全部で22本の記事を公開しています。 アドベントカレンダーはアウトプットの練習に適したイベントです。ZOZOではテックブログをアウトプットの主軸に置いていますが、「まだテックブログを書く自信が無い」「テックブログに書くにはネタが小粒」のような場合に、アドベントカレンダーは良い機会です。 2023年の振り返り ZOZOのアドベントカレンダーでは例年その年を振り返る記事を公開しています。 開発組織については昨年同様、執行役員 兼 CTOの @sonots が「振り返りと現状」記事を執筆しています。 qiita.com また、2021年からはコーポレート広報チームによる「ファッションテックハイライト」も公開しています。あわせてご覧ください。 technote.zozo.com 過去のアドベントカレンダー ZOZOでは、2018年から毎年アドベントカレンダーに参加しています。 ZOZO Advent Calendar 2022 qiita.com 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
はじめに こんにちは! 24年度新卒内定者の石上です。11/21-22に開催された24年度新卒の内定者向け開発合宿に参加してきました! 内定者向けの開発合宿は4年ぶりの開催で、神奈川県の湯河原にて行われました。 今年は、24年度新卒エンジニア職内定者のうち参加を希望した学生と現役エンジニアの方が集まって開催されました。本記事では、開発合宿の様子と参加した感想についてレポートします! 集合 開催地の湯河原には、品川駅から踊り子号に乗って、約1時間で到着しました! 都心からのアクセスもよく、自然が豊かで温暖な気候なので、またプライベートでも訪れてみたいです。 宿 皆到着したら駅から送迎バスで宿に向かいます。丘の上にあるので、宿泊施設の中からきれいな湯河原の景色が見えました! 10分ほどで、今回宿泊するお宿、 レクトーレ湯河原 さんに到着です! 今回開催された宿は、研修施設として用いられており、研修室や大浴場などがありました。ロビーは吹き抜けになっており、開放感がありました。 自己紹介 人自 1 の門垣さんに開発合宿について説明してもらった後に、参加者の自己紹介をしました! みんな緊張しつつ開発合宿を楽しみにしていたようです。 みんなの自己紹介が終わったらお昼ごはんの時間です! 現場のエンジニアの方達だけでなく、全国から集まった内定者メンバーとも交流できました。 開発 こんにちは、24年度新卒内定者の岩崎です。 昼ごはんも食べ終わり、早速開発が始まりました。今回は「日常の課題を解決する」をテーマに、各々で事前に開発案を考えてきました。事前に開発を進めるのはNGだったので、各々0から開発を始めていきます。みなさんすごい切り替え&集中力で驚きました。 実装でわからない部分は内定者同士で相談し合ったり、先輩社員に尋ねたりしながら開発を進めていきました。 途中、人自の方から飲み物や軽食の差し入れも頂きました! ありがとうございました! ご飯 開発の合間に晩ご飯の時間が訪れました。とても豪華な食事が用意され、お酒も提供されました。 内定者と先輩社員がランダムに着席して、それぞれ色々な話をして盛り上がっていました。 開発の疲労を吹き飛ばすくらい美味しいご飯でした! 部屋 部屋の割り当ては、事前のアンケートをもとに1人部屋と4人部屋に分けられました。各部屋にはネット環境も整っているので開発もできます。 晩ご飯以降は自由時間だったので、誰かの部屋やフリースペースに集まって開発を続ける・温泉に入る・コンビニに行く・寝るなど、それぞれ自由に過ごしていました。 発表会 2日目は発表スライドを作り、すぐに発表会が始まりました。 1人5分程度で自分の成果物についてプレゼンします。発表順は1日目にくじ引きで決めました。 アプリやチャットBot、拡張機能など様々な方法で課題解決ができていてとても感心しました! 何よりみなさん短い時間にもかかわらず、しっかり動く物を創りきっていたのが本当に素晴らしいと思いました! 表彰式 こんにちは、24年度新卒内定者の松石です。 表彰は、内定者賞、先輩スタッフ賞、CPO賞、CTO賞がありました。各賞は以下の5項目をもとに選出されました! 成果物のアウトプット全体 課題解決できる手段になっているか 技術選定 ソウゾウのナナメウエ要素 挑戦しているか 発表を聞いて、誰が選ばれてもおかしくないなという印象でした。そのため、参加者はみんな緊張しつつもワクワクしている雰囲気がとてもよかったです! 受賞しなかったプロダクトもですが、受賞したプロダクトは、特に課題解決の手段としてわかりやすく、機械学習や今流行りの生成AIを用いたプロダクトが多い印象を受けました! 生成AIを活用したチャットアプリや、予算から1週間の献立を考える実用的なプロダクトなどがありました。特に、既存のデータから新しい価値を作り出しているプロダクトが表彰されていたように思います! 受賞者の5名からもコメントを頂いています! 内定者賞(桐島) 内定者賞の受賞ありがとうございます。多くの方に投票していただけて、とてもうれしいです! 今後も自分の好きなことをとことん追求し、がんばっていきたいと思います!! 先輩スタッフ賞1(Armin) このような機会また評価をいただき、本当に感謝しています。これからも皆さんと一緒に、人々の生活を少しでも良い方向に変えられるような開発をしていきたいと願っています。 先輩スタッフ賞2(田中) このような賞を頂けてとても光栄でした! これからも自分や、身近な人の課題を解決するような開発を続けていきたいです! CPO賞(畠中) 課題解決と世界観を重視して開発を行ったのでその点を評価いただき受賞出来たこと大変嬉しく思います! これからも日々進歩していきたいと思います! CTO賞(諸田) まさか賞をいただけると思っていなかったので、とてもとても嬉しいです! 企画していただきありがとうございました! 同期の発表を見て、技術の知識やアウトプットの量がまだまだ足りないなと感じたのでコツコツ経験を積んでいけたらなと思います! 懇親会 懇親会では、先輩社員と内定者を含めたグループに分かれて行われました! ここでは、ランチバイキングを楽しみながら開発合宿の感想を言い合ったり、自分がやっている研究や趣味などざっくばらんに話すことができました(疲れた体にビールが染みました!)。 解散 解散後は、バスで駅まで向かいましたが、バスの中で開発合宿の成果物や使用した技術などについて議論しながら帰っていました。駅到着後は、恒例のお土産物色を楽しみました! 感想 2日間とても楽しかったです! これまでも内定者と話す機会はありましたが、今回の開発合宿では、夜一緒に開発したり議論したりするメンバーが多かったです。そのため、これまでよりもコミュニケーションを多く取ることができ、内定者の仲が確実に深まりました! 自分は、賞を取れなかったため、どこかでリベンジしたいと思います! P.S. 2日後に内定者メンバーとフットサルをしに、京都から東京までいきました。全身筋肉痛。 お忙しい中、開発合宿を企画してくださった人自の方々をはじめ、来てくださったメンター社員の方々本当にありがとうございました! おわりに ZOZOでは、新卒エンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com 当社では、「人事」のことを「人自」と表記します。「人事(ひとごと)」ではなく、スタッフ一人ひとりが他人の事も自分の事として考えられる会社にしたいという意味が込められています。 ↩