Kiến trúc Microservices với Docker & Kubernetes: Hướng dẫn thực chiến từ A-Z

Trong quá trình hiện đại hóa hạ tầng công nghệ, việc chuyển dịch từ kiến trúc nguyên khối (Monolithic Architecture) sang kiến trúc Microservices với Docker và Kubernetes đã trở thành nền tảng tiêu chuẩn cho các hệ thống phần mềm đòi hỏi tính sẵn sàng cao (High Availability), khả năng mở rộng độc lập theo từng miền nghiệp vụ và tốc độ phát hành liên tục. Tuy nhiên, thực tế vận hành tại nhiều doanh nghiệp cho thấy một nghịch lý phổ biến: mặc dù đội ngũ kỹ thuật đã đóng gói ứng dụng vào Docker Container và triển khai lên cụm Kubernetes, hệ thống vẫn thường xuyên phát sinh lỗi HTTP 502 Bad Gateway hoặc 504 Gateway Timeout mỗi khi thực hiện cập nhật phiên bản mới (Rolling Update), các Pod liên tục rơi vào vòng lặp khởi động lại (CrashLoopBackOff) khi lưu lượng truy cập tăng đột biến, hoặc bị nhân Linux tiêu diệt đột ngột với mã lỗi OOMKilled (Exit Code 137). Nguyên nhân cốt lõi của hiện tượng này nằm ở việc bê nguyên tư duy vận hành máy chủ vật lý cố định vào môi trường điều phối Container động mà thiếu đi sự phối hợp đồng bộ giữa tầng mã nguồn ứng dụng (xử lý tín hiệu dừng SIGTERM, tách biệt điểm cuối kiểm tra sức khỏe) và tầng cấu hình điều phối của Kubernetes (Probes, Lifecycle Hooks, Resource QoS, Autoscaling và Disruption Budget).

Bài viết kỹ thuật chuyên sâu này cung cấp hướng dẫn kiến trúc và triển khai thực chiến từ đầu đến cuối (End-to-End Production Blueprint) cho một hệ thống Microservices tiêu chuẩn gồm ba dịch vụ cốt lõi (auth-service, order-service, payment-service). Chúng ta sẽ đi từ nguyên lý thiết kế Workload phi trạng thái (Stateless), viết mã nguồn dịch vụ bằng Go 1.22 hỗ trợ đóng kết nối êm ái (Graceful Shutdown), đóng gói Container phi đặc quyền (Non-Root UID 65532:65532), cho đến việc xây dựng bộ Manifest Kubernetes hoàn chỉnh (apps/v1 Deployment, ClusterIP Service, networking.k8s.io/v1 Nginx Ingress, autoscaling/v2 HorizontalPodAutoscaler, policy/v1 PodDisruptionBudget) và quy trình chẩn đoán sự cố thực tế bằng kubectl.


Mục Lục Nội Dung

1. Kiến trúc Tổng thể Hệ thống Microservices Cloud-Native trên Kubernetes

Để vận hành ổn định một hệ sinh thái gồm hàng chục đến hàng trăm dịch vụ nhỏ trên Kubernetes, kiến trúc sư hệ thống không thể chỉ nhìn vào từng tệp cấu hình YAML đơn lẻ mà phải thiết lập một mô hình phân tầng rõ ràng: từ tầng tiếp nhận và chấm dứt mã hóa TLS ở biên mạng (Edge Ingress Layer), tầng khám phá dịch vụ và cân bằng tải nội bộ (Internal Service Mesh / ClusterIP Layer), tầng điều phối vòng đời và co giãn Pod (Workload Orchestration Layer) cho đến tầng lưu trữ dữ liệu bền vững và truyền thông hướng sự kiện (Stateful Backing Data & Event Bus Layer).

1.1 Nguyên lý Thiết kế Microservices Chuẩn Cloud-Native: Stateless Workloads, Loose Coupling và Khả năng Tự phục hồi

Khi thiết kế kiến trúc microservices với docker và kubernetes, sự khác biệt căn bản nhất so với mô hình triển khai trên máy ảo truyền thống là tính chất phù du (Ephemerality) của các đơn vị tính toán. Trên một cụm hạ tầng đạt chuẩn Triển khai cụm Kubernetes High Availability trên hạ tầng Bare Metal, một Pod không bao giờ được coi là một máy chủ cố định có địa chỉ IP vĩnh viễn. Bất kỳ lúc nào, một Pod cũng có thể bị thu hồi và tạo mới trên một Worker Node hoàn toàn khác do bộ tự động co giãn (HorizontalPodAutoscaler) thu hẹp quy mô khi hết giờ cao điểm, do bộ lập lịch (kube-scheduler) tái cân bằng tài nguyên, hoặc do quản trị viên rút cạn Node (kubectl drain) để nâng cấp nhân hệ điều hành.

Để thích ứng tuyệt đối với tính chất phù du này, mọi Microservice triển khai dưới dạng Deployment trên Kubernetes phải tuân thủ nghiêm ngặt 3 nguyên lý kiến trúc Cloud-Native:

  1. Phi trạng thái tuyệt đối ở tầng tính toán (Strict Stateless Compute Workloads): Không một Pod nào của auth-service, order-service hay payment-service được phép lưu trữ dữ liệu phiên làm việc (Session State), bộ đệm cục bộ không thể tái tạo hoặc tệp tin nghiệp vụ trên ổ đĩa nội bộ của Container. Toàn bộ trạng thái phiên và danh sách thu hồi JWT phải được đẩy ra cụm bộ nhớ đệm phân tán Redis Cluster; dữ liệu giao dịch đơn hàng được ghi nhận bền vững tại cụm cơ sở dữ liệu quan hệ MySQL 8.0 InnoDB; và hệ thống tệp tin trong Container được khóa ở chế độ chỉ đọc (readOnlyRootFilesystem: true). Nhờ đó, bất kỳ yêu cầu HTTP tiếp theo nào của cùng một người dùng đều có thể được định tuyến tới bất kỳ bản sao (Replica) nào của dịch vụ mà không làm gián đoạn trải nghiệm.
  2. Cô lập dữ liệu theo từng miền dịch vụ và Liên kết lỏng qua Sự kiện (Database-per-Service & Event-Driven Loose Coupling): Một sai lầm phổ biến biến hệ thống Microservices thành "Khối nguyên khối phân tán" (Distributed Monolith) là cho phép nhiều dịch vụ cùng truy vấn trực tiếp vào một cơ sở dữ liệu vật lý dùng chung. Trong kiến trúc chuẩn, order-service sở hữu độc quyền lược đồ dữ liệu đơn hàng của nó. Khi một đơn hàng được khởi tạo thành công, thay vì thực hiện chuỗi gọi đồng bộ (Synchronous Blocking Call) nối tiếp sang hàng loạt dịch vụ phụ trợ gây nghẽn cổ chai, order-service phát hành một sự kiện OrderCreated vào trục truyền thông điệp RabbitMQ Event Bus để payment-service và các dịch vụ thông báo tiêu thụ bất đồng bộ.
  3. Tự phục hồi đa cấp độ (Multi-Level Self-Healing): Thay vì dựa vào sự can thiệp thủ công của kỹ sư trực hệ thống lúc nửa đêm, nền tảng phải tự động phát hiện và cô lập lỗi. Khi một tiến trình gặp lỗi treo khóa (Deadlock), Kubelet tự động khởi động lại Container thông qua livenessProbe. Khi một Pod tạm thời quá tải hoặc mất kết nối tới cơ sở dữ liệu trong vài giây, readinessProbe tự động rút địa chỉ IP của Pod đó khỏi danh sách nhận tải (EndpointSlice) mà không cần hủy Pod, chờ đến khi kết nối phục hồi để đưa Pod trở lại phục vụ.

