BASE開発チームブログ

Eコマースプラットフォーム「BASE」( https://thebase.in )の開発チームによるブログです。開発メンバー積極募集中! https://www.wantedly.com/companies/base/projects

アプリケーション監視のパターン「Health エンドポイントパターン」を実践する | 書籍『入門 監視 ―モダンなモニタリングのためのデザインパターン』を読んで

f:id:khigashigashi:20190305134900j:plain
出典: https://unsplash.com/photos/JKUTrJ4vK00

BASE BANK株式会社でソフトウェアエンジニアをやっている東口(@hgsgtk)です。即時に資金調達ができる金融サービス「YELL BANK(エールバンク)」というプロダクトを開発・運用しています。

さて、日々、ユーザーに使っていただくサービスを運営していく中で、「サービスを安定的に提供できているか」という観点において、監視する技法について関心があります。 そんな折、『入門 監視――モダンなモニタリングのためのデザインパターン』という書籍が最近発売され、世間的にも監視について、関心が高まっているかと思います。

今回は、この書籍の中から、実際に業務で実践していた「Health エンドポイントパターン」について、実践例書籍の内容の深掘りを含めて紹介しようと思います。

また、Mackerel Meetup #13 Tokyoというイベントでも今回の内容を発表いたしましたので、こちらも合わせてご参照ください。

Health エンドポイントパターンとは

アプリケーションの健全性を伝えるアプリケーション内のHTTPエンドポイントを作るパターンです。 カナリアエンドポイント(canary endpoint)・ステータスエンドポイント(status endpoint)とも呼ばれ、特に名前がついていることを知らずに使っている方も多いのではないでしょうか。(筆者もその一人でした。)

このエンドポイントでは、最低限「HTTPリクエストを受けてレスポンスを返せるか」という情報のみを返すこともできれば、デプロイされたバージョンや依存関係のあるDBなどのサブコンポーネントのステータスといった情報までをレスポンスに含めることもできます。

使用用途としては、次のようなケースがあげられます。

  • Mackerel など監視SaaSからの外形監視
  • ALBなどロードバランサのヘルスチェック
  • アプリケーション起動確認のデバッグ

実際に、実践する場合、アプリケーションの状態を伝えるHTTPエンドポイントを作成します。例えば、/healthcheck/pingなどと言った名前になるでしょうか。 そのエンドポイントは、ヘルスチェックに成功すればHTTPステータスコード200を、失敗した場合は200以外(特に503) を返すという実装になります。

BASE BANKでの実践例

BASE BANKでは、2つのエンドポイントを用途別に用意する方法をとっていて、それぞれ外形監視の対象としています。 ひとつが、コンテナ単体の生存確認を主目的とした、/health、もう一つが、依存しているDB・Redisなどへの接続までを確認対象に含める/health/deep です。

なお、この実践例は、 「Mackerel Meetup #13 Tokyo 」で 山根 (@fumikony) より発表した、『BASEにおけるMackerel利用上の工夫と困りごとのご紹介』内でも事例として言及しております。Mackerel等の監視SaaSとの組み合わせという点では、合わせてこちらを一読いただけるとより良いかなと思います。

単体の生存確認: /health

Go言語で実際に実装する場合は、例えば次のようなHTTP Handlerになります。

// HTTPステータスをフィールドに含むレスポンスフォーマット
type SimpleResponse struct {
    Status  int    `json:"status"`
    Message string `json:"message,omitempty"`
    Detail  string `json:"string,omitempty"`
}

// 200レスポンスが返却される
func SimpleHealthCheck(w http.ResponseWriter, r *http.Request) {
    rs := SimpleResponse{
        Status: http.StatusOK,
    }
    respondJson(w, rs, http.StatusOK)
}

// JSON形式でレスポンスを返却する
func respondJson(w http.ResponseWriter, body interface{}, status int) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    if err := json.NewEncoder(w).Encode(body); err != nil {
        fmt.Fprintf(os.Stderr, "failed to encode response by error '%#v'", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
}

これを、/simple/.health_check に対してルーティング設定した場合は次のようなレスポンスとなります。

-> % curl -i http://localhost:8080/simple/.health_check     
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 04 Mar 2019 13:24:23 GMT
Content-Length: 15

{"status":200}

これは、ロードバランサからの外形監視などの際に利用しています。

依存サービス込みの動作確認: /health/deep

次に、DBやRedisなど依存している外部サービスに接続できているかどうかを確認対象に含めるエンドポイントです。

func SimpleDeepHealthCheck(w http.ResponseWriter, r *http.Request) {
    _, err := NewMySQL(config.DB)
    if err != nil {
        // DBへのコネクションにてエラーが発生した場合は503レスポンス
        rs := SimpleResponse{
            Status:  http.StatusServiceUnavailable,
            Message: "failed to get connection database",
            Detail:  err.Error(),
        }
        respondJson(w, rs, rs.Status)
        return
    }

    rs := SimpleResponse{
        Status:  http.StatusOK,
        Message: "success to connect server",
    }
    respondJson(w, rs, rs.Status)
}

これを、 /simple/.health_check/deep に対してルーティング設定した場合次のようなレスポンスが返却されます。

-> % curl -i http://localhost:8080/simple/.health_check/deep
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 04 Mar 2019 13:33:22 GMT
Content-Length: 53

{"status":200,"message":"success to connect server"}

例えば、データベースに対する接続が失敗した場合は、次のような503レスポンスが返却され、アプリケーションの健康状態について知ることができます。

-> % curl -i http://localhost:8080/simple/.health_check/deep
HTTP/1.1 503 Service Unavailable
Content-Type: application/json; charset=utf-8
Date: Mon, 04 Mar 2019 13:35:38 GMT
Content-Length: 106

{"status":503,"message":"failed to get connection database","string":"sql: connection is already closed"}

このパターンのエンドポイントは、Mackerelからの外形監視で主に利用しています。

利点

外形監視に利用できる

APIの通信ができるかといった外形監視は特にアプリケーションを運用していると最低限関心をもつところだと思います。このパターンでは、ステータスコードで状態を判別できるため、外形監視に使いやすいかと思います。

デバッグに有効

起動したアプリケーションが正しくHTTPリクエストを処理する事ができるかを知る上で、重宝しています。 さらに、依存関係のあるDBなどのサービスへのコネクションが取れるかについても最低限確認できるため、起動確認に有用です。

余談:コンテナベースアプリケーションとの親和性

運用しているYELL BANKというサービスは、以前公開した『ECS(Fargate)でコンテナアプリケーションを動かすための設定情報の扱い方』という記事でも紹介した通り、コンテナ上で動作することを前提としたアプリケーションとして機能提供しています。 コンテナ内で動かすアプリケーションにおいても、外形監視は重要と感じていますが、実際このパターンはコンテナベースアプリケーションにおいてはどのように考えられるでしょうか。 コンテナベースアプリケーション設計として、「Health エンドポイントパターン」をどう評価できるかを考えるにあたり、redhatが公開している『Principles of container-based application design』 から参考になる原則を一つ見てみましょう。

それが、「HIGH OBSERVABILITY PRINCIPLE (HOP) 高観測可能性の原則」 という設計原則です。 これは、コンテナ内部をブラックボックスのように扱う設計前提を持った上で、自身のアプリケーションの活動状況や準備状況など、様々な状態チェックについてAPIを提供するという設計について言及しています。

自身の健康状態を伝える「Health エンドポイントパターン」も、観測度を上げる上で有用なパターンと言えそうですね。

ドラフト段階の共通レスポンスフォーマットについて

さて、「Health エンドポイントパターン」について、パターンと実践について見たところで、少し話を深掘りして『Health Check Response Format for HTTP APIs 』という議論中の共通レスポンスフォーマットについて見ていきたいと思います。 これは、『入門 監視――モダンなモニタリングのためのデザインパターン』の「付録C」という章で言及されているものです。 どういったレスポンスを返すべきか、議論中のこのフォーマットについて少し深掘りしてみます。

Health Check Response Format for HTTP APIs

このフォーマットは、大きく以下の3つの特徴を持ちます。

  • JSONフォーマットを利用する
  • media-typeは、application/health+json とする
  • 必須フィールドである status と いくつかのオプショナルなフィールドを含む

必須フィールド: status

status には「次のどれかの値を設定する」とされています。

  • pass: healthy
    • status code: 2xx-3xx range (MUST)
    • その他、下記の選択肢も可能
      • Node's Terminus をサポートするための ok
      • JavaのSpringBootのための up
  • fail: unhealthy
    • status code: 4xx-5xx range (MUST)
    • その他、下記の選択肢も可能
      • Node's Terminus をサポートするための error
      • JavaのSpringBootのための down
  • warn: healthy, with some concerns
    • status code: 2xx-3xx range (MUST)

warningレベルのタイプが設定できるのは一つ面白いところかなと思いました。

その他フォーマット一覧

必須とされている status 以外にも次のフィールドがオプショナルな項目として提示されています。

  • status
  • version (optional) - サービスの公開バージョン
  • releaseId (optional)
  • notes (optional) - 健康状態に関する記述
  • output (optional) - 生のエラー出力、 statusがpassのときは省略すべき
  • details (optional) - 依存しているサービスも含めた詳細情報、The Details Object というオブジェクトで定義
  • links (optional) - より詳細情報を得るための外部URLなど
  • serviceId (optional) - アプリケーションスコープなユニーク識別子
  • description (optional) - 人間に優しい説明

The Details Object

下流の依存関係やRedisなどのアプリケーションから見たサブコンポーネントの状態を伝えるためのオブジェクトとして提示されています。公式の例では、以下のようにcassandraやcpu・memory状態などアプリケーションが依存するものについての健康状態を伝える例を示しています。

  "details": {
    "cassandra:responseTime": [
      {
        "componentId": "dfd6cf2b-1b6e-4412-a0b8-f6f7797a60d2",
        "componentType": "datastore",
        "observedValue": 250,
        "observedUnit": "ms",
        "status": "pass",
        "time": "2018-01-17T03:36:48Z",
        "output": ""
      }
    ],
    "cassandra:connections": [
      {
        "componentId": "dfd6cf2b-1b6e-4412-a0b8-f6f7797a60d2",
        "type": "datastore",
        "observedValue": 75,
        "status": "warn",
        "time": "2018-01-17T03:36:48Z",
        "output": "",
        "links": {
          "self": "http://api.example.com/dbnode/dfd6cf2b/health"
        }
      }
    ],
    "uptime": [
      {
        "componentType": "system",
        "observedValue": 1209600.245,
        "observedUnit": "s",
        "status": "pass",
        "time": "2018-01-17T03:36:48Z"
      }
    ],
 },

inadarei.github.io

Health Check Response Format for HTTP APIs から学ぶこと

これは、現時点では、ドラフト版なので正式に守るべき標準・制約というわけではありません。しかし、実際に「Health エンドパターン」を実践するにあたって、詳細な点について迷いが生まれた際にこのように議論している場所があると知ると、参考になるかと思います。

最後に

私は、PHPやGo言語でのアプリケーション開発がメインのサーバーサイドエンジニアですが、そのような視点でも、『入門 監視――モダンなモニタリングのためのデザインパターン』は非常に勉強になります。 迷われている方はぜひお手にとって見てはいかがでしょうか。

また、BASE株式会社は、サービスの継続的な提供を守り・発展させていきたいそんな方を募集中です。ご興味があればぜひお気軽に遊びにいらしてください。

open.talentio.com

外部APIコールを含むプログラムの負荷試験

f:id:ymiyamura:20190122163945j:plain

サーバサイドエンジニアの宮村です。

カートの負荷試験について、第3弾の記事です。 最初の記事 でも触れましたが、今回の負荷試験実施にあたり、外部サービスを模擬するモックサービスを作成しました。

外部サービスへ接続する負荷試験を行うには

今回、負荷試験の対象としたのは、BASEのカートシステムです。

カートシステムには決済時に外部サービスを利用する箇所があり、通常の開発時には、外部サービスより提供されている検証環境へ接続しています。

一般的に提供されている検証環境は、本番環境ほどの性能ではなかったり、他の利用者と共用しているというものではないでしょうか。そのため、利用者の都合で自由に高負荷をかけることは難しいと言えます。 負荷試験のような高負荷をかけることが可能か否か、また可能な場合、自由に実施してよいか、事前申請が必要かなどを確認し、それなりの準備を行う必要があります。

また利用が可能だったとして、外部サービス要因でパフォーマンス影響が出る懸念、送信するリクエストの制約など、外部サービスの提供する環境を使用する場合に考慮しなければならない点がいくつかあることがわかってきました。

f:id:ymiyamura:20190225153132p:plain

外部サービスへ接続しない

一方で、提供されている検証環境を使用せずに負荷試験を実施することについて検討します。

  1. 提供されている検証環境と同等の動作をするモックサービスを用意し、接続先を変更する
  2. 提供されている検証環境へ接続する処理を、外部接続せずに処理が完了するように変更する
  3. いっそのことすべてを本番環境で行う

本番環境との差異を小さくするという点において、3を除けば、プログラムの変更が少なくなる1の方法が有利と言えそうです。 稼働中のサービスで3を行うのは、リスクが高い上に、検証等の準備を含めるとコストが低いとは言えません。

調査を行ったところ、用意するモックサービスは、

  • BASEのサービスからのリクエストに対して
  • 処理可能な形式のレスポンスを
  • 適切な時間をかけて返してくれれば十分

という見通しを持つことができました。

これであればモックサービスを準備することが有力な選択肢になるだろうということになり、検討を始めました。

f:id:ymiyamura:20190225153217p:plain

モックサービスとしてSwaggerを検討する

さて、モックサービスをどのように準備するかということになり、調査を開始しました。

たとえば Swagger は、決まったレスポンスを返すモックサーバとして動作してくれるようです。作成したAPI定義がドキュメントとして残るなら、それも良さそうに思えます。 ただし、「適切な時間をかけて返す」という部分が解決できないようで、採用を見送ることにしました。

ここでふと、 sleep(1); したいだけであれば、PHPで書いてしまえばよいのでは?と思い直し、PHPで作ってみることにしました。

PHPなら普段から書いていますので、キャッチアップのコストは不要です。

FastRouteで行為に高速なモックサービスを実装する

簡単なPHPで書けるだろうと高を括っていたため、特にウェブアプリケーションフレームワークは使用せずに実装をはじめました。

今回用意したいモックサービスのエンドポイントが8つほどあります。 いくつか似たものはあるものの、バリエーションのあるURIであったため、ルーティングがあるといいなと思いました。

そこで FastRoute を使うことにしました。これでルールの一定しないエンドポイントでも、仮にエンドポイントを増やす場合にも拡張が容易になります。ずいぶん楽になりました。

<?
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
    $r->addRoute('GET', '/users', 'get_all_users_handler');
    // {id} must be a number (\d+)
    $r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler');
    // The /{title} suffix is optional
    $r->addRoute('GET', '/articles/{id:\d+}[/{title}]', 'get_article_handler');
});

