当Node.js遇见Docker
发布于 5 年前 作者 Fundebug 7872 次浏览 最后一次编辑是 4 年前 来自 分享

Node.js Best Practices - How to Become a Better Developer in 2017提到的几点,我们Fundebug深有同感:

  • 使用ES6
  • 使用Promise
  • 使用LTS
  • 使用Docker

nodejs-docker.png

想必大家都知道ES6,Promise以及LTS,那Docker是啥玩意啊?翻遍Node文档也没见踪迹啊!

<div style=“text-align: center;”> <img style=“width:80%;” src=“nodejs-docker/nodejs-docker.png” /> </div>

GitHub仓库: Fundebug/nodejs-docker

什么是Docker?

Docker是最流行的的容器工具,没有之一。本文并不打算深入介绍Docker,不过可以从几个简单的角度来理解Docker。

从进程的角度理解Docker

在Linux中,所有的进程构成了一棵树。可以使用pstree命令进行查看:

pstree
init─┬─VBoxService───7*[{VBoxService}]
     ├─acpid
     ├─atd
     ├─cron
     ├─dbus-daemon
     ├─dhclient
     ├─dockerd─┬─docker-containe─┬─docker-containe─┬─redis-server───2*[{redis-server}]
     │         │                 │                 └─8*[{docker-containe}]
     │         │                 ├─docker-containe─┬─mongod───16*[{mongod}]
     │         │                 │                 └─8*[{docker-containe}]
     │         │                 └─11*[{docker-containe}]
     │         └─13*[{dockerd}]
     ├─6*[getty]
     ├─influxd───9*[{influxd}]
     ├─irqbalance
     ├─puppet───{puppet}
     ├─rpc.idmapd
     ├─rpc.statd
     ├─rpcbind
     ├─rsyslogd───3*[{rsyslogd}]
     ├─ruby───{ruby}
     ├─sshd─┬─sshd───sshd───zsh───pstree
     │      ├─sshd───sshd───zsh
     │      └─sshd───sshd───zsh───mongo───2*[{mongo}]
     ├─systemd-logind
     ├─systemd-udevd
     ├─upstart-file-br
     ├─upstart-socket-
     └─upstart-udev-br

可知,init进程为所有进程的根(root),其PID为1。

Docker将不同应用的进程隔离了起来,这些被隔离的进程就是一个个容器。隔离是基于两个Linux内核机制实现的,Namesapce和Cgroups。

Namespace可以从UTD、IPC、PID、Mount,User和Network的角度隔离进程。比如,不同的进程将拥有不同PID空间,这样容器中的进程将看不到主机上的进程,也看不到其他容器中的进程。这与Node.js中模块化以隔离变量的命名空间的思想是异曲同工的。

通过Cgroups,可以限制进程对CPU,内存等资源的使用。简单地说,我们可以通过Cgroups指定容器只能使用1G内存。

从进程角度理解Docker,那每一个Docker容器就是被隔离的进程及其子进程。上文pstree的输出中可以分辨出2个容器: mongodb和redis。

从文件的角度理解Docker

基于Namespace与Cgroups的容器工具其实早已存在,例如Linux-VServerOpenVZLXC。然而,真正引爆容器技术的却是后来者Docker。为什么呢?个人觉得是因为Docker镜像以及Dockerfile

在Linux中,一切皆文件,进程的运行离不开各种各样的文件。跑一个简单的Node.js程序,传统的做法是手动安装各种依赖然后运行;而Docker则是将所有依赖(包括操作系统,Node,NPM模块,源代码)打包到一个Docker镜像中,然后基于这个镜像运行容器。

Docker镜像可以通过Docker仓库共享给其他人,这样他们只需要下载镜像即可运行程序。想象一下,当我们需要在另一台主机(比如生产服务器,新同事的机器)上运行一个Node.js应用,仅仅需要下载对应的Docker镜像就可以了,是不是很方便呢?

Docker镜像可以通过文本文件,即Dockerfile进行定义。不妨看一个简单的例子(由于不可抗力,这个Dockerfile构建大概会失败,仅作为参考):

# 基于Ubuntu
FROM ubuntu

