Kubernetes Gateway API 迁移:从 Ingress 到更细粒度治理

背景 Ingress 够简单,但在多团队协作、细粒度权限和高级路由策略上容易吃力。 迁移思路 先并行接入,不强切 从低风险路由开始迁移 明确 GatewayClass 与 Route 的职责边界 总结 迁移重点不在 YAML 语法,而在组织层面的边界和治理模型。 流量治理升级,技术和协作模型要一起升级。

2026年5月6日 · 1 分钟 · BvBeJ

Kubernetes PDB:驱逐安全与发布稳定

背景 很多集群在节点升级、手动驱逐时出现服务抖动,根因常常是没配或错配 PDB。 基本配置 apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: api-pdb spec: minAvailable: 2 selector: matchLabels: app: api 总结 PDB 不是“可选增强”,而是生产集群进行维护操作的安全护栏。 维护窗口稳定与否,很多时候取决于这几行 YAML。

2026年5月3日 · 1 分钟 · BvBeJ

Kubernetes 中 HPA 与 VPA 如何协同而不互相打架

典型事故 业务高峰来临,HPA 根据 CPU 拉副本;同时 VPA 建议提升 requests,导致 Pod 频繁重建。结果不是稳定,而是不断抖动。 协同原则 HPA 负责“横向弹性”(副本数)。 VPA 负责“纵向建议”(资源基线)。 两者不要同时直接控制同一 Deployment 的同一资源维度。 实操模式 在线服务:HPA + VPA(recommendation only)。 离线作业:VPA(auto) + 关闭 HPA。 把 VPA 建议周期性写回 Helm values,再经灰度发布生效。 关键参数 HPA 目标指标建议使用自定义业务指标(QPS、队列深度),不要只盯 CPU。 HPA behavior 里设置 scaleDown 稳定窗口,防止“刚扩就缩”。 VPA 设置 minAllowed/maxAllowed,避免极端建议。 观测面板建议 当前副本数与目标副本数差值。 Pod 重建频率与重建原因。 requests 利用率分布(而非平均值)。 小结 HPA 与 VPA 本质上是两个控制器。让它们协同的关键,不是“都开”,而是“职责分离 + 变更节流 + 可观测闭环”。

2026年4月30日 · 1 分钟 · BvBeJ

Kubernetes 弹性伸缩:HPA、VPA、KEDA 如何取舍

背景 同样是自动伸缩,HPA、VPA、KEDA 解决的问题并不一样。 选择思路 请求型 Web 服务:优先 HPA 资源画像长期不准:引入 VPA 做建议或自动调参 事件驱动消费:优先 KEDA 常见组合 HPA + Cluster Autoscaler:最常见 HPA + KEDA:API + 消费任务混合系统 VPA 先建议模式观察,再决定是否自动 总结 不要为“自动化程度更高”盲目上更多组件。 先把指标质量做好,再谈伸缩策略。 伸缩策略的上限,取决于指标体系的下限。

2026年4月29日 · 1 分钟 · BvBeJ

Kubernetes 资源配额:性能、稳定性与成本平衡

背景 很多集群的问题,最后都落在资源配置上: requests 太高,调度不进去 requests 太低,服务被抢占导致抖动 limits 太严,CPU 被频繁 throttling 基本策略 先测业务基线,再填 requests CPU limits 结合业务特性决定是否设置 内存 limits 必须有,否则容易把节点拖垮 resources: requests: cpu: "300m" memory: "512Mi" limits: cpu: "1000m" memory: "1Gi" 观测指标 建议持续看: container_cpu_cfs_throttled_seconds_total container_memory_working_set_bytes Pod OOMKilled 次数 总结 资源参数不是一次性配置,而是持续调优过程。 用监控数据驱动参数调整,比凭经验拍数值更靠谱。 资源治理本质是容量治理,最终影响的是可用性和成本。

2026年4月26日 · 1 分钟 · BvBeJ

Kubernetes 成本优化实战:先做对,再做省

