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的开发效率和规范性。官方的quickstart和architecture更完整地介绍了其设计理念和使用方法。这里我们就不再赘述。下面基于一个创建好的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的定制化开发。