技术

数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

多集群及clusternet学习 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 在离线业务混部 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 概率论 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签


k8s水平扩缩容

2020年10月10日

简介

新玩法:OpenKruise v0.10.0 新特性 WorkloadSpread 解读用户自己规划基础节点池和弹性节点池。应用部署时需要固定数量或比例的 Pod 部署在基础节点池,其余的都扩到弹性节点池。缩容时,优先从弹性节点缩容以节省成本。

如何根据不同业务场景调节 HPA 扩缩容灵敏度未读

Kubernetes Autoscaling in Production: Best Practices for Cluster Autoscaler, HPA and VPA 未读

Kubernetes 弹性伸缩全场景解析 (一)- 概念延伸与组件布局传统上弹性伸缩主要解决的问题是容量规划与实际负载的矛盾,现在是资源成本与可用性之间的博弈。 Kubernetes 弹性伸缩全场景解读(二) - HPA 的原理与演进 Kubernetes 弹性伸缩全场景解析(三) - HPA 实践手册 Kubernetes 弹性伸缩全场景解析 (四)- 让核心组件充满弹性 Kubernetes 弹性伸缩全场景解读(五) - 定时伸缩组件发布与开源

弹性伸缩的五个条件与六个教训

简介

Horizontal Pod AutoscalerThe Horizontal Pod Autoscaler automatically scales the number of Pods in a replication controller, deployment, replica set or stateful set based on observed CPU utilization (or, with custom metrics support, on some other application-provided metrics). Note that Horizontal Pod Autoscaling does not apply to objects that can’t be scaled, for example, DaemonSets.

The Horizontal Pod Autoscaler is implemented as a Kubernetes API resource and a controller. The resource determines the behavior of the controller. The controller periodically adjusts the number of replicas in a replication controller or deployment to match the observed average CPU utilization to the target specified by user. PS: 理想状态从一个确定的replicas 变成了 规则

  1. 面向的k8s object :deployment, replicaset or statefulset
  2. 扩缩容依据:CPU utilization 和 custom metrics
  3. 实现:定义了一个hpa resource ,以及对应的 controller; 与一般controller 略微不同的是 周期性执行(默认15s)

扩缩容策略

扩缩容的计算规则如下,注意不是一个一个加或减,需要设定的 desiredMetricValue 不是阈值,计算公式 从效果上确实是 “currentMetricValue 大于desiredMetricValue 就扩容,反之就缩容”。hpa针对 metirc 直接计算 desiredReplicas(可能一次扩两三个),之后desiredReplicas 与 minReplicas 和maxReplicas 比较得出 最终的replicas。

// ratio 在一定范围内会被忽略(即认为波动太小)
ratio = currentMetricValue / desiredMetricValue 
desiredReplicas = ceil[currentReplicas * ratio]

运行 hpa controller 需要给kube-controller-manager服务增加三个参数:

--horizontal-pod-autoscaler-sync-period=30s \		# hpa 轮询间隔
--horizontal-pod-autoscaler-downscale-delay=3m0s \
--horizontal-pod-autoscaler-upscale-delay=3m0s

这些参数 在v2beta2 hpa 中由 Behavior 替代。Advanced HPA in Kubernetes

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: example-deployment
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: example-deployment
  minReplicas: 1
  maxReplicas: 10
  behavior:
    scaleDown:
      selectPolicy: Disabled			# scaleDown 关闭
    scaleUp:
      stabilizationWindowSeconds: 120
      policies:
      - type: Percent
        value: 10						# 假设当前有 10个 pod,则 10 * 10% =1 ,即每次扩容batch 扩1个
        periodSeconds: 60
      - type: Pods						# 即每次扩容batch 扩4个
        value: 4
        periodSeconds: 60
      selectPolicy: Max					# 假设当前有 10个 pod,按上述两种策略的最大值,每次扩容batch 扩4个
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

示例

hpa 的两个基本要素

  1. minReplicas/maxRepicas 定义扩缩容的上下限
  2. Target metrics/ targetValue threshold,选一个或多个指标,定一个阈值,来评估当前的负载,If the metric readings are above this value, and (currentReplicas < maxReplicas), HPA will scale up.

示例

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
  namespace: default
spec:
  scaleTargetRef:   # HPA的伸缩对象描述,HPA会动态修改该对象的pod数量
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  # HPA的最小pod数量和最大pod数量
  minReplicas: 1
  maxReplicas: 10
  # 监控的指标数组,支持多种类型的指标共存
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

target共有3种类型:Utilization、Value、AverageValue。Utilization表示平均使用率;Value表示裸值;AverageValue表示平均值。