1.2 Sơ đồ Kiến trúc Định tuyến và Điều phối Workload trên Cụm Kubernetes

Sơ đồ kiến trúc dưới đây mô tả chi tiết luồng phân phối lưu lượng mạng và mối quan hệ điều phối giữa các tài nguyên Kubernetes trong không gian tên (Namespace) production cho ba dịch vụ auth-service, order-service và payment-service:

flowchart TD
    subgraph ExternalTraffic ["1. Lưu lượng Truy cập Bên ngoài (External Clients)"]
        Client["Web / Mobile Client (HTTPS TLS 1.3)"] --> Ingress["Nginx Ingress Controller (api.infoteck.vn)"]
    end

    subgraph K8sServiceLayer ["2. Tầng Định tuyến Nội bộ Kubernetes (ClusterIP Services & EndpointSlices)"]
        Ingress -- "/api/v1/auth" --> AuthSVC["Service: auth-service (ClusterIP :80 -> :8080)"]
        Ingress -- "/api/v1/orders" --> OrderSVC["Service: order-service (ClusterIP :80 -> :8080)"]
        Ingress -- "/api/v1/payments" --> PaySVC["Service: payment-service (ClusterIP :80 -> :8080)"]
    end

    subgraph K8sWorkloadLayer ["3. Tầng Điều phối Pod & Tự động Co giãn (Deployments, HPA v2 & PDB)"]
        AuthSVC --> AuthPods["Auth Pods (Replicas: 3..8 - Non-Root UID 65532)"]
        OrderSVC --> OrderPods["Order Pods (Replicas: 3..12 - Startup/Readiness/Liveness Probes)"]
        PaySVC --> PayPods["Payment Pods (Replicas: 3..10 - Graceful SIGTERM Handler)"]
        HPA["HorizontalPodAutoscaler v2 (CPU 70% / Mem 80%)"] -. "Tự động Scale Out/In" .-> OrderPods
        PDB["PodDisruptionBudget (minAvailable: 2)"] -. "Chống Gián đoạn khi Drain Node" .-> OrderPods
    end

    subgraph BackingDataLayer ["4. Tầng Lưu trữ Dữ liệu & Hàng đợi Sự kiện (Stateful Backing Services)"]
        AuthPods --> Redis[("Redis Cluster (Session & JWT Revocation Cache)")]
        OrderPods --> MySQL[("MySQL 8.0 InnoDB Primary / Replica")]
        OrderPods -- "Publish OrderCreated Event" --> RMQ[("RabbitMQ Event Bus")]
        RMQ -- "Consume Event" --> PayPods
    end

Nhìn vào sơ đồ trên, toàn bộ các Pod ứng dụng hoàn toàn không mở cổng trực tiếp ra mạng ngoài (NodePort hay HostPort). Mọi yêu cầu HTTPS từ người dùng cuối đều đi vào điểm tiếp nhận duy nhất là Nginx Ingress Controller. Sau khi kiểm tra chứng chỉ số TLS và áp dụng các chính sách giới hạn tốc độ truy cập ở biên mạng, Nginx Ingress phân tích đường dẫn URI (/api/v1/auth, /api/v1/orders, /api/v1/payments) để chuyển tiếp gói tin thẳng tới các Pod khỏe mạnh thông qua cơ chế theo dõi đối tượng EndpointSlice gắn liền với từng ClusterIP Service.

1.3 Chuỗi Tương tác Vòng đời Request và Cơ chế Graceful Shutdown khi Rolling Update

Một câu hỏi kỹ thuật mang tính sống còn đối với mọi kỹ sư DevOps/SRE là: "Tại sao hệ thống vẫn trả về lỗi 502 Bad Gateway cho khách hàng trong quá trình chạy Rolling Update, dù chúng ta đã khai báo readinessProbe đầy đủ?". Câu trả lời nằm ở cơ chế thực thi song song bất đồng bộ (Asynchronous Race Condition) bên trong kiến trúc điều khiển của Kubernetes khi một Pod chuyển sang trạng thái Terminating.

Khi kube-apiserver ra lệnh thu hồi một Pod cũ để thay thế bằng Pod phiên bản mới, hai luồng sự kiện độc lập được kích hoạt cùng một thời điểm:

  • Luồng 1 (Cập nhật mạng trên toàn cụm): EndpointSlice Controller phát hiện Pod chuyển sang trạng thái Terminating nên xóa địa chỉ IP của Pod đó khỏi đối tượng EndpointSlice. Tiếp theo, kube-proxy trên từng Worker Node (đang cập nhật bảng quy tắc IPVS/iptables) và bộ điều khiển Nginx Ingress Controller (đang nạp lại danh sách Upstream) sẽ nhận thông báo này để ngừng gửi kết nối mới tới Pod cũ. Tuy nhiên, quá trình lan truyền thông tin qua mạng và nạp lại cấu hình Upstream này tốn từ 1 đến 3 giây.
  • Luồng 2 (Tắt tiến trình tại Node chứa Pod): Cùng lúc đó, tiến trình Kubelet trên Worker Node nhận lệnh dừng Pod. Nếu Pod không khai báo lifecycle.preStop, Kubelet sẽ lập tức gửi tín hiệu SIGTERM tới tiến trình chính (PID 1) bên trong Container ở giây thứ 0. Nếu ứng dụng bên trong Container đóng cổng lắng nghe HTTP ngay khi nhận SIGTERM, thì trong khoảng thời gian trễ 1 đến 3 giây trước khi Nginx Ingress kịp cập nhật bảng định tuyến, mọi yêu cầu mới do Nginx Ingress gửi tới địa chỉ IP của Pod đó sẽ va phải lỗi Connection Refused và trả về HTTP 502 Bad Gateway cho người dùng cuối!

Sơ đồ tuần tự dưới đây minh họa giải pháp chuẩn Production kết hợp giữa lifecycle.preStop (sleep 5) và trình xử lý SIGTERM (http.Server.Shutdown) để triệt tiêu hoàn toàn khoảng trống bất đồng bộ này:

sequenceDiagram
    autonumber
    participant API as kube-apiserver
    participant Kubelet as Kubelet (Worker Node)
    participant EP as EndpointSlice & Nginx Ingress
    participant Pod as Order-Service Pod (Terminating)
    actor Client as In-Flight HTTP Request

    Note over API,Client: Quá trình thu hồi Pod cũ không gây lỗi 502 Bad Gateway khi Rolling Update
    API->>Kubelet: Khởi tạo Pod mới (Ready = True) & Ra lệnh chấm dứt Pod cũ (Status: Terminating)
    par Luồng 1: Cập nhật Bảng Định tuyến Mạng (Bất đồng bộ)
        API->>EP: Xóa địa chỉ IP của Pod cũ khỏi EndpointSlice
        EP->>EP: Nginx Ingress Controller & kube-proxy ngừng gửi kết nối mới vào Pod cũ
    and Luồng 2: Thực thi preStop Hook & Draining tại Pod cũ
        Kubelet->>Pod: Thực thi lifecycle.preStop Hook (sleep 5 giây chờ đồng bộ EndpointSlice)
        Client->>Pod: Request cuối cùng đang trên đường truyền tới Pod cũ (In-Flight Request)
        Pod-->>Client: Xử lý bình thường & Trả về HTTP 200 OK
        Kubelet->>Pod: Gửi tín hiệu SIGTERM tới tiến trình PID 1 (order-service)
        Pod->>Pod: Đổi trạng thái /readyz sang HTTP 503 & Gọi http.Server.Shutdown(ctx)
        Pod->>Pod: Hoàn tất các giao dịch MySQL đang dở dang & Đóng kết nối an toàn
        Pod-->>Kubelet: Tiến trình thoát sạch với Exit Code 0 (Trước thời hạn terminationGracePeriodSeconds: 30s)
    end


