Hatena::ブログ(Diary)

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

2014-12-19

GoでZabbixと通信する、もしくはオレオレZabbix Server/AgentをGoで実装する方法

全国一千万Zabbixユーザの皆様こんにちは。

複数のZabbix Agentから取得した値を集約する zabbix-aggregate-agent や zabbix_get コマンドの Go 実装版 go-zabbix-get を書いて遊んでいるうちに、Go で Zabbix と通信するライブラリが育ってきてしまったので一通りまとめておきます。

"github.com/fujiwara/go-zabbix-get/zabbix" を import して使います。

import "github.com/fujiwara/go-zabbix-get/zabbix"

Zabbix Agentから値を取得する

アイテムでいうところの「Zabbixエージェント」型、ServerやProxyからAgentに対してTCP接続をして値を取得するタイプです。

value, err := zabbix.Get("example.com:10050", "keyname", 5*time.Second)

通信相手のAegentのアドレス、取得したいアイテムのkey名、タイムアウト時間を渡すだけです。簡単ですね。value は string です。

Zabbix Server/Proxy に値を送信する

アイテムでいうところの「Zabbixトラッパー」型、ServerやProxyに対して(Agent以外の何者かが)値を送信するタイプです。Fluentdプラグインfluent-plugin-zabbix もこの形式ですね。

resp, err := zabbix.Send(
	"example.com:10051",
	zabbix.TrapperData{Host: "localhost", Key: "foo", Value: "bar"},
	5 * time.Second,       
)

送信データを zabbix.TrapperData 型として作成して渡してあげるだけです。これも簡単ですね。

これは何が嬉しいのかというと、自作の daemon 類に何か異常があった場合に Zabbix Serever に直接 trap を送りつけるような機能が実装できます。アラートの即応性が上がりますね。

複数データをまとめて送りつける SendBulk() もあります。

res, err := zabbix.SendBulk(
	"example.com:10051",
	zabbix.TrapperRequest{
		Data: []zabbix.TrapperData{
			zabbix.TrapperData{Host: "localhost", Key: "foo", Value: "bar"},
			zabbix.TrapperData{Host: "localhost", Key: "xxx", Value: "yyy"},
		},
	},
	timeout,
)

オレオレZabbix Agentを実装する

ここまでは既存の Agent や Server に対する通信ですが、自分自身で Agent や Server(Trapper) を実装することも可能です。

err := zabbix.RunAgent("0.0.0.0:10050", func(key string) (string, error) {
	switch key {
	case "agent.ping":
		return "1", nil
	// ...
	default:
		return "", fmt.Errorf("not supported")
	}
})

RunAgent() に func(key string) (string, error) な関数を callback として渡してやることで、TCPサーバとなり Server や zabbix_get コマンドに値を返す daemon を実装できます。

自作の daemon が持っている情報を、直接 Zabbix Server から通信して取得するような機能が実装できます。値の取得のために、別途 zabbix-agent から UserParameter で外部コマンドを起動するような必要がなくなります。

オレオレZabbix Server(Trapper)を実装する

zabbix_sender (zabbix.Send()) により送信された値を受信するサーバも簡単に実装可能です。

err := zabbix.RunTrapper("0.0.0.0:10051", func(req zabbix.TrapperRequest) (res zabbix.TrapperResponse, err error) {
	for _, data := range req.Data {
		log.Println(data)
	}
	res.Proceeded = len(req.Data)
	return res, nil
})

これは使いどころがいまいち難しいのですが、fluent-plugin-zabbixのテスト には便利に使っています。

ここまで道具が揃ったら、Go で Zabbix Server 互換実装を作り上げることも夢ではないですね!(やりませんけど…)

全く誰得プロダクトだと思いますが、お楽しみください。

2014-11-10

ISUCON4本選で3位に敗れました #isucon

