上一篇大概介绍了istio引入了wasm以及wasm的简史介绍。虽然引入了一些知识引导读者了解一下它的简单特性,但是还有些欠缺,这里要打个补丁。详细阐述一下 wasm 构建及使用的相关知识。
计算机有一门学科叫图形学, 主要研究如何在计算机中表示图形、以及利用计算机进行图形的计算、处理和显示的相关原理和算法【科普中国】。科班比较关心,我们不用关心。这个高大上的学名让人觉得和屁民没有半毛钱的关系,然而它犹如空气一样,在应用领域大家一点都不陌生,比如游戏,视频,动画,科学计算可视化,虚拟现实,CAD等等。这些领域都是图形学的直接应用。
在CPU不是那么强劲的年代(2010年左右),PC、手机玩个游戏或者看个高清视频,风扇转的起飞,手机烫手当暖手宝。无数用户怒吼某某产品垃圾的时候,或许他们不曾想过为什么会那么烫手。原因就是游戏、视频等它涉及大量的图形(图像)计算,其数学本质就是大学第一门课程高等代数(工科叫线性代数)中的矩阵计算,准确的讲叫矩阵变换。
矩阵计算?开玩笑,考试都是用纸算呢。是的,你很牛逼。但是给你一个10阶的线程方程,用手算算?!就是10阶矩阵相乘,能保证在考场上30分钟内算出来吗?杠精会说,即使有那一样算只是时间问题。废话,这不是不能算的问题,当年我朝造原子弹还是用算盘打出来的呢,浪费了太多时间。而是要在合理的时间内得到结果才有意义,否则,玩游戏走一步等10秒才反应过来,能把手机砸了。别说几秒了,现在网络延迟几十毫秒,都会骂娘。(破解密码一样道理,不是不能破解,是破解的时间太长成本太高已经没有破解的意义,所以图灵造炸弹机破译德军密码和冯诺依曼造计算机计算弹道用就是为了加速得到结果。题外话:为了加速计算,现在有定制CPU/GPU,从硬件层面做矩阵计算,人称人工智能芯,这头衔炒作的噱头)
谈及图形学是因为web端的游戏,视频等等和PC,移动端相比目前还有差距,换言之web游戏达不到PC手机端渲染效果。原因就是JS效率不行,无法进行大量的高效的图形计算。此外,一些加解密的算法也不能放到JS来执行,因为存在一个安全性问题。
众所周知,C语言一直作为高效计算的开发语言而长期稳居高级编程语言的榜单前三(跌落的次数不多),主要原因就是简洁而高效——它能接近于底层汇编的执行速度,并且高于汇编的可编写速度。对于C++这个“要命3000”,撇开C++高级特性而纯粹给C加上面向对象的能力,也让它在科学计算中有一定使用率。(其实还有一门古老的语言在科学计算中占据半壁江山,那就是Fortran。玩过Fortran99,科学计算比C还要略胜一筹,当然语言特性比较原始)。由于C语言的历史原因,已有很多现成的C语言科学计算库。 所以最好的方式是直接复用这些计算库。
对于编译语言,它可以将高级语言翻译成中间结果。然后中间结果根据自己的需要做不同的后端实现。在这里编译器前端其实不重要,发明一个专用于计算的web语言也不是不可,但是生态很重要!!!重造轮子,重新构建生态很难。如果新语言一开始就想兼容其他语言,那还不如直接复用。所以复用了C、C++高级语言,也意味着复用了已有的编译器端,生成自己需要的中间代码,然后后面工作就交给需要的组件,想怎么处理就怎么处理好了。
思想上就是这么个理,但是细节上可能比较繁杂了。虽然Webassembly 应用程序使用高级 c / c++ API,如 c 和 c++ 标准库、 OpenGL、 SDL、 pthreads 等,上层用户可以像普通的 c / c++ 开发一样撸代码,但是后端需要通过使用 WebAssembly 实现提供的底层工具来实现它们的功能。 例如,OpenGL 在 WebGL 上执行,libc Date 和 time 方法使用浏览器的 Date 功能等等。在 Web 上,需要使用 Web API, 在其他情况下,会使用其他低级机制。
这里大致介绍了web面临的问题导致wasm的诞生以及wasm复用C/C++的原因的一点思考。
istio
单独拉取一个新的分支 envoy-wasm
用来支持wasm, 后续会合入envoy 的主分支。使用的是Google的 V8引擎。虽然Webassembly 官方只强调C/C++/Rust
,其实开源社区已有30多种语言可编译成wasm。“条条语言通罗马”,不用强调谁是最好的语言了。
“再强大的工具如果没有好的开发体验等于废品”——这不是我妄加评论,而是 istio 官方吐槽:Powerful tooling is nothing without a great developer experience
。为啥会有这样的调调呢?
因为原生态集成 wasm 是十分繁琐的,需要手动处理依赖的工具链,ABI 版本验证保证兼容、访问权限控制等等,而且部署的时候需要修改 istio-proxy
的配置文件进行部署。一编一部一套搞下来,时间没有了。十分的耗时,繁琐。而且还会遇到一些意外的问题,惊喜连连。
istio官宣使用了 Solo.io 最近发布的 WebAssembly Hub
。这是一套用于构建、部署、共享和发现 Envoy 代理 的Wasm 扩展工具和库。它处理了大量的本质问题,完全自动化了开发和部署 Wasm 扩展所需的许多步骤。 同时,它提供了一个强大的 CLI 以及一个易于使用的图形用户界面。 使用命令用户可以将构建的wasm扩展上传到 Hub 注册中心,然后通过镜像的方式部署到 Istio。
可以把 WebAssembly Hub 理解成wasm
的docker
,一人构建四处分享,一处构建任意部署。寄望于成为wasm的集散地。打造wasm的生态。
WebAssembly Hub官方有详细的文档教程,这里不会重复讲述。重点阐述一下wasme使用流程和踩过的坑。
curl -sL https://run.solo.io/wasme/install | sh
export PATH=$HOME/.wasme/bin:$PATH
如果安装不成功,到github下载release版本,然后拷贝到服务器使用即可
最新版本是
wasme version 0.0.19
wasme 提供自动化工程的命令
wasme init 工程路径/工程名称
按照提示一步步操作即可,目前只支持两种语言C++和JS,服务网格支持Istio1.5x和Goo。自动化初始化的工程主要包括bazel的构建文件,filter的配置proto文件,filter的逻辑实现filter.cc
和wasme的配置信息runtime-config.json
。如果 bazel 比较溜,可以自己手撸构建文件。不推荐手动。
其中runtime-config.json
对wasme尤为重要,它指明了运行时加载插件的ABI的版本和部署所需的rootId
具体实现比较简单了,继承抽象基类定义自己的业务Context,重载自己需要的方法,在重载方法里做自己需要业务逻辑即可(参考官方范例)。既然是重载了,那么就不可能任由用户自由发挥随便造接口了,自己的业务逻辑想怎么玩无所谓,但是onXXX方法就那么多,具体可参看基类 RootContext
和 Context
的定义。每个方法的作用参看源码注释,顾名思义即可。比如 onRequestHeaders、onResponseHeaders
处理请求/回包的头,onRequestBody、onResponseBody
处理请求/回包包体等等。
// NOLINT(namespace-envoy)
#include <string>
#include <unordered_map>
#include "proxy_wasm_intrinsics.h"
class ExampleRootContext : public RootContext {
public:
explicit ExampleRootContext(uint32_t id, StringView root_id) : RootContext(id, root_id) {}
bool onStart(size_t) override;
};
class ExampleContext : public Context {
public:
explicit ExampleContext(uint32_t id, RootContext* root) : Context(id, root) {}
void onCreate() override;
FilterHeadersStatus onRequestHeaders(uint32_t headers) override;
FilterDataStatus onRequestBody(size_t body_buffer_length, bool end_of_stream) override;
FilterHeadersStatus onResponseHeaders(uint32_t headers) override;
void onDone() override;
void onLog() override;
void onDelete() override;
};
static RegisterContextFactory register_ExampleContext(CONTEXT_FACTORY(ExampleContext),
ROOT_FACTORY(ExampleRootContext),
"my_root_id");
bool ExampleRootContext::onStart(size_t) {
LOG_TRACE("onStart");
return true;
}
void ExampleContext::onCreate() { LOG_WARN(std::string("onCreate " + std::to_string(id()))); }
FilterHeadersStatus ExampleContext::onRequestHeaders(uint32_t) {
LOG_DEBUG(std::string("onRequestHeaders ") + std::to_string(id()));
auto result = getRequestHeaderPairs();
auto pairs = result->pairs();
LOG_INFO(std::string("headers: ") + std::to_string(pairs.size()));
for (auto& p : pairs) {
LOG_INFO(std::string(p.first) + std::string(" -> ") + std::string(p.second));
}
return FilterHeadersStatus::Continue;
}
FilterHeadersStatus ExampleContext::onResponseHeaders(uint32_t) {
LOG_DEBUG(std::string("onResponseHeaders ") + std::to_string(id()));
auto result = getResponseHeaderPairs();
auto pairs = result->pairs();
LOG_INFO(std::string("headers: ") + std::to_string(pairs.size()));
for (auto& p : pairs) {
LOG_INFO(std::string(p.first) + std::string(" -> ") + std::string(p.second));
}
addResponseHeader("newheader", "newheadervalue");
replaceResponseHeader("location", "envoy-wasm");
return FilterHeadersStatus::Continue;
}
FilterDataStatus ExampleContext::onRequestBody(size_t body_buffer_length, bool end_of_stream) {
auto body = getBufferBytes(BufferType::HttpRequestBody, 0, body_buffer_length);
LOG_ERROR(std::string("onRequestBody ") + std::string(body->view()));
return FilterDataStatus::Continue;
}
void ExampleContext::onDone() { LOG_WARN(std::string("onDone " + std::to_string(id()))); }
void ExampleContext::onLog() { LOG_WARN(std::string("onLog " + std::to_string(id()))); }
void ExampleContext::onDelete() { LOG_WARN(std::string("onDelete " + std::to_string(id()))); }
需要重点强调的是这一句
static RegisterContextFactory register_ExampleContext(CONTEXT_FACTORY(ExampleContext),
ROOT_FACTORY(ExampleRootContext),
"my_root_id");
它关键在于将业务的类注册到envoy,并指定rootid,这个rootid务必和上一步 runtime-config.json
中的配置保持一致。
另外,目前 envoy 版本支持onResponseHeaders
修改包头,但是onResponseBody
不允许修改包体。这应该有它的考虑,具体原因没有深究。
然后告诉你两个消息,一个好消息,一个坏消息。好消息是年初在envoy-wasm分之中的一次提交已经支持包体的修改;坏消息是wasme还不支持,原因就是istio-proxy还没有支持。为啥我知道呢,因为我在坑里爬了很久了,哈哈哈。
此处还有一坑,贡献给大家。看看下面这两个接口原型
FilterDataStatus ExampleContext::onRequestBody(size_t body_buffer_length, bool end_of_stream)
FilterDataStatus ExampleContext::onResponseBody(size_t body_buffer_length, bool end_of_stream)
好奇第二个参数字段不?对,流结束标示。如果你把它忽略了,那么你就掉坑里了。请求包体比较小没有发生分包的时候还好,它只调用一次,当分包的时候就麻烦了。换言之,如果要提取包体数据,需要一个全局变量,并且循环判断end_of_stream
使用 getBufferBytes
提取body_buffer_length
数据,累加到全局变量上去。否则,就不是完整的数据包。
#最简单的方式,指定一个tag,tag中的YOUR_USERNAME是在 WebAssembly Hub 中注册的个人账号名
wasme build cpp -t webassemblyhub.io/$YOUR_USERNAME/modify_body_filter:v0.1 . #cpp - 是指cpp开发的wasm,点是当前目录
#高级一点 -i 可以指定构建镜像 --tmp-dir 指定临时存储目录存储wasm和缓存
wasme build cpp -i ee-builder:0.4 --tmp-dir /tmp -t webassemblyhub.io/$YOUR_USERNAME/modify-body:v0.1 .
不指定构建镜像,默认使用最新镜像。如果docker本地没有,它会自动拉取 quay.io/solo-io/ee-builder:0.0.19
。
这个构建比较耗时,构建一次在十分分钟左右。原因是通过容器构建,每次运行容器,bazel 的相关缓存,沙箱的下载等等拖慢了速度,适用bazel分析工具测得 99%时间都耗费依赖分析上面了,真实的编译时间占用很少。
个人尝试了以下几点优化,并给wasme官方的问题总结
A)
add docker volumn /tmp/cache:/tmp/cache
change bazel cmd: bazel build --experimental_repository_cache=/tmp/cache ${TARGET}
No obvious change —— fail
B)
add docker volumn /tmp/cache:/root/.cache
cost time changed from 5 minutes to 20 minutes —— fail
C)
remove docker param --rm
try build once
use the container to commit a new image
use the new image to build this project
very good, it cost only tens of seconds
but this image just for this project, not suitable for other projects. because this bazel cache path (/root/.cache/...) has the project sign.
作者 yuval-k 给我的答复是
you might get better speed by using bazel directly; using docker makes everything easy to use, but is slower, especially when using docker for mac.
also the initial build take longer as bazel downloads and configures emscripten for you.
we might be able to improve wasme to re-use the bazel cache
所以,这是一个很"虐心"的构建,每次敲下命令之后,你可以去茶水间喝杯茶去了。
wasme login #已登录可以免去此操作
wasme push webassemblyhub.io/$YOUR_USERNAME/modify_body_filter:v0.1 #构建时的tag
#查看本地有哪些构建
wasme list
它推送的原理就是将~/.wasme/store/tag签名/
目录下的四个文件上传到 webassemblyhub.io 。这四个文件分别是
descriptor.json
—— 定义文件的类型,wasm文件的sha256签名,文件大小和文件名filter.wasm
—— 构建出来的wasm插件image_ref
—— tag标签字符串runtime-config.json
—— 从工程下runtime-config.json
拷贝过来的wasme pull webassemblyhub.io/$YOUR_USERNAME/modify-body:v0.3
#部署
wasme-linux-amd64 deploy istio webassemblyhub.io/$YOUR_USERNAME/modify-body:v0.3 --id=myfilter --namespace=bookinfo --labels app=details -v
#filter资源
kubectl get envoyfilter -n bookinfo
#卸载
wasme-linux-amd64 undeploy istio --id myfilter --namespace bookinfo --labels app=details
#请求 - 这里传递一个自定义header字段用于操作包体的,官方示例是传递 name:hello world 到回包header
kubectl exec -ti -n bookinfo deploy/productpage-v1 -c istio-proxy -- curl -X POST -v --header 'x-body-modification:[{"position":"/","value":"123", "type":"string", "operation":"UPDATE"}]' -d '{}' http://details.bookinfo:9080/details/123
#清空cache
kubectl exec wasme-operator-5b7788d9bb-2wb27 -n wasme -- /usr/local/bin/wasme cache webassemblyhub.io/$YOUR_USERNAME/modify-body:v0.3 --clear-cache=true --port=0
#查看cache
kubectl exec wasme-cache-9nn5r -n wasme -- ls /var/local/lib/wasme-cache
参见Deploying Wasm Filters to Istio
如果你看到cache的操作了,这里暂且不表,下文详述此坑。
如果部署失败,istio-proxy意外crash或其他异常重启一般报如下错误
#istio-proxy
2020-03-24T08:44:03.202808Z info Envoy proxy is NOT ready: config not received from Pilot (is Pilot running?): cds updates: 1 successful, 0 rejected; lds updates: 0 successful, 1 rejected
2020-03-24T08:44:05.203378Z info Envoy proxy is NOT ready: config not received from Pilot (is Pilot running?): cds updates: 1 successful, 0 rejected; lds updates: 0 successful, 1 rejected
2020-03-24T08:44:07.202978Z info Envoy proxy is NOT ready: config not received from Pilot (is Pilot running?): cds updates: 1 successful, 0 rejected; lds updates: 0 successful, 1 rejected
2020-03-24T08:44:09.203095Z info Envoy proxy is NOT ready: config not received from Pilot (is Pilot running?): cds updates: 1 successful, 0 rejected; lds updates: 0 successful, 1 rejected
#istiod
2020-03-24T09:20:56.296215Z info ads LDS: PUSH for node:productpage-v1-846fd56669-56t57.bookinfo listeners:36
2020-03-24T09:20:56.308926Z info ads EDS: PUSH for node:reviews-v2-9d9d8d4f9-d2wdg.bookinfo clusters:36 endpoints:33 empty:4
2020-03-24T09:20:56.349413Z info ads LDS: PUSH for node:reviews-v2-9d9d8d4f9-d2wdg.bookinfo listeners:36
2020-03-24T09:20:56.416474Z info ads RDS: PUSH for node:productpage-v1-846fd56669-56t57.bookinfo routes:20
2020-03-24T09:20:56.416659Z warn ads ADS:LDS: ACK ERROR 172.16.0.166:58028 sidecar~172.16.0.166~productpage-v1-846fd56669-56t57.bookinfo~bookinfo.svc.cluster.local-8 Internal:Error adding/updating listener(s) 172.16.0.166_9080: Invalid path: /var/local/lib/wasme-cache/b9027c9d0c81aa5609d32c49633f98b4226d3b96c97bedca68c678d0434ac645
virtualInbound: Invalid path: /var/local/lib/wasme-cache/b9027c9d0c81aa5609d32c49633f98b4226d3b96c97bedca68c678d0434ac645
有一定延迟性,如果长久不行,建议重启pod。如果还是不行,可能就需要操作缓存了。
现在wasm加载的方式,通过一个cache目录挂载到proxy当中,而不是让proxy直接去拉取镜像,为什么这么做呢?因为业务pod很多的时候,以及proxy当中的插件量比较大的情况下,假设服务重启或者业务升级,就会导致proxy波峰拉取镜像的网络开销。如果webassemblyhub服务故障,那么proxy就全部无法正常启动了。proxy无法正常工作,等于服务瘫痪。这个是不能接受的。所以使用本地缓存(configmap,主机目录,wasme-cache)做一层安全防护,换言之,服务正常(异常)重启都不会去拉取镜像,而是是用本地缓存。
这样就ok吗?没有。这里又引发了新的问题,那就是使用同名tag更新wasm镜像时,由于本地有其缓存,导致重新部署插件时,不生效。可能好奇为啥要使用同名tag更新wasm镜像?因为webassemblyhub没有禁止这么做,允许覆盖更新。其次,webassemblyhub目前没有提供删除镜像操作(很奇怪)。而在开发阶段会形成很多测试版本,不是release版本,那么如果每个版本都添加一个新tag往Hub上推送就会产生一堆垃圾数据。“生而不养”的程序员都是不负责的。
这里有几个解决办法
这四种办法,其中推荐第2种,次选4。目前官方还没有给出解决方案和答复。那么眼下只能通过清缓存来解决。
#!/bin/bash
#1.删除configmap - 这里存储的缓存的key
kubectl delete cm wasme-cache -n wasme
#2.删除wasme-cache - 这里缓存的key和wasm
# key 缓存在/etc/wasme-cache/images.txt
# wasm 缓存在/var/local/lib/wasme-cache
kubectl get po -n wasme | grep cache | awk '{print $1}' | while read cache; do kubectl delete po $cache -n wasme ; done
#3.删除node缓存
rm /var/local/lib/wasme-cache/*
#重启服务
kubectl scale deployment details-v1 --replicas=1 -n bookinfo
清理完这些数据后,再重新部署插件,wasme-operator会创建wasme-cache cm以及pod会重新缓存。上述第一步不一定是必须的,这里删除它,只是保证wasme-cache pod不去拉取新数据(images.txt空)。如果没有第1步,2.3需要同时下发命令才行,因为pod由daemonSet控制的,如果先删除pod,那么它会立马拉起新的pod,读取还是旧的本地镜像。如果先删除本地缓存,pod会立马同步一份下来。所以最好的方案就是这三个全部删了。
条条大路通罗马,上面只是构建方式之一。尚在在发展中,有些问题还有待完善。所以后续发展持续关注。
由于本机安装 emcc 可能会遇到很多问题,换言之依赖太多,很难装。个人没有安装成功。所以普遍使用 docker预制的构建镜像来构建wasm。上面一小节就是方式其一。下面介绍一种和它类似的方式。istio/proxy的方式,示例在这里
这个扩展插件构建执行./build_wasm.sh 即可,看下脚本内容
set -e
docker run -e uid="$(id -u)" -e gid="$(id -g)" -v $PWD:/work -w /work -v $(realpath $PWD/../../extensions):/work/extensions gcr.io/istio-testing/wasmsdk:v3 bash /build_wasm.sh
rmdir extensions
很显然,使用 gcr.io/istio-testing/wasmsdk:v3
的 sdk 镜像构建的。这个镜像运行容器后执行的是工程下的 Makefile ,核心的两个操作 1.执行 protoc 将 proto 文件编译成头文件;2.使用 em++ 构建wasm。
这个构建速度第一次稍微慢一些,后面构建一般两三分钟可以完成。这里构建结果只有wasm文件,那面临的问题依然是手动部署的问题。
如果使用原生态的 emcc/em++ 构建可以参考 emcc的安装,个人不推荐,因为这个坑可能会比较大。原因有2点
#如果同时有Python2和3,那只能
$ python3 emsdk.py install latest
/root/emsdk/upstream/bin/llc: /lib64/libstdc++.so.6: version `GLIBCXX\_3.4.21' not found (required by /root/emsdk/upstream/bin/../lib/libLLVM-11git.so)
/root/emsdk/upstream/bin/llc: /lib64/libstdc++.so.6: version `GLIBCXX\_3.4.20' not found (required by /root/emsdk/upstream/bin/../lib/libLLVM-11git.so)
shared:ERROR: error running `llc --version`. Check your llvm installation (/root/emsdk/upstream/bin/llc)
#查询一下这个库会发现的确没有 GLIBCXX\_3.4.20
$ strings /usr/lib64/libstdc++.so.6 | grep GLIBC
#yum update libstdc++ 是和gcc版本有关的,如果版本不够,怎么升级都无用
这个问题解决办法只有一个:升级 libstdc++.so。有些发行版可以手动下载安装,but,重点来了,CentOS 7 不行,官方无解。对此只能升级内核或者升级 GCC 4.8到8以上版本,升级这两个东西都是十分耗时的工程。所以,如果你爱折腾,恭喜你,这是你的乐园。否则,请绕行——踩坑之谈。
安装成功之后,就要手动写makefile之类的编译文件了,这个倒可以参考上一小节的Makefile中的 em++ 示例。如果是c文件,使用emcc。
上面探讨了三种构建思路,它们各自有各自的问题
因此,一个不是办法的办法就是取各家之长——利用 wasmsdk 构建速度,使用 webassembly-hub/wasme 的发布。怎么操作呢?具体流程如下:
build_wasm.sh
脚本构建 wasm./gen.sh xxx.wasm
#!/bin/bash
size=`ls -l $1 | awk '{print $6}'`
dgst=`openssl dgst -sha256 $1 | awk '{print $2}'`
#filename=`basename $1`
filename="filter.wasm"
tag=`cat image_ref`
#6ff7cc14859b90200c61a05183fa8480 修订为自己的wasm tag 路径
echo -e "{\"mediaType\":\"application/vnd.module.wasm.content.layer.v1+wasm\",\"digest\":\"sha256:${dgst}\",\"size\":${size},\"annotations\":{\"org.opencontainers.image.title\":\"${filename}\"}}" > /Users/$YOUR_USERNAME/.wasme/store/6ff7cc14859b90200c61a05183fa8480/descriptor.json
cp $1 /Users/$YOUR_USERNAME/.wasme/store/6ff7cc14859b90200c61a05183fa8480/filter.wasm
/Users/$YOUR_USERNAME/.wasme/bin/wasme push "${tag}"
本文对wasm特性,生态,wasm三种构建方式以及遇到的问题的介绍。由于envoy、istio引入wasm是最近才发生的事情,因此还不是一个成熟的生态,还有很多问题需要优化解决。对新手或许有一定指引作用,也期待有兴趣的同学一起来学习研究。
更多细节请参考webassembly-hub官方文档
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。