# 安装Node.js与NPM
RUN apt-get update && apt-get -y install nodejs npm

# 安装NPM模块:Express
RUN npm install express

# 添加源代码
ADD app.js /

其中,FROMRUNADD为Dockerfile命令。结合注释,该Dockerfile的含义非常直白。基于这个Dockerfile,使用docker build命令就可以构建对应的Docker镜像。基于这个Docker镜像,就可以运行Docker容器来执行app.js:

var express = require("express");
var app = express();

app.get("/", function(req, res)
{
    res.send("Hello Fundebug!\n");
});

app.listen(3000);

Dockerfile实际上是将Docker镜像代码化了,另一方面也是将安装依赖的过程代码化了,于是我们就可以像管理源码一样使用git对Dockerfile进行版本管理。

为啥用Docker?

当你的系统越来越复杂的时候,你会发现Docker的价值。

从应用架构角度理解Docker

刚开始,你只需要写一个Node.js程序,挂载一个静态网站;然后,你做了一个用户账号系统,这时需要数据库了,比如说MySQL; 后来,为了提升性能,你引入了Memcached缓存;终于有一天,你决定把前后端分离,这样可以提高开发效率;当用户越来越多,你又不得不使用Nginx做反向代理; 对了,随着功能越来越多,你的应用依赖也会越来越多…总之,你的应用架构只会越来越复杂。不同的组件的安装,配置与运行步骤各不相同,于是你不得不写一个很长的文档给新同事,只为了让他搭建一个开发环境

使用Docker的话,你可以为不同的组件逐一编写Dockerfile,分别构建镜像,然后运行在各个容器中。这样做,将复杂的架构统一了,所有组件的安装和运行步骤统一为几个简单的命令:

  • 构建Docker镜像: docker build
  • 上传Docker镜像: docker push
  • 下载Docker镜像: docker pull
  • 运行Docker容器: docker run
从应用部署角度理解Docker

通常,你会有开发测试生产服务器,对于某些应用,还会需要进行构建。不同步骤的依赖会有一些不同,并且在不同的服务器上执行。如果手动地在不同的服务器上安装依赖,是件很麻烦的事情。比如说,当你需要为Node.js应用添加一个新的npm模块,或者升级一下Node.js,是不是得重复操作很多次?友情提示一下,手动敲命令是极易出错的,有些失误会导致致命的后果(参考最近Gitlab误删数据库与AWS的S3故障)。

如果使用Docker的话,开发构建测试生产将全部在Docker容器中执行,你需要为不同步骤编写不同的Dockerfile。当依赖变化时,仅需要稍微修改Dockerfile即可。结合构建工具Jenkins,就可以将整个部署流程自动化。

另一方面,Dockerfile将Docker镜像描述得非常精准,能够保证很强的一致性。比如,操作系统的版本,Node.js的版本,NPM模块的版本等。这就意味着,在本地开发环境运行成功的镜像,在构建测试生产环境中也没有问题。还有,不同的Docker容器是依赖于不同的Docker镜像,这样他们互不干扰。比如,两个Node.js应用可以分别使用不同版本的Node.js。

从集群管理角度理解Docker

架构规模越来越大的时候,你有必要引入集群了。这就意味着,服务器由1台变成了多台,同一个应用需要运行多个备份来分担负载。当然,你可以手动对集群的功能进行划分: Nginx服务器,Node.js服务器,MySQL服务器,测试服务器,生产服务器…这样做的好处是简单粗暴;也可以说财大气粗,因为资源闲置会非常严重。还有一点,每次新增节点的时候,你就不得不花大量时间进行安装与配置,这其实是一种低效的重复劳动。

下载Docker镜像之后,Docker容器可以运行在集群的任何一个节点。一方面,各个组件可以共享主机,且互不干扰;另一方面,也不需要在集群的节点上安装和配置任何组件。至于整个Docker集群的管理,业界有很多成熟的解决方案,例如MesosKubernetesDocker Swarm。这些集群系统提供了调度服务发现负载均衡等功能,让整个集群变成一个整体。

如何用Docker?

编写Dockerfile

正确的Dockerfile是这样的:

# 使用DaoCloud的Ubuntu镜像
FROM daocloud.io/library/ubuntu:14.04

