Finalizer, ownerReferences và garbage collection

K
Kai··7 min read

Ở Bài 24 ta xóa một Deployment và thấy nó "kéo theo cả ReplicaSet lẫn Pod"; Bài 27 xóa CronJob thì Job con cũng đi theo. Mỗi lần đó tôi viết "garbage collection làm việc" rồi đi tiếp. Bài này dừng lại mổ xẻ đúng cơ chế đó, phần vòng đời và liên kết giữa các object: ownerReferences nối cha-con, garbage collector tự dọn con khi cha mất, và finalizer chặn xóa tới khi dọn dẹp xong. Đây là máy móc ngầm đằng sau hầu hết thao tác xóa trong Kubernetes.

ownerReferences: ai sở hữu ai

Tài liệu mở đầu garbage collection bằng owner reference: "Owner references tell the control plane which objects are dependent on others. Kubernetes uses owner references to give the control plane ... the opportunity to clean up related resources before deleting an object. In most cases, Kubernetes manages owner references automatically." Hai vai: owner (cha) và dependent (con). Ta đã thấy chuỗi này nhiều lần (Pod→ReplicaSet→Deployment ở Bài 24, Job→CronJob ở Bài 27) nhưng giờ nhìn vào trường thật. Tạo thẳng một ReplicaSet:

apiVersion: apps/v1
kind: ReplicaSet
metadata: {name: rs-demo}
spec:
  replicas: 2
  selector: {matchLabels: {app: rs-demo}}
  template:
    metadata: {labels: {app: rs-demo}}
    spec:
      containers: [{name: c, image: busybox:1.36, command: ["sleep","3600"]}]
POD=$(kubectl get pods -l app=rs-demo -o jsonpath='{.items[0].metadata.name}')
kubectl get pod $POD -o jsonpath='owner.kind={.metadata.ownerReferences[0].kind} owner.name={.metadata.ownerReferences[0].name} controller={.metadata.ownerReferences[0].controller} blockOwnerDeletion={.metadata.ownerReferences[0].blockOwnerDeletion}{"\n"}'
owner.kind=ReplicaSet owner.name=rs-demo controller=true blockOwnerDeletion=true

Mỗi pod mang một ownerReferences trỏ về ReplicaSet rs-demo, với controller=true (RS này là controller quản nó) và blockOwnerDeletion=true (con này có thể chặn việc xóa cha trong chế độ foreground — sẽ nói dưới). ReplicaSet tự gắn cái này khi tạo pod, đó là sợi dây để garbage collector biết "xóa rs-demo thì dọn luôn hai pod này". Một ràng buộc thiết kế từ tài liệu: "Cross-namespace owner references are disallowed ... A namespaced owner must exist in the same namespace as the dependent." Cha con phải cùng namespace (nối với khái niệm namespace của Bài 28).

Garbage collection: dọn con khi cha mất

Xóa cha thì con bị dọn, nhưng bằng cách nào thì có ba kiểu. Mặc định là background, tài liệu: "In background cascading deletion, the Kubernetes API server deletes the owner object immediately and the garbage collector controller ... cleans up the dependent objects in the background." Xóa rs-demo và xem:

kubectl delete rs rs-demo          # cascade mặc định = background
kubectl get pods -l app=rs-demo
replicaset.apps "rs-demo" deleted

NAME            READY   STATUS        ...
rs-demo-g2wkg   1/1     Terminating
rs-demo-rhcjd   1/1     Terminating

ReplicaSet biến mất ngay (deleted), rồi hai pod con chuyển Terminating, garbage collector dọn chúng sau, ở chế độ nền. Đây đúng là thứ xảy ra mỗi lần ta kubectl delete deployment: object cha đi trước, GC dọn con theo. Tài liệu chốt cơ chế phát hiện: "Kubernetes checks for and deletes objects that no longer have owner references, like the pods left behind when you delete a ReplicaSet."

Chế độ orphan: giữ con lại

Nhưng không phải lúc nào xóa cha cũng muốn mất con. --cascade=orphan xóa riêng cha, để con ở lại — "the dependents left behind are called orphan objects." Tạo lại một RS rồi xóa kiểu orphan:

kubectl delete rs rs-orphan --cascade=orphan
kubectl get pods -l app=rs-orphan
kubectl get pod <pod> -o jsonpath='{.metadata.ownerReferences}'
NAME              READY   STATUS    ...
rs-orphan-2m6pk   1/1     Running
rs-orphan-7jhqx   1/1     Running

[]

Tương phản hoàn toàn với background: RS mất nhưng pod vẫn Running, và ownerReferences của chúng giờ rỗng ([]), sợi dây cha-con đã bị cắt, pod thành object độc lập. Orphan hữu ích khi muốn thay controller mà không gián đoạn pod đang chạy (ví dụ tái tạo một ReplicaSet quản lại đúng đám pod cũ).

Foreground: con trước, cha sau