こんな感じで簡単に書けました。(サンプルより引用)

あとは、事前に調査しておいた資料をもとに、エンドポイントごとのレスポンスと応答時間を設定していきました。これは地道な作業です。

一通り書き上がったところで開発環境の接続先を変更し、疎通確認を行います。HTTPリクエストヘッダを使用している箇所などのいくつかの修正を行い、ついに注文が完了できるようになりました。

これで、設置してcomposer installするだけで使えるモックサービスの完成です。

実戦投入

負荷試験実施の際には、AWSのEC2に設置し、期待通りのレスポンスを返しているかを確認しながら使いました。

試験中、何度か期待通りのレスポンスが出ないことが検知されることがありましたが、Apacheの設定値の調整などを行うことで期待通りの役割を果たしてくれました。

f:id:ymiyamura:20190122164701p:plain
これはモックサーバの設定を見直していたときのやり取りでした。

モックサービスなので、実在するクレジットカード番号を使う必要もなければ、メールアドレスの重複を気にする必要もありません。 シンプルなテストシナリオを繰り返し実行することができ、便利に使用することができました。

まとめ

  • 提供されている検証環境を使わずに自前でモックサービスを作成、使用することも、負荷試験時の選択肢としてアリだと思いました。
  • モックサービスを自由にできることで、アプリケーションやテストシナリオをシンプルにできる点も良いと感じました。
  • 今後、他の外部サービス連携にかかわる部分の負荷試験を行う際も、同じ要領でモックサービスを活用することができそうです。

