#!/bin/sh SOURCE=ec2-api-tools.zip SPEC=ec2-api-tools.spec DESTDIR=/usr wget -q http://s3.amazonaws.com/ec2-downloads/$SOURCE -O $SOURCE VERSION=`unzip -l ec2-api-tools.zip | awk '/ec2-api-tools-/{print $4; exit}' | sed -r 's|ec2-api-tools-([^/]+).*|\1|'` cat <<EOF > $SPEC Summary: Amazon EC2 API Tools Name: ec2-api-tools Version: $VERSION Release: 1 License: Amazon Software License Group: Development/Tools Source: ec2-api-tools.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root BuildArch: noarch %define destdir $DESTDIR %define _unpackaged_files_terminate_build 0 %description The API tools serve as the client interface to the Amazon EC2 web service. Use these tools to register and launch instances, manipulate security groups, and more. %prep %setup -q %install install -d \$RPM_BUILD_ROOT/%{destdir}/bin install -d \$RPM_BUILD_ROOT/%{destdir}/lib install -m 755 bin/* \$RPM_BUILD_ROOT/%{destdir}/bin/ install -m 644 lib/* \$RPM_BUILD_ROOT/%{destdir}/lib/ %files EOF unzip -l $SOURCE | awk '$4 ~ /ec2-api-tools-.+\/(bin|lib)\/.+/ && $4 !~ /\.cmd/{sub(/[^/]+/,"'$DESTDIR'",$4); print $4}' >> $SPEC
重い処理はバックグラウンドに回すのが正解なのかな。
基本的にmutableなオブジェクトの共有はNGな気がするな。
#!/usr/bin/env ruby require 'rubygems' require 'eventmachine' class HTTP < EM::Connection def receive_data(data) operation = lambda do # 重い処理、ごにょごにょ… (<<-EOS).gsub(/\r?\n/,"\r\n") HTTP/1.1 200 OK <html> <head> <title>test</title> </head> <body> test </body </html> EOS end # operation callback = lambda do |res| send_data(res) close_connection_after_writing end EM.defer(operation, callback) end # receive_data end EM.run do EM.start_server('0.0.0.0', 80, HTTP) end
あ、さっきの嘘だ。
#!/usr/bin/env ruby require 'rubygems' require 'eventmachine' class Backend < EM::Connection def initialize(proxy) @proxy = proxy end def receive_data(data) @proxy.send_data(data) end end class Proxy < EM::Connection attr_accessor :backend def receive_data(data) @backend.send_data(data) end end backends = [ ['www.yahoo.co.jp', 80], ['www.hatena.ne.jp', 80], ] EM.run do EM.start_server('0.0.0.0', 80, Proxy) do |srv| host, port = head = backends.shift backends.push(head) srv.backend = EM.connect(host, port, Backend, srv) end end
shell1> ruby lb.rb
shell2> for i in {1..5}; do wget -q -O- 127.0.0.1 | nkf | fgrep '<title>'; done
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>
shell2> for i in {1..5}; do wget -q -O- 127.0.0.1 | nkf | fgrep '<title>'; done
<title>Yahoo! JAPAN</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>
同期の問題は大変デスネー
Hostヘッダ渡さなくてもレスポンス返してくれたのがちょっと意外だった。
#!/usr/bin/env ruby require 'rubygems'a require 'eventmachine' class Backend < EM::Connection def initialize(proxy) @proxy = proxy end def receive_data(data) @proxy.send_data(data) end end class Proxy < EM::Connection def initialize(backends) # コネクション毎の生成なので、このタイミングでバックエンドを決める host, port = backends[rand(backends.length)] @backend = EM.connect(host, port, Backend, self) # プロセス内でグローバルなラウンドロビンをやろうと思うと # プールの同期が必要になって、パフォーマンスが低下する気がする # # "以前に言及したように、接続と接続の間で君のクラスのインスタンスはどんな情報も共有できない。 # 幸運にも、いや、設計されたことだろうが、EventMachine はこれを処理するメカニズムを提供する。" # http://keijinsonyaban.blogspot.com/2010/12/eventmachine.html # # 共有…まじめに同期しようと思うと大変な気がするなー # mutableなオブジェクトについてのソリューションもあるのかしら # # バックエンドのコネクションもプーリングできたらよいけど # EventMachineのパターンと相性悪いような… # まあProxyへの接続が維持されている間はコネクションも維持されるので # そんなに問題ではないのかも end def receive_data(data) @backend.send_data(data) end end EM.run do # 順序が大事!なのでHashは×(たぶん) EM.start_server('0.0.0.0', 80, Proxy, [ ['www.yahoo.co.jp', 80], ['www.hatena.ne.jp', 80], ]) end
shell1> ruby lb.rb
shell2> for i in {1..5}
> do
> wget -q -O- 127.0.0.1 | nkf | fgrep '<title>'
> done
<title>はてな</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>
Murakumo 0.4.5をリリースしました。
https://rubygems.org/gems/murakumo/versions/0.4.5
今回はKeepalived/Heartbeatのような冗長化機能を追加しました。
アクティブなサーバのMySQLが落ちたとき・サーバ自体が落ちたときに、バックアップ側にENIを付け替える例です。

