Hatena::ブログ(Diary)

酒日記 はてな支店 RSSフィード

2014-04-23

HAProxy で MySQL のヘルスチェックをちょっと便利にする

MySQL で slave を複数台立てて参照分散するには、HAProxy を利用してロードバランスと切り離しを行うと手軽に使えて便利です。

option mysql-check という設定で、HAProxy 自身が mysqld に接続してヘルスチェックが可能です。

listen mysql-slave
       bind     127.0.0.1:3307
       mode     tcp
       option   mysql-check user haproxy
       balance  roundrobin
       server   slave1  192.168.1.11 check
       server   slave2  192.168.1.12 check
       server   slave3  192.168.1.13 check

なのですが、この設定だと以下のように少々不便なことがあります。

  • mysqldに接続できるかどうかのみを死活の判断にしているので、レプリケーションが停止しているような場合にも構わず振り分けてしまう
    • 結果、ユーザに古い情報が見え続けてしまう事故が起きうる
  • 作業をするので一時的に特定のホストだけ外したい、という場合に切り離し作業が面倒

ということで、Having HAProxy check mysql status through a xinetd script | Sysbible を参考にして、HTTP経由での死活監視をしてみました。

HTTPリクエストを受けて、localhost の mysqld に接続して 200 / 500 を返すような daemon を実装します。

  • 接続できなければ Status 500
  • SHOW SLAVE STATUS の結果が得られなければ Status 500
  • SHOW SLAVE STATUS の結果を Status 200、JSON で返す

という挙動をします。

#!/usr/bin/env perl
use strict;
use Plack::Runner;
use DBI;
use Try::Tiny;
use JSON;

sub response {
    my ($code, $message) = @_;
    my $content = encode_json({ message => $message });
    my $header = [
        "Content-Type"   => "application/json",
        "Content-Length" => length($content),
    ];
    return [ $code, $header, [ $content ] ];
}

sub app {
    my $env = shift;
    my $result;
    my $res = try {
        my $dbh = DBI->connect(
            'dbi:mysql:database=test;host=127.0.0.1;port=3306',
            'haproxy', '',
            { RaiseError => 1, AutoCommit => 1 },
        );
        $dbh->ping;
        $result = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
        $dbh->disconnect;
        undef;
    }
    catch {
        my $e = $_;
        response 500, $e;
    };
    return $res if defined $res;
    if (ref $result) {
        response 200, $result;
    }
    else {
        response 500, "This mysqld is not a slave.";
    }
}

my $runner = Plack::Runner->new;
$runner->parse_options(@ARGV);
$runner->run(\&app);

これを slave が動作しているホストそれぞれで動作させ、そこに対して HAProxy が HTTP でのヘルスチェックを行うように設定します。死活監視用のユーザには REPLICATION CLIENT の権限を与えておきます。

listen mysql
       bind     127.0.0.1:3307
       mode     tcp
       balance  roundrobin
       option   httpchk
       server   slave1 192.168.1.11:3306 check port 5000
       server   slave2 192.168.1.12:3306 check port 5000
       server   slave3 192.168.1.13:3306 check port 5000

これで接続できなかったり、master になっていたりした場合には HTTP 500 が返るので HAProxy がダウン検知して切り離されます。MHA で slave が master に昇格しても大丈夫。

特定のローカルファイルが存在していたら 503 を返すようにすれば、一時的に切り離したいときには touch で行えます。レプリケーションの状態を見て一定以上遅れていたら切り離しなど、アプリケーションの都合により柔軟に切り離し条件を設定できるので、便利になりますね。

もちろん死活監視用の daemon が落ちたら mysqld が生きていても切り離されてしまうので、万が一落ちても再度起動できるように daemontools 等から起動するとよいかと思います。

2014-03-03

複数の zabbix-agent から取得した値を集約する zabbix-aggregate-agent を書いた

Immutableなインフラがなんやかんやと喧しい今日この頃ですが、インスタンスが頻繁に増えたり減ったりすると、監視サービスで継続的な値を追うのが難しくなるよね、という問題を最近感じています。

サービス全体で複数のホストの合計値を取得しておくことで使用量の推移を見たいような値、具体的には以下のようなものです。

ということで、zabbix-agent への proxy として動作する、複数 agent からの数値を合計して返すようなものがあったら捗るんじゃないかと書いてみました。

公式ページ fujiwara.github.io/zabbix-aggregate-agent