最後に

これまで3回にわたり、負荷試験関連のトピックを紹介させていただきました。興味がある方は、過去の記事も参照いただけると嬉しいです。

devblog.thebase.in

devblog.thebase.in

パフォーマンスに関する改善に終わりはなく、また別の課題に向けて取り組んでいる最中です。 BASEでは、カートのパフォーマンスアップに興味がある方をお待ちしております!

open.talentio.com

次世代の管理画面を作るフロントエンドの取り組み

f:id:aiyoneda:20190205184206p:plain

フロントエンドエンジニアの松原(@simezi9)です。BASEでは現在ショップ向けの管理画面をリニューアルするプロジェクトが進んでいて、UI/UXの更新と同時に創業当時から継ぎ足して作ってきたフロントエンドの技術スタックを一新しようとしています。この記事では、具体的にそのフロントエンドの更新でどのようなことに取り組んでいるのかをいくつかご紹介したいと思います。

Vue + TypeScriptを利用したMPA(multi page application)化

HTMLの構築をPHP(サーバーサイド)からJS(クライアントサイド)へ移行する

従来の「BASE」の画面ではPHPでHTMLの構築を行っていましたが、HTMLの構築をすべてPHPのコードから分離させて、Vueによるクライアントサイドでのレンダリングにしています。また管理画面の特性上(1ページあたりの閲覧時間が長く相対的にローディングの時間が短くなる)、サーバーサイドレンダリングなどは特に行っていません。

