先说明一下,Kubernetes 1.30在本文写作时尚未正式发布(预计2024年4月发布),目前只有alpha/beta版本。本文基于K8s 1.30的已公布新特性和KEP来写,一些API可能会在正式版中调整。生产环境建议等正式版发布后再升级。
Kubernetes每个版本都会带来一些新特性和改进,1.30也不例外。虽然1.30不是一个颠覆性的版本,但有一些新特性和废弃的API,给了我们重构老旧代码的好机会。
很多项目里的K8s相关代码,因为历史原因,写得比较啰嗦、复杂、难以维护,用的还是旧版本的API和模式。K8s 1.30的新特性,加上一些最佳实践,可以让我们把这些烂代码重构成优雅代码。
这篇文章就来聊聊,如何基于K8s 1.30+的新特性和最佳实践,重构K8s相关的代码。
K8s 1.30的重要新特性
在讲重构之前,先快速了解一下K8s 1.30的重要新特性和变化。
结构性特性(Structured Authorization Configuration)GA
结构性授权配置在1.29引入了beta,1.30正式GA了。这个特性允许你用结构化的配置文件来定义授权策略,支持CEL(Common Expression Language)表达式,比传统的ABAC更灵活、更强大。
传统的K8s授权方式是RBAC,虽然功能强大,但对于复杂的授权策略,写起来很麻烦,而且不够灵活。结构性授权配置可以用CEL表达式来定义复杂的授权规则,比如"只有在工作时间内,且来自公司IP的请求,才能修改生产环境的资源"。
CEL for Admission Control GA
CEL准入控制在1.30也正式GA了。ValidatingAdmissionPolicy(VAP)允许你用CEL表达式写准入控制规则,不需要写Webhook。
传统的准入控制需要写Webhook,部署一个服务,配置证书,很麻烦。用VAP,直接写YAML定义规则,K8s自动执行,简单高效。比如"所有Pod必须设置resources.limits"、"禁止使用latest标签"、"Service的type不能是LoadBalancer除非有特定注解",这些规则都可以用VAP轻松实现。
Pod调度就绪(Pod Scheduling Readiness)GA
Pod调度就绪特性在1.30 GA了。它允许你标记一个Pod为"未就绪调度",K8s调度器会跳过它,直到你把它标记为就绪。
这个特性对于批量任务、作业调度很有用。比如你有一批Job要提交,但想等所有Job都提交完了再一起调度,就可以先把它们标记为未就绪,全部提交完之后再统一标记为就绪。
侧车容器(Sidecar Containers)beta
侧车容器在1.29引入了alpha,1.30升级到beta。它允许你在Pod的init containers中定义一个sidecar容器,这个容器会在整个Pod生命周期中运行,和主容器一起启动,一起停止。
传统的sidecar容器是放在普通containers里的,但这样会有一些问题,比如Job完成后sidecar容器还在运行导致Job不结束。用init containers里的sidecar容器,K8s会正确管理它的生命周期,解决了这些问题。
镜像拉取策略改进
K8s 1.30改进了镜像拉取策略,新增了IfNotPresent的一些优化,还有镜像拉取的可观测性提升。kubelet的镜像拉取日志更详细了,方便排查镜像拉取问题。
一些API的废弃和移除
K8s 1.30移除了一些长期废弃的API,比如flowcontrol.apiserver.k8s.io/v1beta2的FlowSchema和PriorityLevelConfiguration,还有一些旧版本的CRD相关API。升级到1.30之前,要检查代码里有没有用这些废弃的API,及时迁移。
kubectl的改进
kubectl在1.30也有一些改进,比如kubectl apply的性能优化,kubectl debug的增强,kubectl events的改进等。这些改进能提升日常操作的效率。
重构案例一:用ValidatingAdmissionPolicy替代Webhook
第一个重构案例,用VAP替代传统的准入控制Webhook。
这是一个常见的准入控制Webhook代码,用Go写的,用来校验Pod必须设置resources.limits:
// webhook.go
func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
pod := &corev1.Pod{}
if err := json.Unmarshal(req.Object.Raw, pod); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
for _, container := range pod.Spec.Containers {
if container.Resources.Limits == nil || container.Resources.Limits.Cpu().IsZero() {
return admission.Denied(fmt.Sprintf("容器 %s 必须设置 CPU limits", container.Name))
}
if container.Resources.Limits.Memory().IsZero() {
return admission.Denied(fmt.Sprintf("容器 %s 必须设置内存 limits", container.Name))
}
}
for _, container := range pod.Spec.InitContainers {
if container.Resources.Limits == nil || container.Resources.Limits.Cpu().IsZero() {
return admission.Denied(fmt.Sprintf("init容器 %s 必须设置 CPU limits", container.Name))
}
}
return admission.Allowed("校验通过")
}除了这段代码,还需要:
- 写HTTP服务器,处理TLS
- 部署Webhook服务
- 配置ValidatingWebhookConfiguration
- 管理证书(cert-manager或者手动签发)
- 维护Webhook服务的可用性和扩展性
整个方案很复杂,代码量大,运维成本高。
用K8s 1.30的ValidatingAdmissionPolicy重构:
# policy.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-resource-limits
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE", "UPDATE"]
validations:
- expression: "object.spec.containers.all(c, has(c.resources.limits) && has(c.resources.limits.cpu) && has(c.resources.limits.memory))"
message: "所有容器必须设置 CPU 和内存 limits"
- expression: "object.spec.initContainers.all(c, !has(c.resources.limits) || (has(c.resources.limits.cpu) && has(c.resources.limits.memory)))"
message: "init容器如果设置了resources,必须包含 CPU 和内存 limits"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-resource-limits-binding
spec:
policyName: require-resource-limits
validationActions: [Deny]
matchResources:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system"]重构后的改进:
- 不需要写Go代码,不需要部署Webhook服务,不需要管理证书,大大降低了复杂度和运维成本。
- 用CEL表达式定义规则,简洁明了,可读性强。
- 性能更好,VAP在API Server内部执行,没有网络开销,比Webhook快。
- 更容易维护和版本控制,规则就是YAML文件,可以用Git管理。
当然,VAP也有局限性,比如不能做复杂的外部调用(查数据库、调外部API),对于这些场景还是需要Webhook。但对于大多数简单的校验规则,VAP完全够用了。
重构案例二:用结构性授权配置替代复杂的RBAC
第二个重构案例,用结构性授权配置替代复杂的RBAC规则。
很多项目里,RBAC规则写得很复杂,大量的Role和RoleBinding,维护起来很麻烦。比如有一个需求:开发人员只能在工作时间(9:00-18:00)修改dev命名空间的Deployment,其他时间只能读。
用RBAC实现这个需求几乎不可能,因为RBAC不支持时间条件。只能用Webhook来做,很复杂。
用K8s 1.30的结构性授权配置实现:
# authorization-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AuthorizationConfiguration
authorizers:
- type: RBAC
- type: Webhook
webhook:
subjectAccessReviewVersion: v1
# 传统的Webhook授权
- type: CEL
cel:
- expression: |
// 开发人员在工作时间可以修改dev命名空间的Deployment
user.groups.exists(g, g == 'developers') &&
request.namespace == 'dev' &&
request.resource.resource == 'deployments' &&
request.verb in ['create', 'update', 'patch'] &&
time.now().getHours('Asia/Shanghai') >= 9 &&
time.now().getHours('Asia/Shanghai') < 18
state:
# 可以定义状态变量
- expression: |
// 其他情况拒绝
false不过需要说明的是,结构性授权配置目前主要是和Webhook配合使用,CEL授权的能力还在发展中。对于大多数场景,RBAC还是主力,但对于需要复杂条件(时间、来源IP、请求属性等)的授权场景,结构性授权配置是一个很好的补充。
重构的思路是:把简单的、静态的权限用RBAC管理,把复杂的、动态的权限用结构性授权配置管理,两者结合,既保持了RBAC的简单高效,又获得了复杂条件的灵活性。
重构案例三:用Sidecar容器重构Job的sidecar
第三个重构案例,用K8s 1.30的Sidecar Containers特性,重构Job中的sidecar容器。
这是一个常见的Job配置,主容器跑数据处理,sidecar容器跑日志收集(比如fluent-bit):
apiVersion: batch/v1
kind: Job
metadata:
name: data-processor
spec:
template:
spec:
containers:
- name: processor
image: data-processor:latest
# 主容器处理完数据就退出
- name: log-collector
image: fluent-bit:latest
# sidecar收集日志,不会主动退出
restartPolicy: Never这个配置的问题:主容器处理完数据退出了,但sidecar容器还在运行,导致Job不会完成,一直处于Running状态。需要写额外的逻辑,让主容器完成后通知sidecar退出,很麻烦。
用K8s 1.30的Sidecar Containers重构:
apiVersion: batch/v1
kind: Job
metadata:
name: data-processor
spec:
template:
spec:
initContainers:
- name: log-collector
image: fluent-bit:latest
# 标记为sidecar容器,会在整个Pod生命周期运行
# 主容器完成后,sidecar会被自动终止
restartPolicy: Always
containers:
- name: processor
image: data-processor:latest
restartPolicy: Never注意:在K8s 1.30中,sidecar容器是定义在initContainers里的,设置restartPolicy: Always。K8s会把它当作sidecar来管理,在主容器启动之前启动,在所有主容器完成之后终止。
重构后的改进:
- 不需要写额外的逻辑来终止sidecar,K8s自动管理生命周期。
- Job能正确完成,主容器退出后sidecar也会被终止。
- sidecar容器的启动顺序有保证,在主容器之前启动,确保日志收集从一开始就生效。
- 语义更清晰,一看就知道这个容器是sidecar。
重构案例四:用Pod Scheduling Readiness重构批量调度
第四个重构案例,用Pod Scheduling Readiness重构批量任务的调度。
场景:有100个训练任务要提交,想等所有任务都提交完了再一起调度,避免部分任务先占用资源,导致后面的任务资源不够。
传统的做法:用一个调度器扩展,或者用优先级队列,很复杂。或者先提交到一个暂停的命名空间,等全部提交完再取消暂停,也很麻烦。
用K8s 1.30的Pod Scheduling Readiness重构:
# 提交任务时,标记为未就绪调度
apiVersion: v1
kind: Pod
metadata:
name: training-task-1
labels:
scheduling-ready: "false"
spec:
schedulingGates:
- name: scheduling-ready
containers:
- name: trainer
image: training:latest所有100个任务都提交之后,用一个命令统一移除schedulingGates,让它们都可以被调度:
# 移除所有任务的调度门控
kubectl label pods -l app=training-task scheduling-ready- --all
# 或者用patch移除schedulingGates
kubectl get pods -l app=training-task -o json | jq '.items[].spec.schedulingGates = null' | kubectl apply -f -重构后的改进:
- 不需要额外的调度器或复杂的逻辑,K8s原生支持。
- 所有任务同时进入调度队列,调度器能做全局最优的资源分配。
- 可以灵活控制什么时候开始调度,比如等资源准备好、等数据加载完、等所有任务都提交完。
重构案例五:清理废弃API
第五个重构案例,清理K8s 1.30中废弃和移除的API。
K8s 1.30移除了一些长期废弃的API,如果你的代码里还在用,升级到1.30之后会出问题。常见的需要清理的API:
- flowcontrol.apiserver.k8s.io/v1beta2的FlowSchema和PriorityLevelConfiguration,要迁移到v1或v1beta3。
旧代码:
apiVersion: flowcontrol.apiserver.k8s.io/v1beta2
kind: FlowSchema
metadata:
name: my-flow-schema
spec:
# ...新代码:
apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: FlowSchema
metadata:
name: my-flow-schema
spec:
# ...- apiextensions.k8s.io/v1beta1的CRD,要迁移到v1。v1beta1在很久以前就废弃了,1.30可能会有更严格的要求。
- 一些旧版本的RBAC API,比如rbac.authorization.k8s.io/v1beta1,要迁移到v1。
- client-go的旧版本,如果你用Go写K8s相关代码,要升级client-go到支持1.30的版本,清理掉使用废弃API的代码。
清理废弃API的步骤:
- 用kubectl api-resources检查集群里有没有用废弃API的资源。
- 用kubectl find检查代码和YAML文件里有没有废弃API。
- 逐个迁移到新的API版本,测试验证。
- 升级集群到1.30。
重构案例六:用kubectl新特性优化运维脚本
第六个重构案例,用K8s 1.30的kubectl新特性,优化运维脚本。
很多项目里有大量的运维脚本,用kubectl做各种操作,代码写得很啰嗦,效率也不高。K8s 1.30的kubectl改进,能让这些脚本更简洁高效。
比如,用kubectl events替代传统的kubectl get events:
旧脚本:
# 查看Pod的事件,需要排序和过滤
kubectl get events --field-selector involvedObject.name=my-pod --sort-by=.lastTimestamp新脚本(kubectl events是1.30增强的):
# kubectl events专门用来查看事件,更友好
kubectl events --for pod/my-pod比如,用kubectl debug的增强功能调试Pod:
旧方式:需要手动创建调试容器,配置网络和PID命名空间,很麻烦。
新方式(kubectl debug在1.30增强了):
# 直接调试运行中的Pod,自动创建调试容器
kubectl debug my-pod -it --image=busybox --target=app-container
# 调试节点
kubectl debug node/my-node -it --image=busybox
# 复制Pod进行调试,不影响原Pod
kubectl debug my-pod -it --copy-to=my-pod-debug --container=debug --image=busybox比如,用kubectl apply的性能优化,批量应用大量YAML文件:
旧方式:一个一个apply,慢。 新方式:kubectl apply在1.30优化了批量处理性能,大量YAML文件apply更快。
重构的最佳实践
基于K8s 1.30重构代码,有一些最佳实践。
第一,先升级测试环境,再升级生产。不要直接在生产环境升级到1.30,先在测试环境验证所有功能正常,特别是用了新特性和废弃API的部分。
第二,渐进式重构,不要一次性全改。K8s的新特性和旧代码是兼容的,可以一个模块一个模块地重构,先重构最复杂、最需要改进的部分。
第三,充分测试。重构可能会引入bug,特别是准入控制、授权、调度这些核心功能,重构之后要充分测试,确保功能正常,没有安全漏洞。
第四,关注废弃API。升级之前,一定要检查所有代码和YAML文件里有没有用废弃API,及时迁移。K8s每个版本都会废弃一些API,不及时迁移,升级后会出问题。
第五,文档和培训。新特性需要团队成员学习和适应。重构之后,要更新文档,组织培训,确保团队成员理解新的代码和工作方式。
第六,保留回滚方案。重构之后,如果出问题,要能快速回滚。用Git管理代码,用蓝绿部署或金丝雀发布,确保重构的风险可控。
K8s代码质量的通用建议
除了K8s 1.30的新特性,还有一些通用的K8s代码质量建议,适用于所有版本。
第一,用Helm或Kustomize管理YAML。不要手写大量重复的YAML,用模板工具管理,减少重复,提高可维护性。
第二,用命名空间和标签做隔离和组织。合理使用命名空间隔离环境和团队,用标签做资源分类和选择,不要用名称来区分。
第三,设置合理的resources和limits。所有Pod都要设置requests和limits,避免资源争抢和OOM。
第四,用探针做健康检查。所有服务都要配置livenessProbe和readinessProbe,确保K8s能正确管理Pod的生命周期。
第五,用ConfigMap和Secret管理配置。不要把配置硬编码在镜像里,用ConfigMap管理非敏感配置,用Secret管理敏感配置。
第六,做好可观测性。给所有服务配置日志、指标、链路追踪,方便排查问题。
第七,用GitOps管理集群。用ArgoCD或Flux等GitOps工具,把集群状态存在Git里,通过Git提交来管理集群,提高可追溯性和可恢复性。
写在最后
Kubernetes 1.30虽然不是一个颠覆性的版本,但它带来的新特性和改进,给了我们重构老旧代码的好机会。ValidatingAdmissionPolicy、Sidecar Containers、Pod Scheduling Readiness、结构性授权配置等新特性,能让我们的代码更简洁、更优雅、更易维护。
但重构不是目的,提升代码质量和可维护性才是目的。不要为了用新特性而用新特性,要根据实际情况,选择合适的特性来重构。有些旧代码虽然老,但如果运行稳定、易于维护,也不一定需要重构。
Kubernetes在快速发展,每个版本都有新特性和改进。作为开发者,要保持学习,跟上技术的发展,用最新的、最好的方式来写代码。但也不要盲目追新,要根据项目的实际情况,选择合适的技术和方案。
希望这篇文章能帮你更好地理解K8s 1.30的新特性,并用它们来提升你的代码质量。有什么问题欢迎交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录