@ijin

[Michael H. Oshita]

Non-RDSなオレオレMulti-AZ MySQL Replication

先日(5/17/13)cloudpack night #6 - Re:Generate -に参加してきました。

当日の様子はcloudpack吉田さんの以下のレポートで。
cloudpack Night #6 - re:Generate - を開催しました

私はDJを少々とLTで参加させて頂いたのでその内容になります。

オレオレMulti-AZのススメ

AWS上でMySQLを使う場合、RDSはてっとり早くて良いんですが、たまにもうちょっと柔軟性が欲しい時があります。 例えば別のストーレジエンジンやディストリビューションを使ったり(Percona Server, MariaDB, TokuDB, Mronnga等)、RDSでは使えないインスタンスファミリー(hi1, m3, c1系)を使ったり、OSレベルでのチューニングができたり、スレーブのバッファプールを予め温めておいたり。等々。

しかし、RDSにはAvailability Zone (AZ)をまたいでフェールオーバーするMulti-AZ機能があり、AWSで設計するにはAZ障害を考慮した方が推奨されます。

そこで、MHAとVPCを組み合わせて柔軟性をもったMulti-AZ環境を実現します。 (ちなみに私の場合はhi1.4xlargeでPercona Serverを冗長化をする必要があったから)

MHA

MHAとはFacebookの松信さんがDeNA時代に作ったMySQLの自動フェールオーバーしてくれうナイスなツールです。Master障害時にbinlog同期とSlaveの昇格を全自動でやってくれます。昇格時にはカタログデータベースに更新をかけて新DB構成のIPの情報を更新するか、Virtual IPを切り替えるかをする必要(この担当部分はmaster_ip_failover_scriptで対応)があるけど、今回は後者的なアプローチになります。

VPC

ENIを使えばVirtual IP的な使い方はできるけどAZは超えられないので、source/dest. checkを無効化した上でrouting tableによって擬似Virtual IPを実現するRouting-Based HAパターン(CDP 2.0候補)を使います。ADSJ片山さんのエントリが発端ですね。

Demo

以下、LT時に見せたデモ動画

擬似Virtual IPに対してそれぞれread/writeを行いつつMasterのプロセスをkillすると、通信できなくエラーが出続けるが20秒以内に自動フェールオーバーが完了しread/write共に再開します。pingも平均0.5msから2.0msに変わった事からAZが移った事が確認できます。

RDSのMulti-AZの場合、フェールオーバーには3-6分かかるので、かなり短時間で復旧が可能です。 また、RDSはCNAME切替によるDNSベースに対して、MHA+VPC構成の場合はIP指定することができます。そうするとアプリからのresolveが不要になり、去年起こった内部DNSが引けない障害が起きても問題ありません。

注意点

  • RDSな自動バックアップがない
    • Xtrabackupとbinlogの定期s3保存で対応
  • Point in Timeリカバリー
    • Chef等で自動化しましょう
  • Read Replicaの作成
    • Chefで頑張りましょう
  • 学習曲線
    • 勉強しましょう
  • API backplaneがSPoF
    • AWSに祈りましょう

特に一番の懸念点は最後のAPI backplane。AWSの今までの大規模障害状況を見ていると、皆が同時に復旧をしようとしAPIへのリクエストが大量に集中してそこがボトルネックになり、リソースの操作不能に陥るという悲惨な事象が何回かありました。まあ、その場合はRDSでも同じような気はしますが、ここは当時の障害を経験にキャパシティが増加されている事を信じておくしかありませんね。。

おわりに

とまあ、こんなLTをしたわけですが、この後に続いたCookpadの菅原さんのLTの方が盛り上がって自分のは余興に終わってしまいました。

あ、ついでにその日はcpniteの資料作成やDJの選曲であんまり寝てなかったにもかかわらず、無事AWSソリューションアーキテクト(Associate)の認定試験に受かりました!

LTスライド

リージョン間高速データ転送

先日のJAWS DAYSのセッション「Behind the scenes of Presidential Campaign」でリージョン間のデータ転送を高速化するツールとしてtsunami udpcloudoptを使った話が出てたので試してみました。

通常、遠距離のサーバはRTTが大きくなるのでスループットが下がり、巨大なデータ転送には苦労しますが、なんと27TBを9時間で転送したとのこと!実際はマシンを並列にして同時転送したらしいので1台での実験です。

前提

  • 東京(server1) -> アメリカ西海岸(server2)
  • RTT: 120msぐらい
  • EC2 instance type: m1.large
  • OS: Ubuntu 12.04