クライアントサイドでのRoutingの導入

Routingに関しては管理画面の機能を大きな単位でいくつかに分割して機能間のRoutingはCakePHPで行い、機能内ではSPAのようにクライアントサイドで細かくRoutingを行っていく構成になっています。フロントエンドのRoutingはVue Routerを利用しています。

f:id:simezi9:20190205125100p:plain f:id:simezi9:20190205125115p:plain

大雑把に責務を整理すると以下のようになります。

  • PHPサーバの役割
    • 大きな機能単位でのRouting
    • RestAPIの提供
  • Javascript(Vue.js)の役割
    • 機能内でのRouting
    • 画面の描画

RestAPIの構築とAPI Blueprintによるカタログの作成

データの取得・操作は基本的に全てAPI経由で行えるように必要なだけのAPIを用意しています。作成したAPIはAPI Blueprintを利用してカタログ化されていてサーバサイドのエンジニアとフロントエンドのエンジニアが疎結合に作業を進めていけるようになっています。

f:id:simezi9:20190205125454p:plain

フロント側でも基本的にAPIの構成と1エンドポイントに対して1ファイルで対応するようにinfra層を構築しています

f:id:simezi9:20190205125511p:plain

なぜVue+TSか

もともとBASEではHTML/CSS/JSによるフロントの構築をすべてデザイナーが担っていてフロントエンドエンジニアというポジションはありませんでした。この次世代管理画面プロジェクトでのフレームワーク選定もデザイナー陣主導で行われました。そのなかでRiot.jsやReactなども試した上で、Vue.jsがもっとも技術導入がスムーズに出来そうだという結論となりVueを採用しました。 また、プロジェクトのサブテーマとして「次の5年を支える」というものがあり、TSが提供する型システムによる安定性がプロダクトのライフサイクルをサポートしてくれることを期待してTSを導入しています。