成本高不一定是机器贵 很多集群成本高,核心原因是资源配置粗放: request 远大于实际使用 HPA 指标失真导致过度扩容 长尾任务长期占用节点 第一件事:拉齐 request 和真实负载 建议先看 7 到 14 天数据,按 P95 使用量设置 request,再保留安全余量。盲目按峰值配置,浪费通常最大。 第二件事:把弹性策略拆开 在线服务:偏稳定,防抖优先 离线任务:可抢占,成本优先 把工作负载分层后,调参会清晰很多。 第三件事:节点池治理 基础池用稳定机型承载核心服务 弹性池接临时流量和批任务 定期清理空转节点 额外收益点 镜像瘦身缩短拉取时间 减少跨可用区流量 对低优先级任务使用 Spot/抢占实例 小结 成本优化不是一次性动作,而是持续运营。先让容量模型可信,再谈更激进的降本策略。

2026年4月24日 · 1 分钟 · BvBeJ

Kubernetes 探针与优雅退出:避免滚动发布抖动

常见事故模式 发布时最容易看到这几类问题: Pod 刚启动就被打流量,依赖还没就绪 应用短暂卡顿被 liveness 误杀 Pod 被删时连接直接断开,导致错误尖峰 三类探针分工 startupProbe:启动阶段保护期 readinessProbe:是否可以接收流量 livenessProbe:进程是否需要重启 不要用 liveness 去做复杂业务检查,它更适合检测“进程是否活着”。 终止流程要完整 示例配置: spec: terminationGracePeriodSeconds: 30 containers: - name: api lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 8"] 应用内部还需要做到: 收到 SIGTERM 后停止接收新请求 等待在途请求结束 关闭连接池与后台任务 一个经验值 如果入口网关或 Service Mesh 更新端点需要几秒,preStop 这几秒很关键。它给控制面收敛时间,避免流量打到即将退出的 Pod。 结语 高可用不是靠单个参数,而是探针策略和退出流程共同生效。发布稳定性通常是这些细节堆出来的。

2026年4月21日 · 1 分钟 · BvBeJ

Kubernetes 零中断发布:不仅是 RollingUpdate 那么简单

背景 很多团队第一次把服务上到 Kubernetes 时,会觉得滚动发布已经帮我们解决了“不中断发布”的问题。 实际上,RollingUpdate 只是开始,不是答案。 线上真正导致发布抖动的,往往是这些细节: 新 Pod 还没准备好就被加进流量 老 Pod 收到 SIGTERM 后立刻退出 长连接请求被中途切断 readiness 和 liveness 配置混乱 ingress、service、应用本身三层状态不同步 想做到真正意义上的零中断,得把整条链路串起来看。 先理解滚动发布到底做了什么 Deployment 默认使用 RollingUpdate 策略,大致过程是: 拉起新 Pod 等新 Pod Ready 逐步减少旧 Pod 直到新版本全部替换完成 一个常见配置如下: strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 这表示: 发布期间不允许可用实例减少 每次最多多起一个新 Pod 它能降低抖动概率,但不能保证应用层面的请求一定不受影响。 Readiness Probe 决定流量什么时候进来 如果只配了 liveness,没有配 readiness,基本等于告诉集群: “只要进程还活着,就可以接流量。” 这在很多服务里是错的。 比如一个 API 服务启动后还要做这些事情: 加载配置 预热缓存 建立数据库连接池 初始化路由和依赖客户端 在这些动作完成之前,进程虽然活着,但根本不适合接请求。 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 3 periodSeconds: 5 timeoutSeconds: 1 failureThreshold: 3 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10 这两个探针不要混用: ...

2026年4月16日 · 2 分钟 · BvBeJ

Kubernetes Operator 开发实战:用 Go 告别手动运维

