后端业务逻辑一般比较复杂,全堆在一个 http 服务里不太现实,所以基本都会用微服务架构来开发了。
比如这样:
![7c112c4f1b61ecdca2f7afb4fb0aaa2a.png](https://i-blog.csdnimg.cn/blog_migrate/929368bda62593db7959b3be1fa7f024.png)
把不同模块的业务逻辑拆分到不同微服务里,然后它们和主服务通过 tcp 通信,最终由主服务返回 http 响应。
比如我用 nest 开发的一个微服务项目(具体开发过程见上篇文章):
两个微服务分别监听了 8888 端口和 9999 端口:
![6259f8ede9a7c5696bfe37571986496e.png](https://i-blog.csdnimg.cn/blog_migrate/a173028c63f493d423213dfd118dafcd.png)
用 yarn start 把它们跑起来:
![5e6339c9911347e5f726d1d040daefc1.png](https://i-blog.csdnimg.cn/blog_migrate/0a17ea0782825ae739e8f40ae9af29c8.png)
主服务里注册了这两个微服务:
![857b60d7360c82804ef95f5074ce09a1.png](https://i-blog.csdnimg.cn/blog_migrate/1f1da20b874bb1dcc6a5151799818806.png)
它监听了 3000 端口,同样用 yarn start 把它跑起来:
![6d4e5c151690b0c7bdccac04e2e3db73.png](https://i-blog.csdnimg.cn/blog_migrate/d381abe193baad03224e104af604b6a2.png)
计算的微服务里有一个求和的逻辑:
![c2e9021ea2ca453e1ce75f6776f4a54e.png](https://i-blog.csdnimg.cn/blog_migrate/b43b020954eb0329b12cd6ed72ae2b9c.png)
日志的微服务里有一个打印日志的逻辑:
![9378a572fc1df2c9e9a983645cc677e9.png](https://i-blog.csdnimg.cn/blog_migrate/d907030515bcb2659303b80132dbdc46.png)
主服务里接受参数,然后把它传给两个微服务:
![932284678f9022eb68b77ffddffe1476.png](https://i-blog.csdnimg.cn/blog_migrate/5a2a98bbbdf49cf64a40bbe939acb23b.png)
跑起来效果是这样的:
![e7e5ddb8325ef94b74e146ac91ce02d4.png](https://i-blog.csdnimg.cn/blog_migrate/542e61d6fd5428679efc767e61a996a8.png)
返回的是求和的结果,并且日志微服务做了打印:
![20cce9f5f8eaa04d1cd51750268db593.png](https://i-blog.csdnimg.cn/blog_migrate/efcb7caf4b125097f5d2d8bc4acf8ffb.png)
这说明微服务和 http 服务开发成功了。
那问题来了,开发完以后怎么部署到线上呢?
其实部署过程说起来也简单,就是执行 npm run build,然后把产物 dist 目录放到服务器上。
比如这个:
![5eaaafc7a03400d67819c28d36562ce7.png](https://i-blog.csdnimg.cn/blog_migrate/67272d06fc90189bc0541f023aca336b.png)
然后用 node 跑起来:
![4cb01374ab852bd3f0a1ae90d8aef38c.png](https://i-blog.csdnimg.cn/blog_migrate/0b854fe90d8db66d19ca96442a96783f.png)
但一般我们不会直接这么搞,而是会使用 docker 来做,因为这样手动搞的话每个服务都要这样来一次,太麻烦了,而且容易出错。
docker 可以通过 Dockerfile 把构建和运行流程封装起来,还可以把运行环境也封装到镜像里,这样每次部署只需要重新构建镜像,然后服务器把镜像拉下来跑就行。
比如 main 服务的 Dockerfile 我们会这么写:
FROM node:alpine as development
WORKDIR /usr/app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:alpine as production
WORKDIR /usr/app
COPY package.json ./
RUN npm install --only=production
COPY . .
COPY --from=development /usr/app/dist ./dist
CMD ["node", "dist/main.js"]
一行行来看:
FROM node:alpine as development
这一行是继承 node 基础镜像的意思,as 后面是给它起个名字。
WORKDIR /usr/app
把容器内的当前目录设置为 /user/app
COPY package.json ./
把宿主机的 package.json 复制到容器当前目录,也就是 /user/app 下。
RUN npm install
package.json 复制过去了,自然就可以在容器内安装依赖了。
COPY . .
然后再把其余的内容都复制过去。
这里可以加个 .dockerignore 文件来排除 node_modules 的复制
![07195ff4011a47f67ded16930fb1c06a.png](https://i-blog.csdnimg.cn/blog_migrate/dfb837c5dac4b4f1d327eed7be46e84d.png)
RUN npm run build
复制完之后执行 npm run build,就在容器内生成了 dist 目录。
FROM node:alpine as production
为啥用重新创建了个镜像呢?
这是因为 build 完之后我们就只需要 dist 目录了,其余的源码啥的都不需要,自然可以在一个新容器里,然后把上个容器的 dist 目录复制过去。
WORKDIR /usr/app
COPY package.json ./
RUN npm install --only=production
COPY . .
然后同样是设置当前目录,复制 package.json,执行 npm install,然后复制其它文件。
这里 npm install 加个 --only=production 可以只安装 dependecies 下的包。
怎么复制呢?还记得我们 as 后面指定了一个名字么,就通过那个来指定从上个容器复制:
COPY --from=development /usr/app/dist ./dist
其实这种叫做分阶段构建,不然你要写两个 Dockerfile 才行。
最后指定容器运行起来的时候执行的命令,也就是 node dist/main.js 把这个服务跑起来:
CMD ["node", "dist/main.js"]
有了这个 Dockerfile 就可以通过 docker build 命令生成 docker 镜像了:
docker build -t main-app .
这行命令的意思就是从 . 目录下的 Dockerfile 来构建一个 docker 镜像,名字是 main-app。
它会一层层构建,我们刚好 14 行命令:
![0632050d2571cc49652a7cc38026ecf4.png](https://i-blog.csdnimg.cn/blog_migrate/aa3b7996a1c5f67322e51e936251c960.png)
构建完可以看到这个镜像的 hash。
当然,在 docker desktop 里也可以看到:
![f0640846d2c42023f1d6e528211990b4.png](https://i-blog.csdnimg.cn/blog_migrate/b900bcccff076a93a34c193140a3e22e.png)
这里用到的 docker desktop 从官网下载就行:
![68e6c4145cae46ba9dd85940eb886ed9.png](https://i-blog.csdnimg.cn/blog_migrate/20310793abf72761096ec683ac46dfe2.png)
它除了会安装 docker 桌面端以外,也会同时安装 docker 和 docker-compose。
然后把它跑起来:
docker run -p 3000:3000 main-app
![f71eea1e486a68d4a86b3b411ce759f1.png](https://i-blog.csdnimg.cn/blog_migrate/fa4ffa25a0db069ba4e93ee05a2835f0.png)
-p 是端口映射的意思,也就是把宿主机的 3000 端口映射到容器的 3000 端口,这样宿主机就可以访问 3000 端口的 http 服务了。
![0c2ad231ac1ad98024130c1d1c14fcf2.png](https://i-blog.csdnimg.cn/blog_migrate/f0644ba127c7e85f249b89a1380230a0.png)
确实可以访问,只不过报了 500,因为两个微服务还没起嘛。
然后我们写下 log 微服务的 DockerFile:
FROM node:alpine As development
WORKDIR /usr/app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:alpine as production
WORKDIR /usr/app
COPY package.json ./
RUN npm install --only=production
COPY . .
COPY --from=development /usr/app/dist ./dist
CMD ["node", "dist/main.js"]
一毛一样,就不解释了。
然后执行
docker build -t ms-log .
同样,是用当前目录的 Dockerfile 构建一个名字为 ms-log 的镜像:
![e1c38e17d1f3938e6e1f8cb02d74bd95.png](https://i-blog.csdnimg.cn/blog_migrate/d0e0c4748904c8cabb8a6dbce8298743.png)
然后用
docker run -p 9999:9999 ms-log
把这个容器跑起来,映射容器内的 9999 端口到宿主机的 9999 端口。
![849127b8fc3e08c653671c1777a10a95.png](https://i-blog.csdnimg.cn/blog_migrate/30d0551042a07c3e617d30782a335366.png)
还有一个计算微服务,我们同样这么搞,就不展开了。
这时候你就可以在 docker desktop 里面看到这三个 image(镜像):
![34ba0e673709f932d19f744769e1b55a.png](https://i-blog.csdnimg.cn/blog_migrate/59bc79087a50b02ec4ae28ae73588e7e.png)
还有它们仨跑起来的 container(容器):
![a618899bbbe74fe6117fcfdf0f82f4ca.png](https://i-blog.csdnimg.cn/blog_migrate/de122905a92eb860b300fd97577681b9.png)
这时候你浏览器访问一下 locahost:3000
![30d25a8d7a85281d0b69500be67e4c13.png](https://i-blog.csdnimg.cn/blog_migrate/03ab68c55fd19a0d859ebb37984aadda.png)
你就会发现返回了计算的结果,日志微服务容器内也打印了日志:
![b5b3c380537804955eddc29df80aef98.png](https://i-blog.csdnimg.cn/blog_migrate/f878eb16b109c9c36ec6861f04a29b17.png)
在 docker desktop 里看更方便一些:
![9e157627bcfed260790b30d53b5ca63f.gif](https://i-blog.csdnimg.cn/blog_migrate/32316374796dc37dc9ade08976927ab5.gif)
至此,我们 docker 部署 node 微服务就成功了!
其实有一点比较重要的我前面没说,这里提一下:
两个微服务要起服务的时候要指定 0.0.0.0 这个 ip:
![76e8031e8d73aefdb7781f85d3f374d1.png](https://i-blog.csdnimg.cn/blog_migrate/19f8c84ad1e2a66fbc96e4f967362956.png)
这涉及到 0.0.0.0 和 127.0.0.1 的区别:
127.0.0.1 和 localhost 一样,都是只本机地址。
0.0.0.0 不是一个 ip 地址,它指代的是本地所有网卡的 ip。
这里如果用默认的 localhost,那服务只在容器内生效,要指定 0.0.0.0 才行。
再就是主服务里访问这两个微服务的时候要用宿主机 ip 地址访问:
![a82b531c491edfde92c9b0d872daeb9a.png](https://i-blog.csdnimg.cn/blog_migrate/7f719e8b4cebf0a587052528215c9739.png)
同样是因为访问的是宿主机的 ip 的那个服务。
有的同学可能会问,跑三个就执行三次 docker build 和 docker run,也太麻烦了吧,要是我有 10 个微服务,之间还有先后顺序的要求呢?
没错,这样确实比较麻烦,所以有了 docker compose。
这个也是 docker 自带的工具。
我们在根目录写这样一个 docker-compose.yml 的文件:
services:
main-app:
build:
context: ./main-app
dockerfile: ./Dockerfile
depends_on:
- ms-calc
- ms-log
- rabbitmq
ports:
- '3000:3000'
ms-calc:
build:
context: ./micro-service-calc
dockerfile: ./Dockerfile
ports:
- '8888:8888'
ms-log:
build:
context: ./micro-service-log
dockerfile: ./Dockerfile
ports:
- '9999:9999'
rabbitmq:
image: rabbitmq
ports:
- '5672:5672'
这个还是比较容易看懂的。
分别指定了 main-app、ms-calc、ms-log、rabbitmq 的 dockerfile 的地址以及端口映射。
而且通过 depends 指定了先后顺序。
这样只要跑一次 docker-compose up 就可以把它们全部跑起来。
(rabbitmq 那个只是用来测试的,其实没用到)
特别要注意 context 的配置,这个是指定路径的基础目录的,比如这个 package.json:
![72d833db94cf61012129a24de880a59d.png](https://i-blog.csdnimg.cn/blog_migrate/2a098671f059e88a46b9abceae55b18f.png)
加上 context: ./micro-service-log 那就是 ./micro-service-log/package.json。
不加找不到路径。
我们把那 3 个容器停掉:
![4eda54b97c06aeabc4b02629b9326917.png](https://i-blog.csdnimg.cn/blog_migrate/f991d5133857c234b440aa084c8d8ff4.png)
执行
docker-compose up
跑起来是这样的:
上面都是 rabbitmq 这个容器的日志。
我们访问下 localhost:3000
![26b085ece05ca7e02ef675e7c491f262.png](https://i-blog.csdnimg.cn/blog_migrate/72a5b0ec6b18b755f6b8042ea99d4d3a.png)
可以看到 main-app 和 ms-log 的日志:
![c274ddcc719d7164d08100bb69f6afbf.png](https://i-blog.csdnimg.cn/blog_migrate/4f8ac440ca145ca08d3bbf49a7a20671.png)
这是因为 docker-compose 把终端合并了,加了个前缀来区分。
在 docker desktop 也可以看到新跑起来的 4 个容器:
![226721895c0cc89a869c0e5389360d48.png](https://i-blog.csdnimg.cn/blog_migrate/3ee4b7201bb7fecae98bee3a21ce33b8.png)
这样 3 个node服务就都跑起来了。感受到 docker compose 的好处了么?
它可以批量创建一批容器,并且指定顺序、参数之类的,也就是容器编排。
总结
我们分别用 docker 和 docker compose 实现了 Node.js 的微服务部署。
dockerfile 里指定宿主机文件到容器内的复制,npm install 以及把 node 服务跑起来的逻辑。
可以使用分阶段构建功能来优化,也就是 from 的时候通过 as 指定一个名字,然后之后再一个 from 重新创建镜像,这时可以从上个镜像里复制文件。
之后执行 docker build 根据 Dockerfile 构建镜像,通过 docker run -p 宿主机端口:容器内端口 把镜像跑起来。
可以通过 docker desktop 来管理,更方便一些。
这里涉及到的 ip 要指定 0.0.0.0 或者具体的宿主机 ip 才行,要注意一下。
但这样三个服务就要跑 3 次 docker 镜像,比较麻烦。
可以用 docker-compose 来做容器编排,指定容器的 dockerfile、启动顺序等等。
这就是用 Docker 或者 Docker Compose 部署 node 微服务的方式,你学会了么?
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。