開発していて実際どうか?

現行のVue2.xとTSの組み合わせはReactなどと比べると劣る面があることは否めない(props周りなど、型が失われてしまう場所が多い)ですが、API通信などのVueが絡まない部分でのTSの型システムのサポートは非常に強力ですし、Vueの次期メジャーバージョンであるVue3.0ではコアがTSに移行し、サポートの強化も表明されており今後に向けた選択肢としてVue.js + TSの組み合わせは有力だと思っています。

Vue.js + TSで最も問題になるのは、公式のドキュメントがJSで書かれていることであり、TSの書き方と若干サンプルコードが異なる という点です。これに関してはVue.jsに馴染みのあるメンバーがいれば大した問題にはなりませんが、全員がVueを初めて触るような場合には、いきなりTSを導入することには慎重になったほうがいいかもしれません(TSとJSはファイル単位で共存できるので後から追加することは難しくない)。

Storeをどうしているか

Vue.jsではデータを一元管理するためのライブラリとしてVuexを使うのが一般的ですが、現状でのVuexはTSとの相性があまりよくなく、せっかくAPIレスポンスの型を定義したのにいざVueコンポーネントで取り出そうとすると型情報が失われてしまいます。そこでTSを活かすためにあえてVuexを採用せず、GlobalEventBusのような構造を拡張した独自のStoreを使っています。この独自実装に関してはそのメリット・デメリットは以下のように現状では一長一短に感じています

  • メリット
    • TSの型をVueコンポーネント内部で利用して堅牢なコードを書ける
    • シンプルな実装で可読性が高い
  • デメリット
    • 最低限の機能しか持たず、Vue.jsのエコシステムの流れからは外れる
    • Vue.js devtoolsのVuexサポートが利用できない

