NFS CSI插件源码浅读

目标

通过研究nfs csi插件,加深对k8s csi插件的理解

安装nfs

安装nfs-server

apt install nfs-kernel-server

创建共享目录,演示用的所以权限为777

sudo mkdir -p /mnt/nfs-data
sudo chmod 777 /mnt/nfs-data

配置共享目录,修改/etc/exports文件,添加一下内容

/mnt/nfs-data *(rw,sync,no_subtree_check,no_root_squash)

/mnt/nfs-data:NFS 服务器上的共享目录
*:表示允许所有客户端访问(可以用特定 IP 限制,如 192.168.1.0/24
rwsyncno_subtree_checkno_root_squash 是 NFS 访问控制选项

控制选项说明
rw允许客户端读写共享目录(默认是 ro 只读)
sync 数据同步写入磁盘 ,保证数据一致性(比 async 更安全但性能稍低)
async 允许 NFS缓存数据,提高写入性能,但可能丢数据
no_subtree_check禁用子目录检查,提高性能
subtree_check启用子目录检查,增加安全性但影响性能
no_root_squash允许 NFS 客户端的 root 用户拥有完全权限
root_squash将客户端 root 用户映射为 nobody

修改了配置后,执行下面的命令让配置生效

exportfs -rav

# 会重启nfs进程
systemctl restart nfs-kernel-server

运行 exportfs -rav 可以使 NFS 服务器重新应用 /etc/exports 配置文件的更改,而无需重启 NFS. no_root_squash 选项的更改可能不会立即生效,可能需要重启 NFS 服务器

安装了nfs服务端的节点上执行下面的命令检查是否已经共享

showmount -e

输出下面的内容表示新配置的目录已经生效了

Export list for iZ7xv0vjzfc2mx9z5v5sdwZ:                                         
/mnt/nfs-data *

验证nfs是否可用

接着尝试用静态绑定的方式创建一个nfs pv看是否能够正常使用
pv

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany  # 允许多个 Pod 共享
  persistentVolumeReclaimPolicy: Retain # 这样 PVC 释放后 PV 仍然可用
  nfs:
    server: 172.xx.xx.xx  # 替换为你的 NFS 服务器 IP
    path: "/mnt/nfs-data"

pvc

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 500Mi
  volumeName: nfs-pv

如果绑定错了,用下面的命令清除pv的绑定状态

kubectl patch pv nfs-pv -p '{"spec":{"claimRef": null}}'

pod中使用pvc验证nfs是否能够正常工作

apiVersion: v1
kind: Pod
metadata:
  name: nfs-test-pod
spec:
  containers:
  - name: test-container
    image: docker.io/library/busybox:1.37.0
    command: ["/bin/sh", "-c", "while true; do echo Hello from $(hostname) >> /mnt/data/test.log; sleep 5; done"]
    volumeMounts:
    - mountPath: "/mnt/data"
      name: nfs-volume
  volumes:
  - name: nfs-volume
    persistentVolumeClaim:
      claimName: nfs-pvc

pod运行后,查看/mnt/nfs-data目录中的文件可以看到test.log说明挂载成功并可用

安装NFS CSI插件

nfs csi插件代码仓库地址,里面有安装以及使用的说明
https://github.com/kubernetes-csi/csi-driver-nfs

这里选kubectl的方式,不选helm的方式安装,因为这样yaml文件更清晰一些
https://github.com/kubernetes-csi/csi-driver-nfs/blob/master/docs/install-csi-driver-v4.10.0.md

本地源码安装

git clone --branch v4.10.0 --single-branch https://github.com/kubernetes-csi/csi-driver-nfs.git
cd csi-driver-nfs
./deploy/install-driver.sh v4.10.0 local

查看安装脚本,并没有安装CSI快照相关的yaml文件
在这里插入图片描述

直接执行apply命令

kubectl apply -f deploy/v4.10.0/rbac-csi-nfs.yaml
kubectl apply -f deploy/v4.10.0/csi-nfs-driverinfo.yaml
kubectl apply -f deploy/v4.10.0/csi-nfs-controller.yaml
kubectl apply -f deploy/v4.10.0/csi-nfs-node.yaml

需要下载的镜像如下:

registry.k8s.io/sig-storage/csi-provisioner:v5.2.0
registry.k8s.io/sig-storage/csi-resizer:v1.13.1
registry.k8s.io/sig-storage/livenessprobe:v2.15.0
registry.k8s.io/sig-storage/nfsplugin:v4.10.0
registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.13.0
registry.k8s.io/sig-storage/csi-snapshotter:v8.2.0

查看pod状态正常后,修改deploy/v4.10.0/storageclass.yaml

---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
  server: 172.xx.xx.xx
  share: /mnt/nfs-data
  # csi.storage.k8s.io/provisioner-secret is only needed for providing mountOptions in DeleteVolume
  # csi.storage.k8s.io/provisioner-secret-name: "mount-options"
  # csi.storage.k8s.io/provisioner-secret-namespace: "default"
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - nfsvers=4.1

修改server以及share字段. server是你安装了nfs server的节点的IP,当然也可以创建service写域名。share上的目录要与/etc/exports文件中的一致。

修改完成后,创建storageClass

kubectl apply -f deploy/v4.10.0/storageclass.yaml

至此nfs csi插件已经安装好了,

验证NFS CSI插件是否可用

接下来创建一个pvc验证是否能够自动创建pv并绑定

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-nfs-dynamic
  namespace: default
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Mi
  storageClassName: nfs-csi

检查可以看到pvc进入Bound状态,表示nfs csi插件能够正常工作了
在这里插入图片描述

如果失败了,用下面的命令查看日志确认是什么问题

kubectl logs -f $(kubectl get pods -n kube-system --no-headers -o custom-columns=":metadata.name" | grep "^csi-nfs-controller-") -n kube-system -c csi-provisioner

NFS CSI插件各个组件的作用

这些镜像都是 Kubernetes CSI(Container Storage Interface) 组件,用于管理 存储的动态分配、挂载、扩容、快照 等功能。它们如何协同工作如下:

1、 创建 PersistentVolume(PV)
csi-provisioner 监听 PVC 请求创建 PV
csi-provisioner 调用 nfsplugin(或其他存储驱动)创建 NFS 挂载
csi-node-driver-registrarNFS 存储注册到 Kubelet
Pod 运行,csi-node 组件确保存储挂载正常

2、在线扩容存储卷
用户修改 PVC 请求扩展存储
csi-resizer 监听 PVC 变化,通知 nfsplugin 执行扩容
csi-node 重新挂载 NFS 目录,扩展存储
PVC 扩容成功,Pod 继续运行

3、存储健康监测
livenessprobe 定期检查 CSI 驱动(nfsplugin) 是否正常
如果存储挂载失败,livenessprobe 触发 自动重启 CSI 组件

4、创建存储快照
用户创建 VolumeSnapshot 请求
csi-snapshotter 监听请求,并调用 nfsplugin 生成快照
nfsplugin 在 NFS 服务器上创建快照
未来用户可以用 VolumeSnapshot 恢复数据

概括每个组件的作用如下:
csi-provisioner 处理 PVC 绑定 PV
csi-resizer处理存储扩容
livenessprobe监测 CSI 运行状态
nfsplugin具体执行 NFS 挂载
csi-node-driver-registrarkubelet 识别 CSI
csi-snapshotter处理存储快照

源码阅读

本篇文章的关注点是csi插件是如何工作的,查看csi-provisioner以及nfsplugin这两个组件的交互的流程,就能猜个大概了,所以接下来就看这两个组件的代码来了解csi插件的工作原理。

csi-provisioner

csi-provisioner 是一个外部的控制器,负责监视 Kubernetes 中的 PersistentVolumeClaim(PVC)对象,并根据 CSI 驱动的实现,调用相应的 CSI 接口来创建或删除存储卷。它在 Kubernetes 集群中作为一个独立的 Pod 运行,通常与其他 CSI 组件(如 csi-attachercsi-resizer 等)协同工作,以提供完整的存储解决方案。

如何调试csi-provisioner源码

下载源码

git clone --branch v5.2.0 --single-branch https://github.com/kubernetes-csi/external-provisioner.git

Makefile查看构建镜像的命令

$(CMDS:%=container-%): container-%: build-%
	docker build -t $*:latest -f $(shell if [ -e ./$(CMDS_DIR)/$*/Dockerfile ]; then echo ./$(CMDS_DIR)/$*/Dockerfile; else echo Dockerfile; fi) --label revision=$(REV) .

shell脚本一开始使用了set -x启用了调试模式,能看到编译以及构建的命令。

总之最终得到要用的命令如下,命令根据我个人的需求有些改动,并不完全与原文的想同

编译程序命令

CGO_ENABLED=0 go build -a -gcflags="all=-N -l" -ldflags ' -X main.version=v5.2.0 -extldflags "-static"' -o /root/external-provisioner/bin/csi-provisioner ./cmd/csi-provisioner

构建前需要修改Dockerfile,以及下载gcr.io/distroless/static:latest镜像。
gcr.io/distroless/static:latestGoogle Distroless 提供的一个 极小的基础镜像,是 Google Distroless 提供的一个 极小的基础镜像,没有shell也没有包管理工具,整个镜像只有25mb左右,特别适用于 Go 这类静态编译的应用。
原始的感觉不太合适调试,看了其github仓库,好像这个更合适调试,所以基础镜像改为
gcr.io/distroless/static-debian12:debug

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 gcr.io/distroless/static-debian12:debug as runtime
LABEL maintainers="Kubernetes Authors"
LABEL description="CSI External Provisioner"
ARG binary=./bin/csi-provisioner
COPY ${binary} csi-provisioner
COPY . /external-provisioner

# 复制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}"

#ENTRYPOINT ["/csi-provisioner"]
ENTRYPOINT ["dlv", "--listen=:30001", "--headless=true", "--api-version=2", "exec", "/csi-provisioner","--"]

构建镜像命令

docker build -t registry.k8s.io/sig-storage/csi-provisioner:v5.2.x -f Dockerfile .

镜像构建好后,导入containerd

docker save -o my-provisioner.tar registry.k8s.io/sig-storage/csi-provisioner:v5.2.x

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

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

修改csi-nfs-controller.yaml

在这里插入图片描述

去掉leader选举的选项是因为,调试的时候,租约续不上会被强制重启的,不方便调试。内存也是,内存不足会被杀掉重启的。

修改后重新创建pod即可,因为该controller配置hostnetwork=true使用的是宿主机的网络,不需要额外配置service暴露端口。

kubectl apply -f deploy/v4.10.0/csi-nfs-controller.yaml

vscode配置如下

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

        {
            "name": "csi-remote",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "remotePath": "${workspaceFolder}",
            "port": 30001,
            "host": "4c",
            "showLog": true,
            "trace": "verbose",
            "substitutePath": [
                {
                    "from": "${workspaceFolder}",       //本地源码路径
                    "to": "/root/external-provisioner"   //编译时,源码所在路径
                },
                {
                    "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源码路径
                },
                
            ]
        }
    ]
}