获取metric 数据

指标来源 api metric 已有实现  
Resource metrics.k8s.io cpu/mem metric-server Pod 的资源指标,计算的时候要除以 Pod 数目再对比阈值进行判断
pods custom.metrics.k8s.io   Prometheus Adapter/ Microsoft Azure Adapter/Google Stackdriver 每个 Pod 的自定义指标,计算时要除以 Pods 的数目
object custom.metrics.k8s.io   Prometheus Adapter/ Microsoft Azure Adapter/Google Stackdriver CRD 等对象的监控指标,直接计算指标比对阈值
external external.metrics.k8s.io 比如某个消息队列的长度   refers to a global metric that is not associated with any Kubernetes object

可以看到,除了 external外,metric 一般用来 describe 一个k8s object。可以做一个推断:除了external外,metric 在k8s的视角里, 一般作为k8s object 的附属而存在,这也体现在 使用k8s api 访问metric 的路径上 /apis/custom.metrics.k8s.io/namespaces/xx/k8sresources/xx/metricName

  1. client-go 是k8s 提供了 访问k8s 核心资源的api库, 对于metric 资源(比如PodMetrics/NodeMetrics),k8s 提供k8s.io/metrics 库 来简化访问。 HPAController 即 通过 k8s.io/metrics 库的MetricClient 获取metric 数据,MetricClient 会根据 hpa 中的resource 配置将其转换为 http url(对应上面3个域名)请求 来访问apiserver 获取metric
  2. apiserver 可以将 http 请求转发给任何web 服务,除了内嵌的核心资源(及path)由apiserver 自身提供,To register an API, you add an APIService object, which “claims” the URL path in the Kubernetes API. At that point, the aggregation layer will proxy anything sent to that API path to the registered APIService. 具体的说
    1. apiserver 将 metrics.k8s.io 的请求 转发到 metrics-server pod 上。PS: 因为kubectl top 等要用
    2. apiserver 将 custom.metrics.k8s.ioexternal.metrics.k8s.io 的请求 转到 查询APIService 配置 确定 的pod(或对应的Service或Ingress) 上。
  3. 汇总一下 hpa + MetricClient ==> apiserver ==> metric-server/prometheus-apadter ==> kubelet/prometheus

metric-server

查看nodes 指标 kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" 查看pods 指标 kubectl get --raw "/apis/metrics.k8s.io/v1beta1/pods"

APIService 将/apis/metrics.k8s.io/v1beta1metrics-server(deployment/service) 关联在一起

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  annotations:
  name: v1beta1.metrics.k8s.io
spec:
  group: metrics.k8s.io
  insecureSkipTLSVerify: true
  service:
    name: metrics-server
    namespace: kube-system
    port: 443
  version: v1beta1

自定义metric 集成

Horizontally autoscale Kubernetes deployments on custom metrics未读

APIService 将/apis/custom.metrics.k8s.io/v1beta1prometheus-adapter(deployment/service) 关联在一起

apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
  name: v1beta1.custom.metrics.k8s.io
spec:
  service:
    name: custom-metrics-apiserver
    namespace: xx
  group: custom.metrics.k8s.io
  version: v1beta1
  insecureSkipTLSVerify: true
  groupPriorityMinimum: 100
  versionPriority: 100

prometheus-adapter 通过 configmap 的形式挂载配置文件,描述了 k8s metric 请求转为 prometheus rest 请求的过程,以 metric volcano_queue_allocatable_milli_gpu{instance="172.31.127.43:8080",job="kubernetes-service-endpoints",kubernetes_name="volcano-scheduler-service",kubernetes_namespace="xx",queue_name="default"} 为例

apiVersion: v1
kind: ConfigMap
metadata:
  name: adapter-config
  namespace: custom-metrics
data
  config.yaml: |
    rules:
    - seriesQuery: volcano_queue_allocatable_milli_gpu
      resources:
        overrides:
          kubernetes_namespace:         		# label
            resource: namespace					# label
          queue_name:                   
            group: scheduling.volcano.sh
            resource: queue
      metricsQuery: volcano_queue_allocatable_milli_gpu{<<.LabelMatchers>>}   # go template
  1. prometheus-adapter 会根据seriesQuery 查询metric http://prometheusip:port/api/v1/series?match%5B%5D=volcano_queue_allocatable_milli_gpu&start=1637846254.136
  2. metricsQuery volcano_queue_allocatable_milli_gpu{<<.LabelMatchers>>} 会被转换为 volcano_queue_allocatable_milli_gpu{kubernetes_namespace="xx",queue_name="xx"}
  3. 当发出请求 kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/queues.scheduling.volcano.sh/default/volcano_queue_allocatable_milli_gpu",请求指定了 namespace=default, queues.scheduling.volcano.sh=default,prometheus-adapter 会向prometheus 发出请求 http://prometheus.uat.ximalaya.com/api/v1/query?query=volcano_queue_allocatable_milli_gpu%7Bkubernetes_namespace%3D%22default%22%2Cqueue_name%3D%22default%22%7D&time=1637897470.763
  4. 可以查看 /apis/custom.metrics.k8s.io/v1beta1的资源 kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1"

