root 发布的文章

Operator 是把运维知识写成代码的方式。本文用一个最小例子讲清楚怎么开发。

一个例子:自动备份 PVC

需求:定义一个 PVCBackup 资源,自动给 PVC 每小时打快照存到 S3。

1. 定义 CRD

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: pvcbackups.storage.example.com
spec:
  group: storage.example.com
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              pvcName:
                type: string
              schedule:
                type: string
              s3Bucket:
                type: string
            required: [pvcName, schedule, s3Bucket]
          status:
            type: object
            properties:
              lastBackup:
                type: string
              nextBackup:
                type: string
  scope: Namespaced
  names:
    plural: pvcbackups
    singular: pvcbackup
    kind: PVCBackup

2. 用 kubebuilder 生成脚手架

kubebuilder init --domain example.com --repo pvcbackup-operator
kubebuilder create api --group storage --version v1 --kind PVCBackup

生成 controllers/pvcbackup_controller.go

3. 实现 Reconcile

func (r *PVCBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)

    // 1. 拿到对象
    var backup storagev1.PVCBackup
    if err := r.Get(ctx, req.NamespacedName, &backup); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 2. 检查 PVC 是否存在
    var pvc corev1.PersistentVolumeClaim
    if err := r.Get(ctx, types.NamespacedName{Name: backup.Spec.PVCName, Namespace: req.Namespace}, &pvc); err != nil {
        log.Error(err, "PVC not found")
        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
    }

    // 3. 判断是否到了下次备份时间
    if backup.Status.NextBackup != "" {
        nextTime, _ := time.Parse(time.RFC3339, backup.Status.NextBackup)
        if time.Now().Before(nextTime) {
            // 等到下次时间再 reconcile
            return ctrl.Result{RequeueAfter: time.Until(nextTime)}, nil
        }
    }

    // 4. 执行备份(创建 VolumeSnapshot + 上传 S3)
    if err := r.doBackup(ctx, &backup, &pvc); err != nil {
        return ctrl.Result{RequeueAfter: 5 * time.Minute}, err
    }

    // 5. 更新 status
    backup.Status.LastBackup = time.Now().Format(time.RFC3339)
    next, _ := cron.ParseStandard(backup.Spec.Schedule)
    backup.Status.NextBackup = next.Next(time.Now()).Format(time.RFC3339)
    return ctrl.Result{RequeueAfter: time.Until(next.Next(time.Now()))}, r.Status().Update(ctx, &backup)
}

4. Reconcile 的几条铁律

  1. 幂等:同一个对象 Reconcile 10 次结果应一样
  2. 不要在 Reconcile 里 sleep:用 RequeueAfter 让 controller-runtime 自己调度
  3. status 单独 update:spec 和 status 是不同的 subresource
  4. 错误处理分类:临时错误 RequeueAfter,永久错误记到 status 别 retry
  5. finalizer 处理删除:要清理外部资源(如 S3 上的备份)就加 finalizer

5. 监控 Operator

Operator 也是普通 Pod,加 Prometheus metrics:

import "sigs.k8s.io/controller-runtime/pkg/metrics"

var (
    reconcileCounter = prometheus.NewCounter(...)
)
func init() {
    metrics.Registry.MustRegister(reconcileCounter)
}

关键指标:
- controller_runtime_reconcile_total:reconcile 次数
- controller_runtime_reconcile_errors_total:错误次数
- workqueue_depth:队列深度(大了说明 reconcile 跟不上)

6. 部署

make docker-build docker-push IMG=registry/pvcbackup:v1
make deploy IMG=registry/pvcbackup:v1

一些反模式

  • ❌ 在 Reconcile 里写状态机:用 phase 字段记当前状态
  • ❌ 跨对象事务:K8s 没事务,要设计成幂等的
  • ❌ 调用外部 API 不超时:必加 context timeout

教训:Operator 难的不是写代码,是把"运维知识"翻译成"幂等的 Reconcile 逻辑"。

OpenStack 经典面试题"nova boot 失败怎么排查",但生产环境的失败往往更狡猾,因为可能是任何子系统的错。本文是我处理过的 7 类典型根因,按发生频率排序。

0. 万能起手式