ベーステスト

10Gファイルの作成

server1$ mkdir _tmp; cd _tmp
server1$ dd if=/dev/zero of=10G count=1 bs=1 seek=10G

転送

server1$ scp -rp 10G server2:
10G     100%   10GB  11.1MB/s   15:22

だいたい、90Mbpsぐらい。

Tsunami UDP

インストールは両サーバで

sudo apt-get install make gcc autoconf
wget http://downloads.sourceforge.net/project/tsunami-udp/tsunami-udp/tsunami-v1.1-cvsbuild42/tsunami-v1.1-cvsbuild42.tar.gz
tar xvfz tsunami-v1.1-cvsbuild42.tar.gz
cd tsunami-udp-v11-b42
make
sudo make install

tsunami udpはfetch型の作りなので、送信側のサーバ(server1)で対象ファイルのあるディレクトリに移動してサービス起動

server1$ cd _tmp
server1$ tsunamid *

で、クライアント(server2)からファイルをfetch

server2$ tsunami connect ec2-xx-x-xx-x-x get 10G quit

本当はftp-likeは対話型クライアントだけど、ワンライナーでも可能。
転送が完了するとクライアント側で情報が出力されます。

Transfer complete. Flushing to disk and signaling server to stop...
!!!!
PC performance figure : 224947 packets dropped (if high this indicates receiving PC overload)
Transfer duration     : 419.32 seconds
Total packet data     : 183339.18 Mbit
Goodput data          : 181958.17 Mbit
File data             : 81920.00 Mbit
Throughput            : 437.23 Mbps
Goodput w/ restarts   : 433.93 Mbps
Final file rate       : 195.36 Mbps
Transfer mode         : lossless

おお、確かに速い!スループットもセッションでも言ってた476Mbpsに近いし。

サーバ側では

Server 1 transferred 10737418241 bytes in 419.33 seconds (195.4 Mbps)

ファイル自身の実際の転送レートはFinal file rateである195.4Mbps。 多分、パラメータチューニングやより速いディスクを使うとスループットはさらに向上すると予想。

cloudopt

次は圧縮、TCP最適化、data deduplication等、様々な技術を用いてWANを高速化するcloudoptの実験。こちらは有料で、ソフトウェアをインストールしてライセンスを購入(15日間のお試しあり)して使う方法とAmazon Marketplaceでセットアップ済みの課金型AMIを起動する方法がとれます。今回はてっとり早く後者で。

AMIはMarketplaceでCloudoptを検索し、各リージョンで1台づつ起動。

Ubuntuベースなのが良いですね。

まずscpから呼べるcloudcopyを使っての転送

server1$ scp -rp -S cloudcopy 10G server2:
10G                 100%   10GB  41.5MB/s   04:07     
CloudCopy transferred 17.22 MB, saving 99.8% of bandwidth by sending 9.983 fewer GB 

お、速い。しかしよく見てみると17.22MBしか転送されてません。どうやらファイル自体が全てゼロ埋めなので圧縮とdeduplicationが最大限効いているみたい。

では、今度は実際のDBのバックアップで転送実験(容量14GB)

server1$ scp -rp -S cloudcopy dbbackup.tar server2:
dbbackup.tar                100%   14GB  11.2MB/s   20:42   
CloudCopy transferred 6.041 GB, saving 55.5% of bandwidth by sending 7.528 fewer GB

スループットはほぼ一緒だけど、転送容量が削減できてます。

一度転送したファイルはbyte cacheされるので、次に転送する時は差分だけ送るので高速化されます。

server2$ rm dbbackup.tar 

server1$ tar rvf dbbackup.tar file
server1$ scp -rp -S cloudcopy dbbackup.tar server2:
dbbackup.tar                100%   14GB  20.0MB/s   11:35  
CloudCopy transferred 59.57 MB, saving 99.6% of bandwidth by sending 13.511 fewer GB

リージョン間レプリケーション

次はMySQLのレプリケーション実験。

各インスタンスでのピア設定

server1$ sudo cloudconfig peer_add server2 server2_local_ip(10.x.x.x)
server2$ sudo cloudconfig peer_add server1 server1_local_ip(10.x.x.x)

これでcloudoptを使ったサーバ間のトンネルが確立されます。MySQLのレプリケーションはprivate ipでの設定が必要。 一通りレプリケーションが出来き、スレーブIOを停止した状態でマスターにしばらく書き込んだのちに再開すると、binlogが転送されるのでcloudstatsコマンドで統計が見れます。