seriesQuery 和 metricsQuery 本例中看着很像,但实际上 在复杂场景下 配置不同,作用不同,这点要 根据 prometheus-adapter 才能确切判断。

对应hpa 配置

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx-deployment
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx-deployment
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Object
    object:
      metric:
	  	name: volcano_queue_allocatable_milli_gpu
	  target:
	    type: Value
	  	Value: 1000
      describedObject:
        apiVersion: scheduling.volcano.sh/v1beta1
        kind: Queue
        name: default

源码分析

启动与初始化

// k8s.io/kubernetes/cmd/kube-controller-manager/app/controllermanager.go
func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc {
    controllers["horizontalpodautoscaling"] = startHPAController
    ...
}
// k8s.io/kubernetes/cmd/kube-controller-manager/app/autoscaling.go
func startHPAController(ctx ControllerContext) (http.Handler, bool, error) {
	...
	return startHPAControllerWithRESTClient(ctx)
}
func startHPAControllerWithRESTClient(ctx ControllerContext) (http.Handler, bool, error) {
	clientConfig := ctx.ClientBuilder.ConfigOrDie("horizontal-pod-autoscaler")
	hpaClient := ctx.ClientBuilder.ClientOrDie("horizontal-pod-autoscaler")
	metricsClient := metrics.NewRESTMetricsClient(
		resourceclient.NewForConfigOrDie(clientConfig),
		custom_metrics.NewForConfig(clientConfig, ctx.RESTMapper, apiVersionsGetter),
		external_metrics.NewForConfigOrDie(clientConfig),
	)
	return startHPAControllerWithMetricsClient(ctx, metricsClient)
}
func startHPAControllerWithMetricsClient(ctx ControllerContext, metricsClient metrics.MetricsClient) (http.Handler, bool, error) {
	hpaClient := ctx.ClientBuilder.ClientOrDie("horizontal-pod-autoscaler")
	hpaClientConfig := ctx.ClientBuilder.ConfigOrDie("horizontal-pod-autoscaler")
	scaleKindResolver := scale.NewDiscoveryScaleKindResolver(hpaClient.Discovery())
	scaleClient, err := scale.NewForConfig(hpaClientConfig, ctx.RESTMapper, dynamic.LegacyAPIPathResolverFunc, scaleKindResolver)
	go podautoscaler.NewHorizontalController(
		hpaClient.CoreV1(),
		scaleClient,
		hpaClient.AutoscalingV1(),
		ctx.RESTMapper,
		metricsClient,
		ctx.InformerFactory.Autoscaling().V1().HorizontalPodAutoscalers(),
        ctx.InformerFactory.Core().V1().Pods(),
        // 扩缩容计算周期,默认15s
        ctx.ComponentConfig.HPAController.HorizontalPodAutoscalerSyncPeriod.Duration,
        // 两次缩容之间的间隔,防止抖动 默认5m
        ctx.ComponentConfig.HPAController.HorizontalPodAutoscalerDownscaleStabilizationWindow.Duration,
        // 如果 ratio 比较接近1,则忽略扩缩容,描述ratio 与1 接近的程度,默认0.1
        ctx.ComponentConfig.HPAController.HorizontalPodAutoscalerTolerance,
        // 用于设置 Pod 的初始化时间, 在此时间内的 Pod,CPU 资源度量值将不会被采纳。默认5m
        ctx.ComponentConfig.HPAController.HorizontalPodAutoscalerCPUInitializationPeriod.Duration,
        // 设置pod 的准备时间,在此时间内的 Pod 统统被认为未就绪,默认30s
		ctx.ComponentConfig.HPAController.HorizontalPodAutoscalerInitialReadinessDelay.Duration,
	).Run(ctx.Stop)
	return nil, true, nil
}
// k8s.io/kubernetes/pkg/controller/podautoscaler/horizontal.go
func NewHorizontalController(
	evtNamespacer v1core.EventsGetter,
	scaleNamespacer scaleclient.ScalesGetter,...
) *HorizontalController {
    hpaController := &HorizontalController{...}
    hpaInformer.Informer().AddEventHandlerWithResyncPeriod(
        // 将hpa resource 加入到 queue 中
		cache.ResourceEventHandlerFuncs{
			AddFunc:    hpaController.enqueueHPA,
			UpdateFunc: hpaController.updateHPA,
			DeleteFunc: hpaController.deleteHPA,
		},
		resyncPeriod,
    )
    hpaController.hpaLister = hpaInformer.Lister()
    hpaController.podLister = podInformer.Lister()
    replicaCalc := NewReplicaCalculator(...)
	hpaController.replicaCalc = replicaCalc
}