ISUCON4 に「fujiwara組」として参戦しましたが、既報のとおり 3位に敗れてきました。順位こそ3位で賞金10万円は獲得できたものの、スコアが示すとおり内容的には完敗です。

まずは主催のLINE社様、出題を担当していただいたCookpad社様、本番サーバ提供をしていただいたテコラス社様にお礼申し上げます。本当に楽しいイベントをありがとうございました。

うちのチームとしてやったことは #isucon 4の本戦で3位を取ってきました (追記あり) - beatsync.net に大変詳しいので、そちらをご参照ください。

簡単に最終的な構成をまとめると

  • Redisは1号機に(動画以外)集約
  • 動画はアップロードを受けたサーバがローカルファイルとして保存しnginxが返す。保存されたサーバのアドレスをメタデータとしてRedisに保存し、APIへのレスポンスに含まれるURLを構築するのに使用する
    • そのため、動画ファイル自体の自ホスト間転送はない
  • リクエストは3台で問題なく受けられるが、ベンチマーカーのアクセスパターンの癖(?)か、先に指定したアドレスのほうに帯域が1.5倍ぐらい偏るような挙動が見られたので、最終的には1号機をシングル構成で動かす
  • ただしeth0, eth1のアドレス両方をベンチマーカーに指定することで1Gbpsのインターフェースを2個使ってスループットを出す

という構成でした。

ローカルアドレスを指定するのって(そもそもベンチマーカー相手に指定できるのって)どうなの、という話はあるのですが、出題者側の目論見としてなんらかのロードバランサーやCDN的なキャッシュサーバが前段にいるということであれば、それらからのアクセスがローカルアドレスでも受けられる可能性はないではない、ということで……うーん……

敗着

今回の敗着はこれに尽きます。33万点のスコアを「チームフリー素材」が出した時点で何か秘孔がある可能性は思い浮かんだのですが、そこを突き詰めずに忘れようとしたところが、最後まで考え続けた優勝チームの「生ハム原木」に完全に及ばなかったところです。

途中の構成では2号機3号機でそれぞれファイルを保存するものの、自ホストにないファイルがリクエストされた場合は nginx の try_files を使って相手ホストに reverse proxy することでファイルを見つける構成になっていました。

これが確か15時ぐらいでしたが、その際に毎回 proxy で相手から取得するのは無駄だから proxy_cache を入れよう、と思ってサーバ上の nginx.conf に設定をコピペまでしていました。

が、ここでベンチ実行待ちの間に念のため自らのホスト間の通信速度を iperf で測定したところ、(おそらく同一物理マシン内のVMのため) 32Gbps という計測結果が得られてしまい…… これだとメモリの少ないVMで proxy_cache のためにファイルIOを行うよりも、ホスト間で通信する方がコストが少なかろう、と判断して結局 proxy_cache を一度もベンチしないで外してしまったという経緯がありました。

そこで1回でも、ローカルベンチでも走らせていたら流れが変わっていた可能性があり、全く悔やみきれないのですが後の祭りですね。

もしくは自分のホスト間の帯域も1Gbpsに制約されていたら、確実に通信よりはファイルIOを選択していたわけで、そこが現実にはあり得ない(同一筐体にあることを想定して構成するわけにはいかない) VM配置だったのがある意味罠として作用してしまった感があります。

本選問題の感想など

おそらく出題の意図としては、もっと早い時点で Cache-Control が効くことを発見するチームがでて、そこからが本当の闘いになるというような目論見だったのかなあと勝手に思っています。

が、競技時間が1時間延長されてなお最後の30分まで(自覚的に)発見したチームがなかった、というのが想定外だったのかなと。

帯域ネックな勝負ではない、ということになれば当然ベンチマーカーの1Gbpsな帯域によってリモートベンチ結果が不安定になることもないでしょうし、そこでベンチマーカーの並列数を2にする判断が遅くなったということもあるんでしょうね。