背景 Kubernetes Operator 是 CNCF 主推的云原生扩展机制。用 Go 写 Operator 是我日常工作的重要部分。 这篇文章聊聊怎么从零开发一个生产级的 Operator。 核心概念 Operator 核心是声明式 API + reconciliation loop: 用户声明期望状态 → Controller 调和 → 实际状态趋近期望 项目结构 my-operator/ ├── main.go ├── api/ │ └── v1/ │ └── myapp_types.go # CRD 定义 ├── controllers/ │ └── myapp_controller.go # Reconciliation 逻辑 └── config/ ├── crd/ └── rbac/ 第一步:定义 CRD (Custom Resource Definition) // api/v1/myapp_types.go package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type MyAppSpec struct { Replicas int32 `json:"replicas,omitempty"` Image string `json:"image"` Port int32 `json:"port"` EnvVars []EnvVar `json:"envVars,omitempty"` } type EnvVar struct { Name string `json:"name"` Value string `json:"value"` } type MyAppStatus struct { AvailableReplicas int32 `json:"availableReplicas,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:shortName=myapp type MyApp struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MyAppSpec `json:"spec,omitempty"` Status MyAppStatus `json:"status,omitempty"` } func (r *MyApp) Hub() {} 第二步:生成代码 # 安装 controller-gen go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest # 生成 CRD + RBAC + DeepCopy controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." # 生成 CRD YAML controller-gen crd:crdVersions=v1 paths="./..." output:crd:artifacts:config=config/crd/bases 第三步:实现 Controller // controllers/myapp_controller.go package controllers type MyAppReconciler struct { Client client.Client Scheme *runtime.Scheme Log logr.Logger } func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("myapp", req.NamespacedName) // 1. 获取资源 var myapp v1.MyApp if err := r.Get(ctx, req.NamespacedName, &myapp); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // 2. 构建 Deployment deploy := r.buildDeployment(&myapp) if err := ctrl.SetControllerReference(&myapp, deploy, r.Scheme); err != nil { return ctrl.Result{}, err } // 3. 创建或更新 Deployment found := &appsv1.Deployment{} err := r.Get(ctx, req.NamespacedName, found) if err != nil && errors.IsNotFound(err) { log.Info("Creating Deployment", "name", deploy.Name) err = r.Create(ctx, deploy) } else if err == nil { // 更新(需要对比 spec 差异) if !r.deploymentEqual(found, deploy) { found.Spec = deploy.Spec log.Info("Updating Deployment") err = r.Update(ctx, found) } } // 4. 更新 Status r.updateStatus(&myapp, found) return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } func (r *MyAppReconciler) buildDeployment(app *v1.MyApp) *appsv1.Deployment { replicas := app.Spec.Replicas if replicas == 0 { replicas = 1 } return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: app.Name, Namespace: app.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": app.Name}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app": app.Name}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "myapp", Image: app.Spec.Image, Ports: []corev1.ContainerPort{{ ContainerPort: app.Spec.Port, }}, Env: r.buildEnvVars(app.Spec.EnvVars), }}, }, }, }, } } 第四步:启动 Controller // main.go func main() { ctrl.SetLogger(zap.New(zap.UseDevMode(true))) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if err = (&controllers.MyAppReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller") os.Exit(1) } if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } 高级特性 1. Webhook 验证 // webhooks/myapp_webhook.go func (r *MyApp) ValidateCreate() error { if r.Spec.Replicas < 0 { return field.Invalid( field.NewPath("spec").Child("replicas"), r.Spec.Replicas, "replicas must be non-negative", ) } return nil } 2. Finalizer(防止误删) func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { myapp := &v1.MyApp{} r.Get(ctx, req.NamespacedName, myapp) // 删除标记? if myapp.DeletionTimestamp.IsZero() { // 添加 finalizer if !containsString(myapp.GetFinalizers(), "myapp.finalizer") { myapp.Finalizers = append(myapp.GetFinalizers(), "myapp.finalizer") r.Update(ctx, myapp) } } else { // 执行清理逻辑 r.cleanup(myapp) // 移除 finalizer myapp.Finalizers = removeString(myapp.GetFinalizers(), "myapp.finalizer") r.Update(ctx, myapp) } } 测试 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("MyApp controller", func() { Context("with basic spec", func() { It("should create a Deployment", func() { myapp := &v1.MyApp{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", }, Spec: v1.MyAppSpec{ Replicas: 2, Image: "nginx:latest", Port: 80, }, } Expect(k8sClient.Create(ctx, myapp)).Should(Succeed()) }) }) }) 部署 Operator # config/manager/manager.yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-operator spec: replicas: 1 template: spec: containers: - name: operator image: myorg/my-operator:v1.0.0 env: - name: WATCH_NAMESPACE value: "" # OLM (Operator Lifecycle Manager) 安装 operator-sdk olm install operator-sdk run bundle myorg/my-operator-bundle:v1.0.0 总结 Operator 开发的核心: ...

2026年4月4日 · 3 分钟 · BvBeJ