TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

988

こんにちは、MA基盤チームの田島です。私達のチームではMAIL、LINE、PUSH通知といったユーザへの配信をしています。その中でもマス・セグメント配信という一斉に行う配信では、配信対象者のセグメント抽出にBigQueryを利用しています。また、配信前に必要なデータをBigQueryに連携しデータマートの集計をしたり、配信後には配信実績の登録などの更新処理をしています。 そのような処理を定期的に行っているため、ネットワークの問題やサーバーの不調などにより処理が途中で失敗することがあります。そこで、リトライを容易にするため、すべての処理を冪等にしました。今回その中でも、BigQueryの追記処理に絞ってどのように冪等化したのかについて紹介します。 目次 目次 マス・セグメント配信基盤の紹介 課題 冪等化 BigQuery追記処理に関する冪等化の取り組み 冪等にならないケース INSERT 初期データ 最初のINSERT処理 INSERT後のデータ INSERT処理をリトライ リトライ後のデータ DestinationTableのWRITE_APPEND 初期データ 参照元のデータ 最初のWRITE_APPEND処理 WRITE_APPEND後のデータ WRITE_APPEND処理をリトライ リトライ後のデータ 3種類の冪等化 Overwrite 例)データ連携 例)データマート更新 使い所 Copy Table & Append 例)データ連携 使い所 Create Table & Merge 例)SELECT結果をAPPEND ひと工夫 もうひと工夫 使い所 比較 冪等化の結果 まとめ マス・セグメント配信基盤の紹介 まず、各配信の流れを簡易化したものが以下になります。 上記の流れはDigdagというワークフローエンジンを利用することで実現しています。 Digdagの基盤については、以下のテックブログをご参照ください。 techblog.zozo.com 課題 バッチ処理においては、ネットワークの問題やサーバーの不調により予期せぬタイミングで処理が異常終了することもあります。発生時にはDigdagのリトライ機能を利用し、処理単位ごとに自動リトライしています。しかし、なにも考えずにリトライするとデータが重複して登録されたり、同じ配信を複数回行ってしまうといった問題が起こり得ます。 BigQueryの更新処理については、最近導入された Transactionの仕組み があります。この機能を使うことで一連のクエリをTransactionの中で完結できます。しかし、クエリの更新処理がすべて完了しCommitの完了後、アプリケーションやDigdagで異常が発生することもあります。そのときリトライすると同じトランザクション処理を再度実行してしまい、同じ処理が重複して実行されてしまいます。 このような事象はかなりレアケースではありますが、安定したバッチ処理をするにはこの問題に関しても対応する必要があります。 冪等化 上記の課題を解決するためにデータの更新処理を含め、すべての処理を冪等化しました。それにより、処理が失敗した場合はただリトライをするだけで整合性が担保されるようにしました。そして、リトライは自動的に行われているので、手動でのオペレーションも不要です。 BigQuery追記処理に関する冪等化の取り組み ここからはBigQueryのデータ処理に限定して、冪等化の取り組みについて紹介していきます。今回扱う更新処理は追記処理に限定しています。 冪等にならないケース BigQueryの追記処理において冪等にならないようなケースについてのパターンを紹介します。今回紹介するケースの他に、自己参照したデータを利用するUPDATE文も冪等にならないことがあります。しかし、私達のシステムではBigQueryにおいてそのような処理を行っていないため、今回は省略し追記処理に限定しました。 INSERT 最初はINSERT文です。BigQueryはユニークキー制約が無いため、INSERT処理は常に追記されることになります。よって、INSERT成功後に処理が失敗しリトライされてしまうとデータの重複が発生します。 PUSH通知の実績テーブルである push_delivered テーブルへのINSERT処理を例に、リトライによりデータが重複するケースを紹介します。 初期データ まず初期データとして以下の2レコードが存在するとします。 user_id message delivered_at 1 message1 2022-07-26 12:00:00 2 message1 2022-07-26 12:00:00 最初のINSERT処理 push_delivered テーブルに対し以下のINSERT処理を行います。 INSERT INTO `project.dataset.push_delivered` (user_id, message, deliverd_at) VALUES ( 3 , " message2 " , " 2022-07-26 12:00:00 " ); INSERT後のデータ INSERT処理が成功すると以下のようにレコードが1行追加されます。これが正しいデータになります。 user_id message delivered_at 1 message1 2022-07-26 12:00:00 2 message1 2022-07-26 12:00:00 3 message2 2022-07-26 13:00:00 INSERT処理をリトライ リトライされた場合、もう一度以下のINSERT処理が行われます。 INSERT INTO `project.dataset.push_delivered` (user_id, message, deliverd_at) VALUES ( 3 , " message2 " , " 2022-07-26 12:00:00 " ); リトライ後のデータ リトライされると、以下のように user_id=3 のデータが重複してしまいます。 user_id message delivered_at 1 message1 2022-07-26 12:00:00 2 message1 2022-07-26 12:00:00 3 message2 2022-07-26 13:00:00 3 message2 2022-07-26 13:00:00 DestinationTableのWRITE_APPEND 続いて紹介するのが、BigQueryのDestinationTableという機能です。 cloud.google.com これは、SELECTした結果をテーブルに書き込むことができる機能で、書き込み方法としては以下の3種類が存在します。 WRITE_EMPTY WRITE_TRUNCATE WRITE_APPEND WRITE_EMPTY と WRITE_TRUNCATE はSELECT文の結果をそのまま指定したテーブルに書き込みます。 WRITE_EMPTY の場合はテーブルのデータが存在しない場合のみ書き込みを行います。そのため、これらの処理は何度実行しても最終的なデータは同じになります。 WRITE_APPEND の場合はSELECT文の結果を指定したテーブルに追記します。そのため、同じ処理を何度も行うとそのたびに同じデータが追記され、重複データが生じてしまいます。以下にデータが重複するケースを紹介します。 初期データ まず先程の例と同じように、初期データとして以下の2レコードが存在するとします。 user_id message delivered_at 1 message1 2022-07-26 12:00:00 2 message1 2022-07-26 12:00:00 参照元のデータ 今回、WRITE_APPENDするデータとして push_delivered_20220727 というテーブルを用意します。 user_id message delivered_at 3 message2 2022-07-27 12:00:00 最初のWRITE_APPEND処理 push_delivered テーブルに対し以下のSELECT処理の結果を追記します。 SELECT * FROM `project.dataset.push_delivered20220727` WRITE_APPEND後のデータ SELECT APPEND処理が成功すると以下のようにレコードが1行追加されます。これが正しいデータになります。 user_id message delivered_at 1 message1 2022-07-26 12:00:00 2 message1 2022-07-26 12:00:00 3 message2 2022-07-26 13:00:00 WRITE_APPEND処理をリトライ リトライされた場合、もう一度以下のSELECT APPEND処理が行われます。 SELECT * FROM `project.dataset.push_delivered20220727` リトライ後のデータ リトライされると、以下のように user_id=3 のデータが重複してしまいます。 user_id message delivered_at 1 message1 2022-07-26 12:00:00 2 message1 2022-07-26 12:00:00 3 message2 2022-07-26 13:00:00 3 message2 2022-07-26 13:00:00 3種類の冪等化 上記で紹介したような処理を冪等化するために、3種類のパターンを利用しています。 Overwrite Copy Table & Append Create Temp Table & Merge それぞれ、どのようなときにどのパターンを利用しているのかについて紹介します。 Overwrite 一番簡単なやり方は、全量のデータをまるごと洗い替えする方法です。以下がデータ更新の流れです。 2つの例を元に、具体的な処理を紹介します。 例)データ連携 最初の例として、PostgreSQLに存在するテーブル push_delivered をBigQueryのテーブルに連携する場合を考えます。ここでは、データ連携にEmbulkというツールを利用したケースを紹介します。 以下がデータ連携を実現するためのEmbulkの設定ファイルです。 in : type : postgresql host : {{ env.postgres_host }} user : {{ env.postgres_user }} password : {{ env.postgres_password }} database : db query : SELECT * FROM push_delivered out : type : bigquery project : {{ env.bq_project }} dataset : {{ env.bq_dataset }} table : push_delivered mode : replace with_rehearsal : false source_format : NEWLINE_DELIMITED_JSON 上記の設定でEmbulkを実行することで、PostgreSQLに存在するテーブル push_delivered の全量データをBigQueryのテーブルに連携します。このとき mode: replace を利用しているため、BigQueryのデータは毎回PostgreSQLのテーブルにあるデータで全量上書きされます。よって、この処理を何回繰り返したとしても、結果はPostgreSQLのテーブルのデータと同じになります。 例)データマート更新 続いてデータマートの更新について考えます。データマートの更新には、DestinationTable機能を利用します。ここではPythonクライアントを使った方法を紹介します。 以下は offer_delivered テーブルにある channel = 'PUSH' のデータを push_delivered マートとしてテーブルを作成している例です。 query=`SELECT offer_delivered WHERE channel = 'PUSH' ` client = bigquery.Client() destination_table = 'project.dataset.push_delivered' job_config = bigquery.QueryJobConfig( destination=destination_table, write_disposition= 'WRITE_TRUNCATE' ) query_job = client.query(query, job_config=job_config) DestinationTable機能のうち WRITE_TRUNCATE を利用しています。 query に格納したSELECT文を実行し、その結果をそのまま push_delivered というテーブルに上書きします。この処理に関しても、参照元のデータが更新されない限り何度実行しても更新対象テーブルである push_delivered のデータは最終的に同じになります。 使い所 この方法はただ単純にデータを全量上書きすれば良いので非常に実装がシンプルになります。しかし、全量のデータを扱うため、データ量が大きいものには向いていません。データ量が大きい場合、データの処理時間や費用増加といったデメリットがあります。 よって、使い所としてはそれほどデータ量が多くないテーブルの連携やデータマートの更新処理に向いています。これから紹介する手法の中でも一番シンプルかつ安定した方法になるため、データ量が許容できるのであればこの方法を利用することをおすすめします。 Copy Table & Append 次は既存のテーブルをコピーしてから INSERT や WRITE_APPEND する方法です。これは、データを差分更新する方法で、洗い替えをするにはデータ量が多すぎる場合に利用できます。以下がデータ更新の流れです。 更新対象のテーブルをバックアップする バックアップしておいたテーブルをtempテーブルにコピーする 追記したいデータをコピーしたテーブルに追記する 最後にtempテーブルを、実際に更新したいテーブルへコピーし、tempテーブルを削除する こうすることで、何度実行しても最終的にはバックアップテーブルに対して更新データ追記したものが更新対象テーブルのデータになります。ここでもデータ連携を具体例として紹介します。 例)データ連携 今回の例は1日1回データ連携することを想定します。以下の例では追記処理としてDestinationTableのWRITE_APPEND機能を利用します。また、ここではテーブルをバックアップしておく代わりに、BigQueryのタイムトラベル機能を利用します。タイムトラベル機能は過去の日時を指定することで、そのタイミングでのデータを参照できる機能です。詳しくは以下のドキュメントを参照してください。 cloud.google.com これを利用することで、テーブルのバックアップを管理する必要がなくなります。ただし、7日間までという成約があるため、1か月ごとのデータ更新等には利用できません。 まずはじめに、差分更新したいデータをPostgreSQLから連携します。これには、Embulkを利用します。以下では、PostgreSQLの push_delivered のうち当日登録されたデータをBigQueryの push_delivered_20220727 というテーブルに連携します。 in : type : postgresql host : {{ env.postgres_host }} user : {{ env.postgres_user }} password : {{ env.postgres_password }} database : db query : SELECT * FROM push_delivered WHERE CAST(delivered_at AS DATE) = current_date out : type : bigquery project : {{ env.bq_project }} dataset : {{ env.bq_dataset }} table : push_delivered_20220727 mode : replace with_rehearsal : false source_format : NEWLINE_DELIMITED_JSON データ連携後、以下のようなスクリプトで差分更新をします。 def copy_table (self, source_table_id, destination_table_id, write_disposition): copy_config = bigquery.CopyJobConfig(write_disposition=write_disposition) job = self.client.copy_table(source_table_id, destination_table_id, job_config=copy_config) job.result() def append (self, source_table_id, destination_table_id, base_time): base_datetime = dt.strptime(base_time, '%Y-%m-%d %H:%M:%S%z' ) snapshot_epoch = int (base_datetime.timestamp()) * 1000 snapshot_table_id = "{}@{}" .format(destination_table_id, snapshot_epoch) temp_table_id = "{}_temp" .format(destination_table_id) self.copy_table(snapshot_table_id, temp_table_id, 'WRITE_TRUNCATE' ) self.copy_table(source_table_id, temp_table_id, 'WRITE_APPEND' ) self.copy_table(temp_table_id, destination_table_id, 'WRITE_TRUNCATE' ) self.client.delete_table(temp_table_id) if __name__ == '__main__' : append(push_delivered_20220727, push_delivered, '2022-07-26 12:00:00+09:00' ) スクリプトの流れは以下のようになっています。 base_time='2022-07-26 12:00:00+09:00' を指定し、更新対象テーブルである push_delivered のいつ時点をバックアップとして扱うのかを設定 バックアップテーブルを 更新対象テーブル_temp という名前でコピー tempテーブルに対して差分データが格納された push_delivered_20220727 のデータを追記 tempテーブルを更新対象テーブルに上書き こうすることで、この処理を何回実行しても push_delivered は半日前のデータに push_delivered_20220727 を追記したデータとなります。任意のタイミングのデータを参照するには テーブル名@UNIXエポック時刻 という名前でテーブルを参照することで取得できます。 また、このとき BigQueryのCopyJob機能 を使っています。DestinationTableのときと同じように WRITE_TRUNCATE WRITE_APPEND をオプションとして指定できます。そのためテーブルのコピー時に、コピー先のテーブルにデータを上書きするか追記するかが選択可能です。CopyJobの利用には料金がかからないため、コスト削減にも繋がります。 使い所 この方法を利用することでデータの差分更新が可能となり、データ量が大きいテーブルに対する処理の更新量を減らすことができます。そのため、処理時間や費用を抑えることができます。ただし、一度データをバックアップからコピーし更新をするため、同じテーブルの更新を同時にできません。また、タイムトラベル機能を利用できない場合はバックアップデータを保持しておく必要があります。 Create Table & Merge 続いての手法は、Copy Table & Appendで扱えなかった、同じテーブルへの並列更新をしたい場合に利用できます。イメージとしてはUPSERT処理に近いです。以下がその流れです。 更新対象のテーブルのスキーマ情報のみをコピー コピーしたテーブルに対してデータを追記 ユニークキーを利用し、テーブル同士をマージ このようにすると、最初の実行ではデータが追記されます。その後リトライされた場合は、MERGE処理のタイミングですでにデータが書き込まれていないデータのみ追記します。よって、この処理を何回実行しても更新対象テーブルは最終的には同じデータになります。 具体例として、SELECT結果を更新対象テーブルに追記したいケースを紹介します。 例)SELECT結果をAPPEND 以下がSELECT結果を更新対象テーブルに追記するためのSQLになります。 CREATE TEMP TABLE insert_data AS SELECT * FROM `project.dataset.offer_delivered` LIMIT 0 ; INSERT INTO insert_data (user_id, message, delivered_at, channel) SELECT user_id, message, delivered_at, ' PUSH ' FROM `project.dataset.push_delivery` MERGE `project.dataset.offer_delivered` target USING insert_data ON (target.user_id = target.user_id and target.message = insert_data.message and target.delivered_at = insert_data.delivered_at) WHEN NOT MATCHED THEN INSERT ROW ; SELECT * FROM APPEND先のテーブル名 LIMIT 0 としてテーブルをコピーし insert_data という一時テーブルを作成 作成したTEMPテーブルに格納したいデータを INSERT MERGE文を使い、更新対象テーブルと一時テーブルをマージ 1の処理にはCopyTable機能も利用できますが、その場合最後に自分でテーブルを削除する必要があります。 LIMIT 0 でのSELECTはスキャンが走らないため料金はかかりません。また、データのマージには条件としてユニークキーを渡す必要があります。 ひと工夫 このとき、テーブルサイズが大きいとMERGE処理時にフルスキャンが走るため、処理時間および料金の増加が発生します。そこで更新対象テーブルをパーティション分割しておき、MERGE文の条件にパーティションの条件を入れることでその問題を解決できます。 CREATE TEMP TABLE insert_data AS SELECT * FROM project.dataset.offer_delivered LIMIT 0 ; INSERT INTO insert_data (user_id, message, delivered_at, channel) SELECT user_id, message, delivered_at, ' PUSH ' FROM `project.dataset.push_delivery` MERGE `project.dataset.offer_delivered` target USING insert_data ON (target.user_id = target.user_id and target.message = insert_data.message and target.delivered_at = insert_data.delivered_at and DATE (target.delivered_at) >= CURRENT_DATE ( ' Asia/Tokyo ' )) WHEN NOT MATCHED THEN INSERT ROW ; もうひと工夫 上記で見ていただいたように、この手法だとテーブル毎に同じような複雑なクエリを書く必要があります。また、ユニークキーやパーティションキーに関しても別途管理が必要になります。そこで以下のようなスクリプトでクエリを生成できるようにしました。ここではテンプレートエンジンに Jinja を利用しています。これにより、必要なデータは更新したいテーブル名と、追記したいデータを抽出するSELECT文のみになります。 def select_and_append (select_sql, target_table_id, partition_base_time = date.today()): TEMPLATE_PATH = 'write_append.tmpl' client = bigquery.Client() target_table = client.get_table(target_table_id) target_table_column_names = [column.name for column in target_table.schema] time_partition_column = target_table.time_partitioning partition_column_name = None if time_partition_column is not None : partition_column_name = time_partition_column.field if time_partition_column.field is not None else '_PARTITIONTIME' params = { 'select_sql' : select_sql, 'target_table_id' : target_table_id, 'target_table_column_names' : target_table_column_names, 'partition_column_name' : partition_column_name, 'partition_base_time' : partition_base_time } env = Environment(loader=PackageLoader( 'bigquery' , 'templates' )) sql = env.get_template(TEMPLATE_PATH).render(params) query_job = client.query(sql) query_job.result() if __name__ == '__main__' : select_sql = 'SELECT user_id, message, delivered_at, ' PUSH ' FROM `project.dataset.push_delivery` select_and_append(select_sql, ' project.dataset.offer_delivery ') CREATE TEMP TABLE insert_data AS SELECT * FROM `{{ target_table_id }}` LIMIT 0 ; INSERT INTO insert_data SELECT {% for column_name in target_table_column_names %} {{ column_name }} {% if not loop . last %} , {% endif %} {% endfor %} FROM ( {{ select_sql }} ); MERGE `{{ target_table_id }}` target USING insert_data ON ( {% for column_name in target_table_column_names %} target.{{ column_name }} = insert_data.{{column_name}} {% if not loop . last %} and {% endif %} {% endfor %} {% if partition_column_name is not none %} and DATE (target.{{ partition_column_name }}) >= parse_date( ' %Y-%m-%d ' , ' {{ partition_base_time }} ' ) {% endif %} ) WHEN NOT MATCHED THEN INSERT ( {% for column_name in target_table_column_names %} {{ column_name }} {% if not loop . last %} , {% endif %} {% endfor %} ) ROW ; 使い所 この手法を利用することで並列に同じテーブルへの追記処理が実施されても問題なく更新できます。また、上記のように1つのクエリで完結します。ただし他の手法に比べてMERGE処理のタイミングでスキャンが行われるため、処理時間および料金の増加が生じます。追記するデータ量が大きい場合は、MERGE文の条件判定に時間もかかってしまいます。 よって、使い所としては間隔が一定でないような追記処理や、追記が並列で実施されるような処理に向いています。 比較 以上を踏まえ、各処理に関しての比較をまとめました。 手法 メリット デメリット 使い所 Overwirte ・シンプル ・扱うデータ量が大きいと使えない ・並列での更新が不可 データ量が多くない更新処理 Copy Table & Append ・差分更新ができる ・差分更新する方法の中ではシンプル ・一定期間毎の更新で無いと使えない ・並列での更新が不可 ・一定間隔ごとの更新処理 ・データ量が多い場合 Create Table & Merge ・並列での更新が可能 ・複雑 ・データ量が多い場合は考慮すべき点がある ・並列での更新処理を行いたい場合 冪等化の結果 紹介したような仕組みを利用し、バッチ処理すべてを冪等化しました。それにより、データ不整合の危険や重複配信などの潜在的な問題を事前回避できるようになりました。そして、すべての処理に対して自動リトライを入れることで、一時的な問題に対して手動でのオペレーションがなくなりました。以上のように、冪等化によって安定したバッチ処理を実現できました。 まとめ 今回BigQueryの追記処理に関する冪等化の取り組みについて紹介しました。私達のチームでは、安定した仕組み開発がしたい人を募集しています。興味がある方は以下のリンクからご応募ください。 hrmos.co
こんにちは。ZOZO ResearchのResearcherの平川と古澤です。2022年7月25日(月)から7月28日(木)にかけて画像の認識・理解シンポジウムMIRU2022に参加しました。この記事では、MIRU2022でのZOZO Researchのメンバーの取り組みやMIRU2022の様子について報告します。 目次 目次 MIRU2022 企業展示 インタラクティブセッション [OL3B-3]条件付き集合変換を用いたファッションコーディネートの補完 (ロングオーラル) [IS3-27]ファッション推薦問題に向けた階層的集合マッチングモデルの検討 [IS3-55]身体と衣服の採寸情報を考慮する仮想試着のためのレイアウト生成モデルの検討 気になった研究発表 [OL1B-4] Cross-Modal Recipe Embeddingを用いたマスクに基づく食事画像生成 [OL2A-2] 深層モデルの汎化性能改善を目的とした特徴抽出器の事後学習 [IS3-73] 布生地の風合いの画像認識 [OL3A-3] Database-adaptive transfer learning for question answering-based re-ranking in cross-modal retrieval オフライン参加によって得られた気付き 最後に おまけ MIRU2022 MIRUとは、Meeting on Image Recognition and Understandingという画像の認識・理解についてのシンポジウムです。2022年の今回はアクリエひめじ(姫路市文化コンベンションセンター)においてオフラインとオンラインのハイブリッド形式で開催されました。数年ぶりに現地参加も可能ということで1243名の方々が参加されたそうです。ZOZO NEXTは、このMIRU2022にゴールドスポンサーとして協賛させていただきました。 sites.google.com 企業展示 企業展示ブースでは、ZOZO Researchにおける取り組みについてポスターを用いて紹介しました。ZOZOの多角的なファッションサービスとそこから得られる情報資産を活用した研究事例について紹介させていただきました。大変うれしいことに多くの方々に興味を持っていただき、お話をさせていただくことができました。ブースまで足を運んでくださった皆さま、誠にありがとうございました。展示していたポスターはこちらです。 インタラクティブセッション ZOZO Researchからはロングオーラル1件とインタラクティブセッション2件の計3件を発表しました。以下に、各研究のサマリーを示します。 [OL3B-3]条件付き集合変換を用いたファッションコーディネートの補完 (ロングオーラル) 中村 拓磨、斎藤 侑輝 (ZOZO Research) ファッションコーディネート補完問題は、複数の衣服やアクセサリーの組み合わせからなるファッションコーディネートを推薦する技術を実現するための重要な課題として知られています。コーディネート補完問題は、完成したコーディネートに対する評価値計算を前提とする従来手法を用いる場合、補完候補アイテム集合から評価値が最大になるアイテムの組み合わせを探索する問題に帰着します[1][2]。しかしながら、補完候補アイテム集合の要素数が増大するにつれて、探索コストが増大するという課題があります。そこで、本研究ではコーディネート補完問題を指定の条件下における集合検索問題として定式化し、入力アイテム集合と補完候補アイテム集合の属性を反映した特徴量を生成可能なモデルとその学習手法を提案しました。提案手法は指定の条件下で入力アイテムの集合と相補的なアイテム集合を直接的に予測可能であるため、探索空間の増大に伴い推論時の計算量が増大する問題を原理的に解決するアプローチと言えます。実データを用いた性能比較実験では提案手法が入力アイテム集合と相補的なアイテム集合を予測できていること及び出力集合の要素の属性情報を制御可能であることを示しました。 [1] Cucurull, Guillem, Perouz Taslakian, and David Vazquez. "Context-aware visual compatibility prediction." Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2019. [2] Saito, Y., Nakamura, T., Hachiya, H. and Fukumizu, K.: Exchangeable Deep Neural Networks for Set-to-Set Matching and Learning, ECCV2020: Proceedings, Part XVII, p. 626‒646. 2020. [IS3-27]ファッション推薦問題に向けた階層的集合マッチングモデルの検討 長瀬准平(ZOZO Research, 芝浦工大)、斎藤侑輝(ZOZO Research)、石渡哲哉(芝浦工大) ファッションコーディネート間のマッチング問題はファッションに関する様々な推薦タスクへの応用が期待される重要な問題です。コーディネートは複数のファッションアイテムからなる集合と見なせますが[2]、本研究ではコーディネートの集合間のマッチング問題を新たに提起し、階層的な相互作用を考慮した深層学習モデルを提案しました。提案手法は、置換不変な特徴量抽出器であるCrossSimilarity関数[2]を拡張した、コーディネートとアイテムという異なる階層の相互作用を考慮可能な階層的集合マッチングモデルから成ります。実データを用いた比較検証実験では従来手法と比較してマッチング精度が改善することを確認しました。 [2] Saito, Y., Nakamura, T., Hachiya, H. and Fukumizu, K.: Exchangeable Deep Neural Networks for Set-to-Set Matching and Learning, ECCV2020: Proceedings, Part XVII, p. 626‒646. 2020. [IS3-55]身体と衣服の採寸情報を考慮する仮想試着のためのレイアウト生成モデルの検討 後藤 亮介、中村 拓磨 (ZOZO Research) オンラインショッピングにおいては購入以前に衣服のサイズを正確に把握することが困難であるという課題があります。近年では画像ベース仮想試着技術の研究が盛んに行われていますが着用者の体型や衣服のサイズを明示的に考慮した研究がなされていないのが現状です[3][4][5]。本研究では、ZOZOTOWNのデータから着用者の身長と衣服の寸法を含むデータセットを構築し、衣服と身体の寸法を明示的に考慮したレイアウト生成モデルのベースラインモデルを学習しました。更に、身長や着丈の情報を反映できていることを確認するための定量評価指標を提案しました。 [3] Han, Xintong, et al. “Viton: An image-based virtual try-on network.” Proceedings of the IEEE conference on computer vision and pattern recognition. 2018 [4] Choi, Seunghwan, et al. “Viton-hd: High-resolution virtual try-on via misalignment-aware normalization.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2021 [5] Neuberger, Assaf, et al. “Image based virtual try-on network from unpaired data.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2020. 気になった研究発表 私たちが個人的に興味を持った研究について紹介します。 [OL1B-4] Cross-Modal Recipe Embeddingを用いたマスクに基づく食事画像生成 陳 仲涛、本部 勇真、楊 景、柳井 啓司 (電気通信大学) 敵対的生成ネットワークによる画像生成の技術の進展により、リアルな画像の生成が可能となってきています。一方で、料理画像を生成するタスクでは、料理は食材だけでなく作り方や盛り付けによっても見た目や形が大きく変わるという特性上、レシピや盛り付けを反映したリアルな料理画像の生成が難しかったそうです。この研究では、レシピテキストと料理領域から料理を料理領域に盛り付けた画像を生成するMask-based Recipe Embedding GAN (MRE-GAN)を提案されていました。レシピテキストから料理画像を生成するRDE-GANというモデルとセマンティック領域適応正規化を組みわせることで、より安定的に学習ができるようになったとのことです。実験結果として、スープのレシピから枝豆を抜くと生成された料理画像から枝豆が消えるという面白い事例を示されておりました。ファッションの分野でも仮想試着などの文脈で画像生成が注目を集めています。個人的には料理の調理方法というのは洋服の着こなしとも対応しているのかなと感じました。将来的にそういった研究もできると面白そうです。 [OL2A-2] 深層モデルの汎化性能改善を目的とした特徴抽出器の事後学習 山田 陵太、佐藤 育郎、田中 正行、井上 中順、川上 玲 (東京工業大学/デンソーITラボ) 深層モデルにおいて、局所解周りの平坦さがモデルの汎化性能と関係していることが示唆されています。例えばSharpness-Aware Minimizationと呼ばれる解の平坦性まで考慮した最適化手法では、より汎化性能の高い解を得やすいということが知られています。このため局所解近傍における損失形状の平坦化が重要です。この研究では、既に充分に学習された深層モデルを初期状態として、そこからさらに平坦な局所解を探索できる事後学習法を提案されていました。提案手法では、まず深層モデルの前半と後半を抽出器と識別器にわけ、識別器側にミニバッチ損失を最小化するような摂動を加えていました。その後、摂動された識別器の平均を最小化するように、さらに抽出器のパラメータを最適化されていました。このように構築されたモデルを評価すると、4つのうち3つのデータセットにおいて、Sharpness-Aware Minimizationによって学習されたモデルを事後的に性能改善できたそうです。一般の学習済み深層学習モデルの汎化性能を向上させられる最適化手法という点で非常に興味深いと感じました。摂動のハイパーパラメータを訓練データとテストデータのミニバッチ損失最小解までの距離から決定されていましたが、個人的にはこのパラメータがデータセットごとにどの程度変わるかという点にも興味を持ちました。 [IS3-73] 布生地の風合いの画像認識 鈴木大智、相澤清晴(東京大学) ECサイトで衣服を販売する際の課題として衣服に使用されている生地の風合いを伝えることが困難であるという点が挙げられます。衣服の手触りに関する先行研究では手触りとの相関が強い物理特性として、生地の厚さ、柔らかさ、粗さが挙げられているそうです。こちらの研究ではKES(Kawabata Evaluation System)と呼ばれる計測機器を用いて生地の物理特性を測定し、衣服の表面画像から生地の厚さ、柔らかさ、粗さを予測モデルを構築するためのデータセットが提案されています。個人的には衣服の表面画像から計測に長時間を要する衣服の物理特性を予測できるという点が非常に興味深いと思いました。将来的には手触りの情報と仮想試着技術を組み合わせることにより、より実店舗に近いユーザ体験を実現できるかもしれませんね。 [OL3A-3] Database-adaptive transfer learning for question answering-based re-ranking in cross-modal retrieval Rintaro Yanagi, Ren Togo, Takahiro Ogawa, Miki Haseyama (北海道大学) ECサイトにおいてキーワード検索の精度はユーザー体験を左右する重要な要素です。画像のキャプションと検索キーワードのテキストベースのマッチンングによるアプローチでは、検索対象の画像へのキャプション付与が必須であるという課題があります。近年ではキャプション付与のコストを軽減するアプローチとして画像と検索キーワードを同一空間に埋め込む手法が提案されていますが、類似画像や曖昧な検索キーワードに対する頑健性には改善の余地があります。こちらの研究では、システムがユーザーに対して質問を生成することにより、検索キーワードに含まれる情報を対話的に補完するシステムが提案されています。ZOZOTOWNの商品検索においても、システムがユーザーの意図を汲み取って対話的に商品検索する仕組みを導入すれば、より良いユーザー体験を実現できるかもしれません。 オフライン参加によって得られた気付き 初日のチュートリアルでは、Transformer誕生の歴史や複数画像から3次元自由視点画像を生成する技術など、近年のコンピュータービジョン分野のトレンドを俯瞰的に知ることができて大変勉強になりました。MIRU2022に投稿された論文はCLIP、自己教師あり学習、ドメイン適用、NeRFがキーワードになる研究が多かったように感じました。インタラクティブセッションでは著者とのディスカッションを通じて弊社のサービスに活かせそうなアイデアを得ることもできました。MIRU2022では様々な研究者や学生の方々と直接オフラインで議論する機会も沢山あり、個人的にはオンライン参加よりも実りの多い時間を過ごせたと思います。 最後に ZOZO NEXTでは次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できるエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 zozonext.com おまけ 学会の合間に明石焼き風たこ焼きを食べに行きました。 ご満悦。
こんにちは。ECプラットフォーム基盤SREブロックの高塚と巣立( @tmrekk_ )です。 ZOZOTOWN はクラウド化・マイクロサービス化を進める中で、監視SaaSの Datadog を採用しました。この数年で多くの知見が蓄積され、今では様々なシーンでDatadogを活用しています。この記事ではそのノウハウを惜しみなく公開します。 ※本記事は、先日開催された Datadog Japan Meetup 2022 Summer にて発表した内容を書き起こして再構成したものです。 当日の発表資料 speakerdeck.com 目次 当日の発表資料 目次 はじめに マイクロサービス基盤に必要な監視の要件 第1部 ZOZOTOWNにおけるDatadogの活用 1. どこで障害が起こっているのか分からない → APM 2. アラートやダッシュボードや外形監視が欲しい → Monitors, Dashboards, Synthetics 3. AWSのメトリクスがDatadogに届くのが遅い → Metric Streams 4. 障害調査や負荷試験などでメトリクスを記録するのが大変 → Notebook 5. サイトの健全性を数値化したい → SLO 6. CI/CDが遅い原因がわからない → CI Visibility 第2部 全社管理者の取り組み 1. Azure ADによるDatadogユーザーの管理 2. Multiple-Organization Accountsを使った一括請求 3. カスタムロールを使ったUsageの確認 おわりに はじめに (高塚)こんばんは。これからZOZOTOWNにおけるDatadogの活用と、それを支える全社管理者の取り組みについて発表させていただきます。 わたくし、株式会社ZOZOでSREをしている高塚大暉と申します。よろしくお願いいたします。 (巣立)同じくSREの巣立健太郎です。Datadogの全社管理者も務めています。よろしくお願いします。 (高塚)ZOZOTOWNは日本最大級のファッションECサイトです。洋服はもちろん、シューズやコスメも取り扱っておりますので、ぜひ使ってみてください。 そのZOZOTOWNは2004年にサービスを開始しました。 ここで皆様にクイズです。2004年のできごとは4つのうちどれでしょうか? (巣立)答えは...全部です! 皆様ご正解です。おめでとうございます! 話を本題に戻すと、2004年にオープンしたZOZOTOWNは、長らくオンプレでモノリスなアプリケーションとして動いてきました。ただ、サービスが成長にするにつれて、スケーリングや開発効率など、様々な点で厳しくなってきました。 そこで 2020年から本格的にクラウド化・マイクロサービス化によるリプレイスを推し進めており、今もその道半ばです。 ( 目次に戻る ) マイクロサービス基盤に必要な監視の要件 現在のアーキテクチャがこちらです。AWS, GCP, オンプレのハイブリッド構成で、 API Gateway が各マイクロサービスにルーティングを行います。 まだマイクロサービス化できていないAPIは、API Gatewayを経由してオンプレを叩きます。マイクロサービス化ができたPathは、API Gatewayで新しいAPIにルーティングを切り替えます。 また、BFF( Backend for Frontend )で複数のAPIのレスポンスをまとめて返すこともあります。APIが他のAPIを叩くこともあります。 そして、各APIで使用技術は大きく異なります。 ここまでをまとめると、 マイクロサービスアーキテクチャにおいてサイト全体の信頼性を上げるには、各マイクロサービスを個別に監視するのではなく、統合的に監視することが必須です。 そのためには、我々は色々な言語やフレームワークを使っていますので、 幅広い技術に対応していて、ひと通りの機能が揃った監視SaaSが必要というわけです。 そこでDatadogです。Datadogは、その条件にマッチしていました。 本発表は2部に分かれています。 第1部はZOZOTOWNにおけるDatadogの活用についてです。我々がぶつかった監視の課題と、それをDatadogでどう解決したか、そしてDatadogの便利なポイントについて話します。 第2部はそれを支える全社管理者の取り組みです。弊社ではSREだけでなく色々な職種のエンジニアがDatadogを使っていますので、それを支える工夫をいくつか紹介します。 ( 目次に戻る ) 第1部 ZOZOTOWNにおけるDatadogの活用 (高塚)第1部では、監視の課題を6つ紹介します。 1. どこで障害が起こっているのか分からない → APM 1つ目の課題は、どこで障害が起こっているか分からないということです。 例えばZOZOTOWNアプリのホーム画面を開いたとき、図の赤い矢印のような通信が発生します。ホーム画面の表示がいつもより遅いというとき、 いろいろなマイクロサービスが原因として考えられる わけです。 仮に、どうやら商品APIのレイテンシが上昇しているぞと分かっても、実は商品API自体は問題なくて、商品APIが呼び出す推薦APIが遅くなっていたということがあり得るわけですね。 API間の通信が複雑になればなるほど、障害箇所の特定が難しくなります。 そこで活用したいのがAPMです。APMは色々なことができますが、今日は特に分散トレーシングについて話します。 導入方法は簡単で、スライドにGo言語での例を入れていますが、パッケージを入れた上でアプリケーションコードに数行追加するだけです。もちろん色々な言語に対応しています。 我々は全マイクロサービスにAPMを入れました。さらに、全てのリクエストの入口であるAPI Gatewayで、クライアントとリクエストPathをトレースのタグに追加しました。独自のタグを追加するのも1行のコードで可能です。 そうした結果、ご覧のようなトレース一覧が見られるようになります。この画面ではスライド左側に書いたような条件で絞り込み表示できます。 例えばステータスコードを500番台に絞って、「あれ、このリクエストPathだけエラーが出ているな」とか「iOSだけエラーが起きているな」とか「AWSのAZ-1cだけ障害が起きているな」というような使い方をしています。 これがとても便利なので、我々のチームは全てのマイクロサービスで、かつ本番環境だけでなく事前環境や開発環境も100%トレースを送信しています。 一覧画面の次は、1つ1つのトレースを見ていきましょう。これがアプリのホーム画面を開いたときの実際のトレースです。 上に見える青色のラインがBFFのスパンです。BFFは色々なAPIを叩いて結果を集約し、ホーム画面の描画に必要なJSONを返すという処理をします。 真ん中の緑色のラインが、叩かれている各APIのスパンです。各緑色のスパンの下に紫色のスパンもあるのですが、これがデータベースへの接続のスパンです。 今こうやってぱっと見ても、 ちょうど真ん中らへんのスパンだけ長い、つまり時間がかかっていることが分かりますよね。 「ホーム画面の表示が遅い」という問題に対して、かなりスピーディーに原因を特定することができました。 今度は別のトレースで、細かいスパンの中まで見てみます。 アプリケーションが実行したMySQLのクエリ1つ1つが、トレースの1番下の行、赤色のスパンで表示されています。画面の下半分には、ロック待ちでタイムアウトしたというエラーが表示されています。非常に細かいレベルまで見られるので便利です。 ( 目次に戻る ) 2. アラートやダッシュボードや外形監視が欲しい → Monitors, Dashboards, Synthetics (巣立)課題2では、Datadogの基本機能であるMonitorやDashboard、Syntheticsについて、便利なポイントをお話します。 まず、Monitorです。Monitorはタグをつけることが可能で、マイクロサービスや環境ごとに管理することができます。 スライドの画像では、serviceタグにzozo-search-apiを、envタグにdevを指定してMonitor一覧を表示しています。このようにタグを使ってフィルターできるので、Monitorの一覧表を別に用意する必要がありません。 また、Monitorで使用するクエリやメッセージにもタグを使用することができます。例えばAWS ALBのMonitorを作成する時には、ALB名ではなくタグを使用してMonitorの設定が可能です。 マルチアラート変数を使えば、タグの値ごとに通知することもできます。スライドの画像ではマルチアラート変数にKubernetesのNamespaceを指定しています。 Namespaceの数だけmonitorを作らずとも、全てのNamespaceに対して一括でMonitorを作成できます。 続いてDashboardです。弊チームではマイクロサービスごとにDashboardを作成しています。リリースや負荷試験などの日々の監視や障害対応にとても活躍しています。 今度はSyntheticsについてです。DatadogのSyntheticsは簡単に設定でき、他のサービスに比べても高機能な印象です。 そして、これが一番お気に入りなのですが、 Syntheticsの画面からAPMトレースを見ることができるので、Syntheticsがエラーになった原因をすぐに調べることができます。 ( 目次に戻る ) 3. AWSのメトリクスがDatadogに届くのが遅い → Metric Streams (高塚)課題3は、AWSのメトリクスがDatadogに届くのが遅い件です。 AWSのメトリクスがDatadogに届くまで10分程度遅れることがあります。右側にALBのグラフがありますが、各グラフの右端だけ線が欠けています。 何が問題かと言うと、 Datadog Monitorが10分遅れると、障害に気づくのも10分遅れてしまう という点です。 そこで、Metric Streamsです。これはAWSの場合に限った話になりますが、CloudWatch Metric StreamsとKinesis Data Firehoseを使ってDatadogにメトリクスをストリーミングすることで、先ほどの 遅延を2〜3分程度に抑えることができます。 これを活用することで、我々はMonitorやDashboardの遅延をほぼ考慮することなく利用できています。 ( 目次に戻る ) 4. 障害調査や負荷試験などでメトリクスを記録するのが大変 → Notebook 4つ目の課題は、障害調査や負荷試験などでメトリクスを記録するのが大変、という点です。 リクエスト数や負荷状況などのメトリクスを記録したいというときに、 我々はこれまでDatadogのDashboardをスクショして、社内Wikiにペーストしていました。 時間のかかる面倒くさい作業でした。 それを改善できるのが、Notebookです。NotebookはDatadog上にドキュメントを作成、共有できる機能です。 これが実際に作ったNotebookです。画面はDashboardに似ていて、例えば右上にタイムフレームがあります。 スクショとは違って、この時刻を変更すればグラフが動的に変わります。 デフォルトを障害発生時刻にする、みたいなこともできます。 コメント機能や同時編集機能もあります。 テンプレート変数を使って、左上のプルダウンメニューから環境やマイクロサービスを切り替えて表示できるようにしたり、Notebookをテンプレートとして保存したりすることもできます。これまでのスクショの苦労がなくなりました。 ( 目次に戻る ) 5. サイトの健全性を数値化したい → SLO 課題5は、サイトの健全性を数値化したいということです。これまで色々な監視の話をしましたが、結局サイト全体の健全性はどうなんだ、数値化できないのか、という話です。 そんなときにSLOが使えます。SLOとは、スライドに記載したようなサービスレベル目標のことですが、Datadogを使うと簡単にSLOを算出して可視化できます。 これが試しにやってみたサンプルです。 障害を防ぐという仕事は「やって当たり前」と思われることも多く、エンジニアでさえ成果が見えにくい部分です。それをSLOで可視化したり、目標を設定したりすることで、モチベーションが湧きやすくなります。 ( 目次に戻る ) 6. CI/CDが遅い原因がわからない → CI Visibility 6つ目の課題は、CI/CDが遅い原因が分からないということです。 我々はCI/CDにGitHub Actionsを使っています。リポジトリが大きくなるにつれて、WorkflowやJobにかかる時間が伸びたり、Workflowの数自体が増えたりして、CI/CDの待ち時間、デプロイ待ちのような時間が生まれてしまいました。 ただ、 WorkflowやJobが多く、どれを改善すれば良いのか分からないという問題がありました。 そこでCI Visibilityです。CI Visibilityは多くのCIサービスに対応しています。 導入も非常に簡単で、例えばGitHub Actionsの場合は、DatadogのIntegrationsの画面からGitHub Appをインストールだけで完了します。リポジトリを指定することも可能です。 導入すると表示されるようになるのがこの画面で、これはCIの実行結果一覧です。APMと同じように、リポジトリ名やブランチ名でフィルター表示できます。 そして、1つの実行結果をクリックすると、なんとトレースが見られます。例えばこのトレースでは、最初に同じJobが複数環境に対して並列に実行されて、そのあといくつか時間のかかるJobが実行されています。 ただ、1回の実行結果を見るよりも、複数回の実行結果を横断的に分析したほうが良いと思いまして... CIのDashboardを作ってみました。色々ありますが、例えば下半分の表は、もっとも時間のかかったWorkflowのランキングです。画面外にはJob単位の表もあります。 これを見ることで、 どのWorkflowやJobに時間がかかっているのか、どれくらいの頻度で実行されているのかを分析できるようになりました。 もう1つ我々がやっている工夫として、実行時間×単価でCIのおよその料金を表示するようにしました。このJobにいくらお金がかかっている、ということを可視化しています。 先ほどと同じ話になりますが、 可視化することで改善のモチベーションが湧くことを実感しています。 以上が第1部です。ご覧のサービスの活用法をご紹介しました。1つでも皆さまにとってお役に立つ情報が含まれていたら幸いです。 ( 目次に戻る ) 第2部 全社管理者の取り組み (巣立)第2部では、ZOZOにおける全社管理者の役割を最初に説明します。その後、具体的な取り組みを3つ紹介します。 弊社では、Datadogやその他のクラウドサービスをたくさんのチームが利用しています。 今回のテーマである全社管理者とは、それらのクラウドサービスのアカウント管理やセキュリティインシデントの監視など、サービス利用者が開発に集中できるようにするための様々な取り組みを行うメンバーです。複数チームのSREから構成されており、Datadogの管理者は現在3名います。 ( 目次に戻る ) 1. Azure ADによるDatadogユーザーの管理 ここから、その全社管理者の取り組みについて紹介します。1つ目はAzure ADによるDatadogユーザの管理についてです。 通常であれば、クラウドサービスごとにアカウントの管理が必要になります。また、Datadogに関して言えば、複数のOrganizationに所属しているメンバーは、Organizationを切り替えるたびにIDやパスワードの入力が必要になります。 そこでAzure ADです。Azure ADでは、様々なクラウドサービスのアカウント管理を一元化し、SSO (Single Sign-on) を実現することができます。Azure ADはDatadogにも対応しています。 Azure ADを使ってDatadogのアカウントを管理するようにし、SSOを実現しました。 これによりOrganizationの切り替えの際にID・パスワードの入力が不要になり、利用者も管理者もアカウント管理の負担が減りました。 ( 目次に戻る ) 2. Multiple-Organization Accountsを使った一括請求 続いて、Multiple-Organizationを使ったDatadogの一括請求についてです。 弊社では、Datadogで複数のOrganizationを利用しています。そのため、Organization毎に請求が発生し、Organizationの数だけ請求書を処理する必要がありました。 そこでMulti-Organizationです。 下の画像のようにOrganizationごとの利用量、例えばInfra hostsがこのくらい、などを親Organizationから確認することが可能になります。そして、全てのOrganizationの請求を親Organizationに一元化できます。 この機能はDatadogサポートへ連絡することで使えるようになります。 Multi-Organizationを利用したことによって、 Organizationごとに請求書を処理する必要がなくなりました。 また、親Organizationから子Organizationの利用状況の確認が可能となりました。 そして、無料枠を全てのOrganizationで合算して計算するようになったため、無料枠の利用が最適化されました。 ( 目次に戻る ) 3. カスタムロールを使ったUsageの確認 3つ目に、カスタムロールを使ったUsageの確認です。 DatadogのUsage、先ほど見せたInfra hostsがこのくらいなどを確認できる画面ですね。このUsageは、Admin Roleを持ったユーザしか表示することができません。 しかし、Admin Roleを持ったユーザ、つまりはDatadogの管理者は、全てのOrganizationでどのくらい利用しているのかを把握することが難しいので、このOrganizationでのこの機能の利用料は適切かどうかを判断することができません。 そこでカスタムロールを使用することにしました。 カスタムロールを説明する前に簡単にDatadogのデフォルトのロールについて説明します。1つずつの説明は省きますが、 デフォルトのロールで請求情報、UsageにアクセスできるのはAdmin Roleだけです。 では、本題のカスタムロールについてです。 カスタムロールでは任意のアクセス許可を定義することができます。そのため、デフォルトのロールよりも適切なアクセス許可をユーザに付与することができます。また、ユーザが複数のロールを持つ場合、例えばデフォルトのロールとカスタムロールの2つを持つ場合は、両方のロールに付与されている全てのアクセス許可が付与されます。 このカスタムロールに請求情報へのアクセス権を付与し、全てのユーザにこのロールを渡すことで、 誰でも自身の所属するOrganizationのUsageの確認が可能となりました。 また、管理者は各Organizationの利用料が適切かどうかをOrganizationの利用者に任せることが可能となりました。 以上が、Datadog全社管理者の取り組みについての紹介でした。 まとめです。本発表では、ZOZOTOWNにおけるDatadogの活用と、それを支える全社管理者の取り組みについてご紹介しました。 ( 目次に戻る ) おわりに 冒頭でご紹介した通り、本内容は Datadog Japan Meetup 2022 Summer で発表させていただきました。 ご来場いただいた皆様、ここまでお読みいただいた皆様、誠にありがとうございました。 私たちのチームでは、Datadog・AWS・Kubernetes・Istioなどをフル活用し、ZOZOTOWNのマイクロサービス基盤を一緒に作る仲間を募集しています。ぜひ下記リンクからご応募ください! hrmos.co
はじめに ZOZOTOWN開発本部ZOZOTOWNアプリ部Android2ブロックの鈴木( @s1u2z1u3ki )です。 本投稿ではZOZOTOWN Androidアプリを、Material Designに準拠したUI/UX 1 とするために取り組んでいる内容を紹介します。 目次 はじめに 目次 Material Designとは? Material Design勉強会について 勉強会の流れ 存在した課題 課題解決へのアプローチ 提案会の実施 提案会の流れ 1. 提案会の準備 2. セクションの復習 3. 提案内容の議論 実装会の実施 「結局やらない」をなくすため モブプロ形式でリリースまでのスピードを上げるため 実装会の流れ 1. タスクの共有 2. タスクを進める 3. 進捗記入 4. 進捗共有 取り組みの結果 リリースした提案 1. ログイン画面のテキストフィールドのフォーカスを強調する 2. 検索結果のFABと商品件数にFadeアニメーションを追加する 3. 商品の詳細画面から戻る際のアニメーションのDurationを調整する メンバーの感想 良かった点 改善点 取り組みは課題解決に繋がっているか? まとめ 最後に Material Designとは? Material Design は2014年にGoogleが提唱したデザインシステムです。Android、Flutter、Web向けの高品質な体験を構築する手助けをしてくれます。 Androidアプリは、Androidプラットフォームや他アプリと一貫性のある表示・操作を提供するため、Material Designに準拠することが推奨されています。 2 例えばリストのアイテムを横スワイプで削除する機能は、AndroidプラットフォームやGoogle製のアプリで共通の操作であり、ガイドラインにも記載があります。ガイドラインに準拠しないと、AndroidプラットフォームやGoogle製のアプリと操作方法が異なり、アプリ操作中にユーザがストレスを抱える可能性が高くなります。 さらに Material Design Components と呼ばれるライブラリが提供されており、ガイドラインを実装したUIコンポーネントが提供されています。 また、2021年にはMaterial Design 3の発表があり、パーソナライズの強化が行われたMaterial Youの登場などアップデートが続いています。 Material Design勉強会について ZOZOTOWN AndroidアプリにはMaterial Designに準拠していない箇所が存在し、さらにデザイナーとエンジニア両者がMaterial Designの知識に乏しい状況でした。 上記を理由に、ZOZOTOWNに関わるAndroidエンジニアとデザイナーを中心に、下記2つを目的として「Material Design勉強会」を実施してきました。 3 Material Designについて理解を深める Material Designに準拠したZOZOTOWN Androidアプリを作る 勉強会の流れ 勉強会は、持ち回りで決めた担当者がセクション(e.g. Motion , Interaction )の内容を和訳・要約した資料を準備します。 資料をもとに、担当者が講義形式で内容を紹介します。ZOZOTOWNに関わるAndroidエンジニアとデザイナーが参加し、質問や感想を出し合いながら理解を深めていきます。 勉強会資料の例 この勉強会のおかげでデザイナーとエンジニアがMaterial Designに関する知識をもとに、UI/UXに対する提案・議論が可能になるなど、勉強会の内容をプロダクトに反映できつつありました。 UI/UXに関する提案例 存在した課題 前述の通り、勉強会のおかげで、新規の画面や改修が入る画面については、Material Designに準拠したUI/UXとなりつつありました。 しかし、 既存の画面についてはMaterial Designへの準拠が進んでおらず、さらなるUI/UXの向上が見込めました。 課題解決へのアプローチ 前述の課題を解決するために、勉強会の他に「提案会」と「実装会」を新たに実施することにしました。 課題解決のアプローチ それぞれ取り組みの詳細を説明していきます。 提案会の実施 デザイナーとエンジニアで、どの箇所が改善可能なのか認識を合わせなければ改善が進まないと判断し、月1回で1時間程度の提案会を実施することにしました。 Material Designに準拠できるように、改善できる箇所の認識合わせを目的としました。 提案会の流れ 提案会は下記のように進行します。 提案会の流れ 1. 提案会の準備 提案会までに、担当者が担当するセクション(e.g. Motion , Interaction )について、プロダクトのどの箇所に改善の余地があるかを考えます。 デザイナーとエンジニア両者の視点を提案に含めるため、担当者を2名にし、なるべくデザイナーとエンジニアがペアとなるようにしました。 下記の項目を埋めて提案資料を作成します。 項目 記述内容 内容 提案内容のサマリーを記載する。 スクリーンショット 提案内容を実装した際のスクリーンショットや動画などを記載する。 ドキュメント該当箇所 Material Designのドキュメント該当箇所のリンクを記載する。 提案書の例 2. セクションの復習 実際の提案会では、担当者が、会で取り扱うセクションの概要を説明します。 これは、勉強会でセクションを学んでから時間が経過していたり、最近入社した方は勉強会で学んでいない可能性が高いために行っています。 基本的には、 勉強会のときに用いた資料 の要点を説明していきます。 これにより、どのような背景の提案なのかが掴みやすくなり、のちの提案内容の理解が容易になります。 3. 提案内容の議論 担当者が提案内容を説明し、実装着手OKな提案なのかを決めるために議論をします。 下記のような観点を総合的に判断し、結論を出します。 デザイン意図に反していないか 既存デザインとの調和を乱さないか 工数がかかり過ぎないか 実装会の実施 月1回で1時間程度エンジニアの時間を確保し、提案会で実装着手OKとなったものを実装する会を開催することにしました。 この会を始めた理由は下記の2点です。 「結局やらない」をなくすため モブプロ形式でリリースまでのスピードを上げるため 「結局やらない」をなくすため ZOZOTOWN Androidブロックでは案件が複数並行して動いています。 そのため、着手OKとなった提案の実装が忘れられてしまう可能性がありました。 「実装会」という名目で時間をあらかじめ確保することで、やらずに放置され続けるというのを防止する狙いがありました。 モブプロ形式でリリースまでのスピードを上げるため モブプロ形式で実装を進めることで、コードを書きながら詳細な実装方針を決めることができ、レビューがほとんど必要ないというメリットがあります。 そのため、スピード感を持ってリリースできると予想できました。 実装会の流れ ZOZOTOWNのAndroidエンジニアは10名以上いるため、実装会内だけのチームを作り実装を進めます。 実装会の流れ 1. タスクの共有 タスクは下図の通りKanbanボードで管理しており、提案会で着手OKとなったものをバックログとして積んでいます。 実装会で使用しているKanbanボード バックログの中から、着手するタスクを決めて共有したり、タスクの進捗を共有したりします。 2. タスクを進める 各チームごとモブプロの役割を決めて、タスクを進めます。 ドライバー(タイピスト) 人数: 1人 やること: モブの意見をもとに実装をする ナビゲーター(モブ) 人数: タイピスト以外 やること: ドライバーに実装して欲しいことを伝える 3. 進捗記入 実装会は基本的には月1回で行っているため、前回の内容を思い出せるように、進捗を記載します。 進捗記載の例 4. 進捗共有 最後に各チームが集まり進捗を共有します。 記入した内容を共有し、成果物がある場合はそれも共有します。 取り組みの結果 提案会を7回、実装会を6回実施して下記のような結果となりました。 4 提案: 46件 着手OKとなった提案: 15件 リリース実績: 3件 リリースした提案 提案会・実装会を経てリリースした提案を紹介します。 1. ログイン画面のテキストフィールドのフォーカスを強調する ログイン画面において、テキストフィールドのフォーカスを強調する対応をしました。 ログイン画面 テキストフィールドのフォーカス強調 (左: Before, 右: After) 提案の背景は、ドキュメントの States に、下記の記述があったためです。 States communicate the status of UI elements to the user. Each state should be visually similar and not drastically alter a component, but must have clear affordances that distinguish it from other states and the surrounding layout. つまり、UIの状態はユーザに伝えるべきものであり、ユーザの行動を促すヒントを含んでいる必要がありました。 対応前は、フォーカスの状態をカーソルのみでユーザに伝えていました。 テキストフィールドの枠を色付けることで、ユーザにフォーカスの状態が伝わりやすくなったかと思います。 2. 検索結果のFABと商品件数にFadeアニメーションを追加する 商品の検索結果が表示される画面の、FAB(Floating Action Button)と商品件数にアニメーションをつける対応をしました。 FABは画面右側からスライドしてくる仕様のため、少しわかりにくいですが、対応後はフェードインしつつスケールを大きくするアニメーションとしています。 ガイドラインの例 に沿って MaterialFade を使用しています。 検索結果画面 FAB (左: Before, 右: After) 商品件数の表示・非表示は、デザイナーの要望もあり、 Animator を使用しフェードのみのアニメーションにしています。 検索結果画面 商品件数 (左: Before, 右: After) 提案の背景は、ドキュメントの Motion に、下記の記述があったためです。 Motion celebrates moments in user journeys, adds character to common interactions, and can express a brand’s style. つまり、Motionを使用することでページへのアクセスを歓迎し、インタラクションに個性を与えることが可能になります。 対応前はアニメーションがありませんでしたが、フェードアニメーションを加えることで、よりスタイリッシュな見栄えになったかと思います。 3. 商品の詳細画面から戻る際のアニメーションのDurationを調整する 変化は小さいですが、商品の詳細画面から戻る際のアニメーションのDurationを短縮しました。 商品詳細から戻る際のDuration (左: Before, 右: After) 提案の背景は、ドキュメントの Motion に、下記の記述があったためです。 Transitions that close, dismiss, or collapse an element use shorter durations. Exit transitions may be faster because they require less attention than the user’s next task. つまり閉じる遷移は、開く遷移と比較してユーザの注意を集める必要がなく、高速にする必要があります。 対応前は、商品の詳細画面を開くとき・閉じるときもDurationは300msとしていました。 ドキュメントに例があるように、閉じる際は250msに変更しました。 メンバーの感想 提案会と実装会に参加しているメンバーから以下のような感想が集まりました。 良かった点 あとは実装会というのを設けることで検討したけど今案件やってるから..となって一生やらないということが無くなるのもメリットかなと思います デザイナーの手が足りず、どうしてもiOSのデザインを調整しただけになりがちだったAndroidデザインが、少しずつでもAndroidらしい姿に変わっていけているところが良い点です。 単にUI/UX改善について議論するということではなく、デザインガイドラインについてみんなで一緒にインプットしそれをベースとすることで「ガイドラインではこうだから」みたいな共通の基準がある状態で議論出来るので、そこがいいなと感じています。 理想は理想のままで終わりがちな物事が多い中、勉強会をやって終わりではなく、提案、議論、そしてプロダクトへの導入まで行うことができたことは非常に良い実践例になったと思います。 改善点 会自体ではないのですが、OS差分が増えていく事になるので実装箇所が増えていった時にちゃんと把握しておきたいなーと思いました(他チームから挙動が違うと言われた時にすぐ答えられるように) 1時間で実装するのは時間的に苦しそうだなと感じました。実装箇所をピンポイントに絞ったり、後でプロダクトに導入するためのコンポーネントを作成するだけに留めるなど工夫できることがあるかもしれません。 できればもう少しダイナミックに変化が見えるような大きい改修点を見つけられると良いですね。 他の勉強会にも言えることですが、開催済みの勉強会内容を新しく入社されてくる方々にキャッチアップしていただく方法は、これからも模索していかないとなと感じています。 取り組みは課題解決に繋がっているか? 提案会と実装会の取り組みは「既存の画面をMaterial Designに準拠したUI/UXへ改善する」ことに繋がっているのでしょうか? 改善点はあるが「繋がっている」と考えています。 取り組みの結果 にも記述した通り、デザイナーと認識を合わせ、着手OKとなった提案が15件あり、そのうち3件はリリースできているからです。 さらにメンバーの感想から業務でUI/UXについて再考するきっかけとなっていたり、案件をやりつつも実装を進められたりと、取り組み全体としてポジティブな意見をいただけたからです。 メンバーからの感想を踏まえて、今後は下記の内容を改善しつつ取り組みを続けていければと考えています。 実装会からリリースまでのスピードを上げる 大きくインパクトのある提案とリリース まとめ 既存画面をMaterial Designに準拠したUI/UXへ改善するための具体的なプロセスを紹介しました。 今後は、Material Design 3の内容についても同様にプロダクトに取り入れていければと思います。 最後に ZOZOではAndroidエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください。 hrmos.co プロダクトのデザイン思想としてガイドラインへの準拠が難しい箇所については、ガイドラインの本質を理解した上で、プロダクトとしてのデザインを尊重するケースもあります。 ↩ Why We Recommend Material Components for Android において詳細が説明されています。 ↩ 勉強会は2019年4月からスタートし、現在も実施しています。 ↩ 2022年7月11日現在 ↩
こんにちは。WEAR部Androidチームの御立田です。先日、WEARチームでコーディネート動画を投稿できる機能を追加しました。 その際、WEARが提供する音楽リストから、ユーザーが好きな音楽を選択する機能を実装する必要がありました。今回は、動画ファイルの音楽データの変更をAndroidの端末上で行ったのでそこで得られた知見を共有したいと思います。 動画ファイル、音楽ファイルのフォーマットや、エンコード、デコードの設定は多岐にわたります。本投稿では、シンプルなパターンでやや抽象的に説明し、この投稿を読んだ人が「Androidにおいて動画の変換する時に何を調べれば良いのかがわかるようになる」を目的としています。 変換の仕様 任意の動画ファイルの音楽データを、任意の音楽ファイルのデータに差し替える 動画は指定のフォーマットで再エンコードする 音楽は元の音楽データをそのまま利用する ただし、動画の長さの方が短い場合、音楽も動画の長さまでとする Android端末上で動画を変換する流れ MediaMetadataRetrieverで動画ファイルの長さを取得する 出力用の動画データを作成する MediaExtractorで動画ファイルのデータ(エンコードされたままの状態)を読み込む MediaCodec(decoder)でエンコードされているデータをデコードする MediaCodec(encoder)でデコードされているデータを、指定のフォーマットにエンコードする 出力用の音楽データを作成する MediaExtractorで音楽ファイルのデータ(エンコードされたままの状態)を読み込む MediaMuxerを利用して、動画と音楽(2と3の出力)を混ぜ合わせて新しい動画ファイルを出力する ※エンコード、デコード処理に、 Surface を利用することでより高速な変換が可能です。 流れとしては上記の通り、非常にシンプルです(実際の実装では2〜4はメモリの効率的な利用のために少しずつ読み込んで何度も繰り返しますが、わかりやすさ優先で各処理ごとに分けて説明しています)。 しかし、それでもMediaMuxerへのデータの渡し方にクセがあったり、エンコードやデコードの処理にクセがあったりしてなかなか一筋縄ではいきませんでした。 以降、各項目について疑似コードで説明していきますが、クセのある部分については多少詳しく説明していきたいと思います。 1. MediaMetadataRetrieverで動画ファイルの長さを取得する MediaMetadataRetrieverを利用して動画ファイルの長さを読み込みます。特に難しいことはありません。音楽ファイルを動画の長さと合わせるために、後で設定します。 val retriever = MediaMetadataRetriever().apply { setDataSource(inputVideoFileDescriptor) } val key = MediaMetadataRetriever.METADATA_KEY_DURATION val videoDurationMs = retriever.extractMetadata(key)?.toLong() 2. 出力用の動画データを作成する MediaExtractorで動画ファイルのデータ(エンコードされたままの状態)を読み込む MediaExtractorを利用して、動画ファイルのデータを読み込みます。大まかな流れは下記の通りです。 1フレームの動画ファイルのデータを受け取れるByteBufferと、読み込んだByteBufferの状態を表すByteBufferInfoを準備する ByteBufferに1フレーム読み込む 読み込んだフレームの状態に応じてByteBufferInfoを設定する 最終フレームまで2〜3を繰り返す 取得したByteBufferやByteBufferInfoは次のデコーダーで利用します。 まずは動画ファイルのデータをExtractorで読み込めるようにExtractorの設定をします。 // 動画ファイルのtrackをextractorに設定する // (動画のvideoFormatやmimeも後で利用するので取得しておく) var trackIndex = 0 val videoTrackIndex: Int val videoFormat: MediaFormat val mime: String while ( true ) { val currentFormat = videoExtractor.getTrackFormat(trackIndex) val currentMime = currentFormat.getString(MediaFormat.KEY_MIME) ?: continue if (currentMime.startsWith( "video/" )) { videoTrackIndex = trackIndex videoFormat = currentFormat mime = currentMime break } trackIndex ++ if (trackIndex >= videoExtractor.trackCount){ error( "video track がありません" ) } } videoExtractor.selectTrack(videoTrackIndex) // 動画ファイルの1フレームのデータを読み込む val sampleDataOutputByteBuffer = ByteBuffer.allocate(SAMPLE_DATA_BUFFER_CAPACITY) val sampleDataOutputByteBufferInfo: BufferInfo val readSampleDataSize = videoExtractor.readSampleData(sampleDataOutputByteBuffer, 0 ) val isEmptySampleData = readSampleDataSize < 0 if (isEmptySampleData) { // 最後まで読み込んだ場合は、BufferInfoを終了状態を表す内容に設定する。 sampleDataOutputByteBufferInfo = BufferInfo().apply { offset = 0 size = 0 presentationTimeUs = 0 flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM } } else { // 読み込んだsampleDataの状態を、BufferInfoに設定する // offset: 今回は利用しないので0 // size: sampleDataのサイズ // presentationTimeUs: extractorから現在のsampleDataの終了時間を取得して設定 // frags: keyFrameかどうかのフラグを設定 val isKeyFrame = videoExtractor.sampleFlags and MediaExtractor.SAMPLE_FLAG_SYNC != 0 val bufferInfoFlags = if (isKeyFrame) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0 sampleDataOutputByteBufferInfo = BufferInfo().apply { offset = 0 size = readSampleDataSize presentationTimeUs = videoExtractor.sampleTime flags = bufferInfoFlags } } // videoExtractorの読み込み位置を次に進めておく videoExtractor.advance() MediaCodec(decoder)でエンコードされているデータをデコードする MediaCodecを利用して、元動画ファイルのデータをデコードします。大まかな流れは下記の通りです。 元動画の形式をデコードできるデコーダーを作成 MediaExtractorで取得した1フレームの情報(ByteBufferとByteBufferInfo)をデコーダーの入力用のキューに入れる デコーダーの出力用キューからデコードされたデータを取得する 2〜3を繰り返す // MediaExtractorで取得した1フレームの情報(ByteBufferとByteBufferInfo)をデコーダーの入力用のキューに入れる val decoder = MediaCodec.createDecoderByType(mime).apply { configure(videoFormat, null , null , 0 ) start() } // decoderの入力用のバッファの準備ができるまで待機。inputQueueのデータが処理されるのを待つ val decoderInputBufferIndex: Int while ( true ) { val inputBufferIndex = decoder.dequeueInputBuffer( 0 ) if (inputBufferIndex >= 0 ) { decoderInputBufferIndex = inputBufferIndex break } delay( 500 .milliseconds) } // 入力用のバッファへデータを書き込んだ後にenqueueを行い、書き込んだデータのdecode処理が行われるようにする val inputBuffer = decoder.getInputBuffer(decoderInputBufferIndex) ?: error { "デコーダー用の input buffer が取得できません" } inputBuffer.clear() inputBuffer.put(sampleDataOutputByteBuffer) decoder.queueInputBuffer( decoderInputBufferIndex, 0 , sampleDataOutputByteBufferInfo.size, sampleDataOutputByteBufferInfo.presentationTimeUs, sampleDataOutputByteBufferInfo.flags, ) // デコーダーの出力用キューからデコードされたデータを取得する val decoderOutputBufferInfo = BufferInfo() val decoderOutputBufferIndex: Int // dequeue の準備ができるまでまつ while ( true ) { val outputBufferIndex = decoder.dequeueOutputBuffer(decoderOutputBufferInfo, 0 ) if (outputBufferIndex > 0 ) { decoderOutputBufferIndex = outputBufferIndex break } delay( 500 .milliseconds) } val decoderOutputBuffer = decoder.getOutputBuffer(decoderOutputBufferIndex) ?: error { "入力ビデオがデコードされたデータが入った ByteBuffer が取得できません" } // output buffer をすぐに解放したいため、bufferをコピーしておく val decodedSrcVideoByteBuffer = ByteBuffer.allocate(decoderOutputBuffer.capacity()).apply { put(decoderOutputBuffer) flip() } decoder.releaseOutputBuffer(decoderOutputBufferIndex, false ) MediaCodec(encoder)でデコードされているデータを、指定のフォーマットにエンコードする MediaCodecを利用して、指定のフォーマットにエンコードします。大まかな流れは下記の通りです。 指定の形式にエンコードできるエンコーダーを作成 decoderの出力をエンコーダーの入力用のキューに入れる エンコーダーの出力用キューからエンコードされたデータを取得する 2〜3を繰り返す // decoderの出力をエンコーダーの入力用のキューに入れる // 出力するビデオのフォーマットを設定し、encoderを作成する val outputFormat = MediaFormat.createVideoFormat( "video/avc" , 1920 , 1080 ).apply { setInteger(MediaFormat.KEY_BIT_RATE, 2 * 1000 * 1000 ) setInteger(MediaFormat.KEY_FRAME_RATE, 30 ) setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3 ) setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) } val encoder = MediaCodec.createEncoderByType(mime).apply { configure(outputFormat, null , null , MediaCodec.CONFIGURE_FLAG_ENCODE) start() } // encoderの入力用のバッファの準備ができるまで待機。inputQueueのデータが処理されるのを待つ val encoderInputBufferIndex: Int while ( true ) { val inputBufferIndex = encoder.dequeueInputBuffer( 0 ) if (inputBufferIndex >= 0 ) { encoderInputBufferIndex = inputBufferIndex break } delay( 500 .milliseconds) } // 入力用のバッファへデータを書き込んだ後にenqueueを行い、書き込んだデータのencode処理が行われるようにする val inputBuffer = encoder.getInputBuffer(encoderInputBufferIndex) inputBuffer.clear() inputBuffer.put(srcVideoDecodedByteBuffer) encoder.queueInputBuffer( encoderInputBufferIndex, 0 , srcVideoDecodedByteBufferByteBufferInfo.size, srcVideoDecodedByteBufferByteBufferInfo.presentationTimeUs, srcVideoDecodedByteBufferByteBufferInfo.flags, ) // エンコーダーの出力用キューからエンコードされたデータを取得する val encoderOutputBufferInfo = BufferInfo() val encoderOutputBufferIndex: Int // dequeue の準備ができるまでまつ while ( true ) { val outputBufferIndex = encoder.dequeueOutputBuffer(encoderOutputBufferInfo, 0 ) if (outputBufferIndex > 0 ) { encoderOutputBufferIndex = outputBufferIndex break } delay( 500 .milliseconds) } val encoderOutputBuffer = encoder.getOutputBuffer(encoderOutputBufferIndex) ?: error { "出力ビデオ用の、エンコードされたデータが入った ByteBuffer が取得できません" } // outputBufferをすぐに解放したいため、bufferをコピーしておく val encodedDstVideoByteBuffer = ByteBuffer.allocate(encoderOutputBuffer.capacity()).apply { put(encoderOutputBuffer) flip() } encoder.releaseOutputBuffer(encoderOutputBufferIndex, false ) 3. 出力用の音楽データを作成する MediaExtractorで音楽ファイルのデータ(エンコードされたままの状態)を読み込む // 音楽ファイルのtrackをextractorに設定する // (音楽のaudioFormatやmimeも後で利用するので取得しておく) var trackIndex = 0 val audioTrackIndex: Int val audioFormat: MediaFormat val mime: String while ( true ) { val currentFormat = audioExtractor.getTrackFormat(trackIndex) val currentMime = currentFormat.getString(MediaFormat.KEY_MIME) ?: continue if (currentMime.startsWith( "audio/" )) { audioTrackIndex = trackIndex audioFormat = currentFormat mime = currentMime break } trackIndex ++ if (trackIndex >= audioExtractor.trackCount){ error( "audio track がありません" ) } } audioExtractor.selectTrack(audioTrackIndex) // 音楽ファイルのデータを読み込む val sampleDataOutputByteBuffer = ByteBuffer.allocate(SAMPLE_DATA_BUFFER_CAPACITY) val srcEncodedByteBuffer: ByteBuffer val srcEncodedByteBufferInfo: BufferInfo val readSampleDataSize = audioExtractor.readSampleData(sampleDataOutputByteBuffer, 0 ) val isEmptySampleData = readSampleDataSize < 0 val isOverVideoDuration: Boolean = audioExtractor.sampleTime >= videoDurationMs * 1000 if (isEmptySampleData) { // 最後まで読み込んだ場合は、BufferInfoを終了状態を表す内容に設定する srcEncodedByteBuffer = ByteBuffer.allocate( 0 ) srcEncodedByteBufferInfo = BufferInfo().apply { offset = 0 size = 0 presentationTimeUs = 0 flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM } } else { if (isOverVideoDuration) { // 動画の長さを超えた場合は、BufferInfoを終了状態を表す内容に設定する srcEncodedByteBuffer = ByteBuffer.allocate( 0 ) srcEncodedByteBufferInfo = BufferInfo().apply { offset = 0 size = 0 presentationTimeUs = 0 flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM } } else { srcEncodedByteBuffer = ByteBuffer.allocate(sampleDataOutputByteBuffer.capacity()).apply { put(sampleDataOutputByteBuffer) flip() } val isKeyFrame = audioExtractor.sampleFlags and MediaExtractor.SAMPLE_FLAG_SYNC != 0 val bufferInfoFlags = if (isKeyFrame) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0 // 読み込んだsampleDataの状態を、BufferInfoに設定する // offset: 今回は利用しないので 0 // size: sampleDataのサイズ // presentationTimeUs: extractor から現在のsampleDataの終了時間を取得して設定 // frags: key frame かどうかのフラグを設定 srcEncodedByteBufferInfo = BufferInfo().apply { offset = 0 size = readSampleDataSize presentationTimeUs = audioExtractor.sampleTime flags = bufferInfoFlags } } } // 次のsampleに進めておく audioExtractor.advance() 4. MediaMuxerを利用して、動画と音楽(2と3の出力)を混ぜ合わせて新しい動画ファイルを出力する MediaMuxerを作成する エンコーダから取得できるMediaFormatを設定する 音楽ファイルのMediaFormatを設定する MediaMuxerをスタートする MediaMuxerに出力用のデータを書き込む エンコーダから出力された動画データを書き込む MediaExtractorで取得した音楽データを書き込む 2を繰り返す // MediaMuxerを作成する val muxer = MediaMuxer(outputPath, OutputFormat.MUXER_OUTPUT_MPEG_4) // エンコーダから取得できるMediaFormatを設定する // * encoder.outputFormatは、encoderから最初のoutputが行われる直前のタイミングで取得できるようになる val videoTrackIndex = muxer.addTrack(encoder.outputFormat) // 音楽ファイルのMediaFormatを設定する。そのまま利用するので最初に取得したaudioFormatをそのまま設定する val audioTrackIndex = muxer.addTrack(audioFormat) // MediaMuxerをスタートする // * start前に、書き込みを行うトラックが追加されている必要がある。今回の場合は、videoTrackとaudioTrack // * 一度startするとトラックの追加はできない muxer.start() // MediaMuxerに出力用のデータを書き込む // * エンコーダから出力された動画データを書き込む muxer.writeSampleData(videoTrackIndex, videoByteBuffer, videoByteBufferInfo) // * MediaExtractorで取得した音楽データを書き込む muxer.writeSampleData(audioTrackIndex, audioByteBuffer, audioByteBufferInfo) まとめ 以上が動画や音楽のエンコード、デコード、差し替えの流れになります。 流れとしてはシンプルですが、実装にあたっていくつかハマった点があるので、それぞれの工程ごとに列挙します。 動画ファイル、音楽ファイルデータ読み込み時 MediaExtractorのselectTrackを忘れずに実行する selectTrackしなくてもエラーにならず空のデータが取得されるだけの挙動となり、原因究明に時間がかかってしまった エンコード、デコード時 releaseOutputBufferを忘れずに実行する encoder,decoderの結果が出力できないので結果として処理が止まり無限ループに陥ってしまった 差し替え時(MediaMuxer利用時) 動画と音楽を1つのファイルに書き込むときは、事前に全てのデータを書き込める状態にしておく必要がある MediaMuxerを開始する前(start()を呼ぶ前)に、書き込みたい動画と音楽をaddTrackしておかなければならない start()は一度実行したらstop()しても再度start()を呼ぶことはできず1つずつトラックを書き込んでいくことができない 動画や音楽のデータをaddTrackする時に指定するMediaFormatは実際にencoderから取得できるものを利用する必要がある encoderからMediaFormatを取得できるようになるタイミングは最初のエンコード結果が出力される直前 MediaFormatの取得を待ってからmuxerの書き込みを開始させた 自分でencoder作成時に指定したMediaFormat ではない ので注意が必要 encoderから取得できるMediaFormat以外でaddTrackした場合、書き込み正常に行われない これらを知っておくことで、うまく変換できなかった時に「何を調べたら良いのか」の足がかりになると思います。この情報が、動画の音声を任意の別データに差し替えてエンコードしたいと思った人の一助になれれば幸いです。 最後までご覧いただきありがとうございました。ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com
はじめに こんにちは。ブランドソリューション開発本部 WEAR部 SREの和田( @wadason )です。普段は 「ファッションコーディネートアプリ WEAR」 のSREとしてクラウドの運用やリプレイスをおこなっています。 WEARはサービス開始から10年が経ち、クラウドやオンプレミスを含む大小様々なシステムが稼働しています。アプリケーションを動かすための基盤にはAmazon ECSのようなコンテナを前提としたものから、オンプレミスのAPIやBatchを動かすIISまで幅広く扱っています。そうした中で、約1年前にSREチームが結成され、技術負債の脱却やクラウドを中心としたインフラの運用を行なってきました。当初取り組んでいた大規模なリプレイス案件も落ち着き、チームメンバーが増えてきたので、現在では 分散した技術スタックをKubernetesへ統一するリプレイスプロジェクト を開始しています。 本記事ではWEARにKubernetesを導入した背景や、移行にあたり工夫した事例を紹介します。Kubernetesの導入を検討している方やSREの活動事例を知りたい方に向けて、少しでも参考になれば幸いです。 目次 はじめに 目次 導入の背景 技術負債の脱却による運用負荷の低減 GitOpsによる開発者体験の向上 リプレイス後の構成 EKS内で利用する様々なツール 複数のEKSクラスタを用意する理由 デプロイ 工夫した点 Digdag WorkerにおけるConfigMapの動的管理 Push通知におけるリアルタイム性を意識した構成 振り返り 運用負荷が下がり積極的な改善に集中できる体質に 継続的に改善に取り組むことが大事 今後の展望 アプリケーションの段階的な移行 サービスに寄り添う継続的な改善 おわりに 導入の背景 技術負債の脱却による運用負荷の低減 こちらはWEARで利用するクラウドやAWS内のシステムを中心に利用状況をまとめた概要図です。 Fastlyによるオンプレクラウド間のパスルーティング、Akamaiによる画像リサイズなど、様々なクラウドサービスを活用しています。AWS内ではOpsWorksで構成管理するDigdagというワークフロー基盤やECSクラスタで稼働するRailsのAPIを運用しています。その他にもプッシュ通知を扱うインスタンス群が別のAWSアカウントに存在します。プッシュ通知に関する構成は後述します。 これらのシステムは構築されてから数年が経過しています。次第にシステムの複雑性が増していき、ノウハウを引き継げていない状態でした。そのため、障害や日々の運用に関して、なかなか対応のスピードを出せないという課題がありました。そこで、全社的にも広く利用されており、チーム内でも経験者が多いKubernetesに全てのアプリケーションをリプレイスしようと考えました。 GitOpsによる開発者体験の向上 アプリケーションをデプロイするために、OpsWorksスタックの仕組み 1 や、AWSのCodeBuild、CodePipelineと様々なサービスを利用しています。 障害などのイレギュラーな事象が発生した際に、このようなバリエーションの多さは素早い判断のボトルネックになってしまう傾向 がありました。 普段の開発では特に課題を感じることはありませんでしたが、障害時は上の図ような複数のサービスの構成を思い出しながら調査を進めることになります。 例えば、CI/CDで利用する環境変数だけに焦点を当てても、以下を利用しています。普段は頻繁に設定を変更しないため、どこで何を設定しているのかが分かりにくい状態にありました。 CircleCIのSecret 2 AWS Systems Managerの一機能であるParameter Store 3 AWS Secrets Manager また、Railsアプリケーションをロールバックする際に、時間がかかってしまう課題がありました。 Dockerイメージをlatestタグのみで管理しているので、ECSのタスクを1つ前にビルドしたイメージでデプロイし直すことができないからです。 アプリケーションはRailsのモノレポであるため、ロジックが膨大でありテスト項目も多いです。もう1度ビルドする場合はRevertしてPull Requestを再作成し、CIによるテストを待たなければいけません。さらに、Pull Requestを適用するとDockerイメージのビルドやECSのタスクを更新するパイプラインを再度実行することになります。 そこで、従来のさまざまな仕組みを利用した複雑なデプロイパイプラインを、可視性が高くシンプルなものへ変更したいと考えGitOps 4 を採用することにしました。GitOpsはGitを「Single Source of Truth」 5 として、普段行うデプロイから緊急時のロールバックまで一通りの操作を開発者が普段から利用するGitに集約できます。 GitOpsを実現するツールにはArgo CD 6 やFlux 7 、Jenkins X 8 などがあります。WEARでは充実したGUIに魅力を感じArgo CDを選びました。 このGUIは、機能追加を行う開発チームも利用することを想定しています。 Kubernetesの導入にあたり調査や検証を行なってきたSREとは違い、ECSやOpsWorksを中心に利用してきた開発チームにとってはEKSにも慣れていく必要があります。 全社的にKuberntesの利用は活発ですが、WEARでの採用は初めてであるため、こういった分かりやすさも重要な点と考えました。 リプレイス後の構成 上述の背景を踏まえ、EKSを中心とするシステム構成へと変更しました。以下はその概要図です。 上で紹介したオンプレミスやFastly, Akamaiは今回のリプレイスの対象外となるので省略しています。 以下に重要な変更点を列挙します。 ワークフロー基盤を扱うOpsWorksを廃止してEKSへリプレイス Railsアプリケーションを扱うECSの各クラスタをnamespace単位でEKSへリプレイス 別アカウントに存在するPush通知を扱うシステムをRailsに置き換えEKSへリプレイス(詳細は後述します) AWS CodeBuildやAWS CodePipelineを廃止し、すべてのシステムを後述するGitOpsによるデプロイ方法へ統一 また、リプレイスにあたり以下のAWSサービスを廃止します。 AWS CodeBuild AWS CodePipeline AWS OpsWorks AWS Systems Manager Parameter Store Amazon ECS 別アカウントに存在するAmazon EC2インスタンス 別アカウントに存在するAmazon SQS 別アカウントに存在するAmazon SNS AWSのサービスだけでも廃止できるものが多く、シンプルになったことが分かります。 EKS内で利用する様々なツール リプレイスにあたり、他のAWSサービスと連携したりモニタリングを行うための様々なツールを導入しました。以下はその一覧です。 ツール 用途 argoproj/argo-cd デプロイ, GUI external-secrets/external-secrets AWS Secrets Manager等と連携しsecretを作成 kubernetes-sigs/aws-load-balancer-controller Ingressと連携しAmazon Elastic Load Balancingを作成 kubernetes-sigs/external-dns Ingressと連携しAmazon Route 53 Recordを作成 kubernetes-sigs/prometheus-adapter モニタリング, HPA prometheus-operator/prometeheus-opereator モニタリング, HPA prometheus-community/postgres-exporter モニタリング, HPA DataDog/datadog-agent モニタリング aws/aws-for-fluent-bit ロギング これらのツールはHelm Chartsで管理しています。Argo CDでは通常のマニフェストの他に、Helm Chartsのリポジトリを指定できるので、Argo CD Applicationを宣言してそれぞれのChartを導入しています。以下はArgo CDをApplicationで管理する例です。 apiVersion : argoproj.io/v1alpha1 kind : Application # Argo CDのカスタムリソースであるApplicationを宣言 metadata : name : argocd spec : project : default source : repoURL : 'https://argoproj.github.io/argo-helm' targetRevision : 4.5.0 # Helm Chartのバージョンを指定 chart : argo-cd helm : values : | # values.ymlと同じように設定を記述 server : service : type : NodePort parameters : # パラメータでもvalues.ymlと同じような設定が可能 - name : "server.ingress.hosts[0]" value : "xxxxxxx" destination : server : 'https://kubernetes.default.svc' Helm Chartsやマニフェスト一式はApp Of Apps Pattern 9 で管理しており、以下のようになっています。 複数のEKSクラスタを用意する理由 Kubernetesのクラスタを構築するにあたり、大まかに以下のパターンが一般的です。 1つのクラスタで複数のシステムを扱うマルチテナント(シングルクラスタ) 複数のシステムをクラスタ単位で扱うシングルテナント(マルチクラスタ) WEARでは基本的にはマルチテナントにする方針です。理由は以下です。 モノレポのRailsアプリケーションで複数のシステムを扱い、内部通信が行われるため 少人数のSREチームでKubernetesクラスタを運用しており、運用負荷や改善の横展開を容易にするため しかし、EKSを導入して間もないWEARでは、極端にマルチテナントに振り切らず、 ユーザーへの影響度 という観点で分離しました。 ワークフロー基盤を中心とするクラスタ Railsアプリケーションを中心とするクラスタ ワークフロー基盤を中心とするクラスタは、一部のシステムが停止した場合に直ちに影響があるわけではありません。一方で、Railsアプリケーションを中心とするクラスタは、停止してしまうとコーデを閲覧できなどサービスを提供する上でユーザーに影響を及ぼします。 さらに、Railsアプリケーションを中心とするクラスタは、それぞれのシステムが内部通信する場合を想定できます。同じクラスタ内で管理することは余計な認証処理を実装する必要がなくなる実装上のメリットもあります。 このように、サービスレベルに応じてEKSクラスタを分離することで、 バージョン更新などの運用による影響範囲を縮小できます。 さらに、システムが拡大してもEKSクラスタの数が増えないので、運用負荷が上がることはなく改善のスピードも落ちません。 デプロイ 続いてKubernetesの導入に伴うデプロイパイプラインのリプレイスについて紹介します。 WEARではRailsアプリケーションをモノレポで管理しています。 新たな環境では、開発者がRailsアプリケーションのリポジトリへPull Requestを送り、変更が適用されるとGitHub Actionsのワークフローが起動します。 ワークフローは2つのステップになっています。まずArgo CDがSyncするリポジトリのDockerイメージタグを書き換えます。次にSync先のリポジトリが変更されるとArgo CDが変更内容を検知しEKSへデプロイを行います。 GitHub Actionsにおける最初のステップではDockerイメージをビルドしECRへプッシュします。このとき、GitのコミットハッシュをDockerイメージのタグに登録します。こうすることでコミットごとにユニークなタグを管理できます。 次のステップはGitOps用のリポジトリを取り込み、新たにビルドしたイメージのタグに書き換えます。 Kustomizeでは kustomization.yaml というファイルの newTag でDeploymentに適用するイメージタグを保持しています。 kustomizeの設定は以下です。 apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization resources : - ../../base patchesStrategicMerge : - deployment.yaml images : - name : rails-app newName : rails-app newTag : a3bc8c6fa529fcc64ae6f51dd6133521a28c7468 GitHub Actionsで newTag を変更する処理は以下です。 $ cd ./path/to/overlays/ $ kustomize edit set image ${IMAGE_URI} = ${IMAGE_URI} : ${GITHUB_SHA} このようにすると新しいイメージタグが kustomization.yaml に適用されるので、リモートリポジトリをGitのコマンドを用いて変更します。 また、デプロイ時に行うGitOps用のリポジトリへの変更は、ロールバックにも応用できます 10 。デプロイ前に格納したイメージタグを取得し、 newTag を変更するだけです。 工夫した点 Digdag WorkerにおけるConfigMapの動的管理 RailsのRakeタスクなどを動かすワークフローは、Digdagの機能であるCommand Excecuter 11 を用いて実現しています。 Digdag WorkerのPodに格納されているKubernetesマニフェストを kube-job というツールを用いて実行します。この辺の細かい設計は @calorie が考えてくれました。 詳細は Kubernetes上でDigdagとEmbulkを動かすワークフロー基盤 に記載してあります。 ここでは、しばらく運用してから発生した課題に触れたいと思います。 Digdag Worker内で保持するマニフェストはConfigMapで生成しており、Pod起動時にマウントすることで参照が可能になります。 ConfigMapでファイルを生成するには data を用います。 apiVersion : v1 kind : ConfigMap metadata : name : sample-batch data : task.yaml : |+ apiVersion : batch/v1 kind : Job (省略) 当初は課題に感じなかったのですが、yamlのシンタックスの不備やマニフェストとして正しいかを検知できず、誤った内容でリリースしてしまう可能性がありました。 開発者もKubernetes secretや起動コマンドの調整などの変更を加える機会が多く、なるべく管理しやすい方法を模索しました。 この問題を解決するために、KustomizeのconfigMapGenerator 12 を採用しました。 configMapGeneratorは名前とファイルを指定することで、動的にConfigMapを生成してくれます。 続いてはその方法です。 /config 内にファイルを配置しています。 この task.yaml は上述のConfigMapにおける data 内で記載したマニフェストがそのまま格納されています。 └── base ├── config │ └── task.yaml # `configMapGenerator`で指定するファイル ├── deployment.yaml └── kustomization.yaml 次に configMapGenerator にファイルパスを指定します。 また、Argo CDのSyncを前提としているためConfigMapを再作成するように generatorOptions を設定しています 13 。 apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization resources : - deployment.yaml configMapGenerator : - name : batch-sample files : - ./config/task.yaml generatorOptions : annotations : argocd.argoproj.io/compare-options : IgnoreExtraneous # Argo CDでSync時に更新を行うために必要 Deploymentには configMapGenerator で指定した名前を記述します。 apiVersion : apps/v1 kind : Deployment metadata : name : digdag-worker spec : template : spec : volumes : - name : batch-sample-volume configMap : name : batch-sample # configMapGenerator.nameと同様 containers : - name : digdag-worker volumeMounts : - name : batch-sample-volume mountPath : /sample このように設定した後に、 kustomize build を実行すると動的にConfigMapが生成されます。 apiVersion : apps/v1 kind : Deployment # (省略) volumes : - configMap : name : batch-sample-ccfh9mc2t6 # ハッシュ付きでConfigMapを読み込んでいる name : batch-sample-volume batch-sample-ccfh9mc2t6 というConfigMapが動的に生成され、Deploymentにも反映されています。 Push通知におけるリアルタイム性を意識した構成 Railsアプリケーションを中心とするEKSクラスタの最初の移行対象としてプッシュ通知を扱うシステムを選定しました。 冒頭の構成図では省略しましたが、以下のようになっています。 プッシュ通知を扱うシステムは非常に古く、当時運用が活発だった頃のドキュメントが少ないため、運用負荷が高い状態でした。 また、サービスの成長に伴い配信する通知の量が増加し、細かい部分に改善を加える必要がありました。そのためSidekiq 14 へリプレイスすることになりました。 ここではSREとバックエンド両チームで工夫してきたことをご紹介します。 WEARで扱う通知は大まかにリアルタイム性が重要かそうでないものに分類できます。 それらを分類してピッチコントロールできるように、以下のような構成にしました。 今後は新規の機能追加に通知が伴う場合、キューを分けるべきかを案件の特性に合わせて決めることができます。現状の負荷と照らし合わせて、HPA等の細かいチューニングを行うことができるはずです。 また、頻繁にスケールすることが想定されているのでヘルスチェックやGraceful Shutdownにも対応しました 15 。 振り返り 運用負荷が下がり積極的な改善に集中できる体質に OpsWorksの運用負荷は高い状態でした。具体的な運用として以下のようなものがありました。 メンバー異動時のOpsWorks Stacks Usersの管理 16 OpsWorksで管理するEC2インスタンスの台数やオートスケールの見直し インスタンス内で動作するミドルウェアの運用(Chefのcookbookの調整) EC2インスタンス内で実行するDigdagジョブのインフラ起因のトラブルシューティング(OOMなど) 切り替えは2022年2月に行いました。下のグラフが示す通り、2021年8月と約8倍ジョブが実行されていることがわかります。 またDigdag Workerとジョブのインフラが切り離されたことで、実行回数が増えても失敗率を減少傾向に止めることができました。 もちろん今後も個別のジョブがメモリ不足により落ちるケースは想定できます。そういった場合はKubernetesマニフェストを修正して対応すれば良く、これまでのようにインスタンス自体のキャパシティに引きずられることはありません。 さらに、ジョブ全体の停止につながる障害も2022年2月以降は発生しなくなりました。 リプレイス前は、月に1件以上インスタンスがOpsWorksのLayerに紐づかなくなる障害が発生していました。 このようにOpsWorksのLayerにプロセスが存在しない場合を検知しています。構成管理の設定が原因で起動できず、調査や復旧に時間を割いていました。 運用負荷が下がったことで、本記事で紹介してきたような改善に集中できました。 継続的に改善に取り組むことが大事 Kubernetesを導入した目的はその拡張性を活かして現行のシステム課題を大きな枠組みで捉え直し、サービスを運営する上で発生する様々な問題を継続的に改善することにあります。プロジェクト発足時の計画に捉われすぎず、解決策や課題を共有し合う取り組みが重要です。 SREチームでは、本記事で取り上げたKubernetesへのリプレイスプロジェクト以外にもさまざまな活動を行なっています。 リプレイスを行う主担当だけでなく、チーム全員がKubernetesによる改善を行えるように「運用改善」という取り組みを始めました。 これは、キャッチアップや日々の学習で得た知識や課題をアウトプットする場です。週に1度、Kubernetesの新しい技術をモブプロで検証してみたり、日々の運用で感じた課題に取り組み結果を共有しあうというものです。 上に挙げたconfigMapGeneratorもこの取り組みの中で実施しました。既に半年ぐらいこの活動をやっていますが、今ではKubernetesに限らずさまざまなことが議論されています。参考までに、運用改善で行った取り組みをいくつか挙げます。 ローリングアップデートによるKubernetesバージョンの更新方法の検証 kube-tools を利用したkubevalによるマニフェストチェックをCIに導入 aws-loadbalancer-controllerとCognitoを組み合わせてDigdagやArgo CDのGUIに認証を導入 Podのログ基盤をS3、Athenaへ統一 Prometheusと postgres_exporter を導入しDigdagのキューサイズに応じてスケールするHPAを導入 GitHub ActionsとAWS間の認証をOIDCへ統一 GitHub Runnerを活用し digdag push などの内部通信を KubeDNS へ置き換える ECRにPrivate Linkを導入してNAT Gatewayのコストを削減 今後の展望 アプリケーションの段階的な移行 引き続きECSクラスタを移行していきます。 残りのECSクラスタで稼働するシステムは既に移行を終えたRailsアプリケーションと同じであり、起動方法を変更すれば良いものがほとんどです。 しかし、移行作業の難易度は低くてもWEARの中心を担うAPIであるため、非機能面での課題や切り替え作業に関して考慮すべき点は増えるはずです。 半期ごとに対象を定め、重要度が低いシステムから順次移行します。 サービスに寄り添う継続的な改善 移行と並行しながら改善を進めることができたのはチーム全員が改善を楽しみながらアイデアを出し合い積極的に取り組んでくれたからです。 今後もKubernetesに限らずですが、運用改善やモブプロをはじめとしたチームでの取り組みを継続する予定です。 おわりに クラスタの構築方針やGitOpsの採用など土台となる部分を整備しつつ、設計の見直しが必要であるワークフロー基盤やPush通知の移行を終えることができました。 Kubernetesやその周辺の技術は変化が激しいので、当初は最善と思えてもあっという間に新しい方法が出てきます。そういった変化を楽しみつつ、より良いサービスにしていきます! WEARでは一緒にサービスを改善してくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://hrmos.co/pages/zozo/jobs/0000021 hrmos.co AWS OpsWorks | Deploying Apps ↩ 詳細は circleci|環境変数の使用 を参照してください。 ↩ AWS Systems Manager Parameter Store ↩ GitOps | What is GitOps? ↩ wearveworks | Guide To GitOps ↩ 詳細は Argo CD を参照してください。 ↩ 現在ではv1はmaintenance modeとなっており、v2である flux2 を参照してください。 ↩ 詳細は jenkins-x を参照してください。 ↩ App Of Apps Pattern ↩ Argo CDにはGUIやCLI上で1つ前のrevisionに戻す機能があるものの、緊急時の場合でも普段から行なっている手法で対処できる方が良いと判断しました。 ↩ Digdag|Command Executor ↩ Declarative Management of Kubernetes Objects Using Kustomize|Generating Resources ↩ Configmapが差分として検知されることでSyncに失敗する事象が発生しました。Argo CDのSync時の挙動を制御するためのアノテーションを設定しました。詳細は Ignoring Resources That Are Extraneous に記載されています。 ↩ Sidekiq ↩ sidekiqはバージョン6以前とそれ以降でGraceful Shutdownの手法が異なっており、詳細を こちら にまとめました。 ↩ Managing AWS OpsWorks Stacks Users ↩
こんにちは!ZOZOTOWN開発本部 Android1ブロックでAndroidテックリードを務めている いわたん です。 2022年6月30日のお昼時に、おいしい健康さん、Rettyさん、アンドパッドさんと4社合同でAndroidエンジニア向けオンラインイベント、「 Jetpack Compose 導入事例【おいしい健康|Retty|ZOZO|アンドパッド】 」を開催しました。 私は「Jetpack Composeでの画面遷移」というタイトルで発表しました。 イベント開催と登壇のきっかけ おいしい健康さんの sobaya15 さんからTwitterのDMで弊社の 堀江 に勉強会の話をいただき快諾したそうです。当初からJetpack Composeをテーマにしたいとのことだったので社内で調整が始まり、直近でComposeを使った開発に関わっていた自分に白羽の矢が刺さりました。 自分に登壇しないかとお誘いがありましたが、どうしても二人の子供の世話の兼ね合いで夜開催だと参加が難しいため、「お昼開催だったらOKですよー」と返しました。そうしたところ、本当にお昼開催の枠を取ってきてくれました。感謝です。 発表資料の準備 ネタだしの段階では、新規リリースしたARメイク関連の話やJetpack Composeのテスト関連、Jetpack Composeの画面遷移とどれを話すか悩みました。発表時間内に収まりそうな題材、ちょうどそのタイミングで調査をしていたということでJetpack Composeの画面遷移について話すことにしました。他の題材に関してはまた別の機会に登壇しようと思います。 久しぶりの登壇ということもあり、張り切って資料を作成しました。しかし、開催2日前にチーム向けに登壇練習をしてみたところ、持ち時間10分に対して、結果17分と時間を盛大にオーバーをしていました。色々と話したいことがたくさんありましたが、必要最低限まで情報を削っていくのは辛い作業でした。 謝辞 アンドパッドさんには、準備や段取りをスムーズに行っていただいて感謝です。おいしい健康の霜重さん、Rettryの松田さん、アンドパッドの橋詰さん、参考になるお話をたくさんして頂き楽しかったです。一緒に登壇して頂きありがとうございました。参加者の皆さんには、YouTubeやTwitterで声援ありがとうございました。何かひとつでも知見を持って帰っていただいて生かしていただけたら幸いです。 最後に ZOZOでは、プロダクト開発以外にも、今回のようなイベントの開催や登壇など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、ブランドソリューション開発本部 WEAR部 Androidブロックの武永です。普段はファッションコーディネートWEARのAndroidアプリを開発しています。 みなさん、GitHub Actionsでの自動化進めてますか? 毎回ローカルでパッケージをビルドしストアに上げその際に人為的ミスが起こったり、担当者の作業が止まってしまっていませんか?GitHub Actionsを使えば、Google Play Console上に自動アップロードを行うことができます。 導入方法 使用するライブラリは r0adkll/upload-google-play です。 github.com 選定理由は次の通りです。 GitHub Actionsで利用できる 段階的公開や、クローズドテストにも対応している それでは実際に見ていきましょう。 .github/workflows直下にファイルを追加 実行ファイルを.github/workflows直下に作成します。.githubとworkflowsのディレクトリがない場合は作成します。 リポジトリから参照するsecretの追加 次にリポジトリから参照するシークレットを追加します。 GITHUB_TOKEN GitHubの設定から追加します 既に作成済みの場合は同じものを流用できます ENCODED_RELEASE_KEYSTORE キーストア Base64形式のテキストにエンコードします KEYSTORE_PASSWORD キーストアのパスワード KEY_PASSWORD キーのパスワード SERVICE_ACCOUNT_JSON サービスアカウントを参照します 次の項目で説明します 上記のシークレットは必須項目です。 Google Cloud Platformでサービスアカウントを作成する Google Cloud Platformに移動し、プロジェクト内でサービスアカウントの作成画面に移動します。サービスアカウント名、サービスアカウントID、サービスアカウントの説明(任意)を入力します。ロールは参照者以上にします。オーナー権限でも可能ですが過剰な権限になってしまうので特に必要がなければ参照者を推奨します。サービスアカウントユーザーロールに、作成したアカウントを入力します。 サービスアカウントの秘密鍵を作成する サービスアカウントを作成したら次は秘密鍵です。サービスアカウントの詳細からキーを選択し、作成します。キーのタイプはJSONにします。 キーは再度ダウンロードできないので大事な場所に保管しておくことを推奨します。 作成したユーザーを招待する サービスアカウントの作成が完了したらGCPからGoogle Play Consoleへ作成したサービスアカウントを招待します。サービスアカウントはZOZOの集団にいるので今回はWEARのプロジェクトに招待する必要があるため権限を付与しています。ユーザーを招待する際にどの権限を付与するかチェックを入れます。 Google Play Console上で権限を付与する アカウントの招待後に同じ権限でアプリへの権限を付与します。 実際のコード トリガー設定 name : Deploy Google Play Console on : pull_request : branches : - main types : [ closed ] まず、どのブランチがトリガーになるかを設定します。プルリクエストがクローズされると発火します。マージ先はmainです。ブランチをPushしたときにでも設定は可能です。 ビルド jobs : build : runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v2 - name : set up JDK 11 uses : actions/setup-java@v1 with : java-version : 11 Java 11でビルドします。 Keystoreをデコード Keystoreは単純なバイナリなのでBase64形式でencodeし、文字列としてシークレットに保存する必要があります。 - name : Decode Keystore run : echo ${{ secrets.ENCODED_RELEASE_KEYSTORE }} | base64 --decode > ${{ test.keystore }} シークレットから読み取る際にdecodeすることによって元のファイルに戻せます。 App Bundle作成 - name : Generate AAB run : ./gradlew :app:bundleRelease env : RELEASE_KEYSTORE_STORE_PASSWORD : ${{ secrets.KEY_STORE_PASSWORD }} RELEASE_KEYSTORE_KEY_PASSWORD : ${{ secrets.KEY_PASSWORD }} App Bundleを作成します。apkも作成できますがApp Bundle化するに越したことないので対応しましょう。 サービスアカウント作成 - name : Create service_account.json id : createServiceAccount run : echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json サービスアカウントを参照します。 デプロイ - name : Deploy to Play Store id : deploy uses : r0adkll/upload-google-play@v1.0.15 with : serviceAccountJson : service_account.json packageName : com.test.deplaoy.app releaseFile : app/build/outputs/bundle/googlePlayRelease/app-googlePlay-release.aab track : beta whatsNewDirectory : whatsnew/ Google Play Consoleにデプロイします。こちらで今回採用したライブラリを使用しています。今回はbeta版にアップロードをし、releaseFileについては作成されたApp bundleが置かれる場所を指定しています。 Slack通知設定 - uses : 8398a7/action-slack@v2.5.2 if : failure() with : status : ${{ job.status }} text : リリースビルドが失敗しました。確認をお願いします。 mention : 'here' env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK }} - uses : 8398a7/action-slack@v2.5.2 if : success() with : status : ${{ job.status }} text : リリースビルドが完了しました! env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK }} 任意設定ですが、Slack通知を設定します。 8398a7/action-slack@v2.5.2 はGitHub ActionsからSlack通知するライブラリです。 アップロード後の運用 クローズドテストが優秀! WEARの開発では上記のフローでリリースをしています。直接本番リリースも可能ですが、本番と同じ形式のクローズドテストで動作確認が行えるので確認後に本番リリースをします。クローズドテストは招待したメールアドレスのユーザーが確認可能です。 リリース前レポートで事前にエラーを発見できる リリース前レポートも優秀で、致命的なクラッシュを事前に知ることができます。Google Play Console上でも便利な機能が多いので、ありがたいですね。 今後の課題 この自動アップロード導入により作業効率が上がりました。CIツールは多様な種類がありますが、個人的にGUIがしっかりしているものが使用しやすかったです。導入後にさまざまな課題がでてきました。例えば、他言語対応しているリリースノートをスプレッドシートで定義しておりそれをGoogle Play Cosole上で手動入力しているのですが、こちらも自動化させたいです。 また、今回は採用しなかったのですがGradleの知識があれば Gradle Play Publisher を使用できると良いと思いました。 最後までご覧いただきありがとうございました。ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com
こんにちは。ECプラットフォームサービスSREチームリーダーの川崎( @yokawasa )です。本記事では、コードレビューを通じたチームのパフォーマンス向上のための取り組みについてご紹介します。なお、コードレビューそのもののテクニックに関する話はしないので、あらかじめご了承ください。 目次 目次 はじめに コードレビューはチーム全体のパフォーマンス向上のため 複数ユニット、複数チームで行う 活動状況を定量的に評価する コードレビュー体験を向上させる レビュアーの負担を減らす 同期・非同期コミュニケーションを使い分ける 参加しやすい雰囲気を作る 1. 心理的な安全性を高める 2. チームの共通目標にする さいごに はじめに まずはじめに、我々はGitHubのPull Request(以下、PR)機能を活用してコードレビューをしています。下記の記事でも書いているようにIaCとCI/CDを基本ルールとしています。可能な限りすべての構成はコードで管理し、PRレビューでApprovalされた更新内容をメインブランチにマージし、CI/CDを通じて環境にデプロイします。よって、本記事におけるコードレビューはPRにおけるレビューと同義になります。 techblog.zozo.com コードレビューはチーム全体のパフォーマンス向上のため コードレビューは一般的に「ソースコードの品質向上」の観点で語られることが多いかと思います。コードの保守性・効率性・信頼性の確認、規約・ルールの徹底、テスト内容や関連ドキュメントの確認などさまざまな目的があります。しかし、我々がコードレビューをする理由はこれだけではありません。我々はコードレビューを通じて究極的にはチーム全体のパフォーマンス向上を目指しています。我々が考えるパフォーマンスの高いチームとは、高いレベルで知識の共有がされ、十分にコラボレーションが促進されているチームを指します。順を追って説明します。 コードレビューは、レビュアーとレビュイー間で知識や技術のトランスファーを行い、互いに学びあえる最高のスキルアップの場です。レビュアーはレビュイーや他のレビュー参加者により良い解決策やアイデアの提示、ベストプラクティスなどの共有ができます。また、他人が書いた洗練されたコードをみることで技術力向上にもつながります。 また、お互いのシステムや活動を深いレベルで理解できるようになると、より具体的なコミュニケーションが取りやすくなりチーム内コラボレーションの促進が期待できます。さらに、これを複数チームに広げることでその組織全体のコラボレーション促進が期待できます。結果、チームや組織全体のパフォーマンスの向上が期待できるというわけです。 複数ユニット、複数チームで行う 我々は複数ユニット、複数チームでクロスコードレビューをしています。 主担当外のユニットやチームが関わることはスピードや効率性の低下、ノイズの増加につながるといったマイナス面はあるかと思います。しかしながら、我々はZOZOTOWNの成長に伴い担当者が増えたとしても組織がサイロ化することなくスケールできることを目指しており、そのためには必要なトレードオフだと考えています。 クロスコードレビュー推進の発端は、2021年当時私が所属していたZOZOTOWNマイクロサービスを担当するSREチーム(以下、PF-SREチーム)の拡大に伴い生じた課題感からでした。 PF-SREはZOZOTOWNリプレイスを推進するSREチームとして2020年4月に発足しましたが、リプレイスの進行とともにチームは拡大し担当するシステムやサービスが増えていきました。そうなってくると、全員がモノリシックにすべてを見るわけにもいかなくなってきたので、チームで担当するシステムを適度な粒度のドメインに分けてドメインごとにユニットを組んで動くようしました。 そこで生じてきた問題がサイロ化です。担当ユニットの専門分野については詳しいが、隣のユニットのことはよく分からない状況が見られるようになってきました。各システムの更新頻度を考えると全メンバーが勉強会・定例会などで同期するには難しい状況がありました。コードレビューにおいては、一部のユニット担当者でのみ議論がなされるようになり、他のユニットで生まれたベストプラクティスや課題感がうまく全体に共有されづらくなりました。また、レビュアーがユニットごとで属人化するようにもなりました。 そうした課題の解決策の1つとして行ったのが、複数ユニットによるクロスコードレビューです。定例会や勉強会と比べ非同期でできるため時間的制約が少なく、また高頻度で回せることが魅力でした。そして、実施した結果、サイロ化は緩和され、知識共有とコラボレーション濃度は高まり、チーム力の向上がみられました。 なお、PF-SREチームはチーム内複数ユニット体制から今では下図のようなドメインを責務とした複数チームに分割されていますが、引き続き複数チーム間でクロスコードレビューをしています。 さらに、これは実験的でありますが、2022年4月よりコードレビュー参加対象をマイクロサービス担当チームに限らずZOZOTOWNを担当するSRE部配下すべてのチームに広げて実施しています。この試みについてはまだ目立った効果は見られないものの、他チームのシステムに対する関心度が変わったであるとか、他チームのレビュー参加意欲が上がったなどポジティブなフィードバックが得られはじめています。 活動状況を定量的に評価する コードレビューの活動状況は定量的な数値データを元に把握し、継続的な改善に努めています。その取組について簡単に紹介します。 まず、コードレビューの活動状況を定量的に把握するために Findy Teams を活用しています。Findy Teamsは、GitHubにおけるPR作成数、レビュー数などさまざまな活動をメトリクスとして可視化してくれるサービスです。活用することで、個人やチームレベルの活動状況を定量的に把握できます。 findy-teams.com 下記はチームの活動状況が確認できるFindy Teamsの「チームレポート」ページのスクリーンショットになります。 中でも、とくに参考にしているのはコードレビューの活動量を示す以下のメトリクスになります。 レビュー数: PRに対してレビューした数(PRコメント除く)です 自分がレビューしたプルリク数: レビューに関わったPRの数です プルリクに対する自分のコメント数: PRへのコメント総数です。コメント数が多いほど多くの議論が行われている可能性があります。一方、コメントする必要のない良質なPRもあるので多いから良いという判断は一概にはできません 自分が最初のレビューをするまでの時間: 最初にレビューするまでの時間です。数値が短いほどよいと言えます この結果データは定期的にレビューしています。Findy Teamsを使い始めて最初の半年くらいは、”マネージャーごと”として一人もくもくとチームの活動状況を眺めていました。結果が思わしくないようだったらどうすればコードレビューが促進されるのかチームミーティングで議論を促していました。しかしそれではスケールしていかないだろうと考えを改め今年から”チームごと”としてメンバー全員で結果データを見ていくようにしました。現在は、週次のチームミーティングのアジェンダの1つとして「チーム生産性活動レビュー」を設け、チーム全員で結果データをみて振り返りを実施しています。 コードレビュー体験を向上させる ここではコードレビューの体験を向上させるために実施したことや、結果的に実施したことでコードレビュー体験が向上したことについてご紹介します。 レビュアーの負担を減らす レビュアーの負担を減らしレビュアーフレンドリーなPRにすることで、レビュアーのストレスは減り、コードレビューの速度は加速されます。以下、レビュアーの負担を減らすために実践していることを簡単に紹介します。 PRの粒度は小さくして目的を絞ります。巨大なPRは見るのが億劫になります。また、1つのPRに複数の目的が混在している場合も同様です。PR粒度を小さくする理由については Google - eng-practices - Small CLs が参考になります PR説明は簡潔で分かりやすく書きます。 PRテンプレート を活用し、PRの目的、関連Issue/PR、テスト内容、変更による影響範囲などPRへの記入項目をあらかじめ用意します 作成されるPRにはGitHub Actionsで ラベル を自動付与します。ラベルはレビュアーが同様の種類の課題をすばやく整理して集約するのに役に立ちます。ラベルの自動付与はGitHub Actions経由で、 labeler を活用しています PRで提案されるコードは、可能な範囲でGitHub Actionsにより事前に自動チェックし、レビュアーの負担を軽減します。たとえば、フォーマットや規約遵守、デプロイ先環境との差分、利用APIやパッケージのセキュリティ課題などがあります 同期・非同期コミュニケーションを使い分ける 非同期・同期コミュニケーションをうまく使い分けることで、コードレビューを効果的にすすめることができます。 コードレビューは非同期コミュニケーション中心の活動になりますが、非同期で効率的に時間を有効活用できる一方、フィードバックが得られるまでのリードタイムが長くなるという課題があります。また、複雑な内容になってくると文章だけではツラいです。 このような課題に対して弊チームでは、非同期コミュニケーションに固執することなく、同期コミュニケーションをうまく取り入れるようにしています。素早い返信を要したり、濃度の高い会話が必要と感じたらビデオ会議で直接会話したり、一緒にモブプロを行うなど同期コミュニケーションにスイッチします。必要に応じて同期・非同期を行き来します。 また、レビュアーと一緒にモブプロしてからPRを作成するという流れもあります。この場合のレビューは間違いなくリードタイムが短くなります。 参加しやすい雰囲気を作る 先述のとおり、コードレビューを通じて知識のトランスファーやコラボレーション促進を目指しています。したがって、コードレビューにはできるだけ多くのメンバーが参加することを期待しています。以下、コードレビューに参加しやすい雰囲気を作るために工夫したこと2点紹介します。 1. 心理的な安全性を高める 担当外の案件のコードレビューだと関連システムを熟知しておらず的確なレビューができず億劫になりがちです。ただし、コードレビューは学びの場であり、コミュニケーションの場でもあります。システムを熟知していないメンバーも議論に参加できるように、PR上ではレビュアーが仕様や不明な点をレビュイーに気兼ねなく質問できる場であるという、雰囲気づくりを心がけました。 2. チームの共通目標にする 私がリードを務めるチームではチーム目標の1つに"クロスレビューへの貢献"をかかげています。全員が同じ方向を向いてコードレビューを盛り上げていくことを共通目標とすることで、レビューに参加し貢献することのモチベーションはあがり、協力しやすい雰囲気が生まれやすくなります。先述のとおり週次ミーティングでコードレビューの活動結果をレビューして、継続的な改善に努めています。全員が同じ目標をもっているので改善のためにの活動も足並みを揃えて実施できます。 さいごに コードレビューにはさまざまな目的があるかと思いますが、我々はコードレビューを通じてチーム全体のパフォーマンス向上を目指しています。本記事ではそのための取り組みについてご紹介しました。我々の取り組みが少しでも参考になれば幸いです。 なお、リモートワーク下においてはコードレビューの実施意義はとくに大きくなったと感じています。コードレビューは時間や空間に囚われることなく行える有用な非同期コミュニケーションツールの1つです。ZOZOではコロナ禍を機に多くの社員がリモートワークを行うようになりましたが、柔軟に時間を調整して実施できるコードレビューのリモートワークとの相性の良さを強く実感しています。 さいごに、ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 https://hrmos.co/pages/zozo/jobs/0000010 hrmos.co また、カジュアル面談も随時実施中です。まずは話を聞いてみたいという方は、以下のリンクからご応募ください。 hrmos.co
こんにちは、カート決済部の佐藤です。普段はZOZOTOWNカート決済サービスの新機能開発、既存改修、運用保守を担当しております。 弊社はモノリスからマイクロサービスへのリプレイスを進めており、カート決済サービスも先日リプレイスPhase1の記事を掲載いたしました。 techblog.zozo.com 本記事ではカートリプレイスPhase1全体を振り返りつつ、リプレイスプロジェクトを進める中で苦労した点や得られた知見等の、リプレイスの裏側をご紹介したいと思います。 カート機能はECサイトの中核を担う機能であり、障害のリスクを考えるとドラスティックな改修には中々手を出しにくい機能だと思っています。しかし、弊社ではリプレイスをしたことで確実に前進できたため、同じようなお悩みを抱えている方の参考になれば幸いです。 はじまり カートリプレイス計画の始動 カートリプレイス計画の策定 アーキテクチャ構成 いよいよ負荷試験 プロダクション環境を使った負荷試験 負荷試験で見つかった課題 課題(1)KDSシャードの一部しか使われておらず想定した性能が出ない 概念図 課題(2)Javaアプリケーション起動時のコールドスタート対応(JVM暖気) 課題(3)エラーを返しているがカートに商品が入っている そしてリリースへ 良かった良かった・・・と言いたいところだが エラーの要因 まとめ 最後に はじまり 2021年元旦、特定商品へのカート投入リクエストの集中によりデータベースのキャパシティを超えてしまいエラーが多発する事象が発生しました。そのようにアクセスがスパイクするような商品を弊社では過熱商品と呼んでいます。 以前から過熱商品の販売によるアクセス数の増加はありましたが、2021年元旦は圧倒的に群を抜いたアクセス数を記録し、明らかに一般ユーザーの導線ではなくBOTによる過剰アクセスと判断できました。当時は過剰アクセスへの対抗策が少なく、結果的に数時間カート投入・注文がしづらい状況となり損害は大きいものとなってしまいました。 その後、BOTによる過剰アクセスにも耐えうるシステムの整備が急務となりましたが、以下の状況により問題はより深刻になっていきました。 インフラ、アプリケーション両軸で対応を進めるものの、改良版BOTとのいたちごっこが続く さらに追い打ちとしてコロナにより外出できない時間が増えたことによるトラフィックの増加 カートリプレイス計画の始動 そんな中、小手先の改修に限界を感じはじめていたのと、弊社はリプレイス過渡期であり、カートシステムについてはオンプレミスで動いているため、抜本的な「スケールできるシステム」の必要性が高まっていました。 「リプレイスを推進するためには組織から変える」という弊社VPoE @sonots の舵取りのもと、カートシステムのリプレイスを推進するために新生カート決済チームが誕生しました。 この組織変更が無かったら通常業務もやりつつ片手間でリプレイスとなっていたため、リリースまで大分時間が掛かっていたはず。まず組織変更するというのは大正義! カートリプレイス計画の策定 いざリプレイス計画を策定しようとすると、現状の課題解消のため「あれもこれも」となりがちです。弊社もまさにその状況となり、ロードマップを引くのに苦労しました。しかし、やりたいことが増えるほどリリーススパンも長くなりエンジニアのモチベーション低下にも繋がりやすく、ビッグバンリリースによるリスクも高まります。「リリーススパンは半年+α」とし、その中でできることをマイルストーンに落とし込んでロードマップを引いていきました。 そして、Phase1では主に過熱商品販売時のBOTによる過剰アクセスにも耐えうるシステムを目的に、以下ゴールを設定しました。 キューによるキャパシティコントロール可能なシステム 目標値として過去のBOTアクセス数からサンプリングしたアクセス数×N倍に耐えうる値 欲張りすぎず、豪華すぎる仕様にしない。 課題を正しく理解する前にどうしても解決を急いでしまいがちなため、そこのバランスは特に意識していました。合わせて技術的に正しくてもサービスへの貢献度が低いなら成果として不十分なため、解決すべき課題を明確にしてスコープを小さくすることも意識しました。また、事前にリリーススパンを決めたことで、そこから逆算した形で落としどころを決めることができました。 アーキテクチャ構成 カートリプレイスPhase1のアーキテクチャ構成の概念図は以下となります。 Phase1では、Cartデータベースの前段にキューイングシステムを置くことでキャパシティコントロールを実現しています。キューイングサービス選定の際、AWSが提供しているサービスという点より以下2択となりました。 Amazon Simple Queue Service Amazon Kinesis Data Streams(以下、KDS) カート投入リクエスト数に耐えうる性能、及び可用性の観点からKDSを採用しましたが、詳細は以下の記事にまとまっているためご興味のある方はご覧ください。 techblog.zozo.com いよいよ負荷試験 無事製造も終わり、いよいよ負荷試験です。キュー導入によるチューニング等は発生するだろうとある程度身構えてはいたものの、想定以上のトラブル続きでした。 プロダクション環境を使った負荷試験 今回のアーキテクチャでは、キューイングサービスにKDSを採用しています。もちろん単体での性能検証、負荷試験を実施しておりますが、カート機能はECサイトの中核を担う機能であり、リプレイスによりユーザビリティを損なってしまうのは本末転倒です。可能な限りUXを損なわずに最適なキューの並列度を探るには、やはりプロダクション環境での負荷試験が必須だと考えました。 プロダクション環境を使うにあたりできるだけサービスに影響が出ないような工夫もしており、詳細は以下の記事にまとまっているためご興味のある方はご覧ください。 techblog.zozo.com 負荷試験で見つかった課題 ここでは負荷試験で見つかった課題と、解決に向けたアプローチについていくつかご紹介させていただきます。 課題(1)KDSシャードの一部しか使われておらず想定した性能が出ない シャード数増減によるStreamの付け替えや、デプロイ・スケールアウト等で新規にWorkerプロセスが起動するタイミングのレスポンス遅延が目立っており、時間経過とともに遅延が解消されていくという事象が発生しました。 概念図 原因 Kinesis Client Library(以下、KCL)のCustomMetricsを見ていくと、新規Workerアプリケーション起動時にシャードの一部しか使われていませんでした。さらにそこからブレークダウンしていくと、以下のKCLパラメータが起因していることが分かりました。 KCLパラメータ 概要 デフォルト値 maxLeasesForWorker アプリケーションの単一のインスタンスが受け入れるリースの最大数 INT_MAX maxLeasesToStealAtOneTime アプリケーションが同時にスティールを試みるリースの最大数 1 ①最初に立ち上がったWorkerが「maxLeasesForWorker:INT_MAX」により、INT最大数のシャードを紐づけようとしていた ②その後KCLの負荷分散処理により他Workerよりリースをスティールするが、「maxLeasesToStealAtOneTime:1」によりスティール最大数が1のため均等化にも時間が掛かっていた 解消に向けたアプローチ ①maxLeasesForWorkerの見直しにより、最初に起動したWorkerがINT_MAX分のシャード紐づけを行わなくなり、適切なシャード数のみ紐づけを行う ②maxLeasesToStealAtOneTimeの見直しにより、KCL負荷分散処理にて1度にスティールできる数が増えたことで高速な均等化が可能となった 初期起動時に限定されていたのと時間経過とともに解消されていたためはまりました。が、Stream、WorkerがN:N構成、かつKCLパラメータがデフォルト値だったことにより発生した事象でした。負荷試験で頻繁にStreamを変更したり、Pod再起動等を行っていたため早期発見に繋がりました。 課題(2)Javaアプリケーション起動時のコールドスタート対応(JVM暖気) 上記により初期起動時のシャード数による遅延は解消されましたが、Javaアプリケーション起動時の重さも課題となり、コールドスタート対策が必要となりました。 JVMの暖機運転 JVMはJIT(Just In Time)コンパイラによるコンパイル方式のため、都度プログラム実行時にコンパイルが行われます。そのため、起動直後の初回プログラム実行時が特に重く、予めアプリケーションを実行してコンパイル済みにしておく暖機運転が求められます。 解消に向けたアプローチ 今回作成したWorkerはKubernetes上で稼働しております。Kubernetes上で稼働するアプリケーションの暖機方式としてSidecar方式、postStart方式等がありますが、今回はpostStart方式を採用しています。 ①KubernetesのpostStartを使ってコンテナ起動時に自エンドポイントへ暖機用パラメータでリクエストする ②後続APIでは暖機用パラメータのリクエストであれば後続処理を行わない このようなフローにすることでユーザー影響を最小限にしつつ、コールドスタート問題を解消させています。 課題(3)エラーを返しているがカートに商品が入っている テストの中で負荷を上げていくと、レスポンスでエラーを返しているが、実際にはカートに商品が入っているケースが発生しました。 原因 サービス間でポーリングタイムアウトを検出しているものの、Workerがdequeueして後続のデータベースにリクエストを投げていたことが原因でした。 解消に向けたアプローチ 課題解消のためには、各サービス間のタイムアウト値とKDSシャード並列数の組合せを考慮する必要がありました。タイムアウト値の延長やシャード並列数を下げることで本事象は解消できますが、単純な変更ではカート投入リクエストのレイテンシが高くなりユーザビリティに影響が出てしまいます。 ある程度の仮説を立てた上でテストシナリオを再構築し、しきい値の二分探索を行いながら最適なタイムアウト値やシャード並列数を探りました。そうすることでユーザビリティを担保しつつ、課題を解消できました。 いずれもマイクロサービス単体での負荷試験では検知しづらい課題でした。事前準備や夜間帯というコストを踏まえても、プロダクション環境を使うことで得られた物は大きかったです。エンジニアの理想で言えばプロダクションと同等の検証環境がベストですが、オンプレ環境だとコスト的に難しいのも実情です。 そしてリリースへ 度重なる負荷試験で全ての目標値をクリアし、万が一に備えてフラグによる切り戻し制御も準備した上で、カナリアリリースによるプロダクションリリースを行いました。 成果として、2021年元旦の過熱商品と同等のトラフィックのある商品で、大幅なエラー率削減を達成できました。 また、同時にAPM(Datadog)によるダッシュボード化も実施しており、各種数値が可視化されたことでモニタリング精度向上、及び障害発生時の原因切り分けまでの時間短縮も実現できました。ダッシュボートはサービス単体のみではなく、APIからWorkerまでの一連のフローをリクエスト単位で見れるような工夫もしております。 以前は過熱商品販売の度にリアルタイム監視をしており現場がピリピリしていましたが、監視コストが下がることでサービスは勿論、組織文化にも良い影響を与えることができました。 良かった良かった・・・と言いたいところだが リリース後も問題なく安定稼働しており一安心といったところでしたが、ある日リプレイスPhase1の目標値を大きく超える商品の販売により、再度エラーが多発する事態となってしまいました。 エラーの要因 過熱商品の特性の変化(商品展開数の増加など) リプレイスPhase1で想定していた以上のアクセス数 想定以上のアクセス数により参照系処理が増加したことによるデータベースのラッチ競合 上記の対策として、アプリケーション、インフラ両軸で複合的な対策が求められますが、具体例として以下を実施しています。 キューのキャパシティコントロール最適化(シャード並列数の再精査) 高負荷時もシステム全体に波及させず影響を最小化するistioを活用したサーキットブレーカーの導入 データベースのRead/Write分離による負荷軽減 Akamaiなど各種セキュリティソリューションの有効活用 過去のistioを活用したサーキットブレーカーの導入事例は以下の記事にまとまっているためご興味のある方はご覧ください。 techblog.zozo.com 網羅的に想定したつもりではあったものの、その想定を大きく超える事象により障害となってしまいとても悔しい結果となりました。「どこまでやるか」はシステム永遠の課題でありスケジュール・コスト等とのトレードオフです。しかし障害対応によるブラッシュアップによって、より盤石なシステム・サービスになると思っています。 まとめ 【結論】やってよかった 最初に組織変更をしたことで、その後のリプレイスを加速できた。逆に言うとそれぐらいの覚悟は必要 問題を正しく理解する前に解決することを急がない 分かっていてもはまりがちなため、何度も自分に言い聞かせた。超大事! 巨人の肩に乗る 先人達のベストプラクティスには理由がある マイクロサービスは特に負荷試験に時間を割く サービス単体では見つけられなかった課題も、プロダクション環境に組み込んで試験することでリリース前に多くの課題を吸収できた 良いシステムはブラッシュアップによって生まれるもの リリース後も常に問題意識を持つことが大事 将来を見据えた設計は重要だが、先を見過ぎない 先を見過ぎるとあれもこれもとなり結果汎用的になりスピード感も落ちてしまう。大事なのは一点突破できる機能 監視コストが下がることで組織文化にも良い影響を与えることができた 一方、BOTによる過剰アクセス対策については日々ブラッシュアップが求められるため、アプリケーション、インフラ両軸で継続的に取り組んでいく必要がある 最後に 本記事はカートリプレイスPhase1の振り返りとなりますが、カート決済サービスのリプレイスは始まったばかりです。 セールやキャンペーンによる突発的な注文フロースパイク 決済サービスのモノリスからの脱却、マイクロサービス化 今後のZOZOTOWN成長にも耐えうる、正確性、信頼性、安全性を重視したスケールできるカート決済サービス モニタリングの洗練化 スピード感、モダンなメインストリーム技術の導入、etc 上記はいくつかの例ですが、まだまだやりたいことが山積みの状態です。このような課題の解消に向けて、一緒に進めていく仲間を募集しています。ご興味のある方は、以下のリンクから是非ご応募ください。 hrmos.co hrmos.co また、カジュアル面談も随時実施中です。是非ご応募ください。 hrmos.co
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの渡辺です。普段はZOZOMATやZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。 先日私達のチームでは、Argo CDと拡張ツールArgo CD Image Updaterを導入した開発環境のCDリアーキテクトを行いました。本記事では、開発環境のCI/CDリアーキテクト設計とArgo CD Image Updaterの導入手順について紹介します。 目次 はじめに 目次 Argo CDとArgo CD Image Updaterについて Argo CD Image Updater導入前の課題 Argo CD Image Updater導入による開発環境CI/CD設計 導入手順 Argo CD Image UpdaterのECR操作権限設定 IAMRoleの作成とPodへのアタッチ PodのECR認証トークン発行 Argo CD Image Updaterリソースの展開(kustomizeの設定) Argo CD Applicationの設定 動作確認 リアーキテクト前後の比較 まとめ 終わりに Argo CDとArgo CD Image Updaterについて Argo CDは、Kubernetes環境での GitOps を実現するためのCDツールです。Gitリポジトリで管理しているKubernetesマニフェストを監視して、Kubernetesクラスターに適用します。 Argo CD Image UpdaterとはArgo CDの拡張ツールで、デプロイされたコンテナイメージを最新のバージョンに自動更新します。こちらはイメージを管理しているリポジトリを監視して、条件に一致した最新のイメージを検知した場合にKubernetesクラスターのコンテナイメージを更新します。 Argo CD Image Updater導入前の課題 計測プラットフォーム開発本部のプロダクトのインフラ基盤は、EKS on Fargateで運用しており、Skaffoldを用いたデプロイを行っていました。また、GitHubで管理しているアプリケーションのリポジトリにKubernetesマニフェストも含まれていました。 例として、ZOZOMATの開発環境CI/CDアーキテクチャを下図に示します。 以下のような流れになります。 対象ブランチをCircleCIのブランチフィルターの定義に追記する 1.をコミットし、GitHubに変更をpushする CircleCIのイメージビルド用のジョブが発火する skaffold buildを実行し、イメージをビルドしてECRにpushする ビルド時に生成されたイメージタグが記録されたjsonファイル(以下、イメージタグファイルとする)をS3にアップロードする ビルド完了を待ち、CDをトリガーするスクリプトを実行する CircleCIからCodePipelineのソースS3にソースコードをアップロードする CodeBuildがソースとイメージタグファイルをS3から取得し、Skaffoldを使ってapplyする 作業完了後、2のコミットを消す 「イメージタグファイル」について補足します。イメージビルド時にSkaffoldの--file-outputオプションを利用してイメージタグをファイルに記録しS3へ配置します。デプロイ時にS3のイメージタグファイルをダウンロードし、Skaffoldの--build-artifactsオプションでファイルを指定することで適切なタグ情報によるデプロイを実行しています。 このフローには、いくつかの問題点がありました。 各開発者の作業量が多く、デプロイまでに多くの時間を要する デプロイ先の環境を指定してCD用スクリプトを実行するため、オペレーションミスによりステージングや本番環境のCDが実行される 作業手順が複雑でツールも散在しているため、リバースエンジニアリングしづらく、他プロダクトへの横展開も難しい 特に作業開始からデプロイまでの時間の長さがネックでした。 また、計測プラットフォーム開発本部は複数のプロダクトを管理しているため、CI/CDは全て統一されることが望ましい状態と言えます。しかし、現状は各プロダクトで異なる手順となっているため、管理コストも大きな課題となっていました。 Argo CD Image Updater導入による開発環境CI/CD設計 前述の課題を解決するために、まずは開発環境にArgo CDと拡張ツールArgo CD Image Updaterを導入することで、デプロイ時間の短縮を図りました。Argo CD導入後の開発環境のCI/CDアーキテクチャを下図に示します。 ここでは深く言及しませんが、GitOpsではアプリケーションのソースコードとKubernetesマニフェストを分けて管理することが推奨されています。このため、既存のGitHubリポジトリからKubernetesマニフェストを抜き出し、別GitHubリポジトリで管理するよう対応しました。 以下のような流れになります。 開発者が対象ブランチを設定したGitHub Actionsの手動ワークフローを実行する イメージをビルドしてECRにpushする Argo CD Image UpdaterがECRリポジトリのイメージを検知する Argo CDが新しいイメージのPodを自動デプロイする 開発者が行う作業は、任意のタイミングでGitHub Actionsを手動実行するだけです。ブランチを指定するだけで開発環境用のイメージがECRに配置されます。 Argo CD単体では、GitHub上のソースコードを更新する必要があるため、Pushしたイメージタグに更新するロジックが必要になります。そこで、Argo CD Image Updaterを導入し、よりスピーディな開発環境へのデプロイを実現します。レジストリに新しいイメージが追加された際に、自動で新しいイメージをデプロイすることが可能になります。 従来の方法では、ビルドしたイメージがECRに配置されるのを待ってからデプロイを行う必要がありました。待っている間に別の作業をしているとついビルドの完了に気づくのが遅れてしまい、結果的にデプロイも遅れるという悩みがメンバー間でありました。自動デプロイによりこうした悩みも解決できました。 承認フローや厳格なGitOpsを求めない開発環境にこそArgo CD Image Updaterは強力なツールとして開発をサポートしてくれます。 導入手順 ここからは、Argo CD Image Updaterの導入手順について紹介していきます(Argo CDは 公式ドキュメント を参考に導入しました)。 Argo CD Image UpdaterのECR操作権限設定 Argo CD Applicationの設定 Argo CD Image UpdaterのECR操作権限設定 Argo CD Image UpdaterがECRリポジトリのイメージタグ情報を取得するため、認証周りの設定が必要になります。ここでは、認証に必要な各リソースについて説明します。 IAMRoleの作成とPodへのアタッチ KubernetesのServiceAccountでIAM Roleを使用するには、クラスター用のIAM OIDCプロバイダーが必要です。クラスター用のIAM OIDCプロバイダー作成後、CloudFormationでIAMRoleを作成します。 IAMRoleArgoCDImageUpdater : Type : 'AWS::IAM::Role' Properties : RoleName : 'argocd-image-updater' AssumeRolePolicyDocument : !Sub | { "Version" : "2012-10-17" , "Statement" : [ { "Effect" : "Allow" , "Principal" : { "Federated" : "arn:aws:iam::${AWS::AccountId}:oidc-provider/oidc.eks.<リージョン>.amazonaws.com/id/xxxxxxxxxxxxxx" } , "Action" : "sts:AssumeRoleWithWebIdentity" , "Condition" : { "StringEquals" : { "oidc.eks.<リージョン>.amazonaws.com/id/xxxxxxxxxxxxxx:sub" : "system:serviceaccount:<argocd-image-updaterを配置するKubernetes namespace名>:<argocd-image-updaterに設定するServiceAccount名>" } } } ] } Path : '/' Policies : - PolicyDocument : Statement : - Effect : 'Allow' Action : - 'ecr:GetAuthorizationToken' - 'ecr:ListImages' - 'ecr:BatchGetImage' - 'ecr:GetDownloadUrlForLayer' Resource : '*' PolicyName : 'argocd-image-updater-ecr' そして、Argo CD Image UpdaterのServiceAccountに作成したIAMRoleをアタッチします。 apiVersion : v1 kind : ServiceAccount metadata : name : argocd-image-updater annotations : eks.amazonaws.com/role-arn : arn:aws:iam::<AWSアカウントID>:role/argocd-image-updater これでArgo CD Image UpdaterにECRリポジトリのタグを取得する権限を付与できました。 PodのECR認証トークン発行 Argo CD Image Updaterがスクリプトを実行し、クレデンシャルを設定する方法で実装しました。 registries.confにはレジストリやスクリプトを設定し、ecr-login.shにはECRの認証トークンを発行するスクリプトを設定しています。 apiVersion : v1 kind : ConfigMap metadata : name : argocd-image-updater-config data : registries.conf : | registries : - name : ECR api_url : https://<AWSアカウントID>.dkr.ecr.<リージョン>.amazonaws.com prefix : <AWSアカウントID>.dkr.ecr.<リージョン>.amazonaws.com credentials : ext:/app/scripts/ecr-login.sh credsexpire : 10h ecr-login.sh : | #!/bin/sh aws ecr --region <リージョン> get-authorization-token --output text --query 'authorizationData[].authorizationToken' | base64 -d argocd-image-updaterのDeploymentに上記ConfigMapで作成したecr-login.shをマウントします。これにより、Argo CD Image Updaterがecr-login.shを実行できます。 apiVersion : apps/v1 kind : Deployment metadata : name : argocd-image-updater spec : template : spec : containers : - name : argocd-image-updater volumeMounts : - name : ecr-login-script mountPath : /app/scripts volumes : - name : ecr-login-script configMap : defaultMode : 0755 items : - key : ecr-login.sh path : ecr-login.sh name : argocd-image-updater-config optional : true なお、マウントしたConfigMapのファイルの権限はデフォルトで0644なので、defaultMode: 0755と設定しています。デフォルトのままではecr-login.shが実行できず、ECRの認証トークンが発行されないので注意してください。 Argo CD Image Updaterリソースの展開(kustomizeの設定) 私達のチームは環境ごとの差分を管理しやすくするためにkustomizeを用いています。開発環境ディレクトリのkustomization.ymlを以下のとおり設定し、Argo CD Image Updaterと上記で作成したリソースをEKS環境に展開します。 namespace : argocd resources : - https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml patchesStrategicMerge : - configmap.yml # ConfigMap - deployment.yml # Deployment - rbac.yml # ServiceAccount Argo CD Applicationの設定 Argo CD Image Updater管理下に置くApplicationのmetadata.annotationsを以下のとおり設定します。 apiVersion : argoproj.io/v1alpha1 kind : Application metadata : annotations : argocd-image-updater.argoproj.io/write-back-method : argocd argocd-image-updater.argoproj.io/image-list : my-image=<AWSアカウントID>.dkr.ecr.<リージョン>.amazonaws.com/<リポジトリ> argocd-image-updater.argoproj.io/my-image.update-strategy : latest argocd-image-updater.argoproj.io/my-image.ignore-tags : latest 省略 write-back-methodにargocdを設定することで、GitHubのソースコードを更新せずに最新のイメージをデプロイできます。 今回私達のチームはArgo CDの特徴である GitOpsの原則 を崩していますが、承認フローなしでソースコードを変更されることに抵抗感があったからです。 一方、write-back-methodにgitを設定するとGitHub上でデプロイするイメージタグを確認できるメリットがあります。こちらはチームの運用などを考慮して選択すると良いと思います。 update-strategyにはlatestを設定し、最新の作成日でタグを更新するようにしています。また、ignore-tagsにlatestを設定しlatestタグを除外しています。 この他にも柔軟な設定ができるので、詳しくは 公式ドキュメント を参照してください。 動作確認 以下のコマンドで動作を確認できます。 kubectl exec -it < ArgoCD Image Updater Pod > -- argocd-image-updater test < AWSアカウントID > .dkr.ecr. < リージョン > .amazonaws.com/ < リポジトリ > --credentials ext:/app/scripts/ecr-login.sh --registries-conf-path /app/config/registries.conf リアーキテクト前後の比較 リアーキテクト前 リアーキテクト後 デプロイ時間 約8分 3分以内 開発者の作業数 2 1 利用するツール CircleCI, CodePipeline, CodeBuild, Shell Script, Skaffold GitHub Actions, Argo CD, Argo CD Image Updater オペレーションミスの可能性 別環境へデプロイする可能性あり 別環境へデプロイする可能性なし 横展開のしやすさ × ◯ Argo CDの変更チェックをデフォルトの3分に設定しているので、リアーキテクト後のデプロイ時間を3分以内としています。 これまでCodeBuildで行っていたデプロイ処理をArgo CDに移行したことで、約5分デプロイ時間を短縮できました。CodeBuildでの各種ツールのダウンロードに要していた時間を削減できたことが一番の要因です。 また、Argo CD Image Updaterを導入したことで、GitHubへのソース変更ロジックを自前で管理せずに済んだのは横展開しやすい大きな要因になります。同じEKSクラスターで管理する他プロダクトに同様の仕組みを導入するためには、各プロダクトのArgo CD Applicationにてアノテーションを追加するだけで完了します。 まとめ 開発環境にArgo CDとArgo CD Image Updaterを導入したことで、デプロイ時間の短縮と管理コストを抑えることができました。 Argo CDを導入しているが、GitHubのコードを変更せずに自動でイメージのデプロイを行いたい、という開発環境の要求にはArgo CD Image Updaterがピッタリのツールでした。 AWS環境での導入に関しては、公式ドキュメントだけでは難しい部分(特にECR認証トークン発行)もありますが、本記事が導入の手助けになれば幸いです。 なお、Argo CD Image Updater導入を進める途中で v0.12.0 がリリースされました。この中で イメージ変更コミットつきブランチを作成する機能 も追加されたので、今後は承認フローが必要な本番環境での導入も検討していきたいと思います。ブランチ戦略やリリース手順の統一化などやるべきこともたくさんありますが、SREチームのメンバーと協力しながらやり遂げたいと思います。 終わりに 計測プラットフォーム開発本部では、今後も ZOZOFIT 等の新しいサービスのローンチを予定しています。更にスピード感を持った開発が求められますが、このような課題に対して楽しんで取り組み、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
こんにちは、ZOZO CTOブロックの @ikkou です。 WWDC Extendedとは WWDC Extendedは、WWDCのメインセッション(Keynote)をさらに楽しむためのイベントです。これまでのWWDC Extendedはヤフーが単独で開催していましたが、今年のWWDC Extended Tokyo 2022はヤフーに加え、LINEとPayPay、そして私たちZOZOの4社で運営しました。今回のイベントもApple公式の Beyond WWDC にも掲載されています。 yj-meetup.connpass.com 今年のWWDC Extended ZOZOで普段開催しているMeetupやTech Talkでは、ZoomとOBSを用いてYouTube Liveでライブ配信し、質疑応答には Slido を用いています。 WWDC Extendedでは、YouTube LiveとSlidoを用いている点は同じでしたが、ZoomとOBSではなく StreamYard で用いて実施しました。また、休憩中にはMiroを、イベント終了後にはDiscordを用いた交流など、オンラインイベントを盛り上げる複数の取り組みを実施しました。 当日のアーカイブ動画は公開されていませんが、イベントの雰囲気は先行して公開されているYahoo! JAPAN Tech Blogのレポート記事をご覧ください。 techblog.yahoo.co.jp ZOZOからはZOZOTOWNアプリ部の @banjun が登壇し、WWDC参加にあたってZOZOで実施している作戦会議やラボ戦略を惜しげなく披露しました。 speakerdeck.com WWDC22での実際のMiro活用事例や、現地参加した3名のエンジニアによる写真を交えたレポート記事は既に公開しているので、ぜひご覧ください。 techblog.zozo.com 最後に ZOZOでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。また、今後もグループ間のシナジー効果を生かしたイベントを開催していきたいと考えています。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、ML・データ部推薦基盤ブロックの寺崎( @f6wbl6 )と佐藤( @rayuron )です。 ZOZOTOWNのホーム画面は2021年3月にリニューアルされ、「モジュール」と呼ばれる単位で商品が表示されるようになりました。 本記事ではユーザーごとにパーソナライズされたモジュール(以降、パーソナライズモジュール)のロジックやシステム構成、および導入時に実施したA/Bテストの内容と結果をご紹介します。 先に結論から言ってしまいますが、今回のパーソナライズモジュールでは機械学習モデルを使わず、ユーザーの回遊行動を分析した結果を元にしたルールベースのロジックを使用しています。本記事のポイントは大きく以下の3点です。 ルールベースのパーソナライズロジック 機械学習モデル導入を見越したシステム設計 ホーム画面のパーソナライズによる効果 本記事がこれから同様のタスクに取り組む方の参考になれば幸いです。 はじめに ZOZOTOWNのホーム画面とモジュールについて モジュールのパーソナライズ 目的と背景 アルゴリズム選定 機械学習モデル導入コストの問題 機械学習モデル導入による効果が未知 アルゴリズム概要 前提 推薦するブランドを決定するルール 推薦するカテゴリを決定するルール アーキテクチャ概要と処理の流れ ワークフロー管理 データ取得と書き込み 機械学習モデルを導入する場合はどうなるか? A/Bテスト 概要 結果 課題と今後の展望 パーソナライズモジュールを表示されるユーザーが限定されている ルールベースロジックによるパーソナライズに限界がある リアルタイム性に欠く 最後に ZOZOTOWNのホーム画面とモジュールについて まずはじめに、ZOZOTOWNのホーム画面とモジュールについて簡単にご紹介します。 ZOZOTOWNのホーム画面は「すべてのアイテム」・「シューズアイテム」・「コスメアイテム」の3つのモールと「すべて」・「メンズ」・「レディース」・「キッズ」の4つの属性に分かれています。この3モール×4属性の合計12個の組み合わせで表示するモジュールを切り替えています。 ※ 上記は2022年6月10日時点でのZOZOTOWNホーム画面です またモジュールは更新頻度や用途に応じた「定常モジュール」と「運用モジュール」の2種類に大別されます。各モジュールの違いを以下の表にまとめます。 用途 更新頻度 定常モジュール 施策内容に依らず常に同じ訴求をする ほぼ更新なし 運用モジュール ビジネスサイドの要望に応じて訴求内容を変える 1か月あたり数回更新 ファッションECの特性上、トレンドや季節に応じてユーザーへ訴求する商品は常にアップデートし続けなければいけません。またZOZOTOWNへ出店していただいているブランド様やショップ様で販売する新商品を訴求する場合もあるため、ビジネスサイドの担当者が月に数回運用モジュールを企画し、開発サイドでリリースしています。 モジュールのパーソナライズ 目的と背景 ホーム画面のモジュールをパーソナライズする目的は複数ありますが、 ZOZOTOWNでの購入経験の有無 という観点では以下の2つに大別できます。 新規購入者の増大 既存購入者の来訪頻度・購入頻度の増大 1点目は「ZOZOTOWNに訪れたことがあるが、まだ商品購入に至っていない」というユーザーを想定しています。ZOZOTOWNに訪れてはいるものの商品を購入していない様々な理由があると思いますが、ユーザーの嗜好に合った商品を訴求できていないような場合は適切なパーソナライズにより購入に繋げられると考えられます。ただ、こうしたユーザーの多くは会員登録をしていないため、ZOZOTOWN上での回遊行動を捕捉しにくいという難しさがあります。 2点目は既存顧客に対してより良いコンテンツを提供し、ZOZOTOWNで再度購入してもらうことを意図しています。ユーザーが興味を持ちそうな商品群をホーム画面で訴求できれば来訪頻度が増え、結果として購入頻度も増加すると考えられます。 どちらもZOZOTOWNの成長には必要な切り口ですが、今回は 回遊行動ログの集めやすさ と 既存顧客へのパーソナライズによる効果の大きさ から、2点目に挙げた「既存購入者の来訪頻度・購入頻度の増大」を目的としてパーソナライズを進めることとしました。 アルゴリズム選定 パーソナライズアルゴリズムを構築する上で機械学習モデルを使用するか、ルールベースを使用するかを検討し、以下2点の理由からルールベースのアルゴリズムを採用しました。 機械学習モデル導入コストの問題 機械学習モデル導入による効果が未知 機械学習モデル導入コストの問題 機械学習モデルを導入する場合、ルールベースと比べてシステムアーキテクチャが複雑化することに加え、機械学習モデルに関連する様々な資産の導入・管理コストが発生します。また、機械学習モデルが正しくワークしていることを担保するためのデータドリフト検知やモデルメトリクスのモニタリングなど、 システムに機械学習を導入する際には様々なコストを支払う ことになります。 今回はベースラインとなるモデルをリリースするにあたってできるだけコストをかけないこと、また既存アーキテクチャの複雑化を避けることを優先しました。 機械学習モデル導入による効果が未知 今回が初めてのパーソナライズモジュールの導入となるため、当然ですがパーソナライズモジュールの効果はリリースしてみないと分かりません。つまり、 パーソナライズモジュールのリリースによる効果を最適化するためのオフライン評価の設計 が難しい状況でした。 そのため今回は機械学習モデルを最適化するための指標に時間を費やすのではなく、ヒューリスティックに設計したルールでベースとなるパーソナライズの効果を測ることとしました。 アルゴリズム概要 前提 今回のパーソナライズモジュールでは開発スピードと売り上げへの影響の大きさという観点から、訴求対象について以下の前提を置いています。 ZOZOTOWN会員かつお気に入りブランドを持つユーザー 直近で何らかの回遊行動があるユーザー ZOZOTOWNでは以下の図のように特定のブランドをお気に入り登録する機能があります。今回のパーソナライズでは 開発スピードを優先して推薦ブランドリストを一から作ることはせず、このお気に入り登録されたブランドから商品を推薦 します。 パーソナライズのアルゴリズムを考えるにあたり、以下の2点を検討の軸にしました。 推薦するブランド 推薦するカテゴリ いずれも商品の閲覧や購買行動といったImplicit Feedbackを使って分析を行い、「 ユーザーが直近で閲覧している商品カテゴリほど購入に結びつきやすい 」というシンプルな仮説のもとルールを策定しました。 推薦するブランドを決定するルール 前述した通り、今回のモジュールではユーザーのお気に入りブランドの中からブランド群を選択します。お気に入り登録しているブランドの数は人によってまちまちで、登録数が1つだけのユーザーもいれば数十個登録しているユーザーもいます。そこでまずは、今回設ける条件によって推薦対象ユーザー数が大きく減ることを防ぐためにユーザー毎のお気に入りブランドの登録数の分布を概観しました。 その後お気に入りブランドの中でも、ユーザーにとって興味があるブランド群のみを選別するための条件を作成することを考えました。今回は購入があったアイテムの最初の閲覧日から購入日までの期間を分析し、 ユーザー毎に直近n日間で商品閲覧があるブランド群を推薦する というルールを作成しています。 推薦するカテゴリを決定するルール 推薦するカテゴリは以下の二軸で決めました。 除外するカテゴリの選定 推薦するカテゴリの優先順位付け 今回のモジュールで推薦可能なカテゴリは300個弱あったため、ここから推薦候補となる一覧を決定します。購入済みのカテゴリの商品が再度推薦される状況を避けることと、多様なカテゴリを訴求することを目的として、「直近で購入済みのカテゴリは訴求しない」という方針を決めました。 この方針のもと、各ユーザーの商品カテゴリごとの購入周期を分析し、 購入履歴のあるカテゴリのモジュールはm日間表示しない というルールを作成しました。 次に、候補となったカテゴリのうち、どのカテゴリをユーザーへ訴求するかを決めます。今回は前述した「直近のユーザーが多く閲覧している商品カテゴリほど購入に結びつきやすい」という仮説に基づき、以下の流れで推薦カテゴリを決定しています。 カテゴリの閲覧回数に時系列の重みを掛け合わせたものを評価値として算出 その評価値順に推薦するカテゴリを決定 アーキテクチャ概要と処理の流れ ここからはモジュールのパーソナライズを行うためのシステム構成と処理の流れをご紹介します。前述の通り今回のパーソナライズでは機械学習モデルを使っていませんが、将来的に機械学習モデルを導入することを見越したシステム構成を意識しています。 モジュールAPIはホーム画面リニューアル時に導入されたもので、前述したモールやユーザーの属性に応じてホーム画面に表示するモジュールの設定情報を返却しています。この構成のうち、パーソナライズモジュールの開発に合わせて導入された要素について説明します。 ワークフロー管理 パーソナライズを行うための一連のワークフローはVertex AI Pipelinesで管理しており、このワークフローが毎時間実行される構成となっています。Vertex AI Pipelinesは今や弊社の機械学習パイプライン実行基盤であり、MLをプロダクトに載せて運用に携わる全てのチームが利用していると言っても過言ではありません。Vertex AI Pipelinesに関する知見は弊社テックブログで公開されていますので、こちらも是非参照ください。 techblog.zozo.com techblog.zozo.com Vertex AI Pipelinesによるワークフロー構築に際し、 こちらの記事で今後の展望として挙げられていた パイプラインテンプレートというものを利用しています。パイプラインテンプレートはGitHubの テンプレートリポジトリ と呼ばれる機能を利用したもので、Vertex AI Pipelinesの実行・スケジュール登録・CI/CD・実行監視等をテンプレート化しています。例えばパイプラインのRunとスケジュール登録は以下のコマンドで実行できます。 # 単発のRunを実行 $ poetry run python pipelines/sample-features run-pipeline --pipeline-name sample-features --env dev # スケジュール実行の登録 $ poetry run python pipelines/sample-features schedule-pipeline --pipeline-name sample-features --env dev またパイプライン実行状態の監視や 公式ドキュメントに記載されている定期実行の仕組み まで、パイプラインテンプレートで簡単にデプロイ・実行できるようになっています。このパイプラインテンプレートによりワークフローのデバッグやデプロイのサイクルを高速に回すことができました。パイプラインテンプレートのより詳細な機能や具体的な実装については別途テックブログで公開できればと思います。 パイプラインは毎時間実行してユーザーの情報を差分更新します。この更新処理バッチが落ちた場合、その時点で再実行等はせずに1日1回の全件更新するバッチを別で実行してリカバリする構成となっています。 データ取得と書き込み パーソナライズに使用するユーザーログは弊社のリアルタイムデータ基盤であるBigQueryから取得し、集計した結果をGoogle Cloud Storage(GCS)へと出力します。GCSへのデータはデータサイズ節約の観点からparquet形式で出力しており、このデータをDataflowで読み込んでBigtableへ投入しています。実際に投入されるデータは概ね以下のような形式となっています。 002-SS3 r:h @ 2022/06/09-03:30:00.000000 "{\"id\": \"075-A20\", \"field_A\": \"xxx\", \"personalize_category\": [{\"id\": 1234, \"title\": \"カテゴリA\", \"category_url\": \"category_A\"}, {\"id\": 5678, \"title\": \"カテゴリB\", \"category_url\": \"category_B\"}]}" ---------------------------------------- 075-A20 r:h @ 2022/06/09-04:30:00.000000 "{\"id\": \"002-SS3\", \"field_A\": \"yyy,zzz\", \"personalize_category\": []}" 上記はユーザーのIDがそれぞれ 002-SS3 と 075-A20 のデータを想定したダミーデータです。1つのセルにまとめて文字列形式でパーソナライズ用のデータを格納しており、モジュール返却用のAPIにリクエストが来るとこの文字列をパースしてパーソナライズされた情報を取得する形にしています。 パイプライン側の処理は全て自前で実装していますが、2022年6月現在ではBigQueryやDataflowにジョブを投げるといった汎用的な処理はGCPの公式コンポーネントとして提供されています。他にも様々なコンポーネントが提供されていますので、これから実装する際には用途に適したコンポーネントがないかドキュメントを一読することをオススメします。 cloud.google.com cloud.google.com 機械学習モデルを導入する場合はどうなるか? 今回はルールベースでのパーソナライズのためBigQueryでの集計で事足りていますが、仮に今回の推論バッチで機械学習モデルでのパーソナライズを行う場合のアーキテクチャを以下に示します。 変更点はBigQueryとGCSの処理の間に機械学習モデルによる予測が入るだけで、データの入出力方法に変更はありません。今回BigQueryからGCSへ一度データを配置してDataflowでの処理を行う構成としているのは、こうしたロジックの追加・変更に柔軟に対応する意図があります。 当然、機械学習モデルを再学習・デプロイするための学習パイプラインやCI/CDの構築は別途必要になりますが、推論パイプラインの構成を大きく変更する必要がないのは大きなメリットになります。 A/Bテスト 概要 パーソナライズモジュールの効果を評価するために、リリース時にパーソナライズ対象のユーザーに対して約1か月間A/Bテストを行いました。今回のA/Bテストではテスト対象となるユーザーがControl群とTreatment群で1:1の振り分けとなるように設定しています。Treatment群に対しては以下画像のようにパーソナライズモジュールが表示されます。 モジュールの並び順に関してはビジネス的な要望もあり、定常モジュールの間に挟む形で決め打ちしています。 また、パーソナライズモジュールは推薦するカテゴリに応じてモジュールのタイトル・表示される商品・「すべて見る」を押下した際の遷移先が変わるようになっています。 結果 A/Bテストの結果サマリを以下に示します。 指標 備考 結果(T/C比・T/C差) ZOZOTOWN全体の受注金額 GMVと代替となる指標 100.4 (%) ホーム画面経由の受注金額 ホーム画面に表示されている商品をクリックしたセッション内での受注金額 104.6 (%) ホーム画面ランディングセッション直帰率 ホーム画面の直帰セッション数÷ランディングセッション数 -0.1 (pt) 訪問者1人あたりホーム画面クリックセッション率 ホーム画面でのクリックセッション数÷全セッション数 +0.8 (pt) ZOZOTOWN全体の受注金額で有意差は見られませんでしたが、ホーム画面経由の受注金額は増加していました。またホーム画面経由の受注金額をKPIツリーに分解してみると、ホーム画面全体でのクリック率向上に伴うクリックセッション数の増加が影響していることがわかります。 またモジュール全体の指標を見ると、パーソナライズモジュールに引っ張られる形で各種指標が増加していることが確認できます。 指標 T/C比率 モジュールインプレッション数 103.4 (%) モジュールインプレッションUU数 107.8 (%) 商品クリック数 108.5 (%) 商品クリックUU数 122.7 (%) 「すべて見る」クリック数 106.7 (%) 「すべて見る」クリックUU数 126.5 (%) 商品CTR(PVベース) 104.9 (%) 商品CTR(UUベース) 113.9 (%) 「すべて見る」CTR(PVベース) 103.2 (%) 「すべて見る」CTR(UUベース) 117.4 (%) 結論として、パーソナライズモジュールの導入によりZOZOTOWN全体での指標改善までは及んでいませんが、少なくともホーム画面ではユーザーの関心を引けているものと考えられます。 課題と今後の展望 ホーム画面へのパーソナライズモジュール導入により、当初目的としていた「既存購入者の来訪頻度・購入頻度の増大」に一定の効果があるというポジティブな結果となりました。一方で、以下のような課題もあります。 パーソナライズモジュールを表示されるユーザーが限定されている ルールベースロジックによるパーソナライズに限界がある リアルタイム性に欠く パーソナライズモジュールを表示されるユーザーが限定されている 現在パーソナライズモジュールが表示されるユーザーはZOZOTOWN会員かつお気に入りブランド登録しているユーザーであり、限定されたユーザーセグメントへの訴求にとどまっています。今回はパーソナライズモジュールの第一歩としてこのような限定されたセグメントへの訴求としましたが、今後はより多くのユーザーにパーソナライズした商品をモジュールという形で届けたいと考えています。 ルールベースロジックによるパーソナライズに限界がある 仮にユーザーセグメントを広げた場合、ルールベースのロジックではパーソナライズできる商品に限界があります。今回作成したルールで言えば「ユーザーがお気に入りブランドに追加しているブランド」に限定した商品を訴求しており、「ユーザーが興味を持ちそうなまだ見ぬブランド」という軸では訴求できません。 ホーム画面という多くのユーザーが最初に訪れる場所だからこそ、見知っているブランドだけではない新しい出会いを機械学習モデルで提供していきたいと考えています。 リアルタイム性に欠く 現状のシステム構成では1時間に1回推論パイプラインが実行されてユーザーのパーソナライズ情報を更新しています。 ユーザーの閲覧に基づいて商品を訴求するのであれば、ユーザーが商品を閲覧してホーム画面へ戻る度、閲覧履歴に基づいた推薦商品が表示されていることが好ましいと考えられます。 一方で閲覧履歴に基づいて表示される商品がリアルタイムで変わっていると、「後でこの商品を見よう」と思ってブラウザバックしてもその商品はホーム画面で表示されていない、という状態となることも考えられます。 ユーザー体験を考えると表示される商品がリアルタイムで入れ替わることは必ずしも良いと一概には言えないため、ここの塩梅を今後探っていきたいと考えています。 最後に 本記事ではZOZOTOWNホーム画面へ導入したパーソナライズモジュールを紹介しました。今後の展望に挙げた項目については絶賛進行中で、これからもZOZOTOWNのパーソナライズはどんどん進んでいきます。 一緒にZOZOTOWNのパーソナライズ化を進めることに興味のある方は以下リンクから是非ご応募ください! hrmos.co hrmos.co
こんにちは、ZOZOTOWNアプリ部の @inokinn です。 日本時間の6月7日から11日にかけて WWDC22 が開催されました。 今年のハイライトは、iOS 16でのロック画面のアップデートをはじめ、WeatherKitやSwift Charts、Passkeysなどの、数多くの新機能の発表だったかと思います。 今年は去年と一昨年に続いてのオンライン開催に加え、抽選に当選すれば現地であるApple Parkでのパブリックビューイングにも参加できました。そして、なんと幸運にもZOZOからも3名が当選し、現地に赴きました! 本記事では、WWDC22でZOZOのiOSアプリ開発メンバーが取り組んだことを紹介します。また、ラボでAppleのスタッフから得られたフィードバックや、海外出張したメンバーによる現地レポートも可能な範囲で公開します。是非最後までご覧ください。 WWDCについて 現地で楽しむWWDC22 6月5日 - イベント前日 6月6日 - イベント当日 ライブビューイング Meet with Teams オンラインで楽しむWWDC22 Digital Lounges Challenges Labs & Sessions Design Lab × FAANS 投稿フローに対するフィードバック 着用アイテムを登録する機能に対するフィードバック 通知の活用 Design Labを利用した感想 WeatherKitはWEARを拡張するか まとめ さいごに WWDCについて WWDC(Worldwide Developers Conference)は、Appleが年に1度開催している開発者向けのカンファレンスです。今年は2019年以来、3年ぶりに現地でもイベントが開催されたので、当選したメンバーは業務の一環として現地参加しました。ZOZOの開発部門では、海外カンファレンスを含むセミナー・カンファレンス参加支援制度が用意されています。 現地で楽しむWWDC22 こんにちは、ZOZOTOWNアプリ部の小松、荻野とWEAR部の坂倉です。 コロナ禍になってから完全オンライン開催のWWDCでしたが、なんと今年は一部オフライン開催もありました。案内としては、6月6日のKeynoteやPlatforms State of the UnionなどをApple Parkでライブビューイングできるとのことでした。 現地参加のスケジュールは以下の通りです。 時間(PST) コンテンツ 場所 7:00 AM チェックイン Apple Park Visitor Center 8:00 AM 朝食 Apple Park - Caffè Macs 10:00 AM Keynote Apple Park 12:00 PM 昼食 Apple Park - Caffè Macs 1:00 PM Platforms State of the Union Apple Park 2:30 PM Meet the Teams Apple Park - Caffè Macs 4:30 PM Apple Design Award Apple Park また、今年は参加記念品として以下のものをもらえました。個人的にお気に入りなのは、Swiftロゴの入ったトートバックです。MacBook Proも入るサイズで愛用しています。 6月5日 - イベント前日 6月5日はイベント前日ではありますが、アーリーチェックインが可能になっており、先行でDeveloper Centerのオープンハウスも開催されていました。 会場はお祭りさながらの雰囲気で非常に盛り上がっていました。 早々にチェックインを済ませ、Developer Centerへ。 Developer Centerはエンジニアやデザイナーが交流したり学ぶための施設とのことですが、Apple Storeさながらのお洒落な部屋がいくつも用意されていました。 実際にコードを書いて学べるワークショップ用の部屋や、壁に巨大なホワイトボードが設置されたUI設計用の部屋、製品設計用の部屋などがありました。あまりの綺麗さに一度はここで仕事をしてみたいと思いました(ちなみに各部屋はこれまでのmacOSの名前が付けられています)。 一番驚きだったのが、Big Surと呼ばれる放送スタジオです。 そこはまるで映画館のような空間になっており、小さな文字もしっかり読める高解像度の超巨大モニターや、色々な角度から音を鳴らすことができるサウンドシステムには度肝を抜かれました。 放送スタジオということで、ここでAppleが観客の前でKeynoteを催すことはないのかもしれませんが、この空間で一度Keynoteを観てみたいと思いました。 6月6日 - イベント当日 ライブビューイング 6月6日、イベント当日。朝の入場待ちの列ではコーヒーが配られ、これから初公開のApple Parkへと足を踏み入れる人たちの熱気に包まれていました。 Appleのスタッフに歓迎されながら道を進むと、ついにApple Parkが姿を現します。汚れひとつない全面ガラス張りの外観は息を呑む美しさでした。 Apple Parkの中に入って朝食を受け取り、大きなスクリーンが置かれている広場であるCaffè Macsへ進みます。 メインスクリーンの正面の部分は窓ガラスが可動式になっており、屋内・屋外どちらからでもスクリーンが見られるように開かれていました。こんなに大きな窓ガラスが本当に動くのか…と規格外の大きさに圧倒されてしまいます。 Keynoteが始まるまでの時間ではAppleのスタッフがいたるところで踊っていたりとお祭りムードでした。 画面が暗転し、ついにKeynoteのライブビューイングが始まると思いきや、そこに現れたのはなんとティム・クックとクレイグ・フェデリギ! 座っていた参加者は全員立ち上がり、先ほどまでの熱気がより一層強まりました。 Keynote本編では、iOS 16や新型MacBook Airをはじめ、さまざまな新機能の発表がありました。新しい機能が発表されるたびに起こる拍手や歓声で喜びを共有できるのは、現地ならではの良さだなぁと感じました。 ちなみに、Keynoteのライブビューイングの中で最大の盛り上がりを見せたのは、超高速で移動するフェデリギ氏がスーパースローで髪を掻き上げているシーンでした(笑)。 Keynoteが大盛況の中終わると、お昼休憩を挟んでPlatforms State of the Unionが始まります。 State of the UnionではXcode CloudやSwiftUI、iOS 16のアップデートに伴うWidgetKitなど、新機能の紹介が行われました。ここでもKeynoteと同様に、リアクションが会場全体から漏れてきます。 ライブビューイングが終了した後は、Apple Park内の公開されているエリアを探索しました。 Appleのスタッフによると、3階からのCaffè Macsの景色がApple Parkで1番美しいスポットとのことでした。3階まで登ると円形になっている建物の全体を見渡すことができ、1番というのも納得の光景が広がっていました。 また、探索中にAppleのスタッフからAppleの環境や運動に対する取り組みに関する話を聞くことができました。 Apple Parkは建物部分が敷地全体の20%しかないらしく、緑との共生を大切にしているとのことでした。また、こうして緑を多くすることによって積極的に外に出るような環境を作り、他のチームとの交流や歩きながらのミーティングを促進しているらしいです。 Apple Park内にはバスケットボールのコートやテニスコート、サッカー場などがあり、日本では絶対にできないような土地の使い方を見ると、さすがアメリカだなぁと感じます。 Meet with Teams Platforms State of the Unionが終わると、Caffè MacsではMeet with Teamsというイベントが行われていました。ここではさまざまな分野のAppleのエンジニアが常駐しており、気軽に雑談ができました。実際に、自分はXcodeエンジニアとFitnessエンジニア、UIKitエンジニアと話しました。 Xcodeエンジニアとは、Xcode AppはXcodeで作られているのか、Appleのエンジニアだったら常に最新のMacに交換し放題なのかといった話をしました。こういったカジュアルな雑談ができるのはラボとの大きな違いだった思います。専門的な話になるとラボを勧められるといった雰囲気でした。 オンラインで楽しむWWDC22 海外出張した3名以外の、ほとんどのメンバーはオンラインでWWDC22に参加しました。開催期間中、ZOZOTOWNアプリ部のメンバーは、現地の時間に合わせて日本時間2:00〜11:00を勤務時間としていました。 チーム内で情報を共有するため、毎日1回、ビデオ通話でのミーティングを行っていました。また、分担して視聴したセッションのサマリや、ラボやラウンジで得た情報はMiroに一元管理して共有していました。色々な情報が共有されたため、WWDC22の全日程が終了した後にはボードの様子は以下のようになっていました。 このやり方は去年のノウハウを活かしたもので、下記のWWDC21参加レポートにより詳しく公開しているので、よろしければこちらもご覧ください。 techblog.zozo.com Digital Lounges WWDC22では、去年に引き続きオンラインならではの取り組みが実施されていました。 「Digital Lounges」はその1つで、Slackを用いて、Appleのエンジニアやデザイナーにチャットで質問することが出来ました。 質問以外にも、ラウンジ上では「Trivia Night」というイベントも開催されていました。これは、Appleプラットフォームに関するトリビアクイズが出題されるというものです。プログラムの実行結果に関する問題や「Apple社の祝日はいつ?」といったApple愛を試されるクイズも出題されました。 Challenges 「Challenges」も、オンラインならではのコンテンツでした。こちらは、いくつかの提示されたお題の中から好きなものに取り組むことが出来るコンテンツです。取り組んだ結果をラウンジでAppleのスタッフや世界中のiOSアプリ開発者たちに公開して意見をもらったり、SNSで共有して盛り上がったりすることが出来ます。お題にはバリエーション豊かなものが毎日追加されるので、興味のあるものに挑むのが楽しかったです。 私も「 Pixel perfect design 」という、アプリのアイコンをピクセルアートで表現するお題に挑戦し、弊社アプリ「ZOZOTOWN」のアイコンを描いてみました! Digital Loungesで見ていただいたところ、お褒めの言葉と同時に「文字のラインを2pxではなく1pxで表現してみては?」というフィードバックをいただけました。得られたフィードバックを反映し、ピクセルアートをこのように改善することが出来ました。 TAKE 1 TAKE 2 TAKE 3 この作品はApple公式サイトの WWDC22 Daily Digest: Wednesday にも取り上げていただけました! Source: https://developer.apple.com/news/?id=pcfa7nkx Labs & Sessions 今年も、ラボではAppleのスタッフからフィードバックを得ることが出来ました。WWDC22に参加したZOZOメンバーから、それぞれが参加したラボで得たフィードバックや、視聴したセッションで得た内容を一部紹介します。 Design Lab × FAANS こんにちは、FAANS部iOSチームの中島です。Design LabでAppleのデザイナーにFAANSアプリのフィードバックを頂いたので、紹介いたします。 投稿フローに対するフィードバック FAANSはWEARと同様にコーディネートを投稿でき、投稿写真の明るさ調整の機能があります。スライダーで「明るさ、コントラスト、彩度」の数値を変更するのですが、この機能に対し「プリセットを提供し、ユーザーが簡易的に明るさを調整できるようにしてはどうか」というフィードバックをいただきました。投稿フローにかかる時間の短縮方法を検討していましたので、今後の改善の参考にしたいと思います。細かいフィードバックをいただいたものの、コーディネート投稿フロー全体として完成度が高い、といっていただけたので今後の開発の励みになりました。 着用アイテムを登録する機能に対するフィードバック 投稿するコーディネートに着用アイテムを紐付けする機能があります。具体的にブランドをどのように選べばいいのか、という質問を受けました。品番/バーコードでの登録、お気に入り(クローゼット)からの登録、カテゴリ、カラー、ブランドを入力して該当の商品を一覧から探して登録、の3種類の方法があります。選択肢が多いことはユーザーを迷わせる原因になると思いましたので改善していきたいです。 通知の活用 FAANSはショップスタッフが主なユーザーです。コーディネート投稿機能に加え、在庫取り置きの機能があります。今のUIだとコーディネート投稿の結果確認がメインになっており、店舗取り置きの導線が少しわかりにくいという問題点に対し、どのように思うか質問しました。店舗取り置きがある場合、見逃さないようにする必要があるのでアイコンに赤い丸をつける等、通知という形でわかれば問題ないという回答をいただきました。 Design Labを利用した感想 FAANSアプリはWEARやZOZOTOWNとも連携する部分があり、英語での説明も相まって、そもそもどのようなアプリなのか説明するのが難しかったです。ユーザーが限られているというのはあるのですが、対象のユーザーでない人でも直感的に理解、操作できるように改善していきたいです。Design Labを利用したのは初めてでしたが、とても有意義なラボでしたので来年も利用したいです。 WeatherKitはWEARを拡張するか こんにちは、WEAR部iOSブロックのしょうごです。個人的にファッションと天気の相関性について、以前より注目をしていました。 なぜなら、冷夏や暖冬といった異常気象の季節には、季節物の販売不振に陥るケースがあります。そのため、ファッションコーディネートアプリのWEARとしても、ファッション業界の動向は注視する必要があると思います。そこで、異常気象によるWEARへの悪影響を想定した場合、アパレル商品の流通量の低下の影響から、ユーザーログイン頻度の低下などは考えられると思います。故に、異常気象の影響を少しでも緩和できる仕組みが備わっていれば尚良いと思っており、以下の様な事を考えていました。 WEAR自体が天気情報をユーザーにPUSH通知を用いて知らせ、WEARアプリに誘導する 多くの人々は外出する前に天気予報を確認するため、アプリのユーザーにとってもメリットがある アプリを開いた後ユーザーの位置情報をもとに、気温や湿度情報などからよく使われる服をレコメンドする 結果として、人々のファッションの悩みを解決するというWEARのミッションも果たせるかもしれない 今回発表のあった内容によるとWeatherKitは、非常に詳細な気象データを取得できるようです。詳細な気象データを取得可能な理由として、高解像度の気象モデルと機械学習および予測アルゴリズムを使用し導き出しているとセッション中で説明がありました。取得できる情報の詳細は Meet WeatherKit - WWDC22 をご参照下さい。 読者の皆さんはWeatherKitに関してどの様な印象をお持ちになりましたか? 技術は使い方次第で可能性は無限大です。WEARに関して考えてみると、個人的にWeatherKitとの親和性は高いと考えています。なぜなら、WeatherKitの精度次第では、天気情報をベースとした新たなファッションの提案が可能になると考えたからです。また、既存のWEARを拡張してユーザーとのタッチポイントを増やす事ができるというシナジーも期待できます。今後WEARならではの新たなサービスを生み出す事も、可能かもしれません。そして、コーディネートと天気の親和性が高いファッション業界に、ZOZOらしくテクノロジーの力を用いて一石を投じる可能性もあると考えています。 まとめ 本記事では、WWDC22の参加レポートをお伝えしました。 海外出張で現地参加したメンバーも、オンラインでリアルタイムに情報をキャッチアップしたメンバーも、それぞれに実りあるイベントでした。今回得られた知見やフィードバックをもとに、より良いサービスの向上に努めていきたいと思います! さいごに ZOZOでは、一緒にモダンなサービス作りをしてくれる仲間を募集しています。ご興味のある方は、以下のリンクから是非ご応募ください! corp.zozo.com hrmos.co hrmos.co hrmos.co
はじめに こんにちは、検索基盤部 検索基盤ブロックの可児( @KanixT )とSRE部 ECプラットフォーム基盤SREブロックの大澤です。 本記事では、ZOZOTOWNの商品検索で利用しているElasticsearchをバージョンアップした知見と、その際に実施した検索基盤の改善についてご紹介します。 目次 はじめに 目次 背景 バージョンアップの流れ 主な作業 変更箇所の調査 新バージョンのMappingやQueryなどの調査 Deprecation logsが有効になっていることの確認 バージョン7.16.0でタイプ(type)を利用 Javaクライアント LTRプラグインのバージョンアップにともなうJavaのバージョンアップ 特徴量キャッシュの機能がマージされた Javaクラスファイルのバージョン確認方法 Elasticsearchクラスタのコード管理化 IaC方法の選択 TerraformによるIaC化 検証環境での負荷試験 負荷試験の実施方法 インスタンスタイプ検証結果 最大負荷時におけるバージョンアップ前後の比較結果 別クラスタに新しいバージョンのElasticsearchを構築 新旧の両クラスタに対してのインデクシング 各種サービスの参照を旧クラスタから新クラスタへ切替え 旧クラスタの削除 Slowlog 最後に 背景 ZOZOTOWNでは商品の検索エンジンとして、Elastic社が提供するElastic Cloudを利用しています。公式サポートの恩恵を受けるためElasticsearchの EOL に気を遣う必要がありました。Elasticプロダクトのサポート期限は、一般公開日(GA)から18か月と定義されており、弊社で利用しているElasticsearchの期限も迫っていました。ZOZOで実施したバージョンアップ、およびバージョンアップのタイミングに合わせて実施した検索基盤の改善について、知見をご紹介します。 なお現在は8.xがリリースされていますが、作業当時の最新は7.xだったため7.10.xからのマイナーバージョンアップについての知見となります。 バージョンアップの流れ Elasticsearchをバージョンアップするタイミングに合わせて、LTRプラグインの独自ビルド廃止やクラスタのコード管理化など、以前からチーム内で課題感のあった点も改善しています。作業は以下の流れで進めました。 なおバージョンアップに関して、当初は Rolling upgrade による更新を検討していました。しかし検索機能で利用しているindexはドキュメントの更新が常時動いており、Rolling Upgradeで失敗した際にデータの復旧が難しくなり、リスクが高いと判断しました。そのため別クラスタに新しいバージョンのElasticsearchを構築し、切替えを行う方針を採用しました。 主な作業 変更箇所の調査 新バージョンのMappingやQueryなどの調査 Javaクライアント LTRプラグインのバージョンアップにともなうJavaのバージョンアップ Elasticsearchクラスタのコード管理化 IaC方法の選択 TerraformによるIaC化 検証環境での負荷試験 負荷試験の実施方法 インスタンスタイプ検証結果 サービスイン試験結果 本番環境の構築 別クラスタに新しいバージョンのElasticsearchを構築 新旧の両クラスタに対してのインデクシング 各種サービスの参照を旧クラスタから新クラスタへ切替え 旧クラスタの削除 変更箇所の調査 新バージョンのMappingやQueryなどの調査 バージョンアップの事前準備としてまずは Migration guide を確認し、利用予定の新バージョンまでにリリースされた機能でMappingやQueryに影響がある変更を一通り確認しました。 次にアップデートターゲットとなるバージョンのElasticsearchを新クラスタに検証目的で構築しました。その環境で現在動作している検索クエリとインデキシングを実行します。Deprecation logsを有効にすると非推奨のElasticsearchの機能を確認できるため、その方法についてご紹介します。 なお、今回のバージョンアップでは検索クエリとインデクシングの両方でクエリ修正は1件もありませんでした。 Deprecation logsが有効になっていることの確認 Kibana Dev Toolsを使用して下記リクエストを実行し、Deprecation logsの設定を確認します。詳細は Deprecation logs の公式ページをご覧ください。 GET /_cluster/settings?include_defaults&filter_path=defaults.cluster.deprecation_indexing 実行結果は次のようになり、 "deprecation_indexing.enabled" : "true" の場合に非推奨のログが出力されます。 { "defaults" : { "cluster" : { "deprecation_indexing" : { "enabled" : "true", "x_opaque_id_used" : { "enabled" : "true" } } } } } Deprecation logsの有効が確認できましたので試しにログを出力し、出力内容を確認します。 非推奨のログメッセージを確認するため、Elasticsearch 7.16.0の環境にて、バージョン7.0で廃止された タイプ(type) を利用します。 バージョン7.16.0でタイプ(type)を利用 次のPUTクエリを実行し、廃止されたタイプ(type)を利用します。 PUT /corp/employee/1 { "first_name" : "hakoneko", "last_name" : "max", "age" : 25 } 実行結果のレスポンスはこちらです。タイプ(type)を廃止した旨のワーニングが表示されました。 #! [types removal] Specifying types in document index requests is deprecated, use the typeless endpoints instead (/{index}/_doc/{id}, /{index}/_doc, or /{index}/_create/{id}). { "_index" : "corp", "_type" : "employee", "_id" : "1", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "_seq_no" : 0, "_primary_term" : 1 } 次にDeprecation logに出力された内容を確認します。ログを検索するクエリはこちらです。 GET .logs-deprecation.elasticsearch-default/_search { "size": 1, "sort": [ { "@timestamp": { "order": "desc" } } ] } 検索結果のレスポンスはこちらです。 { "_index" : ".ds-.logs-deprecation.elasticsearch-default-2022.05.19-000006", "_type" : "_doc", "_id" : "tZ9Q8YAB8Tww7jHGnFO2", "_score" : null, "_source" : { "event.dataset" : "deprecation.elasticsearch", "@timestamp" : "2022-05-23T14:27:11,423Z", "log.level" : "CRITICAL", "log.logger" : "org.elasticsearch.deprecation.rest.action.document.RestIndexAction", "elasticsearch.cluster.name" : "es-docker-cluster", "elasticsearch.cluster.uuid" : "********************", "elasticsearch.node.id" : ""********************",", "elasticsearch.node.name" : "elasticsearch", "trace.id" : "", "message" : "[types removal] Specifying types in document index requests is deprecated, use the typeless endpoints instead (/{index}/_doc/{id}, /{index}/_doc, or /{index}/_create/{id}).", "data_stream.type" : "logs", "data_stream.dataset" : "deprecation.elasticsearch", "data_stream.namespace" : "default", "ecs.version" : "1.7", "elasticsearch.event.category" : "types", "event.code" : "index_with_types", "elasticsearch.http.request.x_opaque_id" : "" } PUTクエリのワーニングと同じように、タイプ(type)を廃止した旨のワーニングが表示されました。このように非推奨の機能が確認可能なため、バージョンアップの際は是非ご利用ください。 Javaクライアント ZOZOTOWNの商品検索API(Spring Boot)は、Elasticsearchへ接続するクライアントに下記テックブログで紹介している通り、 High Level Rest Client (以下、HLRC)を使用しています。 techblog.zozo.com しかしながらHLRCは7.15.0で非推奨になり、新たな Java API Client がリリースされました。そのため今回のバージョンアップ作業として、新Java API Clientに移行するかを 移行ドキュメント と 検索クエリのドキュメント で確認し検討しました。ドキュメントから実装方法が大きく異なっていることを確認したため、改修にはある程度の期間が必要であると想定出来ました。そのためEOLの迫っている現状での対応は見送ることとしました。新Java API Clientを利用することで得られる恩恵は少なからずあると思うので早めの移行したいと思います。 LTRプラグインのバージョンアップにともなうJavaのバージョンアップ こちらのテックブログに記載があるように、弊社ではLTRプラグインを利用しており、特徴量を出力する過程で利用している特徴量キャッシュの機能をコントリビュートしました。 techblog.zozo.com 特徴量キャッシュの機能がマージされた 特徴量キャッシュの機能がリリースされるまでの期間は、本家のリポジトリをForkした独自ビルドを利用していました。本家のリポジトリに送った プルリクエスト がマージされ、Elasticsearch v7.16.3以降を対象にリリースされました。そのため独自ビルドを止め本家のLTRプラグインを利用することとしました。 対象 バージョン LTRプラグイン v1.5.8-es7.16.3 LTRプラグインを利用しているAPI(Spring Boot)は、Java 11で開発していました。 そのためSpring BootのPOM.xmlに依存関係を追加します。 <!-- https://mvnrepository.com/artifact/com.o19s/elasticsearch-learning-to-rank --> <dependency> <groupId>com.o19s</groupId> <artifactId>elasticsearch-learning-to-rank</artifactId> <version>1.5.8-es7.16.3</version> </dependency> いざv1.5.8-es7.16.3以上のLTRプラグインを利用しようとするとこのようなエラーが出ました。 クラス・ファイル/xxxx/.m2/repository/com/o19s/elasticsearch-learning-to-rank/1.5.8-es7.16.3/elasticsearch-learning-to-rank-1.5.8-es7.16.3.jar!/com/o19s/es/ltr/logging/LoggingSearchExtBuilder.classは不正です クラス・ファイルのバージョン58.0は不正です。55.0である必要があります 削除するか、クラスパスの正しいサブディレクトリにあるかを確認してください。 このエラーの内容は、LTRプラグインはJava 14(クラス・ファイルのバージョン58.0)でコンパイルされ、開発環境で利用しているJava 11(クラス・ファイルのバージョン55.0)ではLTRプラグインを利用できないことを意味します。 そのためAPI開発で利用しいているJavaを14以上にバージョンアップする必要が出てきたため、Javaのバージョンアップも急遽実施しました。 Javaクラスファイルのバージョン確認方法 ここではJavaクラスファイルのバージョン確認方法をご紹介します。コンパイルで生成されたクラスファイルに対してjavapコマンドを実行します。 javap -v test.class 実行結果より、major versionの記述がある箇所を確認します。 ・・(抜粋)・・ major version: 55 ・・(抜粋)・・ この例では major version: 55 のため、Java 11をターゲットにコンパイルされたクラスファイルであることが分かります。クラスファイルのバージョンを確認する必要がある場合は、javapコマンドを利用して確認してみてください。 Elasticsearchクラスタのコード管理化 SREチームで運用しているバージョンアップ前のクラスタには以下の課題がありました。 Webコンソールからの操作でクラスタを作成しており、再作成時に必要な初期設定などの再現性が低い ノード拡張はecctl(Elastic Cloud Control)をラップしたスクリプトで操作し、プラグイン設定はWebコンソールから操作する、といった半手動運用によりオペレーションミスが混入し易い 手動運用が入りIaC化出来ていない箇所があるため、インフラ構成変更のレビューコストが高い ecctlをラップしたスクリプトのメンテナンスコストが高い こうした課題を解決するため、SREチームではElasticsearchの構築・運用を全てIaC化し管理したいモチベーションがありました。今回は新しいクラスタへ立て替える機会に合わせてIaC化を実施しました。 IaC方法の選択 IaC化するにあたって幾つかの選択肢が考えらました。 ecctlを利用する Elasticsearch Service APIを利用する Elastic社より提供される Terraform provider を利用する 前述の通り既に一部運用にecctlを利用していますが、ノード拡張といった特定の操作を簡略化するためにecctlをラップしたスクリプトを作り込んでいる状況があります。Elasticsearch Service APIを利用した場合も同様にラップしたスクリプトを作り込む必要が想定されました。またスクリプトを作り込んでいった結果、Terraformで提供されている機能を再現してしまった、という車輪の再発明に至る可能性もあります。 そのため今回は独自な作り込みが不要なこと、またSREチームで既に運用しているTerraform用CI/CDが利用可能なメリットもあることから、TerraformによるIaC化を選択しました。 TerraformによるIaC化 TerraformでのIaC化を進めるにあたり、運用に必要な機能が提供されているか検証する必要があります。 resource “ec_deployment” “poc_cluster” { region = “ap-northeast-1” version = “7.17.0" deployment_template_id = “aws-cpu-optimized-arm” name = “poc_cluster” elasticsearch { autoscale = “ false ” topology { id = “hot_content” size = “1g” zone_count = “1" size_resource = “memory” } extension { name = ec_deployment_extension.poc_plugin.name type = “bundle” version = “*” url = ec_deployment_extension.poc_plugin.url } } kibana {} apm {} enterprise_search {} } resource “ec_deployment_extension” “poc_plugin” { name = “poc_plugin” description = “poc_plugin” version = “*” extension_type = “bundle” file_path = “./poc_plugin.zip” file_hash = “xxxxxxxxx” } これは最小ノード構成のクラスタを作る場合のサンプルコードです。例えばノードサイズの変更ができるか確認する場合は、以下のようにコードを変更しterraform applyを実行する方法で検証しました。 topology { id = “hot_content” size = “1g” -> “60g // 1GBから60GBへ変更できるか確認 zone_count = “1” size_resource = “memory” } このような方法で検証を進め、現在SREチームで行っている運用業務が全てコード変更で行えるか確認した上で、TerraformでのIaC化を最終決定しました。 また、Elasticsearchクラスタは開発環境〜本番環境それぞれ個別に存在しており、クラスタ構成自体に差分もあります。ある環境への変更が他の環境に影響を及ぼすことは避けなければなりません。 こうした環境間の問題を考慮し採用したディレクトリ構成がこちらです。 elastic_cloud ├── main.tf // Terraform verや使用するproviderを定義 ├── plugin │ └── xxxx.zip // 各種プラグインファイルをこのフォルダに配置 ├── dev │ ├── main.tf -> ../main.tf │ ├── local.tf // 使用するプラグインpathなど、環境変数を定義 │ └── elasticsearch.tf // Elasticsearchクラスタを定義 ├── stg │ ├── main.tf -> ../main.tf │ ├── local.tf │ └── elasticsearch.tf └── prd ├── main.tf -> ../main.tf ├── local.tf └── elasticsearch.tf ※.tfstateの出力先管理ファイル等、本解説に関係のないファイルについては割愛しています。 本番環境クラスタは専用のマスタノードを構成する、など環境毎にクラスタ構成の差異が存在します。こういった差異は環境変数では吸収できないため共通化ファイルとはせず、各環境で定義する方式としています。 Elastic Cloud上でのプラグインはクラスタ単位ではなくアカウント単位での管理となります。 全環境で共通に使用されているプラグインを更新すると全環境へ同時に反映されてしまいます。これを防ぐためプラグイン定義を環境毎に分離しました。 このようにElasticsearchクラスタのIaC化を行いました。まだSREチームで運用しているクラスタ全てがIaC化されてはおりませんが、引き続きバージョンアップ作業などを機にIaC化を取り組む予定です。 検証環境での負荷試験 バージョンアップ前のクラスタはインスタンスタイプにm5d(general purpose)インスタンスを選択していました。その後、日々運用していく中でパフォーマンス改善に期待できるc6gd(CPU optimized)インスタンスが提供されました。SREチーム内でも検証したいインスタンスタイプではありましたが、一度作成したクラスタのインスタンスタイプは変更できないこともあり低い優先度となっていました。今回クラスタを作り直す機会に合わせて、m5dインスタンスからc6gdインスタンスへの変更を検討するため負荷試験を実施しました。 また、今回はElasticsearchのバージョンアップの他に、LTRプラグインのバージョンアップもありパフォーマンスの変化が予想されます。 そのため、バージョンアップ後クラスタをサービスインさせるにあたり、年末年始の安定稼働を保証するためZOZOTOWNが想定する最大負荷を掛ける負荷試験を実施しました。 負荷試験の実施方法 負荷試験は検証環境に本番環境と同等構成のアプリケーションpod・バージョンアップ後クラスタを用意し、試験パターンに応じてpod・ノード数を可変させ実施しております。また負荷をかける方法としてOSS負荷試験フレームワークであるgatlingを用いており、API Gatewayから検索API・Elasticsearchを通したレイテンシを計測しています。 gatlingの実行には分散負荷試験ツールGatling Operatorを用いました。Gatling Operatorは分散負荷試験のライフサイクルを自動化するKubernetes Operatorです。先日SRE部より紹介しておりますので詳細はこちらをご覧ください。 techblog.zozo.com インスタンスタイプ検証結果 バージョンアップ前のクラスタを基準にした、バージョンアップ後のm5dクラスタとc6gdクラスタの傾向は以下です。 インスタンスタイプ CPU使用率 99パーセンタイルレイテンシ m5d 同等 同等 c6gd 改善 悪化 ※あくまでZOZOにおける検索の利用方法による結果となります。一概に全ての利用方法で同じ傾向になるということではありません。 SREチームでは99パーセンタイルレイテンシに基準値を設けており、基準値を超えた場合はリリースNG判定をしています。 c6gdインスタンスは、CPU使用率について改善が見られたものの99パーセンタイルレイテンシがリリース基準値を超えてしまいました。 m5dクラスタとc6gdクラスタは異なるCPUアーキテクチャを採用しており、この差異が今回の結果となったと想定しています。 ただし今回はEOLまでの期間が迫っていたこともあり、この差異の詳細についての深掘りはせず引き続きm5dクラスタを使う方針で決定しました。パフォーマンス改善のためにもCPU optimizedインスタンスへの切替は、今後も機を見て挑戦する予定です。 最大負荷時におけるバージョンアップ前後の比較結果 最大負荷を想定したノード数まで拡張を行い実施した負荷試験のバージョンアップ前後の比較です。 CPU使用率 99パーセンタイルレイテンシ 同等 同等 バージョンアップに伴う性能変化はないことを確認した上でサービスインを実施しました。 別クラスタに新しいバージョンのElasticsearchを構築 前述の通り、Terraformコードを基にクラスタを自動構成するため、IaC化が完了した時点で本番環境の構築は簡単に進む想定でした。しかしながら、実際にはクラスタ構築をしたところ以下のようなエラーが発生しました。 Error: failed creating deployment: 2 errors occurred: * api error: clusters.cluster_plan_version_not_permitted: The requested version of [7.17.0] set in [elasticsearch.version] is not permitted as it violates the ESS version policy (resources.elasticsearch[0].elasticsearch.version) * set “request_id” to “l1vwuu43qlzgkgppw51j1lncgc2mboe78cqf1g3g913nyq8co0pbut6xdmtbz81l” to recreate the deployment resources これはElastic Cloudサポートへ確認したところバージョン7.17.0m5dインスタンスクラスタの対応が終了していたことが原因でした。そのため、Elastic Cloudサポートより一時的にクラスタ作成の制限を解除する対応をとっていただきました。 制限の解除後、m5dインスタンスに対応する最終バージョンである7.13.0でクラスタを構築し、構築後7.17.0にアップデートする手順でクラスタを構成しました。 resource “ec_deployment” “elasticsearch” { region = “ap-northeast-1” version = “7.13.0" -> “7.17.0” こちらはTerraformによるクラスタバージョンアップ時の変更コードです。図らずもTerraformによるバージョンアップ作業が正常に行えることの確認にもなり、今後のバージョンアップにも役立つ結果となりました。 新旧の両クラスタに対してのインデクシング 冒頭にも記載しました通りRolling upgradeでのバージョンアップはリスクがあると判断したため新クラスタを準備しました。そのため旧クラスタと新クラスタの両方にインデクシングを行い、同様の頻度で更新を実施することで全く同じ環境を構築しました。 新旧の両クラスタへのインデクシングには単純に直列で行うと2倍の時間がかかりますが、ZOZOTOWNのインデクシングの仕組みには、以前より並列で動作させる仕組みがありました。そのため並列度を上げることで、インデクシングのサービスレベルを落とすこと無くインデックスを構築しました。 構築した新旧クラスタのインデックスの検証には、インデックス比較用にPythonスクリプトを作成し、定期的にインデックス差分が無いかをチェックしました。また、品質管理部にも協力いただき、ZOZOの画面レベルでも検索結果の比較テストを実施することで品質を担保しました。 各種サービスの参照を旧クラスタから新クラスタへ切替え 旧クラスタから新クラスタへの切替作業に関して、弊チームが管理しているAPIを経由するリクエストは容易に切替えることができました。しかしZOZOTOWNの検索機能で利用しているインデックスには他チームが管理しているモデルやインデックスもあり、さらに直接Elasticsearchを参照しているチームもありました。そのため、切替時には各チームと事前に日程を調整し、切替えを実施しましたが、想像以上に連携・調整コストが掛かりました。 バージョンアップでクラスタを切替える度に、他チームと連携・調整するコストはとても負担が大きいです。そのため今後はElasticsearchの直接参照は出来る限り廃止し、Elasticsearchへのリクエストは弊チームが開発したAPIを一律経由させるよう継続して改善を進めています。 旧クラスタの削除 新クラスタへの切替が完了した後に旧クラスタを削除しました。その際、Elasticsearchを直接参照している機能があるため、旧クラスタへの参照が残っていないことの確認はSlowlogを用いて確認しました。 Slowlog インデックス毎にquery/fetch/indexの処理時間をwarn, info, debug, traceレベルで設定できます。設定時間を上回ったクエリは専用ログに出力され、ログを検索することでどのような処理が動いたかを確認出来ます。詳細は Slowlog の公式ページをご覧ください。 インデックスに対する設定内容はこちらです。0sにすることで全てのクエリがログ出力されるようになります。 PUT /zozo-demo-index/_settings { "index.search.slowlog.threshold.query.debug": "0s" } Slowlogを確認するクエリはこちらで、ログ出力時のタイムスタンプを降順にソートさせています。 GET /elastic-cloud-logs-7/_search { "size":1000, "query": { "bool": { "must": [ { "term": { "log.level": { "value": "DEBUG" } } }, { "range": { "@timestamp": { "from":"2022-04-27T08:55", "to":"now" } } } ] } }, "sort": { "@timestamp": { "order": "desc" } }, "_source": ["@timestamp","message"] } 最後に Elasticsearchのバージョンアップサイクルは早いため、追従するにはとても労力が必要な作業です。今回紹介した知見でElasticsearchのバージョンアップが少しでも楽になれば幸いです。 またElasticsearchは新機能の追加やパフォーマンス向上も積極的に行われているため、バージョンアップで得られる恩恵は少なからずあると思います。そのため手が付けられないほど最新バージョンと差が広がる前に、定期的なバージョンアップをおすすめ致します。 弊社では、検索機能を開発・改善していきたいエンジニアを募集中です。 hrmos.co https://hrmos.co/pages/zozo/jobs/0000010 hrmos.co
こんにちは。技術本部SRE部ZOZO-SREブロックに所属している杉山です。SRE部のテックリードとして、オンプレ/クラウドのインフラを担当しています。 ZOZOTOWNでは、既存システムのリプレイスプロジェクトを進めています。各サービスのマイクロサービス化は進んでいますが、バックエンドでは「WindowsServer + IIS」で稼働しているシステムがまだ多く残っています。そのリプレイスプロジェクトを進めるうえで重要なポイントとなる、セッションストアのリプレイス「セッションオフロードPhase 2」が完了しました。本記事では、リプレイスしていくうえでの工夫や課題への対応を紹介します。 目次 目次 セッションオフロードPhase 2について プロジェクト概要 Phase 1:CacheStoreのリプレイス Phase 2:SessionStoreのリプレイス ZOZOTOWNが抱える、セッション管理の課題 1:スティッキーセッションのため、スケーリング運用に支障がある 2:スティッキーセッションのため、サーバー負荷が偏る 3:オフロードしないとフロントエンドリプレイスができない リプレイス前後の構成 採用技術について Amazon Elasticache Redis RedisClusterのシャードとは 冗長構成について セッション期限切れ処理について 本番リリース 「Cookie Persistence」を利用した、N%リリース リリース後に発生した想定外のトラブル まとめ セッションオフロードPhase 2について プロジェクト概要 セッションオフロードプロジェクトは、CacheStoreリプレイスのPhase 1と、SessionStoreリプレイスのPhase 2で構成されています。 Phase 1:CacheStoreのリプレイス Phase 2:SessionStoreのリプレイス Phase 1:CacheStoreのリプレイス Phase 1:CacheStoreのリプレイスについては、こちらの記事をご覧ください。 techblog.zozo.com Phase 2:SessionStoreのリプレイス セッションオフロードPhase 2は、WebサーバーであるIISのサーバー内セッションの機能を無効にし、外部SessionStoreにオフロードさせるリプレイスプロジェクトです。ZOZOTOWNが抱えていた、スケーリング運用やフロントエンドのリプレイスの課題を解決することを目的としています。 ZOZOTOWNが抱える、セッション管理の課題 以下のような課題がありました。 スティッキーセッションのため、スケーリング運用に支障がある スティッキーセッションのため、サーバー負荷が偏る オフロードしないとフロントエンドリプレイスができない それぞれの課題について、具体的に説明します。 1:スティッキーセッションのため、スケーリング運用に支障がある ユーザーセッションにIISの機能であるセッションを利用しており、ユーザーセッションがWebサーバーに紐づきます。LoadBlancerでは、Cookie Persistenceと呼ばれる機能で振り分け先の固定が必要です。そのため、Webサーバーのスケーリング運用のうち「スケールイン」に注意が必要です。ユーザーのセッションが期限切れになるのを待ったり、夜中の時間帯を狙ってスケールインする必要があるなど、運用に支障がありました。 2:スティッキーセッションのため、サーバー負荷が偏る ユーザーセッションごとにWebサーバーを固定する必要があるので、リクエストのロードバランシングで偏りが発生します。これにより、一部のサーバーは負荷が高くなってしまうこともありました。 3:オフロードしないとフロントエンドリプレイスができない ユーザーセッションがWebサーバーに対してスティッキーなため、フロントエンドを段階的にモダンな技術でリプレイスするという手法が取れません。例えば、まずはトップページをコンテナ化しEKSで運用したいが、セッション情報が特定のWebサーバーに保存されているため、セッション情報を維持しにくいです。 Webサーバー内のセッションを、外部のデータストアにオフロードすることで、これらの課題を解決しユーザーリクエストがどのWebサーバーに割り振られてもセッションを維持できるようにする事が目的です。 リプレイス前後の構成 リプレイス前(左)は、WebサーバーのIIS内のセッションを、独自ライブラリを介して利用していました。リプレイス後(右)は、外部SessionStoreにセッションデータをオフロードし、独自ライブラリに実装したクライアントを用いて利用します。 また、リプレイス時のアプリケーションの改修コストを抑えるため、独自ライブラリ内でRedisクライアントをラップして実装しました。そして、アプリケーションコードの改修を最小限に抑えて、Redisへの接続に切り替えることができるようにしました。 採用技術について Amazon Elasticache Redis 世間で広く使われている技術のRedisを採用しました。クラウドサービスとしては「Amazon Elasticache Redis」(以下、Redisという)の「Clusterモード有効」を採用しました。 Redisは、負荷特性に応じてClusterモードの有効/無効を選択できます。ホットキーの多い負荷特性の場合は、Clusterモード無効にして複数のリードレプリカとリーダーエンドポイント使う方が効果的な場合もあります。 セッションをオフロードすることでRedisは全Webサーバーから多くのリクエストを受けることになりますが、セッションデータはユーザー毎のデータなのでホットキーはありません。このことから、Clusterモード有効のRedis(以下、RedisClusterという)を利用してシャードのスケールアウトで負荷分散できるようにしました。 例えば、平常時は30シャード、セール時は40シャード、ZOZOTOWN最大の負荷となる冬セールは60シャード。というように、必要に応じてシャード数を増やし負荷分散させています。 RedisClusterのシャードとは シャード:1~6個のRedisノードで構成される集合の単位。 ノード:Reidsノード単体のことで、「Primary / Replica」の種類がある。 本記事では、「シャード」をスケーリングの単位。「ノード」をRedisノードとして表現します。 冗長構成について RedisClusterの各シャードは、プライマリノードの他に別AZのレプリカを持たせることができ、ノード障害時に「プライマリノードのフェイルオーバー」で自動復旧させることが可能です。検証では、プライマリノードのフェイルオーバーでの復旧は障害の内容にもよりますが、1分弱程度から数分でフェイルオーバーが実現できました。しかし、ユーザーセッションを取り扱うとても大事なシステムとなるため、弊社では30秒程度の復旧を目標としていました。 この要件を満たすために、シングルAZのRedisCluster(レプリカ無し)において、プライマリクラスター/セカンダリクラスターでAZ違いの2クラスター運用にしました。独自ヘルスチェックシステムをEKSで動かし、ノード障害発生時には速やかにクラスターフェイルオーバーを行う事で、約15秒~30秒程度でのフェイルオーバーを実現しました。 2クラスター運用としたことにより、メンテナンスの際にセカンダリを活用することで、ローリングでメンテナンスを実施可能になったことも1つのメリットとなっています。 セッション期限切れ処理について ZOZOTOWNは、ユーザーの買い物体験を向上するための機能として「在庫引き当て」という機能があります。この機能は、商品をカートに投入したユーザーがセッションを維持している間は、決済前でも在庫を確保するという機能です。この在庫引き当ては、セッション情報が紐づいているため、セッション期限切れ(Expire)時に在庫を戻す処理が必要です。 Redisでは、keyにTTLを設定し自動でExpireさせる機能がありますが、検証当時はExpireをトリガーとして任意の処理を実行できませんでした。Pub/Subモードを利用するという事も考えましたが、何らかの障害でSubscriberがメッセージを受信できなかった場合、その商品の在庫引き当て解除ができない事態となることが考えられました。 可能な限り確実に処理を実行できることを担保したい。障害が発生した後でも、在庫引き当て解除の処理が確実に実行されるようにする。これらを実現するために、外部ワーカーでRedisのデータをスキャンし「セッション期限切れの処理」と「在庫引き当て解除」の処理を行っています。 このワーカーは、EKSでJobを稼働させ、後ろで待ち構えているQueueに「在庫引き当て解除リクエスト」を流しています。 本番リリース 「Cookie Persistence」を利用した、N%リリース 本番リリースでは、AkamaiALBを活用することで、スティッキーを外しながらN%リリースしました。リリース時の構成は以下のような構成になります。 AkamaiALBで「スティッキーON / サーバー内セッション」のサーバープールAに向けたOrigin-Aと「スティッキーOFF / セッションオフロード状態」のサーバープールBに向けたOrigin-Bを用意します。 AkamaiALBの「Cookie Persistence」を利用してリクエストの振り分け先をパーセンテージで制御します。これにより、一度Origin-Bに振り分けられたユーザーのリクエストはOrigin-Bに振り続けられます。 切り戻しを行う場合には、Origin-Bのパーセンテージを0%にしてから、CookiePersistence用のCookie設定を再設定します。これにより、Origin-Bへリクエストが振り分けられなくなります。 AkamaiALBを利用した振り分け手法は、過去のブログでも紹介しています。 techblog.zozo.com リリース後に発生した想定外のトラブル ZOZOTOWNは、オンプレ/クラウドのハイブリッド環境で運用しており、Direct Connect(以下DXという)を利用しています。 セール時にはAmazon Elastic Compute Cloud(以下、EC2という)も活用することで、Webサーバーをスケーリング運用していますが、平常時のWebサーバーの多くはオンプレで稼働している運用でした。 N%リリースが50%の時のとある日。平常時よりも負荷が高い状況ではあったのですが、負荷のピーク時間帯なると急激にDXのデータ通信量が増え、10G回線の帯域が上限に達してしまうという事態となりました。この時は切り戻しを行い10%リリースまで縮小させることで対応しました。 分析した結果、アプリケーションのあるロジックが非常に大きなデータをセッション情報に読み書きを行っていて、ユーザーリクエスト数に応じてDXのデータ通信量が増加するというロジックになっていました。 特定の条件下において発生する事象でした。全てのWebサーバーがEC2であればDXの帯域枯渇は起こらなかったでしょう。突き止めた原因に対してロジックの改修を行い、セッションデータの最適化を行う事で、現在はデータ通信量が急増することは少なくなりました。 この対応の後、2週間ほどかけて無事100%リリースとなりました。ハイブリッド環境では、DXにも注意が必要であると痛感しました。 まとめ セッションオフロードプロジェクトは、Phase 1 / Phase 2と合わせると1年以上の長い期間をかけたプロジェクトでした。ZOZOTOWNの大規模サービスを支えるためのインフラの工夫やハイブリッド環境ならではのトラブルなど、いろいろなことを経て無事リリース完了できました。何よりも、今後のフロントエンドリプレイスを加速させていく準備を整えられたことが良かったです。 ZOZOでは、インフラSREを募集しています。ご興味がある方は以下のリンクから是非ご応募ください! corp.zozo.com
はじめに 初めまして。ZOZOTOWN開発本部ZOZOTOWNアプリ部Android2ブロックの下川と申します。ZOZOTOWNアプリ部ではAndroidを担当するチームが今年の4月から2つになりました。1つのチームで運営するには人数が多くなってきたためです。そして私は新しくできたチームのリーダーを務めています。 この記事では、そんな2つになる前のAndroidチームがメンバーを増やすために、オンボーディングで抱えていた課題をどのように解決していったかを紹介します。 目次 はじめに 目次 オンボーディングに対する課題感 課題解決に向けたアプローチ 様々な取り組みの成果 最後に オンボーディングに対する課題感 ZOZOでは入社後に人自部によるオンボーディングが実施されますが、部署配属後にも配属先ごとにオンボーディングを行っています。その中で私が所属している部署では、以前からオンボーディングに対して課題がありました。また、ZOZOではコロナ禍を機に多くの社員が自宅からのリモート勤務になりましたが、オンラインでのオンボーディング実施に対する課題も新たに出始めているところでした。それらの課題を列挙してみると、以下のようなものがありました。 毎回オンボーディングの準備に時間がかかる 実施すべきことが漏れてしまう リモートだと新しいメンバーの状況が把握しづらい 所属している部署では、スムーズに新メンバーを受け入れられるよう上記の課題を解決するオンボーディングの仕組み化が急務でした。なぜなら、オンボーディングに時間がかかると新メンバーは中々チームや業務に馴染めず、チームの生産性が向上しないからです。さらに次の新しいメンバーを迎え入れることも難しくなってしまいます。そこでまずはAndroidブロックというチームの中で少しずつ改善していき、良かった部分は将来的に他の部署へシェアしていく形を目指して改善に着手しました。 課題解決に向けたアプローチ 最初に取り組んだのは、新しく配属されてくるメンバーにメンターを設定することです。メンターは新メンバー配属前に、チーム内で議論して決めるようにしました。時期や新メンバーのスキルに合わせてメンターを選出したいという意図があります。そのためメンターを担当するメンバーは固定ではありません。メンターになった人は主に以下のような役割を担います。 新メンバーの業務のサポート 必要なSlackチャンネルへの招待 参加が必要なミーティングへの招待 誰に聞けば良いか迷うような質問の1次受けの窓口 次にオンボーディングに関するドキュメントの整備をしました。ZOZOではドキュメント・ツールとしてConfluenceを使用しています。「オンボーディングの準備にかかる時間」と「実施すべきことが漏れてしまう」ことを改善するために、新しいメンバーが行うべき項目や手順をConfluence上にまとめました。具体的には、以下のような項目をドキュメント化しました。 メンターのやることリスト 勤怠に関する注意事項 業務上必要なツールの申請や設定手順 業務の進め方に関する注意事項 各種会議体についての説明 ドキュメントの整備は基本的なことですが、社員であれば誰でもアクセス可能な場所に情報が整理されている状態にすることは、最新の情報に安定してアクセスするのに最も効果の高い方法かと思います。特に「誰でもアクセス可能な場所」というのが重要で、もし情報が古くなっていた場合でも気づいた人がすぐに更新することが出来ます。 最近では社内ツールの利用申請で一部の申請方法が変わっていた場合などは、実際に申請した新メンバーがドキュメントを更新してくれることもあり、新メンバーが気軽にコミット出来る点でも良いなと感じています。また、適切に更新されているドキュメントがあると初めてメンターを担当する際の敷居も低くなりますし、オンボーディングを進める上でもメンターの負担がとても軽くなるなどといったメリットもあります。 続いての改善は、新メンバーのサポート面です。Slack上にオンボーディング専用チャンネルを作成し、オンボーディングに関するコミュニケーションを全てそのチャンネルに集約しました。これには2つ目的があります。 1つ目はメンター以外のメンバーでも、積極的に新メンバーのサポートを出来るようにすることです。オンボーディング中はメンターが中心となってサポートしますが、メンターの人も通常業務を抱えているためすぐにサポート出来るとは限りません。そのため、新メンバーの困り事をメンターでなくても素早くキャッチ出来るよう、通常の開発業務で使用しているチャンネルと分けました。 2つ目は、オンボーディングに関する過去のやり取りを見つけ易くするためです。ドキュメント化を進めていても、やはり「前回はどうしたのだろう」や「なぜ今の形になったのだろう」など過去のやり取りを見返したいケースは発生するため、専用チャンネル化することで解決しようという狙いです。 また、新メンバーの自主性に頼り過ぎないよう、新メンバーの状況把握も改善しました。ZOZOでは原則週1回の頻度で上長との1on1を行っていますが、新メンバーはそれにプラスしてメンターと毎日1on1を行うようにしています。新メンバーが分からないことや困っていることを、少しでも早く解決することが目的です。これらのサポート体制の仕上げとして、メンターは1か月間のオンボーディングの終わりに上長や新メンバーと一緒に次のオンボーディングに活かせるよう振り返りを実施しています。 最後に紹介する改善は、コミュニケーションに関するものです。普段使用しているコミュニケーション・ツールはSlackとGoogle Meetです。それ以外にも気軽に話せるようにという目的でDiscordも併用しています。最近ではあるメンバーがメンターをしたときの発案で、それらを活用してWelcome Coffee Chatという時間をオンボーディング期間中に設ける試みも行いました。 1 内容としては、新メンバー+既存メンバー2〜3人くらいの規模を複数回に分けて開催し、新メンバーが全員と話せる会としました。話す内容も学生時代のことや趣味についてなど、お互いのことを知ることが目的となるようなテーマを設定しました。どうしても業務中に新メンバーへ話し掛けるタイミングが(とりわけリモートだと尚更)難しいため、交流する時間をイベントとして設定することで新メンバーと雑談出来るのはとても良かったです。 様々な取り組みの成果 以上のような改善を段階的に進めてきた結果、次のような成果が得られました。 1つ目はオンボーディングに関するドキュメントを整備および継続して更新してきたことによって、準備にかける時間が短縮され、オンボーディング開始後もスムーズに進行出来るようになったことです。また、新メンバーが配属後に数日経ってから「利用申請してないものがある」や「アレまだやってない」などということがほぼ無くなりました。 2つ目はサポート体制を改善したことによって、リモート勤務下でもスムーズにチームや業務に慣れていってもらうことが出来るようになったことです。その結果、その時の業務の状況にもよりますが、新メンバーが配属後3日〜5日でPull Requestを出せるようにもなりました。 ただし、まだ改善が足りていないなと感じる部分もあります。冒頭にもご紹介しました通りチームの規模も大きくなってきました。そのため新メンバーがメンターや上長以外の既存メンバー全員とのコミュニケーションの機会を得るのが難しくなってきています。先のWelcome Coffee Chatやオンラインでの歓迎会などを試しているものの、まだまだ改善の余地があると考えています。また、新メンバーが最初に取り組む丁度良い粒度のタスクが常にあるとは限らないため、チュートリアル的なタスクなどが用意出来ると良いのか、などといった検討も必要だと感じています。 最後に直近1年のオンボーディングの振り返りで、新メンバーからもらったフィードバックを紹介します。 良い点 改善系タスクから入ったのはやりやすかった 3日目ぐらいからコード書けたのが良かった 入社してからやることがドキュメントにまとまっていて、漏れなくスムーズに環境整備ができた メンターの方と毎日1on1をしていただけたため、些細な疑問や相談事を解消しながら業務に取り組むことができた リモートワークでのスタートだったためチームに馴染めるか不安だったが、Welcome Coffee Chatや歓迎会などでチームのメンバーと趣味や休日の過ごし方など雑談する場があったのでよかった 今後改善が必要だと思われる点 コードの量が多くてオンボーディング中に把握しきれない 担当が付くことで他のメンバーとのコミュニケーションが少なくなってしまう 申請が通るまで使えないツールがあり、作業をブロックされることがしばしばあった 自分は導入に最適な軽い案件が運良くあったのでキャッチアップが楽だったが、そのような案件が無い場合のプランが未定 Coffee Chat以降案件メンバー以外との会話する場があまりない このように新メンバーからも積極的にフィードバックをもらうことで、既存メンバーの認識が薄い箇所もしっかりと改善していけるよう工夫しています。 最後に ZOZOではAndroidエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください。 hrmos.co いくつかのチームではGatherというコミュニケーション・ツールを試用していおり、Welcome Coffe Chatを行ったときもGather上で実施しました。 ↩
はじめに こんにちは。検索基盤部の倉澤です。 私たちは、ZOZOTOWNの検索機能の改善に取り組んでいます。ZOZOTOWNのおすすめ順検索ではランキング学習を用いた検索機能の改善に取り組んでおり、A/Bテストにて効果を測定しています。 ランキング学習やElasticsearch Learning to Rankプラグインについては過去の記事で紹介していますので、併せてご覧ください。 techblog.zozo.com techblog.zozo.com 私たちは、機械学習モデルの開発からデプロイまでの一連の処理を実行するワークフローの構築にGoogle Cloud Platform(GCP)の Vertex AI Pipelines を利用しています。 本記事では、Vertex AI Pipelines採用前の運用とその課題点について説明し、次にVertex AI Pipelinesで構築したワークフロー概要とその運用について紹介します。 目次 はじめに 目次 Vertex AI Pipelines採用の背景 従来の運用 papermillとは 抱えていた課題と解決策 Vertex AI Pipelinesによるワークフローの構築 1. 学習データセット生成に必要な期間のデータが揃っているかの確認 2. 学習データセットの生成 3. ハイパーパラメータチューニング及び最適なパラメータによる学習 4. 評価及びオフライン評価結果の描画 5. デプロイ判定 Vertex AI Pipelines導入後の運用 A/Bテストのモデル開発時のブランチ戦略について まとめ Vertex AI Pipelines採用の背景 Vertex AI Pipelines採用に至った背景として、従来の運用と抱えていた課題点を紹介します。 従来の運用 Vertex AI Pipelinesを採用する以前は、GitHubで管理されているスクリプトを、開発者が各自GCPに立てたインスタンスのJupyter Notebook上で順に実行していました。 機械学習モデルの学習期間や特徴量などのパラメータは設定ファイルで管理しており、 papermill でNotebookを自動実行して機械学習モデルを生成します。そして、Elasticsearchの Learning to Rankプラグイン で指定された形式にモデルを変換し、手動でデプロイを行っていました。 papermillとは Jupyter Notebookを実行するライブラリとして記載したpapermillについて簡単に説明します。 papermillは、Jupyter Notebookに定義された各セルを実行するPythonライブラリです。 実行時にパラメータを渡すことで予めセルに定義されたデフォルトのパラメータの上書きが可能です。実行されたJupyter Notebookは別名のNotebookに保存できます。 私たちは、papermillをCLIで実行していました。 papermill input.ipynb output.ipynb -f parameter.yaml 設定ファイルは以下のようにYAMLファイルとして定義できます。 # parameter.yaml train_start_date : 20220101 train_end_date : 20220102 valid_start_date : 20220103 valid_end_date : 20220104 test_start_date : 20220105 test_end_date : 20220106 features : - feature1 - feature2 - feature3 抱えていた課題と解決策 従来の運用フローでは、モデルの数だけ同様の作業を手動で繰り返しており、以下の点を課題に感じていました。 各タスクの実施作業と実施完了の確認作業の工数が多い 実行前に設定ファイルの変更に対するレビューが無いので、誤りがあった場合は機械学習モデルを再度生成し直す必要がある 機械学習モデル生成の一連のタスクが途中で失敗した際に、一から再実行する必要がある これらの課題を解決するために、各タスクを依存関係通りに実行でき、さらに再実行時にはキャッシュが利用できるワークフローエンジンの導入を検討しました。 候補となるワークフローエンジンはいくつかありましたが、弊社MLOpsブロックがVertex AI Pipelinesの実行環境の整備を進めていることもあり、より導入コストが低いVertex AI Pipelinesを選びました。 Vertex AI Pipelinesの実行環境については過去の記事で紹介していますので、併せてご覧ください。 techblog.zozo.com Vertex AI Pipelinesによるワークフローの構築 私たちが構成した機械学習モデルの開発からデプロイまでのワークフローの概要を紹介します。以下の図は、実際に運用しているVertex AI Pipelinesのコンソール画面から確認できるワークフローの全体像です。 本ワークフローではおおよそ以下のことを行っております。 学習データセット生成に必要な期間のデータが揃っているかの確認 学習データセットの生成 ハイパーパラメータチューニング及び最適なパラメータによる学習 評価及びオフライン評価結果の描画 デプロイ判定 それぞれ順に説明します。 1. 学習データセット生成に必要な期間のデータが揃っているかの確認 学習に必要となる期間のデータが、対象のBigQueryテーブルに存在しているかの欠損チェックを行います。以下のようなAssertionクエリをコンポーネントから実行し、指定期間のデータが存在しているかを確認します。 -- check_bq_table.sql DECLARE target_dates ARRAY< DATE >; DECLARE x INT64 DEFAULT 1 ; SET target_dates = ( SELECT ARRAY_AGG( date ORDER BY date ) FROM UNNEST(GENERATE_DATE_ARRAY( ' {{ start_date }} ' , ' {{ end_date }} ' , INTERVAL 1 DAY) ) AS date ); WHILE x <= ARRAY_LENGTH(target_dates) DO ASSERT EXISTS ( SELECT {{ period_column }} FROM `{{ full_table_id }}` WHERE DATE ({{ period_column }}) = target_dates [ORDINAL(x)] ) AS ' target date does not exist in this table ' ; SET x = x+ 1 ; END WHILE; 2. 学習データセットの生成 学習・検証・テストのデータセットの生成をします。 以下のYAMLファイルはコンポーネントの入出力を定義し、学習・検証・テストのデータセットを出力しています。 # component.yaml name : Extract Dataset description : Prepare train/valid/test data inputs : - name : project_id type : String - name : job_name type : String - name : execute_date type : String outputs : - name : train_valid_data description : train/validデータセット type : Dataset - name : test_data description : testデータセット type : Dataset implementation : container : image : gcr.io/your_project_id/sample_component:gitsha-xxxx command : [ python, -m, src, --project_id, { inputValue : project_id } , --job_name, { inputValue : job_name } , --execute_date, { inputValue : execute_date } , --output_train_valid_data_path, { outputPath : train_valid_data } , --output_test_data_path, { outputPath : test_data } , ] 生成したデータセットは Cloud Storage FUSE によってマウントされたGoogle Cloud Storageのバケットに格納され、そのパスを後続のコンポーネントへと渡しています。コマンドライン引数で定義されている output_train_valid_data_path と output_test_data_path がこれに該当します。 実行ファイルの中でデータセットの出力先となるパスをコマンドライン引数として受け取り、データセットを保存します。その後、後続のコンポーネントにてこのパスからデータセットを読み込むという流れになります。 3. ハイパーパラメータチューニング及び最適なパラメータによる学習 前段で生成された学習データセットと検証データセットを用いてモデルのハイパーパラメータチューニングを行います。その結果出力された最適なパラメータでモデルの学習をします。 4. 評価及びオフライン評価結果の描画 テストデータセットを用いて学習済みモデルの評価をします。コンポーネント内でオフライン指標として定めているnDCGを計算します。Vertex AI Pipelinesのコンソール画面はマークダウン形式での表示が可能なので、オフライン指標の計算結果を以下のように出力しています。 また、評価時にはベースラインモデルのオフライン指標も計算し、ベースラインモデルからのアップリフト値も併せて表示しています。 5. デプロイ判定 学習したモデルのオフライン指標及びベースラインモデルからのアップリフト値によって、デプロイして良いモデルなのか判定します。このデプロイ判定のコンポーネントの後にElasticsearchへモデルをアップロードするコンポーネントを用意しています。 Vertex AI Pipelines導入後の運用 A/Bテスト時には、コントロール群に適用するモデル(以下、コントロールモデル)とトリートメントモデル群に適用するモデル(以下、トリートメントモデル)をそれぞれ開発する必要があります。また、複数の実験を同時に行う場合はさらに多くのモデルが必要になります。 このA/Bテスト時のモデル開発における運用について紹介します。 A/Bテストのモデル開発時のブランチ戦略について Vertex AI Pipelinesで利用するコンポーネントやパイプラインのソースコードなどはGitHubで管理しています。ここでは、A/Bテストで用いるコントロールモデル及びトリートメントモデル開発時のGitHubのブランチ戦略について簡単に紹介します。 コントロールモデルとトリートメントモデルのパイプラインの構成自体には基本的に大きな違いはなく、データセットを取得するSQLクエリや学習時のパラメータ値が異なります。 A/Bテストの度にトリートメントモデル用のSQLファイルや設定ファイルを新規に作成すると冗長な構造となってしまいます。そこで私たちは、ブランチごとにモデルの開発を分ける運用を採用しました。 main : コントロールモデルのデプロイ用ブランチ コントロールモデルは定期的に学習及びデプロイされるようにスケジューリング .*-abtest-treatment-[1-9] : トリートメントモデルの開発用及びデプロイ用ブランチ prefixには各A/Bテストの名前がわかる任意の値を付与 suffixにはトリートメントモデルの数に応じて番号を付与 トリートメントモデルのパイプラインは、開発ブランチからマージされた時にCIが実行するようにしています。 A/Bテストの結果、トリートメントモデルが勝った場合はそのブランチをmainブランチへマージし、負けた場合はそのままブランチを削除する運用にしています。 まとめ Vertex AI Pipelinesを導入したことにより、冒頭に記載した以下の課題はおおよそ解決しました。 各タスクの実施作業と実施完了の確認作業の工数が多い 実行前に設定ファイルの変更に対するレビューが無いので、誤りがあった場合は機械学習モデルを再度生成し直す必要がある 機械学習モデル生成の一連のタスクが途中で失敗した際に、一から再実行する必要がある コントロールモデルにおいては一通りの開発が終わり、現在はモデルの学習やデプロイの作業に工数を割くことはほとんどなくなりました。ただ、トリートメントモデルの開発やデプロイは現在も運用によりカバーしている側面もあるので、改善に向けて開発に取り組んでいます。 さいごに、ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。検索機能の改善に興味のある方は、以下のリンクからご応募ください。 hrmos.co hrmos.co
はじめに こんにちは、MA基盤の @gachi-muchi-engineer です。 私達のチームでは、Digdagを利用してユーザーにメールを配信したり、データ連携を定期的に行うような様々なワークフローを運用しています。今回その中でも特定の対象者にポイントを付与したり、メールを配信するなどのビジネス要素が強いワークフローを、エンジニアでない運用者が運用していくなかで課題がいくつか出てきました。そこで、動的にワークフローを起動する仕組みを構築することで課題を解決したので、その方法について紹介します。 目次 はじめに 目次 Digdag 背景 1. スケジュール設定の柔軟性 2. パラメータ定義の柔軟性 課題点のまとめ 解決策 仕組み CMSとDBについて 管理するデータについて シーケンス図 1. select dynamic_workflow_config 2. execute workflow by Digdag rest api 3. response session_id attempt_id 4. running workflow 5. update dynamic_workflow_config with session_id and attempt_id ワークフローでの工夫 +get_dynamic_workflow_config_list +loop 導入結果 今後の展望 運用管理ツール スケジュール設定の分散 リトライ方法 まとめ さいごに Digdag Digdagはワークフローエンジンと呼ばれるOSSのソフトウェアです。複数個のタスク間の依存関係からなるワークフローを定義し、そのワークフローの実行及び管理をします。ワークフローはdigという拡張子のファイルにワークフローのスケジュールやタスクの定義を記述します。詳しくは、 公式サイト を確認してください。MA部では、digファイルをGitHubで管理し、GitHub Actionsを用いてリリースする形で運用しています。 背景 今回動的にワークフローを起動する仕組みを導入した背景には、特にビジネス要素が強いワークフローをエンジニアでない運用者が運用していくなかで、以下の課題があったからです。 スケジュール設定の柔軟性 パラメータ定義の柔軟性 それぞれについて詳しく説明します。 1. スケジュール設定の柔軟性 ビジネス要素が強いワークフローの場合、柔軟にスケジューリングを行いたいケースがあります。例えば、月や週毎に実行する時間や曜日を変更したり、曜日ごとに実行日時を変更したいなどのケースがあります。しかし、Digdagでは以下のようなスケジューリングしかできません。 Syntax Description Example hourly>: MM:SS Run this job every hour at MM:SS hourly>: 30:00 daily>: HH:MM:SS Run this job every day at HH:MM:SS daily>: 07:00:00 weekly>: DDD,HH:MM:SS Run this job every week on DDD at HH:MM:SS weekly>: Sun,09:00:00 monthly>: D,HH:MM:SS Run this job every month on D at HH:MM:SS monthly>: 1,09:00:00 minutes_interval>: M Run this job every this number of minutes minutes_interval>: 30 cron>: CRON Use cron format for complex scheduling cron>: 42 4 1 * * ※ setting-up-a-schedule また、このようなワークフローの運用者はエンジニアではありません。そのため、変更するためにdigファイルを修正しGitHubへPRを作成する作業のハードルが高く、結局運用者が実行したいタイミングでWeb UIから手動でワークフローを実行する状態になっていました。 2. パラメータ定義の柔軟性 Digdagのワークフローは call や !include を利用して別のワークフローやタスクを呼び出すことができます。この機能を利用して、パラメータ化された共通のワークフローを用意していました。しかし、利用する際のパラメータの組み合わせの追加や変更することがエンジニア以外に実施できず、そのために運用コストがかかってしまうケースがありました。具体例として、対象者やタイトル、本文がパラメータ化されたメールを送るというワークフローがあったとして、それを利用するためにワークフローを増やさなければならない状態になっていました。 課題点のまとめ これらの課題点をまとめると解決するべきことは以下の通りになります。 エンジニアではない運用者がスケジュール設定を簡単かつ柔軟に設定できるようにしたい エンジニアではない運用者が実行パラメータの変更/追加をもっと簡単にできるようにしたい 複数のワークフローで同じ課題を抱えている 解決策 解決策として、Digdagのワークフローを起動するワークフローを開発し、ワークフローを動的に実行できる仕組みを導入することを考えました。具体的には、以下のイメージ図のようにCMSから運用者がワークフローのスケジュールと設定を登録し、それを元に実行するワークフローです。この仕組みの導入によって運用者はdigファイルを編集することなくワークフローを動的に実行できるようになります。Dynamic Workflow Starterと名付けましたが、長いのでワークフロースターターとします。 仕組み ここでは具体的な仕組みを紹介します。 CMSとDBについて CMSは今回 Google Sheets と Google Apps Script を利用、DBはBigQueryを採用し、CMSと合わせて簡易的なCMSを作りました。 管理するデータについて ワークフロースターターが実行するワークフローの設定、実行日時を管理し、その値を元にワークフローを実行します。DBで管理するデータは以下のようになります。 テーブル:dynamic_workflow_config column type null note uuid string x uuid start_at timestamp x ワークフローの実行日時 project string x 実行したいワークフローのプロジェクト workflow string x 実行したいワークフロー parameters json x 実行するワークフローに渡すパラメータ session_id string ○ 実行したワークフローのsession_id attempt_id string ○ 実行したワークフローのattempt_id parametersをjsonで持つことによってどんなパラメータでも対応できるようにしています。 シーケンス図 ここでは、シーケンス図を元にワークフロースターターの説明します。 1. select dynamic_workflow_config まずは、シーケンス図の通りのクエリで実行するべきワークフローが存在するか確認します。条件に指定しているsession_id,attempt_idについては後述のシーケンスで説明します。 2. execute workflow by Digdag rest api シーケンス1のクエリの結果で実行するべきワークフローがあればDidgagのAPI( PUT /api/attempts ) を利用してワークフローを実行します。リクエストボディに指定するsessionTimeにはstart_atを指定しparamsにparametersを指定しています。sessionTimeにstart_atを指定するのは、2以降のシーケンスで失敗した際にリトライ時のワークフローの重複実行を防ぐためです。これは「sessionTimeはワークフローの履歴で一意になる」というDigdagの仕様を利用するためです。 該当のDigdagの仕様は以下のようになります。 まず、Digdagのワークフローとセッション、アテンプトの関係は下図のようになっています。 ワークフローにおいて、セッションは実行計画を表しており、アテンプトは実際の実行を表します。sessionTimeはセッションが実行される時間を表しており、ワークフロー単位で一意になります。 参考 * sessions-and-attempts * scheduled-execution-and-session-time また、DigdagのAPI( PUT /api/attempts ) は同じsessionTimeを指定した場合は、すでに実行されているアテンプトがレスポンスされます。今回この仕様を利用して2以降のシーケンスで失敗しても、次回実行時や自動リトライを行った際に同じsessionTimeを指定することによって重複実行が行われないようにしました。 3. response session_id attempt_id シーケンス2で実行したsession_id,attempt_idを後続のBigQueryに保存するため保持します。 4. running workflow DigdagのAPIを利用して実行されたワークフローは非同期で実行されています。 5. update dynamic_workflow_config with session_id and attempt_id シーケンス3で取得したsession_id,attempt_idをBigQueryに保存します。ここでdynamic_workflow_configのsession_id,attempt_idに保存することによって次回の実行時にシーケンス1で実行済みと判断されるようにしています。 ワークフローでの工夫 ここでは、実際に開発したワークフローの一部抜粋から工夫した点を紹介します。 ... +start_workflows: +get_dynamic_workflow_config_list: _retry: 5 ... py>: get_dynamic_workflow_config +loop: for_each>: workflow_config: ${workflow_config_list} _parallel: true _do: +execute_workflow_and_update_table: _retry: 5 _export: start_at: ${workflow_config.start_at} project_name: ${workflow_config.project_name} workflow_name: ${workflow_config.workflow_name} parameters: ${workflow_config.parameters} py>: execute_workflow_and_update_table ... +get_dynamic_workflow_config_list このタスクはシーケンス図の1にあたるタスクになります。 +loop このタスクをパラレルで実行することによりシーケンス図2以降が並列で動作するようにしました。理由は、もしシーケンシャルに実行した場合途中で失敗してしまうと後続のワークフローの実行に影響があるためです。 導入結果 もともとの課題だったスケジュール設定の柔軟性とパラメータ定義の柔軟性の課題を、ワークフロースターターを導入したことによって解決できました。これらの課題によって手動実行しないといけなかったり、パラメータ変更のためにdigファイルを変更しリリースするなどの作業がなくなり運用コストをさげることがきました。また、汎用的に利用できる仕組みにできたので、今後ワークフローを設計する時の実行方法の選択肢を増やすことができました。 今後の展望 運用管理ツール CMSやDB部分に関して簡単に実装できる方法を選択しましたが、やはり今後運用が増えていくにあたってちゃんとした管理ツールの準備が必要だと感じております。管理ツールを用意しなかった理由としては、ワークフロースターターをスモールスタートさせたかったためです。実際に導入してから利用するケースが増えてきたので、管理ツール準備の検討を進めていきたいと思います。 スケジュール設定の分散 スケジュール設定が外部のDBとDigdagで分散してしまった点はデメリットになってしまったと感じました。今後、Digdagのスケジューリングされたワークフローの一覧と、ワークフロースターターで実行されるワークフローの一覧をあわせて確認できる仕組みを検討しています。 これは、そもそも現状のDigdagのUIで、スケジューリングされたワークフローの確認などができない課題も一緒に解決したい思いがあります。一覧で確認したい理由は、新しくワークフローを作成する際や依存するシステムを停止させる際に、いつ/どのワークフローが実行されるのかを知りたいときがあるためです。 リトライ方法 今までパラメータが原因でワークフローが失敗した場合、基本的にはdigファイルを修正してリトライする運用をしていました。ですが、今回導入したワークフロースターターで実行したものはAPIでリトライが必要になってしまいました。この課題に関しては、DigdagのCLIのretry commandで少しでも楽ができるように以下のようなPRを提案しております。 github.com まとめ 今回MA部で導入した、動的にワークフローを実行する仕組みを紹介しました。この仕組みを導入したことによって課題だった点が解決され、汎用的に利用できるようにしたため同様の課題を抱えたワークフローに対しても課題を解決できました。また、ワークフローを起動する方法に選択肢を増やすことができたため、とても良い改善を行えたと思っています。 さいごに ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! hrmos.co
はじめに こんにちは。MA部MA施策・運用改善チームの辻岡です。MA部では、ZOZOTOWNのメルマガ・アプリPUSH通知などの配信・分析等の用途で約数十TBのデータを運用しています。今回は長年MAのデータ基盤として利用してきたオンプレDWHをBigQueryに移行したおはなしをします。 この記事はこんな方におすすめ オンプレDWHからBigQuery移行を検討・実施してる方 ジョブ・スケジューラ、ETLツールの移行を検討・実施してる方 概要 オンプレDWHからBigQuery移行する前後の構成イメージを元に、今回の移行の話について概要を説明します。 次の図が移行前の構成図です。オンプレ環境のWindowsサーバ上でジョブ・スケジューリングと実行を基盤処理として、データウェアハウス(以後オンプレDWH)に対してデータ生成や外部システムとの連携をしていました。 今回、以下を目的にオンプレDWHを廃止してBigQueryにデータを集約しました。 分析効率化 更なる発展性の向上 オンプレDWHの運用負荷を下げる その後、移行スコープを検討した結果、オンプレDWHの移行だけではなくオンプレDWHに直接関連するオンプレWindowsサーバ上の処理全てをGCPへ移行することにしました。 既存のオンプレ基盤処理のままオンプレDWHだけ移行した場合、BigQueryへの接続すら既存のライブラリでは困難な状態でした。よって、オンプレDWHの移行だけではなくオンプレ基盤処理を一緒に移行する方が、安全かつ効率的に移行できると考えました。そこで、以下のように移行を進めることにしました。 移行した結果がこちらです。2022年1月に移行しました。 なんということでしょう。オンプレDWHをBigQueryに移行するだけでなく、オンプレ環境をまるっとGCPに移すことでとてもシンプルな構成に様変わりしました。 次は、移行前の課題と解決策についてお話しします。 オンプレDWHについて オンプレDWHはメルマガなど各種配信用のデータやその実績データを格納しており、約10年もの間MAのデータ基盤として利用されてきました。 課題 オンプレDWHでは、社内外含めて当該DWHに関する情報不足かつ独特な運用のため、保守・運用負荷が高い状態でした。さらにクエリの並行数には制限があるため、分析者が気軽にDWHにクエリを実行できない課題がありました。 解決策:BigQueryへ移行 そこで、移行先としてBigQueryを選択しました。著名かつクエリの並列実行が可能な数も(slot数に応じた遅延はあれど)いくらでも増やせるBigQueryは先の課題を解決してくれます。また、全体的なデータ基盤として既にBigQueryが採用されていたため、BigQueryへデータ集約することにしました。 オンプレ基盤処理について オンプレDWHをメインのデータソースとする基盤処理がありました。基盤処理は、Javaを使ったSQLの実行処理やメール配信SaaSへのリクエスト、ファイル生成処理等をオンプレ環境のWindowsサーバ上で行っていました。 課題 オンプレ基盤処理では、古いバージョンのライブラリを使った処理が複数存在し、環境に関しても開発と本番で分離がされていませんでした。また既存のオンプレWindows環境を含めて知見者も不足している状態でした。この基盤でBigQueryへ移行すると接続方法ですらボトルネックになり、開発効率も下げるリスクがありました。さらに、オンプレ環境への接続都合により、リリース時は必ず手動でのデプロイ作業が入っていました。 解決策:Digdag on GKEへ移行 課題の解決策として、GKE上に作成したDigdagへ移行することを選択しました。オンプレ基盤処理でも既にワークフローツールが使われており、既存の基盤をやめるにしても類似の機能が必要でした。MAでは既に別の処理でDigdagを利用しており運用経験や知見が豊富だったこともあり、Digdag on GKEという統一したバッチ処理基盤を作ることにしました。 クラウド移行する場合GCPを使うことは予め決まっていたのですが、GKEを使う事でよりスケールが楽な状態になりました。 例えば、ファイル連携処理の中でそれなりに大きいファイルの文字コード変換や加工によってリソースを消費する処理がありました。ですが、KubernetesCommandExecutorのおかげで他のtaskへの影響はなく、taskに対してpodのリソース設定するだけで問題なく実装できました。他にもたくさん恩恵を受けていますので以下のテックブログをぜひご覧ください。 techblog.zozo.com また、GitHub Actionsを利用してリリースを自動化しました。 移行手順と工夫 ここからは、リプレイスの泥臭い過程をお話しします。技術的な話はあまりありません。これから移行を実施する方の少しでも事例の参考になればよいという目的で書いています。興味のない方は、この後の"今後の展望"あたりまで飛ばすことをお勧めします。 移行は以下のように行いました。 クエリのリファクタリング 解体新書の作成 未知の領域、夜間バッチからスタート アプリケーションのリプレイス実装 クエリ切り替え 移行テーブル洗い出し 進行期間中にあった移行前基盤の過不足の反映 クエリのリファクタリング 2020年の10月辺りから2021年の5月頃まで、ビジネス案件と並行しつつ移行対象のクエリの一部リファクタリングを移行前に行いました。リファクタリングすることにより処理内容を把握できるといったメリットがありました。 解体新書の作成 ドキュメントがなく、処理が複雑でも、リプレイスは既存の処理を必ず知る必要があります。複雑な処理はリプレイスのためだけに既存の処理の流れを書いたドキュメントを作りました。私はこれを「解体新書」と呼んでいました。本物の 解体新書 とは一切関係ありませんが、作成経緯は似たようなものを想像しました。嘘じゃないよという証明を兼ねて、当時のドキュメント件名の一部を載せておきます。 実装前の内部を把握する目的は果たせたので実施効果はあったと考えます。 未知の領域、夜間バッチからスタート 一番最初に手をつけたのは夜間バッチと呼ばれる処理でした。これは、その名の通り夜間に動く処理です。移行前に使っていたジョブ・スケジューラは、GUIでぽちぽち処理フローを作ることができました。歴代の処理注ぎ足し運用により、蜘蛛の巣のようにジョブが連なっている状態でした。念の為モザイクをかけさせてもらいますが、以下の図が移行前の夜間バッチの処理フローの一部です。図のコメントはわかりやすいように追記したものですが、このGUIと中の処理を見ながらDigdagのWFを実装していきました。 夜間バッチから手をつけた理由は2つあります。 夜間バッチに依存している処理が沢山あったから 誰一人全てを知ってる人がいないパンドラの箱状態だったから 1つめは夜間バッチに依存してる処理が沢山あったため、先に作っておくとその後のリプレイスがスムーズになると考えたためです。2つ目は一番先行きの見えないこの処理を移行しておくことで終わりの見通しを良くするためです。 実際に夜間バッチの移行実装後はリプレイスの見通しがよくなり、プロジェクトの進行がスムーズになりました。よって一番見通しがつかないかつ依存関係の多い処理から移行することは、プロジェクトの進行に大きく寄与しました。 手を動かすことで先が見えない状態から、少し先行きが見えてきた状況の変化が大きな進歩でした。とにかく不明点を潰しながら移行作業をすることで、実装速度を上げる効果があったと考えます。 アプリケーションのリプレイス実装 続いて、ジョブ・スケジューラと既存のソースを読みながらDigdagに処理を移行していきました。バッチの各処理の区切り、クエリの区切りを読み取り、taskをなるべく細かく切ってWFを組みました。例えるとひたすら因数分解をするような作業です。リファクタリングの意図もありましたが、因数分解をしないと難読なコードやクエリが多く、ある程度整理したかったことが本意です。BigQueryの実行にはDigdagのbqオペレータとPythonを使いました。この辺りは以前Meetupで紹介した以下の資料に載ってますので興味のある方はご覧ください。 speakerdeck.com オンプレDWHからBigQueryのクエリに切り替え 続いてクエリをオンプレDWHの文法からBigQueryの文法へ書き換えました。 オンプレDWHの文法とBigQueryの文法の互換性に関する公式ドキュメントは存在しました。しかし、利用しているクエリは公式ドキュメントにない記法が随所にありました。そのため簡単な変換ができる場合に限り変換スクリプトで変換しましたが、基本的には1つ1つ手動でクエリを切り替えました。特に以下に留意しながら進めました。 Timezoneについて、オンプレDWHはJST、BigQueryはUTCであることを考慮 Query is too complexエラー回避 テーブル名をアッパーキャメルケースにした 1つめは、BigQueryの既存データが既にUTCで入っていたので合わせてクエリを変えることです。日付チェックが多いので DATE(target_date, 'Asia/Tokyo') のようなJST変換を全てのクエリに対して行いました。2つめは、BigQueryに携わる誰もが遭遇したであろうクエリの複雑性によるエラーの解消です。これは、クエリを細かく刻むことで解消しました。 最後は少し毛色が違いますが、テーブル名を全て大文字(例:USERTABLE)からアッパーキャメルケース(例:UserTable)に変えました。BigQueryはテーブル名の大文字と小文字を区別するため、この機会しかないと踏んで切り替えました。 上記と同等に留意すべき点だったのに漏れてしまったことが1つありました。それは処理が動く時間です。例えば、翌日用の処理が前日の夜に動き前日データを削除したい場合、指定するのは昨日ではなく今日を指定する必要がある、といったケースです。ほとんどの日次データ生成処理は利用当日の深夜に行う処理が担っていたのですが、前日に翌日分のデータを生成する処理は存在し、残念ながらリリース後にその考慮漏れを発見する形となってしまいました。今思えばとても単純な事とはわかりつつ、同じ境遇の方が同じミスをしないように、考慮不足の例も併せて記載しました。 データ移行対象テーブルの洗い出し 続いて、どのデータをどのように移行するのかをまとめたテーブルリストを作成しました。以下はリストの例です。 毎日洗い替えを行うデータの場合はデータ移行不要ですし、積み上げデータの場合はデータ量に応じてデータ移行の時間がかかる可能性を踏まえ、例のようにデータについての特徴を洗い出しました。 移行期間中に行われたオンプレ基盤処理での修正差分を反映 運用によるオンプレ基盤処理の改修は移行期間中も行なっていたため、移行後の基盤に反映しながら進めました。差分反映は二重の実装期間がかかることはもちろんですが、反映漏れやバグのリスクも増えます。リスクをなるべく避けるため、ビジネス部門と交渉し移行期間中はオンプレ基盤処理の改修を最小限に留める調整を実施しました。 移行直後に発生したこと リリース直後に不具合が発見され、その修正対応に約1か月ほど費やしました。処理自体のエラー検知はDigdagが行ってくれますが、データの中身についてはデータチェック処理や各種検知をリプレイス前と同等の仕組みしか入れなかったため不十分な状態でした。また、GUIのジョブ・スケジューラが廃止されたことでビジネス側の運用効率が大きく下がったと報告を受けました。さらに冪等でない箇所があったこととGKEでの運用スタートがあいまって、自部門のアラート対応負荷も上がりました。開発改善を行い、前述の通り稼働後1か月程度でこれらを安定稼働といえる状態までに収束しました。 移行結果 課題は解決したのか 分析効率 アプリケーションと分析用でBigQueryのJob実行プロジェクトを分けることで、アプリケーション処理に影響なく、分析者がクエリを実行できるようになりました。移行前はオンプレDWHを使った分析の代替方法として、夜間にオンプレDWHからBigQueryに連携してBigQueryから分析していました。しかし、データが1日遅れのためリアルタイムな分析ができない状態でした。また、連携が遅延するとさらに分析が遅れるリスクも負っていました。BigQuery移行後はリアルタイムにBigQueryへデータ格納されるようになったので分析効率は上がったと言えます。 発展性 移行した結果データの鮮度が上がり、GCP基盤で基盤処理が動くようになったため、機械学習でのモデル生成時の精度向上や今後さらなるAI活用などの発展性が増えました。 費用 インフラ費用はBigQueryおよびGCP基盤にしたことで年間費用を約50%削減しました。また、固定費用で支払っていた移行前に対して、BigQuery・GCP基盤にしたことで今後さらなるコスト減も見込めます。 保守・運用コスト 以下の理由により、保守・運用コストが下がりました。 環境分離により、本番影響を気にしながら開発作業をする必要がなくなった リプレイスと処理の整理により、コード量を90%以上削ったため可読性が向上 オンプレ環境独特の保守・運用作業がなくなった 拡張性が向上し、やりたかった事が続々とできるようになっている 想定していなかったメリット 解決したかった課題の他にも、いくつか移行メリットがあったのでご紹介します。 タイムトラベル BigQueryには、 タイムトラベル という過去7日間であればデータ復旧可能な機能があります。オンプレDWHの時は、夜間に毎日dumpを取る処理がありましたが、タイムトラベルの機能を使えば過去データは復旧可能と判断し廃止しました。この機能はデータ不備発生時の調査に非常に役に立ちます。またMAで利用しているデータ基盤のBigQueryテーブルについては、データ基盤チームが更に時をいい感じに戻す仕組みを社内で提供してくれており、こちらも調査で使わせてもらっています。時を遡るBigQueryに関するテックブログをデータ基盤チームが出していますので、興味のある方はこちらをご覧ください。 techblog.zozo.com パーティション分割 BigQueryには、 パーティション分割機能 という機能もあります。パーティション項目を指定することでデータ量の大きいデータでもローコスト、ハイパフォーマンスでのクエリ実行を実現できます。オンプレDWHの時は、ある大きなテーブルで分散キーを指定してもクエリが劇的に遅延する事象が発生していました。先の通り情報が不足していたため原因究明には至りませんでした。事象解消のため、dailyのBigQueryへのデータ連携にデータを残しつつ夜間に該当の過去データをオンプレDWHから削除をする処理がありました。オンプレDWHから削除した過去データを全て蓄積してもこういったデータ量起因と思われる謎の遅延は発生せず、パーティション分割項目も有効に働いたため、該当テーブルの過去データ削除処理を廃止しました。 今後の展望 自動テスト・データチェックの強化 今回は自動テストの不足により、移行後に一定日数が経過した後でデータの不備を発見しました。データ不備は検知がなければ何も気付けません。後日各所からの連絡でじんわり気づくことになります。 恒久的にデータチェックやテストコードを充実し、検知を素早く、開発をより安全に効率よく進めるための改善を引き続き進めています。 配信作業の効率化 配信作業はGUIのジョブ・スケジューラを使ってビジネス部門が対応してくれていましたが、GUIによってポチポチできていた作業が、移行後にできなくなったため、配信作業の効率を急激に下げてしまいました。開発改善を重ねてなんとか移行前とほぼ同等の状態まで戻せましたが、移行前よりも拡張性が向上したため、現在はさらなる効率化に向けて改善を進めています。 リファクタリング 機能の必要有無を確認しながら1つ1つ実装を進めたので処理が冗長な状態で残っているため、より最適な設計を検討しつつリファクタリングを進めていきます。 スキーマ管理 移行作業を優先したためBigQueryのスキーマ管理は移行時にスコープから外しました。一部Terraformで管理されていますが、データを洗い替える処理の中で制約が外れてしまうケースもあり、今後最適な方法を検討しつつ進めていきます。 発展性の実現に向けたアプローチ MAのデータを使った機械学習やAI活用がより正確にかつ利用しやすい状態になったり、GCP基盤になったことで出来ることが増えたため、今後の発展性の実現に向けたアプローチを進めていきます。 まとめ 以下、まとめです。これから同じようなチャレンジをする方の少しでもお役に立てれば幸いです。 移行の経緯は分析と運用の効率化 移行スコープはオンプレDWHの関わるオンプレ基盤全てが対象 ドキュメント、着手順、切り替え時の留意点などを考慮して作業を進行 目的達成に加え、さまざまなメリットあり テスト、運用効率化などについて今後の展望あり 記事は以上です。読んでいただきありがとうございました! さいごに MAでは一緒に働く仲間を募集中です。実は今回のリプレイスで、MAの募集要項に記載の利用技術から移行前の技術の記載がごっそりなくなりました。そんな話も踏まえて、MAに興味を持った方は、以下のリンクからぜひ閲覧・ご応募ください! hrmos.co hrmos.co