实现自定义服务发现

实现自定义服务发现

Prometheus 包含许多服务发现 (SD) 系统的内置集成,例如 Consul、Kubernetes 和公共云提供商(如 Azure)。但是,我们无法为所有服务发现选项提供集成实现。Prometheus 团队已经为了支持当前的一系列 SD 集成而捉襟见肘,因此为每种可能的 SD 选项维护集成是不切实际的。在许多情况下,当前的 SD 实现是由团队以外的人员贡献的,但之后没有得到很好的维护或测试。我们希望承诺仅对我们知道可以维护且按预期工作的服务发现机制提供直接集成。因此,目前暂停新的 SD 集成。

但是,我们知道仍然有希望能够与其他 SD 机制(如 Docker Swarm)集成。最近,一个小的代码更改和一个示例被提交到 Prometheus 存储库中 documentation/examples/custom-sd 目录,用于实现自定义服务发现集成,而无需将其合并到主 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 函数在一个 goroutine 中。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。一个完整的工作示例在相同的示例目录 here 中。

在这里你可以看到我们正在导入适配器代码 "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)。这是 Adapter 代码中的管理器将在 goroutine 中调用的函数。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 收到了关于每个目标的响应,我们会将所有这些目标发送到通道上。还有一个辅助函数 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。我们可以配置 Prometheus 通过 file_sd 配置来获取此文件

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