
PriorityClass、HPA、Argo CD这三个词如果拆开看每一个都够写一篇长文但如果把三者放在 Kubernetes 运维和发布场景里它们其实是同一条链路让该优先跑的先跑让该伸缩的多缩让该上线的稳定上线。我前阵子给团队整理面试题恰好把这三块连同 CI/CD 流水线放在一起边理边发现很多所谓的知识点离开集群环境就是死记硬背真到现场实操才能暴露问题。这篇文章就是我整理后的答案版本也包含不少线上踩坑经验适合刚接手集群的运维、准备 k8s 面试的开发者以及正在搭 GitOps 发布流程的团队参考。1. PriorityClass 到底在解决什么问题1.1 没有优先级时调度器到底怎么选Kubernetes 默认调度的逻辑是把你创建的 Pod 放到一个Pending队列kube-scheduler 不断从这个队列里取任务然后结合节点资源、亲和性、污点等条件找到合适的 Node再完成绑定。这个过程对于普通业务 Pod 来说够用因为它们之间没有“重要程度”的差别谁先提交谁先调度。但是线上环境不会这么温柔总会出现节点资源紧张、临时有高优任务需要插队的情况。只靠提交顺序排队核心组件可能被批量扩容的离线任务堵住严重时候会出现“活跃业务饿死”的假象。PriorityClass 就是为了解决“谁应该先进去”的问题而生的集群级资源。它做的事情很直接给 Pod 打上一个优先级数值数值越高调度器在排序时越先看它。资源充足时所有 Pod 都能正常调度PriorityClass 影响不明显一旦节点资源不足高优先级 Pod 会排在调度队列前面甚至触发抢占把节点上低优先级的 Pod 挤出去腾出资源让自己调度上去。这里要注意PriorityClass 与节点亲和性、污点这些“能不能调度”的约束不同它管的是“谁更有资格被调度”。1.2 配置 PriorityClass 和 Pod 关联PriorityClass 的 YAML 非常简单创建出来就是一个集群级对象名字在集群内唯一。apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: core-business-high value: 1000000 globalDefault: false description: 核心交易链路 Pod 使用的高优先级字段说明valueint32 类型范围从 -2147483648 到 2147483647。值越大优先级越高。globalDefault如果为true集群里没有指定priorityClassName的 Pod 会默认使用这个优先级。description主要给团队看的建议写清楚用途避免后面有人随意复用一个高优 class。Pod 关联时只需要在 spec 里加一行apiVersion: v1 kind: Pod metadata: name: example-pod spec: priorityClassName: core-business-high containers: - name: app image: nginx:1.27创建后可以通过kubectl get pod example-pod -o yaml看到spec.priority和spec.priorityClassName两个字段。注意priority是调度器实际使用的整数priorityClassName只是人类可读的名字。1.3 生产环境使用 PriorityClass 的几个教训第一不要随意创建太多优先级档位。我看过有的集群里 PriorityClass 建了二十多个value 从 10 到 900000 密密麻麻最后连负责人都说不清哪个该用什么。其实对绝大多数业务集群三到四档就够系统组件、核心业务、普通业务、可丢弃的离线任务。档位越多调度队列的排序越复杂也越容易误伤。第二抢占不是“优雅地协商”而是“直接打断”。当高优先级 Pod 需要抢占时kube-scheduler 会选择目标节点上的低优先级 Pod触发它的优雅退出。如果这些低优先级 Pod 没有做优雅退出处理或者退出时间超过terminationGracePeriodSeconds就会被强制杀死。所以允许被抢占的 Pod 一定要设计好优雅退出逻辑并且配置合理的 PodDisruptionBudget避免关键业务被一次性清空。第三globalDefault很容易被忽略。如果集群里创建了多个globalDefault: true的 PriorityClass后创建的可能生效也可能冲突不同版本行为不完全一致。更稳妥的方式是直接给需要控制的 Pod 显式声明priorityClassName把默认优先级留给那些“随缘”的 Pod。系统组件比如 kube-proxy、coredns 这类建议配套一个专门的 PriorityClass 并设置较高值防止节点压力大的时候系统组件反而被挤掉。第四要理解 PriorityClass 和 QoS 类不是一回事。QoS 类是由requests和limits计算出来的比如 Guaranteed、Burstable、BestEffort主要用于节点压力驱逐时的顺序PriorityClass 的数值则更多影响调度顺序和抢占。实际发生节点驱逐时Kubernetes 会优先按 QoS 类排序同一 QoS 类里再考虑优先级。面试题里经常把这两个概念混在一起问答题时最好把这两条线分开讲。2. HPA 不只会数 CPU2.1 HPA 的指标来源和扩缩容算法HPAHorizontalPodAutoscaler是 Kubernetes 内置的副本自动伸缩机制。很多人第一反应是“CPU 超过 80% 就扩容”这没毛病但 HPA 的能力远不止 CPU。它通过autoscaling/v2API 读取三类指标资源指标resource、自定义指标custom和外部指标external。资源指标通常由 metrics-server 提供自定义指标和外部指标则需要部署对应的 adapter例如 Prometheus Adapter 或云厂商的 CloudWatch Adapter。核心计算公式是期望副本数 ceil(当前副本数 * (当前指标值 / 期望指标值))如果配置了多个指标HPA 会对每个指标分别算出期望副本数然后取最大值作为最终值。比如当前 2 个副本CPU 利用率 90%期望 70%内存利用率 120%期望 80%那么 CPU 对应的期望是 3内存对应的是 4最终会扩容到 4 个副本。这里有个比较容易误解的点HPA 判断 CPU 利用率时分母是 Pod 的requests.cpu不是节点的 CPU也不是 Pod 的limits。如果一个 Pod 没有配置requestsHPA 会把它的 CPU 利用率当成未知导致无法计算。所以业务 Pod 如果要用 CPU 做伸缩指标requests是一定要写的。2.2 一次完整的 HPA 配置过程先确认 metrics-server 在工作kubectl get --raw /apis/metrics.k8s.io/v1beta1 | head -n 20能看到 metrics 数据后再创建 HPA。我习惯用 YAML 而不是kubectl autoscale因为 YAML 可以写 behavior 策略方便精细控制。apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web-app-hpa namespace: default spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: web-app minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 20 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 60创建后可以通过kubectl get hpa -w观察状态。TARGETS列会显示当前指标/目标值例如75%/70%。如果显示unknown或FailedGetResourceMetric说明 metrics-server 数据没接上。2.3 线上 HPA 最常见的五个坑扩容不及时HPA 默认每 15 秒同步一次指标但指标本身也有采集延迟。应用流量突增时从 CPU 升高到 HPA 扩容可能要等一两分钟。如果这个时间不能接受可以在behavior.scaleUp.stabilizationWindowSeconds设置一个更短的稳定窗口或者提前把minReplicas调大一点。缩容慢HPA 默认缩容稳定窗口是 300 秒这是故意防抖的。如果测试时觉得缩得太慢可以在scaleDown.stabilizationWindowSeconds调整为 120 秒左右。我见过有人直接改成 0结果流量一波动副本就频繁增减反而把负载均衡器的连接打乱。内存指标扩容后不缩容CPU 利用率下降后Pod 可能还是占着内存因为内存不会自动释放。缩容策略可以结合type: Pods的指标或者定期做滚动重启否则内存 HPA 的缩容效果会很差。多个指标互相打架比如 CPU 正常但内存很高HPA 会按最大期望副本数扩容这时要确认业务到底瓶颈在哪不要在 YAML 里堆一堆凭感觉加的指标。自定义指标接入前要先用kubectl get --raw验证数据是否能查到否则 HPA 会一直 pending。与 Cluster Autoscaler 配合的节奏HPA 把副本数扩大后如果节点资源不足Pod 会处于 Pending直到 Cluster Autoscaler 弹出新节点。整个过程可能持续 510 分钟。优化方向是给节点池设置更合理的扩缩容策略或者给关键应用预留缓冲节点。3. 用 Argo CD 把 Git 变成发布唯一事实源3.1 为什么会从 kubectl apply 转向 GitOps传统 CI/CD 流水线最让我头疼的点是流水线执行完后线上到底是什么状态没人说得清。因为线上可能有人手动kubectl scale了一下也可能有人改了个 ConfigMap这些改动不在 Git 里下次部署一覆盖就丢。Argo CD 的核心思路是“Git 是唯一事实源”集群里要跑什么东西由 Git 仓库里的 manifests 决定Argo CD 负责把集群状态持续收敛到 Git 声明的内容。每次我给别人介绍 Argo CD 时都会说它像一个离线的巡检员不停地对比“Git 里的期望状态”和“集群里的实际状态”发现不同就把它拉回来。对运维来说最大的价值不是省去手敲kubectl apply而是让回滚、审计、多环境管理都变成 Git 操作。比如线上出问题我可以直接git revert上一个 commitArgo CD 会自动把集群拉回旧版本不需要 SSH 到跳板机上敲命令。3.2 安装和接入现有集群Argo CD 的安装方式有很多种我建议直接使用官方提供的 install.yaml 或 Helm Chart。这里用官方默认部署方式做最小化安装kubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml安装完成后默认的 admin 密码存在 Secret 里取出来的方式kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath{.data.password} | base64 -d然后通过端口转发或 Ingress 登录kubectl port-forward svc/argocd-server -n argocd 8080:443 argocd login localhost:8080真实环境不会一直用端口转发而是给它配一个 Ingress并开启 TLS。生产环境建议把admin密码轮换掉然后通过 SSO比如 OIDC做用户认证。如果团队规模不大前期用本地账号也问题不大但要尽早把账号和人员对应起来方便做操作审计。3.3 把一个应用交给 Argo CD 管理假设应用配置仓库是gitlab.example.com/team/app-config.git目录下放了一组 Kubernetes manifests。先添加仓库argocd repo add https://gitlab.example.com/team/app-config.git \ --username gitlab-ci --password ${GIT_TOKEN}然后创建一个 ApplicationapiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: app-prod namespace: argocd spec: project: default source: repoURL: https://gitlab.example.com/team/app-config.git targetRevision: main path: manifests/overlays/prod destination: server: https://kubernetes.default.svc namespace: prod syncPolicy: automated: selfHeal: true prune: true syncOptions: - CreateNamespacetrueautomated.prune: true表示如果 Git 里删掉一个资源本地集群也会跟着删除。这个开关在生产环境要谨慎如果一个应用目录里本来就不该清理某些资源prune 会“好心办坏事”。selfHeal则是发现集群被手动改动后自动纠正。在早期排查问题时我会先关掉这两个开关等确认 Application 的 diff 没问题再打开。3.4 多环境和项目级隔离Argo CD 的项目AppProject是一个很值得利用的隔离层级。你可以规定某个项目只能部署到指定的集群和命名空间还能限制可以使用的资源类型。比如让业务 A 项目只能部署到namespace-a不允许部署 ClusterRole 等集群级资源。这个设置如果一开始不做后面多个团队共用一个 Argo CD 时权限会非常混乱。我对多环境的经验是每个环境用独立的 Application或者使用 ApplicationSet。比如dev环境可以自动同步加自愈prod环境只打开自动同步但关闭 prune或者干脆手动触发同步由发布审批人执行。不要把所有环境都套用一个 syncPolicy一旦有人把不成熟分支推到 main全环境一起变更事故面会非常大。4. 把 Argo CD 和 CI/CD 流水线串起来4.1 GitOps 模式下的发布流程长什么样传统 CI/CD 流水线里CI 阶段编译、构建镜像、推送镜像后CD 阶段通常直接连到集群执行kubectl set image或者helm upgrade。这种模式简单直接但有一个隐患CD 工具需要具备集群的写权限权限过大而且集群状态很难和 Git 对应。GitOps 的做法是让 CI 只负责两件事第一步把代码变成镜像推到镜像仓库第二步把镜像 tag 更新到配置仓库的 manifests 里。剩下的“让集群变成新版本”由 Argo CD 完成。流程可以这样看开发代码 - CI 构建镜像 - 推送 registry - 修改 app-config 仓库镜像 tag - Argo CD 感知变化 - 同步到 k8s 集群CI 阶段不再直接碰集群集群访问权限收口在 Argo CD 的 controller 上。这个模型比传统 CD 更安全也更方便审计因为每次发布实际上对应一个 Git commit。4.2 CI 流水线里需要准备的几个秘密要让流水线能更新配置仓库一般需要有 git push 权限的令牌。建议在 GitLab 或 GitHub 里建一个专用的机器人账号只给配置仓库写权限不要给代码仓库的管理员权限。然后把令牌以 CI 变量方式注入例如GIT_TOKENglpat-xxxx IMAGE_REPOregistry.example.com/app/web在 CI 脚本里使用oauth2:${GIT_TOKEN}作为用户名推送到配置仓库。这个令牌一定要设置过期时间并在 CI 里单独配置受保护变量避免暴露在日志里。如果不想让 CI 去改 Git 仓库还有另一个方案部署 Argo CD Image Updater。它定期检查镜像仓库里有没有新 tag自动更新 Argo CD Application 跟踪的镜像版本。这能减少 CI 步骤但也会让“哪个 commit 触发哪个 tag”的链路变得不够直观需要团队自己权衡。4.3 GitLab CI 参考实现下面是一个简化版 GitLab CI 配置我用它演示构建镜像和更新配置仓库的完整流程。stages: - build - update-manifest variables: IMAGE: registry.example.com/app/web APP_CONFIG_REPO: gitlab.example.com/team/app-config APP_CONFIG_DIR: manifests/overlays/prod build-and-push: stage: build image: docker:stable services: - docker:dind script: - docker build -t $IMAGE:$CI_COMMIT_SHORT_SHA . - echo $REGISTRY_PASSWORD | docker login -u $REGISTRY_USER --password-stdin $CI_REGISTRY - docker push $IMAGE:$CI_COMMIT_SHORT_SHA only: - main update-manifest: stage: update-manifest image: alpine:3.18 script: - apk add --no-cache git - git clone https://oauth2:${GIT_TOKEN}${APP_CONFIG_REPO} config-repo - cd config-repo - sed -i s|${IMAGE}:[A-Za-z0-9_.-]*|${IMAGE}:${CI_COMMIT_SHORT_SHA}|g ${APP_CONFIG_DIR}/deployment.yaml - git config user.name ci-bot - git config user.email ci-botexample.com - git add ${APP_CONFIG_DIR}/deployment.yaml - git commit -m chore: update ${CI_PROJECT_NAME} image tag to ${CI_COMMIT_SHORT_SHA} - git push origin main only: - main这里用sed直接替换 deployment.yaml 里的镜像 tag。如果目录下是 Kustomize可以换成kustomize edit set image。更规范的做法是使用yq修改 YAML避免正则替换踩到格式坑。更新仓库后如果 Argo CD 配置了自动同步它会自动拉起新的版本如果没有自动同步可以在 CI 末尾调用argocd app sync app-prod来触发但这就需要 CI 有 Argo CD API 权限了我用得比较少。4.4 发布流程里值得关注的细节一定要有分支保护。main分支要开启 MR/PR 要求CI 机器人推送之前最好也走一个轻量审批否则任何人提交一个高优先级 manifest 到 mainArgo CD 就会直接同步。配置仓库和代码仓库最好分开。如果代码仓库中一个 MR 同时改了代码和部署配置发布和回滚的边界会变模糊。我建议代码仓库只管代码配置仓库单独维护这样回滚一个 commit 就能精准还原部署状态。不要把 secrets 明文放到配置仓库。Argo CD 有很多 secrets 管理方案比如 Sealed Secrets、External Secrets Operator、SOPS选一个适合团队的去落地。裸 Secret 进 Git等于把数据库账号密码写在门牌上。关注 Argo CD 同步顺序。有些资源之间有依赖比如先建 Namespace、再建 ConfigMap最后建 Deployment。如果目录里资源很多可以通过sync-wave注解控制顺序避免应用起来时配置还没就绪。5. 三个知识点的边界和常见面试误区5.1 PriorityClass 与 QoS 类的关系前面在坑里提过这里再单独讲一遍。QoS 类依赖 Pod 的requests和limits用于节点压力驱逐时的优先级而 PriorityClass 是调度器用来排序的权重。两者不是二选一而是同时存在。简单的记忆方法Pod 运行之后QoS 类决定它在宿主机资源紧张时谁先被淘汰Pod 还没运行的时候PriorityClass 决定调度器先让谁进去。5.2 HPA 与 Cluster Autoscaler 的不同HPA 管的是“副本数”Cluster Autoscaler 管的是“节点数”。Pod 水平扩容后如果节点资源不足Pod 会 Pending这时 Cluster Autoscaler 才会看到等待调度的 Pod并决定是否扩节点。两者不是替代关系是上下层关系。面试题喜欢问“HPA 扩容后新 Pod 还是 Pending 怎么办”答题要点就是检查节点剩余资源和 Cluster Autoscaler 状态而不是继续调大 HPA 范围。5.3 Argo CD 与 Helm 是不是重复Argo CD 是持续部署工具负责让集群状态跟着 Git 走Helm 是模板化打包工具负责把一堆 YAML 变成可参数化的 chart。两者可以配合使用Argo CD 的source可以指向一个 Helm chart同步时由 Argo CD 调用 Helm 渲染。Kustomize 也一样。所以这题别答成“选了 Argo CD 就不用 Helm”更合适的说法是用 Helm/Kustomize 管理模板用 Argo CD 管理发布。我已经不止一次在复盘事故时发现PriorityClass 配太高导致普通业务被抢占、HPA 配置了多个指标导致扩容行为异常、Argo CD 忘记关 prune 导致资源被误删。这些问题的共性是功能本身不难难的是想清楚它们在整条发布链路里的边界。如果你现在正在搭这套体系我的建议是先小范围试一个非核心应用把自动同步、自愈、回滚都跑通再逐步扩展到核心业务。等哪一天线上发布变成“改 Git commit等同步看监控”这种平淡动作你就能体会到这组知识点真正的价值。