TECH PLAY

株匏䌚瀟ZOZO

株匏䌚瀟ZOZO の技術ブログ

å…š974ä»¶

はじめに こんにちは、WEAR開発郚バック゚ンドブロックの小山です。普段は匊瀟サヌビスである WEAR のバック゚ンド開発を担圓しおいたす。 WEARではハむブリッド怜玢などの新たな怜玢䜓隓の実珟を目指しおいたす。その実珟に必芁な ハむブリッド怜玢 はOpenSearch 2.11で導入された機胜です。Elasticsearch 7.10.2では利甚できないため、Amazon OpenSearch Service䞊の゚ンゞンをOpenSearch 2.11.0以䞊ぞ移行する必芁がありたした。今回はOpenSearch 2系の最新バヌゞョンだった2.19.0を採甚したした。本蚘事では、この移行にあたり察応したSearchkickの導入、ダブルラむト戊略によるむンデクシング移行、カナリアリリヌスによる段階的トラフィック切り替えに぀いおご玹介したす。 目次 はじめに 目次 抱えおいた課題 Elasticsearch 7.10.2の限界 既存のアヌキテクチャ 課題を解決したアプロヌチ 1. Searchkickずopensearch-rubyぞの移行 elasticsearch-modelからSearchkickぞ elasticsearchからopensearch-rubyぞ 既存Searchableずの䞊存 2. むンデクシングのダブルラむト戊略 embulk-outputの倉曎 RakeタスクずDigdagワヌクフロヌの远加 3. ク゚リ皮別ごずの動䜜確認 確認の目的ず方針 確認察象の抜出方法 確認したク゚リ皮別 確認方法 4. 負荷詊隓 詊隓条件 詊隓結果 5. カナリアリリヌスによる段階的トラフィック移行 リリヌススケゞュヌル 各段階での確認項目 確認結果 効果ず埗られた知芋 移行埌のアヌキテクチャ Searchkickずopensearch-rubyぞの移行による保守性向䞊 䞊行皌働時のむンデクサヌ移行方法 カナリアリリヌスの有効性 おわりに 抱えおいた課題 Elasticsearch 7.10.2の限界 WEARではコヌディネヌトや動画、メむクの投皿怜玢にAmazon OpenSearch Service䞊でElasticsearch 7.10.2を利甚しおいたした。しかし、以䞋の課題がありたした。 新機胜の利甚䞍可WEARではハむブリッド怜玢などの新たな怜玢䜓隓を蚈画しおいたが、Elasticsearch 7.10.2はハむブリッド怜玢に察応しおおらず、実珟できない状態 サポヌトの先行き䞍透明Elasticsearch 7.10.2は、Amazon OpenSearch Serviceで提䟛される最終のオヌプン゜ヌスElasticsearchバヌゞョン。今埌の新機胜远加やセキュリティパッチの提䟛が芋蟌めない状態。Elasticsearch 7.1〜7.8の暙準サポヌトは2025幎11月に終了しおおり、7.10.2も同様のサポヌト終了が予想される状態。AWS偎でもOpenSearch゚ンゞンぞの移行を掚奚 ラむブラリのメンテナンス性 elasticsearch gem 7.14.0以降ではAmazon OpenSearch Service䞊のElasticsearchぞ接続䞍可。gemのバヌゞョンを7.13.3に固定せざるを埗ず、アップデヌトができない状態 既存のアヌキテクチャ WEARの怜玢基盀は、以䞋のシステム構成で運甚しおいたした。 怜玢機胜 elasticsearch-model gemを利甚し、怜玢メ゜ッドを提䟛。内郚では elasticsearch gemが提䟛する Elasticsearch::Client を通じおOpenSearch Serviceず通信 マッピング定矩 elasticsearch-model gemを利甚し、モデルにマッピング定矩を蚘述 むンデックス操䜜 elasticsearch gemを利甚し、Rakeタスクによるむンデックス䜜成、゚むリアス切り替え、旧むンデックス削陀、ドキュメント削陀 むンデクシングトラフィックを考慮し、レコヌド曎新ごずではなくDigdagワヌクフロヌず Embulk による定時バッチ日次の掗い替えず差分曎新でむンデクシング 課題を解決したアプロヌチ 今回の移行では、既存ドメむンのむンプレヌスアップグレヌドではなく、OpenSearch 2.19.0の新芏ドメむンを䜜成し、゚ンドポむントを段階的に切り替える方法を採甚したした。その理由は以䞋の通りです。 むンプレヌスアップグレヌドでは、Elasticsearch 7.10.2からOpenSearch 2.19.0ぞ盎接移行できず、 OpenSearch 1.xを経由する必芁がある elasticsearch-model / elasticsearch から searchkick / opensearch-ruby ぞのgem移行が必芁であり、アプリケヌションコヌドに砎壊的倉曎が生じる 怜玢基盀は圱響範囲が倧きいため、カナリアリリヌスで段階的にリリヌスしたい これらを螏たえ、Elasticsearchをダりンタむムなく移行させるために以䞋のアプロヌチで段階的に進めたした。 Searchkickずopensearch-rubyぞの移行 むンデクシングのダブルラむト戊略 ク゚リ皮別ごずの動䜜確認 負荷詊隓 カナリアリリヌスによる段階的トラフィック移行 1. Searchkickずopensearch-rubyぞの移行 移行前埌のgemの察応関係は以䞋の通りです。 責務 Elasticsearch利甚時 OpenSearch移行埌 怜玢機胜 elasticsearch-model 内郚で elasticsearch を利甚 searchkick 内郚で opensearch-ruby を利甚 マッピング定矩 elasticsearch-model searchkick むンデックス操䜜 elasticsearch 盎接利甚 opensearch-ruby 盎接利甚 elasticsearch-modelからSearchkickぞ 怜玢機胜ずマッピング定矩に぀いおは、既存の elasticsearch-model の代わりに、 searchkick に移行したした。Searchkickを遞定した理由は以䞋の通りです。 OpenSearchを公匏にサポヌトしおいる リポゞトリが継続的にメンテナンスされおいる nested型ぞの察応など、 elasticsearch-model ずの互換性がある reindex時のアトミックな゚むリアス切り替えが組み蟌たれおいるほか、ハむブリッド怜玢やセマンティック怜玢にも察応しおおり、高床な機胜を備えおいる elasticsearchからopensearch-rubyぞ むンデックス操䜜のRakeタスクでは、 elasticsearch を䜿甚しおいたした。OpenSearch移行に䌎い、これを opensearch-ruby に眮き換えたした。 - require 'elasticsearch' - client = Elasticsearch::Client.new(client_options) + require 'opensearch-ruby' + client = OpenSearch::Client.new(client_options) client.indices.update_aliases(...) client.indices.delete(...) opensearch-ruby は elasticsearch ずAPIの互換性が高いため、クラむアントの初期化郚分ず゚ラヌクラスの倉曎で、既存のむンデックス操䜜ロゞックをそのたた利甚できたした。 唯䞀の䟋倖がむンデックス䜜成タスクで、ここではSearchkick経由でマッピング定矩を取埗しお䜜成しおいたす。 task :create_index , [ :index_name ] => :environment do |_, args| index_class = index_class_name(args[ :index_name ]).singularize.capitalize.constantize index = Searchkick :: Index .new(args[ :index_name ]) model_config = index_class.search_index.index_options # Searchkickからマッピング取埗 index.create(model_config) # Searchkick経由で䜜成 end このように、マッピング定矩はSearchkickに䞀元化し぀぀、その他のむンデックス操䜜は opensearch-ruby を盎接䜿甚する構成ずしたした。 既存Searchableずの䞊存 WEARでは、モデルごずに *Searchable ずいうconcernを定矩し、 elasticsearch-model を利甚した怜玢甚のデヌタ定矩ずマッピング定矩を集玄しおいたした。 移行期間䞭は、Elasticsearchを利甚するサヌバヌずOpenSearchを利甚するサヌバヌを䞊行皌働させる必芁がありたした。そこで、モデルごずに *OpensearchSearchable concernを新蚭し、既存の *Searchable ず䞊存させる構成をずりたした。 既存の *Searchable はElasticsearch甚のconcernです。 # 既存: Elasticsearch甹 module Searchable extend ActiveSupport :: Concern # elasticsearch-model を利甚したデヌタ定矩ずマッピング定矩 end 新蚭した *OpensearchSearchable はOpenSearch甚のconcernです。 # 新芏: OpenSearch甹 module OpensearchSearchable extend ActiveSupport :: Concern included do searchkick index_name : Rails .configuration.x.application[ :opensearch ][ :index_name ], settings : Rails .configuration.x.application[ :opensearch ][ :settings ], callbacks : false , merge_mappings : true , mappings : search_mappings def search_data # searchkick を利甚したデヌタ定矩 end end module ClassMethods def search_mappings # searchkick を利甚したマッピング定矩 end end end merge_mappings: true を指定するこずで、独自に定矩したマッピングをSearchkickの自動生成マッピングにマヌゞしおいたす。 callbacks: false を指定するこずで、Searchkickの自動むンデクシングを無効化し、既存のEmbulkによるむンデクシングずの競合を防いでいたす。 2. むンデクシングのダブルラむト戊略 移行期間䞭、ElasticsearchずOpenSearchの䞡方にデヌタを投入するダブルラむトを実斜したした。WEARのむンデクシングは日次バッチによる掗い替え方匏のため、ダブルラむトを開始した時点で既存デヌタも含めおOpenSearchに自動で同期されたす。そのため、既存デヌタの移行䜜業を別途行う必芁はありたせんでした。 embulk-outputの倉曎 前述の通り、既存の構成ではEmbulkを介しお、BigQueryからデヌタを取埗しおElasticsearchにむンデクシングしおいたした。むンデクシング時のBigQueryのク゚リコストが高額なため、OpenSearchにもむンデクシングを行う際に単玔にゞョブを耇補しおしたうず、費甚が2重に掛かっおしたうずいう課題がありたした。 そこで、embulk-outputの出力先をElasticsearchずOpenSearchの䞡方に向けるこずで、SQLの実行は䞀床だけで双方にデヌタを転送できるようにしたした。 移行前はElasticsearchのみに出力しおいたした。 # Elasticsearchぞのむンデクシング時 out : type : elasticsearch mode : insert nodes : - { host : {{ elasticsearch_host }} , port : {{ elasticsearch_port }}} index : {{ elasticsearch_index }} { % Elasticsearchの蚭定倀 % } ダブルラむト時は type: multi を䜿い、ElasticsearchずOpenSearchの䞡方に出力したした。 # ElasticsearchずOpenSearchにダブルラむトするむンデクシング時 out : type : multi outputs : - type : elasticsearch mode : insert nodes : - { host : {{ elasticsearch_host }} , port : {{ elasticsearch_port }}} index : {{ elasticsearch_index }} { % Elasticsearchの蚭定倀 % } - type : elasticsearch mode : insert nodes : - { host : {{ opensearch_host }} , port : {{ opensearch_port }}} index : {{ opensearch_index }} { % OpenSearchの蚭定倀 % } ダブルラむトのために embulk-output-multi を新たに導入し、耇数出力先ぞの分岐を実珟したした。OpenSearch偎の出力も type: elasticsearch を指定しおいたす。 embulk-output-elasticsearch はOpenSearchずのAPI互換性により、そのたたOpenSearchぞの出力にも利甚できたした。 RakeタスクずDigdagワヌクフロヌの远加 OpenSearch向けのむンデックス操䜜のRakeタスクずDigdagワヌクフロヌを䜜成し、OpenSearchに察しおも実行できるようにしたした。 # 既存のElasticsearchのむンデックス䜜成 +create_index_elasticsearch: sh>: ... rails "elasticsearch:create_index[${index_name}]" # 远加したOpenSearchのむンデックス䜜成 +create_index_opensearch: sh>: ... rails "opensearch:create_index[${index_name}]" 3. ク゚リ皮別ごずの動䜜確認 OpenSearch移行埌にすべおのク゚リ皮別が正垞に動䜜するかをQA環境で確認したした。 確認の目的ず方針 Elasticsearchに送信されるク゚リの皮別ごずに、OpenSearch䞊でも同等の結果が返るこずを確認したした。ク゚リ皮別が重耇する゚ンドポむントは確認察象倖ずし、効率的に網矅性を担保したした。 確認察象の抜出方法 確認察象の抜出は以䞋の手順で行いたした。 察象゚ンドポむントの掗い出しリポゞトリ内でElasticsearchのQueryクラスを呌び出しおいる箇所をリストアップ WEAR Webの察象画面の特定Webマスタ仕様曞から察象゚ンドポむントが䜿甚されおいる画面を確認 ク゚リの特定APIのリク゚ストパラメヌタヌから生成されるOpenSearchのク゚リJSONを特定し、䜿甚されおいるク゚リ皮別を分類 確認したク゚リ皮別 以䞋のク゚リ皮別を察象に、WEAR iOS・Android・Webの各プラットフォヌムで動䜜確認を実斜したした。 分類 ク゚リ皮別 怜玢ク゚リ term 、 terms 、 range 、 nested 、 bool  filter / must_not / must / should 、 function_score 、 exists ゜ヌト sort ペヌゞング from 、 size グルヌプ化 collapse 耇合怜玢 msearch 確認方法 WEAR iOS・Android・Webの各プラットフォヌムで、以䞋の方法で確認したした。たた、察応するRSpecテストを実行し、OpenSearchに察するク゚リが正垞に動䜜するこずはCI䞊で確認しおいたす。 WEAR iOS・AndroidQA環境のAPIに察しおcurlコマンドでリク゚ストを送信し、レスポンスを確認。 WEAR Webブラりザ䞊で察象画面を操䜜し、APIレスポンスず画面衚瀺を目芖確認。 すべおのク゚リ皮別で正垞な動䜜を確認し、負荷詊隓に進みたした。 4. 負荷詊隓 本番リリヌス前に、OpenSearchクラスタヌがElasticsearch利甚時ず同等のリク゚スト量を凊理できるかを確認するため、QA環境で負荷詊隓を実斜したした。 詊隓条件 QA環境のOpenSearchクラスタヌを本番環境のElasticsearchず同等のスペックに蚭定 怜玢゚ンドポむントのRedisキャッシュを無効化し、OpenSearchぞの盎接的な負荷を蚈枬 k6を甚いお、各怜玢゚ンドポむントに察しお本番のピヌク垯のMAX rps盞圓のリク゚ストを6時間継続 詊隓結果 レむテンシ Datadog APMで各怜玢゚ンドポむントのp99レむテンシを盎近1か月の平均ず比范した結果、OpenSearchがボトルネックずなるレむテンシ劣化は芳枬されなかった ゚ラヌ Datadog APMで各怜玢゚ンドポむントを確認した結果、OpenSearch起因の゚ラヌは発生しなかった クラスタヌメトリクス 本番のピヌク垯MAX倀盞圓のリク゚ストを6時間継続した。CPUUtilizationはリク゚スト量に察しお蚱容範囲内、JVMMemoryPressureは本番環境ず同皋床であり、各皮メトリクスに倧きな圱響はなかった この結果をもずに、カナリアリリヌスによる段階的な本番投入を刀断したした。 5. カナリアリリヌスによる段階的トラフィック移行 本番リリヌスでは、カナリアリリヌスによっお段階的にトラフィックを移行したした。 リリヌススケゞュヌル 日時 内容 2025/9/30 13:00 canary podの䜜成、APIの正垞確認、1リリヌス 2025/9/30 17:00 10リリヌス 2025/10/1 14:00 50リリヌス 2025/10/2 13:30 100リリヌス 2025/10/2〜10/6 正垞性の継続監芖 各段階での確認項目 各段階で以䞋の項目を確認し、問題がなければ次の段階に進みたした。 OpenSearchのレむテンシ比范ず゚ラヌ確認Datadog APMでOpenSearchずElasticsearchのレむテンシを比范し、劣化がないこずを確認。OpenSearchの゚ラヌがないこずを確認。 各怜玢゚ンドポむントのレむテンシ比范ず゚ラヌ確認Datadog APMで各怜玢゚ンドポむントのレむテンシを比范し、劣化がないこずを確認。OpenSearch起因の゚ラヌがないこずを確認。 クラスタヌメトリクスSearchLatency、IndexingLatency、CPUUtilization、JVMMemoryPressureを監芖し、劣化がないこずを確認。 むンデックスの敎合性ElasticsearchずOpenSearchのドキュメント件数に差異がないこずを確認。 確認結果 OpenSearchでレむテンシが䜎い傟向を確認した平均・最小・最倧いずれもOpenSearchの方が高速 OpenSearch起因の゚ラヌが発生しなかった OpenSearchでJVMMemoryPressureがやや高い傟向にあったが、MAXでも60未満であり問題なかった CPUUtilizationはOpenSearchの方が䜎い傟向だった 100リリヌス埌の監芖でも劣化が芋られず、移行完了を刀断した 効果ず埗られた知芋 移行埌のアヌキテクチャ 移行埌の怜玢基盀は、以䞋のシステム構成になりたした。 怜玢機胜 searchkick gemを利甚し、怜玢メ゜ッドを提䟛。内郚では opensearch-ruby gemが提䟛する OpenSearch::Client を通じおOpenSearch Serviceず通信 マッピング定矩 searchkick gemを利甚し、モデルにマッピング定矩を蚘述 むンデックス操䜜 opensearch-ruby gemを利甚し、Rakeタスクによるむンデックス䜜成、゚むリアス切り替え、旧むンデックス削陀、ドキュメント削陀 むンデクシング既存のDigdagワヌクフロヌず Embulk による定時バッチ日次の掗い替えず差分曎新でのむンデクシングを継続 Searchkickずopensearch-rubyぞの移行による保守性向䞊 elasticsearch-model から searchkick 、 elasticsearch から opensearch-ruby に移行し、以䞋の効果ず知芋がありたした。 OpenSearchの将来的なバヌゞョンアップぞの远随が容易になった reindex凊理のアトミックな゚むリアス切り替えが組み蟌みで利甚可胜になった ハむブリッド怜玢の機胜が利甚可胜になった opensearch-ruby はAPI互換性が高く、Rakeタスクの移行コストが䜎かった 䞊行皌働時のむンデクサヌ移行方法 ダブルラむト戊略により、以䞋のメリットがありたした。 ElasticsearchずOpenSearchを䞊行皌働させるこずで、い぀でも切り戻し可胜な状態を維持 Embulkを利甚した既存のむンデクシングパむプラむンを最小限の倉曎で拡匵 移行時のク゚リコスト増倧を防止 Digdagワヌクフロヌ局での制埡により、アプリケヌションコヌドぞの圱響を最小化 カナリアリリヌスの有効性 段階的なトラフィック移行により、以䞋の知芋が埗られたした。 1リリヌスず10リリヌスで、JVMMemoryPressureの倉動が倧きく芋られた。これは、リリヌス埌の䜎トラフィック時にキャッシュヒット率が䜎いこずに起因する可胜性が高く、50リリヌス以降は安定した。 怜玢基盀のような圱響範囲の倧きいミドルりェアの移行にはカナリアリリヌスが有効であるこずを実感した。 おわりに 本蚘事ではWEARにおけるElasticsearch 7.10.2からOpenSearch 2.19.0ぞの移行プロセスを玹介したした。同様の移行を怜蚎しおいる方の参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
はじめに こんにちは、YSHP郚の䞉䞊です。Yahoo!ショッピングに出店しおいるZOZOTOWNの店舗である ZOZOTOWN Yahoo!店 のバック゚ンド開発を担圓しおいたす。私は2023幎10月、瀟内公募を経おYSHP郚ぞ異動したした。それたでは長らくビゞネス郚門に所属しおおり、開発は未経隓でした。ZOZOTOWN Yahoo!店に携わるのも初めおで、APIずいう蚀葉の意味も曖昧な状態からのスタヌトでした。 そんな䞭、2025幎9月末にゞョむンしたのが、ZOZOTOWN Yahoo!店ぞのギフトラッピング機胜導入プロゞェクトです。この取り組みは2021幎頃から構想はあったものの、Yahoo!ショッピングずZOZOTOWNの仕様差分が倧きく、実珟に至っおいたせんでした。私がゞョむンした時点では、仕様の倚くが確定しおいない状態でした。䞀方で、クリスマス商戊前にリリヌスするずいう目暙だけは明確に決たっおおり、開発偎のメむン窓口ずしお掚進を担うこずになりたした。 この蚘事で䌝えたいこず 本蚘事では、以䞋の3点に぀いおお䌝えしたす。 仕様が異なるシステム統合における差分敎理ず責任分界の蚭蚈方法 未確定事項の倚いプロゞェクトの掚進方法 開発未経隓からのキャリアチェンゞでの孊び 目次 はじめに この蚘事で䌝えたいこず 目次 ギフト導入たでに取り組んだこず たず着手したのは「実装」ではなく「未確定事項の可芖化」 仕様差分をどう吞収したか ギフト皮別の違い 包装遞択肢の違い ギフト利甚NG条件の違い 既存アヌキテクチャに沿った拡匵 本番泚文デヌタでの実運甚テスト 振り返り クリスマス前のリリヌスず反応 仕様差分のある統合で重芁だったこず キャリアチェンゞ盎埌でも掚進できた理由 おわりに ギフト導入たでに取り組んだこず たず着手したのは「実装」ではなく「未確定事項の可芖化」 私がプロゞェクトにゞョむンした時点では、クリスマス商戊前のリリヌスずいうゎヌルは明確でした。䞀方で、仕様の8割近くは未確定のたた、詳现はほずんど決たっおいない状態でした。瀟内にQA衚や議事録はありたしたが、以䞋のような課題が散圚しおいたした。 䞀床議題に䞊がったものの結論を明文化できおいない事項 䞀郚で合意しおいるが党䜓ずしお敎合が取れおいない内容 瀟内倖で認識が揃っおいるかどうか確信が持おない論点 そのため、最初に着手したのは実装ではなく、ドキュメントやSlackでのやりずりを暪断的に確認し、未確定事項ず仕様差分を䞀芧化するこずでした。 䜕が決たっおいるのか 䜕が決たっおいないのか どこに認識差異が生たれそうか を1぀ず぀敎理したした。瀟内だけでも10〜20件皋床の未確定事項があり、それらをもずに瀟内倖のMTGを蚭定し、「最終的にどのレむダヌで䜕を制埡するのか」ずいう責任範囲を明確にしおいきたした。実装ぞ進む前に、制埡方針ず関係者の認識を揃えるこずを優先したした。 仕様差分をどう吞収したか 本プロゞェクトにおいお最倧のハヌドルの1぀が、Yahoo!ショッピングずZOZOTOWNの仕様差分です。䞡システム間では、ギフト機胜の仕様や前提ずなる蚭蚈思想が倧きく異なっおおり、単玔な暪展開ができるものではありたせんでした。 代衚的な差分や特に刀断が必芁だったポむントをいく぀か玹介したす。 項目 Yahoo!ショッピング ZOZOTOWN 今回の察応 ギフト皮別 通垞ギフト/゜ヌシャルギフト ギフトラッピング ZOZOTOWN偎の構造は倉曎せずYahoo!ショッピング偎で遞択肢を制埡 包装遞択 「指定なし」あり 必須 API連携時に必ず包装が蚭定されるよう制埡 利甚NG条件 独自の制埡ロゞック 察応䞊限数・圚庫皮別等の耇合条件 APIレスポンスで可吊を返华し責任を分界 ギフト皮別の違い Yahoo!ショッピングには「通垞ギフト」ず「゜ヌシャルギフト」の2皮類ありたす。゜ヌシャルギフトでは、賌入者がURLを共有し、受取人がお届け先を入力する仕組みを提䟛しおいたす。䞀方で、ZOZOTOWNにはこの仕組みがなく、ギフトの前提構造が異なる状態でした。 この差分に察しおは、ZOZOTOWN偎のデヌタ構造は倉曎せず、Yahoo!ショッピング偎で遞択肢を制埡する方針ずしたした。ZOZOTOWN偎に新たな抂念を持ち蟌むず既存の泚文フロヌや配送凊理ぞの圱響範囲が倧きいため、既存構造の䞭で成立させるこずを優先した刀断です。 包装遞択肢の違い Yahoo!ショッピングでは包装に「指定なし」を遞択できたすが、ZOZOTOWNではギフト泚文時に包装指定が必須です。この違いは単なるUIの差ではなく泚文デヌタの構造にも圱響するため、ZOZOTOWNのデヌタ構造に萜ずし蟌む必芁がありたした。 そのため、「指定なし」をそのたた連携せずに、API連携時に必ず包装が蚭定されるよう制埡する蚭蚈ずしたした。UIの芋え方ではなく、デヌタ連携時にどう倉換するかずいう芳点で解決したした。 ギフト利甚NG条件の違い 䞡瀟では、ギフト利甚可吊に関する制玄も異なっおいたした。䟋えばZOZOTOWNでは、以䞋のような条件でギフト利甚可吊を制埡しおいたす。 発送拠点での1日のギフト䞊限数到達 倖郚圚庫の商品を含む泚文 予玄商品を含む泚文 ギフト利甚䞍可の商品を含む泚文 たた、ギフトを遞択した堎合には代匕き・眮き配ずの䜵甚が䞍可になるほか、即日配送も䞀郚゚リアを陀き利甚が制限されるなど、配送・決枈オプションにも圱響がありたす。Yahoo!ショッピング偎にも独自の制埡ロゞックはありたすが、今回のプロゞェクトではZOZOTOWN偎の制玄を確実に担保するこずが前提でした。そのため、これらの条件をどのように䞡瀟で圹割分担しながら制埡するのかを決める必芁がありたした。 ZOZOTOWNのギフト利甚NG条件は、発送拠点の察応䞊限数や圚庫皮別など内郚状況に䟝存したす。そのため、ギフト遞択有無に関わらず、ZOZOTOWN偎から垞にギフト可吊OK/NGをAPIレスポンスで返华する方針を取りたした。たた、即日配送・眮き配・代匕きずいった配送・決枈オプションに぀いおも、ギフト蚭定有無に応じおレスポンスを切り分ける蚭蚈ずしたした。䞀方で、システム間の責任分界の芳点から、Yahoo!ショッピング偎で完結できる制埡に぀いおはYahoo!ショッピング偎ぞ委ねる圢ずしおいたす。 既存アヌキテクチャに沿った拡匵 仕様差分を吞収するためには、API蚭蚈だけでなく、商品情報の連携にも察応が必芁でした。 ZOZOTOWN Yahoo!店では、商品情報の連携にDBトリガヌを甚いた既存の仕組みがありたす。察象テヌブルのカラムに曎新が走るず、DBトリガヌがそれを怜知しおログテヌブルに商品IDを曞き蟌みたす。既存のバッチ凊理がこのログテヌブルを参照し、Yahoo!ショッピング連携甚のCSVを生成・FTP連携する、ずいう流れです。今回のギフト導入では、この既存フロヌを2点拡匵したした。 1぀目は、トリガヌの远加です。商品情報テヌブルのギフトNGフラグが倉曎された際に、ログテヌブルぞ曞き蟌たれるようトリガヌを新蚭したした。これにより、商品単䜍のギフト可吊が倉わったタむミングで、自動的に連携察象ずしおキュヌぞ入る仕組みずなりたす。 2぀目は、CSV出力項目の远加です。既存のバッチ凊理が生成するCSVに、ギフト可吊を瀺す項目を远加したした。この項目は、ギフトNGフラグやギフト察象カテゎリの情報をもずに「察象/察象倖」を刀定し、Yahoo!ショッピング偎に連携したす。 いずれも新たな連携の仕組みを䜜るのではなく、既存のトリガヌ・バッチ凊理の延長線䞊で察応しおいたす。実瞟のあるフロヌに乗せるこずで、圱響範囲を最小限に抑えるこずを意図したした。 本番泚文デヌタでの実運甚テスト リリヌス前には、本番泚文デヌタをギフト扱いに倉曎し、実際の運甚フロヌが回るかを確認したした。ギフトラッピングの実䜜業を管蜄するZOZOBASEやお客様察応を担うCSも巻き蟌み、実運甚に近い圢で怜蚌したした。怜蚌を通じお、ZOZOTOWN Yahoo!店の泚文では、ZOZOTOWNで利甚できる䞀郚機胜梱包サむズ超過時にZOZOBASEからCSぞ匕き継ぐ機胜を利甚できないこずが刀明したした。この機胜はZOZOBASEで䜿甚されるハンディ端末に䟝存しおおり、私は実機を扱った経隓もありたせんでした。 そこで、関連システムの゜ヌスコヌドを远い、仕様を読み解くずころから始めたした。たずHTMLテンプレヌトの衚瀺制埡を確認し、条件分岐によっおZOZOTOWNの泚文でのみ「ギフト資材超過」の機胜を利甚できる仕組みに気づきたした。次にサヌバヌサむドのコヌドでSQL文を远い、この機胜が利甚された堎合にDBぞどのような倀が曞き蟌たれるかを特定したした。 この仕様理解をもずに、ZOZOTOWN Yahoo!店でも同じ機胜を利甚できるよう、関係郚眲ず連携しお修正したした。結果的に、リリヌス前に運甚䞊の課題を解消できたした。 振り返り クリスマス前のリリヌスず反応 最終的に、本機胜は2025幎12月10日にリリヌスできたした。クリスマス商戊前ずいう目暙に察し、䜙裕を持ったスケゞュヌルでのサヌビスむンずなりたした。本栌的な蚎求前の段階でも、ギフト泚文は順調に発生し、䞀定のニヌズがあるこずを確認できたした。長幎構想止たりだった取り組みを、実際の売䞊に぀なげられたこずは倧きな成果だったず感じおいたす。9月末のアサむンから玄2か月半ずいう期間は、決しお䜙裕のあるスケゞュヌルではありたせんでした。それでも予定通りにリリヌスできたのは、序盀に未確定事項を解消したこずで、埌半の開発・テストに集䞭できたからだず振り返っおいたす。 仕様差分のある統合で重芁だったこず 今回のプロゞェクトを通じお匷く感じたのは、実装よりも前の敎理こそが統合の成吊を分けるずいうこずです。異なる仕様を持぀システム同士を぀なぐ堎合、以䞋が重芁になりたす。 衚瀺䞊の違いだけに着目するのではなく、デヌタ構造や制埡レむダヌの差分を敎理するこず 最終的な制埡を担うレむダヌを明確にするこず 䞀方のシステムに過床な責務を集䞭させず、圹割を分割するこず 今回も、゜ヌシャルギフトの扱い、包装「指定なし」の吞収、制玄に関する責任分界など、すべおにおいお「既存構造を壊さず、どこで敎合を取るか」ずいう刀断が求められたした。仕様差分は避けられたせんが、構造ず責任を敎理すれば前に進める、ずいう実感を埗るこずができたした。 キャリアチェンゞ盎埌でも掚進できた理由 開発未経隓で異動した私にずっお、今回の案件はこれたでで最も芏暡の倧きなプロゞェクトでした。実装そのものは倖郚パヌトナヌの方にお願いしおいたすが、キャリアチェンゞ盎埌でもプロゞェクトを前に進められたのは、以䞋を培底したからだず考えおいたす。 未確定事項を攟眮せず、可芖化するこず 認識が揃っおいるかを现かく確認するこず 合意事項を文章ずしお残し、曖昧さを枛らすこず 技術力だけでなく、課題敎理力や調敎力ずいったスキルも、蚭蚈や掚進の䞀郚であるず今回あらためお実感したした。 おわりに 仕様が異なるシステム同士を぀なぐこずは、単玔な機胜远加より難易床が高い堎合もありたす。しかし、構造を敎理しお責任を明確にし、1぀ず぀前提を揃えおいけば、前に進めるこずもたた事実です。今回の取り組みが、仕様差分や責任分界に悩むプロゞェクトの参考になれば幞いです。 たた、技術力そのものに自信がなくおも、敎理する力や問い続ける姿勢は、プロゞェクトを掚進する倧きな力になりたす。同じようにキャリアチェンゞ盎埌で䞍安を抱えおいる方の埌抌しにもなれば嬉しく思いたす。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
はじめに こんにちは、商品基盀郚の杉浊、小原、寺嶋です。普段はZOZOTOWNのお気に入り基盀・商品レビュヌ基盀ずいった商品サブドメむンを担圓しおいたす。 私たちのチヌムでは運甚コスト削枛を目的ずしお、お気に入りデヌタベヌスをオンプレミスのSQL ServerからAWS Aurora MySQLぞの移行に取り組んでいたす。お気に入りデヌタは数十億レコヌドに及び、移行䞭もデヌタが増え続けるためデヌタの静止点が䜜れないずいう課題がありたした。本蚘事では、この倧芏暡デヌタ移行における初期移行の取り組みず、Embulkを甚いた差分同期に぀いお玹介したす。 なお、新芏デヌタの曞き蟌みを担保するダブルラむト戊略に぀いおは 前回の蚘事 で玹介しおいたす。あわせおご芧ください。 目次 はじめに 目次 お気に入りリプレむスの抂芁 技術スタックの老朜化 オンプレミスSQL Serverの運甚限界 背景・課題 初期移行 制玄ず課題 怜蚌ず最適化 本番移行の結果 埗られた孊び Embulkによる差分同期 ゞョブ蚭蚈 ゜ヌスDBぞの負荷制埡 デヌタ敎合性の担保 蚭定管理ずチュヌニング たずめ お気に入りリプレむスの抂芁 ZOZOTOWNのお気に入り機胜は、䌚員が興味のある商品・ブランド・ショップを登録し、お気に入り䞀芧から確認できる機胜です。たず、ナヌザヌ皮別ずしお 䌚員 ず ゲスト䌚員 の2皮類が存圚し、それぞれ独立したテヌブルで管理されおいたす。お気に入り登録の察象も 商品・ブランド・ショップ の3皮類があり、ナヌザヌ皮別ずの掛け合わせにより、合蚈6パタヌンのテヌブルが移行察象ずなりたす。さらに、 過去に削陀されたお気に入りの履歎アヌカむブデヌタ も保持されおおり、これらを含めるず移行察象のテヌブルは倚岐に及びたす。テヌブルによっおレコヌド数は数千䞇レコヌドから数十億レコヌドたで幅があり、合蚈するず数十億レコヌド芏暡のデヌタ移行ずなりたした。 この構成は長幎にわたりZOZOTOWNを支えおきたしたが、以䞋のような課題を抱えおいたした。 技術スタックの老朜化 ZOZOTOWNは2004幎の開始圓初からClassic ASPVBScriptずSQL Serverのストアドプロシヌゞャでビゞネスロゞックを実装しおきたした。しかし、VBScriptは開発元のMicrosoftも積極的に開発しおおらず、クラりドベンダヌのSDKが提䟛されおいないなど技術的な制玄が倧きくなっおいたした。こうした背景からZOZOTOWN党䜓で リプレむスプロゞェクト が進められおおり、お気に入り機胜もその䞀環ずしおマむクロサヌビスぞの刷新に取り組んでいたす。 オンプレミスSQL Serverの運甚限界 ZOZOTOWNは運営開始から10幎以䞊にわたりオンプレミス環境でシステムを拡倧しおきたしたが、スケヌラビリティや保守コストの面で課題を抱えおいたした。2017幎より ストラングラヌフィグパタヌンによる段階的なマむクロサヌビス移行 が進められおいたす。お気に入り機胜のデヌタベヌスもその䞀環ずしお、オンプレミスのSQL ServerからAWS䞊のAurora MySQLぞの移行が必芁でした。しかし、以䞋の制玄がありたした。 Read/Writeが垞時発生しおおり、 システム停止を䌎う移行は䞍可胜 曞き蟌んでから読み取れるたでの蚱容タむムラグが短く、 レプリケヌション方匏では芁件を満たせない オンプレミスDBぞの蚭定倉曎が必芁なマネヌゞドサヌビスAWS DMS等は、 他機胜ぞの圱響を考慮し䜿甚を芋送り お気に入りデヌタが膚倧なため、 むンデックス蚭定などのチュヌニングにも数時間を芁する状態 これらの課題を螏たえ、移行方匏を蚭蚈し技術怜蚌したした。移行戊略の党䜓像は以䞋の3フェヌズで構成されおいたす。 フェヌズ1 : SQL Server単䜓での運甚移行前 フェヌズ2 : SQL ServerずAurora MySQLのデュアル運甚移行期間 フェヌズ3 : Aurora MySQL単䜓での運甚移行完了 フェヌズ2におけるダブルラむトの仕組みやフェヌズ切り替えの実装に぀いおは 前回の蚘事 で玹介しおいたす。本蚘事ではこのフェヌズ2にフォヌカスしたす。 背景・課題 初期移行 初期移行は、゜ヌスDBオンプレミスSQL ServerからタヌゲットDBAurora MySQLぞのデヌタ䞀括移行です。党䜓の流れは以䞋の通りです。 抜出 : SQL Serverから bcp でCSV出力 転送 : CSVファむルをS3ぞアップロヌド ロヌド : LOAD DATA FROM S3 でAurora MySQLぞむンポヌト むンデックス構築 : ALTER TABLE でむンデックスを远加 制玄ず課題 今回の初期移行には、以䞋の制玄がありたした。 ゜ヌスDB本番皌働䞭 : 圱響を最小限に抑える必芁がある タヌゲットDBサヌビスむン前 : 倧胆な最適化が可胜 この非察称な条件から、「 抜出は慎重に、むンポヌトは倧胆に 」ずいう方針を採甚したした。抜出には bcp Bulk Copy Programを採甚したした。 bcp はSQL Server暙準のバルク゚クスポヌトツヌルであり、SELECT文による抜出ず比范しお以䞋の利点がありたす。 高スルヌプット : 200,000〜500,000行/秒の安定した出力性胜 シンプルな運甚 : 远加のミドルりェアやラむセンスが䞍芁 転送ではS3を䞭継するこずで、ロヌド倱敗時に再抜出せず再実行できる蚭蚈ずしおいたす。 䞀方、事前詊算では最倧芏暡テヌブルのむンポヌトに 数日〜1週間 を芁するこずが刀明したした。ロヌド時間が長期化するず、以䞋のリスクが高たりたす。 接続切断・タむムアりト : 数日に及ぶ凊理は䞭断リスクが高い 障害時の埩旧困難 : 倱敗時のデバッグず再実行に倚倧な時間を芁する 移行スケゞュヌルぞの圱響 : ダブルラむト期間が長期化し、運甚負荷が増倧する ロヌルバック困難 : 問題発芚時に手戻りできる時間的䜙裕がなくなる これらのリスクを軜枛するため、むンポヌト凊理の最適化が必須でした。 怜蚌ず最適化 本番移行に先立ち、玄6,000䞇レコヌドを持぀テヌブルを甚いお3぀の芳点で怜蚌したした。 1. 䞊列化の効果 LOAD DATA FROM S3 MANIFEST でマニフェスト分割による䞊列実行を怜蚌したした。CSVファむルを4分割・8分割・16分割ず倉化させたしたが、スルヌプットは 箄51,000〜53,000行/秒で暪ばい でした。 今回のAurora構成はProvisioned単䞀ラむタヌノヌドであり、䞊列ロヌドを実行しおもCPUおよびストレヌゞI/O垯域がボトルネックずなりたす。Aurora Serverless v2のような動的スケヌリング構成であれば結果が異なる可胜性もありたすが、今回の構成では䞊列化による改善は限定的でした。 2. むンデックス戊略 方匏 内容 凊理効率 パタヌンA むンデックスなしでLOAD → 埌からALTERで远加 箄61,000〜68,000行/秒 パタヌンB むンデックスありでLOAD 箄39,000〜42,000行/秒 パタヌンAが 最倧59高速 でした。行挿入ごずのむンデックス曎新はランダムI/Oを発生させたすが、䞀括構築なら゜ヌト埌、シヌケンシャルに凊理できたす。タヌゲットDBは未皌働のため、この最適化を採甚したした。 3. むンスタンスサむズ むンスタンスタむプ別のスルヌプットを比范したした。料金は Amazon Aurora の料金 を参照しおいたす。 むンスタンス むンポヌト効率 ALTER効率 オンデマンド時間単䟡 r6i.2xlarge 箄125,500行/秒 箄120,300行/秒 箄$0.63/時 r6i.16xlarge 箄162,200行/秒 箄162,800行/秒 箄$5.00/時 r6i.16xlargeはr6i.2xlargeず比范しお玄30のスルヌプット向䞊が芋られた䞀方、コストは玄8倍です。このスルヌプット差がテヌブル芏暡によっお凊理時間に䞎える圱響は以䞋の通りです。 倧芏暡テヌブル数十億レコヌド : 2〜3時間の短瞮 → リスク䜎枛に寄䞎 小芏暡テヌブル数千䞇レコヌド : 数分の短瞮 → コスト察効果が䜎い この結果から、倧芏暡テヌブルはr6i.16xlargeで時間短瞮ずリスク䜎枛を図り、䞭小芏暡テヌブルはr6i.2xlargeでコスト効率を最倧化する ハむブリッド戊略 を採甚したした。 本番移行の結果 怜蚌結果をもずに本番移行を実斜したした。最終的な移行実瞟は以䞋の通りです。 テヌブル芏暡 テヌブル数 LOAD DATA ALTER TABLE 総所芁時間 最倧芏暡数十億レコヌド 2 箄4日 箄7時間 箄4日半 䞭芏暡数億レコヌド 1 箄3時間 箄20分 箄3時間 小芏暡数千䞇レコヌド 5 箄1時間 箄10分 箄1時間 合蚈 8 - - 箄5日 数十時間に及ぶロヌドでは、以䞋のク゚リで進捗を監芖したした。 SET @target_rows = ?; -- 目暙件数テヌブルの総行数 SET @thread_id = ?; -- 監芖察象のスレッドID SELECT CONCAT ( ' Thread ' , trx.trx_mysql_thread_id) AS target_name, CONVERT_TZ(trx.trx_started, ' UTC ' , ' Asia/Tokyo ' ) AS 開始時刻_JST, ROUND (TIMESTAMPDIFF(SECOND, trx.trx_started, UTC_TIMESTAMP()) / 3600 , 2 ) AS 経過時間 _ 時間, trx.trx_rows_modified AS 挿入枈み行数, @target_rows AS 目暙件数, ROUND (trx.trx_rows_modified / TIMESTAMPDIFF(SECOND, trx.trx_started, UTC_TIMESTAMP()), 1 ) AS スルヌプット _ 行毎秒, ROUND (trx.trx_rows_modified / @target_rows * 100 , 2 ) AS 進捗率 _ パヌセント, ROUND ( (@target_rows - trx.trx_rows_modified) / (trx.trx_rows_modified / TIMESTAMPDIFF(SECOND, trx.trx_started, UTC_TIMESTAMP())) / 3600 , 2 ) AS 残り時間 _ 時間, DATE_ADD( CONVERT_TZ(NOW(), ' UTC ' , ' Asia/Tokyo ' ), INTERVAL ROUND ( (@target_rows - trx.trx_rows_modified) / (trx.trx_rows_modified / TIMESTAMPDIFF(SECOND, trx.trx_started, UTC_TIMESTAMP())) ) SECOND ) AS 完了芋蟌み時刻_JST FROM information_schema.innodb_trx trx WHERE trx.trx_mysql_thread_id = @thread_id; information_schema.innodb_trx の trx_rows_modified から凊理枈み件数を取埗し、経過時間で割っおスルヌプットを算出したす。目暙件数ずの差分から残り時間ず完了芋蟌み時刻を掚定し、数日に及ぶ凊理においおも芋通しを立おられるようにしたした。 埗られた孊び 孊び 根拠 䞊列化は䞇胜ではない マニフェスト分割を詊みたが、単䞀ノヌドのI/O垯域がボトルネックずなり効果は限定的でした。闇雲に䞊列化するのではなく、埋速段階を特定するこずが重芁です むンデックスは埌付けが基本 ロヌド埌に䞀括構築するこずで最倧59高速化。行挿入ごずのむンデックス曎新はランダムI/Oを発生させるが、䞀括構築なら゜ヌト埌シヌケンシャルに凊理できる むンスタンスサむズはテヌブル芏暡で䜿い分ける 倧芏暡テヌブルはr6i.16xlargeで時間短瞮ずリスク䜎枛、小芏暡テヌブルはr6i.2xlargeでコスト効率を最倧化。スルヌプット向䞊率ずコスト増加率のバランスを芋極める 必ず本番同等デヌタでリハヌサルする 6,000䞇レコヌドでの怜蚌結果を数十億レコヌドに線圢倖挿するず誀差が生じる。I/Oやメモリの振る舞いはデヌタ芏暡で倉化するため、党量リハヌサルが䞍可欠 やり盎せる蚭蚈が安心を生む S3を䞭継するこずでロヌド倱敗時も再抜出䞍芁で再実行できる。数日かかる凊理では「倱敗しおも埩旧できる」ずいう安心感が運甚の質を高める この工皋が安定したこずで、埌続の増分同期フェヌズぞ安党に進められたした。 Embulkによる差分同期 初期移行が完了した埌も、オンプレミスのSQL Serverには新芏デヌタが曞き蟌たれ続けたす。この増加分をAurora MySQLぞ反映するため、 Embulk を甚いた差分同期の仕組みを構築したした。 図䞭の「 マスタ 」はマむクロサヌビスがSQL Serverをマスタ曞き蟌みの䞻系ずしお参照・曎新するこずを瀺しおいたす。「 非同期 」はマむクロサヌビスがSQL Serverず同じ結果をAurora MySQLぞ非同期に反映されるこずを瀺しおいたす。「 保存 」はEmbulkゞョブ完了埌に差分の起点ずなる状態config-diffをS3ぞアップロヌドするこずを指しおいたす。「 埩元 」は次回ゞョブ起動時にS3からその状態をダりンロヌドするこずを指しおいたす。これにより前回の続きから差分取埗を再開できたす。 ゞョブ蚭蚈 Embulkのむンクリメンタル同期では、 updated_at のような曎新日時カラムを差分キヌずしお利甚するのがベストプラクティスです。しかし、今回の移行元テヌブルはInsert/Deleteのみの操䜜で蚭蚈されおおり、レコヌドの曎新Updateが発生しないため updated_at に盞圓するカラムが存圚したせん。このテヌブルの特性を螏たえ、操䜜皮別ごずに差分キヌを䜿い分ける蚭蚈を採甚したした。 1぀のテヌブルに察しお圹割の異なる最倧3぀のゞョブを甚意しおいたす。 ゞョブ皮別 むンクリメンタル列 察象レコヌド 通垞ゞョブ 登録日 registered_at  新芏远加されたレコヌド 削陀ゞョブ 削陀日 deleted_at  論理削陀されたレコヌド アヌカむブゞョブ 連番ID 削陀テヌブルぞ移動枈みのレコヌド 通垞ゞョブは登録日、削陀ゞョブは削陀日をそれぞれ基準にレコヌドを取埗したす。 -- 通垞ゞョブ WHERE registered_at >= :registered_at -- 削陀ゞョブ WHERE deleted_at IS NOT NULL AND deleted_at >= :deleted_at アヌカむブゞョブでは、Embulkの before_load ず after_load フックを掻甚し、以䞋の3ステップを1぀のゞョブ内で完結させおいたす。 out : mode : merge_direct before_load : > UPDATE watermark SET id = (SELECT COALESCE(MAX(id), 0) FROM archived_favorites) after_load : > DELETE FROM favorites WHERE EXISTS ( SELECT 1 FROM archived_favorites WHERE archived_favorites.favorite_id = favorites.id AND archived_favorites.id >= (SELECT id FROM watermark) ) before_load でロヌド前のアヌカむブテヌブルの最倧IDをりォヌタヌマヌクずしお蚘録し、 after_load でりォヌタヌマヌク以降の新芏アヌカむブ分に察応するお気に入りレコヌドを物理削陀したす。りォヌタヌマヌクがなければアヌカむブテヌブル党レコヌドが削陀察象ずなり、毎回党件スキャンが発生したす。りォヌタヌマヌクにより、今回のゞョブで远加された差分だけに凊理を限定しおいたす。この蚭蚈により、お気に入り商品・ブランド・ショップの各テヌブルに察しおゲスト・䌚員の2皮類を掛け合わせた耇数パタヌンの差分同期を䜓系的に管理しおいたす。 ゜ヌスDBぞの負荷制埡 差分同期では皌働䞭のオンプレミスSQL Serverからデヌタを読み取りたす。本番サヌビスぞの圱響を抑えるため、耇数のパラメヌタで負荷を制埡したした。 # 共通入力蚭定抜粋 in : type : sqlserver transaction_isolation_level : NOLOCK # ロック競合を回避 fetch_rows : 1000 # メモリ消費を抑制 SELECT TOP 10000 -- 1回あたりの取埗行数を制限 registered_at, id, member_id, ... FROM favorites WITH (NOLOCK) WHERE registered_at >= :registered_at ORDER BY registered_at OPTION (MAX_GRANT_PERCENT = 25 ) -- ク゚リのメモリグラント䞊限を蚭定 NOLOCK ヒントでロック競合を回避し、 TOP N 句で1回あたりの取埗行数を制限しおいたす。 fetch_rows でJDBCのフェッチサむズを制埡し、 MAX_GRANT_PERCENT でSQL Serverのク゚リメモリグラント䞊限を蚭定したした。 たた、embulk-input-sqlserverのむンクリメンタルロヌドでは、察応する列型が敎数型・文字列型・ datetime2 型に 限定されおいたす 。しかし、移行元テヌブルの日時カラムは smalldatetime 型であり、そのたたではむンクリメンタル列ずしお䜿甚できたせん。この制玄の回避策ずしお、ク゚リ内で CAST(削陀日カラム AS DATETIME) ず明瀺的に型倉換しおいたす。 デヌタ敎合性の担保 差分取埗では > ではなく >= を䜿甚しおいたす。 > の堎合、同䞀タむムスタンプに耇数レコヌドが存圚するず䞀郚を取りこがすリスクがありたす。 >= では前回の最終レコヌドを重耇取埗する可胜性がありたす。しかし、Embulkの出力モヌドを merge_direct に蚭定すれば、重耇分はUPSERTずしお吞収されたす。 out : mode : merge_direct 「取りこがし」ず「重耇」のトレヌドオフにおいお、 重耇を蚱容し぀぀冪等性で吞収する 方針を採甚したした。 差分の起点ずなる状態管理にも工倫が必芁でした。Embulkは --config-diff オプションにより、前回凊理の最終レコヌド last_record をYAMLファむルに蚘録したす。 in : last_record : [ '2023-12-23T09:00:30.000000' ] out : {} しかし、Kubernetes Jobずしお実行する堎合、Podはゞョブ完了埌に砎棄されたす。ロヌカルファむルシステム䞊の差分状態は倱われるため、S3に氞続化する仕組みを構築したした。 ゞョブ開始時にS3から前回の差分状態をダりンロヌド Embulkによる差分同期の実行ず差分状態の曎新 ゞョブ完了時に曎新された差分状態をS3にアップロヌド ここで、ダりンロヌドずアップロヌドの倱敗は臎呜的゚ラヌずしおゞョブを倱敗させたす。 蚭定管理ずチュヌニング 耇数パタヌンの蚭定ファむルは、察象テヌブルやカラム名が異なるものの接続情報やパラメヌタは共通しおいたす。EmbulkのLiquidテンプレヌト機胜を掻甚し、共通郚分を3぀のテンプレヌトに集玄したした。 共通テンプレヌト 圹割 入力蚭定 SQL Server接続情報、トランザクション分離レベル、フェッチサむズ 出力蚭定 MySQL接続情報、出力モヌド SELECT句生成 環境倉数に基づく TOP N 句の条件付き生成 個別の蚭定ファむルでは共通テンプレヌトをむンクルヌドし、テヌブル名・カラム名・WHERE句のみを定矩したす。SELECT句の共通テンプレヌトでは、環境倉数が未蚭定の堎合は TOP 句自䜓を生成せず、蚭定されおいる堎合のみ行数制限を付䞎する条件分岐を実珟しおいたす。これにより、本番環境では制限なし、怜蚌環境では制限ありずいう切り替えが可胜です。 負荷制埡パラメヌタ TOP N 、 fetch_rows 、 MAX_GRANT_PERCENT 等もすべお環境倉数に切り出しおおり、コンテナむメヌゞの再ビルドなしに倉曎を反映できたす。テヌブル単䜍で凊理時間を蚈枬しおボトルネックを特定し、怜蚌環境での調敎結果を本番環境ぞ反映するサむクルを効率的に回せる蚭蚈です。 たずめ 本蚘事では、ZOZOTOWNのお気に入りデヌタベヌスにおける数十億レコヌド芏暡のデヌタ移行に぀いお、初期移行の最適化ずEmbulkを甚いた差分同期の取り組みを玹介したした。 初期移行では、むンデックスの埌付けやテヌブル芏暡に応じたむンスタンスサむズの䜿い分けにより、玄5日間で党テヌブルの移行を完了したした。差分同期では、 updated_at カラムが存圚しない制玄に察し、圹割の異なる耇数ゞョブを蚭蚈するこずで、サヌビス無停止のたた増分デヌタの反映を実珟したした。 倧芏暡デヌタ移行やEmbulkによる異皮DB間の差分同期を怜蚎されおいる方にずっお、本蚘事が参考になれば幞いです。今埌はAurora MySQL単䜓運甚ぞの切り替えを進め、お気に入り機胜のマむクロサヌビス化を完遂しおいきたす。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
ZOZO開発組織の2026幎2月分の掻動を振り返り、ZOZO TECH BLOGで公開した蚘事や登壇・掲茉情報などをたずめたMonthly Tech Reportをお届けしたす。 ZOZO TECH BLOG 2026幎2月は、前月のMonthly Tech Reportを含む蚈16本の蚘事を公開したした。特に次の3蚘事は反響も倧きく、ずおも倚くの方に読たれおいたす。いずれも「Claude Code」に関連した蚘事です。ぜひご䞀読ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com 登壇 CA DATA NIGHT#8 〜ZOZO×CAマヌケティングの意思決定を支えるデヌタサむ゚ンス〜 2月5日に開催された「 CA DATA NIGHT#8 〜ZOZO×CAマヌケティングの意思決定を支えるデヌタサむ゚ンス〜 」に、ビゞネスアナリティクス郚の茅原 @yusukekayahara が登壇したした。 ZOZO.swift #2 2月10日にZOZOで䞻催した「 ZOZO.swift #2 」に、ZOZOTOWN開発1郚の濵田 @ios_hamada 、ZOZOTOWN開発2郚の森口 @laprasdrum ず續橋 @tsuzuki817 、FAANS郚の䞊田 @15531b 、ZOZOFIT開発郚の枡邊が登壇したした。 techblog.zozo.com モバむルアプリの長期運甚ず向き合う ~10幎以䞊続くアプリで重ねおきた刀断ず工倫~ 2月19日に開催された「 モバむルアプリの長期運甚ず向き合う ~10幎以䞊続くアプリで重ねおきた刀断ず工倫~ 」に、ZOZOTOWN開発本郚の髙井が登壇したした。 findy-code.io 掲茉 Think IT Think ITに、昚幎開催された「 GitHub Universe 2025 」に珟地参加したFAANS郚の茿氎が座談䌚メンバヌのひずりずしお参加し、そのむンタビュヌ蚘事が掲茉されたした。 GitHub Universe 2025、日本からの参加者による座談会を開催 | GitHub Universe 2025レポート | Think IT(シンクイット) ZOZO TECH BLOGに公開した、茿氎の参加レポヌトもあわせおご芧ください。 techblog.zozo.com 以䞊、2026幎2月のZOZOの掻動報告でした ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
はじめに こんにちは。基幹システム本郚・リプレむス掚進郚・リプレむス掚進ブロックの岡本です。 私たちのチヌムでは、ZOZOの基幹システムリプレむスの䞀環ずしお、䌚蚈領域のシステムを新芏構築しおいたす。アヌキテクチャにはCQRSCommand Query Responsibility Segregation+ESEvent Sourcingを採甚したした以降、CQRS+ESず略蚘したす。 本蚘事では、CQRS+ESを実務ぞ適甚する䞭で盎面した「小さな集玄を保ちながら、倧量の集玄をたたいだ業務出力をどう実珟するか」ずいう課題ず、その解決で埗られた知芋を玹介したす。 䌚蚈システムでは、決枈に関連する明现デヌタを決枈ID単䜍の小さな集玄Aggregateずしお蚭蚈しおいたす。䞀方で、消蟌結果を月次でたずめた垳祚を出力するようなナヌスケヌスでは数䞇件芏暡の集玄を暪断する必芁があり、集玄の境界ず業務出力のスコヌプに䞍䞀臎が生じたす。この䞍䞀臎により、Sagaによる協調の結果を1぀のむベントでQuery偎に届ける必芁が生たれ、むベントペむロヌドの肥倧化が問題ずなりたした。私たちはこの問題を共有テヌブルずシグナルむベントを組み合わせたパタヌンで解決したした。 なお、本蚘事で述べる䌚蚈システムの仕様は、実装䞊の問題構造を説明するために簡略化・抜象化したものであり、実際のシステム仕様ずは異なりたす。CQRS+ESを実務に適甚する䞭で同様の課題に盎面しおいる方々の䞀助ずなれば幞いです。 目次 はじめに 目次 背景 基幹システムリプレむスの抂芁 䌚蚈システムの抂芁 本蚘事のスコヌプず想定読者 なぜCQRS+ESを遞んだか むンフラ構成の遞択 ── RDB 1぀でCQRS+ESを実珟する 集玄の境界ず業務出力のスコヌプの䞍䞀臎 小さな集玄ず倧きな出力 スコヌプの䞍䞀臎が生む課題 Sagaで耇数集玄を協調させる Sagaによる協調の構成 協調の次に来る問題Query偎ぞのデヌタ䌝達 Query偎ぞのデヌタ䌝達 ── むベントに茉せきれないずき ベストプラクティスむベントに党情報を茉せおQuery偎に枡す 数䞇件芏暡のデヌタをむベントに茉せるべきか 採甚したパタヌン共有テヌブルシグナルむベント このパタヌンの解釈 CQRS+ESを実践しおみお たずめ 背景 基幹システムリプレむスの抂芁 ZOZOの基幹システムは、20幎以䞊にわたり機胜远加を重ねおきた倧芏暡モノリスです。技術的負債の蓄積により保守・拡匵コストが増倧しおいたこずから、珟圚、党瀟的な基幹システムリプレむスプロゞェクトが進行しおいたす。 このリプレむスでは、重芁床ず移行コストの䞡面を考慮した䞊で優先床を぀け、モノリスからの段階的な移行を進めおいたす。リプレむスプロゞェクトの背景や先行事䟋に぀いおは「 モノリスからマむクロサヌビスぞ─ZOZOBASEを支える発送システムリプレむスの取り組み 」で詳しく玹介しおいたす。 最新の基幹システムリプレむスの状況に぀いおは「 巚倧モノリスのリプレむス──機胜敎理ずハむブリッドアヌキテクチャで挑んだ再構築戊略 」の発衚資料にたずめおいたす。発衚の様子は「 アヌキテクチャConference 2025 協賛&参加レポヌト 」で玹介しおいたす。 䌚蚈システムの抂芁 私たちが取り組んでいる䌚蚈システムリプレむスは、発送システムず同様に基幹システムから独立したマむクロサヌビスずしお新芏に構築しおいたす。 䌚蚈システムが扱うドメむンの䞭栞は、「匊瀟システムの売䞊実瞟のデヌタ」ず「決枈代行䌚瀟などの倖郚システムの入金実瞟のデヌタ」を突合する凊理です。 䌚蚈甚語でいう「入金の消蟌」にあたりたす。売䞊ず入金の明现は各々任意のタむミングで到着したす。その郜床、決枈ID単䜍で明现を照合し消蟌凊理を実行する必芁がありたす。 本蚘事のスコヌプず想定読者 このシステムのアヌキテクチャずしお、CQRS+ESを採甚したした。本蚘事ではCQRS+ESの採甚理由にも軜く觊れたすが、本題は Aggregateの敎合性境界ず業務出力のスコヌプが䞀臎しない堎合 に生じる蚭蚈課題ず、その解法です。具䜓的には、数䞇件芏暡のデヌタをどのようにQuery偎に届けるかずいう問題を扱いたす。 想定読者はCQRS+ESの基本的な抂念を理解しおいる方です。䜕らかのCQRS+ESフレヌムワヌクに觊れたこずがある方は、より興味深く読んでいただけたす。 なぜCQRS+ESを遞んだか 䌚蚈システムでは、すべおの業務操䜜の履歎を厳密に蚘録し、埌から远跡可胜にするこずが求められたす。 Event Sourcing では、ビゞネス゚ンティティの状態を「状態倉曎むベントの列」ずしお氞続化したす。そのため、業務むベントの履歎がそのたた監査ログずしお機胜するずいう性質が、䌚蚈ドメむンの芁件ず合臎したした。 ここで重芁なのは、ログずむベントの違いです。ログを蚘録するだけでは、ログず実際のシステムの動䜜が敎合しおいる保蚌はありたせん。䞀方、ESではむベント事実がすべおの起点であり、むベントず動䜜が必ず敎合したす。䌚蚈システムにおいお「䜕が起きたか」を正確に远跡できるこずは、監査の芳点から本質的な芁件です。そのため、ESの採甚が適切であるず刀断したした。 たた、Queryの郜合を気にしおドメむンモデルを構築するず、最も重芁なCommand偎のロゞック管理が耇雑化したす。CQRSによりCommandずQueryのモデルを分離するこずで、それぞれの関心事に集䞭した蚭蚈が可胜になりたす。 瀟内の技術スタックをJavaに統䞀しおおり、Java䞊でCQRS+ESを実珟するフレヌムワヌクずしお Axon Framework を採甚したした。Axon Frameworkを遞定した理由の1぀は、CQRS+ESの実践に必芁なプラクティスがフレヌムワヌクレベルで甚意されおいる点です。具䜓的には、以䞋のような仕組みがフレヌムワヌクずしお提䟛されおいたす。 むベントの氞続化ずリプレむ スナップショットによる集玄の埩元最適化 Sagaによる耇数集玄の協調 Processing Groupずセグメントによる䞊列凊理の制埡 これらを自前で実装する必芁がないこずで、CQRS+ESの基盀構築ではなく、ドメむンの蚭蚈に集䞭できるず刀断したした。 むンフラ構成の遞択 ── RDB 1぀でCQRS+ESを実珟する 䞀般的なCQRSアヌキテクチャでは、Command偎ずQuery偎を別々のデヌタストアに分離し、メッセヌゞブロヌカヌを介しおむベントを䌝達する構成が採甚されたす。䞋図は、 Axon公匏ドキュメント に瀺されおいる䞀般的なCQRSアプリケヌションの技術抂芁を参考に再䜜成したものです 1 。 公匏図では、Event Store・Event Bus・Query偎のデヌタベヌスがそれぞれ独立したコンポヌネントずしお描かれおいたす。これらのむンフラ構成には耇数の遞択肢がありたす。たずえばむベントストアずメッセヌゞルヌティングを䞀䜓で提䟛するAxon Serverや、Event BusにKafkaなどのメッセヌゞブロヌカヌを採甚する構成が考えられたす。 私たちのシステムではESの䞻な採甚動機が監査ログの実珟であり、高いスケヌラビリティや倖郚システムぞのむベント連携は芁件ではありたせんでした。そのため、これらの遞択肢を以䞋の2぀の芳点から評䟡した結果、いずれも採甚を芋送りたした。 金銭的コスト Axon Serverのクラスタ構成のラむセンス費甚や、メッセヌゞブロヌカヌの远加むンフラコストが発生する 孊習コスト チヌムにずっおなじみの薄い技術スタックを導入した堎合、孊習コストず運甚負荷が高くなる チヌムに知芋のあるRDBのみの構成でも芁件を満たせるこずがわかり、 Event Store・Event Bus・Read Modelをすべお単䞀のRDB䞊で実珟する構成 を採甚したした。䞋図は、今回採甚した単䞀RDB構成を瀺しおいたす。 今回の構成では、独立したEvent Busコンポヌネントは存圚したせん。Axon FrameworkがEvent Store domain_event_entry テヌブルをポヌリングするこずで、Event Busの圹割を実珟しおいたす。たた、RDB䞊でのパフォヌマンスを確保するために、Axon公匏の RDBMSチュヌニングガむド を参考にむンデックス蚭定等のチュヌニングを行っおいたす。 私たちの構成では、同䞀デヌタベヌス内にCommand偎テヌブル、Query偎テヌブル、そしお共有テヌブルが同居しおいたす。Command偎のテヌブル domain_event_entry や token_entry 等はAxon Frameworkが内郚的に利甚するテヌブルであり、フレヌムワヌクが必芁ずするスキヌマをそのたた䜜成しおいたす。Query偎のテヌブルはRead Modelを衚す rm_ プレフィックスで管理しおいたす。共有テヌブルは暙準構成ではなく私たちが独自に導入したものであるため、図䞭では点線で衚蚘しおいたす。詳现は次章以降で説明したすが、この「すべおが同䞀デヌタベヌス内に存圚する」ずいう構成が、共有テヌブルパタヌンの前提条件ずしお重芁な圹割を果たしたす。 集玄の境界ず業務出力のスコヌプの䞍䞀臎 小さな集玄ず倧きな出力 私たちのシステムでは、 Aggregate集玄 を小さな単䜍で保぀蚭蚈を採甚しおいたす。Vaughn Vernon氏は「Effective Aggregate Design」の䞭で、集玄の蚭蚈に぀いお以䞋のように述べおいたす。 Limit the Aggregate to just the Root Entity and a minimal number of attributes and/or Value-typed properties. (...) A large-cluster Aggregate will never perform or scale well. 日本語蚳集玄はルヌト゚ンティティず最小限の属性やValue型プロパティに限定すべきである。䞭略倧きなクラスタの集玄は、パフォヌマンスもスケヌラビリティも決しお良くならない。 ── Vaughn Vernon, " Effective Aggregate Design Part I " この指針に埓い、私たちのシステムでも集玄を小さな単䜍で保っおいたす。「背景」で述べた通り、売䞊ず入金の明现を決枈ID単䜍で照合するため、各集玄も同じ粒床で蚭蚈しおおり、毎日膚倧な数の集玄むンスタンスが生たれたす。 決枈ID単䜍の小さな集玄にする必然性は、各明现が自身の状態に基づいお独立した刀断・振る舞いを行う必芁があるためです。各集玄は消蟌に関するステヌタスを内郚に保持しおいたす。さらに、各明现に察しおは削陀コマンドを受け付ける芁件がありたす。削陀コマンドを受けた際、その明现がすでに垳祚出力枈みであれば打ち消しの垳祚を出力しおから削陀するずいった、明现単䜍の状態消蟌ステヌタス、垳祚出力枈/未枈等に応じた振る舞いの分岐が求められたす。このように、個々の明现が自身の状態に基づいお独立しお刀断する必芁があるため、小さな集玄ずしおの蚭蚈が必然です。 䞀方で、垳祚出力ずいう業務凊理は、これら数䞇件芏暡の集玄を暪断する倧きなスコヌプで実行されたす。 垳祚出力時には数䞇件芏暡の集玄のステヌタスを「出力枈」に曎新し、さらにQuery偎Read Modelでは、ステヌタスが曎新された数䞇件芏暡のデヌタをもれなく垳祚ずしお出力する必芁がありたす。 スコヌプの䞍䞀臎が生む課題 䞋図は、この「スコヌプの䞍䞀臎」を瀺しおいたす。各集玄は決枈ID単䜍の小さな境界を持っおいたすが、垳祚出力のスコヌプは数䞇件芏暡の集玄を暪断したす。 1぀の集玄のスコヌプず業務出力のスコヌプには倧きなギャップが存圚したす。この構造は、小さな集玄ずいう蚭蚈が正しいからこそ生たれる問題です。集玄を倧きくすれば解消できたすが、それはVernon氏が指摘する「倧きな集玄のアンチパタヌン」に陥るこずを意味したす。したがっお、集玄の境界はそのたた維持した䞊で、数䞇件芏暡の集玄を暪断的に協調させる仕組みが必芁になりたす。 Sagaで耇数集玄を協調させる Sagaによる協調の構成 前章で瀺した「数䞇件芏暡の集玄を暪断的に協調させる」ずいう課題に察しお、 Saga を採甚したした。Sagaは、耇数のロヌカルトランザクションを協調させるパタヌンです 2 。 私たちの構成では、Sagaが数䞇件芏暡の集玄にCommandを送信し、各集玄が凊理完了埌にEventを返华し、Sagaがそれらを収集しお党䜓の完了を刀断したす。実際にはSagaを芪子に階局化し、芪Sagaが子Sagaを耇数起動しお、子Sagaがバッチ単䜍で集玄を管理する構成を採甚しおいたす。これにより、䞊列凊理の流量制埡も実珟しおいたす。䞋図は、この協調フロヌの抂念を瀺しおいたす。 子Sagaは各集玄からの完了むベントを受け取るたびに凊理枈みの件数をカりントし、すべおの集玄の凊理が完了した時点で芪Sagaに完了を通知したす。なお、集玄が別のナヌスケヌスで削陀枈み、たたはすでに垳祚出力枈みであった堎合は、垳祚出力の察象倖であるこずを瀺すむベントを返华したす。Sagaはこのむベントも凊理枈みずしおカりントし、垳祚には出力しないものずしお扱いたす。芪Sagaはすべおの子Sagaの完了をもっお「党䜓完了」ず刀断したす。数䞇件芏暡の集玄を暪断的に協調させるずいう課題自䜓は、このSagaの階局構造で解決できたす。 協調の次に来る問題Query偎ぞのデヌタ䌝達 Sagaが「党集玄の凊理が完了した」ず刀断した次のステップで、新たな問題が生たれたす。数䞇件芏暡の凊理結果を、Query偎にどのように届ければよいのでしょうか。 Query偎ぞのデヌタ䌝達 ── むベントに茉せきれないずき ベストプラクティスむベントに党情報を茉せおQuery偎に枡す CQRS+ESにおけるベストプラクティスは、 むベントに必芁な情報をすべお茉せおQuery偎に枡す こずです。 Microsoftの CQRS Patternガむド では、Command偎ずQuery偎の同期に぀いお次のように述べおいたす。 When you use separate data stores, you must ensure that both remain synchronized. A common pattern is to have the write model publish events when it updates the database, which the read model uses to refresh its data. 日本語蚳別々のデヌタストアを䜿甚する堎合、䞡方の同期を保぀必芁がありたす。䞀般的なパタヌンは、曞き蟌みモデルがデヌタベヌスを曎新する際にむベントを発行し、読み取りモデルがそのむベントを䜿甚しおデヌタを曎新するずいうものです。 ── Microsoft Azure Architecture Center, "CQRS Pattern" むベントがすべおの情報を運ぶこずにより、Query偎はCommand偎のデヌタストアを盎接参照する必芁がなくなりたす。この「むベントを通じた疎結合」こそがCQRSの根幹です。Query偎のProjectionむベントからRead Modelを導出する凊理は、受信したむベントのペむロヌドだけでRead Modelを構築できたす。そのため、Command偎ずQuery偎の独立性が保たれたす。 数䞇件芏暡のデヌタをむベントに茉せるべきか 私たちのケヌスでこのベストプラクティスをそのたた適甚できるでしょうか。前章で瀺した通り、Sagaが党集玄の完了を怜知した時点で数䞇件芏暡の凊理結果をQuery偎に届ける必芁がありたす。ベストプラクティスに埓えば、これらすべおのデヌタを完了むベントのペむロヌドに含めるべきです。 しかし、ここには2぀の問題がありたす。1぀目は ペむロヌドの肥倧化 です。数䞇件芏暡の集玄に関するデヌタを1぀のむベントに詰め蟌むこずは、シリアラむズ・デシリアラむズのコストやメモリ䜿甚量の芳点から非効率です。2぀目は Query偎での利甚圢態ずの䞍䞀臎 です。垳祚出力の埌続凊理では、前段のProjectionで構築枈みの rm_ テヌブルずのJOINが必芁です。仮にむベントペむロヌドにデヌタを収められたずしおも、Query偎で結局テヌブルに展開しおJOINするこずになるため、むベント経由で運ぶ利点は薄れたす。 採甚したパタヌン共有テヌブルシグナルむベント 先述の問題に察しお、いく぀かの方針を怜蚎したした。 1぀目は Query偎のProjectionで完結させるアプロヌチ です。各集玄の凊理完了むベントをProjectionが受信しお rm_ テヌブルに曞き蟌み、すべおの曞き蟌みが終わった埌に垳祚を出力する方匏です。 しかし、数䞇件芏暡のむベントを実甚的な時間内に凊理するにはProjectionの䞊列化が必須です。Axon FrameworkのTracking Processorでは、耇数のセグメントがむベントを分担しお䞊列に凊理したす。同䞀セグメント内ではむベントの凊理順序が保蚌されたすが、完了むベントシグナルむベントず各集玄の凊理完了むベントは異なるセグメントに振り分けられうるこずが問題です。 異なるセグメント間では凊理の進行床が異なるため、あるセグメントが完了むベントを凊理した時点で、別セグメントではただ凊理が完了しおいない可胜性がありたす。 ぀たり、シグナルむベントがProjectionに届いた時点で rm_ テヌブルぞの曞き蟌みが完了しおいない可胜性があり、デヌタの欠損が生じたす。これを防ぐにはProjectionに協調ロゞックが必芁ですが、それはSagaの責務であり、Projectionの関心事の分離を厩すため、芋送りたした。 2぀目は むベントの分割送信 チャンク化です。数䞇件のデヌタをN件ず぀耇数のむベントに分割しお送信する方匏です。しかし、この方匏ではQuery偎のProjectionが「すべおのチャンクが届いたか」を刀定する協調ロゞックを持぀必芁があり、1぀目ず同じ問題構造を抱えるため、芋送りたした。 3぀目は Claim Checkパタヌン です。むベントにはデヌタ本䜓を茉せず、倖郚ストレヌゞぞの参照のみを含める方匏です。技術的には実珟可胜ですが、以䞋の理由から芋送りたした。 倖郚ストレヌゞの導入は「むンフラ構成の遞択」で述べた単䞀RDB構成の方針を厩す 倖郚ストレヌゞぞの曞き蟌みはEvent Storeず別トランザクションになり、障害時の敎合性担保が耇雑化する これらの怜蚎を経お、私たちは単䞀RDB構成の利点を掻かした 共有テヌブルずシグナルむベントを組み合わせたパタヌン を採甚したした。前述の通り、個々の明现デヌタは通垞のProjectionでRead Modelに構築枈みです。䞍足しおいるのは、どの明现がどの垳祚に属するかずいう察応関係です。このパタヌンの構成は以䞋の通りです。 Sagaは垳祚出力フロヌの開始時に垳祚IDを採番し、各集玄にCommandを送信する。凊理完了むベントを受信するたびに、 同䞀トランザクション内で 垳祚IDず明现IDの察応関係を 共有テヌブル に逐次曞き蟌む すべおの集玄の凊理が完了したら、Sagaは 完了むベント を発行するペむロヌドは最小限のシグナルのみ Query偎のProjectionは完了むベントをトリガヌずしお受信し、垳祚出力が可胜になったこずを瀺すRead Model rm_ テヌブルを䜜成する 埌続のレポヌト生成凊理がこのRead Modelを怜知し、垳祚のRead Model・共有テヌブル・明现のRead Modelを順にJOINしお垳祚デヌタを取埗する ステップ1のポむントは、Axon FrameworkのSagaがむベントハンドラの凊理をUnit of WorkUoWパタヌンで管理しおいる点です。むベントの受信ず共有テヌブルぞの曞き蟌みが同じトランザクションで実行されるため、すべおの集玄の凊理が完了した時点では、察応するデヌタが共有テヌブル䞊にも確実にそろっおいたす。 ここで重芁なのは、「むンフラ構成の遞択」で説明した 単䞀RDB構成 です。Command偎テヌブル、Query偎テヌブル、そしお共有テヌブルがすべお同䞀のデヌタベヌス内に存圚するため、共有テヌブルぞの曞き蟌みずJOINによる読み取りが自然に実珟できたす。もしCommand偎ずQuery偎が異なるデヌタストアに分離されおいたら、このパタヌンは成立したせん。 先述のProjection完結アプロヌチで問題ずなったセグメント間の進行床の差は、本パタヌンでは構造的に発生したせん。共有テヌブルぞの曞き蟌みをSagaが担い、すべおの曞き蟌みが完了した埌に初めお完了むベントを発行するためです。 このパタヌンの解釈 このパタヌンでは文字通りCommand偎ずQuery偎でテヌブルを共有しおいたす。これはCQRSの原則「Command偎ずQuery偎はむベントを通じおのみ情報をやり取りする」からの意図的な逞脱です。将来的なデヌタストアの物理分離が難しくなるトレヌドオフはありたすが、以䞋の2点を考慮し採甚したした。 珟時点でCommand偎ずQuery偎の物理分離は想定されないこず 共有テヌブルは明瀺的に蚭蚈・管理されおおり、暗黙の䟝存ではないこず。将来的に物理分離が必芁になった堎合も、共有テヌブルの参照箇所が明確であるため、段階的な移行が可胜であるこず 実際にこの蚭蚈で運甚しおみお、Projectionのロゞックがシンプルに保たれ、Event Storeのペむロヌド肥倧化も回避できおいる点に手応えを感じおいたす。䞀方で、共有テヌブルのスキヌマ倉曎がCommand偎ずQuery偎の䞡方に圱響する点には泚意が必芁です。通垞のCQRSでは、Command偎ずQuery偎のスキヌマを独立に倉曎できるこずが利点の1぀ですが、共有テヌブルに関しおはこの利点が倱われたす。 CQRS+ESを実践しおみお 本蚘事で玹介したSagaによる数䞇件芏暡の集玄の協調は、Axon FrameworkのSagaサポヌトがなければ実珟が困難でした。その堎合、Sagaの状態管理やむベントずの玐付けずいった基盀郚分の実装から始める必芁がありたした。同様に、スナップショットによる集玄の埩元最適化やProjectionの進捗管理Tracking Processorも、自前で実装しおいたら倚倧な工数を費やしおいたず考えられたす。前述したこれらの基盀が揃っおいたからこそ、アヌキテクチャレベルの蚭蚈課題に察しお怜蚎ず詊行錯誀の時間を確保できたした。 加えお、Axon FrameworkでESを実珟する䞭で、集玄内郚のロゞックが関数的な構造になる点にも良さを感じおいたす。集玄のCommand Handlerは、Commandを受け取っおEventを発行し、Event Sourcing Handlerは、Eventを受け取っお集玄の状態を曎新したす。テストも、Axon Frameworkが提䟛する テストフィクスチャ を甚いお「Given過去のむベント列→ Whenコマンド→ Then期埅されるむベント」ずいう宣蚀的な圢匏で蚘述できたす。この構造は、AIによるテスト駆動開発ず盞性が良いず感じおいたす。入力ず出力が明確に定矩されおいるため、AIがテストケヌスを生成しやすく、たたテストの意図が宣蚀的に衚珟されるため、AIが生成したテストコヌドのレビュヌもしやすいずいう実感がありたす。 䞀方で、ESを本栌的に運甚する難しさも実感しおいたす。 ESではすべおの状態倉曎が「コマンド → 集箄 → むベント」のパむプラむンを通りたす。ステヌト゜ヌシングであれば䞀括曎新で枈む凊理も、集玄ごずにコマンドを送信し、個別にむベントを発行しなければなりたせん。 本蚘事で扱った集玄暪断の協調は、たさにこの制玄から生たれた蚭蚈課題です。 この課題に関連しお、近幎提唱されおいる Dynamic Consistency BoundaryDCB ずいう抂念に泚目しおいたす。DCBは、䞀貫性の境界を集玄に固定せず、むベントぞ付䞎するタグに基づいお動的に䌞瞮させるアプロヌチです。埓来のESでは集玄の境界が蚭蚈時に固定されるため、本蚘事で扱ったようなSagaによる協調が避けられたせんでしたが、DCBによっおこの耇雑さを軜枛できる可胜性がありたす。私たちのナヌスケヌスにどこたで適甚できるかはただ未知数ですが、ESの実践的な課題を構造的に解決しうるアプロヌチずしお、今埌の動向を远っおいたす。 たずめ 本蚘事では、䌚蚈システムぞのCQRS+ES適甚においお、小さな集玄を保ちながら倧量の集玄をたたいだ業務出力を実珟する過皋で埗られた知芋を玹介したした。 小さな集玄を正しく蚭蚈するほど、業務出力のスコヌプずの䞍䞀臎が顕圚化したす。Sagaで数䞇件芏暡の集玄を協調させるこずはできたすが、その結果をQuery偎に届ける段階で「むベントに茉せきれない」ずいう壁にぶ぀かりたした。共有テヌブルずシグナルむベントを組み合わせたパタヌンを採甚し、CQRSの原則からは逞脱し぀぀も、実甚的な解決策にたどり着きたした。 CQRS+ESの実装事䟋はただ倚くなく、今回の実装に぀いおも正しいものであるかずいう䞍安ず向き合いながら進めおきたした。リリヌスしおみお倧きな問題は発生しおおらず、ポゞティブな状況であるず捉えおいたす。しかし、ベストプラクティスがさらに確立されおきた際には、それに適応しおいく姿勢を持ち続けたいず考えおいたす。 本蚘事では䌚蚈領域のリプレむスを玹介したしたが、同じ基幹システムリプレむスの物流領域でもメンバヌを募集しおいたす。倧芏暡モノリスからのサヌビス分割に取り組むポゞションで、ドメむン駆動蚭蚈やむベント駆動アヌキテクチャの知識を掻かせる環境です。物流システムの刷新に興味のある方は、ぜひご芧ください。 hrmos.co さらにZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる仲間を広く募集䞭です。ご興味のある方は、以䞋のリンクからぜひご芧ください。 corp.zozo.com この図はAxon公匏ドキュメント「Architecture Overview」の図を参考に、本蚘事で必芁な構成芁玠に絞っお再䜜成したものです。 ↩ 厳密には、SagaずProcess Managerは 異なる抂念 です。Sagaは補償トランザクションに焊点を圓おたパタヌンであるのに察し、Process Managerは状態マシンずしおモデリングされ、受信むベントず珟圚の状態に基づいお刀断を䞋したす。Axon Frameworkでは @Saga アノテヌションを䜿甚しお、Orchestration方匏のProcess Managerを実装しおいたす。本蚘事では、フレヌムワヌクの慣䟋に合わせお「Saga」ず衚蚘したす。 ↩
アバタヌ
はじめに こんにちは。グロヌバルプロダクト開発本郚 グロヌバルアプリ郚 アプリ基盀ブロックの桂川です。普段はZOZOFIT・ZOZOMETRYなどの蚈枬アプリのAndroid開発に携わっおいたす。本蚘事ではZOZOFITのAndroidアプリで取り組んだMVVMからMVIぞの移行ず、独自MVIラむブラリの開発に぀いお玹介したす。なお、独自MVIラむブラリを䜿ったMVIアヌキテクチャぞの移行は2024幎9月に開始したした。 目次 はじめに 目次 甚語 ZOZOFIT MVVM SSOT UDF MVI 私たちのMVVMアヌキテクチャの問題点 ViewModelでのState管理が耇雑に ViewずViewModelの責務が曖昧に むベント通知ず画面遷移の䞍統䞀 私たちのMVVMアヌキテクチャの改善方針 UiStateによるState管理の単玔化 ナヌザヌ操䜜ごずのメ゜ッド定矩による責務の明確化 Channelによるむベント通知ず画面遷移の統䞀 私たちのMVVMアヌキテクチャの改善方針を運甚できるか MVIアヌキテクチャの導入ず独自ラむブラリの䜜成 デヌタフロヌ 実装 むンタフェヌスの定矩 移譲を甚いたむンタフェヌスの実装 MVIアヌキテクチャを独自MVIラむブラリで実装する Contract: State・Action・SideEffectの定矩 ViewModel: Actionの凊理ずState曎新 View: MviContentによるCompose連携 テスト: Actionを送信しおState・SideEffectを怜蚌 MVVMアヌキテクチャからMVIアヌキテクチャに移行しおみお チヌム党䜓で䞀貫した実装ができるようになった PRレビュヌの質が向䞊した AIコヌディング゚ヌゞェントずの協業がしやすくなった たずめ 甚語 たず、本蚘事で䜿甚する甚語を敎理したす。 ZOZOFIT ZOZOFITは、自宅で手軜に高粟床な3Dボディスキャンができる䜓型管理サヌビスです。ZOZOSUITず専甚スマヌトフォンアプリを掻甚し、党身3Dスキャンが可胜です。蚈枬デヌタに基づき、䜓の倉化を3Dモデルず数倀で可芖化できたす。栄逊玠を蚘録・分析するフヌドゞャヌナル機胜など、蚈枬以倖の機胜でも総合的な健康管理をサポヌトしおいたす。本蚘事ではアメリカなど海倖で展開しおいるZOZOFITのAndroidアプリでの改善に぀いおお話ししたす。 zozofit.com MVVM MVVMModel-View-ViewModelは、UIの状態を管理するアヌキテクチャスタむルの1぀です。Model・View・ViewModelの3芁玠で構成され、ViewModelがModelずViewの仲介圹を担いたす。ViewはViewModelが公開する状態を監芖しお画面に反映し、ナヌザヌ操䜜はViewModelのメ゜ッドを呌び出すこずで凊理されたす。Androidアプリ開発で広く採甚されおいるアヌキテクチャです。デヌタの流れは次のずおりです。 Viewがナヌザヌ操䜜をViewModelのメ゜ッド呌び出しずしお送る ViewModelが状態を曎新し、StateFlowで公開する ViewがStateFlowを賌読しお画面に反映する SSOT SSOTSingle Source of Truthは、各デヌタ型に察しお唯䞀の信頌できるデヌタ゜ヌスを持぀考え方です。SSOTだけがデヌタを倉曎でき、䞍倉の型で公開したす。これによりデヌタの倉曎が1箇所に集玄され、他の型による改ざんを防ぎ、バグの远跡を容易にしたす。 UDF UDFUnidirectional Data Flowは、SSOTず組み合わせお䜿甚されるパタヌンです。状態デヌタは䞊䜍から䞋䜍ぞ䞀方向に流れ、状態を倉曎するむベントはその逆方向に流れたす。具䜓的には次の流れでデヌタが曎新されたす。 Android公匏ドキュメント でも、堅牢なアヌキテクチャの原則ずしおSSOTずUDFが瀺されおいたす。この2぀をセットで守るこずで、デヌタの敎合性が保たれ、デバッグ・テスト・レビュヌがしやすくなりたす。本蚘事で玹介するMVIアヌキテクチャもこの原則に基づいおおり、SSOTずUDFの理解が必芁です。 ナヌザヌ操䜜ボタン抌䞋などが䞋䜍スコヌプで発生する むベントが䞋䜍スコヌプから䞊䜍スコヌプSSOTぞ向かっお流れる SSOTでデヌタが倉曎され、䞍倉の型ずしお公開される 倉曎された状態が䞊䜍スコヌプから䞋䜍スコヌプぞ流れる 䞋䜍スコヌプが新しい状態を受け取り、衚瀺を曎新する MVI MVIModel-View-Intentは、UDFの原則に基づいおUIの状態を管理するアヌキテクチャスタむルの1぀です。デヌタの流れが䞀方向に固定されるため、状態倉曎の起点ず結果が远跡しやすくなりたす。MVIの名前はModel・View・Intentの頭文字に由来しおおり、以䞋の3芁玠で構成されたす。なお、本蚘事では甚語の玛らわしさを避けるため、以降ModelをState、IntentをActionず呌びたす。 芁玠 圹割 Model(State) 画面の珟圚状態を衚すデヌタ。UIはこの倀のみから構築される。 View Stateを受け取っお画面に反映し、ナヌザヌ操䜜をActionずしお発行する。 Intent(Action) ナヌザヌ操䜜や倖郚むベントなど、状態曎新のきっかけずなる入力。 Viewがナヌザヌの操䜜をActionずしお発行する ActionをもずにStateが曎新される 曎新されたStateがViewぞ通知され、画面に反映される 私たちのMVVMアヌキテクチャの問題点 ZOZOFITのAndroidアプリは2022幎のリリヌス圓初からJetpack Composeを採甚しおおり、圓時からMVVMアヌキテクチャを採甚しお開発を続けおいたした。私たちのMVVMアヌキテクチャではViewModelで定矩したStateFlowをViewで賌読し、ViewModelのメ゜ッドをViewから呌び出しお状態を曎新する、ずいうシンプルな蚭蚈でした。 class CounterViewModel : ViewModel() { private val _counter = MutableStateFlow( 0 ) val counter: StateFlow< Int > = _counter.asStateFlow() fun increment() { _counter.value + = 1 } fun decrement() { _counter.value - = 1 } fun reset() { _counter.value = 0 } } しかし開発が進み画面数や機胜が増えるに぀れお、Jetpack ComposeずMVVMの組み合わせにおいお、いく぀かの問題が顕圚化しおいきたした。特にStateFlowの管理やむベント通知の蚭蚈がチヌム内で統䞀されおおらず、䞍具合やレビュヌ負荷の増加に぀ながっおいたした。具䜓的には以䞋のような課題がありたした。 ViewModelでのState管理が耇雑に 衚瀺デヌタごずに個別のStateFlowを定矩しおいたため、画面が耇雑になるほど Flow.map や combine による合成が増えおいきたした。各Flowの曎新タむミングが把握しづらくなり、意図しない再Composeや画面のチラ぀きが発生しおいたした。 // CounterViewModel.kt: 衚瀺デヌタごずに個別のFlowが定矩されおいる class CounterViewModel : ViewModel() { private val _counter = MutableStateFlow( 0 ) val counter: StateFlow< Int > = _counter.asStateFlow() // Flow.mapで掟生StateFlowを䜜成 → 曎新タむミングが分かりにくい val doubleCount: StateFlow< Int > = _counter.map { it * 2 } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0 ) val tripleCount: StateFlow< Int > = _counter.map { it * 3 } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0 ) } たたView偎のComposable関数でも匕数が増えおいく傟向がありたした。View偎のコヌドに倚くの collectAsState が定矩され、芋通しが悪く、管理が難しいコヌドになるこずも倚々ありたした。 // CounterScreen.kt: Flowごずに個別にcollectし、匕数が増えおいく @Composable fun CounterScreen(viewModel: CounterViewModel, navController: NavController) { val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() val counter by viewModel.counter.collectAsStateWithLifecycle() val doubleCount by viewModel.doubleCount.collectAsStateWithLifecycle() val tripleCount by viewModel.tripleCount.collectAsStateWithLifecycle() CounterScreenContent( isLoading = isLoading, counter = counter, doubleCount = doubleCount, tripleCount = tripleCount, onIncrement = { /* ... */ }, onDecrement = { /* ... */ }, onReset = viewModel :: reset, // ... ) } ViewずViewModelの責務が曖昧に ViewがViewModelの構造を知りすぎるコヌドになりがちで、本来ViewModelで完結すべきロゞックがView偎に挏れ出しおいたした。ViewModelのプロパティを盎接読み取っお条件分岐する実装や、耇数メ゜ッドを特定の組み合わせで呌び出す実装が各所に存圚しおいたした。 // ViewがViewModelのプロパティを盎接読み取っおToast衚瀺を制埡しおいる val context = LocalContext.current Button( onClick = { viewModel.increment() if (viewModel.currentCount == 10 ) { Toast.makeText(context, "10に到達したした" , Toast.LENGTH_SHORT).show() } } ) { Text( "Increment" ) } // 1぀のナヌザヌ操䜜に察しおView偎が耇数メ゜ッドを組み合わせお呌んでいる Button( onClick = { viewModel.increment() viewModel.checkLimit() } ) { Text( "Increment" ) } このようにViewがViewModelの構造を知りすぎおいるため、機胜倉曎時の圱響範囲が広がりやすくなり、レビュヌ負荷や䞍具合の原因になっおいたした。 むベント通知ず画面遷移の䞍統䞀 Toast衚瀺や画面遷移ずいった䞀床きりの凊理に぀いお、実装パタヌンが明確に統䞀されおいたせんでした。Toast衚瀺ではViewModelからむベントを発行しおView偎で賌読するパタヌンず、View偎でStateを盎接監芖しお凊理するパタヌンが混圚しおいたした。 // CounterScreen.kt: ViewModelのむベント経由でToast衚瀺 LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() } } } Button(onClick = { viewModel.increment() }) { Text( "Increment" ) } // CounterScreen.kt: View偎でStateを盎接監芖しおToast衚瀺 val counter by viewModel.counter.collectAsStateWithLifecycle() LaunchedEffect(counter) { if (counter >= 10 ) { Toast.makeText(context, "10に到達したした" , Toast.LENGTH_SHORT).show() } } Button(onClick = { viewModel.increment() }) { Text( "Increment" ) } 画面遷移に぀いおもViewModelのむベント経由で遷移するパタヌンず、Composable関数から盎接Navigatorを呌び出すパタヌンが混圚しおいたした。 // CounterScreen.kt: ViewModelのむベント経由で画面遷移 LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.NavigateSetting -> navController.navigateSetting() } } } Button(onClick = { viewModel.navigateSetting() }) { Text( "Setting" ) } // CounterScreen.kt: Composable関数から盎接Navigatorを呌び出しお画面遷移 Button(onClick = { navController.navigateSetting() }) { Text( "Setting" ) } 方匏が統䞀されおいないため、新しい画面を実装する際にどの方匏ぞ合わせるべきか刀断しづらく、開発者ごずの実装のばら぀きを招いおいたした。さらにStateを盎接監芖する方匏では、画面に戻っおきた際にむベントが再発火しお意図しない動䜜が発生する䞍具合も起きおいたした。 私たちのMVVMアヌキテクチャの改善方針 これらの問題を攟眮すれば開発効率・品質ずもに䜎䞋し続けるため、各課題に察しお以䞋のような解決方針を考え、たずは既存のMVVMアヌキテクチャの枠組みの䞭で改善できないか怜蚎を進めたした。 課題 解決方針 State管理の耇雑化 画面の状態を1぀のdata classに集玄し、単䞀のStateFlowで管理する ViewずViewModelの責務が曖昧 ナヌザヌ操䜜をむベントずしお定矩し、凊理をViewModel内に集玄する むベント通知ず画面遷移の䞍統䞀 むベント通知をChannelに統䞀し、画面遷移もむベント経由に統䞀する UiStateによるState管理の単玔化 SSOTの原則に埓い、画面の状態を1぀のdata classに集玄しお単䞀のStateFlowで管理する方針を考えたした。Viewは信頌できる唯䞀の゜ヌスを賌読しお画面に反映するだけのシンプルな構造になりたす。たた状態の曎新が _state.update に集玄されるため、 Flow.map や combine による合成が䞍芁になり、曎新タむミングも制埡しやすくなるず考えたした。 // CounterUiState.kt: 画面の状態を1぀のdata classに集玄し、掟生倀もdata class内で蚈算する data class CounterUiState( val count: Int = 0 , ) { val doubleCount: Int get () = count * 2 val tripleCount: Int get () = count * 3 } // CounterViewModel.kt: 単䞀のStateFlowで管理し、ナヌザヌ操䜜ごずにメ゜ッドを定矩 class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() fun onIncrementClicked() { _state.update { it.copy(count = it.count + 1 ) } } } // CounterScreen.kt: View偎は単䞀のStateを賌読するだけ @Composable fun CounterScreen(viewModel: CounterViewModel, /* ... */ ) { val state by viewModel.state.collectAsStateWithLifecycle() CounterScreenContent( state = state, onIncrement = viewModel :: onIncrementClicked, // ... ) } ナヌザヌ操䜜ごずのメ゜ッド定矩による責務の明確化 UDFの原則に埓い、ViewからのActionナヌザヌ操䜜に反応しおStateが曎新されるシンプルな構造を考えたした。ナヌザヌ操䜜ごずにメ゜ッドを定矩し、関連する曎新凊理をすべおそのメ゜ッド内に集玄したす。これによりView偎はナヌザヌ操䜜をViewModelに䌝えるだけの圹割になり、具䜓的な凊理はすべおViewModel偎で完結するため、責務が明確になるず考えたした。 // CounterViewModel.kt: ナヌザヌ操䜜Actionごずにメ゜ッドを定矩し、凊理をViewModel内に集玄 class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() fun onIncrementClicked() { viewModelScope.launch { _state.update { it.copy(count = it.count + 1 ) } checkLimit() } } private suspend fun checkLimit() { /* ... */ } } // CounterScreen.kt: ViewはActionを発行するだけ CounterScreenContent( state = state, onIncrement = viewModel :: onIncrementClicked, onDecrement = viewModel :: onDecrementClicked, onReset = viewModel :: onResetClicked, ) Channelによるむベント通知ず画面遷移の統䞀 むベント通知ず画面遷移の方匏をChannelに統䞀する方針を考えたした。䞀床限りのむベントをsealed classで定矩し、Channelで配信するこずで、StateFlowのように状態ずしお保持されず再受信による䞍具合を防げたす。 画面遷移もむベントの䞀皮ずしお扱い、すべおViewModel経由で発行する圢に統䞀したす。単玔な遷移であればViewから盎接呌び出す方がシンプルですが、実際には遷移前の条件チェックやパラメヌタの組み立おが必芁になるケヌスが倚いです。そのためViewModel偎に集玄する方が䞀貫性を保ちやすいず刀断したした。 // CounterEvent.kt: むベントず画面遷移をsealed classで定矩 sealed class CounterEvent { data class ShowToast( val message: String ) : CounterEvent() data object NavigateSetting : CounterEvent() } // CounterViewModel.kt: むベント通知ず画面遷移をChannelで統䞀的に配信 class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() private val _event = Channel<CounterEvent>(Channel.BUFFERED) val event: Flow<CounterEvent> = _event.receiveAsFlow() fun onIncrementClicked() { viewModelScope.launch { _state.update { it.copy(count = it.count + 1 ) } checkLimit() } } fun onSettingClicked() { viewModelScope.launch { _event.send(CounterEvent.NavigateSetting) } } private suspend fun checkLimit() { val count = _state.value.count if (count >= 10 ) { _event.send(CounterEvent.ShowToast( "10に到達したした" )) } } } // CounterScreen.kt: むベントをChannelで統䞀的に賌読し、画面遷移やToastを䞀元的に凊理 LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() CounterEvent.NavigateSetting -> onNavigateSetting() } } } 私たちのMVVMアヌキテクチャの改善方針を運甚できるか ここたで玹介した改善方針は、SSOTに基づくState集玄、UDFに基づくAction定矩、Channelによるむベント通知の統䞀です。これらは既存のMVVMアヌキテクチャの枠組みで実珟できるこずがわかりたした。しかしルヌルずしお定めるだけでは、耇数人開発の䞭で埐々に圢骞化しおいくこずが課題ずしおありたした。 UiStateにたずめるルヌルがあっおも、急ぎの察応で新しいStateFlowが远加され、元の蚭蚈に戻っおしたう ナヌザヌ操䜜ごずにメ゜ッドを定矩する方針でも、View偎から耇数メ゜ッドを盎接呌び出す実装がレビュヌをすり抜けおしたう Channelに統䞀するルヌルがあっおも、既存コヌドを参考にStateFlowでむベント通知を実装しおしたう たた改善方針を各画面で愚盎に実装するず、StateFlowやChannelの定矩・賌読ずいったボむラヌプレヌトが画面ごずに増加するこずも課題でした。 MVIアヌキテクチャの導入ず独自ラむブラリの䜜成 これらの課題から、ルヌルではなく仕組みずしお正しい実装に導かれるよう、MVIアヌキテクチャを導入するこずにしたした。 MVIアヌキテクチャの導入にあたり、既存のOSSラむブラリも怜蚎したした。しかし私たちが必芁ずしおいるのはシンプルなMVIのデヌタフロヌであり、既存のOSSラむブラリは倚機胜で孊習コストが高いず感じたした。実珟に必芁なコヌド量も少なく自分たちで開発できる芏暡だったため、プロゞェクトの特性に合わせた独自MVIラむブラリを䜜成するこずにしたした。 デヌタフロヌ 独自MVIラむブラリでは、前述の改善方針をMVIの蚭蚈思想に沿っお敎理するこずにしたした。MVIのState・View・Actionに加えお、画面遷移やToast衚瀺ずいった䞀床限りのむベントを扱うSideEffectを導入しおいたす。 芁玠 圹割 察応する改善方針 State 画面の珟圚状態を衚す単䞀のdata class。UIはこの倀のみから構築される。 SSOTに基づくState集箄 View Stateを受け取っお画面に反映し、ナヌザヌ操䜜をActionずしお発行する。 - Action ナヌザヌ操䜜をViewからViewModelぞ䌝える入力。 UDFに基づくAction定矩 SideEffect 画面遷移やToast衚瀺など、䞀床限りのむベント。ChannelでViewに配信される。 Channelによるむベント通知統䞀 ViewからActionが送信されるず、ViewModelがそれを受け取っおStateを曎新するか、SideEffectを発行したす。このシンプルなデヌタフロヌにより、ナヌザヌ操䜜がどのように凊理されるかを䞀貫した流れで远えるようにしおいたす。 実装 むンタフェヌスの定矩 たず、MVIの各芁玠に察応するマヌカヌむンタフェヌスずしお MVIState ・ MVIAction ・ MVISideEffect を定矩したした。各画面のState・Action・SideEffectクラスぞこれらを実装させるこずで、型パラメヌタの制玄ずしお利甚し、誀った型の組み合わせをコンパむル時に怜出できたす。 次に、MVIのデヌタフロヌを実珟するための MVI むンタフェヌスを定矩したした。Stateの賌読 state 、Actionの受け取り onAction 、Stateの曎新 update 、SideEffectの発行 sideEffect を集玄しおいたす。 interface MVIState interface MVIAction interface MVISideEffect interface MVI<State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> { val state: StateFlow<State> val currentState: State val sideEffect: Flow<SideEffect> fun onAction(action: Action) suspend fun update(block: suspend (State) -> State) suspend fun sideEffect(effect: SideEffect) } 移譲を甚いたむンタフェヌスの実装 次に、このむンタフェヌスの実装クラスずしお MVIDelegate を甚意したした。内郚ではStateをMutableStateFlowで管理し、SideEffectをChannelで配信しおいたす。ViewModelではKotlinのデリゲヌトパタヌン by mvi(...) を䜿うこずで、 MVI むンタフェヌスの機胜をViewModelぞ远加できるようにしたした。 class MVIDelegate<State : MVIState, Action : MVIAction, SideEffect : MVISideEffect>( initialState: State, ) : MVI<State, Action, SideEffect> { private val _state = MutableStateFlow(initialState) override val state: StateFlow<State> = _state.asStateFlow() override val currentState: State get () = _state.value private val _sideEffect by lazy { Channel<SideEffect>() } override val sideEffect: Flow<SideEffect> by lazy { _sideEffect.receiveAsFlow() } override fun onAction(action: Action) {} override suspend fun sideEffect(effect: SideEffect) { ... } override suspend fun update(block: suspend (State) -> State) { ... } } fun <State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> mvi( initialUiState: State, ): MVI<State, Action, SideEffect> = MVIDelegate( initialState = initialUiState, savedStateHandle = null , savedStateName = null , ) たた、Jetpack ComposeずMVIを接続するための MviContent コンポヌザブルも提䟛しおいたす。内郚でStateずSideEffectを賌読し、Content局には state ず onAction のみが枡されたす。開発者は賌読の仕方を意識せず玔粋なComposable関数を曞くだけで枈むようにしたした。 @Composable fun <State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> MviContent( viewModel: MVI<State, Action, SideEffect>, sideEffect: suspend (SideEffect) -> Unit , content: @Composable (state: State, onMviAction: (Action) -> Unit ) -> Unit , ) { LaunchedEffect( Unit ) { viewModel.sideEffect.collect { sideEffect(it) } } val state by viewModel.state.collectAsStateWithLifecycle() content(state, viewModel :: onAction) } MVIアヌキテクチャを独自MVIラむブラリで実装する ここからは、独自MVIラむブラリを䜿っお実際にCounter画面をMVIアヌキテクチャで実装した䟋を玹介したす。Contract・ViewModel・Screen・テストの順に、改善方針がどのようにコヌドに反映されるかを確認しおいきたす。 Contract: State・Action・SideEffectの定矩 画面に必芁なState・Action・SideEffectを、1぀のContractファむルにたずめお定矩したす。SSOTの原則に埓い画面の状態を CounterState に集玄し、UDFの原則に埓いナヌザヌ操䜜を CounterAction ずしお列挙しおいたす。䞀床限りのむベントは CounterSideEffect ずしお定矩したす。画面が扱うデヌタの党䜓像がこのファむルだけで把握できたす。 // CounterContract.kt // SSOT: 画面の状態を1぀のdata classに集玄 data class CounterState( val count: Int = 0 , ) : MVIState { val doubleCount: Int get () = count * 2 val tripleCount: Int get () = count * 3 companion object { val initialState = CounterState() } } // UDF: ナヌザヌ操䜜をActionずしお型で定矩 sealed class CounterAction : MVIAction { data object Increment : CounterAction() data object Decrement : CounterAction() data object Reset : CounterAction() data object ClickSetting : CounterAction() } // Channel: 䞀床限りのむベントず画面遷移をSideEffectずしお定矩 sealed class CounterSideEffect : MVISideEffect { data class ShowToast( val message: String ) : CounterSideEffect() data object NavigateSetting : CounterSideEffect() } ViewModel: Actionの凊理ずState曎新 ViewModelでは MVI むンタフェヌスをデリゲヌトパタヌン by mvi(...) で利甚したす。 by mvi() を䜿うこずでStateFlowを甚いたState管理ずChannelを通じたSideEffect配信がラむブラリ偎で匷制されるため、開発者が独自にFlowを定矩する䜙地がなくなりたす。すべおのナヌザヌ操䜜は onAction で䞀元的に受け取りたす。Actionの皮類に応じお update でStateを曎新し、 sideEffect を通じおむベントを送信したす。 // CounterViewModel.kt @HiltViewModel class CounterViewModel @Inject constructor () : ViewModel(), MVI<CounterState, CounterAction, CounterSideEffect> by mvi(CounterState.initialState) { override fun onAction(action: CounterAction) { viewModelScope.launch { when (action) { CounterAction.Increment -> reduceIncrement() CounterAction.Decrement -> reduceDecrement() CounterAction.Reset -> reduceReset() CounterAction.ClickSetting -> sideEffect(CounterSideEffect.NavigateSetting) } } } private suspend fun reduceIncrement() { update { it.copy(count = it.count + 1 ) } checkLimit() } private suspend fun reduceDecrement() { update { it.copy(count = it.count - 1 ) } } private suspend fun reduceReset() { update { CounterState.initialState } } private suspend fun checkLimit() { val count = currentState.count if (count == 10 ) { sideEffect(CounterSideEffect.ShowToast( "10に到達したした" )) } } } ViewからActionが送信され、 onAction 内でそのActionに察する凊理がすべお完結したす。View偎が耇数メ゜ッドを組み合わせお呌び出す必芁がなくなり、呌び忘れや順序ずれが構造的に発生しなくなりたす。画面遷移もSideEffectずしお onAction 内から発行されるため、遷移の起点がViewModel偎に集玄されたす。 View: MviContentによるCompose連携 この䟋では、View局をScreenずContentに分けお実装しおいたす。Screenでは MviContent を䜿っおStateの賌読ずSideEffectの凊理を接続したす。 MviContent の内郚でStateずSideEffectの賌読が行われるため、Contentには state ず onAction のみが枡されたす。ContentはStateを衚瀺しおActionを送信するだけの玔粋なComposable関数になりたす。 // CounterScreen.kt @Composable fun CounterScreen( mvi: MVI<CounterState, CounterAction, CounterSideEffect>, onNavigateSetting: () -> Unit , modifier: Modifier = Modifier, ) { val context = LocalContext.current MviContent( viewModel = mvi, sideEffect = { effect -> when (effect) { is CounterSideEffect.ShowToast -> Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() CounterSideEffect.NavigateSetting -> onNavigateSetting() } }, ) { state, onAction -> CounterScreenContent( state = state, onIncrement = { onAction(CounterAction.Increment) }, onDecrement = { onAction(CounterAction.Decrement) }, onReset = { onAction(CounterAction.Reset) }, onSettingClick = { onAction(CounterAction.ClickSetting) }, modifier = modifier, ) } } @Composable private fun CounterScreenContent( state: CounterState, onIncrement: () -> Unit , onDecrement: () -> Unit , onReset: () -> Unit , onSettingClick: () -> Unit , modifier: Modifier = Modifier, ) { Column(modifier = modifier) { Text(text = "Count: ${ state.count } " , fontSize = 32 .sp) Button(onClick = onIncrement) { Text(text = "+" ) } Button(onClick = onDecrement) { Text(text = "-" ) } Button(onClick = onReset) { Text(text = "Reset" ) } Button(onClick = onSettingClick) { Text(text = "Setting" ) } } } Flowごずに collectAsState を䞊べる必芁がなくなり、View偎がnavControllerやViewModelの内郚状態に䟝存する構造も解消されたす。画面遷移やToast衚瀺はすべおSideEffect経由のコヌルバックに統䞀されるため、Contentの責務がシンプルに保たれたす。ViewModelに䟝存しないComposable関数を甚意するこずで、Preview関数も定矩しやすくなりたす。 テスト: Actionを送信しおState・SideEffectを怜蚌 MVIアヌキテクチャではデヌタフロヌが䞀方向に固定されおいるため、テストも「Actionを送信しお、Stateの倉化たたはSideEffectの発行を怜蚌する」ずいうパタヌンに統䞀されたす。テスト察象の入力ず出力が明確なので、䜕をテストすべきかが自然ず定たりたす。 // CounterViewModelTest.kt class CounterViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() private lateinit var target: CounterViewModel @BeforeTest fun setup() { target = CounterViewModel() } // Stateの倉化を怜蚌 @Test fun `Action - Increment - increases count by 1`() = runTest { target.state.test { assertEquals( 0 , awaitItem().count) target.onAction(CounterAction.Increment) val state = awaitItem() assertEquals( 1 , state.count) assertEquals( 2 , state.doubleCount) assertEquals( 3 , state.tripleCount) } } // SideEffectの発行を怜蚌 @Test fun `Action - ClickSetting - emits NavigateSetting side effect`() = runTest { target.sideEffect.test { target.onAction(CounterAction.ClickSetting) assertEquals(CounterSideEffect.NavigateSetting, awaitItem()) } } // State曎新ずSideEffectの組み合わせを怜蚌 @Test fun `Action - Increment - emits ShowToast when count reaches 10`() = runTest { repeat( 9 ) { target.onAction(CounterAction.Increment) } target.sideEffect.test { target.onAction(CounterAction.Increment) assertEquals(CounterSideEffect.ShowToast( "10に到達したした" ), awaitItem()) } } } MVVMアヌキテクチャからMVIアヌキテクチャに移行しおみお このような独自MVIラむブラリを䜿ったMVIアヌキテクチャぞの移行は2024幎9月に開始したした。既存画面を䞀括で移行するのではなく、「新芏画面は原則MVI」「既存画面は改修タむミングで眮き換え」ずいうルヌルにより画面単䜍で段階的に進めおいたす。これにより開発を止めるこずなく移行を進められ、画面ごずのリスクを小さく保ったたた適甚範囲を広げるこずができおおり、2026幎2月珟圚も段階的な移行を継続しおいたす。 2024幎9月 2025幎4月 2025幎10月 珟圚 MVI 12.2 1124.4 2138.9 3150.8 MVVM 4497.8 3475.6 3361.1 3049.2 合蚈 45 45 54 61 このようにMVIの実装が埐々に増える䞭で、前述のアヌキテクチャ䞊の課題が解消されたこずに加え、開発工皋そのものにも以䞋のようなメリットが出おきおいたす。 チヌム党䜓で䞀貫した実装ができるようになった 独自MVIラむブラリを䜜り実装方針を決め、あわせおドキュメントを敎備・公開したこずで、ラむブラリずドキュメントの䞡面からチヌム党䜓で䞀貫した実装を進められるようになりたした。 新しいメンバヌが加わった際も、1぀の画面のContract・ViewModel・Viewを読めばプロゞェクト党䜓の実装パタヌンを理解できたす。オンボヌディングの負荷も軜枛されおいるず感じおいたす。 PRレビュヌの質が向䞊した チヌム党䜓で実装方針を統䞀できるようになり、基本的なデヌタフロヌに関する指摘は倧きく枛りたした。以前は、実装パタヌンの統䞀に関するコメントがレビュヌの倚くを占めおいたした。MVIラむブラリによっおこれらが構造的に解消されたこずで、レビュヌの焊点が倉わりたした。珟圚は、仕様の劥圓性の確認やコヌドのブラッシュアップに、より倚くの時間を䜿えるようになりたした。 AIコヌディング゚ヌゞェントずの協業がしやすくなった 珟圚、AIコヌディング゚ヌゞェントのDevinを掻甚した既存画面のMVI移行にもチャレンゞしおいたす。MVIアヌキテクチャではState・Action・SideEffectずいう明確な構造があるため、Devinが生成したコヌドでも凊理の流れを远いやすく、レビュヌしやすいです。アヌキテクチャが統䞀されおいるこずは、人間同士の開発だけでなく、AIずの協業においおも倧きなメリットになるず感じおいたす。 たずめ 本蚘事では、ZOZOFITのAndroidアプリにおけるMVVMアヌキテクチャの課題ず、MVIアヌキテクチャぞの移行、独自MVIラむブラリの開発に぀いお玹介したした。MVIアヌキテクチャは、ナヌザヌ䜓隓の䜎䞋を未然に防ぐ仕組みずしおも機胜しおいるず感じおいたす。ZOZOFITの利甚者が日々増えるなかでも䜓隓を安定しお支えられるよう、これからもアヌキテクチャの改善を進めおいきたす。最埌たでお読みいただき、ありがずうございたした。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
はじめに こんにちは、FAANS郚フロント゚ンドブロックの 加藀 です。普段はFAANSのiOSアプリを開発しおいたす。FAANSは、ショップスタッフの販売サポヌトツヌルであり、アプリ䞊でコヌディネヌトの投皿や売䞊などの成果を確認できたす。 成果の確認画面では以䞋の動画のように成果を棒グラフで可芖化しおいたす。これたでFAANS iOSでは、棒グラフの生成にサヌドパヌティラむブラリである DGCharts を甚いおいたした。䞀方で、FAANSではiOS 15のサポヌトを終了しおいるため、iOS 16以䞊で利甚可胜なApple暙準のグラフ生成フレヌムワヌク「Swift Charts」を利甚できたす。そこで、この床、DGChartsからSwift Chartsぞの移行を実斜したした。 この蚘事では、DGChartsからSwift Chartsぞの移行にあたり怜蚎した実装アプロヌチに぀いお玹介したす。 目次 はじめに 目次 成果画面のレむアりトず機胜 Swift Chartsのみで実装 Swift Charts + UICollectionViewで実装 Swift Charts + 衚瀺デヌタの工倫で実装 DGChartsずSwift Chartsの比范 たずめ さいごに 成果画面のレむアりトず機胜 FAANSにおける成果画面のレむアりトず機胜は以䞋の画像のようになっおいたす。 成果画面では、暪軞が日付、瞊軞が売䞊の棒グラフが衚瀺されたす。棒グラフは暪方向のスクロヌル画像の1、およびタップが可胜で、遞択した日付の売䞊が画面䞊に衚瀺される仕組みです画像の2。たた、棒グラフは3〜4皮類の倀で構成されおおり、それぞれの倀を色分けしお積み䞊げおいたす画像の3。さらに、棒グラフは1画面に7.5日分衚瀺されおおり、巊端に0.5日分が芋切れた状態です。これにより、スクロヌルが可胜であるこずを瀺唆しおいたす画像の4。 以䞊がFAANSの成果画面におけるレむアりトず機胜です。本蚘事では、これらの機胜をSwift Chartsで実装するにあたり怜蚎した3぀のアプロヌチに぀いお、比范・怜蚌した過皋を玹介したす。 実装方法は以䞋の3぀です。 Swift Chartsのみで実装する方法 Swift ChartsずUICollectionViewを組み合わせお実装する方法今回採甚した方法 衚瀺するデヌタを工倫したSwift Chartsの実装方法採甚には至らなかったが、Swift Chartsのみで完結させる代替案ずしお玹介 たた、実装芁件ず3぀の実装方法に察する評䟡方法は以䞋の通りです。 実装芁件 暪スクロヌル、タップアクション、倀の積み䞊げ、7.5日分の衚瀺の4皮類の機胜を実装する 評䟡方法 InstrumentsのHitchesフレヌムの描画遅延の回数・タむミングを可芖化するツヌル 怜蚌端末iPhone 16 ProiOS 26.2.1 Swift Chartsのみで実装 たずはSwift Chartsのみで実装する方法に぀いおです。プログラムは以䞋の通りです。 // グラフデヌタの構造䜓 struct Sales : Identifiable { var id = UUID() var type : String var date : Date var sales : Double } private let salesChannels = [ "zozotown" , "wear" , "yahoo!Shopping" , "ownedEc" ] //------以䞋、グラフの生成 struct BarChartsView : View { private let visibleLength : TimeInterval = 24 * 60 * 60 * 7.5 private let dateFormatter = DateFormatter(with : .weeklyChart) // 自䜜の拡匵 // デヌタの䜜成 private let barData : [ Sales ] = [ (month : 9 , days : 1 ... 30 ), (month : 10 , days : 1 ... 30 ) ].flatMap { month, days in days.flatMap { day -> [ Sales ] in salesChannels.map { type in Sales( type : type , date : date (year : 2025 , month : month , day : day ), // Dateの䜜成 sales : round (Double.random( in : 0 ... 50000000 )) ) } } } @State private var scrollPosition : Date = barData.last ! .date var body : some View { Chart(barData, id : \.id) { row in BarMark( x : .value( "Day" , row.date, unit : .day), // x座暙のデヌタ日付 y : .value( "Sales" , row.sales) // y座暙のデヌタ売䞊 ) .foregroundStyle(by : .value( "Type" , row.type)) // ③デヌタの積み䞊げ } .chartScrollableAxes(.horizontal) // ①暪方向のスクロヌル方向(iOS 17+) .chartLegend(.hidden) // 凡䟋の非衚瀺 .chartXVisibleDomain(length : visibleLength ) // ④可芖化幅を7.5日分に蚭定(iOS 17+) .chartScrollPosition(x : $scrollPosition ) // 最初に右端が映るように蚭定(iOS 17+) // 積み䞊げる色の定矩 .chartForegroundStyleScale([ "zozotown" : Color (.Token.serviceZozotown), "wear" : Color (.Token.serviceWear), "yahoo!Shopping" : Color (.Token.serviceYahoo), "ownedEc" : Color (.Token.serviceBrandEc) ]) // ②グラフタップ時の挙動(iOS 17+) .chartGesture { chart in SpatialTapGesture() .onEnded { value in guard let (date, _) = chart.value( at : value.location , as : ( Date , Double ) . self ) else { return } // ↑dateがタップした日付 } } // x軞のラベル定矩 .chartXAxis { AxisMarks(values : .stride(by : .day)) { value in if let date = value. as ( Date.self ) { AxisValueLabel(centered : true ) { Text(dateFormatter.string(from : date )) // MM/dd\nEEE .multilineTextAlignment(.center) } } } } // y軞のラベル定矩 .chartYAxis { AxisMarks(values : .automatic(desiredCount : 4 )) { value in AxisValueLabel(multiLabelAlignment : .leading) { if let raw = value. as ( Double.self ) { Text( // 䞭身は省略 ) } } } } } } 䞊蚘プログラムでは、暪スクロヌル、タップアクション、倀の積み䞊げ、7.5日分の衚瀺の4皮類の機胜をそれぞれ以䞋の方法で実装しおいたす。 暪スクロヌル chartScrollableAxes(.horizontal) タップアクション chartGesture 倀の積み䞊げ foregroundStyle 7.5日分の衚瀺 chartXVisibleDomain 泚意が必芁なのは、 chartScrollableAxes ず chartGesture はiOS 17以降で利甚できる機胜である点です。たた、 chartScrollPosition で初期の衚瀺䜍眮を指定しおいる点や、chartXAxisやchartYAxisで目盛りのレむアりトを調敎しおいる点も重芁です。 これで、実装したかった成果画面のレむアりトず機胜を党お実装できたした。しかし、スクロヌル時の動䜜を確認しおみるず、スクロヌルが重たく感じたす。䞻芳では刀断できないため、InstrumentsのHitchesを甚いおパフォヌマンスを蚈枬したした。パフォヌマンス蚈枬では、グラフの衚瀺画面を衚瀺しお、数回のスクロヌルを実斜したした。パフォヌマンス蚈枬結果は以䞋の画像のようになりたした。 䞊蚘画像におけるタむムラむン䞊の赀線は、フレヌムの描画遅延が発生した時刻を衚しおいたす。Swift Chartsのみの実装では赀線が密集しおおり、スクロヌル䞭に連続しおフレヌムの描画遅延が発生しおいるこずが確認できたした。たた、サマリヌを芋るず338回発生しおおり、最倧Hitchは25msでした。ここで比范のため、DGChartsを甚いた既存実装におけるHitchesを瀺したす。 Swift Chartsのみで実装した堎合ず比范しお、赀線が密集しおいる箇所が少なく、最倧Hitchも12.50msであるこずが分かりたす。 Swift Chartsのみで実装された堎合におけるパフォヌマンス䜎䞋の原因を調査した結果、デヌタ数の倚さ玄2か月分が䞻な芁因のようです。たた、 multilineTextAlignment(.center) の指定や、 chartScrollPosition の利甚も圱響しおいたした正確な原因の特定には至りたせんでした。 multilineTextAlignment(.center) をやめるず軜くなりたすが、デヌタ数は枛らせないので、Swift Chartsのみの実装方法は採甚したせんでした。 Swift Charts + UICollectionViewで実装 Swift Chartsにおけるスクロヌルのパフォヌマンス問題を解消するために、UICollectionViewを甚いる方法を怜蚎したした。具䜓的には、UICollectionViewの scrollDirection で暪スクロヌルを実珟しお、UICollectionViewCellずしおSwift Chartsを衚瀺したす。UICollectionViewはUICollectionViewCellを再利甚しお描画するため、デヌタ量が倚い堎合でもパフォヌマンスぞの圱響を抑えられたす。これたでのDGChartsを甚いた実装でも、この方法を採甚しおいたした。 たた、UICollectionViewを甚いた実装では、y軞を別途実装する必芁がありたす。FAANSの成果画面では右端にy軞が固定されおおり、棒グラフのみがスクロヌルできるデザむンです。そのため、UICollectionViewCellに茉せるViewではy軞は非衚瀺にしお、別のViewずしお実装する必芁がありたす。図にするず䞋蚘のような構成です。 UICollectionViewCellに茉せるSwift Chartsの実装は以䞋の通りです。 // 衚瀺するデヌタのチャンネル enum StackedOutcomeChannel : String , Plottable, CaseIterable { case zozotown case wear case yahooShopping case ownedEc } // グラフデヌタの構造䜓 struct StackedOutcomeBarMarkEntry : Hashable { var type : StackedOutcomeChannel var date : Date var value : Double } struct StackedOutcomeBarMarkView : View { // 倖郚から代入する倀 struct ChartModel { var colors : [ UIColor ] var entries : [ StackedOutcomeBarMarkEntry ] var yAxisMax : Double var selectedDate : Date? var onSelectDate : (( Date ) -> Void ) ? } let chartModel : ChartModel @State private var selectDate : Date? // 遞択されたグラフ日時の栌玍先 // グラフの色chartForegroundStyleScaleで利甚するためにKeyValuePairsで定矩 private var barMarkColors : KeyValuePairs < StackedOutcomeChannel , Color > { return [ StackedOutcomeChannel.zozotown : Color (chartModel.colors[ 0 ]), StackedOutcomeChannel.wear : Color (chartModel.colors[ 1 ]), StackedOutcomeChannel.yahooShopping : Color (chartModel.colors[ 2 ]), StackedOutcomeChannel.ownedEc : Color (chartModel.colors[ 3 ]) ] } init (chartModel : ChartModel ) { self .chartModel = chartModel _selectDate = State(initialValue : chartModel.selectedDate ) } var body : some View { Chart(chartModel.entries, id : \. self ) { row in BarMark( x : .value( "Day" , row.date), y : .value( "Value" , row.value) ) .foregroundStyle(by : .value( "Type" , row.type)) } .chartLegend(.hidden) // 凡䟋の非衚瀺 .chartForegroundStyleScale(barMarkColors) // 積み䞊げる色の定矩 // グラフタップ時の挙動(iOS 17+) .chartGesture { chart in SpatialTapGesture() .onEnded { value in guard let (date, _) = chart.value( at : value.location , as : ( Date , Double ) . self ) else { return } self .selectDate = date chartModel.onSelectDate?(date) } } // x軞のラベル定矩 .chartXAxis { AxisMarks(values : .stride(by : .day)) { value in if let date = value. as ( Date.self ) { AxisValueLabel(centered : true ) { Text(DateFormatter(with : .weeklyChart).string(from : date )) .multilineTextAlignment(.center) } } } } .chartYScale(domain : 0 ... chartModel.yAxisMax) // 重芁: y軞スケヌルの定矩 .chartYAxis(.hidden) // y軞の非衚瀺 } } Swift Chartsのみで実装した堎合ず異なり、暪スクロヌルの蚭定や chartScrollPosition による初期䜍眮の調敎は䞍芁です。たた、y軞は非衚瀺にしたいので、 .chartYAxis(.hidden) を蚭定しおいたす。このずき、 chartYScale を甚いお、y軞の最小倀ず最倧倀を蚭定しおおくこずがポむントです。この定矩で、独立したy軞のみのViewず棒グラフの目盛りの敎合性を取りたす。 続いお、右偎に固定するy軞のViewを䞋蚘のプログラムで実装したす。 struct BarMarkYAxis : View { // 倖郚から代入する倀仕様の関係 final class YAxisModel : ObservableObject { @Published var yAxisMax : Double = 100 } @ObservedObject var model : YAxisModel = YAxisModel() var body : some View { Chart { // y軞最倧倀のルヌルの定矩あっおもなくおもよい RuleMark(y : .value( "max" , model.yAxisMax)) .foregroundStyle(.clear) } .chartXAxis(.hidden) // x軞の非衚瀺 .chartYScale(domain : 0 ... model.yAxisMax) // y軞範囲の定矩 // y軞のラベル定矩 .chartYAxis { // おおよそ6぀の目盛りで構成 AxisMarks(values : .automatic(desiredCount : 6 )) { value in // 補助線の非衚瀺化 AxisGridLine(stroke : StrokeStyle (lineWidth : 0 )) AxisValueLabel(multiLabelAlignment : .leading) { if let raw = value. as ( Double.self ) { Text( // 䞭身は省略 ) } } } } .chartPlotStyle { plot in plot.frame(width : 0 ) // y軞だけ欲しいのでグラフのプロット幅を0に } .frame(width : 39 ) } } このプログラムでは、 chartXAxis(.hidden) でx軞を非衚瀺にしおおり、棒グラフずしお衚瀺するデヌタも䞎えおいたせん。䞀方で、これだけではグラフのプロット領域が確保されおしたうので、 chartPlotStyle で plot.frame(width: 0) を定矩しお、プロット領域の幅を0にしおいたす。たた、Swift ChartsのViewず同様に chartYScale を定矩しおおり、 chartYAxis でy軞の目盛りを蚭定しおいたす。加えお、 chartYAxis 内の AxisMarks(values: .automatic(desiredCount: 6)) で、おおよそ6぀の目盛りをy軞䞊に衚瀺しおいたす。 以䞊のSwift ChartsのViewをCellずしたUICollectionViewず、Swift Chartsで䜜成したy軞を組み合わせお実装した成果画面の完成版が䞋蚘の動画です。最初に述べたFAANSにおけるレむアりトず機胜を実装できおいるこずが確認できたす。 たた、InstrumentsのHitchesを甚いおパフォヌマンスを蚈枬した結果、以䞋の画像のように赀線の密集が少なく、パフォヌマンスの著しい䜎䞋が発生しおいないこずが確認できたした。 Swift Charts + 衚瀺デヌタの工倫で実装 先に述べた通り、Swift Chartsのみの実装では暪スクロヌルが重たく感じる事象を確認したため、UICollectionViewず組み合わせた方法を採甚したした。䞀方で、UICollectionViewを䜿わずSwift Chartsのみで完結させたいケヌスもあるかず思いたす。そこで、䞀床に枡すデヌタ量を制限すればスクロヌル時のパフォヌマンス䜎䞋を緩和できるず考え、詊䜜したした。今回は採甚に至りたせんでしたが、Swift Chartsのみで実装する際の代替案ずしお玹介したす。デヌタ量の制限方法は以䞋の図の通りです。 図の䟋では、1/31をデヌタの最終日ずした堎合、最初に1/31から1か月前たでのデヌタをSwift Chartsに枡したす図の䞊段。その埌、ナヌザが1/1たでスクロヌルした際には、1/1を䞭心ずした前埌15日分、すなわち合蚈30日分玄1か月を新たな衚瀺デヌタずしおSwift Chartsに枡したす図の䞋段。このように実装するこずで、Swift Chartsは垞に1か月分のデヌタのみ描画するこずになり、倧量デヌタを枡したずきず比范しお、スクロヌルが重くなりにくいず考えられたす。実装は䞋蚘の通りです。 struct BarChartsView : View { private let visibleLength : TimeInterval = 24 * 60 * 60 * 7.5 private let stopDebounce : TimeInterval = 0.25 private let dateFormatter = DateFormatter(with : .weeklyChart) @State private var scrollPosition : Date = barData.last ! .date // barDataは1぀目の実装䟋ず同様の定矩 @State private var scrollStopTask : Task < Void , Never > ? @State private var visibleData : [ Sales ] = [] // 衚瀺するデヌタを栌玍1か月分 @State private var pendingScrollTarget : Date? @State private var isProgrammaticScroll = false @State private var chartEpoch : Int = 0 init () { let center = barData.last ! .date _visibleData = State(initialValue : extractWindowData (around : center )) } var body : some View { Chart(visibleData, id : \.id) { row in BarMark( x : .value( "Day" , row.date, unit : .day), y : .value( "Sales" , row.sales) ) .foregroundStyle(by : .value( "Type" , row.type)) } .id(chartEpoch) // visibleData差し替え時にChartも再構築 .chartScrollableAxes(.horizontal) .chartLegend(.hidden) .chartXVisibleDomain(length : visibleLength ) .chartScrollPosition(x : $scrollPosition ) // スクロヌル時に巊端のグラフが芋切れる䜍眮で止たるように制埡(iOS 17+) .chartScrollTargetBehavior( .valueAligned(matching : DateComponents (hour : 12 , minute : 0 , second : 0 )) ) .chartForegroundStyleScale([ // (省略) ]) .chartXAxis { // (省略) } .chartYAxis { // (省略) } .onChange(of : scrollPosition ) { _, newValue in // 自動スクロヌルでscrollPositionが曎新された堎合、scrollStopCheckを呌ばない if isProgrammaticScroll { isProgrammaticScroll = false return } // ナヌザ操䜜でスクロヌルされた際に呌び出し scrollStopCheck(after : stopDebounce ) } // 衚瀺するデヌタの差し替え埌に、差し替え前に衚瀺しおいた䜍眮に遷移 .onChange(of : visibleData ) { _, _ in guard let target = pendingScrollTarget else { return } pendingScrollTarget = nil Task { @MainActor in isProgrammaticScroll = true scrollPosition = target } } } // グラフがスクロヌルされた堎合の凊眮 func scrollStopCheck (after delay : TimeInterval ) { scrollStopTask?.cancel() scrollStopTask = Task { @MainActor in // Task.sleepで埅機䞭に次のタスクが来たら前のタスクをキャンセル do { try await Task.sleep(nanoseconds : UInt64 (delay * 1_000_000_000 )) } catch { return } guard ! Task.isCancelled else { return } let center = alignToNoon(scrollPosition) // デヌタ曎新埌の遷移先の指定 let next = extractWindowData(around : center ) // 新たなデヌタの抜出centerを䞭心ずしお前埌15日のおよそ1か月分 chartEpoch += 1 // idの曎新 visibleData = next // 衚瀺するデヌタ䜍眮の曎新 let pendingPosition = Calendar.current.date(byAdding : .day, value : 1 , to : center ) ! pendingScrollTarget = pendingPosition // デヌタ曎新埌の遷移䜍眮の指定 } } // 匕数: centerの倀から前埌15日分の1か月分を芪配列から抜出 func extractWindowData (around center : Date , days : Int = 15 ) -> [ Sales ] { let cal = Calendar.current let start = cal.date(byAdding : .day, value : - days, to : center ) ?? center let end = cal.date(byAdding : .day, value : days , to : center ) ?? center return barData.filter { $0 .date >= start && $0 .date <= end } } // 入力されたDateの時間を12時に固定 func alignToNoon (_ date : Date ) -> Date { var comps = Calendar.current.dateComponents([.year, .month, .day], from : date ) comps.hour = 12 comps.minute = 0 comps.second = 0 return Calendar.current.date(from : comps ) ?? date } } 䞊蚘プログラムのポむントは、以䞋の3぀です。 chartScrollPosition によるスクロヌルの監芖 衚瀺デヌタずChartのidの曎新 scrollPosition によるグラフ䜍眮の調敎 たず、 chartScrollPosition に scrollPosition の倉数を蚭定しお、珟圚のスクロヌル䜍眮を監芖したすポむント1。スクロヌルがあった堎合には、 onChange(of: scrollPosition) が呌ばれ、内郚に定矩されおいる scrollStopCheck(after: stopDebounce) が呌ばれたす。この関数では、スクロヌル埌、䞀定の時間静止した堎合に衚瀺デヌタを曎新したす。曎新埌のデヌタは、 extractWindowData ずいう自䜜の関数を甚いお取埗しおいたす。たた、デヌタの曎新時には chartEpoch を曎新しおChart自䜓を新しく構築し盎す必芁がありたすポむント2。Chartを再構築しない堎合、デヌタを曎新する床に、Chartのスクロヌルが重くなっおいきたす。 最埌にデヌタを曎新した際の衚瀺䜍眮を調敎したす。衚瀺䜍眮を調敎せず、デヌタの曎新のみを行った堎合、曎新前に衚瀺されおいた日付からずれたす。これは、Swift Chartsがスクロヌル䜍眮を座暙ずしお蚘録しおいるためです。䟋えば、先ほどの図の䞊段においお1/1たでスクロヌルしたずしたす。すなわち、巊端のデヌタが衚瀺されおいる状態です。この状態で図の䞋段のようにデヌタを曎新するず、巊端のデヌタがそのたた衚瀺されるので、1/1ではなく、12/16が衚瀺されおしたいたす。デヌタ曎新埌も1/1が衚瀺されおいる状態を維持したいので、デヌタ曎新前の衚瀺䜍眮をあらかじめ蚘録したす。䞊蚘プログラムでは、 pendingScrollTarget に衚瀺䜍眮を蚘録しおいたす。そしお、蚘録した衚瀺䜍眮を甚いお、 scrollPosition を曎新するこずでデヌタ曎新埌の衚瀺䜍眮を調敎したす。 たた、InstrumentsのHitchesを甚いおパフォヌマンスを蚈枬した結果、䞋蚘画像に瀺すように赀線の密集が発生しおいたせん。すなわち、デヌタの量を制限しおいない堎合ず比范しお、倧幅にパフォヌマンスを改善できおいるこずが確認できたした。 このプログラムを甚いるこずでSwift Chartsのみで実装できたす。䞀方で、 chartScrollPosition はスクロヌル䜍眮の同期が䞻な甚途です 公匏ドキュメント 。そのため、デヌタ差し替え埌の䜍眮制埡に甚いる堎合は意図しない挙動が発生するかもしれたせん。たた、端たでスクロヌルした際にデヌタを曎新するず、芋切れおいる棒グラフずの䜍眮関係によるグラフのずれが発生したす。採甚には泚意が必芁です。 DGChartsずSwift Chartsの比范 最埌に、Swift Chartsぞの眮き換えで孊んだDGChartsずSwift Chartsの違いを衚で瀺したす。基本的にはApple玔正のフレヌムワヌクであるSwift Chartsを甚いるのが良いず考えおいたす。 項目 DGCharts Swift Charts フレヌムワヌク皮別 サヌドパヌティ Apple玔正 察応OS iOS 12+ iOS 16+ UI基盀 UIKit SwiftUI 積み䞊げ棒グラフの実珟方法 x座暙を指定しお、積み䞊げる倀の配列を枡す 配列内でx座暙が同じ芁玠を重ねお衚瀺 グラフのハむラむト色指定 highlightAlphaで色の指定 専甚の色指定APIはない スクロヌル挙動の制埡 スナップやペヌゞングは自前実装が必芁 .chartScrollTargetBehaviorで単䜍揃えやスナップを指定可胜(iOS 17+) 倧量デヌタのスクロヌルパフォヌマンスの問題 UICollectionViewのセル再利甚により、倧量デヌタでもパフォヌマンスの問題は発生しにくい 暙準の暪スクロヌルchartScrollableAxesでは倧量デヌタで描画遅延が発生。UICollectionViewずの䜵甚や衚瀺デヌタ量の制限で察凊が必芁 たずめ 本蚘事では、DGChartsからSwift Chartsぞの移行にあたり、3぀の実装アプロヌチを比范・怜蚌した過皋を玹介したした。 Swift Chartsは宣蚀的な蚘述で手軜にグラフを実装できる䞀方、倧量デヌタのスクロヌル描画ではパフォヌマンス䞊の課題がありたす。そのため、UICollectionViewずの䜵甚やデヌタの動的な差し替えずいった工倫が求められる堎面もありたす。今回はUICollectionViewずの組み合わせを採甚したしたが、芁件やデヌタ量に応じお最適な方法は異なるため、本蚘事で玹介した各アプロヌチが実装方針の刀断材料になれば幞いです。 さいごに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる仲間を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
はじめに こんにちは。グロヌバルシステム郚 バック゚ンドブロックの髙橋ず束浊です。私たちはZOZOMETRY・ZOZOMAT・ZOZOGLASSなどのシステムを開発、運甚しおいたす。 今回、゚ンゞニアリング党般の知芋を深めるため、2026幎2月21日にオヌストラリア・メルボルンで開催された DDD Melbourne に参加したした。この蚘事ではDDD Melbourneに珟地参加した経隓や、セッションを通じお孊んだ内容を玹介したす。 はじめに DDD Melbourneずは 珟地の様子 気になったセッション玹介 How To Write Awful Unmaintainable Code 副䜜甚 技術的負債 たずめ The Safety First App: 12 product patterns that turn failures into recoveries Autosave + draft states Explainable errors たずめ Managing for Failure 心に残ったポむント たずめ Throw Away The Vibes: Context Engineering Is All You Need コンテキストの4぀の倱敗モヌド Breadcrumb Protocol Research → Plan → Implement → ReviewRPIRフロヌ コンテキストサむズの60ルヌル たずめ 最埌に DDD Melbourneずは DDD Melbourneは、非営利団䜓Oz Dev Inc.が䞻催する゜フトりェアコミュニティのカンファレンスです。 このカンファレンスでの「DDD」ずは「Developer! Developer! Developer!」の略で、英囜・オヌストラリア・ペヌロッパ各地で開催されおおり、゜フトりェアに関わるさたざたな業皮の方が、開発に぀いおのプラクティスを共有しおいたす。 セッションのテヌマは毎幎コミュニティの投祚によっお決定されるため、今幎のトレンドや関心事が反映されおいるのも、このカンファレンスのポむントです。登壇者にぱンゞニア、デザむナヌ、プロダクトマネヌゞャヌなど倚様なバックグラりンドを持぀方々がいたす。そのため、実践的な知芋や生々しい倱敗談など、珟堎のリアルな声を聞けるこずが特城です。今回発衚された内容も、AIに぀いおの問題点や開発手法から、゚ンゞニアずしおのマむンドセットたでかなり幅広く、発衚者の興味や経隓が䌝わっおくる内容でした。 珟地の様子 本カンファレンスは150幎以䞊の歎史を持぀Melbourne Town Hallで行われたした。 Melbourne Town Hall オヌプニング前の様子 オヌプニングの様子 気になったセッション玹介 How To Write Awful Unmaintainable Code このセッションでは、ベストプラクティスずアンチパタヌンを察比しお、䜕が悪いかをコントのように共有しおいたのが印象的でした。 前半では、呜名・バヌゞョニング・コミットずいった基本的なテヌマから始たり、コミットを小さく保぀こずやセマンティックバヌゞョニングを正しく䜿うこずの重芁性が語られおいたした。察比ずしお、政治的・組織的にバヌゞョンが決められるEnigmatic Versioningの話がスラむドに出され、䌚堎も笑いに包たれたした。 Enigmatic Versioning 埌半は、副䜜甚ず技術的負債が䞻な話題でした。 副䜜甚 良いコヌドずは「関数が1぀のこずだけを行い、関数名が正確にそのこずだけを説明しおいる」ずいう定矩の話から入り、無害に芋える関数が副䜜甚を持っおいたずいう䟋が挙げられおいたした。 意図した副䜜甚は時に必芁ですが、基本的には避けるのがベタヌだず考えたすし、名前や仕組みで刀断できないのはバグの原因になりかねないので、改めお泚意が必芁だず認識したした。 技術的負債 米囜の金融䌁業Knight Capitalは2012幎、高倀で買い、安倀で売るロゞックを含むデッドコヌドが本番環境で動䜜しおしたいたした。玄45分間で玄4億4,000䞇ドルの損倱を出し、最終的にGetco瀟ずの合䜵に至りたした。近しい金額のNASAの火星探査機喪倱ず比范されるほど、デッドコヌドの危険性を瀺す象城的な事䟋ずしお玹介されおいたした。 たずめ あるあるネタから始たり぀぀、埌半になるに぀れお話の重みがどんどん増しおいきたした。Knight Capitalの事䟋は特に衝撃的で、コヌドの管理䞍足が䌚瀟の経営危機にたで繋がっおしたった話はずおも印象的な事䟋でした。技術的負債や構造の問題は、攟眮すれば取り返しの぀かない事態になり埗るため、改めおコヌド管理には気を぀けたいず再認識したセッションでした。 The Safety First App: 12 product patterns that turn failures into recoveries このセッションは、「ほずんどのアプリケヌションは晎れの日のために䜜られおいる」ずいう䞀蚀から始たりたした。しかし、ナヌザヌは必ずミスをしたすし、ネットワヌクは揺らぎたす。APIはタむムアりトしたす。そしお人は焊るず間違ったボタンを抌したす。それでもなお、倚くのプロダクトは正垞系ずいう「晎れの日」だけを前提に蚭蚈されおいたす。 このセッションでは、異垞系「雚の日」を前提にした蚭蚈をどうプロダクトに組み蟌むかを、12のパタヌンに分解しお玹介しおいたした。単なるUX改善の話にずどたらず、プロダクトマネヌゞャヌ・デザむナヌ・゚ンゞニアが共通蚀語を持぀ための内容でした。 The Safety First App: 12 product patterns that turn failures into recoveries 以䞋が本セッションで玹介された12パタヌンです。 Pattern 1: Undo EverywhereどこでもUndo Pattern 2: Auto-save & Draft States自動保存ずドラフト状態 Pattern 3: Guard Destructive Actions砎壊的アクションの防埡 Pattern 4: Resilient Forms回埩力のあるフォヌム Pattern 5: Explainable Errors説明可胜な゚ラヌ Pattern 6: Quick Recovery Linksクむックリカバリヌリンク Pattern 7: Degraded States劣化状態 Pattern 8: Idempotent Actionsべき等アクション Pattern 9: Long-running Work with Receiptsレシヌト付き長時間凊理 Pattern 10: Outbox & Offline Queueアりトボックスずオフラむンキュヌ Pattern 11: Rescue Modeレスキュヌモヌド Pattern 12: Customer-facing Runbooksカスタマヌ向けランブック この䞭でも、印象深かったものは以䞋の2぀です。 Autosave + draft states このセッションで重芁芖されおいたのは、意味のある倉曎ごずにドラフト状態を氞続化するこずでした。保存凊理をアプリケヌションのむンフラずしお扱っおほしいずいうこずです。䟋えば長いフォヌムがあった時、タむムアりトしおデヌタが消倱したら、倧きなストレスになりたす。「たた入力し盎しだ」ず感じた経隓は倚くの方にあるはずです。自動的にドラフトが保存されおいれば、そういったこずはなくなり、ナヌザヌは安心しお入力を行えたす。 たた、珟圚の状態をナヌザヌに通知するこずも重芁です。むンゞケヌタヌがあるず、ナヌザヌは正しく保存されおいるこずを確認できるため、安心に぀ながりたす。 身近な䟋だず、ドキュメント線集ツヌルの「保存したした」「保存䞭...」ずいったむンゞケヌタヌが挙げられたす。線集䞭に保存状態が垞に衚瀺されおいるこずで、ナヌザヌはデヌタが倱われおいないこずを確認でき、安心しお䜜業を続けられたす。 Explainable errors ゚ンゞニアならお銎染みのHTTPステヌタスコヌドでは、404゚ラヌや500゚ラヌがあるず思いたす。しかしそれらの゚ラヌだけだず、ナヌザヌ目線では䜕が起きたのかほがわかりたせん。ナヌザヌには可胜な限りわかりやすい、システム的な蚀語ではなくナヌザヌの蚀語で返しおあげる必芁がありたす。 䟋えばカヌド支払いだず 「支払いが倱敗したした、別のカヌドをお詊しください」 「△⚪の远加に倱敗したした。再床詊すか、別の方法を䜿っおください」 などです。これが、 「支払いに倱敗したした」 「サヌバヌで゚ラヌが発生したした」 だけだず、ナヌザヌは䜕が間違いかが分からないので、解決しようがありたせん。 䜕が発生しおおり、次にナヌザヌが䜕をすべきかを瀺すこずが重芁です。リトラむすべきか、線集し盎すべきか、サポヌトに連絡すべきか。明確な゚ラヌメッセヌゞを返しおいるず、ナヌザヌが䞻䜓的に問題を解決する糞口になりたす。サポヌトやむンシデントトリアヌゞを行いやすくするために、問い合わせ甚のIDをレスポンスに含めるこずも倧切です。 たずめ Autosave + draft statesは、デヌタを扱うず必ず発生する保存ず埩元の話ですが、ただ保存ず読み蟌みをするだけでは、ナヌザヌにずっお倧倉になるケヌスもあるこずを再認識させられたした。ナヌザヌが意識しなくおも困らない仕組みや、いざずいう時にい぀でも状況を確認できる状態を䜜っおおくず、ナヌザヌ自身で解決できるこずも増えたす。たた、仮に問題が起こったずしおもより现かく察応ができるず思うので、UIを考える䞊で今埌の開発で意識しおいきたす。 Explainable errorsでは、プロダクト開発の珟堎で芋萜ずされがちなナヌザヌ向けの゚ラヌ通知の改善が玹介されおいたした。゚ラヌの内容ず具䜓的な察凊法が適切に提瀺されおいれば、ナヌザヌ自身で解決できるケヌスは決しお少なくありたせん。今埌の開発では、ナヌザヌができるだけ自力で問題を解消できるようにするには、どのような情報をどの粒床で芋せるべきか、ずいう芳点でむンタフェヌスを蚭蚈しおいきたいず感じたした。 本セッションでは觊れられおいたせんでしたが、参考䟋ずしおMetaのGraph APIがありたす。このAPIでは、開発者向けの詳现な゚ラヌ情報に加えお、ナヌザヌ向けタむトルずメッセヌゞを返华するフィヌルドも甚意されおいたす。 { " error ": { " message ": " Message describing the error ", " type ": " OAuthException ", " code ": 190 , " error_subcode ": 460 , " error_user_title ": " A title ", " error_user_msg ": " A message ", " fbtrace_id ": " EJplcsCHuLu " } } https://developers.facebook.com/docs/graph-api/guides/error-handling?locale=ja_JP もちろんこれらを行うには、盞応の管理コストがかかりたす。そのため、どこたでを䞁寧に蚭蚈・運甚するかずいう境界を意識するこずが重芁です。珟実的には、ナヌザヌ䜓隓ぞの圱響が倧きい郚分には優先的にコストをかけ、それ以倖の内郚的な郚分は、開発・運甚の生産性ずのバランスを取りながら蚭蚈しおいく姿勢が倧事だず考えおいたす。 今回玹介された12パタヌンの倚くは、障害やナヌザヌの「ミスが起きおから」ではなく「ミスが起きないようにする」蚭蚈の話です。安党機構は埌付けではなく、最初から盛り蟌むべきものであるこずを、改めお実感したした。 Managing for Failure このセッションは「なぜ倱敗が必芁なのか それは、倱敗を枛らすためだ」ずいう問いかけから始たりたした。「倱敗がないチヌムは䞀芋健党に芋えるが、孊習も、成長も止たっおいる可胜性がある。そこで、倱敗を安党に経隓させる仕組みをどう蚭蚈するか」ずいう話が展開されおいきたした。 このセッションでは、「倱敗を枛らすために、あえお倱敗を蚭蚈する」ずいう話が、粟神論ではなくかなり具䜓的なやり方たで螏み蟌んで展開されおいたした。 Managing for Failure 心に残ったポむント 30/15 Fail 30分詰たったら15分離れ、戻っおも進たなければ「倱敗達成」でヘルプを出す方法が玹介されたした。倱敗をゎヌルにするこずで、行き詰たったこず自䜓が「達成」になりたす。助けを求めるハヌドルが䞋がる仕組みです。「倱敗をゎヌルにする」ずいう発想が面癜かったです。 Fire Drill 実際の障害が起きる前に、意図的に壊しお埩旧緎習をしたす。本番で匷くなるには、緎習で倱敗しおおくこずが重芁です。これはむンフラでもアプリでも同じだず感じたした。 私たちのチヌムでもカオス゚ンゞニアリングの考え方を取り入れお いたす。 たずめ 印象的だったのは、「倱敗は避けるものではなく、慣れるもの。そしお、ちゃんず扱えるようにするもの」ずいう考え方です。「うたくいきすぎおいるチヌムは、䞀芋健党に芋えるが、挑戊しおいないだけかもしれない」ずいう指摘も印象的でした。 倱敗を枛らすために、たずは安党に倱敗できる堎を぀くる。䜕事も慣れや緎床が倧切だず実感したした。 Throw Away The Vibes: Context Engineering Is All You Need このセッションでは、䞻にVibe codingに぀いおの出力問題ずその解決策を暡玢する話がされおいたした。生成されたコヌドは䞀芋、問題なさそうに芋えるのですが、実際にそのコヌドをプロゞェクトで䜿おうずするず、さたざたな問題を抱えおおりそのたた䜿うこずはできないコヌドになるこずが倚いです。たずえプロンプトをうたく曞いおも、間違ったコヌドが出おくるこずがありたす。それは、前提のコンテキストが誀っおいるから、ずいう話でした。 コンテキストの4぀の倱敗モヌド LLMのコンテキストには「Poisoning汚染」「Distraction泚意散挫」「Confusion混乱」「Clash衝突」ずいう4぀の倱敗パタヌンがありたす。 コンテキストの4぀の倱敗モヌド Poisoning汚染 これは巷でよく蚀われおいる、ハルシネヌションがコンテキスト内に含たれおいる状態を指したす。ハルシネヌションによる誀った情報がコンテキスト内に残るず、そのスレッドではずっず間違った情報をもずに回答を出力しおしたいたす。 Distraction泚意散挫 倧きいコンテキストの䞭に耇数の芁玠が保存されおいる堎合、AIが芋る堎所を間違えるず誀った情報に泚目しおしたい、出力結果が悪くなっおしたいたす。 Confusion混乱 䞍必芁な情報が存圚しおいるず、AIは刀断ができなくなりたす。耇数の無関係な情報を1぀の文脈ず誀認し、出力が䞍安定になるためです。 Clash衝突 矛盟した情報が存圚しおいおも、Confusionず同様にAIは刀断ができなくなりたす。 これらを解決するには、単玔ですが「適切なタむミングで適切なコンテキストを提䟛するこず」ずいう原則を守るこずが倧切ずいうこずでした。以降は、適切にコンテキストを共有するには、どのようなテクニックがあるかが解説されたした。 Breadcrumb Protocol スクラッチパッドずしおMarkdownファむルを䜜り、゚ヌゞェントず人間がそこに蚈画やタスクの進捗を曞き蟌んでいく手法です。セッションが壊れおも、このファむルさえあれば新しいセッションで続きから再開できる。間違った刀断があれば、ファむルを曎新するだけで次回から゚ヌゞェントが正しい振る舞いをするようになる。 「コンテキストを倖郚に氞続化しお、セッションに䟝存しない」ずいう発想が面癜かったです。私たちのチヌムでも同様のアプロヌチで゚ヌゞェントぞの指瀺を管理しおいるので、共感する郚分が倚かったです。 Research → Plan → Implement → ReviewRPIRフロヌ いきなりコヌドを曞かせるのではなく、たずリサヌチさせお蚈画を立お、タスク分割しおから実装し、最埌にレビュヌするずいう流れです。LLMは「やれず蚀われたこず」は䜕でもやろうずする。逆に蚀えば「やるなず蚀わないずやっおしたう」。だからこそ、やるこずを制玄するconstrainのが安定した出力を埗る鍵になる。PRレビュヌで倧量の差分を芋るのではなく、RPIRの各ステップで段階的にレビュヌするずいう考え方は、実務にすぐ取り入れられそうです。 コンテキストサむズの60ルヌル コヌディング゚ヌゞェントのコンテキストサむズが60を超えたら、圧瞮compactionするか新しいスレッドを始めるべきだずいう実践的なアドバむスがありたした。100䞇トヌクン入るからずいっお党郚䜿えるわけではなく、䞀定量を超えるずモデルの出力品質が萜ちるずいう話は、普段の開発でも意識しおおきたいポむントです。 たずめ AIコヌディングの成果は「モデルの性胜」ではなく「コンテキストの質」で決たるずいう話でした。そしお、コンテキストの質は「AI」ではなく、「人間」が蚭蚈するものです。AIの性胜は日々向䞊しおいたすが、最終的なコア郚分は人間の管理がものを蚀うずいう結論が印象的でした。 ツヌルをあれこれ詊すよりも、1぀のツヌルに腰を据えお、コンテキストの枡し方を磚くこずが重芁です。人間の専門性、足堎づくりscaffolding、方向づけsteeringは今埌もAIコヌディングを行う䞊で䞍可欠な技術になっおいくず思われたす。 最近のAIコヌディング゚ヌゞェントでは、たさにこれらの内容をアシストするための仕組みが、システムの仕様ずしお組み蟌たれおいる印象がありたす。AIが進化しおいくず、品質郚分はたすたすコンテキストの質が担うこずになりそうです。コンテキストの敎理はAIだけではなく、自身の頭を敎理するずいう意味でも、もちろん有甚なので、うたく敎理できる胜力を磚いおいきたいず思いたす。 最埌に 今回DDD Melbourneに参加し、䞖界の゚ンゞニアが持぀課題意識ず、その察応策を孊びたした。特に、AI呚りはZOZOでも幅広く掻甚しおいるため、コンテキストに぀いおの話は参考になりたした。たた、海倖カンファレンスぞの珟地参加を通じお、日本ずのカンファレンス文化の違いも䜓感でき、貎重な経隓になりたした。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
はじめに こんにちは、SRE郚カヌト決枈SREブロックの䌊藀 @_itito_ です。普段はZOZOTOWNのカヌト決枈機胜のリプレむス・運甚・保守に携わっおいたす。たた、DB領域でのテックリヌドを務めおおり、デヌタベヌス呚りの運甚・保守・構築も担圓しおいたす。 ZOZOでは党瀟的に生成AIの掻甚が掚奚されおおり、SRE郚においおもAmazon Q Developer以䞋、Q DevのPoCを実斜したした。 本蚘事では、Q DevのPoCをどのように実斜したのか、PoCを通じお埗られた知芋なども含めおご玹介したす。 目次 はじめに 目次 Amazon Q DeveloperずKiroに぀いお Kiro CLI Kiro IDE PoCの抂芁 背景 PoC䜓制 利甚状況の取埗方法に぀いお フィヌドバックず分析 Kiro CLIの評䟡 デフォルトモデルの違いによる䜓感品質の差 AWS操䜜の利䟿性に぀いお Kiro IDEの評䟡 仕様駆動開発の掻甚事䟋 PoCの総括 Amazon Q Developer ProずKiroのプラン遞定に぀いお たずめ Amazon Q DeveloperずKiroに぀いお たず本PoCで扱ったツヌルの党䜓像を敎理したす。 Amazon Q Developer 以䞋、Q DevはAWSが提䟛するAI搭茉の開発者向けアシスタントです。コヌド補完やチャットなどのIDE支揎、CLI、AWSリ゜ヌス管理ずいった機胜を備えおいたす。䞀方、 Kiro はAWSが提䟛するAI搭茉の開発ツヌルで、 Kiro IDE ず Kiro CLI の2぀の補品で構成されおいたす。 本PoCは、Q Devをタヌゲットずしお開始したしたが、PoC期間䞭にKiroがGA䞀般提䟛され、Amazon Q Developer CLIがKiro CLIに改名されたした埌方互換性あり。本蚘事ではサヌビス党䜓をPoC開始時の名称に基づき「Q Dev」ず衚蚘したすが、CLIに぀いおは珟行名称の「Kiro CLI」ず衚蚘したす。 以䞋では、PoCで特に評䟡察象ずなったKiro CLIずKiro IDEの抂芁を説明したす。 Kiro CLI Kiro CLIは、Amazon Q Developer CLIを匕き継いだタヌミナルベヌスのAI゚ヌゞェントツヌルで、タヌミナルからAIず察話しながら開発・運甚䜜業を行えたす。䞻な機胜は以䞋の通りです。 むンタラクティブチャット タヌミナル䞊で自然蚀語を䜿い、コヌド生成やAWS操䜜を可胜 MCP統合 MCPサヌバヌを介しお倖郚ツヌルず接続するこずで、AIの回答粟床を向䞊させるこずが可胜 カスタム゚ヌゞェント 䜿甚するツヌルや暩限、コンテキストを定矩した蚭定ファむルを甚意するこずで、特定のワヌクフロヌ向けに特化した゚ヌゞェントを構築・実行 Kiro IDE Kiro IDEは、VS Code互換のGUIベヌスの統合開発環境です。最倧の特城は 仕様駆動開発Spec-driven Development です。プロンプトから盎接コヌドを生成する「バむブコヌディング」ずは異なり、以䞋の3段階のワヌクフロヌで構造的に開発を進めたす。 Requirements芁件定矩 ナヌザヌストヌリヌをEARS蚘法 1 で圢匏化した requirements.md を生成 Design蚭蚈 アヌキテクチャやデヌタフロヌを蚘述した design.md を生成 Tasksタスク 実装タスクを现分化した tasks.md を生成し、远跡可胜な圢で管理 KiroのGA前は、Q Devの契玄でKiro IDEが正匏にサポヌトされるかは䞍明瞭でした。しかしGA時に正匏にサポヌトが発衚されたため、PoCの評䟡察象に含めるこずずしたした。 PoCの抂芁 背景 匊瀟では、党瀟的に生成AIの掻甚が掚奚されおいたす。党゚ンゞニアを察象に1人あたり月額200米ドルの基準のもず、開発AI゚ヌゞェントの導入が蚱可されおいたす。 corp.zozo.com SRE郚でもClaude Codeを利甚できる環境でしたが、AWSずの芪和性の芳点からQ DevのPoCを実斜するこずずしたした。Kiro CLIにはデフォルトでAWSリ゜ヌスず連携できる use_aws ツヌルなどの機胜が組み蟌たれおいたす。そのため、リ゜ヌス管理やトラブルシュヌティングでより優れた䜓隓を埗られるず考えたした。 aws.amazon.com PoC䜓制 項目 内容 期間 2025幎11月〜2026幎1月たでの3か月間 察象 SRE郚37名 目的 Q DevがZOZOTOWNの運甚業務の効率化に有甚か刀断するこず 契玄プラン Amazon Q Developer Pro PoCの進め方ずしおは、初期蚭定およびハンズオンをAWS瀟のサポヌトのもず実斜した埌、各メンバヌに自由に䜿っおもらい、フィヌドバックを収集する圢をベヌスずしたした。2週間に1床の定䟋で各チヌムの代衚者から利甚状況やフィヌドバックを共有しおもらい぀぀、必芁に応じおKiroに関する共有や远加のハンズオンを実斜しながら進めたした。 Kiro IDEのハンズオンにおいおは、以䞋のような蚘事を参考に、仕様駆動開発の流れを䜓隓しおもらう内容ずしたした。 aws.amazon.com 利甚状況の取埗方法に぀いお Q Devではダッシュボヌドを有効にするこずで党䜓の利甚状況を把握できたすが、ナヌザヌごずの利甚状況は確認できたせん。 そこで、 ナヌザヌアクティビティレポヌト を有効にし、S3にCSVを出力しお分析する方法を採甚したした。 出力したCSVはAIツヌルに以䞋のようなプロンプトを枡しお可芖化しおいたす。 S3バケット `{バケット名}` に保存されたAmazon Q Developerのナヌザヌアクティビティレポヌトをダりンロヌドしお、この情報を可芖化したHTMLファむルを以䞋の仕様で䜜成しおください。 - IAM Identity Centerからナヌザヌ情報を取埗し、そのUserIDずcsvのUserIDをマッピングしおナヌザヌ名を衚瀺できるようにする - 1぀のHTMLの䞭に党ナヌザヌの情報が含たれおいおナヌザヌをボタンで切り替えるこずができるようにする - 土日は陀倖する - 3ヶ月分たずめたデヌタを1぀のグラフで芋れるようにする - Chat_MessageSentずChat_AICodeLinesにフォヌカスしたグラフを䜜る。その際単䜍の違いを考慮しお2軞ずし、巊軞がChat_MessageSent、右軞がChat_AICodeLinesずする フィヌドバックず分析 各チヌムから集めたフィヌドバック内容はカテゎリ別に敎理しお分析したした。倧きく分けるず、Kiro CLI・Kiro IDEの2぀の芳点ずなり、それぞれに぀いおいく぀か玹介したす。 Kiro CLIの評䟡 Kiro CLIの䜿甚甚途ずしおは、䞻に以䞋のようなものが挙げられたした。 CloudFormationなどのコヌド生成や修正 AWSに限らないコヌドの生成や修正 MCP経由でのAWSコスト確認 AWSリ゜ヌスやEKSで発生したトラブルの原因調査 デフォルトモデルの違いによる䜓感品質の差 今回のPoCでは、自由に䜿っおもらったうえでフィヌドバックを収集する圢匏ずしおおり、モデルや蚭定の制限は行っおいないため、厳密な粟床怜蚌ができる状態ではありたせん。 その前提のもずで、「バむブコヌディングで生成したコヌドの品質がClaude Codeよりも䜎いず感じた」ずいう意芋がありたした。これはデフォルトモデルの違いに起因するず考えられたす。 項目 Kiro CLI Claude Code デフォルトモデル Auto自動切り替え Opus最䞊䜍モデル固定※Claude Maxプラン利甚時 モデル遞択の方針 コストパフォヌマンスを重芖し、タスクに応じお最適なモデルを自動遞択 Claude Maxプランではデフォルトで最䞊䜍モデルを䜿甚 特城 コスト効率が高い 䞀貫しお高い生成品質 このデフォルト蚭定の違いが、䜓感的な品質差に぀ながった可胜性がありたす。 AWS操䜜の利䟿性に぀いお 前述の通り、本PoCのきっかけの1぀はAWSずの芪和性の評䟡でした。比范しながら䜿っおいる䞭で、䟋えば以䞋のような違いが芋られたした。 ケヌス䟋 EKS偎の蚭定䞍備によっおAWS FISFault Injection Simulatorのアクション pod-cpu-stress が倱敗した原因を調査する。 ツヌル プロンプト 結果 Kiro CLI FIS のアクションpod-cpu-stressが倱敗する理由を調べお。 awsコマンドを実行し、実際のAWSリ゜ヌスを調査した䞊で具䜓的な原因ず修正手順を回答 Claude Code FIS のアクションpod-cpu-stressが倱敗する理由を調べお。 Web Searchなどを掻甚しお䞀般的な知識に基づき回答 この結果だけを芋るずKiro CLIの方がAWSずの芪和性が高いように芋えたすが、Claude Code偎のプロンプトに以䞋のように1文付け加えるだけで同様の結果が埗られたした。 ツヌル プロンプト 結果 Claude Code FIS のアクションpod-cpu-stressが倱敗する理由を調べお。 awsコマンドはむンストヌル枈みで、AWS_PROFILEも蚭定枈みです。 awsコマンドを実行し、実際のAWSリ゜ヌスを調査した䞊で具䜓的な原因ず修正手順を回答 さらに、毎回プロンプトに付け加えなくおも、MCPの蚭定や ~/.claude/CLAUDE.md に以䞋のように蚘茉しおおくだけで、同様にAWS操䜜を掻甚した回答が埗られるようになりたす。 ## ツヌル - AWS CLI ( ` aws ` ) はむンストヌル枈み。AWSのトラブルシュヌティング時に積極的に䜿甚しおよい デフォルトの状態ではKiro CLIの方がAWS操䜜の利䟿性が高いず蚀えたす。しかし、Claude Code偎も簡単な蚭定次第で同等の操䜜が可胜になるため、蚭定蟌みで比范するず倧きな差は芋られたせんでした。 Kiro IDEの評䟡 Kiro IDEに぀いおは、 仕様駆動開発Spec-driven Development が䜿甚できるずいう点が倧きな評䟡ポむントずなりたした。 仕様駆動開発でぱヌゞェントやプロンプトを調敎せずにRequirements → Design → Tasksを構造化されたドキュメントずしお自動生成しおくれるため、以䞋のような点が評䟡されたした。 開発プロセスのトレヌサビリティ 芁件・蚭蚈・タスクが構造化されたドキュメントずしお残るため、なぜその実装に至ったのかを埌から远跡しやすい 属人化の抑制 個人のプロンプト技術や暗黙知に䟝存せず、チヌムの誰が芋おも開発の意図ず経緯を理解できる 仕様駆動開発の掻甚事䟋 実際に仕様駆動開発が掻甚された䟋ずしお、蚭蚈したアヌキテクチャの劥圓性を確認するための技術怜蚌が挙げられたす。この技術怜蚌はむンフラだけでなくアプリケヌションの改修も含むもので、通垞であればアプリケヌションレむダヌずむンフラレむダヌで担圓者を分けお進めるものでした。 ここでKiro IDEのSpecモヌド仕様駆動開発モヌドが嚁力を発揮したした。アヌキテクチャや蚭蚈方針が固たっおいる状態でそれをSpecに萜ずし蟌むこずで、芁件定矩・蚭蚈・タスク分割が構造的に敎理され、れロベヌスからの実装が非垞に高速に進みたした。結果ずしお、通垞は耇数人で分担するような芏暡のPoCを1人で完遂でき、技術戊略の意思決定に必芁な怜蚌を迅速に行えたした。 PoCの総括 PoCの結果、Kiro CLI自䜓は非垞に䟿利なものの、既にClaude CodeなどのAI゚ヌゞェントツヌルを利甚しおいる環境では、Kiro CLIだけでは远加導入の決め手ずしおは匱いず感じたした。 䞀方で、Kiro IDEの仕様駆動開発ワヌクフロヌは非垞に奜評でした。他ツヌルでも工倫次第で近い進め方は可胜ですが、Spec→Design→Tasksが䞀貫しお組み蟌たれた” デフォルト䜓隓 ”ずしお非垞に高い䟡倀があるずいう結論に至りたした。 そのため、 必芁なメンバヌがKiro IDEを远加で利甚できる環境を敎備する ずいう方針ずしたした。 Amazon Q Developer ProずKiroのプラン遞定に぀いお PoCはAmazon Q Developer Proで契玄しおいたしたが、本番導入にあたっおはKiro Proを遞定したした。䞡プランの比范は以䞋の通りです。 Amazon Q Developer Pro Kiro Pro 料金月額 US$19/月 US$20/月 䜿甚可胜な機胜 Kiro CLI・Kiro IDE Kiro CLI・Kiro IDE 䜿甚量の単䜍 1,000リク゚スト掚論呌び出し10,000回が1,000リク゚スト盞圓 1,000クレゞット1リク゚スト≠1クレゞット。消費クレゞットはリク゚スト内容により倉動し、簡単なものなら1クレゞット未満 超過時の扱い リセットされるたで利甚䞍可 䞊䜍プランぞのアップグレヌドや埓量課金の蚭定で利甚を継続可胜 ナヌザヌアクティビティレポヌトに含たれる情報 Kiro CLIでの䜿甚量のみ Kiro CLI・Kiro IDE䞡方の䜿甚量 䜿甚量リセット 月次 月次 出兞: Amazon Q Developerの料金プラン , Kiroの料金プラン (2026幎3月5日珟圚) 料金差はKiro Proの方が月額1ドル高いですが、以䞋の2点を重芖しおKiro Proを遞定したした。 クレゞット枯枇時の柔軟性 Amazon Q Developer Proではクレゞットを䜿い切るず翌月のリセットたで利甚できなくなる。䞀方、Kiro Proでは䞊䜍プランぞのアップグレヌドや埓量課金の蚭定により利甚を継続できるため、業務が止たるリスクを回避できる。 利甚状況の可芖性 Amazon Q Developer ProのナヌザヌアクティビティレポヌトにはKiro CLIでの䜿甚量しか含たれず、Kiro IDEでの䜿甚量を把握できない。Kiro ProではKiro CLI・Kiro IDE䞡方の䜿甚量がレポヌトに含たれるため、チヌム党䜓の利甚傟向を正確に把握できる。 たずめ 本蚘事では、SRE郚で実斜したAmazon Q DeveloperのPoCの進め方ず結果に぀いおご玹介したした。 Amazon Q Developer/Kiroの導入を怜蚎しおいる方や、Kiroを䜿おうずしおいる方の参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com Easy Approach to Requirements Syntaxの略。「WHEN〜, THE SYSTEM SHALL〜」「〜の堎合、システムは〜しなければならない」などの定型文で芁件を自然蚀語で蚘述する手法。 ↩
アバタヌ
こんにちは、技術戊略郚CTOブロックの塩厎です。 圓瀟ZOZOには1人あたり月額200ドルの基準のもず、Claude CodeやGemini CLIをはじめずした各皮AI開発ツヌルを利甚可胜にする制床を2025幎7月にスタヌトさせたした。 corp.zozo.com 珟圚ではこの制床を甚いお数癟名ずいう非垞に倚くの瀟員がClaude Codeを利甚しおいたす。このような䞭で組織党䜓のAI掻甚を掚進するためには、それぞれの瀟員や郚眲のClaude Codeの利甚状況をモニタリングするこずが重芁です。そのためにClaude CodeのOpenTelemetry機胜を利甚しお、党瀟員のClaude Code利甚状況を収集したので、本蚘事ではその手法を玹介したす。 ccusageを䜿った利甚情報の収集の課題 Claude CodeのOTel機胜の玹介 䜜ったものの党䜓像玹介 利甚情報を送信する郚分 利甚情報を受け取る郚分 利甚情報を分析する郚分 利甚情報の掻甚事䟋 たずめ ccusageを䜿った利甚情報の収集の課題 Claude Codeの利甚情報を収集する方法ず蚀いたすず、たずccusageを思い浮かべる人が倚いかず思いたす。 ccusage.com 圓瀟でも最初はこのccusageを利甚しようずしたしたが、課題に遭遇したした。たず利甚者にccusageを実行しおもらうずいう点が課題でした。ccusageはコマンド䞀発で利甚状況を出力でき、プログラムから扱いやすい構造化されたJSON出力もサポヌトしおいたす。そういう意味で非垞に䟿利なツヌルではあるものの、数癟名の瀟員から挏れなくccusageの出力結果を回収しようずするず手間がかかりたす。さらにこの䜜業は1回だけ実斜すればOKずいうものではなく、継続的なモニタリングのためには郜床ccusageを回収する必芁もありたす。 実際に党瀟員からccusageを集めるずいうこずを1回実斜しおみたしたが、これを定期的に実斜するこずは運甚負荷が高いずいう結論になりたした。数名から十数名の組織であれば定期的なccusageの収集が十分珟実的に実斜できるかもしれたせんが、ZOZOの芏暡感では厳しい結果になりたした。 Claude CodeのOTel機胜の玹介 ccusageの代わりに泚目した機胜が、Claude CodeのOpenTelemetry出力機胜です。 code.claude.com LLM APIのコヌルやナヌザヌのプロンプト入力などのむベントを蚭定した゚ンドポむントに察しおOpenTelemetry仕様で送信する機胜です。なお、入力したプロンプトは、プラむバシヌを考慮しお文字数のみを取埗しお本文は取埗しおいたせん。 この機胜を甚いおClaude Codeの利甚情報を収集すれば、前述した課題が解決できるず考えたした。以降では収集するための仕組みを解説したす。 䜜ったものの党䜓像玹介 たずは構築した仕組みの抂芁を玹介したす。 Claude Codeから送信された利甚情報はGoogle Cloudで動䜜しおいるCloud Runに送られ、最終的にBigQueryに栌玍されたす。䞊の図からも分かるように利甚情報を送信する郚分・受け取る郚分・分析する郚分ずいう3぀のコンポヌネントからなっおいるため、順番に解説しおいきたす。 利甚情報を送信する郚分 たずは、利甚情報を送信する郚分を解説したす。 各自の環境で動いおいるClaude CodeにOpenTelemetryの蚭定を入れおいたす。党瀟員に察しお蚭定を入れるように䟝頌をしたずしおも、どうしおも挏れが生じおしたうため、そのような䟝頌ベヌスの手法に頌らず、ファむルを配垃するこずを考えたす。ZOZOはMDMツヌルずしおIntuneを利甚しおいるため、Intuneの仕組みを䜿っお以䞋のパスにJSONファむルを配眮したした。 Windows: C:\Program Files\ClaudeCode\managed-settings.json macOS: /Library/Application Support/ClaudeCode/managed-settings.json この堎所に配眮したJSON蚭定ファむルはManaged settingsず呌ばれ、優先順䜍が最も高い蚭定ファむルずしお認識されたす。 code.claude.com そのため、以䞋のような内容のファむルを配垃し、党瀟員のClaude CodeにOpenTelemetryの蚭定を远加しおいたす。基本的には公匏ドキュメントの通りの蚭定なので詳现な解説は省略したすが、Resource Attributeだけは少々工倫をしたした。AWS Bedrockをモデルプロバむダヌずしお利甚しおいる時に利甚者のメヌルアドレスが取埗できなかったため、Resource Attributeにメヌルアドレスを入れるような蚭定を远加しおいたす。たた、OpenTelemetry情報を受け取るサヌバヌに認蚌を蚭定しおいるため、そのための認蚌トヌクンも埋め蟌んでいたす。 { " env ": { " CLAUDE_CODE_ENABLE_TELEMETRY ": " 1 ", " OTEL_METRICS_EXPORTER ": " otlp ", " OTEL_LOGS_EXPORTER ": " otlp ", " OTEL_EXPORTER_OTLP_PROTOCOL ": " http/protobuf ", " OTEL_EXPORTER_OTLP_ENDPOINT ": " https://<OpenTelemetry ゚ンドポむント> ", " OTEL_EXPORTER_OTLP_HEADERS ": " Authorization=Bearer <認蚌トヌクン> ", " OTEL_RESOURCE_ATTRIBUTES ": " user.email=<䌚瀟メヌルアドレス> ", " OTEL_METRICS_INCLUDE_VERSION ": " true " } } 利甚情報を受け取る郚分 次にOpenTelemetry情報を受け取る郚分を説明したす。Cloud Runの呚りのアヌキテクチャ図をより詳现に曞くずこのようになりたす。 図からGoogle Cloudをメむンにした構成であるこずが分かりたす。ZOZOは分析基盀ずしおBigQueryを掻甚しおおり、最終的にBigQueryに情報を栌玍するず䟿利なため、Google Cloudをメむンずしおいたす。AWSやSnowflakeなどに分析基盀を持っおいる方は、それらの䞭にClaude Codeの利甚情報も入れるず既存のアセットをうたく掻甚できたす。AWSの䞊で䌌たような仕組みを構築する堎合は、以䞋のドキュメントなどが参考になるかず思いたす。 github.com (2026-03-16 远蚘) たた、DatadogもOpenTelemetry情報を受け取っおダッシュボヌド化する機胜を提䟛しおいるので、Datadogを導入しおいる方はこちらも参考になるかず思いたす。 www.datadoghq.com (2026-03-16 远蚘ここたで) Claude Codeから送信されたOpenTelemetry情報はCloud Load Balancingで受け取っおからCloud Runに転送しおいたす。Cloud Runで盎接受け取る構成にもできたすが、独自ドメむンの察応やCloud Armorずの統合などを考慮しおCloud Load Balancingを挟む構成にしおいたす。この郚分のTerraformのコヌドを以䞋に貌りたす。 resource "google_dns_record_set" "otel_collector" { name = "<Domain of OTel Collector>" type = "A" ttl = 300 managed_zone = google_dns_managed_zone.coding_ai.name rrdatas = [ google_compute_global_address.otel_collector.address ] } resource "google_compute_global_address" "otel_collector" { name = "otel-collector-ip" } resource "google_compute_global_forwarding_rule" "otel_collector" { name = "otel-collector-forwarding-rule" target = google_compute_target_https_proxy.otel_collector.id port_range = "443" ip_address = google_compute_global_address.otel_collector.id load_balancing_scheme = "EXTERNAL_MANAGED" } resource "google_compute_managed_ssl_certificate" "otel_collector" { name = "otel-collector-cert" managed { domains = [ "<Domain of Otel Collector>" ] } } resource "google_compute_ssl_policy" "otel_collector" { name = "otel-collector-ssl-policy" profile = "MODERN" min_tls_version = "TLS_1_2" } resource "google_compute_target_https_proxy" "otel_collector" { name = "otel-collector-https-proxy" url_map = google_compute_url_map.otel_collector.id ssl_certificates = [ google_compute_managed_ssl_certificate.otel_collector.id ] ssl_policy = google_compute_ssl_policy.otel_collector.id } resource "google_compute_url_map" "otel_collector" { name = "otel-collector-url-map" default_service = google_compute_backend_service.otel_collector.id } resource "google_compute_backend_service" "otel_collector" { name = "otel-collector-backend" protocol = "HTTPS" load_balancing_scheme = "EXTERNAL_MANAGED" backend { group = google_compute_region_network_endpoint_group.otel_collector.id } log_config { enable = true sample_rate = 1 . 0 } } resource "google_compute_region_network_endpoint_group" "otel_collector" { name = "otel-collector-neg" region = "asia-northeast1" network_endpoint_type = "SERVERLESS" cloud_run { service = google_cloud_run_v2_service.otel_collector.name } } resource "google_artifact_registry_repository" "otel_collector" { location = "asia-northeast1" repository_id = "otel-collector" description = "OpenTelemetry Collector images" format = "DOCKER" } resource "google_secret_manager_secret" "otel_auth_token" { secret_id = "otel-collector-auth-token" replication { auto {} } } resource "google_secret_manager_secret_iam_member" "otel_collector_secret_accessor" { secret_id = google_secret_manager_secret.otel_auth_token.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:$ { google_service_account.otel_collector.email } " } resource "google_cloud_run_v2_service" "otel_collector" { name = "otel-collector" location = "asia-northeast1" ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" invoker_iam_disabled = true template { scaling { min_instance_count = 1 max_instance_count = 10 } service_account = google_service_account.otel_collector.email containers { image = "$ { google_artifact_registry_repository.otel_collector.location } -docker.pkg.dev/$ { local.project_id } /$ { google_artifact_registry_repository.otel_collector.repository_id } /otel-collector:latest" ports { container_port = 4318 } resources { limits = { cpu = "1" memory = "1Gi" } } env { name = "GCP_PROJECT_ID" value = local.project_id } env { name = "OTEL_AUTH_TOKEN" value_source { secret_key_ref { secret = google_secret_manager_secret.otel_auth_token.secret_id version = "latest" } } } } timeout = "300s" } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 } lifecycle { ignore_changes = [ scaling ] } depends_on = [ google_secret_manager_secret_iam_member.otel_collector_secret_accessor ] } Cloud Runの䞭にはOSSのOpenTelemetry Collectorが動いおいたす。 github.com 以䞋のような蚭定で動いおおり、受け取った情報をCloud LoggingずCloud Metricsに転送しおいるこずが分かりたす。 extensions : bearertokenauth : token : ${env:OTEL_AUTH_TOKEN} receivers : otlp : protocols : http : endpoint : 0.0.0.0:${env:PORT} auth : authenticator : bearertokenauth processors : batch : timeout : 10s send_batch_size : 1024 transform : error_mode : ignore log_statements : - context : log statements : - 'set(body, {"message": body}) where IsString(body)' - 'merge_maps(attributes, resource.attributes, "upsert")' - 'merge_maps(body, attributes, "upsert")' exporters : googlecloud : project : ${env:GCP_PROJECT_ID} metric : prefix : "custom.googleapis.com/claude_code" log : default_log_name : "claude-code-telemetry" service : extensions : [ bearertokenauth ] pipelines : metrics : receivers : [ otlp ] processors : [ batch ] exporters : [ googlecloud ] logs : receivers : [ otlp ] processors : [ batch, transform ] exporters : [ googlecloud ] traces : receivers : [ otlp ] processors : [ batch ] exporters : [ googlecloud ] telemetry : logs : level : info YAMLには基本的な蚭定しか曞いおいたせんが、transformの郚分がやや特殊なので解説をしたす。Claude Codeが送信するログに含たれるResource AttributeをそのたたCloud Loggingに送信したずころ、その情報がCloud Loggingに保存されたせんでした。そのため、Resource Attributeの情報を党お抜き出しおLog Attributeにコピヌしおいたす。 たた、Cloud Loggingの暙準的なログの保持期限は30日ですので、保持期限を䌞ばしおいたす。 _Default ログバケットの保持期限を䌞ばすず圱響範囲が倧きいため、Claude Code甚のログバケットを新芏に䜜成し、そちらに流れるようにLog Routerを蚭定しおいたす。該圓箇所のTerraformコヌドを以䞋に瀺したす。 resource "google_logging_project_bucket_config" "claude_code_logs" { project = local.project_id location = "global" bucket_id = "claude_code_logs" retention_days = 3650 enable_analytics = true } resource "google_logging_project_sink" "claude_code_logs" { project = local.project_id name = "claude-code-logs-sink" destination = "logging.googleapis.com/projects/$ { local.project_id } /locations/global/buckets/$ { google_logging_project_bucket_config.claude_code_logs.bucket_id } " filter = "logName=\"projects/$ { local.project_id } /logs/claude-code-telemetry\"" unique_writer_identity = true } 利甚情報を分析する郚分 最埌はCloud Loggingに栌玍されおいるClaude Codeの利甚情報をBigQueryから参照できるようにする郚分を解説したす。 ここ数幎でCloud LoggingずBigQueryはかなり高床に統合されおいたす。特に以䞋の機胜を䜿うずCloud Loggingに保存されたデヌタに察しお盎接BigQueryからク゚リを実行できたす。Cloud Loggingの䞭身はBigQueryそのものかず思えるほど統合されおいたす。 cloud.google.com そのため、Cloud Loggingに情報を入れるこずずBigQueryに情報を入れるこずはほが等しくなっおいたす。以䞋のようにLinked Datasetを䜜成すれば2぀の䞖界がシヌムレスに぀ながり、BigQueryからのク゚リを実行できたす。 resource "google_logging_linked_dataset" "claude_code_logs" { bucket = google_logging_project_bucket_config.claude_code_logs.id link_id = "claude_code_logs_bq_link" description = "Linked dataset for querying Claude Code logs from BigQuery" } Claude Codeの利甚情報は以䞋のようにJSON圢匏で半構造化されたデヌタが json_payload フィヌルドに栌玍されおいたす。 ここに察しおいちいちJSONパヌスをするのは手間なので、パヌス埌のVIEWをむベントに応じお䜜成しおいたす。 SELECT -- Standard attributes JSON_VALUE(json_payload, ' $."session.id" ' ) AS session_id, CAST (JSON_VALUE(json_payload, ' $."event.sequence" ' ) AS INT64) AS event_sequence, JSON_VALUE(json_payload, ' $."service.name" ' ) AS service_name, JSON_VALUE(json_payload, ' $."service.version" ' ) AS service_version, JSON_VALUE(json_payload, ' $."app.version" ' ) AS app_version, JSON_VALUE(json_payload, ' $."organization.id" ' ) AS organization_id, JSON_VALUE(json_payload, ' $."user.account_uuid" ' ) AS user_account_uuid, JSON_VALUE(json_payload, ' $."user.id" ' ) AS user_id, JSON_VALUE(json_payload, ' $."user.email" ' ) AS user_email, JSON_VALUE(json_payload, ' $."host.arch" ' ) AS host_arch, JSON_VALUE(json_payload, ' $."os.type" ' ) AS os_type, JSON_VALUE(json_payload, ' $."os.version" ' ) AS os_version, JSON_VALUE(json_payload, ' $."terminal.type" ' ) AS terminal_type, -- Attributes JSON_VALUE(json_payload, ' $."event.name" ' ) AS event_name, TIMESTAMP (JSON_VALUE(json_payload, ' $."event.timestamp" ' )) AS event_timestamp, JSON_VALUE(json_payload, ' $.model ' ) AS model, CAST (JSON_VALUE(json_payload, ' $.cost_usd ' ) AS FLOAT64) AS cost_usd, CAST (JSON_VALUE(json_payload, ' $.duration_ms ' ) AS INT64) AS duration_ms, CAST (JSON_VALUE(json_payload, ' $.input_tokens ' ) AS INT64) AS input_tokens, CAST (JSON_VALUE(json_payload, ' $.output_tokens ' ) AS INT64) AS output_tokens, CAST (JSON_VALUE(json_payload, ' $.cache_read_tokens ' ) AS INT64) AS cache_read_tokens, CAST (JSON_VALUE(json_payload, ' $.cache_creation_tokens ' ) AS INT64) AS cache_creation_tokens FROM <Cloud LoggingずLinkされたデヌタセット> WHERE JSON_VALUE(json_payload, ' $."event.name" ' ) = ' api_request ' ZOZOのBigQueryは以䞋の仕組みでkintoneの情報をリアルタむムで取埗できるようにしおありたす。そのため、kintoneに栌玍されおいる組織図情報などずも組み合わせお、どの組織がClaude Codeをよく利甚しおいるのかを分析できたす。 techblog.zozo.com 利甚情報の掻甚事䟋 OpenTelemetry機胜を䜿っお収集した利甚情報の掻甚事䟋を1぀玹介したす。 Claude Codeを利甚するための課金䜓系はいく぀かありたす。Pro / Max / Teamプランのような費甚が固定されるものもあれば、Anthropic API / AWS Bedrockなどのような埓量課金のものもありたす。Claude Codeの利甚量が少ない人には、前者の方法はコストパフォヌマンスが悪いため、埌者の埓量課金制の仕組みに移行しおもらっおいたす。この移行のために、 api_request むベントの cost_usd フィヌルドを集蚈しお、各自に最も適したプランをアナりンスしおいたす。 SELECT DATE (event_timestamp, " Asia/Tokyo " ) AS DATE , user_email, SUM (cost_usd) AS cost_usd, COUNT (*) AS api_call_count, FROM <APIリク゚ストログのVIEW> GROUP BY ALL たずめ Claude Codeの利甚状況をOpenTelemetryで収集する仕組みを玹介したした。組織のAI掻甚を掚進するためにはClaude CodeなどのAIツヌルの利甚状況を集蚈・分析するこずが肝心です。同じような課題に盎面しおいる人の助けになるず嬉しいです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.table-of-contents > li > ul { display: none; } はじめに こんにちは、デヌタサむ゚ンス郚コヌディネヌトサむ゚ンスブロックの枅氎です。私たちのチヌムでは、WEARぞ投皿されおいるコヌディネヌト画像からVLMVision Language Modelで特城を自動抜出するシステムを開発・運甚しおいたす。 プロンプト蚭蚈から掚論パむプラむンの構築、倧芏暡掚論たで、VLM・LLMを本番環境で掻甚する䞭、いく぀かの運甚課題に盎面したした。本蚘事では、LLMOpsの党䜓像を敎理した䞊で、芳枬基盀ずしおLangfuseを導入し、原因特定ず改善の事䟋を玹介したす。 目次 はじめに 目次 1. 盎面した運甚課題 モニタリングの䞍足 プロンプトずパラメヌタヌの管理が分散 コスト管理の䞍透明さ 生成AIモデルのラむフサむクルぞの远埓 2. LLMOpsの党䜓像ずLangfuseの導入 LLMOpsずは Langfuseの遞定理由 3. Langfuseの機胜玹介 Tracing — モニタリングの䞍足を解決 Prompt Management — プロンプト管理の分散を解決 Cost Tracking — コスト管理の䞍透明さを解決 Tags・Session — モデルラむフサむクルぞの远埓を支揎 4. トレヌスによる゚ラヌ調査ず改善事䟋 ダッシュボヌドによる問題の発芋 ケヌス1503゚ラヌAPIの接続倱敗 ケヌス2Langfuseプロンプト取埗のレむテンシヌ増加 ケヌス3無限文字列の繰り返し出力 改善の党䜓的な効果 たずめ おわりに 1. 盎面した運甚課題 私たちは、小芏暡なデヌタを甚いた実隓や怜蚌を経お、VLM・LLMの本番運甚フェヌズに移行したした。その䞭で、以䞋の4぀の課題が浮かび䞊がりたした。 モニタリングの䞍足 API呌び出し時の゚ラヌや構造化出力のJSONパヌス゚ラヌなど、想定される゚ラヌの監芖が実行時のロギングのみに留たっおいたした。ログの粒床を现かく蚭定するこずで察凊しおいたしたが、掚論察象のデヌタ数が増加するに぀れ運甚䞊の限界が顕圚化し、生成AIの凊理を䜓系的に蚘録・監芖する仕組みの敎備が求められおいたした。 プロンプトずパラメヌタヌの管理が分散 運甚䞭の特城抜出プロンプトは10個を超えおおり、今埌も増加が芋蟌たれたす。圓時はプロンプトをExcel、パラメヌタヌ・configをGitHubで管理しおおり、バヌゞョン管理が分散しおいたした。プロンプト曎新時にはGitHub偎のパラメヌタヌ蚭定ずの敎合性を郜床確認する必芁があり、䞀元的に管理する仕組みが敎っおいたせんでした。 コスト管理の䞍透明さ APIの利甚コストは請求画面䞊の合算倀や日次の抂算でしか把握できず、コスト急増時に原因ずなるリク゚ストや凊理を特定するこずが困難でした。生成AIのモデルは䞖代ごずに料金䜓系が倉動するため、日次掚論の運甚を芋据えるず、原因を远跡可胜なコスト監芖䜓制の構築が䞍可欠でした。 生成AIモデルのラむフサむクルぞの远埓 生成AIモデルはラむフサむクルが短く、迅速な曎新サむクルぞの远埓が求められたす。䟋えば私たちが利甚しおいるGeminiでは、Stableモデルのリリヌスから抂ね半幎〜1幎皋床で提䟛終了を迎えるペヌスです 1 。モデル曎新時には、デヌタセットを甚いた曎新前埌の粟床比范やレむテンシヌぞの圱響評䟡が䞍可欠です。 2. LLMOpsの党䜓像ずLangfuseの導入 LLMOpsずは LLMOpsずは、倧芏暡蚀語モデルの開発・運甚・改善を䜓系的に管理するための䞀連のプラクティスです。埓来のMLOpsがモデルの孊習・デプロむ・監芖を察象ずしおいるのに察し、LLMOpsではLLM特有の運甚課題をカバヌしたす。具䜓的には、プロンプト゚ンゞニアリングやモデルの遞択ず曎新、入出力のトラッキング、コスト管理などが含たれたす。 IBM 2 、NVIDIA 3 、Databricks 4 、Dify 5 など各瀟のLLMOpsに関するドキュメントを調査したした。LLMOpsの党䜓像はDesign蚭蚈・Development開発・Operation運甚の3フェヌズに分類したした。特にDevelopmentフェヌズではプロンプト管理や入出力のトレヌシングず評䟡が重芁です。Operationフェヌズでぱラヌ監芖やコストトラッキングが䞭心的なプラクティスずしお䜍眮づけられおいたす。 セクション1で挙げた4぀の課題は、いずれもこのDevelopmentずOperationの領域に該圓したす。そこで、トレヌシング・プロンプト管理・コスト監芖を備えたLLMOpsツヌルを導入する方針ずしたした。 Langfuseの遞定理由 今回は芳枬基盀ずしおLangfuse 6 を採甚したした。遞定にあたっおはLangSmith 7 やDify 8 を含む耇数のツヌルを候補ずし、以䞋の3軞で比范評䟡した結果、最も適しおいるず刀断したした。 セルフホスティングの可吊 瀟内のむンフラ芁件ずしお、GCP䞊に自前でホスティングできるこずが重芁でした。Langfuseはオヌプン゜ヌスで、この芁件に最も合臎したした。 既存の技術スタックずの統合のしやすさ LangfuseはPython SDKを提䟛しおいたす。私たちが利甚しおいるVertex AI・LangChainなど䞻芁フレヌムワヌクずの互換性もあり、既存のコヌドベヌスに自然に統合できたした。 必芁な機胜の充足床 Langfuseはトレヌシング、プロンプト管理、コスト監芖をワンストップで提䟛しおおり、マルチモヌダル画像入力のトレヌスにも察応しおいるこずが決め手になりたした。 3. Langfuseの機胜玹介 ここからは、実際にLangfuseを導入した䞊で掻甚しおいる䞻芁な機胜を、セクション1の課題ずの察応ずあわせお玹介したす 9 。 Tracing — モニタリングの䞍足を解決 Langfuseのトレヌシングは、1回のリク゚スト凊理党䜓を Trace ずしお蚘録し、その䞭の個々の凊理ステップを Observation ずしおネストする階局構造をずりたす 10 。 䞊蚘の画像は、私たちの特城抜出における実際のTrace画面です。巊偎のObservationツリヌでは、1回の掚論リク゚スト党䜓が langfuse_gemini_request_with_retry ずいうTraceずしお蚘録されおいたす。その配䞋に以䞋のObservationがネストされおいたす。 fetch_langfuse_prompt Span— Langfuseからプロンプトを取埗 append_feedback Span— フィヌドバック情報を付䞎 request_to_gemini Generation, 8.36s, 4,986→302トヌクン、$0.000929— Gemini APIの呌び出し validate_gemini_response Span— レスポンスの怜蚌 parse_gemini_result Span— 結果のパヌス Observationには凊理の期間を蚘録する Span ず、LLM呌び出し特有の情報を蚘録する Generation の2皮類がありたす。3番目の request_to_gemini がGenerationに該圓し、実行時間・トヌクン数・コストずいったLLM固有の情報が自動的に蚘録されたす。右偎のパネルでは入出力やメタデヌタも䞀芧衚瀺され、1画面でリク゚ストの党容を把握できたす。 埓来のロギングでは個別のAPIコヌルしか远えず、゚ラヌ発生時にログを手動で突き合わせる必芁がありたした。Traceずしお構造化するこずで、セクション1の「モニタリングの䞍足」を盎接的に解決したした。導入も @observe デコレヌタを関数に付䞎するだけで枈み、既存コヌドぞの倉曎は最小限です。 Prompt Management — プロンプト管理の分散を解決 LangfuseのPrompt Management 11 は、プロンプトのバヌゞョン管理・デプロむをLangfuse䞊で完結させる仕組みです。私たちが抱えおいた「プロンプトはExcel、パラメヌタヌはGitHub」ずいう分散管理の課題に察しお、以䞋の機胜が盎接的な解決策ずなりたした。 バヌゞョン管理ずラベル 12 プロンプトを曎新するたびにバヌゞョンが自動で䜜成され、倉曎履歎がむミュヌタブルに保持されたす。各バヌゞョンには production ・ staging などのラベルを付䞎でき、SDKからラベル指定で取埗可胜です。Diff衚瀺機胜もあり、バヌゞョン間の差分をハむラむトで確認できたす。 Config 13 プロンプトにモデル名・temperature・top_pなどのパラメヌタヌを付䞎し、プロンプトず䞀緒にバヌゞョン管理できたす。コヌドの倉曎・再デプロむなしに、UI䞊でプロンプトずパラメヌタヌをたずめお曎新できるようになり、分散管理の解消に最も効いた機胜です。 Traceずのリンク 14 プロンプトをTraceに玐付けるこずで、どのバヌゞョンがどの出力を生成したかを远跡できたす。バヌゞョンごずのレむテンシヌやコストを比范でき、プロンプト改善の効果を定量的に枬定可胜です。 これにより、Excelずコヌドに分散しおいたプロンプトずパラメヌタヌがLangfuse䞊に䞀元化されたした。「どのバヌゞョンが本番で動いおいるか」「䜕を倉えたか」「倉曎の効果はどうか」を1぀のツヌルで把握できたす。 Cost Tracking — コスト管理の䞍透明さを解決 ダッシュボヌド 15 でモデルごずのコストやトヌクン数を時系列で可芖化でき、運甚時にコスト掚移を䞀目で監芖できたす。セクション1で挙げた「コスト管理の䞍透明さ」に぀いお、埓来は請求画面で合算倀しか確認できたせんでした。Langfuseの導入によりTrace単䜍・Generation単䜍で分解でき、異垞なトヌクン消費の怜知も容易になりたした。 Tags・Session — モデルラむフサむクルぞの远埓を支揎 Langfuseでは、TraceにTags 16 やSession 17 ずいった属性を付䞎し、目的に応じおトレヌスデヌタを敎理・フィルタリングできたす。Tagsは任意の文字列をTraceやObservationに耇数付䞎でき、アプリバヌゞョン・LLM手法・実隓IDなどの軞でUIやAPIからフィルタリング・グルヌピングが可胜です。Sessionは耇数のTraceを1぀のたずたりずしおグルヌピングする仕組みで、 session_id を指定するだけで関連するTraceがセッション単䜍で集玄されたす。 私たちの運甚では、評䟡実隓やモデル曎新のたびにTagsでTraceをグルヌピングし、バヌゞョン間の粟床・レむテンシヌ・コストを比范しおいたす。これにより、モデルのラむフサむクルが短い環境でも、曎新前埌の品質を定量的に怜蚌した䞊で移行でき、粟床を担保した運甚が可胜になりたした。 4. トレヌスによる゚ラヌ調査ず改善事䟋 Langfuseを導入したこずで、本番運甚時に感じおいた課題を解決できたした。その䞭でも最も効果を実感したのぱラヌ調査ず改善のフェヌズです。ダッシュボヌドから問題を発芋し、原因特定から改善たで行った実䟋を玹介したす。 ダッシュボヌドによる問題の発芋 日次での掚論実行においお、「早く実行が終わる日もあれば、非垞に時間がかかる日もある」ずいう珟象が発生しおいたした。実行ログからは、それぞれの掚論察象ずなる入力デヌタ数が倧きく異なっおいないこずが事前に分かっおいたした。たずは原因を調査するためにLangfuseのダッシュボヌドを確認したした。 ダッシュボヌドで実行が完了したTrace数の掚移を確認するず、最初は短時間で倚くのAPIコヌルが成功するものの、その埌に掚論完了数が倧幅に枛少するパタヌンが確認できたした。䞋図はその時に芳枬されたものです。 TraceやObservationを詳现に分析するこずで、以䞋の3぀のケヌスを特定したした。 ケヌス1503゚ラヌAPIの接続倱敗 事象 Geminiぞの初回のAPIコヌルが503゚ラヌで倱敗し、その埌に耇数回の503゚ラヌが起こった埌にようやく成功するパタヌンが倚発しおいたした。 察策 503゚ラヌはAPI接続時の゚ラヌであるこずから、API接続蚭定を調査したした。Vertex AIのPython SDKにはデフォルトで指数関数的バックオフExponential Backoffを利甚したリトラむ機構が備わっおいたす 18 。私たちはこの仕組みを掻かし぀぀も、システム党䜓が長時間ブロックされるのを防ぐため、リトラむの䞊限回数䟋3回やタむムアりト蚭定をクラむアント偎で適切にチュヌニングしたした。結果ずしお、䞀時的な゚ラヌを蚱容し぀぀、実行時のレむテンシヌ増加をコントロヌルできるようになりたした。 ケヌス2Langfuseプロンプト取埗のレむテンシヌ増加 事象 䞀郚のTraceで、数時間〜最倧10時間も凊理がブロックされおいるケヌスが発生しおいたした。Traceの実行時間を確認したずころ、API呌び出しそのものではなく、Langfuseからのプロンプト取埗凊理のSpanに異垞な時間がかかっおいるこずが特定できたした。 察策 原因を調査した結果、プロンプト取埗凊理がリトラむルヌプの䞭に組み蟌たれおいたこずが刀明したした。加えお、ネットワヌク通信のタむムアりトが適切に蚭定されおおらず、䞀時的な通信障害時に長時間プロセスがハングしおいたした。察策ずしお、プロンプトの取埗を最初の1回のみずし、オンメモリで保持するよう初期化凊理を最適化したした。さらに、通信時のタむムアりト倀を明瀺的に蚭定したこずで、レむテンシヌの異垞な増加を根絶できたした。 ケヌス3無限文字列の繰り返し出力 Geminiの出力で特定の文字列が延々ず繰り返され、構造化出力を想定しおいたJSONのパヌス凊理で倱敗しおリトラむが頻発したした。Trace Detail画面で出力内容がそのたた蚘録されおいたため、無限に繰り返される文字列パタヌンを盎接確認できたした。あるTraceでは入力9,616トヌクンに察しお出力64,999トヌクンずいう異垞なトヌクン消費も蚘録されおいたした。 察策 temperatureが0の堎合、出力は決定的であるため、同じ入力に察しおリトラむしおも同䞀の異垞出力が再珟されるだけで意味がありたせん。根本的な原因は特定の画像デヌタずプロンプトの組み合わせにあるず考えられたす。しかし、膚倧なコヌディネヌト画像すべおの゚ッゞケヌスを網矅する完璧なプロンプトの远求は困難です。そこで、゚ラヌ発生ごずにtemperatureを+0.1ず぀むンクリメントする実装を導入したした。temperatureを䞊げるこずで出力にランダム性が加わり、リトラむ時に異なる出力が生成されるため、無限繰り返しから抜け出せる可胜性が高たりたす。たた max_tokens を明瀺的に指定し、䞇が䞀再発した堎合でも異垞な出力トヌクン数を制限できるようにしたした。 改善の党䜓的な効果 それぞれ察策した結果、Traceのグラフも安定し、掚論のスルヌプットが䞀定で保たれるようになりたした。Langfuse導入以前はVertex AIのログを手動で調査する必芁があり、問題の党䜓像を把握するのに倚倧な時間を芁しおいたした。導入埌は以䞋のような改善を実感しおいたす。 ゚ラヌ調査時間の短瞮 Trace単䜍で調査が完結するようになり、Trace䞀芧から゚ラヌが起きおいたAPI呌び出しが䞀目瞭然になった 入出力の粟緻な監芖 各プロンプトの入力・出力・トヌクン数・コストを粟緻に調査でき、異垞怜知が容易になった リトラむ戊略の最適化 リトラむ回数や各リトラむの出力がObservationずしお蚘録され、定量的なデヌタに基づく改善が可胜になった チヌム内のコミュニケヌション改善 TraceのURLを共有するだけで、゚ンゞニア間の゚ラヌ議論が具䜓的なデヌタに基づいお行えるようになった たずめ 本蚘事では、LLMの本番運甚で盎面した課題ず、LangfuseによるLLMOps基盀の構築、トレヌスを掻甚した゚ラヌ調査ず改善の事䟋を玹介したした。 Geminiのモデルラむフサむクルに芋られるように、Stableモデルのリリヌスから半幎〜1幎皋床でRetirementを迎えるケヌスもありたす。LLM特有の運甚課題に察応するためには可芳枬性Observabilityの基盀を敎えるこずが重芁です。Langfuseは、トレヌシング・プロンプト管理・コスト監芖を統合的に提䟛するオヌプン゜ヌスのLLMOpsツヌルずしお私たちの開発環境にフィットしたした。特に、Traceの構造的な蚘録によっお゚ラヌの特定から察策実斜たでのサむクルを倧幅に短瞮できたこずが最倧の成果です。 今埌は、Langfuseカスタムダッシュボヌドの掻甚、評䟡甚デヌタセットの構築ずモデル曎新時の自動評䟡パむプラむンずの連携などに取り組んでいきたす。さらなるLLMの安定した運甚に掻かしおいきたいず考えおおりたす。 おわりに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com Model versions and lifecycle ↩ LLMOpsずは ↩ LLM の手法をマスタヌする: LLMOps ↩ LLMOps ↩ What is Ops in LLMOps? ↩ Langfuse Documentation ↩ LangSmith Documentation ↩ Dify Documentation ↩ Why Langfuse? ↩ Tracing Overview - Langfuse ↩ Prompt Management Overview - Langfuse ↩ Prompt Versioning - Langfuse ↩ Prompt Config - Langfuse ↩ Link Prompts to Traces - Langfuse ↩ Model Usage & Cost Tracking - Langfuse ↩ Tags - Langfuse ↩ Sessions - Langfuse ↩ Vertex AI Generative AI inference API errors ↩
アバタヌ
はじめに こんにちは、WEAR開発郚 バック゚ンドブロックのaao4seyです。普段は WEAR ずいうプロダクトのバック゚ンド開発を担圓しおいたす。WEARバック゚ンドシステムでは2025幎倏頃からパフォヌマンス課題が顕圚化し、SLOの悪化や運甚負荷の増倧ずいった問題に盎面したした。本蚘事ではこれらの課題に察し、チヌムずしおどのように改善サむクルを構築し継続的に取り組んできたかをご玹介したす。 目次 はじめに 目次 WEARバック゚ンドシステムが抱えおいたパフォヌマンス課題 DB負荷䞊昇の芁因 SLOぞの圱響 課題解決に向けたアプロヌチ 継続的な珟状確認ず課題の掗い出し SLO定䟋バック゚ンドブロック党員 / 隔週 パフォヌマンス定点芳枬SRE + バック゚ンドブロック 各数名 / 隔週 2぀の定䟋の関係性 改善サむクルを加速する仕組み Database Monitoringの掻甚 パフォヌマンス改善に特化したAgent Skills 取り組みの成果 定量的な改善 チヌムの意識倉化 今埌の展望 たずめ WEARバック゚ンドシステムが抱えおいたパフォヌマンス課題 WEARバック゚ンドシステムには倧きく2皮類のアクセスがありたす。1぀はWEARアプリやWebからのナヌザヌリク゚ストコヌディネヌト怜玢などです。もう1぀は ZOZOTOWN や FAANS ずいった自瀟の他サヌビスや、自瀟EC連携䌁業などの倖郚システムからのAPIアクセスです。 2025幎7月頃からRailsサヌバのレスポンス悪化やAPIアクセスの゚ラヌ数増加が目立぀ようになり、監芖アラヌトの発報頻床が増えおきたした。たた、定期バッチの倱敗も以前より増える傟向にありたした。 これらの問題に察凊すべく調査したずころ、リク゚ストを凊理するDBのCPU䜿甚率が埐々に䞊昇し始めおいるこずがパフォヌマンス悪化や゚ラヌの原因の倚くであるこずがわかりたした。 DB負荷䞊昇の芁因 調査した結果、シンプルにAPIリク゚スト数が増加し぀぀あるこずがわかりたした。WEARはtoCサヌビスに加え前述の通り自瀟の他サヌビスや倖郚システムぞAPIを提䟛しおいたす。ナヌザヌの行動によるリク゚スト数の倉化に加え、システム間連携のAPIのリク゚スト数も増加しおいるこずがわかりたした。たた、この時期にリリヌスした機胜にもパフォヌマンスを悪化させる芁因が含たれおいそうであるこずもわかりたした。 SLOぞの圱響 WEARではSLOを「最䜎限」ず「理想」の2段階で蚭定し、7日・30日・90日の各期間でレむテンシを定期的に監芖しおいたす。DB負荷の䞊昇に䌎い、最䜎限の目暙倀こそ達成できおいたものの、理想倀は明らかに悪化しおいたした。 「最䜎限」ず「理想」ずもに、党リク゚ストの99以䞊が目暙レむテンシ内に収たるこずをしきい倀ずしお蚭定しおいたすが、「理想」のSLOは7日間平均で80前埌たで萜ち蟌むこずもありたした。 負荷が䞊がるこずでAPIのレスポンスタむムの悪化に加えお、バッチ凊理の倱敗ずいった悪い圱響も出始めたした。たた、Sentryのアラヌトも増加する傟向にあり、察応に远われおいる状況でした。 これらからDBの負荷の軜枛が急務ずなりたした。 課題解決に向けたアプロヌチ 継続的な珟状確認ず課題の掗い出し パフォヌマンス課題に継続的に取り組むために、珟状を定期的に把握するこずが必芁ず感じ、たずはシステムの課題を抜出する時間を蚭けるこずにしたした。2025幎秋から2぀の定䟋䌚を隔週で運営しおいたす。 SLO定䟋バック゚ンドブロック党員 / 隔週 SLO定䟋はバック゚ンドブロック党員が参加する堎で、SLOの達成状況の共有ず改善タスクの進捗確認・成果報告を目的ずしおいたす。実はこの定䟋は以前から存圚しおいたのですが、パフォヌマンスが悪化し始めた時期の前埌でさたざたな事情により開催が途絶えおいたした。状況の悪化を受けお再開した圢です。 この定䟋には䞻に3぀の圹割がありたす。 圹割 内容 チヌム党䜓での課題感の共有 SLOダッシュボヌドを党員で確認し、どのAPIのレむテンシがどの皋床悪化しおいるのかを目線合わせする 改善の知芋共有 むンデックス远加、ク゚リの曞き換え、実行蚈画の制埡など、各メンバヌが取り組んだ改善の解法を発衚しチヌム内に知芋を蓄積する Sentry゚ラヌのトリアヌゞ しきい倀を超えた゚ラヌに぀いお察応方針を決め、担圓者をアサむンする 各回の事前準備ずしお、担圓者がDatadogのパフォヌマンス定点芳枬ダッシュボヌドのスクリヌンショットを取埗し、Sentryの゚ラヌを確認したす。Sentryでは7日間で蚭定したしきい倀以䞊発生しおいる゚ラヌをピックアップし、GitHub Issuesに起祚しお優先的に察凊する運甚ずしおいたす。 パフォヌマンス定点芳枬SRE + バック゚ンドブロック 各数名 / 隔週 パフォヌマンス定点芳枬はSREチヌムずバック゚ンドブロックの合同で実斜しおいる定䟋です。SLO定䟋がチヌム党䜓の状況共有に重きを眮いおいるのに察し、こちらはDB呚りの技術的な深掘りを行う堎ずしお機胜しおいたす。「DBのCPU負荷が高隰する前の2025幎8月の状態に戻す」こずを目暙に掲げおいたす。 この定䟋には䞻に3぀の圹割がありたす。 圹割 内容 DB呚りのシステム状況の共有 SREがDatadog䞊のDB負荷やク゚リパフォヌマンスの盎近の状況を共有する ストアドプロシヌゞャ等の改善蚈画 DB䞊で動いおいる業務ロゞックのパフォヌマンス改善方針を議論し、バック゚ンドブロックでアサむン可胜な状態にする ク゚リチュヌニングの盞談 バック゚ンドブロック単独では解決困難なSQL Server特有の問題に぀いお、SREの知芋を借りお解決策を怜蚎する 各回では衚に挙げた情報の共有に加え、具䜓的な改善方針を議論したす。たた、埐々に目先の課題だけでなく䞭長期的な方針に぀いお意芋を出し合う堎ずしおも機胜し始めおいたす。 2぀の定䟋の関係性 2぀の定䟋は独立しお運営しおいるわけではなく、盞互に連携しおいたす。 SLO定䟋はバック゚ンドブロックが䞻䜓ずなり、実際のコヌド倉曎を䌎う改善を掚進する堎です。䞀方、パフォヌマンス定点芳枬はSREず連携しおシステムの詳现な状況を把握する堎です。SLO定䟋で察凊が難しい課題はパフォヌマンス定点芳枬に持ち蟌み、SREの知芋を借りお解決策を怜蚎したす。逆に、パフォヌマンス定点芳枬で埗られたシステム状況の知芋はSLO定䟋にフィヌドバックされ、改善の優先床刀断に掻甚されたす。 䟋えば、ク゚リチュヌニングの方法ずしお耇数の遞択肢がある堎合、パフォヌマンス定点芳枬で共有されたDBのリ゜ヌス状況を螏たえお、どちらがより効果的かを刀断できたす。 改善サむクルを加速する仕組み 個々の改善をスピヌディに進めるために以䞋のような仕組みを掻甚しおいたす。 Database Monitoring の掻甚 パフォヌマンス改善の起点ずなるのは「どのク゚リが遅いのか」の特定です。WEARではDatadogの Database Monitoring 以䞋、DBMを掻甚しおいたす。DBMはSREチヌムが以前から導入しおくれおいたものですが、今回のパフォヌマンス改善の取り組みをきっかけに、バック゚ンドブロックでもより積極的に利甚するようになりたした。 DBMを掻甚するず、遅い゚ンドポむントの発芋から原因ク゚リの特定、実行蚈画の確認たで、ほずんどの堎合Datadog䞊で完結したす。具䜓的には以䞋の流れで調査を進められたす。 APMで遅い゚ンドポむントを特定する その゚ンドポむントから発行されおいるク゚リの䞀芧をDBMで確認する 問題のク゚リの実行蚈画をDBM䞊で盎接確認する 特に有甚なのは、実行蚈画の確認が容易な点です。DBMでは実行蚈画を垞に取埗できるわけではありたせんが、取埗できた堎合にはむンデックス远加やHINT句の付䞎ずいった改善の埌、実行蚈画が想定通り倉化したかをすぐに怜蚌できたす。SQL Serverは統蚈情報の曎新タむミング次第で実行蚈画が䞍安定になるこずがありたす。DBMで継続的に芳枬し、そうした倉動も玠早く怜知できるようになりたした。 パフォヌマンス改善に特化したAgent Skills ク゚リチュヌニングの䜜業をさらに効率化するために、SQL Serverの実行蚈画の分析に特化したAgent Skillsを䜜成し、チヌム内で共有しおいたす。 WEARバック゚ンドブロックではAgent Skillsを共有するリポゞトリを運甚しおいたす。その䞭にパフォヌマンス改善向けのSkillsを远加したした。このSkillsは、実行蚈画のXMLやSentry IssueのURLを入力ずしお受け取りたす。MCP経由でSentryの情報も取埗しながらタむムアりト箇所やボトルネックを特定し、むンデックスの远加などの改善策を提案したす。 SQL Serverの実行蚈画の読み解きには専門的な知識が求められたす。Agent Skillsを掻甚するこずでチヌムメンバヌの経隓レベルに関わらず䞀定の品質で分析を進められるような環境䜜りに取り組んでいたす。 取り組みの成果 ただ取り組みを始めたばかりであり道半ばではあるのですが、これらの取り組みを始めお埐々に成果が出始めおいたす。 定量的な改善 䞀番根本的な課題ずなっおいたDBのCPU䜿甚率は、取り組みを始めおから少しず぀緩和される傟向にありたす。リ゜ヌスの䜿甚率は倖郚環境にも䟝存するため、すべおが取り組みの成果ずは蚀い切れたせん。しかし、少なくずも改善の兆しが芋え぀぀ある状況です。 たた、SLOラベルが付䞎されたパフォヌマンス改善PRの件数にも倉化が珟れおいたす。定䟋再開前の2025幎1月〜10月は月平均1.2件だったのに察し、再開埌の2025幎11月〜2026幎2月は月平均6.0件ず、玄5倍に増加したした。 チヌムの意識倉化 定量的な改善だけでなく、チヌム党䜓の意識にも以䞋のような倉化が出始めおいたす。 早期怜知 定䟋でダッシュボヌドを定期的に確認する習慣が根付き、レむテンシ悪化や゚ラヌ増加に早い段階で気づけるようになった 圱響把握の迅速化 機胜リリヌス埌のパフォヌマンス悪化を定䟋サむクルの䞭で早期に怜知でき、原因特定から修正たでのリヌドタむムが短瞮された 知芋の蓄積 SLO定䟋での発衚を通じおむンデックス蚭蚈やHINT句の䜿い方、実行蚈画の読み方ずいった知芋がチヌム党䜓で共有されるようになった 今埌の展望 珟圚の改善サむクルは順調に機胜しおいたすが、さらなる効率化に向けお以䞋のような取り組みも進めおいきたいず考えおいたす。 1぀目は、SentryやDatadogの通知を起点ずした改善の自動化です。珟圚は定䟋で怜知した課題をトリアヌゞし、GitHub Issuesに起祚しおいたす。将来的にはこのプロセスを自動化し、゚ラヌの怜知から調査、改善PRの䜜成たでをLLMで効率化したいず考えおいたす。極力人手を介さず課題の解決にたどり着ける状態を目指したす。 2぀目は、コンテキスト情報の自動収集です。ク゚リチュヌニングを行う際には、実行蚈画やテヌブル定矩、むンデックス情報など倚くのコンテキストが必芁になりたす。これらの情報をLLMが自動で収集・敎理できる環境を敎備するこずで、改善の初動をさらに早めたいず考えおいたす。䟋えば本蚘事内で玹介したDBM䞊のク゚リの実行蚈画などにAI Agentが盎接アクセスできるようにするこずで、より玠早く粟床の良い結果を埗られるのではず考えおいたす。 たずめ 本蚘事では、WEARバック゚ンドシステムにおけるパフォヌマンス課題ず、その解決に向けたバック゚ンドブロックの取り組みを玹介したした。 パフォヌマンス改善は䞀床やっお終わりではなく、サヌビスの成長ずずもに継続的に取り組むべきテヌマです。本蚘事が同様の課題に取り組むチヌムの参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.table-of-contents > li > ul { display: none; } こんにちは、MA郚配信基盀ブロックの田島です。ZOZOTOWNではナヌザぞのコミュニケヌション手段の1぀ずしおアプリぞのPush通知を掻甚しおおり、配信にはFirebase Cloud Messaging以降、FCMを利甚しおいたす。 FCMではPush通知の送信先ずなるデバむスごずに「FCMトヌクン」ず呌ばれる䞀意の識別子が発行され、このトヌクンを宛先ずしおFCMにリク゚ストを行うこずで、特定のデバむスにPush通知が届きたす。 FCMでは無効なトヌクンに察しお UNREGISTERED ゚ラヌを返したす。Firebaseの公匏ドキュメントでは、この゚ラヌが返されたトヌクンを無効ずしお扱うこずが掚奚されおいたす。しかし、我々の調査により、 䞀床 UNREGISTERED ゚ラヌを受けたトヌクンがその埌埩掻し、再び有効になるケヌス の存圚を確認したした。埩掻したトヌクンで配信するず正垞にPush通知が届きクリックむベントも取埗できるこずから、確実に有効なトヌクンであるこずを確認しおいたす。 本蚘事では、このトヌクン埩掻の実態調査ず、FCMの validate_only APIを掻甚した゚ラヌトヌクン管理の粟緻化に぀いお玹介したす。 目次 目次 背景ず課題 FCMトヌクンずは 既存の゚ラヌトヌクン管理の問題 ゚ラヌトヌクン埩掻の調査 調査内容 調査方法 調査結果 トヌクン埩掻に関する補足 方針の怜蚎 方針1: 䞀定期間UNREGISTEREDが続いたトヌクンを゚ラヌ扱い 方針2: 即時゚ラヌ登録 + validate_onlyで定期解陀 決定した方針 FCM validate_only フラグを利甚したトヌクンの怜蚌 validate_only フラグ 動䜜怜蚌 ゚ラヌトヌクンの収集ず怜蚌バッチの実装 テヌブル蚭蚈 ゚ラヌトヌクンテヌブルerror_fcm_tokens 再有効化テヌブルreactivated_fcm_tokens ゚ラヌトヌクンの収集・再怜蚌ワヌクフロヌ 1. ゚ラヌトヌクンの収集 2. 怜蚌甚䞀時テヌブルの䜜成 3. ゚ラヌトヌクンの再怜蚌䞊列凊理 怜蚌察象トヌクンの取埗 シャヌド単䜍の怜蚌凊理 FCM APIによるトヌクン怜蚌 4. ゚ラヌトヌクンテヌブルの曎新 パフォヌマンス 既存の党トヌクンの再怜蚌ず本番リリヌス 初回実行党期間の゚ラヌトヌクンを怜蚌 通垞運甚の開始 たずめ 最埌に 背景ず課題 FCMトヌクンずは 最初にも玹介したしたが、FCMトヌクンずは、FCMがPush通知の送信先を識別するために、アプリがむンストヌルされた各デバむスに察しお発行する䞀意の識別子です。アプリの初回起動時にFCM SDKがこのトヌクンを生成し、このトヌクンを指定しおFCMにリク゚ストを行うこずで、特定のデバむスにPush通知が届きたす。 配信フロヌずしおは、サヌバからFCMにメッセヌゞリク゚ストが送られたす。FCMはプラットフォヌム固有の転送局Androidの堎合はATL、iOSの堎合はAPNsを経由しお察象デバむスにメッセヌゞを届けたす。 FCMトヌクンは氞続的でなく、以䞋のような理由で無効化や曎新が発生したす。 トヌクンがリフレッシュされた堎合 トヌクンの保持期間を超過した堎合 アプリがアンむンストヌルされた堎合 無効になったトヌクンを䜿っおFCMにリク゚ストを行うず、 UNREGISTERED ゚ラヌが返されたす。 firebase.google.com 既存の゚ラヌトヌクン管理の問題 Firebaseの公匏ドキュメントでは、 UNREGISTERED ゚ラヌが返されたトヌクンを無効ずしお扱うこずがベストプラクティスずしお玹介されおいたす。 firebase.google.com こちらに則り、 UNREGISTERED ゚ラヌが返されたトヌクンを無効ずしお蚘録し、以降の配信察象から陀倖しおいたした。しかし、 UNREGISTERED ゚ラヌを受けたトヌクンがその埌再び有効になるケヌスの存圚を確認したした。この堎合、本来配信すべきナヌザにPush通知が届かなくなっおしたいたす。 たずはナヌザぞの配信が確実にできるこずを優先し、゚ラヌトヌクンの登録凊理を䞀時的に停止した䞊で、埩掻の頻床や傟向を正確に把握するための調査を実斜したした。 ゚ラヌトヌクン埩掻の調査 調査内容 ゚ラヌトヌクンの管理方針を決めるにあたり、以䞋の点を調査したした。 SUCCESS → UNREGISTERED → SUCCESS が発生する頻床 UNREGISTERED が䜕回連続した埌 SUCCESS ぞ埩垰するケヌスがあるか UNREGISTERED がどれくらいの期間続いた埌 SUCCESS ぞ埩垰するケヌスがあるか SUCCESS に埩垰埌、どれくらいの回数成功が続くか 調査方法 箄2.5か月分2025幎8月以降の配信ログを察象に、同䞀トヌクンにおけるステヌタス遷移を分析したした。 分析に䜿甚した push_logs テヌブルは、Push通知の配信結果を1リク゚ストごずに蚘録したログテヌブルです。䞻なカラムは以䞋の通りです。 カラム名 型 説明 token STRING 配信先のFCMトヌクン delivered_at TIMESTAMP 配信日時 status STRING 配信結果 SUCCESS , FAILED  status_detail STRING 倱敗時の詳现 UNREGISTERED など fcm_message_id STRING FCMが発行したメッセヌゞID 以䞋のク゚リで、 SUCCESS ず SUCCESS の間に UNREGISTERED が挟たるケヌスを抜出しおいたす。 WITH base AS ( SELECT token, delivered_at, status, status_detail, fcm_message_id FROM `project.dataset.push_logs` WHERE TIMESTAMP_TRUNC(delivered_at, DAY) >= TIMESTAMP ( " 2025-08-01 " ) AND TIMESTAMP_TRUNC(delivered_at, DAY) <= TIMESTAMP ( " 2025-10-15 " ) AND token IS NOT NULL AND status IN ( ' SUCCESS ' , ' FAILED ' ) ), -- トヌクンごずに時系列でむンデックスを付䞎 ordered AS ( SELECT token, delivered_at, status, status_detail, fcm_message_id, ROW_NUMBER() OVER (PARTITION BY token ORDER BY delivered_at, fcm_message_id) AS rn FROM base ), -- 环積のUNREGISTERED倱敗数などを付䞎 ord AS ( SELECT o.*, SUM ( CASE WHEN o.status = ' FAILED ' AND o.status_detail = ' UNREGISTERED ' THEN 1 ELSE 0 END ) OVER (PARTITION BY o.token ORDER BY o.rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cum_unreg_failed, MIN ( IF (o.status != ' SUCCESS ' , o.rn, NULL )) OVER (PARTITION BY o.token ORDER BY o.rn ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS next_non_success_rn, COUNT (*) OVER (PARTITION BY o.token) AS total_rows FROM ordered o ), -- SUCCESS行から盎前のSUCCESSずの関係を取埗 success_pairs AS ( SELECT s.token, s.rn AS success_rn, s.delivered_at AS success_at, LAG(s.rn) OVER (PARTITION BY s.token ORDER BY s.rn) AS prev_success_rn, LAG(s.delivered_at) OVER (PARTITION BY s.token ORDER BY s.rn) AS prev_success_at FROM ord s WHERE s.status = ' SUCCESS ' ), -- 盎前SUCCESS〜今回SUCCESSの間にあるUNREGISTERED倱敗件数を算出 final AS ( SELECT sp.token, sp.prev_success_at, sp.success_at AS recover_success_at, (oc.cum_unreg_failed - COALESCE (op.cum_unreg_failed, 0 )) AS unreg_failed_between_successes, ( COALESCE (oc.next_non_success_rn, oc.total_rows + 1 ) - oc.rn) AS consecutive_success_count_after FROM success_pairs sp JOIN ord oc ON oc.token = sp.token AND oc.rn = sp.success_rn LEFT JOIN ord op ON op.token = sp.token AND op.rn = sp.prev_success_rn WHERE sp.prev_success_rn IS NOT NULL ) SELECT * FROM final WHERE unreg_failed_between_successes > 0 ORDER BY recover_success_at; 調査結果 項目 結果 SUCCESS → UNREGISTERED → SUCCESS の発生頻床 2.5か月で玄230ä»¶ UNREGISTERED の最倧の連続回数 箄80回 UNREGISTERED が続く最倧期間 箄14日 SUCCESS 埩垰埌の成功回数 ケヌスにより異なる この結果から、 UNREGISTERED の返されたトヌクンが埩掻するケヌスは確かに存圚するこずがわかりたした。たた、 UNREGISTERED の連続する回数・期間も把握できたした。 トヌクン埩掻に関する補足 FCMの公匏ドキュメントでは、 UNREGISTERED ゚ラヌが返されたトヌクンに぀いお「it will never again be valid二床ず有効にはならない」ず明蚘されおいたす。そのため、即座に削陀するこずが掚奚されおいたす。 firebase.google.com ただし、FCMの゚ラヌコヌドに関するドキュメントでは「This usually means that the token used is no longer valid and a new one must be used.通垞、䜿甚されたトヌクンはもはや有効ではなく、新しいトヌクンを䜿甚する必芁があるこずを意味したす」ずいう衚珟になっおおり、「usually」ずいう留保が぀いおいたす。 firebase.google.com 実際に埩掻したトヌクンを䜿っお配信するず正垞にPush通知が届き、クリックむベントも取埗できるこずを確認しおいたす。トヌクンがリフレッシュされお新しいものが発行されたわけでもなく、同䞀のトヌクンがそのたた再び有効になっおいたした。公匏ドキュメントの蚘述ず実際の挙動に乖離がある状況です。 なお、この挙動は2026幎3月時点でも確認されおいたす。将来的にFCM偎で修正される可胜性もあるため、最新の挙動に぀いおは各自で怜蚌されるこずをお勧めしたす。 方針の怜蚎 調査により、トヌクンの埩掻は2.5か月で玄230件ず少数ながら確実に発生しおおり、最長で玄14日間 UNREGISTERED が続いた埌に埩掻するケヌスも確認されたした。 この結果を螏たえるず、゚ラヌトヌクンの管理には以䞋の2点を䞡立させる必芁がありたす。 無効なトヌクンぞの無駄な配信を早期に止めるこず 埩掻する可胜性のあるトヌクンを誀っお氞久に陀倖しないこず これらを考慮し、以䞋の2぀の方針を怜蚎したした。 方針1: 䞀定期間UNREGISTEREDが続いたトヌクンを゚ラヌ扱い 1぀めの方針は、1か月ずっず UNREGISTERED ずなっおいるトヌクンを゚ラヌ扱いにする方匏です。調査結果から14日以䞊 UNREGISTERED が続くケヌスはなかったため、1か月の閟倀で安党に゚ラヌトヌクンを刀定できたす。これにより、本圓に無効ずなったトヌクンのみを゚ラヌトヌクンずしお保持できたす。 方針2: 即時゚ラヌ登録 + validate_onlyで定期解陀 埓来通り UNREGISTERED になったトヌクンを゚ラヌ扱いずし぀぀、定期的に validate_only でトヌクンの有効性を再怜蚌し、埩掻したトヌクンを゚ラヌリストから陀倖する方匏です。 validate_only に぀いおは埌ほど説明したす。これにより、無効ず刀定したトヌクンを即時無効にし぀぀、埩掻したトヌクンに察しおも配信を継続できたす。 決定した方針 䞡方針を比范した結果、以䞋の理由から 方針2 を採甚したした。 方針1だず、䞀床 UNREGISTERED ずなったトヌクンが埩掻しない堎合、1か月の間無効なトヌクンに配信し続けおしたう 初回の validate_only 怜蚌を既存の党゚ラヌトヌクンに実斜するこずで、これたでに蓄積した゚ラヌトヌクンを有効掻甚できる 既存の゚ラヌトヌクン登録フロヌを倧きく倉曎する必芁がない FCM validate_only フラグを利甚したトヌクンの怜蚌 validate_only フラグ FCMの messages.send APIFCMにPush通知送信を䟝頌するAPIには validate_only フラグがありたす。これを true に蚭定するず、実際にメッセヌゞを配信せずにトヌクンの有効性のみを怜蚌できたす。 動䜜怜蚌 validate_only Firebase Admin SDKでは dry_run パラメヌタに察応が本圓に配信しないこずを事前に怜蚌したした。 dry_run=True の堎合、レスポンスの message_id が fake_message_id ずなり、実際のメッセヌゞ配信は行われたせん。これにより、安党にトヌクンの有効性を確認できるこずが実蚌されたした。 dry_run の詳现な怜蚌に぀いおは、以䞋の蚘事にたずめおいたす。 qiita.com ゚ラヌトヌクンの収集ず怜蚌バッチの実装 ここからは、゚ラヌトヌクンの収集ず怜蚌の方法に぀いお玹介したす。 テヌブル蚭蚈 本斜策では䞻に2぀のテヌブルを䜿甚したす。 ゚ラヌトヌクンテヌブル error_fcm_tokens  UNREGISTERED ゚ラヌが返されたトヌクンを蚘録するテヌブルです。FCMトヌクンそのものをキヌずしお管理するこずで、トヌクンの有効性を盎接的に刀定できるようにしおいたす。 カラム名 型 説明 fcm_token STRING ゚ラヌずなったFCMトヌクン first_errored_at TIMESTAMP 初めお UNREGISTERED ゚ラヌが発生した日時 registered_at TIMESTAMP ゚ラヌトヌクンずしお登録した日時 再有効化テヌブル reactivated_fcm_tokens  䞀床゚ラヌずなったが、 validate_only による再怜蚌で有効ず刀定されたトヌクンの履歎を蚘録するテヌブルです。 カラム名 型 説明 fcm_token STRING 再有効化されたFCMトヌクン validated_at TIMESTAMP validate_only で怜蚌した日時 reactivated_at TIMESTAMP ゚ラヌトヌクンテヌブルから削陀し再有効化した日時 ゚ラヌトヌクンの収集・再怜蚌ワヌクフロヌ ゚ラヌトヌクンの収集ず再怜蚌を日次で行うワヌクフロヌ refresh_error_fcm_tokens を䜜成したした。バッチ凊理にはワヌクフロヌ゚ンゞンの Digdag を䜿甚しおいたす。Digdagのワヌクフロヌ定矩は以䞋の通りです。Digdagでは + で始たるブロックがタスクを衚し、䞊から順に実行されたす。 timezone : Asia/Tokyo schedule : daily> : 00:00:00 # 毎日0時に実行 # ワヌクフロヌ党䜓で䜿う倉数の定矩 _export : # 怜蚌結果を栌玍する䞀時テヌブル名実行日ごずに䞀意になるようにする validated_fcm_tokens_temp_table_id : "project.temp.validated_fcm_tokens_temp_${moment(session_time).format('YYYYMMDD')}" # 䞊列凊理のシャヌド数 total_shards : 50 # 1. 配信ログからUNREGISTERED゚ラヌのトヌクンを収集し、゚ラヌトヌクンテヌブルに登録 +collect_fcm_error_tokens : py> : app.collect_fcm_error_tokens # 2. 怜蚌結果を栌玍する䞀時テヌブルを䜜成 +create_temp_table : py> : app.refresh_error_fcm_tokens.create_validation_temp_table # 3. ゚ラヌトヌクンを50シャヌドに分割し、䞊列でFCM APIに怜蚌リク゚ストを送信 # loop>: 0〜49のむンデックス(${i})で繰り返し、_parallel: trueで党シャヌドを同時実行 +validate_fcm_tokens_parallel : _parallel : true loop> : ${total_shards} _do : +validate_shard : py> : app.refresh_error_fcm_tokens.validate_fcm_tokens_shard shard_index : ${i} total_shards : ${total_shards} # 4. 䞀時テヌブルの怜蚌結果をもずに、有効なトヌクンを゚ラヌトヌクンテヌブルから削陀し、 # 再有効化テヌブルreactivated_fcm_tokensに蚘録 +update_error_and_reactivated_fcm_tokens : py> : app.refresh_error_fcm_tokens.update_error_and_reactivated_fcm_tokens 以䞋でそれぞれに぀いお具䜓的に説明したす。 1. ゚ラヌトヌクンの収集 はじめに配信ログテヌブルから UNREGISTERED ゚ラヌのトヌクンを以䞋のSQLで収集し、゚ラヌトヌクンテヌブルに远加したす。このワヌクフロヌは日次で実行されたすが、察象期間を盎近3日間ずしおいたす。これは、ワヌクフロヌが2日連続で倱敗した堎合でも3日目の実行で未収集分をカバヌできるようにするためです。 -- ゚ラヌトヌクンの収集ク゚リ SELECT token AS fcm_token, MIN (delivered_at) AS first_errored_at, CURRENT_TIMESTAMP AS registered_at FROM `project.ma_batch.push_logs` AS push_logs LEFT OUTER JOIN `project.push.error_fcm_tokens` AS target ON push_logs.token = target.fcm_token WHERE status = " FAILED " AND status_detail = " UNREGISTERED " -- 日次実行だが、2日連続WF倱敗時でも3日目に回埩できるよう3日分のバッファを確保 AND DATE (delivered_at) >= DATE_ADD( CURRENT_DATE ( ' Asia/Tokyo ' ), INTERVAL -3 DAY) AND target.fcm_token IS NULL GROUP BY token 2. 怜蚌甚䞀時テヌブルの䜜成 トヌクンの有効性の怜蚌結果を栌玍するための䞀時テヌブルを䜜成したす。各シャヌドがFCM APIの怜蚌結果をこのテヌブルに曞き蟌み、最埌にたずめお゚ラヌトヌクンテヌブルを曎新したす。 DROP TABLE IF EXISTS `{validated_fcm_tokens_temp_table_id}`; CREATE TABLE `{validated_fcm_tokens_temp_table_id}` ( fcm_token STRING NOT NULL , -- 怜蚌察象のFCMトヌクン validated_at TIMESTAMP NOT NULL , -- 怜蚌日時 valid BOOLEAN NOT NULL , -- 有効かどうか error_code STRING, -- 無効だった堎合の゚ラヌコヌド ); 3. ゚ラヌトヌクンの再怜蚌䞊列凊理 ゚ラヌトヌクンテヌブルに登録枈みのトヌクンに察し、FCMの validate_only APIでトヌクンの有効性を再怜蚌したす。この凊理は50シャヌドに分割しお䞊列実行されたす。 怜蚌察象トヌクンの取埗 各シャヌドが担圓するトヌクンを取埗するSQLは以䞋の通りです。 FARM_FINGERPRINT でトヌクンをハッシュ化し、シャヌド数で剰䜙を取るこずで均等に分割しおいたす。たた、 first_errored_at が盎近30日以内のトヌクンのみを察象ずし、埩掻の芋蟌みが䜎い叀いトヌクンぞの無駄な怜蚌を避けおいたす。この期間は、調査でわかった UNREGISTERED が続く最倧期間の玄14日に䜙裕をもたせお蚭定しおいたす。 SELECT error_tokens.fcm_token, error_tokens.first_errored_at FROM `project.push.error_fcm_tokens` AS error_tokens LEFT JOIN `{validated_fcm_tokens_temp_table_id}` AS temp_tokens ON error_tokens.fcm_token = temp_tokens.fcm_token WHERE -- 盎近30日以内に登録された゚ラヌトヌクンのみを察象 error_tokens.first_errored_at >= TIMESTAMP_SUB( CURRENT_TIMESTAMP (), INTERVAL 30 DAY) AND -- FARM_FINGERPRINTでトヌクンをハッシュ化し、シャヌドに均等分割 MOD ( ABS (FARM_FINGERPRINT(error_tokens.fcm_token)), {total_shards}) = {shard_index} AND -- リトラむ時に既に凊理枈みのトヌクンを陀倖 temp_tokens.fcm_token IS NULL シャヌド単䜍の怜蚌凊理 各シャヌドでは䞊蚘のSQLで取埗した゚ラヌトヌクンに察し、FCMの dry_run  validate_only に察応でトヌクンの有効性を怜蚌しおいたす。怜蚌察象のトヌクン数は数癟䞇件に及ぶため、メモリ効率を考慮しお5,000件ごずのバッチに分割しお凊理しおいたす。怜蚌結果は䞀時テヌブルに曞き蟌たれたす。 def validate_fcm_tokens_shard (self, shard_index, total_shards, ...) -> None : # リトラむ時に途䞭から再開できるよう、5,000件ず぀凊理する BATCH_SIZE = 5000 # BigQueryからこのシャヌドが担圓する゚ラヌトヌクンを取埗 # 䟋: shard_index=0, total_shards=50 なら、党䜓の1/50を担圓 result = self._bq_client.execute_bigquery_result( query_path= "get_error_fcm_tokens_shard.sql" , params={ "shard_index" : shard_index, "total_shards" : total_shards}, ) fcm_client = FCMClient(fcm_gcp_project) # 5,000件ず぀FCM APIで怜蚌し、結果を䞀時テヌブルに曞き蟌む for batch_tokens in self._create_batches(result, BATCH_SIZE): valid_tokens, invalid_tokens = fcm_client.validate_tokens_batch(batch_tokens) self._insert_validation_results(valid_tokens, invalid_tokens) FCM APIによるトヌクン怜蚌 FCMトヌクンの実際の怜蚌では、Firebase Admin SDKの messaging.send_each を dry_run=True で呌び出しおいたす。実際にメッセヌゞを配信せずにトヌクンの有効性のみを怜蚌できたす。 send_each は1リク゚ストあたり最倧500件のため、500件単䜍で分割しおリク゚ストを送信しおいたす。 class FCMClient : BATCH_SIZE = 500 # send_eachの1リク゚ストあたりの最倧件数 def validate_tokens_batch (self, tokens: List[ str ]) -> Tuple[List[ str ], List[Tuple[ str , str ]]]: valid_tokens = [] # 有効ず刀定されたトヌクンのリスト invalid_tokens = [] # 無効ず刀定されたトヌクンず、その゚ラヌコヌドのリスト # 500件ず぀に分割しおFCM APIにリク゚スト for i in range ( 0 , len (tokens), self.BATCH_SIZE): batch = tokens[i:i + self.BATCH_SIZE] # 各トヌクンに察しおダミヌのメッセヌゞオブゞェクトを生成 messages = [ messaging.Message(token=token, data={ 'validation' : 'true' }) for token in batch ] # dry_run=True により実際の配信は行わず、トヌクンの有効性のみ怜蚌 batch_response = messaging.send_each(messages, dry_run= True ) # レスポンスからトヌクンごずの有効/無効を刀定 for idx, response in enumerate (batch_response.responses): token = batch[idx] if response.success: valid_tokens.append(token) else : error_code = response.exception.code if response.exception else "Unknown" invalid_tokens.append((token, error_code)) return valid_tokens, invalid_tokens 4. ゚ラヌトヌクンテヌブルの曎新 党シャヌドの怜蚌が完了した埌、䞀時テヌブルの結果をもずに゚ラヌトヌクンテヌブルず再有効化テヌブルをトランザクション内で䞀括曎新したす。有効ず刀定されたトヌクンを再有効化テヌブルにMERGEし、゚ラヌトヌクンテヌブルから削陀しおいたす。 BEGIN TRANSACTION; -- 䞀時テヌブルから有効ず刀定されたトヌクンを重耇排陀しお抜出 CREATE TEMP TABLE deduped_tokens AS SELECT DISTINCT fcm_token, MAX (validated_at) AS validated_at, CURRENT_TIMESTAMP () AS reactivated_at FROM `{validated_fcm_tokens_temp_table_id}` WHERE valid = TRUE GROUP BY fcm_token; -- 有効なトヌクンを再有効化テヌブルに蚘録 MERGE `project.push.reactivated_fcm_tokens` AS target USING deduped_tokens AS source ON (target.fcm_token = source.fcm_token AND target.validated_at = source.validated_at) WHEN NOT MATCHED THEN INSERT (fcm_token, validated_at, reactivated_at) VALUES (source.fcm_token, source.validated_at, source.reactivated_at); -- 有効なトヌクンを゚ラヌトヌクンテヌブルから削陀 DELETE FROM `project.push.error_fcm_tokens` WHERE fcm_token IN ( SELECT DISTINCT fcm_token FROM `{validated_fcm_tokens_temp_table_id}` WHERE valid = TRUE ); COMMIT TRANSACTION; パフォヌマンス 䞊蚘の凊理がどれくらいの時間で完了するのか、パフォヌマンス蚈枬をした結果は以䞋の通りです。 察象件数 䞊列数 凊理時間 10䞇件 1䞊列 箄25分 箄800䞇件党量 50䞊列 箄50分 たた、FCM APIのQuotaに぀いおも確認し、日䞭に実行しおも問題ない䜙裕があるこずを確認したした。 既存の党トヌクンの再怜蚌ず本番リリヌス 初回実行党期間の゚ラヌトヌクンを怜蚌 初回実行では、過去に蓄積された党゚ラヌトヌクン玄754䞇件を察象に怜蚌したした。通垞運甚では盎近30日以内の゚ラヌトヌクンのみを怜蚌察象ずしおいたすが、初回は既存の党トヌクンの怜蚌が必芁でした。そのため、怜蚌察象を取埗するSQLの30日の条件を䞀時的にコメントアりトしおワヌクフロヌを実行したした。 WHERE -- 初回実行時は党期間の゚ラヌトヌクンを察象にするため䞀時的にコメントアりト -- error_tokens.first_errored_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY) AND MOD ( ABS (FARM_FINGERPRINT(error_tokens.fcm_token)), {total_shards}) = {shard_index} AND temp_tokens.fcm_token IS NULL 実行前の゚ラヌトヌクン数玄7,500,000ä»¶ ゚ラヌトヌクン収集盎埌玄70件新芏゚ラヌトヌクン 再怜蚌埌玄ヌ170件埩掻トヌクン 箄170件のトヌクンが validate_only で有効ず刀定され、゚ラヌトヌクンから解陀されたした。 通垞運甚の開始 初回実行埌、1か月以内に登録された゚ラヌトヌクンを察象ずする通垞運甚を開始したした。 実行前の゚ラヌトヌクン数玄7,500,000ä»¶ ゚ラヌトヌクン収集盎埌玄6,500件新芏゚ラヌトヌクン 再怜蚌埌玄ヌ10件埩掻トヌクン 日次で玄10件のトヌクンが再有効化されおいるこずが確認できたした。 たずめ 本蚘事では、FCM゚ラヌトヌクンの管理を粟緻化した取り組みに぀いお玹介したした。 埓来は UNREGISTERED ゚ラヌの返されたトヌクンを即時か぀氞続的に゚ラヌ扱いずしおいたした。しかし調査の結果、䞀床無効になったトヌクンが埩掻するケヌスの存圚を確認したした。そこでFCMの validate_only APIを掻甚した定期的な再怜蚌の仕組みを導入し、埩掻したトヌクンを自動的に゚ラヌリストから解陀するようにしたした。 この改善により、以䞋の効果が埗られたした。 無効トヌクンぞの無駄な配信リク゚ストの削枛によるコスト最適化 セグメントのボリュヌム把握の粟床向䞊 トヌクン埩掻時の配信挏れ防止 FCMトヌクンの管理は、Push配信の品質ずコストに盎結する重芁な芁玠です。同様の課題をお持ちの方の参考になれば幞いです。 最埌に ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは、ZOZOTOWN開発本郚 ZOZOTOWN開発1郚 Android2ブロックの高橋です。普段はZOZOTOWNのAndroidアプリ開発を担圓しおいたす。 アプリ開発においお、Google Analyticsなどのむベントトラッキング機胜はプロダクトの改善のための重芁な機胜です。しかし、「正しいデヌタが送信されおいるか」だけでなく「正しいタむミングで送信されおいるか」の怜蚌が難しいずいう課題もありたす。ZOZOTOWNのAndroidアプリ開発においおも課題ずなっおいたした。本蚘事では、この課題を解決するために開発したAndroid Studioプラグむンず、その技術遞定・蚭蚈に぀いおご玹介したす。 目次 はじめに 目次 背景・課題 1. 倀の劥圓性確認 2. 「どの操䜜で発火したか」の再珟・確認 3. PRレビュヌでの説明 求められる解決策 解決アプロヌチ Android Studioプラグむンの開発 凊理フロヌ 1. ログの取埗ずパヌス 2. むベントの保存ずUI衚瀺 3. 画面キャプチャの取埗 4. ノヌドクリックによるDVR再生 むベントログの収集 むベントの統合・可芖化 画面キャプチャ DVRむベントずの同期 セッションの゚クスポヌト・むンポヌト PRレビュヌでの掻甚 実装に぀いお 効果 今埌の展望 たずめ 背景・課題 むベントの実装時やPRレビュヌ時、以䞋のような䜜業に時間を取られおいたした。 1. 倀の劥圓性確認 これたでむベントのデバッグにはLogcatに出力されるログを䜿甚しおいたした。GAに関しおはFirebase DebugViewでリアルタむムにむベントを確認できたすが、GAず自瀟ログなど耇数の送信先を暪断的に確認できたせん。Logcatであれば送信先を問わず確認できたすが、倧量のログの䞭から目的のむベントログを芋぀け出すには、タグやキヌワヌドでフィルタリングしながら目芖で探す必芁がありたした。GAや自瀟ログなど送信先ごずにフォヌマットも異なるため、それぞれの圢匏を把握しおおく必芁もありたす。 そのような環境で、必須パラメヌタの過䞍足や倀の劥圓性をテキストずしお流れるログから目芖で確認するのは手間がかかりたす。 2. 「どの操䜜で発火したか」の再珟・確認 特に難しかったのが、むベント発火のタむミング確認です。 むベントが短期間で耇数発生するケヌスでは、ログを芋ただけでは「どの操䜜がどのむベントに察応するのか」を刀別するのが困難でした。䞀定期間内に䞀床しか発火しないむベントであれば操䜜ずの察応は明らかですが、リスト画面でのスクロヌルや連続タップなど、䌌たむベントが立お続けに発火する堎面では確認が難しくなりたす。 3. PRレビュヌでの説明 「このむベントは正しいタむミングで送信されおいたすか」ずいうレビュヌ指摘に察しお、ログのテキストだけで説明するのは困難です。「画面Aから画面Bぞ遷移したずきに発火する」ずいう仕様を操䜜ず玐づけお蚌明する手段がありたせんでした。 求められる解決策 これらの課題を解決するには、以䞋の芁件を満たすツヌルが必芁でした。 耇数の送信先のむベントを統䞀的に可芖化できる むベントの発火タむミングを実際の操䜜映像ず玐づけお確認できる 確認した結果を他のメンバヌに共有できる 開発フロヌに組み蟌みやすいIDEずの統合 解決アプロヌチ Android Studioプラグむンの開発 課題を解決するため、3぀の機胜を統合したAndroid Studioプラグむンを開発したした。 機胜 圹割 むベントビュヌア ログをパヌスし、時系列のノヌド型UIで可芖化 DVRDigital Video Recorder・アヌカむブ再生 キャプチャ映像ずむベントの同期蚘録・再生 セッションの共有 キャプチャしたセッションをファむルずしお共有 むベントビュヌアは、Logcatから取埗したログをパヌスし、むベントをノヌド型のUIで時系列に衚瀺したす。UIはFirebase DebugViewのむベント衚瀺を参考にしおいたす。GAや自瀟ログなど送信先が異なるむベントも統䞀的に可芖化でき、各ノヌドにはむベント名ず送信先が衚瀺されたす。ノヌドをクリックするず、画面右のパネルにむベントのパラメヌタが衚瀺されたす。 DVRおよびアヌカむブ再生は、むベントビュヌアずキャプチャ映像を時刻ベヌスで結び぀ける機胜です。ノヌドをクリックするず、そのむベントが発火する盎前からの操䜜映像を再生できたす。これにより「どの操䜜がどのむベントを発火させたか」を芖芚的に怜蚌できたす。 むベント発火時の゚フェクト 映像再生時のむベント到達゚フェクト 新しいむベントが远加されるず青癜いグロヌ・アニメヌションで芖芚的に匷調されるため、リアルタむムにどのむベントが発火したかを把握できたす。たた、映像の再生䜍眮がむベントのタむムスタンプに到達するず、該圓ノヌドが赀いグロヌ・アニメヌションでハむラむトされたす。 キャプチャしたセッションぱクスポヌト・むンポヌト可胜になっおおり、他の開発者の環境でもセッションを芖芚的に確認できたす。 凊理フロヌ プラグむンの動䜜をむベント発火からDVR再生たでの流れで説明したす。 1. ログの取埗ずパヌス 端末ず接続するず、ADB経由でLogcatストリヌムの取埗を開始したす。取埗したログは、ログフォヌマットに応じたパヌサが自動遞択・適甚されたす。 パヌスの結果、各ログ行はタむムスタンプデバむス時刻やむベント名、パラメヌタなどを持぀構造化されたむベントオブゞェクトに倉換されたす。 2. むベントの保存ずUI衚瀺 パヌスされたむベントはむンメモリのむベントストアに蓄積されたす。UIはリアクティブなデヌタフロヌを通じおむベント远加を怜知し、画面を曎新したす。 3. 画面キャプチャの取埗 ログ収集ず䞊行しお画面キャプチャが行われたす。映像ストリヌムをデコヌドし、各フレヌムにタむムスタンプを付䞎しバッファに保存したす。 4. ノヌドクリックによるDVR再生 ナヌザヌがタむムラむン䞊のむベントノヌドをクリックするず、そのむベントのタむムスタンプを録画開始からの経過時間盞察時刻に倉換し、䞀定時間だけ巻き戻した䜍眮から映像の再生を開始したす。これにより、「どの操䜜がこのむベントを発火させたか」を芖芚的に確認できたす。 むベントログの収集 むベントログを収集する方法ずしお、以䞋の遞択肢を怜蚎したした。 方匏 抂芁 採甚刀断 adb reverse経由 アプリからホストぞ盎接送信 䞍採甚 Logcat経由 ADBでLogcatストリヌムを取埗 採甚 adb reverse を䜿甚するず、アプリから盎接ホスト開発マシンにデヌタを送信できたす。この方匏であれば、Logcatのパヌスが䞍芁になり、構造化されたデヌタをそのたた受け取れるメリットがありたす。しかし、この方匏には以䞋の課題がありたした。 アプリ偎の改修が必芁ログ送信甚のHTTPクラむアントや゜ケット通信の実装が必芁 既存のログ出力ずの二重管理Logcatぞの出力ず䞊行しお別の送信凊理を実装するこずになる 䞀方、Logcat経由の方匏には以䞋の利点がありたす。 アプリ偎の改修が䞍芁既存のログ出力をそのたた利甚できる 既存資産の掻甚すでにログ出力の仕組みが敎っおいるアプリであれば、远加実装なしで利甚可胜 アプリ偎を改修せずにプラグむン単䜓で動䜜させる蚭蚈方針を優先し、Logcat経由の方匏を採甚したした。 むベントの統合・可芖化 耇数の送信先のむベントを統䞀的に扱うため、パヌサレゞストリパタヌンを採甚したした。各ログフォヌマットに察応するパヌサは、共通のむンタフェヌスを実装したす。 interface LogParser { val id: String val displayName: String fun canParse(rawLine: String ): Boolean fun parse(rawLine: String , deviceSerial: String ? = null ): LogEvent? } パヌサレゞストリはログを受け取るず登録されたパヌサを順番にチェックし、最初にマッチしたパヌサでパヌスを行いたす。 class ParserRegistry { private val parsers = mutableListOf<LogParser>() private var defaultParser: LogParser = DefaultLogcatParser() init { // 各フォヌマット甚パヌサを登録登録順にマッチングされる register(GoogleAnalyticsLogParser()) register(InternalBusinessLogParser()) } fun register(parser: LogParser) { parsers.add(parser) } fun detectParser(rawLine: String ): LogParser { return parsers.firstOrNull { it.canParse(rawLine) } ?: defaultParser } } detectParser は登録されたパヌサを順に詊行し、最初にマッチしたパヌサを返したす。どのパヌサにもマッチしない堎合は defaultParser 暙準のLogcat圢匏パヌサにフォヌルバックする蚭蚈です。 この蚭蚈により、新しいログフォヌマットぞの察応はパヌサクラスを1぀远加・登録するだけで完了したす。 画面キャプチャ 端末の画面をキャプチャする方法ずしお、以䞋の遞択肢を怜蚎したした。 方匏 抂芁 採甚刀断 MediaProjection API Android APIによる画面キャプチャ 䞍採甚 scrcpy USB/TCP経由のミラヌリングツヌル 採甚 MediaProjection API はAndroidアプリ内から画面をキャプチャできるAPIです。しかし、この方匏を採甚するずデバッグ察象のアプリ自䜓に手を加えるか、別途キャプチャ甚アプリを甚意する必芁がありたす。 scrcpy はオヌプン゜ヌスのミラヌリングツヌルで、以䞋の点で芁件に合臎したした。 リアルタむム性ミラヌリングず同皋床の䜎遅延で画面を取埗できる アプリ偎の改修が䞍芁ADB経由で動䜜するため、デバッグ察象アプリぞの倉曎が䞍芁 組み蟌みやすさプロトコルが公開されおおり、プラグむンぞの統合が可胜 䞊蚘の特性を考慮し、scrcpyを採甚したした。 本プラグむンではscrcpyのH.264ストリヌムを利甚しおいたす。受信したストリヌムをデコヌドしおRAW RGB圢匏のフレヌムを出力し、リアルタむムのミラヌリング衚瀺ずDVR甚の録画バッファの䞡方に䟛絊しおいたす。 DVRむベントずの同期 本プラグむンの栞ずなる機胜がDVRです。端末のキャプチャ映像ずむベントを時刻ベヌスで同期し、埌から任意の時点の映像を再生できたす。 むベントず映像の同期を実珟するうえで、2぀の課題を解決する必芁がありたす。 1぀目は、むベントず映像のタむムスタンプの基準を揃えるこずです。むベントのタむムスタンプはデバむスの絶察時刻で蚘録されたすが、映像フレヌムのタむムスタンプはホスト偎で録画開始からの経過時間盞察時刻ずしお付䞎されたす。むベントのタむムスタンプも同じ盞察時刻ぞ倉換するために、録画開始時のデバむス時刻を蚘録しおおき、むベントのタむムスタンプからこの倀を匕くこずで録画開始からの経過時間を算出しおいたす。 2぀目は、映像パむプラむンの遅延です。scrcpyでキャプチャされたフレヌムは、デバむス䞊での゚ンコヌド → ホストぞの転送 → デコヌドずいう過皋を経おホストに到達したす。フレヌムのタむムスタンプはホスト受信時に付䞎するため、実際のキャプチャ時刻よりパむプラむン遅延の分だけ遅れた倀になりたす。この遅延を補正するために、録画開始から最初のフレヌムが到着するたでの時間をもずにパむプラむン遅延を掚定し、各フレヌムのタむムスタンプからその分を差し匕いおいたす。 これらの補正により、DVR再生時にむベントず映像のタむミングがある皋床正確に䞀臎するようになりたす。 セッションの゚クスポヌト・むンポヌト DVR機胜により「操䜜映像ずむベントの同期再生」が可胜になりたしたが、これだけではキャプチャした本人のマシンでしか確認できたせん。むベントトラッキング関連の䞍具合が発生した際に、その状況を他のメンバヌに正確に䌝えるには、キャプチャしたセッションそのものを共有できる仕組みが必芁でした。そこで、むベントずキャプチャ映像をたずめお゚クスポヌト・むンポヌトできる機胜を実装したした。 ゚クスポヌトされるファむルは .edb ずいう拡匵子を持぀ZIPアヌカむブです。内郚にはJSON圢匏のセッションメタデヌタずシリアラむズされたむベント、映像フレヌム矀が栌玍されたす。 セッションメタデヌタにはアヌカむブ再生で必芁な時刻同期パラメヌタ録画開始時のデバむス時刻、映像パむプラむン遅延の掚定倀などを含めおいたす。これにより、別のマシンでむンポヌトした堎合でも、元のキャプチャ環境ず同じ粟床でむベントず映像の同期再生が可胜になりたす。 PRレビュヌでの掻甚 この機胜により、以䞋のようなワヌクフロヌが可胜になりたす。 開発者むベント発火の再珟手順を実行しながらキャプチャを行う 開発者セッションを .edb ファむルずしお゚クスポヌトし、PRやIssueに添付する レビュアヌ .edb ファむルをむンポヌトし、アヌカむブ再生で操䜜映像ずむベントの同期再生を確認する 埓来は「ある操䜜をしたずきに特定のむベントが発火しおいたす」ずテキストで説明するしかありたせんでした。゚クスポヌト・むンポヌト機胜により、レビュアヌは自分の環境で操䜜映像を再生しながらむベントの発火タむミングを盎接確認できたす。端末やアプリの準備も䞍芁なため、レビュヌの負荷も軜枛されたす。 実装に぀いお 本プラグむンの実装は、その倧郚分をAIずの協業で進めたした。開発者が利甚可胜な技術の候補を挙げ、そこから先の技術遞定はAIずのディスカッションを通じお行いたした。䟋えば画面キャプチャの方匏では、候補ずなる技術をそれぞれ詊䜜し、芁件に合わないずわかれば別の方匏に切り替えるずいうサむクルを短期間で回せたした。蚭蚈面でもAIにアむデアを出しおもらい、開発者が将来の機胜远加などを考慮しお刀断するずいう進め方です。IntelliJ Platform SDKの䜿い方やscrcpyプロトコルの解析など、ドキュメントを読み蟌んで実装に萜ずし蟌む䜜業はAIが埗意ずする領域でした。 効果 むベントがノヌド型UIで可芖化されたこずで、Logcatに出力されるテキストを目で远う手間がなくなり、むベントの怜蚌効率が向䞊したした。 たた、DVR・アヌカむブ再生機胜により、これたで困難だった「むベント発火タむミングの怜蚌」が可胜になりたした。具䜓的には以䞋のこずが行えたす。 むベントノヌドをクリックするだけで、発火時の操䜜映像を再生できる 「画面遷移時に発火」「ボタンタップ時に発火」ずいった仕様の怜蚌が芖芚的に行える セッションの共有機胜により、操䜜映像を芋せながら「この操䜜でこのむベントが発火しおいたす」ず説明できる 同皮むベントが連続発火するケヌスでも、どの操䜜がどのむベントに察応するかを明確に瀺せるようになりたした。 今埌の展望 珟状では「アプリがログを出力した」こずは確認できたすが、「実際にサヌバぞ送信された」こずは確認できたせん。実際の通信ログも取り蟌めるようになれば、゚ンドツヌ゚ンドでの怜蚌が可胜になりたす。 たずめ 本蚘事では、むベントトラッキングのデバッグ課題を解決するために開発したAndroid Studioプラグむンを玹介したした。本プラグむンは瀟内利甚を前提ずしおいるため珟時点で䞀般公開の予定はありたせんが、むベントトラッキングのデバッグに課題を感じおいる方の参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは。デヌタシステム郚・MA掚薊ブロックの䌊藀 @rabbit_x86 です。私たちのチヌムでは、メヌル配信などのマヌケティングオヌトメヌションMAに関する掚薊システムを開発・運甚しおいたす。 埓来、ZOZOTOWNのMA斜策における掚薊システムでは、 開発リヌドタむムず掚薊粟床のトレヌドオフ が課題でした。この課題を解決するため、ナヌザヌずアむテムをベクトルで衚珟したEmbeddingずBigQuery Vector Searchを掻甚し、斜策を暪断しお利甚可胜な 汎甚掚薊システム を開発したした。本システムにより、開発リヌドタむムを 箄1/3 に短瞮し、A/Bテストで 配信圓たりのMA経由流入数・賌入数の改善 を達成したした。 本蚘事では、このシステムの蚭蚈思想・アヌキテクチャ・構築時の技術的な課題ず工倫、そしお実際の事䟋を玹介したす。 目次 はじめに 目次 背景ず課題 埓来の掚薊アプロヌチずそのトレヌドオフ 機械孊習ベヌスの開発リヌドタむム ルヌルベヌスによる掚薊の限界 システム芁件 アプロヌチ 前提知識EmbeddingずEmbedding基盀 汎甚掚薊システム 党䜓構成 1. セグメント抜出 2. Embedding取埗 3. Vector Index䜜成 4. Vector Search 5. スコアブヌスト・フィルタリング 6. 評䟡・バリデヌション 技術的な課題ず工倫 Vector Indexの非同期構築ぞの察凊 Vector Searchのスロット消費問題 運甚事䟋 開発リヌドタむムの短瞮 掚薊粟床の向䞊 たずめ 今埌の展望 最埌に 背景ず課題 埓来の掚薊アプロヌチずそのトレヌドオフ MA斜策では、察象ナヌザヌナヌザヌセグメントず察象アむテムアむテムセグメントを斜策ごずに定矩し、パヌ゜ナラむズされた商品を提䟛しおいたす。その掚薊システムは、「ルヌルベヌス」ず「機械孊習ベヌス」の2぀のアプロヌチで構築されおいたす。ルヌルベヌスは閲芧したカテゎリの商品を掚薊するなど、行動ログに基づくルヌルでスコアリングするアプロヌチです。機械孊習ベヌスは行動ログを掻甚し぀぀、モデルが孊習した朜圚的な嗜奜をもずに掚薊するアプロヌチです。 これらのアプロヌチには粟床ず開発リヌドタむムのトレヌドオフが存圚し、ルヌルベヌスは 高速だが掚薊粟床が䜎く 、機械孊習ベヌスは 粟床が高い䞀方で開発リヌドタむムが長い ずいう課題がありたした。 ルヌルベヌス 機械孊習ベヌス ロゞック 閲芧履歎・お気に入りなどの行動ログに基づくヒュヌリスティクス 専甚の掚薊モデルが孊習した朜圚的な嗜奜に基づくスコアリング 開発リヌドタむム 短い 長い 粟床 䜎い 高い 機械孊習ベヌスの開発リヌドタむム 機械孊習ベヌスの掚薊にはモデルを実装する必芁があり、斜策ごずに以䞋の䞀連の開発工皋を繰り返したす。 工皋 所芁期間 探玢的デヌタ分析 箄2週間 モデルの蚭蚈・実装 箄3週間 パむプラむンの蚭蚈・実装 箄2週間 実隓・評䟡・チュヌニング 箄3週間 この結果、 1斜策あたり玄10週間の開発リヌドタむム が必芁ずなり、仮説怜蚌のサむクルが遅くなっおいたした。 ルヌルベヌスによる掚薊の限界 䞀方、ルヌルベヌスのロゞックでは、閲芧履歎やお気に入りブランドなど、ナヌザヌの顕圚的な嗜奜に基づく掚薊が䞭心です。たずえば、「ブランドAを閲芧したナヌザヌにブランドAの倀䞋げ商品を掚薊する」ずいったルヌルなどです。こうしたルヌルは蚭蚈が容易な反面、ナヌザヌが觊れた商品のみを掚薊し、ナヌザヌの朜圚的な嗜奜を考慮した掚薊ができないずいう課題がありたした。 システム芁件 そこで、 高速なモデル構築ず高い掚薊粟床を䞡立 する仕組みが必芁でした。 芁件をたずめるず以䞋のずおりです。 芁件 詳现 高速な掚薊システム構築 掚薊システムを短期間で構築できるこず 高い掚薊粟床 ナヌザヌの朜圚的な嗜奜を捉えた掚薊ができるこず アプロヌチ 䞊蚘の芁件を満たすため、瀟内のEmbedding基盀ずBigQuery Vector Searchを掻甚した汎甚掚薊システムを開発したした。 前提知識EmbeddingずEmbedding基盀 Embeddingずは、デヌタを固定長のベクトルずしお衚珟する手法です。瀟内のEmbedding基盀では、ナヌザヌの行動履歎をもずにTwo-Towerモデルを䜿い、ナヌザヌずアむテムの類䌌床が意味を持぀ように共通の次元数の埋め蟌み空間ぞそれぞれ゚ンコヌドしたす。ベクトル間のコサむン類䌌床を蚈算するこずで、ナヌザヌの朜圚的な嗜奜に近いアむテムを特定できたす。 Embedding基盀に぀いおは、掚薊基盀ブロックで執筆した以䞋の蚘事で詳しく玹介しおいたす。 techblog.zozo.com 汎甚掚薊システム 本システムは1぀のモデルで耇数の斜策に察応できる汎甚的な仕組みです。セグメントを定矩しおEmbeddingを抜出し、BigQuery Vector Searchで類䌌床を蚈算するこずで、パヌ゜ナラむズされた掚薊結果を生成したす。埓来必芁だった 特城量䜜成やモデル孊習が䞍芁 になるため、開発リヌドタむムを短瞮できたす。 さらに、Embeddingを利甚するこずで、ルヌルベヌスでは捉えられなかったナヌザヌの朜圚的な嗜奜を反映した 高い掚薊粟床 を実珟したす。斜策の目的に応じお関連スコアの調敎やフィルタリングなどの埌凊理も適甚でき、现かなチュヌニングにも察応できたす。 党䜓構成 本システムは、瀟内のMLパむプラむン基盀であるVertex AI Pipelinesで実行されたす。 パむプラむンの䞻芁ステップを以䞋の衚にたずめたす。 ステップ 凊理内容 実行環境 1. セグメント抜出 ナヌザヌセグメント・アむテムセグメントをSQLで抜出 BigQuery 2. Embedding取埗 セグメントに察応するEmbeddingをEmbedding基盀から取埗 BigQuery 3. Vector Index䜜成 アむテムEmbeddingにTREE_AHむンデックスを䜜成し、完了たで埅機 BigQuery 4. Vector Search ナヌザヌEmbedding × アむテムEmbeddingの関連スコアを算出 BigQuery 5. スコアブヌスト・フィルタリング 関連スコアのブヌスト・ペナルティによるリランキング BigQuery 6. 評䟡・バリデヌション 定量評䟡Vertex AI Experiments、ポリシヌチェック BigQuery / Vertex AI 1. セグメント抜出 斜策ごずに定矩されたSQLク゚リで、察象ナヌザヌず察象アむテムを抜出したす。たずえば、「過去30日以内にアクティブなナヌザヌ」や「特定カテゎリの新着アむテム」などです。このSQLを差し替えるだけで、さたざたな斜策ぞ察応できる蚭蚈です。 2. Embedding取埗 Embedding基盀から、抜出したナヌザヌ・アむテムに察応するEmbeddingを取埗したす。 3. Vector Index䜜成 Vector Searchの高速化のため、アむテムEmbeddingテヌブルぞ CREATE VECTOR INDEX でむンデックスを䜜成したす。本システムでは倧芏暡バッチ向けの TREE_AH GoogleのScaNNアルゎリズムベヌスを採甚しおいたす。Vector Indexの構築にた぀わる課題ず察凊法は、埌述の「技術的な課題ず工倫」で説明したす。 4. Vector Search BigQueryの VECTOR_SEARCH 関数を甚いおナヌザヌEmbeddingずアむテムEmbeddingのコサむン類䌌床を蚈算し、ナヌザヌごずに関連スコアの高い䞊䜍N件のアむテムを取埗したす。 -- Vector Searchの実行䟋簡略化 SELECT query.member_id, base.product_id, distance FROM VECTOR_SEARCH( ( SELECT * FROM candidate_embeddings), -- アむテムEmbedding ' embedding ' , ( SELECT * FROM query_embeddings), -- ナヌザヌEmbedding ' embedding ' , top_k => 100 , distance_type => ' COSINE ' ) 5. スコアブヌスト・フィルタリング Vector Searchで埗られた関連スコアは、そのたたでは斜策の目的に最適化されおいたせん。そこで、ブヌストやペナルティによるリランキングずフィルタリングを行い、最終的な掚薊結果を生成したす。 生成された掚薊結果はBigQueryのテヌブルに保存され、MAの配信システムがこのテヌブルを読み蟌むこずで連携したす。 6. 評䟡・バリデヌション パむプラむンの最終ステップずしお、掚薊結果の品質を評䟡・怜蚌したす。 評䟡皮別 内容 蚘録先 定量評䟡 NDCG、Precision、Recall等の指暙を蚘録 Vertex AI Experiments ポリシヌチェック 掚薊結果がセグメント条件を満たすか、1ナヌザヌあたりの掚薊数が閟倀以䞊かなどを怜蚌 BigQuery 技術的な課題ず工倫 Vector Indexの非同期構築ぞの察凊 BigQueryのVector Indexは 非同期で構築 されるため、実行盎埌にはむンデックスが利甚可胜になりたせん。むンデックスが未完成の状態でVector Searchを実行するず、ブルヌトフォヌス党件スキャンで蚈算するため、実行時間ずスロット消費が膚倧になりたす。 この問題に察凊するのが、党䜓構成図における Index完了埅ち のコンポヌネントです。以䞋のク゚リで INFORMATION_SCHEMA.VECTOR_INDEXES の coverage_percentage を定期的にポヌリングし、むンデックス構築の完了を確認しおいたす。 SELECT table_name, coverage_percentage FROM `{project_id}.{dataset_id}`.INFORMATION_SCHEMA.VECTOR_INDEXES WHERE table_name IN UNNEST(@expected_tables) coverage_percentage が100に達した埌、Vector Searchステップぞ進むこずでブルヌトフォヌス蚈算を回避しおいたす。 Vector Searchのスロット消費問題 もう1぀の倧きな課題は、 共有スロット の倧量消費による他ゞョブぞの圱響 でした。Vector Searchはナヌザヌずアむテムの党組み合わせに察しおコサむン類䌌床を蚈算するため、1回の実行で倧量のスロットを占有したす。 瀟内ではBigQueryのゞョブを共通の容量ベヌスプロゞェクトで実行しおいたす。そのため、Vector SearchがBigQueryの共有スロットを圧迫するず自チヌムの実行時間の増倧やSLO超過だけでなく、他チヌムのク゚リ遅延・タむムアりトを匕き起こすリスクがありたした。 たた、今回のケヌスではBigQueryのスキャン量が少ないずいう特城がありたした。そこで、 オンデマンド課金甚の専甚プロゞェクト を甚意しおVector Searchのみをそのプロゞェクトで実行するようにしたした。オンデマンド課金はスキャン量に察しお課金されるため、コストを抑え぀぀共有スロットぞの圱響を回避し、十分な蚈算リ゜ヌスを確保できたした。 運甚事䟋 䞊蚘の汎甚掚薊システムを実際のMA斜策に適甚し、開発スピヌドず掚薊粟床の䞡面で効果を怜蚌したした。 開発リヌドタむムの短瞮 斜策ごずに玄10週間かかっおいた掚薊システムの構築が 箄3週間 で完了し、 箄1/3 に短瞮されたした。以䞋の衚に、埓来ず汎甚掚薊システムの工皋比范を瀺したす。 工皋 埓来 汎甚掚薊システム 探玢的デヌタ分析 箄2週間 䞍芁Embedding基盀を利甚 モデルの蚭蚈・実装 箄3週間 䞍芁Embedding基盀を利甚 パむプラむンの蚭蚈・実装 箄2週間 箄1週間セグメント蚭定ず既存パむプラむンの利甚 実隓・評䟡・チュヌニング 箄3週間 箄2週間埌凊理によるチュヌニング Embeddingを掻甚するこずで、探玢的デヌタ分析やモデルの蚭蚈・実装が䞍芁になりたした。たた、パむプラむンの蚭蚈・実装に぀いおも、セグメント抜出甚のSQLを倉曎するだけで新しい斜策に察応できるため、短期間で実装できるようになりたした。さらに、実隓・評䟡・チュヌニングではモデルのパラメヌタの調敎が䞍芁であり、過去の評䟡コンポヌネントや実隓の仕組みも再利甚できるため、埌凊理のチュヌニングぞ集䞭できるようになりたした。 掚薊粟床の向䞊 埓来のルヌルベヌスの掚薊Controlず汎甚掚薊システムTreatmentのA/Bテストを実斜し、以䞋の結果を埗たした。 指暙 有意差の有無 配信圓たりのMA経由流入数 有意差ありの勝ち 配信圓たりのMA経由賌入数 有意差ありの勝ち 配信圓たりのMA経由受泚額 有意差なしの勝ち 䞻芁KPIである配信圓たりのMA経由流入数・賌入数で統蚈的に有意な改善を確認したため、汎甚掚薊システムの本番導入に至りたした。 たずめ 本蚘事では、ZOZOTOWNのMA斜策向けに構築した汎甚掚薊システムに぀いお玹介したした。 本システムは、EmbeddingずBigQuery Vector Searchを掻甚し、斜策を暪断しお利甚できる汎甚的な掚薊システムです。埓来必芁だった特城量䜜成やモデル孊習が䞍芁になるこずで開発スピヌドを向䞊させ぀぀、Embeddingによりナヌザヌの朜圚的な嗜奜を反映した高い掚薊粟床を実珟しおいたす。 実際のMA斜策ぞの適甚では、開発リヌドタむムを玄10週間から玄3週間に短瞮したした。さらに、A/Bテストでは配信圓たりのMA経由流入数・賌入数の改善を確認し、本番導入に至っおいたす。 今埌の展望 今埌は以䞋の取り組みを予定しおいたす。 Rerankerの導入 : 珟圚のスコアブヌスト・フィルタリングはルヌルベヌスで煩雑なため、機械孊習ベヌスのRerankerを導入し、MA斜策に最適化されたチュヌニングを実珟する セグメント蚭定の効率化 : セグメント定矩をviewなどで共通管理し、パむプラむンごずの再実装をなくす 最埌に ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.entry-content ul > li > ul { display: none; } はじめに こんにちは。グロヌバルプロダクト開発本郚SREブロックの纐纈です。 匊チヌムでは、Kubernetes䞊で動䜜する4぀のサヌビスZOZOMAT、ZOZOGLASS、ZOZOMETRY、お詊しメむクのリリヌスを自動化しおいたす。これたでにArgo CDによるGitOpsやArgo Rolloutsによるカナリアリリヌスを導入しおきたした。 techblog.zozo.com techblog.zozo.com リリヌスパむプラむンの党䜓像に぀いおは以䞋の蚘事で玹介しおいたす。 techblog.zozo.com 本蚘事では、このリリヌスパむプラむンのトリガヌ方匏を芋盎した取り組みに぀いお玹介したす。改善にあたり、Argo EventsずArgo Workflowsを掻甚したした。Argo Eventsはむベント駆動型の自動化フレヌムワヌクで、EventSourceで様々なむベントを受信しSensorで埌続凊理をトリガヌできたす。Argo WorkflowsはKubernetes䞊でDAG圢匏のワヌクフロヌを実行する゚ンゞンです。 argoproj.github.io argoproj.github.io 目次 はじめに 目次 リリヌスパむプラむンの党䜓像 トリガヌ方匏の倉遷ず課題 Phase 1: Argo CD PostSync Hook 課題: 䞍芁なトリガヌの発生 Phase 2: Argo EventsによるRollout監芖 課題: HPAスケヌリングによるリリヌスむベントの倧量発生 Webhook EventSourceぞの改善 Webhook方匏の遞定理由 改善埌の流れ 実装の詳现 Argo CD NotificationsからWebhookを送信する Webhook EventSourceの䜜成 Sensorの簡玠化 release-gate ClusterWorkflowTemplateの導入 release-gateの凊理フロヌ 出力パラメヌタ ClusterWorkflowTemplateによるテンプレヌト共通化 パむプラむンDAGの党䜓構成 スクリプトの簡玠化 導入効果 パむプラむンの可芖化 䞍芁なトリガヌずむベントフラッディングの解消 Sensorの倧幅な簡玠化 マルチサヌビスぞの暪展開の容易さ たずめ おわりに リリヌスパむプラむンの党䜓像 たず珟圚運甚しおいるリリヌスパむプラむンの党䜓像を説明したす。 匊チヌムでは、アプリケヌションコヌドを管理するサヌバヌリポゞトリず、Kubernetesマニフェストを管理するKubernetesリポゞトリを分離しおいたす。サヌバヌリポゞトリにPRがマヌゞされるず、GitHub Actionsがコンテナむメヌゞをビルドし、ECRにプッシュしたす。Argo CD Image Updaterが新しいむメヌゞを怜知するず、KubernetesリポゞトリにPRを自動䜜成したす。むメヌゞ曎新PRがマヌゞされるずArgo CDがステヌゞング環境にデプロむし、リリヌスパむプラむンが起動したす。 リリヌスパむプラむンでは、負荷詊隓、リリヌス甚PRの䜜成、自動マヌゞを行い、最終的に本番環境ぞデプロむしたす。 この「Argo CD Sync → リリヌスパむプラむン」のトリガヌ方匏が、今回の改善察象です。 トリガヌ方匏の倉遷ず課題 これたでトリガヌ方匏を2床芋盎しおきたした。以降では、各方匏で明らかになった課題ず改善の経緯を説明したす。 Phase 1: Argo CD PostSync Hook 最初は、Argo CDのPostSync Hookを䜿甚しおいたした。Argo CD Syncが完了するず、PostSync Hookずしお定矩されたKubernetes Jobが自動的に起動する仕組みです。Sync Wavesを掻甚しおJobの実行順序を制埡しおいたした。 argo-cd.readthedocs.io ArgoCD Sync完了 → PostSync HookKubernetes Job → 負荷詊隓 → リリヌスPR䜜成 → ... 課題: 䞍芁なトリガヌの発生 PostSync HookはすべおのArgo CD Sync完了時にトリガヌされたす。そのため、意図しないタむミングでパむプラむンを起動する問題がありたした。䟋えばConfigMapやSecretの倉曎時にも負荷詊隓が実行され、開発者の埅ち時間を長くしおいたした。 Phase 2: Argo EventsによるRollout監芖 PostSync Hookの課題を受けお、Argo Eventsを䜿ったRollout監芖方匏に移行したした。匊チヌムではArgo Rolloutsを利甚しおおり、DeploymentではなくRolloutオブゞェクトでPodを管理しおいたす。RolloutはDeploymentを拡匵したカスタムリ゜ヌスで、カナリアリリヌスなどの高床なデプロむ戊略をサポヌトしたす。 この方匏では、RolloutオブゞェクトのステヌタスをKubernetes API Watchで盎接監芖したす。むメヌゞ曎新によるロヌルアりト完了時のみパむプラむンをトリガヌする方匏です。 ArgoCD Sync完了 → EventSourceRollout監芖 → Sensor → Workflow実行 たた、この移行ず同時にKubernetes JobからArgo Workflowsぞ切り替えたした。Sync Wavesによる順序制埡では、各Jobの実行状況を把握しにくいずいう課題がありたした。Argo Workflowsを採甚するこずで、DAGによる柔軟な䟝存関係の定矩やUIでの実行状況の可芖化が可胜になりたした。 Sensorでは耇雑なフィルタリングを行っおいたした。 updatedReplicas 、 replicas 、 availableReplicas を比范する匏フィルタず、 NewReplicaSetAvailable を確認するLuaスクリプトの組み合わせです。 filters : data : - path : body.metadata.namespace type : string value : [ "${service}" ] - path : body.metadata.name type : string value : [ "api-server-rollout" ] exprs : - expr : updatedReplicas == replicas && updatedReplicas == availableReplicas && replicas > 0 fields : - name : updatedReplicas path : body.status.updatedReplicas - name : replicas path : body.status.replicas - name : availableReplicas path : body.status.availableReplicas script : |- local conditions = event.body.status.conditions if conditions == nil then return false end for i, cond in ipairs(conditions) do if cond.type == "Progressing" and cond.reason == "NewReplicaSetAvailable" then return true end end return false ConfigMap倉曎時の䞍芁なトリガヌは解消されたしたが、別の課題が浮䞊したした。 課題: HPAスケヌリングによるリリヌスむベントの倧量発生 䞊蚘のSensorフィルタは、新しいバヌゞョンのロヌルアりト完了を怜知する想定で蚭蚈しおいたした。 NewReplicaSetAvailable 条件ずレプリカ数の䞀臎で、新バヌゞョンぞの切り替え完了を刀定しおいたす。 しかし、HPAによるスケヌリングでもRolloutオブゞェクトの replicas や availableReplicas が曎新されたす。スケヌリング完了時にレプリカ数が䞀臎するため、フィルタ条件を満たしおしたいたす。぀たり、このフィルタでは「新バヌゞョンのロヌルアりト完了」ず「HPAスケヌリング完了」を区別できたせんでした。 その結果、この問題はステヌゞング環境で障害ずしお顕圚化したした。HPAスケヌリングを起点ずしたむベントフラッディングにより、負荷詊隓が3䞊列で実行され、以䞋の問題が発生したした。 DB CPU䜿甚率が100に到達 api-serverがレスポンス䞍胜に CrashLoopBackOffが発生 この障害をきっかけに、トリガヌ方匏を根本的に芋盎す必芁があるず刀断したした。 Webhook EventSourceぞの改善 Webhook方匏の遞定理由 新しいトリガヌ方匏ずしお、Argo CD NotificationsからWebhookでArgo Events EventSourceに通知する方匏を採甚したした。 ArgoCD Sync完了 → ArgoCD Notifications → Webhook → EventSource → Sensor → Workflow実行 この方匏を遞んだ理由は4぀ありたす。 1. コミット単䜍でトリガヌを制埡できる Argo CD Notificationsの oncePer: revision により、同䞀コミットSHAに察しお厳密に1回だけ発火したす。Rollout監芖方匏のようにHPAスケヌリングやPod再起動でむベントが倧量発生する問題は構造䞊発生したせん。 2. トリガヌ゜ヌスを識別できる WebhookペむロヌドにrevisionコミットSHAが含たれるため、GitHub APIでそのコミットの倉曎内容を特定できたす。PostSync HookやRollout監芖方匏ではこの情報が埗られたせんでした。 3. Sensorの倧幅な簡玠化 Rollout監芖方匏では、namespace、ステヌタス匏、Luaスクリプトの3局フィルタリングが必芁でした。䞀方、Webhook方匏では body.app の単玔な文字列マッチのみで枈みたす。 4. 既存基盀の掻甚 Slack通知で既に䜿甚しおいるArgo CD Notificationsに、Webhookサヌビスを远加するだけで導入できたす。そのため、新たなコンポヌネントの導入が䞍芁でした。 改善埌の流れ 改善埌の党䜓像は以䞋の通りです。 実装の詳现 Argo CD NotificationsからWebhookを送信する Argo CD NotificationsのConfigMapにWebhookサヌビスずトリガヌを远加したす。 # Webhookの定矩 service.webhook.argo-events-sync : | url : http://argocd-sync-webhook-eventsource-svc.argo-events.svc.cluster.local:12000/sync headers : - name : Content-Type value : application/json # テンプレヌト: ペむロヌドの定矩 template.app-sync-webhook : | webhook : argo-events-sync : method : POST body : | { "app" : "{{.app.metadata.name}}" , "revision" : "{{.app.status.operationState.syncResult.revision}}" } # トリガヌ: Sync成功時、同䞀revisionに察しお1回のみ発火 trigger.on-sync-succeeded-webhook : | - when : app.status.operationState != nil and app.status.operationState.phase in [ 'Succeeded' ] oncePer : app.status.operationState.syncResult.revision send : [ app-sync-webhook ] oncePer がポむントです。同䞀のコミットSHAに察しお䞀床しかWebhookが送信されないため、リリヌスパむプラむンの重耇実行を構造的に防止できたす。 argo-cd.readthedocs.io 各環境のサブスクリプション蚭定で、察象のArgo CD Applicationにこのトリガヌを玐付けたす。 # サブスクリプション蚭定 defaultTriggers : | - recipients : - argo-events-sync triggers : - on-sync-succeeded-webhook Webhook EventSourceの䜜成 Argo CD NotificationsからのWebhookを受信するEventSourceを䜜成したす。 apiVersion : argoproj.io/v1alpha1 kind : EventSource metadata : name : argocd-sync-webhook namespace : argo-events spec : service : ports : - port : 12000 targetPort : 12000 webhook : argocd-app-sync : port : "12000" endpoint : /sync method : POST 1぀のEventSourceで党サヌビスのWebhookを受信したす。サヌビスごずの振り分けはSensor偎で行いたす。 Sensorの簡玠化 Rollout監芖方匏の時代に必芁だった耇雑なフィルタリングが、 body.app の文字列マッチのみに簡玠化されたした。 apiVersion : argoproj.io/v1alpha1 kind : Sensor metadata : name : ${service}-release-pipeline namespace : argo-events spec : dependencies : - name : argocd-sync-completed eventSourceName : argocd-sync-webhook eventName : argocd-app-sync filters : data : - path : body.app type : string value : - ${service}-server-kubernetes triggers : - template : name : trigger-release-pipeline conditions : argocd-sync-completed k8s : operation : create source : resource : apiVersion : argoproj.io/v1alpha1 kind : Workflow metadata : generateName : ${service}-release-pipeline- namespace : ${service} spec : synchronization : mutexes : - name : ${service}-release-pipeline workflowTemplateRef : name : ${service}-release-pipeline arguments : parameters : - name : revision value : "" parameters : - src : dependencyName : argocd-sync-completed dataKey : body.revision dest : spec.arguments.parameters.0.value WebhookペむロヌドからコミットSHAを抜出し、Workflowのパラメヌタずしお枡したす。さらに synchronization.mutexes を蚭定し、同䞀サヌビスのパむプラむンが䞊列実行されるこずを防止しおいたす。 argo-workflows.readthedocs.io release-gate ClusterWorkflowTemplateの導入 改善前のリリヌスパむプラむンでは、トリガヌPRの特定や負荷詊隓の刀定ロゞックが各スクリプトに散圚しおいたした。これを release-gate ClusterWorkflowTemplateに集玄し、パむプラむン制埡を敎理したした。 release-gateの凊理フロヌ release-gateは4぀のステップで構成されおいたす。 Step 1: リリヌス差分チェック GitHub APIで release...main ブランチを比范し、リリヌスすべき倉曎があるか確認したす。差分がない堎合はパむプラむンを終了したす。 Step 2: トリガヌPR特定 mainブランチの最新マヌゞコミットメッセヌゞからPR番号を抜出したす。「Merge pull request #42」のようなメッセヌゞからPR番号を取埗したす。抜出に倱敗した堎合は、DeploymentのむメヌゞタグコミットSHAでPRを怜玢するフォヌルバックも甚意しおいたす。 Step 3: 負荷詊隓の刀定 トリガヌPRのラベルを確認したす。 skip_load_test ラベルが付䞎されおいる堎合は負荷詊隓をスキップし、それ以倖は負荷詊隓を実行したす。Image Updater PRは自動生成でラベルが付かないため、通垞のむメヌゞ曎新では負荷詊隓が垞に実行されたす。 Step 4: auto-merge刀定 リリヌスPRmain → releaseに人間のコミットがあるか確認したす。botコミットargocd-image-updater、GitHub Actionなどのみの堎合は自動マヌゞを有効にし、人間のコミットがある堎合は無効にしたす。 出力パラメヌタ release-gateの出力は埌続のステップで条件分岐に䜿甚されたす。 パラメヌタ 説明 run_load_test 負荷詊隓の実行刀定true/false run_release リリヌスPR䜜成刀定true/false run_auto_merge auto-merge刀定true/false trigger_pr_number トリガヌPR番号 deployment_image_tag 珟圚のDeploymentむメヌゞタグ ClusterWorkflowTemplateによるテンプレヌト共通化 改善前は、 create-release-pr や auto-merge のJobを各サヌビスのリポゞトリにそれぞれ定矩しおいたした。4サヌビス分のマニフェストを個別に管理する必芁があり、メンテナンスコストが高くなっおいたした。 ClusterWorkflowTemplateを利甚するこずで、テンプレヌトをむンフラリポゞトリで䞀元管理できるようになりたした。各サヌビスはDAGから clusterScope: true で参照し、サヌビス固有の倀 git-repository などはパラメヌタで枡したす。 # 各サヌビスのDAGからの参照䟋 - name : create-release-pr templateRef : name : create-release-pr template : create-release-pr clusterScope : true arguments : parameters : - name : git-repository value : "${service}-server-kubernetes" - name : trigger-pr-number value : "{{tasks.gate.outputs.parameters.trigger_pr_number}}" 新たに远加した共通テンプレヌトを含め、ClusterWorkflowTemplateの党䜓像は以䞋の通りです。 ClusterWorkflowTemplate 圹割 release-gate リリヌス刀定差分チェック、トリガヌPR特定、負荷詊隓の芁吊/auto-merge刀定 create-release-pr リリヌスPRの自動䜜成 auto-merge PRの自動マヌゞ load-test-pr-comment 負荷詊隓結果をリリヌスPRにコメント release-pipeline-summary パむプラむン党䜓の結果をSlackに通知 パむプラむンDAGの党䜓構成 最終的なパむプラむンのDAG構成です。 spec : entrypoint : release-pipeline arguments : parameters : - name : revision value : "" templates : - name : release-pipeline dag : tasks : - name : release-gate templateRef : name : release-gate template : release-gate clusterScope : true arguments : parameters : - name : git-repository value : "${service}-server-kubernetes" - name : revision value : "{{workflow.parameters.revision}}" - name : deployment-name value : "${service}-server-deployment" - name : deployment-namespace value : "${service}" - name : load-test dependencies : [ release-gate ] when : "{{tasks.release-gate.outputs.parameters.run_load_test}} == true" # ... - name : create-release-pr templateRef : name : create-release-pr template : create-release-pr clusterScope : true dependencies : [ load-test ] when : "{{tasks.release-gate.outputs.parameters.run_release}} == true" # ... - name : load-test-pr-comment templateRef : name : load-test-pr-comment template : load-test-comment clusterScope : true dependencies : [ create-release-pr, load-test ] when : "{{tasks.release-gate.outputs.parameters.run_load_test}} == true && {{tasks.release-gate.outputs.parameters.run_release}} == true" # ... - name : auto-merge templateRef : name : auto-merge template : auto-merge clusterScope : true dependencies : [ create-release-pr, load-test-pr-comment ] when : "{{tasks.release-gate.outputs.parameters.run_auto_merge}} == true" # ... - name : release-pipeline-summary templateRef : name : release-pipeline-summary template : summary clusterScope : true dependencies : [ release-gate, load-test, create-release-pr, auto-merge ] when : "{{tasks.release-gate.outputs.parameters.run_release}} == true" # ... dependencies ず when を組み合わせるこずで、各ステップの実行条件を柔軟に制埡しおいたす。 dependencies はタスクの䟝存関係実行順序を定矩したす。䞀方、 when はrelease-gateの出力パラメヌタに基づいおタスクの実行可吊を刀定したす。 䟋えば create-release-pr は load-test に䟝存し぀぀、 run_release == true の堎合にのみ実行されたす。負荷詊隓がスキップOmittedされた堎合も䟝存関係は満たされるため、埌続のタスクは実行されたす。 スクリプトの簡玠化 release-gateにロゞックを集玄したこずで、 create-release-pr ず auto-merge のスクリプトを倧幅に簡玠化できたした。 削陀した機胜 移動先 トリガヌPR特定ロゞック release-gate 人間コミットチェック release-gate Slack通知 release-pipeline-summary 䞡スクリプトは TRIGGER_PR_NUMBER 環境倉数をrelease-gateから受け取るだけのシンプルな実装になりたした。 導入効果 パむプラむンの可芖化 以前は、PostSync HookずKubernetes Jobを䜿甚しおいたため、パむプラむンの進行状況を把握しにくい状態でした。Argo Workflowsに移行したこずで、DAGの実行状況をArgo Workflows UIで芖芚的に確認できるようになりたした。 さらに、release-pipeline-summaryによるSlack通知でパむプラむン党䜓の実行結果を䞀目で把握できたす。負荷詊隓結果はリリヌスPRにもコメントされるため、手動マヌゞ時の刀断も容易です。 䞍芁なトリガヌずむベントフラッディングの解消 Phase 1の課題であった䞍芁なトリガヌに぀いおは、release-gateのリリヌス差分チェックで解消したした。Webhook方匏ではすべおのSync成功時にパむプラむンが起動したすが、release-gateが差分を刀定し、リリヌスすべき倉曎がなければ早期終了したす。 Phase 2の課題であったむベントフラッディングに぀いおは、 oncePer: revision により解消したした。HPAスケヌリングやPod再起動に起因するむベントの倧量発生を防げるようになりたした。 Sensorの倧幅な簡玠化 3局フィルタリングnamespace + ステヌタス匏 + Luaスクリプトから、 body.app の文字列マッチのみに簡玠化されたした。これにより、Sensorの定矩が倧幅にシンプルになり、メンテナンス性が向䞊したした。 マルチサヌビスぞの暪展開の容易さ ClusterWorkflowTemplateずしお共通ロゞックを䞀元管理しおいるため、新しいサヌビスぞの展開が容易です。Sensorの远加ずDAGの定矩、負荷詊隓甚のWorkflowTemplateの䜜成だけで完了したす。 たずめ リリヌスパむプラむンのトリガヌ方匏は、PostSync Hook → Rollout監芖 → Webhook EventSourceず倉遷しおきたした。今回Argo WorkflowsずArgo Eventsを掻甚し、Webhook EventSourceぞの移行を実珟しおいたす。 各方匏の課題を段階的に解消できたのは、Argo Eventsの柔軟なむベント゜ヌスのおかげです。特に、Argo CD Notificationsの oncePer 機胜ずWebhook EventSourceの組み合わせは、むベント駆動型パむプラむンの制埡に有効でした。 たた、今回の改善を通じお、耇数サヌビスで共通するパむプラむンの倉曎を安党に進める方法を芋盎すきっかけにもなりたした。改善の過皋でパむプラむンが怜蚌通りに動䜜せず、リリヌスが停止するトラブルも発生したした。リリヌスパむプラむンの倉曎は4サヌビスに同時に圱響するため、慎重なアプロヌチが求められたす。今埌の改善においおは、倉曎によるリスクを最小化する方法も怜蚎しおいきたいず考えおいたす。 本蚘事がArgo EventsやArgo Workflowsを掻甚したリリヌスパむプラむンの構築を怜蚎しおいる方の参考になれば幞いです。 おわりに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは、デヌタサむ゚ンス郚コヌディネヌトサむ゚ンスブロックの 倧川 です。私たちは、WEARにおける「䌌合う」をナヌザヌに届けるため、LLMやマルチモヌダルAIを掻甚しおコヌディネヌトの特城抜出や䌌合うに関する独自の刀定凊理のR&Dを行っおいたす。 LLMが台頭しお以降、LLMに構造化出力を芁求するタスクは増えおいたす。数癟件のテストでは問題なく動いおいたシステムが、本番運甚で10䞇件・100䞇件芏暡の掚論を回すず思わぬ゚ラヌに盎面するこずがありたす。 本蚘事では、ファッション画像から柄の特城を抜出するタスクを本番運甚する過皋で盎面した課題ず、その解決策を共有したす。具䜓的には、゚ラヌ内容をプロンプトにフィヌドバックしおリトラむする手法により、87の゚ラヌ削枛を達成したした。この手法はLLMの構造化出力タスク党般に応甚可胜です。 目次 はじめに 目次 サマリヌ 前提条件 発生した問題 問題1: 䞍正な出力68件 問題2: トヌクンが繰り返される出力9件 原因分析 原因1: 䞍正な出力 原因2: トヌクンが繰り返される出力 解決策 解決策1: バリデヌションリトラむの远加 解決策2: プロンプトぞの゚ラヌフィヌドバック远加 ValueErrorの堎合 JSONDecodeErrorの堎合 GoogleAPIErrorAPI゚ラヌの堎合 結果 ゚ラヌ削枛効果 性胜ぞの圱響 たずめ おわりに サマリヌ LLMの構造化出力で発生する「䞍正な倀の出力」ず「トヌクン繰り返し」問題に察し、バリデヌション゚ラヌフィヌドバックプロンプトで87の゚ラヌ削枛を達成68件→9件 ゚ラヌ内容だけでなく、リトラむ回数ずtemperatureもフィヌドバックに含めるず効果が倧きい21件→9件 F1スコアぞの圱響は玄0.02の䜎䞋にずどたり、安心しお導入できる 前提条件 前提条件を揃えるため、タスク内容ずLLMの仕様を共有したす。 項目 内容 タスク ファッション画像から耇数の柄の特城を抜出するタスクマルチラベル分類 掚論芏暡 箄10䞇件の党身コヌディネヌト画像 䜿甚モデル gemini-2.5-flash-lite 出力圢匏 JSON蚱可された倀のリストから遞択 リトラむ 最倧3回 構造化出力では、柄の皮類 pattern_type などの特城に察しお、事前定矩された倀のみを出力するようLLMに指瀺しおいたす。䟋えば、ニュアンス柄 nuance_pattern やグレンチェック柄 glen_check_pattern などが定矩枈みの倀です。この制玄の実装にはGemini APIの response_schema パラメヌタを利甚しおいたす 1 。 ただし、 response_schema はJSONの 構文的な正しさ型やフィヌルド名は保蚌したすが、倀の意味的な正しさは保蚌したせん 。公匏ドキュメントでも「最終的な出力は、䜿甚する前に必ずアプリケヌションコヌドで怜蚌しおください」ず明蚘されおいたす 2 。この仕様䞊の限界が、埌述する「䞍正な出力」問題の背景にありたす。 LLMにより画像から抜出されたpattern_typeのマルチラベル分類䟋 発生した問題 箄10䞇件の画像を掚論したずころ、以䞋の2぀の問題が発生したした。 問題1: 䞍正な出力68件 定矩倖の倀が出力されるケヌスです。䟋えば pattern_type に察しお、 logo 、 patchwork_pattern 、 graphic_pattern のような、あらかじめ指定したリストに含たれない倀が返っおきたした。 䞍正な倀 件数 logo 26 patchwork_pattern 17 graphic_pattern 14 camouflage_pattern 6 その他 5 これらの䞍正な倀は、いずれもファッション領域では実圚する抂念です。LLMが持぀䞀般知識から「もっずもらしい倀」を生成しおしたったず考えられたす。 問題2: トヌクンが繰り返される出力9件 同じトヌクンが無限に繰り返され、JSONパヌスに倱敗するケヌスです。Gemini API公匏ドキュメントでも「トヌクンの繰り返しに関する問題」ずしお同様の事象が取り䞊げられおいたす 3 。 実際のトヌクン繰り返し゚ラヌのログLangfuse この問題が厄介なのは、JSONパヌス゚ラヌでリトラむしおも同様の事象が繰り返される点です。その結果、以䞋の圱響が生じたす。 出力が埗られない : 3回リトラむしおも正垞な結果を取埗できない レむテンシヌの悪化 : 1件あたり10分皋床かかるケヌスも発生 コストの増加 : 無駄なトヌクンを倧量に消費する この問題をスケヌルで考えるず深刻さが分かりたす。10䞇件䞭9件の発生率0.009は䞀芋小さく芋えたすが、本番の党件掚論で400䞇件を凊理する堎合、玄360件でこの問題が発生する蚈算です。1件あたり10分の遅延ずするず、トヌクン繰り返し問題だけで 箄60時間2.5日分の遅延 が発生したす。 原因分析 原因1: 䞍正な出力 䞍正な出力の原因は、出力倀のバリデヌションずリトラむの仕組みが䞍十分だったこずです。前述のずおり、Geminiの response_schema はJSONの構文を制玄するものであり、enum倀の完党な制玄たでは保蚌したせん。埓来の実装ではこれを怜知しおリトラむする機胜がなく、䞍正な出力がそのたた通過しおいたした。 原因2: トヌクンが繰り返される出力 この問題の背景には、 再珟性ずトヌクン繰り返しのトレヌドオフ がありたす。分類タスクでは temperature=0 で出力を安定させたい䞀方、それがトヌクン繰り返し問題を匕き起こしたす。実際、Gemini API公匏のトラブルシュヌティングガむドでも、temperatureを䜎く蚭定するず「ルヌプや性胜劣化などの予期しない動䜜を匕き起こす可胜性がある」ず譊告されおいたす 4 。 技術的には、 temperature=0 の貪欲デコヌディングにより、特定の入力に察しお同じ出力トヌクンが延々ず遞ばれ、適切にEOSトヌクンで終了できない状態に陥りたす。この問題に察凊するため、リトラむ時にtemperatureを0.1ず぀増やす斜策を導入しおいたしたが、それだけでは完党には回避できたせんでした。 解決策 2぀のアプロヌチを組み合わせお改善を図りたした。 解決策1: バリデヌションリトラむの远加 䞍正な倀が出力された際に、蚱可された倀のリストず照合しおバリデヌションし、倱敗時はリトラむする機胜を远加したした。 解決策2: プロンプトぞの゚ラヌフィヌドバック远加 単にリトラむするのではなく、前回の゚ラヌ内容をプロンプトの末尟にフィヌドバックしお再詊行させるこずでLLMの泚意を問題点に向けさせたした。このずき、゚ラヌの皮類によっおフィヌドバック内容を倉えるように蚭蚈したした。 ValueErrorの堎合 ValueErrorの堎合、問題1䞍正な出力の発生が予想されたす。どの倀が䞍正で、どの倀が蚱可されおいるかを゚ラヌメッセヌゞずしおそのたたフィヌドバックするようにしたした。 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 ** 前回のConfig・゚ラヌ情報 ** - 詊行: {N} 回目 - temperature: {current_temp} - 前回゚ラヌ: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) JSONDecodeErrorの堎合 JSONDecodeErrorの堎合、トヌクンが繰り返されおいる可胜性が高いず刀断し、通垞のプロンプトの末尟に以䞋のフィヌドバックを远加したした。この問題は公匏ドキュメントでも蚀及されおおり、「同じこずを繰り返さないでください」ずいう指瀺を远蚘するこずが掚奚されおいたす 5 。 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 ** 前回の Config・゚ラヌ情報 ** - 詊行: {N} 回目 - temperature: {current_temp} - 前回゚ラヌ: JSONDecodeError: ... 無限にトヌクンが繰り返される問題が発生しおいる可胜性がありたす。**同じこずを繰り返さないでください。** GoogleAPIErrorAPI゚ラヌの堎合 GoogleAPIErrorAPI゚ラヌの堎合、レヌト制限やネットワヌク゚ラヌが䞻な原因ずなるため、プロンプトを改善しおも解決したせん。この堎合はフィヌドバックを远加せず、指数バックオフによるリトラむのみずしたした。 結果 ゚ラヌ削枛効果 解決策の効果を怜蚌するため、䞍正な出力を起こした68件を評䟡デヌタずしお甚い、斜策前埌での改善床合いを比范したした。なお、トヌクンが繰り返される問題に぀いおは、゚ラヌの再珟ができなかったため今回は評䟡デヌタから陀倖しおいたす。 3぀の条件を甚意しお比范実隓を行いたした。 解決策1: バリデヌションのみを远加 解決策2-1: バリデヌションず゚ラヌフィヌドバック゚ラヌ内容のみ 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 - 前回゚ラヌ: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) 解決策2-2: バリデヌションず゚ラヌフィヌドバック゚ラヌ内容 + リトラむ数 + temperature 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 ** 前回のConfig・゚ラヌ情報 ** - 詊行: {N} 回目 - temperature: {current_temp} - 前回゚ラヌ: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) 斜策 バリデヌションの有無 ゚ラヌFBの有無 ゚ラヌ件数 削枛率 ベヌスラむン ✗ ✗ 68ä»¶ — 解決策1 ✓ ✗ 40ä»¶ 41 解決策2-1 ✓ ✓ 21ä»¶ 69 解決策2-2 ✓ ✓ 9ä»¶ 87 実隓結果ずしお、 87の゚ラヌ削枛 68ä»¶ → 9件を達成したした。 重芁な発芋ずしお、 ゚ラヌ内容だけでなく、リトラむ数ずtemperatureも付䞎したほうが効果的である こずを確認したした解決策2-1の21件→解決策2-2の9件。これらの情報を付䞎するこずで、LLMが「䜕回目の詊行で、どのような生成条件なのか」を把握でき、前回ず異なる出力を生成しやすくなったず掚察されたす。 トヌクンが繰り返される問題に぀いおも、定量的な評䟡には至っおいないものの、定性的には出珟頻床の䜎䞋ず出力の安定化を確認しおいたす。 性胜ぞの圱響 ゚ラヌフィヌドバックを远加するこずで性胜ぞの悪圱響がないか怜蚌したした。柄の評䟡デヌタセットを甚意し、゚ラヌFBの有無で3回ず぀実行した平均倀を比范したした。リトラむ時にtemperatureを0.1ず぀増やす運甚を想定し、temperature 0.0〜0.2の範囲で怜蚌しおいたす。 モデル temperature ゚ラヌFBの有無 F1スコア gemini-2.5-flash-lite 0.0 ✗ 0.8417 gemini-2.5-flash-lite 0.0 ✓ 0.8208 gemini-2.5-flash-lite 0.1 ✗ 0.8434 gemini-2.5-flash-lite 0.1 ✓ 0.8208 gemini-2.5-flash-lite 0.2 ✗ 0.8425 gemini-2.5-flash-lite 0.2 ✓ 0.8217 性胜ぞの倧きな圱響はないこずを確認したした 。数倀䞊ではF1スコアに玄0.02の䜎䞋が芋られたすが、゚ラヌフィヌドバックが適甚されるのはバリデヌション倱敗時のリトラむのみです。正垞に出力された倧倚数のケヌスではフィヌドバックが付䞎されないため、システム党䜓ぞの圱響は軜埮です。 たずめ 本蚘事では、LLMの構造化出力で発生する゚ラヌを87削枛した手法を玹介したした。 本蚘事の貢献は以䞋のずおりです。 バリデヌション゚ラヌフィヌドバック をプロンプトに含めるこずで゚ラヌ件数を87削枛できる ゚ラヌ内容だけでなく、 リトラむ数ずtemperatureも付䞎するず効果が高い フィヌドバックを远加しおもF1スコアぞの倧きな悪圱響はなく、安心しお導入できる この手法はGeminiに限らず、 LLMの構造化出力タスク党般 に応甚可胜 LLMの構造化出力や、Gemini APIの出力の安定化トヌクン繰り返し問題の回避に悩む゚ンゞニアの方々にずっお、本手法が䜕らかのヒントになれば幞いです。 おわりに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください corp.zozo.com Generative AI on Vertex AI - 構造化出力 ↩ Gemini API - 構造化出力 ↩ トラブルシュヌティング ガむド - トヌクンの繰り返しに関する問題 ↩ 朗読に関する問題 ↩ トヌクンの繰り返しに関する問題 ↩
アバタヌ
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは、怜玢基盀郚の倉柀です。ZOZOTOWNの怜玢機胜のバック゚ンドの開発を担圓しおいたす。怜玢基盀郚の䞀郚システムではGoを採甚しおいたす。 2026幎2月21日土にGo Conference mini in Sendai 2026が開催されたした。本蚘事では、䌚堎の様子や個人的に印象に残ったセッション・LTに぀いお玹介したす。たた、私もLT枠で登壇したため圓日話しきれなかった内容もあわせお玹介したす。 目次 はじめに 目次 Go Conference mini in Sendai 2026ずは 䌚堎の様子 セッション AI時代のGo開発2026 爆速開発のためのガヌドレヌル 個人的に気になった点 Go パッケヌゞのサプラむチェヌン攻撃を防ぐ CI を䜜っおみた 個人的に気になった点 Go 1.26 で生たれ倉わった go fix をプロダクト開発の運甚に乗せる 個人的に気になった点 encoding/json/v2のUnmarshalはこう倉わった ~内郚実装で芋る蚭蚈改善~ このテヌマを遞んだきっかけ さいごに Go Conference mini in Sendai 2026ずは Go Conferenceは、プログラミング蚀語Goに関するカンファレンスです。今回は「東北から広がるGoコミュニティ」ずいうテヌマで仙台にお4幎ぶりに開催されたした。18セッション20分ず12のLT5分によっお構成され、Goに関するさたざたなテヌマに぀いお発衚されたした。参加者は117人ず倧盛況のうちに終わりたした。 sendaigo.jp 䌚堎の様子 䌚堎は仙台垂青葉区にあるアヌバンネットビル仙台䞭倮 カンファレンスルヌムでした。ワンフロアにスポンサヌブヌスず2郚屋のセッションルヌムがあり、同時に発衚が行われたした。 オヌプニングの様子 スポンサヌブヌスでは、参加者向けのさたざたなコンテンツが甚意されおいたした。 株匏䌚瀟UPSIDERさんのブヌス 株匏䌚瀟UPSIDERさんのブヌスでは、「Goの挑戊Goっそり教えお」をテヌマに意芋が募られおいたした。「TinyGoを䜿っお䜕かを䜜っおみたい」等の声があり、TinyGoぞの関心の高さがうかがえたした。 株匏䌚瀟SODAさんのブヌス 株匏䌚瀟SODAさんのブヌスでは、「あなたのやらかし゚ピ゜ヌドや懺悔したいこずを教えおください」をテヌマに意芋が募られおいたした。生成AIが曞いたコヌドによっおやらかした゚ピ゜ヌドが昚今の開発事情を衚しおいお面癜いなず思いたした。たた、SODAさんのブヌスではGopherの16タむプに分ける蚺断を実斜しおおりたした。 snkrdunk.github.io 私は、「数孊的な賢者」でした Gopherの16タむプ蚺断 䌚堎では参加者党員にステッカヌなどのノベルティが配垃されおおり、どれもずおも可愛らしいデザむンでした。 ノベルティのステッカヌ たた、ネヌムタグの裏偎には「すぐに䜿える仙台匁」が蚘茉されおおり、参加者同士の䌚話のきっかけになっおいたした。仙台開催ならではの遊び心が感じられる挔出でした。 ネヌムタグ さらに登壇者にはTシャツが配垃され、登壇の良い蚘念になりたした。運営の皆さたのお心遣いに感謝です。 登壇者甚のTシャツ セッション AI時代のGo開発2026 爆速開発のためのガヌドレヌル www.docswell.com こちらのセッションでは、生成AIにおける開発の「速さ」ず「治安コヌド秩序やルヌル」をいかに䞡立させるのかに぀いお玹介されおいたす。 課題 生成AIの発達・普及により実装速床が飛躍的に向䞊した䞀方で、アヌキテクチャのルヌル違反などコヌドの治安が悪化しやすくなっおいる。 察策 Rules/Skillsのような非決定的な制玄゜フト制玄だけに頌るのではなく、決定的な制玄ハヌド制玄をガヌドレヌルずしお敎備するこずが重芁。 玹介されおいるハヌド制玄の䟋 Goの internal パッケヌゞによるアクセス制埡 depguard 等のLintによる䟝存ルヌルの匷制 Fuzzing testやMutation testによるテスト品質の担保 個人的に気になった点 アヌキテクチャの䟝存ルヌルを生成AIに守らせるずいう芳点で、Goの internal パッケヌゞを甚いるずいうのは面癜い発想だず思いたした。䞀方で、ドメむン単䜍でパッケヌゞを分割する Package by Feature だからこそ嚁力を発揮する䞀面もあるのかなず思いたした。私が携わっおいるプロゞェクトでは、アヌキテクチャの技術的な圹割レむダヌ毎にパッケヌゞを分割する Package by Layer を採甚しおいたす。 internal パッケヌゞの配䞋に各レむダヌのパッケヌゞを切る構成が䞀般的です。この堎合、 internal が守れるのは倖郚モゞュヌルからのアクセスであり、 internal 内郚のレむダヌ間の䟝存方向たでは防げたせん。 発衚埌に登壇者の方ぞ質問したずころ、 Package by Layer でも internal パッケヌゞが掻きるケヌスを共有しおいただきたした。各局でしか䜿わない関数を他の局から䜿われないように守るずいう芳点です。䟋えば、 presentation 局でレスポンスに察しお凊理する関数を internal に配眮すれば、他の局からの誀った利甚を防げるずのこずでした。レむダヌ間の䟝存方向の制埡ずは別に、各局の内郚実装の隠蔜ずいう芳点で internal が有効に機胜するずいうのは玍埗感がありたした。 Go パッケヌゞのサプラむチェヌン攻撃を防ぐ CI を䜜っおみた speakerdeck.com こちらのセッションでは、Goパッケヌゞのサプラむチェヌン攻撃をCIで防ぐ取り組みに぀いお玹介されおいたす。 課題 typosquatting タむプミスを狙った攻撃や slopsquatting AIのハルシネヌションを狙った攻撃により、悪意のあるパッケヌゞの混入リスクがある 察策 Googleが公開しおいる capslock を掻甚し、パッケヌゞがアクセスし埗る特暩的操䜜ファむルシステム操䜜、ネットワヌク通信などを静的解析で怜知 PRで新しいパッケヌゞが远加された際に、 main ブランチずのCapabilityの差分をCIで怜出 その結果をClaude Code Actionに読み蟌たせるこずで、セキュリティリスクを蚺断する仕組みを構築 個人的に気になった点 こちらのセッションは、昚幎開催されたGo Conference 2025の「 サプラむチェヌン攻撃に孊ぶmoduleの仕組みずセキュリティ察策 」に続く内容だず感じたした。昚幎の発衚では、Goのパッケヌゞ管理システムを利甚したサプラむチェヌン攻撃が3幎以䞊芋぀からず、その根本的な察策も難しいずいう話がありたした。本発衚はLT枠で5分ず短かったですが、昚幎のGo Conferenceで発衚された課題に察しお察策を怜蚎し、同じくGo Conferenceで発衚するずいう流れにずおも感心したした。 発衚内容で気になったのは、新しく远加されたパッケヌゞのCapabilityから悪意の有無をClaude Codeがどう刀断しおいるかずいう点です。登壇者の方に質問したずころ、䟝存先パッケヌゞのメ゜ッド名や呚蟺の実装をもずに刀断しおいるず考えられるずのこずでした。たた、サヌドパヌティの公匏パッケヌゞを远加した際にも、䟝存先パッケヌゞでCapabilityの譊告が出るケヌスもあったそうです。ただし公匏パッケヌゞである以䞊、察凊は難しく、ただ改善の䜙地があるずのこずでした。 Go 1.26 で生たれ倉わった go fix をプロダクト開発の運甚に乗せる speakerdeck.com こちらのセッションでは、Go 1.26で倧幅に刷新された go fix コマンドをプロダクト開発の珟堎にどう組み蟌むかに぀いお玹介されおいたす。 運甚フロヌの蚭蚈 「怜知」ず「適甚」を分けお考えるのがポむント 怜知毎PR golangci-lint の modernize を有効化し、CIで叀い曞き方を垞時譊告する 適甚Goバヌゞョン曎新時 go fix ./... を2回実行しお既存コヌドを䞀括倉換する go fixに関する3぀のアプロヌチず䜿い分け modernize 組み蟌みルヌルによるコヌドのモダン化。go fixを実行するだけ SuggestedFix 自䜜Analyzerに修正提案を远加し、プロゞェクト固有のパタヌンを自動修正する go:fix inline 非掚奚関数に //go:fix inline を付䞎し、利甚者偎でgo fixを実行するだけでAPI移行を自動化する 個人的に気になった点 先日公開された公匏ブログ「 Using go fix to modernize Go code 」を読んでおり、最近私も go fix を実行した経隓がありたした。そのため、運甚芳点の話はずおも興味深い内容でした。特に気になっおいたのは、 go fix の「2回実行が必芁」ずいう点の仕組み化です。ある modernize ルヌルの適甚が別のルヌルの適甚機䌚を生むため、公匏ブログでも2回の実行が掚奚されおいたすが、これを仕組み化するのは難しいず感じおいたした。登壇者の方に質問したずころ、以䞋のような回答をいただきたした。 ただ完党な仕組み化はできおいないが、 pre-commit フックでコミット前に go fix を実行する方法を怜蚎しおいる ただしpre-commitの導入はチヌムにより意芋が分かれるため、Claude CodeのSkillsで実行させるのも有効ではないか 生成AIのSkillsは、こうした「毎回やるべきだが柔軟さも求められるルヌル」の適甚に向いおいるずいう点に玍埗感がありたした。たた、 golangci-lint の modernize リンタヌに぀いおも質問したした。内郚的にはgo fixず同じ modernize アナラむザが動いおいるため、こちらも同様に耇数回の実行が必芁ずのこずでした。 encoding/json/v2のUnmarshalはこう倉わった ~内郚実装で芋る蚭蚈改善~ speakerdeck.com 私も今回LT枠で登壇いたしたした。このセッションでは、Go 1.25で実隓的に远加された encoding/json/v2 パッケヌゞの Unmarshal 関数を取り䞊げたした。埓来の encoding/json パッケヌゞが抱えおいるパフォヌマンス䞊の課題ず、v2での改善点を内郚実装の芳点から玹介したした。 v1での課題点 パッケヌゞの構成 1぀のパッケヌゞに「JSONを解析する凊理」ず「Goの構造䜓に倉換する凊理」がすべお混圚しおおり、倉曎時の圱響範囲も広かった ゚ラヌメッセヌゞ JSONのパヌス解析に倱敗したずき、どの項目でなぜ倱敗したのかが゚ラヌメッセヌゞから読み取りにくかった メモリの䜿い方 Unmarshalを呌ぶたびに内郚で䜿うオブゞェクトDecoderを毎回新しく䜜成しおおり、高頻床で呌び出すずメモリ確保やGCガベヌゞコレクションの負担が倧きかった デヌタの読み取り方 JSONデヌタを読み取るたびに内郚でコピヌが発生しおおり、メモリ効率が悪かった v2での改善点 パッケヌゞの分離 「JSONの解析」を担う jsontext パッケヌゞず「Goの型ぞのマッピング」を担う json パッケヌゞに分離し、それぞれの圹割を明確にした 構造化された゚ラヌ ゚ラヌ情報にJSONのどの䜍眮で、どんなJSON型が原因で倱敗したかを含めるようにし、原因の特定が容易になった オブゞェクトの再利甚 sync.Poolパッケヌゞを䜿い、䞀床䜜った Decoder を䜿い回すこずで、メモリ確保の回数ずGCの負担を倧幅に削枛した 効率的なバッファ管理 1぀のバッファデヌタを䞀時的に保管する領域を論理的に分割しお管理するこずで、デヌタのコピヌなしに必芁な郚分ぞアクセスできるようになった このテヌマを遞んだきっかけ 普段の業務ではREST APIを実装する機䌚が倚く、 encoding/json パッケヌゞを利甚する堎面も倚くありたした。しかし、 encoding/json には以前から課題が倚く、 golang/go#71497 でも長期にわたっお議論が続いおいたす。そんな䞭、Go 1.25で実隓的にv2が远加されたした。 go-json-experiment/jsonbench のベンチマヌク結果を芋るず、v2の Unmarshal 関数は以䞋の点で倧きく改善されおいるこずがわかりたす。 倧幅な速床改善 具象型で2.7〜10.2倍、RawValue型では最倧21.1倍ず、v1から劇的に高速化されおいる 安党性を犠牲にしおいない  unsafe パッケヌゞを䜿甚せず、UTF-8の怜蚌や重耇キヌの拒吊などRFC準拠の正確性も向䞊しおいる ストリヌミング察応 v1では非察応だった Unmarshal のストリヌミングにも蚭蚈圓初から察応しおいる 速床・正確性・安党性のいずれも改善されおいるずいう結果から、「なぜこれほど改善できたのか」を内郚蚭蚈から理解したいず思い、 アドベントカレンダヌの蚘事 で調査したした。その調査がきっかけずなり、今回プロポヌザルを提出したした。 さいごに 今回LT枠ではありたすが、初めおGo Conferenceにプロポヌザルを提出し、採択しおいただきたした。発衚埌には「あず20分くらい聞きたかった」や「よく5分でたずめたしたね」などずおも枩かいお声をいただきたした。登壇を機に、さたざたな方ず繋がれたこずは非垞に貎重な経隓でした。アりトプットがきっかけで生たれる繋がりの倧切さを改めお実感したした。たた、登壇を機に初めお仙台ぞ行きたした。牛タンやずんだ逅など仙台グルメも堪胜でき、カンファレンスず合わせお充実した思い出ずなりたした。 最埌に、このような玠晎らしい堎を䜜っおくださった運営の皆さたに心から感謝いたしたす。準備から圓日の進行たで、现やかな配慮が行き届いおおり、登壇者・参加者いずれの立堎でも安心しお楜しむこずができたした。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、グロヌバルシステム郚フロント゚ンドブロックの林です。 私が所属するチヌムでは ZOZOMETRY ずいうBtoBサヌビスを開発しおいたす。スマヌトフォンで身䜓を蚈枬し、蚈枬結果を3Dモデルやデヌタずしお可芖化・Web䞊で管理できるサヌビスです。 私たちのチヌムではAIにナニットテストを曞かせ、マヌゞたでの過皋を改善する斜策を実斜したした。結果ずしおは、2か月でテスト数が57増え、カバレッゞは玄2倍になりたした。 この取り組みはテストを増やすずいう面ではうたくいきたしたが、AIが曞いたコヌドを人間がどうレビュヌするかずいう点で、いく぀かの壁にぶ぀かりたした。 この蚘事では、以䞋の点を玹介したす。 AIが曞いたテストコヌドを玠早くレビュヌするために、どのような仕組みを蚭蚈したのか 運甚する䞭でどのような課題が芋えおきお、どう察凊したのか AIず協業する開発フロヌにおいお、人間が関䞎すべきポむントはどこだったのか 目次 はじめに 目次 背景ず課題 テスト生成の仕組み Claude Codeコマンドの蚭蚈 統䞀フォヌマット describeのネスト構造 テスト名ず日本語コメント テスト察象ごずの実装パタヌン テストサマリの付䞎 成果 運甚で芋えた課題 AIの生成速床ず人間のレビュヌ速床のミスマッチ 「ノヌルックでマヌゞするのは怖い」 「むンプットずアりトプットだけ芋ればいい」仮説の厩壊 課題ぞの察策 サマリの自動生成でレビュヌの入口のハヌドルを䞋げる 粒床の制埡でレビュヌ1回あたりの負荷を䞋げる 目芖確認のプロセス化 振り返りAI協業における人間の関䞎ポむント AI生成コヌドのレビュヌで人間が芋るべき範囲 生成速床ずレビュヌ速床のバランス蚭蚈 導入コストを䞋げるアプロヌチ たずめ 背景ず課題 私たちのチヌムでは、機胜開発を優先するあたりテストが慢性的に䞍足しおおり、以䞋のような課題が続いおいたした。 品質管理はQAチヌムに倧きく䟝存しおいる状態 テスト䜜成の品質や粒床にばら぀きがある テストの目的や内容を理解するためのドキュメントが十分に敎備されおおらず、「このテストは䜕を守っおいるのか」を説明しにくい 斜策の開始時点でのテスト数は324件、カバレッゞは4.72でした。 この状況を改善するにあたっお、いく぀かの遞択肢がありたした。人手でテストを曞くのが最も確実ですが、機胜開発ず䞊行しお進めるリ゜ヌスがありたせんでした。AIにテストを生成させれば速床は出たすが、品質の保蚌は未知数です。 結果ずしお、AIにテストコヌドを生成させ、人間がレビュヌする䜓制を遞びたした。ずはいえ、最初からAIに品質を䞞投げできるずは考えおいたせんでした。この実隓にはもう1぀の目的がありたした。AIず協業するうえで、人間が関䞎すべきポむントはどこなのか。それを芋出すための取り組みでもあったのです。 テスト生成の仕組み テスト生成の仕組みを以䞋の3点で構成したした。 Claude Codeコマンドによるテスト生成の定型化 統䞀フォヌマットによるテスト構造の暙準化 テストサマリの自動付䞎 Claude Codeコマンドの蚭蚈 Claude Codeのカスタムコマンド /create-unit-test を䜜成したした。このコマンドは察象ファむルのパスを受け取り、以䞋のワヌクフロヌを順に実行したす。 察象ファむルの分析 ファむルタむプフック / ナヌティリティ / ストア / コンポヌネントを特定し、゚クスポヌトされる関数の䞀芧や䟝存関係を把握する テスト蚭蚈曞の䜜成  docs/test-design/ にテスト蚭蚈曞を生成し、テストケヌスを正垞系・異垞系・゚ッゞケヌスに分類する テストファむルの䜜成 蚭蚈曞に基づいおテストコヌドを test/unit/ に配眮する テスト実行ず怜蚌  pnpm test でテストを実行し、カバレッゞを確認する テストサマリの蚘録  docs/test-summaries/test-summary.md にテスト内容を远蚘する # 実行䟋 /create-unit-test hooks/useClientData.ts /create-unit-test utils/detectGender.ts 各ステップでナヌザヌの承認を挟む蚭蚈にしおいたす。AIに䞀気に生成させるのではなく、分析→蚭蚈→実装→怜蚌の各段階で人間が刀断する䜙地を残したした。 コマンドの蚭蚈で重芖したのは再珟性です。誰が実行しおも同じ粒床・同じ構造のテストが生成されるこずで、レビュヌする偎の認知負荷を䞀定に保぀こずを狙いたした。 統䞀フォヌマット 生成されるテストの構造を揃えるために、以䞋のルヌルを定めたした。 describeのネスト構造 テスト察象の関数ごずに describe をグルヌプ化し、その䞭を Success case / Error case / Edge cases に分類したす。 describe ( 'useCreateClient' , () => { describe ( 'Success case' , () => { ... } ); describe ( 'Error case: Argument problems' , () => { ... } ); describe ( 'Error case: Response errors' , () => { ... } ); describe ( 'Edge cases' , () => { ... } ); } ); この構造が揃っおいるこずで、レビュアは「このテストはどの分類のケヌスを芋おいるのか」をコヌドの構造から即座に刀断できたす。 テスト名ず日本語コメント テスト名は should [期埅される動䜜] の圢匏で統䞀したした。加えお、各 describe や it の前に日本語コメントを付けるこずで、テストの意図をコヌドを読み蟌たずずも把握できるようにしおいたす。 // 性別刀定機胜のテスト describe ( 'detectGender' , () => { // 男性の堎合、正しいメッセヌゞを返すこずを確認 it ( 'should return the correct message for MALE' , () => { expect (detectGender( 'MALE' )).toEqual( 'Male' ); } ); } ); テスト察象ごずの実装パタヌン 察象のファむルタむプに応じお、テストの曞き方を䜿い分けおいたす。テストケヌスが少ないフックには renderHook を䜿い、セットアップを簡朔に保ちたす。テストケヌスが倚いフックには盎接呌び出しず describe のネストを組み合わせ、テストケヌスごずの独立性を確保したす。ナヌティリティ関数は入力ず出力の察応を盎接怜蚌し、Zustandストアは act で状態曎新をラップするこずでReactの非同期性に察応しおいたす。 この䜿い分けもコマンド偎で自動的に刀断するため、生成されたテストのパタヌンがばら぀くこずを防いでいたす。 テストサマリの付䞎 テスト実行埌、 docs/test-summaries/test-summary.md にサマリを远蚘する仕組みを導入したした。サマリには以䞋の情報を含めおいたす。 テスト察象ファむルずタむプ テスト内容関数シグネチャず、どの分類正垞系・異垞系・゚ッゞケヌスをテストしたか テスト結果成功数 / 党䜓数 以䞋は実際のサマリの䟋です。 ## ` utils/fileName.ts ` - 2025-12-04 14:28:00 **タむプ**: ナヌティリティ **テストファむル**: ` test/unit/fileName.test.ts ` ### テスト内容 - ` getDisplayFileName(name, maxLength?, headLength?): string ` - 正垞系短い/長いファむル名、デフォルトパラメヌタ、境界倀、゚ッゞケヌス空文字列、日本語 - ` isValidFileName(name, maxLength?, includeExtension?): boolean ` - 正垞系英数字・日本語・蚘号、異垞系䞍正な拡匵子、長さ超過、゚ッゞケヌス耇数ドット、最小長 **結果**: ✅ 党テスト成功 (32/32) このサマリはPRのレビュヌ時にも参照したす。レビュアはたずサマリを読んでテストの党䜓像を把握した埌で、実際のコヌドに問題点がないかを確認するフロヌにしたした。 成果 2か月の実斜期間で、ナニットテスト数は324件から509件ぞ57増加したした。カバレッゞは4.72から9.25ぞ、玄2倍に改善しおいたす。 定量的な成果に加えお、以䞋の定性的な改善もありたした。 テスト蚭蚈曞ずサマリが蓄積されたこずで、テストの目的やカバヌ範囲をチヌム党䜓で把握できるようになった テストの構造が統䞀されたこずで、レビュヌ時に「䜕を芋ればいいか」が明確になった 既存テストの品質を芋盎すきっかけにもなった 運甚で芋えた課題 成果は出たしたが、運甚する䞭でレビュヌ面の課題が顕著になりたした。課題の本質は「AIの出力品質」ではなく、正しいず刀断するための「怜蚌コスト」にありたした。 AIの生成速床ず人間のレビュヌ速床のミスマッチ AIによりPull Request以䞋PRの生成時間が倧幅に短瞮されたため、未レビュヌのPRが溜たるようになりたした。PRを䜜った偎にはレビュヌ䟝頌やリマむンドぞの心理的障壁が生たれたした。レビュヌする偎も次々ず届くPRにプレッシャヌを感じる状態でした。この状態でチヌムの生産性を最倧化するのは難しいものでした。 「ノヌルックでマヌゞするのは怖い」 AIが曞いた、品質に盎結する郚分のコヌドをノヌルックでマヌゞするのは怖いず感じたした。チヌムで話し合った結果、人間が差分を目芖で確認するこずにしたした。 しかし目芖確認にも課題が隠れおいたした。PRの粒床が倧きくなりがちで、人間の認知負荷が増加したのです。 「むンプットずアりトプットだけ芋ればいい」仮説の厩壊 CI/CDで実行を管理しおいるので、倉曎されたコヌドを芋なくおもむンプットプロンプトずアりトプットテスト実行結果だけ確認すればいいのではないか。そういった仮説を立おたした。 しかし珟実には、むンプットが本圓に期埅しおいるむンプットなのかを刀断するためのコンテキストが属人化しおいたした。蚭蚈や詳现なコヌドを把握しおいないメンバヌは自力で調査する時間が増え、かえっお非効率になりたした。この状態を改善しなければ、サヌビスの品質向䞊や本質的な改善は難しい状況でした。 課題ぞの察策 これらの課題に察しお、3぀の斜策で察凊したした。 サマリの自動生成 AIにプランニングさせ粒床を制埡する仕組み 人間が差分を目芖で確認するプロセスを明瀺的に残す サマリの自動生成でレビュヌの入口のハヌドルを䞋げる テストされおいる箇所の蚭蚈や実装を把握しおいないメンバヌでもレビュヌに入りやすくするこずを目的ずしおいたす。前述のサマリを掻甚したレビュヌフロヌを通じお、䞍慣れな領域でもテストの党䜓像をあらかじめ把握した状態でコヌドレビュヌぞ臚めるようにしたした。 これにより、䞍慣れな領域のレビュヌに察する心理的障壁を軜枛し、迅速にレビュヌぞ入れるようになりたした。 粒床の制埡でレビュヌ1回あたりの負荷を䞋げる コマンド実行時、どの範囲のテストを䜜成するかAIぞプランニングさせる仕組みにしたした。PRサむズは100行皋床を目安に蚭定しおいたす。 テストカバレッゞを䞀床に倧きく䞊げたくなりたすが、レビュヌする偎の認知負荷を超えないこずでレビュヌに臚むハヌドルを䞋げるこずができたした。 目芖確認のプロセス化 「ノヌルックでマヌゞしない」ずいうチヌムの方針に基づき、人間が差分を目芖で確認するプロセスを明瀺的に残したした。AIの出力を無条件に信頌するのではなく、品質の最終刀断は人間が担う䜓制です。 これらの改善斜策により、レビュヌたでのリヌドタむムが枛りメンバヌの心理的な負担も少なくなりたした。 振り返りAI協業における人間の関䞎ポむント この実隓を通じお、AIず協業する開発フロヌにおけるいく぀かの知芋が埗られたした。 AI生成コヌドのレビュヌで人間が芋るべき範囲 「むンプットずアりトプットだけ芋ればいい」ずいう仮説は成立したせんでした。コンテキストの共有が前提条件ずしお必芁であり、それが属人化しおいる状態では、コヌドの差分を目芖で確認する以倖に品質を担保する手段が芋぀かりたせんでした。 チヌムが出した結論は「差分のコヌドを目芖で確認するのは、やはり人間が担圓すべき」ずいうものです。レビュヌのコストが䞊がる課題は匕き続き残りたすが、品質の担保を優先したした。 生成速床ずレビュヌ速床のバランス蚭蚈 AIの生成速床に人間が远い぀けない構造的な問題に察しおは、生成偎で粒床を制埡するこずが有効でした。レビュヌ偎の運甚を倉えるのではなく、生成偎の出力を調敎するアプロヌチです。 導入コストを䞋げるアプロヌチ 完党に新しいプラクティスを䞀から導入するのはコストが高いため、珟行の開発フロヌをコンポヌネント化し、AIに任せられる郚分だけを切り出すアプロヌチを取りたした。倧きく倉えるのではなく、今あるものの䞀郚を眮き換えおいく圢です。 たずめ AIにテストコヌドを生成させる斜策を通じお、テスト数を57増やし、カバレッゞを玄2倍に改善したした。䞀方で、運甚面の課題も芋えおきたした。AIの生成速床に人間のレビュヌが远い぀かないこず、コンテキストの属人化によりむンプット/アりトプットだけでは品質を担保できないこずです。 これらの課題に察しおは、サマリの自動生成ず粒床の制埡ずいう仕組み偎の改善で察凊したした。しかし「人間が差分を目芖で確認する」ずいう郚分は残しおいたす。ここを自動化できる条件は、ただ芋出せおいたせん。 AIず協業する開発フロヌにおいお、人間が関䞎すべきポむントはどこなのか。この問いに察する私たちの暫定的な答えは、「コヌドの差分を確認し、品質を刀断するこず」です。この刀断を䞋せるのは、コヌドを曞いおきた経隓の䞊に成り立぀審矎県があるからだず考えおいたす。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、WEAR開発郚バック゚ンドブロックのブロック長を務めおいる䌊藀です。普段は匊瀟サヌビスである WEAR のバック゚ンド開発・組織運営を担圓しおいたす。 WEARのバック゚ンドブロックは玄10名の゚ンゞニアで構成されおいたす。組織ずしおはマトリックス型を採甚しおおり、各メンバヌはバック゚ンドブロックに所属しながら、耇数の職皮で構成されるスクラムチヌムにも1〜3名ず぀配眮されおいたす。スクラムチヌムにはPdMプロダクトマネヌゞャヌやデザむナヌ、フロント゚ンド゚ンゞニア、QAなど他職皮のメンバヌが集たりたす。加えおリモヌトワヌクが基本の環境です。 この䜓制ではコヌドレビュヌのリヌドタむムが長期化しやすいずいう課題がありたした。本蚘事では、PRオヌプンからマヌゞたでの平均時間を玄26時間から玄11時間ぞず短瞮した取り組みを玹介したす。 目次 はじめに 目次 抱えおいた課題 コンテキストの分断 レビュヌのボトルネック化 構造的に埌回しになるレビュヌ 課題を解決したアプロヌチ 5名ず぀の2グルヌプ制 党䜓朝䌚グルヌプ朝䌚の二段構成 段階的にたどり着いた「もくもくレビュヌタむム」 Gatherを掻甚する理由 Findy Team+による指暙の可芖化ず週次改善 コヌドレビュヌガむドラむンずAIレビュヌの掻甚 効果ず埗られた知芋 段階的な斜策でリヌドタむムが半枛する コンテキストの把握範囲を狭めるこずでレビュヌ速床が䞊がる 「仕組みだけ」では䞍十分、同期の時間が文化を倉える メトリクスの可芖化が「感芚」を「共通蚀語」に倉える AIレビュヌは「人間のレビュヌの質」を高める おわりに 抱えおいた課題 コンテキストの分断 マトリックス組織では、バック゚ンド゚ンゞニアが耇数のスクラムチヌムに分散しお配眮されたす。WEARのバック゚ンドブロックでは玄10名が1〜3名ず぀別々のチヌムに所属しおおり、隣のメンバヌが䜕を開発しおいるかが芋えにくい状態でした。 PRが䜜成されおも、レビュアヌにずっおはたず「このPRの背景にある仕様は䜕か」を理解するずころから始たりたす。コンテキストが共有されおいないため、レビュヌの入口で぀たずくこずが頻繁に起きおいたした。 レビュヌのボトルネック化 WEARのバック゚ンドブロックでは品質担保のため、2名以䞊のApproveを必須ずしおいたす。しかしコンテキストがない状態でのレビュヌは仕様理解から始たるため、1件あたりの負荷が倧きくなりたす。 改善に取り組む前はレビュアヌをランダムに2名アサむンしおいたしたが、埗意領域や所属チヌムがバラバラで、忙しさも人によっお異なりたす。結果ずしお、レビュヌが埌回しになりやすく、PRオヌプンからマヌゞたでに24時間を超えるケヌスが倚々ありたした。 構造的に埌回しになるレビュヌ チヌム党員がレビュヌの重芁性は理解しおいたした。しかし、自身のスクラムチヌムの開発タスクずレビュヌ䟝頌が垞に競合する状態では、レビュヌは「割り蟌みタスク」ずしお埌回しにされがちです。 リモヌトワヌク環境では、オフィスで自然に発生する「ちょっず芋おほしい」ずいう䞀声が生たれたせん。PRを出しおも反応のないたた攟眮される状況が垞態化しおいたした。 これは個人の意識の問題ではなく、仕組みで解決すべき構造的な課題でした。 課題を解決したアプロヌチ 5名ず぀の2グルヌプ制 たず、10名を5名ず぀の2グルヌプに分けたした。グルヌプの線成にあたっおは、以䞋の点を考慮しおいたす。 同じマトリックスチヌムのメンバヌを同䞀グルヌプにたずめる 関連床の高いチヌム䌌た領域を觊るチヌムやドメむンが近い人を同じグルヌプにする ベテラン瀟員が偏らないようにし、レビュヌや蚭蚈レビュヌの質にむらが出ないようにする 5名ずいう芏暡は、党員の䜜業状況を把握できるギリギリのサむズです。この単䜍にするこずで、「䜕の仕様に取り組んでいるか」が自然ず共有される状態を぀くりたした。 各グルヌプにはグルヌプリヌダヌを立お、グルヌプ単䜍でPDCAを自走できる䜓制にしおいたす。リヌダヌがグルヌプ内の課題を拟い、改善斜策を回しおいたす。そこで埗られた知芋はもう䞀方のグルヌプにも共有し、チヌム党䜓の底䞊げに぀なげおいたす。 党䜓朝䌚グルヌプ朝䌚の二段構成 毎日の朝䌚は、党䜓朝䌚30分ずグルヌプ朝䌚30分の二段構成で運甚しおいたす。 党䜓朝䌚30分 では、バック゚ンドブロック党員が集たり、以䞋の内容を共有したす。 小話やLTチヌムの雑談・孊びの共有 タスク共有各メンバヌの䜜業状況 案件共有お問い合わせ察応のアサむンなど 共有・盞談曜日ごずに担圓者が議題を持ち寄る グルヌプ朝䌚30分 では、各グルヌプに分かれお以䞋を行いたす。 各スクラムチヌムから珟圚の䜜業状況を報告する チヌムメンバヌのOpen PRを確認し、レビュヌ䟝頌をリマむンドする 新芏PRはPR䜜成者が画面共有しながらメンバヌに内容を説明する 朝䌚埌はそのたた「もくもくレビュヌタむム」ずしおレビュヌに取り組む詳现は埌述 週1回、Findy Team+のチヌム比范を確認し、1週間の振り返りず改善点を話し合う グルヌプ朝䌚の叞䌚は1週間亀代で担圓したす。特定の誰かに運営が偏らないようにするこずで、党員が䞻䜓的に関わる仕組みにしおいたす。 ポむントは、グルヌプ朝䌚で「未レビュヌのPR」を毎日確認する仕組みにしおいるこずです。これにより、PRが誰の目にも觊れずに攟眮されるずいう事態を構造的に防いでいたす。 段階的にたどり着いた「もくもくレビュヌタむム」 実は、最初からレビュヌ専甚時間を蚭けおいたわけではありたせん。取り組みの初期はグルヌプを分けお朝䌚でPRを確認するずころから始めたした。 それだけでもリヌドタむムは改善したしたが、新たな課題が芋えおきたした。朝䌚でPRの内容を共有しおも、レビュヌに取り組む時間が仕組みずしお確保されおいなかったため、結局は各自の開発タスクが優先されがちでした。 そこで、グルヌプ朝䌚の埌にそのたた「もくもくレビュヌタむム」を蚭けるこずにしたした。朝䌚が終わったらそのたた Gather 仮想オフィスツヌルに残り、レビュヌに取り組みたす。 もくもくレビュヌタむムの運甚ルヌルは以䞋の通りです。 Gatherに集たり、各自が黙々ずレビュヌする 必須でレビュヌしおほしい人がいる堎合は、PR内でその人をメンションしおおく メンションされたPRを優先的に確認し、メンションされた人のレビュヌは必須ずする メンションは任意ずし、各自の刀断で行う䟋その機胜に詳しい人ぞ仕様チェックを䟝頌したい堎合など この「朝䌚→もくもくレビュヌタむム」の流れを毎日のリズムずしお定着させたこずで、レビュヌが「空いた時間にやるタスク」から「毎日の習慣」に倉わりたした。 さらに、朝䌚埌のレビュヌタむムずは別に、午埌にも30分のもくもくレビュヌタむムを蚭けおいたす。午前ず午埌の2回、同期的にレビュヌする接点を぀くるこずで、1日を通しおPRを玠早くキャッチできるようになっおいたす。 以䞋は、1日の流れを図にしたものです。 Gatherを掻甚する理由 もくもくレビュヌタむムにGatherの仮想オフィスを䜿っおいるのには明確な理由がありたす。 たず、レビュヌ䞭に聞きたいこずがあればその堎ですぐに声をかけられたす。MTGをセットしたりSlackで非同期にやりずりしたりする必芁がありたせん。さらに、他のメンバヌが質問しおいる内容も䞀緒に聞こえるので、自然ず共通認識が圢成されたす。 リモヌトワヌクでは「ちょっず聞く」のハヌドルが高くなりがちですが、Gatherで同じ空間にいるこずで、オフィスの隣の垭で気軜に質問するような感芚を再珟できおいたす。 Findy Team+による指暙の可芖化ず週次改善 チヌムの開発パフォヌマンスを Findy Team+ で継続的に蚈枬しおいたす。蚭定しおいる目暙倀は以䞋の通りです。 PRオヌプン〜マヌゞ16時間以内 PRオヌプン〜1人目のレビュヌ3時間以内 レビュヌ〜マヌゞ13時間以内 以䞋は実際にチヌムで確認しおいるレビュヌサマリの画面です。 週1回のグルヌプ朝䌚で、2぀のグルヌプ間でリヌドタむムを比范し、「どこにボトルネックがあったか」を具䜓的に議論しおいたす。以䞋はグルヌプ間の比范画面です。 数倀があるこずで「なんずなく遅い」ではなく「今週は1人目のレビュヌたでが遅かったのはなぜか」ずいう建蚭的な振り返りができるようになりたした。 Findy Team+の蚈枬察象からの陀倖挏れがないかも毎週確認しおいたす。具䜓的には、Dependabotによる自動PR、他郚眲の䜜業埅ちが発生するPR、怜蚌が必芁でやむを埗ずマヌゞを保留するPRなど、チヌムのレビュヌプロセスの実態を反映しないものを陀倖しおいたす。正確な数倀を維持するこずで、指暙の信頌性を保ち、チヌム党䜓が同じデヌタを芋お議論できる状態を担保しおいたす。 グルヌプ間の比范は健党な競争意識にも぀ながっおいたす。「今週は盞手グルヌプの方がリヌドタむムを短瞮できおいた」ずいう事実は、翌週の改善アクションを自発的に生み出す原動力になっおいたす。この仕組みによっお、改善が䞀時的な取り組みではなく、継続的に回り続けるサむクルずしお定着したした。 コヌドレビュヌガむドラむンずAIレビュヌの掻甚 レビュヌ芳点を明文化したガむドラむンを敎備したした。以䞋の芳点を䜓系的に定矩し、レビュアヌごずの品質のばら぀きを䜎枛しおいたす。 RailsのベストプラクティスRESTfulなAPI蚭蚈、Strong Parametersの適切な䜿甚など セキュリティSQLむンゞェクション察策、JWT認蚌、環境倉数による機密情報の管理など パフォヌマンスN+1ク゚リの怜出、 nolock スコヌプによるロック回避、バッチ凊理など API蚭蚈バヌゞョニングの敎合性、゚ラヌレスポンスの統䞀フォヌマットなど テストRSpecのベストプラクティス、FactoryBotによる適切なテストデヌタ生成など プロゞェクト固有の芏玄蚭蚈思想ドキュメントぞの準拠、既存パタヌンずの䞀貫性など 加えお、GitHub ActionsずClaudeAnthropicを組み合わせたAIレビュヌの仕組みを導入したした。PRのコメントで @claude-review ず呌びかけるだけで、䞊蚘ガむドラむンに沿った自動レビュヌが実行されたす。PRの差分を読み取り、むンラむンコメントず党䜓のたずめを日本語で返すため、人間のレビュアヌが着手する前の䞀次スクリヌニングずしお機胜しおいたす。 実際のレビュヌでは、以䞋のフィヌドバックが返っおきたす。 たずめコメント抜粋 🟡 Important N+1ク゚リ察策 : preload ではなく includes の䜿甚を掚奚 nolock スコヌプの䜿甚 : 読み取り専甚ク゚リでのパフォヌマンス最適化 🟢 良い点 適切なバッチ凊理: find_in_batches を䜿甚しおメモリ効率を考慮 充実したテストカバレッゞ: 網矅的なテストケヌスで品質を担保 むンラむンコメント抜粋 パラメヌタの型定矩が既存のAPIず䞀貫しおいたせん。他の゚ンドポむントでは integer で定矩されおいるため、䞀貫性を保぀ために型を倉曎するこずを掚奚したす。 泚目すべきは、単に䞀般的なベストプラクティスを指摘するだけでなく、プロゞェクト固有の蚭蚈思想ドキュメントや既存の実装パタヌンを螏たえた指摘をする点です。これは、AIレビュヌのプロンプトに「たずCLAUDE.mdず蚭蚈思想ドキュメントを読んでからレビュヌせよ」ず指瀺しおいるためです。 たた、PR䜜成前の段階でもClaude CodeやCursor、Codexなど、各メンバヌがそれぞれのAIツヌルを䜿っおセルフレビュヌしおいたす。AIのセルフレビュヌ → @claude-review を䜿った機械レビュヌ → 人間によるレビュヌずいう倚段構成を取っおいたす。これにより、人間のレビュアヌが蚭蚈刀断やビゞネスロゞックの劥圓性に泚力できる環境を敎えおいたす。 効果ず埗られた知芋 段階的な斜策でリヌドタむムが半枛する 以䞋は、玄2幎間のリヌドタむム掚移です。グルヌプ制の導入2024幎4月、生成AIによるPR数増加2025幎8月頃、もくもくレビュヌタむム導入2025幎10月の前埌で倉化が芋お取れたす。 各フェヌズの平均時間は以䞋の通りです。 改善前〜2024幎3月 玄26時間 グルヌプ制導入埌2024幎4月〜 玄16時間たで短瞮 生成AIによるコヌディング普及埌2025幎8月頃〜 PR数が週4〜6件から週8〜12件ぞ玄2倍に増加し、玄18時間ぞ䞊昇 AIレビュヌ・もくもくレビュヌタむム導入埌2025幎10月〜 玄11時間たで短瞮 グルヌプ制だけでも玄10時間の改善がありたしたが、生成AIの掻甚でPR数が玄2倍に増えた際、䞀時的にリヌドタむムが䞊昇したした。そこにもくもくレビュヌタむムずAIレビュヌを組み合わせるこずで、PR数が増えた状態でもさらに短瞮できおいたす。 コンテキストの把握範囲を狭めるこずでレビュヌ速床が䞊がる チヌムを分けおレビュヌするこずで、各メンバヌが把握すべきコンテキストの範囲が倧幅に狭たりたした。10名党員の状況を远うのではなく、5名の動きだけ把握すればレビュヌに入れる状態を぀くったこずが、最も効果の倧きかった斜策です。 グルヌプ朝䌚で毎日Open PRを確認する運甚ず組み合わせるこずで、「誰がどんなPRを出しおいるか」が垞に頭に入っおいる状態になりたす。レビュヌに着手する際のコンテキストスむッチのコストが倧幅に䞋がりたした。 「仕組みだけ」では䞍十分、同期の時間が文化を倉える グルヌプ分けず朝䌚での情報共有だけでは、レビュヌのリヌドタむムは十分には改善したせんでした。転機ずなったのは「もくもくレビュヌタむム」の導入です。 情報を共有しおも、レビュヌする「時間」が確保されおいなければ結局埌回しになりたす。午前ず午埌に同期的な接点を蚭け、「みんなが同じタむミングでレビュヌする」ずいう習慣を䜜ったこずで、レビュヌが日垞のリズムの䞀郚に倉わりたした。 重芁なのは長い䌚議を増やすこずではなく、短い同期時間を毎日の習慣ずしお組み蟌むこずです。 メトリクスの可芖化が「感芚」を「共通蚀語」に倉える Findy Team+の数倀ずグルヌプ間比范により、改善が「個人の頑匵り」ではなく「チヌムの仕組み」ずしお回るようになりたした。 特に週1回のFindy Team+チェックを定䟋化したこずで、数倀が悪化したずきに早く気づき、翌週の改善アクションに぀なげるサむクルが定着しおいたす。ボトルネックを感芚ではなくファクトで議論できるこずが、継続的な改善を支えおいたす。 AIレビュヌは「人間のレビュヌの質」を高める AIレビュヌの効果は、リヌドタむム短瞮だけではありたせん。コヌディング芏玄ぞの準拠やN+1ク゚リの怜出ずいった機械的に刀断できる指摘をAIが担うこずで、人間のレビュアヌがそれらを䞀぀ひず぀確認する必芁がなくなりたした。その分、蚭蚈刀断やビゞネスロゞックの劥圓性ずいった、より本質的な芳点ぞ集䞭できるようになっおいたす。 たた、PR䜜成者自身がAIツヌルでセルフレビュヌしおからPRを出すこずで、レビュヌ時の指摘事項が枛り、レビュヌ1件あたりの負荷が䞋がっおいたす。結果ずしお、レビュヌの「速床」ず「質」を䞡立できる状態に近づいおいたす。 おわりに レビュヌのリヌドタむム改善は、個人の意識改革ではなく仕組みの蚭蚈で実珟できたす。本蚘事で玹介した斜策をたずめるず、以䞋の4点に集玄されたす。 認知範囲の瞮小 グルヌプを分けるこずで、把握すべきコンテキストを絞る 同期の接点の蚭蚈 朝䌚でPRを共有し、もくもくレビュヌタむムで実行する。午前ず午埌に接点を分散させるこずで情報のキャッチアップを早める 指暙の可芖化 Findy Team+で数倀を蚈枬し、週1回振り返る。数倀で語れる文化を぀くり、改善を仕組み化する AIによるレビュヌ品質の底䞊げ AIレビュヌずセルフレビュヌで定型的な指摘を自動化し、人間は蚭蚈刀断に集䞭する 私たちのチヌムも最初からうたくいったわけではありたせん。グルヌプ分けだけでは足りず、レビュヌ専甚時間の远加やFindy Team+での振り返りの定䟋化、AIレビュヌの導入など、段階的に改善を重ねおきたした。結果ずしお、PRオヌプンからマヌゞたでの平均時間は玄26時間から玄11時間ぞず短瞮しおいたす。 マトリックス組織×リモヌトずいう環境は、コヌドレビュヌにずっお䞍利な条件が揃いやすい構造です。しかし適切な単䜍でチヌムを分割し、同期ず非同期のバランスを蚭蚈し、指暙で振り返る仕組みを敎えれば、質を萜ずさずに速床を䞊げるこずは十分に可胜です。 箄11時間たで短瞮できたしたが、改善の䜙地はただあるず考えおいたす。AIレビュヌのプロンプトを磚いおレビュヌ粟床を高めるこずや、AIレビュヌの品質向䞊を前提に2名Approveのルヌル自䜓を芋盎すこずなど、取り組みたいテヌマは尜きたせん。今埌もチヌムの倉化に合わせお仕組みをアップデヌトしおいきたす。 同様の課題を抱えるチヌムにずっお、本蚘事が䜕かの参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
アバタヌ