カスタムサービスディスカバリの実装

2018年7月5日筆者: カラム・スティアン

Prometheus には、Consul、Kubernetes、Azure のようなパブリッククラウドプロバイダなど、多くのサービスディスカバリ (SD) システムとの統合が組み込まれています。しかし、すべてのサービスディスカバリオプションに対する統合実装を提供することはできません。Prometheus チームは、現在の SD 統合セットのサポートで手いっぱいの状態であり、可能なすべての SD オプションの統合を維持することは現実的ではありません。多くの場合、現在の SD 実装はチーム外のメンバーによって貢献され、その後適切に保守またはテストされていません。私たちは、維持できるとわかっている、意図したとおりに機能するサービスディスカバリメカニズムとの直接的な統合のみを提供することにコミットしたいと考えています。このため、現在、新しい SD 統合については一時停止措置が取られています。

しかし、Docker Swarm のような他の SD メカニズムとの統合を望む声があることも承知しています。最近、Prometheus メインバイナリにマージすることなく、カスタムサービスディスカバリ統合を実装するための小さなコード変更と例が、Prometheus リポジトリ内のドキュメントの ディレクトリにコミットされました。このコード変更により、内部の Discovery Manager コードを利用して、新しい SD メカニズムと相互作用し、Prometheus の file_sd と互換性のあるファイルを出力する別の実行可能ファイルを記述できるようになります。Prometheus と新しい実行可能ファイルを併置することで、Prometheus が実行可能ファイルの file_sd 互換出力を読み込むように構成し、そのサービスディスカバリメカニズムからターゲットをスクレイピングできるようになります。将来的には、これにより SD 統合を Prometheus のメインバイナリから外に出し、アダプタを使用する安定した SD 統合を Prometheus の discovery パッケージに移動させることが可能になります。

アダプターコードで実装されているものなど、file_sd を使用する統合はこちらにリストされています。

サンプルコードを見てみましょう。

アダプター

まず、adapter.goファイルがあります。カスタムSDの実装のためにこのファイルをコピーするだけで済みますが、ここで何が起こっているのかを理解しておくと役立ちます。

// Adapter runs an unknown service discovery implementation and converts its target groups
// to JSON and writes to a file for file_sd.
type Adapter struct {
    ctx     context.Context
    disc    discovery.Discoverer
    groups  map[string]*customSD
    manager *discovery.Manager
    output  string
    name    string
    logger  log.Logger
}

// Run starts a Discovery Manager and the custom service discovery implementation.
func (a *Adapter) Run() {
    go a.manager.Run()
    a.manager.StartCustomProvider(a.ctx, a.name, a.disc)
    go a.runCustomSD(a.ctx)
}

アダプタはdiscovery.Managerを使用して、カスタムSDプロバイダーのRun関数をゴルーチンで実際に開始します。Managerには、カスタムSDが更新を送信するためのチャネルがあります。これらの更新にはSDターゲットが含まれています。groupsフィールドには、カスタムSD実行可能ファイルがSDメカニズムから認識しているすべてのターゲットとラベルが含まれています。

type customSD struct {
    Targets []string          `json:"targets"`
    Labels  map[string]string `json:"labels"`
}

このcustomSD構造体は、主にPrometheusの内部targetgroup.Group構造体をfile_sd形式のJSONに変換するのに役立ちます。

実行中、アダプタはカスタムSD実装からの更新をチャネルでリッスンします。更新を受け取ると、targetgroup.Groups を別の map[string]*customSD にパースし、Adapter の groups フィールドに格納されているものと比較します。2つが異なる場合、新しいグループを Adapter 構造体に割り当て、それらを JSON として出力ファイルに書き込みます。この実装は、SD 実装がチャネルに送信する各更新が、SD が認識しているすべてのターゲットグループの完全なリストを含んでいることを前提としていることに注意してください。

カスタムSD実装

次に、アダプターを使って独自のカスタムSDを実装します。完全に機能する例は、同じ例のディレクトリのここにあります。

