实现自定义服务发现
2018年7月5日作者 Callum Styan
Prometheus内置了与许多服务发现 (SD) 系统(例如 Consul、Kubernetes 和 Azure 等公共云提供商)的集成。然而,我们无法为现有的所有服务发现选项提供集成实现。Prometheus团队在支持现有SD集成方面已力不从心,因此为每一个可能的SD选项维护集成是不切实际的。在许多情况下,当前的SD实现是由团队外部人员贡献的,并且没有得到良好的维护或测试。我们希望致力于仅提供与我们知道可以维护且按预期工作的服务发现机制的直接集成。因此,目前暂停新增SD集成。
然而,我们知道仍然有人希望能够与其他SD机制(例如Docker Swarm)集成。最近,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函数利用上下文来知道何时退出,并传入一个通道用于发送目标组的更新。
查看所提供示例中的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