株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

こんにちは。開発部データエンジニアの遠藤です。現在、私はデータ×テクノロジーでZOZOグループのマーケティングを支援するデータチームに所属して、データ処理基盤の運用などに従事しています。 本記事では、Lookerを用いて運用中のデータ集計基盤をきれいなデータをスマートに取り出せる基盤に改良した件について報告します。 データ集計基盤で燻っていた問題 1. クエリ管理の限界 2. 集計定義に対するデータの信憑性が謎 Lookerは何が良い? ~データガバナンス機能~ LookML データディクショナリ Gitによるバージョン管理 データ集計基盤(改)の設定フロー データ集計基盤(改)でのデータマート更新 まとめ データ集計基盤で燻っていた問題 ZOZOでは、サービスに関するあらゆるデータをBigQueryに集約しています。BigQueryに集約した大量のデータからデータマートとして必要なデータを取り出し、BIツールの可視化や機械学習システムの入力データなどに用いています。 データマートは以下の2ステップで作成します。 【ステップ1】 ビジネス側でデータマートのアウトプットを決定・集計方法を定義 【ステップ2】 SQLを作成してデータ集計基盤にセット データマートは、BigQueryへのマスタデータ取り込み完了後にデータ集計基盤上でクエリを順次実行して、毎日更新しています。 しかし、データ集計基盤を運用していくにつれて以下の問題が発生しました。 クエリ管理の限界 集計定義に対するデータの信憑性が謎 1. クエリ管理の限界 ZOZOでは現在までに多くのデータマートが生成され、その総数は数百にのぼります。各データマートのクエリはデータ集計基盤内でファイルごとに独立、すなわち "データマートの数"="SQLファイルの数" なので、数百のSQLファイルを管理していることになります。 SQLファイルがここまで増加した背景として、車輪の再発明による量産状態になっていたことが挙げられます。データマートの作成要件があるたびにSQLを1から作り上げたため、アウトプットが似ている当時のベストエフォートクエリが複数のSQLファイルに散在していました。 また、データベースのシステム仕様に変更が生じた際、SQLファイル群から変更箇所を全て探し出して修正しなければならず、クエリの修正工数が膨大になることも問題となっていました。 そのため、クエリの効率的な管理の必要性が叫ばれていました。 2. 集計定義に対するデータの信憑性が謎 データベースのシステム仕様・ビジネス的な集計定義・データマートにおけるアウトプットは、基本的にクエリ内のSQLで表現し、それでも分かりにくい場合はクエリ内にコメントを付記することで管理していました。しかし、その管理では 集計仕様の把握コストの増加(クエリからそのクエリの表現に至った背景・経緯までは理解しにくい) 集計結果が当初取り決めた集計定義とずれる(定義に沿った集計フローに対してシステムレベルまで関係者全員の意識を合わせることは難しい) システム仕様変更に追随しきれない状態になると集計結果の正確性が左右される のような問題が起きてしまいました。 その結果、「今日の数字、何か間違ってない?」と集計データに関する問い合わせが頻発し、 (ビジネス側から見て)データ集計基盤における集計精度の信憑性を疑問視する (システム運用側から見て)原因調査からリカバリまでの誤集計に対する一連の障害対応の工数が膨らむ といった誰も得しない状況に陥る恐れがありました。 そのほかにも、データ集計基盤には様々な根深い問題が存在していて、以下の図に示す内容に集約されます。 現状のデータ集計基盤では、ビジネス側で集計定義が完璧に決められていたとしても、誤った情報などが蔓延りやすくデータドリブンな意思決定の精度を下げるリスクがありました。 Lookerは何が良い? ~データガバナンス機能~ データチームは、データ集計基盤で起こる様々な根深い問題に対してシステムによる解決アプローチを模索したところ、Lookerというツールに目をつけました。LookerはLooker Data Sciences, Inc. がSaaSで提供しているBIプラットフォームです。 ビジネスインテリジェンス&ビッグデータ分析ソフトウェア - Looker 充実したReporting・Visualization機能がLookerの大きな特徴で、Lookerを国内で導入している企業のほとんどがBIツールとして使用しています。 Lookerと他のBIプラットフォームツールの相違点は様々あるのですが、ここで特筆すべき決定的に違う点…。それは…。 データガバナンス機能 です。 システム的にデータ構造を管理して正確なデータ利用を促すアプローチはLookerの唯一無二の特徴です。その特徴がLooker導入の決め手の1つになりました。 Lookerはデータガバナンス機能として以下の機能を備えています。 LookML データディクショナリ Gitによるバージョン管理 LookML LookerではLookMLというデータモデリング言語が採用されています。SQLに代わりLookMLで定義・集計などを記述します。 SQLは言語の特性上クエリ間の情報を転用しづらい側面がありますが、LookMLはSQLが不得意な再利用性・拡張性をカバーできます(LookMLの詳しい説明は 公式ドキュメント をご覧ください)。 例えば、「テストアカウントは計上しない」という集計定義があったとします。 通常、SQLではWHERE句 WHERE member_id NOT IN (12345, 23456, 34567, 45678, 56789) -- テストアカウントは集計から省くようにする を書きますが、LookMLではどのように書いていくのでしょうか? まず、テストアカウントのタグ付けを行うために test_account_tag というdimensionを定義します。 dimension : test_account_tag { case : { when : { sql : $ { member_id } IN ( 12345 , 23456 , 34567 , 45678 , 56789 ) ;; label : "テストアカウント" } } } 最初に定義したdimension test_account_tag は ${test_account_tag} や field: test_account_tag という表現で他の箇所で参照できます。 もしテストアカウントの member_id 構成が変更になった場合、key sql のvalueに記されているIN句のみ修正すれば、テストアカウントに関する条件が含まれる全ての定義が変更されたことになります。 これにより、システム仕様が変更された際に生じる手間が従来のSQLファイル修正と比較すると少なくなる効果があります。 次に、テストアカウントのタグがつけられているかを示すフラグを test_account_flag というdimensionで定義します。 dimension : test_account_flag { type : yesno sql : $ { test_account_tag } = "テストアカウント" ;; } さらに、「テストアカウントは計上しない」をkey filters で表現します。以下のLookMLではユーザー数を集計する際にテストアカウントを除くフィルタを適用させるという定義になります。 measure : user_count { type : count_distinct sql : $ { member_id } ;; filters : { field : test_account_flag value : "no" } } このように、Lookerを用いると無機質に掃き出されたシステムデータをビジネス定義に即した意味あるデータへ変換するELTが容易にできるようになります。 また、Lookerは仮想的にデータモデルの変換・派生カラムの作成ができるので、集計途中の中間データを不必要に量産しないメリットもあります。 データディクショナリ 会員情報を表すテーブル member_info のDDLをLookML化した一部を以下に示します。 view : member_info { sql_table_name : `zozo_original_table.member_info` ;; dimension : member_id { type : number sql : $ { TABLE } .member_id ;; label : "会員ID" } dimension : resign_flag { type : yesno sql : $ { TABLE } .resign_flag ;; label : "退会したか?" description : "TRUE…退会している状態 \n FALSE…退会していない状態" } 上記のように、LookMLには label description というkeyが用意されており、これらのkeyでデータ定義をラベリングできます。データ定義をラベリングできるということはデータディクショナリとしての使用が可能であることを意味します。 開発速度がとても速いWeb開発の現場でDB定義書のようなドキュメントでメタデータ管理を行った場合、ドキュメントがシステムに追いつかない状態、つまりドキュメントとプログラムとの乖離がしばしば発生します。 Lookerは任意のデータソースに存在するテーブルの各DDLから作成した各Viewを集計フローの根幹として使用します。そのため、正確な集計を行うために最新のメタデータに更新せざるをえなくなります。 これにより、Looker内ではデータディクショナリの最新状態を保つことができます。 Gitによるバージョン管理 Lookerは、LookMLのソースコード管理にGitを採用して、ソースコードの品質を担保しています。GitHubなどのGitを用いたソフトウェア開発のプラットフォームにも連携できます。 つまり、既に運用中のGitHubと連携することで複数人開発・相互レビューといった開発体制をとることができて、データチームでもこの体制を取り入れています。 データチーム管理のデータマート作成Gitリポジトリでは、集計ガイドラインやLookMLの命名規則・記述規則といったコーディング規約を決め、互いにReviewしやすい開発体制を敷いています。 データ集計基盤(改)の設定フロー データチームでは今回の取り組みをデータ集計基盤(改)として開発しています。データ集計基盤(改)は以下のフローでデータマート生成の設定が行われます。 まず、BigQueryに存在するテーブルのDDLをOriginal Viewとしてそのまま読み込み( 《1》 スキーマ情報読み込み )、そのOriginal View群を用いて分析に適した仮想データベースを構築します( 《2》 データモデル変換 )。その仮想データベース上で必要な情報抽出から集計まで行ってデータマートViewを作成します( 《3》 データマートView生成 )。 ちなみに、データ集計基盤(改)は「データマート作成のLookML内では原則SQL文完成形を直接書かない」というポリシーを持っています。つまり、 SELECT … FROM … といったSQL文を直接LookMLに入れ込むことはシステム的に可能ですが、それをあえて禁止しています。 というのも、LookerがLookMLを基にクエリを自動生成してくれるので、このポリシーがクエリ品質の均一化・データマート作成過程におけるブラックボックス化防止に繋がるためです。 データ集計基盤(改)でのデータマート更新 Lookerには好きなタイミングでアウトプットを中間データとして保存する機能が存在するのですが( PDT )、データ集計基盤(改)では、既存のデータ処理基盤との兼ね合いを考慮して、GCPの機能を用いてデータマートを更新しています。 BigQueryへのデータ取り込み完了直後にCloud Pub/SubをKickして、Cloud Functions経由でCloud Composer内のAirflow DAGを起動させます。 DAGの実行中にLooker API(Lookerが提供しているAPI)にリクエストを送り、そこからデータマート更新用クエリをレスポンスデータとして取得します。 APIで取得したクエリをBigQueryクライアントライブラリを使用したSDKを通して実行することで、各データマートに即した更新を行っています。 まとめ Lookerを導入してデータ集計基盤の改善を図ることでデータユーティリゼーションを向上させる取り組みを報告させていただきました。 現在、データチームではデータマート更新処理をこのデータ集計基盤(改)へ順次移行している最中です。 移行中はBigQueryの最適パフォーマンスに即したLookML記法を研究してブラッシュアップしていったり…とまだ発展途上のデータ集計基盤(改)ですが、課題が徐々に解決しており、その効果も表れています。 ビッグデータを集計するシステムを運用していると、冒頭に紹介した諸問題を抱えているにもかかわらずその最適なソリューションはなかなか見つからなく、苦い思いをしている方も少なくないのかなと思います(少なくとも私は長年そう思っていた)。 この記事で快適なビッグデータのシステム運用の手助けになれば幸いです。 このように、データチームではエンジニアの強みを生かしながらデータを中心とした技術課題・マーケティング課題に取り組んでいます。メンバーを絶賛募集していますので、ご興味のある方は以下のリンクからぜひご応募ください! www.wantedly.com
はじめに こんにちは! ZOZOテクノロジーズ開発部の鈴木( @zukkey59 )です! 普段は、 「ファッションコーディネートアプリ WEAR」 のAndroidアプリを担当しています。 私のWEARに配属されて初めての大きな仕事は、新規登録・ログイン機能へAndroid Jetpack コンポーネントのNavigationを導入することでした。 導入によってある程度知見がためることができました。 今回はNavigationをこれから導入したい!と検討している方、もしくは興味がある方に向けて、自身の知識の整理もかねて紹介したいと思います。 既に、Navigationの記事はQiitaやMediumに数多くあるので被っている内容もありますが、今回は導入からテストまでの一連の流れとその中で気づいた注意すべきところなどを一通り紹介させていただきます。 要件や仕様として、WEARで使用しなかったNavigationの機能について触れることはありません。 記事を読み進める前の判断として、想定している読者とこの記事に書いてあること、書いていないことをまとめました。 事前に目を通していただけると幸いです。 想定している読者 Android開発を主に業務でされている方 プロダクトにNavigationを利用したいと思っている方 Navigationをまだ利用したことがなく、初めて利用する方 NavigationのUIテストを書いてみたい方 Navigationのハマりどころについて事前に知っておきたい方 Espressoについての基礎知識を持っている方(CodeLaboを終了した程度を想定) AndroidX移行済みである方 今回書いてあること Navigationの説明 Navigationの導入手順 (DeepLinkを除く)Navigationの実装手順、利用の仕方 (DeepLinkを除く)Navigationのテストの書き方 今回書いてないこと NavigationのDeepLink周りについて マルチモジュール関連の挙動 プロダクト全体でのSingleActivityでの運用など Mockito、Espressoの導入、使い方などの説明 実際に導入した箇所はこちらです。 また、開発環境は次のとおりです。 Mac OS Mojave 10.14.5 Android Studio 3.5.1 それでは早速、Navigationについてみていきましょう!! Navigationとは? Android Jetpack コンポーネントのライブラリの一つです。 Android Jetpackは、開発者向けにAndroidアプリを簡単に作成するためのライブラリやツール、ガイダンスをまとめたものです。 その中の一つである、 Navigation はアプリ内の複雑であった画面遷移を、簡単にしてくれるライブラリです。 Navigationの実装のTipsに入る前に、まずはNavigationの原則について確認していきましょう。 Navigationの原則 公式は、 こちら にあります。 簡単に要約すると次の通りです。 Fixed start destination 出発点を決めます。Navigationを利用する場合には 開始位置を固定する 必要があります。 Navigation state is represented as a stack of destinations Navigationの状態はスタックで表現されます。 バックスタックはすべてNavigationが管理 してくれますが、自分でバックスタックを管理することもできます。 Up and Back are identical within your app's task アプリ内において画面上部のapp barに表示されるUpボタンと画面下部のナビゲーションバーに表示される戻るボタンの挙動は 同じ です。 The Up button never exits your app Upボタンはアプリを終了させる事はしません。 Deeplinkでアプリを起動した場合、Upボタンはアプリ内で新たに手動による操作を模倣して作成されたバックスタックをたどり、Deeplinkを起動したアプリに戻ることはありません。 戻るボタンはDeeplinkを起動したアプリに戻ります。 Deep linking simulates manual navigation Navigationは、DeepLinkに対応しています。 DeepLinkは手動での画面操作を模倣したバックスタックを生成します。 DeepLink経由で起動した場合には、既存のバックスタックは削除されてDeepLinkにより生成されたバックスタックに置きかわります。 今回、WEARで導入した新規登録・ログインのリニューアルに関しては、要件としてDeepLinkは用いなかったので、DeepLink以外で関わることのみを紹介いたします。 Navigationの原則について確認いたしましたので、次は導入の手順について見ていきましょう。 導入手順 早速、Navigationを利用する準備を始めましょう! Navigationの最新バージョンは、 2.1.0 です。(2019年10月現在) はじめに、Navigation Supportを導入するためにapp配下の build.gradle に次の2行を記載しましょう。 build.gradle dependencies { // Kotlin implementation "androidx.navigation:navigation-fragment-ktx:2.1.0" // 追加 implementation "androidx.navigation:navigation-ui-ktx:2.1.0" // 追加 } WEARではKotlinを利用しているので上記のように記載しております。 Javaを利用する場合は次のように記載してください。 build.gradle dependencies { // Java implementation "androidx.navigation:navigation-fragment:2.1.0" // 追加 implementation "androidx.navigation:navigation-ui:2.1.0" // 追加 } まだ、Jetpackを利用されていない場合は、プロジェクト配下の build.gradle に次の1文を追加してください。 build.gradle allprojects { repositories { google() // 追加 } } 【参考リンク】 Get started with the Navigation component | Android Developers Adding Components to your Project | Android Developers 導入の準備は、たったこれだけでOKです! 次は実装のTipsについて見ていきましょう! 実装のTips NavigationGraphを作成する NavigationGraphは、 ナビゲーションを管理する役割 を持っており、遷移の目的地と遷移するアクションなどを含むリソースファイルです。NavigationGraph自体はxmlファイルになります。どこに遷移するのか、どのような種類の遷移間のデータを持たせるのかなどをここに定義します。 まずは、NavigationGraphの追加の仕方について説明していきます。 最初に、File > New > Android Resource Fileを選択します。 画像でみると、次のような状態です。 選択後に、New Resource Fileというポップアップが表示されますので、File nameには自分が作成したいNavigationGraphの名前を入れて、Resource typeはNavigationを選択します。これでOKを押すと作成が完了です。 画像でみると、次のような状態です。 作成すると、res > navigationにて自分の定義したファイルが生成されていることがわかります。 今回の機能改修に関しては、 navigation_graph_entrance として作成しました。 画像でみると、次のような状態です。 作成が終わるとXMLの内容は次のようになります。 navigation_graph_entrance.xml <? xml version = "1.0" encoding = "utf-8" ?> <navigation xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" android : id = "@+id/navigation_graph_entrance" > </navigation> これで、NavigationGraphの作成は終了です。 次は、NavHostFragmentを用意していきましょう。 NavHostFragmentを用意する NavHostFragmentの内部のjavadocを見ると、次のようにあります。 NavHostFragment provides an area within your layout for self-contained navigation to occur. ここから考えることとして、NavHostFragmentとは「Navigationが機能するためのハコのようなFragmentである」という認識で良さそうです。 このNavHostFragmentをもった、Navigationの管理をする用に一つのActivityを作成します。 今回の新規登録・ログイン周りでは、入口部分であるという意味を込めて、 EntranceActivity と定義しました。 用意したActivityのレイアウトファイルである、 activity_entrance.xml に次のように記載します。 activity_entrance.xml <? xml version = "1.0" encoding = "utf-8" ?> <layout xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" > <androidx . constraintlayout . widget . ConstraintLayout android : layout_width = "match_parent" android : layout_height = "match_parent" > <fragment android : id = "@+id/nav_host" android : layout_width = "0dp" android : layout_height = "0dp" android : name = "androidx.navigation.fragment.NavHostFragment" app : navGraph = "@navigation/navigation_graph_entrance" app : defaultNavHost = "true" app : layout_constraintTop_toTopOf = "parent" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintStart_toStartOf = "parent" app : layout_constraintEnd_toEndOf = "parent" /> </androidx . constraintlayout . widget . ConstraintLayout> </layout> WEARでは、DataBindingを併用しているためlayoutタグで囲っています。 まずは、 activity_entrance.xml のfragmentタグに着目してください。 ソースコード中の...は省略記号を示します。 activity_entrance.xml <fragment android : id = "@+id/nav_host" ... android : name = "androidx.navigation.fragment.NavHostFragment" app : navGraph = "@navigation/navigation_graph_entrance" app : defaultNavHost = "true" ... /> fragmentのandroid:nameに androidx.navigation.fragment.NavHostFragment を設定します。NavHostを実装したクラスを指定する必要があるようです。特になければ、上記のように設定すれば問題ないでしょう。 次に、fragmentのapp:navGraphに先ほど作成したNavigationGraphを設定します。 今回の場合は、 navigation_graph_entrance を作成したので、 @navigation/navigation_graph_entrance と設定します。 navGraphの属性は、NavHostFragmentに自分が作成したNavigationGraphを関連付ける役割を持ちます。 defaultNavHostをtrueにされたNavHostFragmentが戻るボタンをインターセプトします。 同時に複数あるNavHostのdefaultNavHostをtrueにしてはいけません。 複数のNavHostがなければ、trueにしておくのが良いかと思います。 次は、NavigationEditorについてです。 NavigationEditorを利用する NavigationEditorとは、視覚的にNavigationGraphを編集することができたり、直接xmlでNavigationGraphを編集することができるAndroid Studioの機能の一つです。 WEARで利用している箇所でのEditor表示は次のとおりです。 左の赤い枠の部分が Destinations panel といわれるところで、ここではNavigationのHostと後述する青枠のGraph Editorの中にある全ての遷移先の目的地のリストを表現します。 真ん中の青い枠の部分が先ほど出た Graph Editor といわれるところで、自身が作成したNavigationGraphを視覚的にわかりやすく表示してくれます。ここで編集したことと、xmlの中で編集したことは同期されます。 右の緑の枠の部分が Attributes といわれるところで、後ほど説明いたしますが選択しているGraphのArgumentsやactionなどの属性を表示したり、actionのAnimationやAttributesのIDやPop behaviorなどの属性を表示します。 ここで編集したことと、xmlの中で編集したことは同期されます。 左下のオレンジの枠の部分では、Designタブでデザインのプレビュー画面に、TextタブでXML画面に表示を切り替えることができます。 次は、NavigationEditorで遷移先となるDestinationの追加の仕方について見ていきましょう。 Destinationの追加の仕方 Destinationの追加の仕方は、とてもシンプルです。 上記画像の、赤枠で囲まれている New Destinationアイコンをクリックすると、検索ボックス、作成するボタン、並びにクラスのリストが表示されます。 検索ボックスには、まだ追加されていないクラスを入力すると一覧に出てくる仕組みになっているので、先に新しくActivityやFragmentを作成した場合は、ここから検索してNavigationリソースにも追加するのが良いと思います。 また、青枠で囲まれている、Create new destinationと書かれている箇所をクリックすると、Fragmentを作成する画面が表示され特にチェックを外さずにFinishすると、新しいFragmentとレイアウトリソースファイルを自動的に作成し、Navigationリソースにも追加してくれます。 緑枠には、既存でNavigationリソースに追加していないActivityやFragmentのファイルがリスト形式で表示されます。 xmlにFragmentタグで直接定義することで追加することもできます。 以上が、Destinationの追加の仕方についてでした。 次は、Actionの追加の仕方について紹介します。 Actionの追加の仕方 Actionの追加の仕方もとても簡単です。 赤枠で囲まれている右矢印のアイコンをクリックすると、Add Actionというポップアップが出てきます。 また、 Attributes のActionsの+をクリックすると、リストが表示され Add Action という項目が出てくるので、それをクリックすることでも同様のことができます。 ここで遷移元と遷移先やAnimationの仕方など遷移に関わる項目を設定します。Navigationではこれら全てをまとめてActionとしています。 このとき何も選択していなければデフォルトでFromにnav_hostが入りますが、前章で出てきたDestinations panelのGRAPHのリストの項目に存在する、いずれかのGraphを選択していると選択されたGraphがデフォルトでFromに入ります。 メールフォーム入力画面を選択した状態でしたので、画像ではFromにmail_formと入力されていることがわかります。 IDは、指定がなければ自動的にActionのIDを生成してくれるので、特に気にする必要はありません。 Fromは遷移する元の画面のIDを指定し、Destinationには遷移させたい画面のIDを設定します。 Transitionでは、次画面への遷移時と、元画面へ戻る遷移時のアニメーションを選択することができます。 アニメーションをさせたくない場合は未入力でも問題はありません。自分で作成したアニメーションも設定することが可能です。 Pop Behaviorでは戻るときに「どこまで戻るのか」の定義ができます。 Inclusiveにチェックを入れると、popUpToの前まで戻ることができます。 実際にプロダクトに導入してみたところ、戻るの挙動を変えたい場合が想像以上に出てきたので非常に有用でした。 Launch Optionsではシングルトップで起動するかどうかを選べます。必要な場合はチェックを入れましょう。 後から変更したくなった場合にも、xmlを編集すれば変更可能なので、間違えても問題はありません。 Actionを追加する方法について学んだら、次はAttributesの追加について見ていきましょう。 Attributesの追加の仕方 AttributesもNavigationEditorから簡単に追加することができます。 次の画像を見てください。 Attributes のところで、 Arguments という項目があり右側に+アイコンが表示されています。こちらをクリックすると、Add Argument Linkというポップアップが出てきます。 ここで追加したい情報を設定することができます。 Nameにはargumentの名前を入れます。 Typeは、Integer / Float / Long / Boolean / Stringだけでなく、ResourceやCustomSerializableなども選ぶことができます。 ArrayやNullableもチェックを入れられる場合は設定可能です。また、defaultValueも設定可能です。 もちろん、NavigationEditorからだけでなく直接xmlを編集して追加することも可能です。 SafeArgsを利用する SafeArgsとは、NavigationのGradleプラグインでActivityやFragmentへの引数付きの遷移をする際に、型安全なObjectとビルダークラスを生成してくれます。 こちらを利用することで、自身でキーを設定したり、型があっているかの確認をしなくても自動生成されたクラスを利用するだけになるため非常に便利です。 導入の手順はいたってシンプルです。app配下のbuild.gradleにて次の一文を追加します。 build.gradle apply plugin: 'androidx.navigation.safeargs' 次に、project配下のbuild.gradleに次のコードを追加します。 build.gradle buildscript { dependencies { classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0" } } 2019年10月現在、この記事を書いている時の最新のStableバージョンは 2.1.0 です。 バージョンの確認を行いたい方は、 こちらのリンク から確認することができます。 公式ドキュメントにも導入方法が記載されております。 【参考リンク】 Use Safe Args to pass data with type safety もし、kotlinOptionsをapp配下のbuild.gradleに記載していない場合は、JVM targetを設定する必要があります。 その場合はapp配下のbuild.gradleに次のように記載しましょう。 build.gradle android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 } } SafeArgsの利用手順としては次の通りです。 遷移先にて渡したい情報を持つArgumentを定義 遷移元にてActionを定義 遷移元のFragmentで自動生成されたクラスに渡したい情報を付加して遷移 遷移先にて渡された情報を受け取る 実際のソースコードを見ていきながら、手順を確認していきましょう。 まず、 遷移先にて渡したい情報を持つArgumentを定義 するということを行います。 今回は、メールアドレス入力画面(mail_form)からアカウント作成画面(create_account)への遷移について例にとります。 次の画像の画面間の遷移になります。 アカウント作成画面にて、受け取りたい情報を定義します。 navigationのリソースファイルを開き、次のように記載をします。今回は、度々例に挙げている、 navigation_graph_entrance.xml で説明します。 navigation_graph_entrance.xml <fragment android : id = "@+id/create_account" android : label = "fragment_create_account" android : name = "(package名とパス).CreateAccountFragment" tools : layout = "@layout/fragment_create_account" > <argument android : name = "snsType" app : argType = "(package名とパス).SnsType" /> </fragment> NonNullなSNS連携情報を持つクラスを渡したいので、上記のように遷移先にて定義するようにします。 次に、 遷移元にてActionを定義 します。 メールフォーム画面からアカウント作成画面へ遷移するためのActionタグを、遷移元であるメールフォーム画面のfragmentタグに情報を追加します。 navigationのリソースファイルを開き、次のように記載をします。 navigation_graph_entrance.xml <fragment android : id = "@+id/mail_form" android : label = "fragment_mail_form" android : name = "(package名とパス).MailFormFragment" tools : layout = "@layout/fragment_mail_form" > <action android : id = "@+id/action_mail_form_to_create_account" app : destination = "@+id/create_account" app : enterAnim = "@anim/slide_in_left" app : popEnterAnim = "@anim/slide_in_right" /> </fragment> fragmentタグの中にactionタグを追加してアニメーションとdestinationを遷移先のcreate_accountにします。 Actionの定義が終わったら、最後は 遷移元のFragmentで自動生成されたクラスに渡したい情報を付加して遷移 させます。 遷移元のMailFormFragmentにて、引数なしの場合は次のコードのように定義していた箇所を変更します。 MailFormFragment.kt findNavController().navigate(R.id.create_account) SafeArgsプラグインを利用すれば、 Fragment名 + Directions のクラスが自動生成されて、自身で定義したアクション名をもつ関数が自動で定義されるようになっています。 遷移元のMailFormFragmentでは次のようにして定義します。 MailFormFragment.kt findNavController().navigate( MailFormFragmentDirections.actionMailFormToCreateAccount( snsType ) ) これだけでOKです。 最後に、遷移先のCreateAccountFragmentにて情報を受け取ります。 SafeArgsプラグインを利用すると遷移先のクラスで、 Fragment名+Args のクラスが自動生成されます。 こちらの自動生成されたクラスを用いて、次のようにして情報を受け取ります。 CreateAccountFragment.kt // navArgsでarguments全体を取得する val args: CreateAccountFragmentArgs by navArgs() // 渡したデータを受け取ることもできる val snsType = args.snsType 受け取りはこれだけでOKです。 SafeArgsについての説明は以上です。 次に、今回の開発においてNavigationEditorを利用しましたが、非常に便利であったので、覚えておくといいことをご紹介させていただきます! NavigationEditorを利用する時に覚えておくと良いこと AutoArrange について 色々とDestinationを追加していったら、Graph Editorの中がごちゃごちゃしてくることがあります。 そんな時は、AutoArrangeを利用しましょう。 Auto Arrangeは次の画像の赤い枠で囲った部分をクリックすることで利用できます。 このAutoArrangeを利用した時の挙動が次のとおりです。 一瞬で、整列してくれるので、Graph Editor がゴチャゴチャして見辛くなったときに、利用するようにしましょう。 Go to XML というショートカットについて 開発を行っていると、Graph Editorを利用していたら該当箇所のxmlに素早くジャンプしたい時が出てきます。 その際に便利なショートカットが、 ⌘(コマンドキー) + B (Macの場合)で選択したアイテムのxmlリソースへ飛ぶことができます。 Graphだけでなく、Actionでも遷移することが可能です。 レイアウトの設定について xmlにて、toolsで該当のレイアウトリソースファイルを指定するとGraphEditorに反映され、視覚的にもGraphがどの画面なのかすぐにわかります。 例えば、WEARでのメールアドレス入力画面について見ていきましょう。 xmlでの定義は次のようにします。 navigation_graph_entrance.xml <fragment android : id = "@+id/mail_form" android : label = "fragment_mail_form" android : name = "(package名とパス).MailFormFragment" tools : layout = "@layout/fragment_mail_form" < !-- 追加 --> > ... </fragment> 上記のように対象のレイアウトリソースファイルを指定することで、次のような表示をGraphEditor内で確認できます。 以上、NavigationEditorを利用する際に覚えておくと開発が捗ると思うことの紹介でした。 次は、Espressoを用いたNavigationの遷移のテストについて紹介させていただきます。 Espressoを用いてNavigationの遷移のテストを行う Mockito、Espressoは導入している前提で、進めます。 WEARで利用しているEspressoのバージョンは 3.1.1 です。 Navigationの遷移のテストを行う前に準備をしましょう。 app配下のbuild.gradleに次の1行を追加します。 build.gradle debugImplementation 'androidx.fragment:fragment-testing:1.1.0' 導入はたったこれだけで大丈夫です! Navigationのテストの実装の手順は次のとおりです。 NavControllerをMockする テストしたい画面のFragmentScenarioを作成する FragmentにNavControllerのプロパティをセットする Actionで遷移した結果が正しいかのテストを行う まずはじめに、1~3をやっていきましょう! 今回は度々例として出ていたメールフォーム入力画面(MailFormFragment)を例に紹介いたします。 androidTestディレクトリの中に階層を同じくしたMailFormFragmentTestを作成します。 1~3はテストする前に下準備をします。コードとしては次のようになります。 それぞれコード上にコメントいたしました。 MailFormFragmentTest.kt @RunWith (AndroidJUnit4:: class ) class MailFormFragmentTest { private lateinit var mockNavController: NavController private lateinit var scenario: FragmentScenario<TestMailFormFragment> @Before fun setUp() { // NavControllerをMockする mockNavController = Mockito.mock(NavController:: class .java) // テストしたい画面のFragmentScenarioを作成する scenario = launchFragmentInContainer<TestMailFormFragment>(themeResId = R.style.AppTheme) // FragmentにNavControllerのプロパティをセットする scenario.onFragment { fragment -> Navigation.setViewNavController(fragment.requireView(), mockNavController) } } } setUp関数の中で、一通り1~3を行います。 NavControllerをまずはMockします。Mockitoは今回の趣旨に関わらないので説明を省略します。 NavControllerをMockしたら、テストしたい画面のFragmentScenarioを作成します。 Test用にMailFormFragmentを継承したクラスを作り、DaggerAndroidSupportを利用している場合はTest用にViewModelをMockします。 作成したTest用MailFormFragmentのFragmentScenarioを作成します。この時にthemeResIdを指定する必要があるので、AppThemeを指定するようにしてください。 ここまで準備をしたら、各Actionのテストを行います。 今まで、メールフォーム入力画面からアカウント作成画面を例にとって説明していたので、そちらを例にして説明していきます。 NavigationのActionが通るかどうかについては、次の1行だけ記入すれば、Actionが意図した挙動で成功するかを確かめることができます。 verify(mockNavController) .navigate( /* ここで遷移先のidを指定するか自動生成されたクラスのActionの関数を呼ぶ */ ) 実際のソースコード全体としては次のとおりです。 Espresso部分に関してもコメントを追加しました。 MailFormFragmentTest.kt @RunWith (AndroidJUnit4:: class ) class MailFormFragmentTest { @Test fun testNavigationFromMailFormToCreateAccount() { // MailForm入力欄をクリックする Espresso.onView(ViewMatchers.withId(R.id.mail_form)).perform(ViewActions.click()) // Mail用にランダムな文字列を作成 val randomString = RandomStringUtils.randomNumeric( 3 ) + RandomStringUtils.random( 20 , "abcdefghijklmnopqrstuvwxyz" ) val testMail = "wear $randomString @te.st" // MailForm入力欄に直接入れる。 // InputTextActionというViewActionを実装したクラスを作成して入れるとソフトウェアキーボードの影響を受けない。 Espresso.onView(ViewMatchers.withId(R.id.mail_form)).perform(InputTextAction(testMail)) // Fabをクリックする Espresso.onView(ViewMatchers.withId(R.id.fab)).perform(ViewActions.click()) // モックしたNavControllerでActionが正しく実行されたかどうかを確認する verify(mockNavController) .navigate(MailFormFragmentDirections.actionMailFormToCreateAccount(SnsType.Wear)) } } これで、対象のクラスを右クリックして、 Run 'MailFormFragmentTest(自分で作成したテストクラスの名前)'.. を実行して、テストがパスすればNavigationの遷移のテストとしてはOKです。 実装に際して参考にした公式リンクは次のとおりです。 【参考リンク】 Test Navigation Navigationのテストまで実装をすることができたら、開発中にいくつか気をつけることがあったので、一つずつ紹介させていただきます! プロダクトに導入する際に気をつけること 開始先を分けたい場合は、プログラム的に設定する必要な場合があるので注意 Navigationでは開始する際に、最初のDestinationをデフォルトで設定する必要があります。 先に説明したNavigationの原則1にある通り、最初の目的地をxml上で定義する必要があります。 WEARでは次のように開始先を固定しています。...は省略を意味しています。今回は、度々例に挙げている、 navigation_graph_entrance.xml で説明します。 navigation_graph_entrance.xml <navigation xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" xmlns : tools = "http://schemas.android.com/tools" android : id = "@+id/nav_host" app : startDestination = "@+id/mail_form" > <!-- ここで開始先を定義する --> ... </navigation> しかし、開始先を分けたい状況が、少なからず出てくると思います。 WEARでは、Navigation Drawerにログインと会員登録からの導線があり、その後にNavigationを用いているため、開始先を分ける必要が出てきました。 画像でみると次の画面です。 この場合は、 条件付きナビゲーション という形になります。 このような場合に備えて、上記リンクのFirst-time user experienceという項目で対応方針を公式ドキュメントに記載されてあります。 やり方としては、main_fragmentのようなFragmentを用意して、登録用のNavigationGraphを用意して遷移させる手法です。こちらの方法が一番綺麗にできるため、できる限りこの方法を採用していくのが良いと思います。 ただし、既存のプロダクトで部分的にNavigationを使用する場合で、遷移箇所がDrawerの項目からとなると、また少し工夫する必要があります。 遷移後の画面では、ログインと会員登録にフローが分かれます。遷移元のDrawerの画面には、今回はNavigationの適用を行う予定はなく、「ボタンを押した後にフローを分けたい」という要件で部分的に適用するという状況が起きました。 この為、ドキュメント通りに WEARでは、プログラム的に開始位置をFragmentが作られたタイミングで設定するようにしました。 コードとしては次のような形になります。 AuthType.kt sealed class AuthType : Serializable { abstract fun setUpStartDestination(navigationHostFragment: Fragment?) object Login : AuthType() { override fun setUpStartDestination(navigationHostFragment: Fragment?) { // NavigationHostFragmentからNavigationGraphを取得する val inflater = navigationHostFragment?.findNavController()?.navInflater ?: throw IllegalArgumentException() val graph = inflater.inflate(R.navigation.navigation_graph_entrance) // NavigationGraphに開始先の目的地のFragmentのIDを指定 graph.startDestination = R.id.login // NavigationGraphをNavigationHostFragmentに設定する navigationHostFragment.findNavController().graph = graph } } object Register : AuthType() { override fun setUpStartDestination(navigationHostFragment: Fragment?) { // NavigationHostFragmentからNavigationGraphを取得する val inflater = navigationHostFragment?.findNavController()?.navInflater ?: throw IllegalArgumentException() val graph = inflater.inflate(R.navigation.navigation_graph_entrance) // NavigationGraphに開始先の目的地のFragmentのIDを指定 graph.startDestination = R.id.mail_form // NavigationGraphをNavigationHostFragmentに設定する navigationHostFragment.findNavController().graph = graph } } } AuthTypeというsealed classを定義してLogin用とRegister用のobjectを作成し、コメントにある通り、それぞれの分岐に対して目的地を指定することで開始先を分けることが可能です。 既存に部分的に導入した場合にこのようなパターンがありましたので、少しでも参考になれば幸いです。 次は、Navigationでの戻る挙動を変えたい場合について紹介します。 特定のFragmentの戻る挙動を変えたい場合はCustomBackNavigationを利用する Navigationを用いているときに、要件として「この場合は戻るの挙動を変えて欲しい」ということが出てきます。 特定のFragmentで戻る挙動を変更したい場合に使えるのが、 Custom Back Navigation です。 FragmentでonViewCreatedなどが呼ばれた際に、コールバックを設定します。 Fragmentで戻る挙動が変えたい場合については、コードだと次のようになります。 requireActivity().onBackPressedDispatcher.addCallback( this ) { /** * ここら辺に戻るの挙動で追加したい処理を書く */ } onBackPressedDispatcher.addCallbackにて処理したい内容を書いたコールバックを追加することで、該当のFragmentだけ戻るの挙動を変えることができます。 addCallbackの内部を見ると、defaultでenabled=trueになっているので、要件的にONとOFFを切り替える必要がなければ、上記のソースコードのみで問題ないです。 idを確認してから起動しないとクラッシュする Navigationを用いて遷移する場合に、次のようなエラーが出てクラッシュする場合があります。 stackoverflowにも同様の事象に遭遇している状態がみられるようです。 現状としては、ボタンの連打防止で回避できたという方もいらっしゃいますが、それ以外の条件でも発生している場合もあり、私も連打ではなく、通常通りの押下で再現することがありました。 ライブラリ側のバグの可能性が高いのではないかと考えています。 stackoverflow.com Fatal Exception: java.lang.IllegalArgumentException navigation destination XXX is unknown to this NavController このエラーは、開発中に、次のように遷移する箇所で発生することが度々ありました。 findNavController().navigate( // 遷移先のIDなどを指定する ) 調査したところ、現在のスタックの最上部のDestinationのidが遷移する元のFragmentのidと異なるタイミングがあり、そのことが起因しクラッシュすることがあります。 現状の対応方針としては、現在のスタックの最上部のDestinationのidが遷移する元のFragmentのidであるかどうかを事前にチェックするか、例外でエラーを握りつぶすかの、二つの手法のみのようです。 WEARでは、下記のように、チェックを行うようにして対応しました。 次のコードはメールフォーム入力画面を例にしています。 if (findNavController().currentDestination?.id == R.id.mail_form) { findNavController().navigate( // 遷移先のIDなどを指定する ) } 以上で、プロダクトにNavigationを導入する際に、気をつけることについて「これだけは事前に知っておくと嬉しいかも」をまとめてみました。何かの参考になれば嬉しいです! さいごに いかがだったでしょうか? Navigationを実際にプロダクトに導入してみて、次のようなメリットとデメリットがあると感じました。 メリット 視覚的に遷移先や各Actionで渡す情報を見ることができる 導入までの流れが簡単にできる 公式ドキュメントが充実している スッキリと遷移部分を書くことができる 遷移する部分でキーの設定や型の確認などをせずに、Navigation側(プラグイン)で自動でやってくれる デメリット 学習コストがある 複数人で同一のNavigationGraphを触る場合、コンフリクトに注意する必要がある Navigationライブラリ側のバグがある 他にもやり方があったり、もっとこうしたほうがいいというアドバイスがございましたら、私のTwitterアカウント @zukkey59 までご連絡をいただけますと嬉しいです! 今後、新たに知見が溜まったら追加で書きたいと思います。 さいごに、ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは、ZOZOテクノロジーズで機械学習の研究開発をしている松井・真木です。2019 年 9 月末にコペンハーゲンで行われた推薦システムのトップカンファレンスである RecSys 2019 に参加してきたので、本稿では参加報告と気になった論文の紹介をします。 recsys.acm.org Overview RecSys では推薦システムに関するアルゴリズムはもちろん、インターフェースやユーザー心理など幅広い話題を扱っています。今年は参加チケットが売り切れたことからも注目度の高さが伺えます。研究発表はロングペーパーとショートペーパーからなり、採択率はそれぞれ 19%、24%でした。個人的に印象的だったのは、参加者の 7 割以上が企業所属である一方、発表者の数でみるとアカデミアの勢力も強かった点です。私はこれまでいくつかの国際会議に参加してきましたが、RecSys は笑いを取りに来る発表者が多かったり、発表者に対して口笛や声援が飛んだりするなど、これまでで一番明るく和気藹々とした会議だったように思います。 トレンド トレンドとしてはっきりしていたのは、推薦システムが社会に与えうるインパクトや研究手法に関する自己批判です。最も象徴的なのは、深層学習ベースの手法が本当に従来の手法より優れているのかを検証したメタ研究が最優秀論文賞を受賞した点でしょう。当該論文では、精度比較以前にそもそも再現実験を実施することができた論文が半分以下しか存在せず、その中でも従来法よりほぼ一貫して優れていたのが Mult-VAE という手法だけだったという、いささかショッキングな内容でした。また、2件行われたKeynoteはそれぞれ法学者と社会学者による講演で、いずれも社会とデータとの関わりに関する内容でした。さらに、最終日にはResponsible Recommendation(責任ある推薦システム)というテーマでパネルディスカッションが開催され、フィルターバブルやフェイクニュースを推薦システムの技術が助長しないために何ができるかという点について議論が行われました。研究者コミュニティが単純な精度競争に陥らず、より良い社会を作るための問題意識を共有していることが印象的でした。 Keynote Keynote の内容について簡単に触れます。文中の画像は 公開されているスライド から引用しています。 1件目は、法学者の Mireille Hildebrandt 博士による講演で、行動(データ)主義批判と GDPR(EU における個人情報保護法)に関する内容でした。ヒトは頭で「やるべき」と考えていることを実際に実行するとは限らず、逆に「やるべきではない」と考えていることをついやってしまうこともあります(いわゆるMind-body problem)。したがって、行動データに対して最適化された推薦システムが、人や社会にとって良い結果をもたらすとは限らないという指摘がありました。例えば、ダイエット中の人がカロリーの高い食事を推薦され、それを購入したとします。購入に至ったので行動主義の観点から言えば「良い推薦」と言えますが、社会や本人にとっては必ずしも良いとは言えません。推薦システムの未来を考えるうで興味深い講演でした。 Mind-body Problem (出典: https://recsys.acm.org/recsys19/keynotes/ ) 2 日目は社会学者の Eszter Hargittai 博士から、オンラインサービスから得られるデータに生じうるデモグラフィックなバイアスについて示唆に富んだ講演が行われました。この講演では、年齢・性別・学歴・収入などの違いによって、使用するオンラインサービスの選択、さらにはサービスの使い方にまで差があるということが示されました。例えば、同じWikipediaというサービスでも、閲覧するだけのユーザーと編集を行うユーザーとでは異なるデモグラフィックな特徴があります。したがって、「Wikipedia のユーザー」のような大雑把な認識では、思わぬバイアスに影響される可能性があります。逆に、想像するバイアスが実際には存在しない可能性もあります。例えば、「若者は高齢者よりもインターネットを使うスキルが高い」というステレオタイプはある程度一般的であると思われますが、実際に調査してみると年齢とインターネット・スキルとの間の相関は低かったそうです。サービスの設計を考えるうえでたいへん勉強になる講演でした。 論文紹介 ここからは、筆者の 2 人が個人的に気になった論文を簡単に紹介していきます。担当は以下になります。また、文中の画像はそれぞれの論文から引用しています。 真木 Users in the Loop: A Psychologically-Informed Approach to Similar Item Retrieval Relaxed Softmax for PU Learning 松井 Latent factor models and aggregation operators for collaborative filtering in reciprocal recommender systems SMORe: modularize graph embedding for recommendation Users in the Loop: A Psychologically-Informed Approach to Similar Item Retrieval 著者:Amy A. Winecoff, Florin Brasoveanu, Bryce Casavant, Pearce Washabaugh, Matthew Graham (True Fit) 出典: https://dl.acm.org/citation.cfm?id=3347047 どんなもの? コンテンツの類似度を測る指標として、ユークリッド距離やジャッカード類似度に代わって、心理学の文脈で提案された Tversky の類似度を用いることを提案し、妥当性を検証しました。 先行研究と比べてどこがすごい? ユークリッド距離やジャッカード類似度は、ヒトが心理的に感じる類似度と乖離している可能性が指摘されているので、ヒトの認知の特徴を組み込んだ類似度を使います。また、前述の類似度はすべての特徴量が等しく貢献する式になっていますが、実際は特徴量によって類似度の認知に与える影響が異なるという仮説を検証しました。 技術や手法の肝はどこ? ユークリッド距離やジャッカード類似度は対称ですが、ヒトが感じる類似度は必ずしも対称ではないことが知られています。Tversky の類似度の非対称性を考慮できるモデルになっています。なお、Tversky の類似度は要素が 1 か 0 で表わされるベクトル同士の類似度を測るために使えます(連続値を取るベクトルに対しては使えません)。 どうやって有効だと検証した? 上図のように、クエリの画像が 2 つの選択肢の画像のうちどちらに似ているか答えてもらう主観評価で教師データを収集し、Tversky 類似度を特徴量としてロジスティック回帰で予測しました。比較対象としてジャッカード類似度を用いました。特徴量によって類似度の認知に対する貢献が大きいという仮説は、ロジスティック回帰の係数の比較により確認しました。 議論はある? 論文の著者らは Tversky類似度の有効性を主張していますが、論文に掲載されている数値や図を見る限りでは、ジャッカード類似度との差はほとんどないように見受けられました。心理学の知見を取り入れたアプローチは発展の余地があると思うので、今後の展開に期待したいです。 Relaxed Softmax for PU Learning 著者:Ugo Tanielian (UPMC, Paris, France), Flavian Vasile (Criteo Research, Paris, France) こちらの論文については、 RecSys’19 読み会にて発表してきたスライド があるので、そちらもご覧ください。 どんなもの? 2 値分類において「正例データ」と「ラベル不明データ」の2種類が与えられる Positive-Unlabeled(PU) Learning では、負例をどのようにサンプリングするかが問題になります。そこで新しくボルツマン分布に従って負例をサンプリングする方法を提案しました。 先行研究と比べてどこがすごい? ボルツマン分布を用いた負例サンプリングでは、ボルツマン分布におけるdegeneracy分布を調節することで負例になりやすさ(negativity)に関する事前知識を表現することができます。また、ボルツマン分布の温度パラメータを調節することでサンプリングの性質を変化させられます。 技術や手法の肝はどこ? PU Learning における負例サンプリングでは、以下 2 点のトレードオフを考慮する必要があります。 誤分類しやすい(決定境界の近くにある)データをサンプリングしたい。この場合、本当は正例のデータを負例としてサンプリングしてしまう危険が大きくなります。 正例を誤って負例としてサンプリングしたくない。この場合は決定境界の遠くにあるデータをサンプルすればいいですが、モデルにとっては容易に識別可能なので情報があまり増えません。 提案法では、ボルツマン分布の温度パラメータによってこのトレードオフを調整できます。 どうやって有効だと検証した? 人口データに対する分布推定、単語類似度の学習、Next item prediction のタスクで評価しました。概ね従来法よりも良い結果がでています。 議論はある? 性能を評価する基準によって最適な温度パラメータの値が異なり、理論的に予想されるものと比べて妥当な結果が得られているように思えます。 Latent factor models and aggregation operators for collaborative filtering in reciprocal recommender systems 著者:James Neve (University of Bristol, Bristol, England), Ivan Palomares (University of Bristol & The Alan Turing Institute, Bristol, England) どんなもの? サービス内のユーザーを互いに推薦するシステムを相互推薦システム(RRS, Reciprocal Recommender Systems)と呼びます。相互推薦システムは、オンライン・デーティング・サービス(Tinder、Pairs など)、求人サービスでの応用が考えられます。相互推薦では、相互評価に基づいて推薦が行われます。 相互推薦システム特有の問題は? 任意のユーザーペアについて双方向から 2 つの評価値が得られるため、それらを 1 つの値に集約する関数を考える必要があります。特に、ユーザーペアの間で相互評価が一致している場合(お互いに好き、お互いに嫌い)に推薦するかは自明であるため、相互評価が一致していない場合(片思い)の扱いが特に問題になります。 従来、双方向からの評価値の集約関数には算術平均や幾何平均、調和平均などが用いられてきましたが、性能が比較されてこなかったので、何がベストかはわかっていませんでした。 また、従来法では、ユーザー a からユーザー b への評価値の計算量は、b を過去に高評価したユーザーの人数 n に対し O(n) でした。 先行研究と比べてどこがすごい? それぞれのユーザーを潜在因子で表現し、評価値はそれらの内積とすることで高速に計算できるようにしました。潜在因子は、男性から女性への評価値行列と女性から男性への評価値行列をそれぞれを行列分解することで獲得します。推論時は、ユーザーペアの候補に対し双方向の評価値を予測し、それらを集約した値をもとに推薦します。 また、集約関数の比較を初めて行いました。相互評価が一致していない場合、算術平均では大きい評価値の方を、幾何平均・調和平均では小さい評価値の方を強く反映します。オンライン・デーティングの文脈では、相互評価が一致しない場合は推薦しない方が望ましいので、幾何平均・調和平均の方がより適切であるという仮説を検証しました。 どうやって有効だと検証した? オンライン・デーティング・サービスの Pairs のデータを用いて、ユーザーのペアが成立か否かの 2 クラス分類を行いました。 提案法は、従来法と同程度の F1 スコアを維持しつつ、ユーザーのペアが 100 万ほどある巨大なデータセットでも実用可能な計算量に削減しました。 SMORe: modularize graph embedding for recommendation 著者:Chih-Ming Chen, Ting-Hsiang Wang, Chuan-Ju Wang, Ming-Feng Tsai(National Chengchi University, Taiwan) どんなもの? 本研究は新しい手法の提案ではなく、既存の推薦システムの手法をモジュールに分割した新しい見方を提案し、その見方にもとづいたライブラリを提供しています。モジュール化することで異なる研究を比較するときに見通しが良くなり、実装も楽になります。 github.com 技術や手法の肝はどこ? 協調フィルタリングベースの手法では、学習データはユーザーやアイテムをノードとしたグラフであると考えることができます。もっとも単純な手法ではユーザードメインとアイテムドメインからなる 2 部グラフですが、文脈を考慮する手法では、下図のように 3 つ以上のドメインからなるグラフとみなせます。 出典: http://cherry.cs.nccu.edu.tw/~g10018/recsys19_smore.pdf モデルの学習はグラフ上のノードを何らかの空間(ユークリッド空間など)に埋め込むことであり、推論は空間内での近傍探索を行うことであると解釈できます。 そこで、著者らはグラフ埋め込みを以下の 3 つのモジュールで表現することを提案しました。 Sampler:ユーザー・アイテムのグラフからノードをサンプルする方法 Mapper:サンプルしたノードを特徴量空間に埋め込む学習可能な写像 Optimizer:類似度または距離と、損失関数 発表スライド にある、いくつかの有名な手法を SMORe で表現した図がわかりやすいです。 個人的な感想 SMORe により、いくつかの有名な推薦手法は本質的に、隣接したノードの特徴量どうしは類似度が大きく、隣接しないノードの特徴量どうしは類似度が小さくなるように Mapper を学習していると捉えられるようになりました。 この捉え方は、意味の近いデータの特徴量どうしは近く、意味の異なるデータの特徴量どうしは遠くなるように特徴抽出器を学習させる metric learning の枠組みと似ているように感じました。以前の metric learning に関するブログ記事 で上げた metric learning の 4 つの構成要素と、SMORe の各モジュールの対応は以下のようになると考えています。 サンプリング → Sampler ネットワークの構造 → Mapper 計量 → Optimizer 損失関数 → Optimizer metric learning の応用の 1 つである fine-grained image classification では、N-pair サンプリングが提案されましたが、現在、SMORe で表されている手法の中では言及されていません。ただ、N-pair サンプリングは SMORe の Sampler として表現できます。 このように SMORe の対象分野を、推薦から metric learning の応用先に広げることで、metric learning における既存手法も SMORe の枠組みで捉え直せるか考えてみたいと思います。 また、上記のブログ記事では metric learning の構成要素は、問題設定やデータによって適切な選択肢が異なることを報告しています。そこで、SMORe においても同様に、問題設定やデータごとに各モジュールのどの選択肢が適しているかを明らかにしても面白いと感じました。 最後に 弊社では次々に登場する新しい技術を使いこなし、プロダクトを改善できる機械学習エンジニアを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、ZOZOTOWN iOSチームの荒井です。今回は9月に実施したZOZOテクノロジーズのサマーインターンシップについて紹介したいと思います。インターンシップを開催予定の方、ZOZOテクノロジーズのインターンシップに興味がある方の参考になれば幸いです。 INTERNSHIP 2019 SUMMER概要 この夏、ZOZOテクノロジーズとしては初となる就業型インターンシップ「 INTERNSHIP 2019 SUMMER 」を実施しました。 実施期間 8/28(水)〜9/10(火), 9/12(木)〜9/27(金) 募集職種 サーバーサイドエンジニア/フロントエンドエンジニア/インフラエンジニア/iOSエンジニア/Androidエンジニア/ビジネスプロデューサー 配属プロダクト ZOZOTOWN/WEAR/新規事業 奨励金 390,000円 今年のテーマは「Thank you!」です。期待とエールをこめて奨励金も39万円となっています。私が所属するZOZOTOWN iOSチームにも9/12〜9/27の期間 あおい さんが来てくれました。ZOZOTOWN iOSチームでは初のインターン生となります。 INTERNSHIP 2019 SUMMERの特徴 今回のインターンシップの特徴は「就業型インターンシップ」であることです。メンターと毎日隣り合ってZOZOが運営するサービスに携わります。エンジニアは本番にデプロイすることがひとつの目標です。実際にプロダクトの開発を行うことで、業務イメージが湧きやすく、サービスの理解も深まります。期間中はミーティングなども社員と同様のスケジュールで行ってもらいました。 選考フロー 選考は以下の流れで実施しました。 応募 応募条件は2020年4月から2021年3月の間で、高校/専門/高専/短大/大学/大学院を卒業見込みもしくは修了見込みの学生です。日程すべてに参加できる方を対象としています。6月19日(水)から7月5日(金)の期間募集をし、ありがたいことに多くの学生から応募がありました。ZOZOテクノロジーズ初のサマーインターンシップということもあり、学生の期待も大きかったようです。また、39万円という奨励金も注目を集めました。 書類選考 履歴書の内容から選考します。履歴書の形式は特になく自由です。執筆論文、研究内容やインターンシップ経験などを書く学生が多いです。エンジニアですとGitHubや開発したサービスの紹介などが伝わりやすく、選考する側も判断しやすいと思います。 課題 課題の内容 書類選考を通過した方には課題選考に進んでもらいました。課題は希望職種によって異なります。具体的な内容は伏せますが、今年のiOSの課題は以下のようなものでした。 Web APIを使用したiOSアプリケーションの作成 一覧画面、詳細画面の実装および画面遷移を実装する 詳細ページには指定した機能を実装する WebViewを使用しないといった、いくつかの課題条件を満たす UI、アニメーション、アーキテクチャ等、実装面でのこだわりポイントについて解説する 就業型インターンシップという性質上、アプリケーション開発経験がある学生を対象としています。課題はモバイルアプリエンジニアが作成しており、ZOZOTOWNやWEARの開発で使用するであろう技術が身についているかを判断する課題となっています。課題の取り組み期間は10日間に設定しており、学生の都合も考慮し、ある程度余裕のある期間を設けています。 フィードバック 課題は提出の一発勝負ではありません。弊社のiOSエンジニアが課題をチェックし、要件を満たしていないところ、コード上で気になるところなどをフィードバックします。フィードバックの修正を踏まえた上で合否を決定します。修正内容は後日行われる面談にて触れていきます。 面談 面談はオンラインで実施しました。提出した課題を元に技術的な話が中心となります。あおいさんの面談でポイントとなった点は以下の内容でした。 1年ほどのSwift歴でClean Architectureの習得に至った経緯 課題もClean Architectureを採用していたが、課題規模に対してのアーキテクチャ選定理由 採用ライブラリの選定理由 弊社エンジニアがコード上で気になった点 こちらの面談はZOZOTOWN iOS担当の ばんじゅん が担当したのですが、一番意識していたのは「 実装面でのこだわりポイントについて解説する 」という点です。あおいさんは「アーキテクチャの選定にはこだわるようにしている」とのことだったのでアーキテクチャの話題が中心になりました。後日あおいさんに尋ねたところ、インターンシップを通してこの面談が一番緊張したようです。 面接 課題合格した方は最後に面接があります。面接は配属先予定のチームリーダーが実施します。 今回の面接での確認ポイントは以下の3つです。 一緒に働きたいと思えるか、会社の経営理念と照らし合わせる インターンシップをやりきる力がありそうか 技術的な面の再確認 技術的な面は課題で確認しているため、1と2を重点的に確認します。特に理念との照らし合わせは採用時にも重要視している項目のひとつです。 インターンシップの目標設定 今回のインターンシップで珍しいところは「キックオフ面談」があるところです。 多くのインターンシップでは会社がインターンシップ期間中の課題を決めると思いますが、今回はインターン生の希望を聞いた上で目標を決め、インターンシップ期間中の課題を決定します。 今回あおいさんからは以下の希望がありました。 サービスのレガシーな部分をモダンにする方法を見てみたい 大きなサービスの基盤を触りたい チーム間でどのようなコミュニケーションをとっているか見てみたい 厳しいフィードバックが欲しい この中で特に驚いたのが「レガシーな部分をモダンにする」という内容です。学生から出てくるのは珍しいと思います。インターン生の課題設定は難しく、今回私たちも学生が楽しめるような新機能の追加やUI/UXの変更など、見た目上でも達成感がある課題を用意しようとしていました。そんな中、あおいさんのインターンシップに求める内容の説明は感心させられるものでした。要約すると以下になります。 「エンジニアをやっていく中で、レガシーな部分と向き合っていく必要性を感じている。モダンなUI実装などは個人開発でも試せるが、レガシーな部分をモダンにするという経験はレガシーコードが無いとできない。ZOZOTOWNは長く運営されているサービスなのできっとまだあるはず」 確かにZOZOTOWNは運営期間も長いため、レガシーなコードはまだまだあります。まさに私たちが業務で向き合っている課題であり、学生からこの目標を聞いた時は非常に嬉しく思いました。このようなやり取りを「キックオフ面談」で行い、インターンシップでの目標は「 レガシーなコードをモダンにする技術を学び、実践する 」に決定しました。 こちらで一方的に用意するのではなく、事前に擦り合わせを行うことでインターンシップでの有益性も変わってくると思います。 実施内容 スケジュール 10日間のインターンシップで目標が達成できるようにスケジュールを作成します。初日のセットアップやイベントなどはメンターが事前に決めておきますが、それ以外はインターン生、メンターで随時相談していきます。 9/12 ・人事による説明 ・開発環境のセットアップ ・メンバー紹介 ・インターンシップ目標の詳細を設定 ・インターンシップ課題の説明 ・ZOZOTOWN iOSの説明 ・初日の目標設定 ・ZOZOTOWNのビルドや実行確認 ・練習課題の開発 9/13 ・開発 ・動作確認などのウォークスルー ・状況にあわせてタスクを切り出す(都度メンターがフォロー) 9/17 ・開発 ・ZOZOTOWN iOSのレガシーな構造についてオーバービュー ・iOS社内勉強会への参加 9/18 ・本課題の開発 9/19 ・開発 ・ZOZOテクノロジーズscrumへ参加 9/20 ・開発 9/24 ・開発 ・After iOSDC Japan 2019へ参加 9/25 ・開発 9/26 ・開発 ・成果発表の資料作成 ・iOS技術共有会へ参加 9/27 ・資料の改善 ・成果発表 ・懇親会 開発 今回の課題はZOZOTOWN iOS内にあるObjective-CのコードをSwiftに置き換えるリファクタリングです。事前に「大きなサービスの基盤を触りたい」という要望があったため、機能が多く影響範囲も広いファイルを選定しました。最初の数日はZOZOTOWNアプリの開発に慣れてもらうため、1機能で完結する影響範囲が狭い練習課題から着手してもらいました。 インターンシップ中は常にメンターが隣にいるため、気軽に相談できる環境になっています。開発中は行き詰まることも多かったとのことですが、メンターに相談しながら解決していくことで「サービスのレガシーな部分をモダンにする方法を見てみたい」という希望が叶えられたのではないかと思います。 また、「厳しいフィードバックが欲しい」という希望も事前にあったため、コードレビューはメンター以外のエンジニアも参加しています。 コードレビューには 技術顧問の岸川克己 さんにも参加して頂きました。 あおいさんはiOSDC Japan 2019で岸川さんのセッションを最前列で聴いていたらしく、弊社の技術顧問と聞いた時には驚いていました。開発体制やコードレビューの雰囲気なども感じて頂けたかと思います。 成果発表 最終日は10日間の成果発表を行います。この成果発表は代表取締役社長や取締役、VPoEも出席します。社員も自由に参加できるようになっているため、10日間一緒にやっていたチームメンバー以外も多く出席しました。 今回はObjective-CからSwiftへの移行という、アプリケーションの見た目が変わらないテーマでありましたが、iOSエンジニア以外にも内容が伝わる資料にまとめられていました。インターン生は中々このような発表の経験は少ないことも多いので、資料作成の時間や練習の場、フィードバックを受ける環境などをしっかり整えることが重要だと思います。 発表会の終わりには「Thank you!」にちなんでインターンシップ期間中に関わった39人からのメッセージとメンターから修了証書の授与がありました。 社内イベント 10日間のインターンシップでは、開発以外にも様々な社内イベントに参加してもらいました。社員と同様の働き方をすることで、ZOZOテクノロジーズのメンバーが普段どのように業務にあたっているかがより伝わったかと思います。インターンシップ期間中にあったイベントを紹介します。 iOS社内勉強会 弊社のiOSチームでは定期的に社内勉強会を行っています。今回は「Human Interface Guidelines」の「System Capabilities」がテーマでした。資料は有志のiOSエンジニアが持ち回りで作成していますが、参加者はiOSエンジニアだけでなく、デザイナーや品質管理チームからも希望者が参加しています。社内勉強会の雰囲気を感じて頂ける良い機会だったと思います。 iOS技術共有会 iOSチームは「ZOZOTOWN」「WEAR」「PB」の3チームあります。それぞれ要素技術は違いますし、勤務地も離れているため、「iOS技術共有会」という形で定期的に情報共有しています。この共有会には技術顧問の岸川さんにも参加していただき、ざっくばらんに意見交換を行っています。あおいさんにはiOSエンジニアの前で成果発表の予行練習をして頂きました。iOSエンジニアとしてのチェックや、資料の構成など、詳細を知っているZOZOTOWN担当以外からもアドバイスを受けていました。 ZOZOテクノロジーズscrum 社員が起案し開催するZOZOテクノロジーズの全社員イベント「ZOZOテクノロジーズscrum」に参加してもらいました。今回は「Data for the people」をテーマに弊社 データサイエンス・アドバイザーのアンドレアス ワイガンド氏 によるセミナーが行われました。ZOZOテクノロジーズscrumについては弊社 COMPANY BLOG をご覧ください。 当日は懇親会もあり、普段業務で接点があるエンジニア以外とも交流する良い場になったと思います。 After iOSDC Japan 2019 9月24日、弊社で After iOSDC Japan 2019 を開催しました。Sansan株式会社さん、JapanTaxi株式会社さんとの合同イベントで、本人が参加希望していたイベントということもあり、ZOZOテクノロジーズ関係者として参加してもらいました。当日イベントに参加するだけでなく、弊社登壇者の発表資料読み合わせなど、準備段階から参加してもらいました。 まとめ 今回はINTERNSHIP 2019 SUMMERについて紹介しました。インターンシップは学生が学ぶ場というイメージがありますが、あおいさんのエンジニアとしての学習意欲、問題に対して向き合う姿勢などは良い刺激になり、我々も学びが多い10日間でした。あおいさん、本当にありがとうございました! インターン生あおいさん、メンターばんじゅんからコメント頂いたので記載します。 あおいさんからのコメント 今回インターンシップで取り組んだのは、ZOZOTOWNのiOSアプリのリファクタリングです。メンターのばんじゅんさんの的確すぎるご指導のもと、毎日楽しく学びながら仕事ができました。また、ランチや日々の業務を通じて気づいたことは、ZOZOテクノロジーズのメンバーは本当に優しく温かい人達ばかりだということです。こんな居心地の良い環境の中で自分の大好きなサービスの改善に携わらせていただけたことは自分にとって大きな財産となりました。総括するとこのインターンシップに参加でき本当に良かったです!お世話になった皆さんありがとうございました。 ばんじゅんからのコメント レガシーと戦う技術を持つエンジニアが欲しいといつも思っているなかで、インターン生として、そのモチベーションを高く持っているあおいさんが来てくれたことは、そのレア度もあり驚きました。業務の中ではObjective-Cを攻略する戦術を学んでいただいたのですが、それ自体は広くどこでも役立つ知識とも言えないなかで、あくまでレガシーにフォーカスして楽しんで取り組んでいただけたようです。わたしたちのリファクタリングに対する取り組みにもポジティブな影響が出ていたと思います。 モダン化にあたっては実際の移行作業だけではなく現状の調査や分析が大きなタスクになりますが、今回のインターンシップではプロダクトの実際のコードや業務に貢献する目的があったため、具体的な分析がまだ行なわれていない部分を題材に調査から実施していただきました。Objective-CとSwiftの相互運用の特性を踏まえて分析しながら現実的な規模へのタスクの分解を考えていただき、限られた期間のなかで、プロダクトへ反映できるコードの作成を完了していただきました。これは単純な開発体験ではなく、わたしたちが普段実施する業務そのものです。今回その課題を提供できたことと、実業務への真の貢献を得られたことは、本当に良い結果であったと考えています。 さいごに ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは! 開発部の手塚( @tzone99 )です。普段は社内ERPシステムの開発をしながらその周辺の業務ツールの制作を担当しています。こちらの記事ではGoogle Apps Script(GAS)を使ってアパレル商品の検品結果を登録するツールを作る中でポイントとなった部分を共有します。 使い慣れたGoogleのサービスをGASで連携させてお手軽にサーバーレスなアプリケーションを作りたい、という方の参考になればと思います。 背景 一般的な製造/物流業と同様、ZOZOでも倉庫で保管するアパレル商品にサイズや外観上の不備(ほつれ、汚れ、付属品の不足など)がないかをチェックする検品という作業があります。今回は「通常と異なる手順で検品し、その結果を集計/分析し仕分けに使いたい」という特定の商品群があったため、入力した結果を保存する専用ツールを作成することになりました。 何を作ったか 商品のQRコードを読み取ると検品項目が表示され、検品結果を入力して送信するとSpreadSheetに結果が保存されるツールを試験的に作成しました。 環境構成は以下の通りです。 単純にGitHubのコードをGAS側にデプロイしたい場合は Chrome 拡張 を使う方法もあります。今回はローカルにインストールしたエディタで開発したかったので clasp を使ってローカルのコードをGAS側にデプロイする方式を選択しました。 なぜGASを選んだか 今回のツールは期間限定で特定の商品群のみに使う想定で、また他のシステムとも疎結合で独立して動かせるものだったため、GASの持つ以下のようなメリットを見込んで採用しました。 社員は全員Googleのサービスが使える(アカウントを持っている)のでアクセス権限の管理や仕様の説明が簡単。 サーバー管理を考えずに運用できる。 SpreadSheetにデータを保管していればエンジニアでなくても(SQLがわからない人でも)欲しい形式でデータを取得したり統計処理しやすい。 GASをWebアプリケーションとして使う際のポイント スクリプト実行時、ブラウザにWebページを表示する 以下の記述により、アプリケーションを実行すると src/gas/checking.html がブラウザに表示できます。 //main.gs function doGet() { return HtmlService.createTemplateFromFile('src/gas/checking').evaluate(); } 任意のjsファイル、cssファイルをインポートしたい場合、GASではそれらのファイルをそのまま扱えないためhtmlファイルとして記述する必要があります。例えば今回導入したBootstrap(Bootstrap v4.3.1)をインポートするケースを紹介します。 Bootstrapのcssテンプレート src/gas/css/bootstrap.min.css をインポートする場合はファイルの拡張子をhtml( src/gas/css/bootstrap.min.html )に変更し、ファイルの中身を全て <style> タグで囲います。 < style > /*! * Bootstrap v4.3.1 (https://getbootstrap.com/) * Copyright 2011-2019 The Bootstrap Authors * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ ...中略 </ style > Bootstrapのjsファイル( src/gas/js/lib/bootstrap.min.js )をインポートする場合も同様に拡張子を変更し( src/gas/js/lib/bootstrap.min.html )ファイルの中身を全て <script> タグで囲います。 < script > /*! * Bootstrap v4.3.1 (https://getbootstrap.com/) * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ ...中略 </ script > トップページの checking.html に以下のように追記し、各ファイルをインポートします。 <?!= HtmlService.createHtmlOutputFromFile('src/gas/css/bootstrap.min').getContent(); ?> <?!= HtmlService.createHtmlOutputFromFile('src/gas/js/lib/bootstrap.min').getContent(); ?> このような方法で任意のjs/cssファイルをGASのアプリケーションへ流用できます。 google.script.runでGASのメソッドを非同期に実行する htmlからGASのメソッドを実行する方法は主に以下の2通りです。 任意のメソッドを実行するための doGet() , doPost() を定義し、そこに向かって httpReqest を投げる google.script.run からメソッド指定で呼び出す GASはお手軽に実行できる一方で、一般的なWebアプリケーションよりパフォーマンスに優れているとは言えません。可能な限りAPIは非同期に実行して体感の待機時間を少なくしたいと考えました。 doGet() 、 doPost() をajax通信で非同期実行しようとすると、同一生成元ポリシーの制約により実行が制限されるため素直に google.script.run を使うのが良いと思います。 以下はGASファイル(拡張子がgsのファイル)に定義した insert() をフロント側(html)から google.script.run で実行する場合の例です。 // 検査結果(inspectionData)をSpreadSheetに挿入する(insert)処理の例 google.script.run.withFailureHandler(onInsertFailure).withSuccessHandler(onInsertSuccess).insert(inspectionData); withFailureHandler 、 withSuccessHandler を別途以下のように定義することで実行後の処理やエラーハンドリングの制御が可能です。 function onInsertSuccess(result) { // insertメソッドが成功した場合の処理 } function onInsertFailure(e) { // insertメソッドが失敗した時の処理 } SpreadSheetへの読み書き SpreadSheetに定義したサイズデータから必要な行を抽出する際の実装例を紹介します。以下のように、製品を特定する情報(A列)と検品時の計測部位、想定されるサイズ(製品寸法)が紐づいたサイズデータを予め用意しておきます。 製品のQRコードを読み取った後、QRコードの情報に合致するサイズのみを画面に出力します。エラーハンドリング等、一部省略しています。 function checkSize(qrCode) { var result = {}; const FOLDER_ID = 'xxx'; // サイズデータを格納したGoogle DriveのフォルダID const DEFAULT_SHEET_NAME = 'シート1'; // QRコードに含まれる製品コードを取得 var productCode = qrCode.substr(0, 9); // 製品コードに対応したSpreadSheetをサイズデータフォルダから取得 var ss = getTargetSpreadSheet(FOLDER_ID, productCode); var sheet = ss.getSheetByName(DEFAULT_SHEET_NAME); // サイズデータを含んだシートを取得 var lastRow = sheet.getLastRow(); // 最終行を取得 var allSizeDataRaw = sheet.getRange(1, 1, lastRow, 4).getValues(); // シート内の全てのサイズデータを取得 // 該当製品のサイズデータからQRコードの情報に完全一致する行のみを抽出しreturn var sizeMap = allSizeDataRaw.filter(function (row) { if (row[0] === qrCode) { return row.shift(); } }); result = { status: 200, message: 'OK', content: sizeMap }; return result; } サイズデータはパフォーマンス改善の都合で製品コード単位に分割しました。というのも検品対象となった製品群のサイズデータは約30万行×4列あり、そこからQRコードで読み取った製品のデータのみを抜き出そうとすると約12秒かかっていました。製品コード単位(約300ファイル)に分割してからサイズデータを取得する上記の方法に変えることで約5秒程度までレスポンスが改善できました。APIの実行回数を減らすのがGASのパフォーマンス改善の基本ですが、それだけだとパフォーマンスが不十分なケースもあります。今回のように大量のデータから必要な行だけを読み取るケースでは適切な単位にファイルを分割することも改善策として有効でした。 SpreadSheetへの書き込みは以下のようなコードで実現しています(こちらもエラーハンドリング等一部省略)。 // 検査データをSpreadSheetに保存する function insert(inspectionData) { const FOLDER_ID = 'xxx'; // SpreadSheetが格納されたフォルダID const DEFAULT_SHEET_NAME = 'シート1'; // 記録するシート名 var fileName = 'returns_inspection'; // 保存するSpreadSheetのファイル名 var ss = getTargetSpreadSheet(FOLDER_ID, fileName); var sheet = ss.getSheetByName(DEFAULT_SHEET_NAME); var rowData = [inspectionData.attrA, inspectionData.attrB]; // 検査結果を配列rowDataに格納 sheet.appendRow(rowData); // SpreadSheetに一行追加 } // フォルダから特定の名前のファイルを取得する function getTargetSpreadSheet(folderId, fileName) { var folder = DriveApp.getFolderById(folderId); var files = folder.getFiles(); while (files.hasNext()) { var file = files.next(); if (file.getName() == fileName) { var existingSS = SpreadsheetApp.openById(file.getId()); return existingSS; } } return ''; } GASの使い所はどこか 実際にGASを業務用Webアプリケーション構築で使ってみて、Googleのサービスと簡単に連携できる強みを実感できました。とはいえパフォーマンス面やGAS特有の仕様など、Webアプリケーションの単純な置き換えにはならない面もあり使い所が重要です。今回の検品ツール作成中に検討した範囲で言えば、今後も以下のような条件を満たしていれば積極的にGASを使っていこうと思います。 Googleのサービスを普段使っていて業務データがGoogle Driveの中にある。 例えば専用のDBサーバーを立ててデータを移管するのが仰々しいようなケースで有効です。 アクセス権を事細かに設定する必要がない、利用者が限定的な場合。 例えば現在はスクリプトの実行権限を このように 指定できますが、これ以上の制御が必要であれば別途実装が必要です。 扱うデータ量が少ない、もしくはパフォーマンスを気にしない場合。 例えば利用範囲が限定的、利用頻度が少ない、バッチ処理等で有効です。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、 こちら からぜひご応募ください。
こんにちは。ZOZO研究所Zの藤嶋( @fjkdiet )と中丸( @ixd_circle )です。 本記事では、 新たなテキスタイル材料やその応用可能性について議論するために開催された国際会議、 Comfort and Smart Textile International Symposium 2019 の様子を報告します。 ZOZO研究所Zとは はじめに本ブログに初登場のZOZO研究所Zについて簡単に紹介します。 ZOZO研究所 のなかで、中長期の研究開発を行う部署として新設されました。ファッションに纏わる様々な最新技術、研究潮流について調査し、そこからファッションの未来をつくる研究に取り組んでいます。 今回は、スマートテキスタイルと快適性を主題にした国際会議であるComfort and Smart Textile International Symposium 2019に参加し、国内外の様々な企業や大学といった研究機関の取り組み、動向について調査してきました。それらの内容の一端をご紹介していきます。 開催概要 本国際会議は日本繊維製品消費科学会の60周年を記念し、2019年9月6日〜7日に奈良で開催されました。 近年、第四次産業革命と呼ばれるAI、ロボティクス、IoT(Internet of Things)といったテクノロジーの急速な発展に伴い、あらゆるモノのデジタル化が進んだ結果、繊維産業も急速に変化を遂げています。 特にスマートテキスタイルと呼ばれるセンシングや温度変化といった様々なトランスデュース機能を備えたテキスタイルは、ヘルスケア、スポーツ、医療といった多様な領域のICTソリューションとして重要性を高めると考えられています。 このような潮流を受け、この国際会議ではスマートテキスタイルに関する技術や活用事例を企業や研究者の垣根を越えて共有し、研究領域のさらなる発展を促すことを目的としています。 基調講演は、ヒューマノイドロボット研究者の石黒先生、 Project Jacquard を発表しスマートテキスタイル商品化の先行事例となったGoogle社、スマートテキスタイルの生産効率化への活用を試みているBMWといった企業が行い、最新の開発事例や将来的なビジョンが共有されました。 またポスターセッションでは大学だけではなく、多くの企業も参加し様々な議論が交わされていました。 ここからは本会議に参加して感じた、スマートテキスタイル業界の近年の傾向や紹介された内容の一部を紹介していきます。  本会議での報告内容について 要素技術の研究 各発表では、材料や素材そのものの研究もあれば、心拍をはじめとする身体情報を計測するデバイスについても多くの発表がありました。 例えば、エレクトロスピニングという手法でAtacticのポリスチレンを紡糸した素材の圧電特性について発表していました *1 。素材、特に繊維の研究では、これまで主に通気性や強度、質感といった要素に焦点が当てられてきました。しかし、素材に加わった歪みに対して電気を発生させる圧電性といった特性が繊維と掛け合わせられると、衣服の着用者の動きや、衣服に加わった負荷といった項目を測定することが可能になり、布地の機能が拡張します。こういった動作や変形に対して電気を生み出す技術は、Motion Energy Harvestと呼ばれ、衣服だけではなく、様々な分野に応用が期待されるものです。例えば、このような素材を大きくすることができれば、将来的には発電といった用途にも応用できるでしょう。太陽電池などと異なり、天候に影響を受けない発電装置となる可能性を秘めています。その他にも、導電性のテキスタイルを用いた脈拍モニタリング、伸縮状態を抵抗値の変化から推定する技術なども発表されていました。 こういった素材そのものの技術的な研究に加えて、スマートテキスタイルの活用方法、またスマートテキスタイルを搭載したデバイスやその効能を検証するための研究もなされていました。 これには、デバイスが人が身に着ける衣服という形態をとることで、ユーザーの認知や反応にどのような影響を与えるかといった生理学的な検証といったものがあります。また、衣服型デバイスの展開時に指標となるものとして、視線や心拍といった生体情報を分析に活用する試み *3 。これはペットに厚さ300μm程の柔らかい電極を備えた衣服型デバイスを着用させることでAAI(Animal Assisted Intervation:動物介在介入)中の犬の心拍データをモニターし、その健康状態を把握するといった試みです。 このようにスマートテキスタイルの研究は非常に学際的で、産学が連携して様々なアプローチがなされています。今回この会議に参加したことで、スマートテキスタイルが普及する未来をより身近に感じることができました。 まとめ ZOZOテクノロジーズ・ZOZO研究所Zではファッションの新たな可能性を切り拓くべく、今回紹介したスマートテキスタイルをはじめ、多様な技術やその応用可能性について日々、研究開発を行っています。 それぞれの専門的な視点、技術から新たなファッションを描いていきたい人、ぜひ一緒に挑戦してみませんか。 www.wantedly.com おまけ 会場の周りにもたくさんの鹿が。休憩時間に鹿せんべいを買ったところ、集団に囲まれてびっくりしました。 参考文献リスト *1 : [1]Chonthicha Imusrivun 1 , Shintaro Kurihara 1 , Ryusei Kitayama 1 , Atsushi Yokoyama 1 and Yuya Ishii 1 , "Piezoelectric response of amorphous electro spun fibers off atactic polystyrene", 1. Faculty of Fiber Science and Engineering, Kyoto Institute of Technology *2 : [2]Akie Naito 1 and Reiko Hashimoto 2 , "Impressions Formed from Women's Clothing and Eye Movements of the Evaluators:Differences by Evaluator of Gender", 1.Ochanomizu University, 2.Sugiyama Jogakuen University *3 : [3]Yoko Komatsu 1 , Kazunori Hirata 2 , Yasushi Atsuta 3 , Hiroko Shibanai 4 and Mitsuaki Ohta 5 , "Measurement of Dog's Heart Rate using Dog Wear with Electrodes", 1. TOYOBO CO., LTD., 2. TOYOBO. STC CO., LTD., 3.Unicharm Corporation, 4. Japan Animal Hospital Association, 5. Tokyo University of Agriculture
こんにちは! ZOZOTOWNやWEARのiOSアプリ開発をしている元と小野寺です。 先日、9/5から9/7まで3日間iOSDC Japan 2019が開催されました。今回ZOZOテクノロジーズでは12名のメンバーで参加し、弊社はスポンサーとして協賛しました。 この記事ではiOSDC Japan 2019にて発表されたセッションの一部を紹介すると共に、現場の盛り上がりの様子もお伝えします。 セッション ライブラリのインポートとリンクの仕組み完全解説 最初に紹介するセッションは弊社の技術顧問をされている岸川さんのセッションです。 iOSDC Japan 内容はライブラリを使うときに遭遇するimportエラーやlinkエラーに対してシステマチックに解決するために必要な知識についてです。 ライブラリやフレームワークのimportエラーやlinkエラーを解決するとき、このセッションの内容は非常に役に立ちます。 応用編ではこのセッションで紹介された知識を活用できる方法がいくつか紹介されました。 その中でアプリの起動時間短縮に有効な方法があり、効果があるならZOZOTOWNアプリにも導入しようと思ったので実際検証してみました。着目したのはスライドの以下の部分です。 ダイナミックフレームワークが増えてくると起動時間に影響を与える(dyld2の場合) そのため各フレームワークをスタティックリンクにして1つの大きなダイナミックフレームワークにする この方法はWWDC 2016のセッション「Optimizing App Startup Time」にてアップルが紹介した方法でもあります。 具体的にはダイナミックフレームワークはアプリ起動時にコストの高いリンク処理が発生するので各フレームワークをスタティックフレームワークとしてビルドし、その全てを1つのダイナミックフレームワークにまとめることでリンク処理のコストを抑える方法です。 dyld2に関しては現状サードパーティのフレームワークで使われてるので効果ありだと思いました。 実際に1つのダイナミックフレームワークにまとめてから今回のセッションで学んだ具体的な設定方法を使い、サンプルアプリで検証した結果を共有します。 ■ 検証内容 「複数のダイナミックフレームワークをそれぞれスタティックリンクにしてから1つのダイナミックフレームワークにまとめる」ことで起動時間が短くなるかを確認 ■ 検証対象アプリ テンプレのプロジェクトを生成してからフレームワークを11個追加したアプリ(フレームワークの選定基準はありません) ■ importしたフレームワーク AFNetworking, Alamofire, CocoaLumberjack, CocoaLumberjackSwift, RxBlocking, RxCocoa, RxRelay, RxSwift, SDWebImage, SDWebImageMapKit, SnapKit ■ 端末 iPhone 6s、iOS 12.3.1 ■ 結果 赤い枠の中にある「Total pre-main time」が総計時間、「dylib loading time」がダイナミックフレームワークのロード時間です。両方とも短縮されているのでちゃんと起動時間が短縮されたのを確認できました。もし運用しているアプリの起動時間が長いのであれば試してみるといいかもしれません。 個人開発のアプリが輝くために - アプリのDL数をあげる必殺技 - 弊社ZOZOテクノロジーズからLightning Talkで名取が登壇しました。 iOSDC Japan 発表内容は、自身が作成した個人開発のアプリ「大人のなぞなぞ〜脳トレIQ謎解きアプリ〜」を約100万ダウンロードまで成長させた戦略についての解説です。 これからアプリを実際に公開してみようと考えている方、既に公開している方どちら側であっても参考になる発表内容でした。 個人で開発したアプリでは、プロモーションにお金をかけることは、難しいですよね。 そんな個人開発のアプリを約100万ダウンロードされるまでに成長させた際に行った戦略の紹介です。発表の中で、ASO対策としてキーワード検索で上位に表示させる為の工夫が紹介されました。 ASO対策としては、次のようなポイントに気をつけると良いようです。 タイトルには、何のアプリなのかを表す為のキーワードを取り入れること サブタイトルには、どんなアプリなのかを伝えるワードを取り入れること キーワードには、ひらがなやカタカナ、漢字などを含める セッション内では、カメラアプリを例にしてわかり易く説明されていました。 他にもアプリの評価を上げるために、高評価をしてくれそうなユーザーにレビューを依頼する取り組みについて紹介がありました。 高評価が期待できるユーザーの例として、次のような具体例が紹介されました。 アプリを10回以上訪問してくれたユーザー あるコンバージョンを達成したユーザー 一定期間、毎日アプリを起動したユーザー 実際に100万近くダウンロードされたアプリでの取り組みということもあり、とても説得力がありました。 今後アプリを公開する際により多くのユーザーに使ってもらう為には、どんな事に気をつけたら良いかの指標となる内容でした。 タイトルの記載内容やレビューへ促す条件を、紹介された内容や他のアプリとの比較をしてみることで、自身のアプリでの対策を見返す良い機会にもなりそうです。 FatViewControllerを安全に書き換える方法が見つからなかったので、どういう痛みを許容するか考えた テスト方針を決めるときは状況や環境を考慮する必要があるため、決まった対応パターンが存在しません。だからこそ他社の対応事例は「過去の対応方針を振り返るとき」や「これから決めるとき」参考になります。 このセッションではまさにそのテスト方針を決めるとき、何を考慮し、どう対応したかが事例として紹介されました。 「普段、テスト方針を決めるために自分がやっていることに問題はないか」、「他の人はどう決めてるか」はとても気になってたので自分の過去を振り返りながら、そして同じ状況なら自分はどうやるかを考えながら興味津々に発表内容を聞きました。 セッションで紹介された「再現可能なテストはないが安全に修正したい。だけどミスは許されない」状況で自分なら何を考慮するかを考えたときに思い浮かんだのは以下です。 自動テストがあるか サービス視点でテスト対象の重要度 開発スケジュール的に使えるリソースはどれほどあるか(工数) テストコードを書きやすい設計になっているか テストしたいのは何か(ロジックか、表示か、挙動か) 実装コスト 実行コスト テストの生存期間やメンテナンスコスト セッションでも上記は考慮対象だったので大きくはズレてないと思い、少し安心しましたがUIテストを積極的に検討してる部分は印象的でした。今までUIテストは「手間がかかる」、「メンテコストがかかる」と単純に思い込んでいたのでもう一度UIテストに関して振り返ってみようと思います。他にはサービスの中の重要な価値について「認識を共有する」という項目も印象に残りました。確かにここをはっきりしておかないと暗黙的な基準で判断してしまうこともあるかもしれません。 このセッションはテストに関する自分のやり方を振り返るきっかけになったと思います。他社の事例を知ることは滅多にないのでテストに興味がある方は是非このセッションの資料を見ることをオススメします。 多言語対応と戦う 2019年版 こちらのセッションでは、多言語化が未対応のアプリでの対応の始め方から紹介されていました。 現在私が対応しているWEARアプリでは日本語や英語、繁体字、簡体字といった多言語の対応を行っていることもあり、このセッションを聞いてみることにしました。 WEARアプリでは文言もユーザーに伝わりやすいように、見やすいようにとデザイナーが試行錯誤して決めています。 実装の節目で私たちはデザインチェックを行うのですが、4つの言語の確認となりますとシステムの言語を都度切り替えて確認しなければならないという手間がありました。 そういった背景から私は、このセッションの中で紹介されたうちの1つ 「アプリで言語を切り替えられるようにしよう」 に興味を持ちました。 アプリの言語切り替えは、iOS 13以降からiOSの標準機能として盛り込まれる機能のようです。 Language selection per app Use third‑party apps in a different language from your system language. www.apple.com こちらのセッションでは、これをiOS 12以前のバージョンで実現する方法が紹介されていました。 アプリ内で使用されている言語は、どこから取得されるのか? UserDefault.standard.array(forKey: "AppleLanguages") にて[String]で定義されています。 ここで定義されている言語は、設定>一般>言語と地域の中で追加されてる言語です。 アプリ内では、 UserDefault.standard.array(forKey: "AppleLanguages") から取得された配列の先頭の言語が使用されます。 どうやってアプリ内で言語を切り替えるのか? 例えば現在表示されている言語が、日本語の状態から英語に変える場合を考えます。 UserDefault.standard.array(forKey: "AppleLanguages") で取得される配列の順序を変更することで切り替えが可能なようです。 今回の場合、["ja-JP", "en"]を["en", "ja-JP"]と変更することで切り替えられます。 この他にも、設定アプリで追加されていない言語を追加する方法や、リセット方法なども含めてデモンストレーションを交えながら詳細に説明していただきました。 次に興味を持ったのは、 「翻訳SaaSの導入で翻訳者にプルリクを出してもらおう」 です。 この項目では、エンジニアでない方も翻訳変更に対するプルリクが発行できるような仕組みについて紹介されました。 ここではCrowdinというSaaSを例に説明されました。CrowdinとGitHubの連携時に、管理したいLocalizable.stringファイルを連携すると翻訳リストを管理するUI上で翻訳の変更が可能になります。 変更することで、プルリクが発行されるという仕組みになっているようです。 WEARアプリでは、英語と中国語の翻訳をそれぞれ別の方が担当して対応しています。私たちは、翻訳が必要な文言のやり取りをConfluenceやSlackを使ってやり取りして対応してきました。 現状の運用方法ですと翻訳依頼の取りこぼしや反映漏れなどが稀に発生していたこともあり、改善策を考えていました。そんな中で、今回のような具体的な運用例をあげての紹介はとても参考になるお話でした。 その他にも、翻訳ネームスペースを使った翻訳文言の管理方法や文字装飾のローカライズの方法などが紹介されていて、今後迷った時に参考になる発表内容でした。 会場の様子 思いやりが詰まったセッション会場 選出された登壇者によるセッションは、いくつかのテーマを同時進行でそれぞれの講義室で行われます。 セッション会場は、写真のように席がぎゅうぎゅうになることもあり、セッションに対する参加者の注目度の高さが伺えます。 iOSDC Japan 弊社の名取や技術顧問の岸川さんのセッションの際も、席は参加者で埋め尽くされてました。 運営の方々の1人でも多く参加者がセッションを聞けるようにという想いから「機械的席詰め」というシステムが度々施行されました。 これは、三人掛けの席で右側が空いていたら機械的に右に詰めるシステムという仕組みのものです。 会場の思いやりによって生まれた、この理想的なゾーンディフェンスのように美しいシステムに、私は感動を覚えました。 セッションは今後プロダクトに関わっていく上でどれも興味深く、勉強になりました。 各登壇者が発表の中にデモンストレーションを組み込んでいたり、笑いどころを入れるなどの工夫も見られ楽しく発表を聞くことができました。 活気にあふれたブース会場 iOSDC Japan 2019ではトークセッションの他にも、様々な企業が軒を連ねるブース会場があります。 セッションの合間や、お昼休憩などセッションのない時間帯は特に参加者の方々で会場はとても活気付いていました。 iOSDC Japan この会場ではイベントのスポンサーとして協賛している企業がブースを出展して、参加者とのコミュニケーションを図っていました。 私たちZOZOテクノロジーズのブースを少し覗いてみましょう。 iOSDC Japan 今回ZOZOテクノロジーズでは、以下のテーマでアンケートを用意しました。 アプリのサポートiOS アプリのSwift率 アプリの対応デバイス アプリのレイアウト実装の割合 iOSDC Japan 参加者の方々のアンケートへの協力もあって、たくさんの方々と交流することができました。 実際にアプリを開発しているエンジニアや広報担当が参加者の皆さんの質問に答えるといった、お話もさせていただきました。参加者の皆さんと接する機会をいただけたことで、私たちもとても楽しい時間が過ごせました。 アンケート結果は、Twitterの公式アカウント上でハッシュタグ#iosdc #zozotechにて載せております。是非ご覧になってください。 1日目 iOSDC Japan 2019、本日ZOZOテクノロジーズブースで行ったアンケート結果はこちらです!投票ありがとうございました。明日は別のアンケートを行う予定ですので、お楽しみに! #iosdc #zozotech pic.twitter.com/9pcUa5Wg4s — 株式会社ZOZOテクノロジーズ (@zozotech) September 6, 2019 2日目 iOSDC Japan 2019 本日のアンケート結果はこちらです。皆様投票ありがとうございました! #iosdc #zozotech pic.twitter.com/QE5247rgIk — 株式会社ZOZOテクノロジーズ (@zozotech) September 7, 2019 食事は行列の先に iOSDC Japan 午前中のセッションが終わると参加者の行列が。その先には、参加者分のお弁当とお茶の用意がありました。 iOSDC Japan 私が食べたのは、野菜を豚肉でぐるりと巻いた豚肉巻きとシャケの切り身に明太子のソースが絡み合ったおかずのお弁当。 副菜には、ゴーヤやゆで卵といった色とりどりの食材を使ったサラダが添えられており、セッションの合間の休憩時間を彩ってくれました。 After iOSDC Japan 2019について 9/24(火)弊社オフィスにてSansan様、JapanTaxi様、弊社ZOZOテクノロジーズの3社による合同イベント「After iOSDC Japan 2019」が行われます。 iOSDC Japan 2019を振り返りたい方や感じたことを共有したい方は是非お越しください! zozotech-inc.connpass.com 最後に ZOZOテクノロジーズは、ファッションを技術の力で変えていくというミッション達成のため一緒に働く仲間を募集しています。気になる方は是非応募してください! https://tech.zozo.com/recruit/mid-career/detail50/ tech.zozo.com
こんにちは。カート・決済チームの濱砂です。 今回はZOZOTOWNのサーバーサイドの監視方法や取り組みについて紹介します。 はじめに 監視の課題 1. 可視化 2. アラートの検知 3. エラーの管理 改善後 1. Datadogで可視化 Sample Script DSL DatadogのDashboard 2. DatadogとPagerDutyでエラー検知 DatadogのSlack通知 DatadogのAlertの設定 PagerDutyのスケジュール設定 3. Sentryでエラーの管理 Sample Script Sentryに送られたエラー一覧 まとめ 最後に はじめに 現在、ZOZOTOWNでは現行のシステム基盤や開発プロセスをリプレイスするプロジェクトが進んでいます。 リプレイスは順調に進んでいますが、未だ多くは現行のまま動いており、在庫管理を行う基幹システムやバッチ処理なども同様です。 ⽇々多くの機能や運⽤が増え続けているため、現行のシステムもモダンなツールや開発スタイルを取り入れて、日々の業務の効率化やシステムの安定性を向上させる必要があると考えていました。 また、現行のシステムを担当しているエンジニアも今後は新しいリプレイス後の開発に移行する必要があります。 そのため、モダンな開発スタイルや思想を普段の業務の中に取り入れて、忙しい開発の現場でも少しずつキャッチアップする仕組みが必要だと考えていました。 このような背景があり、先ずは監視に焦点を当てて現行の課題を改善しつつ、モダンな開発スタイルを取り入れる取り組みを進めています。 監視の課題 現行のシステムのアプリケーションの監視には独自ツールを使用しています。メトリクス収集 、ロギング、可視化、アラート通知まで基本的にはこのツールを使用して運用しています。 ZOZOTOWN開設当初からシステムの特性を考慮して作り込まれているため、使いやすい点も多く、個人的には何度も障害を乗り越えるために助けてもらったので愛着もあります。 しかし、サービスの規模が大きくなり開発者も増えていく中でいくつか課題が出てきていたため、その課題を改善するための取り組みを行いました。今回はその中からいくつかピックアップして紹介します。 1. 可視化 アプリケーションのエラーログは独自ツールで収集してSQL ServerのDBに保存しています。 ログの内容を見るためのモニター機能はありますが、エラーの件数を集計してグラフ化したり、キャンペーンの登録数などのエラー以外のメトリクスを見たい場合は別の方法で見る必要がありました。 方法としては開発者がDBから直接SQLクエリを発行して抽出を行い、表やグラフを手動で作成していました。それほど時間はかかりませんが、あまり効率的な作業ではありませんでした。 2. アラートの検知 アラートの検知方法は独自ツールのモニター機能を使うかメール通知のため、深夜帯は気付きにくく対応が遅れることもありました。 また、監視業務をSREチームや協力会社の方々に頼ることも多く、自分たちで気付けたとしても監視意識の高い開発者だけに業務が集中してしまう課題もありました。 3. エラーの管理 直近のエラーの内容や件数を見るためのモニター機能はありますが、集計やイシュー管理は手動で行なっています。 また、業務時間外にアラートの通知が来た場合、独自ツールを見るためには社内ネットワークに入る必要があります。 そのため、接続に時間がかかって対応が遅れることや、必要な環境を持ち合わせていない場合は確認が出来ないこともありました。 改善後 1. Datadogで可視化 可視化にはDatadogを導入しています。Datadogは監視に必要な一通りの機能を備えたMonitoring SaaSです。グラフの作りやすさ、見栄えの良さに定評がある、グローバルスタンダードなツールの1つです。 先ず、エラーやメトリクスの情報をDatadogに定期的に送る必要がありました。 そこで、Datadogが提供しているAPIを使い、DBから取得した情報をカスタムメトリクスとしてDatadogに送信するPython Scriptを作成しました。これをDocker Containerの中で1分に1回、バッチ処理としてcronで実行しています。 Sample Script from datadog import initialize from datadog import api # 初期化 options = { 'api_key' : '<DATADOG_API_KEY>' , 'app_key' : '<DATADOG_APP_KEY>' } initialize(**options) # パラメータのセット # 実際はDBから取得した値をセットしています metric_name = "zozo.custom.error" value = 1 tags = [ 'env:prd' , 'project:zozo' ] # Datadogにメトリクス送信 api.Metric.send(metric=metric_name, points=value, tags=tags) 当初は独自ツールのモニター画面をスクレイピングする仕組みでやっていましたが、メトリクスの追加、変更をする場合はスクリプトの修正が必要という課題がありました。 そのため、各メトリクスの取得条件はDSLにSQLクエリとして記述するようにして、追加や変更が容易に出来るようにしています。 DSL metrics_name : sql_server.custom.zozo.error monitor_sql : | SELECT COUNT(*) AS total_count FROM ErrorTable WITH(NOLOCK) WHERE errorType = 1 monitor_value_positions : - total_count:0 次に収集したメトリクスを使い、DatadogでDashboardを作成して、それを各メンバーが閲覧出来るようにしました。 定期的に見たい情報のDashboardを作成したことで、毎回手動で可視化する必要が無くなりました。 DatadogのDashboard 2. DatadogとPagerDutyでエラー検知 1で収集したメトリクスを使い、DatadogのAlert機能を利用してSlack通知するようにしました。Slackは社内でコミュニケーションハブとして使われていることもあり、エラーの通知に気付きやすくなり、チーム間の情報共有やコラボレーションも円滑に行われるようになりました。 DatadogのSlack通知 また、緊急度の高いインシデントが発生した場合でもすぐ気付けるように、PagerDutyというオンコールシステムを実現するためのSaaSを導入しました。 PagerDutyはオンコールに関する機能が豊富なツールで、インシデント発生時に電話やSMSで通知出来ます。 先ずDatadogとPagerDutyを連携します。その後、オンコールを設定したいDatadogのAlertにPagerDuty用のメンションを設定します。 ここまで設定するとインシデントが発生した場合、PagerDutyの設定の内容に応じてオンコール通知されるようになります。これで深夜帯のアラートも自分たちで気付けるようになりました。 DatadogのAlertの設定 PagerDutyには担当者をカレンダーで設定する機能もあります。また、一次受けの担当者が応答できなかった場合に自動的に二次受け以降の担当者にエスカレーションする、といったようにな機能も備わっています。 これを機に複数人担当の週替わりの運用にしました。スケジュールの管理はPagerDutyで自動行うようになったため、属人的に行ってきた開発者の負担も下げることが出来ました。 初めは慣れない開発者から不安の声もありましたが、フォローしながら少しずつ慣れてもらうことで、監視に対する意識や姿勢も底上げされました。 PagerDutyのスケジュール設定 3. Sentryでエラーの管理 エラーのログの管理にはSentryを導入しています。Sentryはエラーの詳細を収集して可視化出来るサービスです。 対応言語も多く本来であれば導入し易いツールですが、現行ZOZOTOWNのサーバーサイドの使用言語はサポートされておらず、そのまま導入することは出来ませんでした。 そのため、SentryのAPIを使って1と同様に独自ツールでロギングしているエラーの情報をSentryに送信するPython Scriptを作成して定期実行しています。 Sample Script from raven import Client #初期化 client = Client( 'SENTRY_DSN' ) # Sentryにエラー情報送信 # 実際はDBから取得した値をセットしています client.capture( 'raven.events.Message' , message= 'description' , date= 'errorDt' ) ,data={ 'tags' : { 'id' : 'id' , 'serverId' : 'serverId' , 'userAgent' : 'userAgent' , 'requestMethod' : 'requestMethod' , 'scriptName' : 'scriptName' , 'mediatedPath' : 'mediatedPath' , 'referer' : 'referer' , 'serverName' : 'serverName' , 'service' : 'service' , 'line' : 'line' , 'column' : 'column' } }) エラーの情報をSentryに集約したことで、社内ネットワークに入らなくてもエラーの内容が確認出来るようになりました。 Sentryに送られたエラー一覧 今のことろエラーが精査されておらず、件数が膨大なこともあり、全件は送らず閾値を超えた場合のみSentryに送信しています。 全件送るようになれば、Sentryで集計やイシュー管理も出来るので引き続きブラッシュアップしていく予定です。 まとめ 現行のアプリケーションの開発現場にモダンなツールや仕組みを導入したことによって、どのような変化や恩恵があったかについて紹介しました。 ツールは導入して終わりということは無く、日々ブラッシュアップしたり、新しい機能に対応していく必要があります。 また、現行のシステムを改善するには作りや課題を深く理解して、そのシステムに合うやり方で導入する必要があるため、時間も掛かり何かと苦労することも多いです。 しかし、試行錯誤しながら地道に改善に取り組むことはサービスを良くしていくためには必要なことで、リプレイス後もそれは変わらないと考えています。 現行のシステムを深く理解して本質的な改善を行っていくためにも、現行のシステムやそれを支えてきた方々へのリスペクトの気持ちを忘れず、引き続きこのような取り組みを続けていきます。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集しています。 ご興味のある方は、 こちら からぜひご応募ください。
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。今回は7月に実施した 開発合宿 に関して、計画から実施まで運営側がどのようなことを行ったのかをお伝えします。これから開発合宿を計画している方や今ある開発合宿をより良いものにしたい方々に一つでも有益な情報を提供できたら幸いです。 合宿実施概要 日程 7/11(木)-12(金) 場所 おんやど恵(湯河原) 参加者 34名(エンジニア32名、広報1名、人事1名) 実際に開発合宿の計画から7月の実施までどのようなことを決めていったのか、ここから時系列に沿って説明していきます。 前期 予算確保 4月上旬 実行委員メンバーの選出 4月中旬 テーマと実施概要の検討 宿の仮予約 5月上旬 経営会議での承認 5月中旬 人数調査(第1回 アンケート) 声がけ 5月下旬 本応募(第2回 アンケート) 6月上旬 開発内容の事前共有 合宿のしおり作成 6月下旬 懇親会・遅刻・早退調査(第3回 アンケート) 7月上旬 予約の確定 機材の用意 賞品の用意 特急踊り子のきっぷ購入 座席・部屋の割り振り スライドの雛形用意 タクシー乗車準備 各部署との連携 直前のメンバー変更 写真への写り込みの確認 7月中旬 最終的な動きとアサインの決定 当日 東京駅に全員定刻集合 昼食の弁当争奪戦 2日間のスケジュール 合宿後 事務処理 事後アンケート(第4回 アンケート) Q. 開発合宿全体の満足度を教えてください Q. 次回があれば参加したいですか? 成果物 報告書の作成 リスト まとめ 最後に 前期 あらかじめ確保しておくと実施がスムーズになるのが予算です。 予算確保 開発合宿を行う場合、予算が当然必要になります。そのため、まずは予算の確保を行っておきましょう。前期のうちから、来期予定している開発合宿の予算を確保します。これがないと、なかなか「今期開発合宿やりたい」となった時に予算外で取得は難しいことが多いでしょう。必要な場合は経営会議などで予算と実施の承認も事前に得ておきます。 4月上旬 新年度になり、「今期は開発合宿やりたいね」と実施を計画し始めた時期です。 実行委員メンバーの選出 実際に開発合宿をやろうとなったら、推進役となる実行委員を選出しましょう。今回のZOZOテクノロジーズの場合は、CTO室が主軸となり計画をしました。しかし、CTO室は今年4月に新設されたばかりの部署でありCTO室だけではリソースが足りなかったため、開発部より有志で1名実行委員として動いてもらいました。CTO室1名、有志1名の2名で計画から実施まで行いました。 4月中旬 ここから本格的に実施に向けて内容を検討していきます。 テーマと実施概要の検討 今回はZOZOテクノロジーズとして初の開発合宿であり、業務として1泊2日の時間を利用しました。そのため、合宿の成果が業務に貢献できるもの、業務の効率をあげるようなものを作るというテーマを設定しました。 このテーマにした背景には、合宿参加者だけでなく、参加しないエンジニアや他職種の人も開発合宿の恩恵を受けられるようにしたかった点があります。「エンジニアが開発合宿で2日間オフィス不在になるけど、帰ってきたらすごい役に立つものを作ってきてくれるから是非行ってきてくれ」となるような全社で開発合宿に対するポジティブな空気づくりを目指しました。その結果、次回以降の開催時に周囲の理解も得やすくなり、開催がスムーズになることを期待しています。 そして、テーマとは別に開発合宿の意義と目標を明文化しました。合宿後にこれらの項目に関しての達成度を確認をします。 【1】成果物で業務の効率アップにつなげる ・普段の業務の隙間時間ではなく、集中してまとまった時間を確保することで短時間で効率的に開発できる ・成果物が合宿後の業務に役立つ 【2】技術に集中する時間を設けスキルアップ ・普段いじらない・軽くしか触れていない技術を深く利用し、技術スキルUP ・ものづくりをする思考トレーニング ・周りの人がすごいことをやるのを見て、自分も頑張らないと、と思う機会にし、今後の成長曲線を上振れさせ、今後のスキルアップへつなげる 【3】一つの場所で、「開発」をすることによりお互いを深く知る ・新旧・別拠点メンバーの交流機会を設け、今後の業務をさらに円滑にしやすくする 【4】対外的PR ・テックブログで #ZOZOは楽しく働く な発信、及び技術的に面白い成果があれば、それ自体もネタになる 宿の仮予約 次に、宿の候補を絞り込み、宿が空いている日程を確認します。これによって日程の候補は自然と絞られていくでしょう。 同時に、必要な費用も見えてきます。 宿の候補は他社のテックブログなどを参考にしつつ検討しました。 合併前のVASILYの時に一度開発合宿を実施した ことがあり、その際に利用した おんやど恵 も候補でした。前回の知見を活かせることや、前回の課題としてあげられていた「気軽に行ける距離にコンビニがない」という点が今では宿のすぐ隣にコンビニができて解消したこともあり、「おんやど恵」に決めました。 足湯に浸かりながら開発できるのもおんやど恵のポイントです。 この段階では参加人数は見えていないので、最大人数を想定して予約しておきました。その際には人数変動があることを伝え、いつまでに最終的な人数を連絡すればよいか確認しておきます。 5月上旬 宿も仮押さえができて開発合宿の開催も具体的にイメージができるようになってきました。そこで、経営会議にて開催の承認を得ます。 経営会議での承認 予算は確保していましたが、宿や場所も見えてきたのでそこから算出した費用と、エンジニアが1泊2日でオフィス不在になることを経営会議で伝え、今期実施の承認を得ました。 その際の資料には、開発合宿の目的、日程、場所、参加想定人数(今回は40人想定とした)、概算予算と内訳、を記載しました。 5月中旬 より具体的に準備を進めていくため、参加者の規模を把握します。 人数調査(第1回 アンケート) これから準備を進めていく上で、実際にどれくらいの参加者がいるのか規模感を把握しておく必要があります。これによって成果発表の時間などタイムスケジュールに影響したり、費用が変動したり、はたまた参加者がいなければ開催ができなくなることも考えられます。なお、今回の開発合宿は強制参加ではなく、参加の意思があって業務の都合が付く開発部のエンジニアの任意参加としました。 エンジニアに開発合宿を実施することを展開して、現時点の参加意思を確認します。まだこの段階では業務の都合がどうなるのかも不明瞭で参加者の確定はできませんが、ざっくりとした規模感を確認するために簡単なアンケートをSlack上で実施しました。合宿の概要は経営会議向けに用意した資料をベースに、費用などは除いた簡易版にして展開しました。 特に宿の予約人数が変更になる場合、人数を減らす対応はできますが、急に人数が増えてしまうと空き部屋の数など物理的に対応しきれなくなる可能性もあります。そのため、このタイミングの調査では少しでも参加する可能性がある人は全員カウントしておきたいという気持ちがありました。そのような数字を取得できるよう、この回答がそのまま参加表明ではないということを伝えた上で、合宿に興味があるエンジニアの人数を調査しました。また、この段階では参加者不足で開催不可の可能性も無いとは言い切れないので「開発合宿開催します」とは言い切らず「開発合宿を開催した場合」という表現をするように気をつけました。 なお、開催日は全社的な予定やリリースが無さそうな日程を運営側で定め、さらに宿に40名規模でも空きがある日程を確認し、実施予定の日付を絞り込んで決めました。 声がけ 前述のアンケートを開始し、翌日には30人の回答がありました。しかし、回答者の内訳を確認すると、拠点の偏りがありました。開発部は大きく青山オフィスと幕張オフィスの2拠点に分散しています。そのため、今回の合宿では普段接しない別拠点エンジニアとの交流も目的の一つです。ところが、この時点では青山オフィスと幕張オフィスの参加希望者の比率が 2:1 でした。そのため、幕張オフィスのリーダーなどに声がけをし、幕張オフィスで参加したい人が他にいないのか確認をしてもらいました。 その際のやり取りの中から得られた知見としては、特に開発合宿にこれまで一度も参加したことがないメンバーだと「何かすごいものをつくらないとけないのでは」というようにハードルを高くしてしまっている可能性が高いということです。そのため、何を作るのか・どんな難易度のものを作るのかは気にせず、それぞれの状況・レベルに合ったものを作れたらそれで良いということを説明し、心理的ハードルを一つ取り除きました。それにより、幕張オフィスの参加者も増加しました。 5月下旬 参加者の本応募を実施し、実施に向けて準備を進めていきます。 本応募(第2回 アンケート) 事前アンケートにより最少催行人数以上の参加希望者がいることが確認でき、拠点間の偏りも解消できたので予定通り開発合宿の実施が確定しました。 業務上の都合が今後どうなるか分かりませんが、現時点での参加希望者の本応募を受け付けました。やはり、事前調査の回答と差は出てくるので、このタイミングでも再度声がけをします。応募を開始し、翌日には14名、1週間後には31名、6月上旬には最大で38名の状態にりました。38名の内訳は青山オフィスのエンジニア19名・幕張オフィスのエンジニア17名・広報1名・人事1名です。 このタイミングで、広報と人事が1名ずつ参加することも決まりました。広報と人事は開発はしないので、リモートワークをしつつ開発合宿というものを肌で感じてもらいました。広報は カンパニーブログの執筆 や写真撮影、Twitterの公式アカウントでの発信を行いました。人事は今後社内イベントで合宿をする際の勉強のために参加しました。そのため、人事へは準備段階から経費精算の出し方まで情報を逐次共有しました。 その後、業務上の都合などで参加できなくなる人も発生し、最終的には34名(青山オフィス16名・幕張オフィス16名・広報1名・人事1名)となりました。 6月上旬 合宿まで残り約1ヶ月。各自が合宿で何を作るのか事前に考えておきます。 開発内容の事前共有 合宿に行ってから何を作るのか悩んでいては時間がもったいないです。合宿の2日間は開発時間に使えるよう、事前に作るものとそれが今回のテーマである業務効率化にどうつながるかを考え、書き出して事前共有してもらいました。「当日までに考えておいてね」とリマインドするだけでなく事前に全員の決まったものを共有することにより、なかなか作るもののイメージがわかない人にとっては他の人のテーマがヒントになったり、他の人たちが決まっていく様子を見て「自分も早く考えなきゃ」と考えるきっかけになったりを狙いとしていました。 合宿のしおり作成 いわゆる合宿のしおりのような、情報を集約したページを用意します。タイムスケジュールや目的など、確定しいる情報の共有が目的です。決まっていない部分は随時アップデートしていくページとなるので、「合宿のことを知りたければここを見ればOK」という場にしました。また、参加者への連絡用にSlackに専用チャンネルも用意しました。 6月下旬 交通手段を確定するために、追加の調査を行います。 懇親会・遅刻・早退調査(第3回 アンケート) 合宿2日目には成果発表会を実施し、その後懇親会を開催します。懇親会は強制ではなく、任意参加としました。その懇親会の参加有無や遅刻・早退の予定があるのかを調査します。この人数により、電車の指定席予約人数を決めます。そのため、このアンケートの目的も伝えた上で、何か変更の可能性がある場合は事前に相談してもらうようにしました。今回は業務上、2名が遅刻しての参加になりました。 なお、懇親会はこの調査段階では数名参加せずに先に帰る予定でしたが、合宿当日は発表もオンスケで進み、終了時間も遅くならなかったので全員参加して帰りました。 7月上旬 合宿まで残り1週間程度。準備も最終段階です。順次予約も確定していくので、実際に掛かる費用の明細も固まってきます。 予約の確定 人数は確定したので、宿や昼食の予約を確定させます。宿は合宿プランの場合、宿泊人数を伝えたらそれに応じた部屋数が指定されます。昼食は、宿に依頼するのではなく近隣の弁当を配達してくれる業者に注文し、駅弁を16種類用意しました。同じ弁当を配布するだけでなく、弁当争奪じゃんけん大会で楽しめる要素を用意しました。 機材の用意 2日目の成果発表会では、プロジェクターを2台レンタルしています。1台は運営側でスライドを表示、もう1台のプロジェクターでデモを行うように計画しました。デモを行うプロジェクターは発表者の入れ替えをスムーズにするため、HDMI切替器とHDMIケーブルを用意して持参しました。しかし、当日はHDMI切替器とプロジェクターの相性が良くなかったためか、HDMI切替器経由でのプロジェクター出力はできず、1本のHDMIケーブルで実施しました。 賞品の用意 成果発表会では、賞品付きの賞を6個用意しました。具体的にどんな賞を設けるのかは、当日の発表の内容を見ないとなかなか適切なものを定められません。そのため、賞の数だけは6個と事前に定めておき、賞品を用意しておきました。賞品はArduino・Raspberry Pi・IoTデバイス・スクリーン付きスマートスピーカーなどを用意しました。 特急踊り子のきっぷ購入 往路は最初から参加できる32名で移動です。そのため、確実に座れるよう、特急踊り子で指定席を確保しておくことにしました。 計画時、特急踊り子の情報をオンラインで調べるのには苦労しました。曜日だけでなく時期により列車の有無が変化したり、そして今回の目的地である湯河原に止まらない踊り子もあるため、そもそも選択肢としてどの列車があるのかも把握が難しかったです。結論として、みどりの窓口に行き、そこに置いてある紙の時刻表を見るのが一番分かりやすいという結論に至りました。何でもオンラインが最適なのではなく、アナログな表現手法や仕組みが適している場合もまだまだあるなと感じる瞬間でした。 無事に同じ車両にまとまって座席を用意することができました。復路は懇親会終了後のちょうど良い時間帯に踊り子が無いことと、発表会や懇親会で時間が変動する可能性もあるのできっぷは事前に用意しませんでした。 座席・部屋の割り振り 往路の踊り子の座席、開発時の席、昼ごはんの席、宿泊する部屋の割り振りも事前に決めておきます。今回の合宿の目的に別拠点のエンジニアとの交流も含めているため、まず座席を拠点ごとにバランス良く混ざるように2種類に分割しました。左右前後が別拠点の人になるよう、交互に配置しました。その後、各拠点のメンバーをランダムにシャッフルして割り振っていきます。発表の順番も同様にシャッフルをして決めました。 開発部屋は2部屋用意しました。 スライドの雛形用意 合宿で使うスライドの雛形を用意します。今回は、発表時のスライドと表彰時のスライドの2種類を用意しました。 1つ目は発表用のスライドです。発表用のスライドは、参加者がスライド作成に時間をかけ過ぎないように心がけました。そのため、各自発表スライドは1枚までとしました。Googleスライドに発表順に氏名だけ埋めたものを用意しました。発表時間も一人2分と短いので、スライドも発表も要点を簡潔にまとめてもらい、発表はデモをメインで行う形式にしました。32名の発表で、全体で約2時間の発表会です。 2つ目は表彰時に使うスライドです。表彰時のスライドもこのタイミングで仕込みをしておきました。6個分の賞のスライドを用意し、そこには賞品の内容や写真を埋めておきました。当日は賞の名前・受賞者・発表タイトルを入力するだけの状態にしておきます。スライドはGoogleスライドを利用しているので、この段階ではスライド作成者と当日の審査を行うVPoEのみに権限を付けておき、開発合宿後に社員全体へ閲覧権限を付与しました。 タクシー乗車準備 湯河原駅と宿の移動はタクシーです。タクシー約8台に分乗することになるので、誰がどのタクシーに乗ってもスムーズに宿に行けるよう、台数分の封筒を用意しました。その封筒にはお金と宿の住所を印刷した紙を入れておき、タクシーから降りたら領収書とお釣りを入れた状態の封筒を回収しました。回収後は領収書と残金の確認をし、封筒に納入されている現金の金額を記載して、復路の際に再度利用します。 封筒には通し番号を付けてあり、タクシーに乗る際に誰に何番の封筒を渡したのかはチェックしておきました。このような誰に何をしたのかのメモをしやすいよう、参加者の名簿は何枚か印刷して持参していました。 なお、前述の遅れて参加するメンバーには前日までに封筒をオフィスで渡しておきました。 各部署との連携 今回の合宿費用は運営側で一括で立て替えて支払うものが多く、念の為経理に内容と申請フローの確認をしておきました。また、参加者の勤怠登録の確認など、参加者自身が問い合わせをしなくて済むよう、疑問点として上がりそうな部分は事前に関係部署に確認し、前述のしおりのページにまとめておきました。 事前に各部署と連携し、参加者が困るであろう点は情報をまとめてしおりにまとめておきました。 合わせて、宿や弁当の業者は当日の支払いにしてあったので、現金なのかカード払いなのかの確認もしておきます。これにより持参すべき現金の量が大きく変わってきます。 次に、会社からの貸与PCを持ち出すことになるので、社内のPC全台に導入しているデバイス管理ソフトが入っていることの再確認と、紛失には気をつけるよう周知を行いました。 また、開発合宿の様子をTwitterへ投稿する際のハッシュタグ #zozotech開発合宿 の運用も広報と相談しました。ポイントとしては、夜の懇親会時にはアルコールも用意することになると思いますが、日中の業務として開発中の投稿では、良からぬコメントをされないためにも、未開封のアルコールの缶や空き缶などが机の上に放置して写り込まないよう気をつけるのが良いかと思います。 直前のメンバー変更 合宿1週間前、業務都合で行けなくなるメンバーが発生しました。キャンセルで予約を再調整するのも手ですが、せっかくなので代わりに参加できるメンバーを募集し、予定通りの人数での決行になりました。参加者は最後までどうなるか分からないので、予約をする際にはそのことを忘れずにリカバリ策を検討しておくことが必須です。 写真への写り込みの確認 開発合宿の様子はブログやTwitterなどに発信します。その際、写真に写り込みたくないメンバーもいます。写り込みたくない場合は、それを考慮する必要があるため事前に調査し、当日の撮影時には配慮をします。 7月中旬 合宿まであと数日。最後に細かいアサインを決めて事前準備は完了です。 最終的な動きとアサインの決定 細かい運営側のタスク確認とアサインを行います。以下にそれぞれ検討したタスクをまとめています。 前日まで USB Type-C HDMIアダプタとバインダーを用意するタスク。 集合時 出席確認と該当の座席指定のきっぷを渡すタスク。事前に電車の座席番号を記載した出席者リストを印刷しておく。 湯河原到着時 タクシー代と住所が入った封筒を渡し、誰にどの封筒を渡したのか出席者リストに記載するタスク。 宿到着時 宿にチェックインするタスク。 チェックイン後 タクシー代表者から領収書とお釣りを回収し、確認をするタスク。 昼食前 弁当の受け取りと支払いをするタスク。 昼食時 弁当を配布するタスク。 発表時 発表資料のスライドの表示・操作をするタスクとタイマー係。前半と後半でそれぞれアサイン(前半に発表する人は後半にアサイン、後半に発表する人は前半にアサイン) 発表後表彰までの休憩 表彰の内容を考えてスライドを埋めるタスク。 表彰時 表彰をするタスクと、賞品を渡すタスク。 宿出発前 宿をチェックアウトするタスク。 宿出発時 タクシー代と住所が入った封筒を渡し、誰にどの封筒を渡したのか出席者リストに記載するタスク。 駅到着後 タクシー代表者から領収書とお釣りを回収し、確認をするタスクときっぷをまとめて購入するタスク。 当日 いよいよ合宿当日です。 東京駅に全員定刻集合 初日の朝が最大の難関とも言えます。全員指定の電車に乗るという重要なイベントが待っています。予想はしていましたが、運営側としてもやはりこのイベントが一番の難所でした。いくら準備をしてもコントロールできない部分です。 当日は9:00発の特急踊り子に乗車のため、8:45東京駅集合にしました。集合場所はしおりに地図のリンクも添えて共有し、当日も一番乗りで集合場所へ行って写真をSlackで共有しました。準備は整いました。以下、手に汗握る15分間の流れです。 8:45 指定の集合時間。出欠を確認するが3名ほど足りず。 8:48 実行委員の代表者だけ集合場所に残り、他の参加者はホームへ移動。 Slackでやり取りしながら全員集合場所に現れるのを待つ。 8:55 最後の一人が集合場所ではなくホームに出現。Slackのその連絡を見て、実行委員も集合場所からホームへ移動開始。 8:56 電車の扉が開いて順次乗車 8:58 全員乗車 9:00 出発 なんとか全員間に合いました。次回以降はきっぷは事前配布にした方が良さそうです。 昼食の弁当争奪戦 合宿中に2度ある昼食。宿に依頼すれば仕出し弁当を手配してくれます。そうすれば注文や支払いもまとめてもらえるので楽なのですが、今回はあえてそれは利用せず、直接、駅弁を注文しました。特に高いものやおにぎりのような簡易的なものを除いた全種類、16種類を揃えました。この中から好きなものを選んでもらえば昼食も一つの楽しみになります。 じゃんけん大会を実施し、VPoEとじゃんけんで買った人から順に選んでいく形式にしました。 2日間のスケジュール 2日間の開発合宿、全体のスケジュールは以下の通りです。集合が遅かったり、昼食を弁当ではなく外で食べようとすると、初日の開発時間が短くなってしまうため、なるべく初日の午後はすべて開発時間に当てられるようにしました。 1日目 8:45 集合 9:00 東京駅発 10:14 湯河原駅着 10:30 宿到着・チェックイン 11:00 開発開始 12:00 昼食 開発 18:00 夕食 開発 2日目 8:00 朝食 開発 9:30 チェックアウト 開発 12:00 昼食 13:00 発表 15:15 表彰 懇親会 18:00 宿出発 帰路 合宿後 事務処理 合宿終了後、忘れてはいけないのが経費精算。すべての領収書をなくさずに保管しておきましょう。参加者には集合場所までの交通費精算と勤怠登録の案内を出しておきます。 事後アンケート(第4回 アンケート) 次に、記憶が鮮明なうちに参加者にアンケートを回答してもらいます。このアンケートでは、当初目標としていた項目に対してどれくらい達成できているか、次回の合宿に向け、今回の合宿で良かった点・改善点などを回答してもらいました。回答率は100%です。 集計結果の一部を公開します。 Q. 開発合宿全体の満足度を教えてください Q. 次回があれば参加したいですか? どちらの質問も、参加者の6〜7割が最高点を付けています。初回の合宿としては成功と言えるでしょう。2つ目の設問で3と回答している理由としては、「ネタがあれば参加したい」「別の人にも参加して欲しいから」というものがありました。 成果物 合宿で開発したソースコードは、記録として残すためにGitHub上にリポジトリを作り、そこに集約しました。また、この合宿で開発したもので業務で利用しているものや、 Rubyのgemとして公開 されたものもあります。 報告書の作成 開発合宿の締めくくりとして、報告書を作成します。上記のアンケート結果や成果も含め、一つのページにまとめます。このページを参考に次回開催時の説明材料にしたり、次回の開発合宿計画時にPDCAしていくための備忘録にもなります。 リスト 今回紹介したタスクのリストをGoogle スプレッドシートにまとめたので共有します。是非ご活用ください。 docs.google.com まとめ ZOZOテクノロジーズとして初めての開発合宿開催に伴い、その準備工程をお伝えしました。開発合宿は開催頻度も多くはできないのでなかなかPDCAもしにくいイベントです。そのため、各社の開催レポートなどの情報発信が大切かと思います。今回お伝えした内容のうち、一つでもみなさまの開発合宿計画の改善につながれば幸いです。 最後に ZOZOテクノロジーズでは、開発合宿の企画・運営を行った「全社における技術的な戦略策定および、エンジニア組織強化のための施策の推進」をミッションに掲げたCTO室のメンバーも募集中です。また、開発合宿に参加したり、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
ZOZO Technologies dev camp 2019 summer こんにちは。WEARリプレイスチームの id:takanamito です。 先日、開発部のみんなで行った開発合宿でteyuというgemを作ったのでその紹介をしようと思います。 github.com 開発合宿の様子はこちら techcorp.zozo.com teyu このgemは、クラスをnewするときに渡した引数を、そのままインスタンス変数に代入するコードが簡潔に書けるようになるgemです。 日本語で説明するより実際のコードを見たほうがわかりやすいと思うので以下に記載します。 通常の引数の場合 通常のRubyコードでこう書くクラスを class Sample def initialize (foo, bar, baz) @foo = foo @bar = bar @baz = baz end end teyuを使うとこう書けます require ' teyu ' class Sample extend Teyu teyu_init :foo , :bar , :baz end キーワード引数の場合 class Sample def initialize ( foo :, bar :, baz :) @foo = foo @bar = bar @baz = baz end end teyuを使うとこう書けます require ' teyu ' class Sample extend Teyu teyu_init :foo! , :bar! , :baz! end デフォルト値つきキーワード引数の場合 class Sample def initialize ( foo : ' foo ' , bar : ' bar ' , baz : ' baz ' ) @foo = foo @bar = bar @baz = baz end end teyuを使うとこう書けます require ' teyu ' class Sample extend Teyu teyu_init foo : ' foo ' , bar : ' bar ' , baz : ' baz ' end 開発の経緯 以前からZOZOテクノロジーズは技術顧問のMatzさんと月1回のMatz MTGを行っています。 techblog.zozo.com たいていそのMTGの前にRuby trunkのissueを見て疑問に思ったことや、最新のRubyの動向をキャッチアップしているのですが initializeメソッドの中で引数をインスタンス変数へ代入するコードが冗長で煩わしく思っている人が多いのか、似たようなissueがいくつも上がっていることに気が付きました。 Feature #5825: Sweet instance var assignment in the object initializer - Ruby master - Ruby Issue Tracking System Feature #8563: Instance variable arguments - Ruby master - Ruby Issue Tracking System Feature #12820: Shorter syntax for assigning a method argument to an instance variable - Ruby master - Ruby Issue Tracking System このissueについてMatzさんとお話したときに問題を解決するアイデアの1つとして上がったのが、teyuの様なinitializeメソッドを定義する機能を提供することでした。 ちょうど普段書いているRubyコードで同じようにインスタンス変数への代入を煩わしく思ったことがあったので解決策を調べてみるとattr_extrasというgemを見つけました。 github.com このgemでは attr_initialize という名前で同様の機能が提供されており、やりたいことは実現できそうでした。 ただ1つ、キーワード引数を表現するのにArrayとして引数を定義する書き方が自分の中で違和感がありました。 attr_initialize :foo, [:bar, :baz!] もう少し具体的に言語化するのであれば、Arrayからキーワード引数であることが連想しづらいというのが理由として挙げられるかと思います。 そう思っていた時に開発合宿が開催されることとなり、自分が欲しいinitializeメソッドを定義する機能だけをgemとして実装してみることにしました。 次のバージョンでは 初日の朝の時点で設計すらしていない状態で合宿に参加して2日目の昼には成果発表をする必要があったため、クラスや変数の名付けに若干違和感を残したまま駆け足で開発を終えました。 同様にパフォーマンスに関しても考慮するほど余裕がなかったです。 ※合宿先の宿には露天風呂があったのでしょうがないですね! おそらく次のバージョンとしてリリースするのは、リファクタと高速化を含むものになると思います。 まとめ 業務上どうしても普段では丸一日開発に没頭することは難しいのですが、開発合宿では久しぶりにプログラミングへ集中できたのでめちゃくちゃ楽しかったです。 WEARのリプレイスにおいてもteyuを活用できそうなプログラムが既にあるため、開発合宿のアウトプットを業務改善につなげるいいサイクルが作れそうです。 ZOZOテクノロジーズではこういったチャンスを生かして、技術の力で業務を改善することに興味があるエンジニアを募集しています。 ぜひ一緒に次回の開発合宿に参加しましょう。 www.wantedly.com
こんにちは。MLOpsチームリーダーの sonots です。 先日の プレスリリース で発表しました通り、ZOZOTOWNに「類似アイテム検索機能」を追加しました。この機能の技術要素について先日開かれた Google Cloud Next '19 in Tokyo で、本プロジェクトからは2件発表してきました! 技術要素が気になる技術オタクの皆様におかれましては、ぜひ資料と動画をご覧ください! ZOZO画像検索でのMLOps実践とGKEインフラ アーキテクチャ 筆者(そのっつ)の発表になります。発表の概要は以下になります。 ZOZOのAIプロダクトであるZOZO画像検索の概要とアーキテクチャについて紹介します。GKE(Google Kubernetes Engine)とgRPCを使った機械学習APIサーバの構築、Cloud Composerを使った定期的なモデルの更新について話します。特に、Kubernetesを用いてgRPCマイクロサービスアーキテクチャで構築されているHTTP APIサーバを本番レベルに引き上げるために行った工夫について共有します。 筆者は今回はじめてKubernetesを触ったのですが、Kubernetesクラスタを本番レベルに引きあげるために正直な所とても苦労しました。この発表は、まさしく少し前の私が教えて欲しかったものを全て盛りこんだものとなっています。今Kubernetesの検証を進めていて、これから本番導入を考えているタイミングの方に役立つ資料・発表になっていると自負しています。 補足:正式な資料はGoogleから10月に公開されるそうです。ここにはひとまずGoogleに許可をとった資料を載せておきます。正式資料が公開され次第差し替える可能性があります。 Googleが開発したAIプロセッサ『Cloud TPU』とZOZOTOWNでの活用事例 Googleの佐藤氏と福岡研究所の西原、家富の発表です。発表の概要は以下になります。 Cloud TPUは、ディープラーニングの学習と推論に特化した回路設計により従来に比べ大幅に高い価格性能比を実現するグーグルのAIプロセッサです。また、低遅延のインターコネクト技術で2048コアのTPUを結ぶCloud TPU Podは、AIスーパーコンピューティング環境を低価格クラウドサービスとして提供。このセッションでは、ZOZOTOWN画像検索プロジェクトでのCloud TPU導入事例を交え、その実力を解説します。 筆者も聴講しましたが、Googleの佐藤氏からTPUの概要を教えていただいた後に、生々しい現場の話をきけて同じ会社ながら非常に参考になるセッションでした。TPUに関してはまだ学習環境だけの利用となっており、本番での定常的な利用はできていないのですが、これから導入されていく予定です。 おわりに ZOZOTOWNへの画像検索機能の導入は以前から温めていたアイデアで、ついに満を持してリリースできました。 まだまだ改善は進めていく予定ですし、アレやコレやといった新しいAI案件も抱えています。是非一緒に働きましょう。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは。開発部基幹SREチームの廣瀬です。 弊社では、システムの一部にSQL Serverを使用しています。 本記事では、SQL Serverにおけるインデックスのメンテナンス方法である再構成と再構築について、それぞれを実行した場合のクエリ性能の比較結果をご紹介したいと思います。 比較を実施するに至った背景の前に、まずはインデックスの再構成と再構築について説明したいと思います。 インデックスの再構成と再構築 インデックス SQL Serverのインデックスについて簡単にご紹介します。下図は、SQL Serverのデータ構造の概略図です。 テーブルは、1つ以上のインデックスから構成されます。なお、ヒープという別のデータ構造だけからテーブルを構成することもできますが、今回は省略します。 インデックスは複数のページから構成されます。各ページは論理的につながりを持ちB-Treeを形成します。 ページとは、8KBの物理的に連続した領域のことで、各ページに実際のレコードが格納されています。 なお、インデックスの詳細なアーキテクチャについては SQL Server のインデックスのアーキテクチャとデザイン ガイド にまとめられています。 インデックスの断片化 SQL Serverのインデックスは、データの更新パターンによっては断片化が発生します。 断片化が発生すると、本来であれば1000レコードを10ページに格納できるはずが15ページにまばらに格納される、といった状態になります。 断片化により、データ読み取り時に読み取るべきページ数が増えるため、結果的にCPU負荷増やクエリ実行時間の増加などの性能低下を引き起こす傾向にあります。 「どの程度断片化が進んでいるか」を示す値として、「断片化率」という値があります。 こちらはsys.dm_db_index_physical_statsというDMVを使って以下のように取得できます。 declare @DB_ID int ,@OBJECT_ID int set @DB_ID = DB_ID( ' DB名 ' ) set @OBJECT_ID = OBJECT_ID( ' テーブル名 ' ) select * from sys.dm_db_index_physical_stats(@DB_ID, @Object_ID, null , null , ' DETAILED ' ) as A join sys.objects as B on A.object_id = B.object_id 例えば、約200万レコードのテーブルに対して上記クエリを実行すると、以下のような結果が得られました。 index_level この例だと0がリーフレベル、1が中間レベル、2がルートです。 avg_fragmentation_in_percent 各レベルにおける断片化率です。高いほど断片化が激しいことを示します。 page_count 各レベルにおけるページ数です。断片化が激しいと、本来必要なページ数よりも多くなります。 avg_page_space_used_in_percent 各レベルにおいて、各ページにどれだけレコードが詰まっているかを示します。100%に近いほど読み取り性能がよくなります。 断片化を解消する手段として、 インデックスの再構成と再構築 という2つの方法があります。 断片化解消のための再構成と再構築 インデックスの再構成と再構築の違いは、以下の表に分かりやすくまとまっています。 出典: インデックス再構築と再構成の違い プロダクション環境で気にすべきポイントとして、「同時実行性」に特に注意が必要です。再構成は、実行中に取得するロックの範囲、ロックをかけている時間ともに限定的であるためオンライン操作とみなすことができます。 一方、再構築でもEnterprise Editionではオンライン操作が可能ですが、それ以外のエディションの場合オフライン操作となってしまいます。 ここで、オフライン操作とは「その操作を実行中に、他プロセスが同一テーブルに読み書きできなくなる」操作のことを言います。したがってオフライン操作のインデックス再構築に5分間かかる場合、同一テーブルへのSELECTが5分間ブロックされ続けることになります。オンライン操作はその逆で、並行して他プロセスが読み書きできます。 先ほどのテーブルをインデックス再構成してみると、以下のような結果になりました。 リーフレベルの断片化率(avg_fragmentation_in_percent)が約70%から約0%へと変化しています。 それに伴ってリーフレベルを構成するページ数(page_count)が40%ほど少なくなっています。 この結果は、そのまま「テーブルスキャン時のリーフレベル読み取りページ数が約40%少なくなる」と言い換えることができ、読み取り性能の向上に貢献しそうです。 ここまで、SQL Serverにおけるインデックスの再構成と再構築について紹介しました。次に、本記事を書くモチベーションとなった背景についてご説明します。 背景 再構成と再構築のどちらを定期的に実施すべきか、という疑問がありますが、Microsoftのドキュメントでは以下のガイドラインが紹介されています。 出典: インデックスの再構成と再構築 つまり、断片化率がある一定の閾値を超えた場合は再構築(REBUILD)、そうでない場合は再構成(REORGANIZE)を推奨する、というものです。 ただし、再構築をオンラインで実行するためにはSQL ServerのエディションがEnterpriseでなければいけません。 そのため、Standard Editionを使っている場合などは積極的に再構築を実施することはできません。 インデックス再構築と再構成の違い という記事の中で、 「実際の運用を考慮した場合、再構築や再構成前の断片化の状態よりも、再構築や再構成の実行中の状況や実行後のインデックスの状態の方が重要ではないでしょうか?」という問いかけがなされています。 さらに踏み込むと、個人的には「再構築や再構成を実施した結果、性能がどの程度向上するかが重要ではないか」と考えます。 弊社では、リードレプリカDBのインデックスメンテナンスとして、毎日インデックスの「再構成」を実施しています。また、セールなどの高トラフィックなイベント前にはインデックスの「再構築」を実施しています。 リードレプリカDBはStandard Editionであるため、インデックスの再構築はオフライン操作でしか実行できません。 そのため、以下の手順でインデックス再構築を実施していました。 WEBサーバーを数グループに分割 1グループをLoadBalancerから外し、アクセスが無い状況にする アクセスが無い状態でインデックスのオフライン再構築 完了後、グループをLoadBalancerに戻す 2-4を各グループで実施 以下のGIFアニメのイメージです。 この作業は数カ月に一度の頻度で発生する運用で頻度は高くありませんが、作業者の負担が大きい作業でした。 それでも、再構成より再構築のほうが、性能面で優れた結果をもたらすのであれば、継続して実施すべき作業です。 ですが、もし再構成と再構築とでクエリ性能およびサーバー負荷に差が無いことを確認できれば、今後はこの運用を無くすことができます。 以上の背景を踏まえて、インデックスの再構成と再構築の性能比較を実施することになりました。 比較方法 SQL Server 2012 Standard Editionが入った、同一スペックのサーバー2台で比較を実施します。尚、ディスクは「SSD」です。 サーバーAでは全テーブルのインデックス「 再構成 」、サーバーBでは全テーブルのインデックス「 再構築 」を行い、クエリ性能およびサーバー負荷の観点で比較を実施しました。 比較用のメトリクスには パフォーマンスカウンタ の値を使用しました。 留意点1 冒頭でお話したように、インデックスの再構成と再構築には様々な違いがあります。 説明した点以外でも、例えばインデックス再構築の方がテーブル容量を圧縮できるといったメリット等も考えられます。 ただし、今回興味があるのは「どれだけクエリ性能に差が生じるか」という点だけであり、その点に絞って優劣をつけることとします。 留意点2 本当は「インデックスメンテ無し」「インデックス再構成」「インデックス再構築」という3パターンの比較を考えていました。 しかし、プロダクション環境において性能劣化の懸念がある「インデックスメンテ無し」のサーバーを用意することが難しいため、再構成と再構築でのみ比較しました。 比較結果:クエリ実行時間 「 SQLServer:Batch Resp Statistics 」というメトリクスを使うことで、「実行に1ms-2msかかったクエリが〇個あった」という風に、実行時間ごとのクエリ実行回数の分布情報が取得できます。 これはサーバーが起動してからの累積値のため、2点間の差分を取得することで、例えば特定の1時間におけるクエリ性能を確認することができます。 表にまとめると、以下の結果となりました。 (サーバーA:インデックス再構成 / サーバーB:インデックス再構築) サーバーAとサーバーBでは、クエリ実行時間の比率の増減は1ポイント未満であり、クエリ実行時間の傾向に大きな変化は発生していませんでした。 そのため、インデックスの再構築によって、今までは10msだったクエリが1msになり大幅に処理時間が変化したというような傾向は発生していないと考えられます。 比較結果:サーバー負荷 インデックスの再構成および再構築によって、テーブルを構成するページ数に差がでてきます。 そのためデータ読み取り時のディスク負荷や、読み取りページ数増加に伴うCPU負荷等に変化がみられる可能性を考慮し、以下のメトリクスを比較しました。 オレンジ色がサーバーA(インデックス再構成)のデータを示し、青色がサーバーB(インデックス再構築)のデータを示します。 LogicalDisk\Avg. Disk Queue Length ディスク負荷が高い場合に高い値を示すメトリクスです。横軸は時間、縦軸はディスクキューの数を示します。サーバーAとBで顕著な変化はみられませんでした。 LogicalDisk\Disk Read Bytes/sec データの読み取り量を示すメトリクスです。横軸は時間、縦軸は秒間のディスク読み取り数(単位:Byte)を示します。 サーバーB(青色/インデックス再構築)の方が各テーブルを構成するページ数は少ないはずですが、それに伴って読み取りデータ量も減少している、というような傾向はみられませんでした。 Process(sqlserver)\% Processor Time CPU負荷を示すメトリクスです。横軸は時間、縦軸はサーバーのCPU使用率(%)を示します。 データの読み取り量が増えるとCPU負荷も増加する傾向にありますが、読み取りデータ量が変わらなかったことから、CPU負荷にも差はみられませんでした。 考察 今回検証した環境については、インデックス再構成と再構築で、性能観点/サーバー負荷の観点での差はほぼ無いという結果になりました。 これは以下の点を踏まえると納得できます。 再構成による「リーフページの断片化のみ解消」でも、大きく断片化が解消できているという点 インデックス階層においてページ数が多いのは圧倒的にリーフページが存在する階層であった点 今回の検証では性能面での差が無いという結論となりましたが、ワークロードの性質や、ディスク性能次第では顕著に差がでる可能性もあります。 例えば、ディスク性能が低いHDDですと、インデックス再構築の方が性能面で優れるという結果になったかもしれません。 関連するメトリクスを採取して比較し、効果を検証することが大事だと思いました。 まとめ 本記事では、インデックスの再構成と再構築の性能差を比較するための評価手法と比較結果をご紹介しました。 比較対象のメトリクスは、インデックスの再構成と再構築の挙動の違いを踏まえて、影響を受けそうなメトリクスを選定しました。 本記事で紹介した比較方法を使って、他の環境でも性能の優劣を比較していただけると思います。 今回の検証結果では、インデックスの再構成と再構築とで性能およびサーバー負荷の差異はみられませんでした。 そのため、今後は該当サーバーにおけるインデックスのメンテナンスは再構成のみとしました。Enterprise Editionの場合は迷わずオンライン操作の再構築でいいかもしれません。 しかし該当サーバーはStandard Editionのため、再構築が不要と判断できたことで運用作業を1つ削減できました。 今回は性能面を重視して確認を行いましたが、冒頭で紹介したインデックスの再構築と再構成の処理の違いの表にも記載があるように、性能面以外の違いを気にする場面もあると思います。 インデックスのメンテナンス方法(再構成/再構築/オンライン再構築)によって、インデックスメンテナンスの処理時間や使用するトランザクションログのサイズが変わってきます。 短時間でインデックスのメンテナンスを完了させるという観点では、メンテナンス方法を使い分ける必要もでてくると思います。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/
こんにちは。ZOZO Researchの小倉です。2019年7月29日(月)から8月1日(木)にかけてグランキューブ大阪(大阪府立国際会議場)で開催されたMIRU2019に参加しました。今回はその様子をレポートします。 MIRU2019 MIRUとは、今回で第22回目の開催となる画像の認識・理解シンポジウム(Meeting on Image Recognition and Understanding)です。今回は事前登録者数900人強、当日参加者も含めると1,000名を超す方が参加されたそうです。このMIRU2019において、ZOZOテクノロジーズはプラチナスポンサーとして協賛させていただきました。 cvim.ipsj.or.jp 企業展示 企業展示ブースでは、ポスター形式でZOZO Researchにおける取り組みを紹介しました。1枚目ではZOZOグループが提供するサービスとそこから得られる豊富な情報資産について、2枚目では実際の研究事例とその成果について解説しました。大変うれしいことに、多くの方々が興味を持ち話を聞いてくださいました。ポスターまで足を運んでくださった皆さま、ありがとうございました。 展示していたポスターはこちらです。 ランチオンセミナー sites.google.com 3日目には、MIRU2019プラチナスポンサーとして企業企画イベントを開催しました。「ファッションを数値化する」をミッションに掲げるZOZO Researchの取り組みを、産学連携というキーワードから紐解くランチオンセミナーです。講演者として以下の方々をお呼びしました(敬称略)。 桂井 麻里衣(同志社大学 助教) シモセラ エドガー(早稲田大学 専任講師) 古川 徹生(九州工業大学 教授) 山口 光太(株式会社サイバーエージェント) エドガー先生や山口先生はファッションのwebデータを使った先駆的な研究実績があり、現在はそれぞれ大学と企業でご活躍されています。桂井先生や古川先生はZOZOテクノロジーズと共同研究を行なっています。皆さまにはファッション研究の魅力、ZOZOテクノロジーズを始め企業に期待することなど、率直に話していただきました。 会場からは「ファッションやアートに代表される分野を本当に理解するためには、存在するデータを分析するアプローチだけではなく、人の認知や創造性をモデル化する必要があるだろう」といった意見もいただきました。こちらも多くの方々にご参加いただきました。会場まで足を運んでくださった皆さま、ありがとうございました。 インタラクティブセッション 3日目のインタラクティブセッションでは、ZOZO Researchインターンの長瀬が発表しました。 [PS2-21] 部分コーディネート識別不能問題を解決するためのGraph Neural Networksの検討 長瀬 准平(芝浦工大/ZOZO), 斎藤 侑輝, 中村 拓磨(ZOZO) コーディネート生成モデルを評価する際、慣例的にコーディネートの穴埋め問題(Fill-in-the-blank, FITB)の正解率で評価します。本研究では、Graph Convolutional Network(GCN)ベースのFITBを数学的に定式化しました。さらに、GCNにスキップ接続を導入することで、GCNのグラフ情報とコーディネート情報を保つSet Transformerを提案しました。 発表に使用したポスターはこちらです。 気になった研究発表 私が個人的に興味を持った研究について紹介します。 招待講演 [IT2A-2] On the Structural Sensitivity of Deep Convolutional Networks to the Directions of Fourier Basis Functions Yusuke Tsuzuku, Issei Sato(Univ. of Tokyo/RIKEN) 「パンダの画像にあるノイズを加えると、認識精度が高いはずのCNNモデルが99%テナガザルという誤判定を起こしてしまう」というのは以前からよく知られた現象です。このように画像認識モデルの性能を大きく乱すようなパターンをUniversal Adversarial Perturbation(UAP)と呼びます。 この研究では、UAPをフーリエ解析することで、パターン自体を見るだけでは分からない特徴を周波数領域に見出しました。この特徴はデータセットとモデルアーキテクチャの組み合わせごとに固有であり、特定の周波数に対して敏感に反応することがわかりました。その周波数に対応する2次元フーリエ基底関数から生成されるUAPを加えると、実際に画像認識モデルが誤判定を起こしてしまいます。UAPは人間が直接見ても区別しづらいですが、フーリエ変換して周波数領域に移すことでその違いが明瞭になります。例えばガウシアンノイズは周波数領域では一様となってしまうため、特定の周波数にのみ強く反応するようなモデルを乱すことはできません。 この研究がさらに進み、どのようなノイズにも頑強なアーキテクチャが提案されたらとても面白いと思います。 インタラクティブセッション [PS1-16] Data Shakingを用いたBlack Box Networkの解析手法 菅原 俊, 田口 賢佑, 船津 陽平(京セラ) 近年、Google Cloud AutoMLやAmazon Forecastなど、機械学習のフルマネージドサービスが登場しています。このようなサービスは手持ちのデータを使って簡単に機械学習を行いサービスに組み込むことが可能です。しかし、これらは内部でどのような処理が行われているか分からないブラックボックスモデルとなっており、出力として得られた値の信頼性がどの程度なのか分かりません。 この研究では、画像のセマンティックセグメンテーションを行うブラックボックスモデルを対象とし、入力画像に様々な摂動を加えた際の推論結果の分散を観察することで信頼性解析を行いました。期待されるように物体の境界付近における信頼性が低いという結果が得られたほか、摂動のパターンを様々に変えることで信頼性の分布も変化することを報告しています。 この結果だけでも十分に面白いですが、「入力データに摂動を加えることでブラックボックスモデルの振る舞いを解析できる」ということに理論的な裏付けがあるのかという点にも興味があります。 [PS1-54] 分光感度の異なる複数のカメラで撮影された多視点画像からの色と形状の復元 酒井 修二, 高橋 諄丞, 渡邉 隆史(凸版印刷) 撮影画像の色は「物体に固有の分光反射率」に加えて「光源の分光分布」と「カメラの分光感度」に影響されるため、分光反射率を精度よく測定するためには手間やコストがかかります。この研究では、より簡単に、より安価なカメラを用いて分光反射率を求められるような手法を提案しました。色の情報が分光反射率として求まれば、光源を変えた時にその物体の色がどのように変化するかをシミュレーションできます。 この研究はZOZO Researchにおける取り組みのひとつである「衣服のリアルな質感を伝えるためのCG表現」と親和性が高いと感じました。アパレル商品の撮影時の環境光と、私たちが実際にその商品を着用して過ごす時の環境光は異なります。「どの環境光のもとでどのような色に見えるか」をWeb上で確認できれば、「イメージと違った」という理由での返品を減らしお客様により満足していただくことができると考えられます。 感想 私個人としては初めて参加する情報系の学会でした。 画像処理についての知識が浅く勉強不足を痛感しましたが、初日のチュートリアル講演で真っ先にGANや近傍探索アルゴリズムの概観を知ることができ、ありがたかったです。招待講演やオーラルセッションでは国内外の様々な研究成果を聞くことで現在の研究の最先端やトレンドが分かりました。ディープラーニングを解釈しやすくするAttention Mapや、ラベルデータの不完全性に対応するための半教師あり学習といったトピックが多かったように感じました。インタラクティブセッションでは弊社のサービスや研究に活かせるものという観点で見て回り、著者の方とのディスカッションを通じていくつかのヒントを得ることができました。 最後に ZOZO Researchでは次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できる機械学習エンジニアを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com おまけ バンケットにて、弊社の後藤が代表して挨拶しました。 各テーブル盛り上がっている中だったので聞こえづらかったと思いますが、耳を傾けてくださりありがとうございました。 梅田のポケモンセンターにて。
こんにちは。ZOZOテクノロジーズ開発部SREの田島( @katsuyan121 )です。 弊社では JBoss Data Grid (以下JDG)という分散キャッシュサーバーをマーケティングオートメーションシステムに導入し、キャンペーンのリアルタイム配信を実現しています。 JDGは複数台のサーバーでクラスタを組んでおり、先日そのクラスタがSplitBrainを起こしました。 SplitBrainとは、1つのクラスタが2つ以上のクラスタに分断することをいいます。 例えば、クラスタのノード間で通信が分断された場合などに、ノード同士で通信できなくなりSplitBrainが発生します。 調査の過程でJDGはどのような仕組みで動作しているのか、どのような原因でSplitBrainが発生したのか知見が溜まったのでそれを紹介します。 また、実際にSplitBrainの再現を行い対策を行ったのでそれについても紹介します。 JDGについて インメモリDataGrid インメモリなデータストアである 複数サーバー間でデータを共有し協調動作が可能 分散実行が可能 Server-Client modeとEmbedded mode Server-Client mode Embedded mode JDGとInfinispan JDGクラスタの仕組み ディスカバリープロトコル NATIVE_S3_PING グループメンバーシッププロトコル 障害検知プロトコル FD FD_SOCK VERIFY_SUSPECT SplitBrainの発生 SplitBrain SplitBrain発生までの流れ SplitBrainの再現 FDのタイムアウト時間を短く設定 デバッグモードでアプリケーションを起動 JDGのキャッシュに対して大量のデータをinsert JConsoleを利用してFullGCを発生させる 実際にSplitBrain・FDが発生したか確認 クラスタ1 クラスタ2 AP-2によるFDのログ コーディネータノードによるVERIFY_SUSPECTのログ 対策 本システムの課題 キャッシュとアプリケーションが一緒に載っている JDGの情報があまりない まとめ 参考記事 JDGについて インメモリDataGrid 最初にJDGを分散キャッシュサーバーと紹介しましたが、実際にはその名前の通りDataGridのPlatformになります。また、データはすべてメモリに保持されるためインメモリDataGrid Platformと呼ばれています。インメモリDataGridとは参考記事によると以下のことが実現できるものを指すらしいです。参考記事は本記事の最後に載せています。 インメモリなデータストアである 複数サーバー間でデータを共有し協調動作が可能 分散実行が可能 それぞれの項目に対してJDGではどのようになっているかを紹介します。 インメモリなデータストアである JDGのデータはすべてオンメモリとなっています。 そのため、swap等が発生しない限りは高速にデータへアクセスできます。 複数サーバー間でデータを共有し協調動作が可能 JDGでは複数サーバーでクラスタを組み分散してデータを保持できます。1つのデータを何台のサーバーで保持するかを設定でき、1台のサーバーで障害が発生したとしてもデータが失われない仕組みとなっています。また、クラスタにノードが追加・削除された場合はそのノードが保持していたデータを各サーバーに自動でリバランスする機能を備えています。 分散実行が可能 JDGではクラスタの中でアプリケーションを動かすことができます。JDGのデータを利用するようなアプリケーションの場合、データを持っている該当のノードでJavaの特定の処理を行うことができます。これによりデータのアクセスが高速になり、高速なスループットを実現することが可能となっています。また、クラスタ内でMapReduceを行うことができ集計のような処理も高速に行うことが可能です。 Server-Client modeとEmbedded mode JDGには以下の2つのモードがあります。 Server-Client mode Embedded mode Server-Client mode Server-Client modeはアプリケーションとJDGを別に動作させて、HTTPなど任意のプロトコルを利用してJDGにアクセスします。 Server-Client modeを利用することでアプリケーションとデータを分離して管理できます。 Embedded mode Embedded modeはJavaアプリケーションとJDGを同一のJVMで動かします。これにより上記で示したような任意のJavaアプリケーションを分散して動作させることが可能となります。これによりデータ処理を高速化しスループットの高速化を実現できます。 私達のサービスでは高速なスループットが求められるためEmbedded modeを利用しています。例えば膨大な商品データの中から商品の値段が下がったことをリアルタイムで検知しユーザーにお知らせをするということをしています。 しかし、Embedded modeはアプリケーションとデータが同一のJVM上に乗っているのでアプリケーション等の障害でJVMが停止するとデータも失われてしまいます。 また、JVMの再起動つまりアプリケーションの再起動をするとデータが失われるためデプロイのたびにデータを各サーバーでリバランスして回る必要があります。 JDGとInfinispan JDGはOSSで開発されている Infinispan というアプリケーションのエンタープライズ版となっています。 そのため基本的にはInfinispanのソースコードを読むことでアプリケーションの中身を直接把握することが可能です。 JDGクラスタの仕組み JDGでは複数のサーバーでクラスタを組み分散してデータを保持していると紹介しました。 サーバー同士のクラスタ管理はJGroupsというOSSライブラリを使用しています。 JGroupsではクラスタ管理に必要な仕組みを、それぞれのプロトコルとして分けて管理をします。 プロトコルは以下のようなXMLで設定を記述します。以下は実際に私達のシステムで使われている設定ファイルを少しいじったものです。 <config xmlns = "urn:org:jgroups" xmlns : xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi : schemaLocation = "urn:org:jgroups http://www.jgroups.org/schema/jgroups-4.0.xsd" > <TCP bind_addr = "${jgroups.bind_addr:SITE_LOCAL}" bind_port = "${jgroups.tcp.port:7800}" port_range = "30" recv_buf_size = "10m" send_buf_size = "640k" enable_diagnostics = "false" sock_conn_timeout = "2000" thread_naming_pattern = "pl" bundler_type = "no-bundler" thread_pool . min_threads = "50" thread_pool . max_threads = "10000" thread_pool . keep_alive_time = "60000" /> <org . jgroups . aws . s3 . NATIVE_S3_PING region_name = "ap-northeast-1" bucket_name = "${jgroups.s3.bucket}" /> <FD_SOCK/> <FD timeout = "60000" max_tries = "10" /> <VERIFY_SUSPECT timeout = "3000" /> <pbcast . NAKACK2 use_mcast_xmit = "false" xmit_interval = "1000" xmit_table_num_rows = "100" xmit_table_msgs_per_row = "1000" xmit_table_max_compaction_time = "10000" /> <UNICAST3 xmit_interval = "500" xmit_table_num_rows = "20" xmit_table_msgs_per_row = "1000" xmit_table_max_compaction_time = "10000" /> <pbcast . STABLE stability_delay = "500" desired_avg_gossip = "5000" max_bytes = "1m" /> <pbcast . GMS print_local_addr = "true" join_timeout = "3000" max_join_attempts = "400" /> <MFC max_credits = "2m" min_threshold = "0.50" /> <FRAG3 frag_size = "8000" /> </config> 各プロトコルの役割についての概要はこちらのブログにすごくわかりやすくまとめられているのでご参照ください。 https://kazuhira-r.hatenablog.com/entry/20130209/1360426003 ここでは、特に重要なディスカバリープロトコル、グループメンバーシッププロトコル、障害検知プロトコルについて紹介します。 ディスカバリープロトコル ディスカバリープロトコルは、JGroupsのクラスタへの参加時に他ノードやコーディネータを探すときに使われるプロトコルです。 私達のシステムでは、各ノードにEC2を利用しているため NATIVE_S3_PING というプロトコルを利用しています。 NATIVE_S3_PING NATIVE_S3_PING はS3上のファイルを利用することでディスカバリープロトコルの実現を行います。 S3を使ったディスカバリープロトコルはもともと公式で開発されている S3_PING というプロトコルがあります。しかし、最新のバージョンでは S3_PING が非推奨となっており NATIVE_S3_PING を使うように勧めています。そのような経緯から私達のプロジェクトではサードパーティ製のNATIVE_S3_PINGを利用しています。 https://github.com/belaban/JGroups/blob/master/src/org/jgroups/protocols/S3_PING.java#L30 NATIVE_S3_PINGを利用しクラスタを作成すると、指定したS3バケットに以下のようなファイルが生成されます。 AP-1-24102 4f8adfd5-4c86-55ce-cfe3-4fb11dfa145a 10.160.4.1:7800 T AP-2-31780 8db77c66-79fd-93f5-d72f-b718438a6061 10.160.4.2:7800 F AP-3-42443 532qfd3d-543d-fda3-l3rf-ljl35dfahlh5 10.160.4.3:7800 F AP-4-53254 0401172a-201e-a875-dac3-04b74a9f8812 10.160.4.4:7800 F クラスタはコーディネータノード(マスタノード)1台とそれ以外のノードで構成されます。クラスタにノードが参加・退出するとコーディネータノードが上記ファイルを編集します。 上記ファイルでTがついているものがコーディネータノードです。参加したいノードは上記のファイルを参照しコーディネータノードに対して「参加させてくれ」といったメッセージを送ります。 実際にコーディネータノードがクラスタにノードを追加するのは以下のグループメンバーシッププロトコルで行います。クラスタがない初期状態の場合、最初のノードはS3にファイルがないと判断し自身がコーディネータノードとなりS3にファイルを作成します。 グループメンバーシッププロトコル グループメンバーシッププロトコルは、クラスタのメンバー管理を行うプロトコルです。各ノードのクラスタへの参加や退出を管理します。グループメンバーシッププロトコルには特に種類がなく GMS として設定されるもののみとなっています。 障害検知プロトコル 障害検知プロトコルはその名前の通り、クラスタ内のノードに障害が発生した場合にそれを検知するためのプロトコルです。 私達のシステムではFD_SOCKとFDというプロトコルを組み合わせて利用しています。また、VERIFY_SUSPECTというプロトコルも障害検知に必要なプロトコルとなります。 FD まず、FDではクラスタ内で円形になるように以下のように隣接グラフを作成します。 そして隣のノードで障害が発生していないかどうかを監視します。FDでは定期的に隣のノードに対してハートビートを行います。定めた「リトライ回数xタイムアウト時間」の間、隣のノードから応答がなかった場合はそのノードが死んでいるかもしれないと判断します。リトライ回数とタイムアウト時間はXMLの設定の max_tries と timeout で設定できます。 <FD timeout="60000" max_tries="20"/> 隣のノード死んでいるかもしれないと判断すると、それをマスタノードに通知します。この死んでいるかもしれないと疑うことを「SUSPECT」といいます。 FD_SOCK FD_SOCKでもFDと同じように円形の隣接グラフを作成します。FD_SOCKでは隣のノードの障害検知にはTCPコネクションを利用します。そしてTCPコネクションが切られた場合隣のノードが死んだと判断し「SUSPECT」を行います。 VERIFY_SUSPECT VERIFY_SUSPECTはSUSPECTされた状態のノードが本当に死んでいるのかを確認するプロトコルです。 VERIFY_SUSPECTはコーディネータノードによって行われ、SUSPECTされたノードにpingし生きているかを確認します。生きていた場合はSUSPECTされたノードのSUSPECTフラグを消します。死んでいた場合はクラスタから対象のノードを削除します。 SplitBrainの発生 以上のようにプロトコルを組み合わせることでJDGはクラスタを実現しています。しかし、設定が甘かったりするとSplitBrainを起こすことがあります。 今回実際にSplitBrainが発生し調査対応を行ったので紹介します。 SplitBrain 冒頭でも説明しましたがSplitBrainとは、1つのクラスタが2つ以上のクラスタに分断することをいいます。 冒頭ではクラスタのノード間で通信が分断された場合などに、ノード同士で通信できなくなりSplitBrainが発生すると紹介しました。 しかし今回の場合は通信障害が原因ではありませんでした。以下で今回SplitBrainが発生した原因・その流れを紹介します。 SplitBrain発生までの流れ 今回の根本的な原因はGCに起因するstop the world発生が原因でした。 以下のような4台のサーバーによるクラスタ(実際のサーバー台数とは異なります)について考えます。 障害検知プロトコルでは以下のようにノード同士を監視していると仮定した、SplitBrain発生までの流れです。 CノードでGCによる10分以上のstop the worldが発生 BノードがFDプロトコルでCノードが生きているかを確認する CノードはGCによるstop the worldが発生しているためFDに対するレスポンスができない BノードはCノードが死んでいると判断しマスタノードAに通知 マスタノードAはVERIFY_SUSPECTプロトコルによってCノードが実際に死んでいるのか確認 CノードはGCによるstop the worldが発生しているためVERIFY_SUSPECTに対するレスポンスができない マスタノードAはCノードが死んだと判断しCノードをクラスタから外す CノードのGCが終了 Cノードはクラスタから外されているとは気づかない Cノードは他のノードに通信ができないため死んだと判断し自分がマスタノードになる 以上のような流れでマスタノードが2つ出来上がり、クラスタが2つに分断しました。 SplitBrainの再現 本当にGCに起因するstop the world発生によるFD発生が原因でSplitBrainが発生したかについてはあくまでも予測でした。そこで今回FullGCを発生させ、FDが発生することによってSplitBrainが起こるかを再現しました。 以下が検証の手順です。 FDのタイムアウト時間を短くした設定 デバッグモードでアプリケーションを起動 JDGのキャッシュに対して大量のデータをinsert JConsoleを利用してFullGCを発生させる 実際にSplitBrainが発生したか確認 FDのタイムアウト時間を短く設定 検証で10分以上のFullGCを発生させるのは難しかったので、以下のようにFDのタイムアウトを短くしました。これにより1分くらいのFullGCが発生するとFDのタイムアウトが発生すると考えられます。 <FD timeout="6000" max_tries="10"/> デバッグモードでアプリケーションを起動 FDが発生したログはデバッグモードでしかログが吐き出されません。そのため実際にFDが発生しているのかもつかめていない状態でした。そこで検証ではFDが本当に発生するかどうかを確認するためにデバッグモードでアプリケーションを起動します。 JDGのキャッシュに対して大量のデータをinsert 一定期間のFullGCを走らせるために大量のオブジェクトを生成させる必要がありました。JDGのキャッシュは実際にはJavaのオブジェクトなので、ダミーデータをJDGに大量にinsertしました。 JConsoleを利用してFullGCを発生させる 次にFullGCを発生させます。FullGCが発生するまで待つということをしていると日が暮れてしまいます。そこでJConsoleというツールを利用しFullGCを発生させました。以下のように「GCの実行」というボタンを押すとFullGCが走ります。 実際にSplitBrain・FDが発生したか確認 最後にFDのタイムアウトが実際に発生しSplitBrainが発生したかを確認します。 以下のようにS3に2つのファイルが作られたことからSplitBrainが発生していることが確認できました。各ノード名は「SplitBrain発生までの流れ」で説明したノードに対して、「A=AP-1, B=AP-2, C=AP-3, D=AP-4」のように対応しています。 クラスタ1 AP-1-24102 4f8adfd5-4c86-55ce-cfe3-4fb11dfa145a 10.160.4.1:7800 T AP-2-31780 8db77c66-79fd-93f5-d72f-b718438a6061 10.160.4.2:7800 F AP-4-53254 0401172a-201e-a875-dac3-04b74a9f8812 10.160.4.4:7800 F クラスタ2 AP-3-42443 532qfd3d-543d-fda3-l3rf-ljl35dfahlh5 10.160.4.3:7800 F また、以下のログからFDが発生していることが確認できました。 AP-2によるFDのログ また、以下のログからFDのタイムアウトが発生していることが確認できました。 2019-04-10 16:27:27.625 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=1) 2019-04-10 16:27:33.625 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=2) 2019-04-10 16:27:39.626 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=3) 2019-04-10 16:27:45.628 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=4) 2019-04-10 16:27:51.629 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=5) 2019-04-10 16:27:57.630 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=6) 2019-04-10 16:28:03.632 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=7) 2019-04-10 16:28:09.633 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=8) 2019-04-10 16:28:15.634 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=9) 2019-04-10 16:28:21.636 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: received no heartbeat from AP-3-42556 for 10 times (60000 milliseconds), suspecting it 2019-04-10 16:28:21.638 DEBUG [org.jgroups.protocols.FD] (jgroups-50,AP-2-31780) AP-2-31780: suspecting [AP-3-42556] コーディネータノードによるVERIFY_SUSPECTのログ 最後にVERIFY_SUSPECTによりノードCがクラスタから削除対象となっていることを確認しました。 2019-04-10 16:28:24.753 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (VERIFY_SUSPECT.TimerThread-55,AP-2-31780) ISPN000094: Received new cluster view for channel framework: [AP-2-31780|10] (1) [AP-2-31780] 2019-04-10 16:28:24.753 INFO [org.infinispan.CLUSTER] (VERIFY_SUSPECT.TimerThread-55,AP-2-31780) ISPN100001: Node AP-3-42556 left the cluster 以上のようにFullGCの発生により実際にFDのタイムアウトによるSplitBrainが発生していることが確認できました。これにより今回のSplitBrainの根本原因がGCによるstop the worldであると判断できました。 対策 今回SplitBrainが発生するまで異常に気づかなかった原因の一番の問題は監視にありました。 システムの監視はもともとしっかりと行われていたのですが、私達が使い慣れているようなものではなくメトリクス等が簡単に参照できるようになっていませんでした。 そこで、Datadogを導入しメトリクスを可視化、それを元にした監視設定を行いました。 それ以来なにか異常が発生した場合すぐに対応し、システムを見直すというサイクルが生まれました。 また、JVMのパラメータ見直しやJVMの定期的な再起動を行い長時間のGCが起こりにくくなるように対策を行っています。 こちらについては実際に効果があるか長期的に検証を行っています。 本システムの課題 キャッシュとアプリケーションが一緒に載っていてる JDGの情報があまりない キャッシュとアプリケーションが一緒に載っている JDGの説明の中でも説明しましたが、私達のシステムではEmbedded modeを利用しています。そのためキャッシュとアプリケーションが同じJVM上で動いています。JDGは大量のキャッシュデータをJavaオブジェクトとして保持するため長時間のGCが発生することもあります。今回の10分以上のGCも大量データのGCが要因と考えています。これは、JDGキャッシュだけではなくJDGクラスタ上のアプリケーションにも影響を与えてしまいます。 また、アプリケーションのデプロイ時には毎度1台1台キャッシュをリバランスしながら行っています。その結果デプロイに時間がかかってしまうという問題があります。これは時間だけではなくデータのロストの危険性もあります。そこでこの問題を解決するためにキャッシュの切り離しや別サービスの利用等を検討しています。しかし、スループットの問題がありなかなか難航しています。 JDGの情報があまりない JDGの情報を調べていると情報が少ないか、あっても古い情報が多いとわかりました。今回の調査でも公式ドキュメントを参照する方法以外の情報がほとんどありませんでした。そして公式ドキュメントに書かれていないものについての詳細はソースコードを読む以外方法がありませんでした。これも相まって別サービスの利用を検討しています。 まとめ 今回JDGという分散キャッシュサーバーのSplitBrainの発生から、その調査と再現・対策を行いました。 そして、その過程で得た知見についてご紹介しました。 弊社では未知の領域であっても一歩ずつ調査を行い問題解決ができるエンジニアを募集しております。興味がある方は以下のリンクからぜひご応募ください。 tech.zozo.com また分散システム・リアルタイム分析システムに興味がある、こんな仕組みじゃなくてもっとこうするべきでしょとのお考えがある方はぜひお話しましょう! 参考記事 http://otndnld.oracle.co.jp/document/products/coherence/34/doc_cd/coh.340/B52977-01/definingdatagrid.htm https://www.slideshare.net/AdvancedTechNight/infinispan-14380723 https://enterprisezine.jp/dbonline/detail/5010
こんにちは。Innovation Initiative Div.の @ka_bi__ です。 普段は 「コーデ相談 by WEAR」 のプロダクトマネージャーを担当しています。 「業務上で発生する面倒なタスク、さっと自動化したい…!でもエンジニアに頼むにも忍びないし、わざわざコーディングするにはハードルが高い…!」 こんな場面は、多々ありませんか? タスク自動化ツール Zapierを使えば、GUIでサクッと解決! 本記事では、SlackをベースにZapierを活用した事例を3つお伝えしたいと思います。 Zapierとは Zapier とは、アメリカのタスク自動化ツールです。 複数のアプリ(Webアプリケーション)を連携させてワークフローを作り、業務を自動化させることができます。Web UI上からアカウント連携・ワークフロー作成ができるため、プログラミングの知識がなくても簡単に使うことができます。類似サービスとして IFTTT がありますが、業務系アプリとの連携を柔軟にカスタマイズできることから、ZOZO Technologiesでは有料のTeam Planを導入しています。 Zapについて Zapierでは複数のアプリを連携させて作ったワークフローの単位を「Zap」と呼びます。Zapには条件を設定する 「TRIGGER」 と、内容を設定する 「ACTION」 があり、それぞれをセットアップすることでワークフローを実行できます。今回ご紹介するZapを作成するには、Zapierのページ上部にある「MAKE A ZAP!」ボタンをクリックして始めましょう。 Zapの作成画面 1.新しくSlackチャンネルが作成されたら、Slackでbotがお知らせ ■やりたいこと 新しくSlackのチャンネルが作成されたら、特定のチャンネルに通知させたい。 手順 1.TRIGGER:Slack 新しいチャンネルが作成されたら、反応するように条件を設定。 2.ACTION:Slack 「誰がそのチャンネルを作成したか」・「作成されたチャンネル名」をポストするように設定すれば完成! 完成イメージ 2.特定のSlackチャンネルに画像がアップロードされたら、Google Driveに自動保存 ■やりたいこと 画像共有チャンネルにポストされた写真を、いつでも参照できるようGoogle Driveを連携・自動保存したい。 手順 1.TRIGGER:Slack 特定のチャンネルに新しいファイルがアップロードされたら、反応するように条件を設定。 2.ACTION:Google Drive 特定のGoogle DriveフォルダにFileをアップロードするように選択すれば完成! 完成イメージ 3.kintoneの通知がきたら、Slackでbotがお知らせ ■やりたいこと kintoneのレスポンス(承認完了・コメントがついた等)を特定のチャンネルに通知したい。 手順 ※色んな方法がありますが、今回は新着メールのタイトルを通知させています。 この方法ならkintoneの通知以外にも応用が効くので、是非活用してみてください。 0.Gmailでラベルを設定 Gmailにて件名に[kintone]と記載されているものにラベルをつける。 1.TRIGGER:Gmail 新しいメールを受信・[kintone]ラベルがついたもののみ反応するように条件を設定。 2.ACTION:Slack 特定のチャンネルに投稿 Slackのポスト内容は、メンションやURLなども追加・組み合わせも可能です。 任意のテキストをSlackにポストするように、設定すれば完成! 完成イメージ +α Slackに特定のURLがポストされたら、それを元にURLを生成・Slackでbotが投稿 ※(5分ではできませんでしたが)本気を出せばGUIだけでこんなこともできる一例をご紹介させてください。 ■やりたいこと 特定のURL(CMSツールのURL)がポストされたら、 URLを自動生成(確認環境のURL)・Slackにポストすることで確認コストを減らす。 ※URLはダミーです 手順 1.TRIGGER:Slack 特定のチャンネルに新しいメッセージがポストされたら反応するように設定。 2.FILTER 記事以外の内容にも反応すると困るので、FILTERをかけます。 たいてい「記事が上がりました!」とポストするので、特定の文字で指定。 3.Formatter text Transform機能を使って、URL内の文字列を admin から www へ、リプレイス。 ※末尾のURLは後で調整します 4.Formatter text Extract URL機能を使って、URLのみを抜き出し。 5.Slack Slackにポストする際、URLの末尾 ?test_article をくっつけることで確認環境URLを自動生成! 完成イメージ 最後に 業務効率化って「なんだか面倒くさいな…」「やろうと思っていたけど時間が経っていた」そんな悪循環に陥りがちですよね。Zapierはノンプログラミングにも関わらず自由度がとても高いツールです。自由度が高すぎて何をしようかたまに迷ってしまうレベルですが、効率化したい業務・仕様を明確にしておけばものの数分で効率化できます。 今回Zapierを導入してから「どうすれば、より業務を効率化できるだろう?」と高い解像度で考える習慣が身につきました。本質的なことを考える時間を増やすためにも改めて業務を見直し、業務効率化を図る時間を思い切ってとってみるのがいいかもしれません。 ZOZOテクノロジーズのInnovation Initiative Div.では「第2のZOZOTOWNになる収益の柱をつくる」ため、デザインとエンジニアリング・ビジネス開発が完結する組織構成で様々な技術を用いたサービスの仮説検証を行っています。よりよい世の中、顧客体験を提供するために、最高のプロダクトを一緒に作りませんか? tech.zozo.com
こんにちは。音声UIの開発を担当している武田です。Alexaのスキル「 コーデ相談 by WEAR 」はスキルの応答から分析まで、ほぼ全てAmazon Web Services(以下、AWS)のみを使って構成されています。今回は処理の種類ごとにAWSの構成の内容を、 CloudFormation のコードとともに紹介していきます。 ユーザーに応答を返す AWS Lambda Amazon DynamoDB Amazon CloudWatch Amazon S3 CloudFormation 問題があった時にアラートを飛ばす Amazon CloudWatch Amazon SNS AWS Lambda CloudFormation 分析する Amazon Kinesis Data Firehose AWS Lambda Amazon S3 Amazon Athena Amazon QuickSight CloudFormation まとめ さいごに ユーザーに応答を返す Alexaでカスタムスキルを作成する際に、応答の内容を決める処理は任意のサーバーで行うことができます。コーデ相談は AWS Lambda を採用しており、以下のようなサービスがさらに連なっています。 Alexaからのリクエストは基本的に全てLambdaで受け付け、必要な情報や処理に応じて DynamoDB やWEARのAPIを叩く形です。最初に「ほぼ全てAWSのサービス」と表現した理由は、WEARのAPIの部分だけAWS以外のリソースを頼っているためです。サービスごとに役割と採用した理由を説明していきます。 AWS Lambda LambdaはユーザーがAlexaに話しかけたりした際に、Alexaから送られてくるリクエストの窓口です。このリクエストにはユーザーによる入力の内容やデバイスの情報が特定のフォーマットで含まれています。それらの情報をもとに返事の内容を決定し、Alexaが理解できる形でレスポンスを返すことがLambdaの役割となります。 リクエストを受け付けるエンドポイントは自由に指定できるので、EC2のインスタンスを立てたり、すでに稼働している自社のサーバーに向けても問題はありません。コーデ相談でLambdaを採用している理由はAlexaとの相性が良いためです。もう少し詳細に説明します。 本来LambdaはHTTPS等のエンドポイントを提供する場合、 API Gateway や Application Load Balancer (以下、ALB)を挟む必要があります。しかしAlexaのリクエストを受け付ける場合は、Lambdaの ARN(Amazonリソースネーム) をAlexaのコンソールで指定するだけで接続が完了します。つまり、インターネットにエンドポイントを公開する必要がないため、セキュリティに関して考えることが減ります。リクエストを許可するスキルをIDで指定することもできるので、関係ないスキルのリクエストを受け付けてしまうことも防げます。 Alexaのカスタムスキルの開発を助けてくれる Alexa Skills Kit SDK (以下、ASK SDK)というものがあります。このSDKではDynamoDBを用いてユーザーの情報を保存する仕組みも提供されており、Lambdaでの ロール の管理とも相性が良いです。 Lambdaには Amazon Virtual Private Cloud 内のリソースにアクセス可能なVPCモードが存在しますが、今回はこのモードにしていません。ALBを用いている場合など関連しているサービス次第ではLambdaをVPCに置く必要があります。しかし、コーデ相談はこれらのサービスを使っていませんし、VPCモードではコールドスタンバイが発生してしまうためVPCに置いていません。 Amazon DynamoDB Lambdaからユーザー情報の読み書きを行うために用います。コーデ相談では性別やチュートリアルのフラグなど、ユーザーIDからkey-valueの形で取得できる情報のみ扱うのでRDBは使用していません。DynamoDBをASK SDKから使うことで、ユーザーのIDなどを意識せずにユーザーの情報の書き込みと読み込みが可能となります。開発のハードルが下がりますし、データ操作の記述を行う必要も無くなるため、スキル内でユーザーの情報を保持しておくレベルであればおすすめです。 Amazon CloudWatch CloudWatch はスキルに関わるログを保存するために用います。 Lambdaからはログの書き込みのみを行い、コーデ相談では以下のログを保存しています。 ・スキルの起動や終了 ・デバッグ用のログ ・ユーザーの行動のログ Lambda自体が自動で書き込む受動的なログも、デバッグログといった能動的に取ろうとしているログもCloudWatchにまとめて保存しています。こうすることでログがごちゃ混ぜになりますが、データの流れがシンプルになりCloudWatchを見に行けば全てのログが保存されている安心感があります。CloudWatch自体の検索機能や、後述するAthenaを用いることで状況に応じて関係するログのみを抜き出すことも可能なので、現時点までで困ったことはありません。 Amazon S3 S3 には画面付きデバイスで用いるアイコンやチュートリアル動画などのアセットを置きます。AlexaのデバイスからAWSサービスへアクセスするときには基本的に全てLambdaを経由しますが、S3に置いたアセットへのアクセスのみLambdaを経由せず直接参照します。 この部分はHTTPSでアクセスできる形であれば何でも大丈夫です。動画などはそれなりの容量になりますし、しっかりとした配信を意識するならCDNやDNSの設計が必要になると思います。しかしAlexaスキルからの参照に限定した場合、配信するデータが少量の静的ファイルのみということもあり、CloudFrontは置かずS3を直接参照する形にしています。 CloudFormation ユーザーがスキルを使う際に必要なAWSのリソースは以下のテンプレートで作成できます。 AWSTemplateFormatVersion : 2010-09-09 Parameters : GlobalEnvironment : Type : 'String' AllowedValues : - 'production' - 'development' SkillId : Type : 'String' S3BucketNameLambdaSource : Type : 'String' S3KeyLambdaSource : Type : 'String' Resources : S3BucketSkillAssets : Type : 'AWS::S3::Bucket' DeletionPolicy : 'Retain' Properties : AccessControl : 'PublicRead' CorsConfiguration : CorsRules : - AllowedHeaders : - '*' AllowedMethods : - GET AllowedOrigins : - http://ask-ifr-download.s3.amazonaws.com - https://ask-ifr-download.s3.amazonaws.com MaxAge : 3000 BucketName : !Sub ${GlobalEnvironment}-skill-assets DynamoDBTableUsers : Type : 'AWS::DynamoDB::Table' Properties : BillingMode : 'PAY_PER_REQUEST' AttributeDefinitions : - AttributeName : 'id' AttributeType : 'S' KeySchema : - AttributeName : 'id' KeyType : 'HASH' TableName : !Sub ${GlobalEnvironment}-users IAMRoleLambdaAlexaSkillEndpoint : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'lambda.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess' Policies : - PolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Action : - 'dynamodb:*' Resource : - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDBTableUsers} - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDBTableUsers}/* PolicyName : !Sub ${GlobalEnvironment}-lambda-alexa-skill-endpoint LambdaFunctionAlexaSkillEndpoint : Type : 'AWS::Lambda::Function' Properties : Code : S3Bucket : !Ref S3BucketNameLambdaSource S3Key : !Ref S3KeyLambdaSource Runtime : 'nodejs8.10' Timeout : 120 Environment : Variables : ASSETS_BUCKET_NAME : !Ref S3BucketSkillAssets TABLE_NAME : !Ref DynamoDBTableUsers FunctionName : !Sub ${GlobalEnvironment}-skill-endpoint Handler : 'index.handler' Role : !GetAtt IAMRoleLambdaAlexaSkillEndpoint.Arn LambdaPermissionAlexaSkillEndpoint : Type : 'AWS::Lambda::Permission' Properties : FunctionName : !Ref LambdaFunctionAlexaSkillEndpoint Action : 'lambda:InvokeFunction' Principal : 'alexa-appkit.amazon.com' EventSourceToken : !Ref SkillId LogsLogGroupLambdaFunctionAlexaSkillEndpoint : Type : 'AWS::Logs::LogGroup' Properties : LogGroupName : !Sub /aws/lambda/${GlobalEnvironment}-skill-endpoint 全体的にですが、 GlobalEnvironment というパラメータを使ってリソース名をまとめて変えられるようにしています。リリース後は本番環境と開発環境を区別したくなるはずなのでつけておきましょう。 S3BucketSkillAssets はAlexaから直接参照される部分なので、 AccessControl は PublicRead にしてCORSの設定をしておきます。この辺りは こちらの記事 に詳しく書かれています。 DynamoDBの BillingMode を PAY_PER_REQUEST にしているのは、Alexaのスキルの起動回数が読めないためです。ある程度アクセスが落ち着いてきたら PROVISIONED に変更します。テーブルのスキーマはASK SDKで保存されるフォーマットに合わせています。 DynamoDBのテーブルとS3のバケットはそれぞれ、Lambdaの関数内で名前が必要になるので、環境ごとの切り替えのしやすさを考慮して環境変数で渡すようにしています。 CloudWatchの部分はLambdaが1回でも実行されれば自動で作られますが、あとで他のリソースから参照が発生するので明示的に作っておきます。 問題があった時にアラートを飛ばす スキル内でエラーを検知した場合に、Slackにメッセージを流せるようにします。処理の流れはこのようになります。 流れ自体はかなりオーソドックスで、CloudWatchのログを元にエラー数を調べ、閾値を超えた時にLambda関数を実行するという流れです。Lambda関数内でメッセージを成形してSlackのIncoming Webhooksを叩きます。使っているサービスを見ていきましょう。 Amazon CloudWatch スキルのログを流すために使っているCloudWatchですが、ログを元にエラー数を調べることも可能です。具体的には以下のことをやっています。 ・特定の文字列を含むログが一定の期間で何度発生しているかを計算 ・計算結果が指定した閾値を超えているかチェック ・閾値を超えた場合にトピックを発行 トピックは他のサービスで処理を始めるためのトリガーとなるもので、この後に出てくるSNSでどのサービスを動かすか指定していきます。メールを送るだけならトピックを発行せずにCloudWatchだけで完結しますが、状況によってメンションをつけたり、異なるメッセージを流すためにSNSを経由するようにしました。 Amazon SNS SNS はSimple Notification Serviceの略で、トピックに応じてLambda関数を実行する以外にも、プッシュ通知やメールを送信できます。今回、SNSはトピックとLambdaを繋ぐためだけに用いているため、Slackにメッセージを流す具体的な処理は全てLambdaで記述します。 AWS Lambda Slackにメッセージを飛ばします。エラーの種類に応じてメッセージや色を変更しています。詳しい内容は、後述するテンプレートに直接載せてありますのでそちらを見てください。 CloudFormation ログを元にエラーなどのアラートを飛ばすリソースは以下のテンプレートで作成できます。 AWSTemplateFormatVersion : 2010-09-09 Parameters : GlobalEnvironment : Type : 'String' AllowedValues : - 'production' - 'development' SlackChannel : Type : 'String' SlackWebhookUrl : Type : 'String' Resources : LogsMetricFilterSkillError : Type : 'AWS::Logs::MetricFilter' Properties : FilterPattern : 'SKILL_ERROR' # アプリケーション側で指定しているログのプレフィックス LogGroupName : !Ref LogsLogGroupLambdaFunctionAlexaSkillEndpoint MetricTransformations : - MetricName : 'skill-error' MetricNamespace : !Sub ${GlobalEnvironment}-metric MetricValue : '1' CloudWatchAlarmSkillError : Type : 'AWS::CloudWatch::Alarm' Properties : AlarmActions : - !Ref SNSTopicSkillError AlarmName : !Sub ${GlobalEnvironment}-skill-error-alarm ComparisonOperator : 'GreaterThanThreshold' EvaluationPeriods : 1 # 最初は少しでも発生したら飛ばすようにする MetricName : 'skill-error' Namespace : !Sub ${GlobalEnvironment}-metric Period : 60 Statistic : 'Sum' Threshold : 0 SNSTopicSkillError : Type : 'AWS::SNS::Topic' Properties : Subscription : - Endpoint : !GetAtt LambdaFunctionSlackNotification.Arn Protocol : 'lambda' TopicName : !Sub ${GlobalEnvironment}-skill-error-topic IAMRoleLambdaAlexaSlackNotification : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'lambda.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess' LambdaFunctionSlackNotification : Type : 'AWS::Lambda::Function' Properties : Code : ZipFile : | import json import logging import os from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError SLACK_CHANNEL = os.environ['SLACK_CHANNEL'] SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL'] logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context) : logger.info("Event : " + str(event)) message = json.loads(event[" Records"][ 0 ]["Sns"]["Message"]) logger.info("Message : " + str(message)) alarm_name = message['AlarmName'] reason = message['NewStateReason'] text = "想定していないアラームが発生しました: %s" % (alarm_name) if '-error-alarm' in alarm_name: text = '<!channel> コーデ相談スキルでエラーが発生しています。ログを確認してください。' slack_message = { 'channel' : SLACK_CHANNEL, 'text' : text, 'color' : '#FF0000' if ('-error-alarm' in alarm_name) else '#FFFF00' , 'fields' : [ { 'title' : alarm_name, 'value' : reason } , ] } req = Request(SLACK_WEBHOOK_URL, json.dumps(slack_message).encode('utf-8')) try : response = urlopen(req) response.read() logger.info("Message posted to %s", slack_message['channel']) except HTTPError as e : logger.error("Request failed : %d %s", e.code, e.reason) except URLError as e : logger.error("Server connection failed : %s", e.reason) Environment : Variables : SLACK_CHANNEL : !Ref SlackChannel SLACK_WEBHOOK_URL : !Ref SlackWebhookUrl FunctionName : !Sub ${GlobalEnvironment}-slack-notification Handler : 'index.lambda_handler' Role : !GetAtt IAMRoleLambdaAlexaSlackNotification.Arn Runtime : 'python3.6' Timeout : 10 LambdaPermissionSlackNotificationSkillError : Type : 'AWS::Lambda::Permission' Properties : FunctionName : !Ref LambdaFunctionSlackNotification Action : 'lambda:InvokeFunction' Principal : 'sns.amazonaws.com' SourceArn : !Ref SNSTopicSkillError AWS::Logs::MetricFilter の部分でどのようなログを数えるかを指定しています。上記の指定ですと SKILL_ERROR という文字列を含むログを数えてくれますので、同じフォーマットでエラーのログを吐き出せるようにしておけば検知するようになります。今回はエラーだけですが、WARNや警告用の設定も用意しておけば異なるメッセージも簡単に流せます。 アラートを出すときの閾値はかなり下げてあります。Alexaのスキルは基本的にシンプルですし、大量にエラーが出るようなバグを仕込むことは少ないだろうということで、最初はこれくらいで良いと思います。 スキルの処理のように、複雑な関数になる場合はLambda用のコードを別のファイルで管理すると思います。ただ、今回のようにSlackのAPIを叩く程度であれば、テンプレートの中にLambdaのコードを直接記述した方が管理や更新が楽です。 分析する 分析と言っても色々あると思いますが、今回はSQLで集計し、その結果をBIツールで可視化できるようにします。例によって分析に関わる処理の流れがこちらです。 AWSにはAthenaというサービスで集計などを行うことができます。Athenaでクエリを叩くためにはある程度データを整形する必要があるため、Kinesis Data Firehoseで定期的にログを読み込み、S3に書き出します。サービスごとに具体的に説明していきましょう。 Amazon Kinesis Data Firehose Kinesis Data Firehose はログなどのストリーミングデータを、別のサービスに配信できるサービスです。コーデ相談の場合ですと、CloudWatchのログを読み込み、整形した結果をS3に保存しています。 ログが追加されるタイミングでS3にファイルを追加する方法もあります。しかし、その方法ではログの量が増えるにつれて処理自体の回数が増えてしまうため、指定した間隔でまとめて処理を実行できるKinesis Data Firehoseを採用しました。 AWS Lambda ログの読み込みや、データの書き出しはKinesis Data Firehoseがやってくれますので、このLambdaではデータの整形のみ行います。CloudWatchのログをS3に置くだけであれば整形は必要ありませんが、Athenaでクエリを叩くために以下の形にします。 ・1つのレコードを1行にまとめる ・ログの中身をJSONのkey-valueの形にする AthenaはJSON形式以外にもCSVやParquetも指定できます。今回はカラム名をKey名でマッピングでき、整形が楽そうと言うことでJSONにしました。 Amazon S3 Athenaから叩くデータの置き場として用います。BigQueryの場合はBigQueryにデータを読み込ませることになりますが、Athenaの場合S3など他のサービスに置いてあるデータをそのまま参照できます。 Amazon Athena Athena はクエリを書いて集計するために用います。AthenaはSQLのエンジンがPrestoになっており、JSONを扱える関数やWindow関数なども用意されているので、一通りの集計は可能です。 設定としてテーブルを作成(Create table)する必要があります。テーブルを作成とは言っていますが、この作業ではどこにあるデータをどのようなカラムとして扱うかを定義する作業になります。Athenaは現時点でCloudFormationに対応していないので、GUIかCLIで設定していきましょう。 具体的な設定方法は記事がいくつか見つかりますので、割愛します。 Amazon QuickSight QuickSight はデータを可視化するダッシュボードが作成できるBIツールです。可視化するデータはAthenaによる集計結果を用いることができますので、リアルタイムに溜まっていくデータを表示することが可能です。 QuickSightには SPICE という機能が備わっています。この機能を用いると集計対象のデータをインポートしてインメモリで集計するようになるので、表示が高速となります。インポートした データの更新 のタイミングは自由に指定できるので、表示するたびにS3などへのアクセスが発生することも防げます。 Athenaと同様にCloudFormationには対応していないため、初期設定などはGUIで行うようにしましょう。 CloudFormation 分析のために必要なデータをS3に溜め込む処理は、以下のテンプレートで作成できます。 AWSTemplateFormatVersion : 2010-09-09 Parameters : GlobalEnvironment : Type : 'String' AllowedValues : - 'production' - 'development' Resources : S3BucketLogs : Type : 'AWS::S3::Bucket' DeletionPolicy : 'Retain' Properties : AccessControl : 'LogDeliveryWrite' BucketName : !Sub ${GlobalEnvironment}-logs LogsSubscriptionFilterAccessLog : Type : 'AWS::Logs::SubscriptionFilter' Properties : DestinationArn : !GetAtt 'KinesisFirehoseDeliveryStreamAccessLog.Arn' FilterPattern : 'ACCESS_LOG' # アプリケーション側で指定しているログのプレフィックス LogGroupName : !Ref LogsLogGroupLambdaFunctionAlexaSkillEndpoint RoleArn : !GetAtt 'IAMRoleLogsSubscriptionAccessLog.Arn' IAMRoleLogsSubscriptionAccessLog : Type : AWS::IAM::Role Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : !Sub 'logs.${AWS::Region}.amazonaws.com' Action : 'sts:AssumeRole' Policies : - PolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Action : - 'firehose:PutRecord' - 'firehose:PutRecords' Resource : - !GetAtt 'KinesisFirehoseDeliveryStreamAccessLog.Arn' PolicyName : !Sub ${GlobalEnvironment}-policy-log-subscription-access-log KinesisFirehoseDeliveryStreamAccessLog : Type : 'AWS::KinesisFirehose::DeliveryStream' Properties : ExtendedS3DestinationConfiguration : BucketARN : !GetAtt S3BucketLogs.Arn BufferingHints : IntervalInSeconds : 60 SizeInMBs : 50 CompressionFormat : 'GZIP' Prefix : 'skillAccessLog/' ProcessingConfiguration : Enabled : true Processors : - Parameters : - ParameterName : 'LambdaArn' ParameterValue : !GetAtt 'LambdaFunctionKinesisFirehoseTranslate.Arn' - ParameterName : 'BufferSizeInMBs' ParameterValue : 3 - ParameterName : 'BufferIntervalInSeconds' ParameterValue : 60 Type : 'Lambda' RoleARN : !GetAtt 'IAMRoleKinesisFirehoseAccessLog.Arn' IAMRoleKinesisFirehoseAccessLog : Type : AWS::IAM::Role Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : Allow Principal : Service : firehose.amazonaws.com Action : sts:AssumeRole Policies : - PolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Action : - 's3:AbortMultipartUpload' - 's3:GetBucketLocation' - 's3:GetObject' - 's3:ListBucket' - 's3:ListBucketMultipartUploads' - 's3:PutObject' Resource : - !Sub 'arn:aws:s3:::${S3BucketLogs}' - !Sub 'arn:aws:s3:::${S3BucketLogs}/*' - Effect : 'Allow' Action : - 'lambda:InvokeFunction' - 'lambda:GetFunctionConfiguration' Resource : - !GetAtt 'LambdaFunctionKinesisFirehoseTranslate.Arn' PolicyName : !Sub ${GlobalEnvironment}-policy-kinesis-firehose-access-log LambdaFunctionKinesisFirehoseTranslate : Type : 'AWS::Lambda::Function' Properties : Code : ZipFile : | import base64 import gzip import StringIO import json def lambda_handler(event, context) : output = [] for record in event['records'] : payload = base64.b64decode(record['data']) fileobj = StringIO.StringIO(payload) logs = None with gzip.GzipFile(fileobj=fileobj, mode="r") as f : logs = f.read() fileobj.close() # Athena用にデータを整形 log_dict = json.loads(logs) logGroup = log_dict.get("logGroup") log_list = [] for log_event in log_dict.get("logEvents") : log_values = log_event["message"].split('\t') if len(log_values) >= 4: log_event["request_type"] = log_values[ 3 ] if len(log_values) >= 5: log_event["user_id"] = log_values[ 4 ] if len(log_values) >= 6: log_event["supported_interfaces"] = log_values[ 5 ] log_list.append(json.dumps(log_event)) # new line per record log_txt = log_list[ 0 ] + " \n " if len(log_list) >= 2: log_txt = " \n " .join(log_list) + " \n " output_record = { 'recordId' : record [ 'recordId' ] , 'result' : 'Ok' , 'data' : base64.b64encode(log_txt) } output.append(output_record) print('Successfully processed {} records.'.format(len(event['records']))) return { 'records' : output } FunctionName : !Sub ${GlobalEnvironment}-kinesis-firehose-translate Handler : 'index.lambda_handler' Role : !GetAtt IAMRoleLambdaKinesisFirehoseTranslate.Arn Runtime : 'python2.7' Timeout : 120 IAMRoleLambdaKinesisFirehoseTranslate : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'lambda.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess' コーデ相談ではユーザーの行動ベースで分析がしたかったので、専用のログを ACCESS_LOG から始まるようにして流し、データとして蓄積するようにしました。 ログの加工の流れの注意点としては、それぞれの出力の形式です。Kinesis Data Firehoseに対してCloudWatchからログを送る場合、gzipで圧縮された状態になっています。また、Athenaで集計できるデータとして扱う場合拡張子が *.gz になっている必要があるなど注意が必要です( Athenaでサポートする圧縮形式 )。この辺りは AWS::KinesisFirehose::DeliveryStream リソースやLambda関数のコードで吸収しています。 まとめ バケットやパラメータなどの名前は変えてありますが、上記のテンプレートからスタックを作ることで、実際に稼働しているコーデ相談と同じ構成を作ることが可能です。Alexaのスキルで多機能なことは少ないですし、アクセス数も膨大に増えるとは考えづらいので今回紹介した構成でほとんどの場合は事足りると思います。 Alexaだけで見るとコンソールから設定するだけで十分かもしれませんが、CloudFormationはコードベースでのレビューができたり、同じ構成を再現できるメリットがあります。特にアラートや分析の部分はAlexa特有の話ではありません。一般的なサービスでも同じ構成が使えますので、次の開発でも活用し改善していく予定です。 さいごに AWSを意識せずにスキルを提供できる Alexa-hostedスキル なども発表され、Alexaの開発ハードルはどんどん下がっています。しかしサービスとして提供しグロースさせようとしたり、環境が複数になると管理するリソースは増えてきますので、 今回紹介したテンプレートが役に立てば幸いです。 ZOZOテクノロジーズのInnvation Initiative部では今後普及するであろう技術を先行研究し、様々な技術を用いたサービスを開発しています。よりよいユーザー体験を提供するために、技術を駆使して最高のプロダクトを作りませんか? www.wantedly.com
Gopher's design for Ryuta Tezuka( @Tzone99 ) こんにちは、ZOZOテクノロジーズ開発部の池田( @ikeponsu )です。 本記事では、 Go言語における画像処理の可能性を、ベンチマークを通して探ってみたいと思います。 はじめに 業務内でGo言語での画像処理を行う機会があり、Goの標準パッケージやGoCVについて調べていました。 ただ、画像処理に関する記述はまだまだ少なく、実装している人自体も少ないのかなという印象でした。 今回行った「Go言語での画像処理の速度はどの程度か」のベンチマークが、これからGo言語で画像処理の実装を行おうとしている方の参考になればと思います。 ベンチマークの内容 比較対象 C++のOpenCV内のバイリニア補間 GoCV内のバイリニア補間 Go言語とimageパッケージを使って実装したバイリニア補間 処理内容 画像入出力 バイリニア補間で画像サイズを1/2に縮小 処理枚数 以下サイトで入手した人物画像10000枚。 Labeled Faces in the Wild: http://vis-www.cs.umass.edu/lfw/ マシンスペック 検証の内容 C++のOpenCV内のバイリニア補間 使用したライブラリ opencv : https://github.com/opencv/opencv/releases ソースコード #include <opencv2/opencv.hpp> #include <sys/types.h> #include <dirent.h> #include <string> #include <iomanip> #include <sstream> #include <sys/stat.h> #include <sys/types.h> // 読み込む画像枚数を多めに定義 #define MAX_IMAGESIZE 10000 int main( int argc, char *argv[]) { std::cout<< "start" <<std::endl; cv::Mat search_img[MAX_IMAGESIZE]; time_t t = time( nullptr ); // 形式を変換する const tm* lt = localtime(&t); // ディレクトリ名を作成 std::stringstream s; s<< "20" ; s<<lt->tm_year- 100 ; s<<lt->tm_mon+ 1 ; s<<lt->tm_mday; s<< "-" ; s<<lt->tm_hour; s<<lt->tm_min; s<<lt->tm_sec; std::string outputPath = s.str(); outputPath = "../result/" + outputPath; mkdir(outputPath.c_str(), 0 755 ); cv::Size size = cv::Size{ 0 , 0 }; cv::Mat output; std::chrono::system_clock::time_point start; std::chrono::system_clock::time_point end; // 計測開始時間 start = std::chrono::system_clock::now(); for ( int i = 0 ; i < MAX_IMAGESIZE; i++ ){ // 画像のディレクトリ、ファイル名、拡張子を指定 search_img[i] = cv::imread( "../sample/" + std::to_string(i) + ".jpg" , 1 ); // 全ての画像を(連番で)読み込み終えるとループを抜ける if (!search_img[i].data) break ; cv::resize(search_img[i], output, size, 0.5 , 0.5 , cv::INTER_LINEAR); cv::imwrite(outputPath + "/" + std::to_string(i) + ".jpg" , output); } // 計測終了時間 end = std::chrono::system_clock::now(); // 処理に要した時間をミリ秒に変換 double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count(); std::cout<<elapsed / 1000 << "s" <<std::endl; return 0 ; } 検証結果 1回目計測:11.383s 2回目計測:11.303s 3回目計測:11.541s GoCV内のバイリニア補間 使用したライブラリ gocv : https://github.com/hybridgroup/gocv ソースコード package main import ( "fmt" "gocv.io/x/gocv" "image" "image/jpeg" "os" "path/filepath" "strconv" "time" ) // main処理 func main() { count := 0 datapath := filepath.Join( "入力先のパス" , "*.jpg" ) file, _ := filepath.Glob(datapath) var d float64 = 1 / 2 size := image.Point{ 0 , 0 } timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "出力先のパス" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() for _, item := range file{ exc(item, d, size, timeString, count) count++ } end := time.Now() fmt.Println( "A.Total time: " , end.Sub(start)) } func exc(item string , d float64 , size image.Point, timeString string , count int ) { img := gocv.IMRead(item, gocv.IMReadColor) rowSize := int ( float64 (img.Rows()) * d) colSize := int ( float64 (img.Cols()) * d) outputImg := gocv.NewMatWithSize(rowSize, colSize, gocv.IMReadGrayScale) gocv.Resize(img, &outputImg, size, 0.5 , 0.5 , gocv.InterpolationLinear) image, _ := outputImg.ToImage() path := filepath.Join( "出力先のパス" , timeString, strconv.Itoa(count) + ".jpg" ) qt := jpeg.Options{ Quality: 60 , } file, _ := os.Create(path) jpeg.Encode(file, image, &qt) } 検証結果 1回目計測:14.980s 2回目計測:14.841s 3回目計測:14.823s Go言語とimageパッケージを使って実装したバイリニア補間 使用したライブラリ image : https://golang.org/pkg/image/ ソースコード 画像入力 func Input (filePath string ) image.Image { file, err := os.Open(filePath) if err != nil { log.Fatal(err) } pngImage, _, err := image.Decode(file) return pngImage } バイリニア補間 func Bilinear(inputImage image.Image, f float64) image.Image { // 重み値を定義 var x float64 var y float64 // リサイズ後 size := inputImage.Bounds() size.Max.X = int(float64(inputImage.Bounds().Max.X) * f) size.Max.Y = int(float64(inputImage.Bounds().Max.Y) * f) // 逆数 reciprocalScalingRows := 1 / f reciprocalScalingCols := 1 / f // アウトプット画像を定義 outputImage := image.NewRGBA(size) var outputColor color.RGBA64 // 画像の左上から順に画素を読み込む for imgRows := 0; imgRows < size.Max.Y; imgRows++ { for imgCols := 0; imgCols < size.Max.X; imgCols++ { // 双一次補完式 // 元画像の座標定義 // 元画像の縦の座標 inputRows := int(float64(imgRows) * reciprocalScalingRows) // 元画像の横の座標 inputCols := int(float64(imgCols) * reciprocalScalingCols) // 補完式で使う元画像のpixel // point(0, 0) src00 := inputImage.At(inputCols, inputRows) // point(0, 1) src01 := inputImage.At(inputCols + 1, inputRows) // point(1, 0) src10 := inputImage.At(inputCols, inputRows + 1) // point(1, 1) src11 := inputImage.At(inputCols + 1, inputRows + 1) // 重み値を算出 x = float64(imgCols) * reciprocalScalingCols y = float64(imgRows) * reciprocalScalingRows // 小数点以下を抽出 x = x - float64(int(x)) y = y - float64(int(y)) r00, g00, b00, a00 := src00.RGBA() r01, g01, b01, _ := src01.RGBA() r10, g10, b10, _ := src10.RGBA() r11, g11, b11, _ := src11.RGBA() // 拡大後の画素を算出 outputColor.R = uint16((1 - x) * (1 - y) * float64(r00)) outputColor.G = uint16((1 - x) * (1 - y) * float64(g00)) outputColor.B = uint16((1 - x) * (1 - y) * float64(b00)) outputColor.R += uint16(x * (1 - y) * float64(r01)) outputColor.G += uint16(x * (1 - y) * float64(g01)) outputColor.B += uint16(x * (1 - y) * float64(b01)) outputColor.R += uint16((1 - x) * y * float64(r10)) outputColor.G += uint16((1 - x) * y * float64(g10)) outputColor.B += uint16((1 - x) * y * float64(b10)) outputColor.R += uint16(x * y * float64(r11)) outputColor.G += uint16(x * y * float64(g11)) outputColor.B += uint16(x * y * float64(b11)) outputColor.A = uint16(a00) outputImage.Set(imgCols, imgRows, outputColor) } } return outputImage } main package main import ( "fmt" "image/jpeg" "img-test/ioimg" "img-test/procimg" "os" "path/filepath" "strconv" "sync" "time" ) func main() { count := 0 datapath := filepath.Join( "入力先のパス" , "*.jpg" ) file, _ := filepath.Glob(datapath) timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "data" , "result" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() for _, item := range file{ exc(item, timeString, count) count++ } end := time.Now() fmt.Println( "B.Total time: " , end.Sub(start)) } func exc(item string , timeString string , count int ) { img := ioimg.Input(item) rimg := procimg.Bilinear(img, 0.5 ) path := filepath.Join( "出力先のパス" , timeString, strconv.Itoa(count) + ".jpg" ) qt := jpeg.Options{ Quality: 60 , } file, _ := os.Create(path) jpeg.Encode(file, rimg, &qt) } 検証結果 1回目計測:35.220s 2回目計測:35.162s 3回目計測:35.238s goroutineで実装した場合 先程比較したGo言語のソースコードの処理をgoroutineで書いた場合、実装前に比べどの様な差があるか気になったので、追加で検証してみました。 GoCV内のバイリニア補間 ソースコード main package main import ( "fmt" "gocv.io/x/gocv" "image" "image/jpeg" "os" "path/filepath" "strconv" "sync" "time" ) func main() { count := 0 datapath := "Data/sample/*.jpg" file, _ := filepath.Glob(datapath) var d float64 = 1 / 2 size := image.Point{ 0 , 0 } timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "Data" , "result" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() var wg sync.WaitGroup for _, item := range file{ wg.Add( 1 ) go func (item string , d float64 , size image.Point, timeString string , count int ) { defer wg.Done() exc(item, d, size, timeString, count) }(item, d, size, timeString, count) count++ } wg.Wait() end := time.Now() fmt.Println( "A.Total time: " , end.Sub(start)) } 検証結果 1回目計測:3.253s 2回目計測:3.299s 3回目計測:2.975s Go言語とimageパッケージを使って実装したバイリニア補間 ソースコード main package main import ( "fmt" "image/jpeg" "img-test/ioimg" "img-test/procimg" "os" "path/filepath" "strconv" "sync" "time" ) func main() { count := 0 datapath := "Data/sample/*.jpg" file, _ := filepath.Glob(datapath) timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "data" , "result" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() var wg sync.WaitGroup for _, item := range file{ wg.Add( 1 ) go func (item string , timeString string , count int ) { defer wg.Done() exc(item, timeString, count) }(item, timeString, count) count++ } wg.Wait() end := time.Now() fmt.Println( "B.Total time: " , end.Sub(start)) } 検証結果 1回目計測:8.989s 2回目計測:9.118s 3回目計測:9.192s まとめ 今回、比較対象とした3つのソースコードでの処理速度差ですが、多少の処理内容の差や自作コードのチューニング不足によるところもあると思います。 ただ、ベンチマークを行うことで新たな発見もありましたので、あくまで参考の一部としてご確認いただければと思います。 処理 1回目(s) 2回目(s) 3回目(s) C++のOpenCV内のバイリニア補間 11.383 11.303 11.541 GoCV内のバイリニア補間 14.980 14.841 14.823 Go言語とimageパッケージを使って実装したバイリニア補間 35.220 35.162 35.238 【goroutine】GoCV内のバイリニア補間 3.253 3.299 2.975 【goroutine】Go言語とimageパッケージを使って実装したバイリニア補間 8.989 9.118 9.192 「Go言語とimageパッケージを使って実装したバイリニア補間」のソースコードは以下にアップしていますので、良ければご覧ください。 github.com また、Imageパッケージを使って実装した画像処理のその他のアルゴリズムも、これからアップしていく予定です。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、開発部の塩崎です。 最近はCloudFormation・Embulk・Digdagを使った仕事をすることが多く、一番使う言語がYAMLになりました。 今年福岡で開催されたRubyKaigi 2019ではZOZOテクノロジーズはRubyスポンサーとして協賛させていただきました。 カンファレンス中のスポンサーブースの出し物として、DroidKaigi 2019と同様にファッションチェックアプリの展示を行いました。 DroidKaigiの展示と全く同じでは芸がないと考え、今回のRubyKaigiのためにRuby on Lambdaでランキング機能を作成しました。 本記事では、そのランキング機能の説明をしたいと思います。 ファッションチェックアプリのランキング機能とは まず、ファッションチェックアプリの説明をします。 このアプリはDroidKaigi 2019のために作成されたデモアプリです。 スマートフォンのカメラで全身画像を撮影し、機械学習を用いてファッションの採点を行います。 機械学習モデルの作成に当たっては、当社の運営するファッションコーディネートアプリWEARのデータを利用しました。 techblog.zozo.com 今回作成したのは、このファッションチェックアプリのランキングサイトです。 ブース来訪者にはファッションチェックアプリで採点された結果をTwitterに投稿してもらいます。 ランキングサイトは、その投稿結果をTwitterから自動的に集計し、ランキング形式で表示します。 開発の経緯 RubyKaigiにスポンサーとして参加するにあたって何かしらの企画をやることを考えていました。 昨年はRubyに関するアンケートを行いましたが、今年も同じものをやってもつまらないので、何か他の企画をやろうということになりました。 また、各社ともスポンサーブースに対する力の入れ方が年々上がっております。 魅力的な展示を行うために、今まで以上にこだわる必要がありました。 DroidKaigiでデモアプリを作成したら来場者のウケが良かったとの広報からの助言があり、RubyKaigiでもそれを真似してデモアプリを作ることにしました。 RubyKaigi用に作成するデモアプリですので以下の2点を要件にしました。 Rubyを使っている(MUST) 最近にリリースされた「何か」を活用している(SHOULD) というわけで、昨年のre:InventでRubyサポートが発表されたばかりのAWS Lambdaを使い、ファッションチェックアプリのランキングサイトを作ることにしました。 技術構成 全体のアーキテクチャを上図に示します。 また、ソースコードはこちらのGitHubリポジトリで公開しております。 https://github.com/st-tech/fashion_check_ranking このアプリケーションは大きく2つのパートに分かれています。 1つめはクローラーで、Twitterから採点結果に関するツイートを取得し、それをDynamoDBに保存します。 2つめはランキングサイトで、DynamoDBに保存された情報をランキング形式で表示するWebサーバーです。 以下では、これら2つの詳細を紹介します。 クローラー クローラーはまず、Twitter APIを使ってRubyKaigiに関するハッシュタグである#rubykaigiと#zozotechの付いたツイートをすべて取得します。 そして、その中から正規表現を使って採点結果に関するツイートを抜き出します。 最後に、該当するツイートからユーザー名・アイコンURL・採点結果の点数を取得し、DynamoDBに保存をします。 この機能はAWS Lambdaの上で動作していて、定期的にLambda関数を実行するためにALBのヘルスチェック機能を利用しています。 ※ なぜCloudWatch EventsではなくALBを使っているのかは後ほど説明します。 参考: Twitter API クローラー部分のソースコード ランキングサイト ランキングサイトはDynamoDBに書かれたランキング情報を表示するためのWebサーバーです。 画像・JavaScript・フォントなどの静的ファイルはS3に配置し、動的コンテンツはRuby on Lambdaで生成しています。 小ネタですが、ALBのターゲットグループにLambdaを登録する機能も昨年のre:Inventで発表されたばかりなので試してみました。 Lambdaから呼ばれるハンドラーとRackとの間を繋ぐためのコードをAWSが公開していたので、それを使いました。 理論的にはRackサーバー上で動作するどのフレームワークも動作するはずですが、今回は軽量なフレームワークであるSinatraを利用しました。 参考: serverless-sinatra-sample ランキングサイト部分のソースコード 実装に当たって苦労したところ ここからは実装する時に、苦労したところ・工夫したところを紹介します。 クローラーを10秒ごとに起動したい問題 今回のアプリケーションの要件として、ブースの来訪者が別のブースへ行く前にランキングの結果を反映したいというものがありました。 そのため、10秒ごとくらいにクローラーを起動し、投稿内容を取得する必要がありました。 一方、CloudWatch Eventsでは1分ごとにLambda関数を呼び出すのが限界で、10秒ごとという要件を実現することはできません。 そこで、ALBのヘルスチェック機能に目をつけました。 定期実行したいLambda関数をALBのターゲットグループにいれ、ヘルスチェック間隔を10秒に設定しました。 結果的にこの方法はうまくいき、約10秒間隔でLambda関数を実行できました。 しかし、厳密に10秒間隔になることはなく、5〜20秒間隔くらい(実測値)になるときもありました。 どのような条件で本来設定した間隔からずれるのかの原因究明は結局できませんでした。 今回のアプリケーション的に致命的にはならないため、深追いはしませんでした。 このような事情があるため、このALBをcron代わりに使用する作戦を本番環境で行うことはオススメしません。 そもそもの話ですが、ALBのヘルスチェック機能はcronのために作られてはいません。 ALBを外部公開してしまったら、想定外のアクセスがあった ALBをうっかり外部公開するというミスを犯してしまったために、意図せずにクローラーが起動するという現象が発生しました。 クローラーを起動するためのALBを作るときに、外部公開用WebサーバーのALB設定をコピーしてしまったために発生しました。 何者かがALBに対してアクセスをしてきて、そのアクセス回数分だけクローラーが起動してしまいました。 そのために、Twitter APIのアクセス頻度制限に引っかかるということが発生しました。 ALBのDNS名は十分に長く、ランダムなものになっていましたが、IPアドレスを直に指定してのアクセスがありました。 ログからアクセスのあったURLを調査したところ、WordPressの脆弱性を突くようなものでした。 IPアドレスに対してしらみつぶしにアクセスをしながら脆弱性のあるサーバーを見つけるBotが居ると思います。 この問題に対してはセキュリティグループを適切に設定することで外部アクセスを禁止する対応を行いました。 このことからもALBをcron代わりに使用することはオススメできません。 そもそもの話ですが、ALBのヘルスチェ(略) マルチバイト文字の配信問題 ここから先はランキングサイトで発生した問題です。 CSSファイルを配信するために、config.ruに以下のように書き込んで、静的配信用のディレクトリを設定しました。 set :public_folder , Proc .new { File .join(root, ' public ' ) } この状況でCSSファイルの中に日本語(マルチバイト文字)が入っているときに、ファイルの配信に失敗してしまいました。 CloudWatch Logsでログを確認したところ、以下のエラーが出力されていることが確認できました。 Error raised from handler_method { "errorMessage": "\"\\xE3\" from ASCII-8BIT to UTF-8", "errorType": "Function<Encoding::UndefinedConversionError>", "stackTrace": [ ] } このエラーはエンコーディングがASCII-8BITである文字列をUTF-8に変換しようとする時のエラーです。 xxdで確認したところ、この\xE3はCSSファイルにUTF-8で書かれている ヒラギノ角ゴ という文字列の先頭1バイトのバイトに一致していることが分かりました。 \xE3というバイト表現はASCIIコード外なため、正常に文字コードの変換を行うことができないようです。 ASCII-8BITが何であるのかは以下の記事が詳しいので、このあたりに興味ある人は読んでみるといいかもしれません。 Ruby: US-ASCII-8BITというエンコードを理解する(翻訳) 今回のケースでは静的ファイルのエンコーディングはUTF-8しかないことが分かっていました。 そのため、 String#force_encoding で強制的にエンコーディングをUTF-8にする対応を取りました。 Lambda関数の返り値であるresponseハッシュを生成している部分を以下のように書き換えました。 def handler ( event :, context :) # 略 response = { ' statusCode ' => status, ' headers ' => headers, ' body ' => body_content.force_encoding( ' UTF-8 ' ) # force_encoding追加 } # 略 response end バイナリー配信問題 次はバイナリーファイルを配信しようとしたときに発生した問題です。 なお、このアプリケーションで配信していたバイナリーファイルは、フォントとファビコン画像です。 バイナリーファイルもCSSファイルと同様に以下のように配信していました。 set :public_folder , Proc .new { File .join(root, ' public ' ) } このときもまた配信エラーが発生してしまいました。 CloudWatch Logsには以下のエラーが吐き出されていました。 Error raised from handler_method { "errorMessage": "source sequence is illegal/malformed utf-8", "errorType": "Function<JSON::GeneratorError>", "stackTrace: [ ] } Lambdaが内部的にJSONへシリアライズをしているようで、そのときに問題が発生しているようです。 配信しようとしていたバイナリーがUTF-8的に不正なバイト列だったことに起因するものです。 実際に以下のコードで確認したところ、同様のエラーが発生しました。 bin = File .read( ' 問題のファイル ' ) JSON .generate({ ' bin ' => bin}) # エラー発生 この問題に対しては、静的ファイルはS3で配信する対応を取りました。 そしてその前段にCloudFrontを配置し、静的コンテンツと動的コンテンツの振り分けはCloudFrontに担当させました。 後日談になりますが、ALBのドキュメントをみたら以下のように書かれていました。 isBase64Encodedプロパティをtrueにすれば、Lambdaからバイナリーファイルの配信をすることもできそうです。 機会があれば試してみたいと思います。 To include a binary content in the body of the response, you must Base64 encode the content and set isBase64Encoded to true. The load balancer decodes the content to retrieve the binary content and sends it to the client in the body of the HTTP response. ALBのドキュメント 関数インスタンス再利用問題 最後の問題はLambdaによって、関数インスタンスが再利用された問題です。 クローラーによってDynamoDBのデータが更新されても、ランキングページの表示が変わらないという現象が発生しました。 最初はCloudFrontが動的コンテンツまでキャッシュをしてしまっているのかと思いましたが、MaxTTLは0に設定しているため、キャッシュはしないはずです。 また、CloudWatch Logsで確認したところ、Lambda関数はブラウザからのアクセス毎に起動しています。 この原因はLambdaが関数インスタンスを再利用しているためでした。 LambdaのFAQには以下のように書かれており、場合によっては関数インスタンスが再利用されるようです。 Q: Will AWS Lambda reuse function instances? To improve performance, AWS Lambda may choose to retain an instance of your function and reuse it to serve a subsequent request, rather than creating a new copy. To learn more about how Lambda reuses function instances, visit our documentation. Your code should not assume that this will always happen. AWS Lambda FAQs DynamoDBからの読み出し結果をクラスインスタンス変数にキャッシュして使いまわしているコードがあったので、この部分が原因でした。 そのため、クラスインスタンス変数を使用しないように修正するという対策を行いました。 グローバル変数やクラス変数も同様の問題の発生する可能性があるので、これらの変数を使う時には要注意です。 来場者の反響 スポンサーブースに訪れた多くの方にこの企画を楽しんでいただけました。 3日間で210ツイートもしていただいたことに感謝しています。 ブース来訪者の方とRubyに関する話をする時の取っ掛かりにもなったので、そういう意味でもこの企画をやってよかったと思いました。 1日目はランキング表示件数を上位15人にしていましたが、ランキング圏外だからツイートしないという方が何人かいました。 そこで、2日目以降はランキング表示数を倍の上位30位にしたところ、さらに多くの方にツイートしてもらうことができました。 (1日目: 62ツイート 2日目: 82ツイート) 初日よりも2日目のほうがブースの来場者が少なかったことを考えると、この修正は大成功でした。 一部修正が間に合わず、2日目の午前中は502 Bad Gatewayになってしまったこともありました。 RubyKaigiが開催される都市は美味しいものが多く、本会議後のイベントも多いため、当日に修正する時間があまりありません。 このことは来年以降の教訓にしていきたいです。 まとめ RubyKaigi 2019用のデモアプリをRuby on Lambdaで作り、スポンサーブースの企画としました。 以前のRubyKaigiでMatzさんが「Rubyをキメると気持ちイイ」という名言を残しましたが、やはりRubyは書いていて楽しい言語であることを実感できました。 特に最近はYAML職人になっていたので、久しぶりにキメるRubyの気持ちよさは最高でした。 来年もスポンサーをするかもしれないので、そのときは来年のZOZOテクノロジーズブースにも是非おこしください!! ZOZOテクノロジーズでは一緒にサービスを作っていく仲間を募集しています。 Rubyをキメながら開発ができるサービスもありますので、ご興味のある方は以下のリンクから是非ご応募ください。 tech.zozo.com
こんにちは!ZOZOテクノロジーズでバックエンド開発をしている @acjustplay です。 実は!この前!世界で一番大きいScalaカンファレンス Scala Days に行ってきました! 今年のScala Daysはスイスのローザンヌで、6/11〜13の3日間の開催でした。今まではアメリカとヨーロッパのどちらかでの開催が多かったですが、ちょうど今年がScala誕生15周年とScala Days開催10周年が重なり、特別な意味を持つ場所ーScalaとScala Daysの起点と言えるEPFL(スイス連邦工科大学ローザンヌ校)での開催になりました。 ちなみに今年はEPFLの50周年記念と私の初海外カンファレンス参加記念でもあり、もはや奇跡の宝箱としか言えないですね! あの有名な階段はEPFLの中にあります! 今回のScala Daysで私が一番注目していたのは、やはりScala 3の情報だったので、それに関連するセッションの内容を紹介していきたいと思います! Day 1: A Tour of Scala 3 まずはDay 1のオープニングキーノート、EDMとともに登場されたScalaの作者Martin Odersky教授からの「A Tour of Scala 3」。 セッションのスライドはこちらへ 会場おしゃれすぎます キーノートの内容は スライド にわかりやすく記載されているので、詳細はスライドをご覧ください。ここでは、ざっくりとまとめておきます。 Scala 3のロードマップ おそらく、みなさんが一番気になるのは「Scala 3いつ出るの?」という点でしょう。少なくとも私はめっちゃ気になっていました! キーノートによるとScala 3は 2020秋 にリリースされる予定です(Day 2のセッションで、これとはちょっと異なるロードマップも提示されていましたが…)。そしてScala 3へより移行しやすくするために、並行してScala 2.14の開発も進めているようです。 Odersky教授のScala 3おすすめ機能ランキング キーノートで一番比重が大きかったのがこの部分です。Odersky教授が、各レベル向けのScala 3のおすすめ機能ベスト3をピックアップしてくれました。 For beginners(初心者向け) Enums Toplevel definition Drop new For everyday coding(業務で使っている人向け) Delegates Extension Method Union Type For experts(上級者向け) Functions Everywhere Typeclass Derivation Match Types 各機能の細かい説明は スライド 、または dottyのドキュメント に記載されているので割愛します。 この中で、個人的にはDelegatesの導入に一番興味を持っています。Delegatesは今までの implicit の代わりとして紹介されました。モチベーションとしては、 implicit の中でも非常に強力な一部の機能は、簡単に記述できるため乱用されがちでした。その乱用を防ぐためにより安全かつ意図が伝わりやすいDelegatesが用意されたそうです。 dottyのドキュメント でも implicit を delegate で書き換えられる説明が載っているので、3に移行する予定があれば事前に慣れておいたほうがよさそうですね。 ちなみにOdersky教授は事前にTwitterでみんなのおすすめ機能のアンケートを実施して、以下のような結果になったそうです! For beginners(初心者向け): Enums For everyday coding(業務で使っている人向け): Union Type For experts(上級者向け):Typeclass Derivation 私もほぼ同じ考えだったので、日頃からアプリケーションコードを書いている人にとっては、記述量が減らせてシンプルに使える機能の方が人気なのかもしれないですね。 Day 2: Future-proofing Scala: the TASTY intermediate representation EPFLのLAMPチームのGuillaume Martresさんから、Scala 3の独自の変換用フォーマットTASTyの紹介です。 TASTyはdottyと一緒にScala 3から導入されます。例えばFoo.scalaをdottyでコンパイルしたら、Foo.tastyとFoo.class 2つのファイルができます。 tastyファイルはプログラム( .scala )の情報を保持して、実行可能なファイル( .class )に変換できる形式になりますので、イメージとしてはFoo.scala -> Foo.tasty -> Foo.classという感じです。 tastyファイル生成は型チェックの後に行われ、まずプログラムの情報を抽象構文木(Abstract Syntax Tree, AST)にパースして、タイパーで型情報を付与します。その型付けをした抽象構文木(Typed Abstart Syntax Tree)を保存しているのがTASTyです! そしてプログラムなのでコメントなどドキュメント情報も保存できます。フルのフォーマット情報は GitHub で確認できます。 TASTyを導入する一つの理由としては、今までScalaでバージョン間の互換性を担保しづらい課題を解決したいという点があるそうです。 根本的な機能改善や変更を加えたい時に、バイナリー互換性を破壊しないことはほぼ不可能です。例えば最近出た2.13でも、コレクション構造の見直しによって2.12とは非互換になっています。 TASTyを導入することで、対応するScalaバージョンのライブラリjarがまだ存在していなくても、classファイルを消してtastyファイルから再生成すればエラー発生させずに、自分のプロジェクトコードを実行できるとのことです。 ちょっと休憩でコーヒーの写真入ります。Scala Daysでは、常にカフェイン+各種飲み物+スナックが提供されています。 Day 2: How are we going to migrate to Scala 3.0, aka Dotty? LightbendのScalaチームのLukas Rytzさんから、Scala 3の変更点、移行プランの詳細紹介です。 もともとは、"How do we prepare ..." という表現のタイトルでしたが、Scala 3のロードマップが明確になったので、より肯定的な表現にされたらしいです。 セッションのスライドはこちらへ セッションの主な内容は以下の3点です。 Scala 3の変更点 Scala 2、3チーム開発の進め方 マイグレーションのタイムライン ここで要注意なのが、スライドにあるタイムライン(p.27)とキーノートのタイムラインが違う(!!?)ことです。セッションのスライドによるとScala 3のリリースが2021年に予定されていますが、果たしてどちらを信じるべきでしょうか!? そして、変更点として一番に挙げられたのがやはり implicit が変わることでした。10年前だと一つの機能でいろんなことができるのが良いとされていましたが、今では利用者の用途ごと最適な機能を提供するようにシフトしてきました。その考え方の変化が今回の変更につながったとのことです。 セッションの最後に、Scala 3はScala 2+1だ(ScalaのStandard Libraryは2系のものをそのまま使うので)とRytzさんが強調されていました。 Scala 3の変更が大きすぎて、もはや別の言語ではないかという議論がよくされます。このセッションを踏まえて、マイグレーションをスムーズに行えるように開発チームがすでに色々考慮、用意してくれていることがわかったので安心できますね! Day 3: Metaprogramming in Dotty EPFLのLAMPチームのNicolas Stuckiさんから、dottyでのMetaprogrammingのセッションです! ScalaのMetaprogrammingといえばマクロですが、Scala 2のmacrosのデザインはコンパイラの内部実装に依存しているので、そのままdottyとScala 3で使えません。そのためdottyではmacrosがリデザインされました。 その影響でdottyを使いたい場合は既存のマクロを書き直さないといけないです。ただし、マクロのプロジェクトをそのままdottyのプロジェクトから使う分であれば大丈夫だそうです。 セッションのスライドはこちらへ dottyからは新しいmacrosが導入され、さらにmacrosを使わなくてもMetaprogrammingができる機能が追加されます。セッションでは一番コアになる部分を紹介してくれました。 Inline Metaprogrammingのための新機能。このmodifierをつけるとコンパイラがいい感じにコードを展開してくれます。 Match Type 型パラメータをパターンマッチして型を返せる機能です。Metaprogrammingのための機能ではないですが組み合わせて使うと便利です。 Macros: Quotes & Splices ' と $ でマクロを作成できます。 ' はコードをデータに変換し、 $ はその逆の動きをします。お馴染みのstring interpolationに似ていますね! Macros: TASTy Reflect Quotes & Splicesだけではできない、型付け構文木(TASTy)を分析、構築できるAPI。 スライド では各機能の使い方をサンプルコード付きで丁寧に説明されていますので、一通り目を通してみてください! おまけ1:他の面白かったセッション Scala 3以外にも面白かったセッションがいっぱいあります。印象的だったセッションは以下の2つです! Day 2: Testing in the postapocalyptic future Daniel Westheideさんから、Scalaであまり聞かれないMutation Testingをどう実現するかの話です。 スライドはこちら Day 3: Pure Functional Database Programming‚ without JDBC doobie の作者Rob Norrisさんから、jdbcを使わず、かつ関数型プログラミングでPostgreSQLのデータベースを操作する話です。 実装したサンプルは Skunk というライブラリです(また実験中なので使わないでくださいとNorrisさんが言っていました)。 おまけ2:関連イベント 今年は10周年記念でもあり、みんなを盛り上げるために運営チームはかなり 関連イベント に力を入れていました。例えばローザンヌの市内観光ツアー(!)や、ラヴォー地区のブドウ畑観光ツアー(!!)はカンファレンスパスを持てば無料(!!!)で参加できます。私はスケジュールが合わずツアーに参加できなかったですが、それ以外にも素晴らしいイベントがたくさんありましたので少し紹介します! 自撮りブース そうです。自撮りです。あのScalaロゴのモチーフにもなったEPFL校内の階段と自撮りできる機会がカンファレンス期間中用意されていました。 階段と自撮りできます。チャレンジしてみましたが完全に失敗しました。 Scala Spree Scala SpreeはDay 1の朝一からオープニングキーノートが始まるまで、EPFLのキャンパスで行われます。 同じ会場で初心者向けのScala Bridgeも開催されています。 イベントでランチ券が配布されて、EPFLの学食を体験できます。 「開発者にもっとオープンソース文化に慣れてもらう」のがScala Spreeの趣旨です。なので参加者はOSSのプロジェクトにプルリクエストを出してマージしてもらうことが目標です。 プロジェクトの担当者は参加者に手伝ってもらいたいIssueのリストを用意して、参加者はその中からピックアップしてプルリクエストを出せるまで開発する感じです。もちろんリスト以外の自分が気になる課題があればそれを行っても構わないですし、プルリクエストを出さずに各プロジェクトをセットアップして試したりするのもOKです。結構ゆるい感じです。 プロジェクトはみんなさんお馴染みのscalac、Coursier、dotty、Scala.jsなどがあります、プロジェクトのMaintainersもその場でサポートしてくれますので、気軽に質問することができます。   ContributorのTシャツが欲しくてドキドキしながらScalaのリポジトリにプルリクエストを送ってみましたが、最後に、参加者全員にTシャツをプレゼントとして渡されました(笑) メインライブラリーへコミットするのは、ずっと難しそうとか、敷居が高そう、というイメージを持っていましたが、難易度が低くて簡単な修正だけど誰かに直してもらいたいというIssueもたくさんあります。 初めての人でもコミットしてもらいやすくするために、プロジェクト側も色々と工夫をしています。例えばScalaのリポジトリでは good first issue というラベルを、初めてプルリクエストを出す人向けのIssueにつけられています。私もここからIssueを探してプルリクエストを出しました! Diversity & Inclusion Lunch Day 2は、Scalaコミュニティの中で少数派になる人たち(underrepresented groups)のためのランチが用意されました。アイスブレイクとしてピープルビンゴゲームから始まり、そのあとみんなでランチを食べながら交流していきました。初めての海外カンファレンスで知っている人がいなかった状況の中、参加者と交流するきっかけを作ってもらえ、本当に助かりました。 ランチで sphere.it の運営者と話す機会があり、そのあとチケットをいただきました! ありがとうございます! ランチ以外にも、Diversity & Inclusionの パネルディスカッション があり、ダイバシティの重要性を発信する企画に感動しました。 Community Dinner Day 2の夜にカンファレンス参加者が全員参加できるCommunity Dinnerがローザンヌのオリンピック・ミュージアムにて開催されました。 場所をオリンピック・ミュージアムにする理由はこの映える眺めではなく、オリンピック精神である"friendship, solidarity and fair play"(友情、連帯感、フェアプレー)はScalaコミュニティが大事にしているバリューにもあてはまるからです。細かいところまでコミュニティの価値観が体現されています。 そしてまさかのジャズ演奏があります! リズムにのってみんな踊りだす! Community Dinnerで他の参加者とも交流ができて、他の会社/国のテック事情も聞けて大変良かったです。最後にみんなからの熱い要望に応じて、バンドのアンコール演奏を持ってCommunity Dinnerが円満閉幕しました。 おまけ3:ローザンヌ 最後のおまけでは、ローザンヌの風景をお送りします。 ローザンヌ大聖堂 Esplanade de Montbenon ローザンヌの街並み レマン湖の白鳥達 ローザンヌは非常に激しい坂が多い町です。毎日山登り気分でした(日本に帰ってきたら少し筋肉痛になりました)。 坂でもお構いなく駐車しています おわりに 行く前は、正直「どうせそのあと動画上がってくるし行かなくても…」と思いながら参加するかどうか少し迷っていました。しかし、実際に現地でセッションを聞いたり、参加者と交流したり、そして一番重要なのはみんながコミュニティをより良くさせよう、Scalaをもっといろんな人に知ってもらおう、と努力している姿を直接体験することができて、本当に良かったです。 ここで「Scalaのカンファレンス行ってみたいなぁー」と思ったあなた! 日本最大級のScalaカンファレンス、 Scala Matsuri が6/27〜29にお台場の東京国際交流館で開催されます! 面白いセッションはもちろん、パプリック向けの無料ワークショップ(!!!!)もありますので、気軽に参加してみてはいかがでしょうか? ZOZOテクノロジーズも将軍スポンサーとして協賛します! あとブースも出しますので、是非遊びにきてくださいー! 最後の最後に 今回のScala Daysの参加にあたって全ての費用を負担してくれた会社に感謝しかないです。さすが物価世界一高いスイスです。震えます。 ZOZOテクノロジーズは、最新技術を学びたければ会社が惜しげなく後押ししてくれます。エンジニアまだまだ募集していますので一緒に働いてみませんか? www.wantedly.com
こんにちは。音声UIの開発を担当している武田です。 前回の記事 ではAlexa Presentation Language(以下、APL)でどんなことができるかを説明しました。今回はどうやってコードの治安を保ちつつ、APLを書いていくかというお話をします。 APLをJavaScriptのオブジェクトで管理する ディレクティブを関数で生成する パーツを切り分ける 最後に APLをJavaScriptのオブジェクトで管理する *.json として管理するのをやめましょうという話です。APLの 概要 でJSONと言っていたり、 オーサリングツール で扱えるのはJSONファイルですが、JSONはやめてJavaScriptのオブジェクトにしましょう。 公式の中の人もそうしています。   Using AWS Lambda to Bring the Space Explorer Sample Skill to Life JavaScriptにすることで以下のようなメリットがあります。 レイアウト を別モジュールや変数に分けることで、異なる ディレクティブ でも共通化しやすくなる 関数を噛ませることで、 Vueのスロット のようにコンポーネントの中身を入れ替えられる JavaScriptのループ処理を使えば、 コマンド を機械的に生成できる コーデ相談スキル でもAPLは全てJavaScript(正確にはTypeScript)として扱っています。APLはコンポーネントごとに役割がはっきり決まっている性質上、どうしても階層が深くなりがちです。デザインを再現したりマルチデバイスに対応するためにも、切り分けやすい環境にしておくのは重要です。 ディレクティブを関数で生成する APLを使うためには描画される部分だけではなく、ディレクティブとしての フォーマット に従う必要があります。このフォーマットは各画面で共通の部分が多かったり、守るべき階層構造が複雑だったりします。そこでディレクティブを吐き出すための関数を用意してあげましょう。 例がこちらです。 export interface Template { /* ... */ } export interface Layout { /* ... */ } export interface Style { /* ... */ } export function buildRenderDocumentDirective( template: Template, layouts: Layout, styles: Style, options: { datasourceData?: any; } ): Directive { // 渡されたテンプレートと共通で使いたいテンプレートを組み合わせる const mainTemplate = { item: { type: 'Frame' , item: { type: 'Container' , item: template, height: '100vh' , width: '100vw' , } , style: 'mainFrame' , // commonStyles で指定された共通で使いたいスタイル } , } const directive: Directive = { document : { import : [ { name: 'alexa-layouts' , version: '1.0.0' , } , { name: 'alexa-styles' , version: '1.0.0' , } , { name: 'alexa-viewport-profiles' , version: '1.0.0' , } , ] , layouts: { ...commonLayouts, ...layouts } , // スプレッド構文は後勝ちなので 渡された `layouts` が優先される mainTemplate, resources, styles: { ...commonStyles, ...styles, } , // スプレッド構文は後勝ちなので 渡された `styles` が優先される type: 'APL' , version: '1.0' , } , token: 'anydocument' , type: 'Alexa.Presentation.APL.RenderDocument' , } // datasourceData は必要ない場合もあるので条件分岐 if (options.datasourceData !== undefined ) { directive. document .mainTemplate.parameters = [ 'payload' ] directive.datasources = { data: { properties: options.datasourceData, type: 'object' , } , } } return directive } 専用の関数で書き出すようにしたことでフォーマットのミスによる描画失敗はなくなりました。この仕組みを使ってよかった点はこの辺りです。 フォーマットを覚えなくていい 意識しなくても共通のテンプレートやスタイル、 Resources が反映できる データバインディングしたい時の payload の指定忘れがない 共通のレイアウトやスタイルと、画面独自のそれらをいい感じに混ぜてくれる 実際にコーデ相談は全ての画面で発生しうる処理(レベルアップの通知や検索の相槌)があるのですが、問題なく動作してくれています。また、画面独自のテンプレートやスタイルを分離できるのでシンプルになります。スキルによっては token は別のものを指定したくなるかもしれませんが、全ての画面で実行したいコマンドがあると破城するので token も共通にしてしまっています。 パーツを切り分ける ここまでで何度も出ているレイアウトやスタイルの話です。公式ドキュメントでも使おうと言っていますし、使っている人は多いと思います。ここではどのように分離して再利用しているかをコードベースで説明します。 コーデ相談はほとんどの画面でヘッダーとフッターが存在しています。このフッターを例に挙げて説明します。フッターのスクリーンショットと定義がこちらです。 コーデ相談のフッター export const footerLayouts = { Footer: { item: { type: 'Container' , when: '${suggestTexts.length > 0}' , data: '${suggestTexts}' , item: [ { type: 'Text' , text: '「アレクサ、${data}」' , fontSize: '@mainFontSize' , } , ] , lastItem: { type: 'Text' , text: 'と言ってみて' , fontSize: '@mainFontSize' , } , alignItems: 'center' , direction: 'row' , height: '@footerHeightSize' , justifyContent: 'center' , } , parameters: [ { default : [] , name: 'suggestTexts' , type: 'array' , } , ] , } , } @mainFontSize などのリソースを参照している部分はリソース名から意味を察していただけますと幸いです。このコードのポイントはこの辺りです。 Keyをレイアウト名にして、1つのパーツで使うレイアウトを1つのオブジェクトにまとめておく できるだけリソースやパラメーター、Flexboxレイアウトを使うようにして、柔軟に対応できるようにする パラメーターにはtypeを指定する フッターの例ですとレイアウトは1つですが、大きなパーツになってくると複数のレイアウトに分けたくなってくると思います。また大文字などで意味を持ちづらいKeyにレイアウト名を入れたほうが、標準のコンポーネントと同じ type: Footer のように使用できます。これらを踏まえて、レイアウトはオブジェクトでまとめて管理するのがオススメです。使用する場合はスプレット構文を使えば以下のように複数のパーツがある場合でも安心です。 const layouts = { ...headerLayouts, ...footerLayouts, } この方法に限った話ではないですが、レイアウトは名前がかぶるとまずいので命名規則は気をつけましょう。 最後に Alexaのスキルはシンプルなスキルが多く、コードの管理を重視するほど大規模なものは少ないと思います。声でできるタスクとして考えても、スキルはシンプルな方が良いでしょう。しかし、スキルでこなせるタスクがシンプルだったとしても、裏側の処理もシンプルとは限りません。柔軟に対応できるようソースコードは秩序を守っていきたいですね。 今回挙げたものはコーデ相談における一例ですので、「うちはこうしてるよ」などの記事や知見が増えてくると嬉しいです。どうしても海外が先行している音声UI界隈ですが、日本も盛り上げていきたいです。 ZOZOテクノロジーズのR&D新規開発チームでは今後普及するであろう技術を先行研究し、様々な技術を用いたサービスを開発しています。よりよいユーザー体験を提供するために、技術を駆使して最高のプロダクトを作りませんか? www.wantedly.com