从HorizontalController到ReplicaCalculator

// k8s.io/kubernetes/pkg/controller/podautoscaler/horizontal.go
func (a *HorizontalController) Run(stopCh <-chan struct{}) {
	defer utilruntime.HandleCrash()
	defer a.queue.ShutDown()
	if !cache.WaitForNamedCacheSync("HPA", stopCh, a.hpaListerSynced, a.podListerSynced) {
		return
	}
	// start a single worker (we may wish to start more in the future)
	go wait.Until(a.worker, time.Second, stopCh)
	<-stopCh
}
func (a *HorizontalController) worker() {
	for a.processNextWorkItem() {
	}
}
func (a *HorizontalController) processNextWorkItem() bool {
    ...
	deleted, err := a.reconcileKey(key.(string))
	...
}
func (a *HorizontalController) reconcileKey(key string) (deleted bool, err error) {
	namespace, name, err := cache.SplitMetaNamespaceKey(key)
	hpa, err := a.hpaLister.HorizontalPodAutoscalers(namespace).Get(name)
	return false, a.reconcileAutoscaler(hpa, key)
}
func (a *HorizontalController) reconcileAutoscaler(hpav1Shared *autoscalingv1.HorizontalPodAutoscaler, key string) error {
	// hpa 对象
	hpa := hpaRaw.(*autoscalingv2.HorizontalPodAutoscaler)
	// 待扩缩容的对象
	scale, targetGR, err := a.scaleForResourceMappings(hpa.Namespace, hpa.Spec.ScaleTargetRef.Name, mappings)
	currentReplicas := scale.Spec.Replicas
	// 计算 desiredReplicas
	desiredReplicas := int32(0)
	minReplicas = *hpa.Spec.MinReplicas
	rescale := true
	if scale.Spec.Replicas == 0 && minReplicas != 0 {
		// Autoscaling is disabled for this resource
	} else if currentReplicas > hpa.Spec.MaxReplicas {
		rescaleReason = "Current number of replicas above Spec.MaxReplicas"
		desiredReplicas = hpa.Spec.MaxReplicas
	} else if currentReplicas < minReplicas {
		rescaleReason = "Current number of replicas below Spec.MinReplicas"
		desiredReplicas = minReplicas
	} else {
		metricDesiredReplicas, metricName, metricStatuses, metricTimestamp, err = a.computeReplicasForMetrics(hpa, scale, hpa.Spec.Metrics)
		rescaleMetric := ""
		if metricDesiredReplicas > desiredReplicas {
			desiredReplicas = metricDesiredReplicas
			rescaleMetric = metricName
		}
		if desiredReplicas > currentReplicas { rescaleReason = fmt.Sprintf("%s above target", rescaleMetric) }
		if desiredReplicas < currentReplicas { rescaleReason = "All metrics below target" }
		if hpa.Spec.Behavior == nil {
			desiredReplicas = a.normalizeDesiredReplicas(hpa, key, currentReplicas, desiredReplicas, minReplicas)
		} else {
			desiredReplicas = a.normalizeDesiredReplicasWithBehaviors(hpa, key, currentReplicas, desiredReplicas, minReplicas)
		}
		rescale = desiredReplicas != currentReplicas
	}
	// 如果需要扩缩容
	if rescale {
		scale.Spec.Replicas = desiredReplicas
		_, err = a.scaleNamespacer.Scales(hpa.Namespace).Update(context.TODO(), targetGR, scale, metav1.UpdateOptions{})
	} else { desiredReplicas = currentReplicas }
	return a.updateStatusIfNeeded(hpaStatusOriginal, hpa)
}

上述代码 描述了宏观逻辑:从queue 中取出 hpa resource,计算是否 rescale 以及 desiredReplicas,并实施。

