Operator 的测试该写到哪一层?139 个用例之后我划的三条线

更名说明 :本项目原名 KubeSentry ,自 2026-11-03 起更名 KubeStasis (避免与既有项目重名);CRD group 也于 11-04 由 apps.sentry.io 改为 kubestasis.io。前几篇文章里的旧名指的都是同一个项目,代码仓库现为 github.com/weirong-ui/...。
番外篇说明:系列正文八篇在上一篇收尾了。这篇是补的番外,回答正片里一直没展开的问题,这些东西到底是怎么测出来的。可以单独读,不必按顺序。

前八篇里我反复写"实测""跑过一遍",但一直没交代这些数字是怎么来的。这个问题不太好答,因为我一开始也没打算认真测。

真实的过程是:0 个用例 → 31 → 57 → 103 → 113 → 137 → 139。这些数字的每一次跳变,基本都跟着一次修复或一个新能力走,很少有"先把覆盖率补上去"的那种批量加。测试不是先设计好再写的,是被事故一条一条打出来的。

这篇讲我最后划的三条线,以及每条线为什么不能再往外扩一寸。

零、先给结论:三层各管什么

层级 工具 验什么 不验什么 现状
纯函数 go test,无 K8s 对象 决策表、状态机迁移、校验规则、序列化 任何写操作 85 个(scaler 61 + healer 24)
fake client client/fake 单次 Reconcile 的写路径、补丁构造、冲突重试 watch、真实 apiserver 校验、默认值 controller 16 里的写路径部分
envtest 进程内 etcd + kube-apiserver watch → enqueue → Reconcile → 写回 的闭环 kubelet、调度、GC、Deployment 控制器 4 条链路,CI 独立 job
kind e2e 真集群 上面所有"不验"的部分,加镜像和 webhook 快速反馈(分钟级) P0/P2/P3 三条

这张表是结论。下面讲每一层为什么只能停在那里。

一、覆盖率对这个项目没什么用

先说一个不好听的事实:controller-runtime 项目的行覆盖率很容易刷到 80% 以上,同时什么也没测到。

原因在于 Reconcile 的形态。一个 Reconcile 函数里塞着十来次 client.Get / Create / Patch,你把它们全跑一遍,覆盖率自然很高。但真正会出事的地方是这些调用的顺序 和失败分支:status 写在前还是 spec 写在前、conflict 之后重试几次、拿不到对象时是返回 nil 还是返回 err。这些都不是行数能反映的。

所以我后来放弃看覆盖率,改成问三个问题:

  1. 这条用例失败的时候,报错信息能不能直接告诉我是哪个分支错了?
  2. 如果我把被测代码故意改坏一行,有几条用例会红?
  3. 这条用例是锁住一个已经踩过的坑,还是我猜将来可能有坑?

第三个问题最关键。猜出来的用例会写成"happy path 加一个想当然的边界",锁不住任何东西。前八篇里写到的那些坑,凡是能在这一层锁住的,最后都变成了一条名字里带着故障现象的用例。

二、第一层:纯函数,最便宜的回归

internal/scaler 里 61 个用例,internal/healer 里 24 个,加起来占了 139 的六成。这一层单包跑完 1 到 3 秒,是唯一能做到"改一行就跑一遍"的层级。

把时钟做成参数,是这一层能成立的前提。

Decide 早期长这样,Engine 里挂着一个 nowFunc 字段:

go 复制代码
type Engine struct {
    source  MetricsSource
    nowFunc func() time.Time
}

看起来无害,实际有两个后果。一是测试不能并行,因为改 nowFunc 是改共享状态;二是用例之间会互相污染,一个用例设了固定时钟,下一个忘了恢复就飘了。11-05 那次重构把它删掉了,时钟改成 Decide 的第三个参数:

go 复制代码
func (e *Engine) Decide(ctx context.Context, app *appsv1alpha1.App,
    now func() time.Time) Decision

改完立刻能写这两条:

go 复制代码
func TestDecide_DoesNotMutateInput(t *testing.T)   // 决策不改传入对象
func TestDecide_ConcurrentSafe(t *testing.T)       // 并发调用无竞态

第二条和 CI 的 -race 门是配套的:一个在代码层面约束"不许改输入",一个在运行时抓并发写。光靠自觉守不住纯函数这个承诺。

同样重要的是"没数据"和"数据是 0"要分开。

