一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

一休.comスマホサイトのパフォーマンス改善(サーバサイドとQAとリリース編)

こんにちは。 一休.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でキャッシュ
改善前 改善後
f:id:akasakas:20180910203004p:plain f:id:akasakas:20180910203015p:plain
  • 検索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文を実行し、実行計画を確認すると不足インデックスをサジェストしてくれます。↓のように。

f:id:akasakas:20180910203027p:plain

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ブランチが正解の挙動です。
そこで、以下のようなスクリプトを作り自動テストを行いました。

  1. 本番のfastlyのアクセスログからリクエストパスを抽出する。
  2. 本番相当のデータが入っているデータベースにつないだmasterブランチとテスト対象のブランチに対して、chromelessで1. のリクエストパスでリクエスト。両者のレスポンスから表示情報を抽出。
  3. 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スマホサイトのパフォーマンス改善(CSS・その他細かいチューニング編)

こんにちは。 一休.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

上書きに上書きを重ねているので、メンテナンスが大変でした。

f:id:akasakas:20180902204612p:plain:w300

どのスタイルがどこで使われているか不明

ネストが深く、命名ルールもなかったので、どのスタイルがどこで使われているのかがわかりづらかったです。

.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ファイルが呼ばれているなんてこともありました。

レイアウトは下記のようなイメージです。

f:id:akasakas:20180910191958p:plain

一例ですが、
divタグ内でstylesheetが呼ばれているということもありました。

f:id:akasakas:20180907234134p:plain:w500

リニューアルをするにあたり、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 を使って、非同期で読み込むようにしました。

ドキュメント整備

今後の色んなエンジニア・デザイナーがメンテナンスするためにドキュメントを整備しました。 書いたこととしては

  • 設計方針
  • ディレクトリ構成
  • 命名ルール
  • パフォーマンス
  • アンチパターン

です。

f:id:akasakas:20180920185039p:plain:w500

その他細かいチューニング編

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

参考資料

一休.comスマホサイトのパフォーマンス改善(JavaScript編)

宿泊事業本部の宇都宮です。

一休.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-showv-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

f:id:ryo-utsunomiya:20180918171234p:plain

After

f:id:ryo-utsunomiya:20180918171256p:plain

転送量(Fully Loaded > Bytes in)が300KB近く減り、Speed Indexは1000以上改善しました。

パフォーマンス監視の導入

パフォーマンスは放っておくと劣化していくので、Webサイトの速度を維持し続ける仕組みが必要です。ここでは、パフォーマンス予算(Performance Budget)の設定と、監視を行っています。具体的には、パフォーマンス監視SaaSのCalibreを使用し、予算を超過した際にアラートがSlackの開発者向けチャンネルに流れるようにしました。

f:id:ryo-utsunomiya:20180919104010p:plain

今後の展望

ホテルページについては一定のパフォーマンス改善を実現することができましたが、サイト全体としてはまだまだ伸びしろがあると考えています。スムーズに宿泊施設を探すには、ページ単体ではなく、検索導線(トップ・リスト・ホテル)の全体的な回遊性が重要です。パフォーマンス改善も含め、引き続きUI/UXの改善を行っていきたいと考えています。

一休では、UI/UXの改善に熱意のあるエンジニアを募集しています!

hrmos.co

一休.comスマホサイトのパフォーマンス改善(概要編)

こんにちは。 一休.comの開発基盤を担当しています、akasakasです。

今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きます。

UI部分は既存を踏襲する形をとり、UX・パフォーマンス改善にフォーカスして、所々で様々な工夫をしました。

お話ししたいことが盛りだくさんなので

  • 概要編
  • JavaScriptパフォーマンス改善編
  • CSS・その他細かいチューニング編
  • サーバサイド編

の4つに分けて、お送りしたいと思います。

この記事ではスマートフォンホテルページリニューアルの全体像についてお話しします。

詳しいお話をする前に:スマートフォンホテルページってどこ?

こちらになります

https://www.ikyu.com/sd/00001290/

f:id:akasakas:20180902185134p:plain:w300

ここでお話しする内容

  • リニューアル前後のパフォーマンス比較
    • 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

f:id:akasakas:20180902185650p:plain:w400

after

f:id:akasakas:20180918174337p:plain:w400

Audits

Metricの各指標が改善され、指摘事項も概ね解消できているのがわかると思います。

前提

  • Chrome 68(Lighthouse 3.0beta)
  • Simulated Fast 3G, 4x CPU slowdown

before

f:id:akasakas:20180903103305p:plain:w600

after

f:id:akasakas:20180903103234p:plain:w600

Calibre

一休ではWebフロントエンドパフォーマンスモニタリングSaasの Cailbre を使っています。
Time To First Byte をはじめとして、各指標が改善されているのがわかります。

before

f:id:akasakas:20180902191714p:plain:w400

after

f:id:akasakas:20180902191725p:plain:w400

パフォーマンス改善が必要だった理由とリニューアルの背景

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

f:id:akasakas:20180902203526p:plain:w400

after

f:id:akasakas:20180902203536p:plain:w400

肥大化したJavaScript・CSS

上述の通り、肥大化したJavaScript・7,000行越えのメンテナンス困難なCSSがあり、これもまたページスピードを悪化させている要因の1つとして大きかったです。

before:JavaScript転送量

ちなみに、画像転送量と同等のサイズでした。

f:id:akasakas:20180902204042p:plain:w300

