diff --git a/content/zh/blog/haozi007/2020-09-09-isulad-benchmark.md b/content/zh/blog/haozi007/2020-09-09-isulad-benchmark.md new file mode 100644 index 0000000000000000000000000000000000000000..666a247206b2f8c549b47ebbb401889827f62605 --- /dev/null +++ b/content/zh/blog/haozi007/2020-09-09-isulad-benchmark.md @@ -0,0 +1,433 @@ ++++ +title = "iSula性能测试" +date = "2020-09-09" +tags = ["iSulad", "performance"] +archives = "2020-09" +author = "haozi007" +summary = "iSula容器引擎具有很多优点:轻、快等等。那么,如何呈现这些优点呢?这篇文章我们主要关注iSula容器引擎的“快”。" ++++ + +iSula容器引擎具有很多优点:轻、快等等。那么,如何呈现这些优点呢?这篇文章我们主要关注iSula容器引擎的“快”。为了证明“快”,那就需要有参照物进行对比。环视业内,我们发现几个能打的;容器引擎鼻祖Docker、红帽的Podman以及CRI-O。 + +目标确定了,我们开始明确对比范围了。 + +## 测试范围 + +容器引擎的使用模式主要是: + +- 客户端使用模式:多见于个人开发、测试以及部分生产场景; +- PAAS通过CRI接口使用模式:云计算的经典场景,通过CRI接口调用容器引擎能力,管理pod集群; + +为了尽量覆盖应用场景,因此我们需要覆盖上述两种场景,对客户端模式和CRI模式分别进行测试对比。 + +### 客户端模式 + +由于CRI-O不具备客户端功能,所以我们选择的测试对象是: + +- Docker +- Podman +- iSula + +### CRI模式 + +CRI接口,需要通过`cri-tools`工具进行测试。 + +为了对比的观赏性,我们在CRI模式下也选择三个测试对象: + +- Docker +- CRI-O +- iSula + +## 环境准备 + +### 机器环境 + +#### X86 + +| 配置项 | 配置信息 | +| ------ | ---------------------------------------- | +| OS | Fedora32 X86_64 | +| 内核 | linux 5.7.10-201.fc32.x86_64 | +| CPU | 48核,Intel Xeon CPU E5-2695 v2 @ 2.4GHZ | +| 内存 | 132 GB | + +#### ARM + +| 配置项 | 配置信息 | +| ------ | ------------- | +| OS | Euleros | +| 内核 | linux 4.19.90 | +| CPU | 64核 | +| 内存 | 196 GB | + +### 安装iSulad + +参考[官方文档](https://gitee.com/openeuler/iSulad/blob/master/docs/build_guide.md)安装即可。 + +```bash +$ isula version + +Client: + Version: 2.0.3 + Git commit: 3bb24761f07cc0ac399e1cb783053db8b33b263d + Built: 2020-08-01T09:40:06.568848951+08:00 + +Server: + Version: 2.0.3 + Git commit: 3bb24761f07cc0ac399e1cb783053db8b33b263d + Built: 2020-08-01T09:40:06.568848951+08:00 + +OCI config: + Version: 1.0.1 + Default file: /etc/default/isulad/config.json +``` + +### 安装cri-tools + +CRI测试,使用统一的客户端工具进行测试,选择K8S对应的`V1.15.0`版本即可。 + +```bash +$ git clone https://github.com/kubernetes-sigs/cri-tools +$ cd cri-tools +$ git checkout v1.15.0 +$ make +$ export PATH=$PATH:$GOPATH/bin +``` + +### 安装docker + +根据[官方文档](https://docs.docker.com/engine/install/fedora/)安装即可。 + +```bash +$ docker version + +Client: + Version: 19.03.11 + API version: 1.40 + Go version: go1.14.3 + Git commit: 42e35e6 + Built: Sun Jun 7 21:16:58 2020 + OS/Arch: linux/amd64 + Experimental: false + +Server: Docker Engine - Community + Engine: + Version: 19.03.11 + API version: 1.40 (minimum version 1.12) + Go version: go1.14.3 + Git commit: 42e35e6 + Built: Sun Jun 7 00:00:00 2020 + OS/Arch: linux/amd64 + Experimental: false + containerd: + Version: 1.3.3 + GitCommit: + runc: + Version: 1.0.0-rc10+dev + GitCommit: fbdbaf85ecbc0e077f336c03062710435607dbf1 + docker-init: + Version: 0.18.0 + GitCommit: +``` + +### 安装kubelet + +我们选择`V1.15.0`版本作为测试版本,下载源码`https://github.com/kubernetes/kubernetes.git`。 + +#### 准备源码 + +如果下载失败或者太慢,可以配置代理: + +```bash +# 设置国内代理 +go env -w GOPROXY=https://goproxy.cn,direct +# 设置私有仓库地址 +go env -w GOPRIVATE=.gitlab.com,.gitee.com +# 设置sum验证服务地址 +go env -w GOSUMDB="sum.golang.google.cn" +``` + +开始下载源码: + +```bash +$ cd $GOPATH/src/k8s.io +$ git clone https://github.com/kubernetes/kubernetes.git +$ cd kubernetes +$ git checkout v1.15.0 +$ go mod tidy +``` + +#### 编译 + +```bash +$ make all WHAT=cmd/kubelet +``` + +注意: + +- **K8S的版本对go的版本有要求,例如`V1.15.0`需要go 1.12版本** +- 可以使用`go mod tidy`,测试依赖代码下载,如果存在鉴权失败的仓库,可以使用`go get -v -insecure`下载 + +#### 安装 + +```bash +$ cp _output/bin/kubelet /usr/local/bin/kubelet +$ kubelet --version + Kubernetes v1.15.0 +``` + +#### 启动kubelet + +```bash +$ kubelet --network-plugin=cni --runtime-cgroups=/systemd/system.slice --kubelet-cgroups=/systemd/system.slice --cgroup-driver="systemd" --fail-swap-on=false -v 5 --enable-controller-attach-detach=false --experimental-dockershim +``` + +注:cgroup由systemd管理 + +### 安装CRI-O + +由于直接通过`dnf`安装`CRI-O`的`v1.15.4`版本有问题,所以需要源码编译安装。 + +```bash +$ dnf install glib-2.0 glibc-devel glibc-static container-common +$ git clone https://github.com/cri-o/cri-o.git +$ cd crio +$ make +$ make install +$ mkdir -p /etc/crio && cp crio.conf /etc/crio/ +``` + +### 安装podman + +直接使用`dnf`的源安装即可: + +```bash +$ dnf install -y podman + +$ podman --version + podman version 2.0.3 +``` + +## 测试方案 + +本文档主要关注容器引擎的容器生命周期的性能,所以测试方案如下: + +- 单容器的create、start、stop、rm和run等操作的性能; +- 100个容器并发create、start、stop、rm和run等操作的性能; +- 单pod的runp、stopp和rmp等操作的性能; +- 单pod包含单容器的run、stop和rm等操作的性能; +- 100个pod并发runp、stopp和rmp等操作的性能; +- 100个包含单容器的pod并发run、stop和rm等操作的性能; + +注:pod的配置,必须指定linux,不然docker会给pod创建一个默认的网卡,导致cni插件执行失败。 + +```json +{ + "metadata": { + "name": "nginx-sandbox", + "namespace": "default", + "attempt": 1, + "uid": "hdishd83djaidwnduwk28bcsb" + }, + // linux字段必须存在 + "linux": { + } +} + +``` + +### 方案详细设计 + +单次测试和并发测试虽然是两种测试场景,但是单次可以看成并发的特例。因此,设计测试用例的时候,通过控制并发数量来实现两种场景的区分。具体设计如下图: + +```mermaid +graph TD + classDef notestyle fill:#98A092,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5, 5; + classDef parallelstyle fill:#C969A3,stroke:#666,stroke-width:1px,color:#ffa,stroke-dasharray: 5, 5; + subgraph pretest; + A[download images] --> B[do clean] + end + subgraph dotest + C[foreach 1->10] + D{{runtest}} + E(remove max and min cases) + X(calculate avg of residual case) + C --> D + D --> E + E --> X + end + B --> C + subgraph runtest + R(begin test) + F>t1: get begin time point] + G(parallel run all cases) + H[wait all cases finish] + I>t2: get end time point] + R --> F + F --> G + F --> H + H -. wait .-> G + H --> I + end + R -. implements .-> D + subgraph posttest + Z[do clean] + end + X --> Z + class R notestyle; + class G parallelstyle; +``` + +### 客户端模式 + +#### X86环境测试结果 + +单容器操作性能对比 + +| 操作耗时 (ms) | Docker (avg) | Podman (avg) | iSula (avg) | VS Docker | VS Podman | +| ------------- | ------------- | ------------ | ----------- | --------- | --------- | +| create | 287 | 180 | 131 | -54.36% | -27.22% | +| start | 675 | 916 | 315 | -53.33% | -65.61% | +| stop | 349 | 513 | 274 | -21.49% | -46.59% | +| rm | 72 | 187 | 60 | -16.67% | -67.91% | +| run | 866 | 454 | 359 | -58.55% | -20.93% | + +100容器并发操作性能对比 + +| 操作耗时 (ms) | Docker (avg) | Podman (avg) | iSula (avg) | VS Docker | VS Podman | +| ------------- | ------------- | ------------ | ----------- | --------- | --------- | +| 100 * create | 4995 | 3993 | 1911 | -61.74% | -52.14% | +| 100 * start | 10126 | 5537 | 3861 | -61.87% | -30.27% | +| 100 * stop | 8066 | 11100 | 4268 | -47.09% | -61.55% | +| 100 * rm | 3220 | 4319 | 1967 | -38.91% | -54.46% | +| 100 * run | 9822 | 5979 | 4392 | -55.28% | -26.54% | + +#### ARM环境测试结果 + +单容器操作性能对比 + +| 操作耗时 (ms) | Docker (avg) | Podman (avg) | iSula (avg) | VS Docker | VS Podman | +| ------------- | ------------- | ------------ | ----------- | --------- | --------- | +| create | 401 | 361 | 177 | -55.86% | -50.97% | +| start | 1160 | 1143 | 523 | -54.91% | -54.24% | +| stop | 634 | 576 | 395 | -37.70% | -31.42% | +| rm | 105 | 398 | 89 | -15.24% | -77.64% | +| run | 1261 | 1071 | 634 | -49.72% | -40.80% | + +100容器并发操作性能对比 + +| 操作耗时 (ms) | Docker (avg) | Podman (avg) | iSula (avg) | VS Docker | VS Podman | +| ------------- | ------------- | ------------ | ----------- | --------- | --------- | +| 100 * create | 14563 | 12081 | 4172 | -71.35% | -65.47% | +| 100 * start | 23420 | 15370 | 5294 | -77.40% | -65.56% | +| 100 * stop | 22234 | 16973 | 8619 | -61.24% | -49.22% | +| 100 * rm | 937 | 10943 | 926 | -1.17% | -92.33% | +| 100 * run | 28091 | 16280 | 9015 | -67.91% | -44.63% | + +### CRI模式 + +#### X86环境测试结果 + +单pod操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| runp | 681 | 321 | 239 | -64.90% | -25.55% | +| stopp | 400 | 356 | 272 | -32.00% | -23.60% | + +单pod单容器操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| run | 1249 | 525 | 382 | -69.42% | -27.24% | +| stop | 554 | 759 | 564 | +1.81% | -25.69% | + +100并发pod操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| 100 * runp | 13998 | 4946 | 3887 | -72.23% | -21.41% | +| 100 * stopp | 8402 | 4834 | 4631 | -44.88% | -4.20% | +| 100 * rmp | 2076 | 1388 | 1073 | -48.31% | -22.69% | + +100并发pod容器操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| 100 * run | 28158 | 9077 | 5630 | -80.01% | -37.98% | +| 100 * stop | 9395 | 8443 | 8196 | -12.76% | -2.93% | +| 100 * rm | 4415 | 3739 | 1524 | -65.48% | -59.24% | + +#### ARM环境测试结果 + +单pod操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| runp | 1339 | 2366 | 536 | -59.97% | -77.35% | +| stopp | 443 | 419 | 255 | -42.44% | -39.14% | + +单pod单容器操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| run | 2069 | 3039 | 338 | -83.66% | -88.88% | +| stop | 684 | 688 | 214 | -68.71% | -68.90% | + +100并发pod操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| 100 * runp | 27802 | 29197 | 9827 | -64.65% | -66.34% | +| 100 * stopp | 14429 | 11173 | 6394 | -55.69% | -42.77% | +| 100 * rmp | 771 | 9007 | 1790 | +132.17% | -80.13% | + +100并发pod容器操作 + +| 操作耗时 (ms) | Docker (avg) | CRIO (avg) | iSula (avg) | VS Docker | VS CRIO | +| ------------- | ------------- | ---------- | ----------- | --------- | ------- | +| 100 * run | 54087 | 43521 | 5284 | -90.23% | -87.86% | +| 100 * stop | 18317 | 19108 | 2641 | -85.58% | -86.18% | +| 100 * rm | 1592 | 18390 | 2162 | +35.80% | -88.24% | + +## 总结分析 + +从测试数据来看,在容器的生命周期的操作和并发操作上面,我们iSulad都是优于其他容器引擎的。尤其是在ARM上的表现尤为出色,并发性能已经接近于X86的性能了;而其他容器引擎在ARM上面的表现不尽如人意,甚至出现性能下降1倍以上。 + +那么,我们iSulad为什么有这么大的优势呢?我觉得,主要是从下面几个方面来看。 + +- 首先,iSulad是用C/C++语言写的,而Docker/Podman/CRI-O都是用golang写的;C/C++在速度方面本身就有优势; +- 架构设计上面,相对于Docker,iSulad架构更加简单,调用链更短;而Podman是serverless模式,并发更加不具备优势; +- 在容器创建流程中,减小锁粒度、消减容器的依赖(例如镜像管理模块),从而提高了并发的性能; + +### 架构对比 + +iSulad架构设计如下: + +![arch](https://gitee.com/openeuler/iSulad/raw/master/docs/design/arch.jpg) + +Docker官网给的架构图如下: + +![Docker架构图](https://docs.docker.com/engine/images/engine-components-flow.png) + +但是,docker daemon里面还涉及到containerd和runc的流程没有描述,大体结构如下: + +```mermaid +graph LR + A(docker daemon) + B(containerd) + C(runc) + A -. grpc .-> B + B -. fork/exec .-> C +``` + +从架构来看,docker的容器生命周期流程涉及:客户端到docker daemon的restful通信;daemon到containerd的GRPC通信;然后fork执行runc。而iSulad的流程:客户端到服务端的GRPC通信,然后fork执行lxc-start。 + +## 参考文档 + +- https://stackoverflow.com/questions/46726216/kubelet-fails-to-get-cgroup-stats-for-docker-and-kubelet-services +- https://developer.aliyun.com/article/630682 +- https://blog.csdn.net/bingshiwuyu/article/details/107333259 +- https://github.com/cri-o/cri-o +- https://gitee.com/openeuler/iSulad/blob/master/docs/build_guide.md + diff --git a/content/zh/blog/haozi007/2020-09-09-isulad-json-parse.md b/content/zh/blog/haozi007/2020-09-09-isulad-json-parse.md new file mode 100644 index 0000000000000000000000000000000000000000..ddfb390fdffb0108da67a9ccde566ee8739985d0 --- /dev/null +++ b/content/zh/blog/haozi007/2020-09-09-isulad-json-parse.md @@ -0,0 +1,607 @@ ++++ +title = "iSula与JSON的斗争" +date = "2020-09-09" +tags = ["iSulad", "JSON"] +archives = "2020-09" +author = "haozi007" +summary = "iSulad是如何处理JSON的呢?" ++++ + +对于各位习惯各种高级语言的伙伴们来说,JSON的解析和生成是如呼吸般简单自然的事情。但是对于C语言,JSON的解析和生成就麻烦了。根本原因是由于C语言不支持反射,没办法对JSON作动态解析和生成。但是,容器引擎中涉及大量的JSON解析和生成。那么,我们为了更好的和JSON进行和谐相处,做了那些努力呢? + +大体上,iSula经历了几个阶段,为了更好的感受这几个阶段的差距;我觉得通过武器的不同时代来感受一下。 + +## 冷兵器时代 + +C语言还是有一些JSON解析的库的,例如`yajl`,`cjson`等等;这些库提供了把JSON字符串解析为tree结构的元素集合,然后通过遍历书可以快速的找到JSON的`key/value`的对应关系和值。而且也能自己构建对应的元素结合tree,然后生成JSON字符串。那么,如何通过这些库来做JSON和C结构体直接的相互转换呢? + +### 用法 + +以`yajl`为例,实现一个`isula_version`结构体的marshal和unmarshal. + +```c +#include +#include +#include +#include +#include + +struct isula_version { + int large; + int middle; + int small; + char *version; +}; + +void free_isula_version(struct isula_version *ptr) +{ + if (ptr == NULL) { + return; + } + free(ptr->version); + ptr->version = NULL; + free(ptr); +} + +static inline yajl_val get_val(yajl_val tree, const char *name, yajl_type type) { + const char *path[] = { name, NULL }; + return yajl_tree_get(tree, path, type); +} + +struct isula_version *unmarshal(const char *json_str) +{ + char buf[1024]; + yajl_val tree; + struct isula_version *result = NULL; + + if (json_str == NULL) { + return NULL; + } + result = calloc(1, sizeof(struct isula_version)); + if (result == NULL) { + return NULL; + } + tree = yajl_tree_parse(json_str, buf, sizeof(buf)); + if (tree == NULL) { + printf("Invalid json string: %s\n", json_str); + goto err_out; + } + { + yajl_val val = get_val(tree, "Large", yajl_t_number); + if (val != NULL) { + result->large = YAJL_GET_INTEGER(val); + } + } + { + yajl_val val = get_val(tree, "Small", yajl_t_number); + if (val != NULL) { + result->small = YAJL_GET_INTEGER(val); + } + } + { + yajl_val val = get_val(tree, "Middle", yajl_t_number); + if (val != NULL) { + result->middle = YAJL_GET_INTEGER(val); + } + } + { + yajl_val val = get_val(tree, "Version", yajl_t_string); + if (val != NULL) { + char *str = YAJL_GET_STRING(val); + result->version = strdup(str); + } + } + + goto out; +err_out: + free_isula_version(result); + result = NULL; +out: + yajl_tree_free(tree); + return result; +} + +char *marshal(struct isula_version *ptr) +{ + char *result = NULL; + const unsigned char *gen_buf = NULL; + size_t gen_len = 0; + + if (ptr == NULL) { + return NULL; + } + + yajl_gen g = yajl_gen_alloc(NULL); + yajl_gen_status stat = yajl_gen_status_ok; + + stat = yajl_gen_map_open((yajl_gen)g); + if (stat != yajl_gen_status_ok) { + goto free_out; + } + /* gen struct items */ + if (ptr->version != NULL) { + stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Version"), strlen("Version")); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)ptr->version, strlen(ptr->version)); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + } + + stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Large"), strlen("Large")); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + + stat = yajl_gen_integer((yajl_gen)g, (long long int)ptr->large); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + + stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Middle"), strlen("Middle")); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + + stat = yajl_gen_integer((yajl_gen)g, (long long int)ptr->middle); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + + stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Small"), strlen("Small")); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + + stat = yajl_gen_integer((yajl_gen)g, (long long int)ptr->small); + if (yajl_gen_status_ok != stat) { + goto free_out; + } + + stat = yajl_gen_map_close((yajl_gen)g); + if (stat != yajl_gen_status_ok) { + goto free_out; + } + + yajl_gen_get_buf(g, &gen_buf, &gen_len); + if (gen_buf == NULL) { + printf("gen buf failed\n"); + goto free_out; + } + + result = calloc(gen_len + 1, sizeof(char)); + if (result == NULL) { + printf("out of memory\n"); + goto free_out; + } + (void)memcpy(result, gen_buf, gen_len); + +free_out: + yajl_gen_clear(g); + yajl_gen_free(g); + return result; +} + +void show_isula_version(const struct isula_version *ptr) +{ + printf("iSula version: \n"); + if (ptr == NULL) { + return; + } + printf("large: %d\nmiddle: %d\nsmall: %d\n", ptr->large, ptr->middle, ptr->small); + printf("version: %s\n", ptr->version); +} + +int main() +{ + const char *json_str = "{\"Version\":\"1.0.0\", \"Large\": 1, \"Middle\": 0, \"Small\": 0}"; + struct isula_version *ptr = NULL; + char *marshaled = NULL; + + // step 1: unmarshal json string + ptr = unmarshal(json_str); + if (ptr == NULL) { + printf("unmarshal failed\n"); + return -1; + } + show_isula_version(ptr); + + // step 2: marshal isula version + free(ptr->version); + ptr->version = strdup("2.0.0"); + ptr->large = 2; + ptr->middle = 1; + ptr->small = 1; + marshaled = marshal(ptr); + printf("marshal isula version:\n\t%s\n", marshaled); + + free(marshaled); + free_isula_version(ptr); +} +``` + +执行效果如下: + +```bash +$ ./a.out +iSula version: +large: 1 +middle: 0 +small: 0 +version: 1.0.0 +marshal isula version: + {"Version":"2.0.0","Large":2,"Middle":1,"Small":1} +``` + +这种方式虽然没法和支持动态解析的语言一样高效简单,但是也算完成了任务。如果动态解析是热兵器,这个勉强能算是长矛了。 + +### 缺陷 + +从示例来看,完成一个结构体和JSON的映射大概需要160行左右的代码。而上面只是一个简单的结构体,而且有的项目有很多这种结构体需要做映射。这种原始的方式在大型项目中很难保证参与人员代码质量可控;而且效率低下。主要的缺陷总结如下: + +- 映射工作量较大; +- 对每种结构体需要单独适配代码,无法实现自动化; +- 效率低下; +- 代码质量不可控; + +## 伪热兵器时代 + +由于C不支持反射,没法做到动态解析。但是可以通过其他途径简化解析流程、提高效率、实现自动化以及实现代码质量可控。为了避免重复造论子,17年的时候发现了[libocispec项目](https://github.com/containers/libocispec),提供了一个解决C语言JSON映射的思路: + +- 通过[json schema](http://json-schema.org/)描述JSON字符串的结构信息; +- 通过python解析`json schema`信息; +- 根据`json schema`信息自动生成C结构体和JSON的映射代码; + +这种方式,可以解决上面的上一章节的几个缺陷: + +- 工作量大大减小,这需要写好`json schema`文件即可; +- 自动化解析代码工作; +- 效率很高; +- 代码质量可控,取决于于生成框架的质量; + +**注:libocispec早期只能用于解析oci spec的json,在我们发现之后,多个开发人员参与社区,提供了大量的功能升级,才有了今天的强大能力。** + +### iSula集成libocispec结构 + +iSula当前把JSON映射相关的代码,统一放到`lcr`项目中进行管理,通过一个动态库和头文件提供相应功能。 + +生成代码的开源python框架结构如下: + +```bash +$ tree third_party/libocispec/ +third_party/libocispec/ +├── CMakeLists.txt +├── common_c.py +├── common_h.py +├── generate.py +├── headers.py +├── helpers.py +├── read_file.c +├── read_file.h +└── sources.py +``` + +`json schema`文件存放结构(由于iSula涉及的所有JSON结构都在该目录下,所以存在大量的schema文件)如下: + +```bash +$ tree -d 1 src/json/schema/ +src/json/schema/ +├── cni +│   └── network +├── container +├── cri +├── docker +│   ├── image +│   └── types +├── embedded +├── host +├── image +├── imagetool +├── logger +├── oci +│   ├── image +│   └── runtime +├── plugin +├── registry +├── shim +│   └── client +└── storage +``` + +然后在`cmake`的时候,会触发python框架,根据schema目录下面所有的schema来生成对应的映射代码。会看到如下提示信息: + +```bash +$ mkdir build +$ cd build +$ cmake ../ +...... +Reflection: isulad-daemon-configs.json Success +Reflection: timestamp.json Success +Reflection: web-signature.json Success +Reflection: host-config.json Success +Reflection: defs.json Success +Reflection: config.json Success +Reflection: manifest.json Success +Reflection: layers.json Success +...... +``` + +### 用法 + +那么,现在我们如果需要对一个新的结构体和JSON进行映射,需要做的事情就是在`json schema`目录下面新增一个对应的schema文件即可。这里以上一章节的`isula_version`为例。 + +新增schema文件`isula_version.json`: + +```bash +$ cat ../src/json/schema/isula_version.json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "Version": { + "type": "string" + }, + "Large": { + "type": "int32" + }, + "Middle": { + "type": "int32" + }, + "Small": { + "type": "int32" + } + } +} +``` + +重新`cmake`,可以看到新生成了两个文件: + +```bash +$ ls build/json/isula_version.* +build/json/isula_version.c build/json/isula_version.h +``` + +生成的代码对外的接口如下: + +```c +$ cat build/json/isula_version.h +// Generated from isula_version.json. Do not edit! +#ifndef ISULA_VERSION_SCHEMA_H +#define ISULA_VERSION_SCHEMA_H + +#include +#include +#include "json_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char *version; + + int32_t large; + + int32_t middle; + + int32_t small; +} +isula_version; + +void free_isula_version(isula_version *ptr); + +isula_version *make_isula_version(yajl_val tree, const struct parser_context *ctx, parser_error *err); + +yajl_gen_status gen_isula_version(yajl_gen g, const isula_version *ptr, const struct parser_context *ctx, parser_error *err); + +isula_version *isula_version_parse_file(const char *filename, const struct parser_context *ctx, parser_error *err); + +isula_version *isula_version_parse_file_stream(FILE *stream, const struct parser_context *ctx, parser_error *err); + +isula_version *isula_version_parse_data(const char *jsondata, const struct parser_context *ctx, parser_error *err); + +char *isula_version_generate_json(const isula_version *ptr, const struct parser_context *ctx, parser_error *err); + +#ifdef __cplusplus +} +#endif + +#endif + +``` + +测试用例: + +```c +$ cat test.c +#include "isula_version.h" +#include + +void show_isula_version(const isula_version *ptr) +{ + printf("iSula version: \n"); + if (ptr == NULL) { + return; + } + printf("large: %d\nmiddle: %d\nsmall: %d\n", ptr->large, ptr->middle, ptr->small); + printf("version: %s\n", ptr->version); +} + +int main() +{ + const char *json_str = "{\"Version\":\"1.0.0\", \"Large\": 1, \"Middle\": 0, \"Small\": 0}"; + isula_version *ptr = NULL; + parser_error err = NULL; + char *marshaled = NULL; + + // step 1: unmarshal + ptr = isula_version_parse_data(json_str, NULL, &err); + if (ptr == NULL) { + return -1; + } + show_isula_version(ptr); + + // step 2: marshal + free(ptr->version); + ptr->version = strdup("2.0.0"); + ptr->large = 2; + ptr->middle = 1; + ptr->small = 1; + marshaled = isula_version_generate_json(ptr, NULL, &err); + if (ptr == NULL) { + goto out; + } + printf("marshal isula version:\n\t%s\n", marshaled); + +out: + free(marshaled); + free_isula_version(ptr); + return 0; +} +``` + +执行结果如下: + +```bash +$ ./a.out +iSula version: +large: 1 +middle: 0 +small: 0 +version: 1.0.0 +marshal isula version: + { + "Version": "2.0.0", + "Large": 2, + "Middle": 1, + "Small": 1 +} +``` + +### 缺陷 + +通过libocispec可以实现接近于高级语言的`marshal`和`unmarshal`了,只需要简单编写schema文件即可,极大的提高了效率,并且依托开源社区可以提高代码质量。但是,还是存在一些缺陷。 + +例如`golang`中,marshal之后的结构体可以通过map[string]interface保存,可以完整的记录JSON字符串中的信息。而我们当前的实现,只能根据schema来解析JSON字符串,因此,存在信息丢失的情况。有些场景,规范只规定了主体的JSON结构,并且支持拓展配置,例如CNI规范。 + +## 近乎热兵器时代 + +为了解决信息丢失的问题,我们通过在结构体中记录原始的元素集合tree的方案,unmarshal的时候不会丢失原始信息,marshal的时候解析记录的元素信息,从而实现原始数据完整的传递。 + +具体解决方案见官方PR:https://github.com/containers/libocispec/pull/56 + +### 用法 + +使用方式和上面的基本一致,差异主要包括以下几部分: + +1. 生成的代码有部分差异(_residual); + + ```c + $ cat isula_version.h + ... .... + typedef struct { + char *version; + + int32_t large; + + int32_t middle; + + int32_t small; + + yajl_val _residual; + } + isula_version; + ... .... + ``` + +2. 解析是需要指定`struct parser_context`参数为`OPT_PARSE_FULLKEY`; + + ```c + $ cat test.c + #include "isula_version.h" + #include + + void show_isula_version(const isula_version *ptr) + { + printf("iSula version: \n"); + if (ptr == NULL) { + return; + } + printf("large: %d\nmiddle: %d\nsmall: %d\n", ptr->large, ptr->middle, ptr->small); + printf("version: %s\n", ptr->version); + } + + int main() + { + const char *json_str = "{\"Version\":\"1.0.0\", \"Large\": 1, \"Middle\": 0, \"Small\": 0, \"resi_int\": 1, \"resi_str\": \"test\"}"; + isula_version *ptr = NULL; + parser_error err = NULL; + char *marshaled = NULL; + struct parser_context ctx; + ctx.options = OPT_PARSE_FULLKEY; + + // step 1: unmarshal + ptr = isula_version_parse_data(json_str, &ctx, &err); + if (ptr == NULL) { + return -1; + } + show_isula_version(ptr); + + // step 2: marshal + free(ptr->version); + ptr->version = strdup("2.0.0"); + ptr->large = 2; + ptr->middle = 1; + ptr->small = 1; + marshaled = isula_version_generate_json(ptr, &ctx, &err); + if (ptr == NULL) { + goto out; + } + printf("marshal isula version:\n\t%s\n", marshaled); + + out: + free(marshaled); + free_isula_version(ptr); + return 0; + } + ``` + +3. 效果如下 + + ```bash + $ ./a.out + iSula version: + large: 1 + middle: 0 + small: 0 + version: 1.0.0 + marshal isula version: + { + "Version": "2.0.0", + "Large": 2, + "Middle": 1, + "Small": 1, + "resi_int": 1, + "resi_str": "test" + } + ``` + +可以看到拓展的信息,完整的传递下去了。**通过这种方式完美的解决了CNI的拓展配置的支持,从而解决了iSulad动态支持多种插件的技术瓶颈。** + +### 缺陷 + +上面的方案已经基本和支持反射的语言实现的功能相近了,但是,还是存在部分缺陷的。例如,动态修改JSON结构的数据会比较麻烦,需要对底层的解析库比较了解,而且比较麻烦。 + +## 总结 + +虽然当前的框架还有一些缺陷,但是,我们的目标并不是实现一个完美的JSON和C结构体的映射框架,而是解决容器引擎使用JSON的问题。而上面的方案,已经完全满足iSula当前的需求。 + +因此,目前没有进一步优化的需求。如果后续使用场景或者其他用户有需求,可以到[libocispec的社区](https://github.com/containers/libocispec)进行进一步的优化。 + +# 参考文章 + +- https://github.com/containers/libocispec +- http://json-schema.org/ +- https://github.com/lloyd/yajl/tree/master/example diff --git a/i18n/zh.toml b/i18n/zh.toml index 932c3d8c23cd12841591bcf0dc1efc16ba4dee07..31e5193ba0aaee966afe597a1de76f513facea54 100644 --- a/i18n/zh.toml +++ b/i18n/zh.toml @@ -199,13 +199,13 @@ other = "安装指南" other = "/zh/events/20200607.html" [main_cards_live_title] -other = "轻量级容器引擎iSulad性能分析" +other = "轻量级容器引擎iSulad容器基础技术与实践" [main_cards_live_subtitle] -other = "玩转openEuler系列直播之iSula容器专题(5)" +other = "玩转openEuler系列直播之iSula容器专题(6)" [main_cards_live_time] -other = "9月8号(周二) 20:00—21:00 " +other = "9月10号(周四) 20:00—21:00 " [main_cards_live_des] other = "B站openEuler直播间"