DevOpsDocker Swarm và k3s, phần 6/8

Swarm hay k3s cho trụ sở: ánh xạ stack BanHang.Api và chuyển không gián đoạn

kompose dịch stack BanHang.Api thành manifest qua kubeconform, nhưng app không khởi động và mọi thiết lập an toàn của ba bài Swarm rơi mất. Bảng tiêu chí có nguồn, ánh xạ 43 khóa của stack có công cụ C# kiểm, và một kế hoạch chuyển từng máy mà hai cụm cùng phục vụ sau load balancer.

Mục lục
  1. 1. Vấn đề: kompose dịch xong, kubeconform qua, app không chạy
  2. 2. Mục đích: quyết định có nguồn, ánh xạ không sót, chuyển không dừng
  3. 3. Cơ sở lý thuyết: cùng lõi Raft, khác ở những việc Swarm làm ngầm
  4. 4. Cách giải quyết: chuyển khi chấp nhận tự khai báo, ánh xạ có công cụ, chuyển từng máy
  5. 5. Cách cài đặt: k3s ba server, manifest sửa tay, công cụ đối chiếu
  6. 6. Chứng minh: 11 dòng sai và thiếu ở bản kompose, 0 ở bản sửa tay
  7. 7. Kết luận
  8. Đọc tiếp
  9. Nguồn

Đọc nhanh

  • Vấn đề: Manifest do kompose 1.38.0 dịch từ stack Swarm qua kubeconform, nhưng sai 6 chỗ và thiếu 5, trong đó secret gắn sai tên file làm BanHang.Api không khởi động.
  • Cách giải: Chuyển trụ sở sang k3s ba server khi chấp nhận tự khai báo bốn việc Swarm làm sẵn, ánh xạ từng khóa bằng tay có công cụ đối chiếu, rồi chuyển từng máy với hai cụm cùng sau load balancer.
  • Chứng minh: Chưa dựng cụm thật; 43/43 khóa của stack có luật, manifest sửa tay 0 sai 0 thiếu và qua kubeconform, app .NET thật đọc được secret theo bố cục mới.
  • Trong .NET: KeyPerFile cần file /run/secrets/ConnectionStrings__BanHang, Downward API điền BanHang__Node, và SoSwarmK3s.cs viết bằng YamlDotNet chặn ánh xạ sót trong CI.

1. Vấn đề: kompose dịch xong, kubeconform qua, app không chạy

Sau bài 8, 12 cửa hàng chạy k3s và nhận bản mới qua Flux. Trụ sở vẫn chạy BanHang.Api trên cụm Swarm năm máy của bài 1: hai nền, hai bộ công cụ kiểm cấu hình, hai cách triển khai. Ngày 02/10, một kỹ sư thử gom về một nền (diễn biến minh họa, output ở mục 6 là thật): chạy kompose 1.38.0 trên banhang-api.stack.yml, được một Deployment và một Service, và kubeconform báo 2/2 tài liệu hợp lệ.

Đọc kỹ thì khác. Secret được gắn thành file /run/secrets/banhang-sql-v1, trong khi KeyPerFile của bài 3 cần file ConnectionStrings__BanHang: chạy thử với bố cục đó, API thoát ngay vì thiếu chuỗi kết nối. BanHang__Node mang nguyên chuỗi {{.Node.Hostname}}. Service là ClusterIP, nên load balancer phía trước không gọi vào được.

Sửa riêng mấy chỗ đó cũng chưa đủ. Manifest không có readiness, không có preStop, không có gì thay cho delay: 90s và failure_action: rollback. Đó là đúng các lỗi ba bài Swarm đã sửa: request vào task chưa mở cổng, lệnh bị cắt lúc dừng, bản treo lên cả 4 replica. Câu hỏi của bài: trụ sở có nên chuyển sang k3s, và nếu chuyển thì giữ đủ thiết lập và không dừng bán hàng thế nào.

2. Mục đích: quyết định có nguồn, ánh xạ không sót, chuyển không dừng

  • Bảng tiêu chí Swarm và k3s, mỗi dòng ghi nguồn; dòng chỉ dựa trên tài liệu, chưa đo, ghi rõ.
  • 43/43 khóa lá trong kết quả docker stack config có luật ánh xạ. Manifest sửa tay: 0 dòng sai, 0 dòng thiếu; bản của kompose để so.
  • Manifest qua kubeconform -strict với Kubernetes 1.36.5, và BanHang.Api thật đọc được secret theo bố cục manifest gắn vào.
  • Kế hoạch chuyển: ở mọi bước, ít nhất một cụm còn chịu được một máy hỏng, và load balancer không phải đổi danh sách máy.
  • Ngoài phạm vi: dựng cụm thật (bài này chưa dựng), độ sẵn sàng của sql01, công cụ rollout tự động như Argo Rollouts hay Flagger.

3. Cơ sở lý thuyết: cùng lõi Raft, khác ở những việc Swarm làm ngầm

Lõi giống nhau

Manager của Swarm và etcd nhúng của k3s cùng đồng thuận bằng Raft: N máy cần ⌊N/2⌋ + 1 máy để ghi, nên 3 máy chịu được 1 máy hỏng, như bảng quorum ở bài 1. Tài liệu k3s yêu cầu số server lẻ khi dùng etcd nhúng; SQLite qua Kine chỉ dùng được với một server (bài 4). Server của k3s mặc định cũng chạy workload, như manager của Swarm. Chuyển nền không làm cụm chịu lỗi tốt hơn.

Năm việc Swarm làm sẵn, Kubernetes bắt khai báo

Việc Swarm Kubernetes
Kiểm sức khỏe Một healthcheck vừa chặn task chưa mở cổng khỏi mesh, vừa giết task treo Startup, liveness, readiness là ba probe riêng
Dừng task Gỡ khỏi mesh, chờ 2 s, rồi SIGTERM Gỡ endpoint và chạy preStop song song; SIGTERM sau preStop
Bản mới lỗi failure_action: rollback tự quay lại Chỉ báo ProgressDeadlineExceeded; quay lại là việc của công cụ khác
Trải replica Scheduler chọn node ít task của service nhất Mặc định maxSkew: 3, ScheduleAnyway: không bảo đảm mỗi node một pod
Lưu secret Raft log mã hóa sẵn; tên có _, target là tên file Mã hóa khi lưu chỉ khi bật --secrets-encryption; tên chỉ chữ thường, số, -, .