Component - cloudoptimizer
------------------------------------------------------------
           Number of connections:                    1
              Original data size:            111.96 MB
           Transferred data size:             52.44 MB

            Bandwidth Saving:             59.51 MB (53.2%)

53.2%の転送容量削減!

結論

以上を組み合わせれば、新たなCDP候補である「リージョン間レプリケーションパターン (Cross-Region Replication Pattern)」が実現できます。

  • 巨大データをリージョン間で転送するにはtsunami udpが有効そう
  • リージョン間での差分バックアップやレプリケーション向けにはcloudoptで高速化
  • 普通のHTTP通信とかも使えるかも
  • s3へのアップロード高速化も対応しているのでいずれ試してみたい

AWS SSD (hi1.4xlarge) vs Fusion-IOでのMySQLベンチマーク

(※ 追記しました - 5/19/13)

巷ではMySQL 5.6 GAが出て騒がしいですが、ちょっと前に5.5系でAWSのSSDインスタンス(hi1.4xlarge)に載せ替える案件があったので、その時に取ったベンチマークを公表します。以前Fusion-IO (ioDrive Duo)でも同じようにやったので、比較になれば。

経緯

  • あるウェブサービスのDBサイズが巨大でm2.4xlargeでも辛くなってきている
  • アクセスパターンによりパーティショニングが効かない
  • シャーディングをするにはアプリ改修が大変
  • 数週間後に急激なアクセスが予想され、時間的余裕がない!
  • データサイズの急激な増加によりbuffer poolから溢れ、ディスクアクセスのさらなる発生が懸念

というわけで、時間がないのでSSDへの移行を検討し、ベンチマークを取りました。

ベンチマーク

buffer poolが徐々に足りなくなった場合のディスクアクセスの発生をシミュレート

  • sysbenchのoltpモード
  • データサイズは12G(5000万件)
  • readonly
  • uniform(フルスキャン)

主要my.cnfパラメータ

 sync_binlog = 0
 innodb_buffer_pool_size = XXG
 innodb_flush_method = O_DIRECT
 innodb_flush_log_at_trx_commit = 1
 innodb_file_per_table
 innodb_io_capacity = 2000

コマンド

 time sysbench --test=oltp --oltp-table-size=50000000 --db-driver=mysql --mysql-user=root prepare                                                                                                         
 time sysbench --test=oltp --oltp-table-size=50000000 --db-driver=mysql --mysql-user=root --num-threads=16 --max-requests=0 --max-time=180 --init-rng=on --oltp-read-only=on --oltp-dist-type=uniform 2>&1 run                                                                                                         

結果

トランザクション推移

レスポンスタイム推移

Fusion-IOと比べて半分ぐらい。ioDrvie Duoの公称IOPSが250,000+ IOPSでhi1.4xlargeが120,000 IOPSなので、まあ合致しますね。

まだ整理されてないベンチマーク結果があるので、後ほど追記しようと思います。

追記  

先日(5/17/13)のcloudpack night #6 - Re:Generate -でADSJの荒木さんの発表でMySQLのパフォーマンスの話があったのでtpcc-mysqlでベンチマークを取った資料を思い出し追記しました。

  • 500 warehouses (50GBぐらい)
  • 24GB Buffer pool
  • 16スレッド
  • 1時間実行

コマンド(ロードはかなり時間がかかるので注意)

 tpcc_load localhost tpcc root "" 500
 tpcc_start -d tpcc -u root -p "" -w 500 -c 16 -r 300 -l 3600

hi1.4xlargeの方が安定するまで少し時間がかかってます。
以下、安定化しだした900sあたりからの数値です。

Fusion-IO hi1.4xlarge
total 2288758 1444417
avg 8445.6 5330.0
stddev 245.7 132.8

Fusion-IOに比べて6割ってところでしょうか。 今回はSSDのephemeral disk1本で試したので、RAID0にするともうちょっと違ってくると思います。

非ELBなAutoscalingによる自動復旧

バッチ処理等、サーバの冗長化が難しく仕方なく1台で動かさざるを得ない場合があります。でも可用性は確保したい。また、Pacemakerやkeepalived等できなくはないが、お金もあんまりかけられない場合もあります。 そんな時にAWS上でよく使うのがAutoscalingによる1台構成です。最低台数・最大台数共に「1」に設定しておけばEC2インスタンスが壊れても自動的に新しいのにリプレースされて復旧されます。