after:JavaScript転送量

f:id:akasakas:20180902204052p:plain:w300

7000行越えのCSS

f:id:akasakas:20180902204612p:plain:w300

それぞれの課題に対する解決策とアプローチ

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エンジニアの募集がちょっとだけ話題になっていました。

一休レストランPython移行の進捗

レストラン事業部エンジニアの 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 コンテナとして提供される)

構成図 f:id:ninjinkun:20180810165928p:plain

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により提供されているページは以下の通りです。

restaurant2プロジェクトは一気に新システムに移行することは行わず、ビジネス上の優先順位に基づいて手を入れる必要が出たページから順次移行するスタイルを取っているため、入れ替わりにはそれなりに時間がかかる想定です。

とはいえもっと移行を加速したいので、Pythonでアプリケーションを書きたい方はぜひご応募お待ちしております!

まとめ

  • 一休レストランはClassic ASP + VBScriptからFlask + Pythonに移行しています
  • 動的なスクリプト言語かつ人気がある言語としてPythonを選択しました
  • 一緒にPythonで一休レストランを作りましょう!

*1:個人的にはフレームワークを使わない素のPHPに似た印象を持っています

一休のSQL Server AWS移行事例(後編)

一休のSQL Server AWS移行事例(前編)からの続きです。

f:id:ninjinkun:20180718110152j:plain

実施当日

kudoy:
準備段階で色々踏んだので、だいぶリハーサルもしましたし、当日は作業スケジュール組んで、その通りにやった感じです。バッチを事前に流したり…

ninjinkun:
バッチというのは何ですか?

kudoy
宿泊とかレストランシステムなどで夜間に定時実行されているバッチ処理ですね。

ninjinkun:
ああ集計系の。

kudoy
そうですね。それを事前に流せるものは流して。

今回移行対象のDBが十数個あったんですが、各DBの環境上の都合で2通りの方法で移行する必要がありました。

1つ目の方法は、事前にオンプレ側で取得したフルバックアップを移行先となるAWS側にリストアしておく。切替までの間の差分データはトランザクションログを定期的バックアップして、それを使って移行先のAWS側も更新していく。大半のDBはこちらで対応できました。

2つ目の方法は、当日オンプレ側で完全にDBのデータ更新が終わってからフルバックアップを取得して、移行先のAWS側でリストアする。

いくつかのDBでこの対応が必要となりました。

f:id:ninjinkun:20180718110202j:plain

リストア処理について

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:
ちょっと厳しかった。

f:id:ninjinkun:20180718110224j:plain

当日起こったとしたら嫌だったこと

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になったんですね。

以上で聞きたかった話はだいたい聞けたと思います。ありがとうございました。

すごく大変な移行だったとだけ聞いていて、詳細を知らなかったので、今回は勉強になりました。

f:id:ninjinkun:20180718110146j:plain

一休のSQL Server AWS移行事例(前編)

一休.comではオンプレミスで動かしていたサーバー群をAWSに移行する作業を2016年末から2年がかりで進めてきました(以下、クラウド移行と呼びます)。2017年7月にまず全サービスのアプリケーションサーバー移行が完了、2018年2月にデータベースであるSQL Serverの移行が終わり、クラウド移行が完了しました。

一休のシステムは予約や決済などのミッションクリティカルな基幹業務を担ってるため、大規模なシステム移行は難易度が高い仕事でした。また、一休のサービスは予約を取り扱うECの一種であるため、トランザクション機能をはじめとする、DBに対する品質要求水準や機能要件も比較的厳しい環境です。

本記事ではこのクラウド移行の中でも特に難易度が高いDBの移行を牽引したkudoyに、移行計画の設計や勘所についてインタビューします。

f:id:ninjinkun:20180718110142j:plain

  • インタビュイー
    • kudoy デジタルマーケティング部エンジニア(写真右)
  • インタビュワー
    • ninjinkun レストラン事業部エンジニア(写真左)
    • akasakas 宿泊事業部エンジニア(撮影のため写真なし🙏)

ninjinkun:
私は今回のDB移行やSQL Serverについてほぼ何も知らないので、今日は私のような素人にもわかるようにお願いします。

kudoy:
わかりました(笑) 今回のAWS移行で解決したかった問題は3つあって、

  1. ハードウェアを属人的な運用で管理している
  2. ハードウェアの調達に時間がかかる
  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月で解約することになっていたので、もうこの時期にやるしかなかったんです。

f:id:ninjinkun:20180718110214j:plain

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に

  1. EC2(OS)用
  2. WSFC用
  3. SQLServer用

の3つのIPを指定する必要があるのですが、想定していた2つのサブネットにそれぞれ2台づつ配置する4台構成にしてAlwaysOnを構築するとWSFC用IPとSQLServer用IPが上手く認識できなくてWindowsクラスタが正しく動作していませんでした。

どうにも解決策が見当たらなくて、AWSサポートと相談してたところ、WSFCで1 つのサブネットに複数のクラスターノードを配置する方法は、 AWS ではサポートされていないことがわかったんです。そこで、以下の図のようにサブネットを分ければ解決することがわかりました。

f:id:ninjinkun:20180701165921p:plain
一休.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:
(ユーザー向けの)キャンペーンがちょくちょくあって実施可能な日が割と少なかったりとか…。

f:id:ninjinkun:20180718110210j:plain

準備で一番時間がかかったところ

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可用性グループで利用できない制限があった。 参考