Prometheus监测目标数据加工过程

Prometheus监测目标数据加工过程

  1. 简介

Prometheus的自动发现功能可以动态扫描并获取监测目标(Targets),而目标的数量可能会随着时间而增加或者减少。那么面对这些不断变化的监测目标,Prometheus是如何及时准确高效地从每个目标采集监测数据的呢?本文试图通过考察监测目标数据的加工过程来回答这一问题。

监测目标数据来自自动发现协程的输出,其本身并非采集的样本,但是它决定了从哪些目标采集样本,可以认为这些数据是控制数据。这些监测目标数据是以作业名称(job_name)为键的字典,这种结构很自然地导向基于作业(job)维度的并发采样,采样管理器也确实是这么做的。具体地说,采样管理器为每个作业创建一个采样池,每个采样池在自己的协程中处理一个作业的所有监测目标,作业数越多并发量也就越高。

  1. 原理架构

2.1 目标数据加载协程(reloader)

目标数据可以认为是配置信息(元数据),负责处理此类数据的协程称为reloader。reloader协程由管理器协程创建,是管理器的子协程,它与管理器协程之间经由triggerReload通道进行通信与协调,该通道的长度为1。当需要reloader协程工作时,管理器通过triggerReload通道向其发送信号,reloader协程则每隔5秒检查一次triggerReload通道,如果有信号存在就立刻开始工作,否则就等待。因此,管理器控制着reloader协程的工作次数,而reloader协程自己控制着工作的频率,两者各取所需。

监测目标数据存储在管理器的targetSets字段,管理器协程在给reloader发送信号之前已经完成了对targetSets字段的更新,而reloader协程每次工作时只需要直接读取监测目标数据。两者之间对监测数据的读写冲突通过管理器的互斥锁(mtxScrape)解决,两者在读写过程中均须持有该锁,并在读写结束后释放它。由于reloader协程两次工作之间存在5秒的间隔,在此期间管理器协程完全有机会再次获得锁并更新监测目标数据。这种情况下,等到reloader开始工作时监测目标有可能已经发生了两次更新。然而,这两次更新并不会导致发送两次信号,因为triggerReload通道的长度为1,在reloader协程将信号消费掉之前管理器会放弃发送信号。这一机制有赖于default分支的存在,如代码片段1所示。另一方面,由于互斥锁的存在,当reloader协程正在工作时,管理器无法获得锁从而不可能更新监测数据。

代码片段1 管理器向reloader发送信号时的default分支

	select {
	case ts := <-tsets:							//当tsets通道有内容时
		m.updateTsets(ts)						//更新监测目标数据
		select {
		case m.triggerReload <- struct{}{}:	//向reloader协程发送信号
		default:								//通道阻塞时放弃发送信号
		}
	case <-m.graceShut:
		return nil
	}

reloader协程的工作内容很多,为了提高工作效率,它通过数据分解以及协程创建实现了作业维度的并发。具体而言,它为每个作业创建一个采样池(scrapePool实例),并为每个采样池启动一个协程来处理该采样池的数据。由于监测目标数据本身就是基于作业名称的字典,所以这种作业维度的并发方式实现起来非常便捷。采样池所使用的数据主要是配置信息、监测目标、HTTP客户端和数据库接口。在作业维度上,配置信息和监测目标数据在不同作业之间都是相互隔离彼此独立的,而HTTP客户端的创建也保证了各个作业之间不会共用。这种数据和资源层面的独立性为并发提供了保障,各个采样池协程之间不会出现冲突,不必花费协调方面的成本。

reloader协程负责为每一个目标集创建采样池,如果检查发现某个目标集缺少采样池,则尝试给它创建一个(前提是该目标集的配置信息是存在的,如果不存在,说明该目标集已经被放弃,也就不需要创建采样池)。总之,reloader协程加载的是目标集,它对每一个目标集负责。

2.2 采样池scrapePool的数据同步

reloader协程创建的每个采样池都记录在管理器的scrapePools字段中,该字段为字典结构,以作业名称为键,保证了每个作业最多创建一个采样池。当reloader协程第一次加载监测目标数据时,此时尚未创建任何采样池,所以加载过程中遇到的每个作业都需要创建一个采样池。采样池新创建时是空的,不含任何监测目标(但是HTTP客户端已经创建),此时reloader首先要做的是将监测目标数据同步到该采样池中。

