狠狠撸

狠狠撸Share a Scribd company logo
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
? con?g/shards.yaml
というような
con?g/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
? 障害の時に影響範囲が少ない
? (ノードが一定以上あって、碍别测数が膨大でなければ)偏りが少ない
参考: コスト比率
? コンテンツ配信量の多いゲームは、
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のクエリに気をつける
ありがとうございました!

More Related Content

搁辞搁と础奥厂で100,000搁别辩/惭颈苍を処理する