TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは! ZOZOTOWNやWEARのiOSアプリ開発をしている元と小野寺です。 先日、9/5から9/7まで3日間iOSDC Japan 2019が開催されました。今回ZOZOテクノロジーズでは12名のメンバーで参加し、弊社はスポンサーとして協賛しました。 この記事ではiOSDC Japan 2019にて発表されたセッションの一部を紹介すると共に、現場の盛り上がりの様子もお伝えします。 セッション ライブラリのインポートとリンクの仕組み完全解説 最初に紹介するセッションは弊社の技術顧問をされている岸川さんのセッションです。 iOSDC Japan 内容はライブラリを使うときに遭遇するimportエラーやlinkエラーに対してシステマチックに解決するために必要な知識についてです。 ライブラリやフレームワークのimportエラーやlinkエラーを解決するとき、このセッションの内容は非常に役に立ちます。 応用編ではこのセッションで紹介された知識を活用できる方法がいくつか紹介されました。 その中でアプリの起動時間短縮に有効な方法があり、効果があるならZOZOTOWNアプリにも導入しようと思ったので実際検証してみました。着目したのはスライドの以下の部分です。 ダイナミックフレームワークが増えてくると起動時間に影響を与える(dyld2の場合) そのため各フレームワークをスタティックリンクにして1つの大きなダイナミックフレームワークにする この方法はWWDC 2016のセッション「Optimizing App Startup Time」にてアップルが紹介した方法でもあります。 具体的にはダイナミックフレームワークはアプリ起動時にコストの高いリンク処理が発生するので各フレームワークをスタティックフレームワークとしてビルドし、その全てを1つのダイナミックフレームワークにまとめることでリンク処理のコストを抑える方法です。 dyld2に関しては現状サードパーティのフレームワークで使われてるので効果ありだと思いました。 実際に1つのダイナミックフレームワークにまとめてから今回のセッションで学んだ具体的な設定方法を使い、サンプルアプリで検証した結果を共有します。 ■ 検証内容 「複数のダイナミックフレームワークをそれぞれスタティックリンクにしてから1つのダイナミックフレームワークにまとめる」ことで起動時間が短くなるかを確認 ■ 検証対象アプリ テンプレのプロジェクトを生成してからフレームワークを11個追加したアプリ(フレームワークの選定基準はありません) ■ importしたフレームワーク AFNetworking, Alamofire, CocoaLumberjack, CocoaLumberjackSwift, RxBlocking, RxCocoa, RxRelay, RxSwift, SDWebImage, SDWebImageMapKit, SnapKit ■ 端末 iPhone 6s、iOS 12.3.1 ■ 結果 赤い枠の中にある「Total pre-main time」が総計時間、「dylib loading time」がダイナミックフレームワークのロード時間です。両方とも短縮されているのでちゃんと起動時間が短縮されたのを確認できました。もし運用しているアプリの起動時間が長いのであれば試してみるといいかもしれません。 個人開発のアプリが輝くために - アプリのDL数をあげる必殺技 - 弊社ZOZOテクノロジーズからLightning Talkで名取が登壇しました。 iOSDC Japan 発表内容は、自身が作成した個人開発のアプリ「大人のなぞなぞ〜脳トレIQ謎解きアプリ〜」を約100万ダウンロードまで成長させた戦略についての解説です。 これからアプリを実際に公開してみようと考えている方、既に公開している方どちら側であっても参考になる発表内容でした。 個人で開発したアプリでは、プロモーションにお金をかけることは、難しいですよね。 そんな個人開発のアプリを約100万ダウンロードされるまでに成長させた際に行った戦略の紹介です。発表の中で、ASO対策としてキーワード検索で上位に表示させる為の工夫が紹介されました。 ASO対策としては、次のようなポイントに気をつけると良いようです。 タイトルには、何のアプリなのかを表す為のキーワードを取り入れること サブタイトルには、どんなアプリなのかを伝えるワードを取り入れること キーワードには、ひらがなやカタカナ、漢字などを含める セッション内では、カメラアプリを例にしてわかり易く説明されていました。 他にもアプリの評価を上げるために、高評価をしてくれそうなユーザーにレビューを依頼する取り組みについて紹介がありました。 高評価が期待できるユーザーの例として、次のような具体例が紹介されました。 アプリを10回以上訪問してくれたユーザー あるコンバージョンを達成したユーザー 一定期間、毎日アプリを起動したユーザー 実際に100万近くダウンロードされたアプリでの取り組みということもあり、とても説得力がありました。 今後アプリを公開する際により多くのユーザーに使ってもらう為には、どんな事に気をつけたら良いかの指標となる内容でした。 タイトルの記載内容やレビューへ促す条件を、紹介された内容や他のアプリとの比較をしてみることで、自身のアプリでの対策を見返す良い機会にもなりそうです。 FatViewControllerを安全に書き換える方法が見つからなかったので、どういう痛みを許容するか考えた テスト方針を決めるときは状況や環境を考慮する必要があるため、決まった対応パターンが存在しません。だからこそ他社の対応事例は「過去の対応方針を振り返るとき」や「これから決めるとき」参考になります。 このセッションではまさにそのテスト方針を決めるとき、何を考慮し、どう対応したかが事例として紹介されました。 「普段、テスト方針を決めるために自分がやっていることに問題はないか」、「他の人はどう決めてるか」はとても気になってたので自分の過去を振り返りながら、そして同じ状況なら自分はどうやるかを考えながら興味津々に発表内容を聞きました。 セッションで紹介された「再現可能なテストはないが安全に修正したい。だけどミスは許されない」状況で自分なら何を考慮するかを考えたときに思い浮かんだのは以下です。 自動テストがあるか サービス視点でテスト対象の重要度 開発スケジュール的に使えるリソースはどれほどあるか(工数) テストコードを書きやすい設計になっているか テストしたいのは何か(ロジックか、表示か、挙動か) 実装コスト 実行コスト テストの生存期間やメンテナンスコスト セッションでも上記は考慮対象だったので大きくはズレてないと思い、少し安心しましたがUIテストを積極的に検討してる部分は印象的でした。今までUIテストは「手間がかかる」、「メンテコストがかかる」と単純に思い込んでいたのでもう一度UIテストに関して振り返ってみようと思います。他にはサービスの中の重要な価値について「認識を共有する」という項目も印象に残りました。確かにここをはっきりしておかないと暗黙的な基準で判断してしまうこともあるかもしれません。 このセッションはテストに関する自分のやり方を振り返るきっかけになったと思います。他社の事例を知ることは滅多にないのでテストに興味がある方は是非このセッションの資料を見ることをオススメします。 多言語対応と戦う 2019年版 こちらのセッションでは、多言語化が未対応のアプリでの対応の始め方から紹介されていました。 現在私が対応しているWEARアプリでは日本語や英語、繁体字、簡体字といった多言語の対応を行っていることもあり、このセッションを聞いてみることにしました。 WEARアプリでは文言もユーザーに伝わりやすいように、見やすいようにとデザイナーが試行錯誤して決めています。 実装の節目で私たちはデザインチェックを行うのですが、4つの言語の確認となりますとシステムの言語を都度切り替えて確認しなければならないという手間がありました。 そういった背景から私は、このセッションの中で紹介されたうちの1つ 「アプリで言語を切り替えられるようにしよう」 に興味を持ちました。 アプリの言語切り替えは、iOS 13以降からiOSの標準機能として盛り込まれる機能のようです。 Language selection per app Use third‑party apps in a different language from your system language. www.apple.com こちらのセッションでは、これをiOS 12以前のバージョンで実現する方法が紹介されていました。 アプリ内で使用されている言語は、どこから取得されるのか? UserDefault.standard.array(forKey: "AppleLanguages") にて[String]で定義されています。 ここで定義されている言語は、設定>一般>言語と地域の中で追加されてる言語です。 アプリ内では、 UserDefault.standard.array(forKey: "AppleLanguages") から取得された配列の先頭の言語が使用されます。 どうやってアプリ内で言語を切り替えるのか? 例えば現在表示されている言語が、日本語の状態から英語に変える場合を考えます。 UserDefault.standard.array(forKey: "AppleLanguages") で取得される配列の順序を変更することで切り替えが可能なようです。 今回の場合、["ja-JP", "en"]を["en", "ja-JP"]と変更することで切り替えられます。 この他にも、設定アプリで追加されていない言語を追加する方法や、リセット方法なども含めてデモンストレーションを交えながら詳細に説明していただきました。 次に興味を持ったのは、 「翻訳SaaSの導入で翻訳者にプルリクを出してもらおう」 です。 この項目では、エンジニアでない方も翻訳変更に対するプルリクが発行できるような仕組みについて紹介されました。 ここではCrowdinというSaaSを例に説明されました。CrowdinとGitHubの連携時に、管理したいLocalizable.stringファイルを連携すると翻訳リストを管理するUI上で翻訳の変更が可能になります。 変更することで、プルリクが発行されるという仕組みになっているようです。 WEARアプリでは、英語と中国語の翻訳をそれぞれ別の方が担当して対応しています。私たちは、翻訳が必要な文言のやり取りをConfluenceやSlackを使ってやり取りして対応してきました。 現状の運用方法ですと翻訳依頼の取りこぼしや反映漏れなどが稀に発生していたこともあり、改善策を考えていました。そんな中で、今回のような具体的な運用例をあげての紹介はとても参考になるお話でした。 その他にも、翻訳ネームスペースを使った翻訳文言の管理方法や文字装飾のローカライズの方法などが紹介されていて、今後迷った時に参考になる発表内容でした。 会場の様子 思いやりが詰まったセッション会場 選出された登壇者によるセッションは、いくつかのテーマを同時進行でそれぞれの講義室で行われます。 セッション会場は、写真のように席がぎゅうぎゅうになることもあり、セッションに対する参加者の注目度の高さが伺えます。 iOSDC Japan 弊社の名取や技術顧問の岸川さんのセッションの際も、席は参加者で埋め尽くされてました。 運営の方々の1人でも多く参加者がセッションを聞けるようにという想いから「機械的席詰め」というシステムが度々施行されました。 これは、三人掛けの席で右側が空いていたら機械的に右に詰めるシステムという仕組みのものです。 会場の思いやりによって生まれた、この理想的なゾーンディフェンスのように美しいシステムに、私は感動を覚えました。 セッションは今後プロダクトに関わっていく上でどれも興味深く、勉強になりました。 各登壇者が発表の中にデモンストレーションを組み込んでいたり、笑いどころを入れるなどの工夫も見られ楽しく発表を聞くことができました。 活気にあふれたブース会場 iOSDC Japan 2019ではトークセッションの他にも、様々な企業が軒を連ねるブース会場があります。 セッションの合間や、お昼休憩などセッションのない時間帯は特に参加者の方々で会場はとても活気付いていました。 iOSDC Japan この会場ではイベントのスポンサーとして協賛している企業がブースを出展して、参加者とのコミュニケーションを図っていました。 私たちZOZOテクノロジーズのブースを少し覗いてみましょう。 iOSDC Japan 今回ZOZOテクノロジーズでは、以下のテーマでアンケートを用意しました。 アプリのサポートiOS アプリのSwift率 アプリの対応デバイス アプリのレイアウト実装の割合 iOSDC Japan 参加者の方々のアンケートへの協力もあって、たくさんの方々と交流することができました。 実際にアプリを開発しているエンジニアや広報担当が参加者の皆さんの質問に答えるといった、お話もさせていただきました。参加者の皆さんと接する機会をいただけたことで、私たちもとても楽しい時間が過ごせました。 アンケート結果は、Twitterの公式アカウント上でハッシュタグ#iosdc #zozotechにて載せております。是非ご覧になってください。 1日目 iOSDC Japan 2019、本日ZOZOテクノロジーズブースで行ったアンケート結果はこちらです!投票ありがとうございました。明日は別のアンケートを行う予定ですので、お楽しみに! #iosdc #zozotech pic.twitter.com/9pcUa5Wg4s — 株式会社ZOZOテクノロジーズ (@zozotech) September 6, 2019 2日目 iOSDC Japan 2019 本日のアンケート結果はこちらです。皆様投票ありがとうございました! #iosdc #zozotech pic.twitter.com/QE5247rgIk — 株式会社ZOZOテクノロジーズ (@zozotech) September 7, 2019 食事は行列の先に iOSDC Japan 午前中のセッションが終わると参加者の行列が。その先には、参加者分のお弁当とお茶の用意がありました。 iOSDC Japan 私が食べたのは、野菜を豚肉でぐるりと巻いた豚肉巻きとシャケの切り身に明太子のソースが絡み合ったおかずのお弁当。 副菜には、ゴーヤやゆで卵といった色とりどりの食材を使ったサラダが添えられており、セッションの合間の休憩時間を彩ってくれました。 After iOSDC Japan 2019について 9/24(火)弊社オフィスにてSansan様、JapanTaxi様、弊社ZOZOテクノロジーズの3社による合同イベント「After iOSDC Japan 2019」が行われます。 iOSDC Japan 2019を振り返りたい方や感じたことを共有したい方は是非お越しください! zozotech-inc.connpass.com 最後に ZOZOテクノロジーズは、ファッションを技術の力で変えていくというミッション達成のため一緒に働く仲間を募集しています。気になる方は是非応募してください! https://tech.zozo.com/recruit/mid-career/detail50/ tech.zozo.com
アバター
こんにちは。カート・決済チームの濱砂です。 今回はZOZOTOWNのサーバーサイドの監視方法や取り組みについて紹介します。 はじめに 監視の課題 1. 可視化 2. アラートの検知 3. エラーの管理 改善後 1. Datadogで可視化 Sample Script DSL DatadogのDashboard 2. DatadogとPagerDutyでエラー検知 DatadogのSlack通知 DatadogのAlertの設定 PagerDutyのスケジュール設定 3. Sentryでエラーの管理 Sample Script Sentryに送られたエラー一覧 まとめ 最後に はじめに 現在、ZOZOTOWNでは現行のシステム基盤や開発プロセスをリプレイスするプロジェクトが進んでいます。 リプレイスは順調に進んでいますが、未だ多くは現行のまま動いており、在庫管理を行う基幹システムやバッチ処理なども同様です。 ⽇々多くの機能や運⽤が増え続けているため、現行のシステムもモダンなツールや開発スタイルを取り入れて、日々の業務の効率化やシステムの安定性を向上させる必要があると考えていました。 また、現行のシステムを担当しているエンジニアも今後は新しいリプレイス後の開発に移行する必要があります。 そのため、モダンな開発スタイルや思想を普段の業務の中に取り入れて、忙しい開発の現場でも少しずつキャッチアップする仕組みが必要だと考えていました。 このような背景があり、先ずは監視に焦点を当てて現行の課題を改善しつつ、モダンな開発スタイルを取り入れる取り組みを進めています。 監視の課題 現行のシステムのアプリケーションの監視には独自ツールを使用しています。メトリクス収集 、ロギング、可視化、アラート通知まで基本的にはこのツールを使用して運用しています。 ZOZOTOWN開設当初からシステムの特性を考慮して作り込まれているため、使いやすい点も多く、個人的には何度も障害を乗り越えるために助けてもらったので愛着もあります。 しかし、サービスの規模が大きくなり開発者も増えていく中でいくつか課題が出てきていたため、その課題を改善するための取り組みを行いました。今回はその中からいくつかピックアップして紹介します。 1. 可視化 アプリケーションのエラーログは独自ツールで収集してSQL ServerのDBに保存しています。 ログの内容を見るためのモニター機能はありますが、エラーの件数を集計してグラフ化したり、キャンペーンの登録数などのエラー以外のメトリクスを見たい場合は別の方法で見る必要がありました。 方法としては開発者がDBから直接SQLクエリを発行して抽出を行い、表やグラフを手動で作成していました。それほど時間はかかりませんが、あまり効率的な作業ではありませんでした。 2. アラートの検知 アラートの検知方法は独自ツールのモニター機能を使うかメール通知のため、深夜帯は気付きにくく対応が遅れることもありました。 また、監視業務をSREチームや協力会社の方々に頼ることも多く、自分たちで気付けたとしても監視意識の高い開発者だけに業務が集中してしまう課題もありました。 3. エラーの管理 直近のエラーの内容や件数を見るためのモニター機能はありますが、集計やイシュー管理は手動で行なっています。 また、業務時間外にアラートの通知が来た場合、独自ツールを見るためには社内ネットワークに入る必要があります。 そのため、接続に時間がかかって対応が遅れることや、必要な環境を持ち合わせていない場合は確認が出来ないこともありました。 改善後 1. Datadogで可視化 可視化にはDatadogを導入しています。Datadogは監視に必要な一通りの機能を備えたMonitoring SaaSです。グラフの作りやすさ、見栄えの良さに定評がある、グローバルスタンダードなツールの1つです。 先ず、エラーやメトリクスの情報をDatadogに定期的に送る必要がありました。 そこで、Datadogが提供しているAPIを使い、DBから取得した情報をカスタムメトリクスとしてDatadogに送信するPython Scriptを作成しました。これをDocker Containerの中で1分に1回、バッチ処理としてcronで実行しています。 Sample Script from datadog import initialize from datadog import api # 初期化 options = { 'api_key' : '<DATADOG_API_KEY>' , 'app_key' : '<DATADOG_APP_KEY>' } initialize(**options) # パラメータのセット # 実際はDBから取得した値をセットしています metric_name = "zozo.custom.error" value = 1 tags = [ 'env:prd' , 'project:zozo' ] # Datadogにメトリクス送信 api.Metric.send(metric=metric_name, points=value, tags=tags) 当初は独自ツールのモニター画面をスクレイピングする仕組みでやっていましたが、メトリクスの追加、変更をする場合はスクリプトの修正が必要という課題がありました。 そのため、各メトリクスの取得条件はDSLにSQLクエリとして記述するようにして、追加や変更が容易に出来るようにしています。 DSL metrics_name : sql_server.custom.zozo.error monitor_sql : | SELECT COUNT(*) AS total_count FROM ErrorTable WITH(NOLOCK) WHERE errorType = 1 monitor_value_positions : - total_count:0 次に収集したメトリクスを使い、DatadogでDashboardを作成して、それを各メンバーが閲覧出来るようにしました。 定期的に見たい情報のDashboardを作成したことで、毎回手動で可視化する必要が無くなりました。 DatadogのDashboard 2. DatadogとPagerDutyでエラー検知 1で収集したメトリクスを使い、DatadogのAlert機能を利用してSlack通知するようにしました。Slackは社内でコミュニケーションハブとして使われていることもあり、エラーの通知に気付きやすくなり、チーム間の情報共有やコラボレーションも円滑に行われるようになりました。 DatadogのSlack通知 また、緊急度の高いインシデントが発生した場合でもすぐ気付けるように、PagerDutyというオンコールシステムを実現するためのSaaSを導入しました。 PagerDutyはオンコールに関する機能が豊富なツールで、インシデント発生時に電話やSMSで通知出来ます。 先ずDatadogとPagerDutyを連携します。その後、オンコールを設定したいDatadogのAlertにPagerDuty用のメンションを設定します。 ここまで設定するとインシデントが発生した場合、PagerDutyの設定の内容に応じてオンコール通知されるようになります。これで深夜帯のアラートも自分たちで気付けるようになりました。 DatadogのAlertの設定 PagerDutyには担当者をカレンダーで設定する機能もあります。また、一次受けの担当者が応答できなかった場合に自動的に二次受け以降の担当者にエスカレーションする、といったようにな機能も備わっています。 これを機に複数人担当の週替わりの運用にしました。スケジュールの管理はPagerDutyで自動行うようになったため、属人的に行ってきた開発者の負担も下げることが出来ました。 初めは慣れない開発者から不安の声もありましたが、フォローしながら少しずつ慣れてもらうことで、監視に対する意識や姿勢も底上げされました。 PagerDutyのスケジュール設定 3. Sentryでエラーの管理 エラーのログの管理にはSentryを導入しています。Sentryはエラーの詳細を収集して可視化出来るサービスです。 対応言語も多く本来であれば導入し易いツールですが、現行ZOZOTOWNのサーバーサイドの使用言語はサポートされておらず、そのまま導入することは出来ませんでした。 そのため、SentryのAPIを使って1と同様に独自ツールでロギングしているエラーの情報をSentryに送信するPython Scriptを作成して定期実行しています。 Sample Script from raven import Client #初期化 client = Client( 'SENTRY_DSN' ) # Sentryにエラー情報送信 # 実際はDBから取得した値をセットしています client.capture( 'raven.events.Message' , message= 'description' , date= 'errorDt' ) ,data={ 'tags' : { 'id' : 'id' , 'serverId' : 'serverId' , 'userAgent' : 'userAgent' , 'requestMethod' : 'requestMethod' , 'scriptName' : 'scriptName' , 'mediatedPath' : 'mediatedPath' , 'referer' : 'referer' , 'serverName' : 'serverName' , 'service' : 'service' , 'line' : 'line' , 'column' : 'column' } }) エラーの情報をSentryに集約したことで、社内ネットワークに入らなくてもエラーの内容が確認出来るようになりました。 Sentryに送られたエラー一覧 今のことろエラーが精査されておらず、件数が膨大なこともあり、全件は送らず閾値を超えた場合のみSentryに送信しています。 全件送るようになれば、Sentryで集計やイシュー管理も出来るので引き続きブラッシュアップしていく予定です。 まとめ 現行のアプリケーションの開発現場にモダンなツールや仕組みを導入したことによって、どのような変化や恩恵があったかについて紹介しました。 ツールは導入して終わりということは無く、日々ブラッシュアップしたり、新しい機能に対応していく必要があります。 また、現行のシステムを改善するには作りや課題を深く理解して、そのシステムに合うやり方で導入する必要があるため、時間も掛かり何かと苦労することも多いです。 しかし、試行錯誤しながら地道に改善に取り組むことはサービスを良くしていくためには必要なことで、リプレイス後もそれは変わらないと考えています。 現行のシステムを深く理解して本質的な改善を行っていくためにも、現行のシステムやそれを支えてきた方々へのリスペクトの気持ちを忘れず、引き続きこのような取り組みを続けていきます。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集しています。 ご興味のある方は、 こちら からぜひご応募ください。
アバター
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。今回は7月に実施した 開発合宿 に関して、計画から実施まで運営側がどのようなことを行ったのかをお伝えします。これから開発合宿を計画している方や今ある開発合宿をより良いものにしたい方々に一つでも有益な情報を提供できたら幸いです。 合宿実施概要 日程 7/11(木)-12(金) 場所 おんやど恵(湯河原) 参加者 34名(エンジニア32名、広報1名、人事1名) 実際に開発合宿の計画から7月の実施までどのようなことを決めていったのか、ここから時系列に沿って説明していきます。 前期 予算確保 4月上旬 実行委員メンバーの選出 4月中旬 テーマと実施概要の検討 宿の仮予約 5月上旬 経営会議での承認 5月中旬 人数調査(第1回 アンケート) 声がけ 5月下旬 本応募(第2回 アンケート) 6月上旬 開発内容の事前共有 合宿のしおり作成 6月下旬 懇親会・遅刻・早退調査(第3回 アンケート) 7月上旬 予約の確定 機材の用意 賞品の用意 特急踊り子のきっぷ購入 座席・部屋の割り振り スライドの雛形用意 タクシー乗車準備 各部署との連携 直前のメンバー変更 写真への写り込みの確認 7月中旬 最終的な動きとアサインの決定 当日 東京駅に全員定刻集合 昼食の弁当争奪戦 2日間のスケジュール 合宿後 事務処理 事後アンケート(第4回 アンケート) Q. 開発合宿全体の満足度を教えてください Q. 次回があれば参加したいですか? 成果物 報告書の作成 リスト まとめ 最後に 前期 あらかじめ確保しておくと実施がスムーズになるのが予算です。 予算確保 開発合宿を行う場合、予算が当然必要になります。そのため、まずは予算の確保を行っておきましょう。前期のうちから、来期予定している開発合宿の予算を確保します。これがないと、なかなか「今期開発合宿やりたい」となった時に予算外で取得は難しいことが多いでしょう。必要な場合は経営会議などで予算と実施の承認も事前に得ておきます。 4月上旬 新年度になり、「今期は開発合宿やりたいね」と実施を計画し始めた時期です。 実行委員メンバーの選出 実際に開発合宿をやろうとなったら、推進役となる実行委員を選出しましょう。今回のZOZOテクノロジーズの場合は、CTO室が主軸となり計画をしました。しかし、CTO室は今年4月に新設されたばかりの部署でありCTO室だけではリソースが足りなかったため、開発部より有志で1名実行委員として動いてもらいました。CTO室1名、有志1名の2名で計画から実施まで行いました。 4月中旬 ここから本格的に実施に向けて内容を検討していきます。 テーマと実施概要の検討 今回はZOZOテクノロジーズとして初の開発合宿であり、業務として1泊2日の時間を利用しました。そのため、合宿の成果が業務に貢献できるもの、業務の効率をあげるようなものを作るというテーマを設定しました。 このテーマにした背景には、合宿参加者だけでなく、参加しないエンジニアや他職種の人も開発合宿の恩恵を受けられるようにしたかった点があります。「エンジニアが開発合宿で2日間オフィス不在になるけど、帰ってきたらすごい役に立つものを作ってきてくれるから是非行ってきてくれ」となるような全社で開発合宿に対するポジティブな空気づくりを目指しました。その結果、次回以降の開催時に周囲の理解も得やすくなり、開催がスムーズになることを期待しています。 そして、テーマとは別に開発合宿の意義と目標を明文化しました。合宿後にこれらの項目に関しての達成度を確認をします。 【1】成果物で業務の効率アップにつなげる ・普段の業務の隙間時間ではなく、集中してまとまった時間を確保することで短時間で効率的に開発できる ・成果物が合宿後の業務に役立つ 【2】技術に集中する時間を設けスキルアップ ・普段いじらない・軽くしか触れていない技術を深く利用し、技術スキルUP ・ものづくりをする思考トレーニング ・周りの人がすごいことをやるのを見て、自分も頑張らないと、と思う機会にし、今後の成長曲線を上振れさせ、今後のスキルアップへつなげる 【3】一つの場所で、「開発」をすることによりお互いを深く知る ・新旧・別拠点メンバーの交流機会を設け、今後の業務をさらに円滑にしやすくする 【4】対外的PR ・テックブログで #ZOZOは楽しく働く な発信、及び技術的に面白い成果があれば、それ自体もネタになる 宿の仮予約 次に、宿の候補を絞り込み、宿が空いている日程を確認します。これによって日程の候補は自然と絞られていくでしょう。 同時に、必要な費用も見えてきます。 宿の候補は他社のテックブログなどを参考にしつつ検討しました。 合併前のVASILYの時に一度開発合宿を実施した ことがあり、その際に利用した おんやど恵 も候補でした。前回の知見を活かせることや、前回の課題としてあげられていた「気軽に行ける距離にコンビニがない」という点が今では宿のすぐ隣にコンビニができて解消したこともあり、「おんやど恵」に決めました。 足湯に浸かりながら開発できるのもおんやど恵のポイントです。 この段階では参加人数は見えていないので、最大人数を想定して予約しておきました。その際には人数変動があることを伝え、いつまでに最終的な人数を連絡すればよいか確認しておきます。 5月上旬 宿も仮押さえができて開発合宿の開催も具体的にイメージができるようになってきました。そこで、経営会議にて開催の承認を得ます。 経営会議での承認 予算は確保していましたが、宿や場所も見えてきたのでそこから算出した費用と、エンジニアが1泊2日でオフィス不在になることを経営会議で伝え、今期実施の承認を得ました。 その際の資料には、開発合宿の目的、日程、場所、参加想定人数(今回は40人想定とした)、概算予算と内訳、を記載しました。 5月中旬 より具体的に準備を進めていくため、参加者の規模を把握します。 人数調査(第1回 アンケート) これから準備を進めていく上で、実際にどれくらいの参加者がいるのか規模感を把握しておく必要があります。これによって成果発表の時間などタイムスケジュールに影響したり、費用が変動したり、はたまた参加者がいなければ開催ができなくなることも考えられます。なお、今回の開発合宿は強制参加ではなく、参加の意思があって業務の都合が付く開発部のエンジニアの任意参加としました。 エンジニアに開発合宿を実施することを展開して、現時点の参加意思を確認します。まだこの段階では業務の都合がどうなるのかも不明瞭で参加者の確定はできませんが、ざっくりとした規模感を確認するために簡単なアンケートをSlack上で実施しました。合宿の概要は経営会議向けに用意した資料をベースに、費用などは除いた簡易版にして展開しました。 特に宿の予約人数が変更になる場合、人数を減らす対応はできますが、急に人数が増えてしまうと空き部屋の数など物理的に対応しきれなくなる可能性もあります。そのため、このタイミングの調査では少しでも参加する可能性がある人は全員カウントしておきたいという気持ちがありました。そのような数字を取得できるよう、この回答がそのまま参加表明ではないということを伝えた上で、合宿に興味があるエンジニアの人数を調査しました。また、この段階では参加者不足で開催不可の可能性も無いとは言い切れないので「開発合宿開催します」とは言い切らず「開発合宿を開催した場合」という表現をするように気をつけました。 なお、開催日は全社的な予定やリリースが無さそうな日程を運営側で定め、さらに宿に40名規模でも空きがある日程を確認し、実施予定の日付を絞り込んで決めました。 声がけ 前述のアンケートを開始し、翌日には30人の回答がありました。しかし、回答者の内訳を確認すると、拠点の偏りがありました。開発部は大きく青山オフィスと幕張オフィスの2拠点に分散しています。そのため、今回の合宿では普段接しない別拠点エンジニアとの交流も目的の一つです。ところが、この時点では青山オフィスと幕張オフィスの参加希望者の比率が 2:1 でした。そのため、幕張オフィスのリーダーなどに声がけをし、幕張オフィスで参加したい人が他にいないのか確認をしてもらいました。 その際のやり取りの中から得られた知見としては、特に開発合宿にこれまで一度も参加したことがないメンバーだと「何かすごいものをつくらないとけないのでは」というようにハードルを高くしてしまっている可能性が高いということです。そのため、何を作るのか・どんな難易度のものを作るのかは気にせず、それぞれの状況・レベルに合ったものを作れたらそれで良いということを説明し、心理的ハードルを一つ取り除きました。それにより、幕張オフィスの参加者も増加しました。 5月下旬 参加者の本応募を実施し、実施に向けて準備を進めていきます。 本応募(第2回 アンケート) 事前アンケートにより最少催行人数以上の参加希望者がいることが確認でき、拠点間の偏りも解消できたので予定通り開発合宿の実施が確定しました。 業務上の都合が今後どうなるか分かりませんが、現時点での参加希望者の本応募を受け付けました。やはり、事前調査の回答と差は出てくるので、このタイミングでも再度声がけをします。応募を開始し、翌日には14名、1週間後には31名、6月上旬には最大で38名の状態にりました。38名の内訳は青山オフィスのエンジニア19名・幕張オフィスのエンジニア17名・広報1名・人事1名です。 このタイミングで、広報と人事が1名ずつ参加することも決まりました。広報と人事は開発はしないので、リモートワークをしつつ開発合宿というものを肌で感じてもらいました。広報は カンパニーブログの執筆 や写真撮影、Twitterの公式アカウントでの発信を行いました。人事は今後社内イベントで合宿をする際の勉強のために参加しました。そのため、人事へは準備段階から経費精算の出し方まで情報を逐次共有しました。 その後、業務上の都合などで参加できなくなる人も発生し、最終的には34名(青山オフィス16名・幕張オフィス16名・広報1名・人事1名)となりました。 6月上旬 合宿まで残り約1ヶ月。各自が合宿で何を作るのか事前に考えておきます。 開発内容の事前共有 合宿に行ってから何を作るのか悩んでいては時間がもったいないです。合宿の2日間は開発時間に使えるよう、事前に作るものとそれが今回のテーマである業務効率化にどうつながるかを考え、書き出して事前共有してもらいました。「当日までに考えておいてね」とリマインドするだけでなく事前に全員の決まったものを共有することにより、なかなか作るもののイメージがわかない人にとっては他の人のテーマがヒントになったり、他の人たちが決まっていく様子を見て「自分も早く考えなきゃ」と考えるきっかけになったりを狙いとしていました。 合宿のしおり作成 いわゆる合宿のしおりのような、情報を集約したページを用意します。タイムスケジュールや目的など、確定しいる情報の共有が目的です。決まっていない部分は随時アップデートしていくページとなるので、「合宿のことを知りたければここを見ればOK」という場にしました。また、参加者への連絡用にSlackに専用チャンネルも用意しました。 6月下旬 交通手段を確定するために、追加の調査を行います。 懇親会・遅刻・早退調査(第3回 アンケート) 合宿2日目には成果発表会を実施し、その後懇親会を開催します。懇親会は強制ではなく、任意参加としました。その懇親会の参加有無や遅刻・早退の予定があるのかを調査します。この人数により、電車の指定席予約人数を決めます。そのため、このアンケートの目的も伝えた上で、何か変更の可能性がある場合は事前に相談してもらうようにしました。今回は業務上、2名が遅刻しての参加になりました。 なお、懇親会はこの調査段階では数名参加せずに先に帰る予定でしたが、合宿当日は発表もオンスケで進み、終了時間も遅くならなかったので全員参加して帰りました。 7月上旬 合宿まで残り1週間程度。準備も最終段階です。順次予約も確定していくので、実際に掛かる費用の明細も固まってきます。 予約の確定 人数は確定したので、宿や昼食の予約を確定させます。宿は合宿プランの場合、宿泊人数を伝えたらそれに応じた部屋数が指定されます。昼食は、宿に依頼するのではなく近隣の弁当を配達してくれる業者に注文し、駅弁を16種類用意しました。同じ弁当を配布するだけでなく、弁当争奪じゃんけん大会で楽しめる要素を用意しました。 機材の用意 2日目の成果発表会では、プロジェクターを2台レンタルしています。1台は運営側でスライドを表示、もう1台のプロジェクターでデモを行うように計画しました。デモを行うプロジェクターは発表者の入れ替えをスムーズにするため、HDMI切替器とHDMIケーブルを用意して持参しました。しかし、当日はHDMI切替器とプロジェクターの相性が良くなかったためか、HDMI切替器経由でのプロジェクター出力はできず、1本のHDMIケーブルで実施しました。 賞品の用意 成果発表会では、賞品付きの賞を6個用意しました。具体的にどんな賞を設けるのかは、当日の発表の内容を見ないとなかなか適切なものを定められません。そのため、賞の数だけは6個と事前に定めておき、賞品を用意しておきました。賞品はArduino・Raspberry Pi・IoTデバイス・スクリーン付きスマートスピーカーなどを用意しました。 特急踊り子のきっぷ購入 往路は最初から参加できる32名で移動です。そのため、確実に座れるよう、特急踊り子で指定席を確保しておくことにしました。 計画時、特急踊り子の情報をオンラインで調べるのには苦労しました。曜日だけでなく時期により列車の有無が変化したり、そして今回の目的地である湯河原に止まらない踊り子もあるため、そもそも選択肢としてどの列車があるのかも把握が難しかったです。結論として、みどりの窓口に行き、そこに置いてある紙の時刻表を見るのが一番分かりやすいという結論に至りました。何でもオンラインが最適なのではなく、アナログな表現手法や仕組みが適している場合もまだまだあるなと感じる瞬間でした。 無事に同じ車両にまとまって座席を用意することができました。復路は懇親会終了後のちょうど良い時間帯に踊り子が無いことと、発表会や懇親会で時間が変動する可能性もあるのできっぷは事前に用意しませんでした。 座席・部屋の割り振り 往路の踊り子の座席、開発時の席、昼ごはんの席、宿泊する部屋の割り振りも事前に決めておきます。今回の合宿の目的に別拠点のエンジニアとの交流も含めているため、まず座席を拠点ごとにバランス良く混ざるように2種類に分割しました。左右前後が別拠点の人になるよう、交互に配置しました。その後、各拠点のメンバーをランダムにシャッフルして割り振っていきます。発表の順番も同様にシャッフルをして決めました。 開発部屋は2部屋用意しました。 スライドの雛形用意 合宿で使うスライドの雛形を用意します。今回は、発表時のスライドと表彰時のスライドの2種類を用意しました。 1つ目は発表用のスライドです。発表用のスライドは、参加者がスライド作成に時間をかけ過ぎないように心がけました。そのため、各自発表スライドは1枚までとしました。Googleスライドに発表順に氏名だけ埋めたものを用意しました。発表時間も一人2分と短いので、スライドも発表も要点を簡潔にまとめてもらい、発表はデモをメインで行う形式にしました。32名の発表で、全体で約2時間の発表会です。 2つ目は表彰時に使うスライドです。表彰時のスライドもこのタイミングで仕込みをしておきました。6個分の賞のスライドを用意し、そこには賞品の内容や写真を埋めておきました。当日は賞の名前・受賞者・発表タイトルを入力するだけの状態にしておきます。スライドはGoogleスライドを利用しているので、この段階ではスライド作成者と当日の審査を行うVPoEのみに権限を付けておき、開発合宿後に社員全体へ閲覧権限を付与しました。 タクシー乗車準備 湯河原駅と宿の移動はタクシーです。タクシー約8台に分乗することになるので、誰がどのタクシーに乗ってもスムーズに宿に行けるよう、台数分の封筒を用意しました。その封筒にはお金と宿の住所を印刷した紙を入れておき、タクシーから降りたら領収書とお釣りを入れた状態の封筒を回収しました。回収後は領収書と残金の確認をし、封筒に納入されている現金の金額を記載して、復路の際に再度利用します。 封筒には通し番号を付けてあり、タクシーに乗る際に誰に何番の封筒を渡したのかはチェックしておきました。このような誰に何をしたのかのメモをしやすいよう、参加者の名簿は何枚か印刷して持参していました。 なお、前述の遅れて参加するメンバーには前日までに封筒をオフィスで渡しておきました。 各部署との連携 今回の合宿費用は運営側で一括で立て替えて支払うものが多く、念の為経理に内容と申請フローの確認をしておきました。また、参加者の勤怠登録の確認など、参加者自身が問い合わせをしなくて済むよう、疑問点として上がりそうな部分は事前に関係部署に確認し、前述のしおりのページにまとめておきました。 事前に各部署と連携し、参加者が困るであろう点は情報をまとめてしおりにまとめておきました。 合わせて、宿や弁当の業者は当日の支払いにしてあったので、現金なのかカード払いなのかの確認もしておきます。これにより持参すべき現金の量が大きく変わってきます。 次に、会社からの貸与PCを持ち出すことになるので、社内のPC全台に導入しているデバイス管理ソフトが入っていることの再確認と、紛失には気をつけるよう周知を行いました。 また、開発合宿の様子をTwitterへ投稿する際のハッシュタグ #zozotech開発合宿 の運用も広報と相談しました。ポイントとしては、夜の懇親会時にはアルコールも用意することになると思いますが、日中の業務として開発中の投稿では、良からぬコメントをされないためにも、未開封のアルコールの缶や空き缶などが机の上に放置して写り込まないよう気をつけるのが良いかと思います。 直前のメンバー変更 合宿1週間前、業務都合で行けなくなるメンバーが発生しました。キャンセルで予約を再調整するのも手ですが、せっかくなので代わりに参加できるメンバーを募集し、予定通りの人数での決行になりました。参加者は最後までどうなるか分からないので、予約をする際にはそのことを忘れずにリカバリ策を検討しておくことが必須です。 写真への写り込みの確認 開発合宿の様子はブログやTwitterなどに発信します。その際、写真に写り込みたくないメンバーもいます。写り込みたくない場合は、それを考慮する必要があるため事前に調査し、当日の撮影時には配慮をします。 7月中旬 合宿まであと数日。最後に細かいアサインを決めて事前準備は完了です。 最終的な動きとアサインの決定 細かい運営側のタスク確認とアサインを行います。以下にそれぞれ検討したタスクをまとめています。 前日まで USB Type-C HDMIアダプタとバインダーを用意するタスク。 集合時 出席確認と該当の座席指定のきっぷを渡すタスク。事前に電車の座席番号を記載した出席者リストを印刷しておく。 湯河原到着時 タクシー代と住所が入った封筒を渡し、誰にどの封筒を渡したのか出席者リストに記載するタスク。 宿到着時 宿にチェックインするタスク。 チェックイン後 タクシー代表者から領収書とお釣りを回収し、確認をするタスク。 昼食前 弁当の受け取りと支払いをするタスク。 昼食時 弁当を配布するタスク。 発表時 発表資料のスライドの表示・操作をするタスクとタイマー係。前半と後半でそれぞれアサイン(前半に発表する人は後半にアサイン、後半に発表する人は前半にアサイン) 発表後表彰までの休憩 表彰の内容を考えてスライドを埋めるタスク。 表彰時 表彰をするタスクと、賞品を渡すタスク。 宿出発前 宿をチェックアウトするタスク。 宿出発時 タクシー代と住所が入った封筒を渡し、誰にどの封筒を渡したのか出席者リストに記載するタスク。 駅到着後 タクシー代表者から領収書とお釣りを回収し、確認をするタスクときっぷをまとめて購入するタスク。 当日 いよいよ合宿当日です。 東京駅に全員定刻集合 初日の朝が最大の難関とも言えます。全員指定の電車に乗るという重要なイベントが待っています。予想はしていましたが、運営側としてもやはりこのイベントが一番の難所でした。いくら準備をしてもコントロールできない部分です。 当日は9:00発の特急踊り子に乗車のため、8:45東京駅集合にしました。集合場所はしおりに地図のリンクも添えて共有し、当日も一番乗りで集合場所へ行って写真をSlackで共有しました。準備は整いました。以下、手に汗握る15分間の流れです。 8:45 指定の集合時間。出欠を確認するが3名ほど足りず。 8:48 実行委員の代表者だけ集合場所に残り、他の参加者はホームへ移動。 Slackでやり取りしながら全員集合場所に現れるのを待つ。 8:55 最後の一人が集合場所ではなくホームに出現。Slackのその連絡を見て、実行委員も集合場所からホームへ移動開始。 8:56 電車の扉が開いて順次乗車 8:58 全員乗車 9:00 出発 なんとか全員間に合いました。次回以降はきっぷは事前配布にした方が良さそうです。 昼食の弁当争奪戦 合宿中に2度ある昼食。宿に依頼すれば仕出し弁当を手配してくれます。そうすれば注文や支払いもまとめてもらえるので楽なのですが、今回はあえてそれは利用せず、直接、駅弁を注文しました。特に高いものやおにぎりのような簡易的なものを除いた全種類、16種類を揃えました。この中から好きなものを選んでもらえば昼食も一つの楽しみになります。 じゃんけん大会を実施し、VPoEとじゃんけんで買った人から順に選んでいく形式にしました。 2日間のスケジュール 2日間の開発合宿、全体のスケジュールは以下の通りです。集合が遅かったり、昼食を弁当ではなく外で食べようとすると、初日の開発時間が短くなってしまうため、なるべく初日の午後はすべて開発時間に当てられるようにしました。 1日目 8:45 集合 9:00 東京駅発 10:14 湯河原駅着 10:30 宿到着・チェックイン 11:00 開発開始 12:00 昼食 開発 18:00 夕食 開発 2日目 8:00 朝食 開発 9:30 チェックアウト 開発 12:00 昼食 13:00 発表 15:15 表彰 懇親会 18:00 宿出発 帰路 合宿後 事務処理 合宿終了後、忘れてはいけないのが経費精算。すべての領収書をなくさずに保管しておきましょう。参加者には集合場所までの交通費精算と勤怠登録の案内を出しておきます。 事後アンケート(第4回 アンケート) 次に、記憶が鮮明なうちに参加者にアンケートを回答してもらいます。このアンケートでは、当初目標としていた項目に対してどれくらい達成できているか、次回の合宿に向け、今回の合宿で良かった点・改善点などを回答してもらいました。回答率は100%です。 集計結果の一部を公開します。 Q. 開発合宿全体の満足度を教えてください Q. 次回があれば参加したいですか? どちらの質問も、参加者の6〜7割が最高点を付けています。初回の合宿としては成功と言えるでしょう。2つ目の設問で3と回答している理由としては、「ネタがあれば参加したい」「別の人にも参加して欲しいから」というものがありました。 成果物 合宿で開発したソースコードは、記録として残すためにGitHub上にリポジトリを作り、そこに集約しました。また、この合宿で開発したもので業務で利用しているものや、 Rubyのgemとして公開 されたものもあります。 報告書の作成 開発合宿の締めくくりとして、報告書を作成します。上記のアンケート結果や成果も含め、一つのページにまとめます。このページを参考に次回開催時の説明材料にしたり、次回の開発合宿計画時にPDCAしていくための備忘録にもなります。 リスト 今回紹介したタスクのリストをGoogle スプレッドシートにまとめたので共有します。是非ご活用ください。 docs.google.com まとめ ZOZOテクノロジーズとして初めての開発合宿開催に伴い、その準備工程をお伝えしました。開発合宿は開催頻度も多くはできないのでなかなかPDCAもしにくいイベントです。そのため、各社の開催レポートなどの情報発信が大切かと思います。今回お伝えした内容のうち、一つでもみなさまの開発合宿計画の改善につながれば幸いです。 最後に ZOZOテクノロジーズでは、開発合宿の企画・運営を行った「全社における技術的な戦略策定および、エンジニア組織強化のための施策の推進」をミッションに掲げたCTO室のメンバーも募集中です。また、開発合宿に参加したり、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
ZOZO Technologies dev camp 2019 summer こんにちは。WEARリプレイスチームの id:takanamito です。 先日、開発部のみんなで行った開発合宿でteyuというgemを作ったのでその紹介をしようと思います。 github.com 開発合宿の様子はこちら techcorp.zozo.com teyu このgemは、クラスをnewするときに渡した引数を、そのままインスタンス変数に代入するコードが簡潔に書けるようになるgemです。 日本語で説明するより実際のコードを見たほうがわかりやすいと思うので以下に記載します。 通常の引数の場合 通常のRubyコードでこう書くクラスを class Sample def initialize (foo, bar, baz) @foo = foo @bar = bar @baz = baz end end teyuを使うとこう書けます require ' teyu ' class Sample extend Teyu teyu_init :foo , :bar , :baz end キーワード引数の場合 class Sample def initialize ( foo :, bar :, baz :) @foo = foo @bar = bar @baz = baz end end teyuを使うとこう書けます require ' teyu ' class Sample extend Teyu teyu_init :foo! , :bar! , :baz! end デフォルト値つきキーワード引数の場合 class Sample def initialize ( foo : ' foo ' , bar : ' bar ' , baz : ' baz ' ) @foo = foo @bar = bar @baz = baz end end teyuを使うとこう書けます require ' teyu ' class Sample extend Teyu teyu_init foo : ' foo ' , bar : ' bar ' , baz : ' baz ' end 開発の経緯 以前からZOZOテクノロジーズは技術顧問のMatzさんと月1回のMatz MTGを行っています。 techblog.zozo.com たいていそのMTGの前にRuby trunkのissueを見て疑問に思ったことや、最新のRubyの動向をキャッチアップしているのですが initializeメソッドの中で引数をインスタンス変数へ代入するコードが冗長で煩わしく思っている人が多いのか、似たようなissueがいくつも上がっていることに気が付きました。 Feature #5825: Sweet instance var assignment in the object initializer - Ruby master - Ruby Issue Tracking System Feature #8563: Instance variable arguments - Ruby master - Ruby Issue Tracking System Feature #12820: Shorter syntax for assigning a method argument to an instance variable - Ruby master - Ruby Issue Tracking System このissueについてMatzさんとお話したときに問題を解決するアイデアの1つとして上がったのが、teyuの様なinitializeメソッドを定義する機能を提供することでした。 ちょうど普段書いているRubyコードで同じようにインスタンス変数への代入を煩わしく思ったことがあったので解決策を調べてみるとattr_extrasというgemを見つけました。 github.com このgemでは attr_initialize という名前で同様の機能が提供されており、やりたいことは実現できそうでした。 ただ1つ、キーワード引数を表現するのにArrayとして引数を定義する書き方が自分の中で違和感がありました。 attr_initialize :foo, [:bar, :baz!] もう少し具体的に言語化するのであれば、Arrayからキーワード引数であることが連想しづらいというのが理由として挙げられるかと思います。 そう思っていた時に開発合宿が開催されることとなり、自分が欲しいinitializeメソッドを定義する機能だけをgemとして実装してみることにしました。 次のバージョンでは 初日の朝の時点で設計すらしていない状態で合宿に参加して2日目の昼には成果発表をする必要があったため、クラスや変数の名付けに若干違和感を残したまま駆け足で開発を終えました。 同様にパフォーマンスに関しても考慮するほど余裕がなかったです。 ※合宿先の宿には露天風呂があったのでしょうがないですね! おそらく次のバージョンとしてリリースするのは、リファクタと高速化を含むものになると思います。 まとめ 業務上どうしても普段では丸一日開発に没頭することは難しいのですが、開発合宿では久しぶりにプログラミングへ集中できたのでめちゃくちゃ楽しかったです。 WEARのリプレイスにおいてもteyuを活用できそうなプログラムが既にあるため、開発合宿のアウトプットを業務改善につなげるいいサイクルが作れそうです。 ZOZOテクノロジーズではこういったチャンスを生かして、技術の力で業務を改善することに興味があるエンジニアを募集しています。 ぜひ一緒に次回の開発合宿に参加しましょう。 www.wantedly.com
アバター
こんにちは。MLOpsチームリーダーの sonots です。 先日の プレスリリース で発表しました通り、ZOZOTOWNに「類似アイテム検索機能」を追加しました。この機能の技術要素について先日開かれた Google Cloud Next '19 in Tokyo で、本プロジェクトからは2件発表してきました! 技術要素が気になる技術オタクの皆様におかれましては、ぜひ資料と動画をご覧ください! ZOZO画像検索でのMLOps実践とGKEインフラ アーキテクチャ 筆者(そのっつ)の発表になります。発表の概要は以下になります。 ZOZOのAIプロダクトであるZOZO画像検索の概要とアーキテクチャについて紹介します。GKE(Google Kubernetes Engine)とgRPCを使った機械学習APIサーバの構築、Cloud Composerを使った定期的なモデルの更新について話します。特に、Kubernetesを用いてgRPCマイクロサービスアーキテクチャで構築されているHTTP APIサーバを本番レベルに引き上げるために行った工夫について共有します。 筆者は今回はじめてKubernetesを触ったのですが、Kubernetesクラスタを本番レベルに引きあげるために正直な所とても苦労しました。この発表は、まさしく少し前の私が教えて欲しかったものを全て盛りこんだものとなっています。今Kubernetesの検証を進めていて、これから本番導入を考えているタイミングの方に役立つ資料・発表になっていると自負しています。 補足:正式な資料はGoogleから10月に公開されるそうです。ここにはひとまずGoogleに許可をとった資料を載せておきます。正式資料が公開され次第差し替える可能性があります。 Googleが開発したAIプロセッサ『Cloud TPU』とZOZOTOWNでの活用事例 Googleの佐藤氏と福岡研究所の西原、家富の発表です。発表の概要は以下になります。 Cloud TPUは、ディープラーニングの学習と推論に特化した回路設計により従来に比べ大幅に高い価格性能比を実現するグーグルのAIプロセッサです。また、低遅延のインターコネクト技術で2048コアのTPUを結ぶCloud TPU Podは、AIスーパーコンピューティング環境を低価格クラウドサービスとして提供。このセッションでは、ZOZOTOWN画像検索プロジェクトでのCloud TPU導入事例を交え、その実力を解説します。 筆者も聴講しましたが、Googleの佐藤氏からTPUの概要を教えていただいた後に、生々しい現場の話をきけて同じ会社ながら非常に参考になるセッションでした。TPUに関してはまだ学習環境だけの利用となっており、本番での定常的な利用はできていないのですが、これから導入されていく予定です。 おわりに ZOZOTOWNへの画像検索機能の導入は以前から温めていたアイデアで、ついに満を持してリリースできました。 まだまだ改善は進めていく予定ですし、アレやコレやといった新しいAI案件も抱えています。是非一緒に働きましょう。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは。開発部基幹SREチームの廣瀬です。 弊社では、システムの一部にSQL Serverを使用しています。 本記事では、SQL Serverにおけるインデックスのメンテナンス方法である再構成と再構築について、それぞれを実行した場合のクエリ性能の比較結果をご紹介したいと思います。 比較を実施するに至った背景の前に、まずはインデックスの再構成と再構築について説明したいと思います。 インデックスの再構成と再構築 インデックス SQL Serverのインデックスについて簡単にご紹介します。下図は、SQL Serverのデータ構造の概略図です。 テーブルは、1つ以上のインデックスから構成されます。なお、ヒープという別のデータ構造だけからテーブルを構成することもできますが、今回は省略します。 インデックスは複数のページから構成されます。各ページは論理的につながりを持ちB-Treeを形成します。 ページとは、8KBの物理的に連続した領域のことで、各ページに実際のレコードが格納されています。 なお、インデックスの詳細なアーキテクチャについては SQL Server のインデックスのアーキテクチャとデザイン ガイド にまとめられています。 インデックスの断片化 SQL Serverのインデックスは、データの更新パターンによっては断片化が発生します。 断片化が発生すると、本来であれば1000レコードを10ページに格納できるはずが15ページにまばらに格納される、といった状態になります。 断片化により、データ読み取り時に読み取るべきページ数が増えるため、結果的にCPU負荷増やクエリ実行時間の増加などの性能低下を引き起こす傾向にあります。 「どの程度断片化が進んでいるか」を示す値として、「断片化率」という値があります。 こちらはsys.dm_db_index_physical_statsというDMVを使って以下のように取得できます。 declare @DB_ID int ,@OBJECT_ID int set @DB_ID = DB_ID( ' DB名 ' ) set @OBJECT_ID = OBJECT_ID( ' テーブル名 ' ) select * from sys.dm_db_index_physical_stats(@DB_ID, @Object_ID, null , null , ' DETAILED ' ) as A join sys.objects as B on A.object_id = B.object_id 例えば、約200万レコードのテーブルに対して上記クエリを実行すると、以下のような結果が得られました。 index_level この例だと0がリーフレベル、1が中間レベル、2がルートです。 avg_fragmentation_in_percent 各レベルにおける断片化率です。高いほど断片化が激しいことを示します。 page_count 各レベルにおけるページ数です。断片化が激しいと、本来必要なページ数よりも多くなります。 avg_page_space_used_in_percent 各レベルにおいて、各ページにどれだけレコードが詰まっているかを示します。100%に近いほど読み取り性能がよくなります。 断片化を解消する手段として、 インデックスの再構成と再構築 という2つの方法があります。 断片化解消のための再構成と再構築 インデックスの再構成と再構築の違いは、以下の表に分かりやすくまとまっています。 出典: インデックス再構築と再構成の違い プロダクション環境で気にすべきポイントとして、「同時実行性」に特に注意が必要です。再構成は、実行中に取得するロックの範囲、ロックをかけている時間ともに限定的であるためオンライン操作とみなすことができます。 一方、再構築でもEnterprise Editionではオンライン操作が可能ですが、それ以外のエディションの場合オフライン操作となってしまいます。 ここで、オフライン操作とは「その操作を実行中に、他プロセスが同一テーブルに読み書きできなくなる」操作のことを言います。したがってオフライン操作のインデックス再構築に5分間かかる場合、同一テーブルへのSELECTが5分間ブロックされ続けることになります。オンライン操作はその逆で、並行して他プロセスが読み書きできます。 先ほどのテーブルをインデックス再構成してみると、以下のような結果になりました。 リーフレベルの断片化率(avg_fragmentation_in_percent)が約70%から約0%へと変化しています。 それに伴ってリーフレベルを構成するページ数(page_count)が40%ほど少なくなっています。 この結果は、そのまま「テーブルスキャン時のリーフレベル読み取りページ数が約40%少なくなる」と言い換えることができ、読み取り性能の向上に貢献しそうです。 ここまで、SQL Serverにおけるインデックスの再構成と再構築について紹介しました。次に、本記事を書くモチベーションとなった背景についてご説明します。 背景 再構成と再構築のどちらを定期的に実施すべきか、という疑問がありますが、Microsoftのドキュメントでは以下のガイドラインが紹介されています。 出典: インデックスの再構成と再構築 つまり、断片化率がある一定の閾値を超えた場合は再構築(REBUILD)、そうでない場合は再構成(REORGANIZE)を推奨する、というものです。 ただし、再構築をオンラインで実行するためにはSQL ServerのエディションがEnterpriseでなければいけません。 そのため、Standard Editionを使っている場合などは積極的に再構築を実施することはできません。 インデックス再構築と再構成の違い という記事の中で、 「実際の運用を考慮した場合、再構築や再構成前の断片化の状態よりも、再構築や再構成の実行中の状況や実行後のインデックスの状態の方が重要ではないでしょうか?」という問いかけがなされています。 さらに踏み込むと、個人的には「再構築や再構成を実施した結果、性能がどの程度向上するかが重要ではないか」と考えます。 弊社では、リードレプリカDBのインデックスメンテナンスとして、毎日インデックスの「再構成」を実施しています。また、セールなどの高トラフィックなイベント前にはインデックスの「再構築」を実施しています。 リードレプリカDBはStandard Editionであるため、インデックスの再構築はオフライン操作でしか実行できません。 そのため、以下の手順でインデックス再構築を実施していました。 WEBサーバーを数グループに分割 1グループをLoadBalancerから外し、アクセスが無い状況にする アクセスが無い状態でインデックスのオフライン再構築 完了後、グループをLoadBalancerに戻す 2-4を各グループで実施 以下のGIFアニメのイメージです。 この作業は数カ月に一度の頻度で発生する運用で頻度は高くありませんが、作業者の負担が大きい作業でした。 それでも、再構成より再構築のほうが、性能面で優れた結果をもたらすのであれば、継続して実施すべき作業です。 ですが、もし再構成と再構築とでクエリ性能およびサーバー負荷に差が無いことを確認できれば、今後はこの運用を無くすことができます。 以上の背景を踏まえて、インデックスの再構成と再構築の性能比較を実施することになりました。 比較方法 SQL Server 2012 Standard Editionが入った、同一スペックのサーバー2台で比較を実施します。尚、ディスクは「SSD」です。 サーバーAでは全テーブルのインデックス「 再構成 」、サーバーBでは全テーブルのインデックス「 再構築 」を行い、クエリ性能およびサーバー負荷の観点で比較を実施しました。 比較用のメトリクスには パフォーマンスカウンタ の値を使用しました。 留意点1 冒頭でお話したように、インデックスの再構成と再構築には様々な違いがあります。 説明した点以外でも、例えばインデックス再構築の方がテーブル容量を圧縮できるといったメリット等も考えられます。 ただし、今回興味があるのは「どれだけクエリ性能に差が生じるか」という点だけであり、その点に絞って優劣をつけることとします。 留意点2 本当は「インデックスメンテ無し」「インデックス再構成」「インデックス再構築」という3パターンの比較を考えていました。 しかし、プロダクション環境において性能劣化の懸念がある「インデックスメンテ無し」のサーバーを用意することが難しいため、再構成と再構築でのみ比較しました。 比較結果:クエリ実行時間 「 SQLServer:Batch Resp Statistics 」というメトリクスを使うことで、「実行に1ms-2msかかったクエリが〇個あった」という風に、実行時間ごとのクエリ実行回数の分布情報が取得できます。 これはサーバーが起動してからの累積値のため、2点間の差分を取得することで、例えば特定の1時間におけるクエリ性能を確認することができます。 表にまとめると、以下の結果となりました。 (サーバーA:インデックス再構成 / サーバーB:インデックス再構築) サーバーAとサーバーBでは、クエリ実行時間の比率の増減は1ポイント未満であり、クエリ実行時間の傾向に大きな変化は発生していませんでした。 そのため、インデックスの再構築によって、今までは10msだったクエリが1msになり大幅に処理時間が変化したというような傾向は発生していないと考えられます。 比較結果:サーバー負荷 インデックスの再構成および再構築によって、テーブルを構成するページ数に差がでてきます。 そのためデータ読み取り時のディスク負荷や、読み取りページ数増加に伴うCPU負荷等に変化がみられる可能性を考慮し、以下のメトリクスを比較しました。 オレンジ色がサーバーA(インデックス再構成)のデータを示し、青色がサーバーB(インデックス再構築)のデータを示します。 LogicalDisk\Avg. Disk Queue Length ディスク負荷が高い場合に高い値を示すメトリクスです。横軸は時間、縦軸はディスクキューの数を示します。サーバーAとBで顕著な変化はみられませんでした。 LogicalDisk\Disk Read Bytes/sec データの読み取り量を示すメトリクスです。横軸は時間、縦軸は秒間のディスク読み取り数(単位:Byte)を示します。 サーバーB(青色/インデックス再構築)の方が各テーブルを構成するページ数は少ないはずですが、それに伴って読み取りデータ量も減少している、というような傾向はみられませんでした。 Process(sqlserver)\% Processor Time CPU負荷を示すメトリクスです。横軸は時間、縦軸はサーバーのCPU使用率(%)を示します。 データの読み取り量が増えるとCPU負荷も増加する傾向にありますが、読み取りデータ量が変わらなかったことから、CPU負荷にも差はみられませんでした。 考察 今回検証した環境については、インデックス再構成と再構築で、性能観点/サーバー負荷の観点での差はほぼ無いという結果になりました。 これは以下の点を踏まえると納得できます。 再構成による「リーフページの断片化のみ解消」でも、大きく断片化が解消できているという点 インデックス階層においてページ数が多いのは圧倒的にリーフページが存在する階層であった点 今回の検証では性能面での差が無いという結論となりましたが、ワークロードの性質や、ディスク性能次第では顕著に差がでる可能性もあります。 例えば、ディスク性能が低いHDDですと、インデックス再構築の方が性能面で優れるという結果になったかもしれません。 関連するメトリクスを採取して比較し、効果を検証することが大事だと思いました。 まとめ 本記事では、インデックスの再構成と再構築の性能差を比較するための評価手法と比較結果をご紹介しました。 比較対象のメトリクスは、インデックスの再構成と再構築の挙動の違いを踏まえて、影響を受けそうなメトリクスを選定しました。 本記事で紹介した比較方法を使って、他の環境でも性能の優劣を比較していただけると思います。 今回の検証結果では、インデックスの再構成と再構築とで性能およびサーバー負荷の差異はみられませんでした。 そのため、今後は該当サーバーにおけるインデックスのメンテナンスは再構成のみとしました。Enterprise Editionの場合は迷わずオンライン操作の再構築でいいかもしれません。 しかし該当サーバーはStandard Editionのため、再構築が不要と判断できたことで運用作業を1つ削減できました。 今回は性能面を重視して確認を行いましたが、冒頭で紹介したインデックスの再構築と再構成の処理の違いの表にも記載があるように、性能面以外の違いを気にする場面もあると思います。 インデックスのメンテナンス方法(再構成/再構築/オンライン再構築)によって、インデックスメンテナンスの処理時間や使用するトランザクションログのサイズが変わってきます。 短時間でインデックスのメンテナンスを完了させるという観点では、メンテナンス方法を使い分ける必要もでてくると思います。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/
アバター
こんにちは。ZOZO Researchの小倉です。2019年7月29日(月)から8月1日(木)にかけてグランキューブ大阪(大阪府立国際会議場)で開催されたMIRU2019に参加しました。今回はその様子をレポートします。 MIRU2019 MIRUとは、今回で第22回目の開催となる画像の認識・理解シンポジウム(Meeting on Image Recognition and Understanding)です。今回は事前登録者数900人強、当日参加者も含めると1,000名を超す方が参加されたそうです。このMIRU2019において、ZOZOテクノロジーズはプラチナスポンサーとして協賛させていただきました。 cvim.ipsj.or.jp 企業展示 企業展示ブースでは、ポスター形式でZOZO Researchにおける取り組みを紹介しました。1枚目ではZOZOグループが提供するサービスとそこから得られる豊富な情報資産について、2枚目では実際の研究事例とその成果について解説しました。大変うれしいことに、多くの方々が興味を持ち話を聞いてくださいました。ポスターまで足を運んでくださった皆さま、ありがとうございました。 展示していたポスターはこちらです。 ランチオンセミナー sites.google.com 3日目には、MIRU2019プラチナスポンサーとして企業企画イベントを開催しました。「ファッションを数値化する」をミッションに掲げるZOZO Researchの取り組みを、産学連携というキーワードから紐解くランチオンセミナーです。講演者として以下の方々をお呼びしました(敬称略)。 桂井 麻里衣(同志社大学 助教) シモセラ エドガー(早稲田大学 専任講師) 古川 徹生(九州工業大学 教授) 山口 光太(株式会社サイバーエージェント) エドガー先生や山口先生はファッションのwebデータを使った先駆的な研究実績があり、現在はそれぞれ大学と企業でご活躍されています。桂井先生や古川先生はZOZOテクノロジーズと共同研究を行なっています。皆さまにはファッション研究の魅力、ZOZOテクノロジーズを始め企業に期待することなど、率直に話していただきました。 会場からは「ファッションやアートに代表される分野を本当に理解するためには、存在するデータを分析するアプローチだけではなく、人の認知や創造性をモデル化する必要があるだろう」といった意見もいただきました。こちらも多くの方々にご参加いただきました。会場まで足を運んでくださった皆さま、ありがとうございました。 インタラクティブセッション 3日目のインタラクティブセッションでは、ZOZO Researchインターンの長瀬が発表しました。 [PS2-21] 部分コーディネート識別不能問題を解決するためのGraph Neural Networksの検討 長瀬 准平(芝浦工大/ZOZO), 斎藤 侑輝, 中村 拓磨(ZOZO) コーディネート生成モデルを評価する際、慣例的にコーディネートの穴埋め問題(Fill-in-the-blank, FITB)の正解率で評価します。本研究では、Graph Convolutional Network(GCN)ベースのFITBを数学的に定式化しました。さらに、GCNにスキップ接続を導入することで、GCNのグラフ情報とコーディネート情報を保つSet Transformerを提案しました。 発表に使用したポスターはこちらです。 気になった研究発表 私が個人的に興味を持った研究について紹介します。 招待講演 [IT2A-2] On the Structural Sensitivity of Deep Convolutional Networks to the Directions of Fourier Basis Functions Yusuke Tsuzuku, Issei Sato(Univ. of Tokyo/RIKEN) 「パンダの画像にあるノイズを加えると、認識精度が高いはずのCNNモデルが99%テナガザルという誤判定を起こしてしまう」というのは以前からよく知られた現象です。このように画像認識モデルの性能を大きく乱すようなパターンをUniversal Adversarial Perturbation(UAP)と呼びます。 この研究では、UAPをフーリエ解析することで、パターン自体を見るだけでは分からない特徴を周波数領域に見出しました。この特徴はデータセットとモデルアーキテクチャの組み合わせごとに固有であり、特定の周波数に対して敏感に反応することがわかりました。その周波数に対応する2次元フーリエ基底関数から生成されるUAPを加えると、実際に画像認識モデルが誤判定を起こしてしまいます。UAPは人間が直接見ても区別しづらいですが、フーリエ変換して周波数領域に移すことでその違いが明瞭になります。例えばガウシアンノイズは周波数領域では一様となってしまうため、特定の周波数にのみ強く反応するようなモデルを乱すことはできません。 この研究がさらに進み、どのようなノイズにも頑強なアーキテクチャが提案されたらとても面白いと思います。 インタラクティブセッション [PS1-16] Data Shakingを用いたBlack Box Networkの解析手法 菅原 俊, 田口 賢佑, 船津 陽平(京セラ) 近年、Google Cloud AutoMLやAmazon Forecastなど、機械学習のフルマネージドサービスが登場しています。このようなサービスは手持ちのデータを使って簡単に機械学習を行いサービスに組み込むことが可能です。しかし、これらは内部でどのような処理が行われているか分からないブラックボックスモデルとなっており、出力として得られた値の信頼性がどの程度なのか分かりません。 この研究では、画像のセマンティックセグメンテーションを行うブラックボックスモデルを対象とし、入力画像に様々な摂動を加えた際の推論結果の分散を観察することで信頼性解析を行いました。期待されるように物体の境界付近における信頼性が低いという結果が得られたほか、摂動のパターンを様々に変えることで信頼性の分布も変化することを報告しています。 この結果だけでも十分に面白いですが、「入力データに摂動を加えることでブラックボックスモデルの振る舞いを解析できる」ということに理論的な裏付けがあるのかという点にも興味があります。 [PS1-54] 分光感度の異なる複数のカメラで撮影された多視点画像からの色と形状の復元 酒井 修二, 高橋 諄丞, 渡邉 隆史(凸版印刷) 撮影画像の色は「物体に固有の分光反射率」に加えて「光源の分光分布」と「カメラの分光感度」に影響されるため、分光反射率を精度よく測定するためには手間やコストがかかります。この研究では、より簡単に、より安価なカメラを用いて分光反射率を求められるような手法を提案しました。色の情報が分光反射率として求まれば、光源を変えた時にその物体の色がどのように変化するかをシミュレーションできます。 この研究はZOZO Researchにおける取り組みのひとつである「衣服のリアルな質感を伝えるためのCG表現」と親和性が高いと感じました。アパレル商品の撮影時の環境光と、私たちが実際にその商品を着用して過ごす時の環境光は異なります。「どの環境光のもとでどのような色に見えるか」をWeb上で確認できれば、「イメージと違った」という理由での返品を減らしお客様により満足していただくことができると考えられます。 感想 私個人としては初めて参加する情報系の学会でした。 画像処理についての知識が浅く勉強不足を痛感しましたが、初日のチュートリアル講演で真っ先にGANや近傍探索アルゴリズムの概観を知ることができ、ありがたかったです。招待講演やオーラルセッションでは国内外の様々な研究成果を聞くことで現在の研究の最先端やトレンドが分かりました。ディープラーニングを解釈しやすくするAttention Mapや、ラベルデータの不完全性に対応するための半教師あり学習といったトピックが多かったように感じました。インタラクティブセッションでは弊社のサービスや研究に活かせるものという観点で見て回り、著者の方とのディスカッションを通じていくつかのヒントを得ることができました。 最後に ZOZO Researchでは次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できる機械学習エンジニアを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com おまけ バンケットにて、弊社の後藤が代表して挨拶しました。 各テーブル盛り上がっている中だったので聞こえづらかったと思いますが、耳を傾けてくださりありがとうございました。 梅田のポケモンセンターにて。
アバター
こんにちは。ZOZOテクノロジーズ開発部SREの田島( @katsuyan121 )です。 弊社では JBoss Data Grid (以下JDG)という分散キャッシュサーバーをマーケティングオートメーションシステムに導入し、キャンペーンのリアルタイム配信を実現しています。 JDGは複数台のサーバーでクラスタを組んでおり、先日そのクラスタがSplitBrainを起こしました。 SplitBrainとは、1つのクラスタが2つ以上のクラスタに分断することをいいます。 例えば、クラスタのノード間で通信が分断された場合などに、ノード同士で通信できなくなりSplitBrainが発生します。 調査の過程でJDGはどのような仕組みで動作しているのか、どのような原因でSplitBrainが発生したのか知見が溜まったのでそれを紹介します。 また、実際にSplitBrainの再現を行い対策を行ったのでそれについても紹介します。 JDGについて インメモリDataGrid インメモリなデータストアである 複数サーバー間でデータを共有し協調動作が可能 分散実行が可能 Server-Client modeとEmbedded mode Server-Client mode Embedded mode JDGとInfinispan JDGクラスタの仕組み ディスカバリープロトコル NATIVE_S3_PING グループメンバーシッププロトコル 障害検知プロトコル FD FD_SOCK VERIFY_SUSPECT SplitBrainの発生 SplitBrain SplitBrain発生までの流れ SplitBrainの再現 FDのタイムアウト時間を短く設定 デバッグモードでアプリケーションを起動 JDGのキャッシュに対して大量のデータをinsert JConsoleを利用してFullGCを発生させる 実際にSplitBrain・FDが発生したか確認 クラスタ1 クラスタ2 AP-2によるFDのログ コーディネータノードによるVERIFY_SUSPECTのログ 対策 本システムの課題 キャッシュとアプリケーションが一緒に載っている JDGの情報があまりない まとめ 参考記事 JDGについて インメモリDataGrid 最初にJDGを分散キャッシュサーバーと紹介しましたが、実際にはその名前の通りDataGridのPlatformになります。また、データはすべてメモリに保持されるためインメモリDataGrid Platformと呼ばれています。インメモリDataGridとは参考記事によると以下のことが実現できるものを指すらしいです。参考記事は本記事の最後に載せています。 インメモリなデータストアである 複数サーバー間でデータを共有し協調動作が可能 分散実行が可能 それぞれの項目に対してJDGではどのようになっているかを紹介します。 インメモリなデータストアである JDGのデータはすべてオンメモリとなっています。 そのため、swap等が発生しない限りは高速にデータへアクセスできます。 複数サーバー間でデータを共有し協調動作が可能 JDGでは複数サーバーでクラスタを組み分散してデータを保持できます。1つのデータを何台のサーバーで保持するかを設定でき、1台のサーバーで障害が発生したとしてもデータが失われない仕組みとなっています。また、クラスタにノードが追加・削除された場合はそのノードが保持していたデータを各サーバーに自動でリバランスする機能を備えています。 分散実行が可能 JDGではクラスタの中でアプリケーションを動かすことができます。JDGのデータを利用するようなアプリケーションの場合、データを持っている該当のノードでJavaの特定の処理を行うことができます。これによりデータのアクセスが高速になり、高速なスループットを実現することが可能となっています。また、クラスタ内でMapReduceを行うことができ集計のような処理も高速に行うことが可能です。 Server-Client modeとEmbedded mode JDGには以下の2つのモードがあります。 Server-Client mode Embedded mode Server-Client mode Server-Client modeはアプリケーションとJDGを別に動作させて、HTTPなど任意のプロトコルを利用してJDGにアクセスします。 Server-Client modeを利用することでアプリケーションとデータを分離して管理できます。 Embedded mode Embedded modeはJavaアプリケーションとJDGを同一のJVMで動かします。これにより上記で示したような任意のJavaアプリケーションを分散して動作させることが可能となります。これによりデータ処理を高速化しスループットの高速化を実現できます。 私達のサービスでは高速なスループットが求められるためEmbedded modeを利用しています。例えば膨大な商品データの中から商品の値段が下がったことをリアルタイムで検知しユーザーにお知らせをするということをしています。 しかし、Embedded modeはアプリケーションとデータが同一のJVM上に乗っているのでアプリケーション等の障害でJVMが停止するとデータも失われてしまいます。 また、JVMの再起動つまりアプリケーションの再起動をするとデータが失われるためデプロイのたびにデータを各サーバーでリバランスして回る必要があります。 JDGとInfinispan JDGはOSSで開発されている Infinispan というアプリケーションのエンタープライズ版となっています。 そのため基本的にはInfinispanのソースコードを読むことでアプリケーションの中身を直接把握することが可能です。 JDGクラスタの仕組み JDGでは複数のサーバーでクラスタを組み分散してデータを保持していると紹介しました。 サーバー同士のクラスタ管理はJGroupsというOSSライブラリを使用しています。 JGroupsではクラスタ管理に必要な仕組みを、それぞれのプロトコルとして分けて管理をします。 プロトコルは以下のようなXMLで設定を記述します。以下は実際に私達のシステムで使われている設定ファイルを少しいじったものです。 <config xmlns = "urn:org:jgroups" xmlns : xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi : schemaLocation = "urn:org:jgroups http://www.jgroups.org/schema/jgroups-4.0.xsd" > <TCP bind_addr = "${jgroups.bind_addr:SITE_LOCAL}" bind_port = "${jgroups.tcp.port:7800}" port_range = "30" recv_buf_size = "10m" send_buf_size = "640k" enable_diagnostics = "false" sock_conn_timeout = "2000" thread_naming_pattern = "pl" bundler_type = "no-bundler" thread_pool . min_threads = "50" thread_pool . max_threads = "10000" thread_pool . keep_alive_time = "60000" /> <org . jgroups . aws . s3 . NATIVE_S3_PING region_name = "ap-northeast-1" bucket_name = "${jgroups.s3.bucket}" /> <FD_SOCK/> <FD timeout = "60000" max_tries = "10" /> <VERIFY_SUSPECT timeout = "3000" /> <pbcast . NAKACK2 use_mcast_xmit = "false" xmit_interval = "1000" xmit_table_num_rows = "100" xmit_table_msgs_per_row = "1000" xmit_table_max_compaction_time = "10000" /> <UNICAST3 xmit_interval = "500" xmit_table_num_rows = "20" xmit_table_msgs_per_row = "1000" xmit_table_max_compaction_time = "10000" /> <pbcast . STABLE stability_delay = "500" desired_avg_gossip = "5000" max_bytes = "1m" /> <pbcast . GMS print_local_addr = "true" join_timeout = "3000" max_join_attempts = "400" /> <MFC max_credits = "2m" min_threshold = "0.50" /> <FRAG3 frag_size = "8000" /> </config> 各プロトコルの役割についての概要はこちらのブログにすごくわかりやすくまとめられているのでご参照ください。 https://kazuhira-r.hatenablog.com/entry/20130209/1360426003 ここでは、特に重要なディスカバリープロトコル、グループメンバーシッププロトコル、障害検知プロトコルについて紹介します。 ディスカバリープロトコル ディスカバリープロトコルは、JGroupsのクラスタへの参加時に他ノードやコーディネータを探すときに使われるプロトコルです。 私達のシステムでは、各ノードにEC2を利用しているため NATIVE_S3_PING というプロトコルを利用しています。 NATIVE_S3_PING NATIVE_S3_PING はS3上のファイルを利用することでディスカバリープロトコルの実現を行います。 S3を使ったディスカバリープロトコルはもともと公式で開発されている S3_PING というプロトコルがあります。しかし、最新のバージョンでは S3_PING が非推奨となっており NATIVE_S3_PING を使うように勧めています。そのような経緯から私達のプロジェクトではサードパーティ製のNATIVE_S3_PINGを利用しています。 https://github.com/belaban/JGroups/blob/master/src/org/jgroups/protocols/S3_PING.java#L30 NATIVE_S3_PINGを利用しクラスタを作成すると、指定したS3バケットに以下のようなファイルが生成されます。 AP-1-24102 4f8adfd5-4c86-55ce-cfe3-4fb11dfa145a 10.160.4.1:7800 T AP-2-31780 8db77c66-79fd-93f5-d72f-b718438a6061 10.160.4.2:7800 F AP-3-42443 532qfd3d-543d-fda3-l3rf-ljl35dfahlh5 10.160.4.3:7800 F AP-4-53254 0401172a-201e-a875-dac3-04b74a9f8812 10.160.4.4:7800 F クラスタはコーディネータノード(マスタノード)1台とそれ以外のノードで構成されます。クラスタにノードが参加・退出するとコーディネータノードが上記ファイルを編集します。 上記ファイルでTがついているものがコーディネータノードです。参加したいノードは上記のファイルを参照しコーディネータノードに対して「参加させてくれ」といったメッセージを送ります。 実際にコーディネータノードがクラスタにノードを追加するのは以下のグループメンバーシッププロトコルで行います。クラスタがない初期状態の場合、最初のノードはS3にファイルがないと判断し自身がコーディネータノードとなりS3にファイルを作成します。 グループメンバーシッププロトコル グループメンバーシッププロトコルは、クラスタのメンバー管理を行うプロトコルです。各ノードのクラスタへの参加や退出を管理します。グループメンバーシッププロトコルには特に種類がなく GMS として設定されるもののみとなっています。 障害検知プロトコル 障害検知プロトコルはその名前の通り、クラスタ内のノードに障害が発生した場合にそれを検知するためのプロトコルです。 私達のシステムではFD_SOCKとFDというプロトコルを組み合わせて利用しています。また、VERIFY_SUSPECTというプロトコルも障害検知に必要なプロトコルとなります。 FD まず、FDではクラスタ内で円形になるように以下のように隣接グラフを作成します。 そして隣のノードで障害が発生していないかどうかを監視します。FDでは定期的に隣のノードに対してハートビートを行います。定めた「リトライ回数xタイムアウト時間」の間、隣のノードから応答がなかった場合はそのノードが死んでいるかもしれないと判断します。リトライ回数とタイムアウト時間はXMLの設定の max_tries と timeout で設定できます。 <FD timeout="60000" max_tries="20"/> 隣のノード死んでいるかもしれないと判断すると、それをマスタノードに通知します。この死んでいるかもしれないと疑うことを「SUSPECT」といいます。 FD_SOCK FD_SOCKでもFDと同じように円形の隣接グラフを作成します。FD_SOCKでは隣のノードの障害検知にはTCPコネクションを利用します。そしてTCPコネクションが切られた場合隣のノードが死んだと判断し「SUSPECT」を行います。 VERIFY_SUSPECT VERIFY_SUSPECTはSUSPECTされた状態のノードが本当に死んでいるのかを確認するプロトコルです。 VERIFY_SUSPECTはコーディネータノードによって行われ、SUSPECTされたノードにpingし生きているかを確認します。生きていた場合はSUSPECTされたノードのSUSPECTフラグを消します。死んでいた場合はクラスタから対象のノードを削除します。 SplitBrainの発生 以上のようにプロトコルを組み合わせることでJDGはクラスタを実現しています。しかし、設定が甘かったりするとSplitBrainを起こすことがあります。 今回実際にSplitBrainが発生し調査対応を行ったので紹介します。 SplitBrain 冒頭でも説明しましたがSplitBrainとは、1つのクラスタが2つ以上のクラスタに分断することをいいます。 冒頭ではクラスタのノード間で通信が分断された場合などに、ノード同士で通信できなくなりSplitBrainが発生すると紹介しました。 しかし今回の場合は通信障害が原因ではありませんでした。以下で今回SplitBrainが発生した原因・その流れを紹介します。 SplitBrain発生までの流れ 今回の根本的な原因はGCに起因するstop the world発生が原因でした。 以下のような4台のサーバーによるクラスタ(実際のサーバー台数とは異なります)について考えます。 障害検知プロトコルでは以下のようにノード同士を監視していると仮定した、SplitBrain発生までの流れです。 CノードでGCによる10分以上のstop the worldが発生 BノードがFDプロトコルでCノードが生きているかを確認する CノードはGCによるstop the worldが発生しているためFDに対するレスポンスができない BノードはCノードが死んでいると判断しマスタノードAに通知 マスタノードAはVERIFY_SUSPECTプロトコルによってCノードが実際に死んでいるのか確認 CノードはGCによるstop the worldが発生しているためVERIFY_SUSPECTに対するレスポンスができない マスタノードAはCノードが死んだと判断しCノードをクラスタから外す CノードのGCが終了 Cノードはクラスタから外されているとは気づかない Cノードは他のノードに通信ができないため死んだと判断し自分がマスタノードになる 以上のような流れでマスタノードが2つ出来上がり、クラスタが2つに分断しました。 SplitBrainの再現 本当にGCに起因するstop the world発生によるFD発生が原因でSplitBrainが発生したかについてはあくまでも予測でした。そこで今回FullGCを発生させ、FDが発生することによってSplitBrainが起こるかを再現しました。 以下が検証の手順です。 FDのタイムアウト時間を短くした設定 デバッグモードでアプリケーションを起動 JDGのキャッシュに対して大量のデータをinsert JConsoleを利用してFullGCを発生させる 実際にSplitBrainが発生したか確認 FDのタイムアウト時間を短く設定 検証で10分以上のFullGCを発生させるのは難しかったので、以下のようにFDのタイムアウトを短くしました。これにより1分くらいのFullGCが発生するとFDのタイムアウトが発生すると考えられます。 <FD timeout="6000" max_tries="10"/> デバッグモードでアプリケーションを起動 FDが発生したログはデバッグモードでしかログが吐き出されません。そのため実際にFDが発生しているのかもつかめていない状態でした。そこで検証ではFDが本当に発生するかどうかを確認するためにデバッグモードでアプリケーションを起動します。 JDGのキャッシュに対して大量のデータをinsert 一定期間のFullGCを走らせるために大量のオブジェクトを生成させる必要がありました。JDGのキャッシュは実際にはJavaのオブジェクトなので、ダミーデータをJDGに大量にinsertしました。 JConsoleを利用してFullGCを発生させる 次にFullGCを発生させます。FullGCが発生するまで待つということをしていると日が暮れてしまいます。そこでJConsoleというツールを利用しFullGCを発生させました。以下のように「GCの実行」というボタンを押すとFullGCが走ります。 実際にSplitBrain・FDが発生したか確認 最後にFDのタイムアウトが実際に発生しSplitBrainが発生したかを確認します。 以下のようにS3に2つのファイルが作られたことからSplitBrainが発生していることが確認できました。各ノード名は「SplitBrain発生までの流れ」で説明したノードに対して、「A=AP-1, B=AP-2, C=AP-3, D=AP-4」のように対応しています。 クラスタ1 AP-1-24102 4f8adfd5-4c86-55ce-cfe3-4fb11dfa145a 10.160.4.1:7800 T AP-2-31780 8db77c66-79fd-93f5-d72f-b718438a6061 10.160.4.2:7800 F AP-4-53254 0401172a-201e-a875-dac3-04b74a9f8812 10.160.4.4:7800 F クラスタ2 AP-3-42443 532qfd3d-543d-fda3-l3rf-ljl35dfahlh5 10.160.4.3:7800 F また、以下のログからFDが発生していることが確認できました。 AP-2によるFDのログ また、以下のログからFDのタイムアウトが発生していることが確認できました。 2019-04-10 16:27:27.625 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=1) 2019-04-10 16:27:33.625 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=2) 2019-04-10 16:27:39.626 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=3) 2019-04-10 16:27:45.628 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=4) 2019-04-10 16:27:51.629 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=5) 2019-04-10 16:27:57.630 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=6) 2019-04-10 16:28:03.632 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=7) 2019-04-10 16:28:09.633 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=8) 2019-04-10 16:28:15.634 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: heartbeat missing from AP-3-42556 (number=9) 2019-04-10 16:28:21.636 DEBUG [org.jgroups.protocols.FD] (Timer runner-1,AP-2-31780) AP-2-31780: received no heartbeat from AP-3-42556 for 10 times (60000 milliseconds), suspecting it 2019-04-10 16:28:21.638 DEBUG [org.jgroups.protocols.FD] (jgroups-50,AP-2-31780) AP-2-31780: suspecting [AP-3-42556] コーディネータノードによるVERIFY_SUSPECTのログ 最後にVERIFY_SUSPECTによりノードCがクラスタから削除対象となっていることを確認しました。 2019-04-10 16:28:24.753 INFO [org.infinispan.remoting.transport.jgroups.JGroupsTransport] (VERIFY_SUSPECT.TimerThread-55,AP-2-31780) ISPN000094: Received new cluster view for channel framework: [AP-2-31780|10] (1) [AP-2-31780] 2019-04-10 16:28:24.753 INFO [org.infinispan.CLUSTER] (VERIFY_SUSPECT.TimerThread-55,AP-2-31780) ISPN100001: Node AP-3-42556 left the cluster 以上のようにFullGCの発生により実際にFDのタイムアウトによるSplitBrainが発生していることが確認できました。これにより今回のSplitBrainの根本原因がGCによるstop the worldであると判断できました。 対策 今回SplitBrainが発生するまで異常に気づかなかった原因の一番の問題は監視にありました。 システムの監視はもともとしっかりと行われていたのですが、私達が使い慣れているようなものではなくメトリクス等が簡単に参照できるようになっていませんでした。 そこで、Datadogを導入しメトリクスを可視化、それを元にした監視設定を行いました。 それ以来なにか異常が発生した場合すぐに対応し、システムを見直すというサイクルが生まれました。 また、JVMのパラメータ見直しやJVMの定期的な再起動を行い長時間のGCが起こりにくくなるように対策を行っています。 こちらについては実際に効果があるか長期的に検証を行っています。 本システムの課題 キャッシュとアプリケーションが一緒に載っていてる JDGの情報があまりない キャッシュとアプリケーションが一緒に載っている JDGの説明の中でも説明しましたが、私達のシステムではEmbedded modeを利用しています。そのためキャッシュとアプリケーションが同じJVM上で動いています。JDGは大量のキャッシュデータをJavaオブジェクトとして保持するため長時間のGCが発生することもあります。今回の10分以上のGCも大量データのGCが要因と考えています。これは、JDGキャッシュだけではなくJDGクラスタ上のアプリケーションにも影響を与えてしまいます。 また、アプリケーションのデプロイ時には毎度1台1台キャッシュをリバランスしながら行っています。その結果デプロイに時間がかかってしまうという問題があります。これは時間だけではなくデータのロストの危険性もあります。そこでこの問題を解決するためにキャッシュの切り離しや別サービスの利用等を検討しています。しかし、スループットの問題がありなかなか難航しています。 JDGの情報があまりない JDGの情報を調べていると情報が少ないか、あっても古い情報が多いとわかりました。今回の調査でも公式ドキュメントを参照する方法以外の情報がほとんどありませんでした。そして公式ドキュメントに書かれていないものについての詳細はソースコードを読む以外方法がありませんでした。これも相まって別サービスの利用を検討しています。 まとめ 今回JDGという分散キャッシュサーバーのSplitBrainの発生から、その調査と再現・対策を行いました。 そして、その過程で得た知見についてご紹介しました。 弊社では未知の領域であっても一歩ずつ調査を行い問題解決ができるエンジニアを募集しております。興味がある方は以下のリンクからぜひご応募ください。 tech.zozo.com また分散システム・リアルタイム分析システムに興味がある、こんな仕組みじゃなくてもっとこうするべきでしょとのお考えがある方はぜひお話しましょう! 参考記事 http://otndnld.oracle.co.jp/document/products/coherence/34/doc_cd/coh.340/B52977-01/definingdatagrid.htm https://www.slideshare.net/AdvancedTechNight/infinispan-14380723 https://enterprisezine.jp/dbonline/detail/5010
アバター
こんにちは。Innovation Initiative Div.の @ka_bi__ です。 普段は 「コーデ相談 by WEAR」 のプロダクトマネージャーを担当しています。 「業務上で発生する面倒なタスク、さっと自動化したい…!でもエンジニアに頼むにも忍びないし、わざわざコーディングするにはハードルが高い…!」 こんな場面は、多々ありませんか? タスク自動化ツール Zapierを使えば、GUIでサクッと解決! 本記事では、SlackをベースにZapierを活用した事例を3つお伝えしたいと思います。 Zapierとは Zapier とは、アメリカのタスク自動化ツールです。 複数のアプリ(Webアプリケーション)を連携させてワークフローを作り、業務を自動化させることができます。Web UI上からアカウント連携・ワークフロー作成ができるため、プログラミングの知識がなくても簡単に使うことができます。類似サービスとして IFTTT がありますが、業務系アプリとの連携を柔軟にカスタマイズできることから、ZOZO Technologiesでは有料のTeam Planを導入しています。 Zapについて Zapierでは複数のアプリを連携させて作ったワークフローの単位を「Zap」と呼びます。Zapには条件を設定する 「TRIGGER」 と、内容を設定する 「ACTION」 があり、それぞれをセットアップすることでワークフローを実行できます。今回ご紹介するZapを作成するには、Zapierのページ上部にある「MAKE A ZAP!」ボタンをクリックして始めましょう。 Zapの作成画面 1.新しくSlackチャンネルが作成されたら、Slackでbotがお知らせ ■やりたいこと 新しくSlackのチャンネルが作成されたら、特定のチャンネルに通知させたい。 手順 1.TRIGGER:Slack 新しいチャンネルが作成されたら、反応するように条件を設定。 2.ACTION:Slack 「誰がそのチャンネルを作成したか」・「作成されたチャンネル名」をポストするように設定すれば完成! 完成イメージ 2.特定のSlackチャンネルに画像がアップロードされたら、Google Driveに自動保存 ■やりたいこと 画像共有チャンネルにポストされた写真を、いつでも参照できるようGoogle Driveを連携・自動保存したい。 手順 1.TRIGGER:Slack 特定のチャンネルに新しいファイルがアップロードされたら、反応するように条件を設定。 2.ACTION:Google Drive 特定のGoogle DriveフォルダにFileをアップロードするように選択すれば完成! 完成イメージ 3.kintoneの通知がきたら、Slackでbotがお知らせ ■やりたいこと kintoneのレスポンス(承認完了・コメントがついた等)を特定のチャンネルに通知したい。 手順 ※色んな方法がありますが、今回は新着メールのタイトルを通知させています。 この方法ならkintoneの通知以外にも応用が効くので、是非活用してみてください。 0.Gmailでラベルを設定 Gmailにて件名に[kintone]と記載されているものにラベルをつける。 1.TRIGGER:Gmail 新しいメールを受信・[kintone]ラベルがついたもののみ反応するように条件を設定。 2.ACTION:Slack 特定のチャンネルに投稿 Slackのポスト内容は、メンションやURLなども追加・組み合わせも可能です。 任意のテキストをSlackにポストするように、設定すれば完成! 完成イメージ +α Slackに特定のURLがポストされたら、それを元にURLを生成・Slackでbotが投稿 ※(5分ではできませんでしたが)本気を出せばGUIだけでこんなこともできる一例をご紹介させてください。 ■やりたいこと 特定のURL(CMSツールのURL)がポストされたら、 URLを自動生成(確認環境のURL)・Slackにポストすることで確認コストを減らす。 ※URLはダミーです 手順 1.TRIGGER:Slack 特定のチャンネルに新しいメッセージがポストされたら反応するように設定。 2.FILTER 記事以外の内容にも反応すると困るので、FILTERをかけます。 たいてい「記事が上がりました!」とポストするので、特定の文字で指定。 3.Formatter text Transform機能を使って、URL内の文字列を admin から www へ、リプレイス。 ※末尾のURLは後で調整します 4.Formatter text Extract URL機能を使って、URLのみを抜き出し。 5.Slack Slackにポストする際、URLの末尾 ?test_article をくっつけることで確認環境URLを自動生成! 完成イメージ 最後に 業務効率化って「なんだか面倒くさいな…」「やろうと思っていたけど時間が経っていた」そんな悪循環に陥りがちですよね。Zapierはノンプログラミングにも関わらず自由度がとても高いツールです。自由度が高すぎて何をしようかたまに迷ってしまうレベルですが、効率化したい業務・仕様を明確にしておけばものの数分で効率化できます。 今回Zapierを導入してから「どうすれば、より業務を効率化できるだろう?」と高い解像度で考える習慣が身につきました。本質的なことを考える時間を増やすためにも改めて業務を見直し、業務効率化を図る時間を思い切ってとってみるのがいいかもしれません。 ZOZOテクノロジーズのInnovation Initiative Div.では「第2のZOZOTOWNになる収益の柱をつくる」ため、デザインとエンジニアリング・ビジネス開発が完結する組織構成で様々な技術を用いたサービスの仮説検証を行っています。よりよい世の中、顧客体験を提供するために、最高のプロダクトを一緒に作りませんか? tech.zozo.com
アバター
こんにちは。音声UIの開発を担当している武田です。Alexaのスキル「 コーデ相談 by WEAR 」はスキルの応答から分析まで、ほぼ全てAmazon Web Services(以下、AWS)のみを使って構成されています。今回は処理の種類ごとにAWSの構成の内容を、 CloudFormation のコードとともに紹介していきます。 ユーザーに応答を返す AWS Lambda Amazon DynamoDB Amazon CloudWatch Amazon S3 CloudFormation 問題があった時にアラートを飛ばす Amazon CloudWatch Amazon SNS AWS Lambda CloudFormation 分析する Amazon Kinesis Data Firehose AWS Lambda Amazon S3 Amazon Athena Amazon QuickSight CloudFormation まとめ さいごに ユーザーに応答を返す Alexaでカスタムスキルを作成する際に、応答の内容を決める処理は任意のサーバーで行うことができます。コーデ相談は AWS Lambda を採用しており、以下のようなサービスがさらに連なっています。 Alexaからのリクエストは基本的に全てLambdaで受け付け、必要な情報や処理に応じて DynamoDB やWEARのAPIを叩く形です。最初に「ほぼ全てAWSのサービス」と表現した理由は、WEARのAPIの部分だけAWS以外のリソースを頼っているためです。サービスごとに役割と採用した理由を説明していきます。 AWS Lambda LambdaはユーザーがAlexaに話しかけたりした際に、Alexaから送られてくるリクエストの窓口です。このリクエストにはユーザーによる入力の内容やデバイスの情報が特定のフォーマットで含まれています。それらの情報をもとに返事の内容を決定し、Alexaが理解できる形でレスポンスを返すことがLambdaの役割となります。 リクエストを受け付けるエンドポイントは自由に指定できるので、EC2のインスタンスを立てたり、すでに稼働している自社のサーバーに向けても問題はありません。コーデ相談でLambdaを採用している理由はAlexaとの相性が良いためです。もう少し詳細に説明します。 本来LambdaはHTTPS等のエンドポイントを提供する場合、 API Gateway や Application Load Balancer (以下、ALB)を挟む必要があります。しかしAlexaのリクエストを受け付ける場合は、Lambdaの ARN(Amazonリソースネーム) をAlexaのコンソールで指定するだけで接続が完了します。つまり、インターネットにエンドポイントを公開する必要がないため、セキュリティに関して考えることが減ります。リクエストを許可するスキルをIDで指定することもできるので、関係ないスキルのリクエストを受け付けてしまうことも防げます。 Alexaのカスタムスキルの開発を助けてくれる Alexa Skills Kit SDK (以下、ASK SDK)というものがあります。このSDKではDynamoDBを用いてユーザーの情報を保存する仕組みも提供されており、Lambdaでの ロール の管理とも相性が良いです。 Lambdaには Amazon Virtual Private Cloud 内のリソースにアクセス可能なVPCモードが存在しますが、今回はこのモードにしていません。ALBを用いている場合など関連しているサービス次第ではLambdaをVPCに置く必要があります。しかし、コーデ相談はこれらのサービスを使っていませんし、VPCモードではコールドスタンバイが発生してしまうためVPCに置いていません。 Amazon DynamoDB Lambdaからユーザー情報の読み書きを行うために用います。コーデ相談では性別やチュートリアルのフラグなど、ユーザーIDからkey-valueの形で取得できる情報のみ扱うのでRDBは使用していません。DynamoDBをASK SDKから使うことで、ユーザーのIDなどを意識せずにユーザーの情報の書き込みと読み込みが可能となります。開発のハードルが下がりますし、データ操作の記述を行う必要も無くなるため、スキル内でユーザーの情報を保持しておくレベルであればおすすめです。 Amazon CloudWatch CloudWatch はスキルに関わるログを保存するために用います。 Lambdaからはログの書き込みのみを行い、コーデ相談では以下のログを保存しています。 ・スキルの起動や終了 ・デバッグ用のログ ・ユーザーの行動のログ Lambda自体が自動で書き込む受動的なログも、デバッグログといった能動的に取ろうとしているログもCloudWatchにまとめて保存しています。こうすることでログがごちゃ混ぜになりますが、データの流れがシンプルになりCloudWatchを見に行けば全てのログが保存されている安心感があります。CloudWatch自体の検索機能や、後述するAthenaを用いることで状況に応じて関係するログのみを抜き出すことも可能なので、現時点までで困ったことはありません。 Amazon S3 S3 には画面付きデバイスで用いるアイコンやチュートリアル動画などのアセットを置きます。AlexaのデバイスからAWSサービスへアクセスするときには基本的に全てLambdaを経由しますが、S3に置いたアセットへのアクセスのみLambdaを経由せず直接参照します。 この部分はHTTPSでアクセスできる形であれば何でも大丈夫です。動画などはそれなりの容量になりますし、しっかりとした配信を意識するならCDNやDNSの設計が必要になると思います。しかしAlexaスキルからの参照に限定した場合、配信するデータが少量の静的ファイルのみということもあり、CloudFrontは置かずS3を直接参照する形にしています。 CloudFormation ユーザーがスキルを使う際に必要なAWSのリソースは以下のテンプレートで作成できます。 AWSTemplateFormatVersion : 2010-09-09 Parameters : GlobalEnvironment : Type : 'String' AllowedValues : - 'production' - 'development' SkillId : Type : 'String' S3BucketNameLambdaSource : Type : 'String' S3KeyLambdaSource : Type : 'String' Resources : S3BucketSkillAssets : Type : 'AWS::S3::Bucket' DeletionPolicy : 'Retain' Properties : AccessControl : 'PublicRead' CorsConfiguration : CorsRules : - AllowedHeaders : - '*' AllowedMethods : - GET AllowedOrigins : - http://ask-ifr-download.s3.amazonaws.com - https://ask-ifr-download.s3.amazonaws.com MaxAge : 3000 BucketName : !Sub ${GlobalEnvironment}-skill-assets DynamoDBTableUsers : Type : 'AWS::DynamoDB::Table' Properties : BillingMode : 'PAY_PER_REQUEST' AttributeDefinitions : - AttributeName : 'id' AttributeType : 'S' KeySchema : - AttributeName : 'id' KeyType : 'HASH' TableName : !Sub ${GlobalEnvironment}-users IAMRoleLambdaAlexaSkillEndpoint : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'lambda.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess' Policies : - PolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Action : - 'dynamodb:*' Resource : - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDBTableUsers} - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDBTableUsers}/* PolicyName : !Sub ${GlobalEnvironment}-lambda-alexa-skill-endpoint LambdaFunctionAlexaSkillEndpoint : Type : 'AWS::Lambda::Function' Properties : Code : S3Bucket : !Ref S3BucketNameLambdaSource S3Key : !Ref S3KeyLambdaSource Runtime : 'nodejs8.10' Timeout : 120 Environment : Variables : ASSETS_BUCKET_NAME : !Ref S3BucketSkillAssets TABLE_NAME : !Ref DynamoDBTableUsers FunctionName : !Sub ${GlobalEnvironment}-skill-endpoint Handler : 'index.handler' Role : !GetAtt IAMRoleLambdaAlexaSkillEndpoint.Arn LambdaPermissionAlexaSkillEndpoint : Type : 'AWS::Lambda::Permission' Properties : FunctionName : !Ref LambdaFunctionAlexaSkillEndpoint Action : 'lambda:InvokeFunction' Principal : 'alexa-appkit.amazon.com' EventSourceToken : !Ref SkillId LogsLogGroupLambdaFunctionAlexaSkillEndpoint : Type : 'AWS::Logs::LogGroup' Properties : LogGroupName : !Sub /aws/lambda/${GlobalEnvironment}-skill-endpoint 全体的にですが、 GlobalEnvironment というパラメータを使ってリソース名をまとめて変えられるようにしています。リリース後は本番環境と開発環境を区別したくなるはずなのでつけておきましょう。 S3BucketSkillAssets はAlexaから直接参照される部分なので、 AccessControl は PublicRead にしてCORSの設定をしておきます。この辺りは こちらの記事 に詳しく書かれています。 DynamoDBの BillingMode を PAY_PER_REQUEST にしているのは、Alexaのスキルの起動回数が読めないためです。ある程度アクセスが落ち着いてきたら PROVISIONED に変更します。テーブルのスキーマはASK SDKで保存されるフォーマットに合わせています。 DynamoDBのテーブルとS3のバケットはそれぞれ、Lambdaの関数内で名前が必要になるので、環境ごとの切り替えのしやすさを考慮して環境変数で渡すようにしています。 CloudWatchの部分はLambdaが1回でも実行されれば自動で作られますが、あとで他のリソースから参照が発生するので明示的に作っておきます。 問題があった時にアラートを飛ばす スキル内でエラーを検知した場合に、Slackにメッセージを流せるようにします。処理の流れはこのようになります。 流れ自体はかなりオーソドックスで、CloudWatchのログを元にエラー数を調べ、閾値を超えた時にLambda関数を実行するという流れです。Lambda関数内でメッセージを成形してSlackのIncoming Webhooksを叩きます。使っているサービスを見ていきましょう。 Amazon CloudWatch スキルのログを流すために使っているCloudWatchですが、ログを元にエラー数を調べることも可能です。具体的には以下のことをやっています。 ・特定の文字列を含むログが一定の期間で何度発生しているかを計算 ・計算結果が指定した閾値を超えているかチェック ・閾値を超えた場合にトピックを発行 トピックは他のサービスで処理を始めるためのトリガーとなるもので、この後に出てくるSNSでどのサービスを動かすか指定していきます。メールを送るだけならトピックを発行せずにCloudWatchだけで完結しますが、状況によってメンションをつけたり、異なるメッセージを流すためにSNSを経由するようにしました。 Amazon SNS SNS はSimple Notification Serviceの略で、トピックに応じてLambda関数を実行する以外にも、プッシュ通知やメールを送信できます。今回、SNSはトピックとLambdaを繋ぐためだけに用いているため、Slackにメッセージを流す具体的な処理は全てLambdaで記述します。 AWS Lambda Slackにメッセージを飛ばします。エラーの種類に応じてメッセージや色を変更しています。詳しい内容は、後述するテンプレートに直接載せてありますのでそちらを見てください。 CloudFormation ログを元にエラーなどのアラートを飛ばすリソースは以下のテンプレートで作成できます。 AWSTemplateFormatVersion : 2010-09-09 Parameters : GlobalEnvironment : Type : 'String' AllowedValues : - 'production' - 'development' SlackChannel : Type : 'String' SlackWebhookUrl : Type : 'String' Resources : LogsMetricFilterSkillError : Type : 'AWS::Logs::MetricFilter' Properties : FilterPattern : 'SKILL_ERROR' # アプリケーション側で指定しているログのプレフィックス LogGroupName : !Ref LogsLogGroupLambdaFunctionAlexaSkillEndpoint MetricTransformations : - MetricName : 'skill-error' MetricNamespace : !Sub ${GlobalEnvironment}-metric MetricValue : '1' CloudWatchAlarmSkillError : Type : 'AWS::CloudWatch::Alarm' Properties : AlarmActions : - !Ref SNSTopicSkillError AlarmName : !Sub ${GlobalEnvironment}-skill-error-alarm ComparisonOperator : 'GreaterThanThreshold' EvaluationPeriods : 1 # 最初は少しでも発生したら飛ばすようにする MetricName : 'skill-error' Namespace : !Sub ${GlobalEnvironment}-metric Period : 60 Statistic : 'Sum' Threshold : 0 SNSTopicSkillError : Type : 'AWS::SNS::Topic' Properties : Subscription : - Endpoint : !GetAtt LambdaFunctionSlackNotification.Arn Protocol : 'lambda' TopicName : !Sub ${GlobalEnvironment}-skill-error-topic IAMRoleLambdaAlexaSlackNotification : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'lambda.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess' LambdaFunctionSlackNotification : Type : 'AWS::Lambda::Function' Properties : Code : ZipFile : | import json import logging import os from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError SLACK_CHANNEL = os.environ['SLACK_CHANNEL'] SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL'] logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context) : logger.info("Event : " + str(event)) message = json.loads(event[" Records"][ 0 ]["Sns"]["Message"]) logger.info("Message : " + str(message)) alarm_name = message['AlarmName'] reason = message['NewStateReason'] text = "想定していないアラームが発生しました: %s" % (alarm_name) if '-error-alarm' in alarm_name: text = '<!channel> コーデ相談スキルでエラーが発生しています。ログを確認してください。' slack_message = { 'channel' : SLACK_CHANNEL, 'text' : text, 'color' : '#FF0000' if ('-error-alarm' in alarm_name) else '#FFFF00' , 'fields' : [ { 'title' : alarm_name, 'value' : reason } , ] } req = Request(SLACK_WEBHOOK_URL, json.dumps(slack_message).encode('utf-8')) try : response = urlopen(req) response.read() logger.info("Message posted to %s", slack_message['channel']) except HTTPError as e : logger.error("Request failed : %d %s", e.code, e.reason) except URLError as e : logger.error("Server connection failed : %s", e.reason) Environment : Variables : SLACK_CHANNEL : !Ref SlackChannel SLACK_WEBHOOK_URL : !Ref SlackWebhookUrl FunctionName : !Sub ${GlobalEnvironment}-slack-notification Handler : 'index.lambda_handler' Role : !GetAtt IAMRoleLambdaAlexaSlackNotification.Arn Runtime : 'python3.6' Timeout : 10 LambdaPermissionSlackNotificationSkillError : Type : 'AWS::Lambda::Permission' Properties : FunctionName : !Ref LambdaFunctionSlackNotification Action : 'lambda:InvokeFunction' Principal : 'sns.amazonaws.com' SourceArn : !Ref SNSTopicSkillError AWS::Logs::MetricFilter の部分でどのようなログを数えるかを指定しています。上記の指定ですと SKILL_ERROR という文字列を含むログを数えてくれますので、同じフォーマットでエラーのログを吐き出せるようにしておけば検知するようになります。今回はエラーだけですが、WARNや警告用の設定も用意しておけば異なるメッセージも簡単に流せます。 アラートを出すときの閾値はかなり下げてあります。Alexaのスキルは基本的にシンプルですし、大量にエラーが出るようなバグを仕込むことは少ないだろうということで、最初はこれくらいで良いと思います。 スキルの処理のように、複雑な関数になる場合はLambda用のコードを別のファイルで管理すると思います。ただ、今回のようにSlackのAPIを叩く程度であれば、テンプレートの中にLambdaのコードを直接記述した方が管理や更新が楽です。 分析する 分析と言っても色々あると思いますが、今回はSQLで集計し、その結果をBIツールで可視化できるようにします。例によって分析に関わる処理の流れがこちらです。 AWSにはAthenaというサービスで集計などを行うことができます。Athenaでクエリを叩くためにはある程度データを整形する必要があるため、Kinesis Data Firehoseで定期的にログを読み込み、S3に書き出します。サービスごとに具体的に説明していきましょう。 Amazon Kinesis Data Firehose Kinesis Data Firehose はログなどのストリーミングデータを、別のサービスに配信できるサービスです。コーデ相談の場合ですと、CloudWatchのログを読み込み、整形した結果をS3に保存しています。 ログが追加されるタイミングでS3にファイルを追加する方法もあります。しかし、その方法ではログの量が増えるにつれて処理自体の回数が増えてしまうため、指定した間隔でまとめて処理を実行できるKinesis Data Firehoseを採用しました。 AWS Lambda ログの読み込みや、データの書き出しはKinesis Data Firehoseがやってくれますので、このLambdaではデータの整形のみ行います。CloudWatchのログをS3に置くだけであれば整形は必要ありませんが、Athenaでクエリを叩くために以下の形にします。 ・1つのレコードを1行にまとめる ・ログの中身をJSONのkey-valueの形にする AthenaはJSON形式以外にもCSVやParquetも指定できます。今回はカラム名をKey名でマッピングでき、整形が楽そうと言うことでJSONにしました。 Amazon S3 Athenaから叩くデータの置き場として用います。BigQueryの場合はBigQueryにデータを読み込ませることになりますが、Athenaの場合S3など他のサービスに置いてあるデータをそのまま参照できます。 Amazon Athena Athena はクエリを書いて集計するために用います。AthenaはSQLのエンジンがPrestoになっており、JSONを扱える関数やWindow関数なども用意されているので、一通りの集計は可能です。 設定としてテーブルを作成(Create table)する必要があります。テーブルを作成とは言っていますが、この作業ではどこにあるデータをどのようなカラムとして扱うかを定義する作業になります。Athenaは現時点でCloudFormationに対応していないので、GUIかCLIで設定していきましょう。 具体的な設定方法は記事がいくつか見つかりますので、割愛します。 Amazon QuickSight QuickSight はデータを可視化するダッシュボードが作成できるBIツールです。可視化するデータはAthenaによる集計結果を用いることができますので、リアルタイムに溜まっていくデータを表示することが可能です。 QuickSightには SPICE という機能が備わっています。この機能を用いると集計対象のデータをインポートしてインメモリで集計するようになるので、表示が高速となります。インポートした データの更新 のタイミングは自由に指定できるので、表示するたびにS3などへのアクセスが発生することも防げます。 Athenaと同様にCloudFormationには対応していないため、初期設定などはGUIで行うようにしましょう。 CloudFormation 分析のために必要なデータをS3に溜め込む処理は、以下のテンプレートで作成できます。 AWSTemplateFormatVersion : 2010-09-09 Parameters : GlobalEnvironment : Type : 'String' AllowedValues : - 'production' - 'development' Resources : S3BucketLogs : Type : 'AWS::S3::Bucket' DeletionPolicy : 'Retain' Properties : AccessControl : 'LogDeliveryWrite' BucketName : !Sub ${GlobalEnvironment}-logs LogsSubscriptionFilterAccessLog : Type : 'AWS::Logs::SubscriptionFilter' Properties : DestinationArn : !GetAtt 'KinesisFirehoseDeliveryStreamAccessLog.Arn' FilterPattern : 'ACCESS_LOG' # アプリケーション側で指定しているログのプレフィックス LogGroupName : !Ref LogsLogGroupLambdaFunctionAlexaSkillEndpoint RoleArn : !GetAtt 'IAMRoleLogsSubscriptionAccessLog.Arn' IAMRoleLogsSubscriptionAccessLog : Type : AWS::IAM::Role Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : !Sub 'logs.${AWS::Region}.amazonaws.com' Action : 'sts:AssumeRole' Policies : - PolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Action : - 'firehose:PutRecord' - 'firehose:PutRecords' Resource : - !GetAtt 'KinesisFirehoseDeliveryStreamAccessLog.Arn' PolicyName : !Sub ${GlobalEnvironment}-policy-log-subscription-access-log KinesisFirehoseDeliveryStreamAccessLog : Type : 'AWS::KinesisFirehose::DeliveryStream' Properties : ExtendedS3DestinationConfiguration : BucketARN : !GetAtt S3BucketLogs.Arn BufferingHints : IntervalInSeconds : 60 SizeInMBs : 50 CompressionFormat : 'GZIP' Prefix : 'skillAccessLog/' ProcessingConfiguration : Enabled : true Processors : - Parameters : - ParameterName : 'LambdaArn' ParameterValue : !GetAtt 'LambdaFunctionKinesisFirehoseTranslate.Arn' - ParameterName : 'BufferSizeInMBs' ParameterValue : 3 - ParameterName : 'BufferIntervalInSeconds' ParameterValue : 60 Type : 'Lambda' RoleARN : !GetAtt 'IAMRoleKinesisFirehoseAccessLog.Arn' IAMRoleKinesisFirehoseAccessLog : Type : AWS::IAM::Role Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : Allow Principal : Service : firehose.amazonaws.com Action : sts:AssumeRole Policies : - PolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Action : - 's3:AbortMultipartUpload' - 's3:GetBucketLocation' - 's3:GetObject' - 's3:ListBucket' - 's3:ListBucketMultipartUploads' - 's3:PutObject' Resource : - !Sub 'arn:aws:s3:::${S3BucketLogs}' - !Sub 'arn:aws:s3:::${S3BucketLogs}/*' - Effect : 'Allow' Action : - 'lambda:InvokeFunction' - 'lambda:GetFunctionConfiguration' Resource : - !GetAtt 'LambdaFunctionKinesisFirehoseTranslate.Arn' PolicyName : !Sub ${GlobalEnvironment}-policy-kinesis-firehose-access-log LambdaFunctionKinesisFirehoseTranslate : Type : 'AWS::Lambda::Function' Properties : Code : ZipFile : | import base64 import gzip import StringIO import json def lambda_handler(event, context) : output = [] for record in event['records'] : payload = base64.b64decode(record['data']) fileobj = StringIO.StringIO(payload) logs = None with gzip.GzipFile(fileobj=fileobj, mode="r") as f : logs = f.read() fileobj.close() # Athena用にデータを整形 log_dict = json.loads(logs) logGroup = log_dict.get("logGroup") log_list = [] for log_event in log_dict.get("logEvents") : log_values = log_event["message"].split('\t') if len(log_values) >= 4: log_event["request_type"] = log_values[ 3 ] if len(log_values) >= 5: log_event["user_id"] = log_values[ 4 ] if len(log_values) >= 6: log_event["supported_interfaces"] = log_values[ 5 ] log_list.append(json.dumps(log_event)) # new line per record log_txt = log_list[ 0 ] + " \n " if len(log_list) >= 2: log_txt = " \n " .join(log_list) + " \n " output_record = { 'recordId' : record [ 'recordId' ] , 'result' : 'Ok' , 'data' : base64.b64encode(log_txt) } output.append(output_record) print('Successfully processed {} records.'.format(len(event['records']))) return { 'records' : output } FunctionName : !Sub ${GlobalEnvironment}-kinesis-firehose-translate Handler : 'index.lambda_handler' Role : !GetAtt IAMRoleLambdaKinesisFirehoseTranslate.Arn Runtime : 'python2.7' Timeout : 120 IAMRoleLambdaKinesisFirehoseTranslate : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'lambda.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess' コーデ相談ではユーザーの行動ベースで分析がしたかったので、専用のログを ACCESS_LOG から始まるようにして流し、データとして蓄積するようにしました。 ログの加工の流れの注意点としては、それぞれの出力の形式です。Kinesis Data Firehoseに対してCloudWatchからログを送る場合、gzipで圧縮された状態になっています。また、Athenaで集計できるデータとして扱う場合拡張子が *.gz になっている必要があるなど注意が必要です( Athenaでサポートする圧縮形式 )。この辺りは AWS::KinesisFirehose::DeliveryStream リソースやLambda関数のコードで吸収しています。 まとめ バケットやパラメータなどの名前は変えてありますが、上記のテンプレートからスタックを作ることで、実際に稼働しているコーデ相談と同じ構成を作ることが可能です。Alexaのスキルで多機能なことは少ないですし、アクセス数も膨大に増えるとは考えづらいので今回紹介した構成でほとんどの場合は事足りると思います。 Alexaだけで見るとコンソールから設定するだけで十分かもしれませんが、CloudFormationはコードベースでのレビューができたり、同じ構成を再現できるメリットがあります。特にアラートや分析の部分はAlexa特有の話ではありません。一般的なサービスでも同じ構成が使えますので、次の開発でも活用し改善していく予定です。 さいごに AWSを意識せずにスキルを提供できる Alexa-hostedスキル なども発表され、Alexaの開発ハードルはどんどん下がっています。しかしサービスとして提供しグロースさせようとしたり、環境が複数になると管理するリソースは増えてきますので、 今回紹介したテンプレートが役に立てば幸いです。 ZOZOテクノロジーズのInnvation Initiative部では今後普及するであろう技術を先行研究し、様々な技術を用いたサービスを開発しています。よりよいユーザー体験を提供するために、技術を駆使して最高のプロダクトを作りませんか? www.wantedly.com
アバター
Gopher's design for Ryuta Tezuka( @Tzone99 ) こんにちは、ZOZOテクノロジーズ開発部の池田( @ikeponsu )です。 本記事では、 Go言語における画像処理の可能性を、ベンチマークを通して探ってみたいと思います。 はじめに 業務内でGo言語での画像処理を行う機会があり、Goの標準パッケージやGoCVについて調べていました。 ただ、画像処理に関する記述はまだまだ少なく、実装している人自体も少ないのかなという印象でした。 今回行った「Go言語での画像処理の速度はどの程度か」のベンチマークが、これからGo言語で画像処理の実装を行おうとしている方の参考になればと思います。 ベンチマークの内容 比較対象 C++のOpenCV内のバイリニア補間 GoCV内のバイリニア補間 Go言語とimageパッケージを使って実装したバイリニア補間 処理内容 画像入出力 バイリニア補間で画像サイズを1/2に縮小 処理枚数 以下サイトで入手した人物画像10000枚。 Labeled Faces in the Wild: http://vis-www.cs.umass.edu/lfw/ マシンスペック 検証の内容 C++のOpenCV内のバイリニア補間 使用したライブラリ opencv : https://github.com/opencv/opencv/releases ソースコード #include <opencv2/opencv.hpp> #include <sys/types.h> #include <dirent.h> #include <string> #include <iomanip> #include <sstream> #include <sys/stat.h> #include <sys/types.h> // 読み込む画像枚数を多めに定義 #define MAX_IMAGESIZE 10000 int main( int argc, char *argv[]) { std::cout<< "start" <<std::endl; cv::Mat search_img[MAX_IMAGESIZE]; time_t t = time( nullptr ); // 形式を変換する const tm* lt = localtime(&t); // ディレクトリ名を作成 std::stringstream s; s<< "20" ; s<<lt->tm_year- 100 ; s<<lt->tm_mon+ 1 ; s<<lt->tm_mday; s<< "-" ; s<<lt->tm_hour; s<<lt->tm_min; s<<lt->tm_sec; std::string outputPath = s.str(); outputPath = "../result/" + outputPath; mkdir(outputPath.c_str(), 0 755 ); cv::Size size = cv::Size{ 0 , 0 }; cv::Mat output; std::chrono::system_clock::time_point start; std::chrono::system_clock::time_point end; // 計測開始時間 start = std::chrono::system_clock::now(); for ( int i = 0 ; i < MAX_IMAGESIZE; i++ ){ // 画像のディレクトリ、ファイル名、拡張子を指定 search_img[i] = cv::imread( "../sample/" + std::to_string(i) + ".jpg" , 1 ); // 全ての画像を(連番で)読み込み終えるとループを抜ける if (!search_img[i].data) break ; cv::resize(search_img[i], output, size, 0.5 , 0.5 , cv::INTER_LINEAR); cv::imwrite(outputPath + "/" + std::to_string(i) + ".jpg" , output); } // 計測終了時間 end = std::chrono::system_clock::now(); // 処理に要した時間をミリ秒に変換 double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count(); std::cout<<elapsed / 1000 << "s" <<std::endl; return 0 ; } 検証結果 1回目計測:11.383s 2回目計測:11.303s 3回目計測:11.541s GoCV内のバイリニア補間 使用したライブラリ gocv : https://github.com/hybridgroup/gocv ソースコード package main import ( "fmt" "gocv.io/x/gocv" "image" "image/jpeg" "os" "path/filepath" "strconv" "time" ) // main処理 func main() { count := 0 datapath := filepath.Join( "入力先のパス" , "*.jpg" ) file, _ := filepath.Glob(datapath) var d float64 = 1 / 2 size := image.Point{ 0 , 0 } timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "出力先のパス" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() for _, item := range file{ exc(item, d, size, timeString, count) count++ } end := time.Now() fmt.Println( "A.Total time: " , end.Sub(start)) } func exc(item string , d float64 , size image.Point, timeString string , count int ) { img := gocv.IMRead(item, gocv.IMReadColor) rowSize := int ( float64 (img.Rows()) * d) colSize := int ( float64 (img.Cols()) * d) outputImg := gocv.NewMatWithSize(rowSize, colSize, gocv.IMReadGrayScale) gocv.Resize(img, &outputImg, size, 0.5 , 0.5 , gocv.InterpolationLinear) image, _ := outputImg.ToImage() path := filepath.Join( "出力先のパス" , timeString, strconv.Itoa(count) + ".jpg" ) qt := jpeg.Options{ Quality: 60 , } file, _ := os.Create(path) jpeg.Encode(file, image, &qt) } 検証結果 1回目計測:14.980s 2回目計測:14.841s 3回目計測:14.823s Go言語とimageパッケージを使って実装したバイリニア補間 使用したライブラリ image : https://golang.org/pkg/image/ ソースコード 画像入力 func Input (filePath string ) image.Image { file, err := os.Open(filePath) if err != nil { log.Fatal(err) } pngImage, _, err := image.Decode(file) return pngImage } バイリニア補間 func Bilinear(inputImage image.Image, f float64) image.Image { // 重み値を定義 var x float64 var y float64 // リサイズ後 size := inputImage.Bounds() size.Max.X = int(float64(inputImage.Bounds().Max.X) * f) size.Max.Y = int(float64(inputImage.Bounds().Max.Y) * f) // 逆数 reciprocalScalingRows := 1 / f reciprocalScalingCols := 1 / f // アウトプット画像を定義 outputImage := image.NewRGBA(size) var outputColor color.RGBA64 // 画像の左上から順に画素を読み込む for imgRows := 0; imgRows < size.Max.Y; imgRows++ { for imgCols := 0; imgCols < size.Max.X; imgCols++ { // 双一次補完式 // 元画像の座標定義 // 元画像の縦の座標 inputRows := int(float64(imgRows) * reciprocalScalingRows) // 元画像の横の座標 inputCols := int(float64(imgCols) * reciprocalScalingCols) // 補完式で使う元画像のpixel // point(0, 0) src00 := inputImage.At(inputCols, inputRows) // point(0, 1) src01 := inputImage.At(inputCols + 1, inputRows) // point(1, 0) src10 := inputImage.At(inputCols, inputRows + 1) // point(1, 1) src11 := inputImage.At(inputCols + 1, inputRows + 1) // 重み値を算出 x = float64(imgCols) * reciprocalScalingCols y = float64(imgRows) * reciprocalScalingRows // 小数点以下を抽出 x = x - float64(int(x)) y = y - float64(int(y)) r00, g00, b00, a00 := src00.RGBA() r01, g01, b01, _ := src01.RGBA() r10, g10, b10, _ := src10.RGBA() r11, g11, b11, _ := src11.RGBA() // 拡大後の画素を算出 outputColor.R = uint16((1 - x) * (1 - y) * float64(r00)) outputColor.G = uint16((1 - x) * (1 - y) * float64(g00)) outputColor.B = uint16((1 - x) * (1 - y) * float64(b00)) outputColor.R += uint16(x * (1 - y) * float64(r01)) outputColor.G += uint16(x * (1 - y) * float64(g01)) outputColor.B += uint16(x * (1 - y) * float64(b01)) outputColor.R += uint16((1 - x) * y * float64(r10)) outputColor.G += uint16((1 - x) * y * float64(g10)) outputColor.B += uint16((1 - x) * y * float64(b10)) outputColor.R += uint16(x * y * float64(r11)) outputColor.G += uint16(x * y * float64(g11)) outputColor.B += uint16(x * y * float64(b11)) outputColor.A = uint16(a00) outputImage.Set(imgCols, imgRows, outputColor) } } return outputImage } main package main import ( "fmt" "image/jpeg" "img-test/ioimg" "img-test/procimg" "os" "path/filepath" "strconv" "sync" "time" ) func main() { count := 0 datapath := filepath.Join( "入力先のパス" , "*.jpg" ) file, _ := filepath.Glob(datapath) timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "data" , "result" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() for _, item := range file{ exc(item, timeString, count) count++ } end := time.Now() fmt.Println( "B.Total time: " , end.Sub(start)) } func exc(item string , timeString string , count int ) { img := ioimg.Input(item) rimg := procimg.Bilinear(img, 0.5 ) path := filepath.Join( "出力先のパス" , timeString, strconv.Itoa(count) + ".jpg" ) qt := jpeg.Options{ Quality: 60 , } file, _ := os.Create(path) jpeg.Encode(file, rimg, &qt) } 検証結果 1回目計測:35.220s 2回目計測:35.162s 3回目計測:35.238s goroutineで実装した場合 先程比較したGo言語のソースコードの処理をgoroutineで書いた場合、実装前に比べどの様な差があるか気になったので、追加で検証してみました。 GoCV内のバイリニア補間 ソースコード main package main import ( "fmt" "gocv.io/x/gocv" "image" "image/jpeg" "os" "path/filepath" "strconv" "sync" "time" ) func main() { count := 0 datapath := "Data/sample/*.jpg" file, _ := filepath.Glob(datapath) var d float64 = 1 / 2 size := image.Point{ 0 , 0 } timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "Data" , "result" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() var wg sync.WaitGroup for _, item := range file{ wg.Add( 1 ) go func (item string , d float64 , size image.Point, timeString string , count int ) { defer wg.Done() exc(item, d, size, timeString, count) }(item, d, size, timeString, count) count++ } wg.Wait() end := time.Now() fmt.Println( "A.Total time: " , end.Sub(start)) } 検証結果 1回目計測:3.253s 2回目計測:3.299s 3回目計測:2.975s Go言語とimageパッケージを使って実装したバイリニア補間 ソースコード main package main import ( "fmt" "image/jpeg" "img-test/ioimg" "img-test/procimg" "os" "path/filepath" "strconv" "sync" "time" ) func main() { count := 0 datapath := "Data/sample/*.jpg" file, _ := filepath.Glob(datapath) timeLayout := time.Now() timeString := timeLayout.Format( "20060102150405" ) d_path := filepath.Join( "data" , "result" , timeString) if err := os.Mkdir(d_path, 0777 ); err != nil { fmt.Println(err) } start := time.Now() var wg sync.WaitGroup for _, item := range file{ wg.Add( 1 ) go func (item string , timeString string , count int ) { defer wg.Done() exc(item, timeString, count) }(item, timeString, count) count++ } wg.Wait() end := time.Now() fmt.Println( "B.Total time: " , end.Sub(start)) } 検証結果 1回目計測:8.989s 2回目計測:9.118s 3回目計測:9.192s まとめ 今回、比較対象とした3つのソースコードでの処理速度差ですが、多少の処理内容の差や自作コードのチューニング不足によるところもあると思います。 ただ、ベンチマークを行うことで新たな発見もありましたので、あくまで参考の一部としてご確認いただければと思います。 処理 1回目(s) 2回目(s) 3回目(s) C++のOpenCV内のバイリニア補間 11.383 11.303 11.541 GoCV内のバイリニア補間 14.980 14.841 14.823 Go言語とimageパッケージを使って実装したバイリニア補間 35.220 35.162 35.238 【goroutine】GoCV内のバイリニア補間 3.253 3.299 2.975 【goroutine】Go言語とimageパッケージを使って実装したバイリニア補間 8.989 9.118 9.192 「Go言語とimageパッケージを使って実装したバイリニア補間」のソースコードは以下にアップしていますので、良ければご覧ください。 github.com また、Imageパッケージを使って実装した画像処理のその他のアルゴリズムも、これからアップしていく予定です。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは、開発部の塩崎です。 最近はCloudFormation・Embulk・Digdagを使った仕事をすることが多く、一番使う言語がYAMLになりました。 今年福岡で開催されたRubyKaigi 2019ではZOZOテクノロジーズはRubyスポンサーとして協賛させていただきました。 カンファレンス中のスポンサーブースの出し物として、DroidKaigi 2019と同様にファッションチェックアプリの展示を行いました。 DroidKaigiの展示と全く同じでは芸がないと考え、今回のRubyKaigiのためにRuby on Lambdaでランキング機能を作成しました。 本記事では、そのランキング機能の説明をしたいと思います。 ファッションチェックアプリのランキング機能とは まず、ファッションチェックアプリの説明をします。 このアプリはDroidKaigi 2019のために作成されたデモアプリです。 スマートフォンのカメラで全身画像を撮影し、機械学習を用いてファッションの採点を行います。 機械学習モデルの作成に当たっては、当社の運営するファッションコーディネートアプリWEARのデータを利用しました。 techblog.zozo.com 今回作成したのは、このファッションチェックアプリのランキングサイトです。 ブース来訪者にはファッションチェックアプリで採点された結果をTwitterに投稿してもらいます。 ランキングサイトは、その投稿結果をTwitterから自動的に集計し、ランキング形式で表示します。 開発の経緯 RubyKaigiにスポンサーとして参加するにあたって何かしらの企画をやることを考えていました。 昨年はRubyに関するアンケートを行いましたが、今年も同じものをやってもつまらないので、何か他の企画をやろうということになりました。 また、各社ともスポンサーブースに対する力の入れ方が年々上がっております。 魅力的な展示を行うために、今まで以上にこだわる必要がありました。 DroidKaigiでデモアプリを作成したら来場者のウケが良かったとの広報からの助言があり、RubyKaigiでもそれを真似してデモアプリを作ることにしました。 RubyKaigi用に作成するデモアプリですので以下の2点を要件にしました。 Rubyを使っている(MUST) 最近にリリースされた「何か」を活用している(SHOULD) というわけで、昨年のre:InventでRubyサポートが発表されたばかりのAWS Lambdaを使い、ファッションチェックアプリのランキングサイトを作ることにしました。 技術構成 全体のアーキテクチャを上図に示します。 また、ソースコードはこちらのGitHubリポジトリで公開しております。 https://github.com/st-tech/fashion_check_ranking このアプリケーションは大きく2つのパートに分かれています。 1つめはクローラーで、Twitterから採点結果に関するツイートを取得し、それをDynamoDBに保存します。 2つめはランキングサイトで、DynamoDBに保存された情報をランキング形式で表示するWebサーバーです。 以下では、これら2つの詳細を紹介します。 クローラー クローラーはまず、Twitter APIを使ってRubyKaigiに関するハッシュタグである#rubykaigiと#zozotechの付いたツイートをすべて取得します。 そして、その中から正規表現を使って採点結果に関するツイートを抜き出します。 最後に、該当するツイートからユーザー名・アイコンURL・採点結果の点数を取得し、DynamoDBに保存をします。 この機能はAWS Lambdaの上で動作していて、定期的にLambda関数を実行するためにALBのヘルスチェック機能を利用しています。 ※ なぜCloudWatch EventsではなくALBを使っているのかは後ほど説明します。 参考: Twitter API クローラー部分のソースコード ランキングサイト ランキングサイトはDynamoDBに書かれたランキング情報を表示するためのWebサーバーです。 画像・JavaScript・フォントなどの静的ファイルはS3に配置し、動的コンテンツはRuby on Lambdaで生成しています。 小ネタですが、ALBのターゲットグループにLambdaを登録する機能も昨年のre:Inventで発表されたばかりなので試してみました。 Lambdaから呼ばれるハンドラーとRackとの間を繋ぐためのコードをAWSが公開していたので、それを使いました。 理論的にはRackサーバー上で動作するどのフレームワークも動作するはずですが、今回は軽量なフレームワークであるSinatraを利用しました。 参考: serverless-sinatra-sample ランキングサイト部分のソースコード 実装に当たって苦労したところ ここからは実装する時に、苦労したところ・工夫したところを紹介します。 クローラーを10秒ごとに起動したい問題 今回のアプリケーションの要件として、ブースの来訪者が別のブースへ行く前にランキングの結果を反映したいというものがありました。 そのため、10秒ごとくらいにクローラーを起動し、投稿内容を取得する必要がありました。 一方、CloudWatch Eventsでは1分ごとにLambda関数を呼び出すのが限界で、10秒ごとという要件を実現することはできません。 そこで、ALBのヘルスチェック機能に目をつけました。 定期実行したいLambda関数をALBのターゲットグループにいれ、ヘルスチェック間隔を10秒に設定しました。 結果的にこの方法はうまくいき、約10秒間隔でLambda関数を実行できました。 しかし、厳密に10秒間隔になることはなく、5〜20秒間隔くらい(実測値)になるときもありました。 どのような条件で本来設定した間隔からずれるのかの原因究明は結局できませんでした。 今回のアプリケーション的に致命的にはならないため、深追いはしませんでした。 このような事情があるため、このALBをcron代わりに使用する作戦を本番環境で行うことはオススメしません。 そもそもの話ですが、ALBのヘルスチェック機能はcronのために作られてはいません。 ALBを外部公開してしまったら、想定外のアクセスがあった ALBをうっかり外部公開するというミスを犯してしまったために、意図せずにクローラーが起動するという現象が発生しました。 クローラーを起動するためのALBを作るときに、外部公開用WebサーバーのALB設定をコピーしてしまったために発生しました。 何者かがALBに対してアクセスをしてきて、そのアクセス回数分だけクローラーが起動してしまいました。 そのために、Twitter APIのアクセス頻度制限に引っかかるということが発生しました。 ALBのDNS名は十分に長く、ランダムなものになっていましたが、IPアドレスを直に指定してのアクセスがありました。 ログからアクセスのあったURLを調査したところ、WordPressの脆弱性を突くようなものでした。 IPアドレスに対してしらみつぶしにアクセスをしながら脆弱性のあるサーバーを見つけるBotが居ると思います。 この問題に対してはセキュリティグループを適切に設定することで外部アクセスを禁止する対応を行いました。 このことからもALBをcron代わりに使用することはオススメできません。 そもそもの話ですが、ALBのヘルスチェ(略) マルチバイト文字の配信問題 ここから先はランキングサイトで発生した問題です。 CSSファイルを配信するために、config.ruに以下のように書き込んで、静的配信用のディレクトリを設定しました。 set :public_folder , Proc .new { File .join(root, ' public ' ) } この状況でCSSファイルの中に日本語(マルチバイト文字)が入っているときに、ファイルの配信に失敗してしまいました。 CloudWatch Logsでログを確認したところ、以下のエラーが出力されていることが確認できました。 Error raised from handler_method { "errorMessage": "\"\\xE3\" from ASCII-8BIT to UTF-8", "errorType": "Function<Encoding::UndefinedConversionError>", "stackTrace": [ ] } このエラーはエンコーディングがASCII-8BITである文字列をUTF-8に変換しようとする時のエラーです。 xxdで確認したところ、この\xE3はCSSファイルにUTF-8で書かれている ヒラギノ角ゴ という文字列の先頭1バイトのバイトに一致していることが分かりました。 \xE3というバイト表現はASCIIコード外なため、正常に文字コードの変換を行うことができないようです。 ASCII-8BITが何であるのかは以下の記事が詳しいので、このあたりに興味ある人は読んでみるといいかもしれません。 Ruby: US-ASCII-8BITというエンコードを理解する(翻訳) 今回のケースでは静的ファイルのエンコーディングはUTF-8しかないことが分かっていました。 そのため、 String#force_encoding で強制的にエンコーディングをUTF-8にする対応を取りました。 Lambda関数の返り値であるresponseハッシュを生成している部分を以下のように書き換えました。 def handler ( event :, context :) # 略 response = { ' statusCode ' => status, ' headers ' => headers, ' body ' => body_content.force_encoding( ' UTF-8 ' ) # force_encoding追加 } # 略 response end バイナリー配信問題 次はバイナリーファイルを配信しようとしたときに発生した問題です。 なお、このアプリケーションで配信していたバイナリーファイルは、フォントとファビコン画像です。 バイナリーファイルもCSSファイルと同様に以下のように配信していました。 set :public_folder , Proc .new { File .join(root, ' public ' ) } このときもまた配信エラーが発生してしまいました。 CloudWatch Logsには以下のエラーが吐き出されていました。 Error raised from handler_method { "errorMessage": "source sequence is illegal/malformed utf-8", "errorType": "Function<JSON::GeneratorError>", "stackTrace: [ ] } Lambdaが内部的にJSONへシリアライズをしているようで、そのときに問題が発生しているようです。 配信しようとしていたバイナリーがUTF-8的に不正なバイト列だったことに起因するものです。 実際に以下のコードで確認したところ、同様のエラーが発生しました。 bin = File .read( ' 問題のファイル ' ) JSON .generate({ ' bin ' => bin}) # エラー発生 この問題に対しては、静的ファイルはS3で配信する対応を取りました。 そしてその前段にCloudFrontを配置し、静的コンテンツと動的コンテンツの振り分けはCloudFrontに担当させました。 後日談になりますが、ALBのドキュメントをみたら以下のように書かれていました。 isBase64Encodedプロパティをtrueにすれば、Lambdaからバイナリーファイルの配信をすることもできそうです。 機会があれば試してみたいと思います。 To include a binary content in the body of the response, you must Base64 encode the content and set isBase64Encoded to true. The load balancer decodes the content to retrieve the binary content and sends it to the client in the body of the HTTP response. ALBのドキュメント 関数インスタンス再利用問題 最後の問題はLambdaによって、関数インスタンスが再利用された問題です。 クローラーによってDynamoDBのデータが更新されても、ランキングページの表示が変わらないという現象が発生しました。 最初はCloudFrontが動的コンテンツまでキャッシュをしてしまっているのかと思いましたが、MaxTTLは0に設定しているため、キャッシュはしないはずです。 また、CloudWatch Logsで確認したところ、Lambda関数はブラウザからのアクセス毎に起動しています。 この原因はLambdaが関数インスタンスを再利用しているためでした。 LambdaのFAQには以下のように書かれており、場合によっては関数インスタンスが再利用されるようです。 Q: Will AWS Lambda reuse function instances? To improve performance, AWS Lambda may choose to retain an instance of your function and reuse it to serve a subsequent request, rather than creating a new copy. To learn more about how Lambda reuses function instances, visit our documentation. Your code should not assume that this will always happen. AWS Lambda FAQs DynamoDBからの読み出し結果をクラスインスタンス変数にキャッシュして使いまわしているコードがあったので、この部分が原因でした。 そのため、クラスインスタンス変数を使用しないように修正するという対策を行いました。 グローバル変数やクラス変数も同様の問題の発生する可能性があるので、これらの変数を使う時には要注意です。 来場者の反響 スポンサーブースに訪れた多くの方にこの企画を楽しんでいただけました。 3日間で210ツイートもしていただいたことに感謝しています。 ブース来訪者の方とRubyに関する話をする時の取っ掛かりにもなったので、そういう意味でもこの企画をやってよかったと思いました。 1日目はランキング表示件数を上位15人にしていましたが、ランキング圏外だからツイートしないという方が何人かいました。 そこで、2日目以降はランキング表示数を倍の上位30位にしたところ、さらに多くの方にツイートしてもらうことができました。 (1日目: 62ツイート 2日目: 82ツイート) 初日よりも2日目のほうがブースの来場者が少なかったことを考えると、この修正は大成功でした。 一部修正が間に合わず、2日目の午前中は502 Bad Gatewayになってしまったこともありました。 RubyKaigiが開催される都市は美味しいものが多く、本会議後のイベントも多いため、当日に修正する時間があまりありません。 このことは来年以降の教訓にしていきたいです。 まとめ RubyKaigi 2019用のデモアプリをRuby on Lambdaで作り、スポンサーブースの企画としました。 以前のRubyKaigiでMatzさんが「Rubyをキメると気持ちイイ」という名言を残しましたが、やはりRubyは書いていて楽しい言語であることを実感できました。 特に最近はYAML職人になっていたので、久しぶりにキメるRubyの気持ちよさは最高でした。 来年もスポンサーをするかもしれないので、そのときは来年のZOZOテクノロジーズブースにも是非おこしください!! ZOZOテクノロジーズでは一緒にサービスを作っていく仲間を募集しています。 Rubyをキメながら開発ができるサービスもありますので、ご興味のある方は以下のリンクから是非ご応募ください。 tech.zozo.com
アバター
こんにちは!ZOZOテクノロジーズでバックエンド開発をしている @acjustplay です。 実は!この前!世界で一番大きいScalaカンファレンス Scala Days に行ってきました! 今年のScala Daysはスイスのローザンヌで、6/11〜13の3日間の開催でした。今まではアメリカとヨーロッパのどちらかでの開催が多かったですが、ちょうど今年がScala誕生15周年とScala Days開催10周年が重なり、特別な意味を持つ場所ーScalaとScala Daysの起点と言えるEPFL(スイス連邦工科大学ローザンヌ校)での開催になりました。 ちなみに今年はEPFLの50周年記念と私の初海外カンファレンス参加記念でもあり、もはや奇跡の宝箱としか言えないですね! あの有名な階段はEPFLの中にあります! 今回のScala Daysで私が一番注目していたのは、やはりScala 3の情報だったので、それに関連するセッションの内容を紹介していきたいと思います! Day 1: A Tour of Scala 3 まずはDay 1のオープニングキーノート、EDMとともに登場されたScalaの作者Martin Odersky教授からの「A Tour of Scala 3」。 セッションのスライドはこちらへ 会場おしゃれすぎます キーノートの内容は スライド にわかりやすく記載されているので、詳細はスライドをご覧ください。ここでは、ざっくりとまとめておきます。 Scala 3のロードマップ おそらく、みなさんが一番気になるのは「Scala 3いつ出るの?」という点でしょう。少なくとも私はめっちゃ気になっていました! キーノートによるとScala 3は 2020秋 にリリースされる予定です(Day 2のセッションで、これとはちょっと異なるロードマップも提示されていましたが…)。そしてScala 3へより移行しやすくするために、並行してScala 2.14の開発も進めているようです。 Odersky教授のScala 3おすすめ機能ランキング キーノートで一番比重が大きかったのがこの部分です。Odersky教授が、各レベル向けのScala 3のおすすめ機能ベスト3をピックアップしてくれました。 For beginners(初心者向け) Enums Toplevel definition Drop new For everyday coding(業務で使っている人向け) Delegates Extension Method Union Type For experts(上級者向け) Functions Everywhere Typeclass Derivation Match Types 各機能の細かい説明は スライド 、または dottyのドキュメント に記載されているので割愛します。 この中で、個人的にはDelegatesの導入に一番興味を持っています。Delegatesは今までの implicit の代わりとして紹介されました。モチベーションとしては、 implicit の中でも非常に強力な一部の機能は、簡単に記述できるため乱用されがちでした。その乱用を防ぐためにより安全かつ意図が伝わりやすいDelegatesが用意されたそうです。 dottyのドキュメント でも implicit を delegate で書き換えられる説明が載っているので、3に移行する予定があれば事前に慣れておいたほうがよさそうですね。 ちなみにOdersky教授は事前にTwitterでみんなのおすすめ機能のアンケートを実施して、以下のような結果になったそうです! For beginners(初心者向け): Enums For everyday coding(業務で使っている人向け): Union Type For experts(上級者向け):Typeclass Derivation 私もほぼ同じ考えだったので、日頃からアプリケーションコードを書いている人にとっては、記述量が減らせてシンプルに使える機能の方が人気なのかもしれないですね。 Day 2: Future-proofing Scala: the TASTY intermediate representation EPFLのLAMPチームのGuillaume Martresさんから、Scala 3の独自の変換用フォーマットTASTyの紹介です。 TASTyはdottyと一緒にScala 3から導入されます。例えばFoo.scalaをdottyでコンパイルしたら、Foo.tastyとFoo.class 2つのファイルができます。 tastyファイルはプログラム( .scala )の情報を保持して、実行可能なファイル( .class )に変換できる形式になりますので、イメージとしてはFoo.scala -> Foo.tasty -> Foo.classという感じです。 tastyファイル生成は型チェックの後に行われ、まずプログラムの情報を抽象構文木(Abstract Syntax Tree, AST)にパースして、タイパーで型情報を付与します。その型付けをした抽象構文木(Typed Abstart Syntax Tree)を保存しているのがTASTyです! そしてプログラムなのでコメントなどドキュメント情報も保存できます。フルのフォーマット情報は GitHub で確認できます。 TASTyを導入する一つの理由としては、今までScalaでバージョン間の互換性を担保しづらい課題を解決したいという点があるそうです。 根本的な機能改善や変更を加えたい時に、バイナリー互換性を破壊しないことはほぼ不可能です。例えば最近出た2.13でも、コレクション構造の見直しによって2.12とは非互換になっています。 TASTyを導入することで、対応するScalaバージョンのライブラリjarがまだ存在していなくても、classファイルを消してtastyファイルから再生成すればエラー発生させずに、自分のプロジェクトコードを実行できるとのことです。 ちょっと休憩でコーヒーの写真入ります。Scala Daysでは、常にカフェイン+各種飲み物+スナックが提供されています。 Day 2: How are we going to migrate to Scala 3.0, aka Dotty? LightbendのScalaチームのLukas Rytzさんから、Scala 3の変更点、移行プランの詳細紹介です。 もともとは、"How do we prepare ..." という表現のタイトルでしたが、Scala 3のロードマップが明確になったので、より肯定的な表現にされたらしいです。 セッションのスライドはこちらへ セッションの主な内容は以下の3点です。 Scala 3の変更点 Scala 2、3チーム開発の進め方 マイグレーションのタイムライン ここで要注意なのが、スライドにあるタイムライン(p.27)とキーノートのタイムラインが違う(!!?)ことです。セッションのスライドによるとScala 3のリリースが2021年に予定されていますが、果たしてどちらを信じるべきでしょうか!? そして、変更点として一番に挙げられたのがやはり implicit が変わることでした。10年前だと一つの機能でいろんなことができるのが良いとされていましたが、今では利用者の用途ごと最適な機能を提供するようにシフトしてきました。その考え方の変化が今回の変更につながったとのことです。 セッションの最後に、Scala 3はScala 2+1だ(ScalaのStandard Libraryは2系のものをそのまま使うので)とRytzさんが強調されていました。 Scala 3の変更が大きすぎて、もはや別の言語ではないかという議論がよくされます。このセッションを踏まえて、マイグレーションをスムーズに行えるように開発チームがすでに色々考慮、用意してくれていることがわかったので安心できますね! Day 3: Metaprogramming in Dotty EPFLのLAMPチームのNicolas Stuckiさんから、dottyでのMetaprogrammingのセッションです! ScalaのMetaprogrammingといえばマクロですが、Scala 2のmacrosのデザインはコンパイラの内部実装に依存しているので、そのままdottyとScala 3で使えません。そのためdottyではmacrosがリデザインされました。 その影響でdottyを使いたい場合は既存のマクロを書き直さないといけないです。ただし、マクロのプロジェクトをそのままdottyのプロジェクトから使う分であれば大丈夫だそうです。 セッションのスライドはこちらへ dottyからは新しいmacrosが導入され、さらにmacrosを使わなくてもMetaprogrammingができる機能が追加されます。セッションでは一番コアになる部分を紹介してくれました。 Inline Metaprogrammingのための新機能。このmodifierをつけるとコンパイラがいい感じにコードを展開してくれます。 Match Type 型パラメータをパターンマッチして型を返せる機能です。Metaprogrammingのための機能ではないですが組み合わせて使うと便利です。 Macros: Quotes & Splices ' と $ でマクロを作成できます。 ' はコードをデータに変換し、 $ はその逆の動きをします。お馴染みのstring interpolationに似ていますね! Macros: TASTy Reflect Quotes & Splicesだけではできない、型付け構文木(TASTy)を分析、構築できるAPI。 スライド では各機能の使い方をサンプルコード付きで丁寧に説明されていますので、一通り目を通してみてください! おまけ1:他の面白かったセッション Scala 3以外にも面白かったセッションがいっぱいあります。印象的だったセッションは以下の2つです! Day 2: Testing in the postapocalyptic future Daniel Westheideさんから、Scalaであまり聞かれないMutation Testingをどう実現するかの話です。 スライドはこちら Day 3: Pure Functional Database Programming‚ without JDBC doobie の作者Rob Norrisさんから、jdbcを使わず、かつ関数型プログラミングでPostgreSQLのデータベースを操作する話です。 実装したサンプルは Skunk というライブラリです(また実験中なので使わないでくださいとNorrisさんが言っていました)。 おまけ2:関連イベント 今年は10周年記念でもあり、みんなを盛り上げるために運営チームはかなり 関連イベント に力を入れていました。例えばローザンヌの市内観光ツアー(!)や、ラヴォー地区のブドウ畑観光ツアー(!!)はカンファレンスパスを持てば無料(!!!)で参加できます。私はスケジュールが合わずツアーに参加できなかったですが、それ以外にも素晴らしいイベントがたくさんありましたので少し紹介します! 自撮りブース そうです。自撮りです。あのScalaロゴのモチーフにもなったEPFL校内の階段と自撮りできる機会がカンファレンス期間中用意されていました。 階段と自撮りできます。チャレンジしてみましたが完全に失敗しました。 Scala Spree Scala SpreeはDay 1の朝一からオープニングキーノートが始まるまで、EPFLのキャンパスで行われます。 同じ会場で初心者向けのScala Bridgeも開催されています。 イベントでランチ券が配布されて、EPFLの学食を体験できます。 「開発者にもっとオープンソース文化に慣れてもらう」のがScala Spreeの趣旨です。なので参加者はOSSのプロジェクトにプルリクエストを出してマージしてもらうことが目標です。 プロジェクトの担当者は参加者に手伝ってもらいたいIssueのリストを用意して、参加者はその中からピックアップしてプルリクエストを出せるまで開発する感じです。もちろんリスト以外の自分が気になる課題があればそれを行っても構わないですし、プルリクエストを出さずに各プロジェクトをセットアップして試したりするのもOKです。結構ゆるい感じです。 プロジェクトはみんなさんお馴染みのscalac、Coursier、dotty、Scala.jsなどがあります、プロジェクトのMaintainersもその場でサポートしてくれますので、気軽に質問することができます。   ContributorのTシャツが欲しくてドキドキしながらScalaのリポジトリにプルリクエストを送ってみましたが、最後に、参加者全員にTシャツをプレゼントとして渡されました(笑) メインライブラリーへコミットするのは、ずっと難しそうとか、敷居が高そう、というイメージを持っていましたが、難易度が低くて簡単な修正だけど誰かに直してもらいたいというIssueもたくさんあります。 初めての人でもコミットしてもらいやすくするために、プロジェクト側も色々と工夫をしています。例えばScalaのリポジトリでは good first issue というラベルを、初めてプルリクエストを出す人向けのIssueにつけられています。私もここからIssueを探してプルリクエストを出しました! Diversity & Inclusion Lunch Day 2は、Scalaコミュニティの中で少数派になる人たち(underrepresented groups)のためのランチが用意されました。アイスブレイクとしてピープルビンゴゲームから始まり、そのあとみんなでランチを食べながら交流していきました。初めての海外カンファレンスで知っている人がいなかった状況の中、参加者と交流するきっかけを作ってもらえ、本当に助かりました。 ランチで sphere.it の運営者と話す機会があり、そのあとチケットをいただきました! ありがとうございます! ランチ以外にも、Diversity & Inclusionの パネルディスカッション があり、ダイバシティの重要性を発信する企画に感動しました。 Community Dinner Day 2の夜にカンファレンス参加者が全員参加できるCommunity Dinnerがローザンヌのオリンピック・ミュージアムにて開催されました。 場所をオリンピック・ミュージアムにする理由はこの映える眺めではなく、オリンピック精神である"friendship, solidarity and fair play"(友情、連帯感、フェアプレー)はScalaコミュニティが大事にしているバリューにもあてはまるからです。細かいところまでコミュニティの価値観が体現されています。 そしてまさかのジャズ演奏があります! リズムにのってみんな踊りだす! Community Dinnerで他の参加者とも交流ができて、他の会社/国のテック事情も聞けて大変良かったです。最後にみんなからの熱い要望に応じて、バンドのアンコール演奏を持ってCommunity Dinnerが円満閉幕しました。 おまけ3:ローザンヌ 最後のおまけでは、ローザンヌの風景をお送りします。 ローザンヌ大聖堂 Esplanade de Montbenon ローザンヌの街並み レマン湖の白鳥達 ローザンヌは非常に激しい坂が多い町です。毎日山登り気分でした(日本に帰ってきたら少し筋肉痛になりました)。 坂でもお構いなく駐車しています おわりに 行く前は、正直「どうせそのあと動画上がってくるし行かなくても…」と思いながら参加するかどうか少し迷っていました。しかし、実際に現地でセッションを聞いたり、参加者と交流したり、そして一番重要なのはみんながコミュニティをより良くさせよう、Scalaをもっといろんな人に知ってもらおう、と努力している姿を直接体験することができて、本当に良かったです。 ここで「Scalaのカンファレンス行ってみたいなぁー」と思ったあなた! 日本最大級のScalaカンファレンス、 Scala Matsuri が6/27〜29にお台場の東京国際交流館で開催されます! 面白いセッションはもちろん、パプリック向けの無料ワークショップ(!!!!)もありますので、気軽に参加してみてはいかがでしょうか? ZOZOテクノロジーズも将軍スポンサーとして協賛します! あとブースも出しますので、是非遊びにきてくださいー! 最後の最後に 今回のScala Daysの参加にあたって全ての費用を負担してくれた会社に感謝しかないです。さすが物価世界一高いスイスです。震えます。 ZOZOテクノロジーズは、最新技術を学びたければ会社が惜しげなく後押ししてくれます。エンジニアまだまだ募集していますので一緒に働いてみませんか? www.wantedly.com
アバター
こんにちは。音声UIの開発を担当している武田です。 前回の記事 ではAlexa Presentation Language(以下、APL)でどんなことができるかを説明しました。今回はどうやってコードの治安を保ちつつ、APLを書いていくかというお話をします。 APLをJavaScriptのオブジェクトで管理する ディレクティブを関数で生成する パーツを切り分ける 最後に APLをJavaScriptのオブジェクトで管理する *.json として管理するのをやめましょうという話です。APLの 概要 でJSONと言っていたり、 オーサリングツール で扱えるのはJSONファイルですが、JSONはやめてJavaScriptのオブジェクトにしましょう。 公式の中の人もそうしています。   Using AWS Lambda to Bring the Space Explorer Sample Skill to Life JavaScriptにすることで以下のようなメリットがあります。 レイアウト を別モジュールや変数に分けることで、異なる ディレクティブ でも共通化しやすくなる 関数を噛ませることで、 Vueのスロット のようにコンポーネントの中身を入れ替えられる JavaScriptのループ処理を使えば、 コマンド を機械的に生成できる コーデ相談スキル でもAPLは全てJavaScript(正確にはTypeScript)として扱っています。APLはコンポーネントごとに役割がはっきり決まっている性質上、どうしても階層が深くなりがちです。デザインを再現したりマルチデバイスに対応するためにも、切り分けやすい環境にしておくのは重要です。 ディレクティブを関数で生成する APLを使うためには描画される部分だけではなく、ディレクティブとしての フォーマット に従う必要があります。このフォーマットは各画面で共通の部分が多かったり、守るべき階層構造が複雑だったりします。そこでディレクティブを吐き出すための関数を用意してあげましょう。 例がこちらです。 export interface Template { /* ... */ } export interface Layout { /* ... */ } export interface Style { /* ... */ } export function buildRenderDocumentDirective( template: Template, layouts: Layout, styles: Style, options: { datasourceData?: any; } ): Directive { // 渡されたテンプレートと共通で使いたいテンプレートを組み合わせる const mainTemplate = { item: { type: 'Frame' , item: { type: 'Container' , item: template, height: '100vh' , width: '100vw' , } , style: 'mainFrame' , // commonStyles で指定された共通で使いたいスタイル } , } const directive: Directive = { document : { import : [ { name: 'alexa-layouts' , version: '1.0.0' , } , { name: 'alexa-styles' , version: '1.0.0' , } , { name: 'alexa-viewport-profiles' , version: '1.0.0' , } , ] , layouts: { ...commonLayouts, ...layouts } , // スプレッド構文は後勝ちなので 渡された `layouts` が優先される mainTemplate, resources, styles: { ...commonStyles, ...styles, } , // スプレッド構文は後勝ちなので 渡された `styles` が優先される type: 'APL' , version: '1.0' , } , token: 'anydocument' , type: 'Alexa.Presentation.APL.RenderDocument' , } // datasourceData は必要ない場合もあるので条件分岐 if (options.datasourceData !== undefined ) { directive. document .mainTemplate.parameters = [ 'payload' ] directive.datasources = { data: { properties: options.datasourceData, type: 'object' , } , } } return directive } 専用の関数で書き出すようにしたことでフォーマットのミスによる描画失敗はなくなりました。この仕組みを使ってよかった点はこの辺りです。 フォーマットを覚えなくていい 意識しなくても共通のテンプレートやスタイル、 Resources が反映できる データバインディングしたい時の payload の指定忘れがない 共通のレイアウトやスタイルと、画面独自のそれらをいい感じに混ぜてくれる 実際にコーデ相談は全ての画面で発生しうる処理(レベルアップの通知や検索の相槌)があるのですが、問題なく動作してくれています。また、画面独自のテンプレートやスタイルを分離できるのでシンプルになります。スキルによっては token は別のものを指定したくなるかもしれませんが、全ての画面で実行したいコマンドがあると破城するので token も共通にしてしまっています。 パーツを切り分ける ここまでで何度も出ているレイアウトやスタイルの話です。公式ドキュメントでも使おうと言っていますし、使っている人は多いと思います。ここではどのように分離して再利用しているかをコードベースで説明します。 コーデ相談はほとんどの画面でヘッダーとフッターが存在しています。このフッターを例に挙げて説明します。フッターのスクリーンショットと定義がこちらです。 コーデ相談のフッター export const footerLayouts = { Footer: { item: { type: 'Container' , when: '${suggestTexts.length > 0}' , data: '${suggestTexts}' , item: [ { type: 'Text' , text: '「アレクサ、${data}」' , fontSize: '@mainFontSize' , } , ] , lastItem: { type: 'Text' , text: 'と言ってみて' , fontSize: '@mainFontSize' , } , alignItems: 'center' , direction: 'row' , height: '@footerHeightSize' , justifyContent: 'center' , } , parameters: [ { default : [] , name: 'suggestTexts' , type: 'array' , } , ] , } , } @mainFontSize などのリソースを参照している部分はリソース名から意味を察していただけますと幸いです。このコードのポイントはこの辺りです。 Keyをレイアウト名にして、1つのパーツで使うレイアウトを1つのオブジェクトにまとめておく できるだけリソースやパラメーター、Flexboxレイアウトを使うようにして、柔軟に対応できるようにする パラメーターにはtypeを指定する フッターの例ですとレイアウトは1つですが、大きなパーツになってくると複数のレイアウトに分けたくなってくると思います。また大文字などで意味を持ちづらいKeyにレイアウト名を入れたほうが、標準のコンポーネントと同じ type: Footer のように使用できます。これらを踏まえて、レイアウトはオブジェクトでまとめて管理するのがオススメです。使用する場合はスプレット構文を使えば以下のように複数のパーツがある場合でも安心です。 const layouts = { ...headerLayouts, ...footerLayouts, } この方法に限った話ではないですが、レイアウトは名前がかぶるとまずいので命名規則は気をつけましょう。 最後に Alexaのスキルはシンプルなスキルが多く、コードの管理を重視するほど大規模なものは少ないと思います。声でできるタスクとして考えても、スキルはシンプルな方が良いでしょう。しかし、スキルでこなせるタスクがシンプルだったとしても、裏側の処理もシンプルとは限りません。柔軟に対応できるようソースコードは秩序を守っていきたいですね。 今回挙げたものはコーデ相談における一例ですので、「うちはこうしてるよ」などの記事や知見が増えてくると嬉しいです。どうしても海外が先行している音声UI界隈ですが、日本も盛り上げていきたいです。 ZOZOテクノロジーズのR&D新規開発チームでは今後普及するであろう技術を先行研究し、様々な技術を用いたサービスを開発しています。よりよいユーザー体験を提供するために、技術を駆使して最高のプロダクトを作りませんか? www.wantedly.com
アバター
こんにちは! 6月11日から13日にかけて開催された「 CES Asia 2019 」に弊社から3名(白木、中村、新井)が参加しました。本記事ではキーノートやトークセッションの内容、印象的だった展示をご紹介します。 CES Asiaとは? 米国で開催されるテクノロジーショー「CES(Consumer Electronics Show)」のアジア版として、2015年から開催され今回で5回目となります。自動運転を中心とする自動車技術や5G、AI、AR/VR、スタートアップなどのテーマに開催され、会場は上海市にあるShanghai New International Expo Centre(上海新国際博覧中心)で6つのホールを使用して、展示やキーノートスピーチやトークセッションなどイベントが行われました。 “Into the Data Age” 初日のイベントセッション「Trends to Watch」は、“Into the Data Age”という言葉からはじまりました。この10年はコネクティビティをキーワードに技術が発展してきたが、これからはすべてのデータがつながっていく時代だという言葉が印象的でした。 4回目の革命 “蒸気"を使った産業革命に始まり、“電気”の誕生、“IT”革命と続き、今は5GやAIをはじめとした“スマートテクノロジー”が新しい革命を起こしている。新しい体験や生活向上といったものがもうSF映画の話ではなく、現実世界で始まっている。スマートテクノロジーを「5G」「AI」「ビークルテック」「AR/VR」「Startups」の5つにわけてキーノートの各所で紹介されたり、展示会場ブースがまとめられていました。5つ分野については後半で詳しく解説します。 5GとAIがすべての根幹にある 5Gがはじまるとこれまで以上に大容量の情報を遅延少なく、高速に送受信できることですべての産業にインパクトがあります。国をまたいでの心臓手術や遠隔で重機を操作した工事が可能になったり、自動運転やスマートシティの実現を加速させたり。またこれまででは考えられなかった産業や企業同士のコラボ・パートナーシップを生む可能性を大きく秘めています。これまで分散していたデータがつながり、集約されることでAIが各テクノロジーや分野でこれまでできなかったことを次々に可能にしています。5GとAIをベースとして、その上での各種スマートテクノロジーが成り立っています。 5つのスマートテクノロジーのテーマごとに印象的な展示をご紹介します。 5G 各国で5Gの立ち上げを競争している中で、中国では3大キャリアが協力して40の都市で5G実現に向けて取り組んでいます。1Gからの進化としてまず電話やテキストが送れるようになり、3Gでインターネットに接続、4Gではストリーミングが可能になり動画や音楽が発展しました。5Gは建設業と通信会社が協業したりなど、新たなパートナーシップを可能にていきます。5Gは以下のプロダクトを実現する上ですべての根幹として考えられています。 AI AIは無人店舗や顔認証、自動運転や需要予測や分析など生活の至ることろに存在し、存在がもはや特別なものではなくなりつつあります。日本では人口減少による労働力減少をAIやロボティックスの発展で補うことが急務であり、韓国は政府が強く主導してAI開発を牽引したりと各国それぞれ発展の背景が異なります。 教育や接客としてのロボット 今回の展示ではスマートディスプレイにキャラクターを搭載したモデルや、スマートディスプレイを拡張したような子供向けの知育ロボット・接客を効率化するための案内ロボットが多く見られました。 スマートディスプレイに近い知育ロボット「DODOLEON」。対話はもちろん、外国語などの勉強ができる。 http://www.dodoleon.com 接客ロボット「优友U05」。主にホテルの受付やレストランでの案内などに使われることを想定している。 http://www.uurobot.com/ 「無機質なタスク処理を行うロボット」を脱却しようと、人とロボットの関係性を模索しているのが現状のようです。1つの解として「人に寄り添う、キャラクター性のある愛らしい存在」のロボットがたくさん展示されていました。 「编程小将」ビジュアルプログラミングから、本格的なプログラミングも可能な知育ロボット。 https://aiiage.com/%E6%8D%8D%E5%9C%B0.html 「HELLO KITTY / 智能教育机器人」キティちゃんの形をした知育ロボット http://www.uurobot.com/en/kt.html 「PADBOT / 派宝机器人」無人コンビニやモーターショーなどで利用することを想定した接客ロボット。 https://www.padbot.com/padbotp3 接客や配膳ができる給仕ロボット「PEANUT / 花生引领机器人」 http://www.keenonrobot.com/Product/pro1.html 顔認証による需要予測や決済 苏宁易购 では需要予測やアプリ内の会員情報と店舗のセルフレジにて顔の紐付けを行うことで、顔認証による決済が可能になる展示がありました。 特定の位置に立っていると顔をスキャニングしてくれ、おすすめの商品をレコメンドしてくれる。 RFID(Radio frequency identification)を搭載した無人レジでは支払いの際に顔をスキャニングし、初回に顔とアプリの会員情報を紐づけることで以降は顔認証の支払いが可能に。 AR/VR ARデバイスがほぼサングラスと同等のサイズまで軽量でかつコンパクトなものまで登場しはじめました。またVRの技術では視覚以外にも自身が運動したり、音や振動を感じたりと拡張が始まっています。※拡張VRの事例紹介を後述のスタートアップでも紹介しています。 ARメガネ 医療の現場や物流・倉庫などの業務効率化につながる仕事はもちろん、エンターテイメントコンテンツやゲームなど、プライベートで使用することを想定した小型のARメガネの展示が目立ちました。 小型ARメガネ「JIMO」 http://www.shadowcreator.com/jimo/jimo.html スカウター型ARデバイスのプロトタイプ Shanghai Top Smart Technologies Co., Ltd. http://en.topsmarteye.com ゲーム筐体としてのVR VRLEO はゲームセンターなどの店舗開設や人を雇用せずともVRゲームを提供できるゲームの筐体です。 コントローラーが盗まれたり、落として壊れないよう衝撃を吸収する工夫が施されている。 ショッピングモールや地下鉄・娯楽施設など人通りの多い場所の小スペースを活用できるのが特徴的です。WeChat Payで4.98元を支払うと5分間遊ぶことができました。(2019/6/12時点) ビークルテック 5Gが生まれることで、車同士だけでなく街や交通システムとつながることで交通需要予測や事故防止への取り組みなど、自動運転を含め移動手段全体がつながっていきます。スマートでスムーズな移動技術が進歩することで、より快適な生活を育んでいきます。また中国では2018年時点で自動車全体の販売売上が前年割れしている中で、普通車とは対象的にEVが前年160%成長など急速に拡大しています。 ビークルテックのスペースは6会場中2会場を使用し、他のスペースに比べても各社気合の入った展示が多く見受けられました。 中でも、自動運転・カーナビのOSが特徴的で「車の中から家電を操作するデモ」や、「未来で自動運転が当たり前の世界を仮定し、乗車体験を再定義する動画・プロトタイプ」が多く、5G到来時の基盤づくりを行なっている印象です。 Baiduの展示スペース。Baiduが開発した自動車AIシステムは顔認証はもちろん、運転手の疲労観測・ARナビなどの機能を搭載している。詳しくは Baiduの動画を参照 。 Audiのコンセプトカー。車のハンドルは収納することが可能で、基本は自動運転を想定している。 www.youtube.com 「Holoride」車の移動速度とVRコンテンツが連動しており、乗車体験を楽しめるコンセプト動画。外の展示ではこのデモを体験することもできた。 スタートアップ 中国ではスタートアップ企業も活発でたくさんの企業が生まれては消えていく中で、新しいイノベーションも生まれています。 VRの拡張型 Birdly では、マウスやボタンなどは一切使用せず、腕や手を使って鳥になった気分が味わえます。その他にも、ジェットコースターVRと大型筐体ゲーム機を掛け合わせることで、今までにない没入感を体感することができました。 「Birdly」体験者は横に寝転がり、鳥のように、空を飛んでいる映像を見ながらデバイスを操作する。 360度回転可能な筐体で、まるで本当にジェットコースターに乗っているかのような体験ができる。 Whale Future Store 店舗内の「顧客の行動を追跡するカメラ」と「顔認識ができるカメラ」を活用し、ディスプレイに表示される広告や、パーソナライズされたクーポンなどを表示することができます。 RFIDや商品の重さを棚が認識できることで手に取った商品の広告を瞬時に表示することなども可能。 トークセッションでは最新テクノロジーの紹介に合わせて、各国のテクノロジー普及動向についても言及がありました。 アジア地域の人々は、欧州と比べて2倍以上も新しい技術に対して意欲的です。その中でも中国は、スマートスピーカーやデジタルアシスタントといった新しい技術を世界の他の国と比較しても導入スピードが早い。ユーザーも個人の情報をサービス側に提供することに大きな抵抗はなく、むしろレコメンドされることを好む傾向にあります。一方で、日本はマーケットが大きいにも関わらず新しいテクノロジーの導入がとても保守的で遅いので世界的にもユニークな存在です。調査・分析によると日本は情報のやりとりにセンシティブであったり、長い間経済が停滞していた影響で国民全体が新しいものを取り入れるのに積極的ではないとありました。 またスマートスピーカーの活用用途で中国、韓国、日本、シンガポールの4カ国とも天気やニュース、時間などの情報取得が1番でした。日本や韓国の次に来る用途が音楽などのエンタメが来る一方で中国はコマース利用が2番目に挙げられていたのが非常に印象的でした。日本では音声UIの導入が徐々にはじまっているフェーズですが、中国ではモバイルペイメントの普及やテックジャイアントの強力なプロモーション、そして新しいものをどんどん取り込む国民性の影響から、声だけでものを買うのが当たり前になりつつあります。これは北米や欧州と比較しても特筆すべき特徴であると紹介されていました。 所感 白木:新しい技術はその目新しさが落ち着き、実用化を具体的にイメージできる段階にはいりました。プロダクトとしては、掛け合わせ、組み合わせで新たな体験を生み出しているもの、小型化や親近感をわかせる工夫でより身近なものになってきています。私たちも新しい技術を取り入れるだけに留まらず、世界がよりよくなるため、ひとびとの喜びを増幅させるようなプロダクトをつくっていきたいです。 中村:キーノートに「すべてのデータがつながっていく時代」とありましたが、本イベントに参加することで今後の発明は「説明や写真だけでは、何が具体的にどう新しくて素晴らしい体験なのか?」実感値をもって理解することができないものも増えそうだと感じました。これからも新しい体験をキャッチアップしつつ、データを人々のために活用し、みんなの心に寄り添えるサービスを提供していきたいと思います。 新井:強烈に真新しい技術を今回見つけられませんでしたがキーノートや展示からAIをはじめとしたスマートテクノロジーが特殊なものではなく当たり前に生活に入っていく近い未来を痛感しました。国の特徴比較で日本は遅いと言及がありましたがそれを打破できるように人々の生活が豊かにまたおもしろくなるものづくりをしていきたいと思います。 最後に ZOZOテクノロジーズでは、最新技術に対する感度が高くユーザー目線でプロダクトの開発をしたい人を募集しています。興味のある方はこちらからご応募ください! www.wantedly.com
アバター
こんにちは。ZOZOテクノロジーズ リプレイスチームの杉山です。 本記事では、ZOZOTOWNリプレースで行っている「マルチクラウド環境への移行」を目指したデータベースの監視システムを「Kubernetes CronJob」と、監視SaaS「Datadog」を使用して構築した事例をご紹介したいと思います。 マルチクラウドを見据えた設計と監視システムの構築 弊社のリプレースプロジェクトでは、マルチクラウド環境構築での運用を目標としているため、データベースの監視システムも各クラウドベンダー(Azure・AWS、GCPの3クラウドベンダーということで、以下、3Cといいます)のリソースを横断的に監視できる必要があります。 各ベンダーでは、そのクラウドサービスのリソースを監視する機能が提供されています。 しかし、各々のツールを使用して監視運用をすると運用の手間がかかるため、これらを一元管理する必要があります。 設計の段階からマルチクラウドでの使用を見据えて、監視システムを設計しました。 マルチクラウド監視を実現できるサービスを選択 標準ツールでは難しいマルチクラウド監視の実現には、共通で提供しているサービスの選択や、外部の監視SaaS・Slackなどとの連携が不可欠です。 また、システムのメンテナンスコストも考慮して設計する必要があります。 そこで、共通で提供しているサービスリソースから、以下のようなサービスを使用することを選択しました。 <プラットフォーム> Kubernetes(AKS、EKS、GKE) <コンテナエンジン> Docker(Kubernetesがサポートしている) <ジョブスケジューラー> Kubernetes CronJob(Kubernetesリソース)(以下、本文中はCronJobという) <データベース> SQL Server(Azure SQL Server、Amazon RDS for SQL Server、GCPのSQL Server) ※GCPではまだGAされていないが将来的にGAされることを見据えて。 ※SQL Serverを選んだ理由には、会社的な理由もありました。 <監視SaaS> Datadog - 3C各社のIntegrationsに対応 - カスタムメトリクスの送信にも対応 <通知> Slack <オンコール> PagerDuty 監視システムの要件 監視システムの要件としては、以下の内容があげられます。 安定した定期実行と定期的なメトリクス取得 コンテナイメージを3Cそれぞれに最適化せず汎用的に使う SQL Serverのジョブやレプリケーションの遅延状態などのカスタムメトリクス取得 台数増減に合わせた稼働しているデータベースのカスタムメトリクスを動的に取得 これらを考慮したAzureでの構成は以下のようになります。 具体的な設計やポイントを、各フェーズに分けてご説明します。 「Kubernetes CronJob」を選択した理由 以前は、Kubernetes上ではありますが、Linuxのcronを利用して運用していました。 当初は特に問題なく稼働していましたが、監視対象の数が数十台になって来た時に「Can not allocate memory」のエラーがぽつぽつと出るようになりました。 APIのチューニング、並列化、drop_cache設定、flockでの多重実行の制御、ログ回りの最適化などの対策などを講じましたが、根本的に監視システムのパフォーマンスを向上させるためには、新たな定期実行のシステムを設計する必要があると考えるようになりました。 弊社では、アプリケーションの運用にすでにKubernetesを使用していました。 運用コストも考えるとKubernetesでの運用が良いと判断し、Kubernetesの定期実行リソースである「CronJob」を選択しました。 定期実行を実現する「Kubernetes CronJob」の機能 CronJobは、cronと同じようにスケジュール設定に基づいて、定期実行を実現するリソースです。 cronにはない様々な便利な機能が使用できます。 「Kubernetes CronJob」の特長 CronJobには、以下のような特長があります。 スケジュール設定に基づいて、一度だけ実行される処理を実装したコンテナを定期的に起動する。 処理が完了したコンテナは破棄される。 毎回、新規コンテナが起動するため不要なキャッシュなどがコンテナ内に溜まらない。 実行失敗時のリトライ制御・生存可能な時間設定・実行開始の遅延許容の設定・多重実行時の制御などの有用な機能がある。 以下は、CronJobの各種機能のマニフェストサンプルです。 apiVersion : batch/v1beta1 kind : CronJob metadata :  name : my-cronjob spec :  concurrencyPolicy : Allow # Allow(同時実行許可) / Forbid(スキップ) / Replace(キャンセルして入れ替え)  schedule : "*/1 * * * *" #1分毎 cronのスケジュールと同じ設定方法  startingDeadlineSeconds : 30 #30秒 Jobのスケジュール遅延許容  failedJobsHistoryLimit : 1 #異常終了したJobの履歴保有数  successfulJobsHistoryLimit : 3 #正常終了したJobの履歴保有数  suspend : false # スケジューリングの対象とするかどうか trueでスケジュール対象外  jobTemplate :   spec : completions : 1 #正常終了とするJOBの完了回数 parallelism : 1 #Jobで同時にPodを実行できる並列数 backoffLimit : 2 #何らかの理由で失敗したJobのリトライ回数 activeDeadlineSeconds : 300 #300秒 Jobの生存可能な制限時間を秒数で指定する template :     以下にはJobのテンプレート設定を記述。 コンテナイメージを汎用的に使う 作成する各コンテナイメージを3Cで汎用的に使えるようにするためには、環境変数を利用します。 Kubernetesでは、マニフェストと呼ばれる設定ファイルに「ConfigMap(環境変数)」や「Secret(秘匿情報)」を指定して起動することで、コンテナに環境変数を注入できます。 コンテナ内のアプリケーションは、環境変数を参照して稼働させることで、同一イメージから稼働設定を変えたコンテナが起動できます。 例えば、使用するクラウドが変わったとしても、対象クラウドに合わせたマニフェストを作成しapplyすることで3Cそれぞれに対応することが可能です。 (もちろん、適切なネットワーク設定やアクセス権限が必要です)。 以下は、ConfigMap(環境変数)とSecret(秘匿情報)のマニフェストファイルの例です。 apiVersion : v1 kind : ConfigMap metadata : name : my-configmap-common data : YOUR_ENV : <YOUR_ENV> --- apiVersion : v1 kind : Secret metadata : name : my-secret-common data : YOUR_ENV_SECRET : <YOUR_ENV_SECRET_BASE64_ENCORDING> type : Opaque 「Kubernetes CronJob」で利用する各コンテナ設計のポイント Kubernetesでは、適切にコンテナの役割を分けた、コンテナ・デザイン・パターンを考える必要があります。 例えば、サイドカーパターン、アンバサダパターン、アダプタパターンなどのパターンがあります。 今回構築した監視システムのコンテナは、次の2つとなるため、アンバサダパターンで進めました。 Jobコンテナ: APIにメトリクス情報取得をリクエストし、Datadogにメトリクスを送信する。 APIコンテナ: 対象クラスタとデータベースを特定し、必要な処理を実行し各種メトリクス情報を生成する。 役割と目的から考えると、アンバサダパターンで問題無いように思われます。 しかし、この構成では以下のような挙動となり、Jobが完了(Completed)しません。 CronJobで使用する「Job」は先述した通り「1度だけ実行し、終了したら破棄される」という仕様となっています。 そのため、同一Pod内にJobとしてAPIのような「常時待ち受け」をするコンテナがいると、Jobが完了しません。 NAME READY STATUS RESTARTS AGE pod/my-monitor-cronjob-xxxxxx 2/2 Running 0 32s  ↓ NAME READY STATUS RESTARTS AGE pod/my-monitor-cronjob-xxxxxx 1/2 Running 0 52s このため、APIコンテナはdeploymentに変更します。 この場合、JobとAPIの通信ではPodが違うため、ポート番号での通信ができません。 そのため「my-monitor-service」として「ClusterIP」を作成し、名前解決で通信できるようにします。 これにより、Jobコンテナは「my-monitor-service:<port番号>」でAPIへアクセスできるようになり、 Jobが完了します。 NAME READY STATUS RESTARTS AGE pod/my-monitor-cronjob-xxxxxx 1/1 Running 0 14s pod/my-monitor-deployment-xxx 1/1 Running 0 21h  ↓ NAME READY STATUS RESTARTS AGE pod/my-monitor-cronjob-xxxxxx 0/1 Completed 0 37s pod/my-monitor-deployment-xxx 1/1 Running 0 21h 動的に対象クラスター内に存在するデータベースを探す APIコンテナでは、クラスター内に存在しているDBのタグ情報などを動的に取得するため、クラウドが提供しているREST APIを使用しています。 これにより、対象クラスター内のデータベースの台数が変わった場合にも動的にDMV情報の取得が可能になります。 【ポイント】 ターゲットの台数と監視間隔によっては、このREST APIのリクエスト制限を超過することがありました。 この制限は、Azureであれば「30分に700回のリクエスト」という制限で、ベンダーへ緩和対応を要求しましたが制限解除や緩和ができませんでした。 データベースのタグ情報取得の部分は頻繁に変わるものではありませんが、負荷に応じたスケールアウト時などは可能な限りリアルタイムで情報を取得する必要があります。 対策として、許容できるTTLでキャッシュを使用して制限内に収めています。 この点は、1実行で破棄されてしまうJobコンテナではできず、deploymentにしたことで可能になった対応です。 (別でキャッシュサーバーを用意すればJobコンテナからもキャッシュを使用する事は可能です) 各種コンテナのOSイメージ 本システムでは、使用するコンテナのOSにミドルウェアなどをインストールしたイメージを別途作成し、コンテナリポジトリに保管して使っています。 設計時の必要なミドルウェアがインストールされた状態のimmutableなOSイメージを保持しておくことは、コンテナでのシステム運用においては「immutable inflastructure」の観点からも、とても重要です。 理由としては、次のような事があげられます。 ミドルウェアやドライバーが予期せずバージョンアップされてしまうことを防ぐ。 必須のドライバーなどが、何らかの理由で公開が停止になった場合の対策。 実際に、メンテナンス時にSQL Serverのドライバーのインストール方法が変わったことによる影響で、ビルドができなくなってしまったことがありました。 急遽対応を入れて事なきを得ましたが、ミドルウェア構成の変更は緊急対応ではなく検証の時間を取って行いたいものです。 オフィシャルのイメージだったとしても、突然削除される可能性がないとは言えません。 具体的なDockerfileのコードは割愛しますが、今回は、Jobコンテナ・APIコンテナの両方で使用するミドルウェアを全てインストールしたコンテナイメージを作成します。 Jobコンテナ CronJobで使用するJobは「1回だけ実行される処理」をコンテナ化します。 Jobコンテナは、次の2つの処理を1度だけ実行するように設計します。 メトリクス取得をAPIにリクエスト。 取得したメトリクスをDatadogに送信する。 稼働設定を変更する情報は、環境変数で注入します。 Datadogへのメトリクス送信は、公式のDatadog APIを使用しています。 API Referenceページ にもサンプルコードがありますので、Pythonで実装します。 以下は、Datadog APIでTagsのカスタムメトリクスを送信するコードサンプルです。 # -*- coding:utf-8 -*- import os import threading import requests import json from datadog import initialize from datadog import api print ( "monitoring start" ) # env datadog # 環境変数から秘匿情報を読み込み適用します。 dd_api_key = os.environ.get( 'DATADOG_API_KEY' ) dd_app_key = os.environ.get( 'DATADOG_APP_KEY' ) options = { 'api_key' :dd_api_key, 'app_key' :dd_app_key } initialize(**options) def main (): #APIへ渡すパラメーターを環境変数から読み込む target = os.environ.get( "TARGET_CLOUD" ) param = os.environ.get( "DATABASE_CLUSTER" ) #API情報 host = 'my-monitor-service' version = '/api/v1' url = 'http://my-monitor-service:80/monitor/<target>?param=<param>' domain = 'my-monitor.jp' #JSONパース result = requests.get(url) jsonDic = json.loads(result.text) #整形と送信 if result.status_code == 200 : for object in jsonDic #---- #APIからのレスポンスJSONをもとにリクエストを組み立てる # ~ #例: #metricName = 'メトリクス名' #value = '値' #domain = 'ドメイン' #tags = 'タグ情報の配列'  #など #---- #スレッドでDatadogへ並列送信 process = threading.Thread(target=metricToDatadog,args=(metricName, float (value), domain, tags)) process.start() #メトリクス送信ファンクション def metricToDatadog (metricName, value, host, tags): api.Metric.send(metric=metricName, points=value, host=host, tags=tags) # monitor run main() print ( "monitoring end" ) 【Jobコンテナのポイント】 APIのURLは、Kubernetesのservice(ClusterIP)で使用する名前解決の名称と同じ「my-monitor-service」にします。 作成したOSイメージを使用して、モニタリング実行用のコードと、コマンドのシェルを実装したイメージを作成します。 【Jobコンテナのログについて】 ログの出力先は、必要に応じて設定してください。 Jobコンテナは1実行で破棄されるので、ログの出力を残したい場合には標準出力をノードロギングエージェントを使うなどして、外部サービスなどに保管してもよいでしょう。 APIコンテナ APIコンテナは、ご自身の好きな言語で開発していただければと思います。 今回は、Linux・Apache・PHP・Lumen・Swaggerで作成しています。 APIコンテナは、Jobからのリクエストを待ち受けるため、deploymentとしてコンテナ化します。 次の3つの処理を行うように設計します。 クラウドのREST APIを使用しデータベース情報を取得。 ターゲットDBにクエリを発行。 クエリ実行結果からタグ情報を生成しJSONを返却。 秘匿情報は必要に応じて環境変数で注入します。 Lumenフレームワークや、API仕様を定義するSwagger(OpenAPI Specification)については割愛します。 このAPIコンテナには、必要な情報を取得するSQLファイルを、yamlファイルでファイル別に格納しています。 APIは、このSQLファイルの格納されているディレクトリ内にあるクエリを条件に応じて実行するように設計しています。 クエリを追加したい場合は、指定フォルダ内にルールに従ってファイルを追加することで自動的に実行クエリが追加されます。 以下は、yamlで作成したSQLファイルの例です。 メトリクス名、対象データベース、SQLなど、必要な情報を適宜記載しています。 metrics_name : sql_server.custom.database_job database : 'xxxxx' run_type : sqlserver monitor_sql : | ここに情報取得のクエリを記載する こちらも作成したOSイメージを使用して、APIコンテナのイメージを作成します。 なぜ、Jobにデータベースのメトリクス取得の処理をさせずAPIを使うの? 大きな理由として以下の2点があげられます。 1:コンテナ最適化(コンテナの役割は、あまり大きくせず小さくする方がよい) Jobコンテナは可能な限り軽快に立ち上がり、素早く破棄されるように軽く動くような設計にする必要があります。 そのため、比較的処理の重い「メトリクス情報の生成」部分はAPIとして、コンテナ化しています。 2:REST APIリクエスト回数制限 「動的に対象クラスター内に存在するデータベースを探す」でポイントとしてあげたとおり、外部のAPIを使用する場合は、制限も考慮に入れる必要があります。 今回はAPIを別にdeploymentとしたことで、解決していることも1つの要因です。 以上のように、稼働設定などの必要な情報をパラメータや環境変数として注入するように設計・実装することで、どのクラウドでも対応できるようにしています。 クラウドリソース情報を取得するAPIは、各クラウドのAPI仕様に合わせてAPIエンドポイントを作る必要はありますが、イメージが1つで汎用的であることで運用管理コストは格段に下がります。 ローカル確認でのコンテナの連携方法 CronJobで定期実行させるという点を除いて、JobコンテナとAPIコンテナの連携とメトリクスが送信できていることを確認できれば、コンテナテストの目的は達成できます。 ここでは、本番Kubernetesでの設定と、それに近い形での動作確認を「minikube」などを使わずに確認するdocker-composeのテクニックのお話です。 今回はdocker-composeを利用しています。 テスト環境は以下のような構成でコンテナを起動します。 これを実現するdocker-compose設定のサンプルを以下に記載します。 なお、APIの仕様作成やAPIのテストを簡単にする「swagger-editor」と「swagger-ui」も一緒に起動しています。 後述するポイントで軽くお話しします。   version : "2" services : my-monitor : build : ./job links : - my-monitor-service environment : - 実行に必要なパラメーターなどの環境変数   #1回実行を確認する場合   #command: ["/bin/bash", "-c", "/bin/docker-entrypoint.sh"] #※1 #ローカルでdocker-composeでコンテナに入って確認する場合 command : [ "/usr/sbin/httpd" , "-DFOREGROUND" ]  #※2 my-monitor-service : build : ./api ports : - “8888:80” #swagger-ui用 environment : - 実行に必要なパラメーターなどの環境変数 command : [ "/usr/sbin/httpd" , "-DFOREGROUND" ] swagger-editor : image : swaggerapi/swagger-editor container_name : "swagger-editor" ports : - "8881:8080" swagger-ui : image : swaggerapi/swagger-ui container_name : "swagger-ui" ports : - "8882:8080" environment : API_URL : "http://localhost:8888/api-docs" 以下のコマンドで、コンテナを起動します。 docker-compose up -d コンテナの起動状態を確認します。 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 15d8baa91469 my-monitor "/usr/sbin/httpd -DF…" 4 hours ago Up 4 hours 80/tcp my-monitor_1 ff2ac4b06899 my-monitor-service "/usr/sbin/httpd -DF…" 4 hours ago Up 4 hours 0.0.0.0:8888->80/tcp my-monitor-service_1 70178c24d238 swaggerapi/swagger-editor "sh /usr/share/nginx…" 4 hours ago Up 4 hours 0.0.0.0:8881->8080/tcp swagger-editor 3ebbecf86616 swaggerapi/swagger-ui "sh /usr/share/nginx…" 4 hours ago Up 4 hours 80/tcp, 0.0.0.0:8882->8080/tcp swagger-ui 【ポイント】 docker-composeでのポイントを記載します。 <Links> Jobコンテナ「my-monitor」を、APIコンテナのサービス名「my-monitor-service」にLinksで連携させる。これにより、Jobコンテナから「my-monitor-service」の名前解決でAPIを呼び出しできるようになります。 本番のKubernetesでも、Jobは同様の名前解決でAPIを呼び出します。 <swagger-editor> 「localhost:8881」swagger-editorでswaggerファイルを編集できます。 「Conver and save as JSON」swagger-ui用のJSONを書き出しできます。 「Generate Server」「Generate Client」様々な言語の実装コードに書き出しできます。 <swagger-ui> API仕様書としてSwaggerを使用しておりますので、モックサーバー(localhost:8882)を利用してAPIをテストできます。 簡単にAPIテストができるので大変便利です。 <APIの確認方法> 「localhost:8882」swagger-uiでAPIをテストできます。 <Jobの確認> こちらの確認方法は2つあります。 1.docker-composeのコードの※1を有効化する場合 Jobコンテナは、起動すると「/bin/docker-entrypoint.sh」を実行し、完了すると終了します。 成功している場合は、DatadogのMetricsExplorerでメトリクスを確認できます。 2.docker-composeのコードの※2を有効化する場合 コンテナにbashで入り、実行予定の「/bin/docker-entrypoint.sh」を手動で実行します。 実行後、ログを確認します。 以下は、テストのコマンドの例です。 #コンテナに入る docker container exec -ti my-monitor /bin/bash #実行される予定のスクリプトを実行 ./bin/docker-entrypoint.sh #ログを確認する Kubernetesへのapply ここまでで作成したイメージをKubernetesにapplyします。 Kubernetesのマニフェストファイルは肥大化しがちなので、kustomizeを使用しています。 kustomizeは「kustomization.yaml」に記載した複数のマニフェストファイルを1つにマージしてくれる機能です。 「kustomize build」コマンドを使用すると、「kubectl apply」可能なマニフェストファイルを作成してくれます。 (Kubernetes1.14で統合されました) baseとoverlayのマニフェストを用意することで、両者をマージしてくれるため、より効率的に管理できます。 それぞれのマニュフェストのサンプルは、コードが長くなるため割愛しますが、以下のようなイメージです。 メトリクスの可視化を監視SaaSで集約する 【Datadogのサービス内容】 Datadogの主要サービスとしては、次のようなことがあげられます。 ダッシュボード作成 メトリクスの可視化 閾値などを使用したアラート 各種コミュニケーションツールへの通知 ログアナリティクス 外形監視 私のチームでは、使い勝手が良いためDatadogを使用して送信したメトリクスを可視化したり、アラートを設定しています。 【Datadogのクラウド連携機能】 Datadogでは、基本的なクラウドリソースのメトリクスを取得し可視化する「Integrations」という機能があります。 Integrations機能には、次のようなものがあり、3Cのクラウドリソースのメトリクス可視化に対応しています。 Azure Integrations AWS Integrations Google Cloud Platform Integrations これらのIntegrationsで取得できるメトリクスと、本システムで取得したカスタムメトリクスを組み合わせることで、3Cリソースを包括的に監視できます。 まとめ いかがでしたでしょうか? 「Kubernetes CronJobを使ったクラウドSQL Databaseの監視と運用」のお話をさせていただきました。 マルチクラウド化の推進のほか、アプリケーションの分散トレーシングなどにも力を入れていきたいと思っています。 ZOZOテクノロジーズでは、一緒にデータ基盤を作ってくれる方を募集しています。 ご興味がある方は以下のリンクから是非ご応募ください! www.wantedly.com
アバター
こんにちは!  開発部の名取と西山です。 今年のWWDCはSwiftUIをはじめ、Apple Developerの未来を変えそうなおもしろい発表が目白押しでした。 現地時間の2019/6/3-7にアメリカのカリフォルニア州サンノゼ、マッケンリーコンベンションセンターにて行われたWWDC19にZOZOテクノロジーズからは8名が参加してきました。 ZOZOテクノロジーズでは海外カンファレンスへの参加が推奨・サポートされており、多くのエンジニアが様々な海外カンファレンスへ参加しています。 本記事では、WWDC開催期間中の現地の様子とLabsでのフィードバック、AltConf登壇の様子などをお伝えします。 また、この期間は会場周辺でもいくつかのカンファレンスが開催されており弊社からは @banjun が AltConf に登壇してきたのでその様子などもお伝えいたします! WWDC? WWDC(Worldwide Developer Conference)は、Appleが年に1回開催している開発者向けのカンファレンスです。 iOSやwatchOS、macOSの新バージョンなどをはじめ、開発環境周りの新機能などが発表されます。 多くの注目を集めるのは初日のキーノートですが、これは新OSや新機能の概要で、キーノート終了後に行われるPlatforms State of the Unionからがエンジニア向けのセッションとなります。 WWDC期間中の雰囲気 キーノート前日 WWDC期間中は大変盛り上がり、まるでお祭りのようです。 私は今年初めて参加しましたが会場の雰囲気、スタッフ・参加者のテンションの高さなど日本ではなかなか味わえない空気感にただただ圧倒され、とても刺激を受けてきました。 私たちは土曜日の夕方に日本を出発し日曜日の昼頃サンノゼ空港到着、前日チェックインしてきました。チェックイン時は多くのスタッフにハイタッチで歓迎されます。 ちなみに、今年のノベルティはジャケット(MA-1)とピンバッジでした。 キーノート当日 初日のキーノートはみんな前方の良席で見ようと早朝から多くの人が並びます。 私は朝の4:00過ぎから並びましたがすでに100人以上の人が列に並んでいました。下の写真は早朝の3:00ですがすでに並んでいる人がいます。昨年はこの時間にはもっと多くの人が並んでいたようです。 キーノートの開始時間は10:00ですが、スタッフが早朝から盛り上げてくれるため楽しみながら並んで待つことができます。 キーノートの会場は超満員です。 ティムクックが登場すると会場は拍手喝采、大歓声に包まれます。 会場で初めて見るキーノートはこれまでライブ配信で見てきたものとはまるっきり別物でした。 ライブ配信では決して味わうことのできない現場の興奮と熱狂、聴いてる人を惹きつける洗練されたプレゼンテーション、あの会場のワクワク感は開発者でなくとも一度体験するべきだと感じました。 今年のWWDCは、SwiftUI、iPadOS、ダークモードなど開発者にとって魅力的な発表が盛りだくさんでした。 Labsで聞いてきた話などブログでお伝えしたい内容もたくさんあるのですが、NDAの都合上ブログでの技術的な話は割愛させていただきます。 2日目以降 2日目からは技術的なセッションが始まります。それと同時にLabsもオープンします。 Labsではセッションの内容や気になったトピック、日頃の疑問などをAppleのエンジニア・デザイナーに直接相談できます。 Appleのエンジニアやデザイナーと直接話をする機会はそう滅多にないため人気のLabsには長い行列ができます。 私はInterface Builder and Auto Layout Labに行ってAutoLayoutを用いたレイアウトの更新処理に関する質問をいくつかしてきました。 他にもApp Store Labでサービスを海外展開する上で注意すべきことや効果的な対応など日本ではなかなか手に入りにくい情報を得ることができました! Labsの中でも、予約制で抽選に当選した人が参加できるUI Design Labは毎年大好評です。 今年からは、drop in(立ち寄り)形式での相談も1回10分まで受け付けていました。 私は最終日にようやく当選してAppleのデザイナーとディスカッションをしてきました。Appleのデザイナー2人から30分かけてアプリのフィードバックを貰えたのはとても貴重でした。 このUI Design Labでは自分のアプリをAppleのデザイナーに触ってもらい、現状の優れている点や改善点がなどを細かくフィードバックしてもらえます。 UI Design Labは毎朝7:00〜7:30の間にWebから予約し、当選した人のみが参加できる特別なLabsです。 (世界最高峰のUI/UXデザインチームの方に開発中のアプリのフィードバックをもらうだけでWWDCのチケットの金額以上の価値があると思いました) 弊社のアプリ( ZOZOTOWN / WEAR )もたくさんのフィードバックもらってきたので、今回はその一部を公開します! UI Design Lab × WEAR モーダルを直感的に閉じられない コーディネートを「セーブ」する際に表示されるモーダルが直感的に閉じられないという指摘です。 ユーザーが閉じる動作として「ゆっくり、大きくスライドさせる」「はやく、小さくスライドさせる」と例をあげられました。 WEARの場合「はやく、小さくスライドさせる」動作で閉じることができないので、こちらの対応をする必要があります。 投稿画像の明るさ調整 投稿画像の明るさを調整できる画面に関してです。 スライダーでの調整は、ユーザーにとって簡単では無いので、必要ないなら無くした方がいいとのことです。 明るさ調整を入れたい場合は、標準の「写真」アプリのようにビジュアルで結果がわかるような選択肢にした方がいいとアドバイスをいただきました。 ビジュアルの方は操作が少なく直感的なので、検討する余地があります。 タグ付けするフローの画面遷移が多い もう1つ投稿フローから。 WEARにはクローゼット機能があり、アイテムをストックすることができます。 ストックから登録する際はワンタップでタグ付けすることができますが、直接アイテム情報を入力する場合は、 「カテゴリ」「カラー」「ブランド」が必須項目になるため、各項目を選ぶ度に画面遷移してしまいます。 ユーザーを迷わせないように、一画面で完結するようにした方がいいとアドバイスをいただきました。 ランキングのフィルターが簡単ではない ランキング上部に「変更」ボタンがあり、こちらから性別の切り替えなどができます。 「変更」ボタンを選択し、性別を切り替えるのはユーザーにとって簡単ではないとのことです。 改善案としては、WEARの「見つける」画面上部にあるカテゴリ選択部分のようにした方が簡単になると提案を受けました。 いくつかフィードバックを紹介させていただきましたが、どれも根底にあるのは複雑性を排除し、直感的な操作を求めているように感じます。 UI Design Labのメリットととして、次のようなことがあると考えます。 なんとなくイケてないと思っている所をAppleのデザイン原則に基づき言語化してくれる 限られた時間の中で指摘された所はユーザーインパクトが大きい 良いと言われた所は自信につながる Appleのデザイナーから「お墨付き」をもらえることにより、優先度をあげて対応しやすくなったり、良いと言われた所は自分たちの自信に繋がります。 驚いたことに、WEARに関しては特に質問を用意せずざっくりアプリを見てもらいましたが、限られた時間の中で的確で多くのフィードバックをいただけました。 そのプロフェッショナルな仕事ぶりに感動しました。 まだUI Design Labに行ったことがない方は、是非、次回以降のWWDCに参加し足を運んでみてはいかがでしょうか。 希望すれば通訳をつけていただけるので言語に不安がある場合も安心です! AltConfに登壇! WWDC開催期間中は、AltConfやtry! SwiftなどWWDC会場の周辺施設で他のカンファレンスも開催されます。 今年のAltConf 2019に弊社から @banjun がスピーカーとして登壇したのでその様子を簡単にお伝えします。 AltConfの会場はWWDCに隣接するSan Jose Marriott Hotelの2階フロアです。 スピーカールームは2つ用意されており、それぞれの部屋で同時進行します。 参加者はあらかじめスケジュールを確認し、聞きたい話を聞きにいく感じです。 この辺りの形式は日本で行われるカンファレンスと同様でスピーカートークはテンポよく行われます。 @banjun は「Auto Layout with an extended Visual Format Language」というタイトルで発表を行いました。 出典 : https://speakerdeck.com Auto Layoutの制約を書くVisual Format Languageの文法を拡張するカスタムパーサーを実装して、Safe Areaへの対応をわずかなコードの変更だけで実現する手法の話をしました。 発表前は少し緊張している様子でしたが、当日の発表は非常に洗練さており多くの注目を集めるおもしろいものでした! 登壇が決まってからは技術顧問の岸川さんにご協力いただいて練習した他、弊社のエンジニアでピアレビューも行いました。 まとめ 今年、WWDC初参加でしたがとても刺激的で学びの連続でした。 キーノートをはじめとする各セッションは勿論、Appleのエンジニアやデザイナーとのディスカッション、他のWWDC参加者の開発に対する熱量の高さなど非常に学びの多いカンファレンスとなりました。 また、iOS 13に関連する話やLabsで聞いてきた話などを詳しく聞きたい方は、6/12にZOZOテクノロジーズ青山オフィスにて開催予定の MeetUp にて弊社社員がLTを行いますのでぜひご参加ください! カンファレンスに関係する交通費・宿泊費・チケット代などは全て会社に負担してもらいました。 自分から希望すれば国内・海外問わずカンファレンスに参加する機会やサポートをいただけて本当に感謝です! ZOZOテクノロジーズでは、最新技術に対する感度が高く、ユーザー目線でプロダクトの開発をしたい人を募集しています。 興味のある方はこちらからご応募ください! www.wantedly.com おまけ せっかくなので最後にサンノゼの街並みをパシャり。 サンノゼは非常に日差しが強く暑い反面、空気がカラッとしていてとても心地よい気候です。 街の至る所にBIRDと呼ばれるレンタル電動スクーターが設置されており、アプリさえあれば誰でも利用できます。 非常に長閑な街並みで東京よりも時間の流れが遅く感じます。 街を歩いてるとリスを見かけたりもします。 サンノゼの街中には路面電車が走っておりローカルな街並みの雰囲気がでています。 WWDC期間中は街並みがWWDC一色になります。 現地スタッフが至る所に立っていて積極的に話かけてくれます。
アバター
こんにちは、開発部の茨木( @niba1122 )です。主に新規事業系の開発に携わっています。6/4〜6/7にかけて、ビジネスリーダー・開発者向けのAIカンファレンスである Amazon re:MARS に参加してきました。本記事では、筆者が実際に参加して面白かったセッションやワークショップに関して、開発者寄りの視点で書きます。 Opening Remarks & Keynotes Opening Remarks Keynote(day2) Keynote(day3) ワークショップ W03 - Build Intelligent Applications Quickly with AWS AI ソーシャルメディアダッシュボード AIを活用したお問い合わせセンター Amazon Personalizeによる動画リコメンドエンジン W05 - Finding Martians with AWS RoboMaker and the JPL Open Source Rover 印象に残ったセッション A05 - Understanding Customer Intent and Personalizing Shopping Experiences at Scale 類似商品のリコメンド 購買意思によるリコメンド リピートのためのリコメンド A07 - Speech Emotion Detection Multimodal Speech Emotion Recognition Adversarial Autoencoder for Acoustic-Only Emotion Recognition Tech Showcase DeepRacer Smart Home / Smart Mirror 最後に 参考文献 Opening Remarks & Keynotes Opening Remarks MARSはMachine learning、Automation、Robotics、Spaceの頭文字です。 今急速に発展しているこれらの分野に本気で取り組み、プラットフォームを構築していくというAmazon社の強い意思を感じました。MARSは3日間のKeynoteの軸になっており、開催期間を通じて各分野への取り組みや未来が紹介されました。 初日はまずロボットに関する話でした。 Boston Dynamicsのロボットの実演や、工場での荷物を運搬する2足歩行ロボットの紹介がありました。2足歩行ロボットはジャンプや宙返りなどもこなしており、進化に衝撃を受けました。 初日の最後にはアベンジャーズでおなじみのRobert Downey Jr.氏が登場し、会場は大きく盛り上がりました。 Keynote(day2) 2日目はJeff Wilke氏によるリコメンドの話から始まりました。色々な試行錯誤をした結果、過去20年分の購買履歴を学習して1週間後を予測するという比較的シンプルなモデルになったというのは意外でした(そもそも20年分の購買履歴を持っているあたりさすがAmazon社だなぁと思います)。 リコメンドの話の後は、Amazon Goや需要予測に関する発表がありました。 Keynote(day3) 3日目は、Amazon社のCTOであるWerner Vogels氏の発表から始まりました。機械学習のプラットフォームに力を入れていること、もうデータサイエンティストでなくても機械学習が使えることを強調していました。次は、Machine Learningで有名なAndrew Ng氏の発表でした。「スプリントは1日で回せ」「データは小さい方がよい」というのは意外で印象的でした。 この後数名登壇し、最後に登場したのが誰もが知るAmazon社のCEO、Jeff Bezos氏です。登壇はJenny Freshwater氏との対談形式で行われました。「リスクを取らなければならない。リスクがないならそれは誰かがすでにもうやっている」「今後変化しないであろうものに注目しなさい」というメッセージが印象的でした。 ワークショップ 1日目は、Opening Remarksの前にハンズオン形式のワークショップが開催されました。午前と午後に別れており、それぞれ参加しました。 W03 - Build Intelligent Applications Quickly with AWS AI このワークショップでは、Amazon社が提供するAIサービスと連携したアプリケーションを開発しました。題材のアプリケーションが3つもあり、すべてを終わらせることはできませんでした。しかし、音声認識や感情認識といった高度な技術を使ったアプリケーションをほんの数時間で開発できてしまうのが衝撃でした。 ソーシャルメディアダッシュボード ツイートに含まれるもの(人、日付など)や感情を解析し、ダッシュボードボードに可視化するアプリケーションを開発しました。以下のAIサービスを活用しました。 Amazon Comprehend: もの(エンティティ)の抽出や感情分析を行う Amazon Translate: ツイートの翻訳を行う 今までデータサイエンスが必要だった高度な解析も簡単にできました。また、ダッシュボードはQuickSightを用いましたが、GUIの操作のみなのでとても楽でした。 AIを活用したお問い合わせセンター ユーザーからのお問い合わせをAIが補助するアプリケーションを開発しました。 ユーザーとの通話はAmazon Connectで行い、音声の翻訳や感情認識は以下のAIサービスで行いました。 Amazon Transcribe Amazon Comprehend Amazon Translate Amazon Personalizeによる動画リコメンドエンジン このアプリケーションは開発を完了できませんでした。しかし、Jupyter Notebookのセットアップで、SageMakerの便利さを痛感しました。実際に触れることはできませんでしたが、リコメンドはAmazon Personalizeでできるようです。 W05 - Finding Martians with AWS RoboMaker and the JPL Open Source Rover このワークショップでは、火星人を検知するロボットアプリケーションをRobot Operating System(ROS)とAWS RoboMakerで開発しました。 ロボットアプリケーションの開発とAWSサービスとの連携がすべてCloud9上で完結してしまうのが印象的でした。 ビルドに成功するとシミュレータ上に探査機が表示されます。これもブラウザ上です。 こちらは、探査機からの映像で、ちょうど火星人を発見したところです。 火星人を検知すると、スマートフォンに通知が来ます。 物体や火星人を検知した回数はダッシュボードで確認できます。 印象に残ったセッション A05 - Understanding Customer Intent and Personalizing Shopping Experiences at Scale Amazon社には複数の事業領域にまたがる1億以上の商品があります。さらに、ユーザーも1億人以上おり、それぞれ好みやニーズが異なります。そのためユーザーはパーソナライズされたリコメンドを好みます。そのなかで、ユーザーが商品を探すのを手助けするだけでなく、ユーザーが本当に欲しいものを提供するのがリコメンドの目的です。 リコメンドには以下の難しさがあります。 Scale: 数億のユーザーや商品 Latency: 数ミリ秒でリコメンドを返す必要がある Dimensionality: 多様な商品カテゴリがあり、それぞれ性質が異なる Localization: 文化や言語はもちろんのこと物流の考慮も必要 Subjectivity: ユーザー毎の主観があり、リコメンドの受け取り方もそれぞれ異なる Evaluation: リコメンドが成功かどうかは究極的にはユーザーの満足度に依り、過去のデータにもバイアスが掛かっている 類似商品のリコメンド 類似商品のリコメンドには2つのアプローチがあります。 Behavior Similarity: ある商品を買ったユーザーが次に何を買ったかに基づく類似性 Contextual Similarity: 商品情報に基づく類似性 Behavior Similarityの方がスケールしますが、行動データがない初期にリコメンドできない欠点があります。 購買意思によるリコメンド 類似商品のリコメンドだけでユーザーは満足するのでしょうか。Amazon社は購買意思によるリコメンドも行っています。 購買意思は以下のような方法で取得できます。 検索 クリック ウィッシュリストへの追加 購入 しかし、購買意思はそう簡単に理解できません。購買意思は、ユーザー間だけでなくユーザー内でも大きく変わります。 購買意思を知る方法として店舗に着目する方法があります。店舗での 検索 閲覧 購入 によってもユーザーの購買意思を理解できます。 リピートのためのリコメンド アメリカでは60%以上の購入が消費財です。蓄積された購入データを基に、商品がリピートされるのか、いつリピートされるのかを判断しています。 A07 - Speech Emotion Detection Amazon社ではユーザー体験を改善するために感情認識を行っています。 感情認識ではまず感情を定義する方法があります。感情の定義をする時にまず思いつくのは感情を怒り・恐れ・幸せ・驚きなどのようにカテゴリ分けすること(Categorical)でしょう。しかし、このカテゴリ分類には正解がなく、正しく選ぶのが難しいです。 感情を定義するとき、成分毎に分解してそれぞれの成分量で表す方法があります(Dimensional)。 上の定義で感情を表現すると以下のようになります。 実際に感情認識を行うにあたり、2つの研究があります。 Multimodal and Multi-view Models for Emotion Recognition(ICASSP 2019) Improving Emotion Classification through Variational Inference of Latent Variables(ACL 2019) これらの論文を基にした方法が紹介されていました。 Multimodal Speech Emotion Recognition 音声を解析したテキストと生音声を併用して感情を認識する方法です。 Adversarial Autoencoder for Acoustic-Only Emotion Recognition 生音声のみをAdversarial Autoencoderで処理して感情を認識する方法です。 Tech Showcase DeepRacer 強化学習ができる1/18スケールのレーシングカーです。 写真の車は初めレーンに沿って走れないのですが、正しいレーンを繰り返し学ぶことで段々正しく走れるようになっていました。 Smart Home / Smart Mirror RoombaとAlexaが連携した住宅のサンプルです。 住宅の中にスマートミラーもおいてありました。 最後に Amazon社のプラットフォーマーとしての力を痛感した4日間でした。今や機械学習やディープラーニングといった技術は誰でも使えるものになりつつあります。また、すべての発表においてWhyが明確に示されているのがとても印象的でした。課題あっての技術ということを再認識した次第です。 ZOZOテクノロジーズでは、ファッション領域において技術の力で課題解決をしたいエンジニアを絶賛募集中です。 興味ある方はぜひご応募ください! www.wantedly.com 参考文献 https://aws.amazon.com/jp/robomaker/
アバター
こんにちは、開発部の鶴見です。 ZOZOTOWNのリプレースを担当しています。 ZOZOTOWNリプレースですが、オンプレからクラウドに単純に置き換えるのでなく「運用が楽になる」などメリットを考えながら作り替えています。 主にデータベースは、AzureのRDBである SQL Database を利用しています。 先日までSQL Databaseのパフォーマンスとコストがネックになっていました。そこでAzure Automationを利用しSQL Databaseを定期的にスケールアップ/ダウンさせリソース、コストの最適化をしました。 その方法をご紹介します。 はじめに スケールアップ/ダウンについて 多数のモデルが存在するSQL Database モデル選定 オートスケールを考える サンプル(CPUコア数変更) サンプル(クエリ発行) 参考情報(スケールアウト) サンプル(Geoレプリケーション設定) サンプル(ファイヤフォール設定) サンプル(脅威の検出・監査設定) スケール変更注意点 Azure AutomationでSQL Databaseをスケールアップ/ダウン モジュールのインストール Azure Automationの実行アカウント まとめ はじめに パブリッククラウドで利用できるサービスのほとんどが従量課金であり、SQL Databaseも時間単位で課金されています。 コスト、リソースが最適となるようSQL Databaseに対して定期的なスケールアップ/ダウンを行いました。改善にあたり達成目標は次の通りです。 スケールアップ/ダウンの自動化 適切なリソースの確保 適切なコスト ダウンタイム最小化 対応内容は次の2つです。 環境 対応内容 本番環境 時間帯や負荷に合わせて自動でスケールアップ/ダウンする対応。 開発環境 業務時間外に自動でスケールダウンし、業務時間内にスケールアップする対応。 上記の内容に加え「SQL Databaseは、どのようにスケールアップ/ダウンしているのか」「SQL Databaseのモデル選び」もご紹介します。 AWSやGCPでも同じような考えでRDBをスケールアップ/ダウンできると思います。 リプレースにあたりインフラ全体で「システムが安全、構成がシンプル、スケール可能、そしてコストが安い」状態を目指しています。 スケールアップ/ダウンについて SQL DatabaseをCPU4コアから8コアにスケールアップした場合、下図のような仕組みでSQL Databaseのコア数が変わります(スケールダウンの場合も同様)。 スケールアップ実行 新インスタンス起動、旧インスタンスから新インスタンスにデータコピー 新インスタンス準備完了、エンドポイント切替え 多数のモデルが存在するSQL Database SQL Databaseは、様々なモデルがあります。 チーム内でもモデル選びに迷いましたので、一覧にします。 どれもがSQL Serverと互換性のあるPaaSです。 種類 特徴 備考 DTUモデル CPU、メモリ、ディスクI/OなどをまとめてDTUという単位で表現されます。DTUとは、Database Throughput Unitの略です。リソースはDTUで管理されます。 簡単にDatabaseを扱いたい方にはオススメですが、CPUコア数やメモリ数の指定はできないため、細かく管理したい場合は不向きです。 vCOREモデル CPUコア数を指定できます。メモリ数は指定できませんが、CPUコア数と比例して増えます。 CPUコア数にてDatabaseを管理したい場合に向いています。 ハイパースケール ストレージが自動で拡張され、読み取り専用(セカンダリ)のスケールアウトは4台まで可能です。CPUコア数も指定できます。 Amazon Auroraに近いアーキテクチャーです。柔軟にスケールアウト/インできます。 Managed Instance SQL Serverとほぼ100%の互換性があります。CPUコア数も指定できます。 他のモデルではジョブが作成できない。などの制限があります。SQL Serverのフル機能が必要な場合、Managed Instanceを利用します。 サーバレス CPUコア数の最大と最小を指定することでスケールアップ/ダウンできます(2019年5月に発表されプレビューです)。 プレビューのためスケールアップは最大CPU4コアの制限があります。今後に期待します。 モデル選定 DTUモデルはCPU、メモリが管理できないことから採用しませんでした。 正確にはリプレース当初(2017年後半)はDTUモデルしかありませんでした。そのためリプレース当初はDTUモデルを利用していましたが、DTUに馴染めず、その後発表されたCPUコア数を管理できるモデルに乗り換えました。 ハイパースケールについては2018年12月にプレビューを検証した結果、当時はまだ性能面で不安があったので利用を見送りました。最近GAとなったため、また時期を見て再検証してみます。 サーバレスについては、求めていた自動のスケールアップ/ダウンを実現してくれるモデルですが、まだ発表されたばかりで最大CPU4コアの制限もあるため、採用を見送ります。 Managed Instanceは、SQL Serverのほぼ全ての機能を使えるのが売りですが、リプレースに於いて使いたい機能が特にありませんでした。性能に関してはvCOREモデルと同程度でした。 DTUモデル、vCOREモデルはGAから時間が経っていることありプロダクトとして安定しています。 上記のことからZOZOTOWNでは安定していて、CPUコア数も指定できるvCOREモデルを採用しています。 オートスケールを考える vCOREモデルを利用していますが、その他モデルも含めてスケールアップ/ダウンを考えます。 種類 方法 DTUモデル DTUを変更することでスケールできます。※DTUを変えたことでCPU、メモリがどの程度増えるのか分からないため、どのくらいDTUを上げればシステムとして足りていると判断するのか難しいです。 vCOREモデル CPUコア数を変更することでスケールできます。 Managed Instance CPUコア数を変更することでスケールできます。 ハイパースケール CPUコア数の変更に加えて、読み込み専用レプリカ追加(0~4台まで)によるスケールイン/アウトもできます。 サーバレス CPUコア数の最小と最大を設定することで自動的に変更してくれます。 上記のように、どのモデルを利用したとしても動的にスケールアップ/ダウンできそうです。 実装はAzure AutomationでサポートされているPowerShellを利用しました。PowerShellにてAzureを操作(リソースの削除、追加、変更)できます。 下記、vCOREモデルでスケールアップ/ダウンを考えた場合のサンプルです。 サンプル(CPUコア数変更) $resourceGroup = "XXXXXXXX" $databaseName = "XXXXXXXX" $serverName = "XXXXXXXX" $serverEdition = "XXXXXXXX" $computeGeneration = "XXXXXXXX" $vCore = "XXXXXXXX" $environmentName = "XXXXXXXX" $connectionName = "XXXXXXXX" $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName $environment = Get-AzEnvironment -Name $environmentName Add-AzAccount ` -Environment $environment ` -ServicePrincipal ` -TenantId $servicePrincipalConnection .TenantId ` -ApplicationId $servicePrincipalConnection .ApplicationId ` -CertificateThumbprint $servicePrincipalConnection .CertificateThumbprint Set-AzSqlDatabase -ResourceGroupName $resourceGroup ` -DatabaseName $databaseName ` -ServerName $serverName ` -Edition $serverEdition ` -ComputeGeneration $computeGeneration ` -VCore $vCore 上記、PowerShellコードは簡易版のためエラーハンドリングは含まれていません。 他にも「CPU、メモリを 動的管理ビュー で確認し閾値を超えたらスケールアップ」や「時間帯で読み取り専用(セカンダリ)追加によるスケールアウト」なども行うことが可能です。 サンプル(クエリ発行) $query = " SELECT * FROM XXXX " Invoke-Sqlcmd -ServerInstance $serverInstance –Username $userName –Password $password -Database $database -Query $query -ErrorAction Stop -QueryTimeout $queryTimeout PowerShellからSQL Databaseに接続しクエリを実行できます。 CPU、メモリの値を動的管理ビューから取得することも可能です。 参考情報(スケールアウト) ハイパースケール以外でもGeoレプリケーションを利用したスケールアウトができます。 Geoレプリケーションはディザスターリカバリの目的で違う地域にデータを保持できるのですが、読み取り専用(セカンダリ)としても利用できます。 サンプル(Geoレプリケーション設定) $database = Get-AzSqlDatabase -ResourceGroupName $resourceGroupName -ServerName $serverName -DatabaseName $databaseName New-AzSqlServer -ResourceGroupName $resourceGroupName ` -Location $database .Location ` -ServerName $serverName ` -SqlAdministratorCredentials $( New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $user , $( ConvertTo-SecureString -String $pass -AsPlainText -Force)) $database | New-AzSqlDatabaseSecondary ` -PartnerResourceGroupName $resourceGroupName ` -PartnerServerName $serverName ` -AllowConnections $allowConnections ` -SecondaryVCore $vCore ` -SecondaryComputeGeneration $computeGeneration Geoレプリケーションを利用して読み取り専用(セカンダリ)を追加する場合は、ファイヤフォールと仮想ネットワークの設定・脅威の検出・監査設定なども必要に応じて設定します。 サンプル(ファイヤフォール設定) New-AzSqlServerFirewallRule -ResourceGroupName $resourceGroupName ` -ServerName $serverName ` -FirewallRuleName $firewallRuleName ` -StartIpAddress "XXX.XXX.XXX.XXX" ` -EndIpAddress "XXX.XXX.XXX.XXX" サンプル(脅威の検出・監査設定) Set-AzSqlServerThreatDetectionPolicy -ResourceGroupName $resourceGroupName ` -ServerName $serverName ` -NotificationRecipientsEmails "XXXX@XXXX" ` -EmailAdmins $False ` -ExcludedDetectionType "Sql_Injection_Vulnerability" , "SQL_Injection" , "Access_Anomaly" ` -StorageAccountName $storageAccountName Set-AzSqlServerAuditing -State Enabled ` -ResourceGroupName $resourceGroupName ` -ServerName $serverName ` -StorageAccountName $storageAccountName スケール変更注意点 サービスレベルやモデル、容量にもよりますが、スケールアップ/ダウンに数分~数時間掛かります(図②)。どの程度時間が掛かるか検証してから利用してください。 スケールアップ/ダウン切り替えのタイミングで数秒~数十秒の切断が発生します(図③)。リトライ処理などの考慮が必要です。 スケールアップ/ダウン切り替え直後は、キャッシュクリアされているためパフォーマンスが一時的に劣化する可能性があります。 大幅にスケールダウンすると、主要データがメモリにのらずパフォーマンス劣化となる可能性があります。 その他、SQL Databaseでは定期的にreconfigurationがあり一時的に接続できなくなることがあります。この場合もリトライ処理など考慮が必要になります。 メリット・デメリットありますがZOZOTOWNでは開発環境・本番環境にスケールアップ/ダウン(定期的なCPUコア数変更)を入れており、問題なく動作しています。 参考までにGeoレプリケーションを利用したスケールアウトを記載していますが自動化はしておらず、必要に応じて手動設定しています。 Azure AutomationでSQL Databaseをスケールアップ/ダウン 次にAzure Automationを利用して自動化します。 Azure Automationの実態は、PowerShellを定期的(または一時的)に動かすことが出来るジョブです。 SQL Databaseのスケールアップ/ダウンに限らず「Azure上の環境構築を自動化」「定期的なデータ作成」など様々な用途で利用できます。 最初にAzure Automation設定で躓いたところが2点あったのでお伝えます。 モジュールのインストール Azure AutomationからPowerShellを実行したところ必要なモジュールが入っておらず実行できないことがありました。 各リソース毎にモジュールが用意されており、まず必要なモジュールをインストールする必要があります。「SQL Databaseを操作するモジュール」「Application Gatewayを操作するモジュール」と言った具合です。 初回実行でエラーとなった場合、モジュールがインストールされているかを疑います。 また、Azure操作はAzure Resource Manager(以下ARMと略します)というモジュールを使うことが多かったのですが、今後はAzモジュールに切り替わっていきます。 AzモジュールはARMモジュールの代わりになるものです。最新機能はAzモジュールに追加されていきます(AzモジュールがGAされたのは2018年9月頃です)。 世の中の記事を見るとARMモジュールで書かれれていることが多いので、Azコマンドに置き換えての実装することをオススメします。 ARMモジュールは2020年12月まではサポートされるようです。 Azモジュールについては こちら を参照ください。 Azure Automationの実行アカウント Azure Automationアカウントを作成すると自動で、Azure Active Directoryにアプリケーションが登録され、この権限に基づき実行されます。 また、デフォルトではアカウントの有効期限が作成から1年で設定されるため、気を付けないと1年後に実行できなくなります。 実際にあった話ですが、組織のチームメンバー変更により当初設定した人がチームを外れて数か月後、有効期限切れで動かなくなりました。 特に本番環境で定期的にAzure Automationで処理を実行している場合は、障害になりかねないので実行アカウントには注意が必要です。 上記のことに気を付けてPowerShellを用意し、Runbookに登録します。 次にスケジュールを決めて設定完了です。 スケジュールは曜日や時間などで設定できます。 試しにAzure Automationを実行してSQL DatabaseをCPU8コアに変更してみます。 正常終了を確認したので、SQL DatabaseでCPUコア数を確認します。 問題なくCPUコア数が変更されていることを確認できました。 このAzure Automationを定期的に動かすことでスケールアップ/ダウンさせています。 まとめ 今回はAzure Automationを利用してSQL Databaseをスケールアップ/ダウンさせた事例をご紹介しました。結果、開発環境・本番環境で問題なくスケールアップ/ダウンできておりコストを40%削減できました。 コストの詳細は SQL Database 節約術 に記載しています。 ZOZOテクノロジーズでは、このような問題を1つずつ改善しています。 一緒に前向きなチーム作りを盛り上げてくれるエンジニアを大募集中です。 ご興味のある方は、 こちら からぜひご応募ください。
アバター
こんにちは。開発部の廣瀬です。 本記事では、ZOZOTOWNを裏側から支えているバックオフィスシステムにおけるトラブルシューティング事例をご紹介したいと思います。 尚、今回の話題はバックオフィスシステムで使用しているVBScript言語がメインとなります。WebサーバーはIIS、DBMSはSQL Serverを使っている環境です。 ZOZOTOWNのバックオフィスシステムについて ZOZOTOWNのバックオフィスシステム(以後、BOと呼ぶ)では、顧客管理や物流管理などを行っています。 BOの開発はすべて弊社の社員で行っています。ZOZOTOWNを裏側から支えるシステムを開発する責任は大きいですが、その分やりがいを感じることも多いです。例えば、顔見知りの社員がユーザーのため、直接システムへのフィードバックがもらえます。システム改善によって別の部署の業務効率を向上させることができたり、抱えている問題を解決できたときはとても喜んでくれて、それが次の開発へのモチベーションにつながります。 社内の様々な部署でBOが使われていますが、物流拠点「ZOZOBASE」は最もBOユーザーが多い拠点の1つです。 ZOZOBASEではメンバーの大半がBOを使って業務を行っています。そのため、BOのレスポンスタイムは作業効率に大きな影響を及ぼします。 ある時期から、BOのレスポンスタイムが突発的に著しく低下する事象が断続的に発生するようになりました。その問題の調査と解決方法について、順を追ってご紹介します。 BOで発生した問題について ある日、物流部門から「BOが重い」との問い合わせを受けました。 アプリケーションのタイムアウト発生状況を確認すると、確かにタイムアウトエラーが頻発していました。 ただし、通常時と比較して平均のレスポンスタイムがどの程度増加しているのかについては確認する仕組みがありませんでした。その日は1時間ほど経過後、自然解消しましたが、その後1日1回程度の頻度で同様の事象が発生するようになりました。 BOのレスポンスタイムが低下すると、処理効率が低下し、結果的に人件費というコストの増加につながります。 できる限り迅速に問題を解決すべく、調査を実施しました。 原因調査 DBでスロークエリが多発している可能性を疑い、 DBで現在実行中のクエリリスト を取得してみました。 その結果、DBでスロークエリが多発しているわけではありませんでした。このためDB起因でのレスポンス低下ではないと判断しました。 次に、Webサーバー側で処理中の要求一覧を確認しました。 ・[インターネットインフォメーションサービス(IIS)マネージャー]を起動し、[ワーカープロセス]へ ・該当のアプリケーションプール名をダブルクリック ・現在の要求一覧を確認 上記の例では要求が6件ですが、調査実行時は、100件ほどの要求を常時処理している状況でした。通常時と比べて明らかに「経過時間」が遅くなっており、そのため同時実行数が多くなっていました。 この時点で、物流拠点から問い合わせをもらった「BOが重い」という事象について以下のように整理しました。 Webサーバーのレスポンスタイムが通常時より遅延している。 そのためWebサーバーで実行中の要求数が増加している。 ただし、DBでスロークエリが多発しているわけではなく、DB起因の遅延ではない。 したがって、Webサーバー側で何らかの問題が発生して遅延につながっているはず。 さらに調査を続けます。タイムアウトエラーが発生したときのエラーメッセージを確認すると、「[DBNETLIB][ConnectionOpen (Connect()).]SQL Server が存在しないか、アクセスが拒否されました。」となっていました。DBへの接続を確立しようとしてタイムアウトしているようです。 タイムアウトエラーが発生したプログラムの該当行を確認すると、DBに対してクエリを実行する処理でした。SQL Serverへの接続には OLE DB Provider for SQL Server (SQLOLEDB) を使用しています。 SQLOLEDBを介して接続する場合は、デフォルトでコネクションプーリングが有効になっています。ですが、エラーメッセージを確認する限りプールから接続を再利用せずに新規で接続しているようでした。なぜこのような挙動をしているかこの時点では理解できませんでした。 まずは本当に新規で接続しているのか検証するために、該当プログラムの該当関数を開発環境で実行し、その時の接続状況を SQL Server Profiler で確認してみました。 SSMS(SQL Server Management Studio) の[ツール]-[SQL Server プロファイラー]からProfilerを起動します。 [イベントの選択]で[すべての列を表示する]にチェックを入れた後、[Security Audit]-[Audit Login]を選択します。 次に[実行]を押してProfilerを開始し、接続状況を確認したい関数を実行後、Profilerを停止します。 ↑Profilerで取得したイベントです。[EventSubClass]という項目を確認します。 [2-Pooled](コネクションプールから再利用された)の次に、[1-Nonpooled](新規接続が発生)という順番でAudit Loginイベントが発生していました。 推測していた通り、該当関数では新規接続が発生していることが確認できました。 今までの調査から、コネクションプールにプールされたコネクションをうまく再利用できておらず、新規接続が大量に発生している可能性があると推測しました。コネクションまわりのトラブルのため、Webサーバーのポートの状態を確認してみました。 PowerShellで以下のコマンドを実行します。 Get-NetTCPConnection | GROUP-Object State その結果、利用可能な動的ポート数の大半がstate=TIME_WAITとなっていました。監視製品のグラフで確認しても、物流拠点の稼働中は大量のTIME_WAITなポートが確認できました。 (緑色:TIME_WAIT / 赤色:ESTABLISHED / 黒色:Total) ポートのstateがTIME_WAITになると、そのポートが再利用可能な状態となるまでに一定時間(デフォルト値は4分)経過する必要があります。そのため、使用可能な動的ポート数の大半がstate=TIME_WAITとなってしまうと、使用可能なTCPポートが枯渇し、新規接続時にエラーとなってしまう可能性があります。 したがって、今回の不具合の原因としては「何らかの理由で使用可能な動的ポートの大半がTIME_WAITとなり、新規接続に時間がかかることによるレスポンス遅延や、タイムアウトエラーの多発につながった」可能性が高いと判断しました。 次に、使用可能な動的ポートの大半がTIME_WAITとなっている原因を調査しました。 マイクロソフトのドキュメント によると、コードの書き方によっては、暗黙の接続が発生するようです。また、暗黙の接続はプールされないとの記述もありました。プールされないということは、接続を閉じるとstate=TIME_WAITになります。コードの書き方が今回の事象の根本的な原因のようです。 暗黙の接続が発生するコードの例としては以下のようなパターンが考えられます。レコードセットを開いた状態で、同一コネクションオブジェクトで二つ目のクエリを実行しようとすると、既にコネクションが使われている状態のため、暗黙の接続が発生してしまいます。 Dim ConnectionObject Dim RecordSet Set ConnectionObject= CreateObject("ADODB.Connection") ConnectionObject.open "接続文字列" Set RecordSet= ConnectionObject.Execute("select * from SomeTable") '//初回実行 Set RecordSet= ConnectionObject.Execute("select * from SomeTable") '//二回目の実行(暗黙の接続発生) ここまでの調査内容をまとめると、不具合の原因は「暗黙の接続が発生するコードの書き方になっている関数が短期間に大量に実行され、結果として使用可能な動的ポートの大半がTIME_WAITとなり、新規接続に時間がかかることによるレスポンス遅延や、タイムアウトエラーの多発につながった」と結論づけました。 対応内容 調査結果を受けて、2つの方針で対応を実施しました。1つはワークアラウンドな対応で、もう1つは根本的な対応です。それぞれご紹介します。 1. ワークアラウンドな対応 ワークアラウンドな対応として、動的TCPポート数の増加とTCPポートの解放時間の短縮という2つの対応を実施しました。 1-1. 動的TCPポート数の増加 こちら を参考に、動的TCPポートの数を増加させました。これにより、TIME_WAITなポートが多発する状況下でも現状より多くのポートを使用できる状態にしました。 手順:コマンドプロンプトで以下を実行(管理者として実行が必須) ・現在の数を確認し、表示された数値をメモ(万一何かあったときに戻すため) netsh interface ipv4 show dynamicportrange protocol=tcp ・設定を変更する(ipv4の場合) netsh int ipv4 set dynamicport tcp start=動的ポートの開始番号 num=動的ポート数 ・変更が反映されたことを確認 netsh interface ipv4 show dynamicportrange protocol=tcp 1-2. TCPポートの解放時間を短縮 TcpTimedWaitDelay の値を最小の30秒に設定しました。これにより、TIME_WAITになったポートが再利用可能になるまでの時間を規定の4分から30秒へと短縮しました。 手順 「ファイル名を指定して実行」で「regedit」と入力し、レジストリエディタを立ち上げる HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 配下まで移動 右クリック→「新規」→「DWORD(32ビット)値」を選択 名前「TcpTimedWaitDelay」、値「30」を入力 再起動 この対応の結果、以下のようにTIME_WAITのポート数の減少が確認できました。 (左:設定前の24時間 / 右:設定後の24時間) 2. 根本的な対応 タイムアウトが多発していたプログラムについては暗黙の接続が発生しないようコードを修正しました。修正前後の該当関数の処理速度の平均値を計測したところ、約半分にまで実行時間を短縮させることができました。 コードの修正については、以下のようなパターンで修正を実施しましたのでご紹介します。暗黙の接続が発生しないように、1つのコネクションオブジェクトにつき、一度に実行されているクエリが必ず1つだけになるように意識しました。 A. コネクションオブジェクトを使いまわしている場合 Before Dim ConnectionObject Dim RecordSet Set ConnectionObject = CreateObject("ADODB.Connection") ConnectionObject.open "接続文字列" Set RecordSet = ConnectionObject.Execute("select * from SomeTable") '//初回実行 Set RecordSet = ConnectionObject.Execute("select * from SomeTable") '//二回目の実行(暗黙の接続発生) After Dim ConnectionObject Dim RecordSet Set ConnectionObject = CreateObject("ADODB.Connection") ConnectionObject.open "接続文字列" Set RecordSet = ConnectionObject.Execute("select * from SomeTable") '//初回実行 RecordSet.Close() '//一度レコードセットを破棄すれば、同一コネクションを再利用できる Set RecordSet = Nothing Set RecordSet = ConnectionObject.Execute("select * from SomeTable") '//二回目の実行(暗黙の接続発生無し) B. oRS.open(xxx, ConnectionObject, xxx)を利用している場合 Before set oRS = Server.CreateObject("ADODB.Recordset") oRS.Open sSQL, ConnectionObject, 1, 1 After set oRS = Server.CreateObject("ADODB.Recordset") oRS.Open sSQL, "接続文字列", 1, 1 '//コネクションオブジェクトではなく、接続文字列を渡すことで文脈によらずコネクションの使いまわしが発生しない C. CommandObject.Execute()を利用している場合 Before Set CommandObject = CreateObject("ADODB.Command") CommandObject.ActiveConnection = ConnectionObject CommandObject.CommandText = "Stored Procedure Name" CommandObject.CommandType = 4 CommandObject.Execute After Set CommandObject = CreateObject("ADODB.Command") CommandObject.ActiveConnection = "接続文字列" '//コネクションオブジェクトではなく、接続文字列を渡すことで文脈によらずコネクションの使いまわしが発生しない CommandObject.CommandText = "Stored Procedure Name" CommandObject.CommandType = 4 CommandObject.Execute 今後の課題 弊社ではプロダクトによっては外形監視のための製品が導入されていますが、BOでは未導入で、レスポンスタイムについてメンバーが監視や改善をしやすい環境ではありません。今回の問題も物流拠点からの問い合わせで問題に気づくのではなく、レスポンスタイムが増加しているアラートを受け取って気づくのが理想的だったと思います。そのため、BOにも外形監視の仕組みを導入し、みんなでレスポンスタイムについても責任を持てる状況を整備していきたいと考えています。 まとめ 今回はVBScriptというレガシーな技術を使って動いているシステムにおけるトラブルシューティングの事例をご紹介しました。 約15年前に誕生し、凄まじい勢いで成長してきたZOZOTOWNは、VBScriptなど当時のレガシーな技術を用いた開発も行っています。 一方で、アーキテクチャにDocker/Kubernetesを中心としたクラスタ構成を採用したZOZOTOWNのシステムリプレースプロジェクトも進めています。 このように弊社では最新の技術からレガシーな技術まで、さまざまな技術を使ってエンジニアが日々開発に取り組んでいますが、プロダクトをより良いものにしたいという想いはみんな一緒です。 ZOZOテクノロジーズでは、目の前の課題を解決していくことにモチベーションを持って取り組めるエンジニアを絶賛募集中です。 興味のある方はぜひ こちら からご応募ください。
アバター