从自动发现协程接收的监测目标数据不能直接给采样池使用,因为这些监测目标的标签还没有经过重新打标(relabel)。在数据同步过程中,一方面需要为每个监测目标重新打标,另一方面需要为每个活动的目标创建采样循环(loop协程)。如果某个作业的监测目标数量很大,其数据同步过程无疑会花费较长的时间,此时如果采用串行方式无疑会影响其他作业的数据同步。考虑到采样池之间的数据独立性,各个采样池的数据同步采用并发的方式进行,即整体的数据同步时间取决于花费时间最长的采样池。一旦采样池的数据同步完成,相应的协程也就终止了(但是该过程中创建的loop协程还继续存在)。

当然,并不是每次加载监测目标数据都需要创建所有采样池。当第2次加载监测目标时,如果发现某个作业的采样池已经存在,就不需要再创建采样池,直接进行数据同步即可。数据同步是以采样池为单位进行的,各个采样池之间相互独立,并发地进行。

数据同步的第一步是调整监测目标的标签,包括补充新标签以及修改现有标签(relabel,此处为监测目标标签,而非时间序列样本标签)。任何监测目标,无论是否曾经同步过,都需要经历调整标签这一步。所以,即使本次同步的监测目标与上次同步时完全一致也需要调整标签。调整标签时需要为每个监测目标新建标签集并按照relabel规则对其进行修整。

代码片段2 数据同步协程使用的Target结构体

	type Target struct {
		discoveredLabels labels.Labels		//自动发现协程获取的监测目标标签
		labels labels.Labels				//数据同步协程调整之后的标签(含自动发现的标签)
		params url.Values					//配置信息中的params参数
		mtx                sync.RWMutex		//用于协调对metadata、健康状态、标签集等字段的读写访问
		lastError          error
		lastScrape         time.Time
		lastScrapeDuration time.Duration
		health             TargetHealth
		metadata           MetricMetadataStore	//元数据,loop协程处理采样数据时使用
	}

补充的新标签来自配置文件中的6个参数,即job_name、scrape_interval、scrape_timeout、metrics_path、scheme和params。

relabel规则对标签的修整是在补充新标签之后进行的,所以新补充的标签也可以纳入修整的范围。relabel支持7种方式,即Keep、Drop、Replace、HashMod、LabelDrop、LabelKeep、LabelMap,其中前4种是针对标签值,后3种针对标签名称。基于标签值的修整需要限定范围(source_labels参数),基于标签名称的修整则是针对所有标签。除了HashMod之外,其他操作均需要进行至少一次正则匹配。

  • Keep,当标签值不符合指定规则时该监测目标的标签将被忽略(监测目标列入droppedTargets),否则保持原样;
  • Drop,当标签值符合指定规则时将被忽略(监测目标列入droppedTargets),否则保持原样;
  • Replace,当标签值匹配成功时,将指定标签的值修改为指定内容;
  • HashMod,不进行字符串匹配,直接计算标签值的哈希余数,将该值设置为指定标签的值;
  • LabelDrop,检查所有标签,如果标签名称符合指定规则,删除该标签,如果最后不剩任何标签,则该监测目标将被列入droppedTargets;
  • LabelKeep,检查所有标签,如果标签名称不符合指定规则,删除该标签,如果最后不剩任何标签,则该监测目标将被列入droppedTargets;
  • LabelMap,检查所有标签,如果标签名称符合指定规则,将该标签名称替换为指定的字符串。

经过调整标签之后,原始的监测目标数据往往会扩增一些标签,同时监测目标被分为两类,即需要执行采样的目标(下称活动目标)和放入droppedTargets中的目标(不需要采样)。下一步要做的是为活动目标创建loop协程并启动loop。为了避免浪费资源,如果某个目标在上次同步时已经创建了loop协程,则本次不需要重复创建,Prometheus通过计算并比较监测目标的哈希值来判断该目标是否已经存在。

需要注意的是,数据同步过程中需要对采样池内的监测目标数据进行读写,如果某个采样池正在进行数据同步的同时又需要进行采样池配置信息加载,就可能发生数据访问冲突。所以,无论是在数据同步时还是在加载采样池配置信息过程中都需要持有targetMtx锁。实际上,当某个作业取消,需要终止采样池及其loop协程时,也涉及对监测目标的修改,同样需要持有targetMtx锁。

2.3 loop协程的创建与运行

