内容简介:作者:个推Node.js 开发工程师 之诺由于工程数量的快速增长,个推在实践基于 Node.js 的微服务开发的过程中,遇到了如下问题:为了解决上述问题,个推内部开发了一个命令行小工具来标准化项目初始化流程、简化配置甚至是零配置,提供基于 Docker 的一致构建、运行环境。
作者:个推Node.js 开发工程师 之诺
背景与摘要
由于工程数量的快速增长,个推在实践基于 Node.js 的微服务开发的过程中,遇到了如下问题:
- 每次新建项目都需要安装一次依赖,这些依赖之间基本相似却又有微妙的区别;
- 每次新建项目都要配置一遍相似的配置(比如 tsconfig、lint 规则等);
- 本地 Mac 环境与线上 Docker 内的 Linux 环境不一致(尤其是有 C++ 依赖的情况)。
为了解决上述问题,个推内部开发了一个命令行小 工具 来标准化项目初始化流程、简化配置甚至是零配置,提供基于 Docker 的一致构建、运行环境。
CLI: init, build, test & pack
新建一个 Node.js 项目的时候,我们一般会:
- 安装许多开发依赖:TypeScript、Jest、TSLint、benchmark、typedoc 等;
- 配置 tsconfig、lint 规则、.prettierrc 等;
- 安装众多项目依赖:koa、lodash、sequelize、ioredis、zipkin、node-fetch 等;
- 初始化目录结构;
- 配置CI 脚本。
通常,我们会选择复制一个现成的项目进行修改,导致出现众多看似相似却又不完全相同的项目,比如十个项目可能会对应十种配置组合。对于同时跨多个工程的开发人员来说,众多配置组合会增加他们的工作难度。而且,当安全审计发现某些 npm package 出现安全隐患时,开发人员则需要对每个引用这些包的项目逐一检查和修正。
在确定的开发场景下,几乎所有项目的开发依赖都差不多,开发配置也非常相似,因此我们基于 commander.js 写了一个 init 工具,它会开个命令行的向导,自动安装依赖、初始化项目目录结构和配置。从而创建项目,并按照场景将所有配置收缩为特定几种模板,进行统一处理。
随后,我们有了 build、test、pack 命令,托管了 tsconfig、jest 配置、打包配置,自动调用 tsc 编译,构建测试环境,然后调用 Jest 进行测试,进行标准化打包, CI 脚本基本可以简化为几行标准脚本。
CLI: Docker Build
在介绍这个命令前需要先简单了解一下个推的镜像体系:
前面提到我们将大部分依赖封装到了一个 npm 包,这一层封装也反映在个推的 Docker 镜像体系内,可以简单表述为下面的 Dockerfile:
# 公共依赖层的 Dockerfile FROM node:10 RUN mkdir -p /usr/local/lib/webnode/node_modules \ && cd /usr/local/lib/webnode \ && npm install webnode ENV NODE_PATH /usr/local/lib/webnode/node_modules # 项目的 Dockerfile FROM getui/webnode:1.2.3 COPY package*.json ./ RUN npm install COPY . .
当把这层依赖直接做进 Docker 镜像时,虽然每个镜像的 SIZE 还是 1G 多,但是每个镜像的 UNIQUE SIZE 都是极小的,仅有数M的差分层。
一个简单的对比,比如有 800M 公共系统依赖 + 每个服务平均 200M 的 npm 依赖 + 1M 的服务代码,那么由于原先每个服务都会 npm install 大量重复依赖,20 个服务,就会有 800M + 200M 20 + 1M 20 = 4.82G 的总 UNIQUE SIZE。而采用依赖分层共享,则仅有 800M + 200M + 1M * 20 = 1.02G 的总 UNIQUE SIZE。在考虑应用的多版本之后,依赖分层共享带来在存储上的优势会更加明显。
我们以一定的依赖锁定周期和控制为代价,换取了:
减少依赖组合、依赖版本组合的可能性,开发者选择包的简化、初始化项目的简化;审计简化、安全更新简化 。
CI 显著提速,节省等待时间。
传输和存储的压力减少许多。
公共依赖被多个项目使用,得到了更加充分的测试。
webnode docker build 命令可以帮助简化 Docker image 的构建过程,它内置了一个 Dockerfile 和dockerignore,该命令运行时,会基于这两个文件和当前的 Context,自动构建docker 镜像。其中 Dockerfile 内含一些优化和我们的最佳实践,开发人员只需要专注 Node.js 的项目的开发,这个命令则可以负责配置文件权限等操作以及生成标准化的、优化的 Docker 镜像。
其设计目标是:
快:合理的依赖分层,最大程度应用 Docker 缓存机制,通过 .dockerignore 裁剪不必要的 Context,因此可以实现飞快的构建速度 。
小:依据变更频度做 Docker 分层设计、应用 multi-stage build,尽最大可能缩小一个镜像的 UNIQUE SIZE 。
可重现:同样的内容总是构建出相同的结果。
以 node_modules 依赖优化为例,下面两种 Dockerfile 其实会有很大的区别:
FROM getui/webnode:1.2.3 COPY . . RUN npm install FROM getui/webnode:1.2.3 COPY package*.json ./ RUN npm install COPY . .
前者,每次 docker build 时,只要项目内任何代码变了,npm install 的缓存都会失效,需要重新安装,而后者仅当 package*.json 发生改变之时才会触发重新 npm install。另外,我们还会对 package.json 进行预编译,仅保留依赖相关的字段,避免出现修改 package.json 的版本号就重新 npm install的情况。
webnode docker build 不仅可以帮助开发者进行统一化的镜像构建、统一实践最佳优化,节约资源,还能避免所有开发人员都需要接触优化细节,省时省力。
CLI: Webnode Docker Start
在本地调试开发的过程中,我们遇到了一些环境差异引起的问题:
生产环境与本地开发环境 Node.js 版本不一致。
一些含有 C++ 代码的 npm 依赖运行的跨平台问题 。
文件权限配置、系统目录结构与线上运行环境不完全一致 。
启动初始化流程不一致(比如配置预拉取)。
开发本地常常缺少一些二进制工具或版本不一致(比如 consul-template、nc 等)。
与本地直接启动 Node.js 程序有所不同,这个命令会优先基于当前项目利用上面的 webnode docker build 命令构建 Docker 镜像,然后启动镜像。
Docker 可以帮助消解环境差异:
便捷地携带与生产环境一致的Node.js 版本以及其他二进制依赖。
一致的初始化流程。
轻松运行含有 C++ 的 npm 依赖。
文件权限、目录结构与线上运行环境一致。
容器化的Node.js调试方法有些许变化,需要暴露Node.js的Inspector端口,然后配一下Visual Studio Code的localRoot和remoteRoot:
WEBNODE_HOST=${WEBNODE_HOST:-127.0.0.1} WEBNODE_PORT=${WEBNODE_PORT:-3000} DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS \ -it \ --rm \ --network=\"getui-dev\" -p $WEBNODE_HOST:$WEBNODE_PORT:3000 \ -p 127.0.0.1:9229:9229 \ -e NODE_FLAGS=--inspect=0.0.0.0:9229 \ --name $CONTAINER" docker run \ $DOCKER_RUN_OPTIONS \ $DOCKER_IMAGE_TAG { "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Attach Local WebNode", "address": "127.0.0.1", "port": 9229, "restart": true, "protocol": "inspector", "localRoot": "${workspaceFolder}", "remoteRoot": "YOUR_REMOTE_ROOT", "sourceMaps": true }, ] }
基于容器开发 CLI 工具
基于容器的开发可以带来诸多好处。一是便于分发,基于 Docker 的 Tag,开发者可以很方便地做基于小版本、大版本、分支的分发,可以像 nvm 一样去切换版本。
二是CLI 脚本不用处处考虑跨平台兼容的问题,比如:
sed 在 Linux 和 Mac 下工作行为不一致的问题之类的。
有的环境有 Python 3 有的环境只有 Python 2
所有的依赖通过容器带进来,简洁而高效。
在基于 Docker 的工具开发的过程中,我们也遇到一些问题:
一是容器内外 UID/GID 不一致,如果是以非 ROOT 用户运行 docker run,会导致容器内程序在挂载的目录产生的文件权限与当前用户不一致。
Docker for Mac对于文件权限有一些特别的行为,具体可以参见: https://docs.docker.com/docke...
对于 Host 是 Linux 的情况,尤其在 CI 时,需要考虑 UID/GID 的问题。对于这种情况,我们选择覆盖掉了 entrypoint ,然后用 gosu 去做降权来处理。
CLI_EXEC_UID=${CLI_EXEC_UID:-0}
CLI_EXEC_GID=${CLI_EXEC_GID:-0}
exec gosu $CLI_EXEC_UID:$CLI_EXEC_GID env "$@"
其实RedHat 旗下用于设计container runtime 的daemonless (例如 podman),就很适合做CLI工具,可以 rootless 运行,又尊重系统的权限配置。然而其目前尚未成熟,业界采用率也不高,仍需要继续观望。
二是有时候 docker run 速度较慢,个推的解决方案是在首次启动时启动一个 docker run --detach,然后后续的 CLI 执行完全通过 docker exec 来进行,这样避免掉了每次执行命令时启动的开销,速度提升明显。
小结
以上便是个推 Node.js 微服务开发实践中关于 CLI 工具的实践,个推试图标准化、优化项目结构以及镜像构建,减少组合的可能性,有效降低了存储、传输、构建的成本,让开发人员更加省时省力。
后续我们还会继续为大家介绍个推的 Docker 镜像体系设计以及Node.js 微服务开发框架,敬请期待。
参考
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning XML with DOM and Ajax
Sas Jacobs / Apress / 2006-06-05 / USD 39.99
Don't waste time on 1,000-page tomes full of syntax; this book is all you need to get ahead in XML development. Renowned web developer Sas Jacobs presents an essential guide to XML. Beginning XML with......一起来看看 《Beginning XML with DOM and Ajax》 这本书的介绍吧!