しかし、Autoscalingのhealth-check-typeを「EC2」にした場合、インスタンスの起動状態(running, stopped)しか見てくれないので、今までこの構成を実現するのにELBによる死活監視が必要でした。インスタンスがHTTPサーバじゃない場合、ちょっとムダです。

ところが、ちょっと前にAutoscalingがインスタンスの健康状態をチェックするEC2 status checkに対応し、ELBが不要になったはずなので試してみました。

今回は各種AWSサービスに対応した統合されたPython版のAWS CLIツールを使います。

セットアップ

まずは、ツールのインストール

sudo apt-get install python-pip
sudo pip install awscli
complete -C aws_completer aws

Autoscaling設定

Launch Configの設定

aws autoscaling create-launch-configuration --image-id ami-4a12aa4b \
--launch-configuration-name test-lc --instance-type t1.micro --key-name ijin-tokyo \
--security-groups test --iam-instance-profile test_iam

{
    "ResponseMetadata": {
        "RequestId": "c0e66974-7103-11e2-9780-a53199bac60e"
    }
}

Scaling Groupの設定

aws autoscaling create-auto-scaling-group --auto-scaling-group-name test-sg \
--launch-configuration-name test-lc --min-size 1 --max-size 1 \
--health-check-grace-period 180 --tags '{"key":"Name", "value":"as-test"}' \
'{"key":"Use Case", "value":"test"}' --availability-zones ap-northeast-1a --health-check-type "EC2"

{
    "ResponseMetadata": {
        "RequestId": "e3808ef3-7103-11e2-9780-a53199bac60e"
    }
}

通知

aws autoscaling put-notification-configuration --auto-scaling-group-name test-sg \
--topic-arn arn:aws:sns:ap-northeast-1:521026608000:test \
--notification-types autoscaling:EC2_INSTANCE_LAUNCH autoscaling:EC2_INSTANCE_TERMINATE \
autoscaling:EC2_INSTANCE_LAUNCH_ERROR autoscaling:EC2_INSTANCE_TERMINATE_ERROR

{
    "ResponseMetadata": {
        "RequestId": "f68359be-7103-11e2-9a1a-5f77b12b596e"
    }
}

レスポンスがjsonなのが良いですね。 また、Auto Scaling Command Line Toolと違って、tagでスペースが使えるようになったのが素晴らしい!

以上の設定でAutoscalingによってインスタンスが1台立ち上がります。

自動復旧

最後にインスタンス不調(status check failure)をシミュレートする為にインスタンス内からネットワークを落とします。

ubuntu@ip-10-128-25-25:~$ sudo ifdown eth0

これでstatus checkがfailし、Autoscalingが自動的に新しいインスタンスと取り替えてくれるはず!

と、期待して待っていたらなかなかアラートメールが来ません。。

設定間違ったかなーっていろいろ見直していたら20分経った頃にやっと到着。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Service: AWS Auto Scaling
Time: 2013-02-07T09:17:42.304Z
RequestId: f395660b-4847-4415-ad33-f8cc5091bdb3
Event: autoscaling:EC2_INSTANCE_TERMINATE
AccountId: 521026608000
AutoScalingGroupName: test-sg
AutoScalingGroupARN: arn:aws:autoscaling:ap-northeast-1:521026608000:autoScalingGroup:a036877b-dab7-4e5d-a6e1-1d3424b20d14:autoScalingGroupName/test-sg
ActivityId: f395660b-4837-4415-ad33-f8cc5071bdb3
Description: Terminating EC2 instance: i-15fadd17
Cause: At 2013-02-07T09:16:57Z an instance was taken out of service in response to a system health-check.
StartTime: 2013-02-07T09:16:57.389Z
EndTime: 2013-02-07T09:17:42.304Z
StatusCode: InProgress
StatusMessage:
Progress: 50
EC2InstanceId: i-15fadd17
Details: {}

うーん。動く事は動いたけど、ちょっと時間がかかるなぁ。

この後何回か試してみたけど、Autoscaling発動までどれも20分ぐらいかかりました。

結論

20分程サーバダウンが許容できるようなゆるめの条件に限定した場合には非ELBでも使えるかな。まあ、それでも適用する場面は多々あるとは思いますが。(早める方法あるのかなー。)

Auto Scalingの設定とデプロイ方法

CDP Advent Calendar 2012に登録しました。ここ1年ちょいで使い慣れてきたパターンがあり、作った当時はクラウドデザインパターンはなかったのですが、Clone ServerScale Outパターンの組合せに当てはまると思うので紹介します。ちなみにアプリはRails。

