目标
前面看了minikube
的源码了解到其本质是调用了kubeadm
来启动k8s
集群,并没有达到最初看代码的目的。 所以继续看看kubeadm
的代码,看看能否用来方便地构建源码调试环境。
k8s源码编译
kubeadm
源码在k8s
源码库中,所以要先克隆k8s
源码。之前用minikube
创建的k8s
集群是v1.32.0
所以克隆v1.32.0
版本的代码
git clone --branch v1.32.0 --single-branch https://github.com/kubernetes/kubernetes.git
考虑到后续可能要改改源码并保存下来,所以我fork
了master
分支去编译。
在Makefile
中可以看到如何编译
在编译前先修改.go-version
文件中go的版本, 默认里面指定的是1.23.4
。k8s
源码中要求go
版本是1.23.0
以上就可以了,我的是1.23.3
不想重新下载go
压缩包了,所以改了。
修改shell
脚本让其输出编译的命令,看不到命令我不是很放心
通过环境变量指定版本号,修改完版本后执行编译命令编译kubeadm
export KUBE_GIT_VERSION=v1.32.0
export KUBE_GIT_COMMIT=$(git rev-parse --short HEAD)
export KUBE_GIT_TREE_STATE=clean
make all DBG=1 WHAT=cmd/kubelet
可以看到编译的命令已经带上了禁用优化的参数了
kubeadm
在一开始检查的过程中会调用kubelet
获取版本号,所以我把全部二进制文件都编译了
export KUBE_GIT_VERSION=v1.32.0
export KUBE_GIT_COMMIT=$(git rev-parse --short HEAD)
export KUBE_GIT_TREE_STATE=clean
make all DBG=1
调试命令如下
dlv --headless --listen=:8005 --api-version=2 --accept-multiclient --log exec /root/kubernetes/_output/bin/kubeadm -- init --cri-socket unix:///run/containerd/containerd.sock
vscode
配置如下
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "kubeadm",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/root/kubernetes",
"port": 8005,
"host": "4c",
"showLog": true,
"trace": "verbose",
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/root/kubernetes"
},
{
"from": "/Users/wy/wy/workspace_go/pkg/mod", // 本地路径
"to": "/root/go_path/pkg/mod" // 远程路径
},
]
}
]
}
调试源码前准备工作
PATH环境变量
将前面编译源码生成二进制文件的目录添加到PATH
环境变量中,因为kubeadm
需要调用kubelet
containerd启用cri插件
kubeadm
中,contianerd
是默认的容器运行时,containerd
需要启动cri
插件,随docker
启动的contianerd
默认是禁用了cri
插件的。
containerd
配置cri
插件官方文档地址:
https://github.com/containerd/containerd/blob/main/docs/cri/config.md
docker version
命令查看docker
版本是27.5.0
, containerd
对应的版本是1.7.25
containerd
配置文件默认位置是/etc/containerd/config.toml
在配置文档中有完整的配置文件样例且有大量的注释,有需要的时候再来看,但这不是我们目前要关注的
使用命令生成默认配置
# 备份旧配置
cp /etc/containerd/config.toml /etc/containerd/config.toml.bak
# 生成默认配置
containerd config default > /etc/containerd/config.toml
修改的值如下,v1.32.0
版本k8s
要求是3.10
版本,没有的话会触发下载镜像的操作
SystemdCgroup = true
sandbox_image = "registry.k8s.io/pause:3.10"
需要重启containerd
。由于我docker
服务是apt
安装的,估计是自动装的containerd
,是由systemd
托管的。所以重启命令如下
sudo systemctl restart containerd
下载k8s相关镜像
由于网络问题,你得先下载k8s的镜像
# 查看需要下载的镜像
kubeadm config images list
镜像清单如下
registry.k8s.io/kube-apiserver:v1.32.1
registry.k8s.io/kube-controller-manager:v1.32.1
registry.k8s.io/kube-scheduler:v1.32.1
registry.k8s.io/kube-proxy:v1.32.1
registry.k8s.io/coredns/coredns:v1.12.0
registry.k8s.io/pause:3.10
registry.k8s.io/etcd:3.5.17-0
然后用github action
大法下载镜像,下载完成后,检查镜像
ctr --namespace k8s.io images list | awk '{print $1}'
镜像已经拉取成功了
配置kubelet
kubeadm init
命令会使用systemctl
命令重启kubelet
,所以需要编写 /etc/systemd/system/kubelet.service
但是具体怎么写,需要通过官方提供的apt
命令安装kubeadm
后,使用kubeadm
命令安装一次k8s
,然后查看kubelet.service
可以看到具体的脚本是怎么写的
可以看到需要编写两个文件,分别是kubelet.service
以及 10-kubeadm.conf
编写/etc/systemd/system/kubelet.service
[Unit]
Description=kubelet: The Kubernetes Node Agent
Documentation=https://kubernetes.io/docs/home/
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/root/kubernetes/_output/bin/kubelet
Restart=always
StartLimitInterval=0
RestartSec=10
KillMode=process
Delegate=yes
[Install]
WantedBy=multi-user.target
编写/etc/systemd/system/kubelet.service.d/10-kubeadm.conf
,可以看到这里指定了config.yaml
文件
# Note: This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use
# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/default/kubelet
ExecStart=
ExecStart=/root/kubernetes/_output/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
然后启用该service
systemctl enable kubelet.service
调试源码
主要是想搞清kubeadm
是怎么部署k8s集群的,看看能不能用来调试代码。
代码入口是/cmd/kubeadm/kubeadm.go
kubeadm源码调试
kebeadm
中将部署集群的每个步骤抽象成phase
组成一个数组,然后遍历这个数组,运行每个phase
对应的函数,当phase
数组遍历完了,kubeadm init
命令就完成了
在cmd
命令初始化的时候,可以看到有哪些phase
其实在磊哥的《深入剖析Kubernetes》中有写了kubeadm
的部署原理,只是有些东西还是得自己看看才知道
很明显,kubelet
的启动对应是NewKubeletStartPhase
,随后的NewWaitControlPlanePhase
中等待apiserver
启动完成。
... 省略
initRunner.AppendPhase(phases.NewKubeletStartPhase())
initRunner.AppendPhase(phases.NewWaitControlPlanePhase())
... 省略
当 Kubelet
运行时,它会持续监视参数staticPodPath
指定的目录,如果有新的 Pod
配置文件加入,Kubelet
会自动创建 Pod
。这种启动Pod的方式叫静态启动,该过程不需要 apiserver
参与调度。
kubeadm
生成的kubelet
的配置文件中有个staticPodPath
选项,值如下
staticPodPath: /etc/kubernetes/manifests
该目录下的yaml
都是kubeadm
生成的,共四个yaml
文件,分别是
etcd.yaml
kube-apiserver.yaml
kube-controller-manager.yaml
kube-scheduler.yaml
kubelet源码调试
正常情况下,kubelet
是不会有问题的,如果在kubeadm init
命令执行过程提示 kubelet
失败,得看kubelet
到底报啥错了。查看kubelet
日志
journalctl -u kubelet
如果从日志上没看出是啥问题,可以调试下源码看看。当代码运行到启动kubelet
时,启动所需要的配置文件都已经生成了,所以调试kubelet
的时候,可以先退出kubeadm
的调试。
调试kubelet
需要先把kubeadm init
的代码运行到下图中的位置,然后kill
掉调试kubeadm init
的dlv
进程,然后调试kubelet
进程
调试前需要先停止kubelet
,并禁用自动重启
systemctl disable kubelet.service
systemctl stop kubelet
用下面的命令调试kubelet
源码
KUBELET_CONFIG_ARGS="--config=/var/lib/kubelet/config.yaml"
KUBELET_KUBECONFIG_ARGS="--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
KUBELET_KUBEADM_ARGS="--container-runtime-endpoint=unix:///run/containerd/containerd.sock --pod-infra-container-image=registry.k8s.io/pause:3.10"
dlv --headless --listen=:8005 --api-version=2 --accept-multiclient --log exec /root/kubernetes/_output/bin/kubelet -- $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
变量$KUBELET_EXTRA_ARGS
是由文件/etc/default/kubelet
中定义的,默认是空值,所以我没有赋值。
读取pod
文件的代码位置如下,config.NewSourceFile
中会启动goroutinue
监听/etc/kubernetes/manifests
的文件,如果文件有改动,会往一个 channel
发送数据
最后在kubelet
的主循环中处理channel
中的数据,创建、更新或者删除pod
启动的代码东西太多了,等需要的时候再回来看,此处只是记录下代码的位置
kube-scheduler源码调试
回到主题上,k8s
相关的组件都是通过静态pod
的方式启动的,要了解一个pod
,就得看它的yaml
文件,以kube-scheduler
组件为例
可以看到是使用宿主机的网络命名空间,那么就可以直接使用执行源码编译的二进制文件启动kube-schedualer
代替静态pod
的方式来实现断点调试了。
先用kubeadm init
命令把k8s部署成功,默认情况下,出于安全原因,不会在控制平面节点上调度 Pod。想要在控制平面节点上调度需要执行下面的命令
kubectl taint nodes --all node-role.kubernetes.io/control-plane-
然后删除对应的kube-scheduler.yaml
,由于kubelet
是持续监视着/etc/kubernetes/manifests
的,如果文件有变动,则执行对应的操作,例如我删除了yaml
文件,kubelet
会删除掉对应的pod
删除yaml
文件后已经看不到kube-scheduler
了,coredns是要等到CNI插件安装成功后才会启动,这里先不管
参考yaml
中的启动命令,使用下面的命令调试
dlv --headless --listen=:8005 --api-version=2 --accept-multiclient --log exec /root/kubernetes/_output/bin/kube-scheduler -- --authentication-kubeconfig=/etc/kubernetes/scheduler.conf --authorization-kubeconfig=/etc/kubernetes/scheduler.conf --bind-address=127.0.0.1 --kubeconfig=/etc/kubernetes/scheduler.conf --leader-elect=false
参数说明
--authentication-kubeconfig
负责身份认证,确保kube-scheduler
可以连接 API Server。--authorization-kubeconfig
负责权限授权,确保kube-scheduler
有权限调度 Pod。--leader-elect=false
因为没有多个节点,所以把leader
选举关掉
可以看到已经能够成功断点调试了,SchedulerOne
函数就是对Pod
进行调度的入口函数。