个推 Node.js 微服务实践:基于容器的一站式命令行工具链

标签: node js 微服务 | 发表时间:2019-02-20 17:34 | 作者:jack
出处:https://www.diycode.cc/

背景与摘要

由于工程数量的快速增长,个推在实践基于 Node.js 的微服务开发的过程中,遇到了如下问题:

  1. 每次新建项目都需要安装一次依赖,这些依赖之间基本相似却又有微妙的区别;

  2. 每次新建项目都要配置一遍相似的配置(比如 tsconfig、lint 规则等);

  3. 本地 Mac 环境与线上 Docker 内的 Linux 环境不一致(尤其是有 C++ 依赖的情况)。

为了解决上述问题,个推内部开发了一个命令行小工具来标准化项目初始化流程、简化配置甚至是零配置,提供基于 Docker 的一致构建、运行环境。

CLI: init, build, test & pack

新建一个 Node.js 项目的时候,我们一般会:

  1. 安装许多开发依赖:TypeScript、Jest、TSLint、benchmark、typedoc 等;

  2. 配置 tsconfig、lint 规则、.prettierrc 等;

  3. 安装众多项目依赖:koa、lodash、sequelize、ioredis、zipkin、node-fetch 等;

  4. 初始化目录结构;

  5. 配置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/docker-for-mac/osxfs/#ownership

对于 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 微服务开发框架,敬请期待。

参考

https://docs.docker.com/docker-for-mac/osxfs/#ownership

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#entrypoint

https://www.projectatomic.io/blog/2018/02/reintroduction-podman/

https://www.slideshare.net/AkihiroSuda/the-state-of-rootless-containers

https://www.debian.org/doc/manuals/debian-faq/ch-pkg_basics.en.html#s-virtual

相关 [node js 微服务] 推荐:

node js 断点调试

- - Web前端 - ITeye博客
大部分基于 Node.js 的应用都是运行在浏览器中的,. 例如强大的调试工具 node-inspector. node-inspector 是一个完全基于 Node.js 的开源在线调试工具,提供了强大的调试功能和友好. 的用户界面,它的使用方法十分简便. 首先,使用 npm install -g node-inspector 命令安装 node-inspector,然后在终.

什么是Node?

- We_Get - 博客园新闻频道
译者按:前不久Oreilly出了一本小册子“What is Node?”,扼要的讲解了Node的身世和所适用的场景,作者文笔轻松流畅、内容充实,是非常难得的学习资料.   译文全文:http://jayli.github.com/whatisnode/index.html.   作者:Brett McLaughlin ,原文:What is Node?.

Node入门

- - CSDN博客编程语言推荐文章
作者:  Manuel Kiessling. 翻译:  goddyzhao &  GrayZhang &  MondayChen. 本书致力于教会你如何用Node.js来开发应用,过程中会传授你所有所需的“高级”JavaScript知识. 本书绝不是一本“Hello World”的教程. 你正在阅读的已经是本书的最终版.

浅析Hadoop Secondary NameNode,CheckPoint Node,Backup Node

- - CSDN博客云计算推荐文章
Hadoop SecondaryNameNode并不是Hadoop 第二个NameNode,它不提供NameNode服务,而仅仅是NameNode的一个工具. 这个工具帮助NameNode管理Metadata数据. NameNode的HDFS文件信息(即Metadata)记录在内存中,client的文件写操作直接修改内存中的Metadata,同时也会记录到硬盘的Edits文件,这是一个Log文件.

[译]什么是Node?

- blacktulip - Taobao UED Team
译者按:前不久Oreilly出了一本小册子“What is Node?”,扼要的讲解了Node的身世和所适用的场景,作者文笔轻松流畅、内容充实,是非常难得的学习资料. 译文全文:http://jayli.github.com/whatisnode/index.html. 作者:Brett McLaughlin ,原文:What is Node?.

用node作桌面开发

- InterMa - CNode社区
node的定位是,server-side javascript. 但程序员最爱做的事,就是把一个东西用在不该用的地方. 那么,可以把node用在桌面开发上吗. 把Javascript用在桌面开发上,早有先例,比如GTK+的gjs,还有Qt的QML(顺带一提,QML代表着桌面开发的另一个方向,a promising way),GNOME3中,也用javascript作为桌面插件的开发语言.

Vercel 部署 Node 服务

- - 掘金 前端
之前在写 面试常客:HTTP 缓存时,曾经就强缓存和协商缓存写过两个demo,但缓存要在服务端做,只能贴上代码,不能在网页上感受(虽然我贴了gif). 笔者的所有 demo 例子都放在 github page 上,其特点是不需要服务器即可部署静态资源,但它不具备部署服务端应用能力. 最近笔者在了解 CI/CD 方面的知识点,想起了 Vercel,就想着能否将服务端应用架在 vercel 上呢.

WebView JS 交互

- - ITeye博客
WebView加jquery做页面会怎么样呢. // 创建WebView对象. // 把programList添加到js的全局对象window中,. // 这样就可以使用window.programList来获取数据. * 定义js回调java函数. // 绑定键盘的向上,向下按钮事件触发相应的js事件.

無痛安裝 NodeJS 和 Node Framework Express

- Hming - 小惡魔 - 電腦技術 - 工作筆記 - AppleBOY
直接到官網下載 Stable 的版本吧,目前是 node-v0.4.10.tar.gz,也可以先看看 API Document. 安裝 Ububtu 相關套件. 下面會使用最原始的編譯方式,所以必須安裝 g++ 套件,否則下 ./configure 的時候,會吐出來沒有安裝過的套件. 兩種方法:1.用 apt-get install nodejs 2.