常にデプロイして更新し続けるシステムを手動、あるいは自動スケールアウトする時に便利な手法だったりします。

図にするとこんな感じですかね。

Auto Scaling設定

Launch Config

まず、ベースとなるAMIの起動インスタンスサイズやセキュリティーグループを定義したLaunch Configを設定。

as-create-launch-config cdp-lc --image-id ami-22a51d23 --instance-type m1.small \
--group cdp_web, cdp_admin

Scaling Group

次に、Scaling Groupで適用するAvailability Zone、インスタンス数の最小・最大閾値設定、紐尽くELB、死活監視方法・開始待ち時間、タグ等を定義。(ec2addtagではスペース入りのキーを設定できるのにas-create-auto-scaling-groupではできないので注意。早く直して。。)

as-create-auto-scaling-group cdp-sg --launch-configuration cdp-lc \
--availability-zones ap-northeast-1a  --min-size 1 --max-size 9 --load-balancers cdp \
--health-check-type ELB --grace-period 300 --tag "k=Name, v=web.cdp" --tag "k=Use_Case, v=Test"

Scaling Out Policy

Scaling Outのポリシー設定。以下の例ではインスタンス1台を追加し、次のスケールアクションまでは5分間待機。ポリシーのARN (Amazon Resource Name)が出力されるので控えます。

as-put-scaling-policy CDPOut --auto-scaling-group cdp-sg --adjustment 1 --type ChangeInCapacity \
--cooldown 300

> arn:aws:autoscaling:ap-northeast-1:494850320039:scalingPolicy:d0d4dcf1-fb44-428a-a19c-38946633acf5:autoScalingGroupName/cdp-sg:policyName/CDPOut

Cloudwatchトリガー(Scaling Out)

Cloudwatchで閾値を超えたらスケールアウトするように設定。以下の例では対象Scaling Group配下のインスタンス達のCPUが1分間で平均75%以上で推移した場合、先ほど設定したScaling OutポリシーがARN経由で実行されます。また、閾値を超過あるいは下回った場合にSNS経由でアラートメールを飛ばします。

mon-put-metric-alarm  CDPHighCPUAlarm --comparison-operator GreaterThanThreshold \
--evaluation-periods 1 --metric-name CPUUtilization --namespace "AWS/EC2" --period 60 \
--statistic Average --threshold 75 --dimensions "AutoScalingGroupName=cdp-sg" \
--ok-actions arn:aws:sns:ap-northeast-1:494850320039:cdp-alert --alarm-actions \
arn:aws:sns:ap-northeast-1:494850320039:cdp-alert, arn:aws:autoscaling:ap-northeast-1:494850320039:scalingPolicy:d0d4dcf1-fb44-428a-a19c-38946633acf5:autoScalingGroupName/cdp-sg:policyName/CDPOut

Scaling Down Policy

同じくScaling Downのポリシー設定。インスタンスが起動すると最低1時間は課金されるので頻繁に伸縮するともったいないのでスケールダウンアクションはゆっくりやるのがポイントです。

as-put-scaling-policy CDPDown --auto-scaling-group cdp-sg --adjustment=-1 \
--type ChangeInCapacity --cooldown 1500

> arn:aws:autoscaling:ap-northeast-1:494850320039:scalingPolicy:de53fbd5-130c-46a8-bf47-25e29f7d358e:autoScalingGroupName/cdp-sg:policyName/CDPDown

Cloudwatchトリガー(Scaling Down)

スケールダウンのトリガーは平均CPUが35%を25分間下回った場合に実行されます。この閾値周辺のアラートメールはいらないので設定してません。

mon-put-metric-alarm CDPLowCPUAlarm --comparison-operator LessThanThreshold \
--evaluation-periods 1 --metric-name CPUUtilization --namespace "AWS/EC2" --period 1500 \
--statistic Average --threshold 35 --dimensions "AutoScalingGroupName=cdp-sg" --alarm-actions \
arn:aws:autoscaling:ap-northeast-1:494850320039:scalingPolicy:de53fbd5-130c-46a8-bf47-25e29f7d358e:autoScalingGroupName/cdp-sg:policyName/CDPDown

通知

最後にスケールアクション時に通知が飛ぶように設定。例えば、あるインスタンスが不調により反応がなく、ポリシーによってリプレースされる場合に飛んだりします。