TestPrometheusSource_NoDataIsNotZero 这个名字就是结论。Prometheus 查不到序列时返回空结果,如果你把空结果当 0,ErrorRate=0 会让自愈引擎认为"一切正常",QPS=0 会让伸缩器认为"可以缩容"。这两种误判都很难在日志里看出来,因为代码走的都是同一条路径。

处理办法是让信号携带"是否有效":

go 复制代码
type Signal struct {
    Type  appsv1alpha1.MetricType
    Value float64
    Valid bool
}

Valid=false 的信号不参与 max 计算,也不触发动作。用例要同时覆盖三条分支:有数据、无数据、数据恰好是 0。

最值钱的一条用例,来自一个让所有 App 都起不来的 bug。

Scaling / Health 原来是值类型加 omitempty:

go 复制代码
Scaling ScalingPolicy `json:"scaling,omitempty"`

omitempty 对结构体不生效。控制器回写对象时,比如加 finalizer,会把省略的 health 序列化成 {"strategy":""},而 strategy 的 CRD 校验是 enum,空字符串直接被拒。结果就是:任何省略 spec.health 的 App,第一轮 Reconcile 就无限报错。

样例 CR 恰好把字段写全了,所以 P0 阶段完全没暴露。修法是把两个字段改成指针,然后补一条锁住序列化行为的用例:

go 复制代码
func TestAppSpec_OmitsNilPolicies(t *testing.T)

这条用例不测任何业务逻辑,只测 JSON 里有没有多出零值对象。它比任何覆盖率数字都有价值。

三、第二层:fake client,验一层写路径

纯函数测不了"我到底往 apiserver 写了什么"。这一层的职责就是抓补丁。

第一个坑:status 子资源要显式声明。

go 复制代码
c := fake.NewClientBuilder().
    WithScheme(testScheme(t)).
    WithStatusSubresource(&appsv1alpha1.App{}).
    WithObjects(app).
    Build()

少了 WithStatusSubresource,Status().Update() 会被当成普通更新处理,status 和 spec 的写入语义就混在一起了。测试照样绿,但它验的东西和真集群不是一回事。

这一层最典型的一条用例,锁的是"status 取哪个字段"。

go 复制代码
func TestUpdateStatus_ReadsRealReplicas(t *testing.T) {
    // Deployment 只有 2 个就绪,spec 想要 3
    deploy := &appsv1.Deployment{Status: appsv1.DeploymentStatus{
        ReadyReplicas:   2,
        UpdatedReplicas: 2,
    }}
    // ...
    if got.Status.CurrentReplicas != 2 {
        t.Errorf("currentReplicas = %d, want 2 (real ready)", got.Status.CurrentReplicas)
    }
}

为什么值得单独一条?因为写错成 spec.replicas 的时候,kubectl get app 在 Pod 全挂的情况下仍然显示"满编 3/3"。这个错误在真集群里要等到出事那一刻才会被发现,而在 fake client 里三行就能锁死。

并发冲突怎么测?用拦截器注入一次失败。

真实场景是 11-03 那次:L3 回滚时 status 回写刚改过 resourceVersion,spec 写入立刻撞 the object has been modified,错误冒泡成了 KubeStasisError 事件。修法是 retry.RetryOnConflict,但怎么证明重试真的生效?

fake client 的 interceptor.Funcs 可以精确地让第一次 Patch 失败:

go 复制代码
func conflictOnce() (interceptor.Funcs, *int) {
    calls := new(int)
    return interceptor.Funcs{
        Patch: func(ctx context.Context, c client.WithWatch, obj client.Object,
            patch client.Patch, opts ...client.PatchOption) error {
            if _, isApp := obj.(*appsv1alpha1.App); isApp && *calls == 0 {
                *calls++
                return apierrors.NewConflict(
                    schema.GroupResource{Group: "kubestasis.io", Resource: "apps"},
                    obj.GetName(), fmt.Errorf("the object has been modified"))
            }
            return c.Patch(ctx, obj, patch, opts...)
        },
    }, calls
}

用例同时断言三件事:err == nil(冲突被吸收)、*calls == 1(只注入了一次,没有被反复重试掩盖)、replicas 5 → 6(最终写成功)。只断言第一件是不够的,因为"重试十次才成功"和"一次就成功"在 err == nil 里看不出区别。

这一层的能力边界很清楚:它能验我构造的补丁,不能验 apiserver 会不会接受这个补丁。CRD schema、enum、默认值都不在这一层。