Kiểu thứ ba, foreground, ngược thứ tự background. Tài liệu: "the owner object you're deleting first enters a deletion in progress state ... the controller deletes dependents it knows about. After deleting all the dependent objects it knows about, the controller deletes the owner object." Khi đó API server gắn cho cha một deletionTimestamp một finalizer tên foregroundDeletion, giữ cha hiện diện (read-only) cho tới khi mọi con — cụ thể là những con có blockOwnerDeletion=true — bị xóa xong. Dùng kubectl delete ... --cascade=foreground khi bạn cần chắc chắn con đã sạch trước khi cha biến mất. Để ý ở đây xuất hiện chữ "finalizer", khái niệm cuối của bài.

Finalizer: chặn xóa tới khi dọn xong

foregroundDeletion là một finalizer dựng sẵn. Finalizer nói chung, theo tài liệu: "Finalizers are namespaced keys that tell Kubernetes to wait until specific conditions are met before it fully deletes resources that are marked for deletion. Finalizers alert controllers to clean up resources the deleted object owned." Cơ chế xóa khi có finalizer:

"the API server ... Modifies the object to add a metadata.deletionTimestamp field ... Prevents the object from being removed until all items are removed from its metadata.finalizers field ... Returns a 202 status code (HTTP "Accepted")."

Tức object không biến mất ngay — nó vào trạng thái "đang chờ xóa" (có deletionTimestamp) và nằm đó tới khi danh sách finalizer rỗng. Dựng một ConfigMap mang finalizer tự đặt rồi thử xóa:

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-final
  finalizers: ["kkloud.io/cleanup-demo"]
data: {k: "v"}
kubectl delete configmap cm-final
kubectl get configmap cm-final -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp} finalizers={.metadata.finalizers}{"\n"}'
configmap "cm-final" deleted          # <- kubectl in vậy, nhưng...

deletionTimestamp=2026-05-23T16:35:12Z finalizers=["kkloud.io/cleanup-demo"]

kubectl in "deleted" (vì nhận 202 Accepted), nhưng object vẫn còn: nó đã có deletionTimestamp và vẫn giữ finalizers=["kkloud.io/cleanup-demo"]. Đây là object kẹt ở "Terminating" — cảnh tượng quen thuộc khi một namespace hay PVC "xóa mãi không chịu mất". Bình thường, controller phụ trách finalizer sẽ làm xong việc dọn (xóa tài nguyên ngoài, gỡ volume...) rồi tự gỡ key khỏi finalizers; ở đây finalizer là của ta bịa ra, không ai gỡ, nên nó kẹt vĩnh viễn. Gỡ tay để nó xóa thật:

kubectl patch configmap cm-final --type=json -p='[{"op":"remove","path":"/metadata/finalizers"}]'
kubectl get configmap cm-final
configmap/cm-final patched
Error from server (NotFound): configmaps "cm-final" not found

Vừa danh sách finalizer rỗng, object lập tức biến mất — đúng như tài liệu: "When the finalizers field is emptied, an object with a deletionTimestamp field set is automatically deleted." Bài học vận hành: gặp object kẹt Terminating mãi, đừng đập --force mù quáng; xem metadata.finalizers để biết cái gì đang chặn, rồi xử lý đúng controller đó (hoặc gỡ tay nếu chắc chắn việc dọn đã xong). Finalizer dựng sẵn hay gặp: kubernetes.io/pv-protection (chống xóa nhầm PersistentVolume), và finalizer kubernetes trên namespace (giữ namespace tới khi mọi object bên trong sạch).

🧹 Dọn dẹp

kubectl delete pod -l app=rs-orphan --now   # pod mồ côi phải xóa tay
# cm-final đã xóa; rs-demo + pod đã GC

Đám pod mồ côi từ test orphan không còn cha để GC nên phải xóa tay, đúng tinh thần "mồ côi thì không ai dọn hộ". Cụm về lại hai pod CoreDNS. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 29-finalizers-gc.

Tổng kết

Ba cơ chế quản vòng đời và liên kết object. ownerReferences nối con→cha (ta thấy pod trỏ về ReplicaSet với controller=true, blockOwnerDeletion=true; cha con buộc cùng namespace), là sợi dây để dọn dẹp dây chuyền. Garbage collection xóa con khi cha mất, ba kiểu: background (mặc định, cha đi ngay, GC dọn con nền, ta thấy pod Terminating), foreground (con trước/cha sau, cha mang finalizer foregroundDeletion chờ con blockOwnerDeletion sạch), và orphan (--cascade=orphan cắt ownerReferences, pod ở lại Running). Finalizer là key chặn xóa: object bị DELETE chỉ nhận deletionTimestamp và kẹt Terminating tới khi metadata.finalizers rỗng (ta thấy ConfigMap kẹt rồi gỡ finalizer mới mất). Đó là cách gỡ những object "xóa mãi không chịu đi".

Bài 30 khép Part V với phần quản lý object ở tầng thực hành: ba kiểu quản lý (imperative command, imperative object, declarative apply), bộ nhãn khuyến nghị app.kubernetes.io/*, và khái niệm storage version của API — nền cho việc vận hành object một cách nhất quán và nâng cấp về sau.

Related Posts