as-put-notification-configuration cdp-sg -t arn:aws:sns:ap-northeast-1:494850320039:cdp-alert \
-n autoscaling:EC2_INSTANCE_LAUNCH, autoscaling:EC2_INSTANCE_TERMINATE, \
autoscaling:EC2_INSTANCE_LAUNCH_ERROR, autoscaling:EC2_INSTANCE_TERMINATE_ERROR

APP側の設定

ソースコード同期

同期には起動時にupstart経由でrsyncを叩いて管理サーバから最新ソースを取ってきてサービスを再起動します。転送時の圧縮モードはarcfourが一番スループットが出たのでそれに。

Capistrano

デプロイはマスターサーバ(管理兼)上でCapistranoを実行し、ELB配下の生きているインスタンスに対して更新をかけます。deploy.rbに追加する記述は以下の通り。昔はec2 api toolsを直接呼んでパースしたりしてコードが長かったのですが、今はAWS SDK for Rubyがあり、IAM roleでinstance profileを設定すればわずか数行でできてしまいます!

1
2
3
4
5
require 'aws-sdk'
AWS.config(:ec2_endpoint => 'ec2.ap-northeast-1.amazonaws.com', :elb_endpoint => 'elasticloadbalancing.ap-northeast-1.amazonaws.com')
instances = AWS::ELB.new.load_balancers['cdp'].instances.select{|i| i.exists? && i.elb_health[:state] == "InService"}.map(&:public_dns_name)
servers = instances << "localhost"
role :app do servers end

注意点

  • 同期するファイル数が多すぎる(数十万個)と、チェックサム比較で時間がかかってしまい同期が終了する前にAuto Scalingの死活監視によってインスタンスがterminateされ、またlaunchされterminateされるという恐怖のスケールループ地獄に陥る。。。(これ、制限できないのかな)
  • あらかじめトラフィック増の時間帯が分かっていたらScheduled Actionで多めに設定しておいて自動スケールダウンさせた方が吉。
  • マスタサーバがSPOFとなりうるので冗長化する、もしくはrsyncよりs3経由にした方が良い。
  • 急激なトラフィック増ではELB自体のスケールが追いつかない場合も。

ISUCON2に参加してきた

開催2年目となるISUCON (Iikanjini Speed Up CONTEST)に参加してきました。

結果は3位。

事前準備

@cads@fruweに声をかけ、「Mr. Frank & Co.」という米西独チームを編成。 去年のお題を見ると、簡単なブログサイト。実装はphp, python, perl, ruby, node.jsのいずれか。

  • キャッシュ機構入れるんだったら、慣れてるrubyで実装できるようにrailsでスケルトンプロジェクトをgithubに作っておいてみんなで共有。
  • VarnishやResque/Sidekiq, Redisの復習をしてコード追加
  • 試しに他のrubyバージョンのインストール
  • 後はテスト用のec2インスタンスの用意。

お題発表

「NHN48」と「はだいろクローバーZ」のチケット販売システムw。 お、似たようなシステム前作った事あるからいけるかな?セッション対応でもESI使えるからアドバンテージがあるかもと思ってみる。

前半戦

まずは、いつものようにバックアップして戻れるように、gitプロジェクトにも追加。

チームメイト2人にはソースコードを解析をしてもらいつつ、自分は現在の環境の把握とベンチマークを動かすところに集中。どうやら公開されている去年の構成と似た感じでsupervisorによって指定言語のwebアプリが動いている模様。

ひとまず指標となるベースが必要なので、デフォルトで動いているperl版で計ったら大体こんな感じでした。(Scoreが低い方が良い)

Score:533093
Tickets:922

webとdbの負荷が両方高い事を確認。

SQLを見たらCOUNTしてるので、slot分割したcounterテーブルかredisを使えば速くなるよね、と指摘。フロントにキャッシュ入れたいので普段使っているVarnishサポートのlacquer gemがあるので、二人はRailsでの再実装を開始。

ちなみにアクセス分布はこんな感じでした。

perl版のスコアは分かったので、sinatraフレームワークを使ったruby版に変えてベンチマークを取ってみると、

Score:10755528
Tickets:457

性能は約半分。遅いのは分かっていたけど、ジョブワーカーとキャッシュを使えば、対応コードの入ったRailsでもなんとかなるという思いで特に意に介さず。

