Kubebuilder/K8s Controller 实践总结

Monday, June 2, 2025

TOC

一、项目概览

1.1 业务场景

目前公司整体由两个日志采集方案,一个是与微服务框架紧密耦合的,适合微服务场景,能将日志通过RPC方式发送到日志平台,缺点是需要入侵应用代码,也存在一些性能和稳定性问题。 另一个是基于Filebeat的方案,适合传统应用场景,能将日志文件直接采集到日志平台,缺点是配置复杂,整个配置链条较长容易出错,且缺少可观测性手段,出问题时排查难度大。另外当前Filebeat是基于ssh方式安装和配置变更的,容器日志通过挂载到宿主机的方式采集,容器化场景支持不友好(比如无法采集POD标准输出的日志)。

于是我们开发了一个基于Kubernetes Operator的日志采集方案,旨在简化配置、提高可观测性,并支持容器化场景。

1.2 简单回顾Kubebuilder/K8s Controller的原理

Kubebuilder是一个用于构建Kubernetes原生API扩展(即Operator/Controller)的开发框架。其核心思想是通过自定义资源(CRD)和控制器(Controller)实现对Kubernetes资源的自动化管理。

  • CRD(CustomResourceDefinition):允许用户扩展Kubernetes API,定义新的资源类型(如buzzlog)。
  • Controller:通过watch机制监听资源的变化事件(如创建、更新、删除),并在Reconcile循环中对比期望状态(Spec)和实际状态(Status),自动驱动集群状态向期望状态收敛。
  • 工作流程:当用户创建或修改自定义资源时,Controller会收到事件通知,执行Reconcile逻辑,必要时调用外部API或更新其他Kubernetes资源,最终更新Status反馈处理结果(如果有的话)。
  • 幂等性:Reconcile逻辑必须幂等,保证多次执行结果一致。(要求幂等的原因是因为Kubernetes的事件通知机制可能会多次触发同一事件)

Kubebuilder提供了项目脚手架、代码生成、测试等工具,极大提升了Operator的开发效率和规范性。官方的quickstartarchitecture更完整地介绍了其设计理念和使用方法。这里我们就不再赘述。下面基于一个创建好的Kubebuilder项目来分析其项目结构和核心实现。

二、Kubebuilder项目结构分析

使用Kubebuilder创建的项目结构遵循一定的规范,便于管理和扩展。以下是controller项目的标准结构。

beats-on-k8s/
├── api/v1/                     # CRD定义
│   ├── buzzlog_types.go       # 自定义资源类型定义
│   ├── groupversion_info.go   # API版本信息
│   └── zz_generated.deepcopy.go # 自动生成的深拷贝代码
├── cmd/
│   └── main.go                # 程序入口点
├── config/                    # Kubernetes配置文件
│   ├── crd/                   # CRD配置
│   ├── rbac/                  # 权限配置
│   ├── manager/               # Manager配置
│   └── samples/               # 示例配置
├── internal/
│   ├── controller/            # 控制器逻辑
│   └── pkg/                   # 业务逻辑包
├── Dockerfile                 # 容器镜像构建
├── Makefile                   # 构建和部署脚本
└── go.mod                     # Go模块依赖

关键文件包括:

  • api/v1/buzzlog_types.go: 定义CRD结构,包括Spec和Status,修改定义后需要运行make generate重新生成CRD配置
  • controller/buzzlog_controller.go: 核心控制器逻辑,实现Reconcile逻辑,是主要开发的地方
  • cmd/main.go: 程序启动入口,初始化Manager和Controller,这里能看到一些重要的配置,比如Leader Election、Metrics等。

三、自定义资源定义(CRD)设计

buzzlog CRD结构分析

首先,按照K8S Controller的设计习惯,我们定义一个名为buzzlog的自定义资源(CRD),用于描述日志采集配置。以下是buzzlog CRD的基本结构:

/* Spec部分 */
type buzzlogSpec struct {
  // PodSelector用于选择日志采集目标Pod
  PodSelector metav1.LabelSelector `json:"podSelector,omitempty"`
  // ContainerSelector用于配置采集的细节
  Config      buzzlogConfig        `json:"config,omitempty"`
}

type buzzlogConfig struct {
  // App是应用名称,用于标识日志来源, 采集上来的日志会带上这个标签
  App                 string               `json:"app"`
  // Module和App类似,是更细化的应用分类
  Module              string               `json:"module"`
  // ContainerRegistries是容器级别的日志采集配置, 控制POD内每个容器的日志采集方式
  ContainerRegistries []ContainerRegistry  `json:"containerRegistries"`
}

type ContainerRegistry struct {
  // ContainerName是待采集的容器名称
  ContainerName string `json:"containerName"`
  // Tag是日志采集的标签,用于标识日志来源
  Tag string `json:"tag"`
  // Path是日志文件路径, 这里是容器内的路径, 应用上需要将其转化为宿主机路径
  Path string `json:"path"`
  // InputType是日志输入类型, 比如filestream(文件采集), container(容器标准输出)等
  InputType string `json:"inputType"`
  // 日志在线保存时间
  OnlineSaveTimeDays int `json:"onlineSaveTimeDays"`
  // 日志离线保存时间
  OfflineSaveTimeDays int `json:"offlineSaveTimeDays"`
  // etc... 一些其他配置
}

/* Status部分 */
type buzzlogStatus struct {
  // 当前资源的ObservedGeneration, 用于跟踪Spec的变更
  ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}