2. Đóng gói Microservice Chuẩn Production với Docker & Xử lý Tín hiệu Graceful Shutdown

Một hệ thống điều phối Kubernetes mạnh mẽ đến đâu cũng không thể bảo vệ được một ứng dụng "mù" trước các tín hiệu của hệ điều hành hoặc chạy dưới quyền quản trị viên tối cao (root - UID 0). Trong phần này, chúng ta sẽ xây dựng mã nguồn thực tế cho dịch vụ order-service bằng ngôn ngữ Go 1.22 và đóng gói nó vào một ảnh Container siêu nhẹ, phi đặc quyền.

2.1 Thiết lập HTTP Server Hỗ trợ Health Check (/healthz, /readyz) và Bắt Tín hiệu SIGTERM trong Go

Để giao tiếp chuẩn xác với bộ điều phối của Kubernetes, mã nguồn HTTP Server của mỗi Microservice phải cung cấp hai điểm cuối kiểm tra sức khỏe có ngữ nghĩa hoàn toàn tách biệt:

  1. Điểm cuối kiểm tra sự sống (/healthz - Liveness Endpoint): Chỉ kiểm tra xem tiến trình máy chủ HTTP và bộ lập lịch Goroutine bên trong ứng dụng còn phản hồi hay đã bị treo cứng (Deadlock). Điểm cuối này phải cực kỳ nhẹ, phản hồi HTTP 200 OK trong vòng chưa tới 1 mili-giây và tuyệt đối không thực hiện truy vấn sang cơ sở dữ liệu hay dịch vụ bên ngoài.
  2. Điểm cuối kiểm tra tính sẵn sàng nhận tải (/readyz - Readiness Endpoint): Kiểm tra xem bản sao dịch vụ hiện tại có đủ điều kiện tiếp nhận yêu cầu nghiệp vụ mới hay không. Chúng ta quản lý trạng thái này bằng biến nguyên tử atomic.Bool (isReady). Ngay khi ứng dụng khởi tất xong kết nối cơ sở dữ liệu, isReady được bật thành true (HTTP 200 OK). Ngay khoảnh khắc tiến trình nhận được tín hiệu dừng SIGTERM từ Kubernetes (hoặc SIGINT khi chạy cục bộ), ứng dụng lập tức chuyển isReady.Store(false) để trả về HTTP 503 Service Unavailable.

Khi tín hiệu SIGTERM xuất hiện thông qua signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT), thay vì thoát đột ngột bằng os.Exit(0) làm đứt ngang các giao dịch thanh toán và tạo đơn hàng đang xử lý dở dang, chúng ta khởi tạo một ngữ cảnh có thời hạn (context.WithTimeout 20 giây) và gọi hàm chuẩn srv.Shutdown(shutdownCtx). Hàm này thực hiện cơ chế Rút cạn kết nối (Connection Draining): đóng ngay cổng lắng nghe kết nối mới nhưng giữ nguyên các kết nối HTTP hiện hữu cho đến khi toàn bộ các Goroutine xử lý yêu cầu đang chạy (In-Flight Requests) trả về phản hồi hoàn tất cho khách hàng, sau đó mới đóng kết nối cơ sở dữ liệu và thoát tiến trình với mã Exit Code 0.

2.2 Đóng gói Container Phi Đặc quyền (Non-Root USER 65532:65532) Tối giản Bề mặt Tấn công

Nếu nhìn lại cách viết Dockerfile sơ khai thường gặp, nhiều lập trình viên sử dụng FROM scratch hoặc FROM alpine nhưng đặt WORKDIR /root/ và chạy ứng dụng trực tiếp dưới quyền UID 0 (root). Trong môi trường Kubernetes đa dịch vụ, việc chạy Container dưới quyền root là một lỗ hổng bảo mật nghiêm trọng: nếu kẻ tấn công khai thác được một lỗi tràn bộ đệm hoặc thực thi mã từ xa kết hợp với lỗ hổng thoát khỏi không gian tên nhân Linux (Container Escape như CVE-2024-21626 trên runc), tiến trình UID 0 trong Container sẽ lập tức trở thành root toàn quyền trên máy chủ Worker Node vật lý.

Để tuân thủ tiêu chuẩn Kubernetes Pod Security Standards (Restricted Profile) đồng thời vẫn giữ được khả năng thực thi lệnh sleep trong móc vòng đời preStop, chúng ta áp dụng kỹ thuật đóng gói đa tầng (Multi-Stage Build — xem thêm phân tích chuyên sâu về bộ nhớ đệm lớp ảnh tại bài viết Tối ưu hóa Docker Image với Multi-stage Build và Alpine Linux) với hai giai đoạn:

  • Giai đoạn 1 (builder trên golang:1.22-alpine3.20): Biên dịch mã nguồn Go thành tệp thực thi tĩnh hoàn toàn (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) kèm cờ tối ưu hóa kích thước -ldflags="-s -w" (loại bỏ bảng ký hiệu gỡ lỗi DWARF, giảm kích thước tệp nhị phân xuống dưới 8 MB).
  • Giai đoạn 2 (runtime trên alpine:3.20 đã gia cố bảo mật): Chỉ cài đặt gói chứng chỉ gốc TLS (ca-certificates) và dữ liệu múi giờ (tzdata), khởi tạo tài khoản hệ thống phi đặc quyền appuser có định danh chuẩn UID 65532 / GID 65532, không có quyền đăng nhập tương tác (/sbin/nologin), và chạy tệp nhị phân bằng cú pháp mảng ENTRYPOINT ["/app/order-service"] để tiến trình Go nhận trực tiếp PID 1 và bắt chính xác tín hiệu SIGTERM từ Kubelet.

Khối mã hoàn chỉnh dưới đây bao gồm toàn bộ mã nguồn dịch vụ cmd/order-service/main.go và cấu hình đóng gói Dockerfile chuẩn Production:

// File: cmd/order-service/main.go
// Go 1.22+ | Production Microservice with Atomic Readiness & SIGTERM Graceful Draining
package main

import (
    "context"
    "encoding/json"
    "errors"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "sync/atomic"
    "syscall"
    "time"
)