四、第三层:envtest,验闭环

前面两层都有个共同的盲区:没有任何东西真的 watch 过 。我可以在单测里手动调 Reconcile(req),但真实链路上是 apiserver 发事件、informer 入队、workqueue 去重、然后才轮到我。

envtest 补的就是这段。它在进程内起一个真实的 etcd 和 kube-apiserver,没有 kubelet、没有 controller-manager、没有调度器。

test/envtest/suite_test.go 里有几个决定值得单独说,每一个都不是随手写的。

CRD 只装 config/crd/bases。

go 复制代码
env := &envtest.Environment{
    CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
    ErrorIfCRDPathMissing: true,
    AttachControlPlaneOutput: false,
}

装的是 controller-gen 生成的那份产物,和 kubectl apply 装进集群的完全同一份文件。这样 envtest 顺带把 CRD 产物本身也验了:如果 schema 生成错了,测试起不来。

ErrorIfCRDPathMissing: true 是防路径写错。默认行为是路径不存在就跳过装 CRD,然后所有用例因为 no matches for kind "App" 失败。那个报错会让你去怀疑 scheme,而不是怀疑路径。

每个用例独立命名空间。

go 复制代码
func newNamespace(t *testing.T, prefix string) string {
    ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: prefix + "-"}}
    if err := k8sClient.Create(context.Background(), ns); err != nil {
        t.Fatalf("create namespace: %v", err)
    }
    t.Cleanup(func() { _ = k8sClient.Delete(context.Background(), ns) })
    return ns.Name
}

共用 default 命名空间会出事:上一个用例留下的 Deployment 还在,下一个用例的 Get 可能拿到别人的对象。用 GenerateName 而不是 t.Name(),因为测试名里带下划线和斜杠,不能直接做 namespace。

伸缩引擎故意不注入指标源。

go 复制代码
Scaler: scaler.NewEngine(), // 未注入指标源,伸缩保持休眠------用例才确定

这一行是整个套件里最重要的设计。如果注入了真实指标源,Reconcile 每轮都会尝试伸缩,4 条链路就会随机地扩缩容,用例变成"有时过有时不过"。不注入时 Decide 稳定返回 ActionNone + ReasonNoMetricsSource,测的才是部署和状态机,不是伸缩。

四个用例各自锁一条链路。

text 复制代码
TestCreateApp_ReconcilesChildren        创建 → Deployment/Service/ownerRef/finalizer/status
TestDeploymentReady_AppBecomesAvailable 代写 Deployment.status → App Available=True
TestDeleteApp_FinalizerReleased         删 App → 摘 finalizer → 对象消失
TestDrift_DeploymentReplicasCorrected   改 Deployment.replicas=7 → 被拉回 3

第三条和第四条是重点。

删除那条要先等到 finalizer 就位再删,否则 delete 直接成功,什么都没验到:

go 复制代码
eventually(t, waitTimeout, "finalizer 就位", func() (bool, error) {
    if err := k8sClient.Get(ctx, key, &app); err != nil {
        return false, err
    }
    return len(app.Finalizers) > 0, nil
})

漂移那条是幂等性的控制循环级回归。真集群里有人 kubectl scale 绕过 App,CreateOrUpdate 必须把它拉回来。这个行为在单测里只能验"我算出了正确的期望值",验不到"watch 真的把我的期望值应用上去了"。

五、envtest 的四条硬边界

这一节是这篇文章真正的重点。envtest 很容易被当成"轻量版集群",一旦这么认为,就会写出一堆看起来在验集成、实际什么都没验的用例。

边界一:没有 kube-controller-manager,Deployment 的 status 永远是空的。

你 Create 一个 Deployment,.status.readyReplicas 一直是 0。因为填这个字段的是 Deployment 控制器,envtest 里没有它。

所以第二条用例手工代写:

go 复制代码
// 代写 Deployment.status(真实集群里这是 kubelet 干的)
deploy.Status.Replicas = 2
deploy.Status.ReadyReplicas = 2
deploy.Status.AvailableReplicas = 2
deploy.Status.UpdatedReplicas = 2
if err := k8sClient.Status().Update(ctx, &deploy); err != nil {
    t.Fatalf("update deployment status: %v", err)
}