// k8s.io/kubernetes/pkg/controller/podautoscaler/horizontal.go
// computeReplicasForMetrics computes the desired number of replicas for the metric specifications listed in the HPA,
// returning the maximum  of the computed replica counts, a description of the associated metric, and the statuses of
// all metrics computed.
func (a *HorizontalController) computeReplicasForMetrics(hpa *autoscalingv2.HorizontalPodAutoscaler, scale *autoscalingv1.Scale,metricSpecs []autoscalingv2.MetricSpec) (replicas int32, metric string, ...) {
	selector, err := labels.Parse(scale.Status.Selector)
	specReplicas := scale.Spec.Replicas
	statusReplicas := scale.Status.Replicas
	statuses = make([]autoscalingv2.MetricStatus, len(metricSpecs))
	invalidMetricsCount := 0
	for i, metricSpec := range metricSpecs {
        replicaCountProposal, metricNameProposal, timestampProposal, condition, err := a.computeReplicasForMetric(hpa, metricSpec, specReplicas, statusReplicas, selector, &statuses[i])
        // 每个metric 计算一个 需要扩缩容的数量,去最大值 作为最终的扩缩容的replicas
        if err == nil && (replicas == 0 || replicaCountProposal > replicas) {
			timestamp = timestampProposal
			replicas = replicaCountProposal
			metric = metricNameProposal
		}
	}
	// If all metrics are invalid return error and set condition on hpa based on first invalid metric.
	if invalidMetricsCount >= len(metricSpecs) {
		return 0, "", statuses, ...)
	}
	return replicas, metric, statuses, timestamp, nil
}
// Computes the desired number of replicas for a specific hpa and metric specification,
// returning the metric status and a proposed condition to be set on the HPA object.
func (a *HorizontalController) computeReplicasForMetric(hpa *autoscalingv2.HorizontalPodAutoscaler, spec autoscalingv2.MetricSpec,specReplicas, statusReplicas int32, selector labels.Selector, ...) (replicaCountProposal int32, metricNameProposal string,...) {
	switch spec.Type {
	case autoscalingv2.ObjectMetricSourceType:
		metricSelector, err := metav1.LabelSelectorAsSelector(spec.Object.Metric.Selector)
		replicaCountProposal, timestampProposal, metricNameProposal, condition, err = a.computeStatusForObjectMetric(specReplicas, statusReplicas, spec, hpa, selector, status, metricSelector)
	case autoscalingv2.PodsMetricSourceType:
		metricSelector, err := metav1.LabelSelectorAsSelector(spec.Pods.Metric.Selector)
		replicaCountProposal, timestampProposal, metricNameProposal, condition, err = a.computeStatusForPodsMetric(specReplicas, spec, hpa, selector, status, metricSelector)
	case autoscalingv2.ResourceMetricSourceType:
		replicaCountProposal, timestampProposal, metricNameProposal, condition, err = a.computeStatusForResourceMetric(specReplicas, spec, hpa, selector, status)
	case autoscalingv2.ExternalMetricSourceType:
		replicaCountProposal, timestampProposal, metricNameProposal, condition, err = a.computeStatusForExternalMetric(specReplicas, statusReplicas, spec, hpa, selector, status)
	default:
        ...
		return 0, "", time.Time{}, condition, err
	}
	return replicaCountProposal, metricNameProposal, ...
}
// computeStatusForPodsMetric computes the desired number of replicas for the specified metric of type PodsMetricSourceType.
func (a *HorizontalController) computeStatusForPodsMetric(currentReplicas int32, metricSpec autoscalingv2.MetricSpec, hpa *autoscalingv2.HorizontalPodAutoscaler, selector labels.Selector, status *autoscalingv2.MetricStatus, metricSelector labels.Selector) (replicaCountProposal int32, ...) {
	replicaCountProposal, utilizationProposal, timestampProposal, err := a.replicaCalc.GetMetricReplicas(currentReplicas, metricSpec.Pods.Target.AverageValue.MilliValue(), metricSpec.Pods.Metric.Name, hpa.Namespace, selector, metricSelector)
	*status = autoscalingv2.MetricStatus{...}
	return replicaCountProposal, timestampProposal, fmt.Sprintf("pods metric %s", metricSpec.Pods.Metric.Name), autoscalingv2.HorizontalPodAutoscalerCondition{}, nil
}

上述代码负责将计算逻辑转移到 ReplicaCalculator 进行

计算规则