代码入口cmd/csi-provisioner/csi-provisioner.go

csi-provisioner源码逻辑

上篇文章看了prometheus-operator的实现,这次就省点事直奔代码位置了。csi-provisioner中还是老套路,informer负责监听变化,将变化的数据放入队列,controller从队列获取数据处理。开始的标识性语句是 xxxxController.run()之类的代码
在这里插入图片描述

run方法中会启动200个goroutinue,threadiness默认值是100
ctrl.runClaimWorker(ctx)负责pv的创建
ctrl.runVolumeWorker(ctx)负责pv的删除
在这里插入图片描述

建议断点位置如下,创建pvc即可跟着代码看pvc处理逻辑, 文件名为nfs-pvc-example.yaml,调试后续组件的时候,也会用到

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-nfs-dynamic
  namespace: default
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Mi
  storageClassName: nfs-csi

在这里插入图片描述

看到后面会看到发出grpc请求/csi.v1.Controller/CreateVolume给Pod中的另一个容器nfs,其镜像registry.k8s.io/sig-storage/nfsplugin:v4.10.0就是我们说的csi插件
在这里插入图片描述

grpc调用成功后,会继续创建pv对象提交给api server. 代码分得比较散,只能截取部分代码

# grpc调用nfsplugin
rep, err := p.csiClient.CreateVolume(createCtx, req)

