实现自定义服务发现

2018年7月5日作者 Callum Styan

Prometheus 内置了许多服务发现 (SD) 系统的集成,例如 Consul、Kubernetes 以及 Azure 等公共云提供商。然而,我们无法为所有现有的服务发现选项提供集成实现。Prometheus 团队在支持当前的服务发现集成方面已经捉襟见肘,因此维护所有可能的 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` 实际上在一个协程 (goroutine) 中启动我们自定义 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` 结构体转换为 JSON,以符合 `file_sd` 格式。

运行时,适配器将监听一个通道,以接收来自我们自定义 SD 实现的更新。收到更新后,它会将 `targetgroup.Groups` 解析为另一个 `map[string]*customSD`,并与存储在 Adapter 的 `groups` 字段中的内容进行比较。如果两者不同,我们将新的组分配给 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)`。这是适配器代码中的管理器将在一个协程 (goroutine) 中调用的函数。`Run` 函数利用一个上下文 (context) 来知道何时退出,并传入一个通道用于发送目标组的更新。

查看所提供示例中的 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 获得了关于每个目标的响应,我们就会将所有这些目标发送到通道上。还有一个辅助函数 `parseServiceNodes`,它获取 Consul 对单个服务的响应,并从带有标签的后端节点创建目标组。

使用当前示例

在开始编写自己的自定义 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`。我们可以通过 `file_sd` 配置来配置 Prometheus 以读取此文件

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