参加者は基本的にはブラックボックスであるリモートからのベンチマークのみを頼りにチューニングする必要があるので、そこがもう少し安定していたらな、というのは正直な感想なのですが、そのあたりは今後 tagomoris さんが開催するであろう ISUCON benchmarkers casual talks で存分に(自分も出題経験者として) 話せたらなあと思います。

Cache-Controlヘッダの付与なんて、いつも飽きるほどやっていることなのに、それが ISUCON 当日にできないところが競技の難しさだなあと毎回思います。

最後に

自分も来年40歳になりますし、今回3回目の優勝を成し遂げた上で引退して勝ち逃げする目標を立てていたのですが、そうもいかなくなりました。また来年、リベンジできたらなと思います。

主催と共催の皆様、参戦者の皆様、本当にお疲れ様でした。今年も楽しかったですね!

2014-09-29

#isucon 4に参加して予選2日目暫定1位になりました

ISUCON1, 2と「fujiwara組」で連覇し、2013年には出題を担当しましたが、今年は一参戦者として挑戦することになりました。

  • 今年は弊社からの本選枠もなく(共催ではないので)、予選落ちしたらそれまで
  • チームは ISUCON 1,2のメンバーが自分以外全員退職(…) してしまったため、去年の出題担当 @acidlemn @handlename で新規編成

というなかなかプレッシャーのかかる状況でしたが、さしあたり予選2日目の暫定1位スコアを出すことができました。(後述しますが、一部レギュレーションに引っかかる可能性のある修正をしているため、失格となる可能性はあります。その判断が下された場合は、当然受け入れます)

速報結果はこちらです ISUCON4 オンライン予選 二日目の結果発表 : ISUCON公式Blog

例年のことながら、大変楽しいイベントでした。運営・出題をしていただいた皆様ありがとうございます!

当日の詳しい戦況についてはチームメンバーの記事が非常に詳しいのでそれに譲るとして、全体的に考えていたことなどを記録しておきます。

前日まで

チーム編成決定後は特に予習をする時間もなく、リリースしたばかりのサービスの増強、負荷対策、bash脆弱性祭りやAWSの再起動祭りに翻弄されていました。そのため準備としては予選1日目の前日、ランチを食べながら軽く方針を話したぐらいです。

メンバーの役割はざっくりときめておきます

  • @handlename : 主にコードを書き換える実装担当
  • @acidlemon : @handlename と共にアプリケーションの全般担当
  • @fujiwara : 状況調査、ミドルウェア設定、下回り担当

それ以外に決めたことは、

  • 言語は基本Perl
    • 題材によってGoを選択する可能性は視野に入れておく
  • 会社の会議室予約取る (オフィスで参加することにしたため)
  • お昼ご飯は外へ行く時間がもったいないから弁当を持ち込み
    • 甘いものも忘れずに
  • 前日までにAWSでIAMのアカウントを作成し、各人ごとに渡す
  • Github の private repo を作成して、アクセスできることを確認しておく
    • 社内IRCへの通知なども仕組みができているので、普段使い慣れたものを使う
  • 予選ポータルサイトにアクセスしておく
  • サポートチャットの idobata にログインしておく

当日朝に慌てないために、最低限のことだけ確認しました。出題内容の山かけはだいたい外すのでやるだけ無意味なのと、予断を持つのはかえって危険なのでしません。

過去の経験から、ISUCON当日には普段やっていないこと、やったことがないことはまずできないので、考えなくていい作業は極力なくすのが重要だと思います。

当日

開始から12時まで

10時の競技開始後、まずインスタンスを起動してソースコードをGithubへpush、@acidlemon と @handlename にアプリケーションの挙動確認とPerl実装を読み込んでもらっているうちに、サーバまわりの基本設定を終わらせます。

といってもOSがAmazon Linuxだったため、秘伝のタレ的な Shell script と Chef cookbook (CentOS 6, Amazon Linux 両対応) を流すだけで済みました。