# 设置镜像作者
MAINTAINER Fundebug <help@fundebug.com>

# 设置时区
RUN sudo sh -c "echo 'Asia/Shanghai' > /etc/timezone" && \
    sudo dpkg-reconfigure -f noninteractive tzdata

# 使用阿里云的Ubuntu镜像
RUN echo '\n\
deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse\n\
deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse\n\
deb http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse\n\
deb http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse\n\
deb http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse\n\
deb-src http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse\n\
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse\n\
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse\n\
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse\n\
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse\n'\
> /etc/apt/sources.list

# 安装node v6.10.1
RUN sudo apt-get update && sudo apt-get install -y wget

# 使用淘宝镜像安装Node.js v6.10.1
RUN wget https://npm.taobao.org/mirrors/node/v6.10.1/node-v6.10.1-linux-x64.tar.gz && \
    tar -C /usr/local --strip-components 1 -xzf node-v6.10.1-linux-x64.tar.gz && \
    rm node-v6.10.1-linux-x64.tar.gz 

WORKDIR /app

# 安装npm模块
ADD package.json /app/package.json

# 使用淘宝的npm镜像
RUN npm install --production -d --registry=https://registry.npm.taobao.org

# 添加源代码
ADD . /app

# 运行app.js
CMD ["node", "/app/app.js"]

有几点值得注意的地方:

  • 使用国内DaoCloud的Docker仓库,阿里云的ubuntu镜像以及淘宝的npm镜像,否则会出事情的;
  • 将时区设为Asia/Shanghai,否则日志的时间会不大对劲;
  • 使用.dockerignore忽略不需要添加到Docker镜像的文件和目录,其语法与.gitigore一致;

更重要的一点是,package.json需要单独添加。Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。

构建Docker镜像

使用docker build命令构建Docker镜像

sudo docker build -t fundebug/nodejs .

其中,-t选项用于指定镜像的名称。

使用docker images命令查看Docker镜像

sudo docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
fundebug/nodejs               latest              64530ce811a1        32 minutes ago      266.4 MB
daocloud.io/library/ubuntu    14.04               b969ab9f929b        9 weeks ago         188 MB

可知,fundebug/nodejs镜像的大小为266.4MB,在ubuntu镜像的基础上增加了80MB左右。

运行Docker容器

使用docker run命令运行Docker容器

sudo docker run -d --net=host --name=hello-fundebug fundebug/nodejs

其中,-d选项表示容器在后台运行;–net选项指定容器的网络模式,host表示与主机共享网络;–name指定了容器的名称。

使用docker ps命令查看Docker容器

sudo docker ps
CONTAINER ID        IMAGE                             COMMAND                  CREATED             STATUS              PORTS               NAMES
e8eb5473970c        fundebug/nodejs                   "node /app/app.js"       37 minutes ago      Up 37 minutes                           hello-

可知,COMMAND为"node /app/app.js",表示容器中运行的命令。这是我们再Dockerfile中使用CMD指定的。不妨使用docker exec命令在容器内执行ps命令查看容器内的进程

sudo docker exec hello-fundebug ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:14 ?        00:00:00 node /app/app.js

可知,容器内的1号进程即为node进程node /app/app.js。在Linux中,PID为1进程按说是唯一的,即init进程。但是,容器使用了内核的Namespace机制,为容器创建了独立的PID空间,因此容器中也有1号进程。

测试

使用curl命令访问:

curl localhost:3000
Hello Fundebug!

是否用Docker?

一方面,使用Docker能够带来很大益处;另一方面,引入Docker必然会有很多挑战,需要熟悉Docker才能应对自如。想必这是一个艰难的决定。如果从长远的角度来看,Docker正在成为应用开发,部署,发布的标准技术,也许我们不得不用开放的心态对待它。

作为Node.js开发者,真正理解Docker可能需要一些时间,但是它可以给你带来很多便利。

参考链接

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了6亿+错误事件,得到了Google、360、金山软件等众多知名用户的认可。欢迎免费试用!

版权声明

转载时请注明作者Fundebug以及本文地址:

https://blog.fundebug.com/2017/03/27/nodejs-docker/

18 回复

