引子
解决一些目前k3s在RiscV开发板上存在的问题。
kubectl&crictl
kubectl和crictl都是k3sCommandAPI中的命令,但是二者的运行结果却有所差异,我们可以从其差异中找到二者在使用上的异同。
不过需要提前强调的是,kubectl命令是从kubelet的角度看的逻辑状态,而crictl是从底层容器运行时的视角看的底层容器状态。
这是kubectl get pods的结果
kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-696579c6c8-v2wns 1/1 Running 1 (7m57s ago) 30h
b6 0/1 ErrImagePull 0 (6d23h ago) 7d19h
这是crictl pods的结果
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
cf657c901892e 5 minutes ago Ready helm-install-traefik-pv4hv kube-system 5 (default)
c60416658550d 5 minutes ago Ready local-path-provisioner-7b7dc8d6f5-48jjf kube-system 5 (default)
33c39dd34daff 5 minutes ago Ready helm-install-traefik-crd-ktfth kube-system 5 (default)
294e3dec339e2 5 minutes ago Ready metrics-server-668d979685-jthzj kube-system 5 (default)
8b67484f1bfe4 5 minutes ago Ready redis-696579c6c8-v2wns default 1 (default)
04ff59ad5464f 5 minutes ago Ready b6 default 5 (default)
2e1c2f055fb3e 5 minutes ago Ready coredns-b96499967-cpbrk kube-system 5 (default)
e0a0516a940da 30 hours ago NotReady redis-696579c6c8-v2wns default 0 (default)
ec8be446fcabf 7 days ago NotReady b6 default 0 (default)
首先从结果字段中来看,这些字段的含义如下:

可以注意到,crictl pods中所获得的pods,并不是我们想象中的业务pods,而是Pod sandbox,这一点我们可以根据获取Pod的UID来判断——我们都知道每个Pod都会有自己的UID来进行区分。
kubectl get pod redis-696579c6c8-v2wns -o jsonpath='{.metadata.uid}{"\n"}'
a72e1056-01c3-4f74-80d9-3a7ed79fb3c2
可以发现其UID与当前crictl中的PodID根本不同,也进一步印证了我们的想法。同时,使用crictl inspectp <PODID>可以查到相同的uid,再次印证。
crictl inspectp 8b67484f1bfe4
{
"status": {
"id": "8b67484f1bfe48c4de3cd5b8ee5f343013ae6236491622ee82fb8e865fce0d7f",
"metadata": {
"attempt": 1,
"name": "redis-696579c6c8-v2wns",
"namespace": "default",
"uid": "a72e1056-01c3-4f74-80d9-3a7ed79fb3c2"
},
"state": "SANDBOX_READY",
.....
而其中的state完整结果其实是SANDBOX_READY,又证实了我们之前说的,crictl获得的是Pod sandbox。
所以我会问为什么呢?这一定跟k3s中pod的启动流程有关吧。这里我们简单地描述一下pod的启动流程,后续会根据源码进行剖析。
- kubectl apply -f xx.yaml
- kubenetes API Server
- 调度Scheduled
- kubelet检测到新Pod启动
- kubelet调用容器运行时(containerd)
- containerd创建Pod Sandbox
- 拉取业务容器镜像,启动容器
- kubelet监听容器状态,返回给API Server
而Pod Sandbox是运行时为Pod所创建的一个隔离环境,在这个环境中要有network namespace,一个最小的容器pause容器作为网络占位符,只有这样,Pod内的多个容器才可以共享一个网络空间。
所以回到最初,这两条命令的视角就不同,crictl是从容器运行时出发,关注的是Sandbox的状态;而kubectl是从更高的视角出发,其关注的是业务pods(包括业务容器)的运行状态,是从kubectl那里汇报的最新状态。
检查关键组件容器状态
journalctl -u k3s -f
# 结果
peneuler-riscv64 k3s[2438]: E0409 18:38:08.359021 2438 pod_workers.go:951] "Error syncing pod, skipping" err="failed to \"StartContainer\" for \"metrics-server\" with ImagePullBackOff: \"Back-off pulling image \\\"rancher/mirrored-metrics-server:v0.5.2\\\"\"" pod="kube-system/metrics-server-668d979685-jthzj" podUID=e7c219e4-2701-4924-9d7b-84fea6f8274b
4月 09 18:38:08 openeuler-riscv64 k3s[2438]: E0409 18:38:08.359159 2438 pod_workers.go:951] "Error syncing pod, skipping" err="failed to \"StartContainer\" for \"helm\" with ImagePullBackOff: \"Back-off pulling image \\\"rancher/klipper-helm:v0.7.3-build20220613\\\"\"" pod="kube-system/helm-install-traefik-crd-ktfth" podUID=3192257c-7e69-4c14-9b4e-d607ce188a88
4月 09 18:38:08 openeuler-riscv64 k3s[2438]: E0409 18:38:08.359193 2438 pod_workers.go:951] "Error syncing pod, skipping" err="failed to \"StartContainer\" for \"busybox\" with ImagePullBackOff: \"Back-off pulling image \\\"riscv64/busybox:latest\\\"\"" pod="default/b6" podUID=e423f26e-efd9-44bd-a65b-41dc722f3eb2
4月 09 18:38:09 openeuler-riscv64 k3s[2438]: E0409 18:38:09.604483 2438 resource_quota_controller.go:413] unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request
4月 09 18:38:09 openeuler-riscv64 k3s[2438]: W0409 18:38:09.639045 2438 garbagecollector.go:747] failed to discover some groups: map[metrics.k8s.io/v1beta1:the server is currently unable to handle the request]
4月 09 18:38:11 openeuler-riscv64 k3s[2438]: E0409 18:38:11.357904 2438 pod_workers.go:951] "Error syncing pod, skipping" err="failed to \"StartContainer\" for \"local-path-provisioner\" with ImagePullBackOff: \"Back-off pulling image \\\"rancher/local-path-provisioner:v0.0.21\\\"\"" pod="kube-system/local-path-provisioner-7b7dc8d6f5-48jjf" podUID=a4b291fd-db16-498d-a136-9a1432f4649f
4月 09 18:38:14 openeuler-riscv64 k3s[2438]: E0409 18:38:14.357404 2438 pod_workers.go:951] "Error syncing pod, skipping" err="failed to \"StartContainer\" for \"coredns\" with ImagePullBackOff: \"Back-off pulling image \\\"rancher/mirrored-coredns-coredns:1.9.1\\\"\"" pod="kube-system/coredns-b96499967-cpbrk" podUID=04219540-713f-44d1-9d2d-7d5acf08222c
......
ps:pod_workers.go来自于k8s的源码
可以发现首先存在的问题是有一些系统镜像在kube-system下的例如helm-install-traefik-crd-ktfth等存在镜像拉取失败问题,在K3sEP02解决RiscV开发板镜像无法拉取问题这篇文章中,我们解决了pause镜像拉取失败问题,当时pause镜像作为最基础的镜像如果拉取失败会导致整个节点无法使用crictl命令拉取镜像。
而这些系统镜像虽然没有直接影响我们k3s的启动,但对于后续的操作产生了较大的影响例如无法暴露服务,Pod间无法进行通信等等问题。
使用以下命令查看pod状态,发现有几个处于镜像拉取失败,并且可以与上面我们使用crictl pods命令的结果对应上。
kubectl get pods -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
default redis-696579c6c8-v2wns 1/1 Running 1 (148m ago) 32h 10.42.0.35 openeuler-riscv64 <none> <none>
kube-system helm-install-traefik-crd-ktfth 0/1 ImagePullBackOff 0 8d 10.42.0.37 openeuler-riscv64 <none> <none>
kube-system coredns-b96499967-cpbrk 0/1 ImagePullBackOff 0 8d 10.42.0.33 openeuler-riscv64 <none> <none>
kube-system metrics-server-668d979685-jthzj 0/1 ImagePullBackOff 0 8d 10.42.0.36 openeuler-riscv64 <none> <none>
kube-system helm-install-traefik-pv4hv 0/1 ImagePullBackOff 0 8d 10.42.0.39 openeuler-riscv64 <none> <none>
kube-system local-path-provisioner-7b7dc8d6f5-48jjf 0/1 ImagePullBackOff 0 8d 10.42.0.38 openeuler-riscv64 <none> <none>
default b6 0/1 ImagePullBackOff 0 (7d1h ago) 7d21h 10.42.0.34 openeuler-riscv64 <none> <none>
其中CoreDNS是一个很核心的系统组件,如果没有它,我们的Pod间无法进行通信,所以接下来我们先处理CoreDNS镜像的拉取失败问题。
CoreDNS镜像
CoreDNS作为Namespace为kube-system的系统级别镜像,用来实现动态可靠的服务发现.
两个Pod之间进行通信无需硬编码 Pod IP,只需要通过服务名称 service.default.svc.cluster.local 就可以发起请求
这其中 CoreDNS 会与 API Server 交互以获取服务信息——API Server 返回该 Service 的 ClusterIP 和端口。
- PodA 发起 DNS 查询-> CoreDNS ->查询调用 API Server -> API Server 响应 PodB 的 JSON 信息-> CoreDNS 构造 DNS 响应返回给 PodA
与此同时,如果某些 Pod 发生了重启或者迁移,CoreDNS 也会自动更新DNS记录。
接下来我们看看如何将 CoreDNS 镜像移植到我们的开发板上。
首先执行命令确定发现 Coredns 服务并不存在于当前的 k3s 中。
kubectl -n kube-system get svc coredns
Error from server (NotFound): services "coredns" not found
我们默认的 k3s 会去拉取 rancher/mirrored-coredns-coredns:1.9.1 这个镜像,所以我们要自己构建这个镜像在我们的私有仓库中,然后在开发板上拉取这个镜像进行替换使用。
类似于我们当时处理 pause 镜像,不同的是,pause 镜像在 k8s 的源码中是有其源码的,而 coreDNS 本质上是一个外部项目,所以我们需要从其 Github 的源码入手。
1.方法一:本地交叉编译
因为 CoreDNS 是用 Golang 编写的,而 Go 是可以指定架构进行编译的,并且支持 riscv64,所以我的初步思路是将其源码下载到我的mac上并指定为riscv64进行交叉编译。
git clone git@github.com:coredns/coredns.git
git checkout v1.9.1
GOOS=linux GOARCH=riscv64 make
但是这样在构建的过程中会遇到问题
link: golang.org/x/net/internal/socket: invalid reference to syscall.recvmsg
make: *** [coredns] Error 1
GOARCH=riscv64 时,Go 编译器在交叉编译时找不到适用于 RISC-V 架构的某些 syscall 实现,比如 syscall.recvmsg,这是 CoreDNS 或其依赖包 golang.org/x/net/internal/socket 里的系统调用。 这样的问题在网络上也很常见,如这个例子,可以发现 Go 标准库并没有做到实现所有架构的 syscall 支持,特别是一些底层的网络系统调用。
下面给出源码中的 Makefile,可以发现其在构建时会去执行go get,这就是为什么会去找这个系统调用,也是为什么我选择先在mac上进行交叉编译(因为开发板的网络问题)
# Makefile for building CoreDNS
GITCOMMIT?=$(shell git describe --dirty --always)
BINARY:=coredns
SYSTEM:=
CHECKS:=check
BUILDOPTS?=-v
GOPATH?=$(HOME)/go
MAKEPWD:=$(dir $(realpath $(firstword $(MAKEFILE_LIST))))
CGO_ENABLED?=0
GOLANG_VERSION ?= $(shell cat .go-version)
export GOSUMDB = sum.golang.org
export GOTOOLCHAIN = go$(GOLANG_VERSION)
.PHONY: all
all: coredns
.PHONY: coredns
coredns: $(CHECKS)
CGO_ENABLED=$(CGO_ENABLED) $(SYSTEM) go build $(BUILDOPTS) -ldflags="-s -w -X github.com/coredns/coredns/coremain.GitCommit=$(GITCOMMIT)" -o $(BINARY)
.PHONY: check
check: core/plugin/zplugin.go core/dnsserver/zdirectives.go
core/plugin/zplugin.go core/dnsserver/zdirectives.go: plugin.cfg
go generate coredns.go
go get
.PHONY: gen
gen:
go generate coredns.go
go get
.PHONY: pb
pb:
$(MAKE) -C pb
.PHONY: clean
clean:
go clean
rm -f coredns
如何解决呢?可能得去修改 coreDNS 的源码关于系统调用这块了,但是本着不入侵编程的思想(懒比的思想)我们去找其他方法。
2.方法二:找到官方提供的支持riscv的Release
你别说,还真有,只是版本较新,来到了v1.12.1,所以我们暂时先用这个新版本,后续看是否会与开发板上需要的v1.9.1发生冲突。
wget https://github.com/coredns/coredns/releases/download/v1.12.1/coredns_1.12.1_linux_riscv64.tgz
tar -zxvf coredns_1.12.1_linux_riscv64.tgz
构建Docker镜像
FROM alpine:latest
# 换源
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#http://mirrors.aliyun.com/alpine#g' /etc/apk/repositories
WORKDIR /root
COPY coredns /coredns
CMD ["/coredns"]
构建镜像并推送至私有仓库,关于私有仓库见移植镜像这篇文章。
docker build --network host --platform=linux/riscv64 -f Dockerfile.coredns -t 192.168.173.76:6000/riscv64/coredns:1.0 .
docker push 192.168.173.76:6000/riscv64/coredns:1.0
然后我们回到开发板上进行拉取并进行类似于pause镜像的适配crictl pull riscv64/coredns:1.0
首先看看当前镜像Pod的描述状态
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl describe pod <corednsPodName> -n kube-system
得到的结果如下:
Warning Failed 17m kubelet Failed to pull image "rancher/mirrored-coredns-coredns:1.9.1": rpc error: code = DeadlineExceeded desc = failed to pull and unpack image "docker.io/rancher/mirrored-coredns-coredns:1.9.1": failed to resolve reference "docker.io/rancher/mirrored-coredns-coredns:1.9.1": failed to do request: Head "https://registry-1.docker.io/v2/rancher/mirrored-coredns-coredns/manifests/1.9.1": dial tcp [2a03:2880:f134:83:face:b00c:0:25de]:443: i/o timeout
Warning Failed 15m kubelet Failed to pull image "rancher/mirrored-coredns-coredns:1.9.1": rpc error: code = DeadlineExceeded desc = failed to pull and unpack image "docker.io/rancher/mirrored-coredns-coredns:1.9.1": failed to resolve reference "docker.io/rancher/mirrored-coredns-coredns:1.9.1": failed to do request: Head "https://registry-1.docker.io/v2/rancher/mirrored-coredns-coredns/manifests/1.9.1": dial tcp [2a03:2880:f136:83:face:b00c:0:25de]:443: i/o timeout
Warning Failed 15m (x4 over 19m) kubelet Error: ErrImagePull
Warning Failed 15m (x6 over 18m) kubelet Error: ImagePullBackOff
Normal Pulling 14m (x5 over 19m) kubelet Pulling image "rancher/mirrored-coredns-coredns:1.9.1"
Normal BackOff 4m24s (x47 over 18m) kubelet Back-off pulling image "rancher/mirrored-coredns-coredns:1.9.1"
从k3s的源码 manifests 得知,这些系统级别的镜像(容器)例如traefik,coredns 都在这个目录中以 yaml 文件的形式存在. 这会在 k3s 启动时自动对其进行 Pod 的构建,相当于这是 k3s 自带的 Pod 们。