...省略

# 构建PV对象
pv := &v1.PersistentVolume{
		ObjectMeta: metav1.ObjectMeta{
			Name: pvName,
		},
		Spec: v1.PersistentVolumeSpec{
			AccessModes:  options.PVC.Spec.AccessModes,
			MountOptions: options.StorageClass.MountOptions,
			Capacity: v1.ResourceList{
				v1.ResourceName(v1.ResourceStorage): bytesToQuantity(respCap),
			},
			// TODO wait for CSI VolumeSource API
			PersistentVolumeSource: v1.PersistentVolumeSource{
				CSI: result.csiPVSource,
			},
		},
}

# volume就是前面的pv,claimRef是pvc对象的引用
volume.Spec.ClaimRef = claimRef


# 请求api server保存pv对象
_, err := q.client.CoreV1().PersistentVolumes().Create(context.Background(), volume, metav1.CreateOptions{})

csi-provisioner是所有csi插件共用的,代码中没有具体插件的逻辑,是靠入参--csi-address=$(ADDRESS)来识别处理哪种pvc对象的,具体的创建和删除的处理通过grpc调用对应的csi插件来完成的。

nfsplugin

csi-provisioner调用,动态创建以及删除 nfs pv.

如何调试nfsplugin

源码就在前面安装nfs csi插件的仓库中

git clone --branch v4.10.0 --single-branch https://github.com/kubernetes-csi/csi-driver-nfs.git

修改Makefile禁用优化

#EXT_LDFLAGS = -s -w -extldflags "-static"
EXT_LDFLAGS = -extldflags "-static"

...省略