ただ、unicornが50プロセスもあったので、さすがにこれは多いのでベンチをしつつ、いろいろ調整。 結局スコアが一番高かった10プロセスに落ち着く。ついでにrubyのGCを切ったりしたけど、目立った効果はなし。 まだ再実装中で時間もあり、そういえば速いというrubiniusにしたらどうなんだろうと思い、pumaも入れてみたけどmysql2 gemが動かず断念。

DBのmy.cnfも見たけど、データ量の割には十分なパラメータだったので、commit毎のflushを変えたぐらい。

innodb_flush_log_at_trx_commit = 0

また、いつでも使えるようにと、VarnishとRedisもインストールしておく。

次にデプロイ用のテストができるようにテスト用サーバに環境を用意しつつ、capistrano対応。

後半戦

再実装はブラウザからの見た目上は前半でほぼ出来上がっていて、後はベンチを通るかを確認して最適化を始めるというシナリオでした。 ところが、全然通らずここからハマることに。。

表示エラーなのか遅いからなのか、原因が掴めず四苦八苦。 ベンチマークアプリは未公開なのでエラー内容を運営側に聞きつつ、アプリを修正。

その間、リバースプロクシをapacheからnginxに変えてimages/css/js等のstatic contentを直接配信するようにしたけど、あまり効果がなかったので元に戻す。 アプリ修正の度にベンチを走らせるので、別の作業で最適化の確認をとるのが大変でした。チーム内でcontext switchingの問題が発生!

そうこうしていると時間がどんどん流れ、残り1時間を切ったところでRailsを捨てる決断をする。 せめてキャッシュ入れて最適化しよういう事でVarnishを設定し始める。 そうするとスコアはぐんぐん伸びて行き、上位に食い込む。

Score:180724

さらにESIを使えばもう一段跳ね上がるかもという思いでsinatraにコードを追加してテスト。 しかし、圧倒的に時間が足りず最後の1分半まで粘ったが断念。 最後に再起同時のアプリ起動ができる確認をして終了。

最終スコアは以下の通りでした。

Score:216868

正直ダメかなと思っていたけど、3位という結果に驚きました。 チューニングが実質1時間未満の割にはまずまずかも知れないけど、残念感の方が強かったですね。

反省点

方向性の転換をもっと早く見極めるべきでした。後ちょっと、後ちょっとの思いでずるずる再実装にこだわったのが良くなくて、捨てる勇気を持てたらと。ジョブワーカーやRedis対応の案(とコード)はあったので後半戦直後にやっていたら優勝の可能性はあったかも。。。後の祭りですが。

ともあれ、非常に楽しかったのでまた来年参加してみたいと思います。 運営したNHNの方々、ありがとうございます&お疲れ様でした!

おまけ

帰ってから@netmarkjpさんと平行して#soloconやってました。 直後に公開されたnode.jsで書かれたベンチマークが動かなくてもいろいろ修正したけど、良い勉強になりました。(今は対応版が再アップ済み) chckerがrails版に対して失敗していたような箇所を見つけたので後日検証してみようと思います。

第4回チューニンガソン(Tuningathon)で優勝してきた

3回目の参戦となる#tuningathon@tnmtさんと共に優勝してきました。

やった事は相方のブログに書かれているので、補足。

開演前

朝起きると、やたらとアラートが飛んでいるので調べると、うるう秒のせいでサーバ達が高負荷状態に。 @tnmtさんも同じ原因で障害対応中で待ち合わせ時間には間に合わず、参加が危うい感じ。 自分の方はなんとか片付けて、ぎりぎり開演前に到着。

お題発表

前々からやって欲しかったRuby on Rails! で、Refinery CMSというブログのチューニング。
内心喜びました。

前半戦

作業開始前にはまず何よりもバックアップ。 いつでも環境を戻せるようにRailsのフォルダをコピーしてMySQLのdumpを取っておく。

速攻でrbenv + ruby-buildを入れて、rubyの最新バージョン(1.9.3-p194)をインストール。 ビルドの間、まずは環境把握。システムのパッケージ版ruby 1.8.7が入っている事を確認して、 設定フォルダ(tuningathon/config)配下をさらっと目を通す。 my.cnfも見て全然パラメータ設定されてないけど/var/lib/mysqlの容量をチェックしたらほぼ空なので、 ここのチューニングはあまり効果が望めずと判断。やるなら後回し。

ビルドが終えた頃にデフォルト状態のままで動かして、まずベースとなるベンチマーク。 大体こんな感じでした。

topで見ると完全にrubyがCPUを専有しているので、まずそこから着手することに。