Dòng thứ hai quyết định cách dịch RutChoGiay: "0". Trên Swarm, 0 đúng vì Swarm đã chờ 2 giây trước SIGTERM. Trên Kubernetes, SIGTERM có thể tới khi endpoint chưa được gỡ (Triển khai không gián đoạn), nên preStop phải làm bước chờ. Grace period tính từ trước preStop, nên stop_grace_period 20 giây trở thành terminationGracePeriodSeconds 30 giây: lớn hơn preStop 5 giây cộng ShutdownTimeout 15 giây cộng 1 giây của Kestrel.

sequenceDiagram
  participant SW as Swarm
  participant K as Kubernetes
  participant N as Tiến trình .NET
  Note over SW,N: Swarm: tuần tự
  SW->>SW: gỡ task khỏi routing mesh
  SW->>N: chờ 2 s, rồi SIGTERM
  Note over K,N: Kubernetes: song song
  K->>K: gỡ endpoint khỏi Service
  K->>N: cùng lúc chạy preStop sleep 5 s
  K->>N: preStop xong mới SIGTERM, grace 30 s tính từ đầu

Dòng thứ ba quyết định cách dịch delay, monitor và rollback. Với maxSurge: 1, maxUnavailable: 0, Deployment chỉ gỡ pod cũ khi pod mới "available", tức ready liên tục trong minReadySeconds. Đặt minReadySeconds: 90 thì một bản treo sau 20 giây không bao giờ available: rollout dừng ở pod mới đầu tiên, 4 pod cũ vẫn phục vụ. Theo tài liệu Deployment, Kubernetes không làm gì thêm ngoài báo trạng thái khi quá progressDeadlineSeconds; quay lại là git revert để Flux áp bản cũ.

kompose dịch theo chữ, không theo tác dụng: start_period thành initialDelaySeconds của liveness, secret thành file mang tên secret, chuỗi mẫu {{...}} giữ nguyên. Vì vậy manifest hợp lệ với schema mà sai về hành vi.

4. Cách giải quyết: chuyển khi chấp nhận tự khai báo, ánh xạ có công cụ, chuyển từng máy

Có nên chuyển

Tiêu chí Swarm k3s ba server Nguồn
Chịu một máy hỏng 3 manager 3 server etcd nhúng Bài 1; tài liệu k3s, chưa đo
Bản lỗi tự quay lại Có Không; chặn bằng minReadySeconds, quay lại bằng Git Bài 2; tài liệu Kubernetes, chưa đo
Tách sống và sẵn sàng Không Có Bài 1, bài 5
Thêm pod theo tải Không HPA Bài 5
Triển khai từ Git, kiểm manifest docker stack deploy, KiemStack.cs riêng Flux, kubeconform, cùng chuỗi CI với 12 cửa hàng Bài 2, bài 8
RAM của control plane Tài liệu không có số 1606 M cho server etcd kèm workload, k3s v1.26.5 Bài 4, trang Resource Profiling; chưa đo

Bài chọn chuyển, vì ba dòng giữa: readiness tách khỏi liveness, HPA, và một chuỗi Git, Flux, kubeconform cho cả 13 cụm. Cái giá là bốn việc ở mục 3 phải viết ra thành manifest và cờ của k3s, và rollback tự động thành một bước git revert. Đội không muốn đổi thì Swarm vẫn đủ cho yêu cầu chịu lỗi của bài 1.

Chuyển thế nào

Cách Ưu Nhược Khi nào dùng
Dừng Swarm, cài k3s trên năm máy Đơn giản Dừng bán hàng online suốt lúc cài Không dùng
Dựng k3s trên năm máy mới, đổi load balancer Hai cụm đều đủ ba nút điều khiển Gấp đôi số máy trong lúc chuyển Có sẵn máy
Chuyển từng máy, hai cụm cùng sau load balancer Không cần máy mới Có lúc một cụm không chịu được lỗi Năm máy hiện có

Bài chọn cách thứ ba. Load balancer của bài 1 trỏ vào cổng 8080 của cả năm máy. Máy thuộc Swarm trả lời qua routing mesh, máy thuộc k3s qua ServiceLB, một DaemonSet giữ hostPort 8080 trên mọi node. Load balancer kiểm /healthz/ready như ở Triển khai không gián đoạn thì máy đang đổi nền tự rớt khỏi pool rồi tự vào lại, nên danh sách máy không đổi. Cột chịu lỗi tính bằng ⌊(N − 1)/2⌋ trên số manager hoặc server:

Bước Việc Swarm: manager, chịu lỗi k3s: server, chịu lỗi Ai phục vụ
0 Hiện tại 3, chịu 1 Chưa có Swarm
1 sw-wkr-1 rời Swarm, cài server đầu tiên --cluster-init 3, chịu 1 1, chịu 0 Swarm
2 sw-wkr-2 thành server thứ hai; tạo Secret, áp manifest 3, chịu 1 2, chịu 0 Cả hai
3 sw-mgr-3 demote, rời Swarm, thành server thứ ba 2, chịu 0 3, chịu 1 Cả hai
4 sw-mgr-2, sw-mgr-1 rời Swarm, thành agent Hết 3, chịu 1 k3s

Ở mọi bước, một cụm còn chịu được một máy hỏng. Swarm mất quorum thì task đang chạy vẫn phục vụ, chỉ không ai dời chúng (bài 1); với k3s, bài này chưa kiểm hành vi tương ứng. Mỗi máy rời Swarm theo đúng vòng drain của bài 1. Bước 3 và 4 nên làm liền nhau, vì Swarm chỉ còn hai manager.

5. Cách cài đặt: k3s ba server, manifest sửa tay, công cụ đối chiếu

Phiên bản: k3s v1.36.5+k3s1 (Kubernetes v1.36.5), Docker Engine 29.8.2 như bài 1, kompose 1.38.0, kustomize v5.8.2, kubeconform v0.8.0, .NET 10.0.12, YamlDotNet 18.1.0. Lệnh trên máy, chưa chạy trong bài; token và giá trị secret là giả:

# sw-wkr-1, sau khi drain và docker swarm leave
printf 'node-name: sw-wkr-1\ncluster-init: true\nsecrets-encryption: true\ntoken: GIA-token-tru-so\n' | sudo tee /etc/rancher/k3s/config.yaml
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.36.5+k3s1 sh -s - server
# sw-wkr-2, sw-mgr-3: server; sw-mgr-2, sw-mgr-1: "agent --server https://sw-wkr-1:6443"
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.36.5+k3s1 K3S_TOKEN=GIA-token-tru-so sh -s - server --server https://sw-wkr-1:6443 --secrets-encryption
sudo k3s kubectl create secret generic banhang-sql-v1 -n banhang --from-file=connection-string=./banhang-sql.txt
sudo k3s kubectl create secret generic bhpay-api-key-v1 -n banhang --from-file=api-key=./bhpay-api-key.txt

Theo tài liệu k3s, --secrets-encryption thuộc nhóm cờ phải giống nhau trên mọi server. Phần manifest khác Swarm nhiều nhất:

spec:
  replicas: 4
  minReadySeconds: 90                  # delay, monitor 90 s
  strategy: { rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } }   # parallelism 1, start-first
  template:
    spec:
      terminationGracePeriodSeconds: 30
      topologySpreadConstraints:
        - { maxSkew: 1, topologyKey: kubernetes.io/hostname, whenUnsatisfiable: DoNotSchedule,
            labelSelector: { matchLabels: { app.kubernetes.io/name: banhang-api } } }
      containers:
        - name: api
          env:
            - name: BanHang__Node                     # {{.Node.Hostname}}
              valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
          startupProbe:   { httpGet: { path: /healthz/live, port: http }, periodSeconds: 1, timeoutSeconds: 2, failureThreshold: 60 }
          livenessProbe:  { httpGet: { path: /healthz/live, port: http }, periodSeconds: 5, timeoutSeconds: 3, failureThreshold: 3 }
          readinessProbe: { httpGet: { path: /healthz/ready, port: http }, periodSeconds: 5, timeoutSeconds: 2, failureThreshold: 2 }
          lifecycle: { preStop: { sleep: { seconds: 5 } } }
          volumeMounts: [{ name: bi-mat, mountPath: /run/secrets, readOnly: true }]
      volumes:
        - name: bi-mat                                 # hai Secret chung một thư mục
          projected: { sources: [
            { secret: { name: banhang-sql-v1, items: [{ key: connection-string, path: ConnectionStrings__BanHang }] } },
            { secret: { name: bhpay-api-key-v1, items: [{ key: api-key, path: BHPay__ApiKey }] } } ] }

Hai Secret phải nằm chung /run/secrets như trên Swarm, mà hai volume không gắn được cùng một đường dẫn, nên dùng volume projected. path của từng item là tên khóa cấu hình, nên AddKeyPerFile("/run/secrets") và GetConnectionString("BanHang") giữ nguyên. Toàn bộ manifest:

tru-so/: namespace, deployment, service, pdb, kustomizationYAML · 152 dòng
# ===== kustomization.yaml
# Secret banhang-sql-v1 (khóa connection-string) và bhpay-api-key-v1 (khóa api-key) tạo ngoài Git.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - namespace.yaml
  - deployment.yaml
  - service.yaml
  - pdb.yaml
# ===== namespace.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  name: banhang                        # tên stack của Swarm
# ===== deployment.yaml
---
# BanHang.Api trên k3s HA của trụ sở. Ánh xạ từ banhang-api.stack.yml (service api của stack banhang).
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: banhang
  labels:
    app.kubernetes.io/name: banhang-api
spec:
  replicas: 4                          # deploy.replicas
  minReadySeconds: 90                  # update_config.delay và monitor 90 s
  progressDeadlineSeconds: 600         # quá hạn chỉ báo ProgressDeadlineExceeded, không tự rollback
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                      # parallelism 1, order start-first
      maxUnavailable: 0
  selector:
    matchLabels:
      app.kubernetes.io/name: banhang-api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: banhang-api
    spec:
      automountServiceAccountToken: false   # API không gọi Kubernetes API
      terminationGracePeriodSeconds: 30     # > preStop 5 s + ShutdownTimeout 15 s + 1 s
      topologySpreadConstraints:            # Swarm tự trải 4 replica trên 5 node; Kubernetes phải khai báo
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: banhang-api
      containers:
        - name: api
          image: registry.banhang.local/banhang/api:1.5.0
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production
            - name: BanHang__Node             # Swarm: {{.Node.Hostname}}
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: BanHang__Task             # Swarm: {{.Task.Name}}
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: RutChoGiay                # preStop chờ thay cho 2 s Swarm chờ trước SIGTERM
              value: "0"
          resources:
            requests:
              cpu: 500m                       # reservations.cpus 0.5
              memory: 512Mi                   # reservations.memory 512M
            limits:
              cpu: "2"
              memory: 1Gi                     # limits.memory 1024M
          startupProbe:                       # start_period 60 s, start_interval 1 s
            httpGet:
              path: /healthz/live
              port: http
            periodSeconds: 1
            timeoutSeconds: 2
            failureThreshold: 60
          livenessProbe:                      # healthcheck: interval 5 s, timeout 3 s, retries 3
            httpGet:
              path: /healthz/live
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:                     # Swarm không có; như bài 4
            httpGet:
              path: /healthz/ready
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
          lifecycle:
            preStop:
              sleep:
                seconds: 5
          volumeMounts:
            - name: bi-mat
              mountPath: /run/secrets
              readOnly: true
      volumes:
        - name: bi-mat                        # hai Secret chung một thư mục như /run/secrets của Swarm
          projected:
            sources:
              - secret:
                  name: banhang-sql-v1
                  items:
                    - key: connection-string
                      path: ConnectionStrings__BanHang
              - secret:
                  name: bhpay-api-key-v1
                  items:
                    - key: api-key
                      path: BHPay__ApiKey
# ===== service.yaml
---
# ServiceLB của k3s giữ cổng 8080 trên mọi node, như routing mesh của Swarm.
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: banhang
  labels:
    app.kubernetes.io/name: banhang-api
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: banhang-api
  ports:
    - name: http
      protocol: TCP
      port: 8080
      targetPort: http
# ===== pdb.yaml
---
# kubectl drain khi vá máy (docker node update --availability drain ở bài 1): giữ ít nhất 3/4 replica.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api
  namespace: banhang
spec:
  minAvailable: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: banhang-api

SoSwarmK3s.cs trải kết quả docker stack config thành 43 khóa lá, như services.api.deploy.update_config.monitor. Mỗi luật nhận một nhóm khóa bằng regex và đọc chỗ tương ứng trong manifest; khóa không luật nào nhận là CHƯA ÁNH XẠ. Ba luật cuối kiểm việc Swarm làm ngầm. Mã thoát là số dòng sai, thiếu và chưa ánh xạ. Một luật:

(@"^services\.api\.deploy\.update_config\.(delay|monitor)$", "delay, monitor", () =>
{
    double can = Math.Max(double.Parse(Giay(S("deploy.update_config.delay"))), double.Parse(Giay(S("deploy.update_config.monitor"))));
    string? m = V(dep, "spec", "minReadySeconds");
    return m is null ? ("THIẾU", "không có minReadySeconds: bản treo lên cả 4 pod")
        : So(m, 0) >= can ? ("đổi dạng", $"minReadySeconds {m}") : ("SAI", $"minReadySeconds {m} < {can}");
}),

Chạy dotnet run SoSwarmK3s.cs -- banhang-api.render.yml tru-so trong CI cùng kubeconform.

SoSwarmK3s.cs: so 43 khóa của stack Swarm với manifest KubernetesC# · 203 dòng
#:package YamlDotNet@18.1.0
// So từng thiết lập của stack Swarm (kết quả docker stack config) với manifest Kubernetes.
// Mỗi khóa lá của service phải được một luật nhận; luật nói manifest giữ thiết lập đó ra sao.
// dotnet run SoSwarmK3s.cs -- banhang-api.render.yml <manifest.yaml | thư mục manifest>
using System.Globalization;
using System.Text.RegularExpressions;
using YamlDotNet.RepresentationModel;

var swarm = new Dictionary<string, string>();
Phang(DocYaml(args[0])[0], "", swarm);
foreach (var k in swarm.Keys.Where(k => !k.StartsWith("services.") && !k.StartsWith("secrets.")).ToList()) swarm.Remove(k);   // bỏ "version"
var k8s = (Directory.Exists(args[1]) ? Directory.GetFiles(args[1], "*.yaml") : [args[1]]).SelectMany(DocYaml)
    .OfType<YamlMappingNode>().ToList();
var dep = k8s.FirstOrDefault(d => V(d, "kind") == "Deployment");
var pod = N(dep, "spec", "template", "spec");
var c = (N(pod, "containers") as YamlSequenceNode)?.Children.FirstOrDefault();
var svc = k8s.FirstOrDefault(d => V(d, "kind") == "Service");
var pdb = k8s.FirstOrDefault(d => V(d, "kind") == "PodDisruptionBudget");
string S(string p) => swarm.GetValueOrDefault("services.api." + p, "");
const double ShutdownTimeout = 15, KestrelSauAbort = 1, GossipSwarm = 2;   // BanHang.Api.cs, bài 2