// k8s.io/kubernetes/pkg/controller/podautoscaler/replica_calculator.go
func (c *ReplicaCalculator) GetMetricReplicas(currentReplicas int32, targetUtilization int64, metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (replicaCount int32, utilization int64, ...) {
	metrics, timestamp, err := c.metricsClient.GetRawMetric(metricName, namespace, selector, metricSelector)
	replicaCount, utilization, err = c.calcPlainMetricReplicas(metrics, currentReplicas, targetUtilization, namespace, selector, v1.ResourceName(""))
	return replicaCount, utilization, timestamp, err
}
// calcPlainMetricReplicas calculates the desired replicas for plain (i.e. non-utilization percentage) metrics.
func (c *ReplicaCalculator) calcPlainMetricReplicas(metrics metricsclient.PodMetricsInfo, currentReplicas int32, targetUtilization int64, namespace string, selector labels.Selector, resource v1.ResourceName) (replicaCount int32, utilization int64, err error) {
	podList, err := c.podLister.Pods(namespace).List(selector)
	if len(podList) == 0 {
		return 0, 0, fmt.Errorf("no pods returned by selector while calculating replica count")
    }
    // PodPending/Unready/HorizontalPodAutoscalerCPUInitializationPeriod/HorizontalPodAutoscalerInitialReadinessDelay 的pod 加入到ignoredPods(即这些pod的metric 即便有也不能采信),找不到metric 数据的pod 加入到 missingPods,没有上述问题的pod 由 readyPodCount 计数
    readyPodCount, ignoredPods, missingPods := groupPods(podList, metrics, resource, c.cpuInitializationPeriod, c.delayOfInitialReadinessStatus)
    // ignoredPods 不参与 第一轮使用率计算
	removeMetricsForPods(metrics, ignoredPods)
	if len(metrics) == 0 {
		return 0, 0, fmt.Errorf("did not receive metrics for any ready pods")
    }
    // 先根据 包含可以采信的metric 数据的pod 计算一次使用率
	usageRatio, utilization := metricsclient.GetMetricUtilizationRatio(metrics, targetUtilization)
    rebalanceIgnored := len(ignoredPods) > 0 && usageRatio > 1.0    // 扩容 且有pod 的metric 数据无法采信
    /*
         等同于 
         if  len(missingPods) == 0 {  // 所有的pod 都有metric 数据
            // 如果 所有的pod metric 都可以采信  或者 处于缩容状态
            if len(ignoredPods) <= 0 ||  usageRatio <= 1.0{}
         }
    */
	if !rebalanceIgnored && len(missingPods) == 0 {
		if math.Abs(1.0-usageRatio) <= c.tolerance {
			// return the current replicas if the change would be too small
			return currentReplicas, utilization, nil
		}
		// if we don't have any unready or missing pods, we can calculate the new replica count now
		return int32(math.Ceil(usageRatio * float64(readyPodCount))), utilization, nil
	}
    // 补全 missingPods 数据
	if len(missingPods) > 0 {
		if usageRatio < 1.0 {   // 使用率小于1 缩容场景
			// on a scale-down, treat missing pods as using 100% of the resource request
			for podName := range missingPods {
				metrics[podName] = metricsclient.PodMetric{Value: targetUtilization}
			}
		} else {    // 使用率大于 1 扩容场景
			// on a scale-up, treat missing pods as using 0% of the resource request
			for podName := range missingPods {
				metrics[podName] = metricsclient.PodMetric{Value: 0}
			}
		}
    }
    // 等同于 if len(ignoredPods) > 0 && usageRatio > 1.0
	if rebalanceIgnored { // 扩容 且有pod 无法参与判断, 补全ignoredPods 数据
		// on a scale-up, treat unready pods as using 0% of the resource request
		for podName := range ignoredPods {
			metrics[podName] = metricsclient.PodMetric{Value: 0}
		}
	}
	// re-run the utilization calculation with our new numbers
    newUsageRatio, _ := metricsclient.GetMetricUtilizationRatio(metrics, targetUtilization)
    // 在 tolerance 范围内,usageRatio  和 newUsageRatio 扩缩容偏好 矛盾,则放弃扩缩容
	if math.Abs(1.0-newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
		// return the current replicas if the change would be too small,
		// or if the new usage ratio would cause a change in scale direction
		return currentReplicas, utilization, nil
	}
	// return the result, where the number of replicas considered is
	// however many replicas factored into our calculation
	return int32(math.Ceil(newUsageRatio * float64(len(metrics)))), utilization, nil
}

整体思路

  1. 将 pod 分组为 ignoredPods(pod metric 不能采信)/missingPods(pod 没有metric)/readyPods(有可信metric 的pod)
  2. 先根据 包含可以采信的metric 数据的pod(readyPods) 计算一次使用率
  3. 如果pod metric 都可以采信,或基于已有的 可采信metric 数据已经确定缩容了,则直接 按公式计算 扩缩容 replicas即可。否则根据 扩缩容场景 来补全 ignoredPods/missingPods 的metric 数据
  4. 根据扩缩容场景 补全missingPods 的metric 数据,扩容场景下 补全ignoredPods metric 数据
  5. 此时所有的pod 都有了 真实的或补全的metric 数据,重新计算一次 使用率
  6. 如果两次计算的使用率 扩缩容偏好 不矛盾 且 扩缩容Ratio 超过 tolerance ,则计算 扩缩容的replicas 并返回

计算的原料——metric

kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes/<node-name> 可以拿到一个示例的node metric数据

{
    "kind": "NodeMetrics",
    "apiVersion": "metrics.k8s.io/v1beta1",
    "metadata": {
        "name": "192.168.xx.xx",
        "selfLink": "/apis/metrics.k8s.io/v1beta1/nodes/192.168.60.96",
        "creationTimestamp": "2020-10-13T06:39:03Z"
    },
    "timestamp": "2020-10-13T06:37:46Z",
    "window": "30s",
    "usage": {
        "cpu": "1792787098n",
        "memory": "48306672Ki"
    }
}
// k8s.io/kubernetes/pkg/controller/podautoscaler/metrics/interfaces.go
type PodMetric struct {
	Timestamp time.Time
	Window    time.Duration
	Value     int64
}
type PodMetricsInfo map[string]PodMetric
type MetricsClient interface {
    // allows consumers to access resource metrics (CPU and memory) for pods and nodes.
	GetResourceMetric(resource v1.ResourceName, namespace string, selector labels.Selector) (PodMetricsInfo, time.Time, error)
	GetRawMetric(metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (PodMetricsInfo, time.Time, error)
	GetObjectMetric(metricName string, namespace string, objectRef *autoscaling.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error)
	GetExternalMetric(metricName string, namespace string, selector labels.Selector) ([]int64, time.Time, error)
}
// k8s.io/kubernetes/pkg/controller/podautoscaler/metrics/rest_metrics_client.go
type restMetricsClient struct {
	*resourceMetricsClient
	*customMetricsClient
	*externalMetricsClient
}
func (c *resourceMetricsClient) GetResourceMetric(resource v1.ResourceName, namespace string, selector labels.Selector) (PodMetricsInfo, time.Time, error) {
	metrics, err := c.client.PodMetricses(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
	res := make(PodMetricsInfo, len(metrics.Items))
	for _, m := range metrics.Items {
		podSum := int64(0)
		missing := len(m.Containers) == 0
		for _, c := range m.Containers {
			resValue, found := c.Usage[v1.ResourceName(resource)]
			if !found {
				missing = true
				klog.V(2).Infof("missing resource metric %v for container %s in pod %s/%s", resource, c.Name, namespace, m.Name)
				break // containers loop
			}
			podSum += resValue.MilliValue()
		}
		if !missing {
			res[m.Name] = PodMetric{
				Timestamp: m.Timestamp.Time,
				Window:    m.Window.Duration,
				Value:     int64(podSum),
			}
		}
	}
	timestamp := metrics.Items[0].Timestamp.Time
	return res, timestamp, nil
}

解决响应速度问题

官方的这个HPA Controller在实现的时候用的是一个Gorountine来处理整个集群的所有HPA的计算和同步问题,在集群配置的HPA比较多的时候可能会导致业务扩容不及时的问题,其次官方HPA Controller不支持为每个HPA进行单独的个性化配置。为了优化HPA Controller的性能和个性化配置问题,我们把HPA Controller单独抽离出来单独部署。同时为每一个HPA单独配置一个Gorountine,并且每一个HPA多可以根据业务的需要进行单独的配置。

其实仅仅优化HPA Controller还是不能满足一些业务在业务高峰时候的一些需求,比如在业务做活动的时候,希望在流量高峰期之前就能够把业务扩容好。这个时候我们就需要一个定时HPA的功能,为此我们定义了一个CronHPA的CRD和CronHPA Operator。CronHPA会在业务定义的时间进行扩容和缩容,同时还能和HPA一起配合工作。

扩缩容策略优化

AMS 新闻视频广告的云原生容器化之路选取衡量指标的核心思想:是尽量选取变化最大的指标,避免变化较小的指标限制 Pod 数量的变化,导致其他负载指标的变化超出资源限制。对于离线任务而言,一般来说,CPU 使用率更容易随着任务的开启和结束发生变化,而内存一般存储相对固定的上下文,比较稳定,选取 CPU 使用率作为判定标准比较合理。

我们需要根据具体的负载变化曲线,决定弹性缩扩容的策略。

  1. 如果负载曲线在每一天每一个时段下都高度一致,我们可以考虑使用 CronHPA 组件,人工指定不同时段的 Pod 数量,定期调度。
  2. 如果负载曲线每天都在发生变化,不论趋势还是数值,我们都可以使用 HPA 配置,设置 CPU 使用率参考值,要求集群在利用率超过或低于指定值时进行 Pod 数量的调整,将 Pod 的 CPU 使用率维持在容忍范围内。
  3. 如果负载曲线每天发生变化,但是存在定时发生的尖峰,为了避免集群自行调整的速度慢于负载增长的速度,导致集群被压垮,我们可以结合 CronHPA 与 HPA 使用,平时将控制权交给 HPA,在尖峰到来前使用 CronHPA 提前扩容,既保证尖峰不会冲垮集群,又能享受集群自动调整 Pod 数量带来的便利。

达达智能弹性伸缩架构和实践

AHPA:开启 Kubernetes 弹性预测之门一般在 Kubernetes 中管理应用实例数有三种方式:固定实例数、HPA 和 CronHPA 。使用最多的是固定实例数,固定实例数最大的问题就是在业务波谷时造成很明显的资源浪费。为了解决资源浪费的问题所以有了 HPA,但 HPA 的弹性触发是滞后的,这就导致资源的供给也会滞后,资源不能及时供给可能会导致业务稳定性下降。CronHPA 可以定时伸缩,看起来可以解决弹性滞后的问题,但具体定时粒度有多细、业务量有变化时需要频繁地手动调节定时弹性策略吗?AHPA有两个弹性策略:主动预测和被动预测。主动预测基于 RobustSTL 算法从历史数据中识别周期性趋势,主动预测下个周期应用的实例数量;被动预测基于应用实时数据设定实例数量,可以很好的应对突发流量。此外,AHPA 还增加了兜底保护策略,用户可以设置实例数量的上下界。AHPA 算法中最终生效的实例数是主动预测、被动预测及兜底策略中的最大值。

EPA——腾讯推出国内首个云原生成本优化开源项目 Crane传统基于事件的弹性工具会导致一个天然缺陷——当业务指标偏离正常值后才会触发弹性,这种滞后性使得云用户不敢使用弹性。EPA 支持可扩展的预测算法,以预测结果驱动横向和纵向弹性,确保业务能提前弹出来,彻底避免原生弹性能力未弹先死的尴尬。同时 Crane 将社区的 HPA 和 VPA 两种弹性能力统一起来,提出了弹性概念 EPA。

Effective HPA:预测未来的弹性伸缩产品

原生HPA在支持多方数据指标时,需要做大量的工作来适配,并且原生HPA不支持预定的伸缩(解决弹性滞后的问题)、缩容到0(解决测试环境资源浪费问题)等问题,所以就出现各种基于HPA的扩展。EDA是用于 Kubernetes 的基于事件驱动弹性伸缩器,用户只需要创建 ScaledObject 或 ScaledJob 来定义你想要伸缩的对象和你想要使用的触发器,KEDA 会处理剩下的一切!

弹性伸缩最佳实践之灵活调节 HPA 扩缩容速率 未读

技术决策案例

欢乐游戏 Istio 云原生服务网格三年实践思考对于 hpa 笔者早先的态度还是有些犹豫的,因为这本质上是会将服务部署发布时机变得不可控,不像是常规人工干预的发布,出了问题好介入。线上也确实出过一些问题,例如 hpa (会依赖 hpa 关联的 metric 链路畅通)夜间失效导致业务过载;还有就是在日志采集弄好之前,hpa 导致 pod 漂移,前一天夜里某 pod 的告警信息,第二天想看就比较费劲,还得跑到之前调度到的 node 上去看;另外也出现过进程 hpa 启动不起来的问题,配置有误无法加载初始化成功,正在跑着的进程只会 reload 失败,但是停掉重启就会启动失败。不过 hpa 对于提升资源利用率,还是很有价值的,所以我们现在的做法是做区分对待,对于普通的业务,min 副本数可以较小,对于重要的服务,min 副本数则配置稍大一些。PS:我的感觉是,一部分实例交给原来的deployment管,一部分实例交给hpa 管,打上不同的标签,是一个比较安全的方案。