这是模拟输入,不是伪造通过。区别在于:我明确知道这个字段在真集群里由谁填,envtest 里我替它填,验的是从 status 到 App.status.conditions 的那一段转换。如果哪天 Deployment 控制器真的不填这些字段了,这条用例不会告诉你。那件事归 kind e2e 管。

边界二:没有 kubelet,Pod 永远不会 Ready,containerStatuses 永远是空的。

这意味着 healer 的 Inspect(读 containerStatuses[].state.waiting.reason 判断 CrashLoop)没法在 envtest 里验 。Pod 会停在 Pending,status.containerStatuses 是空数组,Inspect 只能看到"没有异常"。

所以 healer 那 24 个用例全部喂构造出来的 Pod 对象,不经过 apiserver。这是唯一可行的做法,代价是"我构造的 Pod 长得像不像真的"这件事没人保证。lastState.terminated.reason 和当前 terminated 都算 OOMKilled,这条规则就写在这里,而它是否覆盖了真集群的全部形态,envtest 回答不了。

边界三:没有 GC,删掉 App 之后 Deployment 会留下。

TestDeleteApp_FinalizerReleased 只断言 finalizer 摘除、App 对象消失。它不断言 Deployment 也被删了,因为 ownerReference 的级联删除由 kube-controller-manager 里的 GC 控制器执行,envtest 里没有这个控制器。

代码注释里写得很直白:

go 复制代码
// (envtest 无 kube-controller-manager,GC 不跑,Deployment 残留是预期------
// 真集群的 ownerRef 级联回收由 kind e2e 兜底验证。)

如果有人不知道这条,会写一个"删 App 后 Deployment 也应该消失"的用例,然后发现它在 envtest 里必挂,进而怀疑自己的 finalizer 逻辑有问题。实际上逻辑是对的,是这个环境不会回收。

边界四:没有 scheduler,Pod 不会被调度到任何节点。

推论和边界二连在一起:既然 Pod 停在 Pending,任何依赖"Pod 跑起来之后"的行为都验不了。探针失败、OOMKilled、CrashLoopBackOff,这三类自愈的触发条件在 envtest 里造不出来。

所以矩阵是这样的:

想验的东西 只能用哪一层
决策算法对不对 纯函数
补丁写对没有、冲突重试了没有 fake client
watch 触发没有、status 回写没有、finalizer 放行没有 envtest
Pod 真崩了会不会自愈、镜像坏了会不会回滚 kind e2e

这张表是从代码注释和目录结构里读出来的,不是一开始就设计好的。suite_test.go 的注释自己就写着"envtest 没有 kube-controller-manager,Deployment.status 由测试代写",删除用例的注释里写着"GC 不跑,Deployment 残留是预期"。这些话是踩过之后补的,写在那儿就是为了让下一个人不用再踩。

六、防止测试静默变绿

这是我踩得最疼的一类坑:测试明明没跑,CI 却是绿的。

第一道:KUBESTASIS_REQUIRE_ENVTEST=1。

没有 envtest 二进制时,TestMain 里 env.Start() 会失败。默认行为是让所有用例 t.Skip,本地 go test ./... 不阻塞开发。但这意味着 CI 上如果忘了装二进制,整套集成测试会静默跳绿。

我实测过这个现场。在没装二进制的机器上跑:

text 复制代码
=== RUN   TestCreateApp_ReconcilesChildren
--- SKIP: TestCreateApp_ReconcilesChildren (0.00s)
=== RUN   TestDeploymentReady_AppBecomesAvailable
--- SKIP: TestDeploymentReady_AppBecomesAvailable (0.00s)
=== RUN   TestDeleteApp_FinalizerReleased
--- SKIP: TestDeleteApp_FinalizerReleased (0.00s)
=== RUN   TestDrift_DeploymentReplicasCorrected
--- SKIP: TestDrift_DeploymentReplicasCorrected (0.00s)
PASS
ok  	github.com/weirong-ui/kubestasis/test/envtest	3.520s

四条全 Skip,包状态是 PASS。如果只看 CI 有没有红叉,这就是一次完美通过。

加上环境变量之后,同一个命令:

text 复制代码
FATAL: start envtest: unable to start control plane itself: failed to start the controlplane. retried 5 times: exec: "\\usr\\local\\kubebuilder\\bin\\etcd": executable file not found in %PATH%
FAIL	github.com/weirong-ui/kubestasis/test/envtest	2.885s

所以 CI 的 envtest job 里这一行不是可选项:

yaml 复制代码
- name: go test ./test/envtest
  env:
    KUBESTASIS_REQUIRE_ENVTEST: "1"
  run: go test ./test/envtest/... -v -count=1

-count=1 也有用。Go 默认缓存测试结果,改了代码但没改依赖时,可能直接返回上次的 ok (cached)。

第二道:-race。

11-05 加进 build job。同期做的一件事是把 Decide 纯化,它原本会改 Engine 的时钟字段,两个 goroutine 同时决策就是数据竞争。这类 bug 不会让测试失败,只会让线上概率性地算错一次决策,普通单测永远抓不到。

第三道:用例数只认一个源。

STATUS 里的 139、README 徽章里的 139、文档正文里的 139,这三处只要有一处漂移,就会有人拿着旧数字去写简历。这件事人工核对漏过一次:用例数已经从 137 加到 139,README 开发节里还写着 113。后来改成 hack/doc_consistency.py 机器查,用例数以徽章为单一事实源,历史演进行豁免。

一个数字能被三处引用,就一定会漂。

七、今天在 Windows 上跑一遍的真实结果

写这篇之前,我在 Windows 上从 c2fef19 起了一份干净克隆,把能在本机跑的两层都跑了一遍。这不是为了引用 CI 的截图(CI 是 Ubuntu),而是想知道换平台会发生什么。kind e2e 没跑,那需要真集群。

全套单测:

text 复制代码
ok  	.../internal/controller	3.392s
ok  	.../internal/healer    	1.690s
ok  	.../internal/metrics   	2.793s
ok  	.../internal/scaler    	1.996s
ok  	.../internal/webhook   	3.094s
ok  	.../pkg/miniinformer   	0.755s
ok  	.../test/envtest       	3.520s

--- PASS 行数 139,--- SKIP 行数 4,和 STATUS 完全一致。分包核对:

包 用例
internal/scaler 61
internal/healer 24
internal/webhook 24
internal/controller 16
pkg/miniinformer 11
internal/metrics 3

装上 envtest 二进制之后,4 条链路真跑:

text 复制代码
--- PASS: TestCreateApp_ReconcilesChildren (2.22s)
--- PASS: TestDeploymentReady_AppBecomesAvailable (0.42s)
--- PASS: TestDeleteApp_FinalizerReleased (0.42s)
--- PASS: TestDrift_DeploymentReplicasCorrected (0.42s)
ok  	github.com/weirong-ui/kubestasis/test/envtest	9.684s

第一条 2.22 秒,后面每条 0.42 秒。差的那 1.8 秒是控制面启动,不是用例本身。整包 9.68 秒,CI 上是 34 秒(Ubuntu,从零装二进制)。

日志里能看到真实的链路:

text 复制代码
installing CRD	{"crd": "apps.kubestasis.io"}
Starting EventSource	{"source": "kind source: *v1alpha1.App"}
Starting EventSource	{"source": "kind source: *v1.Deployment"}
Starting EventSource	{"source": "kind source: *v1.Service"}
Starting workers	{"worker count": 1}
DEBUG	events	app available: 2/2 replicas ready	{"reason": "KubeStasisReconcile"}

三个 EventSource 就是这四行声明的结果:

go 复制代码
func (r *AppReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&appsv1alpha1.App{}).
		Owns(&appsv1.Deployment{}). // Deployment 变化也会触发 Reconcile,加速收敛
		Owns(&corev1.Service{}).    // Service 被误删时自动重建
		Complete(r)
}

日志里的三个 kind source 和 For / Owns 三行一一对应。这条对应关系值得记一下:如果你在 envtest 里发现某个资源的变更没触发 Reconcile,先回去数 Owns 有几行。

但 Windows 上有一个退出时的坑。

text 复制代码
stop envtest: [unable to signal for process ...\kube-apiserver to stop: not supported by windows,
               unable to signal for process ...\etcd to stop: not supported by windows]

env.Stop() 在 Windows 上没法向子进程发信号,于是返回一个错误。包状态仍然是 ok,退出码 0,所以你很可能不会注意到这行。我事后查了进程表,etcd 和 kube-apiserver 都没有残留,说明父进程退出时它们被带走了。

但如果你在 Windows 上遇到过"端口被占用"或"下次启动 apiserver 失败",先看看是不是上一次的进程还没走。CI 跑 Ubuntu,没有这个问题。

八、这套东西没覆盖什么

按这个项目一贯的写法,边界要自己先说出来。