openstack server show <uuid>
openstack server event list <uuid>
openstack server event show <uuid> <req-id>

event show 里的 traceback 是定位关键,一定先看这个再去翻日志。

如果 event 信息不够,按子系统翻:

# nova-compute(最常用)
tail -200 /var/log/nova/nova-compute.log | grep -i "ERROR\|TRACE"

# nova-scheduler(调度失败 NoValidHost)
tail -200 /var/log/nova/nova-scheduler.log

# neutron-server(网络绑定失败)
tail -200 /var/log/neutron/neutron-server.log

1. NoValidHost — 调度找不到节点

最常见。可能原因:

  • 资源不够openstack hypervisor list --long 看 CPU/RAM 剩余
  • 超分被打满cpu_allocation_ratio / ram_allocation_ratio 已经被吃满
  • filter 把节点排除了AvailabilityZoneFilter / AggregateInstanceExtraSpecsFilter 经常是元凶
  • PCI 设备/SR-IOV 不匹配

排查:scheduler 日志开 DEBUG,看 Filter X returned 0 hosts

2. PortBindingFailed — 端口绑定不到节点

nova-compute.log: PortBindingFailed: Binding failed for port xxx

90% 是 OVS agent / Linuxbridge agent 在目标节点上挂了,或者 bridge_mappings 没配对:

openstack network agent list
# 重点看 alive=True / state=UP

另一个坑:vnic_type=direct(SR-IOV)但目标节点没 sriov-agent。

3. 镜像问题

  • 镜像格式不对qcow2 但 backend 是 LVM,需要转换
  • 镜像 size 没设:从 Glance 拉下来还要校验,size 不对会 hang
  • HTTPS 自签证书:nova-compute 拉镜像时 SSL 校验失败

排查:glance image-show <uuid>checksumstatus=active

4. Cinder 挂卷超时

带 volume boot 的 instance 经常这里出事:

Block Device Mapping is Invalid
  • iSCSI/RBD 挂载失败:去 cinder-volume.log
  • 卷状态 errorcreating 卡住:openstack volume showattachments
  • 多路径配置缺失:/etc/multipath.conf 没配

5. DHCP / metadata 拿不到

虚机起来了,但进系统后 IP 拿不到、SSH key 没注入:

  • DHCP agent 死了neutron-dhcp-agent 状态
  • metadata-agent 路径不通:检查 neutron-ns-metadata-proxy 进程
  • config_drive 没启用:对老镜像或者 cloud-init 没装的,建议强制 config_drive=True

6. Libvirt / Hypervisor 层

罕见但很烦:

  • KVM 模块没加载(modprobe kvm_intel / kvm_amd
  • CPU model 不兼容(迁移过来的虚机,目标节点 CPU flag 不够)
  • qemu-kvm 版本和 libvirt 版本不匹配

定位看 /var/log/libvirt/qemu/<instance>.log,最直接。

7. quota 满了

QuotaError: Quota exceeded for cores

简单粗暴:

openstack quota show <project>
openstack quota set --cores 200 <project>

但注意 quota 是 per-project 的,admin 看的是自己 project 的 quota,不是用户的。

我自己的 checklist

新环境出问题,按这个顺序看,95% 五分钟内定位:

  1. server event show 抓 traceback
  2. 看是不是调度问题(NoValidHost)→ scheduler 日志
  3. 看 hypervisor 资源水位 → hypervisor list --long
  4. 看 neutron agent 是不是都活着
  5. 看 nova-compute 日志最近 1 分钟的 ERROR
  6. 看 libvirt qemu 日志(最后兜底)

教训一句话:OpenStack 出问题 80% 在 neutron 和 scheduler,先排这两个能省 80% 的时间。

K8s 给多团队 / 多业务用,怎么隔离?方案从弱到强排列。

L1:Namespace 隔离(最弱)

每个团队一个 namespace + RBAC:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-a-admin
  namespace: team-a
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: admin
subjects:
- kind: Group
  name: team-a

隔离的:API 权限。
不隔离的:网络(所有 Pod 互通)、节点(混合调度)、资源(无限抢)。

适合:内部团队、互相信任的场景。

L2:加 NetworkPolicy + ResourceQuota

# 资源配额
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-a-quota
  namespace: team-a
spec:
  hard:
    requests.cpu: "100"
    requests.memory: 200Gi
    limits.cpu: "200"
    persistentvolumeclaims: "20"

---
# 默认拒绝所有外部流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: team-a
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]

