TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

組織拡大に伴うスケールアウトするTV会議需要をCisco Webexで構築したはなし こんにちは、ZOZOテクノロジーズ コーポレートエンジニアリング 伊藤琢巳です。 社内のTV会議システムをWebexにて構築しているのですが、昨今のリモートワークなどTV会議需要を検討されている方々に1つでも有益な情報を提供できたら幸いです。 課題 株式会社ZOZOは千葉市の海浜幕張に拠点を構えており、幕張周辺に住む社員が多く通勤時間も短く、開発チームはオフィスで直接コミュニケーションを取れる事が強味です。そして、株式会社ZOZOテクノロジーズの設立により拠点が幕張オフィスと青山オフィスに分かれ開発メンバーも分散しました。 青山オフィス竣工時には既に拠点間会議を行う準備としてTV会議機器を導入した会議室が整備されていました。幕張オフィスにはCisco TelePresence TX9000、青山オフィスにはCisco TelePresence SX20を導入しました。 その後、福岡にも福岡研究所として新しい拠点が稼働しはじめ、以下の課題に直面しました。そのため、スケールアウト可能な遠隔コミュニケーション構築が急務でした。 PCで行うTV会議ではカメラ・マイクの性能不足のため、機微なコミュニケーションが伝わりにくい機器の品質課題 デスクトップPCの比率が高く会議時にPCを会議に持ち込めないメンバーも存在していた チーム数が多く、会議室が不足して利用できないという場所の課題 TV会議システムを導入している会議室と導入していない会議室の2種類あるが、会議室の空き状況的にTV会議が不要な会議でもシステム導入済みの会議室を利用せざるを得ず、TV会議システムの稼働率が低かった 開発現場が利用しているTV会議システムの機器と全社で導入しているTV会議システムの機器の相互接続性の課題 利用しているプロトコルの違いから接続不可の状態だった 福岡研究所をはじめ日帰りで往復移動できない距離の拠点の増加によるTV会議増加の課題 拠点間でのコミュニケーションが増加し、TV会議需要が増加した 解決過程 以下では、上記の課題を踏まえ、どのように解決に向けて進めていったのかを説明します。 課題の解決法検討 先述の課題に関して、それぞれの解決方法を検討していきます。 品質の課題解決 それまでは、PC会議としてSkype for BusinessやZoomを利用していました。どころがデスクトップPCの利用者比率の高さや、ノートPCの利用者でも内蔵カメラ・マイクの品質では力不足です。 これらの課題を解決するためにCiscoの導入を検討しました。Ciscoでは参加人数に合わせた製品がそれぞれ用意されていたため、会議室スペースのサイズや用途に合わせて選択が可能でした。 場所の課題解決 各拠点に設置するCisco TelePresence端末(以後、TV会議機器)を適切な台数追加しました。現在、65台のTV会議機器が稼働中です。 相互接続性の課題解決 相互接続性の課題解決に向けて、大きく2つの方向性を検討しました。 まず、社内で利用が浸透し始めていたZoomでの構築を検討しました。これには社内外接続の際にバーチャルルームコネクタ構築が必要です。また、拠点の増加を踏まえた運用やスケールアウト面で断念しました。 次に、ポリコムやソニーなどその他のTV会議システムの検討をしました。先行導入していた高価なTV会議機器を活かす必要性がありました。相互互換性のため業界標準のH.323/SIP接続は必須で検討しました。 TV会議増加による管理の課題解決 拠点増によりTV会議を構築する初期設定から障害調査などの運用の際の考慮はが必要です。また、拠点増により1つの会議への同時接続数が増えるため、多地点接続オプションはも必須。そのため、端末を増やすにあたり検討した内容は以下の点があげられます。 TV会議機器は多地点接続を行うには上位グレードの高価な機器が必要となる 多地点オプション導入しても同時接続数は数接続程度 TV会議機器の管理には専用のサーバ群とネットワーク構築の維持管理が必要となる 社内外との相互接続を行うためのExpressway設置や接続ライセンス管理が必要 広帯域かつ低遅延ネットワークの構築と維持が必要 Webexの選択 検討の結果、Webexを選択しました。Webexにてどのように課題解決したのか説明します。 管理コストの削減 多地点対応検討した時の図 オンプレミス クラウド 上で示す図のように、オンプレミス環境で構築した場合TV会議機器のみではなく、相互接続するためのサーバ群の構築が必要となります。 そこへクラウド環境を導入することにより、TV会議機器はクラウドへ直接接続になり、ネットワークへ接続してアクティベーションコードの入力だけで機器の接続が完了できるようになります。 クラウド環境への移行でサーバ保守が不要により、サービス提供に注力できます。 外部ともTV会議可能な環境整備 登録された端末は外部公開されるSIPアドレス割当により、内線的な組織内接続のみでなく外部との接続も行うことが可能。グループ各社Webexを導入することでお互いの会議室SIPを登録しておくことで会議室を直接呼び出し可能。グループで契約統合した際にも各社Webexを採用していたため移行はとてもスムーズに完了。 クラウド会議室はTV会議機器のみではなくPC・モバイルアプリ、ブラウザからのアクセスも可能なため専用機器を持っていない外部のゲストの方とも会議可能です。 Zoomとの相互接続 H.323/SIPルームコネクタオプションを利用してWebexと相互接続性を維持。 Google hangout meetとの相互接続 メディア変換サービスであるPexip構築の試算。Google hangout meetとの相互での同時接続を増やすとミスマッチになり断念。 PC会議として利用にとどめ、品質対策として数名で会議を行いたい場合にWebカメラの貸し出しや会議マイクスピーカの貸し出しを実施。また、Cisco Room Kit MiniをUSB接続することでPC会議でも品質を高めるこ方法を準備しています。 どこでもTV会議可能な環境整備 クラウド会議の開催はどのTV会議端末、アプリ、ブラウザからでも主催をすることが可能。オンプレミスでは必要だった高価な多地点オプションが不要。 用途に適した機器の選定 狭い会議室、広い会議室、オープンスペースと用途に合わせた機器選定を行う場合には、多地点オプションのために上位機種を選択は不要になり機器選択の自由度向上。そのため多くの会議室へ設置が可能となり、会議室利用の均等化を促進。 導入効果 接続拠点 ※3カ国/11拠点/65台(2020/05現在) 国内 海外 幕張・青山間であっても日に2往復は現実的でない距離。福岡や宮崎に至ると往復は現実的でなく、日程調整は高コストになりました。 会議室間の高品位なCiscoの相互接続は拠点間の移動頻度を削減。 他拠点間での会議参加があっても、他メンバーとのコミュニケーションが可能です。 これは実際の移動による交通費だけでなく移動時間の削減による無理のない業務が可能。別拠点からでもコミュニケーションコストが下がることで心理的な安全性を確保。 設置したTV会議機器の紹介 DX80 Cisco Webex DX80を可搬式スタンド化しています。主にオフィス内で発生する必要なときに短時間で行うミーティングを拠点間でも実現させる目的で設置しており、予約制にはせずミーティングスペースへ数台設置しています。 可搬式スタンド化することで誰でも容易に移動が可能です。 狭いスペースでも利用が可能なため好評であり、会議室に設置済DX80もすべてこの仕様へ変更しました。 Room Kit Mini 参加メンバーが増えてくると特に問題になるのはマイクの性能不足です。前述のDX80は個人デスクで利用するサブモニタ的な扱いの製品でしたが、こちらのRoom Kit Miniはカメラ・マイク・スピーカをハドル利用できるように強化しています。 こちらも自立式の構成なため、容易に設置場所の変更可能です。 さらに、PCへUSB接続すると会議時にカメラ・マイク・スピーカーとして利用可能です。そのため、Google Meetなどのその他のPC会議システムでの利用もできます。 Room 55 主に参加メンバーが10人を超える会議での利用を想定した機器です。各拠点に可搬できるスタンドタイプのRoom 55を設定しています。 しかし、高価なため多くの台数の設定は現実的でないため、DX80やRoom Kit Miniで補完しています。 円会議室(Webex Codec Pro + SpeakerTrack 60 + Ceiling Microphone) 青山オフィスには広い円会議室が設定されています。 通常よりも広く、円形の会議室のためカスタマイズしてTV会議システムを構築しました。 カバー範囲の広いSpeakerTracker 60をスタンド化して設置、Ceiling Microphoneを2台設置して全体で良好な集音を実現させています。 推奨ソフトウェアの指定と利用方法の提示 社内ではWebex Teams利用を推奨。メンバーを事前にチーム招待、トピック毎にスペースを作成する事で場をつくり、ハドル的コミュニケーションに優れセキュリティと利便性のバランスを取ることが可能。 トピックに合わせたスペースを作成できるので、用途ごとのクラウド会議室が必要なタイミングで時間や場所にとらわれなく利用できます。チャットやホワイトボード、必要な資料スペースも完備いています。 外部のゲストを一時的にゲストとして招待し、ブラウザアクセスで利用可能です。メンバー以外の接続は受付処理が必要なため意図しない乱入を防止できます。 オフィスにいる場合、近くにあるCiscoのTV会議機器での資料共有が超音波を利用して容易に接続可能です。超音波で認証が完了するため、資料を共有する際のコネクタ不一致問題やケーブル長さ不足から解消されます。また、来客者など異なるネットワークを利用している場合でもクラウド経由で資料共有が可能になります。 Webex Eventsによる大規模会議やイベントの実施 Webex MeetingやWebex Eventsを利用。発言者・登壇者を固定、他メンバーのミュートや表情も確認し易いグリッド表示はじめ大規模開催に特化。 全グループ社員参加の社内イベント配信インフラとしてWebex Eventsを利用。YouTube Liveに比べると即時性に重点をおいているため動画品質は劣るが、低遅延による双方向性の確保。またURL認証でなくユーザ認証なので社外秘の情報でも安心して配信可能。 その他の導入時に行った施策 青山・幕張拠点を常時接続 会議室を予約して時間をそろえて会議、フリースペースで遠隔地と会議、と順に環境を整備してきました。その次のフェーズとして拠点間を常時接続を行い、他拠点のオフィスの様子が常に分かるようにしています。 場と場をつないでおくことで開発チーム内のコミュニケーション効率が向上し、お互いが別拠点にいても意識しあえる環境を構築しています。 要望のあるチームから順に導入しえおり、今後も徐々に追加予定です。 Azure ADとのSSO・プロビジョニング対応 社外秘情報でもやり取りできる環境を構築したことでコミュニケーションのコスト削減。次は大規模になってきたアカウンティング。プロビジョニングにてユーザアカウント作成の自動化、ログインのSSO化を整備。 社員がそれぞれ持っている会社のアカウントだけで、いつでもコミュニケーション可能となり、退職すると自動アカウント停止となるため運用コストが低減できます。 ログイン認証をAzure ADで一元管理をしているため、不自然なログイン検知の自動化もメリットになります。 海外拠点への導入 海外拠点があるため、そこへの導入も行いました。 機器の購入に関しては、Cisco TV会議機器を購入・保守契約は各国リセラーと契約しました。当初グループ会社ごとに機器の購入とクラウド契約を行ってSIPアドレスを共有していましたが、 現在ではクラウド契約を一本化し、機器も再アクティベーションで再利用しています。 また、弊社には中国の拠点もありますが、中国の場合はグレートファイアウォールに注意が必要です。北京接続用(暗号オプション無効)では接続できないので、現在はシンガポール接続用(暗号化オプション有効)を利用しています。事前にWebex各サーバとのネットワーク接続経路テストをするサイト Cisco Webex Network Test で確認をしましょう。 Googleカレンダーとの連携 社内で利用しているGoogleカレンダーのカレンダーリソースとの連携を強化しています。 時間になるとWebex会議への参加ボタンが自動表示されるのでワンタップ参加。次の予約時間前になると次の予約が表示されるため会議室利用の効率化。 どこでもコミュニケーションへの変化 それはZOZOテクノロジーズ全社員利用の開始直後に発生しました。 令和元年9月直撃した台風15号。大型台風の接近際して試験的開始していたリモートワーク対策済PCを持ち帰るアナウンス。近距離での通勤が多い幕張拠点ですがそれでも通勤は混乱。チームいつでもTV会議を行える環境、リモートワーク対応もすすめていたため無理して出社せず自宅からの業務が可能でした。 その後、グループ再編や東京オリンピックに向けてグループでの導入を進めていたところ、新型コロナウィルス感染症による全社規模のリモートワークが発生しました。拠点間をつなぐ目的で始めた環境整備でしたが、社員間をつなぐ役割へと変化してきました。 オフィス閉鎖やリモートワーク化により、利用形態が変化してきても柔軟に対応でき、全社導入した効果を実感しています。 同時に、接続元や業務形態の変化はコミュニケーションの仕方にも変化を与えていると感じます。 タスクについては以前よりJiraでのチケット管理をしていましたが、業務上近くにいるメンバーとの声かけはオフィスではいつでも出来ていたので、チームの進捗は週次で確認していました。しかし、リモートワークになってからはチーム内対策ポータルを作成し、朝礼でチーム内の作業を日次で確認するよう変更しています。 「この後ちょっといいですか?」や「誰が出席したら良いかな?」という話題を朝礼の時に投げかけ、チーム内の共有を促進しています。 また、ヘッドセットの準備を行い相手に聞きやすい環境を意識しています。ハウリングなどで聞き取りにくい、聞き取れないということが続くとストレスの原因になります。自分自身では気がつきにくい問題なので、相互に指摘するようアナウンスしています。 私も部屋に風鈴を下げていたのですが、「何か音がする」と言われはじめ、TV会議の向こう側へも風鈴の音は流れているという事に気がつきました。今では毎朝仕事を開始する前に風鈴を外して、仕事が終わったら風鈴を戻すのが習慣となっています。 最後に コーポレートエンジニアリング部では各サービスで得手不得手も違うので、相互補間を考慮してWebex以外のサービスも整備しています。 どこかのサービスが落ちた場合、不得手な環境であっても業務を継続出来る環境構築を目指しています。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは!最近気になるニュースはスピノサウルスの尻尾の化石が発見されたこと 1 な、SRE部エンジニアの塩崎です。ZOZOテクノロジーズの前身となった会社の1つであるVASILYでは数多くのクローラーの開発・運用の担当をしてきました。 今回はその知見を生かして、クローラーを楽に運用するためのクラウドサービスを紹介します。 概要 データ解析を円滑に進めるためには、CSVやWeb APIなどの構造化されたデータが必要です。しかし全てのWebサイトにあるデータが構造化データを提供しているとは限りません。むしろ提供していないケースの方がはるかに多いです。そのため、Webクローラーを作成して構造化されていないWebページを解析し、構造化データを生成する必要があります。 しかし、Webクローラーの運用には数多くの「つらみ」があります。特に大量のWebページを1日1回などの頻度で定期的にクロールする際には「つらみ」が多くなります。少しでもこの「つらみ」を減らし、運用を楽にするためのクラウドサービスを紹介します。 クローラーの運用の「つらみ」とは クローラー運用でどのような「つらみ」があるのかを説明します。なお、ここで想定しているクローラーはクロール対象のサイト数が数十以上、ページ数が数百万以上、クロール頻度が毎日というかなり大規模なものです。 インフラの運用 EC2やGCEなどを使いクローラーを動作させるためのインフラを自分たちで管理するのは大変手間がかかります。大規模クローラーを運用する場合は1台のサーバーだけで処理が完結することはありません。多くの場合では非同期ジョブキューを活用した分散システムが必要になります。以前、このような仕組みを作った際には、Mesos + Marathonを使ったインフラを数人月の開発工数で構築しました。頑張って仕組みを作ってみたものの、運用をするために分散システムの知見が必要になり、運用者に求められるレベルが上がるという問題も同時にありました。 techblog.zozo.com サイトの構造が変わることへの対応 クローラーで最も運用負荷がかかるのは、サイト構造の変化への対応です。今日ではWebサイトのリリースは毎週・毎日行うことが当たり前のようになっています。そのため、あるタイミングで作成した要素をパースするための設定がある日突然使えなくなってしまう可能性があります。 Ajaxを使っているサイトの対応 サーバーからダウンロードしたHTML中に欲しい要素がない場合は、要素をパースするのが非常に面倒になります。Vue.jsやReactなどのSPAフレームワークで作られたページについては言うまでもないですが、意外と一部の項目のみがAjaxで取得されているケースも多いです。例えばECサイトの在庫状況のような頻繁に変更されうる情報をユーザーに提供するため、その部分のみをAjaxで取得することがあります。そのようなサイトをクロールするためにはChrome DevToolsなどを使いAjax通信を解析して、同等なAPI呼び出しを行う必要があります。 BANとの戦い 一部のWebサイトはWebクローラーからのアクセスを自動的に遮断するための対策を実装しています。多くの場合はIPアドレスを使い、一定の期間で閾値以上の回数のアクセスがあったらそれを遮断するという処理が行われています。もちろんサイトの運営に支障が出るレベルのアクセスをしたり、利用規約で禁止されているようなクロール行為をするのは論外です。ですが、常識的な 2 アクセス頻度であったとしてもブロックされるケースがあります。そのようなサイトをクロールするためにはIPアドレスを複数個用意し、適切にローテーションを行う必要があります。 選定のポイント 上記で出た「つらみ」を元にクローラー運用を楽にするためのポイントを考えていきます。 インフラの抽象化 大規模クローラーは分散システムになりがちですが、分散システムの運用はモノリシックなシステムの運用に比べて難易度が上がります。そのようなシステムの運用に慣れた人員を集めることはとても困難です。そのため、この部分を「いい感じに」隠蔽し、アプリケーションエンジニアがアプリケーションレイヤーのみに集中できるようなPaaS、SaaSが必要です。 パース結果をGUIで確認する機能 XPathやCSSセレクターなどを使ったパース結果をブラウザなどの画面でグラフィカルに確認したいです。クロール対象サイトのHTML構造が変わった後には速やかにそれに追従する必要があります。そのため、ブラウザ画面をクリックしてパースする要素を指定できる必要があります。わざわざHTMLコードを調べるのに比べ、グラフィカルに指定できる方が効率的です。 JavaScriptの実行をしてくれるヘッドレスブラウザ Ajax通信の解析は骨の折れる作業です。個人的な感覚では、HTML中に直に埋め込まれている要素を取得するのと比較して、数十倍の手間がかかります。ですので、JavaScriptの実行を行ってくれるヘッドレスブラウザがあり、onloadなどのイベントに紐づく処理がある程度実行されたあとのHTMLを取得したいです。 IPアドレスのローテーション機能 BAN対策のために複数個のIPアドレスを使い回し、必要に応じてそれらを使い回しできるような機能が欲しいです。 ProxyMesh や Tor のようにこの機能を単体で提供するSaaSやOSSもありますが、クローラー本体を動かすためのクラウドサービスに統合されていると使い勝手が良いです。 ベンダーロックインしてない 古くからあるクローラー用のクラウドサービスとして kimono は有名でしたが、2016年にPlantirによって買収されたタイミングでサービスの提供が打ち切られました。このような事象が発生した場合、長期的な戦略としては別のクラウドサービスへのマイグレーションが必要です。ですが短期的な視点に立つと、完全なマイグレーションが完了するまでの間のつなぎとして、自分たちのインフラでクローラーを動かす必要が出てくるかもしれません。 techcrunch.com また、スモールスタートで始めたプロジェクトが大きく成長し、十分な運用体制が取れるようになることもあります。そのときに、ベンダーロックインをしていなければ、自分たちのインフラに載せ替えることによってコストメリットを得るというオプションを考えることができます。 このことから、ベンダーロックインをしていない、OSS製品をベースにしたクラウドサービスを利用することにはメリットがあります。しかし、発生頻度などを考慮すると、他の項目と比べた優先度は低いです。 要件を満たしそうなクラウドサービスの検討 Scrapinghub scrapinghub.com 最初に紹介するのは Scrapinghub です。Python製のWebクローラーとして有名なScrapyの開発元が運営しているPaaSです。自社が主導的に開発を行い、OSSとしても公開している Scrapy の実行基盤を提供しています。ScrapyそのものはPythonで書かれたOSSであり、自身のPCやEC2上に構築したLinuxサーバーの上で動かすこともできます。 Webページをパースするための処理はすべてPythonコードで実装します。XPathやCSSセレクターを使って要素を取得したあとに、Pythonコードで文字列処理を実装可能です。Pythonが持っているエコシステムをフル活用できる柔軟性が魅力的です。また、requirements.txt形式でPaaS環境にライブラリをインストール可能であり、それに加えて独自のDockerイメージの利用も可能です。そのため、 MeCab を使った形態素解析などのネイティブライブラリが必要な処理も実装できます。 一方、パース結果をGUIで確認する機能は搭載されていません。同じ開発元がビジュアルスクレイピングツールである Portia というOSSを作っていましたが、他のクラウドサービスに比べるとGUIツールとしての機能が不足しているように感じます。ブラウザベースで抽出したい要素を選択すると、Scrapyを使ったクローラーのソースコードをアウトプットする機能があります。しかし、出力できるクローラーの自由度が高くないため、あくまで補助的なツールとしての利用に限定され、メイン処理はPythonコードで書く必要があります。また、ここ1年くらいコミットが一切ないので、メンテナンスされていない疑惑があります。そのため、パース結果をGUIで確認する機能はScrapyのエコシステムの外のツールに頼る必要があります。以前に以下のブログで紹介した、 XPath Helper というGoogle Chromeの拡張機能を使うことで、目的の要素を抽出するためのXPathを高速に見つけることができます。 techblog.zozo.com また、JavaScriptの実行をするためのヘッドレスブラウザの機能も搭載されています。ただ単にページロード時のレンダリングをするだけでなく、Lua言語で書かれた関数を渡すことによって、フォームのクリックイベントなどを処理できます。クローラー用に軽量のヘッドレスブラウザを独自開発しているという気合の入れ方が凄いです。このモジュールも Splash というOSSとして公開されており、自前のインフラでホスティングすることも可能です。 IPアドレスのローテーション機能もついています。Scrapyとの統合も簡単で、ソースコードに数行の設定を追加するだけでIPアドレスのローテーションを行えます。接続元IPのロケーションを選ぶことができるので、海外IPからの大量アクセスによるアクセス遮断を防げます。 このように、基本的な機能は全てOSSとして公開されているので、これらの機能をEC2などのIaaS上で動作させることも可能です。自前のサーバーでホスティングするためには Scrapyd というツールを使いデーモン化します。しかし、ScrapydのWebコンソールは機能が不足しており、また複数台のサーバーをまとめ上げる機能もありません。そのため、Scrapydだけではクローラークラスターの運用には不十分です。クラスターの運用をするためには、サードパーティツールである、 ScrapydWeb を組み合わせる必要があります。このツールは複数台のサーバーにインストールされているScrapydの状態を一元して監視したり、ジョブをスケジュール実行する機能があります。 ParseHub parsehub.com 次に紹介するのは ParseHub です。先程紹介したScrapinghubとは違い、プログラミングレスでクローラーを作ることができるSaaSです。ParseHubのクライアントツールを立ち上げると、ブラウザのような画面が立ち上がり、その画面でグラフィカルに要素を選択できます。クリックで要素を選択できるだけでなく、XPathやCSSセレクターを使った要素の選択ができる柔軟性も併せ持っています。要素を選択した後は、正規表現やJavaScriptで変換処理を行うことができます。さらに、変数・条件分岐・ループ処理・サブルーチンなどの基本的なプログラミング言語としての機能も有しています。 ヘッドレスブラウザのサポートはScrapinghubよりも充実しています。フォームをクリックしてその結果を取得するという操作などもグラフィカルに行うことができます。Scrapinghubではこのような処理を実現するためにLuaの関数を書く必要があり、手間になりがちです。 IPローテーションの機能もあり、BAN対策をすることもできます。ただし、接続元のIPアドレスのロケーションを指定する機能はないので、そのようなことをしたい場合は Crawlera や ProxyMesh などの外部サービスを使う必要があります。 これらのツールはOSSとして公開されていないので、がっつりとベンダーロックインされる点には注意が必要です。 なお、ParseHubに似たビジュアルスクレイピングサービスとして Octoparse や Import.io があります。ビジュアルスクレイピングツールを求めている場合はこれらの検討もしてみると良いかもしれません。 Apify apify.com 最後に紹介するのは Apify です。これはJavaScriptを使ってクローラーを作成できるPaaSです。ヘッドレスブラウザとしてGoogle ChromeとPuppeteerを使っています。 Apify SDK というOSSが公開されており、クローラーをローカル環境で動かすことも、Apifyが提供しているPaaSで動かすこともできます。 クローラーのマーケットプレイス の存在がApifyの面白い点です。ZOZOTOWNや楽天のような日本のWebサービスのクローラーはありませんが、GoogleやInstagramなどのグローバル展開しているサービスのクローラは数多くあります。クローラーのソースコードがGitHubで公開されているので、これらをforkして自分たちのニーズに合わせてカスタマイズすることもできます。 比較 ここで、今回紹介した3つのクラウドサービスが適しているケースをまとめます。 Scrapinghub Scrapinghubはプログラマー向けのサービスです。以下のケースではScrapinghubを使うのがいいでしょう。 PythonやJavaScriptなどの汎用プログラミング言語を日常的に使っている クローラーをコードベースで管理したい GitHubを用いたコードレビューやCI/CDパイプラインの仕組みを使いたい 機械学習や自然言語処理などのPythonが得意とする処理を組み込んだクローラーを作りたい 将来的に大規模化することが予想されており、自前のインフラで動かすための人員を確保できる予定がある ParseHub ParseHubはノンプログラマーでも扱えるような支援機能が豊富です。以下のケースでParseHubを使うとその良さが発揮されるでしょう。 ノンプログラマーでクローラーの開発・運用をする必要がある ページ内で複数回のクリックをしないと取得できない要素が多い ベンダーロックインをいとわない代わりに開発スピードを上げたい Apify 上記の2つと比較してApifyを積極的に選ぶ場面は多くありません。 あえて挙げるならば、以下のケースではApifyに優位性があります。 Apifyのマーケットプレイスにあるクローラーが自分たちの要件を満たしている クロール対象のサイトがGoogle Chrome固有の機能に強く依存している まとめ 我々のチームでは以上の検討をした結果、Scrapinghubを利用することにしました。クローラーの開発運用を行うメンバー全員がプログラマーであり、コードベースでの管理をする恩恵が大きいと判断したためです。GUIを使ったパース項目の確認機能が不十分ですが、XPath Helperを活用することによって不十分な点を埋め合わせることができます。 このようにZOZOテクノロジーズでは一度作ったシステムをそのままにせず、時代に合わせた最適なアーキテクチャを模索する営みを継続的に行っています。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com 古生物学:スピノサウルスは泳ぎが上手だった ↩ どの程度までなら常識的なのかは議論の対象になりがちですが、一例を挙げるなら 岡崎市立中央図書館事件 の判例で1秒間に1アクセス程度は常識的とみなされました。 ↩
アバター
こんにちは! ZOZOTOWN部の遠藤です。 iOS 13がリリースされて半年が経ちましたね。iOS 13といえばダークモード機能が注目を浴びましたが、それ以外にもたくさんの 新しい機能 が追加されました。 本記事では新しく追加されたフルページのスクリーンショットについて書いていきます。 はじめに ZOZOテクノロジーズではiOS技術のキャッチアップのために定期的に社内勉強会を行っています。その勉強会で話題に上がったフルページのスクリーンショットに興味を持ち、ZOZOTOWNではどのように使用できるかの調査をしてみました。 今回は調査して分かった内容をもとにフルページのスクリーンショットの機能と対応方法について紹介します。 少しでもフルページのスクリーンショット機能を実装する際のお役に立てれば幸いです。 フルページのスクリーンショットとは? フルページのスクリーンショットはiOS 13で追加されました。 フルページのスクリーンショット機能については、 iOS 13で利用できる新機能 の「システム体験」->「フルページマークアップ」に記載されています。 ウェブページ、iWorkの書類、Eメール、地図の全体をとらえたスクリーンショットを撮り、注釈を加えられます。 今まで、スクリーンショットは画面に写っている箇所しか撮れませんでしたが、iOS 13から画面外の要素もスクリーンショットで撮ることができるようになりました。 iOS 13のSafariでスクリーンショットを撮ると、このようにスクリーンショットのプレビューで「フルページ」というタブにページ全体のスクリーンショットが表示されます。 かつ、通常のスクリーンショットと同じくトリミングや文字を書くこともできます。 フルページのスクリーンショットに 対応するには? フルページのスクリーンショットについてはWWDC19の Introducing PencilKit で最後の5分ほど触れられています。 ユーザーがスクリーンショットを撮ると、iOS 13で追加された UIScreenshotService のdelegateが呼ばれます。 呼ばれたdelegateメソッドの返り値にPDFデータを渡すことで、フルページのスクリーンショットが表示されます。 スクリーンショットがPDFであることのメリット PDFは複数ページ持つことが可能です。iOS 13のフルページのスクリーンショットは単一ページのスクリーンショットも複数ページのスクリーンショットも対応しています。 単一ページ例: Safari 複数ページ例: Keynote フルページのスクリーンショット機能で提供するスクリーンショット Introducing PencilKit のセッションでフルページのスクリーンショットを採用している事例としてマップアプリが紹介されています。 マップアプリの通常のスクリーンショットではセミモーダルが表示されていますが、フルページのスクリーンショットでは非表示になっており、マップの情報をより多く見ることができます。 通常のスクリーンショット フルページのスクリーンショット Human Interface Guidelines にはスクリーンショットは画面に写っている内容を変えてはいけないと書かれていますが、フルページのスクリーンショットは必ずしもそうとは限らないようです。 どのようなスクリーンショットがフルページのスクリーンショットとして表示するのが推奨されているかは、Human Interface GuidelinesやUIScreenshotServiceのドキュメントなどに記載されていません。(2020/04/01時点) フルページのスクリーンショットに対応する この画面にフルページのスクリーンショットを対応します。 ここでのフルページのスクリーンショットの定義は、コンテンツが収まるほど長い仮想の端末で見たときの状態のスクリーンショットです。 フルページのスクリーンショットを撮る手法はたくさんあるかと思いますが、今回は2つの手法を試してみました。 手法1: windowの高さを変更して描画する windowの高さをコンテンツが収まるように更新してそのwindowを描画することで求めているスクリーンショットを撮ることができます。 import UIKit class ViewController : UIViewController , UIScreenshotServiceDelegate { override func viewDidAppear (_ animated : Bool ) { super .viewDidAppear(animated) view.window?.windowScene?.screenshotService?.delegate = self } override func viewWillDisappear (_ animated : Bool ) { view.window?.windowScene?.screenshotService?.delegate = nil } func screenshotService (_ screenshotService : UIScreenshotService , generatePDFRepresentationWithCompletion completionHandler : @escaping (Data?, Int, CGRect) -> Void ) { let contentHeight : CGFloat // コンテンツが収まる高さを計算する let renderer = UIGraphicsPDFRenderer(bounds : . init (origin : .zero, size : . init (width : view.frame.width , height : contentHeight ))) let data = renderer.pdfData { context in context.beginPage() let originalHeight = view.frame.height view.window?.frame.size.height = contentHeight view.window?.layer.render( in : context.cgContext ) view.window?.frame.size.height = originalHeight } UIGraphicsEndPDFContext() completionHandler(data as Data, 0 , .zero) } } 順番に説明します。 (1) screenshotServiceのdelegate設定 screenshotServiceのdelegateにselfを設定します。 override func viewDidAppear (_ animated : Bool ) { super .viewDidAppear(animated) view.window?.windowScene?.screenshotService?.delegate = self } (2) PDF作成の準備 UIGraphicsPDFRenderer を使用してPDFを作成します。 UIGraphicsPDFRendererの初期化でPDFを描画する領域のサイズを決めます。 Safariでは幅375ポイントのデバイスでフルページのスクリーンショットを撮ると、幅375ポイントのPDFが生成されました。今回は同じようにデバイス幅をそのまま使います。 func screenshotService (_ screenshotService : UIScreenshotService , generatePDFRepresentationWithCompletion completionHandler : @escaping (Data?, Int, CGRect) -> Void ) { let contentHeight : CGFloat // コンテンツが収まる高さを計算する let renderer = UIGraphicsPDFRenderer(bounds : . init (origin : .zero, size : . init (width : view.frame.width , height : contentHeight ))) let data = renderer.pdfData { context in context.beginPage() // viewの描画 } } (3) PDF作成 pdfData(actions:) のメソッドを使用し、actions内で描画するとPDFデータが取得できます。 描画をする際の注意点ですが、NavigationBarとTabBarも描画したい場合はviewのwindowを描画します。viewを描画してもviewの階層構造にないNavigationBarとTabBarは描画されないからです。 今回はNavigationBarとTabBarも描画したいので、viewではなくwindowを描画することにしました。 また、描画メソッドはCALayerの render(in:) を使用します。描画メソッドには drawHierarchy もありますが、windowの高さを高くしすぎると描画されないことがあるためです。 let data = renderer.pdfData { context in context.beginPage() let originalHeight = view.frame.height view.window?.frame.size.height = contentHeight view.window?.layer.render( in : context.cgContext ) view.window?.frame.size.height = originalHeight } UIGraphicsEndPDFContext() (4) completionHandlerにPDFを渡す 最後に作成したPDFを渡して完了です。 completionHandler はPDFの他に2つのパラメータがあります。この2つのパラメータは、スクリーンショットのプレビューで表示位置を指定するものです。 単一ページの場合はrectInCurrentPageを指定でき、複数ページの場合はindexOfCurrentPageのパラメータが指定できます。 rectInCurrentPageにCGRectZero、indexOfCurrentPagに0を指定するとPDFの一番上から表示されますが、ユーザーが表示していた位置を指定するのが良いでしょう。 completionHandler(data as Data, 0 , .zero) これでフルページのスクリーンショットを撮ることができます。 目的のスクリーンショットを得ることはできましたが、windowの高さを変更することは、レイアウト崩れや一度に全てのcellがロードされることで発生する負荷などの副作用が不安になります。 手法2: 1画面ずつスクロールして描画する windowの高さを変えずに、コンテンツを1画面ずつスクロールしながら描画します。 スクロールのたびにwindowを描画すると、NavigationBarとTabBarが繰り返し描画されてしまいます。 これを回避するために、スクロール時はスクロールのコンテンツのみを描画します。 先程紹介した(3)の描画処理を変更します。 ファーストビューはwindowを描画し、その後1画面ずつスクロールしてscrollViewを描画します。ここでの注意点はTabBarの描画です。 windowを描画することで、TabBarも描画され、スクロールコンテンツの途中にTabBarが表示されてしまいます。 なので、windowを描画する前にTabBarを非表示にし、スクロールコンテンツが全て描画されたあとにTabBarのみを描画します。 let data = renderer.pdfData { context in context.beginPage() let originalContentOffset = scrollView.contentOffset // 全てのコンテンツが表示されるのに必要なスクロール回数を計算する let numberOfScrolls = ceil(contentHeight / scrollView.frame.size.height) ( 0 ..< Int(numberOfScrolls)).forEach { if $0 == 0 { scrollView.contentOffset.y = 0 tabBar.isHidden = true view.window?.drawHierarchy( in : view.frame , afterScreenUpdates : true ) } else { let y = scrollView.frame.size.height * CGFloat( $0 ) scrollView.contentOffset.y = y scrollView.drawHierarchy( in : . init (origin : . init (x : 0 , y : y ), size : scrollView.frame.size ), afterScreenUpdates : true ) } } tabBar.isHidden = false // コンテンツの下に描画されるようにy座標を計算する var drawTabBarFrame = tabBar.frame drawTabBarFrame.origin.y = contentHeight - tabBar.frame.height tabBar.drawHierarchy( in : drawTabBarFrame , afterScreenUpdates : true ) scrollView.contentOffset = originalContentOffset } UIGraphicsEndPDFContext() これでwindowの高さを変更せずともフルページのスクリーンショットを撮ることができました。 しかし、この手法はwindowを伸ばす手法と異なるレイアウトの箇所が出てきます。 コンテンツが収まる高さで描画した スクリーンショット 1画面ずつスクロールして描画した スクリーンショット 本来右下のFloating Action Buttonの位置はTabBarのすぐ上にあるのですが、1画面ずつスクロールして描画したスクリーンショットではファーストビューと同じ位置で表示されています。ファーストビューでwindowを描画した際にTabBarは非表示にしましたが、Floating Action Buttonは非表示にしていないためです。 スクロールコンテンツの最後に表示する要素についてはTabBar同様にスクロールの最後に描画するなどの工夫が各画面で必要になると思われます。 2つの手法のメリット・デメリット 2つのフルページのスクリーンショットの手法を比較してみました。 メリット デメリット windowの高さを変更する手法 実装が簡単 windowの高さを変更することでの副作用 1画面ずつスクロールする手法 windowの高さを変更する手法と比べ副作用は低い 実装量が多い 実装量が多くなってしまいますが、1画面ずつ描画する手法の方が安全性は高いです。windowの高さを変える手法もリスクが許容できる画面であれば有効だと思います。 まとめ 今回はフルページのスクリーンショット機能と対応方法についての紹介でした。 フルページのスクリーンショットは、UIScreenshotServiceDelegateを設定しPDFを渡すことで対応できます。今回は長い端末で見たときの状態を目指しましたが、提供したい内容によっては複数ページや重要なところだけを抜き出したスクリーンショットを作ることも可能です。 この記事がフルページのスクリーンショットを実装の一助になれば幸いです。 ZOZOテクノロジーズでは、iOSエンジニアを募集しています。興味のある方はこちらからご応募ください! www.wantedly.com
アバター
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、現在開催中の 技術書典 応援祭 にて、有志で制作した技術同人誌【 ZOZO TECH BOOK VOL.1 】の頒布を開始しました。 本来であれば、 技術書典8 にゴールドスポンサーとして参加予定でしたが、 新型コロナウイルス感染症の影響により中止 になってしまいました。今回の 技術書典 応援祭は、その代わりとなるオンラインマーケット として実施されています。 ZOZO TECH BOOK VOL.1 頒布開始🎉 技術書典 応援祭にて、ZOZOテクノロジーズの有志によって制作された技術同人誌【ZOZO TECH BOOK VOL.1】の頒布を開始しました!詳細はリンクから💁‍♀️ #技術書典 #zozotech https://t.co/IVxfN9b0qv — 株式会社ZOZOテクノロジーズ (@zozotech) March 12, 2020 ZOZO TECH BOOK VOL.1 こちら のページにて電子版を頒布しています。 techbookfest.org 目次 第1章 ZOZOテクノロジーズの2019年の振り返りと現状(今村 雅幸 / @kyuns & 池田 健人 / @ikenyal) 第2章 WebXRの現状確認 2020 Spring(諸星 一行 / @ikkou) 第3章 速習GitHub Actions 〜 明日からの充実GitHub自動化ライフのための凝縮ポイント 〜(川崎 庸市 / @yokawasa) 第4章 Miro SDK入門(堀江 亮介 / @Horie1024) 第5章 iOSアプリのクラッシュレポート、もう少し詳しく!(元 政燮) 第6章 GoのCLIツールで服作りの業務効率化(手塚 ⻯太 / @tzone99) 第7章 はじめての本番デプロイ(光野 達朗 / @kotatsu360) サンプル 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは。開発部基幹SREチームの廣瀬です。 弊社のサービスではDBMSとしてMicrosoft社のSQL Serverを使用している箇所があります。本記事では、2020年1月1日からスタートしたZOZOTOWN冬セールにおける負荷対策の一環で実施した、SQL ServerのCPUチューニングについてご紹介します。内容としては主に「どうやってプロダクション環境においてCPUボトルネックなクエリを見つけ出すか」についてです。 そのため、SQL Server以外のDBMSをお使いの方にも、「SQL Serverではこんな情報がとれるのか。MySQLだったら〇〇でとれる情報だな」というように比較しながら読んでいただけると嬉しいです。 チューニング実施に至った経緯 初めに、なぜCPUベースのチューニングを実施したかについてお話します。一言でいうと「ZOZOTOWNの冬セールで一部DBのCPUが負荷に耐えきれない試算となったから」です。ZOZOTOWNでは定期的に売上を多く記録するイベントが開催されています。その中でも毎年1月1日からスタートする冬セールは、1年間で最も売り上げが多いイベントの1つです。したがって、DBサーバーの負荷も高くなる傾向にあります。そのようなイベントを数カ月後に控えた昨年の10月ごろに開催されたイベントで、とあるDBのCPU負荷が高騰しました。このイベントは乗り切ったのですが、冬セールの方がより多くのトラフィックを見込んでいました。そのため、このままでは確実にCPUボトルネックによるスロークエリが多発すると判断し、チューニングを実施することにしました。なお、チューニング対象のSQL Serverはオンプレミスのため、スケールアップという手段は考えないこととします。 SQL Serverにおける情報収集の方法 SQL Serverのチューニングにおいて使用することが多い情報収集の方法を4種類説明します。 動的管理ビュー(DMV) サーバーの状態情報が格納されたVIEWのことを指し、沢山種類があります。リアルタイムで更新される情報のため、チューニングだけでなく、即応性が求められるトラブルシューティングでも活躍します。例えば、以下のDMVを使ったクエリを実行すると キャッシュされたクエリの総実行時間 総CPU時間 実行回数 実行プラン などを確認できます。 SELECT TOP 100 qt.text ,total_worker_time ,total_elapsed_time ,qs.execution_count ,qp.query_plan FROM sys.dm_exec_query_stats qs OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt OUTER APPLY sys.dm_exec_query_plan(plan_handle) AS qp 他にも、 インデックスの容量、使用回数(seek/scan/lookupごと)、最終アクセス日、断片化などの情報 現在実行中のクエリの抽出(獲得しているメモリサイズ、実行時間、待ち事象など) 現在のロック取得状況 など、豊富な種類の情報を取得することが可能です。各DMVのデータ保持期間は、「現在の状態のスナップショット」か「サーバー起動後の累積値」のいずれかに大別されます。 DMVを使った情報収集用のクエリを、 私のgithub でいくつか公開しています。 また、Microsoft MVPの 小澤さんのgithub で豊富なDMVクエリが公開されており、おすすめです。 拡張イベント 様々なイベントをキャッチできるトレースツール、といったイメージです。ドキュメントには「拡張イベントは、SQL Server に関する問題の監視とトラブルシューティングを行うために必要なデータを収集できるようにする、軽量なパフォーマンス監視システムです。」とあります。SQL Server 2008 R2までの主流だった SQL Server Profiler や SQL トレース が現在では非推奨となり、代わりに拡張イベントの使用が推奨されています。拡張イベントの方が、情報収集時の負荷がより軽量になったとされています。 設定はクエリベースでも、以下のようにGUIベースでも可能です。例として、下図では「実行完了に1秒以上かかったクエリ」をキャッチする設定を実施しています。 試しに5秒間waitするコマンドを実行した後にデータを表示すると、以下のようにイベントがキャッチできていることが分かります。 キャッチしたイベントは上図のようにGUIベースでも確認できますが、キャッチしたイベント量が多くなると確認や集計が難しくなります。その場合は、テーブルにエクスポートすることも可能なので、SQLを使って好きなように絞り込みや集計を行うことも可能です。 クエリストア クエリの実行統計を自動で保存してくれます。クエリの実行時間やCPU時間、実行プランの変化などが時系列に格納され、特定の時間帯の状態を後から追うことができます。CPUベースのチューニングなどを行う際にかなり強力な機能となります。また、実行プランの変化によりパフォーマンスが劣化したことを把握する目的では最も便利な機能です。 以下のようにGUIも用意されていますが、クエリベースで解析していくことも可能です。 システムモニター Windowsに標準で提供されている、メトリクス収集機能です。Windowsサーバー観点でのメトリクス(CPU使用率など)と、SQL Server観点でのメトリクス(秒間のバッチ実行数など)を取得できます。パフォーマンスモニターと呼ばれることも多い印象です。 ここまでで紹介した情報の取得方法は「取得タイミングや、特定の時間帯においてどのような状態であったか」というようなスナップショット的な情報を取得できるものが多いです。一方で、システムモニターについては「収集間隔で取得された時系列の情報」を取得することができます。「クエリの実行数が何時何分にスパイクしたか」というような状況を簡単に取得することができますので、時系列の状態の変化を取得したい場合にはシステムモニターが適しているケースがあります。 以下のようにGUIで収集するメトリクスを選択できます。 収集間隔は最短で1秒です。そのため、 Datadog や Zabbix といった監視製品よりも短い間隔で情報を収集したいときにも重宝します。私の肌感では、今回紹介した4つの情報取得機能の中では最も軽量です。この機能を有効化していることでの負荷増が気になったことはありません。 今回の調査で何がチャレンジングなのか 理想的には、全てのクエリが使用したCPU時間を収集できれば、確実にCPUボトルネックなクエリを洗い出すことができます。この目的に最も適している機能は、クエリストアです。ただし、今回の調査ではクエリストアは使用しませんでした。 理由としては、クエリストアを有効化したことで、トランザクションログの書き込み量が常に約2倍に増加してしまったためです。クエリストアはSQL Server2016から提供開始した機能で、現在ではSQL Serverにおける定番の情報収集ツールという位置づけになっています。しかしながら、今回のサーバーでは物理リソースへの負荷の増加が許容できる範囲を超えてしまったため、クエリストアを停止した状態で調査を実施しました。(クエリストアに関連した設定により改善される可能性もあるため、今後はクエリストアの使用を再開する予定です。) したがって、動的管理ビュー、拡張イベント、システムモニターを使ってCPUボトルネックなクエリを調査する必要があります。拡張イベントのフィルターの設定によっては、全クエリのCPU時間をキャッチすることは可能ですが、プロダクション環境での負荷増につながるため現実的ではありません。種々の情報を組み合わせながらボトルネックなクエリをどうやって探していくかがチャレンジングな部分になります。 システムモニターを使った調査 Batch Resp Statistics オブジェクト を使うことで、サーバーで実行されているクエリのCPU負荷の分布を確認することができます。 例えば、[CPU使用時間が10ms以上20ms未満のクエリの総実行時間]などです。Batch Resp Statistics(CPU Time:Total(ms))を積み上げ面グラフにしたものと、そのときのCPU負荷(Processor(_Total)\% Processor Time)をグラフにしたものを並べると同じような波形を示します。例として、ある日のCPU高負荷となった時間帯に収集した値をグラフ化したものを以下に示します。 この面グラフを確認することで、CPU負荷増に最も寄与しているのはCPU時間50ms-100msのバッチ(濃い青色)だと判断できます。ミリ秒単位の、単体ではなんら問題の無いクエリが大量に実行されたことでCPU負荷につながっているケースでした。数秒から数分単位でCPU時間を使用しているような、単体で高負荷なクエリがボトルネックとなるケースもあるため、CPU負荷をかけているクエリの性質をざっくり把握するのには非常に便利です。具体的なクエリまではこの方法では分からないのですが、拡張イベントで「CPU使用時間が50ms-100msのクエリ」をキャッチすることでクエリを特定することができます。拡張イベントでは全てのクエリ完了イベントをキャッチすることは負荷増につながる懸念があるため、Batch Resp Statisticsを使って「CPU使用時間をどの範囲でフィルタするか」を決定するという方法は有用です。 DMVを使った調査 ドキュメント には、DMVの一種であるsys.dm_exec_query_statsを使った、CPUボトルネックなクエリを特定するクエリサンプルとして、以下のSQLが紹介されています。 出典: こちら sys.dm_exec_query_statsには、以下の性質があります。キャッシュしているクエリがリコンパイルされると plan_generation_numがカウントアップ creation_timeがコンパイル時間にアップデート execution_count / total_worker_time / total_elapsed_time などが0にリセットされる (正確には、古いplan_generation_numの値をもったレコードが削除され、新しいplan_generationi_numの値をもったレコードがINSERTされるような気もしますが。) また、ドキュメントには以下の記載があります。 つまり、プランがキャッシュから削除されると、対応する行もこのビューから削除されます。 上記性質を踏まえると、MSのドキュメントに記載されているクエリは、「クエリがリコンパイルされることなく、かつ一切キャッシュアウトしない」という前提のもとで正確な値がとれる、ということになります。しかし、多くのクエリが実行されているプロダクション環境においては、クエリの再コンパイルが頻繁に起き、キャッシュアウトされることも想定されます。また、ドキュメントで紹介されているクエリは「平均のCPU使用時間が高いクエリ」を抽出することはできますが、サーバーにCPU負荷をかけているクエリを特定する、という目的であれば、「CPU負荷が高い時間帯における、総CPU使用時間が高いクエリ」を特定したいところです。こうした背景を踏まえて、DMVを使ってできる限り正確にCPU高負荷なクエリを見つける方法を考えてみました。 プロダクション環境でのリコンパイル発生状況を確認する sys.dm_exec_query_statsを使ってリコンパイル発生状況を確認してみます。 select * ,SUBSTRING(qt.TEXT, qs.statement_start_offset / 2 , ( CASE WHEN qs.statement_end_offset = - 1 THEN LEN( CONVERT (NVARCHAR( MAX ), qt.TEXT)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset ) / 2 ) as statement from sys.dm_exec_query_stats qs OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt -- クエリテキスト用 OUTER apply sys.dm_exec_query_plan(plan_handle) as qp -- プランプラン用 WHERE creation_time > dateadd(SECOND, -60 , getdate()) AND execution_count >= 10 このクエリは、「過去60秒以内にリコンパイルされ、リコンパイル後の実行回数が10回以上」なクエリを抽出します。このクエリを実行してレコードがとれる場合は、「本当はCPUを沢山使っているけど、頻繁にリコンパイルされてtotal_worker_timeがリセットされているためにtotal_worker_timeが小さな値になっている」ような状況が発生してる可能性があります。 アイデア 特定の2点におけるsys.dm_exec_query_statsの情報を全てダンプして保存した場合、各レコードはクエリAまたはクエリBのいずれかに該当します。 クエリA:creation_time <= Time① クエリB:creation_time > Time① ※last_execution_time = Time② であると仮定します。プロダクション環境で常に実行され続けているようなクエリばかりの場合はこの仮定で問題ありません。 このとき、Time①とTime②の間の時間帯におけるCPU使用時間が高いクエリを見つけるために、クエリAとクエリBにおいて以下の計算を実施します。 クエリA:Time②のtotal_worker_time - Time①のtotal_worker_time クエリB:Time②のtotal_worker_time * (Time② - Time①) / (Time②-creation_time) これにより、コンパイル時間(=total_worker_timeの算出開始時間)が異なるクエリであっても、同一の時間間隔における使用CPU時間を推定し、その値が大きい順に並び替えることでCPUボトルネックなクエリを推定できると考えました。 作成したクエリと、クエリを使った解析手順 このクエリ を実行してまずダンプ用のテーブルを作成します。 このクエリ を(1分に1回など)定期的に実行します。最低2回取得できればOKです。 このクエリ の、@snapshot_time_earlierと@snapshot_time_laterの2か所に、実在するcollect_dateの値を入れて実行します。 下図のように、CPU使用時間の寄与率が高い順に結果が表示されるので、これらのクエリがチューニングできないかを検討すればOKです。 チューニングについて チューニング対象を特定できたら、あとは実際にクエリチューニングを実施し、同程度のトラフィックに対してCPU負荷がどの程度減少するかを評価します。本記事ではCPU観点でのボトルネッククエリの調査手法をメインに扱いますので、具体的なクエリチューニングの方法については触れません。 結果 下図は、チューニング実施前後における、同程度のバッチ実行数に対するCPU使用率の推移のグラフです。 クエリチューニングを実施したことで、ピーク時間帯におけるCPU負荷を50%以上削減できました。本対応の結果、2020年1月の冬セールで該当DBの負荷がボトルネックとなることは一切ありませんでした。このDBで実行されているクエリは1000種類以上あり、その中でチューニングしたのは10種類ほどでしたので、一部のクエリがCPU負荷へ大きく寄与していた結果となりました。 まとめ 本記事では、CPU使用率を下げる目的でチューニングを実施する際の調査手法について紹介しました。プロダクション環境でCPU過負荷となっていたサーバーに対して実際に調査を実施し、結果としてピーク時のCPU負荷を50%以上削減しました。今回の調査では、DMVを使ってボトルネックなクエリを特定し、チューニング前後の評価にはシステムモニターを使用しました。 調査を実施しているときはアプリケーションについての知識は必要ありません。ただし、実際にクエリをチューニングしていく際は、アプリケーションの開発者と協力してチューニングを進めていく方が良い結果が得られやすいということを体感しました。例えば、開発者であれば重いクエリの一部のJOINが現在では不要なので削除できる、といった判断を下すことができます。DBの調査を得意とする人と、アプリケーションの仕様を熟知した人とが一緒になってチューニングを実施するとより良い結果が得られると思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは! 開発部の @ahiru_starrr です。 本稿では、ZOZOTOWN iOSにSnapshotTestを導入したのでその経緯や導入方法、導入するメリット・デメリット、どんな場面で役に立つのかなどについて書いていきます。 SnapshotTestがどのようなものかよく分からない方や導入を検討している方々のお役に立てれば幸いです。 SnapshotTestとは SnapshotTest導入の背景 2つの課題 エンジニア ↔︎ デザイナー間のコミュニケーションコスト レガシーからモダンへの取り組み 導入方法 環境変数を設定 実装方法 テストクラスを作成 recordModeを設定 テストコードを実装 フォルダ名を設定 ファイル名を設定 stubの設定 FBSnapshotVerifyView SnapshotTestのユースケース リファレンス画像を生成 SnapshotTest!!! SnapshotTest導入のメリット・デメリット まとめ SnapshotTestとは 構成済みのUIViewまたはCALayerからスナップショットを生成し「正しい状態との比較・差分」を検出するためのテストです。 開発を進める中で、「意図せずにデザインやレイアウトが崩れてしまう」ようなケースはしばしば起こるかと思いますがそれを防ぐためのテストとして大変効果的です。 SnapshotTest導入の背景 きっかけとなったのは、iOSDC Japan 2019の スナップショットテスト実戦投入 / Practical Snapshot Testing のセッションです。 このセッションで実際に導入した話を聞いたことで、ZOZOTOWNのプロジェクトに導入するメリットが鮮明になりました。 現状のZOZOTOWN iOSの開発には2つの課題がありSnapshotTestを導入することでそれらの解決・緩和が見込めると考えたのです。 2つの課題 エンジニア ↔︎ デザイナー間のコミュニケーションコスト レイアウトに関連する変更を加えた場合、ZOZOTOWNでは例え小さな変更であったとしても必ずデザイナーにデザイン確認を行なうフローがあります。 いわゆるデザインのリグレッションテストです。 ZOZOTOWNはデザインに強いこだわりのあるサービスのため、このフローは欠かすことができませんでした。 しかしながら、より効率的でスピーディーな開発を目指していく上でこのフローが無駄なコストであることはいうまでもありません。少なくとも「開発の前後でデザインに差分がないことを確認する」だけであればもっと機械的にできるはずです。 またZOZOTOWNは主に千葉と東京、福岡の3拠点で開発が行われています。 そのためデザイナーとエンジニアのコミュニケーションはSlackやテレビ会議で行うことが多いのもコミュニケーションコストの増加に拍車をかけていました。 レガシーからモダンへの取り組み ZOZOTOWNは長い歴史のあるサービスです。 iOSアプリは2010年にリリースされており、iOSアプリだけでみても約10年の歴史があります。 2010年というと、iPhone 4が発売された時期ですね。 長い年月とともに開発環境や開発言語は大きな進化を遂げましたが、一方でプロジェクトのソースコード量は膨大し、一部の取り残されたObjective-Cは大きな態度で居座り続けています。 この半年〜1年でチームメンバーは大きく変わりSwift化をはじめとするコードのモダン化への取り組みが活発に行われるようになりました。 リファクタリングも日常的に行われているため、その際のデザインのリグレッションテストをもっと機械的に効率よく行いたいと模索をしているところでした。 ソースコードは大きく変更したいものの、それに伴う画面のUIは変更したくない状況がZOZOTOWNでは頻繁にありました。 上記2つの課題を解決するためにSnapshotTestを導入しました。 導入方法 ZOZOTOWNでは「 iOSSnapshotTestCase 」×「 OHHTTPStubs 」の組み合わせで導入をしています。 iOSSnapshotTestCaseはUberがFacebookから引き継ぎ今も継続的にメンテナンスが行われているOSSです。 併せてスタブ用のレスポンスを返すためにOHHTTPStubsも導入しました。 ほとんどの画面はAPIからのレスポンスによって動的に構成されるため、SnapshotTestとの相性が非常に良いです。staticなjsonファイルをアプリに持たせておくことで常に同じ表示条件にてSnapshotTestができるのも大きなメリットだと思います。 上記2つのOSSはCocoaPodsにて導入しましたが、予めSnapshotTestを行うためのターゲットを作成しておきそのターゲットのみに導入をしました。 target 'SnapshotTests' do inherit ! : search_paths pod 'iOSSnapshotTestCase' pod 'OHHTTPStubs / Swift' end 環境変数を設定 OSSを導入したら環境変数の設定を行います。 これによりSnapshotTestを行った後の差分画像とリファレンス画像の保存先が設定されます。 XcodeのEdit SchemeからRunの項目を選択して、Environment Variablesに2つの環境変数を設定します。 IMAGE_DIFF_DIR $(SOURCE_ROOT)/$(PROJECT_NAME)SnapshotTests/FailureDiffs FB_REFERENCE_IMAGE_DIR $(SOURCE_ROOT)/$(PROJECT_NAME)SnapshotTests/ReferenceImages FailureDiffsディレクトリにはSnapshotTestを行った後の差分の画像が保存され、ReferenceImages_64ディレクトリにはリファレンス画像が保存されるようになります。 実装方法 下記はUIViewControllerのSnapshotTestの実装例です。 import FBSnapshotTestCase import OHHTTPStubs @testable import ZOZOTOWN class CartStockViewControllerTest : FBSnapshotTestCase { override func setUp () { super .setUp() folderName = "CartStockViewController" fileNameOptions = [.device, .OS, .screenSize, .screenScale] recordMode = false } func testCartSnapshotTest () { stub(condition : isPath ( "/stocklist.json" )) { _ in let stubPath = OHPathForFile( "stocklist_test.json" , type(of : self )) return fixture(filePath : stubPath ! , headers : ["Content-Type": "application/json"] ) } let cartStockViewController = CartStockViewController() cartStockViewController.view.layoutIfNeeded() let exp = expectation(description : "Screen Loaded" ) DispatchQueue.main.asyncAfter(deadline : .now() + 1.0 ) { self .FBSnapshotVerifyView(cartModalStockViewController.view) exp.fulfill() } wait( for : [exp] , timeout : 10.0 ) } } 順番に説明していきます。 テストクラスを作成 はじめにテスト用のファイルを作成し、上記で導入した2つのOSSをimportしてFBSnapshotTestCaseを継承したクラスを作成します。 import FBSnapshotTestCase import OHHTTPStubs @testable import ZOZOTOWN class CartStockViewControllerTest : FBSnapshotTestCase { } SnapshotTestではすでに実装済みの画面に対してテストを行う場合がほとんどのため @testable importを記述します。これにより、プロダクトコードが記述されているターゲット内のinternalで宣言されたソースコードにアクセス可能となります。 recordModeを設定 SnapshotTestを行う際はrecordModeの設定が必要となります。 recordMode 説明 true リファレンス画像を生成 false SnapshotTestを行う リファレンス画像すなわち正しいUIの状態のスナップショットを生成するときはrecordModeをtrueにセットしてSnapshotTestを実行します。 コードのリファクタリングなどを行った後でデザインのリグレッションテストを行う際にはrecordModeにfalseを設定してSnapshotTestを実行します。 テストコードを実装 フォルダ名を設定 folderName = "フォルダ名" folderNameに適当なフォルダ名をセットします。 これを設定するとReferenceImages_64/フォルダ名/以下にリファレンス画像が出力されるようになります。 ファイル名を設定 fileNameOptions = [.device, .OS, .screenSize, .screenScale] fileNameOptionsにてファイル名を設定します。 上記のような設定をした場合は「ファンクション_識別子_デバイス名_OSバージョン_スクリーンサイズ_倍率」のような画像名になります。 stubの設定 APIモックを用意する主な目的は最初の方でも述べましたが、APIのレスポンスによってテスト結果が変わるのを防ぐためです。 APIのレスポンスから動的に構成される画面のSnapshotTestを行う場合、リファレンス画像生成時のAPIのレスポンスとSnapshotTest実行時のAPIのレスポンスは同一である必要があります。 これを実現するために、OHHTTPStubsを導入しています。 stub(condition : isPath ( "/stocklist.json" )) { _ in let stubPath = OHPathForFile( "stocklist_test.json" , type(of : self )) return fixture(filePath : stubPath ! , headers : ["Content-Type": "application/json"] ) } isPathの引数には、画面の表示に必要な情報を取得しているAPIのエンドポイントを指定します。 OHPathForFileの第一引数には予め用意していおいたテスト用のjsonファイル名を指定します。 FBSnapshotVerifyView let cartStockViewController = CartStockViewController() FBSnapshotVerifyView(cartStockViewController.view) SnapshotTest対象のViewController(View)を生成し、FBSnapshotVerifyViewに渡してあげます。 CartStockViewControllerの内部ではViewのライフサイクルイベントをトリガーにAPIの通信処理がはしり、そのレスポンスを受け取って画面が組み立てられる実装になっています。 APIのレスポンス取得からレイアウト作成までは少し時間がかかるためexpectationを使って非同期処理を行なっています。 ZOZOTOWNでは極力既存の実装を変更しないことを優先してテストコードを書きましたが、この部分の実装に関しては 予めモックとなるモデルを用意しておきViewControllerの初期化時に渡す方法 ViewControllerの初期化後にテストFunc内からAPIをコールし画面のレイアウトを行う方法 など様々な実装が考えられると思います。 各プロダクトに応じて最善の実装をしていただければと思います。 SnapshotTestのユースケース ZOZOTOWNでのユースケースを例にSnapshotTestをしてみます。 テスト対象の画面はZOZOTOWNのカート画面です。 コミットログを見る限り、最後に変更が加えられたのはもう何年も前でObjective-Cで記述されている画面です。 今回はSwiftで書き直し、さらにViewの構成も変更しました。 リファレンス画像を生成 まずはリファレンス画像を生成します。いわば、カート画面の模範(正しい状態)となる画像です。 recordModeをtrueに設定してテストを実行することで、ReferenceImages_64/以下にリファレンス画像が生成されます。 SnapshotTest!!! リファレンス画像を生成後、カート画面のリファクタリングを行なったブランチにてSnapshotTestを実行します。 recordModeをfalseに設定してテストを実行し、リファクタリングの前後でUIに差分がなければテストは成功し、差分がある場合にはテスト失敗のアラートが表示されます。 実際にテストを実行したところ下記の画像がFailureDiffs/ディレクトリに出力されました。 リファクタの前後の画像を並べて比較すると差分がないように見えるのですが、実際には一部フォントサイズが違っていたりレイアウトが多少ずれていたりしました。 リファクタ前 リファクタ後 SnapshotTestにより、これら差分の検出がとても簡単に行えます。 諸々修正して再度SnapshotTestを行うと成功しました。 今までの開発では、リファクタリングが終わったタイミングでデザイナーにデザインの確認を依頼してエンジニアが再度修正、修正したら再度デザイナーに依頼....というフローがありましたが、SnapshotTestを導入したことによりこのフローが不要となりました。 デザイナー抜きでは実現できなかったタスクがエンジニアのみで完結できるようになったのです。 もちろんテストコードを書く必要はあるためその分のコストはかかってはしまいますが、それを差し引いても圧倒的に開発速度は向上します。 SnapshotTest導入のメリット・デメリット ここまでSnapshotTestのメリットを挙げてきましたがデメリットもあると思います。 当然ながら、デザインに意図的な変更を加えた場合SnapshotTestは失敗となってしまいます。 その時は再度SnapshotTestでリファレンス画像を生成し直したり、あるいはstubを修正するなどの必要が出てくる場合もあり運用していく上でのコストがかかってきます。 デザインのほんの一部を修正したいだけなのにSnapshotTestのテストの方も修正する必要が出てきて全体としての開発速度が落ちてしまっては本末転倒です。 注意点としては、プロダクトに導入することで本当に恩恵を受けることができるのかよく考える必要はあるかなと思いました。 メリット 導入コストが低い デザインのリグレッションテストをエンジニアのみで行える 開発速度の向上・開発の効率化が期待できる 意図しないデザイン・レイアウト崩れを自動で簡単に検出できる デメリット 運用コスト まとめ SnapshotTestは、その性質を理解し上手に使うことで高い費用対効果を得ることができます。 実際にSnapshotTestを導入してみて、「効率的なチーム開発」「開発速度の向上」を実現するための選択肢の1つとして大きな力を発揮すると感じました。 今回は局所的にSnapshotTestを利用するユースケースをご紹介しましたが、CI/CD環境にSnapshotTestのWorkflowを組むことでデザインのリグレッションテストの自動化も可能です。 この記事がSnapshotTest実装の一助になれば幸いです。 ZOZOテクノロジーズでは、iOSエンジニアを募集しています。 興味のある方はこちらからご応募ください! www.wantedly.com
アバター
こんにちは、ARやVRといったXR領域を推進しているInnovation Initiativeの @ikkou です。2020年1月7日から10日の4日間にかけてラスベガスで開催されたCES 2020に全日程で参加してきました。 CES期間中は現地からTwitterでリアルタイムにレポートしていましたが、本記事ではXR領域のトピックスを中心に、特に興味深かったものをお伝えします。 CESとは 展示会場について 溢れかえるAR Smart Glassesと勢いの弱まるVR HMD Nreal LightとポストNreal Light 突然現れたパナソニック社の眼鏡型VRグラス デルタ航空が示したパラレル リアリティ ディスプレイ バーチャルヒューマン“Neon”の衝撃 High-Tech Retailing Beauty Tech なぜCESに参加するのか まとめ おわりに CESとは The Venetianにある看板を前にして記念撮影する人が多い CESはCTA(Consumer Technology Association)が主催する、毎年1月にラスベガスで開催される世界最大級と言える「テクノロジーのショーケース」です。 日本語圏では「せす」と呼ぶ方もいますが、適切な読み方は「しーいーえす」です。 CES - The Most Influential Tech Event in the World かつては「家電見本市」と称されることが多かったCESですが、昨今のCESは自動運転車をはじめとして「家電」の領域を超えたものが数多く展示されています。主催であるCTAも「家電見本市ではない」と公式に謳っています。 Sands Expoのエスカレーターを上がった先にあるCESロゴ 年始にこの先数年のテクノロジーを俯瞰する場としてこれ以上に相応しいものはなく、日本からも多くのテクノロジー系メディアの方が参加しています。会期終了から少し時間が経ち、多くのテクノロジー系メディアでCES関連の記事は出揃った頃合いなので、既に関連記事を読んだ方も多いのではないでしょうか。 CESは主に「展示」と「講演」の2軸で進行しますが、今回は展示のみ参加しました。自身のCES参加は2018年、2019年に続く3回目となり、CES参加歴で言えばまだまだひよっこながら、過去回との差分を俯瞰できるようになってきました。 展示会場について CES 2020初日、開場直前のLVCC South Hall 1 Sands Expoにあるスタートアップ企業が多く集まるEureka Park CESの展示はTech East・Tech West・Tech Southという3つのエリアに大別されます。 特にLAS VEGAS CONVENTION CENTER(LVCC)から成るTech EastとSands Expoを中心とするTech Westにブースが多く集まっています。さらにTech Eastの中心となるLVCCはNorth・Central・Southに大別され、端から端に歩くだけでも一定の時間を要します。 https://www.ces.tech/Show-Floor/Official-Show-Locations.aspx 会期中の4日間という限られた時間の中で、すべてのブースを細かく見るのは不可能に近いので、今回はLVCCとSands Expoに絞って朝から夜まで歩き回りました。日付上は4日間ですが、最終日は昼過ぎから順次各ブースの撤収が始まるので、実質的には3.5日間程度となります。 溢れかえるAR Smart Glassesと勢いの弱まるVR HMD 私自身が体験したAR Smart GlassesとVR HMDの一部 私が初めて参加したCES 2018では多くのVR HMDが出展されていました。ところがCES 2019ではVR HMDよりもAR Smart Glassesが目立っていました。 CES 2020ではどうだったかと言うと、まずCES 2019で多くの関心を集めたNreal Lightの後を追うように、非常に良く似たメガネ型のデバイスが目立ちました。 Nrealのブースでは日本企業のMESON社やSynamon社によるデモを体験できた そしてVR HMDはかつてほどの勢いを感じられませんでした。もちろん全く展示がなかったわけでもなく、新製品が発表されることもありましたが、AR Smart Glassesの勢いには押されている印象を受けました。 FCA Groupのブースでは大型ディスプレイでのサイネージと合わせてVR HMDが使われていた これはVRがある程度の実用段階に入り、ハードウェアそのものよりコンテンツが重要になってきていることを示唆しているものと考えています。 事実AR/VRコーナー以外のブース、例えばLVCC Northにある自動車エリアでは、コンテンツを見せるためのデバイスとしてVR HMDを用いているブースをいくつも目にしました。 Nreal LightとポストNreal Light CES 2019で話題となりCES2020でも多くの人を集めていたメガネ型デバイスのNreal Lightですが、前述の通り「ポストNreal Light」とでも言うべき、デバイスの形状、プロモーションの打ち方が非常に良く似たメガネ型デバイスをいくつか見かけました。例えば MAD Gaze社のMAD Gaze GLOW 、 0glasses社のRealX 、 Pacific Future社のam glass などが挙げられます。 MAD Gaze GLOWのMAD Gazeブースとam glassのPacific Futureブース その多くはスマートフォンと接続するタイプのもので、SLAM 機能もなく、スマートフォンのディスプレイをメガネに拡張するだけのものも多々あります。今後もハードウェア先行でこういったメガネ型デバイスは数多く出てくると考えられますが、大事なのはそのデバイスを使う理由であるコンテンツだと考えています。多くのブースでは「デモ」以上の体験が出来なかったので、次のアクションが気になりました。 突然現れたパナソニック社の眼鏡型VRグラス AR Smart Glassesが目立った印象のCES2020ですが、決してVR関連の新発表がなかったわけではありません。 例えばLVCC Centralにとても大きなブースを構えていたパナソニック社は、CES初日の1/7に突然 「眼鏡型VRグラス」に関するプレスリリース を配信しました。 あくまで「参考出展」ということで、ケースの中に収められ誰もが体験できる状態にはなっていませんでしたが、今回は良い機会に恵まれてデモ機を試せました。 パナソニック社の眼鏡型VRグラス(※写真の撮影と公開許可は頂いています) その独特な見た目も相まって「○○○に似ている、いや△△△だ」といった見た目に関する感想が多く散見されましたが、たしかに思い切った見た目だと感じました。 聞くところによるとSoCが内蔵されているので、PCを必要としないスタンドアロンで動作することも想定されているようです。しかし、実働するデモではGeForce RTX 2080 SUPERという高価格帯のGPUを搭載したデスクトップPCとUSB Type-Cで接続する形を採っていました。 接続方法は前述のAR Smart Glasses郡と似ています。大きな違いはHDRで、片目4K・両目8Kの解像度且つ超高精細、そしてパナソニック社の音響ブランドであるテクニクスのイヤホンを使うことで超高音質を実現している点です。 実際に体験したところ、プロトタイプながらたしかに高画質・高音質という謳い文句に異論を挟む余地はありませんでした。あくまで「プロトタイプ」であるということは強調して説明され、発売時期も含めて不確定要素の強いデバイスです。しかし、日本発のハードウェアということもあり、CES 2020全体を通して個人的にとても期待を持ったプロダクトのひとつとなりました。 デルタ航空が示したパラレル リアリティ ディスプレイ CES 2020ではデルタ航空が航空会社として初めて基調講演とブース出展を果たしました。基調講演の中では公式アプリであるFly DeltaにAR機能が追加される旨の他、「パラレル リアリティ ディスプレイ」という新しいディスプレイの表示方法を提示しました。 デルタ航空によるパラレル リアリティ ディスプレイのデモ、筆者の名前である“IKKOU”の文字列は他の人には見えていない パラレル リアリティ ディスプレイは、1つのディスプレイで複数人、それも1人や2人ではなく100人程度に対して、それぞれに合わせた内容を表示できる技術です。 仕組みとしては先ずデモブース入場時に対象者を認識、ブース内に設置されたカメラが対象者をトラッキングし、対象者に合わせた内容を、マルチビュー・ピクセルを用いたディスプレイで表示しています。 私が見ている内容はデモを一緒に体験した他の3名には見えていない この技術はデモのためのものではなく、今年2020年中にデトロイト空港で実運用が開始されるとのことです。 news.delta.com バーチャルヒューマン“Neon”の衝撃 CES 2020において、“色々な意味で”もっとも注目を集めたと言っても過言ではない、そしてXR領域とも関わりが深いと言えるNeonについても触れておきます。 Neon社のCEOを務めるPranav Mistry氏による基調講演のワンシーン Neon社はSAMSUNG社の研究開発部門であるSTAR Labsを母体とする企業です。企業名と同じNeonという実在する人間と変わらない見た目を持つ“Artificial Human”分かりやすく言うとバーチャルヒューマンを開発しています。 https://www.neon.life/ www.neon.life 事前情報として公開された“本物の人間と変わらない見た目”のYouTube動画が話題になり、CES初日からブースにはたくさんの人が訪れ、そのあまりのリアルさに驚愕していました。私自身もCESが始まる前夜、あまりのリアルさにとても興奮したことを覚えています。 基調講演後のデモを眺める多くの人 そして迎えた当日、YouTubeに公開された動画や、ブースに展示されている“本物の人間と変わらない見た目”のものは紛うことなき人間であり、Neonではないイメージ映像であることが分かりました。これには多くの人が落胆し、騙されたと言う人まで現れました。 私自身も残念な気持ちになったのは事実ですが、実はイメージ映像であることは示されていたので、見せ方の是非は別として「騙す意図はなかった」と認識しています。 一部で疑義を醸している #NEON ですが、ブースで流れている “イメージ動画” ではない AI による “リアルタイムのもの” はこちらです。 確認したところ服は AI によるジェネレートではなく個々に要デザインとのことでした👖 #CES2020 pic.twitter.com/fI7xdk3aJW — HEAVEN ちゃん (@ikkou) 2020年1月9日 実際にNeonが動いているデモの様子を見ると分かりますが、少なくとも現時点ではYouTube動画やブースに並んでいるほど“本物の人間と変わらない見た目”ではないように見えます。しかし、決してクオリティが低いわけでもなく、今後の開発状況次第ではバーチャルヒューマンとして成立するであろう将来性を感じました。 High-Tech Retailing XR領域はハードウェアとして分かりやすいVR HMDやARグラスだけに限りません。例えばCES2019から新設されたカテゴリであるHigh-Tech Retailingエリアには、XR領域とも言えるプロダクトが複数ブースを出展していました。 両足を三次元計測するAlbert Scanner 例えば両足を3次元計測するAetrex 社の Albert Scannerはそのひとつです。実際に試してみたところ、わずかな時間で足の3Dデータとともに各種サイズが計測され、そのデータを基にして靴に合わせたインソールを作成して自宅に届けてくれるようでした。(残念ながら日本への発送は実施していませんでした) Beauty Tech High-Tech Retailingコーナーに出展しているブースは10数程度でしたが、他にも商品を3D化するソリューションや、スマートミラーを展開する企業が出展していました。また、Beauty Techでもあり、AR × メイクでは老舗とも言えるYouCam Makeupが比較的大きなブースを構えていました。 AR × メイクでは老舗とも言えるYouCam Makeupブース High-Tech Retailingコーナー以外では、アプリと連動するインスタントジェルネイルを提供するO'2NAILSブースが昨年に引き続き賑わっていました。 アプリと連動するインスタントジェルネイルを提供するO'2NAILS また、CES 2020で初出展となるインスタントタトゥーマシンを提供するPrinkerブースにもとても多くの人が訪れて試していました。 インスタントタトゥーマシンのPrinkerブース Prinkerはアプリで選んだ柄を一瞬でプリントできるインスタントタトゥーマシンです。水溶性のインクを使っていて、お風呂で簡単に洗い流せるので、例えばフェスなどのイベントで使うことが想定されています。実際に試しましたが、驚くほど簡単にプリントできました。 O'2NAILSもそうですが、こういった実体験可能なデモを設けているブースは軒並み盛り上がっている印象を受けました。 なぜCESに参加するのか CES 2020 INNOVATION AWARDSを受賞したハードウェアの集まるINNOVATION AWARDS SHOWCASE ここまで現地で見聞きしたものをいくつか紹介してきましたが、CESに関する情報はテクノロジー系メディアを中心として、大半のことはインターネット経由で知り得ることができます。これがテクノロジー系メディアのライターであれば現地に赴き、記事を書くこと自体が仕事となりますが、ライターではない私がなぜ時間とお金をかけて現地に行くのでしょうか。 これは先ずXR領域において『百聞は一“体験”に如かず』という考え方があるからです。 つまり、「百聞は一見にしかず」という言葉以上に、実際に自分の目で見て身を以て体験することが重要だと言えます。前評判では期待できなかったものが実は素晴らしいものである可能性もありますし、その逆もあり得ます。まだ“当たり前”からは少し遠い技術であるがゆえ、しっかりと説明責任を果たせるよう、他人の意見だけではなく、自分自身で語れる状態であることを大切にしています。 XR領域だけを目的とするのであれば、CESよりもMWC(Mobile World Congress)や、より専門性の高いAWE(Augmented World Expo)などが適切です。しかしXRは手段のひとつでしかなく、テクノロジー全体を見渡す意味ではCESに参加する意味があり、だからこそ継続的に参加して差分を理解して未来を予測することも意味があると考えています。 もちろん、良いデバイスや良いソリューションがあれば、どこよりも早くアライアンスを結んで事業に繋げるといった展示会での一般的な目的もあります。実際、日本から来ている方の一部は商談目的やバイヤーの方です。 まとめ 写真だけでも32,000枚近く撮影しているので、そのすべてを紹介することは出来ませんが、特にXR領域で気になった“一部”を紹介しました。 現地参加は3年目となるCESでしたが、初のCES参加時に得られたような感動はもうだいぶ薄くなりました。 そしてXR領域だけで言えば「爆発的な変化」というよりも「順当な変化」を感じる回となりました。テクノロジー全体で言えば、例年大きなブースで出展していた企業が今年は出展しなかったなど、本記事で取り上げていないが大きく変化した部分もありますし。逆にNeonやデルタ航空のように、初出展で挑戦的な試みを実施する企業も出てきました。 今はまだ目新しく映るXR領域も、いずれは社会に融け込んで当たり前のものになると考えています。それがいつなのか分かりませんが、この技術領域を推す身としては、テクノロジーの潮流を追いながら適切なタイミングで技術を生かすことを図っていきたいと改めて胸に誓いました。 おわりに 今回のCESは、 福利厚生のひとつである「セミナー参加制度」 を利用して参加しました。特に海外出張ともなると渡航費・宿泊費だけで相応の金額が必要となるので、こうして自分の目で見る機会が提供されるのは本当に有り難いことです。前述の通りXR領域とは「百聞は一体験に如かず」なので、ここで得た知見をうまく業務に生かしていきたい次第です。 最後に、ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com 特にXR × ファッション領域での「新規事業」に興味のある方は、一度お話しましょう:) 現場からは以上です!
アバター
どうも品質管理部のキムラリョーです。 Selenium & Pythonを利用した自動テストプロジェクトの再構築をDockerを使って簡単にしたい、という話です。 これまでの自動テスト 実行までに必要な手順 1. リポジトリクローン 2. Pythonインストール 3. pipで必要なパッケージをインストール 4. Dockerインストール 5. 自動テスト実行 ターミナルからmainを実行すると、Selenium Gridのコンテナを起動した後にtestautoが実行されます。testautoはSelenium Gridに接続してブラウザを操作しながらテストを行います。 Selenium Gridだから起動時などの設定で様々な形に切り替える事ができます。Nodeを増やしたら並列も可能だし、ヘッドレスも使えるし、気軽にブラウザの設定内容を変えられます。 このプロジェクトは作成者である自分だけが実行していました。試運転と本番実行を行いながら、好きなタイミングで気軽に動かせるならよかったので、自分のPC上で直接Pythonを動かします。 テスト処理という意味ではこのままの形でも大きな問題はありません。 ただ長く利用しているとPCの買い替えや実行PCの追加、実行者が増えたりなど、頻度が多いわけでは無いですが忘れた頃にプログラムの実行環境を再構築する必要が出てきます。 引っ越しって何かと面倒な作業が発生するので、できる限り簡単にしたいと考えました。 問題1「Pythonのインストールが面倒」 プロジェクトをGitHubからクローンしてきて、テストを実行するまでの間に必要な準備が以下の3つです。 - Pythonのインストール - pipで必要なパッケージをインストール - Dockerのインストール Dockerのインストールは特に問題ないと思うのですが、Pythonやpipに関しては簡単ではありません。 一番は、Pythonをよく知らない人にとってインストールは面倒すぎます。 Pythonを理解している人のPCで動かすにしても、localのPythonで動かすわけにはいかないですし。localがダメならpyenv? そうすると「Pythonをよく知らない人」にとってのハードルがどんどん上がります。プロジェクトを作成する自分としても把握が面倒ですし、Pythonそのものやpipインストールするパッケージのバージョン等を気にしたくないです。 テスト業務をメインとするメンバーはプログラミング言語に触れる機会が少ないですが、自動テストを作成する自分よりもテストへの理解度が高いです。Pythonを知らなくても実行できる状態にする事で役割分担を進めて行く事もできますので、無理にPythonを覚えるよりもメリットが大きいと思い、インストール手順をスキップできる方法で改善します。 DockerにはPython入りのコンテナがあるみたいなので、Python入りコンテナでプログラムが動くようにします。コンテナ起動時にはpipインストールも自動で行います。 問題1を改善した自動テスト 実行までに必要な手順 1. リポジトリクローン 2. Dockerインストール 3. サブコンテナ起動 4. メインコンテナ起動 5. 自動テスト実行 問題2「コンテナの中からコンテナを起動したい」 Pythonとpipに関しては「メインコンテナ起動」にまとめる事ができましたが、 mainがコンテナに入った事でmainからSelenium Gridが入ったサブコンテナを起動できなくなりました。それによって必要な手順に「サブコンテナ起動」が増えました。 コンテナの中はlinux、ホストとは異なる環境、別物です。 linux「Docker? なにそれ?」 とクジラの上にいるペンギンが灯台下暗し的な事を言い出します。コンテナを立ち上げたい時はホスト上から起動する必要があります。 もしmainからコンテナを起動できると「mainがtestautoの実行時、テスト内容に合わせた設定のSelenium Gridを起動する」といった事もできるので、今のうちに対応したいところです。 調べて見るとコンテナの中にコンテナを立ち上げる方法と、あくまでもホスト上にコンテナを立ち上げる方法があるみたいです。 - DinD [Docker in Docker] - DooD [Docker outside of Docker] メリットやデメリットなどの詳細については、上記の名称で検索するといくつか出てくると思います。 DinDの場合は、ホストで動いているDockerとはまた別のDockerが存在する事になってややこしい気がしました。 DooDの場合は、コンテナが全てホスト上に立ち上がるのでコンテナ同士の関係性が一切見えません。今回のプロジェクト以外でも同ホスト上にコンテナを立ち上げている場合は管理できなさそうです。 プロジェクトによってはセキュリティ面の確認は必須です、Docker imageも公式のimage限定にするとか。自分もまだ試してませんが、Rootlessモードにしてみるとか。 とりあえず今回はコンテナの管理に気を回す必要はなく、利用者も部内のみなので、DooD [Docker outside of Docker]を選択する事にしました。 自動テストでエラーを検知した際、その内容を確認し必要であれば手動テストも合わせて、検知したエラーが「自動テストの問題」か「テスト対象サイトの問題」かを判断しなければなりません。 この原因解明時に一番困るのが、短時間で原因を特定できず再現もできないエラーです。同PC内の別のプロジェクトによってなんらかの影響を受けた事が原因だった場合などは一番手間取るように思います。たとえ単純な原因だったとしても、別プロジェクトによるモノだと見落としやすくなりますし。 基本的には自動テスト専用PCにする、基本を崩す時は自身でなんとかする。という事でDooDにしました。 問題2を改善した自動テスト 実行までに必要な手順 1. リポジトリクローン 2. Dockerインストール 3. メインコンテナ起動 4. 自動テスト実行 mainとtestautoも切り離した方が綺麗だと思ったのでコンテナを分けました。 必要なコードを書いてみる 問題が解決した事でとりあえず起動までは進めそうなので、簡易的ですがコードを書いていきます。 ファイル構成 TestAutomation |_main | |_main.py | |_testauto | |_testauto.py | |_Dockerfile | |_requirements.txt | |_docker-compose.yml |_Dockerfile |_requirements.txt ./docker-compose.yml version : '3' services : main : build : . container_name : 'main' volumes : - /var/run/docker.sock:/var/run/docker.sock - ./main:/main - ./testauto/Dockerfile:/main/testauto/Dockerfile:ro - ./testauto/requirements.txt:/main/testauto/requirements.txt:ro tty : true environment : - HOST_ROOT_PATH=${PWD} command : python ./main.py mainコンテナを起動する為のdocker-composeファイルです。 docker.sockをマウントしてDockerの情報をホストとコンテナで共有します。 mainがtestautoコンテナを起動する時の為にtestautoのDockerfileとrequirements.txtをマウントしておきます。別にマウントせずにmainディレクトリの中に入れておいても良いんですが、この2つのファイルがtestautoの為の物なんだという事が一目でわかると思ったのでこの配置です。 environmentでホストから見たこのディレクトリのパスをコンテナ内の環境変数に設定します。 commandにmain実行を書く事で、メインコンテナ起動と同時に自動テスト実行も行うようにします。 ./Dockerfile & ./testauto/Dockerfile FROM python:3.7 ENV PYTHONUNBUFFERED 1 RUN mkdir /main か testauto WORKDIR /main か testauto COPY ./requirements.txt /main か testauto/ RUN pip install --upgrade pip RUN pip install -r requirements.txt mainとtestautoのdocker imageを構築する為のファイルです。 とりあえず簡単な内容なので2つぶんまとめて。コンテナ名になる部分だけをそれぞれ変更します。 ./requirements.txt docker==4.0.2 ./testauto/requirements.txt selenium==3.141.0 Dockerfileから参照するpip installの対象パッケージを書きます。 必要な物を追加。それぞれrequirements毎にコンテナが異なるので、同じパッケージが必要ならどちらにも書く。 ./testauto/testauto.py import time print ( 'start' ) for i in range ( 30 ): time.sleep( 1 ) print (i) print ( 'end' ) とりあえず実行されるかがわかればいいので、適当に書いておきます。 ./main/main.py import docker from docker.errors import ContainerError import os import re # docker-compose.ymlで指定した環境変数を使います HOST_ROOT_PATH = os.environ.get( 'HOST_ROOT_PATH' ) ################################################## # testauto container # def run_container_testauto (): client = docker.from_env() testauto_dir = f '{HOST_ROOT_PATH}/testauto/' name = 'testauto' docker_setting = { 'image' : name, 'name' : name, 'volumes' : { testauto_dir: { 'bind' : '/testauto' , 'mode' : 'rw' }, }, 'environment' : [ # 環境設定にhostPCのIPが入ります 'SELENIUM_GRID_HUB_IP=host.docker.internal' , ], 'command' : f 'python ./testauto.py' } # imageを探す、なければDockerfileのpathを渡して作成する if not client.images.list(name): client.images.build(path= './testauto' , tag=name) try : client.containers.run(**docker_setting) except ContainerError: print ( 'run_container_testauto error' ) con = client.containers.get(name) logs = con.logs() file_path = f 'logs/testauto.log' dir_path = os.path.dirname(file_path) if not os.path.exists(dir_path): os.makedirs(dir_path) with open (file_path, 'w' ) as f: f.write(logs.decode( 'utf-8' )) ################################################## # selenium grid container # def run_container_grid_hub (network_name): client = docker.from_env() docker_setting = { 'image' : 'selenium/hub:3.141.59-iron' , 'name' : 'selenium-hub' , 'detach' : True , 'tty' : True , 'network' : network_name, 'ports' : { '4444' : 4444 , }, 'environment' : [ 'TZ=Asia/Tokyo' , ] } client.containers.run(**docker_setting) def run_container_grid_node (network_name, number, browser= 'chrome' ): client = docker.from_env() docker_setting = { 'image' : f 'selenium/node-{browser}-debug:3.141.59-iron' , 'name' : f 'selenium-node-{number}' , 'detach' : True , 'tty' : True , 'network' : network_name, 'ports' : { '5900' : 55550 + (number * 10 ), '5555' : 5555 + number}, 'environment' : [ 'TZ=Asia/Tokyo' , 'NODE_MAX_INSTANCES=5' , 'NODE_MAX_SESSION=5' , 'HUB_PORT_4444_TCP_ADDR=selenium-hub' , 'HUB_PORT_4444_TCP_PORT=4444' ], } client.containers.run(**docker_setting) def run_selenium_grid (node_count= 1 ): network_name = 'grid' client = docker.from_env() # docker networkを探して、なければ作成する if not [v for v in client.networks.list() if network_name == v.name] client.networks.create(network_name) containers = client.containers.list() if [v for v in containers if 'selenium-hub' in v.name]: print ( 'hub あります' ) else : run_container_grid_hub(network_name=network_name) container_names = [v.name for v in client.containers.list()] for number in range (node_count): name = f 'selenium-node-{number}' if name not in container_names: run_container_grid_node(network_name=network_name, number=number) def remove_container_grid (): client = docker.from_env() for v in client.containers.list(): if re.match( r'selenium-(hub|node)(-[0-9]+)?' , v.name): v.stop() v.remove() ################################################## # run testauto # def run_testauto (): client = docker.from_env() run_selenium_grid() run_container_testauto() remove_container_grid() client.containers.prune() client.networks.prune() client.images.prune() if __name__ == '__main__' : run_testauto() サブコンテナを起動する処理です。流れは以下です。 1. Selenium Grid Hubコンテナを起動 2. Selenium Grid Nodeコンテナを起動 3. testautoコンテナを起動しテストが実行される 4. testautoコンテナが終了したら各コンテナの停止 https://pypi.org/project/docker/ https://docker-py.readthedocs.io/en/stable/ ここではコンテナ管理にDocker SDK for Pythonを利用します。Dockerの基本的な利用方法をPythonに置き換えるだけであればややこしい部分はないので、公式ドキュメントを巡ったらなんとなく使えると思います。 コンテナ起動時に実行されるcommandを設定したので、3ステップでtestautoが実行される状態になりました。 1. リポジトリクローン 2. Dockerインストール 3. メインコンテナ起動 試しに全て揃った状態でプロジェクトディレクトリに移動して docker-compose up -d --build を打ってみます。 testautoが合計30秒のtime.sleepで終了するまでに、起動コンテナの docker ps -a と、testautoコンテナのログ docker logs testauto を確認します。 一応上記コードは内容に間違いがなければ、testautoコンテナ終了時にそのログを書き出しておく処理をつけました。main/logsディレクトリ内にテキストファイルが保存されるかと思います。 諸々、確認ができたら完成です。 問題3「mainコンテナの起動に時間がかかる」 一件落着と思ったんですが、実際のプロジェクトで同じ対応を行った時、mainコンテナの起動にやたらと時間がかかっていました。 コンテナが起動するまでのDockerの動きを調べたのですが、コンテナのイメージを構築するタイミングで、Dockerfile以下の階層にあるファイルを全てコピーするらしいです。 今回だとmainコンテナ起動時には使わないファイル、一番大きくなるtestautoディレクトリをコピーしてしまうので、これに極端に時間がかかっているような気がしました。 そこでコピーするファイルを一部除外できるdockerignoreというのがある事を知りました。使い方はgitignoreと同じです。除外したいファイルを指定する形です。 gitignoreと書き方は同じですが、処理の流れが違うようなので、本当に必要な情報だけを書くようにしておくのが良いみたいです。試してはいないですが、ignoreの書き方によっては遅くなるとかなんとか。 dockerignoreを設定した結果、1分くらいかかっていた起動が、数秒に短縮されました。 問題4「何もしてないのにSeleniumのWebDriverが落ちた」 引っ越ししてからある程度時間が経った頃、突然よくわからないタイミングでテスト実行中にWebDriverを掴めなくなる事がありました。 エラーログ等をいろいろ調べていくと、スクリーンショットで失敗した後にWebDriverを掴めなくなっているようでした。さらにエラーが出たタイミングをみると、大きな画面を撮影しているテストでのみ落ちていました。ただ、撮影失敗した後でもWebDriverをちゃんと掴めているケースの方が多かったです。 そうなるとスクリーンショットではなくメモリとかの問題かと思いコンテナを確認していたら、shm_sizeの変更、もしくはdev/shmのマウントがされていない事に気づきました。shm_sizeはコンテナが使用するメモリのサイズです。 上記のコード「main.py」で指定しているdocker_settingに設定を追加します。 サイズ変更なら shmsize: 256 、マウントなら 'volumes': {'/dev/shm': {'bind': '/dev/shm', 'mode': 'rw'}}, です。 今回の改善でコンテナの起動方法がdocker-composeからPythonのDockerに変わりました。コンテナ内からdocker-composeでコンテナを立ち上げる方法がわからなかったのでこうなったのですが、書き移すような時は抜けがないか注意しないと気づきにくいです。 ありがちな凡ミスだと思ったので、ここに書き残しておきます。 おわり 自動テストの最終的な形ですが、せっかくなので外部の環境も記載しました。 最近はAppiumでのアプリテストも追加しようと拡張中です。 Appiumは実機で動かすのであればDockerに入れるのは難しい気がしているので、localで立ち上げておく必要があります。物理的に端末を用意してPCに接続しておかなければならないですし。 Appiumの環境を整えるというのもなかなか手間がかかるので、AppiumやSeleniumの専用PCを用意してip経由で接続というのも良いと思います。 今回の変更によって実行までに必要な手順は5ステップから3ステップに減りました。 変更前 1. リポジトリクローン 2. Pythonインストール 3. pipで必要なパッケージをインストール 4. Dockerインストール 5. 自動テスト実行 変更後 1. リポジトリクローン 2. Dockerインストール 3. メインコンテナ起動 単純に3/5になったというのではなく、そもそもPythonとpipの部分は他のステップと比べても特に時間がかかる部分なので、大きな削減になりました。気持ち的には1/5くらいになったのかな、と思っています。 今回はDinD [Docker in Docker]ではなくDooD [Docker outside of Docker]を利用しましたが、DinDについてももう少し試していこうと思っています。仕組みとしてもこれが正解という訳では無いですし。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは! 開発部SREチームの指原( @sashihara_jp )です。 弊社にはSREチームが複数あり、私が所属しているチームは主にZOZOMATなどの計測システム、ZOZOERPと呼ばれる服の生産に関するシステムなど複数のプロダクトを担当しています。 本記事では私がSREチームのマネージャーとしてチーム内で行ってきた施策についてご紹介したいと思います。現在チーム内で行っている施策は全部で10個程度あるのですが、今回チーム内で事前に各施策について満足度アンケートをとりました。ここでは満足度の高かった施策とそうでなかった施策について、その理由と共に解説していきたいと思います。 前提 満足度が特に高かった施策 1on1 チーム目標設定 意義目標 成果目標 行動目標 満足度が高かった施策 テックリードサポートタイム チーム内ZOZOエール 中長期タスク設定 KPT LT、技術記事執筆 輪読会 満足度があまり高くなかった施策 スキルマップ策定 もくもく会 まとめ 前提 組織のマネージャーの役割はチームとしての生産性やパフォーマンスの最大化だと言われています。そのための手法として昨今1on1(ワンオンワン)と呼ばれる上司と部下の1対1での頻繁な対話が注目されていますが、私がチーム内で行っている施策もこの1on1を軸にして、そこからヒアリングしたメンバーが持つ不満や不安への対策、メンバーからのやってみたいという提案などから生まれたものが中心となっています。 それぞれの施策に関しても最初から今の形に落ち着いているわけではなく試行錯誤の最中でメンバーからのフィードバックを反映しながら変化しています。今後もチームの生産性、パフォーマンスを最大化するために積極的に改善を試みていきたいと思っており、今回紹介する施策はあくまで現時点のものであり、これがベストな最終形だとは思っていません。 また、ここでは偉そうなことを書いていますが、私自身、ベテランのマネージャーではなく、これから勉強していきたいと思っている発展途上の身ですので、温かい目で読んでいただければ幸いです。 それでは、実際に行っている施策について紹介していきたいと思います。 満足度が特に高かった施策 1on1 冒頭でも紹介しました1on1ですが、今回アンケートで5人のメンバー全員がやってよかった、今後も続けて欲しい項目として挙げてくれており、非常に満足度の高い施策です。私のチームでは以下のような形式で実施しています。 1人30分、週に1回 事前に双方から話したいことを1つ以上用意してくる スプレッドシートに議事録を残して振り返るようにする メンバーは5人いるので毎週必ず3時間近く1on1に使っていることになります。よく1on1について聞かれる質問として「普段から隣の席でコミュニケーションを取っているから必要ないのでは?」ということがあります。しかし、実感としてはどんなに普段からコミュニケーションを取っていたとしても、 話す内容を考えることで自分やチームと向き合うことができる フィードバックを細かい頻度で実施できる という2つの大きなメリットがあるので、やるのとやらないのではチームや個人の成長速度が全く違うと感じています。 ただ実際、何を話すのかは最初、難しかったのも事実で、初期の頃は30分間雑談で終わってしまうことも多かったです。そこでお互いに事前にアジェンダを用意してくる形式に変更することで、しっかり向き合う時間になるよう工夫し、議事録も残して後で振り返ることができるようにしました。 内容として気をつけていることは タスクの進捗確認は別の機会があるので極力しない 単なる雑談だけの日があってもいい 普段はあまり考えないようなキャリアプランを考える機会にする プライベートも含めたメンタルヘルスについて確認する うまくいったタスク、いかなかったタスクの原因を振り返ることで、経験学習を促進する 仕事やチームへの不満、改善点を洗い出す などを意識して議題を設定しています。 基本的には何でも話してOKで、ここでなにか問題が発見されたのであれば解決案を一緒に考えることもありますし、答えが出ない場合はチーム全員で案を出し合ったり、その結果、次の施策に繋がったりすることもあります。 好きなドラマや、欲しい生活家電のことを話すだけの日もありますし、お互いの人となりが分かったり、仲良くなったり信頼関係を強化するだけでも十分だとも思っています。 チーム目標設定 次に、これも非常にメンバーの満足度の高かった施策です。チームでは以下のような3つの目標を掲げています。 意義目標 会社が持つ「世界中をカッコよく、世界中に笑顔を。」という企業理念を実現するために、自分たちが関わっている事業をなぜやるべきなのか、どのような未来を我々は作っていこうとしているのかという長期的な視点を忘れないようにしようという目標です。 成果目標 意義目標をブレークダウンして、事業として今期達成すべき具体的な成果、ゴールのイメージを成果目標としています。 行動目標 成果目標をさらにブレークダウンして、大枠でのタスクレベルにまで落とした目標を行動目標としています。実際に今期、成果目標を達成するために必要なタスクとしては何があるのかというレベル感です。 この3つの目標を立てるという考え方は「THE TEAM」という本を参考にして行っているのですが、この形式でなくてもいいと思っていて、大切なのはチームとしてのビジョンや目標を立てることだと思っています。 何事も先を見据えたゴール設定が適切にされていないと進む方向にブレが生じますし、目の前の仕事が必ずしも楽しいものだとは限らないのが常だと思いますので、自分の仕事が最終的に何につながっているのかということを示す指針がチームには必要だと思っています。 満足度が高かった施策 テックリードサポートタイム 弊社ではチームのマネージャーとは別にテックリードという役職が存在します。その名の通り技術でチームや会社を引っ張ることがミッションの役職で、チームメンバーに対して技術的なレビューだったりサポートをすることが求められます。 ただ、どこの会社やチームでもそうだと思いますが、技術的に優れている人ほど仕事が集まりやすく、常に忙しいという状況で、チームメンバーの中でも特に新人に近いメンバーはもっとレビューやフォローをしてほしいけど、テックリードがいつも忙しそうだから話しかけづらいというような課題がありました。 そこでまず、タスクの分配自体でテックリードの手をできるだけ空けられるように調整を行いました。次に、テックリードにはスケジュール上、帯で1日1時間の枠を確保してもらい、その時間はメンバーがテックリードに質問するだけの専用の時間にしてもらいました。 この施策はテックリードの光野さんという方にご協力いただいて「みつのアワー」という親しみを込めた名前で実施しています。「みつのアワー」を始めて半年以上が経ちますが、毎日ほぼ必ず利用されておりメンバーからも好評です。1on1もそうですが、その施策がないときに「いつでも話しかけていいよ」と言っても実際話しかけづらかったりするものですので、あえてそのための時間を設定するということが効果的なんだなという学びになっています。 チーム内ZOZOエール 弊社では wevox という組織のエンゲージメントをスコア化するツールを導入しているのですが、そのwevoxの結果で私たちのチームには「成果に対する承認の機会」が少ないということが分かりました。 振り返ってみるとたしかに、タスクが無事完了したことを祝ったり、仕事での良い動きや姿勢を褒めるというような機会は作っていなかったなと気付きました。そこで最近始めたのがチーム内ZOZOエールという仕組みです。 ZOZOエールというのは社内のピアボーナス制度で、日頃のありがとうや会社の理念やミッションを表現した行動、会社への貢献、感動したことに対して Unipos というツールを使って少額の報酬を与え合うことができるものです。ZOZOエールは社内のありがとうを可視化しようという試みで非常に良い仕組みだと思うのですが、私たちのチームではこちらも使っている人が少なく、活用できているとは言い難い状況でした。 そこでこのZOZOエールを活用し毎週金曜日の朝会の中で、この1週間チームの中で頑張っていた人、成果を出していた人、他の人の見本になるようないい行動や姿勢を見せていた人を褒め合おうというタイムを設定しました。 この褒める内容については仕事に限らなくてよくて、例えばウォーターサーバーの水を率先して替えてくれていましたとか、飲み会の幹事を率先してやってくれてありがとうとか、普段あまり褒められることがないようなポイントについても誰かが見つけて取りこぼさずにピックアップできるという良い仕組みになっているかなと思っています。 もちろん、大きなタスクを達成した時や、技術的に分からないことを教えてくれたなど、普通の仕事上での「おめでとう」や「ありがとう」についても投票されるので、ポジティブなフィードバックの文化が加速できていると思います。 中長期タスク設定 私たちのチームでは複数のプロダクトを担当しており、基本的には各プロダクトからの依頼をさばくというような仕事のスタイルだったのですが、案件のレベル感的に半年や1年かけて完成させるというよりは1週間以内で終わる小粒のタスクばっかりだよねという状況がありました。 また、仕事のタイプは「緊急度の高い低い」「重要度の高い低い」で分類できると思いますが、チームとしてやっている仕事は「緊急度が高くて重要度も高い仕事」か「緊急度が高くて重要度は低い仕事」に偏っていました。しかし「緊急度は高くないけれど重要度は高い仕事」の中にこそ、本当はやるべき価値の高いことが眠っていたりします。 そこで、そのような「緊急度は高くないけれど重要度は高い仕事」に中長期的に取り組む時間を作ることで、会社やプロダクトへの貢献だったりスキル面での向上を目指そうという中長期タスクを作ることにしました。現在は各プロダクトの案件以外のタスクとして全員が中長期タスクを持って取り組んでいます。 例えば弊社では50個近いAWSアカウントを管理していますが、それぞれのアカウントへのログインをシングルサインオンで管理することによって、利便性とセキュリティ面での向上を目指すことを中長期タスクの1つとして設定しています。 また、これらの中長期タスクについては、基本的にはプロダクトのタスク優先になってしまうので、半年後に振り返って何もしていませんでした、というようなありがちなことがないように毎週「中長期タスクサポート会」を実施しています。「中長期タスクサポート会」とは 進捗報告(なければないでも良い) 技術的だったりタスク調整的に困っていることがないか を毎週確認することで、タスクのフォローと締切効果を狙っていて、今のところ効果的に働いています。 KPT 2週間に1回必ず2時間かけてKPTを行っています。 KPTは前回から今回までの間で Keep(できたこと、良かったこと) Problem(できなかったこと、良くなかったこと) Try(次回までにやりたいこと) という3つをそれぞれ会議中に、3分ずつ考える時間を作って発表しあうというスタイルでやっています。 チームとしての課題を見つけたいという意味もあるのですが、いまはどちらかというと個人レベルでの成長への宣言(英語の勉強を始める、資格勉強を引き続き頑張る、システム構築を完了させる)が多いという印象です。 KPTもいろんなスタイルがあると思いますが、目標はたてるだけじゃ意味がなくて定期的に振り返るということが必要だと思いますので、定期開催し続けたいと思っています。 LT、技術記事執筆 もともと、私たちのチームは技術的なアウトプットの習慣がないメンバーが多いという現状がありました。技術ブログだったり、Qiitaで記事を書く、LTで発表するといったアウトプットは実際なかなか労力も必要なので敬遠されがちです。 しかし弊社では業界のリーディングカンパニーの1つとして技術のエコサイクルに貢献していくという目標を掲げています。また、文章を書いたり発表したりという準備をすること自体、本人の成長につながる行為でもありますので、積極的にアウトプットを推奨しています。 とは言え、いままでそういうことをあまりやったことない人がいきなり会社の代表として技術ブログや登壇資料を作ることができるかというと、最初はやはりハードルが高いため、私たちのチームでは週に1回ずつ技術に関する内容で、LT発表とブログ等で技術記事を書いてチーム内で共有するということを朝会で実施しており、担当は持ち回り制としています。 慣れてないメンバーにとっては負担もあり辛いとは思うのですが、メンバーのコンフォートゾーンな施策ばかりやっていても本人の成長に繋がりません。チームマネジメントの観点では、多少辛くてもやれば成長できるやるべきことは半強制的にでもやってもらう方がいいと考えており、施策として取り入れています。 輪読会 毎朝1時間、朝会をやっておりそのコンテンツとして30分を使って輪読会を実施しています。輪読会ではメンバーが読みたい本を募集して全員で同じ本を(会社の書籍購入補助で)購入し、毎日決まったページ数(5〜10ページ)を一緒に読んでいます。 読んでいる中で、気になったフレーズだったり、読解が難しかったところ、面白いと思ったところを議事録ページに記載しあって時間がきたらそれらについて全員で議論しています。 技術的アウトプットについてもそうですが、興味のある技術書があったとしても個人で時間をとって読み進めるのはなかなか大変だったりするので、強制的に時間を作ることで読み進めることができます。また、1人で読むより全員で内容について議論しながら読み進めていくことで、理解が深まるので学習効果を高めることができていると実感しています。 満足度があまり高くなかった施策 スキルマップ策定 新しいメンバーが増えたタイミングで、メンバーがスキル的に何ができて、何を経験したことがなく、何を今後身につけていきたいと思っていくのかという点について知りたいと思いスキルマップを作成しました。 スキルマップは例えばAWSのサービスなどを並べていて、それに対する経験と今後の興味についてマルバツで入力していくような形式です。 これに関してはある程度傾向を知ることができたものの、1回しか実施しておらず、ここから次のステップのタスク割当の参考等にあまり活用できていなくて、チームとしても効果的に使えなかったと思う施策でした。 もっと活用するためにPDCAを回してやり方を工夫していけば満足度が上がる余地はあると思います。 もくもく会 勉強会の一種でもくもく会というものがあります。自分の好きなテーマ、興味のあるテーマを持ち寄って一定の時間の中でインプット・アウトプットするというものです。 もくもく会は参加自由として1年程継続して行ってきたのですが、業務と関係のない仕事を業務時間を使ってやるということは、特に忙しいときほどストレスに感じるのも事実で、テーマ設定の難易度も高く最終的にあまり定着させることができませんでした。 Googleさんの20パーセントルールなどありますが、業務時間を使って業務と関係ないことをするというのは、なかなか高い自己マネジメント能力が必要だなという印象でした。 まとめ 以上のように全部で10個の私たちのチームで試してみたチームビルディングに関する施策を紹介してみました。 満足度と運用コストについてグラフにプロットしてみると以下のようになります。自チームに取り入れる際の参考にしていただければと思います。 いろいろ試行錯誤している最中ですが、やってみて思うのは、まずは取り組んでみることが大事だということです。多くの場合で何かしらポジティブな変化が起きますし、変化がなければ止めたり、工夫を加えて再チャレンジすればよいだけです。 マネージャーとして大切なことは何をしていけばチームメンバーが楽しく働けて、成長することができて、ひいてはプロダクト・会社の成長に貢献できるかを考え続け行動し続けることだと思います。 マネージャーがメンバーの延長でメンバーと同じようなことをし続けるのは楽で簡単なことですが、それではマネージャーとして何もしていないことと同義だと思うので、自分の役割を強く意識して行動に移していかないといけないと思っています。 最後にZOZOテクノロジーズではより良いチームを作っていくマネージャーを各職種で大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは、ZOZOテクノロジーズのVPoEの今村( @kyuns )です。 この記事は ZOZOテクノロジーズ Advent Calendar 2019 の25日目の記事になります。今年はZOZOテクノロジーズとして5つのAdvent Calendar、全125個の記事がありますので、ぜひご覧ください。ちなみに前日の記事は @ikenyal の CTO室はじめました 〜新設CTO室が1年目にやったことと課題 でした。 https://qiita.com/advent-calendar/2019/zozo_tech https://qiita.com/advent-calendar/2019/zozo_tech2 https://qiita.com/advent-calendar/2019/zozo_tech3 https://qiita.com/advent-calendar/2019/zozo_tech4 https://qiita.com/advent-calendar/2019/zozo_tech5 昨年2018年12月のAdvent Calendarで このようなZOZOテクノロジーズの紹介記事 を書きました。 時が経つのは早いもので、あれからもう1年が過ぎ、その間にZOZOテクノロジーズにも非常に大きな変化がありました。 この記事では、2019年を振り返りながら最近のZOZOテクノロジーズの様子について紹介していきたいと思います。 会社の状況 ビッグニュース まず2019年9月12日、我々を震撼させる非常に大きなニュースがありました。 なんといっても 前ZOZO代表の前澤の退任 、そして ヤフーの親会社による買収 の2つのニュースです。 前澤社長の退任の衝撃 ZOZOを代表する人間といえば、やはり前澤という人間だったでしょう。月に行くと言ったり、1億円のお年玉を配ったり、大物女優さんと付き合ったり、昨年からメディアで目にしない日はないぐらい世間を賑わせる存在でした。 それと同時にカリスマ経営者であり、彼なしではZOZOTOWNがここまで成長することはありませんでした。 そんなZOZOの顔である彼がいなくなることは我々にとっても非常に大きな心配事でした。 ニュースの発表前は 「ニュースを知った社員たちはどういう反応をするだろう?」 「前澤のことが好きで入ってきた社員達が一気に辞めるのではないか」 「今後のZOZOTOWNの戦略はどうなるんだろう?」 と、いろんな事を考えながら当日を迎えました。 ニュース発表当日、スタッフ達は朝から一体何が起きたのか、事実を受け入れるまでに時間がかかっていた人が多かったように思えます。昨日までは、自分たちの会社を代表する存在だったのに、ニュースで知ったときにはもうZOZOをやめている、そんな天変地異みたいなが起きることがあるのかと。 ただ、この心配は杞憂に終わりました。 発表の後エンジニア達全員と面談をしましたが、前澤の決断という思いを汲み取ってか、みんな非常に前向きで、これからは自分たちでZOZOを支えていかないといけない、という強い意気込みを感じました。 実際に退任が直接的原因で会社をやめたという人は今のところ聞いていません。 今、ZOZO社員は澤田新社長の元、過去最大級にやる気に満ち溢れています。 ヤフー親会社による買収(TOB) 同時に発表されたのはヤフーの親会社、ZホールディングスによるZOZOのTOBのニュースでした。 僕自身、もともとヤフーに新卒入社して、Yahoo! FASHIONを立ち上げた経験があります。ヤフーでファッション業界を変えようとしたのです。しかしながら、その1年後僕と金山(現ZOZOテクノロジーズ CINO)はヤフーを辞め、VASILYという会社を創業し、ファッションサービスの開発を行う人生を歩みました。そして、数年の時を経て2017年にZOZOへとM&Aされたという経緯があります。 元ヤフー出身の僕としては初めてその事実を聞いたときにはさすがに衝撃すぎて変な声が出ましたが、13年の時を経てまたヤフーとともに日本のファッション業界を盛り上げれるなら、これはもう運命以外の何物でもないだろうと、今となってはワクワクしています。 そしてヤフーと手を組むことは 「日本でナンバーワンのファッションECを共に目指す」 ことが可能になることを意味します。 ZOZOとしてもこれほど大きな目標に更に向かっていける強力なパートナーを得たという意味では今後の協業が本当に楽しみです。 またエンジニアリング組織としても、協力してより強化していくことをヤフーCTOの藤門さんとも話し合いました。 会社の規模はどう変わったか ZOZOテクノロジーズには2019年12月時点で 約330名 の従業員が在籍しており、そのうちの約230名がエンジニアになります。10月にはZOZOUSEDが吸収合併された影響もあり、昨年からすると 約100名 ほど従業員が増えました。 また今年4月には14名の新卒社員が入社し、来年の4月には19名の新卒も入社予定、さらにZOZOのグループ会社であったアラタナの吸収合併も予定されているため、400人を超える組織になっていく予定です。 プロジェクト紹介 この1年でZOZOが行う事業にも非常に大きな変化がありました。 現在進行中のプロジェクトをいくつか紹介したいと思います。 ZOZOTOWN すでに完成していると思われがちなZOZOTOWNですが、非常に多くのプロジェクトが進行しています。ちょうど10月頃に これできる人いませんか? という求人ページを公開しましたが、その中からいくつかの案件を紹介します。 ZOZOTOWNリプレイス 昨年から続いているZOZOTOWNのリプレイスプロジェクトです。 オンプレミス環境および、VBScriptで構成されている今のZOZOTOWNのアプリケーションをマイクロサービス化し、クラウドへと移行を進めています。 現在まだプロジェクト道半ばですが、少しずつ本番での運用実績が溜まってきました。 最近では大きな負荷のかかるセール時でも、本番のトラフィックを一部クラウドへ流し、安定運用することが出来始めてきています。 とはいえ、まだまだ道のりは遠く色々試行錯誤しながら進めている最中です。 検索改善 ECサイトにおいて、商品検索は生命線といっても過言ではありません。 ファッション辞書の構築、検索結果のパーソナライズやサジェスト精度の向上など、まだまだ検索まわりにおいて改善することが山程あります。 われこそは、という検索のプロフェッショナルな方の協力を必要としています。 パーソナライズ ZOZOには膨大なファッションに関するデータが蓄積されていますが、まだそれを十分に活用できていません。それらビッグデータを活用し、機械学習などを用いて、一人一人に合わせた商品の表示を届けるようなパーソナライズの基盤を構築するプロジェクトになります。 機械学習や深層学習を用いて、最適な結果を突き詰めていくやりがいのあるプロジェクトになります。 基幹システム ZOZOTOWNを支える要と言っても過言ではないでしょう、ZOZOの強みは自社でのフルフィルメントにあります。その裏側を支えるシステムの改善全般を行います。BOと呼ばれる商品を管理するためのツールの改修はもちろんのこと、最近ではスマートファクトリーチームと連携して、今まで人間の手で商品のサイズを計測していたものをZOZOSUITの技術を応用し、商品を撮影した画像を解析することによって自動的に商品のサイズを算出するようなシステムの開発を行ったりしています。このように"ささげ"とよばれる作業を技術を用いて自動化していくようなことなども行っています。 MSP(マルチサイズプラットフォーム) 昨年最も話題となったZOZOSUITですが、事業方針の転換により、ZOZOSUITはカスタムオーダーのスーツの注文を除いて配布を停止しています。その背景としては、数百万件の体型データを集めたことにより、身長体重年齢などからある程度の体型を予測することが可能となったことや、プライベートブランドの事業方針の転換などがあります。 現在は、ZOZOSUITで培ったマルチサイズの服を作るという技術を用いて、ZOZOに出店してくださっているブランドさんの人気商品をマルチサイズで展開できるようなサービスへと事業転換をおこないました。それが マルチサイズ と呼ばれる事業です。 現在ZOZOTOWN上ではMSマークのある商品は自分の体形にあったサイズで購入することが可能となっています。また、MSP事業を支える上で必要なのが、効率の良い生産方法の確立です。例えば、型紙をそれぞれの体形のサイズにあったように作る作業、いわゆるグレーディングを自動で行うような研究開発なども行っています。 ZOZOMAT ZOZOSUITについで、我々が目指したのは足の計測でした。足の計測をミリ単位で行うシステム 「ZOZOMAT」 の開発を行っています。ZOZOMATのリリースも控えており、靴選びに革命をもたらすシステムの開発に取り組んでいます。 どのようなUI/UXにすれば、ユーザーがわかりやすく、高精度に計測できるのか、日々試行錯誤しながら靴選びの未来を模索しているプロジェクトになります。 中国版ZOZOTOWN「ZOZO」 12月にはZOZOTOWNの中国進出を開始しました。中国に現地法人を設立し、新しくファッションメディアECとして、WEARに蓄積されたコーディネートやトレンド情報発信に絡めながらファッションを楽しむことができるサービスとして新たに「ZOZO」をローンチしました。 世界進出の足がかりとしてまずは中国での成功に全力を尽くすプロジェクトになります。 PayPayモール出店 ヤフーとのシナジー第一弾はZOZOTOWNのPayPayモールへの出店です。 ちょうど今週PayPayモールへの出店を開始しました。 ZOZOTOWN PayPayモール店 PayPayモール内のZOZOTOWNは、在庫の連携なども行っており、システム的にもインテグレーションの強化を行っていきます。 また、今後はZOZOTOWNへのPayPay決済対応をはじめとして様々な連携作業を控えています。 日本一のファッションECをヤフーとともに作り上げるために、色々な施策を行っていくプロジェクトになります。 ZOZO研究所 ZOZO研究所ではいろんな研究が行われていますが、最近では 類似アイテム検索 のようなプロジェクトでもMLエンジニアが活躍しており、非常に高精度な類似画像検索を実現しています。このように研究開発した結果を、プロダクトに組み込めることもZOZO研究所の魅力の一つとなっています。また取り組み内容を Google Cloud Nextで発表 したり、アカデミックな分野以外での登壇の機会も増えてきました。 今後もパーソナライズをはじめとして、ZOZOTOWN側と協力しながら、様々なアルゴリズムの開発を行っていきます。 Innovation Initiative(新規事業) いわゆる新規事業を行うプロジェクトチームになります。その範囲は非常に幅広く、新規サービスの開発やIoT、スマートスピーカー、XRなど様々な新規事業開発を行っています。 世界中を飛び回って自分たちが必要とする技術を持っている会社を探したり、海外の企業とコラボレーションすることも多いチームになります。最近ではメルカリR4DでXRを推進していた @ikkou さんが入社して新しくXRチームが立ち上がったり、これからの活躍が楽しみなプロジェクトチームになります。 労働環境について ZOZOテクノロジーズの労働環境においても非常に多くの変化がありました。 勤務場所 勤務場所は変わらず青山、幕張、福岡の3拠点が存在します。 更に来年の4月にはアラタナも吸収合併されるため、宮崎にも拠点ができることになります。 環境、福利厚生、制度、手当など ZOZOテクノロジーズにはすでに非常に多くの福利厚生や制度がありますが、今年になってさらにいくつかの新しい制度が出来ました。この1年で、制度面から見ても他社に引けを取らないぐらいの水準になってきたかとは思います。昨年からの変更点をいくつか紹介いたします。 グループウェアをG Suiteへと移行 まず社内のグループウェアがOffice 365から G Suiteへ と変わりました。Office 365もいい面はあるのですが、オンライン上のツールが使いにくかったりプログラムからのインテグレーションが弱かったり、今後の効率化のことを考えてGoogle DriveやSpreadSheetを扱えるG Suiteへと移行しました。チャットツールとして利用しているSlackも現在Enterprise Gridへと移行を進めています。 フルフレックスタイム制度導入、リモートワーク解禁 今年新しく導入された制度で、一番インパクトがあったのは間違いなくこの2つの制度、 フルフレックスタイム と リモートワーク です。 今まではコアタイムありのフレックス制(コアタイムは10時〜17時)でしたがフルフレックスタイムへの移行にあたり、コアタイムは無くなりました。 またリモートワークの仕組みも整えたため、自宅などからも業務を行うことが可能になりました。 しかしながら、この2つを実現するためには多くの壁をクリアする必要がありました。 社員からの要望も非常に強く、我々としてもなんとか実現したかった制度でした。 実現するためにも多くの壁はあり、社内でのプロジェクト管理方法の統一、各チームごとでのローカルルールの許可、VPNの整備、貸与端末のセキュリティ強化など、非常に多くの人達の協力を経て実現するに至りました。 運用開始前はどうなるか心配な部分もありましたが、実際に運用を開始してみると非常に良い結果が出ており、平均残業時間は昨年と比べて約半分の月 20時間を切る ぐらいまでに減りました。運用を始めて4ヶ月が経ちますが、目立った悪用もなく、社員には好評です。 特にお子さんのいる家庭や時短で働いていた方々にも好評で、柔軟な働きかたを提供することが可能になりました。 特許取得に関しての奨励金 会社としてより多くの特許取得を奨励していくために、特許の出願に関しての奨励金の金額を定めました。 一般的な会社と比べても、高い水準の奨励金となっています。 リファラル採用強化 ZOZOレコと呼ばれる仕組みがあり、知り合いを紹介すると会社から謝礼金が支払われます。リファラル経由での採用も少しずつ増えており、より強力な仲間を増やすための仕組みを用意しています。 Unipos導入 ピアボーナスと呼ばれる仕組みです(社内ではZOZOエールと呼ばれています)。社員同士日々の感謝を伝え合う場として、普段表に見えないような社員の動きが可視化されたり、感謝が飛び交う場所になっています。もらったポイントは給与として支給されます。 病気休暇制度導入 こちらのプレスリリース にあるように、病気休暇制度を導入しました。例えば従来だとインフルエンザになった場合、5日間会社を休む必要があり、その場合は有休を消費する必要がありましたがこの制度によって有休を消費することなく、休暇を取得することが可能になりました。いざというときのために有休をとっておく、といったような心理的な負担も減り、より健全な有休取得が可能となりました。 他にも以下のような福利厚生や制度があります。 社員割引 住宅通勤手当(月5万円) 家族手当(一人につき5,000円) 夏季・冬期休暇(夏休み、冬休みがそれぞれ3日ずつ付与) 書籍購入補助(金額、冊数の上限なし) 資格取得補助(資格取得費用全額補助) セミナー、カンファレンス参加費用全額補助(国内外問わず、チケット代、ホテル代、交通費を全額補助) 副業許可 詳しくは こちら を御覧ください。 エンジニアリング組織の改善 エンジニア組織をより良くするために、4月にはCTO室を直下に新設し、自分自身の仕事をスケールできるようにするための体制を整えました。 CTO室でどのようなことを行ったかはCTO室の @ikenyal の CTO室はじめました 〜新設CTO室が1年目にやったことと課題 をご覧ください。 CTO室ではエンジニアのパフォーマンスが最大化できるように、様々な仕組みやフローの整備を行い続けています。 1年を振り返ってみて 昨年からは想像もつかないぐらいのいろんな変化があり、まさに変化を楽しんだと言える1年でした。 また、ZOZOテクノロジーズという会社を世間に知って貰う機会を増やせた1年だったかなと思います。 ファッションテックカンパニーとして、少しずつ着実に成長している実感があります。 もちろん、まだまだ発展途上ではあるので、そんな変化が激しいZOZOテクノロジーズ を一緒に盛り上げてくれる仲間も絶賛募集中です。 この記事を読んで、ZOZOに応募してみたいと思ったエンジニアの方は下記の採用ページからぜひご応募ください。 その際に「テックブログみました」と書いていただけると幸いです。 tech.zozo.com 我々と一緒に楽しく働きましょう!
アバター
はじめに こんにちは。WEAR iOSチームの坂倉 ( @isloop ) です。 この間リリースされたiPadOSはかなり盛りだくさんの内容でしたね。 個人的には、1つのアプリで複数のウィンドウを開ける「Multiple Windows」機能が一番気になりました。 この記事では、WWDC 2019のセッション Introducing Multiple Windows on iPad と Architecting Your App for Multiple Windows を参考にしながらWEARへの仮実装を通して「Multiple Windows」を解説します。 解説 条件 Multiple Windowsは以下の条件で動作します。 Xcode 11以降に含まれるiOS 13.0 SDK以上でビルドされたアプリ iPadOSがインストールされているiPad Supporting Multiple Windows on iPad | Apple Developer Documentation WEARにMultiple Windows機能を実装する 今回、WEARに以下の5つのMultiple Windowsの機能を実装してみました。 複数のウィンドウを開く ドラッグ&ドロップで新しいウィンドウを開く ロングタップからのコンテキストメニューから新しいウィンドウを開く 以前開いていたすべてのウィンドウの状態を復帰させる 現在開いているすべてのウィンドウのUIを更新する これらすべてを実装するのはなかなか大変そうに思えますが、意外に少ないコードで実装できます。 その1. 複数のウィンドウを開けるように、Xcode 11以前で作ったアプリをMultiple Windowsに対応させる 実は、Xcode 11で作ったプロジェクトならば設定画面の「Supports multiple windows」にチェックを入れるだけで対応できます。 しかしながら、Xcode 10以前に作られたプロジェクトの場合はコードを足さないと対応できません。 WEARは現在、Xcode 10.3で開発しているのでコードを足す必要がありました。 と言ってもほんの少しのコードで対応できます。以下の3つを足すだけです。 (1)AppDelegate.swiftに以下のメソッドを追加します。 func application (_ application : UIApplication , configurationForConnecting connectingSceneSession : UISceneSession , options : UIScene.ConnectionOptions ) -> UISceneConfiguration { return UISceneConfiguration(name : "Default Configuration" , sessionRole : connectingSceneSession.role ) } (2)Info.plistに以下のコードを追加します。 <key> UIApplicationSceneManifest </key> <dict> <key> UIApplicationSupportsMultipleScenes </key> <true/> <key> UISceneConfigurations </key> <dict> <key> UIWindowSceneSessionRoleApplication </key> <array> <dict> <key> UISceneConfigurationName </key> <string> Default Configuration </string> <key> UISceneDelegateClassName </key> <string> $(PRODUCT_MODULE_NAME).SceneDelegate </string> <key> UISceneStoryboardFile </key> <string> BrowserViewController </string> // 最初に表示するStoryboard名 </dict> </array> </dict> </dict> (3)SceneDelegate.swiftを新規作成して下のコードをそのまま貼り付けます。 import UIKit class SceneDelegate : UIResponder , UIWindowSceneDelegate { var window : UIWindow ? func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { guard let _ = (scene as ? UIWindowScene) else { return } } } これだけです。アプリを起動し「Dockのアイコンを長押し」してみましょう。 これまで表示されなかった項目「すべてのウィンドウを表示」が現れるようになり、1つのアプリで複数のウィンドウを開くことができます。 ちなみに、これまでUIApplicationDelegateで使用していたライフサイクルメソッドは使えなくなります。そのあたりの処理はUISceneDelegateのsceneDidBecomeActive / sceneWillResignActive / sceneWillEnterForegroundなどに移管する必要があります。 その2. ドラッグ&ドロップで新しいウィンドウを開けるようにする 次は、「ドラッグ&ドロップで新しいウィンドウを開く」を実装してみましょう。 流れとしては、NSUserActivityの中へ新しいウィンドウに必要な情報を入れて生成しUIDragInteractionDelegateを利用してSceneDelegateの「scene(_:willConnectTo:options:)」を呼び出し、新しいウィンドウを作ります。 (1)info.plistに有効なNSUserActivityTypesを定義します。 <key> NSUserActivityTypes </key> <array> <string> wear.multiple.windows.coordinateDetail </string> </array> (2)UICollectionViewの場合は「viewDidLoad()」あたりに以下のコードを追加します。 collectionView.dragDelegate = self (3)UICollectionViewDragDelegateの「collectionView(_:itemsForBeginning:at:)」 で、新しいウィンドウを開くのに必要な情報をNSUserActivityのuserInfoに入れてUIDragItemに渡します。(NSUserActivityのactivityTypeは、先ほどinfo.plistで指定した文字列にすること) extension ViewController : UICollectionViewDragDelegate { public func collectionView (_ : UICollectionView , itemsForBeginning _ : UIDragSession , at indexPath : IndexPath ) -> [UIDragItem] { let userActivity = NSUserActivity(activityType : "wear.multiple.windows.coordinateDetail" ) activity.userInfo?[ "Info" ] = items[indexPath.item] //新しいウインドウを開く際に必要な情報をuserInfoに入れる let itemProvider = NSItemProvider(object : url ) itemProvider.registerObject(userActivity, visibility : .all) let dragItem = UIDragItem(itemProvider : itemProvider ) return [dragItem] } } UITableViewもUITableViewDragDelegateがあるのでほぼUICollectionViewと同じように実装できます。UIButtonやUIViewの場合はUIDragInteractionとUIDragInteractionDelegateを使えば可能です。 ここで1つ注意ですが、 NSUserActivityのuserInfoに入れてもよい のは以下のタイプのみで、それ以外はアプリが落ちてしまうので気をつけてください。 Each key and value must be of the following types: NSArray, NSData, NSDate, NSDictionary, NSNull, NSNumber, NSSet, NSString, or NSURL. The system may translate file scheme URLs that refer to iCloud documents to valid file URLs on a continuing device. (4)SceneDelegate.swiftに新しいウィンドウを開くための処理を追加します。 タブをウィンドウ外にドラッグ&ドロップした際にSceneDelegateの「scene(_:willConnectTo:options:)」が走るので、そこに新しくウィンドウを開く処理を追加すれば実装完了です。 func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { if let userActivity = connectionOptions.userActivities.first { if userActivity.activityType == "wear.multiple.windows.coordinateDetail" , let info = userActivity.userInfo?[ "Info" ] as ? NSString { let vc = BrowserViewController.instantiate() vc.info = info window?.rootViewController = vc } } } その3. ロングタップからのコンテキストメニューから新しいウィンドウを開く ボタンのタップをきっかけに新しいウィンドウを開く動きを実装したいという需要も多いと思います。この場合も少しのコードで実装できます。 ここでは、UICollectionViewのセルをロングタップしてコンテキストメニューを出し「新しいウィンドウを開く」ボタンを押して開く実装例をご紹介します。 (1)UICollectionViewの長押しをハンドリングしてコンテキストメニューを表示させます。(触覚タッチを使った例) extension ViewController : UICollectionViewDelegate { open override func collectionView (_ collectionView : UICollectionView , contextMenuConfigurationForItemAt indexPath : IndexPath , point _ : CGPoint ) -> UIContextMenuConfiguration ? { let actionProvider : ([UIMenuElement]) -> UIMenu ? = { _ in let openNewWindowAction = UIAction(title : "新しいウインドウで開く" , handler : { [unowned self ] _ in //新しいウインドウを開く処理を openNewWindow(indexPath : IndexPath ) }) return UIMenu(title : "コンテキストメニュー" , image : nil , identifier : nil , children : [openNewWindowAction] ) } return UIContextMenuConfiguration(identifier : nil , previewProvider : nil , actionProvider : actionProvider ) } } (2)新しくウィンドウを開くのに必要な情報をNSUserActivityに追加して、UIApplicationの「requestSceneSessionActivation:userActivity:options:errorHandler:」を実行します。 func openNewWindow (indexPath : IndexPath ) { let activity = NSUserActivity(activityType : "wear.multiple.windows.coordinateDetail" ) activity.userInfo?[ "Info" ] = items[indexPath.item] //新しいウインドウを開く際に必要な情報をuserInfoに入れる UIApplication.shared.requestSceneSessionActivation( nil , userActivity : activity , options : nil ) } (3)UIApplicationの「requestSceneSessionActivation(:userActivity:options:errorHandler:)」を実行するとUISceneDelegateの「scene(:willConnectTo:options:)」が走るので、NSUserActivityから情報を取得し、それを元に画面を生成させます。 func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { if let userActivity = connectionOptions.userActivities.first { if userActivity.activityType == "wear.multiple.windows.coordinateDetail" , let info = userActivity.userInfo?[ "Info" ] as ? NSString { let vc = BrowserViewController.instantiate() vc.info = info window?.rootViewController = vc } } } その4. 以前に開いていたすべてのウィンドウの状態を復帰させる これで大体のMultiple Windows機能は実装できましたが、今のままだとアプリを閉じてしまったら閉じる前の状態を復旧できず毎回初期値の状態で起動してしまいます。 以下のコードを足すことで以前開いていたウィンドウの状態を復帰できます。 (1)SceneDelegate.swiftに以下のメソッドを追加します。 @available (iOS 13.0 , * ) func stateRestorationActivity ( for scene : UIScene ) -> NSUserActivity ? { return scene.userActivity } (2)ViewControllerにウィンドウの状態を保存したいタイミングに以下のコードを追加します。 let userActivity = NSUserActivity(activityType : "wear.multiple.windows.coordinateDetail" ) userActivity.title = webView.title userActivity.userInfo?[ "Info" ] = info view.window?.windowScene?.userActivity = userActivity (3)SceneDelegate.swiftの「scene(_:willConnectTo:options:)」にウィンドウの状態を保存している「session.stateRestorationActivity」から情報を取得するコードを追加します。 func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { if userActivity.activityType == "wear.multiple.windows.coordinateDetail" , let info = userActivity.userInfo?[ "Info" ] as ? NSString { let vc = BrowserViewController.instantiate() vc.info = info window?.rootViewController = vc } } } 早速、複数のウィンドウを開き一旦閉じてもう一度起動してみましょう。ウィンドウの状態が復帰できます。 その5. 現在開いているすべてのウィンドウのUIを更新する 現在開いているすべてのウィンドウのUIを変更したい場合は当然出てくると思います。例えば、Safariならば新しくブックマークを追加した場合、すべてのウィンドウにそれが反映されますよね。 これまでならViewControllerに付き1つのウィンドウだけ管理しておけば問題ありませんでした。しかし、Multiple Windowsは、同じViewControllerのウィンドウが複数並ぶことになります。アクティブなウィンドウだけが更新されるのはまずいわけですね。 すべてのウィンドウを更新する方法として、 Introducing Multiple Windows on iPad と Architecting Your App for Multiple Windows で「UserDefaultsによるKVOで行う方法」と「NotificationCenterで行う方法」の2つの方法が紹介されていました。 ケースバイケースでどちらの手段を使うか決めましょう。 UserDefaultsを用いたKVOの場合 UserDefaultsを拡張しKVOで値の変更を監視して全ウィンドウを更新する方法です。 (1)KVO用のKeyPathを作るExtensionを用意します。 extension UserDefaults { private static let isToolbarHiddenKey = "isToolbarHiddenKey" @objc dynamic var isToolbarHidden : Bool { get { return bool(forKey : UserDefaults.isToolbarHiddenKey ) } set { set (newValue, forKey : UserDefaults.isToolbarHiddenKey ) } } } (2)設定画面で、isToolbarHiddenを変更する処理を追加します。 UserDefaults.standard.isToolbarHidden = ! sender.isOn (3)NSKeyValueObservationをインスタンスで保持して、ViewControllerの「viewDidLoad()」に以下のコードを追加します。 private var observer : NSKeyValueObservation ? observer = UserDefaults.standard.observe(\UserDefaults.isToolbarHidden, options : .initial, changeHandler : { [weak self ] (_, _) in self ?.navigationController?.setToolbarHidden(UserDefaults.standard.isToolbarHidden, animated : true ) }) NotificationCenterの場合 ViewControllerが受け取ったイベントをModelControllerに送り、ModelControllerがModelを更新したら、各ViewControllerに通知してUIを更新する方法です。 (1)イベントを更新した際に通知をPostするデータ型を用意します。 enum UpdateEvent { case DeleteFavorite static let DeleteFavoriteName = Notification.Name(rawValue : "DeleteFavorite" ) func post () { switch self { case .DeleteFavorite : NotificationCenter. default .post( name : UpdateEvent.DeleteFavoriteName , object : self ) } } } (2)ModelControllerのメソッド内で処理を完了際に通知を送信する処理を追加します。 final class ModelController { func deleteFavorite () { // 削除処理 let event = UpdateEvent.DeleteFavorite event.post() } } (3)ViewControllerにModelControllerに追加したメソッドを実行する処理を追加します。 extension ViewController { private func deleteButtonAction () { modelController.deleteFavorite() } } (4)ViewControllerに通知を受信する処理と通知を受信した際に行う処理を追加します。 extension ViewController { private func setupNotification () { NotificationCenter. default .addObserver( self , selector : #selector(reload), name : FolderDetail.UpdateEvent.DeleteFavoriteName , object : nil ) } } @objc private func reload (notification : Notification ) { // 通知を受け取った際に行いたい処理を追加 } 実行すると、すべてのウィンドウにUIの変更が反映されます。 まとめ Multiple Windowsは、UIの実装はそれほど難しくないですが、データ保存などの設計をMultiple Windowsありきで考える必要があると感じました。 考えられるケースとしては、AとBのウィンドウで使用しているデータの保存先が同じでそれぞれリアルタイムに更新している場合ですね。 どちらを優先させるのか。またはウィンドウ別に保存させるようにするのか。アプリによってどれが最適なのかを考える必要があり、なかなかすぐに対応するのは難しいのではないかと感じました。 ただ、やはりMultiple Windowsが使えるようになれば使い勝手がとても良くなるのは間違いありません。WEARであれば、コーデの検索画面と詳細画面を一度に確認できたり、お気に入りフォルダの中身を確認しながらコーデを追加できたりと、アプリを巡るスピードが飛躍的に上がりましたので。 最後に、Multiple Windows機能をWEARに仮実装して感じたポイントを3つにまとめました。 Multiple WindowsのUIの実装はそれほど難しくない。ただし、1つのViewControllerを複数のウィンドウで開くことになるので、UIの更新やデータの保存には注意する必要あり。 NSUserActivityに必要な情報を渡し、それを用いて新しいウインドウを開く。ただし、NSUserActivityに渡せるタイプは限られているので注意すること。 AppDelegateのライフサイクルメソッドは使えなくなるので、SceneDelegateのライフサイクルメソッドを用いる必要がある。 この記事がMultiple Windows実装の一助になれば幸いです。 参考 Introducing Multiple Windows on iPad - WWDC 2019 - Videos - Apple Developer Architecting Your App for Multiple Windows - WWDC 2019 - Videos - Apple Developer Supporting Multiple Windows on iPad | Apple Developer Documentation Multiple Windows - System Capabilities - iOS - Human Interface Guidelines - Apple Developer さいごに ZOZOテクノロジーズは、iOSエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは、開発部SREの田島です。私達のチームでは、JP1というツールをワークフローエンジンとして利用しています。JP1のジョブの監視は今まで外部にお願いしていましたが、異常が発生してからすぐ連絡してもらうことができない等の問題がありました。そこでJP1の機能とClojureで作成したスクリプトを組み合わせて異常検知の仕組みを作成しました。本記事ではJP1を使っている背景、並びに異常検知の仕組みをどのように実現したかについてご紹介します。 背景 JP1利用の背景 冒頭でも紹介しましたが私達のチームでは日立製作所が開発・発売しているJP1というツールをワークフローエンジンとして利用しています。多くのチームではDigdagやGCPのCloud Composerが主にワークフローエンジンとして採用されています。 実際私達のチームでもDigdagをフル活用しており、その運用の知見も得られているためワークフローエンジンはDigdagに集約するように動いています。そんな中JP1は約9年前に導入され様々なワークフローがその上で組まれています。そのため簡単にはDigdagに移行できません。 さらにJP1で稼働中のシステムは私達SREチームではなく別のチームが管理しているという状態でした。そこでまずはJP1の運用をSREチームで引き継ぎ、JP1自体の仕組み並びにJP1上で動いているシステムを理解することからはじめました。 JP1ジョブの異常検知 SREチームで運用を引き継ぐため、まずアラート対応に参加しました。そこで早速問題が発生しました。私達のチームでは基本的にSlackでのアラートをフックに当番がそのチャンネルにログを残しつつ対応するというフローをとっています。 しかしJP1のアラートは監視を外部にお願いしており、人間が異常かどうかを判断して電話またはメールで運用担当者に連絡してもらうというフローとなっていました。人間が異常かどうかを判断して連絡してもらうというフローのため異常が発生してから連絡まで最大で30分〜60分かかることがありました。 そこでまずは機械的にJP1の異常を検知し、Slackへ通知することにしました。 JP1について まずSlack通知をする上でJP1がどのような状態になったら通知するかについて検討する必要があります。しかしJP1を触ったことがまったくなかったので、最初にJP1を使った「Hello World」に挑戦しました。 ジョブネット まずはじめに、JP1にはジョブネットという概念があります。ジョブネットとは後ほど説明するジョブを集めたものです。GUI上でジョブをどのような順番で実行するかと言ったDagを作成できます。 ジョブ 次にジョブです。JP1では1つ1つの処理がジョブと呼ばれています。ジョブは以下のような画面から設定でき、実際に実行するコマンドやログの出力先などを指定できます。 以上は、hello.batを実行するような設定になっています。batファイルの中身は以下のとおりです。 @echo off setlocal enabledelayedexpansion cd %~dp0 echo "hello world!" ジョブネットグループ 最後にジョブネットグループです、これはジョブネットをまとめてグループ化する概念となっています。よって階層的には「ジョブネットグループ ∋ ジョブネット ∋ ジョブ」となります。 ジョブネットの実行 最後に設定したジョブネットを実行してみます。実行するには「実行を登録」という機能を使って即時実行、またはスケジューリング実行することが可能です。今回は即時実行してみます。実行の結果以下のようなログが指定したファイルに吐き出されました。 hello world! 以上でJP1でのHello Worldが成功しました。 ここからJP1での異常を検知するには、ジョブネットが異常終了した時Slackに通知すればよいことがわかりました。またジョブネットが正常終了した場合にも別のSlackチャンネルに通知できれば便利かなと考えました。 ジョブネットの異常検知 次にジョブネットが失敗した場合にどのようにSlack通知をするかを考える必要があります。ジョブが失敗した場合に外部へ通知するという機能があればいいのですがそのような機能はありませんでした。 そこで、JP1のイベントをきっかけにジョブを実行する機能があるのでそれを利用することにしました。 JP1イベント JP1はJP1上で起きたイベントをフックにしてジョブを起動する機能があります。イベントの一覧は こちら のリンクにまとめられています。 今回は以下のイベントが利用できそうです。 ジョブネット異常終了イベント ジョブネット正常終了イベント ジョブネット開始遅延イベント ジョブネット終了遅延イベント JP1のイベントをきっかけにジョブを実行する JP1では通常のジョブとは別にイベントジョブといった特殊なジョブをdagの中に組み込むことができます。以下がそのジョブと設定できる項目です。 紹介したリンクのイベントIDを指定することで、IDに紐づくイベントが発生したタイミングでジョブが実行されます。後続に通常のジョブを設定しておくことで、特定のスクリプトをあるイベントが発生したタイミングで実行が可能となります。ただしこのやり方だとジョブネットが実行中のタイミングでしかイベントを検知できません。そのためイベントを1回だけしか検知ができません。 JP1イベントのたびにジョブネットを実行する 以上の問題を解決するために、イベント受信監視という機能を利用します。イベント受信監視を利用するには「起動条件の設定」を利用します。 「起動条件の設定」を作成すると以下のようなジョブネットの条件フィールドが通常のジョブネットと並列に作成されます。フィールドにイベントジョブを並べてやることで、イベントが発生する毎に並列のジョブネットが実行されます。 条件フィールドの中のイベントはOR条件またはAND条件を指定することが可能です。下記ではOR条件となっているため、指定したイベントの中の1つでも当てはまればジョブが起動します。 実行登録時に、登録方法を「即時実行」起動条件を「設定されていれば使用する」に指定することで監視が開始されます。ジョブネットの状態が監視中となっていればイベントが監視されている状態となっています。 最終的なJP1の監視設定 それでは実際に監視ジョブを作成してみます。まず以下のように監視用のジョブネットを作成し、イベント監視を含むジョブを作成しました。 また、発火するイベントは以下のように設定しました。同じ条件が2つあることについては後ほど説明します。 環境変数の受け渡し イベントから発火されたジョブでは こちら のリンクに記載されているパラメータを利用できます。 しかし、このパラメータはJP1上でのマクロ変数として渡されます。マクロ変数を環境変数に変換することで起動されるスクリプトの中で利用できます。 「最終的なJP1の監視設定」の中の環境変数という項目でマクロ変数を環境変数に変換しています。以下のようなフォーマットで環境変数に指定することで、マクロ変数の値を環境変数に指定できます。 環境変数名=?AJS2マクロ変数? 無限イベント検知問題 JP1では発生したイベントに対して、イベントメッセージを正規表現のマッチングにより絞り込むことができます。絞り込みを利用しないと、イベントからキックされたジョブ自体も監視の対象となるため無限に正常終了の通知がSlackに流れてしまうという問題が発生しました。メッセージを確認すると、必ずジョブネットグループの名前が含まれていることがわかりました。そこで、監視用のジョブネットグループ「MONITORING」を作成し、イベントの文言に「MONITORING」が含まれていないという条件にすればいいと考えました。しかし、否定がJP1の正規表現では使えませんでした。 そこで最終的に、監視したいジョブネットグループの名前が含まれている場合にのみイベント発火を行うことで無限イベント検知を回避しました。また、JP1の正規表現ではOR条件も利用できないためイベント毎にジョブネットの数だけイベントジョブを用意してやる必要があります。 先程の例で2つずつイベントジョブが定義されていたのもこれが理由です。 実際の設定ではイベントジョブのメッセージの部分に正規表現で条件を書きます。以下はメッセージにTEST_WORKが含まれている場合にイベントを発火することを表しています。 Slackへの通知スクリプト 言語選定 イベントから発火されたジョブでは こちら のリンクにあるパラメータを環境変数に渡すことで利用できると紹介しました。それらの情報を利用して文言を組み立てSlackに通知します。 上記を実現するためにはスクリプトを作成する必要があります。 P1はWindows上で動作しているため、VBScriptやPowerShell・Windowバッチしかデフォルトでは利用できません。しかし、これらの言語は使い慣れていないかつ、複雑になりそうだったため採用しませんでした。 調べてみると、JP1から呼ばれているジョブはJavaで作成されていることがわかりました。よってJVM言語であれば追加でライブラリをインストールせずに利用できます。そこでJVMで動く言語を利用してスクリプトを作成することにしました。 Clojureの採用 JVMで動作する言語の中で以下の理由からClojureを採用しました。 独立した小さなスクリプトである 普段業務で使っていない言語である 普段使っている言語とパラダイムがぜんぜん違う 筆者がClojure好き まず今回作成するスクリプトは独立した小さなものなので、新しい技術スタックを試すには絶好のチャンスと考えました。 チームの中には新しい言語に触れるという事に抵抗がある人もいます。そこで、普段業務で使っていない言語を利用することでチームメンバーへの刺激になると考えました。 またClojureはS式や関数型といった特徴を持っています。このような普段使っている言語とはパラダイムがぜんぜん違う言語を使うことでチームメンバーへの新たな発見につながるのではと考えました。 そして何よりも筆者がClojure好きというのも採用に大きく影響しています。 Clojureを採用するデメリットとしては他の人が運用できず負債になるかもしれないというものが挙げられました。しかし、小さなスクリプトなので最悪まるっと別言語で書き換えられる、いつかはDigdagにワークフローエンジンを移行したいという理由から新しい技術スタックに挑戦することを優先しました。 ソースコード 以下がJP1からキックされるbatファイルと、batファイルから実行されるClojureスクリプトになります。 Clojureは Leiningen というツールで管理しています。 notification.bat @echo off setlocal enabledelayedexpansion cd %~dp0 echo "event" java -jar F:\JP1Notification\jp1-notification\target\uberjar\jp1-notification-0.1.0-SNAPSHOT-standalone.jar project.clj ( defproject jp1-notification "0.1.0-SNAPSHOT" :dependencies [[ org.clojure/clojure "1.10.0" ] [ clj-http "3.10.0" ] [ org.clojure/data.json "0.2.6" ]] :main ^ :skip-aot jp1-notification.core :target-path "target/%s" :profiles { :uberjar { :aot :all }} :plugins [[ lein-shell "0.5.0" ]] :shell { :env { "EVMSG" "KAVS0262-E ジョブネット(STK-MGR:/TEST_WORK/TAJIMA_TEST_HELLO:@P2944)が正常終了しました" "EVID" "4102:0" "EVDATE" "2019/10/23" "EVTIME" "15:32:29" "EVHOST" "hostname" "EVIPADDR" "192.168.111.111" }}) core.clj ( ns jp1-notification.core ( :gen-class ) ( : require [ jp1-notification.slack : refer [ send -to-slack ]] [ jp1-notification.event : refer [ event ]])) ( defn -main [& args ] ( send -to-slack ( event ))) event.clj ( ns jp1-notification.event ) ; event: http://itdoc.hitachi.co.jp/manuals/3020/30203S0733/AJSJ0164.HTM (def error-event-list [ "4107:0" "4123:0" "4127:0" ]) (def jobnet-group-production [ "Application" ]) ( defn get -jobnet-group [ message ] ( second ( re-find ( re-pattern "STK-MGR:/(.+?)/" ) message ))) ( defn get -event-level [ event-id ] (if ( .contains error-event-list event-id ) :error :info )) ( defn production? [ jobnet-group ] ( .contains jobnet-group-production jobnet-group )) ; env: http://itdoc.hitachi.co.jp/manuals/3020/30203K2143/AJSF0177.HTM ( defn event [] (let [ message ( System/getenv "EVMSG" ) event-id ( System/getenv "EVID" ) jobnet-group ( get -jobnet-group message )] { :event-id event-id :message message :date-time ( str ( System/getenv "EVDATE" ) " " ( System/getenv "EVTIME" )) :host ( System/getenv "EVHOST" ) :ip ( System/getenv "EVIPADDR" ) :jobnet-group jobnet-group :event-level ( get -event-level event-id ) :production? ( production? jobnet-group )})) slack.clj ( ns jp1-notification.slack ( : require [ clj-http.client :as client ] [ clojure.data.json :as json ])) (def slack-url "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ) (def production-channel-alert "#alert_cahannel" ) (def production-channel-notice "#notice_channel" ) (def test -channel "#test_channel" ) (def info-slack-title "[INFO] From JP1" ) (def error-slack-title "<!channel>\n[ERROR] From JP1" ) ( defn channel [ jobnet-group event-level production? ] (if -not production? test -channel (if ( = event-level :error ) production-channel-alert production-channel-notice ))) ( defn title [ event-level ] (if ( = event-level :error ) error-slack-title info-slack-title )) ( defn color [ event-level ] (if ( = event-level :error ) "danger" "good" )) ( defn payload [{ : keys [ event-id message date-time host ip jobnet-group event-level production? ]}] { :channel ( channel jobnet-group event-level production? ) :username "JP1 Event" :attachments [{ :color ( color event-level ) :title ( title event-level ) :text ( str "```" message "```" ) :fields [{ :title "DateTime" :value date-time : short true } { :title "Host" :value ( str host " (" ip ")" ) : short true }]}]}) ( defn send -to-slack [ eve ] ( client/post slack-url { :form-params { :payload ( json/write-str ( payload eve ))}})) 開発時の環境変数 Clojureのスクリプト開発の際にも今回1つ工夫をしています。開発時でもJP1から渡される環境変数を擬似的にClojureに渡してやる必要があります。そこで、今回は lein-shell というライブラリを使うことでそれを実現しています。 まずproject.cljの plugins に lein-shell を追加します。そして以下の行を追加し、実行コマンドの前に lein shell を付与してやることで環境変数がセットされます。 :shell { :env { "EVMSG" "KAVS0262-E ジョブネット(STK-MGR:/TEST_WORK/TAJIMA_TEST_HELLO:@P2944)が正常終了しました" "EVID" "4102:0" "EVDATE" "2019/10/23" "EVTIME" "15:32:29" "EVHOST" "hostname" "EVIPADDR" "192.168.111.111" }} 通知結果 実際の通知は以下のようになりました。上が異常終了時、下が正常終了時の通知です。期待通り通知されていることがわかります。 異常検知を自動化してどうなったか 最後に異常検知を自動化しSlack通知にすることで以下のようなメリットが見られたので紹介します。 異常の発生にすぐに気づくことができるようになった 今どのようなアラートが発生していて誰が対応しているのかがわかりやすくなった 過去のアラートが検索できるようになった JP1自体に詳しくなった アラート対応についての現状の見直し・改善のきっかけとなった まずJP1のジョブの異常が発生してから連絡が来るまでに時間がかかるという問題がありました。この問題は自動検知の仕組み導入により、リアルタイムで異常に気づくことができるようになりました。また、Slackにすべてのアラートが集約されるようになったため、対応のログがすべてSlack残ります。これにより今誰がアラート対応をしているのか、過去にどんなアラートが起こったのかを追うのが簡単になりました。 またこれは副作用ですが、JP1に異常の自動検知の仕組みを入れるためにはJP1自体を詳しく知る必要がありました。これによりシステム改善の第一歩としての現状把握に大きな貢献となりました。 そして何よりも、これをきっかけにチームでのアラート対応についての見直し・改善のきっかけに繋がりました。 今後の展望 アラートの異常検知を自動化することで様々なメリットが得られました。しかしまだまだ問題はあります。今問題となっているのは、夜中の大きなアラートに気づけないというものです。それを解決するための1つとしてPagerDutyの導入を検討してます。 またアラート対応者が誰かに偏っている、知見が誰かに偏っているといった問題があります。これについては、当番制度の見直しなどをチームで日々話し合い改善を行っています。 まとめ 以上で見たように、弊社には改善すべきことがたくさん眠っています。自分の技術スタックとはぜんぜん違う領域に対しても、積極的に挑戦し改善していけるようなエンジニアを募集しています。 tech.zozo.com あと、Clojureのコードレビューもお待ちしております。
アバター
こんにちは、開発部SREチームの西郷です。普段は生産管理のシステムやWEARのインフラに携わっています。弊社では多数のサービスでAWSを採用しており、更なる利活用と学びのため、12/2〜12/6に開催された AWS re:Invent 2019 へ参加してきました。本記事では、Keynoteの内容に加え、一緒に参加した開発部のメンバーが気になったSessionやWorkshop等についてもレポートしたいと思います。 AWS re:Inventとは Keynote KYN201: Monday Night Live KYN202: Keynote by Andy Jassy KYN203: Global Partner Summit Keynote KYN204: Keynote by Dr.Werner Vogels Session SEC207-L: Leadership session: AWS identity CMY303: Using Amazon CloudFront, AWS WAF, and Lambda@Edge to keep spammers out MOB317: Speed up native mobile development with AWS Amplify CMP323-R: [REPEAT] Optimize compute for performance and cost using AWS Compute Optimizer Workshop MGT305: Patch Compliance Using AWS Systems Manager And AWS Config AIM223-R6 - AWS DeepComposer: Get started with generative AI まとめ AWS re:Inventとは 1年に1回ラスベガスで行われるAWS最大のカンファレンスで、8回目の今年は65,000人以上が参加しました。AWSの新サービスや既存サービスのアップデート発表が目玉ではありますが、3,000以上の技術セッションやワークショップ、ハッカソン等が用意されている学習型カンファレンスです。6つのホテルで構成された会場が"Campus(キャンパス)"と呼ばれていることからも、学習の場として強く位置付けられていることがわかります。 Keynote KYN201: Monday Night Live 新規事業部の茨木です。専門はフロントエンドですが、プラットフォームにとらわれずプロトタイプ開発や技術検証に取り組んでいます。最初のセッションにあたるこのKeynoteでは、Amazon社の大規模計算に関する取り組みが紹介されました。大規模計算を行うスーパーコンピュータは専用のネットワークにより接続された複数のサーバーから構成されます。複数台のサーバーによる大規模な計算を高速・安全に実行するための具体的な取り組みとして、以下が紹介されました。 サーバー間ネットワークの改善 EC2の仮想化に最適化されたハードウェア(Nitro Controller)の導入 アプリケーション通信の改善(EFA) 現状、弊社でスーパーコンピュータを利用する場面はありませんが、機械学習などでは分散コンピューティングが登場します。この発表で紹介されたネットワーク改善のアプローチは、機械学習における計算速度の向上に寄与するかもしれません。 KYN202: Keynote by Andy Jassy SREチームのテックリード光野です。普段は、弊社におけるAWS利用のフォローやアーキテクトをしています。re:Inventでは例年3回のKeynoteが開催されますが、その中でもDay2はAWS CEO Andy Jassyが登場し、会期中もっとも盛り上がる舞台の1つです。今年も大変多くの、そして大きな発表が行われました。 まず、例年以上にデジタル・トランスフォーメーションについての言及が多かったように感じました。エンタープライズにおけるオンプレミスからクラウドへの移行、そして「オンプレミス」の壁を壊すためのリリース報告が行われました。 弊社にも倉庫という巨大な資産があり、その中で動くシステムがまた存在します。ハードウェア・ソフトウェア両面で、耐用年数を迎えれば入れ替える必要があるものの、一般的なWebサービスと同じ手段で入れ替えることはできません。 AWSのインフラをオンプレミスに持ち込むOutposts、リージョンの枠を超え地理的に近い場所へAWSを展開するLocal Zones、5GのエッジコンピューティングとなるWavelength。オンプレミスとクラウドの垣根をなくすプロダクトがあることは、将来の選択肢が増えるという点で、とてもうれしく思います。 そして、昨年に引き続き機械学習周りのリリースが怒涛のように行われました。IDEのSageMaker Studio、実験管理を行うSageMaker Experiments、モデルを決定づける要素を調査するSageMaker Debuggerなど。Keynote外での発表も含めればSageMakerだけで10近い新サービス・機能が発表されているのではないでしょうか。弊社はAWS Summit Tokyo 2019にて SageMaker利用における工夫 を発表させていただきましたが、殆どがマネージドで提供されてしまったのではと感じるほどのアップデートです。 最後に、機械学習などに比べれば一見地味ではあるものの、Nitro Systemによるネットワークの高速化やAQUA for Amazon Redshiftによるスループットの改善など増大するデータを正しく快適に扱う下支えについての発表が何よりも魅力的に映りました。 今後もユーザーが必要とするサービス・機能、そしてユーザーを快適にする改善の両面で目が離せないと感じさせる、そんなKeynoteでした。 KYN203: Global Partner Summit Keynote SREチームの指原です。3日目のKeynoteはAWSのHead of Worldwide Channels & AlliancesであるDoug YeumによるGlobal Partner Summitです。本KeynoteではAWSが支える、そしてAWSを支える数多くのパートナー企業に関して事例の紹介とアップデートがありました。 事例としてエネルギー事業を展開しているBP社と、レンタカー事業を展開しているAvis Budget Group社がそれぞれ登壇しました。両社は自社の大規模なシステムをオンプレミスからクラウドへ移行しています。また、単に移行するだけではなく例えばAmazon SagaMakerなどAWSのサービスをフルに活用することで、システムパフォーマンスの向上に成功しているそうです。そのようなダイナミックなデジタルトランスフォーメーションを実現できたのはAWSが認定している技術支援を行うパートナーネットワークのおかげだということでした。 弊社では事例にあったような技術支援を行うパートナー企業との取り組み自体は行っていません。しかし、社内ではZOZOTOWNやWEARのリプレイスという大きなプロジェクトが進んでおり、そのようなプロジェクトではオンプレミスからクラウドへの移行も含んでいます。そこでは事例にあったようにオンプレミスのサーバーを単にEC2に移行するようなことではなく、実際にEKSやFargateを使って再構築を行い従来より柔軟なスケールアウトや運用負荷の低減が実現しています。クラウド移行の際にはサービスをフル活用して効率化を図るという点が共感したところでした。 また、パートナーネットワークについては普段あまり意識したことはなかったのですが、多くの大企業が既に抱えているオンプレミスやレガシーなシステムをクラウドに移行する支援だったり、スタートアップが成長を加速するための支援だったり、多くのパートナー企業が存在しており、AWSが持つプラットフォーマーとしてのエコシステムの巨大さに改めて気付かされました。 KYN204: Keynote by Dr.Werner Vogels SREチームの西郷です。4日目に行われたこのKeynoteはAmazon社のCTOであるDr.Werner Vogelsが登場し、AWSのサービスを技術的に支える部分やAWSがこれまで行ってきたアーキテクチャへの投資についてがトピックでした。 仮想化の観点では、新しいハイパーバイザであるAWS Nitro Systemによるパフォーマンス向上の話がありました。Nitro Systemを搭載したC5系のインスタンスはベアメタルに近いパフォーマンスを発揮できるそうです。また、re:Inventの前半で発表された Nitro Enclaves はNitroを使用して、AWS KMSと統合したローカル接続のみに隔離された領域を提供します。つまり、EC2インスタンス内で個人情報等の機密性の高いデータを保護し、安全に処理できるようになります。 サーバーレスの観点では、FargateやLambdaの裏側で動いているFirecrackerの話がありました。Firecrackerは昨年のre:Inventで発表されたマイクロVMで、セキュリティはもちろん速度にもフォーカスして設計されています。今回のre:InventでFargate on EKSが発表されましたが、EC2と比較して軽量なFargateが使えるようになったことで高負荷時のオーバープロビジョニングが大幅に軽減されると説明がありました。 弊社サービスでもEC2やLambda、Fargateを採用しているため、使える技術の選択肢が増え、より安全で高パフォーマンスな環境になることは非常に嬉しく楽しみです。 また、新しいサービスとして The Amazon Builders' Library が発表されました。これまでAWSが自社のサービスを開発・運用する中で培ってきたナレッジのライブラリで、どれもシニアテクニカルリーダーが書いているそうです。より簡単にそういったテクニカルな情報にアクセスできるようになったことは開発者としてありがたく、早速目を通してみたいです。 最後に、本KeynoteでもAWSを使ってビジネスに変革を起こそうとしている企業の取り組みがいくつか紹介されたのですが、Volkswagen社のThe Volkswagen Industrial Cloud(工場のクラウド化プロジェクト)はその規模の大きさに目を惹かれました。グローバル全体で1つのアーキテクチャのクラウドにあらゆる生産工程を接続させる構想で、いずれは他の企業にも公開しOSSのエコシステムのようなものにしたいとのことでした。 全体的には、ゼロトラストネットワークの概念に法って開発されたAWS Nitro SystemやIndustry 4.0を意識したVolkswagen社の取り組みなど、クラウドの新たなフェーズを感じられるKeynoteでした。 Session SEC207-L: Leadership session: AWS identity 改めまして、SREチームの指原です。昨今、ゼロトラストネットワークというセキュリティの概念が広がってきており、個人情報の観点でもグローバルなサービスではGDPRなど法的に対応する必要性が出てきたりしています。そのようにインターネットやクラウドの世界でもセキュリティや情報保護に関して次々と大きな変化が起きつつあると感じています。このセッションではそのような変化に対応するためのAWSでのアクセス制御、リソース管理について新機能の紹介があるということで参加しました。 本セッションの内容をサマライズすると主に以下の4つについてでした。 AWS Organizationsを利用することで、複数のAWSアカウント全体の一元管理が可能 AWS SSOを利用することでユーザーアクセスの集中管理が可能 タグを利用することで請求の管理ができるだけでなく、セキュリティポリシーやアクセス制御の管理が可能 新サービスのIAM Access Analyzerを利用することで不必要なアクセス制御の許可を減らすことが可能 いくつか小さなアップデートと共に今回新しくリリースされた機能としては4つ目のIAM Access Analyzerが目玉として紹介されていました。IAM Access Analyzerは現在以下の5つのリソースに対応しており、これらのリソースに対して意図した通りのアクセス制御になっているかを分析ができます。 S3 IAM Role KMS Lambda SQS 特徴としては細かい設定をする必要がなく簡単に有効化でき、利用料金も必要ないという点です。また、IAM Access Analyzerはパターンマッチングなどに基づいて分析を行っているわけではなく、自動推論と呼ばれる数学的分析を使っているそうです。その結果、数千ポリシーを数秒で分析ができるパフォーマンスを発揮できるようです。 IAM Access Analyzerが対応している上記の5つのリソースはそれぞれ誤った設定をすると不要な外部公開をしてしまう可能性のあるサービスです。そのような誤った外部公開設定をしていないかどうかを重点的に確認できる機能がIAM Access Analyzerです。 弊社では数十のAWSアカウントを管理しており、GuardDuty等を利用して全アカウントを対象にしたセキュリティ的な脅威検知については担保しています。しかし各リソース単位での細かいセキュリティポリシーに関しては基本的には利用プロダクトに運用を一任しています。そのような事情から個別のリリースに対して外部公開設定されていないかどうをチェックすることが簡単にできるようになったことは非常に便利で、さっそく活用していきたいです。 CMY303: Using Amazon CloudFront, AWS WAF, and Lambda@Edge to keep spammers out 改めまして、SREチームの西郷です。このセッションは、あるWebサイトでユーザー登録数がスパイクしたケースにおけるCloudFront、WAF、Lambda@Edgeを使った対処法の紹介だったため、参考にできるものがあればと思い参加しました。 まとめると以下の内容でした。 前提 スパイクしたのは非営利団体のシステムで、ブログのようなもの(ユーザーがコンテンツを追加したりコンテンツにコメントしたりする) ユーザー登録のプロセスはメールアドレス認証。メールアドレスとキャプチャを入力、オプションで質問に対する答えを入力する 当初のインフラ構成はユーザーからの窓口としてCloudFront、後ろにサーバーのEC2とエラーページや静的ファイルが入ったS3バケット 起こったこと ユーザー登録数がスパイクした(通常は1日1回もしくは2、3日に1回の頻度だったが、1分に1回になった) ユーザー名とメールアドレスが普通じゃなかったので怪しいものだと気づいた 実際に行った対処 ほとんどがロシアまたはウクライナからのアクセスだったので、まずはCloudFrontで地域制限をかけた 地域制限で弾けなかったIPアドレスからの攻撃はWAFのセキュリティオートメーションで提供されているIP Reputation Listsで弾くようにする それでも弾けないものは怪しいIPアドレスかどうかをLambda@Edgeで判定し、怪しい場合は303を返してセキュリティオートメーションで提供されているBad Botのブラックリストに追加する その結果 2回目以降のアクセスはブロックされるようになり、スパイクは99%収束した Bad Botのブラックリストには1341個のIPアドレスが追加された(IP Reputation Listsで防げなかった数) このSessionで知見になったのは、AWS Solutionsで提供されている AWS WAF セキュリティオートメーション の存在です。これは一般的なWebベースの攻撃をフィルタリングするように設計されたWAFのルールセットを自動的にデプロイできるもので、CloudFormationで提供されています。今回のセッションで取り上げられた2つのコンポーネントの機能を調べたところ、以下のようなものでした。 IP Reputation Lists: CloudWatchをトリガーにしてLambdaがサードパーティのIP評価リストを定期的にパースし、WAFのIPリストを更新する Bad Bot: API GatewayとLambdaを使ったハニーポットで、Lambda FunctionがIPアドレスを検出するために検査を行いブロックするIPリストに追加する AWS WAF セキュリティオートメーションというソリューションが提供されていることや、AWS Solutionsというサービスがあることを知らなかったので参考になりました。弊社でもWAFを使用していますが、事前にブラックリスト等を設定しておく、ということはしていないため、ブロックしたいIPアドレスが発生する毎に手動で対応を行なっています。あらかじめブラックリストをルールに適用しておくと、より安全&運用コストの削減に繋がりそうだと感じたので、機会があれば活用していきたいです。 MOB317: Speed up native mobile development with AWS Amplify フロントエンドエンジニアの権守です。AWS Amplifyについては社内で導入を検討したことがなかったので、今後の選択肢の1つとして考えられるように使い方を学べるこのセッションに参加しました。 AWS Amplifyは、モバイルアプリケーション向けにバックエンドを提供するものです。API・認証・機械学習を使った予測などの機能から必要なものだけを選んでバックエンドを構築・利用できます。また、iOS・Android・Web・React Nativeアプリ向けにフレームワークを提供しており、UIコンポーネントや各機能を呼び出すインタフェースが用意されています。 セッションの内容はデモを中心としたもので、Amplify CLIを使ったGraphQL APIの構築や、iOS・AndroidのSDKを使った実装を見ることができました。具体的な実装としては、認証画面の呼び出し、機械学習による翻訳・ラベル付けを見ることができました。 こちらは画像から写っているものを特定して、ラベルとその信頼度を出力しているものです。 セッションを通して、利用のイメージが湧いたのでGraphQLを利用するプロトタイピングなどで試してみたいと思います。モバイル開発向けのAWS公式ブログにも AWS Amplifyの導入記事 が公開されたので興味の湧いた方には併せて読むことをお勧めします。 CMP323-R: [REPEAT] Optimize compute for performance and cost using AWS Compute Optimizer バックエンド・SREチームのテックリード児島です。ZOZOSUITS及びZOZOMATの計測システムのアプリケーション構築及びアーキテクチャ設計を担当しています。 re:Inventには昨年から参加し、今年で2年目となります。例年、EC2には様々なアップデートがあり、今ではその選択肢は270以上にもなるそうです。そのような状況下で、アプリケーションに最適なインスタンスを選定する知識は、年々複雑化しています。今年も開催の1か月前には、Savings Planの発表がありましたが、これに合わせ節約術のベストプラクティスもまた年々多様化していると言えます。re:Inventでは、その節約術提供のためCost Optimizingをテーマとしたセッションが毎年いくつか用意されており、現場で活用できる知見を深めることができます。昨年も、以下のセッションに参加しました。 「 Run Production Workloads on Spot, Save up to 90% (CMP306-R1) 」 「 Amazon EC2 T Instances – Burstable, Cost-Effective Performance (CMP209) 」 CMP209では、本番環境にも積極的にT系インスタンスを採用することを勧める内容で、とても興味深いものでした。 さて、CMP323-Rでは、Cost Explorerに今夏追加されたResource Optimization Recommendationsと比較して、新機能であるAWS Compute Optimizerが紹介されていました。Cost Explorerでは同一のインスタンスファミリアの中からしか最適なインスタンスタイプの推奨がなされませんでした。 これに対し、AWS Compute OptimizerではM/C/R/T/Xの5つのインスタンスファミリアを跨いで、140以上の組み合わせの中から最適なインスタンスタイプが推奨されるそうです。推奨にはMachine Learningが使われており、CPU使用率、ディスクI/O、ネットワークI/Oなどのメトリクスから、より精緻な個別の状況に応じた推奨がなされるそうです。現在は、東京リージョンでの使用は開始されていないようですが、今後のアップデートが待ち遠しいです。 実際の現場において、これまでも新しいインスタンスタイプが追加される度に、利用者がその採用によるパフォーマンスを評価することはとても大きな負担だったと思います。それにより導入が遅れたり、その評価に多大な時間が奪われるなど、大きな課題を生んでいたように思います。しかし、今回のAWS Compute Optimizerは、これらの問題を解決してくれるツールとなり得そうです。今後の益々の拡張に大きな期待をしたいと思います。 Workshop MGT305: Patch Compliance Using AWS Systems Manager And AWS Config 再び光野です。1エンジニアとしては、開発速度の底上げを図れるようなデプロイやガバナンスの仕組み作りに携わっています。re:Inventでは手を動かすWorkshop形式のセッションばかり回っており、ここで取り上げるMGT305もWorkshop形式です。課題の1つであるパッチの運用を解決すべく参加しました。常々パッチの管理と適用を自動化したいと考えており、糸口でも見つかれば、との気持ちでの参加でしたがまさにこのセッションが答えをくれました。Systems Manager(SSM)を組み合わせます。 EC2インスタンスにSSMエージェントをインストール 管理用タグを付ける SSM Inventoryにインスタンスの状態が蓄積される SSM State Managerでインスタンスとパッチの有無を関連付け SSM Patch Managerでインスタンスにパッチを適用 3と4のオプション項目で実行日時を設定(自動化) 残念ながら時間切れでAWS Configまでは進めませんでしたが、こちらでは各インスタンスが有効なSSMエージェントを持っているかを継続的に確認する仕組みが説明されていました。 SSMは、その中に多数の機能が含まれており敬遠しがちだったのですが、実際に触ってみると非常に強力なツールでした。今後は、もっと積極的に触っていこうと思っています。 なお、パッチ適用に際して、気をつけなければならないのが再起動です。パッチによってはインスタンスの再起動が発生します。サポートしてくれたSAに聞いたところ、事前の予告や状態遷移 1 は無いため、Patch Managerに指定する日時の前にサービスアウトしておく事が必要とのことです。大事なプロセスが強制終了されないよう、導入の際にはケアをしてあげてください。 AIM223-R6 - AWS DeepComposer: Get started with generative AI 改めまして、新規事業部の茨木です。日曜日のMidnight Madnessで発表されたDeepComposerを実際に体感してきました。このWorkshopは大変人気で、開始の3時間前から並んでやっと入ることができました。 今回体験したDeepComposerは、キーボードで入力したメロディに対して自動で伴奏を生成してくれるGenerative AIです。GANと呼ばれる教師なし学習を用いており、クラシックやジャズなどのジャンルに応じたモデルが予め用意されています。Workshopでは初めにDeepComposerの講義があり、DeepComposerを構成するインフラストラクチャや機械学習を学びました。 その後、実際にキーボードを用いてメロディを入力し、DeepComposerを使って様々なジャンルの曲を作りました。 メロディによってはあまり上手くいかない場合もありますが、ハーモニーやリズムはジャンルをよく表現している印象でした。実際に会場で作った曲をSoundCloudに上げたので、是非聴いてみてください。 参加者にはDeepComposerのキーボードが配布されました。 教師なし学習によるGenerative AIのアプローチはファッション分野でもコーディネート生成などで活かせるかもしれません。 まとめ 再びSREチームの西郷です。今回初めてAWS re:Inventに参加したのですが、AWSのビジネスの大きさや今後の可能性を体感できた貴重な1週間でした。 AWSがクラウド市場でシェアを獲得している理由はサービスのバリエーションやその開発スピードにあると思っていたのですが、KeynoteやSessionに参加する中で、ユーザーが本当に求めているサービスは何かが常に考えられていること、それが実際にちゃんと形になることも支持される理由なのだと感じました。 今回得た知見を弊社サービスの提供に活かし、ビジネスの成長に繋げていきたいです。 最後に、ZOZOテクノロジーズではサービスをより良くしてくれる仲間を大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 https://tech.zozo.com/recruit/ tech.zozo.com Spot Instanceの終了予告、Auto ScalingのTerminating:Wait相当 ↩
アバター
こんにちは! 開発部基幹SREチームの廣瀬です。 2019年11月4日から8日にかけてシアトルで PASS Summit 2019 が開催され、参加してきました。 初めての海外カンファレンスで少し緊張しましたが、得るものはとても多かったため、その内容をレポートしたいと思います! PASS Summit 2019 PASSとは「Professional Association for SQL Server」の略です。 SQL Serverに限らず、Microsoftのデータプラットフォームを使っているプロフェッショナルのための世界的なコミュニティのことをいいます。 PASS Summitは、毎年秋に開催される、Microsoftのデータプラットフォーム分野における世界最大級のカンファレンスです。 MicrosoftのエンジニアやMicrosoft MVP、及びPASSのコミュニティメンバー等が登壇し、合計約260セッションの中から興味があるセッションを聴くことができます。 11月5日のレセプションパーティと、6日から8日の3日間が本カンファレンスなのですが、4日と5日にプレカンファレンスというものが同時開催されます。こちらは約8時間のセッションを1日1つ受講する、というものです。 1つのテーマについて、一般セッションよりも深い話を聞くことができます。そのため、私はプレカンファレンスも合わせて丸々5日間参加してきました。 公式ページ によると、PASSは1999年に設立された、約20年の歴史があるコミュニティみたいです。 そのためか、カンファレンスの運営がとてもしっかりしており、初参加の人にも優しいカンファレンスであると感じました。 PASS Summitのここが素敵 公式アプリが充実 全日程のイベントやセッションのスケジュール、会場の情報等がアプリに集約されており、自分の興味があるスケジュールだけをピックアップしたMy Schedule作成機能があります。 私は事前に気になるセッションをMy Scheduleへ複数登録しておき、直前にどのセッションを受講するか決めて参加していました。 関連セッションを複数に束ねて紹介する「 Learning Pathways 」 参加者の興味分野に応じて全部で10種類のラーニングパスが用意されていました。これは例えば「AIについて学びたいなら、この3セッションを順番に受講するといいよ」というガイドです。 今年から導入された制度のようですが、受講セッションの選択時にとても参考になりました。 参加者同士の交流を促す仕組み 参加者同士の交流を促す仕組みが豊富だと感じました。 例えば、カンファレンス初参加者が集まって交流する「Fisrt-Timer Event」では6人1組でクイズに挑戦しました。 他にはアジア圏やヨーロッパ圏など、同じ地域の参加者同士で交流できる「PASS Community Zone」が用意されていました。 気になったセッション 全部で14セッションに参加してきたので、その中で特に気になったセッションをピックアップし、レポートしたいと思います。 Session: Bigger Hardware or Better Code and Design? Microsoft SQL Server 2012 Internals の著者の一人であるJonathan Kehayias氏によるセッションでした。 SQL Server観点で最適化を実施するために気をつけるべき点や使えるツールについて、8時間ほどの充実したセッションでした。 ハードウェアのパワーで解決できない問題はたくさんある 冒頭でのこの問いかけは、自分の考えと一致し、一気に話に引き付けられました。 インデックス不足や、遅いクエリの書き方、DBの設定不備などがあるとハードウェアをスケールアップさせても解決できないか、解決できたとしても金額面で折り合いがつきません。 これはクラウドのPaaSでSQL Serverを使ったとしても生じる問題であり、SQL Serverレイヤーでの最適化が必要、という主張は大いに納得できるものでした。 「Matching the Design to the Problem」 これは本当に名言だと思いました。 この考えは、アーキテクティングのレイヤーでも言えることだと思いますし、SQL Serverのレイヤーでも言えることだと思います。 SQL Serverの強力な機能であるIn-memory OLTPやColumnstore Indexにも一長一短あります。 そのため適用したい箇所の性質を把握していないと成功しないよ、という主張には大いに納得しました。 RDBMSに限らず、「問題を正確に把握すること」は、問題解決における基本であり最も重要なことではないでしょうか。 RBAR Processing を避ける 行単位での読み込みが大量発生するような処理をRBAR(row-by-agonizing-row) Processingというそうです。 日本だと ミック本 における「ぐるぐる系」でおなじみの概念かと思います。 カーソルを使って1行ずつ処理する方式はRBAR Processingの代表格ですが、他にもScalar UDFや相関サブクエリもRBARにあたるそうです。 「カーソルを使わずに一括で更新処理を行うべき」という講演者の主張に対し、「それってブロッキング起きませんか?」という会場からの鋭い質問が。 回答は聞き取れませんでしたが、「ロックエスカレーションが起きない程度のバッチサイズ(500行など)でループして処理」がプロダクション環境における現実的な対応策かと思います。 sp_WhoIsActiveが人気 sp_WhoIsActiveとは、デフォルトで用意されているsp_whoやsp_who2といったプロシージャの上位互換といったイメージのもので、無償公開されています。 こちら に使い方が紹介されていますが、海外だとみんな使っているようです。 以前から知っていたものの使っていなかったのですが、コミュニティ内で信頼を得ていることが分かり、改めて使ってみようと思えました。 拡張イベント使用時の注意点 拡張イベントは軽量なことで有名ですが、query_post_execution_showplanというイベントだけは、プロダクション環境で収集すべきではないとのことでした。 秒間1万バッチ実行中の環境でquery_post_execution_showplanイベントを収集開始した途端、バッチ実行数が半減するデモは興味深かったです。 みんなオーバーヘッドを気にしている これは本セッションに限ったことではないのですが、情報取得に伴う負荷について気にする質問が多く飛び交っていました。 「overhead?」と何回聞いたか分かりません。このセッションでは、クエリストアと拡張イベントに関する話のときに負荷についての質問が飛んでいました。 プロダクション環境での情報取得の細かさとサーバー負荷はトレードオフの関係にあるため、ここは全DBAが気にするところなのだなと感じました。 ヒント句は基本使わず、オプティマイザに任せる スピーカーのこの主張は同意です。 skewed data selectivityが一定でない性質をもつカラムを「skewed data」というそうです。 例えば、とあるステータスを持ったカラムがあり、100万レコード中10レコードだけがステータス=1で、それ以外は2であるような場合を考えます。 この場合、where句でステータス=1で絞り込んだ時と2で絞り込んだ時では最適な実行プランが違ってきます。 このような状況は日本でもみんな把握しているとは思うのですが、用語としては存在していない印象です。「skewed data」だけで伝わるのは良いなと思いました。 Session:パフォーマンス関連 パフォーマンスに関するセッションを複数受けまして、内容に重複があったので1つにまとめてレポートします。 Query Optimization Statistics: Driving Force Behind Performance Azure SQL Database: Maximizing Cloud Performance and Availability Query Store In-Depth Performance Tuning Azure SQL Database SQL Server 2019の新機能 2019の新機能は、普段DBAが遭遇する問題をきちんと捉えているなと感じました。 Microsoftの開発チームは、そうした問題の起きない世界をつくろうというヴィジョンを持っているのだと思います。 Memory Grand Feedback(MGF) ワークスペースメモリ確保の過不足があったときに、初回実行のあとに同様のクエリに対してメモリ確保量を調整してくれる機能です。 特定のクエリが必要以上にワークスペースメモリを確保してしまい、そのクエリが複数同時実行されることでRESOURCE_SEMAPHORE待ちが大量発生するというのはDBAあるあるかと思います。 これを自動で防ぐ機能がMGFになります。これは素直に強力な新機能だなと思いました。 Batch Mode On Row Store 解析系クエリにおけるCPUバウンドなクエリで、(作成におけるオーバーヘッドが大きすぎる等の理由で)カラムストアインデックスを作れない場合に有効な手段だそうです。 OLTPと解析系クエリが混在するサーバーで有効な機能だと理解しました。 Interleaved Execution for MSTVFs 今まではMSTVFs(Multi-Statement Table-valued functions)がクエリに入っていると、必ず基数推定が100行となっていました。 そのため実際の行数との乖離が大きい場合に遅い実行プランが生成されてしまう場合がありました。 2019では、MSTVFsの正確な行数を最適化のときに使用するため、基数推定精度が改善しているそうです。 つまりは、あるべき実行プランへと近づきやすくなっているということです。 Batch Mode Adaptive Joins (AJ) skewed dataがあるときに、駆動表のselectivityが分かるまで、hash join/innner joinの決定を遅延できる仕組み。 この機能があることで、nested loop+大量のキー参照によってCPU高負荷な実行プランが生成される、という問題を未然に防ぐことが期待できるのではと感じました。 最適化には非常にわずかなCPUおよび時間しか使えないため、基数を推定するしかありません。 この「推定」と「実際」のズレによるパフォーマンスへの影響をどれだけ柔軟に吸収できるかという観点での進化を感じました。 クラウド(SQL Database)におけるパフォーマンスチューニング SQL ServerでもSQL Databaseでも、起こりうる問題と調査手法、特定した原因の解決方法については王道が整理されている印象を受けました。 複数セッションで同じようなことを話されていたので、業界の主流を知れてよかったです。 SQL ServerおよびSQL Databaseのパフォーマンスチューニングやトラブルシューティングに使える機能として以下の5つを紹介されていました。 DMV XEvents QueryStore AutomaticTuning IntelligentInsights クラウド、オンプレで手法はほとんど変わらないが、IntelligentInsightsはAzure限定とのことでした。 使うツールも大事ですが、普段からbaselineを取得しておくことも大事だと話されていて、基本的なことですが重要だなと改めて思いました。 sys.dm_exec_session_wait_statsというDMVがあり、セッション単位で待ち事象の内訳が取れるためとても便利だなと思いました。使いたい。。! セッション中に「performance tuning skills = immediate cost savings in the cloud」という名言が出ました。 クラウドを使えば容易にスケールアップやスケールアウトできるためチューニングの必要性が薄れるというイメージもあるかもしれませんが、まったくそんなことはありません。 むしろ、クライドを使っている環境においては、チューニングスキルはより直接的に金銭的なコストカットというメリットをもたらす武器になると感じました。 Query Store 2016で導入されたクエリストアですが、解析におけるデフォルトの選択肢になっているようです。 私自身は拡張イベントに頼りがちなのですが、クエリストアについてのスキルも上げていきたいなと思いました。 クエリストアと拡張イベント、どちらを使うべきかという質問に対しては、「状況による」という回答でした。 ただし、「実行プランがいつ変わったか」という問いに答えられるのはクエリストアだそうで、これは確かにと思いました。 クエリストアのlimitationとしては、 ログイン名、ホスト名、アプリケーション名は記録されない 集約された情報だけで、個別クエリの詳細は省かれる 実際の行数といった、実行時の情報は記録されない といった点があげられます。 クエリストアに限った話ではなく、DMVやXEventsにも一長一短があるため、状況に応じてうまく組み合わせて調査することが大事なのだなと理解しました。 そしてそのためには、各ツールの長所短所をもっと理解する必要があると感じました。 Session: Relational Data Modeling Trends for Transactional Applications Microsoft MVPのIke Ellis氏によるとても挑戦的なセッションでした。 データモデリングに関するセッションはほぼ無かったので、こちらのセッションを受講しました。 パフォーマンスチューニングの知識やその上位レイヤーのアーキテクティングの知識も大事ですが、その間に位置づけられるモデリングの知識も大事だなと最近感じています。 「EF Codd's ideas are bit outdated」 EF Coddの考えは今日では少し古くなりつつある、という発言にはっとしました。 スピーカーによると、EF Coddの考えは例えば「ディスクはとても高価である」という当時の状況に基づいています。 そのため、正規化を実施することでデータの重複を防ぎ、ディスク容量を抑える狙いもあった、とのことでした。 それが今日ではディスクはとても安価であり、状況が異なれば最適な設計もまた違ってくるということだそうです。 他にも、「DBは全アプリケーションの中心にあるもの」という当時主流だった思想にも影響を受けているそうです。 今日ではこの思想に基づく設計は、1つのアプリを変更した影響が他のアプリへと波及していまうというデメリットをはらんでいます。 最後に、「DBにデータの正確性の責任を持たせる」という思想もあったそうです。 この思想の問題点としては、ビジネスロジックがDBの中に入り込んでしまう点です。 SQL Serverの中にビジネスロジックが入り込んでしまうと、(ユーザ定義関数などで)その分リソースを使うため、結果的にライセンス料の高騰につながるという問題があります。 ビジネスロジックをストアドプロシージャやトリガーに入れ込んでしまうとテスト、リファクタリング、読解がしづらい状況となってしまいます。 代わりに、アプリ側でビジネスロジックを管理するほうが読みやすくテストもしやすいとの主張は、今日における主流の考え方なのかなと思いました。 こうしたEF Coddの時代と現代の状況を比較した上で、スピーカーはデータモデリングにおける3つの重要な要素を主張していました。 それは①データのクオリティを高める、②スキーマ変更のしやすさ、③microservices method(one application to one database/他アプリからの直接のデータアクセスは失礼という思想)というものです。 Optimize for reads EF Coddは(ディスクが高価であった状況等を踏まえて)書き込みに最適なモデリング手法を提唱しましたが、現在の大多数のアプリケーションはread heavyな性質を持っています。 そのため、読み取り最適化を意識することが重要と話されていました。 この思想だと、例えばRDBMSの顧客テーブルに「現在処理中の注文リストをもったJSONカラム」があっても良いじゃないか!という主張でした。 この主張は名著 SQLアンチパターン に掲載されているアンチパターンですが、スピーカーの思想に基づけば合理的な判断だなとも思いました。 データの一貫性については、DBの責務ではなく、アプリ側の責務という考え方も興味深かったです。 「注文合計金額」といったカラムの更新漏れチェックはアプリ側のテストに盛り込むべきと主張されていました。 Optimize for readsの思想にのっとって、非正規化を恐れない。そして、冗長なデータの更新責務はアプリとアプリ開発者が背負うべきとのことでした。 microserviceについては初心者向けの本を1冊読んだ程度でしたが、もっとこの思想について学ぶ必要があると痛感したセッションでした。 Optimize for network 「create schema for the network, not for the disk」という主張に拍手が沸き起こっていました。 昨今のモダンな設計におけるボトルネックはディスクよりもネットワークにあるため、ネットワーク最適化を意識したスキーマデザインが必要であるとのことでした。 ここも自分にとって新鮮すぎる考え方だったため勉強にはなったのですが、知識不足で具体的なイメージまでは湧きませんでした。 アツい質問 「モノリシックな大きいシステムをクラウドにもっていくのは金銭的なコストがかかるのでは?」という質問がでました。 「コストは問題じゃない。クラウドのほうがオンプレよりもはるかにテクノロジーの進歩が速く、それらを常に使える状態になるほうが経営上のメリットがある」との回答に拍手が起きました。 これは自分でも実感がありまして、オンプレ環境だと2019をプロダクション環境で使えるのは数年に1度のシステム更改時となります。 そのため、強力な機能が存在するのにバージョンの都合で使えないのはもったいないと感じました。(※弊社にはクラウド環境もオンプレ環境も存在しています。) Session: Into the Future with In-memory OLTP SQL Server界隈で超有名なKalen Delaney氏によるIn-memory OLTPに関するセッション。 30年以上SQL Serverに関わっているレジェンドらしいのですが、Microsoftで働き始めたのは半年ほど前から、という謎の経歴の持ち主でした。 セッションの詳細は割愛しますが、「プロダクション環境でIn-memory OLTPを使っている人?」という問いに対して挙手したのが会場全体の1%程度だったのがかなり印象的でした。 In-memory OLTPは、効果は絶大なものの制約が多く、使いどころが非常に難しいイメージがありました。 世界中から集まったDBA達の中でも、やはりプロダクション環境での使用例は少なく、実装のハードルは高いようです。 Session: Hash Match, the Operator Hash Matchの話題だけで2.5時間という超絶マニアックなセッションでした。PASS Summit 2019で唯一のLevel=500(最上級)ということもあり、せっかくなので受講してきました。 ドキュメント化されている情報ではなく、スピーカーが実験の結果たどり着いた結論だそうで、実験の複雑さに圧倒されました。 Call For Speakers 落選理由の分析 PASS Summit 2019に、私も「Trouble Shooting And Performance Tuning On High Traffic EC Site」というタイトルで応募したのですが、落選しました。 そもそも私の英語力ではセッション途中のQ&Aなどに対応することが難しく、現状では無理な挑戦であったなと感じています。 ただ、なぜ落選したのかについては、現地でセッションを受ける中でわりと明確になってきたので、書いておこうと思います。 理由1. PASS Summitの文脈を理解していなかった どういった内容のセッションが多いかは、カンファレンスごとに違うと思います。 PASS SummitはSQL Serverの特定機能についての洞察や、クエリチューニングに特化したセッションなど、1トピックに対して深堀するセッションが多いように感じました。 そして、それは受講者にとっても大変聴きごたえのあるものでした。 一方で、私が応募した内容は、SQL Serverの複数の機能を使って特定の問題を解決した、という事例紹介でした。 したがって、PASS Summitではウケの良くない内容だったなと感じました。 理由2. PASSコミュニティへの貢献がまったく無かった 応募の際は、最低でも3回の登壇経験を求められます。 PASSコミュニティでは、「 SQLSaturday 」という小規模な勉強会も定期開催されています。 SQLSaturdayの登壇経験を積んだ後、PASS Summitで話すという流れの人もいるようでした。 「PASS Summit is a large SQLSaturday」といわれており、コミュニティの小さなイベントとPASS Summitは一続きになっている印象を受けました。 積極的にSQLSaturdayで登壇している人は、PASSコミュニティでウケる内容も自然と理解でき、採択されやすかったのではと思います。 ということで、SQLSaturdayで早速登壇したいと思ったのですが、日本のローカルコミュニティが無いようです。。!自分で作るしか。。ないかな。。! 以上2つの理由が、私が落選した理由だと分析しました。 内容的には初心者向けのセッションもあり、「いかにコミュニティへ貢献できる人物と内容か」ということが大事なのかなと思います。 来年再挑戦するかは迷うところではありますが、いずれ登壇したい、という大きな目標が一つできたことは良かったです。 カンファレンス参加の意義 国内外に関係なく、自分の興味分野における最先端、最大規模のカンファレンスであれば、参加する価値は十分にあると思います。 コミュニティの中に自分を置き、熱量を肌で感じ取り、数日間インプットだけに集中できるのも、セッション動画を見るのではなく直接参加することのメリットだと感じました。 コミュニティのレベルが上がると、自分のレベルも引き上げられます。そして学んだことをコミュニティに還元することで、さらにコミュニティが強くなっていきます。 PASSはこの正のサイクルがうまく回っているコミュニティだと強く感じました。 今までは、Qiitaの記事や会社のテックブログ、勉強会での発信をなんとなく大事だとは思っていたので継続していました。 ただ、カンファレンスに参加したことで、アウトプットは巡り巡って自分のためにもなるという確信を持つことができました。 この変化が、自分にとっては一番大きな成果物だったかもしれません。とても良い経験をすることができました。 あわせて、普段と違う環境で大量のインプットをすることで、新しいことを学ぶだけでなく、新しい行動を起こしたくなるというのもメリットかと思います。 例えば、私の場合は「コミュニティ」が新しいキーワードとなりました。 ベンダに関係なく、RDBMSやデータストレージに関するエンジニア達がお互いの知識を共有し合い学び合えるコミュニティが日本にあればいいのに、と強く思いました。 思うだけでなく、実際に自分でつくるところまでつなげていきたいと思っています。 最後に カンファレンス参加に関わる渡航費・宿泊費などは全て会社に負担していただきました。 自分で希望すれば参加する機会をいただけるZOZOテクノロジーズという会社には本当に感謝です! エンジニアの成長を全力で応援してくれる姿勢を、この支援制度からも感じ取ることができました。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは。開発部データエンジニアの遠藤です。現在、私はデータ×テクノロジーでZOZOグループのマーケティングを支援するデータチームに所属して、データ処理基盤の運用などに従事しています。 本記事では、Lookerを用いて運用中のデータ集計基盤をきれいなデータをスマートに取り出せる基盤に改良した件について報告します。 データ集計基盤で燻っていた問題 1. クエリ管理の限界 2. 集計定義に対するデータの信憑性が謎 Lookerは何が良い? ~データガバナンス機能~ LookML データディクショナリ Gitによるバージョン管理 データ集計基盤(改)の設定フロー データ集計基盤(改)でのデータマート更新 まとめ データ集計基盤で燻っていた問題 ZOZOでは、サービスに関するあらゆるデータをBigQueryに集約しています。BigQueryに集約した大量のデータからデータマートとして必要なデータを取り出し、BIツールの可視化や機械学習システムの入力データなどに用いています。 データマートは以下の2ステップで作成します。 【ステップ1】 ビジネス側でデータマートのアウトプットを決定・集計方法を定義 【ステップ2】 SQLを作成してデータ集計基盤にセット データマートは、BigQueryへのマスタデータ取り込み完了後にデータ集計基盤上でクエリを順次実行して、毎日更新しています。 しかし、データ集計基盤を運用していくにつれて以下の問題が発生しました。 クエリ管理の限界 集計定義に対するデータの信憑性が謎 1. クエリ管理の限界 ZOZOでは現在までに多くのデータマートが生成され、その総数は数百にのぼります。各データマートのクエリはデータ集計基盤内でファイルごとに独立、すなわち "データマートの数"="SQLファイルの数" なので、数百のSQLファイルを管理していることになります。 SQLファイルがここまで増加した背景として、車輪の再発明による量産状態になっていたことが挙げられます。データマートの作成要件があるたびにSQLを1から作り上げたため、アウトプットが似ている当時のベストエフォートクエリが複数のSQLファイルに散在していました。 また、データベースのシステム仕様に変更が生じた際、SQLファイル群から変更箇所を全て探し出して修正しなければならず、クエリの修正工数が膨大になることも問題となっていました。 そのため、クエリの効率的な管理の必要性が叫ばれていました。 2. 集計定義に対するデータの信憑性が謎 データベースのシステム仕様・ビジネス的な集計定義・データマートにおけるアウトプットは、基本的にクエリ内のSQLで表現し、それでも分かりにくい場合はクエリ内にコメントを付記することで管理していました。しかし、その管理では 集計仕様の把握コストの増加(クエリからそのクエリの表現に至った背景・経緯までは理解しにくい) 集計結果が当初取り決めた集計定義とずれる(定義に沿った集計フローに対してシステムレベルまで関係者全員の意識を合わせることは難しい) システム仕様変更に追随しきれない状態になると集計結果の正確性が左右される のような問題が起きてしまいました。 その結果、「今日の数字、何か間違ってない?」と集計データに関する問い合わせが頻発し、 (ビジネス側から見て)データ集計基盤における集計精度の信憑性を疑問視する (システム運用側から見て)原因調査からリカバリまでの誤集計に対する一連の障害対応の工数が膨らむ といった誰も得しない状況に陥る恐れがありました。 そのほかにも、データ集計基盤には様々な根深い問題が存在していて、以下の図に示す内容に集約されます。 現状のデータ集計基盤では、ビジネス側で集計定義が完璧に決められていたとしても、誤った情報などが蔓延りやすくデータドリブンな意思決定の精度を下げるリスクがありました。 Lookerは何が良い? ~データガバナンス機能~ データチームは、データ集計基盤で起こる様々な根深い問題に対してシステムによる解決アプローチを模索したところ、Lookerというツールに目をつけました。LookerはLooker Data Sciences, Inc. がSaaSで提供しているBIプラットフォームです。 ビジネスインテリジェンス&ビッグデータ分析ソフトウェア - Looker 充実したReporting・Visualization機能がLookerの大きな特徴で、Lookerを国内で導入している企業のほとんどがBIツールとして使用しています。 Lookerと他のBIプラットフォームツールの相違点は様々あるのですが、ここで特筆すべき決定的に違う点…。それは…。 データガバナンス機能 です。 システム的にデータ構造を管理して正確なデータ利用を促すアプローチはLookerの唯一無二の特徴です。その特徴がLooker導入の決め手の1つになりました。 Lookerはデータガバナンス機能として以下の機能を備えています。 LookML データディクショナリ Gitによるバージョン管理 LookML LookerではLookMLというデータモデリング言語が採用されています。SQLに代わりLookMLで定義・集計などを記述します。 SQLは言語の特性上クエリ間の情報を転用しづらい側面がありますが、LookMLはSQLが不得意な再利用性・拡張性をカバーできます(LookMLの詳しい説明は 公式ドキュメント をご覧ください)。 例えば、「テストアカウントは計上しない」という集計定義があったとします。 通常、SQLではWHERE句 WHERE member_id NOT IN (12345, 23456, 34567, 45678, 56789) -- テストアカウントは集計から省くようにする を書きますが、LookMLではどのように書いていくのでしょうか? まず、テストアカウントのタグ付けを行うために test_account_tag というdimensionを定義します。 dimension : test_account_tag { case : { when : { sql : $ { member_id } IN ( 12345 , 23456 , 34567 , 45678 , 56789 ) ;; label : "テストアカウント" } } } 最初に定義したdimension test_account_tag は ${test_account_tag} や field: test_account_tag という表現で他の箇所で参照できます。 もしテストアカウントの member_id 構成が変更になった場合、key sql のvalueに記されているIN句のみ修正すれば、テストアカウントに関する条件が含まれる全ての定義が変更されたことになります。 これにより、システム仕様が変更された際に生じる手間が従来のSQLファイル修正と比較すると少なくなる効果があります。 次に、テストアカウントのタグがつけられているかを示すフラグを test_account_flag というdimensionで定義します。 dimension : test_account_flag { type : yesno sql : $ { test_account_tag } = "テストアカウント" ;; } さらに、「テストアカウントは計上しない」をkey filters で表現します。以下のLookMLではユーザー数を集計する際にテストアカウントを除くフィルタを適用させるという定義になります。 measure : user_count { type : count_distinct sql : $ { member_id } ;; filters : { field : test_account_flag value : "no" } } このように、Lookerを用いると無機質に掃き出されたシステムデータをビジネス定義に即した意味あるデータへ変換するELTが容易にできるようになります。 また、Lookerは仮想的にデータモデルの変換・派生カラムの作成ができるので、集計途中の中間データを不必要に量産しないメリットもあります。 データディクショナリ 会員情報を表すテーブル member_info のDDLをLookML化した一部を以下に示します。 view : member_info { sql_table_name : `zozo_original_table.member_info` ;; dimension : member_id { type : number sql : $ { TABLE } .member_id ;; label : "会員ID" } dimension : resign_flag { type : yesno sql : $ { TABLE } .resign_flag ;; label : "退会したか?" description : "TRUE…退会している状態 \n FALSE…退会していない状態" } 上記のように、LookMLには label description というkeyが用意されており、これらのkeyでデータ定義をラベリングできます。データ定義をラベリングできるということはデータディクショナリとしての使用が可能であることを意味します。 開発速度がとても速いWeb開発の現場でDB定義書のようなドキュメントでメタデータ管理を行った場合、ドキュメントがシステムに追いつかない状態、つまりドキュメントとプログラムとの乖離がしばしば発生します。 Lookerは任意のデータソースに存在するテーブルの各DDLから作成した各Viewを集計フローの根幹として使用します。そのため、正確な集計を行うために最新のメタデータに更新せざるをえなくなります。 これにより、Looker内ではデータディクショナリの最新状態を保つことができます。 Gitによるバージョン管理 Lookerは、LookMLのソースコード管理にGitを採用して、ソースコードの品質を担保しています。GitHubなどのGitを用いたソフトウェア開発のプラットフォームにも連携できます。 つまり、既に運用中のGitHubと連携することで複数人開発・相互レビューといった開発体制をとることができて、データチームでもこの体制を取り入れています。 データチーム管理のデータマート作成Gitリポジトリでは、集計ガイドラインやLookMLの命名規則・記述規則といったコーディング規約を決め、互いにReviewしやすい開発体制を敷いています。 データ集計基盤(改)の設定フロー データチームでは今回の取り組みをデータ集計基盤(改)として開発しています。データ集計基盤(改)は以下のフローでデータマート生成の設定が行われます。 まず、BigQueryに存在するテーブルのDDLをOriginal Viewとしてそのまま読み込み( 《1》 スキーマ情報読み込み )、そのOriginal View群を用いて分析に適した仮想データベースを構築します( 《2》 データモデル変換 )。その仮想データベース上で必要な情報抽出から集計まで行ってデータマートViewを作成します( 《3》 データマートView生成 )。 ちなみに、データ集計基盤(改)は「データマート作成のLookML内では原則SQL文完成形を直接書かない」というポリシーを持っています。つまり、 SELECT … FROM … といったSQL文を直接LookMLに入れ込むことはシステム的に可能ですが、それをあえて禁止しています。 というのも、LookerがLookMLを基にクエリを自動生成してくれるので、このポリシーがクエリ品質の均一化・データマート作成過程におけるブラックボックス化防止に繋がるためです。 データ集計基盤(改)でのデータマート更新 Lookerには好きなタイミングでアウトプットを中間データとして保存する機能が存在するのですが( PDT )、データ集計基盤(改)では、既存のデータ処理基盤との兼ね合いを考慮して、GCPの機能を用いてデータマートを更新しています。 BigQueryへのデータ取り込み完了直後にCloud Pub/SubをKickして、Cloud Functions経由でCloud Composer内のAirflow DAGを起動させます。 DAGの実行中にLooker API(Lookerが提供しているAPI)にリクエストを送り、そこからデータマート更新用クエリをレスポンスデータとして取得します。 APIで取得したクエリをBigQueryクライアントライブラリを使用したSDKを通して実行することで、各データマートに即した更新を行っています。 まとめ Lookerを導入してデータ集計基盤の改善を図ることでデータユーティリゼーションを向上させる取り組みを報告させていただきました。 現在、データチームではデータマート更新処理をこのデータ集計基盤(改)へ順次移行している最中です。 移行中はBigQueryの最適パフォーマンスに即したLookML記法を研究してブラッシュアップしていったり…とまだ発展途上のデータ集計基盤(改)ですが、課題が徐々に解決しており、その効果も表れています。 ビッグデータを集計するシステムを運用していると、冒頭に紹介した諸問題を抱えているにもかかわらずその最適なソリューションはなかなか見つからなく、苦い思いをしている方も少なくないのかなと思います(少なくとも私は長年そう思っていた)。 この記事で快適なビッグデータのシステム運用の手助けになれば幸いです。 このように、データチームではエンジニアの強みを生かしながらデータを中心とした技術課題・マーケティング課題に取り組んでいます。メンバーを絶賛募集していますので、ご興味のある方は以下のリンクからぜひご応募ください! www.wantedly.com
アバター
はじめに こんにちは! ZOZOテクノロジーズ開発部の鈴木( @zukkey59 )です! 普段は、 「ファッションコーディネートアプリ WEAR」 のAndroidアプリを担当しています。 私のWEARに配属されて初めての大きな仕事は、新規登録・ログイン機能へAndroid Jetpack コンポーネントのNavigationを導入することでした。 導入によってある程度知見がためることができました。 今回はNavigationをこれから導入したい!と検討している方、もしくは興味がある方に向けて、自身の知識の整理もかねて紹介したいと思います。 既に、Navigationの記事はQiitaやMediumに数多くあるので被っている内容もありますが、今回は導入からテストまでの一連の流れとその中で気づいた注意すべきところなどを一通り紹介させていただきます。 要件や仕様として、WEARで使用しなかったNavigationの機能について触れることはありません。 記事を読み進める前の判断として、想定している読者とこの記事に書いてあること、書いていないことをまとめました。 事前に目を通していただけると幸いです。 想定している読者 Android開発を主に業務でされている方 プロダクトにNavigationを利用したいと思っている方 Navigationをまだ利用したことがなく、初めて利用する方 NavigationのUIテストを書いてみたい方 Navigationのハマりどころについて事前に知っておきたい方 Espressoについての基礎知識を持っている方(CodeLaboを終了した程度を想定) AndroidX移行済みである方 今回書いてあること Navigationの説明 Navigationの導入手順 (DeepLinkを除く)Navigationの実装手順、利用の仕方 (DeepLinkを除く)Navigationのテストの書き方 今回書いてないこと NavigationのDeepLink周りについて マルチモジュール関連の挙動 プロダクト全体でのSingleActivityでの運用など Mockito、Espressoの導入、使い方などの説明 実際に導入した箇所はこちらです。 また、開発環境は次のとおりです。 Mac OS Mojave 10.14.5 Android Studio 3.5.1 それでは早速、Navigationについてみていきましょう!! Navigationとは? Android Jetpack コンポーネントのライブラリの一つです。 Android Jetpackは、開発者向けにAndroidアプリを簡単に作成するためのライブラリやツール、ガイダンスをまとめたものです。 その中の一つである、 Navigation はアプリ内の複雑であった画面遷移を、簡単にしてくれるライブラリです。 Navigationの実装のTipsに入る前に、まずはNavigationの原則について確認していきましょう。 Navigationの原則 公式は、 こちら にあります。 簡単に要約すると次の通りです。 Fixed start destination 出発点を決めます。Navigationを利用する場合には 開始位置を固定する 必要があります。 Navigation state is represented as a stack of destinations Navigationの状態はスタックで表現されます。 バックスタックはすべてNavigationが管理 してくれますが、自分でバックスタックを管理することもできます。 Up and Back are identical within your app's task アプリ内において画面上部のapp barに表示されるUpボタンと画面下部のナビゲーションバーに表示される戻るボタンの挙動は 同じ です。 The Up button never exits your app Upボタンはアプリを終了させる事はしません。 Deeplinkでアプリを起動した場合、Upボタンはアプリ内で新たに手動による操作を模倣して作成されたバックスタックをたどり、Deeplinkを起動したアプリに戻ることはありません。 戻るボタンはDeeplinkを起動したアプリに戻ります。 Deep linking simulates manual navigation Navigationは、DeepLinkに対応しています。 DeepLinkは手動での画面操作を模倣したバックスタックを生成します。 DeepLink経由で起動した場合には、既存のバックスタックは削除されてDeepLinkにより生成されたバックスタックに置きかわります。 今回、WEARで導入した新規登録・ログインのリニューアルに関しては、要件としてDeepLinkは用いなかったので、DeepLink以外で関わることのみを紹介いたします。 Navigationの原則について確認いたしましたので、次は導入の手順について見ていきましょう。 導入手順 早速、Navigationを利用する準備を始めましょう! Navigationの最新バージョンは、 2.1.0 です。(2019年10月現在) はじめに、Navigation Supportを導入するためにapp配下の build.gradle に次の2行を記載しましょう。 build.gradle dependencies { // Kotlin implementation "androidx.navigation:navigation-fragment-ktx:2.1.0" // 追加 implementation "androidx.navigation:navigation-ui-ktx:2.1.0" // 追加 } WEARではKotlinを利用しているので上記のように記載しております。 Javaを利用する場合は次のように記載してください。 build.gradle dependencies { // Java implementation "androidx.navigation:navigation-fragment:2.1.0" // 追加 implementation "androidx.navigation:navigation-ui:2.1.0" // 追加 } まだ、Jetpackを利用されていない場合は、プロジェクト配下の build.gradle に次の1文を追加してください。 build.gradle allprojects { repositories { google() // 追加 } } 【参考リンク】 Get started with the Navigation component | Android Developers Adding Components to your Project | Android Developers 導入の準備は、たったこれだけでOKです! 次は実装のTipsについて見ていきましょう! 実装のTips NavigationGraphを作成する NavigationGraphは、 ナビゲーションを管理する役割 を持っており、遷移の目的地と遷移するアクションなどを含むリソースファイルです。NavigationGraph自体はxmlファイルになります。どこに遷移するのか、どのような種類の遷移間のデータを持たせるのかなどをここに定義します。 まずは、NavigationGraphの追加の仕方について説明していきます。 最初に、File > New > Android Resource Fileを選択します。 画像でみると、次のような状態です。 選択後に、New Resource Fileというポップアップが表示されますので、File nameには自分が作成したいNavigationGraphの名前を入れて、Resource typeはNavigationを選択します。これでOKを押すと作成が完了です。 画像でみると、次のような状態です。 作成すると、res > navigationにて自分の定義したファイルが生成されていることがわかります。 今回の機能改修に関しては、 navigation_graph_entrance として作成しました。 画像でみると、次のような状態です。 作成が終わるとXMLの内容は次のようになります。 navigation_graph_entrance.xml <? xml version = "1.0" encoding = "utf-8" ?> <navigation xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" android : id = "@+id/navigation_graph_entrance" > </navigation> これで、NavigationGraphの作成は終了です。 次は、NavHostFragmentを用意していきましょう。 NavHostFragmentを用意する NavHostFragmentの内部のjavadocを見ると、次のようにあります。 NavHostFragment provides an area within your layout for self-contained navigation to occur. ここから考えることとして、NavHostFragmentとは「Navigationが機能するためのハコのようなFragmentである」という認識で良さそうです。 このNavHostFragmentをもった、Navigationの管理をする用に一つのActivityを作成します。 今回の新規登録・ログイン周りでは、入口部分であるという意味を込めて、 EntranceActivity と定義しました。 用意したActivityのレイアウトファイルである、 activity_entrance.xml に次のように記載します。 activity_entrance.xml <? xml version = "1.0" encoding = "utf-8" ?> <layout xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" > <androidx . constraintlayout . widget . ConstraintLayout android : layout_width = "match_parent" android : layout_height = "match_parent" > <fragment android : id = "@+id/nav_host" android : layout_width = "0dp" android : layout_height = "0dp" android : name = "androidx.navigation.fragment.NavHostFragment" app : navGraph = "@navigation/navigation_graph_entrance" app : defaultNavHost = "true" app : layout_constraintTop_toTopOf = "parent" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintStart_toStartOf = "parent" app : layout_constraintEnd_toEndOf = "parent" /> </androidx . constraintlayout . widget . ConstraintLayout> </layout> WEARでは、DataBindingを併用しているためlayoutタグで囲っています。 まずは、 activity_entrance.xml のfragmentタグに着目してください。 ソースコード中の...は省略記号を示します。 activity_entrance.xml <fragment android : id = "@+id/nav_host" ... android : name = "androidx.navigation.fragment.NavHostFragment" app : navGraph = "@navigation/navigation_graph_entrance" app : defaultNavHost = "true" ... /> fragmentのandroid:nameに androidx.navigation.fragment.NavHostFragment を設定します。NavHostを実装したクラスを指定する必要があるようです。特になければ、上記のように設定すれば問題ないでしょう。 次に、fragmentのapp:navGraphに先ほど作成したNavigationGraphを設定します。 今回の場合は、 navigation_graph_entrance を作成したので、 @navigation/navigation_graph_entrance と設定します。 navGraphの属性は、NavHostFragmentに自分が作成したNavigationGraphを関連付ける役割を持ちます。 defaultNavHostをtrueにされたNavHostFragmentが戻るボタンをインターセプトします。 同時に複数あるNavHostのdefaultNavHostをtrueにしてはいけません。 複数のNavHostがなければ、trueにしておくのが良いかと思います。 次は、NavigationEditorについてです。 NavigationEditorを利用する NavigationEditorとは、視覚的にNavigationGraphを編集することができたり、直接xmlでNavigationGraphを編集することができるAndroid Studioの機能の一つです。 WEARで利用している箇所でのEditor表示は次のとおりです。 左の赤い枠の部分が Destinations panel といわれるところで、ここではNavigationのHostと後述する青枠のGraph Editorの中にある全ての遷移先の目的地のリストを表現します。 真ん中の青い枠の部分が先ほど出た Graph Editor といわれるところで、自身が作成したNavigationGraphを視覚的にわかりやすく表示してくれます。ここで編集したことと、xmlの中で編集したことは同期されます。 右の緑の枠の部分が Attributes といわれるところで、後ほど説明いたしますが選択しているGraphのArgumentsやactionなどの属性を表示したり、actionのAnimationやAttributesのIDやPop behaviorなどの属性を表示します。 ここで編集したことと、xmlの中で編集したことは同期されます。 左下のオレンジの枠の部分では、Designタブでデザインのプレビュー画面に、TextタブでXML画面に表示を切り替えることができます。 次は、NavigationEditorで遷移先となるDestinationの追加の仕方について見ていきましょう。 Destinationの追加の仕方 Destinationの追加の仕方は、とてもシンプルです。 上記画像の、赤枠で囲まれている New Destinationアイコンをクリックすると、検索ボックス、作成するボタン、並びにクラスのリストが表示されます。 検索ボックスには、まだ追加されていないクラスを入力すると一覧に出てくる仕組みになっているので、先に新しくActivityやFragmentを作成した場合は、ここから検索してNavigationリソースにも追加するのが良いと思います。 また、青枠で囲まれている、Create new destinationと書かれている箇所をクリックすると、Fragmentを作成する画面が表示され特にチェックを外さずにFinishすると、新しいFragmentとレイアウトリソースファイルを自動的に作成し、Navigationリソースにも追加してくれます。 緑枠には、既存でNavigationリソースに追加していないActivityやFragmentのファイルがリスト形式で表示されます。 xmlにFragmentタグで直接定義することで追加することもできます。 以上が、Destinationの追加の仕方についてでした。 次は、Actionの追加の仕方について紹介します。 Actionの追加の仕方 Actionの追加の仕方もとても簡単です。 赤枠で囲まれている右矢印のアイコンをクリックすると、Add Actionというポップアップが出てきます。 また、 Attributes のActionsの+をクリックすると、リストが表示され Add Action という項目が出てくるので、それをクリックすることでも同様のことができます。 ここで遷移元と遷移先やAnimationの仕方など遷移に関わる項目を設定します。Navigationではこれら全てをまとめてActionとしています。 このとき何も選択していなければデフォルトでFromにnav_hostが入りますが、前章で出てきたDestinations panelのGRAPHのリストの項目に存在する、いずれかのGraphを選択していると選択されたGraphがデフォルトでFromに入ります。 メールフォーム入力画面を選択した状態でしたので、画像ではFromにmail_formと入力されていることがわかります。 IDは、指定がなければ自動的にActionのIDを生成してくれるので、特に気にする必要はありません。 Fromは遷移する元の画面のIDを指定し、Destinationには遷移させたい画面のIDを設定します。 Transitionでは、次画面への遷移時と、元画面へ戻る遷移時のアニメーションを選択することができます。 アニメーションをさせたくない場合は未入力でも問題はありません。自分で作成したアニメーションも設定することが可能です。 Pop Behaviorでは戻るときに「どこまで戻るのか」の定義ができます。 Inclusiveにチェックを入れると、popUpToの前まで戻ることができます。 実際にプロダクトに導入してみたところ、戻るの挙動を変えたい場合が想像以上に出てきたので非常に有用でした。 Launch Optionsではシングルトップで起動するかどうかを選べます。必要な場合はチェックを入れましょう。 後から変更したくなった場合にも、xmlを編集すれば変更可能なので、間違えても問題はありません。 Actionを追加する方法について学んだら、次はAttributesの追加について見ていきましょう。 Attributesの追加の仕方 AttributesもNavigationEditorから簡単に追加することができます。 次の画像を見てください。 Attributes のところで、 Arguments という項目があり右側に+アイコンが表示されています。こちらをクリックすると、Add Argument Linkというポップアップが出てきます。 ここで追加したい情報を設定することができます。 Nameにはargumentの名前を入れます。 Typeは、Integer / Float / Long / Boolean / Stringだけでなく、ResourceやCustomSerializableなども選ぶことができます。 ArrayやNullableもチェックを入れられる場合は設定可能です。また、defaultValueも設定可能です。 もちろん、NavigationEditorからだけでなく直接xmlを編集して追加することも可能です。 SafeArgsを利用する SafeArgsとは、NavigationのGradleプラグインでActivityやFragmentへの引数付きの遷移をする際に、型安全なObjectとビルダークラスを生成してくれます。 こちらを利用することで、自身でキーを設定したり、型があっているかの確認をしなくても自動生成されたクラスを利用するだけになるため非常に便利です。 導入の手順はいたってシンプルです。app配下のbuild.gradleにて次の一文を追加します。 build.gradle apply plugin: 'androidx.navigation.safeargs' 次に、project配下のbuild.gradleに次のコードを追加します。 build.gradle buildscript { dependencies { classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0" } } 2019年10月現在、この記事を書いている時の最新のStableバージョンは 2.1.0 です。 バージョンの確認を行いたい方は、 こちらのリンク から確認することができます。 公式ドキュメントにも導入方法が記載されております。 【参考リンク】 Use Safe Args to pass data with type safety もし、kotlinOptionsをapp配下のbuild.gradleに記載していない場合は、JVM targetを設定する必要があります。 その場合はapp配下のbuild.gradleに次のように記載しましょう。 build.gradle android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 } } SafeArgsの利用手順としては次の通りです。 遷移先にて渡したい情報を持つArgumentを定義 遷移元にてActionを定義 遷移元のFragmentで自動生成されたクラスに渡したい情報を付加して遷移 遷移先にて渡された情報を受け取る 実際のソースコードを見ていきながら、手順を確認していきましょう。 まず、 遷移先にて渡したい情報を持つArgumentを定義 するということを行います。 今回は、メールアドレス入力画面(mail_form)からアカウント作成画面(create_account)への遷移について例にとります。 次の画像の画面間の遷移になります。 アカウント作成画面にて、受け取りたい情報を定義します。 navigationのリソースファイルを開き、次のように記載をします。今回は、度々例に挙げている、 navigation_graph_entrance.xml で説明します。 navigation_graph_entrance.xml <fragment android : id = "@+id/create_account" android : label = "fragment_create_account" android : name = "(package名とパス).CreateAccountFragment" tools : layout = "@layout/fragment_create_account" > <argument android : name = "snsType" app : argType = "(package名とパス).SnsType" /> </fragment> NonNullなSNS連携情報を持つクラスを渡したいので、上記のように遷移先にて定義するようにします。 次に、 遷移元にてActionを定義 します。 メールフォーム画面からアカウント作成画面へ遷移するためのActionタグを、遷移元であるメールフォーム画面のfragmentタグに情報を追加します。 navigationのリソースファイルを開き、次のように記載をします。 navigation_graph_entrance.xml <fragment android : id = "@+id/mail_form" android : label = "fragment_mail_form" android : name = "(package名とパス).MailFormFragment" tools : layout = "@layout/fragment_mail_form" > <action android : id = "@+id/action_mail_form_to_create_account" app : destination = "@+id/create_account" app : enterAnim = "@anim/slide_in_left" app : popEnterAnim = "@anim/slide_in_right" /> </fragment> fragmentタグの中にactionタグを追加してアニメーションとdestinationを遷移先のcreate_accountにします。 Actionの定義が終わったら、最後は 遷移元のFragmentで自動生成されたクラスに渡したい情報を付加して遷移 させます。 遷移元のMailFormFragmentにて、引数なしの場合は次のコードのように定義していた箇所を変更します。 MailFormFragment.kt findNavController().navigate(R.id.create_account) SafeArgsプラグインを利用すれば、 Fragment名 + Directions のクラスが自動生成されて、自身で定義したアクション名をもつ関数が自動で定義されるようになっています。 遷移元のMailFormFragmentでは次のようにして定義します。 MailFormFragment.kt findNavController().navigate( MailFormFragmentDirections.actionMailFormToCreateAccount( snsType ) ) これだけでOKです。 最後に、遷移先のCreateAccountFragmentにて情報を受け取ります。 SafeArgsプラグインを利用すると遷移先のクラスで、 Fragment名+Args のクラスが自動生成されます。 こちらの自動生成されたクラスを用いて、次のようにして情報を受け取ります。 CreateAccountFragment.kt // navArgsでarguments全体を取得する val args: CreateAccountFragmentArgs by navArgs() // 渡したデータを受け取ることもできる val snsType = args.snsType 受け取りはこれだけでOKです。 SafeArgsについての説明は以上です。 次に、今回の開発においてNavigationEditorを利用しましたが、非常に便利であったので、覚えておくといいことをご紹介させていただきます! NavigationEditorを利用する時に覚えておくと良いこと AutoArrange について 色々とDestinationを追加していったら、Graph Editorの中がごちゃごちゃしてくることがあります。 そんな時は、AutoArrangeを利用しましょう。 Auto Arrangeは次の画像の赤い枠で囲った部分をクリックすることで利用できます。 このAutoArrangeを利用した時の挙動が次のとおりです。 一瞬で、整列してくれるので、Graph Editor がゴチャゴチャして見辛くなったときに、利用するようにしましょう。 Go to XML というショートカットについて 開発を行っていると、Graph Editorを利用していたら該当箇所のxmlに素早くジャンプしたい時が出てきます。 その際に便利なショートカットが、 ⌘(コマンドキー) + B (Macの場合)で選択したアイテムのxmlリソースへ飛ぶことができます。 Graphだけでなく、Actionでも遷移することが可能です。 レイアウトの設定について xmlにて、toolsで該当のレイアウトリソースファイルを指定するとGraphEditorに反映され、視覚的にもGraphがどの画面なのかすぐにわかります。 例えば、WEARでのメールアドレス入力画面について見ていきましょう。 xmlでの定義は次のようにします。 navigation_graph_entrance.xml <fragment android : id = "@+id/mail_form" android : label = "fragment_mail_form" android : name = "(package名とパス).MailFormFragment" tools : layout = "@layout/fragment_mail_form" < !-- 追加 --> > ... </fragment> 上記のように対象のレイアウトリソースファイルを指定することで、次のような表示をGraphEditor内で確認できます。 以上、NavigationEditorを利用する際に覚えておくと開発が捗ると思うことの紹介でした。 次は、Espressoを用いたNavigationの遷移のテストについて紹介させていただきます。 Espressoを用いてNavigationの遷移のテストを行う Mockito、Espressoは導入している前提で、進めます。 WEARで利用しているEspressoのバージョンは 3.1.1 です。 Navigationの遷移のテストを行う前に準備をしましょう。 app配下のbuild.gradleに次の1行を追加します。 build.gradle debugImplementation 'androidx.fragment:fragment-testing:1.1.0' 導入はたったこれだけで大丈夫です! Navigationのテストの実装の手順は次のとおりです。 NavControllerをMockする テストしたい画面のFragmentScenarioを作成する FragmentにNavControllerのプロパティをセットする Actionで遷移した結果が正しいかのテストを行う まずはじめに、1~3をやっていきましょう! 今回は度々例として出ていたメールフォーム入力画面(MailFormFragment)を例に紹介いたします。 androidTestディレクトリの中に階層を同じくしたMailFormFragmentTestを作成します。 1~3はテストする前に下準備をします。コードとしては次のようになります。 それぞれコード上にコメントいたしました。 MailFormFragmentTest.kt @RunWith (AndroidJUnit4:: class ) class MailFormFragmentTest { private lateinit var mockNavController: NavController private lateinit var scenario: FragmentScenario<TestMailFormFragment> @Before fun setUp() { // NavControllerをMockする mockNavController = Mockito.mock(NavController:: class .java) // テストしたい画面のFragmentScenarioを作成する scenario = launchFragmentInContainer<TestMailFormFragment>(themeResId = R.style.AppTheme) // FragmentにNavControllerのプロパティをセットする scenario.onFragment { fragment -> Navigation.setViewNavController(fragment.requireView(), mockNavController) } } } setUp関数の中で、一通り1~3を行います。 NavControllerをまずはMockします。Mockitoは今回の趣旨に関わらないので説明を省略します。 NavControllerをMockしたら、テストしたい画面のFragmentScenarioを作成します。 Test用にMailFormFragmentを継承したクラスを作り、DaggerAndroidSupportを利用している場合はTest用にViewModelをMockします。 作成したTest用MailFormFragmentのFragmentScenarioを作成します。この時にthemeResIdを指定する必要があるので、AppThemeを指定するようにしてください。 ここまで準備をしたら、各Actionのテストを行います。 今まで、メールフォーム入力画面からアカウント作成画面を例にとって説明していたので、そちらを例にして説明していきます。 NavigationのActionが通るかどうかについては、次の1行だけ記入すれば、Actionが意図した挙動で成功するかを確かめることができます。 verify(mockNavController) .navigate( /* ここで遷移先のidを指定するか自動生成されたクラスのActionの関数を呼ぶ */ ) 実際のソースコード全体としては次のとおりです。 Espresso部分に関してもコメントを追加しました。 MailFormFragmentTest.kt @RunWith (AndroidJUnit4:: class ) class MailFormFragmentTest { @Test fun testNavigationFromMailFormToCreateAccount() { // MailForm入力欄をクリックする Espresso.onView(ViewMatchers.withId(R.id.mail_form)).perform(ViewActions.click()) // Mail用にランダムな文字列を作成 val randomString = RandomStringUtils.randomNumeric( 3 ) + RandomStringUtils.random( 20 , "abcdefghijklmnopqrstuvwxyz" ) val testMail = "wear $randomString @te.st" // MailForm入力欄に直接入れる。 // InputTextActionというViewActionを実装したクラスを作成して入れるとソフトウェアキーボードの影響を受けない。 Espresso.onView(ViewMatchers.withId(R.id.mail_form)).perform(InputTextAction(testMail)) // Fabをクリックする Espresso.onView(ViewMatchers.withId(R.id.fab)).perform(ViewActions.click()) // モックしたNavControllerでActionが正しく実行されたかどうかを確認する verify(mockNavController) .navigate(MailFormFragmentDirections.actionMailFormToCreateAccount(SnsType.Wear)) } } これで、対象のクラスを右クリックして、 Run 'MailFormFragmentTest(自分で作成したテストクラスの名前)'.. を実行して、テストがパスすればNavigationの遷移のテストとしてはOKです。 実装に際して参考にした公式リンクは次のとおりです。 【参考リンク】 Test Navigation Navigationのテストまで実装をすることができたら、開発中にいくつか気をつけることがあったので、一つずつ紹介させていただきます! プロダクトに導入する際に気をつけること 開始先を分けたい場合は、プログラム的に設定する必要な場合があるので注意 Navigationでは開始する際に、最初のDestinationをデフォルトで設定する必要があります。 先に説明したNavigationの原則1にある通り、最初の目的地をxml上で定義する必要があります。 WEARでは次のように開始先を固定しています。...は省略を意味しています。今回は、度々例に挙げている、 navigation_graph_entrance.xml で説明します。 navigation_graph_entrance.xml <navigation xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" xmlns : tools = "http://schemas.android.com/tools" android : id = "@+id/nav_host" app : startDestination = "@+id/mail_form" > <!-- ここで開始先を定義する --> ... </navigation> しかし、開始先を分けたい状況が、少なからず出てくると思います。 WEARでは、Navigation Drawerにログインと会員登録からの導線があり、その後にNavigationを用いているため、開始先を分ける必要が出てきました。 画像でみると次の画面です。 この場合は、 条件付きナビゲーション という形になります。 このような場合に備えて、上記リンクのFirst-time user experienceという項目で対応方針を公式ドキュメントに記載されてあります。 やり方としては、main_fragmentのようなFragmentを用意して、登録用のNavigationGraphを用意して遷移させる手法です。こちらの方法が一番綺麗にできるため、できる限りこの方法を採用していくのが良いと思います。 ただし、既存のプロダクトで部分的にNavigationを使用する場合で、遷移箇所がDrawerの項目からとなると、また少し工夫する必要があります。 遷移後の画面では、ログインと会員登録にフローが分かれます。遷移元のDrawerの画面には、今回はNavigationの適用を行う予定はなく、「ボタンを押した後にフローを分けたい」という要件で部分的に適用するという状況が起きました。 この為、ドキュメント通りに WEARでは、プログラム的に開始位置をFragmentが作られたタイミングで設定するようにしました。 コードとしては次のような形になります。 AuthType.kt sealed class AuthType : Serializable { abstract fun setUpStartDestination(navigationHostFragment: Fragment?) object Login : AuthType() { override fun setUpStartDestination(navigationHostFragment: Fragment?) { // NavigationHostFragmentからNavigationGraphを取得する val inflater = navigationHostFragment?.findNavController()?.navInflater ?: throw IllegalArgumentException() val graph = inflater.inflate(R.navigation.navigation_graph_entrance) // NavigationGraphに開始先の目的地のFragmentのIDを指定 graph.startDestination = R.id.login // NavigationGraphをNavigationHostFragmentに設定する navigationHostFragment.findNavController().graph = graph } } object Register : AuthType() { override fun setUpStartDestination(navigationHostFragment: Fragment?) { // NavigationHostFragmentからNavigationGraphを取得する val inflater = navigationHostFragment?.findNavController()?.navInflater ?: throw IllegalArgumentException() val graph = inflater.inflate(R.navigation.navigation_graph_entrance) // NavigationGraphに開始先の目的地のFragmentのIDを指定 graph.startDestination = R.id.mail_form // NavigationGraphをNavigationHostFragmentに設定する navigationHostFragment.findNavController().graph = graph } } } AuthTypeというsealed classを定義してLogin用とRegister用のobjectを作成し、コメントにある通り、それぞれの分岐に対して目的地を指定することで開始先を分けることが可能です。 既存に部分的に導入した場合にこのようなパターンがありましたので、少しでも参考になれば幸いです。 次は、Navigationでの戻る挙動を変えたい場合について紹介します。 特定のFragmentの戻る挙動を変えたい場合はCustomBackNavigationを利用する Navigationを用いているときに、要件として「この場合は戻るの挙動を変えて欲しい」ということが出てきます。 特定のFragmentで戻る挙動を変更したい場合に使えるのが、 Custom Back Navigation です。 FragmentでonViewCreatedなどが呼ばれた際に、コールバックを設定します。 Fragmentで戻る挙動が変えたい場合については、コードだと次のようになります。 requireActivity().onBackPressedDispatcher.addCallback( this ) { /** * ここら辺に戻るの挙動で追加したい処理を書く */ } onBackPressedDispatcher.addCallbackにて処理したい内容を書いたコールバックを追加することで、該当のFragmentだけ戻るの挙動を変えることができます。 addCallbackの内部を見ると、defaultでenabled=trueになっているので、要件的にONとOFFを切り替える必要がなければ、上記のソースコードのみで問題ないです。 idを確認してから起動しないとクラッシュする Navigationを用いて遷移する場合に、次のようなエラーが出てクラッシュする場合があります。 stackoverflowにも同様の事象に遭遇している状態がみられるようです。 現状としては、ボタンの連打防止で回避できたという方もいらっしゃいますが、それ以外の条件でも発生している場合もあり、私も連打ではなく、通常通りの押下で再現することがありました。 ライブラリ側のバグの可能性が高いのではないかと考えています。 stackoverflow.com Fatal Exception: java.lang.IllegalArgumentException navigation destination XXX is unknown to this NavController このエラーは、開発中に、次のように遷移する箇所で発生することが度々ありました。 findNavController().navigate( // 遷移先のIDなどを指定する ) 調査したところ、現在のスタックの最上部のDestinationのidが遷移する元のFragmentのidと異なるタイミングがあり、そのことが起因しクラッシュすることがあります。 現状の対応方針としては、現在のスタックの最上部のDestinationのidが遷移する元のFragmentのidであるかどうかを事前にチェックするか、例外でエラーを握りつぶすかの、二つの手法のみのようです。 WEARでは、下記のように、チェックを行うようにして対応しました。 次のコードはメールフォーム入力画面を例にしています。 if (findNavController().currentDestination?.id == R.id.mail_form) { findNavController().navigate( // 遷移先のIDなどを指定する ) } 以上で、プロダクトにNavigationを導入する際に、気をつけることについて「これだけは事前に知っておくと嬉しいかも」をまとめてみました。何かの参考になれば嬉しいです! さいごに いかがだったでしょうか? Navigationを実際にプロダクトに導入してみて、次のようなメリットとデメリットがあると感じました。 メリット 視覚的に遷移先や各Actionで渡す情報を見ることができる 導入までの流れが簡単にできる 公式ドキュメントが充実している スッキリと遷移部分を書くことができる 遷移する部分でキーの設定や型の確認などをせずに、Navigation側(プラグイン)で自動でやってくれる デメリット 学習コストがある 複数人で同一のNavigationGraphを触る場合、コンフリクトに注意する必要がある Navigationライブラリ側のバグがある 他にもやり方があったり、もっとこうしたほうがいいというアドバイスがございましたら、私のTwitterアカウント @zukkey59 までご連絡をいただけますと嬉しいです! 今後、新たに知見が溜まったら追加で書きたいと思います。 さいごに、ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは、ZOZOテクノロジーズで機械学習の研究開発をしている松井・真木です。2019 年 9 月末にコペンハーゲンで行われた推薦システムのトップカンファレンスである RecSys 2019 に参加してきたので、本稿では参加報告と気になった論文の紹介をします。 recsys.acm.org Overview RecSys では推薦システムに関するアルゴリズムはもちろん、インターフェースやユーザー心理など幅広い話題を扱っています。今年は参加チケットが売り切れたことからも注目度の高さが伺えます。研究発表はロングペーパーとショートペーパーからなり、採択率はそれぞれ 19%、24%でした。個人的に印象的だったのは、参加者の 7 割以上が企業所属である一方、発表者の数でみるとアカデミアの勢力も強かった点です。私はこれまでいくつかの国際会議に参加してきましたが、RecSys は笑いを取りに来る発表者が多かったり、発表者に対して口笛や声援が飛んだりするなど、これまでで一番明るく和気藹々とした会議だったように思います。 トレンド トレンドとしてはっきりしていたのは、推薦システムが社会に与えうるインパクトや研究手法に関する自己批判です。最も象徴的なのは、深層学習ベースの手法が本当に従来の手法より優れているのかを検証したメタ研究が最優秀論文賞を受賞した点でしょう。当該論文では、精度比較以前にそもそも再現実験を実施することができた論文が半分以下しか存在せず、その中でも従来法よりほぼ一貫して優れていたのが Mult-VAE という手法だけだったという、いささかショッキングな内容でした。また、2件行われたKeynoteはそれぞれ法学者と社会学者による講演で、いずれも社会とデータとの関わりに関する内容でした。さらに、最終日にはResponsible Recommendation(責任ある推薦システム)というテーマでパネルディスカッションが開催され、フィルターバブルやフェイクニュースを推薦システムの技術が助長しないために何ができるかという点について議論が行われました。研究者コミュニティが単純な精度競争に陥らず、より良い社会を作るための問題意識を共有していることが印象的でした。 Keynote Keynote の内容について簡単に触れます。文中の画像は 公開されているスライド から引用しています。 1件目は、法学者の Mireille Hildebrandt 博士による講演で、行動(データ)主義批判と GDPR(EU における個人情報保護法)に関する内容でした。ヒトは頭で「やるべき」と考えていることを実際に実行するとは限らず、逆に「やるべきではない」と考えていることをついやってしまうこともあります(いわゆるMind-body problem)。したがって、行動データに対して最適化された推薦システムが、人や社会にとって良い結果をもたらすとは限らないという指摘がありました。例えば、ダイエット中の人がカロリーの高い食事を推薦され、それを購入したとします。購入に至ったので行動主義の観点から言えば「良い推薦」と言えますが、社会や本人にとっては必ずしも良いとは言えません。推薦システムの未来を考えるうで興味深い講演でした。 Mind-body Problem (出典: https://recsys.acm.org/recsys19/keynotes/ ) 2 日目は社会学者の Eszter Hargittai 博士から、オンラインサービスから得られるデータに生じうるデモグラフィックなバイアスについて示唆に富んだ講演が行われました。この講演では、年齢・性別・学歴・収入などの違いによって、使用するオンラインサービスの選択、さらにはサービスの使い方にまで差があるということが示されました。例えば、同じWikipediaというサービスでも、閲覧するだけのユーザーと編集を行うユーザーとでは異なるデモグラフィックな特徴があります。したがって、「Wikipedia のユーザー」のような大雑把な認識では、思わぬバイアスに影響される可能性があります。逆に、想像するバイアスが実際には存在しない可能性もあります。例えば、「若者は高齢者よりもインターネットを使うスキルが高い」というステレオタイプはある程度一般的であると思われますが、実際に調査してみると年齢とインターネット・スキルとの間の相関は低かったそうです。サービスの設計を考えるうえでたいへん勉強になる講演でした。 論文紹介 ここからは、筆者の 2 人が個人的に気になった論文を簡単に紹介していきます。担当は以下になります。また、文中の画像はそれぞれの論文から引用しています。 真木 Users in the Loop: A Psychologically-Informed Approach to Similar Item Retrieval Relaxed Softmax for PU Learning 松井 Latent factor models and aggregation operators for collaborative filtering in reciprocal recommender systems SMORe: modularize graph embedding for recommendation Users in the Loop: A Psychologically-Informed Approach to Similar Item Retrieval 著者:Amy A. Winecoff, Florin Brasoveanu, Bryce Casavant, Pearce Washabaugh, Matthew Graham (True Fit) 出典: https://dl.acm.org/citation.cfm?id=3347047 どんなもの? コンテンツの類似度を測る指標として、ユークリッド距離やジャッカード類似度に代わって、心理学の文脈で提案された Tversky の類似度を用いることを提案し、妥当性を検証しました。 先行研究と比べてどこがすごい? ユークリッド距離やジャッカード類似度は、ヒトが心理的に感じる類似度と乖離している可能性が指摘されているので、ヒトの認知の特徴を組み込んだ類似度を使います。また、前述の類似度はすべての特徴量が等しく貢献する式になっていますが、実際は特徴量によって類似度の認知に与える影響が異なるという仮説を検証しました。 技術や手法の肝はどこ? ユークリッド距離やジャッカード類似度は対称ですが、ヒトが感じる類似度は必ずしも対称ではないことが知られています。Tversky の類似度の非対称性を考慮できるモデルになっています。なお、Tversky の類似度は要素が 1 か 0 で表わされるベクトル同士の類似度を測るために使えます(連続値を取るベクトルに対しては使えません)。 どうやって有効だと検証した? 上図のように、クエリの画像が 2 つの選択肢の画像のうちどちらに似ているか答えてもらう主観評価で教師データを収集し、Tversky 類似度を特徴量としてロジスティック回帰で予測しました。比較対象としてジャッカード類似度を用いました。特徴量によって類似度の認知に対する貢献が大きいという仮説は、ロジスティック回帰の係数の比較により確認しました。 議論はある? 論文の著者らは Tversky類似度の有効性を主張していますが、論文に掲載されている数値や図を見る限りでは、ジャッカード類似度との差はほとんどないように見受けられました。心理学の知見を取り入れたアプローチは発展の余地があると思うので、今後の展開に期待したいです。 Relaxed Softmax for PU Learning 著者:Ugo Tanielian (UPMC, Paris, France), Flavian Vasile (Criteo Research, Paris, France) こちらの論文については、 RecSys’19 読み会にて発表してきたスライド があるので、そちらもご覧ください。 どんなもの? 2 値分類において「正例データ」と「ラベル不明データ」の2種類が与えられる Positive-Unlabeled(PU) Learning では、負例をどのようにサンプリングするかが問題になります。そこで新しくボルツマン分布に従って負例をサンプリングする方法を提案しました。 先行研究と比べてどこがすごい? ボルツマン分布を用いた負例サンプリングでは、ボルツマン分布におけるdegeneracy分布を調節することで負例になりやすさ(negativity)に関する事前知識を表現することができます。また、ボルツマン分布の温度パラメータを調節することでサンプリングの性質を変化させられます。 技術や手法の肝はどこ? PU Learning における負例サンプリングでは、以下 2 点のトレードオフを考慮する必要があります。 誤分類しやすい(決定境界の近くにある)データをサンプリングしたい。この場合、本当は正例のデータを負例としてサンプリングしてしまう危険が大きくなります。 正例を誤って負例としてサンプリングしたくない。この場合は決定境界の遠くにあるデータをサンプルすればいいですが、モデルにとっては容易に識別可能なので情報があまり増えません。 提案法では、ボルツマン分布の温度パラメータによってこのトレードオフを調整できます。 どうやって有効だと検証した? 人口データに対する分布推定、単語類似度の学習、Next item prediction のタスクで評価しました。概ね従来法よりも良い結果がでています。 議論はある? 性能を評価する基準によって最適な温度パラメータの値が異なり、理論的に予想されるものと比べて妥当な結果が得られているように思えます。 Latent factor models and aggregation operators for collaborative filtering in reciprocal recommender systems 著者:James Neve (University of Bristol, Bristol, England), Ivan Palomares (University of Bristol & The Alan Turing Institute, Bristol, England) どんなもの? サービス内のユーザーを互いに推薦するシステムを相互推薦システム(RRS, Reciprocal Recommender Systems)と呼びます。相互推薦システムは、オンライン・デーティング・サービス(Tinder、Pairs など)、求人サービスでの応用が考えられます。相互推薦では、相互評価に基づいて推薦が行われます。 相互推薦システム特有の問題は? 任意のユーザーペアについて双方向から 2 つの評価値が得られるため、それらを 1 つの値に集約する関数を考える必要があります。特に、ユーザーペアの間で相互評価が一致している場合(お互いに好き、お互いに嫌い)に推薦するかは自明であるため、相互評価が一致していない場合(片思い)の扱いが特に問題になります。 従来、双方向からの評価値の集約関数には算術平均や幾何平均、調和平均などが用いられてきましたが、性能が比較されてこなかったので、何がベストかはわかっていませんでした。 また、従来法では、ユーザー a からユーザー b への評価値の計算量は、b を過去に高評価したユーザーの人数 n に対し O(n) でした。 先行研究と比べてどこがすごい? それぞれのユーザーを潜在因子で表現し、評価値はそれらの内積とすることで高速に計算できるようにしました。潜在因子は、男性から女性への評価値行列と女性から男性への評価値行列をそれぞれを行列分解することで獲得します。推論時は、ユーザーペアの候補に対し双方向の評価値を予測し、それらを集約した値をもとに推薦します。 また、集約関数の比較を初めて行いました。相互評価が一致していない場合、算術平均では大きい評価値の方を、幾何平均・調和平均では小さい評価値の方を強く反映します。オンライン・デーティングの文脈では、相互評価が一致しない場合は推薦しない方が望ましいので、幾何平均・調和平均の方がより適切であるという仮説を検証しました。 どうやって有効だと検証した? オンライン・デーティング・サービスの Pairs のデータを用いて、ユーザーのペアが成立か否かの 2 クラス分類を行いました。 提案法は、従来法と同程度の F1 スコアを維持しつつ、ユーザーのペアが 100 万ほどある巨大なデータセットでも実用可能な計算量に削減しました。 SMORe: modularize graph embedding for recommendation 著者:Chih-Ming Chen, Ting-Hsiang Wang, Chuan-Ju Wang, Ming-Feng Tsai(National Chengchi University, Taiwan) どんなもの? 本研究は新しい手法の提案ではなく、既存の推薦システムの手法をモジュールに分割した新しい見方を提案し、その見方にもとづいたライブラリを提供しています。モジュール化することで異なる研究を比較するときに見通しが良くなり、実装も楽になります。 github.com 技術や手法の肝はどこ? 協調フィルタリングベースの手法では、学習データはユーザーやアイテムをノードとしたグラフであると考えることができます。もっとも単純な手法ではユーザードメインとアイテムドメインからなる 2 部グラフですが、文脈を考慮する手法では、下図のように 3 つ以上のドメインからなるグラフとみなせます。 出典: http://cherry.cs.nccu.edu.tw/~g10018/recsys19_smore.pdf モデルの学習はグラフ上のノードを何らかの空間(ユークリッド空間など)に埋め込むことであり、推論は空間内での近傍探索を行うことであると解釈できます。 そこで、著者らはグラフ埋め込みを以下の 3 つのモジュールで表現することを提案しました。 Sampler:ユーザー・アイテムのグラフからノードをサンプルする方法 Mapper:サンプルしたノードを特徴量空間に埋め込む学習可能な写像 Optimizer:類似度または距離と、損失関数 発表スライド にある、いくつかの有名な手法を SMORe で表現した図がわかりやすいです。 個人的な感想 SMORe により、いくつかの有名な推薦手法は本質的に、隣接したノードの特徴量どうしは類似度が大きく、隣接しないノードの特徴量どうしは類似度が小さくなるように Mapper を学習していると捉えられるようになりました。 この捉え方は、意味の近いデータの特徴量どうしは近く、意味の異なるデータの特徴量どうしは遠くなるように特徴抽出器を学習させる metric learning の枠組みと似ているように感じました。以前の metric learning に関するブログ記事 で上げた metric learning の 4 つの構成要素と、SMORe の各モジュールの対応は以下のようになると考えています。 サンプリング → Sampler ネットワークの構造 → Mapper 計量 → Optimizer 損失関数 → Optimizer metric learning の応用の 1 つである fine-grained image classification では、N-pair サンプリングが提案されましたが、現在、SMORe で表されている手法の中では言及されていません。ただ、N-pair サンプリングは SMORe の Sampler として表現できます。 このように SMORe の対象分野を、推薦から metric learning の応用先に広げることで、metric learning における既存手法も SMORe の枠組みで捉え直せるか考えてみたいと思います。 また、上記のブログ記事では metric learning の構成要素は、問題設定やデータによって適切な選択肢が異なることを報告しています。そこで、SMORe においても同様に、問題設定やデータごとに各モジュールのどの選択肢が適しているかを明らかにしても面白いと感じました。 最後に 弊社では次々に登場する新しい技術を使いこなし、プロダクトを改善できる機械学習エンジニアを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは、ZOZOTOWN iOSチームの荒井です。今回は9月に実施したZOZOテクノロジーズのサマーインターンシップについて紹介したいと思います。インターンシップを開催予定の方、ZOZOテクノロジーズのインターンシップに興味がある方の参考になれば幸いです。 INTERNSHIP 2019 SUMMER概要 この夏、ZOZOテクノロジーズとしては初となる就業型インターンシップ「 INTERNSHIP 2019 SUMMER 」を実施しました。 実施期間 8/28(水)〜9/10(火), 9/12(木)〜9/27(金) 募集職種 サーバーサイドエンジニア/フロントエンドエンジニア/インフラエンジニア/iOSエンジニア/Androidエンジニア/ビジネスプロデューサー 配属プロダクト ZOZOTOWN/WEAR/新規事業 奨励金 390,000円 今年のテーマは「Thank you!」です。期待とエールをこめて奨励金も39万円となっています。私が所属するZOZOTOWN iOSチームにも9/12〜9/27の期間 あおい さんが来てくれました。ZOZOTOWN iOSチームでは初のインターン生となります。 INTERNSHIP 2019 SUMMERの特徴 今回のインターンシップの特徴は「就業型インターンシップ」であることです。メンターと毎日隣り合ってZOZOが運営するサービスに携わります。エンジニアは本番にデプロイすることがひとつの目標です。実際にプロダクトの開発を行うことで、業務イメージが湧きやすく、サービスの理解も深まります。期間中はミーティングなども社員と同様のスケジュールで行ってもらいました。 選考フロー 選考は以下の流れで実施しました。 応募 応募条件は2020年4月から2021年3月の間で、高校/専門/高専/短大/大学/大学院を卒業見込みもしくは修了見込みの学生です。日程すべてに参加できる方を対象としています。6月19日(水)から7月5日(金)の期間募集をし、ありがたいことに多くの学生から応募がありました。ZOZOテクノロジーズ初のサマーインターンシップということもあり、学生の期待も大きかったようです。また、39万円という奨励金も注目を集めました。 書類選考 履歴書の内容から選考します。履歴書の形式は特になく自由です。執筆論文、研究内容やインターンシップ経験などを書く学生が多いです。エンジニアですとGitHubや開発したサービスの紹介などが伝わりやすく、選考する側も判断しやすいと思います。 課題 課題の内容 書類選考を通過した方には課題選考に進んでもらいました。課題は希望職種によって異なります。具体的な内容は伏せますが、今年のiOSの課題は以下のようなものでした。 Web APIを使用したiOSアプリケーションの作成 一覧画面、詳細画面の実装および画面遷移を実装する 詳細ページには指定した機能を実装する WebViewを使用しないといった、いくつかの課題条件を満たす UI、アニメーション、アーキテクチャ等、実装面でのこだわりポイントについて解説する 就業型インターンシップという性質上、アプリケーション開発経験がある学生を対象としています。課題はモバイルアプリエンジニアが作成しており、ZOZOTOWNやWEARの開発で使用するであろう技術が身についているかを判断する課題となっています。課題の取り組み期間は10日間に設定しており、学生の都合も考慮し、ある程度余裕のある期間を設けています。 フィードバック 課題は提出の一発勝負ではありません。弊社のiOSエンジニアが課題をチェックし、要件を満たしていないところ、コード上で気になるところなどをフィードバックします。フィードバックの修正を踏まえた上で合否を決定します。修正内容は後日行われる面談にて触れていきます。 面談 面談はオンラインで実施しました。提出した課題を元に技術的な話が中心となります。あおいさんの面談でポイントとなった点は以下の内容でした。 1年ほどのSwift歴でClean Architectureの習得に至った経緯 課題もClean Architectureを採用していたが、課題規模に対してのアーキテクチャ選定理由 採用ライブラリの選定理由 弊社エンジニアがコード上で気になった点 こちらの面談はZOZOTOWN iOS担当の ばんじゅん が担当したのですが、一番意識していたのは「 実装面でのこだわりポイントについて解説する 」という点です。あおいさんは「アーキテクチャの選定にはこだわるようにしている」とのことだったのでアーキテクチャの話題が中心になりました。後日あおいさんに尋ねたところ、インターンシップを通してこの面談が一番緊張したようです。 面接 課題合格した方は最後に面接があります。面接は配属先予定のチームリーダーが実施します。 今回の面接での確認ポイントは以下の3つです。 一緒に働きたいと思えるか、会社の経営理念と照らし合わせる インターンシップをやりきる力がありそうか 技術的な面の再確認 技術的な面は課題で確認しているため、1と2を重点的に確認します。特に理念との照らし合わせは採用時にも重要視している項目のひとつです。 インターンシップの目標設定 今回のインターンシップで珍しいところは「キックオフ面談」があるところです。 多くのインターンシップでは会社がインターンシップ期間中の課題を決めると思いますが、今回はインターン生の希望を聞いた上で目標を決め、インターンシップ期間中の課題を決定します。 今回あおいさんからは以下の希望がありました。 サービスのレガシーな部分をモダンにする方法を見てみたい 大きなサービスの基盤を触りたい チーム間でどのようなコミュニケーションをとっているか見てみたい 厳しいフィードバックが欲しい この中で特に驚いたのが「レガシーな部分をモダンにする」という内容です。学生から出てくるのは珍しいと思います。インターン生の課題設定は難しく、今回私たちも学生が楽しめるような新機能の追加やUI/UXの変更など、見た目上でも達成感がある課題を用意しようとしていました。そんな中、あおいさんのインターンシップに求める内容の説明は感心させられるものでした。要約すると以下になります。 「エンジニアをやっていく中で、レガシーな部分と向き合っていく必要性を感じている。モダンなUI実装などは個人開発でも試せるが、レガシーな部分をモダンにするという経験はレガシーコードが無いとできない。ZOZOTOWNは長く運営されているサービスなのできっとまだあるはず」 確かにZOZOTOWNは運営期間も長いため、レガシーなコードはまだまだあります。まさに私たちが業務で向き合っている課題であり、学生からこの目標を聞いた時は非常に嬉しく思いました。このようなやり取りを「キックオフ面談」で行い、インターンシップでの目標は「 レガシーなコードをモダンにする技術を学び、実践する 」に決定しました。 こちらで一方的に用意するのではなく、事前に擦り合わせを行うことでインターンシップでの有益性も変わってくると思います。 実施内容 スケジュール 10日間のインターンシップで目標が達成できるようにスケジュールを作成します。初日のセットアップやイベントなどはメンターが事前に決めておきますが、それ以外はインターン生、メンターで随時相談していきます。 9/12 ・人事による説明 ・開発環境のセットアップ ・メンバー紹介 ・インターンシップ目標の詳細を設定 ・インターンシップ課題の説明 ・ZOZOTOWN iOSの説明 ・初日の目標設定 ・ZOZOTOWNのビルドや実行確認 ・練習課題の開発 9/13 ・開発 ・動作確認などのウォークスルー ・状況にあわせてタスクを切り出す(都度メンターがフォロー) 9/17 ・開発 ・ZOZOTOWN iOSのレガシーな構造についてオーバービュー ・iOS社内勉強会への参加 9/18 ・本課題の開発 9/19 ・開発 ・ZOZOテクノロジーズscrumへ参加 9/20 ・開発 9/24 ・開発 ・After iOSDC Japan 2019へ参加 9/25 ・開発 9/26 ・開発 ・成果発表の資料作成 ・iOS技術共有会へ参加 9/27 ・資料の改善 ・成果発表 ・懇親会 開発 今回の課題はZOZOTOWN iOS内にあるObjective-CのコードをSwiftに置き換えるリファクタリングです。事前に「大きなサービスの基盤を触りたい」という要望があったため、機能が多く影響範囲も広いファイルを選定しました。最初の数日はZOZOTOWNアプリの開発に慣れてもらうため、1機能で完結する影響範囲が狭い練習課題から着手してもらいました。 インターンシップ中は常にメンターが隣にいるため、気軽に相談できる環境になっています。開発中は行き詰まることも多かったとのことですが、メンターに相談しながら解決していくことで「サービスのレガシーな部分をモダンにする方法を見てみたい」という希望が叶えられたのではないかと思います。 また、「厳しいフィードバックが欲しい」という希望も事前にあったため、コードレビューはメンター以外のエンジニアも参加しています。 コードレビューには 技術顧問の岸川克己 さんにも参加して頂きました。 あおいさんはiOSDC Japan 2019で岸川さんのセッションを最前列で聴いていたらしく、弊社の技術顧問と聞いた時には驚いていました。開発体制やコードレビューの雰囲気なども感じて頂けたかと思います。 成果発表 最終日は10日間の成果発表を行います。この成果発表は代表取締役社長や取締役、VPoEも出席します。社員も自由に参加できるようになっているため、10日間一緒にやっていたチームメンバー以外も多く出席しました。 今回はObjective-CからSwiftへの移行という、アプリケーションの見た目が変わらないテーマでありましたが、iOSエンジニア以外にも内容が伝わる資料にまとめられていました。インターン生は中々このような発表の経験は少ないことも多いので、資料作成の時間や練習の場、フィードバックを受ける環境などをしっかり整えることが重要だと思います。 発表会の終わりには「Thank you!」にちなんでインターンシップ期間中に関わった39人からのメッセージとメンターから修了証書の授与がありました。 社内イベント 10日間のインターンシップでは、開発以外にも様々な社内イベントに参加してもらいました。社員と同様の働き方をすることで、ZOZOテクノロジーズのメンバーが普段どのように業務にあたっているかがより伝わったかと思います。インターンシップ期間中にあったイベントを紹介します。 iOS社内勉強会 弊社のiOSチームでは定期的に社内勉強会を行っています。今回は「Human Interface Guidelines」の「System Capabilities」がテーマでした。資料は有志のiOSエンジニアが持ち回りで作成していますが、参加者はiOSエンジニアだけでなく、デザイナーや品質管理チームからも希望者が参加しています。社内勉強会の雰囲気を感じて頂ける良い機会だったと思います。 iOS技術共有会 iOSチームは「ZOZOTOWN」「WEAR」「PB」の3チームあります。それぞれ要素技術は違いますし、勤務地も離れているため、「iOS技術共有会」という形で定期的に情報共有しています。この共有会には技術顧問の岸川さんにも参加していただき、ざっくばらんに意見交換を行っています。あおいさんにはiOSエンジニアの前で成果発表の予行練習をして頂きました。iOSエンジニアとしてのチェックや、資料の構成など、詳細を知っているZOZOTOWN担当以外からもアドバイスを受けていました。 ZOZOテクノロジーズscrum 社員が起案し開催するZOZOテクノロジーズの全社員イベント「ZOZOテクノロジーズscrum」に参加してもらいました。今回は「Data for the people」をテーマに弊社 データサイエンス・アドバイザーのアンドレアス ワイガンド氏 によるセミナーが行われました。ZOZOテクノロジーズscrumについては弊社 COMPANY BLOG をご覧ください。 当日は懇親会もあり、普段業務で接点があるエンジニア以外とも交流する良い場になったと思います。 After iOSDC Japan 2019 9月24日、弊社で After iOSDC Japan 2019 を開催しました。Sansan株式会社さん、JapanTaxi株式会社さんとの合同イベントで、本人が参加希望していたイベントということもあり、ZOZOテクノロジーズ関係者として参加してもらいました。当日イベントに参加するだけでなく、弊社登壇者の発表資料読み合わせなど、準備段階から参加してもらいました。 まとめ 今回はINTERNSHIP 2019 SUMMERについて紹介しました。インターンシップは学生が学ぶ場というイメージがありますが、あおいさんのエンジニアとしての学習意欲、問題に対して向き合う姿勢などは良い刺激になり、我々も学びが多い10日間でした。あおいさん、本当にありがとうございました! インターン生あおいさん、メンターばんじゅんからコメント頂いたので記載します。 あおいさんからのコメント 今回インターンシップで取り組んだのは、ZOZOTOWNのiOSアプリのリファクタリングです。メンターのばんじゅんさんの的確すぎるご指導のもと、毎日楽しく学びながら仕事ができました。また、ランチや日々の業務を通じて気づいたことは、ZOZOテクノロジーズのメンバーは本当に優しく温かい人達ばかりだということです。こんな居心地の良い環境の中で自分の大好きなサービスの改善に携わらせていただけたことは自分にとって大きな財産となりました。総括するとこのインターンシップに参加でき本当に良かったです!お世話になった皆さんありがとうございました。 ばんじゅんからのコメント レガシーと戦う技術を持つエンジニアが欲しいといつも思っているなかで、インターン生として、そのモチベーションを高く持っているあおいさんが来てくれたことは、そのレア度もあり驚きました。業務の中ではObjective-Cを攻略する戦術を学んでいただいたのですが、それ自体は広くどこでも役立つ知識とも言えないなかで、あくまでレガシーにフォーカスして楽しんで取り組んでいただけたようです。わたしたちのリファクタリングに対する取り組みにもポジティブな影響が出ていたと思います。 モダン化にあたっては実際の移行作業だけではなく現状の調査や分析が大きなタスクになりますが、今回のインターンシップではプロダクトの実際のコードや業務に貢献する目的があったため、具体的な分析がまだ行なわれていない部分を題材に調査から実施していただきました。Objective-CとSwiftの相互運用の特性を踏まえて分析しながら現実的な規模へのタスクの分解を考えていただき、限られた期間のなかで、プロダクトへ反映できるコードの作成を完了していただきました。これは単純な開発体験ではなく、わたしたちが普段実施する業務そのものです。今回その課題を提供できたことと、実業務への真の貢献を得られたことは、本当に良い結果であったと考えています。 さいごに ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは! 開発部の手塚( @tzone99 )です。普段は社内ERPシステムの開発をしながらその周辺の業務ツールの制作を担当しています。こちらの記事ではGoogle Apps Script(GAS)を使ってアパレル商品の検品結果を登録するツールを作る中でポイントとなった部分を共有します。 使い慣れたGoogleのサービスをGASで連携させてお手軽にサーバーレスなアプリケーションを作りたい、という方の参考になればと思います。 背景 一般的な製造/物流業と同様、ZOZOでも倉庫で保管するアパレル商品にサイズや外観上の不備(ほつれ、汚れ、付属品の不足など)がないかをチェックする検品という作業があります。今回は「通常と異なる手順で検品し、その結果を集計/分析し仕分けに使いたい」という特定の商品群があったため、入力した結果を保存する専用ツールを作成することになりました。 何を作ったか 商品のQRコードを読み取ると検品項目が表示され、検品結果を入力して送信するとSpreadSheetに結果が保存されるツールを試験的に作成しました。 環境構成は以下の通りです。 単純にGitHubのコードをGAS側にデプロイしたい場合は Chrome 拡張 を使う方法もあります。今回はローカルにインストールしたエディタで開発したかったので clasp を使ってローカルのコードをGAS側にデプロイする方式を選択しました。 なぜGASを選んだか 今回のツールは期間限定で特定の商品群のみに使う想定で、また他のシステムとも疎結合で独立して動かせるものだったため、GASの持つ以下のようなメリットを見込んで採用しました。 社員は全員Googleのサービスが使える(アカウントを持っている)のでアクセス権限の管理や仕様の説明が簡単。 サーバー管理を考えずに運用できる。 SpreadSheetにデータを保管していればエンジニアでなくても(SQLがわからない人でも)欲しい形式でデータを取得したり統計処理しやすい。 GASをWebアプリケーションとして使う際のポイント スクリプト実行時、ブラウザにWebページを表示する 以下の記述により、アプリケーションを実行すると src/gas/checking.html がブラウザに表示できます。 //main.gs function doGet() { return HtmlService.createTemplateFromFile('src/gas/checking').evaluate(); } 任意のjsファイル、cssファイルをインポートしたい場合、GASではそれらのファイルをそのまま扱えないためhtmlファイルとして記述する必要があります。例えば今回導入したBootstrap(Bootstrap v4.3.1)をインポートするケースを紹介します。 Bootstrapのcssテンプレート src/gas/css/bootstrap.min.css をインポートする場合はファイルの拡張子をhtml( src/gas/css/bootstrap.min.html )に変更し、ファイルの中身を全て <style> タグで囲います。 < style > /*! * Bootstrap v4.3.1 (https://getbootstrap.com/) * Copyright 2011-2019 The Bootstrap Authors * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ ...中略 </ style > Bootstrapのjsファイル( src/gas/js/lib/bootstrap.min.js )をインポートする場合も同様に拡張子を変更し( src/gas/js/lib/bootstrap.min.html )ファイルの中身を全て <script> タグで囲います。 < script > /*! * Bootstrap v4.3.1 (https://getbootstrap.com/) * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ ...中略 </ script > トップページの checking.html に以下のように追記し、各ファイルをインポートします。 <?!= HtmlService.createHtmlOutputFromFile('src/gas/css/bootstrap.min').getContent(); ?> <?!= HtmlService.createHtmlOutputFromFile('src/gas/js/lib/bootstrap.min').getContent(); ?> このような方法で任意のjs/cssファイルをGASのアプリケーションへ流用できます。 google.script.runでGASのメソッドを非同期に実行する htmlからGASのメソッドを実行する方法は主に以下の2通りです。 任意のメソッドを実行するための doGet() , doPost() を定義し、そこに向かって httpReqest を投げる google.script.run からメソッド指定で呼び出す GASはお手軽に実行できる一方で、一般的なWebアプリケーションよりパフォーマンスに優れているとは言えません。可能な限りAPIは非同期に実行して体感の待機時間を少なくしたいと考えました。 doGet() 、 doPost() をajax通信で非同期実行しようとすると、同一生成元ポリシーの制約により実行が制限されるため素直に google.script.run を使うのが良いと思います。 以下はGASファイル(拡張子がgsのファイル)に定義した insert() をフロント側(html)から google.script.run で実行する場合の例です。 // 検査結果(inspectionData)をSpreadSheetに挿入する(insert)処理の例 google.script.run.withFailureHandler(onInsertFailure).withSuccessHandler(onInsertSuccess).insert(inspectionData); withFailureHandler 、 withSuccessHandler を別途以下のように定義することで実行後の処理やエラーハンドリングの制御が可能です。 function onInsertSuccess(result) { // insertメソッドが成功した場合の処理 } function onInsertFailure(e) { // insertメソッドが失敗した時の処理 } SpreadSheetへの読み書き SpreadSheetに定義したサイズデータから必要な行を抽出する際の実装例を紹介します。以下のように、製品を特定する情報(A列)と検品時の計測部位、想定されるサイズ(製品寸法)が紐づいたサイズデータを予め用意しておきます。 製品のQRコードを読み取った後、QRコードの情報に合致するサイズのみを画面に出力します。エラーハンドリング等、一部省略しています。 function checkSize(qrCode) { var result = {}; const FOLDER_ID = 'xxx'; // サイズデータを格納したGoogle DriveのフォルダID const DEFAULT_SHEET_NAME = 'シート1'; // QRコードに含まれる製品コードを取得 var productCode = qrCode.substr(0, 9); // 製品コードに対応したSpreadSheetをサイズデータフォルダから取得 var ss = getTargetSpreadSheet(FOLDER_ID, productCode); var sheet = ss.getSheetByName(DEFAULT_SHEET_NAME); // サイズデータを含んだシートを取得 var lastRow = sheet.getLastRow(); // 最終行を取得 var allSizeDataRaw = sheet.getRange(1, 1, lastRow, 4).getValues(); // シート内の全てのサイズデータを取得 // 該当製品のサイズデータからQRコードの情報に完全一致する行のみを抽出しreturn var sizeMap = allSizeDataRaw.filter(function (row) { if (row[0] === qrCode) { return row.shift(); } }); result = { status: 200, message: 'OK', content: sizeMap }; return result; } サイズデータはパフォーマンス改善の都合で製品コード単位に分割しました。というのも検品対象となった製品群のサイズデータは約30万行×4列あり、そこからQRコードで読み取った製品のデータのみを抜き出そうとすると約12秒かかっていました。製品コード単位(約300ファイル)に分割してからサイズデータを取得する上記の方法に変えることで約5秒程度までレスポンスが改善できました。APIの実行回数を減らすのがGASのパフォーマンス改善の基本ですが、それだけだとパフォーマンスが不十分なケースもあります。今回のように大量のデータから必要な行だけを読み取るケースでは適切な単位にファイルを分割することも改善策として有効でした。 SpreadSheetへの書き込みは以下のようなコードで実現しています(こちらもエラーハンドリング等一部省略)。 // 検査データをSpreadSheetに保存する function insert(inspectionData) { const FOLDER_ID = 'xxx'; // SpreadSheetが格納されたフォルダID const DEFAULT_SHEET_NAME = 'シート1'; // 記録するシート名 var fileName = 'returns_inspection'; // 保存するSpreadSheetのファイル名 var ss = getTargetSpreadSheet(FOLDER_ID, fileName); var sheet = ss.getSheetByName(DEFAULT_SHEET_NAME); var rowData = [inspectionData.attrA, inspectionData.attrB]; // 検査結果を配列rowDataに格納 sheet.appendRow(rowData); // SpreadSheetに一行追加 } // フォルダから特定の名前のファイルを取得する function getTargetSpreadSheet(folderId, fileName) { var folder = DriveApp.getFolderById(folderId); var files = folder.getFiles(); while (files.hasNext()) { var file = files.next(); if (file.getName() == fileName) { var existingSS = SpreadsheetApp.openById(file.getId()); return existingSS; } } return ''; } GASの使い所はどこか 実際にGASを業務用Webアプリケーション構築で使ってみて、Googleのサービスと簡単に連携できる強みを実感できました。とはいえパフォーマンス面やGAS特有の仕様など、Webアプリケーションの単純な置き換えにはならない面もあり使い所が重要です。今回の検品ツール作成中に検討した範囲で言えば、今後も以下のような条件を満たしていれば積極的にGASを使っていこうと思います。 Googleのサービスを普段使っていて業務データがGoogle Driveの中にある。 例えば専用のDBサーバーを立ててデータを移管するのが仰々しいようなケースで有効です。 アクセス権を事細かに設定する必要がない、利用者が限定的な場合。 例えば現在はスクリプトの実行権限を このように 指定できますが、これ以上の制御が必要であれば別途実装が必要です。 扱うデータ量が少ない、もしくはパフォーマンスを気にしない場合。 例えば利用範囲が限定的、利用頻度が少ない、バッチ処理等で有効です。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、 こちら からぜひご応募ください。
アバター
こんにちは。ZOZO研究所Zの藤嶋( @fjkdiet )と中丸( @ixd_circle )です。 本記事では、 新たなテキスタイル材料やその応用可能性について議論するために開催された国際会議、 Comfort and Smart Textile International Symposium 2019 の様子を報告します。 ZOZO研究所Zとは はじめに本ブログに初登場のZOZO研究所Zについて簡単に紹介します。 ZOZO研究所 のなかで、中長期の研究開発を行う部署として新設されました。ファッションに纏わる様々な最新技術、研究潮流について調査し、そこからファッションの未来をつくる研究に取り組んでいます。 今回は、スマートテキスタイルと快適性を主題にした国際会議であるComfort and Smart Textile International Symposium 2019に参加し、国内外の様々な企業や大学といった研究機関の取り組み、動向について調査してきました。それらの内容の一端をご紹介していきます。 開催概要 本国際会議は日本繊維製品消費科学会の60周年を記念し、2019年9月6日〜7日に奈良で開催されました。 近年、第四次産業革命と呼ばれるAI、ロボティクス、IoT(Internet of Things)といったテクノロジーの急速な発展に伴い、あらゆるモノのデジタル化が進んだ結果、繊維産業も急速に変化を遂げています。 特にスマートテキスタイルと呼ばれるセンシングや温度変化といった様々なトランスデュース機能を備えたテキスタイルは、ヘルスケア、スポーツ、医療といった多様な領域のICTソリューションとして重要性を高めると考えられています。 このような潮流を受け、この国際会議ではスマートテキスタイルに関する技術や活用事例を企業や研究者の垣根を越えて共有し、研究領域のさらなる発展を促すことを目的としています。 基調講演は、ヒューマノイドロボット研究者の石黒先生、 Project Jacquard を発表しスマートテキスタイル商品化の先行事例となったGoogle社、スマートテキスタイルの生産効率化への活用を試みているBMWといった企業が行い、最新の開発事例や将来的なビジョンが共有されました。 またポスターセッションでは大学だけではなく、多くの企業も参加し様々な議論が交わされていました。 ここからは本会議に参加して感じた、スマートテキスタイル業界の近年の傾向や紹介された内容の一部を紹介していきます。  本会議での報告内容について 要素技術の研究 各発表では、材料や素材そのものの研究もあれば、心拍をはじめとする身体情報を計測するデバイスについても多くの発表がありました。 例えば、エレクトロスピニングという手法でAtacticのポリスチレンを紡糸した素材の圧電特性について発表していました *1 。素材、特に繊維の研究では、これまで主に通気性や強度、質感といった要素に焦点が当てられてきました。しかし、素材に加わった歪みに対して電気を発生させる圧電性といった特性が繊維と掛け合わせられると、衣服の着用者の動きや、衣服に加わった負荷といった項目を測定することが可能になり、布地の機能が拡張します。こういった動作や変形に対して電気を生み出す技術は、Motion Energy Harvestと呼ばれ、衣服だけではなく、様々な分野に応用が期待されるものです。例えば、このような素材を大きくすることができれば、将来的には発電といった用途にも応用できるでしょう。太陽電池などと異なり、天候に影響を受けない発電装置となる可能性を秘めています。その他にも、導電性のテキスタイルを用いた脈拍モニタリング、伸縮状態を抵抗値の変化から推定する技術なども発表されていました。 こういった素材そのものの技術的な研究に加えて、スマートテキスタイルの活用方法、またスマートテキスタイルを搭載したデバイスやその効能を検証するための研究もなされていました。 これには、デバイスが人が身に着ける衣服という形態をとることで、ユーザーの認知や反応にどのような影響を与えるかといった生理学的な検証といったものがあります。また、衣服型デバイスの展開時に指標となるものとして、視線や心拍といった生体情報を分析に活用する試み *3 。これはペットに厚さ300μm程の柔らかい電極を備えた衣服型デバイスを着用させることでAAI(Animal Assisted Intervation:動物介在介入)中の犬の心拍データをモニターし、その健康状態を把握するといった試みです。 このようにスマートテキスタイルの研究は非常に学際的で、産学が連携して様々なアプローチがなされています。今回この会議に参加したことで、スマートテキスタイルが普及する未来をより身近に感じることができました。 まとめ ZOZOテクノロジーズ・ZOZO研究所Zではファッションの新たな可能性を切り拓くべく、今回紹介したスマートテキスタイルをはじめ、多様な技術やその応用可能性について日々、研究開発を行っています。 それぞれの専門的な視点、技術から新たなファッションを描いていきたい人、ぜひ一緒に挑戦してみませんか。 www.wantedly.com おまけ 会場の周りにもたくさんの鹿が。休憩時間に鹿せんべいを買ったところ、集団に囲まれてびっくりしました。 参考文献リスト *1 : [1]Chonthicha Imusrivun 1 , Shintaro Kurihara 1 , Ryusei Kitayama 1 , Atsushi Yokoyama 1 and Yuya Ishii 1 , "Piezoelectric response of amorphous electro spun fibers off atactic polystyrene", 1. Faculty of Fiber Science and Engineering, Kyoto Institute of Technology *2 : [2]Akie Naito 1 and Reiko Hashimoto 2 , "Impressions Formed from Women's Clothing and Eye Movements of the Evaluators:Differences by Evaluator of Gender", 1.Ochanomizu University, 2.Sugiyama Jogakuen University *3 : [3]Yoko Komatsu 1 , Kazunori Hirata 2 , Yasushi Atsuta 3 , Hiroko Shibanai 4 and Mitsuaki Ohta 5 , "Measurement of Dog's Heart Rate using Dog Wear with Electrodes", 1. TOYOBO CO., LTD., 2. TOYOBO. STC CO., LTD., 3.Unicharm Corporation, 4. Japan Animal Hospital Association, 5. Tokyo University of Agriculture
アバター