覆盖不到的,写在 STATUS 里: envtest 没有覆盖 webhook 与 controller 的联动,也没有覆盖 status 子资源的并发写竞争。前者要靠 kind e2e 的负向用例(:latest 被拒、默认值注入生效),后者目前完全没有自动化测试,只在 11-03 那次事故后靠 RetryOnConflict 和 conflictOnce() 那条单测兜着。

kind e2e 也只覆盖三条链路。 P0(部署 + Available)、P3(webhook 拒绝与默认值)、P2(crash 镜像 → L2 → L3 自动回滚 + 防乒乓)。伸缩那条链路有对照压测的数据,但没有进 e2e 断言,因为负载生成器和采样周期让它的时间成本远超一个 CI job 的合理范围。

探针的失败路径没有被测过。 probes_test.go 验的是"探针配置生成了什么",不是"探针失败之后会发生什么"。后者需要 kubelet,只能真集群。

139 个用例里没有一条是模糊测试或属性测试。 决策函数的输入空间不算大,我判断收益不高。这个判断可能是错的。

九、要照抄的话,抄这四件事

如果你也在写 Operator,这套结构里真正可迁移的只有四条:

  1. 时钟做参数,不要做字段。 这是纯函数层能成立的唯一前提,也是并行测试的前提。
  2. 每一层都写清楚"这一层造不出什么"。 envtest 造不出 CrashLoop,fake client 造不出 CRD 校验,纯函数造不出 watch。写下来,就不会有人写两小时注定失败的用例。
  3. 给"测试没跑"单独设一道闸。 Skip 是开发期的便利,CI 上必须变成失败。REQUIRE=1 这种变量只需要一次 os.Exit(1)。
  4. 让用例名带上故障现象。 TestAppSpec_OmitsNilPolicies 比 TestScalingSerialize 有用,因为半年后你只记得"那次所有 App 都起不来",不记得"序列化"。

至于用例数量。没人定过 139 这个目标,它是跟着事故一条一条长出来的。


系列其他文章

  1. HPA 只会伸缩不会自愈?我设计了一个 K8s 应用自愈 Operator:KubeStasis
  2. 从零写一个 K8s Operator:kubebuilder 初始化到第一个 Reconcile
  3. 为什么 HPA 会震荡?手写一个带冷却窗口的伸缩决策引擎
  4. 给 K8s 加一个"自动值班员":分级故障自愈引擎
  5. 图解 client-go Informer:手写一个 mini-Informer 才敢说懂
  6. 给 Operator 加 Admission Webhook 踩的 4 个坑(其中 3 个本可以不踩)
  7. 跟原生 HPA 做对照压测:五轮里前四轮全废,最后一个结论也被我自己降级
  8. Operator 上生产前补的 6 件事,和 5 件我明确不做的

代码与完整能力清单:github.com/weirong-ui/...(v0.1.0 快照) · docs/STATUS.md(唯一可信账本,含"已知不解决的问题") · docs/DESIGN.md §5 测试策略

文中的测试输出来自 c2fef19,2026-11-09 在 Windows 本地实测;CI 数字来自 Ubuntu 上的 envtest job。两边平台不同,耗时不可直接比较,结论一致。

相关推荐
逐流人3 小时前
手工建集群到全自动发布:k8s集群部署与DevOps流水线整合实战
ci/cd·云原生·kubernetes·云计算·devops
我不会起名字3224 小时前
K8s Pod 反复重启 CrashLoopBackOff:从 exit code 到根因的 5 步排查
云原生·容器·kubernetes·k8s
我不会起名字3225 小时前
K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序
运维·kubernetes·k8s·diskpressure·evicted
wzq11_6661 天前
Kubernetes集群——存储管理
云原生·容器·kubernetes
闲云自留地2 天前
K8s 的记忆大脑!ETCD 集群部署、etcdctl 命令与备份恢复全通关
容器·kubernetes·etcd
weirong2 天前
Operator 上生产前补的 6 件事,和 5 件我明确不做的
kubernetes
孙克旭_2 天前
k8s基础组件与创建资源
运维·docker·容器·kubernetes
吴声子夜歌3 天前
Kubernetes权威指南——网络原理(一)
网络·容器·kubernetes
无言听风雨丶3 天前
Kubernetes Service Mesh 与 mTLS 实战:从网络可达走向零信任服务通信
网络·kubernetes·service_mesh