prometheus-operator源码浅读

目标

大家都知道operator是用来自定义控制器来实现对复杂应用的管理,现在通过学习prometheus-operator 的源码,来加深对Opertaor的理解以及学习其代码是如何设计的。

如何使用prometheus-operator

完整prometheus监控系统

要使用前需要先安装opertaor,官方的安装文档如下
https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/getting-started/installation.md

文档中介绍了3中安装方法,分别是

  • Install using YAML files
  • Install using Kube-Prometheus
  • Install using Helm Chart

文档中说第二种方式是最简单,安装方法如下

1、下载指定版本的文件

git clone  --branch v0.14.0 --single-branch https://github.com/prometheus-operator/kube-prometheus.git

2、执行命令会执行下面的yaml文件创建CRD以及namespace

kubectl create -f manifests/setup

3、等待CRD创建完毕, 如果输出"No resources found" 表示CRD创建完毕

until kubectl get servicemonitors --all-namespaces ; do date; sleep 1; echo ""; done

4、继续执行命令,创建prometheus相关的CR以及operator

kubectl create -f manifests/

执行完命令后,k8s就会启动operator,然后就会根据CR去部署prometheus的各个组件。但是你去查看部署的情况的时候,会发现部分pod失败了,需要手动下载镜像

# 查看pod的启动情况
kubectl get pod -n monitoring

在这里插入图片描述

失败的镜像共3个:
grafana/grafana:11.2.0
registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.13.0
registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.12.0

白嫖github action + 阿里云仓库下载镜像后,再看pod的情况,已经全部弄好了
在这里插入图片描述

查看grafanaweb页面,默认的用户名密码是admin/admin

kubectl port-forward --address 0.0.0.0 -n monitoring svc/grafana 8005:3000 

可以看到grafana已经能够正常运行了
在这里插入图片描述

删除prometheus命令如下

kubectl delete --ignore-not-found=true -f manifests/ -f manifests/setup

operator文档上有更详细的说明
https://prometheus-operator.dev/docs/platform/platform-guide/

只部署operator

由于弄完上面整个prometheus之后才看到的,就不重复做了,记录下文档位置
https://github.com/prometheus-operator/prometheus-operator/blob/main/README.md
在这里插入图片描述

如何调试operator代码

上面部署 Prometheus 的原理是:首先创建 CRDNamespace,然后部署 Operator Pod,并创建一系列自定义资源(CR),例如 Prometheus Server 等。Operator Pod 启动后,从 API Server 获取这些 CR,并循环处理。其处理逻辑是根据 CR 的定义,创建和管理相应的 PodService 等资源。 当然并不是所有的Pod都是operator启动,还有一些直接是以Deployment的方式部署的,例如kube-state-metricsblackbox-exporter

开始准备调试工作前,执行下面的命令把已经启动了的pod以及创建了的CR删掉,方便后续调试operator

kubectl delete --ignore-not-found=true -f manifests/

mainfests目录关于operatoryaml如下,很明显重点关注的应该是这个deployment.yaml
在这里插入图片描述

deployment中启动了两个容器
在这里插入图片描述

kube-rbac-proxy 是一个 轻量级的反向代理(reverse proxy),用于为 Kubernetes 内部服务提供 RBAC(基于角色的访问控制,Role-Based Access Control) 认证。该镜像由 brancz 维护,并广泛用于 Prometheus 生态中的组件,如 Prometheus OperatorKube State MetricsNode Exporter,以确保只有经过授权的客户端才能访问相关的 metrics

默认情况下,Prometheus 和其他 Exporter 组件会**直接暴露HTTP端点(如 /metrics),这意味着:

  1. 未经过身份验证的客户端 可能访问这些数据,存在安全风险。
  2. Kubernetes 原生 RBAC 无法直接控制对 /metrics 端点的访问

kube-rbac-proxy 通过代理 Prometheus、Exporter 等应用的 /metrics 端点,实现基于 Kubernetes RBAC 规则的身份验证,确保:

  • 只有拥有特定 ServiceAccount、Role 或 ClusterRole 权限的用户或服务可以访问这些端点。

现在要关注的是quay.io/prometheus-operator/prometheus-operator:v0.76.2这个镜像,对应的源码如下

git clone  --branch v0.76.2 --single-branch https://github.com/prometheus-operator/prometheus-operator.git

毫无疑问,第一步是看Makefile,可以看到构建镜像以及二进制文件的命令。由于命令过于明显以及直接,这里就直接给出结论了