type OrderItemResponse struct {
    OrderID   string `json:"order_id"`
    Status    string `json:"status"`
    AmountVND int64  `json:"amount_vnd"`
    PodName   string `json:"served_by_pod"`
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
    slog.SetDefault(logger)

    var isReady atomic.Bool
    isReady.Store(true)

    podHostname, err := os.Hostname()
    if err != nil {
        podHostname = "unknown-order-pod"
    }

    mux := http.NewServeMux()

    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte(`{"status":"ALIVE"}`))
    })

    mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        if !isReady.Load() {
            w.WriteHeader(http.StatusServiceUnavailable)
            _, _ = w.Write([]byte(`{"status":"DRAINING_NOT_READY"}`))
            return
        }
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte(`{"status":"READY"}`))
    })

    mux.HandleFunc("GET /api/v1/orders", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        payload := []OrderItemResponse{
            {
                OrderID:   "ord-9f8e7d6c-2026",
                Status:    "CONFIRMED",
                AmountVND: 2450000,
                PodName:   podHostname,
            },
        }
        w.WriteHeader(http.StatusOK)
        _ = json.NewEncoder(w).Encode(payload)
        slog.Info("processed order request", "path", r.URL.Path, "pod", podHostname)
    })

    srv := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      15 * time.Second,
        IdleTimeout:       60 * time.Second,
    }

    signalCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
    defer stopSignals()

    go func() {
        slog.Info("starting order-service HTTP server", "addr", srv.Addr, "pod", podHostname)
        if serveErr := srv.ListenAndServe(); serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
            slog.Error("HTTP server failed unexpectedly", "error", serveErr)
            os.Exit(1)
        }
    }()

    <-signalCtx.Done()
    slog.Info("received termination signal, flipping /readyz to 503 and draining connections")
    isReady.Store(false)

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    if shutdownErr := srv.Shutdown(shutdownCtx); shutdownErr != nil {
        slog.Error("graceful shutdown timed out", "error", shutdownErr)
        os.Exit(1)
    }
    slog.Info("order-service exited cleanly with code 0")
}

// -----------------------------------------------------------------------------
// File: Dockerfile (Multi-Stage Non-Root Production Container Packaging)
// -----------------------------------------------------------------------------
// # syntax=docker/dockerfile:1.7
// FROM golang:1.22-alpine3.20 AS builder
// WORKDIR /src
// COPY go.mod ./
// RUN go mod download
// COPY cmd/ ./cmd/
// RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 
//     go build -trimpath -ldflags="-s -w" -o /out/order-service ./cmd/order-service/main.go
//
// FROM alpine:3.20 AS runtime
// RUN apk --no-cache add ca-certificates tzdata && 
//     addgroup -g 65532 -S nonroot && 
//     adduser -u 65532 -S -G nonroot -H -s /sbin/nologin nonroot
// WORKDIR /app
// COPY --from=builder --chown=65532:65532 /out/order-service /app/order-service
// USER 65532:65532
// EXPOSE 8080
// ENTRYPOINT ["/app/order-service"]


3. Thiết kế Kubernetes Deployment: Resource QoS, SecurityContext và Bộ 3 Probes (Startup, Readiness, Liveness)

Sau khi đã có ảnh Container registry.infoteck.vn/ecommerce/order-service:v1.4.2 được biên dịch tĩnh và chạy dưới quyền UID 65532, bước tiếp theo là khai báo đối tượng apps/v1 Deployment để Kubernetes quản lý vòng đời các bản sao (Replicas) của dịch vụ. Một tệp cấu hình Deployment dùng cho môi trường Production khác xa với các đoạn YAML mẫu chỉ dài 20 dòng: nó phải định nghĩa rõ ràng cơ chế kiểm tra sức khỏe 3 giai đoạn (Probes), hạn mức tài nguyên (Resource Requests & Limits), ngữ cảnh bảo mật hạt nhân (securityContext) và quy tắc phân tán Pod trên nhiều máy chủ vật lý (topologySpreadConstraints).

3.1 Phân biệt Vai trò của startupProbe, readinessProbe và livenessProbe Tránh Vòng lặp Khởi động Lại (CrashLoopBackOff)

Trong kiến trúc Kubernetes, Kubelet sử dụng ba bộ dò (Probes) độc lập để giám sát từng Container. Việc cấu hình sai thông số thời gian hoặc nhầm lẫn mục đích giữa ba bộ dò này là nguyên nhân hàng đầu dẫn đến thảm họa sập dây chuyền (Cascading Failure) trên môi trường Production:

Tiêu chí Kỹ thuật startupProbe (Bộ dò Khởi động) readinessProbe (Bộ dò Sẵn sàng) livenessProbe (Bộ dò Sự sống)
Mục đích cốt lõi Xác nhận ứng dụng đã hoàn tất giai đoạn khởi tạo ban đầu (nạp cấu hình, khởi tạo Connection Pool, làm nóng bộ đệm). Xác nhận Pod hiện tại đủ khả năng xử lý các yêu cầu HTTP mới từ phía khách hàng. Phát hiện tiến trình bị rơi vào trạng thái treo cứng (Deadlock) không thể tự thoát ra được.
Thời điểm kích hoạt Chạy ngay khi Container vừa khởi tạo; vô hiệu hóa hoàn toàn livenessProbe và readinessProbe cho đến khi startupProbe thành công lần đầu tiên. Chạy định kỳ xuyên suốt toàn bộ vòng đời của Container sau khi startupProbe đã vượt qua. Chạy định kỳ xuyên suốt toàn bộ vòng đời của Container sau khi startupProbe đã vượt qua.
Hành động của Kubernetes khi thất bại vượt ngưỡng (failureThreshold) Kubelet tiêu diệt và khởi động lại Container theo chính sách restartPolicy. Kubelet không khởi động lại Container, mà chỉ rút địa chỉ IP của Pod ra khỏi đối tượng EndpointSlice của Service để ngừng gửi lưu lượng mới tới Pod. Kubelet tiêu diệt và khởi động lại Container ngay lập tức.
Điểm cuối đích và Nguyên tắc kiểm tra Trỏ vào /healthz với ngân sách thời gian rộng (failureThreshold: 30 * periodSeconds: 2s = 60s) để bảo vệ các tiến trình khởi động chậm khi Node đang chịu tải cao. Trỏ vào /readyz (periodSeconds: 5s, failureThreshold: 2); kiểm tra biến trạng thái isReady và tình trạng hàng đợi xử lý. Trỏ vào /healthz (periodSeconds: 10s, failureThreshold: 3); tuyệt đối không kiểm tra kết nối Database hay dịch vụ phụ trợ bên ngoài.

Tại sao livenessProbe tuyệt đối không được kiểm tra kết nối cơ sở dữ liệu? Giả sử cụm cơ sở dữ liệu MySQL gặp hiện tượng nghẽn mạng thoáng qua trong 6 giây. Nếu lập trình viên viết hàm /healthz thực hiện lệnh db.Ping(), cả 10 Pod của order-service sẽ đồng loạt trả về HTTP 500 cho livenessProbe. Ngay lập tức, Kubelet trên toàn cụm sẽ tiêu diệt và khởi động lại cả 10 Pod cùng một lúc, biến một sự cố chập chờn 6 giây thành sự cố ngừng hoạt động toàn phần và gây ra bão kết nối (Connection Storm) dội ngược vào MySQL khi các Pod đồng loạt khởi động lại. Bằng cách tách riêng /healthz (chỉ kiểm tra nội tại tiến trình) và /readyz (điều khiển lưu lượng mạng), hệ thống vẫn giữ nguyên tiến trình sống và tự động nhận tải trở lại ngay khi kết nối ổn định.

3.2 Cấu hình Deployment Hoàn chỉnh với Resource Requests/Limits, preStop Hook và topologySpreadConstraints

