平时除了维护公司和私人在公有云的 Kubernetes clusters 之外,个人网络环境下还有些需要运行在本地的 workload;比如用于监控本地路由设备(~作~ XD)的 Prometheus exporters 和一些新奇玩意儿。为了能够运行这些应用,我在家组建了一套「边缘计算集群」,来看看是怎么做的吧。
硬件准备
我手头上目前有一台 Raspberry Pi 3 B+,我想使用它作为 Master 节点:
和两块 Nanopi NEO 2:
搭建集群
K3s
为了能够适应较低的计算性能,我选择了使用 K3s 部署 Kubernetes 集群。K3s 是一款 Rancher 开源的轻量 Kubernetes 实现,主要目标为物联网和边缘计算等场景。
如果你是在搭建测试集群,不妨试试 Minikube 和 MicroK8s,它们能够提供更加接近生产环境集群的体验。
不同于以上两款产品,K3s 除了更加轻量外,还支持多节点,因此比较符合我的使用场景。
以上产品的详细对比可参考 这篇帖子。
K3sup
K3sup 是由 OpenFaaS 的创始人 Alex Ellis 开发的一款小工具,可用于快速部署 K3s 节点,例如部署 master node:
k3sup install --ip "$MASTER" --user pi
执行以上命令,K3sup 将以用户 pi
的身份通过 SSH 连接 $MASTER
(也就是作为 Master 节点的树莓派 IP 地址),在下载并安装 K3s 后,K3sup 会将生成的 Kubeconfig 从远端拉取到本地的工作目录中。
接下来部署 worker node:
k3sup join --ip "$WORKER" --server-ip "$MASTER" --user pi
随后即可通过 kubectl
管理集群了:
export KUBECONFIG="$(pwd)/kubeconfig"
kubectl get node
具体的安装过程限于篇幅不再详述,可参考 Alex Ellis 的这篇博客。
使用 NodeAffinity 处理不同 CPU 架构问题
根据上图可以发现,树莓派的 CPU arch 是 arm
,而 NanoPi 是 arm64
。为了能够将对应其架构的容器镜像调度到正确的节点,使用 NodeAffinity 是解决方案之一。例如部署内网穿透项目 inlets:
apiVersion: apps/v1
kind: Deployment
metadata:
name: inlets-arm64
spec:
selector:
matchLabels:
app: inlets
replicas: 1
template:
metadata:
labels:
app: inlets
spec:
containers:
- name: inlets
image: inlets/inlets:2.6.4-arm64 # 镜像为 arm64 版本
args: [server]
affinity:
nodeAffinity:
# 在 Pod Scheduling 时强制要求
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms: # 节点选择器数组
- matchExpressions: # 匹配节点 labels
- key: kubernetes.io/arch # label 名称
operator: In # 要求满足以下任意值其一
values: [arm64] # 可指定多个值
可以看到我们在 Pod Spec 内定义了 affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms
字段,值为 NodeSelectorTerm 数组。我们定义了一条规则为:label kubernetes.io/arch
的值必须存在于数组 [arm64]
中。本例中只有单个值,因此等效于:必须等于 arm64
。
Docker Manifests
如果你想要部署的应用镜像是你自己构建的话,那么强烈推荐试试看 Docker image manifest v2 的特性 —— 可创建 manifest lists 包含多个不同 platforms 和 architectures 的 image manifests。
Docker Client 也提供了一个实验性的命令 <code>docker manifest</code> 来创建、推送 manifest lists。我结合实例来说说它的用法。
由于该命令目前是「实验性」的,首先需要通过环境变量开启才能够使用。我们顺便配置几个变量备用:
export DOCKER_CLI_EXPERIMENTAL=enabled # 开启 Docker CLI 的实验性功能
export IMAGE_REPO=vendor/app # 镜像名称,请按需填写
export IMAGE_TAG=v1.0.0 # 镜像 tag,请按需填写
假设你已经分别构建好针对 arm64
和 arm
的镜像,接下来先将它们推送到 registry:
docker push "${IMAGE_REPO}:${IMAGE_TAG}-arm64"
docker push "${IMAGE_REPO}:${IMAGE_TAG}-arm"
随后创建 manifest list 指向多个 image manifests:
docker manifest create --amend \
"${IMAGE_REPO}:${IMAGE_TAG}" \ # manifest list 名称
"${IMAGE_REPO}:${IMAGE_TAG}-arm64" \ # 针对 arm64 的镜像
"${IMAGE_REPO}:${IMAGE_TAG}-arm" # 针对 arm 的镜像
最关键的一步到了,为它们添加注解。将每个 manifest 绑定至特定的 os
和 arch
:
# 注解 arm64 image manifest
docker manifest annotate \
--os linux \ # 系统为 Linux
--arch arm64 \ # 架构为 arm64
"${IMAGE_REPO}:${IMAGE_TAG}" \ # manifest list 名称
"${IMAGE_REPO}:${IMAGE_TAG}-arm64" # 被注解的 manifest
# 注解 arm image manifest
docker manifest annotate \
--os linux \ # 系统同为 Linux
--arch arm \ # 架构为 arm
"${IMAGE_REPO}:${IMAGE_TAG}" \ # manifest list 名称
"${IMAGE_REPO}:${IMAGE_TAG}-arm" # 被注解的 manifest
此时 manifest list 已经创建好了,可使用 docker manifest inspect
命令检查一下具体信息。确认无误后,推送到 registry 即可:
docker manifest push --purge "${IMAGE_REPO}:${IMAGE_TAG}"
需要注意的是,此处的 --purge
参数是必不可少的。因为 Docker CLI 并没有提供 docker manifest remove
或是 docker manifest purge
之类的命令。如果不随着推送直接清理,那就只能到本地的 $HOME/.docker/manifests
目录手动删除了… 虽然早在 2018 年初就有人针对此问题提出 issue,但截止发稿前仍没有仓库 member 回复。
最后,使用刚刚创建的 manifest list 名称代替有后缀的 image manifest 名称即可,甚至可以增加 replicas
的数量,通过 PodAntiAffinity
刻意将多个副本部署在不同 CPU 架构的节点上而无需区分 image
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: inlets
spec:
selector:
matchLabels:
app: inlets
replicas: 2
template:
metadata:
labels:
app: inlets
spec:
containers:
- name: inlets
image: inlets/inlets:2.6.4 # 后缀 `arm64` 已被移除
args: [server]
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/arch
labelSelector:
matchLabels:
app: inlets
完。