Go言語でサーバ負荷量を通知してみる

  • 4
    いいね
  • 0
    コメント

この記事は SLP-KBIT Advent Calendar 2016 その2の3日目の記事です.

Go言語の学習ついでにプチSensuのようなものを作ってみます.

サーバ構成

structure-1.PNG

クライアント(管理下サーバ)からホスト(管理サーバ)に対して,負荷情報を通知します.
ホストは受け取った情報をグラフ化します.
負荷量が一定以上になったらアラートを流すなどの機能はとりあえず作らない予定.

この記事では,クライアントから負荷量を通知する部分について主に解説します.
(単純にサーバ側が間に合わなかっただけですが)
サーバ側は簡単なechoサーバを今回は使っています.

負荷量通知プログラム mouryou-dog

コードはGithubに公開しています.

mouryou-dog

サーバ名,メモリ使用量,ディスクIO,Apacheのリクエスト処理量,
計測した時間をJSON形式で送信します.
CPU使用量も測定データに含めたかったのですが,うまくJSONに変換できなかったので
今回は含めていません.近日中に追加予定です.

通信方法

gorilla/websocketでコネクションを作成しています.
websocketの実装方法には,gorilla/websocketの他にも
Goの標準パッケージのgolang.org/x/net/websocketがあるのですが,
gorillaの方が開発盛んだし,使える機能多いぜ!と両者のサイトにあるので,
gorilla/websocketを使うことにしました.強いぜgorilla.

ホスト(管理サーバ)側

gorilla/websocketのechoサーバ例が丁度よかったので,
そのままサーバ側は利用しています.
Client and server example

websocketサーバを作成している部分を一部抜粋して解説します.

...
var addr = flag.String("addr", "localhost:8080", "http service address")
var upgrader = websocket.Upgrader{}

func echo(w http.ResponseWriter, r *http.Request) {
  c, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    log.Print("upgrade:", err)
    return
  }
  defer c.Close()
  for {
    mt, message, err := c.ReadMessage()
    if err != nil {
      log.Println("read:", err)
      break
    }
    log.Printf("recv: %s", message)
    err = c.WriteMessage(mt, message)
    if err != nil {
      log.Println("write:", err)
      break
    }
  }
}

func main() {
  flag.Parse()
  log.SetFlags(0)
  http.HandleFunc("/echo", echo)
  log.Fatal(http.ListenAndServe(*addr, nil))
}

echo関数の中でwebsocketコネクションを生成しています.
websocket.Upgrader構造体のUpgrade関数でConn構造体cを生成します.

Conn構造体のReadMessage関数で
websocket経由で送信された内容を受け取ります.

for文で無限ループにしており,停止命令を送るまでずっと待ち続けます.
ざっくりした説明ですが,サーバ側はこんな感じです.

クライアント(管理下サーバ)側

サーバプログラムと同じくgorilla/websocketのechoサーバ例にあったものを参考にしています.
client.go

サーバとの接続は以下のように行います.
urlを生成して,Dialer構造体のDial関数でコネクションを確立(?)します.

var addr = flag.String("addr", "localhost:8080", "monitoring address")
...
u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)

次に,time.NewTicker関数でticker構造体を生成します.
一定周期で実行したい処理があるときにこれが便利です.
参考Goで一定周期で何かを行う方法

今回は,これを利用して毎秒負荷量を測定してwebsocket経由で送信するようにしています.

測定方法

ホスト名,メモリ量,ディスクIOなどの情報はgopsutilパッケージを利用しています.
ドキュメントが分かりやすいです.gopsutil - GoDoc
また,作者の方が解説記事を書いてくれています. CPUやメモリなどの情報を取得するgopsutilのご紹介

httpリクエスト処理数については,apachectl statusコマンドでapacheのステータスを取得して,
ゴリゴリ分割してます.

最大処理数と現在の処理数から稼働率を取得します.
稼働状況が以下のように表示されます.
Wが処理中のプロセス,_が待機中のプロセス,.が未起動のプロセスを表しています.
詳しくはこちら. サーバステータスの監視機能
wを現在の処理数,全てを足したものを最大処理数としています.

W____...........................................................
................................................................
......................

実装内容です.
なかなか泥臭いことをやってます.

func (s *ServerStat)GetApacheStat() {
    var dataLine int
    out, _ := exec.Command("apachectl", "status").Output()
    d :=string(out)

    lines := strings.Split(strings.TrimRight(d, "\n"), "\n")

    for k, v := range lines {
        if v == "Scoreboard Key:" {
            dataLine = k
            break
        }
    }

    board := lines[dataLine-4]
    board = board + lines[dataLine-3]
    board = board + lines[dataLine-2]
    all := len(strings.Split(board, ""))
    idles := strings.Count(board, "_") + strings.Count(board, ".")

    r := float64((all - idles)) / float64(all)

    s.ApacheStat = r
}

デモ

実験環境の簡単な図

structure-2.PNG

ホスト用サーバ(管理サーバ)を立てます.
sample-server.goをコンパイルしたものを置いて実行します.

クライアント用サーバ(管理下サーバ)を何台か(1~?)立てます.
plugin.goをコンパイルしたものを置いて実行します.
今回は3台立ててみます.
それぞれApacheをインストール・起動しておきます.
デフォルトで使えたと思いますが,mod_statusを有効にしておきます.

負荷生成用のサーバを何台か(1~?)立てます.
クライアント用サーバと同じ数 or 倍数を立てます.
ApacheBenchコマンドなどを使ってクライアント用サーバに負荷をかけまくります.
コマンド例
ab -n 1000000 -c 1000 http:192.168.11.31/
均等にやる必要は特にないですが,
1クライアントにつき1負荷生成用サーバから負荷をかけるようにしています.

ホスト用サーバ(管理サーバ)の受信内容を見ます.

b38e07e33938d1a7c0fbc2a1780f4172.gif

楽しい.

苦労した点

実装するときに困ったところのメモ.

埋め込み構造体を送信できない

埋め込み構造体をそのまんまwebsocketで送信しようと思ったら空が渡ってしまいます.
まぁそうですよね,ポインタを持ってるだけですし...
結局埋め込み構造体の内容を文字列に変換して,変数に代入しています.
もっといい方法は無かったものか...

func ConvertMapToString(m map[string]disk.IOCountersStat) (string) {
    var str string

    str = "{"
    for k, v := range m {
        str  = str + string(k) + ":{"
        str = str + "ioTime:" + fmt.Sprint(v.IoTime) + ","
        str = str + "weightedIO:" + fmt.Sprint(v.WeightedIO) + "},"
    }
  str = strings.TrimRight(str, ",")
    str = str + "}"
    return str
}

今後

websocketはたまにコネクションが切れるから困ることがあるかもという助言をもらったので,
httpでpush型APIを作ってみて試してみる予定です.
もしかしたら,httpだと毎回コネクションを生成するので
オーバーヘッドが積み重なるかもしれません.
毎秒測定するようにしたいので,そこが問題になるのかも.
Sensu-clientなどを参考にして実装してみます.

可視化部が全くできていないので,可視化部の実装.

ログをどう残していくのか検討できていないので,ログの検討.

終わりに

サーバの負荷量を通知するクライアントプログラムを試作しました.
いつの日か可視化部を実装して記事を書きます.