简单总结一下buzzlog CRD的设计思路:

  • 声明式API: Spec和Status分离,Spec描述期望状态,Status描述实际状态
  • 嵌套结构: 分层映射,支持复杂的配置需求
  • 标签选择器: 灵活匹配目标Pod,不入侵Pod资源配置
  • 容器级配置: 支持单个Pod内多容器的不同日志配置,单独配置每个容器的日志采集方式和路径

四、Controller核心逻辑实现

4.1 Reconcile循环设计

// internal/controller/buzzlog_controller.go
func (r *buzzlogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  // 1. 获取资源实例
  buzzlog := &buzzlogv1.buzzlog{}
  err := r.Get(ctx, req.NamespacedName, buzzlog)

  // 2. 处理资源删除(基于Finalizer机制)
  if controllerutil.ContainsFinalizer(buzzlog, buzzlogFinalizer) {
    logApi.DeleteLogConfig(ctx, buzzlog.Spec) // 调用外部API删除日志配置(幂等操作)
    // ... 处理删除逻辑
  }

  // 3. 处理资源创建/更新
  if !controllerutil.ContainsFinalizer(buzzlog, buzzlogFinalizer) {
    // 3.1 首次创建资源
    controllerutil.AddFinalizer(buzzlog, buzzlogFinalizer)
    logApi.CreateOrUpdateLogConfig(ctx, buzzlog.Spec) // 调用外部API创建日志配置(幂等操作)
  } else {
    // 3.2 资源更新(基于Generation/ObservedGeneration模式)
    if buzzlog.Generation != buzzlog.Status.ObservedGeneration {
      // 3.2.1 更新外部API
      err = logApi.CreateOrUpdateLogConfig(ctx, buzzlog.Spec)
      // 3.2.2 更新ObservedGeneration
      buzzlog.Status.ObservedGeneration = buzzlog.Generation
      err = r.Status().Update(ctx, buzzlog)
    }
  }
}

Finalizer模式 Finalizer是Kubernetes控制器中常用的一种机制,用于确保在资源被删除前,能够优雅地清理与该资源相关的外部依赖(如外部API、云资源等)。当资源被删除时,Kubernetes首先会将资源的deletionTimestamp标记为非空,但不会立即删除资源对象,而是等待控制器移除Finalizer字段。控制器在此期间可以执行必要的清理操作,完成后移除Finalizer,Kubernetes才会真正删除资源。这可以有效避免外部资源泄露或“脏数据”残留。

Generation/ObservedGeneration模式
Kubernetes的每个资源对象都有一个generation字段,每当Spec发生变更时,generation会自增。而Status中的observedGeneration字段用于记录控制器上次处理的generation。通过比较两者,控制器可以判断Spec是否发生了新的变更,只有在generation != observedGeneration时才会执行更新逻辑,处理完成后将observedGeneration更新为当前generation。这种模式可以避免重复处理相同的Spec,提高控制器的幂等性和性能。

结合Finalizer和Generation/ObservedGeneration模式,可以确保在资源删除时能够正确清理外部依赖,并且避免重复处理相同的资源变更(尤其是controller启动后的list全量资源操作)。

4.2 其他配置

// cmd/main.go
func main() {
  // 配置Leader选举
  flag.BoolVar(&enableLeaderElection, "leader-elect", false,
    "Enable leader election for controller manager. "+
      "Enabling this will ensure there is only one active controller manager.")
  flag.StringVar(&leaderElectionNS, "leader-election-namespace", "", "The namespace in which the leader election configmap will be created.")
  // 配置Metrics端口
  flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
    "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
  flag.BoolVar(&secureMetrics, "metrics-secure", true,
    "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
}

Leader Election 为了保证服务的高可用性,我们选择多副本部署Controller,并使用Leader Election机制来确保同一时间只有一个实例在处理Reconcile逻辑。通过配置--leader-elect参数启用选举机制,Kubernetes会在指定的命名空间下创建一个ConfigMap或Lease对象作为锁,只有获得锁的实例才会成为Leader。相关参数如--leader-election-namespace可指定锁的存放位置。

Prometheus Metrics 最后是对服务本身的监控指标配置,打开之后可以通过Prometheus监控Controller的运行状态和性能指标。或者添加自定义的指标。当然,这里只是暴露了一个http的metrics接口,如何实现controller节点的发现和指标拉取,是另一个话题,这里不做展开了。

五、就这?Filebeat呢?

这里我们只介绍了如何使用Kubebuilder/K8s Controller实现一个简单的日志采集配置管理器,核心是通过自定义资源和控制器实现对日志采集配置的声明式管理。准确一点说,我们的Controller并不直接采集日志,而仅仅是管理buzzLog这种自定义资源的创建、更新和删除,这更符合Kubernetes的设计理念。而更大的层面上,我们还将Filebeat以DaemonSet的方式部署到每一台宿主机上。Filebeat进程自己也会watch这些buzzLog以及所有pod资源的变化,当根据PodSelector选择到目标Pod后,Filebeat会根据buzzLog的配置来动态生成Filebeat配置文件,并将日志采集到日志平台。

Filebeat本身我们也做了一些定制化的开发,比如上面说的watch自定义资源buzzLog、以及动态地生成和解析配置文件、增强的日志采集功能等。但这就不在这里展开了,后续可以单独写一篇文章介绍Filebeat的定制化开发。