RoRとAWSで100,000Req/
Minを処理するには?
または、インフラを構築すると
きに最低限気をつけていること
株式会社アカツキ
田中 勇輔
株式会社アカツキ
CTO 田中 勇輔 @csouls
Rubyとインフラが好きです!
好きなキーボード配列は
Qwertyですが、最近Dvorak
派が社内に増えて挫けそうです
今日話すこと
• RoR, AWS で 100,000 Req/Min を処理するの
に必要なこと
• 失敗の共有
前提
• RESTなAPIサーバ
• なぜRailsなのか?
• 既存資産を活かせる
• 認証や課金のライブラリ
• 環境構築、デプロイの仕組み
• Rails-APIでも十分
余談
• Rails 5 からWebSocketサポート・Turbolinksでの部分更新・Rails-API を追加
• リアルタイム更新性の強化というリッチ方面と、Rails-APIの追加というシン
プル方面の両面を強化
• Railsがあればなんでも出来ると捉えるか、Rails学習初期コスト高すぎと捉える
か
• 企業にとっては、Railsが常に進化していて、追って行けばたいていのことが
出来るというのは楽です。利用者が増えることにより、周辺技術が発達して
いくのも嬉しいことです
• これからもアカツキはRails/Rubyを応援していきます!
RoR, AWS で 100,000 Req/Min
を処理するのに必要なこと
• スケールアウト戦略
• 負荷テスト
• 地雷をうまく避ける
RoR, AWS で 100,000 Req/Min
を処理するのに必要なこと
• どれだけ大量のリクエストがあったとしても、難し
く考える必要はない
• 基本は、ボトルネックを特定し、スケールアップ or
スケールアウトのどちらかで対応するか(対応でき
るか)を判断し、やるだけ
• パターンを知ることで素早く対応できるようになる
スケール
アウト
アプリケーション
アプリケーション
• c3.4xlarge x 30~50台
• 負荷テストをした時に、8xlarge x 15台より
も、4xlarge x 30台の方が安定した
• スケールアウトしたアプリケーションサーバの
構成管理、デプロイ方法はきちんと考えておく
補足: 負荷テスト条件
c3.8xlarge x 15 c3.4xlarge x 15
nginx worker_processes 36 16
worker_connections 1024 1024
client_body_timeout 30 30
client_header_timeout 30 30
proxy_read_timeout 30 30
Unicorn worker_processes 144 64
timeout 30 30
• 以下条件で10万Req/Min を超える負荷を掛けた時、c3.8xlarge x 15 では、
HTTP Response Code: 500, 504 が発生した
• 原因の深追いは時間の制約で出来ていない…
アプリケーション構成+デプロイ
• Nginxをリバースプロキシとして、UnicornをWebサーバと
して使い、GodでUnicornプロセスを監視する
• Nginx1.6.2
• Unicorn 4.8.3
• God 0.13.6
• Rails 4.2
• God 設定(chef cookbook): https://github.com/csouls/chef-
god-unicorn
RDB
• db.r3.8xlarge! + r3.4xlarge * 8 の富豪構成
• どちらも、ピーク時CPU30%未満なので、4xlarge
+ 2xlarge にダウンしてもまったく問題ない
RDB
• ゲームデータは1DB。ユーザの行動によって
増えるデータは、ユーザIDを元にShardingし
て複数DBに分割
RDB
• DB Parameters
• 一般的なWebサービスであれば、DBパラメータは、RDSデ
フォルトでほぼ問題ない
• 要件に合わせてマニアックに変更することはある
• Sharding
• ユーザデータを分割
• RailsでのShardingはOctopusを使っている。辛さはある
RDB
• config/shards.yaml
というような
config/shards.ymlを用意して
default: &default
adapter: mysql2
encoding: utf8
charset: utf8
collation: utf8_general_ci
reconnect: false
pool: 5
<—— snip —->
octopus:
environments:
- production
production:
user01:
<<: *default
database: "user01"
host: user01
user02:
<<: *default
database: "user02"
host: user02
RDB
• リクエスト単位でユーザDB向けに Octopus.using を指定
class ApiController < ApplicationController
around_action :select_shard
private
def select_shard(&block)
if current_user.blank?
logger.error "select_shard current_user is blank"
yield
else
Octopus.using(User.shard(current_user.id),
&block)
end
end
end
RDB
• すると、ゲームデータ(1DB)へのアクセスは、ActiveRecord
レベルでusing(:master)を指定することになる
• 以下の様に、ActiveRecord::Baseを継承して、Baseモデル
を作りたい
class MasterModel < ActiveRecord::Base
self.abstract_class = true
octopus_establish_connection(Rails.configuration.database_configuration[Rails.env])
end
class Card < MasterModel
end
RDB
• Octopusの辛み
• https://github.com/tchandy/octopus/
issues/219
• 継承したクラスで、
octopus_establish_connection がうまくいか
ない問題
RDB
• 毎回 using(:master) 書く
• めんどくさい
• ゲーム共通データもUser Shardに置く
• 更新時の差分が怖い
RDB
• 毎回 using(:master) 書く
• めんどくさい
• ゲーム共通データもUser Shardに置く
• 更新時の差分が怖い
RDB
• 今思えば、ゲーム共通データもUser Shardに
置く方が辛くなかった…orz
• 一部辛みがあるとはいえ、今のところ
Sharding用途ではOctopusが筋良さそう
• 改良に取り組んでいきたい
デプロイ on AWS
• Capistrano3 with EC2 tag
• EC2のタグを元に、デプロイ先を決定する
module Ec2Helper
def self.included(_klass)
::AWS.config(access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
max_retries: 8)
end
def tagged_servers(tag_key, tag_value, default = [])
@ec2 ||= ::AWS::EC2.new(ec2_endpoint: 'ec2.ap-northeast-1.amazonaws.com')
addresses = @ec2.instances.map do |instance|
next if instance.tags[tag_key] != tag_value
next if instance.status != :running
instance.dns_name || instance.private_dns_name || instance.ip_address
end.compact
return default if addresses.empty?
addresses
end
def ec2_tag(tag_value, *args)
::AWS.memoize do
tagged_servers(fetch(:tag_key), tag_value).each do |host|
server(host, *args)
end
end
end
end
デプロイ on AWS
include Ec2Helper
set :tag_key, 'Role'
set :tag_value, ENV['EC2_TAG'] || 'app'
ec2_tag fetch(:tag_value), user: 'deployer', roles: %w(web app)
• Capistrano3 with EC2 tag
• EC2のタグを元に、デプロイ先を決定する
Redis
• m3.large x 64!!!
• なぜこんなことになってしまったのかは後ほど共有し
ます
永続化層:Redis
• RDBと同じようにSharding
• redis-rbの、Redis::Distributedをシンプルに使
えばOK
• コンシステント・ハッシュ法が使われている
ので、何も考えなくてもいい感じに分散して
くれる(便利)
第3回 IIJ社における分散DB技術「ddd」(1)
http://thinkit.co.jp/article/1030/1/page/0/1
永続化層:Redis
• 障害の時に影響範囲が少ない
• (ノードが一定以上あって、Key数が膨大でなければ)偏りが少ない
参考: コスト比率
• コンテンツ配信量の多いゲームは、
CloudFrontの比率が高くなりがち
負荷テスト
ruby-jmeter
• みんなだいすき JMeter の、jmxスクリプトを
簡易に作れるようになる Ruby Gem
• 覚えておくと負荷テストがめっちゃ楽になり
ます
ruby-jmeter
extract_id =<<EOS
var json = JSON.parse(prev.getResponseDataAsString());
var id = json['key'][0]['id']
vars.put('id', id);
EOS
test do
threads count: 100 do
header({name: "Content-Type", value: "application/json"})
header({name: "X-Platform", value: "android"})
header({name: "X-ClientVersion", value: "1.0.0"})
post name: '/api_with_body', url: "#{protocol}://#{host}:#{port}/api_with_body",
raw_body: {"user_account"=>{"some_parameter"=>"SomeValue",
"some_parameter2"=>"SomeValue2"}}.to_json do
extract name: 'return_value', regex: %q{"value":s?([d]+)}
end
get name: '/get_api', url: "#{protocol}://#{host}:#{port}/get_api/#{return_value}"
post name: "/api/js", url: "#{protocol}://#{host}:#{port}/api/js", raw_body:
params.to_json do
bsf_postprocessor name: "extract_id", scriptLanguage: 'javascript', script: extract_id
end
end
end.jmx
ruby-jmeter
test do
threads count: 100 do
end
end.jmx
• スレッド数の指定
ruby-jmeter
test do
threads count: 100 do
header({name: "Content-Type", value: "application/json"})
end
end.jmx
• リクエストヘッダの指定
ruby-jmeter
test do
threads count: 100 do
post name: '/api_with_body', url: "#{protocol}://
#{host}:#{port}/api_with_body", raw_body:
{"user_account"=>{"some_parameter"=>"SomeValue",
"some_parameter2"=>"SomeValue2"}}.to_json do
end
end
end.jmx
• リクエストBodyの指定
ruby-jmeter
test do
threads count: 100 do
post name: '/api_with_body', url: "#{protocol}://
#{host}:#{port}/api_with_body", raw_body:
{"user_account"=>{"some_parameter"=>"SomeValue",
"some_parameter2"=>"SomeValue2"}}.to_json do
extract name: 'return_value', regex: %q{"value":s?([d]+)}
end
end
end.jmx
• レスポンスBodyから、正規表現で値を抽出 to ‘return_value’
ruby-jmeter
test do
threads count: 100 do
get name: '/get_api', url: "#{protocol}://#{host}:#{port}/
get_api/#{return_value}"
end
end.jmx
• ‘return_value’ を使って、GETリクエスト
ruby-jmeter
extract_id =<<EOS
var json = JSON.parse(prev.getResponseDataAsString());
var id = json['key'][0]['id']
vars.put('id', id);
EOS
test do
threads count: 100 do
post name: "/api/js", url: "#{protocol}://#{host}:#{port}/api/js",
raw_body: params.to_json do
bsf_postprocessor name: "extract_id", scriptLanguage: 'javascript',
script: extract_id
end
end
end.jmx
• レスポンスBodyから、JavaScriptを使って値を抽出 to ‘extract_id’
地雷を避ける
地雷を避ける
• 他の人の失敗から学ぶ
• 致命的な処理を発見できるようにする
愚者だけが自分の経験から学ぶと信じている。私はむし
ろ、最初から自分の誤りを避けるため、他人の経験から
学ぶのを好む。
愚者は経験に学び、賢者は歴史に学ぶ。
失敗
• Rails 4.1/Arel 5.0 でコネクション切断時にス
キーマキャッシュが使われない
• Redis: KEYS pattern
Rails 4.1/Arel 5.0 でコネクション切断
時にスキーマキャッシュが使われない
• sonots/activerecord-refresh_connection を利用
• リクエストの度に、SHOW FULL FIELDS FROM
~~ が発行される
• User DBは問題ないが、Master DBは死亡
• 当時、原因を潰す時間的余裕がなかった
Rails 4.1/Arel 5.0 でコネクション切断
時にスキーマキャッシュが使われない
• 原因: http://so-wh.at/entry/2015/03/15/Rails_4.1/
Arel_5.0%E3%81%A7%E3%82%B3%E3%83%8D%E3%82%AF
%E3%82%B7%E3%83%A7%E3%83%B3%E5%88%87%E6%96%AD
%E6%99%82%E3%81%AB%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E
%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%8C
%E4%BD%BF
• 対処 : winebarrel/arel_columns_hash を使う
or sonots/activerecord-refresh_connectionを
止める
Redis: KEYS pattern
Redis: KEYS pattern
Redis: KEYS pattern
• 全てのサーバにリクエストがいくので、サーバ増やしても
意味ない
• そもそもO(N)以上の計算量のコマンドを使ってはいけない
• レビュー漏れた & 負荷テストでRedisのデータ量の
チェックが漏れていた
• 64台まで増加した要因に。本当は8台で十分
もう一つ重要なこと
• 最小のコストで最大の利益を得る
• パレートの法則 : プログラムの処理にかかる時間の80%は
コード全体の20%の部分が占める
• 実際は1%に満たない部分が大量リクエストのボトルネッ
クになることが多い(大きくなればなるほど、1つのミス
が致命的になりやすい)
• “致命的な部分”を発見できるシンプルな仕組み作りが重要
致命的な処理の発見 : まずやること
• NewRelic
• テスト環境サーバから入れておく
• 負荷テスト
• 良いツール(例えば ruby-jmeter)を使って作りやすく、メンテしやすくして
おく
• Railsはクソクエリが生まれやすい。pt-query-digest 等のツールを使い、
負荷テスト環境のクエリを分析しておく
• 監視
• CloudWatch Alert : 発見できなかったらダメ、設定しすぎて狼少年になっ
てもダメ
NewRelic
• 本番環境はもちろんですが、テスト環境に Pro 版
を導入しておくのを推奨
• Liteだと、Database Reportが見れないのが辛い
• 一台あたり149$/月を全台導入するのはコスト高
すぎるので、一部のサーバのみ設定しておく
NewRelic
• DatabaseReport - Slowest
NewRelic
• DatabaseReport - Most time consuming
NewRelic
• 以下設定しておいて、環境変数で切り替え
• 実運用では、5台適当に選択して有効にしている
• デプロイでサーバが変わることがあるが、利用料金は
5台で済む
production:
<<: *default_settings
monitor_mode: <%= ENV['NEWRELIC_MONITOR_MODE'] || "false" %>
• dotenv-rails + Capistrano
• role(:app)のサーバを数台選択し、”NEWRELIC_MONITOR_MODE=true”
を設定して配布
NewRelic
# config/deploy/production.rb
set :newrelic_monitor_number, 5
# lib/capistrano/tasks/deploy.rake
namespace :deploy do
desc 'Upload added NEWRELIC_MONITOR_MODE=true .env file'
task :upload_newrelic_env do
monitor_true = "NEWRELIC_MONITOR_MODE=truen"
temp = Tempfile.new("env")
tempfile = temp.path
temp.write(File.open(".env", "r").read)
temp.write(monitor_true)
temp.close
on roles(:app).to_ary[0...fetch(:newrelic_monitor_number).to_i] do |host|
upload! tempfile, File.join(shared_path, ".env")
end
FileUtils.rm(tempfile)
end
end
CloudWatch 設定の一例
対象 メトリック 設定例
ELB
RequestCount Sum > 150,000 for 3 minutes
HTTPCode_Backend_5XX Sum >= 200 for 2 minutes
UnHealthyHostCount Max >= 1 for 3 minutes
Latency Ave. > 1 for 2 minutes
EC2 CPUUtilization Ave. >= 60 for 3 minutes
RDS
CPUUtilization Ave. >= 50 for 1 minute
ReplicaLag Ave. > 1 for 2 minutes
WriteIOPS Ave. >= 2,750 for 1 minute
FreeStorageSpace Min < 200,000,000,000 for 1 minute
WriteLatency Ave. >= 0.2 for 2 minutes
Elaticache
CPUUtilization Ave. >= 35 for 3 minutes
CurrConnections Ave. > 50,000 for 5 minutes
FreeableMemory Ave. < 500,000,000 for 5 minutes
Evictions Sum > 1 for 1 minutes
• とりあえずこれらのメトリックを設定して、後で追加する
まとめ
• スケールアウト戦略
• 設計が重要。例えば、Octopusを後で導入するのは辛いとはいえ、DBの
Shardingは普通必要ない。

一概には言えないが、目安として 4~50,000Req/Min あたりからDB分割を検
討しても良いのでは
• 負荷テスト
• 計測大事
• 地雷をうまく避ける
• 基本的な監視 + Railsのクエリに気をつける
ありがとうございました!

RoRとAWSで100,000Req/Minを処理する