要构建镜像,需要先执行make build命令构建二进制程序,然后再执行make image构建镜像。
需要构建二进制文件如下,k8s-gen是利用k8s提供的代码生成工具生成Kubernetes 客户端代码。
在这里插入图片描述

第一步,先修改编译参数,添加禁用优化标识
在这里插入图片描述

第二步修改operatorDockerfile,由于基础镜像是busybox,需要添加godlv,从go的官方镜像中复制go sdk以及dlvbusybox中。Dockerfile修改后如下

ARG ARCH="amd64"
ARG OS="linux"
# 第一阶段:使用 Golang 官方镜像,准备 Go SDK
FROM --platform=$ARCH golang:1.23-alpine AS builder

ENV GOPROXY=https://goproxy.cn,direct
RUN go env -w GOPROXY=https://goproxy.cn,direct && \
    go env -w GOSUMDB=off

#  dlv
RUN go install github.com/go-delve/delve/cmd/dlv@v1.24.0

FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest as operator

# 复制Go 编译环墨境到镜像中
COPY --from=builder /usr/local/go /usr/local/go
COPY --from=builder /go/bin/dlv /usr/local/go/bin/dlv
# 设置环境变量(如果需要)
ENV PATH="/usr/local/go/bin:${PATH}"


# 使用 root 用户,确保 dlv 能正常运行
USER root

LABEL org.opencontainers.image.source="https://github.com/prometheus-operator/prometheus-operator" \
    org.opencontainers.image.url="https://prometheus-operator.dev/" \
    org.opencontainers.image.documentation="https://prometheus-operator.dev/" \
    org.opencontainers.image.licenses="Apache-2.0"


# 复制 Go 编译的二进制文件
COPY operator /bin/operator
# 复制operator的源码
COPY . /prometheus-operator

ENV GOPROXY=https://goproxy.cn,direct
RUN go env -w GOPROXY=https://goproxy.cn,direct && \
    go env -w GOSUMDB=off

# 切换工作目录
WORKDIR /prometheus-operator
# 下载所有依赖包源码
RUN go mod tidy && go mod vendor

# 还原工作目录
WORKDIR /
ENTRYPOINT ["/bin/operator"]

构建镜像的命令如下,构建镜像的时候,会先编译源码的,所以不需要额外执行编译源码的命令

TAG=v0.0.0 make image

其实本篇文章中调试只用到了operator的镜像,可以执行下面的语句即可

make .hack-operator-image

docker导出镜像到containerd

# 导出
docker save -o my-operator.tar quay.io/prometheus-operator/prometheus-operator:v0.0.0

# 导入
ctr -n=k8s.io images import my-operator.tar

# 检查镜像
ctr -n k8s.io i ls | grep operator

回到kube-prometheus项目中, 修改operator对应的yaml文件prometheusOperator-deployment.yaml, 用dlv命令覆盖entrypoint

在这里插入图片描述

除了覆盖命令外,根据你的需要修改容器安全属性,当然不改也是可以的

readOnlyRootFilesystem: false

或者删除yaml后面的非root用户设置

runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534

我是为了验证一些东西,进入容器中,想要执行go env命令都不行,因为nobody用户没有权限操作/tmp目录.

接着创建operator相关的资源

kubectl apply -f manifests/prometheusOperator-clusterRoleBinding.yaml
kubectl apply -f manifests/prometheusOperator-clusterRole.yaml
kubectl apply -f manifests/prometheusOperator-deployment.yaml
kubectl apply -f manifests/prometheusOperator-networkPolicy.yaml
kubectl apply -f manifests/prometheusOperator-prometheusRule.yaml
kubectl apply -f manifests/prometheusOperator-serviceAccount.yaml
kubectl apply -f manifests/prometheusOperator-serviceMonitor.yaml
kubectl apply -f manifests/prometheusOperator-service.yaml

同时还要创建一个service,因为operator没有暴露dlv监听的端口

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
    app.kubernetes.io/part-of: kube-prometheus
    app.kubernetes.io/version: 0.76.2
  name: prometheus-operator-debug
  namespace: monitoring
spec:
  type: NodePort  # Service 类型
  ports:
    - name: debug
      port: 8005       # Service 内部端口
      targetPort: 8005 # Pod 内部应用的端口
      nodePort: 30000  # 外部访问的端口
      protocol: TCP
  selector:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
    app.kubernetes.io/part-of: kube-prometheus

vscode配置如下,需要按实际情况调整映射的路径

