当Node.js遇见Docker
AlvinGraynd
8年前
<ul> <li> <p>使用ES6</p> </li> <li> <p>使用Promise</p> </li> <li> <p>使用LTS</p> </li> <li> <p>使用 <a href="/misc/goto?guid=4958839306020721371" rel="nofollow,noindex">Docker</a></p> </li> <li> <p>...</p> </li> </ul> <p>想必大家都知道ES6,Promise以及LTS,那 Docker 是啥玩意啊?翻遍 Node文档 也没见踪迹啊!</p> <p><img src="https://simg.open-open.com/show/03eb3f161b6e40470f5859f88edce9a6.png"></p> <p>GitHub仓库: <a href="/misc/goto?guid=4959746773698858940" rel="nofollow,noindex">Fundebug/nodejs-docker</a></p> <h3>什么是Docker?</h3> <p>Docker是最流行的的容器工具, <strong>没有之一</strong> 。本文并不打算深入介绍Docker,不过可以从几个简单的角度来理解Docker。</p> <p>从进程的角度理解Docker</p> <p>在Linux中,所有的进程构成了一棵树。可以使用 <a href="/misc/goto?guid=4959746773790170969" rel="nofollow,noindex">pstree</a> 命令进行查看:</p> <pre> <code class="language-javascript">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</code></pre> <p>可知,init进程为所有进程的根(root),其PID为1。</p> <p>Docker将不同应用的进程隔离了起来,这些被隔离的进程就是一个个容器。隔离是基于两个Linux内核机制实现的,Namesapce和Cgroups。</p> <p>Namespace可以从UTD、IPC、PID、Mount,User和Network的角度隔离进程。比如,不同的进程将拥有不同PID空间,这样容器中的进程将看不到主机上的进程,也看不到其他容器中的进程。这与Node.js中模块化以隔离变量的命名空间的思想是异曲同工的。</p> <p>通过Cgroups,可以限制进程对CPU,内存等资源的使用。简单地说,我们可以通过Cgroups指定容器只能使用1G内存。</p> <p>从进程角度理解Docker,那 <strong>每一个Docker容器就是被隔离的进程及其子进程</strong> 。上文pstree的输出中可以分辨出2个容器: mongodb和redis。</p> <p>从文件的角度理解Docker</p> <p>基于Namespace与Cgroups的容器工具其实早已存在,例如 Linux-VServer , OpenVZ , LXC 。然而,真正引爆容器技术的却是后来者Docker。为什么呢?个人觉得是因为 <strong>Docker镜像</strong> 以及 <strong>Dockerfile</strong> 。</p> <p>在Linux中,一切皆文件,进程的运行离不开各种各样的文件。跑一个简单的Node.js程序,传统的做法是手动安装各种依赖然后运行;而Docker则是将所有依赖(包括操作系统,Node,NPM模块,源代码)打包到一个 <strong>Docker镜像</strong> 中,然后基于这个镜像运行容器。</p> <p>Docker镜像可以通过 <strong>Docker仓库</strong> 共享给其他人,这样他们只需要下载镜像即可运行程序。想象一下,当我们需要在另一台主机(比如生产服务器,新同事的机器)上运行一个Node.js应用,仅仅需要下载对应的Docker镜像就可以了,是不是很方便呢?</p> <p>Docker镜像可以通过文本文件,即 <strong>Dockerfile</strong> 进行定义。不妨看一个简单的例子(由于不可抗力,这个Dockerfile构建大概会失败,仅作为参考):</p> <pre> <code class="language-javascript"># 基于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 /</code></pre> <p>其中, <strong>FROM</strong> , <strong>RUN</strong> 与 <strong>ADD</strong> 为Dockerfile命令。结合注释,该Dockerfile的含义非常直白。基于这个Dockerfile,使用 <strong>docker build</strong> 命令就可以构建对应的Docker镜像。基于这个Docker镜像,就可以运行Docker容器来执行app.js:</p> <pre> <code class="language-javascript">var express = require("express"); var app = express(); app.get("/", function(req, res) { res.send("Hello Fundebug!\n"); }); app.listen(3000);</code></pre> <p>Dockerfile实际上是将 <strong>Docker镜像代码化</strong> 了,另一方面也是将 <strong>安装依赖的过程代码化</strong> 了,于是我们就可以像管理源码一样使用git对Dockerfile进行版本管理。</p> <h3>为啥用Docker?</h3> <p>当你的系统越来越复杂的时候,你会发现Docker的价值。</p> <p>从应用架构角度理解Docker</p> <p>刚开始,你只需要写一个Node.js程序,挂载一个静态网站;然后,你做了一个用户账号系统,这时需要数据库了,比如说MySQL; 后来,为了提升性能,你引入了Memcached缓存;终于有一天,你决定把前后端分离,这样可以提高开发效率;当用户越来越多,你又不得不使用Nginx做反向代理; 对了,随着功能越来越多,你的应用依赖也会越来越多...总之,你的应用架构只会越来越复杂。不同的组件的安装,配置与运行步骤各不相同,于是你不得不写一个很长的文档给新同事,只为了让他搭建一个 <strong>开发环境</strong> 。</p> <p>使用Docker的话,你可以为不同的组件逐一编写Dockerfile,分别构建镜像,然后运行在各个容器中。这样做,将复杂的架构统一了,所有组件的安装和运行步骤统一为几个简单的命令:</p> <ul> <li> <p>构建Docker镜像: docker build</p> </li> <li> <p>上传Docker镜像: docker push</p> </li> <li> <p>下载Docker镜像: docker pull</p> </li> <li> <p>运行Docker容器: docker run</p> </li> </ul> <p>从应用部署角度理解Docker</p> <p>通常,你会有 <strong>开发</strong> , <strong>测试</strong> 和 <strong>生产</strong> 服务器,对于某些应用,还会需要进行 <strong>构建</strong> 。不同步骤的依赖会有一些不同,并且在不同的服务器上执行。如果手动地在不同的服务器上安装依赖,是件很麻烦的事情。比如说,当你需要为Node.js应用添加一个新的npm模块,或者升级一下Node.js,是不是得重复操作很多次?友情提示一下,手动敲命令是极易出错的,有些失误会导致致命的后果(参考最近Gitlab误删数据库与AWS的S3故障)。</p> <p>如果使用Docker的话, <strong>开发</strong> 、 <strong>构建</strong> 、 <strong>测试</strong> 、 <strong>生产</strong> 将全部在Docker容器中执行,你需要为不同步骤编写不同的Dockerfile。当依赖变化时,仅需要稍微修改Dockerfile即可。结合构建工具 Jenkins ,就可以将整个部署流程自动化。</p> <p>另一方面,Dockerfile将Docker镜像描述得非常精准,能够保证很强的一致性。比如,操作系统的版本,Node.js的版本,NPM模块的版本等。这就意味着,在本地开发环境运行成功的镜像,在 <strong>构建</strong> 、 <strong>测试</strong> 、 <strong>生产</strong> 环境中也没有问题。还有,不同的Docker容器是依赖于不同的Docker镜像,这样他们互不干扰。比如,两个Node.js应用可以分别使用不同版本的Node.js。</p> <p>从集群管理角度理解Docker</p> <p>架构规模越来越大的时候,你有必要引入集群了。这就意味着,服务器由1台变成了多台,同一个应用需要运行多个备份来分担负载。当然,你可以手动对集群的功能进行划分: Nginx服务器,Node.js服务器,MySQL服务器,测试服务器,生产服务器...这样做的好处是简单粗暴;也可以说财大气粗,因为资源闲置会非常严重。还有一点,每次新增节点的时候,你就不得不花大量时间进行安装与配置,这其实是一种低效的重复劳动。</p> <p>下载Docker镜像之后,Docker容器可以运行在集群的任何一个节点。一方面,各个组件可以共享主机,且互不干扰;另一方面,也不需要在集群的节点上安装和配置任何组件。至于整个Docker集群的管理,业界有很多成熟的解决方案,例如 Mesos , Kubernetes 与 Docker Swarm 。这些集群系统提供了 <strong>调度</strong> , <strong>服务发现</strong> , <strong>负载均衡</strong> 等功能,让整个集群变成一个整体。</p> <h3>如何用Docker?</h3> <p>编写Dockerfile</p> <p>正确的 Dockerfile 是这样的:</p> <pre> <code class="language-javascript"># 使用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"]</code></pre> <p>有几点值得注意的地方:</p> <ul> <li> <p>使用国内 DaoCloud 的Docker仓库,阿里云的ubuntu镜像以及淘宝的npm镜像,否则会出事情的;</p> </li> <li> <p>将时区设为Asia/Shanghai,否则日志的时间会不大对劲;</p> </li> <li> <p>使用.dockerignore忽略不需要添加到Docker镜像的文件和目录,其语法与.gitigore一致;</p> </li> </ul> <p>更重要的一点是, <strong>package.json需要单独添加</strong> 。Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。</p> <p>构建Docker镜像</p> <p>使用 <strong>docker build</strong> 命令构建Docker镜像</p> <pre> <code class="language-javascript">sudo docker build -t fundebug/nodejs .</code></pre> <p>其中,-t选项用于指定镜像的名称。</p> <p>使用 <strong>docker images</strong> 命令查看Docker镜像</p> <pre> <code class="language-javascript">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</code></pre> <p>可知,fundebug/nodejs镜像的大小为266.4MB,在ubuntu镜像的基础上增加了80MB左右。</p> <p>运行Docker容器</p> <p>使用 <strong>docker run</strong> 命令运行Docker容器</p> <pre> <code class="language-javascript">sudo docker run -d --net=host --name=hello-fundebug fundebug/nodejs</code></pre> <p>其中,-d选项表示容器在后台运行;--net选项指定容器的网络模式,host表示与主机共享网络;--name指定了容器的名称。</p> <p>使用 <strong>docker ps</strong> 命令查看Docker容器</p> <pre> <code class="language-javascript">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-</code></pre> <p>可知,COMMAND为"node /app/app.js",表示容器中运行的命令。这是我们再Dockerfile中使用CMD指定的。不妨使用 <strong>docker exec</strong> 命令在容器内执行ps命令 <strong>查看容器内的进程</strong> :</p> <pre> <code class="language-javascript">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</code></pre> <p>可知,容器内的1号进程即为node进程 <strong>node /app/app.js</strong> 。在Linux中,PID为1进程按说是唯一的,即init进程。但是,容器使用了内核的Namespace机制,为容器创建了独立的PID空间,因此容器中也有1号进程。</p> <p>测试</p> <p>使用curl命令访问:</p> <pre> <code class="language-javascript">curl localhost:3000 Hello Fundebug!</code></pre> <h3>是否用Docker?</h3> <p>一方面,使用Docker能够带来很大益处;另一方面,引入Docker必然会有很多挑战,需要熟悉Docker才能应对自如。 <strong>想必这是一个艰难的决定</strong> 。如果从长远的角度来看,Docker正在成为应用开发,部署,发布的标准技术,也许我们不得不用开放的心态对待它。</p> <p>作为Node.js开发者,真正理解Docker可能需要一些时间,但是它可以给你带来很多便利。</p> <h3>参考链接</h3> <ul> <li> <p><a href="/misc/goto?guid=4958986608713928133" rel="nofollow,noindex">DOCKER基础技术:LINUX NAMESPACE(上)</a></p> </li> <li> <p><a href="/misc/goto?guid=4958986608823436229" rel="nofollow,noindex">DOCKER基础技术:LINUX NAMESPACE(下)</a></p> </li> <li> <p><a href="/misc/goto?guid=4958986608928671167" rel="nofollow,noindex">DOCKER基础技术:LINUX CGROUP</a></p> </li> <li> <p><a href="/misc/goto?guid=4959746773966808480" rel="nofollow,noindex">从GITLAB误删除数据库想到的</a></p> </li> <li> <p><a href="/misc/goto?guid=4959746774049044866" rel="nofollow,noindex">AWS 的 S3 故障回顾和思考</a></p> </li> </ul> <p> </p> <p> </p> <p>来自:https://segmentfault.com/a/1190000008945039</p> <p> </p>