面向物联网的NGINX Plus:MQTT负载均衡

NGINX | 2017年3月23日

在互联网高流量网站市场中,NGINX和NGINX Plus占据主导地位。事实上,在全球访问量较高的100万个网站中,使用NGINX的网站数量超过了其他任何Web服务器。NGINX能够在单台服务器上处理超过100万个并发连接,因此被Airbnb、Netflix和Uber等“超大规模”网站和应用采用。

虽然NGINX Plus最广为人知的用途是充当Web服务器、HTTP反向代理和负载均衡器,但它也是一款功能完备的应用交付控制器(ADC),支持TCP和UDP应用。它的事件驱动架构以及使其成功应用于HTTP用例的其他所有特性,同样适用于物联网(IoT)。

本文将介绍如何使用NGINX Plus对MQTT流量进行负载均衡。MQTT最初发布于1999年,用于与偏远油田通信。2013年,MQTT经过更新以适用于IoT用例,此后成为许多IoT部署的首选协议。包含数百万台设备的生产环境IoT部署要求负载均衡器具备高性能和高级功能。本系列分为两篇博文,将探讨以下高级用例。

测试环境

为探索NGINX Plus的功能,我们将使用一个简单的测试环境。该环境包含一个MQTT代理集群,可体现IoT环境的关键组件。此环境中的MQTT代理是运行在Docker容器内的HiveMQ实例。

用于MQTT负载均衡和会话持久性的测试环境

NGINX Plus充当MQTT代理的反向代理和负载均衡器,侦听默认MQTT端口1883。这样可为客户端提供简单且一致的接口,同时后端MQTT节点可以横向扩展(甚至下线),而不会对客户端造成任何影响。我们使用Mosquitto的命令行工具作为客户端,用于模拟测试环境中的IoT设备。

本文和第 2 部分中的所有用例均使用此测试环境,所有配置均可直接应用于图中所示的架构。有关构建测试环境的完整说明,请参阅“附录 1”。

通过主动健康检查对MQTT进行负载均衡,实现高可用性

A primary function of a 负载均衡器的一项主要功能是为应用提供高可用性,使后端服务器的添加、移除或离线不会影响客户端。要可靠地实现这一点,必须通过健康检查主动探测每台后端服务器是否可用。借助主动健康检查,NGINX Plus可以在实际客户端请求到达故障服务器之前,将其从负载均衡组中移除。

运行状况检查是否有效,取决于它能否准确模拟真实的应用流量并分析响应。ping等简单的服务器存活检查无法确保后端服务正在运行。TCP端口开放检查也无法确保应用本身运行正常。此处,我们为测试环境配置基本负载均衡,并通过运行状况检查确保每台后端服务器都能接受新的MQTT连接。

我们将修改两个配置文件。

在主配置文件nginx.conf中,我们添加以下stream块和include指令,使NGINX Plus从stream_conf.d子目录中的一个或多个文件读取TCP负载均衡配置。该子目录与nginx.conf位于同一目录。这样就无需将实际配置直接写入nginx.conf。

正在加载代码示例…

然后,在与nginx.conf相同的目录中创建stream_conf.d目录,用于存放TCP和UDP配置文件。请注意,不要使用已有的conf.d目录,因为默认情况下,该目录专用于http配置上下文,在其中添加stream配置将会失败。

正在加载代码示例…

在stream_mqtt_healthcheck.conf中,我们首先定义MQTT流量的访问日志格式(第1–2行)。该格式特意采用与HTTP通用日志格式相似的设计,以便将生成的日志导入日志分析工具。

接下来,我们定义名为“upstream”的hive_mq组(第4–9行),其中包含3台MQTT服务器。在测试环境中,每台服务器都可通过localhost上的唯一端口号访问。zone指令定义了一块由所有NGINX Plus工作进程共享的内存,用于维护负载均衡状态和运行状况信息。

The match块(第 11–15 行)定义了用于测试 MQTT 服务器可用性的健康检查。send指令是一个完整 MQTT CONNECT数据包的十六进制表示形式,其中客户端标识符(ClientId)为nginx health check。每次触发健康检查时,都会将此数据包发送到上游组中定义的每台服务器。相应的expect指令描述了服务器必须返回的响应,NGINX Plus 据此将其视为健康状态。此处,4 字节十六进制字符串20 02 00 00是一个完整的 MQTT CONNACK数据包。收到此数据包表明 MQTT 服务器能够接受新的客户端连接。

The 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.log192.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模块用例》。]

本节代码已按如下方式更新,以反映自本博客最初发布以来NGINX JavaScript实现的变化:

  • 使用为Stream模块重构的会话(s)对象,该对象于NGINX JavaScript 0.2.4中引入。
  • 使用js_import指令。该指令在js_include指令的基础上取而代之,适用于NGINX Plus R23及更高版本。有关详细信息,请参阅NGINX JavaScript模块的参考文档,其中的“配置示例”部分展示了NGINX配置文件和JavaScript文件的正确语法。

轮询负载均衡是一种在一组服务器之间分配客户端连接的有效机制。但是,由于多种原因,它并不适合MQTT连接。

MQTT服务器通常要求客户端与服务器之间保持长连接,服务器上可能会积累大量会话状态。遗憾的是,由于IoT设备及其所用IP网络的特性,连接可能会中断,导致部分客户端频繁重新连接。NGINX Plus可以使用其哈希负载均衡算法,根据客户端IP地址选择MQTT服务器。只需在upstream块中添加hash $remote_addr;,即可启用会话持久性,这样,每当来自特定客户端IP地址的新连接接入时,都会选择同一台MQTT服务器。

但我们不能指望IoT设备每次都从同一IP地址重新连接,尤其是在使用蜂窝网络(例如GSM或LTE)时。为确保同一客户端重新连接到同一MQTT服务器,必须使用MQTT客户端标识符作为哈希算法的键。

基于MQTT ClientId的会话持久性

MQTT ClientId是初始CONNECT数据包的必需元素,这意味着在将该数据包代理到上游服务器之前,NGINX Plus即可获取此元素。可以使用NGINX JavaScript解析CONNECT数据包,并将ClientId提取为变量,随后由hash指令使用该变量来实现MQTT专用的会话持久性。

NGINX JavaScript 是“NGINX 原生”编程式配置语言。它是专为 NGINX 和 NGINX Plus 打造的独特 JavaScript 实现,专门用于服务器端用例和逐请求处理。它具备以下三个关键特性,适合用于实现 MQTT 会话持久性:

  • NGINX JavaScript与NGINX Plus处理阶段紧密集成,因此可以在将客户端数据包负载均衡到上游组之前对其进行检查。
  • NGINX JavaScript使用内置JavaScript方法处理字符串和数值,从而能够高效解析第4层协议。实际解析MQTT CONNECT 数据包所需的代码不到20行。
  • NGINX JavaScript可以创建可供NGINX Plus配置使用的变量。

有关启用NGINX JavaScript的说明,请参阅“附录 2”。

用于会话持久性的NGINX Plus配置

此用例的NGINX Plus配置仍然相对简单。以下配置修改自使用主动健康检查进行负载均衡中的示例。为简洁起见,其中省略了健康检查。

正在加载代码示例…

首先,使用js_import指令指定NGINX JavaScript代码的位置。js_set指令指示NGINX Plus在需要计算setClientId函数时调用$mqtt_client_id变量。我们在第5行将此变量追加到mqtt的日志格式中,以便在访问日志中记录更多详细信息。

我们在第12行使用hash指令启用会话持久性,并将$mqtt_client_id指定为键。请注意,我们使用了consistent参数。这样,如果某台上游服务器发生故障,其承担的流量将均匀分配到其余服务器,同时不会影响已在这些服务器上建立的会话。有关一致性哈希的更多信息,请参阅我们的博客文章对Web缓存进行分片,其中介绍的原理和优势同样适用于此处。

The js_preread指令(第18行)指定在请求处理的预读阶段执行的NGINX JavaScript函数。每个数据包(双向)都会触发预读阶段,该阶段发生在代理之前,以便在$mqtt_client_id块中需要upstream的值时可以使用该值。

用于会话持久性的NGINX JavaScript代码

用于提取MQTT ClientId的JavaScript定义在mqtt.js文件中,该文件由NGINX Plus配置文件(js_import指令加载。stream_mqtt_session_persistence.conf)。

正在加载代码示例…

主函数getClientId()在第 4 行声明。该函数接收名为s的对象,此对象表示当前TCP会话。会话对象具有许多属性,此函数使用了其中的多个属性。

第5至9行确保当前数据包是从客户端收到的第一个数据包。后续客户端消息和服务器响应将被忽略,因此连接建立后不会给流量增加额外开销。

正在加载代码示例…

第10至24行检查MQTT标头,以确认数据包为CONNECT类型,并确定MQTT有效负载的起始位置。

正在加载代码示例…

第27至32行从有效负载中提取ClientId,并将其值存储在JavaScript全局变量“client_id_str”中。随后,通过“setClientId”函数将此变量导出到NGINX配置中(第43至45行)。

验证会话持久性

现在,我们可以再次使用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 始终连接到 node1(端口 18831),ClientId bar 始终连接到 node2(端口 18832),ClientId baz 始终连接到 node3(端口 18833)。

$ tail /var/log/nginx/mqtt_access.log192.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如何使用主动健康检查来提高IoT应用的可用性和可靠性,以及NGINX JavaScript如何通过为TCP流量提供会话持久性等第7层负载均衡功能来扩展NGINX Plus。在第二篇中,我们将探讨NGINX Plus如何通过卸载TLS终止和对客户端进行身份验证,帮助增强IoT应用的安全性。

无论与NGINX JavaScript结合使用还是单独使用,NGINX Plus凭借其出色的性能和效率,都非常适合作为IoT基础设施的软件负载均衡器。

要试用NGINX JavaScript与NGINX Plus,请开始30天免费试用,或联系我们,讨论您的用例。

附录

创建测试环境

我们将测试环境安装在虚拟机上,以确保环境相互隔离且可重复创建。不过,也可以将其安装在物理“裸机”服务器上。

安装NGINX Plus

请参阅《NGINX Plus 管理指南》中的说明。

安装HiveMQ

可以使用任何MQTT服务器,但此测试环境基于HiveMQ(在此下载)。在本示例中,我们在一台主机上安装HiveMQ,并为每个节点使用一个Docker容器。以下说明改编自《使用Docker部署HiveMQ》。

  1. 在与hivemq.zip相同的目录中为HiveMQ创建Dockerfile。

    正在加载代码示例…

  2. 在包含hivemq.zip和Dockerfile的目录中创建Docker镜像。
$ docker build -t hivemq:latest .
  1. 创建3个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
  1. 检查所有3个HiveMQ节点是否都在运行。(为便于阅读,以下示例输出省略了“COMMAND”、“CREATED”和“STATUS”列。)
$ docker psCONTAINER 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

[ngx_snippet name=’njs-enable-instructions’]


分享

关于作者

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。