TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、9/29に After iOSDC Japan 2020 を開催しました。 zozotech-inc.connpass.com 本イベントは、Sansan、note、ZOZOテクノロジーズの3社による合同イベントです。9月19日〜9月21日に開催されたiOSDC Japan 2020について、各社の社員によるLT、パネルディスカッションを行いました。本イベントには、ZOZOテクノロジーズの技術顧問でもある岸川氏も登壇しました。 登壇内容 まとめ Sansan、note、ZOZOテクノロジーズよりそれぞれ1名ずつ、合計3名がLTで登壇し、ZOZOテクノロジーズ 技術顧問の岸川 克己氏が特別講演を、その後パネルディスカッションも実施されました。 LT1:iOSアプリの起動時間短縮にむけて (株式会社ZOZOテクノロジーズ 元 政燮) LT2:Firebase In-App Messaging で過去バージョンのユーザーへ更新を促したい! (Sansan 中川 泰夫 / @ynakagawa33 ) LT3:note社でのMagic Pod活用事例 (note 植岡 和哉 / @fromkk ) 特別講演: SourceKit-LSPを使ってWebブラウザでSwiftの入力補完を実現する (ZOZOテクノロジーズ 技術顧問 岸川 克己 / @k_katsumi ) パネルディスカッション: 『初のオンライン iOSDC、どうでしたか?』 (モデレーター: note / 森口 友也、パネラー: Sansan / 栗山 徹( @kotetu )・note / 植岡 和哉・ZOZOテクノロジーズ / 西山 博貴) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
アバター
ポリゴンメッシュの紹介 皆様、はじめまして! 計測プラットフォーム部バックエンドチームの村木と申します。 本記事では私達のチームが、お客様の足の形状を分析していく中で得た、様々な知見を紹介していきたいと思います。 まず、足の形を3Dで表現するために私達が採用しましたモデリング手法「ポリゴンメッシュ」について紹介します。 「ポリゴンメッシュ」(以降では単に「メッシュ」と表記します)は、3Dのオブジェクトをその表面を覆う多角形、または三角形の集合として表現する手法です。 上の画像は、足の形状をメッシュで表現した例になります。多数の細かい三角形でオブジェクトの表面が覆われているのが見て取れるかと思います。 trimeshの紹介 3Dオブジェクトを多角形の集合で表現するということについて、私達が使用しているPythonライブラリ、 trimesh を使って詳しく見ていきましょう。 trimeshは、三角形メッシュ(triangle mesh)を読み込み、簡単に操作・分析するためのピュアPythonライブラリです。 「三角形メッシュ」とは、その名の通り一般の多角形ではなく三角形のみでオブジェクトの表面を覆うメッシュの一種を指します。 では、trimeshを使って基本的なメッシュデータを定義してみます。 import trimesh mesh = trimesh.Trimesh( vertices=[[ 0 , 0 , 0 ], [ 0 , 0 , 1 ], [ 0 , 1 , 0 ]], faces=[[ 0 , 1 , 2 ]] ) このコードでは、引数 vertices に3つの頂点の情報を与え、また引数 faces に1つの面すなわち三角形の情報を与えています。1つの faces を構成する [0, 1, 2] という数値は、それぞれ0,1,2番目の vertices から構成される三角形であることを示しています。 このように頂点と面の集合をリストとして与えることで、メッシュを定義することが出来ます。 足の形状の特徴データ 次に、私達が考案しました、以下の1から3の処理で求まる足の形状に関するデータを紹介します。 XY平面に、等間隔で配置した格子点を設ける。 それぞれの格子点から、Z軸方向にのばした直線とメッシュデータとの交点を求める。 YZ平面、ZX平面についても同様の処理でメッシュデータとの交点を求める。 こちらが、1つの平面のみを図示したものです。 この処理で得られた交点の座標の集合は、足の形状の特徴を表すデータとして扱えることが、私達の分析の中で分かってきました。 次の節ではこの処理、すなわち「格子状に配置したベクトルとメッシュとの交点を求める処理」を、trimeshを使って実装していきます。 ベクトルとメッシュの交点計算 以下のコードが、格子状に配置したベクトルとメッシュとの交点を求め、ビジュアライズする処理です。 import trimesh import numpy as np def load_and_create_mesh (): npz_kw = np.load( 'np_vertices_faces.npz' ) vertices = npz_kw[ "vertices" ] faces = npz_kw[ "faces" ] return trimesh.Trimesh(vertices=vertices, faces=faces) def create_ray_origins (): x_coord = np.linspace(- 160 , 160 , num= 160 , endpoint= False ) y_coord = np.linspace( 0 , 80 , num= 40 , endpoint= False ) X, Y = np.meshgrid(x_coord, y_coord, sparse= False , indexing= 'xy' ) return np.dstack([np.zeros(X.shape), X, Y]).reshape([- 1 , 3 ]) def create_ray_directions (size): ray_directions = np.array([[ 1 , 0 , 0 ]] * size) return ray_directions def ray_intersects_location (mesh, ray_origins, ray_directions): locations, index_ray, index_tri = mesh.ray.intersects_location( ray_origins=ray_origins, ray_directions=ray_directions) return locations def create_scene (mesh, ray_origins, ray_directions, locations): # stack rays into line segments for visualization as Path3D ray_visualize = trimesh.load_path(np.hstack(( ray_origins, ray_origins + ray_directions)).reshape(- 1 , 2 , 3 )) # make mesh transparent- ish mesh.visual.face_colors = [ 100 , 100 , 100 , 100 ] # create a visualization scene with rays, hits, and mesh scene = trimesh.Scene([ mesh, ray_visualize, trimesh.points.PointCloud(locations)]) return scene if __name__ == '__main__' : mesh = load_and_create_mesh() ray_origins = create_ray_origins() ray_directions = create_ray_directions(ray_origins.shape[ 0 ]) locations = ray_intersects_location(mesh, ray_origins, ray_directions) scene = create_scene(mesh, ray_origins, ray_directions, locations) scene.show() なお、ビジュアライズの処理は、trimeshのリポジトリに含まれているサンプルコードを参考にしています。 github.com 以下、各処理の説明です。 load_and_create_mesh関数 vertices, facesのロードと、trimeshオブジェクトの生成 create_ray_origins関数 intersects_locationに与える引数ray_originsの生成 等間隔でならんだ格子状の座標を生成している create_ray_directions関数 intersects_locationに与える引数ray_directionsの生成 ray_intersects_location関数 格子状に配置した直線とメッシュとの交点を求める create_scene関数 pygletを使ったメッシュと交点の座標をビジュアライズする 最も注目して頂きたいのは、ray_intersects_location関数で行われている処理です。trimeshオブジェクトのフィールドのrayオブジェクトとそのメソッドである intersects_location を使って、複数のベクトルとメッシュ表面との交点を一括して計算しています。 メソッド intersects_location は、その引数ray_originsでは交点を求めたいベクトルの起点となる座標のリストを与え、引数ray_directionsでは各ベクトルの方向を表す座標を与える仕様であることにも注意して下さい。 さて、メソッド intersects_location の呼び出しによって無事に格子状に配置したベクトルとメッシュ表面との交点を求めることが出来ました。しかし、この処理では格子が細かい場合に処理速度が遅く、求める交点が数十万に及ぶ場合は数十秒に渡る時間がかかってしまうという問題がありました。 embreeを用いた高速化 幸いなことにtrimeshでは、rayオブジェクトを使った処理は、embreeというライブラリを使用することで高速化することが可能でした。 embreeは、Intelによって開発されたCPUベースの高性能レイトレーシングライブラリです。Intelの最新のプロセッサ向けにパフォーマンスが最適化されたライブラリであり、レンダリング処理のパフォーマンスを向上させることが出来ます。 trimeshはこのembreeの組み込みに対応しており、embreeとそのPythonラッパーであるpyembreeをインストールするだけで、ソースコードを修正することなく処理の高速化を行うことが出来ます。 embreeとpyembreeのインストール方法は、 公式のスクリプト が参考になります。 #!/bin/bash set -xe # Fetch the archive from GitHub releases. wget https://github.com/embree/embree/releases/download/v2. 17 . 7 /embree-2. 17 . 7 .x86_64.linux.tar.gz -O /tmp/embree.tar.gz -nv echo " 2c4bdacd8f3c3480991b99e85b8f584975ac181373a75f3e9675bf7efae501fe /tmp/embree.tar.gz " | sha256sum --check tar -xzf /tmp/embree.tar.gz --strip-components=1 -C /usr/ local # remove archive rm -rf /tmp/embree.tar.gz # Install python bindings for embree (and upstream requirements). pip install --no-cache-dir numpy cython pip install --no-cache-dir https://github.com/scopatz/pyembree/releases/download/ 0 . 1 . 6 /pyembree-0. 1 . 6 .tar.gz 上記のスクリプトの手順でDockerコンテナにembreeとpyembreeを組み込み、手元の環境(MacBook Pro / Intel Core i7 3.5 GHz)でembree組み込み前後の速度を、計算対象の交点数を変更して比較しました。 交点の数 embree組み込み前 embree組み込み後 8,000 0.57秒 0.01秒 64,000 4.70秒 0.06秒 168,000 12.41秒 0.18秒 320,000 25.66秒 0.33秒 embreeの組み込み後は、処理速度が数十倍高速になることが確かめられました。 まとめ 本記事では、ライブラリtrimeshでメッシュを扱う基本的な方法と複数のベクトルとメッシュ表面との交点を一括して計算する方法、さらにembreeを使用した高速化について説明しました。 なお、私達計測プラットフォーム部バックエンドチームでは、ZOZOMATでより精度の高いサイズを推奨するバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募下さい! www.wantedly.com
アバター
はじめに MSP技術推進部の基幹化推進チームの中嶋です。 私達のチームでは、 マルチサイズプラットフォーム事業(MSP) におけるデジタルトランスフォーメーション(DX)の取り組みを行っています。その取り組みの1つにAndroidを使って、検品結果を記録するアプリの開発・導入があります。 実はこの施策は約2週間で開発されたものです。今回のブログではどうやって短期間でリリースできたのかを紹介します。 開発の背景 検品検寸アプリ誕生のきっかけは、製品の販売前の検品にかかる時間を効率化したいという声からでした。 MSPの全ての製品は、工場が行う検品とは別に弊社が検品会社と協力して検品を行っています。 検品後速やかに弊社側で検品結果をまとめ、製造を担当している商社・工場へレポートする必要がありました。しかし、この工程では作業時間の多くをレポートを作成する業務が占めており、作業効率が大変悪い状態でした。 MSP事業の生産管理・品質管理の担当と話して、この問題はAndroidアプリを使って解決できると分かり、開発がスタートしました。 アプリ開発のアプローチ方法 どのようなアプローチで短納期の開発が実現できたのかを説明します。 システム このプロジェクトは開発者の人数が限られており、開発に使える時間が短いものでした。それに加えAndroidアプリ開発の比重が大きく、サーバーサイドの開発に工数を掛けられないため開発ボリュームを抑えたいと考えていました。そこでアプリのバックエンドとしてFirebaseを採用することにしました。 下図は開発時に目指した、Androidアプリとシステムが連携した構成です。 Firebaseを選んだ理由 以下の特徴があるため、Firebaseを選択しました。 導入のしやすさ モバイルデバイス(Android・iPhone)と連携することを前提としたサービスプラットフォームである点 ドキュメントが充実している点 会社がG Suiteを導入していることにより、社員アカウントで開発を始めらる点 設計・実装時間の圧縮 データベースとストレージサービスの両方が利用可能である点 Cloud Firestore:NoSQLデータベースで検品・検寸データを保存 Cloud Storage:不良画像の保存とアプリ更新配信のために最新APKを格納 スキーマレスDBでデータ保存できるという点 サービス立ち上げ時に好都合だった SDKが充実しており実装コストが少ない点 社内の導入実績 ZOZOTOWN Androidチームによる導入のサポートが得られる点 ZOZOTOWN Androidチームの利用実績がある点 参考:Firebase プロダクト Cloud Firestoreについて データ作成 Firebaseで新規プロジェクトを作成すると、利用したいプロダクトが表示されます。 そこで、データベースはCloud Firestoreを選択します。Cloud Firestoreのコレクションは、ボタンを数回押せばデータベースができあがります。 参考:Cloud Firestore スキーマレスなデータベースのためデータ構成を設計・調整しながら、クライアントとなるアプリケーション開発も別軸で進められます。限られた期間内で開発するような場合に、この柔軟さは工数の短縮に繋がります。 簡単な例ですが、 user というデータを考えます。 object user { name = "xxxx" age = 37 address = "xxxxxxxxxxxxx" telephone = "03-123-9876" } 格納するデータを設定する画面です。 データタイプも豊富です。オブジェクトの属性は、mapタイプで設定していきます。 セキュリティ データリソース毎にアクセス制御する仕組みがあり、各データに対するルールを設定することで柔軟なセキュリティルールを指定できるようになっています。 アクセス設定 権限種類 read 読取 write 書込 create 作成 update 更新 delete 削除 参考:基本的なセキュリティ ルール 下の例はCloud Firestoreのリソースへのアクセス制御の「ルール記述」のサンプルです。 service cloud.firestore { match /databases/ { database } /documents { // リソースへのアクセス制限をパスで指定する match /コレクション名/リソース名 { // リソースへの読み書きを条件付き許可する allow read, write: if <条件>; } } // 検品検寸アプリの権限設定は、特定のコレクションに対してはフルアクセスとしている match /databases/ { database } /documents { // *(アスタリスク)はワイルドカードでMatchさせるために指定 // Inspectionコレクションに対するセキュリティの設定例 match /Inspection/ { Inspection=** } { // リソースへの読み書きを常に許可する allow read, write: if true ; } } } セキュリティに関しても簡易にかつきめ細かく設定できるという点は安心ができました。 参考:セキュリティの記述方法 Androidからのアクセス実装方法 SDKをimportして実装します。 コールバックは非同期で受け取れるため、アプリケーション側はそれを考慮した実装にする必要があります。 Kotlinでの実装サンプルを下に示します。 データ登録 // Firestoreインスタンス val db = FirebaseFirestore.getInstance() // 設定 val settings = FirebaseFirestoreSettings.Builder() .setPersistenceEnabled( true ) .build() db.firestoreSettings = settings // 登録データはmapで設定 val user = hashMapOf( "name" to "User name" , "age" to 29 , "address" to "xxxxxxxxxxxxx" "telephone" to "090-123-4567" ) // 登録処理 db.collection( "users" ) // コレクション名 .document( "xxxxxxxxxxxxx" ) // リソースパス . set (user) .addOnSuccessListener { documentReference -> // 成功時のコールバック Log.d(TAG, "DocumentSnapshot added with ID: ${ documentReference.id } " ) } .addOnFailureListener { e -> // 失敗時のコールバック Log.w(TAG, "Error adding document" , e) } データ読み取り db.collection( "users" ) // コレクション名 . get () .addOnSuccessListener { result -> // 成功時のコールバック for (document in result) { Log.d(TAG, " ${ document.id } => ${ document. data } " ) } } .addOnFailureListener { exception -> // 失敗時のコールバック Log.w(TAG, "Error getting documents." , exception) } ドキュメントやサンプルなども揃っており、記述をしながらも躓くことなく直感的に実装できました。この実装のハードルの低さも工数削減につながりました。 Android開発 MSP技術推進部のメンバーはAndroid開発経験がなかったため、ZOZOTOWN Androidチームに開発のサポートを依頼しました。うまくコラボレーションするために以下の観点で担当分けをし、それぞれ実装を分担しています。 ビュー周りはAndroid独自の概念が多いためAndroidチーム ロジック周りは事業周りの知識が必要になるためMSP技術推進部メンバー アプリケーション構成 下図のような構成を考えてもらいました。特徴は ViewModel と Activity は「1対1」です。 MainActivity にぶら下がる Fragment がアプリケーションの表示の中心になります。 データ取得から表示までのイメージ UIなどイベント発行を Fragment で検知し、 ViewModel にアクションを移譲 ViewModel は要求されたアクションを実行しFirestoreにデータをリクエスト ViewModel がレスポンスを受け取る ViewModel が Fragment にデータへ渡す Fragment がデータを表示 Fragment の実装例を紹介します。 各Fragmentは下のようにViewModelの参照を持ちます。 vm = ViewModelProvider(requireActivity()). get (SharedViewModel:: class .java) userListButtonボタンのクリックイベント設定します。ViewModelのgetUserList(ユーザーリストの取得)メソッドを呼び出して非同期処理を設定しています。 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { userListButton.setOnClickListener { vm.getUserList().addOnSuccessListener { result -> // 成功処理 } .addOnFailureListener { exception -> // エラー処理 } } } 次に、 ViewModel の実装例です。 FirestoreのTaskオブジェクトを返し、Viewにコールバック処理を移譲します。結果は非同期のため、成否処理を表示側でコントロールしてもらいます。 fun getUserList() Task<QuerySnapshot> { return db.collection( "users" ). get () } 表示は主に Fragment の実装、ロジックは ViewModel とそれと連携する Model などを中心に実装しました。 それぞれの実装だけに注力できたため大きな混乱もなく開発ができました。 画面遷移 アプリの各画面表示はFragmentが入れ替わっているだけです。 BLEメジャーと通信してアプリに結果表示する BLEメジャーで計測したその結果をアプリの画面に表示する実装を紹介します。 BLEメジャーとAndroidアプリ 下図が連携しているイメージです。 デバイス同士のペアリングは予め設定しておきます。ペアリングしていれば、入力待ちになっている(フォーカスがある)テキストフィールドに採寸・測定値が自動的に入ってくる仕様です。 したがって、連携の実装はとてもシンプルで、テキストフィールドのフォーカスの管理とテキストが入力されたときの処理を実装するだけです。 メジャーからは「採寸値 + キー入力値」が入ってくるため、自動でフォーカスが移動します。 // 実装ではテキストフィールドのフォーカスの移動に処理を入れている editText.setOnFocusChangeListener { _, hasFocus -> if (hasFocus.not()) { // フォーカスが外れた時の処理 // // 判定: 採寸値が許容寸の範囲内で仕上がっているかどうかを計算する // 結果を表示に反映する } else { // フォーカスが入ったときの処理は、特に何もせず入力値が入るのを待つ } } 採寸入力デモ ペアリング中のメジャーから採寸値が入力され、検寸結果が即座に判定されます。 配信とアプリのアップデート方法 今回作成したアプリケーションはクローズドなアプリですが、自社以外に配布する必要がありました。さらに日本以外での展開を当初から念頭に入れていました。そのためアプリの配布と更新については、Google Playのストアを経由しない配布とアップデートの仕組みを考慮する必要がありました。 そこで配信に使用したのがFirebaseのCloud Storageです。Androidアプリ側でアプリケーションのバージョンをチェックし、更新の必要な場合はアプリケーションが自動的にダウンロードをするような仕組みを実装しました。 下図がCloud FirestoreとCloud Storageを利用した更新の流れです。 Androidの仕様上アプリ外に一度処理は流れますが、ユーザー操作数を極力少なくできているため、大きな障壁とはなりませんでした。 まとめ 新規Androidアプリとデータベースの組み合わせを2週間という納期でリリースできたポイントを振り返ると以下の通りです。 Firebaseを利用した開発が目指していたアプリサービスの要件と実装の難易度があっていた サーバレス Androidデバイスとの連携 アプリケーション開発の実装ハードルの低さ リリースまでの開発でいくつかの同時進行で開発ができたことで工数を削減できた クライアント側の実装を止めることなくバックエンドのデータ構成を調整しながら開発を進められた Android開発で開発スコープを絞った分業実装(表示レイアウトとロジック実装の開発の分割)ができた チーム内外でのサポートが受けられた ZOZOTOWNチームから人員と実装アイデアなどのサポートをもらえた チームからはデータ構成や資料提供などを優先的にしてもらえた 技術的なポイントとチームとしての連携がうまく絡み合い成し遂げられたリリースでした。 成果 「開発の背景」で説明した検品結果の記録と集計作業をシンプルにするため、下図の構成に変更しました。 検品検寸アプリ導入前は、製品の検品結果は紙とペンを使って記入していました。記入された紙は現場のマネージャーが集計し弊社にメールで送信、その後メールに書かれた結果を弊社で集約し、サマリーレポートとして作成していました。 導入後、検品会社のオペレーターやマネージャーが行っていた結果の記入・転記・集計作業を自動化することができ、作業全体の効率化に貢献できました。 1つ成果として、検品結果の集計作業では以下のような改善効果が得られました。 おわりに 本記事ではMSP技術推進部の取り組みの1つのAndroid検品・検寸アプリの立ち上げについて紹介しました。 ZOZOテクノロジーズでは、ZOZOTOWNやWEARのサービスをはじめ、事業を支えるさまざまな職種を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com ※「QRコード」は、株式会社デンソーウェーブの登録商標です。
アバター
こんにちは。ECプラットフォーム部の廣瀬です。 ZOZOテクノロジーズでは、お客様の氏名や住所をはじめとする秘密情報を保護するための様々な取り組みを行っています。本記事ではその中の一部である、データベース(以下、DB)に保存している秘密情報の取扱いルールについてご紹介したいと思います。なお、今回の内容は特定の製品の機能に関する話ではなく、取り組みの基本的な考え方についての話となっています。 背景 何も対策を講じない場合、開発者がDBの秘密情報を権限的には閲覧することが可能な状態となり得ます。例えば、 select * from [秘密情報が含まれるテーブル] といったSQLを実行することで、秘密情報を閲覧できてしまいます。 意図的に機密性の高い情報にアクセスするようなエンジニアは基本的にはいないでしょう。ただし、秘密情報を利用する意図がなくても、新機能のリリース時のデータ確認などで自然と秘密情報が目についてしまう可能性はあります。例えば、単一テーブルのデータを参照する際に「select *」で情報の確認を行った場合などです。 弊社では、以前から フルリモートワーク制度を導入 していました。また、3月末からは原則としてリモートワーク推奨となっております。弊社だけでなく、社会情勢に応じてリモートワークの機会が増えた会社は少なくないと思います。このような状況下においてリモートワークでDBの情報を参照した際に、秘密情報へのアクセスを制限し、情報の流出リスクを最小限に抑える必要性が高まっていると感じています。 そこで、弊社で運用しているDB秘密情報の取扱いルールをご紹介することで、秘密情報保護の取り組みの参考となる内容をご提供できればと考えました。まずは、「どのような情報を秘密情報とするか」についてご説明したいと思います。 秘密情報の明確化 弊社では、保護対象の情報を明確化するために「情報区分表」を作成しています。これは、機密性のレベルを数段階用意し、各レベルにどういった情報が対応するのかを定義した表です。 以下に、情報区分のイメージを示します。 秘密情報への該当 情報区分 考え方 例 〇 機密性最高 最も保護すべき機密性の高いデータ ... 〇 機密性高 ... ... ... ... ... ... × 機密性低 一般的に広く認知されているデータ 都道府県のマスタデータ この情報区分表において、機密性のレベルが一定以上の重要度が高い項目に該当している情報を秘密情報としています。 次に、秘密情報に関連するデータの管理方法についてご紹介します。 管理台帳の作成 弊社では、秘密情報に関連する台帳を2種類作成しています。 1. 秘密情報管理台帳 秘密情報に該当するテーブル、カラムを管理する台帳です。プロダクトやDB単位で作成し、秘密情報が増えるたびに追記していきます。弊社で使用している様々なデータストアに存在する秘密情報を集約するためにこの台帳を作成しています。この台帳があることで、例えば以下のようなメリットがあります。 別のDBにテーブルのデータを連携する際、台帳を確認するだけでマスク対象のカラムを判断できる センシティブな情報を可視化することで、今後必要なセキュリティ関連の施策やルール変更時に該当箇所の洗い出しを一からやらなくて済む 2. 閲覧者管理台帳 秘密情報の閲覧可能者を管理する台帳です。ここに記名のある人物だけが秘密情報を閲覧できる状態となっています。また、気軽に記名できるわけではありません。責任者の許可が必要である点や、DBごとに記名できる人数を制限している点など、記名のためのルールを整備しています。「誰が」「どのデータストアの秘密情報を閲覧可能なのか」を集約して管理するためにこの台帳を作成しています。 ここまでで、「どういった情報が秘密情報に該当するのか」や、「秘密情報やその情報を閲覧可能な人物をどう管理するのか」についてご紹介しました。続いて、秘密情報の取扱いルールを策定するために整備したフローをご紹介します。 秘密情報取扱いルールを策定するためのフロー 弊社で秘密情報の取扱いルールを策定した際には、次のようなフローの整備を行いました。 新しく追加されるデータの取扱い 既存データで秘密情報に該当する項目の洗い出し 秘密情報にアクセスできるアカウントの制限 権限のないアカウントからのアクセス制限 権限保持者の大幅な削減による運用負荷増への対処 以降、順を追って説明します。 1. 新しく追加されるデータの取扱い 「現時点で保持している情報」だけでなく「今後新しく取得される情報」の取扱い方法についても検討が必要となります。例えば、機能の改修や追加に伴ってDBに追加されるデータの種類が増える場合、そのデータが秘密情報に該当するかを判断する必要があります。そこで、新たに取得する情報について、「誰が」「どのような基準で判断し」「秘密情報に該当すると判断した場合はどういったアクションが必要なのか」を明確にしています。 「誰が」 プロダクトまたはDBごとにアサインしたレビュー担当者 「どのような基準で判断し」 社内の情報区分における機密性が一定以上の情報 「秘密情報に該当すると判断した場合はどういったアクションが必要なのか」 秘密情報管理台帳に記載 その秘密情報を閲覧可能となる人物が追加になる場合は閲覧者管理台帳に記載 秘密情報をマスク化し、アクセスできるアカウントを制限 情報の管理を行っている部署に報告 2. 既存データで秘密情報に該当する項目の洗い出し 弊社では、以前より情報区分が整理されており、どのような情報が秘密情報の対象となるかが明文化されていました。また、どのカラムが秘密情報に該当するかの精査も行われている状態でした。既存のシステムでこうした取り組みが行われていない状況下では、秘密情報に該当するか精査されていないカラムが大量に存在することになると思います。全カラムの精査が現実的でない場合は、効率的な既存データの精査方法として、以下のような手順が考えられます。 文字列型のカラムは氏名や住所など、秘密情報が入っている可能性が高いため、全ての文字列カラムの実データをカラムごとに上位100件ほどを目視で確認し、秘密情報が入っていそうなカラムリストを作成 「address」など、秘密情報に該当する可能性が高いカラム名で各DBのメタデータを検索し、秘密情報が入っていそうなカラムリストを作成 作成した「秘密情報が入っていそうなカラムリスト」を人力で精査 最後に責任者のチェックを通して、最終的な秘密情報カラムリストを作成 秘密情報管理台帳に記載 3. 秘密情報にアクセスできるアカウントの制限 DBの秘密情報を閲覧できる人数を限定します。例えば1DBあたり2名など、大きく絞り込むほうが望ましいです。何も対策を実施していない状況から人数の絞り込みを行えば、秘密情報の閲覧可能者を9割以上削減することも可能かと思います。 また、弊社では秘密情報が閲覧可能な人に対しても、秘密情報が「閲覧不可能なログインアカウント」と「閲覧可能なログインアカウント」の2種類を発行しています。これにより、普段は秘密情報を閲覧できないログインアカウントを使ってもらい、どうしても調査に必要な時だけ別のログインアカウントを使って秘密情報を使った調査を実施できるようにしています。 4. 権限のないアカウントからのアクセス制限 秘密情報と判断したカラムのアクセス制御をどこまで行えるかは、DB製品の機能次第かと思いますが、カラム単位でアクセス制御可能な製品も少なくないと思います。弊社では、閲覧者管理台帳に記載のない人は、秘密情報管理台帳に記載のあるカラムは一切閲覧できない状態を構築しています。 5. 権限保持者の大幅な削減による運用負荷増への対処 単純に秘密情報を閲覧する人数を限定するだけでは、エンジニアの調査や運用の負荷が大幅に増加してしまう懸念があります。例えばオンコール担当者がアラートの調査でどうしてもDBの秘密情報を閲覧する必要がある場合に、閲覧権限を保持している人に調査を依頼することで、権限保持者に負担が集中するケースなどです。 こういった懸念への解決策として、例えばワークフロー申請経由で誰でも一時的に有効な、秘密情報閲覧権限を発行できる仕組みの整備などが考えられます。 弊社で整備した仕組みのイメージ図です。kintoneのワークフローの承認をトリガとして専用のAPI経由でログインアカウントが発行されます。ログインの有効期間は短く、期限切れになると自動で削除されます。 まとめ 弊社で実施している、DB秘密情報の取扱いルールに関する取り組みについてご紹介しました。エンジニアの運用負荷をできるだけ上げずに、秘密情報の閲覧可能者をできるだけ限定することが重要だと考えています。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは! ZOZOTOWNのiOSアプリ開発をしている林と松井です。先日、9/19から9/21までの3日間iOSDC Japan 2020が開催されました。 ブログを書くまでがiOSDC!#didyoublog? 今年はコロナ禍でオンライン開催となり、現地の盛り上がりを体感できませんでしたが、ニコニコ生放送の弾幕などオンラインならではの楽しみがありましたね。また、例年通り素晴らしい発表が盛り沢山でした! オンライン開催においても弊社はスポンサーとして協賛し、10名を超えるメンバーが参加しています。うち5名はスピーカーとしての参加でした。この記事ではiOSDC Japan 2020で登壇するために行った弊社の取り組みと、登壇した社員の発表をご紹介いたします。 採択率向上と登壇内容ブラッシュアップのための取り組み iOSDC Japan 2019ではZOZOテクノロジーズの登壇者が1名のみでしたが、今年はなんと5名も採択されました! 担当しているプロダクトとしてはZOZOTOWNから4名、WEARから1名が登壇しました。今回はZOZOTOWN iOSチームからの採択率を大幅に向上させた取り組みを大公開します。 ネタの整理 過去にCfPに応募する際に、「何を発表すればいいのかわからない」という悩みを持っているメンバーがいました。 ZOZOTOWN iOSアプリは10年以上、改善を続けて進化しているサービスです。レガシーからモダンへのリファクタリング、新しい技術を導入してファッション業界を盛り上げたサービスなど、普段の開発業務でも掘り下げてみると面白い内容がたくさんあったはずです。 それらを可視化するために、iOSチームではネタ表を作成し、整理してみました。まず、最近導入した技術・解決した問題・または興味があって研究したいことを定期的に記入します。そしてチーム内に共有することで、ネタがどんどん集まりました。 実際にこのネタ表から展開して今回のiOSDC Japan 2020の発表タイトルとなった内容がいくつも存在します。 作業時間の確保 ネタがあっても、なかなか調査や検証に着手できないことが多々ありますよね。そこで「もくもく会」を導入しました。もくもく会とは、ネタに対して集中して作業できる専用の時間です。毎週の通常業務として1時間を確保し、チームメンバーが黙々と各自のネタを検証し、最後に進捗を共有します。CfPの応募期限が直前となった時期には、1回のもくもく会を2時間へ増やしたこともありました。気を散らさず集中的に作業することで、スムーズに調査や検証ができました。 このような取り組みを通して、チーム全体の力を合わせてメンバー一人一人をサポートし合っています。その結果、ZOZOTOWN iOSチームがCfPに応募した7件のうち、レギュラートーク2件、LT2件の合計4件が採択されました。 技術顧問レビュー そして採択された後も、当日を迎えるまでサポートは続きます! たとえば、弊社では毎月iOS技術共有会を開催しています。この会は弊社のiOS開発者全員が集まり、それぞれのプロダクト開発で得た知見を共有する場です。共有会には技術顧問である 岸川さん も出席しており、毎月様々なアドバイスを頂いています。iOSDC Japan 2020開催直前は、発表資料に対するレビュー会が行われ「iOSDC Japan殿堂入り」のスピーカーから貴重な意見をいただけました。チーム内の手厚いレビューを経て登壇資料が完成します。 iOSDC Japan 2020当日の工夫 今年のiOSDC Japan 2020は初のオンライン開催となりましたが、弊社ではすでにオンライン開催でのWWDC20参加を経験していたため、大きな混乱はありませんでした。WWDC20についてのまとめは他の記事にてまとめていますので、ぜひご覧ください。 techblog.zozo.com ここでは、開催期間中の働き方、効率的にトークを見るためにチームで行った取り組みをお話します。 開催期間中の働き方 弊社はカンファレンスへの参加が推奨されており、今回のiOSDC Japan 2020も勤務扱いとし、3日間それぞれ振替休日をとる形となりました。 チーム内での効率化 どの発表も興味深い内容であったため、トークの選択に迷われた方も多かったのではないでしょうか。より多くのトークを効率よく視聴するために、チーム内で視聴予定を共有できる工夫をしました。 まず、Miroというツールで事前にどのトークを見る予定か、もしくは迷っているかを宣言するタイムテーブルを作成します。その上にみんなそれぞれ自分の名前ラベルを貼っていき、どの時間帯にだれがどのトークを見ているのかをわかるようにしました。 このようにすると、迷っていた方のトークを他の人が見る予定であれば、もう1つの方を視聴し、後で共有し合うなどといった選択も可能になります。 実際にMiroで用意した画面をご紹介します。 iPadでフルページスクリーンショットを3枚撮り、貼って置くだけの簡単運用です。フルページスクリーンショット便利ですね! 拡大してみると、こんな感じです。 登壇内容の紹介 最後に、弊社のCTO今村からスポンサーセッション1件、採択された5件、技術顧問である岸川さん2件の合計8件のトークをご紹介します。 「ファッション業界を技術で変える、ZOZOの挑戦〜CTOが語る理想の組織像とは〜」 最初に紹介するのは、CTO今村( @kyuns )のスポンサーセッションです。 本セッションではiOSエンジニアの働き方やこの1年間の組織・プロダクトの変遷と改革について紹介しました。私たちiOSエンジニアが日頃どのように開発に取り組んでいるのか、組織やプロダクトはどのように進化しているのかを様々な数値やキーワードとともに説明しています。iOSエンジニアが日頃活用している開発環境や補助制度(iPad Proの貸与やApple Siliconの開発者Kit購入補助など)についても紹介しております。 fortee.jp 「iOSではじめるWebAR」 次は、 @ikkou のレギュラートークです。 ARやVRといったXR領域が大好きなikkouは、ARKitに関する技術を定常的にキャッチアップしています。今回テーマとした選んだWebARは、Web技術ということもあり取っつきやすさはピカイチです。もっと多くの人にARについて知ってもらいたく、iOSDC Japan 2020に登壇しました。ARの基礎からサンプル付きで主流の技術まで、目的別で各技術を詳しく紹介しています。これからWebARをはじめたい方はぜひご覧ください! fortee.jp 「iOSアプリのバッテリー消費を意識する」 次は、 @arara_jp のレギュラートークです。 Appleやユーザーはバッテリーを非常に重視していますが、デベロッパーはクラッシュフリー率や起動速度など他の指標と比べると、バッテリーにそれほど関心を示していない傾向があります。 ですが、この発表の後、Ask the Speakerで「バッテリーの調査方法ありがとう!」とコメントをいただいたり、「自分のアプリも見てみよう」とツイートがあったりと、少しでもバッテリー消費に関心を持つきっかけを生み出せた気がします。ぜひご覧いただき、開発者の皆さんに少しでもバッテリー消費に関しての意識付けがされたら幸いです。 fortee.jp 「テストコードが増えるとバグは減るのだろうか?「0%→60.3%」で見えた世界の話」 次は、 @ahiru のレギュラートークです。 テストコードを書く文化がなかったZOZOTOWNのコードにテストを導入し、この1年でテストカバレッジの割合を0%から56.4%(タイトルの60.3%から訂正)にまで増加させたお話です。 当日はトーク後のAsk the Speakerも大行列ができており、自動テストやQAチームとの勉強会の内容など、ZOZOTOWNの実情に関する質問が途切れませんでした。他社のテストコード導入事例に興味のある方が多いのではないでしょうか。本トークでは実体験を軸にしたメリット・デメリット、組織や開発体勢に適したテスト手法の一例も紹介しています。テストコードを導入した他社事例をはじめ、テストに興味がある方はぜひこのトークを見てください。 fortee.jp 「文字列をコピーできるスクリーンショットを作る」 次は、 @re___you のLTです。 こちらのLTでは、文字列をコピーできるスクリーンショットの作り方について、コードやわかりやすい図を使って解説しています。画像を文字に起こす必要がなくなり、iOSエンジニアとしてもデザインデータの検索をする際など、とても便利だと思います。iOS 13からフルページのスクリーンショットが可能となったことについて「知らなかった!」という声も多くありましたが、フルページのスクリーンショットの作成については他の記事にまとめてありますので、ぜひこちらもご覧ください! techblog.zozo.com fortee.jp 「100人以上の中高大学生にiOSアプリ開発を教えていて感じたこと」 次は、 @tosh_3 のLTです。今回、初めてのカンファレンス参加かつ登壇となりました。 新卒のtosh_3は、なんとすでに100人以上の中高大学生へ教える立場にあったんです! その驚異の経験から、発表中のニコ生コメントも「新卒とは」「新卒ですでに教える立場」とかなり盛り上がっていました。教え方の工夫だけではなく、iOSエンジニアとして考えるべき視点についても語っていますので、ぜひスライドを覗いて見てくださいね。 fortee.jp 「SourceKit LSPをブラウザでコードを読むために活用する」 ここからは岸川さんのレギュラートークを紹介します。 本トークは、岸川さんが今年仕事や趣味で携わってきた、SourceKit-LSPについての発表でした。ブラウザでGitHub上のSwiftコードを読むときに、Xcodeのようにメソッドの定義元にジャンプできるデモが行われた際には、ニコ生コメントで弾幕ができるほど盛り上がりました。SourceKit-LSPの基礎から活用まで一連の知識がわかりやすく紹介されて、コードレビューの効率が革命的に上がる技術になりますので、ぜひご覧ください。 fortee.jp 「400種類のアプリを毎日ビルドする自動化の技術」 次にも、岸川さんのレギュラートークとなります。 このトークでは、Slackbot・fastlane・Bitriseなどの機能を活用して、CIビルド用の情報の自動収集や更新・プッシュ証明書の自動更新・App Store Connectの申請情報の自動更新など、究極の自動化を追い求める技術が幅広く語られました。現状のZOZOTOWNアプリでも、究極を求めて改善できるところが多々あります。岸川さんにご協力を頂きながら、より自動化されたZOZOTOWNアプリを目指していきます。 fortee.jp 以上、セッションの紹介でした。 After iOSDC Japan 2020を開催 ブログを書くまでがiOSDC! と言いますが……いえいえ、iOSDC Japan 2020はまだ終わりませんよ! 今回開催されたiOSDC Japan 2020について、弊社ではAfter iOSDC Japan 2020を9月29日(火)に開催します。Sansan(株)、note(株)、(株)ZOZOテクノロジーズの3社による合同イベントです。 各社の社員によるLT、パネルディスカッションを行いますので、興味のある方はぜひご参加ください! こんな方におすすめです。 iOS関連技術およびすべてのソフトウェアエンジニア iOSDC Japan 2020の振り返りを一緒にしたい方 iOSDC Japan 2020には参加していないけど、情報が知りたいという方 本イベントには、ZOZOテクノロジーズの技術顧問でもある岸川さんも登壇します。 iOSDC Japan 2020に参加した人もそうでない人も、みんなで振り返りましょう! イベント申し込みはこちらから! zozotech-inc.connpass.com さいごに ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
アバター
デバイスに関わる全国の情シスの皆様、日々の業務お疲れ様です。コーポレートエンジニアリング部ファシリティチームの佐藤です。いわゆる”情シス”と呼ばれる役割のチームに所属し、社内インフラ(PCやネットワーク機器)の管理・運用に携わっております。 今回はこのリモートワークが普及してきた中で情シスが解決したい課題の1つである貸与デバイスのキッティング、特に管理者が端末に一切触れずにキッティングを完了できる ゼロタッチキッティング を実現するまでの話を紹介します。 ゼロタッチキッティング導入前の状況 ZOZOグループではWindows 10およびMacのPC端末が数多く存在します。我々ZOZOテクノロジーズの情シスも日々デバイス関連の対応に尽力しています。 1つ大きな課題として、 社員(以下、ユーザーと呼びます)へPC端末を貸与して、社内規定のポリシーに従って正しく利用を開始してもらうための運用確立 というものがあります。 ゼロタッチキッティング導入前は、Windows端末のキッティングを オンプレミスActive Directory参加によるHybrid Azure AD参加 で対応しておりました。以下の通り、仕組みとしては単純でオンプレミスActive DirectoryドメインへPCを参加させると、GPO(グループ・ポリシー・オブジェクト)にて自動でIntune(現在はMicrosoft Endpoint Configuration Manager(MEM)に統合されているため、以降はMEMと記載します)へ登録されるというものです。 以前は当たり前のように会社へ出社して、当たり前のように情シスがその場で設定してユーザーへ手渡すという運用となっていました。 しかし。。。 テレワーク環境への対応と対策 2020年4月、新型コロナウィルス感染症拡大による緊急事態宣言の発令前に当社は強制在宅勤務が開始されました。そのような状況下で既存のオンプレミスActive Directory環境だけを用いてのキッティング運用では、ユーザーが即利用可能になるレベルでのPC配布には無理がありました。 問題点を挙げるときりがありません。 そもそもオフィスに行けないため、情シスからユーザーへ直接PCを手渡しすることができない そもそも情シスメンバーもオフィスに行けないため、オフィスに配送されたPCを社内LANに繋ぐことができない PCは一度社内LANに接続することで、MEMへの登録、規定ソフトのインストールを実施していました そもそもオフィスに行けないため、ユーザーアカウントでのログインができない 結論としては オフィスを訪れないとキッティングできない 状態でした。 このことから考え方を180度変えて、ユーザーがオフィスに来なくてもPCがセットアップできるようにするしかないという決断に至りました。そのため新しくHybrid Azure AD参加からAzure AD参加でのキッティング運用に取り掛かりました! ここで少しネタバレをしますと、このような状況になる前、情シスの内部でAzure AD参加やAutopilotの検証やプレビュー版の動作確認などを行っておりました。そのためMEMやAzure ADでの動作や条件付きアクセスに伴うセキュリティ関連の設定などはすでにある程度把握済みであったため、強制在宅勤務への対応にすばやく切り替えられました。 Azure AD参加によるキッティング ZOZOテクノロジーズではPCのキッティング方式を Azure AD参加を利用したユーザー主導のキッティング へ移行しました。 具体的な手順は以下の通りです。 Windows 10端末を初期化された状態で、ユーザーの自宅へ配送する 情シスはユーザーがキッティングを実施する前に、ユーザーアカウントを用意し、適切な構成プロファイルやアプリが導入されるためのグループへユーザーアカウントの追加作業を行う ユーザーは自宅で手順書を参照しながらキッティングを開始し、情シスは必要に応じてサポートを行う Azure ADへの参加完了後、自動でMEMへの登録や構成プロファイル、アプリケーションがバックグラウンドで導入されるため、自宅から業務ができる環境の構築が完了 Azure AD参加のキッティングへ移行することによって ユーザーがオフィスへ訪れることなくキッティングが完了する という目的を達成しました。 もちろん、その結果として貸与デバイスに「Azure AD参加端末」と「Hybrid Azure AD参加端末」が混在することになり、多くの運用方法の修正やIntuneの追加設定作業が発生しました。 Azure AD端末の運用で解決した主な事例を紹介します。 Azure AD参加端末のローカル管理者グループの編集 この課題は 構成プロファイルで解決 しました。 構成プロファイルの種類で「カスタム」を選択し、OMA-URI の設定にて以下の値を変更することでローカル管理者グループの編集が可能となります。 ./Device/Vendor/MSFT/Policy/Config/RestrictedGroups/ConfigureGroupMembership Azure AD参加端末はあくまでワークグループ端末になるため、GPOでのポリシーの適用が不可になります。そのため一括でのポリシー変更はMEMでの構成プロファイル機能を使用します。まだまだGPOと同じようなきめ細かいポリシー設定はできませんが、カスタム設定(OMA-URI)を使用することで設定範囲を広げることができます。 Intune で Windows 10 デバイス用のカスタム設定を使用する Windows Updateの制御 この課題は Windows 10 更新リングで解決 しました。 更新リングの作成によりWindows Update for Business からの Windows 10 ソフトウェア更新プログラムのインストールを制御しました。品質更新または機能更新プログラムの延期日数や自動更新時の動作の設定が可能になり、個別のPC対応は不要となります。 Azure AD参加端末はあくまでワークグループ端末になるため、GPOでのポリシーの適用が不可になるため、WSUSによる更新プログラム制御はできません。 この状態では、Windows 10は更新プログラムが適用可能時期になると累積更新や機能更新プログラムを即適用してしまうため一律でバージョン固定を行う必要があります。 更新リングでは品質も機能も延期期間の設定がそれぞれ指定できるため、適用時期のタイミングを日数にて制御することができます。 WSUSのように特定の更新プログラムに対しての個別での制御は不可になりますのでご注意ください。 Windows 10 更新プログラムの展開リングの構築 「Azure AD参加端末」と「Hybrid Azure AD参加端末」が混在した環境での問題の切り分けとして大事なことは、その端末がどのオンプレ環境へ依存しているのかを考えることです。 例としては、WSUSでできたことをどうすればMEMで同じように再現できるのかなどを柔軟に検討することです。 もともとAzure AD参加について検証やテストを実施していたことで、大きな混乱や問題が発生することなくこのキッティングを開始することができました。事前検証は大事だなと感じました。 Azure AD参加型キッティングに残る課題 2020年9月現在もいまだ在宅勤務期間は続き、新しい働き方の流れが進む中で情シスのPCキッティングも新しい様式へ変化していくことになります。この期間でAzure AD 参加を利用したユーザー主導のキッティングを進めてきましたが、この方式では完全な自動化にはなっていないのが現状でした。 現状では、ユーザーがキッティングを開始するタイミングで、我々情シスも都度サポート対応が必要になります。また、アプリやプロファイルが適用されるタイミングが端末やネットワーク環境によって異なり、キッティングの進捗確認が困難でもあります。 上記の課題があるので、このキッティング方式についてはユーザーが自由なタイミングでキッティングが開始できず、情シスとのスケジュール確認や連絡などが業務のボトルネックとなりました。いつでもどこでも安定したキッティング環境を提供できるようにするため、我々情シスはキッティングに関して多くの情報と検証を進めました。 ホワイトグローブ(White Glove)の導入 前述の課題を解決するために我々はWindows 10 バージョン1903 からサポートされる ホワイトグローブ(White Glove)展開 を新たに採用しました。 ホワイトグローブ展開とは、Windows 10のセットアップのうちユーザーに関連する一部を除く大部分のセットアップをOEMベンダーと情シスの手元で完了させておく展開方式です。 ホワイトグローブ展開の主な流れは以下の通りです。 OEMベンダーからキッティングセンターにPCが配送される OEMベンダーが配送したPCのデバイスIDをパートナーインビテーションを許可したZOZOグループのクラウドに登録する 情シスはデバイスの登録を確認後、適切なプロファイルおよびアプリが展開されるグループへ割り当てを行う キッティングセンターにて、ホワイトグローブ展開のためのセットアップを各PCにて実施する(これによりプロファイルやアプリなどがPCへ登録される) ユーザーの自宅へPCが配送されるため、ユーザーはPCを起動しAzure ADアカウントで初回ログインする ユーザーの個別設定のみ登録が開始され、ユーザーは即座に業務ができる状態となる これでOEMベンダーおよびキッティングセンターでクラウド登録からPCの基本設定などが完結するため、ホワイトグローブ展開でキッティング運用を行うと情シスの作業が少なくなりました。 また、社内の貸与デバイスでどのPCがホワイトグローブ展開でキッティングを行ったのかも、Azure ADのデバイス一覧でアイコンにて確認することができます。 ホワイトグローブ展開でのポイントとしては、ホワイトグローブ展開を設定する端末は初回起動時にインターネット接続されていない状態にすることです。 インターネット接続状態で起動すると即時でMicrosoftアカウントでのログインを求める画面になりますが、インターネット未接続の状態で起動すれば通常の言語選択の画面になるので、Windowsキーを5回押してホワイトグローブ展開設定の画面に遷移することができます。 まとめ ホワイトグローブ展開の採用でWindows 10端末のキッティング運用がスムーズになり、実際に利用していただくユーザーへの負担を大幅に減らすことができました。 もちろんこれでキッティングに関しての課題が無くなった訳ではありませんが、コーポレートエンジニアリング部ではどのような状況にも対応できるように新しいサービスや技術をいち早くキャッチアップできるように日々努めていく所存です。 また社内にはWindows 10端末以外にもMacやモバイルデバイスなども存在し、今後も増えていくことが想定されるため、そちらのゼロタッチキッティングにも取り掛かっていきます。 さいごに ZOZOテクノロジーズでは、社内の課題をITの力で解決する仲間を募集中です。WindowsとMacはもちろんiPhoneやiPadなど様々なデバイスを効率よく管理と制御することにどんどんチャレンジできます! ご興味のある方は、下記のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは、ECプラットフォーム部の鶴見、竹中です。普段はZOZOTOWNのリプレイスに関わるID基盤とAPI Gatewayの開発を行っています。 本記事では、API Gatewayの開発で取り入れているJSON Schemaを使ったドキュメントの自動生成および、スキーマの自動検証を紹介します。 API Gateway設定ファイルの運用改善 弊社で開発しているAPI Gatewayは、APIへのリクエストのルーティングやリトライなど様々な機能の制御を設定ファイルで行っています。 バックエンドチームはアプリケーションおよび、設定ファイルの仕様書を作成し、SREチームはインフラの構成やパフォーマンスを考慮しながら仕様書に記載されたフォーマットで設定ファイルを記述しています。 設定ファイルの仕様書はConfluenceで、アプリケーションはGitHubで管理しています。設定ファイルの仕様変更が発生した場合は、アプリケーションのデプロイ後に仕様書を手動で更新するという運用をしていました。しかし、手動での更新は更新漏れやタイプミスによる設定可能値の誤りなどの問題を発生させていました。 この問題を解決するための方法として、以下の3つが考えられます。 仕様書のレビュー体制の強化 設定値に対する検証処理コードからの仕様書の自動生成 仕様書からの設定値に対する検証処理コードの自動生成 今回は、実現の容易さから最後の「仕様書からの設定値に対する検証処理コードの自動生成」を選びました。この場合、自然言語からの自動生成は困難ですが、何らかのスキーマ言語を用いることで実現が比較的容易になります。 このスキーマ言語に対して求められることは、仕様を記述するのに十分な表現力を有すること、および検証処理の自動生成が容易であることの2つです。 また同時に、スキーマ言語を用いることで可読性が落ちるため、HTMLドキュメントなどへの変換が容易に行える必要もあります。 これらの条件を満たすスキーマ言語として JSON Schema を選定しました。JSON Schemaは、JSONドキュメントを検証可能にするための標準規格です。 改善方法 今回の運用改善ですることをまとめると、以下の通りです。 JSON Schemaで設定ファイルの仕様を定義し、アプリケーションと同じGitHubリポジトリで管理 JSON SchemaからHTMLドキュメントを自動生成 アプリケーション起動時、個別に行っていた設定値の検証処理をJSON Schemaに基づいた検証処理へ変更 上記の改善で行ったJSON Schemaを使ったドキュメントの自動生成および、スキーマの自動検証の2つについて具体的な実装方法を紹介します。 JSON Schemaを使ったドキュメントの自動生成 最初にJSON Schemaからドキュメントを自動生成する方法を紹介します。ドキュメント生成には、Python製の JSON Schema for Humans を利用しました。 JSON Schema for Humans JSON Schema for Humansは、JSON Schemaからドキュメント化した静的なHTMLページを生成します。実際はAPI Gatewayの仕様を定義したJSON Schemaを使用しますが、今回は説明のため 公式サンプル のJSON Schemaを元にドキュメント化してみます。 { " $id ": " https://example.com/arrays.schema.json ", " $schema ": " http://json-schema.org/draft-07/schema# ", " description ": " A representation of a person, company, organization, or place ", " type ": " object ", " properties ": { " fruits ": { " type ": " array ", " items ": { " type ": " string ", " examples ": [ " apple " ] } } , " vegetables ": { " type ": " array ", " items ": { " $ref ": " #/definitions/veggie " } } } , " definitions ": { " veggie ": { " type ": " object ", " required ": [ " veggieName ", " veggieLike " ] , " properties ": { " veggieName ": { " type ": " string ", " description ": " The name of the vegetable. ", " examples ": [ " potato " ] } , " veggieLike ": { " type ": " boolean ", " description ": " Do I like this vegetable? ", " examples ": [ " true " ] } } } } } generate-schema-docコマンドを使い、JSON Schema(schema.json)を元にドキュメント(schema_doc.html)を生成します。 generate-schema-doc schema.json schema_doc.html コマンド実行により、以下3ファイルが自動生成されました。 schema_doc.html schema_doc.min.js schema_doc.css 生成されたHTML(schema_doc.html)を確認します。 JSON Schemaがドキュメント化されました。 ドキュメント生成の自動化 JSON Schemaを修正するたびに、generate-schema-docコマンドでドキュメント生成できます。しかし、手間がかかるため自動化します。自動化には GitHub Actions を利用しました。 GitHub Actionsは、ワークフローを自動化するサービスです。JSON SchemaはGitHubリポジトリで管理しているため、プッシュやプルリクエストがマージされたタイミングでドキュメント生成処理をすることで自動化できると考えました。 以下のステップでドキュメントを自動生成しています。 JSON Schemaを修正し、GitHub masterブランチにマージ GitHub Actions上でPythonおよび、JSON Schema for Humansをセットアップ generate-schema-docコマンドにより、JSON Schemaからドキュメント(HTML/CSS/JavaScript)を生成 ドキュメントファイルをS3にアップロード 自動化によりS3へアクセスすれば、常に最新のドキュメントを確認できます。S3は、社員のみ参照できるようアクセス制限をしました。 GitHub Actions GitHub Actionsで行っている処理内容を紹介します。JSON Schema for Humansをセットアップ、JSON Schemaからドキュメントを生成し、S3にアップロードする方法です。 name : Update Docs on : pull_request : branches : - master types : - closed paths : - schema.json jobs : updateDoc : name : Update Docs runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v2 - name : Set up Python uses : actions/setup-python@v2 with : python-version : '3.8.5' - name : Generate Docs run : | pip install json-schema-for-humans generate-schema-doc schema.json ./docs/schema_doc.html - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ap-northeast-1 - name : Upload working-directory : docs run : aws s3 sync . s3://docs --delete プルリクエストがマージされたタイミングで動作するように設定しています。また、GitHubリポジトリにはJSON Schema以外のファイルも含まれているため、pathsで対象JSON Schemaファイル(schema.json)を指定しました。これによりJSON Schemaに変更があった時のみGitHub Actionsが動作します。 JSON Schemaを使ったスキーマの自動検証 設定ファイル(スキーマ)の誤りをアプリケーション起動時に気付けるよう、アプリケーション内で設定ファイルに対しJSON Schemaを利用して検証する仕組みを作りました。 API GatewayではGoを用いて開発しているため、実装にあたり gojsonschema というパッケージを利用しました。また、設定ファイルはYAMLで管理していたため、JSONにはせず、YAMLのままとしました。検証時はYAMLをJSONに変換しています。 JSON Schemaは、先程の JSON Schema for Humansのサンプル を利用します。 次に、検証するための設定ファイルを用意します。 fruits : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato - veggieLike : false veggieName : broccoli JSON Schemaを利用して設定ファイルを検証する実装が以下のソースコードです。YAMLからJSONへの変換には、 yaml というパッケージを利用します。 package main import ( "fmt" "io/ioutil" "github.com/ghodss/yaml" "github.com/xeipuuv/gojsonschema" ) func main() { fruitsData, e := ioutil.ReadFile( "fruits.yaml" ) if e != nil { panic (e) } converted, e := yaml.YAMLToJSON(fruitsData) if e != nil { panic (e) } schemaLoader := gojsonschema.NewReferenceLoader( "file:///home/me/fruits-schema.json" ) documentLoader := gojsonschema.NewStringLoader(fmt.Sprintf( "%s" , converted)) result, e := gojsonschema.Validate(schemaLoader, documentLoader) if e != nil { panic (e) } if result.Valid() { fmt.Printf( "The document is valid \n " ) } else { fmt.Printf( "The document is not valid. see errors : \n " ) for _, desc := range result.Errors() { fmt.Printf( "- %s \n " , desc) } } } 実行結果は以下のようになり、設定ファイルがJSON Schemaの定義通りであることが確認できます。 > go run main.go The document is valid JSON Schemaとは異なる定義にした設定ファイルを用意します。 fruits : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato # 必須プロパティ不足 - veggieName : broccoli 実行結果はエラーになり、設定ファイルのミスに気づけます。 > go run main.go The document is not valid. see errors : - vegetables.1: veggieLike is required 細かい制御の追加 ここからは、より実用的な制御例を紹介します。JSON Schemaの例は全体の一部を抜粋して記述しています。他の詳細なオプションは 公式ドキュメント をご参照ください。 不要なプロパティを許可しない "additionalProperties": false の追加により、記述されたプロパティ以外をエラーにします。typoを防ぐのにも役立ちます。 { " type ": " object ", " properties ": { " fruits ": { " type ": " array ", " items ": { " type ": " string " } } , " vegetables ": { " type ": " array ", " items ": { " $ref ": " #/definitions/veggie " } } } , " additionalProperties ": false } typoしたYAML設定ファイルを用意します。 # fruitsのtypo fruit : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato - veggieLike : false veggieName : broccoli プロパティのエラーとして正しく検出されます。 > go run main.go The document is not valid. see errors : - ( root ) : Additional property fruit is not allowed 特定の値のみ許可する "enum": ["potato", "broccoli", "pumpkin"] の追加により、記述された値以外をエラーにします。 " definitions ": { " veggie ": { " type ": " object ", " required ": [ " veggieName ", " veggieLike " ] , " properties ": { " veggieName ": { " type ": " string ", " enum ": [ " potato ", " broccoli ", " pumpkin " ] , " description ": " The name of the vegetable. " } , " veggieLike ": { " type ": " boolean ", " description ": " Do I like this vegetable? " } } } } 許可されていない値を指定した設定ファイルを用意します。 fruits : - apple - orange - pear vegetables : - veggieLike : true veggieName : potato - veggieLike : false # enumに定義されていない名前 veggieName : carrot 値のエラーとして正しく検出されます。 > go run main.go The document is not valid. see errors : - vegetables. 1 .veggieName: vegetables. 1 .veggieName must be one of the following: " potato " , " broccoli " , " pumpkin " まとめ JSON Schemaを使ったドキュメントの自動生成およびスキーマの自動検証を紹介しました。JSON Schemaを用いたことで仕様が管理しやすくなり、仕様書と実際の検証処理に乖離が発生することを防ぐことができました。YAML、JSONを使った設定ファイルの運用方法に迷っている方は参考にしてみてください。 最後に、ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
アバター
こんにちは。ZOZOテクノロジーズSRE部の市橋です。普段は主にAWSを用いてプロダクトのシステム構築、運用に携わっています。今回は弊チームで取り組んでいるZOZOMATのシステム改善業務の一例として、JVMの暖機運転の仕組みを取り入れた話をご紹介します。 ZOZOMATとは お客様の足を3Dで計測するために開発された計測用マットです。ZOZOMATでの計測情報をもとに、靴の推奨サイズを参照するなどのサービスをご利用いただくことが可能です。ご興味のある方は こちら をご確認ください。 JVMの暖機運転とは 今回テーマとして取り上げるJVMの暖機運転とは何かについて簡単に触れていきます。JVMではJIT(Just In Time)コンパイラによるコンパイル方式が取り入れられています。これはアプリケーションの実行前にプログラムの全てを機械語にコンパイルするのではなく、プログラムの実行時にコンパイルを行います。そのため起動直後の一度もプログラムが実行されていない状態では、インタプリタ型の言語のようにプログラムの各行を機械語に変換しながら実行します。コンパイル済みの場合は高速に動作することが期待できますが、機械語に変換しながら実行しなければならないケースでは、しばしば性能問題となることがあります。 JVMの暖機運転とは、コンパイル済みの状態で本番トラフィックの処理が可能になるよう、サービスインの前にアプリケーションを実行することです。これによりJITコンパイラによって引き起こされる問題を回避できます。 ZOZOMATとJVMの暖機運転について ZOZOMATでは開発言語としてScala、実行基盤としてEKSを採用しています。ScalaはJVMの上で動作するため、JVM上で動くことによる恩恵、制約を共に受けます。当該環境でもデプロイ時やスケールアウト時など、新規にアプリケーションが起動するタイミングでレスポンス遅延を確認しており、JVMが未暖機であることに起因する問題が発生していました。この問題を解消することが今回の目的となります。 この仕組みを取り入れる際、以下の記事が今回の要件と合致していたため大変参考にさせていただきました。 KubernetesでJVMアプリを動かすための実践的ノウハウ集 / JVM on Kubernetes - Speaker Deck 本記事では詳しく触れていないZOZOMATのシステム構成については別のブログ記事にて紹介しております。よろしければご確認ください。 techblog.zozo.com JVMの暖機運転の導入以前のレスポンス遅延問題 以下はデプロイ時のAPIごとのレスポンスタイムの平均値と最大値をそれぞれ表すグラフになります。波形が跳ね上がっている箇所がデプロイによりアプリケーションコンテナが入れ替わったタイミングです。APIによって差はあるものの、デプロイ直後にレスポンスタイムが悪化し、その後収束する傾向にあることがわかります。 レスポンスタイム - 平均値 レスポンスタイム - 最大値 JVM暖機運転の事前検証 暖機運転の有効性の確認 ここでは今回問題として取り上げたレスポンス遅延に対して、暖機運転の有効性を確認することを目的とした検証を行います。当該環境で実行回数が多い参照系、更新系のAPIを1つずつ選択し、未暖機状態と1から1,000回の間で段階的に回数を増やして暖機運転を行い、その後のレスポンスタイムを計測しました。結果は以下の通りです。 暖機回数 レスポンスタイム(参照系) レスポンスタイム(更新系) 暖機運転なし 4.38s 11.53s 暖機回数 1 0.30s 0.44s 暖機回数 10 0.23s 0.42s 暖機回数 100 0.17s 0.35s 暖機回数 1,000 0.16s 0.35s 暖機運転なしの状態と暖機運転ありの状態を比較すると、暖機運転ありの場合にレスポンスタイムが改善しているため、暖機運転の導入は有効と考えられます。ここでの結果をまとめると以下のようになります。 未暖機の状態では顕著にレスポンスが遅く、一度でも実行されると大幅に改善する。 暖機回数が増えるにつれてレスポンス速度が向上する。 一定回数を超えると暖機回数が増えるにつれて徐々にレスポンス速度の改善効果は緩やかになる。 JVM暖機運転の構成 暖機運転の有効性が確認できたところで、JVMの暖機運転を行うための構成を考えていきます。構成案としては以下の三点になります。 方式 概要 Pros Cons カナリアデプロイ方式 ロールアウト時にn%のトラフィックを新しく起動したpodに流す。 既存のpodに変更を加えることなく実現可能。 ユーザー影響が発生することに変わりはない。 Sidecar方式 暖機運転用コンテナをSidecarとして設定し、暖機運転がなされた後に本番トラフィックを流す。 既存のアプリケーションコンテナに変更を加える必要がない。 サイドカーコンテナのリソースが暖機運転を終えても残り続けるため、予約しているリソースが無駄になる。 postStart方式 KubernetesのpostStartの仕組みを利用し、アプリケーションコンテナの起動後に自分自身に対してリクエストを発行することで暖機運転を行う。 追加でリソースを予約する必要がない。 postStartはコンテナのentrypointの実行後に呼ばれる保証がない。 アプリケーションコンテナに暖機運転を行うための変更を加える必要がある。 それぞれの構成のイメージとしては次のようになります。 以上の方式の中から、ユーザー影響を発生させず、既存のアプリケーションコンテナに変更を加えることなく実現できるSidecar方式を採用しました。 この構成にする上ではいくつか考慮するポイントがありました。 本構成を導入する上での考慮ポイント 暖機運転の実行前にアプリケーションコンテナの起動を保証するには アプリケーションコンテナの起動前に暖機運転が開始されてしまうと、リクエストを処理できず暖機運転の効果が得られないため、これを回避する必要があります。この対策として、アプリケーションコンテナが最低限起動に要する時間まで暖機運転用コンテナが起動を待つように、KubernetesのinitialDelaySecondsを設定しました。 しかし、これだけではアプリケーションの起動に想定よりも時間がかかってしまった場合、起動前に暖機運転が開始してしまう懸念を拭いきれません。アプリケーションコンテナの起動に余裕を持たせるため、暖機運転用コンテナの起動開始を必要以上に遅らせてしまうと、今度はデプロイ時間が伸びるという別の問題にぶつかります。 そこで、暖機運転を行う前に暖機運転用コンテナからアプリケーションコンテナに対して、ヘルスチェック用のエンドポイントが実行可能かチェックする処理を設けました。私たちのアプリケーションはgRPCで稼働しているため、 grpc-health-probe を利用してアプリケーションの起動を確認しています。このチェックが通った後に暖機運転を開始することで、アプリケーションコンテナが起動していることを保証できます。 暖機運転の完了後に本番トラフィックが転送されることを保証するには 暖機運転が完了する前に、アプリケーションコンテナにリクエストが転送されてしまうと、レスポンス遅延発生の可能性があります。そのため、暖機運転の完了後にリクエストが転送されることを保証する必要があります。 これについてはkubernetesの機構であるReadinessProbeを利用しました。ReadinessProbeはコンテナがトラフィックを受ける準備ができているかを確認する設定です。これにパスしないとpodにIPが割り当てられず、リクエストは転送されません。これを暖機運転用コンテナに対して、暖機運転の完了後にパスするように設定することで、未暖機状態のアプリケーションコンテナにリクエストが転送されることを防ぎます。 また、これに似た設定としてLivenessProbeがあります。こちらはコンテナの生存確認を行う設定で、これにパスできない場合はコンテナが停止し、再生成されます。今回は暖機運転用コンテナにReadinessProbeを設定し、アプリケーションコンテナにLivenessProbeを設定しました。このように設定することでアプリケーションコンテナの死活監視を行いつつ、暖機運転の完了後にリクエストが転送されるように制御できます。 これを実現するには様々な手段があると思いますが、一例としてReadinessProbeを設定した暖機運転用コンテナの定義ファイルを以下に示します。ReadinessProbeでは /tmp/healthy ファイルが存在することをチェックしています。暖機運転用スクリプトが完了したことを以て、 touch /tmp/healthy コマンドを実行して対象のファイルを生成します。これによりReadinessProbeをパスし、トラフィックがアプリケーションコンテナに転送されます。 apiVersion : apps/v1 kind : Deployment metadata : name : api-deployment labels : app : api spec : # --------- omit template : metadata : labels : app : api spec : containers : - name : warmup image : zozomat/warmup command : [ "sh" , "-c" ] args : [ "bash scripts/warmup.sh && touch /tmp/healthy && tail -f /dev/null" ] readinessProbe : exec : command : [ "cat" , "/tmp/healthy" ] initialDelaySeconds : 60 periodSeconds : 5 failureThreshold : 5 # --------- omit 起動直後の遅延が解消するかの検証 暖機運転を行うための構成が決定したため、リリース前に実際のトラフィックに近い条件下で暖機運転の効果を最終確認するため、負荷試験を行いました。負荷試験ツールはGatlingを使用しています。暖機運転の導入前後で比較できるように暖機運転ありと暖機運転なしの状態でそれぞれ実施しました。以下は負荷試験時のレスポンスタイムを表すグラフです。上は暖機運転なし、下は暖機運転ありの結果となります。 暖機運転なしの負荷試験の結果 暖機運転ありの負荷試験の結果 暖機運転なしの結果では、開始直後は10秒を超えるリクエストが大多数を占めており、非常に低速です。開始から少し経つと徐々にレスポンス速度が向上していき、1分経過後は安定して高速にレスポンスできていることがわかります。暖機運転ありの結果では、未暖機時に発生していた開始直後の遅延が解消できていることがわかります。 導入後の改善効果 以下は参照系、更新系のデプロイ前後のレスポンスタイムの平均値と最大値を、暖機運転の導入有無の観点で比較した表とそれをグラフ化したものです。 導入前後のレスポンスタイム比較 - 平均値 API 導入前 (ms) 導入後 (ms) 差 (ms) 改善率 (%) 参照系 API1 27 5 -22 81.48 参照系 API2 373 98 -275 73.73 参照系 API3 28 5 -23 82.14 参照系 API4 253 49 -204 80.63 参照系 API5 289 197 -92 31.83 更新系 API1 486 423 -63 12.96 更新系 API2 468 434 -34 7.26 更新系 API3 119 134 15 -12.61 導入前後のレスポンスタイム比較 - 最大値 API 導入前 (ms) 導入後 (ms) 差 (ms) 改善率 (%) 参照系 API1 1,005 13 -992 98.71 参照系 API2 20,000 429 -19,571 97.86 参照系 API3 1,009 13 -996 98.71 参照系 API4 9,110 151 -8,959 98.34 参照系 API5 17,000 1,158 -15,842 93.19 更新系 API1 3,316 2,411 -905 27.29 更新系 API2 4,518 2,792 -1,726 38.2 更新系 API3 317 231 -86 27.13 改善した点 参照系APIについては平均値、最大値共にレスポンスタイムが改善しました。特に最大値においては90%以上レイテンシが改善し、極端に遅いリクエストの発生を抑止できました。この点は大きな収穫だと考えております。 残課題 更新系APIについては改善傾向はみられたものの、まだまだ遅いリクエストが目立つ印象です。また、更新系API3では最大値は改善しているものの、平均値では暖機運転前よりもわずかに劣化する結果となっています。 これらの要因として、暖機運転の対象をデータ集計には影響しないAPIに限定し、かつ不要なデータ生成を極力抑制するために暖機運転のリクエスト回数を減らしたことが挙げられます。更新系APIのうち暖機運転の対象としたものは更新系API1のみで、それ以外は対象外としています。これらのAPIには共通で読まれるコードがあり、暖機運転の対象としていないAPIにも改善がみられたことから、その部分の暖機運転の効果は得られていると言えます。一方で、改善効果の低さや一部の結果で劣化していることから、十分な効果を得られているとは言えません。この点については残課題として対応方法を検討中です。 まとめ 今回はZOZOMATシステムの改善業務の一例としてJVMの暖機運転を導入し、JVM環境特有のアプリケーションの起動直後の遅延を抑制する方法についてご紹介しました。これはほんの一例でまだまだやらなければならないことが山積みの状態です。 ZOZOテクノロジーズでは、一緒にサービスをより良い方向に改善して頂ける方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは、WEAR部運用改善チームの三浦です。私たちのチームでは、WEARの日々の運用業務を安全かつ効率的に行えるよう改善を行っています。今回はバッチの定期実行に使用しているWindowsのタスクスケジューラーの運用改善について紹介します。 背景 WEARではバッチをWindowsサーバー上で定期実行させており、定期実行するために タスクスケジューラー を使用しています。WEARではバッチ実行用のサーバー(バッチサーバー)を用意しており、バッチサーバーへのアクセス権限を持つ人がタスクスケジューラーの設定を下記のような画面から変更していました。 しかしこの運用方法では次のような課題がありました。 バッチサーバーへのアクセス権限がある人しかタスクの設定を見ることができない タスクスケジューラー上ではタスクの変更履歴や変更した経緯が残らない GUI上での手動変更では操作ミスが起こる 引継ぎが手間 実際にこれらの課題により、業務に影響を及ぼす事象も発生しました。 タスクの実行頻度を元に戻したいが、以前の設定がどうなっていたか見ることができない タスクを手動で止め、実行頻度を変更後に再開するのを忘れ、タスクが止まったままになっていた 実現したいこと 前述した課題を改善するために2つの改善を行うことにしました。 タスクの設定をGitHubで管理する 各タスクの設定をファイル出力してGitHub上で管理します。これによりバッチサーバーへのアクセス権限を問わずタスクの設定を確認できるようになります。 また、Gitでのバージョン管理によって変更履歴も確認できるようになります。 コマンドでタスクの設定を変更する コマンドでタスクの設定が書かれたファイルを読み込んで反映させます。コマンドを実行するだけでよくなり、これまで必要だった手動作業が不要になることにより、結果として誤操作のリスクを防ぐことができます。事前にコマンドもレビューしておくと更に安全に変更ができます。 これらを実現するためにschtasksというコマンドを使用します。 schtasksについて schtasksはWindows OSに標準搭載されているコマンドで、タスクスケジューラーのタスクの操作ができます。Windowsのコマンドプロンプトから実行してタスクの登録や実行頻度の変更といった編集操作が行えます。 コマンドは下記のように操作用のオプション(create, change, query, etc.)とその操作に対するオプション(対象のタスク、実行頻度、etc.)を指定して実行します。各オプションの詳細などは 公式ドキュメント を参照ください。 schtasks <操作オプション> <その他オプション>... schtasksコマンドでは現在動いているタスクの設定内容をXMLで出力したり、XMLファイルを読み込んでタスクの設定内容を変更できます。 タスクの設定をXMLで出力する schtasks /query /tn ${XML出力したいタスクの名前} /XML XMLを読み込んでタスクの設定内容を変更する schtasks /create /tn ${変更したいタスクの名前} /XML ${読み込むXMLの完全修飾パス} /F ※厳密には既存のタスクを一旦削除してXMLを元に作り直しています。 今回はこのXMLの入出力の機能を使いたいと思います。 各タスクの設定をXML出力してGitHubで管理する 各タスクの設定をXML出力し、そのXMLをGitHubで管理する手順を説明します。 Windows PowerShellを管理者実行で開く バッチサーバーにログインしWindows PowerShellを管理者実行で開きます。管理者実行ではない場合、ログインしているユーザーが作成したタスクしか操作できないので注意が必要です。 schtasksコマンドはコマンドプロンプトからでも実行できますが、今回はPowerShellのコマンドも使用したいため、Windows PowerShellを開いています。 schtask + PowerShellのコマンドでXML出力 schtasksコマンドで現在動いているタスクの一覧を取得します。そして、一覧をループして各タスクの設定をschtasksコマンドでXML出力します。 schtasks /query /nh /fo CSV | Select-String -NotMatch Microsoft | ForEach-Object { $task = ($_-split(','))[0]; $file = Join-Path '${保存先ディレクトリ}' ([regex]::Replace($task, '[\[\]"\s]', '')); schtasks /query /tn $task /XML > $file'.xml' } 最初の schtasks /query /nh /fo CSV ではschtasksの検索機能(query)で現状動いているタスクを全て取得し、CSV形式で出力します。 取得結果は下記画像のように、1行に1タスクのタスク名、次回の実行時刻、実行ステータスがCSV形式で出力されます。この時点ではWEARのバッチとは関係ないWindows OS標準のタスクが混ざってしまうので、PowerShellのSelect-Stringコマンドで除外しています。 ループの中で schtasks /query /XML を実行して、各タスクの設定をXML出力します。出力結果は下記画像のようになります。 なお、XMLのスキーマに関しては 公式ドキュメント に詳しく記載されています。 <?xml version="1.0" encoding="UTF-16"?> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2020-06-18T18:21:35</Date> <Author>***</Author> <URI>\miura-task</URI> </RegistrationInfo> <Principals> <Principal id="Author"> <UserId>***</UserId> <LogonType>Password</LogonType> </Principal> </Principals> <Settings> <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <Enabled>false</Enabled> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> </Settings> <Triggers> <CalendarTrigger> <StartBoundary>2020-06-18T18:20:00</StartBoundary> <Repetition> <Interval>PT1M</Interval> </Repetition> <ScheduleByDay> <DaysInterval>1</DaysInterval> </ScheduleByDay> </CalendarTrigger> </Triggers> <Actions Context="Author"> <Exec> <Command>C:\Users\***\Desktop\task-test\test.vbs</Command> <Arguments>hoge piyo hello</Arguments> </Exec> </Actions> </Task> 最後に、コマンドでタスク毎に出力されたXMLファイルをGitHubに保存して完了です。 GitHubに保存したXMLでタスクの設定変更を行う ここまでの手順で、各タスクの設定をXML出力し、そのXMLをGitHubで管理できるようになりました。次に、GitHubに保存したXMLでタスクの設定変更を行う手順を説明します。 タスク設定のXMLを修正 実行頻度やコマンドライン引数などタスクの設定を変更したい場合は、GitHubに保存したタスクのXMLを任意の値に修正します。修正したXMLをPull Requestに出し、コードレビューを行います。 XMLをバッチサーバーに持ってくる バッチサーバーに変更内容を記載したXMLを git pull で取り込みます。 schtasksコマンドでXMLの内容を反映 schtasksコマンドでXMLを読み込んでタスクの設定を変更します。 schtasks /create /tn ${変更したいタスクの名前} /ru ${バッチ実行ユーザー} /rp %PASS% /XML ${読み込みたいXMLの完全修飾パス} /F WEARではバッチ実行用のアカウントでバッチを実行させているため、バッチの実行ユーザーとパスワードを明記しています。パスワードはWindowsの環境変数で登録しておいたものを参照させています。実行ユーザーを明記しない場合はこのコマンドを実行したユーザーがバッチの実行ユーザーになります。 反映内容の確認 schtasksコマンドでタスクを検索し、反映した内容を確認できます。 schtasks /query /tn ${変更したタスクの名前} /v /fo LIST 検索結果は下記のように表示されます。 まとめ タスクスケジューラーの運用改善として、GitHubでのタスクの設定管理 + schtasksコマンドでリリースする方法をご紹介しました。この運用によって事前にレビューできるようになり、より安全にリリースができるようになりました。また、コマンドで簡単に更新ができるので操作ミスがなく、作業時間の短縮にもつながりました。 今後の改善として、CI/CD周りを整備してXMLのシンタックスチェックを自動で行なったり、Pull Requestマージ後に自動でリリースして手動作業を完全になくす運用を目指しています。 さいごに 他チームとコミュニケーションを取りながら既存の運用を安全かつ効率的に行えるよう改善していくのはとてもやりがいがあります。 ZOZOテクノロジーズでは、コミュニケーションと技術力を活かしながら一緒に会社を盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、9/8に 第一回 AWSマルチアカウント事例祭り を開催しました。 zozotech-inc.connpass.com AWSを活用する複数社が集まり、事例に関してお話しする祭典が「AWSマルチアカウント事例祭り」です。専門性の高い、ここでしか聞けないコアなトークをお届けしました。特にAWSを使用している方、AWSのマルチアカウント運用を始めたい方、AWSのマルチアカウント運用に課題を感じている方に向けたイベントです。 登壇内容 まとめ ZOZOテクノロジーズ、ウェザーニューズ、リクルートよりそれぞれ1名ずつ、合計3名が登壇しました。 AWS Configを用いたマルチアカウント・マルチリージョンでのリソース把握とコンプライアンス維持への取り組みについて (株式会社ZOZOテクノロジーズ 光野 達朗 / @kotatsu360 ) マルチアカウント運用の開始までの取り組み (株式会社ウェザーニューズ 小野 晃路) 「進化し続けるインフラ」のためのマルチアカウント管理 (株式会社リクルート 須藤 悠) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
ZOZOテクノロジーズ ECプラットフォーム部 マイグレーションチームの會田です。 ZOZOTOWNでは 先日公開した記事 の通り、すべての検索をElasticsearchへリプレイスしました。 検索エンジンのリプレイスに伴い、VBScriptで稼働していた検索システムをJavaへリプレイスすることも併せて行われました。 本記事ではその際に得た知見を、Elasticsearch初心者の方及びElasticsearch Java APIを初めて触る方向けに紹介します。 環境(開発当時) Elasticsearchバージョン:7.3.2 Javaバージョン:8 Spring Bootバージョン:1.5.15 Mavenバージョン:2.17 準備 <dependency> <groupId> org.elasticsearch </groupId> <artifactId> elasticsearch </artifactId> <version> 7.3.2 </version> </dependency> <dependency> <groupId> org.elasticsearch.client </groupId> <artifactId> elasticsearch-rest-high-level-client </artifactId> <version> 7.3.2 </version> </dependency> 上記をpom.xmlに記載するとElasticsearchのライブラリ及び後述するRestHighLevelClientが使えるようになります。 基本的なライブラリの説明と使用方法 ※以降に出てくるカラム名及びIDなどは全て実在するものではなくダミーデータです。 RestLowLevelClient(RestClient) RestLowLevelClient はhttp経由でElasticsearchクラスタと通信できるクライアントです。Elasticsearchのすべてのバージョンと互換性があります。 参考:   Java Low Level REST Clientのリファレンス RestHighLevelClient RestHighLevelClient はそれまで利用されていたTransportClientに代わって推奨されている、RESTクライアントです。これを使用することで、Javaアプリケーションからhttpを介してElasticsearchへアクセスできます。 なお、Java High Level REST ClientはJava Low Level REST Client上で動作しています。 final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials( "user" , "password" ) ); RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost( "localhost" , 9200 , "https" )) .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder .setDefaultCredentialsProvider(credentialsProvider)) ); searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); 参考:   Java High Level REST Clientのリファレンス SearchSourceBuilder SearchSourceBuilder は検索動作を制御する大半のオプションを制御できます。 下記ソースコードの詳細は後述のライブラリ紹介の中で別途説明します。 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termQuery( "FashionItemId" , 1 )); searchSourceBuilder.from( 0 ); searchSourceBuilder.size( 100 ); searchSourceBuilder.sort( new FieldSortBuilder( "ItemPrice" ).order(SortOrder.ASC)); 参考:   SearchSourceBuilderリファレンス SearchRequest 下記リファレンスより引用。 SearchRequestは、ドキュメント、集約、サジェストを検索する操作に使用され、結果として得られるドキュメントのハイライト表示を要求する方法も提供します。 とありますが、APIを使ってElasticsearchへリクエストを送るための大元になるものという捉え方でよいと思います。 ※Elasticsearchでは、データをドキュメントという単位で扱います。 下記コードの詳細は後述のライブラリ紹介の中で説明します。 SearchRequest searchRequest = new SearchRequest(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termQuery( "FashionItemId" , 1 )); searchRequest.source(searchSourceBuilder); 参考:   SearchRequestリファレンス CountRequest CountRequest は実行したクエリに一致したドキュメント数を取得するために使用します。 CountRequest countRequest = new CountRequest(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.matchAllQuery()); countRequest.source(searchSourceBuilder); 参考:   CountRequestリファレンス QueryBuilders 検索クエリを作成するためのユーティリティクラスです。 参考:   QueryBuilders一覧 下記に代表的な検索クエリとQueryBuildersユーティリティクラス内の対応するQueryBuilderクラスおよびヘルパーメソッドを紹介します。 matchAllQuery すべてのドキュメントを取得します。 QueryBuilders.matchAllQuery(); 参考:   matchAllQueryリファレンス termQuery 条件に合致するか。SQLの = と同義。 QueryBuilders.termQuery( "FashionItemId" , 1 ); 参考:   termQueryリファレンス   termQuery Javadoc termsQuery 条件に合致したドキュメントがあるか。SQLの IN 句と同義。 Integer[] fashionItemIds = { 1 , 2 }; QueryBuilders.termsQuery( "FashionItemId" , fashionItemIds); 参考:   termsQueryリファレンス   termsQuery Javadoc rangeQuery 条件で指定した範囲のドキュメントがあるか。SQLの >= 、 <= 、 > 、 < と同義。 // StartDatetime >= "2020-01-01 00:00:00" QueryBuilders.rangeQuery( "StartDatetime" ).gte( "2020-01-01 00:00:00" ); // EndDatetime <= "2021-01-01 00:00:00" QueryBuilders.rangeQuery( "EndDatetime" ).lte( "2021-01-01 00:00:00" ); // StartDatetime > "2020-01-01 00:00:00" QueryBuilders.rangeQuery( "StartDatetime" ).gt( "2020-01-01 00:00:00" ); // EndDatetime < "2021-01-01 00:00:00" QueryBuilders.rangeQuery( "EndDatetime" ).lt( "2021-01-01 00:00:00" ); 参考:   rangeQueryリファレンス   rangeQuery Javadoc matchQuery 全文クエリを実行するための標準クエリ。指定されたテキスト、数値、日付、またはブール値に一致するドキュメントを返します。 第一引数にフィールド名、第二引数に検索ワードを指定します。 QueryBuilders.matchQuery( "FashionItemName" , "t-shirts" ); 参考:   matchQueryリファレンス   matchQuery Javadoc multiMatchQuery 前述した matchQuery() に基づいて構築され、複数フィールドにまたがる検索クエリを可能にします。 第一引数に検索ワード、第二引数以降に検索するフィールドを指定します。 QueryBuilders.multiMatchQuery( "t-shirts" ,  "FashionItemName" , "FashionItemCategoryName" ); 参考:   multiMatchQueryリファレンス   multiMatchQuery Javadoc boolQuery 複数のクエリを組み合わせるために使用します。AND, OR, NOTを組み合わせることができます。 boolQueryには4種類あります。 クエリ 説明 must SQLの AND と同義。指定された条件によってスコアが計算されます。 filter SQLの AND と同義。mustとは異なり、スコアが計算されません。 should SQLの OR と同義。 must not SQLの NOT と同義。 Integer[] fashionItemCategoryIds = { 1 , 2 }; QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery( "FashionItemId" , 1 ) .filter(QueryBuilders.termsQuery( "FashionItemCategoryId" , fashionItemCategoryIds)); FunctionScoreQueryBuilder Elasticsearchはデフォルトで score の高い順にソートされて検索結果が返ってきます。この score をチューニングすることによって目的に最適な結果を取得できるようになります。 そのために用いるのが FunctionScoreQueryBuilder です。 function_score は、 query にヒットするドキュメントに対して、 functions に複数のスコアリングのルール( function )を設定して検索結果のソートを行うことができます。 function_score について詳しく知りたい方は 公式リファレンス でご確認ください。 ※score…検索条件に適合したドキュメントを返すための基準となる値のこと。 下記は書き方の一例です。 ArrayList<FunctionScoreQueryBuilder.FilterFunctionBuilder> functionScoreArrayList = new ArrayList<>(); filterFunctionList.add( new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery( "FashionItemId" , 1 ), ScoreFunctionBuilders.fieldValueFactorFunction( "field1" ).factor(Float.valueOf( "0.0254389" )).missing( 0.2 ))); filterFunctionList.add( new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchAllQuery(), ScoreFunctionBuilders.fieldValueFactorFunction( "field2" ).factor(Float.valueOf( "0.0937572" )).missing( 0.2 ))); // ArrayList型をFunctionScoreQueryBuilder.FilterFunctionBuilder[]にする FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = functionScoreArrayList.toArray( new FunctionScoreQueryBuilder.FilterFunctionBuilder[functionScoreArrayList.size()]); FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(queryBuilder, functions).scoreMode(FunctionScoreQuery.ScoreMode.SUM).boostMode(CombineFunction.REPLACE); searchSourceBuilder.query(functionScoreQueryBuilder); 参考:   Function score queryリファレンス   FunctionScoreQueryBuilder Javadoc fetchSource Elasticsearchの _source はSQLの SELECT 句と同義で取得したいフィールドを指定できます。APIの場合は取得するフィールドを fetchSource を使って指定します。 取得するフィールドを絞り込むことでデータ量の削減にもつながるので、速度の改善が期待できます。 第一引数には取得するフィールド、第二引数に除外するフィールドを指定します。 searchSourceBuilder.fetchSource( new String[]{ "FashionItemId" , "ItemPrice" , "FashionItemSize" , "FashionItemLargeCategory" , "FashionItemSubCategory" }, "ExclusionFashionItemId" ); 参考:   Source filteringリファレンス FieldSortBuilder FieldSortBuilder はSQLの ORDER BY と同義で、ソートの制御に使用します。 SearchSourceBuilder オプションの1つで、 SearchSourceBuilder に対して .sort でソートを追加できます。 searchSourceBuilder.sort( new FieldSortBuilder( "ItemPrice" ).order(SortOrder.ASC)) .sort( new FieldSortBuilder( "FashionItemId" ).order(SortOrder.DESC)) .sort( new FieldSortBuilder( "StartDatetime" ).order(SortOrder.DESC)); 参考:   Sortリファレンス   FieldSortBuilderリファレンス from & size SQLの OFFSET と LIMIT と同義です。 SearchSourceBuilder のオプションの1つで、 SearchSourceBuilder に対して .from .size で指定できます。 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.from( 0 ); searchSourceBuilder.size( 100 ); 参考:   ページングリファレンス   SearchSourceBuilderリファレンス SQLとElasticsearchとJavaでそれぞれ同じクエリを表現 SQLで以下のように書くクエリをElastiseach、Javaで記述するとどのように書くことができるのか比較してみます。 StartDatetime >= ' 2019-12-06 17:33:18 ' AND ( ( FashionItemLargeCategory <> 1 AND FashionItemSubCategory NOT IN ( 10 , 20 , 30 ) AND FashionItemSize IN ( 1 , 2 ) ) OR ( ( FashionItemLargeCategory = 2 OR FashionItemSubCategory IN ( 40 , 50 , 60 ) ) AND FashionItemSize IN ( 9 , 10 ) ) ) Elasticsearchの場合。 " bool ": { " filter ": [ { " range ": { " StartDatetime ": { " from ": " 2019-12-06 17:33:18 ", " to ": null , " include_lower ": true , " include_upper ": true } } } , { " bool ": { " filter ": [ { " bool ": { " should ": [ { " bool ": { " filter ": [ { " terms ": { " FashionItemSize ": [ 1 , 2 ] } } ] , " must_not ": [ { " term ": { " FashionItemLargeCategory ": { " value ": 1 , " boost ": 1 } } } , { " terms ": { " FashionItemSubCategory ": [ 10 , 20 , 30 ] } } ] } } , { " bool ": { " filter ": [ { " terms ": { " FashionItemSize ": [ 9 , 10 ] } } ] , " should ": [ { " term ": { " FashionItemLargeCategory ": { " value ": 1 } } } , { " terms ": { " FashionItemSubCategory ": [ 40 , 50 , 60 ] } } ] } } ] } } ] } } ] } Javaの場合。 Integer[] subCategories1 = { 10 , 20 , 30 }; Integer[] itemSizes1 = { 1 , 2 }; Integer[] subCategories2 = { 40 , 50 , 60 }; Integer[] itemSizes2 = { 9 , 10 }; BoolQueryBuilder qb1 = boolQuery() .mustNot(termQuery( "FashionItemLargeCategory" , 1 )) .mustNot(termsQuery( "FashionItemSubCategory" , subCategories1)) .filter(termsQuery( "FashionItemSize" , itemSizes1)); BoolQueryBuilder qb2 = boolQuery() .should(termQuery( "FashionItemLargeCategory" , 2 )) .should(termsQuery( "FashionItemSubCategory" , subCategories2)) .filter(termsQuery( "FashionItemSize" , itemSizes2)); BoolQueryBuilder qb3 = boolQuery() .should(qb1) .should(qb2); BoolQueryBuilder qb4 = boolQuery() .filter(rangeQuery( "StartDatetime" ).from( "2019-12-06 17:33:18" ).to( null )) .filter(qb3); Javaで生成したクエリの確認方法 BoolQueryBuilder に対して toString() することでElasticsearchのクエリを取得できます。 取得したクエリを想定していたクエリと照らし合わせたり、 Kibana で実行するなどしてクエリが正しいものか確認しましょう。 System.out.println(qb4.toString()); SearchResponse SearchResponse はRestHighLevelClientに対して .search を指定することで使用できます。 SearchResponse searchResponse = null ; // searchRequestにヒットした検索結果をSearchResponseの形式で取得できます。 searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 取得したドキュメントにアクセスするにはまずレスポンスに含まれるSearchHitsを取得する必要があります。 SearchHits searchHits = searchResponse.getHits(); // 個々の検索結果はSearchHits内にネストされているので細かい検索結果を見たい場合はこのように取得します。 SearchHit[] searchHit = searchHits.getHits(); // ドキュメントの総ヒット数はTotalHitsに対して.valueで取得できます。 long totalHitsNum = searchHits.getTotalHits().value; // ドキュメントの最大スコアはSearchHitsに対して.getMaxScore()で取得できます。 float maxScore = searchHits.getMaxScore(); for (SearchHit hit : searchHit) { // 各ヒットのスコアを取得できます。 float score = hit.getScore(); // Map<String, Object>形式でドキュメントソースを取得できます。 Map<String, Object> source = hit.getSourceAsMap(); // Map形式で取得したドキュメントソース対して、キー名を指定して.get("フィールド名")で値を取得できます。 Integer fashionItemId = Integer.parseInt(sourceAsObject.get( "FashionItemId" ).toString()); } 参考:   SearchAPIリファレンス   SearchResponse Javadoc CountResponse CountResponse はRestHighLevelClientに対して .count を指定することで使用できます。 CountResponse countResponse = null ; countResponse = restHighLevelClient.count(countRequest, RequestOptions.DEFAULT); long count = countResponse.getCount(); このようにするとsearchRequestにヒットした件数をlong型で取得できます。 参考:   CountAPIリファレンス   CountResponse Javadoc 開発中苦労したこと Elasticsearchのクエリに自信が持てない中、開発すること。 少し踏み込んだ実践的なElasticsearchのJava APIの参考となる資料が少なく、壁にぶつかると中々乗り越えられない。 私はElasticsearchの経験が無かったのでこの記事を読んでいる方の状況に近かったと思います。少しでもそのような方々の参考になればと思います。 ECプラットフォーム部 マイグレーションチームでは、仲間を募集しています。ご興味のある方は、こちらからご応募ください。 tech.zozo.com
アバター
※AMP表示の場合、数式が正しく表示されません。数式を確認する場合は 通常表示版 をご覧ください ※2020年11月7日に、「Open Bandit Pipelineの使い方」の節に修正を加えました。修正では、パッケージの更新に伴って、実装例を新たなバージョンに対応させました。詳しくは対応する release note をご確認ください。今後、データセット・パッケージ・論文などの更新情報は Google Group にて随時周知する予定です。こちらも良ければフォローしてみてください。また新たに「国際会議ワークショップでの反応」という章を追記しました。 ZOZO研究所と共同研究をしている東京工業大学の 齋藤優太 です。普段は、反実仮想機械学習の理論と応用をつなぐような研究をしています。反実仮想機械学習に関しては、拙著の サーベイ記事 をご覧ください。 本記事では、機械学習に基づいて作られた 意思決定の性能 をオフライン評価するための Off-Policy Evaluation (OPE) を紹介します。またOPEを含めたバンディットにまつわる研究利用のためにZOZO研究所が公開した Open Bandit Dataset と開発したパッケージ Open Bandit Pipeline の特徴や使い方を解説します。 research.zozo.com github.com なお、本記事は こちらのプレスリリース に関連する内容になっています。また本記事の内容は、先日arXivで公開した 論文 の内容を噛み砕いて日本語で紹介したものになっています。気になる方はぜひ元論文も参照してみてください。さらに、2020年8月27日にオンラインで開催された CFML勉強会 にて本記事の内容に関する発表を行っており、その際の 発表資料 も公開しています。この発表資料は、本記事の内容を補完するような例を紹介していたりするので、是非合わせてご覧ください。 モチベーション 機械学習は予測のための技術としてもてはやされています。実際多くの論文が、解く意味があるとされているタスクにおいてより良い予測精度を達成することを目指しています。もちろん予測値をそのまま活用する場面も多くあるでしょう。しかしとりわけウェブ産業における機械学習の応用場面に目を向けてみると、 機械学習による予測値をそのまま使うのではなく、予測値に基づいて何かしらの意思決定を行っていることが多くあります 。例えば、各ユーザーとアイテムのペアごとのクリック率を予測し、その予測値に基づいてユーザーごとにどのアイテムを推薦すべきか選択する。または、商品の購入確率予測に基づいてユーザーごとにどの商品の広告を提示するか決定する、などです。これらの例では クリック率や購入確率の予測そのものよりもそれに基づいて作られる推薦や広告配信などの意思決定が重要 です。 本記事の興味は、 機械学習による予測値などに基づいて作られる意思決定policyの性能を評価すること にあります。例えばクリック率の予測値に基づいてアイテム推薦の意思決定を行う場合、オフラインでクリック率の予測精度を評価指標としていることがあるでしょう。しかし、これは非直接的な評価方法です。予測値をそのままではなく何らかの意思決定を行うために用いているのならば、 最終的に出来上がる意思決定policyの性能を直接評価するべき です。 意思決定policyの性能評価として理想的なのは、policyをサービスに実装してしまい興味があるKPIの挙動を見るという オンライン実験 でしょう。しかし、何でもかんでもオンライン実験ができるわけではありません。なぜならば、オンライン実験には大きな実装コストが伴うからです。性能の悪い意思決定policyをオンライン実験してしまったときに、実験期間においてユーザー体験を害してしまったり、収入を減らしてしまう恐れもあります。従って、 古い意思決定policyにより蓄積されたデータのみを用いて、新しい意思決定policyをオンラインに実装したときの性能を事前に見積もりたい 、というモチベーションが生まれます。 このように、新たな意思決定policyの性能を過去の蓄積データを用いて推定する問題のことを Off-Policy Evaluation (OPE) と呼びます。NetflixやSpotify、Criteoなどの研究所がこぞってトップ国際会議でOPEに関する論文を発表しており、特にテック企業から大きな注目を集めています。正確なOPEは、多くの実務的メリットをもたらします。例えば、ある旧ロジックをそれとは異なる新ロジックに変えようと思ったとき、新ロジックがもたらすKPIの値を旧ロジックが蓄積したデータを用いて見積もることができます。また、ハイパーパラメータや機械学習アルゴリズムの組み合わせを変えることによって多数生成される意思決定policyの候補のうち、どれをオンライン実験に回すべきなのかを事前に絞り込むこともできます。 以降、OPEの一般的な定式化と標準的な推定量(手法)を紹介します。 Off-Policy Evaluationの定式化 ここでは、OPEの問題を定式化します。特徴量ベクトルを 、取り得る 個の行動を 、観測される報酬を とします。 は、潜在目的変数と呼ばれる因果推論で良く用いられる記法で、例えばある行動 を選択した時の報酬は として表されます。ここで注意が必要なのが、 という行動を選択したならば、その行動に紐づく潜在目的変数 しか観測されないことです。それ以外の潜在目的変数の組 は分析者には観測されないということになります。これらの記号をイメージするための例を表1に示しました。 表1:特徴量、行動、目的変数の例 応用例 特徴量 行動 目的変数 映画推薦 年齢、性別、過去の映画視聴履歴など 映画の種類 クリック有無、視聴時間など 投薬 年齢、性別、体重、過去の検査結果など 薬の種類 生存有無、血糖値など さて、 意思決定policy とは、特徴量 から行動空間 上の確率分布への写像 として定義されます。つまり は、ある というベクトルで特徴付けられるデータに対して、 という行動を選択する確率です(つまり、 )。 は、まさに どんな状況でどの行動をとるべきかを司る意思決定policyと言えるでしょう 。 ここである意思決定policy の性能を次のように定義します。 つまり は、 という意思決定policyを導入した際の目的変数の期待値 です。例えば目的変数がクリック有無ならば、 は によってもたらされるクリック率、ということになり、意思決定policyの性能の定義として妥当でしょう。 を実システムで一定期間走らせるオンライン実験ができるならば、その期間に観測される目的変数の平均をとることで困難なく を推定できます。しかし、冒頭で説明したようにオンライン実験を行うこと自体に多くの困難が付きまといます。 よってオンライン実験の代替案として、 の性能をオフライン評価することを考えてみましょう 。オフライン評価のためのデータとして、過去の意思決定policy(旧ロジック)である によって次のような 個のデータを含む で表されるデータセットを蓄積していたとします。 なおなぜ過去のpolicyの添え字が なのかというと、このようなデータを集める際に走っていた過去のpolicyのことを論文では良く behavior policy と呼ぶからです。一方で、これからオフラインで性能を評価したい新たなpolicyのことは counterfactual policy や evaluation policy と呼びます。 は、観測される特徴量ごとに過去の意思決定policyが というふうに行動を選択し、それに紐づく潜在目的変数 が観測されることで構成されます。OPEは、過去のpolicyとは異なる新たな意思決定policy(新ロジック) の真の性能である を過去の蓄積データ を用いて精度良く推定してくれる推定量 を作ることを目的とします。なお推定量 の理論的な性能は次の mean-squared-error (MSE)で評価されます。MSEが小さい推定量ほど、正確なオフライン評価を可能にしてくれるということです。 2つ目の等式はいわゆるbias-variance分解です。ここで、 で です。MSEの意味で良い推定精度を達成するためには、biasとvarianceの両方を考慮してあげる必要があります。次章で紹介する標準的な推定量は、ざっくりとbiasを抑えるのが得意なものとvarianceを抑えるのが得意なものに分けられます。bias抑制重視の推定量なのかvariance抑制重視の推定量なのかを理解しておくことは、場面ごとにどの推定量を用いるべきなのかを考える上で役に立ちます。 標準的な推定量 ここでは、意思決定policyの性能をオフライン評価するための標準的な方法として、Direct Method (DM)・Inverse Probability Weighting (IPW)・Doubly Robust (DR)という3つの推定量を紹介します。 Direct Method (DM) DMはまず、過去に蓄積されたデータ を使って特徴量から目的変数の期待値を推定するモデル を得ます。 には、リッジ回帰やランダムフォレストなど良く知られた機械学習の手法が用いられます。次に、 を用いて次のように の性能を推定します。 つまり、DMは潜在目的変数を で置き換えてしまおうという発想だとわかります。もちろんDMの推定精度は、 の推定精度に大きく依存します。 が を良く推定できていればそれを用いて出来上がる も を良く推定します。しかし、 の推定精度が悪ければ のオフライン評価に失敗してしまいます。現実的な設定において、 を良く推定することは難しい場合が多いです。これらの点から、DMはMSEのうちbiasの部分が大きいという問題を抱えていることが知られています。一方で、varianceが問題になることはあまりありません。 Inverse Probability Weighting (IPW) 次に紹介するのは、DMとは全く異なる発想に基づくIPWという手法です。これは、過去の意思決定policyと評価したい新たな意思決定policyの行動選択確率の比で観測されている目的変数を重み付けることで、次のように を推定します。 IPWはいくつかの妥当な仮定のもとで不偏性を持ちます。すなわち、MSEのうちbiasの部分が0ということです。一方で、データが少ない場合や と が大きく異なる場合に、varianceが大きくなってしまう問題があります。つまり、DMとIPWの間にはbiasとvarianceのトレードオフがあるのです。基本的にデータが十分にあればIPWが望ましいですが、データが少ないときにはDMの方が良い推定精度を発揮することがあります。 Doubly Robust (DR) DRはここまでに紹介したDMとIPWをうまく組み合わせた手法で、次のようにして の性能を推定します。 つまり、DRはDMによる推定値をベースラインとしつつも、第二項においてIPWのような方法によりDMが使っている目的変数の期待値のモデル の推定誤差を補正していることがわかります。このような賢い方法をとることにより、DRはIPWの不偏性を保ちつつ(多くの場合)varianceを減少できることが知られています。 まとめ 本章では、DM・IPW・DRというOPEにおいて標準的な推定量を簡単に紹介しました。これらの理論的性質やより発展的な推定量に関して詳しいことが知りたい方のために、本記事の最後にさらなる学習のための参考文献をまとめています。ここで紹介した基本となる3つの推定量さえきちんと理解しておけば、他のもう少し複雑な手法もそんなに悩むことなく理解できるはずです。 Off-Policy Evaluationの研究の課題 さて実はここからが本記事の本題です。ここまでOPEのモチベーションと標準的な推定量を簡単に紹介してきました。「早速使ってみたい」と思った方もいるかもしれません。事実、研究分野としてとても盛り上がっており、ここ数年で多くの理論的知見が得られています。しかし、OPEに関する論文で行われている実験に目を向けてみると、実は次のような課題があることに気が付きます。 理論系の論文では多クラス分類問題を無理やりOPEの設定とみなすなどの 人工的で非現実的 な実験が行われている 実証系の論文では実データを使う場合があるものの公開はされておらず、他の研究者による 再現が不可能 である OPEに関する既存論文が採用している実験方法を表2にまとめました。私たちの知る限り OPEの実験を可能にする研究用公開実データは存在しません 。もしご存知の方がいらっしゃったらぜひ連絡をください。 表2: 各論文の実験方法の分類。数字は記事末の参考文献の数字と対応。 OPEの実験方法 その実験方法が取られている論文 多クラス分類問題を無理やりOPEの設定とみなすなどの人工的な方法 [1-11] 実データを使っているが非公開 [12-16] さて現実的でかつ再現可能なOPEの実験評価を行うためには、次のような条件を満たす公開実データが必要になります。 複数の意思決定policyによって収集されたデータが収録されている データ収集に用いられた意思決定policyが何なのか公表されている 以上の条件が満たされていれば、後の章で示す方法によってOPEの実験評価を行うことができます。また追加で以下のような条件も満たされていると色々な設定での信頼度の高い実験が可能になるという意味で望ましいです。 大規模である(数千万レコード以上) 多くの意思決定policyによって収集されている 複数の目的変数が収録されている もしこれらの条件を満たすデータを公開可能だという方がいらっしゃったら、論文とともに世に送り出すことによって学術的に大きな貢献になる可能性があるので、是非検討してみてください。 なお Criteoが2016年に公開している実データ は一見OPEの実験が可能に見えますが、1つの意思決定policyによるデータしか含まれない、意思決定policyが何なのか公表されていないという点からOPEの実験に使うことはできません。もう少し新しいところでは Spotifyも似たようなデータセットを公開 しているようですが、これも1つの意思決定policyによるデータしか含まれないという理由によりOPEの実験に使うことはできません。 Open Bandit Dataset の公開 OPE研究の実験における課題を解決すべく私とZOZO研究所、Yale大学の成田悠輔氏らによる研究チームは、特にOPEの研究に適した Open Bandit Dataset を公開しました。このデータセットは、株式会社ZOZOが運営する大規模ファッションECサイト ZOZOTOWN で収集されたものです。同社は、ZOZOTOWNのトップページにおいて多腕バンディットアルゴリズムを用いて意思決定policyを構成し、数あるファッションアイテムの中からユーザーごとに適したアイテムを推薦しています。バンディットアルゴリズムによるファッションアイテム推薦の例を図1に示しました。 図1:ZOZOTOWNにおけるファッションアイテムの推薦の例 私たちは2019年11月下旬に7日間にわたる実験を行い、全アイテム(All)・男性用アイテム(Men's)・女性用アイテム(Women's)に対応する3つのキャンペーンでデータを収集しました。それぞれのキャンペーンでは、トップページ来訪ユーザーに対してRandomまたはBernoulli Thompson Sampling(BernoulliTS)という2種類の意思決定policyを確率的にランダムに選択して適用しています。表3はOpen Bandit Datasetの記述統計を示しています。 表3:Open Bandit Datasetのキャンペーンとデータ収集方策ごとの記述統計 3つのキャンペーン(campaigns)と2つのbehavior policyごとに、データ数(#Data)・アイテム数(#Items: のこと)・クリック率(CTR: behavior policyの性能 )などが記述されています。 1200万レコードを含む全アイテムキャンペーンとBernoulliTSの組み合わせを筆頭に、合計2600万レコード以上の大規模データセットとなっていることがわかります。 最後に、表4にOpen Bandit Datasetのイメージを示しました。各レコードは、推薦されたアイテムのid( )、推薦位置、行動選択確率( )、 クリック有無( )、特徴量( )で構成されています。なお推薦位置は、図1で示したZOZOTOWNにおけるファッションアイテムの推薦枠の3箇所の推薦位置のどこで推薦されたかを表しています(左から順に、1・2・3の値)。 表4:Open Bandit Datasetに含まれる情報 少量のサンプルデータを こちらのディレクトリ に置いているので、気になる方はチェックしてみてください。 Open Bandit Pipeline の公開 Open Bandit Pipelineの概要 さてOpen Bandit Datasetの公開だけでもOPEの研究を特に実験面からサポートするという意味で大きな貢献だと言えます。しかし我々の研究チームはデータセットに加えて、 Open Bandit Pipeline (OBP) というバンディットアルゴリズムやOPEの性能評価のためのPythonパッケージを実装・公開しました。我々のOBPにより、研究者はOPE部分の実装に集中しつつ、再現性のある手順で他の手法との性能比較を行うことができるようになります。また機械学習エンジニアやデータサイエンティストなどの実践者は自社サービスにおいて旧ロジックが収集した過去のデータを使って簡単に新ロジックの性能を推定することが可能になります。図2にOBPの構成を記しました。 図2:Open Bandit Pipelineの構成 図に示したように、OBPは4つの主要モジュールで構成されています。 datasetモジュール には、Open Bandit Dataset用のデータ読み込み用のクラスや人工データ生成のためのクラスを実装しています。 policyモジュール には、バンディットアルゴリズムなどに基づいた意思決定policyを実装するためのインタフェースやいくつかの標準的なアルゴリズムを実装しています。 simulatorモジュール には、オフラインで意思決定policyのシミュレーションを行うための関数を提供します。 opeモジュール には、標準的なOPE推定量や新たな推定量を実装するためのインタフェースを実装しています。 OBPを活用することで、研究者は独自の意思決定policyやOPE推定量の実装に集中し、それらの性能を評価できます(図2の赤い部分)。 さらに実践者は、独自のデータセットをパイプラインと組み合わせることで、自社の設定・環境でOPEを用いた意思決定policyの性能評価を行うことができるのです。 OBPについての詳細は リポジトリ や ドキュメント をご覧ください。 github.com zr-obp.readthedocs.io Open Bandit Pipelineの使い方 ここではOBPの基本的な使い方を紹介します。説明のために、「 旧ロジックであるRandom policyが収集した過去データを用いて、新ロジックであるBernoulliTS policyの性能をオフライン評価する 」という仮想的な分析例を用います。このようなオフライン評価は、OBPを使うと次のように実装できます。 >>> from obp.dataset import OpenBanditDataset >>> from obp.policy import BernoulliTS >>> from obp.ope import OffPolicyEvaluation, Inverse Probability Weighting as IPW # (1) データの読み込みと前処理 >>> dataset = OpenBanditDataset(behavior_policy= 'random' , campaign= 'all' ) >>> bandit_feedback = dataset.obtain_batch_bandit_feedback() # (2) オフライン方策シミュレーション >>> evaluation_policy = BernoulliTS( n_actions=dataset.n_actions, len_list=dataset.len_list, is_zozotown_prior= True , campaign= "all" , random_state= 12345 ) >>> action_dist = evaluation_policy.compute_batch_action_dist( n_sim= 100000 , n_rounds=bandit_feedback[ "n_rounds" ] ) # (3) Off-Policy Evaluation >>> ope = OffPolicyEvaluation(bandit_feedback=bandit_feedback, ope_estimators=[IPW()]) >>> estimated_policy_value = ope.estimate_policy_values(action_dist=action_dist) # Randomに対するBernoulliTSの性能の改善率(相対クリック率) >>> relative_policy_value_of_bernoulli_ts = estimated_policy_value[ 'ipw' ] / bandit_feedback[ 'reward' ].mean() >>> print (relative_policy_value_of_bernoulli_ts) 1.198126 ... 以下、重要な要素について解説します。 まず最初にデータを読み込みます。ここではすでにOBPのdatasetモジュールに実装されている OpenBanditDataset クラスを用いて、Open Bandit Datasetを読み込んでいます。 # 「全アイテムキャンペーン」においてRandom policyが集めたログデータを読み込む(これらは引数に設定) >>> dataset = OpenBanditDataset(behavior_policy= 'random' , campaign= 'all' ) # 過去の意思決定policyによる蓄積データ`bandit feedback`を得る >>> bandit_feedback = dataset.obtain_batch_bandit_feedback() >>> print (bandit_feedback.keys()) dict_keys([ 'n_rounds' , 'n_actions' , 'action' , 'position' , 'reward' , 'pscore' , 'context' , 'action_context' ]) 次にオフラインで意思決定policyのシミュレーションを行います。これは教師あり機械学習のモデルを評価する際に、一度検証用データに対して予測をかけることに対応します。 # 評価対象の意思決定policyとして、BernoulliTSを用いる >>> evaluation_policy = BernoulliTS( n_actions=dataset.n_actions, len_list=dataset.len_list, is_zozotown_prior= True , # ZOZOTOWN上での挙動を再現 campaign= "all" , random_state= 12345 ) # シミュレーションにより、BernoulliTSによる行動選択確率を算出 >>> action_dist = evaluation_policy.compute_batch_action_dist( n_sim= 100000 , n_rounds=bandit_feedback[ "n_rounds" ] ) is_zozotown_prior=True とすることにより、データ収集期間にZOZOTOWNの推薦枠で実際に稼働したBernoulliTSの挙動を再現できます。なお、 is_zozotown_prior=False とすると、自ら設定した事前分布か無情報事前分布が反映されます。 最後に、BernoulliTSの性能のオフライン評価を行います。ここでは、opeモジュールに実装されているIPW推定量を使います。 # 算出されたBernoulliTSの行動選択確率に基づき、IPW推定量を用いて性能をオフライン評価する # OffPolicyEvaluationクラスには、過去の意思決定policyによる蓄積データとOPE推定量を渡す(複数設定可) >>> ope = OffPolicyEvaluation(bandit_feedback=bandit_feedback, ope_estimators=[IPW()]) >>> estimated_policy_value = ope.estimate_policy_values(action_dist=action_dist) # 設定されたOPE推定量ごとの推定値を含んだ辞書が出力される >>> print (estimated_policy_value) { 'ipw' : 0.004553 ...} # 最後に、新ロジック(BernoulliTS)の性能と旧ロジック(Random)の性能を比較する # 新ロジックの性能はOPEによる推定値を、Randomの性能はログデータの目的変数の平均で推定できる真の性能を用いる >>> relative_policy_value_of_bernoulli_ts = estimated_policy_value[ 'ipw' ] / bandit_feedback[ 'reward' ].mean() # 以上のOPEによって、BernoulliTSの性能はRandomの性能を19.81%上回ると推定された >>> print (relative_policy_value_of_bernoulli_ts) 1.198126 ... 以上の簡単な実装で、新旧の意思決定policyの性能を旧ロジックが蓄積したデータのみに基づいて比較できました。ここでの実装例だと、旧ロジックのRandomによる過去の蓄積データのみを用いて、新ロジックであるBernoulliTSの性能が旧ロジックのそれを19.81%上回るという評価を得ました。この結果に基づいて、新ロジックをいきなり実戦投入したり、大きな被害が出ないと踏んだ上で安心してオンライン実験に進んだりできるのです。 なおここで用いた簡易例はquickstart exampleとして、 notebook で動かせるようになっているので確認してみてください。その他にも、人工データなどを用いた豊富な 活用例 も提供しており、すぐに手を動かしながらOBPの使い方を把握することが可能になっています。 是非、 git clone https://github.com/st-tech/zr-obp してから遊んでいただけたらと思います。 DatasetとPipelineを活用したOPEの実験評価 最後に、Open Bandit DatasetとOpen Bandit Pipelineの組み合わせによって、標準的なOPE推定量の性能評価(意思決定policyの性能のオフライン評価の正確さの評価)を行ってみます。このOPE推定量の実験評価が、データ公開とOBPの実装によって我々が可能にしたかったことになります。 Open Bandit Datasetを用いたOPEの評価方法 ここではOpen Bandit Datasetを用いてOPE推定量の性能評価を行うための手順を紹介します。準備のため、意思決定policyA(例えばRandom)によって収集されたデータを 、意思決定policyB(例えばBernoulliTS)によって収集されたデータを と表します。また意思決定policyAを 、意思決定policyBを としておきます。 次のような手順をとることで、OPE推定量 の正確さを評価することが可能です。 OPEの評価のための手順 と の性能をそれぞれもう一方のpolicyが収集したデータをもとに推定する。すなわち、 の真の性能 を で推定し、 の真の性能 を で推定する。 と の性能をそれぞれのpolicy自身が収集したデータを用いて推定し、これらを と の真の性能とみなす。すなわち、 を で、 を で代替する。これは、オンライン実験を行って と の性能を推定していることに相当するため、真の性能とみなすことに妥当性がある。 OPEの評価指標を用いて の正確さを評価する。ここでは次の relative estimation error をOPEの実験的な評価指標として用いる。これが小さい推定量ほど、意思決定policyの性能の正確なオフライン評価ができているということになる。なお、AとBを入れ替えると の性能を推定する場合のrelative estimation errorの定義式になる。 少々複雑な手順に見えますが、 と を交互に新旧ロジックとみなしてOPEを行うようなシミュレーションを行うことで推定量 の正確さを評価しています。上記のようなOPEの評価を行うためには、 複数の意思決定policyによって収集されたデータ が含まれており、かつ や をログデータ上で動作させる必要があるため それらの意思決定policyが何なのか公表されている ことが必要だということがわかります。我々のデータセットと実装は、このような現実的で再現可能なOPEの評価を可能にするのです。 DM・IPW・DRの性能比較 さて上述のOPE推定量の評価手順を用いて、標準的なOPE推定量であるDM・IPW・DRの推定性能評価を行ってみました。実験設定は以下の通りです。 Randomをbehavior policy(旧ロジック)、BernoulliTSをcounterfactual policy(新ロジック)とみなして、BernoulliTSの性能を推定した際のオフライン評価の正確さを評価 DMやDRに必要な機械学習モデル には、ロジスティック回帰を使用 10個の異なるブートストラップサンプルによる結果を用いて、relative estimation errorの信頼区間を推定 得られた実験結果を表5に示しました。 表5:キャンペーンごとのOPE推定量の推定精度 (relative estimation error) 全アイテム (All) 男性向けアイテム (Men's) 女性向けアイテム (Women's) DM 0.2319 0.2150 0.2261 IPW 0.1147 0.1347 0.0788 DR 0.1181 0.1200 0.0786 ここではシンプルなロジスティック回帰を使ったこともあるのか、DMは一貫して悪い推定精度を示しました。一方で、IPWとDRはほとんど大きな差が見られませんでした。よってより実装が簡単なIPWでオフライン評価をするのが実務的には良さそうです。しかし、DRもDMと同様に機械学習モデルを用いているため、これを違うモデルに変更するとDRの推定が良くなる可能性もまだ残されています。サンプリングなどによってデータ数を意図的に変えてみたりbehavior policyとcounterfactual policyを入れ替えてみると異なる結果が得られるかもしれません。是非色々試してみてください。 ここで行ったOPE推定量の性能評価に用いた実装は、 こちらのリポジトリ に置いてあります。READMEには、relative estimation errorの信頼区間も含めた詳細な結果を記載しています。 国際会議ワークショップでの反応 本記事で紹介したデータ公開とパッケージ実装を含む研究プロジェクトはこれまでに、 ICML2020 RealML や RecSys2020 REVEAL など、トップ国際会議の関連ワークショップで口頭発表を行ってきました。 特に、2020年9月26日にオンライン開催されたRecSys2020併設のREVEAL workshopでは、AmazonやGoogle・Criteo・Microsoftなどで研究をされている界隈で有名な方々と並んで約200人の聴衆の前で30分間本研究プロジェクトの内容を口頭発表する機会に恵まれました。反応は以下の通り大変好評で、発表が終わった後Zoomのチャットに好意的なコメントが並んでいた時は、とても嬉しかったです。本研究の方向性が間違っていないことを専門家からの反応によって確かめることができたり、プロジェクトの内容を広く周知できたとても良い研究発表機会になりました。 昨日、RecSys恒例のREVEAL WSにて、約200人の前で30分 live talkしました。発表自体が好評だっただけでなく、発表後に米トップ大の教授から直々に誘いを受けるなど反響がありました。オンライン学会でも入念に準備すれば十分チャンスや繋がりを掴めるようなので、あなどらず是非本気出していきましょう pic.twitter.com/MH869hxJvg — usaito (@usait0) September 27, 2020 twitter.com また学会後に執筆されたいくつかのブログ記事にて、Open Bandit DatasetやPipelineについて触れていただきました。界隈で大きな注目を浴びることに成功したので、これからもデータの拡大やパッケージの機能追加、及び国際会議での周知などに一層力を入れ、OPEの分野では知らぬものがいないオープンソースに成長させていくつもりです。 RecSys 2020: Highlights of a Special Conference Similar to last year, the REVEAL workshop attracted the most attention with more than 900 participants being around. You should definitely check it out. There was also the release of Open Bandit Pipeline – a python library for bandit algorithms and off-policy evaluation that was considered as one of the highlights of this workshop. 推薦システムの国際学会RecSys2020の参加録 REVEAL 2020: Bandit and Reinforcement Learning from User Interactions バンディットや強化学習に関するワークショップになります。NetflixやMicrosoftなどの企業から発表が多いです。日本からは、@usaitoさんのバンディットアルゴリズム用の大規模データセットの公開に関する発表があり、大きな注目を集めていました。 さいごに 本記事では、OPEと呼ばれる機械学習による予測ではなく意思決定policyの性能を直接評価する方法を紹介しました。またOPEの研究を 現実的で再現可能 なものにするために我々が公開した大規模データセットと研究用パッケージについて紹介しました。今後もZOZOTOWNでの追加実装をもとにデータセットを増強したり、継続してパッケージのメンテナンスや機能追加を行っていくつもりですので、ぜひチェックしてみてください。 なお本記事の内容や元論文・データセット・パッケージに関しての質問、間違いの指摘、改善の提案などがありましたらメール (ZOZO研究所: zozo-research@zozo.com, 本記事の著者: saito.y.bj@m.titech.ac.jp)やTwitter ( @usait0 )でご連絡ください。 また、ZOZOテクノロジーズでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com さらなる学習のための資料 ここではOPEのことをもっと知りたい方のために、私自身がこれまで独学でOPEを勉強してきた際に活用してきた有用資料や論文をいくつか紹介します。 Doubly Robust Off-policy Evaluation with Shrinkage ICML’19 Workshop on Real-world Sequential Decision MakingでのMicrosoft ResearchのMiro Dudík氏による招待講演。前半のOPEの導入が秀逸。OPEの定式化やDM・IPW・DRのなんとなくのイメージが沸いた方が視聴すると良い整理になるはず。 バンディットと因果推論 CyberAgent、AI Lab 安井 翔太氏のCFML勉強会での発表資料。本記事では紹介を省いたOPEにおける困難や必要な仮定などが説明されており、眺めてみると良い勉強になるだろう。 Contextual Bandits in Recommendation 元Spotifyで現Netflix ResearchのJames Mclnerney氏によるチュートリアル資料。推薦における利用の観点からcontextual bandit (意思決定policyを作る方法の一つ)から、そのオフライン評価に至までかなり詳細に解説されている資料。特に推薦システムにおけるOPEの利用を考えている方は眺めておいて損はないだろう。 Off-policy Evaluation and Learning Alekh Agarwal、Sham Kakade両氏によるワシントン大学での講義資料。本記事で紹介したDM・IPW・DRの理論的な基礎をきっちり把握したい方は目を通してみると良いだろう。 Doubly Robust Policy Evaluation and Optimization 本記事で紹介した標準的な推定量であるDM・IPW・DRの理論的背景がまとまっているので、こちらもこれらの推定量の理論的な基礎を把握したい方は一読してみると良いだろう。 Intrinsically Efficient, Stable, and Bounded Off-Policy Evaluation for Reinforcement Learning Nathan Kallus・Masatoshi Uehara両氏によるNeurIPS'19論文。1章と2章においていくつかのOPE推定量の理論性質の比較がなされており、推定量のいろんな理論性質を知りたい方は楽しめると思う。 その他、もう少し発展的な推定量について知りたい方は[2,3,7,10,13,14]を企業による活用事例を知りたい方は応用系学会に出ている[12,15,16]あたりを読んでみると良いでしょう。 参考文献 Miroslav Dudík, Dumitru Erhan, John Langford, and Lihong Li. Doubly Robust Policy Evaluation and Optimization . Statistical Science , 29:485–511, 2014. Yu-Xiang Wang, Alekh Agarwal, and Miroslav Dudik. Optimal and Adaptive Off-policy Evaluation in Contextual Bandits . In Proceedings of the 34th International Conference on Machine Learning , 3589–3597. 2017. Mehrdad Farajtabar, Yinlam Chow, and Mohammad Ghavamzadeh. More Robust Doubly Robust Off-policy Evaluation . In Proceedings of the 35th International Conference on Machine Learning , 1447–1456. 2018. Nathan Kallus and Masatoshi Uehara. Intrinsically Efficient, Stable, and Bounded Off-Policy Evaluation for Reinforcement Learning . In Advances in Neural Information Processing Systems . 2019. Nikos Vlassis, Aurelien Bibaut, Maria Dimakopoulou, and Tony Jebara. On the design of estimators for bandit off-policy evaluation . In International Conference on Machine Learning , pages 6468–6476, 2019. Cameron Voloshin, Hoang M Le, Nan Jiang, and Yisong Yue. Empirical study of off-policy policy evaluation for reinforcement learning . arXiv preprint arXiv:1911.06854 , 2019. Adith Swaminathan, Akshay Krishnamurthy, Alekh Agarwal, Miro Dudik, John Langford, Damien Jose, and Imed Zitouni. Off-policy Evaluation for Slate Recommendation . In Advances in Neural Information Processing Systems , pages 3635–3645, 2017. Noveen Sachdeva, Yi Su, and Thorsten Joachims. Off-Policy Learning with Deficient Support . In ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD) , 2020. Yi Su Pavithra Srinath Akshay Krishnamurthy. Adaptive Estimator Selection for Off-Policy Evaluation . In International Conference on Machine Learning , 2020. Aman Agarwal, Soumya Basu, Tobias Schnabel, and Thorsten Joachims. Effective Evaluation Using Logged Bandit Feedback from Multiple Loggers . In ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD) , 2017. Masahiro Kato, Masatoshi Uehara, and Shota Yasui. Off-Policy Evaluation and Learning for External Validity under a Covariate Shift . arXiv preprint arXiv:2002.11642 , 2020. James McInerney, Brian Brost, Praveen Chandar, Rishabh Mehrotra, and Ben Carterett, Counterfactual Evaluation of Slate Recommendations with Sequential Reward Interactions . arXiv preprint arXiv:2007.12986 , 2020. Yusuke Narita, Shota Yasui, and Kohei Yata. Off-policy Bandit and Reinforcement Learning . arXiv preprint arXiv:2002.08536 , 2020. Yusuke Narita, Shota Yasui, and Kohei Yata. Efficient counterfactual learning from bandit feedback . In Proceedings of the AAAI Conference on Artificial Intelligence , volume 33, pages 4634–4641, 2019. Alexandre Gilotte, Clément Calauzènes, Thomas Nedelec, Alexandre Abraham, and Simon Dollé. Offline a/b testing for recommender systems . In Proceedings of the Eleventh ACM International Conference on Web Search and Data Mining , pages 198–206, 2018. Alois Gruson, Praveen Chandar, Christophe Charbuillet, James McInerney, Samantha Hansen, Damien Tardieu, and Ben Carterette. Offline evaluation to make decisions about playlist recommendation algorithms . In Proceedings of the Twelfth ACM International Conference on Web Search and Data Mining , pages 420–428, 2019. Damien Lefortier, Adith Swaminathan, Xiaotao Gu, Thorsten Joachims, and Maarten de Rijke. Large-scale validation of counterfactual learning methods: A test-bed . arXiv preprint arXiv:1612.00367 , 2016. Weihua Hu, Matthias Fey, Marinka Zitnik, Yuxiao Dong, Hongyu Ren, Bowen Liu, Michele Catasta, and Jure Leskovec. Open Graph Benchmark: Datasets for Machine Learning on Graphs . arXiv preprint arXiv:2005.00687 , 2020.
アバター
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、8/27に ZOZO Technologies Meetup~マーケティング基盤とそれを支えるデータ基盤~ を開催しました。 zozotech-inc.connpass.com ZOZOテクノロジーズのマーケティング基盤とデータ基盤の開発に興味のある方を対象としたイベントで、ZOZOテクノロジーズが扱う大規模データを支える基盤の開発・活用方法についてのご紹介や、GCPやDataflowを用いてどのように大規模データを扱い、運用しているのかを現場のエンジニア目線で詳しくお伝えしました。 登壇内容 まとめ 弊社の社員4名が登壇しました。 ZOZOTOWNにおけるマーケティングオートメーションの概要 (川名 智久 / @emsgraphixx ) LINEセグメント配信基盤について (長澤 修平 / @snagasawa_ ) リアルタイムマーケティング基盤の紹介とリプレイス計画 (田島 克哉 / @katsuyan121 ) ZOZOTOWNを支えるリアルタイムデータ基盤 (谷口 恵輔 / @csk_pos ) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは。ZOZOテクノロジーズの廣瀬です。 私は2020年8月に、Microsoft MVPをData Platformカテゴリにおいて受賞しました。本カテゴリにおける現在の日本の受賞者は私で10人目となります。本記事ではMicrosoft MVPの概要と、受賞するためにどのようなことを意識して、どのような行動をとっていたのかについてご紹介します。 Microsoft MVPとは Microsoft MVPとは、Microsoftに関連する技術コミュニティにおいて大きく貢献した人物を表彰する制度で、1年間有効なアワードです。再受賞のためには、毎年審査が必要になります。Microsoft MVP for xxxというように、特定のカテゴリごとに受賞者が決まり、現在では11カテゴリにおいて約2900名のMVPが世界中に存在しています。受賞者は MVP一覧ページ にプロフィールが掲載されます。 mvp.microsoft.com 公式ページ によると、Microsoft MVPの人物像について以下のような記述があります。 熱意をもって自身の知識をコミュニティへ共有するテクノロジーの専門家 Microsoftの製品とサービスに関する極めて深い知識がある 優れた技術力に加えて、常に進んで他者を助ける また、MVPを受賞すると以下のような特典を受けることができます。 Microsoft製品への早期アクセス 製品チームと直接やり取りできるチャネルへの参加 本社が主催する年次イベントGlobal MVP Summitへの招待 MVPになるには、以下の2ステップが必要となります。 Microsoftの常勤従業員(FTE)またはMicrosoft MVPから推薦状を提出してもらう 自分自身で過去1年間のコミュニティ活動実績をアピールする MVP受賞者には、記念品が贈られます。 大きな箱が届きます。外見もかっこいいです。 トロフィーや盾などが入っています。早速部屋に飾りました。 次に、Microsoft MVPを受賞するためにどのようなことを意識して、どのような行動をとっていたのかについてご紹介します。 受賞のためにしたこと 1. Microsoft MVPの受賞を目標に設定する 私がMicrosoft MVPの受賞を目指そうと決めたのは、2年半ほど前のことです。その当時はまだコミュティ活動を活発に行えているとは言えない状況でした。そこで、受賞を目指すために必要な行動を整理し、少しずつアウトプットの質と量とチャネルを増やしていきました。その結果、受賞した活動期間(2019/07-2020/06)においては、以下のような活動実績となりました。 会社のテックブログ 4本 Qiitaの記事 18本 カンファレンスでの登壇 1回 コミュニティの勉強会での登壇 10回 社内勉強会を約20回開催 GitHubでの情報取得クエリ群 の公開 Microsoftの技術フォーラムでの回答活動 でのTOP5%の回答者 Twitter での情報発信 2. 継続的にコミュニティへ参加する 私は Japan SQL Server User Group という技術コミュニティに所属し、勉強会でほぼ毎月登壇しています。コミュニティに自身の学びを還元することで、他の方の学びにつながることもあれば、逆に参加者から有益なフィードバックをいただけることもあります。 登壇すると直接フィードバックをいただけるので、次回の登壇のモチベーションにつながります。このように、コミュニティに参加することでアウトプットのモチベーションを維持し続けることができたと思います。 3. コミュニティをアップデートするために自分ができることを考える SQL Serverに関する技術記事を読んだり勉強会に参加していると、以下のようなことを思いつくことがあります。 「こういうことが知りたいんだけどちょうどいい記事が無いな」 例えば、「クエリストアを使わず、サーバー負荷を上げずにCPUボトルネックなクエリを全体最適の観点で特定する方法は無いだろうか」という疑問から、 Qiitaの記事 にまとめました。 「この概念、自分だったらもっと分かりやすく説明できそう」 例えば、SQL Serverのロックについて分かりやすく説明することを目指して書いた Qiitaの記事 のPV数は約7万で、自分が書いた記事の中では最もPV数が多い記事の1つです。また、「SQL Server ロック」でGoogle検索すると1番目にヒットするため、SQL Serverのロックという概念の理解の助けになれていると感じています。 「こういう話をしている人がいないけど自分がやったらおもしろそう」 大規模な自社サービスでSQL Serverを運用しているという経験はあまり得られる機会も少なく、発生した障害の詳細や解決方法について話したらおもしろいのではと思い、 テックブログ にまとめました。このように、どうやったら自分が所属している技術コミュニティをアップデートできるか、ということを考え、思いついたことは実践してみる、ということを継続するようにしていました。 このように、「自分が参加しているコミュニティにまだ無いと思われる知見」を持ち込んだり、「より分かりやすくしてあげることでみんなの役に立ちそうなこと」をアウトプットすることで、コミュニティのアップデートに貢献できると思い、行動していました。 4. 社内向けの資料作成時も、公開可能な情報はQiitaやTwitterで発信 私のアウトプットは、業務の中で生じた疑問や課題についての検証結果をまとめることが多いです。業務に紐づいているため、社内にしか公開できない情報もありますが、その中で公開可能な検証結果についてはQiitaやTwitterで発信するように意識していました。 例えば、「変更の追跡」という機能を導入する際は、以下のようにアウトプット先を切り替えていました。 「どの環境のどのDBのどのテーブルに導入するか」といった情報は外部公開できないため、社内Wikiに記載 設定をプロダクション環境へ反映する際にハマった点があり、純粋に技術的な内容であったため Qiita で公開し、URLを社内Wikiに記載 このように、「アウトプットのためにネタを考える」のではなく、「業務の中から外部へアウトプット可能な箇所を切り出す」ことで、ネタを考える時間も、別途資料を作成する時間もできる限り省略するように意識していました。 「何かアウトプットしなくては」という意識だとハードルが高いと感じてしまいますが、普段の業務の中で自然と外部へのアウトプットが生まれるよう意識していたことで、アウトプットのハードルを低く保ち続けられたと思います。 Microsoft MVPの審査の際に提出した文章 他のMVP受賞者の方 を参考に、私もMVPの審査の際に提出した文書を公開したいと思います。 なぜMVPになりたいのか? コミュニティへの良い影響力を強めていきたいからです。 コミュニティのことを強く意識するきっかけとなったのは、2019年11月にPASS SUMMITへ参加したことです。学んだことをみんなが積極的にコミュニティへ還元することでPASSコミュニティが強くなり、それによってスピーカー自身もコミュニティから有益なフィードバックをもらえるという正のサイクルがうまく回っていると感じました。この経験から、私自身も学んだことをコミュニティに還元していくことで日本のSQL Serverコミュニティをより強く、より良いものにしていきたいと思うようになりました。MVPに認定いただけることで、今後のコミュニティへの貢献活動のさらなるモチベーション向上につながると思っています。 この1年間のコミュニティへの貢献の中で最もインパクトが大きいものは? 勤務している会社が展開している、日本最大級のファッションECサイト「ZOZOTOWN」で生じたSQL Serverに関する障害と、それを調査して原因を解消させた話をコミュニティに共有したことです。 db tech showcase2019 での登壇と、審査期間ではないですが会社の テックブログ (1万PV、120はてブ、Facebook250シェア)と、ユーザーコミュニティの勉強会での登壇を通して、多くの人々に大規模なSQL Serverを使ったシステムにおける障害事例や調査ノウハウを共有しました。登壇内容は、他社の教育資料として活用されているそうです。今までのコミュニティに持ち込まれていなかった情報を共有する、ということを日ごろから意識しており、ZOZOTOWNはSQL Serverを使ったサービスとしては日本最大級であり、そこで生じた生々しい障害と調査の過程を公開するということは今までのコミュニティではあまりされていなかったと思います。他にも、ZOZOTOWNを運用していく中で生じた課題と解決策についてブログで発信しております。例えば、 こちらの記事 や、 こちらの記事 などです。 今後コミュニティに対してどういった影響を与えていきたいか? 私がSQL Serverコミュニティに足りないと感じている点は、実務に直結する有益なノウハウの公開が不足している点です。 この問題点を踏まえて、たとえば このブログ記事 のように、単純に拡張イベントの使い方ではなく、「特定の目的をもった拡張イベントの設定方法と、解析方法」というように、直接実務で使えるようなノウハウをひとまとめの記事にして共有することを意識しています。 他の例として、 こちらの記事 と GitHub で公開しているクエリのように、ZOZOTOWNの運用経験から得られた情報後追いのためのDMVのダンプクエリの仕組みを整備して公開しております。 他の例として、 こちらのブログ のように、メンテ無しでプロダクション環境でのデプロイを成功させるためのノウハウについて、自社の高トラフィックなWebサイトでも成功例のある方法を紹介しています。 今年も、同様の貢献をコミュニティに対して行っていきます。合わせて、自身が最も得意としているPerformance Tuningについて、ウェビナーの講師として解説動画を収録して公開する予定です。日本のSQL Serverのチューニングレベルを引き上げていくために、ビデオの公開やユーザ勉強会でチューニングまわりの情報提供なども実施していきます。 コミュニティをより良くしていくために現在行っている活動は? ユーザーコミュニティの勉強会やDBカンファレンスで定期的に登壇したり、技術的な記事を定期的に執筆したりすることで、会社の業務を通して得たSQL Serverの知見をコミュニティに還元しています。 Microsoft MVP日本事務局の方からのコメント 今回のブログ執筆にあたり、Microsoft MVP日本事務局の森口様からコメントをいただきました。森口様、素敵なコメントをありがとうございました! Microsoft森口様より 国内でも有数のアクセスを集めるECサイト運営に関わるお仕事を通じて得たユニークな知見を登壇およびブログ寄稿などコミュニティ活動を通じて積極的に発信いただき、多くのデータベースエンジニアの学びに寄与されたご貢献により、この度廣瀬さんにData PlatformカテゴリでMicrosoft MVPアワードを授与いたしました。 業務で学んだ実践的なSQL Serverの知識を、社内で共有するだけにとどまらず社外のコミュニティメンバーの技術の学習のためにご尽力いただけますと、今後さらにSQL Serverを活用したい方への貴重な情報源となります。ひいては日本全体のSQL Serverコミュニティの力となる、大変ありがたい活動です。今後はぜひMVPとしてさらにコミュニティ活動をお楽しみいただくとともに、マイクロソフトとのコラボレーションにより製品やコミュニティをより良くするためにお力をお借りできましたら幸いです。 まとめ Microsoft MVPの受賞できたことはとても喜ばしく、アウトプットを継続していくためのモチベーションにもつながりました。これからもコミュニティへ自分が良い影響を与えていけるように考えながら活動していきたいと思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは、SRE部MA基盤チームの谷口( case-k )です。私達のチームでは、データ連携基盤の開発・運用をしています。 データ基盤には大きく分けて2種類あり、日次でデータ連携してるものとリアルタイムにデータ連携しているものがあります。本記事ではリアルタイムデータ連携基盤についてご紹介します。 既存のデータ連携基盤の紹介 リアルタイムデータ連携基盤の紹介 なぜ必要なのか 活用事例の紹介 データ連携の仕組みと課題  リプレイス後のリアルタイムデータ連携基盤 SQL Serverの差分データの取り方を検討 アーキテクチャ概要と処理の流れ Fluentdのプラグインを使った差分データの取得 Dataflowでメッセージの重複を排除 Dataflowで動的にBigQueryの各テーブルに出力 Pub/Subのメッセージ管理 イベントログ収集基盤 個人情報の取り扱い ビルド・デプロイ戦略 監視 データの欠損 データの遅延 性能評価 まとめ 既存のデータ連携基盤の紹介 まず既存のデータ連携基盤について簡単にご紹介させていただきます。 既存のデータ連携基盤ではオンプレ環境やクラウドにあるデータをBigQueryへ日次で1回連携しています。ETLツールはSQL Server専用の「bcp」とTreasure Dataが開発しているOSSである「Embulk」を使っています。まずbcpを使い、オンプレ環境の基幹データベース内のテーブルを中間データベースへ連携します。中間データベースへ連携されたデータはEmbulkを使って、BigQueryへ連携されます。この際秘密情報のハッシュ化なども行っています。 これらの処理はワークフローエンジンで制御されていてTreasure Dataが開発しているOSSであるDigdagを使っています。余談ですがZOZOテクノロジーズにはDigdagのコントリビューターが7人もいます! リアルタイムデータ連携基盤の紹介 なぜ必要なのか これまでZOZOテクノロジーズでは日次でBigQueryへデータを連携していました。最近では機械学習を使った案件も増えてきており、リアルタイムなデータを必要とするサービスが増えてきています。機械学習の他にも配信基盤では商品が残り1点になったタイミングで通知を行う仕組みがあります。このような要件に対応するためには商品の在庫状況をリアルタイムで連携する必要があります。その他にも、施策のモニタリングや不正利用を素早く検知したいなど様々な案件があり、ZOZOテクノロジーズでもリアルタイムデータ連携基盤を構築することになりました。 活用事例の紹介 リアルタイムデータ連携基盤は検索パーソナライズ基盤の商品在庫の連携で使われています。検索パーソナライズとはユーザごとに商品をおすすめ順で紹介する機能となります。商品在庫がないとユーザに対してレコメンドしたにもかかわらず、商品が既に売り切れているといった機会損失が起きてしまいます。このような要件から検索パーソナライズ基盤ではリアルタイムに商品の在庫状況を知る必要があります。 データ連携の仕組みと課題 リアルタイムデータ連携基盤はオンプレ環境からGCP環境まで多段にレプリケーションを行いデータ連携をしています。SQL ServerからKafkaへの差分連携にはQlik Replicateを採用してます。 Qlik ReplicateはSQL ServerからCDCを取得し、解析可能なメッセージの形へ変換する役割をになっています。CDCとはChange Data Captureの略で、データベース内で行われた変更履歴を追うことができる機能です。オンプレ環境のSQL Serverはバージョンが古くCDCを使うことができないため、Compute Engine上にSQL Serverを立てて差分データを取得しています。 https://www.qlik.com/us/attunity www.qlik.com techblog.zozo.com 多段にレプリケーションを行うことで高頻度連携を実現しようとしましたが、運用する過程でデータの欠損や遅延が課題としてあがりました。 データの遅延 オンプレ環境からBigQueryまでに多段にレプリケーションを行うことで10分から30分程度の遅延が発生していました。 データの欠損 既存の処理系にメモリリークがあり、定期的な再起動によるデータの欠損も発生していました。 コスト インフラ費用も月額で約200万円程度かかっており汎用的な基盤として使うには課題がありました。  リプレイス後のリアルタイムデータ連携基盤 既存のリアルタイムデータ連携基盤の課題を解決するため、私たちのチームでリプレイスをすることになりました。 SQL Serverの差分データの取り方を検討 新規に作成するリアルタイムデータ連携基盤では、差分の取得にSQL ServerのChange Trackingを採用しました。 Change TrackingとはCDCのようにSQL Serverの差分データを取得する仕組みです。CDCとの違いはCDCが非同期的な連携であるのに対しChange Trackingは同期的に連携します。CDCよりもリアルタイム性をもって連携できる一方で、変更履歴などは取得はできません。取得できるのは削除や更新、追加といった更新処理の内容や更新バージョン、それと更新のあった主キーのみです。 具体的には、次のようにして差分データの最新の状態を取得しています。 SELECT a.SYS_CHANGE_OPERATION as changetrack_type, a.SYS_CHANGE_VERSION as changetrack_ver, #{columns} FROM CHANGETABLE(CHANGES #{@tablename}, @前回更新したバージョン) AS a LEFT OUTER JOIN #{@tablename} ON a.#{@primary_key} = b.#{@primary_key} 差分データの取得方法としてはChange Tracking以外にも、テーブルの更新タイムスタンプを参照する方法やCDCを使う方法も検討しました。 しかし、更新タイムスタンプは付与されているテーブルが非常に少なく、付与されていても更新されないタイムスタンプが多くありました。CDCもオンプレ環境にCDCが使える2016以降のSQL Serverがほとんどありませんでした。また、非同期的なCDCより同期的なChange Trackingの方が高速にデータを取得できます。 このような理由からChange Trackingを使って差分データを取得することになりました。 アーキテクチャ概要と処理の流れ ここからはリプレイス後のリアルタイムデータ連携基盤のアーキテクチャ概要と処理の流れについてご紹介します。 アーキテクチャの全体図は次の通りです。 Fluentdのプラグインを使った差分データの取得 Change Trackingの実行からPub/Subへのメッセージ転送はFluentdのプラグインを使っています。 冗長構成を実現するため、Compute Engine 2台にプラグインをデプロイしています。片方のインスタンスに問題が起きても、もう片方が生きていればデータの欠損が起きない仕組みとなっています。 オンプレ環境からGCP環境へ高速にデータ連携できるよう専用線としてDedicated Interconnectを使っています。多段にレプリケーションを行ったことによる遅延が課題だったので、最も根元の基幹データベースからデータを取得するようにしています。取得したデータはPub/Subのアウトプットプラグインを使い転送されます。 cloud.google.com プラグインでは次のようにChange Tracking実行時にレコード単位でユニークとなるメッセージIDを生成しています。生成されたメッセージIDはDataflowのメッセージの重複排除で使います。 BigQueryで主キーの最新の状態を集計できるようChange Trackingのバージョンも渡しています。Dataflowでテーブル名を考慮してBigQueryへ書き込みができるようテーブル名も渡しています。 query = """ declare @last_synchronization_version bigint; SET @last_synchronization_version = #{ changetrack_ver } ; SET lock_timeout #{ @lock_timeout } SELECT CONCAT(' #{ @tablename } ','-',a. #{ @primary_key .join( ' , ' ).gsub( ' , ' , ' ,a. ' ) } ,a.SYS_CHANGE_VERSION) as massage_unique_id, ' #{ @tablename } ' as table_name, ' #{ @changetrack_interval } ' as changetrack_interval, ' #{ Time .now.utc } ' as changetrack_start_time, a.SYS_CHANGE_OPERATION as changetrack_type, a.SYS_CHANGE_VERSION as changetrack_ver, #{ columns } FROM CHANGETABLE(CHANGES #{ @tablename } , @last_synchronization_version) AS a LEFT OUTER JOIN #{ @tablename } ON a. #{ @primary_key } = b. #{ @primary_key } """ Pub/Subのアウトプットプラグインでは差分データに加えてattributeにDataflowの重複排除で使うメッセージIDを渡しています。Dataflowの重複排除は次章でご紹介します。 <system> workers ' <worker count> ' < / system> <worker 1> <source> / / Input Plugin < / source> <match '<tag_name>' @type gcloud_pubsub project " #{ ENV [ ' PROJECT_ID ' ] } " key /us r/src/app/config/gcp_credential.json topic " projects/ #{ ENV [ ' PROJECT_ID ' ] } /topics/<topic-name> " autocreate_topic false max_messages 1000 max_total_size 9800000 max_message_size 4000000 attribute_keys [ " message_unique_id " ] // Buffer Plugin < / match> < / worker> github.com docs.fluentd.org Dataflowでメッセージの重複を排除 Fluentd2台の冗長構成によるデータの重複はDataflowのidAttributeを使い重複を排除しています。idAttributeを使うことで、プラグインで付与したメッセージIDを参照して10分以内であれば同じメッセージの重複を排除します。Dataflowを使うとPub/Subで自動的に付与されるメッセージIDの重複は自動で排除でき、at least onceを採用しているPub/Subとは相性の良いツールです。しかし、パブリッシャーが複数回同じメッセージを送った場合、Pub/Subでは異なるメッセージと扱われるため重複の自動排除はできません。このような理由からメッセージのユニーク性を担保したい場合はDatafflowでidAttributeを使います。idAttributeを使うことで2台のFluentdから送られてくるデータの重複を排除しています。 次のサンプルはidAttributeを使ってメッセージの重複排除をする例です。 public static PipelineResult run(Options options) { // Create the pipeline Pipeline pipeline = Pipeline.create(options); pipeline .apply( "Read PubSub Events" , PubsubIO.readMessagesWithAttributes() .withIdAttribute( "message_unique_id" ) .fromSubscription(options.getInputSubscription())) .apply( "Filter Events If Enabled" , ParDo.of( ExtractAndFilterEventsFn.newBuilder() .withFilterKey(options.getFilterKey()) .withFilterValue(options.getFilterValue()) .build())) .apply( "Write PubSub Events" , PubsubIO.writeMessages().to(options.getOutputTopic())); return pipeline.run(); } cloud.google.com cloud.google.com Dataflowで動的にBigQueryの各テーブルに出力 Pub/Subに送られ重複排除されたメッセージはDataflowを使ってBigQueryのテーブルに書き込まれます。DataflowのDynamic Destinationsを使うとメッセージ内のテーブル名に基づいて、出力先のテーブルを動的に振り分けることが可能です。そのため、Dynamic Destinationsを使うことで、1つのDataflowで複数テーブルのデータ連携ができるようになりインフラコストを抑えることができます。 なお、DataflowのDynamic Destinations機能は現時点だとJavaのみサポートしてます。 次のサンプルはDynamic Destinationsを使ってストリーム内のテーブル名を参照してBigQueryのテーブルに書き込む例です。監視や分析用の遅延時間を計測するため、BigQueryへのインサート時刻も取得しています。 WriteResult writeResult = convertedTableRows.get(TRANSFORM_OUT) .apply( BigQueryIO.<TableRow>write() .to( new DynamicDestinations<TableRow, String>() { @Override public String getDestination(ValueInSingleWindow<TableRow> elem) { return elem.getValue().get( "table_name" ).toString(); } @Override public TableDestination getTable(String destination) { return new TableDestination( new TableReference() .setProjectId( "project_id" ) .setDatasetId( "dataset_name" ) .setTableId( "table_prefix" + "_" + destination), // destination: table name "destination table" + destination); } @Override public TableSchema getSchema(String destination) { TableSchema schema = new TableSchema() switch (destination) { case "table_a" : schema.setFields(ImmutableList.of( new TableFieldSchema().setName( "column" ).setType( "STRING" ).setMode( "NULLABLE" ))); break ; case "table_b" : schema.setFields(ImmutableList.of( new TableFieldSchema().setName( "column" ).setType( "STRING" ).setMode( "NULLABLE" ))); break ; default : } return schema } }) // BigQuery Insert Time .withFormatFunction((TableRow elem) -> elem.set( "bigquery_insert_time" , Instant.now().toString())) .withoutValidation() .withCreateDisposition(CreateDisposition.CREATE_NEVER) .withWriteDisposition(WriteDisposition.WRITE_APPEND) .withExtendedErrorInfo() .withMethod(BigQueryIO.Write.Method.STREAMING_INSERTS) .withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors())); www.case-k.jp beam.apache.org Pub/Subのメッセージ管理 重複排除されてPub/Subに送られてきたメッセージは別のサブスクライバーからも参照できるよう7日間メッセージを保持しています。新しくサブスクライバーを作ればBigtableなどBigQuery以外にも出力できるようになっており、Dataflowのウィンドウ処理等でリアルタイムに特徴量生成などもできるようになっています。 次のサンプルはPub/Subでメッセージを7日間保持するTerraformの例となります。retain_acked_messagesをtrueとすることでサブスクライブされたメッセージを破棄せずに保持します。 message_retention_durationはメッセージの保有期間を決めることができます。なお、メッセージの保有期間は最大で7日です。 resource "google_pubsub_subscription" "message_hub" { name = "message_hub" topic = google_pubsub_topic.message_hub.name # subscribe from multiple subscriber message_retention_duration = "604800s" retain_acked_messages = true ack_deadline_seconds = 60 } cloud.google.com beam.apache.org イベントログ収集基盤 まだ構想段階ではありますが、データ量の多いイベントログのリアルタイムデータ連携基盤も作ろうとしています。イベントログについてもPub/Subに投げてもらうのが理想的ですが、クライアント側の負担も考慮し現在検討中です。 個人情報の取り扱い BigQueryとPub/Subに保持される個人情報や秘密情報はアクセスできるユーザを制限しています。 BigQueryではカラムレベルでのアクセス制御を行い、Pub/Subはトピック単位でアクセス制御をしています。カラムレベルのアクセス制御を行うため、ポリシータグを個人情報や秘密情報のカラムに付与しています。ポリシータグとはBigQueryのテーブルに対してカラムレベルのアクセス制御を行うリソースです。 ポリシータグのカラム付与はTerraformで次のようにしてできます。ポリシータグ自体を作ることはまだTerraformではサポートされていないようです。 resource " google_bigquery_table " " table-name " { dataset_id = google_bigquery_dataset. < dataset - name > .dataset_id table_id = " <table-name> " schema = << EOF [ { " name " : " column-name> ", " type " : " STRING ", " mode " : " NULLABLE ", " policyTags " : { " names " : [ " projects/<project-id>/locations/<location>/taxonomies/<taxonomies-id>/policyTags/<policy-tag-id> " ] } } ] EOF } cloud.google.com github.com Pub/SubはDataflowで個人情報や秘密情報をNULL置換したトピックを作ろうと考えています。トピック単位で参照ユーザを制限することで、秘密情報を必要としないサブスクライバーからは参照できないようにします。 cloud.google.com ビルド・デプロイ戦略 FluentdのプラグインとDataflowのビルド・デプロイ方法についてご紹介できればと思います。CI/CDツールとしてはCircleCIを使っています。Fluentdのプラグインはコンテナイメージを作り、作られたコンテナイメージをContainer RegistryにPUSHしています。Container RegistryのコンテナイメージはCompute Engine起動時にPULLされデプロイされます。データ欠損や遅延が発生しないよう2台のCompute Engineを1台ずつ再起動させ無停止でデプロイできるようにしています。 module " gce-container " { source = " Terraform-google-modules/container-vm/google " version = " ~> 2.0 " container = { image = " gcr.io/${var.project}/<image-name> " tty : true } restart_policy = " Always " } resource " google_compute_instance " " compute engine " { name = " name " machine_type = " n2-custom-4-10240 " zone = " asia-northeast1-a " boot_disk { initialize_params { image = module.gce - container.source_image size = 500 } } metadata_startup_script = " #!/bin/bash /usr/bin/docker-credential-gcr configure-docker EOF " metadata = { gce - container - declaration = module.gce - container.metadata_value google - logging - enabled = " true " google - monitoring - enabled = " true " } service_account { email = " ${google_service_account.tracker_app.email} " scopes = [ " https://www.googleapis.com/auth/cloud-platform ", ] } } Dataflowはカスタムテンプレートをビルドし、既存のパイプラインの更新を行います。 DataflowのカスタムテンプレートはGoogle提供のテンプレートをベースにカスタマイズしています。ビルド時にenableStreamingEngineオプションを利用すると使用するディスク容量を420GBから30GBにインフラ費用を抑えることができます。 次のコードはテンプレートをビルドする際にenableStreamingEngineオプションを指定する例です。 mvn - Pdataflow - runner compile exec : java \ - Dexec.mainClass = com.google.cloud.teleport.templates.PubsubToPubsub \ - Dexec.args = " --project= ${project_id} \ --tempLocation=gs:// ${project_id} /tmp \ --templateLocation=gs:// ${project_id} /templates/<template-name> \ --experiments=enable_stackdriver_agent_metrics \ --enableStreamingEngine \ --runner=DataflowRunner " cloud.google.com cloud.google.com github.com ビルドされたテンプレートはupdateオプションを使い既存のパイプラインの更新を行っています。互換性チェックにより、中間状態やバッファデータなどが前のジョブから置換ジョブに確実に転送することが可能です。 次のコードはPythonクライアントを使ってパイプラインを更新する例です。 def create_template_request (self, job_name, template_path, parameters, environment, update_options): request = self.dataflow.projects().templates().launch( projectId = self.project_id, location = 'us-central1' , gcsPath = template_path, body = { "jobName" : job_name, "parameters" : parameters, "environment" : environment, "update" : update_options } ) return request def deploy_dynamic_destinations_datatransfer (self, active_jobs): job_name= 'dynamic_destinations_datatransfer' template_name = 'PubSubToBigQueryDynamicDestinationsTemplate' template_path = "gs://{}/templates/{}" .format(self.project_id, template_name) input_subscription = 'message_hub' output_default_table = 'streaming_datatransfer.streaming_dynamic_changetracktransfer' parameters = { "inputSubscription" : "projects/{}/subscriptions/{}" .format(self.project_id, input_subscription), "outputTableSpec" : "{}:{}" .format(self.project_id, output_default_table), "autoscalingAlgorithm" : "THROUGHPUT_BASED" } environment = { "machineType" : 'n2-standard-2' , "maxWorkers" : 5 } update_options= 'false' if 'dynamic_destinations_datatransfer' in active_jobs: update_options= 'true' request = self.create_template_request(job_name, template_path, parameters, environment, update_options) request.execute() cloud.google.com 監視 監視対象としてはデータの欠損や遅延が発生してないかCloud LoggingやMonitoring、Redashを使い監視しています。 データの欠損 データの欠損はリトライログとメモリの使用率を確認してます。リトライの上限を超えるとデータが欠損してしまうのと、メモリの使用率が100%に達すると基幹データベースへ接続ができなくなるからです。 次のようにしてFluentdのプラグイン側でリトライ時にログを出力しています。 def execute_changetracking (changetrack_ver) try = 0 begin try += 1 query = generate_query(changetrack_ver) changetrack_results = execute_query(query) if !changetrack_results.nil? changetrack_results.each_slice( @batch_size ) { |rows| es = MultiEventStream .new rows.each do |r| r[ " changetrack_end_time " ] = Time .now.utc es.add( Fluent :: Engine .now, r) if changetrack_ver < r[ " changetrack_ver " ] then changetrack_ver = r[ " changetrack_ver " ] end end router.emit_stream( @output_tag , es) } update_changetrack_version(changetrack_ver) end rescue => e puts " Write Retry Cnt: #{ try } , Table Name: #{ @tablename } , Error Message: #{ e }" sleep try** 2 retry if try < @retry_max_times raise end end リトライ時のログはCompute Engine起動時にデプロイしたCloud Loggingエージェントでログを取得しています。 metadata = { gce - container - declaration = module.gce - container.metadata_value google - logging - enabled = " true " google - monitoring - enabled = " true " } 次のようにしてCloud Loggingでメトリクスを作ります。今回リトライ回数が10回でアラートを通知するように設定しました。 resource " google_logging_metric " " retry_error_tracker_a_metric " { name = " retry-error-tracker-a/metric " filter = " resource.type=\"gce_instance\" severity>=DEFAULT jsonPayload.message: \"Write Retry Cnt: 10\" resource.labels.instance_id: \"${google_compute_instance.streaming_datatransfer_a.instance_id}\" " metric_descriptor { metric_kind = " DELTA " value_type = " INT64 " } } 作成したメトリクスを使いCloud Monitoringでアラートを通知します。 resource " google_monitoring_alert_policy " " tracker_a_retry_error_alert_policy " { display_name = " Tracker A Retry Error " depends_on = [ google_logging_metric.retry_error_tracker_a_metric ] combiner = " OR " conditions { display_name = " condition " condition_threshold { filter = " metric.type=\"logging.googleapis.com/user/retry-error-tracker-a/metric\" resource.type=\"gce_instance\" " duration = " 0s " comparison = " COMPARISON_GT " aggregations { alignment_period = " 60s " per_series_aligner = " ALIGN_DELTA " } trigger { count = 1 } threshold_value = 0 } } enabled = true # gcloud alpha monitoring policies list -- project = streaming - datatransfer - env notification_channels = [ " projects/${var.project}/notificationChannels/${var.slack_notification_channel_id} " ] } メモリが枯渇するとプラグインから基幹データベースへのデータ取得が失敗するので、メモリ使用率が80%を超えた場合アラートを投げるよう設定してます。 resource " google_monitoring_alert_policy " " tracker_a_memory_alert_policy " { display_name = " Tracker A Memory Utilization " combiner = " OR " conditions { display_name = " condition " condition_threshold { filter = " metric.type=\"agent.googleapis.com/memory/percent_used\" resource.type=\"gce_instance\" resource.labels.instance_id=\"${google_compute_instance.streaming_datatransfer_a.instance_id}\" metric.label.\"state\"=\"used\" " duration = " 60s " comparison = " COMPARISON_GT " aggregations { alignment_period = " 60s " per_series_aligner = " ALIGN_MEAN " } trigger { count = 1 } threshold_value = 80 } } enabled = true notification_channels = [ " projects/${var.project}/notificationChannels/${var.slack_notification_channel_id} " ] } データの遅延 データの遅延はRedashを使い定期的にクエリを投げて監視しています。 データの遅延はChange Trackingの開始時間とBigQueryのインサート時刻の差分を確認しています。不正なレコードが混在した際はBigQueryの_error_recordsテーブルに書き込まれるため、書き込みを検知してアラートを通知するようにします。 また、CPUの使用状況も遅延に影響があるため、メモリ使用率と同様に監視しています。 filter = " metric.type=\"agent.googleapis.com/cpu/utilization\" resource.type=\"gce_instance\" resource.labels.instance_id=\"${google_compute_instance.streaming_datatransfer_a.instance_id}\" metric.label.\"state\"=\"used\" " 性能評価 リプレイスよってデータ欠損もなくなり、遅延時間としても取得のインターバルを除けば数秒程度でデータ連携を行うことができるようになりました。 次の図はChange Trackingで取得したレコード数と遅延時間(秒)の関係となります。取得するレコード数が多いと遅延しますが、40万レコードほどの更新でも5分以内に連携できる基盤を作ることができました。 またコスト面でも月間で約200万円ほどかかっていましたが約5万円程度にできました。 まとめ 今回リアルタイムデータ連携基盤についてご紹介しました。 現在ZOZOTOWNでは、リアルタイムデータを活用した案件が増えてきています。この記事を読んで、もしご興味をもたれた方は是非採用ページからお申し込みください。 https://tech.zozo.com/recruit/ tech.zozo.com また、8/27(木)にリアルタイムデータ連携基盤含めMAの取り組みについてのイベントを行いますのでぜひご参加ください。 zozotech-inc.connpass.com
アバター
こんにちは。ECプラットフォーム部のMA(マーケティングオートメーション)アプリケーションチームで、社内向けのマーケティング運用ツールを開発している長澤( @snagasawa_ )です。 先日、日本時間の2020年7月18日に Vue 3.0のRelease Candidate(v3.0.0-rc.1) がリリースされ、今後は最終リリースまで主要なAPIのbreaking changeは想定していないとのアナウンスがされました。アナウンスを受け、現在社内ツールで進めているOptions APIからComposition APIへの移行で得られたTipsについて紹介します。 この記事では公開時点でのVue 3.0 betaへのアップグレードの方法と、Vue + TypeScriptでのOptions APIからComposition APIへの移行のTipsについてまとめました。Vue 3.0へのアップグレードを検討されている方、またはComposition API単体での導入を検討されている方の参考になりましたら幸いです。なお、あくまで公開時点での情報であるため、今後は更新される可能性があることをご留意ください。 Vue CLIによるVue 3.0 betaへのアップグレード方法 Vue本体のアップグレード マイグレーションヘルパー Vue 3.0 へアップグレード可能かどうかの判断 Vue 3.0 betaへのアップグレードを断念した理由 Composition APIのメリットと導入理由 Options APIからComposition APIへの書き換え 書き換えの順番 Vue.extend -> defineComponent data -> reactive methods -> Functions v2.x computed -> v3.0 computed emit -> context.emit props -> setup(props) Composition Function methodsやcomputedの関数化 引数の型はRef<T> まとめ さいごに Vue CLIによるVue 3.0 betaへのアップグレード方法 Vue本体のアップグレード 初めにVue CLI(v4.5.3)で利用可能(または提供予定)なVue 3.0 betaへのアップグレードの方法を紹介します。npmかyarnで vue@next をインストールすることもできますが、Vue 3.0 betaを試すためのプラグインをインストールする方法もあります。 github.com vue add vue-next このコマンドでは vue-cli-plugin-vue-next というプラグインをインストールします。 このプラグインではVue本体だけでなく、Vuex, Vue Routerもアップグレードされます。また、コードが自動で書き換えられ、 src/main.ts, src/store/index.ts, src/router/index.ts がそれぞれのbetaのAPIに書き換えられます。以下はサンプルコードのdiffです。 diff --git a/src/main.ts b/src/main.ts index e9c1f28..39f0f54 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,6 @@ -import Vue from 'vue'; +import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import store from './store'; -Vue.config.productionTip = false; - -new Vue({ - router, - store, - render: (h) => h(App), -}).$mount('#app'); +createApp(App).use(router).use(store).mount('#app'); diff --git a/src/router/index.ts b/src/router/index.ts index 25a46df..f6e682c 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,9 +1,6 @@ -import Vue from 'vue'; -import VueRouter, { RouteConfig } from 'vue-router'; +import { RouteConfig, createRouter, createWebHashHistory } from 'vue-router'; import Home from '../views/Home.vue'; -Vue.use(VueRouter); - const routes: Array<RouteConfig> = [ { path: '/', @@ -20,7 +17,8 @@ const routes: Array<RouteConfig> = [ }, ]; -const router = new VueRouter({ +const router = createRouter({ + history: createWebHashHistory(), routes, }); diff --git a/src/store/index.ts b/src/store/index.ts index 9ea7685..73b4b8d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,9 +1,6 @@ -import Vue from 'vue'; import Vuex from 'vuex'; -Vue.use(Vuex); - -export default new Vuex.Store({ +export default Vuex.createStore({ state: { }, mutations: { このように一括でのアップグレードとコードの自動変換を行ってくれますが、あくまでトライアウト用のプラグインのようです。 issueのコメント によるとVue CLIの vue create コマンドでVue 3を選択可能になった現在はこのプラグインが不要になるとのことでした。開発も最終コミットが5月でそれ以降は中断されているようで、このプラグインの最新バージョンのv0.1.3ではVue 3.0 betaのバージョンが最新ではないことに注意してください。 とはいえ、既存プロジェクトでの自動コード変換は現状では他の手段で提供されていないようなので、そのために利用してみるのもよいかもしれません。 マイグレーションヘルパー 次に、マイグレーションヘルパーの提供が予定されていますが、こちらは8月21日現在では開発中のため利用できません。アップグレードが急ぎでない、もしくは比較的規模が大きいプロダクトは、こちらの完成を待つのが得策かもしれません。 Where should I start in a migration? Start by running the migration helper (still under development) on a current project. v3.vuejs.org 過去の v1.xからv2.0へのマイグレーションヘルパー ではdeprecatedな記法をwarningとして出力できます。おそらく、これに近いものが提供されるものと予想しています。 以上の方法でVue 3.0 betaやパッケージのアップグレードの一部を簡略化できます。最終リリース以降には開発中のマイグレーションヘルパーを含めて、より便利なアップグレード方法が提供されることを期待したいですね。 Vue 3.0 へアップグレード可能かどうかの判断 前述のようにアップグレードのサポートは提供されているとはいえ、Vue 3.0へのアップグレードはGlobal APIの書き換えが必要なため、影響範囲はプロダクト全体に及びます。当然のことながら、アップグレード後のプロダクトの正常な挙動を担保しなければなりませんが、中〜大規模であった場合にQAのリソースの確保が難しくなることも予想されます。 以下の観点をクリアできている場合には、Vue 3.0へのアップグレードも可能でしょう。 Vue 3.0に未対応のパッケージを利用していない テストコードが十分に書かれている プロダクトの規模がそれほど大きくない、あるいはQAのリソースを十分に確保できる 一方でVue 3.0へのアップグレードが困難ではあるものの、Vue 3.0 betaの新APIを採用したい場合は Composition APIをプラグイン としてその一部をコンポーネント単位で利用することもできます。ここからは冒頭で触れた社内ツールの開発において進めているComposition APIへの移行について紹介します。 Vue 3.0 betaへのアップグレードを断念した理由 社内ツールでComposition APIを採用した一方で、Vue 3.0 betaへのアップグレードは一旦断念することになりました。理由としては利用している VuetifyがVue 3.0に未対応 だったためです。なお、Vuetifyの Roadmap では2020 Q3/Q4での対応を予定しているとのことです。 Composition APIのメリットと導入理由 Composition APIのメリットはいくつかありますが、特に採用の決め手となったのはロジックの関心ごとにコードをまとめやすくなることでした。 v2.xのOptions APIの課題は、コードがコンポーネントの this に依存するため関心ごとの単位でまとめることが難しく、Options APIのoptions(data, computed, methodsなど。以下: v2 options)にコードが分散しがちであることでした。 実際に開発している社内ツールでは、入力フォームの画面でフィールドが入れ子になるようなケースが複数ありました。フォームは複数のコンポーネントに分割し、関数はシンプルな実装を心がけていましたがコードの分散は避けられませんでした。そのため、機能追加や修正を行う際にコードの中からしばしば該当のコードを探す必要があり、致命的ではないものの可読性に課題感を持っていました。 そんな状況でComposition APIのRFCを読み、実装を試してみたところ手応えがあったため、この課題を解消すべく採用を決めました。 Options APIからComposition APIへの書き換え ここからはサンプルコードを交えて、コンポーネントをOptions APIからComposition APIに書き換えていく際のTipsについて紹介します。 インストール方法はGitHubリポジトリの README にある通りなので省略します。 書き換えの順番 Composition APIへの移行で念頭に置いておきたいことは Options APIと併用可能 という点です。 これはコンポーネント内の依存関係を考慮して順番に書き換えを進めることで、そのすべてが終わらなくとも、途中でテストコードや動作確認で正しい挙動を確認しつつ進められることを意味しています。特に大きなコンポーネントの際に、すべてを書き換え終えたつもりでようやく動作確認を始めたものの、正常に動作せずバグ修正に苦心するという事態を避けられるかもしれません。 併用した場合の処理の順番はComposition APIの setup が先に呼ばれ、そのあとv2 optionsが解決されます。 setup 内では this は undefined でなおかつv2 optionsの前に呼ばれるため、 setup 内からv2 optionsのプロパティにはアクセスできません。逆に setup でreturnしたプロパティはv2 optionsからアクセスできます。したがって、移行の流れとしてはコンポーネント内で他の実装に依存していないものから setup 内に移していくとよさそうです。 以下が順番の例です。順番には絶対解はなく、ある程度入れ替え可能です。 Vue.extend -> defineComponent data -> reactive methods -> Functions v2.x computed -> v3.0 computed emit -> context.emit props -> setup(props) Composition Function サンプルコードは去年の アドベントカレンダーで書いた記事 のリポジトリを元にOptions APIで実装し直したものです。 github.com < template > < div > < div > < input v-model = "taskName" type= "text" / > < button @click = "addTask" > Add < /button > < /div > < div >< input v-model = "searchText" type= "text" / > Search < /div > < div class= "task-list-wrapper" > < ul > < h4 > DOING < /h4 > < li v- for= "(task, index) in doingTasks" :key = "index" > < input type= "checkbox" :checked = "task.status" disabled / > < label > {{ task.name }} < /label > < button @click = "toggleTask(task, true)" > toggle < /button > < /li > < /ul > < ul > < h4 > COMPLETED < /h4 > < li v- for= "(task, index) in completedTasks" :key = "index" > < input type= "checkbox" :checked = "task.status" disabled / > < label > {{ task.name }} < /label > < button @click = "toggleTask(task, false)" > toggle < /button > < /li > < /ul > < /div > < /div > < /template > < script lang = "ts" > import Vue from 'vue' ; import { Task } from '../types' ; interface Data { taskName: string ; searchText: string ; tasks: Task [] ; } export default Vue.extend ( { data: () : Data => { return { taskName: '' , searchText: '' , tasks: [] , } ; } , computed: { doingTasks () : Task [] { return this .searchedTasks.filter ( t => !t. status); } , completedTasks () : Task [] { return this .searchedTasks.filter ( t => t. status); } , searchedTasks () : Task [] { return this .tasks.filter ( t => t.name.includes ( this .searchText )); } , } , methods: { addTask () { this .tasks.push ( { name: this .taskName , status : false , } ); this .taskName = '' ; } , toggleTask ( task: Task , status : boolean ) { const index = this .tasks.indexOf ( task ); this .tasks.splice ( index , 1 , { ...task , status : status } ); } , } , } ); < /script > < style scoped > .task-list-wrapper { display: flex ; justify-content: center ; } < /style > Vue.extend -> defineComponent 初めに Vue.extend を defineComponent に変更します。 Options APIでTypeScriptの型推論のために Vue.extend が必要だったのと同様、Composition APIでも defineComponent が必要になります。 -import Vue from 'vue'; +import { defineComponent } from '@vue/composition-api'; -export default Vue.extend({ +export default defineComponent({ + setup() { + }, 空の setup を定義してv2 optionsの挙動を確認した後、順次この setup にコードを移していきます。 data -> reactive 次に data をreactive関数に書き換えます。 data のinterfaceは reactive の型引数として渡します。 returnでは ...toRefs(state) のように書くのがオススメです。 state をそのままreturnすることもできますが、その場合はtemplateやv2 optionsで state.xxx のように書き換える必要があります。 また、 toRefs に渡さずspread operatorで展開する場合はstateがリアクティブではなくなってしまうため、この場合は toRefs が必須です。 export default defineComponent({ setup { - data: (): Data => { - return { + const state = reactive<Data>({ taskName: '', searchText: '', tasks: [], + }); + + return { + ...toRefs(state), }; }, methods -> Functions methods は関数を setup に切り出し、 this を state に変えてreturnで返すだけです。 export default defineComponent({ setup() { const state = reactive<Data>({ taskName: '', searchText: '', tasks: [], }); + const addTask = () => { + state.tasks.push({ + name: state.taskName, + status: false, + }); + state.taskName = ''; + }; + + const toggleTask = (task: Task, status: boolean) => { + const index = state.tasks.indexOf(task); + state.tasks.splice(index, 1, { ...task, status: status }); + }; + return { ...toRefs(state), + addTask, + toggleTask, }; }, computed: { @@ -60,19 +75,6 @@ export default defineComponent({ return this.tasks.filter(t => t.name.includes(this.searchText)); }, }, - methods: { - addTask() { - this.tasks.push({ - name: this.taskName, - status: false, - }); - this.taskName = ''; - }, - toggleTask(task: Task, status: boolean) { - const index = this.tasks.indexOf(task); - this.tasks.splice(index, 1, { ...task, status: status }); - }, - }, }); v2.x computed -> v3.0 computed Composition APIの computed はgetter関数を引数にとり、computedプロパティを返します。 こちらも書き換えはシンプルで methods とほぼ同じ要領です。 以下がその2つの比較です。 Options API computed: { searchedTasks () : Task [] { return this .tasks.filter ( t => t.name.includes ( this .searchText )); } , doingTasks () : Task [] { return this .searchedTasks.filter ( t => !t. status); } , completedTasks () : Task [] { return this .searchedTasks.filter ( t => t. status); } , } , Composition API const searchedTasks = computed (() => { return state.tasks.filter ( t => t.name.includes ( state.searchText )); } ); const doingTasks = computed (() => { return searchedTasks.value.filter ( t => !t. status); } ); const completedTasks = computed (() => { return searchedTasks.value.filter ( t => t. status); } ); 今回はシンプルなアプリのため、ここまでの変更でv2 optionsはすべて setup 内に書き換えられました。 ここまでの変更に少し手を加えたバージョンのソースコードは以下で確認できます。 github.com emit -> context.emit emit はsetupの第2引数の context から呼び出します。 以下の例はTaskを表示する列をコンポーネント化したコードです。 github.com 注意点として、 emit は camelCase では呼び出せなくなっており、 kebab-case で呼び出す必要があります。 export default defineComponent ( { props: { title: { type : String , required: true } , tasks: { type : Array as () => Task [] , required: true } , } , setup ( props , context ) { const toggleTask = ( task: Task ) => { context.emit ( 'toggle-task' , task ); } ; return { toggleTask , } ; } , } ); props -> setup(props) propsの定義は上の例のようにOptions APIの時と変わらず、setupの第1引数として受け取ることができます。setupの引数に型を指定せずともpropsの型推論が効きます。 Composition Function ここまでの説明でコンポーネント内の主要なAPIの書き換えの流れはおおよそ掴むことができるかと思います。最後にComposition APIによって実装可能なComposition Functionを実装する際のTipsを紹介します。 Composition Functionとは、関連するロジックでまとめられ、カプセル化された関数のことです。Composition APIによって this への依存がなくなり、コードは関心ごとでまとめられるようになりました。まとめられた関数は純粋なJavaScript or TypeScriptの関数として抽出し、他のコンポーネントで再利用しやすくなります。 Notice how all the logic related to the create new folder feature is now collocated and encapsulated in a single function. The function is also somewhat self-documenting due to its descriptive name. This is what we call a composition function. vue-composition-api-rfc.netlify.app methodsやcomputedの関数化 methods や computed を関数でラップ(あるいはカリー化)しておくと、Composition Functionが作りやすくなり、関数合成がしやすくなるというメリットがあります。 下は先述の computed を関数化した例です。 const searchedTasks = (( tasks , text ) => computed (() => { return tasks.value.filter ( t => t.name.includes ( text.value )); } ))( toRef ( state , 'tasks' ), toRef ( state , 'searchText' )); const doingTasks = ( tasks => computed (() => { return tasks.value.filter ( t => !t. status); } ))( searchedTasks ); const completedTasks = ( tasks => computed (() => { return tasks.value.filter ( t => t. status); } ))( searchedTasks ); 先ほどの computed の例に戻って関数化していない例をもう一度見てください。 setup 内であれば関数化しない実装も可能ですが、その場合は他の実装に依存します。例の computed をComposition Function化する場合は、 state.tasks や searchedTasks への依存を関数化した時と同じように引数への変更が必要です。その点、関数化しておくと変更が少なくて済みます。 以下がComposition Functionとして抽出した例です。 import { computed , Ref } from '@vue/composition-api' ; import { Task } from '@/types' ; export default function useFilter ( tasks: Ref < Task [] >) { const doingTasks = computed (() => tasks.value.filter ( t => !t. status)); const completedTasks = computed (() => tasks.value.filter ( t => t. status)); return { doingTasks , completedTasks , } ; } 逆に関数化をしない方がパッと見ではシンプルでわかりやすいというメリットがあるため、Composition Function化の可能性が低い場合は2番目のような実装に留めておくのがよさそうです。 引数の型はRef<T> Composition Functionや関数化された関数で実装する時に、リアクティブな引数を受け取る場合の型定義はどうすべきか疑問に浮かぶかもしれません。 引数次第では推論で Ref<T> や ComputedRef<T> などになる可能性があり、一見すると使い分けるか、もしくはいずれかで統一するといった選択の余地がありそうに見えます。 結論としては Ref<T> で統一すればよさそうですが、理解を深めるために型定義を確認してみましょう。 Ref<T> は ref 、 reactive などでリアクティブ化されたオブジェクトで、 ComputedRef<T> は computed が返すオブジェクトの型です。 ComputedRef<T> は WritableComputedRef<T> をextendsしていますが、 WritableComputedRef<T> は Ref<T> をextendsしているため、引数の型定義と渡される引数の組み合わせがいずれでもTypeErrorにはなりません。 したがって基本となる Ref<T> で統一するとよいでしょう。 interface ComputedRef < T = any > extends WritableComputedRef < T > { readonly value: T ; } interface WritableComputedRef < T > extends Ref < T > { } interface Ref < T = any > { readonly [ _refBrand ] : true ; value: T ; } まとめ Vue 3.0 betaへのアップグレード方法の紹介と、社内ツール開発でのComposition APIへの移行について紹介しました。Composition APIでの実装で最近よく考えることは、コードの再編の自由度が上がった分、コードをまとめる境界をどのように見つけるのがよいかという点です。Composition Functionへの分割をパターン化できるとより開発をスムーズに進められそうです。その点を踏まえて、引き続きVue 3.0と周辺パッケージのキャッチアップと移行を進めていきたいと思います。 さいごに ZOZOテクノロジーズではマーケティングに関連するプロダクトの開発や、フロントエンド開発に興味のあるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! tech.zozo.com また、8/27に、この記事で紹介した社内ツールを含めたMAの取り組みについてのイベントを行いますので、こちらも奮ってご応募ください! zozotech-inc.connpass.com
アバター
こんにちは。ZOZOテクノロジーズZOZOTOWN部 検索チーム 兼 ECプラットフォーム部 検索基盤チームの有村です。 ZOZOTOWNでは 先日公開した記事 の通り、すべての検索をElasticsearchへ置き換えました。置き換え直後は順調に見えたのですが、実際に数%ずつリリースしていく中で一部時間帯、一部リクエストでレスポンス速度の低下がみられました。 本記事ではその解決のために行ったパフォーマンス調査、チューニング方法についてご紹介します。なお、一般的に行われるであろうElasticsearch本体のパラメータチューニングの話ではなく、クエリやmapping、setting面の話がメインとなります。 改善前後の速度について 詳細な内容の前に、本改善によるレスポンス速度の最終的な改善結果を示します。 今回の計測では、一定パターンのリクエストを10秒間繰り返し、95%tileのレスポンス速度をp95、99%tileのレスポンス速度をp99と表しています。 p95 (sec) p99 (sec) 改善前 0.553 0.667 改善後 0.147 0.205 95%tileで 約276% の改善、99%tileで 約225% の改善となりました。 調査方法 環境 今回の検証は以下の環境で行いました。 ツール バージョン Elasticsearch 7.5.1 Kibana 7.5.1 Gatling 3.3.1 Search Profilerの使用 RDBMSのチューニングでは実行計画を見て適切なインデックス設計・クエリチューニングを行うように、今回はKibanaの Search Profiler を用いて検証を行いました。 Search ProfilerはElasticsearchの Profile API を可視化したものであり、具体的には以下の情報が取得可能です。 項目 詳細 create_weight 検索の最中にWeightを保持するオブジェクトの生成にかかる時間 build_scorer スコアラー(≠ドキュメントのスコア)オブジェクトの生成にかかる時間 next_doc マッチした次のドキュメントIDを返すのにかかった時間 advance next_docの内部でも呼ばれている、低レベルなイテレータの実行にかかった時間 match phrase queryのような、2フェーズ目の厳密なマッチの際にかかった時間 score スコアラーを用いて実際に特定のドキュメントをスコアリングした際にかかった時間 *_count 特定のメソッドの呼び出し回数 下記はKibanaをインストールした際にデフォルトで選択可能なSample eCommerce ordersデータセットを用いてプロファイリングしたサンプルです。 -- 検索リクエスト GET /kibana_sample_data_ecommerce/_search { " query ": { " bool ": { " filter ": [ { " term ": { " currency ": " EUR " } } , { " range ": { " products.base_price ": { " gte ": 50.00 } } } ] } } , " size ": 0 , " profile ": true } -- レスポンス { " took " : 29 , " timed_out " : false , " _shards " : { " total " : 1 , " successful " : 1 , " skipped " : 0 , " failed " : 0 } , " hits " : { " total " : { " value " : 2097 , " relation " : " eq " } , " max_score " : null , " hits " : [ ] } , " profile " : { " shards " : [ { " id " : " [8Xdz-7ZTSzyo4IAHkWSTsA][kibana_sample_data_ecommerce][0] ", " searches " : [ { " query " : [ { " type " : " BooleanQuery ", " description " : " #currency:EUR #products.base_price:[50.0 TO Infinity] ", " time_in_nanos " : 21453981 , " breakdown " : { " set_min_competitive_score_count " : 0 , " match_count " : 0 , " shallow_advance_count " : 0 , " set_min_competitive_score " : 0 , " next_doc " : 3014562 , " match " : 0 , " next_doc_count " : 2097 , " score_count " : 0 , " compute_max_score_count " : 0 , " compute_max_score " : 0 , " advance " : 848900 , " advance_count " : 7 , " score " : 0 , " build_scorer_count " : 14 , " create_weight " : 45100 , " shallow_advance " : 0 , " create_weight_count " : 1 , " build_scorer " : 17543300 } , " children " : [ { " type " : " TermQuery ", " description " : " currency:EUR ", " time_in_nanos " : 20980103 , " breakdown " : { " set_min_competitive_score_count " : 0 , " match_count " : 0 , " shallow_advance_count " : 0 , " set_min_competitive_score " : 0 , " next_doc " : 277970 , " match " : 0 , " next_doc_count " : 591 , " score_count " : 0 , " compute_max_score_count " : 0 , " compute_max_score " : 0 , " advance " : 7522992 , " advance_count " : 1828 , " score " : 0 , " build_scorer_count " : 21 , " create_weight " : 7400 , " shallow_advance " : 0 , " create_weight_count " : 1 , " build_scorer " : 13169300 } } , { " type " : " IndexOrDocValuesQuery ", " description " : " products.base_price:[50.0 TO Infinity] ", " time_in_nanos " : 4954816 , " breakdown " : { " set_min_competitive_score_count " : 0 , " match_count " : 0 , " shallow_advance_count " : 0 , " set_min_competitive_score " : 0 , " next_doc " : 683762 , " match " : 0 , " next_doc_count " : 1506 , " score_count " : 0 , " compute_max_score_count " : 0 , " compute_max_score " : 0 , " advance " : 217728 , " advance_count " : 598 , " score " : 0 , " build_scorer_count " : 21 , " create_weight " : 5800 , " shallow_advance " : 0 , " create_weight_count " : 1 , " build_scorer " : 4045400 } } ] } ] , " rewrite_time " : 6600 , " collector " : [ { " name " : " EarlyTerminatingCollector ", " reason " : " search_count ", " time_in_nanos " : 2358000 } ] } ] , " aggregations " : [ ] } ] } } これをKibanaのDev Tools内にあるSearch Profilerから可視化すると以下のような表示となります。 右側にあるView detailsをクリックすると、上に記載した詳細な項目が表示され、内部でどのような処理にどれだけ時間がかかるか可視化されます。 また、analyzerを適用したフィールドに対する検索をプロファイリングした場合、内部でどのように展開され、どれだけ時間がかかるか可視化されます。 -- マッピング PUT /kuromoji_sample { " mappings ": { " properties ": { " category ": { " type ": " keyword ", " fields ": { " kuromoji ": { " type ": " text ", " analyzer ": " kuromoji " } } } } } , " settings ": { " index ": { " analysis ": { " analyzer ": { " kuromoji ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer " } } } } } } -- データ登録 POST /kuromoji_sample/_bulk { " index ": {} } { " category ": " 靴 " } { " index ": {} } { " category ": " かばん " } { " index ": {} } { " category ": " Tシャツ " } -- 検索 GET /kuromoji_sample/_search { " query ": { " match ": { " category.kuromoji " : " 通勤かばん " } } } 効果のあった変更点 Rangeクエリの丸め込み ZOZOTOWNでは発売日での絞り込みやタイムセールの制御など、検索の要所要所でdate型データに対する絞り込みを行っています。その際、Elasticsearchへのリクエストには現在時刻のタイムスタンプを用いていました。 { " query ": { " bool ": { " filter ": { " range ": { " order_date ": { " gte ": " 2020-08-20T09:01:12+00:00 " } } } } } } しかし、 Elasticsearch公式のドキュメント にもある通り、時刻によるフィルタリングはfilter cacheに載らないため、毎回絞り込みが行われ低速になる傾向がありました。 Tune for search speed を参照すると、丸め込まれた時刻指定のfilterクエリはfilter cache対象になる、との記載がありサイトとしての必要要件を確認する事になりました。その結果、発売日の絞り込みやその他イベントに関して1分単位の精度が確保できていればよく、必ずしも秒単位で考える必要がないと判明しました。 そこでリクエストを以下のような末尾に ||/m を付与し、明示的に丸め込んでいることがわかる形式に変更しました。 { " query ": { " bool ": { " filter ": { " range ": { " order_date ": { " gte ": " 2020-08-20T09:01:12+00:00||/m " } } } } } } p95 (sec) p99 (sec) 改善前 0.553 0.667 改善後 0.202 0.516 95%tileで 約174% の改善に対し、99%tileでは 約29% の改善に留まりました。 search_analyzerの使用 上記のRangeクエリの改善によって95%tileのレスポンス速度は改善しましたが、99%tileの改善度合いが思わしくなかったため、追加の改善案を模索しました。 この時点までは全体の最適化を行っていましたが、リクエスト単位に注目して深堀りしたところ、特定のリクエストが毎回遅くなっていることが判明しました。 その特定のリクエストは日本語によるキーワードが含まれるような検索リクエストで、かつZOZOTOWN独自で定義しているシノニム(類義語)が含まれるものでした。 そこで、前述のSearch Profilerを用いて内部でどの部分に時間を要しているのか確認したところ、シノニムによって展開されたキーワード分検索リクエストが走っている様子でした。 インデックスのマッピングは、以下の設定となっていました。 { " mappings ": { " properties ": { " category ": { " type ": " keyword ", " fields ": { " synonym ": { " type ": " text ", " analyzer ": " synonym_analyzer " } } } } } , " settings ": { " index ": { " analysis ": { " filter ": { " synonym_graph ": { " type ": " synonym_graph ", " synonyms ": [ " かばん, カバン, バッグ, 鞄 " ] } } , " analyzer ": { " synonym_analyzer ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer ", " filter ": [ " synonym_graph " ] } , } } } } } デフォルトの設定では、検索時に使用されるアナライザと、インデキシング時に使用されるアナライザは同一のものが設定されます。そのため、上記の設定ではインデキシング時・検索時共にkuromojiとシノニムが適用されることになります。 一方、追加の設定として search_analyzer を設定することにより、インデキシング時と検索時で異なるアナライザを指定することも可能です。 { " mappings ": { " properties ": { " category ": { " type ": " keyword ", " fields ": { " synonym ": { " type ": " text ", " analyzer ": " synonym_analyzer ", " search_analyzer ": " kuromoji " } } } } } , " settings ": { " index ": { " analysis ": { " filter ": { " synonym_graph ": { " type ": " synonym_graph ", " synonyms ": [ " かばん, カバン, バッグ, 鞄 " ] } } , " analyzer ": { " synonym_analyzer ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer ", " filter ": [ " synonym_graph " ] } , " kuromoji ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer " } } } } } } このようにsearch_analyzerではシノニムの展開を行わない設定が可能ですが、この設定には以下のようなメリット・デメリットがあります。 メリット インデキシング時のみにシノニム展開するため、検索時の負荷が低減される デメリット 再インデックスしない限り、既存のドキュメントに対して類義語の更新が反映されない 詳しくはElastic社公式の 記事 で議論されていましたので、そちらをご覧ください。記事内ではデメリットとメリットを比較した結果、検索時にアナライザを適用する方向で収束していました。 しかし、弊社におけるユースケースでは以下のポイントから、インデキシング時に適用することとなりました。 インデキシングリクエスト数より検索リクエスト数が圧倒的に多いため、検索時のパフォーマンスを重視したい 日次で新規インデックスに対して全件インデキシングを行い、エイリアスを用いて検索先の管理を行っているため、類義語の更新に対して再インデックスを考える必要がない ドキュメントに対するマッチスコアを用いた評価を行っていない 実際に上記の設定を適用したうえで計測されたパフォーマンスは以下の通りとなります。 p95 (sec) p99 (sec) 改善前(オリジナル) 0.553 0.667 改善前(Range丸め込み) 0.202 0.516 改善後(Range丸め込み + search_analyzer) 0.147 0.205 Range丸め込み改修後と比較すると、95%tileで 約37% の改善、99%tileで 約152% の改善となりました。 Search Profilerで確認しても、検索時にシノニムが展開されていないことが確認できます。 効果のなかった変更点 max_result_window Elasticsearchのindexのsettingsに max_result_window という設定項目があり、これによって取得できる件数の制限が発生しています。 具体的には、検索時に指定する from と size の合計がmax_result_window以下でなくてはならず、超えた場合は query_phase_execution_exception が発生します。 弊社では初期設定時に max_result_window の値を内部のドキュメント数以上に設定し、制限なく検索が可能な状態にしていました。 { " mappings ": { ... } , " settings ": { " index ": { " max_result_window ": 100000 } , ... } } 一方で、 公式ドキュメント に記載がある通り、深いページングを行う際は (from + size) * number_of_shard 分のドキュメントを取得するためコストがかかります。 このオプション値の変更によって内部のパフォーマンスがどれ程変わるかは全く未知数でしたが、悪影響を与えていないかを確認するため検証を行うことにしました。 以下がその検証結果です。 p95 (sec) p99 (sec) max_result_window = 10,000,000 0.599 0.763 max_result_window = 10,000 0.580 0.653 95%tileでは約17%、99%tileで約3%の改善とあまり効果はありませんでした。 改善度合いに対して、一定数以上の検索に対する制限をかけてしまうデメリットが大きいと考え、今回採用は見送ることとなりました。 番外編 BulkのExceptionによるパフォーマンス影響 こちらは完全にデータの前処理が原因のミスでしたが、自分への戒めも兼ねて書き残します。 上記で解決した全体的なレスポンス速度低下の問題とは別に、1日の中で数十分ほど局所的にレスポンス速度の低下がみられました。 その際、具体的には以下のような症状が見られました。 レスポンス速度の低下(タイミングによっては、通常の数倍かかることも) /_nodes/stats から観測できるOld GCの増加 該当の時間にはドキュメントの一部を更新する処理が走っており、 _id 指定でbulk updateを行っていたのですが、多くのリクエストで更新対象がインデックス内に存在していませんでした。 Bulkで返ってくるレスポンスの実態は BulkResponse 、 BulkItemResponse ですが、この BulkItemResponse の挙動の差に原因がありました。通常アップデートに成功すると、この BulkItemResponse には DocWriteResponse 型のオブジェクトが格納されますが、失敗時には Failure 型のオブジェクトが格納されます。このオブジェクトにはメンバー変数として、例外の状態を示すオブジェクトが存在しており、 DocWriteResponse よりオブジェクトのサイズが大きいと考えられます。 またもう1つの要因として、Bulkの1リクエスト当たりのサイズを全バッチ共通の値でかなり大きめに設定していたことも関係していました。1フィールドだけを更新するシンプルなリクエストの場合、1リクエストで1万件以上の更新がかかっている状況でした。レスポンスは List<BulkItemResponse> として保持されているため、最大1万件以上分の BulkItemResponse が更新中メモリ上に居座ります。そのため、上記画像のような特定時間帯に集中したGCが発生し、局所的にリクエストが遅くなる状況となっていました。 検証 実際に今回のケースで発生していた DocumentMissingException と、成功時に生成される UpdateResponse を用いて、オブジェクトのサイズ比較を行いました。 また、1万件分を実際のレスポンスである BulkResponse に格納した際、差がどれほど出るかの検証も合わせて行いました。 public void test() throws IOException { int nbBulkItems = 10000 ; List<BulkItemResponse> succeedItems = new ArrayList<>(); List<BulkItemResponse> failureItems = new ArrayList<>(); UpdateResponse updateResponse = null ; Failure failure = null ; for ( int i = 0 ; i < nbBulkItems; i++) { updateResponse = new UpdateResponse( new ShardId( "index" , "index_uuid" , 0 ), "type" , "id" , - 2 , 0 , 0 , UPDATED); failure = new Failure( "index" , "type" , "id" , new DocumentMissingException( new ShardId( "index" , "index_uuid" , 0 ), "1" , "1" ), RestStatus.fromCode( 404 )); succeedItems.add( new BulkItemResponse(i, DocWriteRequest.OpType.UPDATE, updateResponse)); failureItems.add( new BulkItemResponse(i, DocWriteRequest.OpType.UPDATE, failure)); } BulkResponse succeedResponse = new BulkResponse(succeedItems.toArray( new BulkItemResponse[succeedItems.size()]), 0 , 0 ); BulkResponse failureResponse = new BulkResponse(failureItems.toArray( new BulkItemResponse[failureItems.size()]), 0 , 0 ); System.out.println( "SucceedResponse => " + RamUsageEstimator.sizeOf( new BulkItemResponse( 1 , DocWriteRequest.OpType.UPDATE, updateResponse))); System.out.println( "FailureResponse => " + RamUsageEstimator.sizeOf( new BulkItemResponse( 1 , DocWriteRequest.OpType.UPDATE, failure))); System.out.println( "SucceedResponse(10000) => " + RamUsageEstimator.sizeOf(succeedResponse)); System.out.println( "FailureResponse(10000) => " + RamUsageEstimator.sizeOf(failureResponse)); } 結果 ucceedResponse => 696 FailureResponse => 1392 SucceedResponse(10000) => 1960560 FailureResponse(10000) => 6840768 上記のような簡易的な検証でも、 BulkItemResponse 単体で2倍、1万件の BulkResponse では約3.5倍となっていることがわかりました。 解決策 解決策として以下の2つを試したところ、GC・レスポンス速度共に大幅な改善が見られました。 Bulkリクエストのサイズ縮小 DocumentMissingExceptionの回避(更新対象の再考) まとめ 本記事では、ZOZOTOWNの検索改善におけるクエリ、mapping、settingのパフォーマンス調査、チューニング方法について紹介しました。個人的にはSearch Profilerが特に気に入っており、SQLの実行プランと同じ感覚で実際に発行されるリクエストレベルの確認できるためとても勉強になります。 最後に、ZOZOテクノロジーズでは検索をさらに改善する検索エンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com 謝辞 本記事に含まれる検証の一部については、検索分野の技術顧問である 大谷氏(@johtani) による協力のもと行われました。また、テックブログの執筆に際してもレビュー・アドバイスを頂くなど、多大なるご協力を頂きましたことをこの場を借りて御礼申し上げます。
アバター
こんにちは、SRE部MA基盤チームの田島です。 私達のチームでは、マーケティングシステムの開発・運用を自前で行っています。マーケティングシステムの内容としては、主にユーザに向けてのメールやLINE・PUSH通知などへの配信です。 マーケティングシステムは大きく分けて2種類あります。1つ目がSQLによるセグメント抽出を行い、抽出したユーザに対してバッチで配信を行うバッチ配信システムです。2つ目がユーザの行動や商品情報等データの変更をリアルタイムに検知して配信を行うリアルタイムマーケティングシステムです。 本記事ではリアルタイムマーケティングシステム(RTM)について紹介します。また現在、RTMのリプレイス計画を行っているのでそれについても紹介いたします。 ZOZOTOWNのリアルタイムマーケティングシステム リアルタイムマーケティングシステムではユーザの行動や商品情報等データの変更を検知し、ユーザへリアルタイムでアクションを行います。例えばある商品の値段が下がったとき、RTMはその商品情報をリアルタイムに検知し、その商品をお気に入りしているユーザに対して配信を行います。 なぜリアルタイムに配信する必要があるのか リアルタイム配信を行っている理由は2つあります。1つ目が配信システムのバージョンアップの変遷で、2つ目が配信キャンペーンの性質にあります。 配信システムのバージョンアップの変遷 元々ZOZOTOWNでは1日1回決まった時間に配信をするということしかしていませんでした。 そこでまず、1日に1回抽出したユーザに対して9時・12時・18時の3パターンでユーザ毎に配信時間の振り分けをしてみたしたところ、効果に差が出ました。次に、配信ごとに最新のデータを使ってセグメント抽出し配信をするとCVRの向上がみられました。 このような検証を得てRTMによるリアルタイム配信が誕生しました。リアルタイム配信だけが要因ではありませんが、実際RTMによる配信によって開封率・CTR・CVRの指標が全て向上(最大で開封率2倍、CVR5倍)しました。 配信キャンペーンの性質 ZOZOTOWNのユーザへの通知には、「商品が残り1点になったお気に入りアイテムをユーザにお知らせする」と言ったキャンペーンがあります。そのため、商品が残り1点になり数時間たってからユーザに通知を行っても、既にその商品が完売になっている可能性があります。 また、まだ未実装の状態ではありますが配信チャネルとして訪問中のユーザに対しアプリ内通知としてメッセージを送るということを当初より計画していました。そのため、訪問中のユーザが離脱するよりも前にメッセージを届ける必要があります。 このようなことからZOZOTOWNではリアルタイムマーケティングの仕組みが必要になります。 RTMの機能 イベントの検知から、配信までの流れは以下のようになっています。 以下でそれぞれの機能の詳細を説明します。 イベント検知 まず最初に行うのがイベント検知です。イベントはユーザ行動とZOZOTOWNのデータの変化があります。ユーザ行動はアクセスログから検知を行い、データの変更はZOZOTWONのDBの差分データを取得して検知を行っています。イベント検知されたものを整形しキャンペーン判定の処理に渡します。 キャンペーン判定 イベントを検知するとキャンペーン判定を行います。キャンペーンとはどのようなイベントが発生したときにどのようなお知らせをユーザに提供するかを定めたものです。例えば、商品が値下がりしたときにその商品をお気に入りしているユーザに対して、「あなたがお気に入りしているこの商品が値下がりしました」といったメッセージを配信するキャンペーンがあります。 ユーザ行動や商品情報の変更のタイミングでどのようなキャンペーンを実施するかを決めるのが、このキャンペーン判定の処理になります。 ユーザ抽出/メンバーフィルタ 続いての処理がユーザ抽出処理です。キャンペーンが決まると、そのキャンペーンに該当するユーザの抽出を行います。そしてユーザ抽出後、本当にそのユーザにキャンペーンを配信するべきかのフィルターをユーザごとに行います。これによって得られたユーザに対してキャンペーンの配信を行います。 チャネル最適化 RTMから配信されるチャネルは複数あり、現在MAIL/LINEがあります。また、PUSH配信・アプリ内通知も対象チャネルとして実装を進めています。RTMからどのチャネルに対して配信されるかは、システムによって決められます。例えば、MAILへの反応は悪いがLINEにはよく反応するユーザに対してはLINEのみで配信をするといったことを行います。 時間最適化 RTMではリアルタイム配信を行っていますが、ただただイベント検知からリアルタイムに配信をするだけではなく時間最適化ということも行っています。例えば、このユーザは特定の時間にMAILを確認することが多いと判断すると、リアルタイム配信せず一度キューにためます。その後最適な時間にキューから取り出しキャンペーン配信を行います。もちろん、キャンペーンやチャネルによってリアルタイム配信のほうが良いと判断したら、キューにためずリアルタイム配信を行います。 通数最適化 ユーザへの配信はただ数多く配信をすればいいというものではありません。ユーザが配信をうるさいと感じオプトアウトしてしまうと、ユーザとコミュニケーションを取ることができなくなってしまいます。そこでRTMでは通数最適化を行っています。これはユーザにとって最も最適な通数は何件なのか判断し、それ以上の通知はしないようにしています。 コンテンツ生成・配信処理 コンテンツ生成では、実際に配信するコンテンツを生成します。コンテンツとはLINEであれば、LINEに配信するメッセージと対応するJSONです。 最後に、作成したコンテンツを各チャネルに対し配信を行います。 リアルタイムマーケティングシステムを支える技術 続いて実際にマーケティングシステムで利用している技術について紹介します。 アーキテクチャ アーキテクチャは以下のようになっています。RTMのシステムはすべてAWS上で動いています。 以下でそれぞれの項目について紹介します。 Analyzer Analyzerは上記で説明した「キャンペーン判定」から「配信処理」までを行うアプリケーションです。このアプリケーションがRTMのメインアプリケーションとなります。Analyzerについては次の章で詳しく説明します。 RTM DB RTM DBはRTMのデータを永続化するためのDBです。RTMに関係するデータはこのDBに格納されます。DBにはPostgreSQLを利用しています。 レプリDB ZOZOTOWNではDBにSQL Serverを利用しており、用途によって複数のSQL Serverに別れています。RTMでは、ZOZOTWONのDBを直接参照するのではなくレプリケーションしたDBをAWS上に立てて利用しています。 SQL Serverではテーブル単位でレプリケーションできるため、各DBから必要なテーブルのみをレプリケーションし、1つのDBに集約しています。 Tracker TrackerはZOZOTOWNのDBからレプリケーションしたSQL Serverの変更データを追跡し、Analyzerに差分データとして連携するアプリケーションです。差分データの取得にはSQL ServerのChange Trackingという仕組みを利用しています。Change Trackingをかんたんに説明すると、変更が発生したテーブルのPKを保持する機能です。Change Trackingの詳細については以下をご参照ください。 docs.microsoft.com Change Trackingを有効にすると CHANGETABLE(CHANGES ...) を利用できるようになります。 CHANGETABLE(CHANGES ...) からは以下のリンクようなデータを取得できます。 docs.microsoft.com この機能を利用すると以下のSQLで差分データを取得できます。削除された行に関しては deleted という列を設けて判断できるようにしています。以下はorderテーブルの例です。 SELECT CT.id id, O.必要なカラム 1 , O.必要なカラム 2 CASE WHEN O.id IS NULL THEN 1 ELSE 0 END deleted FROM CHANGETABLE(CHANGES dbo. order , 前回の取得したときのversion) AS CT LEFT OUTER JOIN dbo. order AS O ON CT.id = O.id 上記クエリで差分データを取得したらそれをJSONの形式に整形しAnalyzerへ連携します。 Analyzer 先程紹介したとおり、Analyzerが「キャンペーン判定」から「配信処理」までを行うRTMのメインアプリケーションとなっています。リアルタイム配信を実現するため高速かつ柔軟なルール判定が必要となります。それらを実現している技術について紹介します。 Analyzerの非機能要件 「RTMの機能」で説明した機能要件の他に、Analyzerは以下の非機能要件が必須となります。 複雑なルール判定 動的なルール追加 高速な条件判定 複雑なルール判定 Analyzerではユーザの取った様々な行動や、ZOZOTOWNで扱う様々なデータの変化を検知しキャンペーン判定を行います。また各種最適化の処理条件は何パターンにもなり複雑になります。 そこでそれらの複雑なルール判定を統一的かつシンプルに管理できるような仕組みが必要となります。 動的なルール追加 マーケティングシステムのキャンペーン判定や最適化処理のルールはエンジニアではなくビジネスサイドのメンバーによって定められます。そのためプログラムでルール判定をハードコードするのではなく動的にルールの追加や修正できることが必要となります。 高速な条件判定 システムの必須条件としてリアルタイム配信があります。それを実現するためには定義したルール判定を高速に行う必要があります。またメンバーフィルターのような機能はキャンペーン対象のメンバー一人ひとりに対して条件判定を行います。ZOZOTOWNで扱う商品数やユーザ数を考えると同時に発生するキャンペーンの数並びにキャンペーンの対象となるメンバー数は膨大になります。そこでそれらを高速に処理できるよな仕組みが必要となります。 技術構成 上記で紹介してきた機能要件・非機能要件を満たすシステムを構築するためAnalyzerの技術構成は以下のようになっています。 言語 Java 8 フレームワーク JBoss EAP(Java EE) JBoss Data Grid Red Hat Decision Manager JBoss EAP JBoss EAPはRed Hatが提供しているJava EEの実装です。Analyzerでは以下で紹介するJBoss Data GridやRed Hat Decision Managerと組み合わせて機能の実現を行っています。 Red Hat Decision Manager 「複雑なルール判定」並びに「動的なルール追加」を実現するために導入したのがRed Hat Decision Managerです。キャンペーン判定処理やユーザ抽出・メンバーフィルター・チャネル最適化のような処理はすべてルールベースで行っています。それらのルールはすべてRed Hat Decision Managerというルールエンジンによって定義・判定処理を行っています。 Red Hat Decision Mangerを利用することで、Javaで言うところの複雑なswitch文をExeclで表現できます。以下はデバッグユーザ以外を除外するルールを定義したものです(実際に使われているルールではありません)。 3行目がプログラムから渡って来るオブジェクトを定義しており、プログラムからDecision Mangerを呼び出すときに引数で値を渡すことができます。この3行目が実際の条件式になっておりJavaの式をそのまま書くことが可能です。 $param に5行目以降の値が入った状態で評価されます。それぞれオレンジの部分の条件を評価しそれがすべてtrue判定となると一番右のActionが実行されます。 この例では、 campaignId == 1 かつ memberId not in (1,2,3) の場合に配信対象外フラグを立てます。campaignIdが2の場合は配信対象外フラグが立つことはありません。また、このExcelで定義したルールはアプリケーションの再起動をすることなしに動的にロードできます。 JBoss Data Grid Analyzerアプリケーションで1番の肝となる技術がJBoss Data Grid(JDG)です。これによって「高速な条件判定」を実現しています。JDGはかんたんに言うとインメモリな分散キャッシュデータストアで、KeyValue形式でJavaのオブジェクトを直接保存します。JDGでは複数サーバーでクラスタを組み分散してデータを保持します。 JDGにはServer-Client modeとEmbeded modeがありますが、AnalyzerではEmbededモードを利用しています。Embeded modeはJavaアプリケーションとJDGを同一のJVMで動かします。Analyzerでは必要なデータをほぼ全てJDGのキャッシュとして保持しています。このとき以下の機能を組み合わせることですべてのデータへのアクセスを、アプリケーションがアクセスするメモリと同一メモリへのアクセスだけで完結させることが可能です。 アプリケーションの分散実行 キャッシュが自分のノードに存在するかの判定 これを使った、具体的な処理に関しては次の「メンバー抽出後のメンバーフィルタリング処理」で実例を紹介します。JBoss Data Gridについては以前に以下のブログにて詳しく説明していますので、合わせてご参照ください。 techblog.zozo.com メンバー抽出後のメンバーフィルタリング処理 どのようにして複雑な条件判定を高速に実現しているかの例として、メンバー抽出後のメンバーフィルタリング処理を擬似コードで紹介します。簡潔にするため、色々と省略しています。 メンバーフィルタリングの実行 int campaign_id = xxx; List<Integer> memberIds = [id1, id2, id3 ... ]; AdvancedCache<Object, Object> memberCache = JdgUtil.lookupCache(CacheNames.MEMBER); DefaultExecutorService des = new DefaultExecutorService(memberCache); task = new DistributedMemberFilteringService(campaign_id, memberIds); des.submitEverywhere(task, memberIds) 実際のメンバーフィルタリング処理 public class DistributedMemberFilteringService { public DistributedMemberFilteringService(List<Integer> memberIds) { this .campaign_id = campaign_id; this .memberIds = memberIds; } public void call() { List<Integer> localMemberIds = narrowDownToLocal(memberIds); List<Integer> offerMemberIds = new ArrayList<Integer>(); for (Integer memId : localMemberIds) { Member member = (Member)memberCache.get(memberId); MemberFilteringFact fact = new MemberFilteringFact(member); MemberFilterRule rule = new MemberFilterRule(fact); rule.fire(); if (!fact.isFiltered()) { offerMemberIds.add(memberId); } } 次の処理(offerMemberIds); } private List<Integer> narrowDownToLocal(List<Integer> memberIds) { List<Integer> localMemberIds = new ArrayList<Integer>(); AdvancedCache<Object, Object> memberCache = JdgUtil.lookupCache(CacheNames.MEMBER); JdgDistributionUtil localityChecker = new JdgDistributionUtil(memberCache); for (Integer memberId : memberIds) { if (localityChecker.isLocal(memberId)) { localMemberIds.add(memberId); } } return localMemberIds; } } メンバーフィルタリングはメンバー抽出処理によりキャンペーンの対象となりうるメンバーidの一覧が取得できている状態からスタートします。メンバーidはメンバーキャッシュというメンバーの情報を格納しているキャッシュテーブルのkeyとなります。 des.submitEverywhere(task, memberIds) を実行すると、memberIdsに含まれるmemberIdをkeyとして1つでも保持しているノード全てでtaskを実行します。 taskは、 des.submitEverywhere が呼ばれたタイミングで call() メソッドが実行されます。callメソッドの中で最初に narrowDownToLocal(memberIds) を呼び出しています。JDGの機能に指定したkeyが自分のノードで保持しているかどうかを判定するためのメソッドがあります。それを利用し narrowDownToLocal(memberIds) では、メソッドを実行しているノードが保持しているmemberIdのみになるようmemberIdのListを絞り込みます。これによって、絞り込まれたmemberIdでキャッシュを取得すると必ずアプリケーションと同じメモリからデータを取得できるようになります。 その後、絞り込んだmemberIdのリストをmemberIdごとにRed Hat Dicision Mangerのルールエンジンによってフィルタリングします。ルールの結果はfactのフィールドに保存されます。ルール判定が終わるとfilterに引っかからなかったid( !fact.isFiltered() )について次の処理に進みます。 課題 以上のようにリアルタイム配信の仕組みや各種最適化の紹介してきました。それらの機能はうまくいっているものばかりではなく様々な課題があります。現在課題となっているものの一部を紹介します。 リリースの問題 アプリケーションのパフォーマンスを最大化するために、アプリケーションとキャッシュを同一JVMで動作させていると紹介しました。これにより最も課題となるのがリリースになります。 アプリケーションを止めてしまうとそのノードのキャッシュも同時に失われてしまいます。そのためリリースは1台ずつアプリケーションを停止し、キャッシュを別ノードにリバランスしながら行う必要があります。 上記の方法で無停止リリースは可能ですがサーバーが複数台あるため現在3〜4時間ほどリリースに時間がかかっています。メンテナンスタイムを設けることでキャシュをダンプしアプリケーションをリリース、キャッシュをロードし直すといった方法も可能です。この方法であれば1.5時間とリバランスを行うよりも短時間ですみますが、それでも時間がかかります。また、アプリケーションに問題があったときのロールバックにも時間がかかってしまいます。 以上のことからAnalyzerのリリースは慎重に行う必要があり、リリースのサイクルが遅いといった問題があります。 スケーリングの問題 アプリケーションとキャッシュが同一JVMでのっていることはスケーリングも困難にしています。サーバーが増減した場合JDGはキャッシュのリバランスを行います。ただし、サーバーが増えるぶんにはいいのですがサーバーが減る場合適切にキャッシュをリバランスしてやらないとデータロストしてしまいます。またサーバーを減らしすぎた場合キャッシュがクラスター内のメモリにのり切らなくなってしまうと言った問題も発生します。 これらのことから現在RTMではオートスケーリングの仕組み使っておらず常にピークに備えたサーバーを用意して運用しています。 シングルAZ構成 現在AnalyzerシステムはあえてシングルAZ構成にしています。Analyzerで必要となるデータをすべてJDGのキャッシュにのせていると紹介しました。そのためメモリを大量に確保する必要があり、かなり大きめのインスタンス複数台でクラスタを構成しています。 JDGはサーバーが2台以上同時にダウンするとデータロストする設定にしています。そのため、マルチAZ対応を考えるとJDGクラスタをもう一式用意する必要があります。JDGクラスタをもう一式用意するコストと、AZ障害の発生頻度ならびに復旧時間を比較検討した結果シングルAZのほうが好ましいと判断しました。 しかしAZ障害発生時にはダウンタイムは避けられず、ユーザへの配信が滞ってしまいます。これはユーザへの価値提供を犠牲にしていることにほかなりません。 分散キャッシュの運用の難しさ 分散システムは普通のWebアプリケーションよりも格段に複雑で運用が難しくなります。実際に運用しているJDGクラスタがスプリットブレインを起こしたといったことがありました。 techblog.zozo.com また、JDGの情報がインターネット上に出回っていないため知見があまり得られないという問題もあります。あったとしても数年前の情報ということがほとんどです。このようにAnalyzerを運用し続けることだけでも大変な手間がかかっています。 ルールベースでの条件判定 現在キャンペーン判定やそれぞれの最適化処理はすべてヒューリスティックなルールベースで条件を決めています。ルールはすべてRed Hat Decision Managerで定めており、機械学習での判定処理などはほんの一部にしか適用できていません。 さらにRed Hat Decision Managerの導入理由としてビジネスサイドのメンバーが動的にルールを追加できるようにするためと紹介しました。しかし実際に運用してみるとルールで使うためのデータを参照できるようにする必要があったりと、ルールの追加のたびにプログラムの改修が高頻度で発生してしまっています。 リプレイス計画 以上のような課題を解決するために、RTMシステムの大幅なリプレイスを検討しています。まだ具体的な構成や進め方は決まっていませんが、現在の考えていることを紹介いたします。以下が検討しているアーキテクチャの概要です。 Analyzerのアプリケーションとキャッシュが同じJVMにのっている問題を解決するために何らかのデータストアを外出しすることが第1の目標となります。ただし、外出しすることで現在のパフォーマンスを維持できるかが問題になります。そのため、どのようなデータストアを利用するかと言ったことはまだ決められていません。また、今までAnalyzerの中でやっていた各処理をジョブキューの形にすることで柔軟なスケーリングを実現したいと考えています。 そして現在RTM専用となっていた配信処理の部分を、配信チャネルごとにモジュール化したいと考えています。こうすることでZOZOTOWNのトランザクション処理の中で配信が必要な場合、統一した仕組みで配信できます。 Trackerの部分ですが、現在リアルタイムデータ連携基盤というものを開発しています。それを利用することで差分データを取得できます。アクセスログについても独立した基盤の作成またはツールの導入を検討しています。 まとめ 今回リアルタイムマーケティングシステムについてご紹介しました。紹介したように現在ZOZOTOWNでは、リアルタイムマーケティングシステムのリプレイスを計画しています。この記事をよんでこんなアーキテクチャがいいのでは、もっとこんなことができるのではと思った方はぜひお話しましょう。 tech.zozo.com また、8/27にリアルタイムマーケティングシステムを含めMAの取り組みについてのイベントを行いますのでぜひご参加ください。 zozotech-inc.connpass.com
アバター
基幹システム部ブランド連携チームの三橋です。ZOZOTOWNとお取引きをさせていただいているテナント様とのデータ連携部の開発・保守運用を行っております。 TECH BLOGという事で技術的なところにフォーカスした記事が多いのですが今回はZOZOTOWNの主要なサービスでもある取寄せ商品を業務・システム面より(業務系強めの記事として)ご紹介させていただきます。なお、先日公開されました同チームメンバーのブログ ZOZOBASEの出荷データ連携を支えるAPI も併せてご覧いただけると幸いです。 取寄せ商品ってナニ? 取寄せ商品とは、ご注文受付後に販売ショップの店舗や倉庫から取り寄せる商品のことです。取寄せ商品は、カートに入れた時点で「取寄せ商品」と表示され発送までの期間が表示されます。 取寄せサービスを導入した経緯と意義 要はZOZOBASEに在庫がない商品でもテナント様の倉庫や店舗に在庫があれば、ZOZOTOWNから取寄せ注文ができるというサービスなのですが下記のような目的を持って生まれました。 顧客満足度の向上 お客様のZOZOTOWNに対する不満の第1位は『欲しい商品の在庫がない』こと。 ZOZOTOWNの倉庫であるZOZOBASEにある商品(在庫)が欠品した場合、次回の入荷があるまでお客様は購入できず機会損失となっていました。そこで、テナント様倉庫・店舗にて保持する在庫をお客様が購入できたらそれを解決できるのではと考えました。 テナント様全体での在庫消化率の向上 プロパー消化率(定価での販売割合)が上昇し、利益率の向上の一手につながります。 ZOZOTOWNの実在庫での販売フローと取寄せ商品での販売フロー ZOZOTOWNの実在庫での販売フローと取寄せ商品での販売フローを以下にまとめました。 ZOZOTOWNの実在庫での販売フロー (1)テナント様よりZOZOBASEへの出荷指示 (2)テナント様倉庫よりZOZOBASEへ商品を出荷 (3)ZOZOBASEにて商品を荷受検品しZOZOTOWNの販売可能個数(実在庫)となる (4)ZOZOBASEの在庫数分受注が付いた場合、在庫切れ状態となってしまう ZOZOBASE内にある実在庫でのみ商品購入可能であるため実在庫以上の受注は取れず機会損失となります。 ZOZOTOWN取寄せ商品フロー (1)テナント様倉庫・店舗在庫情報をZOZOTOWNへ連携し販売可能数(取寄在庫)となる (2)ZOZOBASE在庫が無くても販売可能数(取寄せ在庫)にて受注が可能となる (3)取寄せ在庫での受注後、毎朝倉庫・店舗スタッフ様宛に受注した商品情報をメールにて配信 (4)集配業者への集荷依頼情報を連携 (5)集荷スタッフがテナント様店舗へ商品集荷に伺う (6)商品集荷後ZOZOBASEまで商品を移送 (7)取寄せ注文に入荷した商品が引き当たりお客様へ発送処理。また、商品入荷が無く取寄せ期限を過ぎてしまった場合は取寄せ不可となり注文のキャンセルと共にお客様へお詫びのメールを通知。 ZOZOBASE内の実在庫を売り切った場合、取寄せ在庫での購入が可能となりテナント様の倉庫や店舗の在庫で受注をとれるため 『欲しい商品の在庫がない』 が解消され 顧客満足度の向上につながりテナント様全体での在庫消化率も向上 といった効果が見込めます。 欠品対策について 取寄せ商品のためテナント様倉庫、店舗にて同タイミングで注文がついて欠品となる場合もあります。 テナント様へのご連絡(以後「配分」と呼ぶ)後、取り寄せまでの期限を超過してしまったものが取寄せ不可になるのは理解できるけど、その期限までただ待つだけ?欠品対策は?(と思った方は鋭いです。是非 こちら まで。) ここからは、そのような課題の中でも、欠品対策について紹介します。 欠品対策について 取寄せ商品の受注よりテナント様への配分は2回実施 初回配分から2日後までに入荷が無ければ、3日目に初めに配分した店舗以外の他倉庫・店舗に配分されます。要求のリトライ処理という位置づけです。 配分フローの簡単なイメージを図で示します。 ※キャンセルメール・・・N+5日までに検品完了されなければ、お客さまに自動配信 取寄せ商品の入荷は優先で検品 取寄せ商品が納品されたものの検品作業が遅れてしまい入荷期限切れでキャンセルされてしまう事もあるため、ZOZOBASEでは取寄せ商品用の検品作業は優先的に行われます。 販売可能数には閾率を掛けて販売 閾率とは販売してはいけない数量を算出する割合です。 テナント様倉庫、店舗にて購入等での欠品リスク防止のため閾率を掛けたもので販売可能数が決まります。 テナント様の在庫コントロールも各々で違ってくるため決定方法は2パターンから選択可能としました。 ※SKU=とある商品の1つのカラー・サイズの組み合わせ 監視について 当然ながら監視も行っています。 残念ながら詳しい画面はお見せできませんが各倉庫、店舗へ配分された回収率の推移を可視化して監視しており、回収率が低い店舗が出てきた場合は弊社営業とテナント担当者様で協議し、閾率の見直しや優先店舗のコントロール、店舗スタッフ様への伝達等をして改善していく運用をしています。 また、閾率の設定、ヒューマンエラーによるミスが大量キャンセルなどの障害にも繋がってしまうためアラート監視を実施しており、何か異常があれば都度対策を講じています。 テナント様が取寄せサービスを導入するまでのハードル この取り寄せサービスはZOZOTOWN側だけの準備・設定だけでは実現できません。 以下のようにシステム面・運用面でテナント様にご協力をいただく必要があります。 テナント様側のシステムに関する準備 テナント様倉庫・店舗在庫の情報を連携する必要があります。自動化していない場合には管理画面上で都度アップロードしてもらう運用が発生します。そのあたりの運用を自動化できるようAPIの提供もしていますが、APIを利用する場合はテナント様側の開発が必要となってきます。 店舗のオペレーション変更 毎朝取寄せ注文の連絡が配信され、店舗スタッフは受信した商品情報をもとに取寄せ商品を確保し、当日配送業者の集荷スタッフに商品を渡して頂きます。 普段の店舗での業務に加えてZOZOTOWNの取寄せサービスに関する業務も増えるため、店舗オペレーション再整理と店舗スタッフ様への落とし込みが必要になります。 店舗スタッフのマインドの切り替え 「店舗の在庫を取られる」と思われがちなため、サービス開始時には実際にそのような反発を耳にすることもありました。 ZOZOTOWNとしてはしっかり取寄せの売上実績を作ること、テナント様には店舗の売上成績に取寄せ商品の回収実績を加味していただいたりするなど、地道な努力の結果、取寄せに対する認知が進み、導入実績も増加しています。 まとめ ZOZOTOWNの客注実施ショップは全体の3割を超えてきており、取寄せ対象の店舗は数千店規模になっています。 取寄せ注文の売上比率も伸びており、取寄せ導入時の課題解決に大きく寄与できています。しかし、まだまだ伸びしろのあるサービスであるため、今後もなるべくリードタイムをZOZOBASEの在庫販売に近づける工夫や、欠品によるご迷惑をおかけしないような仕組みをアップデートし、より多くのお客様・テナント様にご利用いただけるよう改善をしていきます。 さいごに 前述のとおり、導入する準備としてテナント様にも多分にご協力をいただく必要があるサービスではありますが、導入や課題解決に関するノウハウはかなり蓄積されています。そのため、しっかりとしたサポートも可能です。取寄せサービス含め、データ連携にご興味を持たれたテナント様がおられましたらZOZOTOWNの営業までお気軽にお問い合わせください。 ZOZOテクノロジーズでは、今回紹介したような裏側の仕組み作りや、運営する様々なサービスを一緒に作り上げていただける方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 https://tech.zozo.com/recruit/ tech.zozo.com
アバター
こんにちは。SRE部BtoBチームの竹田です。本記事では、クラウドインフラ環境のセキュリティ対策を講じようと思いつつも何から着手すれば良いのか分からないという方向けに、マルチクラウドに対応したオープンソースのセキュリティ監査ツールであるScout Suiteを紹介します。 Scout Suiteとは Scout Suiteはマルチクラウドに対応したオープンソースのセキュリティ監査ツールです。各クラウドプロバイダーから公開されているAPIを利用してクラウドの設定情報を収集し、リスクとなる項目をHTML形式のレポートファイルで出力してくれます。マルチクラウドとあるように2020/08/07時点では以下のクラウドサービスに対応しています。 Amazon Web Services Microsoft Azure Google Cloud Platform Alibaba Cloud (alpha) Oracle Cloud Infrastructure (alpha) 参考: nccgroup/ScoutSuite: Multi-Cloud Security Auditing Tool AWSに対しての実行 今回はCentOS 7.8環境にScout Suiteの実行環境を整えます。まずはAWSに対してScout Suiteを実行してみます。Scout Suiteの実行までにはいくつか準備が必要となるため、順を追って説明します。 必要なパッケージのインストール Scout SuiteはPythonで記述されており、Pythonとpipが必要になります。Pythonのバージョンは3.5以上をサポートしています。 $ yum install python3 python3-devel python3-pip Scout Suiteのインストール GitHubからScout Suiteの本体をダウンロードします。2020/08/07時点の最新バージョンは5.9.1でした。 $ git clone https://github.com/nccgroup/ScoutSuite.git ScoutSuiteディレクトリに移動して、必要なパッケージをインストールします。 $ cd ScoutSuite $ pip3 install -r requirements.txt AWS CLIのインストール AWS CLIをダウンロードします。 $ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" zipを解凍してインストールします。 $ unzip awscliv2.zip $ sudo ./aws/install AWS CLIの初期設定をします。 $ aws configure AWS Access Key ID [None]:{アクセスキーID} AWS Secret Access Key [None]:{シークレットアクセスキー} Default region name [None]:ap-northeast-1 Default output format [None]:json 以下のポリシーはScout Suiteの実行に必要な権限を保有しているため、このポリシーを関連付けたIAMユーザーのアクセスキーIDおよびシークレットアクセスキーを設定します。 ReadOnlyAccess SecurityAudit 最低限必要な権限に絞ったカスタムポリシーもあるようです。 AWS Minimal Privileges Policy Scout Suiteの実行 ScoutSuiteディレクトリ内に移動してscout.pyを実行します。 $ python3 scout.py aws 以下のように監査が進行していくので、監査が完了するまでしばらく待機します。 2020-07-21 08:40:46 hostname scout[19403] INFO Launching Scout 2020-07-21 08:40:46 hostname scout[19403] INFO Authenticating to cloud provider 2020-07-21 08:40:51 hostname scout[19403] INFO Gathering data from APIs 2020-07-21 08:40:51 hostname scout[19403] INFO Fetching resources for the ACM service 2020-07-21 08:40:51 hostname scout[19403] INFO Fetching resources for the Lambda service 2020-07-21 08:40:52 hostname scout[19403] INFO Fetching resources for the CloudFormation service 2020-07-21 08:40:53 hostname scout[19403] INFO Fetching resources for the CloudTrail service 2020-07-21 08:40:54 hostname scout[19403] INFO Fetching resources for the CloudWatch service 2020-07-21 08:40:55 hostname scout[19403] INFO Fetching resources for the Config service 2020-07-21 08:40:55 hostname scout[19403] INFO Fetching resources for the Direct Connect service 2020-07-21 08:40:56 hostname scout[19403] INFO Fetching resources for the EC2 servicea . . . 監査が完了すると、ScoutSuiteディレクトリの下にscoutsuite-reportディレクトリが生成されます。このディレクトリの中身がレポートファイル一式になっています。 レポート結果の確認 scoutsuite-reportディレクトリの中のHTMLファイルをブラウザで開くと以下のようなレポート結果が確認できます。 サービス毎に監査項目の数や発見された問題の数がリストされています。一番左側のアイコンが緑色の場合は問題無し、アイコンが黄色や赤色の場合は問題ありです。EC2の項目が赤くなっているので確認してみます。 EC2の設定に対する問題点がリストされています。各項目の右側にあるプラス記号をクリックすると、項目毎の詳細を確認できます。Security Group Opens SSH Port to Allの項目のプラス記号をクリックしてみます。 SSH(22)という攻撃の格好の餌食となるポートがフルオープンになっていると書いてあります。また、22番ポートのオープンが必要な場合はIP制限すると攻撃を受ける可能性が減少するといったアドバイスもあります。プラス記号ではなく項目名自体をクリックすると、問題が見つかったセキュリティグループの設定情報を確認できます。 アウトバウンドルールやインバウンドルールの設定内容に加え、対象のセキュリティグループの利用状況も確認できます。問題となる設定は赤くハイライトされており、確かに22番ポートがフルオープンになってしまっているようです。アドバイスに従ってIP制限を施したり、22番ポートを利用していなければルールごと削除するのも手です。そもそもセキュリティグループが利用されていないようであればセキュリティグループごと削除してしまうと良いでしょう。 GCPに対しての実行 マルチクラウドに対応しているということで、GCPに対してもScout Suiteを実行してみます。必要なパッケージとScout Suite本体の準備は整っているものとして、GCP独自で必要な準備について説明します。 サービスアカウントの作成と鍵ファイルの取得 サービスアカウントを作成してJSON形式の鍵ファイルを取得しておきます。サービスアカウントに付与するIAMロールは以下のものです。 閲覧者(Viewer) セキュリティ審査担当者(Security Reviewer) Stackdriverアカウント閲覧者(Stackdriver Account Viewer) Cloud Resource Manager APIの有効化 監査するプロジェクトのCloud Resource Manager APIを有効にしておく必要があります。Cloud Resource Manager APIが無効の場合、一見問題なく監査が進みますが、監査対象が0という悲しいレポート結果が出力されてしまいます。 Scout Suiteの実行 ScoutSuiteディレクトリ内に移動してscout.pyを実行します。--service-accountには先ほど取得しておいた鍵ファイルを指定します。 $ python3 scout.py gcp --service-account {/path/to/key.json} AWSとGCPのレポート結果の違い 監査対象となるサービス数や監査項目の数はAWSの方が多く、また以下のような点からもAWSの方が良くメンテナンスされているのかなという印象を受けました。 AWSだとSSHやRDPなどポート毎に項目分けされていたものが、GCPだとウェルノウンポートでまとめられている GCPのファイアウォールの設定で22番ポートがフルオープンでも黄色(Warning)判定される まとめ AWSにはTrusted AdvisorやConfig、GCPにはRecommenderといった同様機能を持つサービスがあります。しかし、Scout Suiteは異なるクラウド環境を横断して、統一したオペレーションでレポートを出力できるメリットがあります。そのため、特に複数のクラウド環境を運用している方にとっては、Scout Suiteの利用が運用の省コスト化につながるのではないでしょうか。また、機能開発も頻繁に行われており、監査対象となるサービスや監査項目の充実も予定されているようです。執筆時点においてSRE部BtoBチームではまだ本格導入には至っていないですが、引き続き評価しつつ今後のバージョンアップに期待しています。 さいごに 今回紹介したScout Suiteの監査項目はごく一部であり、他にも多数の監査項目があります。オープンソースのScout Suiteを利用すれば、クラウド環境のセキュリティ監査を無料で実施できるので、まずは一度ご自身のクラウド環境の監査を実施してみてはいかがでしょうか。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター