Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

mackerel-agentが作るシステムメトリックグラフをOpenTelemetryで可能な限り再現する

mackerel-agentをコマンド1つでインストールすると、ホストが登録されMackerel上で以下のようにホストのシステムメトリックのグラフを閲覧することができます。最速便利。

さて、メトリックを収集する仕組みはOpenTelemetryという標準規格に統一されようとする世の中の流れがあります。Mackerelとしてもこの標準に乗っかっていく、さらにメトリックにラベルという概念を加えてより自在に引けるようにすることを目指して、OpenTelemetry対応を進めています。現在ベータテスト中ですので、ぜひお試しください。

mackerel.io

今回は、mackerel-agentをインストールしてできるシステムメトリックのグラフを、Mackerelのラベル付きメトリック機能とOpenTelemetry Collectorを使ってできる限り再現してみることにします。

メトリックの収集

OpenTelemetry Collectorのインストール

以下の記事に従ってOpenTelemetry Collectorをインストールします。

opentelemetry.io

ただし、今回はcontrib版に入っているReceiverやProcessorを利用したいので、以下のようにパッケージのURLを変更しておきます。

 # debの場合
 sudo apt-get update
 sudo apt-get -y install wget systemctl
-wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.92.0/otelcol_0.92.0_linux_amd64.deb
+wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.92.0/otelcol-contrib_0.92.0_linux_amd64.deb
-sudo dpkg -i otelcol_0.92.0_linux_amd64.deb
+sudo dpkg -i otelcol-contrib_0.92.0_linux_amd64.deb

設定ファイルの記述

インストールができたら、Collectorの設定ファイルである/etc/otelcol-contrib/config.yamlを編集して以下のように記述します。

receivers:
  hostmetrics:
    collection_interval: 60s
    scrapers:
      cpu:
      disk:
      load:
      filesystem:
      memory:
      network:

processors:
  batch:
    timeout: 1m
  resourcedetection:
    detectors: ["system"]
    system:
      hostname_sources: ["os"]

exporters:
  otlp/mackerel:
    endpoint: otlp.mackerelio.com:4317
    compression: gzip
    headers:
      Mackerel-Api-Key: ***censored***

service:
  pipelines:
    metrics:
      receivers: [hostmetrics]
      processors: [resourcedetection, batch]
      exporters: [otlp/mackerel]

上のブロックから順に解説します。

ホストのメトリックを取得するには、Host Metrics Receiverを利用します。Mackerelのメトリックの最小粒度は1分なので、1分ごとメトリックを取得するように collection_interval: 60s と設定しておきます。scrapersに指定できる取得対象はもっとたくさんあるのですが、mackerel-agentがデフォルトで取得するメトリックに合わせておきました。

Resource Detection Processorを使うことで、投稿されるメトリックに属性(ラベル)を付与することができます。逆にこれがないと、投稿したメトリックをホストごと区別できない(あるいは別の方法で属性をつけなければならない)ことになります。今回の設定では、OSに設定されているホスト名を見て、host.name="arthur-pal"のようにホスト名を属性として付与してくれます。

MackerelにOpenTelemetry Metricsを投稿する際に利用するのは、OpenTelemetry Protocol Exporterです。MackerelはOTLPをサポートしているので、OpenTelemetry標準のエクスポーターをそのまま利用できます。headersにMackerelのAPIキーを入れて認証します。

最後にこれらの設定をservice.pipelines.metricsでまとめて完成です。

メトリックの描画

OTLPでMackerelに投稿したメトリックはPromQLライクな言語でクエリをかけてグラフとして描画することができます。PromQLで作ったグラフでシステムメトリックのグラフを再現していきます。

loadavg

system.cpu.load_average.1m{host.name="arthur-pal"}

メトリック名だけだとどのホストかを絞り込めないので、{host.name="arthur-pal"}というラベルマッチャを書いています。

cpu

sum without (cpu) (irate(system.cpu.time{host.name="arthur-pal"}[2m]))
/ (
  scalar(sum without (cpu) (irate(system.cpu.time{host.name="arthur-pal",state="user"}[2m])))
  + scalar(sum without (cpu) (irate(system.cpu.time{host.name="arthur-pal",state="nice"}[2m])))
  + scalar(sum without (cpu) (irate(system.cpu.time{host.name="arthur-pal",state="system"}[2m])))
  + scalar(sum without (cpu) (irate(system.cpu.time{host.name="arthur-pal",state="idle"}[2m])))
)
* 600

左が積み上げグラフなのに対し、クエリグラフでは積み上げに対応していないので分かりづらいですが、値はほぼ一致しています。6コアなので×600しているところが微妙なのでなんとかしたいですね。

memory

system.memory.usage{host.name="arthur-pal"}

disk

irate(system.disk.operations{host.name="arthur-pal"}[2m])

system.disk.operationsは単調増加していくカウンタなので、範囲ベクトルとして取得した上でirate()関数でwrapすると、1秒あたりの増加率として値が得られます。

interface

irate(system.network.io{host.name="arthur-pal", device="eth0"}[2m])

device名を指定していますが、 sum without (device) irate(system.network.io{host.name="arthur-pal", device="eth0"}[2m]) としてデバイスごとの値を合算してしまったほうが良いのかもしれません。

filesystem

system.filesystem.usage{host.name="arthur-pal",device="/dev/vda2"}

ということで、mackerel-agentが取得するシステムメトリックのグラフを、OpenTelemetry Collectorで取得したメトリックのグラフとして再現することができました!PromQLを書くのは慣れないと大変なので、このあたりの支援ができたらより便利になりそうですね。

PalworldのDedicated ServerをConoHa VPSで建てた

Palworldというゲームが2023/1/19に発売された。

www.pocketpair.jp

このゲームはMinecraftのようにDedicated Serverを個人で建てて誰かと一緒のワールドで遊ぶことができるようだ。友人にサーバを建ててくれと依頼されたので構築することにした。

構築の流れ

サーバを用意する

ConoHa VPSの8GBプランでVPSを建てた。これは推奨要件が

  • CPU: 4コア
  • メモリ: 8GB以上

だったからだ。実際にこのプランで運用しているとメモリはそれなりに使っていてCPUはかなり余っているという状態だった。ちなみに4GBのサーバでは途中で接続不能になってしまった。

(2024-01-21追記)どうやら要件が変更されて16GB(安定した稼働のためには32GB推奨)となったようです。これはちょっとしんどすぎる。

OSは使い慣れているUbuntu Server 22.04を選択した。Debianよりは色々なものが最初から入っているのでやりやすい。

SSHなどのテンプレ設定を済ませたり、監視のためにmackerel-agentをインストールしたりした。私は監視SaaS Mackerelの中の人(ダイレクトマーケティング)。

ConoHa VPSにはセキュリティグループ機能があるので、Ubuntuのファイアウォールは以下のようにして切った。

sudo ufw disable

また、Palworldのサーバで使うポート(デフォルトだと8211)を許可するセキュリティグループを作って、作成したサーバにattachしておく。

Palworldのサーバ一覧でpingの値を表示したい場合にはここでICMPを許可するセキュリティグループもつけておきましょう。

PalServerを立ち上げる

マニュアルが用意されているので、Linuxのところの記述の通りに作業していく。

tech.palworldgame.com

一つ詰まったところとして、この手順の通りだとsteamclient.soが見つからないとエラーになってサーバが立ち上がらなかった。エラーログを読んで、以下のようにシンボリックリンクを貼ったら通った。

ln -s ~/Steam/steamapps/common/PalServer/linux64/steamclient.so ~/.steam/sdk64/steamclient.so

(2024-01-21追記)上記のハックについては、現在同様の方法がドキュメントに記載されています。

あとはSystemdにServiceとして登録して常駐できるようにしたら完成。現在はパスワード認証に対応していないということだったので、念のため port オプションを渡すことでデフォルトのポートから変えておいた。(ConoHaのセキュリティグループの設定でもこれに合わせたポートを指定することに注意。)

ExecStart=/home/steam/Steam/steamapps/common/PalServer/PalServer.sh port=12345 players=10 -useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS

slog時代のGoではloggerをcontextで引きまわさなくて良い気がする

Goのloggerを引き回す際に皆さんはどのような手法を取っていますか?

  1. グローバル変数にloggerのインスタンスを入れておく
  2. contextにloggerのインスタンスを入れておく
  3. トレースIDなどを入れたloggerを適宜作ってcontextに格納する
  4. 構造体のフィールドにloggerのインスタンスを入れておく(DI)

などなど、ソフトウェアの規模や特性を鑑みて各自使い分けているかと思います。

ところで、私は 2. 3. の手法があまり好きではありませんでした。単純に面倒だし美しくありません。contextに入れる、取り出すだけでも数行のコードを毎度書く必要があってダルいな〜と思っていました。けれども、ログにトレースIDなどを入れたいだろうなあと思い、Webサーバの実装においては 2. を渋々選択していました。

さて、Go 1.21ではslogパッケージが登場しました。slogは単に構造化ログが扱えるというだけでなく、使いやすかったりトレースと統合できたりということも考慮されて設計されています。

go.googlesource.com

slogパッケージではslog.SetDefault(l *Logger)という関数が用意されており、デフォルトのロガーを登録しておけます。実装としては大体グローバル変数に格納する手法と一緒で、複数のgoroutineから呼ばれることを考慮してsync/atomicを用いているようです。*1

さらに、ログを出力する際にはslog.InfoContext(ctx context.Context, msg string, args ...any)のようにcontextを渡すことができる関数が用意されており、loggerのハンドラーは出力の際にcontextの値を利用することができます。

pkg.go.dev

私はこの2つを見て、contextによって構造化ログに含まれる特定の値(トレースIDなど)を変えたいという目的程度であればloggerごとcontextに入れる実装はやらなくて良いと解釈しました。以下のようにcontextからrecordに付与したい情報を取得するようなHandle()を実装したHandlerを使ってslogのインスタンスを作り、SetDefault() でデフォルトロガーに設定すれば良いのです。こうすることで、ログを出すためだけにわざわざcontextからloggerを取得する必要がなくなります。

type LogWithTraceHandler struct {
    inner slog.Handler
}

func NewLogWithTraceHandler(inner slog.Handler) LogWithTraceHandler {
    return LogWithTraceHandler{inner}
}

func (h *LogWithTraceHandler) Handle(ctx context.Context, r slog.Record) error {
    sc := trace.SpanContextFromContext(ctx)
    if sc.IsValid() {
        r.AddAttrs(
            slog.String("trace_id", sc.TraceID().String()),
            slog.String("span_id", sc.SpanID().String()),
        )
    }
    return h.inner.Handle(ctx, r)
}

func main() {
    logger := NewLogWithTraceHandler(slog.NewJSONHandler(os.Stderr, nil))
    slog.SetDefault(logger)
    // あとは context に SpanContext を詰めて slog.InfoContext(ctx, "message", ...) で呼び出し
}

(コードはブラウザ上で適当に書いたので動かないかもしれません。机上論。)

もちろん、あるcontextの中ではloggerの実装ごと変えてしまいたいというユースケースがないとは言いませんし、そういう場合にはcontextにloggerの実体を格納すると良いでしょう。しかし、典型的なWebサーバを作るという目的において、自分だったらslog.SetDefault()にセットしたloggerを使うので十分かなと現時点では考えています。

Ubuntu・Debianで再起動が必要な際に知らせてくれるMackerelのチェックプラグイン

家でいくつかRaspberry Piを飼っておりそれぞれにUbuntuをインストールしているのですが、ログインした際に以下のように「System restart required」と言われることがあります。

Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-1044-raspi aarch64)

(中略)

*** System restart required ***
Last login: Mon Jan 15 22:41:41 2024 from 192.168.100.18

Ubuntuはデフォルトでパッケージを自動更新するようで、カーネルなどが更新された際にSystem restart requiredの状態になるようです。せっかく自動更新しているのに再起動待ちで更新を反映できないと勿体無いので、Mackerelのチェックプラグインを自作してこの状態に気づけるようにしてみました。

github.com

使い方

このチェックプラグインはmkr installに対応している(cf.) mkr plugin installでプラグインをインストールする - Mackerel ヘルプ)ので、まずは以下のようにしてプラグインをインストールしましょう。

sudo mkr plugin install Arthur1/check-debian-reboot-required

あとはmackerel-agent.confを書き換えてmackerel-agentのサービスを再起動するだけです。

+[plugin.checks.check-debian-reboot-required]
+command = ["/opt/mackerel-agent/plugins/bin/check-debian-reboot-required"]

もしOSのrebootが必要な状態になっているなら、以下のようにアラートが発報されます。

このホストでsudo rebootを実行すると、アラートがクローズされました。

実装

DebianやUbuntuではOSの再起動が必要な際に/var/run/reboot-requiredというファイルが生成されるようです。また、どのパッケージが要因なのかについても/var/run/reboot-required.pkgsというファイルに書き込まれています。

よって、チェック監視のプログラムではこれらのファイルの有無を見れば良いわけです。Go言語ではos.Stat()os.IsNotExist()を利用することでファイルの有無を判定することができます。

if _, err := os.Stat("/var/run/reboot-required"); err == nil {
    // reboot is required
} else if os.IsNotExist(err) {
    // reboot is not required
} else {
    // unexpected error
}

実際の実装も非常にシンプルなコードになっているので、チェックプラグインを自作してみたい人はぜひリポジトリを参考にしてください。

Go言語のクロスコンパイルを活かすDocker BuildxのBUILDPLATFORM・TARGETARCH

Go言語で作ったアプリケーションをコンテナイメージにするとき、以下のようにマルチステージビルドを利用したDockerfileを書くことが多いでしょう。

FROM golang:1.21-bookworm as builder

WORKDIR /opt/app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -ldflags="-s -w" -o server ./cmd/server

FROM gcr.io/distroless/base-debian12:nonroot

COPY --from=builder --chown=nonroot:nonroot /opt/app/server /server

ENV PORT 8000
EXPOSE $PORT

ENTRYPOINT ["/server"]

ここで、ベースイメージである golang:1.21-bookwormgcr.io/distroless/base-debian12:nonroot はマルチアーキテクチャイメージなので、--platform オプションを利用することでこのDockerfileのままホストのアーキテクチャと異なるイメージをビルドすることができます。

例えばGitHub Actions上でbuildすると基本的にはamd64のランナーを使うことになりますが、docker/setup-qemu-actiondocker/setup-buildx-actiondocker/build-push-actionといったactionを利用することで、arm64向けのイメージをビルドすることができます。もちろんマルチアーキテクチャイメージを作ることも可能です。

docs.docker.com

ここで、amd64のホスト上で先ほどのDockerfileからarm64向けのイメージを作ろうとすると、最初のbuilderステージからQEMUを利用してarm64をエミュレートした環境でgo buildが行われます。エミュレーションなのでとても遅いです。自分の個人開発プロジェクトで作ったちょっとしたLambdaのイメージビルドに7分30秒もかかっていました。

今回は、これを1分30秒まで短縮する方法をご紹介します。

本画像は https://github.com/7474/NantoNBai をお借りして生成しました。分数の計算合ってる?

Go言語はクロスコンパイルが得意な言語なのに、先ほどのdocker buildの方法ではそれが活かせていないと言えます。しかし、かといってdockerの外でbuildしたバイナリをCOPYするような仕組みにするのも美しくありません。

Docker wayから外れずにGo言語の良さを活かすためのBuildxの機能がBUILDPLATFORM, TARGETARCH引数です。これらの引数はBuildKitを使う際に自動で設定されます。BUILDPLATFORMはbuild環境のplatform(例: linux/amd64)、TARGETARCHはビルドしたい対象のアーキテクチャ(例: arm64)です。

docs.docker.com

以下のようにDockerfileを書き換えることで、builderステージは必ずビルド環境と同じプラットフォームで実行されます。つまりこのステージでのエミュレーションが不要になり、ビルドが高速になります。一方、go build時にアーキテクチャを明示する必要が出てきたため、TARGETARCHをGOARCHとして渡してあげています。

-FROM golang:1.21-bookworm as builder
+FROM --platform=$BUILDPLATFORM golang:1.21-bookworm as builder
+ARG TARGETARCH

 WORKDIR /opt/app

 COPY go.mod go.sum ./
 RUN go mod download

 COPY . .
-RUN go build -ldflags="-s -w" -o server ./cmd/server
+RUN GOARCH=${TARGETARCH} go build -ldflags="-s -w" -o server ./cmd/server

 FROM gcr.io/distroless/base-debian12:nonroot

 COPY --from=builder --chown=nonroot:nonroot /opt/app/server /server

 ENV PORT 8000
 EXPOSE $PORT

 ENTRYPOINT ["/server"]

Advent Calendarを25日分続ける技術、あるいは物を発明し素早く作る技術

このエントリははてなエンジニアAdvent Calendar 2023の2024年1月8日の記事です。

昨日はid:handatさんのGitHubでパブリックリポジトリへの誤投稿を防ぐ拡張機能「GitHub Public Repo Alert」の紹介 - handatのdatファイルでした。


id:arthur-1はサーバー監視サービスMackerelの開発チームでアプリケーションエンジニアをしております。

昨年末に開催したMackerel Advent Calendar 2023において、「arthur-1 Mackerel Advent Calendar 2023 マラソン」と称し、一人で25日分記事を投稿することができました。Mackerel開発の裏話を放出したり、ちょっとした便利ツールや実装例を作ってリリースしたり、将来の夢を語ったりしました。

このエントリでは「Advent Calendarを25日分続ける技術、あるいは物を発明し素早く作る技術」と題して、25日分の投稿を実現させた様々な技術についてご紹介します。

周囲を見渡してネタ帳を書く

Advent Calendarを25日分書くにあたって最初の関門が25日分のネタを探すところにあります。私はAdvent Calendarに限らずアウトプットできそうなことが思いついたら手元のネタ帳に書き留めるようにしています。

  • 世の中にまだないけど作れそうなもの
  • プロダクトを良くするためのアイデア
  • 具体的な仕事から得た一般化できそうな知見
  • 技術的に試行錯誤してみた記録

上記に限りませんが、思いついたことは忘れないうちにどこかに記録しておきます。思いついた段階ですでにストーリーがある程度浮かんでいる場合には早速ブログ記事を書いたり、社内のグループウェアでラフな感じに書いたりすることもあります。

自分の仕事の責任範囲が広がると勝手に見える景色も変わってくるし、そこまでしなくても周りの仕事にちょっと首を突っ込んで観察するだけでもネタ探しに役立ちます。また、いろんな意味でオープンな環境で仕事をすることで、自分で捻り出さなくてもネタが見つかると思います。はてなのオープンネスを重視した環境(例えば他のチームのSlackの様子も見られる)がまさに絶好の場です。

論理的思考からアイデアを生む

自分は何かを体系的に学ぶより、モノづくりの過程で学んだことをアウトプットすることが多いです。実際、今回のMackerel Advent Calendarでもさまざまなソフトウェアを作りました。そして、モノづくりにはアイデアが必要不可欠です。

アイデアマンでいるために重要なのが論理的思考(とりわけゼロベース思考)とドメインへの理解だと思っています。

「誰のどのような問題を解決したいのか」という本質だけを固定して思考実験すると、世界には無数の択があることがわかります。すると、固定観念や既存のプロダクトの仕様に引きずられずにアイデアを得ることができるでしょう。そしてそれがまだ世の中(あるいはもう少し狭くして現在のプロダクト)に存在しないものだからこそ、ユニークな情報としてブログのネタにすることができます。

ここで攻めた思考をする際に道を踏み外さないために、ドメインへの十分な理解というガードレールが役に立つと考えています。ユーザーが本当に求めているのは何か、これを解像度高く理解していなければ、本当に突拍子もなく役に立たないアイデアに走ってしまう可能性があります。ドメインへの理解が十分だからこそ、道を離れる(≠踏み外す)ことができるのです。

新しい技術に触れる

枯れた技術に関する情報はネットを検索すると先駆者がいて出てくることが多いです。Advent Calendarで二番煎じなネタに走らないためにも、日頃から新しい技術に触れておくのは大事なことです。