後者のdevtoolsが使えない点に関しては、公式の実装を参考にして、storeの内部状態のダンプなど最低限のデバッグは可能にしています。

f:id:simezi9:20190205125532p:plain

AtomicDesignに基づいた共通UIライブラリの構築

次世代管理画面プロジェクトと並行して社内のサービスで共通して利用するためのUIコンポーネントライブラリを構築しています。現在BASEではAtomic Designを採用したデザインシステムの構築を進めており、そのSketch上のデザインシステムと対応する形でStorybookを利用してコンポーネントライブラリを作っています。 storybook-addon-vue-info@storybook/addon-knobsを利用して、メンバーがStorybookを見るだけでアプリを構築していけるように整備を進めています。

f:id:simezi9:20190205125548p:plain f:id:simezi9:20190205125555p:plain

ブランチ単位での自動デプロイ

このライブラリのリポジトリではブランチを切ってPRを作るたびにブランチ単位でStorybookがビルドされるようになっており、デザイナーとエンジニアの確認が簡単に行えるようになっています。

f:id:simezi9:20190206120054p:plain

自前でUIライブラリを実装していく上での工夫・苦労

  • 最初にコンポーネントを全部用意しようと考えない
  • 一番小さいレイヤ(Atoms)のコンポーネントから作る
  • コンポーネントだけを作ろうとせず、実際に使いながらライブラリを育てる
    • 特にformを構成するコンポーネントなど
  • SketchのデータとStorybookのデータを一致させることにこだわりすぎない
    • 見た目だけではわからないデザインの意図を拾ってStorybookに反映することを心がける

汎用的なコンポーネントを作るのは見た目の単純さに反して考慮することが多く、一発で作ろうとしてもなかなか作りきれません。有名なVueのUIコンポーネントライブラリの一つとしてVuetifyがありますが、その実装を見てもbuttonのtsファイルが170行inputで300行あります。実際自分でコンポーネントを作っても、「あの機能がない」、「このカスタムができない」と問題が続々と出てきては一個ずつ対処する繰り返しになります。その状態で複雑なコンポーネントを用意しても、品質が上がらずコンポーネントの修正の足かせになりがちなので、小さなコンポーネントから我慢して作っていくことが必要だと感じました。

最後に

BASEの次世代フロントエンド環境はまだ開発がはじまったばかりで、実際にはまだまだ課題が山積みです。次世代の管理画面も、UIコンポーネントライブラリも、自分の力で構築していってみたいというエンジニアを募集しているので、興味がありましたらぜひ一声かけていただければと思います。

open.talentio.com

PHPカンファレンス仙台2019にてBASEから2名登壇・スポンサー協賛しました #phpconsen

f:id:khigashigashi:20190204204450j:plain

こんにちは、BASE BANKでエンジニアをしている東口(@hgsgtk)です。

さていきなり本題ですが、2019年1月26日(土)にPHPカンファレンス仙台2019が開催され、ネットショップ作成サービス「BASE」は、シルバースポンサーとして協賛しました。

大変盛り上がった最高のカンファレンスだったのですが、BASEからは、私と田中(@tenkoma)がセッションスピーカーとして登壇しましたので、それぞれの発表内容についてレポートいたします。

phpcon-sendai.net

発表資料

テストを書くのがつらくならないテスト駆動開発のアプローチ

私、東口(@hgsgtk)は、午前の最初の登壇で30分の発表をしました。

「テストがつらい」状況の様々な要因のうち、「テストを書くのが億劫」なことや「テストを書くのが難しいコードになる」といった要因にフォーカスを当て、それらに対してテスト駆動開発のアプローチを活用することで解決できないかという内容です。

f:id:khigashigashi:20190204205341j:plain
ライブコーディング中の様子

当日は、ライブコーディングにてテスト駆動開発の流れを説明しました。 懇親会や後日Twitterなどで、感想や質問をいただけたのが大変ありがたかったです。

当日頂いた質問