// Trạng thái: khớp | đổi dạng (trường khác, cùng tác dụng) | thêm mới (Swarm làm ngầm) |
//             thay bằng quy trình | không tương đương | SAI | THIẾU
var luat = new List<(string Mau, string Ten, Func<(string, string)> Kiem)>
{
    (@"^services\.api\.image$", "image", () => Bang(V(c, "image"), S("image"))),
    (@"^services\.api\.deploy\.(mode|replicas)$", "replicas", () => Bang(V(dep, "spec", "replicas"), S("deploy.replicas"))),
    (@"^services\.api\.deploy\.resources\.limits\.cpus$", "limits.cpus", () => Cpu(V(c, "resources", "limits", "cpu"), S("deploy.resources.limits.cpus"))),
    (@"^services\.api\.deploy\.resources\.limits\.memory$", "limits.memory", () => Ram(V(c, "resources", "limits", "memory"), S("deploy.resources.limits.memory"))),
    (@"^services\.api\.deploy\.resources\.reservations\.cpus$", "reservations.cpus", () => Cpu(V(c, "resources", "requests", "cpu"), S("deploy.resources.reservations.cpus"))),
    (@"^services\.api\.deploy\.resources\.reservations\.memory$", "reservations.memory", () => Ram(V(c, "resources", "requests", "memory"), S("deploy.resources.reservations.memory"))),
    (@"^services\.api\.deploy\.restart_policy\.condition$", "restart_policy.condition", () =>
        V(pod, "restartPolicy") is null or "Always" ? ("khớp", "restartPolicy Always") : ("SAI", V(pod, "restartPolicy")!)),
    (@"^services\.api\.deploy\.restart_policy\.delay$", "restart_policy.delay", () =>
        ("không tương đương", "kubelet tự lùi 10 s, 20 s, 40 s… tối đa 300 s")),
    (@"^services\.api\.environment\.ASPNETCORE_ENVIRONMENT$", "env ASPNETCORE_ENVIRONMENT", () => Bang(V(Env("ASPNETCORE_ENVIRONMENT"), "value"), S("environment.ASPNETCORE_ENVIRONMENT"))),
    (@"^services\.api\.environment\.BanHang__Node$", "env {{.Node.Hostname}}", () => TuPod("BanHang__Node", "spec.nodeName")),
    (@"^services\.api\.environment\.BanHang__Task$", "env {{.Task.Name}}", () => TuPod("BanHang__Task", "metadata.name")),
    (@"^services\.api\.environment\.RutChoGiay$", "rút trước SIGTERM", () =>
    {
        double rut = double.Parse(V(Env("RutChoGiay"), "value") ?? "0", CultureInfo.InvariantCulture), cho = PreStop();
        string k = $"RutChoGiay {rut} + preStop {cho} s";
        return rut + cho >= GossipSwarm ? ("đổi dạng", k) : ("SAI", k + $": SIGTERM tới cùng lúc gỡ endpoint, Swarm chờ {GossipSwarm} s");
    }),
    (@"^services\.api\.ports\[0\]\.target$", "ports.target", () => Bang(V(N(c, "ports") is YamlSequenceNode p ? p.Children[0] : null, "containerPort"), S("ports[0].target"))),
    (@"^services\.api\.ports\[0\]\.(published|mode|protocol)$", "routing mesh :8080", () =>
        svc is null ? ("THIẾU", "không có Service")
        : V(svc, "spec", "type") == "LoadBalancer" && V(N(svc, "spec", "ports") is YamlSequenceNode sp ? sp.Children[0] : null, "port") == S("ports[0].published")
            ? ("đổi dạng", "Service LoadBalancer :8080, ServiceLB trên mọi node")
            : ("SAI", $"Service {V(svc, "spec", "type") ?? "ClusterIP"}: không nhận request từ ngoài cụm")),
    (@"^services\.api\.healthcheck\.test\[\d\]$", "healthcheck.test", () =>
    {
        var lv = N(c, "livenessProbe");
        string url = S("healthcheck.test[2]"), duong = new Uri(url).AbsolutePath;
        if (V(lv, "httpGet", "path") == duong) return ("đổi dạng", $"livenessProbe httpGet {duong}");
        if (N(lv, "exec", "command") is YamlSequenceNode cmd && cmd.Children.Any(x => x.ToString() == url)) return ("khớp", "livenessProbe exec KiemTra");
        return ("THIẾU", "không có livenessProbe");
    }),
    (@"^services\.api\.healthcheck\.interval$", "healthcheck.interval", () => Bang(V(c, "livenessProbe", "periodSeconds"), Giay(S("healthcheck.interval")))),
    (@"^services\.api\.healthcheck\.timeout$", "healthcheck.timeout", () => Bang(V(c, "livenessProbe", "timeoutSeconds"), Giay(S("healthcheck.timeout")))),
    (@"^services\.api\.healthcheck\.retries$", "healthcheck.retries", () => Bang(V(c, "livenessProbe", "failureThreshold"), S("healthcheck.retries"))),
    (@"^services\.api\.healthcheck\.(start_period|start_interval)$", "start_period, start_interval", () =>
    {
        double han = double.Parse(Giay(S("healthcheck.start_period"))), buoc = double.Parse(Giay(S("healthcheck.start_interval")));
        var st = N(c, "startupProbe");
        if (st is not null)
        {
            double p = So(V(st, "periodSeconds"), 10), f = So(V(st, "failureThreshold"), 3);
            return p == buoc && p * f >= han ? ("đổi dạng", $"startupProbe {p} s × {f}") : ("SAI", $"startupProbe {p} s × {f}");
        }
        string? tre = V(c, "livenessProbe", "initialDelaySeconds");
        return tre is null ? ("THIẾU", "không có startupProbe") : ("SAI", $"liveness initialDelaySeconds {tre}: kiểm muộn {tre} s, không có startupProbe");
    }),
    (@"^services\.api\.stop_grace_period$", "stop_grace_period", () =>
    {
        double grace = So(V(pod, "terminationGracePeriodSeconds"), 30), cho = PreStop(), can = cho + ShutdownTimeout + KestrelSauAbort;
        string k = $"terminationGracePeriodSeconds {grace}" + (cho > 0 ? $", tính từ trước preStop {cho} s" : "");
        return grace > can ? (cho > 0 ? "đổi dạng" : "khớp", k) : ("SAI", k + $": cần hơn {can} s");
    }),
    (@"^services\.api\.deploy\.update_config\.(parallelism|order)$", "parallelism, start-first", () =>
    {
        var ru = N(dep, "spec", "strategy", "rollingUpdate");
        string k = $"maxSurge {V(ru, "maxSurge") ?? "25%"}, maxUnavailable {V(ru, "maxUnavailable") ?? "25%"}";
        return V(ru, "maxSurge") == S("deploy.update_config.parallelism") && V(ru, "maxUnavailable") == "0" ? ("đổi dạng", k) : ("SAI", k);
    }),
    (@"^services\.api\.deploy\.update_config\.(delay|monitor)$", "delay, monitor", () =>
    {
        double can = Math.Max(double.Parse(Giay(S("deploy.update_config.delay"))), double.Parse(Giay(S("deploy.update_config.monitor"))));
        string? m = V(dep, "spec", "minReadySeconds");
        return m is null ? ("THIẾU", "không có minReadySeconds: bản treo lên cả 4 pod")
            : So(m, 0) >= can ? ("đổi dạng", $"minReadySeconds {m}") : ("SAI", $"minReadySeconds {m} < {can}");
    }),
    (@"^services\.api\.deploy\.update_config\.failure_action$", "failure_action: rollback", () =>
        V(dep, "spec", "progressDeadlineSeconds") is { } h
            ? ("thay bằng quy trình", $"progressDeadlineSeconds {h} chỉ báo lỗi; quay lại bằng git revert")
            : ("THIẾU", "Deployment không tự rollback, không có hạn báo lỗi")),
    (@"^services\.api\.deploy\.rollback_config\.", "rollback_config", () =>
        ("không tương đương", "rollout undo và git revert dùng lại strategy của Deployment")),
    (@"^services\.api\.secrets\[\d\]\.(source|target)$", "secrets", () => BiMat()),
    (@"^secrets\.", "secret external", () =>
        k8s.Any(d => V(d, "kind") == "Secret") ? ("SAI", "Secret nằm trong manifest") : ("khớp", "Secret tạo ngoài Git")),
};
// Swarm làm ngầm, không có khóa trong stack nhưng bài 1 dựa vào.
var ngam = new List<(string Ten, Func<(string, string)> Kiem)>
{
    ("trải 4 replica trên 5 node", () => N(pod, "topologySpreadConstraints") is YamlSequenceNode t
        && t.Children.Any(x => V(x, "maxSkew") == "1" && V(x, "topologyKey") == "kubernetes.io/hostname" && V(x, "whenUnsatisfiable") == "DoNotSchedule")
        ? ("thêm mới", "topologySpreadConstraints maxSkew 1") : ("THIẾU", "mặc định maxSkew 3, ScheduleAnyway")),
    ("vào mesh khi healthcheck đạt", () => N(c, "readinessProbe") is not null
        ? ("thêm mới", $"readinessProbe {V(c, "readinessProbe", "httpGet", "path")}") : ("THIẾU", "pod nhận request trước khi mở cổng")),
    ("drain giữ 3/4 replica", () => pdb is not null ? ("thêm mới", $"PodDisruptionBudget minAvailable {V(pdb, "spec", "minAvailable")}") : ("THIẾU", "không có PodDisruptionBudget")),
};

var ketQua = new List<(string TrangThai, string Ten, string Swarm, string K8s)>();
var daNhan = new HashSet<string>();
foreach (var (mau, ten, kiem) in luat)
{
    var khoa = swarm.Keys.Where(k => Regex.IsMatch(k, mau)).ToList();
    if (khoa.Count == 0) continue;
    daNhan.UnionWith(khoa);
    var (tt, kv) = kiem();
    ketQua.Add((tt, ten, string.Join(", ", khoa.Select(k => swarm[k]).Distinct()), kv));
}
foreach (var (ten, kiem) in ngam) { var (tt, kv) = kiem(); ketQua.Add((tt, ten, "(ngầm)", kv)); }
var chuaNhan = swarm.Keys.Where(k => !daNhan.Contains(k)).ToList();

