metrics-server源码中的缓存策略:LRU与TTL设计
引言:缓存策略在Kubernetes监控中的关键作用
在Kubernetes(K8s)生态系统中,metrics-server作为资源指标聚合器,承担着为HPA(Horizontal Pod Autoscaler)等核心组件提供实时容器资源数据的重要角色。其缓存系统的设计直接影响着指标采集的实时性、系统资源占用以及整体稳定性。本文将深入剖析metrics-server源码中的缓存实现,重点解析LRU(Least Recently Used,最近最少使用)与TTL(Time-To-Live,生存时间)两种缓存淘汰策略的设计理念、实现细节及其在Kubernetes环境中的应用场景与挑战。
当集群规模达到数千节点和数万Pod时,metrics-server面临着每秒数千次的指标查询请求。若每次请求都直接从Kubelet拉取原始数据,不仅会显著增加网络带宽消耗,还会导致Kubelet API负载过高,甚至引发指标获取延迟,影响HPA的决策速度。因此,一个高效的缓存系统是metrics-server高性能运行的基石。metrics-server通过精妙的LRU与TTL结合的缓存策略,在保证数据新鲜度的同时,最大限度地减少了重复计算和网络请求,从而支撑起大规模Kubernetes集群的稳定运行。
metrics-server缓存系统架构概览
metrics-server的缓存系统主要集中在pkg/storage包中,通过storage.go、node.go、pod.go和types.go等核心文件实现。其整体架构采用了双层缓存结构,分别针对节点(Node)和Pod/容器(Container)的指标数据进行缓存管理。
Storage结构体是缓存系统的入口,它内部维护了nodeStorage和podStorage两个子缓存,分别用于节点和Pod/容器的指标数据。nodeStorage和podStorage都采用了类似的设计模式,即通过两个映射(map)last和prev来存储最近两次抓取的指标数据点(MetricsPoint)。这种双缓存设计是实现TTL策略的核心基础,我们将在后续章节详细阐述。
MetricsBatch结构体用于封装一次指标抓取周期内获取到的所有节点和Pod指标数据。MetricsPoint则是指标数据的最小单元,包含了指标的开始时间(StartTime)、采集时间(Timestamp)、累积CPU使用量(CumulativeCPUUsed)和内存使用量(MemoryUsage)等关键信息。这些结构体的定义为缓存策略的实现提供了数据基础。
TTL(Time-To-Live)策略的实现与应用
TTL策略的核心思想
TTL策略是一种基于时间的缓存淘汰机制,它为每个缓存项设置一个固定的生存时间。当缓存项的存储时间超过其TTL值时,该缓存项将被自动淘汰。在metrics-server中,TTL策略主要通过维护两个连续的指标数据点(last和prev)来实现,这两个数据点之间的时间间隔即为缓存的有效窗口。
源码中的TTL实现细节
在nodeStorage的Store方法中,我们可以清晰地看到TTL策略的实现逻辑:
func (s *nodeStorage) Store(batch *MetricsBatch) {
lastNodes := make(map[string]MetricsPoint, len(batch.Nodes))
prevNodes := make(map[string]MetricsPoint, len(batch.Nodes))
for nodeName, newPoint := range batch.Nodes {
if _, exists := lastNodes[nodeName]; exists {
klog.ErrorS(nil, "Got duplicate node point", "node", klog.KRef("", nodeName))
continue
}
lastNodes[nodeName] = newPoint
if lastNode, found := s.last[nodeName]; found {
// If new point is different then one already stored
if newPoint.Timestamp.After(lastNode.Timestamp) {
// Move stored point to previous
prevNodes[nodeName] = lastNode
} else if prevPoint, found := s.prev[nodeName]; found {
if prevPoint.Timestamp.Before(newPoint.Timestamp) {
// Keep previous point
prevNodes[nodeName] = prevPoint
} else {
klog.V(2).InfoS("Found new node metrics point is older than stored previous, drop previous",
"node", nodeName,
"previousTimestamp", prevPoint.Timestamp,
"timestamp", newPoint.Timestamp)
}
}
}
}
s.last = lastNodes
s.prev = prevNodes
// Only count last for which metrics can be returned.
pointsStored.WithLabelValues("node").Set(float64(len(prevNodes)))
}
上述代码展示了节点指标数据的存储过程。lastNodes和prevNodes分别存储当前和上一次抓取的节点指标数据。对于每个新抓取到的指标点(newPoint),如果它的时间戳(Timestamp)晚于last中存储的时间戳,则将last中的旧数据移至prev,并将新数据存入last。这意味着prev中存储的是上一个周期的指标数据,其与last中数据的时间差即为TTL窗口。
在GetMetrics方法中,只有当last和prev中都存在某个节点的指标数据时,才能计算出该节点的资源使用率:
func (s *nodeStorage) GetMetrics(nodes ...*corev1.Node) ([]metrics.NodeMetrics, error) {
results := make([]metrics.NodeMetrics, 0, len(nodes))
for _, node := range nodes {
last, found := s.last[node.Name]
if !found {
continue
}
prev, found := s.prev[node.Name]
if !found {
continue
}
rl, ti, err := resourceUsage(last, prev)
if err != nil {
klog.ErrorS(err, "Skipping node usage metric", "node", node)
continue
}
results = append(results, metrics.NodeMetrics{
ObjectMeta: metav1.ObjectMeta{
Name: node.Name,
Labels: node.Labels,
CreationTimestamp: metav1.NewTime(time.Now()),
},
Timestamp: metav1.NewTime(ti.Timestamp),
Window: metav1.Duration{Duration: ti.Window},
Usage: rl,
})
}
return results, nil
}
resourceUsage函数利用last和prev两个数据点计算资源使用率:
func resourceUsage(last, prev MetricsPoint) (corev1.ResourceList, api.TimeInfo, error) {
if last.StartTime.Before(prev.StartTime) {
return corev1.ResourceList{}, api.TimeInfo{}, fmt.Errorf("unexpected decrease in startTime of node/container")
}
if last.CumulativeCPUUsed < prev.CumulativeCPUUsed {
return corev1.ResourceList{}, api.TimeInfo{}, fmt.Errorf("unexpected decrease in cumulative CPU usage value")
}
window := last.Timestamp.Sub(prev.Timestamp)
cpuUsage := float64(last.CumulativeCPUUsed-prev.CumulativeCPUUsed) / window.Seconds()
return corev1.ResourceList{
corev1.ResourceCPU: uint64Quantity(uint64(cpuUsage), resource.DecimalSI, -9),
corev1.ResourceMemory: uint64Quantity(last.MemoryUsage, resource.BinarySI, 0),
}, api.TimeInfo{
Timestamp: last.Timestamp,
Window: window,
}, nil
}
这里的window变量就是last和prev两个数据点之间的时间差,即TTL窗口。CPU使用率的计算依赖于这个时间窗口内累积CPU使用量的变化。
TTL策略在metrics-server中的应用场景
TTL策略在metrics-server中主要应用于以下场景:
- 指标数据的新鲜度保证:通过固定的抓取周期(
metricResolution),确保缓存中的数据不会过时太久。默认情况下,metrics-server的抓取周期为15秒,这意味着缓存数据的TTL约为15秒。 - 资源使用率计算:如前所述,资源使用率的计算需要两个连续的指标数据点。TTL策略确保了这两个数据点之间有足够的时间间隔,从而使计算结果更加准确。
- 平滑指标波动:通过使用两个连续周期的指标数据进行计算,可以在一定程度上平滑瞬时的指标波动,提供更稳定的资源使用率数据。
LRU(Least Recently Used)策略的隐含实现与分析
LRU策略的核心思想
LRU策略是一种基于访问频率的缓存淘汰机制,它认为最近被访问的数据在未来一段时间内被再次访问的概率更高,因此当缓存空间满时,会优先淘汰最久未被访问的数据。虽然metrics-server的源码中没有显式使用传统的LRU缓存结构(如带有双向链表的哈希表),但其缓存实现中蕴含了LRU的思想。
metrics-server中LRU思想的体现
在podStorage的Store方法中,我们可以看到LRU思想的体现:
func (s *podStorage) Store(newPods *MetricsBatch) {
lastPods := make(map[apitypes.NamespacedName]PodMetricsPoint, len(newPods.Pods))
prevPods := make(map[apitypes.NamespacedName]PodMetricsPoint, len(newPods.Pods))
var containerCount int
for podRef, newPod := range newPods.Pods {
podRef := apitypes.NamespacedName{Name: podRef.Name, Namespace: podRef.Namespace}
if _, found := lastPods[podRef]; found {
klog.ErrorS(nil, "Got duplicate pod point", "pod", klog.KRef(podRef.Namespace, podRef.Name))
continue
}
newLastPod := PodMetricsPoint{Containers: make(map[string]MetricsPoint, len(newPod.Containers))}
newPrevPod := PodMetricsPoint{Containers: make(map[string]MetricsPoint, len(newPod.Containers))}
for containerName, newPoint := range newPod.Containers {
if _, exists := newLastPod.Containers[containerName]; exists {
klog.ErrorS(nil, "Got duplicate Container point", "container", containerName, "pod", klog.KRef(podRef.Namespace, podRef.Name))
continue
}
newLastPod.Containers[containerName] = newPoint
if newPoint.StartTime.Before(newPoint.Timestamp) && newPoint.Timestamp.Sub(newPoint.StartTime) < s.metricResolution && newPoint.Timestamp.Sub(newPoint.StartTime) >= freshContainerMinMetricsResolution {
copied := newPoint
copied.Timestamp = newPoint.StartTime
copied.CumulativeCPUUsed = 0
newPrevPod.Containers[containerName] = copied
} else if lastPod, found := s.last[podRef]; found {
// Keep previous metric point if newPoint has not restarted (new metric start time < stored timestamp)
if lastContainer, found := lastPod.Containers[containerName]; found && newPoint.StartTime.Before(lastContainer.Timestamp) {
// If new point is different then one already stored
if newPoint.Timestamp.After(lastContainer.Timestamp) {
// Move stored point to previous
newPrevPod.Containers[containerName] = lastContainer
} else if prevPod, found := s.prev[podRef]; found {
if prevPod.Containers[containerName].Timestamp.Before(newPoint.Timestamp) {
// Keep previous point
newPrevPod.Containers[containerName] = prevPod.Containers[containerName]
} else {
klog.V(2).InfoS("Found new containerName metrics point is older than stored previous , drop previous",
"containerName", containerName,
"pod", klog.KRef(podRef.Namespace, podRef.Name),
"previousTimestamp", prevPod.Containers[containerName].Timestamp,
"timestamp", newPoint.Timestamp)
}
}
}
}
}
containerPoints := len(newPrevPod.Containers)
if containerPoints > 0 {
prevPods[podRef] = newPrevPod
}
lastPods[podRef] = newLastPod
// Only count containers for which metrics can be returned.
containerCount += containerPoints
}
s.last = lastPods
s.prev = prevPods
pointsStored.WithLabelValues("container").Set(float64(containerCount))
}
在这段代码中,lastPods和prevPods是全新创建的映射,它们只包含当前抓取周期内获取到的Pod指标数据。这意味着,对于那些在当前抓取周期内没有新数据的Pod,它们的旧数据将不会被复制到新的lastPods和prevPods中,从而被自动淘汰。这种机制实际上实现了一种简化的LRU策略:只有最近被访问(抓取)的Pod数据才会被保留在缓存中,而长时间未被访问的Pod数据则会被自动剔除。
LRU与TTL的协同工作
在metrics-server的缓存系统中,LRU和TTL策略并非孤立存在,而是协同工作,共同优化缓存性能:
- TTL保证数据时效性:TTL策略确保缓存中的数据不会超过一定的时间窗口,从而保证了提供给HPA等组件的指标数据的新鲜度。
- LRU优化缓存空间:LRU思想(通过在每个抓取周期重建缓存映射实现)确保了缓存中只保留最近活跃的Pod和节点的指标数据,避免了缓存空间的无限增长。
- 双重淘汰机制:一个缓存项要被淘汰,需要同时满足两个条件:超过TTL时间未更新,并且在最近的抓取周期中没有被访问。这种双重淘汰机制使得缓存系统更加高效和可靠。
缓存策略的性能考量与调优
缓存命中率分析
缓存命中率是衡量缓存策略有效性的关键指标。在metrics-server中,缓存命中率主要受以下因素影响:
- 抓取周期(metricResolution):默认的抓取周期为15秒。如果抓取周期过短,会导致缓存中的数据更新过于频繁,可能降低命中率;如果抓取周期过长,则可能导致数据新鲜度不足。
- 集群规模与Pod生命周期:在大规模集群中,Pod的创建和销毁非常频繁。LRU策略(通过重建缓存映射实现)可以自动淘汰那些已被销毁或长时间未被访问的Pod的缓存数据,从而保持较高的缓存命中率。
- 指标查询模式:HPA等组件通常会定期查询特定Pod的指标数据。如果查询模式比较固定,缓存命中率会相对较高。
缓存大小的动态调整
metrics-server的缓存大小会随着集群中活跃Pod和节点的数量动态变化。在Store方法中,lastPods和prevPods的初始容量被设置为len(newPods.Pods),这意味着缓存大小会根据每次抓取到的Pod数量进行调整。这种动态调整机制使得缓存空间能够被高效利用,避免了内存资源的浪费。
缓存策略的调优建议
基于以上分析,我们可以提出以下缓存策略的调优建议:
- 根据集群规模调整抓取周期:对于大规模集群(节点数超过1000),可以适当延长抓取周期(如从15秒增加到30秒),以减少Kubelet API的负载和网络带宽消耗。但需要注意的是,过长的抓取周期可能会影响HPA的响应速度。
- 监控缓存命中率:虽然metrics-server没有直接提供缓存命中率的指标,但可以通过监控
pointsStored指标(缓存中的有效数据点数)和查询请求数来间接评估缓存命中率。如果pointsStored指标稳定,而查询请求数增加,可能意味着缓存命中率在下降。 - 考虑使用更高级的LRU实现:目前metrics-server的LRU实现比较简单(通过重建缓存映射实现)。如果未来需要进一步优化缓存性能,可以考虑引入更高级的LRU实现,如带有访问频率记录和自适应淘汰策略的LRU-K算法。
缓存策略在故障场景下的表现
网络分区场景
当metrics-server与部分Kubelet之间发生网络分区时,缓存策略可以起到一定的缓冲作用。在网络分区期间,metrics-server无法从这些Kubelet获取新的指标数据。此时,TTL策略会导致这些节点和Pod的缓存数据在经过一个抓取周期后被标记为无效(prev中没有对应的数据)。当网络恢复后,metrics-server会重新开始抓取这些节点和Pod的指标数据,并在两个抓取周期后恢复正常的指标计算。
Kubelet重启场景
当Kubelet重启后,其提供的指标数据(尤其是累积CPU使用量)会被重置。metrics-server的缓存策略能够通过StartTime字段检测到这种情况:
if last.StartTime.Before(prev.StartTime) {
return corev1.ResourceList{}, api.TimeInfo{}, fmt.Errorf("unexpected decrease in startTime of node/container")
}
if last.CumulativeCPUUsed < prev.CumulativeCPUUsed {
return corev1.ResourceList{}, api.TimeInfo{}, fmt.Errorf("unexpected decrease in cumulative CPU usage value")
}
当检测到StartTime后退或累积CPU使用量减少时,metrics-server会记录错误日志并跳过该指标数据的计算。这种机制可以防止因Kubelet重启导致的错误指标数据被提供给HPA等组件。
总结与展望
缓存策略的核心贡献
metrics-server源码中的缓存策略(结合TTL和LRU思想)是其能够在大规模Kubernetes集群中高效运行的关键因素之一。通过TTL策略,metrics-server保证了指标数据的时效性;通过隐含的LRU策略,实现了缓存空间的动态优化。这种双重策略使得metrics-server能够在资源受限的情况下,为HPA等核心组件提供稳定、可靠的指标数据服务。
未来优化方向
尽管当前的缓存策略已经能够满足大多数场景的需求,但仍有一些潜在的优化方向:
- 引入显式的LRU缓存结构:目前的LRU思想是通过在每个抓取周期重建缓存映射实现的。引入显式的LRU缓存结构(如使用
github.com/hashicorp/golang-lru/v2库)可以更精确地控制缓存的淘汰行为,提高缓存命中率。 - 实现多级缓存:可以考虑实现多级缓存,如一级缓存(内存)和二级缓存(磁盘或分布式缓存),以应对更大规模的集群场景。
- 自适应缓存策略:根据集群的负载情况、Pod生命周期特征等动态调整缓存策略参数(如TTL时间、LRU淘汰阈值等),以实现更优的性能。
通过不断优化缓存策略,metrics-server可以更好地满足Kubernetes集群在可扩展性、可靠性和性能方面的需求,为容器编排平台的稳定运行提供更加强有力的支持。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