{
    "version": "0.2.0",
    "configurations": [

        {
            "name": "operator",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "remotePath": "/prometheus-operator",
            "port": 30000,
            "host": "4c",
            "showLog": true,
            "trace": "verbose",
            "substitutePath": [
                {
                    "from": "${workspaceFolder}",       //本地源码路径
                    "to": "/root/prometheus-operator"   //编译operator时,源码所在路径
                },
                {
                    "from": "/Users/wy/wy/workspace_go/pkg/mod", // 本地模块路径
                    "to": "/root/go_path/pkg/mod" // 远程模块路径
                },
                {
                    "from": "/opt/homebrew/Cellar/go/1.23.3/libexec/src", // 本地go sdk源码路径
                    "to": "/root/go/src/" // 远程go sdk源码路径
                },
                
            ]
        }
    ]
}

然后就可以愉快地看源码了
在这里插入图片描述

下面是调试过程中常用到的一些命令

# 别称,快速查看
alias kdescm="kubectl describe -n monitoring pod"
alias kgetm="kubectl get pod -n monitoring"

# 查看容器日志,替换pod的名字
kubectl logs prometheus-operator-94b467d84-n92cm -n monitoring -c prometheus-operator

# 持续查看容器日志,替换pod的名字
kubectl logs -f prometheus-operator-94b467d84-5d52r -n monitoring -c prometheus-operator

# 删除pod, 重新debug,替换pod的名字
kubectl delete pod prometheus-operator-74c7dfbbc5-l62xd -n monitoring

# 进入容器中查看,替换pod的名字
kubectl exec -it -n monitoring prometheus-operator-94b467d84-n92cm -c prometheus-operator -- /bin/sh


# 删除namespace为monitoring中,prometheus-operator-xxxxx的pod
kubectl delete -n monitoring pod $(kubectl get pods -n monitoring --no-headers -o custom-columns=":metadata.name" | grep "^prometheus-operator-")

源码阅读

代码入口

Operator的原理是利用了 Kubernetes 的自定义 API 资源(CRD),来描述我们想要部署的"有状态应用";然后在自定义控制器里,根据自定义 API 对象的变化,来完成具体的部署和运维工作。

prometheus-operator创建的operator以及controller如下

var po *prometheuscontroller.Operator
var pao *prometheusagentcontroller.Operator
var ao *alertmanagercontroller.Operator
var to *thanoscontroller.Operator
var kec *kubelet.Controller

AlertManager这个CRD为例,看operator是如何工作的,operator代码入口是cmd/operator/main.go

关于AlertManager相关的代码很明显
在这里插入图片描述

会先验证 CRD 是否已安装在集群中,并检查是否有足够的权限来管理资源,然后才会创建operator

operator原理回顾

要看operator中代码需要先回顾一下Operator的工作原理。下图取自极客时间中磊哥的《深入剖析Kubernets》第25课深入解析声明式API(二):编写自定义控制器

在这里插入图片描述

Operator 通过 Informer 监听 API Server 的资源变更。当资源发生 Add、Update 或 Delete 事件时,Informer 会将变更信息存入 DeltaFIFO 队列,并将事件加入 WorkQueue。
Controller 组件会循环从 WorkQueue 取出资源变更,并调用 Reconcile() 进行业务处理,确保资源状态与期望状态一致。

informer初始化

所以接下来的工作就是:找到informer以及弄清楚controller的Reconcile是如何处理的
回到代码上,New方法中,创建了一个Operator对象、一个ResourceReconciler,并调用了operator的bootstrap方法。
在这里插入图片描述

bootstrp方法中,会创建该operator的需要的多个informer
在这里插入图片描述

这几个informer创建的代码都长一个样,区别就是传递的参数有点区别。以第一个informer的代码为例

var err error
c.alrtInfs, err = informers.NewInformersForResource(
	informers.NewMonitoringInformerFactories(
		config.Namespaces.AlertmanagerAllowList,
		config.Namespaces.DenyList,
		c.mclient,
		resyncPeriod,
		func(options *metav1.ListOptions) {
			options.LabelSelector = config.AlertmanagerSelector.String()
		},
	),
	monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.AlertmanagerName),
)
if err != nil {
	return fmt.Errorf("error creating alertmanager informers: %w", err)
}

informers.NewInformersForResourceinformers.NewMonitoringInformerFactories这两个方法都是pkg/clinet/informers中的代码,而informers包则是有k8s代码生成工具生成的,是模板代码。
在这里插入图片描述