いつものアカウント名とSSH鍵、小物ツール (ack, ag, ltsvrとか)、個人の設定ファイル(.screenrcとか) まで一気に揃うので、これで作業のストレスがなくなります。

こういう意味では、同じ会社で (チームは必ずしも同じではないですが) 同じような業務をしているメンバーで闘うメリットは大きいのかなと思います。fujiwara組は過去3回、すべてその時点で在籍している社員で構成しています。

すぐに見て分かる最低限のインデックスMySQLに設定し、静的ファイルをnginxから配ったところで17,000程度、ローカルポートあふれは頻出問題なので upstream keepalive の設定で簡単に解消、CPUが明らかに余っているので --workload 3 にして28,000程度が12時時点のスコアでした。

f:id:sfujiwara:20140929144356p:image

スコアの立ち上がりが早くできると、終盤のコード修正に時間を割けるので初速を出すのは大事かなと。

12時〜15時

実は予選1日目のスコアの上がりかたを観察して、以下のような目論見を立てていました。こういうことができるのは2日目が有利な点ですね…

  • 早い段階で大きくジャンプアップする手がある
    • 時間的に抜本的なコード修正などではなく、下回りの設定などで到達できるはず
  • しかしそこから上げられなくて苦しむ題材ぽい
  • 最後の1時間に圏外から上位に飛び込むチームが (毎回ですが) あるので、勝ち抜け確定レベルにいくにはコードに相当手を入れる必要がありそう
  • 昨年は予選中の最高スコアが3万程度だったので、昨年よりも更に高qpsな展開になりそう

そのため、前半伸ばしてからそのままの延長で5,6万点にいけそうにない場合、遅くとも14〜15時には判断して、大きくコードを書き換える方向に転換しよう、という方針は共有済みでした。

15時〜18時

上位を狙える想定として60,000点を出すためには、60,000 / 60sec = 1,000 qps でアプリケーションを回す必要があります。つまり、1リクエストに平均 1ms しか使えない。

nginxのアクセスログで request_time, upstream_response_time を観察し、現状でもほぼ 1〜5ms で返せているものの、それを平均 1ms まで上げるためには…

ここまでデータストアは素直にMySQLを使っているので、slow query log の閾値を 1ms に設定してログを観察し、MySQLではアプリケーションの平均レスポンスを 1ms に収めるのは無理であろうと判断しました。

ということで、以下のような方針でアプリケーションに手を入れました。

  • ベンチ走行中に一切変更がないユーザ情報はアプリケーションプロセスのオンメモリハッシュ
  • データが増える login_logs は Redis にいれ、banの判断もRedisで行う
  • 最終的にはデータ保全のため、RedisからMySQLに書き戻す

最初から自分はコードは読むけど書かない、と決めていたので、実装は @handlename, @acidlemon を完全に信頼して任せます。3人で寄ってたかってコードを書いても conflict したりしてろくなことがないですし。

ミドルウェア構成

最終的には、以下のような構成になりました

  • フロントは Varnish
    • 静的ファイルは nginx に振る(Varnishがキャッシュ)
    • / へのアクセスでリファラがないものは同一内容なので静的ファイルを配る
    • それ以外のアクセスは nginx を介さず直接 app に振る
  • つまり nginx には最初の数アクセスしか行かない

データストアは、前述のようにベンチ走行中は基本的に全て Redis、最後の /report へのアクセス時にMySQLに書き戻しています。

Varnishの設定は上述の条件分岐を素直に記述して、以下のような感じです。nginxのifは複雑な条件を扱うのが難しいので、こういう場面では Varnish 便利ですね。

ちなみにこのような設定は 2013年の社内ISUCON で行ったことがあるので、当時の資料からコピペして書き換えました。

