なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

Minecraft サーバーの状況を Mackerel で監視する

この記事は「はてなエンジニア Advent Calendar 2024 - Hatena Developer Blog」の36日目の記事です。昨日は id:hogashi さんの Redashではクエリ結果にHTMLを使えるので便利 長いカラムをdetailsで畳める ほか - hogashi.* でした。

わたしはプライベートで身内用の Minecraft サーバーを運用しているのですが、プレイヤー数やデータサイズなどを Mackerel に送信して監視しているので、その話をやっていきます。

ダッシュボードの様子

Minecraft サーバーには、 UT3 (or GameSpot) Query Protocol という、サーバーの状態を取得するためのプロトコルが実装されており、サーバー側の設定で有効にしていれば、ネットワーク越しにプレイヤー数やレイテンシーなどを取得することが可能です。 今回はこれで得られる情報をプラグイン経由で Mackerel に送信します。完成品はこちら:

github.com

まず、 Mackerel プラグインのひな形を作っていきましょう。 必要なのは3つのメソッドで、以下のように実装します:

package main

import (
    "flag"
    mp "github.com/mackerelio/go-mackerel-plugin"
)

// MinecraftPlugin Mackerel Agent
type MinecraftPlugin struct {}

// メトリクスデータを取得する
func (mc MinecraftPlugin) FetchMetrics() (map[string]float64, error) {
    return map[string]float64{}, nil // あとで実装する
}

// どのようにグラフで表すかを定義する
func (mc MinecraftPlugin) GraphDefinition() map[string]mp.Graphs {
    return map[string]mp.Graphs{} // あとで実装する
}

// プレフィクスを取得する、今回は固定値
func (mc MinecraftPlugin) GetPrefix() string {
    return "minecraft";
}

func main() {
    tempfile := flag.String("tempfile", "", "Temp file name")

    flag.Parse()

    mc := MinecraftPlugin{}

    helper := mp.NewMackerelPlugin(mc)
    helper.Tempfile = *tempfile

    helper.Run()
}

次に、先に述べたプロトコルを叩いてサーバー情報を得ましょう。これは都合の良いライブラリがすでに存在していたので、そちらを使うことで、以下のように取得できます:

import (
    "github.com/sch8ill/mclib"
)

type ServerStatus struct {
    Latency       int
    MaxPlayers    int
    OnlinePlayers int
}

func GetServerStatus(address string) (*ServerStatus, error) {
    client, err := mclib.NewClient(address) // minecraft.game.natsuneko.cat:25565 などのアドレス
    if err != nil {
        return nil, err
    }

    res, err := client.StatusPing()
    if err != nil {
        return nil, err
    }

    return &ServerStatus{
        Latency:       res.Latency,
        MaxPlayers:    res.Players.Max,
        OnlinePlayers: res.Players.Online,
    }, nil
}

これで現在のサーバーレイテンシー、オンラインプレイヤー数、設定上の最大プレイヤー数が取得できました。 今回は使用していませんが、 MOTD やバージョン、サーバー実装ソフトウェアの名前なども取得することが出来ます。

次は、 GraphDefinition を変更します。これは単純に Integer を返す実装にしてしまいましょう。

func (mc MinecraftPlugin) GraphDefinition() map[string]mp.Graphs {
    prefix := mc.GetPrefix()
    player := prefix + ".player"

    return map[string]mp.Graphs{
        player: {
            Label: "Minecraft Server Status",
            Unit:  mp.UnitInteger,
            Metrics: []mp.Metrics{
                {Name: "max", Label: "Limit"},
                {Name: "online", Label: "Current Players"},
                {Name: "latency", Label: "Latency"},
            },
        },
    }
}

今回の場合は、ラベル名として Minecraft Server Status、そこに表示される値として最大プレイヤー数 max 、現在のプレイヤー数 online、レイテンシーとして latency を返すようにします。 このようにすることで、画像のようなデータが取得できます。

今回はダッシュボードで別途表示を整えてしまうのでまとめてしまっていますが、プレイヤー数とレイテンシーでは単位が異なるので、分けておくと良いですね。 最後に値を返しましょう。次のような実装を FetchMetrics に追加することで、送信するメトリクスデータを出力することが出来ます。

func (mc MinecraftPlugin) FetchMetrics() (map[string]float64, error) {
    status, err := GetServerStatus(mc.GetServerAddress()) // 先ほどのモジュールを呼び出す、 GetServerAddress はアドレスを返す関数を定義する
    if err != nil {
        return nil, err
    }

    return map[string]float64{
        "max":                   float64(status.MaxPlayers),
        "online":                float64(status.OnlinePlayers),
        "latency":               float64(status.Latency),
    }, nil
}

最後に、ビルドしたファイルを適当な場所に配置し、 /etc/mackerel-agent/mackerel-agent.conf の最後に以下を追記すれば完成です:

[plugin.metrics.minecraft]
command = "/opt/mackerel-agent/plugins/bin/mackerel-plugin-minecraft"

これでしばらく待つとメトリクスデータが表示されます。お疲れさまでした。 最後に、 Minecraft サーバーでもこのような監視をするメリットとして、適切なスペックを選べるようになることが挙げられます。 例えば、次のグラフは四角で囲まれた時間帯はプレイヤーが1人オンラインとして遊んでいたのですが、

プレイヤーが存在することで何らかの処理が行われた結果、 CPU 使用率が +50% 消費されていることが分かります。 では、単純にプレイヤー1人につき CPU を 50% 消費するのか、というのも正確に求めるには計測する必要があるのですが、こちらも次のグラフを見ると:

実際にはそういうことは無く、プレイヤーが2人、3人となったとしても、 CPU 使用率は大きくは変わらないということが分かるかと思います。 このように、プレイヤー数と CPU 使用率・メモリ使用率が記録されていることで、適切なサーバースペックであるか、などがある程度計測によって求められるようになります。 計測大事。

ということで、アドベントカレンダー36日目の記事でした。