加 NetworkPolicy 后跨 namespace 默认不通,加 ResourceQuota 后无法抢资源。

适合:公司内部多业务、生产级隔离。

L3:节点池隔离

不同租户跑在不同节点上:

# 节点打 taint
kubectl taint nodes worker-team-a tenant=team-a:NoSchedule

# 业务 Pod 加 toleration + nodeSelector
spec:
  nodeSelector:
    tenant: team-a
  tolerations:
  - key: tenant
    operator: Equal
    value: team-a
    effect: NoSchedule

物理隔离,避免 noisy neighbor。但成本上升(每个团队都要预留节点)。

适合:金融、政企等强合规场景。

L4:vCluster(虚拟集群)

每个租户一个虚拟 K8s 集群,跑在 host 集群的 namespace 里:

helm install vcluster-team-a vcluster --namespace team-a

每个租户看到的是独立 API server、独立 etcd(轻量),但底层共享 host 节点。

适合:SaaS 平台、PaaS 服务。

L5:独立集群

最强隔离 = 一租户一集群。

适合:完全不能共享的场景(多家公司、跨地域)。

决策树

跨公司租户? → 独立集群
跨业务(同一公司),强合规? → 节点池隔离
内部团队,要看似独立的 K8s? → vCluster
内部团队,控制资源 + 网络就够? → Namespace + Quota + NetworkPolicy
完全信任,简单分组? → Namespace + RBAC

Quota 设置的坑

ResourceQuota 一旦设置,namespace 里所有 Pod 必须设 requests/limits,否则创建失败。配合 LimitRange 设默认值:

apiVersion: v1
kind: LimitRange
metadata:
  name: team-a-defaults
  namespace: team-a
spec:
  limits:
  - default:
      cpu: 500m
      memory: 512Mi
    defaultRequest:
      cpu: 100m
      memory: 128Mi
    type: Container

教训:多租户隔离不是越强越好——节点池隔离用户体验下降明显,能用 Quota 解决就别上节点池。

K8s 一个季度一个版本,升级是个常态。本文按 kubeadm 跨 3 个版本的实战记录。

跨版本规则

K8s 官方只支持 N→N+1 升级。要跨多版本就要"逐级跳":

1.27 → 1.28 → 1.29 → 1.30

每跳一次必须等集群稳定(一天以上),不能连续跳。

单步升级流程

以 1.27 → 1.28 为例。

1. 升 control plane(第一个 master)

# 1. 升 kubeadm
dnf install -y kubeadm-1.28.0-0 --disableexcludes=kubernetes

# 2. 看升级计划
kubeadm upgrade plan

# 3. 真正升级
kubeadm upgrade apply v1.28.0

# 4. drain 节点
kubectl drain master1 --ignore-daemonsets

# 5. 升 kubelet/kubectl
dnf install -y kubelet-1.28.0-0 kubectl-1.28.0-0
systemctl daemon-reload
systemctl restart kubelet

# 6. uncordon
kubectl uncordon master1

2. 升其他 master

# 在 master2、master3 上
kubeadm upgrade node    # 注意是 node 不是 apply
# 然后同上升 kubelet

3. 升 worker

逐个节点:

# 控制面执行
kubectl drain worker1 --ignore-daemonsets --delete-emptydir-data

# 节点上执行
dnf install -y kubeadm-1.28.0-0 kubelet-1.28.0-0
kubeadm upgrade node
systemctl restart kubelet

# 控制面执行
kubectl uncordon worker1

必看 release notes 的坑

跨版本最容易翻车的点:

1.28 砍了

  • --feature-gates 里的 GA feature 不再接受(会报 invalid feature gate)
  • in-tree GCE/AWS/Azure/vSphere 存储插件全部移到 CSI

1.29 砍了

  • Secret 默认开启 SecretImmutability
  • KubeProxyConfiguration v1alpha1 弃用
  • Pod 安全准入策略对照 v1.29 检查