文章讲得通俗易懂,感觉很适合初学学习。

一直纠结,一直纠结,一直纠结,一直纠结,纠结了好久node+docker方案,docker的优势在我们的应用场景好像都很鸡肋:

  1. 运行环境:我们的node程序基本上是0配置的,git clone、npm install、pm2 run就运行起来了,甚至用了rsync之后就只剩下pm2 run了;
  2. 分布式部署云服务提供弹性伸缩和快照部署,自动填加的机器一分钟内启动服务,比docker构建要快很多;
  3. 云服务多台低配服务器方案比单台高配方案成本低(主要是自动弹性伸缩的功劳)、性能强(超售的锅)、可靠性高(互为替补),单台机器只跑一个实例的话没有隔离需求;

好纠结好纠结好纠结。。。谁能给我个理由再试一试docker呢?

@libook 引入Docker是个很重要的决定,所以慎重一点是很好的。

  1. 文章里也讲了,Docker的价值是随着架构/流程变复杂之后才体现出来的,所以如果你的应用很简单,使用Docker可能真没什么用。
  2. Docker容器可以在数秒内启动,比虚拟机快一个数量级。Docker镜像并不是在需要的的时候构建,和虚拟机镜像一样,它也应该事先就构建好了。
  3. 同一台机器运行多个Docker容器,能够对资源进行更加细化的分配,理论上是要比手动分配服务器资源要高效很多。资源分配这种事情,应该是让集群系统来做,手动分配并不具备可扩展性。

这个问题很值得讨论呀,欢迎私聊~

@laoqiren 欢迎加群交流咯

@Fundebug 我们就是让集群自动做资源分配的呀,自动弹性机制是以虚拟主机为单位自动添加和释放的,是根据阈值设定纯自动调整的,添加主机是以快照为基础,这部分功能与Docker的效果相同,真的是一样一样一样的,所以有云服务提供这种机制的话Docker的机制就是多此一举了。

现在云计算服务通常都是易于横向扩展而难于纵向扩展,加一台服务器总比升级一台服务器的配置要简单快捷。

我们的服务每天只有3个小时是高负载,需要数十台服务器分压;但是其余21个小时只需要不到十台服务器,可以算笔账,比如我们一个全都是单核最低配虚拟机集群,每天有5个小时是12台服务器(短时间计算量需求从3台飙升至12台),其余19个小时是2台服务器互为替补保证可靠性,阿里云上两种付费方案,按需:包月的价格大概是3:1;那如果我使用Docker的方案,买12倍性能的主机,必须是两台互为替补以保证可靠性,所以是两台6核心的高性能主机,云服务通常只提供2次幂的核心数,所以只能买两台8核心的,包月的话如果单核最低配主机假设大约是120元一个月,那两台8核心的的大约是21208=1920,假设有优惠到1800,这就是我一个月的开销;但如果是单台低配+自动弹性的方案的话,就是120312/245+12032/2419=1203/24(125+219)=1470。

这还只是多个集群中的一个。

实际上不可能我往常是12倍计算力需求我就只买这么多计算力,不考虑自动弹性的话至少要预留30%的性能缓冲,这种情况下Docker方案并没有优势,反而成本会高。而且遇到比如App Store突然把我们的应用放到首页、地推人员开展大型推广活动等等情况,性能需求会不可预计的飙升,这时候只能运维人员人工评估计算量需求然后再选择相应配置的服务器购买添加,释放的时候也要人工确认负载下降然后手动释放,这实际上是提升了运维成本的,什么?用自动弹性?那为什么不一开始就用完全低配自动弹性的方案呢?您说是吧?

可能有人会说我们的这种情况太少见,其实并不是,因为现在移动互联网好多领域都是这样的用户使用场景:用户只会在早上上班(学)、中午吃饭、晚上回家后集中使用移动互联网应用,也只会在这几个时间段是高计算力需求的,所以我相信遇到这种情况的并不只是我们一家。希望您认真看了我的的回复(包括上一条其实是有一些误会的)能理解我目前对于Docker应用上的困境。