次にdevelopment.rbのキャッシュ周りのパラメータをいじる。 development modeだとソースの変更が即時反映・確認できるよう、毎回クラスをロードしているので遅いんです。

config.cache_classes = true
config.action_controller.perform_caching = true

これでだけでスコアが倍に。

Score: 5.255 (get=2.900, comment=2.355(3), check=1.000)

次にruby 1.9.3-p194で動かして計測し、6超え。

Score: 6.102 (get=3.600, comment=2.502(3), check=1.000)

この頃にやっと相方が合流し、予めGoogle Docsに書いてあった作業ログを共有。 unicornへの置き換えを試しみる事に合意。 設定ファイルを作ってもらっている最中に、jrubyへの置き換えを試すが、 制約上Gemfileの変更が不可だった為、mysql2のgemを外す事ができずDB接続ができなかったので断念。

unicorn単体で動かすと9ぐらい出たので、worker数を変えたり、 unicorn_rails gemからRails 3で推奨されている素のunicorn gemを試したりして、16-21。

Score: 16.205 (get=9.900, comment=6.305(7), check=1.000)
Score: 19.869 (get=13.000, comment=6.869(7), check=1.000)
Score: 21.812 (get=14.000, comment=7.812(8), check=1.000)

ベンチを取り続けると、commentがどんどん溜まって行くので viewのrender時間が増えてスコアが落ちたりしたので、毎回DBを初期化したのとRailsを再起動する事によってスコアが安定しだす。

最後にrubyのガベージコレクションを切ったりして、この時点での自己ベストがこんな感じでした。

Score: 29.733 (get=19.000, comment=10.733(11), check=1.000)

後半戦

rubyで出来そうなところは一通りやったので、次は前々から構想していた Railsの前段にReverse Proxyを置く事にチャレンジ。Varnishを入れて設定し始める。

ちょうどこの時期にTL上で@netmarkjpさんが1000超えのスコアを呟きはじめるので、 なんらかのReverse Proxyを使ったんだと確信。

実はこの時、1000超えのスコアはこちらでも出てたけど、たまに1とか0が出る不安定さだったので、 呟きませんでした。(運営側に計測されなかったのは別port<Varnishデフォルトの6081番>で試していたから)

後は黙々と設定をいじりつつ、相方に念の為DBのテーブルにインデックスを張ってもらったり、 ベンチマークスクリプトを解析してもらって、適切な設定を探るべく試行錯誤な感じ。

16時からの公式計測 段階ではまだスコアが安定してなかったので、ひとまず入賞が確実なrubyオンリーな構成で 2〜3回来るのを確認。その後、終了20分前になんとか安定したので一気にportを置き換えて、 DBを初期化し、railsを再起動して待ちかまえる。次の計測でそれまでトップだった@netmarkjpさんを逆転したようです。

まだ若干時間が余っていたので、Varnishのスレッド数等をいじったりして、17時終了の30秒前に設定を入れて再起動。 後から知ったのですが、この設定がどうやら効いたようで1番最後の計測でさらにスコアが跳ねた模様。 最終的なスコアは「1351.54」でした。

ちなみにローカル計測ベスト(3200.23)だとこんな感じだったけど、リモート経由だとネットワーク(?)がボトルネックになって そこまで出ませんでした。

varnishの設定ファイルはこんな感じ。 TTLを強制有効にして、POST後にいかにキャッシュクリアさせるかがポイント。 じゃないと、チェックの成功率の2乗でペナルティが課され、すごい勢いでスコアが減点されます。

/etc/sysconfig/varnish


/etc/varnish/default.vcl

世に出てるの設定例はVarnish 2系が多いので、3系はsyntaxが違うので注意。 コメントアウトされたコードはデフォルト挙動なので、記述されたのとマージされます。 @netmarkjpさんの設定はvcl_fetch()内でキャッシュクリアしてるけど、 Varnishのフロー的には 一番最初に呼ばれるvcl_recv()内でやった方がより効果的です。

まとめ

  • Railsは比較的遅いので、いかにヒットさせないかを考える
  • でもRubyのチューニングも重要。多分これが2位との差
  • ベンチマークスクリプトをよく読んで動きやスコアリングを把握する
  • 運営側はチャレンジングな事は先にやっておけと推奨していたけど、まず堅実なチューニングをして最後の冒険が良いかと。
  • 最後まで諦めない気持ち

おまけ

翌日、出社したら何故か優勝記念と称してMountain Dewが10本贈呈されてましたw

tuningathon優勝記念

最後に運営の皆様、ありがとうございました!