foreach (var r in ketQua) Console.WriteLine($"{r.TrangThai,-20}{r.Ten,-30}{Cat(r.Swarm, 28),-30}{r.K8s}");
foreach (var k in chuaNhan) Console.WriteLine($"{"CHƯA ÁNH XẠ",-20}{k}");
Console.WriteLine($"{swarm.Count} khóa lá trong stack, {daNhan.Count} được luật nhận, {ketQua.Count} dòng so: " +
    string.Join(", ", ketQua.GroupBy(r => r.TrangThai).OrderBy(g => g.Key).Select(g => $"{g.Key} {g.Count()}")));
int loi = ketQua.Count(r => r.TrangThai is "SAI" or "THIẾU") + chuaNhan.Count;
return loi;

// ---- đọc YAML ----
static List<YamlNode> DocYaml(string f) { var y = new YamlStream(); using var r = File.OpenText(f); y.Load(r); return y.Documents.Select(d => d.RootNode).ToList(); }
static void Phang(YamlNode n, string duong, Dictionary<string, string> ra)
{
    if (n is YamlMappingNode m) foreach (var (k, v) in m.Children) Phang(v, duong == "" ? k.ToString() : $"{duong}.{k}", ra);
    else if (n is YamlSequenceNode s) for (int i = 0; i < s.Children.Count; i++) Phang(s.Children[i], $"{duong}[{i}]", ra);
    else ra[duong] = n.ToString();
}
static YamlNode? N(YamlNode? n, params string[] duong)
{
    foreach (var d in duong)
        if (n is YamlMappingNode m && m.Children.TryGetValue(new YamlScalarNode(d), out var con)) n = con; else return null;
    return n;
}
static string? V(YamlNode? n, params string[] duong) => N(n, duong) is YamlScalarNode s ? s.Value : null;
YamlNode? Env(string ten) => (N(c, "env") as YamlSequenceNode)?.Children.FirstOrDefault(e => V(e, "name") == ten);
double PreStop() => So(V(c, "lifecycle", "preStop", "sleep", "seconds"), 0);

// ---- so giá trị ----
static (string, string) Bang(string? k, string s) => k is null ? ("THIẾU", "-") : k == s ? ("khớp", k) : ("SAI", k);
(string, string) TuPod(string ten, string truong)
{
    var e = Env(ten);
    if (V(e, "valueFrom", "fieldRef", "fieldPath") == truong) return ("đổi dạng", $"fieldRef {truong}");
    return e is null ? ("THIẾU", "-") : ("SAI", $"chuỗi \"{V(e, "value")}\": Kubernetes không thay mẫu của Swarm");
}
static (string, string) Cpu(string? k, string s) => k is null ? ("THIẾU", "-")
    : SoCpu(k) == double.Parse(s, CultureInfo.InvariantCulture) ? (k == s ? "khớp" : "đổi dạng", k) : ("SAI", k);
static (string, string) Ram(string? k, string s) => k is null ? ("THIẾU", "-")
    : Byte(k) == long.Parse(s) ? (k == s ? "khớp" : "đổi dạng", k) : ("SAI", k);
(string, string) BiMat()
{
    var loi = new List<string>(); var dung = new List<string>();
    foreach (int i in new[] { 0, 1 })
    {
        string nguon = S($"secrets[{i}].source"), dich = S($"secrets[{i}].target"), ten = nguon.Replace('_', '-');
        string? tep = TepBiMat(ten);
        if (tep == $"/run/secrets/{dich}") dung.Add($"{ten} -> {tep}"); else loi.Add($"{ten} -> {tep ?? "không gắn"}");
    }
    return loi.Count == 0 ? ("đổi dạng", string.Join("; ", dung)) : ("SAI", string.Join("; ", loi) + ": KeyPerFile không thấy khóa");
}
// Đường dẫn file mà Secret `ten` hiện ra trong container, theo volumeMounts, items và subPath.
string? TepBiMat(string ten)
{
    foreach (var vol in (N(pod, "volumes") as YamlSequenceNode)?.Children ?? [])
    {
        var nguon = N(vol, "secret") is { } s1 ? new[] { s1 } : (N(vol, "projected", "sources") as YamlSequenceNode)?.Children.Select(x => N(x, "secret")).OfType<YamlNode>().ToArray() ?? [];
        foreach (var s in nguon)
        {
            if ((V(s, "secretName") ?? V(s, "name")) != ten) continue;
            string item = (N(s, "items") as YamlSequenceNode)?.Children.Select(x => V(x, "path")).FirstOrDefault() ?? ten;
            var gan = (N(c, "volumeMounts") as YamlSequenceNode)?.Children.FirstOrDefault(m => V(m, "name") == V(vol, "name"));
            if (gan is null) return null;
            return V(gan, "subPath") is not null ? V(gan, "mountPath") : $"{V(gan, "mountPath")!.TrimEnd('/')}/{item}";
        }
    }
    return null;
}
static double So(string? s, double macDinh) => s is null ? macDinh : double.Parse(s, CultureInfo.InvariantCulture);
static double SoCpu(string s) => s.EndsWith('m') ? double.Parse(s[..^1], CultureInfo.InvariantCulture) / 1000 : double.Parse(s, CultureInfo.InvariantCulture);
static long Byte(string s)
{
    var m = Regex.Match(s, @"^(\d+)(Ki|Mi|Gi|k|M|G)?$");
    long x = long.Parse(m.Groups[1].Value);
    return x * m.Groups[2].Value switch { "Ki" => 1L << 10, "Mi" => 1L << 20, "Gi" => 1L << 30, "k" => 1000, "M" => 1_000_000, "G" => 1_000_000_000, _ => 1 };
}
static string Giay(string s) => Regex.Matches(s, @"([\d.]+)(h|ms|m|s)").Sum(m =>
    double.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture) * m.Groups[2].Value switch { "h" => 3600, "m" => 60, "ms" => 0.001, _ => 1 })
    .ToString(CultureInfo.InvariantCulture);