Bên cạnh bộ 3 Probes, tệp cấu hình k8s/order-deployment.yaml dưới đây áp dụng 4 kỹ thuật gia cố quan trọng cho môi trường Production:

  1. Chiến lược cập nhật cuốn chiếu không gián đoạn (maxUnavailable: 0, maxSurge: 1): Bắt buộc Kubernetes phải khởi tạo Pod phiên bản mới và chờ cho đến khi Pod mới vượt qua cả startupProbe lẫn readinessProbe trước khi được phép chấm dứt bất kỳ Pod phiên bản cũ nào, đảm bảo tổng năng lực xử lý của dịch vụ không bao giờ sụt giảm dưới 100% trong suốt đợt triển khai.
  2. Móc vòng đời lifecycle.preStop kết hợp terminationGracePeriodSeconds: 30: Khi một Pod cũ bị thu hồi, Kubelet chạy lệnh ["/bin/sh", "-c", "sleep 5"] trước khi gửi SIGTERM, tạo khoảng đệm 5 giây để EndpointSlice và Nginx Ingress Controller rút hoàn toàn địa chỉ IP của Pod đó khỏi danh sách cân bằng tải.
  3. Ngữ cảnh bảo mật hạt nhân (securityContext chuẩn Restricted): Ép buộc runAsNonRoot: true, runAsUser: 65532, khóa hệ thống tệp gốc ở chế độ chỉ đọc (readOnlyRootFilesystem: true), gắn ổ đĩa tạm trên RAM (emptyDir: {medium: Memory}) tại /tmp cho các tệp tạm thời, chặn leo thang đặc quyền (allowPrivilegeEscalation: false), thu hồi toàn bộ năng lực nhân Linux (capabilities.drop: ["ALL"]) và áp dụng bộ lọc lời gọi hệ thống seccompProfile: RuntimeDefault.
  4. Phân bổ chống điểm lỗi đơn lẻ (topologySpreadConstraints): Đảm bảo các bản sao của order-service luôn được rải đều trên các Worker Node và vùng khả dụng (topology.kubernetes.io/zone) khác nhau, tránh rủi ro toàn bộ 3 Pod cùng nằm trên một máy chủ vật lý duy nhất.

# File: k8s/order-deployment.yaml
# Kubernetes v1.29+ | Production-Hardened Microservice Deployment Manifest
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
  labels:
    app.kubernetes.io/name: order-service
    app.kubernetes.io/part-of: ecommerce-platform
    app.kubernetes.io/version: v1.4.2
spec:
  replicas: 3
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app.kubernetes.io/name: order-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app.kubernetes.io/name: order-service
        app.kubernetes.io/part-of: ecommerce-platform
        app.kubernetes.io/version: v1.4.2
    spec:
      automountServiceAccountToken: false
      terminationGracePeriodSeconds: 30
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        runAsGroup: 65532
        fsGroup: 65532
        seccompProfile:
          type: RuntimeDefault
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: order-service
      containers:
        - name: order-service
          image: registry.infoteck.vn/ecommerce/order-service:v1.4.2
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          envFrom:
            - configMapRef:
                name: order-service-config
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "1000m"
              memory: "512Mi"
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
          startupProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 2
            timeoutSeconds: 2
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            successThreshold: 1
            failureThreshold: 2
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - -c
                  - sleep 5
          volumeMounts:
            - name: tmp-memory-volume
              mountPath: /tmp
      volumes:
        - name: tmp-memory-volume
          emptyDir:
            medium: Memory
            sizeLimit: 64Mi


4. Định tuyến Nội bộ (ClusterIP Service) và Phơi bày Dịch vụ qua Nginx Ingress Controller

Khi các Pod của order-service, auth-service và payment-service đã khởi chạy ổn định dưới sự điều phối của Deployment, mỗi Pod sẽ được mạng CNI (như Calico hoặc Cilium) cấp một địa chỉ IP nội bộ riêng biệt (ví dụ 10.244.1.18, 10.244.2.45). Tuy nhiên, vì địa chỉ IP của Pod liên tục thay đổi mỗi khi triển khai mới hoặc tự động co giãn, các dịch vụ khác cũng như cổng API Gateway không bao giờ kết nối trực tiếp vào IP của từng Pod, mà phải thông qua lớp trừu tượng hóa mạng của Kubernetes: Service và Ingress.

4.1 Cơ chế Khám phá Dịch vụ (CoreDNS) và Cân bằng Tải Nội bộ với Service kiểu ClusterIP

Đối tượng Service kiểu ClusterIP đóng vai trò là điểm neo mạng tĩnh (Stable Virtual IP & DNS Name) đứng trước một nhóm các Pod động được gom nhóm thông qua bộ chọn nhãn (selector.app.kubernetes.io/name: order-service). Cơ chế vận hành bên dưới của ClusterIP Service diễn ra qua ba thành phần cốt lõi:

  1. Đồng bộ hóa danh sách điểm cuối thời gian thực (EndpointSlices): Bộ điều khiển EndpointSlice Controller trên Control Plane liên tục theo dõi toàn bộ các Pod khớp với nhãn của Service. Chỉ những Pod nào đang ở trạng thái Running và có kết quả kiểm tra readinessProbe đạt True mới được đưa địa chỉ IP và cổng (:8080) vào danh sách hoạt động của đối tượng EndpointSlice. Ngay khi một Pod trả về HTTP 503 tại /readyz, địa chỉ IP của nó lập tức bị đánh dấu ready: false trong EndpointSlice.
  2. Phân giải tên miền nội bộ qua CoreDNS: Khi Service order-service được khởi tạo trong không gian tên production, dịch vụ DNS nội bộ của cụm là CoreDNS tự động tạo bản ghi A/AAAA cho tên miền đầy đủ (FQDN): order-service.production.svc.cluster.local trỏ về địa chỉ IP ảo (ClusterIP) của dịch vụ. Nhờ vậy, khi một dịch vụ nội bộ khác muốn gọi sang order-service, nó chỉ cần gửi yêu cầu HTTP tới http://order-service.production.svc.cluster.local/api/v1/orders.
  3. Ảo hóa mạng ở cấp độ Nhân Linux (kube-proxy ở chế độ IPVS): Trên mỗi Worker Node, tiến trình kube-proxy theo dõi sự thay đổi của EndpointSlices và lập trình các quy tắc chuyển tiếp gói tin ngay trong không gian nhân Linux (Kernel Space). Trong các cụm Production quy mô lớn chạy chế độ IPVS (IP Virtual Server) dựa trên bảng băm (Hash Table với độ phức tạp tra cứu O(1)), mọi gói tin gửi tới địa chỉ ClusterIP:80 sẽ được nhân Linux tự động viết lại đích đến (DNAT) và phân phối đều sang địa chỉ IP thực tế của các Pod khỏe mạnh ở cổng :8080 mà không phát sinh độ trễ của một tiến trình Proxy trung gian.

4.2 Cấu hình Ingress (ingressClassName: nginx), Chấm dứt TLS (TLS Termination) và Định tuyến theo Đường dẫn

