TECH PLAY

株式会社一休

株式会社一休 の技術ブログ

161

この記事は一休.comアドベントカレンダー2018の6日目です。 qiita.com 一休では、2016年の10月から Rundeck を使ってバッチジョブの実行管理を行なっています。 導入からおおよそ2年たちました。 その間にデータセンターからAWSへの移行やいくつかの運用トラブルなどを経験しました。知見が溜まってきたので導入編と運用編の2つの記事に分けて紹介したいと思います。 今回はまず、導入編として、導入の背景と実際の導入作業で工夫した点、苦労した点を紹介します。また、Rundeckを導入したことで得られた改善についても紹介します。 Rundeckとは Rundeck社 が提供するOSSのジョブ管理ソフトウェア。有償版もある。 ジョブフロー構築、失敗の自動リトライ、開始終了に対する通知フックなど、 一般的なジョブエンジンの機能を持つ。 Java + Groovy + Grailsで実装されている。 スケジューリングの定義は、cron形式。 管理画面はWebブラウザで操作できる。 SSH経由でリモートのコマンドを実行できる。 SSHが通るマシンであれば、OSを問わず、どのマシンのプログラムでも定期実行できる。 脱Windows タスクスケジューラ Rundeckを導入する前、一休ではサービス運用に必要なバッチ処理をWIndows タスクスケジューラで実行していました。登録されたタスクの数は、100を超えていました。 タスクスケジューラで100を超えたタスクを管理するのはとても辛いです。なにが辛いかというと、 タスクスケジューラのGUIは大量のタスクを管理するのに向いていない。 タスクスケジューラのGUIはフィルタや検索ができません。タスクの数が少なければ問題ないのですが、100以上のタスクを管理しようとするとかなり苦労します。例えば、手動でタスクを動かす必要がある場合、タスク一覧を目視で舐めて目的のタスクを見つけるという辛い作業をしていました。 ジョブフローが組めない。 100以上のタスクがあれば、実行順序に依存関係のあるタスクもあります。しかし、タスクスケジューラでは、「タスクAが正常に実行を完了したらタスクBを動かす」というようなジョブフローが組めません。 なぜRundeckを選んだのか 脱Windows タスクスケジューラを目指すため次の4つの要件を満たすジョブエンジンを探しました。 ジョブフローが組めること GUIがわかりやすいこと。ブラウザでアクセスできること。 WindowsとLinux両方で動くこと。 一休はWindows系の技術スタックをメインに使っていますので、Windowsで動くことが必要ですが、Linuxサーバも使いますので、Linuxもサポートする必要があります。 OSSであること。 プロプライエタリな製品だと設計や運用でライセンスを気にする必要が出てきます。また、機能検証しにくいこともあるかもしれないので、まずは、OSSで探して、良さそうなものが見つからなければ市場調査をしようと考えました。 この基準で判断した結果、以下のふたつが、候補になりました。 Rundeck SOS Jobscheduler 決め手のひとつはGUIのシンプルさでした。SOS JobschedulerもブラウザでアクセスできるGUIを持っていますが、Rundeckの方がわかりやすいです。 また、日本語の情報もRundeckの方が多かったため、Rundeckを選択しました。 そして最大の決め手は、「SSHさえ疎通すれば、どんなマシンのどんなコマンドでもcron実行できる」という柔軟さです。これによって、ジョブ管理をするサーバとジョブを実行するサーバを分離できます。ジョブ実行に必要なリソースが足りなくなったら、ジョブ実行をするサーバだけ増やせば済みます。そして、ジョブを実行するサーバはOSを問いません。 構成 現在の構成は以下の通りです。 RundeckサーバもジョブサーバもすべてEC2です。導入当時はデータセンターの物理マシンでしたが、構成自体は上の図とほとんど変わりません。 ジョブの実行ログはS3に保存し、RundeckのデータベースにはRDSを利用しています。AWSのサービスを最大限利用した構成にしました。 現時点では、RundeckサーバもジョブサーバもWindows Serverです。 導入にあたって、工夫した点、苦労した点を紹介します。 管理画面の認証 Rundeck自身がユーザー管理の機能を持っています。しかし、50人近くいる開発者全員のアカウントをRundeckに登録して適切に管理するのは大変です。外部の認証機構と連携する必要があるのは自明でした。 RundeckにはActive Directoryと連携する機能があります。また、oauth2_proxyに対応しているので外部のOAuth 2.0 サービスプロバイダーとも連携できます。 一休ではデータセンターにRundeckがあった時代は、AD連携機能を使って認証を行なっていました。クラウドに移行したときに、Rundeckサーバから社内のADが見えなくなってしまったので、oauth2_proxyとGitHubのOauth2の仕組みを使って、GitHubアカウントでログインできるようにしました。 ※ oauth2_proxy は bitly社が開発しているOSSで、リバースプロキシとして動作し、oauthプロバイダとのやりとりを代理してくれる便利なツールです。Goで開発されているのでWindowsでも問題なく動作します。 ※ このoauth2_proxy+GitHubでの認証の仕組みの構築には、 minamijoyoさんの記事 を参考にさせていただきました。ありがとうございます! EC2プラグイン Rundeckには EC2プラグイン があります。このプラグインを使うと特定のタグがついているEC2インスタンスを自動的にRundeckのジョブサーバにできます。 これによって、なんらかのメンテナンスでジョブサーバの入れ替えや再構築が必要なときも簡単に対応できます。 また、動的にジョブサーバを追加することもできます。例えば、常時動作しているバッチサーバのスペックでは処理しきれないような大規模バッチ処理がある場合、高スペックなEC2インスタンスを起動して処理をさせ、完了したら、そのEC2インスタンスを停止する、という一連の流れを手動操作を介在させることなく実現できます。 sshサーバ Linuxサーバなら悩む必要はないのですが、一休ではジョブを実行するサーバはWindowsサーバです。環境を構築した時点では、公式のWindows環境の OpenSSH の実装は、動作が不安定で使えませんでした。そこで、Windows環境で動作するSSHサーバを有償無償問わず、調査したところ、 bitvise ssh server 最適と判断しました。 有償ですが、安い。ライセンス買い切り。 設定が簡単でシンプル。 当初は、無償の freesshd で進めようと思っていたのですが、動作が安定せず、断念しました。 タスク移行 タスクスケジューラ上の100以上あるタスクを手動でRundeckのジョブとして移行していたら絶対にミスをします。また、当然、移行作業中も通常のサービス開発は行われています。新しいバッチ処理が追加されているかもしれません。移行作業と開発との間の齟齬が起きないようにする必要がありました。 そこでタスクスケジューラからxml形式でタスクをエクスポートし、Rundeckのジョブ定義xmlに変換するプログラムを書き、そのジョブ定義xmlをRundeckにインポートすることで、スムーズかつ齟齬がないように移行しました。 ジョブのエラー通知 Rundeckには、タスクの完了(成功、失敗)を通知する仕組みがあります。メール通知、webフックの呼び出しができます。また、プラグインを利用することでslackにも通知が飛ばせます。一休ではサービスのエラー通知はすべてslackに飛ばしています。なので、プラグインを使ってslackに通知しようと考えました。しかし、以下のふたつの理由でこのプラグインを使ってのslack通知はやめました。 ひとつひとつのジョブに設定しなければならないのが面倒。新しく追加したジョブに設定漏れが起きそう。 通知内容が少ない。特にエラーになった場合は、実行したコマンドのステータスコードや標準出力の内容も通知したい。 そこで、以下のような方法にしました。 S3プラグイン を使ってジョブの実行ログをS3に出力する。 S3へのログのPutをトリガにして動作するAWS Lamdbaを実装する。 このLamdbaはログの中身をみてエラーだったら、エラー内容(ステータスコードや標準出力)をSlackに通知する。成功の場合は通知しない。 このようにすることで、ジョブの通知設定に関わらず全てのエラーをslackに通知することができました。 ansibleを使って環境構築 oauth2_proxyのインストールやRundeckのインストール、各種構成ファイルの設定、監視の設定はすべてansibleで行うようにしました。こうすることでトライアンドエラーを繰り返しながら上述したような技術検証ができました。また、ansibleがWindows環境でも問題なく使えることがわかったのも収穫でした。 改善ポイント 当初導入によって目論んでいた改善はほばすべて達成しました。 ブラウザから管理画面にアクセスできるようになったのでタスクスケジューラよりもはるかに簡単に管理できるようになりました。ジョブのフィルタもできます。また、タスクスケジューラ時代はさまざまな事情がありジョブの登録や変更は特権を持った特定のエンジニアしかできないようになっていました。このルールもRundeck移行によって見直すことができました。 ジョブフローも活用されています。導入当初は、使われていませんでしたが、数ヶ月経つと、特定のエンジニアが導入を推進する、ということをしなくても、自然と使われるようになっていきました。 また、当初見込んでいた改善ではないですが、タスクスケジューラを使ったバッチ処理よりもインフラの可用性は大きく向上しました。バッチの実行管理をするRundeckサーバと実行の定義のストアであるRDS、そして実際にバッチを実行するバッチサーバの3つを分離できました。この3つそれぞれ別々に障害対策を考えればいいので運用がしやすいです。フェールオーバの手順はタスクスケジューラ時代の障害時フェールオーバの手順よりもかなりシンプルになりました。 終わりに 今回紹介した内容は1年半くらい前に実施したことなので少し情報が古いかもしれません。しかし、LinuxのcronではなくWindowsのタスクスケジューラからRundeckに移行した例はあまりないのでは、と考え、紹介しました。RundeckはWindows環境でも十分に活用できます。 次回は、運用編として、この2年間で起こった運用トラブルとその対処について、紹介したいと思います。 この記事の筆者について システム本部CTO室所属の 徳武 です。 サービスの技術基盤の開発運用、宿泊サービスの開発支援を行なっています。
アバター
この記事は一休.comアドベントカレンダー2018の5日目です。 qiita.com こんにちは。 CTO室の村石です。 一休ではAmazon Connect を導入し、カスタマーサービスの一部コールセンターを新しい体制へと変えました。 今回は導入した Amazon Connect に関して、概要から導入後の運用まで幅広く話をしようと思います。 Amazon Connect とは AWSが提供するクラウド型のCTI *1 システムのことで、コールセンターを簡単に作れることが魅力のサービスです。 また、Webサービスになっているため、ブラウザがあれば利用できます。 aws.amazon.com コールセンターではよくある以下のようなことも、少しの設定で実現出来ることは大きな魅力だと思います。 音声ガイダンスを流す 案内で番号をプッシュさせる 特定の担当部署に電話をつなげる 料金については、従量課金制となっており、通話が多いほど高くなります。 一方、オペレータ数などは料金の対象にならないため、柔軟に変更が可能です。 Amazon Connect は現在東京リージョンで提供されていませんが、近い将来提供されることが発表されていて今後が楽しみなサービスです。 (18/12/11 追記) 正式に東京リージョンでサービス開始されたようです。 ますます Amazon Connect の今後が楽しみなサービスですね。 aws.amazon.com Amazon Connect の導入方法については割愛しますので、AWSの各種ドキュメントをご覧ください。 Amazon Connectの特徴 個人的に特に気になった特徴を紹介します。 短期間、初期費用不要で構築が可能 Amazon Connect インスタンスを作ってから、単純に電話できるようになるまでは、慣れれば数分で実現出来ます。 実際の運用環境を整えるには別途時間がかかりますが、時間のかかる電話線工事などがカットできるのは大きいです。 各種設定変更が楽 ここでいう各種設定とは以下のようなことです。(あくまで一部です。) オペレータの追加 コールフローの編集 営業時間の変更 Amazon Connect では、これらを管理サイトから変更することができます。 コールセンターの構築にベンダーを使うと、依頼が必要があったりと、オペレーションコストかかってしまいます。 時間がかかりがちの調整をせずとも、すぐに設定変更を行うことが出来るのは大きなポイントだと思います。 (現時点で)シドニーリージョン 前項でも述べたように、まだ日本で提供されていません。 今すぐ利用するためにはシドニーリージョンでの利用となります。 懸念として音声データの遅延が挙げられる思いますが、弊社が検証したところ、業務に差し支えるほどの遅延は確認できませんでした。 また、リージョンが海外のため、内部的には国際電話となります。 料金はそれほど気になりませんが、電話をかける際のダイヤルには気をつけないといけません。 しかし、東京リージョンでサービス開始されたとして、ダイヤルする際は国際電話でない保証はありません。 (18/12/11 追記) 「Amazon Connect とは」で追記した通り、東京リージョンにてサービス開始されましたので、削除させていただきました。 Amazon Connect の専用管理サイトが存在 AWS コンソール上の運用になるイメージを持つかもしれませんが、違います。 AWS コンソールでAmazon Connect のインスタンスを作成すると、専用の管理サイトが作成され、コールセンター業務は作成されたサイトで管理することになります。 電話を受けるオペレータのアカウントは、管理サイトから発行することになり、AWS アカウントとは別に存在します。 一通り構築が出来たらエンジニアは運用業務から離れることが出来る、というのも一つ特徴かと思います。 Amazon Connect のシステム システム構成 電話のフローから見る構成図です。 システム構成-電話 オペレータはブラウザからWebサービスである Amazon Connect にログインして待機します。 Amazon Connect が着信を受け取り、実際にオペレータに繋がるまでに、いくつかの工程を通ります。 コールフロー / キュー / ルーティングプロファイル、この3点については、Amazon Connect を使う上で重要な項目のため少し詳しく説明します。 コールフロー コールフローは、「着信した電話をどのように処理するか」を制御する重要な場所です。 電話がかかってきた時は、最初にコールフローに入ります。 コールフロー設定画面 上図のように、GUIを用いて設定することになっていて、シーケンスフローを組み立てる感覚で設定できます。 紹介しているのは、通話内容の録音設定を行い、後続のキューに転送するだけの単純な構成ですが、以下のように複雑な制御も設定可能です。 営業時間内であるかチェックして、時間外なら音声ガイダンスを流して終了する Lambda と連携し、任意の処理を走らせる 発信者に番号プッシュの自動案内を行い、入力値によって応答するオペレータを変える 他にも、一般的なコールセンターで出来ることは一通り揃っていて、それらはすべてコールフローで設定することになります。 キュー 「着信した電話をため込む場所」です。 コールフローの終了後は基本的にキューに入り、オペレータと繋がるのを待つ状態になります。 待ちになった電話はFIFOのアルゴリズムに則り、順次オペレーターに繋がります。 ルーティングプロファイル 「キューとオペレータの橋渡し役」です。 キューに入った電話は、ルーティングプロファイルによって、電話可能のオペレータと結び付けられます。 ルーティングプロファイルとオペレータが「多対1」の関係であるのに対し、ルーティングプロファイルとキューは「多対多」の関係になっている、という点は混乱しやすいので注意が必要です。 一般的には、部署毎にルーティングプロファイルを作り、1対1の関係でキューを作るのが基本かと思いますので、気にならないかもしれません。 AWS の他サービスとの連携 Amazon Connect が取り巻く周辺のAWS事情です。 今後も更に連携できるものが増えていきそうです。 システム構成-AWS 自社システムとの連携について ここからは Amazon Connect と自社システムとの連携方法についてです。 連携の方法は大きく分けて2つの方法が考えられます。 コールフローの中で Lambda を使って連携 オペレータが電話を受けたタイミングで連携 ここでは、弊社で行った Lambda を使った連携を紹介します。 弊社が作成したのは、「Lambda を使って会員の情報を取得し、着信と同時にオペレータのブラウザに表示する」というものです。 簡単に処理フローを図示したものです。 Lambda連携-フロー図 これを実現するには、Lambda の他に amazon-connect-streams という JavaScript のライブラリも必要になります。 順を追って説明していきます。 Lambda 編 Lambda との連携はコールフローから行います。 まずはコールフローの中で Lambda と連携するための設定が必要です。 実際にコールフローを設定した内容です。 Lambda連携-コールフロー設定 Amazon Connect と Lambda を繋げる 「AWS Lambda 関数を呼び出す」の設定項目には、呼び出したい Lambda のARNを設定します。 Lambda連携-関数を呼び出す Lambda の返却値を Amazon Connect で設定する 「問い合わせ属性の設定」の設定項目には、Lambda 関数が返却した値を設定します。 この工程を行わないと、電話と共にデータが伝搬されません。 Lambda連携-問い合わせ属性の設定 Lambda の処理 こちらは Lambda に記述する実際のソースコードになります。 python を用いています。 辞書型で渡される event 引数に発信者電話番号が入っています。 注意点としては、返却値のvalueは必ず文字列/数値/真偽値のいずれかである辞書型にする必要があります。 def lambda_handler (event, context): logger.info(event) end_point = event[ "Details" ][ "ContactData" ][ "CustomerEndpoint" ] # ここで自社システムと連携して任意の処理を行う return { "response" : json.dumps(end_point) } 引数 event 対するログ出力内容はこのようになっています。 { 'Details' : { 'ContactData' : { 'Attributes' : {} , 'Channel' : 'VOICE' , 'ContactId' : 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' , 'CustomerEndpoint' : { 'Address' : '+81xxxxxxxxxx' , 'Type' : 'TELEPHONE_NUMBER' } , 'InitialContactId' : 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' , 'InitiationMethod' : 'INBOUND' , 'InstanceARN' : 'arn:aws:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' , 'MediaStreams' : { 'Customer' : { 'Audio' : None } } , 'PreviousContactId' : 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' , 'Queue' : { 'ARN' : 'arn:aws:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' , 'Name' : 'test-queue' , 'OutboundCallerId' : { 'Address' : '+81xxxxxxxxxx' , 'Type' : 'TELEPHONE_NUMBER' } } , 'SystemEndpoint' : { 'Address' : '+81xxxxxxxxxx' , 'Type' : 'TELEPHONE_NUMBER' } } , 'Parameters' : {} } , 'Name' : 'ContactFlowEvent' } amazon-connect-streams編 Lambda で連携したデータを電話を受けた時に表示するために必要になります。 amazon-connect-streams とは Amazon Connect のオペレータ画面で使用する JavaScript ライブラリです。 実際に電話を受ける、かける、などの操作ができます。簡単に言うと電話機です。 これを使うことで、電話機を自作のWebサイトで利用することができます。 github.com 実際に amazon-connect-streams を使うには、AWS でのIPホワイトリストの設定と、Node.jsのビルドが必要です。 導入方法については、ここでは割愛しますので、上記ドキュメントをご覧ください。 CCP (Contact Control Panel)の表示 CCP とは、amazon-connect-streams の中で使われる機能の一部で、電話機のUIを指します。 以下のソースコードが示すように表示したい場所にdivタグを用意し、DOMを渡すことでその場所にiframeが展開されます。 let containerDiv = document .getElementById( "containerDiv" ); connect.core.initCCP(containerDiv, { ccpUrl: "https://[インスタンスエイリアス].awsapps.com" // 実際の管理サイトのURL } ); 表示されるCCPです。 CCP 着信時のデータ受け取り // initCCPの後 connect.contact((contact) => { contact.onConnecting((conectingContact) => { let attributes = conectingContact.getAttributes(); console.log(attributes); } ); } ); 出力した結果は以下となり、lambda で連携した内容と一致していることが確認できます。 amazon connect streams データ取得 (おまけ) Amazon Connect Service API編 Amazon Connect では、Web API が用意されていて、データ取得/操作が行えます。 これらを利用すれば、オペレータに表示する画面から直接 Amazon Connect の機能を使えます。 docs.aws.amazon.com API を利用するには、IAM のポリシー設定も必要になります。 キューに存在する待ち呼 *2 の数を取得 例として、キューに存在する待ち呼数の取得を紹介します。 当該情報を取得するには、 get_current_metric_data というAPIを利用します。 python を用いて説明します。 def getCurrentQueue (): import boto3 # API クライアントを生成します。(IAMのポリシー情報を渡します。) client = boto3.client( "connect" , region_name= "ap-southeast-2" , aws_access_key_id= "xxxxxxxxxxxxxxxxxx" , aws_secret_access_key= "xxxxxxxxxxxxxxxxxx" ) # キューの現在の情報を取得します。 current_metoric = client.get_current_metric_data( InstanceId= "[Amazon Connect インスタンス ID]" , Filters={ 'Queues' : [ "[キューID]" ] }, CurrentMetrics=[ { 'Name' : 'CONTACTS_IN_QUEUE' , 'Unit' : 'COUNT' } ] ) logger.info(current_metoric[ "MetricResults" ]) get_current_metric_data APIには、引数の CurrentMetrics 配列に別の情報を追加することで様々なデータが取得できます。 詳しくは、API ドキュメントをご覧ください。 docs.aws.amazon.com 以下は出力内容になります。 [ { 'Collections' : [ { 'Metric' : { 'Name' : 'CONTACTS_IN_QUEUE' , 'Unit' : 'COUNT' } , 'Value' : 0 } ] } ] Amazon Connect を導入してみて Amazon Connect の導入を行い、技術面、運用面で思うところをあげました。 良かったこと 構築がとても楽 とにかく電話が出来るまでが早くて驚きました。 電話線工事などにかかる時間がなくなり、代わりに自社システム連携などに時間を割くことが出来ました。 拡張性が優秀 AWS のサービスであるため、Lambda など他サービスの連携が楽という利点があります。 また、amazon-connect-streams では電話機の機能だけを切り出してくれているため、それ以外すべてを使いやすくカスタマイズ出来ます。 もはや自社システムに 電話サービスを追加する感覚とも言えるでしょう。 コールフローの設定で柔軟に運用が可能 自社システムの連携を随時アップデートしていく環境では、どうしても本番環境で動作確認したい場合があります。 そんな時もコールフローで特定の電話番号だけ別の処理をさせることも出来るので、動作確認が容易になります。 メンテナンスによる急な営業時間外の設定など、緊急性のある運用もカバーできるところも良い点だと思います。 オペレーター事情に左右されにくい Amazon Connect ではユーザに対する権限の付与や、人員の増減、権限の設定変更、などが簡単に出来ます。 ベンダーによっては、権限の変更に依頼が必要だったり、料金変更があることもあります。 人員変更が滅多にない組織なら気にならないかもしれませんが、スタートアップや柔軟に変更を行う組織の場合はありがたいでしょう。 課題に感じること 学習コストが高い(技術) 東京リージョンで提供されることが発表されているとは言え、 まだ日本で導入している企業は少ないため、ドキュメントは少ないです。 弊社もトライ・アンド・エラーを繰り返して習得しました。 月の無料利用額があるため、まずはその額内でいろいろ試して習得するのが良いかと思います。 (18/12/11 追記) 「Amazon Connect とは」で追記した通り、東京リージョンにてサービス開始されましたので、削除させていただきました。 学習コストが高い(運用) 技術視点だけではなく、運用面でも学習コストが高いと感じました。 運用に入ると、コールセンターの管理者に Amazon Connect の管理を任せたいものです。 コールフロー、エージェント管理、メトリクスの作成など、習得しなければ使いこなせないものが多い印象です。 技術視点と同様に、ドキュメントが少ないため、こちらもトライ・アンド・エラーで習得しなければならないのが現状です。 一部メトリクス情報に難あり 一部の内容で満足いかない箇所がありました。 例えば、弊社で感じている箇所として以下があります。 リアルタイムのキュー内容表示において、自動更新が約15秒間隔(待ち呼に気付くのが遅れる) 問い合わせ検索(発着信の内容を調べられる)では、通話に関するタイムスタンプに秒単位の表示が無い(APIでは取得しているのであくまで表示だけのようです。) 細かい話と捉えることも出来ますが、組織が定めるコールセンター業務のKPIに影響することも有りえます。 特に大規模な組織では気になるところではないでしょうか。 オペレータのステータス管理に不安あり Amazon Connect ではオペレータに「Available」「offline」など、状態を管理出来ます。 「Available」という状態が電話を受け付けている状態を表しますが、この状態は明示的に変更しない限り、そのまま維持されます。 変更を怠ると、物理的に電話が取れない状況であろうと、ルーティングプロファイルによって電話が割り当てられてしまう危険があります。 これを運用以外の方法で防ぐには、Web APIを利用して自動的に状態を変更する、コールフローとシフトを連動させる、など一工夫必要になります。 導入するにあたり、方針は決めておく必要がありそうです。 また、ACW *3 の状態に手動で変更出来ない、ということもあります。 Amazon Connect では、任意のカスタムステータスの作成が可能であり、似たステータスを作ることは可能です。 しかしメトリクスに影響するなど不安は残ります。 オペレータへ繋がる仕組みが今ひとつ Amazon Connect では、ルーティングプロファイル内のオペレータに優先度設定や、特定のオペレータへ繋ぐ機能はありません。 組織のやり方によりますが、他のオペレータが全て電話で埋まった時に管理者も電話を受ける、という方針があった場合、実現する方法は難しいです。 埋まった時点で状態を変更する運用回避や、ルーティングプロファイルや Lambda を駆使する方法もありますが、難易度は高い印象です。 (18/12/08 追記) 上記取消線の内容は、管理者用のルーティングプロファイルを別途作成し、キューの優先度及び遅延時間を設定することで実現できました。 まとめ AWS の1サービスとして提供される Amazon Connect ですが、まだ日本のコールセンター業務に適応するには、運用面で課題が残りそうな印象があります。 ここは、日本企業の導入が進むのと同時に、Amazon Connect も進化し、適応していくことを期待しています。 一方、技術面の視点から見ると、AWS との連携やカスタマイズの容易さなど、エンジニアがとっかかりやすい環境が揃っていると感じました。 今回弊社が行った自社システムの連携もほんの一部に過ぎず、コールセンターにおける業務効率化の可能性はとても大きいと思います。 以上、Amazon Connect の導入を検討している会社の参考になれば幸いです。 muraishis *1 : Computer Telephony Integration の略。電話とシステムの連携を行う技術のこと。 *2 : オペレータに繋がるのを待っている着信のこと *3 : After Call Workの略。電話を切った後、対応内容の記録など、オペレータが作業をする時間のことをいう。
アバター
本記事は、一休.com Advent Calendar 2018の4日目の記事です。 qiita.com 宇都宮です。宿泊事業本部でWebフロントエンドの開発をしています。 今日は、パフォーマンス改善に取り組むフロントエンドエンジニアの多くが頭を悩ませているであろう、サードパーティスクリプト(3rd Party JavaScript)について書きます。 サードパーティスクリプトとは サードパーティスクリプトとは、外部のドメインから読み込むJavaScriptのことです。典型的には、Google Analytics等のスクリプトが、サードパーティスクリプトに該当します。 一休.comでは、サードパーティスクリプトを、アクセス解析・広告のリターゲティング・A/Bテスト等、様々な用途に使用しています。これらのスクリプトは、一つ一つは小さなものであるため、画面表示のスピードに対するインパクトは意識しづらいものです。しかし、細かいコストが積み重なることで、サイトの読み込み速度に大きな悪影響を与えることもあります。 一休.comにおけるサードパーティスクリプト 一休.comでも、様々なサードパーティスクリプトを利用しています。たとえば、スマホのホテルページ( https://www.ikyu.com/sd/00001290/ )では、以下の外部ドメインからスクリプトを読み込んでいました。 bam.nr-data.net js-agent.newrelic.com b92.yahoo.co.jp www.google-analytics.com www.googleadservices.com s.btstatic.com yjtag.yahoo.co.jp s.yjtag.jp www.googletagmanager.com googleads.g.doubleclick.net static.criteo.net s.yimg.jp これらを合計した転送量は、ページ内で読み込んでいるJSの3分の1を占めます(300KB中、100KB)。 サードパーティスクリプトがページ読み込み速度に与える影響 ↓は、スマホホテルページのパフォーマンス計測結果です。 https://www.webpagetest.org/result/181017_SD_14a6718d17894b5d25bea6ff2c01713d/ サードパーティスクリプトあり 画面の描画完了まで(Speed Index)が3.142s、ユーザが操作可能になるまで(First Interactive)には8.3秒かかっています。 次に、WebpagetestのBlock機能を利用して、サードパーティスクリプトの読み込みを行わない状態で計測してみます。 https://www.webpagetest.org/result/181017_9Z_9051ce3990f33aadaae363806147fada/ サードパーティスクリプトなし 画面の描画完了まで(Speed Index)が3.142s => 2.787s、ユーザが操作可能になるまで(First Interactive)は8.3秒 => 5.6秒に改善しました。 サードパーティスクリプトの多くはasyncで読み込むため、レンダリングに与える影響は軽微です。しかし、あるjsが別のjsを連れてくる、という構成になっていることが多く、jsファイルの読み込みがダラダラと続きがちです。このことは計測結果のWaterfallを見るとわかりやすいと思います。これによって、画面の表示が終わってもブラウザはまだローディング中で、画面を操作するともたつく、という状況が生まれます。 このように、サードパーティスクリプトを使用する際は、それがユーザ体験に与える悪影響も考慮する必要があります。 他のサイトは? 一休.comでは、サードパーティスクリプトによって一定のパフォーマンス劣化が見られることがわかりました。 では、他のサイトではどうでしょうか? 一例として、 A/B Testing, Ads and Other Third Party Tags というトークでは、あるWebサイトから1つスクリプトを取り除くことで、ページのロード時間が4秒改善し、サイトの売り上げが26%改善したことが紹介されています(このトークは、他にも面白い事例が満載なので、サードパーティスクリプトの改善に興味のある方は視聴をおすすめします)。 日本の事例としては、日本経済新聞社の宍戸さんが公開されている、『PWA導入の成果と課題』というスライドが、身につまされる内容で参考になります。 PWA導入の成果と課題 / nikkei-pwa-html5conf2018 - Speaker Deck このスライドでは、インターネット全体で、サードパーティスクリプトのサイズが自前のスクリプトのサイズよりも多くなってきていること(1st party 100KBに対して3rd party 250KB)、日経新聞でも、サイズは明言されていないものの、jsリクエストの半分がサードパーティスクリプトであることなどが紹介されています。 最適化されたサイトの事例 次に、サードパーティスクリプトの配信が最適化されたサイトの事例も紹介しておきます。 宿泊予約サイトのExpediaは、一休.comと同様、様々なサードパーティスクリプトを使用しています。Expediaでは、パフォーマンスを最適化したSPA版を開発しており、スマートフォン向けホテル検索ページはSPAになっています。 Expediaのスマートフォン向けホテル検索ページ (※PCから閲覧する場合はUAをスマホのものにしてください)を開いて、Chrome DevToolsのNetworkタブ等で読み込んでいるjsのhostを確認すると、全て同一ドメイン( https://c.travel-assets.com おそらくExpediaのCDN)から取得していることに気づきます。 推測ですが、サードパーティスクリプトをセルフホストするか、トラッキングやA/Bテストのためのスクリプトを内製しているものと思われます。外部からは一切読み込まないという決断ができるのがすごいところ。 サードパーティスクリプトのパフォーマンス改善 サードパーティスクリプトの最適化について書かれた包括的なドキュメントとしては、Google Web Fundamentalsの Loading Third-Party JavaScript がおすすめです。 抜粋して紹介すると、 サードパーティスクリプトはasyncまたはdeferで読み込むこと サードパーティスクリプトの配信サーバが遅い場合、セルフホストを検討すること サイトに明確な価値をもたらさないスクリプトは削除すること preconnect/dns-prefetch等のresource hintsを活用すること などです。 一休.comでも、サードパーティスクリプトは基本asyncで読み込み、さらに、サードパーティスクリプトのドメインにはpreconnectを行っています。 一番強力な方法は「削除」なのですが、サードパーティスクリプトは多くがビジネスサイド主導で導入されており、開発側の判断だけでは削除まで踏み切れません(宍戸さんのスライドでも、 技術の問題だけではない と言及があります)。 サードパーティスクリプトの最適化には、エンジニアリングチームだけでなく、サービスを運営する組織全体が、UXに高い意識をもつ必要があります。 "User First and Challenge" を社是とする一休としては、積極的に取り組んでいきたいところです。 今後の方針 一休.comで、今後行えそうなサードパーティ最適化の施策としては、以下を検討しています。 サードパーティスクリプトのアセットバジェットの導入 読み込んでよいJSはトータルでXXKB以内、等 Google Tag Manager(gtm.js)の削除 他のスクリプトを連れてくるだけのJSがVue.js 2個分(44KB)は大きすぎ 簡単にスクリプトを追加できる = 簡単にサイトを遅くできる まとめ サードパーティスクリプトには、それぞれに独自のメリットがあります。たとえば、Google Analytics等のアクセス解析は、サイトを運営するうえで欠かせない情報を提供してくれます。 一方、サードパーティスクリプトにはコストがかかります。サードパーティスクリプトを読み込むと、サイトは確実に遅くなります。 用法・容量を守って、価値あるWebサービスを提供していきたいですね 💪 参考文献 vimeo.com speakerdeck.com developers.google.com
アバター
この記事は一休.comアドベントカレンダー2018の3日目の記事です。 qiita.com 宇都宮です。宿泊事業本部でWebフロントエンドの開発をしています。 一休.comにVue.jsを導入して、約1年が経ちました。 スマートフォン版の予約入力画面 から始まり、PCとスマートフォン版のホテルページほか、さまざまなUIコンポーネントがVue.jsで実装されるようになってきています。 また、予約入力画面のような複雑な状態管理を伴う画面の実装のため、Vuexを導入しています。 ここ1年ほどVue.js + Vuexというスタックで開発を行ってきて、アプリケーションの設計について色々と思うところがあったので、今回は現状でどういう構成が最適と考えているか、紹介します。 Vue.jsアプリケーションのState Vueコンポーネントには、親から受け取るpropsと、自分自身で保持するstate(data)という2つの概念があります。また、子コンポーネントから親コンポーネントに何かしらの値を戻したい場合、eventの仕組みを利用することができます。 propsとdataについては説明を省きますが、eventについてはサンプルコードを載せておきます。 KeywordSearch.vueというコンポーネントがあって、このコンポーネントは検索キーワードの入力とバリデーションを受け持つとします。一方、検索処理の実行はAPI経由なのか画面遷移なのかが画面によって変わるため、親コンポーネントに委譲します。 このような場合、以下のようにeventの仕組みを利用することで、子から親へ値を受け渡すことができます。 // KeywordSearch.vue < template > < div > < label > 検索キーワード < input type = "text" name = "query" v-model= "keyword" /> </ label > < button type = "button" @click= "search" > 検索 </ button > </ div > </ template > < script > export default { name: "keyword-search" , data () { return { keyword: "" , } ; } , methods: { search () { // search イベントを発火し、keywordを引数として渡す this .$emit ( "search" , this .keyword ) ; } } } ; </ script > // KeywordSearch.vueの親コンポーネント < template > < div >< keyword-search @search= "handleSearch" /></ div > </ template > < script > import KeywordSearch from "./KeywordSearch.vue" ; export default { name: "App" , components: { KeywordSearch } , methods: { // KeywordSearchのsearchイベントのハンドラ handleSearch ( keyword ) { // 検索APIに問い合わせたり、画面遷移したり } } } ; </ script > まとめると、Vueコンポーネントにおいて、データの流れは以下のようになっています。 自分自身で保持: data 親 => 子: props 子 => 親: event このように、コンポーネント間のデータの流れがpropsとeventによって行われることをとらえて、「Props Down, Event Up」と呼ぶことがあります(もともとはReact.jsのコミュニティ発祥のフレーズだと思います)。 この辺の話はVue.jsのドキュメントでも詳しく説明されています。 jp.vuejs.org グローバルな状態管理の必要性 ここまで説明してきたのは親子関係のあるコンポーネントの話です。しかし、実際のアプリケーションでは、親子関係のないコンポーネント間でデータを共有したい場合があります。これについても、Vue.jsおよびVuexのドキュメントに、Vuexを使うべき場面についての解説が載っています。 jp.vuejs.org vuex.vuejs.org 一休.comの画面の例としては、「今すぐポイント割引後料金を表示」の切り替えボタンは、直接の親子関係のないコンポーネントがオン・オフの状態を共有するため、Vuexを使っています。 「今すぐポイント割引後料金を表示」の切り替えボタン Vuexのバッドプラクティス Vuexは便利なのですが、何でもかんでもVuexを使うのは良いやり方とはいえません。Vuexを使っていると、以下のようなコンポーネントを書いてしまうことがあります。 < template > < div > {{ someState }} </ div > </ template > < script > import { mapState } from "vuex" ; export default { computed: { ...mapState ( [ "someState" ] ) } } ; </ script > このコンポーネントの作りがイマイチなのは、単に値を受け取って表示するだけのコンポーネントなのに、propsではなくStoreに依存している点です。この設計が良くないことを説明するために、モジュール結合度という概念を紹介します。 モジュール結合度 モジュール結合度とは、あるモジュールと別のモジュールの結合度を表す度合いです。低いほど疎結合、高いほど密結合となります。見た目を制御するコンポーネントの場合、様々な場所での使用が想定されるため、結合度は低い方が望ましいです。 モジュール結合度 説明 1 データ結合 引数で単純なデータを渡すパターン 2 スタンプ結合 引数で構造体などのオブジェクトを渡すパターン 3 制御結合 引数の種類によって、メソッドの内の処理が変わるパターン 4 外部結合 単一のグローバルデータを参照しているパターン 5 共通結合 複数のグローバルデータを参照しているパターン 6 内容結合 他のオブジェクトの内部を参照しているパターン qiita.com Vueコンポーネントでいうと、propsは「データ結合」ないし「スタンプ結合」なので、結合度は1〜2です。これに対して、storeは概念としてはグローバルデータの参照に当たり、複数のデータを参照することが多いため、「共通結合」の5、gettersやactionsに依存している場合は「内容結合」の6になります。 つまり、storeを参照するコンポーネントはstoreと密結合し、storeなしでは利用できなくなってしまうのです。 このような密結合が常に問題になるわけではありませんが、コンポーネントツリーの末端に近いようなコンポーネントがstoreを参照しているのは、コンポーネントの設計が歪になっていることのシグナルかもしれません。 Vue + Vuexアプリケーションの状態管理の指針 Vuexがその設計の参考としているFluxアーキテクチャでは、状態が一カ所に集約されていて、Single Source of Truthになると安心できる、という考え方があります。この考え方を適用すると、データは基本的にVuexのstoreから引いてくるのが正しいように思えます。 こうしたSingle Source of Truthの考え方と、疎結合なコンポーネント設計は、対立する概念ではありません。ここで参考になるのは、Presentational ComponentとContainer Componentという考え方です。 medium.com ざっくりいうと、Presentational Componentは、propsを受け取って、表示をするだけのコンポーネント、Container ComponentはPresentational Componentを管理し、値を受け渡すコンポーネントです。 Container Componentはstoreを参照し、必要に応じてPresentational Componentにデータを渡す Presentational Componentはstoreは参照しない このようにコンポーネントを分類することで、storeと密結合する部分をContainer Componentに限定し、Presentational Componentは疎結合に保つことができます。 また、もう1つの設計指針として、「Vuexはグローバルな状態のストアである」という点を強調しておきたいと思います。modulesに区切ることで細かいstoreを定義することは可能ですが、実装上どこからでも参照できるVuex storeは、概念的にはグローバル変数と同じです。コンポーネントローカルな状態(要素の表示・非表示等)は、Vuex Storeで管理する状態ではありません。一方、ローディングの表示等、画面全体に影響するような状態はVuexで管理すると便利です。 まとめ 以下のような指針で設計を行うことで、Vueコンポーネント設計の基本を守りつつ、Vuexを適材適所で活用できます。 親子関係のあるコンポーネント間のデータの受け渡しは「Event Up, Props down」で 親子関係のないコンポーネント間のデータの共有はVuexで コンポーネントの固有のデータはコンポーネントのローカルステート(data)で グローバルなデータはVuexで 参考文献 jp.vuejs.org jp.vuejs.org vuex.vuejs.org qiita.com medium.com
アバター
この記事は一休.com アドベントカレンダー 2018の2日目の記事です。 qiita.com 宇都宮です。宿泊事業本部でフロントエンドの開発を行っています。 今回は、最近一休.comに導入した、SVGスプライトによるアイコンの作り方・使い方について紹介します。 StorybookのSVGスプライトアイコン一覧 アイコンの一般的な使い方 アイコンは、一般的に、以下のような方式で使用されると思います。 ビットマップ画像(gif, png等) アイコンフォント( Font Awosome 等) SVG このうち、ビットマップ画像によるアイコンは拡大・縮小に弱いため、様々な解像度の画面に対応する必要のある現代には不向きです。アイコンフォントはベクター画像なので拡大・縮小に強く、豊富なアイコンがライブラリとして提供されているのが魅力です。SVGもアイコンフォントと同様のベクター画像ですが、フォントにはない柔軟性を備えています。 一休.comは歴史のあるサービスのため、これらのアイコンが混在していますが、最近はSVGアイコンを使うことが多いです。 SVGの柔軟性 SVGは、HTMLに直接埋め込んで使用可能です。そのため、CSSによるスタイリングが可能です。アイコンフォントも色の設定やサイズの調整が可能ですが、パーツ毎に色を塗り分けたりするような柔軟なスタイリングは、SVGでしかできません。また、JavaScriptから操作しやすいという特徴もあります。 一休.comにおけるSVGアイコン使用の問題点 一休.comでのSVGアイコンの使用方法は、いくつかの変遷をたどっています。 はじめはimgタグでsvgファイルを読み込む使い方でしたが、これではSVGの柔軟性で挙げた特徴はほとんど活用できません。 <img src="/path/to/icon.svg" /> インラインSVGにすると、柔軟性は得られますが、記述が煩雑になります。 <svg ..(アイコンにもよるが、200バイトくらい).. /> そこで、Vueコンポーネント化が試みられました。以下のように、1つのSVGアイコンに対して、1つのVueコンポーネントを作る設計です。 < template > < svg .../> </ template > < script > export default { name: 'some-icon' , } ; </ script > これによってSVGのスタイリングの柔軟性は増しましたが、この方式には2つ問題がありました。 1つはパフォーマンスで、アイコン1個あたり1KB(minify+gzip)、アイコン20個で約20KBものJSサイズ増加が発生したことです。本来SVGアイコンは1個200~300byte程度で、gzipするとさらに縮みます。Vueコンポーネント化することで、本来のサイズの10倍ほどに膨らんでしまっています(これはVueコンポーネント設計のまずさに起因しているため、個別にコンポーネントを作るのではなく、汎用的なSVGアイコンコンポーネントを導入するようにしていれば、パフォーマンスへの悪影響は緩和できたと思います)。 もう1つの問題は、VueコンポーネントはVueのコンテキストの中でしか使えないことです。一休.comはフルSPAではないので、サーバサイド(aspx/cshtml)で出力している部分もあります。サーバサイドで出力している部分では、Vueコンポーネントは使えないため、imgタグなどを使う必要があります。 これらの問題を解消し、 SVGの機能をフル活用できる パフォーマンス上のオーバーヘッドが最小限である JSフレームワークに依存せず、どこでも使える という条件を満たすソリューションを検討しました。 SVGスプライトとuse要素 SVGについて調べたところ、 use要素 というものがあることがわかりました。ページ内の別のSVGに定義されている要素を呼び出して使うことができます。 <!-- アイコン定義 --> < svg > < symbol id = "someIcon" ... /> </ svg > <!-- アイコン使用 --> < svg > < use xlink: href = "someIcon" /> < svg > use要素を使う場合、必要なアイコンをSVGスプライトにまとめて、それをインラインSVGとしてHTML内に書き出す必要があります。インラインSVGはネットワークリクエストが発生しないため、パフォーマンス面ではアイコンを個別に読み込むよりも有利です。 この方式を採用すると、 SVGの機能をフル活用できる パフォーマンス上のオーバーヘッドが最小限である JSフレームワークに依存せず、どこでも使える という要件を全て満たせることがわかりました。 残る課題は現行の開発環境への組み込みです。 gulpによるSVGスプライト SVGスプライト化にはgulpを使いました。webpackオンリーの環境ならwebpackでやっても良いと思います。webpackの役割をあまり増やしたくないのと、SVGスプライト関係のサンプルコードはgulpを使っているものが多かったこともあり、gulpを使用しました。 SVGスプライトのためのgulpタスクは以下のようになっています。 const gulp = require( 'gulp' ); const path = require( 'path' ); const svgmin = require( 'gulp-svgmin' ); const svgstore = require( 'gulp-svgstore' ); const cheerio = require( 'gulp-cheerio' ); const commonDir = 'path/to/common' ; gulp.task( 'svg-sprite' ,() => { gulp .src(commonDir + 'icon/*.svg' ) .pipe( svgmin(file => { const prefix = path.basename( file.relative, path.extname(file.relative), ); return { plugins: [ { cleanupIDs: { prefix: prefix + '-' , minify: true , } , } , ] , } ; } ), ) .pipe(svgstore( { inlineSvg: true } )) .pipe( cheerio( { run: function ($) { $( 'svg' ).attr( 'style' , 'display:none' ); } , parserOptions: { xmlMode: true } , } ), ) .pipe(gulp.dest(commonDir + 'icon-dist' )); } ); このタスクは以下の流れで処理を行います。 common/icon 配下にあるsvgファイルを取得 gulp-svgmin を使ってSVGを圧縮 gulp-svgstore を使ってSVGを結合 gulp-cheerio を使って、結合したSVGファイルを非表示に common/icon-dist にファイル(icon.svg)を書き出し このようにして、 https://www.ikyu.com/common/icon-dist/icon.svg (実際にサイトで使用しているSVGスプライト)は作成されています。また、このSVGスプライトは、サーバサイドでHTMLのbodyの開始直後に書き出されています。 利用側では、以下のように、svg要素の中にuse要素を配置し、use要素のxlink:href属性にSVGのid(#ファイル名)を指定することで、アイコンを参照できます。スタイリングはCSSで行います。 < svg class = "l-header-search-icon" >< use xlink: href = "#search" ></ use ></ svg > まとめ 以上、SVGスプライトを使用したアイコンの作り方について紹介しました。 参考文献 GulpでSVGスプライトとアイコン一覧を一発生成 - Bit Journey's Tech Blog
アバター
この記事は 一休.comアドベントカレンダー2018 の1日目です。 こんにちは。レストラン事業本部の西村です。 11月12、13日にサンフランシスコで開催された Chrome Dev Summit 2018 に参加しました。 今年はChromeが10周年ということで、この10年で変わったこと、これからについての話で始まりました。 2日に渡って行われた22のセッションの中で、特に注目した点について深掘りしていきます。 1日目のセッション 1日目は現在提供している技術について、具体的な事例を交えながら紹介されました。 VisBug VisBug は、hoveringとKeyboard shortcutsでブラウザ上でサイトの画像を差し替えたり、一部のコンテンツの内容やスタイルを変更できるChrome extensionです。 ブラウザ上でちょっとしたスタイル修正や画像の入れ替えをしてデザイン決め、といったことが簡単にできます! こちらは Day 1 Keynote の最後に紹介されました。 Performance 例年以上にWebサイトのパフォーマンスに関する話が多く、ケーススタディも豊富でした。 ネイティブアプリがメインだったけれど、Webでも利用できるようにしたらアプリのダウンロード数も上がった!という話がSpotifyの事例で紹介されていました。 こちらは Get Down to Business: Why the Web Matters の中で紹介されています。 改善戦略の立て方 セッションの中で、プロジェクトの進め方に関する重要な教訓がありました。 長期的なビジョンを持つ 短期的な目標を計画する 目標の照準を長期的なビジョンに合わせていく パフォーマンスを改善してもビジネス的なKPIに直結するわけではなく、すぐに成果が出ないことに悩む時もあると思います。 長期的なビジョンを持ちながら短期的な目標を設定し着実にクリアしていく ことを意識した方が達成感も持てると感じました。 Walmartの事例では、長期的な目標としてTime to Interactiveを70%削減、それに対して短期的な目標としてJavaScriptを500KB/CSSを40KB削減と設定しています。 こちらはECの事例を用いて、 Modern Websites for E-commerce in the Real World の中で語られています。 Performance Budgetの設定 Performance Budgetとは、Webサイトのパフォーマンスに影響する要素における数値的な限界値です。 計測ツールを使って様々な視点から分析し、適切なPerformance Budgetを設定することが重要です。 こちらはCarousellが設定しているPerformance Budgetです。 Pinterestの事例では、JavaScriptのバンドルサイズ、そしてPinner Wait Time(PWT)という独自の指標を設定していました。 PWTはTime to Interactive + Image Loadingです。サービスにとって重要な画像を意識した指標ですね。 このように、 それぞれの事業に沿ったPerformance Budgetを設定する ことはパフォーマンス改善の戦略を立てる上でとても重要です。 継続的な測定、改善 You can't improve what you don't measure. - Peter Drucker 計測しないものは改善できない、ということですね。セッションの中でも計測ツールが複数紹介されました。 Lighthouse Lighthouse とは、Webサイトの品質向上に役立つ、オープンソースの自動化されたツールです。 Chrome Dev ToolsのAuditsタブに組み込まれているので、すでに利用している方も多いと思います。 今回は最新の変更点を紹介していました。 PWAのチェック項目が追加 Scoreの判定が厳しくなった 回線の呼称を変更(Fast 3G => Slow 4G) Scoreの判定が厳しくなった結果、緑と黄の比率が大きく変わっています。 Lighthouseは lighthouse-ci のように、CIにも活用しやすくなってきています。 また、他のツールとの統合も積極的に進めていて、 Calibre や treo 、 SpeedCurve や SquareSpace といったサードパーティ製のツールが紹介されました。 Chrome User Experience Report Chrome User Experience Report(CrUX) は、ChromeユーザーによるWeb体験の実データを集計したレポートです。 BigQueryやPageSpeed Insightsで利用することができます。 最近の更新では、国ごとにデータセットが用意され地域別の分析ができるそうです。また、CrUX Dashboardではレポートをカスタマイズし、共有できます。 PageSpeed Insights PageSpeed Insights とは、Webサイトのパフォーマンスを数値化して具体的な改善点を提案してくれる、統一されたツールです。 今回は、新しくリリースしたv5での変更点が紹介されていました。 分析にLighthouseを使用 Lighthouseのパフォーマンスカテゴリのスコアを提供 フィールドデータにChrome User Experience Report(CrUX)を活用 これまでPageSpeed Insightsは独自の仕組みで測定をしていましたが、Lighthouseを利用することで以前とは違う検証結果になっているようです。 web.dev web.dev は、Lighthouseを使用した統合ツールの1つです。計測ツールとドキュメントが一体化されているため、測定後の次のステップが分かりやすくなっています。 現在beta版として提供されているので、少し紹介しようと思います。 Lighthouseを使って結果をレポートしてくれます。その下にTodoがあり、優先度も表示されています。 ドキュメントがリンクされているのも特徴的です。 いくつか計測ツールを紹介しましたが、Googleは これまでバラバラだった計測ツールをLighthouseに一本化していく ことを進めていくようです。 Google製のツールだけでなく、サードパーティ製のツールも奨励していました。 計測するツールに関する基本的な部分は、 State of the Union for Speed Tooling で語られています。 画像やフォント、JavaScriptに関するパフォーマンス改善の具体的なTipsについては、 Speed Essentials: Key Techniques for Fast Websites を見ると参考になります。 2日目のセッション 2日目は現在開発している新しい技術について、これからの展望が語られました。 すでにトライアルできるものから開発初期段階のものまで様々です。 virtual-scroller virtual-scroller は、バーチャルスクロール(画面内の必要なコンテンツのみレンダリングし、ユーザーのスクロールに応じて更新していく)を実現します。 Layered API プロジェクトの1つとして進められていて、開発初期段階です。 Virtual Scrollとは 大量のコンテンツを表示するケースではレンダリングに時間がかかってしまいます。 例としてAddress BookやSNSのフィードなどが挙げられていました。 Virtual Scrollでは、画面外の不要なDOMを削除し必要な部分のみレンダリングするため、このようなケースのパフォーマンス改善に有効です。 Virtual Scrollの技術自体は以前から存在し、特にネイティブアプリではお馴染みの技術です。 しかしWebに関してはまだ最善とは言えず、解決すべき問題が存在します。例えば、 同じコンテンツ内でのリンクが動かない(全てのDOMが揃っていないため) ページ内検索できない 検索にインデックスさせられない SEO観点での問題が大きいですね。画面外のDOMが存在しないため、実用面においてある程度犠牲を払わなければならないのが現実です。 どのように解決するのか virtual-scrollerでは、リンクやページ内検索などの問題を Invisible DOM で解決しようとしています。 Invisible DOMとは、見えないけれども検索ができるDOMです。 検索などはできますが、レイアウトやスタイリング等のコストが不要なためパフォーマンスが上がります。 通常のVirtual Scrollでは、画面外の不要な部分はDOMが存在しませんでした。Invisible DOMを利用したvirtual-scrollerでは、画面外の部分は見えませんがDOM自体はすべて存在します。 virtual-scollerではInvisible DOMをサポートする他にも、新たな問題を探しその解決にも取り組むそうです。 virtual-scroller: Let there be less (DOM) で詳細が語られています。 バーチャルレンダリングでは主にSEO観点での懸念があり、メリット/デメリットを理解した上で使用する必要があるというのが現状です。 今後Invisible DOMをサポートできるようになればより実用的になりそうです。 Web Packaging Web Packaging は、端的に言うとWebサイトをパッケージ化する技術のことで、今回は Signed Exchanges と Bundled Exchanges が紹介されました。 1つ目の Signed Exchanges は、Exchange(HTTPリクエスト/レスポンスのペア)に署名して、任意のキャッシュサーバーから配信できるようにする仕組みです。 活用例として、AMPページのURLの最適化が紹介されていました。 Signed Exchangesにより署名されたAMPページを公開することで、Google検索は、AMPのキャッシュから配信されるAMPページではなく、Googleのキャッシュから配信される署名されたAMPページにリンクするようになります。 これがユーザーにとって何が嬉しいのか、もう少し具体的な話をしましょう。 こちらは一休レストランのAMPページです。Googleの管理するAMPのキャッシュサーバーから配信されているので、URLを見るとドメインはwww.google.co.jpとなっています。 また、AMPページ用のヘッダーがアドレスバーの下に表示されています(上図赤枠)。 Signed Exchangesを利用することで、 AMPページを通常のページと同様のドメインで、AMP用のヘッダーの表示なしで提供 することができます。 2つ目の Bundled Exchanges は、複数のExchange(HTTPリクエスト/レスポンスのペア)を1つにまとめる仕組みです。 現在プロトタイプの段階だそうですが、オフラインでのPWAのインストールなどに活用できるとのことでした。 Portals Portals は、 SPAのようにサイト・ページ間のスムーズな遷移をブラウザで実現してくれる 、現在開発中の技術です。 セッションでは NavigationからTransitionへ と説明されていましたが、異なるドメインであっても遷移間の摩擦を感じないページの移動が可能になります。 メリットとしては、 サイトを再構築することなく、 異なるドメインであっても、 seamlessなページ遷移を可能にするという点です。 <portal> タグを埋め込むことでiframeのように利用することができます。iframeとの具体的な違いは以下の通りです。 Portalが作動すると、documentがportalに置き換えられるため、常にトップレベルのブラウジングコンテキストとして作られます。 iframeは入れ子構造になるため、その点でも違いがあります。 まだ仕様策定の初期段階ではありますが、実用化が楽しみです。 デモでは「となりのヤングジャンプ」の事例が紹介されていましたが、ページ数の多い電子書籍にはかなり活用できそうですね。 Web PackagingとPortalsの話は、 From Low Friction to Zero Friction with Web Packaging and Portals で語られました。 フォーラム 朝から夕方まで休憩を挟みながらセッションが続きますが、その間隣の会場ではフォーラムが開催されています。 ここでは各ブースでデモを見たり、Googleのエンジニアと直接交流することができます。 セッションで語られたweb.dev、Lighthouse、VisBugなどのデモはもちろん、WebのパフォーマンスやUI/UXなど何でも相談できるReview Clinicというブースもあります。 今回はReview Clinicで 一休.com レストラン のスマホサイトのUI/UXについて聞いてみました。 最近のUI変更について聞いてみました! 10月にリリースした、プラン一覧のフィルターのUIについても聞いてみました。 プランを平日限定、飲み放題などの属性や時間帯で絞ったり、並べ替えを切り替えたりする機能です。 上からこだわりフィルター(横スクロール)、時間帯タブ、並べ替えトグルの順番で並んでいます。 便利ですが多機能なため、どのように分かりやすく見せるかが難しい点です。 全部違うデザインなので少し分かりにくい。揃えたほうが良いと思う。 例えばフィルターの部分に関しては、スクロールできることが明らかに分かる方が良い。 スクロールバーを常に表示するとか、文字が分かりやすく途中で切れるようにするなど、何がベストか探してみてほしい。 やはり懸念していたように、複雑に見えたようです。 明らかなデザインをどのように実現していくかは、実際のユーザーの行動を数値的に見て判断する必要があります。 A/BテストをしたりしてどのようなUIが好まれるのか深掘りしてみると良いかもしれません。 学んだこと Be Obvious: 明らかなデザインを意識すべき 会話の中で何度も " Be Obvious " と語られていました。 Googleでも様々なUIをユーザーの行動の結果を見て比較しているけれど、「 明らかなデザイン 」が良いという結果が出ているそうです。 機能としてはまず使ってもらえないと意味がなく、 使ってもらうためには明らかに分かりやすいデザインでなければならない と感じました。 具体的に言うと以下のようなアプローチがあります。 カルーセルは画像がスクロールする方向に必ず半分くらいはみ出るようにして明らかに続きがあるように見せる クリックできるボタンのスタイルを揃えてクリックできないものと区別する アイコンにテキストで機能的な説明をつける(例: トレンドのアイコンに「トレンド」と説明をつける、など) アクセシビリティを意識すべき アクセシビリティとは、簡単に言うと、いかなるユーザーが、いかなるデバイスを使い、いかなる環境の下であっても利用できるようにする、ということです。 Webの制作者としては、具体的に以下の3つを意識すると良い、と話されていました。 タップターゲット alt属性 色のコントラスト比 ちなみにアクセシビリティはLighthouseで測定できます。 1日目のセッションで紹介したVisBugでも簡単にチェックすることができるので、そちらを利用してもいいかもしれません。 定量/定性的なユーザーフィードバックを元に意思決定すべき 1つ目の明らかなデザインの中でも触れていますが、UIに関する意思決定は 定量/定性的なユーザーフィードバックを元に判断する べき、という話もされていました。 当然のことですが、一概に「このデザインが良い・悪い」ということはなく、 ユーザーの動向を見ながら改善戦略を考える ことの大切さを改めて感じました。 まとめ 初のChrome Dev Summit参加でしたが、総じて興味深い内容が多かったです。 特にセッション内でもケーススタディが多く、PWAの導入事例が国内外問わず増えたと感じました。 ケーススタディでは、実際に各社が設定したPerformance Budgetの話もされていて参考になりました。 技術的な観点で言うと、最も興味深かったのはやはり Web Packaging と Portals です。 アプリケーションを再構築することなく利用できる、というのが嬉しいですね。 また、フォーラムやアフターパーティーのように交流できる場もあり、Googleのエンジニアやデザイナーとフランクに話ができるのも魅力でした。 おまけ:Women & Allies Receptionに参加しました! 実はChrome Dev Summit 2018の前日の夜に Women & Allies Reception というイベントにも参加しました。 Women TechMakers が主催していて、Chrome Dev Summit 2018参加者の女性全員が招待されていたようです。 実際参加していたのは50名くらいだったと覚えています。 イベント名の通り女性同盟なので参加者同士の交流がメインで、Googleの女性エンジニア、プロダクトマネジャー数人によるKeynote sessionもありました。 また、Googleからは、男性も含めChrome Dev Summitでセッションを担当している方など他に数名参加されていました。 Keynote sessionでは、ハラスメント問題や女性スピーカーが少ない問題などが話されていました。 つい最近起こったGoogle社でのストライキも、ハラスメント問題が関係していましたね。 女性スピーカーが少ない問題については、こちらの記事が取り上げられていました。 インポスター症候群やハラスメントに対する恐れなどが挙げられています。 Keynote session後のQ&Aでもこのあたりに関する議論が多かったように感じます。 明日は@ryo511さんによる「SVGスプライトアイコンの作り方・使い方」です。お楽しみに!
アバター
こんにちは。今日はイベント開催のご案内です。 12/12(水) に一休.com / 一休レストランの開発事例についてのミートアップイベントを開催いたします。 Ikyu Frontend Meetup 今回は「フロントエンド開発」をテーマとして 一休レストラン スマートフォン検索ページのSPA化 一休.com スマートフォンホテルページのパフォーマンス改善 を軸にNuxt.jsの導入、コンポーネント設計、CSS設計、画像最適化によるパフォーマンス改善などの事例をご紹介いたします。 セッションのほかにも、パネルディスカッションを予定していますので、参加者の皆さまと交流しながら、日々の学びを交換できればと思っています。 お申込みはこちらから。 ikyu.connpass.com イベント実施に至ったきっかけ user-first.ikyu.co.jp この記事を公開したところ、「情報交換しましょう!」というお問合せを多数いただいたので、内容を膨らませてフロントエンド開発の取組みについてお話したら面白いのではないか、ということでイベントを開催することにしました。 お問合せをいただいた会社の方には、よいきっかけを与えてくださり、とても感謝しています。ありがとうございます。 まだ、募集枠には余裕がありますので、皆さまのご応募をお待ちしております!
アバター
2018年4月、データセンター完全クローズ 一休は、今年の4月にデータセンターを完全にクローズしました。現在、すべてのサービスをAWSを使って提供しています。 この過程で各種運用ツールやビルド/デプロイのパイプラインなどをすべて外部サービスを使うように変更しました。 これによって、インフラエンジニアやサービス運用担当者の役割や業務が大きく変わりました。本稿では、その背景を簡単に紹介したいと思います。 ざっくり言えば、 物理サーバのセットアップ&データセンターへの搬入のような仕事はなくなった。 アプライアンスの保守契約、パッチ適用、運用ツールのバックアップのような仕事もなくなった。 各種メトリクスを見ながら、Infrastructure as Codeでクラウドリソースの管理や調整をする仕事がメインになった。 必要に応じて、プロダクトのソースコードに踏み込んで必要な改修を行い、サービスの安定化を支援する仕事も増えている。 結果として、SREとしての役割が求められるようになっています。 クラウド移行 一休のクラウド移行は、2016年末にキックオフ、2017年夏のアプリケーションサーバの移行を経て、2018年2月のデータベースの移行で、完全に完了しました。 概要は、過去の @shibayan と id:ninjinkun の記事でも紹介しています。 user-first.ikyu.co.jp user-first.ikyu.co.jp user-first.ikyu.co.jp これに伴って、以下のようなツールやミドルウェアも外部サービスに移行しました。 ソースコード管理はGitHub Enterprise から GitHub.comへ CI/CDはJenkins + ファイル同期ツールからAppVeyor & CircleCIへ リバースプロキシはBIG-IPからFastlyへ メールサーバはアプライアンス製品からSendGridへ etc それぞれに、 移行の動機 移行のタイミングで一緒に改善できたこと 移行によってなくなった作業 があります。 GitHub Enterprise(GHE) から GitHub.comへ [移行の動機] GHEを使い続ける必然性がない。 [一緒に改善] 不要ファイルを削除してリポジトリのサイズダウン。 [なくなった作業] GHEのバックアップとバージョンアップ。 もともと、社内にGitHub Enterprise(GHE)を立て、ソースコードを管理していました。しかし、社内環境で自前で管理する必然性がありませんでした。 そこで、運用環境をクラウドに移行するのをきっかけにして、GitHub.comへコードを移行しました。 このとき、宿泊予約サービスのソースコードリポジトリに大量に残っていた不要な画像をGitHubへの移行対象外にしました。これによってリポジトリが小さくなり、CI/CDの速度が向上し、開発時のリポジトリ操作の快適になりました。 そして、GHEのバックアップやバージョンアップという作業がなくなりました。 ※ 画像の改善については、以下の id:kentana20 のスライドに詳しいです。 speakerdeck.com Jenkins + ファイル同期ツールからAppVeyor & CircleCIへ [移行の動機] 複雑になってしまったJenkinsをクラウドに持っていく必然性がなかった。 [一緒に改善] ビルドパイプラインの安定化 &リリース回数アップ。 [なくなった作業] Jenkinsのジョブのメンテナンス、不安定なファイル同期ソフトの管理。 CI/CDは社内に立てたJenkinsが担っていました。自前でブルーグリーンデプロイを実現するために複雑なジョブを組んだ結果、メンテナンスしにくくなっていました。またJenkins はバージョン1系を使っていたのでジョブの定義をGitHubで管理できていませんでした。 そして大きな問題だったのがビルド結果を配布するのに使っていたファイル同期ツールです。このツール、有償の製品だったのですが、動作が安定しない。。。 結果、途中でデプロイが失敗して手動でリカバリする、という辛い作業を繰り返していました。 運用環境をクラウドに移行するに伴い、このデプロイの仕組みをそのまま持っていく必然性はありませんでした。 また、アプリケーションはAWS Elastic Beanstalk(EB)を使うことに決まっていたため、デプロイは完全にEBが提供仕組みに任せることができました。 あとは、ビルドをどうやるか、ですが、AppVeyorとCircleCIで実現することにしました。CircleCIだけではWindowsプラットフォームで動作するアプリケーションのビルドに対応できないため、AppVeyorも併用しています。 AppVeyorもCircleCIもymlファイルで制御できるので、アプリケーションのリポジトリで一緒に管理することでメンテナンスしやすくなりました。 この移行によって複雑なJenkinsジョブや不安定なファイル同期ソフトのメンテナンスをする必要がなくなり、CI/CDパイプライン自体が大幅に安定しました。 その結果、リリースに手がかからなくなったため、本番リリースの回数を増やすことができました。 BIG-IPからFastlyへ [移行の動機] クラウドに持っていくのは高コスト。 [一緒に改善] FastlyのCDN積極活用によるサイトのUX改善と安定化。 [なくなった作業] アプライアンスの監視、メンテナンスや保守契約更新。 データセンターでは、ロードバランサ/リバースプロキシとしてBIG-IPを使っていました。 BIG-IPにはiRuleと呼ばれる独自のリライト、ルーティングの機構が搭載されており、一休でもこれを使っていましたが、管理が大変でした。 また、BIG-IPをクラウドに持っていくのも、移行後の運用コストを考えると難しいと判断しました。 そこで、クラウド移行に伴い、BIG-IPの代わりにFastlyを使うことにました。 FastlyをマネージドなVarnishが搭載されたリバースプロキシととらえ、VCLファイルをGitHubで管理し、TerraformとCircleCIでCI/CDを構築しました。結果、インフラエンジニアでなくても、ルーティングのルールを修正できるようになりました。変更に対する心理的な負担も大幅に軽減されました。 また、負荷対策やUXの改善のためにCDNとしての機能も積極的に活用しています。 さらに、にFastlyの進化の恩恵を自動的に受けられるのも大きいです。例えば、ほとんど何もせずにHTTP/2を導入することができました。 アプライアンス製品のメールサーバからSendGridへ [移行の動機] クラウドにアプライアンスを持って行くわけにいかない。 [一緒に改善] メール関連の処理をリファクタリングして合理的に。メルマガ配信も簡単にSendGridに移行。 [なくなった作業] アプライアンスの監視、メンテナンスや保守契約更新。 データセンターではアプライアンス製品のSMTPサーバを使っていましたが、これもクラウドに持っていけません。そこですべてのメールをSendGridを使って送信するようにしました。日本の代理店である構造計画研究所様にもしっかりとサポートしていただきました。 移行の詳細は、過去の @shibayan の記事や、 @minato128 のスライドが詳しいです。 user-first.ikyu.co.jp https://speakerdeck.com/minato128/ikyu-mail-platform 移行に伴い、積極的にリファクタリングしました。 例えば、メール送信の必要性を棚卸しして、不要なメールは廃止しました。また、開発者向けのエラー通知メールなどメールである必要のないものはSlackへの通知に切り替えました。 この移行でアプライアンスの保守更新やパッチ適用などの各種メンテナンス作業から解放されました。 また、別の外部サービスで実現していたマーケティングのメール配信も比較的簡単にSendGridに移行できました。これによってメールマガジンの配信コストが大幅に削減できました。 仕事が減った(^^) じゃあ何する? ほかにも、アプリケーションログの管理や本番データベースの定期運用などにも同様の変化が起きました。この結果、インフラエンジニアの管理作業が大幅に削減できました。 では、今はどんなことをしているかというと、、、 Infrastructure as Codeでクラウドリソースの管理 もちろん、クラウドに移行したからといって、キャパシティプランニングや配置設計のような仕事がなくなるわけではありません。 むしろ、リソースを柔軟かつ素早く確保できるというクラウドのメリットを積極的に活用して サービスの安定化 に貢献する必要があります。 一休ではクラウドリソースの管理をTerraformを使ってAWSのリソースを管理しています。 モニタリングはDataDogを使っています。DataDogのメトリクスを見ながら、リソースの消費具合の推移をにらみつつ、必要な調整を行うのは重要な仕事の一つです。 また、新規サービス構築ではプロダクトを開発しているエンジニアと協力して、Terraformを使って必要なクラウドインフラをセットアップし、モニタリングの設定を行います。Terraform自体のバージョンアップやメンテナンスも重要な仕事です。 プロダクトのソースコードに踏み込んで必要な改修を行う。 プロダクトのコードを修正する機会も増えています。特に、 サービスの安定化 に貢献できるような修正をしています。 例えば、キャッシュの導入や高負荷なSQLの分割 & 非同期化などです。 つまり、 サービスの安定化 がミッションになっています。 というわけで、 インフラエンジニア改めSREになりました。 一休ではインフラエンジニア改めSREとして活躍してくれる仲間を募集しています。 hrmos.co 当社については以下の紹介記事をご覧ください。 user-first.ikyu.co.jp この記事の筆者について システム本部CTO室所属の 徳武 です。 サービスの技術基盤の開発運用、宿泊サービスの開発支援を行なっています。
アバター
一休.com レストランは今年の 7 月 18 日、スマートフォン向け検索ページのリニューアルを行いました。このエントリーでは、その中身について少し紹介させていただきます。 検索ページの課題 一休.com レストランではスマートフォン向け検索ページに対して「遅い」という課題意識がありました。これは技術面で少しブレイクダウンすると; パーソナライズドを含む複雑な処理を行っているため、サーバーサイド処理が重い。 UI 上無駄な遅延処理を行っているため、クライアントサイドの描画が遅い。 というサーバー側とクライアント側両方の課題がありました。クライアントサイドの「無駄な遅延処理」というのは; 検索結果取得が REST API 化されているにも関わず、再検索の度にページリロードを行い、サーバーサイドの描画からやり直している。 という実装に問題がありました。下図がリニューアル前のページ描画の様子です: 画面描画後に動的な検索結果が遅延描画されている図 社内では UI 上のこの問題点に対して課題意識が大きく; 「検索と再検索の回遊性を改善したい」 という要求が強くなっていました。 Web フロントエンドのコンポーネント化 話は変わり、一休.com レストランは古い技術セットの上に構築されています。Microsoft の Classic ASP と VBScript を主体としたアプリケーションで永らく運営されてきました。その一方でユーザー向け web ページの表現力を向上させて行きたいという要求から、Vue.js によるフロントエンド実装も徐々に始まっていました。 Vue.js は SPA のような高い表現力を簡単な記述で実現できるのが魅力です。従来の web 開発では; HTML CSS JavaScript という ファイルタイプによる縦割り開発 が主流でした。しかし; BEM や ECSS のようなCSS フレームワークの提唱する思想、 Vue.js のような JavaScript フレームワークのメカニズム、 Web Components による標準化の流れ から明らかなように、UI 改善において コンポーネントを中心に串刺し設計する事の重要性 が認知されてきています。 一休.com レストランでは、あらゆるフロントエンド実装を Vue.js に移行する事で Smart UI パターンのような開発形態から、コンポーネント化された web アプリケーションへ移行する事を技術的な目標としています。 実現すればコンポーネント指向の持つユーザーメリットを享受できるようになり、一休の掲げる「ユーザーファースト」を後押しする開発体制が実現できると考えています。 SEO とサーバーサイドレンダリング しかしここで問題がありました。SEO の考慮です。一休.com レストランの高い集客力は SEO に真摯に取り組んできた結果でもあります。 この SEO の観点から; ページ上のあらゆる重要なコンテンツは、サーバーサイドレンダリングされていなければならない。 という要求がありました。これは、クライアントサイドレンダリングを基本とする Vue.js でコンポーネント化された web アプリケーションへ移行する目論見と衝突しました。 コンポーネント指向開発を実現する事。 重要なコンテンツはサーバーサイドレンダリングされる事。 この 2 つが「ユーザーファースト」を目指す上で必要な技術要件でした。 尚、今年の Google I/O 2018 の Google Webmasters からのアナウンスで状況は変わりつつありますが、リニューアルの意思決定から開発の段階では、クライアントサイドレンダリングで良しと言える状況ではありませんでした。 業務課題と技術課題の合致 このような背景から、コンポーネント指向とサーバーサイドレンダリングという 2 つ の技術課題の解決手段として、サーバーサイド JavaScript の導入が現実味を帯びてきました。Vue.js においてはユニバーサル JavaScript を実現するフレームワーク Nuxt.js が存在しており、これによるプロトタイプを社内で進めるようになります。 結果として、一休.com レストランの技術スタックの理想像は下図のようになって行きました: フロントエンド構成のヴィジョン これなら業務課題である検索ページの UI 改善と、技術課題を解決できます。最終的に、一休.com レストランの検索ページリニューアルで、Nuxt.js の導入を決断しました。 次の項では Nuxt.js で実装が開始された検索ページの設計を紹介します。 コンポーネント指向設計 検索ページリニューアルでは、全面的にコンポーネント指向設計を導入しました。 コンポーネントの定義 一休.com レストランの web フロントエンドでは、コンポーネントを下図のように捉え定義しました: コンポーネントの定義 データ、テンプレート、ロジック、スタイル、それぞれ関連性が深いもの同士をモジュール化 ファイルタイプによる縦割りではなく、関連性によるファイルタイプ横断の串刺しでグループ化 フロントエンド実装のあらゆるアセットをコンポーネントと捉えて管理 上記を基本とし、CSS、JavaScript、画像、Vue 単一ファイルコンポーネントなど全ての分割粒度として、共通のコンポーネントという概念を前提としました。 ITCSS によるレイヤードアーキテクチャ これらコンポーネントを共通のレイヤードアーキテクチャで管理するため、一休.com レストランでは CSS アーキテクチャの 1 つである ITCSS を採用しました。 ITCSS レイヤードアーキテクチャ ITCSS 自体は CSS エンジニアである Harry Roberts 氏 が提唱する CSS の詳細度を管理するためのレイヤードアーキテクチャです。 CSS でコンポーネント指向設計を実践する事を前提にしたアーキテクチャである点、 抽象度を管理するレイヤードアーキテクチャとして柔軟である点、 これらの点から web フロントエンドのコンポーネント抽象度化レイヤーとして自然に捉え直す事ができます。このアーキテクチャを利用し、以下のようにレイヤーごとの責務を定義しました: レイヤー 定義 Settings CSS 変数や、定数などのデータを扱う。 Tools CSS ミックスイン、フィルター、またはバリューオブジェクト、DTO のようなアプリケーション上の型となる定義を扱う。 Generic CSS 要素型セレクターによるグローバルスタイル定義、アプリケーション全体で共通化された処理、グローバルな副作用を持つビジネスロジックを扱う。 Elements Atoms のようなプリミティヴなコンポーネントを扱う。これ以下のレイヤーで Vue.js SFC を扱う。 Objects Molecules のようなアプリケーション上のコンテキストを含むコンポーネントを扱う。 Components Organisms のようなアプリケーション上意味のある機能単位のコンポーネントを扱う。 上図のように Elements レイヤーより Atomic Design のレイヤー概念も取り入れています。そもそもとして、下図のような Atomic Design によるレイヤードアーキテクチャも検討しました。 Atomic Design レイヤードアーキテクチャ しかし; Atomic Design における Atoms レイヤーはグローバルなデータやビジネスロジックなどを表現するレイヤーとしてはスコープが広くなり過ぎる。 一方でこれらの表現に向いていそうなクリーンアーキテクチャでは、UI コンポーネントの抽象化を表現するには表現力が低い。 ITCSS はその点で、グローバルなデータやビジネスロジックを表現するレイヤーと UI コンポーネントを表現するレイヤーが最初から定義されておりバランスが良い。 と考え ITCSS を採用しました。 パフォーマンスの観点 ITCSS は CSS 詳細度を管理するアーキテクチャであり CSS のクライアントパフォーマンスを最大化する目的があります。また web フロントエンドはコンパイラより実装者にパフォーマンス最適化の責務があると考えます。 この観点から Tools と Generic レイヤーでデータ定義とビジネスロジックを扱うレイヤーを分ける事が、オブジェクトにメソッドを生やしてビジネスロジックを実装するのを避ける事につながり、webpack の tree-shaking による最適化を享受しやすい実装を導出するメカニズムになると考えています。 コンポーネント指向の置き換え可能という特性を、パフォーマンスの観点を持ったレイヤードアーキテクチャで管理する事で DRY を実践しやすく、結果としてハイパフォーマンスなフロントエンドが実現可能なメカニズムとなる事を期待しています。 つづいて Nuxt.js による BFF 実装を進めていく上で難しかった点をいくつか紹介します。 ユニバーサル JavaScript まず Nuxt.js の最大の特徴は、Vue.js による実装で、サーバーサイドもクライアントサイドも透過的に記述できる事でしょう。これによりコンポーネント指向設計において、サーバーとクライアントという動作環境の違いを、ファイルシステムの違い同様、串刺しにコンポーネントとしてカプセル化できるようになります。これが Nuxt.js の大きなメリットです。 サーバーサイドとクライアントサイド API の違い しかし同じ JavaScript とは言え、Node.js と web ブラウザの API には違いがあります。Nuxt.js が用意していないインターフェースで、サーバー/クライアントを透過的に扱いたかったのが次の 2 つです: サーバー/クライアントでの Cookie インターフェースの違いを吸収する バグレポード Bugsnag のサーバー/クライアントサイドのクライアントを透過的に扱う これらの実現に、Nuxt.js の modules と plugins 機能を用いました。 ユニバーサル Cookie Nuxt.js で Cookie を透過的に扱うにあたって UniversalCookie コンポーネントを作成し、サーバー/クライアントサイドでの CRUD 処理を透過的に記述できるインターフェースを用意しました。そしてこれを Nuxt.js の plugins として登録しました。 plugins/cookie.ts : import * as http from 'http' ; import { createUniversalCookie } from '@/components/generic/UniversalCookie' ; export default function ( { req , res } : { req: http.IncomingMessage , res: http.ServerResponse } , inject ) : void { const cookie = createUniversalCookie ( req , res ); inject ( 'cookie' , cookie ); } 上記のような plugin を登録する事で、Nuxt.js のコンテキスト上では app.$cookie.set(key, value) や this.$cookie.get(key) と言った記述で、サーバー/クライント関係なく cookie の読み書きができるようになりました。 ユニバーサル Bugsnag 一休.com レストランではクライアントサイドのバグ検知に Bugsnag を利用しており、ユニバーサル JavaScript 化に当たって、サーバーサイドの JavaScript 処理エラーも Bugsnag へレポートするインターフェースを用意しました。 modules/bugsnag/index.js : const path = require( 'path' ); const bugsnag = require( 'bugsnag' ); module.exports = function BugsnagModule(moduleOptions) { bugsnag.register(moduleOptions.SERVER_API_KEY, { appVersion: (process.env.VERSION_SHA1).slice(0, 7), autoCaptureSessions: false , autoNotify: process.env.NODE_ENV !== 'development' , releaseStage: process.env.NODE_ENV || 'development' , } ); this .nuxt.hook( 'render:setupMiddleware' , app => app.use(bugsnag.requestHandler)); this .nuxt.hook( 'render:errorMiddleware' , app => app.use(bugsnag.errorHandler)); this .addPlugin( { src: path.resolve(__dirname, 'plugin.js' ), options: moduleOptions, } ); } ; modules/bugsnag/plugin.js : import Vue from 'vue' ; export default function (context, inject) { const VERSION = (process.env.VERSION_SHA1).slice(0, 7); // サーバーサイド Bugsnag if (process.server) { const bugsnag = require( 'bugsnag' ); bugsnag.register( '<%= options.SERVER_API_KEY %>' , { appVersion: VERSION, autoCaptureSessions: false , autoNotify: process.env.NODE_ENV !== 'development' , releaseStage: process.env.NODE_ENV, } ); inject( 'bugsnag' , bugsnag); } // クライアントサイド Bugsnag if (process.client) { const bugsnagJs = require( 'bugsnag-js' ); const bugsnagVue = require( 'bugsnag-vue' ); const client = bugsnagJs( { apiKey: '<%= options.CLIENT_API_KEY %>' , appVersion: VERSION, autoCaptureSessions: false , autoNotify: process.env.NODE_ENV !== 'development' , releaseStage: process.env.NODE_ENV, } ); client.use(bugsnagVue(Vue)); inject( 'bugsnag' , client); } } 上記のような module を登録する事で、 app.$bugsnag.notify(new Error('...')) ないし this.$bugsnag.notify(new Error('...')) でハンドリングされたエラー処理のレポートを透過的に記述できるようになり、例外もサーバー/クライアントサイド両方を検知できるようにしました。 ただしこれらは Nuxt.js のコンテキストを通じてインターフェースを初期化する必要があるため、 this.$cookie や this.$bugsnag への参照を持つコンポーネントは Nuxt.js 実装に密結合となります。なのでこれら plugins へのアクセスは layouts/pages を通じてのみ行うルールとし、コンポーネントの責務をコントロールしています。 副作用の考慮 クライアント JavaScript をユニバーサル JavaScript に対応する過程にも注意する事がありました。グローバルな初期処理による副作用です。 例えば都度参照ではコストが大きい window.innerWidth のようなグローバルプロパティ値をキャッシュする次のようなモジュールがあります。 windowsize.js : let windowWidth; let windowHeight; function resize() { windowWidth = window .innerWidth; windowHeight = window .innerHeight; } window .addEventListener( 'resize' , resize, false ); window .addEventListener( 'orientationchange' , resize, false ); resize(); export { windowWidth, windowHeight, } ; このようなモジュールが Nuxt.js の pages コンポーネントで import されるとサーバーエラーとなり、ユーザーには 500 エラーが返る事になります。しかしクライアントサイドに限った JavaScript 実装であれば、ありがちな実装であり、グローバルな副作用を局所化する手段としても合理的です。しかし window オブジェクトのようにクライアント JavaScript にしか存在しない API の暗黙的な参照が発生する上記のようなモジュールをうっかり Nuxt.js で import すればアプリケーションが起動しなくなります。こういった点はユニバーサル JavaScript において煩わしさを感じる点でもあり、副作用の影響を考える上で面白い点でもあると思います。 これを解決する方法としては、あらゆるグローバルな処理を Nuxt.js (Vue.js) のライフサイクルに載せるという方法を取りました。上記の windowsize.js は次のように変更しました。 windowsize.js : import Vue from 'vue' ; export const WindowSize = Vue.extend( { data() { return { height: undefined , width: undefined , } ; } , created() { if ( this .$isServer) { return ; } window .addEventListener( 'resize' , this .resize, false ); window .addEventListener( 'orientationchange' , this .resize, false ); this .resize(); } , beforeDestroy() { window .removeEventListener( 'resize' , this .resize); window .removeEventListener( 'orientationchange' , this .resize); } , methods: { resize(): void { this .height = window .innerHeight; this .width = window .innerWidth; } , } , } ); export const windowSize = new WindowSize(); こうする事でクライアントサイドの API に依存するグローバルな処理が記述されたモジュールでも透過的に import できるようになりました。 クライアントサイドのみまたはサーバーサイドのみの実装とは違い、いくつか考慮するべき事はありますが、結果的により堅牢な実装を求められる点がユニバーサル JavaScript の面白さでもあると思います。 リニューアルの成果 リリースされた検索ページでは下図のように不要なリロードを必要としない SPA にリニューアルされました。これによってスマートフォンでの再検索のストレスが軽減されたと考えています。 SPA 化による不要なリロードの無くなった検索ページ またクライアントサイドのパフォーマンス指標である Speed Index の RUM 値も、リリースを境に改善する事ができました。 RUM-SpeedIndex トラッキング値の変化 このリニューアルを契機に Classic ASP による密結合なアプリケーション実装から、Python をバックエンド、API とし、BFF と Nuxt.js をフロントエンドとする疎結合な開発体制が確立しました。今後はこの開発体制のメリットを最大化しユーザー体験の向上へと繋げていくのが、新フロントエンドの課題です。 モダン・フロントエンドで提供する価値とは 変化の激しい web フロントエンドですが、昨年は PWA の推進や AMP の登場、パフォーマンス指標の定量化など、めまぐるしい年だったように思います。一休.com レストランの web サイトは、これら技術を最大限活用し、ユーザーにとってより良い web サービスを提供してゆきたいと考えています。 今後の web フロントエンドの取り組みとしては; 継続的なパフォーマンス改善 PWA 化による web 体験のエンハンスメント ユーザー体験の向上につながるドラスティックな UI 改善 などを考えています。 そんなわけで 一休.com レストランでは、ユニバーサル JavaScript が得意なフルスタックエンジニア、BFF 設計や GraphQL を得意とされる Node.js エンジニア、コンポーネント指向を実践でき Web Components のような標準仕様にも敏感な web フロントエンドエンジニア、デザインシステムや Brad Frost 氏の提唱する Atomic Design へ高い関心をお持ちのデザイナーなど、プロフェッショナリズムにあふれたメンバーを募集しています。ラグジュアリーなサービスを最高のクラフトマンシップで支えてくれる方からのご応募お待ちしております! www.ikyu.co.jp www.wantedly.com 以上、CTO 室レストラン担当エンジニアの稲尾 id:supercalifragilisticexpiali がお伝えしました。
アバター
こんにちは。 一休.com の開発基盤を担当しています、akasakasです。 今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善した話を書きます。 概要編はこちらになります。 user-first.ikyu.co.jp JavaScriptパフォーマンス改善編はこちらになります。 user-first.ikyu.co.jp CSS・その他パフォーマンスチューニング編はこちらになります。 user-first.ikyu.co.jp この記事ではスマートフォンホテルページリニューアルで実施したサーバサイドチューニングについて書きます。 ここでお話しする内容 サーバサイドチューニング前後のOverview プロジェクトの大まかなタイムライン ボトルネック洗い出し 対策 SQL改善 不足インデックスの設定 => 600ms の改善 複雑なselect文をシンプルなselect文に分割し、aync/await で非同期化 => 130ms の改善 Solr高速化 SolrへのリクエストをHttpClientを使って可能な限り async/awaitで非同期にする。=> 20ms 程度の改善 Solrを5系から6系にバージョンアップ => 100ms の改善 Solrが動いているEC2インスタンスをc4系からc5系にスケールアップ => 30ms の改善 JavaScriptを使った遅延取得化 => 100ms の改善 ここまでやって、速くなった、が、、、 アプリケーションをASP.NET MVCに移行 => 250ms の改善 I/O処理を一か所に集めて、async/awaitで非同期 鮮度が求められないデータはElasticacheでキャッシュ QAとリリース QA chromelessを使った自動テスト 手動&実機テスト リリース まとめ サーバサイドチューニング前後のOverview スマートフォンホテルページのサーバサイドの改善前の構成は、以下の図の通りです。 ASP.NET Web FormsのWebアプリがページを提供している このWebアプリは、検索API経由で対象ホテルの宿泊プランや部屋の情報を取得している このWebアプリは、宿泊プランや部屋の情報以外の情報は、検索APIを経由せずに直接DBアクセスして取得している 改善後は WebアプリからASP.NET Web FormsからMVCにリプレイス Webアプリは、DBを直接見ない データ取得処理は、検索APIの一か所にまとめてasync/awaitで取得 検索APIの呼び出しもasync/await 鮮度を問わないデータはElasticaheでキャッシュ 改善前 改善後 検索APIが内部通信ではなくFastly経由になっているのは、Fastlyのキャッシュの仕組みを利用しているためです。 プロジェクトの大まかなタイムライン 4月に、速度改善プロジェクトを本格的に始動 この時点の構成は↑の改善前の図 4月の時点で、サーバサイドの処理速度は1週間平均で1500msくらい これを、200ms以下にするのが目標 まず、比較的簡単な性能課題を解消することで、6月の時点で500msくらいまで改善 その後、全面的な作り直しを実施 7月末にほぼ実装完了し、QA開始 8月中旬に先行リリース、8月末に完全リリース 最終的には、9月初頭の時点で、1週間平均で250msくらい 中央値なら200ms前後。ほぼ目標達成 現時点での構成は↑の改善後の図 ボトルネック洗い出し まず、どこで時間がかかっているかを、洗い出しました。 ツールは、 Newrelic 。それと、アプリケーションのログも参考にしました。 見つかったボトルネックは、 外部へのI/O処理、つまり、検索APIのデータベースアクセスとSolrへのクエリ ユーザーの操作に応じてフロントエンドでJavaScriptを使って遅延取得すればいい情報をサーバーサイドで取得している。 まずは、一番取り組みやすく効果も見込めるselect文の改善に着手しました。 ↑の改善前の図の「表示する情報を取得①」と「表示する情報を取得②」のselect文です。 Newrelicなら、ひとつのリクエストで、どのSQLが平均何回呼ばれ、1回あたりどのくらい時間がかかっているか教えてくれます。 有償版なら、対象のSQL文全体を表示してくれます。 対策 SQL改善 不足インデックスの設定 => 600ms の改善 性能改善の基本的なアプローチで、一番簡単かつ効果があるのが、不足インデックスの付与です。 SQL Serverなので、SQL Server Management Studioでselect文を実行し、実行計画を確認すると不足インデックスをサジェストしてくれます。↓のように。 Newrelicで採取したselect文にパラメータを補完して、本番と同等のデータが入っている開発用データベースでSQLを実行し、不足インデックスを洗い出します。その結果、数本のselect文でインデックス不足が見つかりました。 このインデックス付与で大幅に改善しました。 複雑なselect文をシンプルなselect文に分割し、aync/await で非同期化 => 130ms の改善 インデックス付与には大きな効果があったのですが、まだまだ目標には達しません。 「表示する情報を取得②」のselect文はかなり複雑になっており、インデックスでの改善は見込めませんでした。 そこで、この複雑なselect文をデータの多重度がそろうように3分割し、async/awaitで非同期で取得して、ソースコード上で、Joinするようにしました。 ソースコード上でJoin、というとコードが複雑になりそうですが、LinqとObject to Object Mapperライブラリである Mapster を活用することで、2,3行で実現できました。 .NET で Object to Object Mapperというと AutoMapper がメジャーですが、より高速でよりシンプルに扱えるMapsterを選択しました。 検索APIは、ASP.NET Web APIですが、アクションメソッドにasyncキーワードがついていませんでした。そこで、このタイミングで、すべてのアクションメソッドをasyncにして、非同期処理をかけるようにしました。 Solr高速化 SolrへのリクエストをHttpClientを使って可能な限り async/awaitで非同期にする。=> 20ms 程度の改善 検索APIのすべてのアクションメソッドをasyncにしたので、HttpClientを使った外部へのHttpリクエストもasync/awaitで非同期にできるようになったため、非同期にしました。 最初のリクエストの結果が次のリクエストの結果に入力になっている場合は、 非同期にできませんが、それ以外は可能な限り非同期化、です。 Solrを5系から6系にバージョンアップ => 100ms の改善 もともとSolrの5系を使っていたので、6系にアップしました。 単純にバージョンを上げたら性能が改善するんじゃないか、という仮説でした。 これに伴ってJavaのバージョンも上がりました。 Solrが動いているEC2インスタンスをc4系からc5系にスケールアップ => 30ms の改善 一休のサービスはすべてAWS上で運用されており、SolrもEC2で動いています。 c4系のインスタンスを使っていたのですが、昨年発表されたc5インスタンスに移行しました。 単純にCPUのスペックが上がっているので、cpu intensiveなSolrならある程度恩恵をうけるんじゃないか、という仮説でした。 JavaScriptを使った遅延取得化 => 100ms の改善 Below the fold のパーツに対して、画面表示のリクエストでサーバサイドでデータを取得している箇所がありました。 これを、ユーザーの操作に応じてフロントエンドから非同期でデータを取得するようにしました ここまでやって、速くなった、が、、、 ここまでの改善で、1500msかかっていた処理が、500msくらいまで改善しました。 大幅に改善したのですが、目標はサーバサイドの処理で200msを切る、です。 主に検索APIの改善でここまで高速化しました。 逆に言うと検索APIのこれ以上の雑巾絞りできなさそうです。 アプリケーションをASP.NET MVCに移行 => 250ms の改善 あとは、Web Formsで作られているアプリケーションの改善のポイントを見出すしかありません。 ここまでの改善で、async/awaitを積極的に使えば、まだまだ改善が見込めることは明らかでした。 そこで、ホテルページだけASP.NET MVCで全面的に作り直すことにしました。以下のような理由です。 既存のWeb Formsのコードがかなり複雑。この複雑さを前提にして速度改善を進めるのは厳しい。 フロントエンド側も、大幅な性能改善のためには全面的に書き直しが必要な状態だった。 Web Formsでasync/awaitを使うには特殊な書き方をする必要がある。Web Forms固有の概念や書き方に縛られたくない。 事前に @shibayan が横断的な関心事を処理するMVCの基盤を作ってくれていましたので、これをベースに黙々と作り直しをしました。 既存のコードを読み、動作を確認し、Todoを洗い出し、実装する、というのをぐるぐる繰り返しました。 I/O処理を一か所に集めて、async/awaitで非同期 改善前は、画面に表示する情報を、Web FormsのアプリケーションとWeb APIの検索APIの両方で取得していました。 これをWeb APIの一か所にまとめて、可能な限りasync/awaitを使って非同期にすることで、IO待ちをなるべく発生しないようにしました。 鮮度が求められないデータはElasticacheでキャッシュ 画面に表示する情報には、頻繁に変更されないデータがあります。その中で、データ取得のコストが高いデータはElasticacheでキャッシュするようにしました。 QAとリリース QA 8割できかがったところで、同じく全面的に作り直しているフロントエンドと結合して、試験をします。 試験は、2段階に分けて行いました。 chromelessを使った自動テスト 今回の速度改善は、機能的には改善前と改善後で変わりません。既存のmasterブランチが正解の挙動です。 そこで、以下のようなスクリプトを作り自動テストを行いました。 本番のfastlyのアクセスログからリクエストパスを抽出する。 本番相当のデータが入っているデータベースにつないだmasterブランチとテスト対象のブランチに対して、 chromeless で1. のリクエストパスでリクエスト。両者のレスポンスから表示情報を抽出。 2.の表示情報を突合。差異があったらエラーとする。 これを、2週間くらい毎日繰り返しました。リクエストにばらつきがあったほうが、バグの検出が高まるからです。 また、スクリプトをあまり厳密に作りこみすぎないように注意しました。発生条件が複雑なバグは手で触らないと見つからないだろう、と割り切って、あくまで「正常系のユースケースでちょっと触ったらすぐに見つかりそうなバグ」を見つけるためのツールとして作りました。 手動&実機テスト 実装が一通り終わった段階でテストケースを作成し、プロジェクトメンバー3人で実機テストをしました。 テストケースをすべて消化したら、会議室を押さえて、スマートフォンホテルページの仕様に詳しい有識者6人くらいで1時間黙々と実機で触りました。 この最後のQA会でもいくつかの重要な問題が見つかりました。 仕様に詳しい有識者だからこそ発見できる問題があり、ここで大きなBugfixを潰すことができたのは大きかったと思います。 リリース リリースもテスト同様、QA同様、2段階で行いました。 まず、いくつかのホテル、施設のみを先行リリースします。 表示情報が多く、それほどアクセスが多くない施設を選びました。施設タイプ(ホテル、旅館、ビジネスホテル、貸別荘)もばらつかせました。 先行リリースには、IISのRewrite機能を使いました。 先行リリース後は、DatadogやNewrelic、Google Analyticsなどを用いて、先行リリースした施設に何かおかしなことが起こっていないか、注視しました。 QAフェーズに時間をかけたのが功を奏し、とくに問題は起こりませんでした。 1週間ほど様子を見て、問題なかったため、全面的にリリースしました。 サーバサイドの1週間平均の処理速度は、500msから250msまで下がりました。 まとめ 今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善した話を書きました。 その中でも、サーバサイドチューニングとQA・リリースについて書きました。 このプロジェクトを通じて、感じたことをまとめると async/awaitは積極的に使っていくべき。 複雑なJoinのselect文を一発実行するよりも、複数のシンプルなselect文をasync/awaitで発行して、ソースコード上でJoinするほうが、いろいろな面で良い。 複雑なJoinのselect文はメンテしにくい。かつ、実行計画の変化で突然、遅くなる場合がある。 QA大事。 計画に対して1週間ほどスケジュールが遅れましたが、これは、QAに予想以上に時間がかかったため。 QAにかかる工数の見積もりは難しい、と感じましたが、時間をかけただけの意味はあったようです。 リリース後に見つかったバグはほとんどなかった。 Newrelicは非常に価値のあるプロダクトだと改めて感じました。 ボトルネックの調査で大活躍。 性能面の調査だけでなくインシデント発生時の調査でも有効。
アバター
こんにちは。 一休.com の開発基盤を担当しています、akasakasです。 今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きます。 概要編とJavaScriptパフォーマンス改善編はこちらになります。 user-first.ikyu.co.jp user-first.ikyu.co.jp この記事ではスマートフォンホテルページリニューアルで実施したCSS・その他細かいチューニングについてお話しします。 ここでお話しする内容 CSS再設計&チューニング編 リニューアル前のスマートフォンホテルページのCSSの現状整理と抱えていた課題 リニューアルをするにあたり、CSS再設計 CSS Modules FLOCSS パフォーマンス インライン展開 非同期読み込み ドキュメント整備 その他細かいチューニング編 resource hints/preconnect 画像の遅延ロード imgix まとめ おまけ:こんなスタイルは嫌だ CSS再設計&チューニング編 リニューアル前のスマートフォンホテルページのCSSの現状整理と抱えていた課題 リニューアル前のスマートフォンホテルページのCSSに関する課題は以下の点が大きな悩みでした。 7,000行越えのメンテナンス困難なCSS どのスタイルがどこで使われているか不明 いろんなところでいろんなCSSファイルが呼ばれている 7,000行越えのメンテナンス困難なCSS 上書きに上書きを重ねているので、メンテナンスが大変でした。 どのスタイルがどこで使われているか不明 ネストが深く、命名ルールもなかったので、どのスタイルがどこで使われているのかがわかりづらかったです。 .search_cont #rmsrch_frame_disp_main .rmsrch_frame_disp_row .pep_frame { width : 24% ; } いろんなところでいろんなCSSファイルが呼ばれている 概要編でお話しましたが 一休.comのほとんどはVB.NETでシステムが構築されています。 ASP.NET Web Formsベースの独自フレームワークです。 プレゼンテーション層はMasterPageがあり、 それぞれの画面に対して、aspxファイルがあり、 また、細かい部品ごとに対して、ascxファイルがあります。 それぞれのファイルに対して、cssファイルが定義されていたため、 bodyタグの途中でCSSファイルが呼ばれているなんてこともありました。 レイアウトは下記のようなイメージです。 一例ですが、 divタグ内でstylesheetが呼ばれているということもありました。 リニューアルをするにあたり、CSS再設計 リニューアルをするにあたり、上記の課題を解決し、パフォーマンス面の改善も図りました。 キーワードは以下になります。 CSS Modules 一休では Vue.js を採用しています。 Vue コンポーネントに関しては CSS Modules を積極的に採用しています。 CSS Modules を採用し、コンポーネントごとにスコープを切れば、どのスタイルがどこで使用されているかがわかりやすくなり、メンテナンス性が保証されると考えました。 FLOCSS しかし、CSS Modules 以外でグローバルに影響を及ぼすCSSを書かなければいけないケースが発生します。 上記のケースについては別途対応が必要だと考えました。 こちらはFLOCSSを採用しました。 FLOCSSについてはこちらをご覧いただければ、わかると思います。 github.com ディレクトリ構成は下記のようなイメージです。 ├─app │ sd_hotel.css │ ├─foundation │ _base.css │ _reset.css │ ├─layout │ _footer.css │ _header.css │ └─object ├─component │ _component1.css │ _component2.css │ ├─project │ │ │ └─hotel │ _hotel_part1.css │ _hotel_part2.css │ └─utility _utility1.css _utility2.css CSS プリプロセッサにPostCSSを採用しました。 Foundation/Layout/Objectのcssファイルを統括するためのsd_hotel.cssがあるようなイメージです。 sd_hotel.cssはFoundation/Layout/Object内のcssファイルを適宜インポートしています。 ここはFLOCSSの基本的な考え方を採用しています。 @charset "utf-8" ; /* ========================================================================== // Foundation ==========================================================================*/ @import "../foundation/_reset.css" ; @import "../foundation/_base.css" ; /* ========================================================================== // Layout ==========================================================================*/ @import "../layout/_header.css" ; @import "../layout/_footer.css" ; /* ========================================================================== // Object ==========================================================================*/ /* ========================================================================== // Component ==========================================================================*/ @import "../object/component/_component1.css" ; /* ========================================================================== // Project ==========================================================================*/ @import "../object/project/hotel/_hotel_part1.css" ; @import "../object/project/hotel/_hotel_part2.css" ; /* ========================================================================== // Utility ==========================================================================*/ @import "../object/utility/_utility2.css" ; パフォーマンス CSSはレンダリングブロック対象になります。 レンダリングブロックはパフォーマンス低下に繋がるので、これを解消するために下記2点を気をつけました。 インライン展開 Above the Fold に入るスタイルはインライン展開することでレンダリングブロックを回避しました。 ただ、htmlファイルに直接、インライン展開したスタイルを書くとメンテナンス性を保証することはできません。 そこで、サーバサイド側で外部cssファイルを読み込んで、インライン展開するヘルパーメソッドを1つ用意しました。 これにより、外部ファイルで管理できて、かつインライン展開ができるので、メンテナンス性とパフォーマンスの向上を図ることができました。 非同期読み込み Below the Fold になるスタイルはインライン展開せずに、 loadCSS を使って、非同期で読み込むようにしました。 ドキュメント整備 今後の色んなエンジニア・デザイナーがメンテナンスするためにドキュメントを整備しました。 書いたこととしては 設計方針 ディレクトリ構成 命名ルール パフォーマンス アンチパターン です。 その他細かいチューニング編 resource hints/preconnect サードパーティドメインに対してはpreconnectを指定して、あらかじめDNSの名前解決に加え、TCPコネクションまで貼っています。 画像の遅延ロード Below the Fold の部分の画像は初回リクエストに含めず、遅延ロードさせてます。 一休では画像の遅延ロードにlazysizesを使っています。 github.com imgix imgixの導入で画像最適化ができたので、パフォーマンス改善に大きく寄与しました。 imgixについての記事はこちらをご覧頂ければと思います。 user-first.ikyu.co.jp まとめ 今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きました。 その中でも、CSS・その他細かいチューニングについてお話させて頂きました。 リニューアル前には下記の課題がありましたが、 7,000行越えのメンテナンス困難なCSS どのスタイルがどこで使われているか不明 いろんなところでいろんなCSSファイルが呼ばれている 上記の課題に対して CSS Modules/FLOCSS を採用し、メンテナンス性を担保 インライン展開/loadCSSを使った非同期読み込みでパフォーマンス改善 今後はこれをスマートフォンホテルページだけでなく、他のページにも展開して行き、ユーザに高い価値を提供していきたいと思います。 おまけ:こんなスタイルは嫌だ CSS再設計をしている途中で見つけたリプレイス前の残念スタイル集を少しだけお見せします。 過去から学び、今後に活かしていきたいです。 HTMLに直接書いている < span style = "font-size:13px; font-weight:normal;" > ネストが深すぎる、HTML側のDOM修正時に影響を受けやすい #sdGuidePage .figureimg-list-roomlist .shisetu_box.guide_top_photo .photoTop img , #sdGuidePage .figureimg-list-roomlist .shisetu_box.guide_top_photo .photoNext img , #sdGuidePage .figureimg-list-roomlist .shisetu_box.guide_top_photo .photoPrev img { height : 24px ; } 同じ内容 .mt10 { margin-top : 10px ; } .top_m_10px { margin-top : 10px ; } CSSエンジニア募集 一休ではユーザに高い価値を提供することができるCSSエンジニアを募集しています! hrmos.co 参考資料 FLOCSS loadCSS
アバター
宿泊事業本部の宇都宮です。 一休.com スマホサイトのホテルページパフォーマンス改善プロジェクト では、フロントエンドには以下のような要件がありました。 デザイン面は既存を踏襲する 機能はほぼ従来通り 日付等を変更した際の再検索は、画面遷移を挟まず、画面内で行えるようにする パフォーマンスをできるだけ改善する 要するに、従来と同様の機能+αを実現し、かつ、従来と同等以上のパフォーマンスを実現する、というミッションです。 このために、どのような取り組みを行ったか、紹介します。 パフォーマンス目標値の設定 まず、パフォーマンスの目標値を設定する必要があります。モバイルでは、ユーザの帯域幅は回線や時間帯によって大きな変動があります。多少回線状況が悪くても、閲覧を妨げない程度のパフォーマンスを実現する必要があります。 一休へアクセスするユーザのモニタリングを見ると、極端に遅い回線を使っているユーザは少ないことから、回線速度のベースラインは1.4Mbpsとしました(この数字はChrome DevToolsのFast 3Gの回線速度なので、シミュレートしやすく、開発上都合が良いという理由もあります)。また、ロードが完了し、ユーザーが操作できるようになるタイミング(TTI, Time To Interactive)までの所要時間は5秒以内を目標とします。 計測とボトルネックの把握 次に、現行サイトのパフォーマンスの計測を行いました。詳細な計測結果については、 概要編 をご覧ください。 計測の結果、現在のパフォーマンスは目標値(3G FastでTTI 5秒以内)に届いていないことがわかりました。TTIはおよそ15秒で、快適に使えるとはいいがたい状況でした。 また、フロントエンドでは、js/cssによるレンダリングブロックが大きなボトルネックになっていることがわかりました。 レンダリングブロックの解消 レンダリングブロックとは、ブラウザが画面の描画を開始するまでに読み込みの必要なリソースがあるせいで、レンダリングを始められない現象です。具体的には、headタグ内でjsやcssを読み込んでいる際に発生します。レンダリングブロックが発生しているページは、たとえサーバのレスポンスが十分に速くても、画面が表示されるのは遅くなってしまいます。 JavaScriptのレンダリングブロック解消のための手段は簡単で、全てのJavaScriptをbodyタグの末尾で読み込むことです。しかし、既存コードでは、headタグでライブラリを読み込んでいたり、bodyの途中にscriptタグを書いていたりして、レンダリングブロックが発生していました。そこで、全てのJSコードを見直し、bodyタグの末尾で読み込む単一のjsファイルが全ての処理の起点になるよう改めました。バグを作り込まないよう慎重に行う必要はありますが、それほど難しい作業ではありません。 これによってレンダリングブロックの解消ができました。めでたしめでたし…といいたいところですが、これだけでは十分なパフォーマンス改善は実現できませんでした。 リソースの削減 前述したように、帯域幅のベースラインは1.4Mbpsとしました。1.4Mbpsの回線で、5秒以内に操作可能にするためには、Webページの初期ロード時に読み込む全リソースの合計を、875KB(1.4Mbps(=175KB/s) * 5s)に収める必要があります。この数字はJavaScriptの実行時間等を考慮しない、理論上の上限値であり、実際にはこれより減らす必要があります。 では、一休の現行ホテルページはどうかというと、約1MBのリソースを読み込んでいました。これでは、どう頑張ってもパフォーマンスの目標値を達成することはできません。そこで、全リソースを合計したサイズの上限を、ひとまず700KBに設定します。 次に、Webページにおいて最も「重い」リソースであるJavaScriptについて。JavaScriptは、ダウンロードだけでなく、実行の時間もかかるため、極力サイズを減らす必要があります。一休の現行ホテルページでは400KBものJavaScriptを読み込んでいましたが、これは300KBに抑えることを目標にしました。 まとめると、 全体で700KB JSは300KB を上限として、読み込むリソースの削減を目指しました。 JavaScriptの最適化 初期ロード時のJavaScriptの最適化には、2つのポイントがあります。1つはjsファイルの削減、もう1つは実行時間の削減です。 ここでは、主にビルド周り(webpack, Babel等)の最適化と、Vue.jsアプリケーションの最適化を行いました。 JSバンドルサイズの最適化 JavaScriptコードを削減するには、不要なコードの読み込みを減らす必要があります。一休.comでは、JavaScriptはwebpackでバンドルしています。従来のwebpackビルド設定は、バンドルの粒度が大きめで、不要なコードを読み込みがちという問題がありました。そこで、ページ毎にバンドルを作成するようにしました。これによって、不要コードを大幅に削減することができました。 また、後述しますが、dynamic importを使ってVueコンポーネントを動的に読み込むことで、初期ロード時のバンドルサイズを削減しています。 Babel設定の最適化 先日、 Babel 7 の正式版がリリースされました。Babel 7を導入することで、若干ですが、ビルドサイズの削減が見込めます。ホテルページの場合、productionビルド後のjsファイルのサイズが584KB => 549KBに減りました。 また、一休.comでは、IE 11でもPromise等のES2015以上で利用可能な機能を使えるようにするため、 @babel/polyfill を使っています。最新ブラウザをターゲットにするなら、polyfillを減らせます。そこで、モバイルOSのみをターゲットにビルドして、ビルド後のサイズがどの程度になるか確認しました。結果、ターゲットをiOS >= 9にすると544KB、iOS >= 10にすると518KBといった結果になりました(ちなみに、最新のChromeだけをターゲットにすると507KBまで減らせます)。 iOS 9系のユーザ数はかなり少なくなっていて、近い将来、ターゲットはiOS >= 10になることが予想されます。そこで、PCとモバイルでbabelのpresetを分け、別々にビルドするようにしました。 Vue.jsのランタイム限定ビルドを使う Vue.jsには完全ビルドとランタイム限定ビルドがあります。ランタイム限定ビルドはVueコンポーネントのコンパイラを含まないため、30%ほどサイズが小さくなります。vue-cliを使えばデフォルトでランタイム限定ビルドが使われますが、webpackのconfigを手作りしている場合には完全ビルドを参照していることがあるので、設定を見直してみると良いと思います。 https://jp.vuejs.org/v2/guide/installation.html#%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%93%E3%83%AB%E3%83%89%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6 遅延レンダリングによるJavaScript実行時間の削減 Vue.jsでは、条件付きレンダリングを行うためのディレクティブとして、 v-show と v-if が用意されています。この2つの使い分けは、ドキュメントには以下のように記載されています。 一般的に、v-if はより高い切り替えコストを持っているのに対して、 v-show はより高い初期描画コストを持っています。 そのため、とても頻繁に何かを切り替える必要があれば v-show を選び、条件が実行時に変更することがほとんどない場合は、v-if を選びます。 https://jp.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show どちらもそれほど変わりがないように思えますが、大量のコンポーネントを条件付きレンダリングする際には、どちらを使うかはとても重要です。私は、条件付きレンダリングでは原則として v-if を使うべきだと考えています。なぜなら、v-ifを使うと、その要素は遅延レンダリングされるからです。 // SomeComponent のコードは実行されない <some-component v-if="false" /> // SomeComponent のコードは実行され、描画も行われるが、非表示になる <some-component v-show="false" /> デフォルトで非表示の要素 => v-ifで遅延レンダリングする デフォルトで表示する要素 => 原則v-ifを使うが、v-showでもよい といった具合で使い分けています。また、v-ifは次節で解説する非同期コンポーネントと組み合わせできる点でも、パフォーマンス上有利です。 非同期コンポーネント ECMAScriptのモジュールシステムには、静的なimport( import 'module' )と、動的なimport( import('module') )があります。後者の動的なimport(dynamic import)を使うと、必要になったタイミングでjsコードを取得できます。 特に、複雑なVueコンポーネントはサイズが大きくなりがちなため、dynamic importで動的にコードを読み込むようにすると、初期ロード時に必要なjsの量を大きく減らすことができます。 Vue.jsでは、以下のように、コンポーネント登録時にimport()を呼び出す関数を使用することで、コンポーネントをdynamic importできます。 // グローバル登録の場合 Vue.component('RoomDetail', () => import('./RoomDetail.vue')); // ローカル登録の場合 export default { name: 'RoomList', components: { RoomDetail: () => import('./RoomDetail.vue'), } }; このように、dynamic importされるVueコンポーネントのことを、Vue.jsのドキュメントでは非同期コンポーネント(async component)と呼んでいます。 なお、dynamic importは通信のオーバーヘッドが発生する分、静的なimportよりも遅いです。dynamic importの対象は、初期描画には不要なコンポーネントのみにすべきです。 また、v-ifでレンダリングしない状態になっているコンポーネントは、フラグがtrueになった段階でdynamic importされます。以下のように、初期状態では非表示で、フラグによってレンダリングされるコンポーネントがある場合、dynamic importを使用した遅延読み込みを検討すべきです。 <button type="button" @click="flag = true">Click me</button> <!-- SomeComponentは非同期コンポーネントにできる --> <some-component v-if="flag" /> <!-- このdivはコンポーネント化すれば非同期コンポーネントにできる --> <div v-if="flag"> ... </div> Vueコンポーネント設計のアンチパターン Vueコンポーネントは、ビルド後のサイズはそれほど小さくありません。シンプルなコンポーネントでもminify後で1KBほどになります。たとえば、ボタンやアイコン等、見た目が異なるだけのコンポーネントを細かくコンポーネント化すると、ビルド後のjsファイルのサイズが膨らんでしまいます。パフォーマンスの観点からすると、細かいVueコンポーネントを大量に作るのは避けるべきです。 ライブラリの削減 不要なライブラリを削除するのはもちろんですが、それだけでなく、ライブラリの使用量自体を抑えることを考えるべきです。今回のプロジェクトでは、jquery-migrateを削除することはできましたが、jQueryを依存関係から取り除くには至りませんでした。 jQueryは軽量なライブラリではないため、パフォーマンスの観点からは削除したいライブラリの1つです。一方で、その便利さから至るところで使われているため、簡単に依存を取り除くことはできませんでした。 改善結果 概要編 にもいくつか結果が載っていますが、ここでは転送量に着目するためWebPageTestの結果を掲載します。 Before After 転送量(Fully Loaded > Bytes in)が300KB近く減り、Speed Indexは1000以上改善しました。 パフォーマンス監視の導入 パフォーマンスは放っておくと劣化していくので、Webサイトの速度を維持し続ける仕組みが必要です。ここでは、パフォーマンス予算(Performance Budget)の設定と、監視を行っています。具体的には、パフォーマンス監視SaaSの Calibre を使用し、予算を超過した際にアラートがSlackの開発者向けチャンネルに流れるようにしました。 今後の展望 ホテルページについては一定のパフォーマンス改善を実現することができましたが、サイト全体としてはまだまだ伸びしろがあると考えています。スムーズに宿泊施設を探すには、ページ単体ではなく、検索導線(トップ・リスト・ホテル)の全体的な回遊性が重要です。パフォーマンス改善も含め、引き続きUI/UXの改善を行っていきたいと考えています。 一休では、UI/UXの改善に熱意のあるエンジニアを募集しています! hrmos.co
アバター
こんにちは。 一休.com の開発基盤を担当しています、akasakasです。 今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きます。 UI部分は既存を踏襲する形をとり、UX・パフォーマンス改善にフォーカスして、所々で様々な工夫をしました。 お話ししたいことが盛りだくさんなので 概要編 JavaScriptパフォーマンス改善編 CSS・その他細かいチューニング編 サーバサイド編 の4つに分けて、お送りしたいと思います。 この記事ではスマートフォンホテルページリニューアルの全体像についてお話しします。 詳しいお話をする前に:スマートフォンホテルページってどこ? こちらになります https://www.ikyu.com/sd/00001290/ ここでお話しする内容 リニューアル前後のパフォーマンス比較 PageSpeed Insights Audits Calibre パフォーマンス改善が必要だった理由とリニューアルの背景 ASP.NET Web Forms(VB.NET) → ASP.NET MVC(C#) へのアーキテクチャリプレイス 宿泊スマートフォンサイトが抱えていた大きなボトルネック Time To First Byte レンダリングブロック 肥大化したJavaScript・CSS それぞれの課題に対する解決策とアプローチ まとめ リニューアル前後のパフォーマンス比較 PageSpeed Insights PageSpeed Insightsのスコア比較は以下の通りです。 サードパーティクッキーのキャッシュコントロール以外の項目は解消することができました。 before after Audits Metricの各指標が改善され、指摘事項も概ね解消できているのがわかると思います。 前提 Chrome 68(Lighthouse 3.0beta) Simulated Fast 3G, 4x CPU slowdown before after Calibre 一休ではWebフロントエンドパフォーマンスモニタリングSaasの Cailbre を使っています。 Time To First Byte をはじめとして、各指標が改善されているのがわかります。 before after パフォーマンス改善が必要だった理由とリニューアルの背景 Mobile-First Indexなどの理由はあると思いますが、 初期表示が遅かった 再検索のたびにリロードが走り、遅かった という点を改善し、ユーザに快適に使って欲しいというところがモチベーションとしてありました。 上記2点を解消する上で障壁となったのが ASP.NET Web Forms(VB.NET) によるレガシーアーキテクチャ 単純に非同期で読み込むことができないレンダリングブロックしている古のJavaScript 上書きに上書きを重ね、7,000行を越えていたCSS(メンテナンス困難) でした。 宿泊サイトは前回の大規模リプレイスから10年近くと長きに渡って価値を生んでいるサービスなのですが、上記の問題を抱えたままでパフォーマンスを改善し、より高い価値を提供することが難しくなってきたという背景がありました。 これを機会にスマートフォンホテルページのみ部分的に新しいアーキテクチャでリプレイスをしようという決断をしました。 ASP.NET Web Forms(VB.NET) → ASP.NET MVC(C#) へのアーキテクチャリプレイス 一休.comのほとんどはVB.NETでシステムが構築されています。 ASP.NET Web Formsベースの独自フレームワークです。 大規模リプレイスをしたのが2009年頃なので、宿泊サービスを10年弱支えてきてくれました。 ここまでのお話を聞いて頂ければ、色々と察してくださる方も多いと思います。 上述の通り、パフォーマンス面での性能・UX改善が難しくなってきたという背景から、 ASP.NET C# へのアーキテクチャリプレイスを一部実施しました。 過去の学びから大規模リプレイスはやめて、薄いコンポーネントだけ被せる、MVC の作法に則る形をとるようにしました。 ここは宿泊システムのレガシーアーキテクチャ改善を進めてくれた @shibayan に基盤を作ってもらいました。 補足:ASP.NET Core MVC (.NET Core)を選ばなかった理由 「なぜ、ASP.NET Core MVC (.NET Core)を選ばなかったのか?」と疑問に思われる方もいるかもしれませんので、補足しておきます。 既存のFramework/Componentsを使う必要があり、そこの互換性が.NET Core だと対応してなかった(主に認証に関わる部分) Web forms を捨てることにスコープを置いた これまでのアーキテクチャ改善によって DB アクセス周りや新しい処理は C# へのリプレイスが進んでいた プレゼンテーションレイヤーだけC#で書けない状況を打開することに注力した パフォーマンス改善の文脈とは別にアーキテクチャ改善に重きを置いていたプロジェクトが先行して走っていて、スマートフォンホテルページリニューアルがそのパイロットプロジェクトとなった という経緯があります。 宿泊スマートフォンサイトが抱えていた大きなボトルネック アーキテクチャリプレイスだけで、万事解決!!!というわけではなく、他にもパフォーマンス面で抱えていた課題がありました。 上記でも簡単に触れましたが、宿泊スマートフォンサイトが抱えていた大きなボトルネックは以下になります。 Time To First Byte 複雑なSQLをいろんなところから複数回呼び出すような処理が散乱していました。 このため、Time To First Byteが遅かったです。 また、ASP.NET Web Forms(VB.NET)の独自フレームワークでは非同期処理の実装が難しかったという側面もあり、これもTime To First Byte悪化の一因でした。 レンダリングブロック 古のJavaScriptに悩まされ、単純にdefer/asyncを使った非同期読み込みができず、Critical Request Chainが長くなり、ページスピードを下げる一因となっていました。 before/afterでのCritical Request Chainの長さを見れば、一目瞭然だと思います。 before after 肥大化したJavaScript・CSS 上述の通り、肥大化したJavaScript・7,000行越えのメンテナンス困難なCSSがあり、これもまたページスピードを悪化させている要因の1つとして大きかったです。 before:JavaScript転送量 ちなみに、画像転送量と同等のサイズでした。 after:JavaScript転送量 7000行越えのCSS それぞれの課題に対する解決策とアプローチ Time To First Byte ASP.NET MVC C# により、非同期処理の実装が容易にできたこと データ取得処理は、検索APIの一か所にまとめてasync/awaitで取得 検索APIの呼び出しもasync/await 鮮度を問わないデータはElasticaheでキャッシュ というところで、Time To First Byte改善につなげました。 詳しい話はサーバサイド編でお話しできればと思います。 レンダリングブロック ASP.NET MVC C# リプレイスに乗じて、古のJavaScriptたちも全て捨てました。 Vue.jsに寄せ、必要な箇所はコンポーネント化 async/deferで非同期読み込み bodyタグの末尾でJavaScriptの読み込み などでレンダリングブロックを解消し、Critical Request Chainを削減し、パフォーマンス改善ができました。 詳しくはJavaScriptパフォーマンス改善編でお話しできればと思います。 肥大化したJavaScript・CSS 上記に対しては JavaScript dynamic importによる初期ロード時のリソース削減 遅延レンダリングによるJavaScript実行時間の削減 CSS インライン展開 loadCSSを使った非同期読み込み FLOCSSを採用 不要なCSSセレクタの削除 を実施しました。 至極当然ですが、無駄なJavaScript・CSSをなくせたこともパフォーマンス面でけっこう大きいのかなと思いました。 詳しくは JavaScriptパフォーマンス改善編 CSS・その他細かいチューニング編 でお話しできればと思います。 まとめ 一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きました。 単純にレガシーシステムからリプレイスするだけでパフォーマンスが劇的に向上するわけではなく、ちゃんとボトルネックを理解した上で改善しなければ、同じ轍を踏みかねないということを学ぶことができたのは個人的に大きかったです。 また、今まで長い間頑張って価値を生み続けてくれた既存のシステムには感謝と敬意を払いながら、未来のために新システムに切り替えていきたいです。 今回、お話しした内容はあくまでも概要であり、サーバサイド・フロントエンドそれぞれの深いところのお話しは次回以降、お話しできればと思っていますので、ご期待ください。
アバター
一休で働くことに興味を持っていただくために、一休のサービスやビジネス、開発組織がどうなっているかを紹介する資料を公開しました。 もし興味を持っていただけたら、いつでも話を聞きに来てください! www.ikyu.co.jp おまけ 先日、弊社のCSSエンジニアの募集がちょっとだけ話題になっていました。 "CSSエンジニア"って求人初めて見た… 「近年のCSSをはじめとするWebフロントエンドの新しい技術、ツール、テクニックを前提とした次世代のWebデザインを提案・実現する役割」 "CSSエンジニア:株式会社一休:キャリア採用" https://t.co/l3HkXFcAs0 — azu (@azu_re) September 4, 2018 一休、フロントエンドとは別にCSSエンジニアとかPython分けてて、すてき。 https://t.co/DtIP3G7Nxv pic.twitter.com/CtCLsLndwQ — Iori / いおり@技術書典イラレ本 (@96usa_koi) September 5, 2018
アバター
レストラン事業部エンジニアの id:ninjinkun です。 一休レストランでは10年以上動いているシステムをPython 3で書かれた新システム(以下restaurant2)に順次移行する作業を進めています。現在では PC用のレストランページ や主要な API を含め、いくつかのページがrestaurant2で提供されるようになっている状態です。本記事ではこの移行の経緯と、restaurant2システムの詳細、Pythonを選んだ理由、現在の進捗状況をお伝えします。 経緯 一休レストランはサービスローンチ時よりClassic ASP(言語はVBScript)でシステムが構築されてきました(こちらに驚かれる方も多いと思いますが、歴史的経緯という言葉で強引にまとめて話を先に進めます)。このシステムは現在も一休レストランを支えているのですが、長年の改修による複雑性の増加、言語の古さ、言語機能の貧弱さなどにより、事業成長の足かせになっているという事実がありました。 そこで昨年よりrestaurant2プロジェクトが立ち上がり、昨年末に AMPページ を切り替えたのを皮切りに、移行が進んでいます。 restaurant2の概要 restaurant2の概要は以下の通りです。 Python 3.7 ・・・ typing, enum, data_classes, async/await (asyncio) など Python 3 の新機能を積極利用 フレームワークは Flask DDD の Layered Architecture / Clean Architecture を参考にした薄い階層アーキテクチャ Python 3 + mypy による type-hinting を使った型定義 flake8 + autopep8 によるコード規約 & 自動フォーマット nosetests w/ factory_boy でのユニットテスト + Circle CI API Blueprint による RESTful API の管理 / dredd によるドキュメントの自動テスト フロントエンドは BFF + nuxt + vue.js による Universal JavaScript + コンポーネント指向設計 実行環境は Docker + AWS Elastic Beanstalk (開発環境も Docker コンテナとして提供される) 構成図 Pythonの選定理由 なぜPythonを選択したのかという質問をよくいただくので、こちらで選定理由を説明します。 一休レストランではこれまでデザイナーがマークアップも行うスタイルで開発が行われてきました。また、一部開発が得意なデザイナーはサーバーサイドロジックにまで踏み込み実装を行っていました。デザイナーがエンジニアと一緒になって開発を行うフローは、コミュニケーションロスを大幅に減らしてくれます。これを維持し、デザイナーが引き続き開発に参加しやすいように、開発環境のセットアップ、実装、プレビューまでを簡単に行える環境が求められました。既存システムのClassic ASP環境ではVBScriptをHTMLに埋め込み可能な動的なテンプレートエンジンとして利用していたので *1 、同じようにテンプレートエンジンが使える動的言語がターゲットになりました。 この要件に当てはまり、かつ広く使われている言語としてはPerl, Ruby, PHP, Python, JS (node.js)あたりがターゲットになると思いますが、Pythonを選んだ理由としては以下の通りです。 言語仕様がシンプルで学習コストが低い 文法の自由度が低く、メンバー間の書き方の差異が減らせる flake8 や autopep8 によりコーディングをスタイルを自動的に統一することも可能 機械学習/AIのデファクトになっているため利用者が拡大しており、今後も言語の進化とエコシステムの高活性が見込める 動的言語でありながら型定義による事前検査が可能 他にも社内にPython経験者が複数人居たり、選定の当時機械学習の勉強会が社内で流行っていた影響でPython熱が高まっていた…などの背景もあります。 マイクロフレームワークの選定理由 また、一休レストランではPythonで書かれたマイクロフレームワークであるFlaskを選択しました。この背景としては、 ビッグリライトは行わず、既存のシステムと並行運用しながら段階的に移行して行く という意志決定があり、これにより以下の要求が発生したためです。 DBは既存システムと共用し、スキーマも既存のまま運用する しかし既存DBスキーマのテーブル名、カラム名がユビキタス言語的な命名になっていなかった 複数テーブルをjoinしないとEntityを表現できない場合があった このためrestaurant2ではDataMapperパターンを使い、DBのテーブル構造から独立したオブジェクトを作成、オブジェクト名とカラム名をユビキタス言語に変換している テーブル構造とオブジェクト構造が一致してしまうActiveRecordパターンのORMはこの方針には適さない ORMを自分たちで選べる薄いWAFが望ましかった 既存システムを開発しているメンバーも徐々にrestaurant2での開発に移行する キャッチアップのし易さを考えると、マジカルな部分が多い厚いWAFよりは単純なWAFの方が好ましい このような背景から、現在のrestaurant2の構成が決定されました。 現在の進捗 現在restaurant2により提供されているページは以下の通りです。 AMPページ PC用のレストランページ モバイル用の検索結果画面 上記ページの構築に必要な RESTful Web API restaurant2プロジェクトは一気に新システムに移行することは行わず、ビジネス上の優先順位に基づいて手を入れる必要が出たページから順次移行するスタイルを取っているため、入れ替わりにはそれなりに時間がかかる想定です。 とはいえもっと移行を加速したいので、Pythonでアプリケーションを書きたい方はぜひご応募お待ちしております! まとめ 一休レストランはClassic ASP + VBScriptからFlask + Pythonに移行しています 動的なスクリプト言語かつ人気がある言語としてPythonを選択しました 一緒にPythonで一休レストランを作りましょう! *1 : 個人的にはフレームワークを使わない素のPHPに似た印象を持っています
アバター
一休のSQL Server AWS移行事例(前編) からの続きです。 実施当日 kudoy: 準備段階で色々踏んだので、だいぶリハーサルもしましたし、当日は作業スケジュール組んで、その通りにやった感じです。バッチを事前に流したり… ninjinkun: バッチというのは何ですか? kudoy 宿泊とかレストランシステムなどで夜間に定時実行されているバッチ処理ですね。 ninjinkun: ああ集計系の。 kudoy そうですね。それを事前に流せるものは流して。 今回移行対象のDBが十数個あったんですが、各DBの環境上の都合で2通りの方法で移行する必要がありました。 1つ目の方法は、事前にオンプレ側で取得したフルバックアップを移行先となるAWS側にリストアしておく。切替までの間の差分データはトランザクションログを定期的バックアップして、それを使って移行先のAWS側も更新していく。大半のDBはこちらで対応できました。 2つ目の方法は、当日オンプレ側で完全にDBのデータ更新が終わってからフルバックアップを取得して、移行先のAWS側でリストアする。 いくつかのDBでこの対応が必要となりました。 リストア処理について ninjinkun: ちょっとそのリストアとそうではないものの違いがよく分からなかったんですが。 kudoy: データベースの復旧モデルと呼ばれる設定の違いによってDBリストアとなるのかトランザクションログのリストアになるのかが分かれました。復旧モデルの詳細については、 こちら 一休では移行前は、完全復旧モデルというトランザクションログが取得できるモデルと単純復旧モデルというトランザクションログが取得できないモデルの2つを用途別に採用していました。 多くのデータベースは、一般的に使用される完全復旧モデルを採用していましたが、1日に数回だけデータが更新される集計系テーブルなどやアプリケーションからは使用されない運用上で必要だけどフルバックアップを取得した時点まで戻せればよい性質のデータを扱っているデータベースについては、単純復旧モデルを採用していました。 こういった事情で事前にある程度移行できるものと当日に移行リストアしなければならないもの2パターンの移行手段が必要だったんです。 移行先のAWSにAlwaysOnをあらかじめ構築しておいて、データのみ差分更新でオンプレの環境と同期させておくといったことが出来れば、作業時間も短くて済むんですが、先ほど話した単純復旧モデルのデータベースがある、また今回移行するにあたって、SQLServerのバージョンも2012->2017へバージョンアップしたので、バージョンアップに対応する設定変更の必要もあったので、AlwaysOnの構築は当日行いました。 DBデータファイルの分割とインデックス再構築といった作業も移行時にやっておきたかったので、こういった作業も同日おこないました。 ninjinkun: めっちゃ大変そうですね kudoy: 細かい話をするとWindowsのクラスタ(WSFC)はDBリストアの影響を受けないので先に構築してあって、SQL ServerのAlwaysOnも全て当日構築すると時間が掛かるので、一度リハーサル時に構築して、移行直前に一部の設定だけ削除しておいて、DBリストア後から必要な設定だけしていくといった手順で作業時間を短縮しています。部分的な再構築するといったイメージですかね。 ninjinkun: リストアというのはMySQLで言うmysqldumpしたものを食わせるのと同じようなものなんでしょうか kudoy: うーん、まあそんなイメージ?バイナリなんですが。 ninjinkun: あーでは後から順次食わせるのではなく、テーブルごと一気に復旧してしまう感じなんですね。 kudoy: そうですね。DBのオプション設定やユーザ、テーブルとかDBに格納されているものを一気に復旧する感じです。 あとは、アプリケーション側でクラウド移行後の設定とかを変えてもらった部分があるので、それをリリースしてもらって、定期実行のスケジュールを止めていたバッチを裏側で動かして、問題無かったのでサイトオープン。 でもオープンしたらCPU使用率高いね、という(笑) akasakas: バッチを走らせまくってちょっと焦ってましたね。 kudoy: あーそれがありましたね。深夜作業対で走っていた破壊力のあるバッチを一度に動かしたので。 ninjinkun: じゃあCPU使用率はそのバッチが原因だったんですか? kudoy: それもあったんですが、バッチが終わってもまだCPU使用率は高かったです。 ninjinkun: そこからパフォーマンスチューニングみたいな話になったんですよね。 kudoy: インデックスいっぱい作ってもらったりとか、アプリケーション側も直してもらったりとか。 akasakas: 当日朝からお祭り感がありましたね。 kudoy: でも思ったほどではなかった。それまでもオンプレで運用している段階でDBの負荷は高くなってきていて、ちょくちょくアラートが鳴る頻度は高くなってきていたので。 そういう意味では結構移行はギリギリのタイミングでした。あのままオンプレ続けてたら、手の打ちようがなくなっていた。 akasakas: あのままオンプレ続けていたら、この夏はもう… kudoy: ちょっと厳しかった。 当日起こったとしたら嫌だったこと ninjinkun: こういうのが起こってら嫌だったなというのありますか? kudoy: やっぱりリリースした後にあれこれ動かない系。事前に検証して貰っているので、そんなに心配しては居なかったです。でもフルの構成では検証し切れていないので、そこで何か出るのは嫌だなと思っていました。 あとは想定していてちょっと嫌だなと思ったことは、結局ハードを自分たちで管理できていないところなので、AWSに移行してしまった後は、転送速度が遅いとかあると今までと勝手が違うので嫌だなと思っていました。 ninjinkun: それは今回は杞憂だったということですか。 kudoy: そうですね。思ってたより逆に早かった。日中に試験しているときは転送速度遅いなと思っていたんですが、夜中に試したことはそれまでなくて。当日は夜中で帯域が空いている?のか早かったですね。 akasakas: 当日もちょっと巻いてましたよね。 kudoy: EBS(io1)のIOPS上げたおかげもあると思うんですが、深夜という時間帯による影響もあったのかなと感じています。 準備段階で重点的にやったこと ninjinkun: この辺が怖かったので重点的にやった、みたいなのはありますか? kudoy: それはやっぱり時間の部分ですね。これはリハーサルを相当やったので。10回くらいはやってます。 ninjinkun: リハーサルも最初はうまく行かなかったりしたんですか? kudoy: 作業時間以外はうまくいかないといったことはないですね。1番の問題が作業時間が長いといったところだったので、各手順単位で作業(処理)時間を計測して、時間が掛かる作業を抽出して短縮する方法を検討/検証して時間を計測するといった繰り返して少しづつ時間を短縮していきました。 あとは机上で手順や作業を上手く組み替えて、何度もリハして何とか収まるようにした部分が一番苦労した部分ですかね。12時間かかりますとか言えないので(笑) ninjinkun: 今回の作業はある程度リスクを取って実行していると思うのですが、やはり何度もリハーサルしたのが良かったんですかね。 kudoy: いろんな検証を繰り返しやったので、その辺りは安心感に繋がってます。何か起きてもなんとかなるだろうと。アプリケーション で何か起きても心強いアプリの担当エンジニアがなんとかしてくるだろうと(笑)負荷試験も元々は1週間の予定だったんですが、見直して3週間くらいやっていました。 akasakas: 負荷試験は JMeter を使ってコントローラーを1台置いて、slaveを15台置いて一度に投げたりしていました。実際のリクエストからパラメータを生成して主要な導線や負荷が高いAPIをピックアップして夏のピークの負荷x1.5くらいのリクエストを投げて試験しました。 kudoy: 世の中的に同じようにAWSでやっている事例が少なかったというのがありますね。同じようなのは日本だとH.I.S.さんくらいしか居ないですね。H.I.S.さんについては 事例 が出ています。 ninjinkun: やっぱりSQL Serverでクラウドだと普通はAzureになるんですかね? kudoy: うーん、Microsoftの製品ですし親和性は高そうですよね。でもAzureは今回は見送りました。 決めるときはAWS、GCP、Azureで比較しました。GCPはその当時まだ追いついてなかったので外して。 結局AWSとAzureともDBがSQL Serverであることが引っかかっていて、それを安定的かつお金的にも最適なと考えるとAzureはちょっとバランスが悪かったんですよね。インスタンスを立ててそれを分けるという構成だと。そもそもPaaSは制限が多すぎて両方とも使えなくて。 ninjinkun: なるほど、フルマネージドは無理だねと。 kudoy: そう、それは無理でインスタンスでとなったときにIOPSの調整がAWSの方が柔軟だった。そこの決定的な違いは、AWSだとディスクのサイズがある程度あれば2万IOPSまで上げられるんです。500GBでも2万IOPSとかできる。Azureの場合は容量によって性能が決まっているので、IOPSを上げると容量全然使ってないのに使っていない部分に課金が発生してしまうので、これはクラウドのうまみがないよねという話になりました。 じゃあそうなるとAWSもWebサービスの事例自体はいっぱいあるし、こっちかなと。 ninjinkun: なるほど、サービス特性に合わせられる柔軟性と事例の豊富さでAWSになったんですね。 以上で聞きたかった話はだいたい聞けたと思います。ありがとうございました。 すごく大変な移行だったとだけ聞いていて、詳細を知らなかったので、今回は勉強になりました。
アバター
一休.comではオンプレミスで動かしていたサーバー群をAWSに移行する作業を2016年末から2年がかりで進めてきました(以下、クラウド移行と呼びます)。2017年7月にまず全サービスのアプリケーションサーバー移行が完了、2018年2月にデータベースであるSQL Serverの移行が終わり、クラウド移行が完了しました。 一休のシステムは予約や決済などのミッションクリティカルな基幹業務を担ってるため、大規模なシステム移行は難易度が高い仕事でした。また、一休のサービスは予約を取り扱うECの一種であるため、トランザクション機能をはじめとする、DBに対する品質要求水準や機能要件も比較的厳しい環境です。 本記事ではこのクラウド移行の中でも特に難易度が高いDBの移行を牽引したkudoyに、移行計画の設計や勘所についてインタビューします。 インタビュイー kudoy デジタルマーケティング部エンジニア(写真右) インタビュワー ninjinkun レストラン事業部エンジニア(写真左) akasakas 宿泊事業部エンジニア(撮影のため写真なし🙏) ninjinkun: 私は今回のDB移行やSQL Serverについてほぼ何も知らないので、今日は私のような素人にもわかるようにお願いします。 kudoy: わかりました(笑) 今回のAWS移行で解決したかった問題は3つあって、 ハードウェアを属人的な運用で管理している ハードウェアの調達に時間がかかる ハードウェア製品のライフサイクル(製品寿命)に合わせて都度移行作業が発生する という感じです。 ninjinkun: クラウド移行前はハードウェア調達にどれくらい時間がかかっていたのでしょうか。 kudoy: ハードウェアの調達は選定から始まって急いでも一ヶ月、セットアップから投入まででトータル二ヶ月くらい時間がかかっていました。 DB移行の時期がアプリケーションサーバより遅れた理由 ninjinkun: 2017年の夏にはアプリケーションサーバーの移行は完了していたわけですが、DB移行が後になったのはどういった理由だったのでしょうか? kudoy: 一休のサービスに必要な機能がSQL Server 2017でないと使えなかったためです。具体的には、一休ではホテルやレストランの予約の際に宿泊データのDBとユーザーデータのDBで異なるDBをまたいだトランザクションを使用しているのですが、このための機能がSQL Server 2016にはありませんでした。 ninjinkun: でも一休のサービスはこれまでも同じように異なるDBにまたがるトランザクションを利用して動いていたわけですよね。なぜ今回のクラウド移行で新たな機能が必要になったのでしょうか? kudoy: これまでは、DBサーバの冗長化を ftServer と呼ばれる全ての部品が2重化されているハードウェア側で行っていました。このハードウェアは、OSやSQLServerからは1台のサーバとして扱えることで、複数DB間のトランザクションに関する制約に該当しなかったんです。しかしクラウド環境でも同じ可用性を確保するためには、 AlwaysOn という複数のインスタンスでMaster DBを冗長化する機能を使う必要がありました。その結果、インスタンスを超えてトランザクションを使う分散トランザクション *1 が必要になったのです。 ninjinkun: なるほど、クラウド移行でハードウェアではなくサーバー構成で冗長化する必要が出てきて、そのためにインスタンスを分けたから分散トランザクションが必要になったんですね。 kudoy: SQL Server 2017でAlwaysOnでのDBまたぎの分散トランザクションがサポートされることがわかっていたので、アプリサーバーの移行を先に行い、SQL Server 2017のリリースを待っていました。 この時期は、AWS Direct ConnectでAWSに移行したのアプリケーションサーバとオンプレのDBサーバを専用線で繋いでいました。 ninjinkun: 他にスケジュールに影響を与えた要因はあるのでしょうか? kudoy: 実は現在のハードウェアのキャパシティでは2018年中にはトラフィックを支えきれなくなる見込みで、かつ2017年の時点でデータセンターも2018年3月で解約することになっていたので、もうこの時期にやるしかなかったんです。 DB移行の準備がスタート kudoy: 2017年の10月にSQL Server 2017が出てきたので、そこからようやく本当のスタートでした。まずSQL Server 2017でできるようになったことを調べて、それから環境を作るためにAWS側のネットワークの設計や構築、DBの設定、セキュリティ、Microsoft ADの設定、運用周りの設計だとかを並行してやっていきました。 ninjinkun: 実際に動かして試したりもしたんですか? kudoy: 動かしていましたね。でもテスト環境なので、最小構成で作ってしまっていて、いざ本番構成を作るときに見えていなかった考慮すべき点が出てきてしまったんです。 ninjinkun: 具体的にはどんな問題があったんでしょうか。 kudoy: ネットワーク構成の問題ですね。 Multi-AZで、それぞれのAZにデータベース用のサブネットを用意して、AlwaysOnを4台構成(+1台ファイル共有マジョリティ用ノード)にして、2台+2台で配置する想定でいましたが、2台構成だと上手くいくけど、4台構成にするとうまくいかない。事例を調べても2台構成はあるけど、4台構成の情報が見当たらなくて。。。。 ninjinkun: MySQLでの事例はあったんでしょうか? kudoy: 選択する仕組み自体が違ってくると思いますが、MySQLだとレプリケーションを使用するかと思うので、EC2起動時に割り当てられたマスター/スレーブの1つのIPを指定するのではないでしょうか。 EC2でAlwaysOnを構成する場合、まずWSFCというWindowsクラスタが組まれていることが前提条件になります。構築するに当たり1つのENIに EC2(OS)用 WSFC用 SQLServer用 の3つのIPを指定する必要があるのですが、想定していた2つのサブネットにそれぞれ2台づつ配置する4台構成にしてAlwaysOnを構築するとWSFC用IPとSQLServer用IPが上手く認識できなくてWindowsクラスタが正しく動作していませんでした。 どうにも解決策が見当たらなくて、AWSサポートと相談してたところ、WSFCで1 つのサブネットに複数のクラスターノードを配置する方法は、 AWS ではサポートされていないことがわかったんです。そこで、以下の図のようにサブネットを分ければ解決することがわかりました。 一休.comのAlwaysOn構成 この構成がオープンになっている事例は探してみたところ見当たらないので、図だけでも面白いかもしれません。そもそもAWSでSQL Serverを運用している事例自体が少ないですが。 ninjinkun: 元々は2つのサブネットで行けるはずが、4つのサブネットが誕生してしまったんですね 機能検証 kudoy: その後、まずは機能検証用に今のアプリケーションをSQL Server 2017に変えただけで動くのかを検証しました。小さな環境を作って、E2Eテストや直接アクセスしてもらって検証したのですが、その時点でアプリケーションはほとんど問題無く動いていました。 それが終わった後に負荷試験用に本番と同じくらいのサイズのインスタンスを立ててパフォーマンスの検証をやってもらった感じです。 機能検証、パフォーマンス検証がクリア出来たところで、DB移行のリハーサルを始めました。 データ転送の時間や、AlwaysOnを構築したあとのプライマリインスタンスからレプリカインスタンスへのDB同期させる時間はすごく長くかつブレがありました。サイトのメンテナンス時間をユーザーやパートナーに告知しないといけないので、短くする方法を模索していました。 24:30から8:00で止めると告知しました。 まあ告知した時点ではリハーサル時間は全然収まりそうもなかったんですが(笑) 一同: (苦笑) ninjinkun: でもビジネス的にはそれ以上止められないということで、時間が決まったんですよね。 kudoy: 10時間超えるとか言ったらさすがに…となりますからね(笑) akasakas: でも最初のリハーサル12時間とかじゃなかったでしたっけ? kudoy: 最初はそんな感じでしたね。でも色々、検討&検証してみたら何とかなりました。 ninjinkun: それは事前にできるところをやっておくとかでしょうか。 kudoy: そこも含めて、手順を簡略化したりとか、順番を入れ替えたりして。 EC2インスタンスタイプの選択も重要でした。当初R4系を使おうと思っていましたが、最終的にはI3系を選択しました。選択した大きな理由としては、I3系には永続的なデータの保存には使えませんが、インスタンスストレージとして、NVMeのストレージが付いていることでした。 インスタンスストレージは、インスタンスを完全に停止してしまうとデータが消えてしまいますが、起動している間は追加料金なしで使用できるすごく高速なストレージです。移行時には、データ転送やDB再構成、移行後もスナップショット的なDBバックアップなど、一時的な作業で且つ出来るだけ処理時間を短くしたいという用途に向いていました。このNVMeのストレージを上手く活用することで、AlwaysOnを構築する際に必要なDBバックアップとリストアの処理時間を短縮することができました。 あとはストレージ(EBS)のパフォーマンスを調整できるので、IOPS(ストレージのパフォーマンス)を一時的に上げて、そこでパフォーマンスが出たので机上の計算よりは速く収まりました。 ninjinkun: 結局一番効いたのは何でしたか? kudoy: NVMeストレージの活用と、IOPSの調整は効いてる思います。 ninjinkun: 環境構築してリハーサルも行って、見積もりも出ましたと。後は… kudoy: それと並行して開発系DBの移行の準備も進めていました。 移行に関しては丸々サイト止めてバックアップ取ってからデータをそのまま持っていくとすごく時間がかかるので、事前にフルバックアップを前日に取ってそれを先に送って、当日は差分のログだけを流すようにしました。できるところは事前にと言うのは、そういうところですね。 ninjinkun: 移行が11月からスタートして2月の中旬でしたっけ、実質4ヶ月弱でここまで来ているわけですね。 akasakas: 結構調整が大変でしたよね、移行当日のスケジューリングとか。 kudoy: (ユーザー向けの)キャンペーンがちょくちょくあって実施可能な日が割と少なかったりとか…。 準備で一番時間がかかったところ ninjinkun: ちなみに、一番準備段階で時間がかかったのはどこですか? kudoy: 負荷試験じゃないですかね。負荷試験の環境を作るのもそうだし、さっき言った通り最小構成でテスト環境作っていたので、ネットワーク環境の作り方を調べるのに時間がかかって。 あとはすぐにインスタンスが確保できなかったという問題がありました。AWSの制約上、大きいインスタンスだと最初は2個しか確保できず、4台使いたいと思って制限解除の申請したら2日3日掛かって、IOPSの上限を上げるのも申請制でした。 ninjinkun: それは間違って契約するのを防ぐためなんですかね。 kudoy: たぶん意図しない課金を防ぐためと、あと制限なしにいきなり大きなリソースを沢山のアカウントから確保されるとAWS側もリソース供給出来なくなってしまいますよね。EBS(io1)のIOPS上限の緩和は時間が掛かるケースもあって一週間くらいかかったりすることもありました。 ninjinkun: クラウドとは…という気持ちになりますね。 kudoy: 本番用のインスタンスも料金節約のため直前に用意するつもりだったのですが、AWS側のリソース提供状況によってはインスタンスが起動できないことがあることがわかったので、早いけど一週間前にはインスタンスを確保する為に起動しておこうと。 クラウドの怖いところを直前になって思い知らされました。 ninjinkun: 直前だと意外と調達できないという… 後編に続く 後編はDB移行の当日作業や、なぜAWSを選んだのかなどについて話します! *1 : 複数のデータソース間でトランザクションの整合性を保証する技術。分散トランザクション自体はSQL Server 2016でも利用できたが、AlwaysOn可用性グループで利用できない制限があった。 参考
アバター
こんにちは。 一休.com の開発基盤を担当しています、akasakas です。 今回は、画像最適化配信サービスである imgix を 宿泊 ・ レストラン サイトに導入して、 画像最適化・サイトスピード改善につなげたお話をしたいと思います。 ここでお話しする内容 サイトスピードという観点での一休が抱えていた課題(の一部) imgixの特徴とそこでできる解決策 imgix導入の効果 imgix導入をする上で大変だったこと これから画像最適化を考える人たちへ まとめと感想 おまけ(与太話) 諸注意 imgixを導入して、画像最適化という面でサイトスピード改善につながりましたが、 サイトスピードという観点で一休が抱えている課題はまだまだあります。 imgixを導入すれば、サイトスピードは万事解決!!!という話ではありませんので、悪しからず。 サイトスピードという観点での一休が抱えていた課題(の一部) 画像最適化ができてなかったため、サイトスピードが遅かった 画像の最適化ができてなかったことが大きな原因の一つでした。 一休というサイトの特性上、ホテル・旅館・レストランの画像を綺麗に見せたいというのが前提としてあります。 ホテル・旅館・レストランの高級感をユーザーに伝えたいため、画像サイズが大きい&高品質な画像を取り扱っています。 サイズ・転送量の大きい画像を扱っていることがサイトスピード低下の大きな原因となっていました。 無駄なリソースの削除をして、画像最適化・サイトスピード改善につなげたかったというのが背景としてあります。 今まで画像最適化ができてなかった結果、サイトスピードが遅くなり、ユーザーにご不便をかけていたのを改善して、より快適に使ってもらいたいという思いから画像最適化プロジェクトが始まりました。 imgix導入前のPageSpeed Insightsのスコア imgix導入前のPageSpeed Insightsのスコアが以下になります。 宿泊 [補足] 宿泊のキャプチャについては2018/01/19時点のもので、厳密にいうと画像最適化だけでなく、他にもいろんなボトルネックがありましたが、スコアが低い大きな原因は画像最適化の部分でした。 レストラン imgixの特徴とそこでできる解決策 上記の課題を解決するために imgix を導入しました。 imgixは画像最適化に特化したCDNサービスというイメージです。 国内でimgixの導入企業として有名なのが、 日本経済新聞社 さんですね。 個人的に思うimgixの特徴としては以下が大きいと思います。 拡張子自動判別 自動圧縮設定 ロスレス圧縮 ストレージサービス連携(Amazon S3/Google Cloud Storage) imgixの API Reference などに詳細が記載されていますが、こちらでも簡単に触れてみます。 拡張子自動判別 imgixの画像URLパラメータに auto=format でjpegの画像もブラウザによってwebpなどに変換してくれます 自動圧縮設定 imgixの画像URLパラメータに auto=compress で自動で圧縮してくれます ロスレス圧縮 ロスレス圧縮が可能な画像に対して、 lossless=0 とすれば、ロスレス圧縮されます lossless=1 ではないというのが地味に罠でした。 ストレージサービス連携(Amazon S3/Google Cloud Storage) 画像の移行をする上で、これが大変ありがたかったです。 imgixではストレージサービスを指定するだけで画像のの最適化・配信ができます。 一休ではAmazon S3に画像コンテンツを格納しているので、 S3バケットの指定と、Access Key ID/Secret Access Keyを設定すれば、 imgixから画像が参照できるというところまでできました。 既存資産(一休でいうS3)をそのまま使えたので画像移行がスムーズに行うことができました。 before:S3バケットをみるCDNサービスを after:imgixに切り替えたというイメージです URLの変更はありましたが、そこは気合いと根性でアプリケーション側を直しました。 ストレージサービス連携はS3だけでなく、Google Cloud Storageにも対応しています。 imgix導入の効果 PageSpeed Insightsのスコアは宿泊・レストラン共に大きく改善されたのがわかります。 宿泊 before(再掲) after beforeのキャプチャについては2018/01/19時点のもので、厳密にいうと画像最適化だけでなく、他にもいろんなボトルネックがありましたが、スコアが低い大きな原因は画像最適化の部分でした。imgixの導入で20〜30点ほどスコアが改善されました。 レストラン before(再掲) after 画像転送量 ある画面でimgix導入前後の画像転送量比較が以下になります。 before(imgix導入前) after(imgix導入後) 10MB近く削減されています。 imgix導入前は全く最適化できてませんでした。 そもそも10MB越えの画面はどうなんだ?という疑問もありますが。 上記の例が最も画像転送量を削減できましたが、他の画面でも軒並み画像転送量を抑えることができたので、導入の効果は非常に高かったと感じます。 imgix導入をする上で大変だったこと 技術面 既存の資材(S3)を使えたので、切り替えが楽だった URLの変更はあったけど、そこは気合いと根性で直した ので、正直なところ技術面で大変だったことはなかったです。 技術面以外 実はこっちの方が大変でした 英語でサポートと契約などのやり取り imgixは日本の代理店がありません。 最初にサポートに確認したら「ない!!!」って言われて、そのメールをそっ閉じしたのは今思えば、懐かしいです。 英語が堪能な帰国子女さんのお力を借りて、なんとかVolume Pricingの交渉ができました。 料金見積もり 見積もりが難しかったです。 実際に1ヶ月フリーで使わせてくれて、そこで正確な見積もりをとりました。 imgixのサポートは丁寧で優しかったです。 導入の承認 CDN費用のコストカット+技術的な改善ができることを説明して、導入の承認を得ることができました。 これから画像最適化を考える人たちへ 実際にimgixを導入しましたが、自分の進め方がよくなかったと思う部分が多々ありました。 反省/振り返りを含めて、どうやって導入を進めればよかったかというのを考えてみました。 あまりテッキーな話ではないです。仕事の進め方的な話がメインです。 ポイント imgixに限らず、導入効果とコストカットの説明ができれば、導入できると思いました。 技術選定 候補としては以下の3つがあげられるのかなと思います imgix Cloudinary Fastly Image Optimizer 技術比較 一休の場合、画像最適化をポイントとしました。 imgix や Cloudinary の方が画像最適化を主眼に置いたサービスであるため使いやすい印象を覚えました。 移行難度 既存資産(S3や一休の画像配信構成)をそのまま使えるという点でimgixの方が移行をスムーズに進めることができるという印象がありました。 なので、この時点でほぼほぼimgixに決めてました 移行コスト 正直、imgixの見積もりしかとってなかったです。 理想は3つそれぞれ見積もりを出して、比較するべきだったというのが反省点です。 imgixの導入コストが既存より安いor同等であればOKかなと思ってました。 技術的改善が見込めれば、導入前後のコストが同等でも問題ないだろうという考えがあったからです。 その他細かい話 あと細かいところで 特定画面だけimgixを部分導入して、詳細なBefore/Afterの効果比較を実施し CDN費用のコストカット+技術的な改善(画像最適化)ができることを説明して、導入の承認を得ることができました。 まとめと感想 imgixを導入して、画像最適化&サイトスピードを改善ができました。 モダンなインフラに乗っかると、割と簡単にベストプラクティスを実践できるのだなというのが正直な感想です。 既存の環境でチューニングをがんばっていた諸氏には感謝をしつつ、未来のためにはどんどん新システムに塗り替えていきたいですね。 これから画像最適化を考えているエンジニアの方々にとって、この記事が少しでも参考になれば幸いです。 諸注意をもう一度 imgixを導入して、画像最適化という面でサイトスピード改善につながりましたが、 サイトスピードという観点で一休が抱えている課題はまだまだあります。 imgixを導入すれば、サイトスピードは万事解決!!!という話ではありませんので、悪しからず。 おまけ レストランの導入が想定よりも遥かに速かった 当初は宿泊のみ先行して対応するという話で進めてて、レストランの対応は冬ぐらいになる はずでしたが、 やっぱりモダンな技術とその効果が気になって、飛びついたご様子です。 技術調査・契約・見積もりなどのめんどくさいことをやって、慎重に事を進めてきた自分としては おいしいところだけをかっさらったレストランのエンジニアをみて、イラッときたのは内緒です。 でも、レストランのサービスが改善できたのでOKです 最後に imgix移行に協力して頂いた方々に感謝 インフラ面で協力してくれたエンジニア 英語のやりとりが発生した中で、サポートして頂いた帰国子女の営業さん 画像の変更で作業ワークフロー上、大きな影響を受けたにも関わらず、ご理解とご協力を頂いたデザインさん この場を借りて、御礼申し上げます。 参考 imgix 日経電子版を速くする
アバター
一休のデータサイエンス部に所属しています小島です。 以前データ分析基盤の構築で記事を上げていましたが、今回はETL *1 周りの話をしようと思います。 user-first.ikyu.co.jp 今回ETLのツールとして導入したのはAirflowというツールです。 2017年のアドベントカレンダーでも紹介させていただきました。 一休のデータフローをAirflowを使って実行してみる 一休のETLの現状について 一休のETL周りは以下の画像のようになっていました。 課題 ETLの処理時間が伸びた(出社後も処理が続いていた) エラーのリカバリ作業に時間がかかる(ログが確認しにくい, サーバーに入って作業しなければいけない) 複雑な依存関係の定義がしにくい(どれとどれが依存しているかわからない) リソース負荷(全て並列で実行していた) 処理毎のボトルネックが把握できない ツールの問題というよりは正しくツールを使えていないことが原因でしたが、上記の理由でDigdagをAirflowに移行することを決定しました。 なぜAirflowにしたか 処理が複雑にかける 管理画面の依存関係のグラフと、tree形式での表示がわかりやすい Pythonで記述できるので、機械学習などと相性がいい 管理画面から簡単に再実行できる Airflowの導入した結果できるようになったこと ETLの時間が把握できるので、ボトルネックとなる処理に関して対処が可能になった 再実行が管理画面から簡単にできるので、リカバリーが簡単になり運用に時間がかからなくなった 並列数などを簡単に設定できるので、負荷が少なくなった 依存関係がとても簡単に定義できる ボトルネックとなる処理の検知 めちゃめちゃ簡単にわかるようになりました。 対象日付のリカバリーが管理画面から簡単にできる 導入に当たって苦労したこと ETL処理は意外と追加や変更が多いので開発しやすい環境を作ること Airflowの制約があったこと ETL処理は意外と追加や変更が多いので開発しやすい環境を作ること ETLで肝なのはデータが正確にDWHに格納できるかというところです。 なので開発環境を作る必要があります。 ただ、開発環境を作成する際に考えなければいけないのがアウトプットするDBについてです。 インプットに関しては参照するだけなので、あまり考慮しなくていいのですが アウトプット用のDBをどのように用意するかを考え、結果的に開発環境ではdockerコンテナを立て件数を自動的に絞ることで開発ができるようにしています。 BigQueryや、S3などについてはコンテナを作れないので環境ごとにbucketを変更することで開発できるようにしています。 Airflowの制約があったこと Airflowは1.9.0を使用していますが、内部的に全てUTCで動いています。 利用としてはこちら記事にわかりやすく書いてありましたが、タイムゾーンに依存しないように内部のシステムは全てUTCを使用するようにしているらしいです。 [AIRFLOW-289] Use datetime.utcnow() to keep airflow system independent - ASF JIRA なので一休では表示側はUTCで処理部分は明示的にJST => UTCに変換しています。 今後ETLツールの利用方法 レコメンドなどに使用しているモデルなどについて機械学習の学習部分を自動で行なっていくことと、 ボトルネックになっている処理を改善し、ETLの最適化を図るようにしていこうと思います。 *1 : Extract、Transform、Loadの略で、企業内に存在する複数のシステムからデータを抽出し、抽出したデータを変換/加工した上でデータウェアハウス等へ渡す処理
アバター
こんにちは。一休.com の宿泊開発基盤のお手伝いをしている id:shiba-yan です。 先週の話になるのですが、SendGrid の日本国内代理店の方から「一休.com での SendGrid を利用した運用について事例を話してほしい」とお願いされたので、簡単に出したが話してきました。 当日は SendGrid 本社から技術系の偉い人が来日して、世界初の話を含めていろいろと話してくれました。 様々な話がありましたが、中でも Geopod *1 が東京にもあることは知りませんでした。昔はシンガポールを使っていたみたいですが、今は日本からでも低レイテンシーで送信できるようになっているようでした。 おそらく開催ブログは公開されると思いますが、先にスライドを公開しておきます。メインは 2017 年に発生した、割と大規模な SendGrid 障害時の話です。 一休.com ではメールが非常に重要な役割を果たしているので、多少の送信エラーが発生したとしても、出来るだけリカバリーが行えるように実装しています。 しかし、障害発生時にはいろいろな問題が発覚したため、反省しつつ対応を行っていきました。 割と当たり前のことが出来てなかったと今では思いますが、当たり前を当たり前に行うのは非常に難しいことだと再認識し、今後の開発にも繋げていきたいと考えています。 参考 *1 : SendGrid が世界中に用意しているエンドポイントの一つ
アバター