loop协程的任务是对监测目标进行采样,要完成这一工作就需要准备一些必备条件,包括用于HTTP通信的客户端、采样超时时长参数、采样间隔参数、采样结果的最大长度、单次采样的样本数量限额、样本标签relabel配置,以及用于控制协程的上下文、用于暂存采样结果的缓冲池、用于输出样本值的数据库接口等。其中HTTP客户端、父级上下文和缓冲池在创建采样池时就已经存在,这些资源在同一采样池内由所有监测目标共享。缓冲池虽然由所有loop协程共享,但是每个协程只使用其中的一个区域,所以各协程对缓冲区的访问不会发生冲突。数据库接口则由storage.Appender定义,loop协程主要使用该接口的append方法将样本值添加到数据库中。而剩余的一些参数信息主要从配置文件加载获得。类似于采样池之间的相互独立,各loop协程之间也是独立的,彼此没有共享数据。

一旦loop协程创建完毕,对监测目标数据的加工就结束了,至此监测目标数据完成了它的使命,即启动对所有监测目标的采样过程。可见新建loop协程是相对复杂的过程,而loop协程的终止要简单得多,只需要调用上下文的cancel函数从而向Done通道发送信号,loop协程每次循环都会检查Done通道,从而可以在识别到信号后终止自己。

2.4 采样时间偏置——jitterSeed

jitterSeed是一个64位整数,该值根据服务器主机名称和外部标签(external_labels)组成的字符串进行哈希计算得到。也就是说,同一主机上如果有两个Prometheus服务并且配置相同的外部标签,那么两者生成的jitterSeed是相同的。外部标签可以视为对Prometheus服务的一种身份描述,不同的服务应设置不同的标签。在进行采样时,Prometheus服务需要与外部服务进行交互,此时可以用外部标签表明自己的身份。采样管理器可以下辖多个采样池,但是所有采样池及其内部的所有loop协程都会使用相同的jitterSeed,也就是采样管理器的jitterSeed。

该种子值的作用在于计算每个监测目标的下次采样时间。假设Prometheus的采样时间戳按照interval对齐,也就是保证每次采样的时间戳都是interval的倍数。这种情况下如果没有抖动机制,所有具有相同时间间隔(interval)的监测目标将在同一时间点同时采样,这造成的结果就是在时间维度上,负载被集中到一个时间点上。所以需要一种机制在时间维度上将压力分散开,也就是将单个时间点的压力尽可能平均地分散到interval区间内。实现这一目标的方法之一是基于监测目标的哈希值实现,可以认为每个监测目标的哈希值是随机整数,如果用该哈希值对interval取余(下称时间偏置量),就可以使用该余数来对采样时间进行偏置,从而达到分散压力的效果。这种方法在只有1个Prometheus服务的情况下是可行的,如果同时存在多个Prometheus服务,这种方法就会产生另一个问题。假设两个Prometheus服务以相同间隔对同一个监测目标采样,由于监测目标相同,该监测目标在两个Prometheus服务中的时间偏置量也相同。最终的结果就是,两个服务总是同时对该监测目标采样。从监测目标的角度出发,这可能会造成一定的压力,并可能导致同步与协调方面的问题。所以,Prometheus在进行时间偏置量计算时并非单纯依赖监测目标的哈希值,而是将哈希值与jitterSeed结合(两者进行位与运算),这样就可以避免同时对监测目标进行采样的问题。当然,如果两个Prometheus使用相同的jitterSeed,那么跟不使用jitterSeed效果是一样的。

代码片段3 时间偏置量的计算

	func (t *Target) offset(interval time.Duration, jitterSeed uint64) time.Duration {
		now := time.Now().UnixNano()
		var (
			base   = int64(interval) - now%int64(interval)
			offset = (t.hash() ^ jitterSeed) % uint64(interval)	//哈希值与jitterSeed结合
			next   = base + int64(offset)
		)
		if next > int64(interval) {
			next -= int64(interval)
		}
		return time.Duration(next)
	}

基于上述规则,无论各个作业的采样间隔是否相同,采样操作在时间维度上的总体分布密度都是均衡的。假设有两个作业,其中一个包含100个监测目标且采样间隔为10秒,另一个包含500个监测目标且采样间隔为25秒,那么前者的密度为每秒10个目标,后者的密度为每秒20个目标,两者合计的密度为每秒30个目标。即使将每个目标的样本数量考虑进来,仍然可以实现时间维度上的均衡。假设前者每个目标返回60个样本,后者每次返回80个样本,则可以合理地期望平均每秒返回2200(10*60 + 20*80)个样本。

任意一个loop协程只需要最初计算一次时间偏置量,一旦计算完毕并确定了首次采样时间点,此后的每次采样只需要间隔interval就可以。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值