1.30 砍了

  • app.kubernetes.io/component 一些保留值变化
  • CRI v1alpha2 完全删除,runtime 必须支持 v1

CNI 兼容性

CNI 插件也要升:

  • Calico:跨版本 K8s 升级前先升 Calico 到对应版本
  • Cilium:同上
  • 自研 CNI:每次升前测一遍
# 查当前 CNI 版本
kubectl get daemonset -n kube-system -l k8s-app=calico-node -o jsonpath='{.items[0].spec.template.spec.containers[0].image}'

回滚

K8s 不支持降级!只能:
- 备份 etcd
- 失败就用 etcd 备份恢复,然后从快照重启

我的升级 checklist

  1. ☐ 备份 etcd(强制)
  2. ☐ 看 release notes(关键)
  3. ☐ staging 演练过(必须)
  4. ☐ kubelet 配置文件检查兼容性
  5. ☐ CRI runtime 版本检查
  6. ☐ 准备 PodDisruptionBudget(drain 才不会断业务)
  7. ☐ 一台一台升,每台升完观察 10 分钟

教训:跨版本升级出问题最常见的是 webhook(如 cert-manager、ingress-nginx)和 CNI 不兼容,新版本一定先升 webhook 再升 K8s。

OSD down 是 Ceph 运维最常见的事件。新手最容易犯的错是看到 down 就立刻 ceph osd out,结果触发数据迁移,等磁盘其实自己能恢复,白白搬一遍数据。本文是我团队内部用的决策树。

一图先行(文字版)

OSD down
  ├─ down 时长 < 10 分钟?
  │   └─ 是 → 等 mon_osd_down_out_interval 自动处理,先别动
  │
  ├─ 节点是否在线?(ping / ssh)
  │   ├─ 节点离线 → 是计划维护吗?
  │   │     ├─ 是 → noout + 维护,完事 unset noout
  │   │     └─ 否 → 排查节点
  │   └─ 节点在线 → 进入磁盘排查
  │
  ├─ 磁盘 SMART 是否正常?
  │   ├─ 异常 → destroy & 换盘
  │   └─ 正常 → 看 OSD 日志,可能是 OOM / 段错误 / 文件系统问题

几个关键参数搞清楚

  • mon_osd_down_out_interval:OSD down 多久自动 out(默认 600s)
  • noout:禁止 OSD 自动 out(维护必用)
  • nobackfill / norecover:禁止回填/恢复
  • noscrub / nodeep-scrub:禁止 scrub

维护场景三连:

ceph osd set noout
ceph osd set nobackfill
ceph osd set norecover
# 维护...
ceph osd unset norecover
ceph osd unset nobackfill
ceph osd unset noout

out vs destroy vs purge

操作 作用 数据迁移 OSD ID 保留
ceph osd out X 标记为不参与数据分布 触发
ceph osd destroy X 标记销毁,清除认证密钥 不触发
ceph osd purge X 彻底删除 不触发

实战决策

  • 短暂故障要恢复 → 啥都不做,等它自己 up
  • 换盘但想复用 OSD ID(推荐,少一次 CRUSH 抖动)→ destroy → 换盘 → ceph-volume lvm create --osd-id X
  • 彻底下线一块盘 → out → 等回填 → purge

一个真实踩坑

一台机器 4 块 OSD 全 down,我同事下意识 ceph osd out 了 4 个 ID。结果触发大量数据迁移,集群 IO 抖动 1 小时。其实那台机器只是网卡 down 了,5 分钟修好。

正确做法:发现整节点掉了,先 ceph osd set noout,去现场查节点,节点恢复后 OSD 自己 up 就行了,零迁移。

让运维少出事的几个习惯

  1. 节点维护前永远 noout:写进 SOP,没商量
  2. 批量换盘别同时换:一次只换一块,等回填完了再下一块
  3. 集群配 mon_max_pg_per_osd_hard_limit:避免大规模迁移把单个 OSD 撑爆
  4. 死磁盘 destroy 而不是 purge:保留 ID,重建后 CRUSH 拓扑不变,少一轮全盘扫描

教训一句话:OSD down 先别 out,10 分钟内的故障往往是网络或者 OSD 进程的事,不是磁盘的事。