当谈到互联网上最繁忙的网站时,NGINX 和 NGINX Plus 占据着市场主导地位。 事实上, NGINX 为全球最繁忙的 100 万个网站提供支持,其数量比任何其他 Web 服务器都多。 它能够在单个服务器上处理超过 100 万个并发连接,这推动了 Airbnb、Netflix 和 Uber 等“超大规模”网站和应用程序采用它。
尽管 NGINX Plus 通常被称为 Web 服务器、HTTP 反向代理和负载均衡器,但它也是一个功能齐全的应用交付控制器 (ADC),支持 TCP 和 UDP应用。 它的事件驱动架构以及使其在 HTTP 用例中取得成功的所有其他属性同样适用于物联网 (IoT)。
在本文中,我们展示了如何使用 NGINX Plus 来平衡MQTT流量负载。 MQTT 最初于 1999 年发布,用于与偏远油田的通信。 它于 2013 年针对物联网用例进行了更新,并从此成为许多物联网部署的首选协议。 具有数百万台设备的生产物联网部署需要负载均衡器的高性能和高级功能,在这两部分系列博客文章中,我们将讨论以下高级用例。
为了探索 NGINX Plus 的功能,我们将使用一个简单的测试环境,该环境具有一组 MQTT 代理,代表 IoT 环境的关键组件。 此环境中的 MQTT 代理是在 Docker 容器内运行的HiveMQ实例。
NGINX Plus 充当 MQTT 代理的反向代理和负载均衡器,监听默认 MQTT 端口 1883。 这为客户端提供了一个简单且一致的界面,同时后端 MQTT 节点可以扩展(甚至离线)而不会以任何方式影响客户端。 我们使用Mosquitto命令行工具作为客户端,代表测试环境中的物联网设备。
本文和第 2 部分中的所有用例均使用此测试环境,并且所有配置都直接适用于图中所示的架构。 有关构建测试环境的完整说明,请参阅附录 1 。
负载均衡器的主要功能是为应用提供高可用性,以便可以添加、删除或离线后端服务器而不会影响客户端。 可靠地执行此操作的本质是健康检查,即主动探测每个后端服务器的可用性。 通过主动健康检查,NGINX Plus 可以在实际客户端请求到达之前从负载平衡组中删除故障的服务器。
健康检查的实用性取决于它模拟真实应用流量和分析响应的准确程度。 简单的服务器活跃性检查(例如 ping)不能确保后端服务正在运行。 TCP 端口开放检查并不能确保应用本身是健康的。 在这里,我们为测试环境配置基本的负载均衡并进行健康检查,以确保每个后端服务器都能够接受新的 MQTT 连接。
我们正在对两个配置文件进行更改。
在主nginx.conf文件中,我们包含以下流
块和包含
指令,以便 NGINX Plus 从stream_conf.d子目录中的一个或多个文件读取 TCP 负载均衡的配置,该子目录与nginx.conf位于同一目录中。 我们这样做,而不是在nginx.conf中包含实际配置。
然后在与nginx.conf相同的目录中,我们创建目录stream_conf.d来包含我们的 TCP 和 UDP 配置文件。 请注意,我们不使用预先存在的conf.d目录,因为默认情况下它保留用于http
配置上下文,因此在那里添加流
配置将会失败。
在stream_mqtt_healthcheck.conf中,我们首先定义 MQTT 流量的访问日志格式(第 1-2 行)。 这与 HTTP 通用日志格式有意相似,以便可以将生成的日志导入到日志分析工具中。
接下来,我们定义名为hive_mq的上游
组(第 4-9 行),其中包含三个 MQTT 服务器。 在我们的测试环境中,它们每个都可以通过唯一的端口号在本地主机上访问。 zone
指令定义了所有 NGINX Plus 工作进程共享的内存量,以维护负载平衡状态和健康信息。
匹配
块(第 11 至 15 行)定义用于测试 MQTT 服务器可用性的健康检查。 发送
指令是一个完整的 MQTT CONNECT
数据包的十六进制表示,其中客户端标识符(ClientId)为nginx
health
check
。 每当健康检查触发时,都会将其发送到上游组中定义的每个服务器。 相应的expect
指令描述了服务器必须返回的响应,以便 NGINX Plus 认为它是健康的。 这里,4字节十六进制字符串20
02
00
00
是一个完整的MQTT CONNACK
数据包。 收到此数据包表明 MQTT 服务器能够接收新的客户端连接。
服务器
块(第 17-25 行)配置 NGINX Plus 如何处理客户端。 NGINX Plus 监听默认 MQTT 端口 1883,并将所有流量转发到hive_mq上游组(第 19 行)。 health_check
指令指定针对上游组执行健康检查(默认频率为五秒),并使用mqtt_conn匹配
块定义的检查。
为了测试这个基本配置是否正常工作,我们可以使用 Mosquitto 客户端将一些测试数据发布到我们的测试环境。
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001"客户端 thing001 发送 CONNECT 客户端 thing001 接收 CONNACK 客户端 thing001 发送 PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 个字节))客户端 thing001 发送断开连接 $ 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 [2017 年 3 月 23 日:11:42:28 +0000] TCP 200 23 4 127.0.0.1:18831
[编辑器– 以下用例只是 NGINX JavaScript 模块的众多用例之一。 有关完整列表,请参阅NGINX JavaScript 模块的用例。]
本节中的代码更新如下,以反映自博客最初发布以来 NGINX JavaScript 实现的变化:
JavaScript
0.2.4中引入的 Stream 模块的重构会话对象。js_import
指令,它取代了 NGINX Plus R23 及更高版本中的js_include
指令。 有关更多信息,请参阅NGINX JavaScript 模块的参考文档 -示例配置部分显示了 NGINX 配置和 JavaScript 文件的正确语法。循环负载均衡是在一组服务器之间分配客户端连接的有效机制。 但是,有几个原因导致它不适合 MQTT 连接。
MQTT 服务器通常希望客户端和服务器之间建立长期连接,并且可以在服务器上建立大量会话状态。 不幸的是,物联网设备及其使用的 IP 网络的性质意味着连接会中断,迫使一些客户端频繁重新连接。 NGINX Plus 可以使用其哈希负载平衡算法根据客户端 IP 地址选择 MQTT 服务器。 只需将哈希
$remote_addr;
添加到上游块即可启用会话持久性,以便每次从给定客户端 IP 地址进入新连接时,都会选择相同的 MQTT 服务器。
但我们不能依赖物联网设备从同一个 IP 地址重新连接,尤其是在使用蜂窝网络(例如 GSM 或 LTE)的情况下。 为了确保同一个客户端重新连接到同一个 MQTT 服务器,我们必须使用 MQTT 客户端标识符作为散列算法的密钥。
MQTT ClientId 是初始CONNECT
数据包的强制元素,这意味着在数据包代理到上游服务器之前,NGINX Plus 可以使用它。 我们可以使用 NGINX JavaScript 来解析CONNECT
数据包并提取 ClientId 作为变量,然后可以通过hash
指令使用该变量来实现 MQTT 特定的会话持久性。
NGINX JavaScript 是“NGINX 原生”编程配置语言。 它是 NGINX 和 NGINX Plus 的独特 JavaScript 实现,专为服务器端用例和每个请求处理而设计。 它具有三个关键特性,使其适合实现 MQTT 的会话持久性:
CONNECT
数据包的实际解析需要不到 20 行代码。有关启用 NGINX JavaScript 的说明,请参阅附录 2 。
此用例的 NGINX Plus 配置仍然相对简单。 以下配置是“具有主动健康检查的负载平衡”中示例的修改版本,为简洁起见删除了健康检查。
我们首先使用js_import
指令指定 NGINX JavaScript 代码的位置。 js_set
指令告诉 NGINX Plus 在需要评估$mqtt_client_id
变量时调用setClientId
函数。 我们通过将此变量附加到第 5 行的mqtt日志格式来为访问日志添加更多详细信息。
我们在第 12 行使用hash
指令指定$mqtt_client_id
作为键来启用会话持久性。 请注意,我们使用一致
参数,以便如果上游服务器出现故障,其流量份额会均匀分布在剩余的服务器上,而不会影响这些服务器上已建立的会话。 我们在有关分片网络缓存的博客文章中进一步讨论了一致性哈希 - 其原理和好处在这里同样适用。
js_preread
指令(第 18 行)指定在请求处理的预读阶段执行的 NGINX JavaScript 函数。 预读阶段在每个数据包(两个方向)上触发,并在代理之前发生,以便当上游
块需要时$mqtt_client_id
的值可用。
我们在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始终连接到节点 1 (端口 18831),ClientId bar始终连接到节点 2 (端口 18832),ClientId baz始终连接到节点 3 (端口 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 如何通过提供第 7 层负载平衡功能(如 TCP 流量的会话持久性)来扩展 NGINX Plus。 在第二部分中,我们探讨 NGINX Plus 如何通过卸载 TLS 终止和身份验证使您的 IoT应用更加安全。
无论是与 NGINX JavaScript 结合使用还是单独使用,NGINX Plus 固有的高性能和效率使其成为物联网基础设施的理想软件负载均衡器。
要尝试使用 NGINX JavaScript 和 NGINX Plus,请开始30 天免费试用或联系我们讨论您的用例。
附录
我们在虚拟机上安装了测试环境,以便它是隔离的和可重复的。 但是,没有理由不能将其安装在物理的“裸机”服务器上。
请参阅NGINX Plus 管理指南中的说明。
可以使用任何 MQTT 服务器,但此测试环境基于 HiveMQ(在此处下载)。 在此示例中,我们使用每个节点的 Docker 容器在单个主机上安装 HiveMQ。 以下说明改编自使用 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容器ID 镜像... 端口名称 17303b900b64 hivemq:latest ... 0.0.0.0:18833->1883/tcp node3 47992b1f4910 hivemq:最新... 0.0.0.0:18832->1883/tcp node2 ff2c012c595a hivemq:最新... 0.0.0.0:18831->1883/tcp 节点1
Mosquitto 命令行客户端可以从项目网站下载。 安装了Homebrew的 Mac 用户可以运行以下命令。
$ brew 安装 mosquitto
通过向其中一个 Docker 镜像发送简单的发布消息来测试 Mosquitto 客户端和 HiveMQ 安装。
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001" -p 18831客户端 thing001 发送 CONNECT 客户端 thing001 接收 CONNACK 客户端 thing001 发送 PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 个字节))客户端 thing001 发送 DISCONNECT
[ngx_snippet 名称=’njs-enable-instructions’]
“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”