当谈及互联网上最繁忙的网站时,NGINX 和 NGINX Plus 在市场中占据主导地位。事实上,在全球最繁忙的一百万个网站中,NGINX 支持的网站数量超过了其他任何 Web 服务器。它能够在单台服务器上处理超过 100 万个并发连接,因此被爱彼迎(Airbnb)、网飞(Netflix)及优步(Uber)等“超大规模”网站和应用争相采用。
尽管 NGINX Plus 通常被称为 Web 服务器、HTTP 反向代理和负载均衡器,但它也是一款功能齐全的应用交付控制器(ADC),可支持 TCP 和 UDP 应用。其事件驱动型架构及适用于 HTTP 用例的其他所有属性同样适用于物联网(IoT)。
本文将展示如何使用 NGINX Plus 对 MQTT 流量进行负载均衡。MQTT 最初发布于 1999 年,用于偏远油田的通信,后又于 2013 年针对物联网用例进行了更新,自此成为许多物联网部署的首选协议。连接数百万设备的生产物联网部署需要负载均衡器具备高性能和高级功能,在本系列博文(共两篇)中,我们将探讨以下高级用例。
- MQTT 流量负载均衡(本文)
- 对 MQTT 流量进行加密和身份验证
测试环境
为了探讨 NGINX Plus 的功能,我们将使用一个简单的测试环境,代表具有 MQTT Broker 集群的物联网环境的关键组件。该环境中的 MQTT Broker 是在 Docker 容器内运行的 HiveMQ 实例。.
NGINX Plus 充当 MQTT Broker 的反向代理和负载均衡器,监听 MQTT 默认端口 1883。这为客户端提供了一个简单且一致的接口,同时后端 MQTT 节点可以向外扩展(甚至脱机),且不会对客户端产生任何影响。我们将 Mosquitto 命令行工具用作客户端,代表测试环境中的物联网设备。
本文和第 2 篇文章中的所有用例均使用该测试环境,所有配置都直接应用于图中所示的架构。有关构建该测试环境的完整说明,请参阅附录 1。
借助主动健康检查实现 MQTT 负载均衡,确保高可用性
负载均衡器的一个主要功能是为应用提供高可用性,这样后端服务器的添加、移除或脱机都不会影响客户端。为此,必须执行健康检查,以主动探测每台后端服务器的可用性。借助主动健康检查,NGINX Plus 能够在实际客户端请求到达故障服务器之前将其从负载均衡组中移除。
健康检查的有效性取决于它模拟实际应用流量和分析响应的准确性。简单的服务器存活性检查(如 ping)无法确保后端服务正在运行。TCP 端口打开检查也不能确保应用本身处于健康状态。下面我们为测试环境配置基本负载均衡,并进行健康检查,以确保每台后端服务器都能接受新的 MQTT 连接。
我们将修改两个配置文件。
在 nginx.conf 主文件中,我们添加以下 stream
块和 include
指令,让 NGINX Plus 从 stream_conf.d 子目录(与 nginx.conf 位于同一目录)下的一个或多个文件中读取 TCP 负载均衡配置,而非将实际配置文件添加到 nginx.conf 中。
然后,在与 nginx.conf 相同的目录下创建 stream_conf.d 目录,以添加 TCP 和 UDP 配置文件。请注意,我们不使用预先存在的 conf.d 目录,因为默认情况下,该目录留给 http
配置上下文,因此无法向其添加 stream
配置文件。
在 stream_mqtt_healthcheck.conf 中,我们首先定义 MQTT 流量的访问日志格式(第 1-2 行)。该格式特意类似于 HTTP 通用日志格式,以便生成的日志能够导入到日志分析工具中。
接下来,我们定义名为 hive_mq 的上游组(第 4-9 行),其中包含三台 MQTT 服务器。在我们的测试环境中,它们都支持在本地主机上通过唯一的端口号进行访问。zone
指令定义了所有 NGINX Plus worker 进程共享的内存容量,以维护负载均衡状态和健康信息。
match
块(第 11-15 行)定义了用于测试 MQTT 服务器可用性的健康检查。send
指令是完整的 MQTT CONNECT
数据包的十六进制表示形式,其中包括 nginx
health
check
的客户端标识符(ClientId)。每当健康检查触发时,该指令便会被发送到上游组中定义的每台服务器。相应的 expect
指令描述了服务器必须返回的响应,以确保 NGINX Plus 被视为处于健康状态。此处,4 字节十六进制字符串 20
02
00
00
是一个完整的 MQTT CONNACK
数据包。收到该数据包表明 MQTT 服务器能够接收新的客户端连接。
server
块(第 17–25 行)配置了 NGINX Plus 处理客户端的方式。NGINX Plus 监听 MQTT 默认端口 1883,并将所有流量转发到 hive_mq 上游组(第 19 行)。health_check
指令指定对上游组执行健康检查(默认频率为 5 秒),并使用 mqtt_conn match
块定义的检查。
验证配置
为了测试这一基本配置是否有效,我们可以使用 Mosquitto 客户端将一些测试数据发布到测试环境中。
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001"
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT
$ tail --lines=1 /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Mar/2017:11:41:56 +0000] TCP 200 23 4 127.0.0.1:18831
访问日志中的一行显示,NGINX Plus 共接收了 23 个字节,其中 4 个字节被发送到客户端(CONNACK
数据包)。我们还可以看到,MQTT node1 被选中(端口 18831)。如访问日志中的以下几行所示,当我们重复进行测试时,默认的轮询负载均衡算法依次选择 node1、 node2 及 node3。
$ tail --lines=4 /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Mar/2017:11:41:56 +0000] TCP 200 23 4 127.0.0.1:18831
192.168.91.1 [23/Mar/2017:11:42:26 +0000] TCP 200 23 4 127.0.0.1:18832
192.168.91.1 [23/Mar/2017:11:42:27 +0000] TCP 200 23 4 127.0.0.1:18833
192.168.91.1 [23/Mar/2017:11:42:28 +0000] TCP 200 23 4 127.0.0.1:18831
使用 NGINX JavaScript 模块实现 MQTT 负载均衡及会话保持
[编者按 – NGINX JavaScript 模块的用例有很多,以下用例只是其中之一。查看完整列表,请参阅《借助NGINX JavaScript 模块的用例,充分利用JavaScript的强大功能和便利性以快速处理每个请求》。
针对 NGINX JavaScript 的实现自该博文最初发布以来的变化,本节中的代码更新如下:
- 使用 NGINX JavaScript 0.2.4 中引入的 Stream 模块的重构会话(
s
)对象。 - 在 NGINX Plus R23 及更高版本中,使用
js_import
指令取代js_include
指令。如欲了解更多信息,请参阅 NGINX JavaScript 模块的参考文档 – “示例配置”一节显示了 NGINX 配置和 JavaScript 文件的正确语法。
]
轮询负载均衡是在一组服务器之间分配客户端连接的一种有效机制。但是,多种原因导致它不适合 MQTT 连接。
MQTT 服务器通常期望在客户端和服务器之间建立长连接,并会在服务器上积累大量会话状态。遗憾的是,物联网设备的性质及其使用的 IP 网络意味着连接会中断,进而迫使一些客户端频繁地重新连接。NGINX Plus 可以使用其哈希负载均衡算法,根据客户端 IP 地址选择 MQTT 服务器。只需将 hash
$remote_addr;
添加到 upstream 块即可实现会话保持,这样每次从给定客户端 IP 地址进入新连接时,便会选择同一 MQTT 服务器。
但我们不能依靠物联网设备从同一 IP 地址重新连接,尤其是这些设备在使用蜂窝网络(如 GSM 或 LTE)的情况下。为了确保同一客户端重新连接到同一 MQTT 服务器,我们必须使用 MQTT 客户端标识符作为哈希算法的密钥。
基于 MQTT ClientId 的会话保持
MQTT ClientId 是初始 CONNECT
数据包的必要元素,这意味着在数据包被代理到上游服务器之前,便可供 NGINX Plus 使用。我们能够使用 NGINX JavaScript 来解析 CONNECT
数据包,并将 ClientId 提取为变量,然后哈希指令就可以使用该变量来实现 MQTT 特定的会话保持。
NGINX JavaScript 是“ NGINX 原生的”编程配置语言。它是面向 NGINX 和 NGINX Plus 的一种独特的 JavaScript 实现,专为服务器端用例和按请求处理进程而设计。它具有以下三个主要特征,因此非常适合实现 MQTT 会话保持:
- NGINX JavaScript 与 NGINX Plus 处理阶段紧密集成,因此我们可以在客户端数据包被负载均衡到上游组之前对其进行检查。
- NGINX JavaScript 使用内置 JavaScript 方法进行字符串和数字处理,可高效解析四层协议。MQTT
CONNECT
数据包的实际解析只需不到 20 行代码。 - NGINX JavaScript 能够创建可用于 NGINX Plus 配置的变量。
有关启用 NGINX JavaScript 的说明,请参阅附录 2。
会话保持的 NGINX Plus 配置
该用例的 NGINX Plus 配置仍然相对简单。以下配置是“通过主动健康检查实现负载均衡”中示例的修改版本,为简洁起见,删除了健康检查。
首先,我们借助 js_import
指令来指定 NGINX JavaScript 代码的位置。js_set
指令能够指示 NGINX Plus 在需要评估 $mqtt_client_id
变量时调用 setClientId
函数。我们可以通过将此变量附加到第 5 行的 mqtt 日志格式中,实现在访问日志里添加更多细节。
我们在第 12 行通过指定 $mqtt_client_id
为密钥的哈希指令启用会话保持。需要注意的是,我们使用一致的参数,以便在上游服务器发生故障时,将其流量平均分发给其余服务器,而不影响这些服务器上已建立的会话。我们在有关 Web 缓存分片的博文中进一步探讨了一致的哈希,其原理和优势同样适用于此。
js_preread
指令(第 18 行)指定在请求处理的预读阶段执行的 NGINX JavaScript 函数。每个数据包都会触发预读阶段(双向),该阶段发生在代理之前,这样 $mqtt_client_id
的值在 upstream
块需要时便可使用。
会话保持的 NGINX JavaScript 代码
我们在 mqtt.js 文件中定义了用于提取 MQTT ClientId 的 JavaScript,该文件通过 NGINX Plus 配置文件(stream_mqtt_session_persistence.conf)中的 js_import
指令进行加载。
主函数 getClientId()
在第 4 行声明。它传递的对象名为 s
,代表当前 TCP 会话。会话对象具有许多属性,其中几个属性会在该函数中用到。
第 5-9 行确保当前数据包是从客户端接收到的第一个数据包。后续的客户端消息和服务器响应都将被忽略,因此在建立连接后,不会增加流量负载。
第 10-24 行检查 MQTT 请求头,以确保该数据包是 CONNECT 类型,并确定 MQTT 有效载荷的起始位置。
第 27-32 行从有效载荷中提取 ClientId,并将该值存储到 JavaScript 全局变量 client_id_str
中。然后,使用 setClientId
函数(第 43-45 行)将该变量导出到 NGINX 配置。
验证会话保持
现在,我们可以再次使用 Mosquitto 客户端来测试会话保持,方法是发送一系列具有三个不同 ClientId 值(-i
选项)的 MQTT 发布请求。.
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "bar"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "baz"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "bar"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "bar"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "baz"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "baz"
访问日志显示,ClientId foo、ClientId bar 及 ClientId baz 分别始终连接到 node1(端口 18831)、node2(端口 18832)及 node3(端口 18833)。
$ tail /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Mar/2017:12:24:24 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:28 +0000] TCP 200 23 4 127.0.0.1:18832 bar
192.168.91.1 [23/Mar/2017:12:24:32 +0000] TCP 200 23 4 127.0.0.1:18833 baz
192.168.91.1 [23/Mar/2017:12:24:35 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:37 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:38 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:42 +0000] TCP 200 23 4 127.0.0.1:18832 bar
192.168.91.1 [23/Mar/2017:12:24:44 +0000] TCP 200 23 4 127.0.0.1:18832 bar
192.168.91.1 [23/Mar/2017:12:24:47 +0000] TCP 200 23 4 127.0.0.1:18833 baz
192.168.91.1 [23/Mar/2017:12:24:48 +0000] TCP 200 23 4 127.0.0.1:18833 baz
这里需要注意的是,无论我们是否使用会话保持或其他任何负载均衡算法,MQTT ClientId 都会出现在访问日志行中。
结语
在本系列博文(共两篇)的第一篇中,我们介绍了 NGINX Plus 如何使用主动健康检查来提高物联网应用的可用性和可靠性,以及 NGINX JavaScript 如何通过为 TCP 流量提供会话保持等七层负载均衡功能来扩展 NGINX Plus。在第二篇博文中,我们将探讨 NGINX Plus 如何通过卸载 TLS 和对客户端进行身份验证来提高物联网应用的安全性。
无论是结合使用 NGINX JavaScript 还是单独使用,NGINX Plus 固有的高性能和高效率使其成为物联网基础架构的理想软件负载均衡器。
如欲试用 NGINX JavaScript 和 NGINX Plus,请立即下载 30 天免费试用版,或与我们联系以讨论您的用例。
附录
创建测试环境
我们将测试环境安装在一台虚拟机上,方便隔离和重复使用。当然如果有条件也可以安装在物理服务器上。
安装 NGINX Plus
请参阅《NGINX Plus 管理指南》中的说明。
安装 HiveMQ
可以使用任何 MQTT 服务器,但本测试环境基于 HiveMQ(点击此处下载)。在此示例中,我们在单个主机上安装 HiveMQ,使用 Docker 容器配置每个节点。以下说明改编自《使用 Docker 部署 HiveMQ》。
-
在与 hivemq.zip 相同的目录下,为 HiveMQ 创建一个 Dockerfile。
-
在包含 hivemq.zip 和 Dockerfile 的目录下,创建 Docker 镜像。
$ docker build -t hivemq:latest .
-
创建三个 HiveMQ 节点,每个节点暴露在不同的端口上。
$ docker run -p 18831:1883 -d --name node1 hivemq:latest ff2c012c595a $ docker run -p 18832:1883 -d --name node2 hivemq:latest 47992b1f4910 $ docker run -p 18833:1883 -d --name node3 hivemq:latest 17303b900b64
-
检查所有三个 HiveMQ 节点是否都在运行。(在下面的示例输出结果中,为了便于阅读,省略了
COMMAND
、CREATED
和STATUS
列。)$ docker ps CONTAINER ID IMAGE ... PORTS NAMES 17303b900b64 hivemq:latest ... 0.0.0.0:18833->1883/tcp node3 47992b1f4910 hivemq:latest ... 0.0.0.0:18832->1883/tcp node2 ff2c012c595a hivemq:latest ... 0.0.0.0:18831->1883/tcp node1
安装 Mosquitto
Mosquitto 命令行客户端可从项目网站下载。已安装 Homebrew 的 Mac 用户可以运行以下命令。
$ brew install mosquitto
通过向其中一个 Docker 镜像发送简单的发布消息来测试 Mosquitto 客户端和 HiveMQ 平台。
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001" -p 18831
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT
为 NGINX 开源版和 NGINX Plus 启用 NGINX JavaScript
- 为 NGINX Plus 加载 NGINX JavaScript 模块
- 为 NGINX 开源版加载 NGINX JavaScript 模块
- 将 NGINX JavaScript 编译为 NGINX 开源版的动态模块
为 NGINX Plus 加载 NGINX JavaScript 模块
NGINX JavaScript 可作为免费动态模块供 NGINX Plus 用户使用。有关加载说明,请参阅《NGINX Plus 管理指南》。
为 NGINX 开源版加载 NGINX JavaScript 模块
GINX JavaScript 模块默认包含在 NGINX Docker 官方镜像中。如果您的系统配置为使用 NGINX 开源版的官方预构建包,并且您安装的版本是 1.9.11 或更高版本,那么您可以安装 NGINX JavaScript 作为您平台的预构建包。
-
安装预构建包。
-
Ubuntu 和 Debian 系统:
$ sudo apt-get install nginx-module-njs
-
RedHat、CentOS 和 Oracle Linux 系统:
$ sudo yum install nginx-module-njs
-
-
在 nginx.conf 配置文件的顶层(“main”)上下文(而非
http
或stream
上下文)中添加一个load_module
指令,以启用该模块。本例面向 HTTP 和 TCP/UDP 流量加载 JavaScript 模块。load_module modules/ngx_http_js_module.so; load_module modules/ngx_stream_js_module.so;
-
重新加载 NGINX,以便将 NGINX JavaScript 模块加载到运行实例中。
$ sudo nginx -s reload
将 NGINX JavaScript 编译为 NGINX 开源版的动态模块
如果您更喜欢从源码编译 NGINX 模块:
- 按照说明从开源代码库构建 HTTP 和/或 TCP/UDP NGINX JavaScript 模块。
- 将模块二进制文件(ngx_http_js_module.so、ngx_stream_js_module.so)复制到 NGINX 根目录的 modules 子目录(通常为 /etc/nginx/modules)。
- 执行“为 NGINX 开源版加载 NGINX JavaScript 模块”的第 2 步和第 3 步。