Node.js 和 Redis 部署在 Docker 容器中,使用 Docker Compose - 然后使用 Nginx 对 Node.js 服务器进行负载平衡
本文包含两个主要阶段:
(1) 使用 Dockerfile 和 Docker Compose 将 Node.js 服务器应用程序和 Redis 数据库实例容器化到两个单独的 Docker 容器中,并展示这两个应用程序如何相互通信。
(2) 使用容器化的 Nginx 反向代理对 Node.js 服务器进行负载平衡。
**让我们从第一阶段开始:
(1) 使用 Dockerfile 和 Docker Compose 将 Node.js 服务器应用程序和 Redis 实例容器化到两个独立的 Docker 容器中,并展示这两个应用程序如何相互通信**
从一个简单的 Node.js 服务器 应用程序(我们称之为“test-webapp”)开始,它通过显示“访问次数”来响应 HTTP GET 请求。下面的编号方案(即 (1.1)、(1.2)、(1.3) 等)与下图中的编号相匹配:
[组件示意图](https://res.cloudinary.com/practicaldev/image/fetch/s--vjd6PFTp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3yp6moj945jli4n6vfl4.png)
图1.a - 组件示意图
在上面的“图1.a - 组件示意图”中,我们有以下组件:
(1.1) “Docker Container 1” - 容器运行 Node.js 服务器,名为“test-webapp”,与左侧的浏览器通信。每次我们刷新 URLlocalhost:80
即我们向 Node.js 服务器“test-webapp”发送一个GET
命令,服务器代码增加访问次数,然后将此值保存到 Redis 数据库实例在“Docker Container 2”上运行,并且还在浏览器中显示该值。
(1.2) “Dockerfile” - 定义和控制“Docker Container 1”中的 Node.js 服务器 进程。
(1.3, 1.3.1, 1.3.2) “docker-compose.yml” – Docker Compose 配置文件定义和控制“ Docker 容器 1”和“Docker 容器 2”。 “Docker Container 1”运行Node.js服务器进程“test-webap_p”。 “_Docker Container 2”运行 Redis 数据库实例。
(1.3.3) Docker Compose 默认在“Docker Container 1”和“Docker Container 2”之间建立通信网络,允许 Node.js 服务器 进程“test-webapp”与 Redis 数据库实例通信,并在它们之间交换“对应用程序/Web 服务器的访问次数”(numVisits
)值。
(1.3.4) Docker Compose 将本地主机端口 80 映射到“Docker Container 1”端口 5000。端口 5000 是 Node.js 服务器“test-webapp”所在的端口” 监听并响应浏览器发送的GET
条命令。
(1.4) 连接到“Docker Container 2”的shell,然后通过“redis-cli
”连接到**Redis数据库实例的客户端命令行我们可以看到numVisits
的值(代表数字浏览器向 Node.js 服务器发出GET
命令的次数)与 Node.js 服务器在浏览器中显示的值同步——因此表明进程之间发生了进程间通信“Docker Container 1”中的“test-webapp”和“Docker Container 2”中的Redis**进程。
(1.5) 这一步说明了 Docker Compose 中的restart
指令和能力(在配置文件“docker-compose.yml”中指定)——当连接到“Docker Container 1”的 Linux shell 时,我们可以kill -9
Node.js server 进程,但是 Node.js server 进程会被 Docker Compose 自动重启——说明 Docker Compose 提供的自动恢复。
现在让我们描述一下这个场景的步骤和流程。下面描述中的编号方案(即(1.1)、(1.2)、(1.3)等)与“图 1.a – 组件示意图”中的编号相匹配。
(1.1) 文件结构:
[的文件结构](https://res.cloudinary.com/practicaldev/image/fetch/s--_yC5myBq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rm92ysovtnnq9azlunop.png)
图 1.b – 第 1 阶段的文件结构
进程‘test-webapp’的Node.js文件:
目录“test-webapp”的内容,Node.js 服务器“test-webapp”的源代码所在的位置:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--bSempo81--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880 /https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jqed4cw6m1mpm1upws9m.png)
(1.2) Dockerfile _containerizes 并控制 Node.js 应用程序,方法是从Docker Hub下载“_node:alpine”映像,在容器上安装 Node.js ,将源文件复制到容器 - 然后启动 Node.js 服务器 Web 应用程序(请参阅文件“server.js”中的源代码)。
(1.3) 转到上面的一个目录,我们看到“docker-compose.yml”文件,它组织了容器化并设置了所有组件的架构。 (文件
“docker-composer-nginx.yml”将在本文的第2阶段中介绍和解释)
[](https://res.cloudinary.com/practicaldev/image/fetch/s--naPa4IgZ--/c_limit%2Cf_auto%2Cfl_progressive %2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tqtlkv6v7vams8x3xq9d.png)
清除所有图像和容器:
我们运行命令docker system prune -a
来清除所有 Docker 映像和容器,并从一个干净的状态开始。
C:\test-docker\test-redis>docker system prune -a
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all images without at least one container associated to them
- all build cache
Are you sure you want to continue? [y/N] y
进入全屏模式 退出全屏模式
(1.3) 使用 Docker Compose 构建并运行“test-webapp”镜像
使用命令docker-compose -f <config-filename> build
构建容器以及将在每个容器中运行的应用程序:
C:\test-docker\test-redis>docker-compose -f docker-compose.yml build
请参阅以下构建的 Docker 映像的结果:
C:\test-docker\test-redis>docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test-redis_test-webapp latest e8145bea0fec 4 minutes ago 175MB
进入全屏模式 退出全屏模式
使用 'docker-compose' 运行 'test-webapp' 和 'redis' 容器:
让我们启动“test-webapp”和“redis”服务,如配置文件中所述
“docker-compose.yml”,使用docker-compose -f <config-filename> up
命令。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--7d3NTR5i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880 /https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66ukw6kmm4djhk2nzxsa.PNG)
我们可以从上面的输出中看到,“redis”容器(“test-redis_1”——对应于图 1.a 中的“Docker Container 2”)和“test-webapp”容器(与图 1.a 中的“Docker Container 1”相对应的“test-webapp_1”正在运行并打印到命令行窗口中的标准输出,我们在命令行窗口中启动了 Docker Compose 以运行这两个容器。
查看“test-webapp”和“redis”运行容器:
C:\test-docker\test-redis\test-webapp>docker ps
CONTAINER ID IMAGE PORTS
NAMES
928b8b07415d test-redis_test-webapp 0.0.0.0:80->5000/tcp test-redis_test-webapp_1
a8756127bff5 redis:alpine 6379/tcp test-redis_test-redis_1
进入全屏模式 退出全屏模式
(1.3.1, 1.3.2) 上面的两个容器匹配上面图1.a中的容器“Docker Container 1”和“Docker Container 2”。请注意“CONTAINER ID”列,我们将在下面使用其值对每个正在运行的容器执行操作。
(1.3.4) Node.js 服务器 "test-webapp" 容器中的端口 5000 映射到本地(托管)端口 80,因此当在本地(托管)浏览器中连接到 URL 时**http://localhost:80,对于每次刷新,“test-webapp”容器中的Node.js进程**都会增加变量numVisits
中的访问次数,该变量设置并保存在变量numVisits
中的 Redis - 此值也被发送回并显示在浏览器中。
“Docker-compose”默认设置一个包含“test-webapp”容器(图 1.a 中的“Docker Container 1”)和“redis”容器(中的“Docker Container 2”)的网络图 1.a) 在这个网络中,两个容器都可以通过这个网络相互访问。
本地浏览器与 Node.js 服务器 容器通信。在浏览器中刷新连接时,会调用服务器回调,以更新的访问次数响应浏览器。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--BKBbVg8w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880 /https://dev-to-uploads.s3.amazonaws.com/uploads/articles/euscx0msi6am2kz9tk7j.png)
(1.4)我们使用docker exec -it
命令允许我们连接到正在运行的容器,而-it
选项允许我们捕获该容器的标准输入/标准输出。然后我们指定从上面的docker ps
命令获得的 CONTAINER ID a8756127bff5,然后是我们要在进入容器时启动的 shell (sh)。
C:\test-redis\test-webapp>docker exec -it a8756127bff5 sh
然后,一旦我们进入容器的外壳,我们使用redis-cli
命令连接到 Redis 数据库。在 Redis 提示符下,我们使用get numVisits
来获取“redis”内部变量“numVisits”的值。我们可以看到“redis”实例与其各自容器中的“test-webapp”进程通信,并且**Redis**数据库实例中的变量“numVisits”与其在浏览器中的值同步。在这种情况下,两者都具有值“8”,因为我们刷新了 8 次“localhost:80” URL,因此在浏览器中发出了GET
命令,该命令被 *Node.js 服务器拦截* 增加“访问次数”(numVisits
)变量。 “访问次数”值由“test-webapp”进程发送回浏览器,该进程还将该值保存在“redis”数据库中的变量numVisits
中)。
/data # redis-cli
127.0.0.1:6379> get numVisits
"8"
127.0.0.1:6379>
进入全屏模式 退出全屏模式
在“redis”容器(“Docker Container 2”)中的“redis-cli”中,我们还可以在 Redis 中手动将“numVisits”变量设置为随机值,比如“342” “......
[](https://res.cloudinary.com/practicaldev/image/fetch/s--5mFZNh5z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https: //dev-to-uploads.s3.amazonaws.com/uploads/articles/3snsztp8x4gcgco7t7ec.png)
...numVisits
变量在“test-webapp”Node.js 服务器(在“Docker Container 1”中运行)中更新,因此在浏览器中更新(因为为了调用**Node.js server,需要刷新“localhost:80”的连接,访问量增加1**,因此342 + 1 u003d 343. 这表明我们在“Docker Container 1”和“Docker Container 2”中运行的进程之间有双向进程间通信。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--FNra9-r7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880 /https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oapryvdzrnppyzztuxfn.png)
(1.5) Docker Compose 提供的一个有用功能是能够在“docker-compose.yml”中指定“restart”选项。
这将允许我们在连接到“Docker Container 1”的外壳时,“杀死” Node.js server 进程,但 Node.js server 进程将被 ** 自动重启Docker Compose**“restart”指令。
C:\test-docker\test-redis>docker ps
CONTAINER ID IMAGE PORTS NAMES
c675ff6c0464 test-redis_nginx 0.0.0.0:80->80/tcp test-redis_nginx_1
3137d1468ec7 test-redis_test-webapp-2 0.0.0.0:3009->5000/tcp test-redis_test-webapp-2_1
57d399295421 redis:alpine test-redis_test-redis_1
b30635f44151 test-redis_test-webapp-1 0.0.0.0:3008->5000/tcp test-redis_test-webapp-1_1
进入全屏模式 退出全屏模式
连接到 ID 为 928b8b07415d 的 Docker 容器并调用 shell (sh)。
C:\test-redis\test-webapp>docker exec -it 928b8b07415d sh
在容器内,在 shell 提示符下,显示所有使用ps -al
的进程 ID。
/usr/src/app # ps -al
PID USER TIME COMMAND
1 root 0:00 npm start
19 root 0:00 node server.js
30 root 0:00 sh
36 root 0:00 ps -al
进入全屏模式 退出全屏模式
通过发出kill -9 <process-id>
命令继续“杀死”“node server.js”进程:
/usr/src/app # kill -9 19
在运行 Docker Compose 的命令行窗口中,我们可以看到“test-webapp”如何接收“终止信号”(SIGKILL
),并以代码“1”退出,然后自动重新启动。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--PyOXsmV8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7m25v5kmnu9zlbobgqgf.PNG)
结论
在本示例的 Stage 1 中,我们展示了 Docker Compose 如何让我们轻松建立相互通信的独立环境,以及 Docker Compose 的自动容错(故障时重启)能力。
**让我们继续第 2 阶段:
(2) 在容器化 Nginx 反向代理的帮助下对 Node.js 服务器进行负载平衡**
“图 2.a - 阶段 2 组件示意图”中的图表描述了一种类似于前面“图 1.a - 组件示意图”中描述的架构,但具有下面描述的变化。
[组件示意图](https://res.cloudinary.com/practicaldev/image/fetch/s--dW6tGtNm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto %2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/02s6wkco89eaeu00ezxu.png)
图 2.a – 第 2 阶段的组件示意图
在“图 2.a – 第 2 阶段组件示意图”中,我们有以下组件:
(2.1.1, 2.1.2) “Docker Container 1”和“Docker Container 2”——两个相同的容器,其源代码位于目录“test-webapp-1”中和“test-webapp-2”(如下面的“图 2.b – 第 2 阶段的文件结构”所示),它们几乎是应用程序“test-webapp”的相同副本,之前在第 1 阶段中进行了描述。这次我们使用两个 Node.js 服务器 进程,它们将从本地主机为客户端浏览器提供服务,从 Stage 1 扩展和负载平衡原始单服务器配置。这两个容器分别由它们各自的“Dockerfile”(2.1.1.1)和(2.1.1.2)定义和控制。每个 Node.js 服务器“Docker Container 1”和“Docker Container 2”都会计算来自本地主机浏览器的访问次数。然后它将访问次数保存到 Redis 数据库中,它还向浏览器返回访问次数以及使用哪个特定的 Node.js 服务器 为来自浏览器,通过向浏览器发送以下类型的消息:
“test-webapp-1:访问次数为:”,或
“test-webapp-2:访问次数为:”
...从而突出了这个阶段的负载均衡性质。
(2.1.3) “Docker Container 3” – 运行 Redis 数据库实例的容器,与 Stage 1 中描述的相同,存储由localhost 机器浏览器到“localhost:80”。访问次数由Node.js服务器进程“test-webapp-1”和“test-webapp-2”存储在**Redis变量numVisits
中,其值由每个Node传输每次在本地主机浏览器上刷新时,将 .js 服务器** 复制到 Redis 数据库。
(2.2) “docker-compose-nginx.yml” – Docker Compose 主配置文件定义和控制: (I) “Docker Container 1” 运行 Node.js服务器“test-webapp-1”,(II)“Docker Container 2”运行Node.js服务器“test-webapp-2”,(III)“Docker Container 3”运行**Redis** , 和 (IV) 运行 Nginx 的“Docker Container 4”。
(2.3) “Docker Container 4” running “Nginx” – 这是在 Stage 2 中引入的附加容器,由其自己的 Dockerfile (2.3.1) 定义和控制,运行一个“nginx”实例,并充当反向代理,路由来自本地主机浏览器的 HTTP GET 请求。 “Docker Container 4”中的“Nginx”进程以循环方式路由来自本地主机浏览器“localhost:80”的_HTTP GET_请求((2.3 .3) 和 (2.3.4)),发送到“Docker Container 1”中的“test-webapp-1”Node.js 服务器或“test-webapp-2” Node.js 服务器在“Docker Container 2”中。 “Docker Container 4”中的“nginx”进程由**Nginx复制的_Nginx_config文件“nginx.conf”定义和控制容器的_Dockerfile_到“Docker Container 4”环境文件“/etc/nginx/conf.d./default.conf”(这是标准的Nginx设置)。 “nginx”实例分配来自本地主机浏览器的传入流量,从而扩展和负载平衡阶段 1** 中呈现的单容器 Web/应用服务器架构。
现在让我们描述一下这个场景的步骤和流程。下面描述中的编号方案(即(2.1)、(2.2)、(2.3)等)与“图 2.a – 第 2 阶段组件示意图”中的编号相匹配。
(2.1) 文件结构:
[的文件结构](https://res.cloudinary.com/practicaldev/image/fetch/s--5AOs-x2p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto% 2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8v7qndptehh545sknesh.png)
“图 2.b - 阶段 2 的文件结构”中描述的文件结构与前面“图 1.b - 阶段 1 的文件结构”中描述的文件结构几乎相同,具有以下内容变化:
(2.1.1, 2.1.2) Stage 1 目录“test-webapp”中的文件被复制到目录“test-webapp-1”和“test-webapp- 2”。
(2.2) 转到上面的一个目录,我们看到“docker-compose-nginx.yml”配置文件,它组织了容器化并设置了所有组件的架构:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--A8ZD3MU4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23ztsgagx0fthaktnn3n.png)
清除所有图像和容器:
与 Stage 1 一样,我们运行命令docker system prune -a
以清除所有 Docker 映像和容器,并从一个干净的状态开始。
(2.3) 使用 Docker Compose 构建并运行“test-webapp-1”、“test-webapp-2”、“redis”和“nginx”镜像
使用 Docker Compose 构建:
C:\test-docker\test-redis>docker-compose -f docker-compose-nginx.yml build
使用 Docker Compose 运行:
C:\test-docker\test-redis>docker-compose -f docker-compose-nginx.yml up
在我们发出docker-compose -f docker-compose-nginx.yml up
命令的命令行窗口中,Docker Compose 回复:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--Ufv9ddaS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/xi20q9vn6kp7m67olqst.PNG)
...显示所有4个Docker容器都已成功启动并启动并运行:“test-redis_1”对应于“Docker Container 3”中运行的**Redis进程,“test-webapp-2_1 ”对应“Docker Container 2”中运行的Node.js server进程,“test-webapp-1_1”对应“Docker Container 1”中运行的Node.js server进程,而“nginx_1”对应于“Docker Container 4”中运行的Nginx**服务器。
查看“test-webapp-1”、“test-webapp-2”、“redis”和“nginx”运行容器:
C:\test-docker\test-redis>docker ps
CONTAINER ID IMAGE PORTS NAMES c675ff6c0464 test-redis_nginx 0.0.0.0:80->80/tcp test-redis_nginx_1
3137d1468ec7 test-redis_test-webapp-2 0.0.0.0:3009->5000/tcp
test-redis_test-webapp-2_1
57d399295421 redis:alpine test-redis_test-redis_1
b30635f44151 test-redis_test-webapp-1 0.0.0.0:3008->5000/tcp test-redis_test-webapp-1_1
进入全屏模式 退出全屏模式
上面的四个容器匹配“图 2.a – Stage 2 组件示意图”中的容器“Docker Container 1”到“Docker Container 4”
以上。请注意“CONTAINER ID”列,我们将在下面使用其值来潜在地对每个正在运行的容器执行操作。
让我们在主机上运行浏览器的前两个实例,并将它们指向 URL “localhost:80”:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--OpVMnmS4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto %2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/42dwmonlt8jn07ifovp0.png)
[](https://res.cloudinary.com/practicaldev/image/fetch/s--sOWO-DD__-/c_limit%2Cf_auto%2Cfl_progressive %2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d06t4jutlz9c47pd5po8.png)
请注意,由于 Nginx 反向代理所采用的 round-robin 路由机制,“GET localhost:80”请求一次路由到“test-webapp-1”Node.js服务器,一次到“test-webapp-2” Node.js 服务器,实现我们打算演示的扩展和负载平衡。
让我们连接到运行 Redis 的容器,连接到它的 sh (shell) 环境:
C:\test-docker\test-redis>docker exec -it 57d399295421 sh
然后,在容器内部,让我们使用“redis-cli”连接到 Redis 本身:
/data #
/data # redis-cli
127.0.0.1:6379>
127.0.0.1:6379> get numVisits
"8"
127.0.0.1:6379>
进入全屏模式 退出全屏模式
请注意 Redis 中的get numVisits
命令如何返回“访问次数”的预期值,该值从运行 Node.js 应用服务器的容器传送到“redis”容器.
结论
在本示例的 Stage 2 中,我们展示了 Docker Compose 如何使我们能够轻松地建立多个容器及其相互通信的独立环境,以及如何使用 Nginx 实现扩展和负载平衡。
源代码:
https://github.com/marcelkatz/test-docker-nodejs-redis-nginx
更多推荐
所有评论(0)