リポジトリ zabbix-aggregate-agent Github

アーキテクチャ

[zabbix-server(or zabbix-proxy)]
    |           ^
    | (1)key    | (6)aggregated value
    |           |
    v           |
 [zabbix-aggregate-agent] <--- (2) list of zabbix-agents from static list or file or command output
    |           ^
    | (3)key    | (4)values(*)
    v           |
  [zabbix-agents(*)]

1. zabbix server(またはproxy) が aggregate-agent に値を要求

2. aggregate-agente はファイルやコマンドから値を集約する対象 agent を解決

    • 解決には、設定ファイルに定義した静的リスト、行区切りのテキストファイル、標準出力に対象を出力するコマンド、が利用できます

3. aggragate-agentは解決した複数の agent に値を並列に要求

4. それぞれの agent は自分の持っている値を返す

5. aggregate-agent が値を集約する

6. server(proxy) に値を返す

Zabbixには「Aggregate checks」という仕組みがあるけど…?

Aggregate checks - Zabbix

それぞれ、利点欠点があります。

Aggregate checks

Pros.

  • 標準機能です

Cons.

  • 集約したいアイテムにそれぞれについて、アイテムとグラフとトリガの定義を行う必要があります
    • Web UIでぽちぽちぽちぽち……
  • 集約したい zabbix-agent はすべて zabbix-server に登録する必要があります

zabbix-agentの自動登録によって登録することは簡単ですが、不要になった情報を別途削除するために別途 Zabbix API を利用する処理を書く必要があります。

zabbix-aggregate-agent

Pros.

  • アイテム、グラフ、トリガの定義は既に agent 向けに定義されたものをそのまま利用できます
    • zabbix-serverから見ると agent そのものに見える
  • 集約される zabbix-agent は、必ずしも zabbix-server 上に定義されている必要はありません
    • たとえばオートスケールで起動して短時間で落ちるような一時的なインスタンスについて aggregate-agent が存在を認知(APIなどで)できれば、zabbix-serverへの登録は不要です

Cons.

  • zabbix-aggregate-agentのdeamonを1プロセス、起動する必要があります

使用方法

Goで書いてあるので、公式ページからバイナリをダウンロードするだけで動作可能です。

$ zabbix-aggregate-agent --config /path/to/config.toml

設定ファイル例は以下のような感じです。

複数の aggregate-agent インスタンスを1プロセスで起動可能です。

zabbix-serverには集約用のホスト定義を行い、Listenするアドレスを zabbix-agent のアドレスとして指定します。

[[agent]]
# Name: 識別子(ログに出力される)
Name = "web_servers"
# Listen: aggregate-agentが Listen する address (default "127.0.0.1:10052")
Listen = "0.0.0.0:10052"
# List : 静的リストによる解決
List = [ "web01:10050", "web02:10050" ]

[[agent]]
Name  = "app_servers"
Listen = "0.0.0.0:10053"
# ListFile: 改行区切りのテキストファイルによる解決
ListFile = "/path/to/agent.list"

[[agent]]
Name = "db_servers"
Listen = "0.0.0.0:10054"
# ListCommand: 標準出力に集約対象を改行区切りで出力するコマンドを指定
ListCommand = [ "/path/to/generate_list.sh", "arg1", "args2" ]
# CacheExpires : コマンド実行結果のキャッシュ保持期間
CacheExpires = 300

で実際どうなの

Aggregate checksでは、

  • すべての監視対象を zabbix-server に登録する
  • 集約対象ごとにホストグループを定義する

必要があるので、一時的に増えたり減ったりするすべてのホストをかっちりzabbix-serverで登録、削除する仕組みがあればこちらを使うほうがよいと思います。

aggregate-agentはファイルやコマンドの実行結果で集約対象を定義できるため、

  • zabbix以外のAPIでホストグループ的なものが解決できる

(つまりzabbix-serverにホストメタデータの管理を全部寄せきれていない場合) には便利に使えるかと思います。

あとは既存の監視対象アイテム、グラフ、トリガの定義をそのまま流用できるのが個人的には嬉しいところで、Web UIでぽちぽち定義をしていく必要がありません。

書いてみて

複数の agent にリクエストを飛ばして受けるところ、複数の aggregate-agent インスタンスを立てるところは Goroutine で簡単に並列化できたので、大変楽ですね。PerlでAnyEventで非同期処理とか、複数の子プロセスをforkしてプロセス間通信するのとは比較する気も起きないぐらいです。

