目录
前言
上一篇笔记在介绍 kubernetes 水平自动伸缩实现前先预设了整体架构。本篇笔记将围绕 kubernetes 的采集器 Metrics Server 进行介绍。
架构
kubernetes 采集器的架构如下图所示:
对比 kubernetes 水平自动伸缩学习笔记(一) 中我们设计的架构主要有以下区别:
- HPA 控制器和 API 服务器交互,而不是直接和采集器 Metrics-Server 交互,需要 API 服务器开启聚合服务。这种设计的优势在于:1. 规整,组件和 kube-apiserver 交互,不单独交互;2. 认证、鉴权、准入等由 kube-apiserver 支持,复用现有组件能力;
- Metrics-Server 不仅支持 HPA 还支持 kubectl top 展示节点/pod 资源使用情况,方便 debug 等操作;
设计
本篇学习笔记基于 metrics-server release-0.6 进行介绍。
metrics-server release-0.6 有以下核心设计要点:
- 并发+随机采集,防止并发同时采集导致网络拥塞等情况;
- kubelet 采集 api 从 /metrics/summary 更新为 /metrics/resource,获取指标数据更加精简,节省内存和序列化/反序列化操作;
- metrics-server 采集 cpu 和 memory 指标,引入 prev 和 last 存指标以计算 cpu 使用率;
这里面有一些要考虑的问题:
- 如果资源指标缺失怎么办?比如 pod 的 container 资源没采集到或者 Deployment 的 pod 资源采集不到,metrics-server 会如何处理?
- 如果 prev 和 last 数据不一致怎么办?比如 prev 有 pod-a 的 cpu 指标,但是 last 却没有 pod-a 的 cpu 指标,metrics-server 会如何处理?
这两个问题可以归为一类回答,采集器作为独立的组件并不和具体业务耦合。那么它要做的就是尊重事实(而不是像 hpa controller 尊重业务)。
metrics-server 会尽可能推导出 prev/last 的资源使用率但是尊重事实。
对于第一问,如果没有采集到则不采集,不推断/重采缺失指标。
对于第二问,如果不一致则清空 prev/last 不计算该 pod 的使用率。
源码
本节从源码角度分析 metrics-server 的工作流程。
轮询采集
go
func (s *server) runScrape(ctx context.Context) {
ticker := time.NewTicker(s.resolution)
defer ticker.Stop()
s.tick(ctx, time.Now())
for {
select {
case startTime := <-ticker.C:
// 采集器开始采集
s.tick(ctx, startTime)
case <-ctx.Done():
return
}
}
}
创建定时器定时采集,采集周期默认为 15s,可配置。
并发随机采集
go
func (c *scraper) Scrape(baseCtx context.Context) *storage.MetricsBatch {
// informer 从 apiserver 获取节点信息
nodes, err := c.nodeLister.List(labels.Everything())
if err != nil {
...
}
// 采集的节点资源写到 responseChannel 通道
responseChannel := make(chan *storage.MetricsBatch, len(nodes))
defer close(responseChannel)
startTime := myClock.Now()
// 这段代码很有意思,设置随机延迟时间,防止同时并发采集导致网络拥塞
delayMs := delayPerSourceMs * len(nodes)
if delayMs > maxDelayMs {
delayMs = maxDelayMs
}
for _, node := range nodes {
// 并发采集
go func(node *corev1.Node) {
// 协程内部休眠,休眠会让出 CPU 避免网络拥塞
sleepDuration := time.Duration(rand.Intn(delayMs)) * time.Millisecond
time.Sleep(sleepDuration)
// 防止请求超时
ctx, cancelTimeout := context.WithTimeout(baseCtx, c.scrapeTimeout-sleepDuration)
defer cancelTimeout()
// 采集节点指标
m, err := c.collectNode(ctx, node)
if err != nil {
...
}
// 将采集结果写到 responseChannel 通道
responseChannel <- m
}(node)
}
res := &storage.MetricsBatch{
Nodes: map[string]storage.MetricsPoint{},
Pods: map[apitypes.NamespacedName]storage.PodMetricsPoint{},
}
for range nodes {
srcBatch := <-responseChannel
if srcBatch == nil {
continue
}
// 收集 node 指标
for nodeName, nodeMetricsPoint := range srcBatch.Nodes {
if _, nodeFind := res.Nodes[nodeName]; nodeFind {
klog.ErrorS(nil, "Got duplicate node point", "node", klog.KRef("", nodeName))
continue
}
res.Nodes[nodeName] = nodeMetricsPoint
}
// 收集 pod 指标
for podRef, podMetricsPoint := range srcBatch.Pods {
if _, podFind := res.Pods[podRef]; podFind {
klog.ErrorS(nil, "Got duplicate pod point", "pod", klog.KRef(podRef.Namespace, podRef.Name))
continue
}
res.Pods[podRef] = podMetricsPoint
}
}
return res
}
这里并发采集有点意思,如果用协程分批次收集可能是一种反模式。虽然能控制协程数,但是收集的总时间延长了,并不好。当然如果超大规模集群这么并发创建协程还是有风险的。
指标收集
通过 kubelet client 收集指标:
go
func (c *scraper) collectNode(ctx context.Context, node *corev1.Node) (*storage.MetricsBatch, error) {
...
// kubelet client 收集指标
ms, err := c.kubeletClient.GetMetrics(ctx, node)
...
return ms, nil
}
func (kc *kubeletClient) GetMetrics(ctx context.Context, node *corev1.Node) (*storage.MetricsBatch, error) {
...
url := url.URL{
Scheme: kc.scheme,
Host: net.JoinHostPort(addr, strconv.Itoa(port)),
Path: "/metrics/resource",
}
// get /metrics/resource api 收集指标
return kc.getMetrics(ctx, url.String(), node.Name)
}
存储指标
存储指标是重点,指标通过哈希表的形式存储以实现 O(1) 时间复杂度查询。查看实现如下:
go
func (s *server) tick(ctx context.Context, startTime time.Time) {
...
// 采集所有节点的指标
data := s.scraper.Scrape(ctx)
// 存储指标
s.storage.Store(data)
...
}
func (s *storage) Store(batch *MetricsBatch) {
s.mu.Lock()
defer s.mu.Unlock()
// 存储指标到 nodes
s.nodes.Store(batch)
// 存储指标到 pods
s.pods.Store(batch)
}
收集的指标根据 node 或 pod 分开存储到内存 storage 中,代码见 这里。
把存储逻辑拆成如下分支:
| 分支 | 条件 | 行为 |
|---|---|---|
| 新鲜容器 (L121) | StartTime < Timestamp 且 Δt ∈ [10s, metricResolution) |
合成 prev:(Timestamp=StartTime, CumulativeCpuUsed=0),保证首次采集也能算 CPU rate |
| 未重启且新点更新 (L130) | last 中有该容器且 StartTime < last.Timestamp 且 new.Timestamp > last.Timestamp |
把旧 last 点降级为 prev(滚动窗口) |
| 时间未前进 (L133-L143) | new.Timestamp ≤ last.Timestamp 时看 s.prev |
prev 比新点旧则保留 prev;否则丢弃(防止窗口倒退) |
| 重启/无历史 | StartTime ≥ last.Timestamp 或 last/prev 中无该容器 |
不写 prev,该容器本轮无法返回指标(allContainersPresent 在 GetMetrics 中会整 pod 丢弃) |
从 metrics-server 的行为看,存储是尊重事实的,如果指标在 prev/last 缺失,不能构造 pre/last 区间指标的话就不存。
具体的业务逻辑由上层应用去处理,解耦业务和通用组件。
性能
metrics-server 的性能可以参考 官网,这里就不过多介绍了。
调优
对于 metrics-server 的调优可以从以下角度入手:
- 调整采集周期;
- 更新 cpu/memory 容量;