TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

988

はじめに こんにちは。DevRelブロックの @wiroha です。9月20日に ZOZO Tech Talk #8 - Go を開催しました。ZOZOのエンジニアがGoを利用した開発事例を紹介する、ランチタイムのイベントです。 登壇内容まとめ 弊社から次の2名が登壇しました。 コンテンツ 登壇者 UseCaseの凝集度を高めるGoのpackage戦略 ブランドソリューション開発本部 バックエンド部 / 田村誠基 事業成長を加速させるGoのコード品質改善の取り組み ブランドソリューション開発本部 バックエンド部 / 田島太一 当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com UseCaseの凝集度を高めるGoのpackage戦略 ブランドソリューション開発本部 バックエンド部 / 田村誠基 speakerdeck.com 田村からはショップスタッフの販売サポートツール「FAANS」における改善を発表しました。これまでの構成は依存関係が増加しているという問題や、似た命名のstructがあることで見通しにくくなっているという課題がありました。パッケージを細かく分割することでこれらの問題点が解決されたそうです。実際試して良かったところ、気になるところも紹介しました。 事業成長を加速させるGoのコード品質改善の取り組み ブランドソリューション開発本部 バックエンド部 / 田島太一 speakerdeck.com 田島からはGoのコード品質改善のために取り組んだことを5つ紹介しました。徐々に厳しくするLinter設定、スタイルガイド「Google Go Style Guide」の導入、エラーハンドリングの改善、凝集度を高める実装パターン、ボーイスカウトルールによる既存コードの継続的改善の5つです。詳細はYouTubeやスライド資料をご覧ください。 最後に 質疑応答の様子 それぞれの発表の後には質疑応答も行いました。多くのご質問ありがとうございました! 皆さまの開発のヒントになっていれば幸いです。 ZOZOではGoを活用し、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、SRE部 検索基盤SREブロックの花房と大澤です。普段はZOZOTOWNの検索関連マイクロサービスのインフラ運用を担当しています。 ZOZOTOWNの検索基盤では、商品検索に関わる大規模なデータを取り扱うためにElasticsearchを利用しています。Elasticsearchを運用していく中で、私たちはパフォーマンスとインフラコスト、運用トイルの問題に直面していました。本記事では、私たちが抱えていた問題と、それを解決したアプローチとしてシャーディング最適化とオートスケーラー開発の取り組みについてご紹介します。 目次 はじめに 目次 背景・課題 パフォーマンスの課題 インフラコストの課題 運用トイルの課題 解決策 シャーディング最適化 Elasticsearchのシャーディング ノードのインスタンスタイプ変更 負荷試験によるパフォーマンス検証 コスト見積 安全なリリース方法 導入効果 オートスケーラー開発 オートスケール方針の検討 オートスケーラーの設計と開発 夜間のスケールインと日中のスケールアウト 売り上げ予想に基づくノードサイズ(メモリサイズ)の算出 (1). 過去のデータの取得 (2). 最大リクエスト量の予測 (3). 必要ノード数(メモリサイズ)の算出 PRの自動生成とマージ ノード拡張(terraform applyの実行) 拡張監視 導入効果 おわりに 背景・課題 ZOZOTOWNでは、2017年からレガシーシステムのリプレイスを実施しています。レガシーシステムの刷新の中で、検索基盤においてはElasticsearchの導入・運用を開始しました。リプレイスの進展や、おすすめ順検索の利用拡大に伴い、検索基盤へのトラフィックは増大し、Elasticsearchへの負荷も増加していきました。しかし、Elasticsearchのインフラ運用の知見は十分に溜まっておらず、パフォーマンス・インフラコスト・運用トイルの3つの側面で課題が発生しました。 最初に、それら3つの側面の課題について紹介いたします。 パフォーマンスの課題 検索基盤では、ZOZOTOWNの商品情報の更新のために、Elasticsearchへのデータ投入を毎分実行しています。この高頻度のデータ投入と、マイクロサービス側での大量の検索リクエストにより、クラスタ全体のCPU負荷は常に高い状態でした。ノード数を増やしても処理速度の向上は見られず、さらに負荷が増大すると、レスポンスやインデキシングが遅れ、求めるパフォーマンスを維持できなくなります。そのため、負荷増加を招く可能性のある新たな施策には挑戦しにくい状態になっていました。 インフラコストの課題 検索基盤のElasticsearchは、Elastic社が提供するElastic Cloud上で稼働しています。フルマネージドなElastic Cloudではノードの稼働時間にコストが比例します。当然のように思えますが、自前で管理するElastic Cloud on Kubernetes (ECK) 1 の場合、利用量でなくライセンスの最大量で契約を結ぶ必要があるため、ノード数を下げてもライセンスの料金の節約には繋がりません。ZOZOTOWNの場合、時間帯や日別のイベントによりトラフィックが大きく変化します。ノード数の変動が大きい弊社では、コストカットができるようECKではなくElastic Cloudを採用しています。 リクエスト量は時間帯によって大きく異なるため、それに応じてリソースを最適化できれば余計なコストを減らせます。以下の図は、時間ごとの検索リクエスト数のグラフです。横軸が時間、縦軸が秒間リクエスト数を示しています。 Elastic Cloudにはディスク利用量トリガーによるオートスケール機能は存在しますが、リクエストやCPU負荷に応じてスケールする仕組みは存在しません。そのため、リクエストが少ない時間帯(深夜、日中など)でも、常にピークタイムのリクエストを処理できるノード数を準備して対応していました。これによりZOZOTOWNのインフラの中でも、特に膨大なインフラコストが発生しており、そのコスト削減が課題になっていました。 運用トイルの課題 ZOZOTOWNは、土日の午後9時頃にかけてリクエスト数が最も多くなる傾向を持っています。ピークである土日のリクエスト数を捌けるノード数を常時抱えておくのはコストが勿体ないため、週末のみスケールアウトする運用にしています。この運用では、土日の売上予算からリソース見積を算出する作業と、Elasticsearchのスケールアウト作業を手動で行っていました。作業はテンプレート化されていますが、運用トイルとなっている状況が問題でした。下記のスクリーンショットは、その作業のGitHub Issueであり、チームメンバー内の輪番により毎週対応していました。 解決策 パフォーマンス課題の解決案としては、シャーディングによる負荷分散を試しました。シャーディングの詳細については後述します。さらに、ノードをCPUコア数の多いマシンへ変更することも検証しました。これによりCPUの余力を作り、シャーディングの効果を一層引き出せると考えたためです。 運用トイルの課題に対しては、ZOZOTOWNの売上予測をベースに必要なリソース量を算出し、Elasticsearchを自動スケールさせるような独自の仕組みを開発しようと考えました。既存のツールでは、私たちの運用に合うオートスケーラーが存在しなかったためです。 パフォーマンス課題と運用トイル課題の解決は、結果的にインフラコストの課題解決にも繋がりました。以降では、3つの課題の解決策となったシャーディング最適化とオートスケーラー開発の詳細についてご紹介します。 シャーディング最適化 Elasticsearchのシャーディング シャーディングとは、インデックスを分割して複数のノードに保持させることです。分割されたデータをシャードと呼びます。下記にノードとシャード、レプリカの関係を図で示します。ノード数6、シャード数4、レプリカ数2の場合の例です。 CPUリソースに余裕がある場合、複数ノードでの並列処理により、クエリのレイテンシは改善します。各ノードで処理するデータがシャーディングにより分割されたデータ量に絞られるためです。 2 ただし、並列処理によりクラスタ全体のCPU利用は増加し、各ノードの処理結果をマージするオーバーヘッドも増加するため、スループットの悪化を招いたり、さらにレイテンシ悪化の可能性もあります。 インデックスのシャード数は、インデックス作成時に下記のパラメータを指定することで設定できます。 { " settings ": { " index " : { " number_of_shards ": 4 } } } このパラメータはインデックスごとに個別での設定が必要です。さらに、後からの変更は不可能であり、変更が必要な場合はインデックスを再作成する必要があります。今回の検証では、2シャードと4シャードの設定を試しました。 ノードのインスタンスタイプ変更 シャーディングの効果を引き出すためにはCPUリソースの余裕が必要です。そのため、ノードのインスタンスタイプをCPUコア数の多いマシンへ変更することにしました。 ノードのインスタンスタイプは、Elastic Cloudの管理画面から設定できます。管理画面には「hardware profile」という項目が存在し、そこでは「General purpose」や「CPU optimized」といった選択肢があります。この設定変更により、用途に合わせたリソースのインスタンスタイプの利用が可能です。私たちはAWSをクラスタのクラウドプロバイダとして選択しているため、ノードにはAWSのインスタンスタイプが適用されます。 今回のインスタンスタイプ変更では、「hardware profile」を「General purpose」から「CPU optimized」に変更しました。これにより、以前適用されていたm5dインスタンスから、c6gdインスタンスへの変更が行われます。私たちはTerraformコードにより適用しました。下記に、弊社で利用しているTerraformコードの例を示します。 resource "ec_deployment" "zozo_tech_blog" { region = "ap-northeast-1" version = "7.17.0" deployment_template_id = "aws-cpu-optimized-arm-v6" # この部分を変更 name = "ZOZO TECH BLOG" elasticsearch { autoscale = "false" topology { id = "hot_content" size = "60g" zone_count = "3" size_resource = "memory" } topology { id = "master" size = "8g" zone_count = "3" size_resource = "memory" } } } m5dとc6gdのCPUの比較は下記の通りです。 m5d c6gd vCPUコア数 16 32 プロセッサ Intel製 ArmベースのAWS Graviton2 実は、以前にもインスタンスタイプ変更によるパフォーマンス向上を検証したことがあります。その際もm5dからc6gdへの切り替えを検証しました。検証の結果、99パーセンタイルのレイテンシが100msほど悪化したため、この変更は本番環境にリリースできませんでした。当時はインデックスが1シャード構成であり、CPUコア数が多くても効果的な並列活用ができず、CPU1コアのパフォーマンスがレイテンシに直接影響したためと考えました。 最終的に、インスタンスタイプ変更によるCPUの2倍確保で書き込みと読み込みが相互影響しにくい状態にした上で、さらに余ったCPUを効率良く利用できるようにシャーディングを行う狙いになりました。 負荷試験によるパフォーマンス検証 負荷試験では、本番環境と同じ構成のステージング環境を利用します。今回はElasticsearchの検証を実施するため、ステージング環境に対して下記を設定しました。 ステージング環境のアプリケーションを検証対象のElasticsearchクラスタに接続 アプリケーション側のキャッシュを無効化し、Elasticsearchに負荷がかかるように設定 アプリケーションのリソースだけでなく、Elasticsearchへのインデキシングも本番環境と同様に動作する状態に変更 負荷試験におけるアプリケーションへのリクエスト数はElasticsearchのノード数に応じて変化させ、下記の条件の組み合わせで試験を実施しました。 3 負荷試験の実行では、弊社で開発しているOSSであるGatling Operatorを活用しています。Gatling Operatorについては こちらの記事 をご参照ください。 条件 内容 インスタンスタイプ c6gd インデックスのシャード数 2, 4 Elasticsearchのノード数 3 ~ 年間最大トラフィック想定の台数 リクエスト数(req/sec) 50 ~ 年間最大トラフィック 今回の負荷試験では、主にアプリケーション側の99パーセンタイルのレイテンシと、Elasticsearchのスループット、インデキシングバッチの処理時間の3つに焦点を当てています。また、4シャードの結果が2シャードよりも良好であったため、これまでの構成と4シャードの新構成で比較しました。下記は年間最大トラフィックでの比較結果です。 項目 1シャード x m5dインスタンス 4シャード x c6gdインスタンス 99パーセンタイルレイテンシ 670ms 370ms 1ノードあたりのスループット 40 query/sec 40 query/sec インデキシングバッチ処理時間 120分 60分 比較結果から、新構成はスループットを維持したまま、2つの項目では優れていることが確認できました。それぞれの項目について、考察を下記にまとめます。 99パーセンタイルレイテンシ シャーディングにより4つのノードに処理が分散されたことで、レスポンスを結合するオーバーヘッドを含めても、処理時間を短縮できた結果だと考えています。 1ノードあたりのスループット シャード数が4に増えた分、各シャードへのクエリ回数は4倍に増加するためスループットは減少しそうですが、今回はスループットを維持できています。これはインスタンスタイプ変更によるCPUコア数の増加の効果だと考えています。 インデキシングバッチ処理時間 新構成では旧構成と比較してCPUが2倍になったため、書き込みと読み込みが別々のCPUで処理されることが多くなり、書き込みでCPUを占有できるようになった結果、処理時間が短縮したと考えています。反対に、旧構成ではCPUの数が少なく、1つのCPUで書き込みと読み込みの両方を担うことが多かったため、処理に時間がかかっていたと考えています。また、シャードが分かれたことにより、インデキシングを並列で実行できるようなった効果も大きいと考えています。 コスト見積 リリースの判断を下すためには、インフラコストの見積が必要です。最終的なコストの詳細は記載できませんが、各要素について説明します。 1ノードのインスタンスサイズについては、メモリ容量で決定しています。Elastic Cloudのインスタンスは、基本的に他のユーザとリソースを共有する環境です。私たちのElasticsearchノードと、他のユーザのノードが同じインスタンス上に配置される可能性があります。その場合、リソースを取り合う形になってしまうため、安定した性能が発揮できないこともあります。しかし、一定以上のメモリサイズを指定することで、占有リソースのインスタンスが利用可能です。そのため、私たちはメモリサイズが60GBのインスタンスを「Hot data and Content tier」として使用しています。このサイズのノードについて、今回比較したインスタンスタイプの料金は2023年9月時点でそれぞれ以下の通りです。 m5d c6gd 1時間あたりのコスト $3.648 $4.452 コスト見積では、過去のリクエスト数データと負荷試験の結果を元に必要なノード数を計算し、Elastic Cloudの料金情報に基づいてコストを算出しました。1時間あたりのコストの計算方法は下記の通りです。 ( 検索APIのリクエストによって発生するElasticsearchへのクエリ量[query/sec] / ノード1台のスループット[query/sec] ) x ( ノード1台の1時間あたりのコスト[$] x ドル円為替[¥/$] ) x バッファ 下記2パターンについて、以前の構成と比較した際のコスト削減率を記載しています。 コスト算出パターン 以前の構成と比較した際のコスト削減率 最もリクエスト数が多い日のコスト -20% 年間コスト -35% 新構成では1台あたりのインスタンスコストが上がってしまうため、全体のコストも上がるように思います。しかし、レイテンシに関してはガードレール指標を定めており、レイテンシの改善分をガードレール指標まで落とすことでスループットを増やせました。その結果、ノード数を削減してコスト削減が実現できました。 安全なリリース方法 負荷試験結果から新構成のパフォーマンス、コストに問題がないことは分かりました。しかし、本番環境にリリースしてユーザトラフィックに晒した時、想定外のエラーやパフォーマンス悪化といったリスクが発生しないとは言い切れません。そのため、カナリアリリースによる安全なリリースを実施します。カナリアリリースは手動ではなく、 以前の記事 でご紹介した、Flaggerを用いたプログレッシブデリバリーにより実行しました。Flaggerはプログレッシブデリバリーを実現するKubernetes Operatorであり、検索基盤のマイクロサービスには導入済みです。 旧構成から新構成に切り替える方法は2つ考えられました。 同じクラスタ内でインデックスを作成し、エイリアスを切り替える案 旧クラスタと同じデータを持つ新クラスタを作成し、アプリケーション側から接続するElasticsearchのエンドポイントを切り替える案 シャーディングのみ適用すると切り替え期間は2つのインデックスへの書き込みが必要になり、CPUリソース不足になる可能性があります。また、インスタンス変更のみ適用するとレイテンシ悪化の可能性があります。そのため、今回は両方を同時に適用できる2つ目の方法を選択しました。 エンドポイントの切り替えは、Kubernetes上の検索APIの deployment リソースに記載している環境変数の更新により行います。また、新クラスタへの接続情報を格納する secret の作成と、それを参照するよう変更も行いました。これらの更新により、Flaggerが新クラスタへ接続する検索APIのカナリアバージョンを作成し、カナリアリリースを自動で進めてくれます。エラーの多発やレイテンシの悪化が発生した場合は、Flaggerにより自動的に元のクラスタを向いているバージョンに切り戻されるため、安全なリリースが実現できました。下記はプログレッシブデリバリーがKubernetes上でどのように実施されるかを表した図です。 リリースまでの手順は以下の通りです。まず検証環境で本手順通りにリリースの予行演習をし、問題がないことを確認しました。その後、本番環境で本手順通りにリリースしました。 新クラスタの構築 新旧2つのクラスタへのインデキシングを開始 プログレッシブデリバリーでのクラスタ切り替え 旧クラスタへのインデキシングの停止 導入効果 シャーディング最適化による導入効果を改めて下記にまとめます。 分散処理による、検索リクエストのレイテンシおよびインデキシング処理時間の改善 スループット向上によるコストカット 本番環境のレイテンシには変動があるため、1か月ほど様子を見た上で改善されたと判断しました。改善幅は負荷試験時ほどではありませんでしたが、99パーセンタイルレイテンシは平均で約150ms改善されました。本番環境と負荷試験との主な違いは、サイト内セールの影響や、それに伴う検索リクエスト種類の割合です。これらの要因や、コスト削減のためにレイテンシを落としてスループットを上げたことにより、負荷試験時とは異なる結果になったと考えています。インデキシングバッチについては、約4.5時間かかっていた処理が半分以下の約1.7時間で完了するようになりました。さらに、トイル削減にも繋がっており、セールイベント時に行っていたインデキシングバッチのチューニングが不要になりました。 シャーディング最適化によって、パフォーマンスとインフラコストの課題について解決できました。以前はCPUに余裕がなく断念せざるを得なかったパーソナライズに関する施策にも挑戦できるようになりました。 オートスケーラー開発 前述の通りインフラコスト・運用トイルを改善するためのアプローチとして、日中夜間・平日休日に適したリクエスト量を見積もり、かつ適切なノード数へ自動変更する手法を模索しました。この章ではSREチーム独自の仕組みとしてオートスケーラーを開発するに至った経緯と、その概要についてご紹介します。 オートスケール方針の検討 SREチームではElastic Cloud上でElasticsearchクラスタを運用しています。可能であれば公式に提供されている機能を利用したいところです。しかしながら、現在のところElastic Cloudよりスケジュール設定やCPU使用量、リクエスト量に応じたノード数変更の仕組みは提供されていません。そのため、実現方法としては下記の2つを検討しました。 既存のOSSを利用する 独自の仕組みとして開発する 既存のOSSを利用する方法として、 elastic-cloud-autoscaler を検討しました。このOSSは、Elastic CloudのAPIを通してスケジュールやクラスタ負荷に応じたノード数へ変更できます。導入にあたって検証を進めたところ、SREチームでの運用に不適ないくつかの課題が確認できました。 以前の記事 にて紹介のとおり、SREチームではTerraformによるコード管理にてElasticsearchクラスタを運用しています。これによりクラスタの状態とリリースブランチのコードの状態を常に一致させるという安定した運用基盤を構築しています。 OSSのオートスケーラーによる運用では、Terraformのコードとクラスタの状態との間に差分が生じてしまうことが最大の問題点でした。OSSのオートスケーラーはコードの状態を考慮せずにノードのスケーリングを実施します。この振る舞いによりコード差分が生じ、緊急時にTerraformでクラスタの構成を変更しようとすると、予期せぬ問題やエラー発生の可能性があります。また、本OSSで提供されているスケジュール設定についても、私たちの運用には合っていないことが分かりました。ZOZOTOWNでは、日によってセールなどの各種要因によりElasticsearchへ流入するリクエスト量が異なります。そのため、適切なノード数も日によって異なり、シンプルなスケジュール設定は合わなかったのです。 さらに、Elastic Cloudからは将来的にサーバーレスアーキテクチャが提供される予定もあり、オートスケーラーは一時的な運用となる可能性もあります。これらの点を加味し、SREチームの運用に適した、独自の仕組みを持つオートスケーラーを導入する方針が固まりました。以下がSREチームで検討した独自オートスケーラーの要件です。 クラスタの状態はTerraformのコードで管理する (Single Source of Truth) クーポンやイベント、広告により日々異なる売り上げ予想から、日々適切なノード数を自動で見積り、自動でスケールを行う 既存の仕組みを利用し、最小限の実装でオートスケールを実現する Elasticsearch以外に、Kubernetesのpod数変更や検索ドメイン以外でも利用できるような汎用的な作りにする オートスケーラーの設計と開発 SREチームの現在のクラスタ運用は、GitHub Actions (GHA) のCI/CDにより実施しています。リリースブランチへのPRマージをトリガーにGHAが起動し、 terraform apply を実行することでクラスタの状態とコードの状態を一致させています。この運用方法をベースに、最小限の実装でオートスケーリングを実現するため、GHAで新しいワークフローを構築しました。以下がワークフローの概要になります。左が今回新しく構築したオートスケール全体を担うワークフロー、右がPRマージ時にCI/CDで動作する既存のワークフローです。 大きく以下の要素で構成されています。 夜間のスケールインと日中の段階的なスケールアウト 売上予算に基づく必要ノード数(メモリサイズ)の算出 PRの自動生成とマージ ノード拡張(terraform applyの実行) 拡張監視 各要素のポイントを以降で説明します。 夜間のスケールインと日中のスケールアウト 時間帯で異なるトラフィック傾向に合わせて、以下のようにGHAのcronを設定しています。 9時 : 昼休みの時間帯のトラフィック増に備えてピーク時リソースの8割に増加 16時 : ゴールデンタイムのトラフィック増に備えてピーク時リソースの10割に増加 25時 : トラフィックのピークを過ぎたためピーク時リソースの6割に減少 売り上げ予想に基づくノードサイズ(メモリサイズ)の算出 Pythonスクリプトにて毎日の売上予算を基に、オートスケール実施日のリクエスト量を予測し最適なノード数を算出しています。以下は算出プロセスの詳細です。 (1). 過去のデータの取得 過去N日分の売上予算と最大リクエスト量を取得します。SREチームではElasticsearchの各種メトリクス情報をDatadogに送信しているため、DatadogのAPIを利用して過去N日分の最大リクエストを取得します。 (2). 最大リクエスト量の予測 取得したN日分の過去のデータをもとに、重回帰分析を実施し当日の売上予算から最大リクエスト量を予測します。 def calculate_trend (self, y_values, x_values, target): x = np.array(x_values) # x軸:過去の売上予算 y = np.array(y_values) # y軸:過去のリクエスト量 # 傾きと切片を計算 slope, intercept, _, _, _ = linregress(x, y) # 売上予算に対するリクエスト量を計算 trend_value = intercept + slope * target # target:当日の売上予算 return trend_value (3). 必要ノード数(メモリサイズ)の算出 terraformによるノード数の変更はメモリサイズ単位で行います。そのため、予測された最大リクエスト量をもとに、必要となるノード数を計算し必要メモリサイズへ変換します。 PRの自動生成とマージ クラスタを構成するterraformファイルには以下のようにタグ # {autoscaling} を設定しています。shellコマンドでこのタグが設定された行を検出し、算出したメモリサイズへファイルを更新します。 # タグを設定したterraformファイル topology { id = "hot_content" size = "900g" # {autoscaling} zone_count = "3" size_resource = "memory" } # sedコマンドによるタグ検出とファイル更新例 esfile=terraform/elastic_cloud/es-cluster.tf sed -i '/# {autoscaling}/s/size = "[0-9]*g"/size = "${{ env.ESTIMATE_SIZE }}g"/' ${esfile} ファイル更新後はリリースブランチへPRを作成し自動でマージしています。 ノード拡張(terraform applyの実行) リリースブランチへのPRマージを機に既存のCI/CDプロセスがトリガされ、クラスタの状態が最新のコードに一致するように更新されます。 拡張監視 監視方法として、Elasticsearchのヘルスチェックエンドポイントを叩き、クラスターステータス、Unassigned Shards数を監視します。なお、スケールインの失敗はサービスに最悪の影響を及ぼさないため許容できます。しかしながら、スケールアウトの失敗はピークタイム時のサービスに重大な影響を及ぼすため、万一の失敗時は即時対応が必要となります。そのため、スケールアウト時に一定時間経ってもクラスターステータスが正常にならない場合や、ワークフロー実行中にエラーが発生した場合にはPagerDutyでオンコールする仕組みを導入しています。 導入効果 オートスケーラーの導入により以下の効果が得られました。 インフラコスト削減:適切なリソース管理により、オートスケール導入前と比較し月額コストが20%改善しました。 運用トイル削減:輪番担当の際の負荷見積りやノード数調整、監視にかかる時間が無くなり、作業負荷が軽減されました。 これらの効果により、サービスの運用管理がスムーズになり、より価値のある開発や改善に時間とリソースを割くことが可能になりました。また、今回は記載を省略しましたが、Elasticsearchのノード数スケールと同様の方法で、検索APIのpod(minReplicas設定)についてもオートスケールの対象にしています。 おわりに 本記事では、Elasticsearchを運用していく中で私たちが直面した問題と、その問題をシャーディング最適化とオートスケーラー開発によって解決する過程をご紹介しました。 今回の取り組みによって、Elasticsearchのパフォーマンス、インフラコスト、運用トイルの改善を進めることができました。Elasticsearchの運用で同様の課題を抱えている方がいれば、ぜひ参考にしてみてください。 今後は、シャーディングのさらなる最適化とオートスケーラーのブラッシュアップ、その他にもワークロードに合わせたクラスタの分割などの施策を進め、改善を継続していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co https://github.com/elastic/cloud-on-k8s ↩ シャーディングにより、デフォルトでは単語頻度の計算がシャードごとになります。全てのシャードから収集した情報で、グローバルな単語頻度を計算させるには、 search_type として dfs_query_then_fetch を指定する必要があります。 https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-type ↩ クラスタの拡張縮退を繰り返す際はマスターノードのリソースを増やしておくことを推奨します。シャード再配置に失敗し、クラスタが利用不可になる可能性があるためです。 ↩
はじめに こんにちは。DevRelブロックの @wiroha です。9月11日に After iOSDC Japan 2023 を開催しました。9月1日〜3日に開催されたiOSDC Japan 2023の協賛企業であるLINE株式会社、PayPay株式会社、株式会社ZOZO、ヤフー株式会社の4社合同での振り返りイベントです。 登壇内容まとめ 4社の社員による発表の後、パネルディスカッションを行いました! コンテンツ 登壇者 風レーダーを支える技術 冨田 悠斗 / ヤフー株式会社 15分でお伝え!iOSDC Japan 2023におけるZOZOの取り組み 加藤 祥真 / 株式会社ZOZO パフォーマンスモニタリングの取り組み かしはら / PayPay株式会社 LINEアプリのサポートバージョンの考え方 富家 将己 / LINE株式会社 Q&A & パネルディスカッション giginet, 長谷川 健, 中岡 黎, Shota Kashihara 当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com 風レーダーを支える技術 冨田 悠斗 / ヤフー株式会社(撮影:ヤフー株式会社 たなたつさま) www.docswell.com 冨田さまからは風が視覚的にわかる「風レーダー」を実装するための技術についての発表がされました。風レーダーについては動画がわかりやすいため、ぜひYouTubeのアーカイブをご覧ください。風レーダーはオーバーレイをしている情報が2つあったり、アニメーションをしていたりと複雑な画面でした。その実装にはMetalというグラフィックAPIを使っているそうです。難しそうな印象を持ちやすい部分ですが、意外と簡単に実装ができるそうでコードも含めて紹介していました。風レーダーの描画でのみMetalを使い、計算部分はSwiftで行うことで、低いコストで運用できるようにしているのは良い工夫だと感じました。 15分でお伝え!iOSDC Japan 2023におけるZOZOの取り組み 加藤 祥真 / 株式会社ZOZO 弊社の加藤は初のLTということで、会場からはあたたかい拍手が送られていました。スポンサーブースの取り組みの紹介と、登壇までの流れ・サポート体制を発表しました。現地には10名以上の社員が参加し、ブースに来てくださった皆さまとお話をしました。会場ではMiroを使ってアンケートを行い「Vision Pro買う?」「これ勉強してます!」「みんなと話したいこと」などをお聞きしました。回答の詳細は iOSDC Japan 2023参加レポートブログ をぜひ読んでみてください! CfPのネタだし&ネタレビュー会のドキュメントも一部紹介しました。チームでのレビューやDevRelブロックによるレビューを経て、採択されるようにプロポーザルを改善します。「サポートが手厚い」「自信を持って話せそう」とたくさんの反響をいただきました! パフォーマンスモニタリングの取り組み かしはら / PayPay株式会社(撮影:LINE株式会社 佐藤さま) かしはらさまからは、パフォーマンスの継続的なモニタリングについて発表がありました。ホーム画面のバーコード表示を例として、計測を導入するフローが丁寧に説明されました。実際に取り組む中でFirebase Performance Monitoringでは足りない部分に気付き、Looker Studioを使うようにしたのはリアルな知見だなと思いました。実際に改善したホーム画面のBefore・Afterの比較はとてもわかりやすかったです。 LINEアプリのサポートバージョンの考え方 富家 将己 / LINE株式会社(撮影:LINE株式会社 佐藤さま) speakerdeck.com 古いOSバージョンをサポートするメリット・デメリットや、対応方法などを整理して発表いただきました。どこまでサポートするかはみなさん悩む問題ですよね。LINEのサポートバージョンと、それぞれのユーザシェアを公開いただけるのは助かります。しかし、LINEは全世界のOSシェアを監視しており、サポート終了のタイミングは遅めなので参考にするのはおすすめしないそうです。日本のリージョンはアップデート率が高いので、自身のサービスのシェアを確認して判断するのが良いとのことでした。 Q&A & パネルディスカッション 質問1. どのセッションが面白かった? まずは乾杯をしてQ&A & パネルディスカッションがスタートしました。ひとつめの質問は「どのセッションが面白かった?」です。パネリストがフリップを使って紹介していきました。当日見切れなかった分の動画を見返すヒントになりますね。 質問2. オンライン or オフラインどちらで参加した?(撮影:ヤフー株式会社 水田さま) ふたつめの質問は「オンライン or オフラインどちらで参加した?」です。オンラインではセッションが見やすく、子育てをしながらでも見られるのが助かるという意見がありました。 質問3. イベントの企画で楽しかったこと 「イベントの企画で楽しかったこと」は3名から「LTのペンライト」という意見が出ており、とても好評でした。今回の新しい取り組みでインパクトがありましたね。「ご飯」「スポンサーブース」もあげられていました。ZOZOのブースでARメイクを試して楽しかった、ノベルティの手鏡が家族に喜ばれた、と言っていただきとても光栄です! 質問4. 自社の取り組み(撮影:ヤフー株式会社 水田さま) 「自社の取り組み」としてはCfPをたくさん出す、CfPレビュー、協賛、スポンサーセッションなどがあげられました。各人はじめての挑戦も見られ、カンファレンスはとても良い機会だなと感じました。最後にiOSDC Japan 2024への抱負を語り、パネルディスカッションは終了しました。パネルディスカッションの後、オフライン会場では交流会も行いました。 最後に オフライン・オンラインともに多くのご参加ありがとうございました。iOSDC Japan 2023の余韻を楽しみつつ、新しい知見も学べる場となったのではないでしょうか。今後もiOSに関するイベントを開催していきますのでご期待ください! ZOZOでは一緒にサービスを作り上げてくれるiOSエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ECプラットフォーム部会員基盤ブロックの turbofish です。弊社ではモノリスのプログラムで動いているZOZOTOWNをマイクロサービス化する取り組みを行なっており、複数チームが1つの大きなオンプレシステムをマイクロサービスでリプレイスしています。その中で私が所属する会員基盤ブロックでは、ZOZOTOWNの会員情報を管理するマイクロサービスを開発しています。 本記事では、弊チームを含む複数のマイクロサービス開発チームにおいて、既存のアプリケーションの一部をマイクロサービスを使用する処理に置き換えた際、サービス無停止でオンプレ環境にあるDBからマイクロサービスが使用するクラウド環境のDBにデータを移行した戦略を紹介します。 ディスクレイマー 本記事で紹介するデータ移行方法には下記の制約があり、全ての状況に対応できるわけではありません。 DBへの書き込み処理と読み取りの処理の実装リリースタイミングを分けられない場合は実行できません プライマリキーを持たず、テーブル内のレコードを一意に識別できない場合はデータの正しさを保証できません また、この記事で紹介する方法は、手間をかけてでもデータ移行の際にサービス停止を発生させないことにフォーカスしています。同様の戦略を検討する際は、データ移行を実行する環境において全ての過程を実現できるか、サービス停止を回避するためにどの程度の手間をかけられるかなどを考慮の上、状況に応じた戦略を決定する必要があります。 本記事において、随所で「オンプレ」「クラウド」という単語を用いていますが、サーバーの場所は本記事で紹介する戦略に影響しません。あくまで本記事に登場する複数のDBを見分けるための名前だと考えてください。 目次 はじめに ディスクレイマー 目次 データ移行の背景と前提 データ移行の要件と課題 採用した戦略 戦略決定の背景 具体的な移行手順 1. データ調査と(必要に応じて)データ修正 2. DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正 3. データ移行手順のテスト実行 4. オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納 5. 一時DBに格納されたデータをクラウドDBへ格納 6. クラウドDBに保存されたデータの検証 7. データを参照する処理を、マイクロサービスAPIを使用するよう修正 工夫した点 移行元と移行先のDBのレコードのID(プライマリキー)が同じ値になるようにする データ移行前後でマイクロサービスAPIの実装を変える データ格納のロジック実装時に、データの削除方法を考慮する データ格納後のデータ検証のやり方 データ移行結果 データ移行にかかった時間 苦労した点 まとめ データ移行の背景と前提 ZOZOTOWNのマイクロサービス化の背景については、過去の記事をご覧ください。 techblog.zozo.com 弊社では各マイクロサービスが専用のDBを持ち、リプレイスを通じてDBMSとDBスキーマを変更したいと考えていました。そのため、マイクロサービスを使用する実装をリリースする際に、オンプレ環境で使用しているDBからマイクロサービスで使用するクラウド環境にあるDBへ、データをコピーする必要がありました。 以降、オンプレ環境で使用しているデータ移行元のDBを「オンプレDB」、クラウド環境にあるマイクロサービスで使用しているデータ移行先のDBを「クラウドDB」と表記します。また、オンプレDBからデータを抽出し、クラウドDBへ格納するETL処理のことを「データ移行」と呼びます。 本来、安全にデータを移行するためには、一旦サービスを停止してDBに書き込みがされない状態にしてからデータをコピーすることが最も確実です。特に会員基盤チームでは、ユーザーの個人情報を含むデータを扱うため、データの欠損や不整合は許されませんでした。しかし、ZOZOTOWNは非常にアクセスが多く、サービス停止による機会損失が大きいことから、多少の手間を許容してでもデータ移行に伴うサービス停止を無くす方法を考える必要がありました。 データ移行の要件と課題 弊社のマイクロサービス化のプロジェクトに共通する、データ移行の要件は下記の通りです。 移行元と移行先のDBMSの種類が異なる オンプレDBはMicrosoft SQL Server、クラウドDBはAmazon Aurora MySQLを使用 移行元と移行先のDBでスキーマが異なる 移行元のDBは長い歴史を経て最適な状態ではなくなっており、マイクロサービス側のDBでテーブルを新設する際にスキーマを再設計する必要がある データ移行1回につき、扱うデータ量は大体の場合多くても3千万件程度 ダウンタイムなしでデータを移行する必要がある 上記の要件を考慮し方針を検討したところ、下記の課題がありました。 DBの種類やテーブルスキーマが違うため、レプリケーションによって複製したDBをそのまま用いることができない 移行過程でテーブルのスキーマ変更に対応するため、柔軟なデータ変換を実現する必要がある マイクロサービス化を推進するためにも、移行元であるオンプレDB依存なアーキテクチャにはしたくない CDC(Change Data Capture)のような仕組みは極力採用したくない 採用した戦略 単純にDBやデータをコピーするだけでは、データの正しさとサービス無停止を両立することは難しいと考えられました。そこで、下記の通りアプリケーションレイヤーで工夫することで、上述した課題を全てクリアできると考えました。 オンプレDBに書き込みを行う処理を修正し、オンプレとクラウドの両DBにアトミックに書き込みを行うよう実装する データ移行でオンプレDBのデータをクラウドDBにコピー。この時、アプリケーションはオンプレDBのデータを参照している状態 オンプレDBのデータを参照している処理を修正し、マイクロサービスAPIからデータを取得するよう実装する 戦略決定の背景 データを移行してからマイクロサービスAPIを使用するアプリケーションの実装をリリースすると、データ抽出の開始時から実装リリースまでの間の時間に発生するデータ変更処理がクラウドDBに反映されません。つまり、サービス停止をせずにデータの正しさを担保するためには、アプリケーション実装リリースの後にデータを移行する必要がありました。 一方で、その場合アプリケーション実装リリース時にはクラウドDBに全くデータが存在しない状態となります。そのため、マイクロサービスのデータを参照せず、DBへの書き込み処理のみをオンプレとクラウド両方のシステムで行われるよう実装することで、サービスにクラウドDBの状態を表出させることなくデータ移行の時間を確保しようと考えました。正しくデータがDBに書き込まれる状態にしてからデータを移行することで、両方のDBに正しく全てのデータが揃っている状態を実現します。最後にクラウドDBのデータを参照する実装をリリースし、データ移行の過程は完了します。 具体的な移行手順 データ移行のために実行した手順をまとめると、下記の通りです。 データ調査と(必要に応じて)データ修正 DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正 データ移行手順のテスト実行 オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納 一時DBに格納されたデータをクラウドDBへ格納 クラウドDBに保存されたデータの検証 データを参照する処理を、マイクロサービスAPIを使用するよう修正 以下、それぞれの過程を具体的に説明します。 1. データ調査と(必要に応じて)データ修正 オンプレDBとクラウドDBのスキーマが異なる場合、もしくはクラウドDBに格納するデータが移行元のオンプレDBと異なる場合は、データを抽出した後にどうやって変換するかを検討する必要があります。オンプレDBの既存データが、マイクロサービスAPIで受け付けられる状態になっているかという観点で調査しました。例えば、マイクロサービスAPIが電話番号や郵便番号に全角文字や数字以外の文字を許容しない場合は、既存のデータにそれらが含まれていないか。enumのような特定の選択肢を期待しているデータに例外がないか、移行先のクラウドDBのカラムサイズを超えるデータがないかなどを確認しました。 データを変換する必要がある場合は、下記のどちらかを検討する必要があります。 オンプレDBに存在するデータをクラウドDBのスキーマに合わせて修正する 後述するデータ変換・データ格納のプロセスでデータを変換する 2. DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正 データを移行する前に、DBに書き込みを行う全ての処理においてオンプレとクラウドの両DBがアトミックに更新されることを担保します。アプリケーションコードにおいて、1つのトランザクション内でオンプレDBへの書き込み処理とマイクロサービスAPIの処理を行い、オンプレDBとクラウドDBの間でデータの整合性を取ります。この、「オンプレDBとクラウドDBに書き込む処理をアトミックに実行する」ことを、本記事では「ダブルライト」と呼びます。 ダブルライトの実装がリリースされた時点での、DB書き込みを伴うユーザー操作の処理を図示すると下記のようになります。 ダブルライトの具体的なフローの例として、会員情報を登録する処理は下図のようになります。フロー図はイメージが湧きやすいよう実際にチームで使用している図と近いものを使用していますが、マイクロサービスAPIを叩く際に必ず通過するAPI Gatewayについては、この記事の範疇を超えるため他の図では省略しています。 注意点として、この時クラウドDBに当たる会員基盤DBにはデータが何もないため、マイクロサービスAPIは、更新・削除APIにレコードが存在しないIDを指定したリクエストが来たとしても、オンプレDBと整合性のとれたデータが保存されるように実装する必要があります(詳細は「工夫した点」として後述します)。 3. データ移行手順のテスト実行 データ移行に失敗するとDBに何度も高い負荷をかけることになるので、本番環境での作業ミスを防止するため、本番環境での移行前に練習としてデータ移行のテスト実行を行いました。弊チームでは、実際にDBにどの程度の負荷がかかるのかを計測するため、より本番環境に近いデータを保持するステージング環境を使用しました。データ修正実行後に本番環境のデータを(個人情報をマスクして)ステージング環境にコピーし、ダブルライトの実装を全ての環境にリリースしたのち、ETL処理をテスト実行しました。 4. オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納 ダブルライトの実装をデータ移行に先立ってリリースすることにより、データが正しくDBに書き込まれることを担保したら、いよいよオンプレDBのデータをクラウドDBに移行するETL処理を実行します。本記事で紹介する手順では、データのETL処理をいくつかのツールを用いて実行します。データ抽出&変換(ET)にEmbulkというツールを使用し、データ格納(L)についてはGo言語でツールを自作しました。それら2つを、KubernetesのJobで実行しました。 データ移行中にもユーザー操作によるDBへの書き込み処理が継続していることを考えると、単純に移行元のオンプレDBのデータを全て抽出して移行先のクラウドDBに挿入するだけでは、要件を満たすことができません。そのため、まずはクラウド環境にデータ移行のための専用のDBを作成し(以降、このDBのことを「一時DB」と表記)、オンプレDBのデータを抽出して保存することにしました。 以降において、「データ抽出」はオンプレDBのデータを全て抽出すること、「データ変換」は抽出したデータをクラウドDB互換のスキーマに変換し、一時DBに保存することを指します。 データ抽出&変換には、社内での使用実績もある Embulk を使用しました。Embulkは、移行元のDBからselectしてデータを抽出し、YAMLで書かれた設定ファイルに沿ってテーブルを作成し、データを変換して挿入することが可能です。プラグインを用いて異なる種類のDB間での移行も可能で、弊チームの要件である、SQL ServerからMySQLへのETLに対応したプラグインもありました。Embulkの設定ファイルにデータ格納時のクエリを設定できるため、シンプルなロジックであれば、テーブル構造の差分を解決できました。例えば、移行元のDBにある日本語の文字列を移行先では英語のEnumで扱いたいといった場合には、Embulkが使用するクエリ中にswitch文を記載することでデータを変換してくれます。SQL文での実現が難しい、より複雑なロジックでデータを変換する必要がある場合は、この次に説明するデータ格納の際に使用する自作ツールでデータを変換するロジックを実装し、データ格納時に変換できます。 この時点の状況を整理すると、一時DBのデータはアプリケーションからアクセスされないため、データ抽出開始より後に発生したユーザー操作によるDBへの書き込み処理は反映されません。一方で、クラウドDBには常に書き込み処理が行われている状態です。 5. 一時DBに格納されたデータをクラウドDBへ格納 一時DBからクラウドDBへデータを格納する処理を、本記事では「データ格納」と呼びます。 既にダブルライトの処理が本番環境にリリースされているため、クラウドDBは随時ユーザー操作に伴いデータが更新される状態になっています。つまり、一時DBにはデータ移行プロセス開始以前の状態のデータが更新されない状態で保存されていて、クラウドDBにはデータ移行プロセス開始後の最新のデータだけが保存されている状態です。 そこで、すでにクラウドDBに存在する最新のデータは更新せずに、一時DBに保存されているデータをクラウドDBに格納するツールを自作することにしました。 データ格納ツールは、基本的には一時DBのレコードを1件ずつクラウドDBに挿入します。但し、既に最新のデータが存在するDBに更新されていない状態のDBのデータを格納するため、既存のツールではカバーできないやや柔軟な機能が必要でした。データ格納処理において、クラウドDBと一時DBに同じIDのレコードが存在した場合、データ抽出後にデータ更新処理が行われた可能性が高いと考えられます。そのため、アプリケーションからクラウドDBに直接書き込まれたデータは上書きせず、そのまま処理を継続しました。クラウドDBにレコードがすでに存在した場合は、念のためそのレコードがデータ抽出開始より後に作成または更新されていることを確認しました。 格納の処理は1,000レコードずつのバッチ処理で行いました。IDが重複した場合にエラーで処理が止まらないよう、複数レコードの同時挿入に失敗した時には重複したIDのレコードを避けて、成功するレコードのみ再度挿入するような実装にしました。 6. クラウドDBに保存されたデータの検証 移行元のDBと移行先のDBのデータに齟齬がないことを確認します。データ格納ツールの実行完了後に、データを検証する自作ツールを実行しました(詳細は「工夫した点」として後述します)。 7. データを参照する処理を、マイクロサービスAPIを使用するよう修正 クラウドDBがオンプレDBと同様のデータを保持した状態であることを確認したら、DBのデータを参照する処理にマイクロサービスAPIを使用する実装をリリースできます。リリース後の処理フローは下図のようになります。 工夫した点 移行元と移行先のDBのレコードのID(プライマリキー)が同じ値になるようにする データのETL処理が正しく行われていることを検証するため、プライマリキーでデータを識別し、移行元と移行先のDBに保存されているレコードを比較します。そのため、新規にレコードを追加する処理においては、オンプレDBで発行されたIDをマイクロサービスAPIに渡し、両方のDBでプライマリキーを一致させるようにしました。必然的に、いずれオンプレDBへの書き込みをせずにマイクロサービスAPIのみを使用するよう実装する際、レコードを追加するマイクロサービスAPIを修正する必要があります。 データ移行前後でマイクロサービスAPIの実装を変える クラウド本番DBにデータが存在しない状態でマイクロサービスAPIがリリースされるため、APIはデータ更新もしくは削除処理の際にデータが存在しなくても、404エラーを返さないようにしておく必要があります。つまり、APIの実装としては、更新処理でもレコードが存在しなければ登録する、UPSERTのような処理を行うようにしました。マイクロサービスAPIにおいてDBにデータが存在しない場合にエラーレスポンスを返すようにするためには、データ移行後にハンドリングを入れた実装をリリースする必要があります。 データ格納のロジック実装時に、データの削除方法を考慮する データ格納ロジックは、データのCRUDを行うアプリケーションロジック、テーブルを跨ぐデータの関係などに影響を受けます。特に注意する必要があるのは、DBへのデータ挿入、更新、削除のうち削除の処理です。データを物理削除するか論理削除するかによって、対応方法が変わります。 削除パターンが論理削除の場合 論理削除フラグをもつテーブルの場合は、データ格納において特別なロジックを追加する必要はありません。一方、削除済みデータ用のテーブルを持つタイプのデータの場合は、必要な全てのテーブルについてETL処理を行う必要があり、データ格納用のツールの仕様も複雑になります。メインのデータを保存するテーブルにあるレコードの状態が一時DBのレコードと異なる場合に、削除済みのデータが保存された別のテーブルをチェックするよう実装する必要があります。全てのデータ更新をクラウド環境内で検知できるため、オンプレ環境からの移行を考えている場合には物理削除のパターンよりもこちらの方が比較的実装は楽かもしれません。 削除パターンが物理削除の場合 物理削除されるデータの場合は、データ抽出開始後に削除されたデータのIDをDBから確認できません。そのため、一旦一時DBの全てのデータを格納してから、削除されるべきレコードのIDを特定してデータ格納完了後に削除する必要がありました。 データ移行期間中に発生する削除処理の回数が少ないケースでは、データ抽出開始〜データ格納完了の間に処理した削除リクエストのログからIDを抽出し、クラウドDBに直接SQLを流して削除しました。 短時間に大量に削除リクエストが発行されるアプリケーションの場合、一時的に削除されたレコードを保存する別テーブルを作成し、APIの削除処理を論理削除を行う実装にした上で、データ移行が完了したら物理削除を行う実装に修正するなどの方法も考えられます。 データ格納後のデータ検証のやり方 弊社では、移行元のオンプレDBへのアクセスを多くのマイクロサービスから行いたくなかったため、オンプレDBとクラウドDBのデータを直接突合させるツールを作成できませんでした。そこで、マイクロサービスが保持する一時DBとクラウドDBのデータに齟齬がないかをまずは確認し、齟齬があった場合はログを出力して後で調査しました。データ抽出開始より後に作成もしくは更新されたと考えられるデータの場合は、スプレッドシートなどを用いてオンプレDBのデータと突合しました。物理削除された可能性があるデータの場合は、一時DBに保存されているIDを出力し、アプリケーションログにて削除処理が行われたことを確認しました。もし検証プログラムが移行元と移行先の両DBにアクセスできるのであれば、それぞれのDBのレコードが全て一致していることを確認するだけでクラウドDBに正しくデータが移行されたことを確認できます。 データ移行結果 結果として、この戦略は要件を全て満たす状態で成功し、ノウハウが社内で共有され複数のチームで採用されるようになりました。会員基盤チーム以外の事例も含めて、移行にかかった時間や手間などについて紹介します。 データ移行にかかった時間 約3千万件のデータ移行で、データ抽出&変換は40分、データ格納は2時間程度で完了し、ETL処理の工程は1日あれば全て完了しました。一方で、イベントを記録する場合などデータ量がより多くなる場合には、数億件のデータを1週間かけて移行した事例もありました。データ移行にかかる時間は、DBのバージョンやスペック、データの複雑さ、レコードの件数と大きさ、移行中にDBにかかる負荷など、複数の要因に影響を受けます。 ※ 移行対象のデータの数はデータ移行の対象となったテーブルの行数を示しており、ZOZOTOWNの会員数もしくは会員情報の総数などを表すものではありません。 苦労した点 データ移行の前にダブルライトの処理をリリースすることにより、それぞれの過程で考えることが非常に多くなり、自作ツール開発などの追加作業が発生しました。加えて、マイクロサービスAPIを使用する実装のリリースタイミングを2段階設けたことにより、リリース前に行う必要があるテストを複数回実施する必要があり、これも工数が増えた要因でした。特にダブルライトのテストは画面上からデータを確認できないため、操作時に入力したデータをスプレッドシートに記録しておき、オンプレとクラウドそれぞれのDBのデータと突合する必要がありました。 ノウハウが溜まっていない時期には、考慮漏れにより全ての工程をやり直したこともありました。データ調査を怠ってデータ変換に失敗したり、データ検証の際にダブルライトの実装が漏れていたことに気づいたこともありました。前者のデータ修正の追加対応を行なったケースでは、オンプレDBのデータ調査、データ修正ののち、データ移行の工程を全てやり直し、結局データ移行を開始してから完了するまでに10時間弱かかりました。 また、データ移行のやり方がデータの性質によっても大きく変わってくるため、データの修正など準備にかける手間が非常に大きくなることもありました。例えば、プライマリキーを持たないテーブルに新たにプライマリキーを作成した上でデータを移行し、移行元のDBの修正処理を含めると合計5日ほど時間がかかったこともありました。 まとめ 本記事では、データ移行の前にデータ移行元と移行先の両方のDBにアトミックに書き込む実装をリリースすることで、サービスを停止させずにデータを移行する戦略について説明しました。本記事で紹介した戦略は、単純なデータコピーやデータレプリケーションと比較すると大幅に手間がかかります。しかし、サービス無停止でのデータ移行の成功例ができたことにより、機会損失を出すことなく複数のチームでマイクロサービス化のプロジェクトを進めることができるようになりました。マイクロサービス化の過程でサービス停止を伴わないデータ移行を検討している方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
こんにちは。検索基盤部の橘です。検索基盤部では、ZOZOTOWNのおすすめ順検索の品質向上を目指し、機械学習モデル等を活用しフィルタリングやリランキングによる検索結果の並び順の改善に取り組んでいます。 最近行った並び順の精度改善の取り組みについては以下の記事をご参照ください。 techblog.zozo.com また、検索基盤部では新しい改善や機能を導入する前に、A/Bテストを行い効果を評価しています。A/Bテストの内容や分析の自動化への取り組みについては以下の記事をご覧ください。 techblog.zozo.com 検索基盤部ではA/Bテストの事前評価として、オフラインの定量評価と定性評価を実施しています。特に定量評価は、並び順の精度改善の仮説検証を迅速に行う手段として有効です。 しかし、ZOZOTOWNのおすすめ順検索の商品ランキングロジックの1つであるフィルタリング処理についてはこれまで明確な定量評価の指標がなく、評価が難しく改善を進めることが困難な状況でした。本記事では、この課題に焦点を当て、特に定量評価の指標を決定するアプローチについて詳細に記載していきます。 目次 目次 ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 フィルタリング処理における定量評価 A/Bテストにおける定量評価の位置付けと目的 検索結果の並び順のロジックを定量評価するには 導入した評価指標 『精度の維持度』の評価指標について 『多様性度』の評価指標について 『類似性』の評価指標について ケンドールの順位相関係数の難点 RBO(rank biased overlap) まとめ おわりに ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 ZOZOTOWNのおすすめ順検索の商品のランキングロジックは2つのフェーズに分けられています。 フィルタリング処理 :再現率を高めることを目的にルールベースのロジックや軽量な機械学習モデルを用いて商品のフィルタリングを行います。 リランキング処理 :フィルタ時のスコア結果トップN件に絞って「ランキング学習」と呼ばれる手法の機械学習モデルを用いた並び替え処理を行なっています。 以上のリランキング処理された並び順を検索結果に使用しています。 フィルタリング処理によりどのような商品群がリランキングされるのかが決まるので、フィルタリングの精度は最終的な検索結果に大きく影響を及ぼすことがわかります。続いて、このフィルタリング処理の精度の定量評価について記載していきます。 フィルタリング処理における定量評価 A/Bテストにおける定量評価の位置付けと目的 以下にA/Bテスト実施までのフローを示します。 定量評価及び定性評価は、事前評価としてA/Bテストの前に実施します。 事前評価を実施する目的として、以下が挙げられます。 『不要なA/Bテスト施策の排除』 :A/Bテストは社内の人的リソースを多く必要とします。明確に改善効果がない、または悪化する可能性がある施策をA/Bテスト前に排除することで、リソースの無駄を防ぎます。 『リスクの低減』 :致命的なエラーをA/Bテスト前に回避します。 『施策の内容理解』 :関係者が施策の内容や意図をより深く理解できるようにします。 この記事で取り上げる定量評価は主に1の不要なA/Bテスト施策の排除を目的としています。以下では、その精度の改善効果を確認するための評価方法や指標について詳細に記載していきます。 検索結果の並び順のロジックを定量評価するには 検索エンジンは、入力された検索キーワードに対して組み込まれたロジックに基づき商品を並び替えて出力します。定量評価では、入力された検索キーワードに対する新旧ロジックの検索結果の並び順を比較することによって新旧ロジックの良し悪しを評価・判断します。 以下に「リバーシブルパーカー」で検索した場合の新旧ロジックの比較イメージを示します。 導入した評価指標 フィルタリング処理における『良い検索結果の並び順のロジック』は、具体的に何が期待されるのでしょうか? 一般的な検索結果の並び順のロジックの良し悪しを評価する手法として、過去のユーザー行動ログを基に「正解データ」を作成し、その正解データとロジックの検索結果を比較してnDCGなどの指標で評価します。 しかし、ランキングの指標をそのままフィルタリング処理の評価指標として用いるのは必ずしも適切とは言えません。フィルタリング処理に関しては以下2点を考慮する必要があります。 正解データは旧ロジックによる検索結果が基になっていることにより、旧ロジックの精度が過大評価される可能性(historical bias)がある。 フィルタリング処理の検索結果は後のリランキング処理により並び替えられるので、正解データでフィルタリング処理時点での並び順の良し悪しを評価する必要性は低い。 これらの点を踏まえ、フィルタリング処理における良い検索結果の並び順のロジックを 新ロジックは旧ロジックの精度を保ちつつ、旧ロジックとは明確な差異を持ち、異なる商品を数多く表示できるもの と定義しました。 以上の定義を基に、以下の3つが確認できるような定量評価の指標を導入しました。 『精度の維持度』 :新ロジックは旧ロジックと同等の(もしくはそれ以上の)精度を有しているか。 『多様性度』 :新ロジックが、旧ロジックとは異なる商品を多く表示できるか。 『類似度』 :新ロジックと旧ロジックの検索結果の並び順がどの程度類似しているか。 以下にそれぞれの評価指標について詳細に説明していきます。 『精度の維持度』の評価指標について 精度の維持度の指標として コンバージョンカバー率 を導入しました。具体的には、新旧ロジック検索結果の上位N件の商品の過去のコンバージョン数を比較し、新ロジックが旧ロジックの数値をどれだけカバーしているかを割合で表します。 ※検索結果の上位N件は後のリランキング処理で並び替えを行う件数に該当します。 コンバージョンカバー率をベン図の部分で表すと以下のようになります。 本指標を使ってどのように新ロジックの良し悪しを判断するかを説明します。 ケース1ではコンバージョンカバー率が20%となっています。これは新ロジックの検索結果の上位N件のコンバージョン数が旧ロジックのそれの20%しかカバーしていないことを意味します。 ケース2ではコンバージョンカバー率が95%となっています。これは新ロジックの検索結果の上位N件のコンバージョン数が旧ロジックのそれの95%をカバーしていることを意味します。 ケース2はケース1と比較しコンバージョンカバー率が大きいので、新ロジックの検索結果の上位に過去ユーザーにとってコンバージョンしやすい商品が多く含まれていると解釈できます。つまり、コンバージョンカバー率が大きいほど新ロジックは旧ロジックと同等の検索精度を持っていると期待できます。 『多様性度』の評価指標について もし新ロジックの精度が旧ロジックの精度を維持していたとしても、新旧ロジックの検索結果が殆ど同等だった場合はA/Bテストをする必要性は低くなってしまいます。このようなロジックを除外できるように、旧ロジックとは異なる新しい検索結果をどれだけ取り入れているのかの指標が必要です。 これを踏まえ、多様性度の指標として 新表示商品率 を導入しました。具体的には、新ロジックで検索結果の上位N件に表示された商品の中で、旧ロジックで検索結果の上位N件に表示されない商品の割合で表します。 該当部分をベン図の部分で表すと以下のようになります。 先ほどの指標を使ってどのように新ロジックの良し悪しを判断するかを説明します。 ケース3では新表示商品率が50%になっています。これは新ロジックが旧ロジックに比べて新しい検索結果を50%も増やせていることを意味します。 ケース4では新表示商品率が5%になっています。これは新ロジックが旧ロジックに比べて新しい検索結果を5%しか増やせていないことを意味します。 ケース3はケース4と比較し新表示商品率が大きいので、新ロジックの検索結果の上位には旧ロジックとは異なる新しい検索結果が多く含まれていると解釈できます。 『類似性』の評価指標について 『多様性度』と同様に、新旧ロジックで検索結果の並び順がほぼ類似している場合、A/Bテストをする必要性は低くなってしまいます。このようなロジックも除外できるようにする必要があります。 検索結果の並び順の類似性を評価する際の代表的な指標としてスピアマンの順位相関係数、ケンドールの順位相関係数、RBO(rank biased overlap)などがあります。 指標を選定する際、おすすめ順検索のフィルタリング処理における検索結果の並び順は「2つの検索結果の並び順は互いに含まれていない商品を含んでいる場合がある」点を考慮する必要がありました。 以上を考慮する場合、RBOは使うことができますが、スピアマンの順位相関係数とケンドールの順位相関係数は扱いが難しいです。 以下にコードを踏まえながら、上記の難点を説明します。 ケンドールの順位相関係数の難点 まず、ケンドールの順位相関係数を挙げて上記の難点について説明します。ケンドールの基本的な説明や算出方法は Wikipediaの内容 をご参照ください。 簡単なPythonコードでケンドールの順位相関係数の計算例を示します。ここではscipyの kendalltau ライブラリを使います。 以下は両リストが同じ商品を含む場合のコードです。 !pip install scipy import numpy as np from scipy.stats import kendalltau # 2つの検索結果の並び順 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品E' ] search_result_2 = [ '商品A' , '商品B' , '商品E' , '商品C' , '商品D' ] # 2つの検索結果に含まれる商品をリスト化 all_goods = sorted ( list ( set (search_result_1 + search_result_2))) # 商品リスト内の商品に対して並び順の順位をつける関数 def assign_ranks (results, all_contents): return [results.index(c) for c in all_contents] ranked_1 = assign_ranks(search_result_1,all_goods) ranked_2 = assign_ranks(search_result_2,all_goods) # ケンドールの順位相関係数を計算 tau, _ = kendalltau(ranked_1, ranked_2) # 結果を出力 print ( "Kendall's tau:" , tau) >> Kendall 's tau: 0.6 ケンドールの順位相関係数をこのような場合に適用するときは、検索結果の並び順を基に商品リスト内の商品に対して並び順の順位をつけるようにし、それらを入力とし値を計算します。ケンドールの順位相関係数の値は-1から1の値をとり、値が低いほど負の相関、値が高いほど正の相関が高いことを示します。 この例の場合ケンドールの順位相関係数の値は0.6となります。 次に、異なる商品を含む場合のコードです。 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品hoge' ] search_result_2 = [ '商品A' , '商品B' , '商品fuga' , '商品C' , '商品D' ] 商品hogeと商品fugaという両リストに異なる商品が含まれています。この場合、これらの商品の順位をどのように扱えば良いのかという問題があります。 一般的には、含まれていない商品を無視する意図で順位をNaNに置き換える方法があります。 def assign_ranks (results, all_contents): return [results.index(c) if c in results else np.nan for c in all_contents] NaNを含む順位のデータを扱う場合、kendalltauの引数nan_policyを'omit'にすることで順位のデータ中のNaNを無視できます。 tau, _ = kendalltau(ranked_1, ranked_2, nan_policy = 'omit' ) print ( "Kendall's tau:" , tau) >> Kendall 's tau: 1.0 しかし、この場合のケンドールの順位相関係数の値は1.0と出力されてしまい、先ほどの互いに同じ商品を含んだ場合と比較しても違和感のある結果となってしまいます。 他にも様々な前処理方法が考えられますが、前処理方法により出力の値が変わってしまいます。よって、ケンドールの順位相関係数は、2つの検索結果の並び順において両リストに異なる商品を含んでいる場合に扱いが難しいと分かります。 RBO(rank biased overlap) 続いて、RBOを用いた場合について説明します。 RBOの値は0から1の値をとり、値が高いほど互いの並び順が類似していることを示します。RBOの特徴として、2つの検索結果の並び順の上位の商品が異なっていた場合に値を大きく減衰させます。 一般的に検索結果の並び順は上位の商品がより重視されるので、検索結果の並び順における類似性の評価指標として採用しやすい指標といえます。 RBOについての詳細はこれに関する論文である A Similarity Measure for Indefinite Rankings をご参照ください。 簡単なPythonコードでRBOの計算例を示します。ここでは rbo ライブラリを使います。 以下は2つの検索結果の並び順が互いに同じ商品を含んだ場合です。 !pip install rbo import rbo # 2つの検索結果の並び順 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品E' ] search_result_2 = [ '商品A' , '商品B' , '商品E' , '商品C' , '商品D' ] # RBOを計算する value_rbo = rbo.RankingSimilarity(search_result_1, search_result_2).rbo() print ( "RBO:" , value_rbo) >> RBO: 0.88 この例の場合、RBOの値は0.88となります。 次に、互いに含まれていない商品が存在する場合です。 search_result_1 = [ '商品A' , '商品B' , '商品C' , '商品D' , '商品hoge' ] search_result_2 = [ '商品A' , '商品B' , '商品fuga' , '商品C' , '商品D' ] ... print ( "RBO:" , value_rbo) >> RBO: 0.41 この例の場合、RBOの値は0.41となります。互いに同じ商品を含んだ場合と比較し値が低くなっているので違和感のない値になっています。 RBOは互いの並び順にあるリスト内の共通商品数をベースに算出する手法のため、互いに含まれていない商品が存在する場合でも違和感のない結果を出すことができます。 以上を踏まえて『類似性』の評価の指標としてはRBOを導入しました。 まとめ 本記事ではZOZOTOWNのおすすめ順検索の精度改善における定量評価及びその指標決定のアプローチを紹介しました。定量評価の指標が整ったことで、A/Bテスト前に精度改善の検証が迅速にできるようになり、よりA/Bテストの実施頻度の向上が期待されます。 今後は施策を重ねていく中で、これまで紹介した指標のブラッシュアップや新規指標を追加していく予定です。また、A/Bテストの負担軽減のため、これらの指標値をより迅速に算出できるような仕組みの構築も進めていく必要があります。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
こんにちは、ZOZOTOWN開発本部ZOZOTOWNアプリ部の らぷらぷ です。先日9/1から9/3までの3日間、iOSDC Japan 2023が開催されました。弊社からは3名が登壇し、プラチナスポンサーとして協賛してブースを構え、10名以上がスタッフメンバーとして参加しました。 technote.zozo.com この記事では今年のiOSDCで登壇した3名の発表と弊社のスポンサーブースについてお伝えします。 登壇内容の紹介 マウスポインターを掴む?! 〜標準フレームワークで作る非接触でMacを操作する技術〜 WWDC Labsは怖くない。 Labsの準備とコツ、完全公開します 続・全力疾走中でも使えるストップウォッチアプリを作る 〜LiDARを使った精度への挑戦〜 スポンサーブース アンケート結果 Vision Pro買う? 今からアプリ作るなら何を選ぶ? これ勉強しています! チームは何人体制? これがないと始まらない!手放せない開発ツールは? 実はあれのコミッター、コントリビューターなんです 聞いてくれ!嬉しかった開発エピソード 掲示板 ~みんなと話したいこと~ 最後に 登壇内容の紹介 今年のiOSDCではDay1で1人、Day2で2人登壇しました。早速紹介していきましょう。 マウスポインターを掴む?! 〜標準フレームワークで作る非接触でMacを操作する技術〜 中岡黎 @rei_nakaoka / 計測プラットフォーム開発本部 計測アプリ部 iOSブロック speakerdeck.com 中岡からはVision Framework、Speech Framework、Core Motion Frameworkを駆使してMacの画面上の操作を実現する手法をデモを交えて発表しました。手や顔の動きでマウスカーソルを操作し、セリフからショートカットキーを入力する様子に、会場からは拍手や「おぉ〜」という声も上がりました。 WWDC Labsは怖くない。 Labsの準備とコツ、完全公開します 小松悟 @tosh_3 / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 iOS1ブロック speakerdeck.com 小松からは2020年から毎年参加し続けているWWDC Labsに臨む上での壁とその乗り越え方について発表しました。時差の壁に英語の壁、そしてもうひとつの壁のお話。この発表を聞いて来年はWWDC Labsに参加してみようかなと思っていただける方が増えると嬉しいです。 続・全力疾走中でも使えるストップウォッチアプリを作る 〜LiDARを使った精度への挑戦〜 荻野隼 @juginon / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 iOS2ブロック speakerdeck.com 荻野からは去年のiOSDC Japan 2022の続編として高精度なストップウォッチアプリの制作と実験結果について発表しました。ARKitとLiDARを活用したストップウォッチアプリは陸上競技歴16年の彼の全力疾走を精度高く計測できるのか。会場からの笑いもあり、テンポの良さもあって全力疾走感のある発表でした。 スポンサーブース ここからは弊社のスポンサーブースについてご紹介します。5月下旬から準備を進めて、各プロダクトのiOSエンジニアやDevRelブロックの方を始めとする多くの方にご協力頂いてブースのコンテンツやノベルティを用意できました。 ブース全体 ディスプレイにはアンケートや会社紹介をまとめたMiroを表示していました。アンケートは3日間で200人以上の方に答えていただきました。みなさまご回答頂きありがとうございました! 詳しいアンケート結果はこの後紹介します。 Miro全体図 今年のノベルティはステッカーやボールペンなど多種用意しました。 かわいいステッカーたち こちらはZOZOGLASSと手鏡になります。手鏡はZOZOTOWNの ARメイク を体験していただいた方に特別にお渡ししていました。 手鏡はスタッフの案から採用されました ARメイクデモ中 手書き広告で宣伝中 そして例年存在感のあるZOZOSUITを着たマネキンも健在です。 ZOZOGLASSもバッチリ アンケート結果 みなさんにお答え頂いたアンケート内容を見てみましょう。 Vision Pro買う? 今年のWWDCでお披露目されたVision Proですが、自腹でも買いたいという方が結構いらっしゃいました。会社や研究室で買っていただけると嬉しいと望まれてる方も多いですね。国内でVision Proがどのように流行っていくのか楽しみです。 今からアプリ作るなら何を選ぶ? iOSDCに来られるみなさまはSwiftで書く方が多いのか、はたまたReact NativeやFlutterに代表されるクロスプラットフォームやその他の方法を取られる方が多いのか聞いてみました。やはりSwiftを選ばれる方が多いですね。Flutterは個人開発やハッカソンなら使ってみたいという方もいらっしゃいました。 これ勉強しています! SwiftやSwiftUIにSwift Concurrency、Core MLやCore BluetoothなどのFrameworkといったiOSエンジニアなら気になるトピックが上がっていますね。他にもAndroidやFlutterにも興味を持っている方、英語をしっかり勉強したい方、ソフトウェアテストに関心のある方、マネジメントや会計にマーケティングと幅広くお答えいただきました。 チームは何人体制? 企業で開発されている方に個人開発の方、友達と開発している学生の方などにお答えいただきました。弊社のプロダクトチームも合わせて掲載しております。お答え頂いた中で1番大きいのは20人以上のチームという回答でした。 これがないと始まらない!手放せない開発ツールは? 普段の開発に欠かせないツールと聞かれて思い浮かべるものをお答えいただきました。無料ツールだとやはりXcodeが多いですね。Shiftltはウィンドウ位置やサイズをショートカットキーで調整できるツールです。こちらのツールとは別ですが私も似たようなツールを使っています。有料ツールだとChatGPTを挙げる方が多かったです。 お話いただいた方の中には、何か作りたいものがあるけど実現方法が分からないときはChatGPTに質問して回答を出してもらい、エラーなく動作するまで修正する方もいらっしゃいました。他の方は日々の開発にどう役立てているのか気になるところです。 実はあれのコミッター、コントリビューターなんです OSS活動をしている方はどれくらいいらっしゃるのか聞いてみました。弊社にもOSS活動をしている社員がおり、私もその一人です。ブースに訪れた方から「OSS活動って難しそう」というお話を頂き、そういったハードルの越え方をお話しさせていただきました。 聞いてくれ!嬉しかった開発エピソード こちらは質問ではなく、ブースに訪れた方から最近嬉しかった開発エピソードを披露していただきました。技術的な話だけではなくサービス改善やブログ発信など、聞いていて笑顔になるお話をたくさん頂けました。 掲示板 ~みんなと話したいこと~ こちらはブースのお客さんが会場の人に聞いてみたいことを挙げる場になっています。Vision Proで作ってみたいアプリを語って思いを馳せたり、SwiftUIとUIKitの併用の難しさを語って「そうだよね」と頷いたり、みなさん思い思いに話してくれました。 先の開発エピソードとこちらの掲示板は、「iOSDCに来ているお客さんと一緒に盛り上がりたいな」というスタッフの想いから生まれました。 最後に お客さんや他の企業ブースの方ともiOSの話で盛り上がり、オフラインの会場の体験を堪能できたiOSDC Japan 2023でした。このような素晴らしい場を提供してくれた運営さんに感謝しつつ、来年のiOSDCでも多くの方とワイワイ盛り上がれることを楽しみにしています。 ZOZOでは、来年のiOSDC Japanを一緒に盛り上げるエンジニアを募集しています。ご興味のある方はこちらからご応募ください。 hrmos.co
はじめに こんにちは、ZOZOMO部OMOバックエンドブロックの杉田です。普段は Fulfillment by ZOZO (以下、FBZ)が提供するAPIシステムを開発・運用しています。 FBZでは、昨年からビルドの高速化や自動デプロイをはじめとしたCI/CDパイプラインの最適化に取り組んできました。本記事では、それらの取り組みの詳細とその効果についてご紹介します。 目次 はじめに 目次 FBZにおけるCI/CDと構成管理の現状 リリースサイクルの見直し リリースまでの流れ 顕在化した課題 長時間のデプロイ ビルド環境のメンテナンス性の低さ 手動デプロイが抱える人為的なリスク CI/CDパイプラインの改善に向けて デプロイフローの見直し CodeBuildのバッチビルド タスク定義の実装例 個々のタスクを高速化 Direct deploymentsの有効化 devDependenciesの依存解決 デプロイフローを改善した効果 手動運用からの卒業 GitHub Actionsの活用 タグの生成 ビルド環境の自動アップデート 自動デプロイ 新生CI/CDパイプライン さいごに FBZにおけるCI/CDと構成管理の現状 FBZでは、 GitHub Actions (以下、GHA)と AWS CodeBuild (以下、CodeBuild)を用いてCI/CDパイプラインを構築しています。 利用用途などは、それぞれ以下の表の通りです。 GHA CodeBuild 主な利用用途 ・ユニットテスト ・静的コード解析 ・カバレッジ計測 ・アプリケーションのビルド ・AWSへのデプロイ ・E2Eテスト トリガー ・プルリクエストへのPush ・手動実行 設定管理 ・コードベース ・手動管理 また、FBZはサーバーレスアーキテクチャを採用しており、 AWS Lambda (以下、Lambda)を中心としたAWSが提供しているフルマネージドサービスを中心に構築されています。サービス構成やアーキテクチャ戦略の詳細については、以下の記事をご参照ください。 techblog.zozo.com 構成管理ツールとしては、サーバーレスアーキテクチャと相性の良い Serverless Framework を採用しています。 FBZでは、管理対象のリソースが多いことから関心事毎に定義ファイルを分割しています。そして、分割された定義ファイルはCodeBuildから直列実行されることで、デプロイ対象となるスタックの状態を最新化していきます。これらの構成はFBZの開発当初からあまり変わっておらず、大きな課題に直面することもなく、最近まで開発してきました。 一方で、リリースサイクルに関してはここ数年で大幅な見直しを行いました。 リリースサイクルの見直し 以前までは、隔週水曜日をリリース日としており、ある程度まとまった量の修正内容を一度にリリースするリリースサイクルを採用していました。 しかし、昨年からユーザーへの価値提供の速度を向上させることを目的として、リリースサイクルの見直しが行われました。その結果、リリース可能な状態になった修正は、可能な限り早いタイミングでリリースするという、リリース日を固定しないリリースサイクルへと変わりました。 この変化により開発者にも以下の利点がありました。 ビックバンリリースがほとんど行われなくなり、精神的な安定感が得られた 万が一リリースに伴う障害が発生しても、原因の調査や特定が容易になった リリースまでの流れ リリースサイクルを早く回していくという体制になりましたが、開発着手からリリースまでの流れは以前とあまり変わっていません。 以下の図は、FBZ開発におけるリリースまでの一連の流れになります。 この図で注目していただきたいのは、AWS上で実施される「検証・リリース」の2つです。 「設計・実装/テスト・レビュー」については、通常、開発者やレビュアのローカル環境で完結します。これらの3つのタスクにおいて待ち時間は、ユニットテストの実行中に数分程度という限定的なものです。 しかし、「検証・リリース」に関しては、AWS上へのデプロイを伴うため、デプロイが完了するまでに1時間以上もの待ち時間が発生します。検証にデプロイを伴う理由は、Lambdaベースのサーバーレスアーキテクチャを使用しており 1 、AWS上のLambdaに修正したコードを反映させないと動作確認できないことが挙げられます。 顕在化した課題 リリースサイクルを早くしたことでリリースする機会が増え、必然的にデプロイ回数も増えました。 その結果、次のような課題が見えてきました。 長時間のデプロイ ビルド環境のメンテナンス性の低さ 手動デプロイが抱える人為的なリスク 長時間のデプロイ 前述した通り、FBZでは開発当初に構築したCI/CDパイプラインを使ってきましたが、FBZは開発開始から6年以上が経過しています。日々、開発・保守を続けてきたことでアプリケーションコードをはじめ、サービス構成も複雑かつ肥大化してきました。その結果、開発時間のうち、リリースや検証作業といったデプロイを伴う作業にかかる時間の割合が増えてしまいました。 FBZではリリースで問題が発生した際に、再デプロイによって切り戻しを行うことがあります。そのため、デプロイに時間がかかってしまうと、それだけサービスの信頼性にも影響が出てしまいます。 ビルド環境のメンテナンス性の低さ CodeBuild上に作成するビルドプロジェクトと呼ばれる環境の中では、以下の項目をはじめとする様々な設定ができます。 メモリやCPUのスペック ビルド対象のソースコード 環境変数 しかし、これらの項目をメンテナンスする上で、次のような課題がありました。 設定が手動で追加・更新されていた 変更内容のレビューが困難であった 変更履歴が追跡できなかった これらの課題もあり、従来のビルド環境はメンテナンスし易いとは言いづらい状態でした。 手動デプロイが抱える人為的なリスク 手動によるリリースは手間がかかるだけでなく、人為的な問題を起こしてしまうリスクがあります。実際に、FBZではCodeBuild上でデプロイ対象のブランチ名の入力や環境変数を更新する場合など、リリース作業中は常にWチェックしながら操作に誤りが無いかを目視で確認していました。 CI/CDパイプラインの改善に向けて 先程までの課題を整理すると、いずれもCI/CDパイプラインを改善することで解決できることが分かりました。 以降は、それぞれの課題に対して取り組んだ内容を紹介していきます。 デプロイフローの見直し CodeBuildのバッチビルド ビルド定義の実装例 デプロイフローの見直し デプロイに時間がかかっていた要因を分析したところ、複数のServerless Framework定義ファイルを、単一のビルドプロジェクト内部で直列実行していたことが原因と分かりました。そこで、直列実行していたデプロイ処理を並列実行させる方法として、CodeBuildのバッチビルドという機能に注目しました。 CodeBuildのバッチビルド CodeBuildは、いくつかの種類のバッチビルドをサポートしています。 バッチビルドに関する詳細は、以下のドキュメントを御覧ください。 docs.aws.amazon.com FBZではビルドグラフという機能を利用しました。 ビルドグラフでは、タスク同士の依存関係を定義し、その定義された依存関係に基づいてビルドが実行できます。また、定義の仕方によって複数のタスクを並列実行させることもできます。 タスク定義の実装例 FBZのビルド構成を参考として、タスク定義の方法を紹介していきます。 今回は、以下の依存関係を持つタスク定義を実装していきます。 定義ファイル(buildspec)は以下のようになります。 # buildspec.yml version : 0.2 batch : build-graph : # PRE BUILD - identifier : PRE_BUILD_1 buildspec : buildspec_pre_build_1.yml # BUILD # 共通のbuildspecを使い、タスク毎に環境変数でデプロイ対象を制御 - identifier : API_BUILD_1 buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : api_a depend-on : - PRE_BUILD_1 - identifier : API_BUILD_2 buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : api_b depend-on : - PRE_BUILD_1 - identifier : BATCH_BUILD_1 buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : batch_a depend-on : - PRE_BUILD_1 # POST BUILD - identifier : POST_BUILD_1 buildspec : buildspec_post_build_1.yml depend-on : - API_BUILD_1 - API_BUILD_2 - identifier : POST_BUILD_2 buildspec : buildspec_post_build_2.yml depend-on : - API_BUILD_1 - API_BUILD_2 - BATCH_BUILD_1 特徴として、メインとなるタスク( API_BUILD_1 ・ API_BUILD_2 ・ BATCH_BUILD_1 )では buildspec を共通化していることが挙げられます。 タスクごとに環境変数を注入することで、ビルド処理を制御できるようにしています。 - identifier : BUILD_n buildspec : buildspec_build.yml env : variables : DEPLOY_TARGET : xxxxxxxx # 任意の値 こうすることで、今後さらにビルド対象が増えたとしても、環境変数を変えるだけでタスクの追加が行えるので、複雑になりがちなタスク定義が冗長にならず保守しやすくなります。 定義ファイルの詳細は以下のドキュメントを御覧ください。 docs.aws.amazon.com 個々のタスクを高速化 Serverless Frameworkのオプションを見直すことで、各タスクのデプロイ時間短縮を図りました。 Direct deploymentsの有効化 devDependenciesの依存解決 Direct deploymentsの有効化 CloudFormationのスタック作成時に変更セットを作成しないことで、デプロイ時間の高速化を実現する設定があります。以下のように定義します。 # serverless.yml provider : deploymentMethod : direct なお、以下のドキュメントにある通り、次期バージョンであるServerless Framework v4からは上記の設定がデフォルトとなるようです。 www.serverless.com 今後も変更セットを利用したい場合は、以下の定義を明示的に記述することで、Serverless Framework v3までと同じ設定でデプロイが可能です。 # serverless.yml provider : deploymentMethod : changesets deploymentMethod: direct とした場合のCloudFormationの挙動については、以下のドキュメントを御覧ください。 docs.aws.amazon.com devDependenciesの依存解決 デプロイパッケージ作成時に、devDependenciesの依存解決の除外処理に時間がかかっていたので、それらの処理を実行させないことで時間短縮を図りました。定義は以下のとおりです。 # serverless.yml package : excludeDevDependencies : false 詳細はドキュメントを御覧ください。 www.serverless.com デプロイフローを改善した効果 今まで紹介した改善策の実施前後で、ビルド時間にどれだけ変化があったかをキャプチャした結果が以下の図です。 改善前のビルド時間: 1時間14分 改善後のビルド時間: 24分 改善後はバッチビルドを利用しているため、改善前と比べるとフェーズの内訳が異なりますが、改善後の IN_PROGRESS で改善前の全フェーズを並列で実行しているイメージになります。 ビルドの並列化とServerless Frameworkのオプション見直しを行ったことで、およそ70%程度のデプロイ時間短縮を実現できました。 手動運用からの卒業 今まで手動で行ってきたCodeBuildのメンテナンスやデプロイですが、GitHubへの操作をトリガーに、各操作が自動で実行される仕組みを構築しました。 GitHub Actionsの活用 FBZではリリース毎にGitHub上でタグを付与しています。CodeBuildでのデプロイ時にはそのタグをデプロイ対象として使用しています。 タグの生成 タグの生成には、以下のアクションを利用しています。 github.com GHAのワークフローは以下の通りです。 name : Release Drafter on : push : branches : - main jobs : create-draft-release : name : Create Draft Release. runs-on : ubuntu-latest steps : - uses : release-drafter/release-drafter@v5 env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} 挙動を図化したものが以下になります。mainブランチへのpushをトリガーにアクションが起動してタグを生成します。 ビルド環境の自動アップデート CodeBuildで管理しているビルドプロジェクトに対して、変更履歴の追跡や事前のレビューを可能にするため、CodeBuildのリソース定義をコード化して管理することにしました。コード化するにあたり、CloudFormationのテンプレートを利用しました。 スタックの更新は、以下GHAのワークフローを用いて実現しています。 on : push : tags : - '*' env : STACK_NAME : your_stack_name AWS_REGION : your_aws_region jobs : update-build-project : name : Update CodeBuild Stack. runs-on : ubuntu-latest permissions : id-token : write contents : read issues : write pull-requests : write steps : - uses : actions/checkout@v3 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v2 with : role-to-assume : arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx aws-region : {{ env.AWS_REGION }} # テンプレートのデプロイと、削除保護の有効化 - name : Update Codebuild Build Project run : | aws cloudformation deploy \ --role-arn "arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx" \ --template "your_template_name.yml" \ --stack-name ${{ env.STACK_NAME }} \ --capabilities CAPABILITY_IAM aws cloudformation update-termination-protection \ --enable-termination-protection \ --stack-name ${{ env.STACK_NAME }} 上記ワークフローでは、CloudFormationのテンプレートに基づいて、対象のスタックに差分がある場合にのみ更新が行われます。 スタックに差分がない場合は、以下の画像のように更新は行われず処理が終わります。 これによって、CodeBuildの設定変更の履歴をGitHubで管理できるようになったほか、CI上で自動的にスタックの最新化が行われるようになりました。 自動デプロイ 先程までのワークフローに、デプロイを行うためのJobを追加します。 # (中略) env : STACK_NAME : your_stack_name AWS_REGION : your_aws_region # 追加 PROJECT_NAME : your_project_name jobs : # update-build-project: # (中略) deployment : name : Deploy. needs : update-build-project runs-on : ubuntu-latest permissions : id-token : write contents : read issues : write pull-requests : write steps : - uses : actions/checkout@v3 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v2 with : role-to-assume : arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx aws-region : {{ env.AWS_REGION }} - name : Start CodeBuild Batch Build run : | aws codebuild start-build-batch \ --project-name ${{ env.PROJECT_NAME }} \ --source-version ${{ github.sha }} ワークフロー内部でやっていることはシンプルで、AWS CLIを実行することでビルドプロジェクトのデプロイを開始させています。ここで追加したJobは、 needs: update-build-project を指定することで、ビルドプロジェクトの最新化が終わり次第起動します。 deployment : name : Deploy. needs : update-build-project ここまで、タグの生成をトリガーとした前提でワークフローの紹介をしてきましたが、タグの生成以外にも様々なイベントをトリガーにワークフローを実行させることができます。 詳細はドキュメントを御覧ください。 docs.github.com 新生CI/CDパイプライン ここまでの改善によって、今まで手動で行っていたデプロイに関係する操作が、タグの生成をトリガーとしてGHAのワークフローから自動実行されるようになりました。また、デプロイも直列実行から依存関係に従って並列実行されるようになりました。 改善前のデプロイフロー 改善後のデプロイフロー さいごに 本記事では、FBZにおけるCI/CDパイプラインの最適化させる取り組みと、それらの効果についてご紹介しました。 CI/CDパイプラインの最適化を実施した結果、以下の恩恵を得ることができました。 ビルドを並列化したことでデプロイ時間短縮 開発中の待ち時間が減って開発サイクルが高速化された 問題発生時の復旧にかかる時間が早くなった 手動で行っていた設定や操作の自動化 定義をコード化して管理できるようになったことで、変更追跡ができるようになった ビルド環境のメンテナンスが楽になった 変更内容がレビュー可能になった CodeBuildやServerless Frameworkを利用している方や、GHAを使ったCI環境の構築に興味がある方はぜひ参考にしてみてください。 ZOZOMO部では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co Serverless Frameworkには 任意のLambdaのみをデプロイする機能 がありますが、CloudFormation スタックの管理対象外となり、リソース管理が煩雑になるためFBZでは利用していません ↩
こんにちは、MA部MA開発1ブロックの齋藤( @kyoppii13 )です。 8/29-8/31に開催された Google Cloud Next '23 へ参加してきました。今年は4年ぶりとなるオフライン開催で、アメリカ・サンフランシスコで開催されました。弊社からはMA部の齋藤・松岡・中原の3名が参加しました。 今年は生成AIにフォーカスした内容がとても多く、それに関連する新サービスの発表も多くありました。本記事では、現地での様子と特に興味深かったセッションをピックアップして紹介します。 現地での様子 3日間に渡って開催されたGoogle Cloud Nextの会場はモスコーニ・センターという大きな展示施設で、メインルームではキーノート、他ルームでセッションが発表されるというものでした。発表以外にもワークショップやたくさんの企業ブースがあり大変賑わっていました。 Moscone Center Main Room Company Booth Google Cloud Nextは毎年開催されているイベントですが、今年は4年ぶりのオフライン開催ということもあり、非常に多くの参加者がいました。そのため、初日最初のキーノートは満席で入場制限が発生していました。セッションやワークショップは基本的には予約制で、予約なしで当日に並んで見ることも可能ですが、人気のものは長い待機列になっていました。 今年は生成AIに関連する発表がとても多かったです。GCPのマネージドなAIサービスであるVertex AIやGoogle Workspaceにおいて生成AIでユーザをサポートするDuet AIに関連する発表が多く見られました。 以降で現地参加したメンバーが気になったセッションについて紹介します。 セッション紹介 PostgreSQL the way only Google can deliver it: A deep dive into AlloyDB MA部MA開発1ブロックの齋藤です。 このセッションでは、昨年12月にGAとなったPostgreSQL互換のマネージドDBサービスであるAlloyDBの詳細と移行事例について紹介されました。 AlloyDBは標準のPostgreSQLと比較し、TPM(transaction per minute)が4倍以上、分析クエリにおいては最大100倍のパフォーマンス向上が期待できます。分析クエリのパフォーマンス向上により、従来のアプリケーションでは実行できなかったクエリも実行できるようになると紹介されていました。 パフォーマンス以外のメリットとして可用性やスケーラビリティ、オートパイロット機能・AI/ML機能についても述べられていました。また、OSSのPostgreSQLをベースとしているため、商用DBとは異なり、ベンダーロックインもなく、コストやライセンス管理も複雑になりません。 Linkより引用 移行事例としてCME Group社によるOracle Exadataからの移行について紹介されていました。移行に伴い従来のパフォーマンスを維持できるかのPoCを実施し、PoCの結果として、従来と同等またはそれ以上のパフォーマンスを実現できたと述べられていました。また、チューニングについても述べられていました。例えば、ORのUNIONへの置き換え、NOT EXISTSを使用しないなどのクエリ修正をすることでアプリケーションが要求するパフォーマンスを満たすことに繋がったと述べられていたのは興味深かったです。 ここからは高パフォーマンスがどのように実現されているかの紹介です。まずはレプリケーションとフェイルオーバーについてです。 アーキテクチャとして、コンピューティングとストレージでレイヤーが分かれていることで、それぞれでスケールでき、レプリケーションの遅延は通常のPostgreSQLと比較して25倍高速になります。フェイルオーバーにおいては、最近のリリースによってプライマリはダウンタイムが1秒未満、リードレプリカは0秒を実現しています。 Linkより引用 ダウンタイムが非常に短い点はとても気になりました。MA部では配信システムの開発・運用をしており、配信にはバッチによる配信とリアルタイムな配信があります。また、DBサービスとしてCloud SQLも一部システムで利用しています。Cloud SQLでは決められたメンテナンス時間帯に自動的にメンテナンスをしますが、数十秒のダウンタイムが発生します。特にリアルタイムな配信においてはダウンタイムが短いことは、機会損失を防げる点で非常に有益だと思いました。 参考:Cloud SQL インスタンスでのメンテナンスについて 次の高パフォーマンスの実現ではキャッシングと カラムナエンジン について述べられていました。AlloyDBでは複数のキャッシングを実装しています。RAMベースのキャッシングやUltra-fast Cacheと呼ばれるものです。RAMベースのキャッシュにおいては、通常のPostgreSQLでの行指向のキャッシングに加えて、列指向のキャッシングにも対応しています。このキャッシングに加えて、更にカラムナエンジンという列指向でのデータ処理を実施するエンジンも組み合わせることで、非常に高速な処理が可能となります。 それまでのRDBでは当たり前だった行指向でのデータの扱い方に加えて、BigQueryで培った列指向でのデータの扱い方を組み合わせるというのはGoogleだからこそできる技術だと思いました。 次にオートパイロット機能についてです。以下の機能が述べられていました。 Index Advisor(インデックスアドバイザー) Adaptive memory management(適応的メモリ管理) Adaptive vacuum management(適応的バキューム管理) Index Advisorはクエリパフォーマンス向上のために最適なインデックスを計算する機能です。Adaptive memory managementはOOMが発生しないようにバッファキャッシュを自動計算する機能です。Adaptive vacuum managementは現在のワークロードへの影響を最小限にしながらVACUUM処理を自動化するものです。 オートパイロット機能で特に気になったのは、Adaptive vacuum managementです。MA部では配信に利用するデータを生成するために、バッチ処理でクエリを実行しています。クエリパフォーマンスを保つためにAUTO VACUUMだけではなく、毎日深夜帯にVACUUM処理を実施しています。しかし、VACUUM処理が長引き、配信時間帯になっても終わらないことがあります。このような場合は配信影響を考慮し、VACUUMを停止するというオペレーションが発生します。このような問題を解決するために非常に有用な機能だと思いました。 最後に新機能について述べられました。新機能とは、この日最初のOpening Keynoteで新機能の発表であったAlloyDB AIについてです。 Linkより引用 SQL文の実行でエンベディング生成やVertex AIのモデルを呼び出せ、またそれらが高速に動作することで、DBのデータを利用した生成AIのアプリケーションを容易に作成できるようになります。現在はプレビュー版であり、本番利用はできません。詳しくは以下の公式ブログをご覧ください。 Google Cloud Blog - プレビュー版が提供開始された AlloyDB Omni で新たな生成 AI 機能を構築 MA部ではまだAlloyDBを利用していません。しかし今回の発表を聞いて非常に移行メリットは大きいと感じました。移行においてもオンプレやクラウドからの移行ツールである Database Migration Service が提供されています。 また、新機能であるAlloyDB AIもデータベースのデータを利用することで簡単にアプリケーションとして生成AIを組み込めるというのは大きなメリットだと思いました。私が今年もっとも衝撃を受けた技術である生成AI、今後の動向に注目したいです。 What's next for application developers MA部MA開発2ブロックの松岡( @pine0619 )です。 本セッションでは、アプリケーションエンジニアの開発者体験を変えるプロダクトとして以下2つの紹介がありました。 Jump Start Solutions Duet AI in Google Cloud Jump Start Solutions このプロダクトではユースケースに応じてアプリケーションとインフラのテンプレートが用意されており、それを使うことで簡単にアプリケーションをデプロイできます。新しくクラウドを使い始める際には様々なサービスを組み合わせ、ベストプラクティスを学習するなど障壁が高いという課題があります。このJump Start Solutionsを使うことで、最初のアプリケーション実行までを、より簡単かつセキュリティを担保した上で高速に実現できます。 現在は14種類の テンプレート が用意されています。 ユースケースとテンプレートが合致する場合という条件はあるものの、簡単にアプリケーション・インフラを構築し、初回デプロイまでの時間を短縮出来るのは便利だと感じました。今後テンプレートが増えていくとより使える場面が増えると思うので今後の動向に注目したいです。 Duet AI in Google Cloud 本イベントでは Google Cloud Duet AI in Google Cloudのプレビュー版 が発表されました。Duet AIとはGoogleが提供する生成AIを使った支援機能です。今回の発表でDuet AIによるアシスタント機能がコーディングやBigQuery、データベースなどGoogle Cloudの様々なプロダクト、サービスで利用できるようになりました。 本セッションは、そのDuet AIの機能の中でも開発者向けの機能であるコード補完、コード生成、チャットアシスタント、テスト生成などについての紹介です。 中でもチャットアシスタントはIDE上から呼び出すことができ、コーディングに関する質問やコード全体を要約するなど、効率的に開発を進める上で非常に便利な機能だと感じました。 セッションでは実際にデモも行われ、Duet AIを利用したコーディングの便利さを実感することが出来ました。 What's next for operations and platform builders 次も松岡から"What's next for operations and platform builders"のセッションについてご紹介します。 本セッションでは以下の機能についての紹介がありました。 Cloud TPU v4 on GKEのGAリリースおよびCloud TPU v5e on GKEのプレビュー提供 Cloud Run sidecars supportのGAリリース GKE Enterprise edition Interactive Troubleshooting PlaybooksのGAリリース 中でもCloud Run sidecars supportとInteractive Troubleshooting Playbooksが気になったので簡単に内容をまとめます。 Cloud Run sidecars support Cloud Run sidecars supportは今年の5月にプレビュー提供が開始され、今回GAリリースとなった機能で、Cloud Runでメインコンテナと並行して動作する独立したサイドカーコンテナが使えるようになります。使う場面としては、DatadogやOpenTelemetryといった監視ツールの動作や、nginxやEnvoyといったproxyとしての使用が挙げられます。サイドカーが使えるようになったことで今までに比べ選択肢が広がり、よりCloud Runの採用がしやすくなったのではないでしょうか? Cloud Run sidecarsについては以下のセッションで詳しく紹介されていましたので興味がある方はそちらもご覧ください。 Extend your Cloud Run containers’ capabilities using sidecars Interactive Troubleshooting Playbooks Interactive Troubleshooting PlaybooksはGKEのトラブルシューティングを行うための機能で、発生したトラブルに応じてログやメトリクスの出力、トラブルシューティングのための次のステップなどが表示され、解決への手引きをしてくれます。 現状は Pod unschedulable や CrashLoopBackOff などのいくつかのトラブルに関するplaybookが提供されていますが、今後他のトラブルに関するplaybookも提供される予定です。 詳しくは以下の公式ブログで紹介されていますので気になる方はご覧ください。 Simplify troubleshooting in Google Kubernetes Engine with new playbooks MA部ではメールやLINEなどへの配信のバッチ処理をGKE Autopilot上に構築したDigdagで行っています。そちらに関しては以下のテックブログで詳しく紹介しています。 techblog.zozo.com Interactive Troubleshooting Playbooksの機能を使うことでGKEの運用がより楽になることを期待したいです。また、本機能はGKEに限定されていますが、今後他のGCPサービスでも同様な機能が出て運用負荷の軽減が出来ることを期待しています。 What's new with BigQuery MA部MA開発1ブロックの中原です。 "What's new with BigQuery"について紹介します。 このセッションでは、 data clean rooms のプレビュー提供やBigQuery Omniの新機能、生成AIを使った新機能など多くのBigQueryに関する新しい発表がありました。その中でも生成AIを使った新機能に絞って紹介します。大きく以下3つの紹介がありました。 データエンジニアリング、アナリティクス、予測分析を1つに統合したインタフェースであるBigQuery Studioを発表 Vertex AI基盤モデルとの統合により、BigQueryのデータにAIを導入 Looker、BigQuery、Dataplexなどのサービスでデータ作業を再構築するために、Google CloudのDuet AIをプレビュー提供 それぞれについて簡単に内容をまとめます。 BigQuery Studioについて BigQuery Studioは現在プレビュー提供されています。これまでデータ分析や機械学習を活用するチームが、データウェアハウスやデータレイク、機械学習モデルなどを管理するために異なるツールを使用しており、生産性が低下してしまうという課題がありました。BigQuery Studioを利用することで1つのインタフェースでツールを切り替えることなく上記の作業をできるようになりました。 また、新しく発表されたPython向けノートブックの新サービス Colab Enterprise と統合されたUIでPythonによりBigQueryのデータを操作できるようになります。 Vertex AI基盤モデルとの統合について Googleが開発した言語モデルであるPaLM 2を含むVertex AI基盤モデルへのBigQueryからの直接アクセスできるようになり、以下の内容が発表されました。 BigQueryのコンソールでCREATE MODEL構文を使って、BigQuery MLモデルをVertex AI Model Registryに登録できる機能をGA提供 SQLのみで大規模言語モデル(LLM)のテキスト生成ができる機能をプレビュー提供 1つ目のVertex AI Model Registryへの登録については、機能の概要と詳細が 公式ドキュメント に記載されています。BigQueryのコンソール画面のみでモデルのバージョン管理、評価、デプロイを行い、オンライン予測ができるようになりました。 2つ目のLLMのテキスト生成については、使用例が 公式ブログ に記載されています。SQLのみで簡単な生成AIを実行でき、Cloud SQLやSpannerなどのデータベースに格納されているデータやCSVファイル、他の外部データソースのデータに対してもLLM分析が可能になりました。 Vertex AI基盤モデルとの統合と機能追加によって効率的にデータ分析ができるようになりました。 Duet AIについて 生成AIによる作業支援ツール「Duet AI」がLooker、BigQuery、Dataplexなど、Googleの様々なサービスでプレビュー提供すると発表されました。 BigQueryにおいては、自然言語での指示によるSQLの自動生成や、表やグラフを含むLookerStudioダッシュボードの自動生成などが可能になります。 詳しい内容は以下の公式ブログをご覧ください。 Reimagine data analytics for the era of AI MA部ではBigQueryのコンソールはよく使うため、Duet AIにより生産性が上がることを期待したいです。将来的にはAIがテーブル名やカラム、データの中身からどのようなテーブルなのかを理解し、どのようなデータを取りたいか指示すると自動でSQLの生成からデータの取得まで行えるようになるのでしょうか。今後の進化が楽しみです。 Go from idea to app with no coding using AppSheet 次も中原から"Go from idea to app with no coding using AppSheet"について紹介します。 AppSheetはノーコードで簡単にアプリケーションを作成できるサービスです。 このセッションでは、AppSheetに追加された新機能と機能強化によって、あらゆるスキルレベルのユーザにとってアプリ開発がより身近なものになったとデモも混じえながら紹介がありました。 発表された内容は、Duet AIがAppSheetで動作するようになり、自然言語で指示するとアプリを作成してくれるようになったということです。GoogleChatで「Duet AI in AppSheet」というアプリをスペースに追加すると、チャットの対話に基づいてアプリケーションを作成してくれます。また、アプリケーションの画面で変更したい箇所にマウスのカーソルを持ってきてクリックするだけで編集画面を開くことができ、直感的に画面をカスタマイズできるようになりました。 デモでは、オフィスのビルで働く人が管理者に対してメンテナンスが必要な箇所をモバイルで報告し、管理者にメールで通知が届きそれらを管理できるアプリを作成していました。データの登録や更新をするだけではなく、画像の添付やメール送信もあり少し複雑なものでしたが、チャットでAIと対話しながらテーブルの作成からアプリケーションの作成までできていました。 AppSheetはGmailやMeetなどのGoogle Workspaceと連携できるため、アイデアがあれば業務プロセスを効率化するようなアプリケーションを簡単に作れるようになりそうです。例えばMA部では、メルマガやLINE、アプリプッシュの配信システムを運用しているため、配信に関する問い合わせが様々な部署から来ます。今はSlack上で問い合わせを受け付けていますが、問い合わせの受け付けと管理をAppSheetのアプリケーションで行えば、問い合わせの管理がより効率的になるのではというアイデアが浮かびます。Duet AI機能を使えば、このアイデアも簡単にアプリケーションにできそうです。 AI-assisted collaborator and the changing workforce つづいても中原から"AI-assisted collaborator and the changing workforce"について紹介します。 このセッションは、AIと変化する職場についてのパネルディスカッションでした。 AIが職場と仕事に与える影響について以下のようなことを話されていました。 利点 AIは業務効率を向上させ、重要な業務プロセスを自動化する データの活用をサポートし、データから有用な情報を抽出できる メールの自動整理や適切なコミュニケーションのタイミングを提案することで、従業員間のコミュニケーションを助け、チームの連携が向上する トレーニングや教育プログラムをカスタマイズし、個々の従業員の成長をサポートする 課題 プライバシーとセキュリティの懸念が存在し、データの適切な保護が必要 従業員がAIを活用できるようにトレーニングとスキル向上のサポートが必要 AIの導入には文化の変化と変更管理が必要 AIからデータや情報を多く提供されると、有益な情報の抽出を難しくなる可能性があるため、AIが生成するデータのフィルタリングが必要 AI導入のベストプラクティス 明確なビジョンと戦略を持ち、段階的な導入を検討する 従業員の教育とスキル向上に投資し、外部の専門家を活用する データの品質とセキュリティに注意を払い、透明性を重視する 組織全体でのコミュニケーションを促進し、文化の変化をサポートする AIの導入と活用についてアドバイス AIは組織にとって貴重なツールであり、戦略的なパートナーとして活用すべき AIを受け入れ、学び、適応することが重要で、トレーニングとサポートを提供することが求められる 様々な場面でAIの導入の加速が予想されますが、それと同時にAIを活用するスキルが今後必要になってくると考えられます。ChatGPTをはじめとしたチャットAIやGitHub Copilotをはじめとするコード補完ツールなど様々なAIツールがありますが、最大限に活用できている人は多くないと思います。AI導入のベストプラクティスの「従業員の教育とスキル向上に投資」という観点では、ZOZOでは GitHub Copilotを全社導入 しています。私も業務で使用していますが、GitHub Copilotが得意なことや不得意なこと、うまく活用するコツを理解して活用するには至っていません。AIツールも進化していくので今のうちからAIツールを積極的に使い、AIツールをうまく最大限に活用する方法を模索していきたいです。 まとめ 参加メンバーがそれぞれ気になったセッションについて紹介いたしました。ここまで紹介したように今年はAI関連、特に生成AIに関する発表が多かったです。弊社でもすでにGitHub Copilotによる生成AIの開発支援は導入していますが、GCPのサービスでも生成AIを活用したものの導入が検討できそうだと思いました。 また、初めての海外カンファレンス参加でしたが、セッションでの学びはもちろんのこと、他社のエンジニアや企業ブースでの得られたコミュニケーションは、現地参加だからこその体験だと思いました。 紹介したセッション以外にもたくさんの興味深い発表がありました。全てのセッションは公式サイトの Session Library から見ることができます。ぜひご覧ください。 おまけ 会場やその周辺では様々なところにGoogle Cloud Nextの装飾やロゴがありました。 Entrance Next logo in San Francisco Certified Lounge ワークショップにも参加しました。参加したのはVertex AIでの生成AIとプロンプトデザインについてのワークショップです。 Vertex AIのGenerative AI Studio UIという画面上から生成AIモデルの作成や実行ができるツールとJupyter Notebookを操作しながら、生成AIについて学びました。課題をクリアしながらポイントをゲットしていくという形式で、ポイントランキング上位の人には景品があり、楽しく学ぶことが出来ました。 ランチは毎日メニューが異なり、複数のお弁当から選ぶことができました。これは1日目のチキンサンドイッチです。ビッグサイズでした。 Google Cloud Nextの開催前日には日本からの参加者向けに、ウェルカムパーティーを開催していただきました。Googleの方や他社のエンジニアの方と多くの方とお話して、とても有意義な時間を過ごすことができました。海近くのレストランのため、テラスからは心地よい風を感じられました。 Google Cloud Nextの終了翌日にはGoogleの新社屋であるベイ・ビューのオフィスツアーをしていただきました。とても大きく、オフィス間の移動には車や自転車を使うということでした。オフィスは独特な形をしています。これはソーラーパネルでの発電効率の向上や雨水を集めるための設計です。 社内には様々なGoogleロゴやオブジェがありました。 何箇所かマイクロキッチンと呼ばれる食べ物やドリンクが置かれているエリアがありました。 お昼は社内のカフェでランチをいただきました。いくつかの料理から選べるのですが、私はハンバーガーにしました。とても美味しかったです。 オフィスツアー後にはGoogle Merchandise StoreというGoogleグッズが売っているショップに連れて行っていただきました。アパレルグッズがとても多かったです。 ベイ・ビューのオフィス周辺はシリコンバレーエリアということで、GoogleplexやApple Parkも見てきました。スケールの大きさに驚くばかりでした。 最後に カンファレンス参加に伴う渡航費や宿泊費は 福利厚生 のひとつであるセミナー・カンファレンス参加支援制度によって全て会社負担です。 ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! corp.zozo.com
はじめに 検索基盤部の内田です。検索基盤部はZOZOTOWNの商品検索ロジックや検索動線上の各機能の改善に取り組んでいます。検索機能に関連したバックエンド実装にはJavaを使うことが多かったのですが、近年ではGo言語を採用することも増えてきました。 この記事は、Go言語で実装したWeb APIからElasticsearchへの検索処理を実装した際に調べたことをまとめたものです。Go言語でElasticsearchを取り扱うみなさまの助けとなれば幸いです。 2つのElasticsearchクライアント Go言語のElasticsearchクライアントについて調べると、主に以下の2つのライブラリが使われているのが見受けられます。 elastic/go-elasticsearchは、Elasticsearchを提供するElastic公式のクライアントです。公開されたのは2019年末と比較的最近で、サポートしているElasticsearchのバージョンも6から8系と新しめのものです。今後もElasticsearchのバージョンアップに合わせてアップデートされると思われるため、Go言語でElasticsearchを取り扱う際の有力な候補となるクライアントです。 一方のolivere/elasticはサードパーティ製クライアントであり、2012年に公開されて以来、Go言語でElasticsearchを扱う際の有力な選択肢として長い期間利用されてきました。サポートしているElasticsearchのバージョンは1から7系と幅広く、長い間コミュニティを支えてきたことが伺い知れます。しかし、後発の公式ライブラリの充実に伴い、olivere/elasticは2022年3月の更新を最後に開発が終了し、現在の利用は非推奨となっています。 現在、Go言語のElasticsearchクライアントを利用するならば、公式クライアントであるelastic/go-elasticsearchが最有力候補となります。しかし、公式クライアントの登場は比較的最近でolivere/elasticから主流が移ってまだ日が浅いため、参考となる資料があまり豊富ではありません。また、 Elasticが公開しているドキュメント も現時点ではあまり充実していません。 公式クライント elastic/go-elasticsearch についての知見 この節では、elastic/go-elasticsearchを利用して検索処理を実装した際に調べたことを紹介します。執筆時点でのelastic/go-elasticsearchの最新バージョンはv8.9.0です。 2種類のクライアント elastic/go-elasticsearchを用いて検索処理を実行するには、まずクライアントを生成する必要があります。クライアントを生成する関数には elasticsearch.Config 構造体を渡します。この構造体では、Elasticsearchへの接続や認証、通信に関する設定などを行うことができます。 認証については公式ドキュメントが詳しいのでご参照ください。通信に関する設定については本記事で後述します。 v8.9.0現在、クライアントには以下の2種類があります。基本的に提供されている機能は同じで通信処理なども共通ですが、機能の呼び出し方が異なります。 elasticsearch.Client elasticsearch.TypedClient elasticsearch.Client elasticsearch.Client は初期から存在するデフォルトのクライアントです。 elasticsearch.NewDefaultClient 関数はこちらのクライアントを生成します。提供されているAPIは esapi パッケージで確認できます。パッケージが巨大で、GoDocページが重めになっているのでご注意ください。 // type Client struct { // BaseClient // *esapi.API // } es, _ := elasticsearch.NewClient(elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く }) body := bytes.NewReader([] byte ( ` { "query": { "match_all":{} } } ` )) res, _ := es.Search( es.Search.WithContext(ctx), es.Search.WithIndex( "index-name" ), es.Search.WithBody(body), ) // ↓のように書いてもいい // req := esapi.SearchRequest{ // Index: []string{"index-name"}, // Body: body // } // res, _ := req.Do(ctx, es) defer res.Body.Close() body, _ := io.ReadAll(res.Body) // res.Bodyから読みだしたbyte列をjson.Unmarshalなどに渡す elasticsearch.Client には esapi.API 構造体へのポインタが埋め込まれているため、 esapi.API 構造体が持つフィールドや *esapi.API 型に紐づいたメソッドを呼び出すことができます。検索の実行に対応するメソッドは Search です。 Search メソッドを呼び出すと、内部では esapi.SearchRequest 構造体が生成され、その Do メソッドが呼び出されるようになっています。実装を確認したかぎり *esapi.API 型に紐づいたメソッドは基本的にすべて、対応するRequest構造体を生成してその Do メソッドを呼び出すようになっているようです。そのため、 *esapi.API 型に紐づいたメソッドの具体的な処理内容や設定可能な項目について知りたいときは、対応するRequest構造体のドキュメントや実装を調べるといいでしょう。埋め込まれた *esapi.API のメソッドを呼び出すのではなく、Request構造体を直接生成して Do メソッドを呼び出すように実装するのもシンプルでおすすめできます。 Elasticsearchの処理の実行結果は、呼び出したAPIの種類に関わらず *esapi.Response 型で返されます。 Body フィールドからバイト列を読み出し、呼び出したAPIに応じてJSONをパースし各要素にアクセスする必要があります。 elasticsearch.TypedClient elasticsearch.TypedClient はv8.4.0から追加された新しいクライアント実装です。提供されているAPIは typedapi パッケージで確認できます。こちらのクライアント実装は公式ドキュメントに 専用のページ が用意されていて、今後はこちらを推していきたいという雰囲気を感じます。 // type TypedClient struct { // BaseClient // *typedapi.API // } es, _ := elasticsearch.NewTypedClient(elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く }) res, _ := es.Search().Request(&search.Request{ Query: &types.Query{ MatchAll: &types.MatchAllQuery{}, }, }).Index( "index-name" ).Do(ctx) // resは*search.Response型 // 型付けされているため、res.Hits.Hits[0]のようにして各要素にアクセスできる elasticsearch.TypedClient 構造体には typedapi.API 構造体へのポインタが埋め込まれています。 elasticsearch.Client の場合と同様に埋め込まれた構造体のフィールドやメソッドにアクセスできます。こちらはメソッドチェーンの形でリクエスト内容の構築と実行ができるようになっています。 こちらのクライアントの特徴は、機能ごとにパッケージが切られて構造体や処理がまとめられていることです。例えば、検索の機能は searchパッケージ にまとめて定義されています。機能ごとに型付けされた構造体の操作でリクエスト内容の構築やレスポンス内容へのアクセスができるため、 elasticsearch.Client と比べるとJSON文字列の扱いを省略できる分シンプルかつ安全に取り扱うことができます。もちろん、 io.Reader 型を引数として受け取る Raw メソッドも用意されているので、従来どおりJSON文字列としてリクエストの内容を設定することもできます。 Raw メソッドを利用する場合は、 Request メソッドで渡した内容は無視されます( 該当箇所 )。 後発の実装なだけあり、 elasticsearch.Client と比べてよく整理されているので、 elasticsearch.TypedClient の利用から検討してみるといいと思います。ただし、歴史の浅いelastic/go-elasticsearchの中でも elasticsearch.TypedClient は新しいクライアントであるため、現時点では資料があまりありません。公式ドキュメントやGoDocを読んで自分のやりたい処理に対応するパッケージを調べましょう。 通信処理 通信に関する設定はelastic/go-elasticsearchの elasticsearch.Config で行うことができます。一方、通信処理の実装自体は別ライブラリ elastic/elastic-transport-go に切り出されています。 elasticsearch.Client もしくは elasticsearch.TypedClient を初期化すると、elastic/elastic-transport-goの elastictransport.New 関数が呼び出され、 elastictransport.Client が生成されます。生成された elastictransport.Client はクライアント構造体の中に格納され、すべての通信処理を担います。 elasticsearch.Config で設定したほとんど全ての項目はこの elastictransport.Client の生成時に利用されます。 elasticsearch.Config の GoDoc には、各フィールドがどのような設定項目なのかがまとめられており、何も設定しなかった場合のデフォルト値も記載されています。しかし、デフォルト値の記載がない一部のフィールドについては、elastic/elastic-transport-goを調べる必要があります。 例えば、コネクションや細かいタイムアウトの設定ができる Transport フィールドについてelastic/go-elasticsearchのドキュメントにはデフォルト値の記載がありません。しかし、elastic/elastic-transport-goを見ると、何も指定されなかった場合に elastictransport.New 関数内で http.DefaultTransport が使われるようになっていることが分かります( 該当箇所 )。そのため、デフォルトの挙動を踏襲しつつ一部の設定を変えたい場合は、以下のように http.DefaultTransport を元に生成したTransportの一部の設定を書き換えて利用するのがいいでしょう。 // http.DefaultTransportをコピーする tr, _ := http.DefaultTransport.(*http.Transport) t := tr.Clone() // DefaultTransportの値から変更したい項目を設定する t.MaxIdleConns = maxIdleConns t.MaxIdleConnsPerHost = maxIdleConnsPerHost t.MaxConnsPerHost = maxConnsPerHost t.IdleConnTimeout = idleConnTimeout cfg := elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く Transport: t, } Datadog APMとの連携 ZOZOTOWNではサービスの監視にDatadog APMを利用しています。Datadogからは公式のGoクライアントであるDataDog/dd-trace-goが公開されており、その中にelastic/go-elasticsearchと連携するための実装が含まれています。この実装を利用することで、クライアントの内部で行われているElasticsearchとの通信処理をトレースできます。ファイル名にv6と書かれていて不安になりますが、Elasticsearchの7系や8系でも利用可能です。 Elasticsearchとの通信処理のトレースは、 NewRoundTripper 関数で生成したオブジェクトを elasticsearch.Config の Transport フィールドに渡すことで実現できます。このRoundTripperもデフォルトでは http.DefaultTransport を元に生成されます( 該当箇所 )。先述したようなTransportのカスタマイズを行いたい場合は、下記のように WithTransport 関数を使って元となる http.RoundTripper インタフェースを満たす実装を渡す必要があります。 import ( elastictrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/elastic/go-elasticsearch.v6" ) t := MyRoundTripper() cfg := elasticsearch.Config{ // 接続や認証、通信に関する設定をここに書く Transport: elastictrace.NewRoundTripper( elastictrace.WithServiceName( "service-name" ), elastictrace.WithTransport(t), // http.DefaultTransportではなく、自分で用意したRoundTripperをベースにRoundTripperを生成させる ), } まとめ Go言語におけるElasticsearchクライアントについて紹介しました。改めて、本記事の概要を以下に列挙します。 メジャーなクライアントライブラリが2種類ありますが、elastic/go-elasticsearchの利用をおすすめします elastic/go-elasticsearchの中にさらに2種類のクライアント実装がありますが、 elasticsearch.TypedClient の利用をおすすめします 通信に関する処理は別ライブラリelastic/elastic-transport-goに切り出されているので、分からないことがあったらこちらの実装を調べると解決することがあります Datadog公式クライアントにはelastic/go-elasticsearchと連携するための実装が含まれているので、これを利用することでクライアント内部の通信処理をトレースできます おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、検索基盤部の伊澤です。検索基盤部では普段から、ZOZOTOWNの検索機能に関するデータ分析や、データ分析を踏まえた検索性能の改善に取り組んでいます。 検索に関するデータ分析では、検索クエリの傾向把握や課題のあるクエリの特定のために、検索クエリごとの検索結果のクリック率やコンバージョン率といったパフォーマンス指標を評価しています。 本記事では、検索クエリごとのデータ分析に関する情報共有を効率化するため、ウェブフレームワークの「 Dash 」で開発したダッシュボードを活用した事例を紹介します。 目次 はじめに 目次 検索クエリごとのデータ分析の重要性 分析結果のチーム内共有時の課題 Dashを用いたダッシュボードの開発 Dashとは Dashを選定した理由 検索クエリごとのパフォーマンス指標のダッシュボード 1. 検索クエリごとのパフォーマンス指標のテーブル 2. 検索クエリごとのパフォーマンス指標のグラフ 3. テーブルやグラフを操作するコントローラー ダッシュボードによる情報共有の効果 まとめ 検索クエリごとのデータ分析の重要性 ZOZOTOWNでは、ユーザーが欲しい商品を見つけやすくするために、以下のような検索機能を提供しています。 ユーザーが検索クエリを入力した際に、入力の続きを補完したキーワードを提示するサジェスト機能 検索結果の並び順を最適化するおすすめ順検索 検索の絞り込みクエリ提案機能 サジェスト機能の詳細は以下の記事をご参照ください。 techblog.zozo.com おすすめ順検索については以下の記事をご参照ください。 techblog.zozo.com ZOZOTOWNの検索の絞り込みクエリ提案機能は、以下の赤枠内に設置されています。ここで表示されているキーワードをクリックすることで、そのキーワードにより検索をさらに絞り込むことができます。 これら検索関連の機能の改善を進めるために、検索全体の商品クリック率やコンバージョン率だけではなく、検索クエリごとの指標を評価して効果的な検索クエリとそうでない検索クエリを把握することも必要です。これにより、適切な検索結果が表示されない検索クエリを特定し、改善の余地を見つけることができます。 例えば、おすすめ順検索においては、検索回数に対して商品クリック数やコンバージョン数が少ないような検索クエリを把握できれば、ランキングが効果的でない検索クエリを特定できます。 サジェスト機能や絞り込みクエリ提案機能では、検索クエリごとのパフォーマンス指標から、次に提案すべきクエリの示唆を得ることができます。 このように、「検索クエリごと」のパフォーマンス指標を把握することも重要となります。検索基盤部では、検索クエリごとのパフォーマンス指標を把握し、チーム内で共有して次の施策の改善案や優先順位を検討しています。 分析結果のチーム内共有時の課題 データ分析結果のチーム内共有時には、以下のような課題がありました。 膨大な検索クエリパターン :検索クエリごとの分析といってもそのパターンは膨大であり、それらを一覧で見て判断することは困難。そのため、検索回数の上位数十件に焦点を当てた限定的な範囲での結果の共有となってしまう。 ファッション業界の季節性 :ファッションの世界では、季節性によって検索されるクエリは日々更新されるため、検索クエリごとの分析結果も日々変化する。そのため分析も高頻度で行うことになるが、共有のためのグラフやテーブルをその都度作り直すことには非常に大きな手間がかかる。 チーム内での検索クエリへの関心の違い :チームメンバーの関心のある検索クエリが異なる場合、共有時のグラフに含まれない検索クエリはその場で確認できないことになる。 こうした課題を解決し、効率的な分析と共有を可能にするため、共有時にチームメンバーがインタラクティブに確認できるような状況を整える必要があります。 Dashを用いたダッシュボードの開発 上述の問題を解決するために、各検索クエリに関する商品クリック率やコンバージョン率をインタラクティブに可視化するダッシュボードを開発しました。 ダッシュボードの開発にあたっては、Pythonのウェブフレームワークである「Dash」を用いました。 Dashとは Dashは、Pythonでデータアプリケーションを開発できるウェブフレームワークです。少ないプログラムコード(Low-Code)で、様々なグラフや表を表示するダッシュボードを開発できます。 dash.plotly.com 以下にサンプルコードとサンプルアプリケーションのイメージを示します。サンプルコードは、公式の A Minimal Dash App を参考にしています。 from dash import Dash, dcc, html, callback, Output, Input import plotly.express as px import pandas as pd # サンプルデータを作成 data = { 'category' : [ 'tops' , 'tops' , 'tops' , 'bottoms' , 'bottoms' , 'bottoms' ], 'sub_category' : [ 't-shirt' , 'polo' , 'knit' , 'denim' , 'cargo' , 'chino' ], 'value' : [ 10 , 20 , 15 , 25 , 20 , 30 ] } df = pd.DataFrame(data) # Dashアプリを作成 app = Dash(__name__) # レイアウトを定義 app.layout = html.Div([ html.H1( 'Sample Dash App' ), dcc.Dropdown(df.category.unique(), 'tops' , id = 'dropdown' ), dcc.Graph( id = 'bar-chart' , figure=px.bar(df, x= 'sub_category' , y= 'value' , title= 'Sample Bar Chart' ) ) ]) # コールバックを定義 @ callback ( Output( 'bar-chart' , 'figure' ), Input( 'dropdown' , 'value' ) ) def update_bar_chart (value): dff = df[df.category == value] return px.bar(dff, x= 'sub_category' , y= 'value' , title= 'Sample Bar Chart' ) if __name__ == '__main__' : app.run_server(debug= True ) サンプルコードでは、レイアウトとコールバック関数を定義しています。レイアウト内のDropdownコンポーネントにidを付与し、コールバック関数内のInputにDropdownコンポーネントのidを指定しています。これにより、Dropdownコンポーネントの値が変化した時にコールバック関数が呼び出され、コールバック関数のOutputに指定したグラフの値を更新できます。 上記のコードだけで、以下のようなアプリケーションを立ち上げることができます。 その他、公式のサイトにサンプルアプリケーションが豊富にあります。Dashでどんなことができるか知りたい方は、以下を参照してください。 plotly.com Dashを選定した理由 今回、検索クエリごとのパフォーマンス指標のダッシュボードを作成するにあたっては、以下の理由からDashを選定しました。 Low-Codeでクイックにダッシュボードを開発できる 「 Plotly 」のグラフをダッシュボードに統合できる コンポーネントが豊富にありレイアウトのカスタマイズ性が高い Low-Codeでクイックに開発ができる点は、上記で見たように、レイアウトとコールバック関数を定義するだけでインタラクティブなダッシュボードを作成できることから明らかです。 Plotly は、様々なグラフを描画するPythonのライブラリです。Plotlyで作成したグラフに対しては、グラフ上でデータポイントをホバーして詳細を表示する、ズームする、グラフ内の範囲を選択するなどのインタラクティブな操作が可能です。Dashでは、 dcc.Graph モジュールを使って、Plotlyのグラフをアプリケーション内で表示できます。Plotlyのグラフに対するレイアウトやスタイルのカスタマイズも可能です。 さらに、Dashには、入力フォーム、ドロップダウン、ボタン、テーブルなど、オープンソースのコンポーネントが豊富にあります。これらのコンポーネントを組み合わせて、用途に応じたダッシュボードを作成できます。 Dashのほかにも、Low-Codeのウェブフレームワークとしては、 Streamlit があります。Streamlitを使っても、データ処理と可視化を一貫して行うことができます。しかし、Dashと比べてレイアウトのカスタマイズに制限があります。今回のダッシュボードについては、テーブルとグラフを水平方向に並べる際、レイアウトの微調整を必要としました。StreamlitではCSSを用いたスタイルの適用は困難ですが、Dashでは各コンポーネントモジュールにstyleを指定することでレイアウトの微調整が可能であるため、Dashを用いることとしました。 検索クエリごとのパフォーマンス指標のダッシュボード 今回作成した、検索クエリごとの商品クリック数、コンバージョン数(または率)を可視化したダッシュボードを紹介します。 ダッシュボードは、パラメータに分解した検索クエリを格納したテーブルと、各検索クエリに対応するパフォーマンス指標を表示するグラフで構成されています。 以下はダッシュボードのイメージです(グラフには適当な値を入れています)。 今回のダッシュボードには、以下の要素が含まれています。 検索クエリごとのパフォーマンス指標のテーブル(左下) 検索クエリごとのパフォーマンス指標のグラフ(右下) テーブルやグラフを操作するコントローラー(上部) これらの要素を順に説明します。 1. 検索クエリごとのパフォーマンス指標のテーブル ダッシュボードの左下にあるテーブル(下図)では、検索クエリを各パラメータ(ショップ、ブランド、カテゴリー等)をそれぞれのカラムに分割して表示しています。このテーブルは公式ドキュメントの「 DataTable Interactivity 」を参考に作成しました。 このテーブルには、ソート機能、フィルタ機能、ページング機能があります。ソート機能は、カラム名の横の三角形をクリックすることで使用でき、そのカラム内の値でテーブル全体をソートできます。また、フィルタ機能では、指定したフィルタの条件でデータを絞り込むことができます。紹介したイメージでは、platformが PC の検索クエリに絞って表示しています。さらに、ここでは1ページに20件の検索クエリを表示していますが、テーブル下部のページング機能から次の20件のデータを表示できます。 2. 検索クエリごとのパフォーマンス指標のグラフ 右下のグラフ(下図)は、テーブルの検索クエリごとの検索回数、商品クリック数、コンバージョン数を表示しています。テーブルの unique_id とグラフの unique_id によって、テーブル内の行とグラフ内のバーが対応しています。グラフの描画にはPlotlyを使っているため、ホバーやズーム等の操作ができます。 このグラフはテーブルの内容と連動しており、テーブルへの操作が行われるとコールバック関数が呼ばれてグラフも更新されるようになっています。 以下に、コールバック関数 update_graph の内容を説明します。 Input("table", "data") としており、これにより id が "table" であるコンポーネントの data が変わった時に update_graph 関数が呼ばれます。 update_graph 関数は、変更されたデータを受け取って新しいグラフを作成し、新しいグラフを格納したコンポーネントを返します。 update_graph 関数から返ってきた内容で、 Output に指定した id="graph-container" を持つ html.Div の中身が置き換えられ、表示されるグラフが更新されます。 # レイアウトを定義 app.layout = html.Div( [ ... html.Div( dash_table.DataTable( id = "table" , ... ) ), ... html.Div( id = "graph-container" ), ... ] ) @ app.callback ( Output( "graph-container" , "children" ), Input( "table" , "data" ), Input( "radio-button" , "value" ), ) def update_graph (queries: list [ dict ], radio-option: str ) -> html.Div: fig = go.Figure() # データの更新処理およびグラフの作成処理 ... # 更新したグラフを格納したコンポーネントを返す return dcc.Graph( id = "bar-chart" , figure=fig ) 3. テーブルやグラフを操作するコントローラー 上部のコントローラーには、データを検索クエリのパラメータでグルーピングするドロップダウン(下図左)と、グラフの表示データを切り替えるラジオボタン(下図右)を配置しています。 検索クエリのパラメータでのグルーピングには、先ほどのサンプルアプリケーションでも使用していたDropdownコンポーネントを使用しています。Dropdownコンポーネントで multi=True とすることで、複数のオプションから選択できるようになります。このドロップダウンからパラメータを選択すると、選択したパラメータでデータがグルーピングされ、その結果がテーブルに反映されます。 dcc.Dropdown( id = "multi-select-dropdown" , options=dropdown_options, value=dropdown_options, multi= True , ) ラジオボタンでは、グラフの数と率の表示を切り替えることができます。以下のラジオボタンのコードでは、 inputStyle でコンポーネントに適用するスタイルを指定しています。これはRadioButtonに限らず、そのほかのコンポーネントでも同様にスタイルを指定できます。 dcc.RadioItems( radio_options, "absolute" , id = "radio-button" , inline= True , inputStyle={ "margin-left" : "15px" , "margin-right" : "5px" , }, ) 今回作成したダッシュボードによって、表示するデータの範囲やパラメータの種類などをインタラクティブに操作しながら、検索クエリごとのパフォーマンス指標を可視化できるようになりました。 ダッシュボードによる情報共有の効果 今回のダッシュボードを用いるようになったことで、データ分析結果の情報共有時に以下の効果が得られました。 まず、効率的に共有が行えるようになりました。従来、データ分析のたびに、グラフやテーブルを作成してそれらをドキュメントにまとめる作業が必要でした。ファッション業界の季節性もありデータ分析結果の共有の頻度も高く、そのたびに新たなドキュメントを作成する手間が発生していました。今回のダッシュボードの導入により、共有のためのグラフやテーブルを手動で作り直す手間を軽減できました。これにより、定型作業から解放されたことで、分析や洞察に集中することが可能となりました。 さらに、インタラクティブな情報共有が可能になりました。これまでは、情報が一方的に共有されるだけであり、チームメンバーが共有された検索クエリ以外に関心がある場合はその場で確認することが困難でした。今回のダッシュボードにより、報告者以外のメンバーもデータを操作し、必要な情報を自由に探索できるようになりました。これにより、共有時のミーティングが対話的なものとなり、意見交換や意思決定が円滑に行えるようになりました。 まとめ 本記事では「Dash」を用いて検索クエリごとのパフォーマンス指標をインタラクティブに可視化した事例を紹介しました。 今後は、このダッシュボードを拡張させ、検索に関するそのほかのデータも可視化することで、さらに課題抽出や意思決定を加速化していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。検索基盤部の倉澤です。 検索機能におけるtypo(誤字脱字や綴り間違いなど)は難しい問題 1 とされています。typoの扱い方によってはユーザーに悪い検索体験を提供してしまう恐れがあります。例えば、typoを含む検索クエリを入力された時にユーザーが意図している検索結果を得ることができないといった問題があります。 例に漏れず、ZOZOTOWNでもtypoを含む検索クエリが入力された場合に検索結果が表示されないといった問題が発生しています。以下、「レディース」と入力するつもりが「レデース」と入力してしまった場合の検索結果です。 今回は日本語におけるtypoの一般的な解決策を調査・検証し、その結果・課題点を紹介します。手法の検証が容易であることを優先し、以下の2つの方法について検証しました。 Elasticsearchを用いてtypoを含む検索クエリでも検索結果を得る方法 ユーザーの検索クエリログを用いてtypoを正しいクエリに修正する方法 目次 はじめに 目次 Elasticsearchを用いた検索時のtypoの扱い方 Fuzzy match Synonym token filter ユーザーの検索クエリログを用いたスペルコレクション Peter Norvig, "How to Write a Spelling Corrector"を元にしたスペルコレクション 再検索クエリログを用いたスペルコレクション さいごに Elasticsearchを用いた検索時のtypoの扱い方 本章では、Elasticsearchを用いてtypoを含む検索クエリでも検索結果を得る方法について説明します。1つ目はFuzzy match(あいまい検索)と呼ばれる手法で、ユーザーが完全な検索クエリを入力しなくても、関連する結果を得られるようにします。2つ目はSynonym token filterを用いた手法で、typoクエリを修正クエリへ変換し検索結果を得られるようにします。 以下の表に、それぞれの手法の概要と、精度と運用コストの観点からの評価をまとめました。 手法 概要 精度 運用コスト Fuzzy match ElasticsearchのQuery DSLで用意されている Fuzzy query を用いたあいまい検索により検索結果を返す Not Good 入力されたクエリの文字数に対するfuzzinessの設定など検索結果をコントロールするのが難しい Good ElasticsearchのVersionによるメンテナンス程度で負担が小さい Synonym token filter Elasticsearchの Synonym token filter によりtypoクエリから正しいクエリへ展開し検索結果を返す Good Synonym token filterで定義しているクエリによってヒットする結果をコントロールすることができる Not Good 新しいtypoクエリが発見された場合にSynonym token filterの定義に追加するコストが発生し続ける Fuzzy match Elasticsearchには、あいまい検索を実現するために Fuzzy query が用意されています。Fuzzy queryは、レーベンシュタイン距離(以下、編集距離と呼ぶ)によりtypoを含む検索クエリでも検索結果を返すことができます。 編集距離は、ある文字列を別の文字列へ変換するために必要な操作回数を表します。具体的には、以下の4つの操作を用いて文字列を変換します。 文字の挿入 文字の削除 文字の置換 文字の転置 例えば、「レデース」という文字列を「レディース」に変換する場合、文字の挿入を1回行えば変換ができます。この場合の編集距離は1となります。 Fuzzy queryでは、この編集距離を fuzziness というパラメータを用いて指定でき、設定した値を最大値として範囲内に収まるキーワードをマッチさせることが可能です。 一方、Fuzzy queryには検索結果をコントロールするのが難しいという問題点があります。指定の編集距離内であれば、どのようなクエリでも検索結果を返してしまうため、関連しない結果がヒットしてしまう可能性があります。特に文字数が少ないクエリの場合は、指定の編集距離内に収まるクエリが多くなり、この問題が顕著になります。 例えば、ユーザーがズボンに関連する商品を探しており「ズボン」と入力するつもりが「ザボン」と入力してしまった場合について考えます。 GET kurasawa_test_index/_search { " query ": { " match ": { " keyword ": { " query ": " ザボン ", " fuzziness ": " AUTO " } } } } 上記の検索クエリを実行すると、以下のような結果が返ってきます。 ... " hits " : [ { " _index " : " kurasawa_test_index ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " ズボン ", ... } } , { " _index ": " kurasawa_test_index ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " リボン ", ... } } , { " _index ": " kurasawa_test_index ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " おぼん ", ... } } , ] 上記の検索結果を見てみると、ユーザーが意図している「ズボン」がヒットしていることがわかります。しかし、「リボン」や「おぼん」もヒットしていることがわかります。これらは「ザボン」に対する編集距離が1であることからヒットしており、ユーザーが意図していない結果もヒットしてしまっています。 このようにFuzzy queryではユーザーが意図していない結果もヒットしてしまうため、検索結果をコントロールするのが難しいという問題点があります。 過去の検索クエリのログからtypoのクエリとそれに対応する修正クエリが傾向として把握できている場合は、次章のSynonym token filterを用いることで検索結果をコントロールできます。 Synonym token filter 検索時のAnalyzerに Synonym token filter を追加することで、typoクエリから修正クエリへ変換し検索結果を返すことができます。Fuzzy queryとは異なり、予め定義した正しいクエリへと変換できるため、検索結果をコントロールできます。 以下に、Synonym token filterによる検証手順を説明します。前項の例を元に「ザボン」を「ズボン」に変換するようにSynonym token filterを定義します。 # インデックスの作成 PUT /kurasawa_test_synonym # アナライザーの定義 PUT kurasawa_test_synonym/_settings { " analysis ": { " filter ": { " my_synonym_filter ": { " type ": " synonym ", " synonyms ": [ " ザボン=>ズボン " ] } } , " analyzer ": { " my_index_analyzer ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer " } , " my_search_analyzer ": { " type ":" custom ", " tokenizer ": " kuromoji_tokenizer ", " filter ": [ " my_synonym_filter " ] } } } } # マッピングの定義 PUT kurasawa_test_synonym/_mapping { " properties ": { " keyword ": { " type ":" text ", " analyzer ": " my_index_analyzer ", " search_analyzer ":" my_search_analyzer " } } } # ドキュメントの登録 POST /kurasawa_test_synonym/_bulk { " index ": { " _id ": 1 } } { " keyword ": " ズボン " } { " index ": { " _id ": 2 } } { " keyword ": " リボン " } { " index ": { " _id ": 3 } } { " keyword ": " おぼん " } 上記で定義したインデックスに対して、以下の検索クエリを実行します。 GET kurasawa_test_synonym/_search { " query ": { " match ": { " keyword ": " ザボン " } } } 以下の検索結果から、ユーザーが意図している「ズボン」のみがヒットしていることがわかります。 { ... " hits " : [ { " _index " : " kurasawa_test_synonym ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " ズボン " } } ] } } このようにSynonym token filterを用いることで、typoクエリから修正クエリへ変換し検索結果をコントロールできます。 ただし、Synonym token filterを用いる場合は、予めtypoクエリと正しいクエリの対応を把握しておく必要があります。さらに、新しくtypoと思われるクエリが発見された場合は都度filterに定義を追加するなどの運用コストがかかります。そのため、Synonym token filterを用いるかどうかは、検索結果に対する精度と運用コストのバランスから判断する必要があります。 ユーザーの検索クエリログを用いたスペルコレクション 本章では検索する前にtypoクエリを修正することを想定したスペルコレクションの手法を紹介します。また、今回紹介する手法はユーザーの検索クエリのログを用いた手法です。 手法 概要 精度 Peter Norvig, "How to Write a Spelling Corrector" を元にしたスペルコレクション Peter Norvig, "How to Write a Spelling Corrector" の提案手法を日本語に適用するために改良した方法。typoクエリに対する修正候補を編集距離を用いて生成し、文章中の出現確率が最大となるクエリを出力する ZOZOTOWNのデータを用いて検証した結果、正しく修正できたクエリの割合: 69% 再検索クエリログを用いたスペルコレクション 各ユーザーの連続する検索クエリログから再検索されたクエリを修正候補のクエリとみなし、出現確率が最大となるクエリを出力する ZOZOTOWNのデータを用いて検証した結果、正しく修正できたクエリの割合: 14% Peter Norvig, "How to Write a Spelling Corrector"を元にしたスペルコレクション Peter Norvig, "How to Write a Spelling Corrector" の提案手法がスペルを修正する手順を簡単に説明します。具体的な手法については、文献を参照してください。 文章中の各クエリの出現頻度を算出する typoクエリに対する編集距離1の文字列を生成し、手順1の文章中に存在するクエリを修正候補のクエリとする typoクエリに対する編集距離2の文字列を生成し、手順1の文章中に存在するクエリを修正候補のクエリとする 修正クエリを以下の優先順位で決定する 4.1. 編集距離0の文字列が手順1の文章中に存在すれば、入力されたクエリを修正クエリとする 4.2. 手順2の修正候補クエリが存在すれば、その中から出現頻度が一番高いクエリを修正クエリとする 4.3. 手順3の修正候補クエリが存在すれば、その中から出現頻度が一番高いクエリを修正クエリとする 4.4. 入力されたクエリを修正クエリとする この提案手法を日本語に適用しZOZOTOWNのデータを用いて検証するためには、以下の点が課題になりました。この課題に対する原因と解決策を記載します。 課題 原因 解決策 ファッション用語に特化したコーパスの準備 ファッション用語に特化した外部公開されたコーパスが存在しない ZOZOTOWNのユーザーの検索クエリのログを用いる 日本語で編集距離1または2の文字列を生成する際の計算量が膨大 漢字・カタカナ・ひらがなの文字数を2000文字とすると、長さnの文字列に対する編集距離1の文字列を生成するための操作回数は、削除(n回)と置換(2000n回)、転置(n-1回)、挿入(2000(n+1)回)の合計4002n+1999回が必要となる 編集距離1または2の文字列の中から既知のクエリの集合を取得する処理をElasticsearchのFuzzy matchを用いる ElasticsearchのFuzzy matchを用いることで、編集距離1または2の文字列の中から修正候補となる既知のクエリの集合を取得する処理の高速化がのぞめます。 Peter Norvig, "How to Write a Spelling Corrector" の実装を一部変更し、ElasticsearchのFuzzy matchを用いたスペルコレクションを実装しました。 # 各クエリの出現頻度を保持する辞書 TERM_FREQ = { "ズボン" : 10 , "リボン" : 5 , "おぼん" : 2 } # Fuzzy queryを実行するためのクエリ ES_QUERY = { "query" : { "fuzzy" : { "keyword" : { "value" : "" , "fuzziness" : "2" } } }, "size" : 20 } # Elasticsearchのインデックス名 ES_INDEX_NAME = "kurasawa_test_index" # Fuzzy matchを用いて編集距離1または2の文字列の中から修正候補となる既知のクエリの集合を取得する関数 def edit (es_client, word): ES_QUERY[ "query" ][ "fuzzy" ][ "keyword" ][ "value" ] = word result = es_client.search(index=ES_INDEX_NAME, body=ES_QUERY) return set (hit[ "_source" ][ "keyword" ] for hit in result[ "hits" ][ "hits" ]) def known (word): return set (w for w in word if w in TERM_FREQ) def correct (es_client, word): candidates = known([word]) or edit(es_client, word) or [word] return max (candidates, key= lambda w: TERM_FREQ.get(w, 0 )) correct(es_client, "ザボン" ) >> "ズボン" 今回、上記の解決策を用いてスペルコレクションを実装した結果の評価結果を以下に記載します。評価方法は、 Peter Norvig, "How to Write a Spelling Corrector" の「Evaluation」の章で紹介されている方法を用いて、独自に準備した正解データ約50件に対して、修正されたクエリが正解データと一致しているかどうかを確認しました。 検索クエリログ 評価結果 過去1週間 かつ 検索回数が100回以上の検索クエリログ 正しく修正できたクエリの割合: 69% 約70%のクエリを正しく修正できていますが、以下のような課題も残っています。 出現回数を保持する辞書に存在しないが、typoではないクエリを誤変換してしまう可能性がある 正しいクエリよりも出現回数が多いtypoクエリを修正候補として返してしまう可能性がある 再検索クエリログを用いたスペルコレクション 各ユーザーの連続する検索クエリのログから起点になるクエリとその直後に発生する再検索クエリを1つのペアとします。このクエリ同士の編集距離と検索ログ内でのペアの発生確率から修正クエリを見つけることを考えます。 例えば以下のような検索クエリログがあったとします。 ユーザーID クエリ 再検索クエリ A ザボン ズボン B ザボン ズボン C ザボン リボン 「クエリ」と「再検索クエリ」をペアとして、発生確率と編集距離を計算します。 (クエリ, 再検索クエリ) 発生確率 編集距離 (ザボン, ズボン) 2/3 1 (ザボン, リボン) 1/3 1 編集距離2以下のペアに限定し、発生確率が最大となる再検索クエリを修正クエリとします。つまり、上記の例では「ザボン」を「ズボン」に修正します。 これらの簡易的な手順を実装すると以下のようになります。 import pandas as pd import Levenshtein def correct (df, word): result = {} for record in df[df[ "query" ]==word].values: query = record[ 0 ] next_query = record[ 1 ] ratio = record[ 2 ] distance = Levenshtein.distance(query, next_query) if distance < 3 : result[next_query] = ratio return max (result, key=result.get) df = pd.DataFrame( data={ "query" : [ "ザボン" , "ザボン" ], "next_query" : [ "ズボン" , "リボン" ], "ratio" : [ 0.6666 , 0.3333 ] } ) word= "ザボン" correct(df, word) >> "ズボン" こちらの手法も、前章で紹介した手法と同様に評価しました。また、正解データも同様のものを用いました。 検索クエリログ 評価結果 過去1週間 かつ 検索回数が100回以上の検索クエリログ 正しく修正できたクエリの割合: 14% 以下の課題により、前章で紹介した手法よりも精度が低くなってしまいました。 過去の検索クエリのログにない未知なtypoクエリを修正することが出来ない ユーザーがtypoに気づかず再検索クエリのログに正しいクエリが存在しない場合に修正することが出来ない さいごに 今回はElasticsearchを用いた検索時のtypoへの対応方法とtypoを修正するためのスペルコレクションについて紹介しました。 検索におけるtypoは、検索結果の精度やユーザー体験を低下させる要因となります。一方で、日本語は漢字、カタカナ、ひらがなといった複数の文字で構成されていることや同音異義語、多様な表記揺れにより、一般的にtypoの修正は難しいとされています。 ZOZOTOWNの検索機能でもtypoへの対応にはまだまだ課題があります。引き続き検索精度の向上に取り組んでいきます。 ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com 書籍「 Query Understanding for Search Engines 」にも同様な言及がなされています。 ↩
はじめに こんにちは。基幹システム本部・物流開発部の作田です。現在、ZOZO社内で使用している基幹システムのリプレイスを担当しています。 現在行っているリプレイスでは、既存の基幹システムから発送機能を切り出し、マイクロサービスに移行しています。リプレイスの詳細については、ZOZOBASEを支える発送システムリプレイスの取り組みをご覧ください。 techblog.zozo.com マイクロサービスは発送業務の各作業が完了したことを基幹システムに連携しており、この連携を実現するために Amazon Managed Streaming for Apache Kafka (以降、Amazon MSK)を採用しました。今回は、サービス間のデータ連携にAmazon MSKを採用した理由やAmazon MSKでの実装例と考慮点について紹介します。MySQLなどのリレーショナルデータベースに対してAmazon MSKを用いて非同期でデータを連携する事例として参考になれば幸いです。 目次 はじめに 目次 システム要件と設計 システム要件 発送マイクロサービスが独立して動作可能 更新したデータの即時連携 システム設計 技術選定 選定理由 非機能要件の検証 信頼性と可用性の検証 オフセット情報の消失 Kafka BrokerやMSK Connectの停止 検証によって分かったデメリット 実装例 MSK Connect Event Consumer consume処理 例外発生時の処理 おわりに システム要件と設計 システム要件とそれを満たすために考えた設計を順番に説明します。 システム要件 発送マイクロサービスから基幹システムにデータを連携させるにあたって2つの要件がありました。 発送マイクロサービスが独立して動作可能 更新したデータの即時連携 この2つのシステム要件について簡単に説明します。 発送マイクロサービスが独立して動作可能 発送マイクロサービスは基幹システムに障害が発生しても発送業務を遂行できることを目指しています。そのため、発送マイクロサービスが独立して動作できるシステムを構築する必要がありました。 更新したデータの即時連携 発送マイクロサービスは発送業務の各作業(ピック作業や梱包作業)が完了したことを基幹システムに連携する必要があります。連携されたデータは実績の確認などに使用され、一時的に発送マイクロサービスとデータが異なることは許容できますが結果整合性を担保する必要があります。また、実績の乖離による作業者の混乱を防ぐためデータの連携には即時性が求められました。 これらの要件を満たすには、サービス間を疎結合にして非同期でデータを連携する必要があります。要件を満たすためのシステム概要図は以下の通りです。基幹システムは発送業務に必要なデータを非同期で発送マイクロサービスに連携し、発送マイクロサービスはデータの変更を非同期で基幹システムに連携します。今回は発送マイクロサービスから基幹システムへのデータ連携に焦点を当てます。 システム設計 非同期でデータを連携させる上で、データベースの更新とメッセージングシステムへの書き込みに整合性が必要となります。データベースとメッセージングシステムへそれぞれ書き込みを行った場合、どちらかの処理が失敗するとデータの不整合が発生してしまいます。この問題を解決するために、 Outboxパターン を採用しました。 発送マイクロサービスでのOutboxパターンの実装例を説明します。前提として、データベースには社内で広く使われているAurora MySQLを選定しています。Outboxパターンの考えに基づいて、集約の状態更新(itemsテーブルの更新)とドメインイベントの発行(item_eventsテーブルへの書き込み)を同一トランザクションで行います。そして、メッセージリレーはイベントテーブルに書き込まれた内容をメッセージブローカーに送信します。これにより、データベースの更新とメッセージングシステムへの書き込みの整合性を担保できます。 技術選定 Outboxパターンを実現するために、 Apache Kafka のフルマネージドサービスであるAmazon MSKを採用しました。次の節で、Amazon MSKを採用した理由について説明します。 選定理由 Amazon MSKを採用した理由は、Debeziumコネクタが提供されているApache Kafkaをフルマージドで運用可能だからです。なぜDebeziumやApache Kafkaが必要なのかを順番に説明します。 データベースにMySQLを選定しているため、Outboxパターンの実現にはMySQLのデータ変更を検知してメッセージブローカーにデータを送信する仕組みが必要です。データがいつどのように変更されたかを検出する仕組みのことをChange Data Capture(以降、CDC)と言います。今回はCDCを実現するために Debezium を採用することにしました。Debeziumは様々なデータベースに対してCDCを実現でき、MySQLではbinlogを読み取りコミットされた変更を検知しています。 DebeziumはApache Kafka Connectを使用してデプロイされることが一般的 で、Kafka Connectを使用することでデータソースやデータターゲットへの接続が容易になります。そのため、メッセージリレーにApache Kafka Connect、メッセージブローカーに Apache Kafka を採用しました。Apache Kafkaはストリーミングデータを処理するために必要なアプリケーションの実行や構築ができるプラットフォームです。 そして、Apache Kafkaの運用のコストをできるだけ下げるために、フルマネージドサービスであるAmazon MSKを採用しました。Amazon MSKには、Kafka Brokerを持つMSK ClusterとDebeziumをカスタムプラグインとしたMSK Connectが含まれています。MSK Connectではイベントテーブルの変更を検知して、MSK Clusterにメッセージを送信しています。Event Consumerは、MSK Clusterに対して100ミリ秒間隔でメッセージのポーリングを行い、データベースを更新しています。Amazon MSKによって、できるだけ早く別のサービスにデータ連携するという要件をフルマネージドなサービスで実現できました。 非機能要件の検証 Amazon MSKは社内での導入実績がなかったため、SRE担当者と協力し以下の非機能要件について検証しました。 性能・拡張性 信頼性 可用性 性能・拡張性に関しては、複数のシナリオを考慮した負荷試験を行い、本番相当のデータをinsertし続けてもMSK ConnectがMSK Clusterにデータを送れることを確認しました。信頼性と可用性に関しては次の節で詳しく説明します。 信頼性と可用性の検証 信頼性と可用性を高めるために以下の点について考慮しました。 オフセット情報の消失 Kafka Brokerの停止 MSK Connectの停止 オフセット情報の消失はアプリケーション側で考慮しており、Kafka BrokerやMSK Connectの停止はインフラ側で考慮しているのでそれぞれ説明します。 オフセット情報の消失 Event ConsumerがPartitionのどこまでメッセージを読み込んだかのオフセットはKafka Broker内のトピックで管理しています。このオフセット情報が何かしらの理由で失われてしまった場合を考慮して、 ConsumerConfig.AUTO_OFFSET_RESET_CONFIG に earliest を設定しています。earliestに設定すると、オフセット情報が存在しない場合はPartitionの最初からメッセージを読み直します。これにより全てのメッセージが1回以上読み出されるため、Event Consumerは冪等性を考慮した実装が求められます。本システムでは、イベントテーブルにシーケンス用のカラムを用意し、イベントの発行順序が分かるようにしています。このシーケンス番号を元に処理済みのメッセージかどうかを判断し、重複処理をしないようにしています。 また、MSK Connectもどこまでメッセージを送信したかのオフセットを指定したトピックに保存しています。このオフセット情報が消失した場合、binlogの最初から再度メッセージが送信されますが、Event Consumer側で重複処理をしないように実装しているため問題ありません。 Kafka BrokerやMSK Connectの停止 Kafka BrokerやMSK Connectが何かしらの理由で停止してしまった場合を考慮して、AZ障害を模擬した障害試験を実施しました。MSK Clusterでは3つのBrokerを用いて冗長化し、Partitionごとにデータをレプリしているため、Kafka Brokerの1つが停止してもデータが欠損しません。実際に1つを停止してみましたが、発送マイクロサービスから別のサービスへのデータ連携が滞りなく行われることを確認しました。 MSK ConnectではDebeziumを使用しているため、 1つのタスクしかサポートされておらず 冗長化できません。また、何らかの理由でfailed(=produce処理ができない)の状態で止まってしまった場合は再起動できず、再作成をする必要があります。再作成に15分程度かかりますが、再作成が完了すればデータの欠損なく連携できることを確認しています。頻繁にMSK Connectの再作成が必要であれば、 Airbyte などの他のツールへの乗り換えを検討していきたいです。 検証によって分かったデメリット 検証によって、MSK Connectがfailedの状態で止まった場合や設定を変更した場合に再作成しなくてはならないことが分かりました。MSK Connectの再作成時に発生するデメリットは以下の通りです。 再作成が完了するまでの15分程度はメッセージの送信ができない 再作成時にテーブルに対して読み取りロックが発生する(100万件で1分程度) 今回は、これらのデメリットを容認できたため、Amazon MSKでデータ連携することを決めました。参考としてご覧いただけると幸いです。 実装例 ここからはMSK ConnectとEvent Consumerの実装について紹介します。 MSK Connect Producerの実装は行わず、MSK ConnectにDebeziumを採用することでマネージドにCDCを実現しました。MSK Connectはイベントテーブルのみを監視するように設定し、イベントテーブルごとにトピックを用意しています。これにより、イベントテーブルAの変更はトピックA、イベントテーブルBの変更はトピックBにメッセージが送信されます。 Kafkaは 同一のPartitionでのみ順序を保証している ため、順序保証が必要であれば同一のPartitionにメッセージを入れる必要があります。デフォルトでは監視しているテーブルの主キーのハッシュに基づいて各Partitionにメッセージを振り分けます。今回のケースでは、同じ集約IDのイベントに対して順序保証をしたいため、イベントテーブルの主キーではなく集約IDのカラムに基づいて各Partitionに振り分ける設定を入れています。現時点では、バージョン2.1のDebeziumを使用しているため ComputePartition で設定していますが、廃止予定なので PartitionRouting で設定できるように対応する予定です。 Event Consumer 次に、Event Consumerの実装例を紹介します。使用している言語のバージョンは以下の通りです。 種類 バージョン Java 17 Spring Boot 2.7.1 Spring Boot 2.7.xでKafkaを使用するため、 build.gradle に依存関係を追加します。 implementation 'org.apache.kafka:kafka-clients:3.2.3' implementation 'org.springframework.kafka:spring-kafka:2.9.3' コードの関係は以下のようになっています。今回は EventConsumer.java と KafkaConsumerConfig.java について説明していきます。 app/ |-- EventConsumer.java `-- config |-- KafkaConsumerConfig.java `-- KafkaConsumerSettings.java consume処理 EventConsumer.java では、 @KafkaListenerアノテーション を使用してメッセージのconsume処理を実装しています。consumeメソッドはメッセージが受信できたときに呼び出され、どのパーティションからメッセージが来たのかなどを把握できます。このコード例では受け取ったメッセージをログに出力しています。後続の処理では、メッセージの値をJavaのクラスに変換してデータベースを更新していますが、consume処理とは直接関係ないため割愛します。 @Slf4j public class EventConsumer { @KafkaListener (topics = "${kafka.consumer.topic}" ) public void consume(ConsumerRecord<String, String> record) { log.info( String.format( "Consumed event from %s topic, partition %d : key = %s, value = %s" , record.topic(), record.partition(), record.key(), record.value())); // record.value()に対して処理を行う } } @KafkaListener アノテーションを使用するためには、 @Configuration と @EnableKafka アノテーションが付与されたクラスを作成し、リスナーコンテナーファクトリを用意する必要があります。コード例に記載されている KafkaConsumerSettings クラスでは application.yaml から読み取った環境変数の値を保持しており、 KafkaConsumerConfig クラスではリスナーコンテナーファクトリを提供しています。 KafkaConsumerConfig クラスの consumerFactory メソッドではConsumerに関する設定を定義しており、 getDefaultErrorHandler メソッドでは例外発生時の処理方法を指定しています。 @Slf4j @EnableKafka @Configuration @RequiredArgsConstructor @EnableConfigurationProperties ({KafkaConsumerSettings. class }) public class KafkaConsumerConfig { private final KafkaConsumerSettings settings; @Bean KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() { final ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); factory.getContainerProperties().setPollTimeout( 3000 ); factory.setCommonErrorHandler(getDefaultErrorHandler()); return factory; } private ConsumerFactory<String, String> consumerFactory() { final HashMap<String, Object> props = new HashMap<>(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, settings.getBootstrapServers()); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, settings.getAutoOffsetReset()); props.put(ConsumerConfig.GROUP_ID_CONFIG, settings.getGroupId()); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer. class ); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer. class ); return new DefaultKafkaConsumerFactory<>(props); } private DefaultErrorHandler getDefaultErrorHandler() { // 3秒から始まり2倍ずつ増えていく。最大値は30秒 // ex) 3, 6, 12, 24, 30, 30, 30, ... final var backOff = new ExponentialBackOff( 3000L , 2 ); // 合計で1分経過したら再試行を停止する backOff.setMaxElapsedTime( 60000L ); final var defaultErrorHandler = new DefaultErrorHandler( (consumerRecord, exception) -> { log.error(exception); // 処理できなかったメッセージをデータベースに保存する }, backOff); // SQL周りで発生した例外だけリトライする defaultErrorHandler.addNotRetryableExceptions(Exception. class ); defaultErrorHandler.addRetryableExceptions(SQLException. class ); return defaultErrorHandler; } } 例外発生時の処理 getDefaultErrorHandler メソッドでは、SQL周りで例外が発生した場合のみ処理をリトライするように設定しています。リトライ時は、エクスポネンシャルバックオフを指定し、書き込み先のデータベースに過度な負荷がかからないようにしています。リトライ試行期間が合計1分を超えたら DefaultErrorHandler で定義した処理が実行されます。DefaultErrorHandlerで定義した処理にも失敗した場合は、オフセットがコミットされないので再び同じメッセージをポーリングします。この仕組みによって、書き込み先のデータベースがダウンしているときは、永遠に同じメッセージを処理し続けるようにしてデータの連携が止まるようにしています。 メッセージが処理できなかった場合は、受け取ったメッセージをそのままデータベースに保存するようにDefaultErrorHandlerで設定しています。 dead-letter用のトピックにメッセージを送信する方法 が一般的で、デットレタリングされたメッセージを元のトピックに戻すなどのリカバリー処理が容易です。しかし、処理できなかったメッセージを直接確認するためにはトピックからメッセージを受け取る仕組みを作成する必要があり、データベースと比べて手間がかかります。初期リリース時は処理できなかったメッセージの確認をすぐに行いたかったため、データベースにメッセージを保存するという選択をしました。 おわりに 発送マイクロサービスの初期リリースが完了し、現在は既存の基幹システムから段階的に移行しています。この過程では、引き続き既存の基幹システムを使用しながら、発送マイクロサービスで少しずつ発送処理をしています。現時点では、データ処理量が限定的なのでデータの連携は数秒ほどで完了しています。負荷試験において、本番環境と同量のデータが処理できることを確認しましたが、既存システムと同量のデータを持続的に処理したわけではありません。処理するデータ量が増えたときに問題点が見つかれば改善していきたいです。 本記事では、マネージドサービスであるAmazon MSKを用いてMySQLに対してCDCを実現する事例を紹介しました。Amazon MSKはOutboxパターンでドメインイベントを伝える仕組みと相性が良いため、イベント駆動型アーキテクチャでの導入を検討してみてはいかがでしょうか。 ZOZOの基幹システムの開発・リプレイスに興味を持ってくださった方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめまして、ZOZOTOWNアプリ部Android1ブロックの池田一成です。普段はZOZOTOWN Androidアプリ開発を担当しています。 ZOZOTOWNアプリは歴史の長いアプリのため、レガシーなコードがいくつか残っています。そのため、Android Lintで検出されるビルドワーニングが複数放置されたままの状態になっていました。これらのビルドワーニングは潜在的なバグを生み出す可能性やメンテナンスコストを増加させる可能性があります。ZOZOTOWNアプリにおいても機能改修をした際に新たに発生したビルドワーニングを検知できず、リリース後不具合に繋がってしまったことがありました。本記事では、JetBrains製の Qodana という静的解析ツールを用いた既存のビルドワーニングの可視化と新規のビルドワーニングを発生させない仕組みづくりについての取り組みをご紹介します。 Qodanaとは 導入背景 Qodanaを導入する 構成ファイルの準備 GitHub Actionsへの導入 計測結果の可視化 Qodanaを活用した取り組み 重大度の高いビルドワーニングへの対応 新規のビルドワーニングの検知 まとめ Qodanaとは Qodanaは各種CIツールと連携可能なコード品質プラットフォームで、Java、JavaScript、PHP、Kotlin、Go、C# など60以上の言語で記述されたコードの問題を検出、解析、および解決できます。 公式ブログ より抜粋したQodanaに委任できるタスクの一覧を以下に引用します。 コードの問題を早期にキャッチ。コードが実際に本番環境にプッシュされる前に問題を解決できます。 後で見つかった問題を解決するのはコストが高くつきます。 異常なコードを検出。プロジェクトには一般的でない方法で記述されたコード箇所は、プロジェクトのセキュリティリスクになる可能性があります。 コードレビューを自動化。未使用のインポート、複製、スペルや書式の問題など、複数のチェックを自動化し、フィードバックループにかかる時間を短縮できます。 デッドコードを除去。これにより、無関係な処理の実行をなくし、プログラムの実行時間を短縮できます。 コンプライアンスリスクを低減。Qodana のライセンス監査でプロジェクトが使用している依存関係を追跡できます。 追跡することで、ライセンス要件の準拠を維持しやすくなります。 コード構造を改善。コードを読みやすく、メンテナンス性の高いものにします。 Qodana なら、インデント、名前付けスキーム、行の長さ制限など、コードの一貫性を確保できます。 コーディングのベストプラクティスを導入。プロジェクトやビジネスの要件に基づき、コードを独自のコードポリシー(特定のプログラミングスタイルガイドなど)に準拠させることができます。 導入背景 ZOZOTOWN Androidアプリには既にたくさんのビルドワーニングが存在しています。そのため、Android Lintではアプリの改修等で新しく発生した問題の検知が難しく、アプリ全体のビルドワーニングの可視化と新規のビルドワーニングを検知する仕組みが必要でした。Qodanaの機能の1つにビルドワーニングの種別 1 をサンバースト図として表示する機能があります。この機能を活用することで、前述した課題を解決し、下記の目的を達成できると判断したためQodanaを導入することにしました。 アプリ全体のビルドワーニングの数や種類の可視化 新規のビルドワーニングの発生を検知 また、ZOZOTOWN Androidチームでは静的解析ツールに ktlint と Android Lint を導入しています。ktlintはKotlinコードのスタイルガイドに従ってコードを検証し、一貫性のあるスタイルを保つのに役立ちます。主にコードフォーマットやスタイルに関連する問題を検出するために導入しています。Android LintはAndroidプロジェクト内で一般的なバグや問題を検出するためのツールとして、非推奨なAPIの使用、リソースの問題、潜在的なメモリリークなどを検出するために導入しています。ktlintとAndroid Lintは、それぞれコードのスタイルやAndroidアプリの品質向上のための優れたツールですが、下記の観点から並行してQodanaを導入することにしました。 JetBrainsが設計した独自の検査により総合的な静的解析を提供するため、Android Lintとktlintでは見逃されるかもしれない幅広い問題やバグを検出できる Qodanaはカスタマイズ性が豊富であり、プロジェクトのニーズに合わせてチェックを調整できる。そのため既存のktlintやAndroid Lintルールと組み合わせて、さらに厳格な検証ができる Qodanaを導入する ZOZOTOWN AndroidチームではGitHub Actionsを利用しているため、その導入手順を紹介します。 公式ドキュメント が非常に整っていたため、簡単に導入できました。 構成ファイルの準備 プロジェクトのroot配下にqodana.ymlを作成します。このファイルをカスタマイズすることで解析の条件や使用するリンターを設定できます。 version : "1.0" linter : jetbrains/qodana-jvm-android:2023.2 profile : name : qodana.recommended projectJDK : 11 今回はAndroidのプロジェクトを解析するため、リンターには qodana-jvm-android を設定します。このリンターを設定することでJavaやKotlin、Gradleなど複数の言語の解析ができます。その他にはPHPやPython、Goなどのリンターが用意されています。リンターの詳細な内容については公式ドキュメントの 「Linters」ページ をご参照ください。 profileには starter と recommended を設定できます。 starter は重要なチェックのみ使用され、プロジェクトの初回スキャンに最適です。 recommended はほとんどのプロジェクトに幅広く適した事前選択済みのインスペクション一式を有効にする設定と説明されています。ZOZOTOWNではより詳細な解析をするため recommended を利用しています。profileの詳細な内容については公式ドキュメントの 「Inspection profiles」ページ をご参照ください。 GitHub Actionsへの導入 Qodanaには過去の結果と比較できる ベースライン機能 があります。これにより、新しい問題、変更されていない問題、解決された問題を確認できます。ZOZOTOWN Androidチームではこのベースラインを利用して運用しています。そのJobは下記のようになります。 qodana : runs-on : ubuntu-latest steps : - uses : actions/checkout@v3 - name : calculate previous run number env : NUM : ${{ github.run_number }} run : | echo "GITHUB_PREVIOUS_RUN_NUMBER=$(($NUM - 1))" >> $GITHUB_ENV - name : Download artifact id : download-artifact uses : dawidd6/action-download-artifact@v2 with : run_number : ${{ env.GITHUB_PREVIOUS_RUN_NUMBER }} if_no_artifact_found : ignore - uses : JetBrains/qodana-action@v2023.2 with : args : --baseline,./qodana-report/results/qodana.sarif.json upload-result : true - name : Deploy to GitHub Pages uses : peaceiris/actions-gh-pages@v3 with : github_token : ${{ secrets.GITHUB_TOKEN }} publish_dir : ${{ runner.temp }}/qodana/results/report destination_dir : ./docs/qodana 本来、ベースライン機能を利用する場合、比較対象となる qodana.sarif.json 2 をプロジェクトのroot配下に置いてQodanaを実行する必要があります。しかし、そのような運用をGitHub Actionsで行うとGitのリポジトリサイズが増加したり、定期的なjsonファイルの更新が必要になります。そのため、他のWorkflowやJobのArtifactsの取得を可能にする action-download-artifact というライブラリを利用することにしました。これにより、CI上でArtifactsにアップロードされた qodana.sarif.json を参照できるようにしました。 このJobでは現在実行している run_number から前回の実行時の run_number を計算し、前回のArtifactsを取得することでベースライン機能を利用しています。これにより前回実行時との差分を新規のビルドワーニングと既存のビルドワーニングとしてダッシュボードに出力できます。ダッシュボードについては後述します。 計測結果の可視化 Qodanaで計測されたデータをGitHub ActionsのArtifactsに保存し、GitHub Pagesにホストすることで計測結果を可視化しました。サンプルプロジェクトでの計測結果を用いて簡単にダッシュボードの使い方を紹介します。 ACTUAL PROBLEMS:この計測でQodanaが検出したビルドワーニングを表示します BASELINE:以前の実行からそのまま残っているビルドワーニングを表示します タブを切り替えることで、この計測において新たに検知したビルドワーニングと前回の計測からそのまま残っているビルドワーニングを分けて確認できます。また、ビルドワーニングの種類や言語、重大度によってビルドワーニングをフィルターして表示できます。 画像のように、ビルドワーニングの発生しているファイル名や内容、行数を確認できるためプロジェクト全体のビルドワーニングの把握がしやすくなり、スムーズに修正に取り掛かることができます。 Qodanaを活用した取り組み ここからは、ZOZOTOWN Androidチームにおけるビルドワーニングへの取り組みを紹介します。 重大度の高いビルドワーニングへの対応 プロジェクト内に存在する多くのビルドワーニングに対応するため、QodanaのSeverity(重大度)による分類をもとに優先順位をつけて修正しました。 具体的には、優先度の高いビルドワーニング対応のチケットをファイル単位で作成し、そのファイルに存在するビルドワーニングを優先度に関わらずすべて修正する方針で進めました。このように進めることで修正による影響範囲を抑えつつ、優先度の高いビルドワーニングを着実に減らしながら、全体のビルドワーニング数も効率的に減らせるようにしました。 新規のビルドワーニングの検知 ZOZOTOWN Androidチームでは2つの方法でQodanaを実行するように仕組み化しています。 1つ目は機能実装するfeatureブランチのベースブランチであるdevelopブランチに変更がプッシュされたタイミングで実行しています。これは、機能実装の間で発生したビルドワーニングの変化を確認したいためです。これをしておくことで、後述するfeatureブランチで実行する仕組みと合わせて差分が確認できるようになります。 2つ目は、Pull Requestに特定のラベルを貼ったタイミングで実行しています。これはfeatureブランチでQodanaを実行したい時に行います。このJobでは1つ目で実行されたQodanaの計測結果を取得しベースライン機能を活用して、developブランチとfeatureブランチ間の計測結果の差分を表示できるようにしています。こうすることにより、featureブランチで発生したビルドワーニングをダッシュボードで確認できるようになります。また、Qodana実行の際の引数に post-pr-comment を渡すことで下記のような計測結果のサマリーをコメントしてくれるためレビュー時の見落としが減るかと思います。PRが作成されたタイミングやプッシュされたタイミングでQodanaを実行する方法も検討しましたが、実行時間が長いため、ラベルで実行を選択できるようにしました。 MeasureWarning : name : Measure the warning if : github.event.label.name == 'ワーニング計測' runs-on : ubuntu-latest steps : - uses : actions/checkout@v3 - name : Download artifact id : download-artifact uses : dawidd6/action-download-artifact@v2 with : workflow : code_quality.yml - name : Qodana uses : JetBrains/qodana-action@v2023.2 with : args : --baseline,./qodana-report/results/qodana.sarif.json upload-result : true - name : Deploy to GitHub Pages uses : peaceiris/actions-gh-pages@v3 with : github_token : ${{ secrets.GITHUB_TOKEN }} publish_dir : ${{ runner.temp }}/qodana/results/report destination_dir : ./docs/qodana まとめ 本記事では、ZOZOTOWN AndroidにおけるQodanaを用いたビルドワーニングへの取り組みを紹介しました。Qodanaの導入によりビルドワーニングの可視化と定期的な計測ができるようになりました。ZOZOTOWNアプリ内のビルドワーニングの数は導入前と比較すると減少しているものの、まだまだたくさん残っています。そのため、今後はビルドワーニングを0件にする運用の検討を進めていきたいと考えています。Qodanaの導入を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co 本記事のビルドワーニングはAndroid Studio内の問題のあるコードが黄色でハイライト、重大な問題の場合は、コードに赤色の下線が引かれるポップアップテキストを示します。 ↩ Qodana実行時に出力されるSARIF仕様に従ってフォーマットされたJSONファイル形式のレポート ↩
はじめに こんにちは、ブランドソリューション開発本部バックエンド部SREブロックの小林( @mirai_kobaaaaaa )です。普段は WEAR や FAANS というサービスのSREとして開発、運用に携わっています。 WEARではAmazon Elastic Kubernetes Service(以下、EKSと呼ぶ)を用いて複数システムのインフラ基盤を構築・運用しています。その中の1つとして、ワークフロー処理の実行基盤が存在しています。 本記事では、そのワークフロー実行基盤が抱えていた課題と、それらをどのように解決したのかを紹介します。また、付随して得られたメリットについても紹介いたします。 目次 はじめに 目次 WEARにおけるワークフロー ワークフロー処理内容 ワークフロー実行基盤の構成 ワークフロー実行基盤の課題 コスト内訳の調査 過剰なPodスペック Fargate実行時間の増大 ワークフロー実行基盤の改修方針 EKS on EC2へのリプレイス ワーカーノードのプロビジョニング方法 各Podのノード配置戦略 ワーカーノードのインスタンスタイプ選定 ワーカーノードのスケーリングプロダクト選定 スケーリングプロダクトの概要 選定ポイントと選定結果 リプレイス作業 EKSクラスターの作成 Operator系Podの配置 Karpenterの設定 切り替え 結果 その他影響 終わりに WEARにおけるワークフロー まずは、WEARにおけるワークフローとは何か、どのような構成だったのかを紹介します。 ワークフロー処理内容 WEARで運用しているワークフロー実行基盤は、例えば以下のような処理を行っています。 コーディネート情報の更新 アイテム情報の更新、紐付け ユーザー情報の更新 これらは決まった時間に実行されるスケジュールワークフローとして稼働しています。処理内容によって数分で終わるものから数時間かかるものが存在しており、1日あたり約1500件が実行されています。 ワークフロー実行基盤の構成 前述の通り、ワークフロー実行基盤はEKSを用いて構築されていました。また、Pod実行基盤としてはAWS Fargate(以下、Fargateと呼ぶ)を採用していました。Fargateで実行されるPodは自動的にワーカーノードがプロビジョニングされるため、運用負荷を減らせると考えたからです。 ワークフロー実行時には子タスクとして1つ以上のJobが起動します。Job実行時には単一のPodがプロビジョニングされます。ワークフローの内容によっては複数のJobが実行されることもあります。 また、このEKSにはワークフローシステム以外に各種Kubernetes Operator(以下、Operatorと呼ぶ)が存在していました。GitOpsで利用するArgo CDやAWSのElastic Load Balancingを管理するためのAWS Load Balancer Controller等です。 ワークフロー実行基盤の課題 ワークフロー実行基盤が抱える最たる課題はコストでした。ワークフロー数や実行回数が増えるにつれ、EKS内でプロビジョニングされるPodも単純増加し、比例してコストも増加していきます。 WEARにおけるコーディネート情報更新数やアイテム処理件数の増加はサービス拡大と同意義です。しかし、サービス拡大とサービスを支える基盤コストが単純比例してしまう構成では、サービス成長を鈍化させる要因の1つとなってしまいます。 これらの理由から、コスト効率性の高い基盤を構築することが急務と考え、ワークフロー実行基盤の改修を決定しました。 コスト内訳の調査 ワークフロー実行基盤において、まずは何がコストセンターになっているかを把握する必要がありました。そのため、AWS Cost Explorerや各種ログ、メトリクスを用いてコスト内訳を調査しました。 その結果、下記要因によりコスト増加を引き起こしていることがわかりました。 過剰なPodスペック Fargate実行時間の増大 Fargateコストについても触れながら、それぞれの要因について説明していきます。 過剰なPodスペック Fargateは、割り当てられたスペック、実行時間、Pod数に応じて課金されます。つまり、Podに割り当てられたスペックが大きいほど、コストも増加していくことになります。 CloudWatchやDatadogを確認したところ、Podに割り当てられたスペックと実際に消費されるメトリクスに乖離がありました。これにより不要なコストが発生していました。 この要因については、各Podに対して適切なリソースを割り当て直すことで早々に改善が見込めると考えました。 Fargate実行時間の増大 続いて、Fargate実行時間の増大についてです。 先に述べた通り、Fargateコストは実行時間にも比例します。実行時間はイメージのダウンロードを開始した時間からPodが終了するまでの時間を指します。 Kubernetesログを確認したところ、1Pod起動にあたり25秒程度のイメージダウンロード時間がかかっていました。1日に起動されるPod数は1500以上であるため、1日あたりのイメージダウンロード時間は約10時間にも及びます。 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Pulling 27s kubelet Pulling image "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/workflow:latest" Normal Pulled 2s kubelet Successfully pulled image "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/workflow:latest" in 25.246397319s Normal Created 2s kubelet Created container workflow Normal Started 2s kubelet Started container workflow この問題を解決するため、まずはイメージサイズの削減を検討しました。WEARはモノリシックなアプリケーションとして構成されており、イメージ自体が大きくなりがちでした。そのため、このアプローチが有効であると考えました。 イメージサイズの削減方法としてはzstdによるイメージ圧縮方法の変更を検討しました。zstdはMeta社によって開発された圧縮アルゴリズムであり、使用していたgzip圧縮より圧縮率と解凍速度の向上が見込めたからです。 zstd圧縮テストの結果、イメージサイズを削減できました。しかし、私たちのケースではイメージダウンロード時間へ与える影響は極小規模に留まったため、別のアプローチを検討することにしました。 余談ですが、zstd圧縮アプローチについてはAWS公式よりブログが公開されています。イメージサイズにお困りの方は是非ご覧ください。 aws.amazon.com 最終的に選ばれたのは、EKS on FargateからEKS on EC2へのリプレイスでした。ワーカーノードとしてEC2を使用することで、イメージキャッシュを用いたイメージダウンロード時間の削減が期待できます。 また、ワークフローの性質上、Podは頻繁に入れ替わることが予想されます。そのため、ピーク時の同時実行数をもとにEC2サイズを決定することで、Fargateと比べてより少ないワーカーノードでワークフローを処理でき、ワーカーノードのコスト削減が可能だと考えました。 ワークフロー実行基盤の改修方針 各要因へのアプローチ方法を検討した結果、下記方針で進めることにしました。 Podに割り当てられたスペックの再調整 EKS on FargateからEKS on EC2へリプレイス まずは、1つ目のPodに割り当てられたスペックを再調整しました。 計測された実績値に基づいて各Podに割り当てられたスペックを調整します。テスト等を挟みましたが、作業自体は数日で完了しました。 2つ目のEKS on FargateからEKS on EC2へのリプレイスについては、より詳細に設計しました。 EKS on EC2へのリプレイス EKS on EC2へリプレイスするにあたり、下記を決定しました。 ワーカーノードのプロビジョニング方法 各Podのノード配置戦略 ワーカーノードのインスタンスタイプ選定 ワーカーノードのスケーリングプロダクト選定 ワーカーノードのプロビジョニング方法 最初にワーカーノードをどのようにプロビジョニングするかを考えました。EKSでEC2をノードとして利用する場合、大きく分けてセルフマネージドノードとマネージドノードの2種類があります。セルフマネージドノードは自身で作成したEC2インスタンスをノードとして利用し、必要に応じてAmazon EC2 Auto Scalingを作成、管理します。一方マネージドノードは、AWSによってEC2インスタンスが自動作成され、それに紐づくAmazon EC2 Auto Scalingもプロビジョニングされます。 今回はセルフマネージドノードを使用するほどのカスタマイズ性が必要ないこと、既に別クラスターで運用経験があったことからマネージドノードを採用しました。また、ワークフローの途中停止リスクを下げたいため、スポットインスタンスではなくオンデマンドインスタンスを使用することにしました。 各Podのノード配置戦略 2つ目は、どのPodをどのワーカーノードに配置すべきかという問題です。先に述べた通り、ワークフロー実行基盤のEKSクラスター内にはArgo CD等のOperatorも存在しています。Operatorと同様のワーカーノードにPodを配置することで、ノード数を削減し、コストがより圧縮できます。しかし、Operator系Podによるノードレベルの影響を避けたいと考え、ワークフロー実行PodはOperatorとは別のワーカーノードに配置しました。 ワーカーノードのインスタンスタイプ選定 続いて3つ目に記載したワーカーノードのインスタンスタイプ選定です。調査でも述べましたが、ワークフローの性質上、常に一定のPodが実行されているわけではありません。そのため、ピーク時の同時実行数や負荷傾向をもとにインスタンスタイプを決定しました。 ちなみに、現在は後述するKarpenterの採用によってインスタンスタイプを細かく管理していません。ワークロードの負荷傾向から最適となるようインスタンスファミリーを選択するのみに留まっています。 ワーカーノードのスケーリングプロダクト選定 最後に、ワーカーノードのスケーリングプロダクト選定です。Fargateは1つのPodに対して1つのワーカーノードが自動でプロビジョニングされます。つまり、Horizontal Pod Autoscaler等を用いてPod数を増加させることでワーカーノードを含めた水平スケーリングが可能です。 しかし、EC2をワーカーノードとして使用する場合、Podのスケーリングだけでなくワーカーノードのスケーリング方法も考慮する必要があります。今回はEKSで利用できるスケーリングプロダクトを調査し、比較の上選定しました。 スケーリングプロダクトの概要 EKSでは下記2つのスケーリングプロダクトをサポート 1 しています。 Cluster Autoscaler Karpenter Cluster AutoscalerはKubernetesが公式で用意しているスケーリングプロダクトです。EKSにおける動作としてはマネージドノードグループ及びAuto Scaling Groupを書き換えることで、EC2インスタンスを追加起動し、起動プロセス完了後にEKSへワーカーノード登録をします。 一方で、KarpenterはOSSとして公開されているスケーリングプロダクトであり、Cluster Autoscalerとは異なる方法でスケーリングを行います。Karpenterは待機中Podのリソースリクエストに応じて必要なワーカーノードのサイズを計算し、必要に応じてワーカーノードを追加、削除します。この時、Auto Scaling Groupは利用しません。ワーカーノード追加時には、待機中Podの情報をもとにした必要な容量の計算、要求を満たすEC2インスタンスの選択と起動、EKSへのワーカーノード登録をします。 ( https://karpenter.sh/ より引用) 選定ポイントと選定結果 選定のポイントとして、スケーリング速度に重点をおきました。ワークフロー実行基盤において、スケーリング速度は非常に重要です。スケーリングに時間がかかってしまえば、ワークフロースケジュール全体、ひいてはサービスへの悪影響が発生してしまいます。 下記はPodがスタートするまでの比較結果 2 です。 イメージキャッシュなし はワーカーノード追加と読み替えてください。Fargateはワーカーノード追加も同時に行うため、 イメージキャッシュなし としています 3 。 スケーリングプロダクト イメージキャッシュなし イメージキャッシュあり Cluster Autoscaler 70s 3s Karpenter 60s 4s Fargate 57s - EC2インスタンスをワーカーノードとして登録する工程が発生するため、Fargateが優位だろうと推測していました。しかし、意外にもKarpenterも十分な速度を有していました。また、イメージキャッシュが存在する場合のPod起動速度はやはりEC2が非常に高速です。 検討を重ねた結果、Karpenterを採用しました。決め手となったのは、必要なリソースリクエストに応じて柔軟にEC2インスタンスを選択する機能を有していたからです。これは今回の課題に対して非常に魅力的でした。 リプレイス作業 最終的な構成はこのようになりました。 Operator系Podはcommonというマネージドノードグループに配置します。CPU使用率の低いPodがほとんどであり、極稀に負荷が上がるもののバーストクレジットで対応可能と考えT系インスタンスを使用しています。 Karpenterは普段から一定の処理をしており、かつ本構成の心臓部分です。そのため、安定したワークロードを実行できるようにM系インスタンスと独立したマネージドノードグループで構成しています。 実際にワークフローが実行されるワーカーノードの管理はKarpenterに委ねています。スケーリングプロダクト選定でも述べた通り、Karpenterは待機中Podのリソース要求をもとに最適なインスタンスタイプを計算、決定し起動します。 EKSクラスターの作成 既存環境への影響を考慮し、Blue/Greenデプロイメントを利用してクラスターを切り替えることにしました。そのため、新しいクラスターを作成する必要があります。以下は terraform-aws-modules/eks/aws を用いたクラスター設定例です。Submoduleの eks-managed-node-group を使用し、マネージドノード設定も記述しています。 # main.tf module "workflow_cluster" { source = "terraform-aws-modules/eks/aws" version = "19.6.0" cluster_name = "workflow-cluster" cluster_version = 1 . 24 vpc_id = var.vpc_id subnet_ids = var.subnet_ids enable_irsa = true # Karpenterがこのタグを見てノードを立ち上げる node_security_group_tags = { "karpenter.sh/discovery" = "workflow-cluster" } # Submoduleのeks-managed-node-groupを利用 eks_managed_node_groups = { # Operatorを設置するマネージドノードグループ common = { name = "common" desired_size = 2 min_size = 2 max_size = 6 instance_types = [ "t3.large" ] disk_size = 20 capacity_type = "ON_DEMAND" iam_role_additional_policies = { AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } labels = { NodeGroupName = "common" } } # Karpenter本体を設置するマネージドノードグループ karpenter = { name = "karpenter" desired_size = 2 min_size = 2 max_size = 6 instance_types = [ "m5.large" ] disk_size = 20 capacity_type = "ON_DEMAND" iam_role_additional_policies = { AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } labels = { NodeGroupName = "karpenter" } } } } Operator系Podの配置 構成図の通り、マネージドノードグループが2種類存在するため、適したPodを適したノードグループに配置する必要があります。そのため、各OperatorのmanifestにNode Affinityを設定し、Podを配置するノードを宣言的に選択します。私たちはArgo CD経由でHelm Chartを使用していたため、helm valuesに設定を追加します。 helm : values : | affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : NodeGroupName operator : In values : - common この設定により、NodeGroupNameというLabelにcommonという値が設定されたノードにのみPodが配置されます。 Karpenterの設定 続いてKarpenterの設定です。KarpenterがEC2を起動できるようIRSAを作成します。 # main.tf # Karpenter用のIRSAを作成 module "karpenter_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" version = "5.14.0" role_name = "workflow-cluster-karpenter-controller" attach_karpenter_controller_policy = true karpenter_tag_key = "karpenter.sh/discovery" karpenter_controller_cluster_id = module.workflow_cluster.cluster_name karpenter_controller_node_iam_role_arns = [ module.workflow_cluster.eks_managed_node_groups [ "karpenter" ] .iam_role_arn ] oidc_providers = { ex = { provider_arn = module.workflow_cluster.oidc_provider_arn namespace_service_accounts = [ "karpenter:karpenter" ] } } } # Karpenterが起動するEC2インスタンスにアタッチするインスタンスプロフィール resource "aws_iam_instance_profile" "karpenter" { name = "workflow-cluster-KarpenterNodeInstanceProfile" role = module.workflow_cluster.eks_managed_node_groups [ "karpenter" ] .iam_role_name } Karpenter本体をArgo CDのApplicationとしてデプロイします。 # karpenter.yaml apiVersion : argoproj.io/v1alpha1 kind : Application metadata : name : karpenter namespace : argocd finalizers : - resources-finalizer.argocd.argoproj.io spec : destination : namespace : karpenter server : https://kubernetes.default.svc project : default source : chart : karpenter repoURL : public.ecr.aws/karpenter targetRevision : v0.27.0 helm : releaseName : karpenter parameters : - name : 'settings.aws.clusterName' value : 'workflow-cluster' - name : 'settings.aws.defaultInstanceProfile' value : 'workflow-cluster-KarpenterNodeInstanceProfile' - name : 'settings.aws.clusterEndpoint' value : 'https://xxxxxxxxxxxxxxxxxxxxxx.gr7.eu-west-1.eks.amazonaws.com' - name : 'serviceAccount.annotations.eks\.amazonaws\.com/role-arn' value : workflow-cluster-karpenter-controller values : | affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : karpenter.sh/provisioner-name operator : DoesNotExist - matchExpressions : - key : NodeGroupName operator : In values : - karpenter syncPolicy : syncOptions : - CreateNamespace= true automated : prune : true KarpenterはProvisionerというCustom Resourceを用いてワーカーノードの管理をします。インスタンス構成やネットワーク設定を指定することで、起動するインスタンスの制御が可能です。 # provisioner.yaml # KarpenterがAWSに立ち上げるEC2インスタンスの設定 apiVersion : karpenter.sh/v1alpha5 kind : Provisioner metadata : name : workflow-provisioner spec : # 起動するワーカーノードにworkflowというlabelを設定 labels : NodeGroupName : workflow requirements : - key : karpenter.k8s.aws/instance-category operator : In values : [ m, r ] - key : karpenter.sh/capacity-type operator : In values : [ "on-demand" ] - key : kubernetes.io/os operator : In values : - linux - key : kubernetes.io/arch operator : In values : - amd64 provider : # EC2を起動するサブネットやセキュリティグループ、タグを指定 subnetSelector : karpenter.sh/discovery : workflow-cluster securityGroupSelector : karpenter.sh/discovery : workflow-cluster tags : karpenter.sh/discovery : workflow-cluster # DaemonSet以外のPodが存在しない状態が30秒続くとワーカーノードを削除する ttlSecondsAfterEmpty : 30 最後に、ワークフロー実行PodがKarpenter経由で起動したワーカーノードに配置されるようNode Affinityを設定します。 # workflow.yaml spec : affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : NodeGroupName operator : In values : - workflow Node Affinityを設定することで、ワークフロー実行PodはworkflowというLabelが設定されたノードにのみ配置されるようになります。また、ワーカーノードのリソースが枯渇している場合はKarpenterが自動でEC2インスタンス起動、EKSへワーカーノードを登録します。 切り替え 先ほど述べた通り、Blue/Greenデプロイメントを用いた切り替えを行いました。 旧環境のワークフローを停止 新環境のワークフローを開始 停止期間中のワークフローを再実行(必要に応じて) 各種管理コンソールのドメイン切り替え 事前に疎通確認は取れていたこと、ほとんどのワークフローが後続実行でリカバリーできることから、ユーザーへの影響は発生しませんでした。 結果 これらの改修により、ワークフロー実行基盤のコストを大幅に削減できました。青い部分が旧環境の想定コスト、赤い部分が実際のコストです。 特に効果的だったのは、EC2インスタンスへPodを集約したことでした。これにより、効果的にリソースを使用でき、30%程度のコスト削減を実現できました。 その他影響 今回の改修は主にコスト削減を目的としていましたが、パフォーマンスにも影響がありました。特にイメージキャッシュの効果は大きく、連続して複数Podが実行されるようなワークフローでは、実行時間を20%程度高速化できています。 また、ワークロードに最適なEC2インスタンス 4 を割り当てることで処理も高速化され、7時間程度かかっていたワークフローを5時間程度まで高速化できました。 これらはWEARのサービス性質上、非常に喜ばしい結果でした。 終わりに 以上のように、WEARのワークフロー実行基盤を改修する取り組みを進めました。 ワークフロー実行基盤の構築当初は、Fargateを使用することで運用負荷を抑制でき、その時間を他の開発に割り当てできました。しかし、「今のWEAR」から見つめ直した時、違うアプローチを取るべきだと判断しました。このように、実装当時は最適であったものが、サービスの成長や環境変化と共に常に変わることを再認識できました。 現在、ワークフロー実行基盤は安定して稼働しています。引き続きサービスをより良いものにできるようコストとパフォーマンスの両軸で改善を進めていきます。 WEARでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://hrmos.co/pages/zozo/jobs/0000021 hrmos.co 2023年8月16日時点の情報です。今後変更される可能性があります。 ↩ 測定結果は環境によって異なります。あくまで参考値としてご覧ください。 ↩ 厳密にいえばFargateは純粋なスケーリングプロダクトではありません。今回は比較のために記載しています。 ↩ EC2インスタンスはインスタンスタイプによってプロセッサやアーキテクチャが異なります。そのため、ワークロードによっては同一vCPU、Memoryのインスタンスであってもパフォーマンス差異が発生します。参考: https://aws.amazon.com/jp/ec2/instance-types/ ↩
はじめに こんにちは。ブランドソリューション開発本部バックエンド部SREの山岡( @ymktmk )です。普段はファッションコーディネートアプリ「 WEAR 」のSREとしてクラウドの運用やリプレイスをおこなっています。 昨年から、私たちのチームでは分散した技術スタックをKubernetesへ統一するリプレイスプロジェクトを開始し、先月ついにKubernetesへの移行が完了しました。 techblog.zozo.com また、Kubernetesへの段階的な移行と並行して、Kubernetesの柔軟性を活かした運用改善や開発者体験の向上に取り組んできました。その一環として、k6-operatorを活用した負荷試験基盤を作成しました。 本記事ではWEARにKubernetesネイティブな負荷試験基盤を導入した背景とその効果についてご紹介します。Kubernetes環境における負荷試験基盤の導入を検討している方の参考になれば幸いです。 目次 はじめに 目次 導入の背景 負荷試験基盤の設計 要件 負荷試験ツールの選定 選定対象の負荷試験ツールの概要と比較 k6の選定理由 負荷試験基盤のアーキテクチャ GitHub Actionsワークフローを介した負荷試験の実行 負荷試験結果レポートのDatadogダッシュボードの活用 負荷試験の実施フローとその仕組み テストシナリオの作成 テストシナリオのクラスターへの自動適用 負荷試験の実行 負荷試験の結果 導入後の効果 まとめ おわりに 導入の背景 私たちのチームでは、これまで負荷試験に関して、各々が異なる負荷試験ツールを使用しており、統一されていませんでした。そのため、チームにおける負荷試験のノウハウの蓄積が難しく、特に新規メンバーは負荷試験の実施が容易ではありませんでした。テストシナリオも手探りで書く必要があり、開発効率が悪くなっていました。 このような課題を解決し、負荷試験の容易な実施とノウハウの蓄積を可能にするため、負荷試験基盤を導入しました。 負荷試験基盤の設計 要件 負荷試験基盤を構築するにあたって、3つの要件を定めました。 1. 簡素で馴染みのある言語でテストシナリオを記述できること 導入の背景で述べた通り、私たちのチームでは各々が異なる負荷試験ツールを使用しており、チーム内でのノウハウが蓄積されていませんでした。そのため、負荷試験ツールの統一が必要でした。 負荷試験ツールの統一にあたり、負荷試験を実施する開発者が簡素で馴染みのある言語でテストシナリオを記述できることはテストシナリオの作成・共有が容易になり、開発効率の向上が期待できます。 2. 分散負荷試験が可能であること 単一のマシン上で行う負荷試験では、そのマシンの性能の限界に達するまでの負荷しかかけることができません。分散負荷試験は、想定される大きな負荷を再現し、アプリケーションの振る舞いを再現できます。 3. 負荷試験結果をレポートとして残せること 負荷試験結果をレポートとして残すことで、チームメンバーとの負荷試験結果の共有を容易にし、レビュー時においてメンバーが理解しやすくなります。また、過去の負荷試験結果と比較できるため、パフォーマンスの劣化・向上とその要因を調査しやすくなります。 これらの要件を満たすことで、チームの課題を解決できると考えました。そして、適切な負荷試験ツールの選定とインフラ設計をしました。 負荷試験ツールの選定 選定対象の負荷試験ツールの概要と比較 はじめに述べた通り、私たちのチームは全ての基盤をKubernetesに移行することを決定しました。こうした背景から、Kubernetesの特徴である柔軟性を活かして負荷試験基盤を作成しようと考えていました。 そのため、Kubernetes上で実現可能な負荷試験ツールを調査しました。その結果、以下のツールが選定対象として挙げられました。 Gatling Locust k6 それぞれのツールの特徴を見ていきます。 GatlingはテストシナリオをScala(Gatling 3.7からはJavaやKotlinもサポート)のDSLで記述します。社内でも利用実績が豊富であり、GatlingをベースとしたKubernetes Operatorである gatling-operator を開発しています。 techblog.zozo.com LocustはテストシナリオをPythonで記述します。Google Cloud Platformでは、「Google Kubernetes Engineを使用した負荷分散テスト」というテーマでKubernetes環境におけるlocustを使用した分散の負荷試験を紹介しています。 cloud.google.com k6はテストシナリオをJavaScriptで記述します。k6にはKubernetes Operatorである k6-operator が公式から提供されています。 これら3つのツールを要件に基づいて比較した結果が以下です。 言語の馴染み深さ 分散負荷試験 レポート出力 Gatling × ○ ○ Locust ○ ○ ○ k6 ○ ○ ○ k6の選定理由 Gatlingは社内で多くの利用実績がありましたが、テストシナリオをScalaで記述する必要があり、普段Rubyで開発しているWEARのエンジニアには馴染みがありませんでした。そのため、選択肢から外れました。 選定候補として残ったのがLocustとk6の2つでした。どちらも最低限の要件を満たしていましたが、最終的にk6を選択しました。k6を選んだ理由は、私たちのチームでは保守・運用性を重視し、Kubernetes Operatorであるk6-operatorが公式から提供されているk6を選択しました。さらに、チーム内で既にk6の利用実績があったことも選定の決め手となりました。 負荷試験基盤のアーキテクチャ 要件を考慮した上で、負荷試験基盤のアーキテクチャは以下のようになりました。 負荷試験の開始から終了までは以下の流れになります。 GitHub Actionsワークフローを介してk6 Custom Resourceをクラスターに適用 k6-operatorがk6 Custom ResourceをWatchし、Podを起動して負荷試験を開始 Slackに負荷試験結果レポートのDatadogダッシュボードを送信 GitHub Actionsワークフローを介した負荷試験の実行 負荷試験の実行には、k6-operatorが実行されているクラスターにk6 Custom Resourceを適用する必要があります。ただし、k6 Custom Resourceをローカルから手動適用するのは手間がかかります。そのため、GitHub Actionsのワークフローを実行し、k6 Custom Resourceを適用するようにしました。GitHub Actionsを利用することで負荷試験を容易に実施でき、利便性が高いと考えました。 負荷試験結果レポートのDatadogダッシュボードの活用 負荷試験結果レポートはk6が Datadogのインテグレーション に対応していることから、Datadogダッシュボードを利用することにしました。また、私たちのチームでは以前からアプリケーションやDBのモニタリングにDatadogを利用していたため、Datadogダッシュボードを使って負荷試験結果レポートを確認できることは好都合でした。 次に実際に負荷試験を実施する際のフローとその仕組みを説明します。 負荷試験の実施フローとその仕組み テストシナリオの作成 負荷試験基盤の実施者は、テストシナリオを記述します。以下はテストシナリオの簡単な例です。 1人の仮想ユーザーが100秒間、1秒につき1回 https://test.k6.io に対してGETリクエストを送信し、レスポンスのHTTPステータスコードが200であることを確認する負荷試験です。 import http from 'k6/http' ; import check from 'k6' ; export const options = { vus: 1, duration: '100s' , rps: 1, } ; export default function () { const response = http.get( 'https://test.k6.io' ); check(response, { "status is 200" : (r) => r. status === 200 } ); } テストシナリオのクラスターへの自動適用 k6-operatorを使用して負荷試験を実施するには、テストシナリオを記述するだけでは不十分です。実際に負荷試験を行うには、k6-operatorが実行されているクラスターに以下のようなk6 Custom Resourceを適用する必要があります。 apiVersion : k6.io/v1alpha1 kind : K6 metadata : name : k6 namespace : k6-operator-system spec : parallelism : 1 arguments : --out statsd script : configMap : name : scenario file : test01.js runner : env : - name : K6_STATSD_ENABLE_TAGS value : "true" - name : K6_STATSD_ADDR value : "datadog-agent.datadog.svc.cluster.local:8125" k6 Custom ResourceはConfigMapと、それに格納しているテストシナリオのJavaScriptファイルを指定する必要があります。したがって、記述したテストシナリオを元に負荷試験を行うためには、その都度テストシナリオのJavaScriptファイルからConfigMapを作成しなければなりません。 そこで、 kustomize のconfigMapGeneratorを使用してテストシナリオのJavaScriptファイルからConfigMapを動的に生成するようにしました。 apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization configMapGenerator : - name : scenario namespace : k6-operator-system files : - scenario/test01.js - scenario/test02.js - scenario/test03.js - scenario/test04.js generatorOptions : annotations : argocd.argoproj.io/compare-options : IgnoreExtraneous 私たちのチームでは、 Argo CD を使ってKubernetesマニフェストのデプロイを実現しています。そのため、テストシナリオを負荷試験の専用のGitHubリポジトリへPushするだけで、テストシナリオのJavaScriptファイルからConfigMapを動的に生成し、クラスターに適用されます。 負荷試験の実行 GitHub Actionsの workflow_dispatch トリガーを使ってWeb UIからワークフローを実行し、k6 Custom Resourceを適用します。引数を設定する画面にて、テストシナリオであるJavaScriptファイルをプルダウンから選択することで、容易に負荷試験を実施できます。また、負荷試験の実行時に、vus、duration、rps、parallelismなどのパラメータを容易にオーバーライドできるよう実装しています。 ワークフローを実行すると、k6 Custom Resourceがクラスターに適用され、k6-operatorがPodを起動し、負荷試験を行います。この際の負荷試験のメトリクスは、Datadog Agentを介してDatadogに送信されます。 負荷試験の結果 負荷試験が終了すると、負荷試験用のSlackチャンネルに負荷試験結果レポートである、DatadogダッシュボードのURLが送信されます。 URLをブラウザで開くと、Datadogダッシュボード上で負荷試験結果レポートが閲覧できます。 導入後の効果 今回作成した負荷試験基盤の導入により、今年入社した新卒エンジニアなどの新規メンバーでも手軽に負荷試験を実施でき、負荷試験を実施するハードルを下げることができました。 また、負荷試験ツールの統一により、チーム内での負荷試験のノウハウが蓄積され、開発者の開発効率が向上しました。 さらに、今回の取り組みにより、専用のGitHubリポジトリを使用して負荷試験を管理できるようになったことが非常に良かったです。テストシナリオの作成から負荷試験の実行、結果の確認まで、チームレビューが行き届くようになりました。これにより、以前に個人で行っていた時と比べて、負荷試験の妥当性が向上し、心理的な負担も軽減しました。 まとめ 今回は、WEARにおけるKubernetesネイティブな負荷試験基盤の導入とその効果についてご紹介しました。この負荷試験基盤の導入により、負荷試験が手軽に実施できるようになり、負荷試験へのハードルが下がりました。 また、これを機に従来の負荷試験についても見直すことができました。今後も負荷試験基盤の利用者がより使いやすくなるよう継続的に改善していきます。 さらに、今後は負荷試験基盤だけではなく、Kubernetesの柔軟性を活かして運用改善や開発者体験の向上に取り組んでいきます。 おわりに WEARでは一緒にサービスを改善してくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co
はじめに こんにちは、DevRelブロックの @wiroha です。DevRelの3名による連載「ZOZO TECH BLOGを支える技術」の3本目、最終回として Looker Studio (旧データポータル)を使ったBLOGの数字分析についてご紹介します。 Looker Studioは、データを視覚的にわかりやすいグラフやチャートにして表示するツールです。ZOZO内ではCSレポートの作成や、計測プロダクトのデータ分析などで活用しており、これまでに記事も公開しています。 techblog.zozo.com techblog.zozo.com ZOZO TECH BLOGの数字分析において実際に運用しているレポート画面は下図のとおりです。この画面の作成に至るまでの経緯と作成方法を解説していきます。 現在運用しているレポート画面 目次 はじめに 目次 背景・課題 導入検討 レポート作成の前準備 レポートの作成手順 1. レポートを作成する 2. レポートにデータを追加する 3. コンポーネントを追加する 4. フィールド・フィルタを追加する 5. 共有する 応用編:はてなブックマークの件数を表示する レポートの活用 まとめ 背景・課題 以前までは数字分析にGoogle アナリティクスを導入していたものの、把握したい数字だけを見やすくしたレポート化・可視化までは行っていませんでした。そのため、記事を書いてもどれだけ見られているかの反響を拾いにくいという課題がありました。また、Google アナリティクスは権限を持つ一部の人しか閲覧できないという課題もありました。今年2月に私がDevRel専任として入社し、これらを解決すべく取り組むことにしました。 導入検討 アクセス数・SNSシェア数・SNSコメント内容など把握したい項目を洗い出し、優先度をつけて実現の可否を調査しました。X(Twitter)上でのRT・コメントはリアルタイム性が高いため専用のSlackチャンネルへ流すことにし、任意のタイミングで見たい数字面をレポート化することにしました。レポート化の手段は次の理由でLooker Studioを採用しました。 グラフや表を使ってデータをわかりやすく視覚化できる レポートエディタのUIが優れておりストレスなく作成できる 複数のデータソースに接続してデータを統合できる ZOZOはGoogle Workspaceを使用しているため、作成したレポートを組織内のユーザに共有しやすい 特に複数のデータソースからデータを統合できる点は魅力的です。Google アナリティクス、Google スプレッドシート(以下、スプレッドシートとする)、BigQueryなどさまざまなデータソースからデータを取得し、1つの表に結合して表示できます。利用用途とも合致しており、スムーズに導入が決まりました。 接続可能なデータソース(一部) レポート作成の前準備 レポートの作成には元となるデータソースが必要です。ZOZO TECH BLOGは既にGoogle アナリティクスを導入していました。もし導入していない場合は設定します。はてなブログであれば、はてなブログ ヘルプの「 Google Analyticsを導入する 」ページに詳細の解説があります。 追加でカスタムディメンションの設定も行いました。これにより記事の投稿日やリンククリックの計測などができるようになります。はてな開発ブログの「 はてなブログで Google アナリティクス 4の設定が可能になりました 」を参考にpost_date, link_url, link_textといったディメンションを追加しました。これで前準備は完了です。 追加したカスタムディメンションの一覧 レポートの作成手順 基本的な流れは次のとおりです。 Looker Studio にログインし、レポートを作成する レポートにデータを追加する コンポーネント(グラフやコントロール)を追加する フィールドやフィルタの追加により表示したい内容に更新する 共有する それぞれ画像を含めて詳細を解説します。 1. レポートを作成する Looker Studio にログインすると、空のレポートを作成するか、テンプレートを元にした新規作成ができます。テンプレートから作成しカスタムしていくと手間が少なくおすすめです。 「テンプレートを使って開始」画面 2. レポートにデータを追加する テンプレートを元に作成する場合、上部の「自分のデータを使用」からデータソースを指定し追加できます。 「データのレポートへの追加」画面 3. コンポーネントを追加する 上部の「グラフを追加」を選択すると表・スコアカード・グラフなどのコンポーネントを追加できます。 「グラフを追加」で種類を選択できる Google アナリティクスの数字はディメンションと指標の組み合わせで構成されており、表示内容は画面右側の「グラフ」パネルで設定します。「グラフ」内のディメンション・指標は右端の「データ」パネルにあるフィールドから選択します。 「グラフ」のディメンション・指標で設定 上記画像で「大陸」「地域」のプルダウンになっている部分が「コントロール」と呼ばれるコンポーネントです。デバイスごとに見比べたり、期間を1週間・1か月など切り替えて見たいときに活用できます。 4. フィールド・フィルタを追加する 既存のフィールドや関数を組み合わせて独自のフィールドを追加できます。活用例を見てみましょう。次の図は記事ごとのイベント数を表にしたものです。 このままでも良いのですが、ページタイトルの末尾にすべて「 - ZOZO TECH BLOG」がついており、繰り返しになっています。社内で見る分には自明な情報なのでカットすると次のようになります。 スッキリとして見やすくなりました。これは次の図のようにページタイトルをREPLACE関数で加工したものを新しいフィールドとして保存し、ディメンションに設定することで実現しています。 使用できる関数はLooker Studioのヘルプページ「 関数リスト 」にまとめられています。特にURLへのリンクを付与する HYPERLINK 関数は便利です。 また先ほどの表ではフィルタ機能も使っています。投稿日でソートするために、下図のような設定をしてpost_dateがfalseのデータを除外しました。トップページやカテゴリー画面など投稿日が存在しないページではfalseが入る仕様のようです。グラフパネルの「フィルタを追加」から行います。 グラフパネル設定下部の「フィルタを追加」を選択 「フィルタの編集」で除外条件・一致条件を設定できる フィルタの条件には正規表現も使用できます。こうしてコンポーネント・フィールド・フィルタを追加して見たい内容を作っていけばレポートの作成は完了です。 5. 共有する Google ドキュメントやGoogle スプレッドシートを使ったことがある方なら馴染みのある画面で共有の設定ができます。リンクを知っている組織内のユーザーであれば誰でも閲覧できるようにすると、複雑なユーザ管理を考えなくて済みます。もちろん秘匿性の高い情報のレポートを作成する場合はきちんと制限できます。 アクセス権の管理とリンク設定を行える 応用編:はてなブックマークの件数を表示する 応用編として2つのデータソースを1つの表に結合している例を紹介します。レポート上の「最近の記事」部分のはてなブックマーク数はスプレッドシートから、PV数はGoogle アナリティクスからデータを取得しています。 レポートの「最近の記事」部分 データソースのスプレッドシート スプレッドシート側の数値は はてなブックマーク件数取得API を利用して取得しています。タスク自動化ツールの「 Zapier 」から定期的にAPIを呼び出し、結果を書き込む仕組みです。きちんと実装するのであればプログラムを書いてcronで実行し、APIで取得した結果はデータベースに保存するところですが、なるべくノーコードで実現して保守性を高める方向性にしました。Zapierの使い方を説明すると長くなってしまうため、ここでは割愛します。 スプレッドシートの作成後、Looker Studio上部の「データを追加」から「Google スプレッドシート」を選択して追加します。追加できたら画面上部メニューの「リソース」から「統合を管理」を選択します。「統合を追加」を選択すると、結合の設定画面が開きます。 結合の設定画面 結合条件を設定し、Google アナリティクスとGoogle スプレッドシートのデータを結合します。データソース名をつけて保存すると「混合データ」として追加され、これを元に表やグラフを作成できるようになりました。 「混合データ」が追加されている 応用編を含め、今回紹介したノウハウを駆使すれば冒頭で紹介したレポートが作成できます。同じような課題を抱いている方の参考になれば幸いです。 レポートの活用 Looker Studioの導入によって数字状況を追いやすくなり、反響の多かった記事から派生してイベントを開催するなど次のアウトプットにもつながるようになりました。 レポートを活用し、執筆者へフィードバックする試みも新たに始めました。PV数・はてなブックマーク数・記事に対するコメントなどを共有することで「書いて良かった」「また書きたい」と感じてもらいたいと考えています。 ただし気をつけたいのは、短期的なPV数だけにとらわれないことです。特に執筆者は「他の人に比べて見られていないのではないか」とかえって不安になるかもしれません。分析を通して行いたいのは、記事の魅力をより伝わりやすくしたり、まだ外に出ていない知見を見つけ出したり、執筆者のモチベーションにつなげたりする改善をまわすことです。これらを損なわないよう分析だけでなくコミュニケーションも大切にしながら、継続的なアウトプットの支援を続けていきたいと思います。 まとめ 本記事ではLooker Studioを使ったTECH BLOGの数字分析について紹介しました。レポートによって可視化することで日々の確認や、他のアウトプットへの展開、執筆者へのフィードバックがしやすくなりました。今後はカテゴリーごとの偏りや読了率といった高度な分析もしていきたいと考えています。 現在DevRelブロックメンバーの募集は行なっていませんが、一緒にZOZOのサービスを作り上げてくれる方を募集中です。TECH BLOGなどのアウトプットが好きな方はDevRelがサポートします。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、CTO/DevRelブロックの堀江( @Horie1024 )です。本記事はZOZO DevRelチームによる連載「ZOZO TECH BLOGを支える技術」の2本目の記事です。 前回の記事ではZOZO TECH BLOGの概要とその運用について紹介しました。今回の記事ではTECH BLOGの運用プロセスのうち記事の執筆に焦点を当て、執筆とそのレビュー体制を支えるCI/CDフローの整備について紹介します。 目次 はじめに 目次 ZOZO TECH BLOGでのCI/CDの活用 記事の静的解析と文章校正 記事のプレビュー環境へのデプロイ CI/CDフローの構築 CI/CDフローの概要 文章校正 プレビュー環境へのデプロイ フォーマット・画像のアップロード プレビューへの記事の反映 公開済みの記事一覧を取得 記事の新規投稿または更新 事例紹介 文章校正 textlint-disableの利用 校正ルールの運用 記事のプレビュー 導入成果 レビューパフォーマンス CI/CDフロー整備の影響 今後の展望 まとめ さいごに ZOZO TECH BLOGでのCI/CDの活用 CI/CDは、Continuous Integration(継続的インテグレーション)およびContinuous Delivery(継続的デリバリー)の略です。ソフトウェア開発プロセスの自動化による品質向上を目的とした手法です。CIでは、開発者がソースコードをリポジトリへプッシュするたびにビルドプロセスを実行しコードを統合します。そしてCDでは、CIの成果物を任意の環境へ自動的にデプロイします。これにより、開発者はより迅速かつ信頼性の高いソフトウェアを提供できるようになります。 ZOZO TECH BLOGを支える技術 #1 これまでとこれから で記事公開までのおおまかなプロセスを紹介しています。このうち、記事の執筆に関わるプロセスは次のとおりです。 記事の執筆とテストページへのデプロイ 記事のレビュー 指摘箇所の修正と再レビュー この3つのプロセスを記事が完成するまで繰り返します。これらのプロセスはソフトウェア開発と変わらないと言っても違和感はないでしょう。ZOZO TECH BLOGの記事は全てソースコードと同様にGitHubリポジトリで管理しており、記事のレビューもPull Requestを介して行います。執筆のプロセスにもCI/CDの手法を適用することで執筆者の執筆をサポートし、ZOZO TECH BLOGとしてより良い記事を公開することに繋がります。 記事の静的解析と文章校正 投稿する記事には正確で分かりやすい文章であることが求められます。文章の質の担保はレビューによって行いますが、人によるレビューの実施前に自動化された文章校正を実施する事でレビューの効率化を図れます。 CIによって反復的に実行されるプロセスには一般的に次のようなものがあります 1 。 ソースコードのコンパイル 分析(静的解析、動的解析等) テスト ZOZO TECH BLOGでは、これらのプロセスのうち分析にあたる静的解析を記事に対して行い文章校正をします。 具体的には、文章がリポジトリにプッシュされることをトリガーに textlint を実行し、その結果をPull Requestにコメントとして書き込みます。これにより、記事の執筆者は文章校正の結果を確認できます。 記事のプレビュー環境へのデプロイ 記事が読者にどう表示されるかを確認することは読みやすい記事を執筆する上で重要です。意図しない改行や文字化けが発生していないか、記事中のコードブロックが正しく表示されているかや画像のサイズが適切かなど実際に記事を表示して確認することが望ましいです。 ZOZO TECH BLOGでは、記事の執筆はMarkdownで行っており、それをはてなブログに投稿することで公開しています。このため、実際に記事がどのように表示されるかを確認できるプレビュー環境を非公開のはてなブログとして整備しています。 文章がリポジトリにプッシュされることをトリガーに、はてなブログのAPIを利用して執筆中の記事がプレビュー環境にデプロイされます。このプレビュー環境は記事の執筆者が記事のプレビューを確認するためだけに利用します。また、2023年8月時点では本番環境への公開はDevRelチームが手動で行っています。 CI/CDフローの構築 CI/CDフローを整備することで実現したことは次の2点です。 執筆中の記事の文章校正 執筆中の記事のプレビュー環境へのデプロイ これらを実現するCI/CDフローがどのように構成されているかを紹介します。 CI/CDフローの概要 文章校正とプレビュー環境へのデプロイを行うCI/CDフローの概要を図に示します 2 。CIサービスとしては GitHub Actions を用いています 3 。文章校正とプレビュー環境へのデプロイ共にPull Requestの作成またはコミットのプッシュをトリガーにワークフローを実行します。 CI/CDフローの概要 文章校正 文章校正は次のようなプロセスで実現します。 差分の検出 textlintの実行 実行結果のフィードバック これらのプロセスをPull Requestの作成またはコミットのプッシュをトリガーに実行します。textlintによる文章校正の結果はPull Requestにコメントとして書き込まれ執筆者にフィードバックされます。この一連の流れを実行するワークフロー定義は次のとおりです。 name : "textlint & reviewdog" on : pull_request : paths-ignore : - '**/README.md' env : REVIEWDOG_GITHUB_API_TOKEN : ${{ secrets.GITHUB_TOKEN }} jobs : linter : runs-on : ubuntu-latest steps : - name : "Checkout" uses : actions/checkout@v3 with : fetch-depth : 0 - name : "Setup nodejs" uses : actions/setup-node@v3 with : node-version : 16 cache : 'yarn' - name : "Setup reviewdog" uses : reviewdog/action-setup@v1 with : reviewdog_version : latest - name : "Install node dependencies" run : yarn install - name : textlint and reviewdog if : ${{ (github.event_name == 'pull_request' ) }} run : | DIFF_FILES=`git diff --name-status origin/master --diff-filter=MA | grep -E ".*.md" | cut -f2` if [ -z "${DIFF_FILES}" ] ; then exit 0; fi $(yarn bin)/textlint --ignore-path config/.textlintignore -c config/.textlintrc -f checkstyle $(echo ${DIFF_FILES}) | reviewdog -f=checkstyle -name="textlint" -reporter=github-pr-review --fail-on-error= true -filter-mode=added textlint and reviewdog stepで文章校正とPull Requestへのフィードバックをします。具体的な処理内容は次のとおりです。 差分の検出 textlintの実行 reviewdogでのPull Requestへのコメント追加 textlintの実行時には次のオプションを指定しています 4 。 オプション名 指定内容 オプションの概要 --ignore-path config/.textlintignore textlintの対象から除外するファイルを.textlintignoreに記載し指定 -c config/.textlintrc textlintの設定を.textlintrcに記載し指定 -f checkstyle 出力形式にcheckstyleを指定 また、textlintの実行結果をPull Requestにコメントとしてフィードバックするために reviewdog を使用しています。reviewdogはtextlintの実行結果を受け取り、その結果をPull Requestにコメントとして書き込みます。reviewdogの実行時には次のオプションを指定しています 5 。 オプション名 指定内容 オプションの概要 -f checkstyle 入力形式にcheckstyleを指定 -reporter github-pr-review 結果の出力にPull Requestへのコメントを指定 -fail-on-error true 1つでもエラーが発生した場合にジョブを失敗させるよう指定 -filter-mode file 追加されたファイル単位で結果をフィルタリングするよう指定 プレビュー環境へのデプロイ 記事のプレビュー環境へのデプロイは次のようなプロセスで実現します。 フォーマット・画像のアップロード プレビューへの記事の反映 これらのプロセスを文章校正と同様にPull Requestの作成またはコミットのプッシュをトリガーに実行します。 フォーマット・画像のアップロード プレビュー環境へのデプロイにおいて画像の扱いを考慮する必要があります。ZOZO TECH BLOGの標準的な記事のディレクトリ構成は次のとおりです。 articles └── sample_article ├── entry.md └── images └── sample.png entry.mdから画像を参照する場合は次のように記述します。 ![ sample ]( ./images/sample.png ) ここで、entry.mdをプレビュー環境へデプロイする際、 ./images/sample.png をはてなフォトライフへアップロードします。そして、 ./images/ のパスを https://cdn-ak.f.st-hatena.com/images/fotolife/ に変換しentry.mdの該当箇所を書き換えます。この一連の処理をフォーマットと呼んでいます。フォーマットが完了するとentry.mdは次のようになります。 ![ sample ]( https://cdn-ak.f.st-hatena.com/images/fotolife/<USERNAME>/<UPLOADED_IMAGE_PATH> ) GitHub Actionsでこのフォーマット処理を行うワークフロー定義は次のとおりです。entry.mdの差分を検出し、差分のあるファイルに対してフォーマットを行います。 name : "format & post to hatenablog" on : pull_request : paths : - '**/entry.md' env : HATENA_ACCESS_TOKEN : ${{ secrets.HATENA_ACCESS_TOKEN }} HATENA_ACCESS_TOKEN_SECRET : ${{ secrets.HATENA_ACCESS_TOKEN_SECRET }} HATENA_CONSUMER_KEY : ${{ secrets.HATENA_CONSUMER_KEY }} HATENA_CONSUMER_SECRET : ${{ secrets.HATENA_CONSUMER_SECRET }} jobs : format : runs-on : ubuntu-latest steps : - name : "Checkout" uses : actions/checkout@v3 with : fetch-depth : 0 - name : "Upload Image" run : | ARTICLE_PATH=`pwd`/`git diff --name-status origin/master --diff-filter=MA | grep -E "articles/.*.md" | cut -f2` cd scripts bundle bundle exec ruby format_article.rb $ARTICLE_PATH ../entry.md - name : "mv formatted article" run : mkdir /tmp/workspace && cp entry.md /tmp/workspace/formatted_entry.md - name : "Upload formatted_entry.md for job: post_to_hatenablog" uses : actions/upload-artifact@v3 with : name : formatted_entry path : /tmp/workspace/formatted_entry.md このワークフロー定義では、 Upload Image stepで format_article.rb を実行することで画像のアップロードと記事のフォーマットを行います。ここではてなフォトライフへのアップロードには rlho/hatena_fotolife を使用しています。 次に mv formatted article stepでフォーマット済みの記事を /tmp/workspace/formatted_entry.md に移動します。最後に Upload formatted_entry.md for job: post_to_hatenablog stepでフォーマット済みの記事を成果物としてアップロードします。フォーマット済みの記事は後続の post_to_hatenablog jobでプレビュー環境へデプロイします。 プレビューへの記事の反映 プレビュー環境は非公開のはてなブログです。成果物としてアップロードされたフォーマット済みの記事をプレビュー環境へ反映するには、はてなブログへの投稿が必要になります。 フォーマット済みの記事を受け取りはてなブログへの投稿するワークフロー定義は次のとおりです。 env : BLOG_USERNAME : ${{ secrets.BLOG_USERNAME }} BLOG_DOMAIN : ${{ secrets.BLOG_DOMAIN }} BLOG_API_KEY : ${{ secrets.BLOG_API_KEY }} jobs : format : ... post_to_hatenablog : needs : format runs-on : ubuntu-latest steps : - name : "Checkout" uses : actions/checkout@v3 with : fetch-depth : 0 - name : "Setup hatenablog CLI" uses : x-motemen/blogsync@v0 - name : "Download formatted_entry.md from job: format" uses : actions/download-artifact@v3 with : name : formatted_entry path : /tmp/workspace - name : "cp formatted_entry.md" run : cp /tmp/workspace/formatted_entry.md . - name : "Add blogsync config" run : | echo -e "${BLOG_DOMAIN} \" : \"\n username \" : \" ${BLOG_USERNAME} \n password \" : \" ${BLOG_API_KEY} \n default \" : \"\n local_root \" : \" ./ \n omit_domain: true" | tr -d \" >> blogsync.yaml - name : "Create/Update article" run : | blogsync pull $BLOG_DOMAIN BRANCH=${{ github.head_ref }} ARTICLE_PATH=`git diff --name-status origin/master --diff-filter=MA | grep -E "articles/.*.md" | cut -f2` # 整形したものに置き換え mv -f formatted_entry.md $ARTICLE_PATH if grep '^URL:' $ARTICLE_PATH; then # ブログが投稿されている場合は投稿されている記事を更新 blogsync push $ARTICLE_PATH else # ブログが一度も投稿されてない場合は投稿 blogsync post --title=$BRANCH --custom-path=$BRANCH $BLOG_DOMAIN < $ARTICLE_PATH # 記事メタデータの付与 BLOGSYNC_PATH="entry/${BRANCH}.md" cp -r $ARTICLE_PATH $ARTICLE_PATH.old head -8 $BLOGSYNC_PATH | cat - $ARTICLE_PATH.old > $ARTICLE_PATH fi # 画像のアップロード・メタデータが付与されていればコミットしてpush if git status -s | grep articles; then git config --global user.email ${BOT_EMAIL} git config --global user.name 'TechBlog Bot' git checkout $BRANCH git add $ARTICLE_PATH git commit -m "Add hatena blog meta data" git push ${TECH_BLOG_REPO_URL} $BRANCH fi - name : "Print blog URL" run : | ARTICLE_PATH=`git diff --name-status origin/master --diff-filter=MA | grep -E ".*.md" | cut -f2` echo `grep -e "^URL:" $ARTICLE_PATH | awk '{print $2}' ` Download formatted_entry.md from job: format stepでフォーマット済みの記事を成果物としてダウンロードします。はてなブログへの投稿には x-motemen/blogsync を使用し、 Add blogsync config stepでblogsyncの設定ファイルを作成します。そして、 Create/Update article stepでは次のことを行います。 公開済みの記事一覧を取得 記事の新規投稿または更新 公開済みの記事一覧を取得 blogsync pull コマンドで公開済みの記事一覧を取得します 6 。 blogsync pull $BLOG_DOMAIN 記事の新規投稿または更新 記事がプレビュー環境に投稿されている場合は更新し、投稿されていない場合は新規投稿します。 blogsync push コマンドで記事を更新します 7 。titleとcustom-pathはブランチ名、投稿内容は標準入力で指定します。 blogsync post --title = $BRANCH --custom-path = $BRANCH $BLOG_DOMAIN < $ARTICLE_PATH 投稿後、記事は自動的にダウンロードされ entry/${BRANCH}.md に保存されます。この時に、記事の先頭には記事のURL等のメタデータが付与されています 8 。例えば、本記事の場合次のようになります。 Title: ZOZO TECH BLOGを支える技術 #2 執筆をサポートするCI/CD Date: 2023-08-08T09:55:22+09:00 URL: https:// < BLOG _DOMAIN> /entry/techblog-writing-support-by-ci-cd EditURL: https://blog.hatena.ne.jp/ < BLOG _USERNAME> / < BLOG _DOMAIN> /atom/entry/... CustomPath: techblog-writing-support-by-ci-cd このメタデータのURLが記事のプレビューURLとなりますが、メタデータはZOZO TECH BLOGのリポジトリでは管理されていません。リポジトリ管理下にあるのは記事の本文のみです。そのため、記事の本文にメタデータを付与しZOZO TECH BLOGのリポジトリにコミットします。 記事の本文へのメタデータの付与は次のように行います。 BLOGSYNC_PATH = " entry/ ${BRANCH} .md " cp -r $ARTICLE_PATH $ARTICLE_PATH .old head -8 $BLOGSYNC_PATH | cat - $ARTICLE_PATH .old > $ARTICLE_PATH そして、ZOZO TECH BLOGのリポジトリにプッシュします。 git config --global user.email ${BOT_EMAIL} git config --global user.name ' TechBlog Bot ' git checkout $BRANCH git add $ARTICLE_PATH git commit -m " Add hatena blog meta data " git push ${TECH_BLOG_REPO_URL} $BRANCH 事例紹介 今回紹介したCI/CDフローが実際にどう活用されているか、ZOZO TECH BLOGでの事例を紹介します。 文章校正 「GitHub Copilotの全社導入とその効果」の執筆中に指摘があった例を紹介します。 techblog.zozo.com 丸かっこの指摘 冗長な表現の指摘 文字数の指摘 textlint-disableの利用 文章校正の結果を無視したい場合もあります。例えば、次の例ではアンケートの選択肢の部分でtextlintの指摘が出てしまいます。 アンケートの選択肢についてのtetlintの指摘 この場合、次のように textlint-disable を利用することで、指摘を無視できます 9 。 <!-- textlint-disable --> 文章校正を無効にしたい文章 <!-- textlint-enable --> 校正ルールの運用 技術文書向けのtextlintルールプリセットである textlint-rule-preset-ja-technical-writing をはじめとする複数のルールを導入しています。このプリセットには、文章中の漢字の連続文字数を制限する textlint-jatextlint-rule-max-kanji-continuous-len が含まれています。このルールは、次のように漢字が連続していることを指摘します。 漢字の連続文字数の指摘 しかし、 平均削減金額 のように指摘を無視したい場合があります。 textlint-disable を利用しても良いですが、恒久的な対応として例外に登録可能です。 漢字の連続文字数の例外登録 記事のプレビュー プレビュー環境は記事に付与されたメタデータからアクセスできます。例えば、本記事のプレビューは次のようになります。 プレビュー表示 導入成果 CI/CDフローの導入によって、執筆者とレビュアの双方で記事の体裁の指摘が減少しています。また、自動的にプレビュー環境が生成されるため表示確認も容易になっています。DevRelチームとしても記事の体裁の指摘が減少しているため、記事の内容にフォーカスしてレビューできています。 レビューパフォーマンス レビューのパフォーマンスを定量的に評価することは難しいですが、レビュー開始からアプルーブまでの時間を指標として見てみます。 Findy Team+ の「レビュー分析」でZOZO TECH BLOGリポジトリのレビュー開始からアプルーブまでの時間を表示しました。集計期間は過去1年間です。ZOZO TECH BLOGの場合、レビュー開始からアプルーブまで平均で225.5時間かかっています。 過去1年間のレビューからアプルーブまでの平均時間 2023年2月からリードタイムが大きく改善しています。DevRelチームの発足によるレビュー体制強化が大きな要因として考えられます。 次は、DevRelチームが発足した2023年2月1日から2023年8月14日までで集計した結果です。この期間の平均リードタイムは、レビュー開始からアプルーブまで平均で145時間です。 2023年2月以降のレビューからアプルーブまでの平均時間 ここで、ZOZO TECH BLOGの月別の記事公開数は次のようになります。 公開月 公開本数 2023年2月 7 2023年3月 16 2023年4月 4 2023年5月 12 2023年6月 15 2023年7月 8 2023年8月 2 3月、5月、6月は公開記事数の増加によりレビューが重なりました。しかし、DeRelチームですべて捌ききることができ、レビュー開始からアプルーブまでの時間の増加もチームとしての許容範囲内に収められています。レビューの負荷が高い状況でも大きくパフォーマンスは悪化していません。 CI/CDフロー整備の影響 CI/CDフローの整備は1年以上前に行われていますので、レビューパフォーマンス改善の主要因ではありません。しかし、CI/CDフローの整備によってレビュアがレビューする際の効率化が図られているため、レビュー体制を強化した効果がより発揮されていると考えることはできそうです。 CI/CDフローの整備を進めていく上で「レビュー開始からアプルーブまでの時間」のような具体的な指標を追うことは、整備の方向性を模索する上で重要な検討材料となります。また、記事のアイデアを出す段階から公開されるまでの流れを見直すことで、記事の公開までの時間をより短縮できるかもしれません。 今後の展望 整備したCI/CDフローのサポートによって記事の執筆およびレビューの効率化が実現されています。しかし、まだまだ改善の余地があります。例えば、アップロードする画像サイズの最適化は著者にその実施を委ねていますが、CI/CDフローの中で実施できた方が執筆者の負担は減るでしょう。また、AIの活用についても検討を重ねていきたいです。文章校正やレビューについてAIを活用したり、記事のテーマや構成案の検討にも活用できそうです。 まとめ 本記事では、ZOZO TECH BLOGの執筆をサポートするCI/CDフローについて紹介しました。ZOZOではDevRelチームを中心に執筆をサポートする体制を整えています。今後も引き続き執筆環境を改善することで執筆者が少しでも書きやすい環境になり、それにより記事の執筆者が増え、社内の取り組みをより多く社外に発信できればと思います。 さいごに ZOZOでは一緒に楽しく働くエンジニアを絶賛募集中です。ご興味のある方は下記リンクからぜひご応募ください。 corp.zozo.com ビルドという言葉はコンパイルと同じ意味で使われる場合もありますが、CIの文脈ではコンパイルだけでなく、テストや分析まで含まれることがあります。 ↩ GitHub Actionsのアイコンは 公式サイト から引用しています。 ↩ 当初はCIサービスとしてCircleCIを利用していましたがGitHub Actionsに移行しました。 ↩ textlintのオプションは textlintリポジトリのREADME.md から確認できます。 ↩ reviewdogのオプションは reviewdogリポジトリのREADME.md から確認できます。 ↩ https://github.com/x-motemen/blogsync#%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8Bblogsync-pull ↩ https://github.com/x-motemen/blogsync#%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8Bblogsync-push ↩ https://github.com/x-motemen/blogsync#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88 ↩ textlint-enable で再度有効にする必要があります。 ↩
はじめに こんにちは。検索基盤部の岩崎です。検索基盤部ではZOZOTOWNの検索機能の改善に日々取り組んでいます。ZOZOTOWNのおすすめ順検索のプロジェクトでは、機械学習モデルを活用した検索結果の並び順の改善に取り組んでおり、全ての施策はA/Bテストで検証しています。なお、最近の並び順精度改善の取り組みについては以下の記事をご参照ください。 techblog.zozo.com 本記事におけるA/Bテストとは、特定期間中ランダムに振り分けたユーザーに対してそれぞれ別の施策を提示し、その成果の差を検定するテストのことを指します。A/Bテストは施策の効果を検証するための優れた手段として広く知られており、おすすめ順検索改善のリリース判断には欠かせない存在となっています。ZOZOではA/Bテスト基盤の整備を進めており、おすすめ順検索以外にもさまざまな施策でA/Bテスト基盤を用いた運用がされています。 しかし、おすすめ順検索のプロジェクトではA/Bテストを重ねていくうちにデータ分析の工程が徐々に複雑化してしまい、その手動作業が分析者の負担となっていました。ダッシュボードに表示する指標もどんどん増え、修正作業の難しい状態となっていました。本記事ではA/Bテストのデータ分析工程に関して、従来の課題とその改善策について焦点をあて、取り組みを紹介していきます。A/Bテストのデータ分析工程に関して類似した課題を抱えている方の一助となれば幸いです。 目次 はじめに 目次 従来の運用 改善1. 集計のワークフローをVertex AI Pipelinesで自動化する 改善2. 指標の再整理 まとめ おわりに 従来の運用 A/Bテスト導入当初はスプレッドシートの カスタムクエリ の機能を用いて集計処理が行われていました。以下は実際に当初運用していたクエリの編集画面になります。なお具体的なクエリの内容については加工処理をしています。 この集計結果をもとに、A/Bテスト期間中の各施策の指標を比較できるダッシュボードをスプレッドシート内に作成していました。A/Bテスト期間中は定期的にカスタムクエリを実行し、指標の速報値をプロジェクトメンバー内で共有し、もし指標値の急激な悪化が見られた場合は早期に切り戻しする、といった運用をしていました。A/Bテスト期間後に発生する後続の分析調査についても同じスプレッドシート内に追加していくことで運用されていました。 そして、A/Bテストが行われるたびに上記の集計クエリやダッシュボードが格納されたスプレッドシートを複製し、クエリ編集画面にあるパラメータやクエリ内容を次のA/Bテストの設定に修正していました。以上が従来の運用方法になります。 この運用は、比較的軽量な作業で運用可能な状態を作り上げられるというメリットがあります。一方で、コードの管理体制が十分でないことやスプレッドシート自体の制約により、運用を継続していく上で多くの課題が生じました。以降にその課題と改善の取り組みを説明していきます。 改善1. 集計のワークフローをVertex AI Pipelinesで自動化する 従来の運用では、集計クエリのコードがスプレッドシート内に存在しており、その内容を直接バージョン管理できていませんでした。レビュー体制も構築できておらず、クエリ修正作業の属人化が進行していました。また、集計クエリがダッシュボードに依存しているという課題もありました。これによりA/Bテストが行われるたびに集計クエリが複製されるため、さらに差分を追いづらい状態になっていました。ダッシュボードを他のツールに移行することも難しく、ツール再選定の余地がない状態になっていました。 加えて、カスタムクエリのタイムアウトは通常のクエリのタイムアウトよりも短く設定されていることが一般的です。このため、カスタムクエリの集計期間によってはタイムアウトになることもありました。これを防ぐために、通常のクエリ実行で集計した結果を一時的なテーブルとして保存しておき、カスタムクエリでその結果を取得する、といった暫定対応なども必要になりました。こういった手動作業の複雑化についても課題となっていました。 これらの課題により、A/Bテストのたびに行う分析者の手動作業が多く、分析者のリソースが逼迫していました。人的ミスの入り込む可能性も高く、クエリ修正作業の属人化も進行していました。これらの課題の解決のために、集計クエリの実行の自動化に取り組みました。 集計クエリの自動化には、Google Cloud Platform(GCP)のサービスである Vertex AI Pipelines を利用しました。選定理由は以下の3点です。 1点目の選定理由は、条件分岐やループをもつワークフローを簡単に記述できる点です。これにより、「A/Bテスト期間中のみ速報値を日次で出力したい」といった要件や「複数のA/Bテストが同時期に行われることも想定したい」といった要件に対しても柔軟に対応できます。 2点目の選定理由は、Vertex AI Pipelinesがおすすめ順検索の開発者にとって馴染みのあるサービスであるという点です。Vertex AI Pipelinesは一般的には機械学習のワークフローを管理するサービスであり、おすすめ順検索の機械学習モデルの開発時にも活用しています。そのため、習得すべき技術項目を増やすことなく分析工程の改善にも取り組むことができます。なお、おすすめ順検索の機械学習モデルの開発からデプロイまでのワークフローの自動化については以下の記事をご覧ください。 techblog.zozo.com 3点目の選定理由は、社内にVertex AI PipelinesのCI/CD、監視、スクリプト類のまとまったテンプレートリポジトリがある点です。このリポジトリは Cloud Scheduler と Cloud Functions によるVertex AI Pipelinesの定期実行も簡単に実装できるようになっています。こちらを活用することにより、今回目指している集計クエリの自動化がより容易に実現できます。以下の記事でVertex AI Pipelinesの導入事例やテンプレートリポジトリを紹介しているので併せてご参照ください。 techblog.zozo.com 以上の理由から、Vertex AI Pipelinesを採用して、集計クエリの自動化を行いました。 実際に運用している集計のワークフローを以下に示します。この図はVertex AI Pipelinesのコンソール画面から確認できるワークフロー全体像となっています。 簡単にこのワークフローを説明すると、以下のとおりです。このワークフローが日次で実行されることにより、集計の自動化が実現できています。 集計の基準日を取得する(デフォルトではワークフロー実行日となる) 前段で取得した基準日に行われているA/Bテストがいくつなのか、A/Bテスト名などの情報とあわせて取得する 前段で取得したA/Bテストそれぞれについて、指標算出のための集計クエリを実行し、結果をBigQueryに出力する なお、移行にあたっては、特定のA/Bテストについて従来の運用と新規の運用を並走させて同一の結果を出力することを確認する、という方針で安全に行いました。 この取り組みにより、集計クエリとダッシュボードとの依存関係を軽減でき、ダッシュボードのツールを再選定しやすい状態になりました。現状のダッシュボードは習熟コストの低さを優先してスプレッドシートを引き続き採用していますが、今後のツール選定の余地を残したまま運用できています。 また、GitHub上で管理されたSQLファイルを直接実行する仕組みとなったため、取得する指標の変更に応じてレビューが適切に行われるようになりました。加えて、SQLのLinterである SQLFluff をGitHub Actionsに導入することでコードの品質も保つことができるようになりました。 改善2. 指標の再整理 従来の運用では、これまでのA/Bテストで作成された指標がそのまま次のA/Bテストのダッシュボードに引き継がれていく仕組みとなっていました。A/Bテスト期間後の事後分析用のクエリも引き継いでいるため、徐々にダッシュボードが複雑になっていき、「集計しているが数値の確認はしていない」といった状態の指標が発生していました。またレビュー体制が整っていないこともあり、「A/Bテストの判断に必要な指標」と「事後分析などの追加調査のための指標」という別の目的をもった集計が単一クエリに混在している状態でした。以上のことから、クエリやダッシュボードも必要以上の複雑さになっており、修正作業の属人化が進行していました。 この状態の解決のために、現在算出している指標とその算出に必要なカラムを全て洗い出し、指標を分類する取り組みを行いました。関係者と議論して「A/Bテストの判断に必要な指標」とそうでない指標を分類し、前者のみを自動化の対象とし、その算出に必要なカラムのみを抽出するように集計クエリを作り直しました。 これらの対処の結果、追加調査は別クエリとして切り出されるようになり、集計クエリのコードの肥大化防止につながりました。追加調査もより気軽に、他の集計を気にせず行える仕組みとなりました。指標数やクエリのコード量は従来の1/5程度に抑えられ、クエリ修正作業の属人化の解消にもつながりました。ダッシュボードもシンプルになり、KGIやKPI、A/Bテストが正しく行われているかの指標を信頼区間つきで端的に示されることで、リリース可否の判断もしやすくなりました。 以下は改善後のダッシュボード例になります。数値はダミーの値に加工しています。 この図から、新規施策が多くの指標で上回っており、そのうち2つの指標についてその差が統計的に有意であることが読み取れます。 まとめ 今回の取り組みにより手動作業の多くは自動化され、分析者はA/Bテスト期間中でも他の分析へ注力できるようになりました。クエリの簡単化とレビュー体制の構築により、属人化の問題を解消できました。さらに、これらの仕組み改善のおかげで、Sample Ratio Mismatchの検知や信頼区間の導入など、リリース判断にとって重要な取り組みを実施できるようになりました。 今回の取り組みによって分析工程の自動化をある程度進めることができた一方、A/Bテストの実施工程の全体を見つめ直すとまだまだ自動化できる要素がたくさん残っています。現状はリリースフロー上関係部署と多くのやり取りをする必要があるため、協調して仕組みを自動化できればと考えております。日々の運用を改善していき、大量のA/Bテストを負担なく実行できるような体制につなげられればと思います。 おわりに ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、エンジニア組織の技術広報を担うDevRelブロックの @ikkou です。ARやVRをはじめとするXRといった技術領域を担う創造開発ブロックと、技術戦略の策定やエンジニア組織の強化を担うCTOブロックも兼任しています。 これから3つの記事にかけて、皆さんが見ているこの「ZOZO TECH BLOGを支える技術」を紹介していきます。1本目となる本記事では、これまでの取り組みと今後の展望を紹介します。 目次 はじめに 目次 はじめに DevRelブロックについて ZOZO TECH BLOGについて ZOZO TECH BLOGを支える技術 これまで これまでの仕組みと課題、その解決 スケジューリング 概要のレビュー デザイナーチームに画像の作成依頼 記事の執筆とテストページへのデプロイ 記事のレビュー 予約投稿と公開 X(Twitter)にシェア 今後の展望 AI導入による効率化 反響の可視化 まとめ はじめに まずはじめに、DevRelブロックとZOZO TECH BLOGについて紹介します。 DevRelブロックについて DevRelブロックは、ZOZOにおけるエンジニア組織の「技術広報」を担うチームです。この「ZOZO TECH BLOG」の運用・執筆支援をはじめとして、技術カンファレンスや勉強会での登壇支援や開催など、エンジニア組織の技術発信とブランディングを支援しています。 これまではCTOブロックがその役割を担ってきましたが、今年の春から専門チームとして新設されました。私のように他ブロックを兼任しているメンバーと、DevRelブロック専任のメンバーで構成されています。 技術広報を担う組織の場合、人事やコーポレート広報の経験者が担当または兼任しているケースもありますが、ZOZOの場合は全員エンジニアです。それぞれ素地となる専門領域は異なりますが、エンジニアとしての視点を持って取り組んでいます。 ZOZO TECH BLOGについて まさに今皆さんが読んでいる、このZOZO TECH BLOGは、今年で13年目となり、現在までに様々な技術領域の記事やイベントレポートなどが約650本投稿されています。歴史を紐解くと、まず 2011年5月9日に「株式会社VASILY」の「VASILY Tech Blog」として始まりました 。2018年4月に複数社で「株式会社スタートトゥデイテクノロジーズ」として合併、 名称が「スタートトゥデイテクノロジーズ テックブログ」に変わりました 。そして同年10月に商号が「株式会社ZOZOテクノロジーズ」に変わり、あわせて名称も「ZOZO Technologies TECH BLOG」に変わりました。その後、 2021年秋の吸収分割で「株式会社ZOZO」と「株式会社ZOZO NEXT」に分かれ、現在の「ZOZO TECH BLOG」になりました 。 このように何度かの組織改編や名称変更を経てきましたが、テックブログに対する思いや熱量は変わることなく、現在はZOZO所属のエンジニアとZOZO NEXT所属のエンジニアが記事を執筆しています。 ZOZO TECH BLOGを支える技術 早速、本記事のテーマとなる「これまで」と「これから」を紹介していきます。 これまで 前述の通り、これまではCTOブロックが技術広報の役割を担ってきました。そのため、このZOZO TECH BLOGの運用も同様にCTOブロックが担っていました。テックブログにかける思いや、その運用方法については、前任の担当者が2020年末に記事として公開しています。今でも十二分に通用するテックブログの運営・運用ノウハウが詰まっているので、ぜひご一読ください。 techblog.zozo.com これまでの仕組みと課題、その解決 一部、先の記事にも記載されていますが、記事公開までおおまかには次のプロセスを経ていました。 各部署から執筆予定をヒアリング スケジューリング 概要のレビュー デザイナーチームに画像の作成依頼 記事の執筆とテストページへのデプロイ 記事のレビュー 指摘箇所の修正 予約投稿と公開 X(Twitter)にシェア 長年の運用によって培われてきたこのプロセスですが、一部の運用に改善の余地がありました。引き継いだタイミングと、DevRelブロックが組成されたタイミングで、それらの課題を解決するためのアプローチを考えました。 スケジューリング 当初はConfluenceで運用していましたが、見やすさを重視するためGoogle スプレッドシートに移行しました。また、それぞれのステータスを追いやすい形式に変更しました。 テックブログ予定表の一例 行を固定している#0は凡例です。スケジューリングの前工程として、各部署から上期・下期ごとの執筆予定の記事数をヒアリングするとともに、この予定表に記載しています。執筆者には、執筆予定の記事について、概要を記載してもらい、後工程に続きます。それぞれの工程では、依頼前・依頼済み・確認済みなどのステータスを変更することで、公開までの進捗を把握できるようにしています。 概要のレビュー まず、執筆者の上長による概要のレビューを経て、DevRelブロックでも同様にレビューしています。このレビューは、検閲のようなものではなく、記事の内容を把握するためのものです。また、執筆者が記事を書く際に、概要を書くことで記事の方向性を明確にできます。 デザイナーチームに画像の作成依頼 ZOZO TECH BLOGの記事冒頭にあるOGP画像は、すべてデザイナーが作成しています。Slackに「TECH BLOG 画像作成依頼ワークフロー」を用意し、公開日・タイトル・概要・確認ページのURL・使用可能な画像アセットを記した上で作成を依頼します。デザイナーは、このワークフローを参照して画像を作成しています。この画像は、記事の内容を表現するだけでなく、SNSでシェアした際にも目を引くように工夫しています。 画像の作成依頼は公開日の14営業日前までに依頼するようにしています。デザイナーのマンパワーにも限りがあるため、記事は公開できる状態になっているが、画像が作成できていないという状況が時々発生してしまいます。対策として、昨今流行りの画像生成AIの利用や、記事タイトルのテキストを画像化するなども検討していますが、現時点ではクオリティの観点から見送っています。今後の課題のひとつです。 記事の執筆とテストページへのデプロイ 概要のレビューが済んだ後、各エンジニアは専用のリポジトリに記事を作成します。記事の執筆には、以前から変わらずMarkdownを採用しています。textlintを導入しているため、執筆中に必要最低限のチェックを行なえます。そしてPushすることで非公開のテストページに自動的にデプロイされ、表示を確認できます。その際にtextlintも実行されるので、手元でのチェックから漏れたエラーを確認の上、修正できます。 このCI/CDはもともとCircleCIによって実現していましたが、現在はGitHub Actionsに移行しています。この移行の詳細は後続の記事で紹介します。 記事のレビュー 一定まで執筆した、あるいはすべて書き終えた記事は、まずチーム内でレビューされます。チーム内レビューでは、記事内の技術的な内容の正しさを担保しています。ZOZOには一部の技術領域においてテックリードという制度があるので、ときにはチームの垣根を越えてテックリードがレビューすることもあります。 チーム内・テックリードの後、DevRelブロックでもレビューします。画像の作成依頼と同じように、Slackに用意してある「TECH BLOG レビュー依頼ワークフロー」からレビューを依頼してもらいます。DevRelブロックのレビューでは、誤字脱字や表記揺れ、表記誤りなどを修正するとともに、記事の方向性が概要と一致しているかを確認します。DevRelブロックが組成されるまで、このレビューパートは1人で行なっていたため、レビューのタイミングが遅くなるといった課題がありました。何より属人的になることで、可用性が低い運用となっていました。そういった背景もあり、現在のDevRelブロックが組成されたことで、マンパワーの増強によりできることが増え、その結果、これまで培ってきたことの改善と今後に向けた施策も考えられるようになりました。 マンパワーが増強されたとはいえ、期末など記事の公開が集中しがちなタイミングでは、レビューが追いつかないこともあります。画像の作成同様にAIを活用することも検討していますが、現時点ではまだ実現していません。これもまた今後の課題のひとつです。 予約投稿と公開 画像が用意され、各レビューが完了した記事は、執筆者自身の手で本番環境に下書き保存しています。下書き保存の完了後、スケジュールを確認しながらDevRelブロック公開日時を決めて予約投稿を設定しています。公開日の大原則は以前から変わらず、「1日に複数の記事は公開しない」と「午前11時に公開する」としています。 X(Twitter)にシェア 記事公開に先立ち、執筆者には記事の概要文を用意してもらっています。この概要文を記事公開にあわせてX(Twitter)のZOZO Developersアカウント( @zozotech )でシェアしています。これまでの経緯からこのシェアは人事組織が担っていましたが、現在はDevRelブロックが担当しています。 今後の展望 一連の流れは紹介した通りです。画像作成やレビューといったマンパワーに起因する課題はありますが、DevRelブロック組成以前に比べて可用性が高まっていると感じています。これからは、この可用性を維持しつつ、さらに改善していくことを目指しています。その一部を紹介します。 AI導入による効率化 前述の通り、画像作成とレビューにおけるAIの導入は今後の課題です。前者の画像作成については、例えばこれまでのOGP画像を学習データとして与えることで、ZOZO TECH BLOGらしい画像を生成できるかもしれませんが、まだ公開できるフェーズではありません。後者のレビューも同様ですが、textlintを活用することで、誤字脱字や表記揺れ、表記誤りなどの多くは自動的に修正できるようになっています。それ以上の言い回しや表現方法をAIが判断できるのか、今はまだ引き続き検証が必要な段階です。 別の観点では、記事執筆にGitHub Copilotが使える可能性も感じています。ちなみに、ZOZOは先日GitHub Copilotを全社導入しました。詳細はぜひ以下の記事をご覧ください。 techblog.zozo.com GitHub Copilotは多くの場合、コードの補完に利用されていますが、テックブログのような文章を書く場面でも活用できます。実際にこの記事も一部でGitHub Copilotの恩恵を受けています。記事すべてをGitHub Copilot然りAIに完全に任せるのはまだ早いと考えていますが、補完のような形であれ、AIを活用することで執筆者の負担を軽減できるとも考えています。 反響の可視化 改善のひとつとして執筆者に記事公開後の反響を伝えようと考えています。実はこれまで、記事公開後に記事のPV数やシェア数を執筆者に伝えることはしていませんでした。現在は「DevRelブロック通信」という社内報で前月に公開された記事と、ブックマーク数が目立った記事を紹介していますが、記事公開後のフォローが不十分だと感じています。そこで、PV数・シェア数・はてなブックマーク数といった情報の他、記事に対するコメントや質問を共有することで、執筆者が記事を書くモチベーションにつながると考えています。 この記事公開後の反響の可視化については、もともと導入していたGA4とLooker Studioを組み合わせることで実現できます。このLooker Studioに関する詳細も後続の記事で紹介します。 まとめ 本記事ではDevRelブロックと、ZOZO TECH BLOGのこれまでとこれからを紹介しました。ZOZO TECH BLOGについては、後続の2つ目と3つ目の記事も楽しみにしてもらえると嬉しいです。 現在、DevRelブロックメンバーの募集は行なっていませんが、DevRelブロックが技術広報という形で支援するのはエンジニアの皆さんです。一緒にZOZOのサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。ZOZO Researchの研究員の古澤・北岸・平川です。2023年7月25日(火)から7月28日(金)にかけて画像の認識・理解シンポジウムMIRU2023に参加しました。この記事では、MIRU2023でのZOZO Researchのメンバーの取り組みやMIRU2023の様子について報告します。 目次 目次 MIRU2023 企業展示 全体の動向 若手プログラム インタラクティブセッション [IS3-46] 着用者の体型を考慮したファッションコーディネート推薦 [IS3-87] ファッショントレンドの検出と予測:SNS投稿データのクラスタリングと時系列解析 気になった研究発表 [OS3B-L2] Instruct 3D-to-3D: Text Instruction Guided 3D-to-3D conversion [OS4A-L2] 数式ドリブン教師あり学習によるセマンティックセグメンテーション [OS1A-L2] アテンションはアノテーションの代わりになるか?:テキスト−画像生成モデルの注意機構を利用した領域分割の弱教師あり学習 [IS2-16] セグメンテーションマスクを利用した動画からの静的なNeRF表現の学習 まとめ 最後に おまけ MIRU2023 MIRUとは、Meeting on Image Recognition and Understandingという画像の認識・理解についてのシンポジウムです。2023年の今回はアクトシティ浜松においてオフラインとオンラインのハイブリッド形式で開催されました。昨年に引き続き今年もハイブリッド開催ということで、過去最多の1513名(オンライン参加含む)もの方々が参加されたそうです。ZOZO NEXTは、このMIRU2023にゴールドスポンサーとして協賛させていただきました。 cvim.ipsj.or.jp 昨年のMIRU2022に参加した際のレポートは以下の記事をご覧ください。 techblog.zozo.com 企業展示 企業展示ブースでは、ZOZO NEXTにおける取り組みについてポスターを用いて紹介させていただきました。ZOZOが提供する多角的なファッションサービスとそこから生み出される多様なデータを活用した研究事例について、最新の研究成果も交えて紹介させていただきました。大変喜ばしいことに多くの方々に興味を持っていただき、お話をさせていただくことができました。ブースまで足を運んでくださった皆さま、誠にありがとうございました。展示していたポスターはこちらです。 全体の動向 今回のMIRUでは、生成モデルと3Dモデルについての研究が目立ちました。特に生成モデルに関しては、昨年は敵対的生成ネットワークを使用する研究が多かったと記憶していましたが、今年はStable Diffusionなどで注目を集めている拡散モデルを用いた研究へとシフトが見られました。3Dモデルに関しては、昨年に引き続きNeural Radiance Fields (NeRF)に関する研究が行われており、新たにNeRFと拡散モデルを組み合わせる研究も見受けられました。 また、近年の生成モデルの発展を受け、「ニューラルデジタルヒューマン合成の最先端」と「大規模言語モデル時代のHuman-in-the-Loop機械学習」の2つのチュートリアル講演が行われました。前者の講演では、人間の3Dモデル生成について、敵対的生成ネットワークを用いた研究から近年の拡散モデルを使用した研究までレビューされており非常に興味深かったです。後者の講演では、クラウドソーシングによる大規模教師データの作成や安全性・公平性・多様性を担保するための人間からのフィードバックの活用について紹介がありました。 特別講義では、「画像と言語の基盤モデルの現状とこれから」と「見立てて見て取るための視覚的表現とインタラクティビティ」の2つの講演が行われました。前者の講演では、大規模モデルのサステナビリティや今後の発展の方向性などについても議論が行われ、今後の研究を考える上で大変参考になるお話を多く聞くことができました。また、後者の講演では、文脈などの見えないものを「見立てて見て取る」ヒトの能力を、コンピュータで解明するにあたっての課題と展望について伺いました。人が情報を認識するときに、情報そのものだけでなく、誰が言っているかなどの文脈を考慮して認識しているというご指摘が興味深かったです。 若手プログラム 弊社の研究員である後藤が、MIRU2023若手プログラムに参加しました。本プログラムは画像認識の分野で研究をしている若手を対象に、研究・開発を進める上で役立つ知見を学び合い、参加者同士で交流をする企画です。今年は「データをMIRU」をテーマに、9つのグループに分かれて画像認識に深く関わりのある様々な領域のサーベイを行いました。 後藤は、「データとプライバシー」に関するグループサーベイを行いました。データ駆動のビジネスモデルが勢いを強める中、学習データや出力に関するプライバシーや著作権に関する訴訟が増加しています。作成した資料では、公開データセットの学習に関するプライバシーの問題点を明らかにし、データセット・法律・アルゴリズムによる解決法を議論しています。 若手の会グループサーベイ資料は以下のページから見ることができます。 sites.google.com インタラクティブセッション ZOZO Researchからはインタラクティブセッション2件を発表しました。各研究の要約は以下の通りです。 [IS3-46] 着用者の体型を考慮したファッションコーディネート推薦 平川優伎, 斎藤侑輝 (ZOZO Research) パーソナライズされたファッションコーディネート推薦を実現する上で着用者の体型情報は重要な情報です。一方で、先行研究としてドレスなど単一のアイテムと体型情報を考慮した研究が知られていますが、複数のアイテムから成るコーディネートを考慮する場合は議論の余地があります。そこで、本研究では集合マッチングに基づくコーディネート推薦モデルを拡張し、着用者の体型とコーディネートを構成する複数のアイテムの相性を推定する深層学習モデル及び学習手法を提案しました。さらに、コーディネートを構成するアイテム画像と着用者の体型データから成る独自データセットを用いた性能評価実験により、コーディネート補完問題における提案手法の優位性を示しました。 [IS3-87] ファッショントレンドの検出と予測:SNS投稿データのクラスタリングと時系列解析 北岸毅一, Sai Htaung Kham, 後藤亮介 (ZOZO Research) ファッショントレンドの形成は多様な要素が絡み合うものであり、その分析は専門的知識と高度な労力を必要とします。これにより、トレンドの分析が頻繁に更新されることは少なく、微細なトレンドは見落とされる傾向にあります。本研究では、5年間のファッションSNSの投稿データから、タグデータの潜在トピックを抽出し、スナップ画像からのスタイルクラスタを構築しました。さらにGranger causality検定を用い、スタイルに影響を与えるトピックを検出しました。これにより、月単位でのファッションスタイルの変遷と、その発生要因の検出を自動化するための手法を確立しました。 気になった研究発表 私たちが個人的に興味を持った研究について紹介します。 [OS3B-L2] Instruct 3D-to-3D: Text Instruction Guided 3D-to-3D conversion Hiromichi Kamata, Yuiko Sakuma, Akio Hayakawa, Masato Ishii, Takuya Narihira (Sony Group) この研究では、テキストの指示に従って画像を編集する拡散モデルであるInstructPix2Pixの拡張として、テキスト指示による3Dモデルの編集に取り組んでいます。ただし、単純に学習方法を3次元に拡張するのではなく、NeRFとInstructPix2Pixを組み合わせることで3Dモデルの編集を実現しています。具体的には、まず、元の3Dモデルを作成するsourceモデルとそのコピーとしてtargetモデルを準備します。次に、元の3Dモデルから取得した2D画像に対しノイズを加え、InstructPix2Pixでテキストに基づきノイズを推論します。最後に、ノイズの推論結果からScore Distilation Samplingと呼ばれる方法でtargetモデルの勾配を取得し、targetモデルの重みを更新していきます。これにより編集後の3Dモデルを作成するモデルを得ることができるとのことです。また、この研究では、Dynamic Scalingという提案手法により編集強度の制御を可能にしているそうです。結果は こちらのプロジェクトページ からも見ることができるそうなので、興味のある方はぜひ見てみてください。 [OS4A-L2] 数式ドリブン教師あり学習によるセマンティックセグメンテーション 篠田理沙 (AIST, 京大), 速水亮 (AIST, 東京電機大), 中嶋航大 (AIST), 井上中順, 横田理央 (東工大), 片岡裕雄 (AIST) データセットの作成の際には、各データに正解ラベルを付与するのには膨大な人的・時間的コストが掛かってしまうという課題や不適切なデータが混入してしまう危険性があります。この研究では、特にセマンティックセグメンテーションというタスクに着目し、様々な中空の多角形を数式に基づいて生成することで、実画像や人手のアノテーションを必要としないデータセットを作成されていました。さらに、提案データセットとCOCO-Stuff-164kという実画像データセットのそれぞれについて事前学習を行なったモデルの、ファインチューン後の精度を比較されていました。結果としては、ADE-20k・Cityscapesという実画像データセットに対するファインチューンについて、提案データセットを用いたモデルの方がより良い結果を出したとのことです。個人的には、数式に基づいてデータを生成しそれを学習することで、実画像よりも良い事前学習ができてしまうというのはとても面白くかつ強力な手法であると思いました。企業で研究している身としては、商用利用が可能な点も非常に嬉しい点です。また、画像データだけではなく、音声や言語といったデータにも拡張できるのかといった点も気になりました。 [OS1A-L2] アテンションはアノテーションの代わりになるか?:テキスト−画像生成モデルの注意機構を利用した領域分割の弱教師あり学習 吉橋亮太, 大塚雄也, 土井賢治, 田中智大 (ヤフー) 近年、Semantic Segmentation、Poanoptic Segmentation、Instance Segmentationなど様々な領域分割タスクに関する研究が盛んに行われています。一般に、領域分割モデルの学習には大量のセグメンテーションマスク付きの画像が必要になりますが、1枚あたりのアノテーションコストが非常に高いため、データセットサイズがスケールしないという課題があります。本研究では拡散モデルベースの画像生成モジュールと領域分割モジュールを組み合わせることで、テキストと画像のペアからセグメンテーションマスク付き画像をEnd-to-Endに生成する手法を提案されています。このアプローチでは、セグメンテーションマスクのアノテーション付き画像が生成されるため、領域分割モデルの学習データとして使用する場合に、アノテーションが原理的に不要になります。PASCAL VOCデータセットを用いた実験では提案手法は生成データのみを用いてmIoU 50%程度と比較的良好な結果を達成できたそうです。ファッションの領域においても、スナップ画像の認識タスクなどを高精細に実行するためには、領域分割の技術が必要不可欠です。本研究のようなアプローチを用いて生成した大量のラベル付き画像を用いて半自動的に学習した領域分割モデルを、実データの分析や認識モデルの性能向上に活かすことができるのか気になります。 [IS2-16] セグメンテーションマスクを利用した動画からの静的なNeRF表現の学習 大隣嵩 (東大), 池畑諭 (NII, 東工大, 東大), 相澤清晴 (東大) この研究は、動く物体を含む動画からのNeRF表現の学習に関する研究です。NeRFは、特定の物体を様々な角度から撮影し、その結果をもとに高品質な3Dモデルを生成する技術です。複数の角度から撮影された画像を動画で代用する研究はよく知られていますが、撮影中に物体が動いてしまうと3Dモデルの作成が難しくなるという課題が存在します。この問題を解決するため、SegFormerから取得したマスクをタスクに合わせて拡張し、動的な物体を除外して3Dモデルを生成するという提案がされていました。具体的には、Test-time augmentation(複数の拡張結果を融合させることでエラーを軽減する)や、Gradual mask dilation(動く物体を覆う領域を広げて動く物体をもれなく学習から除外する)という方法を用いたとのことです。ファッション分野においても、このような手法の進化により、より容易にリアルな質感の3Dモデルが作成できるようになる可能性があると感じました。 まとめ 本記事では、MIRU2023の参加レポートをお伝えしました。昨年に引き続き今年のMIRUも現地で楽しむことができ、研究のトレンドや技術進展を学び、多くの方々と意見交換を行う良い機会となりました。MIRU2023で得られた知見や経験を今後の研究に取り入れ、より良い研究開発を行なっていきたいと思います。 最後に ZOZO NEXTでは次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できるエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 zozonext.com おまけ 学会の合間にさわやかのハンバーグを食べに行きました。さわやかはTV・YouTube等で何度も取り上げられている有名なハンバーグ店で、新鮮な牛肉を使ったハンバーグを実際に味わうことができたのは非常に嬉しかったです。