TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

981

DroidKaigiで展示したファッションチェックアプリについて こんにちは。ZOZOテクノロジーズ開発部山田( @yshogo87 )です。 DroidKaigi 2019ではプラチナスポンサーとして、ブースを出展させていただきました。 DroidKaigi 2019 そのコンテンツとしてファッションチェックアプリを展示させていただきました。 今回はファッションチェックアプリがどのような仕組みになっているかを説明させていただきます。 ファッションチェックアプリとは ファッションチェックアプリとは、ユーザーが撮影した全身の写真について、WEARに投稿されたコーディネートを元に作成した学習モデルを使用して採点を行うものになっています。 技術的構成 技術的な構成は下記のようになっています。 フロントエンド: Flutter バックエンド: Firebase(ML Kit、Cloud Firestore)、GCP(Cloud Vision API) このファッションチェックアプリは、別のイベントでも使う予定があり、iOSでも動かせるようにする必要があったため、クロスプラットフォームで開発できるFlutterを選択しました。 また、バックエンドもTensorFlow Liteで作成した学習済みモデルをFirebaseにアップロードすることで特別なAPIを作成することなく、SDK経由で簡単に使うことができるためFirebase ML Kitを選択しました。 Cloud Firestoreは検出された結果をログとして保存しています。 FlutterからFirebase ML Kitを使う Firebaseの導入 FirebaseはFlutterから使用することができます。 導入手順は こちら の公式ページをご参照ください。 Firebase ML KitをFlutterで使う Firebase ML KitをFlutterで使うための プラグイン もOSSで公開されています。 今回使用するFirebase ML Kit Custom Modelもこのプラグインに内包されています。このプラグインを使うと、AndroidとiOSを同時に一つのコードで動かすことができるので非常に便利です。 ただしCustom ModelをFlutterから使用する場合には一部状況で注意が必要です。今回はこのプラグインを使用せず実装しました。 FlutterからFirebase ML Kit Custom Modelを使うときの注意点 Custom ModelをFlutterから使用する場合、返却される型に注意が必要です。Dartではfloat型が存在しないので、Custom Modelからの結果がfloat型の場合うまく受け取れません。そこで、KotlinでFirebase ML Kitとのやりとり部分を書いてFlutter側に結果を返すコードを書きました。 実装 FlutterからKotlinのコードを実行する 次のように invokeMethod によってFlutterからKotlinのコードを呼び出し、その実行結果が result に返ってきます。 const platform = const MethodChannel( "firebaseCustomModel#run" ); try { final String result = await platform.invokeMethod( "getResult" , < String , dynamic > { 'imageFile' : cameraImage.path, 'gender' : widget.gender, }).catchError((err) { print( "エラーが発生しました" ); setState(() { _isError = true ; }); }); 次にKotlin側でFlutterからきたデータを受け取ります。 MethodChannel クラスを使ってコールバッククラスを設定することでFlutterから getResult という文字列でリクエストがくるとKotlin側で受け取れます。 override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith( this ) MethodChannel(flutterView, "firebaseCustomModel#run" ).setMethodCallHandler( object : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val camera = call.argument<String>( "imageFile" ) val bitmap = BitmapFactory.decodeFile(camera) val gender = call.argument< Int >( "gender" ) startCustomModel(bitmap, gender, result) } }) } Flutterから設定されたパラメータは下記のコードで取得しています。 val camera = call.argument<String>( "imageFile" ) val bitmap = BitmapFactory.decodeFile(camera) val gender = call.argument< Int >( "gender" ) Flutterからネイティブのコードを呼び出す方法については公式ページがあるので詳しくは こちら をご覧ください。 Firebase ML Kitからデータを取得する ここからはFirebase ML KitからCustom Modelをダウンロードし、結果を取得します。 下記のコードは Firebase ML Kitのドキュメント を参考に実装しています。 fun startCustomModel(bitmap: Bitmap, gender: Int ?, result: MethodChannel.Result) { var conditionsBuilder: FirebaseModelDownloadConditions.Builder = FirebaseModelDownloadConditions.Builder().requireWifi() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { conditionsBuilder = conditionsBuilder .requireCharging() .requireDeviceIdle() } val conditions = conditionsBuilder.build() val cloudSource = FirebaseCloudModelSource.Builder( "batai" ) .enableModelUpdates( true ) .setInitialDownloadConditions(conditions) .setUpdatesDownloadConditions(conditions) .build() FirebaseModelManager.getInstance().registerCloudModelSource(cloudSource) val modelName = if (gender == 0 ) { "wear_model" } else { "wear_model_women" } val options = FirebaseModelOptions.Builder() .setCloudModelName(modelName) .build() val firebaseInterpreter = FirebaseModelInterpreter.getInstance(options) val inputOutputOptions = FirebaseModelInputOutputOptions.Builder() .setInputFormat( 0 , FirebaseModelDataType.FLOAT32, intArrayOf( 1 , 224 , 224 , 3 )) .setOutputFormat( 0 , FirebaseModelDataType.FLOAT32, intArrayOf( 1 , 1 )) .build() val batchNum = 0 val input = Array( 1 ) { Array( 224 ) { Array( 224 ) { FloatArray( 3 ) } } } for (x in 0 .. 223 ) { for (y in 0 .. 223 ) { val pixel = bitmap.getPixel(x, y) input[batchNum][x][y][ 0 ] = ((Color.red(pixel) - 128 ) / 128 ).toFloat() input[batchNum][x][y][ 1 ] = ((Color.green(pixel) - 128 ) / 128 ).toFloat() input[batchNum][x][y][ 2 ] = ((Color.blue(pixel) - 128 ) / 128 ).toFloat() } } val inputs = FirebaseModelInputs.Builder() .add(input) // add() as many input arrays as your model requires .build() firebaseInterpreter !! .run(inputs, inputOutputOptions) .addOnFailureListener { e -> // Task failed with an exception // ... e.printStackTrace() }.continueWith { task -> val labelProbArray = task.result !! .getOutput<Array<FloatArray>>( 0 ) result.success(getTopLabels(labelProbArray)[ 0 ]) } } private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> { val data = labelProbArray[ 0 ][ 0 ] val list = ArrayList<String>() list.add( " $data " ) return list } 下記のコードでは、先ほどの説明した通りFlutterではfloat型を受け取れなかったので、KotlinでString型に変換してFlutter側に返却しています。 val labelProbArray = task.result !! .getOutput<Array<FloatArray>>( 0 ) result.success(getTopLabels(labelProbArray)[ 0 ]) private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> { val data = labelProbArray[ 0 ][ 0 ] val list = ArrayList<String>() list.add( " $data " ) return list } Cloud Vision APIを叩く 開発期間内では学習済みモデルの精度が高められるか不安があったので、Cloud Vision APIも叩いて加点することにしました。 Cloud Vision APIでは写真の情報から写っている画像に対して得られる情報を返すGCPのサービスの1つです。 cloud.google.com ファッションチェックアプリでは、Cloud Visionから「cool」や「fashion」などのファッションチェックとしてプラスになりそうなキーワードが検出されると加点しています。 加点するキーワードについてはCloud Firestoreで管理しています。 Cloud Vision APIを叩くコードは下記になります。 _requestCloudVision(File cameraImage, String result) async { String url = "https://vision.googleapis.com/v1/images:annotate" ; String apiKey = "api key" ; List < int > imageBytes = cameraImage.readAsBytesSync(); Map json = { "requests" : [ { "image" : { "content" : base64Encode(imageBytes)}, "features" : [ { "type" : "LABEL_DETECTION" , "maxResults" : 100 , "model" : "builtin/stable" } ], "imageContext" : { "languageHints" : [] } } ] }; Response response = await http.post(url + "?key=" + apiKey, body : jsonEncode(json), headers : { "Content-Type" : "application/json" }); var body = response.body; print(body); var bodyJson = jsonDecode(response.body); List < dynamic > responces = bodyJson[ "responses" ]; if (responces == null || responces.length == 0 ) { _showErrorDialog( "Label not found" ); return ; } Map < String , dynamic > labelAnnotations = responces[ 0 ]; if (labelAnnotations != null && labelAnnotations.length != 0 ) { List < LabelAnnotationModel > list = [] ; for ( dynamic label in labelAnnotations[ "labelAnnotations" ]) { LabelAnnotationModel model = LabelAnnotationModel.fromJson(label); list.add(model); } } } このコードではlistに検出されたラベル一覧が格納される方法になります。 結果はログとしてCloud Firestoreに保存 撮影した写真以外のデータはCloud Firestoreに保存していてリアルタイムでログを見れるようにするためのアプリも実装しました。 こちらのアプリでは、Flutterだけで実装しているため、AndroidでもiOSの動かすことができます。 ログとして保存したのは下記です。 男女ごとのスコア Cloud Visionから検出されるキーワード キーワードにヒットした時の加点数 Firebase ML Kitから返却される数値の平均点と、Cloud Vision APIからよく出力されるキーワードを監視していました。 開発当初、学習済みモデルの出来が悪く全員が同じ点数になることが危惧されたのでCloud Vision APIからキーワードで加点するように対策をしていましたが、結果的に危惧していた事態は起きず、この機能の出番はありませんでした。 まとめ 本記事ではファッションチェックアプリの仕組みと、Firebase ML Kit、 Cloud Vision APIの簡単な使い方について紹介させていただきました。 ZOZOテクノロジーズでは、技術でファッションを盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com www.wantedly.com
こんにちは。ZOZOテクノロジーズ開発部の田島です。 今時のシステム開発ではさまざまなツールを利用することが当たり前になっています。 そして各種ツールは日々新しいものが開発され、今まで当たり前だったものがレガシーなツールと呼ばれることも珍しくありません。 弊社では、GitHubやCircleCI・Slackなど様々なツールを利用しています。 私達のチームでもこれらのツールを利用していますが、それ以外にもGitBucketやJenkins・Redmineを独自で管理し利用していました。 今回ある理由からそれらのツールをSaaSへ移行しました。その経緯と移行手順を紹介します。 概要 開発支援サーバの紹介 利用しているGitBucket・Jenkins・Redmineは開発支援サーバと呼ばれる一台のEC2インスタンスの上で動作していました。 やったこと これらのツールを以下の図のように、「GitBucketをGitHub」・「JenkinsをCircleCI」・「RedmineをGitHub issue」へ移行しました。 ツール移行の経緯 なぜ開発支援サーバーが必要だったのか まず開発支援サーバーが必要だった理由を紹介します。以下のような理由から開発支援サーバーの運用を行っていました。 プロジェクト発足当時、全社的に利用するツールが統一されていなかった セキュリティのルールが定まっていなく、SaaSを利用することがためらわれていた 開発支援サーバを利用しているプロジェクトは外部委託しているものだったためツールをコントロールできなかった なぜ移行を行ったのか 次になぜツールの移行を行ったのかを紹介します。以下のような理由から開発支援サーバーのツールの移行を決意しました。 全社的に利用するツールの統一が行われ始めた プロジェクトをまるごと内製化するため自分たちでツールをコントロールできるようになった 同じような役割のツールが複数存在し、情報が分散されてしまっていた EC2に開発支援サーバーを立てているためデータのバックアップなどすべて自分たちでやる必要があった サーバーがダウンすると3つすべてのツールにアクセスできなくなる 最も大きな理由としては、全社的に利用するツールが統一され始めたこと。外部委託をしていたプロジェクトをまるごと内製化し自分たちでツールをコントロールできるようになったことが主な動機となりました。 移行先の選定 それぞれのツールの移行先として、様々なツールを検討しました。最終的にどのような理由から移行先のツールを決定したのかを紹介します。 GitHub GitBucketの移行先をGitHubにした理由は以下になります。 全社的にリモートリポジトリがGitHubに統一された 開発者全員が利用したことのあるツールである 権限管理などが柔軟 SaaSのため自分たちで管理の必要がない 基本的には社内でリモートリポジトリがGitHubに統一されたことが大きな要因になりました。 CircleCI Jenkinsの移行先をCircleCIにした理由は以下になります。 全社的にCircleCIが利用されている チームメンバーがCircleCIに慣れている 設定をコードで管理できる Jenkinsでやっていたことがすべて実現できる SaaSのため自分たちで管理の必要がない チームメンバーがCircleCIに慣れており、かつやりたいことを柔軟に実現できたことが採用の大きな要因になりました。 GitHub issue Redmineの移行先をGitHub issueにした理由は以下になります。 進行中のタスクと完了したタスクを分けることができる 新たにツールの導入が必要ない 検索ができる SaaSのため自分たちで管理の必要がない 全社的にドキュメントはConfluenceにチケット管理はJiraに統一しているので、はじめはどちらかに移行することを検討しました。そしてRedmineの移行先の条件としては進行中のタスクと完了したタスクを分けることのできるツールが好ましいと判断しました。Jiraはこの条件に当てはまりますが、Redmineでは一部の作業履歴などドキュメントのように利用していました。また、Jiraでは工数管理も同時に行っているため移行先として好ましくないと判断しました。そこで今までの作業履歴が参照できれば良い程度の要件だったため、GitHub issueが適当であると判断しました。 ツールの移行 移行手順 ツールの移行は以下の順番で行いました。 GitBucket Jenkins Redmine まずはじめに、GitBucketの移行を行いました。Jenkinsの移行先であるCircleCIではGitHubとの連携を考えていました。そこで先にリポジトリをGitHubを移行する必要がありました。次にJenkinsを移行します。Redmineはどちらのツールにも依存をしていないため最後に移行をしました。以下ではそれぞれのツールの移行手順を紹介します。 GitBucket to GitHub 最初にGitBucketからGitHubへの移行手順を紹介します。 今回移行が必要だったリポジトリは10個ほどだったので無理にスクリプトを作成したりツールを利用せず手作業で移行を行いました。 移行作業では「 Gitリポジトリの中身を、ブランチとタグも含めて別リポジトリにコピーする 」の記事を参考にさせていただきました。 手順は以下の通りです(1〜4は上の記事通りに進めました) GitBucketのリポジトリを手元にcloneする すべてのブランチ・タグを取得 リモートリポジトリにGitHubを追加する GitHubにコンテンツをpush GitBucket上で作成されていたPull RequestをGitHub上で作成 Jenkinsの向き先をGitHubへ変更する 1. GitBucketのリポジトリを手元にcloneする まず手元に移行が必要なGitBucket上のリポジトリをCloneします。 $ git clone GitBucketのリポジトリ 2. すべてのブランチ・タグを取得 次に全てのブランチ・タグを取得します。 $ git branch -r | grep -v " \- > " | grep -v master | while read remote; do git branch --track " ${remote # origin/ } " " $remote " ; done $ git fetch --all $ git pull --all 3. リモートリポジトリにGitHubを追加する リモートリポジトリにGitHubを追加します。 origin の部分はGitBucketと同じものにして上書きしても別にしても作業しやすいものを指定してください。 $ git remote set -url origin GitHubのリポジトリ 4. GitHubにコンテンツをpush GitHubにコンテンツをpushします。 $ git push --all origin $ git push --tags 5. GitBucket上で作成されていたPull RequestをGitHub上で作成 GitBucket上で作成されていたPullリクエストをGitHubで作成します。 こちらも、数が多くなかったので手動で行いました。 6. Jenkinsの向き先をGitHubへ変更する 利用しているJenkinsではGitBucketからコードをPullしてビルドを行っていました。 次の章でJenkinsをCircleCIに移行しますが、移行が完了するまでJenkinsのリポジトリの参照先をGitHubへ変更する必要があります。 以下がその手順です。 GitHub用のデプロイキーを作成 github用のデプロイキーをリポジトリの数だけ発行します。以下では github-repository1 、 github-repository2 リポジトリ例でのデプロイキー作成の例になります。 $ ssh-keygen -l -f github-repository1.pem $ ssh-keygen -l -f github-repository2.pem .ssh/configにHostを追加 ~/.ssh/configの設定を発行したデプロイキーの数だけ追加します。 Host github-repository1 User git Port 22 HostName github.com IdentityFile ~/.ssh/github-repository1.pem TCPKeepAlive yes IdentitiesOnly yes Host github-repository2 User git Port 22 HostName github.com IdentityFile ~/.ssh/github-repository2.pem TCPKeepAlive yes IdentitiesOnly yes GitHubへデプロイキーを配置 GitHubの各リポジトリのSettingsでデプロイキーを登録します。 Jenkinsの設定を変更 最後にJenkinsのプロジェクトの設定から、ソースコード管理の欄を変更します。 Repository URLには先ほど。 .ssh/config で設定したHost名を利用します。またCredentialsにはデプロイキーを発行したユーザーと紐付いたものを利用します。 それぞれの項目の例は以下のようになります。 RepositoryURL: git@github-repository1:user_name/repository_name Credentials: Jenkins Jenkins to CircleCI 次にJenkinsをCircleCIへ移行します。今回Jenkinsで扱っているプロジェクトはすべてMavenで管理されたJavaアプリケーションでした。 そこで、以下の手順で移行を行いました。 Jenkinsでやっていることを確認 JenkinsでやっていることをCicrcleCIで再現 CircleCIによってビルドされたファイルの動作確認 1. Jenkinsでやっていることを確認 まずはじめにJenkinsでやっていることを確認する必要があります。 Jenkinsの各Mavenプロジェクトの「設定」で何をやっているのか把握を行いました。 1. Mavenでやっていることを確認 Jenkinsで扱っていたプロジェクトはすべてMavenプロジェクトのためビルド項目の「ルートPOM」と、「ゴールとオプション」を調べます。これによりJenkinsで実行されるMavenのゴール・オプションがわかります。以下の例では、単純に mvn clean package を行っていることがわかります。 2. JDKのバージョン確認 JavaのバージョンをCircleCI移行時に合わせる必要があるので、ビルド時のJDKのバージョンを調べておく必要があります。以下の例では、「OpenJDK 1.8」が利用されていることがわかりました。 3. ビルド・トリガの確認 どのようなタイミングでビルドが行われるかを確認します。私達のプロジェクトでは以下のように、定期的にビルドを実行するような設定になっていました。 4. 成果物の確認 最後にJenkinsの実行によって生成される成果物を確認します。CircleCI上でも同じように成果物を出力する必要があるため調べておく必要があります。 2. JenkinsでやっていることをCicrcleCIで再現 次にJenkinsでやっている内容をCircleCIで再現します。以下がその例になります。やっている内容はコメントをご参照ください。 version : 2.1 jobs : build-job : # Jenkinsサーバーがcentos7だったため同じOSを利用する docker : - image : centos:7 user : root working_directory : ~/repo # Jenkinsサーバーと同じ環境変数をセット environment : LANG : ja_JP.UTF-8 LANGUAGE : ja_JP:ja TZ : Asia/Tokyo steps : - checkout - run : name : 'Setup' command : | cp /usr/share/zoneinfo/Japan /etc/localtime # OpenJDKとMavenをインストール # localtimeの設定 - run : name : 'Install Dependencies' command : | yum -y install java-1.8.0-openjdk yum -y install maven # 依存ライブラリをローカルにダウンロード # NOTE : mvn dependency:go-offline command has a bug. Refer to https://issues.apache.org/jira/browse/MDEP-516 # - run: # name: 'Install dependency maven' # command: mvn dependency:go-offline # テストを実行 - run : name : 'Test' command : mvn clean test # パッケージング - run : name : 'Package' command : mvn package -Dmaven.test.skip=true # 成果物をCircleCI上の画面からダウンロードできるようにする - run : name : 'Set Artifacts' command : | mkdir /tmp/artifacts cp artifact.jar /tmp/artifacts - store_artifacts : path : /tmp/artifacts workflows : version : 2 build-deploy : jobs : - build-job Jenkinsでは mvn clean package のみを行っていました。しかし、CircleCIでは失敗した場合にどのタイミングで失敗したのかがわかりやすいようにテストとパッケージングを別々にしました。また、依存ライブラリのインストールも別にしたかったのですが問題が生じて断念しました。1つのプロジェクトで複数のMavenプロジェクトに分けている場合 mvn dependency:go-offline で失敗してしまいます。詳細は こちらのissue をご参照ください。 3. CIによってビルドされたファイルの動作確認 JenkinsでビルドしたjarとCircleCIでビルドしたjarを100%同じものにすることはできませんでした。そこで動作確認を開発環境やステージング環境できちんと検証する必要があります。検証ができたものから順次本番のjarをCircleCIでビルドされたものに切り替えて行きます。 Redmine to GitHub issue 最後にRedmineをGitHub issueへ移行する方法を紹介します。手順は以下のようになります。 移行のためのツールを探す RedmineとGithubの設定をする 移行ツールをfetchして自分好みにカスタマイズ redmine2githubを実行 1. 移行のためのツールを探す Redmineのチケットは300ほどあったため、流石にこれを手作業で移行すると日がくれてしまいます。そこで、これに関しては自動化することを検討しました。 今どきRedmineからどこかに移行するというニッチなツールが無く、最初は自分で作ろうと思いました。しかし、redmine2githubという素晴らしいツールのおかげで最小限の力で移行を実現できました。 以下がredmineからGitHubへ移行するためのツールで、Ahmy YulrizkaさんがGitHubで公開しています。利用するタイミングではライセンスが書かれていなかったのですが、事情を説明したところ丁寧にライセンスの付与をしていただくことができました。ちなみにライセンスはMITライセンスです。 https://github.com/yulrizka/redmine2github このツールでできないこと 以下のことはredmine2githubでは実現できません。しかし、今回の要件では作業履歴等が参照できれば良いという程度の要件だったためこのツールを利用することにしました。 ファイルの移行 Redmine上の他のチケットへのリンクの参照をGitHub issueに合わせて変換を行う 2. RedmineとGithubの設定をする redmine2githubを利用するために、Redmine及びGitHubの設定を行う必要があります。 Redmine 管理 > 認証のページから「RESTによるWebサービスを有効にする」 管理画面の承認ページから「RESTによるWebサービスを有効にする」を有効にします。 個人設定のAPIアクセスキーをメモ 個人設定の画面からAPIアクセスキーをメモします。これはredmine2github実行時に指定する必要があります。 csvをダウンロード チケット一覧のページで必要なチケットだけ絞り込んだら、以下の画面右下のCSVを選択しチケットの一覧をダウンロードします。 github GitHub側の設定はissueを作成できる権限のあるユーザーの作成が必要です。 各個人のユーザーでもかいまいませんが、そのユーザーとしてissueが作成されます。 3. 移行ツールをcloneして自分好みにカスタマイズ Redmineを日本語で利用している場合は、以下のようにカスタマイズしないとカテゴリや題名などを取得できません。 https://github.com/katsuyan-stt/redmine2github/pull/2/files#diff-222c979e694bb42a7f8468f67bbaddf0R111 redmine2githubは1ファイルのRubyスクリプトでできており、簡単にカスタマイズが可能です。自分のRedmine環境に合わせてカスタマイズして利用してください。 4. 実行 最後にredmine2github.rbを実行します。 オプションはredmine2githubの README を参照してください。 ruby redmine2github.rb リポジトリのユーザ/組織 リポジトリ名 ダウンロードしたCSVのパス -e 上の3でメモしたAPIキー,RedmineのURL -m -s -v -c 完了 移行したことによる意外なメリット ツールの移行によってEC2の料金や管理コストが減ることなどは考えていましたが、他にも以下のようなメリットがありました。 コードや問題の把握が加速した コードレビューがより活発になった CIツールの移行によってプロジェクトの理解が深まった コード管理やチケット管理を普段使い慣れているGitHubに移行したことにより、コードや問題の把握が加速しました。また、GitHubにコード管理が統一されたことによってコードレビューがより活発になりました。そして一番のメリットは、CIツールの移行によりプロジェクトの理解が深まったことです。CIツールの移行にはプロジェクトのビルドだけでなく、アプリケーションにもある程度詳しくなる必要があります。これにより、プロジェクトの理解を深めることができました。 今後の展望 以上のようにツール移行によりさまざまなメリットがありました。しかし、まだまだ改善の余地があり将来的には以下の実現を考えています。 CircleCIを利用した継続的デプロイメント(CD)の実現 今回CIをJenkinsからCircleCIに移行しましたが、Jenkinsでやっていたことを移行したのみで継続的デプロイメントの実現はできませんでした。もともとCDをしようという動きがないプロジェクトだったため簡単には実現できません。しかし、よりリリースのサイクルを早めユーザーに価値を提供できるようCDの実現を試みています。 まとめ 本記事ではEC2で管理していたツールをSaaSへ移行したことについて紹介しました。RedmineからGitHub issueへの移行など少しニッチな内容も含まれていましたが、少しでもお役に立てれば幸いです。 ツールの改善は直接ユーザーに価値を提供することはできません。しかし、チームや会社全体の作業効率やサービスの品質を高めるなど間接的にユーザーへの価値提供を加速させることができます。弊社では、チームや会社全体の業務の仕組み化を自ら進んで考えることができるエンジニアを募集しています。興味がある方は以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは。 最近愛猫にトイレの出待ちをされるようになった、品質管理部エンジニアリングチームの高橋です。 品質管理部ではアプリの自動テストを主に担当しております。 本記事はAI(Artificial Intelligence, 人工知能)を活用したテスト自動化の奮闘記となっております。 内容的にはお世辞にも先進的と言えるものではありませんが、是非あたたかい目で見て頂けると幸いです。 AI時代におけるソフトウェアテスティング 言うまでもなく、今やAIは身近な存在となっています。 ソフトウェアテスティング業界も例外ではなく、既にAIを用いたテストツールやサービスが公開されている状況です。そしてその流れは今後更に大きくなり、AIによる自動テストが当たり前の時代がやってくると予想されます。 私達もその波に乗り遅れないよう、自動テストにAIを導入することを目標に掲げ、AIの調査・検討を開始しました。 現在、品質管理部では、モバイルアプリの回帰テストを全て自動で行なっています。 iOS/Androidそれぞれのテストフレームワークを用いて、対象アプリ毎にテストスクリプトを実装し、日々メンテナンスを行なっております。 スクリプト形式の自動化の為、コーディング通りにテストが行われる単純なものではありますが、リリース前の速やかな動作確認&リグレッションテストに一役買っています。 そしてここにAIの力が加われば、伝統的なスクリプト形式のテスト自動化の枠を超えることができるのではと考えました。 画像認識AIの活用 現在のスクリプト形式自動テストの悩ましい点の1つに、「 人間の認知・判断 」が必要な項目は自動化を行うことができないということが挙げられます。 例えば、「キッズ」で絞り込んだ際に、「キッズの画像」が表示されていることを確認するテストです。このような人間にしか判断できない項目はテスターが手動で確認する必要がありました。しかし既にディープラーニングによる画像認識の精度は十分に人間に追いついています。よってまずはAIの得意分野である、画像認識を利用した自動テストを実装することに決定しました。 また現在のスクリプト形式のコードはアプリのUIヒエラルキーを1階層ずつ辿っていくような書き方であり、自然とコードが長くなってしまいがちです。 WEARのUIヒエラルキー 画像認識を用いて「この画像を探してタップする」という書き方で統一できれば、1つ1つのテストコードがよりシンプルになり、メンテナンスがしやすくなる可能性もあります。 WEAR-画像認識 WEAR画面上の投稿コーディネートを認識させる AIのライブラリはTensorFlowを利用してみることにしました。 オープンソース/ライブラリが豊富/知識さえつけば自由度が高いという魅力があるらしく、公式チュートリアルも充実している為、こちらを採用することにしました。 https://www.tensorflow.org/ 本来AIが物体認識を行うには機械学習でモデルを学習させるプロセスが必要になりますが、TensorFlowは学習済みのライブラリを提供しています。WEARアプリは"人間"が写っているコーディネート画像が表示されている為、今回は物体認識の学習済モデルを利用します。 1.TensorFlow実行環境の構築 まずTensorFlow Object Detection APIのインストールが必要ですが、ここでは詳細手順は省略します。下記の手順通りに実行すれば、問題なくインストールできると思います。 https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md 次に、学習済のTensorFlow Object Detectionモデルをダウンロードしておく必要があります。今回は下記の物体検出モデルを使用しています。 http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2017_11_17.tar.gz その他の物体検出モデルは、次のURLからダウンロードできます。 https://github.com/tensorflow/models 2.モデルダウンロード、メモリへロード ここからは、実際のテストコードとなります。 WebCameraに写っている物を認識するサンプルコードを参考に致しました。 https://github.com/tensorflow/models/tree/master/research/object_detection # ダウンロード&保存済みのモデルとラベル・マップのパス設定 PATH_TO_CKPT = 'ssd_inception_v2_coco_2017_11_17' + '/frozen_inference_graph.pb' PATH_TO_LABELS = os.path.join( 'data' , 'mscoco_label_map.pbtxt' ) # モデルをメモリへロード / 下記、TensorFlowチュートリアルより detection_graph = tf.Graph() with detection_graph.as_default(): od_graph_def = tf.GraphDef() with tf.gfile.GFile(PATH_TO_CKPT, 'rb' ) as fid: serialized_graph = fid.read() od_graph_def.ParseFromString(serialized_graph) tf.import_graph_def(od_graph_def, name= '' ) 3.ラベル、マップをロード # 下記、TensorFlowチュートリアルより NUM_CLASSES = 90 label_map = label_map_util.load_labelmap(PATH_TO_LABELS) categories = label_map_util.convert_label_map_to_categories( label_map, max_num_classes=NUM_CLASSES, use_display_name= True ) category_index = label_map_util.create_category_index(categories) 4.テスト対象のAndroid WEARアプリを起動 # am startコマンドでMainActivity起動 subprocess.Popen( "adb shell am start -n com.starttoday.android.wear/.main.MainActivity" , shell= True , stdin=subprocess.PIPE, stdout=subprocess.PIPE).wait() 5.画像認識画面取得 # adbコマンドでscreenshot実行 TEST_IMAGE_PATH = <ホストPC保存先+ファイル名> subprocess.Popen( "adb shell screencap -p /sdcard/image.jpg" , stdout=subprocess.PIPE).wait() subprocess.Popen( "adb pull /sdcard/image.jpg TEST_IMAGE_PATH" , stdout=subprocess.PIPE).wait() subprocess.Popen( "adb shell rm /sdcard/image.jpg" , stdout=subprocess.PIPE).wait() 6.物体検出処理開始 # 分析対象画像の配列化 image = Image. open (TEST_IMAGE_PATH) image_np = load_image_into_numpy_array(image) image_np_expanded = np.expand_dims(image_np, axis= 0 ) # 1つの画像に対する物体検出 # run_inference_for_single_image()は、Tensorflow公式Tutorial提供関数 # 画像より検知した物体データがoutput_dictに保存 output_dict = run_inference_for_single_image(image_np, detection_graph) # run_inference_for_single_imageの処理内容は下記の通りである def run_inference_for_single_image (image, graph): with graph.as_default(): with tf.Session() as sess: # Get handles to input and output tensors ops = tf.get_default_graph().get_operations() all_tensor_names = {output.name for op in ops for output in op.outputs} tensor_dict = {} for key in [ 'num_detections' , 'detection_boxes' , 'detection_scores' , 'detection_classes' , 'detection_masks' ]: tensor_name = key + ':0' if tensor_name in all_tensor_names: tensor_dict[key] = tf.get_default_graph().get_tensor_by_name( tensor_name) if 'detection_masks' in tensor_dict: # The following processing is only for single image detection_boxes = tf.squeeze(tensor_dict[ 'detection_boxes' ], [ 0 ]) detection_masks = tf.squeeze(tensor_dict[ 'detection_masks' ], [ 0 ]) # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size. real_num_detection = tf.cast(tensor_dict[ 'num_detections' ][ 0 ], tf.int32) detection_boxes = tf. slice (detection_boxes, [ 0 , 0 ], [real_num_detection, - 1 ]) detection_masks = tf. slice (detection_masks, [ 0 , 0 , 0 ], [real_num_detection, - 1 , - 1 ]) detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks( detection_masks, detection_boxes, image.shape[ 0 ], image.shape[ 1 ]) detection_masks_reframed = tf.cast( tf.greater(detection_masks_reframed, 0.5 ), tf.uint8) # Follow the convention by adding back the batch dimension tensor_dict[ 'detection_masks' ] = tf.expand_dims( detection_masks_reframed, 0 ) image_tensor = tf.get_default_graph().get_tensor_by_name( 'image_tensor:0' ) # Run inference output_dict = sess.run(tensor_dict, feed_dict={image_tensor: np.expand_dims(image, 0 )}) # all outputs are float32 numpy arrays, so convert types as appropriate output_dict[ 'num_detections' ] = int (output_dict[ 'num_detections' ][ 0 ]) output_dict[ 'detection_classes' ] = output_dict[ 'detection_classes' ][ 0 ].astype(np.uint8) output_dict[ 'detection_boxes' ] = output_dict[ 'detection_boxes' ][ 0 ] output_dict[ 'detection_scores' ] = output_dict[ 'detection_scores' ][ 0 ] if 'detection_masks' in output_dict: output_dict[ 'detection_masks' ] = output_dict[ 'detection_masks' ][ 0 ] return output_dict 7.物体検出結果の解釈 output_dictの出力結果の項目は次のようになっており、検出物体の詳細情報が保存されています。 - class 認識結果 - prediction 検出精度 - boundingbox 検出された座標位置 8.検出結果の判定と画面操作コード実行 # output_dictに保存している検出結果の判定処理を開始 for i in range ( len (output_dict[ 'detection_boxes' ])): # class_name = 検出物体名(クラス名、例:person, car, bag, cellphone,,,) # accuracy = 検出物体の精度(例:"person"クラスである確率がXX%) class_name = category_index[output_dict[ 'detection_classes' ][i]][ 'name' ] accuracy = output_dict[ 'detection_scores' ][i] # 物体検出の精度が70%未満であれば何もしない if accuracy < 0.7 : break # 検出物体クラスが'人(person)'の場合のみ処理実行 if class_name != 'person' : break else : # 分析画像の横縦長さ:adbコマンドで検出物体のxy座標計算時に利用 width = image_np.shape[ 1 ] # Number of columns height = image_np.shape[ 0 ] # number of rows # 該当投稿の座標を指定しタップ実行 # コマンドの例:adb shell input touchscreen tap x座標 y座標 print ( "詳細画面へ移動(クリック)" ) xPosition = int (width * output_dict[ 'detection_boxes' ][i][ 1 ])+(( int (width * output_dict[ 'detection_boxes' ][i][ 3 ])- int (width * output_dict[ 'detection_boxes' ][i][ 1 ]))/ 2 ) yPosition = int (height * output_dict[ 'detection_boxes' ][i][ 0 ])+(( int (height * output_dict[ 'detection_boxes' ][i][ 2 ])- int (height * output_dict[ 'detection_boxes' ][i][ 0 ]))/ 2 ) subprocess.Popen([ 'adb' , 'shell' , 'input' , 'touchscreen' , 'tap' , str (xPosition), str (yPosition)], stdout=subprocess.PIPE).wait() # 投稿をクリック後、正常に画面遷移が行われ、コーデ詳細画面に移動しているかの確認 # WEARアプリのコーディネート詳細画面Activity名:DetailSnapActivity out = check_output([ 'adb' , 'shell' , 'dumpsys' , 'activity' , '|' , 'grep' , 'mResumedActivity' ]) if b 'DetailSnapActivity' in out: print ( "OK! 詳細画面への正常遷移" ) print ( "前の画面へ移動(キーイベントで移動、KEYCODE_BACK=4)" ) subprocess.Popen( "adb shell input keyevent 4" , shell= True , stdin=subprocess.PIPE, stdout=subprocess.PIPE).wait() time.sleep( 2 ) else : print ( "NG! 詳細画面への遷移失敗" ) 実行すると…しっかりコーディネート画像をタップしてくれました! 画像認識 コーディネート画像タップ 今後の展望 今回は学習済モデルを使用した為、予想よりも短い時間で実装までたどり着くことができました(もちろん四苦八苦しましたが)。 このチャレンジによって少しずつTestingにおける画像認識AIの活用性が見えてきた気がします。 今後は以下のような人依存のテストパターンもAIを利用して行ってみたいと考えています。 ・投稿コーディネートが性別と一致しているか ・投稿コーディネートが本当にファッション関連投稿なのか ・投稿コーディネートが該当タグ(帽子、ブランド名等)に一致しているか また将来的には、弊社アプリ向けにモデルをトレーニングしてテストを実行してみたいと思います。 以上が、AI-assistedの第一歩としてTensorFlowの学習済モデルを使用してみた体験談です。 AI門外漢/文系出身の自分としては、機械学習に関する知識が全く足りていないことを痛感しました。理想が実現するのは近い将来ではなさそうですが、日々精進していきたいと思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは。iOS担当の荒井です。 今回はiOSチームで構築しているCI/CDのWorkflowについて紹介します。 CI/CD環境 はじめに、ZOZOテクノロジーズのiOSチームがどのようなCI/CD環境を整えているかを簡単に説明します。ZOZOテクノロジーズではZOZOTOWNやWEAR、プライベートブランドZOZOなど様々なサービスを運営しています。プロダクトによりCI/CD環境は違うのですが、多くのプロダクトでBitrise + DeployGateという構成をとっています。今回お話するWorkflowもBitriseを例に紹介していきます。 Bitrise 多くのCIサービスが存在していますが、以下の理由でBitriseを選定しています。 導入時、利用目的に対して価格面が見合っていた Xcodeバージョンアップへの対応速度 日本市場に注力 日本市場に注力していくという話もあり、今後サポートに期待が持てます。 最近では 日本語の記事 も掲載されています。 DeployGate TestFlightやFabricを使っている方も多いと思いますが、弊社のiOSチームはDeployGateを選択しています。元々CI/CDの環境が整っていないプロダクトが多かったため、環境構築を進めるにあたり、会社としてAndroidチームで運用実績があるDeployGateにしました。プロダクトをまたいでテストを行うチームが存在するため、ツールは統一しようといった理由があります。 Workflow それではWorkflowの話に移ります。 WorkflowはStepと呼ばれる定義済みスクリプトをまとめたものです。例えば「Carthageコマンドを実行する」には「Carthage」というStepを設定します。BitriseではGUIで簡単にSetpを追加し目的に応じたWorkflowを組むことができます。今回は以下のようなフローを例に、自動化する際どのようなWorkflowを組んでいるかを紹介します。 エンジニアがGitHubにコードをPush GitHubへのPushをWebhook Workflowの実行 DeployGateへのアップロード Slackへ完了通知 よくある「エンジニアがコードを書いてGitHubにPushしたら、テストが実行され、アーカイブしてQAに配布する。終わったらSlackに通知する」というものです。実運用ではPushのたびに配布をしていたら頻度が高いため、PullRequestの生成タイミングなど、目的に応じてトリガーを設定しています。 上記のようなフローを構築する場合、Bitrise上で以下のようなWorkflowが考えられます。 SSH keyの登録 リポジトリのクローン ビルドキャッシュのダウンロード 証明書のインストール Carthageコマンドの実行 キャッシュの更新 スクリプトの実行 テストの実行 アーカイブ DeployGateへのアップロード Slack通知 しかし弊社ではこれらのStepのほとんどをfastlaneに任せています。 fastlane fastlaneはアプリの様々な作業を簡単に解決してくれるツールです。冒頭のフローを実装するのに必要となる「ビルドに必要な証明書類の管理」「DeployGateへのアップロード」などはすべてfastlaneで行なっています。fastlaneを使用すると以下のようなWorkflowになります。 SSH keyの登録 リポジトリのクローン ビルドキャッシュのダウンロード Carthageコマンドの実行 キャッシュの更新 スクリプトの実行 fastlaneの実行 fastlane以外のStepスクリプトを使用しない理由は、CIサービスの移行を容易にするためです。他のCIサービスに魅力的な機能が出たり、何かしらの理由でCIサービスを変更しなくてはいけない場合でもfastlaneに処理を寄せておけば移行がスムーズになります。Bitrise上ではCode Signingの設定もせず、出来ることは可能な限りfastlaneにしています。サンプルのフローを例にSlackへ通知するまでのfastlaneの実装を紹介します。 証明書のインストール iOSに関わる証明書などのファイルは暗号化されGitHubで管理しています。fastlaneではアクションという形で様々な機能が提供されており、証明書の管理はfastlaneの match というアクションを使って実装しています。今回は詳しく触れませんが、証明書の管理はGitHubにしておくと何かと便利です。まだすべてのプロダクトで使用しているわけではないので現在対応を進めています。 テスト テストには scan というアクションを使用します。 fastlaneの処理はFastfileという設定ファイルに記述して実装していきます。 lane :test do scan( scheme : " SCHEME-NAME " , clean : true , skip_slack : false ) end FastfileはRubyで記述していくことになりますが、ドキュメントも充実しており、Rubyに馴染みのないiOSエンジニアでも記述に困らないと思います。 Swiftでの記述 もサポートがあるため、こちらを試しても良いかもしれません。 アーカイブ アプリケーションのビルドサンプルです。ビルドには gym というアクションを使用しています。 private_lane :build do match # 証明書のダウンロード gym( scheme : " SCHEME-NAME " , configuration : " InHouse " , clean : true , export_method : " enterprise " ) end DeployGateへのアップロード DeployGateへのアップロードもfastlaneです。私たちはDeployGateを使用していますが、TestflightやFabricなどのアクションも揃っています。 private_lane :deploy_gate do | options | target = options[ :release ] ? " release " : " development " deploygate( api_token : ENV [ " DEPLOYGATE_API_TOKEN " ], user : " DEPLOYGATE-USER " , ipa : " ./ #{ target } .ipa " , message : last_git_commit[ :message ] ) end Slack通知 全員が業務でSlackを使用しているため、何かの処理が終わった場合にはSlackで通知するように組んでいます。プロダクトに影響しない処理はサービス間で同じ設定を使うこともあります。 手元のテストでSlack通知が行われないように、CI上のみで通知をすることが多いです。 before_all do ENV [ " SLACK_URL " ] = " WEBHOOKURL " end after_all do | lane | if is_ci? slack( message : " Test Successfully " ) end end error do | lane, exception | if is_ci? slack( message : exception.message, success : false ) end end Bitriseに設定するlane Bitriseに設定するlaneを作成します。 lane :deliver_qa do build deploy_gate( release : false ) end 今回はビルドとデプロイのみにしていますが、 他にもビルドバージョンやGitタグなどもfastlaneで自動化できます。 fastlane運用をしてみての利点 すでにCircleCIからBitriseに移行したサービスがありますが、基本的にfastlaneで書いてあったため移行はとてもスムーズでした。CI上の細かな設定を確認せずとも、fastlaneの設定ファイルであるFastfileを確認すれば処理の流れが把握出来るのも利点だと思います。また、fastlaneによるコードでの記述を行なっていれば、プロダクトをまたいで設定ファイルを簡単に共有できるメリットもあります。CI上でGUIによるWorkflow構築は簡単ですが、コードで記述していくメリットも十分あるように感じています。 まとめ 今回はiOSチームで構築しているWorkflowについて紹介しました。CI/CD環境は随時改善を進めており、どのチームも自動化を目指しています。まだまだ改善の余地があり今後も注力していきたいと思います。CI/CDに興味があり、自動化が好きな方下記よりご応募お待ちしております。 www.wantedly.com
ZOZO研究所の後藤です。本記事ではGoogle Cloud TPUを使った計量学習の高速化の事例を紹介します。 はじめに 深層学習を用いたプロダクトを開発・運用する上で、モデルの学習にかかる膨大な時間はボトルネックの1つです。 ファッションにおける深層学習を用いた画像認識技術にも同じことが言えます。 今回はファッションの分野において定番のタスクであるStreet2shopの課題設定に対し、Google Cloud TPUを用いて計量学習の高速化を試みます。 Street2shopは、スナップ画像から商品部分を切り出す物体検出のパートと、切り出した画像と類似した商品を検索するクロスドメイン画像検索のパートに分けられます。 今回の取り組みでは、後者のパートで利用する画像間の距離を測るためのモデルの学習の高速化を行います。 目次 はじめに 目次 Google Cloud TPUとは タスク Street2shop 計量学習 学習データ 学習 プロファイリング 各オペレーションとメモリ使用量の関係 各オペレーションとTPUリソースの関係 学習結果 計算にかかる料金 モデルの活用 Shop2shop トップス アウター ドレス ボトムス スカート シューズ Street2shop スナップ1 スナップ2 まとめ 最後に Google Cloud TPUとは Tensor Processing Unit(以下、TPU)は、機械学習で頻繁に行われる行列演算を高速に処理することを目的にGoogleが開発したハードウェアです。 CPUやGPUは様々なソフトウェアやアプリケーションに対応するための汎用プロセッサとして設計されている一方で、TPUは汎用性を犠牲にして行列演算に特化した設計がなされています。 そのため、行列演算を多用する機械学習の計算にTPUを用いることによって、モデルの学習や推論にかかる時間を大幅に短縮することが可能になります。 TPUがなぜ高速に演算できるのかを理解するには、公式のブログ記事が詳しいです。 cloudplatform-jp.googleblog.com 速いことは良いことです。 学習速度が速いと、学習が収束したモデルをより短時間で獲得できるようになります。 また、限られた時間の中での試行錯誤の回数を増やすことができるため、ハイパーパラメータチューニングが捗り、最終的に得られるモデルがより良いものになる可能性が高くなります。Cloud TPUはTPUをクラウド上のインスタンスとして利用できるようにしたサービスです。TPUの強力な演算能力を時間単位で誰でも利用できます。 注意点として、TPUは設計上、得意な演算が限られるため、あらゆるモデルを高速化できるわけではありません。 適切な計算環境の選び方に関しては、以下の記事が参考になります。 cloud.google.com 例えば、以下の4つの項目に当てはまる場合、TPUによる高速化が達成可能かもしれません。 ほとんどの計算が行列計算である 学習ループにカスタムTensorFlow operationsが含まれない 学習に数週間から数ヶ月かかる 大きなバッチサイズで学習する 一方で、TPUを使うのに向いていない場合もあります。 頻繁なブランチングや要素ごとの積が多いプログラム スパースなメモリアクセス 高精度演算 カスタムTensorFlow operationを含むネットワーク タスク Street2shop Street2shopはストリートで撮影されたスナップ写真から、商品部分を検出して、ショップの商品と対応づけるタスクです。 ECサイトの商品検索の文脈においてこのタスクを応用すると、言葉では表現しにくい直感的な検索を画像を使って行うことができます。 弊社では過去に、この問題設定を解決するシステムの解説をブログに書いています。 techblog.zozo.com 計量学習 写り方の異なる同一商品の画像や、見た目が似ている商品を検索するには、画像同士の距離や類似度が適切に測れる空間が必要です。画像をそのような空間に埋め込むモデルを獲得するタスクが計量学習です。今回は損失関数にN-pair loss、ベースとなるネットワークにResNet-50、最適化の手法にAdam、空間の次元数は256を採用しています。問題設定の詳細な解説は過去のテックブログを参考にしてください。 techblog.zozo.com 実装のフレームワークにはTensorFlowを用います。 TensorFlowのTPUEstimator APIを利用して、CPU、GPU、TPU全てのチップで動作する汎用的なコードが得られます。 Googleが用意しているリファレンス実装を元に、自前のタスクに関する部分を書き換えることでTPUを使って動作するコードを得られます。 cloud.google.com 学習データ 今回の計量学習のために、商品画像と着用時の画像のペアを120万組ほど用意しました。 ストリートで取られたスナップ画像に対して商品検出器をかけ、商品部分を切り出しておきます。今回対象としたカテゴリはトップス、アウター、ドレス、パンツ、スカート、シューズの6種類で、女性ファッションのみに絞りました。 学習 プロファイリング Cloud TPUによる学習を高速化するためには、速度のボトルネックとなっている部分を特定して、適宜チューニングしていくという作業が必要になります。その際、cloud_tpu_profilerというツールを利用すると、ボトルネックの特定が容易になります。この辺りの作業は、一度Brennan Saetaさんによる実演を見ておくと理解がスムーズかと思います。 www.youtube.com cloud.google.com cloud_tpu_profilerを実行すると、TensorBoardでプロファイリングの結果が閲覧できるようになります。 サマリー画面は以下のようなものです。 この画面の3段目「Recommendation for Next Steps」に、学習のパフォーマンスを上げるためのアドバイスが書かれています。 summary TPUの学習速度に対してデータのインプットが追いついていない場合は、TPUに待ち時間が発生しますし、その逆も考えられます。プロファイラの各項目を深掘りすることで、どこが計算のボトルネックになっているのかを特定できます。 各オペレーションとメモリ使用量の関係 各オペレーションとTPUリソースの関係 今回の学習の場合、リファレンス実装をそのまま利用することにより高いパフォーマンスでCloud TPU動作させることができたため、特別なチューニングは行なっていません。 TPUはフィードフォワードとバックプロパゲーションが高速であり、データの読み込みがボトルネックになることがあります。 例えば、データの読み込み速度と学習速度のバランスをみてバッチサイズを変えるというチューニングが考えられます。 今回は、1バッチあたり1024枚と大きなミニバッチサイズを設定します。 学習結果 TPUにはコアが8つあるため、シングルGPUとの単純な比較は不公平ですが、現状の計算環境とTPUを使った場合の差分をはっきりさせるために敢えて比較します。 以下の表は、同じタスクを5日間ほど回した際の各評価値です。 学習の速度の観点では、P100で学習した場合、1秒あたり20組の画像を学習するのに対し、TPU v2では1100枚の画像を学習できました。 Cloud TPUを使うことにより、これまでに比べて55倍の学習の高速化が達成できたと言えるでしょう。 精度の観点では、同じ期間内にイテレーションをより多く回せたため、TPUで学習したモデルのAccuracyがGPUで学習したモデルのものより高い値になっています。 Unit Batch Size Top1 Accuracy (batch size 64) Top5 Accuracy (batch size 64) Train Loss Val Loss Elapsed Time / 1000 step N image / sec Nvidia P100 64 0.6511 0.9085 1.784 1.355 ~ 54 min ~ 19.75 image Cloud TPU v2 1024 0.8070 0.9573 1.155 0.7812 ~ 15.5 min ~ 1100 image 計算にかかる料金 2019年1月31日の時点でCloud TPU v2の単位時間あたりの料金は$4.50 USDです。 cloud.google.com 一方で、NVIDIA P100のGPUの料金は単位時間あたり1GPUあたり$1.46 USDです。 cloud.google.com 具体的な料金はCPUの数やメモリ、ディスクの容量にもよりますが、料金はせいぜい数倍の差です。その一方で、55倍の速さで学習ができたと考えると、Cloud TPUを使った方が最終的にかかる料金が低く抑えられるかもしれません。 モデルの活用 今回学習したモデルはインプットとして、「スナップ画像から商品部分を切り出した画像」と「商品画像」の両方を受け付け、256次元の数値をアウトプットします。 Shop2shopという単なる類似画像検索と、Street2shopという問題設定の両方を定性的に評価してみます。 評価時の入力と検索対象には、学習には用いていないバリデーション用データセットを利用しています。 Shop2shop 左側の赤枠で囲んだ画像を空間に埋め込み、ユークリッド距離が近い順に商品を選び出すという検索を行なっています。商品画像同士の類似度を直接学習したわけではありませんが、多くの場合で、形、色、柄、素材の観点で似ている商品が検索できています。以下、カテゴリ別の検索結果になります。 トップス 色、形はもちろんのこと、中央に写っているキャラクターも一致している ネック部分や袖の形の一致、花の刺繍部分の特徴も捉えることができている shop2shop_tops アウター 丈、色、カテゴリーが近いものを検索できている スカジャンの入力画像のポーズが独特だが、正面を向いたものが検索できている shop2shop_outer ドレス 丈、色、形、ネックの一致が見られる シースルーの素材感や柄の特徴も捉えられている shop2shop_dress ボトムス 色、形、柄の一致が見られる 例には載せていないが、ダメージジーンズのダメージ具合やダメージ位置の特徴の一致も検索できる shop2shop_bottoms スカート 丈、素材の一致が見られる デニムスカートのボタンの位置の特徴を押さえることが出来ている shop2shop_skirt シューズ 色、形、丈、素材感の一致が見られる 検索結果に着用画像と商品単体画像が混じり、その両方をある程度公平に評価できていることがわかる shop2shop_shoes Street2shop ストリートの環境で撮影された画像の商品部分を切り出して入力に使うタスクです。 自然光のもとで撮られたり、影が発生したりと複雑なシチュエーションが多くShop2shopよりも難しいタスクです。 スナップ1 トップスの文字部分の特徴を捉えている フォントの形も似ているものが選ばれている パンツは形が異なる商品も含まれる一方で、色、幅、丈が一致する商品も含まれる street2shop_1 スナップ2 ロゴが含まれたTシャツを検索することができる スカートのボタンのポイントは押さえているが、丈や形の面で間違えるパターンがある シューズはつま先とヒール部分を押さえられている street2shop_2 まとめ Cloud TPUを使うことで今回の計量学習に関しては、単体のGPU(Tesla P100)を使って学習した場合に比べて、55倍の学習速度の改善ができました。 55倍の速度改善は、1週間かかっていたモデルの学習が3時間で終わることを意味します。 Cloud TPUによる高速化が可能かどうかの見極めは必要になりますが、時間短縮のためにTensorFlow + Cloud TPUを使うことは一考に値するでしょう。 最後に 弊社では次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できる機械学習エンジニアを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは! ZOZOテクノロジーズ開発部の塩崎です。 この記事ではCloudFormationにDBのマスタパスワードなどの秘密情報を渡す3つの方法を説明いたします。 前提 我々のチームではAWSインフラリソースのプロビジョニングにCloudFormationを使用しています。 CloudFormationのテンプレートファイルはGitHubでバージョン管理されており、スタックに対するチェンジセットの作成をCircleCIから行っています。 このあたりの詳細は以下の記事に書かれているため、詳細はそちらをごらんください。 techblog.zozo.com 課題 このような方法でCloudFormationテンプレートを管理していましたが、それに伴う課題が生まれました。 DBのマスタパスワードなどの情報をどのようにして渡すかということです。 テンプレート内で使用するためのパラメーターは以下のような形式でテンプレートの中に埋め込み・参照できます。 Parameters : VPCCidrBlock : # パラメーターの定義 Type : String Default : '10.0.0.0/16' Resources : EC2VPC : # VPCを作成 Type : 'AWS::EC2::VPC' Properties : CidrBlock : !Ref VPCCidrBlock # こんな感じで参照。他の箇所からも!Refで参照できる。 しかし、この方法をそのまま使うと秘密情報をテンプレートにそのまま埋め込む必要が出てしまいます。 ちょうど以下のテンプレートのような形になります。 Parameters : RDSMasterUserPassword Type : String Default : 'Very_$ecret_Data' Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : !Ref RDSMasterUserPassword これではGitHubにアクセスできる人から秘密情報が丸見えです。 可能ならばこれらの情報はGitHubにコミットしたくありません。 さらにCircleCIなどの外部SaaSなどに秘密情報(もしくはどこかから秘密情報を取り出すことができる権限情報)を渡すことも避けたいです。 一方でCloudFormation側の事情を考えると、CloudFormationは何らかの方法で平文の秘密情報を知っておく必要があります。 CloudFormationが内部的にAWSのAPIを呼び出してリソースを作成するときにはこの情報が必要なためです。 これらの要件をまとめると、以下の図で示すようなCloudFormationに秘密情報を渡す「何らかの仕組み」が必要です。 解決策 上の図の「何らかの方法」に対応する解決策を3つ紹介いたします。 UsePreviousValue 最初に紹介するのは、CloudFormationにパラメーターを渡す時にUsePreviousValueをTrueにする方法です。 この方法で以前テンプレートに渡した値を引き継げます。 ですので、そのパラメーターを2回目以降使う時はテンプレートの中に値を埋め込む必要がなくなります。 Parameters : RDSMasterUserPassword Type : String Default : '' # ここは空でOK Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : !Ref RDSMasterUserPassword チェンジセットを作成する際には以下のようなJSONを作成し、awsコマンドを叩くことで以前の値を再利用することが出来ます。 [ { " ParameterKey ": " RDSMasterUserPassword ", " UsePreviousValue ": true } ] aws cloudformation create-change-set --stack-name =< stack name > --parameters = parameters.json 参考: https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-change-set.html カンの良い方ならば既にお気づきかもしれませんが、この方法には最初にパラメーターをセットするときにはどうするのかという問題があります。 最初にパラメーターをセットするためにはCloudFormationのテンプレートをAWSマネジメントコンソールもしくは手元のターミナルから反映する必要があります。 せっかくCircleCIを使ったCI/CDを構築しているのに、この部分だけが手動反映なのは残念な気持ちになります。 また、parameters.jsonを作成する必要があるのも少々面倒です。 Systems ManagerのSecureStringを使用する 次に紹介する方法はSystems ManagerのSecureStringを使用する方法です。 Systems ManagerはEC2インスタンスやオンプレのインスタンスを管理するためのものです。 その中に文字列を暗号化して保存するためのストレージがあるので、それを活用します。 Systems Managerに保存した文字列はDynamic Referenceという方法でCloudFormationから参照できます。 Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : '{{resolve:ssm-secure:rds-master-user-password:1}}' 最終行がDynamic Referenceでパラメーターを参照している部分です。 Dynamic Referenceの書式は以下に示すように : で区切られた4つのセクションに分かれています。 {{resolve:ssm-secure:parameter-name:version}} 前半の2つはSystems ManagerのSecureStringを使うことを指定しています。 後半の2つで保存されたパラメーターのキーとバージョンを指定しています。 最新のバージョンを指定するということはできず、必ず数字でバージョンを指定する必要があります。 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-ssm-secure-strings パラメーターをセットするためには以下のようにawsコマンドを叩くか、もしくはAWSマネジメントコンソールで行います。 aws ssm put-parameter --name rds-master-user-password --value ' Very-$ecret-Data ' --type SecureString Systems Managerがこの値を保存するときにはKey Management System(KMS)を用いた暗号化がなされます。 暗号化に使う鍵はデフォルトキーだけでなく、 --key-id パラメーターを使ってユーザーキーを指定することも可能です。 この方法を使うことでCloudFormationにパラメーターを渡すことが出来ますが、注意点もあります。 適用できるリソースの種類に制限があるということです。 現時点ではIAMユーザーのパスワードやRDSのマスタパスワードなどの11種類のリソースに対してのみこの方法を適用できます。 そのため、任意の箇所へ秘密情報を埋め込むということは出来ません。 たとえば、ECSで動いているアプリケーションに対して環境変数を通して秘密情報を渡すことを考えると以下のようなテンプレートになるかと思います。 AWS::ECS::TaskDefinitionリソースはSecureStringに対応していないため、SSM SecureStringを使った方法は適用できません。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : Environment : - Name : 'DB_PASSWORD' Value : 'Very-$ecret-Data' # ここには{{resolve:ssm-secure:rds-master-user-password:1}}と書けない Secrets Manager 最後に紹介する方法はSecrets Managerを使って秘密情報を渡す方法です。 この方法はSystem ManagerのSecureStringを使用する方法に似ていますが、少々異なる面もあります。 Secrets Managerを使った方法もDynamic Referenceを使用してパラメーターの参照を行うため、書式がかなり似ています。 Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : '{{resolve:secretsmanager:rds:SecretString:password}}' こちらの方法のDynamic Referenceの書式は以下に示すように : で区切られた5つのセクションに分かれています。 secret-id と json-key の2つを指定して秘密情報の取り出しを行います。 Secrets Managerのそれは連想配列型であるため、これら2つの情報を使って秘密情報を指定します。 {{resolve:secretsmanager:secret-id:SecretString:json-key}} 秘密情報をセットするためには以下のコマンドを使用します。 この例ではRDSのパスワードだけを設定していますが、RDSのユーザーやホスト名などの関連する情報を設定することも出来ます。 また、Secrets ManagerでもKMSのユーザーキーを用いた暗号化を行うことが出来ます。 aws secretsmanager put-secret-value --secret-id rds --secret-string ' {"password":"Very-$seret-Data"} ' Systems Managetとは異なり、Secrets Managerを使ったDynamic Referenceはどのリソースに対しても使用することが出来ます。 このような利便性がある一方で、うっかりと秘密情報を公開してしまう危険性も同時に持っているため注意が必要です。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : Environment : - Name : 'DB_PASSWORD' Value : '{{resolve:secretsmanager:rds:SecretString:password}}' 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-secretsmanager 3つの手法の比較 上で紹介した3つの方法の比較です。 UsePreviousValue SSM Secure String Secrets Manager CloudFormationのチェンジセット作成を自動で行えるか X O O パラメーターが暗号化されて保存されるか X O O KMSのユーザーキーを利用できるか X O O 任意のCloudFormationリソースに対して使用できるか O X O 他のAWSサービスとのインテグレート X O O この表を見てみると、Secrets Managerを使うのが現状のベストだと思います。 他のサービスとのインテグレートについて 今回紹介した方法はCloudFormationとSSM、CloudFormationとSecrets Managerという組み合わせでの使い勝手を見るという観点でした。 一方でSSMやSecrets ManagerはCloudFormation以外のサービスと組み合わせて使うことも出来ます。 その観点から見ると、現時点ではSSMのほうが他のサービスとのインテグレートへの対応が早いかと思います。 例えばElastic Container Service(ECS)とのインテグレートを考えます。 ECSでタスクを起動する時に秘密情報を環境変数にセットしてから起動したいとします。 2019年2月現在ではこのようなことが出来るのはSSM SecureStringのみです。 Secrets Managerの方が後発のサービスであることを考えると致し方ない気もしますが、SecretsManagerのこれからに期待したいです。 2/5追記 いつの間にかSecrets ManagerからECSに秘密情報が渡せるようになっていました。 気づいたらどんどん便利になっていくaws!! Secrets Managerの方が後発のサービスなので対応時期に少々の差(2018年11月と2019年1月)が出てしまっていますが、将来的にはこの差が縮まっていくことを期待しています。 よいまとめありがとうございます! 実は1月末の時点で ECS から Secrets Manager の値取れるようになってるので、ブログ記事にも反映して欲しいです〜 — ポジティブな Tori (@toricls) 2019年2月5日 参考: https://aws.amazon.com/jp/about-aws/whats-new/2018/11/aws-launches-secrets-support-for-amazon-elastic-container-servic/ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html#secrets-create-secret まとめ CloudFormationに秘密情報を渡す3つの方法をお伝えしました。 現時点ではSecrets Managerを使うのがベストだと思います。 我々Marketing AutomationチームではAWSのインフラ構成をCloudFormationで管理することによって、Infrastructure as Codeを実現しています。 最近ではGCPのインフラも増えてきたことからTerraformの使用も視野に入れて日々の開発運用を行っております。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
前書き こんにちは、スマートファクトリー向け制御ソフトウェア開発チームの高石( @ksk_taka )です。 本記事では、アパレル業界や製造業界など、CADを取り扱う業界で広く使われているdxfファイルを 一括で画像ファイルに変換する 手法について記載します。 dxfファイルとは そもそも dxfファイルとは何ぞや? という方のために簡単に説明をします。 dxfファイルは CAD間を仲介する中間ファイル として使うことを目的としたファイルです。 例えば機械設計をする際に3D-CADが利用されますが、よく使われる3D-CADソフトとして、以下のものがあります。 CATIA SolidWorks Creo Parametric これらのそれぞれのソフトで作られる図面データは 別々のファイル形式(拡張子) を持っており、基本的に互換性がありません。 アパレル業界で用いられるCADソフトも同様で、以下の様な異なるCADソフトにて生成される図面データには互換性がありません。 クレアコンポ AGMS このままでは一方のCADソフトで生成した図面データを、別のCADソフトを持った人が開こうとすると「開かない!」という状況になってしまいます。 そんなの困る! ということで、必要となるのが 中間ファイル という存在です。 dxfファイルは恐らく 現在最も広く使われている であろう中間ファイルで、 使用するCADソフトに関わらず開くことができる図面データ になります。 dxfファイルを用いることで、異なるCADソフトを利用している人同士でも、図形データのやり取りが可能になります。 ※但し、「各社独自のCADオブジェクト」などはdxfでは再現できません。やり取りの際には注意が必要です。 dxfファイルを画像化する目的 上記のように便利なdxfファイルですが、万能なわけではありません。 例えば以下のような状況では、もっと汎用的なデータ形式の方が望ましいと言えます。 CADソフトを持たない人に図面データを渡したい場合 Web上や社内のドキュメントファイルなどに図面(絵柄)を貼付したい場合 そのような状況に対応するため、dxfファイルに含まれる図面データを一括で画像化するソフトをGo言語で実装しました。 dxfファイルの構造 dxfファイルはテキスト形式のファイルです。その為、テキストエディタで簡単に内容を確認できます。 ファイル内のテキストは2行で1組となっており、1行目は グループコード 、2行目はグループコードに応じて 文字列 、 数値 などが入ります。 dxfファイルの例を以下に記載します。 0 SECTION //SECTION開始 2 BLOCKS //BLOCKS SECTION開始 0 BLOCK //1個目のBLOCKの開始 8 1 //1個目のBLOCKの階層 2 1_BlockName //1個目のBLOCKの名前 10 0 //1個目のBLOCKのX座標 20 0 //1個目のBLOCKのY座標 0 POLYLINE //1個目のBLOCK内の初めのENTITYデータ(POLYLINE:連続した頂点で描画される図形) ・ ・ ・ 0 LINE //1個目のBLOCK内の2個目のENTITYデータ(LINE:2点を結ぶ線分で描画される図形) ・ ・ ・ 0 ENDBLK //1個目のBLOCK項目の終了 0 BLOCK //2個目のBLOCK項目の開始 ・ ・ ・ 0 ENDBLK //n個目のBLOCK項目の終了 0 ENDSEC //BLOCKS SECTION終了 0 EOF //FILEの終了 今回はひとまず、使用頻度の高い POLYLINE と LINE の2つの図形を描画する機能を実装します。 実装方法 ここからは具体的な実装について記載していきます。 今回、ソフトウェアを実装する上で肝となるのは以下の機能です。 dxfファイルを1行ずつ読み取る機能 読み取ったデータを構造体に格納する機能 構造体の格納データに応じて画像を描画する機能 1つずつ見ていきましょう。 事前準備 まず、事前準備から。 目標の機能を実装するにあたり、Go言語のパッケージは以下のものを使用します。 import ( "bufio" "image" "io/ioutil" "os" "path/filepath" "regexp" "runtime" "strconv" "sync" "time" "github.com/llgcode/draw2d/draw2dimg" "image/color" "fmt" "golang.org/x/text/encoding/japanese" "golang.org/x/text/transform" ) 更に、読み取ったdxfファイルから各要素を格納していくための構造体を準備しておきましょう。 // Section is a top level group. type Section struct { Blocks []Block Entities []Entity Headers [][] string Tables [][] string } // Block is a second level group in Section. type Block struct { Name string LayerName string BlockType string X float64 Y float64 Entity []Entity } // Entity is a third level group in Section or Entities. type Entity struct { TYPE string Name string FULL [][] string } dxfファイルを1行ずつ読み取る まずはdxfファイルを1行ずつ読み取り、Sliceデータとして返す関数を実装します。 func getFileStream(inputpath string , fileName string ) (data [][][] string ) { var row [][] string var scangroup [][][] string input, err := os.Open(filepath.Join(inputpath, fileName)) if err != nil { // Openエラー処理 panic (err) } defer input.Close() scangroup = nil //dxfファイル ロード開始 sc := bufio.NewScanner(transform.NewReader(input, japanese.ShiftJIS.NewDecoder())) for i := 0 ; sc.Scan(); i++ { if err := sc.Err(); err != nil { // エラー処理 break } if i != 0 && sc.Text() == " 0" { scangroup = append (scangroup, row) //区切り文字で塊を作る row = [][] string {} //塊を作ったら初期化 } //2行で1要素分なので判別しやすいようにまとめてSlice化 gkey := sc.Text() sc.Scan() gvalue := sc.Text() row = append (row, [] string {gkey, gvalue}) } scangroup = append (scangroup, row) row = [][] string {} return scangroup } ファイルパス と ファイル名 の文字列が与えられると、該当ファイルを1行ずつ読み込む関数です。 上でお伝えした通り、dxfファイルは2行で1組となっているため、 1組分をまとめてSlice にしています。 データを構造体に格納する 続いて、読み取ったデータを各種構造体に格納する処理を実装していきましょう。 func makeSection(data [][][] string ) (sec Section) { var isBlocks bool var isEntities bool var blk Block var ent Entity var copyData [][][] string var copyGroup [][] string var copyRows [] string for _, g := range data { for _, r := range g { copyRows = make ([] string , 2 ) copy (copyRows, r) copyGroup = append (copyGroup, copyRows) } copyData = append (copyData, copyGroup) copyGroup = [][] string {} } sec = Section{} for _, group := range copyData { if group[ 0 ][ 0 ] == " 0" { switch group[ 0 ][ 1 ] { case "SECTION" : //SECTIONの処理 for _, rows := range group { if rows[ 0 ] == " 2" { switch rows[ 1 ] { case "BLOCKS" : isBlocks = true isEntities = false case "ENTITIES" : isBlocks = false isEntities = true case "HEADER" : isBlocks = false isEntities = false sec.Headers = group case "TABLES" : isBlocks = false isEntities = false sec.Tables = group default : } } } case "VERTEX" , "LINE" , "POLYLINE" , "SEQEND" , "TEXT" : //図形データ ent = Entity{} ent.TYPE = group[ 0 ][ 1 ] ent.FULL = group if isBlocks { blk.Entity = append (blk.Entity, ent) } else if isEntities { sec.Entities = append (sec.Entities, ent) } case "BLOCK" : //画像は1BLOCKにつき1枚 blk = Block{} //BLOCKの処理 for _, rows := range group { switch rows[ 0 ] { case " 8" : blk.LayerName = rows[ 1 ] case " 2" : blk.Name = rows[ 1 ] //file名に使用 case " 70" : blk.BlockType = rows[ 1 ] case " 10" : blk.X, _ = strconv.ParseFloat(rows[ 1 ], 64 ) case " 20" : blk.Y, _ = strconv.ParseFloat(rows[ 1 ], 64 ) default : } } case "ENDBLK" : //BLOCKの終わりを示す sec.Blocks = append (sec.Blocks, blk) case "EOF" : //fileの終わりを示す default : } } } return sec } 先ほど作成したデータを入力として、各構造体にデータを格納していくコードです。 最終的に全てのSECTIONをまとめたものがSliceで返されます。 画像化する 続いて、構造体に格納されたデータを用いて画像を描画する機能を実装します。 func exportPNGperBlk(sec Section, dir string ) { const PrefixSEQEND = - 1 var vertexX, vertexY float64 var lstartX, lendX, lstartY, lendY float64 var maxX, maxY, minX, minY float64 var exportFlag bool var vertexs [][] float64 var lines [][] float64 var name string var i int exportFlag = false for _, bl := range sec.Blocks { //file名にはディレクトリ名とブロック名を使用する name = dir + "-" + bl.Name exportFlag = true for _, en := range bl.Entity { if en.TYPE == "SEQEND" { //線の切れ目 vertexs = append (vertexs, [] float64 {PrefixSEQEND, PrefixSEQEND}) continue } if en.TYPE == "VERTEX" { //POLYLINEで頂点を繋ぐ線分を描画する際に使用 for _, rows := range en.FULL { switch rows[ 0 ] { case " 10" : //頂点のX座標 vertexX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if vertexX > maxX { maxX = vertexX } if vertexX < minX { maxX = vertexX } case " 20" : //頂点のY座標 vertexY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if vertexY > maxY { maxY = vertexY } if vertexY < minY { minY = vertexY } } } //"vertexs"に、頂点のXY座標を格納 vertexs = append (vertexs, [] float64 {vertexX, vertexY}) } if en.TYPE == "LINE" { //LINEで線分描画する際に使用 for _, rows := range en.FULL { switch rows[ 0 ] { case " 10" : //開始地点のX座標 lstartX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lstartX > maxX { maxX = lstartX } if lstartX < minX { minX = lstartX } case " 11" : //開始地点のY座標 lendX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lendX > maxX { maxX = lendX } if lendX < minX { minX = lendX } case " 20" : //終了地点のX座標 lstartY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lstartY > maxY { maxY = lstartY } if lstartY < minY { minY = lstartY } case " 21" : //終了地点のY座標 lendY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lendY > maxY { maxY = lendY } if lendY < minY { minY = lendY } } } //"lines"に、開始-終了地点のXY座標を格納 lines = append (lines, [] float64 {lstartX, lstartY, lendX, lendY}) } } if exportFlag { //画像サイズがPartsごとに異なるので毎回準備する img := image.NewRGBA(image.Rect( int (minX), int (minY), int (maxX+ 1 ), int (maxY+ 1 ))) gc := draw2dimg.NewGraphicContext(img) gc.SetFillColor(color.White) gc.SetStrokeColor(color.RGBA{ 0 , 0 , 255 , 255 }) gc.Fill() //POLYLINE描画 for _, vertex := range vertexs {} i++ fmt.Println( len (vertexs), vertex, i) if vertex[ 0 ] == PrefixSEQEND && vertex[ 1 ] == PrefixSEQEND { if i < len (vertexs) { //線の切れ目では描画をせず座標移動だけ実施 gc.MoveTo(vertexs[i][ 0 ], maxY-vertexs[i][ 1 ]) } continue } //線を描画。画像とdxfでY座標の方向が反転する点に注意 gc.LineTo(vertex[ 0 ], maxY-vertex[ 1 ]) } //LINE描画 for _, line := range lines { gc.MoveTo(line[ 0 ], maxY-line[ 1 ]) gc.LineTo(line[ 2 ], maxY-line[ 3 ]) } gc.Stroke() gc.Close() //出力フォルダを作成する outputpath, err := filepath.Abs(filepath.Join( "." , "file" , "output" , dir)) if err != nil { panic (err) } os.Mkdir(outputpath, 0777 ) //出力フォルダを作成する outputpath, err = filepath.Abs(filepath.Join( "." , "file" , "output" , dir, "png" )) if err != nil { panic (err) } os.Mkdir(outputpath, 0777 ) //png画像として保存 draw2dimg.SaveToPngFile(filepath.Join(outputpath, name+ ".png" ), img) //各変数を初期化 exportFlag = false vertexs = [][] float64 {} lines = [][] float64 {} maxX = 0 maxY = 0 minX = 0 minY = 0 i = 0 } else { fmt.Println( "画像化対象のパーツが見つかりません。" , "パーツ名[" , name, "]" ) } } } 最後に、main関数を実装します。 func main() { // 正規表現を使って対象ファイルを設定 rep := regexp.MustCompile( "[A-Z]*[a-z]*[0-9]*.dxf" ) // Inputディレクトリがあるかどうか確認 inputparentpath, err := filepath.Abs( "./file/input/" ) if err != nil { panic (err) } dirs, err := ioutil.ReadDir(inputparentpath) if err != nil { panic (err) } var wg sync.WaitGroup cpus := runtime.NumCPU() // CPUの数 limit := make ( chan struct {}, cpus) // Inputディレクトリ配下の全ファイルを読み込み for _, dir := range dirs { dirName := dir.Name() if dir.IsDir() != true { continue } inputpath, err := filepath.Abs(filepath.Join(inputparentpath, dirName)) if err != nil { panic (err) } files, err := ioutil.ReadDir(inputpath) if err != nil { panic (err) } // 各ディレクトリ配下の全ファイルを読み込み for i, file := range files { fileName := file.Name() // dxfファイルが見つからない場合 if !rep.MatchString(fileName) { continue } if file.IsDir() { // ディレクトリはスキップ continue } // ファイル読み込み〜出力までは並列処理で実行 wg.Add( 1 ) go func (i int ) { defer wg.Done() limit <- struct {}{} fmt.Println( "処理開始" , "[" , i, "]" , dirName, fileName) //dxfファイル読み取り scangroup := getFileStream(inputpath, fileName) //データを構造体に格納 sec := makeSection(scangroup) //画像ファイル生成 exportPNGperBlk(sec, dirName) fmt.Println( "処理済" , "[" , i, "]" , dirName, fileName) <-limit }(i) wg.Wait() } } // 全ての処理が完了するまで待機 wg.Wait() fmt.Println( "全ての処理が完了しました。キーを押すと終了します。" ) fmt.Scanln() } 実行結果 実際に、冒頭で紹介したdxfファイル(デニムパターン)に対して本機能を適用してみました。 実行した結果出力された画像は以下の通り。 画像1: デニム左前身頃 画像2: デニム右前身頃 画像3: デニム左後身頃 画像4: デニム右後身頃 画像5: デニム後ポケット(左) その他、デニムパーツ多数出力(数が多いので省略) 想定通り、複数の図形データを持つdxfファイルを、画像に一括で変換できていることが確認できます。 最後に 今回は「POLYLINE」「LINE」という2つの図形に絞って画像化する機能を実装しました。 恐らく他にもdxfファイルに利用されている図形はあると思いますので、気になる方は是非続きを実装してみて下さい。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。一緒に楽しく仕事しましょう! www.wantedly.com
こんにちは!ZOZOテクノロジーズ開発部の池田( @yuji_ikepon )です。 本記事では、 ケアラベルデザイン発行を自動化した際に使用したパッケージの紹介と、プロトタイプ開発までのプロセスを紹介したいと思います。 ケアラベルとは ケアラベルとは上記の様な、繊維製品になくてはならない品質表示のことを指します。ケアラベルは、品質表示法の下に適切で明確な表示が義務づけられています。 プロトタイプ製作までの経緯 ケアラベルのデザインを行なっていた担当者から、「ケアラベルを自動的に発行できないだろうか」という相談が来ました。 担当者から聞いた仕様は以下の通りです。 エクセルのデータを元にアウトプット出来る事。 指定したフォントが使用できる事。 プログラミング未経験でも使える事。 Macだけでなく、Windowsでも動く事。 開発環境によらず実行できる事。 これらの条件を踏まえ、PDFでのデザイン出力を軸にプロトタイプを作成することにしました。 また、実行環境の事を考え、言語はGo言語を使用します。 使ったパッケージ、ライブラリ プロトタイプを実装した中で使った、Go言語のライブラリとソースコードの一例を以下に示します。 xlsx : https://github.com/tealeg/xlsx エクセルの任意のシートを読み込む // xlsx読み込み excel, err1 := xlsx.OpenFile(file) // xlsxが読み込めなかった場合 if err1 != nil { log.Printf("error: not open xlsx file.") return false } else { log.Printf("debug: success open xlsx file.") } var sheet *xlsx.Sheet = nil // シート名で該当のシートを検索 for k, _ := range excel.Sheet { searchSheet := excel.Sheet[k] if searchSheet.Name == "検索したいシート名" { log.Printf("debug: success open xlsx sheet.") sheet = searchSheet break } else { // 何もしない } } // 該当シートが見つからない場合 if sheet == nil { log.Printf("error: failur open sheet.") return false } // エクセルの値を参照 for rowKey, rowValue := range sheet.Rows { // このループではエクセルの1行ごとに処理ができる // 左から0番目のセルを参照できる cell := rowValue.Cells[0].Value } gopdf : https://github.com/signintech/gopdf pdfの作成 pdf := gopdf.GoPdf{} // rect作成 rect := gopdf.Rect{} rect.W = "pdfの幅" rect.H = "pdfの長さ" // gopdf作成 config := gopdf.Config{} config.PageSize = rect pdf.Start(config) fontPath := string("data/Font/" + data.FontPath + ".ttf") pdf.AddTTFFont(data.FontPath, fontPath) pdf.SetFont(data.FontPath, "", 2*13) pdf.AddPage() // 画像書き込み // 画像の場所 inputPath = "画像のパス" // 出力 size := gopdf.Rect{ H:"画像の高さ", W:"画像の幅", } pdf.Image(inputPath, "pdfのX座標", "pdfのY座標", &size) // テキスト書き出し // 座標を設定 pdf.SetX("pdfのX座標") pdf.SetY("pdfのY座標") // 出力 pdf.Cell(nil, "テキスト") 出来たデザイン 下記の様なデザインで出力する事ができました。 ※ 表示されている情報はあくまでテストの為の仮の表記です。 海外向けケアラベル 日本向けケアラベル(表面) 日本向けケアラベル(裏面) プロトタイプのデザイン自体はそれっぽく実装する事ができました。 しかし、使うフォントの種類によっては、表示される位置がずれてしまったり、改行の位置が揃わないといった問題がありました。 次回プロトタイプ改修の際にはこれらの問題点と合わせて、デザイン的な観点からも実装を考えられたらなと思います。 Go言語で書く上でつまずいた点 今回Go言語でプロトタイプを作成していく中で、以下の様な点で苦労しました。 パッケージ間の共有データをどう持つか エクセルからデータをインプット→PDF or PNGでアウトプットを行う際に、パッケージ間でデータの受け渡しが必要でした。 その為、データ共有のパッケージを作成し、そこに構造体を保持する事でデータを参照していました。 しかし、保持したいデータの中に構造体の配列を入れた際に、初期化のタイミングの違いにより、参照が上手くいかない場合があり苦労しました。 ライブラリの情報不足 Go言語のライブラリの中には、基本的な使い方しか表記されていないものが多く、自分でライブラリ内のコードを見ながら探っていくことが多かったです。 アウトプットするまで実装が正解か分からないこともあり、デバックに苦戦を強いられました。 最後に 今回はプロトタイプの作成のフローを紹介させて頂きましたが、この他にも色々な業務を行なっています。 制御系エンジニアに関しての詳しい情報は以下のリンクからご覧になれます。 https://tech.zozo.com/recruit/mid-career/detail29/ ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは、ZOZOテクノロジーズでプロジェクトマネージャーをしている 新井(@masamasaking) です。 最先端の小売業界のトレンドを体験できる全米最大のリテール向けのイベント「 NRF Big Show 」が今年も1月13日から開催されました。現地視察を通じて感じたことをレポートします。 この記事では気になった出展企業や全体を通して感じたことをご紹介します。 NRF Big Showとは 世界99カ国から機器やソリューションが800以上の出展。37,000人が参加するNRF(全米小売業協会)が行っている小売業界向けのイベントです。展示と並行して6つのステージで3日間セミナーやキーノートスピーチも行われ小売業界の今がここに集まっていきます。 気になった展示 AIがもはや当たり前かのように AIやMLを使ったオムニチャネル化、商品トレンドの分析、需要予測、ダイナミックプライシング、価格算出など顧客体験の向上や業務オペレーション改善だけでなく小売のいたるところにこれらの技術を使われるのは必須でした。オフライン上でのデータを収集およびユーザーごとにデータを蓄積し、足元の店舗の最適化からその先のダイナミックプライシングやディスプレイの見せ方の個別最適化の進んだ世界が近い将来くることが伺える。消費者が何を求めていて、どのタイミングで商品を並べるのか、どういった価格で・どういった情報を訴求するのか・・・・といった課題に対してAIやMLがこれでもかというくらい絡んでくる。これまでバズワードであった「AI」がいよいよ小売の現場に本格化したようです。 ディスプレイのリッチ化・インタラクティブ化 *1 これまで店頭は紙などのPOPが主流であったがデジタル化が進んでいる。情報をただ見せるだけであればオンラインのストアでもできるが、その先をいくインタラクティブな見せ方のソリューションが散見されました。 実際に商品を手に取るとすぐさまディスプレイに連動して、動画が流れ商品を訴求。またスマートミラーの出展もあり、消費者が試着をするたび鏡に商品情報が表示される。それだけにとどまらず近くにある商品で次はこれが似合うと思うのでいかがですか?とレコメンドまでしてくれます。 ディスプレイの表示内容の最適化と分析 *2 インテルが展示していたディスプレイが目を引きました。ただの大きいディスプレイだと思ったがよくみるとカメラが取り付けられている。そのカメラから見てるユーザーのデモグラを推測し、また視線の動きを追尾してどの商品・モデルに興味をもっているかが把握できる。また天候や曜日なども加味しそれらの情報を逐一分析し、商品ごとのスコアリングがされている。店舗側は推すべき商品が把握出来るのに加えて、ユーザーはついつい見てしまうデジタルサイネージができるかもしれない。また画像認識とオンラインの・オフライン合わせたユーザーデータの突合が進めばユーザーごとに見せる商品の出し分けができる未来がくるかもしれないです。 ドローンによる商品確認 店舗でドローンを飛ばして陳列棚を周遊しながら画像解析をして、商品の在庫確認ができる。商品が従来の位置になかったり・正面を向いていなくても認識ができます。それも瞬時に。例えば、高さ2m・長さ5mの商品棚であれば1分も掛からずに集計ができます。小さな店舗であれば不要かもしれないがショッピングモールやスーパーといった大型店舗になればなるほどこれまで人が随時手作業で在庫のチェックしていたものがドローンを活用することであっという間にかつ正確にできる未来がすぐそこまで来ています。 イノベーションラボ/スタートアップゾーン またスタートアップ系の企業・サービスが集まる2018年から新設されたエリアからもいくつか気になったものをご紹介します。 Texel 身体計測をする技術を持っているロシアの企業。3D化だけでなく、手足の長さなどの計測も可能。アプリ版(未リリース)もあり被写体を中心にアプリ(カメラ)を周囲360度を撮影することで計測が可能です。今後実用レベルまで開発が進みより多くの人が自分の正確な身体データをもてたらオンラインでもより洋服やアクセサリーが買いやすくなるので今後の開発・普及が楽しみです。 americhip プロペラを使ったホログラムを表示する技術をもった会社。多少音が気になるが投影物がVRゴーグルを使わなくても目の前に立体的に見えます。複数のプロペラ使えば大きな表示も可能です。 caper Amazon GOのように天井のいたるところにカメラやセンサーを設置し無人レジを実現するのに対して、こちらはスマートカートと呼ばれるカメラつきのカートを用いることでショッピング自動化を模索しています。導入コストが前述のものより低いため差別化できる。またショッピングカートに収集されたショッピングデータを陳列方式の改善や品揃えに活かす。カートに商品を入れずに売り場から立ち去った場合にどうなるか気になるものの違うアプローチでショッピング自動化を目指していて、要チェックです。 まとめ エクスポで目立ったのが「AI/ML」をベースにしたテクノロジーの活用でした。オフラインにある情報を店頭に設置したカメラやセンサーで画像解析等を通じてデータ化。これまでの店舗全体での商品のトレンドや売上の増減ではなく、各顧客の把握にシフトしています。またオムニチャネル化を進めてオンライン・オフラインに関係なくユーザーの情報を溜めて解析をしている。アルゴリズムをもとに算出された提案をもとに商品の価格設定を柔軟に変更したり、店頭での商品説明もPOPからサイネージに切り替わることでよりインタラクティブに、そしてゆくゆくはユーザーごとに最適化した魅せ方に変わっていく。 また商品の在庫確認や物流のロボ化なども進み人の手を介さずに小売ができるようになる「無人化」関連のテクノロジーが多くみられました。これらはオペレーションの効率化といった店舗コストの削減はもちろんのこと、ここでもこれまでオフラインにしかなかったユーザーの購買情報を収集・蓄積するために進化が進んでいるといったほうがただしく流れを読み取れそうです。無人レジにすることで人件費を削減よりかは店舗でのユーザーの行動を追尾ができるようになることでデータを収集・解析しその次に商品需要の予測やレイアウトの検証をしている。小売がテクノロジーを活用して、デジタル化を進めているだけでなく複数のデータソースから予測分析をAIを用いて収益最大化を図っています。 小売業界のデジタル化やテクノロジー化を「オペレーションコスト改善」とだけ見るか、その先にある「AI活用による個別最適化の推進」と見るかで近未来の予測を読み誤ったり、戦略策定を誤ったりするので意識の転換が必要です。 もはや小売業界にもAI活用の流れは不可逆であると感じます。 世界中を飛び回ってテクノロジーを使ったファッション革命を一緒に目指すPM・エンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com *1 : シューズを手に取るとそのプロモーションビデオがすぐさま流れる *2 : 黒い柱の中央にある黒い塊がカメラである
どうも品質管理部エンジニアチームの木村です。 最近の話ではないんですがWeb UIテスト自動化をしようとなった時の事を書きます。 まずは初期段階の実行環境についてです、自動テストスクリプトの構築や処理そのものはまた次回。 Seleniumでテストを自動化したい! ZOZOTOWN に限らず最近のサービスはなんでもリリース頻度が高いです。 そして何故なのか、いついかなる時も、開発スケジュールは押し気味になります。 これは業界七不思議の1つです。たぶん。 品質管理部としてのテストは開発スケジュールの一番最後に置かれます。 つまり…短期決戦必須となります…。 そんなよくある話からSeleniumを使ってWeb UIテストを自動にしたいという流れになりました。 リリース頻度が高ければ高いほど、リグレッションテストはおろそかになると思うので、そこを自動テストで改善できると素敵です。 じゃあ自動テストスクリプトは書くけれど、その前にどこで実行するの? という事で、正直なところSeleniumもよくわかっていないところから、どうしたら良いのかを調べていました。 実行環境への要望としては下記の5点です。 ・ 並列処理でテスト実行時間を最大限短くしていきたい ・ 作業中PCで自動テストを実行した時、ブラウザがピョコピョコ動いてるのは目障り ・ 実行するPCによって環境がかわるのは避けたい ・ でも実行環境は気軽に変えられるようにしたい ・ 実行する人数やテスト数が増えた時に環境を増強できるようにしたい 調べていると、Selenium Gridを使えば上記問題は全て解決できそうだとわかりました。 Selenium Grid / docker-selenium Selenium ブラウザ上でのwebページ操作をスクリプトから行うためのツールです。 https://www.seleniumhq.org/ http://www.selenium.jp/ Selenium Grid Seleniumで実行される動作を管理するツールです。 https://www.seleniumhq.org/docs/07_selenium_grid.jsp node内に起動するブラウザでテスト処理を実行し、hubがnodeを管理してテストスクリプトからの命令を仲介します。 nodeや、node内のブラウザを複数起動してもhubが接続を管理してくれるので並列処理なども簡単に行えます。 hubとnodeは分けて起動できるので、nodeの追加や変更時にhubやその他のnodeを止める事なく簡単に行えます。 またdocker-seleniumというDocker版SeleniumGridもありました。 Dockerのコンテナを使用するので、場所を選ばず、起動が手軽で、管理もしやすく、クジラも可愛いです。 Dockerよくわからないけど素敵です。 https://github.com/SeleniumHQ/docker-selenium とりあえず作ってみた環境 内容に合わせて3種類に分けました。 ・ 自動テストスクリプトを実行するPC ・ hubを起動するPC ・ nodeを起動するPC 自動テストスクリプトはhubの情報さえ持っておけば、nodeの起動場所を一切気にする必要なくテスト処理を行えるので、PCを分けても問題なく動いてくれます。 hubはhub専用、もしくは各テストスクリプトの設定データや結果等、自動テストスクリプトで使用される共有データを保管しておくのも良いかもしれません。簡易的な形でdbを入れてみたりとか。 nodeはnode専用にして、社内の片隅に放置されてる可哀想なPCを集めて使うのも優しい気持ちになれて良いかもしれません。モッタイナイ精神。 画像内に表示してるPCアイコンは適当に選んだだけなので、これらを使用しているわけではないです。 いざ環境作成 とりあえず1台のPC内で環境を作ってみます。 以降の環境作成を実際に試す時はDockerとPython 3.6をインストールしてください。今回使用していたPCは全てmacOS High Sierraです。 尚、以下の作成手順にはIPを書き込む部分がいくつかありますが、1台で動かしている間は全て同じIPです。 必要なファイルを全部まとめて作っておきます。 TestAutomation |_hub | |_docker-compose.yml | |_node | |_docker-compose.yml | |_selenium_grid_setup.py | |_test_automation.py hubの準備 dockerでhubを起動する為に、設定ファイルを作ります。 ./hub/docker-selenium.yml version : "3" services : selenium-hub : image : selenium/hub:3.14.0-helium container_name : selenium-hub ports : - "4444:4444" environment : - GRID_BROWSER_TIMEOUT=120 - GRID_TIMEOUT=150 - GRID_MAX_SESSION=30 使用するselenium imageのバージョンは全て統一しないと動かなくなる時があるので、管理の事も考えてとりあえず書いておきます。 environmentのところで設定を変更できます。 GRID_BROWSER_TIMEOUTがブラウザのタイムアウト、クリック等で失敗を判断するまでの時間です。 GRID_TIMEOUTはhubがnodeを未使用の状態だ判断するまでの時間です。例えば自動テストスクリプトがエラーで終了した時、 hubに終了した事を伝えられないままになるのでnodeを掴んだまま解放されなくなります。なので、命令の無い状態が一定時間続いた時に解放するよ、という設定です。たぶん。 詳しくは公式で https://github.com/SeleniumHQ/docker-selenium/blob/master/Hub/Dockerfile.txt ymlファイルを作ったら起動します。 mac:TestAutomation user$ docker-compose -f hub/docker-compose.yml up -d URLでアクセスできたら成功です。 http://<hub用PCのIP>:4444/grid/console nodeの準備 hubと同様にnode用の設定ファイルを作ります。 ./node/docker-selenium version : "3" services : browser_0 : image : selenium/node-chrome-debug:3.14.0-helium container_name : chrome0 ports : - "55550:5900" - "5555:5555" environment : - NODE_MAX_INSTANCES=5 - NODE_MAX_SESSION=5 - HUB_PORT_4444_TCP_ADDR=<hub用PCのIP> - HUB_PORT_4444_TCP_PORT=4444 - REMOTE_HOST="http://<node用PCのIP>:5555" browser_1 : image : selenium/node-chrome-debug:3.14.0-helium container_name : chrome1 ports : - "55560:5900" - "5556:5555" environment : - NODE_MAX_INSTANCES=5 - NODE_MAX_SESSION=5 - HUB_PORT_4444_TCP_ADDR=<hub用PCのIP> - HUB_PORT_4444_TCP_PORT=4444 - REMOTE_HOST="http://<node用PCのIP>:5556" この設定ファイルでchromeを5つまで動かせるnodeを2つ起動できます。 browser_0とbrowser_1です。 portsを設定しておかないとホスト側のポートがランダムに選ばれるのでちゃんと書く必要があります。 左側がホストのポート、右側がコンテナのポートです。ホスト側のポートは空いているポートを指定してください。 portsの"5555:5555"がhubからnodeへの命令時に使用されるポートで、"55550:5900"がvnc接続に使用するポートです。 自動テストスクリプトの作成中はvncで確認したい事があると思うので、ポート番号がわかりにくいと面倒です。 各nodeのホスト側ポート「5555」「5556」は気軽に確認できるので、そこから連想しやすい番号だと楽です。上に書いたファイルの場合は末尾に0をつける形です。 nodeのenvironmentでもいろいろと設定できます。 NODE_MAXは1つのnode内で起動しておくブラウザの個数や、同時に動かせるブラウザの個数です。 タイムアウト等様々な設定がありますが必要ないものはもちろん、自動テストスクリプトの中で切り替える事ができるものもあります。 特殊な使い方をしないのであればほどほどで良いのかなと思います。 困ったら足すくらいの流れで。 https://github.com/SeleniumHQ/docker-selenium/blob/master/NodeBase/Dockerfile.txt ymlファイルをnodeフォルダに保存したら、起動します。 mac:TestAutomation user$ docker-compose -f node/docker-compose.yml up -d もう一度アクセスしてみて表示内容が変わっていれば成功です。 http://<hub用PCのIP>:4444/grid/console 自動テストスクリプトを実行した時にちゃんと動作しているかがわかりにくいので、vnc接続もしておきます。 Finder>移動>サーバーへ接続から下のアドレスを入力して接続です。vncのパスワードはsecretです。 vnc://<node用PCのIP>:55550 vnc://<node用PCのIP>:55560 Windowsの場合はUltraVNCとか入れると良いと思います。使い方は、わかりません! 必須ではないけれどnodeの起動方法 node起動時はnode数・使用ブラウザ・使用port等の設定内容を変更しながら試していたので、その都度ファイルを書き換えるのは面倒でした。 なので「ymlファイルを作ってからnodeを起動する」というスクリプトを書く形にしました。 node毎に設定するportはSTART_PORTで決めています。1つ目のnodeは5555、2つ目は5556といった形になっています。 ./selenium_grid_setup.py import os NODE_MAX = 5 START_PORT = 5555 def get_yml_text (node_count, hubip, myip, image, browser_name): text = 'version: "3" \n services: \n ' for i in range (node_count): text += ' browser_{num}: \n ' \ ' image: selenium/{image} \n ' \ ' container_name: {browser_name}{num} \n ' \ ' ports: \n ' \ ' - "{port}0:5900" \n ' \ ' - "{port}:5555" \n ' \ ' volumes: \n ' \ ' - /dev/shm:/dev/shm \n ' \ ' environment: \n ' \ ' - NODE_MAX_INSTANCES={NODE_MAX} \n ' \ ' - NODE_MAX_SESSION={NODE_MAX} \n ' \ ' - HUB_PORT_4444_TCP_ADDR={hubip} \n ' \ ' - HUB_PORT_4444_TCP_PORT=4444 \n ' \ ' - REMOTE_HOST="http://{myip}:{port}" \n ' . format ( NODE_MAX=NODE_MAX, image=image, browser_name=browser_name, hubip=hubip, myip=myip, num= str (i), port= str (START_PORT + i) ) return text def create_file (name, text): f = open (name, 'w' ) f.write(text) f.close() def launch_node (): hubip = '<hub用PCのIP>' myip = '<自身のIP>' node_count = 2 image = 'node-chrome-debug:3.14.0-helium' browser_name = 'chrome' yml_text = get_yml_text(node_count=node_count, hubip=hubip, myip=myip, image=image, browser_name=browser_name) yml_file_name = 'node/docker-compose.yml' create_file(yml_file_name, yml_text) os.system( 'docker-compose -f node/docker-compose.yml up -d' ) # 作る前に停止 os.system( 'docker-compose -f node/docker-compose.yml down' ) launch_node() ちょっと適当な感じが滲み出ちゃってますが、こんな形で作りたい内容に合わせてymlファイルを作って保存します。 yml設定ファイルを上書きする前に、古いyml設定ファイルで起動されているコンテナを停止するのだけ忘れないように。忘れるとコンテナで溢れかえります。 どうせ作るならhubの設定ファイルもスクリプトで作るようにするのが良いですね。imageのバージョンを1箇所で管理したいですし。 あと、流石に直接Pythonで文字列を作るより、PyYAMLを使うのが綺麗かもしれないですね。何も考えずに書いても長さは特に変わっていませんが。 import yaml from collections import OrderedDict # dict だと並び順が変わってしまったのでOrderedDictを使いました # OrderedDict だと出力の形式が違ったのですがyaml.add_representerで設定を変更できるみたいです def represent_odict (dumper, instance): return dumper.represent_mapping( 'tag:yaml.org,2002:map' , instance.items()) yaml.add_representer(OrderedDict, represent_odict) # get_yml_text を改変します def get_yml_od (node_count, hubip, myip, image, browser_name): services_dict = OrderedDict() for i in range (node_count): num = str (i) port = str ( 5555 + i) services_dict[f 'browser_{num}' ] = OrderedDict([ ( 'image' , f 'selenium/{image}' ), ( 'container_name' , f '{browser_name}{num}' ), ( 'ports' , [ f '{port}0:5900' , f '{port}:5555' ]), ( 'volumes' , [ f '/dev/shm:/dev/shm' ]), ( 'environment' , [ f 'NODE_MAX_INSTANCES=5' , f 'NODE_MAX_SESSION=5' , f 'HUB_PORT_4444_TCP_ADDR={hubip}' , f 'HUB_PORT_4444_TCP_PORT=4444' , f 'REMOTE_HOST="http://{myip}:{port}"' ]) ]) return OrderedDict([( 'version' , '3' ), ( 'services' , services_dict)]) # yaml の為にちょっと設定を追加します def create_file (name, yml_dct): with open (name, 'w' ) as file : yaml.dump(yml_dct, file , default_flow_style= False ) # 引数は適当です od = get_yml_od(node_count= 2 , hubip= '0.0.0.0' , myip= '1.1.1.1' , image= 'node-chrome-debug:3.14.0-helium' , browser_name= 'chrome' ) create_file( 'node/docker-compose.yml' , od) 実行してみる テスト処理を行うスクリプトを作って、実行します。 JavaとPythonでは書いてましたが、RubyやJavaScriptでは書いた事ないです。いろんな言語でかけるみたいです。 ./test_automation.py from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities def get_web_driver (): return webdriver.Remote( command_executor=f 'http://<hub用PCのIP>:4444/wd/hub' , desired_capabilities=DesiredCapabilities.CHROME ) driver = get_web_driver() driver.get( 'https://www.google.com' ) driver.quit() ↓せっかくなのでJavaの場合 import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import java.net.URL; public class Main { public static WebDriver getWebDriver(){ try { return new RemoteWebDriver( new URL( "http://<hub用PCのIP>:4444/wd/hub" ), DesiredCapabilities.chrome()); } catch (Exception e){ System.out.println(e); return null } } public static void main(String args[]){ WebDriver driver = getWebDriver(); driver.get( "https://www.google.com" ); driver.quit(); } googleを開いて、ブラウザを閉じるだけです。 スクリプトが動いてくれたら成功です。 ここまで1台のPCで動かしていたので、次はPCを3台にわけてみます。 分ける前に、今現在立ち上がってるdockerのコンテナを全て停止しておきます。 mac:TestAutomation user$ docker-compose -f node/docker-compose.yml down mac:TestAutomation user$ docker-compose -f hub/docker-compose.yml down 「実行用PC」「hub用PC」「node用PC」の3台にわける 追加するPCのdockerインストールやPythonのバージョンにご注意ください。 ここまで書いたファイルの<hub用PCのIP><node用PCのIP>の部分を書き直します。 IPを書き直したら全てのファイルを3台のPCにコピー&ペーストします。コピペなんてやってられるか!Gitだ!Git! と、おわかりの方はGitよろしくです。 あと、実際の運用を考えると、別途設定ファイルを用意して置いてそこにIPを書いて置いた方が切り替えやすいので良いですね。 各PCにファイルを揃えたらこれまでの手順通りに進めます。 1.hub用PCでhubを起動 2.node用PCでnodeを起動 3.起動したnodeにvnc接続 4.最後に実行用PCで自動テストスクリプトを実行 動いてくれたら完成です。もしも動かないという時は各IPを確認してください。 あとはhub用PCにも追加でnodeを起動してみたり、さらに4台目のPCを用意してnodeを追加してみたり、hubを複数にしてみたり。 使用中に起こった困った事 selenium使用中に発生した環境に関する問題で思い出せた事例を書いておきます。 <困ったこと1> 自動テストスクリプトの中でWebDriverに対して何らかの命令を送った時に、実行されなくなった事がありました。 各箇所で使用しているseleniumのバージョンが異なる時に、問題が発生しやすいみたいです。 絶対問題が出るというわけではないので余計に見落とします。 docker-compose.ymlで指定してるhubやnode、自動テストスクリプト内で使用してるselenium、ローカル上のブラウザで動かす時はそのpcに入っているブラウザ等、それぞれのバージョンに気をつける必要がありそうです。 docker-compose.yml version : "3" services : selenium-hub : image : selenium/hub:3.14.0-helium java - pom.xml <dependency> <groupId> org.seleniumhq.selenium </groupId> <artifactId> selenium-java </artifactId> <version> 3.14.0 </version> </dependency> Python - pip ターミナルから「pip list」でインストールしているバージョンを確認できるので確認。 <困ったこと2> 連続したテスト実行中に、開始時は問題なかったのに途中からWebDriverを掴めなくなる事がありました。エラーを見るとhubへの接続が失敗しているような雰囲気でした。 自動テストスクリプトの中でWebDriverに対して何らかの操作を行うと、 作業pc → hub → nodeという流れで命令を送信します。hubは中間地点として大量の命令をさばいてます。 ウィルス/スパイウェアで聞いた事があるような「自動で大量のリクエストを送信する」という行為と同じようなことをやってます。 社内環境だとセキュリティがしっかりかかってる事も多いと思いますから、そのウィルスのような挙動が原因となってセキュリティソフトに接続をブロックされる場合がありました。 リクエストが多いと言っても抑えられる範囲なので、同時実行数を抑えたり、hubを分けて分散させたりで解決できると思います。もしくはセキュリティチームに相談したり。 おわり ツールの使い方はいろんなところに書いてあるのですが、どんな環境・流れで動かしているか、どんなトラブルがあったかという記事は見当たらないことが多かったので書いてみました。 と言っても、まずは初期段階のSeleniumGrid環境についてでしたので基本的な事ばかりではありますが。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.starttoday-tech.com
こんにちは! ZOZOテクノロジーズ フロントエンドエンジニアの高橋(ニックネームはQ)です( @anaheim0894 ) 昨年12/26、毎年年末に行われる大忘年会(ZOZOCAMP2018)で、 グループ会社 も含めた1,000人規模でのリアルタイムアンケートを、FirebaseとVue.jsを使って制作しました。 当日会場にて弊社の昨年の事業紹介や、「楽しく働く」というコンセプトの動画を流し、動画の合間で質問をし動画と一体となるような演出を行いました。 その質問に対して全社員それぞれのスマートフォンで回答できるシステムを作ったので、その制作の裏側や、当日の様子などご紹介させていただきます。 まずは当日の様子の紹介 これを実現するまでの様子をご紹介いたします。 CAMP運営してくれている社員にもらった要件 CAMPの2週間前、運営の社員の方からこのような依頼をもらいました。 会場で、 リアルタイムで アンケートをWebでとり、その集計を即時に、目の前のプロジェクタに映し出す 選択肢はYES / NOの2つ 質問は全部で7〜8問 QRコードを読み取って、 スマホで回答できる いい感じのデザイン でプロジェクタに出したい 質問と回答は動画と動画の間に行いたい なかなか厳しい要件でした。 どうやったら実現できるか考える 依頼に答えるべく実現方法を検討しました。 Slackの Polly を使って全社員が投票、Slack APIを使ってプロジェクタに表示 → グループ会社含めるとSlackには全社員のアカウントがないため、NGに。 外部のサービスを使う →リアルタイムアンケートのサービス、 respon があったものの、カスタマイズができるか不明なこと、2週間で要件を完璧に実現できないと感じたためNGに。 PBグローバル を担当しているフロントエンドチームで自作 → Firebaseならリアルタイムデータ集計の可能性があったため、 こちらで進めていくことに決定しました 。 自作にあたって整理した要件 CAMP運営からの要件を元に、自作するための要件を整理しました。 質問は7〜8問 動画演出の間に質問を差し込み 動画と質問の切り替えはプロジェクタのスイッチングのみ 選択肢 Yes / No の二択 QRコードで読み取ってサイトを表示 回答結果がリアルタイムで集計され、すぐに会場のプロジェクタで表示 次の質問に移るときはリロードなし 質問の切り替えは管理画面を用意 スマホでの回答は1回のみで解除は不可 回答時間は15秒(質問画面10秒その後5秒のカウントダウン演出) 結果はYES/NOのパーセンテージ割合で表示 当日その場で質問の追加はなし 準備 / 事前に確認したこと CAMPは1,000人規模のイベントです。 そのため、 当日起きることをどれだけ予測できるか 予測外の事態にどれだけすばやく対応できるか が重要になると考え、様々なケースを想定して情報を集めました。 参加人数 約1,000人 1,000人に耐えられるネットワークの準備 イベント自体はイベント会社が入っていたため、イベント会社にWi-Fiの用意を依頼(NTT様にご協力頂きました) 会場スクリーンスペック 解像度:1920×1080 コンテナ:MOV コーデック:映像H.264 / 音声PCM 48kHz フレームレート:59.94または29.9 会場スクリーンの台数 4画面 スクリーンに繋ぐマシンのネットワーク環境 有線で用意 当日のリハーサルの可否 15時から30分〜40分リハーサルができた 1,000人規模のテスト 人数を用意できないため、ぶっつけ本番 (擬似的にコードレベルでの負荷テストは実施) 当日不具合があった場合の対応 質問だけのスライドを用意する。最悪挙手なども考えられる デザイン 社内のデザイナーに依頼 さまざまなスマホでの表示確認 社内のテスト検証を専門に行なっているチームに依頼 選定した技術 上記の情報から自作するために、以下の技術を選定し、作成しました。 データ管理: Firebase フロントエンド: Vue.js 結果のアニメーション: TweenMax 作成した画面 以下の3画面を作成しました。 回答画面 各ユーザーがスマートフォン上で回答を送信する画面 質問の開始・終了時に画面の活性を制御 質問/結果画面 会場のスクリーンに表示される質問や結果の画面 質問の開始・終了時に質問/結果画面を切り替え、結果画面には各ユーザーの回答を集計して表示 管理画面 進行管理者が質問を切り替えるための画面 質問を切り替えると同時に、質問の終了時刻も設定 Firebaseセットアップ 質問の表示切り替えと投票結果の集計をリアルタイムで行うため、リアルタイムリスナー搭載のFirestoreがあるFirebaseを選定しました。 開発に携わったのが全員フロントエンドのメンバーだということもあって、サーバーサイドの開発はせずクライアントJSからデータベースに直接アクセスする構成を取りました。 ここでは、FirebaseをWebアプリケーションで使用するためのセットアップ方法をご紹介します。 プロジェクトのセットアップ Googleアカウントが無ければ作成します。 Googleにログインして、 Firebaseコンソール からプロジェクトを追加します。 プロジェクト名を入力します。 アナリティクスやテクニカルサポートについては必要無さそうなのでチェックを入れずに進んでいきます。 自動でプロビジョニングが行われ、プロジェクトページに飛びます。 デフォルトでは無料のSparkプランが設定されています。ちょっとしたアプリケーションなら無料枠で問題なく動くと思います。 今回は1,000人規模のユーザーがリアルタイムで操作するということだったので、念のため従量課金のBlazeプランに設定しました。 Firestoreセットアップ Firestoreを設定していきます。 左側のナビゲーションから 開発 > Database を選択し、データベースを作成します。 セキュリティルール:今回は要件上セキュリティ面をさほど気にする必要がないので、スピード重視でテストモードを選択します。 コレクション(RDBでいうテーブルのようなもの)が無い空のデータベースが作成されます。 FirestoreはNoSQLなのでスキーマの作成も必要ありません。 Webアプリケーションとの連携を行います。コンソールトップのHTMLタグ風のアイコンをクリックします。 ポップアップでスニペットが出てくるのでこれを開発中のHTMLコードに貼り付けます。 (アプリケーション側のコードをまだ何も作成していなかったので、ここでVue.jsのプロジェクトをinitializeします) Vue.jsのセットアップは こちら を参考にしてください。 ポップアップのメッセージにある HTMLの一番下、他のスクリプトタグの前 ではなく、 main.js に貼り付けます。 .vue ファイル内で import firebase from 'firebase' と記述すれば、 firebase 変数でFirebaseのAPIが利用できます。 GUIコンソール上でガイドにしたがって画面操作するだけでセットアップが完了しました。 Firebase Hostingセットアップ WebアプリケーションのホスティングにもFirebaseを利用することにしました。 こちらはFirestoreと比較しても、とても簡単に設定できます。 左側のナビゲーションから 開発 > Hosting を選択し、 使ってみる ボタンから開始します。 ポップアップのガイドにしたがってfirebaseのコマンドラインツールをインストールします。 $ firebase login でGoogleアカウントにログインします。 開発中のvueプロジェクトのディレクトリに移動し、 $ firebase init でデプロイ設定ファイルを作成します。 生成された firebase.json の hosting.public にデプロイ対象ディレクトリを設定します。 今回はvueプロジェクトをwebpackでビルドしたコードが dist ディレクトリに出力される構成なので、 dist と入力します。 $ firebase deploy で、デプロイ完了です。 firebaseコンソールのHosting画面にデプロイ先のサーバーのドメインが表示されているので、このURLでブラウザからアクセスできます。 以上でFirebase側の設定は完了です。 実際20分くらいでインフラセットアップ作業が完了し、すぐさまアプリケーション側の開発に入れる状況まで持ってこられました。 Firestoreと画面の連携 アプリケーション側のFirestore連携部分の実装を紹介します。 3画面の状態を各端末で同期的に制御するために、Firestoreで以下を管理することにしました。 現在の質問id 回答の締切時間 ユーザーの回答 実際には以下のようなデータ構造でこれらを管理しました。 (実際のデータはJSONではありませんが、便宜上JSON形式で書いています) { " questions ": { " current ": { " id ": 1 , // 現在の質問id " endTime ": 1545906141 // 回答の締切時間 } , } , " votes ": { // 各ユーザーの回答 " 001EjYAwtSMXlrWWTP5r ": { // (Firestoreが自動付与するid) " answerId ": 0 , // 回答id " questionId ": 2 // 質問id } , " 004gO0YzXUJ2bNFSbc5y ": { " answerId ": 1 , " questionId ": 3 } , . . . } } 回答・質問/結果画面でこれらのデータの変更を購読します。 現在の質問idと回答の締切時間(回答画面・質問/結果画面) import firebase from 'firebase' export default { . . . created() { // this.db = firebase.firestore() this .unsubscribe = this .db.collection( 'questions' ).doc( 'current' ) .onSnapshot((doc) => { const currentQuestion = doc.data() // => // { // "id": 1, // "endTime": 1545906141 // } } ) } . . . } 各ユーザーの回答(質問/結果画面) import firebase from 'firebase' const db = firebase.firestore() export default { . . . created() { // this.db = firebase.firestore() this .db.collection( 'votes' ).onSnapshot((collection) => { this .votes = collection.docChanges().reduce((votes, c) => { const vote = c.doc.data() // => // { // "answerId": 0, // "questionId": 2 // } return { ...votes, [ vote.questionId ] : [ ...(votes [ vote.questionId ] || [] ), vote ] } } , this .votes) // => // { // . // . // . // "2": [ // 質問id毎に集計 // { // "answerId": 0, // "questionId": 2 // }, // . // . // . // ], // "3": [ // { // "answerId": 1, // "questionId": 3 // }, // . // . // . // ], // . // . // . // } } ) } } 結果画面では質問毎に集計を行うので、質問id毎に回答のデータをまとめています。 また、回答のデータに関しては課金を最小限にするため docChanges() を用いて差分だけ取得するようにしています。 一方、管理画面では以下のようなコードでFirestoreに書き込みを行います。 import firebase from 'firebase' import moment from 'moment' export default { . . . methods: { setQuestion() { db.collection( 'questions' ).doc( 'current' ).set( { id: questionId, endTime: moment().add(questionTime, 'second' ).unix() } ) } } . . . } 管理画面で質問を開始するときには、Firestoreに質問idと締切時間を書き込みます。 書き込みが行われた時点でFirestoreから会場の全端末に通知され、一斉に質問が開始されます。 そして、各端末が締切時間に応じて質問の回答を締め切ります。 以上でFirestore連携は完了です。 ここまで来たら、後は画面にアニメーションや装飾を施すのみです。 アニメーションについて 質問/結果画面のアニメーションに関しては TweenMax で実装しました。 TweenMaxについて GSAP(グリーン・ソック・アニメーション・プラットフォーム)モジュールの1つです。 使用可能ライブラリとプラグイン以下のとおりです(要は全部入り) TweenLite TimelineLite TimelineMax CSSPlugin AttrPlugin RoundPropsPlugin BezierPlugin EasePack (後々考えると今回の実装内容だとTweenLiteのみでよかったかも…) 採用した理由 Vue.jsのデータ駆動設計と相性が良さそうだった 複雑なアニメーション作成に適している 構文が直感的でわかりやすい 使用方法 import import { TweenMax } from 'gsap' Methods 今回は主に下記Methodsを使用しました。 値(初期値)を設定する TweenMax.set( target:Object, vars:Object ) 初期値から設定した値にアニメーションさせる TweenMax.to( target:Object, duration:Number, vars:Object ) 設定した初期値から設定した値にアニメーションさせる TweenMax.fromTo( target:Object, duration:Number, fromVars:Object, toVars:Object ) 配列化されたtargetをindex順に設定した初期値から設定した値にアニメーションさせる TweenMax.staggerFromTo( targets:Array, duration:Number, fromVars:Object, toVars:Object, stagger:Number ) 実装サンプル See the Pen countdown by masahito.ando ( @masahito_ando ) on CodePen . 質問画面にて、 questions.current.endTime (Firestoreに書き込まれた締切時間)をフロント側でも setInterval にて監視し、締切時間が残り5秒になったタイミングで上記アニメーションを開始しています。 See the Pen graph by masahito.ando ( @masahito_ando ) on CodePen . 締切時間に到達した時点で結果画面を表示します。 そのタイミングで votes.[Firestoreが自動付与するid].questionId を questions.current.id でフィルタリングして現在の質問の回答データを取得し、YESとNOのパーセンテージとグラフの高さを計算します。計算完了後、上記アニメーションを開始しています。 当日起きたこと 動画と動画の合間に質問を入れるタイミングを画面のスイッチャーと息を合わせる必要があったため、リハーサルで入念にタイミングを確認しました。 実際にリハーサルしてみると、動画と質問の間に違和感があり、スムーズに見えませんでした。 そのため、動画と一体になっているような演出に見えるよう質問の前に真っ白な画面を用意しました。また、動画が真っ白にフェードアウトするようにしてもらいました。 1,000人同時アクセスされた場合ちゃんと動くかテストできずぶっつけ本番だったため、1問目が終わった時にちゃんとデータが入った(動いた)時に 全員でガッツポーズ をしました。 裏方の様子 イベント会社の方が、 こんなリアルタイムでアンケートなんて、これまでやったことないです! どうやってるんですか? と興味津々に食いついていただけて、嬉しかったです。 料金について Firebaseのプランですが、今回1,000人規模かつ初の試みということもあり、従量課金Blazeプランにしました。 イベント終了後、請求金額を見てみると…。 2円 なんと2円でした! 笑 データの作り方にもよりますが、2円の予算で実現できます。 今後の課題 全体を通してうまく作ることできたのですが、当日、質問と質問の間の動画の尺が長く、開いていたiPhoneなどの端末がスリープしておりました。 質問の回答時間15秒だったため、スリープ解除している間に回答時間が終了してしまうという事態が発生してしまいました。 アンケートとしては十分な回答時間でしたが、動画と合わせたときの状況を考え、回答時間が適切かを考えるべきだったなと思いました。 最後に制作メンバーの紹介 今回の制作は弊社 PBグローバル のフロントエンドチームで制作をしましたので、ご紹介させていただきます。 進行管理:高橋( @anaheim0894 ) フロントエンド、アニメーション:安藤 Firebaseなどバックエンド担当:松井( @nahokomatsui )、茨木( @niba1122 )、権守( @AmatsukiKu ) 進行補助:松浦( @mtmn07384 ) こんな突発的かつ刺激的なことや、ZOZOを作ってみたい! という方。 弊社では一緒に作ってくれる方を大募集しています。 ご興味がある方は以下のリンクから是非ご応募ください。 www.wantedly.com
こんにちは、ZOZO研究所の岩本です。 2018年12月5日から12月8日にかけて富山で開催された学会 SCIS & ISIS 2018 に、同じく研究所の岩崎と参加してきました。 SCIS & ISISについて SCIS & ISISはソフトコンピューティングと知能システムに関する国際学会です。 SCISとISISはそれぞれ別の学会ですが、今年は共同で開催されました。 また、ISWSというワークショップも同時に同じ会場で開催されていました。 今回はこの学会に参加して他の方の発表を聞いたり、研究成果をポスターで発表したりしてきました。 学会のプログラムは主に、招待講演、トークセッション、ポスターセッションに分かれています。 招待講演では国内外の大学教授の方などがホールでいくつかのテーマについてお話しされていました。 トークセッションでは、いくつかの部屋で20分程度の発表が並列して行われます。 ポスターセッションでは会場に20枚ほどのポスターが掲示され、参加者は興味のあるポスターの前に行って発表者から説明を受けたり、内容に関するディスカッションを行ったりします。 ポスターセッションでは自分たちも発表していたため他の発表を見ることが出来ませんでしたが、招待講演とトークセッションでは色々な興味深い話を聞くことが出来ました。 また、現在共同研究を行っている九州工業大学の古川研究室からも、4人の学生さんがトークセッションとポスターセッションで発表をされていました。 このレポートでは、今回私たちが行った発表の紹介と、その他の発表の中で特に興味深かったものを紹介したいと思います。 ZOZO研究所のポスター発表について ZOZO研究所からは「Visualization of User-item Rating Matrix by Hierarchical Tensor SOM Network(階層型Tensor SOMネットワークによるユーザー・アイテム評価行列の可視化)」というタイトルでポスター発表を行いました。著者は岩本海童、岩崎亘、古川徹生(九州工業大学)です。ここでは、簡単にその概要をご紹介します。 現在、岩崎と岩本(私)のチームは 九州工業大学との共同研究 で、大規模な関係データの可視化を研究しています。 関係データの可視化と言ってもイメージが難しいと思うので、例を使って説明してみます。 何人かの人にいくつかのアイテム(例えば食べ物など)の好みについて質問したアンケートデータがあるとします。 このようなデータを解析すると、「どんな人がどんなアイテムを好んでいるのか」や「ある人と好みが似ている人たち」「あるアイテムと好む人の傾向が似ているアイテム」を直感的に可視化することが出来ます。 しかし、ユーザーやアイテムの数が多い場合には、個別のユーザーやアイテムだけではなくもう少し俯瞰的な視点でデータを見たいことがあります。 例えば、「甘い食べ物が好きなのはどのユーザーなのか」「30代のユーザーはどんな食べ物が好きなのか」などです。 そこで、私たちは関係データと一緒にユーザー属性情報とアイテム属性情報を解析してみました。 属性情報とは、例えばユーザーの場合は性別や年齢層、アイテムの場合はカテゴリーや価格帯などです。 その結果、「あるアイテムはどんなユーザー属性の人に好まれているのか」や「あるユーザー属性の人は、どんな属性の商品が好きなのか」といった情報を視覚的に確認できるようになりました。特に後者が重要です。先行研究ではユーザー属性だけを利用していたのですが、今回はユーザーとアイテムの両方を一緒に解析し、それらの関係を可視化しました。 そうすることにより、 ユーザーとアイテムそれぞれ について、 個別のデータレベルの視点と属性レベルの2つの視点から関係データを視覚的に見る ことができるようになりました。 文章だけだとよく分からないと思うので、可視化の様子の一例を簡単に図にしてみました。 実際はこのように静的なものではなく、フォーカスするものを切り替えながら、インタラクティブに可視化された結果を確認できます。 (それを表現するにはポスターでは限界があったので、発表ではiPadで可視化のデモを行いました) 今回の発表では寿司の好みに関するアンケートデータを利用しましたが、最終的にはこの手法を使ってZOZOの購買データやWearのコーディネートのデータを可視化したいと考えています。 膨大なデータを可視化することで、今まで知ることのできなかったデータの一面が発見できるかもしれません。 また、今回発表したポスターで、 ポスターセッションアワードを頂きました 。 全く予想外の出来事で、大変嬉しく思います。 ポスター発表の様子 こちらはポスターの前で参加者に研究を説明しているところです。インタラクティブな可視化を見ていただくためにiPadでのデモンストレーションも行いました。 ポスターの前に立つ私です。 その他の発表の紹介 ここでは、学会で視聴した発表の中から特に印象的だったものをいくつかを紹介します。 なお、見出しの下の斜体の文は、発表者名と私が訳したタイトルです。 Cognitive Mechanism of Humor in Riddles: Examination of Relationship between Humor and Semantic Structure of Riddles Asuka Terai, 「謎かけのユーモアの認知メカニズム:ユーモアと謎かけの意味構造の関係の調査」 謎かけを自動生成して、その面白さと使われている単語との関係を調べる研究です。 ここでは、「AとかけてBと解く。その心はC(C')」(AとBはそれぞれ名詞で、CとC'は同音異義語でそれぞれAとBに関係がある)というような限定したスタイルの謎かけを扱います。 AとBの単語をランダムに選び、それぞれに意味が近い単語CとC'をWikipedia日本語版のコーパスとword2vecを使って選ぶことで、謎かけを生成します。 あらかじめ用意したいくつかの謎かけについて、ユーモアがあるかどうかのアンケートを行い、またA-C, B-C', C-C'間の単語の類似度を調べました。 その結果、A-CとB-C'の類似度が共に高い謎かけは高評価が得られやすかったそうです。しかし、生成した謎かけはA-CとB-C'が共に高くなることはあまりなかったそうです。 感想 ビジネス寄りの実用的な研究が多い中、こういうある種の「遊び」に関する研究はすごく面白いなと思います。 もっと複雑な文章の謎かけを生成したり、あるいはユーモアに関してもっと多角的に研究(ユーモアの種類、地域や世代などによる感性の違いなど)してみたりすると面白そうだと思いました。 Non-parametric Continuous Self-Organizing Map Ryuji Watanabe, 「ノンパラメトリックで連続な自己組織化写像」 九州工業大学の渡辺さんの発表です。 (現在行っている共同研究にも関連する内容です) 自己組織化写像(self-organizing map, SOM)は高次元のデータの低次元表現を離散的に求める手法です。 この研究では連続した空間での低次元表現を求められるようにSOMを改良しました。 また、求められる低次元表現はデータ点だけに依存する(ノンパラメトリック)ようになっています。 その結果、ユニット数を増やした高解像度なSOMと同じような表現を得られることが実験から確認できたそうです。 感想 離散化した写像を求めるSOMを、連続かつノンパラメトリックに拡張する流れがとても自然で美しいです。 データ数が増えた場合にいかにメモリと計算時間を節約するかが実用に向けた課題だと思います。 Visualized Onomatopoeia Thesaurus Maps based on Deep Autoencoder Daiki Urata, 「深層オートエンコーダーによる類似オノマトペのマップ」 オノマトペの類似度を可視化する研究です。 オノマトペを音声に変換したものをオートエンコーダーにかけて2次元の表現を得ます。 それを2次元上に表示すると、似た音のものや、同じカテゴリー(汁をすする音、飲み物を飲む音など)のものが近くに配置されるそうです。 未知のオノマトペに対しても、同様に2次元の表現を得て、そこからもっとも近くにあるオノマトペのカテゴリーをそのオノマトペのカテゴリーとします。 その結果、高い精度で未知のオノマトペのカテゴリーを推定できたそうです。 感想 オノマトペの研究というのを初めて知ったので、とても興味深かったです。 ファッションを表現する際にもオノマトペを使うことがあるので、雰囲気や質感を表す言葉から服やコーディネートを提案できたら面白いなと思いました。 An Extreme Learning Machine Based Pretraining Method for Multi-Layer Neural Networks Pavit Noinongyao, 「エクストリームラーニングマシンに基づく多層ニューラルネットワークの事前学習の手法」 エクストリームラーニングマシン(ELM)はある種のニューラルネットワークですが、勾配法を使わず、1層目の重みをランダムに初期化し2層目の重みを逆行列を使って決定的に求めます。 このELMをベースにしたオートエンコーダーを用いると、多層ニューラルネットワークの事前学習を行うことができます。 この研究では、通常のELMとは逆に2層目をランダムに初期化し1層目を計算するbackward-ELMという手法を提案し、事前学習の手法として有用であることを実験により示しました。 感想 ELMという手法について詳細を知らなかったのですが、勾配法を使わずに一気に重みを計算するというのはとても大胆だなと思いました。 時間があるときに詳しく調べてみたいです。 参加した感想 実は、今回は私にとって初めてのポスター発表でした。 私は人の前で話すのがあまり得意ではないのですが、視聴者の方々が優しく聴いてくださり、説明が不十分なところを適宜質問していただいたのでなんとか研究の内容をお伝えできたように思います。 また、今回は可視化の研究であり、iPadでのデモンストレーションにより視覚的に相手に訴えることができたというのも良かった点かなと思っています。 ポスター発表は1人(または少数の人)を前にして相互にコミュニケーションしながら発表を行うことができます。 これは多くの人の前で行う口頭発表にはない良さですが、一方で事前に頭の中で発表する内容をよく整理しておく必要があることに気づきました。 次にポスターで発表する機会があれば、もっとスムーズに説明できると良いなと思っています。 招待講演やトークセッションでは、色々な分野の発表を聞くことができて、大変勉強になりました。 また、今回の学会では自分の英語の能力の足りなさを実感しました。 今後、もっと英語の発表を聴いて良く理解したり、もっと英語で情報を伝えられるようになりたいと思いました。 おまけ 上に書いていない学会や富山の様子を写真で一部ご紹介します。 九州工業大学の学生さんたちによる発表 古川研究室から4人の学生さんがトークセッションとポスターセッションで発表されていました。 晩餐会 学会の最終日の前日の夜に、晩餐会が行われました。 運営の方の挨拶や学会の歴史についての紹介が行われた他、杖道のパフォーマンスも行われました。 富山の食べ物 富山名物の、富山ブラックと呼ばれるラーメンです。 元々、肉体労働者に食べてもらうために作られたこともあり、かなり醤油の塩味が効いています。 味が濃すぎるあまり途中でライスを注文しましたが、1杯では全然足りませんでした。 そして、富山といえば何と言っても海の幸です。 私たちも学会の期間中、何回かお刺身や焼魚などの海の幸を味わいました。 また、魚と一緒に飲む富山の日本酒も美味しかったです。 最後に、岩崎と私で会場前でパシャリ。 お疲れ様でした。 おわりに ZOZO研究所では、ファッションに関する研究を行なっています。 私と岩崎のようにデータの可視化を研究しているチームもあれば、コーディネートの研究をしているチームもあり、日々さまざまな課題に取り組んでいます。 あなたも一緒にZOZOのデータ資産を活用して「似合う」を研究しませんか? ご興味のある方は下のリンクからご連絡いただき、ぜひ一度オフィスに来てください。 研究所のオフィスは青山と福岡にあります。 www.wantedly.com www.wantedly.com
こんにちは。開発部の廣瀬です。 本記事では、昨年障害が発生してしまったZOZOTOWNの福袋発売イベントについて負荷対策を実施し、今年の福袋イベント期間を無傷で乗り切った話をご紹介したいと思います。 大規模サイトの障害に関する生々しい話はあまり公開されていないように思いますので、長くなってしまいましたが詳細に書いてみました。尚、今回のお話は弊社のサービスで使用しているDBMSの1つである、SQL Serverに関する話題がメインです。 福袋イベント「ZOZO福袋2019」とは 年に1度、多数のブランドの福袋が一斉に発売される、ZOZOTOWNの年末の風物詩的イベントです。今年は450以上のブランド様にご参加いただきました。お客様からも毎年大変ご好評いただいており、年間を通して最も多くのトラフィックを記録するイベントの1つです。 アクセスが殺到するが故に、昨年は福袋の発売直後からエラーが多発し、一時的に買い物し辛い状態を発生させてしまいました。昨年も負荷対策を実施していたのですが、それでもエラーが多発する状況となってしまい、エンジニア一同、お客様にご不便をおかけして申し訳ない気持ちで一杯でした。 今年こそは絶対に何事もなく福袋イベントをお客様に楽しんでいただこうと、負荷対策に力をいれましたので、順を追って紹介します。 昨年の福袋イベントで発生した障害について 昨年の福袋イベントにおける障害を時系列でまとめると以下のようになります。 12:00 福袋発売開始。 12:01 開始直後のみエラー無く好調だったものの、1分後から大量のエラーが発生。 主にDBサーバーへの接続エラーであった。 必ずエラーが発生する状態ではないものの、購入フローの途中でエラーとなるケースが多発。 注文し辛い状態が続く。 13:40 エラー多発状況が自然解消され、スムーズに注文ができるようになる。 注文数の推移からも、障害の影響が確認できます。 最初の1分間のみ多くの注文が入っています。しかし、その後急激に注文が入らなくなっています。13時40分ごろ急激に注文数が増大したことから、潜在的にはより多くのご注文をいただける余地があったと考えられます。購入フローにおいて、注文確定までの数回の遷移のいずれかでDBの接続エラー発生によりなかなか注文完了できなかったと思われます。 障害の原因調査 適切な負荷対策を実施するためには、昨年発生した障害の原因を突き止める必要があります。そのために調査を実施しました。原因調査には、障害発生時にリアルタイムで収集しておいた 動的管理ビュー の情報と、弊社で導入している監視製品の Spotlight を使用しました。 Spotlightについて簡単に説明しますと、サーバーの様々な情報を定期的に収集しておいて、後から任意の時間帯のサーバー状況を確認できるソフトウェアです。CPUやメモリなどのリソース使用状況や、秒間バッチ実行数、コネクション数などの各種メトリクスから実行中のクエリテキストまで様々な情報が保存されます。そのため、事後調査の際に重宝します。 サーバーの状況をSpotlightで追っていきます。秒間のバッチ実行数をSpotlight上で確認します。 12時の開始直後は普段の約5倍のクエリ実行要求があったようです。その後急速にバッチ実行数が低下しています。この波形が自然でないことは、エラーが大量に発生していた事実からも、注文数の推移からもいえると思います。一度は大量のバッチ実行数を記録したDBサーバーが、何らかの原因で実行数を抑えつけられているようです。DBの内部で起きていたことをさらに調査していきます。 コネクション数の推移です。SQL Serverは仕様で同時接続数が最大32,767と決められており、上限を超えるとクライントにはエラーが返されます。平常時より高いコネクション数で推移しているものの、上限値まで達するということは無く余裕があり、コネクションボトルネックではありませんでした。 次に、障害発生中のDBサーバーの待ち状態の傾向を確認します。 SQL Serverでは、クエリ実行要求を受け付けてから実行完了するまでの間に生じた待ち時間を、ロックなどの待ち事象の項目ごとに確認できます。この情報は動的管理ビューの1つである sys.dm_os_wait_stats から取り出すことができます。SpotlightからもこのViewと同等の情報を確認できます。 秒間の累積待ち時間が多い順に並び替えると、平常時は以下のような傾向を示します。 一方で、障害発生時は以下のような状況でした。平常時と比べて、まったく違う傾向を示していました。また、各項目の待ち時間も非常に高い数値を示していました。 ここでは、上位5つのWaitTypeについて紹介します。 THREADPOOL : ワーカースレッドの確保待ち LCK_M_S : 共有ロックの獲得待ち PAGELATCH_SH : 共有ページラッチの獲得待ち LCK_M_U : 更新ロックの獲得待ち PAGELATCH_EX : 排他ページラッチの獲得待ち 「ワーカースレッド」と「ページラッチ」については、聞いたことのない方もいらっしゃるかもしれませんので、それぞれの用語について少し補足をしておきたいと思います。 ワーカースレッドとは、SQL Serverのクエリ実行時にスレッドで使用されるリソース、ページラッチは、SQL Serverのデータ格納領域である「ページ」の一貫性を保つための排他制御の仕組みとイメージしていただければと思います。 (SQL Serverでは、1ページあたり8KBで管理され、ページの中には同一テーブルのレコードが複数入っています。1ページあたり何レコード格納されるかは、1レコードあたりのデータサイズに依存します。) 先ほどの、障害発生時の待ち事象の1位はTHREADPOOLのwaitが圧倒的に多く、次いでページラッチ系とロック系のwaitが2位から5位までを占めていました。平常時は、これらのwaitが多く発生することはありません。 CPUリソース不足時に高い数値を示すSOS_SCHEDULER_YIELDの値はTHREADPOOL待ち時間の100分の1程度でした。そのため、CPU負荷もボトルネックでは無いと判断しました。 THREADPOOL waitが圧倒的に多かったため、多発していたエラーとの関連性を調べるために、ローカル環境のDBでワーカースレッドを意図的に枯渇させてクエリを実行してみました。その結果、本番環境で多発していたものと同様のエラーが発生することを確認できました。 このことから、多発していたDB接続に関するエラーは、ワーカースレッドが枯渇したことが、原因の一つであると判断しました。ということで、昨年の福袋の障害の原因は、DBの状態としては「ワーカースレッドの枯渇により大量のTHREADPOOL waitが発生した」ということが確認できました。 ここで、「ではワーカースレッドの最大数を増やせば解決するのでは?」という疑問がわきます。 ただし、現状のワーカースレッドでも平常時の5倍の要求を一時的に受け付けていることから、単純にワーカースレッド数を増やせば解決する問題でもなさそうです。 次に、ワーカースレッドが枯渇してしまった原因について調べました。Spotlightを使って、エラー発生時に実行中だったクエリリストを確認します。 「Last Wait Type」「Last Wait Resource」から、同一のリソースに対して、大量のページラッチ獲得待ち(PAGELATCH ****)と、キーロック獲得待ち(LCK_M **)がそれぞれ発生していることが確認できます。「LCK_M_U」の情報から、獲得しようとしているロックの粒度がKEYであるため、特定のレコードに対する読み取り要求、更新要求が大量に実行されていることが分かります。 「PAGELATCH_SH」「LCK_M_U」の「Last Wait Resource」に表示されているリソースの値だけでは「どのデータが対象になっていたのか」までは、判断することができません。そこで、次の方法を使用して、データの取得を行いました。 PAGELATCH_SH で出力されていた、Last Wait Resourceの値を使って、具体的にどのテーブルのどのようなレコードが含まれているページへのアクセスであるかを調査しました。 ページラッチ待ちが多発しているLast Wait Resourceの値と「DBCC PAGE」を使ってページの中身をダンプさせます。 DBCC TRACEON( 3604 ) DBCC PAGE (N 'DB名' , 1 , 4xxxxx8, 3 ) DBCC TRACEOFF( 3604 ) 次に、ロック待ち(LCK_M_U)が多発しているLast Wait Resourceについて対応するテーブルとレコードを特定します。KEY:DBID:hobt_id(%%lockres%%)という構成になっているため、下記のクエリを使います。 --1.hobt_idを指定してテーブル名を取得 select sc.name as schema_name ,so.name as object_name ,si.name as index_name from sys.partitions as p join sys.objects as so on p.object_id = so.object_id join sys.indexes as si on p.index_id = si.index_id and p.object_id = si.object_id join sys.schemas as sc on so.schema_id = sc.schema_id where hobt_id = 72xxxxxxxxxxxxx28; --2.取得したテーブルのレコードを、%%lockres%%を使って絞り込んで特定 SELECT * FROM テーブル名 WHERE %%lockres%% = '(4bxxxxxxxx5d)' ; 以上のクエリを実行した結果、ページラッチとロックは同じテーブルの特定レコードに関連する待ちであることが分かりました。購入フローの中でDB上の在庫に関するデータを更新する処理があるのですが、人気福袋の購入希望者が殺到したことで読み取り要求、更新要求の競合が多発してボトルネックとなっていたようです。 図示すると以下のようになります。 ロックはレコード単位で競合するのに対し、ページラッチはページ単位で競合します。そのため、ページラッチについては、人気福袋と同一ページにある他の福袋商品へのアクセスとも競合してしまっていました。 障害発生中、ページラッチ待ちとロック待ちが発生しているクエリは、実行中の全クエリの約半分を占めていました。そのため通常よりワーカースレッド解放に時間がかり、大量のDBアクセスを捌ききれなくなり、ワーカースレッド確保待ちが大量に発生してエラーの多発につながったと考えられます。 この状況を図示します。まず、SQL ServerのCPUのアーキテクチャの概要図です。 SQL ServerはCPU1コアにつき1つのスケジューラが割り当てられます。各スケジューラに対して複数のワーカースレッドが割り当てられます。クエリ実行時にワーカースレッドが1つ以上利用され、クエリの実行が行われます。SQL Server では、作成できるワーカースレッドの数に上限があります。(作成できるワーカースレッド数の上限は、「max worker thread」という設定で変更できます。) 実行されていたクエリのうち、ページラッチとロック競合が発生していなかったクエリは全体の半分を占めていました。これらのクエリではワーカースレッドの利用と解放が高速に行われていました。一方で、解放および競合が発生していたもう半数のクエリでは、ページラッチやロックといったリソース獲得待ち状態で、ワーカースレッドの解放が遅くなっていました。結果として利用可能なワーカースレッドの低下による全体のスループットが低下し、にもかかわらずクエリの要求数は増え続け、慢性的なワーカースレッド枯渇状態に陥って大量のエラー多発、というシナリオだと思います。 人気商品以外の商品を閲覧、購入されていたお客様は、ワーカースレッドが確保できてしまえば、高速に要求を処理できていたと考えられます。一方で人気商品へのアクセスは、ワーカースレッドを運よく確保できたとしても、実行時にロック獲得待ちなどの理由で実行時間がかなり伸びてしまいます。そのためタイムアウトも発生しやすい状況であったはずです。 ただし、12時からの1分間は大量に注文が入っていることから、その後のエラー多発状態へと移行したタイミングで何が起きていたかの説明はできていません。 12時1分を過ぎたタイミングから、ロックおよびページラッチ獲得待ちが大量に発生していることから、ブロッキング(ロック競合)が発生していたと考えられるため、Spotlightでブロッキングのグラフを確認しました。 ブロッキングが大量に発生しています。ブロックされているプロセス(オレンジ色)が多数あるのに対して、ブロックを引き起こしているプロセス(青色)の数は多くありません。少数のプロセスがその他大勢のプロセスをブロックしていることが分かります。ブロッキングの情報をさらに追っていきます。 ↑13時15分時点のブロッキング情報の一部です。Head Blocker(黄色線の情報、ブロッキングを発生させる原因となっているプロセス)のStatusが「Seeping, blocking」となっています。つまり、スリープしているプロセスが、大量のプロセスをブロックしていたことになります。「スリープしてるのにブロックしてる」とはどういう状況なのだろうと調べてみたところ、同じような症状に関する記事を見つけました。 Sleeping SPID blocking other transactions この記事によると、明示的なトランザクションを張ったまま途中でタイムアウトしてしまうと、クエリの実行方法によっては、ロールバックされずトランザクションが継続された状態になります。したがってロックを獲得している場合はロックも保持し続けることになります。ローカル環境にて実験したところ、確かに再現しました。 ただし、ローカル環境で試す中でタイムアウト後に即座にロールバックされる場合もありました。調べたところ、明示的にトランザクションを開始し、ロックを保持したままタイムアウトした場合、そのロックの解放タイミングは「コネクションプールに戻ったコネクションが別のクエリによって再利用されるとき」でした。それは0.1秒後かもしれないし、数分後かもしれません。 ※コネクション/コネクションプールの仕組みについては この記事 で非常に詳しく書かれていました。 昨年の福袋イベントのケースにあてはめて考えると、大量にアクセスされていた状況ならすぐにコネクションが再利用されることでロックが解放されてもおかしくないのでは?という疑問がわきます。この疑問を検証するために実験を行ったところ、ロールバックされるためには、コネクションの再利用時にまずワーカースレッドを確保する必要があるとの結論に至りました。 これらの調査を踏まえて、福袋発売後に以下のシナリオが発生してバッチ実行数が著しく低下したと考えました。 1.購入フローにおいて、人気商品に関するデータへの読み取り要求、更新要求が大量に発生。 それに伴い特定のレコードに対してのロック、ページラッチの獲得待ちが発生。 2.人気商品データのロックを獲得したプロセスAが処理の途中でタイムアウト。 ロールバックされず、取得したロックが解放されないままコネクションプールに戻る。 3.別プロセスが該当コネクションの再利用を試みるも、 ワーカースレッド枯渇のためTHRADPOOL waitが長時間発生してエラー発生。 そのためロールバックとロックの解放が長時間おこなわれなかった。 4.プロセスAが保持したロックによって、他のプロセスが大量にブロックされる。 (複数の人気商品において2~4の事象が発生) 5.ブロッキングによって各プロセスの実行速度が急速に低下。 ワーカースレッドの解放が遅くなり全体としてスループットが低下した。 昨年の障害発生原因を踏まえた対応を実施 ここまでのまとめ ・昨年の福袋イベント中にエラーが多発したのは、DB内部でワーカースレッド確保待ちが大量発生したため ・ワーカースレッド確保待ちが大量発生したのは、人気福袋データに対するクエリ実行要求が集中し、かつタイムアウトしたプロセスがロックを解放しないことでブロッキングが急激に増大したため ・昨年の障害発生時には、以下の五項目の待ち時間が圧倒的に多かった THREADPOOL LCK_M_U LCK_M_S PAGELATCH_SH PAGELATCH_EX 以降では、昨年の障害調査を踏まえて実施した負荷対策についてまとめます。 対策1. プロセスをSleepingかつBlockingにさせない タイムアウトしたプロセスがそのままロックを掴みっぱなしになっていたことが、ブロッキングの状況を加速度的に悪化させた原因のため、タイムアウト時のトランザクションを適切に処理する必要があります。 対応方法としては、 SET XACT_ABORT ON をトランザクション開始前に設定しました。このオプションは、トランザクション内でエラーが発生すると即座にロールバック+ロックの解放を行うように指示できるオプションです。 この対策を実施することで、特定のクエリがタイムアウトした途端にブロッキングが加速するというリスクを無くすことができます。 対策2. 昨年多く発生していた待ち事象を減らす ■ THREADPOOL この待ち事象は、ブロッキングが大量に発生したためにワーカースレッドの解放が遅れたことが原因と考えられるため、THREADPOOL Waitを減らすためには、ブロッキングを減らす必要があります。ただし、万一ブロッキングが大量発生する等の理由でワーカースレッドが枯渇するときのことを考慮し、ワーカースレッドの最大数を2倍に増やしました。合わせて、maxサーバーメモリの値を減少させました。これは、ワーカースレッドの確保にはメモリが必要となり、このメモリはバッファプール用に確保しているメモリ(maxサーバーメモリ)とは別の領域から確保する必要があるためです。 そのため、maxサーバーメモリを減少させ、ワーカースレッドを作成する際に必要なメモリを確保できるように、メモリサイズの調整を行いました。 この対応により、THREADPOOL Waitを削減できるわけではありませんが、大量にワーカースレッドを使用する環境下においては同時実行性能の向上が期待できます。 ■ LCK_M_U 購入フローの一部にDB上の在庫に関するデータを更新する箇所があり、人気商品の購入希望者が殺到すると、人気商品の在庫に関するデータレコードの更新処理で大量のブロッキングが発生していました。これを軽減させるために、人気商品に関しては、サイト上の見え方およびユーザーには影響ない形で在庫に関するデータを分割して販売しました。 ■ LCK_M_S 一部のクエリにwith(nolock)というロックヒントを付与することで、Sロックよりも競合が少ないSch-Sロックを取得するに留めさせました。もちろん、ダーティリードを許可できる箇所である、というのが前提となります。 ■ PAGELATCH_SH / PAGELATCH_EX 獲得できるページラッチは1ページあたり1つだけのため、同一ページ内の異なるレコードへの更新要求同士であってもページラッチの競合は発生します。そのため、1ページの中に福袋商品の在庫に関するデータレコードが1つだけ存在する状況を意図的に作り出しました。具体的には、福袋商品以外のレコードは、サイト上からは見えないダミーの在庫に関するデータレコードで埋めました。これにより、ページラッチ競合の改善が期待できます。 これらをすべて対応した際のイメージを以下の図にまとめました。 昨年の状況と比較すると、以下のように読み取り要求、更新要求に伴う排他制御の分散が期待できます。 当日のモニタリング環境の整備 昨年の障害時はワーカースレッドが枯渇していたため、クライアントソフトから情報収集のためのクエリを実行することすらできない時間帯がありました。そこで今回は、ワーカースレッドの枯渇に備えて、 DAC で事前に接続しておき、いかなる状況でもDBから情報を採取できる体制を整えておきました。DACで接続すると、専用のワーカースレッドをつかえるので、ワーカースレッド枯渇時でもクエリが実行できます。今年はワーカースレッドが枯渇することはありませんでしたが、準備しておいたことでいつでも情報取得できるという安心感につながりました。 今年の福袋イベントについて 本記事で紹介した負荷対策を実施したことで、今年は福袋イベントを障害無しで乗り切ることができました。 以下のグラフは、去年と今年の福袋イベントにおける、分間注文数の推移をグラフにしたものです。 今年は、開始と同時に瞬間的に注文数が跳ね上がり、その後ゆるやかに減少しています。今年はほぼエラーが起きていないため、あるべき注文数の推移であり、お目当ての商品をスムーズに購入していただけたようです。昨年と今年の波形と比較すると、昨年は障害発生によって注文数の波形が不自然であることがよくわかります。 また、DB起因のエラーも、昨対比で99.99%以上削減でき、平常時とほぼ同じようなサーバー負荷状況でした。 まとめ 本記事では、昨年障害を発生させてしまった福袋イベントについて、障害原因に関する調査内容をご紹介しました。また、調査結果を踏まえて今年実施した負荷対策についても紹介しました。 人気商品にアクセスが集中するという特定の性質をもったワークロードにおいて、同時実行性能をできる限り向上させることに注力したことが功を奏しました。性質が異なれば実施すべき対応策も変わってくるため、ワークロードの性質を適切に把握することと、障害発生時はできる限り根本的な原因を特定することが、何よりも大切だなと感じました。 今年はお客様にストレスなく福袋イベントを楽しんでいただけたと思いますので、エンジニア一同がんばってよかったです! ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://www.wantedly.com/companies/zozo-tech/projects
こんにちは、ZOZOテクノロジーズ VPoEの今村( @kyuns )です。 この記事は ZOZOテクノロジーズ Advent Calendar の25日目の記事になります。 今年の4月にスタートトゥデイテクノロジーズ(現ZOZOテクノロジーズ)が発足してから約8ヶ月が経ちました。新型ZOZOSUITやプライベートブランド「ZOZO」の発表など今年は色々と新しいチャレンジをしていた弊社ですが、外から見たときにエンジニア観点だとまだまだ謎めいている部分がたくさんあると思います。 ちょうど先日代表の前澤がツイートしたことでTwitter採用が話題になりましたが、反響も非常に大きく、多くの方にご応募いただき実際に何名かのエンジニアを採用することができました。 news.yahoo.co.jp この祭りでも 非常に多くの質問 をいただきましたが、このエントリではZOZOテクノロジーズが行っている事業やプロダクトの話を交えつつ、ZOZOテクノロジーズが求めているエンジニア職を改めて紹介したいと思います。 この記事が少しでも弊社を知ってもらう機会となれば幸いです。 ZOZOテクノロジーズとは? 株式会社ZOZOテクノロジーズは株式会社ZOZOの100%子会社です。 ZOZOとZOZOテクノロジーズ、それぞれ主な役割が違っており、ZOZO本体にはZOZOTOWNを運営するために必要なスタッフ、例えばEC営業・マーケティング・カスタマーサポート・ZOZO BASEなどの物流まわりのスタッフやプライベートブランドの服作りをするスタッフなどが在籍しています。 一方ZOZOテクノロジーズにはWebサイトやアプリ、システム開発を行う為に必要なスタッフ、エンジニア・デザイナー・アナリスト・リサーチャーなどが在籍しています。 2018年12月25日時点ではZOZOテクノロジーズには約250名の従業員が在籍し、そのうちエンジニアは約150名ほどになります。 企業理念、事業理念、経営理念 我々の企業理念はZOZO本体と同じく 「世界中をかっこよく、世界中に笑顔を。」 テクノロジーズの事業理念は 「70億人のファッションを技術の力で変えていく」 です。 事業理念が示すとおり、我々の対象は世界70億人、そして領域はファッション、その手段として技術を重要視しています。 経営理念は 「いい人をつくる」 です。 ここでいういい人とは以下の3つを満たした人物です。 想像力と創造力に富んだ人 GIVE&TAKEのバランスがとれた人 いい人を作れる人 また、 前澤代表のメッセージ にもあるように 我々は 「楽しく働く」 ことを徹底するような行動を心がけています。 どんな事業が存在するのか? 現在ZOZOテクノロジーズが関わる主な事業は以下の4つになります。 日本最大級のファッションECサイト「ZOZOTOWN」 自分サイズのベーシックアイテムブランド「ZOZO」 日本最大級のファッションコーディネートアプリ「WEAR」 ファッションに関する研究開発を行うZOZO研究所 それぞれの事業が行っているプロダクト開発の内容を、どのようなエンジニアが向いているのかということも含めて紹介していきたいと思います。 ZOZOTOWN ZOZOのメイン事業である日本最大級のファッションショッピングサイトです。 年間購入者数は730万人以上と非常に多くのユーザーに利用されています。 ZOZOTOWNの特徴としてはシステム開発・デザイン・物流など、ECに関わる機能を全て自前で開発しています。そのため色々な箇所でエンジニアの力が必要になってきます。 募集職種 ZOZOTOWN SRE/インフラエンジニア(オンプレミス) ZOZOTOWNおよびWEARのオンプレのインフラを支えるエンジニアになります。 現在ZOZOTOWNのインフラのほとんどはオンプレに存在しています。画像サーバーは一部AWSに置くなど、クラウドへの移行も進めていますが、まだまだオンプレの役割も重要です。そのため、オンプレに詳しいインフラエンジニアが必要となります。オンプレでの運用経験があってハードウェアやネットワークに詳しいけど、クラウドにも興味があるというようなインフラエンジニアが非常に向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server / 仮想化基盤 / 各種NW機器 ZOZOTOWN フロントエンドエンジニア(マークアップ) ZOZOTOWNのサイト改修やコーディングを行うエンジニアです。フロントエンドには現在ZOZOTOWNのUI改善や運用を行うチームと、特集などの企画ページの開発を行うチームがあります。ZOZOTOWNのトラフィック規模になると少しのUI改善が売上に直結します。たとえ0.1%の改善でも売上への影響は非常に大きく、様々なUI改善の施策をやりがいをもって開発することができます。また後者のチームでは、非常に多くの企画や施策を行います。しかもその企画を通した売上規模は何十億円となることもあります。どちらのチームともUI/UXにこだわりをもって開発していけるようなエンジニアが向いています。 また、モダン化も並行して進めているので、今のZOZOTOWNのフロントエンドをもっと新しくパフォーマンスよくしたい!という方、大歓迎です。 利用技術: HTML / CSS / SASS / JavaScript(ES5) / jQuery ZOZOTOWN フロントエンドエンジニア(iOS/Android) ZOZOTOWNのiOS/Androidアプリの開発を担当するエンジニアです。 利用ユーザーも非常に多く、どの画面をとってもUI/UXの改善が使い勝手や売上に直結します。 また、QAエンジニアと連携してテストの自動化などを行ったりもしており、より効率的な開発を行えるような基盤づくりなどもしています。とにかくこだわりを持って大規模ECサイトのアプリを開発したいアプリエンジニアが向いています。 利用技術: iOS: Xcode / Objective-C / Swift / Carthage / Bitrise Android: Android Studio / Java / Kotlin / CircleCI ZOZOTOWN バックエンドエンジニア(Web/API) ZOZOTOWNのサーバーサイドの開発を行うエンジニアです。サーバーサイドといっても非常に多くの機能があり、既存機能の改修や新機能の開発、iOS/Androidアプリに必要なAPIの開発なども行います。 商品検索やカート、決済処理部分など、ZOZOTOWNの非常に重要な部分を担う開発を担当しています。 商品検索を改善していきたい、様々なミドルウェアを扱いたい、APIの設計にこだわりをもってやりたい、と思えるようなエンジニアが向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server / ElasticSearch ZOZOTOWN バックエンドエンジニア(リプレイスプロジェクト) ZOZOTOWNの既存システムをクラウド移行するための開発を行うエンジニアです。 凄まじい勢いで成長してきたZOZOTOWNは、VBScriptを用いた開発を始めとして、システム的にもレガシーな部分が存在しているのが事実です。 そのため、このような技術的負債に対応するために将来的な拡張性や柔軟性の確保を見据えて、既存のオンプレ環境をパブリッククラウドへと移行するプロジェクトを進めています。単純にクラウドへ移行するといってもその規模は巨大ですし、システムの停止は極力避けなければなりません。 大規模プロジェクトになるため、いくつかのフェーズに分けて現在取り組んでいます。 アーキテクチャとしてはDocker/Kubernetesを中心としたクラスタ構成になっており、セールなどの負荷にも柔軟に耐えれるようなAPIの開発を行っています。 モダン技術を用いて巨大なAPIやインフラ基盤の開発を行いたいエンジニアが向いています。 利用技術: SQL Server / Java / Spring / Azure / Docker / Kubernetes / NewRelic / Wercker / SonarQube / Datadog / PagerDuty / Prometheus / Sentry リプレイスまでの道程や詳細については、 今月号のWEB+DB PRESS Vol.108 で特集されていますので、気になった方はぜひご覧ください。 ZOZOTOWN バックエンドエンジニア (マーケティングオートメーション) 何百万人ものZOZOTOWNのユーザーとの接点を受け持つ部分の開発を支えるエンジニアです。 プッシュ通知やメルマガ配信、LINE@のメッセージ配信など、ユーザーとのコミュニケーションを技術の力でより良くしていく部門であり、A/Bテストやデータを用いたレコメンデーションなどを行います。 ユーザーの行動に応じて、様々なチャネルで、最適な情報を届けるためのリアルタイムマーケティングシステムと呼ばれるシステムの開発も行います。 また、社内に存在するあらゆるデータを集約させるためのデータ基盤の開発も行っています。 データ基盤にはBigQueryを用いており、fluentd・Embulk・Digdagなども活用しています。 ここに集められたデータはサービスの改善や研究開発などに利用されます。 データの規模も非常に大きく、何百億レコード、数百テーブル、リアルタイムで飛んでくるイベントログなどをうまくさばく技術が必要になります。 データをつかってユーザーに最適な情報を届けたい、ユーザーとの接点をより良くしたい、大量のデータをうまく扱えるような巨大データ基盤を開発したい、と思うようなエンジニアが向いています。 利用技術: AWS / GCP / JBoss Data Grid / Aurora / Java / Ruby / fluentd / Embulk / Digdag ZOZOTOWN バックエンドエンジニア(基幹システム) ZOZOTOWNのバックオフィスと呼ばれる裏側を支えるシステムの開発を行うエンジニアです。 主に、社内や取引先に開放している販売管理システムや物流業務システムの企画・開発・改修やブランド様などからの問い合わせ対応などを行います。また、弊社は物流ロジスティクスも自前で保有しているため、エンジニアリング視点でどのオペレーションや業務を効率化できるか、という視点がとても重要になります。他社とのデータ連携などを受け持つデータ連携基盤の開発なども行っています。 ZOZOTOWNを支える裏側の仕組みに興味がある人、倉庫や物流などのオペレーションを技術で改善していくことに興味のあるエンジニアが向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server ZOZOTOWN ロボットエンジニア(BASE/倉庫) ZOZOTOWNの物流倉庫で利用予定のロボットの開発を行うエンジニアです。既にある程度自動化されているZOZOBASEですが、さらなる自動化のために各種ロボットの導入を検討しています。様々な最適化のためにシュミレーションを行ったり、実際にロボットの試作機の開発などを行います。ハードウェアやロボット開発に興味のあるエンジニアが向いています。 利用技術: C / C++ / Python / CAD / Solidworks プライベートブランド「ZOZO」 プライベートブランド「ZOZO」(以下PB)に関する事業です。 PBはZOZOSUITを用いて計測した、ユーザーのサイズにジャストフィットなベーシックアイテムを販売する事業になります。現時点ではTシャツやデニム、シャツやビジネススーツ、ニットなど様々なアイテムを販売しています。PBは日本以外の国でも販売を開始しており、海外向けのグローバルECサイトzozo.comをはじめとしてZOZOSUIT、スマートファクトリーなど最新の技術が活用されるプロダクトになります。 PB事業には以下のプロダクトが存在します。 海外向けグローバルECサイト zozo.com ZOZOSUIT 生産管理/ERP 生産管理/生産マネジメント パターン自動生成 スマートファクトリー PB事業ではウェブサイトだけでなく、ZOZOSUITやスマートファクトリーなど、ソフトウェアだけでなくハードウェアの技術なども必要とされているのが特徴です。 海外向けグローバルECサイト zozo.com プライベートブランド「ZOZO」を海外に向けて販売していくためのサイト「 zozo.com 」の開発を行います。海外の拠点とやり取りする機会も多く、業務内容によっては海外出張に行くこともあります。 募集職種 グローバルECサイト フロントエンドエンジニア(Web) グローバルECサイトのフロントエンドの開発を行います。グローバルECサイトのフロントエンドは現在TypeScriptとVue.jsを用いたSPAで作られており、多言語対応や決済機能まであるSPAの規模としても非常に大きいものになります。また、現在Nuxt.jsを用いたリニューアルを行うなど、フロントエンド周りの新しい技術や海外事業に興味のある人が向いています。 詳しくは こちらのスライド をご覧ください。 利用技術: HTML / CSS / Vue.js / Vuex / webpack / TypeScript / Nuxt.js グローバルECサイト フロントエンドエンジニア(iOS/Android) プライベートブランド「ZOZO」のiOS/Androidアプリの開発を行います。世界中で使われるアプリのため、多言語対応などももちろんのこと、使いやすいUI/UXを考慮しながら開発する必要があります。世界で使われるアプリを開発してみたいというアプリエンジニアが向いています。 利用技術 - iOS: Xcode / Objective-C / Swift / Carthage - Android: Android Studio / Java / Kotlin グローバルECサイト バックエンドエンジニア(API) グローバルECサイトのサーバーサイドの開発を行います。フロントエンドがSPAなのでAPIサーバーを主に作ることになりますが、グローバルECサイトなので国際化対応に紐づくような決済、通貨、サイズ、配送など様々な事象を考慮したサイトの開発を行う必要があります。サーバーサイド側のAPIは Scala / Playで開発されており、Scalaを用いたマイクロサービスの開発に興味があるエンジニアが向いています。 利用技術: AWS / Aurora / Postgres / DynamoDB / Scala / Play Framework / nginx / Sentry / CircleCI グローバルECサイト インフラエンジニア/SRE(クラウド) グローバルECサイトやZOZOSUITの計測サーバーのインフラの開発および運用を行います。世界展開を考慮した上でのインフラ設計やマイクロサービス実現のためのインフラの構築を行います。現在はすべてAWSで動いています。グローバル展開しているサイトのインフラに興味がある人が向いています。 利用技術: AWS / Aurora / Postgres / DynamoDB / Scala / Play Framework / nginx / Sentry / CircleCI ZOZOSUIT ZOZOSUITの開発や計測精度向上などを行います。 募集職種 ZOZOSUIT バックエンドエンジニア(ZOZOSUIT) ZOZOSUITの計測データを受け取るサーバーの開発を行います。世界中から集まるZOZOSUITのデータを安全に管理し、様々なチームと連携しながら安全にデータが利用できるようなAPIの開発を行います。ZOZOSUITに興味のあるエンジニアがとても向いています。 利用技術: AWS / Aurora / Python / Flask ZOZOSUIT フロントエンドエンジニア(iOS/Android) ZOZOSUITの計測SDKの開発を行います。様々なチームと連携しながらZOZOSUITの計測精度の向上なども行っています。ZOZOSUITの計測精度向上に興味があるアプリエンジニアがとても向いています。 利用技術 - iOS: Xcode / Objective-C / Swift / Carthage / Bitrise - Android: Android Studio / Java / Kotlin / CircleCI 生産管理 バックエンドエンジニア(ERP) プライベートブランド「ZOZO」の生産管理や在庫管理、会計管理などを行う独自のERPの開発を行います。サイトとの連携APIだけでなく工場とのデータ連携など、非常に多岐にわたるシステムの開発を行います。生産の裏側を技術で支えていきたいエンジニアが非常に向いていると思います。また、このチームではGoをメイン言語として利用しています。 利用技術: AWS / Postgres / Go / Docker 生産管理 インフラエンジニア/SRE (ERP) プライベートブランド「ZOZO」の生産管理システムのインフラの開発、運用を行います。日本国内だけでなく海外の工場との連携などもあるため、世界各国からの利用を意識したインフラの構築を行う必要があります。世界を股にかけるインフラ基盤を構築してみたいインフラエンジニアが向いていると思います。 利用技術: AWS / Postgres / Go / CloudFormation / Docker 生産管理 バックエンドエンジニア(生産マネジメント) プライベートブランド「ZOZO」の生産工場へのシステム導入支援や、プロジェクトマネジメントを行います。海外にある生産工場に出向いて、工場へのシステムの導入支援や運用の改善、管理システムの開発などを行います。基本的に海外出張が多いので海外に興味があり、工場の効率化や自動化に興味のあるエンジニアがとても向いています。 利用技術: AWS / Postgres / Go / CloudFormation / Docker パターン自動生成 エンジニア(パターン自動生成) ZOZOSUITで計測したデータを用いて、ユーザー一人一人に応じた最適なサイズの型紙(パターン)を自動生成する完全オーダーメイド技術の開発を行います。エンジニアとして服作りを勉強してみたいエンジニアがとても向いています。 利用技術: 言語非公開 / 服作りの知識 / CAD スマートファクトリー 自動縫製工場におけるファクトリーオートメーション化の機械制御アプリケーションの開発や自動裁断機、ミシン、ハンガーラインなどの制御プログラムの開発、社外のハードウェアと連携したアプリケーションやIoTプログラムの開発を行います。 募集職種 スマートファクトリー バックエンドエンジニア(Web/アプリケーション) PB「ZOZO」の縫製工場におけるファクトリーオートメーション化にむけたアプリケーションの開発/運営を行うエンジニアです。システム設計、インターフェース設計、インフラ設計、データベース設計、コード設計、実装、レビューなど、スマートファクトリーの実現に関する非常に幅広い業務を担当していただきます。 WebやIoTなどに興味のあるエンジニアが向いています。 利用技術: AWS / Go スマートファクトリー バックエンドエンジニア(制御系) ファクトリーオートメーション化における機器を操作するためのアプリケーション(ミドルウェア)を開発するエンジニアです。モーターやセンサーなどを駆使して生産ラインの自動化を洋服のプロたちと主に目指します。Webシステムから自動裁断機、ミシン、ハンガーラインをコントロールするためのシステム全般業務をしていただきます。ロボットアームや、センサー制御など、ハードウェアの知識も必要になります。色々なメーカー出身の方々が集まってスマートファクトリーの実現を目指しています。回路設計やロボットアームの制御など、ハードウェア経験があって新しいことを実現したいエンジニアが向いています。 利用技術: C / C++ / Go / ROS WEAR 日本最大級のファッションコーディネートアプリです。世界1200万ダウンロードを超え、さらなる成長を目指しています。投稿されているコーディネートの投稿数も800万点以上、MAUが1100万と巨大なファッションメディアになります。また、10万人以上のフォロワーを持つWEARISTAと呼ばれるユーザーがいたり、高橋愛さんや吉岡里帆さんなどをはじめとした 多くの芸能人 にも利用されています。 募集職種 WEAR フロントエンドエンジニア(マークアップ/Web) WEARのPC/SPサイトのフロントエンドの開発を行うエンジニアです。WEARは多くの面白い特集や企画なども頻繁に行っています。またチームメンバーにはWEARがとにかく大好きなエンジニアが多く、ファッションメディアサービスを開発したいエンジニアが非常に向いています。 利用技術: HTML / CSS / JavaScript / jQuery / Windows Server(IIS) / VBScript / SQL Server WEAR フロントエンドエンジニア(iOS/Android) WEARのiOS/Androidアプリの開発を行うエンジニアです。ベストアプリも受賞したことのあるアプリになりますので、UI/UXを非常に大事にしています。メディア系サービスのアプリ開発を行いたいエンジニアが向いています。 利用技術: iOS: Xcode / Objective-C / Swift / Carthage Android: Android Studio / Java / Kotlin WEAR バックエンドエンジニア(Web/API) WEARのサーバーサイド側の開発を行うエンジニアです。います。コーディネートの検索機能の改善を行ったり、アプリのAPIの開発などもやコーディネートの検索をより使いやすくしたり、ユーザーファーストな開発を行っています。 利用技術: Windows Server(IIS) / VBScript / SQL Server WEAR バックエンドエンジニア(リプレイスプロジェクト) こちらも同じくサーバーサイドですが、現在のWEARプロジェクトを新しいアーキテクチャでリプレイスする開発を行っています。現在ZOZOTOWNやWEARはVBScriptで書かれていますが、ZOZOTOWN側にもリプレイスプロジェクトがあるように、こちらWEAR側でもRubyを用いたリプレイスプロジェクトを進めています。大規模メディアサイトをRailsで開発したいエンジニアが向いています。 利用技術: AWS / Ruby / Ruby on Rails / Aurora ZOZO研究所 ( ZOZO RESEARCH ) ZOZOテクノロジーズの研究開発部門として今年の1月に「ファッションを数値化する」をミッションとしてZOZO研究所は設立されました。 現在は青山および福岡に拠点が存在し、約20名程度のメンバーが在籍しています。 ZOZOのビッグデータを用いた研究開発や、機械学習、深層学習を用いたアルゴリズムの開発、新しい技術を用いたプロトタイプの開発など様々な活動を行っています。 現在ZOZO研究所では主に3つのテーマを軸に研究開発を行っています。 最近は コーデ生成に関する論文を投稿 したり、 海外カンファレンスでも発表 したりする機会が増えてきました。世界一のファッション研究所にすべく、新しいチャレンジを沢山行っているチームになります。 募集職種 リサーチャー ファッションに関する技術の研究開発を行うエンジニアです。研究領域の対象はECサイトの利便性向上だけでなく、物流最適化、ZOZOSUITに代表されるような計測デバイスなど、非常に多岐にわたります。リサーチサイエンティストは様々な課題に対し、各自が最も興味のある対象と得意とする手法を選択して研究に取り組むことができます。 有効性が確認された研究成果はジャーナルや国内外のカンファレンス等での発表以外に、エンジニアやデザイナーと協力しながら実際のサービスに導入します。ZOZOにしか出来ない研究を行い、世界をリードするような研究成果を出していきたい人が向いています。 利用技術: 機械学習(MachineLearning) / 深層学習(DeepLearning) / Python / Chainer / TensorFlow / PyTorch / AWS / SageMaker / GCP / GPU / TPU MLエンジニア(機械学習/深層学習) リサーチサイエンティストとともに、ZOZOTOWNやWEARから得られるビッグデータを使い、ユーザークラスタリング、画像認識、画像生成タスクなどの機械学習・深層学習を用いた実装を行うエンジニアです。 アルゴリズムの開発、実装だけでなく、サーバー側への実装なども行います。自らの技術を用いて、ZOZOTOWNや事業に貢献したいエンジニアが向いています。 利用技術: 機械学習(MachineLearning) / 深層学習(DeepLearning) / Python / Chainer / TensorFlow / PyTorch / AWS / SageMaker / GCP / GPU / TPU リサーチエンジニア(最新技術) 今後流行するであろう技術を用いたPoCの開発などを行うエンジニアです。現在だとAR/VRなどを用いた3D試着やGoogle HomeやAmazon Echoなどのスマートスピーカーを用いたPoCの開発など様々な開発を行っています。未来のサービスのベースとなるようなコンセプトを、新しい技術やZOZOの資産を活用しつつ実現するエンジニアになります。Webやアプリ、サーバーサイドなど、幅広い技術や最新技術に興味関心のあるエンジニアが向いています。 利用技術: Web / iOS / Android / GCP / AWS / Google Assistant / Unity その他 上記で紹介した4つの事業以外にもエンジニアを募集しています。 募集職種 コーポレートエンジニア(社内情報システム) ZOZOグループの社内インフラや社内システムの開発、設定、運用を担当していただきます。オフィス各拠点だけでなく、各倉庫の拠点のための社内ネットワークやセキュリティ、デバイス管理やグループウェアなどその業務領域は非常に多岐に渡ります。自らの技術や働きによって、社員が業務を効率的に行えるために何ができるか、を自ら考え提案および改革を推進できる必要があります。現在はG Suiteへの移行やリモートワーク環境の整備なども行なっています。技術で社員を裏側から支えたいエンジニアが非常に向いていると思います。 利用技術: Azure Active Directory / Office 365 / G Suite / Firewall / 拠点間NW構築 / Cisco / リモートワーク関連技術 / ISMS / GDPR 20年度新卒エンジニア ZOZOテクノロジーズでは20年度卒の新卒エンジニアを募集しています。 フロントエンド、サーバーサイド、インフラ、iOS/Android、データサイエンティストなど幅広いエンジニアやデザイナーを募集しています。 労働環境について さて、ここまで30職種近いエンジニア職を紹介してきました。働く上で仕事内容も重要ですが、労働環境も重要です。ここからはZOZOの労働環境について紹介していきたいと思います。 勤務場所 ZOZOテクノロジーズのオフィスは現時点で青山、幕張、福岡の3拠点が存在します。 担当するプロダクトによって勤務場所が変わりますが、ざっくり分けると現時点では以下のような形です。 青山 PB(グローバルEC) / ZOZOTOWN(アプリ) / 新規事業 ZOZO研究所(青山) 幕張 ZOZOTOWN(アプリ以外) / WEAR / PB(生産) 福岡 ZOZO研究所(福岡) よく幕張勤務ですか?と尋ねられますが、担当するプロダクトによって変わるため必ずしも幕張というわけではありません。幕張と青山の人数比率はおおよそ6:4です。 勤務体系や働き方など ほとんどの社員はフレックスタイム制(コアタイム10:00〜17:00)になっており、 一部の専門スキルを持っているスペシャリストおよび研究所メンバーは裁量労働制となっています。 子供がいるママさんなどは時短勤務の制度がありますので、そちらを利用して働いています。ちなみに弊社は出産された方の産休復帰率が100%となっており、子供を持つ母親としても非常に働きやすい環境となっています。 また、リモートワークに関しては現時点では正式に制度としては存在していませんが、現在環境を整えている最中です。世の中の働き方が多様化する中、しっかりと弊社でもリモートワークができる体制や制度を整える必要があると感じており、着々と準備を進めています。 給与 一番良く質問されるのが「給与って全員一律なんでしょう?」という質問です。 この問いに対する答えは半分正解、半分間違いです。 現在、給与は 基本給+職能給+賞与+各種手当 という構成になっており、基本給および賞与は全員一律同じですが、職能給は各々によって違います。 職能給はいわゆる給与テーブルによって定義されており、年2回の人事考課面談によって決定されます。給与の上限は、天才枠採用の場合最大1億円となります。 あくまでもその人のスキルや実績に合った適切な報酬を支払う、というスタンスです。 環境、福利厚生、制度・手当等 ZOZOでは社員がパフォーマンスを最大限発揮できるための環境を最大限用意できるようにしています。特徴的なものをいくつか紹介いたします。 支給マシン エンジニアは入社の際に希望のPCスペックを選ぶことができます。MacやWindows、iMacやMacBookProなど、モニターやキーボードなど必要に応じたマシンやスペックを選択可能です。 公休 会社としての休みは土日・祝日、育休、慶弔などがあります。有給は試用期間終了を待たずとも、入社日に10日間付与されます。(付与日数は入社月によって変動) 夏休み、冬休みがともに3日間存在します。有給とあわせてまとめて1週間ほど休んで旅行に行く人などが多い印象です。 住宅通勤手当 住宅通勤手当として全員に月5万円が支給されます。会社の近くに住めば実質5万円がまるまる住宅補助となります。 家族手当 家族がいる人には家族手当として扶養家族一人あたりにつき月5,000円が支給されます。 社員割引 自社サービスを積極的に使ってもらう為にも、ZOZOTOWNの商品を社員特別価格で購入することができます。 退職金制度 3年以上在籍している社員には退職金が在籍年数に応じて支払われます。社員にはZOZOで長く働いてほしいという思いから、このような制度があります。 書籍購入補助 書籍購入の補助が全額出ます。事前の申請などは必要なく、上限も今の所存在していません。ルールとしてあるのは、本のレビューをslackに投稿するだけです。 カンファレンス参加費用補助 国内外のカンファレンスへの参加費用を全額負担します。例えば今年はGoogle I/O、WWDC、AWS Re:invent、Google Cloud Nextなど、海外の主要なカンファレンスに参加しています。航空券やホテル代などは全て会社が負担しますし、さらに出張手当として1日あたり一定の金額が支給されます。 社会人ドクター支援制度 ZOZO研究所の研究に関連する分野において、博士号の取得を目指す方を対象とした制度です。共同研究先との研究開発及び修学を優先的な業務とし、給与の支給のほか修学費用を支援します。これにより働きながら学位取得を目指すことができるため、経済的な不安解消と研究時間の確保によるスキル向上が可能となります。 副業OK 本業に支障をきたさないという前提で、会社として副業を許可しています。個人サイト運営、アプリ開発、友達の手伝い、書籍執筆など自らの可能性を広げるための活動を行っている人達もいます。 この他にも現在様々な制度を整備している最中です。どうすれば今よりももっと生産性が高く、より効率的に楽しく働ける環境にできるかどうか、日々模索しています。 テックカンパニーとしてのZOZO ZOZOテクノロジーズが誕生してまだ8ヶ月、僕自身がZOZOのエンジニアリング組織を任されるようになってからまだ日が浅いですが、着実にテックカンパニーへと生まれ変わろうとしています。 ZOZOにJOINしてから感じていることとして、この会社はZOZOSUITのように理想や夢を現実のものにできる、そんな素晴らしい力を持ったエンジニア達がたくさんいます。 テックカンパニーとしてのビジョンを描き、仕組みや制度を整えることによって、確実にもっと強いエンジニアリング組織にできるという確信があります。どういう取り組みを行っているかなど、ZOZOのエンジニアリング組織についてのお話はまた後日詳しく紹介できればと思います。 まとめ ここまで紹介してきたとおり、ZOZOでは非常に多様なエンジニアを募集しています。 言語もインフラも、ソフトウェアだけでなくハードウェアまで様々な技術スタックを活用してサービスの開発を行うことができますし、皆さんの技術を活かせる仕事が必ずあると思います。 ファッションの世界を技術で変えていくことに興味のある方はぜひ応募してみてください。 この記事を読んで、ZOZOに応募してみたいと思ったエンジニアの方は下記の採用ページからぜひご応募ください。その際に「テックブログみました」と書いていただけると幸いです。 tech.zozo.com 2019年も世間をあっと驚かせるようなプロダクトを出していきたいと思います。 私達と一緒に楽しく働いてみませんか?
こんにちは。そろそろ生後7か月になる息子が可愛くて仕方がないうっちー(@k4ri474)です。 12/10〜12/13に開催された KubeCon+CloudNativeCon へ参加してきました。 大型カンファレンスらしい演出のKeyNoteやハンズオンセッション、プレゼンテーションなど盛り沢山なイベントでした。 今回、僕はKubernetes運用経験やDockerを用いた設計・開発の経験がほとんどない状態で参加しました。 現状はチーム・個人として業務の中心にはなっていないものの、今後の技術選定に関わるタイミングで選択肢としてKubernetesを持てるようにキャッチアップしておきたかったというのが理由です。 知っていて選定しないのと、知らずに選定できないのとでは大きな違いがあると思っています。 カンファレンスはKubernetesの盛り上がりを肌で感じて手を動かして身につけるキッカケにしようと意気込んで申し込みました。 本記事では、ビギナー目線で印象に残ったセッションをメインにしていくつかご紹介します。 KeyNoteは後ほど動画がアップロードされるかと思うので取り上げません。また、セッションのスライドURLを貼りましたが、埋め込みできる形式でアップロードされておりませんでしたので、リンクを開いてご覧いただければと思います。 KubeCon+CloudNativeConとは コンテナオーケストレーションツールであるKubernetesと、CNCFがホストするPrometheusやEnvoyなどのOSSについて、最新技術やユースケースなどが紹介されるカンファレンスです。 概ねKeyNoteから始まってプレゼンテーション・ハンズオンを挟み、KeyNoteで終わるといった構成になっています。 KeyNoteの時間以外は同時刻に15ほどの別々のテーマの発表が行われていて、参加するセッションを自由に選択する形式です。 また、常時スポンサーブースが開設されていて、営業・広報担当者だけでなく時には開発者と直接会話できます。 印象に残ったセッション You can't have a cluster[bleep] without a cluster You can't have a cluster[bleep] without a cluster heptioのSenior Developer Advocateによる、Kubernetes自体の複雑さ・リスクを認識した上でどのような取り組みをして乗りこなしていけばよいか、というセッションでした。 Kubernetesの望ましい特徴として回復性・効率性・反復性が挙げられるが、複雑性やリスクも内包している、ということが最初に強調されました。 Kubernetesはアプリケーション・OS・ハードウェアなど多くのコンポーネントに対して干渉できる故、把握すべき範囲が広がっていることが一因とのことでした。 また、先日発覚した CVE-2018-1002105 も触れられ、適切な設定で構築しないとKubernetesクラスタが攻撃者の手に落ちるというような大惨事も発生しうるということを改めて振り返りました。 これらの複雑性・リスクと向き合うためには、以下の4つが重要だそうです。 Learning to mitigate hazards and observe our systems Learning to measure our systems Learning to build tools to gain confidence Learning from our mistakes 僕はこれまでKubernetesの優れている話を聞く機会が多かったです。実際には便利なだけではなく複雑なシステムになりうることを認識し、うまく扱っていくことが重要なのだと認識できた良いセッションでした。 Monolith to Microservice Monolith to Microservice GitLabがシステムのマイクロサービス化に取り組んでいる中で過去に取ったアプローチや、今後の挑戦について語られたセッションでした。 モノリスとマイクロサービス の長所短所の紹介から始まり、Gitホスティングサービスの特性・直面する課題について触れつつ、モノリスが持つ制限から解放されるためにマイクロサービス化を推進し始めた話に入っていきます。 GitLabではディスクをスケール・高速化するために導入したNFSによって、ディスクI/Oとネットワークスループットという新たな課題に直面した過去があるそうです。 これを解決するために、 Gitaly というRPCを利用したOSSを開発し、プロダクション環境へ導入しています。 GitalyのREADME によると、GitLab 11.5の時点でほぼ全てのアプリケーションコードが、ディスクへの直接アクセスではなくGitalyを通じたアクセスに置き換わっているようです。 GitLabはGitalyによっていくつかの問題を解決していますが、ロードバランシングやスケーリングなどまだ課題があるようで、引き続き推進していきたいとのことでした。 システム全体を俯瞰で見た時にどのような課題があって、それをどうやって解決していくかを見据えて開発を行なっているようで、この視点・進め方は見習いたいと感じました。 MySQL on Kubernetes MySQL on Kubernetes Kubernetesの機能を利用することで、RDBも自然な形でコンテナとして管理することが可能だ、ということをデモを交えて紹介するセッションでした。 Cattle vs. Pets という例えがありますが、一般的にRDBはペットのように扱われるため、イミュータブルなコンテナでRDBを動かすことは難しいと僕は思っていました。 RDBのコンテナ運用は以下の機能・ソフトウェアを利用することで実現するようです。 Affinity and anti-affinity :DB用の特定のホストにMySQLのPodを配置する StatefulSets :ノード障害時の台数維持を行う Headless Service :MySQLのPodをディスカバリーする StorageClass, VolumeClaimTemplate :永続化ボリュームのプロビジョニングとその定義 MySQL Operators :Kubernetes上でCRDを使ってMySQLクラスターを作成・設定・管理するためのソフトウェア 上記のリストを見てわかる通り、MySQL Operatorsの存在がキーのようでした。 OSSとしては MySQL Operator や Presslabs MySQL Operator など複数あるため、要件に合わせて選択することになります。 僕は業務でAmazon RDSのAuroraクラスターを運用することがあるのですが、まさしくペットのように扱っています。このセッションを通してRDBもコンテナ運用できるという選択肢を持てたので、1つ自分の中の手札が増えてためになりました。 帰国次第、サンプル等を使って自分で手を動かして構築してみようと思っています。 まとめ Intro系セッションへの参加がメインでしたが、様々な切り口のプレゼンが行われていため毎日毎セッションが興味深く、意欲的に参加できました。 このブログには書きませんでしたが、心に残ったものが他にいくつもあります。 大変勉強になったと同時に技術・人の両方の盛り上がりを肌で感じられて非常に良い経験でした。 実際の業務へどのようにして取り入れられるか考えながら参加したため、自分ごととして学べたことが成果だと感じています。 それと同時にCNCFのプロダクトの魅力に触れ、自分も開発に携わりたいと感じました! ZOZOテクノロジーズでは、最新技術をキャッチアップしつつ地に足のついた技術選定ができるエンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
初めまして。ZOZO Technologies 分析部部長の牧野( @makino_yohei )です。 今回はZOZOのビッグデータを収集・加工してビジネスに活用する私の部門、分析部について紹介させてください。 「分析部」のミッション ミッションは2つです。データを活用して・・・ 1.大きな売上を作る 2.業務の効率や精度を上げる としているのですが、まあ、それはそうだろうという感じでしょうか。 1に関しては、部門の中期目標は 部門発信の施策で年間取扱高1,000億円の純増を作る というもので、 一人称で売上を作るぞと言っているのが少しユニークなところかなと思っています。 現在の進捗は10数%くらい。頑張ります。 「分析部」の役割 仕事の中身は、およそ以下のように分類できます。 1. ビジネスプランニングのサポート 施策や事業自体をデータから事前・事後に評価して、次のアクションを決めるのを支援するという仕事です。 また、依頼のみではなく提案もよくやります。SQL→XLS→PPTという流れが多いです。 2. ビジネスロジックのモデリング 主に機械学習を使って業務の裏で使う予測モデルを作るという仕事です。 予測するのは人単位の購入意欲や、SKU単位の需要など。DataRobotも活用しています。 3. ビジネスインテリジェンス MicrosoftのPowerBIを使ってダッシュボードや分析ツールを作る仕事です。 BIの裏のデータマートも設計・構築します。 4. データエンジニアリング DWH(BigQuery)とDMPを中心としたデータ分析基盤・マーケティング基盤を設計・構築する仕事です。 また、2で作ったモデルをシステム上に実装するのもこのチームの仕事です。 5. ハンズオンサポート 仕事内容は上記の1,3ですが、特定の事業部門にはりついて作業をするのが特徴です。 現時点ではプライベートブランドZOZOの担当部門に数名のメンバーが常駐して、データで事業開発を支援しています。 メンバーの紹介 上記のように、分析部門としての一通りの業務機能を備えているのですが、実は昨年2017年の夏に設立した1歳半くらいの組織です。 1年半前に会社やグループを横断してアナリティクスサービスを提供する組織として、 外から入社した私と数人のメンバーで立ち上げ、現在は約15人、90%以上が2年以内に入社したメンバーです。 面接などでどんなメンバーがいるのかよく聞かれますので、いくつか数字でまとめてみます。 年齢の平均値 = 31歳、最頻値 = 37歳、最小値 = 26歳、最大値 = 37歳 20代後半から30代前半がボリュームゾーンです。世代が近いのでみんな仲が良いです。 女性比率 = 20%弱 男女問わず募集中です。 B2C企業出身メンバーの割合 = 約50% インターネット系の企業出身者が多いです。残りのB2Bはコンサル、SIer、ツールベンダーなど 千葉県在住率 = 約70%、徒歩・チャリ通勤率 = 約40% 自分は東京在住なので徒歩通勤、羨ましいです。。 1年半を経て続々と優秀なメンバーが入社してくれています。 ディレクター・マネージャー陣を何名か軽く紹介させてください。 私、牧野(37) 部門の責任者で、総合コンサルティング会社で、ビジネスコンサルティングを10年強経験してZOZOに入りました。 前職ではビッグデータを使ったビジネスサービスの分析・設計をしてきました。 KOさん(36) ZOZO社員歴13年。CRMの専門家・エンジニアで、CRMチームのリーダーから部門立ち上げ時に異動してくれました。 ゼロから一緒に体制を作ってくれました。 全体のマネジメントと、5のハンズオンサポートをリードしています。 MYさん(33) 7月にインターネット企業から移籍。前職では数100人のエンジニア組織のデータ責任者をやっていたメンバーです。 検索基盤・分析基盤・DMPなどあらゆるプラットフォームを作ってきたスーパーエンジニアで、 4のデータエンジニアリングをリードしてくれています。 MNさん(34) つい先日、11月にJOIN。DMP業界では名の知れたデジタルマーケティングのスペシャリストです。 彼は、1のビジネスプランニングを中心に、1から4まで暴れまわってもらいます。 YKさん(38) 3のビジネスインテリジェンスのところも年度内にスペシャリストがJOINしてくれる予定です。 2のビジネスロジックのモデリングは、特に強化していきたいので、ご興味をお持ちの方は是非連絡ください!  若手メンバーも皆、優秀で向上心も高く、そしていいやつです。 一様に「あれっ、入ったのそんなに最近だっけ?」という状況で、 気が付くと馴染んでいて、気が付くとメンバーが集まって幕張界隈で飲んでいます。 最後に、ZOZOの分析の仕事が面白い5つの理由 ブログは慣れないのですが、最後に私の所感をよくある感じで。。 1. みんな本当にいいやつ 経営理念が「いい人をつくる」で、実際に部内のメンバーも社内クライアントである各部門やグループ会社のメンバーも一緒に仕事をしていて気持ちの良い人ばかり。 縦割りの概念がなく、いい仕事を一緒にして成果が出て一緒に喜ぶというヘルシーな組織です。 当然、分析の仕事なのでトンネルをなかなか抜けられないという苦しいフェーズもありますが、 その先に一緒に喜べる仲間がいるというのは、分析の仕事をするうえで結構重要なんじゃないかと思います。 2. 大きな仕事ができる 良くも悪くも、データを使って改善すべき案件がゴロゴロしています。その分、大きな仕事ができます。 例えば、pythonでクーポンの売上を事前に予測するツールを作って数10億(後半)売上増を作ったり、新しい事業の根幹となるロジックを作ったり。 コンサル会社から来た若手メンバーも、こんなに大きくこんなに面白い案件をいきなり任せてもらえるなんて…と、士気高く頑張っています。 3. 次から次に新しいことができる 私も入社時はデータを使ってZOZOTOWNを改善していく業務が中心というイメージをもっていました。 入社して間もなくZOZOSUITが登場し、プライベートブランドZOZOが出て、海外展開が始まり…、 1年後どうなっているか全く分かりません! 4. 扱えるデータが多様 というのが面接中に魅力としてよく言われます。確かにZOZOTOWNの実績・マスタデータだけでなく、 ZOZOSUITの計測データ、WEARやおまかせ定期便の洋服同士の組み合わせデータ、一つ一つのアイテムにつけられたタグデータやサイズデータなど他社にはない種類のデータがたくさんあってそれらを使って分析やモデルを作ることも多いです。 ただ、個人的にすごいと思うのは横ではなく縦に大量のデータを持っているということで、普通の量なら傾向あるね、 統計的に有意だねとしか言えないことが滑らかなグラフとして出せたりします。 それを初めて体験したときは鳥肌が立ちました。 5. 部門自体が発展途上 まだまだ1歳半の部門でケイパビリティも全然足りていません。 ここから優秀なメンバーを集めたり、社内・社外のプレゼンスをあげたり、成果を積み重ねていくフェーズです。 ないものを嘆くのではなく自分で作れる人にとっては、自分自身も成長し、部門の成長を感じられる場所です。 メンバー一同で助け合いながら、部門を育てています。 というわけで、いいメンバー・面白い仕事が待っているので、ご興味を持たれた方は是非↓からご応募を!  よろしくお願いします。 データサイエンティスト tech.zozo.com ビジネスアナリスト tech.zozo.com データエンジニア tech.zozo.com
こんにちは。品質管理部エンジニアリングチームの遠藤です。 前回の壮絶な失敗を何事もなかったかのように忘れ去り、次のテーマへ移りたいと思います。 工夫しなければいけなかったこと ZOZOSUIT自動測定については前回のとおり何となくぼんやりとイメージはついていたのですが、「これはどうしよう」と思ったことが2つありました。 デバイスIDに依存したくない 外部ディスプレイにどうやってブラウザ開くの? といったことでした。 Androidデバイスの制御をする場合、デバイスIDをもとに制御するわけですが、これをそのままプログラム内に書いてしまうとその端末しか制御できません。またディスプレイが複数ついているためどのディスプレイにどの端末を向けているのかを事前に登録してからプログラムを動作させなければいけません。最初はとりあえずプロトタイプということでそのまま動かしていましたが、まずはここをなんとかしたかったです。 とりあえず、やってみます。 私やる!やるったらやる! では具体的にどうすればよいでしょうか。デバイスに接続しているUSBケーブルの情報から接続しているデバイスIDがわかればなんとかなりそうです。とりあえず何も接続しない状態で、USBのシステム情報を確認してみます。 % ioreg -p IOUSB -w0 +-o Root <class IORegistryEntry, id 0x100000100, retain 15> +-o AppleUSBXHCI Root Hub Simulation@14000000 <class AppleUSBRootHubDevice, id 0x100000351, registered, matched, active, busy 0 (0 ms), retain 15> +-o Bluetooth USB Host Controller@14300000 <class AppleUSBDevice, id 0x100000fc5, registered, matched, active, busy 0 (9 ms), retain 33> こんどはUSBポートを増設した状態でやってみます。 % ioreg -p IOUSB -w0 +-o Root <class IORegistryEntry, id 0x100000100, retain 15> +-o AppleUSBXHCI Root Hub Simulation@14000000 <class AppleUSBRootHubDevice, id 0x100000351, registered, matched, active, busy 0 (0 ms), retain 17> +-o Bluetooth USB Host Controller@14300000 <class AppleUSBDevice, id 0x100000fc5, registered, matched, active, busy 0 (9 ms), retain 33> +-o 4-Port USB 2.0 Hub@14100000 <class AppleUSBDevice, id 0x1000022ba, registered, matched, active, busy 0 (2 ms), retain 21> +-o 4-Port USB 3.0 Hub@14400000 <class AppleUSBDevice, id 0x1000022d0, registered, matched, active, busy 0 (1 ms), retain 21> ポートが認識されています。そこにデバイスを接続してみます。 % ioreg -p IOUSB -w0 +-o Root <class IORegistryEntry, id 0x100000100, retain 15> +-o AppleUSBXHCI Root Hub Simulation@14000000 <class AppleUSBRootHubDevice, id 0x100000351, registered, matched, active, busy 0 (0 ms), retain 17> +-o Bluetooth USB Host Controller@14300000 <class AppleUSBDevice, id 0x100000fc5, registered, matched, active, busy 0 (9 ms), retain 33> +-o 4-Port USB 2.0 Hub@14100000 <class AppleUSBDevice, id 0x1000022ba, registered, matched, active, busy 0 (2 ms), retain 22> | +-o F5321@14110000 <class AppleUSBDevice, id 0x100002315, registered, matched, active, busy 0 (7 ms), retain 25> +-o 4-Port USB 3.0 Hub@14400000 <class AppleUSBDevice, id 0x1000022d0, registered, matched, active, busy 0 (1 ms), retain 21> 14110000にF5321が接続されています。いろいろ抜き差しして調査してみた結果、この番号は基本的に変わらないようでした。 自分の環境ではこんな感じでした。このナンバーのケーブルをあらかじめ対応させる予定の外部ディスプレイに向けておけばいい感じにいけそうです。 で、さっきのコマンドでデバイスIDを確認してっと、 これよく見たらデバイスIDじゃなくてデバイス名じゃねーか! adb devices でデバイス名との対応ができればいいのですが、表示されません。 と思ってたら  adb devices -l で表示されました。 最初(さっきまで)はこれを知らず、 system_profiler SPUSBDataType から無理やり取得していました。 コードに置き換えてみる(python3) では早速これをコードに置き換えてみます。 import subprocess def cmd (_cmd_str): # print("cmd:" + _cmd_str) proc = subprocess.run(_cmd_str, stdout=subprocess.PIPE, shell= True ) return proc.stdout.decode( "utf8" ) #指定したUSB IDに繋がっているAndroidデバイス名を取得する def check_usb_connect (_check_usb_id): cmd_str = "ioreg -p IOUSB -w0 | sed 's/[^o]*o //; s/ <.*$//' | grep '.*@.*' | grep -v Hub | grep -v USB" res = cmd(cmd_str) # print(res) for line in res.splitlines(): dn, usb_id = line.split( '@' ) # print(dn, usb_id) if usb_id == _check_usb_id: return dn return "" #デバイス名からデバイスIDを取得する。 def deviceId_from_deviceName (_deviceName): cmd_str = "adb devices -l" res = cmd(cmd_str) for line in res.splitlines(): if "product:" in line: device_id, etc = line.split( ' ' ) elist = etc.split( ' ' ) # print(device_id) device_name = elist[ 4 ].split( ':' )[ 1 ] if device_name == _deviceName: return device_id return "" print ( "-----------" ) device_name = check_usb_connect( "14110000" ) print ( "device_name:" + device_name) device_id = deviceId_from_deviceName(device_name) print ( "device_id:" + device_id) できました! 注意点 ただ、これだと同じデバイス名の端末を接続すると見分けがつきません。なので system_profiler SPUSBDataType などの情報から、USBポートとの対応など考慮してデバイスIDを取得したほうがよさそうです。 次回予告 幾夜魘されたか知らない悪夢、目の前の僅かな一跨ぎ。それができない泥沼の中で 俺は喘ぐ。 俺はどうしようもない絶望の中、次のミッションに取り掛かった。 今回はうまく行ったが次の問題がうまく行くと保証されたわけじゃない。 だが前に進まねばならない。俺が俺でいるために。 人を人たらしめるもの、それが目の前の問題をただがむしゃらに考え生きる、 ということなのだとするならば、あるいは俺も・・・。 そんなことを考えながら、俺は目を閉じ、今日も水玉のスーツに袖を通す。 次回、最終章(発動編)。ブラウザの扉は開くのか? 最後に ZOZOテクノロジーズでは、一緒にサービスを作成、サポートしてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
こんにちは。開発部に所属している鶴見です。 弊社ではAzure SQLDatabaseを利用して運用している箇所があります。 SQLDatabaseのデータ検索に利用している列ストアインデックスについて紹介します。 はじめに Azure SQLDatabaseはMicrosoft社が提供しているマネージドデータベースであり、SQL Serverエンジンと互換性があります。 ZOZOTOWNで扱っている大量データの中から対象データをピンポイントに探す場合は、SQLDatabaseのB-treeインデックスを利用し高速に検索しています。 しかし、B-treeインデックスは広範囲の検索が苦手という弱点があります。 そこで、広範囲の検索をする場合は、列ストアインデックスを利用してみました。 主にデータウェアハウスやデータ分析にて利用されていますが、 ここ数年の進化によりOLTPでも利用しやすいインデックスとなりました。 列ストアインデックスとは 広範囲の検索で性能向上が見込める列指向型インデックスです。 B-treeインデックスが行単位でデータを扱うのに対して、列ストアインデックスは列単位でデータを扱います。 列ストアインデックスとB-treeインデックスの比較です。 インデックス種類 B-treeインデックス 列ストアインデックス 指向性 行指向 列指向 読込 ページ単位(1ページ8kb) セグメント単位(1セグメント約100万件) 圧縮 可 可 scan操作 可 可 seek操作 可 不可 単一行検索 得意 苦手 広範囲検索 苦手 得意 B-treeインデックスで1カラム100万件のデータ取得を行うと大量のデータページを読込みます。 対して、列ストアインデックスは1カラム100万件を1セグメントとしているため、1回の読込みで済みます。 また、列単位で圧縮されるため圧縮率が高く非圧縮データ サイズと比較して、最大10倍のデータ圧縮が見込めます。 列単位で管理されることによりB-treeインデックスのように指定するカラムの順番を気にする必要もありません。 よって大量データを集計する処理などデータ分析に向いています。 SQL Server2012で登場した当初は「行データの更新・削除・挿入は行えない」などの制限があり、 常にデータ更新のあるデータベースでは利用しにくい状況でした。 しかし、SQL Server2014~2017にかけて制限が緩和され、データ更新に対応しました。 SQLDatabaseで列ストアインデックスを利用できるモデルは次の通りです。 Premiumレベル Standard S3レベル以上 General Purposeレベル Business Criticalレベル 列ストアインデックス作成 次の構文にて列ストアインデックスを作成可能です。 CREATE NONCLUSTERED COLUMNSTORE INDEX [index_name] ON [table_name] ( [column_1], [column_2], [column_3] ) 性能検証 実際にテストデータで性能を検証してみます。 テストテーブルに1億件のデータを登録し、そのうち1千万件データを取得した場合のB-treeインデックスと列ストアインデックスの処理能力を検証します。 -- テストテーブル CREATE TABLE test ( id int NOT NULL , age int NOT NULL , name nvarchar( 100 ) null CONSTRAINT primary_key_test PRIMARY KEY CLUSTERED ( id ASC ) 1億件のテストデータを登録した後、 カラム[age]に対してB-treeインデックス、列ストアインデックスをそれぞれ作成します。 -- B-treeインデックス作成 CREATE NONCLUSTERED INDEX [NONCLUSTERED_INDEX] ON [dbo].[test] ([age]) GO -- 列ストアインデックス作成 CREATE NONCLUSTERED COLUMNSTORE INDEX [COLUMNSTORE_INDEX_test] ON [dbo].test ([age]) GO 検証のため各インデックスが利用されるようヒント句でインデックスを固定しクエリを発行します。 (テストデータ1億件のうち、age = 21が1000万件となるようにデータを作成しています) -- 1000万件取得クエリ B-treeインデックス使用 select count(*) From test with ( index (NONCLUSTERED_INDEX)) where age = 21 -- 1000万件取得クエリ 列ストアインデックス使用 select count(*) From test with ( index (COLUMNSTORE_INDEX_test)) where age = 21 検証結果 検証結果は次の通りです。 B-treeインデックス(非クラスター化インデックス)を利用した場合。 実行プラン / 実行時間 列ストアインデックス(非クラスター化列ストアインデックス)を利用した場合。 実行プラン / 実行時間 列ストアインデックスが利用されていることが確認できる。 クエリにてCOUNTを行ったためハッシュ ベースの集計関数が利用されHash Match操作となった。 並列実行のためParallelism操作となった。 インデックス種類 CPU time elapsed time B-treeインデックス 2220ms 1647ms 列ストアインデックス 46ms 8ms CPU time及び、elapsed time(経過時間)の結果から、列ストアインデックスを利用した方が高速でした。 処理時間が「CPU time > elapsed time」となっているのは、並列で処理されているためです。 実行プラン詳細にて、論理操作はインデックススキャンであることが確認できます。 これは列単位で全件スキャンを行っているためです。 また、列ストアインデックスを利用すると実行モードはBatchとなります。 気を付けたいポイント 一見良さそうな列ストアインデックスですが、メンテナンスを怠ると性能が低下します。 列ストアインデックスに含まれる列データが大量に更新された場合、更新データは非圧縮状態になります。 圧縮されていることで最適なパフォーマンスとなるため、非圧縮データが増えてしまうと読込むデータが増え性能低下します。 列ストアインデックス状態確認 次のクエリで列ストアインデックスの圧縮状況を確認できます。 -- 列グループ状態確認 select OBJECT_NAME(object_id) AS object_name ,* from sys.column_store_row_groups total_rowsの値を確認すると、約100万件の単位でグループ化されていることが分かります。 state_descriptionの値がCOMPRESSEDの場合はデータ圧縮されており最適な状態です。 データ更新されるとstate_descriptionの値がOPENに変わり非圧縮状態となります。 OPENの件数が増えた場合は、インデックス再構成を実行して圧縮状態に戻すことができます。 インデックス再構成はオンラインで実行可能です。 更新が多いテーブルで列ストアインデックスを利用する場合、どのようなタイミングでインデックス再構成を行うか考える必要があります。 実際に利用する場合は、よく検証してから利用することをお勧めします。 まとめ 狭域の検索はB-treeを利用し、広範囲の検索は列ストアインデックスに置き換えました。 これにより、どのような検索であっても高速に処理することが可能となりました。 B-treeインデックスに比べると、考慮すべきことが多い列ストアインデックスですが、使いどころによってはかなり有効かと考えます。 参考文献 列ストア インデックス: 概要 | Microsoft Docs 最後に 本文でも紹介しましたが、本システムにはまだまだ問題が残されています。弊社では一緒にデータ基盤を作ってくれる方を大募集しています。 ご興味がある方は以下のリンクから是非ご応募ください! www.wantedly.com
こんにちは、サーバーサイドエンジニアの竹若です。11/13 ~ 11/15にかけてロサンゼルスで開催された RubyConf2018 にZOZOテクノロジーズから竹若・高木( @rllllho )・田島( @katsuyan121 )の3人が参加しました。 今年のRubyConfは講演数60、参加者数840の大規模なカンファレンスでした。この記事では私たちが興味を持った講演をいくつか紹介させていただきます。 Opening Keynote Sweat the Small Stuff Being Good: An Introduction to Robo- and Machine Ethics Empowering Early-Career Developers Ethical Data Collection for Regular Developers The Ruby Developer's Command Line Toolkit Pointers for Eliminating Heaps of Memory Parallel programming in Ruby 3 with Guild Building Serverless Ruby Bots Runnning a Government Department on Ruby for over 13 Years It's Down! Simulating Incidents in Production Yes, You Should Provide a Client Library For Your API Unlearning: The Challenge of Change まとめ Opening Keynote Opening Keynoteは、Rubyのパパであり弊社ZOZOテクノロジーズの技術顧問でもあるMatzさんの発表です。 コミュニティの力や、Rubyのこれからについて話されていました。 プログラミング言語は人間によって作られたものであり、C言語はOSを書くためHaskellは教育のためなどそれぞれの言語が作られた背景には明確な目的や意図があります。 RubyはMatzさんのアイデアを表現するために開発されたそうです。 Matzさんが自分のために作り始めたRubyが世界中で使われるようにまでなった理由として、Rubyコミュニティの力があるそうです。 OSSコミュニティは人の集まりではなくバーチャルなようなものです。 コミュニティは例えるなら台風で、目には見えないがとても巨大なものであるとおっしゃられていました。 またRubyコミュニティは誰か個人のものではなく、Matzさん自身もコミュニティの一部に過ぎないとおっしゃられていました。 OSSコミュニティは知的好奇心、コミュニケーション、責任の3つが大事で、Rubyはこれらを持ったとてもよいコミュニティであるとおっしゃられていました。 "Ruby is dead" という意見を聞くことがあります。 ハイプ・サイクルをもとに考えるとRubyは『Trough of Disillusionment(幻滅期)』のフェーズに位置しており、今こそRubyへ新しい投資ができるフェーズで積極的にRubyを使って欲しいとおっしゃっていました。 近年開発されたSwiftやGoなどのプログラミング言語は会社が開発したものですが、Rubyはコミュニティによって開発されています。 これからRubyが生き続けるために、 Never give up Keep moving Community Be nice Be happy であることが大事であるとお話しされていました。 Rubyのこれからの改善については、2018年12月にRuby2.6がリリースされ2020年にRuby3、2025年ごろにはRuby4の開発もありうるかもしれないとお話しされていました。 発表を聞き改めてコミュニティの大事さと自分もRubyコミュニティに貢献していきたいと思いました。 RubyConfはとても初めての参加者が多く、このような間口の広さもRubyコミュニティのいいところだと思います。 Matzさんが2025年には60歳になるとのことで、Rubyの言語デザインについてはどのように引き継がれていくのかについて気になりました。MatzさんのAI化も本当にありうるかもしれません。 これからのRubyも楽しみです。 Sweat the Small Stuff Aaron Harpoleさんによる、組織をスケールさせる際によく見られるアンチパターンとその対処法を紹介する講演でした。 「チームが小さかった頃は全員がプロダクトを熟知していて、ミーティングもなく、デプロイとフィードバックのサイクルを小さく収めることができていた。しかしチームが大きくなるに従い既存のやり方が通用しなくなりチームの歯車がうまく回らなくなっていった」 といった事態に陥らないためのTIPSが紹介されていました。 具体的には以下のような内容でした。 エンジニアが生産性を高めるための投資を惜しまない(CIサービス、マシンのスペック) トライ&エラーがしやすい開発環境を用意してデプロイを安全にする ミーティング前に各々が論点を紙にまとめておいてミーティングの最初に読み合う エンジニアの採用ではまず最初にエンジニアが採用候補にコンタクトする またチームの既存文化を変えることの難しさを説き、リクルーティングは文化を構築する最大のチャンスであると述べていました。 私が所属しているチームもまだ編成されて日が浅いのですが、今回のAaronさんの講演は今後チームをスケールさせていく上で気を配っておくべきポイントを明確化するのに役立つと思いました。 参考 : https://icanthascheezburger.com/wordpress/?page_id=222 Being Good: An Introduction to Robo- and Machine Ethics Eric Weinsteinさんによる、技術者が持つべき倫理と人工知能が持つべき倫理に関する講演でした。 技術者が持つべき倫理のお話ではソフトウェアのバグが原因で複数の死者を出した放射線療法機器「セラック25」やシステムの脆弱性をハックされ巨額のお金が不正送金されたブロックチェインを使ったプロジェクト「The DAO」を引き合いに出しながら以下のことを述べていました。 医療と同じで技術も技術者が標準治療を遵守しなければ人命を危険にさらしてしまうこと システムの要件を満たすのに十分に強力であると同時に必要以上に強力ではない、必要最低限の力を持ったプログラムを書くことが必要であること 人工知能が持つべき倫理のお話ではFace IDや自動運転技術を例に出して以下のことを述べていました。 人工知能に「決定」や「認識」などの人間的な行いをさせるということは暗黙的に人工知能の行動に倫理的な重みを持たせているということ 人工知能の行動に対する説明とその責任を取ること 今後技術が発展し今まで人間のやってきた仕事が機械に置き換えられていくに従い、機械の保つべき倫理の水準などの議論が更に増すはずです。 技術者の関心は何かと技術そのものだけに集中してしまう傾向にあります。しかしそれにまつわる倫理や付随する責任を考慮して初めて世に役立つものが生まれると思います。 今回のEricさんの講演の内容には今後技術と倫理に関する議論をするために必ず認識しておかなければならない重要な事実が詰まっていると感じました。 Empowering Early-Career Developers Mercedes Bernardさんによる、自身の経験に基づいた経験の浅いエンジニアをベテランエンジニアにまで育てるためのアプローチを紹介する講演でした。 Mercedesさんのチームでは経験の浅いエンジニアを育てるためにキャリアの早い段階で色々な裁量を与えているそうです。 経験の浅いエンジニアがいきなりベテランエンジニアの仕事を振られて困らないように早めに経験を積ませてあげることが重要であるとおっしゃっていました。 Mercedesさんはコンサルタントのチームを率いているそうで、具体的な内容はエンジニアに当てはまるものではありませんでした。しかし経験の浅いエンジニアをベテランエンジニアにするためのアプローチは、フレームワークとしては有用な気がします。 個人的にはメンバーそれぞれが優秀で頼りあうことのできるチームが理想です。チームの他のメンバーをレベルアップするアプローチを紹介する今回のMercedesさんの講演は非常に興味深かったです。 参考 : http://downloads.ctfassets.net/kueynwv98jit/2Dmircv1tmgoSAkc6UKggO/2120031f5d7f810ab6a13edae3642fe5/EmpowerEarlyCareerDevs.pdf Ethical Data Collection for Regular Developers Colin Flemingさんによる、自身の経験を基にした機密性の極めて高いデータを扱う際の倫理についての講演でした。 昨今の巨大テックカンパニーのデータの扱いをみて、データの扱いにもACM Code of Ethicsのようなベストプラクティスが必要になっているとのことでした。 ACM Code of Ethicsとはコンピューターのプロフェッショナルが倫理的な決断を下すのを助けるためのガイドラインです。 ColinさんはこのACM Code of Ethicsをシンプルに3つのポイントに要約していました。 Everyone is a stakeholder in computing Avoid harm and protect privacy avoid yolo-driven development あくまで倫理に関する講演だったのでデータの収集、セキュリティなどの技術的なお話はありませんでした。 今回のColinさんの講演は中絶に関するデータという非常に機密性の高いデータを扱っているところに興味を惹かれたので、この講演を1日目で一番楽しみにしていました。 機密性の高いデータをクライアントから集める時にもう一度聞き返したくなるような内容でとても勉強になりました。 The Ruby Developer's Command Line Toolkit Brad Uraniさんによる、生産性を上げるツールの数々を紹介する講演でした。 dotfiles zsh tmux fzf のような多くの開発者の支持を得ている便利ツールが中心に紹介されていました。 他にはhomesickやgit/hooksでcommitの前に何かを仕込むTIPSであったり、gemrcやrailsrcを使って開発を少し便利にするTIPSの紹介もありました。 またQ&Aでは質問者が便利だと思うツールが挙がったりして、moshなどのツールが紹介されていました。 中心的に紹介されていたツールはどれも素晴らしく便利なものばかりで、開発を始めたばかりの人なら誰にでも聞く価値のある講演でした。 私も開発を始めた頃にこれらの便利ツールを知っていたら生産性が上がって幸せになっていただろうなぁと思います。 もし今回のBradさんの講演で紹介されていたツールの中に試したことのないツールがあればぜひ試して見ることをお勧めします。 使ったことのないツールが自分の作業効率にどれだけ貢献してくれるかは使ってみなければわからないので、すぐに試してみるのがよいと思います。 私もhomesickやfzfを試してみたいと思います。 参考 : https://docs.google.com/presentation/d/18LyKvXRYZZA1RuPsZ4MvwKlufMhXDhWIM15LmK3oQ8A/edit#slide=id.p Pointers for Eliminating Heaps of Memory MRI、RailsのコアコミッターであるAaron Pattersonさんによる、Rubyのメモリ消費量を削減する2つのパッチを紹介する講演でした。 1つ目のパッチは require のメモリ消費量を削減するものでした。 このパッチを理解するのに前提知識として必要なのがShared Stringという概念です。 これはRubyの文字列オブジェクト(RString)が同じ文字列を共有することなのですが、1つ注意点があります。 それはRubyの文字列オブジェクト(RString)が参照する文字列が最後の文字まで等しくないと、同じ文字列が共有されないということです。 例えば /a/b/c.rb と /b/c.rb の文字列オブジェクトは /a/b/c.rb という文字列を共有しますが a/b/c.rb と a/b/c の文字列オブジェクトは文字列を共有せず、それぞれ a/b/c.rb と a/b/c という別の文字列を参照します。 require '/a/b/c.rb とすると loaded_fatures_index というハッシュに以下のような8つの文字列がキャッシュされます。 '/a/b/c.rb', '/a/b/c', 'a/b/c.rb', 'a/b/c', 'b/c.rb', 'b/c', 'c.rb', 'c' 接尾辞に .rb とつく文字列オブジェクトは /a/b/c.rb という文字列を共有するのですが、接尾辞に .rb がない文字列オブジェクトはそれぞれ別の文字列( /a/b/c , a/b/c , b/c , c )を参照するようになっています。 今回そこで接尾辞に .rb とつかない文字列オブジェクトが全て /a/b/c という文字列を参照するようにすることで同じ文字列を共有できるようにしたそうです。 結果シンプルなRailsアプリケーションの起動時のメモリ消費量を4.2%削減することに成功したそうです。 2つ目のパッチはISeqオブジェクトから文字列を直接参照することでメモリ消費量を削減するものでした。 ISeqオブジェクトとは、Rubyのコードをコンパイルすると生成されるオブジェクトです。 そしてこのISeqオブジェクトの中にバイトコードが格納されます。 ここで登場するのが mark array というオブジェクトがGCに消されないようにオブジェクトをマークするのに使われる配列です。 今まではISeqオブジェクトはこの mark array を通してオブジェクトを参照していました。 そこでこのパッチでISeqオブジェクトから直接オブジェクトを参照するようにしたようです。 結果Railsプロセスのメモリ消費量を6%削減することに成功したそうです。 今回のAaronさんの講演はRubyの内部をわかりやすく紹介する内容にもなっていてとても面白かったです。 またAaronさんはハンバーガーの帽子をかぶって登場し、GithubがMicrosoftに買収されたことにかけたダジャレを連発したりしてとてもユーモアがあって素敵でした。 Parallel programming in Ruby 3 with Guild Koichi SasadaさんによるGuildを使った並列処理の講演でした。 GuildとはRuby 3で導入予定の新しい並列処理です。 1つのGuildは1つ以上のスレッドを含み以下のような構成になっているそうです。 異なるGuildに入っているスレッドは並列処理できる 同じGuildに入っているスレッドは並列処理できない またGuild間では共有できるオブジェクトが決まっており、Guildを使うことでスレッドセーフな処理を簡単に書くことができるとのことでした。 Guild間で共有できるオブジェクトには以下が挙げられていました。 イミュータブルなオブジェクト(fronzeしたオブジェクトはそのオブジェクトに含まれるオブジェクトもすべてfrozenされている必要がある) Class/Moduleオブジェクト Isolated Proc また、共有できるオブジェクトについては特別なプロトコルを設けたいとも話していました。 案としては紹介されていたものは、GuildからGuildへのオブジェクトの受け渡しです。 Guildから他のGuildへオブジェクトを渡し、元のGuild内からは渡したオブジェクトが参照できなくなると言ったアクターモデル。 または、Goのchannelのような特別なオブジェクトを利用したCSPモデルが案として紹介されていました。 またこのGuildという名前はまだ決定しておらず code name だそうです。 デモではフィボナッチ数列を40vCPUの上で40個のGuildを使って並列処理した結果とシリアル処理した結果の比較を見ることができました。 n = 30の時点で実行時間に約16.2倍の差が出ていました。 今回のSasadaさんの講演ではGuildの実装ステータスなどのお話もあり、Guildに関する色々な情報を知ることができてとても面白かったです。 Ruby 3でGuildを使った並列処理を書くのが楽しみで仕方がないです。 参考 : http://www.atdot.net/~ko1/activities/2018_rubyconf2018.pdf Building Serverless Ruby Bots NetflixのDamir SvrtanさんによるServerlessをRubyで実現するお話でした。 AWS Lambda上でRubyを動かす話が中心に行われました。 現在AWS LambdaはRubyに対応しておらず、直接RubyをLambda上で動かすことができません。 そこで、 Traveling Ruby ・ mruby ・ jruby の3つRuby処理系を利用しRubyをLambdaで実行可能にする方法が紹介されました。 Traveling Ruby、mrubyの場合はjsにコンパイルを行い、jrubyはjarにコンパイルしLambda上でRubyの実行を実現していました。 それぞれの処理系のコードサイズ、メモリ使用量、実行時間についての比較が行われていました。 処理系 コードサイズ メモリサイズ 処理時間 ウォームアップ後の処理時間 Traveling Ruby 6.7MB 25MB 3900ms 3300ms mruby 945KB 40MB 3900ms 3300ms jruby 23.2MB 150MB 25000ms 1200ms やはりLambdaのコールドスタートの影響で、jvmの起動に時間がかかると示されました。 ただしウォームアップされた状態だとjrubyの処理時間がダントツで早くなったそうです。 AWS Lambda以外でのServerlessについてもお話され、IBMクラウドのサーバレスのシステムがRubyに対応していることが紹介されました。 また、Rubyに対応したServerlessフレームワークServerless/FaaStRubyが紹介されました。 AWSLambdaでRubyを利用するには、まだまだ必要であり早く公式対応をしないかなと思いました。 3つの処理系でのメトリクスの比較はウォームアップ後にjrubyでの実行が高速になることなど、Lambdaやjvmの特性に合わせて計測を行っており参考になるなと感じました。 また知らないクラウド技術やフレームワークが紹介されておりとても興味深かったです。 Runnning a Government Department on Ruby for over 13 Years Jeremy Evans( @jeremyevans0 )さんによる California State Auditor という政府組織のレガシーなシステムをRubyを利用してモダナイズしているというお話でした。 もともとのシステムはすごくレガシーな状態でRubyを利用してモダナイズしているそうです。 モダナイズにRubyを選んだ理由としては以下の4つ挙げていました。 高速に開発が可能 簡単にモダンなアプローチを採用できる 容易に学習が可能 楽しい 高速開発、並びにモダンなアプローチは様々なOSSを駆使することが可能で、Rubyには豊富なOSSライブラリが揃っています。 また、政府組織のシステムということもあって採用の問題があると語っていました。そのため、学習が容易であるRubyを利用することで、誰でも開発に参加できます。 また、スピーカーの方は Roda というルーティングライブラリ。 Sequel というDBライブラリを作成しており、California State Auditorのシステムで利用しています。 Railsを利用せずにCodaやSequleを開発している理由としては、モダナイズのプロジェクトを進めるにあたり、スタートは小さくシンプルにしたかったとのことでした。 利用している技術スタックがFreeBSD/PostgreSQL/nginx/Unicornと私達にとってすごく身近なものでした。 勝手な思い込みですが政府のシステムでRubyが使われているとは思っていなかったのですごく興味深かったです。 参考 : https://code.jeremyevans.net/presentations/rubyconf2018/index.html It's Down! Simulating Incidents in Production Kelsey PedersenさんによるStitch Fixで実際に行っているインシデント発生時のシミュレーションやカオスエンジニアリングについてのお話でした。 インシデント発生シミュレーションは9つのステージにそって行われます。Stich Fixではシミュレーションの当日をGameDayと呼んでいるそうです。 1. Define simulation まずはじめに、どのインシデントを対象にしたシミュレーションを行うのかを決定します。インシデントはデータベース障害・インスタンス障害・外部サービスの障害など様々な障害が原因で発生します。 発表ではサービス障害インシデントのシミュレーションを選択し、サービスが500エラーを返した場合についてシミュレーションされていました。 2. Implementation 1で設定したインシデントが発生するように実装を行います。 今回のケースではHTTPクライアントライブラリの Faraday にシミュレーション用の実装をしていました。 まず、インシデントを発生させるためのユーザーを作成します。そして、対象ユーザーの場合のみFaradayが常に500を返すようにoverwriteしていました。 3. Gather expectations チーム内で、対象のインシデントがサービスにどのような影響が出るのか議論を行います。 実際に行なった議論では、動かなくなる部分は発生するがクリティカルな問題は発生しないという意見が7割であったそうです。 4. Huddle as a team テーム全員が集合します。インシデントを想定して各人がリモートでビデオチャットを利用し議論している様子が見られました。 5. Talk through expectaion インシデントに対してどのようなことが起こるのか議論し、洗い出しを行います。それを以下の2点の観点からまとめを行っていました。 bullet point list of expectations list the set of instructions 6. Run it! 以下のようなコマンドで、準備したインシデントを発生させます。 bundle exec rake gameday:start そして、実際に何が起こるのかをサービスのページ・サービスのメトリクスを見て確認します。 最後に以下のコマンドで、GameDayを終了します。 bundle exec rake gameday:end 7. Revisit expectation 事前に想定していたインシデントと比べ、実際におきたインシデントがどう違ったかを振り返ります。 8. Write and edit runbooks インシデントが起こった時にどのような対応を行ったかをゲーム実施前のドキュメントに追加します。 9. Update dashboards エラー監視ツールのダッシュボードなどにGameDayなどタグ付をし、エラーメトリクスがGameDayのものであることを判別できるようにします。 実際のインシデントに対してシミュレーションを行っておくことで、いざそのような状況が発生した場合に焦らず対応ができるようになるだろうなと思いました。 さらにインシデントが発生した場合に起こる問題を事前に対策できるなと考えました。 シミュレーションを行う環境を整えることは大変なことですが、安全で柔軟なシステム運用を行うにあたりとても大切なことであると実感しました。 弊社でもこの発表を参考にインシデントに対するシミュレーションを行なってみたいと思いました。 Yes, You Should Provide a Client Library For Your API Daniel Azumaさんによる、APIのクライアントライブラリについての話でした。 Danielさんは、Google Cloud Platform(GCP)のRubyAPIクライアント開発者の方です。 Googleでは膨大な数のAPIとAPIのクライアントライブラリを提供しています。 それらをメンテナンスし続けるにあたり、APIクライアントライブラリを開発する際に気をつけるべき点についてお話しされていました。 具体的にはAPIクライアントライブラリ内で以下の4つのことを意識することが大事とのことでした。 独自で処理をラップしたクラスなどを作らずRubyの記法をつかって開発を行う エラーハンドリングを適切に行う セキュリティを担保する パフォーマンスを意識する パフォーマンスを意識するという話の中で、ログの書き込み処理などについてはユーザーからのリクエストを同期的に処理せず、非同期にバルク処理を行うことでパフォーマンスを向上させていると話されておりとても参考になりました。 またOpenAPIのようなインタフェース記述言語を使用し、コードジェネレータを用いてAPI定義書からAPIクライアントライブラリのインタフェースを生成することを推奨されていました。 API定義書からコードジェネレータを用いることで、APIクライアントライブラリのインタフェース以外にも リファレンスのドキュメント サーバーのスケルトン テスト などを生成することもできるため、API開発と保守の双方の役に立つということでした。 この発表を聞き、REST APIの開発はSwaggerなどのAPI定義書からAPIのインタフェースやドキュメント、テストはコードジェネレータで生成するという開発の流れがよさそうだと思いました。 発表スライドがとてもわかりやすいので、興味のある方はぜひご参考ください。 弊社でも以前、SwaggerからAPIのシリアライザーを生成するgemを開発した記事を公開しているのでこちらも参考にしてください。 techblog.zozo.com Unlearning: The Challenge of Change RubyConfでは、女性の参加者・登壇者の多さがとても印象的でした。 カンファレンス自体とても初心者歓迎の空気を感じましたし、Rubyの話だけでなく開発手法やエンジニアとしての働き方などの話もあり女性の方も参加しやすいのかなと感じました。 また3日間の最初のKeynoteの登壇者が、Matzさん以外女性であったこともとても印象的でした。 3日目のKeynoteはJessie ShternshusさんのUnlearningについてのお話でした。 Unlearningとは今まで学んだ知識や自分が持っている価値観を意識的に捨て、新たに学び直すことを意味するそうです。 新しい知識やスキルを身につける際、既存の知識があると既存の知識や習慣に頼ってしまいがちです。 しかし未学習の場合は外部から様々な学習法を探し取り入れようとします。そのため組織やチームで、新しいことを身につけたり変化を起こそうとする時に、すでに知識を身につけていることが足枷になることがあります。 そのためunlearningnの重要性についてお話しされていました。 一度自分がもってしまった価値観を捨てることは難しいため、日常的に何をunlearningしたいのかを考え意識的に習慣や行動を変えることが必要とのことでした。 発表中にunlearningを体験するために、2人1組で相手が空中に書く数字を反対側からなぞるというペアワークを行なったり、自分がunlearningしたいものを書いて紙飛行機を飛ばしたりと体験が混ざった発表で楽しかったです。 まとめ 今回私にとって初めての海外カンファレンス参加でしたが、会場の雰囲気がとても暖かくオープンで世界中から集まったRubyist達と交流できた素晴らしい経験になりました。 ロサンゼルスは少し肌寒かったです。それなのに会場は冷房がかかっていて思わぬカルチャーショックでした。食べ物に関してはさすがアメリカの中でも特に人種的な多様性に富んだ州ということで、様々な国の食べ物を楽しむことができました。特にメキシコ料理が良かったです。 講演は全て英語だったわけですが、中には早口なスピーカーもそれなりにいて英語慣れしていない人には厳しいように感じました。個人的には会話中に声量がなさ過ぎて聞き返されることが多々あったので、少しうるさいと感じるくらいの声量を意識して会話をしていました。 また、今回のRubyConf参加に関する費用はすべて会社が負担してくれました。 来年のRubyConfはテネシー州ナッシュビルで開催予定です。一緒に行きたい! という方がいましたら、ぜひ以下のリンクからご応募ください。お待ちしております。 www.wantedly.com
こんにちは。イノベーション推進部の武田です。 Google Assistantアプリを開発するときの本番環境と開発環境の切り分けについて紹介します。 はじめに 最近Google AssistantやAlexaなどのVoice User Interfaceが熱いですね。 毎日のように新しい記事を目にしますし、新しいハードもどんどん登場しています。 プラットフォームごとにSDKも公開されており、誰でもアプリを開発し公開できます。 開発の初期段階では自分だけが触るので、試せる環境が1つあれば十分です。 しかし、開発したアプリを審査に出したりリリースしたとなるとそうは行きません。 そこで 審査や実際に稼働している処理に開発の影響を出さない ことを目的とした環境の構成を紹介します。 アプリの構成と役割 Google Assistantアプリは基本的にActions on GoogleとDialogflow、 そしてCloud Functionsなどのバックエンドサーバーの3つで構成されています。 簡単に説明すると以下のような役割を持ちます。 Actions on Google アプリ自体の管理を担当。アプリ名や言語などを設定したり、審査やリリースといった操作が可能。 Dialogflow 発話内容の分析を担当。会話のパターンや認識する単語を設定できる。 バックエンドサーバー(Cloud Functionsなど) 発話内容に応じて任意の処理を実行する。自分でサーバーを用意することになるため、AWS Lambdaなどでも良い。 ユーザーからのリクエストを含めた処理の流れが下記の図です。 場合によってはバックエンドサーバー自体がなかったり、 バックエンドサーバーの先に別のAPIだったりDBだったりがつらなってきます。 今回は図に示した構成で考えていきます。 本番環境と開発環境の切り分け 切り分けにはActions on GoogleのスナップショットとGoogle Cloud Platform(以降GCP)のプロジェクトを利用します。 図にすると以下のようになります。 切り分け方ごとに説明して行きましょう。 Actions on GoogleとDialogflow Actions on GoogleとDialogflowはスナップショットの仕組みを利用します。 Actions on Googleはバージョン管理機能を備えており、 スナップショットは各バージョンの処理内容を保存したものです。 スナップショットは審査の提出やテスターへの配信の際に自動で作成されます。 開発している途中のようにスナップショット作成時から何かしら変更がある場合、 現在の状態はドラフトとして保存されています。 作成されたスナップショットは、審査やテスト配信など状況に合わせた環境へデプロイされます。 開発中の内容が保存されているドラフトは、開発者のアカウントやシミュレーターで実行可能です。 そのため、無理に別のプロジェクトに分けなくても動作環境は分離されているわけです。 バックエンドサーバーの接続先はDialogflowで設定します。 スナップショットとドラフトで別の設定をしておけば、 リクエストを別々のバックエンドサーバーに飛ばすことができます。 ただ、接続先を手動で切り替えることになるため開発体制によっては何かしら対策が必要です。 Actions on GoogleとDialogflowもGCPの一部であるため、 プロジェクト自体を切り替えて環境を分けることは可能です。 しかし、これらはGUIでの操作がメインです。 プロジェクトを分けると情報の同期が手間になってしまうので、スナップショットを利用する方法を取っています。 バックエンドサーバー(Cloud Functions) Cloud Functionsを利用している場合、 GCPのプロジェクトを別にしてしまうのが良いと思います。 下の図の赤枠の部分です。 Cloud Functionsはデプロイせずにローカルで検証していくことがある程度可能ですので、 プロジェクトを分けなくても動作環境は分離できます。 しかし、Google Homeなどのデバイスやシミュレーターで会話の検証をする場合、 設定したエンドポイントにリクエストが飛ぶことになるためデプロイが必須となってきます。 また、開発するアプリがFirebaseのMessagingやFirestoreを利用していると、 プロジェクトを切り替えれば全てまとめて切り替わってくれるのは楽です。 複数のプロジェクトの管理はFirebase CLIを利用すれば簡単に行えます。 まとめ この方法を使えば、審査や本番に影響を出さずに開発もガンガン進めていくことが可能です。 しかし途中で挙げた手動切り替えなどの問題を考慮すると、 Actions on GoogleとDialogflowも別プロジェクトに切り分けた方がいい状況もあると思います。 このあたりはCLIでの管理が進んできたり、開発のプラットフォームが安定したら考えていきたいところです。 VUIはチャレンジングで非常に面白い領域です。 プロダクトのUXを考えるのもそうですし、開発の方針や設計も手探りな状態なのでやること全てが挑戦です。 今回は技術寄りの内容でしたが、もっとプロダクト寄りの話もしていきたいと思ってます。 最後に DroidKaigi 2019 に採択されました! Google Assistantアプリのベータテスター配信やCLIでテストしやすい構成など、ちゃんとサービスを作るための内容を発表します。 もし都合が合う方はぜひお越しください! VUI界隈を盛り上げたかったり、新しいプロダクトを作りたい人を募集中です! 一緒に世界をカッコよくしていきましょう! www.wantedly.com