.PHONY: nfs
nfs:
	CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) go build -a -gcflags="all=-N -l" -ldflags "${LDFLAGS} ${EXT_LDFLAGS}" -mod vendor -o bin/${ARCH}/nfsplugin ./cmd/nfsplugin

编译命令

make nfs

修改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 registry.k8s.io/build-image/debian-base:bookworm-v1.0.4 AS runtime

# 加速apt
# 添加国内源(这里使用清华镜像)
RUN echo "deb http://mirrors.tuna.tsinghua.edu.cn/debian bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
    echo "deb http://mirrors.tuna.tsinghua.edu.cn/debian bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
    echo "deb http://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
    apt update && apt clean

RUN apt update && apt upgrade -y && apt-mark unhold libcap2 && clean-install ca-certificates mount nfs-common netbase

ARG binary=./bin/nfsplugin
COPY ${binary} /nfsplugin

# 复制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}"
# 复制源码到镜像中
COPY . /csi-driver-nfs

# ENTRYPOINT ["/nfsplugin"]
ENTRYPOINT ["dlv", "--listen=:30001", "--headless=true", "--api-version=2", "exec", "/nfsplugin","--"]

构建镜像命令

docker build -t registry.k8s.io/sig-storage/nfsplugin:v4.10.x .

镜像构建好后,导入containerd

docker save -o my-nfsplugin.tar registry.k8s.io/sig-storage/nfsplugin:v4.10.x

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

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

在启动前还需要修改csi-nfs-controller.yaml文件,否则无法调试。修改的内容有两项
1、注释掉整个liveness-probe容器
在这里插入图片描述

2、注释掉nfs容器定义的部分内容
在这里插入图片描述

虽然注释掉了存活检查相关的配置,但调试的时候,还是会出现容器被重启的情况,可能是csi-provisioner那边grpc调用超时问题导致整个pod被重启了,但还好,这个时间间隔足够调试代码了。如果不行就两个容器一起调试吧。

修改完成后,先清理旧的pod,然后重新创建

kubectl delete -f deploy/v4.10.0/csi-nfs-controller.yaml
kubectl apply -f deploy/v4.10.0/csi-nfs-controller.yaml

vscode配置复制csi-provisioner中的配置,只需要修改源码映射路径即可

{
	"from": "${workspaceFolder}",       //本地源码路径
	"to": "/root/csi-driver-nfs"   //编译nfsplugin时,源码所在路径
},

查看日志命令

kubectl logs -f $(kubectl get pods -n kube-system --no-headers -o custom-columns=":metadata.name" | grep "^csi-nfs-controller-") -n kube-system -c nfs

nfsplugin源码

在看csi-provisioner的时候,已经知道grpc的请求是/csi.v1.Controller/CreateVolume,直接搜索关键字,查看谁引用了该变量就可以找到调用的代码了
在这里插入图片描述

删除掉最初的pvc,重新创建pvc即可进行调试

kubectl delete -f nfs-pvc-example.yaml
kubectl apply -f nfs-pvc-example.yaml

在这里插入图片描述
下面是从代码中截取出来的关键代码,看注释即可

# 将请求参数中关于pv的信息转换为 nfsVolume 结构对象
nfsVol, err := newNFSVolume(name, reqCapacity, parameters, cs.Driver.defaultOnDeletePolicy)

# 挂载nfs共享目录 /mnt/nfs-data  到 容器内的 /tmp/pvc-xxxxx
cs.internalMount(ctx, nfsVol, parameters, volCap)

# 创建目录 /tmp/pvc-xxxxx/pvc-xxxxx, 新建的这个pvc-xxxxx就是pv对应的远程目录
os.MkdirAll(internalVolumePath, 0777)

# 返回pv信息给csi-provisioner,  csi-provisioner根据返回信息创建pv对象提交给api server
return &csi.CreateVolumeResponse{
		Volume: &csi.Volume{
			VolumeId:      nfsVol.id,
			CapacityBytes: 0, // by setting it to zero, Provisioner will use PVC requested size as PV size
			VolumeContext: parameters,
			ContentSource: req.GetVolumeContentSource(),
		},
}, nil

可以看到,创建pv时,nfsplugin的工作就是在nfs的共享目录里面以pvc的名称创建一个子目录。该pv之所以能被使用,是因为这些pv本质上能够被远程使用( 使用mount命令挂载到linux上,作为目录使用)。pvc被pod使用的时候,kubelet将该子目录挂载到pod所在的宿主机上,pod再挂载对应目录。容器进程往目录中写入数据,会写入到nfs服务器对应的目录上。

一直说的csi协议,其实就是实现下面这些grpc接口
在这里插入图片描述

后面的内容就不看了,目前没有特别想看的点,等想看再写吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值