ここでは、アダプターコード "github.com/prometheus/prometheus/documentation/examples/custom-sd/adapter" と他のいくつかの Prometheus ライブラリをインポートしていることがわかります。カスタム SD を記述するには、Discoverer インターフェースの実装が必要です。

// Discoverer provides information about target groups. It maintains a set
// of sources from which TargetGroups can originate. Whenever a discovery provider
// detects a potential change, it sends the TargetGroup through its channel.
//
// Discoverer does not know if an actual change happened.
// It does guarantee that it sends the new TargetGroup whenever a change happens.
//
// Discoverers should initially send a full set of all discoverable TargetGroups.
type Discoverer interface {
    // Run hands a channel to the discovery provider(consul,dns etc) through which it can send
    // updated target groups.
    // Must returns if the context gets canceled. It should not close the update
    // channel on returning.
    Run(ctx context.Context, up chan<- []*targetgroup.Group)
}

Run(ctx context.Context, up chan<- []*targetgroup.Group) という関数を1つだけ実装する必要があります。これは、Adapter コード内のマネージャーがゴルーチン内で呼び出す関数です。Run 関数は、いつ終了するかを知るためにコンテキストを使用し、ターゲットグループの更新を送信するためのチャネルが渡されます。

提供された例のRun関数を見ると、他のSDの実装で必要となるいくつかの重要なことが行われていることがわかります。この例では、Consulに定期的に呼び出しを行い(この例のために、組み込みのConsul SD実装がまだないものと仮定します)、その応答をtargetgroup.Group構造体のセットに変換しています。Consulの仕組み上、まず既知のすべてのサービスを取得するための呼び出しを行い、その後、各サービスごとにすべてのバックエンドインスタンスに関する情報を取得するための呼び出しを行う必要があります。

各サービスに対してConsulを呼び出しているループの上のコメントに注目してください。

// Note that we treat errors when querying specific consul services as fatal for for this
// iteration of the time.Tick loop. It's better to have some stale targets than an incomplete
// list of targets simply because there may have been a timeout. If the service is actually
// gone as far as consul is concerned, that will be picked up during the next iteration of
// the outer loop.

これにより、すべてのターゲットの情報を取得できない場合、不完全な更新を送信するよりも、まったく更新を送信しない方が良いと述べています。一時的なネットワークの問題、プロセスの再起動、HTTPタイムアウトなどによる誤検知を防ぎ、しばらくの間古いターゲットのリストを持つことを優先します。Consulからすべてのターゲットに関する応答が得られた場合、それらのターゲットすべてをチャネルに送信します。また、個々のサービスに対するConsulの応答を受け取り、バックエンドノードからラベル付きのターゲットグループを作成するヘルパー関数parseServiceNodesもあります。

現在の例を使用する

独自のカスタムSD実装を書き始める前に、コードを見た上で現在の例を実行してみるのが良いでしょう。簡潔にするため、私は通常、サンプルコードを扱う際にはDocker Composeを使ってConsulとPrometheusを両方ともDockerコンテナとして実行しています。

docker-compose.yml

version: '2'
services:
consul:
    image: consul:latest
    container_name: consul
    ports:
    - 8300:8300
    - 8500:8500
    volumes:
    - ${PWD}/consul.json:/consul/config/consul.json
prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
    - 9090:9090

consul.json

{
"service": {
    "name": "prometheus",
    "port": 9090,
    "checks": [
    {
        "id": "metrics",
        "name": "Prometheus Server Metrics",
        "http": "http://prometheus:9090/metrics",
        "interval": "10s"
    }
    ]

}
}

docker-compose経由で両方のコンテナを起動し、その後、例のmain.goを実行すると、localhost:8500のConsul HTTP APIに問い合わせ、file_sd互換ファイルがcustom_sd.jsonとして書き込まれます。Prometheusをfile_sd設定経由でこのファイルを読み込むように設定できます。

scrape_configs:
  - job_name: "custom-sd"
    scrape_interval: "15s"
    file_sd_configs:
    - files:
      - /path/to/custom_sd.json