具体在开发板上的路径是/var/lib/rancher/k3s/server/manifests
现在,我们的思路变成——修改coredns.yaml文件使k3s在构建coredns容器时使用我们私有镜像仓库中的构建的来自于官方v1.12.1中适合riscv64的镜像。 于是我先将其打标签
# 可以用crictl images | grep coredns查看
ctr -n k8s.io images tag <拉取下来的镜像名> docker.io/rancher/mirrored-coredns-coredns:1.9.1
非常值得注意的是,这里打标签前面必须要有docker.io,如果没有的话可能会出现无法识别本地镜像的问题.具体见Github上的相关问题。
之后修改coredns.yaml,将其中镜像名称改为 docker.io/rancher/mirrored-coredns-coredns:1.9.1,拉取策略改为 Never ,并且在arg参数上面添加一行command: ["/coredns"]
最后,删除对应的Pod,k3s会自动重新拉取镜像并构建此Pod,再来检查其运行状态与构建过程。
kubectl delete pod -n kube-system -l k8s-app=kube-dns
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl describe pod <corednsPodName> -n kube-system
最终得到结果如下
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 34s default-scheduler Successfully assigned kube-system/coredns-fcc987b6f-nkn92 to openeuler-riscv64
Normal Pulled 33s kubelet Container image "docker.io/rancher/mirrored-coredns-coredns:1.9.1" already present on machine
Normal Created 33s kubelet Created container coredns
Normal Started 33s kubelet Started container coredns
Warning Unhealthy 2s (x18 over 33s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 503
这里虽然出现503错误但是只是健康检测出现问题,可能是配置有误或者插件失败,但是至少容器coredns创建成功了.
并且再次执行journalctl -u k3s -f后可以发现之前的 coredns 创建失败的日志不见了。
解决遗留问题
上面只是解决了CoreDNS组件的镜像拉取问题,但是拉取下来之后其还存在一些其他问题,如果你在上面的构建过程中也遇到了类似问题,请见下方处理方案。
问题1:启动覆盖问题
首先,如果我们下一次重启k3s,就会发现 Coredns 这个容器又构建失败了,并且原因正是我们修改过的 coredns.yaml 文件。
Warning Failed 72s (x2 over 73s) kubelet Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "-conf": executable file not found in $PATH: unknown
这是因为k3s会在重启时自动恢复一些核心组件的yaml文件为默认值,覆盖掉我们的更改。 在K3s官方的Issue中也有类似的问题,而官方文档中的说法也提到了Auto-Deploying Manifests。
所以,根据官方文档中的内容,我们应该先去更改k3s的启动命令,而我们的k3s是由systemd启动的,所以执行sudo systemctl edit k3s,在ExecStart的最后添加上--disable=coredns
For example, to disable traefik from being installed on a new cluster, or to uninstall it and remove the manifest from an existing cluster, you can start K3s with –disable=traefik. Multiple items can be disabled by separating their names with commas, or by repeating the flag.
ExecStart=/usr/local/bin/k3s \
server \
--disable=coredns \
修改 yaml 文件使得 k3s 在启动时使用我们自定义的 yaml 文件。修改内容同上,在 arg 前加一行command["/coredns"]
cp coredns.yaml custom-coredns.yaml
重启服务
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl restart k3s
至此,可以解决启动覆盖问题,即k3s不会在每次启动时都用默认的coredns.yaml来启动corednsPod,而是使用我们自定义的custom-coredns.yaml文件来进行。
如果后续我们自己从头构建适合 riscv 的k3s可执行文件,直接从源码层面修改镜像名称即可,不用这么麻烦.
问题2:Readiness probe failed
虽然我们使用了本地镜像并且修改了 k3s 默认的关于 coreDNS 的配置,但是仍然会遇到问题. 即上面提到的这个 Readiness probe 的网络问题.
Readiness probe failed: HTTP probe failed with statuscode: 503
对于这个问题,目前我还没有找到合适的解决方法,我个人倾向于是开发板 iptables 的问题,在这里我放一些排查的手段以供后面的操作。
# 查看当前的Corefile内容
kubectl -n kube-system get configmap coredns -o yaml
# 进入Pod调试(但是当前的Pod不断地重启)
kubectl -n kube-system exec -it <coredns-pod-name> -- sh
curl -v localhost:8080/health
# /etc/resolv.conf配置可能出现问题,无法访问到其中的DNS,如果可以进入Pod,检查是否可以访问设置的DNS
wget -O- http://192.168.173.153
# 检查kube-proxy是否生效
kubectl -n kube-system get pods -l k8s-app=kube-proxy
故我们先继续解决其他系统 Pod 以及 Iptables 等问题。
总结
对于 CoreDNS 系统组件,我们发现其官方给出了支持 riscv 的版本,所以无需我们自己重新编译. 我们选择的方法是本地构建对应的镜像并拉取到开发板上,并修改掉源码中的名称为我们构建的名称,最终实现了镜像的替换.
但是这是否太过于依赖于本地的构建?我们应该能做到从某个能够拉取的地址自动拉取,可以将docker.io这个修改为我们自己的服务器吗?
这是我们后续要探索的方向之一.
export INSTALL_K3S_EXEC="--system-default-registry=your.registry.com"
INSTALL_K3S_SKIP_DOWNLOAD=true bash k3s-install.sh
后续更新
最近在 k3s 的官方源码 fork 中找到了一位大佬的 fork,其对于 coredns.yaml 只进行了以下的修改:
image: "%{SYSTEM_DEFAULT_REGISTRY}%rancher/mirrored-coredns-coredns:1.11.3"
image: "%{SYSTEM_DEFAULT_REGISTRY}%coredns/coredns:1.11.3"
系统默认镜像一般是docker.io,所以其只是改了个名字?
后续我们在自行构建的时候,暂时决定将这些系统镜像全部放在自己的私有镜像仓库中, 然后在启动 k3s 的时候指定默认仓库为私有镜像仓库.
这里是LTX,感谢您阅读这篇博客,人生海海,和自己对话,像只蝴蝶纵横四海。