import std;
backend nginx_static {
  .host = "127.0.0.1";
  .port = "81";
}
backend app {
  .host = "127.0.0.1";
  .port = "8080";
}
sub vcl_recv {
  if (req.http.x-forwarded-for) {
    std.collect(req.http.x-forwarded-for);
  }
  if ( req.url ~ "/stylesheet" || req.url ~ "/images" || (req.url == "/" && req.http.referer !~ "^http://" ) ) {
    set req.backend = nginx_static;
    return (lookup);
  }
  set req.backend = app;
  return (pass);
}

最後の(疑念の)一手

これは前述した、レギュレーション違反の懸念がある手です。

benchmarkerがレスポンスに含まれるHTMLの<link>を解析してスタイルシートにアクセスしてくるため、そこを削除すると静的ファイルへのアクセスが激減し、その分アプリケーションに処理を回すことができるためスコアが向上します。

「見た目が極端に変化しない」=「人間がJavaScript有効なブラウザでアクセスして判断する」という認識を運営に確認したため、<link>タグの出力を JavaScript の document.write() によって行うように修正しました。

レギュレーションには「DOM構造が変化しない」という項目があるのですが、静的HTMLとして見た場合には<link>がなくなっているので変化しているので違反の可能性ありですね。

JavaScriptが動作後には元と同一のDOM構造になる……と強弁できないことはないのですが、この点については運営の判断を仰ぎます。

最後のバグ

最終スコア登録が 17:57 という終了3分前になったのはダマで張っていたわけでは全くなく、実は /report の整合性チェックでベンチが失敗していました。

ということに終了12分前に気がついたときにはチーム全員大慌てでしたが、@handlenameが /report の結果に含まれるデータのを作るためにMySQLに保存する順序が重要である、ということを指摘したためにギリギリで修正してスコアを出すことができました。

二人がソースコードをちゃんと読み込んでいたのが、最後の最後で奏功したのかなと思います。

予選を振り返って

最終的に疑念の一手で2日目トップに躍り出てしまったのは、かなり微妙な気分ではありますが、そのあたりの判断は、繰り返しになりますが運営にお任せします。

もし本選に出られたら、またよろしくお願いいたします!

2014-08-22

[]in_tail+(in|out)_forwardができるログエージェントfluent-agent-hydraをGoで書いている

タイトルが長いですが、つまりそういうものをGoで書いています。

fluent-agent-hydra - Github

(hydraっていうのは首のいっぱいあるアレです。キングヒドラとか)

特徴

  • fluent-agent-lite 的なファイルを tail -F のように追尾する機能
    • 1プロセスで複数ファイルを追跡できます
    • in_tail のような pos_file, parse 機能は今のところありません
  • in_forward 的な TCP で msgpack 形式のログを受け取る機能
    • 各種言語の logger (Ruby, Perl, Go など) から投げたログを受け取って fluentd に送り直せます
    • JSON 形式には対応していません
    • 簡易的なオンメモリバッファを持っています
  • 上記から入力されたログを fluentd に送信する out_forward 的な機能
    • 複数の送信先を登録し、primary の fluentd がダウンしたら次の fluentd に送信を試みます
    • 送信先がダウンしてる場合はファイルの追尾を進めないので、内部バッファが溜まることはありません
    • (in_forward のバッファは溢れることがあります)
  • 動作状況を JSON で返す stats monitor httpd
    • 監視に便利
  • Goで書かれているのでバイナリ1ファイル(+設定ファイル)で動作します
    • daemonize 機構はないので、何らかの supervisor (daemontools, supervisord, runitなど) 経由で動作させる必要はあります
  • Windowsでも動きました

f:id:sfujiwara:20140822110551p:image

開発背景

これまで自分の環境では、Web(App)サーバから集約 fluentd へのログ転送について

  • ファイル追尾には fluent-agent-lite
  • アプリケーションからログを受け付けるのは fluentd (in_forward)

を利用していました。

fluent-agent-lite では 1プロセスで 1ファイルを扱うため、多数のファイルを追尾する場合にはファイル数分 fluent-agent-lite + tail のプロセスが必要になります。

