个推Node.js 微服务实践:基于容器的一站式命令行工具链
作者:个推Node.js 开发工程师 之诺
背景与摘要
由于工程数量的快速增长,个推在实践基于 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:
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.
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.
COPY . .
RUN npm install FROM getui/webnode:1.2.
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:-}
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS \
-it \
--rm \
--network=\"getui-dev\"
-p $WEBNODE_HOST:$WEBNODE_PORT: \
-p 127.0.0.1:: \
-e NODE_FLAGS=--inspect=0.0.0.0: \
--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": ,
"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:-}
CLI_EXEC_GID=${CLI_EXEC_GID:-}
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微服务实践(一)
什么是微服务 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独立部署,各个微服务之间是松耦合的.每个微服务仅关注于完成一件任务并很好地完成该任务.在所有情况下 ...
- Kubernetes 实践指南之Kubernetes 的命令行工具详解
kubectl作为客户端CLI工具,可以让用户通过命令行的方式对Kubernetes集群进行管理.本节内容将对kubectl的子命令和用法进行详细描述. 一.kubectl 用法概述 kubectl语 ...
- Redis进阶实践之十四 Redis-cli命令行工具使用详解
转载来源:http://www.cnblogs.com/PatrickLiu/p/8508975.html 一.介绍 redis学了有一段时间了,以前都是看视频,看教程,很少看官方的东西.现在redi ...
- Redis进阶实践之十四 Redis-cli命令行工具使用详解第一部分
一.介绍 redis学了有一段时间了,以前都是看视频,看教程,很少看官方的东西.现在redis的东西要看的都差不多看完了.网上的东西也不多了.剩下来就看看官网的东西吧,一遍翻译,一遍测试. ...
- Redis进阶实践之十五 Redis-cli命令行工具使用详解第二部分(结束)
一.介绍 今天继续redis-cli使用的介绍,上一篇文章写了一部分,写到第9个小节,今天就来完成第二部分.话不多说,开始我们今天的讲解.如果要想看第一篇文章,地址如下:http: ...
- 让你如绅士般基于描述编写 Python 命令行工具的开源项目:docopt
作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...
- 微软开放技术发布针对 Mac 和 Linux 的更新版 Azure Node.JS SDK 和命令行工具
发布于 2013-12-04 作者 Eduard Koller 这次为我们使用Linux 的朋友带来了更多关于部署云上虚拟机的消息.今天,微软开放技术有限公司 (MS Open Tech),想与大家分 ...
- 浅谈微服务架构、容器技术与K8S
关注嘉为科技,获取运维新知 企业应用系统:从单体应用走向微服务架构:从裸金属走向容器. 如果在诸多热门云计算技术诸如容器.微服务.DevOps.OpenStack等之中,找出一个最火的方向,那么可能非 ...
- .NET 微服务和Docker容器
.NET 微服务:适用于容器化 .NET 应用的体系结构 容器和 Docker 简介 什么是 Docker? Docker 术语 Docker 容器.映像和注册表 为 Docker 容器选择 .NET ...
随机推荐
- system表空间不可改名
SQL> startup mount;ORACLE instance started. Total System Global Area 814227456 bytesFixed Size ...
- JAVA 文件读取写入后 md5值不变的方法
假如我们想把某文件读入 StringBuffer 并写入新文件,新文件md5值需要保持不变(写入新文件后保证和源文件一模一样), 我们就需要在操作 StringBuffer 时附加换行符: Strin ...
- Sterling B2B Integrator与SAP交互 - 02 安装配置
系统组成: 1. 服务器OS及硬件: OS: Red Hat Enterprise Linux Server release 6.6 Hardware: Virtual Machine, x86_64 ...
- VirtualBox虚拟机怎么导入已经存在的vdi文件
VirtualBox虚拟机怎么导入已经存在的vdi文件 第一章 1.原因 早上一不小心将virtualBox 卸载了,(不知道怎么了, 里面得虚拟机全部都没有了,但是vdi文件还在) 2.解决办法 直 ...
- 【LeetCode】数组--合并区间(56)
写在前面 老粉丝可能知道现阶段的LeetCode刷题将按照某一个特定的专题进行,之前的[贪心算法]已经结束,虽然只有三个题却包含了简单,中等,困难这三个维度,今天介绍的是第二个专题[数组] 数组( ...
- 技术进阶:Kubernetes高级架构与应用状态部署
在了解Kubernetes应用状态部署前,我们先看看Kubernetes的高级架构,方便更好的理解Kubernetes的状态. Kubernetes 的高级架构 包括应用程序部署模型,服务发现和负载均 ...
- 二叉树 c++
树 非空树 有一个(root)根节点r 其余节点可分为m个互不相交的有限集(子树)T1....Tm 具有n个节点的树,具有(n-1)条连接(指针域),需要构成结构体,尽可能减少空间域的浪费,使用儿子兄 ...
- (第十二周)Debug阶段成员贡献分
项目名:食物链教学工具 组名:奋斗吧兄弟 组长:黄兴 组员:李俞寰.杜桥.栾骄阳.王东涵 个人贡献分=基础分+表现分 基础分=5*5*0.5/5=2.5 成员得分如下: 成员 基础分 表现分 个人贡献 ...
- Linux内核分析 笔记四 系统调用的三个层次 ——by王玥
一.知识点总结 (一)用户态.内核态和中断 1.内核态:在高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态 2.用户态:在低级别的指令状态下,代码 只能在级别允许的特 ...
- Docker打DB2 9.7镜像采坑相关
概况:以centos:7.2.1511镜像为基础镜像,使用docker commit方式进行构建 步骤: 运行centos7.2.1511镜像(以特权模式运行,后续内核参数修改必需参数) dock ...