Trong khi ClusterIP Service giải quyết bài toán giao tiếp Đông-Tây (East-West Traffic) bên trong cụm, thì để tiếp nhận lưu lượng Bắc-Nam (North-South Traffic) từ người dùng ngoài Internet đi vào qua một tên miền thống nhất (https://api.infoteck.vn), chúng ta sử dụng tài nguyên chuẩn networking.k8s.io/v1 Ingress được điều khiển bởi Nginx Ingress Controller.

Khác với việc cấp phát một bộ cân bằng tải đám mây (type: LoadBalancer) riêng rẽ cho từng Microservice gây lãng phí chi phí và phân mảnh chứng chỉ bảo mật, đối tượng Ingress cho phép hợp nhất định tuyến (Consolidate Routing) cho toàn bộ hệ sinh thái Microservices tại một điểm vào duy nhất:

  • Chấm dứt mã hóa TLS tập trung (Centralized TLS Termination): Chứng chỉ số X.509 và khóa riêng tư của tên miền api.infoteck.vn (thường được cấp phát và gia hạn tự động qua cert-manager kết nối Let's Encrypt) được lưu trữ trong một đối tượng Secret kiểu kubernetes.io/tls mang tên infoteck-api-tls. Nginx Ingress Controller nạp chứng chỉ này để thực hiện bắt tay TLS 1.3 ở biên mạng và tự động chuyển hướng mọi kết nối HTTP không mã hóa sang HTTPS (nginx.ingress.kubernetes.io/ssl-redirect: "true").
  • Định tuyến trực tiếp tới Pod (Service Upstream Bypass): Một ưu điểm kỹ thuật vượt trội của Nginx Ingress Controller là nó không gửi lưu lượng qua địa chỉ ảo ClusterIP của Service (tránh một bước dịch địa chỉ mạng thừa của kube-proxy), mà sử dụng tên Service khai báo trong Ingress để truy vấn trực tiếp danh sách địa chỉ IP của các Pod khỏe mạnh từ EndpointSlices và cân bằng tải thẳng tới từng Pod.
  • Bảo vệ biên mạng qua Annotations: Thiết lập giới hạn kích thước gói tin tải lên tối đa (proxy-body-size: "10m"), thời gian chờ đọc/gửi với dịch vụ phía sau (proxy-read-timeout: "30", proxy-send-timeout: "30") nhằm ngăn chặn các kết nối treo làm cạn kiệt Worker của Nginx.

Tệp cấu hình đa tài nguyên k8s/order-service-ingress.yaml dưới đây định nghĩa trọn vẹn ConfigMap, các ClusterIP Service và quy tắc định tuyến Ingress cho cả ba dịch vụ auth-service, order-service và payment-service:

# File: k8s/order-service-ingress.yaml
# Kubernetes v1.29+ | ConfigMap, ClusterIP Services & Nginx Ingress Controller Manifest
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
  namespace: production
data:
  APP_ENV: "production"
  LOG_FORMAT: "json"
  HTTP_PORT: "8080"
  DB_MAX_OPEN_CONNS: "40"
  DB_MAX_IDLE_CONNS: "20"
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: production
  labels:
    app.kubernetes.io/name: order-service
    app.kubernetes.io/part-of: ecommerce-platform
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: order-service
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ecommerce-api-gateway
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "5"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "30"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.infoteck.vn
      secretName: infoteck-api-tls
  rules:
    - host: api.infoteck.vn
      http:
        paths:
          - path: /api/v1/auth
            pathType: Prefix
            backend:
              service:
                name: auth-service
                port:
                  number: 80
          - path: /api/v1/orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80
          - path: /api/v1/payments
            pathType: Prefix
            backend:
              service:
                name: payment-service
                port:
                  number: 80


5. Tự động Co giãn Theo Tải (HorizontalPodAutoscaler) & Chống Gián đoạn bằng PodDisruptionBudget

Triển khai thành công một Microservice với số lượng bản sao cố định (replicas: 3) mới chỉ giải quyết được một nửa bài toán vận hành trên đám mây. Trong thực tế thương mại điện tử, lưu lượng truy cập vào order-service trong các khung giờ Flash Sale có thể tăng gấp 5 đến 8 lần so với mức nền ban đêm, trong khi cụm hạ tầng Kubernetes lại thường xuyên phải thực hiện các đợt bảo trì định kỳ (nâng cấp phiên bản Kubernetes hoặc vá lỗi bảo mật nhân hệ điều hành trên từng Worker Node). Để tự động hóa hoàn toàn hai kịch bản này, chúng ta kết hợp bộ đôi tài nguyên autoscaling/v2 HorizontalPodAutoscaler (HPA) và policy/v1 PodDisruptionBudget (PDB).

5.1 Thiết lập HorizontalPodAutoscaler (autoscaling/v2) với Chính sách Co giãn Mượt mà (behavior.scaleDown)

Bộ điều khiển HorizontalPodAutoscaler hoạt động theo chu kỳ kiểm tra mặc định mỗi 15 giây: nó truy vấn chỉ số tiêu thụ tài nguyên thực tế của các Pod từ Metrics Server (metrics.k8s.io) và tính toán số lượng bản sao mục tiêu theo công thức chuẩn của Kubernetes:

desiredReplicas = ceil[currentReplicas * (currentMetricValue / desiredMetricValue)]

Trong đó, currentMetricValue là tỷ lệ phần trăm sử dụng trung bình so với mức resources.requests (chứ không phải limits) đã khai báo trong Deployment. Chẳng hạn, với requests.cpu: 250m và ngưỡng mục tiêu averageUtilization: 70, nếu 3 Pod hiện tại tiêu thụ trung bình 400m CPU mỗi Pod (tức 160% so với mức requests), HPA sẽ tính toán ceil[3 * (160 / 70)] = ceil[6.85] = 7 Pod và tự động nâng quy mô của order-service từ 3 lên 7 bản sao.

Khi sử dụng phiên bản API chuẩn autoscaling/v2, chúng ta cần đặc biệt lưu ý hai kỹ thuật tinh chỉnh quan trọng:

  1. Kết hợp đa chỉ số (Multi-Metric Scaling trên cả CPU và Memory): Một dịch vụ xử lý đơn hàng có thể bị nghẽn CPU khi giải mã chữ ký mật mã hoặc bị đầy bộ nhớ RAM khi xử lý các danh sách đơn hàng lớn. Khi khai báo đồng thời ngưỡng CPU (70%) và Memory (80%) trong mảng metrics, HPA sẽ tính toán số lượng bản sao đề xuất cho từng chỉ số độc lập và chọn giá trị lớn nhất để đảm bảo hệ thống không bị thắt cổ chai ở bất kỳ chiều tài nguyên nào.
  2. Chống hiện tượng co giãn giật cục (Anti-Flapping / Anti-Thrashing) bằng behavior.scaleDown: Khi lưu lượng truy cập biến động lên xuống theo từng đợt ngắn, nếu HPA thu hồi Pod (scaleDown) ngay lập tức khi tải vừa giảm nhẹ, chỉ 30 giây sau khi đợt sóng truy cập tiếp theo ập tới, hệ thống sẽ rơi vào tình trạng thiếu hụt Pod trầm trọng trong lúc chờ Pod mới khởi tạo. Bằng cách thiết lập behavior.scaleUp.stabilizationWindowSeconds: 0 (cho phép mở rộng tức thì khi tải tăng) kết hợp với behavior.scaleDown.stabilizationWindowSeconds: 300 và giới hạn chỉ thu hồi tối đa 1 Pod mỗi 60 giây (type: Pods, value: 1, periodSeconds: 60), chúng ta buộc Kubernetes phải quan sát mức tải trong suốt 5 phút liên tục và chỉ giảm quy mô một cách từ tốn, mượt mà.

5.2 Bảo vệ Tính Sẵn sàng Cao trong Quá trình Bảo trì Node bằng PodDisruptionBudget (PDB)

Trong khi HPA bảo vệ dịch vụ trước sự biến động của lưu lượng người dùng, thì policy/v1 PodDisruptionBudget (PDB) bảo vệ dịch vụ trước các Gián đoạn Chủ động (Voluntary Disruptions) từ phía hạ tầng vận hành — tiêu biểu nhất là khi kỹ sư hệ thống chạy lệnh kubectl drain <node-name> để rút cạn một máy chủ vật lý phục vụ bảo trì, hoặc khi Cluster Autoscaler thu hồi các máy ảo dư thừa.

Nếu không có PodDisruptionBudget, và chẳng may tại thời điểm chạy lệnh kubectl drain có nhiều Pod của order-service đang nằm trên Node được rút cạn trong khi các Node còn lại đang tạm thời thiếu tài nguyên để khởi tạo Pod thay thế ngay lập tức, lệnh drain vẫn sẽ xóa các Pod cũ và làm dịch vụ bị giảm năng lực đột ngột hoặc mất khả năng phục vụ hoàn toàn. Khi khai báo đối tượng PodDisruptionBudget với thông số minAvailable: 2 khớp với nhãn của order-service, Eviction API của Kubernetes sẽ chặn đứng lệnh trục xuất Pod trên Node đang bảo trì nếu tổng số Pod ở trạng thái Ready của dịch vụ trên toàn cụm đang nhỏ hơn hoặc bằng 2, buộc quá trình rút cạn Node phải chờ cho đến khi Pod thay thế trên Node mới khởi động xong và đạt trạng thái Ready = True.


6. Kiểm thử Triển khai Thực tế với kubectl và Chẩn đoán Sự cố Vận hành

Sau khi hoàn thiện toàn bộ mã nguồn dịch vụ và 3 tệp cấu hình Manifest (order-deployment.yaml, order-service-ingress.yaml, order-hpa-pdb.yaml), bước tiếp theo là áp dụng cấu hình lên cụm Kubernetes và thiết lập quy trình xác minh tự động cũng như sổ tay xử lý sự cố (SRE Runbook) cho đội ngũ vận hành.

6.1 Quy trình Áp dụng Manifest, Giám sát Rolling Update và Kiểm tra Trạng thái Endpoints/HPA

Trong quy trình làm việc chuẩn GitOps / DevSecOps, việc biên dịch ảnh Container, quét lỗ hổng bảo mật bằng Trivy và áp dụng các tệp Manifest lên cụm Kubernetes không nên thực hiện thủ công từ máy tính cá nhân của lập trình viên mà cần được tích hợp vào đường ống tự động hóa theo hướng dẫn tại bài viết Xây dựng CI/CD Pipeline tự động triển khai với GitHub Actions và Docker.

Tại bước triển khai và kiểm chứng sau phát hành (Post-Deployment Verification), kịch bản cần kiểm tra tuần tự 4 dấu hiệu sức khỏe cốt lõi:

  1. Lệnh kubectl rollout status deployment/order-service -n production --timeout=120s trả về thông báo xác nhận toàn bộ các bản sao đã cập nhật thành công (successfully rolled out).
  2. Đối tượng EndpointSlice của order-service hiển thị đầy đủ danh sách địa chỉ IP nội bộ của cả 3 Pod với cờ READY=true, chứng tỏ startupProbe và readinessProbe đều đã vượt qua.
  3. Đối tượng HorizontalPodAutoscaler đọc được chỉ số CPU/Memory thực tế từ Metrics Server (ví dụ 32%/70%) thay vì báo <unknown>/70%.
  4. Đối tượng PodDisruptionBudget ghi nhận ALLOWED DISRUPTIONS lớn hơn hoặc bằng 1, sẵn sàng cho các hoạt động bảo trì Node an toàn.

Khối cấu hình dưới đây cung cấp trọn vẹn tệp k8s/order-hpa-pdb.yaml cùng các lệnh kiểm chứng thực tế verify-k8s.sh và kết quả đầu ra chuẩn trên cụm Kubernetes:

# File: k8s/order-hpa-pdb.yaml
# Kubernetes v1.29+ | HorizontalPodAutoscaler v2 & PodDisruptionBudget v1 Manifest
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
  namespace: production
  labels:
    app.kubernetes.io/name: order-service
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      selectPolicy: Max
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30
        - type: Pods
          value: 3
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
      selectPolicy: Min
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: order-service-pdb
  namespace: production
  labels:
    app.kubernetes.io/name: order-service
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: order-service

# -----------------------------------------------------------------------------
# File: verify-k8s.sh (CLI Rollout & Verification Commands + Expected Output)
# -----------------------------------------------------------------------------
# kubectl create namespace production --dry-run=client -o yaml | kubectl apply -f -
# kubectl apply -f k8s/order-service-ingress.yaml
# kubectl apply -f k8s/order-deployment.yaml
# kubectl apply -f k8s/order-hpa-pdb.yaml
# kubectl rollout status deployment/order-service -n production --timeout=120s
#
# # Expected Output:
# # Waiting for deployment "order-service" rollout to finish: 1 old replicas are pending termination
# # deployment "order-service" successfully rolled out
#
# kubectl get pods,svc,endpointslices,ingress,hpa,pdb -n production -o wide
#
# # Expected Output:
# # NAME                                 READY   STATUS    RESTARTS   AGE   IP            NODE
# # pod/order-service-6b9d8f7c4d-4k2lm   1/1     Running   0          95s   10.244.1.42   k8s-worker-01
# # pod/order-service-6b9d8f7c4d-8x9pq   1/1     Running   0          80s   10.244.2.19   k8s-worker-02
# # pod/order-service-6b9d8f7c4d-m3v7n   1/1     Running   0          65s   10.244.3.51   k8s-worker-03
# #
# # NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# # service/order-service   ClusterIP   10.96.142.108   <none>        80/TCP    2m
# #
# # NAME                                                 ADDRESSTYPE   PORTS   ENDPOINTS                               AGE
# # endpointslice.discovery.k8s.io/order-service-7x8k2   IPv4          8080    10.244.1.42,10.244.2.19,10.244.3.51     2m
# #
# # NAME                                              CLASS   HOSTS             ADDRESS         PORTS     AGE
# # ingress.networking.k8s.io/ecommerce-api-gateway   nginx   api.infoteck.vn   203.0.113.150   80, 443   2m
# #
# # NAME                                                    REFERENCE                  TARGETS              MINPODS   MAXPODS   REPLICAS   AGE
# # horizontalpodautoscaler.autoscaling/order-service-hpa   Deployment/order-service   cpu: 28%/70%, mem: 41%/80%   3         12        3          2m
# #
# # NAME                                            MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
# # poddisruptionbudget.policy/order-service-pdb    2               N/A               1                     2m

6.2 Chẩn đoán và Xử lý Sự cố OOMKilled (Exit Code 137), ImagePullBackOff và Lỗi 502 Bad Gateway trên Ingress

Ngay cả khi hệ thống đã đi vào hoạt động ổn định, kỹ sư SRE vẫn cần nắm vững phương pháp chẩn đoán gốc rễ cho 3 sự cố vận hành kinh điển nhất trên Kubernetes thông qua việc kết hợp công cụ dòng lệnh kubectl và nền tảng giám sát tập trung (xem hướng dẫn thiết lập chi tiết tại bài viết Xây dựng hệ thống Monitoring toàn diện với Prometheus và Grafana):

  1. Sự cố Pod bị tiêu diệt với trạng thái OOMKilled (Exit Code 137):
    • Nguyên nhân kỹ thuật: Khi dung lượng bộ nhớ thực tế (Working Set Memory container_memory_working_set_bytes) của tiến trình bên trong Container vượt quá giới hạn cứng resources.limits.memory (512Mi), trình quản lý bộ nhớ của nhân Linux (cgroups v2 OOM Killer) sẽ gửi tín hiệu SIGKILL (Signal 9) để tiêu diệt tiến trình ngay lập tức (128 + 9 = Exit Code 137).
    • Quy trình chẩn đoán và khắc phục: Chạy lệnh kubectl describe pod <pod-name> -n production để kiểm tra mục Last State: Terminated (Reason: OOMKilled, Exit Code: 137) và chạy kubectl logs <pod-name> -n production --previous để đọc các dòng nhật ký cuối cùng trước khi tiến trình bị giết. Đối với ứng dụng viết bằng Go (từ phiên bản Go 1.19+), bộ thu gom rác (Garbage Collector) mặc định không biết giới hạn bộ nhớ của Container mà chỉ nhìn thấy tổng bộ nhớ vật lý của cả máy chủ Worker Node, dẫn đến việc trì hoãn thu gom rác cho đến khi chạm trần 512Mi. Giải pháp triệt để là bổ sung biến môi trường GOMEMLIMIT=440MiB (khoảng 85% đến 90% của limits.memory) vào ConfigMap để bộ thu gom rác của Go chủ động giải phóng bộ nhớ trước khi chạm ngưỡng OOM của cgroups.
  2. Sự cố ImagePullBackOff / ErrImagePull:
    • Nguyên nhân kỹ thuật: Kubelet trên Worker Node không thể tải ảnh Container từ Registry nội bộ (registry.infoteck.vn) do một trong ba lý do: nhãn phiên bản (tag) không tồn tại, thiếu thông tin xác thực imagePullSecrets trong Deployment hoặc ServiceAccount, hoặc Worker Node bị chặn kết nối mạng tới cổng HTTPS của Registry.
    • Quy trình chẩn đoán và khắc phục: Kiểm tra phần Events ở cuối kết quả lệnh kubectl describe pod <pod-name> -n production. Nếu thông báo lỗi là unauthorized: authentication required, cần tạo đối tượng Secret kiểu kubernetes.io/dockerconfigjson và gắn vào thuộc tính spec.template.spec.imagePullSecrets của Deployment.
  3. Sự cố xuất hiện lỗi 502 Bad Gateway ngắt quãng trên Nginx Ingress khi tải cao hoặc khi triển khai:
    • Nguyên nhân kỹ thuật: Ngoài nguyên nhân thiếu móc lifecycle.preStop đã giải quyết ở Mục 1.3 và 3.2, lỗi 502 Bad Gateway khi tải cao còn xảy ra do xung đột thời gian chờ kết nối bền vững (Keep-Alive Timeout Race Condition) giữa Nginx Ingress Controller và HTTP Server của ứng dụng. Nếu Nginx Ingress tái sử dụng một kết nối TCP Keep-Alive để gửi yêu cầu mới đúng vào mili-giây mà HTTP Server của Go vừa đóng kết nối đó do hết hạn IdleTimeout, Nginx sẽ nhận về gói tin TCP RST và trả lỗi 502.
    • Quy trình khắc phục: Luôn cấu hình IdleTimeout trên HTTP Server của ứng dụng (ví dụ 60s như trong main.go) lớn hơn thông số upstream-keepalive-timeout của Nginx Ingress Controller (mặc định là 30s hoặc 60s, nên đặt IdleTimeout ở tầng ứng dụng là 75s nếu Nginx để 60s) để đảm bảo phía đóng kết nối nhàn rỗi chủ động luôn là Reverse Proxy chứ không phải dịch vụ phía sau.

7. Tổng kết & Tài liệu tham khảo (References)

Xây dựng và vận hành kiến trúc microservices với docker và kubernetes ở cấp độ Production đòi hỏi sự ăn khớp chính xác giữa kiến trúc phần mềm bên trong Container và các cơ chế điều phối của cụm hạ tầng. Khi ứng dụng được đóng gói phi đặc quyền (Non-Root UID 65532), tách biệt rõ ràng điểm cuối /healthz và /readyz, xử lý trọn vẹn tín hiệu SIGTERM kết hợp cùng lifecycle.preStop, HorizontalPodAutoscaler v2 và PodDisruptionBudget, hệ thống sẽ đạt được khả năng phát hành liên tục không gián đoạn (Zero-Downtime Deployment) và tự động co giãn bền vững trước mọi biến động lưu lượng.

7.1 Checklist Triển khai Microservices lên Kubernetes Production

Bảng kiểm dưới đây tổng hợp các tiêu chuẩn kỹ thuật bắt buộc cần nghiệm thu trước khi đưa bất kỳ dịch vụ Microservice nào lên cụm Kubernetes Production:

Hạng mục Kiểm soát Cấu hình Kỹ thuật Bắt buộc Mục tiêu Vận hành & Bảo mật
Đóng gói Container & Quyền thực thi Multi-stage build, biên dịch tĩnh (CGO_ENABLED=0), chạy dưới USER 65532:65532, không gắn thẻ :latest mà khóa cứng phiên bản (v1.4.2). Loại bỏ rủi ro leo thang đặc quyền root trên Worker Node và đảm bảo tính bất biến giữa các môi trường.
Ngữ cảnh Bảo mật Pod (securityContext) runAsNonRoot: true, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], automountServiceAccountToken: false. Tuân thủ chuẩn Kubernetes Pod Security Standards (Restricted), ngăn chặn ghi mã độc vào hệ thống tệp Container.
Bộ 3 Kiểm tra Sức khỏe (Probes) Khai báo đầy đủ startupProbe (/healthz), readinessProbe (/readyz) và livenessProbe (/healthz không phụ thuộc DB ngoài). Ngăn chặn vòng lặp CrashLoopBackOff dây chuyền và chỉ định tuyến lưu lượng vào các Pod thực sự sẵn sàng.
Đóng kết nối Êm ái (Zero-Downtime Shutdown) Kết hợp lifecycle.preStop (sleep 5), terminationGracePeriodSeconds: 30 và trình bắt tín hiệu SIGTERM gọi srv.Shutdown(ctx). Triệt tiêu hoàn toàn lỗi HTTP 502 Bad Gateway trong quá trình cập nhật phiên bản mới (Rolling Update).
Quản lý Tài nguyên & QoS Khai báo tường minh resources.requests và resources.limits cho cả CPU và Memory, kèm biến GOMEMLIMIT tương ứng với limits.memory. Đảm bảo bộ lập lịch kube-scheduler phân bổ đúng năng lực Node và ngăn ngừa lỗi OOMKilled (Exit Code 137).
Tự động Co giãn & Chống Gián đoạn Cấu hình autoscaling/v2 HPA có behavior.scaleDown.stabilizationWindowSeconds: 300, policy/v1 PDB (minAvailable: 2) và topologySpreadConstraints. Mở rộng tức thì khi tải tăng, chống co giãn giật cục và duy trì tính sẵn sàng cao khi bảo trì rút cạn Worker Node.

7.2 Tài liệu tham khảo & Nguồn đối chiếu Chính thống

  1. Kubernetes Official Documentation: Configure Liveness, Readiness and Startup Probes. Truy cập tại: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
  2. Kubernetes Official Documentation: Pod Lifecycle — Termination of Pods (preStop Hook, SIGTERM & terminationGracePeriodSeconds). Truy cập tại: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination
  3. Kubernetes Official Documentation: Ingress & Ingress Controllers (networking.k8s.io/v1). Truy cập tại: https://kubernetes.io/docs/concepts/services-networking/ingress/
  4. Kubernetes Official Documentation: Horizontal Pod Autoscaling (autoscaling/v2 Scaling Policies & Stabilization Windows). Truy cập tại: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
  5. Kubernetes Official Documentation: Specifying a Disruption Budget for your Application (policy/v1 PodDisruptionBudget). Truy cập tại: https://kubernetes.io/docs/tasks/run-application/configure-pdb/