WebAssembly (Wasm)技术的高性能和安全性正让我们越来越重视它的实际应用。我决定了解它是什么以及它是如何运作的。我想尝试在Kubernetes中使用Wasm——这样我就能享受到Kubernetes的所有好处,例如资源共享、容错性、可扩展性等。
但是,在普通的 Kubernetes 集群上运行 Wasm 应用并不像听起来那么简单,因为在工作节点上设置运行时环境相当复杂。内置的 K8s 工具并没有设计得对普通用户来说容易定制节点。这种方法的缺点在于,如果你需要尝试不同的运行时或运行大量应用程序,你希望集群扩展尽可能简单轻松。在这种情况下,以声明性的方式管理节点也非常合适。因此,我想尝试使用 Deckhouse Kubernetes 平台 (DKP) 来运行 Wasm 应用程序。该平台极大地简化了 Kubernetes 集群的部署和管理过程。
我叫叶戈尔·拉扎列夫,在 Flant 的 DevOps 工程师。在这篇文章里,我会教你如何在 DKP 管理的 Kubernetes 集群里运行 Wasm 应用。我们将搭建环境,安装所需的组件,并运行一个简单的 WebAssembly 程序。
我觉得把常规工作负载和Wasm工作负载分开是有道理的,这样可以为实验工作负载分配专门的节点。为此,我们创建一个NodeGroup,平台将使用它来管理单独的节点。在配置NodeGroup时,你应该为这些节点添加标签。这样,你可以使用NodeSelector将工作负载分配给合适的节点。
kubectl create -f -<<EOF
apiVersion: deckhouse.io/v1
kind: 节点组
metadata:
name: wasm
spec:
云实例:
类型引用:
类型: YandexInstanceClass
名称: worker
maxPerZone: 1
minPerZone: 1
区域:
- ru-central1-a
中断策略:
批准模式: 自动
kubelet:
容器日志最大文件数: 4
容器日志最大文件大小: 50Mi
资源预留:
模式: 自动
节点模板:
标签:
node.deckhouse.io/group: wasm
节点类型: 云暂态
EOF
一旦NodeGroup被创建,DKP将在eu-west-1a
区域中为AWSInstanceClass=worker
实例类型创建一台虚拟机,并在该虚拟机上添加node.deckhouse.io/group=wasm
标签。
Kubernetes 需要一个专门的运行时,即 WebAssembly System Interface (WASI),来运行 Wasm 应用程序。在本文中,我们将使用 WasmEdge。除此之外,我们还需要更新 containerd 配置,以便支持新的运行时环境。NodeGroupConfiguration 资源使你能够在节点上运行 bash 脚本,因此我们将使用它来安装 WasmEdge 并进行一些额外的设置。
检查是否已存在WASI bin文件,若不存在则下载它。接下来,使用bashbooster将主containerd配置与/etc/containerd/conf.d/*.toml
中的配置合并。修改/etc/containerd/config.toml
这会促使containerd重启:
kubectl create -f -<<EOF
apiVersion: deckhouse.io/v1alpha1
kind: NodeGroupConfiguration
metadata:
name: wasm-additional-shim.sh
spec:
bundles:
- '*'
content: |
[ -f "/bin/containerd-shim-wasmedge-v1" ] || curl -L https://github.com/containerd/runwasi/releases/download/containerd-shim-wasmedge%2Fv0.3.0/containerd-shim-wasmedge-$(uname -m | sed s/arm64/aarch64/g | sed s/amd64/x86_64/g).tar.gz | tar -xzf - -C /bin
mkdir -p /etc/containerd/conf.d
bb-sync-file /etc/containerd/conf.d/additional_shim.toml - containerd-config-changed << "EOF"
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge]
runtime_type = "io.containerd.wasmedge.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge.options]
BinaryName = "/bin/containerd-shim-wasmedge-v1"
EOF
nodeGroups:
- "节点组"
weight: "权重"
EOF
定义新的运行时类别
安装了 WasmEdge 后,你需要定义一个新的 Runtime 类。这将让你可以指定如何运行特定的工作负载:使用默认的运行时,或者通过在 spec.runtimeClassName
中明确指定另一个运行时。
kubectl apply -f -<<EOF
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: wasmedge
handler: wasmedge
EOF
运行测试 Wasm 应用程序
首先,确保平台已经完成了节点配置,并且更新了containerd的配置。
root@wasm:~# 用 `grep` 命令搜索 wasm 关键词在容器配置文件中
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge]
runtime_type = "io.containerd.wasmedge.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge.options]
BinaryName = "/bin/containerd-shim-wasmedge-v1" # 二进制文件名,这里指的就是容器运行时的启动程序
现在你可以运行这个测试的Wasm应用程序了。为此,你需要创建一个Job,并使用一个简单的WebAssembly模块。然后,你需要在Job中指定NodeSelector,并使用新创建的wasmedge
RuntimeClass。
kubectl apply -f -<<EOF
apiVersion: batch/v1
kind: 任务
metadata:
name: wasm-test
规范:
模板:
规范:
容器:
- image: wasmedge/example-wasi:latest
name: wasm-test
资源: {}
重启策略: Never
运行时类名: wasmedge
节点选择器:
node.deckhouse.io/group: wasm
回退限制: 1
EOF
检查 pod 的状态和日志内容,确保一切运行顺畅,看看是否有任何问题。
root@test-master-0:~# kubectl get pods
NAME 准备就绪 状态 重启次数 存活时间
wasm-test-2g5jl 0/1 已完成 0 18秒
root@test-master-0:~# kubectl logs wasm-test-2g5jl
随机数: -700610054
随机字节: [163, 184, 229, 154, 4, 145, 145, 96, 181, 77, 64, 159, 123, 45, 5, 134, 93, 193, 207, 74, 129, 113, 204, 174, 188, 152, 172, 151, 125, 78, 199, 177, 127, 112, 116, 255, 188, 180, 47, 110, 22, 241, 63, 87, 78, 168, 36, 202, 168, 90, 248, 79, 38, 59, 204, 128, 141, 92, 209, 205, 129, 51, 71, 214, 91, 237, 115, 145, 77, 136, 166, 115, 221, 66, 123, 186, 19, 39, 122, 204, 103, 221, 89, 97, 148, 57, 250, 255, 165, 53, 14, 241, 97, 138, 147, 201, 204, 29, 76, 219, 128, 48, 143, 165, 138, 231, 62, 235, 190, 94, 142, 63, 197, 37, 57, 241, 33, 99, 240, 215, 216, 33, 68, 141, 82, 21, 152, 93]
来自 wasi 的打印: 这是主函数中的内容
这是主函数中的内容
环境变量如下:
KUBERNETES_SERVICE_PORT_HTTPS: 443
KUBERNETES_PORT_443_TCP: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP_ADDR: 10.222.0.1
KUBERNETES_PORT_443_TCP_PROTO: tcp
KUBERNETES_SERVICE_PORT: 443
HOSTNAME: wasm-test-2g5jl
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_SERVICE_HOST: 10.222.0.1
KUBERNETES_PORT: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP_PORT: 443
参数如下:
/wasi_example_main.wasm
文件内容为这是在文件中的内容
如果是这样,pod 的状态将是 Completed
,这意味着任务已经执行完成,且一切正常。
在日志中,你应该能看到一个随机数和许多由应用程序生成的随机字节,正如预期的那样。这也说明应用程序确实可以访问环境和文件系统。
运行一个使用初始化容器的测试 Wasm 应用程序现在让我们把事情稍微难一点。经常需要在 Pod 中运行 init 或 sidecar 容器,它们基于常规的容器镜像。因此,你需要为每个容器指定不同的运行时。runtimeClassName
是在 Pod 级别定义的,而不是在单个容器级别。
Containerd 支持容器运行时的切换,因此你需要一个工具来确定为特定容器应使用哪个运行时。默认情况下,集群中常规使用的 runc 不支持这一点。幸运的是,crun 的测试版则支持这种特性。
首先,你需要自己构建crun,因为它从官方仓库使用包管理器安装时不支持WasmEdge。NodeGroupConfiguration
可以帮助你完成这个步骤。
kubectl apply -f -<<EOF
apiVersion: deckhouse.io/v1alpha1
kind: NodeGroupConfiguration
metadata:
name: crun-install.sh
spec:
bundles:
- '*'
content: |
if ! [ -x /usr/local/bin/crun ]; then
apt-get update && apt-get install -y make git gcc build-essential pkgconf libtool libsystemd-dev libprotobuf-c-dev libcap-dev libseccomp-dev libyajl-dev go-md2man autoconf python3 automake
cd /root
[ -f "/root/.wasmedge/bin/wasmedge" ] || curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
git clone https://github.com/containers/crun && cd crun
./autogen.sh
source /root/.wasmedge/env && ./configure --with-wasmedge
make
make install
cd .. && rm -rf crun
fi
echo "crun has been installed"
mkdir -p /etc/containerd/conf.d
bb-sync-file /etc/containerd/conf.d/add_crun.toml - containerd-config-changed << "EOF"
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
runtime_type = "io.containerd.runc.v2"
pod_annotations = ["*.wasm.*", "wasm.*", "module.wasm.image/*", "*.module.wasm.image", "module.wasm.image/variant.*"]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
BinaryName = "/usr/local/bin/crun"
EOF
nodeGroups:
- wasm
weight: 30
EOF
上面的代码片段将安装 WasmEdge(这不同于我们在本文早些时候安装的 WasmEdge 运行时),同时安装所需的依赖项并构建 crun。此外,您还需要在 /etc/containerd/config.toml
配置文件中添加一个新的运行时容器,就像我们之前为 Wasm 添加运行时那样。
请注意 pod_annotations
:这是一个要传递给运行时环境和容器OCI注解的列表。稍后我会解释为什么这一步是必需的。
接下来,创建一个新的运行时类:
kubectl apply -f -<<EOF
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: crun
handler: crun
EOF
应用RuntimeObject类(RuntimeClass)配置到crun运行时。
现在来试试运行你的工作负载。
kubectl apply -f -<<EOF
apiVersion: batch/v1
kind: Job
metadata:
name: wasm-test
# 元数据定义了作业的名称和注释
spec:
template:
metadata:
annotations:
module.wasm.image/variant: compat-smart
spec:
initContainers:
- name: hello
image: busybox:latest
command: ['sh', '-c', 'echo "你好,中二病!"']
# 命令: ['sh', '-c', 'echo "Hello, Medium!"']
containers:
- image: wasmedge/example-wasi:latest
name: wasm-test
resources: {}
restartPolicy: Never
runtimeClassName: crun
nodeSelector:
node.deckhouse.io/group: wasm
# 节点选择器用于选择符合特定条件的节点
backoffLimit: 1
# 后退限制定义了任务在失败后可以重新尝试的最大次数
EOF
The runtimeClassName: crun
参数表示现在用 crun
而不是默认的 runc
来启动容器。另外,module.wasm.image/variant: compat-smart
标签让 crun 使用哪种模式。
为了使这生效起来,你需要在构建WASM图像,的逗号应移除,改为在构建WASM图像后添加如下所示的OCI注解:
...
"annotations": {
"run.oci.handler": "wasm"
},
...
Crun 利用 containerd 配置中的 pod_annotations
注解以及 K8s 对象上的 compat-smart
注解来判断哪个工作负载由其自身执行,哪个交给 Wasm 运行时执行。
检查 pod 的状态和日志。你应该会看到和以前一样的情况,如下。
root@test-master-0:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
wasm-test-pn4gv 0/1 Completed 0 32s
root@test-master-0:~# kubectl logs wasm-test-pn4gv
默认的容器是 "wasm-test",它是 wasm-test 和 hello (init) 的其中之一
随机数: -158793507
随机字节: [210, 246, 181, 132, 184, 214, 110, 71, 198, 68, 154, 182, 253, 103, 116, 207, 5, 205, 185, 81, 19, 28, 61, 61, 85, 26, 222, 111, 239, 110, 21, 68, 119, 245, 153, 190, 105, 175, 191, 163, 48, 198, 41, 207, 155, 30, 122, 166, 23, 56, 59, 168, 91, 57, 103, 213, 145, 10, 130, 224, 28, 5, 73, 176, 206, 111, 37, 241, 38, 57, 98, 158, 150, 115, 249, 233, 194, 156, 13, 109, 85, 130, 232, 91, 253, 16, 8, 233, 92, 162, 237, 197, 151, 112, 52, 140, 83, 179, 31, 48, 233, 56, 54, 75, 43, 239, 233, 169, 169, 81, 36, 52, 59, 66, 102, 40, 52, 202, 34, 56, 167, 229, 197, 25, 72, 136, 147, 254]
从 wasi 打印: 这是从 main 函数输出的
环境变量如下:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME: wasm-test-pn4gv
KUBERNETES_PORT: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP_PROTO: tcp
KUBERNETES_PORT_443_TCP_PORT: 443
KUBERNETES_PORT_443_TCP_ADDR: 10.222.0.1
KUBERNETES_SERVICE_HOST: 10.222.0.1
KUBERNETES_SERVICE_PORT: 443
KUBERNETES_SERVICE_PORT_HTTPS: 443
HOME: /
命令行参数如下:
/wasi_example_main.wasm
文件内容是 This is in a file
查看初始化容器的日志信息。
root@test-master-0:~# kubectl logs wasm-test-pn4gv -c hello
你好,中型!
结论
或以更口语化的方式表达为:话说回来,结论是。
在 Kubernetes 上运行 WebAssembly 应用程序可能听起来挺难的,但使用 Deckhouse Kubernetes 平台,这个过程变得相当容易。本文将详细介绍如何设置环境、安装所需组件以及运行一个测试的 Wasm 应用程序。希望这些信息能对你有所帮助。
DKP提供了许多用于管理Kubernetes集群的特性。我们将在接下来的文章中分享新的实践和技巧。请拭目以待!
随时可以在下方评论中提出你的问题并提出建议。你也可以通过Deckhouse 电报群提问,或在Deckhouse 项目在 GitHub 上的仓库中提交一个 Issue。我们很乐意帮助你。如果你喜欢这个项目,别忘了给它点个赞 这里。