static string Cat(string s, int n) => s.Length <= n ? s : s[..(n - 1)] + "…";
ThuBoCuc.cs: BanHang.Api thật với bố cục secret của kompose và của manifest sửa tayC# · 64 dòng
#:package Microsoft.Data.SqlClient@7.1.1
#:property PublishAot=false
// ThuBoCuc.cs: chạy BanHang.Api thật với hai bố cục /run/secrets và hai cách điền BanHang__Node.
//  - kompose 1.38.0: file mang tên Secret, env giữ nguyên chuỗi mẫu của Swarm.
//  - manifest sửa tay: file mang tên khóa cấu hình, env do kubelet điền từ spec.nodeName.
// dotnet run ThuBoCuc.cs   (BanHang.Api.cs ở ./app; tạo rồi xóa database Kumeo_G2 trên LocalDB). Giá trị bí mật là giả.
using System.Diagnostics;
using Microsoft.Data.SqlClient;

const string Master = @"Server=(localdb)\MSSQLLocalDB;Database=master;Integrated Security=true;Encrypt=False";
const string ChuoiSql = @"Server=(localdb)\MSSQLLocalDB;Database=Kumeo_G2;Integrated Security=true;Encrypt=False";
const string KhoaGia = "bhpay_test_sk_GIA_v1_7Qm2Xc9Lp4";
Process.Start("dotnet", "build app/BanHang.Api.cs -o out/api -v q").WaitForExit();
string exe = Path.GetFullPath(Path.Combine("out/api", OperatingSystem.IsWindows() ? "BanHang.Api.exe" : "BanHang.Api"));
await Sql("IF DB_ID('Kumeo_G2') IS NULL CREATE DATABASE Kumeo_G2");
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };

// Bố cục file trong /run/secrets, đúng như hai manifest gắn Secret vào container.
string ThuMuc(string ten, params (string tep, string giaTri)[] tep)
{
    var d = Path.GetFullPath(Path.Combine("bimat", ten));
    if (Directory.Exists(d)) Directory.Delete(d, true);
    Directory.CreateDirectory(d);
    foreach (var (t, v) in tep) File.WriteAllText(Path.Combine(d, t), v);   // như kubelet: không thêm xuống dòng
    return d;
}
var boCuc = new (string ten, string thuMuc, string node)[]
{
    ("kompose 1.38.0", ThuMuc("kompose", ("banhang-sql-v1", ChuoiSql), ("bhpay-api-key-v1", KhoaGia)), "{{.Node.Hostname}}"),
    ("kompose, sửa riêng secret", ThuMuc("kompose-sua-secret", ("ConnectionStrings__BanHang", ChuoiSql), ("BHPay__ApiKey", KhoaGia)), "{{.Node.Hostname}}"),
    ("sửa tay", ThuMuc("sua-tay", ("ConnectionStrings__BanHang", ChuoiSql), ("BHPay__ApiKey", KhoaGia)), "sw-wkr-1"),
};
int cong = 5281;
foreach (var (ten, thuMuc, node) in boCuc)
{
    string url = $"http://127.0.0.1:{cong++}";
    var psi = new ProcessStartInfo(exe, ["--urls", url, "--ThuMucBiMat", thuMuc, "--SqlChoKhoiDongGiay", "5"])
        { RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true };
    psi.Environment["BanHang__Node"] = node;   // kompose: chuỗi mẫu; sửa tay: kubelet điền tên node
    var sw = Stopwatch.StartNew();
    var p = Process.Start(psi)!;
    var loi = new List<string>();
    p.OutputDataReceived += (_, _) => { };
    p.ErrorDataReceived += (_, e) => { if (e.Data != null) lock (loi) loi.Add(e.Data); };
    p.BeginOutputReadLine(); p.BeginErrorReadLine();
    while (!p.HasExited && await Ma(url + "/healthz/ready") != 200) await Task.Delay(50);
    Console.WriteLine($"{ten}: {string.Join(", ", Directory.GetFiles(thuMuc).Select(Path.GetFileName))}");
    if (p.HasExited)
    {
        await Task.Delay(200);
        string dong = loi.FirstOrDefault(d => d.Contains("Exception")) ?? "";
        Console.WriteLine($"  không khởi động được, thoát mã {p.ExitCode}: {dong.Split("Exception: ")[^1].Trim()}");
        continue;
    }
    var may = System.Text.Json.JsonDocument.Parse(await http.GetStringAsync(url + "/api/may"));
    Console.WriteLine($"  ready 200 sau {sw.Elapsed.TotalSeconds:F1} s; /api/may trả node = {may.RootElement.GetProperty("node")}");
    p.StandardInput.WriteLine("term"); await p.WaitForExitAsync();
}
SqlConnection.ClearAllPools();
await Sql("ALTER DATABASE Kumeo_G2 SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE Kumeo_G2");
Console.WriteLine($".NET {Environment.Version}, Kumeo_G2 đã xóa");

async Task<int> Ma(string u) { try { using var r = await http.GetAsync(u); return (int)r.StatusCode; } catch { return 0; } }
static async Task Sql(string lenh) { await using var cn = new SqlConnection(Master); await cn.OpenAsync(); await new SqlCommand(lenh, cn).ExecuteNonQueryAsync(); }

6. Chứng minh: 11 dòng sai và thiếu ở bản kompose, 0 ở bản sửa tay

Bài chưa dựng cụm thật: không chạy Docker, k3s hay Swarm. Bằng chứng gồm cấu hình đã qua công cụ kiểm, .NET chạy thật trên laptop Intel Core Ultra 5 125U, Windows 11, và bảng chịu lỗi ở mục 4 tính theo công thức quorum của bài 1.

kubeconform v0.8.0, -strict, Kubernetes 1.36.5: bản kompose 2/2 tài liệu hợp lệ, bản sửa tay 4/4; kubernetes-validate 1.36.0 cũng cho 4/4. SoSwarmK3s.cs trên cùng 43 khóa, 29 dòng so (26 luật và 3 việc ngầm):

Trạng thái kompose 1.38.0 Sửa tay
Khớp 14 10
Đổi dạng, cùng tác dụng 2 13
Thêm mới, Swarm làm ngầm 0 3
Thay bằng quy trình, không tương đương 2 3
Sai 6 0
Thiếu 5 0

Bản kompose có 11/29 dòng sai hoặc thiếu; bản sửa tay giữ đủ, phần lớn bằng trường khác tên