我们要关注的是,哪些是需要手工编写的逻辑代码,例如如何调用informers包获取informer
对比一下调用代码
在这里插入图片描述

唯一需要说明的是第5个参数tweakListOptions ,通常情况下,填写nil就可以了,因为这个参数的主要作用是提供一种机制,允许调用者在执行 Kubernetes API 列表操作时自定义 ListOptions。这可以用于设置标签选择器、字段选择器、资源版本等,以便过滤和限制返回的资源列表。

创建informer的模板代码已经知道了,本小节剩余的文字可以不用看了,但是都看了还是保留下来凑凑字数吧。下面是创建informer用到的两个函数的理解:

informers.NewMonitoringInformerFactories 返回的是一个实现了FactoriesForNamespaces接口的对象

// FactoriesForNamespaces is a way to combine several shared informers into a single struct with unified listing power.
type FactoriesForNamespaces interface {
	ForResource(namespace string, resource schema.GroupVersionResource) (InformLister, error)
	Namespaces() sets.Set[string]
}

实际上,返回的是一个map对象,key是namespace,vlaue是工厂对象,其实现了FactoriesForNamespaces接口
在这里插入图片描述

informers.NewInformersForResource中就是通过遍历该map对象,获取到工厂对象创建对应的informer。最终返回一个包含了多个namespace对应informerForResource对象.这里默认的namespace为"" ,表示所有namespace共用一个informer
在这里插入图片描述

调谐

informer创建后,接下来要看看controller调谐的逻辑了。在main.go最后会启动前面创建的那堆opertaor
在这里插入图片描述

在各个operatorRun方法内,又会创建一堆goroutinue来启动informer以及ResourceReconciler,然后还调用了addHandlers方法.
在这里插入图片描述

c.rr就是在创建operator时创建的ResourceReconciler

o.rr = operator.NewResourceReconciler(
	o.logger,
	o,
	o.metrics,
	monitoringv1.AlertmanagersKind,
	r,
	o.controllerID,
)

但是ResourceReconciler并不是AlertManager特有的,是在pkg/operator包下面的,是被所有operator复用的。名为调谐器,调谐的逻辑也是由它触发的。其run方法如下

// Run the goroutines responsible for processing the reconciliation and status
// queues.
func (rr *ResourceReconciler) Run(ctx context.Context) {
	// Goroutine that reconciles the desired state of objects.
	rr.g.Go(func() error {
		for rr.processNextReconcileItem(ctx) {
		}
		return nil
	})

	// Goroutine that reconciles the status of objects.
	rr.g.Go(func() error {
		for rr.processNextStatusItem(ctx) {
		}
		return nil
	})
}

根据CR,创建POD对象的逻辑就在rr.processNextReconcileItem(ctx)
在这里插入图片描述

如图,这部分逻辑是共用的,区别的地方在于这个syncer对象。
prometheus-operator在实现上用的是statefulSet来实现pod的创建的,具体的逻辑就在这个syncer中。在创建ResourceReconciler的时候,syncer入参值就是operator本身。
在这里插入图片描述

operator本身是根据CRD本身定制的,存在与各自的包中。例如该opeator是在pkg/alertmanager包下,prometheus severoperator则在pkg/prometheus包下。

至此,调谐的代码位置以及入口已经知道了,还剩下,调谐是被触发的还没交代。
回看调谐的逻辑,第一步是从队列中获取到数据

item, quit := rr.reconcileQ.Get()
...省略
key := item.(string)
...省略
err := rr.syncer.Sync(ctx, key)

根据informer的原理可知,informer通过AddEventHandler注册事件处理器,当CRD对应的资源有变化的时候,会调用相应的事件数据入队列,然后调谐器从队列中获取数据进行处理。
operator.run方法启动了一大堆informer之后,调用了operartor.addHandlers() 来完成各个informer的事件处理器注册.
在这里插入图片描述

可以看到,ResourceReconciler就是对应的事件处理器,其实现了事件处理器对应的三个方法,OnAdd,OnUpdate,OnDelete. 三个方法的最后都会往队列reconcileQ里面添加数据。
然后在goroutinue中运行的ResourceReconciler就能够从reconcileQ中获取到数据从而调用operator.sync来完成调谐。

总结

作为一种代码设计的思路,可以参考,prometheus-operator中哪些代码是复用的,哪些是需要定制的。后续需要实现复杂的operator时候,可以抄抄代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值