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

Docker 多阶段构建:让你的镜像小而美

从一个真实案例说起 之前帮一个项目 Dockerize,初始镜像 800MB,每次部署慢得让人怀疑人生。后来用多阶段构建优化完,8MB,部署时间从 3 分钟变成 10 秒。 这篇文章讲讲多阶段构建的原理和实战技巧。 为什么镜像那么大 看个典型 Dockerfile: FROM golang:1.22 WORKDIR /app COPY . . RUN go mod download RUN go build -o myapp . ENTRYPOINT ["./myapp"] 问题在哪? golang:1.22 镜像本身就 800MB+,因为包含了完整的编译工具链 你的源代码、依赖缓存、编译中间产物全在里面 运行一个简单程序,凭什么需要 gcc、git、make ? 多阶段构建:原理很简单 Dockerfile 可以有多个 FROM 指令,每个阶段是独立的。前面的阶段可以复制文件到后面,最终镜像只包含最后一个阶段的内容。 # 第一阶段:构建 FROM golang:1.22 AS builder WORKDIR /app COPY . . RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o myapp . # 第二阶段:运行 FROM alpine:3.19 RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app COPY --from=builder /app/myapp . USER appuser ENTRYPOINT ["./myapp"] 关键点: ...

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

Go 微服务可观测性:日志、指标、追踪实战

背景 微服务架构下,服务间调用链路错综复杂。一旦出问题,没有可观测性支撑,排查起来就是噩梦。 可观测性三驾马车:日志(Logs)、指标(Metrics)、追踪(Traces)。 日志:结构化日志是基础 别再用 fmt.Printf 了,结构化日志才是正道: import "github.com/rs/zerolog" func main() { log := zerolog.New(os.Stdout). With(). Timestamp(). Caller(). Logger() log.Info(). Str("service", "user-service"). Int("request_id", 12345). Msg("User login successful") } 输出: {"level":"info","service":"user-service","request_id":12345,"time":"2026-04-11T10:00:00Z","caller":"main.go:25","message":"User login successful"} 指标:Prometheus + Grafana import "github.com/prometheus/client_golang/prometheus" import "github.com/prometheus/client_golang/prometheus/promhttp" var ( httpRequests = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total HTTP requests", }, []string{"method", "path", "status"}, ) httpDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Buckets: prometheus.DefBuckets, }, []string{"method", "path"}, ) ) func init() { prometheus.MustRegister(httpRequests, httpDuration) } // 中间件示例 func promMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rw := &responseWriter{ResponseWriter: w, statusCode: 200} next.ServeHTTP(rw, r) duration := time.Since(start).Seconds() httpRequests.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(rw.statusCode)).Inc() httpDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration) }) } 分布式追踪:OpenTelemetry import "go.opentelemetry.io/otel" import "go.opentelemetry.io/otel/exporters/jaeger" import "go.opentelemetry.io/otel/sdk/trace" func initTracer() (func(), error) { exp, err := jaeger.New(jaeger.WithAgentEndpoint()) if err != nil { return nil, err } tp := trace.NewTracerProvider( trace.WithBatcher(exp), trace.WithSampler(trace.AlwaysSample()), ) otel.SetTracerProvider(tp) return func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() tp.Shutdown(ctx) }, nil } // 在 HTTP handler 中使用 func handleGetUser(w http.ResponseWriter, r *http.Request) { ctx, span := otel.Tracer("user-service").Start(r.Context(), "GetUser") defer span.End() span.SetAttributes( attribute.String("user.id", r.URL.Query().Get("id")), ) user, err := getUserFromDB(ctx, r.URL.Query().Get("id")) if err != nil { span.RecordError(err) // ... } // 传递给后续调用 go someAsyncOperation(ctx, user) } 三者结合:一个完整示例 type UserService struct { logger zerolog.Logger tracer trace.Tracer metrics *UserMetrics userRepo *UserRepository } func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) { // 1. 开始追踪 ctx, span := s.tracer.Start(ctx, "UserService.GetUser") defer span.End() span.SetAttributes(attribute.String("user.id", id)) // 2. 记录指标 s.metrics.requests.Inc() timer := s.metrics.duration.NewTimer() // 3. 结构化日志 s.logger.Info(). Str("user_id", id). Str("trace_id", span.SpanContext().TraceID().String()). Msg("Fetching user") // 4. 业务逻辑 user, err := s.userRepo.FindByID(ctx, id) if err != nil { // 记录错误,包含追踪上下文 s.logger.Error(). Err(err). Str("user_id", id). Str("trace_id", span.SpanContext().TraceID().String()). Msg("Failed to fetch user") span.RecordError(err) s.metrics.errors.Inc() return nil, err } timer.ObserveDuration() return user, nil } 可视化:用 Grafana 大盘 常见 Dashboard 布局: ...

2026年4月11日 · 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