メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

RailsアプリケーションにThrusterを導入する

こんにちは。サーバーサイドエンジニアの @atolix_です。

今回は37signalsが公開しているThrusterを、メドピアで本番運用をしているアプリケーションのkakariに導入してみました。

kakari.medpeer.jp

Thrusterとは

Thrusterはアセット配信やX-Sendfileのサポートといった機能を持ったGo言語製のHTTP/2プロキシです。

詳細な設定不要でRails標準のPumaサーバーと組み合わせて動作して、nginxを構成に加えた時と同様のリクエスト処理を行うことが可能です。

github.com

www.publickey1.jp

導入前の状況

導入以前のkakariは以下のような構成を取っていました。 アセット配信の為にALBの背後にnginxを置いています。

今回は管理コストを下げる為に、nginxを剥がしてThrusterからアセット配信を行えるように構成を変更していきます。

※Cloudfront等のCDNを使用する選択肢もありましたが、本サービスのクライアントへの影響を抑える為に、ドメイン及びインフラ構成の変更を最小限に留めることを優先して今回はThrusterを選定しています。

移行の前にnginxでやっていた処理と同等の事がThrusterでも可能かチェックします。

チェック項目 Thrusterで可能か
Body Sizeの制限 MAX_REQUEST_BODYというオプションで設定可能。
gzip圧縮 js/css ファイルは自動的に圧縮されて配信される(content-encoding: gzipが付与される)
配信するアセットのキャッシュと最大サイズ指定 キャッシュ可能。最大サイズはMAX_CACHE_ITEM_SIZEで指定。

移行手順

安全に切り替えを行う為に今回はECS serviceごと差し替える方法を取りました。

1. Rails側にThrusterを導入する

一時的にnginxでリクエストを受けるECS serviceと、Thrusterでリクエストを受けるECS serviceを並行に稼動させたいので、Thruster用のポートを8080番で開けます。

加えてnginxの設定と同等のカスタム設定を記述していきます。Thrusterのオプションに用いる環境変数は接頭辞にTHRUSTER_を付けても認識されるので、他の環境変数と区別しやすいように今回は付け加えています。

また、ThrusterはPumaをラップして起動するのでPumaの起動コマンドの頭にthrustを付け加えます。

# Dockerfile
ENV THRUSTER_HTTP_PORT 8080 # デフォルトでは80番ですが、nginxの設定とぶつからないように8080に変更
ENV THRUSTER_MAX_REQUEST_BODY 104857600 # リクエストの最大サイズ
ENV THRUSTER_MAX_CACHE_ITEM_SIZE 3145728 # キャッシュの最大サイズ
ENV THRUSTER_HTTP_READ_TIMEOUT 300 # READのタイムアウト秒数
ENV THRUSTER_HTTP_WRITE_TIMEOUT 300 # WRITEのタイムアウト秒数
...
CMD ["thrust", "bundle", "exec", "puma", "-C", "config/puma.rb"]

EXPOSE 3000 8080 # 3000番にリクエストを投げるとThrusterを無視してpumaにリクエストが飛ぶ

2. Thrusterでリクエストを受けるECS serviceを追加して並行稼動させる

コンテナ定義では先ほど開けた8080番をRailsコンテナのポートマッピングに指定しておきます。

# container_definition.json

{
    "name": "rails",
    "image": "${rails_image}",
    "portMappings": [
      {
        "hostPort": 8080,
        "protocol": "tcp",
        "containerPort": 8080
      }
    ],
    ...

ALBのTarget Groupでも8080番に対応したリソースを追加して、一旦はダミーのListener Ruleを追加します。

# app_alb.tf

resource "aws_alb_target_group" "app" {
  for_each = toset(["blue", "green"])

  name              = "${local.prefix}-app-tg-${each.key}"
  port              = 8080
  protocol          = "HTTP"
  vpc_id            = aws_vpc.main.id
  target_type       = "ip"
  proxy_protocol_v2 = false
  ...
}

resource "aws_alb_listener_rule" "app_listener_https_thruster" {
  listener_arn = aws_alb_listener.app_listener_https.arn
  ...
  condition {
    host_header {
      values = ["dummy.example.com"]
    }
  }
# ecs_service_app.tf

resource "aws_ecs_service" "app" {
  name    = "${local.prefix}-app"
  ...

  load_balancer {
    target_group_arn = aws_alb_target_group.app["blue"].id
    container_name   = "rails"
    container_port   = "8080"
  }

この段階ではThruster駆動のECS serviceにアクセスが飛ぶことはない状態です。

3. Thruster駆動のECS serviceにリクエストを流す

nginx駆動のserviceとThruster駆動のserviceそれぞれのTarget Groupのconditionを入れ替えて、リクエストの向きを切り替えます。

ここまででnginxをThrusterに差し替えることが出来ました。

実際のリクエストを見てしっかりキャッシュヒットしている事を確認しました。

移行後の影響

パフォーマンス面

Thruster切り替え後のパフォーマンスをDatadogで一週間分計測しました。

App Latency

パーセンタイル nginx駆動時(avg) Thruster駆動時(avg)
p50 15.3ms 13.4ms
p90 69.3ms 79.5ms
p95 125.7ms 133.5ms
p99 197.6ms 203.2ms

レイテンシは微妙に上がっています。 nginxよりパフォーマンスは劣ることは予想していましたが、そこまで差は大きくなさそうです。

App Task CPU

nginx駆動時(avg) Thruster駆動時(avg)
7.23% 8.35%

タスク全体ではやや増加していました(新たにgzip圧縮の処理が入っているのでその分上がっているかもしれません)

Rails Container CPU

nginx駆動時(avg) Thruster駆動時(avg)
4.42% 8.01%

ThrusterはRailsコンテナ内で実行されるのでこちらもCPU使用率が上がっていますね。

若干のパフォーマンス低下は見られましたが、現状は問題なくアプリケーションは動作しています。

まとめ

Thrusterに切り替えることでリクエストの処理をRailsコンテナ内で完結させる事が出来る為、nginxの管理コストを減らせる点はとても良いと感じました。

一方で勿論Railsコンテナのパフォーマンスにマイナスの影響が出る場合があるので、移行をする際は今までnginxでやっていた事と同等の処理が出来るか調査することが必須になりそうです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp