Pull 模式无法扩展 - 真的吗?

2016年7月23日作者 Julius Volz

我们来谈一个流传已久的迷思。每当讨论监控系统并提到 Prometheus 基于 pull 模式的指标收集方法时,总会有人插话说,pull 模式“根本上无法扩展”。他们给出的理由通常很模糊,或者只适用于与 Prometheus 有本质区别的系统。事实上,我们拥有在最大规模环境下运用 pull 模式监控的经验,这种说法与我们的实际运维经验背道而驰。

我们已经有一篇 FAQ 文章介绍为什么 Prometheus 选择 pull 而不是 push,但那篇文章没有专门关注扩展性方面。下面,让我们来仔细看看围绕这一说法的常见误解,并分析它们是否以及如何适用于 Prometheus。

Prometheus 不是 Nagios

当人们想到一个主动拉取数据的监控系统时,他们通常会想到 Nagios。Nagios 以其扩展性不佳而闻名,部分原因在于它为主动检查生成子进程,这些子进程可以在 Nagios 主机上运行任意操作以确定某个主机或服务的健康状况。这种检查架构确实扩展性不好,因为中心的 Nagios 主机很快就会不堪重负。因此,人们通常将检查配置为每隔几分钟才执行一次,否则就会遇到更严重的问题。

然而,Prometheus 采取了完全不同的方法。它不执行检查脚本,而是仅仅通过网络从一组被监控的目标中收集时间序列数据。对于每个目标,Prometheus 服务器只是通过 HTTP 获取该目标所有指标的当前状态(使用 goroutine 高度并行地进行),并且没有其他与 pull 相关的执行开销。这引出了下一点。

由谁发起连接并不重要

从扩展性的角度来看,由谁发起用于传输指标的 TCP 连接并不重要。无论哪种方式,建立连接的成本与指标的有效载荷以及其他所需的工作相比都非常小。

你可能会说,但是 push 模式可以使用 UDP,从而完全避免连接建立的开销!的确如此,但在 Prometheus 中,TCP/HTTP 的开销与 Prometheus 服务器为接收数据所做的其他工作(尤其是将时间序列数据持久化到磁盘)相比,仍然可以忽略不计。用一些数字来说明:一个大型的 Prometheus 服务器可以轻松存储数百万个时间序列,记录显示每秒可接收 80 万个样本(这是在 SoundCloud 使用真实生产指标数据测得的)。假设抓取间隔为 10 秒,每个主机有 700 个时间序列,那么一个 Prometheus 服务器就可以监控超过 10,000 台机器。这里的扩展瓶颈从来都与拉取指标无关,而通常与 Prometheus 服务器将数据接收到内存中,然后可持续地在磁盘/SSD 上持久化和清理过期数据的速度有关。

此外,尽管如今的网络已经相当可靠,但使用基于 TCP 的 pull 模式可以确保指标数据可靠到达,或者至少在因网络故障导致指标传输失败时,监控系统能够立即知晓。

Prometheus 不是一个基于事件的系统

一些监控系统是基于事件的。也就是说,它们在每个独立事件(如一次 HTTP 请求、一个异常等)发生时,会立即将其报告给中央监控系统。这个中央系统要么将事件聚合成指标(StatsD 是这方面的典型例子),要么单独存储事件以供后续处理(ELK 技术栈就是这样一个例子)。在这样的系统中,pull 模式确实会带来问题:被监控的服务需要在两次拉取之间缓冲事件,并且拉取必须非常频繁,才能模拟出 push 模式那样的“实时性”,同时避免事件缓冲区溢出。

然而,还是要再说一遍,Prometheus 不是一个基于事件的监控系统。你不会向 Prometheus 发送原始事件,它也无法存储这些事件。Prometheus 的业务是收集聚合后的时间序列数据。这意味着它只关心定期收集一组给定指标的当前*状态*,而不是导致这些指标产生的底层事件。例如,一个被监控的服务不会在处理每个 HTTP 请求时都向 Prometheus 发送一条消息,而只是在内存中对这些请求进行计数。这个计数过程可以每秒发生数十万次,而不会产生任何监控流量。然后,Prometheus 只是每隔 15 或 30 秒(或你配置的任何时间)向服务实例询问当前的计数器值,并将该值与抓取时间戳一起作为一个样本存储起来。其他指标类型,如 gauges、histograms 和 summaries,也以类似的方式处理。由此产生的监控流量很低,pull 模式在这种情况下也不会造成问题。

但是现在我的监控系统需要知道我的服务实例了!

使用 pull 模式,你的监控系统需要知道哪些服务实例存在,以及如何连接到它们。有些人担心这会在监控系统方面需要额外的配置,并将其视为一个运维上的可扩展性问题。

我们会说,对于任何严肃的监控设置,无论如何你都无法逃避这种配置工作:如果你的监控系统不知道世界*应该*是什么样子,哪些被监控的服务实例*应该*存在,它又怎么能判断出一个实例是从未上报数据、因故障宕机,还是真的不再应该存在了?只有当你完全不关心单个实例的健康状况时,这才可以接受,比如你只运行一些临时的 worker,只要有足够多的 worker 上报结果就够了。大多数环境并非完全如此。

如果监控系统无论如何都需要知道世界的期望状态,那么 push 模式实际上需要*更多*的配置。不仅你的监控系统需要知道哪些服务实例应该存在,你的服务实例现在也需要知道如何联系你的监控系统。Pull 模式不仅需要的配置更少,还使你的监控设置更加灵活。使用 pull 模式,你可以在笔记本电脑上运行一个生产监控的副本进行实验。它还允许你使用其他工具获取指标,或手动检查指标端点。为了实现高可用性,pull 模式允许你并行运行两个配置完全相同的 Prometheus 服务器。最后,如果你需要移动监控系统的访问端点,pull 模式不需要你重新配置所有的指标源。

在实践层面,Prometheus 通过其内置对各种云提供商和容器调度系统的服务发现机制的支持,使得配置世界的期望状态变得容易:Consul、Marathon、Kubernetes、EC2、基于 DNS 的 SD、Azure、Zookeeper Serversets 等等。如果需要,Prometheus 还允许你接入自己的自定义机制。在微服务世界或任何多层架构中,如果你的监控系统使用与你的服务实例发现其后端相同的方法来发现监控目标,这本身就是一个根本性的优势。这样你就可以确保你正在监控的是为生产流量提供服务的相同目标,而且你只需要维护一种发现机制。

意外地 DDoS 你的监控系统

无论是 pull 还是 push,任何时序数据库在接收到超出其处理能力的样本时都会崩溃。然而,根据我们的经验,push 模式更有可能意外地搞垮你的监控系统。如果对从哪些实例接收哪些指标的控制不是集中式的(在你的监控系统中),那么你就有可能面临实验性或失控的任务突然向你的生产监控系统推送大量垃圾数据并使其崩溃的风险。虽然 pull 模式下仍然有很多方式可能发生这种情况(它只控制从哪里拉取指标,而不控制指标负载的大小和性质),但风险较低。更重要的是,这类事件可以在一个中心点得到缓解。

现实世界的证明

除了 Prometheus 已经在现实世界中被用于监控非常庞大的系统(例如用于在 DigitalOcean 监控数百万台机器 )之外,还有其他著名的例子表明,基于 pull 模式的监控在可能的最大规模环境中也取得了成功。Prometheus 的灵感来源于谷歌的 Borgmon,它曾经(并且部分现在仍然)在谷歌内部用于通过 pull 模式监控其所有关键的生产服务。我们在谷歌遇到的 Borgmon 的任何扩展性问题也都不是由其 pull 模式引起的。如果一个 pull 模式能够扩展到一个拥有数十个数据中心和数百万台机器的全球性环境,你很难说 pull 模式无法扩展。

但是 pull 模式还有其他问题!

确实存在一些难以用 pull 模式监控的场景。一个典型的例子是,当你有许多分散在世界各地的端点,由于防火墙或复杂的网络设置而无法直接访问,并且在每个网络分段中直接运行 Prometheus 服务器是不可行的。这并非 Prometheus 设计初衷所针对的环境,尽管通常可以找到变通办法(通过 Pushgateway 或重构你的架构)。无论如何,这些关于 pull 模式监控的遗留问题通常与扩展性无关,而是由于围绕开放 TCP 连接的网络操作困难所致。

那么一切都好了吗?

本文探讨了围绕 pull 模式监控最常见的可扩展性问题。鉴于 Prometheus 和其他基于 pull 模式的系统在非常庞大的环境中成功使用,并且 pull 模式在现实中并未构成瓶颈,结果应该很明确:“pull 模式无法扩展”的论点并非一个真正的问题。我们希望未来的讨论能集中在比这个转移视线的论点更重要的方面。