2014-02-24

Redisを使って排他制御するwrapperコマンド Redis-Setlock をPerlとGoで書いた

しばらく前に作って書きそびれていましたが、Yokohama.pm #10 でLTしたのでエントリもあげます。

LTのスライドはこちら ⇒ Redis-Setlockを書いたはなし

なにをするもの?

setlockコマンドのロック処理をRedisサーバで行うもの」です。

setlockはflockを使ってロックを獲得したら引数に渡されたコマンドをexecする、daemontools付属のwrapperコマンドで、cronでコマンド実行するときなど多重実行を制御する場合に重宝します。

flockだとホストをまたいだロック処理が行えないため、その部分をRedisを使った排他制御に置き換えたものを書いてみました。

使い方

$ redis-setlock [-nNxX] KEY program [ arg ... ]
$ go-redis-setlock [-nNxX] KEY program [ arg ... ]

--redis (Default: 127.0.0.1:6379): redis-host:redis-port
--expires (Default: 86400): The lock will be auto-released after the expire time is reached.
--keep: Keep the lock after invoked command exited.
-n: No delay. If KEY is locked by another process, redis-setlock gives up.
-N: (Default.) Delay. If KEY is locked by another process, redis-setlock waits until it can obtain a new lock.
-x: If KEY is locked, redis-setlock exits zero.
-X: (Default.) If KEY is locked, redis-setlock prints an error message and exits nonzero.

オプションと使い方は基本的に setlock と同じで、使用する Redis サーバ、ロックを自動開放するための expire、終了してもロックを(expireまで)開放しないというオプションが追加されています。

実装

Redisでの排他制御は 公式ドキュメントに書いてあるものをそのまま使用しています

SET に NX フラグを付けると、「すでにKeyが存在しない場合のみSETが成功する」という動作をするためにロックに使えるんですね。

setlockはロックを獲得したらそのまま単にexecveすることで、プロセス終了時にロックが解放されるという素敵な実装なのですが、Redisを使う場合は自分でRedisサーバにコマンドを発行して開放処理を行う必要があるため、execではなく子プロセスをforkしています。

その関係で、(Go版での) 標準入出力の扱いやシグナルハンドリングなど、いろいろ泥臭いコードが増えてしまっているのが美しくないところです……

Go版はクロスコンパイル可能なので、本来はWindows版も作れるはずなのですが、子プロセスの終了コードを取得する処理でWindowsだと動かないコードがあるらしく、コンパイルできませんでした。自分では使わないからまあいいか、とそのままになっています。

Go版は http://fujiwara.github.io/go-redis-setlock/ からバイナリをダウンロードするだけで動くので deploy がお手軽だと思います。

Perl版にはライブラリとして使用できるインターフェースも用意してあります。

my $redis = Redis->new( server => 'redis.example.com:6379' );
if ( my $guard = Redis::Setlock->lock_guard($redis, "key", 60) ) {
   # got a lock!
   ...
   # unlock at guard destroyed.
}
else {
   # could not get lock
}

複数ホスト間で気軽に排他制御できるので、是非お試しください。

2014-01-29

nginx で gzip_static と gunzip を使ってストレージを節約する

一月ほど前に 社内Gyazoの画像をAmazon S3に逃がしてスケーラブルに運用する - 酒日記 はてな支店 というエントリを書いて一段落と思いきや、そのサーバには社内向けの nopaste アプリも同居しており、気がつけばテキストファイルが10GB以上積もっていたのでした…

社内 nopaste アプリの実装はDBなどを使用せず単にテキストファイルを保存しているだけだったので、ファイルを gzip して nginx の http_gzip_static_module を使って配信したらディスクを節約できていいんじゃないか、と思いついたのですが、Accept-Encoding: gzip でないクライアントからアクセスすると 404 になってしまうので圧縮前のファイルが消せない。

今時ブラウザで対応していないものは少ないとはいえ、curlとか各種言語のHTTPクライアントでアクセスする場合もあるので、gzip 非対応クライアントには展開した内容を返したいところです。

それ ngx_http_gunzip_module でできるよ、ということでこうなりました。

gzip_static always を指定するため、nginx version 1.3.6 以降が必要です。

また、gzip_static と gunzip モジュールデフォルトでは組み込まれないため、build時に --with-http_gunzip_module --with-http_gzip_static_module を指定して有効にする必要があります。

    location ~ ^/nopaste/([0-9a-f]+)\.txt {
         gunzip       on;
         gzip_static  always;
    }