それ自体はメモリ消費が多少大きくなるぐらいでさほど問題ではないのですが

  • agent-lite は起動時にファイルが存在しないとプロセスが終了してしまう
    • プロセスの起動順により、追尾したいログがまだない、ということがあり得る
  • agent-lite プロセス数で監視を行うと、追跡するファイル数が増えるごとに監視設定を追従する必要がある

という点が多少不便だったのと、自分のユースケース的には in_forward も1プロセスで賄えたら便利かな、というのと、あと単純に作ってみたかったからです。

【追記】

今はファイルがなくても死なないオプションがあるそうです。


Go での fluentd 代替実装については Ik が既にありますが、本家 fluentd のように plugin アーキテクチャになっていて重厚なのと、あとドキュメントがなかったので手を出せずに、という感じです。

ただし、in_forward 機能の実装にあたって、Ikからコードを頂いた部分が多くあります。ありがとうございます。

パフォーマンス

fluentd (in_tail+out_forward), fluent-agent-lite, fluent-agent-hydra の3者で、fluentd-benchmark / one_forwardベンチマークを取りました。

ベンチマーク結果はこちらです https://github.com/fujiwara/fluent-agent-hydra#benchmark

ざっとまとめると、通常使用する領域 (秒間数百〜数万lines/secまで) において

  • CPU使用率は lite < hydra < fluentd
  • メモリ使用量は hydra < lite << fluentd

という結果です。

ピーク性能では lite が 580,000/sec、hydraでバッファサイズとGOMAXPROCSを調整すれば 700,000/sec までいけましたが、秒間50万行ファイルにログを書く人はいないと思いますので、あまり意味のある結果ではないですね。

現状

数日間、某所環境で特にメモリリークもローテート時の取りこぼしもなく快調に動いているので、一応使えるレベルではないかと思いますが、α版状態です。

今後、本番に導入を進めて安定させたいと思います。

バイナリリリースもありますので、お試しいただければ幸いです。

2014-08-18

MHAをAWSで使うための支援ツールMHA::AWSをアップデートしてCPANに上げた

以前に作って、プロダクションでもいくつかのサービスに導入している MHA::AWS ですが、failover 方法を ENI 付け替えの他に VPC Route Table の書き換えもサポートしました。

ENI付け替えでは同一 Availability Zone 内での failover しかできませんが、VPC Route Table の書き換えによる方法では Multi-AZ 環境での failover も可能になります。

CPANにも上げましたので、 cpanm MHA::AWS でインストール可能です。

以前の紹介記事 → #11 MySQL Master HA を AWS で動作させる場合のフェイルオーバー支援ツール MHA::AWS のご紹介 | tech.kayac.com - KAYAC engineers’ blog


# /etc/masterha_default.cnf
master_ip_failover_script=mhaws master_ip_failover --interface_id=eni-xxxxxxxx
master_ip_online_change_script=mhaws master_ip_online_change --interface_id=eni-xxxxxxx
shutdown_script=mhaws shutdown --interface_id=eni-xxxxxxxx

使用方法は以前と変わらず、master_ip_(failover|online_change)_script と shutdown_script に対して引数を適切に設定した mhaws コマンドを指定すれば動きます。

Usage:
        $ mhaws [subcommand] --interface_id=ENI-id [... args passed by MHA]

        required arguments:
          1. failover method is ENI attach/detach
            --interface_id=[ENI-id for master VIP]

          2. failover method is RouteTable change destination
            --route_table_id=[RouteTable-id]
            --vip=[master VIP]

        subcommand:
          master_ip_failover
          master_ip_online_change
          shutdown

実はまだ、幸か不幸か(テスト以外で) 本番環境で事故による failover が発動したことはないのですが、一応ちゃんと動くと思います。

深遠な理由で RDS でなく EC2環境で MySQL を動作させる必要がある、MHAでフェイルオーバーしたい、というかたはどうぞご利用ください。