server-01にENIをアタッチして、ルーティングテーブルを設定します。
[root@server-01 ~]# route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 169.254.169.254 * 255.255.255.255 UH 0 0 0 eth0 10.0.0.0 * 255.255.255.0 U 0 0 0 eth0 10.0.0.0 * 255.255.255.0 U 0 0 0 eth1 default 10.0.0.232 0.0.0.0 UG 0 0 0 eth0
[root@server-02 ~]# route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 169.254.169.254 * 255.255.255.255 UH 0 0 0 eth0 10.0.0.0 * 255.255.255.0 U 0 0 0 eth0 default 10.0.0.232 0.0.0.0 UG 0 0 0 eth0
※169.254.169.254を設定しておかないとメタ情報がとれなくてはまります
yum install ruby-devel sqlite-devel make gcc-c++ gem install murakumo
yum install mysql-devel mysql-server gem install mysql
Murakumoの設定はこんな感じ。
--- address: 0.0.0.0 port: 53 auth-key: onion log-level: debug resolver: 10.0.0.2, 8.8.8.8 max-ip-num: 8 domain: ap-northeast-1.compute.internal init-script: /etc/murakumo-init.rb addr-includes: ^10\..* notification: host: 127.0.0.1 sender: sender@mail.from recipients: - recipient@mail.from # alias hostname, ttl, master/secondary/backup, weight alias: - mysql-server,60,master,100 health-check: mysql-server: interval: 5 timeout: 5 healthy: 2 unhealthy: 2 script: | mysql_check 'root' # hook of activation of backup/secondary activity-check: mysql-server: start-delay: 60 interval: 10 active: 2 inactive: 2 on-activate: /usr/local/sbin/attach_if
murakumo-init.rbでサーバ情報の取得するので、どちらのサーバも内容は同じです。
Aliasは「mysql-server」。
murakumo-init.rbはこんな感じ。
AWS_ACCESS_KEY_ID = '...' AWS_SECRET_ACCESS_KEY = '...' REGION = 'ap-northeast-1' # get self ip address ip_addr = Murakumo::Util.self_ip_address # get hostname tags = Murakumo::Util.ec2_tags(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, REGION) hostname = tags['Name'] || `curl -s http://169.254.169.254/latest/meta-data/local-hostname` # rewrite host option @options['host'] = "#{ip_addr}, #{hostname}" # get instances ip_addrs = Murakumo::Util::ec2_private_ip_addresses(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, REGION) # rewrite initial-nodes nodes = ip_addrs.select {|inst_id, ip_addr, status| status == 'running' }.map {|inst_id, ip_addr, status| ip_addr } @options['initial-nodes'] = nodes.join(',') unless nodes.empty?
それからENIをフェイルオーバーするスクリプトがこんな感じ。
#!/bin/sh AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... REGION=ap-northeast-1 IF_ID=eni-... /usr/bin/murakumo-attach-ec2-attach-interface \ -k "$AWS_ACCESS_KEY_ID" -s "$AWS_SECRET_ACCESS_KEY" \ -r $REGION -n $IF_ID 2>&1 | logger exit 0
最後にserver-01/02でMySQLとMurakumoを起動します。
[root@server-01 ~]# /etc/init.d/mysqld start Starting mysqld: [ OK ] [root@server-01 ~]# /etc/init.d/murakumo start Starting daemon... Waiting for daemon to start... Daemon status: running pid=15143
初期状態ではserver-01にENIが刺さっています。
[root@server-01 ~]# ifconfig | grep ^e eth0 Link encap:Ethernet HWaddr 02:40:B5:1B:3F:9A eth1 Link encap:Ethernet HWaddr 02:40:B5:12:49:35
[root@server-02 ~]# ifconfig | grep ^e eth0 Link encap:Ethernet HWaddr 02:40:B5:37:0B:C0
Murakumoのステータスはこんな感じです。
[root@server-01 ~]# mrkmctl -L IP address TTL Priority Weight Activity Hostname --------------- ------ --------- ------ -------- ---------- 10.0.0.133 60 Master 100 Active mysql-server 10.0.0.133 60 Origin - Active server-01 10.0.0.55 60 Backup 100 Active mysql-server 10.0.0.55 60 Origin - Active server-02
まず、server-01のmysqlを落としてみると…
[root@server-01 ~]# tail /var/log/murakumo.log I, [2012-01-22T12:27:25.606223 #15143] INFO -- : health condition changed: mysql-server: unhealthy I, [2012-01-22T12:27:25.722457 #15143] INFO -- : sent the notice: unhealthy I, [2012-01-22T12:27:40.563515 #15143] INFO -- : activity condition changed: mysql-server: inactive I, [2012-01-22T12:27:40.640112 #15143] INFO -- : sent the notice: inactive [root@server-01 ~]# ifconfig | grep ^e eth0 Link encap:Ethernet HWaddr 02:40:B5:1B:3F:9A
[root@server-02 ~]# tail /var/log/murakumo.log I, [2012-01-22T12:27:42.704476 #13467] INFO -- : activity condition changed: mysql-server: active I, [2012-01-22T12:27:42.820635 #13467] INFO -- : sent the notice: active [root@server-02 ~]# ifconfig | grep ^e eth0 Link encap:Ethernet HWaddr 02:40:B5:37:0B:C0 eth1 Link encap:Ethernet HWaddr 02:40:B5:12:49:35
[root@server-01 ~]# mrkmctl -L IP address TTL Priority Weight Activity Hostname --------------- ------ --------- ------ -------- ---------- 10.0.0.133 60 Master 100 Inactive mysql-server 10.0.0.133 60 Origin - Active server-01 10.0.0.55 60 Backup 100 Active mysql-server 10.0.0.55 60 Origin - Active server-02
server-01がUnhealthyになり、ENIがserver-02にアタッチされます。
[root@server-01 ~]# tail /var/log/murakumo.log I, [2012-01-22T12:31:15.765068 #15143] INFO -- : health condition changed: mysql-server: healthy I, [2012-01-22T12:31:15.842994 #15143] INFO -- : sent the notice: healthy I, [2012-01-22T12:31:30.658938 #15143] INFO -- : activity condition changed: mysql-server: active I, [2012-01-22T12:31:30.739008 #15143] INFO -- : sent the notice: active [root@server-01 ~]# ifconfig | grep ^e eth0 Link encap:Ethernet HWaddr 02:40:B5:1B:3F:9A eth1 Link encap:Ethernet HWaddr 02:40:B5:12:49:35
[root@server-01 ~]# tail /var/log/murakumo.log I, [2012-01-22T12:31:28.776223 #13467] INFO -- : activity condition changed: mysql-server: inactive I, [2012-01-22T12:31:28.855345 #13467] INFO -- : sent the notice: inactive [root@server-02 ~]# ifconfig | grep ^e eth0 Link encap:Ethernet HWaddr 02:40:B5:37:0B:C0
[root@server-01 ~]# mrkmctl -L IP address TTL Priority Weight Activity Hostname --------------- ------ --------- ------ -------- ---------- 10.0.0.133 60 Master 100 Active mysql-server 10.0.0.133 60 Origin - Active server-01 10.0.0.55 60 Backup 100 Active mysql-server 10.0.0.55 60 Origin - Active server-02
今度はserver-01がActiveになり、ENIがserver-01にアタッチされます。
最後にサーバ自体が落ちたものとして、server-01のMurakumoを落としてみます。
[root@server-02 ~]# tail /var/log/murakumo.log I, [2012-01-22T12:36:08.879923 #13467] INFO -- : activity condition changed: mysql-server: active I, [2012-01-22T12:36:08.995964 #13467] INFO -- : sent the notice: active [root@server-02 ~]# ifconfig | grep ^e eth0 Link encap:Ethernet HWaddr 02:40:B5:37:0B:C0 eth1 Link encap:Ethernet HWaddr 02:40:B5:12:49:35
しばらくするとserver-01のダウンが検知されてserver-02がActiveになり、ENIがserver-02に移ります。
…とまあこんな感じで、元々内部DNS用途だったMurakumoですが、Gossipプロトコルがなかなか便利だったので冗長化機能も持たせてみました。非VPCだとENIは使えませんが、そこは本来のDNSによる冗長化を使うということでどうせオワコンだし
Keepalivedはそもそも使えないしL7のチェックはちょっとめんどくさいし、Heartbeatはマルチキャスト使えないしヘルスチェックはやっぱりめんどいし、、、ということでミドルウェアのヘルスチェック込みで手軽にVPCで冗長化したい案件にはちょうどよいと思います。
絶賛、人柱募集中 ぜひぜひご利用ください。
#!/usr/bin/env ruby require 'cgi' require 'base64' require 'net/https' require 'openssl' require 'rexml/document' Net::HTTP.version_1_2 class EC2Client API_VERSION = '2011-12-15' SIGNATURE_VERSION = 2 def initialize(accessKeyId, secretAccessKey, endpoint, algorithm = :SHA256) @accessKeyId = accessKeyId @secretAccessKey = secretAccessKey @endpoint = endpoint @algorithm = algorithm end def query(action, params = {}) params = { :Action => action, :Version => API_VERSION, :Timestamp => Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ'), :SignatureVersion => SIGNATURE_VERSION, :SignatureMethod => "Hmac#{@algorithm}", :AWSAccessKeyId => @accessKeyId, }.merge(params) signature = aws_sign(params) params[:Signature] = signature https = Net::HTTP.new(@endpoint, 443) https.use_ssl = true https.verify_mode = OpenSSL::SSL::VERIFY_NONE https.start do |w| req = Net::HTTP::Post.new('/', 'Host' => @endpoint, 'Content-Type' => 'application/x-www-form-urlencoded' ) req.set_form_data(params) res = w.request(req) res.body end end private def aws_sign(params) params = params.sort_by {|a, b| a.to_s }.map {|k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&') string_to_sign = "POST\n#{@endpoint}\n/\n#{params}" digest = OpenSSL::HMAC.digest(OpenSSL::Digest.const_get(@algorithm).new, @secretAccessKey, string_to_sign) Base64.encode64(digest).gsub("\n", '') end end # EC2Client ACCESS_KEY_ID = '<ACCESS_KEY_ID>' SECRET_ACCESS_KEY = '<SECRET_ACCESS_KEY>' ENDPOINT = 'ec2.ap-northeast-1.amazonaws.com' ec2cli = EC2Client.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT) source = ec2cli.query('DescribeInstanceStatus') doc = REXML::Document.new(source) errors = [] doc.each_element('/DescribeInstanceStatusResponse/instanceStatusSet/item') do |item| eventsSet = item.elements['eventsSet'] next unless eventsSet instance_id = item.text('instanceId') az = item.text('availabilityZone') eventsSet.each_element('item') do |i| code = i.text('code') description = i.text('description') not_after = i.text('notAfter') not_before = i.text('notBefore') errors << "#{instance_id}:#{code}" # 間違い #errors << "#{instance_id}:#{code}" unless description =~ /Completed/ end end unless errors.empty? puts errors.join(', ') exit 2 end puts 'OK'