私は仕事以外でも精力的に個人開発をしていますが、そこは自分の庭のようなもので金銭面以外での制約がほとんどないため、積極的に新しい技術を取り入れるようにしています。その結果得た知識をブログなどにアウトプットしています。例えばこのブログでは、Next.js App RouterでのStatic Exportsに疲弊している記事のアクセス数が比較的多いですね。

2023年に自分が新たに触れたのは以下のような技術で、普段のブログやAdvent Calendarではこれらと絡めた投稿を多めにしていました:

  • GraphQL
  • Kubernetes
  • GitOps
  • Next.js App Router
  • OpenTelemetry
  • Slack次世代プラットフォーム

開発からリリースの広い範囲において、自分の鉄板技術を持つ

思いついたアイデアを形にするためのモノづくりに時間がかかっていると、Advent Calendarのスピード感に追いつけません。

私はアプリケーションエンジニアとして仕事をしていますが、アプリケーションを作る(サーバーサイド・フロントエンド)、そしてそれをリリースするまでの一連の技術を一通り触れるようにしています。また、典型的なアプリケーションを作るならこの技術をこのようにして使うというテンプレートを自分の中に持つようにしています。

最近自分がWebアプリケーションを作るとしたら選択する鉄板スタックは以下の通りです:

  • サーバー: Go・go-chi
  • フロント: TypeScript・React
  • CI: GitHub Actions
  • インフラ*1: Terraform・AWS Lambda・API Gateway・Cloudflare Pages

Advent Calendarのために何らかの物を作る際も、新しい挑戦以外の部分では自分の手に馴染む技術をこれまでと同じように利用する(=だいたいコピペでOK)ことで高速に開発・リリースできるようにしています。

隙間家具コーディネート職人になる

これも素早く物を作るためのテクニックなのですが、できる限り自分で手作りしすぎないようにしています。ではどうするかというと、すでに世の中に存在するライブラリや隙間家具OSS*2をうまく繋ぎ合わせて実現できないかをまず考えるようにしています。

例えば以下の記事では、Mackerelでシナリオ監視ができるツールを、runnというシナリオテストツールを組み込むことによって開発しました。本来であればシナリオの記述やテストの実行ロジックをまるまる作らなければならなかったところを、runnのソースコードで公開されている関数をimportすることでサクッと実装することができました。

blog.arthur1.dev

汎用的なライブラリを作ってくださっている人たちのおかげで、自分がアイデアの実現に集中してクリエイティブに活動できるのは本当にありがたいことです。

仕事でも良い文章をたくさん書く

いくらアイデアや制作物があっても、Advent Calendarとしては他者が読む文章にしなくてはなりません。読者に伝わる良い文章を書くために、普段の仕事の経験も活かすことができます。

エンジニアとして自分が良く書いている文章はArchitecture Decision Record(ADR)です。ADRとは、アーキテクチャの決定やその背景・理由などを記録する文書です。Mackerel開発チームでは技術的な決定を行う際にADRを書き、テックリードの確認を受けることになっています。

自分がADRを書く際には、一目読んだだけで何に関するどのような決定かが分かるようなタイトルをつけたり、決定の合理性が他者に伝わるよう簡潔に論理的に記述したりすることを心がけています。

他にも、Mackerelのブログで公開されているリリース告知に対して開発者としてレビューする機会があります。

ここでは、一文が長くなり過ぎないように区切るなど、読みやすい日本語にするための指摘を気付きベースで行っています。また、新機能のターゲットとなるユーザー像を思い浮かべ、その機能で何が嬉しいのかがより伝わりやすいように具体例を提示するといった助言もするようにしています。

こういった業務での経験が、頭の中の思考をAdvent Calendarの記事として書き起こす際にも非常に役立っていると感じます。


雑多に色々書き綴ってしまったのでまとめます。Advent Calendarマラソンのために特化した技術はそこまで必要ではなくて、モノづくりができる人であり続ければその延長線として実現できることなのだと思っています。

これを読んだ皆さんも今年のMackerel……に限らず何かのAdvent Calendarを一人で完走してみませんか?

*1:個人開発なので安さ重視の選定になっています。マネージドRDBはどうするの問題は未解決です

*2:cf.) https://speakerdeck.com/fujiwara3/xi-jian-jia-ju-ossfalsesusume