目标
通过研究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
)
rw
、sync
、no_subtree_check
、no_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-registrar
将 NFS
存储注册到 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-registrar
让 kubelet
识别 CSI
csi-snapshotter
处理存储快照
源码阅读
本篇文章的关注点是csi插件是如何工作的,查看csi-provisioner以及nfsplugin这两个组件的交互的流程,就能猜个大概了,所以接下来就看这两个组件的代码来了解csi插件的工作原理。
csi-provisioner
csi-provisioner
是一个外部的控制器,负责监视 Kubernetes 中的 PersistentVolumeClaim
(PVC)对象,并根据 CSI 驱动的实现,调用相应的 CSI 接口来创建或删除存储卷。它在 Kubernetes 集群中作为一个独立的 Pod 运行,通常与其他 CSI 组件(如 csi-attacher
、csi-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:latest
是 Google 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
接口
后面的内容就不看了,目前没有特别想看的点,等想看再写吧。