TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

987

はじめに こんにちは、ZOZOTOWN開発1部iOSブロックの なんしー です。普段はZOZOTOWN iOSアプリの新機能の開発や既存画面のリファクタリングなどを担当しています。 ZOZOTOWN iOSアプリは2010年11月にサービスを開始して以来、ZOZOSUITやZOZOGLASSをはじめ、様々な新機能を提供してきました。最近では、購入した商品に対してレビューが投稿できる「アイテムレビュー」という機能も追加されました。 新機能が追加されていく一方、以下のようなレガシーな部分が残り続けてしまっていることが課題となってきています。 Objective-Cで書かれたコードが残ってしまっていること API通信等のビジネスロジックにあたる部分がViewController内部に書かれており、Fat ViewControllerになってしまっていること 今回はZOZOTOWN iOSアプリに残るFat ViewControllerを取り上げ、どのように解消を図っているのかをご紹介します。 目次 はじめに 目次 商品詳細画面が抱える課題 リファクタリングを進めるために 大規模なリファクタリングのための工数確保 新たなアーキテクチャの採用 チームとしてのリファクタリングの進め方 アーキテクチャレビュー リファクタリングを複数のStepに分割 おわりに 商品詳細画面が抱える課題 ZOZOTOWN iOSアプリにはさまざまな画面が存在しています。その中でも商品の詳細情報を確認できる画面(以下、商品詳細画面)は、改修の頻度が高く、CVRにも直結する重要な画面の1つです。 そんな重要な画面であるにもかかわらず、商品詳細画面は以下のような課題を抱えていました。 API通信など、ビジネスロジックにあたる実装がViewController内に書かれていてる 責務が分割できておらず、複数人で同時に開発できる設計になっていない ユニットテストが存在しておらず、かつ書ける設計になっていない コード行数は2,500行を超えており、可読性が悪い 商品詳細画面はいわゆるFat ViewControllerになってしまっており、ViewControllerが抱えるべきではない責務が多々入り込んでしまっている状況でした。 そんな状況の中、以下のような話が挙がりました。 複数のプロジェクトで商品詳細画面に変更を加える計画が浮上した ZOZOTOWN開発本部では「開発生産性の向上 1 」が重要な目標の1つとして掲げられた Fat ViewControllerになってしまっている状況では、複数人が同時に開発することは難しい状況にありました。また、メンテナンス性や可読性の悪いコードベースになってしまっているため、開発生産性の向上を図る上での障壁となっていました。今後も商品詳細画面にはさまざまな改修が想定されるため、大規模なリファクタリングの検討を始めました。 リファクタリングを進めるために 商品詳細画面をリファクタリングするにあたり、以下を目標にリファクタリングを進めることにしました。 責務が分割できており、複数人が同時に開発できる状態であること ユニットテストが書ける状態であること 新規機能を追加するとなった際、今までよりも小さい工数で実装できること これらを達成するため、次のようなアプローチでリファクタリングを進めていきました。 大規模なリファクタリングのための工数確保 複数のプロジェクトが並行で進むこと、「開発生産性の向上」が目標として掲げられていることに鑑み、大規模なリファクタリングを決断しました。そのため、まずは大規模なリファクタリングを行うための工数を確保するところから始めました。 プロジェクトの合間の時間を使用してリファクタリングを進めることも検討しましたが、リファクタリングの完了までに時間がかかりすぎてしまう懸念がありました。そのため、リファクタリング自体をプロジェクト化して進められないかを検討しました。 ZOZOTOWN開発本部では、時間がかかったり、プロダクト品質向上につながったりする改善タスクはプロジェクト化し、工数を確保して進めるという文化があります。そのため、今回の商品詳細リファクタリングも同様にプロジェクト化し、まとまった工数を確保しつつ進めることになりました。 こうして、大規模なリファクタリングを進めるための工数を確保できました。 新たなアーキテクチャの採用 ZOZOTOWN iOSチームではMVVM(Model-View-ViewModel)アーキテクチャを採用しています。しかし、この構成では1画面での機能が増えた際、ViewModelの責務が肥大化してしまい、Fat ViewModelになってしまうという課題がありました。今回のリファクタリング対象である商品詳細画面は特に機能が多い画面であるため、Fat ViewModelとなる懸念が挙がりました。 そこで、 Android Architecture Components を参考に、Domain Layer(UseCase)を導入することにしました。Domain Layerを設けることでViewModelからビジネスロジックを分離でき、Fat ViewModelになってしまうことを避けることができました。 最終的なclass構成と依存関係は以下のようになります。 UseCaseを導入した結果、ビジネスロジックをViewModelから切り離すことができ、ViewModelは画面の状態管理の責務を担うようにできました。その結果、ViewModelの肥大化を防ぎ、コードの保守性が大幅に向上しました。 チームとしてのリファクタリングの進め方 ここまではリファクタリングを進めるための前段階の話をご紹介しました。ここからは、ZOZOTOWN iOSアプリチームでのリファクタリングの進め方についてお話しします。 以下のようなステップに分け、チームで認識のすり合わせをしながらリファクタリングを進めました。 アーキテクチャレビューの実施 リファクタリングを複数のStepを分割 Stepごとにリファクタリングを実施 StepごとにQA、リリース アーキテクチャレビュー ZOZOTOWN iOSチームでは、新規で画面を作る際にアーキテクチャレビューという会を行なっています。アーキテクチャレビューでは、「基本設計を実現するために必要なアーキテクチャを把握し、それをチーム内でどう開発すべきかを合意すること」を目的としています。実装前に擦り合わせをしておくことで、実装時の手戻りを減らすだけでなく、Pull Requestをレビューする際の負担軽減も期待できます。 アーキテクチャレビューでレビューしている内容は以下の2点です。 レイヤー間の値の受け渡し レイヤー内の責務分割 レイヤー間の値の受け渡しでは、各レイヤー間でどのような値が、どのようなインタフェースで実現されているか、レイヤーを跨ぐ際に値がどう変換されるかをレビューします。アーキテクチャレビューでは、各レイヤーのProtocolをもとにレビューをします。 /// APIからのレスポンスをパースするためのModel struct GoodsDetailResponse : Decodable {} /// 商品の詳細情報を表現する、View用のModel struct GoodsDetail {} protocol GoodsDetailAPIClientProtocol { func getGoodsDetail ( id : Int , completion : @escaping (( Result < GoodsDetailResponse , Error >) -> Void ) ) } protocol GoodsDetailUseCaseProtocol { func fetchGoodsDetail ( id : Int , completion : @escaping (( Result < GoodsDetail , Error >) -> Void ) ) } protocol GoodsDetailViewModelInputsProtocol { func viewDidLoad () } protocol GoodsDetailViewModelOutputsProtocol { var goodsDetailAnyPublisher : AnyPublisher < GoodsDetail , Never > } 上記のようなProtocolをもとにアーキテクチャレビューを実施します。このProtocolに沿って実装すると、成果物となるコードもある程度予想でき、設計時と実装時の認識のズレを減らすことができるのではと期待しています。 リファクタリングを複数のStepに分割 商品詳細画面は抱える機能も多く、変更量はとても多くなることが予想されました。一気にリファクタリングした場合、不具合を発生させるリスクやQAでの見落としが懸念されました。そのため、今回のリファクタリングでは複数のStepに分割し、段階的なリリースをする方針で進めました。 商品詳細画面リファクタリングのStep分割は、APIの呼び出し単位で分割しました。APIの呼び出し単位でStepを分割することで、Pull Requestのレビューがしやすいといったメリットもありました。また、Stepを細かく分割していることで、急遽優先度の高いプロジェクトが入ってきた時でも柔軟に対応できました。 おわりに 本記事では、Fat ViewControllerになってしまっている商品詳細画面をリファクタリングする話をご紹介しました。リファクタリングを始めるタイミングで設定した目標通り、責務は適切な粒度で分割され、ユニットテストも書かれている状態になりました。まだ比較はできていませんが、新機能を追加するとなった場合も今までより少ない工数で実装できる見込みです。 商品詳細画面だけでなく、ZOZOTOWN iOSアプリにはまだまだレガシーなコードが残っています。今後も負債と向き合いつつ、より良いコードを目指し改善を進めていく予定です。 ZOZOでは、一緒にサービスを作り上げてくれる仲間、レガシーなコードを書き換えていく仲間を募集中です。ご興味がある方は、以下のリンクからぜひご応募ください! https://hrmos.co/pages/zozo/jobs/1809846973241688209 hrmos.co corp.zozo.com ZOZOTOWNにおける開発生産性の向上に関する取り組みに関しては、「 ZOZOTOWNにおける開発生産性向上に関する取り組み 」のスライドをご覧ください。 ↩
こんにちは、DevRelブロックの ikkou です。2024年8月22日の夕方から24日の3日間にわたり「iOSDC Japan 2024」が開催されました。ZOZOは昨年同様プラチナスポンサーとして協賛し、スポンサーブースを出展しました。 technote.zozo.com 本記事では、前半は「iOSエンジニアの視点」から、ZOZOから登壇したセッションとiOSエンジニアが気になったセッションを紹介します。そして後半は「DevRelの視点」から、ZOZOの協賛ブースの様子と各社のブースコーデのまとめを写真多めでお伝えします。 登壇内容の紹介 LT: 全力の跳躍を捉える計測アプリを作る ポスターセッション: Haptic Feedbackでクセになるユーザー体験を提供しよう! ZOZOのiOSエンジニアが気になったセッションの紹介 Server-Driven UI入門: 画面のStateを直接受け取るアプローチ by nade 例: タップルの検索機能 watchOS 最前線 〜現代のApple Watch向けアプリの作り方〜 by kouki_dan Accessibility for Swift Charts 〜 by Mika Ito 健康第一!MetricKitで始めるアプリの健康診断 Apple Siliconを最大限に活用する方法 ZOZOブースの紹介 協賛企業ブースのコーデまとめ スポンサーブースA(1F) スポンサーブースB(1F) スポンサーブースC (2F展示ルーム) スポンサーブースD (1Fコミュニティスペース) Tシャツスポンサー アフターイベントを開催します おわりに 登壇内容の紹介 今年のiOSDCではLTに1名、ポスターセッションに1名、パンフレット寄稿に3名が採択されました。会場で発表されたLTとポスターセッションについて紹介します。 iOSDC Japan 2024の公募に採択されたZOZOスタッフ LT: 全力の跳躍を捉える計測アプリを作る 連続登壇3年目にしてLTの大トリを務めたおぎじゅん( @juginon ) speakerdeck.com 新卒3年目にして連続登壇3年目となるiOSエンジニアのおぎじゅんは、昨年・一昨年の「疾走」から「跳躍」にテーマを変えてトークを披露しました。手拍子まじりで進められるLTはおぎじゅんならではの非常に勢いがあるものだったのではないでしょうか。 このLTの背景や苦労話は個人ブログに詳しくまとめられているので、あわせてご覧ください。 ogijunchang.hatenablog.com おぎじゅんからのコメント 今年は大トリということでプレッシャーを感じていましたが、全力で楽しく発表できました! 皆さんの頭の中に少しでも残るLTになっていたら幸いです。 ポスターセッション: Haptic Feedbackでクセになるユーザー体験を提供しよう! ポスターセッション中のイッセー( @15531b ) speakerdeck.com 新卒1年目のiOSエンジニアであるイッセーは、Haptic Feedbackに関するポスターセッションを行いました。期間中、各日1時間をコアタイムとしてポスターの前に立ち、来場者とのコミュニケーションを楽しんでいました。Haptic Feedbackはその特性上、実際に触ってみることで理解が深まるため、iPhone実機に触れてもらいながら説明できるポスターセッションは非常に有効な手法でした。 iPhone実機でHaptic Feedbackを体験してもらっている様子 イッセーからのコメント 今年4月に新卒入社し、初めて登壇の機会をいただきました。参加者の皆様とポスターを見ながらお話しする中で、Haptic Feedbackに関する知見をさらに深めることができました。足を運んでいただいた皆様、ありがとうございました。 ZOZOのiOSエンジニアが気になったセッションの紹介 Server-Driven UI入門: 画面のStateを直接受け取るアプローチ by nade ZOZOTOWN開発本部iOSブロック1内定者アルバイトのだーはまです。nadeさんの「 Server-Driven UI入門: 画面のStateを直接受け取るアプローチ 」が個人的にかなり良かったので紹介します! Server-Driven UI (以下:SDUI) を初めて耳にする方もいると思います。SDUIとは、事前に定義されたUIコンポーネントのLayout/State/Actionをサーバーからレスポンスとして返すというコンセプトをもとにした開発設計です。クライアント側で実装していたUIロジックを(クライアントではなく)サーバーに押し込むような設計となっており、SDUIを導入することで開発工数やクライアントからのリクエスト数などを削減でき、開発スピードの向上が見込めます。 トークの構成は3部制(入門、番外、応用)となっていて、SDUIの基礎から実践までを網羅的に学べます。特に、応用編で述べられていた既存アプリ(タップル)へSDUIを導入する話は、SDUI導入を検討しているエンジニアにとっては必見です。 下記は個人的に重要だと感じたトーク内容をまとめています。 nadeさんは、SDUIのコンセプトを3つの参考記事から紐解いています。 WWDC 2010:Building a Server-Driven User Experience Spotify 2016:Backend-driven native UI AirBnB2021:A Deep Dive into Airbnb’s Server-Driven UI System WWDC 2010は、サーバーからクライアントへUI Componentのプロパティごと返却することで開発の柔軟性をあげようという思想のもと発表されたようです。 上記3つの記事から導かれたSDUIのコンセプトは、”事前に定義されたUIコンポーネントのプロパティを含めてサーバーから返却すること”です。 以下に示すようUIコンポーネントのデータが含まれたJSONを、クライアントはサーバーから受け取ります。また、swiftではCodableによってJSONをstructへ簡単に変換できます。これを用いてJSONデータをSwiftUI.view(Struct)へ変換しUIを構築します。 { "screens" : [ { "id": "ROOT", "layout": { "wide": {}, # landscape mode "compact": { # portrait mode "type": "SingleColumnLayout", # レイアウトの種類 "main": { "type": "MultipleSectionsPlacement", "sectionDetails": [ # 画面要素のsectionIdのみ { "sectionId": "hero_section" }, { "sectionId": "title_section" } ] } } } } ] } { "sections" : [ { "id": "toolbar_section", "sectionComponentType": "TOOLBAR", "section": { "type": "ToolbarSection", "nav_button": { "onClickAction": { # タップ時のアクション "type": "NavigateBack", "screenId": "previous_screen_id" } } } }, { "id": "hero_section", "sectionComponentType": "HERO", "section": { "type": "HeroSection", "images": [ # HeroSectionコンポーネントのState、typeごとに型が違う "api.hoge.com/..." ] } } ] } メリットとデメリットは以下の通りです。 メリット 画面構成を変更するたびにアプリの審査やリリースする必要がなくなる iOS,Androidで開発している場合、これまでは画面に関するロジックを2つ(iOS,Android用)書く必要があったが、統一可能となる トーク中、nadeさんからモバイル(iOS,Android)の開発において同じロジックをOS毎に分けて書いているのは、DRY原則(Don't repeat yourself=繰り返しを避けろ)になるのではと話されていて、モバイル開発の問題をついていて面白いなと思いました。 サーバーへのリクエスト数を減らせる これまでは、1画面を作るのに画面を組み立てるために”プロパティを得るため複数回のリクエスト”を送っていました。しかし、SDUIではサーバーからプロパティ込みでレスポンスがあるので、複数回リクエストする必要がなくなり、リクエスト数を減らせます。 デメリット 事前のUIコンポーネント定義や基盤構築が大変 クライアントーバックエンドのスキルを併せ持つ人材が必要 BFFを書けるクライアントエンジニア + UIのStateを適切に考えられるバックエンドエンジニア 属人性が高くなる システムの境界を引く位置が既存の開発手法と異なるというSDUIの性質を応用して、nadeさんは境界の位置を調整した軽量SDUIを提案しています。軽量SDUIは、全てのUIロジックをサーバーに移行するのではなくシステム運用可能性の高いものだけを限定して譲渡する設計です。 例: タップルの検索機能 layoutはクライアントが担当し、state,actionをバックエンドが担当する。そして、SwiftUI.viewにStateを定義し、そのままバックエンドから受け取る。つまり、layout/state/action全てをバックエンドに渡すのではなく、特定のstate/actionのみを渡す、これが軽量化されたSDUI、軽量SDUIです。FatVIewではなく状態管理が適切に行われているプロジェクトであれば、SDUIの導入も比較的容易だと思います。 KMPと似ていてロジックを共通化して開発効率をあげようという設計、面白いです。 watchOS 最前線 〜現代のApple Watch向けアプリの作り方〜 by kouki_dan ZOZOTOWN開発本部iOSブロック1のだーはまです。kouki_danさんの「 watchOS 最前線 〜現代のApple Watch向けアプリの作り方〜 」がApple Watch開発のキャッチアップとしてとても参考になったので紹介します。 本トークでは、Apple Watch(以下:Watch)アプリ実装に関するTipsが紹介されています。トークを聞くことで、Watchアプリの実装イメージが具体化され、開発してみようと(重い腰)を上げられるようになるはずです。kouki_danさんが個人で開発している2つのアプリを例として話が進むため、実践的で分かりやすいトークでした。 トークを聞き、個人的に納得したのがiPhoneとWatchではUXが異なるという点です。iPhoneは操作を画面と指で行うのに対して、Watchは画面と指に加えてDigital Crownも用います。また、画面サイズも小さいです。そのため、ユーザーがアプリを使う場面や操作方法が異なり、Watch独自のユースケースを考える必要があります(だーはまの声:何かZOZOでもWatchアプリ作れないかな…)。 WatchアプリはSwiftUIと実装コードが非常に似ているため、SwiftUI触ったことある人なら比較的簡単に実装できると思います。しかも、iPhoneに比べて画面サイズが小さいため、実装工数も少ないです。 下記で、Watchアプリ実装のTipsを紹介していきます。 TabView : タブ表示が可能となる モディファイア.tabViewStyle(.verticalPage) によって、Digital Crownのスクロール方向を指定可能です。実装をみてわかるように、VStack, Image, TextなどSwiftUIをそのまま流用できる箇所が多くあります。 // MARK : - TabView TabView { VStack { Image(systemName : "globe" ) .imageScale(.large) .foregroundStyle(.tint) Text( "Tab1" ) } VStack { Image(systemName : "globe" ) .imageScale(.large) .foregroundStyle(.tint) Text( "Tab2" ) } List( 0 ..< 20 ) { Text( "Item \( $0 ) " ) } } .tabViewStyle(.verticalPage) TimelineView : 時間経過によって変化するViewの描画に使う contextが時間経過とともに変化していき、 Circle() を再描画させます。 TimelineView(.animation) { context in let elapsedTime = context.date.timeIntervalSince(start) ZStack { Circle() .stroke(Color.gray, lineWidth : 20 ) Circle() .trim(from : 0 , to : elapsedTime / 60 ) .stroke(Color.green, lineWidth : 20 ) .rotationEffect(.degrees( - 90 )) Text(String(format : "%.2f" , max( 0 , 60 - elapsedTime))) .monospacedDigit() .font(.title) } } .difitalCrownRotation(_:) : デジタルクラウンの回転状況を検知可能 デフォルトでは回転方向が縦になっています。 Text( " \( rotation, specifier : "%.1f" ) " ) .focusable() .digitalCrownRotation( $rotation ) .scenePadding(_:) : paddingの指定が可能となる。 Watchは画面が小さく、また角丸になっているため、paddingが重要になってきます。この .scenePadding(_:) を指定しないとpaddingがなくなり、コンポーネントが見切れてしまう可能性あります。 トーク内では、上記以外の実装についても触れられています。気になる方はスライドやアーカイブをチェックしてみてください。 Watchアプリの実装について網羅的に知れるとても有意義なトークでした! iOSDCではトーク後5分間のディスカッションの時間が設けられています。僕が実際質問したことを書いて、このトークの紹介を終わりにしたいと思います。 Q. iPhoneとWatchではUXがかなり異なると思うが、参考にするべきサイトやアプリはありますか? A. Appleが出しているWatchアプリ 今後Watchアプリを開発しデザイナーと話す際、思い出して参考にしたいと思います。 Accessibility for Swift Charts 〜 by Mika Ito FAANS部フロントエンドブロックiOSエンジニアの ましょー です。私は、Mikaさんの「 Accessibility for Swift Charts 」についてご紹介します! このセッションは、目の見えない、見えにくい方のために、Swift Chartsで作成されたグラフのデータをVoiceOverやAudio Graphsを用いて音で表現してみたという内容でした。日経平均株価の折れ線グラフを音で表現したり、グラフを音にしたりしてみると音楽になっていたりと、とても面白く、惹きつけられるセッションでした。 まず、このセッションで利用されているSwift Charts、VoiceOver、Audio Graphsについて紹介します。Swift ChartsはSwiftUIを用いて折れ線グラフ、散布図などのグラフを作成できるフレームワークです。また、VoiceOverとは、グラフのデータにカーソルを合わせると値を読み上げてくれる機能です。歩数計だと「〇〇年〇月 歩数 1日平均〇〇歩」のように読み上げてくれるようです。Audio Graphsはグラフのデータを音声に変換する機能です。こちらはVoiceOverとは異なり、文章ではなく音の高低でデータを表現します。 このセッションではSwift Chartsで作成されたデータをVoiceOverやAudio Graphsでいかに読み上げるかについて言及しています。前提としてSwift ChartsはVoiceOverやAudio Graphsを自動サポートしているようです。例えば、以下のようなプログラムでグラフを生成したとします。 struct Climate : Identifiable { var id : UUID = . init () var month : Int var precipitation : Double } var body : some View { Chart { ForEach(data, id : \.id) { item in BarMark( x : .value( "Month" , item.month), y : .value( "Precipitation" , item.precipitation) ) } } .chartXAxisLabel( "月" ) .chartYAxisLabel( "降水量" ) ... } 上記のプログラムでは、月ごとの降水量を棒グラフで表示しています。この棒グラフをVoiceOverやAudio Graphsで読み上げてみると、以下のような問題が発生します。 単位が読み上げられないため、何の値かわかりにくい(VoiceOver) 降水量はDoubleなのに小数点以下が読み上げられていない(VoiceOver) 例えば7月のデータをタップした際に「7to8」のように読み上げられてしまう(VoiceOver) 「X軸はMonthです、Y軸はPrecipitationです」のように各軸が英語で説明される(Audio Graphs) これらの問題を解決するためにMikaさんは次のような変更をプログラムに加えていました。 var body : some View { Chart { ForEach(data, id : \.id) { item in BarMark( x : .value( "月" , item.month), y : .value( "降水量" , item.precipitation) ) .accessibilityLabel( " \( item.month ) 月" ) .accessibilityValue( " \( String(format : "%.1f" , item.precipitation) ) ミリメートル" ) } } .chartXAxisLabel( "月" ) .chartYAxisLabel( "降水量" ) ... } 上記のように、単位の追加やラベルを日本語に修正するなどの変更を加えることで先述の問題点を解決でき、意図した読み上げを行えるようです。音を用いてデータを伝える場合には、データがどのように読み上げられるか、聞き手に正しく伝わるかを考慮して、プログラムを作成する必要があると感じました。 本セッションを聞いて、データを音で表現することにとても興味が湧きましたし、たくさんの可能性を感じました! 私の開発しているFAANSでも、コーデの売上や送客数などをグラフで表現しているため、アクセシビリティ対応の一環としてデータの読み上げ機能を実装してみたいと思います! 健康第一!MetricKitで始めるアプリの健康診断 ZOZOTOWN開発本部 iOSブロックの @tsuzuki817 です! 自分からは nekowen さんの「 健康第一!MetricKitで始めるアプリの健康診断 」をご紹介いたします! iOSアプリのパフォーマンス改善の指標は以下の4つです。 応答性 タップやジェスチャーに対してアプリがどのくらいの速度で応答するか アプリの起動時間 ユーザーがアプリアイコンをタップしてから起動するまでの時間 メモリの使用量 アプリがデバイスのメモリを利用している使用量 バッテリー効率 デバイスのバッテリーの持ちを良くする またパフォーマンス情報の収集の方法として3つ挙げられておりました。 Xcode Organaizer 設定不要(XcodeのOrganizerで確認できる) 基本的なパフォーマンスデータが可視化できる TestFlightでは集計されない MetricKit 要実装(比較的簡単) 収取したデータをCrashlyticsなどに集約できる カスタムイベントの追加が可能 Firebase Perfomance Monitoringなどの外部サービス SDKの導入だけで自動的に収集される カスタムイベントの追跡が可能 詳細なパフォーマンスデータは取れない それぞれに長所・短所があるので各々のプロダクトに見合った計測方法の検討が必要です。 パフォーマンス情報で見ておくべき観点は以下の3つです。 アプリの起動時間が伸びていないか アプリがシステムによって終了されていないか アプリがフリーズしていないか アプリの起動時間は今回のiOSDCの他のセッションでも度々挙げられており、さまざまなシーンでアプリが普及している現代ではかなり重要度が上がっていると思います。 MetricKitの情報はFirebaseのCrashlyticsに一緒に送ることができます。実例でCrashlytics、TestFlightにクラッシュログが落ちていないクラッシュをMetricKitを使って事前に検知する仕組みを導入しており、目に見えにくいかつ他ツールではハンドリングが難しいクラッシュの把握に役立つことがわかりました。 アプリも人間も健康第一だと思います。アプリの健康診断の項目を適切に考え、より良いアプリを作り続けていくための仕組みづくりを整えていきたいと思わせてくれる素晴らしいセッションでした! Apple Siliconを最大限に活用する方法 ZOZOTOWN開発本部 iOSブロックの @tsuzuki817 です! 自分から @EXCode013 さんの「 Apple Siliconを最大限に活用する方法 」をご紹介いたします! Apple Siliconのスマートフォン、PC、スマートウォッチを使っているのにApple Siliconのパワーを最大限に活用しないのは勿体無いと思いセッションを聞かせていただきました! CoreMLを使うことでApple SiliconのNeural Engineのパワーを引き出すことができるそうです。CoreMLと聞くと一見難しそうでしたが、CoreMLを動かす上位のフレームワークである以下のようなフレームワークを使うことでも良いとのこと! Vision Neural Language Speech Sound Analysis CoreMLでモデルを作る前にこちらのフレームワークで事足りるか調査すると良さそうですね! また、AccelerateフレームワークはApple Siliconのパワーを引き出しつつ簡単に使いやすいよう抽象化されたAPIになっており既存の処理でも高速化できそうだなと思えました。その中でも一番使っていて効果が出そうだなと思ったのは vImage を使った画像処理です。 実際にセッション中にデモをしていただいたのですが、画像処理の代表と言っても過言ではないCIFilterと vImage を使って画像にブラーをかける処理を行いました。 vImage はCIFilterを使ったブラー処理よりも1.5倍ほど処理が速く、大量にCIFilterをかける処理を書いているケースなどではさらに効力を発揮するのではないかと思いました。 Ask The Speakerで vImage を使った画像処理のコードを読ませてもらったのですが、Metalのような複雑な処理は特にいらずに書かれており学習コストはそこまで高くないと感じました。デモに使ったコードはそのうち公開してくれるらしいので楽しみにしています! ZOZOブースの紹介 会期中は15名以上のZOZOスタッフが入れ替わりながらブースに立っていました。今回、ZOZOブースでは今年5月にリニューアルした「 WEAR by ZOZO 」の「 ファッションジャンル診断 」をメインコンテンツとして展示していました。 今回のメインコンテンツだったWEAR by ZOZOのファッションジャンル診断 この「ファッションジャンル診断」は、WEARに投稿されている好みのコーディネートを5枚以上選ぶことで、AIがファッションジャンルを診断し、おすすめのコーデを教えてくれる機能です。 「ファッションジャンル診断」を試している様子 ブースに訪れた方々には、お手元のiPhoneまたデモ用のiPhoneでこの「ファッションジャンル診断」を体験してもらいました。 どのジャンルでしたか? この「ファッションジャンル診断」はWEARでいつでも体験できる機能ですが、ブースで体験または診断結果を見せてくれた方には、その診断結果にあわせた診断結果ステッカーをお渡ししました。ステッカーの種類は全部で144種類! 名札に入れてもらったり、お手持ちのiPhoneに貼ってもらったり、皆さんそれぞれの使い方で楽しんでいました。 ちょっと麗しいシンプル もっとも多かったジャンルは「リラックスしがちなシンプル」で、次点に「少々スッキリしたラフ」そして「少々スッキリしたストリート」と続きました。また、当初の予想に反して意外と多かったのは「少々アクティブなシンプル」と「少々スッキリしたアウトドア・スポーツ」でした。ちなみに私は「リラックスしがちなシンプル」です! 箱猫マックスくんのステッカー その他、昨年制作したZOZOTOWNのキャラクターである「箱猫マックスくん」のステッカーは新作を交えて配布しました。このステッカーはLINEスタンプで「 箱猫マックス Vol.6 」として配信しているものです。エンジニア間のコミュニケーションに使いやすいスタンプが揃っているので、ぜひ使ってみてください! 色“縁”ぴつ また、デザイナー発案の「洒落の効いたアイテム」として「“一合一会”米」や「“失敗を水に流す”トイレットペーパー」に続き、今年は参加者の皆さんとの「縁」を結ぶ「色“縁”ぴつ」を作成しました。こちらは特定の条件を満たした方にお渡ししていました。 今年もとても多くの方にブースを来訪していただき、ZOZOの取り組みやサービスに興味を持っていただけたようで、とても嬉しく思います。また、ZOZOスタッフも多くの方とお話しでき、楽しい時間を過ごすことができました。会期の直前に「 ZOZOのiOSエンジニアに興味をお持ちの方へ 」を公開したこともあり、プロダクトごとの技術スタックの違いなどもご説明できて良かったです。来年もZOZOブースで皆さんとお会いできることを楽しみにしています! 協賛企業ブースのコーデまとめ あっすーです。iOSDC Japan 2024の協賛企業ブースを回ってきましたので、各ブースのコーデをお送りします! 各社の雰囲気に合わせたデザイン・着こなしは、やはりZOZOとしても気になるポイント。当日の会場の様子を思い出しながらご覧ください。 スポンサーブースA(1F) A1:楽天グループ株式会社さん A2:STORES 株式会社さん / 背面ロゴは会社メンバーで書いたそうです。 A3:サイボウズ株式会社さん / Tシャツの代わりにお揃いサコッシュを作成したそうです。 A4:チームラボ株式会社さん A5:LINEヤフー株式会社さん A6:ウォンテッドリー株式会社さん A7:株式会社ディー・エヌ・エーさん / トークンはネームストラップで隠れる位置に。 A9:ZOZO / 表面はZOZOのコーポレートロゴに裏面は西千葉本社の住所です。 A10:スパイダープラス株式会社さん A11:株式会社サイバーエージェントさん A12:株式会社MagicPodさん A13:株式会社メルカリさん / 右のシャツはユニフォームを意識して制作されたそうです。 A14:フェンリル株式会社さん / シルクスクリーン印刷だそうです。 A15:GO株式会社さん / 写真に映っていない黒も含めて全4色展開だそうです。 A16:株式会社アイリッジさん A17:株式会社Flatt Securityさん A18:株式会社ゆめみさん スポンサーブースB(1F) B1:Forkwellさん B2:株式会社マネーフォワードさん B3:転職ドラフトさん スポンサーブースC (2F展示ルーム) C1:Sansan株式会社さん C2:株式会社タイミーさん C3:合同会社DMM.comさん C4:株式会社メドレーさん / あえてLサイズで大きめに着用しているそうです。 C5:ROLLCAKE株式会社さん / 今回唯一のワッペンデザイン。会社のイベントで作られたそうです。 C6:株式会社アンドパッドさん C7:株式会社カサレアルさん C8:株式会社kubell(旧Chatwork株式会社)さん / 社名変更が伝わるように。 C9:RIZAPグループ株式会社さん C10:ピクシブ株式会社さん スポンサーブースD (1Fコミュニティスペース) D1:株式会社ビットキーさん D2:株式会社ガラパゴスさん D3:クックパッド株式会社さん D4:株式会社プログリットさん D5:株式会社リクルートさん D6:株式会社キャリアデザインセンターさん D7:KINTOテクノロジーズ株式会社さん / KINTO = 車のイメージを持ってもらえるよう頑張っているそうです。 D8:newmo株式会社さん D9:株式会社ココナラさん Tシャツスポンサー WED株式会社さん .images-row {width:860px !important;} こうやって並べてみると各社の雰囲気がわかりますね。色の傾向としてはやはり黒と白が多かったです。お忙しい中ご協力いただいたブースの皆さん、本当にありがとうございました! アフターイベントを開催します 締めの前に告知です! iOSDC Japanは参加レポート記事を書く #iwillblog 文化がありますが、それと同じくらいアフターイベント文化も活発です。観測する今年は限り9つのiOSDC関連アフターイベントが催されます。私たちもそのひとつとしてマネーフォワードさんと非公式の合同イベント「iOSDC Japan 2024 After Talk」を9月10日(火)19時より開催いたします。 zozotech-inc.connpass.com ZOZOからはLTに登壇したおぎじゅん、ポスターセッションに登壇したイッセーが登壇する他、協賛やブース運営に関してDevRel文脈のパネルディスカッションも予定しています。YouTubeを視聴するオンライン形式となりますので、connpassに参加登録の上、お気軽にご視聴ください! わいわいしましょう! おわりに ZOZOから参加した一部メンバーで撮影した集合写真 ZOZOは毎年iOSDC Japanに協賛し、ブースを出展していますが、多くの方との交流を通して今年も最高の3日間を過ごせました。実行委員会の皆さんに感謝しつつ、来年もまた素敵な時間を過ごせることを楽しみにしています! ZOZOでは、来年のiOSDC Japanを一緒に盛り上げるエンジニアを募集しています。ご興味のある方はこちらからご応募ください。 hrmos.co また、会期中は混雑していることも多く、じっくりとお話しする時間が取れなかったので、もう少し詳しく話を聞きたい! という方はカジュアル面談も受け付けています。 hrmos.co それではまた来年のiOSDC Japanでお会いしましょう! 現場からは以上です!
はじめに 技術評論社様より発刊されている Software Design の2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。 ZOZOTOWNリプレイスプロジェクトで採用したマイクロサービス化のアプローチでは、安全かつ整合性のとれたデータ移行が必須となりました。第4回では、このマスタDBの移行について紹介します。 目次 はじめに 目次 はじめに マスタDB移行 マスタDB移行について 要件と課題 テーブル構成を再設計したうえでデータ移行を実施する ダウンタイムなしでデータ移行を実施する 方針 異なるDBおよびデータスキーマ間で移行を実施するためEmbulkを使用する ダブルライトをリリースし、データ移行中に発生するDBへの書き込みを両DBにアトミックに実施する データを一時DBに格納し、一時DBから移行先DBにデータを移行する BulkloadとBackfillを複数回実施する データ移行の手順 1. ダブルライト処理の実装 2. データの移行の実施(1回目のBulkload&Backfill) 1回目のBulkload 1回目のBackfill 3. 削除データの対応(2回目のBulkload&Backfill) Column: Backfillの効率化 データ移行の実施 DB移行の実施 移行後に発生した問題 不整合の解消手順と実施 おわりに はじめに はじめまして。株式会社ZOZO技術本部ECプラットフォーム部の渋谷と裵です。 ZOZOTOWNは運営開始から10年以上の間オンプレミス環境で構築されたシステムを、アーキテクチャを変えずに拡大してきました。レガシーなシステムは、スケーラビリティや保守コストの問題など多くの課題が存在しています。それらを解決すべく弊社は2017年からZOZOTOWNのマイクロサービス化を進めています。 第3回 でもお伝えしたとおり、ZOZOTOWNリプレイスプロジェクトは、ストラングラーフィグパターンを採用したマイクロサービス化を進めています(図1)。ストラングラーフィグパターンとは、古いシステムの機能を徐々に新しいマイクロサービスに移行し、最終的にはすべての機能が新しいシステムに置き換えられた段階で、旧システムを停止するという戦略です。段階的にシステムを移行していくには、旧システムからマイクロサービスが使用するデータを安全に移行し、リプレイスプロジェクトが完了するまでの期間、新旧システムが整合性を保ちながら共存させる必要があります。 図1 ストラングラーフィグパターンによるマイクロサービス化戦略 そのため第4回では、安全かつ新旧システムで整合性を保証したうえでマスタDBの移行を行った方法を紹介します。 マスタDB移行 マスタDBの移行の説明に入る前に、本記事で使用する各用語について定義しておきます。 用語 定義 移行元DB 移行対象のデータが格納されているSQL Serverのテーブル 移行先DB 恒常的な本番運用を想定しているMySQLのテーブル 一時DB 移行元DBからダンプしたデータを格納する一時的なMySQLのテーブル 削除用一時DB 移行元DBと移行先DBの不整合を解消するために、削除対象となるデータを格納する一時的なMySQLのテーブル Bulkload 移行元DBから一時DBにデータをロードすること Backfill 一時DBと移行先DBの差異を埋める処理のこと td:nth-child(2) { text-align:left; } マスタDB移行について 今回のテーマである「DBの移行」は、オンプレ環境で使用していたSQL Serverから、マイクロサービスアーキテクチャに合わせたクラウド環境のAurora MySQLへ移行することを指します。 ZOZOTOWNは、各マイクロサービスが専用のDBを持っており、DB移行の際にはそれぞれのマイクロサービスが使用するデータを、オンプレ環境のSQL Serverからコピーする必要があります。しかし、ただSQL ServerからAurora MySQLへデータをコピーするだけでは済みません。レガシーな設計をモダンに再構築することや、「事業を止めない」というZOZOTOWNリプレイスプロジェクトのポリシーに則し、ダウンタイムなしでデータ移行を実施する必要がありました。 ここでは、ユーザーに影響を与えず安全にデータ移行を実施するための要件、およびそれに伴う課題や実際に採用した移行戦略について紹介します。 要件と課題 データ移行を実施するにあたって、次のような要件を満たす必要がありました。 テーブル構成を再設計したうえでデータ移行を実施する ダウンタイムなしでデータ移行を実施する テーブル構成を再設計したうえでデータ移行を実施する 冒頭でも言及しましたが、DB移行はただ単純にデータをコピーするだけでなく、既存のレガシーな設計をモダンなものに再設計することも重要な目的の1つです。SQL Serverに存在する主要なテーブルは2006年前後に設計されたものが多く、長い歴史を経たことで最適とは言えない状態にありました。 マスタDBの移行に伴い、移行元DBに存在していた不要カラムの整理、データ型やテーブル間の関係の見直しをするためにテーブル構成を再設計する必要がありました。また、オンプレ側とクラウド側でDBの種類やテーブルスキーマが異なり、レプリケーション等の手法を用いてデータ移行を実施できないため、スキーマ変更の実現手段を検討する必要もありました。 ダウンタイムなしでデータ移行を実施する ZOZOTOWNは年間1,100万人以上(2024年3月時点)の方々にご利用いただいています。アクセスが非常に多く、サービスを停止することによる機会損失が大きいため、複雑なプロセスを経てもダウンタイムなしでデータ移行を実施する必要がありました。 サービスを停止せずにデータ移行を実施するということは、データ移行実施中も移行元のDBに変更が頻繁に発生します。データ移行が完了したときは、当然ですがオンプレ側とクラウド側でデータの不整合が発生していないことが要求されるため、データ移行中に発生した変更も含めて完全に移行先DBに反映されている必要がありました。 方針 先に記載した要件と課題に対して、次の方針を立てました。 異なるDBおよびデータスキーマ間で移行を実施するためにEmbulkを使用する ダブルライトをリリースし、データ移行中に発生するDBへの書き込みを両DBにアトミックに実施する データを一時DBに格納し、一時DBから移行先DBにデータを移行する BulkloadとBackfillを複数回実施する 異なるDBおよびデータスキーマ間で移行を実施するためEmbulkを使用する Embulk は、異なるデータベース間でのデータ移行を容易にするオープンソースのETL *1 ツールです。並列処理をサポートしているため、大量のデータを効率的に移行できます。 Embulkを採用することで、SQL Server とAurora MySQLのような異なるDBおよびデータスキーマ間でのデータ移行を効率的かつ正確に行えます。 ダブルライトをリリースし、データ移行中に発生するDBへの書き込みを両DBにアトミックに実施する データ移行実施中に正しくデータが書き込まれるように、先にダブルライト処理を実装しました。ダブルライト処理とは、DBへのINSERT/UPDATE/DELETEの書き込み処理が発生した際に、移行元DBおよび移行先DBの両DBに書き込むことを指します。 ただ、ダブルライトするそれぞれのDBは、異なるDBインスタンス・異なるDB管理システムです。オンプレへの更新リクエストとクラウドへの連携の両方が成功した時点で更新をコミットする必要があります。そのため、オンプレで成功していてもクラウド側で失敗したらオンプレ側をロールバックするように、オンプレ側でトランザクションを開始する形でアプリケーションコードを実装しました。 このようにアプリケーションレベルでトランザクションを管理することにより、データ移行中のDBへの書き込みを移行元と移行先のそれぞれ異なるDB間でアトミックに実施できるようになりました。 データを一時DBに格納し、一時DBから移行先DBにデータを移行する 次の2つの要因で、移行先DBにEmubulkで直接ロードするのではなくクラウド側の一時DBを挟む方法を採用しました。 1つは、移行元DBと移行先DBはそれぞれ別のチームが管理しており、一度自分たちの管理するクラウド側にデータを持ってきたほうが操作しやすいためです。 もう1つは、データ移行実行中のデータ読み込み(クエリ実行)と該当データの書き込みの間でレコード削除が実行されると、本来削除されるはずのレコードが後から挿入されることになり差異が発生するにもかかわらず、Embulkではこれらの条件を判別するなどの細かい制御ができないためです。 自作ツールを使用して一時DBから移行先DBへデータを格納することで、SQLだけでは実装が困難な変換が可能になりました。また、ダブルライトの影響を受けることなくダウンタイムなしかつデータ整合性を確保しつつデータ移行を実施できます。 BulkloadとBackfillを複数回実施する ダブルライトが実装されていたとしても、その後のデータ移行で同様にデータ不整合が発生する可能性があるため、BulkloadとBackfillを複数回実施することにしました。 データ移行の手順 前節でも説明したとおり、データ移行実施中にもダブルライトは実行され続けており、その差異を埋めるためにBackfillを複数回実行します。本節ではレコードを一意に特定できるサロゲートキーを持つテーブルを前提として紹介していきます。 今回のDB移行は次のステップで行いました。 ダブルライト処理の実装 データ移行の実施(1回目のBulkload&Backfill) 削除データの対応(2回目のBulkload&Backfill) 1. ダブルライト処理の実装 SQL Server の移行元DBでINSERT/UPDATE/DELETEが発生した際、MySQLの移行先DBでも同様の処理を行うように実装します。この時点で移行先DBは空の状態なので、存在しないデータをUPDATEしないように移行先DBではUPSERT(データが存在すればUPDATE、しなければINSERT)する必要があります。 2. データの移行の実施(1回目のBulkload&Backfill) 1回目のBulkload Embulkで1回目のBulkloadを行い、移行元DBのデータをMySQLの一時DBに転送します。Embulkでは転送元・転送先DBの情報や、転送対象のデータを抽出するためのクエリをリスト1のように指定します。 in : type : sqlserver host: ' {{env.SQLSERVER_HOST}} ' port: ' {{env.SQLSERVER_PORT}} ' user : ' {{env.SQLSERVER_USER}} ' password: ' {{env.SQLSERVER_PASSWORD}} ' database: ' {{env.SQLSERVER_DB}} ' query: ¦ SELECT sqlserver_id as mysql_id, sqlserver_name as mysql_name, sqlserver_password as mysql_password FROM sqlserver_table out : type : mysql host: ' {{env.MYSQL_HOST}} ' user : ' {{env.MYSQL_USER}} ' password: ' {{env.MYSQL_PASSWORD}} ' database: ' {{env.MYSQL_DB}} ' table : mysql_table リスト1 1回目のBulkload inフィールドで移行元DBを、outフィールドで一時DBを定義します。移行元DBと移行先DBでカラム名が異なるので、対応するカラムをas句で変換しています。outフィールドではcolumn_optionsを使用すればカラムごとの制約も設定できますが、今回は移行先DBがテーブル定義に基づいてすでに作成されており、一時DBからのBackfill時に違反検知が可能なので使用しませんでした。 1回目のBackfill 次に、一時DBのデータをダブルライト中の移行先DBにBackfillします。ここで、BulklaodしてからBackfillするまでの間に、移行先DBにダブルライトで先にINSERTされるデータを想定し、Duplicate entryエラーを処理する必要があります。 ほかにも、Backfillする際には保存するデータを正しく取捨選択する必要があります。次のケースを考えてみます。 Aさんの情報が移行先DBにダブルライトでINSERTされる Bulkloadを実行する Backfillする前にダブルライトで移行先DB上のAさんのデータがUPDATEされる Backfillを実行し、Duplicate entryエラーが発生する この場合、ダブルライトでUPDATEされたほうが正の会員データなので、Duplicate entryエラーが発生した古い会員データは破棄する必要があります。 Backfillが完了したら、想定どおりBackfillされているかを確認するために一時DBと移行先DBの会員データを1つずつ突合して整合性を確認します。ダブルライトが続いている移行先DBは一時DBの会員を包含しているので、一時DBの会員がすべて移行先DBに存在するかを確認することになります。 ダブルライト中に1回目のBulkload&Backfillを実行した際、データは図2のように遷移していきます。最後の移行元DBと移行先DBのデータを比較すると、この段階ではまだデータが一致していない可能性があることがわかります。 図2 1回目のBulkload&Backfill実行時のデータ遷移の例 3. 削除データの対応(2回目のBulkload&Backfill) 図2のように1回目のBulkloadを行った後、移行元DBで会員の退会が発生したケースを考えてみます。 移行元DBの会員は削除されますが、Bulkloadを行った時点では存在していた会員なので、1回目のBackfillにより移行元DB上で存在しない会員を移行先DBにINSERTすることになり不整合が生じてしまいます。これを解消するためには、一時DBの作成と同時に削除対象の会員を保存する削除用一時DBを作成し、DELETEした会員を削除用一時DBにINSERTした後、Backfill完了後に削除用一時DBの会員を移行先DBからDELETEする、といった手法が考えられます。 今回移行元DBでは退会した会員をDBからDELETEする物理削除ではなく、退会したことをフラグとしてカラムで管理する論理削除を採用していたので、この情報を基に削除対象会員を抽出しました。 2回目のBulkloadの実行内容の例はリスト2のとおりです。移行元DBの削除フラグを基に削除対象会員を取得しています。また、Bulkloadされた後の会員が削除対象なので、FIRST_BULKLOAD_START_AT以降の会員を取得するようにしています。 in : type : sqlserver host: ' {{env.SQLSERVER_HOST}} ' port: ' {{env.SQLSERVER_PORT}} ' user : ' {{env.SQLSERVER_USER}} ' password: ' {{env.SQLSERVER_PASSWORD}} ' database: ' {{env.SQLSERVER_DB}} ' query: ¦ SELECT DISTINCT sqlserver_id as mysql_deleted_id FROM sqlserver_table WHERE sqlserver_delete_flag = 1 AND sqlserver_deleted_at >= ' {{env.FIRST_BULKLOAD_START_AT}} ' out : type : mysql host: ' {{env.MYSQL_HOST}} ' port: ' {{env.MYSQL_PORT}} ' user : ' {{env.MYSQL_USER}} ' password: ' {{env.MYSQL_PASSWORD}} ' database: ' {{env.MYSQL_DB}} ' table : mysql_deleted_table リスト2 2回目のBulkload 2回目のBackfillでは、移行先DBから削除用一時DBの会員をDELETEします。その後、1回目と同様に想定通り会員が削除されているか、削除用一時DBの会員が移行先DBに含まれていないかを検証します。 ダブルライトしながら2回目のBulkloadとBackfillを行った場合、データは図3のように遷移します。1回目のBulkload&Backfillで発生していたデータの不一致が解消されていることがわかります。 図3 2回目のBulkload&Backfill実行時のデータ遷移の例 Column: Backfillの効率化 Backfillを行う際、1レコードずつINSERTするのは非常に時間がかかります。そのため、リストAのように1回の実行で大量のデータをまとめて取り込む(BulkInsert)ようにしました。 INSERT INTO mysql_table(name, email, password) VALUES ( ' 鈴木太郎 ' , ' tanaka@example.com ' , ' xxxx ' ), ( ' 佐藤花子 ' , ' suzuki@example.com ' , ' xxxx ' ), ( ' 田中一郎 ' , ' tanaka@example.com ' , ' xxxx ' ); リストA BulkInsertの例 しかし、これだけだとDuplicate entryエラー発生時、どのレコードが原因だったのかを特定できません。そのため、BulkInsert中にいずれかがDuplicate entryエラーになった場合は、1件ずつINSERTしなおすように実装する必要があります。 今回Bulkloadするレコード数の単位は1,000件としました。これは、BulkInsert中はテーブルロックがかかってしまうため件数を多過ぎないようにしたいという意図と、少な過ぎると速度向上が期待できなくなるという懸念を加味して経験則から設定された値です。 データ移行の実施 DB移行の実施 いきなり本番DBで移行を試みるわけにはいかないので、先に検証用の環境で素振りを行います。素振りの結果、メールアドレスと旧ID(メールアドレス以前にログインIDとして使用していたもの)を管理するそれぞれのカラムで次の問題が発生しました。 移行元DBでは重複を許可していたが、移行先DBではユニークキーとして定義されている 文字コードが移行元DBと移行先DBで異なるため、全角文字が「???」になってしまう データの重複に関しては、移行元DBで最後にアクセスした日時を保存しているので、メールアドレスを管理するカラム・旧IDを管理するカラムともに、一番新しいデータを正として移行するようにしました(同じメールアドレス・旧IDを使用しているが利用者は別の会員がいた場合、カスタマーサポートでの問い合わせで対応するようにしました)。 文字コードに関しては、カラムごとに別の対応を取りました。 メールアドレスを管理しているカラムの場合、重複時の対応と同様に最終アクセス日時が新しいデータを正として移行しました。一方旧IDを管理しているカラムの場合、全角文字を含むカラムはNULLとして保存するようにしました。この方針は、今後の新規会員は旧IDで登録されないこと、旧IDがNULLになることでログインできなくなる会員がごく少数なのでカスタマーサポートへの問い合わせで十分対応できることを考慮して至った結論です。 上記に対応して検証環境で正常に移行できることを確認した後、本番環境でもおおむね想定通りデータ移行を完了できました。約2千万件のデータ移行を完了するまでのBulkload&Backfillの所要時間は、それぞれ1回目が23分と16分、2回目が24秒と9秒でした。 移行後に発生した問題 DBの移行が完了して一件落着かと思いましたが、移行してからしばらく経過した後に新たな問題が発生しました。ZOZOTOWNではPayPayに連携して決済を行えるのですが、この方法で決済ができないというお問い合わせが多発したのです。 調査の結果、原因はPayPay連携解除時のダブルライトができていなかったことでした。 本来であれば、連携解除した際にオンプレ側のDBで解除処理をした後でマイクロサービス側のDBでも解除処理をする必要があります。しかし、その考慮が漏れていたためマイクロサービス側に連携解除済みの会員が残ることでデータ不整合が発生し、その会員が再連携したタイミングで既に連携データが存在するというエラーが返っていたのです。 不整合データ数が少なければ手動やスクリプトでの対応を考えたのですが、想定よりもはるかに多くのデータに不整合が生じていたので、マスタDBと同様にオンプレDBからデータ移行を行うことにしました。 不整合の解消手順と実施 基本的にはマスタDB移行の手順と同じですが、より正確に移行したことを確認するために今回は1、2回目のBulkload&Backfillを完了した後で3回目のBulkloadを行い、抽出したデータとマスタDBのデータを突合しました。 こちらも先に検証環境で素振りを行いました。結果として計4回の素振りを行いましたが、検証環境では次のようにオンプレやクラウドDBのPayPay連携会員テーブルが本番では想定していない状態で運用されていたので、この原因調査や対応を行う必要がありました。 ZOZOTOWN会員テーブルへの外部参照を持っているPayPay連携会員テーブルのカラムに、ZOZOTOWN会員テーブルに存在しないレコードが登録されていた オンプレDBに重複した会員が登録されており、Backfill時に重複エラーが発生した 検証環境での動作確認を終えた後、本番環境で本番データの移行を行いました。一部想定外のデータが存在していたり、移行作業中に想定外の操作をした会員が存在していたりしたので多少エラーは発生したものの、手動でこれらを対応しつつ、無事PayPay連携で決済できない問題を解消できました。 おわりに 連載第4回では、ZOZOTOWNリプレイスに伴うデータ移行について紹介しました。 アクセス数が非常に多い大規模なシステムを、サービス停止することなくリプレイス対象の機能ごとにデータ移行をする必要があるうえに、レガシーなテクノロジーからモダンなテクノロジーへの移行を行うという要求がある中で、手間はかけつつも安全にデータ移行を成功させることができました。 また、本記事で紹介した戦略を用いて、今後ほかのマイクロサービス化プロジェクトにおいても迅速かつ安全にデータ移行を成功させることができると考えています。 大規模なシステムからマイクロサービス化への移行を検討している方々の参考になれば幸いです。 本記事は、技術本部 ECプラットフォーム部 ID基盤ブロックの渋谷 宥仁、裵 城柱によって執筆されました。 本記事の初出は、 Software Design 2024年8月号 連載「レガシーシステム攻略のプロセス」の第4回「ZOZOTOWNリプレイスにおけるマスタDBの移行」です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com *1 : Extract Transform Loadの頭文字を取ったもので、データを転送元から抽出し、適切なフォーマットに変換し、転送先へ出力する処理のこと。
はじめに こんにちは。基幹システム本部・物流開発部の上原です。昨年度に中途入社しまして、現在はZOZO基幹システムのリプレイスを担当しています。前職では、SESエンジニアとしてリプレイスプロジェクトに上流工程から参画し、大規模なシステムの言語リプレイスを経験してきました。さて私の紹介はこの辺りにして本題に入ります。 基幹システムリプレイスは既に進行しており、本年度には発送領域の機能を発送マイクロサービスとして切り出してリリースしました。それに続いて、入荷領域の機能をマイクロサービス化ではなくモジュラーモノリスに移行するリプレイスも進んでおり、こちらは細かく区切った単位でリリースをしています。 本記事では、自動テストによる「等価比較」を本番環境で実施しながら言語リプレイスを進めた事例を紹介します。この事例では、「言語間での処理の等価性を保証し、安心・安全にリプレイスをする」ということを目的としています。この事例が大規模なシステムの言語リプレイスの一助となれば幸いです。また、この事例の前段として先日行われたMeetUpにて入荷リプレイス自体の要件などを説明しています。 speakerdeck.com このスライドを見てから本記事を読むと更に理解が深まるかもしれませんので、よろしければぜひ。 目次 はじめに 目次 基幹リプレイスの方針 モジュラーモノリス基盤に移行するには 既存システムと並行開発を進めるために なぜ並行開発をする必要があるのか 既存ファイルの変更通知 段階的リプレイスとフェーズについて フェーズ1の定義 フェーズ2の定義 等価比較について 等価比較の仕組みの導入 等価の定義 等価比較の種類と概要 取得系の場合 更新系の場合 等価比較の実装 取得系の実装イメージ 等価比較APIの実装イメージ 更新系の実装イメージ 等価比較バッチの実装イメージ 等価比較のON/OFF更新用の画面を作成 Slack通知の導入 等価比較のメリデメ まとめ 基幹リプレイスの方針 既存の基幹システムは、モノリシックかつレガシーな技術で稼働しています。異なる領域の機能が同居するモノリシックな構成であるが故に、ある領域における障害が全体に影響を与えてしまうという課題が存在します。その状況を打破するべく、昨年、発送機能のリプレイスを開始しました。 発送機能は、その他の機能との障害分離が必須要件であることに加え、その他の機能との結びつきも比較的弱いという状態でした。そのため発送リプレイスは、言語の置き換え・アーキテクチャの刷新・DB分離を実現した発送マイクロサービスとして計画的なビッグバンリリースをしました。言い換えるとマイクロサービス化によって発送機能は完全に独立したモジュールになりました。 ただ、最初からマイクロサービス化できるかどうかには条件があります。 対象機能が独立して開発・運用できるか データの分割ができるか この条件が満たせない限りは、モジュール化の難易度がグッと上がります。この難易度を差し引いてもメリットがあれば、マイクロサービスを目指します。そうでなければ、モノリスのまま段階的にモジュール性を高めていき、いわゆるモジュラーモノリスを目指すことを基幹リプレイスの方針としました。 今回紹介する入荷リプレイスの事例は、発送リプレイスほど障害分離の優先度が高くなく、モノリスの他の機能と結びつきが強い領域です。そのため、マイクロサービス化のメリットが少ないと判断し、この領域は、モジュラーモノリスを目指して段階的にリリースをすることにしました。まずは基盤を移行して、完全に独立したモジュールにできるかを開発しながら検討していく方式です。 モジュラーモノリス基盤に移行するには モジュラーモノリス基盤 1 への移行では様々な観点の変更が必要です。その1つとしてVBScriptからJavaへの言語リプレイスがあります。言語リプレイスには、処理の等価性を保証するという大きな壁があり、等価性を保証するには、以下2つの方法があると私は考えています。 移行前と後の言語をそれぞれ調査して、処理単位で等価か人間の目で判断する方法 機械的に何らかの方法でテストをして、等価性を保証する方法 今回は、後者の方法を取って等価性を保証するようにしました。そこにいくつかの工夫を加えて、モジュラーモノリスへの基盤移行を安心・安全に進めました。本題の等価比較の話の前に、並行開発と段階リリースについて、次章で詳しく説明していきます。 既存システムと並行開発を進めるために なぜ並行開発をする必要があるのか 今回のモジュラーモノリス基盤移行では、緊急度の高い改修は移行を待たず反映させたいので、既存の開発を完全には止めず必要に応じて並行開発を行います。 既存ファイルの変更通知 前述した通り、既存開発と並行開発しているので、リプレイス予定のコードであっても緊急度の高い場合は変更されます。それとは別に意図せずリプレイス予定のコードが変更されることも考えられます。リプレイス側ではそれら全ての変更を検知し、取り込む必要があります。なので、対象コードが更新されたらSlack通知するようにしました。 対象コードに入った変更をファイル単位で通知しているので、内容を確認し、自分たちの実装に影響があるかどうかを確認します。その上で要対応ならスタンプをつける運用にしました。スタンプをつけた上で対応方針や取り込み時期などをSlackのスレッドに記載する対応をします。そして、そのスレッドを元に後日取り込みを行います。 段階的リプレイスとフェーズについて モジュラーモノリス基盤移行では、段階的にリプレイスをするためにフェーズ分けをしました。フェーズ1〜3までを検討しており、フェーズ2まで確実にする予定です。 入荷領域には、複数の実作業があり、この作業単位での開発をしました。作業単位毎に独立してフェーズが進行しています。いきなり入荷領域全てをリプレイスしているわけではありません。これを段階的リプレイスと呼んでいます。フェーズごとに区切りがあるので、この先のフェーズに進むか再度検討もできます。 次に、各フェーズの定義を説明するのですが、今回はフェーズ2まで紹介します。 フェーズ1の定義 既存システムでは、VBScript内にビューとビジネスロジックが混在しています。これもまた処理の複雑さを生んでしまっている原因の1つです。本来ビューは画面表示に関わることのみ考えればよく、ビジネスロジックは画面の処理について考える必要がありません。しかし、既存システムでは、それぞれの責務がはっきりとしておらず、相互依存しています。なのでフェーズ1では、主にビューとビジネスロジックの分離を目指します。 そのために以下の手順が完了するとフェーズ1を完了とします。 VBScriptで実装しているビジネスロジックをJavaで実装したWeb API(以降、JavaAPIと呼ぶ)に移行する VBScriptからの基幹DBへのアクセスを無くす 具体的には、JavaAPIは、既に存在しているモジュラーモノリスへ実装し、基幹DBアクセスできるようにします。これを図示すると以下の通りです。 左側は既存システムの状態を表しており、右側はフェーズ1完了後を表しています。後述しますが、置き換え後のJavaAPIの処理は置き換え前のVBScriptの処理と等価比較をしており、等価と判断されたのちにJavaAPIのみを呼び出すように切り替えます。 フェーズ2の定義 フェーズ1でビューとビジネスロジックの分離は完了しました。次は、フロントエンドのアプリケーションをJavaで実装し、 脱VBScript を目指します。これでリプレイス対象をモノリスから切り離すことができます。ここまでの作業が完了するとフェーズ2が完了です。 図示すると以下の通りです。 先ほどと同じように左側はフェーズが進む前の状態、つまり、フェーズ1の状態です。右の図はフェーズ2完了後を表しています。フェーズ2は、モノリスからビューを切り離し、入荷用フロントエンドとして独立させます。 次の章から本題の等価比較についての説明に入ります。 等価比較について 等価比較の仕組みの導入 フェーズ1を進めるために等価比較の仕組みを導入しました。仕組みについて語る前に、まずは等価比較の概念について説明します。 言語リプレイスは、リファクタリングをすることである と私は考えています。 書籍リファクタリング では、以下のようにリファクタリングについて定義されています。 リファクタリングとは、ソフトウェアの外部の振る舞いを保ったままで、内部の構造を改善していく作業を指します。 言語リプレイスも同様に振る舞いを変えずに詳細を変える作業だと思っています。つまり、言語リプレイスに1番重要なのは、振る舞いが変わっていないことを確認することです。では何が必要でしょうか。機械的に振る舞いが変わっていないことを判断できるテストが必要です。これを自動でテストし、判断するのが等価比較の仕組みです。自動テストには、inputとoutputが必要です。 今回は、本番環境のユーザ入力とシステムの出力を使用しました。そのため、検証期間中はユーザがシステムをいつも通り利用するだけで、開発者は振る舞いが変わっていないかどうかの確認ができます。概要についての説明は以上です。次は、等価比較の定義について説明します。 等価の定義 ここでは、等価比較の仕組みにおける等価の定義を説明します。以下を満たす場合、等価であると定義します。 画面に表示される内容が一致している DBなどの外部システムの状態が一致している これを機械的に判断するために、以下の指標を立てました。 HTMLテンプレートとそこに埋め込む変数の値が一致している SQLなどの外部システムへのコマンドが一致している 上記の指標を満たしている場合、対応する等価の定義を満たしていると考えます。 では、ここから等価比較の詳細についてお話ししていこうと思います。 等価比較の種類と概要 等価比較は、取得系と更新系に分けて考えます。取得系の等価比較は、Javaで実装したWeb APIを使用します。以降、これを等価比較APIと記載します。更新系の等価比較では、等価比較APIに加えてJavaで実装した定期実行バッチを使用します。以降、これを等価比較バッチと記載します。等価比較バッチは、SQLなどの外部システムへのコマンドの履歴を比較します。等価比較APIは、指標1を満たすか、等価比較バッチは、指標2を満たすかそれぞれ検証します。 取得系の場合 取得系の等価比較は、以下の流れで行います。 開発環境で比較して、エラーや不等価にならないかを確認する 本番環境で比較して、エラーや不等価にならないかを確認する リクエストを比較用ファサードで受け取ります。比較用ファサード内では、旧実装の処理と新実装の呼び出し処理が書かれており、それぞれの処理を行ったのち処理結果をオブジェクトに格納します。そして両方のオブジェクトを等価比較APIに渡し、等価か判断します。 更新系の場合 比較の流れと比較用ファサードについては取得系と同じです。前述した通り、等価比較では本番環境を使用しているので新旧どちらも更新処理をしてしまうことはできません。なので、新実装から先に実行し、新実装のみ処理の最後でコミットせずにロールバックします。処理の中で実際に発行されたSQL文を履歴として残し、その新旧処理の履歴を等価比較バッチで比較します。ここからさらに取得系、更新系それぞれの詳細な実装の話に進みます。 等価比較の実装 取得系の実装イメージ Set User = GetUser () If User = Null Then FrameWorkObject . ProcessTemplate ( "ErrorTemplate" ) Else FrameWorkObject ( "userId" ) = User ( "userId" ) FrameWorkObject ( "userName" ) = User ( "userName" ) FrameWorkObject . ProcessTemplate ( "UserTemplate" ) End If 比較前の実装はこちらです。これはVBScriptの擬似的なコードです。ユーザを取得して、取得成功すればIDとNameを埋め込んだユーザ画面を表示し、失敗の場合はエラー画面を表示します。 '比較用ファサード Function Facade () ' 旧処理の結果を格納するオブジェクト Dim BeforeObject ' 新処理の結果を格納するオブジェクト Dim AfterObject Set User = GetUser () If User = Null Then FrameWorkObject . ProcessTemplate ( "ErrorTemplate" ) BeforeObject . template ( "ErrorTemplate" ) Else FrameWorkObject ( "userId" ) = User ( "userId" ) BeforeObject ( "userId" ) = User ( "userId" ) FrameWorkObject ( "userName" ) = User ( "userName" ) BeforeObject ( "userName" ) = User ( "userName" ) FrameWorkObject . ProcessTemplate ( "UserTemplate" ) BeforeObject . template ( "UserTemplate" ) End If 'ここで新処理を実行し結果をAfterObjectに格納 Set NewUser = Replace_GetUser () If NewUser = Null Then AfterObject . template ( "ErrorTemplate" ) Else AfterObject ( "userId" ) = NewUser ( "userId" ) AfterObject ( "userName" ) = NewUser ( "userName" ) AfterObject . template ( "UserTemplate" ) End If ' 等価比較APIにリクエスト Call ExecuteComparison ( "GET" , "/user/id" , BeforeObject , AfterObject ) End Function 比較後の実装はこちらです。新旧の処理結果を格納するオブジェクトを用意し、それぞれの処理結果を格納します。そして、新旧の処理結果が格納されたオブジェクトと呼び出したエンドポイント名を等価比較APIに渡し、等価か判断します。また、既存の処理は変更していないので、ユーザに影響はありません。等価比較の期間後に旧処理とファサードを削除し、新実装に切り替えるだけで良い実装になっています。 等価比較APIの実装イメージ 等価比較APIの処理は、旧処理のJSONオブジェクト、新処理のJSONオブジェクトをそれぞれ受け取って、JSONオブジェクトが等価か判定するという方法を取ることにしました。本番環境では、結果を保存する処理によるオーバーヘッドを無くすため、等価の場合、結果を残さないようにしました。それ以外の環境は、全ての結果を残します。 public Result execute(UseCaseInput input) { final var isEqual = isEqual(input.beforeParameter(), input.afterParameter()); final Map<String, String> env = System.getenv(); if (isSaveTarget(env.get( "APP_ENV" ), isEqual)) storeLogs.save( input.endpoint(), input.method(), isEqual, toSaveFormat(input.beforeParameter()), toSaveFormat(input.afterParameter())); if (Boolean.TRUE.equals(isEqual)) { return Result.success(); } else { return Result.failure(); } } /** 正規化を行った後、比較処理を行う */ Boolean isEqual(Parameter beforeParameter, Parameter afterParameter) {} 以下がJSONのリクエストイメージです。この場合は、新処理の結果が空なので、等価比較の結果は不等価です。 { " endpoint ": " /user/id ", " before ": { " object ": { " userId ": " 1 ", " userName ": " ZOZO太郎 " } , " template ": { " userTemplate " } } , " after ": { " object ": {} , " template ": {} } } 処理結果で不等価な場合は、以下のようにDBに保存されます。 さらに比較期間中はAPI呼び出しのタイムアウト時間を1秒に設定して、本番にできるだけ影響を与えないようにしています。こちらの設定は比較完了後、通常のタイムアウト時間に戻します。 更新系の実装イメージ 更新系において指標2を検証する実装イメージについて説明します。 '比較用ファサード Function Facade () ' VBScriptでUUIDを発行 Dim UUID: UUID = GenerateUUID () ' APIを呼び出す(ロールバックされる) UpdateUser ( UUID ) ' SQLを実行&SQL文をオブジェクトに一時保存する Dim SQL SQL = "UPDATE user SET name = 'ZOZO太郎' WHERE id = 1" cmd . execute ( SQL ) ExecutionHistory . SetCommand ( SQL ) ' 実行履歴としてSQLを残す Call SaveLogs ( UUID , ExecutionHistory . toJsonString (), "user/id" ) End Function VBScriptでUUIDを発行し、APIを呼び出し、SQL文を発行します。そして、履歴としてSQL文を残します。サンプルコードには記載がありませんが、実際は呼び出したAPIでも同じようにSQL文を履歴として残しています。 等価比較バッチの実装イメージ public void execute() { // エンドポイント毎にまとめた比較待ちの実行履歴を取得する final var compareReadyCmdExecLogsEachEndpoint = cmdExecLogsQueryDataSource.getCompareReadyCommandExecutionLogsEachEndpoint(); // 比較を実行してエンドポイント毎の比較結果を得る final var compareResultEachEndpoint = compareReadyCmdExecLogsEachEndpoint.stream() .map(logsByEndpoint -> compareCmdExecLogsByEndpoint.execute(logsByEndpoint)) .toList(); // 比較結果をUUID毎に保存する saveCmdExecLogsComparisonResults.execute(compareResultEachEndpoint); // 実行履歴を比較済みに更新する final var comparedUuidList = toDistinctUuidList(compareReadyCmdExecLogsEachEndpoint); cmdExecLogsUpdater.updateToCompleteStatus(comparedUuidList); // 結果が空でない場合は比較結果をSlackに送信する if (!compareResultEachEndpoint.isEmpty()) sendCmdExecLogsComparisonReport.execute(compareResultEachEndpoint); } 等価比較バッチは定期実行なので、複数のエンドポイントの履歴が残っている可能性があります。そのため、処理の冒頭でエンドポイント毎にまとめた比較待ちの実行履歴を取得しています。また、前述した通り、VBScriptとJavaAPIは別々に履歴を残していますが、両者が揃うタイミングで比較待ちステータスになるように工夫して片方だけが拾われないように工夫しています。 実装の説明はここまでになります。次は、等価比較をするにあたって工夫したことについて説明します。 等価比較のON/OFF更新用の画面を作成 等価比較の検証は実行頻度と連動しています。実行頻度が1以上の場合、等価比較の検証が有効になります。実行頻度は等価比較の頻度を示し、例えば3に設定すると、対象処理の3回に1回等価比較をします。この頻度を調節しながら段階的に高めることで、等価比較を活用した段階的リリースを実現しています。開発者が容易に等価比較を実施できるように専用の画面、API、テーブルを作成し、検索、登録、更新機能を実装しています。また、チーム単位やエンドポイント単位での検索機能を追加し、意図しないAPIへの等価比較を防止しています。 Slack通知の導入 前述の通り、等価比較の結果が不等価だった場合、Slackに通知をするようにしました。 等価の時は見なくても良いですが、不等価の場合は、見逃したくないのでメンションします。メンションが来たら、等価にならなかった処理の調査をし、修正、リリースを繰り返します。 等価のときはSlack通知にメンションがついていない 不等価のときはSlack通知にメンションがついている 最後に、等価比較のメリット・デメリットについて説明します。 等価比較のメリデメ 等価比較には、以下のようなメリット・デメリットがあります。 メリット 本番環境でテストができるので安心して処理の切り替えができる テストエビデンスの用意に手間がかからない デメリット 等価比較を実施すると処理量が増えるため、本番環境に高負荷がかかってしまうことがある メリットは多いのですが、デメリットもあるため、等価比較をする際には注意が必要です。ただし、デメリットに挙げた負荷に関しては実行頻度を調整することで軽減可能です。 まとめ 今回、「本番環境で自動テストをする等価比較を活用した言語リプレイス」について説明しました。等価比較は、リプレイスを進める上で非常に重要な要素です。等価比較の仕組みを導入することで機械的に処理の等価性を判断できます。この結果を活用することで安心・安全に段階的なリプレイスを進めることができます。今後も等価比較を活用して、モジュラーモノリス基盤移行を進めていきます。 現在もなお、フェーズ1とフェーズ2を並行して進めています。入荷全体をフェーズ2まで進めることは確定していますが、フェーズ3以降のマイクロサービス化を進めるかどうかはまだわかりません。ただ、領域を分けたことでDBの分割ができるかもしれないという希望も見えたし、イベントストーミングなどを活用し、マイクロサービス化できるかどうかを検討し続けています。今後もモジュラーモノリス基盤移行に関する情報発信をしていくので、チェックして頂けると嬉しく思います。ありがとうございました。 ZOZOの基幹システムの開発・リプレイスに興味を持ってくださった方は、以下のリンクからぜひご応募ください。 https://hrmos.co/pages/zozo/jobs/1809846973241688190 hrmos.co 基盤の移行先はリンクの記事の図にある新モノリスです。 ↩
はじめに こんにちは。株式会社ZOZOのSRE部プラットフォームSREチームに所属している はっちー と申します。 本記事では、Kubernetesクラスター上で自動カナリアリリース機能を提供するFlaggerが導入済みのマイクロサービスにおいて、手動カナリアリリースを実施する方法について紹介します。一見、矛盾するように思えるかもしれません。しかし、時にはそのような要件も発生することがあります。また、手動カナリアリリースで運用している状態からFlaggerの導入を検討している場合、導入後も念のために現行の手動カナリアリリースができるのか、という点は気になるかと思います。すでにFlaggerを導入している、これからの導入を検討している、という方の参考になりましたら幸いです。 目次 はじめに 目次 前提知識(Flagger) Manual Gatingの基本 Manual Gatingとは Manual Gatingが必要な背景 Manual Gatingを実現する要素 Webhooks loadtester Webhooks + loadtester Manual Gatingによる手動カナリアリリース手順 正常系 Step1. CanaryリソースのWebhooksの設定を追加する Step2. Deploymentの変更 Step3. トラフィック進行 Step4. Webhooksの設定削除 ロールバック 注意点 Step1. ロールバックする Step2. ロールバックを終了する Step3. Deploymentの変更を戻す 運用の工夫 周知 Manual Gating中に他のリリースをブロックする仕組み Reusable Workflowの実装 Reusable Workflowの利用 loadtesterのエンドポイントを叩く操作をスクリプト化する 自動ロールバックの発生を防ぐ 原因 対応案 案1 案2 案3 NGな方法 まとめ We are hiring 前提知識(Flagger) Flaggerは、Progressive Deliveryのツールです。Progressive Deliveryは、カナリアリリースやA/Bテスト、Blue-Green Deploymentなどの手法を組み合わせて、リスクを最小限に抑えながらリリースを進める手法です。Flaggerを使うことで、カナリアリリースを自動化できます。より詳細については、Flaggerの 公式ドキュメント や以下の弊社テックブログなどを参照してください。 techblog.zozo.com 本記事は、Flaggerの基本知識がある前提での説明となります。 Manual Gatingの基本 Manual Gatingによる手動カナリアリリースを実現するために必要な基本知識を説明します。 Manual Gatingとは Manual Gating とは、Flaggerで手動カナリアリリースをすることです。なお、「Manual Gating」という機能がFlaggerで特別に用意されているわけではありません。Flaggerのカスタムリソースである、CanaryリソースのWebhooksなどを利用して実現します。 Manual Gatingが必要な背景 FlaggerのProgressive Deliveryにより、カナリアリリースの進行におけるメトリクスの確認や判断、加重率の変更作業、切り戻し作業などを自動化し、リリースにおける工数を削減できます。自動化できるものをわざわざ手動で行う背景は何でしょうか。それは、より慎重にリリースをしたいケースがあるからです。たとえばZOZOの場合、 内製しているzozo-api-gateway の一部のリリースにおいて、以下のケースでManual Gatingを利用しています。 1週間程度の長い期間をかけて手動カナリアリリースをしたいケース。 とくにリクエスト数が多い週末にはN%で留めて様子を見ておき、週明けにリリースを進めたい。 Flaggerの自動カナリアリリースでは、進行が速すぎるかつ、このような柔軟な進行を実現できない。 FlaggerのMetricTemplateリソースによる外部ツール(Datadogなど)のメトリクスを利用した機械的判断では不十分なケース。 たとえば、パーソナライズされた検索結果を返すような機能のリリースの場合、単純なHTTPステータスのエラー率では判断できない。誤ったパーソナライズでの検索結果でも200レスポンスであるため。人の目で丁寧に動作確認する必要がある。 なお、zozo-api-gatewayにそのような検索機能のビジネスロジックが実装されているわけではない。カナリアリリースを導入できないレガシーシステムへのルーティングもzozo-api-gatewayが担っている。zozo-api-gatewayのルーティングコンフィグを変更したうえで、zozo-api-gateway自体のカナリアリリースをして、そのレガシーシステムのカナリアリリースしている。 Manual Gatingを実現する要素 Manual Gatingは、CanaryリソースのWebhooksとloadtesterのいくつかのエンドポイントを組み合わせて、実現します。 Webhooks Webhooksは、Flaggerのanalysisを拡張する機能です。各Webhookはそれぞれの実行タイミングで実行され、CanaryリソースはそのWebhookの応答ステータスコード(成功なら2xx)からカナリアリリースが失敗しているかを判断します。詳細は Webhookのページ をご確認ください。 以下のように、Canaryリソースの spec.analysis.webhooks[].type でWebhookの種類を設定します。 spec : analysis : webhooks : - name : "gate" type : confirm-rollout url : http://flagger-loadtester.test/gate/halt loadtester loadtesterは、gateエンドポイントを通じてリリースの進行を制御するPodです。gateエンドポイントの一覧は以下です。 /gate/check カナリアリリースの進行を確認する。 gateがopenであれば approved true として、カナリアリリースを進行する。closeであれば approved false として、進行しない。 /gate/open gateをopenにする。 /gate/closeが実行されない限り、gateはopenのまま。 /gate/close gateをcloseにする。 /gate/openが実行されない限り、gateはcloseのまま。 /gate/approve 常にHTTPステータスコード202を返す。 トラフィックの進行を進めるために使用されるが、今回は使用しない(理由は後述)。 /gate/halt 常にHTTPステータスコード403を返す。 トラフィックの進行を停止するために使用されるが、今回は使用しない(理由は後述)。 Webhooks + loadtester Webhooksとloadtesterを組み合わせてManual Gatingを行う方法を2つ紹介します。 1つ目は、 type: confirm-rollout のurlを /gate/halt にしておき、カナリアリリースを進行する際に /gate/approve へ変更する方法です。一時停止するたびに /gate/halt へ戻します。 YAMLファイルには以下の設定を追加します。 spec : analysis : webhooks : - name : "gate" type : confirm-rollout url : http://flagger-loadtester.test/gate/halt # 進行させる場合は/gate/approveにする これは、公式ドキュメントで紹介されている方法です。しかしながら、この方法は実際の運用には難しいと個人的に考えています。理由は、手動でkubectlのapplyコマンドを実行する必要があるからです。N%で一時停止したい場合に、タイミングがシビアなので、CIでの反映は難しいです。本番環境で手動applyをする運用は緊急時以外にしたくないですし、タイミングが間に合わず、加重率が意図せずに進んでしまう可能性が容易に発生しうると考えています。したがって、今回は /gate/approve と /gate/halt を使用しません。 2つ目は、定期的に /gate/check して、リリースを進行するタイミングになったら手動で /gate/open することで、1つ進行したら自動ですぐに /gate/close する方法です。基本的にgateはcloseにしておきます。checkしてopenだったらすぐにcloseする理由は、openのままだと次以降のcheckでもカナリアリリースが進行し続けてしまうからです。意図せず進行してしまわないように、自動でcloseするのは要件として必須としました。図にまとめると次の通りです。 一時Podを起動してloadtesterの /gate/open を手動で叩きます(1)。Canaryリソースが定期的にloadtesterの /gate/check を叩きます。もし、gateがopenになっていれば、VirtualServiceのweightを操作してカナリアリリースを進行させます。closeであれば何もしません(2、2')。Canaryリソースが /gate/close を叩きます(3)。 YAMLファイルには以下の設定を追加します。なお、通知がノイジーになるため、muteAlertをtrueにしています。 - name : confirm-traffic-increase-gate-check type : confirm-traffic-increase url : http://flagger-loadtester.istio-system/gate/check muteAlert : true - name : confirm-traffic-increase-gate-close type : confirm-traffic-increase url : http://flagger-loadtester.istio-system/gate/close muteAlert : true type: confirm-traffic-increase でurlを /gate/check にしているので、トラフィック進行判断の直前にcheckされます。つまり、 spec.analysis.interval で設定した間隔でcheckされます。進行させたいタイミングになったら手動で /gate/open を叩いてopenにします。そうすると、次のcheckでopenであることが確認され、カナリアリリースが進行されます。進行した場合は、自動で即座に /gate/close されます。同じ type: confirm-traffic-increase で設定されているため、 gate/check の後に /gate/close が実行されるためです。 また、ロールバック用に以下の設定もします。 /rollback/check はgateエンドポイントと同様で、openであればロールバックをして、closeであればロールバックしません。ロールバックの通知はノイジーでないので、muteAlertをtrueにしません。 - name : rollback type : rollback url : http://flagger-loadtester.istio-system/rollback/check 今回は、この2つ目の方法でManual Gatingします。 Manual Gatingによる手動カナリアリリース手順 今回は、zozo-api-gatewayを例に、実際のリリース手順を紹介します。 正常系 Manual Gatingにより、0%から100%まで手動カナリアリリースを進行する手順です。 Step1. CanaryリソースのWebhooksの設定を追加する 以下の spec.analysis.webhooks の設定をCanaryリソースに追加します。 spec : analysis : webhooks : - name : confirm-traffic-increase-gate-check type : confirm-traffic-increase url : http://flagger-loadtester.istio-system/gate/check muteAlert : true - name : confirm-traffic-increase-gate-close type : confirm-traffic-increase url : http://flagger-loadtester.istio-system/gate/close muteAlert : true - name : rollback type : rollback url : http://flagger-loadtester.istio-system/rollback/check この時点では、CanaryリソースのSTATUSに変化ありません。 Step2. Deploymentの変更 たとえば、Containerのイメージを変更します。 この変更により、CanaryリソースのSTATUSがProgressingになりますが、WEIGHTは0%のままです。つまり、期待通り、カナリアリリースが自動で進行せずに止まっています。 loadtesterのPodで以下のようなログ(抜粋)を確認できます。 approved false になっています。 {"level":"info","ts":"2024-04-24T06:19:28.806Z","caller":"loadtester/server.go:79","msg":"api-gateway.api-gateway gate check: approved false"} Step3. トラフィック進行 Kubernetesクラスター内に一時的なPodを起動して、loadtesterの /gate/open を叩きます。この一時的なPodはcurlの実行が完了すると自動で削除されます。適切なnamespaceで起動してください。curlのBodyにはCanaryリソースの名前とnamespaceを指定します。 kubectl run tmp-$(date "+%Y%m%d-%H%M%S")-<your_name> --image alpine:latest -n <tmp_pod_namespace> --rm -it -- sh -c "apk add --no-cache curl; curl -v -m 10 -d '{\"name\": \"<canary_name>\",\"namespace\":\"<canary_namespace>\"}' http://flagger-loadtester.istio-system:80/gate/open" curlが成功したら、CanaryリソースのWEIGHTが増えることを確認します。今回の例ですと、10%進行しています。この値は spec.analysis.stepWeight で決定されます。また、 stepWeights で変動的な値にもできます。 loadtesterのPodで以下のようなログ(抜粋)を確認できます。 gate opened -> approved true -> gate closed になっています。この後、 approved false に戻ります。 {"level":"info","ts":"2024-04-25T05:51:38.011Z","caller":"loadtester/server.go:110","msg":"api-gateway.api-gateway gate opened"} {"level":"info","ts":"2024-04-25T05:52:29.329Z","caller":"loadtester/server.go:79","msg":"api-gateway.api-gateway gate check: approved true"} {"level":"info","ts":"2024-04-25T05:52:29.333Z","caller":"loadtester/server.go:141","msg":"api-gateway.api-gateway gate closed"} 目標のN%になるまで上記のcurlを実行する作業を複数回実施し、進行させます。 spec.analysis.maxWeight を50%にしている場合、50%まで進行させてからもう1回 /gate/open を叩くと、100%までカナリアリリースが進行します。CanaryリソースのSTATUSが Promoting -> Finalizing -> Succeeded と遷移します。 Step4. Webhooksの設定削除 次回のリリースではManual Gatingを利用しない場合、Step1のWebhooksの設定を削除します。続けて、Manual Gatingによる手動カナリアリリースする場合は、そのままで結構です。 ロールバック Manual Gatingによるカナリアリリース進行の途中で、リリース作業をする人(以下、リリーサー)の判断により手動ロールバックする場合の手順です。Flaggerのanalysisにより自動でロールバックされる場合の話ではありません。したがって、正常系手順のStep3内で実行することを想定しています。 注意点 ロールバックをするとWEIGHTは0%に戻ってしまいます。たとえば、10%まで進行していたとして、20%に進行した段階でリリーサー判断によりロールバックをしたとすると、10%に戻るのではなく0%に戻ります。もし10%に戻したい場合は、再度、正常系の手順2の「マイクロサービスの変更」からやり直しです。 Step1. ロールバックする /rollback/open を叩きます。 kubectl run tmp-$(date "+%Y%m%d-%H%M%S")-<your_name> --image alpine:latest -n <tmp_pod_namespace> --rm -it -- sh -c "apk add --no-cache curl; curl -v -m 5 -d '{\"name\": \"<canary_name>\",\"namespace\":\"<canary_namespace>\"}' http://flagger-loadtester.istio-system:80/rollback/open" CanaryのSTATUSがFailedになり、WEIGHTが0%になります。 loadtesterで以下のようなログ(抜粋)を確認できます。 rollback opened -> approved true になっています。 {"level":"info","ts":"2024-04-24T09:56:27.253Z","caller":"loadtester/server.go:207","msg":"rollback.api-gateway.api-gateway rollback opened"} {"level":"info","ts":"2024-04-24T09:56:28.802Z","caller":"loadtester/server.go:177","msg":"rollback.api-gateway.api-gateway rollback check: approved true"} Step2. ロールバックを終了する /rollback/close を叩きます。 kubectl run tmp-$(date "+%Y%m%d-%H%M%S")-<your_name> --image alpine:latest -n <tmp_pod_namespace> --rm -it -- sh -c "apk add --no-cache curl; curl -v -m 5 -d '{\"name\": \"<canary_name>\",\"namespace\":\"<canary_namespace>\"}' http://flagger-loadtester.istio-system:80/rollback/close" loadtesterで以下のようなログ(抜粋)を確認できます。 rollback closed になっています。 {"level":"info","ts":"2024-05-21T08:07:56.017Z","caller":"loadtester/server.go:237","msg":"rollback.api-gateway.api-gateway rollback closed"} Step3. Deploymentの変更を戻す Deploymentの変更を戻します。 運用の工夫 上記の手順を、より安全で効率的に実施するための運用面で工夫した点を紹介します。 周知 Manual Gatingによるカナリアリリース期間中は、そのマイクロサービスに関する他のリリースはできません。もしカナリアリリース期間中に対象マイクロサービスの新しい変更をリリースしてしまうと、その新しい変更で0%から振り出しに戻ってしまいます。したがって、その期間は他のリリース作業はストップとなることを、関係各所へ事前に連絡しておくルールとしています。 Manual Gating中に他のリリースをブロックする仕組み 上記の周知による方法では限界があります。そこで、仕組みで問題が発生しにくいようにしています。具体的には、Manual Gating中に対象マイクロサービスの変更Pull Request(以下、PR)が作成されると、CIでエラーとなるようにしています。具体的には、あるマイクロサービスがManual Gating中であるかを返すGitHub Actionsの Reusable Workflow を実装し、それを使うようにしています。 Reusable Workflowの実装 以下は、そのReusable Workflowです。どのマイクロサービスでも利用できるように、Reusable Workflowとして汎用的に実装しています。 # This workflow checks if the microservice is under the process of manual gating by Flagger. name : check-under-manual-gating-flagger on : workflow_call : inputs : ... cluster_name : description : EKS cluster name required : true type : string canary_name : description : service name required : true type : string namespace : description : namespace required : true type : string ... jobs : check-under-manual-gating-flagger : runs-on : ubuntu-latest steps : ... - name : check if under manual gating process run : | aws eks --region "$AWS_REGION" update-kubeconfig --name ${{ inputs.cluster_name}} WEBHOOKS=$(kubectl get canary ${{ inputs.canary_name }} -n ${{ inputs.namespace }} -o json | jq '.spec.analysis.webhooks' ) if echo "$WEBHOOKS" | jq 'map(select(.type == "confirm-traffic-increase" and .url == "http://flagger-loadtester.istio-system/gate/check")) | length' | grep -q 0; then echo "Not under manual gating process" exit 0 else echo "Under manual gating process" exit 1 fi kubectl get canary の結果を確認しています。typeが confirm-traffic-increase かつ、urlが /gate/check であれば、1でexitします。1の場合はManual Gating中という意味です。 Reusable Workflowの利用 上記で定義したReusable Workflowを利用して、以下の2つの条件を満たす場合はエラーとするjobをマイクロサービスごとに用意します。ここではzozo-api-gatewayを例にします。 PRに対象のマイクロサービスの変更含まれる 対象のマイクロサービスがManual Gating中である k8s-block-update-under-manual-gating-api-gateway : uses : st-tech/zozo-platform-shared-infra/.github/workflows/check-under-manual-gating-flagger.yaml@v147 if : ${{ contains(needs.k8s-directory-changes.outputs.changed_dir, 'api-gateway' ) }} needs : - set-env - k8s-directory-changes with : kubectl-version : ${{ needs.set-env.outputs.kubectl_version }} oidc_role_arn : arn:aws:iam::xxx:role/zozo-platform-infra-gha cluster_name : prd-zozo-platform canary_name : api-gateway namespace : api-gateway 1つ目の条件に関しては tj-actions/changed-files を利用した別のjob( k8s-directory-changes )を利用しています。これはFlaggerのManual Gatingとは趣旨がずれますので詳細は割愛します。興味がございましたら ついに最強のCI/CDが完成した 〜巨大リポジトリで各チームが独立して・安全に・高速にリリースする〜 をご一読ください。 エラー時は、以下のようになります。 万が一、CIでエラーになっているにもかかわらずPRをマージしてしまった場合は、そのPRをRevertし、元のN%まで /gate/open を叩いて戻します。 なお、この方法ですとマイクロサービスの数が多いとjobの数も多くなり、きつくなりそうです。しかし、今のところはManual Gatingを利用するマイクロサービスがzozo-api-gatewayしか社内に存在しないため、問題になっていません。増えてきたら、より汎用的な実装にした方が良さそうです。 loadtesterのエンドポイントを叩く操作をスクリプト化する 上記の正常系の手順には、本番環境でPodを立てる手順が含まれます。しかしながら、本番環境のKubernetesクラスター上でPodを手動により作成する作業には、人為的ミスのリスクが伴います。そのリスクを少しでも軽減するために、操作を補助する以下のシェルスクリプトを開発しました。 #!/usr/bin/env bash [[ -n $DEBUG ]] && set -x set -e # Required config: # - config/global_config.sh # - config/k8s.sh SCRIPTS_DIR = " $( cd " $( dirname " $0 " ) "; pwd ) " . " ${SCRIPTS_DIR} /config/global_config.sh " function usage_exit() { echo " Usage: $0 -e prd -o zozo-api-gateway-ops -c api-gateway -n api-gateway [-g|-r] -a open " 1 >&2 echo " Options: " 1 >&2 echo " -e Env of EKS cluster(dev-onprem, dev, stg, prd) " 1 >&2 echo " -o Ops namespace such as zozo-api-gateway-ops " 1 >&2 echo " -c Canary resource name such as api-gateway " 1 >&2 echo " -n Namespace of Canary resource such as api-gateway " 1 >&2 echo " -g Gate " 1 >&2 echo " -r Rollback " 1 >&2 echo " -a Action of service(open, close) " 1 >&2 echo " NOTE: AWS Profile requires Power User or higher permissions " 1 >&2 echo " Prerequisites: " 1 >&2 echo " AWS Credentials you need to connect each env: " 1 >&2 echo " dev-onprem - ${AWS_ACCOUNT_ALIAS_DEV} " 1 >&2 echo " dev - ${AWS_ACCOUNT_ALIAS_DEV} " 1 >&2 echo " stg - ${AWS_ACCOUNT_ALIAS_STG} " 1 >&2 echo " prd - ${AWS_ACCOUNT_ALIAS_PRD} " 1 >&2 exit 1 } readonly REGION = " ap-northeast-1 " while getopts e:o:c:n:gra:h OPT do case $OPT in e ) ENV = $OPTARG ;; o ) OPS_NAMESPACE = $OPTARG ;; c ) CANARY_NAME = $OPTARG ;; n ) CANARY_NAMESPACE = $OPTARG ;; g ) GATE = 1 ;; r ) ROLLBACK = 1 ;; a ) ACTION = $OPTARG ;; h ) usage_exit ;; * ) usage_exit ;; esac done # validation for args if [[ -z " ${ENV} " ]] || [[ -z " ${OPS_NAMESPACE} " ]] || [[ -z " ${CANARY_NAME} " ]] || [[ -z " ${CANARY_NAMESPACE} " ]] || [[ -z " ${ACTION} " ]] ; then echo " ERROR: required arguments are missing " 1 >& 2 usage_exit fi case " ${ENV} " in dev-onprem|dev|stg|prd ) ;; * ) echo " ERROR: -e ${ENV} is invalid " 1 >&2 usage_exit ;; esac if [[ -z " ${GATE} " ]] && [[ -z " ${ROLLBACK} " ]] ; then echo " ERROR: Either -g or -r option is required. " 1 >& 2 usage_exit fi if [[ -n " ${GATE} " ]] && [[ -n " ${ROLLBACK} " ]] ; then echo " ERROR: Both -g and -r options are not able to pass at the same time. " 1 >& 2 usage_exit fi if [[ -n " ${GATE} " ]] ; then if [ " ${ACTION} " = "close" ]; then echo " ERROR: Request /gate/close is not expected in this script. " 1 >& 2 usage_exit fi RESOURCE = " gate " else RESOURCE = " rollback " fi # run bastion pod and curl echo "" echo " You're requesting to the loadtester pod by / ${RESOURCE} / ${ACTION} " . echo "" . " ${SCRIPTS_DIR} /config/k8s.sh " iam_username = " $( aws sts get-caller-identity --query Arn --output text | awk -F " . " ' {print $NF} ' ) " aws eks --region " ${REGION} " update-kubeconfig --name " ${CLUSTER_NAME} " exec kubectl run " tmp-manual-gating- $( date " +%Y%m%d-%H%M%S " ) - ${iam_username} " --image = alpine:latest -n " ${OPS_NAMESPACE} " --rm -it --restart = Never -- \ /bin/sh -c " apk add --no-cache curl > /dev/null 2>&1 ; \ curl -v -m 10 -d '{ \" name \" : \" ${CANARY_NAME} \" , \" namespace \" : \" ${CANARY_NAMESPACE} \" }' http://flagger-loadtester.istio-system:80/ ${RESOURCE} / ${ACTION} " 一見するとコード量が多く見えますが、実際には最後のawsコマンドのみが重要です。Podを起動してcurlをインストールし、loadtesterのエンドポイントを引数の情報に応じて実行します。他の処理は、usageの説明であったり、引数を取得してvalidationしているだけです。 使い方は以下の通りです。 # /gate/open ./scripts/flagger_loadtester.sh -e prd -o zozo-api-gateway-ops -c api-gateway -n api-gateway -g -a open # /rollback/open ./scripts/flagger_loadtester.sh -e prd -o zozo-api-gateway-ops -c api-gateway -n api-gateway -r -a open # /rollback/close ./scripts/flagger_loadtester.sh -e prd -o zozo-api-gateway-ops -c api-gateway -n api-gateway -r -a close 自動ロールバックの発生を防ぐ Manual Gatingによる手動カナリアリリース期間中に意図せず、自動ロールバックされてしまう事象が発生しましたので、その対応です。 原因 ロールバックされた原因は、Datadogへの通信が一定数(Canaryリソースの spec.analysis.threshold の値)以上エラーになったことでした。弊社の場合、analysisのメトリクス取得で、MetricTemplateリソースにより1分の間隔( spec.analysis.interval )でDatadogのクエリを叩いています。この通信はEKSクラスターとDatadog間のインターネット経由のため、数日間にわたるカナリアリリース期間中に通信エラーが複数回発生することは想像に難くありません。実際に、今回のケースでは約1週間の間に3回発生しました。 なお、エラー発生時のloadtesterのログは以下のようになります。 {"stream":"stderr","logtag":"F","message":"{\"level\":\"error\",\"ts\":\"2024-07-03T18:25:59.976Z\",\"caller\":\"controller/events.go:39\",\"msg\":\"Metric query failed for error-count: request failed:(省略) 対応案 案1 Manual Gatingを実施する際は、Canaryリソースの spec.analysis.threshold を極端に大きな値に設定する案です。極端に大きな値とは、 spec.analysis.interval が1分の場合かつ、10日間の期間でリリースする場合には、14400です。内訳は、 60m * 24h * 10days = 14400 です。 問題発生時はこの方法を最初に思いつき、暫定対応として採用しました。その後、恒久対応として採用されました。 なお、この案に限りませんが、Flaggerによるanalysisとロールバックは期待できなくなります。つまり、Manual Gatingによる手動カナリアリリース期間中には、Datadogのメトリクスを人間が確認し、適切なアラートを設定することが必要になります。ただし、これはFlagger導入前の手動カナリアリリースの時と同じ運用ですので、Flaggerを導入しても手動カナリアリリースをするのであれば、受け入れられるものかと思います。 案2 案1と同じような方法として、Canaryリソースの spec.analysis.metrics.thresholdRange.max を極端に大きな値にするという案もあります。しかし、あえて案1から変更するメリットはないため、選択しませんでした。 案3 Canaryリソースの spec.analysis.metrics を設定しない案です。この方法でも問題ありません。Canaryリソースの設定もシンプルになります。しかし、同じく、あえて案1から変更するメリットはないため、選択しませんでした。 NGな方法 意外かもしれませんが、Canaryリソースの spec.skipAnalysis をtrueにする方法はNGです。なぜならば、そもそもManual Gatingにならず、リリースを開始すると自動で100%まで進行してしまうからです。 まとめ 本記事では、Flaggerを導入したマイクロサービスにつきましても、手動カナリアリリースができることを紹介しました。方法は、CanaryリソースのWebhooksの設定とloadtesterのgateエンドポイントを利用することです。さらに、運用面における工夫も詳細に紹介しました。 普段はFlaggerによる自動カナリアリリースを利用し、必要に応じて手動カナリアリリースすることで、より安全かつ柔軟にリリースを進めることができます。もし、Flaggerの導入を検討している、運用で困っている、といった場合は参考にしていただけますと幸いです。 We are hiring ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
はじめに こんにちは、 ZOZOMO店舗在庫取り置き というサービスの開発を担当している、ZOZOMO部OMOブロックの木目沢です。 皆様のチームでは定期的に振り返りをしていますか? 弊ブロックでは ZOZOMO店舗在庫取り置き サービスをスクラムで開発しています。スプリント期間は1週間で、スプリントの終わりには毎週振り返り(スクラムの用語では「スプリントレトロスペクティブ」)をしています。 今回はなぜ振り返りが欠かせないか、毎週振り返りを行ってきた成果や数々のプラクティスやワークと共に紹介します。 目次 はじめに 目次 なぜスクラムでは振り返りが必要なのか? 振り返りが続かない・活かされない理由 チームとしての振り返りになっていない チームとしての場ができていない 振り返りのプラクティスやチームのワークを紹介 KPT as ART チームコンピテンシーマトリックス デリゲーションポーカー おわりに なぜスクラムでは振り返りが必要なのか? スクラムは複雑な問題に適応するためのフレームワークでアジャイルにおける開発プロセスの1つです。 スクラムには振り返り(スプリントレトロスペクティブ)がプロセスの一部として組み込まれています。現時点で最新である 2020年のスクラムガイド には以下のように振り返り(スプリントレトロスペクティブ)が説明されています。 スプリントレトロスペクティブの⽬的は、品質と効果を⾼める⽅法を計画することである。 スクラムチームは、個⼈、相互作⽤、プロセス、ツール、完成の定義に関して、今回のスプリントがどのように進んだかを検査する。多くの場合、検査する要素は作業領域によって異なる。スクラムチームを迷わせた仮説があれば特定し、その真因を探求する。スクラムチームは、スプリント中に何がうまくいったか、どのような問題が発⽣したか、そしてそれらの問題がどのように解決されたか(または解決されなかったか)について話し合う。 (中略) スプリントレトロスペクティブをもってスプリントは終了する。 スプリントの終わりに必ず振り返りを行い、うまくいったこと、いかなかったことをチームで話し合います。 スプリントの終わりに振り返りを行う理由はメンバーの認知負荷の問題によるものです。複雑な状況に適応しているということは、もともとそれだけメンバーの認知負荷が高い状況にあります。振り返りの間隔が長くなるとチームのメンバーの認知負荷はさらに高まり、有効な振り返りがしづらい状況になります。1か月前のできごと、半年前のできごとを覚えているでしょうか。起きたことを記録していたとしてもそれらを掘り起こして思い出すのも大変困難です。このような状況で、開発をもっとうまくやろう、プロダクトをもっとうまくやろうと振り返ることは難しいのです。 一方で、ウォーターフォールのような予測計画型のプロセスでは、フェーズごとに異なる作業をするため、短期間での振り返りが効果的に活用されないことがあります。とはいえ、プロジェクトを成功させるため、次のプロジェクトで活用するためにも、適切なタイミングでの振り返りが重要です。 振り返りが続かない・活かされない理由 振り返りを実施していたがうまくいかずにやめてしまった、または頻度を減らしてしまったというようなチームも多いと思います。ここではうまくいかない振り返りのやり方とその理由・対処法をいくつか紹介したいと思います。 チームとしての振り返りになっていない 振り返りを行う際に、個々のメンバーの感想が中心になってしまうことがあります。しかし、これではチーム全体の成長や学びに繋がりにくくなります。個人の振り返りも大切ですが、組織として目指すべきは、チーム全体としての学習と成長です。 なぜチームなのか? ということを理解することが重要です。 ソフトウェアを作ることが容易になった現代において、ソフトウェアの適用分野が広がり、ソフトウェアに求める要求が複雑化してきたという背景が影響しています。さらに、ソフトウェアを作ることは容易になりましたが、実装技術やアーキテクチャは非常に複雑化しているということです。iOSやAndroidといったスマホアプリ・AWSなどのクラウド・マイクロサービス、CQRS・CI/CD・DevOpsなどそれぞれに専門家が必要なくらい技術領域が広がっています。 ビジネス的・技術的に複雑化している今の時代は一人で大きなことを成し遂げるのが困難な時代と言えます。 そんななかでチームにおける個々人はいわゆる「 群盲象を評す 」状態です。各メンバーは同じ事象を経験しているにもかかわらず人により見ている観点が違うため、それを主張・共有しあってはじめて今起きていることを把握できるのです。 振り返りも個人の振り返りだけではなく、それぞれがチームを評してこそチームの現状が見えてくるというものです。 チームとしての場ができていない 場とはなにか? 先ほどの「群盲象を評す」のお話を例に説明します。 この話には数人の暗闇の中の男達が登場する。男達は、それぞれゾウの鼻や牙など別々の一部分だけを触り、その感想について語り合う。しかし触った部位により感想が異なり、それぞれが自分が正しいと主張して対立が深まる。しかし何らかの理由でそれが同じ物の別の部分であると気づき、対立が解消する、というもの。( Wikipedia より要約) まず 「主張して対立」 する。その後に 「何かきっかけ」 があって別の部分だと気づいて対立が解消するのです。 つまりチームがまず「主張し合える」環境にないといけないことがわかります。 この点をダニエル・キムは Organizing for Learning という書籍で「組織の成功エンジン」という図を用いて説明しています。 (出典:ダニエル・キム著 Organizing for Learning : Strategies for Knowledge Creation and Enduring Change ) うまくいっていない組織ではしばしば、経営陣と現場、部署間、上司・部下、あるいはチーム内で犯人探しをしたり、必死に自己正当化したりすることがあります。つまり、「関係性の質」が低い状態です。関係性の質が低いと共有や共働が起こらず「思考の質」が狭まります。そうすると、無秩序でバラバラの行動になり「行動の質」が下がる。最終的に結果が出ず、失敗からの学びもないため「結果の質」が下がります。そして結果が出ないため、さらに「関係の質」が下がるという負のループが起こります。 この負のループを打破するために必要なのが 「場の質」 です。チームメンバーが話し合う場を高め、関係性の質を改善すると負のループだった組織の成功エンジンは正のループに変わっていきます。 よく 「心理的安全性」 と言ったりしますが、心理的安全性がチームに必要な理由はこの点になります。 もちろん犯人探しをしたり、自己正当化したりといった上記の例は極端な例ではあります。程度の差はありますが、以下のような状況も関係性の質が下がっている状態と言えるでしょう。 雑談が少ない 仕様に関する会話はするが、アーキテクチャに関する議論やプロセスに関する議論がしづらい チームを超えた提案や事業に関する提案・議論がしづらい 関係性の質が現時点でどの程度高いのか、低いのかをチェックしてみるとよいでしょう。 場の質を高めるためには、ワークショップが適しています。ワークショップでは全員が参加し、手を動かすことが前提となるため必ずメンバー全員の話を聞くことになります。そのため、お互いの主張を言い合える良い場になりやすいです。 たとえば、スキルマップをみんなで作るとか、 ドラッガー風エクササイズ でお互いの期待を共有するとか、 パーソナルマップ でお互いを知るなど色々なワークがあるので参考にしてみてください。ZOZOMO部で行ったスキルマップや自分が作成したパーソナルマップを画像で紹介します。 ZOZOMO部で行ったスキルマップ パーソナルマップ。作って共有するのではなく、見せてチームメンバーに質問してもらうのが肝です。 場を作るのも、ワークショップをするのも、振り返りも準備を含め時間をわざわざ取る必要があります。設計や実装する時間以外にそこそこの時間が必要なわけですが、組織の成功エンジンは ループ になっているので、関係性の質が上がれば上がるほど結果の質がさらに上がる構造になっています。つまり場の質、関係性の質を改善することに対する時間の投資は簡単にペイできるということです。 場の質・関係性の質を改善し、チームの振り返りが「群盲象を評す」でいうところの「何かきっかけ」になると良いチームになっていくと思います。 振り返りのプラクティスやチームのワークを紹介 最後に、ZOZOMO部で行った振り返りのプラクティスやワークをいくつか紹介します。 ZOZOMO部では普段KPT(Keep/Probrem/Tryを洗い出す)を使って振り返りをしています。 毎回同じ手法で振り返りを行うのもループが回ってよいですが、やはり飽きてしまっていつの間にか同じ意見しか出なかったり、個人の感想しか出なくなったりします。刺激を入れるためにときには別の振り返りをしてみるというのも継続するコツでもあります。 KPT as ART 実施したのが年末だったので1年を振り返ってみるというのも兼ねてとにかくアウトプットしまくろうというものでした。枠を埋めるまでKeep/Probrem/Tryを書きまくるというプラクティスです。最後には、来年に向けて抱負となりそうなTryを選択して終了しました。これはどちらかというと場をつくるためのワークに近いですね。 チームコンピテンシーマトリックス こちらは「自分たちのチームに必要な能力」は何かというのを可視化して、チームとしてそれらの能力をどのくらい有しているかを確認し合うワークです。将来的に身につけたいというのも確認し、将来的なチームの成長も見越した計画を立てるために行いました。 デリゲーションポーカー たとえば上司や部下といった上下関係間、PO・スクラムマスタ・開発チームといった役割間においてやり取りが必要なケースを洗い出します。各やり取りの場面において「指示する」ことが必要なのか、「説得」することが必要なのか、「相談」すればよいのかなど、必要なコミュニケーションの程度を確認し合うワークです。それぞれの場面に対して、どのコミュニケーションが必要なのか一斉に投票してもらうと意外に合わないことが多いです。ここで認識合わせをすることでよりワーク後、円滑なコミュニケーションが期待できます。 おわりに 今回はチームになぜ振り返りが必要なのか? そして、ZOZOMO部で実施した振り返りやワークを紹介してきました。ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com 最後までご覧いただきありがとうございました!
はじめに こんにちは、WEARフロントエンド部Webブロックの 新 です。普段は WEAR のWebサイトのリプレイス開発を担当しています。リプレイスを進める中で、不具合やリプレイス前後での変化にいち早く気づくため、Lighthouse CIによる日々の記録を可視化し定期的に通知する仕組みを作りました。本記事ではその取り組みについて詳しくご紹介します。 目次 はじめに 目次 背景・課題 Lighthouse CI とは やったこと 1. GitHub Actionsでページ毎にLighthouse CIを実行し、結果をGoogleスプレッドシートに保存 2. GoogleスプレッドシートをデータソースとしてLooker Studioでグラフ化 3. Looker StudioからGoogle Apps Scriptを使用するアカウントへメール配信 4. Google Apps ScriptでLooker Studioからの最新メールを取得し、添付されている画像をSlackへ転送 おわりに 背景・課題 WEARのWebフロントエンドチームでは、10年来のVBScriptの環境からNext.jsへのリプレイスを進めています。WEARのWebサイトはオーガニック検索からの流入で閲覧してくださるユーザーがとても多いため、リプレイスを行う動機として開発生産性の向上の他にSEOの改善もありました。詳細は下記の記事をご覧ください。 techblog.zozo.com SEO改善のため、Lighthouseによる計測を継続的に行うべきと判断し、Lighthouse CIによるスコアの蓄積及び比較をすることにしました。また、リプレイス後環境のリグレッションに気づくためにも、日々のLighthouseスコアを可視化するべきだと考えました。 Lighthouse CI とは まず、 Lighthouse はGoogleが提供するWebページの品質を測定するツールで、 Lighthouse CI はそのLighthouseによる測定結果を継続的に取得するための支援ツールです。 Lighthouse CIには、Lighthouse CI CLIとLighthouse CI Serverというパッケージがあります。 Lighthouse CI Server はLighthouse CI CLIの実行結果を、蓄積、可視化まで一括して行うことができるパッケージです。しかし、今回はLighthouse CI Serverを用いずLooker Studioによる可視化を採用しました。Lighthouse CI Serverはセルフホスティングを前提としており、今回の用途ではリグレッションに気づくことができれば十分なのでコスト的な面で採用しませんでした。 やったこと 課題を解決するため「定期的に、Lighthouseによる解析・スコアの収集・グラフ化・Slackへ通知する」という仕組みを作りました。その仕組みの流れを図で表したものがこちらです。 GitHub Actionsでページ毎にLighthouse CIを実行し、結果をGoogleスプレッドシートに保存 GoogleスプレッドシートをデータソースとしてLooker Studioでグラフ化 Looker StudioからGoogle Apps Scriptを使用するアカウントへメール配信 Google Apps ScriptでLooker Studioからの最新メールを取得し、添付されている画像をSlackへ転送 1. GitHub Actionsでページ毎にLighthouse CIを実行し、結果をGoogleスプレッドシートに保存 まず、GitHub ActionsでLighthouse CIを実行すると、各ページの計測結果は .lighthouseci ディレクトリにJSON形式で格納されます。次に、格納されたJSONを読み込みGoogleスプレッドシートへ保存するスクリプトを実行します。 .lighthouseci ディレクトリに格納された計測結果は以下のようなコードで取得できます。 import { readdir, readFile } from "fs/promises" ; import { join } from "path" ; /** * 指定ディレクトリ内のlhr jsonファイルのパス一覧を取得 */ const listLhrJson = ( dir : string ): Promise < string []> => readdir(dir, { withFileTypes : true } ). then (( entries ) => entries . filter (( e ) => e.isFile() && e. name . match ( /^lhr-\w+\.json$/ )) . map (( e ) => join(dir, e. name )) ); const jsonPaths = await listLhrJson( ".lighthouseciディレクトリのパス" ); const resultTexts = await Promise . all ( jsonPaths. map (( p ) => readFile(p, "utf8" )) ); const results = resultTexts. map (( resultText ) => JSON . parse (resultText)); 上の results は以下のようなページごとの配列になっています。 [ { lighthouseVersion : "12.1.0" , requestedUrl : "https://wear.jp/" , // ... categories : { performance : { // ... score : 0.82 , } , accessibility : { // ... score : 0.75 , } , "best-practices" : { // ... score : 0.78 , } , seo : { // ... score : 0.85 , } , } , // ... } , // ... ] ; 上記の取得した結果を次のように加工します。 [ [ "https://wear.jp/" , { performance : 51 , accessibility : 73 , seo : 83 , "best-practices" : 59 , } , ] , // ... ] ; 加工した結果を次の関数に渡し、Googleスプレッドシートに保存します。 import { AuthClient } from "google-auth-library" ; import { GoogleSpreadsheet } from "google-spreadsheet" ; const storeToSpreadsheet = async ( sheetId : string , auth : AuthClient , urlAvgMap : [ string , Record < string , number > ] [] ) => { const doc = new GoogleSpreadsheet(sheetId, auth); const date = new Date (). toISOString (); await doc.loadInfo(); const firstSheet = doc.sheetsByIndex[ 0 ]; await firstSheet.loadHeaderRow(); const rows = urlAvgMap. map (( [url , avgs] ) => ( { url , date , ...avgs, } )); await firstSheet.addRows(rows); } ; 2. GoogleスプレッドシートをデータソースとしてLooker Studioでグラフ化 Lighthouseのスコアを保存したGoogleスプレッドシートをデータソースとして、Looker Studioでグラフ化していきます。Looker Studioでのデータの連携やレポートの詳しい作成手順は下記記事をご覧ください。 techblog.zozo.com 実際に蓄積したデータをLooker Studioでグラフ化したものの一部がこちらです。下のメンバー詳細はちょうどリプレイスを行ったばかりのページなため、SEOやパフォーマンスなどが改善されているのがわかると思います。 パフォーマンスなどのスコアにバラつきがあるのは、GitHub Actionsが実行される環境の違いです。GitHubホステッドランナーを利用しているため、割り当てられるインスタンスのスペックも様々でスコアにバラつきが出てしまっています。私たちの用途では、Lighthouse CIから計測したグラフを異常な変化の検知に用いているため、多少のバラつきは大きな問題ではありません。 3. Looker StudioからGoogle Apps Scriptを使用するアカウントへメール配信 Looker Studioでグラフ化した結果の画像をSlackへ転送するため、メール配信の設定をします。Looker Studioから画像を取得するためのAPIは無くメール配信のみのため、メール経由でSlackに転送します。そのためにLooker Studioから通知したいページや通知頻度を設定します。 4. Google Apps ScriptでLooker Studioからの最新メールを取得し、添付されている画像をSlackへ転送 Looker Studioから配信されたメールの画像を抜き取り、Slackへ転送します。Google Apps Scriptで書いた次のコードは、Looker Studioから配信されたメールの画像データを抜き取る関数の例です。メールは設定したタイトルをもとに取得します。 function getPhotosData () { const reportTitle = "Lighthouse 週次レポート" ; const gmailThread = GmailApp . search ( `from:looker-studio-noreply@google.com subject: ${ reportTitle } ` ) ; const messages = gmailThread [ 0 ] . getMessages () ; const attachments = messages [ 0 ] . getAttachments ({ includeInlineImages : true , includeAttachments : false , }) ; return attachments . map (( attachment ) => { return { blob : attachment . getAs ( attachment . getContentType ()) , name : attachment . getName () , contentType : attachment . getContentType () , } ; }) ; } 次に、取得した画像データをSlackに転送するコードの例です。 getFileUploadUrl で画像データからアップロード用のURLを取得し、取得したURLにPOSTリクエストを送信してファイルをアップロードします。 completeFileUpload に getFileUploadUrl から受け取ったファイルIDを渡してアップロード処理を完了させます。 // ファイルアップロード用URLを取得する function getFileUploadUrl ( filename , length ) { const options = { method : "get" , headers : { Authorization : "Bearer " + PropertiesService . getScriptProperties () . getProperty ( "SLACK_TOKEN" ) , } , payload : { filename : filename , length : length . toString () , } , } ; const response = UrlFetchApp . fetch ( "https://slack.com/api/files.getUploadURLExternal" , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; if ( ! data . ok ) { throw new Error ( data ) ; } return [ data . upload_url , data . file_id ] ; } // ファイルアップロード処理を完了する function completeFileUpload ( fileId ) { const options = { method : "post" , headers : { Authorization : "Bearer " + PropertiesService . getScriptProperties () . getProperty ( "SLACK_TOKEN" ) , } , contentType : "application/json" , payload : JSON . stringify ({ files : [{ id : fileId }] , channel_id : CHANNEL_ID , initial_comment : `< ${ LookerStudioReportURL } | ${ WEAR Lighthouseレポート } >` }) , } ; const response = UrlFetchApp . fetch ( "https://slack.com/api/files.completeUploadExternal" , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; if ( ! data . ok ) { throw new Error ( data ) ; } } // ファイルをアップロードする function uploadFile ( photoData ) { const [ fileUploadUrl , fileId ] = getFileUploadUrl ( photoData . name , photoData . blob . getBytes () . length ) ; const options = { method : "post" , payload : photoData . blob , } ; const response = UrlFetchApp . fetch ( fileUploadUrl , options ) ; completeFileUpload ( fileId ) ; } function main () { const photos = getPhotosData () ; photos . forEach (( photo ) => { uploadFile ( photo ) ; }) ; } 最後に、上記のコードを実行させるため、Google Apps Scriptで main 関数の実行タイミングを設定します。実行タイミングはLooker Studioのメール配信で設定した時刻より後に設定する必要があります。 実行されると以下のようにSlackの指定したチャンネルで通知されるようになります。 おわりに 本記事では「定期的に、Lighthouseによる解析・スコアの収集・グラフ化・Slackへ通知する」仕組みについて紹介しました。この仕組みを導入にしたことによって、リプレイス後環境のリグレッションにいち早く気づくことができるようになりました。現在はGitHub Actions上でLighthouse CIを実行していますが、AWS EC2のスポットインスタンスを利用するなど、コストの削減やスコア変動の改善にも取り組みたいです。日々のLighthouseのスコアの定点観測を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは! WEARバックエンド部バックエンドブロックの高久です。普段は弊社サービスであるWEARのバックエンド開発・保守を担当しています。 10周年を迎えた WEAR は2024年5月9日に大規模な アプリリニューアル を行いました。アプリリニューアルに伴い負荷試験を行ったので、本記事ではどのように負荷試験を計画したか事例をご紹介します。 記事は計画編と実施編の2部構成で、本記事は前編の計画編となります。後編の実施編は近日、公開予定です。 目次 はじめに 目次 背景 計画の重要性 計画の策定 目的の整理 目標値の設定 スループット レイテンシ 試験方針 試験環境・データ 対象範囲 取得情報・確認観点の整理 負荷シナリオ 実施方法 リスク リリースしてみたらユーザ数が予想を超えてしまい性能問題が発生する 試験対象外とした箇所に性能問題が発生する 本番環境での試験時に性能問題が発生しユーザ影響が出る まとめ 背景 前述の通りWEARは2024年5月9日に大規模なアプリリニューアルを実施しました。このリニューアルでは、アプリのほぼすべての画面が刷新され、新たにAIを活用したファッションジャンル診断や、ユーザの閲覧履歴をもとにしたレコメンド機能、ARによるメイク試着機能などが追加されました。リリース後には、会員登録時にポイントがもらえるキャンペーンやWeb広告の出稿により、新規ユーザのさらなる増加が見込まれていました。 このような複数の機能追加・改修やユーザ数の増加に伴い懸念されるのが性能問題です。例えば、新たに作成した機能のクエリに性能問題があってアプリの動作が遅くなったり、ユーザ数の増加によってサーバリソースが不足し、最悪の場合にはシステムダウンしたりする可能性もあります。リリース後にこうした性能問題が発生しないよう、事前に負荷試験を行い、性能品質を担保する必要がありました。 計画の重要性 一般的に負荷試験は本番運用に先立って性能問題が発生しないことを事前に保証することを目的としています。そのため理想的な負荷試験は、本番環境と全く同じ条件、すなわち負荷、データ量、シナリオ、環境などを本番と同様に再現し、性能に問題がないことを確認することです。しかし現実的にそれを実現することはほぼ不可能だと考えています。リリース後にどの機能をどれくらいの人数が利用するかを完璧に予測することは難しかったり、全てのリクエストパターンを網羅するには膨大なシナリオ作成が必要で、多大な工数がかかったりするためです。 理想にできるだけ近づけることは重要ですが、未来の予測は困難で、工数を無限にかけるのも現実的ではありません。そのため現状の環境、人的リソース、期間を考慮し、いかに効率的に性能品質を保証できるかを事前に計画しておくことが重要です。理想に達しなかった部分については、リスクとしてどのように対処するかを計画段階で検討することも必要です。 計画の策定 どのように計画したか、以下のポイントに沿ってご紹介していきます。 目的の整理 目標値の設定 試験方針 試験環境・データ 対象範囲 取得情報・確認観点の整理 負荷シナリオ 実施方法 リスク 目的の整理 まず負荷試験の目的を整理しました。WEARでは以下の点を負荷試験の目的としました。 性能要件の担保 リリース後に予想されるピーク負荷において、目標とするスループットやレイテンシが達成されているかを確認します。 目標に達しない場合は、達成できるように改善します。 他にも負荷試験ではシステムの限界点の測定やボトルネックの特定、サーバスペックや台数の適正化(サイジング)を目的とすることもあります。しかし今回は負荷試験に割ける期間が限られていたため「性能要件の担保」のみに焦点を当てました。 目標値の設定 目標値(≒性能要件)として「スループット」と「レイテンシ」の2つを定めました。 スループット 現行のピーク負荷量の1.5倍を目標としました。 リリース後1年の中で最も負荷がかかると予想されたのは、リリースから3か月後に予定されているキャンペーン期間で、このタイミングで予想されるユーザー数が現行ピーク負荷の1.3倍でした。この値に安全率を考慮して1.5倍としました。 負荷量の増加要因としてはキャンペーンやプロモーションによる一時的な急増と、サービスの成長に伴うユーザー数の継続的な増加が考えられました。これらを踏まえプロダクトオーナーにヒアリングしながら、最も負荷が大きくなるタイミングを決定しました。 レイテンシ APIの処理時間について、一次目標を500ms以下、二次目標を1秒以下と設定しました。 測定対象としては、本来クライアント側の処理時間も含めたエンドツーエンドで計測するのが理想です。しかしクライアントのレイテンシ計測方法が確立しておらず、計測には多大な工数がかかると判断したため、今回はAPIの処理時間のみを測定対象としました。クライアント側の処理時間に関しては特別な試験は行わず、関係者(開発者、PM、QA、デザイナー)がアプリを使用する中で「遅い」と感じた箇所など体感ベースで改善する方針としました。 秒数の目標値については、元々WEARで設定されていたSLO(サービスレベル目標)を基に定めました。SLOについては以前 テックブログ で紹介していますので、ご興味があればご覧ください。 基本的には一次目標である500msを目指しました。もし500msを超えてしまった場合は、エンドポイントの重要度(リクエスト数など)や処理内容(重い処理かなど)に応じて、二次目標を基準に測定を続けるか、500ms以下に改善するかを都度判断しました。 試験方針 初めにAPI単体の性能試験を実施し、その後システム全体でリクエストされる想定のAPIに対して一斉に負荷をかけて本番運用を模擬した負荷試験を実施しました。 API単体の性能試験では、単発でAPIを実行しレイテンシが目標値を下回っているかを確認します。もし目標に達していなければチューニングを行います。このAPI単体の性能試験を先に行う理由は、試験の効率化のためです。いきなり負荷試験を実施しても良いのですが、仮に目標未達のAPIがあった場合その都度、負荷試験を実施してチューニングと再測定を繰り返すと調整や工数が増加します。また全てのAPIを負荷シナリオに含めることは工数的に難しい場合が多いため、リクエスト頻度が低いなどの優先度が低いAPIについては、負荷試験を行わずAPI単体の性能試験のみで最低限の品質を担保します。 試験種別 確認観点 担保できる品質 API単体の性能試験 単発でリクエストを送った際に、レイテンシが目標を満たしているか確認 ・遅い処理がないこと(例: N+1、無駄なループ、非効率なクエリ・実行計画、外部システム) 負荷試験 複数のAPIに対して複合的な負荷をかけた際に、レイテンシやスループットが目標を満たしているか確認 ・サーバリソースが十分であること(例: CPU、メモリ、ディスク) ・DBロックが頻発しないこと ・コネクション数(例: DBのコネクションプールなど)が十分であること など 試験環境・データ 試験環境は本番運用中の本番環境、データも本番環境のものを使用しました。 今回APIを提供するサーバはリニューアルせず、既存のサーバにAPIを追加する形でした。そのため本番環境で負荷試験を行うと、現行のWEARユーザに影響を与えるリスクがありました。 WEARにはステージング環境と本番環境の2種類があります。ユーザへの影響を考慮すると、できればステージング環境で試験を実施したかったのですが、以下の理由により本番環境での実施を決定しました。 DBスペックの違い ステージング環境のDBスペックは本番環境よりも低い状態でした。このため、ステージング環境で負荷試験を行った場合、レイテンシが悪化し性能問題が発生する可能性が高くなります。仮に性能問題が発生した場合、その原因がシステム本体の問題なのか、環境要因によるものなのかを切り分ける必要が生じ、追加の工数と時間がかかることが予想されました。 データの量と質の違い ステージング環境では、時間的制約から本番環境と同等のデータ量や質を揃えることが難しい状況でした。特にSQLの実行計画や処理時間はデータの量と質に大きく依存しており、その違いはパフォーマンス測定の正確性に直接影響を与えます。例えばデータボリュームの少ない環境で負荷試験を行うと、データ取得にかかる時間が早くなって本番環境よりもいい結果になってしまうこともあるなど、本番稼働時に予期しない遅延や問題のリスクがあります。 試験の正確性とリスクを天秤にかけ、今回は本番環境で試験を実施することにいたしました。 対象範囲 試験の対象範囲を事前に以下のように決定しました。 観点 試験対象内(例) 試験対象外(例) インフラ APIサーバ、DB、外部システムの一部、Elasticsearch クライアント、外部システムの一部、Push通知基盤、メール送信基盤 API(単体の性能試験) 新規API、既存API(改修あり)、新規実装したバッチ 既存API・バッチ(改修なし)  API(負荷試験) 想定リクエスト量がNrps以上のAPI 想定リクエスト量がNrps以下のAPI、シナリオ作成が難しいAPI  試験範囲を広げれば、その分負荷試験にかかる工数も増加します。今回の負荷試験では限られた時間の中で実施する必要があったため、リニューアルによって懸念がある箇所や負荷量増加が予測される部分を優先的に試験対象として設定しました。 取得情報・確認観点の整理 負荷試験中の問題調査や未然の問題検知を目的として、事前にSREチームと共に取得するメトリクスを整理しました。要件であるスループットやレイテンシに加え、サーバリソース(CPU、メモリなど)、エラー、DBロック状況といった、特に負荷がかかった際に問題が発生しやすいポイントを中心に取得しました。 一部のメトリクスには達成基準を設け、負荷試験中にその基準を満たしているかを確認しました。 例: 観点 取得方法 達成基準 CPU使用率 datadog 70%以下であること メモリ datadog 現行と同等程度であること 試験実施時には、Datadog上にすべてのメトリクスをひとまとめに表示できるダッシュボードを作成し、リアルタイムで監視と結果取得をしました。 負荷シナリオ リリースから3か月後のピーク時に予想されるAPIとその負荷量について検討しました。計画段階では、以下のようにAPIと負荷量の一覧を整理しました。また、今回は運用中の本番環境を利用するため、算出したピーク負荷量をそのままかけてしまうと既存負荷分が余分にかかってしまう状況でした。そのため既存負荷分を差し引いた負荷をかけられるように既存負荷量も時間帯ごとに整理しておきました。 例: API 想定ピーク負荷量(rps) 7~19時の現在負荷量 21時の現在負荷量 4時の現在負荷量 コーデ詳細取得API 100 60 80 30 ユーザ情報取得API 50 20 40 10 コーデ投稿API 10 5 6 3 今回、負荷量予測が難しかったです。これまでの機能単体リリースであれば現行システムの負荷量を基におおよその予測はできました。しかし今回は画面や機能の大幅に変更によって、リニューアル前後で負荷量の傾向は大きく変わる可能性があり、予測は難しい状況でした。 以下に負荷量予測の算出方法を簡単に説明します。 現行アクセスログの分析 : 現行システムの直近1年間のピーク秒を特定し、そのピーク秒を含む5秒間のAPIごとのアクセス数を取得します。このデータを基にリニューアル後の負荷量を予測します。 リニューアル後の予測 : 新しい画面や機能のアクセス傾向を予測します。例えば「新A画面は旧B画面に似ているので、B画面のリクエスト数と同程度だろう」や「この機能は初回の起動以外ほとんど使われないだろうから、初回起動のリクエスト数と同等だろう」といった形で予測を立てます。予測の精度を高めるには時間がかかるため、重要な画面に関しては詳細に、その他の画面は感覚で決めました。 API一覧の整理 : 各画面や機能ごとに呼び出されるAPIの一覧を整理します。HTTP通信トレースツール(Charles)を使用し、画面ごとに呼び出されるAPIを抽出しました。 負荷量の算出 : 1、2、3の情報を基に、各APIの負荷量を算出します。 試験対象の絞り込み : 効率化のためにリクエスト量が0.7rps以下のAPIは試験対象から外しました。また外したAPI分の負荷量を、試験対象APIの負荷量に追加しました。 この方法で負荷シナリオを整理しました。負荷掛けツールでの実行ファイルへの落とし込み方法については、実施編で説明する予定です。 実施方法 本番環境のサーバに対して、k6というツールを使用して負荷をかけて測定しました。詳細については、実施編でお伝えする予定です。 リスク 事前に負荷試験を実施する際とリリース後に起こり得るリスクを洗い出し、それぞれに対する対処案を整理しました。以下のようなリスクが考えられました。 リリースしてみたらユーザ数が予想を超えてしまい性能問題が発生する 対策 想定ピーク負荷量に安全率を持たせ、予想を少し超えても問題がないことを確認しておく。 ユーザ数の増加が見込まれるキャンペーンやテレビでのサービス紹介などのイベント時には、監視体制を整え、問題発生時に迅速に対応できるようにする。 リリース後は負荷量を定期的にモニタリングし、傾向や問題の有無をチェックして未然に問題を防ぐ。 システムの限界時にボトルネックとなりそうな箇所を事前に推測し対策案を検討しておくことで、万が一システム限界が訪れた場合、素早く対策できるようにする。 試験対象外とした箇所に性能問題が発生する 対策 影響が大きい利用頻度の高い箇所や、新規に開発または改修した性能問題の発生が懸念される箇所は試験対象に含めて、問題発生の影響や可能性を下げる。 リリース後定期的にモニタリングを行い、問題を発見できるようにする。 本番環境での試験時に性能問題が発生しユーザ影響が出る 対策 負荷量を段階的に増やし(10%→30%→50%→100%)、都度結果を確認し負荷を増やしても問題がないかを確認することで、問題発生の可能性を下げる。 ユーザ数が少ない夜間帯に試験を実施し、万が一問題が発生した場合の影響を最小限に抑える。 試験中はエラーを常に監視し、ユーザ影響が出た場合にすぐに負荷を中止できるように準備しておく。 最悪の場合にシステム停止の可能性もあるため、事前に関係者と情報連携の方法を共有し、問題発生時にスムーズに対応できるようにする。 まとめ WEARアプリのリニューアルにおける負荷試験の計画についてご紹介しました。負荷試験の方法は、サービスの特性や環境、利用できる期間や人的リソースによって大きく変わるため、プロダクトに最適な試験を実施するための計画が重要です。WEARでの事例が参考になれば幸いです。なお、本計画に基づいて実施した結果は、後編の「実施編」で紹介しますので公開をお待ちください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、ZOZOTOWN開発本部でZOZOTOWN iOSの開発を担当している らぷらぷ です。 2024年8月現在、ZOZOTOWN iOSチームは正社員と業務委託の方をあわせて全14人で構成されてます。 組織上、ZOZOTOWNのiOSチームは開発1部と開発2部の2チームに分かれています。しかし、リリースバージョンの計画・開発フロー・技術課題・その他チームに関わる課題意識はiOSチームメンバー全員で共有しながら改善を進めています。 そのような共有・議論がここ数ヶ月、週次イベントとして固定化してきたので、各イベントの振り返りも兼ねて皆さまにZOZOTOWNのiOSチームを支えるチーム運用の数々を紹介します。 1つのバージョンがリリースされるまで まずはこちらの図をご覧ください。 ざっくりですが開発期間を省いてQA準備からリリースまでの流れを描きました。今年から毎週金曜に定期的にQA配布する流れになりました。 週次イベントは「リリース系」と「チーム活動改善系」に分けられます。 リリース系 バージョン計画会 ぽちぽち会 QAビルド配布 App Store申請 チーム活動改善系 開発生産性MTG Rethink! Confluenceリファクタリング会 今週の振り返り スタンドアップ 毎日スタンドアップミーティングを30分設けてます。図中金曜日の「今週の振り返り」はスタンドアップミーティングの中でやってます。 ちなみにこのSlack AppはZapierで実装しました。毎週火曜日は1時間枠の定例なので、その曜日だけはConfluenceの議事録を取得してSlackに投稿するよう分岐してます。 リリース系 バージョン計画会 担当する案件を持つメンバー(以下案件オーナー)とともにリリースバージョンを整理し、スケジュール進行およびリスクを定点チェックします。 各案件オーナーは主に以下の情報を整理します。 案件情報(Confluence、Jira) 開発時期 QA時期 App Storeに掲載するリリースノートの有無 App Storeへのリリース日が固定か 固定の場合、バージョンに同梱される他案件のスケジュールも当案件に優先される リリースノート・リリース日に影響する要素の有無 スケジュール決定・進行にまつわるTODO・リスク 各リリースバージョンにはバージョンマネージャーが決まっており、バージョンマネージャーは上記の案件オーナーと協力して以下の情報を整理します。 各イベント開催日 ぽちぽち会 QAビルド配布 App Store申請 毎週金曜日のQAビルド配布という動きが決まるまでは、リリース日から逆算してバージョンをどう調整するかパズルのように組み合わせるのが大変でした。 加えて案件以外の小さなbugfixを入れるタイミングがルール化されてなかったので、「この案件に相乗りしてもいいのか、でもそれはQA負荷高いのかな?」と思考することも多かったです。 今では定期リリース枠に何が入るかを整理しているので、スケジュール立ては以前ほど考えることが少なくなっています。その結果、開発からリリースまでのスケジュール立てに対する認識が全員揃いやすく、小さな改善も定期的にリリースできるようになりました。 ぽちぽち会 リリース対象の機能・修正をQAビルド配布前にチェックする会です。 Gatherで集まってJiraの内容を確認しつつ実機・シミュレーターで“ぽちぽち”やっています。このタイミングでバグを見つけることもあり、見つけ次第修正してからQAチームに配布しています。 例えば「この動作って想定外ですか?」といった仕様確認や、「このテキストをこう変えるとお客さんに伝わりやすいかも?」「このタップエリアはもっと広くした方が使いやすいね」などのフィードバックが出てきます。 この会に力を掛け過ぎるとQAチームのチェックと重複することもあり、お互いにどう品質をカバーしあうと良いか模索中です。 QAビルド配布・申請・リリース QAチームにチェックをお願いし、問題なければApp Storeへ申請します。 このルールが言語化されて他チームに伝えやすくなってから、スケジュール立てが楽になりました。そうとはいえ、このドキュメントは他チームに伝え続けないといけないので、参照されやすさをどうConfluence上またはSlackで実現したらいいかなと考えてます。 チーム活動改善系 開発生産性MTG コードベースの技術的な課題を除くチームの課題に対して議論する場です。 技術的課題を外すという意味で「開発生産性(仮)」と付けてましたが、よい候補が見つからずこのままの名前になってます。案件オーナーとしての開発マネジメント、PRレビュー、メンバー間の情報格差など、チームがより成熟するのに必要な課題を整理してアクションを設計してます。 Rethink! 技術的な課題をチームメンバーで定期的に見直す会です。技術的負債と感じてること、Appleが提示している技術への所感や適応への方針、自分だけしか分かってないかもと不安になることなど、お題は多岐に渡ります。 Rethink!で話したことはアーカイブとしてConfluenceに残し、過去の意思決定資料として参照します。 開発生産性MTGとRethink!は「立ち止まって振り返って分析する」イベントで、ファスト&スローでいうところのスローにあたります。案件開発中の迅速な意思決定(ファスト)だけで方針の方向性を埋めないようにしてます。 Confluenceリファクタリング会 iOSチームのドキュメントはConfluenceにまとまっていますが、時が経つにつれてドキュメント(知識・運用)も風化していきます。この会は、メンバーの入れ替わりや運用の改善などチームの状況に応じてドキュメントを絶えず改善するための会です。 主に以下の内容を話しています。 最近追加したドキュメント 最近削除したドキュメント 今後欲しいドキュメント 書きたい 可能なら誰かに書いてほしい ちなみにiOSチームのConfluenceは以下のような階層リストになっています。 背景 コードを読んでも理解できないドメインの説明 設計方針 ルール 実装例 開発・デバッグ方法 運用ガイド メモ 背景・ルール以外 書き始める場所に困った時ここから書き始める アーカイブ 過去の経緯・議事録など、最新情報ではないが資料として残したいもの 今でこそ、このような階層リストになってますが、それまでは欲しい情報を辿るのに難しく整理しづらい状態でした。この構造に決めるまでの議論を開発生産性MTGで進めていました。 今週の振り返り 毎週金曜日はスタンドアップ中にチーム振り返りをしてます。うまくできたこと、思うようにいかなかったこと、来週の不安などをシェアしています。 また、業務委託の方含め全員が揃う木曜日は「今週の凄い人達を褒める」ことを習慣化しています。導入時はチームメンバー内の褒め合いになり、褒め合い慣れてない独特の空気がちょっとおもしろかったです。今では他のチームの褒めも積極的にやっていこう、というのがZOZOTOWN iOSチームのテーマになってます。 おわりに ZOZOTOWNのiOSチームの開発、雰囲気、文化を維持して改善するための運用を紹介しました。 ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co corp.zozo.com
はじめに こんにちは、ZOZOTOWN開発2部Androidブロックの小林( @kako_351 )です。普段はZOZOTOWN Androidアプリの開発を担当しています。今年の3月に入社して機能改修や既存機能の調査などの業務に携わってきました。その中でZOZOTOWN Androidアプリについて知見を持っていないため、調査や開発の際に学習コストがかかるといった課題が見えてきました。本記事ではAndroidアプリの実装を把握するアプローチをご紹介します。 目次 はじめに 目次 背景 実装を把握するアプローチの全体像 ドキュメントの把握 目的 アプローチ モジュール構成や画面遷移などの全体構造の把握 目的 モジュール構成 画面遷移 アーキテクチャの把握 目的 アプローチ ライブラリや使用技術の把握 目的 アプローチ ビルドやデプロイなどCI/CD環境の把握 目的 アプローチ テストの把握 目的 アプローチ 実際にコードを読む 目的 アプローチ コンポーネントの特定 レイアウトファイルから辿る Layout Inspectorを使う その他の方法 詳細を読み解く チームメンバーとのコミュニケーション 目的 アプローチ まとめ 背景 初めて触れるプロダクトへの知見が全くない状況で、調査や開発時に学習コストがかかっていました。そのため既存のメンバーよりもどうしても調査や開発に時間がかかっていました。ZOZOTOWN Androidアプリの規模の大きさから、調査や開発の度に実装を1から読んでいくと毎回学習コストがかかってしまうため、全体や方針を把握してこの学習コストを減らせないか考えました。 実装を把握するアプローチの全体像 基本的なアプローチとして、プロジェクトの大枠から詳細へと理解を深めていきます。 ドキュメントの把握 モジュール構成や画面遷移などの全体構造の把握 アーキテクチャの把握 ライブラリや使用技術の把握 ビルドやデプロイなどCI/CD環境の把握 テストの把握 コードを読む チームメンバーとのコミュニケーション ドキュメントの把握 目的 開発時に参考となるドキュメントが存在している場合があります。これらのドキュメントを把握することで、開発時に必要な情報を得ることができます。 アプローチ ドキュメントをキャッチアップします。ZOZOTOWN Androidチームでは以下のようなドキュメントがまとめられています。 アーキテクチャの説明 コーディング規約 ライブラリ ブランチ運用、GitHub運用 ライセンス管理 リリースフロー Android Studioの設定 開発環境 さらに、オンボーディングの一環でこれらのドキュメントがメンターから共有されます。入社したばかりのタイミングではどこに情報があるのかわからないので、このサポートは非常に嬉しいです。 モジュール構成や画面遷移などの全体構造の把握 目的 依存関係やファイルの配置場所など、どのような実装がどこに配置されているのかを把握します。また、自分が開発する際に何をどこに配置するべきかを理解します。 モジュール構成 最近のAndroidアプリは、マルチモジュール化していることが多いのでモジュール構成を把握します。 プロジェクトにモジュールの依存関係グラフを生成するGradleタスクがある場合はそれを活用できます。例えば、 projectDependencyGraph のようなタスクがあります。ZOZOTOWN AndroidアプリにもprojectDependencyGraphが存在しているので、依存関係グラフを生成してみました。 モジュール名から未公開情報が推測できるため、一部伏せています この図はおおまかな構成の予測を立てるために活用します。楕円形がモジュールを表し、矢印が依存関係を表しています。例えば、「:app」モジュールから複数の矢印が伸びていて、その先を見ると「:feature:hoge」や「:feature:fuga」というモジュールがあります。このことから、機能単位でモジュールを作成していることが読み取れます。また、複数の「:feature」モジュールから「:common」というモジュールに矢印が伸びています。これは「:common」という名前と複数のモジュールから矢印が伸びていることから、共通部品を管理しているモジュールであることが読み取れます。 このようにモジュール名と依存関係から情報が読み取れます。例えばZOZOTOWN Androidアプリでは以下のような情報が読み取れました。 featureモジュールで各機能単位をモジュール化している 共通部品はcommonモジュールで管理している ModelやRepositoryなどのデータレイヤーはdataモジュールに集約させている モジュール名から推測する役割に被りがあるのでプロダクトの成長と共にモジュール構成の見直しが行われている モジュール構成を簡略化した図としては次のようになります。 かなり簡略化しましたが、このような構成であることがわかりました。 画面遷移 画面遷移をどのように管理しているのか把握します。いくつか考えられる候補があります。 Jetpack Navigation FragmentManager Navigation Compose Fragmentベースであれば、Jetpack NavigationやFragmentManagerを用いた画面遷移が考えられます。一方、フルComposeのアプリならNavigation Composeが候補に挙がってきます。 ZOZOTOWN AndroidアプリはFragmentベースで、Navigation Graphは存在しない構成です。よってFragmentManagerで画面遷移を実現している事が分かります。 モジュール間を跨ぐ画面遷移をどのように実現しているか把握します。ZOZOTOWN AndroidアプリではEventBusを利用してappモジュールを仲介する形で画面遷移を実現しています。 例えば、featureAモジュール内のFragmentAからfeatureBモジュール内のFragmentBに遷移する場合、featureAモジュールからFragmentBは直接参照できません。ZOZOTOWN AndroidアプリではEventBusを使いappモジュールのMainActivityにコールバックして、FragmentAからFragmentBへ画面遷移しています。図にすると以下のようなイメージです。 アーキテクチャの把握 目的 保守性や変更性、開発生産性のためにアーキテクチャを把握します。 アプローチ ドキュメントが存在している場合はその内容をキャッチアップします。ただし、プロダクトで採用するアーキテクチャにも変化がありえます。アーキテクチャの候補としては以下のようなものがあります。 MVVM MVP MVI Flux Androidアプリでは Android公式のアーキテクチャガイド が存在しており、これを参考にしている場合があります。 ZOZOTOWN Androidアプリはアーキテクチャに関するドキュメントが存在しています。また、ZOZOTOWN Androidチームにはアーキテクチャについて議論するアーキテクチャ座談会という取り組みがあります。その議事録からも現在の方針を把握できます。 ZOZOTOWN Androidアプリはその歴史の長さや規模感から、画面や機能によってアーキテクチャが異なります。現在の方針は、MVVMに近い形が採用されているようでした。ただし、一部の画面はRedux 1 で実装されているなど、画面や機能によって異なるアーキテクチャが採用されていることがわかりました。 ライブラリや使用技術の把握 目的 使用しているライブラリや技術を把握することで既存実装を理解します。 アプローチ ドキュメントとGradleを見ることで把握していきます。最近であればversion catalogでライブラリ管理している場合があるので、TOMLファイルなどを参照すれば理解が早いかもしれません。 ZOZOTOWN Androidアプリでもversion catalogを利用しています。一例ですが、具体的には次のようなライブラリを把握できました。 UI系 Jetpack Compose Epoxy API通信 Retrofit2 OkHttp Gson 非同期処理 Kotlin Coroutines RxJava 2 画像系 Picasso Coil DB greenDAO Room テスト MockK JUnit Robolectric Truth その他 Dagger Hilt Firebase また、CoroutinesやComposeはBOMで管理しています。具体的には以下のような記述からBOM管理であることがわかります。 [versions] compose-bom = "2024.01.00" [libraries] compose-bom = { group = "androidx.compose" , name = "compose-bom" , version.ref = "compose-bom" } compose-ui = { module = "androidx.compose.ui:ui" } compose-foundation = { module = "androidx.compose.foundation:foundation" } # .. . ビルドやデプロイなどCI/CD環境の把握 目的 テストやリリースフロー、Lintチェック、ライセンスチェックなどCI/CDで自動化している場合があります。それらを把握し、チームの開発・運用を理解します。 アプローチ Pull Requestを確認することでジョブと実行環境が確認できます。ZOZOTOWN AndroidアプリではGitHub Actionsを採用しています。 もしくは、プロジェクトのフォルダやファイルを確認することでどのようなジョブが設定されているか確認できます。例えば、GitHub Actionsでktlintによる静的解析を自動チェックしている環境であれば以下のようなフォルダ、ファイルが存在していると思います。 .github ∟ workflows ∟ ktlint.yaml リリース版アプリのビルドからGoogle Play Consoleへのアップロードも自動化している場合があります。ZOZOTOWN AndroidアプリでもビルドからリリースまでのフローはCI/CDで自動化されていました。 テストの把握 目的 開発時に期待されているテストの内容を把握します。 アプローチ ユニットテストを書く文化があるかやどこまで書いているか、また自動化されているかを把握します。自動化は前述のCI/CD環境の把握の際に、ユニットテストを自動化していることなどを把握できます。カバレッジを収集している場合は、カバレッジレポートをどのように活用しているかなども合わせて確認します。 どのようなテストを書くべきかを理解しておくと開発時にスムーズでいいかもしれません。例えば、以下のような方針やルールがあるかもしれません。 実装の詳細ではなく振る舞いをテストする テスト名は命名規則に従う ZOZOTOWN Androidチームでは、テックリードがチーム向けにユニットテストについて説明した資料がありました。その資料や過去のPull Requestを拝見することでテストの内容を参考にできます。 それらを参考にZOZOTOWN Androidチームはテスト文化があることや、ユニットテストの自動化もされていることがわかりました。また一部のメンバー間ではTDD(テスト駆動開発)で開発を進めていることもわかりました。 実際にコードを読む 目的 アプリの機能や画面がどのように実装されているのかを理解し詳細を把握します。 アプローチ 具体的なタスクを持つとモチベーションになるので、調査や開発タスクを進める過程でコードを読んでいくと理解が進みやすいと思います。そのようなタスクがない場合、機能単位でコードを見ていくとよいと思います。 ここではZOZOTOWN Androidアプリの商品詳細画面を例に挙げ、コードを読んでいく過程を紹介します。 コスメなど一部商品の場合、商品詳細画面に「衛生商品のため、返品・交換対象外です」といったメッセージを表示する以下のようなコンポーネントが存在します。 今回の例では、このコンポーネントがどのように表示されているかを特定していきます。 この記事で紹介するコードは、実際のZOZOTOWN Androidアプリのコードとは一部異なります。 コンポーネントの特定 まずはコンポーネントの特定です。以下のような方法があります。 レイアウトファイルから辿る Layout Inspectorを使う Flipperなどのデバッガーツールを使う ZOZOTOWN AndroidアプリはFragmentManagerで画面遷移をしていることがわかっているので、Fragmentのレイアウトファイルからコンポーネントを特定できます。ここでは、Fragmentから辿る方法とLayout Inspectorを使う方法を紹介します。 レイアウトファイルから辿る まずはFragmentを特定します。Fragmentベースのアプリであれば以下のadbコマンドで現在表示しているFragmentを特定できます。 adb shell dumpsys activity top | grep ' Added Fragments ' -A 1 # result # ... # Added Fragments: # #0: ItemDetailFragment{e53881d} (2e9ebbae-534f-4f02-90d1-dda3c302a8fb id=0x7f0a0147 tag=PACKAGE_NAME) Fragmentが特定できたらそのFragmentのレイアウトファイルを見ていきます。レイアウトファイルからコンポーネントを特定できます。 Layout Inspectorを使う Android StudioのLayout Inspectorでレイアウトをツリー構造で確認できます。ここから特定のコンポーネントを見つけることが可能です。 その他の方法 他には Flipper などのデバッガーツールを利用することでLayout Inspectorのようにレイアウトを確認できます。こちらはプロジェクトに導入済みであれば利用できますが、Layout Inspectorと同等の機能なのでこの記事では説明を割愛します。 詳細を読み解く コンポーネントを特定できたら、UIレイヤーからデータレイヤー方向へコードを読み進めていきます。この時、これまで説明したアーキテクチャやライブラリを念頭におきながらコードを読んでいくと理解しやすいかと思います。 例えば、「衛生商品のため返品・交換対象外です」のメッセージがどのように表示されているかを調べるとします。 まずは特定したコンポーネントを見ていきます。この画面全体はFragmentですが、コンポーネントはJetpack Composeで実装されています。 @Composable fun ItemStatusInfo( viewData: ItemStatusInfoViewData, // ... ) { when (viewData) { is ItemStatusInfoViewData.Visible.NonReturnable -> { NonReturnableItemStatusItem( viewData = viewData.nonReturnableViewData, ) } // ... } } ItemStatusInfoViewDataにより、表示するUIを分岐していることがわかりました。このItemStatusInfoViewDataの値がどのように確定されるのか見ていきます。 class ItemStatusInfoViewDataMapper { companion object { @JvmStatic fun fromDomainModel(itemStatusInfo: ItemStatusInfo) { if (itemStatusInfo.returnType != RETURN_TYPE_POSSIBLE /* 返品不可を表すType */ ) { return ItemStatusInfoViewData.Visible.NonReturnable( /* ... */ ) } // ... その他の分岐 } } } ItemStatusInfoViewDataMapperでDomainModelのデータからItemStatusInfoViewDataの中身を決定しています。この先はデータの取得部分を読んでいきます。 事前のアーキテクチャの把握で、一部画面はReduxで実装されていることがわかっています。この商品詳細画面もReduxで実装されているので、その点を意識しながらコードを読んでいきます。 itemStatusInfoをどのように取得しているのかを読んでいきます。 package example.itemdetail.model data class ItemDetailState( // ... val itemStatusInfo: ItemStatusInfo, ) itemStatusInfoはItemDetailStateの中に存在しています。続いて、ItemDetailStateはReducerで作られているので中身を見ていきます。ReducerはActionに応じて新しいStateを生成する役割を持っています。 class ItemDetailReducer { suspend fun reduce(action: ItemDetailAction, state: ItemDetailState): ItemDetailState { return when (action) { is ItemDetailAction.GetItemDetailSucceeded -> { val itemDetail = action.itemDetail ItemDetailState( /* itemDetailを元にステートを更新して返す */ ) } } } } ItemDetailActionが持つパラメータのitemDetailを元にItemDetailStateを作成しているのがわかりました。 次にitemDetailがどのように生成されるのか見ていきます。 ZOZOTOWN AndroidアプリにおけるReduxでは、APIリクエストなどの非同期処理はMiddlewareで行われています。以下のようなGetItemDetailMiddlewareが存在します。この中でItemDetailRepository.getItemDetailを経由してitemDetailを取得しています。 class GetItemDetailMiddleware( private val itemDetailRepository: ItemDetailRepository ) { fun dispatch(): Dispatcher<ItemDetailAction> : (Dispatcher<ItemDetailAction>) -> Dispatcher<ItemDetailAction> = { next -> return when (action) { is ItemDetailAction.GetItemDetail -> { val itemDetail = itemDetailRepository.getItemDetail( /* parameters */ ) ItemDetailAction.GetItemDetailSucceeded(itemDetail) } } } } class ItemDetailRepositoryImpl @Inject constructor ( private val apiService: ItemDetailApiService, ): ItemDetailRepository { override suspend fun getItemDetail( /* parameters */ ) { val response = apiService.getItemDetail( /* parameters */ ) // ... 後続処理 } } apiService.getItemDetailでAPIリクエストをどのように実装しているか見ていきます。API通信においてRetrofit2が利用されていることを既に知っているため、以下のコードを見てインタフェースを理解して終わりです。 interface ItemDetailApiService { @GET ( /* endpoint */ ) suspend fun getItemDetail( /* query parameters */ ): Response<ItemDetailResponse> } このようなアプローチで事前に把握した情報を合わせながら、コードを読んでいくと理解が進みやすいと思います。 チームメンバーとのコミュニケーション 目的 コードや資料のみではわからないことも存在します。そのため、メンバーとのコミュニケーションを通して実装がそうなっている理由や経緯を理解します。 アプローチ Slackなどの社内チャットツールやメンター制度があればその場を活用するとよいと思います。質問する際には、参考にした情報、聞きたい内容を明確にすると適切な回答が得られやすいです。 ZOZOTOWN Androidチームには、開発に関する困りごとを気軽に相談できるSlackチャンネルが存在しています。そのSlackチャンネルで相談すると誰かしらがすぐに反応してコメントくれるので困る事が少なかったです。また、私がZOZOに入社したばかりの頃はメンターに口頭でも相談していました。 チーム側にこのようなフォロー体制があることで、JOINした側としては安心できました。 実際のSlackでのやりとり まとめ 本記事では大規模なAndroidアプリの実装を紐解いていくアプローチを紹介しました。構成や実装の解像度が上がったことで以前よりも学習コストを減らせたと思います。新しい環境になり既存のプロダクトへの知見がなく困っている方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com Reduxは元々JavaScriptのライブラリでStateを管理するためのフレームワークです。ZOZOTOWN AndroidアプリではReduxをカスタマイズして一部画面で導入されています。Reduxの詳しい説明は Reduxの公式ウェブサイト を参考にしてください。 ↩
はじめに こんにちは、ZOZOMO部SREブロックの蔭山です。普段は Fulfillment by ZOZO や ZOZOMO のSREを担当しています。 今回ZOZOMOで提供しているサービスの1つである「ブランド実店舗の在庫確認・在庫取り置き」のマイクロサービス(通称realshop-api)にてMySQLにアクセスできる運用ユーザーの権限管理の最適化を行いました。本記事でその取り組みについてご紹介いたします。 目次 はじめに 目次 なぜ権限管理を最適化したのか 権限管理が複雑化してきた 秘密情報を閲覧できるメンバーを制限する必要がでてきた どのように最適化したか ロール機能を使った権限の標準化 秘密情報の保護自動化 秘密情報カラムの管理 秘密情報カラムへの権限剥奪を自動化 秘密情報カラムを除いたVIEWの自動作成 実施した結果 まとめ なぜ権限管理を最適化したのか realshop-apiではDBにAmazon DynamoDBとAmazon Aurora MySQLの2つを採用しています。どのように2つのDBを使っているのかについては過去テックブログでご紹介していますので、興味のある方は以下のリンクからご覧ください。 techblog.zozo.com 運用作業や調査の一環でAurora MySQLへアクセスする必要があり、リリースから暫くの間は以下のような状態で運用していました。 運用メンバーごとにMySQLユーザーを発行 権限は必要となったタイミングで個別にMySQLユーザーに付与 しかし、サービスやチームの成長に伴って以下のような問題が出てきました。 権限管理が複雑化してきた チームメンバーの増加や様々な運用作業や調査をおこなっていくにつれてどのMySQLユーザーにどの権限が付与したのかが把握しづらくなってきました。実際に各メンバーが作業する上でもメンバーAは特定のクエリが実行できてメンバーBは実行できずに権限の付与依頼を行うなどのタスクが発生し、効率の悪化が目に見えてわかるような状態となってきました。 また権限の棚卸しを実施するにも、どのユーザーにどのような権限が付与されているのか、どの権限を付与・剥奪すべきかが定まっておらず、権限の棚卸し作業自体が困難になってきました。 秘密情報を閲覧できるメンバーを制限する必要がでてきた サービスの拡張に伴い、Aurora MySQL上に秘密情報を保持する必要が出てきました。社内の開発ルールでは秘密情報を閲覧できるメンバーをごく少数に絞る必要がありましたが、上記のように複雑化した権限の状態で更にカラムごとでの権限を制御するのは難しい状態でした。 どのように最適化したか 上記の問題もあり、このタイミングで権限管理を1から見直すこととしました。今回どのように最適化していったのか実例をご紹介します。 ロール機能を使った権限の標準化 MySQL 8.0より ロール 機能が実装されています。MySQL 8系をベースとしているAurora MySQL 3系でも利用できる機能です。ロールに対して権限を付与し、各MySQLユーザーにロールを割り当てることによりロールに紐づいた権限をユーザーにも継承できる機能です。 今回、まずはこのロール機能を使って付与権限の標準化を行うことにしました。権限の標準化にあたっては、運用メンバーのユースケースに合わせて以下のように定義しました。 権限名 ロール名 付与想定ユーザー 付与する権限 参照権限 read_only_developer_role 更新操作をする必要がないユーザー。特殊な権限を保つ必要がない運用メンバーにのみ付与。 秘密情報を含まないテーブルへのSELECT 更新権限 power_user_developer_role 更新操作をする必要があるユーザー。一部の運用メンバーにのみ付与。 秘密情報を含まないテーブルへのINSERT、SELECT、UPDATE、DELETE 管理者権限 admin_developer_role 秘密情報を含むテーブルへもアクセスする必要があるユーザー。チームマネージャーのみに付与。 すべてのテーブルへのINSERT、SELECT、UPDATE、DELETE 定義後はロールを作成し、ユーザーに今まで付与した権限をすべて剥奪後ロールを付与することでユーザーごとの権限の差異がなくなり、標準化が実現できました。 秘密情報の保護自動化 次に秘密情報を全運用ユーザーが閲覧できないような状態を作るために秘密情報の保護の自動化に取り組みました。ここからはどのように秘密情報の保護を自動化していったのか順を追ってご紹介します。 秘密情報カラムの管理 自動化するために、まずは秘密情報を保持するカラムの管理をシステムがわかりやすいようにしました。今回は秘密情報を保持したテーブル・カラムを管理するテーブルを作成しデータとして保持する方針としました。 実際には以下のような情報を保持するテーブルを作成しました。今回は sensitive_columns という名前でテーブルを作成しました。 カラム名 保存する内容 サンプル table_name 秘密情報を持つテーブル名 secret_tables column_name 秘密情報を持つカラム名 secret_column type どのような秘密情報を持っているか(会員名・住所・メールアドレスなど) email また開発ルールとして秘密情報を持つカラムが追加された場合、DBマイグレーションツールを使って上記テーブルへINSERTを行うようなルールとしました。 秘密情報カラムへの権限剥奪を自動化 次に秘密情報カラムへの権限剥奪の自動化を行いました。 realshop-apiではDBマイグレーションに Flyway を利用しています。新規でバッチなどは準備せず、開発コストを最小化して実現するためDBマイグレーション実行後にトリガーされる afterMigrate を使って権限の付け替えを実施することにしました。 しかしFlywayのafterMigrateではSQLで記載されている必要があるため、権限の付け替えロジックをストアドプロシージャで定義し実行することにしました。実際には以下のようなストアドプロシージャを定義しました。 DELIMITER // CREATE PROCEDURE sp_operation_set_sensitive_roles() BEGIN DECLARE table_name TEXT; DECLARE column_names TEXT; DECLARE done BOOL DEFAULT FALSE ; -- テーブルごとにアクセス可能なカラム一覧を取得 DECLARE tablesCursor CURSOR FOR SELECT t.TABLE_NAME AS table_name, GROUP_CONCAT(c.COLUMN_NAME ORDER BY c.ORDINAL_POSITION) AS column_names FROM INFORMATION_SCHEMA.TABLES t INNER JOIN INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME WHERE t.TABLE_SCHEMA = ' DB名 ' AND t.TABLE_TYPE = ' BASE TABLE ' AND NOT EXISTS ( SELECT 1 FROM sensitive_columns WHERE sensitive_columns.table_name = t.TABLE_NAME AND sensitive_columns.column_name = c.COLUMN_NAME) GROUP BY t.TABLE_NAME; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE ; OPEN tablesCursor; read_loop: LOOP FETCH tablesCursor INTO table_name, column_names; IF done THEN LEAVE read_loop; END IF ; -- Power Userロールへの権限付与 IF EXISTS (SELECT 1 FROM mysql. user WHERE user = ' power_user_developer_role ' ) THEN SET @power_user_grant_sql = CONCAT ( ' GRANT SELECT ( ' , column_names, ' ), UPDATE ( ' , column_names, ' ) ON DB名. ' , table_name, ' TO '' power_user_developer_role ''' ); PREPARE power_user_grant_stmt FROM @power_user_grant_sql; EXECUTE power_user_grant_stmt; DEALLOCATE PREPARE power_user_grant_stmt; END IF ; -- Read Onlyロールへの権限付与 IF EXISTS (SELECT 1 FROM mysql. user WHERE user = ' read_only_developer_role ' ) THEN SET @read_only_table_grant_sql = CONCAT ( ' GRANT SELECT ( ' , column_names, ' ) ON DB名. ' , table_name, ' TO '' read_only_developer_role ''' ); PREPARE read_only_table_grant_stmt FROM @read_only_table_grant_sql; EXECUTE read_only_table_grant_stmt; DEALLOCATE PREPARE read_only_table_grant_stmt; END IF ; END LOOP ; CLOSE tablesCursor; END // DELIMITER ; 内容は以下の通りです。 MySQLのテーブル・カラム情報を保持している INFORMATION_SCHEMA.TABLES と INFORMATION_SCHEMA.COLUMNS 、秘密情報を保持している sensitive_columns を使って通常通り閲覧できるカラムを抽出 抽出した結果をもとに権限ごとでGRANT文を生成 生成したGRANT文を実行 上記で定義したストアドプロシージャをafterMigrateで実行することにより、権限の付け替えを自動で実施できるようにしました。 秘密情報カラムを除いたVIEWの自動作成 秘密情報カラムへの権限剥奪は実現できました。しかしこの対応によって秘密情報へ参照するクエリがすべてエラーとなるため、運用体験が悪化してしまう懸念がありました。 そこで社内でも実績があった秘密情報カラムを除いたVIEWをAurora MySQL上でも実現することにしました。テーブル名の先頭に v_ を付与することで既存クエリのエラーを少しでも防ぎ、運用体験の悪化を防ぐことを目的としました。 今回参考にしたSQL Serverでの秘密情報の保護に関しても過去テックブログでご紹介しています。こちらも興味のある方はぜひご覧ください。 techblog.zozo.com 秘密情報カラムを除いたVIEWに関しても前章と同じく、FlywayのafterMigrateで特定のストアドプロシージャーを実行することにしました。実際には以下のようなストアドプロシージャを定義し実行する形としました。 DELIMITER // CREATE PROCEDURE sp_operation_create_views() BEGIN DECLARE upsert_view_sql TEXT; DECLARE done BOOL DEFAULT FALSE ; -- テーブルごとにCREATE VIEW文を生成 DECLARE upsertSQLCursor CURSOR FOR SELECT CONCAT ( ' CREATE OR REPLACE VIEW v_ ' , t.TABLE_NAME, ' AS SELECT ' , GROUP_CONCAT( CASE WHEN EXISTS ( SELECT 1 FROM sensitive_columns WHERE sensitive_columns.table_name = t.TABLE_NAME AND sensitive_columns.column_name = c.COLUMN_NAME) THEN CONCAT ( ''' ******** '' AS ' , c.COLUMN_NAME) ELSE c.COLUMN_NAME END ORDER BY c.ORDINAL_POSITION), ' FROM ' , t.TABLE_NAME, ' ; ' ) as upsert_view_sql FROM INFORMATION_SCHEMA.TABLES AS t INNER JOIN INFORMATION_SCHEMA.COLUMNS AS c ON t.TABLE_NAME = c.TABLE_NAME WHERE t.TABLE_SCHEMA = ' DB名 ' AND t.TABLE_TYPE = ' BASE TABLE ' GROUP BY t.TABLE_NAME; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE ; OPEN upsertSQLCursor; read_loop: LOOP FETCH upsertSQLCursor INTO upsert_view_sql; IF done THEN LEAVE read_loop; END IF ; SET @upsert_view_sql = upsert_view_sql; PREPARE stmt FROM @upsert_view_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; END LOOP ; CLOSE upsertSQLCursor; END // DELIMITER ; 内容は以下の通りです。 MySQLのテーブル・カラム情報を保持している INFORMATION_SCHEMA.TABLES と INFORMATION_SCHEMA.COLUMNS 、秘密情報を保持している sensitive_columns を使ってCREATE VIEW文を生成 生成したGRANT文を実行 上記の対応によって権限の付け替えと同様に自動化できました。 実施した結果 このように権限管理の最適化を実施した結果、問題としていた権限管理の複雑さはロールによる標準化で解消されました。またどのユーザーにどのロールが付与されていたかもわかりやすくなったため、棚卸しも実施しやすい状態にできました。 また秘密情報カラムに関しても必要最低限のメンバーしかアクセスできない状態にできました。秘密情報カラムを除いたVIEWも準備したことで運用メンバーの運用体験に大きな影響を与えることなく秘密情報の保護が実現できました。 今回ここまででご紹介してきた形で実現できたものの以下のような改善点が見えてきており、こちらに関しては今後解消していく予定です。 秘密情報の区分に合わせたマスクされる形式の変更 一時的な秘密情報カラムの権限付与 まとめ 本記事ではrealshop-apiで実施したMySQLでの権限管理の最適化についてご紹介しました。権限管理にお困りの方はぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
こんにちは。ZOZO Researchの研究員の古澤・川島・平川、ZOZOのデータサイエンティストの荒木・小林です。2024年8月6日(火)から8月9日(金)にかけて熊本で開催された画像の認識・理解シンポジウムMIRU2024に参加しました。この記事では、MIRU2024でのZOZO Research・ZOZOのメンバーの取り組みやMIRU2024の様子について報告します。 目次 目次 MIRU2024 企業展示 全体の動向 招待講演・インタラクティブセッション [IS2-35] The Niau Dataset: A Comprehensive Resource for Fashion Image Recognition [IS2-126] 大規模視覚言語モデルを用いた「似合う」の自動評価法 [IS-2-097] Moon & SpencerのAesthetic Measureを用いたファッションのカラー・コーディネート評価の検討 気になった研究発表 [IS1-119] Neural Lookup TableとPrompt Guidance Lossを用いた解釈可能な画像補正 [OS-1D-07] NeuraLeaf: Neural Parametric Leaf Models with Shape and Deformation Disentanglement [OS-1E-04] スパイキングニューラルネットワークによる画像生成拡散モデル [IS3-39] 集合データを対象とした識別と生成のマルチタスク学習と信頼度較正への応用 [IS3-56] 協調フィルタリングにおける潜在因子モデルの埋め込み表現のICAを用いた線形構造の分析 まとめ 最後に おまけ MIRU2024 MIRUとは、Meeting on Image Recognition and Understandingという画像の認識・理解についてのシンポジウムです。2024年の今回は熊本城ホールにおいて原則オフラインの形で開催されました。今年は過去最多の1591名もの方々が参加されたとのことで、会場が大きな賑わいを見せていました。ZOZO NEXTは、この MIRU2024 にゴールドスポンサーとして協賛させていただきました。 また、今年は5年ぶりにバンケットも開催されました。バンケットは立食形式で行われ、おいしい食事やお酒、熊本ラーメンを堪能しながら他の参加者たちと交流しました。このように、同じ分野で研究している他の研究者たちと現地で交流したり、研究内容についてカジュアルに話し合ったりできるのもシンポジウムの良さのひとつです。私たちも、初めてお話しする方はもちろんのこと、過去に研究を通して知り合った方との交流を楽しみ、明日の研究への良い刺激をいただきました。 昨年のMIRU2023に参加した際のレポートは以下の記事をご覧ください。 techblog.zozo.com 企業展示 企業展示ブースでは、ZOZO NEXTの取り組みをポスター形式でご紹介しました。ZOZOの多角的なファッションサービスと多様なデータ資産に加え、機械学習や最適化問題の実サービスへの応用事例、そして、ZOZO Researchが近年発表した論文についてご説明しました。多くの方々からご関心をお寄せいただき、お話をさせていただけたことを大変嬉しく感じています。ブースにお越しいただいた皆さま、誠にありがとうございました。展示していたポスターはこちらです。 また、ブースでご案内したZOZO NEXTの求人の最新情報はこちらからご覧になれます。 zozonext.com 全体の動向 昨年のMIRUでは、Stable Diffusionをはじめとする生成モデルや基盤モデルを活用した研究が多く見られました。今年は、これらの手法がさらに浸透し、研究のトレンドは「当たり前に使う」段階へとシフトしています。これに伴い、アルゴリズムの詳細な検討や、モデルが持つ事前知識を効果的に活用する方法に焦点が当てられるようになったと感じました。また、ファインチューニングや転移学習を行う際に生じるドメインギャップを埋めるためのドメイン適応の研究も目立ちました。加えて、Neural Radiance Fieldやそれを用いたComputational Photographyに関する研究も盛んに発表されていました。 チュートリアルでは「自動運転のためのビジョン技術」「ビジョン研究のための評価方法」「様々なセンサやモダリティを用いたシーン状態推定」という3つの講演が行われました。 まず「自動運転のためのビジョン技術」講演では、自動運転におけるセンサーやカメラの基本的な役割から最新の深層学習モデルまで、幅広い内容が取り上げられました。単一の鳥瞰図の特徴量を元に、主タスクである経路計画に加え、動作予測などの補助タスクを同時に学習させることで、主タスクの性能を向上させるPARA-Drive手法が非常に興味深いものでした。 次に、「ビジョン研究のための評価方法」では、機械学習の評価手法に関する議論が行われました。この講演では、評価指標が本当に評価したい対象を正確に捉えているのか、また評価指標そのものの性能やデータ選択のノイズの影響、さらに評価データに潜む問題点などが指摘されました。評価手法を評価するための基準を確立するには、多くの小さな問題を考慮する必要があり、その難しさを再認識しました。 最後に、「様々なセンサやモダリティを用いたシーン状態推定」はRGB画像以外の様々な認識手法に関する講演でした。イベントカメラやTime of Flightカメラ、アクティブ音響センシングなどが取り上げられ、これらの技術が従来のRGB画像ベースの手法と異なる環境で有効ということが紹介されました。特に、悪照明環境や遮蔽のある環境での性能向上に寄与するこれらの技術の可能性には驚かされました。RGB画像と比較すると研究例がまだ少ない分野ではありますが、RGB画像以外の認識方法に触れることで、認識技術の世界がさらに広がったと感じました。 招待講演・インタラクティブセッション 招待講演ではZOZOの機械学習エンジニアの住安がCVPR2024に採択された論文について発表しました。こちらの研究については CVPR参加レポート で紹介しているので、ぜひご覧ください。加えて、インタラクティブセッションでは、ZOZO Researchから2件、ZOZOから1件の研究をポスター形式で発表しました。各研究の要約は以下の通りです。 [IS2-35] The Niau Dataset: A Comprehensive Resource for Fashion Image Recognition Sai Htaung Kham , 森下和哉 , 和田崇史 , 中村拓磨 , 平川優伎 , 斎藤侑輝 ( ZOZO Research, BuySell Technologies Co., Ltd) ファッションにおいて、似合うかどうかの数値化は個人のスタイルを理解し、適切な提案をするために重要です。本研究では、ZOZOが運営するファッションSNSであるWEARに投稿された画像とスタジオで撮影した6,000枚のファッション画像からなるNiauデータセットを構築しました。各ファッション画像には、体型、ヘアスタイル、顔の形、着用アイテム、年齢といったラベルに加え、Niauスコアが付与されています。このNiauスコアは、人間のアノテーターに画像のペアを提示し、「どちらがより似合っているか」という質問の回答に基づいて算出された数値です。特に、OpenSkillアルゴリズムを使用することで、得られた回答からスコアを計算し、次の対戦ペアを生成しました。データ品質を確保するために、989名と981名の2つのアノテーターグループが画像を評価し、各グループから得られたNiauスコアの相関をスピアマン順位相関係数およびピアソン相関係数によって確認しました。 [IS2-126] 大規模視覚言語モデルを用いた「似合う」の自動評価法 平川優伎, 森下和哉, 和田崇史, 清水良太郎, 古澤拓也, Sai Htaung Kham, 斎藤侑輝 (ZOZO Research) 上記のNiauスコアの算出には多大なアノテーション費用がかかるという課題があります。本研究では似合う度の評価における大規模視覚言語モデル (Large-scale Vision Language Models, LVLMs) のゼロショット推論と人間による評価の整合度を検証しました。クラウドソーシングを利用して検証用データセットを構築し、人間による評価と主要なLVLMsの評価との間に一定の相関が存在することを確認しました。この結果は、LVLMsに埋め込まれた世界知識と視覚的な認識能力の画像を元にした似合う度の自動評価における有効性を示唆しています。 [IS-2-097] Moon & SpencerのAesthetic Measureを用いたファッションのカラー・コーディネート評価の検討 小林めぐみ , 吉本一平 , 光瀬智哉 ( ZOZO, ex-ZOZO) ファッションにおけるカラー・コーディネートは、視覚的な美しさや調和を重視する重要な要素の1つです。しかし、コーディネートの定量的な評価手法は未だ確立されておらず、主観的な判断に依存しているのが現状です。この研究では、配色の美的評価において広く利用されるMoon & Spencerの色彩調和指標 (Aesthetic Measure, AM) をカラー・コーディネートに適用できるかを検証しました。まず、ランダムな配色と比較してコーディネートされた配色の方が高いAMの値 (M) を持つという仮説を立て、両者の平均値を比較しました。コーディネートのlike数とMとの関連性を調査し、これを可視化することで、AMがファッションのカラー・コーディネート評価に単純には適用できない可能性があることを示しました。 気になった研究発表 今回の学会では多くの興味深い研究発表がありました。特に私たちが興味を持った研究についていくつか紹介します。 [IS1-119] Neural Lookup TableとPrompt Guidance Lossを用いた解釈可能な画像補正 小林哲 (東工大) 画像補正技術は、InstagramやPhotoshopなど様々なプラットフォームにおいて、写真の印象を操作するために利用されている技術です。旧来のフィルタ技術では、ピクセル間の変換処理をLookup Table (LUT) として保持しておき、入力された画像の各ピクセル値をLUTに従って変換する処理により実現されています。近年では、複数のLUTの加重平均をとることにより、複雑なフィルタ処理を学習ベースで獲得する研究が行われていますが、フィルタ名との対応を人間が解釈可能な形式で取ることができないという課題がありました。この研究では、学習可能かつ解釈可能な画像補正フィルタを実現するために、補正内容を表すテキストと補正後の画像に対して、CLIP-IQA(画像の品質評価に特化したCLIP)を用いて類似度を計算します。特に、負例のテキストと比較して類似度が大きくなるようにフィルタを学習する手法が提案されていました。また、従来手法ではLUTの加重平均ベースのモデルであったのに対し、こちらの研究では比較的シンプルなMLPが導入されており、ベンチマークにおいても良好な結果が得られたそうです。テキストベースで画像の変換処理を行う技術は近年のトレンドであり、生成画像の不自然な点をテキストベースで編集するアプローチなどは応用の幅も広く、弊社としても今後の重要な研究課題になりそうです。 [OS-1D-07] NeuraLeaf: Neural Parametric Leaf Models with Shape and Deformation Disentanglement Yang Yang, Hiroaki Santo, Yasuyuki Matsushita, Fumio Okura (Osaka Univ.) こちらは、葉の3Dモデルの再構築・形状生成についての研究です。人間や動物に関する3Dモデリングと再構築の研究は活発になされていますが、植物、特に葉のモデリングにおいては、多様な形状と柔軟な変形を正確に表現することに課題があります。例えば、人間や動物のモデルで用いられるような骨格推定や明確なパーツのセグメンテーションは、葉のモデルに直接適用できません。この研究では、事前学習済みの画像特徴抽出器を用いて葉を擬似的なパーツに分割しています。葉の3次元構造を、2次元の基本形状と3次元の変形に分離し、潜在コードで表現することで、葉の再構成と生成が可能な新しいパラメトリックモデルであるNeuraLeafを提案しています。また、数千枚の葉をスキャンして新しい3次元データセットDeformLeafも作成されていました。対象である葉の特性や特徴について実物をよく観察したうえで、うまく課題を切り分けていると感じました。 [OS-1E-04] スパイキングニューラルネットワークによる画像生成拡散モデル 渡邊諒 (東大), 椋田悠介 , 原田達也 ( 東大, 理研) スパイキングニューラルネットワーク (SNN) は、人間の脳内における信号伝達の仕組みを模倣したニューラルネットワークです。SNNは生物学的に妥当でありながらも計算効率が良いことで、エッジコンピューティングへの実装にも適しています。この研究は、近年話題の拡散モデルによる画像生成ネットワークを、SNNのみを用いて実現することを目的としています。通常の拡散モデルではニューラルネットワークによりガウス分布のパラメータを推定しますが、SNNのニューロンの出力はスパイク列(バイナリ)であるため、ガウス分布のパラメータ推定が困難という課題があります。また、SNNを拡散モデルに適用する場合、確率分布のパラメータを計算するためにSNNの出力をデコードし、再サンプリングする必要がありますが、単純なSNNではこれを解決できません。この研究では、通常の拡散モデルの拡張として、Fully Spiking Denoising Diffusion Implicit Modelを提案しています。さらに、拡散モデルにおける生成過程の各ステップをSNNの計算に置き換えるため、シナプス電流学習も提案し、先述の問題を解決しています。データセットを用いて生成性能を比較した結果、提案手法は既存のSNN画像生成モデルよりも良い性能を示しました。加えて、提案モデルと同じ構造を持つ通常のニューラルネットワークと比べて、計算効率も大幅に向上しているとのことです。一方で、生成性能に関しては通常のニューラルネットワークに及ばず、今後、U-Net部分の性能を向上させるといった改善の余地があるようです。近い将来、モバイル端末などでも拡散モデルを用いた画像生成が活用されていく未来が想像できますし、そんな社会に向けて、本研究は非常に意義のあるものになると思いました。 [IS3-39] 集合データを対象とした識別と生成のマルチタスク学習と信頼度較正への応用 佐藤文興, 早志英朗, 長原一 (阪大) 半教師あり学習は多数のラベルなしデータと少数のラベルつきデータを有効に用い、ラベルつきデータのみを用いた場合より高性能な予測器の構築を目指すタスクです。この研究では集合データの半教師あり学習に際して、教師なしの確率的生成モデルと教師ありの分類器の2つのネットワークを用意します。それらのネットワークでエンコーダ部を共有させることで、ラベルなしのものを含めたデータ分布の再現とラベルつきデータの識別を同時にこなすマルチタスク学習として半教師あり問題を扱うことができます。生成モデルのみに注目すれば教師なし学習なので、ラベルなしデータも有効に活用できると言えます。加えて、このようなアプローチは識別器が出力する信頼度の較正にも有用であることが知られているようです。ところで集合データを扱うネットワークには、入力集合内の要素の並べ替えに対する出力値の不変性 (permutation invariance) など特別な性質が要求されます。これらを満たすSetVAEとよばれる確率的生成モデルを組み込むことで、多くのデータセットに対してラベルなしデータを使用しない場合よりも識別精度が向上し、識別器の出力確率の較正効果も確認できたそうです。集合データの機械学習はZOZO Researchにおいても積極的に取り組んでいるテーマのひとつであり、集合データ特有の難しさがあるのかどうかなど、より精緻な議論が発展していくことを期待したいです。 [IS3-56] 協調フィルタリングにおける潜在因子モデルの埋め込み表現のICAを用いた線形構造の分析 岡村洋希, 前田圭介, 藤後廉, 小川貴弘, 長谷山美紀 (北大) 協調フィルタリングは「類似した消費行動を行う消費者は類似した嗜好をもつ」という信念にもとづいて、各消費者にパーソナライズされた推薦をする手法です。このとき潜在因子モデルを用いると、消費者やアイテムの埋め込み表現を得ることができます。得られた埋め込み表現の解釈は実用上重要な課題ですが、埋め込み表現は一般に高次元であるため、3次元空間の住人である我々にとって容易ではありません。近年、自然言語処理分野において、単語の埋め込み表現を独立成分分析 (ICA) によって次元削減することにより、解釈可能性に優れた軸を得ることができるという報告がなされました。この研究では同様のアプローチを潜在因子モデルによって得られたアイテムの埋め込み表現に適用し、推薦システムの文脈にも有効であることを示しました。具体的には、MovieLensと呼ばれる複数のユーザによる映画の評価データに適用したところ、ICAによって得られた軸の一部が映画のジャンルに対応することが確認されました。ICAは線形変換であるため、一度有効な軸が得られれば新しく追加されたアイテムの埋め込み表現にも適用可能であるなど、扱いやすさの点で優れています。近年は非線形の次元削減が頻繁に用いられますが、「本当に線形の手法ではダメなのか」ということは手法選択の上で常に自問するべきであると改めて感じました。 まとめ 本記事では、MIRU2024の参加レポートをお伝えしました。MIRU2024に参加し、多くの新たな知見を得ることができました。今年も現地で研究の最前線に触れ、最新の技術動向を直接感じることができたのは大変貴重な経験でした。ここで得た知見を今後の研究活動に積極的に取り入れ、さらなる成果を目指していきたいです。 最後に ZOZO NEXTでは次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できるエンジニアや研究開発を行うリサーチャーを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 zozonext.com おまけ 学会期間中は熊本名物である馬刺しにすっかり魅了され、毎日舌鼓を打ちながら味わっていました。
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの 山本 です。普段はZOZOMATやZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。 我々のチームは、複数サービスを運用する中で障害対応の経験不足や知見共有の難しさといった課題に直面していました。そこで、半年ほど前にカオスエンジニアリングの導入を開始しました。 本記事では、カオスエンジニアリングを一過性のものではなくチームの文化として根付かせ、継続的な改善サイクルを生み出すための導入から運用まで、我々のチームでの実践から得られた具体的な方法をお伝えします。 これからカオスエンジニアリングを始めようとしている方はもちろん、すでに導入しているものの効果的な運用に悩んでいる方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 カオスエンジニアリング導入の流れ 1. 目標とKPIの設定 1.1 目標設定 1.2 KPIの設定 1.3 KPIの測定方法 1.4 KPI設定時の考慮点 2. 障害シナリオの作成とツール選定 2.1 シナリオ作成のプロセスとシナリオ例 2.2 シナリオによる影響予測 3. カオスエンジニアリングの実施フローの整備 3.1 カオスエンジニアリングの実行者と対応者を決める 3.2 (実行者のみ)障害内容を決め、システムの挙動を予測する 3.3 カオスエンジニアリング実施のアナウンスを行う 3.4 カオスエンジニアリングを開始する 3.5 障害対応をする 3.6 振り返りを行う 4. 実践と改善 カオスエンジニアリング導入の効果 効果1. 月に1回GameDayを行う文化の醸成 効果2. 障害対応フローの効率化 効果3. システムへの理解度向上・障害リスクの把握 効果4. アラート・モニタリング設定の最適化 カオスエンジニアリングの導入後に感じた課題と対策 課題1. 障害が発生していないのか負荷がかかっていないだけなのかわからない問題 問題の詳細 対策 課題2. GameDayをスキップしがちになってしまう問題 問題の詳細 対策 課題3. 改善タスクが作りっぱなしになってしまう問題 問題の詳細 対策 終わりに 背景・課題 我々のチームでは、オンコール担当をローテーションで回しながらシステムの安定運用に努めています。しかし、以下のような課題に直面していました。 障害対応の経験不足 システムの安定性が向上し、実際の障害の発生頻度が低下したことで、チームメンバーが障害対応の経験を積む機会が減少した 役割の固定化 障害発生時、特定のメンバーが特定の役割を担当する傾向があった それにより障害が発生したタイミングで、メンバーの負荷状況に応じて柔軟に対応することが難しくなっていた 知見の偏り システム障害時の勘所や、調査時の必須知識(コマンドや監視ツールのどこを見るべきか、などといった知見)がチーム全体に浸透していなかった これらの課題を解決し、チーム全体の障害対応の能力を向上させるため、カオスエンジニアリングの導入を決定しました。 カオスエンジニアリング導入の流れ 我々のチームではカオスエンジニアリングの導入を以下のステップで行いました。 目標とKPIの設定 障害シナリオの作成とツール選定 実施フローの整備 実践と改善 それぞれのステップについて説明します。 1. 目標とKPIの設定 カオスエンジニアリングを効果的に実施するため、まずチーム目標に基づいた具体的な目標とKPIを設定しました。これは非常に重要なステップで、これをやらないとカオスエンジニアリングの効果が半減すると言っても過言ではないと思います。 1.1 目標設定 前述した課題の解消を目指し、目標を以下のように設定しました。 チームメンバーの障害対応の能力を向上させ、障害対応フローを効率化する システムの障害リスクを特定しチーム全体で把握する システムの耐障害性と安定性を向上させる 目標を設定することで、チーム内外でのカオスエンジニアリングに対する認識を揃え、今後何をやって何をやらないか判断できるようになります。 1.2 KPIの設定 各目標に対し、以下のようにKPIを定めました。 チームメンバーの障害対応の能力を向上させ、障害対応フローを効率化する 各メンバーのカオスエンジニアリング実施回数:1回/月以上 インシデント対応にあたったメンバーの自己評価スコア:80点以上 システムの障害リスクを特定しチーム全体で把握する カオスエンジニアリング実施時の挙動予測スコア:80点以上 障害発生から検出までの時間:3分以内 システムの耐障害性と安定性を向上させる 障害発生から復旧までの時間(MTTR):10分以内 カオスエンジニアリング実施時のエラーバジェット消費率:エラーバジェット枯渇までの期間がSLO目標の全期間の25%以上をキープ 例えば、SLO目標の全期間が30日の場合に7.5日でエラーバジェットが枯渇するような消費率を超えるとアウト、ということ これらの目標とKPIは、カオスエンジニアリングの実施ごとに見直し、改善を重ねています。 1.3 KPIの測定方法 各KPIの測定方法について、いくつか例を挙げます。 カオスエンジニアリング実施回数 実施ごとにドキュメントを残し、実施の都度確認する 自己評価スコア 評価項目を事前に用意し、実施後に自己採点する 障害検出までの時間 カオスエンジニアリング開始時からアラート通知までの時間を計測する 検知されない場合はKPI未達とする エラーバジェット消費 Datadogの SLOバーンレートアラート を使用し、アラートが発火された時点でKPIは未達となる これらのKPIは、カオスエンジニアリング実施の都度確認し、目標の達成度を測ります。なお、実際の自己評価の項目は後述の『実施フローの整備』の部分で詳細に紹介しています。 1.4 KPI設定時の考慮点 KPIを設定する際は、以下の点を考慮しました。 目標とKPIの整合性 KPIの達成が目標の達成に繋がること 現実的な計測可能性と改善可能性 計測が出来る指標であることと、改善が現れる指標であること KPIとしての有用性 数値が正常化されることで改善に繋がるKPIであること 逆に数値が正常化されたからといって改善に繋がっているとは限らないKPIはNG このように目標とKPIを設定することで、カオスエンジニアリングの効果を具体的に測定し、継続的な改善サイクルを確立することが出来ました。また、これにより取り組みの成果を可視化し、チーム全体で改善に向けた意識を高めることが出来ています。 カオスエンジニアリングの導入を検討されている方々は、チームの状況に合わせて適切な目標とKPIを設定することをお勧めします。 2. 障害シナリオの作成とツール選定 カオスエンジニアリングを効果的に実施する上では、起こりうる障害シナリオとその影響を事前に予測しまとめておくことが重要です。 これは以下の記事でも紹介されているので、ぜひご覧ください。 techblog.zozo.com このステップにより、システムの障害リスクに気づくきっかけとなるだけでなく、実際のカオスエンジニアリング実施時に予期せぬ影響が見つかった際により大きな学びが得られます。 2.1 シナリオ作成のプロセスとシナリオ例 前提として、カオスエンジニアリングの対象とするサービスは1つに絞った上で、以下のステップでシナリオを作成しました。 過去の障害ログの分析 過去に発生した実際の障害事例を確認 システム構成図からの検討 利用しているプラットフォーム(Datadog, Kubernetes, AWS)ごとに起こりうる障害の洗い出し 重要度と実現可能性による絞り込み 影響の大きさと、カオスエンジニアリングツールでの再現可能性を考慮 これらのステップを通して、以下のシナリオを扱うことに決定しました。 Availability Zoneネットワーク障害 Dynamo DBネットワーク障害 ElastiCacheインスタンス障害 S3ネットワーク障害 Pod CPU圧迫 Pod Memory圧迫 なお、シナリオの実現可能性を考慮し、ツールとしては AWS Fault Injection Service (以降、FISと記載)を使用することにしました。FISはAWSリソースへの障害注入に特化しているだけでなく、EKS上のPodへの障害注入も可能で、カオスエンジニアリングの対象とするサービスのインフラ構成に最適であったためです。 ツールの比較・検討をする中で、 Litmus Chaos はGitOps対応やFIS経由の障害注入など魅力的な特徴がありましたが、現時点ではFISのみで十分と判断しました。 2.2 シナリオによる影響予測 次に、決定されたシナリオの実行時にシステムが受ける影響を予測します。各シナリオで以下の項目を考慮して進めました。 障害の影響範囲 システムへの影響予測 影響を受けるメトリクスとSLI/SLO アラート・モニタリングでの検知可能性 障害検知と復旧までの予想時間 なお、ここでは1つを深掘りし過ぎないようにし「予測→実践→改善」のサイクルを回してブラッシュアップしていくことを優先する意識をしていました。 以下は、これらを踏まえて『S3ネットワーク障害』での影響を予測した例になります。 3. カオスエンジニアリングの実施フローの整備 シナリオまで考えることが出来れば、あとは実施していくだけなのですが、長期的に見ると「いかにカオスエンジニアリングを形骸化させず継続的にチーム全員で運用していくか」が重要です。 そのため、以下のようにカオスエンジニアリングの実施フローを整備しました。 カオスエンジニアリングの実行者と対応者を決める (実行者のみ)障害内容を決め、システムの挙動を予測する カオスエンジニアリング実施のアナウンスを行う カオスエンジニアリングを開始する 障害対応をする 振り返りを行う 実施フローの各項目について説明します。 3.1 カオスエンジニアリングの実行者と対応者を決める 我々のチームでは、障害を注入する実行者を一人決め、他のチームメンバーはそれに対応する対応者として役割を分けることにしました。 一般的にはチーム全体で障害シナリオとその影響を考えることが多いと思いますが、起こる障害が事前に分かっていないことで、対応者の障害対応スキル向上に繋がると考えてこのような形式にしています。これは「チームメンバーの障害対応の能力を向上させ、チームとしての対応フローを効率化する」という目標に沿っています。 なお、実行者と対応者はツールを使ってランダムに選出するようにしています。 3.2 (実行者のみ)障害内容を決め、システムの挙動を予測する このステップでは実行者が実施する障害と障害を継続しておく時間を決め、システムの挙動を予測します。これは先ほどの『障害シナリオの作成』のセクションでやったことそのものです。 もちろん、アプリケーションにこっそりバグを混入させるなどしてFISを利用せずに障害を起こすのもありです。 なお、実施内容と挙動の予測はカオスエンジニアリングを実施する前にドキュメント化し、まとめておきます。 3.3 カオスエンジニアリング実施のアナウンスを行う カオスエンジニアリングを行う環境を使っている他チームに影響を与えてしまう可能性があるので、前日までにその旨を伝えておきます。 3.4 カオスエンジニアリングを開始する ついに障害を注入していきます。 理由は後述しますが、我々のチームではステージング環境でカオスエンジニアリングを実施することにしています。そのため、障害発生中にサービスにより近い状態を再現すべく、ローカルから負荷をかけながら障害を注入していきます。 具体的には、FISを使って障害注入をする場合、 aws fis start-experiment コマンドを実行することで事前に用意したシナリオを発火します。 3.5 障害対応をする 我々のチームでは、障害対応時の役割として指揮者・コミュニケーション担当・記録者・オペレータを決めるルールにしているので、初めに対応者間で役割を確認します。 カオスエンジニアリングが開始されたら、対応者はアラートなどを元に原因を理解し、サービスを継続させるための対策をしていきます。なお、「ユーザーが問題なくサービスを受けられている」ことを「サービス継続」と定義しています。 ポストモーテムもカオスエンジニアリング用にドキュメントとして作成します。 また、実行者はこのタイミングで「カオスエンジニアリング実施中のタイムライン」をまとめておきます。 3.6 振り返りを行う 障害注入が終了し、障害対応が完了したら実行者と対応者で振り返りミーティングを行います。振り返りミーティングの内容は以下のような流れになっています。 概要 詳細 1. 実行者から障害内容について共有 事前準備で用意した障害シナリオとシステムへの影響予測、カオスエンジニアリング実施中のタイムラインを共有する。対応者からすると、ここで答え合わせが行われるイメージ。 2. 対応者から対応の流れの共有 障害対応時に用意したポストモーテムを元に対応の流れを振り返る。 3. KPIの達成度を記入 各KPIの達成度をメンバーごとに評価する。 4. 改善点のブレスト KPIの達成度を元に、実行者・対応者のそれぞれの観点で改善点をブレストする。 5. ネクストアクションの整理 ブレストで挙がった改善点のうち、必要なものをタスクに落とし込む。 なお、KPIの達成度は、以下のように各項目を数値化し得点や結果を残すようにしています。 KPI KPIの項目 数値化する方法 1. 各メンバーのカオスエンジニアリング実施回数が1回/月以上 今月の実施回数を数える 2. (対応者のみ)インシデント対応にあたったメンバーの自己評価スコアが80点以上(7項目合計) 初動対応の適切さ 障害発生時の最初の対応は適切かつ迅速だったかを振り返り、対応者全員が点数をつける(15点満点) 障害対応時のチームワーク 障害対応時に適切に各メンバーのロールを決めて動けたか・連携は適切だったかを振り返り、対応者全員が点数をつける(15点満点) コミュニケーションの適切さ チーム内外への情報共有が明確かつタイムリーに行われたかを振り返り、対応者全員が点数をつける(15点満点) 障害記録の適切さ 障害の詳細、原因などポストモーテムに適切に記録できたかを振り返り、対応者全員が点数をつける(15点満点) 障害の原因特定までの速さ 障害の原因をいかに迅速に特定できたかを振り返り、対応者全員が点数をつける(15点満点) 障害の原因特定の正確さ 障害の原因をいかに正確に特定できたかを振り返り、対応者全員が点数をつける(15点満点) ツール活用の度合い 利用可能なツールやリソースを最大限活用して障害対応にあたることができたかを振り返り、対応者全員が点数をつける(10点満点) 3. (実行者のみ)カオスエンジニアリング実施時の挙動予測スコアが80点以上(5項目合計) 事前準備における「システムに起こること」の正確さ 事前に予測した障害が与えるシステムへの影響がいかに正確であったかを振り返り、実行者が点数をつける(20点満点) 事前準備における「障害の影響が想定される範囲」の正確さ 事前に予測した「障害の影響が想定される範囲」がいかに正確であったかを振り返り、実行者が点数をつける(20点満点) 事前準備における「障害により影響を受けるメトリクス・SLI/SLO」の正確さ 事前に予測した「障害により影響を受けるメトリクス・SLI/SLO」がいかに正確であったかを振り返り、実行者が点数をつける(20点満点) 事前準備における「アラート・モニタリングで気づけるか」の正確さ 事前に予測した「アラート・モニタリングで気づけるか」がいかに正確であったかを振り返り、実行者が点数をつける(20点満点) 事前準備における「気づくまでにかかる時間・復旧にかかる時間」の正確さ 事前に予測した「気づくまでにかかる時間・復旧にかかる時間」がいかに正確であったかを振り返り、実行者が点数をつける(20点満点) 4. カオスエンジニアリング実施時の障害発生後から検出されるまでの時間が3分以内 カオスエンジニアリング終了後に検出までの時間を計算する 5. カオスエンジニアリング実施時の障害発生から復旧までの時間(MTTR)が10分以内 カオスエンジニアリング終了後に復旧までの時間を計算する 6. カオスエンジニアリング実施時において、エラーバジェット枯渇までの期間がSLO目標の全期間の25%以上をキープ DatadogのSLOバーンレートで計測する フローをドキュメント化し、チームの合意を得たことで、属人化を防ぎ、全員でカオスエンジニアリングを継続的に運用しやすくなりました。 4. 実践と改善 フローの整備まで完了すれば、あとはフローに沿ってカオスエンジニアリングを実施していくだけです。 我々のチームでは、毎月GameDayとしてカオスエンジニアリングを実施し、その都度振り返りを行うことにしています。 また、我々は現在、ステージング環境でカオスエンジニアリングを実施しています。これは「本番環境での直接検証」を推奨する Principles of chaos engineering の内容とは異なりますが、リスク軽減のため現段階では適切と判断しています。 本番環境での実施も検討していますが、ツール等の進化により、ステージング環境でも本番同様のトラフィックでの試験が可能になってきているため、安全かつ本番と同様の状況下での試験方法を探っています。 カオスエンジニアリング導入の効果 カオスエンジニアリングの導入により、以下のような効果が得られました。 月に1回GameDayを行う文化の醸成 障害対応フローの効率化 システムへの理解度向上・障害リスクの把握 アラート・モニタリング設定の最適化 それぞれ説明していきます。 効果1. 月に1回GameDayを行う文化の醸成 カオスエンジニアリングを導入して以来、月に1回必ずGameDayを実施するようになりました。 ここまでカオスエンジニアリングが浸透したのは「小さく始めて敷居を下げ、徐々に拡大していく」というアプローチが要因の1つだと考えています。 個人での「セルフカオスエンジニアリング」からスタート SRE内での実施へ拡大 開発チームを巻き込んだ全体での実施へ この段階的なアプローチにより、チームメンバーの心理的ハードルを下げることが出来たのだと思います。また、既存のツール(FIS)を活用し、最もシンプルな構成のプロダクトから始めたことも、スムーズな導入に寄与しました。 さらに、明確な目標とKPIを設定し、毎回の振り返りで「次はこうしよう!」という前向きな議論が生まれたことも、継続的なカオスエンジニアリングの実施を後押ししています。 効果2. 障害対応フローの効率化 振り返りの際にKPIをベースに足りなかった点・改善点を話し合う中で、既存の障害対応フローで至らない点を改善するサイクルができました。 効率化のアイデアも挙がり、それを次回のGameDayで試してみようという流れも出来ています。 例えば、我々のチームでは障害時には専用のSlackスレッドを作り、その中でそれぞれが調査内容などを自由に投稿していくスタイルでした。しかし、これだと他の人が何を調査しているのかが見えず作業が被ってしまうケースがありました。そこで、Confluenceの同時編集の機能を活用し、それぞれが作業している内容とその調査結果をリアルタイムで見られるようなフローにするアイデアが出たこともありました。 さらに、GameDayの中で普段よくやる・やったことのある役割は出来るだけやらないようにすることで、役割の固定化も解消することが出来ています。 指揮者と調査担当者とのコミュニケーションの形はどうするとやりやすいか 原因特定か影響範囲の割り出しのどっちを優先すべきかを指揮者の立場から判断しかねたので、どうすべきだったか など、役割ごとに「どのような動きが望ましいか」を振り返りでフィードバックし合うようにしたことも影響していると思います。 効果3. システムへの理解度向上・障害リスクの把握 GameDayを行う際には、障害を仕掛ける側は毎度システムにどのような作用があるか仮説を立て、それを振り返りで採点するフローになっているため、システムの理解度が上がりました。 また、システムへの作用について仮説を立て、実際にカオスエンジニアリングを実施する中で、予想外の障害リスクを発見できました。 例えば、GameDayでS3ネットワーク障害を発生させたところ、ECRからimageのpullが出来なくなり、デプロイが失敗するようになることがわかりました。これは、『2.2 シナリオによる影響予測』のセクションで例に挙げたS3ネットワーク障害の影響予測には記載されていなかった影響で、完全に予想外の障害リスクでした。 障害を仕掛ける役割をローテーションすることでこのような発見をするチャンスが全メンバーに与えられるだけでなく、振り返りで共有も行われるので知見の偏りを防ぐことができるようになりました。 効果4. アラート・モニタリング設定の最適化 カオスエンジニアリングによって、いくつかのアラートやモニタリング設定の穴に気づくことも出来ました。 例えば、GameDayで障害対応をする中で、ログが構造化されておらず検索効率の悪いアプリケーションがあることに気づき、改善につながることがありました。 また、ElastiCacheのfailoverを発生させた際には、本来発生するはずのアラートが発生しないケースもありました。そこで、なぜアラートが発生しなかったのかを分析したところ、必要な監視項目が不足していたことが判明しました。また、そもそもElastiCacheは構成上必要ないかもしれないという仮説も生まれました。 細かい点だと、我々のチームでは複数プロダクトを管理しているため、不慣れなプロダクトでアラート発生した際にどの情報を確認すべきかを素早く判断することが難しいと分かりました。そこで、即座に対応できるよう、アラートのDescriptionに「監視設計のリンク」や「確認すべき情報源」を明記するなど小さな改善も行われています。 カオスエンジニアリングの導入後に感じた課題と対策 ここまで、我々のチームにカオスエンジニアリングを導入した方法とその効果を紹介してきましたが、当然全てがいきなり上手くいったわけではありません。 カオスエンジニアリングを導入し実践していく中で感じた課題がいくつかあったので、ここではそれらの課題と対策について共有します。 課題1. 障害が発生していないのか負荷がかかっていないだけなのかわからない問題 問題の詳細 前述の通り、我々はステージング環境でカオスエンジニアリングを実施しているため、本番環境の負荷を再現するために負荷試験を流しながら障害を実行しないといけません。 その制約により、GameDayが始まったにもかかわらずアラートが発生しない場合に「異常を検知できていない」のか「実行者が負荷をかけておらずエラーになっていない」のか判別できないという問題がありました。 特に、特定のプロダクトに閉じない障害を実施した際に1つのプロダクトのみでアラートが発生している場合、障害対応のミスリードを誘ってしまうケースがありました。具体的には、S3ネットワーク障害時にZOZOGLASSにのみ負荷をかけた場合、ZOZOGLASSのみでアラートが発生し「障害はZOZOGLASSに限定されている」と誤解するケースです。 対策 そこで、対策として『SREで管理するプロダクト全てに対し負荷をかけながら障害を注入する』というルールを決めました。こうすることで、前述のミスリードが無くなるだけでなく、もし特定のプロダクトで異常が検知出来ていない場合に気付きやすくなりました。 また、このルールを実現するために全プロダクトで負荷試験の整備も行ったので、副次的に不備の改善にも繋がりました。 課題2. GameDayをスキップしがちになってしまう問題 問題の詳細 当初、GameDayは丸一日使って障害の実行・障害対応・振り返りまで全て行っていました。 しかし、振り返りは「障害の流れの共有→KPIの評価→改善点のブレスト→タスク化」とかなりやることが多く、継続的なシステムの改善のためには必須であるものの時間がかかります。ブレストの際にはファシリテーターが上手くまとめないと議論が発散し過ぎてしまうこともあります。 GameDayに参加する上で多くの時間が必要となると、優先度の高いタスクを抱えていて参加出来ないメンバーが出てきてしまうことも往々にしてあります。そういった理由から敷居が上がってしまい、GameDayをスキップすることが増えていました。それにより、メンバー全員に障害対応のノウハウが行き渡りにくく、改善が回りづらい状況でした。 対策 そこで『GameDayを丸一日ではなく、数時間に縮小する』ことで対策しました。具体的には、「障害対応は午前から午後にかけて1〜2時間程度、振り返りは夕方に1時間程度」といった具合に各作業を細かく分けました。 これにより、GameDayをする日でも他のタスクに集中できる時間を作れるため参加しやすくなり、スキップすることはかなり少なくなりました。 ただ、『丸一日カオスエンジニアリングにだけ集中する時間にする』という方法にも「障害対応にしっかり時間をかけることが出来る」・「それにより大規模な障害の訓練も可能になる」などメリットはあります。そのため、この対策は参考程度に留め、チームの状況に応じて適切な方法を選択していただくのがベターだと思います。 課題3. 改善タスクが作りっぱなしになってしまう問題 問題の詳細 GameDayを経て得られた改善点は、実際に改善に繋げなければ意味がありません。しかし、「カオスエンジニアリングから得られた改善タスクが放ったらかしになってしまう」・「他タスクで手が空かず次のGameDayを迎えてしまう」といったことがよくありました。 これは「チーム運営のフロー上、カオスエンジニアリングで挙がったタスクが拾いきれていなかったこと」・「カオスエンジニアリングから得られた改善タスクの優先度が明確に出来ていなかったこと」が原因でした。 対策 そこで『カオスエンジニアリングの振り返りの時点で、改善タスクをバックログに入れて優先度もつけておく』というフローにすることで対策しました。 我々のチームではスクラムを模したタスク管理方法をとっているのですが、この対策で、プランニング時に改善タスクを見落とすことが無くなり、次のGameDayまでに取り組むべき改善がより明確になりました。 弊チームのタスク管理方法については以下の記事をご覧ください。 techblog.zozo.com 「改善タスクの優先度を明確にすることで放ったらかしにしない」というのは今考えれば当たり前のことですが、実際にカオスエンジニアリングを運用して初めて気付いたポイントでした。同じ轍を踏まないよう、ぜひ参考にしていただければ幸いです。 終わりに 本記事では、計測プラットフォーム開発本部SREブロックにおけるカオスエンジニアリングの導入プロセスとその効果について紹介しました。 まだ半年程度の運用ですが、すでにカオスエンジニアリングの導入によって、チーム全体の障害対応の能力を向上させシステムの信頼性を高めることが出来たと言って良いほどの効果がありました。カオスエンジニアリングを始める前に目標とKPIを定め、それを元に振り返りを行なっていくことで、継続的に改善のサイクルを生み出せていることがポイントだと思います。 同様の課題を抱えているチームがいれば、ぜひ参考にしてみてください。 今後は、ステージング環境での本番環境の再現や、カオスエンジニアリングの自動化などを検討していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ブランドソリューション開発本部でWEAR by ZOZOのiOSアプリの開発を担当している山田( @gamegamega_329 )です。 2024年の5月、WEARはAIを活用したファッションジャンル診断などの新たな機能やコンテンツを導入し 「WEAR by ZOZO」 (以下、WEAR)としてリニューアルしました。私が入社してからすぐにWEARアプリのリニューアルに取り組んできました。 当時、私は業務経験の浅い新卒1年目であり、リニューアルにおいてどのようにチームに貢献できるか不安を抱えていました。しかし、業務経験が未熟な中でも、自分の力で挑戦できるタスクを見極め、それに取り組み解決することでチームに貢献しました。 本記事では、大規模リニューアルプロジェクトに参画する中で私が直面した課題と、その課題を解決するための取り組みをご紹介します。業務に対して不安を感じる新卒iOSエンジニアの参考になれば幸いです。 目次 はじめに 目次 リニューアルプロジェクト概要 プロジェクトの背景と目的 チーム構成 進め方 自身の担当範囲 課題と解決アプローチ 課題1.好みのジャンル傾向のグラフの開発工数が膨らむ スライド提案で仕様調整することで工数削減 実装可能な仕様に落とし込んで工数を削減 結果と学び 課題2.ビルドおよび配布プロセスにおけるエラーに気づきにくい Xcode Preview専用ターゲットのビルド失敗を検知する 配布失敗をSlackbotで通知する 結果と学び 課題3.スプレットシートで管理している職種間のタスクを把握しづらい Slackbotでタスクの締め切りを通知する 結果と学び 最後に リニューアルプロジェクト概要 プロジェクトの背景と目的 このプロジェクトは、 経営戦略 「MORE FASHION × FASHION TECH ~ ワクワクできる『似合う』を届ける ~」を掲げて進められました。WEARでは、ユーザーが自分に「似合う」ファッションコーディネートを見つけられるアプリとして、その価値を提供することを目的としています。 チーム構成 WEARはマトリックス型のチームで構成され、「ホーム」「探す」「メイク」の3つのチームに分かれています。マトリックス型のチームとは、組織内で複数の部門や専門分野のメンバーが集まって構成されるチームのことです。1つのチーム内に、PM、デザイナー、エンジニア(iOS / Android / バックエンド / QA)が所属しています。 私は「探す」チームに所属し、検索を中心とした似合うコーディネートを探せる機能の実装を担当しました。「探す」チームは4名のiOSエンジニアで開発を担当しました。 進め方 プロジェクトが開始してからリリースまでの主な流れは以下の通りです。 仕様のフィジビリティ調査 工数見積もり 仕様 / デザインの調整 設計 / 実装 デザインレビュー / バグ修正 リリース 自身の担当範囲 私が主に担当した範囲は以下の通りです。 探すタブのトップ画面 :ユーザーが探すタブを開いたときに表示されるトップ画面の設計と実装 好みのジャンル傾向のグラフ :ジャンルごとの傾向を視覚的に表現するグラフの作成 ジャンルで絞り込む機能 :ユーザーがジャンルで絞り込み検索をするためのコンポーネントの実装 CI/CDの運用改善やマトリックスチームの効率化 チームメンバーに難易度の高い機能に挑戦したいとお願いしたところ、「好みのジャンル傾向」のグラフを担当することになりました。好みのジャンルとは、好みに近いスタイルを見つけるために12種類に分類されたファッションのカテゴリのことです。例えば、ラフ、きれいめ、モードなどがあります。グラフのイメージは以下の通りです。 課題と解決アプローチ リニューアルプロジェクトが進む中で、私が直面した課題とその解決アプローチについてご紹介します。 課題1. 好みのジャンル傾向のグラフの開発工数が膨らむ 課題2. ビルドおよび配布プロセスにおけるエラーに気づきにくい 課題3. スプレットシートで管理している職種間のタスクを把握しづらい 課題1.好みのジャンル傾向のグラフの開発工数が膨らむ グラフの実装は、見積もりの段階で大きな工数がかかりました。要因は以下の通りです。 その1. ジャンルの割合を変更するデザインの難易度が高い その2. 12種類のラベルの位置の選定方法の難しさ リリース後には大型プロモーションを予定しており、関係各部署と足並みを揃える必要があったため、リリースまでの時間が限られていました。この状況を踏まえ、リリース日に全ての実装を完了させるには、仕様やデザインを調整し、工数を削減する必要がありました。 スライド提案で仕様調整することで工数削減 「その1. ジャンルの割合を変更するデザインの難易度が高い」に対して、開発の観点から工数がかかる要因と代替案をスライドでビジュアル化し、デザイナーに提案しました。デザイナーに別のデザインを考えてもらうためには、なぜ工数がかかるのかを理解してもらう必要がありました。しかし、文字だけで伝えるのは専門外の人には難しく、納得してもらうのも困難だと思いました。 そこで、図や表を使ったスライドを作成し、ビジュアルで伝えることで理解してもらうようにしました。工数削減のためにエンジニアの観点から提案し、どのようなコンポーネントであれば工数を削減できるか、デザイナーが判断しやすくなるような材料を提供しました。 仕様変更により初回リリースには含まれませんでしたが、実際に使用したスライドは以下の通りです。 実装可能な仕様に落とし込んで工数を削減 「その2. 12種類のラベルの位置の選定方法の難しさ」に対して、現実的な工数で実装可能な仕様に落とし込み、工数を削減しました。デザイン要件と現実的な工数のバランスの取れた仕様にするまでに、多くの試行錯誤がありました。ジャンルを示すラベルに対し、提示された基本的なデザイン要件は、以下の内容でした。 ジャンルは12種類 1行と2行の2パターン 円とラベルの間は適度なスペースを保つ この基本要件を保ちつつ、複数のジャンルが選択された状態では、以下の手法Aのような規則でのコンポーネントの配置が求められました。 手法A:ラベルの高さと円グラフのサイズを考慮して赤枠のサイズを調整する 一方で私は、保守性を重視するためには、手法Bでの設計をするのが望ましいと考えました。 手法B:ラベルを配置してぶつかったところで少しずらす デザイン要件が複雑であるため、手法Aや手法Bを採用しても、デザイン通りに実装することが難しい状況でした。納期を優先するために、チームリーダーと相談しながらデザイナーに別の手法を提案して、デザイン要件を擦り合わせることを考えました。 新たに考えた手法Cのイメージは以下の通りです。 手法Cは、4つの領域に分割し、白円の中心座標に対し、各領域のテキスト位置を決定するような方法です。 それぞれの手法を比較した結果、以下の通りです。 手法 実装コストが低いか? デザイン要件を満たすか? 変更に柔軟か? A × ◯ × B △ △ ◯ C ◯ ◯ △ 最終的に、納期を厳守しながら、デザイン要件も満たせる手法Cを選択し、実装しました。 結果と学び 結果として、ユーザ体験を損なうことなく現実的な仕様に落とし込み、無事にリリースできました。また、リリース後も致命的なクラッシュや表示のズレは発生せず、安全に実装できました。 保守性の観点からいくつかの課題があります。例えば、ジャンルの種類が増えた場合や多言語対応する際、文字が重なってしまう可能性があります。この点については、現在も設計の見直しとリファクタリングを進めて対応しています。 この経験を通じて、 優先すべきことを明確 にし、 トレードオフを理解しながら最適な落とし所を見つけること を学びました。プロジェクトの状況次第ですが、仕様通りに実装することだけにこだわらず、限られた時間の中で現実的でバランスの取れた解決策を見つけることも重要だと思いました。 課題2.ビルドおよび配布プロセスにおけるエラーに気づきにくい ビルドおよび配布プロセスにおけるエラーに気づきにくいことで2つの問題がありました。 1つ目. 他の開発者がXcode Previewを表示できない 2つ目. デザイナーとエンジニアに余分な確認作業が発生 1つ目に関して、WEAR iOSチームはビルド時間短縮のためXcode Preview専用ターゲットを作成しましたが、ファイルの追加忘れで他の開発者がXcode Previewを表示できない問題が多発していました。 2つ目に関して、デザイナー確認のために毎日ビルドを配布していましたが、配布が失敗して変更が反映されていないことに気づかないことがありました。 Xcode Preview専用ターゲットのビルド失敗を検知する 「1つ目. 他の開発者がXcode Previewを表示できない」に対して、CI/CDツールとして利用しているBitriseのワークフローを改善しました。 プルリクエストを親ブランチへマージする前、CI環境でXcode Previewターゲットをビルドし、失敗を事前に検知させました。 Xcode Previewターゲットをビルドするために、Bitriseが提供している「Xcode Build for Simulator」のステップ(特定のタスクを実行するためのスクリプト)を追加しました。bitrise.ymlの設定は以下の通りです。 // bitrise.yml - xcode-build-for-simulator@2 : inputs : - scheme : WEARPreview この設定により、プルリクエストを親ブランチへマージする前にビルドの失敗を検知できるようになりました。 配布失敗をSlackbotで通知する 「2つ目. デザイナーとエンジニアに余分な確認作業が発生」に対して、配布が失敗した時にBotを活用してSlackに通知を送りました。 Slack上で通知するために、Bitriseの「Send a Slack message」ステップを追加しました。この設定と一緒に、SlackのWebhook、APIトークン、およびSlackボットの設定が必要です。bitrise.ymlの設定は以下の通りです。 // bitrise.yml - slack@4 : title : Slack Notification failed deploy is_always_run : true run_if : ".IsBuildFailed" inputs : - webhook_url : "$SAMPLE_WEBHOOK_URL" 配布が失敗した時の検知は、 is_always_run: true 、 run_if: ".IsBuildFailed" を設定に加えることでビルドが失敗した時にステップを実行します。詳細は Running a Step only if the build failed をご確認ください。 Slackで通知された時のイメージは、以下の通りです。 結果と学び 結果として、チームの作業負担を減らせました。また、今回の解決策が他のチームにも役立てることができました。 今回の経験を通じて、 他のチームメンバーが開発しやすくなるように工夫することが重要 であると学びました。作業量自体は比較的少なかったものの、Swiftでのコーディングだけでなく、開発プロセス全体の改善にも取り組むことがチームに貢献するためには大切だと感じました。 課題3.スプレットシートで管理している職種間のタスクを把握しづらい 基本的にエンジニアはJiraを使ってタスクを管理していますが、PM、デザイナー、QAなど複数の職種が利用することを考慮し、スプレッドシートで主に仕様調整に関するタスクを管理していました。 しかし、締め切り、起票者、担当者、完了状況など多くの情報が混在していたため、タスクの把握が難しい状況でした。起票者と担当者を決め、締め切りまでに対応方針を確定する流れで進めていましたが、タスク量の増加と職種間のやり取りの多さから、タスクの把握がさらに難しくなっていました。 Slackbotでタスクの締め切りを通知する 他のチームで運用されていた締め切り通知Botを参考に、タスクの締め切りを通知するSlackbotを追加しました。締め切り当日のタスクをSlackbotを活用して通知します。 投稿の内容には、対応期日やタスクの内容、起票者、担当者を含めました。完了状況は週1回の全体ミーティングで確認するようにしました。スプレットシートで管理していたことから、Google App Scriptで実装しました。 結果と学び この取り組みの結果、タスクの締め切りを意識しやすくなり、タスクが遅れそうな場合は各自で調整するようになりました。チーム内で振り返りを行った際には、他のメンバーから感謝の言葉をもらいました。 この経験を通じて、 他チームのアプローチを参考にして自チームに適用すること を学びました。今回、私は初めてGoogle App Scriptを触りました。他のチームの参考コードを修正して自チームに適用することで、短時間で実装できるので非常に有用だと感じました。 最後に 本記事では、リニューアルにおけるいくつかの課題解決について紹介しました。業務経験が未熟な中でも、自分の力で挑戦できるタスクを見極め、それに取り組んで解決することで、微力ながらもチームに貢献しました。 未経験の実装や技術に挑戦できたのは、チームメンバーのサポートのおかげです。また、社内のiOSメンバーからも多くの協力と支援を受け、本当に感謝しています。ZOZOの人々の良さを改めて実感しました。業務に対して不安を感じる新卒iOSエンジニアの参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
はじめに こんにちは。ZOZOTOWN開発本部フロントエンドの菊地( @hiro0218 )です。 現在、ZOZOTOWNではWebフロントエンド技術のリプレイスプロジェクトが進行しています。以前の記事ではCSS in JSの技術選定をした際の背景や課題について紹介しました。 techblog.zozo.com その後、「 ZOZO Tech Meetup - Web フロントエンド 」でおよそ1年後の状況を簡単に共有させて頂きました。 speakerdeck.com 今回はZOZO Tech Meetupでお話した内容に加えて、CSS in JS導入から2年後の現状を改めて紹介したいと思います。 CSS in JS導入後の運用状況 ZOZOTOWNの開発体制は、Webフロントエンドだけでも5つのチームが存在し、さらに外部の業務委託メンバーも加えると、開発に携わるメンバーは執筆時点でのべ50名を超えています。これだけの多人数で開発をすると、開発環境のオンボーディングのようなイニシャルコストや日々の開発に関する問い合わせ対応は膨大なコストがかかってしまうため、以下のような取り組みを実施しています。 開発用ドキュメントの整備 Stylelintによるコードスタイルの統一 コードの記述方法に迷いが生じないような仕組み 単純に開発ドキュメントだけ整備するのでは伝達や統制に限界がありますが、細かい指針や推奨事項は、仕組み化していくことで開発者のスキルや経験に依存しない高品質な水準の開発を担保しています。また、イニシャルコストの低減だけではなく、コードレビューの効率化やメンテナンス性の向上も実現できています。 これらの中から、いくつかCSS in JSの運用に関わるものを紹介します。 styledの記法 ZOZOTOWNではCSS in JSのライブラリとして Emotion を採用しています。 Emotionでstyledの記法は「タグ付きテンプレートリテラル記法 (以下、テンプレートリテラル記法) 」と「オブジェクトスタイル記法」の2つの記法を利用できます。ZOZOTOWNのフロントエンド開発においては 「テンプレートリテラル記法」を推奨 しています。 それぞれの記法については、以下の通りです。 // テンプレートリテラル記法 const Section = styled.section ` background-color: #333; color: #fff; ` ; // オブジェクトスタイル記法 const Section = styled . section ({ backgroundColor : "#333" , color : "#fff" , }) ; 「テンプレートリテラル記法」を推奨した理由としては以下の通りです。 従来の CSS(Sass)と同様の記述方法ができる 導入当初、CSS in JSを初めて利用するメンバーも多く、書き慣れた従来のCSSの記述にすることで参入コストを少しでも下げたかった オブジェクトスタイル記法に比べて、CSSに慣れ親しんだメンバーにとっては直感的であり可読性や保守性が高い リプレイス前のCSSの実装を移行しやすい利点がある スタイルのネストの記述がしやすい 親子関係のスタイルや複雑な構造を表現するためのネスト構造 (例: &:hover ) を直接記述できる 複雑なスタイリングの記述がしやすい メディアクエリや擬似クラス、擬似要素など、CSSの高度な機能をそのまま利用できる CSSの継承やカスケードのルールをそのまま適用できるため、スタイルの一貫性を保ちやすい リプレイス前の環境と共通設定の Stylelint のルールが利用でき一貫性を保てる properties-order 系のプラグインなどオブジェクトスタイル記法だと一部動かないものがあった Visual Studio Codeの拡張機能をレコメンドする Visual Studio Code を利用している開発者が多いため、 .vscode/extensions.json ファイルをリポジトリ内に用意して、拡張機能をレコメンドしています。 例えばテンプレートリテラル記法は、エディタが標準ではスタイルとしてシンタックスハイライトをサポートしていないため、 vscode-styled-components のような拡張を利用します。この拡張は styled-components の拡張機能として提供されていますが、同様のシンタックスを有しているEmotionでも問題なく利用できます。 .vscode/extensions.json へ以下のように記述をするだけで拡張機能を開発者へレコメンドできます。 # . vscode / extensions . json { " recommendations ": [ " styled-components.vscode-styled-components " ] } 情報格差(拡張機能を知っているか)で開発効率に差を生まないようにしたい意図があります。 必要な情報はコンテキストに含める ThemeProvider のコンテキストには、端末の判定情報やSassの @mixin や @function に相当する関数を渡しています。各コンポーネントごとで関数を呼び出さずにstyled内からアクセスできるようにしています。 以下は、 font-weight に指定する値を function 経由で取得している例です。 const Text = styled.span ` font-weight: ${ ( { theme } ) => theme.function.fontWeight( "bold" ) } ; ` ; // iOS の Hiragino Sans のウェイトには W3, W6, W8 が含まれるが、`font-weight: bold`を指定すると 700 (W7) と同義になり意図よりも太い表示になる。 // 上記の function を利用すると iOS の場合は`font-wight: 600`の指定になり、それ以外の端末は`font-weight: bold`になる。 スタイリングへの関心事を ThemeProvider に集めることで実装者の迷いを減らしています。 Linterで誘導する スタイル向けのLinterとして Stylelint を導入しています。 Stylelintはリプレイス以前から導入しており、リプレイス後の環境のルールセットは基本的にそのままリプレイス以前のものを移行しています。 独自のルールとして、先述の「 font-weight に bold を直接記述しない」というルールがあります。このルールは、開発に参加したてのメンバーだと気付きづらく、またレビューでも指摘が漏れたり、テスト時に発覚してしまったりするといったケースも考えられます。これを事前に気付けるように declaration-property-value-disallowed-list を利用して、 bold の直接指定をルール上は許可しないようにしています。 // stylelint.config.js { rules : { "declaration-property-value-disallowed-list" : { "font-weight" : "bold" , } , } , } 社内の開発ドキュメントでも bold 指定は function を経由するようにと記載していますが、読み飛ばしや解釈違いもあるため、Linterによって二重で防ぐような処置をしています。 開発者の満足度や意見 開発メンバーに対して実施したアンケートの結果と、直近で行ったヒアリングの内容を共有します。 過去に実施したアンケート結果 導入から1年近く経ったZOZO Tech Meetupに際して、「CSS in JSの使い心地」に関するアンケートを実施しました。 対象 : リプレイス後の環境でCSS in JSを利用して開発を実施した部署内のフロントエンドエンジニア。 設問 (一部抜粋): CSS in JSに変わったことで、作業効率はどの程度変わりましたか? CSS in JS導入による全体的な満足度を評価してください CSS in JSはZOZOTOWNに適していると思いますか? CSS in JSを利用して新規に実装されたスタイルの品質はどうですか? 社内ドキュメントやインターネット上の情報を利用して開発が滞りなく進められていますか? CSS in JS(Emotion)のAPIの使い勝手はどうですか? その他、ご意見ご感想があれば記入してください 1. CSS in JSに変わったことで、作業効率はどの程度変わりましたか? 大いに向上した: 28.6% やや向上した: 57.1% 変わらない: 14.3% やや低下した: 0% 大いに低下した: 0% 2. CSS in JS導入による全体的な満足度を評価してください 非常に満足: 14.3% やや満足: 71.4% 普通: 14.3% やや不満: 0% 非常に不満: 0% 3. CSS in JSはZOZOTOWNに適していると思いますか? 非常に適している: 28.6% やや適している: 28.6% 普通: 42.9% やや適していない: 0% 全く適していない: 0% 4. CSS in JSを利用して新規に実装されたスタイルの品質はどうですか? 非常に高い: 14.3% やや高い: 57.1% 普通: 28.6% やや低い: 0% 非常に低い: 0% 5. 社内ドキュメントやインターネット上の情報を利用して開発が滞りなく進められていますか? 非常に満足: 42.9% やや満足: 28.6% 普通: 28.6% やや不満: 0% 非常に不満: 0% 6. CSS in JS(Emotion)の API の使い勝手はどうですか? 非常に使いやすい: 14.3% やや使いやすい: 71.4% 普通: 14.3% やや使いづらい: 0% 非常に使いづらい: 0% 7. その他、ご意見ご感想があれば記入してください リプレイス以前の環境でスタイルを新規で書く場合は、webpack用のentryファイルを用意する必要があり、それに比べて初期設定が簡単になりスタイルの記述までの作業が減った CSS in JS(Emotion)の使い勝手は非常に良い CSS in JSに不慣れなメンバーでも参入が容易だった CSS in JSを利用することでクラス名管理の問題などが解消された (Emotionの)日本語解説が少ないため、高度な利用例 (例:ThemeProviderにおけるThemeのマージ) に関する情報が不足していると感じた。今後はそのような知見を蓄積しドキュメントを整備する必要がある アンケートの総括 全体的にポジティブな回答が得られました。 CSS in JSライブラリ(Emotion)に対する不満が見られないことから、以下のような理由が考えられました。 初めて触れるメンバーでも参入しやすかった(簡単) ライブラリの機能性に足りない点や問題点が少なかった ドキュメントや情報が不足していると感じるメンバーもいるものの、全体的には満足度が高い結果だったと言えます。しかし、今よりも対象になる開発メンバーが少なかった事もあり、当然ながら現状とは状況や結果が異なっている可能性があります。 直近で実施したヒアリング結果 先のアンケートから時間も経過しており、関係するメンバーも増えたため、直近で改めて使用感に関するヒアリングを実施しました。 以下のような回答を得られました。 CSS設計を考える手間が減った リプレイス以前の環境では、CSSのクラス名の命名ルールにMindBEMding(BEMの派生)を採用していました 1 。この方法は堅牢な命名を可能にする一方で、適切なクラス名を考える手間も必要でした。CSS in JSの導入により、この手間が大幅に軽減され、コンポーネントの命名を集中して考えられるようになりました。 techblog.zozo.com CSSの変更が意図しない箇所やページにまで影響を与えてしまうことがあり、そのリスクを軽減するために命名規則のルールを設けていました。CSS in JSによって自動的にCSSのスコープが制限され、影響範囲も限られることで、それらの問題が発生しにくくなりました。 コードの可読性が向上した コンポーネント定義とスタイル定義を同じファイル内に共存できます。これによって、コードが見やすくなり、コード間の移動が減り実装スピードも向上しました。また、レビューも同じ観点でしやすくなりました。 複数の状態を持つコンポーネントの場合でも、コードが整理しやすく、読みやすさが向上したと感じています。 JavaScriptとCSSの統合性が向上した CSS in JSの導入によって、JavaScriptとCSSで共通定義を利用できるようになりました。これにより、細かい設定の一貫性を保つことができました (例:ヘッダーの高さをスタイリング用とスクロール時の位置操作用で同じ値を利用できる) 。 スタイルを操作するロジックがJavaScriptで簡潔に記述できるようになり(動的なスタイリングの実装)、複雑なデザイン要件にも対応できるようになりました。 CSS in JSへの課題感 開発者からはポジティブな意見が多く寄せられていますが、現状維持を良しとしているわけではなく、課題感も持って動向を注視しています。 パフォーマンスの懸念 ランタイムCSS in JSを利用していることから、パフォーマンスに関する課題感を持っています。 しかしながら、現状ではパフォーマンス上の問題が顕在化したことはありません。これは、ZOZOTOWN全体におけるリプレイス済みの機能(CSS in JSを利用している箇所)の割合がまだ低いため、問題が表面化していないという可能性も考えられます。 Zero-runtime CSS in JSへのライブラリのリプレイスについては、現時点で具体的な移行計画はありません。技術選定時の評価では、Zero-runtime方式はZOZOTOWNの開発要件を満たせないと判断しており、要件を満たすライブラリが登場しない限りは採用できないと考えています 2 。 今後、負荷テストなども実施しながら継続的にパフォーマンスを注視していきたいと思います。 可読性の課題 複雑なUIを実装しているとstyledの中で条件分岐が多くなり可読性を下げてしまうケースがあります。 以下のように簡単な分岐でも、条件分岐の記述方法がいくつか考えられます。どのような記述方法にするのが良いのか議論がありました 3 。 // (1) const Div = styled.div ` height: ${ ( { theme } ) => theme.device.isPc ? "55px" : "calc(55 / 375 * 100vw)" } ; ` ; // (2) const Div = styled.div ` ${ ( { theme } ) => { return theme.device.isPc ? css ` height: 55px; ` : css ` height: calc(55 / 375 * 100vw); ` ; } } ` ; 上記の記述方法としては、どちらも正しくコーディング規約などで記述方法を制限するようなことはしていません。意見としては以下のようなものが出ており、このような観点も記述の判断材料の1つとすると良いという結論に至りました。 (1): 他のプロパティが併記されている際にStylelintのソート( properties-order )が有効に機能する (2): PCとSPで大きく定義が異なる場合は可読性が高い 記述方法に厳格な制限を設けていないため、コードの統一性が多少失われることもあります。しかし、過度に複雑な記述 (例:多重の条件分岐や特殊な関数の利用) は避けるようにしています。これは、将来的なライブラリのリプレイス時の困難を防ぐためです。そのため、コードレビューやメンバー間での意見交換を通じて、適切な記述方法の意思統一を図っています。 まとめ 本投稿では、CSS in JS導入後の運用について紹介しました。CSS in JSの導入によって、可読性やメンテナンス性の向上といった多くのメリットがもたらされています。 技術選定では「どのようなスキルセットの開発メンバーが関与するか」「選定ライブラリが参入障壁にならないか」を意識していたため、参入障壁になっていないことがアンケートなどの結果から確認でき安心しました。一方で、Runtime CSS in JSのパフォーマンスに関する課題などは存在しているため、今後も技術的な観点で動向を注視していく必要はあると考えています。 同様の技術選定に悩む開発者の参考になれば幸いです。 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com ITCSSを採用して共同開発しやすいCSS設計をZOZOTOWNに導入した話 - ITCSSと命名規則(MindBEMding + 接頭辞)の組み合わせで実現できること ↩ ZOZOTOWN Web フロントエンドリプレイスにおける CSS in JS の技術選定で Emotion を選定した話 - LinariaZero-runtime-CSS-in-JS を検証する ↩ ZOZOTOWN開発本部のフロントエンドエンジニア有志で「スタイル分科会」を発足しており、スタイル周りの技術共有や各チームからの相談を受ける場として活動している。 ↩
はじめに こんにちは。技術本部ECプラットフォーム部マイグレーションブロックの小原です。 本記事では、Spring Bootの ApplicationRunner インタフェースを活用したバッチアプリケーション(CLIアプリケーション)の構築方法について解説します。 バッチ処理の実装において、SpringフレームワークはSpring Batchという強力なツールを提供しています。しかし、比較的単純なバッチ処理の場合、Spring Batchの使用はオーバーエンジニアリングとなる可能性があります。 そこで、軽量なアプローチとして ApplicationRunner を利用した実装方法を説明します。この方法は、シンプルなバッチ処理に適しており、Spring Bootの機能を活用しつつ、必要最小限の実装で効率的なバッチアプリケーションを構築できます。 なお、本記事は下記の環境にて検証しました。 Java 21 (Eclipse Temurin) Spring Boot 3.3.1 目次 はじめに 目次 ApplicationRunnerを選択した背景 ApplicationRunnerとCommandLineRunnerの比較 ApplicationRunnerを利用したバッチアプリケーションの実装 エントリポイントのコード 依存関係にspring-boot-starter-webを含まない場合 依存関係にspring-boot-starter-webを含む場合 バッチを起動するコマンド ExitCodeGeneratorを利用した終了コードの制御 テスト実装 @SpringBootTestを利用する方法 @ContextConfigurationを利用する方法 まとめ さいごに ApplicationRunner を選択した背景 今回のバッチアプリケーションにてSpring Batchではなく ApplicationRunner を選択した主な理由は以下の通りです。 マイクロサービスアーキテクチャの採用 : バッチの処理対象となるドメインではマイクロサービスアーキテクチャを採用しており、データ操作のためのAPIが既に存在していました。 インフラ構成のシンプル化 : データベース操作はマイクロサービスAPIの責務とし、バッチアプリケーション自体はデータベースを直接操作しない設計としました。これにより、バッチアプリケーションのインフラ構成をシンプルに保つことができました。 Spring Batchのオーバースペック : 上記の理由から、バッチアプリケーションはデータベースを直接操作することがありません。そのため、Spring Batchは学習曲線が高く、機能的にもオーバースペックであり、導入するメリットが薄いと判断しました。 軽量性 : ApplicationRunner を使用することで、必要最小限の機能を持つ軽量なバッチアプリケーションを構築できます。 これらの背景を踏まえ、 ApplicationRunner を活用した軽量バッチアプリケーションの構築方法を以下で詳しく解説します。 ApplicationRunner と CommandLineRunner の比較 Spring Bootには、アプリケーション起動時に処理を実行するためのインタフェースとして ApplicationRunner と CommandLineRunner が用意されています。以下に詳細な比較を示します。 特徴 ApplicationRunner CommandLineRunner メソッド run(ApplicationArguments args) run(String... args) 引数の処理 Spring Bootが解析済みの引数を提供 独自に引数を解析する必要がある オプション引数の扱い --key=value 形式を簡単に扱える 独自でパースが必要 非オプション引数の扱い getNonOptionArgs() で取得可能 配列の要素として直接アクセス 実行順序の制御 @Order アノテーションで制御可能 @Order アノテーションで制御可能 ここで、オプション引数とは --key=value の形式で指定される引数を指し、非オプション引数とはそれ以外の単純な値として渡される引数を指します。 例えば、 java -jar app.jar --input=data file1 file2 というコマンドでは、以下の通りとなります。 オプション引数 : --input=data 非オプション引数 : file1 と file2 本記事では、引数の扱いやすさから ApplicationRunner を選択しています。 ApplicationRunner および CommandLineRunner の詳細は、以下のJavadocを参照してください。 Interface ApplicationRunnerのドキュメント Interface CommandLineRunnerのドキュメント ApplicationRunner を利用したバッチアプリケーションの実装 以下に、 ApplicationRunner を利用したバッチアプリケーションの実装例を示します。 import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; @Component public class BatchApplicationRunner implements ApplicationRunner { private final RestClient restClient; public BatchApplicationRunner(RestClient.Builder restClientBuilder) { this .restClient = restClientBuilder.baseUrl( "http://microservice/api" ).build(); } @Override public void run(ApplicationArguments args) throws Exception { // コマンドライン引数から特定のキーの値を取得 String inputData = args.getOptionValues( "input" ).get( 0 ); System.out.println( "Input data: " + inputData); // マイクロサービスのAPIを呼び出してデータ操作を行う String response = restClient.get() .uri( "/data?input=" + inputData) .retrieve() .body(String. class ); System.out.println( "API Response: " + response); // バッチ処理のロジックを実装 performBatchProcessing(response); } private void performBatchProcessing(String data) { // バッチ処理の実装 System.out.println( "Processing data: " + data); } } エントリポイントのコード バッチアプリケーションのエントリポイントとなるクラスを、依存関係に spring-boot-starter-web を含まない場合と含む場合で分けて示します。 依存関係に spring-boot-starter-web を含まない場合 import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BatchApplication { public static void main(String[] args) { SpringApplication.run(BatchApplication. class , args); } } 依存関係に spring-boot-starter-web を含む場合 import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; @SpringBootApplication public class BatchApplication { public static void main(String[] args) { new SpringApplicationBuilder(BatchApplication. class ) .web(WebApplicationType.NONE) .run(args); } } 上記のコードでは、 SpringApplicationBuilder を使用して明示的にWebアプリケーションタイプを NONE に設定しています。これは、 spring-boot-starter-web が依存関係に含まれていても、Webサーバーを起動させないようにするためです。 この方法により、 RestClient や RestTemplate などのWeb関連のユーティリティを使用しつつ、Webサーバーを起動せずにバッチ処理を実行できます。 バッチを起動するコマンド バッチアプリケーションを起動する際のコマンドは以下のようになります。 java -jar app.jar --input=somedata このコマンドを実行すると、 BatchApplicationRunner の run メソッドが呼び出され、指定したデータを使ってバッチ処理が実行されます。 ExitCodeGenerator を利用した終了コードの制御 バッチアプリケーションが任意の終了コードを返すために、 ExitCodeGenerator インタフェースを実行できます。以下に例を示します。 import org.springframework.boot.ExitCodeGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class BatchApplication { public static void main(String[] args) { System.exit(SpringApplication.exit(SpringApplication.run(BatchApplication. class , args))); } @Bean public ExitCodeGenerator exitCodeGenerator() { return () -> { // ここで適切な終了コードを返す // 例: 0は成功、1は一般的なエラー、2は特定のエラーなど return 0 ; }; } } この例では、 ExitCodeGenerator を実装したBeanを定義しています。 exitCodeGenerator メソッド内で、バッチ処理の結果に応じて適切な終了コードを返すロジックを実装できます。 テスト実装 ApplicationRunner を利用したバッチアプリケーションのテストには、主に2つのアプローチがあります。それぞれの特徴と使用方法を説明します。 @SpringBootTest を利用する方法 @SpringBootTest アノテーションを使用すると、 @SpringBootApplication アノテーションが付与された main メソッドのエントリポイントが実行されます。 @SpringBootTest を利用して、 main に引数を渡す場合のサンプルコードは次のとおりです。なお、テストコードは Spock を利用していますが、JUnitにおいても同様です。 import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.ApplicationArguments import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification @SpringBootTest(args = [ "--input=somedata" ]) class BatchApplicationSpec extends Specification { @Autowired ApplicationArguments applicationArguments def "バッチ処理のテスト" () { expect : // 期待される結果を検証 applicationArguments.getOptionValues( "input" ) == [ "somedata" ] } } このアプローチの主なポイントは以下の通りです。 @SpringBootTest アノテーションの args パラメータを使用して、コマンドライン引数をテストに渡すことができます。上記の例では、 --input=somedata という引数を指定しています。 ApplicationArguments インタフェースを @Autowired でインジェクションすることで、テストメソッド内で渡されたコマンドライン引数を取得し検証できます。 applicationArguments.getOptionValues("input") を使用して、特定のオプション引数の値を取得できます。この方法は、 ApplicationRunner の実装でコマンドライン引数を処理する方法と同じです。 アプリケーション全体のコンテキストが起動するため、実際の動作環境に近い状態でテストできます。 ただし、テスト実行時にバッチ処理が自動的に起動してしまうため、この挙動が問題となる場合は @ContextConfiguration を利用することで解消できます。 @ContextConfiguration を利用する方法 @SpringBootTest を使用すると、 @SpringBootApplication のエントリポイントが実行されてバッチ処理が自動的に起動してしまいます。 @ContextConfiguration アノテーションと適切なコンポーネントスキャンによりバッチ処理の自動起動を防ぎ、より細かいテスト制御が可能になります。 import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer import org.springframework.test.context.ContextConfiguration import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.ComponentScan import spock.lang.Specification @ContextConfiguration( classes = [TestConfig], initializers = [ConfigDataApplicationContextInitializer] ) class BatchApplicationSpec extends Specification { @TestConfiguration @ComponentScan(basePackages = [ "com.example.batch" ], excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = [BatchApplication])) static class TestConfig { // 必要であればテストに必要な設定を追加する } def "バッチ処理のテスト" () { expect : // 期待される結果を検証 } } このアプローチの主なポイントは以下の通りです。 TestConfig クラスで @ComponentScan アノテーションを使用し、バッチアプリケーションのコンポーネントをスキャンします。ただし、バッチのエントリーポイント( BatchApplication )を除外します。 @ContextConfiguration アノテーションにおいて、 TestConfig と ConfigDataApplicationContextInitializer を指定します。 Spring Bootの ApplicationContext をカスタム設定で初期化し、バッチ処理の自動起動を防ぎます。 ConfigDataApplicationContextInitializer を使用することで、 application.properties や application.yml の設定を読み込めます。 @SpringBootTest を利用した場合と同様の挙動です。 まとめ ApplicationRunner を利用することで、Spring Batchを使用せずに軽量で柔軟なバッチアプリケーションを構築できました。この方法は以下のような場合に適しています。 シンプルなバッチ処理 マイクロサービスアーキテクチャとの統合 データベースを直接参照しない処理 しかしながら、上記の状況に当てはまらないケースにおいてはSpring Batchの導入を検討することも視野に入れてください。プロダクトの要件に応じて適切なアプローチを選択しましょう。 さいごに ZOZOでは一緒にサービスを作り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに 技術評論社様より発刊されている Software Design の2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。 ZOZOTOWNリプレイスでは、段階的にシステムを置き換えるというアプローチによってマイクロサービス化を進めています。第3回は、マイクロサービス化の要となったAPI Gatewayの自社開発の話を中心に紹介します。 目次 はじめに 目次 はじめに API Gateway自社開発の動機 マイクロサービス化に向けたアプローチ API Gatewayの自社開発を決めた理由 開発の柔軟性 動作の軽量さを重視した技術選定 ZOZO API Gatewayが担う機能 ルーティング ユーザー認証 クライアント認証 トレースIDの発行 Istio導入以前に必要だった機能 リトライ リクエストタイムアウト 加重ルーティング ZOZO API Gatewayの運用 Istioの導入 ZOZO API GatewayとIstioの責務の整理と機能分担 監視 ZOZO API Gateway本番稼働開始から3年を経て ストラングラーフィグパターンの功績 自社開発のコストメリット まとめ はじめに ZOZOTOWNは2004年から運営を開始し、オンプレミス環境で動くモノリシックなシステムとして10年以上の間アーキテクチャを変えずに拡大してきました。ビジネスが順調に成長する一方で、古いシステムはスケーラビリティの限界、モノリシックなシステムの保守コストの増大、使用技術の陳腐化など多くの課題に直面していました。それらの課題を抜本的に解決すべく、ZOZOでは2017年からZOZOTOWNのマイクロサービス化を進めています。 この記事は、シリーズ連載の第3回として、ZOZOTOWNにおける段階的なマイクロサービス化に向けたアプローチと、その戦略の要となるZOZO API Gatewayを自社開発した話をします。一般的なマイクロサービス化に関する説明は割愛します。 前提として、ZOZOTOWNの旧システムはオンプレミス環境、移行先の新システムはクラウド環境にあります。また、本記事において「ZOZO API Gateway」という表現は、ZOZOで自社開発しているシステムを指します。Amazon Web Servicesなど他社のプロダクトにAPI Gatewayという名称が含まれるプロダクトについては、異なるシステムもしくはサービスであることがわかるよう、正式名称で記載します。 API Gateway自社開発の動機 マイクロサービス化に向けたアプローチ ZOZOTOWNのマイクロサービス化のためのアプローチとして、ZOZOではストラングラーフィグパターンを採用することを決めました。ストラングラーフィグパターンとは、リプレイス対象のシステムの機能を段階的に新しいマイクロサービスに置き換えていき、すべての機能を置き換え最終的に移行元のシステムを停止する戦略です。 ストラングラーフィグパターンには、フロントエンドにインターフェースを提供し、バックエンドの状態を隠蔽するファサードが必要です。ファサードが各マイクロサービスへの動的なルーティング機能を持ち、リクエストの送信先を徐々に新システムに切り替えていくことで、段階的なシステム移行を実現します。ZOZOTOWNの規模を考えると、マイクロサービスに移行して完全に旧システムを停止できるまでに、数年もしくはそれ以上の時間が必要と考えられました。その間に断続的に行われる置き換え作業をシームレスに実現するためのファサードとして、ZOZO API Gatewayを開発することを決定しました。リプレイス初期のシステム構成を簡略化すると、図1のようになります。 図1 ストラングラーフィグパターンによる旧システムから新システムへの処理の切り替え また、ZOZO API Gatewayはファサードであると同時に、API Gatewayパターンで定義されるAPI Gatewayの役割の一部も担います。フロントエンドに単一エントリポイントを提供することに加え、バックエンドに対してサービス横断的な機能も提供します。API GatewayパターンではAPI GatewayはBFF(Backends for Frontends)として振る舞う前提で語られることがありますが、ZOZOではそれらを別のシステムとして開発しています。BFFは複数のマイクロサービスからのレスポンスの集約や、ビジネスロジックに関するサービス横断的な機能の実現を担当しており、連載第6回で紹介予定です。 リプレイスが完了した時点でのシステム構成は図2のようになることを想定しています。 図2 旧システムからの切り替え後の理想的なシステム構成 API Gatewayの自社開発を決めた理由 ZOZOでは、API Gatewayをスクラッチで開発することを選択しました。自社開発という意思決定に至った決定的なメリットとして、開発の柔軟性と、動作の軽量さを重視した技術選定ができたことが挙げられます。 開発の柔軟性 マイクロサービス化を決定した当初、巨大化していたZOZOTOWNの機能や仕様をすべて洗い出すことは困難でした。そのため、リプレイスの具体的な進め方やサービス横断的に必要となる機能の要件が固まりきっていない状態でリプレイスプロジェクトをスタートせざるを得ませんでした。API Gatewayの仕様追加・変更に手間がかかる状態だと、バックエンドの開発を進められない状況を作り出し、API Gatewayがプロジェクト全体のボトルネックになるリスクがありました。 そこで、開発をすばやく柔軟に進められる状態にしておくために、自社開発が妥当と判断しました。既存のOSSやAPI Gatewayの機能を提供するクラウドサービスの使用も検討したものの、機能拡張のハードルの高さから開発スピードの低下が懸念されました。実際にプロジェクト開始後には、技術スタックやアーキテクチャがまったく異なる新旧両システムを連携させる役割をZOZO API Gatewayが担うことになり、会社特有の事情を考慮した開発も発生しました。結果として自社開発という選択は、開発コストとシステム保守の負担の軽減につながったと感じています。 動作の軽量さを重視した技術選定 ZOZO API Gatewayは、リプレイス完了時にはZOZOTOWNのほぼすべてのリクエストを通すことになります。そのため、レイテンシが低いこと、また障害発生時にも早急に復旧できる必要がありました。したがって、OSSやマネージドなクラウドサービスを使用して充実した多くの機能を付与できることよりも、軽量かつ起動が容易で、チューニングしやすいアプリケーションを作るメリットのほうが大きいと判断しました。ZOZOではサーバサイドのアプリケーション開発にJavaもしくはGoの採用を推奨しており、とくにGoは上記の条件を満たしていました。 ZOZO API Gatewayが担う機能 ZOZO API Gatewayの主な機能は次のとおりです。 ルーティング ユーザー認証 クライアント認証 トレースIDの発行 リトライ リクエストタイムアウト 加重ルーティング レートリミット 稼働を開始した当初は上記の機能をすべて提供していましたが、Istioを導入するにあたって責務の分担をし、現在はリクエストタイムアウト、リトライ、加重ルーティング、レートリミットの機能をIstioが担っています。この節ではZOZO API Gatewayの機能を説明し、Istioについての詳細は次節で説明します。 ルーティング ルーティングは、受け取ったリクエストを検証し、設定されたマイクロサービスもしくはオンプレミスシステムに転送する機能です。マイクロサービスの増加に伴いルーティングの設定も増えるため、ZOZO API Gatewayのアプリケーションとは切り離し、YAMLファイルで宣言的に設定をしています。 ルーティングを設定するためのYAMLファイルは routes.yaml (リスト1)と target_groups.yaml (リスト2)の2つがあります。この例では、リクエストのパスが正規表現で ^/sample/(.+)$ に一致した場合、targetAに転送します。また転送時には、 $1 がキャプチャグループのマッチした部分に置き換えられます。この設定を必要に応じてYAMLに記述し、ルーティングを実現しています。 - from : path : ^/sample/(.+)$ to : destinations : - target_group : targetA path : /$1 リスト1 routes.yaml targetA : targets : - host : sample.example.com port : 8080” リスト2 target_groups.yaml ユーザー認証 ユーザー認証は、トークンを用いてリクエスト送信者が誰であるかを確認する機能です。ただしログイン時に認証するのはマイクロサービスの1つであるID基盤サービスで、このサービスでログイン時にトークンを発行します。ZOZO API GatewayではID基盤APIを用いてリクエストに付与されたトークンを検証し、ユーザーの認証情報が正しいことを確認します。 クライアント認証 ZOZOTOWNのAPIは一般公開していません。そのためリクエストできるクライアントを制限しています。リクエストする際はトークンをヘッダに付与してもらうことで、ZOZOTOWNのネイティブアプリや社内サービスなどのクライアントを識別し、クライアントを認証します。クライアント認証後は、クライアントを識別できるヘッダを伝搬し、バックエンドの処理で認可に利用しています。 リクエストを許可するクライアントは、 routes.yaml のclientsで設定できます。リスト3の設定では、targetAにリクエストできるクライアントはserviceAだけに制限できます。トークンの詳しい設定方法については、セキュリティ上の理由から割愛します。 - from : path : ^/sample/(.+)$ clients : - serviceA to : destinations : - target_group : targetA path : /$1” リスト3 clientsを指定した routes.yaml トレースIDの発行 ストラングラーフィグパターンによりリクエストが新旧システムに振り分けられ、さらに複数のマイクロサービスを通る場合もあるため、リクエストの追跡が困難になりました。そこでZOZO API Gatewayで独自のトレースIDを発行し、転送先へ伝搬しました。新旧システムで受け取ったトレースIDをログに出力することで、サービスを横断したリクエストを追跡できます。 Istio導入以前に必要だった機能 リトライ、リクエストタイムアウト、加重ルーティングの機能は現在Istioに置き換えられており、ZOZO API Gatewayではすでに使われていないため、簡潔に紹介します。いずれも target_groups.yaml で設定しました。 リトライ リトライは、リクエストが何かしらのエラーで失敗した場合に再試行する機能です。次のパラメータを用いて、転送先ごとに設定できます。 max_try_count:リクエストを試行する最大回数 retry_cases:リトライする条件(サーバエラーやリクエストタイムアウト時など) retry_non_idempotent:冪等でないHTTPメソッド(POST、PATCH)に対するリトライ設定 retry_to:リトライ先の指定 リクエストタイムアウト リクエストに対するタイムアウトの設定も、リトライと同じく、転送先ごとに設定できます。 connect_timeout:1リクエストあたりのTCPコネクション確立までの間のリクエストタイムアウト値(ミリ秒単位) read_timeout:1リクエストあたりのリクエスト開始からレスポンスボディを読み込み終わるまでの間のリクエストタイムアウト値(ミリ秒単位) idle_conn_timeout:データが送受信されなかった場合にコネクションを維持する時間(ミリ秒単位) max_idle_conns_per_host:1ホストあたりに保持するアイドル状態のコネクションの最大数 加重ルーティング 新旧システムへのリクエスト比率の重みを設定できます。この比率を調整することで、旧システムから新システムへの移行の際にカナリアリリースを実現しました。リスト4の設定では、old-systemへ80%、new-systemへ20%の割合でリクエストされます。 targetA : targets : - host : old-system port : 8080 weight : 4 - host : new-system port : 8081 weight : 1” リスト4 加重ルーティングを適用した target_groups.yaml ZOZO API Gatewayの運用 Istioの導入 連載第2回(本誌2024年6月号) でお伝えしたとおり、ZOZOTOWNのマイクロサービスが稼働するKubernetesクラスタ「プラットフォーム基盤」では、オープンソースのサービスメッシュであるIstioを導入しています。導入の背景は、新たなトラフィック制御の要件です。リプレイスが進み、新しくマイクロサービスが他サービス(クラスタ外のサービスを含む)を呼び出す通信要件が発生しました。当時マイクロサービス起点のZOZO API Gatewayを経由しない通信では、一貫したトラフィック制御機能がありませんでした。そのため、各マイクロサービスは必要に応じて独自に機能を追加する非効率的な状況に陥っていました。そこで、プラットフォーム基盤で一貫したトラフィック制御機能の提供が課題となりました。この課題に対し、ZOZOでは3つの案を検討しました。 案1:マイクロサービス起点の通信でもZOZO API Gatewayを介し、ZOZO API Gatewayのトラフィック制御機能を使う 案2:トラフィック制御機能を提供する共通ライブラリを作成し各マイクロサービスに組み込む 案3:サービスメッシュを導入し、マイクロサービスを変更することなくサイドカーパターンでプロキシを注入して透過的にトラフィック制御機能を追加する 案1はZOZO API Gatewayへの負荷が大きく、スケールによるコスト増加が現実的ではありませんでした。また、案2はライブラリの作成やそのメンテナンス、ライブラリをマイクロサービスに組み込み逐次更新するといった手間が発生します。さらに、その作業の担当をどうするか、といった課題も挙がりました。最終的に、案3のサービスメッシュが最も現実的であるという結論に至りました。ZOZOでは次のような理由からIstioを選定し、2020年後半から検証を進め、導入を決めました。 サービスメッシュを実現するOSSの中で利用実績が多い 分散トレーシングに利用しているDatadogが、Istioとのインテグレーションをサポートしている Istioの導入により、マイクロサービスのPodにはistio-proxyというサイドカーコンテナが注入されます。マイクロサービス起点の通信はすべてistio-proxyをプロキシとして経由することになり、ZOZO API Gatewayを経由せずにIstioの設定による一貫したトラフィック制御ができるようになりました(図3)。 図3 Istioによるトラフィック制御 ZOZO API GatewayとIstioの責務の整理と機能分担 ZOZO API Gatewayはネットワークの入口でのみトラフィック制御機能を提供します。一方Istioは、メッシュネットワーク全体にトラフィック制御機能を提供します。IstioではVirtualServiceというカスタムリソースを用いて、各マイクロサービスでトラフィック制御をリスト5のように定義します。 apiVersion : networking.istio.io/v1beta1 kind : VirtualService metadata : name : virtualservice spec : hosts : - microservice http : - route : - destination : host : microservice.ns.svc.cluster.local subset : subset retries : attempts : 1 perTryTimeout” perTryTimeout : 3s retryOn : 5xx timeout : 4s リスト5 virtualservice.yaml リスト5の場合、メッシュネットワーク内でマイクロサービスへHTTPリクエストを行ったとき、3秒でリクエストタイムアウトとなります。HTTPステータスコードで5xx(リクエストタイムアウト504を含む)が返却された場合、1回リトライします。リトライを含むリクエスト全体で4秒経過するとクライアントにリクエストタイムアウトが返ります。ZOZO API Gatewayをメッシュネットワークに追加する際、トラフィック制御を両方設定しているとトラフィック制御が二重で行われてしまう状態となります。その場合、意図しないトラフィック制御により正常なリクエストができずZOZOTOWNの画面が表示されないなどの事象を引き起こす可能性があります。この課題に対し、ZOZOでは次の方針で重複する機能の責務を整理しました。 一貫した機能提供のため、メッシュネットワーク全体に関わる機能はIstioの責務とする 入口のGatewayレイヤーでのみ必要となる機能はZOZO API Gatewayの責務とする この責務に従い、各機能を表1のとおり分担することにしました。 機能 ZOZO API Gateway Istio ルーティング ○ ユーザー認証 ○ クライアント認証 ○ トレースIDの発行 ○ リトライ ○ リクエストタイムアウト ○ 加重ルーティング ○ レートリミット ○ 表1 責務整理後における機能の分担結果 ZOZO API Gatewayで不要となる機能はサービスメッシュへの追加と併せて削除していきました。結果、意図しないトラフィック制御などの問題が発生することなく、プラットフォーム基盤全体にIstioを導入できました。このように、Istioの導入でZOZO API Gatewayは責務を変え、Gatewayレイヤーで必要な機能のみ提供する形になりました。 監視 リプレイスプロジェクトではDatadog APMを使用し、分散トレーシングを実現しています。ZOZO API Gatewayでは、traceのspanタグにクライアント識別子を追加することでリクエスト元を判断できるようにしており、エラー発生時の可観測性を向上させています。ZOZO API GatewayはZOZOTOWNのリクエストのほぼすべてを処理する前提で開発していることから、このようなしくみによってすべてのサービスのエラーを検知し、分析できます。 監視の運用については特有の難しさもあります。どのサービスが発したエラーであれ、システム異常の可能性があることに変わりはないため、ZOZO API Gateway開発チームでは、検知したエラーにはほぼすべて対応します。チームはアラートを受けて、エラーが発生したサービスの通知が流れるSlackチャンネルを確認したり、担当チームにヒアリングを行ったり、事情を考慮してZOZO API Gateway側でのエラー通知の基準を調整したりなどの意思決定をします。現状、マイクロサービスが障害を起こす頻度は低く、監視で対応するケースは激しいスパイクアクセスを処理する機能を提供するサービスに起因した避けられないエラーの通知がほとんどです。しかし、安全な運用のために、ZOZO API Gatewayを開発するチームは関連するマイクロサービスの性質についてもある程度幅広く知っておく必要があります。 ZOZO API Gateway本番稼働開始から3年を経て ZOZO API Gatewayは2020年4月に稼働を開始し、2024年時点で3年以上の間本番環境で動いています。本記事の締めくくりとして、この節ではリプレイスプロジェクトにおける具体的な成果を紹介します。以降の説明で使用されるデータは、すべて2024年4月上旬時点のものです。 ストラングラーフィグパターンの功績 ファサードが存在することにより、バックエンドの開発が完全にフロントエンドから切り離され、リプレイスの戦略に大きな柔軟性を生みました。フロントエンドはAPIのホストが新旧システムのどちらであるかを意識する必要はなく、ZOZO API Gatewayの設定を変更するだけで新システムでの処理に切り替えることができます。これは、たとえばオンプレミスDBからマイクロサービスDBへのデータ移行時に役立ちます。リプレイスプロジェクトでは、ZOZOTOWNの稼働を止めずにデータを移行するために、書き込みを行うAPIとデータ取得のAPIを異なるタイミングでリリースします。ファサードがなければリクエストの切り替えに都度フロントエンドの修正が必要ですが、ストラングラーフィグパターンではフロントエンドのチームがバックエンドの事情を考慮する必要はありません。データ移行戦略の詳細についてはテックブログ *1 をご覧ください。 プロジェクト全体としても、マイクロサービス開発組織のスケールを実現できました。現在20以上のサービスの開発が同時に進んでおり、ZOZOTOWNを止めることなくリプレイスを進めています。エンジニアの採用も積極的に行い、リプレイスプロジェクトに携わるエンジニアの数は150人程度に増えました。 自社開発のコストメリット ZOZOTOWNのアクセス量の多さから、Amazon API Gatewayのような従量課金の料金体系のサービスを使用するよりも、自社でサービスを開発したほうが安くなるのではないかというもくろみが開発当初からありました。実際にコストメリットがあるのか、試算してみます。ここでは、Amazon API Gatewayサービスと、ZOZO API Gatewayが稼働しているAmazon EC2(以下EC2)の料金を用います。 執筆時点のAmazon API Gatewayの料金体系では、最初の3億リクエストは100万件当たり1.29USD、それを超えた分は100万件当たり1.18USDかかります。一方、ZOZO API GatewayはAmazon Elastic Kubernetes Service上で稼働しているため、そのEC2ノードにかかるコストが主な利用料金となります。リプレイス完了後にZOZO API Gatewayがどの程度のアクセスを処理することになるかは現時点では明言できませんが、たとえば月間のリクエスト数を10億とした場合、それぞれの概算コストは表2のようになります。リクエスト数はアプリケーションの設計にもよりますが、試算のとおり、月間10億リクエスト程度であれば、ZOZO API GatewayはAmazon API Gatewayと比べて60%程度のコストでアクセスをさばくことができると言えます。 Amazon API Gateway ZOZO API Gateway 試算 3億✕$1.29/100万 + (10億-3億)✕$1.18/100万 省略 (弊社実績とEC2の料金体系からの概算) コスト(月額) $1,213 $734 表2 Amazon API GatewayとZOZO API Gatewayの概算コスト比較 実際にZOZO API Gatewayが処理するアクセス数はより多く、よりZOZO API Gatewayにコストメリットがある状態です。またZOZO API GatewayはEC2のリザーブドインスタンスを契約するなどの工夫により、試算した金額感よりもさらにコストを抑えることができています。そのため、当初のもくろみどおりZOZO API Gatewayの自社開発はマネージドサービスの従量課金と比較してコストメリットがあることがわかります。 一方で、Amazon API GatewayにはAPIの作成、保守運用などに必要な豊富な機能が備わっています。ZOZOTOWNでのユースケースでは自社開発が結果的にコスト抑制にもつながりましたが、初期開発コストを抑えてより一般的な機能を実現したい場合や、アプリケーションの要件によっては、Amazon API Gatewayを採用するほうが妥当な可能性があります。 まとめ ZOZOTOWNではストラングラーフィグパターンによるマイクロサービス化を進めており、その戦略の要となるファサードとしてZOZO API Gatewayを開発しました。ZOZO API Gatewayは一般的なルーティングなどの役割を担い、Istioの導入によってリクエストタイムアウトやリトライなどの一部機能を分担し、開発規模は比較的小さく保っています。プロジェクトの進行に伴い、一部新旧システムの仕様の差を吸収する役割を担うことになったものの、スクラッチから開発したことにより柔軟に開発を進めることができました。ZOZOTOWNのケースでは、API Gatewayの自社開発はリプレイスプロジェクトのスムーズな進行に大きく寄与し、コスト低減にも役立っています。 本記事は、技術本部 ECプラットフォーム部 会員基盤ブロックの富永 良子、吉江 守弘と、技術本部 SRE部 プラットフォームSREブロック ブロック長の亀井 宏幸によって執筆されました。 本記事の初出は、 Software Design 2024年7月号 連載「レガシーシステム攻略のプロセス」の第3回「API Gatewayとサービスメッシュによるリクエスト制御」です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com *1 : サービス無停止を実現するデータ移行戦略
こんにちは、SRE部カート決済SREブロックの飯島です。普段はZOZOTOWNのカート決済機能のリプレイス・運用・保守に携わっています。またSplunkの管理者としても活動しています。 本記事ではSplunk CloudにおけるInfrastructure as Code(IaC)についてご紹介します。 背景 Splunkに対して感じていた課題 Splunk CloudのIaC化検討 Splunk Appを使ったIaC App API IaC対象のSplunkリソース 既存のリソースのエクスポート IaCの全体像 Appのディレクトリ構成 CI/CDパイプライン 検証の詳細 デプロイの詳細 効果、メリット 終わりに 背景 弊社では様々な用途でSplunkを活用しています。過去にいくつかテックブログを公開していますので、興味のある方はぜひご覧ください。 techblog.zozo.com techblog.zozo.com Splunkには、オンプレミスやクラウドプロバイダーにインストールして運用するSplunk Enterpriseと、SaaSとして提供されるSplunk Cloudの2種類があります。弊社では後者のSplunk Cloudを利用しています。 Splunkに対して感じていた課題 冒頭でも説明した通り、弊社ではSplunkを様々な用途で活用しており、その重要性は導入時よりも高まっています。一方で以下の課題が生じていました。 SPLの習得難易度が高く新規利用の開始までのハードルが高い コード管理されていないためダッシュボードなどがいつの間にか変更されていることがある 何の用途で作成したか不明なリソースが存在する IaCを導入すると以下のメリットが生まれ、上記の課題を解決できます。 Splunkが不慣れでも他のコードを参考にして効率よくリソースの作成、変更が可能 コード管理されているとSplunkを触ったことがない人にも勧めやすい コード管理ツール(例えばGitHub)に変更履歴が残る コード管理ツールでレビューできる/レビューが受けられる Splunk CloudのIaC化検討 上述した課題を解決するためにSplunk Cloudをコード管理する方法を検討しました。 社内でインフラ構成管理ツールとしてTerraformを利用しているケースが多かったため、まずはTerraformでの管理を検討しました。しかしSplunk CloudではTerraformで管理できるリソースが一部に限られており、ダッシュボードなどのコード管理に対応できないことから課題解決に至らないと判断しました。 補足としてSplunk Enterpriseはほぼ全てのリソースがTerraformに対応しているため、Splunk Cloudについても今後対応リソースが増えていく可能性は十分あります。 github.com Splunk Appを使ったIaC そこで今回Splunk App(以下、App)と、Splunk社から提供されるApp用の2つのAPIを使ってIaCを実現しました。 App Appとは、Splunkの設定を集約して管理するための単位です。デフォルトではSearch & ReportingというAppがあらかじめ利用可能になっています。ユーザー自身でAppを新規作成したり、 Splunkbase からAppをインストールしたりもできます。 今回Search & Reportingの既存リソースを、新規作成したAppに移行してコード管理する方針としました。 API 以下が使用した2つのAPIです。 AppInspect API Appの品質と準拠性を検証するためのAPI(ref. Validate your private app ) Admin Config Service (ACS) API AppをSplunk Cloudにデプロイすることが可能(ref. Install an app ) デプロイするにはAppInspect APIの検証に合格する必要がある IaC対象のSplunkリソース 弊社でよく利用されているダッシュボードとアラート・レポートを対象としました。 Appを使ったIaCでは、Terraformのようにリソースを インポート して既存リソースをコード管理下に置く手段がありません。リソースをコードで定義すると、新規リソースとして作成されます。そのため、既存リソースを新規リソースに置き換えることが難しいもの(例えばIndex)は対象外としました。 既存のリソースのエクスポート 既存リソースの構成ファイルはエクスポート可能です。Splunk Enterpriseでは利用者自身でエクスポート可能ですが、Splunk Cloudではこの機能が存在せずSplunkサポートに依頼する必要があります。 今回既存のリソースが集約されているSearch & Reporting Appをエクスポートし、IaC対象のダッシュボードとアラート・レポートの構成ファイルを入手しました。 IaCの全体像 Appの構成ファイルはGitHubで管理しています。また、AppInspect APIとACS APIを使用し、GitHub ActionsのWorkflowを利用して、アプリの検証とデプロイを行うCI/CDパイプラインを構築しました。 Appのディレクトリ構成 Appは 開発者ガイド にある通り、決められたディレクトリ構成に準拠する必要があります。もし準拠できていない場合、AppInspect APIの検証に失敗します。 以下はAppの必要最低限のディレクトリ構成に、ダッシュボードとアラート・レポートを追加した構成です。 . ├── bin │ └── README ├── default │ ├── app.conf │ ├── data │ │ └── ui │ │ ├── nav │ │ │ └── default.xml │ │ └── views │ │ └── README │ │ └── sample_dashboard.xml │ └── savedsearches.conf ├── metadata │ └── default.meta └── static ├── appIcon.png ├── appIconAlt.png ├── appIconAlt_2x.png └── appIcon_2x.png 以下は上記の構成の主なファイルの説明です。 ファイル 説明 ./default/app.conf Appの設定 ./default/savedsearches.conf アラート・レポートの設定 ./default/data/ui/views/dashboard.xml ダッシュボードの設定 ./metadata/default.meta 各種リソースのアクセス権限の設定 CI/CDパイプライン GitHub上でPRを作成すると、ダッシュボードのXMLファイルのLintと、AppInspect APIを使用したAppの検証が実行されます(CI)。マージすると再度CIが実行され、その後ACS APIを使用したAppのデプロイと(CD)、CI/CDの結果のSlack通知が実行されます。 以後、Appの検証とデプロイの処理について説明します。 検証の詳細 以下がAppの検証処理の流れです。 splunk.comにログインしてアクセストークンを取得 -(1) Appのコードをtarコマンドでgzip圧縮 -(2) (1)と(2)を添付してAppInspect APIを実行 AppInspectの結果から検証の合否を判定 以下サンプルコードです。 # splunk.comにログインしてアクセストークンを取得 loginstatus = $( curl -u " $username : $password " --url " https://api.splunk.com/2.0/rest/login/splunk " ) access_token = $( echo " ${loginstatus} " | jq .data.token | tr -d ' " ' ) # Appのコードをtarコマンドでgzip圧縮 COPYFILE_DISABLE = 1 tar --format ustar -cvzf " ${appname} " .tar.gz " ${appname} " tarfile_path = $( realpath " ${appname} .tar.gz " ) # AppInspect API実行 request = $( curl -X POST \ -H " Authorization: bearer ${access_token} " \ -H " Cache-Control: no-cache " \ -F " app_package=@ ${tarfile_path} " \ -F " included_tags=cloud " \ --url " https://appinspect.splunk.com/v1/app/validate " ) # AppInspectの進捗を確認 request_id = $( echo " ${request} " | jq .request_id | tr -d ' " ' ) appinspect_status = " PROCESSING " while [ " ${appinspect_status} " = "PROCESSING" ]; do echo " waiting the app inspect reviewing " echo " fetch to check the status of inspecting from https://appinspect.splunk.com/v1/app/validate/status/ ${request_id} " sleep 10 status_request = $( curl -s -X GET \ -H " Authorization: bearer ${token} " \ --url " https://appinspect.splunk.com/v1/app/validate/status/ ${request_id} " ) echo ""; echo " -------------- App Inspect Status -------------- " echo " ${status_request} " | jq . appinspect_status = $( echo " ${status_request} " | jq -r .status ) done # AppInspectの成否の判定 errors = $( echo " ${status_request} " | jq -r .info.error ) failures = $( echo " ${status_request} " | jq -r .info.failure ) result = " failed " if [ " ${errors} " -eq 0 ] && [ " ${failures} " -eq 0 ]; then result = " success " fi AppInspectの結果(上記サンプルコードの status_request )には以下の項目が含まれます。errorとfailureが0であれば検証は合格です。 " status ": " SUCCESS ", " info ": { " error ": 0 , " failure ": 0 , " skipped ": 0 , " manual_check ": 0 , " not_applicable ": 0 , " warning ": 0 , " success ": 1 } AppInspectの結果の詳細はレポートで確認できます。レポートにはJSON形式とHTML形式があり、Content-Typeでファイル形式を選択できます。 # JSON形式 curl -X GET \ -H " Authorization: bearer ${access_token} " \ -H " Cache-Control: no-cache " \ -H " Content-Type: application/json " \ --url " https://appinspect.splunk.com/v1/app/report/ ${request_id} " # HTML形式 curl -X GET \ -H " Authorization: bearer ${access_token} " \ -H " Cache-Control: no-cache " \ -H " Content-Type: text/html " \ --url " https://appinspect.splunk.com/v1/app/report/ ${request_id} " デプロイの詳細 以下がACS APIを実行してSplunk CloudにAppをデプロイする処理です。 curl -X POST " https://admin.splunk.com/ ${STACK} /adminconfig/v2/apps/victoria " \ --header " X-Splunk-Authorization: ${access_token} " \ --header " Authorization: Bearer ${json_web_token} " \ --header " ACS-Legal-Ack: Y " \ --data-binary " @ ${tarfile_path} " | jq . CIで取得したアクセストークンとtarコマンドでgzip圧縮したAppのコードを添付します。またJSON Web Tokenが必要になるため、事前にSplunk CloudのUIもしくはACS APIで作成します。 効果、メリット IaC化により、他のコードを参考にしてリソースの作成や変更が可能となりました。これにより、Splunkに不慣れな場合でも効率的にリソースの作成や変更ができ、Splunkに触っていない人にもSplunkの利用を勧めやすくなりました。またGitHub上のPRで変更内容をレビューでき、その変更内容が履歴に残るメリットも得られました。 終わりに 弊社と同様にSplunk Cloudを使い、IaC化を検討している方の参考になれば幸いです。 今回Splunkのカスタマーサクセスチームの皆様には多大なご協力をいただきました。この場を借りて、心から感謝申し上げます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、データシステム部推薦基盤ブロックの寺崎( @f6wbl6 )です。 私たちのチームではZOZOTOWNにおけるパーソナライズ機能と推薦システムを開発しており、2022年6月のテックブログではZOZOTOWNのホーム画面をパーソナライズした事例の1つを紹介しました。 techblog.zozo.com 今回は上記記事の続編として、ホーム画面での商品訴求の単位である「モジュール」の並び順をパーソナライズした取り組みをご紹介します。本記事がパーソナライズ機能や推薦システムを開発している方の参考になれば幸いです。 目次 はじめに 目次 ZOZOTOWNのホーム画面における「モジュール」について パーソナライズ施策の比較 パーソナライズの方針 モジュール並び順パーソナライズの要件 パーソナライズ方法 概要 モデル構成 モジュールのスコア算出方法 システム構成 1.Two-Towerモデル学習パイプライン 2.商品ベクトル化パイプライン 3.モジュール並び順予測パイプライン A/Bテスト 概要 結果 A/Bテストを経て挙がった課題 定性的な課題 定量的な課題 最後に ZOZOTOWNのホーム画面における「モジュール」について まずはじめに、前回記事のおさらいとしてZOZOTOWNのホーム画面における「モジュール」について簡単に説明します。 モジュールは商品やブランド、ショップなどのコンテンツを表示させるための枠の総称で、ZOZOTOWNのホーム画面はこれらのモジュールによって構成されています。 2024年7月19日時点でのZOZOTOWNホーム画面 モジュールは訴求内容や更新頻度に応じて「定常モジュール」と「企画モジュール」の2種類に大別されます。各モジュールの違いは下表の通りです。 運用方法 更新頻度 定常モジュール 常に同じ訴求内容のコンテンツを掲載する ほぼ更新なし 企画モジュール ビジネスサイドが自由に訴求内容を企画・管理する 1~2週ごとに更新 定常モジュールは「チェックしたアイテム」や「世代別アイテムランキング」など、ファッショントレンドなどの外的要因によらず継続的に特定の内容を訴求するものです。一方、企画モジュールはビジネスサイドの企画チームがトレンドやキャンペーンに応じて柔軟に訴求内容を変更するもので、定期的に更新される点が特徴です。 企画モジュールには商品の検索条件が紐づいており、この検索条件を元に商品検索用のAPI(検索API)から取得した商品をモジュールに表示する仕組みとなっています。そのため企画チームは商品の検索条件を設定するだけで多様な訴求軸のモジュールを作成できます。 パーソナライズ施策の比較 「ホーム画面のモジュールをパーソナライズする」と一口に言ってもパーソナライズのパターンがいくつかあるため、ここではそのパターンを列挙しつつ過去の施策と今回の施策がどのパターンに該当するかを説明します。 モジュール内に表示される商品の表示順序を変える モジュール内に表示される商品の検索条件を変える 表示されるモジュールの順序を並び替える 過去のテックブログ で紹介したパーソナライズ施策は上記のうち「 2.モジュール内に表示される商品の検索条件を変える 」に該当します。前述の通りモジュールには特定の検索条件で取得した結果を表示しているため、ユーザーごとにパーソナライズしたブランドやカテゴリを検索条件に反映させることでモジュール内の商品をパーソナライズしていました。 過去のパーソナライズ施策のイメージ 今回のパーソナライズ施策は「 3.表示されるモジュールの順序を並び替える 」に該当します。モジュールそのものの並び順をユーザーごとに変化させ、ユーザーが興味を持つと考えられるモジュールを上位に表示することを目指しています。 今回のパーソナライズ施策のイメージ また、パーソナライズの対象となるモジュールは前述した「 企画モジュール 」に限定しています。定常モジュールはいわゆるマス向けの訴求軸であるため、パーソナライズの効果はそこまで期待できません。一方で企画モジュールは企画チームが多様な訴求軸のモジュールを作成できるため、パーソナライズによる効果が大きいと考えられます。 パーソナライズの方針 過去の施策ではパーソナライズの目的として「 既存購入者の来訪頻度・購入頻度の増大 」を掲げていましたが、今回の施策ではまず「 より良いモジュールの並び順とはどのような状態か 」を定義することから始めました。 ユーザーにとって最適な並び順でモジュールを表示することは長期的には購入頻度や受注金額の増大に繋がると考えられますが、短期的には興味のあるモジュールをクリックする頻度が増えるはずです。ここから「良いモジュールの並び順」を「ユーザーが興味を持ってクリックするモジュールが上から順に並んでいる状態」と定義し、まずは「 モジュールのクリック数増大 」を目指すこととしました。 この目的を元に、「ユーザーにクリックされるモジュールを優先的に表示する」という方針でパーソナライズシステムを開発しました。以降、モジュールの並び順のことを「モジュール並び順」と記述します。 モジュール並び順パーソナライズの要件 今回のプロジェクトで挙げられた要件をいくつかピックアップします。 会員ごとにパーソナライズされた並び順でモジュールを表示する パーソナライズされた並び順が定期的に更新される パーソナライズロジックのA/Bテストを実施できる モジュール並び順のパーソナライズができないユーザーに対してデフォルトの並び順でモジュールを表示できる 任意のモジュールを特定の位置に固定表示できる 1〜3番目の要件については特に違和感はないと思います。 4番目の要件は、ユーザーがパーソナライズに必要な行動ログを持たない場合やパーソナライズシステムが障害等で稼働できない状況を想定したものです。パーソナライズ機能に依存してZOZOTOWNのホーム画面にモジュールが表示されなくなる、という状況を避けるためにデフォルトの並び順を表示する要件を設けています。 5番目の要件はビジネス要求に基づくものです。ZOZOTOWNでは特定のブランド・ショップの商品の訴求を強めたり、セール期間中にセール商品の訴求を強めたりするケースがあり、そういった際に一部のモジュールを特定の位置に固定表示します。また広告系のモジュールやパーソナライズ以前から訴求効果が高かった「チェックしたアイテム」モジュールなど、表示位置を迂闊に変えるべきでないモジュールもいくつかあります。今回のプロジェクトではこれらのモジュールはパーソナライズの対象外として特定の位置に固定表示しています。 パーソナライズ方法 ここからはメイントピックであるモジュール並び順のパーソナライズ方法について説明します。 概要 モジュール並び順のパーソナライズにはユーザーとモジュールの相性を表すスコアを利用しており、このスコアの降順にモジュールを配置することでパーソナライズを実現しています。モジュールのスコアを計算する方法の概念図を以下に示します。 パーソナライズ方法の概念図 ユーザーを表現するベクトルはZOZOTOWN内での回遊行動をもとに作成し、モジュールを表現するベクトルはモジュールに表示される商品の情報をもとに作成しています。モジュールのベクトルはそのモジュールに表示される商品ベクトルの集合と考えられるため、商品ベクトルの集合とユーザーベクトルを用いてモジュールに対するスコアを計算します。 このパーソナライズ方法を実現するには ユーザー情報と商品情報をベクトルにするための仕組み が必要になります。次節ではこのベクトル化に必要な機械学習モデルとシステム構成について説明します。 モデル構成 ユーザー情報や商品情報をベクトル化する手法は多々ありますが、今回は弊社の別のパーソナライズシステムでも利用されている Two-Towerモデル を採用しました。Two-Towerモデルはドメインが異なるデータを同一のベクトル空間にマッピングする2つのエンコーダーから成っています。これらのエンコーダーを使ってユーザーと商品それぞれのベクトルを獲得します。 Two-Towerモデルの概念図 モデルの学習方法や使用したライブラリについては以下の通りです。 項目 内容 学習の方針 商品閲覧を最適化 モデル構築に使用したライブラリ TensorFlow Recommenders 近似最近傍探索の手法 ScaNN モジュールのスコア算出方法 ユーザーベクトルと商品ベクトルを使ってモジュールのスコアを決めますが、スコアの算出方法はパーソナライズ結果に大きく影響を与えるため、オフラインでの実験時にいくつかのパターンを試しました。 全商品ベクトルの平均をモジュールのベクトルとみなし、 ユーザーベクトルとのコサイン類似度をモジュールのスコアとする 全商品ベクトルの各次元でmaxをとったベクトルをモジュールのベクトルとみなし、 ユーザーベクトルとのコサイン類似度をモジュールのスコアとする ユーザーベクトルと各商品ベクトルのコサイン類似度を算出し、 得られたコサイン類似度の平均値をモジュールのスコアとする ユーザーベクトルと各商品ベクトルのコサイン類似度を算出し、 得られたコサイン類似度の最大値をモジュールのスコアとする 上記のスコア算出方法に対して定量・定性評価を実施した結果、案4の算出方法が定量的・定性的に優れていたことから案4を採用しました。 設計時は案3を採用する想定でしたが、定性評価をしてみると意図しない・もしくは意図がわからない推薦をしているものが多い結果となっていました。これはモジュールに掲載される商品では必ずしも統一感があるわけではなく、そうした商品のスコアの平均を取るとスコアが平滑化されてしまうことが原因と考えています。 商品スコアの最大値を取ることで「ユーザーの興味を惹く商品が少なくとも1つ以上ある」という条件を満たすモジュールを優先的に表示することになります。しかし、必ずしもそうした商品がモジュールの先頭に表示されているわけではないので、商品の表示位置を考慮した重み付けなど改善の余地があると考えています。 システム構成 Two-Towerモデルの学習から予測結果のサービングまでを含めた全体のシステム構成は以下のようになっています。 パーソナライズシステムの構成図 モジュール並び順を予測するためにVertex AI Pipelinesを用いたバッチシステムが3つ存在します。以下ではそれぞれのシステムの役割を説明していきます。 1.Two-Towerモデル学習パイプライン このパイプラインは定期的にTwo-Towerモデルを学習し、学習済みモデルをVertex AI Model Registryに登録することを目的としています。先述した通りTwo-Towerモデルからはユーザーと商品それぞれをベクトル化するエンコーダーが得られるため、後続システムではこれらのエンコーダーを使ってベクトル化を行います。図中ではユーザーと商品それぞれのエンコーダーを User tower , Item tower と記述しています。 2.商品ベクトル化パイプライン このパイプラインは、モジュールに表示される商品のベクトル化を目的としています。モジュールに表示される商品の情報は商品検索用のAPIから取得し、Item towerを使ってこれらの商品情報をベクトル化します。ベクトル化された商品情報はBigQueryに保存され、後続システムであるモジュール並び順予測パイプラインで使用されます。 運用上の理由から、モジュールに表示される商品リストは1日に複数回更新されることがあります。そのためこのパイプラインは1日に数回実行し、モジュールに紐づく商品ベクトルが最新の状態となるようにしています。 3.モジュール並び順予測パイプライン モジュールの並び順を予測するためのパイプラインです。Vertex AI Model Registryに格納されている学習済みのUser towerを使用し、BigQueryから取得したユーザー情報をベクトル化します。またBigQueryにはモジュールに紐づく商品のベクトルも格納されているため、これらのベクトルを使ってモジュールのスコアを計算し、モジュールの並び順を決定しています。 予測結果はBigtableに保存され、リアルタイムにリクエストを処理する推薦APIが会員IDごとのモジュール並び順を取得する構成となっています。 A/Bテスト 概要 モジュール並び順のパーソナライズによる効果を評価するために、ZOZOTOWN会員を対象として約1か月のA/Bテストを実施しました。Control群とTreatment群を比較すると以下のような変化が生じます。 今回の施策ではすべてのモジュールがパーソナライズされるわけではなく、ビジネス的な要望から常に特定の位置に固定表示されるモジュールがある点に留意ください。例えば上図の「モジュールA(固定)」と記載されているモジュールはA/Bの割り振り関係なしで上から2番目に固定表示されています。 結果 A/Bテストの結果サマリを以下に示します。 指標 備考 結果(T/C比) ホーム画面訪問者の受注金額 ホーム画面にランディングしたユーザーの合計受注金額 100.1 (%) モジュール経由の受注金額 モジュールに表示されている商品の合計受注金額 100.1 (%) モジュール経由の受注点数 モジュールに表示されている商品の合計受注商品点数 100.4 (%) モジュール掲載商品のインプレッション数 モジュールに表示されている商品の合計インプレッション数 105.2 (%) モジュール掲載商品のクリック数 モジュールに表示されている商品の合計クリック数 103.2 (%) 受注系の指標はControlと比較して大きなアップリフトはありませんでしたが、 モジュールに掲載されている商品のインプレッションや閲覧に関する指標は大きく改善している ことがわかります。このことから、ユーザーはモジュールに興味を持ってホーム画面を回遊してより多くのモジュールを閲覧し、モジュールから商品ページに遷移する頻度が増えたと考えられます。 一方で受注系の指標に特段の変化がなかったことから、購入するほどの商品は提示できていなかったか、1か月という期間ではユーザーの購入体験を変えるほどの効果は計測できなかったと考えられます。 受注系の指標まで改善すれば理想的な結果でしたが、まずはプロジェクトの目的である「 モジュールのクリック数増大 」は達成できたと言える結果となりました。 A/Bテストを経て挙がった課題 上述した課題点も含め、A/Bテストを経て挙がった定性・定量的な課題と改善案について簡単に紹介します。 定性的な課題 「訴求軸は同じだが並んでいる商品が微妙に異なるモジュール」が連続して表示される 見たことのないカテゴリの商品が表示されにくい どちらの課題もモジュールのスコア算出で使用しているTwo-Towerモデルの最適化方針に原因があると考えられます。Two-Towerモデルは「次に閲覧する商品を予測する」という問題設定で学習しているため、ユーザーが直近で閲覧しそうな商品を提示することには成功しています。しかし、 その結果モジュールに掲載される商品が同じカテゴリやブランドに偏ってしまう という傾向が確認されました。 極端な例では以下のように全く同じ商品がファーストビューに並んでいるケースを確認しています。 この課題はアルゴリズムの設計時点で考慮事項として挙げられていたものの、日々の運用でカバーできるものと判断して対策をしていませんでした。 改善案として同じ商品が並んでいるモジュールを除外するルールベースのアプローチや、 MMR(Maximal Marginal Relevance) といったナイーブなリランキング方法の導入が考えられます。また今回はパーソナライズ方法としてモジュールのスコアを降順に並べただけのシンプルな方法を採用していますが、機械学習モデルでリランキングするなどの方法も考えられ、まだまだ改善の余地を残しています。 定量的な課題 受注系の指標がアップリフトしていない 特定カテゴリで各種指標が低下している 受注系の指標がアップリフトしていない原因として、前述の通りモジュールに表示される商品がユーザーにとって購入意欲を喚起するほどのものではなかったことが挙げられます。この課題の対処法としてパーソナライズロジック自体を改善するのはもちろんですが、 パーソナライズに使用されるモジュールの訴求軸を多様にする というアプローチが考えられます。 今までは全てのユーザーに同じモジュールを表示する都合上マス向けの訴求に偏る傾向がありましたが、これからは多種多様な訴求軸でユーザーに合ったモジュールを提示できます。この「多種多様なモジュール」を企画するのはビジネスサイドの企画チームであるため、モジュール並び順のパーソナライズを改善するには企画チームとの密な連携が欠かせません。 2つ目の課題はユーザーに閲覧される頻度が多くないカテゴリに関して閲覧や受注系の指標が悪化している、というものです。今まではモジュールの企画チームがその時訴求したい内容をモジュールで表現してユーザーに提示することで、カテゴリごとの閲覧や受注のばらつきが少ない状態でした。モジュール並び順がパーソナライズされた後はそうしたカテゴリごとのばらつきなどが意識されていないため、特定の期間で特定カテゴリの訴求を強める仕組みなどを検討しています。 最後に 本記事ではZOZOTOWNのホーム画面に表示するモジュールの並び順をパーソナライズするシステムとその効果について紹介しました。今回取り上げた部分以外にも改善すべき箇所が大量にあるので、これからも1つずつ改善していくことでユーザーにとってより良いZOZOTOWNを提供できるよう邁進していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、技術本部 SRE部 基幹プラットフォームSREチームの斉藤です。普段はZOZOの持っている倉庫システムやブランド様が触る管理ページなどのサービスのオンプレミスとクラウドの構築・運用に携わっています。またDBREとしてZOZOTOWNのデータベース全般の運用・保守も兼務しております。 7月11日、12日に行われた「 db tech showcase 2024 」に、DBREから5名のエンジニアが参加しました。この記事では会場の様子と印象に残ったセッションについてご紹介します! db tech showcaseとは 会場の様子 セッションレポート おわりに db tech showcaseとは 国内最大規模のデータとデータベース関連のカンファレンスです。このイベントでは、データベースの専門家やエンジニア、IT業界のリーダーたちが一堂に会し、新しい技術やソリューション、事例、ノウハウを共有します。 db tech showcaseの主な内容には以下のようなものがあります。 講演:データベース技術の最新トレンド、事例や実践的なアプローチについての講演が行われます。 展示:企業やベンダーが最新の製品やサービスを展示し、デモンストレーションします。 ネットワーキング:業界の専門家や同僚と交流する機会が提供され、情報共有やビジネスの機会が広がります。 www.youtube.com 会場の様子 入口 db tech showcase 2024はTKP市ヶ谷カンファレンスセンターで開催されました。受付を終えるとパスが発行され、イベントのパンフレットが配布されました。 EXPO データベースに関連する企業のブースが並んでいました。セッションの間で立ち寄ったOracle社のブースではHeatWaveの話題で盛り上がり、セッションさながらの事例や最新情報を熱弁頂きました。他にも、休憩スペースでセッションの感想で盛り上がってる人達がいたり、ミニセッションも開催されていたりしました。EXPOエリアではこうした交流が盛んに行われいて会場全体に活気がありました。 セッション 2日間で75以上のセッションが行われました。 db tech showcase 2024セッションスケジュール その他 企業の各ブースでは魅力的なグッズを配布していました。 セッションレポート DBREのメンバーが気になったセッションを紹介します。 MySQLアプリケーション開発を爆速にする最新手法 Amazon Bedrockで検証!データベース移行時のクエリ修正を簡素化するLLMの活用法 爆速成長中のFindyがぶつかった課題と解決に向けた実践手法 【出光興産様事例紹介】SAP DB運用の課題と今後の計画 SQL Server 技術支援のための動作の調査 - dbts2024 版 - MySQLアプリケーション開発を爆速にする最新手法 基幹プラットフォームSREチームの斉藤です。ZOZOのメインデータベースはSQL Serverを使用していますが、マイクロサービス化が進んでいるシステムではAurora MySQLを採用しています。MySQLの最新情報や運用に使用できるツールの事例などをキャッチアップすべく本セッションに参加させていただきました。 MySQLとVS Codeの統合 VS Codeで利用できるオープンソースの拡張機能として、MySQL Shell for VS Codeが提供されています。MySQL WorkbenchのEOLが迫っている中で後継となるもので、MySQLの運用には欠かせない機能になっていくのではないだろうかと思います。MySQL WorkbenchからはSQLエディタやスキーマの確認・データの検索結果の表示など主要な機能は全て継承されていると思われます。さらに「MySQL Shell for VS Code」はただのMySQL Workbenchの代わりではなく、VS Codeを使用してきた開発者とMySQL Workbenchを使用してきた管理者が共通して使用できるIDEとして設計されています。NotebookというエディタからはSQLだけではなくJavaScriptやTypeScriptを実行し結果をグラフで表示できます。これらの機能を使用してシステム開発が効率化されていると感じました。 MySQL Shell for VS Codeついては公式ドキュメント「 MySQL Shell GUI / MySQL Shell for VS Code 」を参照してください。 MySQLのJavaScriptサポート MySQL Enterprise Editionの機能でストアドプロシージャや関数をJavaScriptで作成し実行できます。そのストアドプロシージャや関数は実行するSQL上でも呼び出すことができ、処理の幅が広くなりました。 MySQLでJavaScriptを使用するできるメリットは次の通りです。 高い表現力 JavaScriptアプリケーション開発者向けの大規模なツールのエコシステム サードパーティライブラリの充実 人気のある開発言語の1つで、1300万人もの開発者が存在する MySQLのJavaScriptサポートついての情報はThe Oracle MySQL Japan Blogの「 MySQLのJavaScriptサポートについて 」で詳しく紹介されております。 MySQL REST Service MySQL REST ServiceはMySQL Routerの拡張機能として提供されています。RESTを使ってHTTP経由でMySQLのデータを参照、更新ができます。MySQL Shell for VS Code上から設定し運用します。 基本的なアーキテクチャ 画像引用元: The Oracle MySQL Japan Blog「 MySQL REST Serviceの紹介 」 MySQL REST Serviceついての情報はThe Oracle MySQL Japan Blogの「 MySQL REST Serviceの紹介 」で詳しく紹介されております。 感想 MySQLは「MySQL Shell for VS Code」やJavaScriptサポート、「MySQL REST Service」などの最新機能により、開発効率と運用性が大幅に向上し、現代のアプリケーション開発に重要なソリューションへと進化を続けていると感じました。 Amazon Bedrockで検証!データベース移行時のクエリ修正を簡素化するLLMの活用法 カート決済SREの遠藤です。 DBのバージョンアップや異なるDBへリプレースする際、クエリの互換性が課題になることがあります。この課題に対し、昨今注目されるLLM(大規模言語モデル)を活用してクエリの修正工数を減らせるかの検証デモが行われました。 現在、ZOZOTOWNで利用しているSQL Serverのサーバー分離とバージョンアップを担当しており、LLMを使ったクエリ修正に興味がありこのセッションに参加しました。 内容 異なるDBMS製品への移行や、バージョンアップに伴うクエリ修正の例が5つほど紹介されました。まず、何も工夫をせずLLMに対してクエリ修正を指示した場合、半分以上が間違った回答でした。こうしたハルシネーション(生成AIの嘘)を防止するために、よくある工夫をプロンプトに取り入れることで精度が向上しました。 クエリ互換性に関する内容であることの記述 専門家としての視点の強調 思考の連鎖を促す指示(step by step) 事例の提供(Few-Shotプロンプティング) また、RAG(検索拡張生成)としてAmazon Bedrockを使い、DBMS製品の公式リリースノートをナレッジベースとすることでさらに精度が向上しました。 Bedrockの場合、ナレッジベースをS3に格納するのですが、以下のような加工をすることでさらに精度が向上しました。 リリースノートからSQLの互換性に関係のない記述を削除 Markdown形式に変換 最終的に、5つのデモ全てにおいてクエリを正しく修正できていました。100%の精度は保証されないものの、LLMはSQL変換タスクにも有用であり、修正工数を大幅に減らすことができそうです。 感想 ChatGPTをはじめとしたLLMはすでに業務で活用していますが、ここまで具体的にDB移行業務に活用できる例を見ることができ、新たな発見がありました。LLMでは100%正しい回答が得られるとは限らないため、人力でのチェックは必須です。そこでRAGを利用することにより、ナレッジベースを元に回答の根拠も示してくれるため、ハルシネーションが発生しても比較的簡単に正当性を判断できそうです。 今回のセッションではOracleDBからPostgreSQLへの移行でしたが、弊社ではSQL ServerやMySQLを採用することが多いためそちらでも試してみたいと思いました。また、弊社ではテーブル設計ガイドラインを作成し、日常的にDBREがテーブル設計レビューを行っています。ガイドラインをナレッジベース化することで、テーブルのCREATE文を元に一次レビューを自動化できるかもしれません。 アプリケーション開発ではGitHub CopilotをはじめとしたLLMによる開発のサポートが既に充実しています。しかし、今後SQLの開発にも同様のことが行えるようになり、DBA/DBREの働き方も大きく変わってくると感じました。 爆速成長中のFindyがぶつかった課題と解決に向けた実践手法 基幹システム本部・物流開発部の黒岩です。 普段はZOZO社内で使用している基幹システムのリプレイスをする傍ら、DBREとしても活動しています。db tech showcaseは今回初めての参加でしたが、どの発表も内容が幅広く、興味深いセッションが多かったです。 私はファンディ株式会社 @gessy0129 げっしーさんの「爆速成長中のFindyがぶつかった課題と解決に向けた実践手法」というセッションについて紹介します。 DB面での課題と解決方法 サービスが急成長し、多くのユーザーに使われていく中で直面したDB面での課題とその解決方法が紹介されました。 .tal{text-align:left !important;} 性能問題 解決方法 DB負荷が高まり、CPU使用率が上昇 RDSからAuroraに移行してリソース利用を効率化 AuroraをARMベースのインスタンスに変更し、同じ性能をより低コストで実現 SQLのレスポンスが悪化 Performance Insighsを活用してクエリパフォーマンスを把握し、ボトルネック箇所を可視化 クエリの実行計画を分析して足りないインデックスを特定、インデックスを追加 インデックスが使われないクエリの形を修正(例:IN句の中に大量のデータが入っている、LIMITの数が適切でない場合など) DBエンジニアとアプリケーションエンジニアの協力 セッションで印象に残ったのは、スピーカーのげっしーさんが言った「性能試験は総合格闘技」という言葉でした。この言葉通り、DBエンジニアだけで解決するのではなく、アプリケーションエンジニアと密に連携して問題解決に取り組んだ例が紹介されていました。具体的には、以下2つの取り組みが印象的でした。 ReaderとWriterのインスタンスノードの振り分け AuroraのReaderインスタンスが有効に活用されていない課題に対して、DBエンジニアとアプリケーションエンジニアがペアプロしながら解決した事例です。DBエンジニアがReaderインスタンスの役割や性能向上の仕組みを説明し、アプリケーションエンジニアがその知識をもとにアプリケーションコードを改修して、書き込み操作時にはWriterインスタンス、読み込み操作時にはReaderインスタンスへ分けるようにしたそうです。これにより、読み込み処理時にReaderインスタンスが使われるようになり、効率的な負荷分散を実現したとのことでした。この取り組みでは、両者がそれぞれの専門知識を共有し、リアルタイムでコードの変更とその効果を確認しながら進めたことが成功の鍵となったそうです。 アプリケーションコード内のクエリ最適化 もう1つの課題として、より複雑な処理をクエリで一括処理するか、データを分割して取得した上で、アプリケーション側で処理するかという課題があったそうです。ここでも相互にペアプロなどで検証しながら性能を検証し、例えば「MySQLが苦手とする集計処理」について、クエリで一括処理していた部分を一部アプリケーション側で処理することで、DBの負荷を軽減した例が紹介されていました。 感想と今後の取り組み このセッションを通じて、DBエンジニアとアプリケーションエンジニアの協力がDBの負荷軽減において非常に重要であることを再認識しました。現在、弊社でも基幹DBの負荷軽減を図るためにクエリチューニングを進めていますが、この取り組みはまだ効果が限定的です。今回のセッションで学んだように、DBの性能課題に真摯に向き合い、基幹システムに関わるチーム全体で性能問題解決に取り組む姿勢を大切にし、今後もさらなる協力と改善を進めていきたいと思います。 【出光興産様事例紹介】SAP DB運用の課題と今後の計画 基幹システム本部・データ連携ブロックの馬場です。現在ZOZOTOWNの基幹システムで使用しているデータベースの性能改善を課題としています。 社内システム統合時に発生したデータベースのスローダウンに対し、どのようなツールを利用し課題対応しているか、という事例紹介のセッションに参加しました。 MaxGauge スローダウンが発生しているボトルネッククエリを調査すべく、データベース監視ツールである「MaxGauge」が紹介されました。「MaxGauge」はデータベースサーバにエージェントを導入することで、リアルタイムにSQLのトラッキングを行い、蓄積されたトラッキングデータにより、パフォーマンス低下対象の事後分析が可能です。 弊社ではDPMツールとして「Datadog」を導入していますが、情報収集に掛かるデータベース側への負荷や、クエリの取得間隔等を比較してみたいです。 MAJESTY 次にデータベース開発支援ツールの「MAJESTY」の紹介です。パフォーマンスが低下しているクエリに対し、最適なインデックス設計をアドバイスしてくれることで効率的に性能分析・改善が可能です。最大の特徴は「アクセスパターン分析」を実装しており、テーブルへのアクセスをパターン化し、最適なアクセスパスを分析することです。これにより、SQL単体を分析する手法よりも大幅にコストを抑えた性能改善が実現可能です。 感想 今回の事例ではSQL Serverを利用し、基幹システムに性能問題を抱えている境遇が弊社と似ているため参考になるセッションでした。 チューニングを行うにはデータベースに対し一定の習熟度が必要になりますが、社内のエンジニア全員が同じ習熟度を保つことは難しいです。本セッションで紹介されたツールを導入することにより、誰もが同じ視点で性能改善を実施し、日々の性能改善コストが大幅に削減できる環境が作れると良いと思います。 SQL Server 技術支援のための動作の調査 - dbts2024 版 - 技術本部SRE部カート決済SREブロックの伊藤です。 こちらのセッションはMicrosoft MVPの小澤さんが、SQL Serverの挙動について、実演を交えて調査方法を紹介してくれるセッションとなっていました。例えば、クエリストアや拡張イベントを有効化する場合にクエリのスループットにどの程度影響が出るかをどのように調べるか、といった内容です。 内容が濃密なこともあり、事例全てを紹介していただくことは時間の都合上できませんでしたが、会場を巻き込んで挙手制でアンケートを取りながら紹介事例を選ぶ姿は、オフラインイベント感を感じる一幕でした。 事例の中で私が最も印象に残ったのは「オンラインでのインデックス再構築による同時実行性の低下の有無」です。 オンライン再構築による同時実行性の低下の有無 SQL Serverではオンラインでのインデックス操作に対応しています。 ONLINE オプションは、このようなインデックス操作中に基になるテーブルまたはクラスター化インデックス データ、ならびに関連付けられた任意の非クラスター化インデックスへ同時ユーザー アクセスを可能にします。 たとえば、あるユーザーがクラスター化インデックスを再構築している最中に、そのユーザーと他のユーザーが基になるデータの更新やクエリを続行できます。 引用元: オンラインでのインデックス操作の実行 この時に、ロックは一切かからないのかを解説交えながら検証の実演が行われました。具体的には以下のような手順です。 長時間かかるようなSELECT文を実行する SSMSから実行するときには結果を出力しないようにクエリオプションで破棄する設定にしておく この時 sys.dm_tran_locks などでロックの状況を確認すると共有ロックがかかっている状態が見られる バックグラウンドでSELECTを大量に実行しておく Ostress を使うことで並列に大量のクエリを流し続けておくことができる REBUILD INDEX WITH(ONLINE=ON)を実行する 最終段階でSch-Mのロックが必要になるため1の処理と競合してロックの解放待ち状態となる この時に2で実行していたクエリをブロックする可能性がある 発生の有無はロックパーティションの状態次第 CPUのコア数で挙動が変わる部分でもある ロックタイムアウトの活用でブロッキングの連鎖は防ぐことができる 後日実際に自分の環境でも試してみた 上記の内容を改めて実際に試してみました。これは過去にオンラインでインデックス作成が行われた際にブロッキングの発生を観測したことがあり、ユーザーに影響するものなので対策含めて運用ドキュメントに取り入れていきたいという思いがあったからです。 実際に流したクエリは以下の通りです。 -- 長時間かかるSELECT文 SELECT a.SampleColumn FROM _testTable AS a CROSS JOIN _testTable AS b -- Ostressから実行したバックグラウンドで動かすSELECT文 SELECT TOP( 1 ) * FROM [dbo].[_TestTable] tablesample( 1 percent) WITH (NOLOCK) -- インデックスのリビルド ALTER INDEX [IX__TestTable_SampleIndex] ON [dbo].[_TestTable] REBUILD WITH ( ONLINE = ON ) GO 上記のクエリをそれぞれ流したところ、インデックスリビルドの最終段階でSch-Mのロック要求が発生していることを確認できました。 sys.dm_tran_locksから確認したロック情報 Datadogが入っている検証環境ということもあり、Datadogからも確認したところ、ブロッキングが連鎖してOstressで流していたクエリも巻き込まれていることが確認できました。 Datadog Database MonitoringのBlocking Queriesからのキャプチャ これに対し、インデックスリビルド時に SET LOCK_TIMEOUT を付けて同様に再現したところロック要求のタイムアウトの発生を確認できました。インデックスのリビルドは失敗するため、改めて実施する必要はありますが、これでユーザー影響を抑えることができます。 -- 1秒ロックが取れなかったらインデックスのリビルドを中断する SET LOCK_TIMEOUT 1000 ; ALTER INDEX [IX__TestTable_SampleIndex] ON [dbo].[_TestTable] REBUILD WITH ( ONLINE = ON ) GO エラーメッセージが出力されて処理が中断 実演を見た上で自分でも試してみて、インデックス操作時の挙動及び調査方法への理解を深められました。インデックス操作時の SET LOCK_TIMEOUT 指定の必須化などは弊社内での運用にも取り入れていこうと思います。 おわりに 今回のセッションを通じて多くの知見を得ることができました。セッション以外にも、企業ブースや他社のエンジニアとの交流を通じて、多くの学びと刺激を受けました。実際に現地に足を運ぶことで得られるものは非常に大きかったです。 db tech showcase 2024で学んだことをZOZOのデータベースシステムの向上に活かしていこうと思います。 ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com