これで動作としては

  • gzip_static always によりクライアントの gzip 対応非対応お構いなしに .gz ファイルの内容を配信
  • gunzip on により、クライアントが gzip 非対応であればその場で展開

となり、どちらのクライアントに対しても .gz で保存された内容を送信することが可能になりました。

2013-12-26

社内Gyazoの画像をAmazon S3に逃がしてスケーラブルに運用する

Gyazo、便利ですよね。大変便利なので、社内でプライベートなGyazoサーバを用意して使っている会社も多いと思います。

うちでもサーバのパフォーマンスは特に必要ないので社内に適当なVMを立てて運用していたのですが、数年単位で運用していると画像ファイルが増えていくためdiskをなんとかする必要に迫られました。

ここでどんどん増えるファイルはAmazon S3に逃がそう、という自然な発想に至るわけですが、Gyazoサーバアプリが投稿を受けたときにS3にアップロードするような改修をするのは年末の忙しい時期に面倒。楽したい。

ということで S3 と nginx を組み合わせていいかんじに運用できるようにしてみました。

Gyazoに限らず、

  • ローカルに書き込んだファイルをhttpで閲覧する
  • 一度書き込まれたファイルには変更がない
  • ファイルは消えないでどんどん増える

ようなものには応用できると思います。

S3 に bucket を用意

普通に S3 に bucket を作ります。ここでは internal-gyazo.example.com とします。

社内のみにアクセス制限している画像なので、S3から直接一般に公開してしまうわけにはいきません。しかし、S3 API をちゃんと実装して nginx から画像を取得するのは面倒なので、IPアドレス制限をして特定のIPアドレス(ここでは社内gyazoサーバ) からのみ閲覧を許可します。

bucket policy の設定を開いて、AWS Policy Generator で生成した以下のようなJSONを記述します。

{
	"Version": "2008-10-17",
	"Id": "Policy1",
	"Statement": [
		{
			"Sid": "Stmt138795173604",
			"Effect": "Allow",
			"Principal": {
				"AWS": "*"
			},
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::internal-gyazo.example.com/*",
			"Condition": {
				"IpAddress": {
					"aws:SourceIp": "192.0.2.1/32"
				}
			}
		}
	]
}
  • s3:::internal-gyazo.example.com/* に対して
  • 192.0.2.1/32 から
  • s3:GetObject を許可する

というルールです。

nginxの設定

try_files を利用して、

  • ローカルファイル (/usr/local/gyazo/data/*) がある場合はそれを返す
  • ない場合は S3 に proxy_pass

という設定をします。

実際は別途Basic認証を掛けているため、AuthorizationヘッダをS3に送ってしまうと認証エラーになるので proxy_pass する前に削除しています。

S3 のホスト名を proxy_pass に直書きしないで一旦変数に set している記述については、 Nginxでproxy_passにホスト名を書いた時の名前解決のタイミング - (ひ)メモ を参照してください。

また、無駄に S3 にリクエストを飛ばさないために proxy_cache も設定しています。

proxy_cache_path /var/lib/nginx/tmp/proxy levels=1:2 keys_zone=gyazo:500m max_size=1000m inactive=24h;
server {
    listen 80 default_server;
    resolver  127.0.0.1;
    location /gyazo/data {
        root /usr/local;
        try_files $uri @s3;
    }
    location @s3 {
        set  $s3_host  "internal-gyazo.example.com.s3-ap-northeast-1.amazonaws.com";
        proxy_set_header   Host $s3_host;
        proxy_set_header   Authorization "";
        proxy_set_header   Cookie "";
        proxy_cache        gyazo;
        proxy_cache_valid  404 1m;
        proxy_cache_valid  any 24h;
        proxy_pass  https://$s3_host;
    }
    location /gyazo {
        proxy_pass  http://127.0.0.1:5000;
    }
}

運用

  • Gyazoサーバに投稿があるとローカルファイルが生成されます。その後の閲覧はローカルファイルから配信されます
  • 適当なタイミングで、ローカルファイルを S3 にアップロードして削除します
  • ローカルファイルが削除された後は、S3 から GET したファイルが配信されます

これで、ローカルファイルは適宜削除できるので disk 容量を気にする必要がなくなりました。年末にdisk 掃除ができてすっきりです。