テストを書くことに慣れてないと実際にやり始めるのに一苦労しそう

「テストを先に書く」という点について、テストを書くのに慣れていないと確かに難しいと思います。そのため、まずはテストを書くことに慣れるというのは前提として必要だろうという話をしました。

こちらについては、以前PHPカンファレンス大阪2018にて、テストを書いたことがないエンジニアがテストを書けるようになるまでやったことという発表しましたので、こちらの資料を参考にしていただくと良いかもしれません。

PhpStormとPHPUnitを連携してユニットテスト作成を楽にする

田中(@tenkoma)より、午後2つ目の登壇で30分の発表をしました。

PHPStormとPHPUnitを連携することでユニットテスト作成・実行を効率的に進めるというテーマで発表です。

f:id:khigashigashi:20190204205536j:plain
発表中の様子

田中の個人ブログにて当日の発表の様子をレポートしておりますので、こちらも併せてご覧ください。

tenkoma.hatenablog.com

最後に

PHPカンファレンス仙台2019は今年初開催でしたが、良い意味で「初開催とは思えない」ほどしっかり運営されていて、とても楽しいイベントでした。 運営の皆様、素晴らしいカンファレンスをありがとうございました!

次はPHPerKaigi2019

次のカンファレンスは、3月29日〜3月31日にPHPerKaigi 2019が開催されます。 BASEからは今回仙台で登壇した田中・東口が同じく登壇します。

田中からは、「PhpStormでコードを理解する技術」(レギュラートーク 15分)というテーマで発表します。

fortee.jp

東口からは、「「質」の良いユニットテストを書くためのプラクティス」(レギュラートーク 30分)というテーマを発表いたします。

fortee.jp

また、ネットショップ作成サービス「BASE」は、ゴールドスポンサーとして協賛しています。

ぜひ、皆さんPHPerKaigi 2019でお会いしましょう!

カートの負荷試験におけるApache JMeterの活用

f:id:ymiyamura:20190122163945j:plain

先週に引き続き、BASEでサーバサイドエンジニアをしている宮村です。

先日、負荷試験の取り組みについて紹介させていただきましたが、今回はその際に使用したApache JMeterの活用について紹介させていただきたいと思います。

JMeterは高機能なツールなので使いこなすと強力ですが、少し複雑な機能のテストを行おうというとき、ややとっつきにくい部分もあるのではないかと思います(私はそうでした)。具体的な使い方をいくつか知っておくだけで、ぐっと便利に使えるようになると思いますので、これから負荷試験を行おうという方に少しでも参考になれば幸いです。

選定理由

負荷試験を行うツールはいくつかありますが、今回は下記の条件が満たせるものを探していました。

  • セッション管理できること
  • ページ遷移を伴うシナリオが作成できること
  • シナリオでレスポンスの値を取得して使えること
  • 攻撃サーバがスケールすること
  • 習熟コストが高すぎないこと

これが満たされていれば、どれでもいいかな〜と思っていたので、以前少し使ったことがあったJMeterでできそうだと見えてきた時点でJMeterで進めることに決めました。

他に見たものを少しだけ紹介します。

Apache Bench:導入及び使用がとても簡単ですが、複雑なシナリオを扱うことができないため、カートの負荷試験には不向きであると判断しました。

Locust:JMeterと機能的には類似しているとのことで良さそうにも思いましたが、私個人にとってPythonでシナリオを書けることがあまりメリットにならないこともあり、選択しませんでした。

シナリオ作成

下記の手順で、シナリオを準備していきます。

  1. テストケースの作成
  2. 作業PCへのJMeterの導入
  3. 記録コントローラでテストシナリオのベースを作成
  4. 実現したいテストシナリオへ改修

1. テストケースの作成

どのような高負荷状態を作りたいかを決めます。〇〇のページに、単位時間あたり〇〇のアクセスを、〇〇の時間発生させる、というようなものになるかなと思います。 これはツールによらない工程ですが、これがなければシナリオは作れません。今回は、過去の高負荷状態を再現できるようにケースを作成しました。

例)商品Aを1個、クレジットカードで購入するユーザが1分間に100人のペースで5分間来る、等(※数字はイメージです。)

2. 作業PCへのJMeterの導入

公式ページに従い、インストールを行います。

必要に応じてJavaのインストールを併せて行います。

