高质量Node.js微服务的编写和部署
bluesky329
8年前
<p>为促进Docker、Kubernetes等技术的交流传播,同时帮助用户更全面地了解时速云产品及其应用,时速云每两周会进行一次技术分享,分享时间为 <strong>周四晚8:30-9:30</strong> 在用户微信群、时速云技术分享群等进行产品、容器技术相关的技术直播分享和现场答疑。以下整理自 <strong>7月28日第十三期技术分享内容</strong> ,由 <strong> 时速云工程师 张鹏程</strong> 分享。</p> <p>微服务架构是一种构造应用程序的替代性方法。应用程序被分解为更小、完全独立的组件,这使得它们拥有更高的敏捷性、可伸缩性和可用性。</p> <p>一个复杂的应用被拆分为若干微服务,微服务更需要一种成熟的交付能力。持续集成、部署和全自动测试都必不可少。编写代码的开发人员必须负责代码的生产部署。构建和部署链需要重大更改,以便为微服务环境提供正确的关注点分离。</p> <p>后续我们会聊一下如何在时速云平台上集成 DevOps。</p> <p><img src="https://simg.open-open.com/show/f4cbbeaafd1ef8d8fb3141ca996542d8.png"></p> <p>Node.js 是构建微服务的利器,为什么这么说呢,我们先看下 Node.js 有哪些优势:</p> <ol start="1"> <li>Node.js 采用事件驱动、异步编程,为网络服务而设计</li> <li>Node.js 非阻塞模式的IO处理给 Node.js 带来在相对低系统资源耗用下的高性能与出众的负载能力,非常适合用作依赖其它IO资源的中间层服务</li> <li>Node.js轻量高效,可以认为是数据密集型分布式部署环境下的实时应用系统的完美解决方案。</li> </ol> <p>这些优势正好与微服务的优势:敏捷性、可伸缩性和可用性相契合(捂脸笑),再看下Node.js 的缺点:</p> <ol start="1"> <li>单进程,单线程,只支持单核CPU,不能充分的利用多核CPU服务器。一旦这个进程 down 了,那么整个 web 服务就 down 了</li> <li>异步编程,callback 回调地狱</li> </ol> <p>第一个缺点可以通过启动多个实例来实现CPU充分利用以及负载均衡,话说这不是 K8s 的原生功能吗。</p> <p>第二个缺点更不是事儿,现在可以通过 generator 、 promise 等来写同步代码,爽的不要不要的。</p> <p><em>下面我们主要从</em> <em>Docker 和 Node.js 出发聊一下高质量Node.js微服务的编写和部署:</em></p> <ol start="1"> <li>Node.js 异步流程控制:generator 与 promise</li> <li>Express、Koa 的异常处理</li> <li>如何编写 Dockerfile</li> <li>微服务部署及 DevOps 集成</li> </ol> <h2>1. Node.js 异步流程控制:Generator 与 Promise</h2> <p>Node.js 的设计初衷为了性能而异步,现在已经可以写同步的代码了,你造吗?</p> <p>目前 Node.js 的 LTS 版本早就支持了 Generator , Promise 这两个特性,也有许多优秀的第三方库bluebird、q 这样的模块支持的也非常好,性能甚至比原生的还好,可以用 bluebird 替换Node.js 原生的 Promise:</p> <pre> global.Promise = <strong>require</strong>('bluebird')</pre> <p>blurbird 的性能是 V8 里内置的 Promise 3 倍左右(bluebird 的优化方式见 <a href="/misc/goto?guid=4959675953632450642" rel="nofollow,noindex">https://github.com/petkaantonov/bluebird/wiki/Optimization-killers</a> )。</p> <h3>1.1 ES2015 Generator</h3> <p>generator 就像一个取号机,你可以通过取一张票来向机器请求一个号码。你接收了你的号码,但是机器不会自动为你提供下一个。</p> <p>换句话说,取票机“暂停”直到有人请求另一个号码( next() ),此时它才会向后运行。下面我们看一个简单的示例:</p> <p><img src="https://simg.open-open.com/show/a8a90105d8bbf72ea55111948b43c98f.png"></p> <p><img src="https://simg.open-open.com/show/6724a7ad4332c0e43a2046e4bf5ed934.jpg"></p> <p>从上面的代码的输出可以看出:</p> <ol start="1"> <li>generator 函数的定义,是通过 function *(){} 实现的</li> <li>对 generator 函数的调用返回的实际是一个遍历器,随后代码通过使用遍历器的 next() 方法来获得函数的输出</li> <li>通过使用 yield 语句来中断 generator 函数的运行,并且可以返回一个中间结果</li> <li>每次调用 next() 方法,generator 函数将执行到下一个 yield 语句或者是 return 语句。</li> </ol> <p>下面我们就对上面代码的每次next调用进行一个详细的解释:</p> <ol start="1"> <li>第1次调用 next() 方法的时候,函数执行到第一次循环的 yield index++ 语句停了下来,并且返回了 0 这个 value ,随同 value 返回的 done 属性表明 generator 函数的运行还没有结束</li> <li>第2次调用 next() 方法的时候,函数执行到第二循环的 yield index++ 语句停了下来,并且返回了 1 这个 value ,随同 value 返回的 done 属性表明 generator 函数的运行还没有结束</li> <li>… …</li> <li>第4次调用 next() 方法的时候,由于循环已经结束了,所以函数调用立即返回, done 属性表明 generator 函数已经结束运行, value 是 undefined 的,因为这次调用并没有执行任何语句</li> </ol> <p>PS: 如果在 generator 函数内部需要调用另外一个 generator 函数,那么对目标函数的调用就需要使用 yield* 。</p> <h3>1.2 ES2015 Promise</h3> <p>所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。</p> <p><img src="https://simg.open-open.com/show/0b226b83f96da249e757fd3a11d374e3.png"></p> <p>一个 Promise 一般有3种状态:</p> <p>1.pending : 初始状态, 不是 fulfilled ,也不是 rejected .</p> <p>2.fulfilled : 操作成功完成.</p> <p>3.rejected : 操作失败.</p> <p>一个 Promise 的生命周期如下图:</p> <p><img src="https://simg.open-open.com/show/240d9d7dd95d1e74f59d3b3430fd1ac8.png"></p> <p>下面我们看一段具体代码:</p> <p><img src="https://simg.open-open.com/show/91ca64229e45e48d3e79616eeb951047.png"></p> <p>asyncFunction 这个函数会返回 Promise 对象, 对于这个 Promise 对象,我们调用它的 then 方法来设置 resolve 后的回调函数, catch 方法来设置发生错误时的回调函数。</p> <p>该 Promise 对象会在 setTimeout 之后的 16ms 时被 resolve , 这时 then 的回调函数会被调用,并输出’Async Hello world’ 。</p> <p>在这种情况下 catch 的回调函数并不会被执行(因为 Promise 返回了 resolve ), 不过如果运行环境没有提供 setTimeout 函数的话,那么上面代码在执行中就会产生异常,在 catch 中设置的回调函数就会被执行。</p> <p><img src="https://simg.open-open.com/show/869a38af9a10237ed91372601033b596.png"></p> <h3>小结</h3> <p>如果是编写一个 SDK 或 API,推荐使用传统的 callback 或者 Promise,不使用 generator 的原因是:</p> <ul> <li>generator 的出现不是为了解决异步问题</li> <li>使用 generator 是会传染的,当你尝试 yield 一下的时候,它要求你也必须在一个 generator function 内</li> </ul> <p>看来学习 Promise 是水到渠成的事情。</p> <h2>2. Express、Koa 的异常处理</h2> <p><img src="https://simg.open-open.com/show/0094032bef3e990d892f87e3fbd45311.jpg"></p> <p>一个友好的错误处理机制应该满足三个条件:</p> <ol start="1"> <li>对于引发异常的用户,返回 500 页面</li> <li>其他用户不受影响,可以正常访问</li> <li>不影响整个进程的正常运行</li> </ol> <p>下面我们就以这三个条件为原则,具体介绍下 Express、Koa 中的异常处理。</p> <h3>2.1 Express 异常处理</h3> <p>在 Express 中有一个内置的错误处理中间件,这个中间件会处理任何遇到的错误。</p> <p>如果你在 Express 中传递了一个错误给 next() ,而没有自己定义的错误处理函数处理这个错误,这个错误就会被 Express 默认的错误处理函数捕获并处理,而且会把错误的堆栈信息返回到客户端,这样的错误处理是非常不友好的。</p> <p>还好我没可以通过设置 NODE_ENV 环境变量为 production ,这样 Express 就会在生产环境模式下运行应用,生产环境模式下 Express 不会把错误的堆栈信息返回到客户端。</p> <p>在 Express 项目中可以定义一个错误处理的中间件用来替换 Express 默认的错误处理函数:</p> <p><img src="https://simg.open-open.com/show/f470b33ccab37f9ce7173a04fcea28cc.png"></p> <p>在所有其他 app.use() 以及路由之后引入以上代码,可以满足以上三个友好错误处理条件,是一种非常友好的错误处理机制。</p> <h3>2.2 Koa 异常处理</h3> <p>我们以 Koa 1.x 为例,看代码:</p> <p><img src="https://simg.open-open.com/show/11c5bbeab906f5c5ed005b9101dd5bb9.png"></p> <p>把上面的代码放在所有 app.use() 函数前面,这样基本上所有的同步错误均会被 try{} catch(err){} 捕获到了,具体原理大家可以了解下 Koa 中间件的机制。</p> <h3>2.3 未捕获的异常 uncaughtException</h3> <p>上面的两种异常处理方法,只能捕获同步错误,而异步代码产生的错误才是致命的, uncaughtException 错误会导致当前的所有用户连接都被中断,甚至不能返回一个正常的 HTTP 错误码,用户只能等到浏览器超时才能看到一个 no data received 错误。</p> <p>这是一种非常野蛮粗暴的异常处理机制,任何线上服务都不应该因为 uncaughtException 导致服务器崩溃。</p> <p>在Node.js 我们可以通过以下代码捕获 uncaughtException 错误:</p> <p><img src="https://simg.open-open.com/show/dbf5e78b2352101b660d4e96b22bc305.png"></p> <p>捕获 uncaughtException 后,Node.js 的进程就不会退出,但是当 Node.js 抛出uncaughtException 异常时就会丢失当前环境的堆栈,导致 Node.js 不能正常进行内存回收。</p> <p>也就是说,每一次、 uncaughtException 都有可能导致内存泄露。既然如此,退而求其次,我们可以在满足前两个条件的情况下退出进程以便重启服务。</p> <p>当然还可以利用 domain 模块做更细致的异常处理,这里就不做介绍了。</p> <h2>3. 如何编写 Dockerfile</h2> <h3>3.1 基础镜像选择</h3> <p>我们先选用 Node.js 官方推荐的 node:argon 官方 LTS 版本最新镜像,镜像大小为 656.9 MB (解压后大小,下文提到的镜像大小没有特殊说明的均指解压后的大小)</p> <p>我们事先写好了两个文件 package.json , app.js :</p> <p><img src="https://simg.open-open.com/show/67708d02c3be1ab5083f8f0f4971c01a.png"> <img src="https://simg.open-open.com/show/cf58861333b3f977639217a045bbb2bd.png"></p> <p>下面开始编写 Dockerfile,由于直接从 Dockerhub 拉取镜像速度较慢,我们选用时速云的docker官方镜像docker_library/node(https://hub.tenxcloud.com/repos/docker_library/node),这些官方镜像都是与 Dockerhub 实时同步的:</p> <p><img src="https://simg.open-open.com/show/80148a00d7589ec1d09635bf2faabcd2.png"></p> <p>执行以下命令进行构建:</p> <pre> docker build -t zhangpc/docker_web_app:argon .</pre> <p>最终得到的镜像大小是 660.3 MB ,体积略大,Docker 容器的优势是轻量和可移植,所以承载它的操作系统即基础镜像也应该迎合这个特性,于是我想到了 Alpine Linux ,一个面向安全的,轻量的 Linux 发行版,基于 musllibc 和 busybox 。</p> <p>下面我们使用 alpine:edge 作为基础镜像,镜像大小为 4.799 MB : <a href="https://simg.open-open.com/show/80148a00d7589ec1d09635bf2faabcd2.png" rel="nofollow,noindex"> </a> <img src="https://simg.open-open.com/show/01a60370678a0fb716a231065d9f7306.png"></p> <p>执行以下命令进行构建:</p> <pre> docker build -t zhangpc/docker_web_app:alpine .</pre> <p>最终得到的镜像大小是 31.51 MB ,足足缩小了20倍,运行两个镜像均测试通过。</p> <h3>3.2 还有优化的空间吗?</h3> <p>首先,大小上还是可以优化的,我们知道 Dockerfile 的每条指令都会将结果提交为新的镜像,下一条指令将会基于上一步指令的镜像的基础上构建。</p> <p>所以如果我们要想清除构建过程中产生的缓存,就得保证产生缓存的命令和清除缓存的命令在同一条 Dockerfile 指令中,因此修改 Dockerfile 如下:</p> <p><img src="https://simg.open-open.com/show/b155c21d98d601c97b404e4aa9cd21c7.png"></p> <p>执行以下命令进行构建:</p> <pre> docker build -t zhangpc/docker_web_app:alpine .</pre> <p>最终得到的镜像大小是 21.47 MB ,缩小了10M。</p> <p>其次,我们发现在构建过程中有一些依赖是基本不变的,例如安装 Node.js 以及项目依赖,我们可以把这些不变的依赖集成在基础镜像中,这样可以大幅提升构建速度,基本上是秒级构建。</p> <p>当然也可以把这些基本不变的指令集中在 Dockerfile 的前面部分,并保持前面部分不变,这样就可以利用缓存提升构建速度。</p> <p>最后,如果使用了 Express 框架,在构建生产环境镜像时可以设置 NODE_ENV 环境变量为 production ,可以大幅提升应用的性能,还有其他诸多好处,下面会有介绍。</p> <h3>小结</h3> <p><img src="https://simg.open-open.com/show/c247284ad1c1a15538af3f80896ff158.jpg"></p> <p>我们构建的三个镜像大小对比见上图,镜像的大小越小,发布的时候越快捷,而且可以提高安全性,因为更少的代码和程序在容器中意味着更小的攻击面。</p> <p>使用 node:argon 作为基础镜像构建出的镜像(tag 为 argon)压缩后的大小大概为 254 MB ,也不是很大。</p> <p>如果对 Alpine Linux 心存顾虑的童鞋可以选用 Node.js 官方推荐的 node:argon 作为基础镜像构建微服务。</p> <h2>4.微服务部署及 devops 集成</h2> <p>部署微服务时有一个原则:一个容器中只放一个服务,可以使用stack 编排把各个微服务组合成一个完整的应用:</p> <p><img src="https://simg.open-open.com/show/4a07bbadbce16a274f93ea373456f90a.png"></p> <h3>4.1 Dokcer 环境微服务部署</h3> <p>安装好 Docker 环境后,直接运行我们构建好的容器即可:</p> <pre> docker run -d <em>--restart=always -p 8080:8080 --name docker_web_app_alpine zhangpc/docker_web_app:alpine</em></pre> <h3>4.2 使用时速云平台集成 DevOps</h3> <p>时速云目前支持github、gitlab、bitbucket、coding 等代码仓库,并已实现完全由API接入授权、webhook等,只要你开发时使用的是这些代码仓库,都可以接入时速云的 CI/CD 服务:</p> <p><img src="https://simg.open-open.com/show/aec12c9297bb6a38bf438c4aec5142b2.jpg"></p> <p>下面我们简单介绍下接入流程:</p> <ol start="1"> <li>创建项目,参考文档 <a href="/misc/goto?guid=4959675953731342290" rel="nofollow,noindex">http://doc.tenxcloud.com/doc/v1/ci/project-add.html</a></li> <li>开启CI<br> <img src="https://simg.open-open.com/show/68aa01eb3d19ec2d32bc808e4751eb5f.png"></li> <li>更改代码并提交,项目自动构建<br> <img src="https://simg.open-open.com/show/ced13d4b7fa25600707ad33774f08bb3.png"></li> <li>用构建出来的镜像( tag 为 master )创建一个容器</li> </ol> <p><img src="https://simg.open-open.com/show/76c21f12581a0df5369bb74c4382a6b7.png"> <img src="https://simg.open-open.com/show/876d8ca40f8695d2d7c9867cdb31bfca.png"></p> <ol start="5"> <li>开启CD,并绑定刚刚创建的容器</li> </ol> <p><img src="https://simg.open-open.com/show/70051d58c8532e03f58b97dd8b39fc01.png"></p> <ol start="6"> <li>更改代码,测试 DevOps</li> </ol> <p><img src="https://simg.open-open.com/show/ff42c7ba0b020ae9830a588ae662cca8.png"></p> <p><img src="https://simg.open-open.com/show/0d50a549fc45f441d1f9e58583bb2bfb.png"></p> <p><img src="https://simg.open-open.com/show/0d50a549fc45f441d1f9e58583bb2bfb.png"> <img src="https://simg.open-open.com/show/c023ae6ebd1a5f845f4230ad57b14571.png"></p> <p>我们可以看到代码更改已经经过构建(CI)、部署(CD)体现在了容器上。</p> <h3>参考资料:</h3> <ul> <li>《微服务、SOA 和 API:是敌是友?》 <a href="/misc/goto?guid=4959675953824710554" rel="nofollow,noindex">http://www.ibm.com/developerw…</a></li> <li>《解析微服务架构(一):什么是微服务》 <a href="/misc/goto?guid=4959675953910422208" rel="nofollow,noindex">http://t.cn/RtXiKLS</a></li> <li>《微服务选型之Modern Node.js》 <a href="/misc/goto?guid=4959675953996175802" rel="nofollow,noindex">https://github.com/i5ting/mod…</a></li> <li>帅龙攻城狮《镜像构建优化之路》 <a href="/misc/goto?guid=4959675954079644256" rel="nofollow,noindex">http://blog.tenxcloud.com/?p=…</a></li> <li>《微容器:更小的,更轻便的Docker容器》 <a href="/misc/goto?guid=4959675954167208455" rel="nofollow,noindex">http://blog.tenxcloud.com/?p=…</a></li> <li>黄鑫攻城狮的内部分享《Dockerfile技巧分享》</li> <li>《Node 出现 uncaughtException 之后的优雅退出方案》 <a href="/misc/goto?guid=4959675954252115347" rel="nofollow,noindex">http://www.infoq.com/cn/artic…</a></li> <li>《Express Error handling》 <a href="/misc/goto?guid=4959675954339544609" rel="nofollow,noindex">https://expressjs.com/en/guid…</a></li> <li>《Promise 迷你书》 <a href="/misc/goto?guid=4959675954408838546" rel="nofollow,noindex">http://liubin.org/promises-book/</a></li> <li>《如何把 Callback 接口包装成 Promise 接口》 <a href="/misc/goto?guid=4959675954504517152" rel="nofollow,noindex">http://www.75team.com/post/ho…</a></li> </ul> <h3>Q&A:</h3> <p>1. 自动构建对程序的要求是什么?有dockerfile就可以了吗?</p> <p>答:自动构建对程序没有要求,只要有Dockerfile就行。</p> <p>2. 一个容器只放一个服务,这个成本有点高吧?</p> <p>答:结合 stack 编排,成本还是可控的,一个容器对应一个服务比较符合微服务的理念</p> <p>3. node是单进程的 容器中的node也是单进程部署的吗?或者说容器的cpu需不需要配置多核?</p> <p>答:容器的CPU一般是按时间片划分的,容器中的 node 一般都是单进程部署,结合 k8s 可以建立多个实例,实现负载均衡。</p> <p>4.以前没有容器 node进程挂了 操作系统关心的是进程 现在容器中跑node k8s去关心容器挂不挂 容器中的node如果挂了 就是容器去把node再重启吗?</p> <p>答:如果是单机部署的话 可以用 –restart=always 命令实现容器自动重启; k8s可以支持对容器内服务定义探针,根据规则可以对服务进行重启或者从前端路由摘除</p> <p>5.docker daemon挂了的话那也没办法了?</p> <p>答:docker daemon挂了的话,可以通过节点agent自动恢复docker daemon,或者自动把服务迁移到其他正常服务节点。</p> <h3> </h3> <p>来自:http://blog.tenxcloud.com/?p=1566</p> <p> </p>