对于我们这种情况,可能只有至少发生以下之一的变化才会适合使用Docker:

  1. 云计算服务商提供容器服务,且容器服务在性能与虚拟主机相近的情况下价格更低,而且稳定性也不会受到超售、无效的性能隔离等影响;
  2. 计算力需求模型变化,全天超过24/3=8小时的高负载,这样包月+自动弹性方案的性价比更高,使用Docker可以在高配包月服务器上做多服务多实例的批量部署;
  3. 云计算服务商的可靠性真的像宣传那样有保障,阿里云每周都有问题,Azure甚至Amazon都出过较大故障,如果我们一个服务集群是两台高配机器为主力的话,那么一旦其中一台挂掉了,另外一台也会被压爆;
  4. 从公有云计算服务商迁移至私有云机房,可以考虑让性能强大的服务器直接跑容器,而不是虚拟化成低配主机。
  5. 您能提供一套方案能有效解决我们遇到的问题😆。

@libook

1. 是否使用Docker,云服务器成本不是最重要的考虑因素。

发现你的所有论点都集中在云服务器的成本控制,但是呢,现在互联网公司支出大头是人力成本,相对而言,云服务器的支出其实是小头。根据你的计算,使用Docker的话不会增加太多成本。所以,是否使用Docker,不必太纠结于云服务器的成本。

2. Docker和云服务器自动扩容并不是互相冲突的

如果你的服务计算需求波动比较大,确实可以使用云服务器的自动扩容服务。但是,这个和使用Docker是不冲突的。

应用运行在Docker容器里面,Docker容器运行在云服务器里面,然后扩容云服务器就好啦。Docker容器也是进程,和普通的Node进程没有什么区别,照样可以使用云服务器自动扩容服务。

这样做的话,你的快照会简单很多,只需要安装Docker,然后下载各种Docker镜像就好啦。应用修改的时候,只需要下载新的Docker镜像,不需要手动去修改服务器配置。

3. Docker可以共享集群

貌似你有多个集群。使用Docker之后,每一台服务器都是一样的,可以运行任何应用。所以不同的应用可以共享集群的,不需要去搭建很多个集群。

4. Docker可以节省人力成本

第1点已经提过了,人力成本是互联网公司支出的大头。所以,想办法节省人力成本才是王道。Docker将所有应用统一了,然后将配置过程代码化了,这样是可以提高开发效率的。

@Fundebug 但实际上node项目是0配置的,不需要配置环境,而且使用云服务的镜像部署都不需要clone代码,并没有看到docker如何节省运维人力成本。

@libook 如果你们只运行node应用,并且一直都是这样,那应该木有必要用Docker。正如我在博客里说的:

当你的系统越来越复杂的时候,你会发现Docker的价值。

@Fundebug 看来目前好像只能这样了。。。

谢谢分享,这两天正也在学习docker,请教几个问题:

  1. COPY 命令是将host的文件拷贝到container里面,如果是将host的目录映射到container里面(以便在host中写代码), 那Dockerfile要怎么写呢?在启动container的时候我知道通过 -v参数 -v /Users/js/Desktop/nginx.conf:/etc/nginx/nginx.conf。譬如我想在Dockerfile中写,将当前(Dockerfile所在目录)目录映射到container中去。
  2. 如果container被stop后,如果再 start 或者 restart 的话,container里面保存的文件应该还在吧?
  1. Dockerfile中有个VOLUME命令,但是它并不能将当前目录映射到容器中,所以不适用。因此你只能通过运行容器时通过-v选项挂载,或者在Dockerfile中使用COPY指令将nginx.conf拷贝到容器中。
  2. 容器被stop之后start,文件都在。容器被rm后,容器中文件才会被删掉。

好文章,楼主的文章质量很好

正在使用,求推荐一个可视化的swarm管理工具,有多个云上的swarm需要统一管理。

看上去很有用收藏一发 上次听到的说法是 docker可以统一对外输出,可以把node,java等程序通过docker对下一个工序测试什么的输入输出一致。 现在还没尝试在生产跑docker自己看过一点点例子, 请问用了docker后就不需要pm2这样的守护进程了?还是写进image里全部。

@Fundebug @libook 感谢两位的讨论,两种选择都从各自的角度给出了观点,受益匪浅,不过还是决定拥抱Docker啦!

回到顶部