(この工程の情報はWeb上にたくさんありますので、詳細は割愛させていただきます)

3. 記録コントローラでテストシナリオのベースを作成

シナリオ作成には「記録コントローラ」を使用しました。

JMeterをプロキシとして動作させ、ブラウザのプロキシ設定をJMeterに向けることで、ブラウザでの操作をJMeterで記録する機能です。

これを使えば記録したシナリオをそのまま使用できるはずでしたが、いくつかのパラメータが欠けていたので補ったり、不要なシナリオを削除したりといった作業が必要でした。

ただ、この修正の工程が必要であったとしても、記録コントローラを使うメリットは大きいと感じましたのでおすすめです。

4. テストシナリオの改修

1で決めたテストケースを実現できるよう、各種の設定値を書き加えたり変更したりします。

今回は、たとえば商品IDなどのいくつかのパラメータは変数として定義するなど、必要に応じて記録されたシナリオに改修を加えていきました。

ここで、便利だったエレメントを以下に紹介します。

便利だったエレメント8選

  1. ユーザ定義変数
    • 手元の環境でシナリオを作成し、試験環境にで実行するということをしたので、urlなど環境固有の値を簡単に切り替える必要があり使用しました。
  2. HTTP認証マネージャ
    • basic認証をかけた環境で試験をするのに使用しました。
  3. HTTPクッキーマネージャ
    • セッションを利用するので使用しました。
    • 異なるユーザの購入を想定したので、「繰り返しごとにクッキーを破棄する」設定にしました
  4. アサーション
    • 各リクエストで、レスポンスコードは問題ないが、期待するレスポンスが返らない場合にテストを失敗させるため使用しました。
    • 具体的には、正しく次の画面に遷移した場合と、エラーで同じ画面にとどまっているにもかかわらずレスポンスコードは正常な場合を区別したい場合に使用しました。
  5. HTMLリンクパーサ
  6. 正規表現抽出
    • 前の画面で生成した値を、後続のリクエストのパラメータとして使う場合に使用しました。
    • はじめは後述のHTMLリンクパーサを使っていたのですが、JMeterサーバを複数台構成にして、リモート実行した際に正常に動作しないことがあったため、正規表現抽出に切り替えました。
  7. 統計レポート
    • シナリオ実行の様子を眺めるのに使いました。
    • JMeterには様々な高機能なリスナがあり便利なのですが、リクエスト数を増やした場合に、リスナの処理が重くなってしまうそうです。それを避けるため、これとシンプルデータライタの2つだけを実際の試験実行時には使用しました。
  8. シンプルデータライタ
    • シナリオ失敗の具体的な状況を調査するため、結果はすべてCSVファイルに書き出しました。
    • リクエスト失敗の原因調査等、必要に応じてExcelで調査しました。

f:id:ymiyamura:20190128134101p:plain
完成したシナリオの例

実行

シナリオ実行

AWS上のWindowsサーバ1台にJMeterを設置し、少ないリクエスト数から徐々に負荷をかけていきました。

最初は、シナリオや設定値の妥当性の確認も兼ねて、1リクエストから実行していきます。

リクエスト数を増やす過程で、JMeterサーバがボトルネックかな?というタイミングでリモート(Linuxサーバ2台)で実行するように変更しました。 socket write error, failed to respond, closed connection というエラーが、シンプルデータライタで取得したログに出力されたタイミングがそれです。

結果の記録

どういうシナリオを流したか、結果はどうだったかをスプレッドシートに都度記録しました。「実験ノート」をとる要領で、下記のような内容を都度記録しておきました。これが後に結果レポートになっていきます。

  • リクエストの種類
  • 成功数、失敗数
  • シナリオ完了までの時間
    • 成功はしているが遅延している、を検知
  • 実行時刻
    • 各種ログを後から調査するため

まとめ

  • フォームの入力やセッションを利用し、複数画面の遷移を伴う機能であるカート機能の負荷試験を、JMeterを使うことで実施できました。
  • ツールの選定には、試験でどういった機能が必要かをまず明確にするのが重要でした。
  • シナリオが資産として残っているので、今後も改善しながら負荷対策に役立てていく予定です。

最後に

本番相当の環境でのパフォーマンス改善に興味のある方、これからもさらなる改善を行っていくことになると思いますので、ぜひご連絡ください!

open.talentio.com