KhớpĐổi dạng hoặc thêm mớiKhông tương đươngSaiThiếu
kompose 1.38.029 dòngSửa tay29 dòng
SoSwarmK3s.cs, YamlDotNet 18.1.0, .NET 10.0.12, trên kết quả docker stack config của stack chung ba bài Swarm. Cả hai bản đều qua kubeconform v0.8.0 với Kubernetes 1.36.5.
Bảng số liệu
KhớpĐổi dạng hoặc thêm mớiKhông tương đươngSaiThiếuTổng
kompose 1.38.014 dòng2 dòng2 dòng6 dòng5 dòng29 dòng
Sửa tay10 dòng16 dòng3 dòng0 dòng0 dòng29 dòng

Các dòng sai của bản kompose, bỏ cột giá trị Swarm và rút gọn lời giải thích:

SAI  env {{.Node.Hostname}}        chuỗi mẫu giữ nguyên, Kubernetes không thay
SAI  rút trước SIGTERM             RutChoGiay 0 + preStop 0 s
SAI  routing mesh :8080            Service ClusterIP, không nhận request từ ngoài
SAI  start_period, start_interval  liveness initialDelaySeconds 60, không startupProbe
SAI  secrets                       /run/secrets/banhang-sql-v1: KeyPerFile không thấy khóa

Dòng thứ sáu là {{.Task.Name}}, cùng lỗi với dòng đầu. Năm dòng thiếu là minReadySeconds, hạn báo lỗi rollout, topologySpreadConstraints, readiness và PodDisruptionBudget cho lúc drain. Ba dòng không tương đương ở bản sửa tay là restart_policy.delay (kubelet tự lùi 10, 20, 40 giây, tối đa 300 giây), rollback_config, và failure_action thay bằng progressDeadlineSeconds cộng git revert.

ThuBoCuc.cs chạy bản build của BanHang.Api.cs chung ba bài Swarm, với SQL Server 2019 LocalDB, database Kumeo_G2 tạo rồi xóa, .NET 10.0.12:

kompose 1.38.0: banhang-sql-v1, bhpay-api-key-v1
  không khởi động được, thoát mã -532462766: Thiếu ConnectionStrings:BanHang (secret ConnectionStrings__BanHang)
kompose, sửa riêng secret: BHPay__ApiKey, ConnectionStrings__BanHang
  ready 200 sau 1.4 s; /api/may trả node = {{.Node.Hostname}}
sửa tay: BHPay__ApiKey, ConnectionStrings__BanHang
  ready 200 sau 1.3 s; /api/may trả node = sw-wkr-1

Với bố cục của kompose, app dừng ở dòng throw của bài 3; trên Kubernetes pod sẽ vào vòng restart. Sửa riêng secret thì app chạy, nhưng mọi pod báo cùng một tên node vô nghĩa. Ở bản sửa tay, biến môi trường đứng thay cho giá trị kubelet điền từ spec.nodeName.

Điều chưa chứng minh: chưa cài k3s, nên hành vi ServiceLB khi một máy rời cụm, thời gian rollout với minReadySeconds 90, và việc pod cũ còn phục vụ khi bản mới treo là suy từ tài liệu Kubernetes, chưa đo. Kế hoạch năm bước chưa chạy thử; mã thoát -532462766 là của Windows, trên Linux tiến trình thoát theo cách khác.

7. Kết luận

Chuyển Swarm sang k3s không làm cụm chịu lỗi tốt hơn; nó đổi rollback tự động lấy readiness, HPA và một chuỗi GitOps chung với 12 cửa hàng. Chuyển được an toàn khi mọi khóa của stack có một luật nói rõ thiết lập đó sống ở đâu trong manifest.

Trong dự án .NET của bạn:

  • Đặt path của Secret bằng tên khóa cấu hình và gom các Secret bằng volume projected vào /run/secrets, để AddKeyPerFile không đổi.
  • Thay {{.Node.Hostname}} bằng Downward API spec.nodeName; thay RutChoGiay của Swarm bằng preStop sleep và tăng terminationGracePeriodSeconds tương ứng.
  • Dịch delay và monitor thành minReadySeconds, đặt progressDeadlineSeconds, và để Flux áp git revert khi rollout báo lỗi.
  • Chạy kubeconform và một công cụ đối chiếu như SoSwarmK3s.cs trong CI; mã thoát khác 0 thì không merge.

Những chỗ hay hiểu sai

  • "kompose ra manifest hợp lệ là xong." Hợp lệ với schema, nhưng 11/29 dòng sai hoặc thiếu.
  • "k3s HA chịu lỗi tốt hơn Swarm." Cả hai là Raft trên 3 máy, chịu được 1 máy hỏng.
  • "Kubernetes tự rollback như Swarm." Deployment chỉ báo ProgressDeadlineExceeded.

Đọc tiếp

Nguồn

Đọc tiếp

Bài tiếp theo trong series

Dữ liệu offline của cửa hàng trên k3s: PVC local-path, tên node và sao lưu bằng CronJob

SSD của máy ch06 hỏng sau gần 3 giờ mất mạng và 41 đơn bán offline mất theo; ở ch02, đổi tên máy làm pod kẹt Pending vì PV local-path gắn với tên node cũ. Cố định tên node, sao lưu SQLite mỗi 15 phút bằng API backup qua CronJob ra ổ thứ hai, và để initContainer tự khôi phục volume trống.

13 phút đọc

Trong Docker Swarm và k3s

Máy chủ cửa hàng chạy k3s: backend tự thay khi treo trên mini PC 8 GB

Backend ở cửa hàng ch04 treo lúc 19:40 và cửa hàng không bán được 52 phút, vì systemd chỉ chạy lại tiến trình đã thoát. k3s gói Kubernetes vào một binary dưới 100 MB với SQLite, containerd, Flannel, Traefik, ServiceLB và local-path; Deployment có liveness probe thay pod treo trong 32–67 giây theo mô hình.

14 phút đọc

Trong Docker Swarm và k3s

Probe, giới hạn bộ nhớ và HPA trên k3s: pod cửa hàng trụ được flash sale

Flash sale 9/9 ở ch01, pod BanHang.CuaHang restart liên tục vì liveness gọi endpoint có kiểm trụ sở, heap limit ghi bằng số thập phân thành 16 GiB, và một pod không gánh nổi tải. Tách ba probe, để GC tự lấy 75% memory limit, và chạy HPA 2–3 pod; kiểm bằng app .NET chạy thật, Job Object 512 MiB và mô hình kubelet, HPA.

13 phút đọc