TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

988

こんにちは。ZOZOTOWN部フロントエンドチームの菊地( @hiro0218 )です。 2021年3月、ZOZOTOWNは10年ぶりのリニューアルをしました。この記事では、そのリニューアルで再考したCSS設計について紹介します。 背景 今回のリニューアルでは、ウェブとアプリが部分的に共通のデザインになりました。 アプリ ウェブ このデザイン刷新には、CSSの大規模変更が必要です。チーム内で検討を重ね、最終的に、大きく書き換えるのであればコンポーネント駆動開発 1 ができるようにCSS設計を見直すべきという結論に至りました。 CSS設計で特別に考慮する点 現在、ZOZOTOWNのフロントエンドは、「Classic ASP」から「 React 」へのリプレイスを進めています。新規開発や変更のタイミングで、Classic ASPに依存した実装をReactへ改修します。 ただ、今回のリニューアルではClassic ASPをReactへリプレイスする時間的余裕がなく、見た目は共通でもClassic ASPとReactが混在する実装にせざるを得ませんでした。Classic ASP側にドラスティックな変更を入れず、かつシステム全体を通してはコンポーネント駆動開発を実現する、そのようなCSS設計が必要でした。 設計 体制 現在、ZOZOTOWNのWebフロントエンドは3チームに分かれています。全てのチームで共通の設計を採用できるよう、各チームから1人ずつCSSリーダーを選出し、合計3名で新たなCSS設計をしました。実装や運用に伴って発生する問題もCSSリーダーが取りまとめ、都度設計に反映、コードやルールに落とし込むという体制です。 要件 既存のCSSでも一部はコンポーネントごとに定義がまとめられています。しかしながら、仕様追加の積み重ねによってカスケードが多重化され、CSSファイルからその状態を把握することが難しくなっていました。 一方、今回のリニューアルプロジェクトはスケジュールが逼迫しており、チームを横断して多数のメンバーが同時期に実装することが必要でした。これらを踏まえ、設計の要件を下記のように定めました。 作業分担しやすいこと 複数のメンバーが同時に作業をしても競合しづらい 保守性が高いこと リプレイス中のClassic ASPとReactで共通のルールが採用できる CSS全体を一貫したルールで記述できる 導入コストが低いこと Classic ASPの変更を最小限にできること CSSには様々な設計手法がありますが、以上の要件を満たしつつ現状のZOZOTOWNにマッチする設計手法は、「ITCSS」という判断に至りました。 ITCSSとは ITCSSは、 CSS Wizardy の Harry Roberts 氏が提唱したCSSの詳細度を管理する設計思想です。 「Inverted Triangle CSS (逆三角形のCSS)」の略で、設定の詳細度順に階層化して記述します。7つのレイヤーが定義されており、この記述が逆三角形として可視化されます。 ITCSSのレイヤー Settings Tools Generic Base Objects Components Trumps 「CSSプリプロセッサなどで利用する変数や設定」であればSettings、「OOCSSの概念に基づいた定義」であればObjects、というように各レイヤーの役割が決まっています。 なお、ITCSSのレイヤーは、必要に応じて追加・削除することも許容されます。 CSSプリプロセッサを利用していない場合、SettingsやToolsなどのレイヤーを削除 OOCSSを使用していなければObjectsレイヤーを削除 テーマ性が必要であればThemeレイヤーを追加 といった調整も可能です。今回のリニューアルプロジェクトでは標準のITCSSに加えて、カスタムレイヤーを加えました。詳細は後述します。 各レイヤーは次の性質を持っています。 下位レイヤー(図の下側)ほど詳細度が上がる 上位レイヤーが下位レイヤーを上書きしない この性質は、複数人での同時作業を助けます。 複数の作業者が同時に作業しながら破綻を避けるには、定義や分割粒度を設計者以外でも同じように行えることが重要です。それはITCSS以外の設計手法を用いても可能ですが、ITCSSの場合は各定義の責務を理解しやすいのが大きなメリットだと感じました。 例えばAtomic Designは、デザイナー・エンジニア間で浸透した設計思想であるため、共通の思想で各インタフェースの責務を分割できるのが大きなメリットです。ただ、デザイナー同士でも責務分割の粒度が異なるケースもあります。また、デザイナーとエンジニアでも言語や仕様などの都合によって、責務分割の粒度が大きく異なることも少なくありません。さらにはエンジニア同士の責務分割の粒度が異なってしまうことも当然あり、そうなるとソースコードの混沌は避けられません。 デザインの意図を正確にソースコード(CSS)に落とし込むことができればCSSの破綻は防げるのですが、デザイナーと実装者が同じでない限り、例に上げたように伝達の過程で歪みは生じやすくなります。その点、ITCSSでは定義の責務が明確で個人の判断に任せる部分が少なく、一貫した設計が可能です。 コンポーネントの命名規則 ITCSSでは、コンポーネントごとの命名規則が定められていません。私達はMindBEMding(BEM)に接頭辞を組み合わせることにしました。 MindBEMding(BEM) MindBEMding は、BEMから派生した命名規則です。こちらもITCSSと同様にHarry Roberts氏が提唱しています。 下記の命名パターンで構成します。 名称 説明 .Block 親要素 / 独立した要素 .Block__Element Blockに紐付いた要素 / Block内でいくつも存在できる .Block--Modifier .Block__Element--Modifier バリエーションや状態を変化させるときに指定する 命名は基本的なBEMに準じていれば良しとしています。ただし、 .Block__Element__Element という命名パターンだけは、構造が複雑になり見通しも悪くなることが明らかであったため採用していません。 BEMを利用した命名のメリットに「クラス名からクラスが持っている役割が分かりやすくなる」という点がありますが、さらにITCSSとの組み合わせによって役割と責務が一見して分かりやすい構造になります。共通の命名規則が決まっていることで実装とレビューが円滑にできるようになりました。 接頭辞 今回のリニューアルは、広範囲に渡る大規模な改修ですが、それでもCSSを全て置き換えるわけではありません。そのため、命名によっては既存CSSと競合する可能性があります。そこで競合しないように接頭辞を整備しました。 ITCSSのレイヤーに応じた接頭辞を付与します。これにより接頭辞を一目見ただけで役割を把握できます。また、レイヤー同士の名前衝突も避けられます。次の表は、接頭辞の例です。 レイヤー名 接頭辞 Objects .o- Components .c- Trumps(Utility) .u- 既存のCSSに接頭辞が基本的に付いていなかったこともあり、副次的なメリットとして、ソースコードを一見して新旧コードが分かりやすくなりました。 ITCSSと命名規則(MindBEMding + 接頭辞)の組み合わせで実現できること 以下のような設計が実現できます。 ITCSS レイヤーに沿うだけで詳細度が管理できるため、破綻しにくく、保守しやすいコードを書ける レイヤーが分かれているため、複数メンバーが同時に開発しても競合しづらい 管理方法が分かりやすいため、設計者以外のメンバーでも定義のズレがなく、分割粒度が揃いやすい MindBEMding + 接頭辞 クラス名を一見して定義のもつ役割が分かりやすい 命名のブレや迷いが少なくなる ITCSSのレイヤーに基づく接頭辞を使うことで、コンポーネント同士で命名の衝突が少なくなる 元より接頭辞がない既存CSSとは衝突しない 詳細 最終的に次のように設計しました。 ITCSSディレクトリ構造例 style ├── Settings │ ├── _colors.css │ ├── _variables.css │ └── ... ├── Tools │ ├── _animation.css │ ├── _mixins.css │ └── ... ├── Generic │ ├── _font.css │ ├── _reset.css │ └── ... ├── Base │ ├── _global.css │ └── ... ├── Layouts │ ├── _grid.css │ └── ... ├── Objects │ ├── _form.css │ └── ... ├── Vendor │ ├── _swiper.css │ └── ... ├── Components │ ├── _breadcrumbs.css │ ├── _button.css │ └── ... ├── Model │ ├── _pagination.css │ └── ... ├── Site │ ├── _drawer.css │ ├── _header.css │ ├── _footer.css │ └── ... ├── Pages │ ├── _home.css │ ├── _goods.css │ ├── _cart.css │ └── ... └── Trumps ├── _text.css └── ... CSS命名例 /* Objects */ .o-scroll {} .o-scroll__container {} /* Components */ .c-catalog {} .c-catalog-header {} .c-catalog-body {} .c-catalog-body__title {} /* Model */ .m-catalog-scroll {} .m-catalog-scroll__item {} 順を追って説明します。 レイヤー構造 ZOZOTOWNでは多くのページを管理する必要があり、責務をより明確にするにはデフォルトのレイヤーだけでは足りなかったため、独自のレイヤーを追加しました。 下記のようなレイヤー構成にしています(太字のものが追加したレイヤーです)。 # レイヤー 役割 追加レイヤーの役割 1 Settings CSSプリプロセッサなどで利用する変数や設定 〃 2 Tools CSSプリプロセッサで利用する mixin や function などの定義 〃 3 Generic リセットスタイルや固有のリセットスタイル定義 〃 4 Base 素のHTML 要素のスタイル定義 〃 5 Layouts - ページ間で共通の大きなレイアウト定義 6 Objects OOCSSの概念に基づいた定義 〃 7 Vendor - 外部ライブラリから提供される固有のスタイルを定義 8 Components 再利用可能なコンポーネント(UIパーツ)を定義 〃 9 Model - コンポーネント同士の組み合わせやコンポーネントの粒度に満たない汎用的なUIの定義 10 Site - サイトを横断的に利用されるUIの定義 11 Pages - ページ固有の定義や上位のレイヤーの定義を上書きするような定義 12 Trumps(Utility) ヘルパー・ユーティリティ系の汎用スタイルを定義 〃 各レイヤーの利用方法の詳細は、以下の通りです(各種コードはサンプルです)。 Settings このレイヤーには、CSSプリプロセッサなどで利用する変数や設定を配置します。 CSS Custom Propertiesの定義もこのレイヤーで行いました。 $font-family: -apple-system , BlinkMacSystemFont , "Segoe UI" , Roboto , "Helvetica Neue" , Arial , sans-serif , "Apple Color Emoji" , "Segoe UI Emoji" , "Segoe UI Symbol" , "Noto Color Emoji" !default; $color-ui: #bada55 ; $spacing-unit: 10px; Tools このレイヤーには、CSSプリプロセッサで利用する mixin や function などの定義を配置します。CSSプリプロセッサを利用していない場合は不要かもしれません。 @function str-replace($string , $search , $replace: "" ) { $index: str-index($string , $search); @if $index { @return str-slice($string, 1 , $index - 1 ) + $replace + str-replace( str-slice($string, $index + str-length($search)), $search, $replace ); } @return $string; } @mixin font-brand() { font-family : "UI Font" , sans-serif ; font-weight : 400 ; } Generic このレイヤーには、リセットスタイルや固有のリセットスタイル定義を配置します(低詳細度で広範囲に当たる定義)。 @import "reset.css" ; * , * :: before , * :: after { -webkit- box-sizing : border-box ; -moz- box-sizing : border-box ; box-sizing : border-box ; } Base このレイヤーには、素のHTML要素のスタイル定義を配置します。クラスセレクターなどは使用せず、 a , h1…6 , ul…li などの要素セレクターのみで構成します。 ul { list-style : square outside ; } Layouts このレイヤーは独自に追加したレイヤーです。 このレイヤーには、ページ間で共通の大きなレイアウト定義(グリッドなど)を配置します。余白や幅など装飾を持たないスタイルを定義します。 .l-main { margin : 0 auto ; width : 100% ; } Objects このレイヤーには、OOCSS(Object Oriented CSS)の概念に基づいた定義を配置します。Layoutsレイヤーとの違いとしては、ページ内で繰り返し使えるものを想定しています。余白や幅など装飾を持たないスタイルを定義します。 .o-ui-list { margin : 0 ; padding : 0 ; list-style : none ; } .o-ui-list__item { padding : $spacing-unit; } Vendor このレイヤーは独自に追加したレイヤーです。 このレイヤーには、外部ライブラリから提供される固有のスタイルを読み込み、外部ライブラリが定義しているスタイルを上書きするための定義を配置します。 @import 'swiper/swiper-bundle.css' ; .swiper-module { overflow : hidden ; } Components このレイヤーには、再利用可能なコンポーネント(UIパーツ)を定義します。コンポーネントにマージンは持たせず、ObjectsやModelレイヤーとの組み合わせで余白は再現します。 .c-products-list { @include font -brand(); border-top : 1px solid $color-ui; } .c-products-list__item { border-bottom : 1px solid $color-ui; } Model このレイヤーは独自に追加したレイヤーです。 このレイヤーには、コンポーネント同士の組み合わせやコンポーネントの粒度に満たない汎用的なUIの定義を配置しています。 コンポーネント同士を組み合わせたい場面はよくあるものの、保守性の観点から、同レイヤー同士の組み合わせをルールで禁止しています。一方、組み合わせの定義を繰り返すと冗長になってしまうため、このレイヤーを設けています。中間層のレイヤーを導入したことにより、可読性の面だけではなく、詳細度の複雑さを和らげる効果もありました。 下記は、年月日のフォームの例です。 c-input の組み合わせで特殊なフォームを再現しています。このフォームは情報登録・編集画面の限られた共通パーツであったため、Modelレイヤーに定義をします。 .m-input-decoration-birth { display : flex ; @each $date-name, $date- label in (year: "年" , month: "月" , day: "日" ) { & __ # { $date-name } { display : flex ; flex : 1 ; align-items : center ; &::after { content : $date-label; width : 24px ; text-align : center ; } } } } < div class = "m-input-decoration-birth" > < div class = "m-input-decoration-birth__year" > < div class = "c-input" > < input type = "text" class = "c-input__text" > </ div > </ div > < div class = "m-input-decoration-birth__month" > < div class = "c-input" > < input type = "text" class = "c-input__text" > </ div > </ div > < div class = "m-input-decoration-birth__day" > < div class = "c-input" > < input type = "text" class = "c-input__text" > </ div > </ div > </ div > Site このレイヤーは独自に追加したレイヤーです。 このレイヤーには、サイトを横断的に利用されるUI(グローバルヘッダーやグローバルフッターなど)の定義を配置しています。コンポーネントとほぼ役割は変わりませんが、ページ内で一度登場するようなものをこのレイヤーにまとめています。 .s-header { display : flex ; align-items : stretch ; justify-content : space-between ; margin : 0 auto ; } Pages このレイヤーは独自に追加したレイヤーです。 このレイヤーには、ページ固有の定義や上位のレイヤーの定義を上書きするような定義を配置します。 これまでのレイヤーで定義したコンポーネントやレイアウトスタイルの組み合わせで完結しないような、ページ固有のUIパーツやコンポーネントのバリエーションを再現するのに使います。 Pagesレイヤーではカスケード用のクラスを用意して、トップレベルのHTML要素にクラス付与して実装を定義します。また、Pages固有の定義については、カスケード用のクラスを継承した名称で定義します。 .p-product-detail { background-color : $bg-color- gray ; .o-form-group { margin-bottom : 16px ; } .c-button { border : none ; } } .p-product-detail-heading { font-size : 28px ; margin-bottom : 8px ; } < body class = "p-product-detail" > < header class = "l-header" > < div class = "s-header" ></ div > </ header > < main class = "l-main" > < h1 class = "p-product-detail-heading" > heading </ h1 > < div class = "o-form-group" > < input type = "text" class = "c-input" /> < button type = "button" class = "c-button" > button </ button > </ div > </ main > < footer class = "l-footer" > < div class = "s-footer" ></ div > </ footer > </ body > Trumps(Utility) このレイヤーには、ヘルパー・ユーティリティ系の汎用スタイルを定義します。これまでのレイヤーよりもスコープが最も狭くなるよう1つのDOMだけに影響させるような定義をします。 本来は「Trumps(切り札)」という名称ですが、あまり馴染みのある表現ではなかったので、ZOZOTOWNでは「Utility」という名称で運用しています。 .u-text-color { color : $color- text -important !important ; } しかしながら、このTrumpsレイヤーは、本当に切り札として使うのが最適だと考えています。詳細度を管理する上での問題もありますが、汎用クラスはその実装から「どのような状態になるのか」を理解しづらいためです。例えば「色を変えたい」のか「セール価格として強調したい」のかをCSSの実装だけで汲み取るのは困難で、HTMLと前後の文脈を照らし合わせる必要があります。こういう理由から、汎用クラスは後のリファクタリグの際に実装を紐解いていく必要があり、使い方によっては負債になりがちな要素となります。 まとめ 以上が今回のリニューアルで再考したCSS設計の思想とルールです。 詳細度の管理のためにITCSSを、既存実装に影響を与えないようにクラス命名規則にMindBEMdingと接頭辞を採用しました。これらの導入によって、CSSの詳細度の管理が容易となり、設計に造詣が深くないメンバーでもITCSSのルールに則るだけで破綻しにくいCSS実装が可能となりました。まだまだリプレイスの途中ということもあり、過去の資産と共存するために記載以外でも拡張している箇所があります。しかし、ITCSSの柔軟性のおかげで追加仕様もその枠組で吸収できています。 なお、本記事では触れませんでしたが、ZOZOTOWNのCSSを取り巻く状況は以下のようになっています。 モジュールバンドラは webpack を利用 CSSプリプロセッサは PostCSS を利用 Classic ASPとReactが生成するHTMLは、同じグローバルCSSによってスタイリングされる React CSS ModulesをCSSクラス名の型チェックのために利用 CSSクラス名に付与されるsuffix指定は無効化[^webpack] [^webpack]: webpackの css-loader の options に modules.localIdentName: '[local]' を設定します。 CSS Modulesが生成する一意なCSSクラス名を排除しているものの、ITCSSと命名規則によってCSSクラス名に一意性が保たれるため、クラス名の衝突という点では全く支障ありませんでした。 CSS ModulesはCSSクラスの出力順序がJavaScript側の参照順に依存しているため、JavaScriptの変更でカスケード順が変わると予期せぬスタイル崩れが起こる恐れもあります。ZOZOTOWNではページによって表現パターンをカスケードして変えることが多く、クラス名をCSS Modulesに任せるよりも、CSS設計でクラス名を決定する方が安全に長期運用ができると判断しました。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからご応募ください! corp.zozo.com コンポーネント駆動開発とは、UI開発をコンポーネントから始めて、コンポーネントを徐々に組み合わせて最終的にページを作り上げていくボトムアップな開発プロセスのことです。コンポーネントを分離して構築するため、開発と設計の並列化による効率性の向上などのメリットが見込まれます。 ↩
こんにちは。EC基盤本部 検索基盤部 検索基盤チームの有村( @paki0o )です。 みなさん、Elasticsearchのマッピングはどこまで厳密に管理されているでしょうか。 弊社では以前のテックブログでご紹介した通り、一部を除き Explicit Mapping にてデータを管理しています。 techblog.zozo.com 設定している項目は、フィールド名・タイプ・適用するアナライザなど一般的な項目であり、詳細まで詰め切れているとは言い切れない状況でした。今回、マッピング設定の変更がパフォーマンスに与える影響を検証しましたので、その内容についてご紹介いたします。 背景と課題 マッピングの設定について index doc_values enabled 3項目の比較 検証 前準備 比較項目 検証結果 平日での比較結果 休日での比較結果 考察 まとめ 背景と課題 ZOZOTOWNの商品情報インデックスは数百万件のドキュメント、100以上のフィールドから構成されています。その中には、おすすめ順で並び替えるためのアイテム特徴量情報や人気順情報を表すフィールドなど、特定の用途でのみ利用するフィールドも含まれています。 またオペレーションの面では、週次で全件洗い替えを行うバッチや数時間毎に人気順情報を更新するバッチ、アイテム特徴量情報を更新するバッチなど、大規模な更新がかなり多いシステムとなっています。 ElasticsearchはUpdateクエリによるパーシャルアップデートをサポートしており、先のアイテム特徴量更新バッチでも利用しています。ただ、パーシャルアップデートは ドキュメント にもある通り、更新されたドキュメント全体のリインデキシングが走ります。人気順更新バッチやアイテム特徴量更新バッチは、全商品データをパーシャルアップデートするため、弊社のシステムは1日に複数回も全件洗い替えと同等の負荷がかかっている状況となっています。 また、ビジネス要件の実現や様々な検索改善への取り組みの一環で、日々新規のフィールドを追加しており今後も継続すると考えられます。現に今年度の頭には、これまで100項目ほどであったフィールド数を、150項目ほどまで増やす改修がありました。このようなフィールド数増加によるインデキシングパフォーマンスの低下は大きな課題です。今後も同規模の改修が検討されており、設定レベルでインデキシングを効率化できないか検証しました。 マッピングの設定について Elasticsearchのマッピングでは、フィールドごとに指定できるパラメータがバージョン7.13時点で27項目存在しています。 www.elastic.co 今回は、この中でもデータをどのように扱うかを指定する以下の3つのパラメータを設定し、パフォーマンスにどのような影響があるか検証しました。 index doc_values enabled index index は文字通り、該当のフィールドをインデキシングさせるかを指定するためのパラメータです。インデキシングを無効化したフィールドは検索対象への指定ができなくなるため、検索が不要なフィールドに設定します。デフォルトは true で、明示的に false を指定することで、インデキシングを無効化できます。 設定例は以下の通りです。 PUT /sample_index { " mappings ": { " properties ": { " sample_field ": { " type ": " integer ", " index ": false } } } } doc_values doc_values は各フィールドの doc_values を保存するかを指定するパラメータです。 doc_values はソートや集計クエリ、scriptクエリなどフィールド単位の処理が必要とされる際に利用される列指向なデータです。デフォルトは true で、明示的に false を指定することで、 doc_values 形式での保存を無効化できます。 設定例は以下の通りです。 PUT /sample_index { " mappings ": { " properties ": { " sample_field ": { " type ": " integer ", " doc_values ": false } } } } enabled enabled は入力されたデータを有効化するかを指定するパラメータです。これを無効にすると、インデキシングやその他の形式を含めてデータがストアされません。検索やソートなどの対象として一切利用できなくなりますが、例えばタイムスタンプやUUIDのような_sourceで確認さえできれば問題ないフィールドに対しては有効な設定です。デフォルトは true で、明示的に false を指定することで、フィールド全体を無効化できます。 注意点として、 enabled を false に設定する際には、フィールドの型をObject型に指定する必要があります。 設定例は以下の通りです。 PUT /sample_index { " mappings ": { " properties ": { " sample_field ": { " type ": " object ", " enabled ": false } } } } 3項目の比較 ここまで紹介した3項目について、それぞれ単体で指定した際にどのような用途でフィールドが利用できるのか・できないのかを以下の表でまとめました。 用途 index=false doc_values=false enabled=false 検索 × 〇 × script 〇 × × ソート 〇 × × 集計 〇 × × _source 〇 〇 〇 弊社でもソート・scriptだけに用いるフィールド、検索だけに利用できれば問題ないフィールドなど、その用途ごとに最適化の余地がありました。必要最低限の項目のみを設定し、どの程度インデキシング処理が変わるのか検証しました。 検証 前準備 各フィールドごとに適切なパラメータを設定する前準備として、現状の各フィールドと用途を洗い出し、必要な項目を整理しました。単純にクエリログから抽出可能であればスクリプトで処理可能でしたが、プラグインなどの諸事情によりクエリログからすべての情報を得ることが難しかったため、スプレッドシートにまとめる形を取りました。 まとめた結果、現状と理想形の設定差分は以下の通りとなりました。カッコ内の数字はそれぞれ全体のうちの割合を示しています。 項目 検証前の設定数 検証後の設定数 変化分 フィールド数 159 159 - index=false 65(40.8%) 79(49.6%) +14 (+8.8%) doc_values=false 5(3.1%) 55(34.6%) +50 (+31.5%) enabled=false 4(2.5%) 14(8.8%) +14 (+6.3%) 比較項目 変更前後のパフォーマンス計測はElasticsearchのMonitoring機能を用いて行いました。Monitoring機能で取れるメトリクスは非常に豊富なため、今回はその中からインデキシング関連のメトリクスに対象を絞り、以下の項目について比較検証を行いました。 項目名 内容 store.size_in_bytes インデックスサイズ (byte) segments.count セグメント数 segments.index_writer_memory_in_bytes Index Writerの使用メモリ (byte) indexing.index_time_in_millis / indexing.index_total インデキシング速度 (ms/document) なお取得タイミングによって値に差が出るため、それぞれ変更を適用する前後、同曜日の1日の平均値で比較しました。またECサイトの特性上、イベント事やセールなどの要因によりリクエストの傾向が異なるため、平日と休日からそれぞれ1日ずつ比較しています。 検索速度についても同様に計測を試みましたが、対象期間に検索クエリを変更するABテストを実施しており、正確な計測が難しいとの判断から今回は対象外としています。 検証結果 検証結果は以下の表の通りです。表中の「インデックスサイズ」と「Index Writerの使用メモリ」は、見やすいように単位を調整しています。 平日での比較結果 項目 適用前(平日) 適用後(平日) 変化分 インデックスサイズ (GB) 15.327 14.788 -3.5% セグメント数 37.862 35.764 -5.5% Index Writerの使用メモリ (MB) 48.106 44.199 -8.1% インデキシング速度 (ms/document) 1.210 1.138 -5.9% 休日での比較結果 項目 適用前(休日) 適用後(休日) 変化分 インデックスサイズ (GB) 15.650 14.773 -5.6% セグメント数 37.250 35.339 -5.1% Index Writerの使用メモリ (MB) 51.367 42.370 -17.5% インデキシング速度 (ms/document) 1.120 1.082 -3.4% 考察 今回計測したすべてのメトリクスにおいて改善している状態が確認できました。特に、Index Writerの使用メモリは変更を加えたフィールド数の割合と近い値で減少しており、適切な設定の重要さが伺えます。 一方で、今回の改修により一部フィールドが検索、もしくはソートや集計に利用できなくなったのも事実です。設定を戻し、インデキシングし直すことで再度有効化も可能ですが、データサイズによっては即座にできるものではありません。このあたり小回りを優先するかパフォーマンスを優先するかは要件に依存する部分かとは思いますが、弊チームではパフォーマンスを優先し、今回の改修を採用しました。 まとめ 本記事では、Elasticsearchへの継続的なフィールド数追加の要望に対応するため、設定の深堀りと見直しを行いました。取り組みの結果、パラメータを用途に応じて最適化することでインデックスのサイズやクラスタへの負荷、インデキシングにかかる時間を低減できることがわかりました。 今回は検索を主な用途とするクラスタで検証しましたが、例えばロギング用途のようなログの閲覧と集約さえできれば問題ないようなユースケースでは更に改善も期待できると思いますので、是非お試しください。 最後に、ZOZOテクノロジーズでは、検索機能を開発・改善していきたいエンジニアを全国から募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co
こんにちは、ZOZOテクノロジーズ 技術戦略室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、7/28に ZOZO Tech Meetup〜マイクロサービス化に取り組む、16年目のZOZOTOWN〜 を開催しました。 zozotech-inc.connpass.com 本イベントでは、ZOZOテクノロジーズが進めてきたリプレイスプロジェクトの中で、特に「マイクロサービス化」にフォーカスし、各担当者からお伝えしました。 登壇内容 まとめ 弊社の社員5名が登壇しました。 ZOZOTOWN(16歳)の悩みをSREが赤裸々に語る (SRE部 ECプラットフォームSRE / 髙塚 大暉) Backends For Frontends(BFF)をプロダクションレディするまでの取り組み (SRE部 ECプラットフォームSRE / 三神 拓哉) ZOZOTOWNトップページの裏側 (ECプラットフォーム部 カート決済 / 高橋 和太郎) ZOZOTOWN 検索機能のマイクロサービス化への取り組みについて (検索基盤部 検索基盤チーム / 可児 友裕) リプレイスを通して実現した、より高度なサービス改善 (検索基盤部 検索基盤チーム / 有村 和真) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに こんにちは、EC基盤本部・MA部・MA基盤チームでマーケティングオートメーションのシステムを開発している長澤( @snagasawa_ )です。この記事では、社内で運用しているLINEメッセージ配信基盤の課題を、アーキテクチャ改善によって解決した話をご紹介します。 当時、LINEメッセージ配信基盤では、配信処理を担っていたApp Engineで2つの課題を抱えていました。「メモリ不足による配信処理の中断」と「リクエストタイムアウト後の意図しない処理の継続」です。一時はスケールアップによるメモリ増強を検討しましたが、後者の課題を解決できないためアーキテクチャの変更に着手しました。 結果として、App Engineが担っていた処理をBigQuery・Cloud Storage・Dataflow Batch Jobに置き換えることにより、この2つの課題を解決しました。加えて、配信対象ユーザーの増加にも対応しました。この記事が類似するシステムを開発されているエンジニアの方にとって参考になれば幸いです。 はじめに LINEメッセージ配信基盤とは アーキテクチャの改善に取り組んだ背景 改善前のアーキテクチャ 課題1: メモリ不足による配信処理の中断 課題2: リクエストタイムアウト後の意図しない処理の継続 改善後のアーキテクチャ BigQueryへのメッセージ出力 Cloud Storageへのファイル出力 Dataflow Batch Jobによるメッセージのpublish Cloud Pub/Sub & App Engine(フロー制御) Cloud Pub/Sub & Cloud Functions(配信) 各GCPサービスのボトルネック BigQuery Cloud Storage Dataflow Cloud Pub/Sub Cloud Functions 今後の改善 まとめ さいごに LINEメッセージ配信基盤とは はじめにLINEメッセージ配信基盤を説明します。 LINEメッセージ配信基盤とは、我々が「LINE Friendship Manager(以下、LFM)」と呼んでいるLINEユーザー向けのメッセージ配信システムです。 ZOZOTOWNではLINEの企業公式アカウントを運用しています。エンドユーザーはこの公式アカウントをLINE友だちに追加することで、ZOZOTOWNのキャンペーン情報やお気に入りアイテムの値下げ情報などをLINE上で受け取ることができます。 LFMはLINE社が提供するMessaging APIを利用して、LINE友だちにメッセージやリッチメニューのバッチ配信を予約配信できます 1 。 LFMの詳細は採用イベントにて発表した登壇資料があります。こちらを参考にしてください。 アーキテクチャの改善に取り組んだ背景 ZOZOTOWNではLINEを重要なユーザーコミュニケーションのチャネルと位置づけて、戦略的なマーケティング施策を展開しています。 www.linebiz.com その甲斐もあって、昨今のZOZOTOWN LINE公式アカウントでは年々LINE友だちが増加しています。具体的には、累計LINE友だち数(配信対象外のブロックユーザーを含む)が2021年4月に1,000万ユーザーを突破し、2018年年初の約1.4倍になりました。 LINE友だち数が増えれば、LFMで扱うユーザー数もまた増えます。そのため、LFMでは冒頭の課題を解決しつつ、今後の配信対象ユーザー数増加に耐えうるスケーラビリティを備えるようアーキテクチャ改善に着手しました。 改善前のアーキテクチャ LFM改善前のメッセージ配信方法を紹介します。 前提として、配信に必要なデータはそれぞれ以下の通り格納されています。 Cloud SQL 配信開始の予約時間 配信対象ユーザーを抽出する条件 メッセージのコンテンツ情報(画像のURLやクリック後の遷移先URLなど) BigQuery 配信対象のユーザーIDリスト 処理の流れは以下の通りです。 (1) App Engine Cron Jobで予約時間になった配信処理を開始 (2) Cloud SQLからメッセージのコンテンツを取得 (3) BigQueryからユーザーIDのリストを取得 (4) ユーザーIDとメッセージの組み合わせをCloud Pub/Subにpublish (5)〜(8) Cloud FunctionsでLINE Messaging APIにリクエスト 参考までに、以下のコードがこの (2)~(4) の実装を単純化したものになります。 require ' google/cloud/bigquery ' require ' google/cloud/pubsub ' require ' json ' # ユーザーIDのリストを取得 bigquery = Google :: Cloud :: BigQuery .new sql = ' SELECT user_id FROM `project_id.dataset_id.users` ' user_ids = bigquery.query(sql) # メッセージを生成 content_id = 1 content = Content .find(content_id) messages = user_ids.map do | user_id | { user_id : user_id, messages : [ { type : ' image ' , url : content.image_url, size : ' full ' , action : { type : ' uri ' , uri : content.action_url, }, } ] } end # メッセージをCloud Pub/Subにpublish pubsub = Google :: Cloud :: Pubsub .new topic_name = ' topic_name ' topic = pubsub.topic(topic_name) topic.publish do | batch | messages.each do | message | batch.publish( JSON .dump(message)) end end この実装はMVPとしてファーストリリース当初の要件を問題なく満たすものでしたが、運用中に先述の課題が発覚しました。 課題1: メモリ不足による配信処理の中断 1つ目の「メモリ不足による配信処理の中断」は、上記の (2)〜(4) の処理を行なっていたApp EngineのFlexible Environment(以下、FE)で発生しました 2 。 下のグラフは、実際に配信が中断した直前1週間のメモリ使用量推移です。数日間稼働しているFEのインスタンスのメモリ使用量が右肩上がりに増えていき、グラフの最後では メモリ不足による502エラー によって配信が中断されて2GiBほど減少しています。このようにメモリリークによって消費メモリが肥大しているため、スケールアップしたとしても502エラーの再発が懸念されました。 課題2: リクエストタイムアウト後の意図しない処理の継続 それでもまだメモリ不足だけであればスケールアップを検討する余地もありましたが、別の課題が存在しました。それが2つ目の「リクエストタイムアウト後の意図しない処理の継続」です。 FEには リクエストの最大タイムアウトが60分 という制約があります。一方で、LFMでは時折このタイムアウトを超過しても配信処理が継続していることに気がつきました。調査の結果、これはバックグラウンドスレッドによるものでした。 App Engineのドキュメント にもある通り、FEではバックグラウンドスレッド・バックグラウンドプロセスが動作します。 LFMのコード上ではバックグラウンド処理を行っていませんでしたが、使用していたCloud Pub/Subのクライアントライブラリで 内部的にスレッドを生成 していました。この影響で、タイムアウトになると504エラーのレスポンスが返ってくるものの、バックグラウンドで配信処理は継続するという現象が発生していました。 開発チームとしてもこれは事後的に発覚したものであり、実装当初からは想定外の挙動でした。また、 バックグラウンド処理自体、推奨されていません 。そのため、このバックグラウンド処理を避けるべくFE以外の選択肢を検討し始めました。 まず、同じApp Engineの中で、タイムアウト上限が24時間であるStandard EnvironmentのBasic Scaling・Manual Scalingが候補となりました。しかし、インスタンスクラスの最大メモリが2048MBとスペック不足であり断念しました。 cloud.google.com 以上の理由からApp Engine以外での解決に取り組みました。 改善後のアーキテクチャ 以下が改善後の配信方法です。 BigQueryへのメッセージ出力 1ステップ目はBigQueryへのメッセージ出力です。変更前の配信開始時の処理では、BigQueryから取得したユーザーIDのリストと、Cloud SQLから取得したメッセージのコンテンツを組み合わせてメッセージを生成していました。しかし、変更後はユーザーIDとメッセージのコンテンツを組み合わせた「クエリ」を生成し、その実行結果をBigQueryの別のテーブルに非構造化データとして出力します。 具体的には以下のような再帰メソッドを使うことで、Rubyの連想配列をBigQueryのStructの文字列に変換し、その結果をクエリに組み込んで非同期のクエリジョブを実行します。 def hash_to_bigquery_struct_string (hash, parent_key : nil ) array_of_hash_to_bigquery_struct_string = proc do | array_of_hash , key | array_of_hash .map { | hash | hash_to_bigquery_struct_string(hash) } .join( " , \n" ) .then { | str | " [ \n#{ str }\n ] AS #{ key }" } end case hash when Array then array_of_hash_to_bigquery_struct_string.call(hash, parent_key) when Hash hash .map { | key , value | case value when Array then array_of_hash_to_bigquery_struct_string.call(value, key) when Hash then hash_to_bigquery_struct_string(value, parent_key : key) else "#{ value.is_a?( String ) ? " ' #{ value } ' " : value } AS #{ key }" end } .join( " , \n" ) .then { | str | " STRUCT( #{ str }#{ parent_key ? "\n ) AS #{ parent_key }" : ' ) '}" } else raise ( ArgumentError , "#{ hash } is neither hash nor array of hash. " ) end end message_contents = [ { type : ' image ' , url : ' https://cdn.sample.jp/images/example_01.png ' , size : ' full ' , action : { type : ' uri ' , uri : ' https://zozo.jp/sale/ ' , } } ] hash_to_bigquery_struct_string(message_contents, parent_key : ' messages ' ) # => # [ # STRUCT('image' AS type, # 'https://cdn.sample.jp/images/example_01.png' AS url, # 'full' AS size, # STRUCT('uri' AS type, # 'https://zozo.jp/sale/' AS uri # ) AS action) # ] AS messages CREATE TABLE dataset_id.table_id OPTIONS ( expiration_timestamp=TIMESTAMP " 2022-01-01 00:00:00 " ) AS ( WITH segments AS ( SELECT ' 1111 ' AS user_id UNION ALL SELECT ' 1112 ' AS user_id ) SELECT user_id, [ STRUCT( ' image ' AS type , ' https://cdn.sample.jp/images/example_01.png ' AS url, ' full ' AS size , STRUCT( ' uri ' AS type , ' https://zozo.jp/sale/ ' AS uri ) AS action) ] AS messages FROM segments ) 上のクエリ実行すると次のテーブルが作成されます。 これによってApp Engineのメモリ消費を抑制しつつ、LINE Messaging APIに渡すリクエストボディとほぼ同様の構造のままデータをテーブルに出力できます。実際にこの変更によって Standard Environmentの「B8」インスタンスクラス でも事足りるようになりました。 配信後のテーブルは不要なため、クエリのオプションに expiration_timestamp を指定します。これで自動的にテーブルが削除され、ストレージの課金コストを削減できます。 cloud.google.com Cloud Storageへのファイル出力 2ステップ目では、BigQuery APIによって前の手順で出力したテーブルをJSONファイルとしてCloud Storageに出力します。 cloud.google.com 注意点としては、Cloud Storageへの単一ファイルの出力には最大1GBという上限があります。このため、 ワイルドカードURLを指定 することで自動的に複数ファイルに分割して出力されるようにします。 このJSONファイルも配信後は不要なため、 バケットを作成する際に ライフサイクルルールを指定 します。これで自動的にファイルが削除され、 ストレージの課金コストを削減できます。GCPのリソースはTerraformで管理しており、以下のように記述することでバケットにライフサイクルルールを適用できます。 resource " google_storage_bucket " " pubsub-message " { name = " ${var.project}-pubsub-message " location = " US " force_destroy = true lifecycle_rule { action { type = " Delete " } condition { age = 1 } } } Dataflow Batch Jobによるメッセージのpublish 3ステップ目では、Cloud StorageのJSONファイルを読み込み、Pub/Sub TopicへpublishするDataflow Batch Jobを実行します。このJobもCloud Storageへのファイル出力後にRailsから起動します。 Dataflow Batch Jobは処理対象のデータ量に合わせてワーカーとインスタンスをオートスケールさせるため、大量のデータも高速に処理できます。今回はGoogle Cloudが提供するテンプレートの「 Cloud Storage Text to Pub/Sub (Batch) 」を利用します。 テンプレートの利用により、Dataflowの実装が不要になります。 今回のケースでは、RubyのGCPクライアントライブラリ google-cloud-ruby でAPIがサポートされていなかったため、 google-api-ruby-client と google-auth-library-ruby を利用して実装しました。 require ' googleauth ' require ' google/apis/dataflow_v1b3 ' DataflowV1b3 = Google :: Apis :: DataflowV1b3 class Dataflow def launch_gcs_to_pubsub_template ( project_id :, job_name :, input_file_pattern :, output_topic :) service.launch_project_template( project_id, DataflowV1b3 :: LaunchTemplateParameters .new( job_name : job_name, parameters : { inputFilePattern : input_file_pattern, outputTopic : " projects/ #{ project_id } /topics/ #{ output_topic }" , }, environment : DataflowV1b3 :: RuntimeEnvironment .new( temp_location : " gs:// #{ project_id } -dataflow-template/temp " , ), ), gcs_path : ' gs://dataflow-templates/latest/GCS_Text_to_Cloud_PubSub ' , ) end private def service @service ||= DataflowV1b3 :: DataflowService .new.then do | service | service.authorization = credentials service end end def credentials credentials = Google :: Auth :: ServiceAccountCredentials .make_creds( scope : [ DataflowV1b3 :: AUTH_COMPUTE ]) credentials.fetch_access_token! credentials end end Cloud Pub/Sub & App Engine(フロー制御) 4ステップ目では、App EngineとCloud Pub/Subによるフロー制御をしています。この実装はオプションであり、要件次第では不要です。今回はLINE Messaging APIのレート制限を回避するために実装しています。 LINE Messaging APIには 特定のエンドポイントを除いて「2000リクエスト/秒」という制限 があり、超過するとエラーが返る仕様になっています。 Dataflowは大量データを高速に処理可能ですが、それゆえにフロー制御をしないスループットだとこのレート制限を超過してしまいます。そのため、今回は2種類のPub/Sub Topicを用意してあります。ひとつはフロー制御用で、もうひとつはCloud Functionsの配信トリガー用です。 フロー制御用のTopicにメッセージがpublishされると、App Engineからメッセージをsubscribeし、1秒ごとに配信用Topicへpublishします。この処理はスループットが一定に保たれるため、配信メッセージ数が増加してもスケーリングは不要になります。なお、別のアプローチとして、Dataflowのパラメータによるワーカー数の固定も試しました。これはJavaのStackOverflowErrorが頻発した事、毎秒制御のほうがより確実であった事からあえなく断念しました。 この処理はApp EngineのCron Jobによって1分間隔で実行しています。 注意点として、このCron Jobは開始時に前の処理を終了していないとその処理がスキップされてしまうため、 次の開始時間を超えないようにレスポンスを返す必要があります。 cloud.google.com Cloud Pub/Sub & Cloud Functions(配信) 最後の5ステップ目に、Pub/SubトリガーのCloud FunctionsがLINE Messaing APIにリクエストを送信し、エンドユーザーのLINE上でメッセージが表示されます。 Cloud FunctionsもPub/Sub Topicのトラフィックによってインスタンスがスケールし、並列処理によって大量メッセージを高速に処理できます。参考値として、フロー制御をしない場合、約500万ユーザーへの配信が35分ほどで完了します。この時間にはBigQueryへのテーブル出力やDataflow Jobの起動による10分程度のオーバーヘッドを含みます。 ここまでの一連の流れによって、先の課題に対応しつつ、GCPのリソースを活用したスケーラブルな配信処理が可能になります。 各GCPサービスのボトルネック さて、スケーラブルになったとして現実的にはどこまでスケール可能なのでしょうか。ここまで読んでいただいた方も気になる部分かと思います。結論としては、スケール上限はGCPの使い方に準じます。今回使用している各GCPサービスにおけるボトルネックについて紹介します。 BigQuery BigQueryはよくペタバイト級のデータウェアハウスとして謳われますが、クエリの内容によってはそのスケールメリットを享受できず、ボトルネックになる可能性があります。 下記記事によると、ORDER BY句や分析関数、番号付関数などはすべてのデータを対象にシングルノード上で処理が行われるという性質上、メモリ上限に引っかかりやすいとのことです。 Operations that need to see all the data in the resulting table at once have to operate on a single node. Un-partitioned window functions like RANK() OVER() or ROW_NUMBER() OVER() will operate on a single node. Another operation that is mentioned to face a lot of problems is the ORDER BY clause. It is an operation that requires a single node to see all data, so it may hit the memory limit of a single. medium.com 公式ドキュメントにおいても、クエリパフォーマンスの最大化について以下のような説明があります。 When you use an ORDER BY clause, it should appear only in the outermost query. Placing an ORDER BY clause in the middle of a query greatly impacts performance unless it is being used in a window (analytic) function. Another technique for ordering your query is to push complex operations, such as regular expressions and mathematical functions to the end of the query. Again, this technique allows the data to be pruned as much as possible before the complex operations are performed. cloud.google.com LFMでもABテストのグループ分けのために分析関数を使用しており、ユーザーID数が1,400万に達すると以下のメモリ超過エラーに引っかかることが判明しています。直近では問題ありませんが、将来的には適切なバッチサイズに分割してクエリを実行するように修正する必要があります。 Resources exceeded during query execution: The query could not be executed in the allotted memory. Cloud Storage Cloud Storageへの出力は先述の通り、ワイルドカードURLにより複数ファイルに分割する必要があります。 しかし、速度的には十分に高速であるため、将来的にボトルネックになりうる懸念はほとんどありません。データ量によって出力完了までの時間は多少増減しますが、参考値として500万ユーザーを対象する配信でも1分程度で完了します。 Dataflow Dataflowはオートスケールに加えて、パラメータでCompute Engineのマシンタイプを変更可能なため、ボトルネックになりうる懸念は少ないです。しかし、ジョブの同時実行数はプロジェクトにつき上限「25個」と少ないため要注意です。これはソフトリミットのため、サポートへの問い合わせにより上限緩和できます。 cloud.google.com Cloud Pub/Sub Cloud Pub/Subは リージョンごとのスループットに上限 が設けられているため、メッセージサイズおよびメッセージ数が大きい場合には注意が必要です。 Pub/Subで割り当て可能なリージョンは大規模と小規模の2種類があり、スループットの上限はこの種類によって大きく差があります。例えばLFMのようにPub/SubトリガーのCloud Functionsを利用している場合、pushサブスクライバーのスループットに上限があります。大規模リージョンでは「8,400,000 KB(140 MB/秒)」であるのに対し、小規模リージョンでは「1,200,000 KB(20 MB/秒)」と7分の1しかありません。このため、同時間帯に複数の配信がされたり、メッセージサイズが大きいユースケースには注意が必要です。 前提条件によりますが、Pub/Subのリージョンはリクエストを送信したクライアントのリージョンにルーティングされることがあります。LFMのケースであれば、フロー制御のApp Engineインスタンスのリージョンが配信用Pub/Subのリージョンになります。したがって、Pub/Subのリージョンと合わせてリクエストを送信するクライアントのリージョンにも留意する必要があります。 現在のLFMは小規模リージョンに存在しており、配信によっては上限近くのスループットに達するため、いずれは大規模リージョンへ移行する予定です。 cloud.google.com Cloud Functions Cloud FunctionsはPub/Subトリガーで実行される場合、バックグラウンド関数のみに適用される割り当てが存在するため要注意です。 特にLFMのようなフロー制御をしない場合、パフォーマンス要件によっては関数ごとの最大同時呼び出し回数がボトルネックになりうる懸念があります。LFMではフロー制御に加えて トリガーのPub/Sub TopicとCloud Functionsを並列化している ため、この呼び出し回数がボトルネックにはなりません。 cloud.google.com 今後の改善 今後の改善案として検討しているのは、Dataflow SQL Jobの採用です。Dataflow SQL Jobは内部的にBigQuery Storage Read APIを利用します。これによりCloud Storageへの出力を介さずにBigQueryから直接Pub/Sub Topicにメッセージをpublishできます。 現在はまだDataflow SQLクエリの実行方法がCloud Consoleか gcloud コマンドの2択しか存在しないため、クライアントライブラリでのサポート開始後に本格的に採用検討します。 cloud.google.com まとめ 本記事では、GCPのアーキテクチャ改善によって、既存の課題を解決しつつ更なるスケーラビリティを実現したLINEメッセージ配信基盤について紹介しました。今後もDataflow SQL Jobの採用検討などにより、さらなる改善を取り入れていきたいと考えています。 さいごに ZOZOテクノロジーズではGCPのアーキテクチャ改善やマーケティングに関連するプロダクトの開発に関心のあるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! tech.zozo.com 一部のバッチ配信とリアルタイム配信は別システムでも配信しています。詳しくは 別記事 を参照ください。 ↩ FEを使っていた理由は、その当時使っていたRubyのバージョンにStandard Environmentが未対応だったため。 ↩
こんにちは。計測プラットフォーム本部バックエンド部SREチームの市橋です。 私たちのチームではZOZOSUIT、ZOZOMAT、ZOZOGLASSといった計測技術に関わるシステムの開発、運用を担当しています。現在のZOZOMATとZOZOGLASSは、どちらも独立したEKSクラスタ上で動いていますが、ZOZOGLASSの環境を構築する際に将来のマルチテナント化を踏まえ大きく設計を見直しました。今回は、この設計見直し時に考慮した点を紹介します。 ZOZOGLASSとは ZOZOGLASSは顔の情報を計測し、イエローベースとブルーベースの2タイプ、及び春夏秋冬の4タイプの組み合わせからなるパーソナルカラーを診断するサービスです。計測した顔の情報から肌の色に近いファンデーションを推薦します。2021年7月時点で、ZOZOGLASSが推薦するコスメアイテムはファンデーションのみですが、今後はファンデーション以外のコスメアイテムも追加される予定です。ZOZOGLASSは無料で予約可能ですので、ご興味のある方は こちら からご予約ください。ちなみに私のパーソナルカラー診断の結果はブルーベースの冬でした。 EKSマルチテナントの構想 マルチテナント化を検討した背景 ZOZOGLASSでマルチテナント化を検討した背景について触れる前に、マルチテナントについて整理します。マルチテナント(シングルクラスタ)とは単一のEKSクラスタの上で複数のサービスを運用することを指します。比較対象となる構成としてマルチクラスタ・シングルテナントがあり、これらを図示すると以下のようになります。 両者を比較するとそれぞれ次のような特徴があると考えています。 構成 マルチクラスタ・シングルテナント シングルクラスタ・マルチテナント 説明 費用 △ ○ マルチクラスタ・シングルテナントの場合、管理しているクラスタ数分の課金、及びクラスタ管理用リソースのためのマシンリソース分の料金が追加で必要になる。 運用コスト × ○ マルチクラスタ・シングルテナントの場合、管理しているクラスタ数分のアップデート作業が必要になる。 また、クラスタ設定の構成管理とその自動化(IaC、CI/CDなど)、監視・観測設定もクラスタ分必要になる。 クラスタ障害時の影響範囲 ○ △ シングルクラスタ・マルチテナントでクラスタ障害が発生した場合、クラスタ内で運用しているサービス全てに影響が及ぶ。 権限管理の容易性 ○ △ シングルクラスタ・マルチテナントの場合、テナントごとに権限を分離するための考慮や設定が必要になる。 両者の選択については様々な記事が公開されており、それによると組織構造やプロダクトの成熟度も考慮すべき事項だと感じました。弊チームは複数のサービスを1つの開発チームと1つのクラスタ管理チームで運用する体制になっています。このような体制であるにも関わらず、下図のようにサービス毎にEKSクラスタが存在していました。 この場合、運用する上で冗長な部分が多くありました。具体例を1つ挙げると、EKSクラスタのアップデート対応です。ZOZOMATの時点で、非本番環境を含めると多数のクラスタが存在しており、アップデート対応に苦しめられました。ZOZOGLASSを開発するにあたり、ZOZOMATの設計を踏襲すると、負荷が更に増えてしまいます。 以上のことから、クラスタの管理コスト削減を目指し、ZOZOGLASSでは将来のマルチテナント化を前提とした設計を進めることにしました。マルチテナント化後の体制は下図のイメージになります。 また、今後も新規サービスが計画されており、その開発時にもクラスタの作成やそれに付随する諸作業を省略できると考えました。 マルチテナント化は次のように進めます。 マルチテナント化前提にZOZOGLASSを設計し、共通基盤としてのEKSクラスタ measurement-platform で運用(本記事の主題) ZOZOMATを measurement-platform へ移行(2021年7月時点で実施中) 移行のイメージは以下の図の通りです。 ZOZOMATのクラスタ移行については、現在実施中ということもあり深く触れません。本記事ではEKSでマルチテナントを実現するために、考慮した事について紹介します。 設計時の考慮事項 マルチテナント化を進めるにあたっては、 AWS公式ブログの記事 を参考にして考慮事項を1つずつ確認しました。この記事ではテナントを以下の3つの観点から分離することが重要であると述べています。 コンピューティングの分離 ネットワークの分離 ストレージの分離 私たちの環境ではKubernetesのストレージ機構を利用していないため、コンピューティング、ネットワークの観点を中心に確認しました。その他、Kubernetesマニフェストをどのリポジトリで管理するかについても重要なポイントだと感じたため、少しアレンジして以下の観点で確認しました。 コンピューティングの分離 ネットワークの分離 Kubernetesマニフェストの管理リポジトリの分離 コンピューティングの分離 マルチテナント化に向けた1つ目の観点として、コンピューティングの分離を考えます。先のブログ記事では次のことを担保すべきとしています。 権限が分離できること マシンリソースの競合を回避できること 私たちはコンピューティングタイプとしてFargateを採用しています。Fargateを利用することでこの2つの要件を比較的容易に解決できました。 権限分離の観点 KubernetesはRBACと呼ばれるロールベースのアクセス制御の機構を持ち、これを利用して誰が何を実行できるかを決定します。これにはクラスタ全体で共通利用できるClusterRoleと、単一のnamespaceでのみ有効なRoleがあります。テナントごとに利用する権限を設定する場合にはRoleを利用するため、namespaceをテナントごとに作成しておいた方が権限を分離しやすくなります。そのため、今回はテナントごとに1つのnamespaceを作成しました。この後にも度々namespaceが登場しますが、分離度を高める上で重要な概念のため、どのような単位でnamespaceを分けるか予めよく考える必要があります。 podがAWSサービスにアクセスするにはRBACとAWSのIAMを統合して利用します。この際、権限の分離度を高めるためにはIAM roles for service accounts(以下、IRSA)を利用することが望ましいです。これはpodに付与されているServiceAccountsにIAMRoleを紐づけるもので、これによりpod内のコンテナからAWSリソースの操作が可能になります。 コンピューティングタイプがEC2の場合は、podがEC2インスタンスに付与されているIAMロールを利用できます。そのため、1インスタンスで動作する全podのAWSサービスへのアクセス権限をひとまとめにして、1つのIAMロールにポリシーを詰め込んで使うこともできます。シングルテナントの場合はこれで問題ないケースもありますが、マルチテナントの場合は他のテナントで使っているAWSリソースの操作も許可されてしまうため、権限を制限しておくことで安全性が増します。なお、Fargateの場合はインスタンスの権限を利用できないため、IRSAの利用が必須となります。そのため、必然的にpodごとに権限の分離度を高める機会を得られることになります。IRSAの設定は以下の流れで行います。 STEP1 OIDCプロバイダーの作成 まず、podからIAMRoleを利用する際に必要となるOIDCプロバイダーを作成します。私たちが構築した当時はCloudFormationが対応しておらず、eksctlも機能不足 1 により利用できなかったため、Webコンソールから作成しました。 現在は両方対応しており、手元で試せてはいませんがCloudFormationは AWS::IAM::OIDCProvider リソースを使って、eksctlであれば以下のコマンドで作成できるはずです。 $ eksctl utils associate-iam-oidc-provider --cluster < cluster_name > --approve STEP2 IRSA用のIAMRoleの作成 次にIAMRoleを作成します。以下のYAMLは、IRSA用IAMロールを作成するCloudFormationテンプレートのサンプルです。IRSAを利用するために、AssumeRolePolicyDocumentへSTEP1で作成したOIDCプロバイダと信頼関係を結ぶ設定を記述します。 IAMRole : Type : 'AWS::IAM::Role' Properties : RoleName : 'irsa-for-api' AssumeRolePolicyDocument : !Sub | { "Version" : "2012-10-17" , "Statement" : [ { "Effect" : "Allow" , "Principal" : { "Federated" : "arn:aws:iam::${AWS::AccountId}:oidc-provider/${EKSOIDCProvider}" } , "Action" : "sts:AssumeRoleWithWebIdentity" , "Condition" : { "StringEquals" : { "${EKSOIDCProvider}:sub" : "system:serviceaccount:${Namespace}:api" } } } ] } Path : '/' Policies : - PolicyDocument : Statement : - Effect : 'Allow' Action : 's3:PutObject' Resource : - !Sub '${S3Bucket.Arn}/*' PolicyName : 'irsa-for-api-s3' ${EKSOIDCProvider}には以下のコマンドで取得できるURLのうち、idの部分を指定します。 $ aws eks describe-cluster --name < cluster_name > --query " cluster.identity.oidc.issuer " --output text STEP3 ServiceAccountの作成 最後に、podに割り当てるServiceAccountを作成します。annotationsにSTEP2で作成したIAMRoleのArnを指定します。 apiVersion : v1 kind : ServiceAccount metadata : name : serviceaccount-api annotations : eks.amazonaws.com/role-arn : arn:aws:iam::000000000000:role/irsa-for-api ここで生成されたServiceAccountをpodに付与することで、IAMRoleのポリシーに含まれるAWSリソースへのアクセス権限を得られます。 マシンリソースの競合の回避 「コンピューティングの分離」の最後はマシンリソースの競合について考えます。これを回避する方法にはpodの割当リソースに制限をかけるか、配置するノードを分けるか、2つの選択肢があります。 前者は リソースクォータ を利用することで実現可能です。リソースクォータはnamespaceごとに定義できるため、これを利用することでテナント間のマシンリソースの奪い合いを制限できます。 後者はコンピューティングタイプをEC2、Fargateのどちらで運用しているかで対応方法が変わります。EC2の場合、テナントごとにノードグループを作成して nodeSelectorsやnodeAffinity を使って、どのノードグループにpodを配置するかを制御することでリソースの競合を回避できます。Fargateの場合はそもそもVM環境が分離された状態でpodが実行され、podの要求リソースに応じたマシンリソースを持ったノードが自動で割り当てられます。そのため、特に意識することなくマシンリソースの競合を回避できます。細かい設定をせずにマシンリソースを分離できる点はFargateを利用するメリットの1つかと思います。 ネットワークの分離 マルチテナント化に向けた2つ目の観点として、ネットワークの分離を考えます。デフォルトではクラスタ内であればnamespace越しの通信が許可されています。もし、これを許容できない要件があれば、NetworkPolicyを利用することで制限できます。ただし、NetworkPolicyが利用できるのはコンピューティングタイプがEC2の場合のみで、Fargateを利用する場合はこれを利用できません。代替案としては、AppMeshの機能で通信を制御する方法が推奨されています。詳しく調べられていないのですが、podの全通信にEnvoyが介在するような形になり、Egressのフィルタリングでnamespace越しの通信を遮断するような形になると考えています。それぞれ図示すると以下のようになります。 最終的にAppMeshの導入については、導入工数とシステムへの影響を鑑みて充分にPoCを行った上で導入可否を決定した方が良いと考え、ZOZOGLASSのリリース時点では導入を見送りました。リリース時点ではシングルテナントの構成であり、ZOZOMATアプリケーションを集約してマルチテナント構成になるまでに対策を講じるという判断です。その後、どうすべきかあれこれ考えているうちに、 Fargate podにセキュリティグループを設定可能 になるリリースが2021年6月に発表されました。これによりFargateでも上図のNetworkPolicyのような制御が可能になると考えています。容易に導入できるメリットから、これを最有力候補として検討しています。ちょうどいいタイミングでリリースされて助かりました。 Kubernetesマニフェストの管理リポジトリの分離 マルチテナント化に向けた3つ目、最後の観点として、Kubernetesマニフェストの管理リポジトリの分離を考えます。ここでの内容はマルチクラスタ・シングルテナントを前提としていたZOZOMATを、マルチテナント型に作り替えることを考えた時に感じたやりにくさを踏まえたものになります。そのため、まずはZOZOMATでのマニフェスト管理を説明します。 ZOZOMATでは全てのマニフェストファイルをアプリケーション用のリポジトリで管理していました。これにはaws-authやmetrics-server、ClusterRoleのようなクラスタ内に1つあればよいリソースも含みます。シングルテナントであればアプリケーションとクラスタの設定が集約されるため、シンプルな管理が可能です。 一方、マルチテナントで運用する場合、この設計は設定変更の足かせとなります。別サービスの事情でクラスタ管理用のリソースへ変更を加える際、ZOZOMATのアプリケーションリポジトリにも手を加える必要があります。以上のことから、クラスタ管理用リソースとサービス固有のリソースで、リポジトリを分けることにしました。ingressとaws-loadbalancer-controllerを例に説明します。 aws-loadbalancer-controllerは、ingressリソースが作成されたことを検知してELBを作成するリソースです。aws-load-balancer-controllerはクラスタ内に1つあればよく、ingressはELBが必要なテナントごとに作成します。下図のイメージになります。 この場合、aws-loadbalancer-controllerはクラスタ管理用リポジトリ、ingressはサービス固有のアプリケーションリポジトリでマニフェストファイルを管理するようにしました。こうすることで、同じような構成でテナントを増やす際、既存のマニフェストファイルをテンプレートとして再利用しやすくなりました。 まとめ 以上の考慮事項を踏まえてマルチテナントを前提としたEKSクラスタを構築し、無事にZOZOGLASSの計測システムをリリースできました。マルチテナント化のネクストアクションは、ZOZOMATリソースを共通基盤へ移行することで、目下この作業に取り組んでいます。 今後リリースを予定している ZOZOSUIT 2 や ZOZOMAT for Hands はパートナー企業様と協業で進めるサービスとなっています。これらのサービスは信頼性やアクセス管理の面において、より厳格な要件を求められることが予想されます。今回得られた知見を生かして開発に取り組み、要件を満たした上でいち早くサービス提供することに尽力していきます。 私たちはこのような課題に対して楽しんで取り組み、一緒にサービスを作り上げてくれる方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 tech.zozo.com 当時は、AWS SSOから生成される認証情報を使ってeksctlを実行できませんでした。こちらの issue で報告されており、 2021年5月ごろ対応が完了したようです。エラーが出た場合はeksctlのバージョンを上げることで回避できる可能性があります。 ↩
こんにちは、SRE部の谷口( case-k )です。私たちのチームではデータ基盤の開発や運用をしています。1年ほど前からBigQueryのコストパフォーマンス改善を目的にFlex Slotsを導入しています。 本記事ではFlex Slotsの導入効果や運用における注意点、ワークフロー設計についてご紹介します。BigQueryのコストやパフォーマンスで課題を抱えているチームや管理業務を行っている方の参考になれば幸いです。 BigQuery Reservationsとは コミットメント 予約 割り当て なぜFlex Slotsを使う必要があるのか Flex Slotsを用いたコストパフォーマンス改善設計 管理プロジェクトの作成 月次コミットメントの活用 Flex Slotsの活用とワークフロー設計 ワークフロー タスク 「コミットメントの購入」タスク 「コミットメントの削除」タスク 「予約」タスク 運用におけるFlex Slotsの注意点とワークフロー設計 運用上の注意点2選 コミットメント購入時は冪等にする オンデマンド料金モデルに自動で切り替える ワークフロー ラッパータスク コミットメント コミットメントの購入 コミットメントの削除 予約 予約の作成 割り当て 割り当ての作成 割り当ての削除 オンデマンド料金モデルへ切り替え タスク コミットメント コミットメントの購入 コミットメントの削除 コミットメントの取得 コミットメントの状態取得 予約 予約の作成 割り当て 割り当ての作成 割り当ての削除 割り当ての取得 Flex Slotsの導入効果 パフォーマンス面の効果 コスト面の効果 Flex Slotsのメリット・デメリット メリット パフォーマンスが改善される コストを削減できる デメリット リトライしても購入できない場合がある オンデマンド料金モデル切り替えに伴いコストが発生する ワークフローが煩雑化する 今後の活用 おわりに BigQuery Reservationsとは Flex Slotsを紹介する前に、まずBigQueryの費用を管理するプラットフォームであるBigQuery Reservationsをご紹介します。 BigQuery ReservationsとはBigQueryの費用や組織・プロジェクトのワークロードを管理するプラットフォームです。 BigQueryの料金モデルには「オンデマンド料金モデル」と「定額料金モデル」の2種類あります。オンデマンド料金モデルはBigQueryのクエリスキャン量に基づいた料金モデルです。一方、定額料金モデルの場合は事前にBigQueryのコンピューティングリソースであるスロットを購入する料金モデルです。 デフォルトではオンデマンド料金モデルが適用されます。オンデマンド料金モデルでは、1プロジェクトあたり2000スロットまで保証されますが、2000スロット以上は保証されていません。そのため、BigQuery全体で空きがあれば2000以上も使えますが、なければ使えません。つまり、データ量が多くなるにつれ、BigQuery Reservationsを使って定額料金モデルにする方が、コストメリットやパフォーマンスの恩恵を受けやすいと言えます。 Maximum concurrent slots per project for on-demand pricing — 2,000 引用: Quotas and limits  |  BigQuery  |  Google Cloud BigQuery Reservationsを使って定額料金モデルにするには「コミットメント」「予約」「割り当て」が必要です。この操作をすることで、スロットの購入からプロジェクトへの割り当てが可能となります。 各操作の関係は以下のようになっています。 コミットメントでスロットを購入し、予約でプロジェクトに対して割り当てるスロットを決める 予約したスロットに対して割り当てを行い、プロジェクトを紐付ける 紐付いたプロジェクトで、予約で確保したスロットを利用できるようになる 引用: Workload management using Reservations  |  BigQuery  |  Google Cloud 続いて、各操作の詳細を説明します。 コミットメント コミットメントではBigQueryのコンピューティングリソースであるスロットを購入します。スロットの購入には次の3つの方法があります。 月次コミットメント 30日単位でスロットを購入 購入後30日間はキャンセルできない 年次コミットメント 365日単位でスロットを購入 購入後365日間はキャンセルできない Flex Slots 60秒単位でスロットを購入 購入から60秒後にはキャンセル可能なため、より柔軟にスロットを購入できる スロットは100スロットから購入でき、100スロット単位で増やせます。なお、オンデマンド料金モデルは1プロジェクトあたり、2000スロットまでは保証されます。一方、定額料金モデルで2000スロット以下にするとパフォーマンスが落ちる可能性もあります。 BigQueryの利用状況によっては、2000スロット以上必要とすることはなく、その分コストを抑えたい場合もあるかと思います。その場合には、Cloud MonitoringやInformation Schemaを用いてBigQueryのスロット利用状況を確認することで、利用状況に応じた最適なスロット数を決めることができます。 Estimating how many slots to purchase 引用: Workload management using Reservations  |  BigQuery  |  Google Cloud The minimum commitment size is 100 slots, 引用: Reservations details and limitations  |  BigQuery  |  Google Cloud Note: On-demand pricing gives you access to 2,000 slots per Google Cloud project. With flat-rate pricing, you can commit to fewer than 2000 slots, but your queries might be less performant, depending on your workload demands. 引用: Introduction to Reservations  |  BigQuery  |  Google Cloud 予約 購入したコミットメントはバケットに対して割り当てられます。この操作を「予約」と呼びます。予約の操作では、どのくらいスロットを割り当てるか決めます。これをすることにより、購入したスロットをプロジェクトや組織に適用できるようになります。 例えば、2000スロット購入している場合、1000スロットを予約すれば、割り当てられたプロジェクトで1000スロットまで使えます。2000スロット全ての予約も可能です。なお、購入したスロット数以上の予約はできません。 割り当て 予約したスロットに対してGCPプロジェクトなどのリソースを割り当てます。この操作を「割り当て」と呼びます。リソースには組織、フォルダ、プロジェクトを割り当てることができます。企業で利用する場合に当てはめると、組織には会社名、フォルダには部署名、プロジェクトには部署で管理するGCPプロジェクトが該当します。なお、このリソースは継承関係を持ち、プロジェクトはフォルダの割り当てを継承し、フォルダは「組織」の割り当てを継承しています。この「割り当て」の有無で料金モデルが決まります。もし、オンデマンド料金モデルに戻したい場合は割り当てを削除します。 なぜFlex Slotsを使う必要があるのか 次にどうしてFlex Slotsを使う必要があったのか、弊社のデータ基盤が抱えていた課題を紹介します。 弊社でもデータの利活用が進んでおり、全てのデータをBigQueryに集める方針があります。集められたデータはデータ分析やML案件など幅広く用いられています。データの利活用が進むにつれ、BigQueryのデータ量やそれを扱う利用者も増加しました。データ量や利用者、BigQueryを使う案件も増えたことで「パフォーマンス」や「コスト」の課題が見えてきました。 パフォーマンスの課題 データマートなどのバッチ処理の集計が課題になっていた データ量が少ない時は必要な時間までにバッチ処理の集計を終えることができていたが、データ量や必要なデータマートが増えるにつれ、必要な時間までに集計を終えることができなくなった オンデマンド料金モデルではプロジェクトあたり2000スロットまでしか保証されないため、安定したパフォーマンスを得るためにはもっとスロットが必要だった コストの課題 クエリ実行に伴うコストも上がっていた データの利活用が進むにつれ、データ量に加えてBigQueryの利用者も増え、オンデマンド料金モデルなので当然費用も上がった 利用者のBigQueryのリテラシーも人それぞれなので、あまり詳しくない人でもコストを意識せずに使えるように管理する必要があった これらの課題を解決するためにFlex Slotsを導入しました。先に述べたようにFlex Slotsを用いることで、より柔軟にスロットを購入できます。60秒単位で購入のキャンセルができるため、重いバッチ処理など一時的に多くのスロットを使う場合に有効です。 従来の定額料金モデルには、年次コミットメントと月次コミットメントの2種類しかありませんでした。月次コミットメントを使えばコストを抑えることができますが、スロットを多く使うバッチ処理の集計を時間内に終えることができません。一方、月次コミットメントでバッチ集計に必要な量のスロットを購入すれば、集計を時間内に終えることができますが、コスト面でのデメリットが大きくなります。定額料金モデルが2種類しかなかった頃は、私たちのユースケースとは相性が良くありませんでした。 そこに、スロットの購入方法として新たにFlex Slotsが導入され、より柔軟なスロットの購入が可能となりました。スロットをあまり必要としないアドホックなクエリなどは、月次コミットメントでコストを抑えます。一方でバッチ処理の集計など多くのスロットが必要な時は、バッチ集計中のみFlex Slotsで必要なスロットを追加で購入し、割り当てることができます。Flex Slotsを導入することでパフォーマンス、コスト面の課題を解決することが可能になりました。 Flex Slotsを用いたコストパフォーマンス改善設計 私たちは月次コミットメント2000スロット、データマート集計前にFlex Slots 7000スロットを購入しています。データマート集計時は月次コミットメントで購入した2000スロットとFlex Slotsにて購入した7000スロット、合計9000スロットを割り当てます。本章では、管理プロジェクトの作成からFlex Slotsを用いたワークフロー設計まで、弊社の活用事例をご紹介します。 Icons made by irasutoya from www.irasutoya.com 管理プロジェクトの作成 まず、BigQuery Reservations用の管理プロジェクトを作成します。既存のプロジェクトとは分けて管理用のプロジェクトを作ります。 そして、この管理プロジェクトを用いて、組織内の各プロジェクトにあるBigQueryのスロットを管理できるようにします。なお、管理プロジェクトではBigQuery Reservations APIを有効化する必要があります。 弊社では複数のGCPプロジェクトが存在しますが、現在BigQuery Reservationsを用いて定額モデルを採用しているのはデータ基盤のプロジェクトのみです。そして、権限は管理用のプロジェクトと割り当てプロジェクト両方で必要な点に注意しましょう。 Icons made by irasutoya from www.irasutoya.com Project-to-reservation assignment requires that you grant permission in both the administration project and the assignee projects. We recommend that you grant administrators the bigquery.resourceAdmin role at the organization or folder level. 引用: Reservations details and limitations  |  BigQuery  |  Google Cloud 月次コミットメントの活用 月次コミットメントを用いて、スロットの購入から予約、プロジェクトに対する割り当てまでの流れを説明します。Flex Slotsと違い、一度操作するだけで良いものなので、GCPコンソールから設定します。 Icons made by irasutoya from www.irasutoya.com 最初にスロットの購入をします。GCPコンソールから、月次コミットメントを2000スロット購入します。 次に予約です。先ほど購入した2000スロットを全て予約します。予約では手持ちのスロットのうち何スロット割り当てるか決めます。弊社の場合、データ基盤用のGCPプロジェクト1つのみが対象なので、購入したスロット全てを予約しています。 最後に割り当てです。予約から割り当てを作り、スロットを割り当てるプロジェクトを選択します。そして、割り当てが完了すると、スロットを割り当てたプロジェクトはオンデマンド料金モデルから定額料金モデルに切り替わります。その結果、割り当て完了後には、弊社の環境の場合はデータ基盤のプロジェクトで2000スロットまで使うことができるようになります。 Flex Slotsの活用とワークフロー設計 コストパフォーマンスを改善させるために、Flex Slotsを用いてバッチ処理の前にスロットを購入し、完了後にスロットを破棄させるようにします。この制御はワークフローエンジンであるDigdagを用います。ここでは、Digdagのワークフローや実行してるタスクもご紹介します。 ワークフロー Digdagで実行しているワークフローを紹介します。Digdagではコンテナイメージ内部でオペレータのタスクを実行できます。それを利用するために、GCPのサービスアカウントなどの秘密情報をDigdag Secretに登録し、タスク実行時に環境変数として渡しています。 タスクの流れを順に説明します。まず、バッチ処理の実行前にFlex Slotsで7000スロット購入します。次に、月次コミットメントで購入した2000スロットと合わせて9000スロット割り当てます。なお、割り当ては既に月次コミットメント適用時に作成済みなので、再度作る必要はありません。 Icons made by irasutoya from www.irasutoya.com +bigquery_flex_slots_up: +bigquery_flex_slots_commitment : _retry: limit: 5 interval: 10 interval_type: exponential _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_purchase_commitment.sh 7000 +bigquery_flex_slots_reservation : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_reservation.sh batch 9000 続いて、購入完了後にワークフローエンジンでバッチ処理を実行します。バッチ処理完了後は再度予約で2000スロットに戻してから、Flex Slotsで購入した7000スロットを削除します。なお、2000スロットへ戻す前にFlex Slotsを削除してしまうと、予約で割り当てたスロットを満たすことができなくなり、エラーが発生します。 Icons made by irasutoya from www.irasutoya.com +bigquery_flex_slots_down: +bigquery_flex_slots_reservation : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_reservation.sh batch 2000 +bigquery_flex_slots_removement : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_remove_commitment.sh タスク コミットメント、予約、割り当て実行しているタスクをご紹介します。 「コミットメントの購入」タスク コミットメントの購入は以下のスクリプト bigquery_flex_slots_purchase_commitment.sh で行います。当時はbqコマンドしかサポートされていなかったため、bqコマンドを使っています。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID slots = $1 # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc bq mk --project_id= ${admin_project_id} --location=US --capacity_commitment --plan=FLEX --slots= ${slots} そして、スロットの購入に成功すると以下のようなログが出力されます。stateがACTIVEであれば、問題なく購入できていることを示しています。 name slotCount plan renewalPlan state commitmentStartTime commitmentEndTime --------------------------------------------- ----------- ------ ------------- -------- ----------------------------- ----------------------------- admin-gcp-project:US. 12697877815420638341 7000 FLEX ACTIVE 2021-07-06T01:03:56.570385Z 2021-07-06T01:04:56.570385Z この状態でGCPコンソールを確認すると、月次コミットメントに加え、購入したFlex Slotsが表示されていることを確認できます。 「コミットメントの削除」タスク コミットメントの削除は以下のスクリプト bigquery_flex_slots_remove_commitment.sh で行います。なお、削除対象のコミットメントIDを指定する必要があります。そのため、Flex SlotsのコミットメントIDを取得し、そのIDを利用して対象のコミットメントを削除します。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc capacity_commitment_id = $( bq ls --capacity_commitment --location US --format prettyjson --project_id= ${admin_project_id} | jq ' map(select(.["plan"] | startswith("FLEX"))) | .[] | .name | split("/") | .[5] '| sed ' s/"//g ' ) # removement bq rm --project_id= ${admin_project_id} --location=US --capacity_commitment ${admin_project_id} :US. ${capacity_commitment_id} 「予約」タスク 予約は以下のスクリプト bigquery_flex_slots_reservation.sh で行います。予約により、プロジェクトに対し割り当てるスロットを確保します。ここでは、月次コミットメント2000とFlex Slots 7000、合計9000スロットを予約します。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID reservation = $1 assignment_project_slot = $2 # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc # reservation bq update --project_id= ${admin_project_id} --location=US --slots= ${assignment_project_slot} --reservation ${reservation} そして、予約に成功すると以下のようなログが出力されます。 name slotCapacity ignoreIdleSlots creationTime updateTime ------------------------------ -------------- ----------------- ----------------------------- ----------------------------- admin-gcp-project:US.batch 9000 False 2020-06-01T01:48:43.480961Z 2021-07-06T01:06:55.357921Z また、GCPコンソールの予約を確認すると2000スロットから9000スロットに更新されていることも確認できます。 運用におけるFlex Slotsの注意点とワークフロー設計 上記の運用を実際に行ってみると、Flex Slotsを購入できないことが1〜2か月に1度程度発生します。スロットが購入できない場合、そのままでは月次コミットメントで購入した2000スロットを用いてバッチ処理の集計をすることになります。 本来9000スロットを必要とするバッチ処理の集計を2000スロットで実施することになり、完了していなければいけない想定の時間までに集計を終えることができません。そのため、Flex Slotsを購入できなかった場合には、割り当てを削除し、定額料金モデルからオンデマンド料金モデルに切り替える必要があります。 それらを考慮し、以下のワークフロー設計で運用をしています。 運用上の注意点2選 前述のフロー設計で運用していく場合にも注意点が存在するので、2つ紹介します。どちらもFlex Slotsが購入できない場合に必要となる対応です。 コミットメント購入時は冪等にする コミットメント購入時には冪等性を意識する必要があります。 実際に次のようなエラーで購入できない場合があります。 BigQuery error in mk operation: Failed to create capacity commitment in '' : The service is currently unavailable. このエラーの場合、リトライすることで購入できます。リトライによりFlex Slotsを2回購入してしまうと費用がその分加算されてしまうので、以下のスクリプト例のように、既に購入したコミットメントを削除して冪等性を意識した処理にします。なお、PENDING状態でもしばらくするとACTIVE状態になるため、スロット分の費用が発生します。 #!/bin/bash slots = $1 bash tasks/bigquery_flex_slots_remove_commitment_wrapper.sh bash tasks/bigquery_flex_slots_purchase_commitment.sh $slots オンデマンド料金モデルに自動で切り替える Flex Slotsで購入したコミットメントの状態がPENDING状態の場合、次の購入が長時間できない状態になることが多いです。Exponential Backoffでリトライしても購入できません。そのため、後続処理が遅延しないように定額料金モデルからオンデマンド料金モデルへ切り替える必要があります。公式ドキュメントでもそのような対応を勧めています。 発生頻度や遅延の影響を考慮すると、自動切り替えが可能なワークフロー設計にする必要があります。それには、割り当てを削除することで切り替え可能な設計にできます。月次コミットメントは削除できませんが、PENDING状態のFlex Slotsは削除できるので、割り当てと一緒に削除します。なお、PENDING状態の場合には費用は請求されませんが、ACTIVE状態になると請求されてしまいます。 Slots are subject to available capacity. When you purchase slots and BigQuery allocates them, then the Status column shows a check mark. If BigQuery can't allocate the requested slots immediately, then the Status column remains pending. You might have to wait several hours for the slots to become available. If you need access to slots sooner, try the following: If a slot commitment fails or takes a long time to complete, consider using on-demand pricing temporarily. With this solution, you might need to run critical queries on a different project that's not assigned to any reservations, or you might need to remove the project assignment altogether. 引用: Working with Reservations  |  BigQuery  |  Google Cloud ワークフロー 運用上の注意点で述べたように、Flex Slotsを購入できなかった場合には、オンデマンド料金モデルに切り替える必要があります。冒頭で紹介したワークフローと違い、各タスクで冪等性を考慮したり、コミットメントの状態を確認し、オンデマンド料金モデルに切り替えられるよう設計し直しています。実行してるタスクはラッパータスクとして後ほどご紹介します。 +bigquery_flex_slots_up: +bigquery_flex_slots_commitment : _retry: limit: 5 interval: 10 interval_type: exponential _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_purchase_commitment_wrapper.sh 7000 +bigquery_flex_slots_verify_for_ondemand_planning : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_verify_for_ondemand_planning.sh +bigquery_flex_slots_reservation : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_reservation.sh batch 9000 これにより、オンデマンド料金モデルへ切り替えた場合、割り当ては削除されるようになります。バッチ処理完了後に割り当ての有無を確認し、割り当てがなければ割り当てを作る点が、前述のタスクとの違いです。 +bigquery_flex_slots_down: +bigquery_flex_slots_reservation : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_reservation.sh batch 2000 +bigquery_flex_slots_removement : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_remove_commitment_wrapper.sh +bigquery_flex_slots_reserve_assignment : _retry: 3 _env: GCP_CREDENTIAL: ${secret :gcp.credential } _export: docker: image: ${docker_cloudsdk.image} pull_always: ${docker_cloudsdk.pull_always} sh > : tasks/bigquery_flex_slots_reserve_assignment_wrapper.sh ラッパータスク 次に、ワークフローから実行しているタスクを紹介します。冪等性やエラーのハンドリングをする必要があるため、ラッパータスクを用意しています。なお、ラッパータスクから呼び出しているタスクは後述します。 コミットメント 最初に、コミットメントのラッパータスクをご紹介します。 コミットメントの購入 コミットメントの購入時に、以下のエラーが出力される場合もあります。このエラーがでた場合、購入したコミットメントはPENDING状態になります。 BigQuery error in mk operation: Failed to create capacity commitment in '' : The service is currently unavailable. PENDING状態になった場合、しばらくするとACTIVEになり請求対象となります。そのため、リトライの際は対象のコミットメントを削除してから再購入しています。 Flex Slotsを運用してみたところ、Flex Slotsでコミットメントを購入できないケースが2パターン見つかりました。 1つ目は、コミットメント購入時に上記のエラーが出力されて購入できない場合です。この場合は、Exponential Backoffなどでリトライすることで購入できます。 2つ目は、コミットメント購入時にエラーは出力されないものの、購入したコミットメントの状態がPENDINGになる場合です。 コミットメントの購入に失敗することは、前述の通り1〜2か月に1度程度発生し、ほとんどが上記の2つ目のパターンです。2つ目の場合、Exponential Backoffなどでリトライしても購入できないため、即時オンデマンド料金モデルへ切り替える必要があります。 上記の内容を、以下のスクリプト bigquery_flex_slots_purchase_commitment_wrapper.sh で行います。 #!/bin/bash slots = $1 bash tasks/bigquery_flex_slots_remove_commitment_wrapper.sh bash tasks/bigquery_flex_slots_purchase_commitment.sh $slots コミットメントの削除 コミットメントを削除するタスクです。まず、オンデマンド料金モデルへの切り替えを考慮し、割り当ての有無を確認します。その後、割り当ての確認ができたらFlex SlotsのコミットメントIDを取得して、対象のコミットメントIDを削除しています。 上記の内容を、以下のスクリプト bigquery_flex_slots_remove_commitment_wrapper.sh で行います。 #!/bin/bash # removement assignment = $( bash tasks/bigquery_flex_slots_assignment_status.sh ) if [ -n " $assignment " ]; then capacity_commitment_id = $( bash tasks/bigquery_flex_slots_fetch_commitment.sh ) if [ -n " $capacity_commitment_id " ]; then bash tasks/bigquery_flex_slots_remove_commitment.sh $capacity_commitment_id fi fi なお、コミットメントを削除する際に、以下のエラーが出力される場合もあります。一見、コミットメントの削除に失敗していそうですが、実際には削除できています。そのため、リトライした際に削除済みであるか、コミットメントの有無を確認しています。 BigQuery error in rm operation: Failed to delete capacity commitment ' admin-gcp-project:US.11812766842974244240 ' : The service is currently unavailable. 予約 次に予約のタスクをご紹介します。 予約の作成 このタスクではFlex Slotsで購入した7000スロットと月次コミットメントを合わせた、合計9000スロットをデータ基盤用のプロジェクトへ割り当てるために利用しています。 上記の内容を、以下のスクリプト bigquery_flex_slots_reservation.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID reservation = $1 assignment_project_slot = $2 # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc # reservation assignment = $( bash tasks/bigquery_flex_slots_assignment_status.sh ) if [ -n " $assignment " ]; then bq update --project_id= ${admin_project_id} --location=US --slots= ${assignment_project_slot} --reservation ${reservation} fi 割り当て 定額料金モデルからオンデマンド料金モデルに切り替えるには、作成した割り当てを削除する必要があります。割り当てがあるか確認し、もしなければ割り当てを作ります。 割り当ての作成 このタスクはワークフローでオンデマンド料金モデルに切り替えたのち、バッチ集計完了後に実行しています。割り当てがない場合は割り当てを作ります。 上記の内容を、以下のスクリプト bigquery_flex_slots_reserve_assignment_wrapper.sh で行います。 #!/bin/bash # reservation_assignment assignment = $( bash tasks/bigquery_flex_slots_assignment_status.sh ) if [ -z " $assignment " ]; then bash tasks/bigquery_flex_slots_reserve_assignment.sh bash tasks/bigquery_flex_slots_alert_notice.sh 2 fi 割り当ての削除 割り当てを削除するタスクです。具体的には、作成した割り当てを取得して削除します。これは、定額料金からオンデマンド料金モデルへ切り替えるために利用しています。 上記の内容を、以下のスクリプト bigquery_flex_slots_remove_assignment_wrapper.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID assignment = $( bash tasks/bigquery_flex_slots_fetch_assignment.sh ) bash tasks/bigquery_flex_slots_remove_assignment.sh $assignment オンデマンド料金モデルへ切り替え Flex Slotsで購入したコミットメントの状態を確認してPENDING状態だった場合、即時にオンデマンド料金モデルへ切り替えています。Flex SlotsはPENDING状態であれば請求されませんが、しばらくするとACTIVE状態になり、請求対象になります。そのため、購入したスロットをまず削除します。 その後、割り当てを削除し、定額料金モデルからオンデマンド料金モデルへ切り替えます。前述の通り、この場合はExponential Backoffなどでリトライしても購入できないため、即時オンデマンド料金モデルに切り替えています。しかし、オンデマンド料金モデルの場合、確実に遅延します。遅延の原因調査をやりやすくするために、料金モデルを切り替えた際には通知を飛ばしています。 上記の内容を、以下のスクリプト bigquery_flex_slots_verify_for_ondemand_planning.sh で行います。 #!/bin/bash # verification flex_slots_status = $( bash tasks/bigquery_flex_slots_fetch_commitment_status.sh ) if [ $flex_slots_status = "PENDING" ]; then # change plan from flex slots to ondemand bash tasks/bigquery_flex_slots_remove_commitment_wrapper.sh bash tasks/bigquery_flex_slots_remove_assignment_wrapper.sh bash tasks/bigquery_flex_slots_alert_notice.sh 1 fi その結果、購入したスロットを見るとPENDING状態であることが分かります。 Capacity commitment admin-gcp-project:US. name slotCount plan renewalPlan state commitmentStartTime commitmentEndTime -------------------------------------------- ----------- ------ ------------- --------- --------------------- ------------------- admin-gcp-project:US. 9638566938320457134 7000 FLEX PENDING タスク 次にラッパータスクから実行しているタスクを紹介します。 コミットメント コミットメントの購入、削除、取得、状態確認をするスクリプトをご紹介します。 コミットメントの購入 まずはコミットメントを購入するスクリプトです。その際に、コミットメントでは購入したいスロット数を指定します。弊社では前述の通り7000スロット購入しています。 上記の内容を、以下のスクリプト bigquery_flex_slots_purchase_commitment.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID slots = $1 # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc bq mk --project_id= ${admin_project_id} --location=US --capacity_commitment --plan=FLEX --slots= ${slots} コミットメントの削除 次に、コミットメントを削除するスクリプトです。購入したコミットメントは削除しないかぎり請求されるため、バッチ処理が終わったら、購入したコミットメントを即時削除する必要があります。 上記の内容を、以下のスクリプト bigquery_flex_slots_remove_commitment.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID capacity_commitment_id = $1 # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc # removement bq rm --project_id= ${admin_project_id} --location=US --capacity_commitment ${admin_project_id} :US. ${capacity_commitment_id} コミットメントの取得 コミットメントを取得するスクリプトです。コミットメントを削除する際にはコミットメントIDを取得する必要があるため、その用途で利用するものです。 上記の内容を、以下のスクリプト bigquery_flex_slots_fetch_commitment.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc bq ls --capacity_commitment --location US --format prettyjson --project_id= ${admin_project_id} | jq ' map(select(.["plan"] | startswith("FLEX"))) | .[] | .name | split("/") | .[5] ' | sed ' s/"//g ' コミットメントの状態取得 コミットメントの状態を確認するスクリプトです。PENDING状態の場合に、コミットメントと割り当てを削除し、定額料金モデルからオンデマンド料金モデルへ切り替えるために利用します。 上記の内容を、以下のスクリプト bigquery_flex_slots_fetch_commitment_status.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc bq ls --capacity_commitment --location US --format prettyjson --project_id= ${admin_project_id} | jq ' map(select(.["plan"] | startswith("FLEX"))) | .[] | .state | split("/") | .[0] ' | sed ' s/"//g ' 予約 次に予約を作成するスクリプトをご紹介します。 予約の作成 予約を作成するスクリプトです。Flex Slotsで購入した7000スロットと月次コミットメントを合わせた、合計9000スロットをデータ基盤用のプロジェクトへ割り当てるために利用しています。 上記の内容を、以下のスクリプト bigquery_flex_slots_reservation.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID reservation = $1 assignment_project_slot = $2 # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc # reservation assignment = $( bash tasks/bigquery_flex_slots_assignment_status.sh ) if [ -n " $assignment " ]; then bq update --project_id= ${admin_project_id} --location=US --slots= ${assignment_project_slot} --reservation ${reservation} fi 割り当て 次に割り当ての作成、削除、取得、作成有無を確認するスクリプトをご紹介します。 割り当ての作成 割り当てを作成するスクリプトです。割り当てを作成することにより、BigQueryの料金モデルがオンデマンド料金モデルから定額料金モデルに切り替わります。 上記の内容を、以下のスクリプト bigquery_flex_slots_reserve_assignment.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc # reservation_assignment bq mk --reservation_assignment --project_id= ${admin_project_id} --assignee_id=assignee-gcp-project --location=US --assignee_type=PROJECT --job_type=QUERY --reservation_id= ${admin_project_id} :US.batch 割り当ての削除 割り当てを削除するスクリプトです。割り当てを削除することにより、BigQueryの料金モデルが定額料金からオンデマンド料金モデルへ切り替わります。 上記の内容を、以下のスクリプト bigquery_flex_slots_remove_assignment.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID assignment = $1 # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc # removement bq rm --project_id= ${admin_project_id} --location=US --reservation_assignment $assignment 割り当ての取得 割り当てたリソース名を取得するスクリプトです。オンデマンド料金モデルへ切り替える際に、リソース名を取得して削除するために利用しています。 上記の内容を、以下のスクリプト bigquery_flex_slots_fetch_assignment.sh で行います。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc bq ls --project_id= ${admin_project_id} --location=US --reservation_assignment --format prettyjson | jq ' map(select(.["assignee"] | startswith("projects/assignee-gcp-project"))) | .[] | .name ' | sed ' s/projects//g ' | sed ' s/locations/:/g ' | sed ' s/reservations/./g ' | sed ' s/assignments/./g ' | sed ' s/\///g ' | sed ' s/"//g ' 割り当ての有無を確認するために、以下のスクリプト bigquery_flex_slots_assignment_status.sh も利用しています。 #!/bin/bash admin_project_id = $ADMIN_PROJECT_ID # authorization echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json export BIGQUERYRC =/root/.bigqueryrc # flex slots status bq ls --project_id= ${admin_project_id} --location=US --reservation_assignment | sed ' s/No reservation assignments found.//g ' Flex Slotsの導入効果 本章では、Flex Slotsの導入効果をご紹介します。 パフォーマンス面の効果 Flex Slots導入により、2〜6時間ほどのパフォーマンス改善が実現されました。スロットを購入できた場合、1.5時間前後でバッチ処理は完了します。スロットを購入できなかった場合は、即時オンデマンド料金モデルへ切り替えています。オンデマンド料金モデルだと、改善された分のパフォーマンスが得られないため、スロットを購入できた場合と比べ2〜6時間ほど遅延します。 コスト面の効果 現時点で毎月数十万円ほどのコストメリットを得られています。 オンデマンド料金モデルでは1TBあたり日本円にして500円ほどで、毎月1TBまでは無料で利用できます。なお、クエリで処理しているバイト数はBigQueryのInformation Schemaより確認できます。 SELECT SUM (total_bytes_processed) /( 1024 * 1024 * 1024 * 1024 ) AS total_terabyte_processed FROM `assignee-gcp-project`.`region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT WHERE creation_time BETWEEN ' 2021-06-01 00:00:00.000 UTC ' AND ' 2021-06-30 23:59:59.000 UTC ' cloud.google.com Flex Slotsのメリット・デメリット Flex Slotsを導入して実感したメリットとデメリットをご紹介します。 メリット Flex Slotsを利用するメリットは以下の通りです。 パフォーマンスが改善される Flex Slots導入により、オンデマンド料金モデルに比べて格段に集計時間が早くなりました。3〜6時間かかっていた集計が1.5時間程度で終わります。 BigQueryのオンデマンド料金モデルのままでパフォーマンス要件を満たせない場合、Flex Slotsを導入して得られるメリットは大きいでしょう。 コストを削減できる Flex Slotsの導入効果で述べたように、弊社の利用状況だと月々数十万円ほどのコスト削減を実現できています。なお、既に述べたようにコストメリットはBigQueryの利用状況にも依存します。コストとパフォーマンスはトレードオフなので、Cloud MonitoringやInformation Schemaを使い、適切なスロット数を割り当てることでコスト削減が可能です。 デメリット Flex Slotsを利用するデメリットは以下の通りです。どれもスロットを購入できないことに起因するものです。 リトライしても購入できない場合がある Flex Slotsが購入できない場合、そのまま数時間にわたり購入できないことが多いです。月次コミットメントだと購入したスロット数以上のスロットは割り当てられないため、オンデマンド料金モデルに切り替えています。発生頻度は前述の通り1〜2か月に1度程度ですが、最長だと5日間連続で購入できないことも過去に発生しました。発生頻度が低いとは言えないため、手動ではなく自動で切り替わるようにしておかないと、運用負荷が大きくなります。 Slots are subject to available capacity. When you purchase slots and BigQuery allocates them, then the Status column shows a check mark. If BigQuery can't allocate the requested slots immediately, then the Status column remains pending. You might have to wait several hours for the slots to become available. If you need access to slots sooner, try the following: 引用: Working with Reservations  |  BigQuery  |  Google Cloud オンデマンド料金モデル切り替えに伴いコストが発生する 定額料金モデルからオンデマンド料金モデルへ切り替える場合、割り当てとFlex Slotsで購入したコミットメントを削除します。 しかし、Flex Slotsのコミットメントは削除できますが、月次コミットメントは購入から30日間は削除できません。そのため、切り替えに伴ってオンデマンド料金に加え、月次コミットメント分の費用が余分に発生します。 ワークフローが煩雑化する Flex Slotsが購入できない場合を考慮したワークフロー設計にする必要があるため、それに伴ってコードも煩雑化します。サポート状況などの確認が必要ですが、必要に応じてOSSにPull Requestを投げつつ、Pythonのクライアントライブラリに置き換えたいと考えています。 googleapis.dev 今後の活用 現在は、社内でもデータ基盤用のプロジェクトのみ、定額料金で運用しています。今後は全社展開を行い、コスト・パフォーマンス改善を全社規模で行いたいと考えています。 社内では、機械学習の活用に伴いBigQueryの利活用が進んでおり、推論のバッチやBigQuery MLなど、Flex Slotsを使ってパフォーマンス面で解決できる部分は多く存在しています。さらに、検索系の案件など、全社のBigQuery費用の上位に該当するプロジェクトもあります。これらのプロジェクトでコスト、パフォーマンスの両面で最適化を行えば、その効果は大きなものになるでしょう。 また、現時点ではBigQueryのジョブユーザ管理ができていません。BigQueryはストレージとコンピューティングリソースが分離されており、IAMもBigQuery閲覧権限とBigQueryジョブユーザと別れています。クエリ実行に伴う請求はこのBigQueryジョブユーザ権限を付与したプロジェクトに対して行われます。下図の場合、プロジェクトBでジョブユーザを管理し、プロジェクトAのテーブルやビューを参照できます。 引用: BigQuery for data warehouse practitioners  |  Cloud Architecture Center しかし、現時点では社内でジョブユーザの管理をうまくできていないため、重複管理やオンデマンド料金モデルのプロジェクトに対しても付与されています。ユーザに紐付くアドホックなクエリなど、定額料金モデルを適用したプロジェクトで管理できるとBigQueryのコストを下げることができるので、この点は今後の課題としています。 おわりに Flex Slotsを導入することで、BigQueryのコストやパフォーマンスの改善が可能であること、その運用上の注意点をご紹介しました。Flex Slotsの購入に失敗することを考慮したワークフロー設計が必要になるなどの煩雑さはありますが、欠点を大きく上回る利点があります。今後もFlex Slotsを積極的に使っていく予定です。 データの利活用を促進する際に、本記事が同じような課題を抱えている方の参考にれば幸いです。 私たちのチームの業務内容は、以下の記事で紹介しているのでご覧ください。今回紹介した内容以外にも、ログ収集基盤の開発など、データ基盤に関する業務全般を行っています。 https://it-career.blm.co.jp/interviews/zozotechnologies-it-taniguchi-interview it-career.blm.co.jp ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
はじめに こんにちは、ZOZOアプリ部でZOZOTOWN iOSアプリを開発している小松です( @tosh_3 )。 気づけば、ZOZOテクノロジーズに新卒入社して1年が過ぎていました。オフィスの近くに引っ越したのですが、オフィスに出社する前に、オフィスが移転しました。 さて突然ですが、最近ZOZOTOWNに大きな変化があったことをみなさんお気づきでしょうか。2021年3月18日よりZOZOTOWN全体が大幅リニューアルされ、コスメモールがオープンされるなどの大きな変化がありました。アプリも7.0.0とメジャーバージョンの更新を行い、ほとんど全ての画面が新デザインになりました。 そこで、本記事ではHome画面のリニューアルを担当した私が、そこで使用した技術とその背景について触れながら、ZOZOTOWN iOSアプリのHome画面リニューアルの裏側をお伝えします。 ZOZOTOWNアプリの新旧デザイン比較 ZOZOTOWNのHome画面がどのくらい変わったのか、リニューアル前後のUIを比較してみましょう。 旧デザイン 新デザイン 黒ベースから白ベースへと変化し、よりモダンなデザインになっています。今まで、1つのページで構成されていたデザインは「すべて」「シューズ」「コスメ」と3つのタブの構成になりました。 Home画面のリアーキテクチャ この改修をするにあたり、まずHome画面の再設計をするか否かを考えました。仮に再設計せず、既存のHome画面に新しい機能を追加していく場合、後述するHome画面が抱える潜在的な課題に遭遇する可能性が高くなります。開発の終盤で大きな変更を求められる課題に直面すると、スケジュール的にも場当たりな対応になりがちです。今までのZOZOTOWNアプリ開発の中でも似たようなシチュエーションが何度かありました。 また、再設計という選択肢を取った場合、QAチームによるフルリグレッションテストが必要になります。そのため、なかなか手軽に再設計をする判断をすることは難しいです。しかし、今回のケースでは再設計の有無に関わらず、どちららの選択をしてもQAではフルリグレッションテストを行う予定でした。以上の観点も踏まえると、再設計を行うには絶好の機会だったとも言えます。そのため、既存の課題の多くを救うためにも、再設計するという選択肢を取りました。 既存のHome画面が抱えていた課題 まず、既存のHome画面が抱えていた課題を紹介します。 課題1. 改修すると様々な機能が影響範囲になる ViewControllerが必要以上にたくさんの責務を担っており、改修に対して影響範囲が大きくなってしまう可能性が高かった 課題2. Conflictの温床だった Storyboardメインの開発が行われており、現在のiOSチーム構成(8人体制)の元では、Conflictが多発する原因になっていた HomeViewControllerは数あるVCの中でも改修が多い対象であり、実際にプロジェクト内全てのVCの中で3番目にマージされた数が多かったです。 既存の課題解決へのアプローチ 前述の課題を解決するために、それぞれ以下のアプローチを取ることにしました。 課題1. 改修すると様々な機能が影響範囲になる 3タブ構成になることを踏まえて、HomeViewControllerの役割を再定義する 疎結合かつ役割が明確になるようにクラスを定義する 課題2. Conflictの温床だった Storyboardベースからコードベースへと移行する 次に、疎結合とStoryboardの使用について説明していきます。 HomeViewControllerの整理 まず、既存のHomeViewControllerが持っていた役割を整理しました。 Home画面コンテンツの管理 Home画面コンテンツのAPI通信 Google Analytics(以下、GA)に関する機能 アプリのライフサイクルに関する機能 Home画面のライフサイクルに関する機能 これを見る限り、今までのHomeViewControllerはHome画面としての機能だけではなく、一番最初の画面としての機能も持っていました。結果として、HomeViewControllerは1400行以上にも及ぶFatViewControllerと化していました。 また、3タブ構成(すべて、シューズ、コスメ)へ変更すると、HomeViewControllerはさらに肥大化することが明らかです。そこで、HomeViewControllerの役割を再定義し、新しく各TabのViewControllerとLoggerクラスを作成しました。 それぞれの役割は以下の通りです。 HomeViewController Home画面のタブの管理 アプリのライフサイクルに関する機能 Home画面のライフサイクルに関する機能 TabViewController Home画面に存在する3つのタブ(すべて、シューズ、コスメ)をそれぞれ管理するViewController。 Home画面コンテンツの管理 Home画面コンテンツのAPI通信 Logger GAに関する機能 以上のように、それぞれのクラスに明確な役割を持たせることで、3タブ構成に変更しても今までのコード量と同等で実装できます。 Storyboardを使用することは悪なのか? StoryboardはConflictが多発する原因となっていましたが、「Storyboardを使用することはそんなに悪いことなのか?」と言われるとそうとは思いません。コードベースでUIを書くこと、StoryboardでUIを構築していくことそれぞれに良さがあります。 ZOZOTOWN iOSチームは現在8人体制で開発を行っている 今後もチームのスケールアップを行う可能性が高い このような環境では、Storyboardを使用するよりも、コードベースでUIを構築していくことの方が確実に向いていました。 他の画面では、Storyboardで多数のConflictが発生し、その度に手間がかかるということが多々発生している状態でした。ZOZOTOWNは10年以上の歴史を持つアプリなので、恐らくStoryboardで開発するのが向いていた時代もあったが今は向いていない、ただそれだけのことです。今回の改修では今までHome画面全体だけではなくUICollectionViewCellまでStoryboardで構成されていたものを、Home画面のHeader部分を除き全てコードベースへと変更することに成功しました。 Home画面のリインプリメンテーション 本章では、Home画面の再設計をどのように行ったのかを紹介します。 今回のリニューアルで実装したこと Home再設計にあたり、以下の項目に挑戦しました。 Sandboxの作成 さまざまなパターンのHome画面をすぐに確認できるため、たくさんの試行錯誤が可能に CompositionalLayoutの採用 新デザインに柔軟に対応するために、CompositionalLayoutを採用 適切なComponentへと切り出すことに成功 Storyboardの削除 Storyboardベースで設計されていた画面をコードベースへと移行 HomeViewControllerの疎結合化 GA用のクラスを別に切り出すなど、責任過多を解消 1400行から600行に 本当は全ての内容を紹介をしたいのですが、本記事では特にCompositionalLayoutについて取り上げます。 CompositionalLayoutの採用 大幅なデザイン変更を行うということは、同時にHome画面で使用している技術を刷新する機会でもあります。また、チームとしても既存の部分とうまく結合できるのであれば、積極的に新しい技術に挑戦していく方針があります。そこで、ComopositionalLayoutを使用してみることにしました。CompositionalLayoutはWWDC19で紹介された機能です。続くWWDC20でも新たにCollectionViewListが強化されるなど、Appleとしてもここ数年はCollectionViewに力を入れていると感じていたので、是非機会があれば導入してみたいと考えていました。 なお、CompositionalLayoutとは、CollectionViewのレイアウト方法の1つであり、App Storeのようにセクションごとに異なるレイアウトを簡単に組むことのできる技術です。 引用: Layouts | Apple Developer Documentation App Storeのようなカルーセルでも、FlowLayoutのカスタマイズや、中に別のCollectionViewを置くことなく容易にレイアウトを組めます。 さて、ここまでの説明を読みながら気になっていた方もいると思いますが、CompositionalLayoutが使用可能なのはiOS 13以上です。ZOZOTOWN iOSアプリは原則として3つの最新バージョンをサポートしており、当時サポートしていたOSはiOS 12、iOS 13、iOS 14でした。そのため、iOS 12でCompositionalLayoutをどのようにして使用するか、という問題に直面しました。 調べてみると、iOS 12でもCompositionalLayoutを使用可能にする、 IBPCompositionalLayout というライブラリがありました。そして、下記3点の理由からこのライブラリの導入を決めました。 ライブラリの作成者が弊社の技術顧問である 岸川克己さん なので、何か困ったことがあった際にはいつでも相談できる iOS 13以降では、純正のCompositionalLayoutを使用しており、iOS 12を切るタイミングではほとんど労力なく切り替えることができる 弊社のWEARチームでも使用しているライブラリであり、社内での利用実績がある このライブラリをZOZOTOWNで使ってみると、contentInsetAdjustmentBehaviorの値によってはうまく動かないパターンが見つかりました。原因を特定できたので、修正のPull Requestを出したため、現在では解消されています。ちなみに、このPull Requestで、初めてOSSにコントリビューションする実績をあげることができました。 github.com CompositionalLayoutは、まだ新しい技術ということもあり、慣れるまではどのようにレイアウトを組むのが正解なのかわかりませんでした。そこで、技術検証を十分に行い、実装を進める上で疑問が生じた際には技術顧問の岸川克己さんにも相談に乗っていただきました。 実際に使用してみて感じた、CompositionalLayoutのメリット・デメリットを下記に挙げます。 メリット App Storeのようなセクションごとに異なるレイアウトを組み合わせることが簡単な記述で実装できる カルーセルの動きもサポートされている カルーセル用のスクロールビューを自前で置く必要がない デメリット セクション内のスクロールビューの制御が必要になることにより、実装が難しくなる ライブラリを使用しない限り、iOS 12以前のOSをサポートできない OSによって挙動が若干異なり、一部OSでのみ発生するバグが存在する 個人的には、 今回、新たしい技術に挑戦してみて、確かに難易度が高い部分もありましたが、とても便利な機能だと感じました。是非このようなレイアウトを組む際には、検討してみるといいでしょう。 リザルト 課題とその解決法、そして技術的なアプローチを紹介してきました。それらを利用し、冒頭で紹介したZOZOTOWNが本質的に抱えていた課題をどのように解決したのか、一度まとめておきます。 課題1. 改修すると様々な機能が影響範囲になる 再設計する際に、疎結合にすることを意識し、役割ごとに明確なクラス分岐を行った 1400行以上あったHomeViewControllerは役割が明確になり、600行ほどへ 課題2. Conflictの温床だった Storyboardの使用箇所を大幅に減らし、大部分をコードベースの設計へと変更した その結果、Storyboardの制限に縛られることなくコード上で柔軟な分岐をすることが可能になった 今回の修正により、今後何か改修する際には、より少ない労力、かつ小さい影響範囲で対応できるようになりました。 リキャップ 再設計方法やその考え方に決して正解はありません。しかし、今回の再設計を通じ 疎結合にすること 、 積極的に新しい技術を検討していくこと の重要性を再認識しました。また、技術選定をしていく中で、 チームの状況とこれからを考える という視点を持つ重要性にも新しく気づくことができました。 最後に ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクから是非ご応募ください! tech.zozo.com
こんにちは、ZOZOTOWN部フロントエンドチームの高橋( @anaheim0894 )です。 Chrome 92から 「SharedArrayBuffer」の仕様が変更 されます。それに伴い、 ZOZOTOWN の対応方針と解決策をご紹介いたします。そもそも「SharedArrayBuffer」が何のことなのか分からず困っている方も多いかと思います。本記事で紹介するZOZOTOWNの取り組みが対応時に皆様の参考になれば幸いです。 取り組みのきっかけ 2021年3月、Google Search Consoleに以下メッセージが送られてきました。 Googleの公式アナウンス によると、「Chrome 92からはcross-origin isolation(クロスオリジン分離)が構成されていないと正常に動作しなくなる」と書かれていました。 つまりSharedArrayBufferを引き続き使うには、他サイトのリソースをzozo.jp内で読み込むために明示的に許可する必要があります。 現状では、Chrome以外に既にこの仕様になっているものもあります。 Firefox バージョン76でこの仕様になっている Firefoxの最新バージョンは89(2021/06/29現在) Android版 Chrome バージョン88でこの仕様になっている しかし、上記環境におけるZOZOTOWNの不具合報告は特に受けていません。 なお、サーチコンソールのメッセージには「Chrome 91から」と記載がありましたが、その後の Google Developersのアナウンス により、対応時期が「Chrome 92(2021/07/20リリース)から」に延期されました。 SharedArrayBufferに関する詳細は「海外SEO情報ブログ」にまとまっているので、併せてご覧ください。 www.suzukikenichi.com SharedArrayBufferの概要 SharedArrayBuffer(以下、SAB)は以下のようにまとめられる技術です。 Webサイトのスレッド間でメモリ空間を共有するためのJavaScriptのオブジェクト Web Worker(バックグラウンド処理)間でメモリを共用利用するための技術 詳細は、MDN Web Docsをご覧ください。 developer.mozilla.org 調査・検証 調査・確認として、以下の5点を実施しました。本章ではそれぞれの内容を紹介します。 「SharedArrayBuffer」でソースコード全検索 cross-origin isolation(クロスオリジン分離)の確認 テスト環境でクロスオリジン分離状態のテスト ReportingObserverでSAB使用箇所を特定 有識者への相談 1.「SharedArrayBuffer」でソースコード全検索 「SharedArrayBuffer」という文字列でZOZOTOWNのソースコードを全検索をしました。 その結果、「ヒット無し」で使われている箇所がないことを確認できました。 2. cross-origin isolation(クロスオリジン分離)の確認 そもそも、ZOZOTOWNがクロスオリジン分離状態になっているかどうかを確認します。Chromeの検証ツールを利用し、Consoleで「self.crossOriginIsolated」を叩くことで確認できます。 その結果、「false」の表示が返ってきました。つまり、「クロスオリジン分離状態にはなっていない」と解釈できます。 3. テスト環境でクロスオリジン分離状態のテスト 前述の確認でクロスオリジン分離状態になっていないことは確認できました。そこで、テスト環境で意図的にクロスオリジン分離状態を作りました。HTTPヘッダーに以下の値を追加すると、クロスオリジン分離状態にできます。 Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin すると、zozo.jp以外からの読み込みファイルは全てブロックされた状態、具体的には画像などが全て表示されない状態になります。 同様の現象になることを防ぐには、サードパーティ側(zozo.jp以外のドメイン側)のHTTPヘッダーにCORS(Cross-Origin Resource Sharing)か、CORP(Cross-Origin-Resource-Policy)のどちらかを付与してもらう必要があります。 access-control-allow-origin: * cross-origin-resource-policy: cross-origin このヘッダーの追加により、zozo.jp内で読み込み可能となります。 しかし、zozo.jp以外の読み込みドメインの数を確認したところ、かなりの量がありました。そのため、それぞれの企業にこのヘッダーを追加していただくのは非現実的だと判断しました。 4. ReportingObserverでSAB使用箇所を特定 サーチコンソールで「SABが使用されている」と検知されているため、使用箇所を特定する必要があります。 JavaScriptの ReportingObserver を使用すると以下の情報を取得できます。 type(レポートの種類:deprecation/intervention/crash) url(対象ページ) body(詳細情報) id(レポートID) message(Console上の警告テキスト) lineNumber(何行目) columnNumber(何列目) sourceFile(対象ファイル) anticipatedRemoval(現在のブラウザから削除されることが予想される日付) SABの場合、Typeが「 deprecation 」になり、上記の項目を取得できます。なお、「 intervention 」や「 crash 」の場合、Bodyの中身で取得できる項目が異なるのでご注意ください。 SABを使用している箇所がある場合、Chromeの検証ツールで以下の警告が出ます。 [Deprecation] SharedArrayBuffer will require cross-origin isolation as of M91, around May 2021. See https://**/ for more details. つまり、共通で呼び出されるJavaScript内にReportingを仕込めば、Warningを検知して使用箇所を特定できます。 関係する部内のチームが連携し、レポートの実装をしました。 フロントエンドチーム ReportingObserver の実装 バックエンドチーム フロントエンドから呼び出すエンドポイントの実装 SREチーム Splunkにデータ集積 実装方法の詳細は、下記のGoogleの公式ドキュメントをご参照ください。 developers.google.com また、ReportingObserverを入れた状態のZOZOTOWNでレポートログを取得したところ以下の事が分かりました。 発生したページ TOP/検索結果/商品詳細/カートなど 対象のJavaScriptファイル 「chrome-extension://」から始まるJavaScript 上記の内容を含んだログが取得できました。発生したページは主要ページではあるものの規則性が無く、対象のJavaScriptファイルは全てChrome拡張機能で使用されているJavaScriptでした。 その結果、サーチコンソールで検知されたのは特定のユーザーが利用しているChrome拡張機能で使用されたJavaScriptにSABが使用されており、その警告が検知されたのではないかと予想がつきました。 5. 有識者への相談 インターネット上の情報だけでは判断・理解しきれない内容もあったので、本件に詳しい有識者の方へのヒアリングを試みました。 弊社社員の人脈から、本件に詳しい 「Eiji Kitamura / えーじ」 さんとつながることができました。 えーじさんはGoogleの公式ドキュメントとしてSABの取り組みを執筆されており、気になる点を相談するにはまさに理想的な方にお話を伺うことができました。 以下の内容が、えーじさんに相談することで得られた知見です。 ファーストパーティとしてSABを利用していないことが確認できれば特に心配する必要はない 全ソースコードで「SharedArrayBuffer」という文字列が検索でヒットしなければ、サードパーティ製ライブラリを含め、ファーストパーティにもSABが使用されていないという判断で問題ない 既に仕様に組み込まれているFirefoxなどで、問題なく動いていればほぼ問題ない Chrome Canary(開発者向けChrome) を利用すると、Chrome 92で予定されているSABがcross-origin isolationを必須としている仕様なので、現時点でテストが可能である Chrome拡張機能のJavaScriptでログが取れた場合、Content Scriptにてサイトに挿入されたものであれば、サーチコンソール検知対象に含まれている可能性が高い サードパーティでiframeを使用している場合、その箇所はReportingObserverを使用してもログに上がってこないので実際にテストを実施する必要がある サーチコンソールに届いたメッセージは、このケースに該当する場合にも送られている React 17.0.2未満のバージョンではSABが利用されていているが、下位バージョンでのProduction buildファイルに検索して出てこないのであれば心配はいらない サードパーティ側のHTTPヘッダーにCORS/CORPのどちらかを付与してもらうのが難しい場合に対応するため、CORSやCORPに対応していなくてもクロスオリジンのリソースを読み込めるよう、ブラウザがCookieを取り除いて(盗まれて困る情報がない状態で)読み込む COEPモードの追加 が進められている COEPモードの追加が実装されるまでにSABを利用したいページがある場合は、 Origin Trial に登録することで、Chrome 92以降でもこれまで通りSABを使い続けることができる ただし、この対応策は時限的対応なので、Chrome 96以降はcross-origin isolation対応しなければSABが使えなくなる Chrome 96までの予定だが、上記を含むいくつかの改善点が実装されるまでは 延長される予定 ZOZOTOWNの対応内容 上記の調査・確認結果より、現状のZOZOTOWNを以下のように分析しました。 「SharedArrayBuffer」の文字列検索にヒットしていないため、ファーストパーティにSABは使用されていない Chrome拡張機能のJavaScript、もしくはサードパーティのJavaScriptでSABが使用されているのが原因でサーチコンソールに検知された可能性が高い Firefoxで不具合報告がないので問題ない Chrome Canaryでテストの実施が可能である その上で、以下の2点を実施することで、サービスに影響が出ないという結論を出しました。 Chrome Canaryや最新版Firefoxで、主要機能・決済までのフロー・サードパーティのJavaScript使用箇所をメインとした網羅的テストを実施する Reportingの実装・リリースを行い、Warningが出ている箇所を特定する 特定箇所が検出された場合、テストの実施と個別に対応する ZOZOTOWNで取得したログでは全てChrome拡張機能のJavaScriptが対象だったため、恐らくこれが原因と思われる 最後に サーチコンソールにメッセージが届き、はじめは戸惑いました。しかし、えーじさんの協力も得ることができ、自信を持って解決策を見出すことができました。 Chromeは、今後もよりセキュアなブラウザにするためのアップデートが続いていくでしょう。その際にも、今回のような調査を行い、そこで得られた知見のアウトプットを続けていきます。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに こんにちは、検索基盤部 検索基盤チームの可児( @KanixT )です。以前は通勤に片道2時間ほどかかっていましたが、フルリモートワークの環境になり空いた時間で生後4か月の娘の子育てに奮闘中です。 本記事では、検索基盤チームが取り組んだZOZOTOWN検索機能のマイクロサービス化の事例・工夫点を紹介します。これから検索機能のマイクロサービス化にチャレンジする方の参考になれば幸いです。 目次 はじめに 目次 背景と課題 検索機能に特化したマイクロサービスの構築 どのように構築したか 構築時にやったこと APIの実装 静的解析 ヘルスチェックの実装 複数エンドポイントのヘルスチェック 単一エンドポイントのヘルスチェック 各種バージョンアップ Java Swagger OSSのライセンスチェック 外部サービス リリース リリース時にやったこと 得られた効果 開発速度の向上 検索機能に特化したマイクロサービスの開発 まとめ おわりに 背景と課題 ZOZOTOWNでは、ASPからJavaへのリプレイスプロジェクトが数年前より実施されており、これまで多くのAPIを改善・改修してきました。一方、そのリプレイスされた環境には、1つのマイクロサービスに非常に多数のAPIが存在している状態にもなっていました。検索基盤チームが管理する検索APIも、このマイクロサービス(以下、既存マイクロサービス)の中にありました。 既存マイクロサービスは別チームが主管のため、機能追加や改修の際は別チームにレビュー・リリース依頼をしていました。 そのため、改修した内容のマージや、リリースのタイミング等を検索基盤チームが自由にハンドリングできない状態でした。また、把握していないAPIや共通処理等も多数ある状況故に、開発難度が高くなってしまうという課題もありました。 既存マイクロサービスはSQL ServerとElasticsearchを参照しており、1つのマイクロサービスとしては責任が大きく、障害発生時は複数チームが原因特定に動く状態でした。そのため、「リクエスト数が非常に多い検索機能に特化したElasticsearchのみを参照するマイクロサービス」を構築したいという思いがありました。 検索機能に特化したマイクロサービスの構築 検索基盤チームが主管である検索APIのみのマイクロサービスを構築することで、別チームへの依頼事項は無くなり、精通したAPIの開発に集中できます。そのため、開発・改修・リリースに掛かるサイクルの短縮が見込まれます。また、チャレンジングな実装の場合でも、スムーズな意思決定が可能となると考えました。 そこで既存マイクロサービスから検索APIを切り出し、検索機能に特化したマイクロサービスを構築するに際し、下記の目的を定めました。 開発速度を向上させる 検索機能に特化したマイクロサービスを開発する バックエンドはElasticsearchのみとする 既存マイクロサービスから検索APIを切り出すイメージは下図の通りです。この図はZOZOTOWNのシステム概要図であり、青色の部分が今回構築した検索機能のマイクロサービスです。なお、詳細は一部省略しています。 既存マイクロサービス 検索機能のマイクロサービス実装後 検索APIで利用する技術スタックは以下の表の通りです。 技術スタック 言語 Java フレームワーク Spring Boot データベース Elasticsearch どのように構築したか 分割する方針はいろいろと考えられますが、下記の2案で検討しました。 既存マイクロサービスのリポジトリをコピーする 既存のリポジトリを丸々コピーした別のマイクロサービスを構築し、検索機能のリクエストのみを受け付け、不要なAPIは後々消す方式。 メリット リポジトリをコピーするため少ない作業量で短期間の本番リリースが可能 デメリット 不要なAPIのコードが丸々残る SQL Serverの参照が残る 古くなっているライブラリ等もそのまま残る 既存マイクロサービスから検索APIのみを切り出す 検索APIのコードのみをコピー・リファクタリングし、別のマイクロサービスを構築する方式。 メリット 検索APIのみのマイクロサービスが構築できる Elasticsearchのみの参照にできる コードのコピー・リファクタリングのタイミングで各種のバージョンアップが可能 デメリット 1のパターンより作業量が多い 検討の結果、2. の方針を採用し、検索機能に特化したマイクロサービスを構築することにしました。 選定の主な理由は、2. は 1. より実装コストがかかりますが、このタイミングで不要なAPIを取り除いたマイクロサービスを構築することで負債を抱えずに今後の検索機能の開発に集中できると考えたためです。また、このタイミングで、各種バージョンアップや不要なライブラリを削除することでアプリケーション全体の整理整頓ができるメリットもありました。 構築時にやったこと APIの実装 既存マイクロサービスを分割して切り出す対象となるAPIは全部で4本でした。 一からすべてのコードを書き直す余裕はなかったため、既存マイクロサービスの検索APIのコードを移植し、必要に応じて部分的に再実装しました。その際、ユニットテストが十分に書いてあったおかげで安心して移植と再実装ができました。ユニットテストを書くことは安定した品質につながり、コードを変更する作業が非常に容易になると再認識できる良い経験でした。 静的解析 コードの静的解析ツールとして SonarCloud を利用し、コードの状態を可視化しています。チームの取り組みとして、ユニットテストガイドラインを作成し、テストカバレッジを毎週確認することで、コードの品質を保つようにしています。 また、GitHubリポジトリへのPull Request単位でユニットテストのカバレッジが確認できるため、レビュー依頼時には開発者自身でテスト不足がないかを確認してもらうようにしています。 なお、現在のカバレッジは86%です。 この程度のカバレッジになると、実装時にはユニットテストを書くことが当たり前になっているため、「テストを書くこと」が浸透していると実感できます。 1 ヘルスチェックの実装 アプリケーションのヘルスチェックに Spring Boot Actuator のElasticsearch用ヘルスチェックの利用を検討しました。Actuatorは単一のElasticsearchエンドポイントのみに対応しており、複数エンドポイントで運用している弊社には対応できないため独自のヘルスチェックを実装しました。 複数エンドポイントのヘルスチェック ヘルスチェックの独自実装の方法は非常に単純で、各Elasticsearchエンドポイントに対してindexの存在有無を確認するAPIを実装しました。indexの確認方法は次の通りです。 boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); Elasticsearchの Index Exists API より引用 単一エンドポイントのヘルスチェック 複数エンドポイントに比べ、単一のElasticsearchヘルスチェックを行う場合はさらに簡単で、実装は不要で設定のみで実現できます。 まず、 pom.xml に依存関係を追加します。なお、以下に示すXMLはビルドツールにMavenを利用している場合の例です。 <dependencies> <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-actuator </artifactId> </dependency> </dependencies> 次に、Elasticsearchに対するヘルスチェックを有効にします。 Spring Boot Actuatorの公式ドキュメント も併せてご覧ください。 management : health : elasticsearch : enabled : true Java(Spring Boot)とElasticsearchを組み合わせたアプリケーションの運用ノウハウにご興味ある方は、こちらの記事も是非ご覧ください。 techblog.zozo.com 各種バージョンアップ 検索機能のマイクロサービス化の際に行った大きなバージョンアップ作業は、JavaとSwaggerのバージョンアップです。なおバージョンアップ後のバージョンは非公表とさせていただきます。 対象 バージョンアップ前 Java 8 Swagger 2.x Java バージョンアップに伴うコード修正はなく、スムーズにバージョンアップできました。しかし、GCをCMSからG1へ変更したため負荷テストとメモリサイズのチューニングを実施しました。 Swagger Swagger 2.xは古くなっていたため、RESTful APIの標準規格と言われるOpen APIへ変更しました。また、今までは非常に大きな1つのyamlファイルに定義が集約されており、開発し辛い状況でした。そのため、yamlファイルを分割し開発をやり易くしました。 OSSのライセンスチェック アプリケーション内では様々なライブラリを利用しているため、OSSライセンスのチェックを行いました。ここでは、そのチェック方法を紹介します。 具体的には、Spring Bootの依存関係からライセンスの一覧を作成し、各ライブラリのライセンスが社内のOSS利用ガイドラインを順守しているかを目視で確認していきました。なお、OSS利用ガイドラインに関する情報は以下の記事に書かれています。 techblog.zozo.com ライセンス一覧は下記コマンドで出力できます。 $ mvn license:add-third-party -D license.excludedScopes=test $ cat ./target/generated-sources/license/THIRD-PARTY.txt | sort > license.txt 参考: License Maven Plugin license:add-third-party 依存関係も把握しておきたい場合、下記コマンドで出力できます。 $ mvn dependency:tree > dependency_tree.txt 外部サービス 運用・監視には下記の外部サービスを活用しています。どのサービスも運用・監視にはなくてはならないサービスです。個別の説明は省きますので、各社のWebページをご参照ください。 Datadog マイクロサービスのモニタリングとアラート検知 Sentry エラー通知 SonarCloud コードの静的解析 PagerDuty インシデントのオンコール通知 リリース すでに本番稼働しているAPIなので、リクエスト先の切り替えは慎重に実施しました。当然のことですが、通常の開発案件も平行で動いているため、それらの開発案件のリリースの合間をみて検索機能のマイクロサービスをリリースしていきました。 リリース時にやったこと 既存マイクロサービスの検索APIと新APIでの比較テスト 同一のリクエストをそれぞれのAPIへリクエストし、同等の結果が得られることを確認する 既存マイクロサービスの開発案件の差分取込 毎週担当者を決めて差分をウォッチし、検索機能に関係する差分がある場合は内容を確認してコードの差分を取り込む カナリアリリース 検索APIは非常に大量のリクエストを受けるため、1度に全リクエストの切り替えず、カナリアリリースで段階的に切替える カナリアリリースについてご興味ある方は、こちらの記事も是非ご覧ください。 techblog.zozo.com 得られた効果 検索機能に特化したマイクロサービスを構築することで、前述の下記の目的が達成できたかを検証してみます。 開発速度の向上 検索機能に特化したマイクロサービスの開発 開発速度の向上 定量的な測定ができていないため、定性的な評価になってしまいますが、自チームでハンドリングできるマイクロサービスは意思決定が早く、開発作業のスピードは確実に上がっていると感じています。 機能の開発だけでは無く、開発がやり易くなるような改善やリファクタリングもチームメンバーが自発的に実施しているため、チームの気持ちのこもったマイクロサービスへと着々と進化しています。 検索機能に特化したマイクロサービスの開発 本番リリース後はプログラム起因による障害は無く、ZOZOTOWNの検索機能のリクエストを日々安定して処理できています。データベースはSQL Serverを参照することは無く、Elasticsearchのみを参照しており、パフォーマンスとアーキテクチャの両面で想定通りのマイクロサービスが構築できました。 なお、既存マイクロサービスの一部APIでは、まだElasticsearchを参照しているため、完全に目的を達成したとは言えないところが残念ではあります。 まとめ 本記事では、ZOZOTOWNで本番稼働している肥大化したマイクロサービスから検索APIを切り出し、検索機能に特化したマイクロサービスを構築した事例を紹介しました。肥大化したマイクロサービスや役割が多いマイクロサービスをシンプルな形にすることで受けられる恩恵は十分にあると思いました。 ZOZOTOWNにおける検索機能は「ZOZOTOWN利用者が欲しい商品を見つける」ための重要な機能でかつ、リクエスト数も膨大です。今回ご紹介したようにシステムの改修を柔軟に対応できる形へ切り出せた事で、今では更なる検索速度や精度を改善に取り組む環境が整いました。このような検索基盤を開発する経験は個人的にも非常に良い経験でした。 おわりに ZOZOテクノロジーズでは、検索機能を開発・改善していきたいエンジニアを全国から募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co ユニットテストのガイドラインを作成いただいた木目沢さん、ありがとうございました! ↩
ZOZO研究所 の清水です。弊社の 社会人ドクター制度 を活用しながら、「社内外に蓄積されているデータからビジネスへの活用が可能な知見を獲得するための技術」の研究開発に取り組んでいます。 弊社の社会人ドクター制度に関しては、以下の記事をご覧ください。 technote.zozo.com 私が現在取り組んでいるテーマの1つに、「機械学習が導き出した意思決定の理由の可視化」があります。この分野は「Explainable Artificial Intelligence(XAI)」と呼ばれ、近年注目を集めています。 図.XAIに関連する文献数の推移(引用: https://arxiv.org/abs/1910.10045 ) その中でも今回はユーザに対するアイテムの推薦問題に焦点を当て、「なぜこのユーザに対して、このアイテムが推薦されたのか?」という推薦理由の可視化が可能なモデルを紹介します。 本記事の概要 機械学習から得られた意思決定の理由を明確にすることの必要性が増している 「XAI」と呼ばれる研究領域が注目されている Attentionを用いて推薦理由を可視化・解釈可能な、Knowledge Graph Attention Networkという手法を紹介する ZOZOTOWNに蓄積されているデータにKnowledge Graph Attention Networkを適用してみた結果の一部を紹介する 目次 本記事の概要 目次 背景 機械学習が抱える解釈性の課題 Explainable Artificial Intelligence Explainable Recommendation model-agnostic approach model-intrinsic approach Attentionを用いた意思決定の理由の解釈 Knowledge Graph Attention Network 概要 モデル構造と学習 CKG Embedding Layer Attentive Embedding Propagation Layer Prediction Layer 学習の仕組み(まとめ) 入出力 損失関数 実験 実験条件 推薦理由の可視化の例 推薦精度に関する評価 関連手法 終わりに 背景 機械学習が抱える解釈性の課題 近年、機械学習が人間の意思決定を支援したり、代替するような場面が徐々に増えてきています。弊社のサービスにおいても、例えばZOZOTOWNでユーザに推薦するアイテムの選定をする場面など、あらゆる場面で機械学習が活用されています。 機械学習によるアイテムの推薦では、蓄積された過去の購買履歴データなどの情報から、「このユーザにこのアイテムを推薦すべき」という情報を得ます。そして、得られた情報に基づいて、アイテムの推薦が行われています。以下では、この一連を仕組みを「推薦システム」と呼びます。なお、推薦システムの内部で利用されている技術に関しては、過去の記事で紹介しているので、併せてご覧ください。 techblog.zozo.com しかし、この推薦システムを含む、機械学習を用いた意思決定システムは「なぜそのような決定に至ったのか」という意思決定の理由については不明瞭である場合が多いです。不明瞭なままでは良くないとされる場面の分かりやすい具体例としては、自動運転の技術を搭載した自動車に関するものが挙げられます。自動運転の技術を搭載した自動車が事故を起こした場合、事故を起こした際の意思決定の理由が解釈できなければ、改善点を考察することや責任の所在を明らかにすることは困難です。そして、この問題が自動運転の技術を実用化する上での1つのハードルとなっていることは間違いないでしょう。 なお、このような機械学習が抱える解釈性の課題から、消費者庁が発表した AI利活用ガイドライン における「AI利活用原則(AI利用者が留意すべき事項)」には「透明性の原則」が含まれています。 Explainable Artificial Intelligence この問題に対して、「なぜそのような決定に至ったのか」を説明するための研究分野が存在します。この分野はExplainable Artificial Intelligence(XAI)と呼ばれ、特に中身が複雑なディープラーニング技術の実応用が話題となっている現代において注目を集めています。 下図はディープラーニングやアンサンブル学習を用いるような精度の高いモデルほど、モデルの解釈性は低くなるというトレードオフの関係を表しています。 図.モデルの解釈性と精度のトレードオフについて記述した図(引用: https://arxiv.org/abs/1910.10045 ) 図における「高い精度であるが解釈性は低い」と位置付けされているモデルが、近年多くの分野で大きな成果を発揮しています。このことを背景とし、モデルを解釈しようとする当分野も注目を集めています。 この分野の研究に関する情報は、以下の資料にまとまっています。 私のブックマーク「説明可能AI」(Explainable AI) Explainable Artificial Intelligence (XAI): Concepts, Taxonomies, Opportunities and Challenges toward Responsible AI Interpretable Machine Learning 図.XAIのイメージ(引用: https://www.darpa.mil/program/explainable-artificial-intelligence ) Explainable Recommendation 説明可能な推薦システムに関する研究も多数発表されており、 Explainable Recommendation: A Survey and New Perspectives にはそれらが体系的にまとまっています。対象問題が推薦システムであるため、機械学習によって「なぜこのユーザに、このアイテムが推薦されたのか」という推薦理由を把握することを目的としています。 前述の文献 などにおいてEplainable Recommendationは、説得力・有効性・ユーザ満足度などを向上させるのに役立つとされています。そして、実際に様々な企業からこの分野に関連する研究成果が発表されています。 以下はその一例です。 Google Transparent, Scrutable and Explainable User Models for Personalized Recommendation Spotify Explore, Exploit, Explain: Personalizing Explainable Recommendations with Bandits また、以下の事例が実際にサービス化されている分かりやすい例です。 Facebook(app) Why Am I Seeing This? We Have an Answer for You このEplainable Recommendationのアプローチの仕方は、大きく分けて以下の2種類です。 model-agnostic approach(=post-hoc approach) 推薦モデルとは別に推薦理由を解釈(説明)するためのモデルを学習する方法 model-intrinsic approach 何らかの工夫により、予め解釈可能な推薦モデルを学習する方法 model-agnostic approach model-agnostic approachでは、まず推薦モデルを学習させ、その次に推薦の理由を説明するためのモデルの学習を別途行います。 このアプローチでは、事後の学習によって推薦理由の解釈を得るため、推薦モデルから直接理由が得られている訳ではありません。故に「本当に意思決定用のモデルを正確に説明できている(理由が正確に表現できている)」という保証はありません。また、そこに対して様々な工夫もされていますが、今回は詳しく扱いません。 しかし、この方法を用いると意思決定モデル自体を、どれだけ複雑にしても問題にならないというメリットがあります。 また、人間の意思決定メカニズムは以下のステップで行われる場合もあります。 まず直感的な意思決定を行う その意思決定に対して、後から理由付けを行う この意思決定のパターンを再現しているという意味では面白いアプローチです。具体的には、「とあるアイテムに一目惚れして購入を決意した後に、なぜこのアイテムが気に入ったのかを後から考える」ような流れを再現していると言えます。 このアプローチの関連研究として以下のものが挙げられます。 Explanation Mining: Post Hoc Interpretability of Latent Factor Models for Recommendation Systems Posthoc interpretability of learning to rank models using secondary training data EXS: Explainable search using local model agnostic interpretability A Reinforcement Learning Framework for Explainable Recommendation Incorporating interpretability into latent factor models via fast influence analysis Explore, exploit, and explain: personalizing explainable recommendations with bandits model-intrinsic approach 前述のmodel-agnostic approachと比較し、model-intrinsic approachは、意思決定の理由を推薦モデルから直接獲得できる点が異なります。 こちらのアプローチでは、最初から合理的な理由に基づいて意思決定を行うような状態の再現を目標としています。具体的には、どのポイント(ブランドや値段など)をどのくらい重要視するのかなどを考慮しながら、最終的にそのアイテムを購入するかを決定する流れを再現しています。この状況において、他者から「なぜそれにしたの?」と質問された場合に回答する理由は、後付けしたものではなく、購入に至った正確な理由であるはずです。 こちらのアプローチの難しい点としては、このモデルの出力が直接推薦に活用されるため、説明可能性を担保しながらも高い推薦精度を実現する必要があることです。また、闇雲に活用したい全ての補助情報をモデルに学習させることは推薦モデル自体の精度の低下や計算時間の増加を招くため、どの補助情報を活用するかについても精査する必要があります。 なお、関連研究を本記事の末尾にいくつか挙げているので、興味のある方はそちらをご覧ください。 Attentionを用いた意思決定の理由の解釈 model-intrinsic approachの中でも、近年注目されているAttentionなどを用いることで高い推薦精度を保ちながら、意思決定の理由を直接的に解釈可能とする方法があります。この方法を用いることで、分析者は「なぜこのユーザにこのアイテムを推薦したか」という理由を、いくつかの要素とその寄与の大きさに分けて把握することが可能になります。「いくつかの要素」の部分は入力データとした情報に含まれる要素(ユーザ・アイテム・それらの補助情報など)となり、「寄与の大きさ」の部分はAttentionで表現します。 この類の手法は近年脚光を浴びており、多くの手法が提案されています。その中でも今回は Knowledge Graph Attention Network (KGAT)を次章で紹介します。KGATはAttentionを用いて、どの繋がりを重視するかを考慮しながら、グラフ構造のデータを学習する Graph Attention Networks (GATs)をベースとした推薦モデルの一種です。 図.グラフ構造のイメージ Knowledge Graph Attention Network 概要 Wangら は、ユーザとアイテムの2部グラフと、アイテムとアイテムの補助情報からなる知識グラフを使ったGATsベースの推薦モデルを提案しました。アイテムの補助情報とは、ZOZOTOWNのデータで言えば、ショップやブランドなどのアイテムに付随する情報です。 このモデルはGraph Neural Networksと呼ばれるモデル群において、関係性の学習にAttentionを採用したGATsに、補助情報(知識グラフ)を取り入れたモデルとして位置付けられます。ディープラーニングにより特徴量を自動的に獲得するend-to-end方式の学習を実現することで、複雑な顧客の嗜好やネットワーク構造の学習を可能にしています。 また、補助情報を用いることで、購買履歴データが十分に蓄積されていないユーザに対しても精度の高い推薦を実現します。さらに、得られたAttentionを分析することで推薦の根拠を示すことができるため、このモデルはXAIの分野においてはmodel-intrinsic approachに位置付けられます。 構築された推薦システムの出力の根拠を人間が解釈可能な形で示してくれるため、実際のマーケティングにおけるデータに応用することで強力な成果を発揮することが期待できます。 モデル構造と学習 KGATは3つのレイヤーを通して学習を遂行します。 それぞれのレイヤーの概要を以下で解説します。 CKG Embedding Layer Attentive Embedding Propagation Layer Prediction Layer 本記事では、より多くの方に概要だけでも理解していただけるよう、数式を記載せずに言葉で解説をしていきます。それが逆に分かりにくい方は、 原著の論文 と照らし合わせながら読んでいただけると幸いです。 KGATのモデルの全体像は以下の図の通りです。 図.KGATの構造のイメージ (引用: https://arxiv.org/abs/1905.07854 ) CKG Embedding Layer ユーザとアイテムの2部グラフと、アイテムやユーザの補助情報からなる知識グラフを併せた「協調知識グラフ」の構造を保持した(各ノード・エッジに対する)埋め込み表現を獲得します。 学習の際は、 TransR というグラフ埋め込みの手法を用いて各Tripletをベクトル化し、グラフ上に存在するTripletと存在しないTripletの差を最大化するようにパラメータを更新します。なお、Tripletとは、先頭ノード・ エッジ・末尾ノードの3点セットのことを指します。 図.Tripletの例 これにより、「ユーザ」と「ユーザが購入したアイテム」、「ユーザ」と「ユーザの補助情報」、「アイテム」と「アイテムの補助情報」のノード同士が埋め込み空間上で近くに配置されるように埋め込み表現が学習されていきます。 図.CKG Embedding Layerのイメージ Attentive Embedding Propagation Layer 各ノードやエッジに対する埋め込み表現をもとに、各Tripletに対して重要度を算出します。そして、この重要度をもとにどの関係性を重視するかを考慮しながら、Prediction Layerにおいて購買確率の算出に利用するための埋め込み表現を、各アイテムとユーザに対して算出します。 この層は、 層の構造を有しており、 次近傍( 個先の隣接したノード)までの関係性を考慮可能です。周辺ノードの埋め込み表現を各Tripletの重要度で重み付けした平均値を算出し、周辺ノードの特徴を集約することで新しい埋め込み表現を獲得します。そして、このレイヤーから得られた各アイテムとユーザに対する新たな埋め込み表現を、次のPrediction Layerにおいて購買確率の算出に利用します。 つまり、推薦において重要と判断される 次近傍の関係性に従って、より洗練された、各ユーザとアイテムに関する購買確率の計算に用いるための埋め込み表現を獲得します。 図.Attentive Embedding Propagation Layerのイメージ Prediction Layer Attentive Embedding Propagation Layerから得られた各ユーザと各アイテムに対する埋め込み表現をもとに、各ユーザとアイテムのペアについて、購買確率を算出します。そして、実際に購買が発生したユーザとアイテムのペアに対して計算されるスコア(購買確率)と、発生していないペアに対して計算されるスコアの差が大きくなるように学習を遂行します。 図.Prediction Layerのイメージ 学習の仕組み(まとめ) 長めの説明となってしまいましたが、学習の仕組みを簡単にまとめると、以下のようになります。 入出力 入力 購買履歴データと、アイテムやユーザの補助情報を併せたデータ 出力 各ユーザが各アイテムを購入する確率 損失関数 Pairwise Ranking損失 埋め込み空間上におけるノードの位置関係が入力データの構造に則っているかに関する損失 Bayesian Personalized Ranking損失 埋め込み表現などから算出した購買確率がユーザの行動を再現できているかに関する損失 上記の双方を考慮して学習を遂行。 実験 ここまで紹介してきたKGATを、ZOZOTOWNに蓄積されている購買履歴データと各ユーザとアイテムの補助情報に適用し、得られた結果を用いて実際に推薦理由の可視化を行ってみた例を紹介します。さらに、推薦精度の評価実験を行い、KGATが推薦精度の面でどの程度有効であるのかを確認した結果を紹介します。 実験条件 今回の実験では、2020年2月〜2021年1月の1年間の購買回数が5回以上60回未満のユーザからランダムにサンプリングを行い、抽出された購買履歴データを利用します。 補助情報にはアイテムのブランド・ショップ・カテゴリ・価格帯、ユーザの年代・性別・お気に入りブランドやショップなどの全17種類のデータを用いました。購買履歴データは約30万件、補助情報は約70万件です。 こちらの文献 の実験条件を参考に、各グラフの埋め込み表現の次元数は64、Attentive Embedding Propagation Layerは[64・32・16]次元の3層としました。また、確認する精度指標も同様の決め方で、TopN精度(Recall・NDCG)としています。 比較手法は補助情報を用いない手法のベースラインとしてBPRMF、同じく補助情報を含んだグラフ構造を学習する手法であるCKEとCFKGとしています。 BPRMF Bayesian Personalize Ranking Matrix Factorization CKE Collaborative Knowledge base Embedding CFKG Collaborative Filtering over Knowledge Graph 推薦理由の可視化の例 まず、記事の前半でメイントピックとして解説してきた「推薦理由の可視化」についてです。KGATの出力結果をそのまま用いることで、各ユーザに対して「なぜそのアイテムが推薦されたか」を容易に説明できます。今回はどのような形で説明可能になったのかを紹介するために1つだけ例を紹介します。 以下の図は、実際に得られた結果の中から抽出してきた例で、「ユーザ に対してアイテム が推薦されている」状況を表現しています。ユーザ から、実際に推薦されたアイテム までのノードとエッジを辿り、それらの重要度を確認することで推薦理由を把握できます。 図.推薦理由の可視化マップの例 このグラフを見ると、ユーザ に対してアイテム が推薦された理由は、以下の点であることが分かります。 同ブランド のアイテム を過去に購入していること ブランド をお気に入り登録していること アイテム と同じショップ のアイテムであること アイテム と同じタイプ のアイテムであること また、重要度(エッジ上に記載されている数値)を確認することで、それぞれがどの程度推薦に寄与しているのかを定量的に把握できます。 この結果を活用し、ユーザ にアイテム を推薦する際、アイテム がブランド のアイテムであることを強調するなどの施策が容易に考えられます。また、推薦するアイテムと併せて、単純に推薦理由とスコアを並べて表示する施策も考えられます。これにより、ユーザは「だからこのアイテムを良いと感じるのか」「だからあまり良いと思わないアイテムが推薦されたのか」のように、納得感を持って買い物を楽しめるかもしれません。 今回は結果の活用事例として推薦理由の可視化のみを紹介しました。しかし、実際には他にも各ノード(アイテム・ユーザ・補助情報)やエッジに対して埋め込み表現が得られているので、これらを分析することも施策立案の一助となります。実際に適用して得られた結果を多角的に分析した結果、改めてとても汎用性の高いモデルだなと感じています。 推薦精度に関する評価 以下に示す結果の表を見ると、KGATは同様の補助情報を用いる他の手法と比較して、精度面でも有効なモデルであることが分かります。特にBPRMFよりも精度が高いことから、補助情報を活用することの有効性を示唆しています。 また、CKEやCFKGよりも精度が高いことから、KGATが上述した学習アルゴリズムを通して補助情報を含んだグラフ構造を上手に学習できていることが考えられます。 表.評価実験の結果比較 また、下図より、他の手法と比較して学習データ内にまだ多くの購買履歴データが蓄積されていないユーザ群に対しても、ある程度頑健な推薦ができていることが分かります。つまり、コールドスタート問題にも対応できていると言えます。 図.学習データに含まれる購買履歴数で層別した各ユーザ群に対する推薦の精度 推薦精度に関する評価実験の結果をまとめると、以下のことが分かります。 KGATが他の類似したモデルと比較して、高い推薦精度が期待できるモデルであること 過去にZOZOTOWNであまり商品を購入していないユーザに対しても、ユーザやアイテムの補助情報を上手に学習し、効果的な推薦ができていること 関連手法 XAIの文脈でKGATを紹介しましたが、Attentionを活用した解釈可能な推薦システムの研究としては以下のものも挙げられます。 Interpretable Convolutional Neural Networks with Dual Local and Global Attention for Review Rating Prediction A Context-Aware User-Item Representation Learning for Item Recommendation Neural attentional rating regression with review-level explanations Sequential Recommendation with User Memory Networks また、知識グラフの学習をベースとする解釈可能な推薦システムの研究としては、以下などが挙げられます。 Explainable entity-based recommendations with knowledge graphs Improving sequential recommendation with knowledge-enhanced memory networks Learning Heterogeneous Knowledge Base Embeddings for Explainable Recommendation Reinforcement knowledge graph reasoning for explainable recommendation 終わりに 今回はXAIの文脈で、KGATの紹介をしました。この分野は現在非常にアツく、様々な方法が提案されているので、将来的にはそれらを網羅的に紹介する記事も執筆したいと思います。最後までお読みいただき、ありがとうございました! ZOZOテクノロジーズでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは、ECプラットフォーム部の権守です。普段はID基盤やAPI Gatewayの開発を行い、ZOZOTOWNのリプレイスに携わっています。 本記事では、ID基盤で開発・導入したMySQL実行計画の簡易検査を行うツールを紹介します。 ツール開発の経緯 RDBにおけるテーブル設計は利用するクエリに応じて適切なインデックスを設定するなど専門的な知識を必要とし、設計できる人が限られてきます。しかし、アプリケーション上で利用されるクエリは機能の追加・改修に伴って日々変化していくため、それら全てに目を通し、漏れなく適切な設計することは困難です。そこで、専門的な知識がなくても設計に問題がないかの簡易的な検査を行えるツールを開発し、CIに組み込むことで自動的に問題を検出できるようにしました。 ツール開発のアプローチ ID基盤ではDBMSとしてAmazon Aurora MySQLを使用しています。そのため、ツールはMySQL向けのものとして開発しました。また、 Amazon Aurora MySQL 2.0がMySQL 5.7.12によるバグ修正までを取り込んでいる 背景から、動作検証もMySQL 5.7.12で行っています。 スロークエリログを用いたアプローチ スロークエリログ を監視することで、テーブル設計の不備を検知できる場合があります。MySQLのスロークエリログはクエリの所要時間が設定した値を超えている、または行参照にインデックスを使用していない場合にそのクエリを出力できます。 前者は、レコード数が増加し、クエリの所要時間が増えてきて初めて検知できるため、パフォーマンスの劣化を未然に防ぐには手間がかかります。具体的には、テスト環境に本番同等のスペックと十分なレコード数を用意し、検証する必要があります。 一方、後者の場合は、有用な情報ではありますが、問題を検知できないパターンがあります。次のような定義のテーブルとクエリを例に考えてみます。 CREATE TABLE `events` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR ( 32 ) NOT NULL , `from_date` DATE NOT NULL , ` to_date ` DATE NOT NULL , PRIMARY KEY(`id`), INDEX `idx_events_from_date_to_date`(`from_date`, ` to_date `) ); SELECT `id` FROM `events` WHERE ` to_date ` < " 2021-04-01 " ; この場合、MySQLのオプティマイザは idx_events_from_date_to_date インデックスを使用する可能性がありますが、その場合のスキャン範囲はインデックス全体となります。なぜなら、このインデックスは下に示す表のように from_date を優先する順序でインデックスツリーが構成されているため、 to_date に関する比較は全体を見る必要があるからです。スロークエリログはこのようなスキャン範囲が全体に及ぶような場合であってもインデックスを使用したということでログには出力されません。 from_date to_date 2020-04-01 2021-08-01 2020-08-01 2021-03-31 2020-10-01 2021-04-30 2020-12-01 2021-02-28 EXPLAINステートメントを用いたアプローチ EXPLAINステートメントを用いることで、クエリの実行計画に関する情報を知ることができます。実行計画とは、クエリが実行されるに当たり、どのようにテーブルのスキャンや結合、ソートなどが行われるかを示したものです。これを見ることでインデックスの使用の有無を知ることができ、どこにインデックスを追加するとパフォーマンスを改善できるかといったことが分かります。 このアプローチを採用するには次の課題があります。 検査対象のクエリの管理 EXPLAINステートメントの出力結果を元に、設計に不備がないかを判断するロジックの開発 スロークエリログを使ったアプローチであれば、クエリの実行に伴って自動的にログが出力されるため、検査対象を別途取得する必要がありません。一方、こちらのアプローチでは明示的にEXPLAINステートメントにクエリを与える必要があるため、そのクエリを管理しなければいけません。また、EXPLAINステートメントの結果は独自の出力フォーマットであるため、そのままでは前提知識のない人が見ても、結果に問題があるのかの判断ができません。 しかし、これらの課題を解決できれば、前述したスロークエリログを用いたアプローチでは対応できなかったパターンにも対応できるようになります。そのため、今回はこちらのアプローチを採用しました。 検査対象のクエリの管理 EXPLAINステートメントの対象とするクエリを管理する必要がありますが、そのための設定ファイルを作ってしまうと、アプリケーションコード中に記述されているものと二重管理になってしまいます。その結果、クエリを追加・変更するような機能の改修が入った際に設定ファイルの更新も必要になってしまい、漏れなく管理することは困難です。そのため、二重管理しないで済む方法を検討しました。 検査対象の取得 シンプルに考えると「アプリケーションコードを解析して抽出する方法」があります。しかし、プレースホルダーを含むクエリの場合には、クエリに与える引数の動的解析が必要となり実装が複雑になります。そこで、MySQLの 一般クエリログ に注目しました。一般クエリログはMySQLサーバーが受け取ったSQLステートメントを記録したものです。今回はテストコードを実行した際に出力される一般クエリログを用いて、検査対象のクエリを取得するようにしました。 検査対象の選別 テスト実行によって出力された一般クエリログを利用することで、アプリケーション上で利用されるクエリを取得できます。しかし、それらのクエリの中には検査対象外としたいクエリが含まれることもあります。例えば、書き込み操作が多いテーブルでは、インデックスを意図的に追加していない場合もあります。そのような場合を考慮し、除外するクエリを判別するためのアノテーションをアプリケーションコード中に与えることを考えました。なお、詳細は後述しますが、アプリケーションコードにはGo言語を利用しています。 // @mydctr:skip q := "SELECT * FROM `users` WHERE `name` = ?" 上記のコードのコメント部分がアノテーションに相当し、このクエリを検査対象外とすることを示しています。 mydctr は開発したツールの名称で、アノテーションのプレフィックスとして記述しています。 EXPLAINステートメントの出力結果の判断ロジック まず、EXPLAINステートメントの 出力フォーマット の理解が必要です。今回は type カラムと Extra カラムに注目しました。 type カラムはテーブルへのレコードアクセスをどのように行っているかを、 Extra カラムはオプティマイザがどのような戦略を選択したかなどの追加情報を示しています。 今回のツールでは、検査によって以下の2つのパターンを検出したいと考えました。 検索条件の解決に適切なインデックスがなく、テーブル全体もしくはインデックスツリー全体のスキャンが行われるパターン ソートをインデックスのみで解決できず、クイックソートが行われるパターン 1つの目のパターンを判断するには、まず、 type カラムの値に注目します。 type カラムが ALL の場合にはテーブル全体を、 index の場合にはインデックスツリー全体をスキャンしていることが分かります。しかし、これだけで判断すると WHERE 句を利用していないクエリでもテーブル全体、またはインデックスツリー全体をスキャンするため、警告が出力されてしまいます。そこで、 Extra カラムに Using where の値が含まれているかどうかも合わせて確認する必要があります。 Using where があれば行がフェッチされた後に、 WHERE 句によって絞り込まれていることが分かります。そのため、 WHERE 句を利用していないクエリを対象から除外できます。 2つ目のパターンを判断するには、 Extra カラムの値に Using filesort の値が含まれているかどうかを確認することで判断できます。 ツールの実装とCIへの組み込み ツールの具体的な実装方法を紹介します。 開発言語にはID基盤の技術スタックと同じGo言語を採用しました。採用した理由は、ID基盤を構成するGoプログラムからアノテーションを抽出する際に、 go/parser パッケージを用いることで簡単にGoプログラムを構文解析できるからです。 一般クエリログからのクエリ取得 まず、一般クエリログのフォーマットを簡単に説明します。 ログファイルには以下のように各行に操作時刻、接続ID、コマンド、コマンド引数が順に記録されています。 2021-05-24T07:36:41.773817+09:00 1 Query SELECT `name` FROM `users` WHERE `id` = 1 プリペアドステートメントを利用した場合は次のように出力されます。 2021-05-24T07:38:02.260023+09:00 1 Prepare SELECT `name` FROM `users` WHERE `id` = ? 2021-05-24T07:38:02.260696+09:00 1 Execute SELECT `name` FROM `users` WHERE `id` = 1 2021-05-24T07:38:02.262475+09:00 1 Close stmt また、接続時には次のようにログが記録されます。 2021-05-24T07:37:41.808776+09:00 3 Connect user@host on db using TCP/IP 一般クエリログにはシステムデータベースの作成といった初期化のクエリも含まれます。テスト実行中に出力されたクエリだけを取得するために、初期化のクエリをスキップします。初期化のクエリはrootユーザーで実行されるため、テスト内で利用されるユーザーによる接続ログまでスキップすることで、初期化のクエリを取り除けます。 テスト実行中のログにも検査に利用しないログが多く含まれています。ここから検査に利用するクエリを抽出するにはコマンドが Query もしくは Execute であるログのクエリだけに絞ればよいので、その条件を表す正規表現を用います。 具体的な実装の例を以下に示します。 func extractQueries(user string , generalLog io.Reader ) ([] string , error ) { re, e := regexp.Compile( ` (Execute|Query)\t(.+)$` ) if e != nil { return nil , e } queries := [] string {} scanner := bufio.NewScanner(generalLog) skipped := false stopper := fmt.Sprintf( "Connect \t %v@" , user) for scanner.Scan() { line := scanner.Text() if !skipped && !strings.Contains(line, stopper) { continue } skipped = true matches := re.FindStringSubmatch(line) if len (matches) > 1 { queries = append (queries, matches[ 2 ]) } } if e := scanner.Err(); e != nil { return nil , e } return queries, nil } Goプログラム中のアノテーションの抽出 ID基盤ではGo言語を用いて開発しているため、Goプログラム中のアノテーションの抽出を実装しました。 まず、検査対象とするアプリケーション配下のGoファイルのファイルパス一覧を取得します。次に、各Goファイルからコメント部分を取得するために構文解析し、取得したコメントを正規表現でアノテーションのフォーマットに一致するコメントだけに絞り込みます。アノテーションコメントを絞り込んだ後は、コメントが掛かっている変数への代入文を取得し、代入しているSQLのクエリ文字列を抽出します。コメントに書かれたアノテーションの種類毎にクエリの一覧を作成し、最終的にJSON形式で出力します。 例えば、アプリケーションコード中に次のような記述があるとします。 // @mydctr:skip q1 := "SELECT * FROM `users` WHERE `name` = ?" // @mydctr:skip q2 := "SELECT * FROM `users` WHERE `age` > ?" この場合、次のようなJSONが出力されます。 { " skip ": [ " SELECT * FROM `users` WHERE `name` = ? "," SELECT * FROM `users` WHERE `age` > ? " ]} 具体的な実装は以下の通りです。 type annotation string const ( annotationSkip annotation = "skip" annotationTodo annotation = "todo" annotationAll annotation = "all" annotationFilesort annotation = "filesort" annotationTemporary annotation = "temporary" annotationPrefix = "@mydctr:" ) var annotationExpression = regexp.MustCompile(annotationPrefix + "(.+)" ) func NewAnnotation(a string ) (annotation, error ) { switch annotation(a) { case annotationAll, annotationSkip, annotationTodo, annotationFilesort, annotationTemporary: return annotation(a), nil } return "" , errors.New( "invalid annotation" ) } func ExtractAnnotatedQueries(dir string ) ([] byte , error ) { annotatedQueries := map [annotation][] string {} // 指定したディレクトリ配下のファイル・ディレクトリに対する処理 e := filepath.Walk(dir, func (path string , info os.FileInfo, e error ) error { if e != nil { return e } // Goファイルのみに処理を限定 if !strings.HasSuffix(info.Name(), ".go" ) { return nil } queries, e := extractAnnotatedQueries(path) if e != nil { return e } for k, v := range queries { annotatedQueries[k] = append (annotatedQueries[k], v...) } return nil }) if e != nil { return nil , e } return json.Marshal(annotatedQueries) } func extractAnnotatedQueries(filename string ) ( map [annotation][] string , error ) { annotatedQueries := map [annotation][] string {} fset := token.NewFileSet() // Goファイルを構文解析 f, e := parser.ParseFile(fset, filename, nil , parser.ParseComments) if e != nil { return nil , e } // コメントとその対象の関連付けを取得 commentMap := ast.NewCommentMap(fset, f, f.Comments) for node, commentGroups := range commentMap { annotationComment := func () string { for _, commentGroup := range commentGroups { for _, comment := range commentGroup.List { // 正規表現を用いてアノテーションコメントのみを取得 matches := annotationExpression.FindStringSubmatch(comment.Text) if len (matches) <= 1 { continue } return matches[ 1 ] } } return "" }() if annotationComment == "" { continue } // コメントに記述されたアノテーションの検証 annotation, e := NewAnnotation(annotationComment) if e != nil { return nil , e } // コメントの対象が代入文であることをチェック if stmt, ok := node.(*ast.AssignStmt); ok { // 複数代入の場合には右辺も複数になる for _, expr := range stmt.Rhs { // 右辺に文字列の結合が含まれる場合にも対応 joined, e := joinStringExpression(expr) if e != nil { return nil , e } annotatedQueries[annotation] = append (annotatedQueries[annotation], joined) } } } return annotatedQueries, nil } func joinStringExpression(expr ast.Expr) ( string , error ) { switch expression := expr.( type ) { case *ast.BinaryExpr: // 文字列結合の場合には再帰的に呼び出し if expression.Op != token.ADD { return "" , errors.New( "contains not add operator" ) } x, e := joinStringExpression(expression.X) if e != nil { return "" , e } y, e := joinStringExpression(expression.Y) if e != nil { return "" , e } return x + y, nil case *ast.BasicLit: if expression.Kind != token.STRING { return "" , errors.New( "contains not string literal" ) } fset := token.NewFileSet() // \nなどにも対応するためにEvalを利用 evaluated, e := types.Eval(fset, nil , token.NoPos, expression.Value) if e != nil { return "" , e } return constant.StringVal(evaluated.Value), nil } return "" , errors.New( "contains not supported expression" ) } EXPLAINステートメント結果の解析 EXPLAINステートメントを実行する前に一般クエリログから抽出したクエリに対し、アノテーションの存在有無を確認します。Goプログラムから抽出したクエリはプレースホルダーを含むものなので、実行された具体的な値を伴うクエリと比較する際には正規表現における任意の文字列を表す表記に置き換えて比較します。 例えば、次のようなアノテーションが抽出されたとします。 { " skip ": [ " SELECT * FROM `users` WHERE `name` = ? " ]} この場合、次のような正規表現に置き換えて比較されます。 "SELECT * FROM `users` WHERE `name` = .+" 実装は以下の通りです。 var annotatedQueries map [annotation][] string e := json.NewDecoder(annotationReader).Decode(&annotatedQueries) if e != nil { return e } // プレースホルダーを含むクエリを正規表現に変換 annotationPatterns := map [annotation][]*regexp.Regexp{} for a, queries := range annotatedQueries { patterns := []*regexp.Regexp{} for _, q := range queries { pattern, e := regexp.Compile(strings.ReplaceAll(regexp.QuoteMeta(q), `\?` , ".+" )) if e != nil { return e } patterns = append (patterns, pattern) } annotationPatterns[a] = patterns } func findAnnotations(query string , patternMap map [annotation][]*regexp.Regexp) map [annotation] struct {} { annotations := map [annotation] struct {}{} for a, patterns := range patternMap { found := func (query string , patterns []*regexp.Regexp) bool { for _, pattern := range patterns { matched := pattern.MatchString(query) if matched { return true } } return false }(query, patterns) if found { annotations[a] = struct {}{} } } return annotations } skip アノテーション、もしくは todo アノテーションが存在した場合にはクエリを検査対象外とします。それ以外の場合にEXPLAINステートメントを実行していきます。 todo アノテーションは修正予定のあるものに、 skip アノテーションはテストコード内でのみ利用されるクエリなどの修正予定がないものに使う想定です。 EXPLAINステートメントの出力結果を元に、修正を要する可能性があるものに対して警告を出力します。本記事の執筆時点では警告の種類は次のものを実装しています。 全体のスキャン ファイルソート 一時テーブル 「EXPLAINステートメントの出力結果の判断ロジック」の章で挙げたパターンに加え、一時テーブルの使用に関しても警告を出力するようにしました。 全体のスキャンに関する警告は適切なインデックスが設定されておらず、テーブル全体もしくはインデックスツリー全体のスキャンが行われる場合に出力されます。 ここで注意すべき点は、テーブル内のデータに依存してEXPLAINステートメントの出力結果が変わることです。例えば、適切にインデックスが設定されており、インデックスの範囲検索が有効となる条件文を含むクエリがあるとします。この場合、通常は条件に一致する範囲のみをスキャンするのでインデックスツリー全体のスキャンとはなりません。しかし、条件の値が全件を取得するような値であった場合、EXPLAINステートメントの出力結果はインデックスツリー全体のスキャンを表すこととなり、警告が出力されてしまいます。 この問題を回避するには、実際のクエリ実行時に格納されているデータと近い傾向のデータをダミーデータとして検査実行前にテーブルへ挿入しておくことが必要です。また、テーブル内のレコード数が数件しかない場合には、オプティマイザがインデックスを使うよりテーブル全体をスキャンした方が速いと判断する場合もあります。これに関してはダミーデータをある程度入れておくことで誤った警告が出力されることを回避できます。実装上、本当に全体のスキャンが必要な場合にはツールにそれを知らせるために all アノテーションを使います。これにより、この警告の対象外にできます。 ファイルソートの警告はソート時にインデックスを利用できずソート処理が実行される場合に出力されます。ソートのためのインデックスをあえて追加しない場合には、 filesort アノテーションを使うことでこの警告の対象外にできます。 一時テーブルの警告はクエリの実行中に一時テーブルを必要とする場合に出力されます。一時テーブルはその大きさが小さい場合はメモリ上に作成されますが、大きくなった場合にはディスク上に作成されます。ディスク上へテーブル作成するコストを考慮すると、一時テーブルを必要としている場合には何らかの対応を求められる可能性が高いです。 具体的な対応としては、クエリの改善やメモリ上に作成する一時テーブルの最大サイズを設定する tmp_table_size 変数の調整が必要になる場合があります。この警告は実装の容易さに対して、有用な情報であると判断したので、追加で実装しました。 集計用のクエリなど一時テーブルの利用を許容する場合には、 temporary アノテーションを使うことでこの警告の対象外にできます。 実装は以下の通りです。 type DSN struct { User string Password string Host string Port int DB string } type explainResult struct { id int selectType sql.NullString table sql.NullString partitions sql.NullString joinType sql.NullString possibleKeys sql.NullString key sql.NullString keyLen sql.NullInt32 ref sql.NullString rows sql.NullInt64 filtered sql.NullFloat64 extra sql.NullString } func Examine(generalLog io.Reader , annotationReader io.Reader , dsn DSN) error { // ... プレースホルダーを含むクエリの正規表現変換処理 db, e := sql.Open( "mysql" , fmt.Sprintf( "%v:%v@tcp(%v:%d)/%v" , dsn.User, dsn.Password, dsn.Host, dsn.Port, dsn.DB)) if e != nil { return e } defer db.Close() queries, e := extractQueries(dsn.User, generalLog) if e != nil { return e } for _, q := range queries { // 検査対象をSELECT、UPDATE、DELETEに限定 if !strings.HasPrefix(q, "SELECT" ) && !strings.HasPrefix(q, "UPDATE" ) && !strings.HasPrefix(q, "DELETE" ) { continue } annotations := findAnnotations(q, annotationPatterns) if _, ok := annotations[annotationSkip]; ok { continue } if _, ok := annotations[annotationTodo]; ok { continue } // EXPLAINステートメントの実行 rows, e := db.Query( "EXPLAIN " + q) if e != nil { panic (e) } defer rows.Close() // EXPLAINステートメントの出力結果を元に警告を作成 warnings := [] string {} results := []explainResult{} for rows.Next() { var r explainResult e = rows.Scan(&r.id, &r.selectType, &r.table, &r.partitions, &r.joinType, &r.possibleKeys, &r.key, &r.keyLen, &r.ref, &r.rows, &r.filtered, &r.extra) if e != nil { panic (e) } if r.joinType.Valid && (r.joinType.String == "ALL" || r.joinType.String == "index" ) && r.extra.Valid && strings.Contains(r.extra.String, "Using where" ) { if _, ok := annotations[annotationAll]; !ok { warnings = append (warnings, "絞り込みに必要なインデックスが不足している可能性があります" ) } } if strings.Contains(r.extra.String, "Using filesort" ) { if _, ok := annotations[annotationFilesort]; !ok { warnings = append (warnings, "インデックスが用いられていないソート処理が行われています" ) } } if strings.Contains(r.extra.String, "Using temporary" ) { if _, ok := annotations[annotationTemporary]; !ok { warnings = append (warnings, "クエリの実行に一時テーブルを必要としています" ) } } results = append (results, r) } if e := rows.Close(); e != nil { panic (e) } if e := rows.Err(); e != nil { panic (e) } if len (warnings) == 0 { continue } // 警告がある場合に、クエリとEXPLAINステートメントの出力結果を伴って出力 fmt.Println(q) table := tablewriter.NewWriter(os.Stdout) table.SetHeader([] string { "id" , "select_type" , "table" , "partitions" , "type" , "possible_keys" , "key" , "key_len" , "ref" , "rows" , "filtered" , "extra" }) for _, r := range results { table.Append([] string { strconv.Itoa(r.id), mapFromNullString(r.selectType), mapFromNullString(r.table), mapFromNullString(r.partitions), mapFromNullString(r.joinType), mapFromNullString(r.possibleKeys), mapFromNullString(r.key), mapFromNullInt32(r.keyLen), mapFromNullString(r.ref), mapFromNullInt64(r.rows), mapFromNullFloat64(r.filtered), mapFromNullString(r.extra), }) } table.Render() for _, warning := range warnings { fmt.Println(warning) } fmt.Println( "" ) } return nil } func mapFromNullString(s sql.NullString) string { if !s.Valid { return "NULL" } return s.String } func mapFromNullInt32(s sql.NullInt32) string { if !s.Valid { return "NULL" } return strconv.Itoa( int (s.Int32)) } func mapFromNullInt64(s sql.NullInt64) string { if !s.Valid { return "NULL" } return strconv.Itoa( int (s.Int64)) } func mapFromNullFloat64(s sql.NullFloat64) string { if !s.Valid { return "NULL" } return fmt.Sprintf( "%f" , s.Float64) } CIへの組み込み CIへの組み込みは次のような操作で実現できます。 テストコード実行 テスト用データベース内のデータクリア ダミーデータの挿入 アノテーション抽出 検査実行 既存テストは、プルリクエストの作成・更新・マージのタイミングで実行するようしています。上記の操作は既存テストに追加する形で実行されるように組み込んだので、同様にプルリクエストの作成・更新・マージのタイミングで実行されます。 アノテーションの抽出は、現状はGo言語にしか対応していないため、他の言語によって実装されたアプリケーションで利用する場合は 4. のステップを省略しています。ただし、アノテーションはシンプルなJSONで表現されているため、手動で管理することによって検査ツールにアノテーションを与えることは可能です。 まとめ MySQLの実行計画を簡易的に検査するためのツールを開発し、CIに組み込みました。それによってリリース前に問題のある実行計画をある程度把握できるようになりました。 今後の改善点は、一般クエリログに同様のクエリが存在した場合に同じ警告を重複して出力しないようにまとめあげる点などが挙げられます。また、開発して間もないので運用を通して改善した後にOSSとして公開できればと考えています。 最後に、ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに こんにちは。ブランドソリューション開発部 プロダクト開発チームの杉田です。 Fulfillment by ZOZO (以下、FBZ)が提供するAPIシステムの開発・運用を担当しています。 本記事では、サーバーレスアーキテクチャを採用しているFBZのAPIを例に、Datadog APMを使った分散トレーシングの導入手順と運用する際のポイントを紹介します。 「サーバーレスアーキテクチャを採用しているけど分散トレーシングを導入していない」という方や、「既にDatadogは活用しているけどAPMの機能は使っていない」という方に読んでいただけると幸いです。 FBZにおけるサービス監視 FBZでは、CloudWatchメトリクスやAWS Lambda、API Gatewayのログを解析し、PagerDutyやDatadogなどの外部サービスに連携して監視をしています。最近では、Lambda Destinationsの活用や頻度ベースによるアラート通知を実施したことで、サービス監視に要していた運用工数の削減を実現しました。 運用改善のためにアラート最適化を行った事例を過去記事で紹介しているので、併せて御覧ください。 techblog.zozo.com サーバーレス監視の課題 アラート通知の最適化により、サービス監視を効率的に行える環境を構築できました。その結果、APIにリクエストが送られてからレスポンスを返すまで、以下のような処理の流れの構成になっています。 しかし、この処理の流れを起因とする課題も残っていました。サーバーレスアーキテクチャでは、複数のサービスを組み合わせて構築するため、1サービスだけのログや個々のメトリクスだけではボトルネックの特定は困難です。特に、FBZ APIでレスポンスの遅延やタイムアウトが発生した場合、AWS LambdaやAPI Gatewayなど複数ログをまたがって確認する必要があり、調査しづらいという課題がありました。 分散トレーシングの導入 前述の課題を解決するために、Datadog社が提供している Datadog APM というサービスを導入しました。分散トレーシングを実現するためのツールはいくつかありましたが、以下の理由でDatadog APMの採用に至りました。 Serverless Frameworkのインテグレーションが存在した 複数のAWSアカウントによる運用をしていたため、各アカウントの情報を一元管理してシームレスにサービス監視できる環境を作りたかった 既にログ監視でDatadogを使っており、社内にDatadogに関するノウハウがあった Datadog APMのセットアップ 本章では、Datadog APMのセットアップ手順を紹介します。 Datadogのマニュアル に従ってセットアップを行います。 AWSインテグレーションのインストール AWSインテグレーション を設定することで、DatadogがCloudWatchからLambdaメトリクスを取り込むことができるようになります。 インテグレーションのインストールを行う過程で、 Datadog Forwarder と呼ばれるLambdaが作成されます。このDatadog ForwarderがAWSの各種サービスの情報をDatadogに対して送信します。 なお、 こちらのページ からもDatadog Forwarderのインストールを行えますが、AWSインテグレーションのインストールで既に作成されている場合はスキップしてください。 Datadog Serverless Pluginの設定 FBZのAPIは Serverless Framework を使って開発しています。DatadogがServerless Framework向けのPluginである Datadog Serverless Plugin を提供しているので、それを利用します。このプラグインはメトリクス、トレース、ログをDatadogに送信するLambdaレイヤーを作成します。 Datadog Serverless Pluginのセットアップは、 こちら を参考にして実施します。 下記3つの手順によって、Datadog Serverless Pluginのインストールと設定が可能です。 # Datadog Serverless Pluginのインストール $ yarn add --dev serverless-plugin-datadog # serverless.yml # プラグインを追加 plugins : - serverless-plugin-datadog # serverless.yml # セクションを追加 custom : datadog : addExtension : true apiKey : # Your Datadog API Key goes here. その他、プラグインに関する詳細は DatadogのGitHubリポジトリ にまとめられています。 以下に示す設定例は、上記リポジトリに記載されているものです。ログレベルやタグの付与に関する設定が可能です。 # serverless.yml # パラメータの例 custom : datadog : flushMetricsToLogs : true apiKey : "{Datadog_API_Key}" apiKMSKey : "{Encrypted_Datadog_API_Key}" addLayers : true logLevel : "info" enableXrayTracing : false enableDDTracing : true forwarderArn : arn:aws:lambda:us-east-1:000000000000:function:datadog-forwarder enableTags : true injectLogContext : true exclude : - dd-excluded-function 以上の設定で、分散トレーシングができるようになります。 セットアップ時の注意点 実際にDatadog APMをセットアップしていく中で、いくつか注意すべき点を発見しました。本章では、その注意点を説明します。 リージョンの統一 AWSインテグレーションのインストール時には、CloudFormationを使ってスタックを作成します。その際、リージョンがデフォルトでは us-east-1 となっているため、必要に応じて監視対象のLambda関数と同じリージョンに変更する必要があります。この設定を間違えると、DatadogとAWSの連携ができなくなるので、再度セットアップをやり直すことになります。 ログとトレースの接続 アプリケーションから出力されるログとトレースした情報を接続するには、以下の2つの作業が必要となります。 アプリケーション側 ログにトレースIDを挿入 Datadog側 パイプラインの設定 トレースIDの挿入 Datadog Serverless Pluginのオプションで injectLogContext: true とすると、ログにトレースIDやスパンIDが自動で挿入されます。しかし、CloudWatchに出力しているログフォーマットを独自に設定している場合、それらの設定が上書かれて異なるログフォーマットとなってしまいます。 FBZのAPIではログの可読性向上を目的として、LambdaのログフォーマットをLTSV形式の独自フォーマットに変更しています。そのため、 こちら を参考にし、手動で分散トレースに必要なログを設定しました。 # FBZで利用しているLTSV形式のログフォーマット FORMAT = ( # 省略 ' \t datadog:[dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s]' # 接続のために追加 # 省略 ) パイプラインの設定 前述のように、ログの設定をしただけではトレースとの接続はできません。手動で設定したログを、Datadogの共通形式にするために、ログのデータ構造を変換する必要があります。 こちら を参考に設定しました。 AWSインテグレーションをインストールした際に自動作成されたパイプラインは編集できないので、クローンして新たなパイプラインを作成します。 作成したパイプラインの中から、 Grok Parser: Parsing Lambda logs という名前のプロセッサーを選択して編集します。 Log samplesの5つの項目のいずれかにログのサンプルをセットし、マッチするかを確認しながらパースのルールを書いていきます。この際にヘルパールールを用いることで目的ごとに名付けができます。 # LTSV形式をパースするルール例 # Sample Rule sample_rule %{datadog_trace} # Helper Rules datadog_trace datadog:(\[dd.trace_id=%{word:dd.trace_id} dd.span_id=%{word:dd.span_id}\]) この設定が完了すると、ログとトレースの接続が実現されます。 運用上でのポイント 次に、実際に運用する中で得られたポイントとなる点を紹介します。 タグの活用 Datadogでは env service version の3つのタグが予約済みタグとして利用されています。公式ドキュメントでは、タグを使うことで以下の3つが可能となると書かれています。 バージョンでフィルタリングされたトレースおよびコンテナメトリクスでデプロイへの影響を特定する 一貫性のあるタグを使用して、トレース、メトリクス、ログ間をシームレスに移動する Datadogアプリ内で統一された方法で環境またはバージョンに基づいてサービスデータを表示する 公式ドキュメント より引用 FBZでも、タグを使ってフィルタすることで検索性が向上したり、APIやBatchといった処理系ごとの処理時間の傾向などを確認できるようになりました。 タグの自動付与 Datadog Serverless Pluginをインストールすると、 serverless.yml に定義している値からLambdaに対して自動的にタグ付けをしてくれます。プラグインの機能を使うと各Lambdaで重複するタグを書かなくてもよくなるので、定義ファイルへの記述を減らすことができます。 # serverless.yml service : service-name provider : name : aws stage : prod # Lambdaのenvタグに「prod」が付与される plugins : - serverless-plugin-datadog functions : hello : handler : handler.hello # この関数は、プラグインによって上記で構成されたサービスレベルのタグを継承する # tags: #env: provider.stageの値が自動付与される #service: serviceの値が自動付与される world : # この関数は、タグを上書きする handler : handler.users tags : env : "<ENV>" service : "<SERVICE>" タグに関する詳細は 公式ドキュメント を御覧ください。 まとめ Datadog APMのServerless Frameworkへの導入方法から運用する際のポイントを紹介しました。 サーバーレスアーキテクチャで運用している場合、内部でいくつものサービスを経由して処理が進みます。そのため、サービス間の依存関係やパフォーマンス面で問題が発生した場合、調査が困難になることが多いです。その課題の解決のために、Datadog APMを導入することで既存のコードへの修正は最小限に抑えながら、分散トレーシングを実現できました。 分散トレーシングを実現できたことで、上図のようにタイムアウトが発生した際のボトルネックの箇所の特定が容易になり、各サービスのレイテンシを可視化できるようになりました。 さいごに ZOZOテクノロジーズでは、サーバーレスアーキテクチャやAWSのマネージドサービスを活用しサービスを成長させていきたい仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは。ZOZOアプリ部の遠藤と林です。 日本時間の6月8日から12日にかけて開催され今年の WWDC21 も、昨年と同様にオンライン開催でした。 FaceTimeの新機能であるSharePlayや、プライバシーが更に強化されたiCloud+、アプリ開発を一元管理できるようになったXcode 13など、新機能から開発環境周りまで幅広い発表が目白押しでした。 本記事ではオンライン開催が2年目になったWWDC21に対し、弊社のエンジニアが昨年の経験を活かしてどのように臨んだのか、また開催期間中の活動内容をお伝えします。今年3月に10年ぶりにリニューアルしたZOZOTOWNアプリとZOZOGLASSに対するデザインフィードバックも可能な範囲で紹介しますので、是非最後までご覧ください。 WWDCの概要 WWDC(Worldwide Developers Conference)は、Appleが年に1度開催している開発者向けのカンファレンスです。iOS、iPadOS、macOS、watchOS、tvOSのアップデートをはじめ、開発環境周りの新機能などが発表されます。また、例年通りの各種セッションやLabsに加え、今年は新たに「Digital Lounges」と「Challenges」が追加されました。それらの新しい体験を加えた形でWWDCを楽しむことができます。 昨年の経験を活かしたオンライン参加の工夫 オンライン開催のWWDCへの参加も2回目ということで、昨年の経験を活かしてより効果的な参加方法を模索しました。その結果、開催日までの事前準備や、開催期間中の情報共有も昨年よりもスムーズに行えました。ここでは、そこで期間中の働き方や、実施した事前準備や情報共有の工夫点を紹介します。 開催期間中の働き方 弊社では海外カンファレンスへの参加が推奨・サポートされており、オンライン開催でも業務の一環として参加できます。 1-on-1 Developer Labs (以下、ラボ)へ参加することもあり、今年も昨年同様、希望したメンバーは業務調整を事前に行った上で現地時間に合わせた勤務時間として参加しました。昨年の内容は以下の記事をご覧ください。 techblog.zozo.com 現地時間に合わせて参加するメンバーは、以下の点を考慮してスケジュールを組みました。 現地時間に合わせた勤務時間の調整 日本時間2:00〜11:00を勤務時間とする 休日出勤を利用した勤務日調整 最終日が日本では土曜日なので、該当の6月12日を休日出勤として、振替休日を取得する 事前準備と情報共有にMiroを活用 昨年もラボやセッションへ参加するための準備・共有は実施していました。質問したい内容をスプレッドシートで管理し、各自が参加するセッションをSlackで共有していました。しかし、昨年のやり方では情報が分散してしまうことと、情報が流れてしまうことが課題として挙がりました。そこで、今年は Miro を使用した情報の一元管理を実施しました。 Miroのボードには、ラボでの質問だけではなく、後日社内に向けて共有すべき情報を書き込めるようにしました。 WWDC21の全期間を終え、Miroにはたくさんの情報が集約されました。以下の画像は開催前と開催後のMiroのボードを比較したものです。開催後の一番大きい枠は、ラボに関する内容で、11個の質問がまとめてあります。 WWDC21開催前 WWDC21開催後 事前準備やセッション、ラボで聞いた内容はMiroのマインドマップのテンプレートを使用して整理しました。要素を簡単に増やせ、その内容を繋げることで、あとから見たときに関連性を把握しやすくまとめることができました。 どのようにまとめたのか、その一部を紹介します。以下はラボで質問した内容をまとめた一例です。 予想ビンゴ! 毎年WWDCの開催前には、どのような発表がされるのかを予想し、SNS上に多数投稿されます。今年は、社内でそれを実施しました。何が発表されるのかをビンゴ形式で予想するようにし、楽しむ要素をプラスして実施しました。 ビンゴの結果を発表します。 下図の赤色の内容が正解したものです。残念ながらビンゴは成立しませんでした。しかし、「Scribbleの日本語対応」などいくつか予想が当たっている項目もありました。「iPadでXcodeが動く」についてズバリ当たりはしませんでしたが、「Swift PlaygroundsからApp Storeへアプリを公開できるようになった」ということで当たりと判定しました。 「ビンゴに書いた内容が当たるかな」とKeynoteで内容が発表される度にドキドキして楽しみながら参加できました。来年はビンゴが成立できるよう、リベンジしたいと思います。 WWDCの新しい楽しみ方 今年のWWDCには新しく2つの要素が追加されました。追加されたこれらの要素に参加したので、その内容を紹介します。 Digital Lounges まず1つ目は「Digital Lounges」です。こちらは、Slack上にデベロッパーツール、SwiftUI、アクセシビリティ、機械学習についてのチャンネルが用意され、そこで質問ができる仕組みでした。質問できる内容は限られていますが、ラボに行かなくてもSlack上でAppleのエンジニアとデザイナーに気軽にリアルタイムで質問できます。 そして、各チャンネルでは質問だけではなく、「Trivia Night」というクイズ大会などのイベントも開催されていました。Trivia Nightを通してAppleの歴史を知ることができ、楽しむことができるコンテンツでした。 Challenges 2つ目は「Challenges」です。Challengesというタイトルから推測できる通り、問題にチャレンジして達成できたら、その内容をApple Developer ForumsやDigital Loungesに共有して楽しむことができます。 出題された問題に、「Throwback with SwiftUI」というものがありました。これは、1984-2013年の範囲からランダムに指定された年に対し、「その年のUIっぽいもの」をSwiftUIで作るという問題です。 チャレンジしたメンバーは1984年を引いて、その年の有名なCMを再現していました。 出題される問題には関連するセッションのリンクも付いています。セッションを見るだけではなく、実際に手を動かして問題を解く体験もセットででき、とても楽しく参加できました。出題される問題はどれも面白く、毎日追加されるので、問題の追加をワクワクしながら過ごすこともできました。 Labs & Sessions この章では、WWDC21へ参加した社員が、それぞれ参加したラボやセッションの内容を紹介します。 Design Lab × ZOZOTOWN 最初の報告はZOZOアプリ部の林がお送りします。 WWDC21では、姿を消していたDesign Labが1年ぶりに復活しました。AppleのデザイナーにリニューアルしたZOZOTOWNアプリのフィードバックを頂いたので、その一部を紹介します。 ラージタイトルに関するフィードバック ZOZOTOWNアプリのリニューアル時にラージタイトルを導入しました。しかし、「お気に入り」ページのような上部にタブがある場合、標準のNavigationではラージタイトルを対応できないため独自で実装しました。 Human Interface Guidelines にも、このパターンに関する記載がないので、Appleのデザイナーに質問してみました。そして、「タブを切り替える際、ユーザーの気が散らないようにナビゲーションバーの状態を維持すべきだ」というフィードバックを頂きました。今後、UI改善をする際の参考にしたいと思います。 ダークモードに関するフィードバック ダークモードが普及していく中で、リニューアル後の白を基調としたZOZOTOWNアプリはダークモード対応の必要性が高くなってきます。その将来を見据えて、ダークモード対応の注意点を確認してきました。商品画像についてはダークモード用の画像を用意しなくてもいいということや、WebViewともバランスよくダークモードに合わせるべきなど、ダークモード対応について貴重なアドバイスを頂きました。 全体へのフィードバック 全体へのフィードバックとして、「ZOZOTOWNアプリは洗練されていて一貫性があるクリーンなECアプリであり、個人的にも気に入った」という嬉しいコメントを頂きました。今回の大規模なリニューアルに携わったメンバーとして、このようなコメントを頂くことができ、とてもやりがいを感じました。 Design Lab × ZOZOGLASS こんにちは、ZOZOアプリ部の松井です。ZOZOTOWNでは、自宅にいながら簡単にフェイスカラー計測ができる 「ZOZOGLASS(ゾゾグラス)」 を3月18日にリリースしました。このZOZOGLASSを使った一連の計測フローを、より使いやすいUIにするために、Appleのデザイナーに意見を聞いてみました。 計測フローをお見せしながら説明したところ、「一連の流れに筋が通っており、とても使いやすい」と言って頂けました。「手順やコンテンツについてきちんと説明がされているし、改善点がないくらい。私も使ってみたい!」という嬉しい感想を頂けました。「強いて改善点を挙げるならば、UXの流れの中で表示されるコンテンツが、何のために表示されているのか説明があれば分かりやすい」というフィードバックを頂けました。 全体を通し、コンテンツを見たユーザー自身が何をすれば良いか理解できること、コンテンツの表示理由が明確であることを意識しているように感じました。 Design Labに過去10回以上参加している同僚によると、ほとんどの場合はフィードバックが止まらなく、時間が足りなくなるようです。ところが、それに該当せず、非常に好評だったZOZOGLASSのUI/UX。まだお使いでない方は、是非Appleのデザイナーも認めるUI/UXを堪能してみてください。 Object CaptureはECにおけるARの利活用を加速させるか? ARやVRといったXR領域に注力している @ikkou です。ここ数年のWWDCではAugmented Reality(AR)関連の発表も続いているため、特にAR関連セッションを注視しています。 AR関連で特に注目すべきは 2D写真から3Dモデルを生成 する「Object Capture」です。これはMacでフォトグラメトリを実現するものです。既にLiDARが搭載されたiPad/iPhoneを使ったものもありますが、こちらは スキャンから3Dモデルを生成 する、似て非なるものです。 私は会期中にApple M1チップが搭載された私物のMacを購入し、 #WWDC21Challenges のお題でもある Object Captureを試しました 。その結果、ラフに撮影したにも関わらず、思いのほか綺麗なUSDZファイルが生成されて驚きました。 これまで、フォトグラメトリには「 RealityCapture 」を始めとする「有償」アプリケーションの利用が一般的でした。しかし、今回発表されたObject Captureは 動作するMacの機種に制限がある ものの「無償」です。 RealityCaptureは建造物のような広い範囲を対象とする「広域フォトグラメトリ」にも対応しています。対してObject Captureは広域フォトグラメトリ向きではなく、必ずしも比較すべき対象ではありません。それでも特定の用途に限っては、ECにおけるARの利活用の課題として挙げられる「3Dモデルの生成」を容易にします。 ARKitによってARが身近なものになり、Object CaptureによってARで映し出す3Dモデルの生成が容易になりました。今後「ARで商品を隅々まで眺めてから購入するという買い物体験」が今まで以上に加速することは想像に難くありません。非常に楽しみです。 自前実装で悩んだ日々にさようなら、UISheetPresentationControllerで頑張らないハーフモーダル こんにちは、 でらけん です。WEAR部のiOSチームで日々頑張っています。WEARアプリでは、類似画像の検索画面など、既にいくつかの画面でハーフモーダルを取り入れてきました。そのため、今回のWWDC21で、私は「 Customize and resize sheets in UIKit 」に釘付けでした。 実際に サンプルコード で動作を試してみたので、そこから得られた知見を紹介します。 リリースアプリで実装しているハーフモーダルは、モーダルに見立てたViewを1つ用意し、次の機能を組み合わせて動作させています。 UIPanGestureRecognizer パンジェスチャーによる移動量の監視 UIView.transform 移動量をViewの拡大・縮小へ反映 UIView.animate ジェスチャーを止めた際のViewの拡大・縮小のアニメーション UIGestureRecognizerDelegate モーダル内のCollectionViewのスクロールとジェスチャーを同時に認識させる スクロールを用いたモーダルの拡大・縮小 iOS 15からは、 UISheetPresentationController を使用することで、1.〜3. を自前で実装することなく、ハーフモーダルを実現することが可能になりました。4. は、スクロールのタイミングで animateChanges(_:) を使って拡大させることが可能です。こちらを使用することで、スクロールに限らずボタンをタップしたら拡大させるなど、様々なアクションと紐づけることができそうです。 まだ、一部のみを試した段階ではありますが、スワイプの制御を意識する必要がなくなるだけでも非常に負担が軽減されたと感じます。 dyld3時代でも「Static Frameworkをマージする」手法は有効性を持つか ZOZOアプリ部のげんです。ラボ(C, C++, Obj-C, compiler, analyzer, debugger, and linker lab)に参加し、「Dynamic Frameworkをstaticにビルドしてマージする手法」がdyld3時代となった現在でも有効性をもつのか、をAppleのエンジニアに確認しました。 結論は「少し効果あり」でした。 同時に「Xcodeからアプリをロードする場合はdyld2が使われる」ということと、Umbrella Frameworkに対して私はAppleのエンジニアと異なる認識を持っていることも分かりました。AppleがUmbrella Frameworkで指し示すものは「Frameworkの中にFrameworkが入っているもの」であり、「Static Frameworkをマージしたもの」ではないとのことでした。 そして、「Framework周りはストア審査の際に見られるので注意した方が良い」というアドバイスも頂きました。リジェクトされる可能性を考えると、確かに事前に注意した方が良さそうです。 テストでキーボード入力をいい感じにしたい テックリードの @banjun です。InternationalizationとClockKitとCamera Captureのラボに行ってきました。そのうちのひとつをご紹介します。 ZOZOTOWNのテストケースでは KIF を使ってタップやキーボード入力をシミュレートしていました。そのときのキーボードは目的の言語のものである必要があります。今まではテストコードということもありプライベートなAPIを活用していたのですが、最近は失敗することもあったため、ラボで聞いて解決を図ることにしました。 ラボでは、より安定しそうな公開APIの手法をいくつか提案して頂きました。例えば .asciiCapable をセットする、 insertText: で入れる、ペーストしてしまう、などです。それぞれの不利な点もあるそうですが、必要なシミュレートの粒度に応じて適切なものを選択できます。これで UIなんとかImpl クラスを直接触らなくて済む日が来るかもしれません。 まとめ 以上、WWDC21の参加レポートでした。 カンファレンスのオンライン開催が当たり前の時代になっていることを実感できたWWDCでしたね。「Digital Lounges」や「Challenges」など新しい仕組みを導入して、Appleはオンライン開催でも開発者がより情報をキャッチアップできるような試みを実施していました。弊社も昨年のオンライン参加の経験を活かして、Miroなどの活用で情報共有がよりスムーズにできました。 WWDC21の最後に、Appleから「2年連続でオンライン開催を実施してみたがどうだったか」「オフラインイベントに参加したいのか」のアンケートがありました。みなさんはいかがですか? さいごに ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
こんにちは、WEAR部 運用改善チームの三浦です。普段は WEAR の運用改善を行っていますが、最近は新規プロジェクトの開発にも携わっています。 本記事では、WEARのS3への画像アップロード機能をインフラ・バックエンド両面からリプレイスを行い、パフォーマンスの向上と安全かつ効率的に運用保守を行えるよう改善をした事例を紹介します。 背景 現在取り組んでいる新規プロジェクトで、WEARの外部連携用APIを通してWEARへコーデ投稿をできる機能を作ることになりました。WEARのコーデ画像はAmazon S3で管理しており、今回作成するコーデ投稿機能でもWEARのバケットに対して画像をアップロードする必要があります。しかし、現状の画像アップロードの仕組みには様々な課題がありました。 その仕組みと課題の概要を説明します。 現状の画像アップロード機能の仕組み WEARの現状の画像アップロードの仕組みは以下の通りです。 WEARのアプリからAPIを呼び出し、WEARサーバーに画像アップロード APIからS3バケットへ画像をアップロード 画像がバケットへアップロードされたのをトリガーにAWS Lambdaが起動 Lambdaがアップロードされた画像のリサイズを行い、サイズ毎にバケットへ保存 画像の参照は、「現在使用しているCDN」から「Amazon CloudFront」を経由して行います。 課題 現状の画像アップロードの仕組みではインフラ、バックエンドそれぞれ下記のような課題を抱えていました。 インフラ インフラリソースの設定がコード管理されていない 開発環境用のS3バケットがないため、本番のバケットでテストをしなければならない Lambdaの画像のリサイズ処理が遅い 複数種類にリサイズして保存しているため、S3の利用料がかさむ バックエンド APIを経由してS3バケットへ画像をアップロードするので時間がかかる APIのリプレイスはRailsで行うが、このままリプレイスするとPumaのスレッドを長時間占有したり、メモリの使用量が増えるなどパフォーマンスに影響を及ぼす Railsでリプレイスを行う背景は 過去記事 で説明しているので、併せてご覧ください。 techblog.zozo.com これらの課題をクリアし、画像アップロード機能のパフォーマンスの向上と運用効率を改善するため、インフラ、バックエンド両面からリプレイスを行うことにしました。 実現したいこと 最終的なゴールは、画像アップロードのリプレイスをすることにより、前述の保守・開発・パフォーマンスの課題を解決することです。このリプレイスは4つのフェーズに分けて行うことを計画しています。 本記事ではフェーズ1とフェーズ2で予定している構想を紹介し、今回対応したフェーズ1で工夫した点や得られた効果を紹介します。 フェーズ1 フェーズ1ではCDNを切り替えます。なお、その理由は後述します。 具体的な手順は以下の通りです。 既存とは別に新しいS3バケットを用意する 画像のアップロードは最新のアプリでは新バケットに向けて行う ユーザーがWEARのアプリをアップデートするまでは新旧バケット両方への画像アップロードが発生することになる 最新のアプリの画像のアップロードはPresigned URLを発行してクライアントから直接新バケットへ画像をアップロードする 旧環境で使用しているCDNを現在使用しているCDNからAkamaiへ切り替える アップロードした画像のURLはDBに保存しているため、画像参照時はそのURLを基に新旧どちらのバケットに保存されているかを判断し画像をダウンロードします。 このフェーズではCDNの切り替えを行いますが、DNSのレコードを修正することで既存の画像URLは変更せず今まで通りアクセスできるようにしています。 フェーズ2 フェーズ1の手順を終え、すべてのWEARユーザーのアプリのバージョンが最新になると、旧環境のバケットには新規のファイル追加がなくなります。それを踏まえ、フェーズ2では下記の手順を実施します。 旧バケットにある画像をすべて新バケットに移行する DBに保存している旧バケットの画像のURLを新バケットのURLに変更する 新環境にAkamaiを導入する なお、後続するフェーズ3〜4では画像リサイズ機能のリプレイスや、不要になった旧環境のリソースを削除する予定です。 次章では、フェーズ1で実際に行ったこと内容をインフラ・バックエンドの両面から紹介します。 フェーズ1の実施内容と効果 本章では、フェーズ1で実際に行った内容とそのポイント、そこで得られた効果を紹介します。 インフラのリプレイス 既存とは別のS3バケットを用意する 既存の仕組みでは、インフラリソースの設定がコード管理されていない課題がありました。そのため、既存のS3バケット上で修正を加えていくのはリスクがあると判断しました。 そこで、新しくAWS環境を用意し、S3バケットを新規で用意しました。 新バケットでは、AWS CloudFormationを用いることでインフラをテンプレート(YAMLファイル)管理できるようにしました。また、CloudFrontの Origin Access Identity を使用し、S3バケットへのアクセスをCloudFront経由に限定しました。これにより、外部から新バケットに対してダイレクトURLでアクセスできないように制限しています。さらに、新バケットでは開発環境と本番環境を分離することで、開発環境でのテストも本番へ影響を与えることなく安全にできます。 旧環境のCDNをAkamaiへ切り替える 旧環境で使用していたCDNをAkamaiに変更します。その理由は、Akamaiの方が全体の配信単価が安くなる点と、 Image Manager を活用することでLambdaを使用しなくても画像のリサイズが可能な点です。フェーズ1ではImage Managerの導入は行いませんが、リサイズ機能のリプレイスを行うフェーズ3で導入予定です。 バックエンドのリプレイス 画像をアップロードするためのURLは、S3のPresigned URL機能を利用して発行します。画像のアップロードはAPI経由ではなく、このURLを使うことでクライアントから直接アップロードできるようにします。 ここで使用するPresigned URLは、署名をクエリ文字として含むURLを発行する機能です。この機能を利用することで、認証情報を保持する必要がなく、期間限定で指定したS3バケットに対して操作が可能です。 WEARでも、クライアントで認証情報を保持することを避けたいので、このPresigned URLを使用した画像アップロード機能を実装しています。 Presigned URLの詳しい内容は 公式サイト をご覧ください。 リプレイス前後の画像アップロードのフロー比較 Presigned URLの導入により、画像アップロードのフローがどのように変化するのかを説明します。 これまでの方法では、クライアントのアプリから画像データを一度APIへ渡していました。その後、APIは受け取った画像データをS3バケットへアップロードします。 一方、Presigned URLを導入すると下記のフローに変わります。クライアントはS3へアクセスを行うために、APIに対してPresigned URLの発行を要求します。そして、APIはS3へ署名情報を渡し、Presigned URLを発行してクライアントへ返却します。その結果、クライアントは受け取ったPresgiend URLに対して画像データを渡すことで、APIを介さずにS3バケットへ直接画像をアップロードすることが可能です。 Presigned URLの発行方法 ここでは、Presigned URLの発行方法に触れておきます。 前述の通り、Presigned URLはAPIで発行します。今回はRubyを利用しているので、AWS公式のGemである AWS SDK for Ruby(Version 3) で実装しています。なお、他の開発言語のSDKは 公式サイト に記載されています。 次に、実装時のポイントを紹介します。 認証情報は、Amazon ECSコンテナにアタッチされたIAMロールを使用する Presigned URLを発行する時点で、アップロード先のバケット名とオブジェクトキーを指定する URLの有効期限は expired_in で秒単位で設定可能だが、セキュリティを考慮し、デフォルトより短い時間に設定し、画像アップロードの度にPresigned URLを発行する デフォルトは900秒(15分)、最大1週間まで期限を設定できる なお、オプションの詳細は 公式リファレンス にも記載されています。 上記の内容を踏まえたソースコードのサンプルです。 client = Aws::S3::Client.new(profile: 'default') resource = Aws::S3::Resource.new(client: client) resource.bucket(#{bucket_name}).object(#{object_key}).presigned_url(:put, expires_in: 60) # => https://xxx.amazonaws.com/yyy.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=aaa&X-Amz-Date=bbb&X-Amz-Expires=60&X-Amz-SignedHeaders=ccc&X-Amz-Security-Token=ddd&X-Amz-Signature=eee クライアントは、API側で発行されたこのPresigned URLに対して画像を送る事で、S3バケットへ画像をアップロード可能です。 導入効果 フェーズ1のリプレイスを実施し、以下の課題を解決できました。 インフラリソースの設定をCloudFormationでコード管理することにより、変更履歴の確認や複製が容易になった 開発環境用と本番環境のバケットを分離することにより、開発が安全に行えるようになった Presigned URLを使った画像アップロードを行うことにより、サーバーサイドへの負荷を削減とパフォーマンスの向上を実現できた まとめ 本記事では、WEARの画像アップロード機能のリプレイスのフェーズ1において、インフラの構成変更やPresigned URLを使用した画像アップロードの仕組みを紹介しました。今後は、フェーズ2では旧バケットから新バケットへの画像データの移行、フェーズ3以降では画像のリサイズのリプレイスを引き続き行っていく予定です。 さいごに 運用改善チームではWEARの過去の負債から目を背けず、正攻法で解決案を議論し、運用改善に取り組んでいます。そのため、コミュニケーションと技術力を活かしながら一緒に会社を盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは。EC基盤本部 SRE部の渡邉です。去年の今頃はリモートワークによる運動不足を解消するために毎朝ロードバイクで走っていたのですが、3か月目に突入したころ急に飽きてしまいました。継続することの大切さを痛感しています。 さて、以前公開した記事でも Splunkを導入した話 について書きました。今回はSplunkをもっと活用していくために、効率的なサーチ方法やダッシュボード作成のTIPSを紹介します。 メトリクスのダッシュボード作成TIPS あらかじめ、よく使うサーチやメトリクスのダッシュボードを作成しておくと、都度SPL(サーチ処理言語)を書く手間が省けます。しかし、1枚のダッシュボードに漠然とパネルを作ってしまうと、動作が重くなったり視認性が悪くなってしまいがちです。今回はメトリクスのダッシュボードを作る際に行ったちょっとした工夫を紹介します。 Timechartに別要素を追加する Splunkでは、メトリクスを収集するために mstats を使います。 以下の例では平均値と95Percentileでデータを抽出しています。 | mstats avg (_value) p95(_value) prestats= true WHERE metric_name= " Processor.%_Processor_Time " AND " index " = " em_metrics " AND (host=WEB*) AND `sai_metrics_indexes` span=1m | timechart avg (_value) AS " 平均値 " p95(_value) AS p95 span=1m BY host | rename " 平均値: WEB " AS " CPU(平均値) " | rename " p95: WEB " AS " CPU(p95) " | fields - _span* このTimechartに appendcols を使うことで、他の要素を埋め込むことができます。 | appendcols[ search index=main host=WEB* | timechart span=1m count ] | mstats avg (_value) p95(_value) prestats= true WHERE metric_name= " Processor.%_Processor_Time " AND " index " = " em_metrics " AND (host=WEB*) AND `sai_metrics_indexes` span=1m | timechart avg (_value) AS " 平均値 " p95(_value) AS p95 span=1m BY host | rename " 平均値: WEB " AS " CPU(平均値) " | rename " p95: WEB " AS " CPU(p95) " | fields - _span* | appendcols[ search index =main host=WEB* | timechart span=1m sum ( count ) AS アクセス数] しかし、SPLを記述しただけではグラフには描画されません。そのため、 Format Visualization から Chart Overlay のタブを選択し、追加したいデータをセットしていきます。 この設定を入れることで、アクセス数のデータも一緒に表示可能です。下図のように、アクセス増加に応じてCPU USAGEが上昇している状況を把握できます。他にもエラーカウントを組み合わせても良いでしょう。 個人的な見解ですが、2つ以上の appendcols はグラフ描画に時間がかかってしまう恐れがあるため、1つまでにしておくのが無難です。 CPU USAGEをインスタンス別で把握する インスタンスを数百台以上の規模で管理していると、特定のインスタンスだけが高負荷状態に陥っている場合があります。そういった状況を把握するために、CPU USAGEを5段階でレベル分けして表示するダッシュボードを作りました。紹介する例では、95Percentileを取得した結果を表示させています。パネルごとに色の設定が可能なため、現状でどの程度の負荷状況に分布されているのか一目で把握できます。 | mstats p95(_value) prestats= true WHERE metric_name= " Processor.%_Processor_Time " AND " index " = " em_metrics " AND " host " = " WEB* " AND `sai_metrics_indexes` span=1m BY host | bin _time span=1m |stats p95(_value) AS avg_cpu BY host | eval Description= case (avg_cpu<= 30 , " Clear " ,avg_cpu<= 50 , " Attention " ,avg_cpu<= 70 , " Warning " ,avg_cpu<= 85 , " Problem " ,avg_cpu<= 100 , " Critical " ) | stats count BY Description | eval range= case (Description= " Clear " , " low " , Description= " Attention " , " guarded " , Description= " Warning " , " elevated " , Description= " Problem " , " high " , Description= " Critical " , " severe " ) こちらは Visualization を Single Value に設定し、 Trellis Layout を有効化します。分割の単位はSPLで定義したDescriptionを使います。 以上で、それぞれのCPU USAGEに対するカウント数を表示するダッシュボードができました。パネルの構成は以下の通りにしました。 Clear(緑):0〜30% Attention(青):30〜50% Warning(黄):50〜70% Problem(橙):70〜85% Critical(赤):85〜100% Criticalまで負荷が上昇しているインスタンスが9台存在しているようです。ここではどのインスタンスなのかを特定できるサーチもあると便利だと感じたため、次の連携部分で説明していきます。 ダッシュボードを連携させる では、次にダッシュボードを連携させていきます。 今回は CPU Level のパネルをクリックすると詳細のダッシュボードへと遷移し、該当するインスタンスをリスト表示させます。そのために、連携させたいダッシュボードに対してパラメータを設定していきます。調査する 時間範囲 と 選択したCPU Level をトリガーとするため、パラメータは以下の3つを使用します。これを Drilldown Editor で設定します。 項目 パラメータ名 トークン名 開始時刻 form.time_token.earliest $earliest$ 終了時刻 form.time_token.latest $latest$ CPU Level form.cpulevel $trellis.value$ このトリガーを受け、遷移先のダッシュボードではトリガーを受け取るパラメータを設定が必要です。 Add Input から Text と Time を設置します。 続いて、先程設置したTextフォームを以下のように編集します。 Token には選択した CPU Level を渡すため、このように入力します。 そして、サーチ文を以下のように用意します。 | mstats p95(_value) prestats= true WHERE metric_name= " Processor.%_Processor_Time " AND " index " = " em_metrics " AND " host " = " WEB* " AND `sai_metrics_indexes` span=1m BY host | bin _time span=1m | stats p95(_value) AS avg_cpu BY host | eval Description= case (avg_cpu<= 30 , " Clear " ,avg_cpu<= 50 , " Attention " ,avg_cpu<= 70 , " Warning " ,avg_cpu<= 85 , " Problem " ,avg_cpu<= 100 , " Critical " ) | eval range= case (Description= " Clear " , " low " , Description= " Attention " , " guarded " , Description= " Warning " , " elevated " , Description= " Problem " , " high " , Description= " Critical " , " severe " ) | WHERE Description= " $cpulevel$ " | rename avg_cpu AS " CPUUSAGE " | fields host CPUUSAGE なお、先程設定した Token は、サーチ文では以下のように指定します。 | WHERE Description="$cpulevel$" そして、下図が完成したダッシュボードです。ダッシュボード内から、別のダッシュボードに遷移させることで、さらに詳細を調査するといった使い方が可能です。 サーチに要する時間を短縮させる工夫 Splunkは、取り込んだデータを元に目的のフィールドを抽出するなど、作り込み可能な点が最大の特徴です。しかし、膨大な量のログからデータを抽出する際には、かなりの時間を要する可能性もあります。例えば、過去のレスポンスタイムをTimechartで表示したい場合に、対象の期間が長ければ長いほどサーチにかかる時間も比例して増加します。 Search Modeによる実行時間の差 基本的には Verbose Mode でサーチを実行するケースが多いでしょう。しかし、 Fast Mode に変更するだけで実行時間の改善を見込める場合があります。実際、実行時間にどのくらいの影響を与えるのか確認していきます。 実験に使用したサーチ文は以下の通りです。これを使い、過去24時間分のデータを取得してみます。 index =main sourcetype= " ms:iis:auto " host=WEB* | eval response_time_msec=(response_time/ 1000 ) | timechart avg (response_time_msec) AS " レスポンス(平均値) " perc95(response_time_msec) AS " レスポンス(p95) " span=1m まず、 Verbose Mode でサーチを実行してみます。開始から7:50経過した段階では、24%しか進行していません。 同様に Fast Mode でサーチを実行してみます。すると、半分程度の3:27で完了しました。 このように、取得したいフィールドデータが限定されている場合には Fast Mode を使うことで時間を短縮できます。しかし、複数のパネルが設置されているダッシュボードの場合には、結果が返ってくるまでに時間を要することもありえます。 また、複数人で重いダッシュボードを開いてしまうとサーチヘッドやインデクサーといったSplunkのコンポーネントが悲鳴をあげるでしょう。 Summary Indexを使ってみる 前述の課題を解決するために、 Summary Index を利用することをお勧めします。ライセンスやコストには影響がないため、使用することによるデメリットは基本的にありません。 Summary Index の設定をしていきます。 Index name は分かりやすいものにしましょう。そして、 Max raw data size は0GBにします。次に設定する Searchble time と Archive Retention Period は今回の例ではいずれも3年としています。 入力が完了し、保存をすると数分でIndexが作成されます。 次に、新しく作成したIndexにサーチ結果を蓄積させていきます。 まず、実行させるサーチ文を書きます。レスポンスタイムを取得するサーチ文の最後に | collect index=response_data" を追加してください。このサーチ対象時間は今回の例では過去60分としています。 index =main sourcetype= " ms:iis:auto " host=WEB* | eval response_time_msec=(response_time/ 1000 ) | timechart avg (response_time_msec) AS " レスポンス(平均値) " perc95(response_time_msec) AS " レスポンス(p95) " span=1m | collect index =response_data 作成したサーチ文の実行後、 Save As で Report に保存します。ここでも名前は分かりやすいものにします。 作成したレポートを定期実行させることで、データの蓄積が自動で行われます。定期実行をするために、 Edit Schedule から実行間隔を登録します。 サーチ時間を過去60分としているので、毎時0分になったら実行するように設定します。サーチにかかる時間が数秒から10秒程度であれば、毎分稼働させることも可能です。 スケジュール登録が完了したら、実際にどのようにデータが保存されているか確認してみましょう。 index =response_data Index のみを指定してサーチを実行することで、データの格納形式を確認できます。 項目名がそのままフィールド名として格納されているので、これを利用してTimechartにしてみます。 ここでは、過去24時間でTimechartを描画します。 index =response_data | timechart span=1m avg (レスポンス(p95)) AS レスポンス(p95) avg (レスポンス(平均値)) AS レスポンス(平均値) これを実行すると、過去24時間のサーチが1秒で完了しました。 このように、取得する項目があらかじめ決まっていて、値だけが欲しい場合には Summary Index を駆使したダッシュボードを活用することで、どれだけパネルが増えても、ストレスなく表示させることができます。現在、ZOZOTOWNのサービス監視で利用しているダッシュボードのパネルの大半で Summary Index を利用しています。 さいごに Splunkを導入して1年半が経過し、様々なチームでSplunkの活用が進んでいます。サーチ結果で異常検知したらSlackにアラートを通知をするなど、運用改善にも貢献できています。 また、 Splunk Synthethic Monitoring や Splunk Real User Monitoring などの新製品がリリースされているので、機会があれば試してみたいと思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは。 ZOZO研究所 の後藤です。普段はZOZOTOWNの推薦システムの開発や社内で利用するための機械学習システムの開発に携わっています。 本記事では、近年目覚ましい進展を見せている画像ベースの仮想試着の研究を紹介し、実用化を考える際に解決すべき課題とアプローチの考察も併せて紹介します。 目次 目次 はじめに 画像ベースの仮想試着の課題 モデルアーキテクチャの課題 性能の課題 データセットの課題 課題へのアプローチ VITON M2E-TON FiNet O-VITON まとめ モデルアーキテクチャの課題 性能の課題 データセットの課題 最後に 参考 はじめに コロナ禍の状況も相まってECでの買い物需要が高まっています。普段使いしている消耗品であれば気軽に購入できますが、ZOZOTOWNで扱うようなファッション商材に関しては、実際に店舗で試着をして着用イメージを確認してから購入する方も多いでしょう。 しかし、気になった商品の着用イメージを仮想的にでも確認できれば、実際に店舗で試着をしてから購入している方々に対し、オンラインでの購入の後押しになるのかもしれません。 (引用:VITON-HD) 例えば、上の画像はVITON-HD 1 という仮想試着システムの出力例です。1番左の参照画像(白いTシャツの人物の画像)に対して、右側4枚の画像は、各画像の左下に掲載している商品画像を実際に着ているかのように当てはめています。元の画像では二の腕部分や胸元までTシャツで隠れていますが、右側の画像ではシステムによってその部分が補完されています。 オンライン上の衣服画像と自分自身の写真をこのように合成できたら、具体的な着用イメージが湧き、購入の意思決定の後押しになるでしょう。 このような着せ替え技術を売りにしているAIスタートアップも続々と登場しており、最近ではウォルマートがZeekitを買収したことがニュースになりました 2 。 歴史的には3Dの身体モデルに服を着せる仮想試着の方が先行しています。しかし、本記事では比較的低コストで手に入れられる画像データを使った仮想試着の技術に注目します。そして、実用化を検討する際に直面する課題を列挙し、近年登場したアプローチとの対応関係をまとめます。この記事を通して、仮想試着の技術に興味を持っていただければ幸いです。 画像ベースの仮想試着の課題 本章では、GANを使った先駆的な研究を紹介し、実用化にむけて解決しておくべき課題を挙げます。 任意の商品画像を仮想的に着用する技術の先駆的な例はJetchevらによるConditional Analogy GAN(CAGAN) 3 です。彼らはヨーロッパにECサイトを展開しているZalandoの研究部門、Zalando Researchの研究者で、ファッションECの課題を研究しています。 (引用:Conditional Analogy GAN) この研究では、上図の左側に掲載しているようなEncoder-Decoder型のGeneratorを、GANのアーキテクチャで学習させます。 Generatorへの入力として、3種類の画像を使います。商品 (赤色のパーカー)を着用した人物画像 と商品画像 のペア、着せたい商品 (青色のTシャツ)に関する商品画像 の3種類です。これらの画像を受け取って、Generatorは顔やポーズなどの人物の個性を保存したまま商品 に着せ替えた画像 を生成します。数式だと、 と表現できます。 Discriminatorは人物画像と商品画像のペアを受け取り、正しいペア をReal、 や のように作られたペアをFakeとして学習させます。 これらを正しく見分けられるように学習しながら、GeneratorがよりRealに近いデータを生成できるようフィードバックを与えます。 (引用:Conditional Analogy GAN) Generatorの学習には、GAN Lossによるリアルな画像の生成だけでなく、ピクセルレベルで正確に着せ替えを実行させる方針が必要です。 ここで問題になるのが、教師データです。 と同じ人物・ポーズの商品 の着用画像を大量に用意することは難しいため、Generatorの出力に対してピクセルレベルの教師データを与えることはできません。その点を著者らは、Cycle GANのアイデアを取り入れ、上手く解決しています。商品 に着せ替えた画像を再度Generatorに入力し、商品 に着せ替え直した画像 を生成させます。 と をピクセルレベルで比較しGeneratorに誤差を返しています。 この研究の実験結果の一部を示します。 (引用:Conditional Analogy GAN) 上図では、ある人物画像に様々なタイプの商品を着せています。商品の色や形など大まかな特徴は転写できており、大まかな雰囲気を知りたい程度であれば、これで十分かもしれません。しかし、より細かく見ていくと、商品のテクスチャやロゴが崩れている例や、ネック部分が保持されていない例があります。 (引用:Conditional Analogy GAN) 上図では、同じ商品を様々な人物に着せています。人物のサイズやポーズの違いにはきちんと対応できているようです。しかし、顔が崩れたりボトムスやバッグの色が変わったりと、注目している部分と関係のない部分が保持できないという問題があります。 この研究が登場した2017年当時、このような条件付き生成モデルによる服の着せ替えは衝撃的で、我々も再現実装を行いました。しかし、現実的なシチュエーションに対応するためには、いくつかのギャップを乗り越えなければならいことがわかりました。そこで得られた課題を紹介します。 モデルアーキテクチャの課題 CAGANでは、学習時と推論時のどちらにも人物画像に対応する商品画像が必要になります。人物画像と商品画像のペアのデータの収集はアノテーションのコストがかかります。そして、自分自身の写真に任意の商品を着せたい場合、自身が着ている衣服の商品画像を別途用意する手間がかかります。 また、この研究の実験はトップスカテゴリに限定されています。実際にはボトムスやアウター、シューズの着せ替えも試してみたいでしょう。着せ替え領域の指定はモデル内で暗黙的に行われるため、モデルは商品カテゴリ毎に用意したほうが良いでしょう。複数の商品を同時に着用したい場合は、商品の数だけ推論する必要があります。 性能の課題 出力結果を観察すると、顔やポーズが崩れているという個性の保持の問題と、縞模様が崩れていたりロゴやフォントが潰れるといった転写の性能の問題があることがわかります。 論文に示されていない例として、以下のパターンが考えられます。 肌の露出が大きく変わる場合(長袖からノースリーブへの着せ替えなど) シルエットが大きく変わる場合(パンツからスカートへの着せ替えなど) 肌の描写やシルエットの変更はGeneratorの負担を増やすことになるため、上手くいかない可能性があります。 商品にはサイズのバリエーションがあり、どのサイズを選ぶかによって見た目は変化します。生成結果は、一見それらしい着せ替えを実現していますが、サイズバリエーションは反映されていません。生成結果は、実際の商品サイズを反映したものであるべきです。 データセットの課題 学習に使われるデータのほとんどはファッションモデルが商品を着用しているものです。細身で頭身の高い傾向の人物しか含まれていません。実際に推論したいデータには、さらに多様な体型の人物が含まれているはずです。 また、モノトーンの背景・商品が映えるような環境光やポーズといった、商品のカタログ的なシチュエーションが設定されています。推論時には屋外で撮られた、背景・環境光・ポーズがより複雑なデータを扱うことになります。 このように、実用の際には学習時と推論時で入力データの質の違いが問題になり得ます。 CAGANの登場以降、これらの課題を解決するための様々なアプローチが提案されました。次章では、そのような研究の概要をいくつか紹介します。 課題へのアプローチ 本章の内容は専門性が上がるため、まずはじめに各研究の特徴を表にまとめておきます。 ※表を拡大表示 全体的に人物・商品のペア画像が不要となる代わりに、Human Parsingやポーズ推定の前処理が必要になってきている傾向があります。 前述のモデルアーキテクチャと性能面の課題に関して、レイアウトの生成と見た目の生成という二段構えのアプローチを取ることで大きな改善が見られています。しかし、商品サイズの違いを反映するモデルはまだ見たことがありません。 ここで挙げた手法以外にも多くの研究があるので、興味を持っていただけた方は、参考文献の引用から関連する研究を調査すると良いでしょう。 VITON (引用:VITON) VITON 4 はファッション関連の面白いタスクをたびたび提案するXintong Hanらによる研究です。VITONは着せ替えのプロセスを、衣服領域のマスクの生成と、衣服領域に合うように歪ませた商品画像のテクスチャの貼り付けの二段階に分けて行います。CAGANの課題だった、推論時に人物画像 とその人物が着ている商品画像 のペアが必要であった点や、個性の保持・テクスチャの転写の性能を改善しています。 (引用:VITON) 1段目のEncoder-Decoder Generatorは、入力データとして人物画像そのものではなく、ポーズ・身体形状・顔と髪の領域の組み合わせという、衣服の情報を削ぎ落としたものを使います。これに商品画像を加え、元の人物画像を復元するという戦略で学習させます。CAGANでは、人物画像 のどの部分が衣服領域であるかを商品画像 との関係から推測する必要がありました。一方、VITONでは前処理の段階で、確実に人物画像から衣服の情報を取り除いているため、これを商品画像で条件付けることにより、元の人物画像を復元するだけで着せ替えが実現します。この工夫により、ピクセルレベルの教師あり学習をさせることが可能なため、Cycle GANの構造が不要になります。 1段目の出力は、復元された人物画像と衣服領域のマスクです。衣服領域のマスクは、商品画像をどのように歪めたら人物画像にフィットするのかの情報を持っています。この情報を使い、Thin Plate Spline Transformationで商品画像を人物に合わせて歪めたものを生成します。 2段目のRefinement Networkは、1段目で得られた人物画像と衣服領域に合わせて歪めた商品画像を入力とし、衣服領域のテクスチャの合成を行います。テクスチャをGeneratorで生成するのではなく、元の商品のテクスチャを貼り付けるというイメージなので、商品のテクスチャが綺麗に反映されやすくなっています。 M2E-TON (引用:M2E-TON) Model2Everyone Try-On Network(M2E-TON) 5 は、商品画像を使わず、人物が着用している商品を任意の人物に転写するアーキテクチャを提案しています。CAGANやVITONは人物と商品のペアが必要でしたが、人物画像だけを使うという点で学習データの収集コストを大幅に減らしています。このアーキテクチャは、人物から人物への着せ替えの工程を以下の3つのEncoder-Decoder型のGeneratorで実現します。 Pose Alignment Network (PAN) Texture Refinement Network (TRN) Fitting Network (FTN) また、補助情報として、3Dの人体表面を推定するDense Poseモデルの推論結果を追加します。上図では、緑色の矢印で示されています。モデル人物画像のDense Poseとターゲット人物画像のDense Poseの人体表面を対応付けて歪ませると、ひび割れたようなテクスチャ が得られます。PANはモデル人物画像とひび割れたテクスチャを使ってモデル人物画像のポーズをターゲット人物のポーズに合わせるGeneratorです。この出力 はぼんやりとしており、テクスチャの詳細部分が失われているため、元のひび割れたテクスチャ と合成することで細部の表現を取り戻します。しかし、ひび割れ部分のエッジ部分が目立つのでTRNで綺麗に修復します。端的に言えば、二度のEncoder-Decoderでモデルのポーズを無理やりターゲットのポーズに変形して画像のあらを修復したのです。 (引用:M2E-TON) 上述のPAN・TRN・FTNの学習は、CAGANと同様の理由によりピクセルレベルの教師データを与えるのが困難です。M2E-TONはこの課題をUnpaired Training(上図左)とPaired Training(上図右)の2つの学習戦略を合わせることで対応します。 Unpaired Trainingは、服装やポーズに対応関係のないモデルとターゲットの情報を入力し、ポーズで条件付けたGANの損失関数を使って学習を進めます。しかし、この学習戦略で評価されるのは、出力と入力のポーズの一致具合と出力が本物の画像に近いかどうかだけです。衣服部分のテクスチャが正確に反映されているかどうかを評価する方針を別に与える必要があります。その対策としてPaired Trainingを行います。Paired Trainingは、ECサイトで手に入りやすい同じ人物が同じ服を着て異なるポーズを取っている画像ペアを使います。GANの損失関数に加えて入力 と出力 のピクセルレベルの誤差を損失関数として導入します。 (引用:M2E-TON) Paired Trainingを行うことにより、Generatorにポーズの違いだけでなくテクスチャの細部にまで転写できるような学習方針を取らせることができます。上図の4番目の画像と6番目の画像はPaired Trainingの有無の差を表しています。 ボトムスの着せ替えは論文中に一例のみだったので、上手くいかなかった可能性があります。アーキテクチャ的にはマルチカテゴリに対応できそうですが、解決できなかった課題が残っている可能性があります。 FiNet (引用:FiNet) FiNet 6 はVITONと同じ著者の研究です。人物に指定の商品を着せるタスクではなく、着せ替えたい領域を欠損させて、欠損を修復することで様々な着用イメージを生成するというタスクを提案しています。さらに、適当な衣服を埋めて修復するのではなく、欠損させた部分以外の文脈をきちんと反映させた上で修復するため、ファッションとして調和した生成結果が得られるように工夫されています。 FiNetはShape GenerationとAppearance Generationの2段階で、画像を修復します。 (引用:FiNet) Shape Generationは元の人物の顔領域とポーズ、欠損させたレイアウト、欠損させた部分をどのように補うかを示すShape Codeを入力とし、元のレイアウトを復元します。学習時はShape Codeとして元の衣服領域(Input Shape)を符号化したものを使います。そして、推論時は欠損させた部分以外の衣服画像(Contextual Garments)を符号化してGeneratorに与えます。学習時と推論時で利用する情報が異なる点は、両者が同じ分布から生成されるようにEncoderを学習することで対応しています。 (引用:FiNet) Appearance Generationも、Shape Generationと同じアーキテクチャで元の人物画像を復元します。 (引用:FiNet) 上図は実際の推論結果を示しています。レイアウトの生成と外見の生成をトップス(左)、ボトムス(右)に関して行っています。Shape CodeとAppearance Codeをサンプリングすることで、全体として調和した外見を数多く生成することに成功しています。 ここまでに紹介した研究は、明示的には複数カテゴリへの対応はしていませんでした。一方、FiNetはトップスやボトムスだけでなく、シューズやハットまで対応できており、汎用性の高さが伺えます。 FiNetは変更を加えたい部分をマスクして、その領域を埋めることで様々な外見を生成できる手法です。さらに、マスクした部分以外の文脈情報を考慮して全体として調和した結果を生成するため、その保証はないですが、ファッションセンスの高いものになっているというファッションらしいタスクを解いています。ただし、このタスクは指定の衣服を着せるというこれまでに紹介したタスクと異なります。 (引用:FiNet) 著者らは、メインのタスクではないとしながらも、上図のように任意の衣服が着せられるかを実験しています。上図にはReference、Input、Transferの3種類の画像が掲載されています。Referenceの人物が着用している衣服をInputの人物に着せた結果がTransferです。ReferenceからShape CodeとAppearance Codeを生成し、欠損させた画像(Input)と合わせて渡すことで、任意の衣服を着せることが可能であり、しかも上手くいくようです。このようにFiNetは任意の衣服を着せるタスクにも利用でき汎用性を持っています。 O-VITON (引用:O-VITON) O-VITON 7 はAmazon Lab126の著者らがCVPR2020で発表した研究です。FiNetと同様に、Shape Generation(上図の緑色部分)とAppearance Generation(上図の青色部分)を順に実施して着せ替えを実現します。 他のモデルと大きく異なる点は、複数のカテゴリの商品を同時に着せることができるところです。入力データは人物が着用している画像を領域分割したものです。そして、入力として使う人物画像の各衣服領域の特徴マップを、着せたい衣服の特徴マップと差し替えてレイアウト生成モジュールや外見生成モジュールに入力します。どちらのモジュールも学習時は着せ替え自体を行わず、特徴マップから元の画像を復元することにフォーカスしています。なお、複数の商品を着せる能力自体は汎化性能によるものです。 また、損失関数として、Shape Generatorはレイアウトのピクセルレベルの誤差とレイアウトのShape Feature Mapで条件付けたGAN Lossを用います。Appearance Generatorはレイアウトで条件付けたGAN Lossと、生成物と元データのFeature Matchingを用います。 (引用:O-VITON) FiNetではShape GenerationとAppearance Generationの2つで終わりでした。しかし、O-VITONはさらに見た目を改善するために、推論時にAppearance Generatorに個別のファインチューニング(Online Optimization)をかけます。 この段階では、リファレンス画像のレイアウトと、衣服を着せたい人物のレイアウト(クエリ)を入力します。リファレンス画像の復元結果はFeature Matchingを、クエリへの着せ替え結果はDiscriminatorの損失を評価して学習します。リファレンス画像の衣服がクエリレイアウトにきちんと転写されるまで最適化をかけます。 (引用:O-VITON) Online Optimizationを取り入れた場合(一番右の列)、単に二段階の過程で生成した場合(右から2番めの列)に比べて、元の商品のテクスチャが正確に反映できています。 (引用:O-VITON) O-VITONは学習時には複数の商品を着せての評価はしていません。しかし、クエリ画像の各カテゴリの特徴マップをReference Garmentsの特徴マップに差し替えることで、複数のカテゴリの商品をクエリの人物に対して着せることができています。本来解いているタスクではないのに、このようなことができるということは、モデルの汎化性能が高いということを示しています。 まとめ ※表を拡大表示 モデルアーキテクチャの課題 CAGANは学習時と推論時の両方で、人物画像に対応する商品画像が必要でした。一方で、CAGAN以降の研究では、Human Parsingやポーズの推論結果を利用することで制約が緩和されています。また、M2E-TON・FiNet・O-VITONのように人物画像のみで学習・推論が可能なモデルも登場しています。 また、FiNetやO-VITONはレイアウトを明示的に扱うことにより、単一のモデルで複数のカテゴリの着せ替えに対応可能となり、高い柔軟性を示しています。 性能の課題 着せ替えの出力をGeneratorに任せると顔やポーズ、テクスチャが崩れるといった問題がありました。その点に関しては、VITONのように事前に切り出しておき、それを後続のタスクに渡して微修正する方法が有効な場面が多いでしょう。Generatorに任せる場合は、O-VITONのように推論のたびにチューニングし直し、レアなパターンやロゴを正確に転写させる方法が良さそうです。他の手法の仕上げ段階でも取り入れられる汎用的なアイデアです。 肌の露出やシルエットの変化は、FiNetやO-VITONのようにレイアウトを編集するモジュールを取り入れることで対応できそうです。 しかし、実際の商品サイズを考慮する点に関しては、サイズ情報を扱う方法は模索されていません。今後の課題になるでしょう。 データセットの課題 推論時の多様なシチュエーションへの対応は、多くの研究で検証できていません。今回紹介した論文では唯一、VITONが屋外の複雑な環境の写真に対しての推論結果を掲載しています。 (引用:VITON) 対象のポーズと商品がシンプルな左の2つの画像は、若干服が浮いて見えますが貼り付け自体は上手くできているように感じます。しかし、中央の画像は肌の露出が増えており、腕の形や色の生成に不自然さが残ります。そして、右の画像では、テクスチャの転写に失敗しています。 下記のブログでは、着せ替えモデルを以下の4パターンでテストしています。 www.kdnuggets.com Replication of the authors’ results on the original data and our preprocessing models (Simple). Application of custom clothes to default images of a person (Medium). Application of default clothes to custom images of a person (Difficult). Application of custom clothes to custom images of a person (Very difficult). 上述のVITONの例は綺麗な商品画像とカスタムの写真を使っているので 3. のDifficultに相当します。このブログでは、カスタムの商品画像としてCGや絵を渡してみたりと、チャレンジングな状況でのテストも実施しています。自前で用意したデータに対する実用性を測る上で、重要なテスト項目です。 最後に ZOZO研究所では、機械学習の社会実装を推し進めることのできるMLエンジニアを募集しています。今回紹介したVirtual Try-Onのタスク以外にも、検索、推薦、画像認識の技術など幅広い分野で研究や開発を進めていけるメンバーを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co hrmos.co hrmos.co 参考 Choi, Seunghwan, et al. "VITON-HD: High-Resolution Virtual Try-On via Misalignment-Aware Normalization." arXiv preprint arXiv:2103.16874. 2021. ↩ Sarah Perez, 「ウォルマートがAIやコンピュータービジョンを駆使したバーチャル試着のZeekitを買収」 ,TechCrunch. 閲覧日:2021年6月9日. ↩ Jetchev, Nikolay, and Urs Bergmann. "The conditional analogy gan: Swapping fashion articles on people images." Proceedings of the IEEE International Conference on Computer Vision Workshops. 2017. ↩ Han, Xintong, et al. "Viton: An image-based virtual try-on network." Proceedings of the IEEE conference on computer vision and pattern recognition. 2018. ↩ Wu, Zhonghua, et al. "M2e-try on net: Fashion from model to everyone." Proceedings of the 27th ACM International Conference on Multimedia. 2019. ↩ Han, Xintong, et al. "Compatible and diverse fashion image inpainting." arXiv preprint arXiv:1902.01096. 2019. ↩ Neuberger, Assaf, et al. "Image based virtual try-on network from unpaired data." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2020. ↩
はじめまして、ZOZO研究所 福岡の家富です。画像検索システムのインフラ、機械学習まわりを担当しています。 今回は、t検定におけるサンプルサイズが与える影響を解説します。 目次 目次 t検定の使われ方 t検定 t検定の問題点 論文手法 実際の購入金額データに対する考察 まとめ さいごに t検定の使われ方 近年、施策が有効かどうかをデータを元に統計的に判断していこう、という話を聞くことが増えてきました。 経済学の流行においても、統計的な指標を重要視する流れが強まってきています。例えば、貧困対策にお金をどの程度どのような用途で支給するのが良いか、といった議論で利用されることも多くなってきています。 www.amazon.co.jp Web業界においても、サイトの変更や施策の有効性をA/Bテストなどを実施し、統計的に判断していく流れが主流になってきています。なお、本記事では以下の文献を「A/Bテスト本」と呼びます。 www.amazon.co.jp そして、このようなA/Bテストから得られた統計情報に対し、それが有効か否かを判断する方法の1つにt検定があります。 t検定 例として、次のような各ユーザーの一週間の購入金額のデータを考えます。 2つの施策AとBがあり、それぞれの施策に対し、以下のような購入者の購入金額のデータが得られたとします。 施策Aの購入者のデータ: 同様に施策Bの購入者のデータ: ここで、n、mはそれぞれ施策A、Bの購入者数を表し、 、 はそれぞれのユーザーの購入金額とします。 この時に、「施策Aの分布と施策Bの分布を比べて、施策Aの方が良いと言えるのか?」というのが一般的に考えたい問題かと思います。 一番最初に思いつく方法は、それぞれの平均値 、 を見る方法です。 ならば施策Aの方が良いと考えます。 ここで問題になるのは、「たまたま施策Aの平均の方が少し良かっただけなのでは?」という可能性を捨てきれない点です。非常に小さな差しかなければ、「たまたまじゃないのか」という気になります。では、「どれほどの差があれば十分施策Aの方が良いと言えるのか?」というの疑問が湧いてきます。検定を使えば、この疑問に答えることができます。 t検定の場合、次のような論法をとります。 まず、施策A・Bのどちらのデータも、ある潜在的な確率分布から独立にサンプリングされたものと考えます。なお、この見方は実際のデータに対してよく仮定されるものです。次に「施策Aの潜在的な分布の平均値」と「施策Bの潜在的な分布の平均値」は等しいと仮定します。そして、以下の式を計算します。 この、 は以下で定義されるものです。 「ある仮定のもと(詳細は後述します)」で、この値は自由度m+n–2のstudentのt分布に従うことが証明されています。正確には、ウェルチのt検定なので自由度が多少異なりますが、m=nの時はほぼ同様の自由度となります。 en.wikipedia.org そのため、t分布の表を見ることで、上記で得られたt値が実際にどの程度の確率で生じたものなのかがわかります。 例として、m=n=121の場合、つまり該当のt分布の自由度が240となる場合を考えてみます。計算されたt値が1.392であったとします。これは自由度240のt分布において上位5%となる点は1.651なので、そこには含まれません。一方、上位10%でのt分布の値は1.285です。そのため、上記の値は5〜10%の間で生じる値ということになります。 検定でよく採用される5%を基準とした場合、上記の値は起こりうる確率の範囲と捉えることができ、特に矛盾はないと考えます。そして「施策Aの潜在的な分布の平均値」と「施策Bの潜在的な分布の平均値」が等しいことに問題はないと解釈します。 また、10%を基準とした場合には、上記の値は起こりうる確率の範囲外にあると考えられます。すなわち、「施策Aの潜在的な分布の平均値」と「施策Bの潜在的な分布の平均値」は等しくない、施策Aは有意に有効であると考えられます。 上述のように、2つのデータの違いを評価することに使われます。 この「破られることを期待される仮定を置く」という論法が若干わかりにくくしている部分であり、時々誤解を生んでいることもあります。このような論法が使われる背景には、たくさん施策し数値的に特に目立った施策だけを拾っていこう、という発想があります。 t検定の問題点 前章でt検定の使用方法を説明しました。実際に使用する際には、t値がt分布に従うかどうかがポイントになります。 「ある仮定のもと」と前章で述べましたが、ここではその点をさらに見ていきます。t値がt分布に収束するには、以下のように定義された値 がt分布に従う必要があります。 なお、 は「潜在的な分布の平均値パラメータ」であり、データから推測はできますが、一般のデータからは得られないことに注意が必要です。 次に、 がt分布に従うための条件を見るために、以下のように分解します。 なお、 は「潜在的な分布の標準偏差パラメータ」であり、データから推測はできますが、一般のデータからは得られないことに注意が必要です。 ここでは、 が中心極限定理より「nが十分大きい時に」正規分布に収束します。また、 が「nが十分大きい時に」自由度n-1のカイ二乗分布に収束します。このことから、 は「nが十分大きい時に」自由度n-1のt分布に従うと言えます。なお、nが十分大きい時なので、nとn-1の差はほぼ考えなくても良いです。 この時「nが十分大きい時に」という表現が、実際にどれくらいの大きさならば誤差を無視できるのかが問題となります。 「A/Bテスト本」では、元の分布の歪度( )が大きい場合の問題を指摘しています。なお、 は以下のように定義されます。 なお、 は分布の期待値、 で定義される分布の分散を表します。歪度が大きい場合、中心極限定理による近似度があまり良くありません。そのため、t検定をする際にはサンプルサイズを大きく取る必要があると主張しています。 「A/Bテスト本」では、サンプルサイズnは 以上必要だと主張しており、歪度が10以上となるとかなりのサンプルサイズが必要だと主張しています。さらに、この主張がどこからくるのかについて調べていくと「How Large Does n Have to Be for Z and t Intervals?, Dennis D. Boos and Jacqueline M. Hughes-Oliver」という論文によるものだとわかりました。 次章では、上記論文の内容を紹介していきます。 論文手法 「How Large Does n Have to Be for Z and t Intervals?, Dennis D. Boos and Jacqueline M. Hughes-Oliver」(以下「本論文」と呼ぶ)ではまず、歪度と中心極限定理の関係について述べています。 1次元の実数上の確率分布に対し、正規分布との関係を述べた定理としてGram–Charlier A seriesがあります。この定理を に対して適用したものがEdgeworth seriesです。 en.wikipedia.org この定理により、一般の確率分布の累積分布関数をn -1/2 の級数展開として得ることができます。そして、n -1/2 以降の項はn -1 ,n -3/2 と続きます。サンプルサイズnが大きくなる場合、n -1 以降は収束が速いため、n -1/2 の項のみに注目すれば良いことがわかります。ここで、n -1/2 の項の係数をみると歪度が出てくるため、収束の速度に歪度が重要なパラメータとして関わっていることもわかります。 また、中心極限定理の収束速度を表す定理の1つであるBerry-Esseenの定理においても、やはり収束誤差を で抑えられると言われています。 en.wikipedia.org 上記の知見を元にし、本論文では「上位5%を表す閾値0.05でのt値に対し、 だけずれる。なお、cは分布に依らない定数。」と仮定します。一般にこの閾値は「 」のように で表すことが多いため、本記事でもこの表記を使用します。 次に、先程登場したcを求める方法について述べます。本論文では様々な既存の分布から実際にサンプリングを行い、歪度とズレに関して線形回帰をすることによって求めています。よく知られた解析的な確率分布を用いているので、 のような潜在的なパラメータをシミュレーション結果からではなく、実際に計算できるので、結果としてt値を計算することが可能となります。 本論文では以下の分布を使用しています。 ガンマ分布 = 1, k=1, 1.78, 4, 16としたもの ja.wikipedia.org ワイブル分布 scale parameter=1, shape parameter= 1.2, 1.6, 2.2としたもの en.wikipedia.org 論文「An Asymptotically Distribution-Free Test for Symmetry versus Asymmetry」による3パターンのオリジナル分布 lam1, lam2, lam3, lam4 = 0, 1.0, 1.4, 0.25 # 7 lam1, lam2, lam3, lam4 = 0, 1.0, 0.000070, 0.10 # 8 lam1, lam2, lam3, lam4 = 0, -1.0, -0.1, -0.18 # 12 上記の分布に対し、本論文においてはズレ(miss)を次のように計算します。 まず、各分布に対して30サンプル抽出した時のt値を10,000セット用意します。なお、今回の場合だと300,000サンプル合計抽出します。 次に、用意した10,000個のt値を大きい順に並べ替え、大きい方からt値を調べていきます。正確な自由度30のt分布においては5%に相当するt値=1.697と比較していき、それを下回るのが何番目になるかをチェックします。例えば、上位から125番目が該当した場合、上位5%とのずれは0.05 - 125/10000 = 0.0375となります。 このように様々な歪度の分布からのt値を計算し、実際のズレのデータを取得します。論文では、さらに小さい方からのt値も見ていき、左側検定の場合のズレも計算しています。 以下はズレを取得する処理のPythonの疑似コードです。 t_list = [] for s_id in range ( 10000 ): np.random.seed(s_id) xs = [] for _ in range ( 30 ): # scale parameter=1, shape parameter=1 のgamma分布からのサンプリング a = np.random.gamma( 1 , 1 ) xs.append(a) t_list.append( get_t(xs) ) # t値を計算して、リストに登録 t_list.sort() for i,t in enumerate (t_list): # left if t > - 1.697 : left_percentile = i/ 10000 break for i,t in enumerate ( reversed (t_list)): # right if t < 1.697 : right_percentile = i/ 10000 break print ( 0.05 - left_percentile, 0.05 - right_percentile) 本論文では、左側のズレが0.116、右側のズレが-0.067という結果が掲載されています。一方で、私が行った実験結果では左側のズレが0.11186489、右側のズレが-0.03688539という結果が出ています。 最後に、この結果を用いて必要なサンプルサイズを求めます。本論文ではズレ(miss)が =0.01以下となるようにサンプルサイズnを決めます。 これをnの式に直すと、 となります。 先程の実験結果から、値の大きい左側を使うと以下の結果が算出されます。 本論文結果: 私の実験結果: 誤差を0.005以下にすると考えた場合、係数は本論文では538.24、私の実験結果では500.55となります。「A/Bテスト本」で述べられていた355という係数は得られませんでしたが、数値のオーダーはほぼ同等のものが得られたと考えられます。 実際の購入金額データに対する考察 冒頭で紹介した「各ユーザーの一週間の購入金額のデータ」に対して歪度を計算してみました。歪度は700.43となり、精度を得るために必要なサンプルサイズはn=61,389,051.39と算出されます。これは現実的ではない値です。 「A/Bテスト本」ではこのような場合、ある閾値以上の部分は落とすという方法を推奨しています。今回は10万円以上のユーザーは、別データに分けました。そうすることで、10万円未満のデータの分布は歪度が2.750となり、現実的な数値となりました。 なお、10万円以上のデータの分布はこれにより歪度が35.637となり、だいぶ小さい値になりました。扱いやすくなりましたが、まだ工夫が必要な値です。さらに細かく層を分けて対応するなどの工夫が必要でしょう。 まとめ 本記事では、t検定におけるサンプルサイズの考察を紹介しました。 実際にt検定の理論を深堀りした際には、Edgeworth seriesなどは意外と文献が見つけにくく、苦労しました。しかし、自ら数値実験をすることで実際の数値とのズレ具合など、知見を得ることができました。また、実データは想像以上に歪みの大きいデータが多いため、そのまま適用とはいかないケースがかなりあることも実感しました。 さいごに ZOZOテクノロジーズではZOZO研究所のMLエンジニアを募集しています。本記事に興味を持っていただけた方は、ぜひご応募ください。 hrmos.co
こんにちは。ZOZO研究所の山﨑です。 ZOZO研究所では、検索/推薦技術をメインテーマとした論文読み会を進めてきました。週に1回の頻度で発表担当者が読んできた論文の内容を共有し、その内容を参加者で議論します。 本記事では、その会で発表された論文のサマリーを紹介します。 目次 目次 検索/推薦技術に関する論文読み会 発表論文とその概要 SIGIR [SIGIR 2005] Relevance Weighting for Query Independent Evidence [SIGIR 2010] Temporal Diversity in Recommender System [SIGIR 2017] On Application of Learning to Rank for E-Commerce Search [SIGIR 2018] Should I Follow the Crowd? A Probabilistic Analysis of the Effectiveness of Popularity in Recommender Systems [SIGIR-workshop eCom 2018] Towards Practical Visual Search Engine within Elasticsearch [SIGIR 2020] Models Versus Satisfaction: Towards a Better Understanding of Evaluation Metrics [SIGIR 2020] Studying Product Competition Using Representation Learning [SIGIR 2020] Understanding Echo Chambers in E-commerce Recommender Systems [SIGIR 2020] Cascade or Recency: Constructing Better Evaluation Metrics for Session Search KDD [KDD 2012] Summarization-based Mining Bipartite Graphs [KDD 2019] Applying Deep Learning To Airbnb Search [KDD 2020] Embedding-based Retrieval in Facebook Search [KDD 2020] Controllable Multi-Interest Framework for Recommendation [KDD 2020] On Sampled Metrics for Item Recommendation [KDD 2020] Personalized Image Retrieval with Sparse Graph Representation Learning [KDD 2020] Managing Diversity in Airbnb Search [KDD-workshop 2020] Lessons Learned Addressing Dataset Bias in Model-Based Candidate Generation at Twitter TheWebConf (旧WWW) [WWW 2020] NERO: A Neural Rule Grounding Framework for Label-Efficient Relation Extraction [WWW 2020] The Difference Between a Click and a Cart-Add: Learning Interaction-Specific Embeddings RecSys [RecSys 2018] Calibrated recommendations [RecSys 2019] A Pareto-Eficient Algorithm for Multiple Objective Optimization in E-Commerce Recommendation その他 [WSDM 2010] Anatomy of the Long Tail: Ordinary People with Extraordinary Tastes [VLDB 2013] Supporting Keyword Search in Product Database: A Probabilistic Approach [ACL-short 2018] ‘Lighter’ Can Still Be Dark: Modeling Comparative Color Descriptions [ECIR 2020] From MAXSCORE to Block-Max Wand: The Story of How Lucene Significantly Improved Query Evaluation Performance [MLSys 2020] Understanding the Downstream Instability of Word Embeddings [COMPUTER GRAPHICS Forum 2020] Interactive Optimization of Generative Image Modelling usingSequential Subspace Search and Content-based Guidance まとめ おわりに 検索/推薦技術に関する論文読み会 ZOZO研究所では主に検索/推薦技術に関する論文読み会を進めてきました。論文読み会とは週に1度、1人が読んできた論文を発表し、その内容を議論する場です。 論文の選択基準は特に指定せず発表者に一任していましたが、 SIGIR や KDD といったトップカンファレンスの論文が人気でした。 これまでに約30本の検索/推薦技術を中心とした幅広い分野の論文が社内で共有され、実際のプロダクト開発にも活かされています。 論文読み会はチームに閉じた形式ではなく、この取り組みに賛同する社員であれば誰でも参加できる形式を取りました。その結果、10〜20名程度が参加し、活発な議論が生まれました。 次章では、発表された論文のサマリーを紹介します。 発表論文とその概要 本章で掲載している画像は、特別な記載が無い限り全て原著論文より引用しています。 SIGIR [SIGIR 2005] Relevance Weighting for Query Independent Evidence どんなもの? クエリに依存しない特徴を、クエリに関連するスコア(BM25など)に活用してランキングを調整する方法を提案した。 こちらは、 Elasticsearch 7.0のrank_featureの開発の基となった論文 である。 先行研究と比べてどこがすごい? クエリに依存しない静的な特徴をBM25と組み合わせる手法の提案と、その手法がどの程度成功したかを測定した。 技術や手法のキモはどこ? 静的な特徴との組み合わせを3つの関数で実験した。 静的な特徴とクエリに関連するスコアの組み合わせに単純な独立性を仮定すると、うまく動作しないケースがあった。そのため、FLOEという手法を用いて解決した。 どうやって有効だと検証した? ベースライン手法のBM25と静的な特徴の組み合わせの手法を比較した。 TRECのデータを使用して、MAPが改善した。 [SIGIR 2010] Temporal Diversity in Recommender System どんなもの? レコメンドシステムにおける推薦アイテムの時間的な多様性を研究した。 同じ商品が繰り返し推薦され続けるシステムは時間的な多様性が低い、というイメージ。 また、精度を大幅に減少させること無く、多様性を最大化するレコメンド手法を提案した。 先行研究と比べてどこがすごい? レコメンドにおける推薦アイテムの時間的な多様性の重要性をアンケート調査によって明らかにした。 技術や手法のキモはどこ? レコメンドの多様性を促進するために、レコメンドモデルを時間経過に伴って切り替えた。 どうやって有効だと検証した? 5週間の継続的なアンケート調査によって、レコメンドシステムの多様性がアイテムのレーティングに影響を与えることを発見した。 また、Netflixのデータセットを使って複数のレコメンド手法に対する多様性の評価・比較をした。 [SIGIR 2017] On Application of Learning to Rank for E-Commerce Search どんなもの? ECサイト検索でランキング学習モデルを活用する際の効果的な特徴量とRelevancy(クリック率・カート追加率など)について調査した。 また、クラウドソーシングを用いた結果がモデルに活用できるかを調査した。 先行研究と比べてどこがすごい? ECサイト検索において網羅的にランキング学習モデルの良し悪しを測った研究は、これまで存在しなかった。 ECサイト検索にランキング学習モデルを導入する際の特徴量やRelevancyについて実践的な知見を展開した。 技術や手法のキモはどこ? ECサイト検索に有効な特徴量・モデル・Relevancyを評価する実験設計を行った。 どうやって有効だと検証した? 実際の企業データを用いて検索結果のnDCGを測定する実験を行い、以下のことが分かった。 LambdaMARTがモデルとして精度が良い。 ECサイト検索の評価をクラウドソーシングで実現することは困難である。 Relevancyは注文率が良い。 [SIGIR 2018] Should I Follow the Crowd? A Probabilistic Analysis of the Effectiveness of Popularity in Recommender Systems どんなもの? アイテムの人気度(多くのpositiveな反応があるアイテム)はレコメンドシステム構築において取り除くべきバイアスかどうかを検証した。 先行研究と比べてどこがすごい? これまで、レコメンドシステムで人気度を「回避すべきバイアス」として扱うべきかの議論があった。 本研究ではレコメンドシステムにおいて、人気度が効果的となる条件とその逆の条件を特定した。 技術や手法のキモはどこ? 人気度の有効性は以下の3変数の相互作用に依存することを発見した。 適合性 : アイテムが多くのユーザーに好まれやすいか 発見性 : 好みのアイテムを発見しやすいか 評価するユーザーの判断 : 好みのアイテムを評価しやすいか その上で、人気度がレコメンドにおいて効果的な条件とその逆の条件を特定した。 どうやって有効だと検証した? クラウドソーシングで構築した独自のデータセットを用いて実験を行い、理論的に導いた結論が観測結果とどの程度一致するか検証した。 一般的なデータセットからバイアスを除去し、バイアスがある状況との精度の違いを示した。 レコメンドにおいて多くの場合、平均評価が評価数よりも効果的であることを発見した。 [SIGIR-workshop eCom 2018] Towards Practical Visual Search Engine within Elasticsearch どんなもの? Elasticsearch上で画像検索を実装し、その手法を説明した。 先行研究と比べてどこがすごい? より近いベクトルがより多くの文字列トークンを共有するように、画像特徴ベクトルを文字列トークンのグループにエンコードした。 技術や手法のキモはどこ? 画像特徴ベクトルをそのまま使うのではなく、転置インデックスに適した文字列の形にエンコードして検索可能な状態にした。 どうやって有効だと検証した? Jet.comの約50万枚・1536次元の画像から、1,000件をランダムに検索したときの精度と速度を既存手法と比較し、改善されていることを示した。 [SIGIR 2020] Models Versus Satisfaction: Towards a Better Understanding of Evaluation Metrics どんなもの? ユーザーの行動データに対して最適化された評価指標が、ユーザー満足度の推定においても同様に機能するかどうかを調査した。 先行研究と比べてどこがすごい? 検索システムの評価指標の妥当性を、ユーザーの行動の予測の正確性とユーザーの満足度の2つの側面で整合性があるかを調査した。 また、データセットも独自にフィールドスタディを行って作成した。 技術や手法のキモはどこ? C/W/Lフレームワーク を用い、代表的な評価指標に対してユーザーモデルの精度とユーザー満足度との相関を調査した。 どうやって有効だと検証した? 独自で作成したデータセットと公開されている検索行動データセットを用いた。 ユーザーのクリック行動に合わせて最適化された評価指標は、ユーザー満足度の情報でキャリブレーションされたメトリクスと同等の性能を発揮できることが分かった。 [SIGIR 2020] Studying Product Competition Using Representation Learning どんなもの? 数百万商品のEC市場で、商品レベルの競合関係を購買情報を用いて分析した。 先行研究と比べてどこがすごい? 商品情報をEmbeddingすることにより、製品数で計算コストが線形に増大しないモデルを作成した。 技術や手法のキモはどこ? 商品情報はWord2vecと同様の手法でEmbeddingした。 競合関係は代替品と補完品を定量的に区別して分析した。 どうやって有効だと検証した? 公開されたベンチマークで、既存手法と比較し、HitRateなどの指標で改善されていることを示した。 [SIGIR 2020] Understanding Echo Chambers in E-commerce Recommender Systems どんなもの? ECサイトのレコメンドでも エコーチェンバー効果 が発生しているかを分析し、クリックに関しては傾向があることを観測した。 先行研究と比べてどこがすごい? 人工的に作成された環境ではなく、大規模なECサイトの実データを用いて初めてエコーチェンバー効果を評価した。 技術や手法のキモはどこ? ユーザーのクラスターの状態の時系列変化を分析することで、エコーチェンバーを観測した。 どうやって有効だと検証した? Alibaba Taobaoのデータを用いて分析し、クリックに関してはエコーチェンバー効果の傾向が見られた。 購入に関してはその効果が緩やかになっているという結果も分かった。 [SIGIR 2020] Cascade or Recency: Constructing Better Evaluation Metrics for Session Search どんなもの? カスケード仮説と親近効果の両方を考慮したセッションベースの検索指標(RSMs)を提案した。 カスケード仮説 :順位の低い検索結果はユーザーの注目度が低いため、評価時には小さな重みを割り当てる方が良い 親近効果 :ユーザーが同セッション内の後段で発行したクエリに、より大きな重みを割り当てる方が良い 先行研究と比べてどこがすごい? 親近効果を考慮した検索指標を初めて提案した。 検索のユーザー満足度を測るデータを作成し公開した。 技術や手法のキモはどこ? 親近効果を「セッション内最後のクエリとその時に発行されたクエリの距離」として定義し、既存の評価のフレームワークに組み込んだ。 どうやって有効だと検証した? 独自のユーザーの検索行動データセットを作成し、既存のセッションベースメトリクスと比較することで、ユーザー満足度との相関関係を明らかにした。 KDD [KDD 2012] Summarization-based Mining Bipartite Graphs どんなもの? 二部グラフから真の関係情報を抽出し、その情報を利用して複数のタスクを解いた。 先行研究と比べてどこがすごい? 結果の解釈がシンプルかつ容易である上に、リンク予測問題やクラスタリングのタスクを精度良く解くことができた。 技術や手法のキモはどこ? 二部グラフに対し、両タイプのノードを同時にクラスタリングした上で、クラスタ同士の関係性を可視化した。 クラスタリングはMDLを用いてエッジの追加削除を選択した。 どうやって有効だと検証した? 人工データと一般のデータを用い、リンク予測問題とクラスタリングのタスクを従来手法よりも良い結果で解いて有用性を示した。 また、クラスタ同士の可視化も従来手法よりも容易に解釈が可能となった。 [KDD 2019] Applying Deep Learning To Airbnb Search どんなもの? Airbnbがディープラーニング(DL)を検索ランキングへ適用するために行ったことと、それまでの歴史をまとめた。 下図のようにDLで成功するまでの長い道のりが記されている。 先行研究と比べてどこがすごい? Airbnbの中での検索ランキングモデルの歴史と失敗した取り組みまで紹介されている。 技術や手法のキモはどこ? 段階的にランキングモデルをどのように変更して改善したかが書かれている。 ハンドメイドのスコアリング GBDTとFMをNNの入力にしたモデル DNNを適用 どうやって有効だと検証した? 実際にオフラインとオンラインでテストを実施して精度向上を確認した。 [KDD 2020] Embedding-based Retrieval in Facebook Search どんなもの? Facebookは従来Boolean Matchの検索だったが、Embedding-basedな検索にした。 その際に改善させたアーキテクチャなどを紹介している。 先行研究と比べてどこがすごい? Facebookが抱えていた、テキスト情報だけでは検索がうまく行われない問題を解決した。 Embedding-basedな検索にする際、テキスト情報と付加情報(位置情報やユーザー情報)をうまく組み合わせた。 技術や手法のキモはどこ? テキスト情報と負荷情報を組み合わせたEmbedding-basedな検索システムを開発した。 損失関数にTriplet lossを使用しており、その際のHard Negative Samplingについても検索という観点から考察した。 どうやって有効だと検証した? 実際にオンラインでA/Bテストを行い、有効であることを検証した。 [KDD 2020] Controllable Multi-Interest Framework for Recommendation どんなもの? ユーザーの商品のクリックのシーケンス情報から興味情報に応じた推薦結果を出力する。 その結果の多様性をコントロール可能なフレームワークを提案した。 先行研究と比べてどこがすごい? 複数の関心を抽出できる研究は存在していたが、それと同時に出力結果の多様性をコントロールできるようにした。 技術や手法のキモはどこ? 下図のように複数の興味抽出の機構と、得られた興味のベクトルに似たアイテムを多様性を考慮して選択する機構を構築した。 どうやって有効だと検証した? Amazon BooksやTaobaoの商品データを用いて、既存手法に比べてRecall/nDCG/HitRateが改善されていることを示した。 [KDD 2020] On Sampled Metrics for Item Recommendation どんなもの? Recall/Precision/nDCGといった評価指標をサンプリングで計算すると推定値にバイアスが乗る可能性を指摘した。 また、そのバイアスを補正する方法を提案した。 先行研究と比べてどこがすごい? 多くのレコメンドアルゴリズムの論文が採用している評価指標の危険性を指摘した。 技術や手法のキモはどこ? 複数のデータセットで、サンプリングによって評価指標にバイアスと不安定性が存在することを示した。 どうやって有効だと検証した? レコメンドアルゴリズムの出力結果をサンプリングされた方法で計算し、サンプリングを使用しない手法と比較して強いバイアスが存在することを示した。 バイアス補正の方法も、同様の実験で動作することを示した。 [KDD 2020] Personalized Image Retrieval with Sparse Graph Representation Learning どんなもの? Adobe Stock での画像検索のパーソナライズを改善した。 先行研究と比べてどこがすごい? 画像検索のパーソナライズの精度を、画像間の類似度を用いて疎なユーザーと画像間の関係を補強したグラフ構造に対してのGCNを用いて向上した。 技術や手法のキモはどこ? GCNを用いて画像埋め込みにユーザー行動情報を反映した。 疎なユーザーと画像間の関係を画像の類似度の情報を用いることで補強した。 どうやって有効だと検証した? Adobe Stockデータでクリックされるのポジションの平均値とRecallが改善されていることを示した。 [KDD 2020] Managing Diversity in Airbnb Search どんなもの? AirbnbではDLのランキングモデルとは別に多様性のモデルを開発しており、その取り組みの歴史をまとめた。 先行研究と比べてどこがすごい? Airbnbの多様性の手法について、ヒューリスティックベースの手法からDNNに至るまでの手法をまとめている。 技術や手法のキモはどこ? 多様性の評価尺度を、Mean Listing RelevanceやLocation Diversityなど様々な独自指標で定義して実験している。 モデルはTwo Towerで、かつ各Embedding間での距離を遠ざけている。 どうやって有効だと検証した? オフライン評価では、通常の検索の評価指標であるnDCGと多様性の両方が改善するモデルを採用した。 オンライン評価ではnDCGやCV(予約数など)を計測し、多様性改善によって過去数年で最大の改善が実現されていることを示した。 [KDD-workshop 2020] Lessons Learned Addressing Dataset Bias in Model-Based Candidate Generation at Twitter どんなもの? 2段階のレコメンドシステムにおいて、1段階目のCandidate Generation(以下CG)に影響するデータセットのバイアスについて調査した。 また、ランダムランブリングでバイアスを軽減する方法を示した。 先行研究と比べてどこがすごい? 従来のバイアス除去の技術ではCGにおいて効果が薄かった。 本手法ではCGに効果的なバイアス軽減方法を提案した。 技術や手法のキモはどこ? CG後にも2段階目でのランカー学習のために負例対象を残す必要があるため、Implicit Negativeな結果を負例として扱う。 さらに負例のサンプリングを行うことで、明らかな負例を少しだけ残しておく。 結果として、少量の明らかな負例・正例に近い負例・正例から構成された候補集合の作成を見込むことができる。 どうやって有効だと検証した? オフラインでは上記手法を使用してROC-AUCを計測して改善することを確かめた。 オンラインではTwitterでテストを行い、Fine-tuningしたモデルでコンバージョン(お気に入り登録・リツイート数)が改善されていることを示した。 TheWebConf (旧WWW) [WWW 2020] NERO: A Neural Rule Grounding Framework for Label-Efficient Relation Extraction どんなもの? 関係抽出のタスク(下図参照)において、アノテーションされたルールにマッチしない文章のラベリングルールも学習することで精度(F1値)向上を実現した。 先行研究と比べてどこがすごい? 文章単位でのラベルを人手でアノテーションすること無く、ラベル付けのルールを学習できる。 できるだけ少ない労力で精度向上できるフレームワークを提案した。 技術や手法のキモはどこ? 下図(D)のようにハードマッチしない文章に擬似ラベルを割り当てて学習する。 どうやって有効だと検証した? 文章とアノテーションされた関係のデータセットに対してF値で評価したところ、従来手法に比べて改善されていることを示した。 また、10分の1程度のアノテーションの時間で従来と同程度の精度を達成した。 [WWW 2020] The Difference Between a Click and a Cart-Add: Learning Interaction-Specific Embeddings どんなもの? ユーザーへの商品のレコメンドの精度を向上した。 ユーザーがある商品をクリックをした後とカートに商品を追加した後にレコメンドする商品は、それぞれ異なることが望ましいという仮説に則ってモデル化した。 先行研究と比べてどこがすごい? 過去に閲覧などのアクションを起こした商品の情報だけではなく、そのアクション情報も組み合わせた埋め込み表現を使用した。 技術や手法のキモはどこ? アクションとアイテムのペアを系列とみなして学習した。 (l1, click), (l2, click), ...というような系列を学習する(l1、l2はアイテムを表す)。 どうやって有効だと検証した? オフライン評価は Etsy の1年間のログを用いて、過去に購入されたアイテムをどの程度捕捉できているかという指標で比較し、改善されていることを示した。 オンライン評価は実際に7日間A/Bテストを行い、CV率が向上していることを示した。 RecSys [RecSys 2018] Calibrated recommendations どんなもの? オフライン評価で精度に最適化されたモデルが、ユーザーの関心のない分野をレコメンドしてしまう問題がある。 映画のジャンルなどのカテゴリに偏りがあるレコメンドシステムを、キャリブレーションすることで上記問題を解決する新しいリランキング手法を提案した。 先行研究と比べてどこがすごい? ジャンルの多様性を考慮した、レコメンドシステムの出力を後処理するシンプルな手法を提案した。 技術や手法のキモはどこ? アンバランスなレコメンドを補正するための劣モジュラ性を持つキャリブレーションメトリックを提案した。 どうやって有効だと検証した? MovieLensのデータを対象に、レコメンドシステムにジャンルの多様性を補完する仕組みを導入した。 その結果、Recallは減少するが多様性が改善されることを観察した。 [RecSys 2019] A Pareto-Eficient Algorithm for Multiple Objective Optimization in E-Commerce Recommendation どんなもの? トレードオフの関係にある複数の目的関数(例:GMV vs. CTR)のランキング学習に対して、パレート効率的な解を算出するフレームワークを提案した。 先行研究と比べてどこがすごい? 多目的なランキング学習に対して、理論的にパレート効率性の保証のあるアルゴリズムのフレームワークを考案した。 先行研究ではルールベースかヒューリスティックな探索のため、パレート効率性の保証がない。 実際のECサイトのインプレッション・クリック・購入などの情報から構成されるデータを作成して公開した。 技術や手法のキモはどこ? モデルパラメータと重みを交互に最適化することで、理論的に保証のある解が得られる。 どうやって有効だと検証した? 独自に作成したオープンなECのデータ(EC-REC)を用いて、GMVとCTRの2つの目的関数で実験して有効性を検証した。 その他 [WSDM 2010] Anatomy of the Long Tail: Ordinary People with Extraordinary Tastes どんなもの? アイテムを人気度順にソートしたとき、テール部分に属するアイテムの重要性を示した。 ニッチな商品は一部のユーザーしか見ないのではなく、多くのユーザーがニッチな商品を少しずつ調べている、ということを示した。 先行研究と比べてどこがすごい? 先行研究では、ニッチな商品は一部のユーザーにしか見られないという仮定が強かった。 多くのユーザーがトップのアイテムもテールのアイテムも両方欲している、という仮説を立ててこれが正しいことを証明した。 技術や手法のキモはどこ? ユーザー満足度・ユーザーのEccentricity(どれだけニッチな商品を見ているか)を定義した。 テール商品が売上に関連することを示すユーザー行動をモデル化した。 どうやって有効だと検証した? ユーザー満足度・ユーザーのEccentricityを定義し、従来の仮説だとNetflixやYahoo! Searchの実データの結果に合わないことを示した。 テールの商品を補完することで、間接的に売上が向上することを理論的に検証した。 [VLDB 2013] Supporting Keyword Search in Product Database: A Probabilistic Approach どんなもの? 「安価なゲーム用PC」のようなキーワード検索を用いて、構造化された製品情報に対する検索を最適化する方法を研究した。 先行研究と比べてどこがすごい? 「安価」といった曖昧なユーザーの嗜好にマッチする製品のランキングを作るためのブラックボックスでない確率モデルを初めて提案した。 確率モデルを用いたキーワード検索以外のアプリケーションも提案した。 技術や手法のキモはどこ? ユーザーの検索クエリと商品のスペックのギャップを解決する新たな確率モデルを提案した。 また、そのモデルをユーザーレビューや過去の行動ログを使って改善した。 どうやって有効だと検証した? Best BuyとWalmartの商品データとレビューと検索のログデータを用いて、検索結果のnDCGを評価して従来手法よりも改善されていることを示した。 [ACL-short 2018] ‘Lighter’ Can Still Be Dark: Modeling Comparative Color Descriptions どんなもの? 色の比較級(darkerなど)は元の色が無いとRGBで表すことができないが、基準の色と比較級のみを与えることでRGB空間での方向を表した。 先行研究と比べてどこがすごい? 基準となる色をベースとして比較級をRGBのベクトルで表現した。 直接的な色の比較級の情報が必要なく、基準の色とその比較級があれば良い。 技術や手法のキモはどこ? 元の色と比較級を与えると、元の色を起点にRGB中で色の変化を表すベクトルを出力する。 どうやって有効だと検証した? 色の変化を 色差 とコサイン類似度を用いて評価した。 [ECIR 2020] From MAXSCORE to Block-Max Wand: The Story of How Lucene Significantly Improved Query Evaluation Performance どんなもの? Block-Max WAND がLucene 8に実装されるまでの道のりと性能測定をまとめた。 先行研究と比べてどこがすごい? アカデミックなアルゴリズムを利用者が既に存在するライブラリに実装した。 技術や手法のキモはどこ? Luceneのインデックスのフォーマットやスコア計算処理を既存のインデックスに影響が出ないように変更した。 どうやって有効だと検証した? ClueWebのデータセットを用いて計算時間が改善することを確認した。 [MLSys 2020] Understanding the Downstream Instability of Word Embeddings どんなもの? モデルをどの程度の頻度で更新するのが良いかをモデルの学習の不安定さから解釈しようとした。 単語埋め込みの安定性とメモリのトレードオフを示し、不安定性のための新たな尺度を提案した。 先行研究と比べてどこがすごい? 上流のモデルの固有値の不安定性(EIS)を測ることで、下流のモデルの不安定性にも強い相関があることを示した。 また、安定性とメモリのトレードオフはナレッジグラフの埋め込みにも適用できることを示した。 (上記画像は slide より引用) 技術や手法のキモはどこ? 下流のモデルの不安定性にも強い相関がある上流のモデルの固有値の不安定性(EIS)を提案した。 また、メモリのパラメータが下流モデルの不安定性に影響を受けることを示した。 どうやって有効だと検証した? メモリのパラメータを変更することで不安定性を改善することを示した。 ナレッジグラフの埋め込みについても同様に改善することを示した。 [COMPUTER GRAPHICS Forum 2020] Interactive Optimization of Generative Image Modelling usingSequential Subspace Search and Content-based Guidance どんなもの? 学習済みのGANとインタラクティブな入力を用いて目的の画像を生成するシステムを開発した。 本システムでは以下のようなツールなど(画像編集ツールなども可能)を用いて、ユーザーは自由に重みを選択できる。 その重みに応じた画像を表現できる。 先行研究と比べてどこがすごい? 既存のインタラクティブなシステムでは、独自のアーキテクチャや追加のデータが必要だった。 本システムは任意のモデルに追加のデータやアーキテクチャ無しで自分の意図を表現できる。 技術や手法のキモはどこ? ユーザーの入力に基づいたベクトルをサンプリングして、高次元空間を効率よく逐次最適化できる。 どうやって有効だと検証した? 本手法を様々な生成画像モデリングアプリケーションで実験し、既存手法のiGANよりも優れた性能を示した。 まとめ 論文読み会では多くの方に集まっていただき、多くの有益な情報を共有できました。 最後に、本記事でご紹介した論文のアブストラクトのWord Cloudを作成してみました。 「search」「recommendation」以外にも「user」や「model」という文字も大きく表示されていますね。 本記事では、ZOZO研究所が検索や推薦技術で取り組んでいるアカデミックな調査の概要をお伝えしました。本記事を通して、ご興味のある論文を見つけた際には是非とも原著論文を読むことをお勧めします。 おわりに ZOZO研究所では検索エンジニア・MLエンジニア・サーバサイドエンジニアのメンバーを募集しております。今回紹介した検索/推薦技術に興味ある方はもちろん、幅広い分野で一緒に研究や開発を進めていけるメンバーも募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co hrmos.co https://hrmos.co/pages/zozo/jobs/0000029 hrmos.co
はじめに こんにちは。メディアプラットフォーム本部 WEAR部 WEAR-SREの長尾です。 WEAR は2013年にリリースされ、現在8年目のサービスです。そして、2004年にリリースされた当時のZOZOTOWNと同じアーキテクチャを採用しているため、比較的古いシステム構成で稼働しています。本記事では、そのWEARのWebアプリケーション刷新とクラウド移行で実践している、Fastlyを活用したパスベースルーティングによる段階移行の取り組みを紹介します。 WEARをリプレイスする理由 WEARのWebアプリケーションは、データセンターでオンプレミス(以下、オンプレ)上で稼働しています。また、DBはSQL Serverを利用しています。長年このアーキテクチャで成長を続けてきましたが、今後さらに成長を加速させていくためには以下の3点を実現する必要があります。その実現に向け、2年前からリプレイスに着手しています。 開発スピードの加速化 コストの削減 人材の増強 経緯に関する考え方はZOZOTOWNのシステムと同様なため、以下のスライドもご覧ください。 speakerdeck.com リプレイス後のシステムは、クラウドはAWSを採用し、ALB、ECS、Fargate、Railsアプリケーションをベースにした構成です。 Fargate x Railsアプリケーションの詳細は、 id:takanamito さんの記事で紹介されているので、合わせてご覧ください。 techblog.zozo.com リプレイスにおける課題と解決策 WEARは日々新しい機能追加や機能改善をしているサービスです。そのため、新機能を取り込みつつ、全く新しいアプリケーションを作って切り替える、ビックバンアプローチは難しいと考えました。 そこで、パス単位でオンプレ環境とAWS環境に適宜ルーティングする機能(パスベースのルーティング)を用意し、パス単位でAWS側に機能を作成して徐々に切り替えていくアプローチをとることにしました。 パスベースのルーティングを実現する方法として、CloudFrontとFastlyを比較しました。その結果、これから紹介する2つの理由により、Fastlyのほうが導入の難易度が低く、保守性も高いと判断し、Fastlyを採用しました。 1. DNSの移行が不要 WEARのホストはZone Apexである「wear.jp」を利用しています。wear.jpのDNSはAWS外で管理されており、CloudFrontを利用するためには事前にDNSをRoute 53に移設する必要があります。一方で、FastlyはAnycast Addressが払い出され、それをAレコードに利用できるため、DNSを移行しなくてもZone Apexの設定が可能です。 詳しくは 公式ドキュメント をご参照ください。 2. 設定反映が速い Fastlyは設定の反映が高速で、ロールバックが必要になったとしてもすばやく対応できます。WebのトラフィックをすべてFastlyで受けるため、トラブルが発生した際の影響を最小限にするためにも、最短でロールバックできることは大きなメリットだと考えました。 サービス 設定反映までの時間 参考リンク Fastly 5秒程度 Fastly network map | Fastly CloudFront 5分程度 AWS Blog Fastlyを利用したパスベースのルーティング設定 まずはじめに、Fastlyについて少し触れておきます。一般的なFastlyのイメージはCDN(Content Delivery Network)だと思いますが、VCLを使った柔軟な設定ができるという特徴があります。下図のように「あらかじめ定義したバックエンドに対し、条件に合わせてルーティングを設定する」という用途にも適しています。 パスベースのルーティング設定例 以下のサンプルは、Edge Dictionariesとtable.contains関数を利用したパスベースのルーティング設定例です。 詳細については公式のドキュメントをご確認ください。 docs.fastly.com まずバックエンド(オリジンサーバ)を作成します。 backend backend1 { .between_bytes_timeout = 10s; .connect_timeout = 1s; .dynamic = true; .first_byte_timeout = 15s; .host = "backend1のHost名"; .max_connections = 600; .port = "443"; .share_key = "xxxxxxxxxxxxxx"; .ssl = true; .ssl_cert_hostname = "Fastly側のHost名"; .ssl_check_cert = always; .ssl_sni_hostname = "Fastly側のHost名"; .probe = { .expected_response = 200; .initial = 3; .interval = 5s; .request = "HEAD /healthcheck HTTP/1.1" "Host: xxxxx.jp" "Connection: close" "User-Agent: Varnish/fastly (healthcheck)"; .threshold = 3; .timeout = 32.767s; .window = 5; } } backend backend2 { .between_bytes_timeout = 10s; .connect_timeout = 1s; .first_byte_timeout = 15s; .host = "backend2のHost名"; .max_connections = 600; .port = "80"; .share_key = "xxxxxxxxxxxxxx"; .probe = { .expected_response = 200; .initial = 3; .interval = 5s; .request = "HEAD /healthcheck HTTP/1.1" "Host: xxxxx.jp" "Connection: close" "User-Agent: Varnish/fastly (healthcheck)"; .threshold = 3; .timeout = 32.767s; .window = 5; } }∂ 次に Edge Dictionaries を使用して、パスを登録していきます。 table path_routing_dict { "/path1": "aws", } table.contains関数 を利用して、Edge Dictionariesに登録されているパスとURLパスが一致するかを判定します。 if (table.contains(path_routing_dict,req.url.path)) { set req.backend = backend1; } else { set req.backend = backend2; } Edge Dictionariesを使った方法は、シンプルな記述ができるというメリットがありますが、パスが完全一致する場合にしか使かえないという制約がありました。そのため、「/path1/以下はすべてbackend1にルーティングする」という場合は、前方一致のif文を記述しています。 if (req.url.path ~ "^/path1/") { set req.backend = backend1; } else { set req.backend = backend2; } 導入時に発生した課題と解決策 次に、Fastlyをクライアント(ブラウザなど)とバックエンドの間に挟むことで発生した課題とその解決策をご紹介します。 発生した課題 弊社がオンプレ環境で利用しているロードバランサは、歴史的経緯でパーシステンス機能を有効にしており、以下のようなロジックで動いています。 これにより、同一クライアントのリクエストは同一アプリケーションサーバに転送する仕組みを実現しています。 パスベースのルーティング構成ではクライアントとロードバランサの間にFastlyが挟まっています。そのため、ロードバランサは下図のように、Fastly EdgeとのTCPコネクション(Keep-Alive)からサーバ割り当てを判定します。 クライアントは毎回同じFastly Edgeを経由する保証が無いため、リクエスト毎に違うアプリケーションサーバを割り当てられてしまうという事象が発生しました。これにより、既存システムの仕様により一部の機能が正常に動作しなくなることが判明しました。 解決策 解決方法は2つ考えられました。 ロードバランサの判定ロジックを変更する FastlyでKeep-Aliveを無効化する 検討の結果、ロードバランサで対応する場合は影響範囲の確認と動作検証に時間を要すると考え、FastlyでKeep-Aliveを無効化する方針を採用することにしました。 【設定例1】コネクションをクローズさせる設定 sub vcl_miss { # 省略 set bereq.http.connection = "close"; } sub vcl_pass { # 省略 set bereq.http.connection = "close"; } sub vcl_pipe { # 省略 set bereq.http.connection = "close"; } Keep-Aliveを無効化すると、リクエストは下図のように転送されるため、発生していた課題を解決できました。 判定ロジックにより、リクエストがすべて下図の赤枠部分に該当するようになります。 【設定例2】1Edge当たりのコネクション最大数(max_connections)の設定 前述の設定により課題は解決したのですが、新しい懸念も発生しました。この構成では1リクエストごとに新規セッションを確立するため、コネクション数が枯渇するリスクがあります。また、TCPコネクションを確立するためのオーバーヘッドがレスポンスタイムに影響します。この点は、システムの計測とチューニングを行った結果、現状問題は発生していません。 backend backend1 { #省略 .max_connections = 1000; } 今回紹介した課題と解決策はほんの一例です。既存環境に新しいリソースを組み込むことから、想定外の挙動は発生するものだと考え、柔軟に対応していく必要があります。そのため入念な検証を実施することをお勧めします。 Fastlyの運用 実際に、どのようにFastlyを運用しているのかを簡単に紹介します。 リリースの仕組み Fastlyの設定はすべてTerraformで作成しており、そのコードはGitHubで管理しています。CI/CDはCircleCIを利用しています。mainブランチへマージしたタイミングでステージング環境に反映し、リリースタグを切ったタイミングで本番環境へ反映するように自動化しています。CI/CDに関する詳細な説明は、後日別の記事で紹介したいと思います。 監視の仕組み 監視は、FastlyのメトリクスとログをDatadogに取り込んで実現しています。 コストの観点から、Datadog Logsのログ保存期間は2週間にしており、それ以前のものはS3へ転送することで長期保存も実現しています。過去ログの検索はS3のログをAthenaで検索する運用にしています。 次に、監視している項目を一部ご紹介します。 503エラー 503エラーをレスポンスのメッセージごとにカウントし、それぞれ対策を実施していきます。 503エラーの原因については、公式のドキュメントに詳しく記載されていますのでご参照ください。 docs.fastly.com 同一IPからのリクエスト数 同一IPから大量にリクエストが来ていないかを監視しています。一定の閾値を超えたIPに対し、攻撃かクローラーアクセスかを判定し、必要なものはブロックする運用をしています。 まとめ オンプレ環境とAWS環境を共存させながら、パス単位で徐々にクラウド移行していくアプローチについて、Fastlyを利用した事例をご紹介しました。今後はFastlyを利用したサイト高速化へのアプローチや、DoS防御などのセキュリティ向上への取り組みに関しても別の記事でご紹介できればと思います。 さいごに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
はじめに SRE部 ECプラットフォームSREチームの小林 ( @akitok_ ) です。 ZOZOTOWNでは、マイクロサービス間通信におけるトラフィック制御のために、 Istio によるサービスメッシュを導入しています。本記事ではZOZOTOWNのマイクロサービスプラットフォーム基盤(以下、プラットフォーム基盤)において、Istioをいかにプロダクションレディな状態で本番に投入していったか、その取り組みを紹介します。 なお、Istioによるサービスメッシュを導入した背景については、以下の記事で紹介しています。 techblog.zozo.com はじめに What is Istio? Istioをプロダクションレディにするまでに直面した3つの課題 どのようにリソース消費量を見積もるか Data Planeサイジング Envoyプロキシのチューニング 負荷試験 Istioベンチマーク試験 サービス単体負荷試験 サービス結合負荷試験 Control Planeサイジング 何を監視するか メトリクス監視 Data Planeメトリクス Control Planeメトリクス 分散トレーシング どのように可観測性を向上させるか まとめ 終わりに What is Istio? Istioは、マイクロサービスの複雑性を解決する一手段である「サービスメッシュ」を実現するためのフレームワークです。サービスメッシュは、マイクロサービスの実装においてビジネスロジックに集中できることを目指して生まれた手法です。 具体的にはサービス間の通信制御をサービスごとに実装させるのではなく、すべてプロキシ経由の通信とし、ルーティングや認証などのプロキシ設定を全体に伝搬させます。プロキシ経由でサービス間に網状の構成を取ることから、サービスメッシュと呼ばれています。 Istioは以下の特徴を持ちます。 Google、IBM、Lyftの3社共同開発により、2017年5月にOSS化されたサービスメッシュフレームワーク KubernetesのPodにプロキシ(Envoy)をサイドカーコンテナとして注入させることで、サービスのコード変更を伴わずにサービスメッシュの実現が可能 アーキテクチャは以下の通りです。 Istioのアーキテクチャは、Data PlaneとControl Planeに分割して考えることができ、それぞれ以下の特徴を持ちます。 Data Plane サイドカーとして注入されるEnvoyプロキシ(正確にはEnvoyプロキシの拡張)のコンテナから構成される このプロキシがマイクロサービス間の通信を仲介・制御する Control Plane Envoyプロキシコンテナのサービスへの注入や設定伝搬を司る Istioをプロダクションレディにするまでに直面した3つの課題 ZOZOTOWNでは、これからも持続的に成長を続けていくことを目的とし、現在レガシーシステムのリプレイスを進めています。ZOZOTOWNのリプレイス戦略については、以下のスライドをご覧ください。 speakerdeck.com その一環で、モノリシックアーキテクチャから、マイクロサービスアーキテクチャへの移行も行われています。そして、マイクロサービス化が進むにつれ、プラットフォーム基盤上で稼働する各サービス間通信に複雑性が生まれていました。 そこで、この課題を解決していくために、昨年度末にIstioの導入を推進しました。その際に、Istioをプロダクションレディな状態で導入していくために、以下3つの大きな課題に直面しました。 どのようにリソース消費量を見積もるか 何を監視するか どのように可観測性を向上させるか 本記事では、それぞれどのように検討・対処を進めていったかをご紹介します。 どのようにリソース消費量を見積もるか Istioのリソース消費量を見積もり、適切なキャパシティプランニングを行う必要があります。そのためには、アーキテクチャに基づき、Data PlaneとControl Planeをそれぞれ分けて考慮する必要があります。 Data Planeサイジング Istioの 公式ドキュメント によれば、Data Plane(Envoy)のパフォーマンスについて、以下のようにレポートされています。 Envoyプロキシは、プロキシを通過するリクエストにおいて、1000リクエスト/秒あたり0.35vCPUと40MBメモリを使用する Envoyプロキシは、90パーセンタイルで、レイテンシに2.65ミリ秒を追加する 上記の数値は以下の前提で行われた負荷テストによる結果です。 Istio 1.10を使用する サービスメッシュが1000個のサービスと2000個のEnvoyプロキシ(サイドカーコンテナ)で構成される サービスメッシュ全体で1秒あたり70000回のリクエストがある 実際にはData Planeのパフォーマンスは、以下にあるような要素にも依存し、変動します。 クライアント接続数 目標リクエストレート リクエストサイズとレスポンスサイズ プロキシワーカースレッド数 プロトコル CPUコア数 これらの要因により、レイテンシやスループット、EnvoyプロキシのCPUやメモリのリソース消費量は変化します。そのため、Istioの公式ドキュメントを参考にしながらも、実際に負荷試験を行い、実環境で計測することが非常に重要です。 Envoyプロキシのチューニング 負荷試験の説明を進める前に、まずData Planeのチューニングポイントである、Envoyプロキシのチューニングについて説明します。 Envoyプロキシの resource 設定は、Envoyプロキシを注入するリソースに対し spec.template.metadata.annotations の指定を追加することで、チューニング可能です。 resource設定に関するannotationは以下の通りです。 annotation 説明 sidecar.istio.io/proxyCPU EnvoyプロキシのCPU Requestを指定する sidecar.istio.io/proxyCPULimit EnvoyプロキシのCPU Limitを指定する sidecar.istio.io/proxyMemory EnvoyプロキシのMemory Requestを指定する sidecar.istio.io/proxyMemoryLimit EnvoyプロキシのMemory Limitを指定する 以下の例は、Deploymentリソースに注入するEnvoyプロキシのCPU Limitを500m、Memory Limitを512Miに指定する例です。 apiVersion : apps/v1 kind : Deployment metadata : name : test-api spec : template : metadata : annotations : sidecar.istio.io/proxyCPULimit : 500m sidecar.istio.io/proxyMemoryLimit : 512Mi その他のannotationでの設定は、 公式リファレンス をご参照ください。 本記事で説明する負荷試験では、試験結果を見ながら、このannotationによるチューニングを繰り返し行いました。 負荷試験 プラットフォーム基盤では、以下の3つの負荷試験フェーズに分け、パフォーマンス測定を行い、チューニング精度を上げていくようにしました。 Istioベンチマーク試験 サービス単体負荷試験 サービス結合負荷試験 また、プラットフォーム基盤でのIstioの導入は、BFF(Backends For Frontends)を実現するZOZO Aggregation APIがファーストターゲットとなりました。以下に示す負荷試験イメージは、このAPIの負荷試験を対象として記しています。 ZOZOTOWNのBFFへの取り組みについては、以下の記事をご参照ください。 techblog.zozo.com Istioベンチマーク試験 Istioベンチマーク負荷試験は、以下の構成で実施しました。 この構成では、実際のマイクロサービスをData Planeに置くのではなく、 Fortio という負荷試験クライアントのPodにEnvoyプロキシを注入し、Data Planeに組み込んでいます。FortioがEnvoyプロキシ経由でコールするバックエンドサービスは、 httpbin というモックを水平スケールさせた状態で稼働させています。この状態でクライアントからcurlコマンドでHTTPリクエストを実行し、Fortio経由でEnvoyプロキシに負荷をかけ、検証しました。 この試験は、各マイクロサービスに注入するEnvoyプロキシの初期リソース(CPU、Memory)サイジングに役立ちました。また、マイクロサービスのリソースサイジングだけでなく、Istioのバージョンアップにおけるパフォーマンスの変化を確認できる環境としても役立っています。 サービス単体負荷試験 サービス単体負荷試験は、以下の構成で実施しました。 この構成では、試験対象であるマイクロサービスのPodにEnvoyプロキシコンテナを注入し、連携先である他サービスは、 Nginx を用いて静的コンテンツを返すWebサービスモックを用意しました。さらに、負荷試験はIstioベンチマーク試験とは異なり、本番環境へのリクエストを想定したテストシナリオを作成し、 Gatling を負荷試験クライアントとして活用しています。 複雑なサービスメッシュ構成において一気にすべてのサービスを接続し、想定したパフォーマンスが出ない、あるいはエラーが頻発するというような事象が発生した場合、問題切り分けが非常に困難になります。被疑箇所は、以下のように分割して考える必要があります。 接続元サービス 接続元サービスのEnvoyプロキシ 接続先サービスのEnvoyプロキシ 接続先サービス そこでサービス単体試験環境を用意し、接続元サービスと接続元サービスのEnvoyプロキシのチューニングを完了させた上で、実際のマイクロサービスを連携させた負荷試験のフェーズに進むことが重要と考えました。 サービス結合負荷試験 サービス結合負荷試験は、以下の構成で実施しました。 この構成では、連携する他サービスも含め、本番環境と同等の環境を用意しています。 この試験結果が期待通りでない場合は、単体負荷試験と比較しながら切り分けを行うことで、マイクロサービス間での課題整理をスムーズに進めることができました。 Control Planeサイジング Control Planeを構成するIstiodコンポーネントのパフォーマンスは、以下の要素に依存し、変動します。 Deploymentの変更頻度 Configurationの変更頻度 Istiodに接続するEnvoyプロキシ数 またIstiodは水平にスケール可能なので、CPU使用率などをトリガーとしてKubernetesの HPA(Horizontal Pod Autoscaler) 設定で、オートスケールさせると良いです。 なお、本記事ではIstioの構築には深く触れていませんが、プラットフォーム基盤では Istio Operator を活用した構築をしています。Istio OperatorによりHPAの設定は自動生成され、IstiodのCPU使用率が80%に到達したら、オートスケールするようにしています。 何を監視するか プラットフォーム基盤における運用監視には、 Amazon CloudWatch と Datadog を採用しています。特に今回、既にDatadogで取得している各サービスの監視対象メトリクスなども合わせてダッシュボード化していくことも考慮し、Istioに関するメトリクスもDatadogで取得する方針としました。 DatadogにおけるIstioインテグレーションについては、 公式ドキュメント をご参照ください。 メトリクス監視 監視対象のメトリクスについても、Data PlaneとControl Planeに分けて考慮しました。 Data Planeメトリクス プラットフォーム基盤上に稼働している各マイクロサービスは、Datadog APMを活用し、マイクロサービス単位でのメトリクス収集・監視は十分に実施できている状況でした。そのため、Data Planeの監視は、個々のマイクロサービスに着目するのではなく、Data Plane全体のエラーレートを監視するのが良いと考えました。 そこで、以下の2つのメトリクスを用いて、 エラー数 / リクエスト数 = エラーレート で算出した値を監視することにしました。 メトリクス 説明 trace.envoy.proxy.hits Envoyプロキシが受け付けたリクエスト数 trace.envoy.proxy.errors Envoyプロキシが受け付けたリクエストのエラー数 Control Planeメトリクス 前述の通り、プラットフォーム基盤ではIstio Operatorを用いた構築をしています。これによりControl Planeは、Istio Operatorのマニフェストファイルに基づき、自動運用されます。 例えば、Control PlaneのPod障害などがあった場合には自動で再起動され、Podのリソース消費が大きい場合にはオートスケールされるなど、回復性および拡張性をもった構成になっています。そのため、Control Planeの単純なインフラメトリクスの変化ではなく、Control Planeが正しい挙動をしていない以下のような状態を捕捉すべきと考えました。 何らかの原因でEnvoyプロキシの注入に失敗している 何らかの原因でEnvoyプロキシへの設定伝搬に失敗している それぞれ以下のメトリクスを監視することで、捕捉できます。 メトリクス 説明 istio.sidecar_injection.failure_total Envoyプロキシの注入に失敗した回数 istio.galley.validation.failed Envoyプロキシへの設定伝搬に失敗した回数 分散トレーシング プラットフォーム基盤ではIstioサービスメッシュの導入により、マイクロサービス間の通信が透過的にルーティングされ、複雑性が増しています。あるサービスのレイテンシ遅延の原因を調査したい場合に、1つのリクエスト起点で発生する複数のマイクロサービス呼び出しをすべてトレースし、どこで何が起きているのか特定するのは至難の業です。分散トレーシングは、まさにこれらのリクエストを追跡するための技術です。Istio Data Planeの分散トレーシングについては、Istioの公式ドキュメントで、 Zipkin や Jaegar 、 Lightstep を活用した方法が紹介されています。 前述の通りプラットフォーム基盤では、Datadog APMを活用し、各マイクロサービスの分散トレーシング情報を既に収集していました。そこで、Envoyプロキシを通過した通信も同様にDatadog APMを活用し、各マイクロサービスの通信とIstio Data Planeの通信を一気通貫でトレースできるようにしました。 Datadogを活用したIstio Data Planeのトレーシング情報の収集については、 公式リファレンス をご参照ください。 以下は、実際の各マイクロサービスとEnvoyプロキシを通過する通信を含むトレーシング情報の図です。 この図の「えんじ色」の部分が、Envoyプロキシのトレーシングを示しています。他のサービスからの呼び出し関係などを含め、一気通貫したトレースが容易になっています。 どのように可観測性を向上させるか ここまでは、Istioの監視メトリクスについて、いくつか紹介してきました。 一方で、他にも常に監視対象とする必要はないものの、運用状態として可観測性を高く保っておきたいメトリクスもありました。それらはDatadog Dashboardを使い、一箇所に情報を収集・可視化し、Istioサービスメッシュの健康状態の把握を分かりやすくしています。 以下が実際のダッシュボードです。 IstioのDatadog Dashboardの作成にあたっては、 公式ドキュメント と ブログ が非常に参考になりました。 プラットフォーム基盤上のマイクロサービスで、パフォーマンス劣化やエラーなどが観測された際に、特にウォッチしているグラフは以下のものです。 グラフ 説明 Request count by destination 宛先ごとのリクエスト数 Top request destination リクエスト数上位の宛先 Average request latency by destination 宛先ごとの平均レイテンシ Top latency destination レイテンシ上位の宛先 Request count by resource マイクロサービスごとのリクエスト数 Top request resource リクエスト数上位のマイクロサービス Error count by resource マイクロサービスごとのエラー数 Top error resource エラー数上位のマイクロサービス 例えば、サービスメッシュ全体のエラーレートが高騰した際に、特定のマイクロサービスに集中して発生している問題なのか、サービスメッシュ全体での問題なのか切り分ける必要があります。その際には、Top request resouceとTop error resourceのグラフを参考にしています。 具体的には、プラットフォーム基盤全体で発生している問題であれば、Top request resourceとTop error resourceのランキングは相関した動きになるはずです。一方で、特定のサービスに起因する場合は必ずしも相関せず、特定のサービスのみ大量にエラー発生している状態が読み取れるでしょう。 このように特定メトリクスを監視するだけでなく、Dashboardなどを活用した可観測性の向上も継続的に実施することが、サービスメッシュを拡大していく上では非常に重要です。 まとめ 本記事では、Istioサービスメッシュをプロダクションレディな状態で、ZOZOTOWNプラットフォーム基盤に導入してきた取り組みを紹介しました。Istioは今後ますますマイクロサービス全体に利用を拡大し、さらなる可観測性の向上や、サーキットブレーカーなどの高度な機能も取り入れていく予定です。また新たな知見が得られたら、紹介したいと思います。 終わりに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com