探索在Windows Server上跑起Kubeedge边缘节点
这里我将详细介绍在 Windows Server 上跑起 Kubeedge 边缘节点的艰难历程,我评价为举步维艰,一步一个坑,但是最终还是成功了(指启动最基本的Edgecore,调度一个Nginx容器到Windows Server并成功从Windows宿主机通过Pod IP访问,不包含云边通信)
和本次实验有关的一些软件/技术
Windows 容器技术
官方文档,讲的真的很好。
总的来说,Windows有两种运行容器的方式:
一种是基于Hyper-V的虚拟机容器,这种容器和Linux的容器不同,是基于虚拟机的,所以性能会比较差,但是可以运行不支持Windows的应用。
另一种是基于进程隔离的容器,这种容器和Linux的容器类似,是基于进程隔离的,性能会比较好,但是只能运行支持Windows的应用。
比如说我们最常用的 Docker Desktop,就是基于Hyper-V的虚拟机容器(运行在WSL2的Linux容器中),此时Windows上运行的容器,都是运行在Hyper-V虚拟机里面的,也正因为这样,我们运行的容器都是linux容器。
基于进程隔离的容器(Host Process Container),这个大家平时在Windows中不太会用到,因为这只有Windows Server支持这种容器。他的实现方式和Linux的容器类似,通过Windows 内核的命名空间功能来提供进程级别的隔离,并使用 Windows Server 上的资源管理器来限制容器的资源使用。性能会比较好,但是只能运行支持Windows的应用(linux/amd64之类的镜像就不能用咯)。
containerd、runc、runhcs、cni、sdn、kubelet、edgecore(edged)
runc是一个用于创建和运行容器的命令行工具。它是Open Container Initiative(OCI)的一部分,OCI是一个由多家公司共同推动的开放标准组织,旨在定义容器格式和运行时的标准。runc实现了OCI Runtime Specification,该规范定义了容器运行时的接口和行为。它负责启动和管理容器,处理容器的生命周期,包括创建、启动、停止和销毁容器等操作。runc本身是一个轻量级的工具,它依赖于Linux内核的容器特性(如cgroups和命名空间)来实现容器的隔离和资源管理。
containerd是一个面向生产环境的容器运行时。它是一个守护进程(daemon),负责管理和运行容器。containerd提供了一组API,用于创建、启动、停止和销毁容器,以及管理容器的镜像和存储等。它的设计目标是提供一个稳定、可靠、高性能的容器运行时,适用于各种容器管理工具和平台。containerd还支持插件机制,可以通过插件扩展其功能,例如支持不同的容器镜像格式、网络插件等。
(linux)runc和containerd的关系是容器运行时和容器管理器之间的关系。runc负责实际的容器运行时功能,而containerd则是一个容器管理器,使用runc来创建和管理容器。containerd通过调用runc提供的接口来操作容器,同时提供了更高级的功能,如镜像管理、存储管理、网络管理等。containerd还可以与其他工具和平台集成,例如Kubernetes,作为其底层容器运行时。
(Windows Server)runhcs是runc的Windows版本,实现的功能和runc一致,在Windows Server中containerd就是调用的runhcs来创建和管理容器。
cni是一个用于容器网络的规范和接口,它定义了容器运行时(如containerd)和网络插件之间的通信方式。CNI的目标是提供一个统一的、可插拔的网络接口,使容器运行时能够与各种网络插件进行交互,从而实现容器的网络配置和连接。containerd如果没有cni网络插件的支持,创建出来的容器就是个玩具,没有任何网络功能。在containerd中可以通过配置cni插件来管理容器的网络,CNI插件可以负责为容器分配IP地址、配置网络路由和防火墙规则等。
sdn是Software Defined Network的缩写,即软件定义网络。它是一种网络架构,通过软件来实现网络的配置、管理和控制,而不是通过硬件设备来实现。在本次实验中,我会使用nat作为节点sdn网络cni插件,而nat的工作方式则是在Windows宿主机上创建一个虚拟网卡,然后将容器的网络流量转发到这个虚拟网卡上,再通过Windows宿主机的网络设备转发到外网。
kubelet是Kubernetes集群中的一个组件,它负责管理节点上的容器,包括创建、启动、停止和销毁容器等操作。kubelet通过调用容器运行时(如containerd)的接口来管理容器。
edgecore(edged)是Kubeedge边缘节点的核心组件,edged是一个裁剪过的kubelet。它的运行方式和k8s上kubelet最大的区别是,kubelet是守护进程,但是edged不是守护进程,它随edgecore启停。
ctr和crictl
ctr和crictl是与容器运行时相关的工具,用于管理容器和容器镜像。ctr是和containerd一起分发的,它是containerd的客户端工具。
crictl是一个用于与cri兼容的容器运行时进行交互的命令行工具。crictl需要自己下载。
假如你不懂内在区别,我们可以理解crictl为给k8s用的命令行工具(最直观的感受就是,crictl的命令格式和kubectl十分相似。同时,ctr有namespace的概念,但是crictl没有,因为通过crictl执行的操作都在k8s.io里)。
为什么ctr和crictl彼此看不见对方拉的镜像?命名空间的问题咯,直接crictl pull的镜像,是在k8s.io的命名空间里,而ctr pull的镜像,是在default命名空间里。
操作环境
一台云服务器,在境外,系统是Ubuntu22,将作为k8s集群的master节点,并部署 Kubeedge1.14.1 Cloudcore。
一台 Windows11 电脑,装有VMWare Workstation Pro 16,创建立一个 Windows Server 2019 的虚拟机,将作为Kubeedge windows node。
探索过程
部署云端
这里不是重点,粗略带过。因为我用了境外主机,所以不存在什么网络问题,直接按照k8s和kubeedge官方文档部署即可。
- 配置内核参数 https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/#install-and-configure-prerequisites
此网页会要求你选择cgroup driver,我选择的是systemd(即文档中的systemd cgroup),因为我用的是Ubuntu22,而Ubuntu22默认使用systemd初始化系统。 - 安装容器运行时 https://github.com/containerd/containerd/blob/main/docs/getting-started.md
值得注意的是,本次实验请使用1.6.20版本的containerd(为了和边缘统一,虽然没啥意义,你用最新版也无妨,但是至于为什么边缘要用1.6.20,后文会详细介绍)。具体步骤跟着文档走,即下载release包(注意这里不要下载包名中含有cni和cri的包,因为跟着文档走会发现cri特性已经包含在基础包中,而cni我们会单独安装),解压到/usr/local(这里可以看见包里有之前介绍的ctr),然后下载service文件并添加到systemd,接着安装runc,最后安装cni - 配置crictl,
crictl config runtime-endpoint unix:///run/containerd/containerd.sock
。如果你要用crictl调试k8s,那么必须这样配一下,否则crictl会报错找不到runtime - 关闭防火墙(这里我直接关了,因为毕竟是做实验,免得少开了啥端口出问题debug)
- 安装指定版本的kubeadm、kubelet和kubectl https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl
这里安装命令请替换成apt-get install -y kubelet=1.24.14-00 kubeadm=1.24.14-00 kubectl=1.24.14-00
,因为kubeedge当前最新版本已经支持k8s1.24.14,所以我们就用1.24.14版本的k8s,这样可以避免一些版本兼容性问题。
注意:如果你看着v1.24的文档,那么该就文档中添加google源的过程是有过时的,按照最新版的文档走就好了 - 使用kubeadm初始化master节点 https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/
本实验我不需要复杂的配置,kubeadm init --control-plane-endpoint=<公网IP>
。至于为什么要指定这个参数,这不得不扯一下国内云厂商的这个弹性公网IP了,总之就是,你拥有的公网IP不会直接出现在机器的网卡上,而是通过一种叫做弹性公网IP的技术,将你的公网IP映射到机器的网卡上,直接ifcongig
,你会看见自己的ip是一个虚拟内网的IP,这会影响到k8s的初始化。如果你要跨虚拟网络部署集群,那么这里的配置会很麻烦,甚至包括手动修改etcd的yaml,配不好的话网络工具像flannel会直接烂掉,这里先不详细介绍了
当然还推荐使用--pod-network-cidr
指定当前节点的子网如10.244.0.0/16
,不然后面要手动执行kubectl patch node [node name] -p '{"spec":{"podCIDR":"10.244.0.0/16"}}'
来完成分配(10.244.0.0/16
是fannel默认的子网,当然你也可以换了它,但是在部署flannel的时候需要修改配置) - 部署网络插件flannel,当然也可以使用其他网络插件像calico,cilium,或是直接使用k8s自带的网络插件。但是我这里就用flannel了,因为我用过,而且我觉得它的配置最简单。https://github.com/flannel-io/flannel
直接下载yaml并apply即可,注意修改配置文件中的子网。部署成功以后,会看见master节点的状态变成ready。 - 去掉控制面的污点。由于要运行cloudcore,但我们的集群没有node,所以必须让master可调度,否则cloudcore会一直处于pending状态。https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/taint-and-toleration/
执行kubectl taint node [node name] node-role.kubernetes.io/control-plane-
和kubectl taint node [node name] node-role.kubernetes.io/master:NoSchedule-
即可 - 使用keadm部署kubeedge。https://kubeedge.io/zh/docs/setup/install-with-keadm
先去github下载keadm的release包,这里版本我们选择1.14.1,然后解压,最后直接keadm init --advertise-address=<公网IP>
,这里显式指定IP还是因为弹性网卡的问题 - 运行
keadm gettoken
拿到密钥,边缘加入做准备
Windows Server上的工作
我们要做的工作就是,安装containerd,安装cni,运行edgecore。命令行我们使用Powershell
1. 安装containerd
版本暂时请用1.6.20,因为我在使用1.6.22的时候踩了莫名其妙的坑(containerd死都加载不了cni插件,containerd始终报错说no network config found in /etc/cni/net.d: cni plugin not initialized
),同时也请先不要用1.7.x,和1.24.x的k8s windows不匹配。至于安装过程,微软有给出一套安装脚本文档地址,containerd仓库里也有一套Github地址,以及k8s的sig-windows-node也有一套Github。经过我的多次踩坑,我推荐最后一种,因为这个脚本里的安装路径能很好配合后面cni和kubelet等程序的运行(不然大概率跑步起来)。下载Install-Containerd.ps1
后,执行.\Install-Containerd.ps1 -ContainerDVersion 1.6.20 -skipHypervisorSupportCheck
,根据提示重启后再执行一遍这个命令即可(因为启用了一些系统功能,需要重启)。此脚本会帮我们装好containerd(不包含cni)和crictl
2. 安装cni插件
containerd的仓库里有个install-cni的脚本,里面包含了下载cni并生成默认配置文件,但是事情抽象在是个sh脚本,里面还夹杂舍powershell脚本,总之我是跑不起来。所以这边我们自己装。https://github.com/microsoft/windows-container-networking 这是仓库地址,但是我暂时不建议直接从这里下载release,因为踩坑了x,并且他没有给默认配置,指下载这个仓库中的release可能会一脸懵逼(网上零零散散的配置事例很乱,甚至还有issue里教你自己创建一个SDN网络给cni用,但是实际上完全是多余的)。
windows-container-networking中也有一个配置样例,但是先别用!
这里我推荐直接从containerd仓库的release中下载和containerd版本相匹配的带有cni的包,从中解压出已经打包好的cni二进制文件和配置。虽然containerd文档中提到不需要下载cri-containerd-(cni-)<VERSION>-<OS-<ARCH>.tar.gz
,并且还说「The cri-containerd-… archives are deprecated, do not work on old Linux distributions, and will be removed in containerd 2.0.」,但是由于我在自行安装cni的过程中踩了坑,所以这里还是极力建议本实验选用1.6.20带有cni的包下载地址。
解压后cni文件夹中存在cni/bin
和cni/conf
,把他们放到c:\Program Files\containerd\bin
和c:\Program Files\containerd\conf
下即可(如果使用sig-windows-node的脚本,那么就是这个路径,但是如果使用其他安装containerd的脚本,配置文件里的cni路径可能不是这个,但是在代码里却写死了地址是c:\Program Files\containerd
,这里是大坑)。
有一个要修改的地方,release包中给出的配置文件,gw不在子网里,潜在的结果可能是子后面部署Pod后,主机ping不通PodIP
还有一个要改的地方是dns配置,样例中capabilities.dns=true
,即将使用运行时的DNS配置覆盖其他设置,但是我们的运行时并没有配置dns,导致的结果就是在容器里无法使用dns解析,能ping通外部ip但是无法实现域名解析,解决方法是把这行删了,并手动给出Nameservers(这可能不是常规行为),我直接写了宿主机IP,当然也可以写8.8.8.8
或者114.114.114.114
这种公共DNS。
最终配置文件如下:
1 |
|
最后记得重启一下containerd。
此时输入ipconfig
不会有任何新的虚拟网络出现,不用担心,在containerd调用cni插件创建容器的时候,会自动创建虚拟网卡并绑定到容器上。
如何测试安装成功?使用ctr
工具创建一个pod出来试试。
1 |
|
如果报错cni not initialized,那么就是cni没装好
如果有输出,可以看见ipconfig会输出一个他的IP(在我们分配的子网下),那么就是cni装好了,并成功运行,如上图所示。
现在在宿主机运行ipconfig
,会看见多了一个虚拟网卡,这就是cni创建的,名字叫vEthernet (nat)
同时,在Powershell中执行Get-HnsNetwork
,也可以看见更详细的虚拟网络信息(如果命令不存在,从Github Microsoft SDN仓库下载脚本安装一下):
此时可以看看路由表,cni已经为SDN网络创建了路由:
3. 运行edgecore
这一步是本次操作的核心。
先是准备环境,goland+golang1.20,拉源码。然后建议配置Goland忽略vender,使用module
然后修改egde/pkg/edged/config/config.go
的ConvertConfigEdgedFlagToKubletFlag
函数,增加两行:
1 |
|
WindowsPriorityClass
参数是windows上运行Kubelet必须的,不填会Panic,可填写类型看Windows 文档
还有一个要临时修改的地方,位于egde/cmd/edgecore/app/server.go:179
:
1 |
|
当我们不开metaserver
的时候会检查宿主机是否运行了kubelet和kube-proxy,但是这里的检查是有问题的,这里的process.Name()
很可能会出错,有些进程没有名字。暂时把return err
改成continue
(前提是我们必须保证我们确实没在宿主机装这些东西)。
编译前需要准备环境,安装要mingw-64,不然编译不了sqlite相关(一定是64位,不能是32位,否则运行的时候sqlite相关会无法启动)。
接下来编译成可执行文件,在项目根目录执行:
1 |
|
可能需要一些别的环境变量,我不太清楚,我是直接用goland的编译工具编译的,可能要开CGO,因为sqlite需要CGO。
编译成功后,执行.\egdecore.exe --minconfig
生成一份最小配置文件写入到文件config.yaml
。我这里提供一个运行过后edgecore重新生成的完整配置(大部分模块都没有开,核心是测试edged):
1 |
|
由于大部分参数还没有摸透,我是改了几个地方让它能跑,所以不排除此配置文件中部分参数是处于不生效的状态。
和默认配置相比,需要改的不仅是IP地址,还有几个比较关键的地方:
- 文件路径前缀一律改成
C:\
,并修改分隔符为\
,因为在windows上,/
是不被允许的,会报错。(文件中有还是/etc
的,但是该模块都没开,所以不影响,以后要用的时候还是要改的) - 增加
windowsPriorityClass
参数,我给的值是NORMAL_PRIORITY_CLASS
,具体可选项可以看上文给出的微软文档 - 关闭edged的cgroupsPerQOS和cpuCFSQuota,这两个Windows不支持,会报错
- 清空enforceNodeAllocatable数组,此选项在windows上不生效,并且会报错
- 修改容器运行时接口为
npipe://./pipe/containerd-containerd
- 修改Sandbox为
mcr.microsoft.com/k8s/core/pause:1.2.0
,理论上保持不变也不会有问题,但我这里出问题了 - resolvConf留空,因为windows上没有这个文件,但不能删掉这行,否则会使用默认值
/etc/resolv.conf
,这个文件在windows上也是不存在的 <token>
改成keadm拿到的token
然后就可以运行了,.\egdecore.exe --config config.yaml
,如果一切正常,会看见如下输出
此时在云端执行kubectl get nodes -o wide
可以看见:
1 |
|
由于网络插件nat早已安装成功,所以node直接变为Ready状态
部署一个Nginx
这一步我有被折磨到,很难想象这个镜像有多难打
我试图去网上找现成的镜像,微软官方在Github给出了Dockerfile,但是亲测跑不起来,所以无奈只能自己造了。
Windows镜像和Linux镜像的区别在于,Windows容器必须以Windows系统作为基础镜像:
1 |
|
Dockerfile中没有写CMD了,因为这边有大坑,一旦没写好,就会看见这样的报错:
1 |
|
然后陷入CrashLoopBackOff,无法启动,从各个Issue来看,原因尽然CMD写错了,系统找不到你的启动命令,类似于linux容器没有找到/bin/sh
这里我复制了一个Bat文件进去(这不是一个优雅的做法,还在找解决方案):
1 |
|
为什么要这样,为什么不直接运行nginx.exe
?这我难说,我自己在宿主机测试nginx的时候,批处理会阻塞那,但是一旦在容器里,直接就退出了!所以无奈只能临时写个死循环,让容器不退出。
现在制作镜像,我们 需要使用buildx
交叉编译出windows/amd64版本的镜像,在一个装有docker和buildx的机子上执行:
1 |
|
记得先登录docker,这样打好的镜像就能直接推到仓库了。
ps:其实tag不应该加hostprocess
,hostprocess指的是类似linux特权容器的东西,可以到宿主机命名空间运行,具体文档看 https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/create-hostprocess-pod/。但我最开始没了解这个,想当然加了这个,其实是错误的x。
最后编写nginx.yaml:
1 |
|
CMD命令请严格按照["cmd", "/c"]
开始,由于没有在Dockerfile中没有把powershell程序的位置c:\windows\system32\WindowsPowerShell\v1.0
加入环境变量,所以不能直接运行powershell,只能用cmd来启动容器。
最后在Master执行kubectl apply -f nginx.yaml
,等待长时间的Pending状态(在拉镜像),然后在云端执行kubectl get pods -o wide
,可以看见Pod已经开始运行,并且已经成功分配了IP:
当然这个IP从云端是ping不通的,因为我们还没有部署Edgemesh,但是可以在Windows server宿主机上ping通:
在主机访问映射的主机端口9002,可以看见nginx的欢迎页面:
至此,测试成功!🎉
一些可能会用到的网址
- Windows 容器最精简的镜像,类比alpine:https://github.com/microsoft/windows-host-process-containers-base-image
- 运行容器报错
starting container fails with: The system cannot find the file specified.: unknown
:https://github.com/containerd/containerd/issues/6300 - 和Dockerfile CMD相关报错的讨论:https://github.com/containerd/containerd/issues/5067
- CNI相关问题排查:https://kubernetes.io/docs/tasks/administer-cluster/migrating-from-dockershim/troubleshooting-cni-plugin-related-errors/#:~:text=incompatible%20CNI%20versions%3B,pod%20may%20run
- HCN配置格式:https://learn.microsoft.com/zh-cn/windows-server/networking/technologies/hcn/hcn-json-document-schemas