代码 9.1 使用配置文件

    作为一个健壮的线上环境,肯定不希望自己的应用程序垮掉。然而,现实开发中在代码中总是会时不时出现未捕获的异常导致程序崩溃,真实编程实践中,我们肯定会对代码慎之又慎,但是想要代码100%无bug是不可能的,想想那个整天升级打补丁的微软。
    我们用下面代码监听未捕获异常:

    1. process.on('uncaughtException', function(err) {
    2. try {
    3. errorlogger.error('出现重大异常,重启当前进程',err);
    4. } catch(e) {
    5. console.log('请检查日志文件是否存在',e);
    6. }
    7. console.log('kill current proccess:'+process.pid);
    8. process.exit();
    9. });

    代码 9.2.1 监听未捕获异常
    代码 9.2.1中最后一行将当前进程强制退出,这是由于如果如果不这么做的话,很有可能会触发内存泄漏。我们肯定希望进程在意外退出的时候,能够重新再启动。这种需求其实可以使用 Node 的 cluster 来实现,这里我们不讲如何通过代码来达到如上需求,我们介绍一个功能十分之完备的工具——。
    首先我们运行 cnpm install pm2 -g 对其进行全局安装。为了做对比,我们首先来观察不用pm2的效果。本章用的源码是第6章的基础上完成的,由于在第6章中我们使用了登陆拦截器,为了不破坏这个结构,我们新生成一个路由器,放置在 routes/test.js,然后在 app.js 中引入这个拦截器:

    1. app.use('/test',testRotes);
    2. app.use(authFilter);
    3. app.use('/', routes);

    代码 9.2.2 添加测试路由器
    然后在 routes/test.js 中添加让程序崩溃的代码:

    1. router.get('/user', function(req, res) {
    2. setTimeout(function() {
    3. console.log(noneExistVar.pp);
    4. res.send('respond with a resource');
    5. },0);
    6. });

    代码 9.2.3 导致进程崩溃
    可能你要问,这个地方为啥要加个 setTimeout ,因为如果你不把这个错误放到异步代码中,就会像代码 5.2.6那样被express本身捕获到,就不会触发未捕获异常了。
    最后启动应用,访问 /test/user 路径,不出意外,程序崩溃了。
    然后我们用 pm2 来启动:

    pm2 start src/bin/www
    运行成功后会有如下输出:

    1. [PM2] Spawning PM2 daemon
    2. [PM2] PM2 Successfully daemonized
    3. [PM2] Starting src/bin/www in fork_mode (1 instance)
    4. [PM2] Done.
    5. ┌──────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────────────┬──────────┐
    6. App name id mode pid status restart uptime memory watching
    7. ├──────────┼────┼──────┼──────┼────────┼─────────┼────────┼─────────────┼──────────┤
    8. www 0 fork 5804 online 0 10s 29.328 MB disabled
    9. └──────────┴────┴──────┴──────┴────────┴─────────┴────────┴─────────────┴──────────┘
    10. Use `pm2 show <id|name>` to get more details about an app

    输出 9.2.1
    pm2 命令还有好多命令行参数,如果单纯手敲的话就太麻烦了,幸好它还提供了通过配置文件的形式来指定各个参数值,它支持使用 json 或者 yaml 格式来书写配置文件,下面给出一个 json 格式的配置文件:

    配置文件 9.2.1 process.json

    接着运行如下命令来启动项目:

      1. pm2 restart process.json

      命令 9.2.2

      如果想关闭当前进程,运行:

      1. pm2 stop process.json

      命令 9.2.3

      你还可以使用命令 来查看当前项目的日志。最后我们来测试一下,访问我们故意为之的错误页面http://localhost:8100/test/user,会看到控制台中会打印重启日志:

      1. chapter7-0 ReferenceError: noneExistVar is not defined
      2. chapter7-0 at null._onTimeout (/home/gaoyang/code/expressdemo/chapter7/src/routes/test.js:7:17)
      3. chapter7-0 at Timer.listOnTimeout (timers.js:92:15)
      4. chapter7-0 [2016-09-16 23:28:14.016] [ERROR] error - 出现重大异常,重启当前进程 [ReferenceError: noneExistVar is not defined]
      5. chapter7-0 ReferenceError: noneExistVar is not defined
      6. chapter7-0 at null._onTimeout (/home/gaoyang/code/expressdemo/chapter7/src/routes/test.js:7:17)
      7. chapter7-0 at Timer.listOnTimeout (timers.js:92:15)
      8. chapter7-0 [2016-09-16 23:28:14.025] [INFO] console - kill current proccess:6053
      9. chapter7-0 load var [port],value: 8100
      10. chapter7-0 load var [debuglogfilename],value: /tmp/debug.log
      11. chapter7-0 load var [tracelogfilename],value: /tmp/trace.log
      12. chapter7-0 load var [errorlogfilename],value: /tmp/error.log
      13. chapter7-0 [2016-09-16 23:28:14.908] [INFO] console - load var [db],value: { url: 'mongodb://localhost:27017/live',
      14. chapter7-0 dbOption: { safe: true } }
      15. chapter7-0 [2016-09-16 23:28:14.934] [INFO] console - load var [redis],value: { port: 6379, host: '127.0.0.1' }
      16. chapter7-0 [2016-09-16 23:28:15.003] [INFO] console - Listening on port 8100

      输出 9.2.1
      我们看到进程自己重启了,最终实现了我们的目的。

      虽然我们在服务上线的时候,可以请高僧来给服务器开光,其实只要不是傻子就看得出来那只不过博眼球的无耻炒作而已。机器不是你想不宕就不宕,所以说给你的服务加一个开机自启动,是绝对有必要的,庆幸的是 pm2 也提供了这种功能。

      以下演示命令是在 Ubuntu 16.04 做的,其他服务器差别不大,首先运行 pm2 startup,正常情况会有如下输出:

      按照上面的提示,用 pm2 save 产生当前所有已经启动的 pm2 应用列表,这样下次服务器在重启的时候就会加载这个列表,把应用再重新启动起来。

      最后,如果不想再使用开机启动功能,运行 pm2 unstartup systemv 即可取消。

      随着智能设备的蓬勃发展,整个互联网的网民总数出现了井喷,对于软件开发者来说,面对的用户群体越来庞大,需求变化原来越快,导致软件开发的规模越来越大,复杂度越来越高。为了应对这些趋势,最近几年一些新的技术渐渐被大家接受,比如说 devops,比如说我们接下来要讲的 ) 容器。

      pm2 提供了生成 Dockerfile 的功能,不过生成的文件实用性不是很强,我需要稍加改造了一下。另外为了方便的演示docker使用,专门在 oschina 新建一个代码仓库用于第8章代码。下面演示一下dockerfile的编写,具体流程是在docker构建的时候,使用 git clone 从仓库中拿去代码,然后安装所需的依赖。构建完成之后,每次启动这个docker容器的使用使用 pm2 命令启动当前应用。dockerfile的示例代码如下:

      1. FROM mhart/alpine-node:latest
      2. RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
      3. RUN apk update && apk add git && apk add openssh-client && rm -rf /var/cache/apk/*
      4. #创建应用目录
      5. RUN mkdir -p /var/app
      6. RUN mkdir -p /var/log/app
      7. #将git clone用的sshkey的私钥拷贝到.ssh目录下
      8. COPY deploy_key /root/.ssh/id_rsa
      9. RUN chmod 600 ~/.ssh/id_rsa
      10. #将当前git服务器域名添加到可信列表
      11. RUN ssh-keyscan -p 22 -t rsa git.oschina.net >> /root/.ssh/known_hosts
      12. WORKDIR /var/app
      13. #clone代码
      14. RUN git clone git@git.oschina.net:nodebook/chapter9.git .
      15. #拷贝配置文件
      16. COPY process.production.json process.json
      17. #安装cnpm
      18. RUN npm install -g cnpm --registry=https://registry.npm.taobao.org
      19. #安装pm2
      20. RUN cnpm install pm2 -g
      21. RUN cnpm install
      22. #向外暴漏当前应用的端口
      23. ## 设置环境变量
      24. ENV NODE_ENV=production
      25. # 启动命令
      26. CMD ["pm2-docker", "process.json"]

      代码 9.4.1 Dockerfile示例

      其中 From 代表使用的基础镜像, 是一个非常轻量级的 linux 发行版本,所以基于其制作的 docker 镜像非常小,特别利于安装。这里的 alpine-node 在 alipine 操作系统上集成了 node ,单纯 pull 安装的话也非常小。然后 RUN 和 COPY 两个命令是在构建的时候执行命令和拷贝文件,注意 COPY 命令仅仅只能拷贝当前执行docker 命令的目录下的文件,也就是说拷贝的时候不能使用相对路径,比如说你要执行 COPY xxx/yyy /tmp/yyy 或者 COPY ../zzz /tmp/zzz 都是不允许的。为了正确的 clone git 服务器上的代码,我们还需要配置一下 部署密钥。
      谈到部署密钥的概念,这里还要多说几句。我们一般从git服务器上clone下来代码后,会对代码进行编写,然后 push 你编写后的新代码。但是服务器上显然是不适合在其上面进行直接改动代码的从左,所以就有了部署密钥的概念,使用部署密钥你可以做 clone 和 pull 操作,但是你不能做 push 操作。

      1. $ ssh-keygen -f deploy_key -C "somebody@somesite.com"
      2. Generating public/private rsa key pair.
      3. Enter passphrase (empty for no passphrase):
      4. Enter same passphrase again:
      5. Your identification has been saved in deploy_key.
      6. Your public key has been saved in deploy_key.pub.
      7. The key fingerprint is:
      8. SHA256:S3JbyWc68K43kifBwYcJJxlIFlDlXz9MJDGI6gEhFKw somebody@somesite.com
      9. The key's randomart image is:
      10. +---[RSA 2048]----+
      11. |+o+==+o+ .+.. |
      12. | o....= o + |
      13. |. . ..= o. . |
      14. |E o .=o.= |
      15. | . ...So+ * |
      16. | . +o* + . |
      17. | oo+ |
      18. | +.+. |
      19. | .*.. |
      20. +----[SHA256]-----+

      命令9.4.1 生成密钥对

      我们在第8章项目代码根目录下新建一个 deploy 文件夹,进入这个文件夹然后运行 命令 9.4.1,一路回车即可。然后我们就得到了 代码 9.4.1 中的 deploy_key了。生成完了之后去 git.oschina.com 上配置一下公钥(也就是我们生成的 deploy_key.pub 文件),在项目页(在这里是 )上点击 管理 导航链接(),在打开的页面中点击 部署公钥管理,然后选择 添加公钥,用记事本打开刚才生成的 deploy_key.pub 文件,全选复制,然后贴到输入框中:


      图 9.4.1 添加部署公钥

      最后要注意一下 EXPOSE 命令,他代表 docker 及向宿主机暴漏的端口号,如果不暴漏端口的话,在宿主机上没法访问我们应用监听的端口。
      我们运行 docker build -t someone/chapter8 . 其中 -t 参数指定当前镜像的 tag 名称, someone 是指你在 docker hub 网站上注册的用户,build 成功后你可以通过 docker push someone/chapter8 将构建后的结构 push 到 docker hub 网站上去,然后在服务器上运行 docker pull someone/chapter8 来拿取你当初 push 的仓库。当然你可以直接将 Dockerfile 拿到你的服务器上执行 build 命令,这时候 -t 参数可以随便指定,甚至不写。

      build 命令运行完成之后,运行 docker images 会输出:

      1. REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
      2. someone/chapter8 latest 2a1a00cc1b41 4 minutes ago 147.7 MB
      1. fb0d726a86dc someone/chapter8 "pm2-docker process. 4 seconds ago Up 4 seconds 8100/tcp chapter8

      本章代码9.1、9.2小节代码和第8章存储在相同位置: , 9.4章节代码为演示方便专门做了一个仓库,位于:http://git.oschina.net/nodebook/chapter8