在日常的互联网服务架构中,我们常常听到“负载均衡”这个词。它就像一位经验丰富的交通指挥,将来自五湖四海的网络请求,合理地分配到后方多台服务器上,避免某台服务器“堵车”甚至“瘫痪”,从而确保整个服务稳定、高效地运行。当我们谈到负载均衡时,很多人首先想到的是处理HTTP/HTTPS流量的七层代理,这确实是Nginx的看家本领。但今天,我们要把目光投向一个同样强大却稍显低调的功能——Nginx的四层代理与负载均衡。
你可能用过或听说过MySQL、Redis、SSH、DNS、游戏服务器等应用。它们有一个共同点:通信协议基于TCP或UDP,工作在网络的传输层,也就是OSI模型中的第四层。对于这类非HTTP的应用,七层代理就“看不懂”它们的协议内容了。这时,就需要四层代理登场。它不关心传输的具体数据内容是什么,只负责在TCP/IP层面进行流量转发。Nginx从1.9.0版本开始,通过引入 ngx_stream_core_module 模块,正式支持了四层TCP/UDP代理功能,让我们能用熟悉的Nginx配置语法,轻松为各种TCP/UDP服务穿上负载均衡的“铠甲”。
一、核心概念:四层与七层的本质区别
在深入配置之前,我们有必要先理清四层和七层代理的根本差异,这能帮助你更好地理解应用场景。
四层代理(传输层代理): 它工作在TCP/UDP层。代理服务器看到的数据包,其“有效载荷”部分对代理来说是透明的。代理只认源IP、源端口、目标IP、目标端口这四元组信息。它的工作模式是:客户端与代理服务器建立连接,代理服务器再与后端某台真实服务器建立另一个连接,然后在两个连接之间简单地转发原始数据流。它速度快、效率高,因为无需解析高层协议。
七层代理(应用层代理): 它工作在HTTP/HTTPS等应用层。代理服务器会完全解析HTTP协议,能读取请求头、URL、方法、Cookie等信息。基于这些信息,它可以做出非常精细的路由决策,比如根据URL路径将请求转发到不同的后端集群,或者修改请求/响应头。功能强大,但开销也相对更大。
简单来说,四层是“转发管道”,七层是“智能路由器”。对于MySQL、Redis这类有自己私有协议的服务,我们必须使用四层代理,因为Nginx无法解析它们的协议内容来进行七层路由。
二、配置基石:Stream模块的启用与基础结构
Nginx的四层代理功能由 ngx_stream_core_module 模块提供。在编译Nginx时,默认可能不包含此模块,需要使用 --with-stream 配置参数来启用。对于大多数主流Linux发行版的预装包或Docker镜像,该模块通常已包含。
一个最基础的四层代理配置,其结构如下所示。请注意,它与我们熟悉的 http {} 块是同级的,在 main 全局配置上下文中使用 stream {} 块来定义。
# 技术栈:Nginx
# 文件名:nginx.conf 或 stream.conf (通过include引入)
# events块和http块可能已存在,stream块与它们并列
events {
worker_connections 1024;
}
http {
# 这里是你的七层HTTP/HTTPS配置...
}
# 四层TCP/UDP代理配置块开始
stream {
# 定义一个上游服务器组,名为 backend_mysql_servers
upstream backend_mysql_servers {
# 负载均衡算法,默认为轮询(round-robin)
# 这里指定为最少连接数(least_conn)
least_conn;
# 定义后端服务器,格式:server [地址]:[端口] [参数]
# weight=2 表示权重为2,在轮询或加权算法中生效
# max_fails=3 允许失败次数
# fail_timeout=30s 失败后暂停服务的时间
server 192.168.1.101:3306 weight=2 max_fails=3 fail_timeout=30s;
server 192.168.1.102:3306 weight=1 max_fails=3 fail_timeout=30s;
server 192.168.1.103:3306 backup; # backup参数表示这是一台备份服务器,仅当主服务器都不可用时才启用
}
# 定义一个服务器块,监听特定端口,并将流量代理到上游组
server {
# 监听本机33306端口上的TCP流量(默认就是TCP)
listen 33306;
# 指定代理的上游服务器组名称
proxy_pass backend_mysql_servers;
# 连接代理过程的超时时间设置
proxy_timeout 30s; # 代理与客户端或后端连接两次读/写操作之间的超时
proxy_connect_timeout 5s; # 代理连接后端服务器的超时时间
# 可选:启用TCP长连接优化(对于数据库类连接很重要)
proxy_socket_keepalive on;
}
# 可以定义多个server块,代理不同的服务
server {
listen 6380 udp; # 注意这里的‘udp’关键字,表示处理UDP流量
proxy_pass backend_dns_servers; # 假设有一个定义好的UDP上游组
proxy_timeout 30s;
}
}
上面的配置展示了一个完整的、为MySQL数据库集群提供负载均衡代理的例子。外部应用不再直接连接数据库的3306端口,而是连接Nginx服务器的33306端口。Nginx会根据配置的负载均衡算法(这里是最少连接数least_conn),将连接请求转发到后端的101、102或103服务器上。backup参数定义的备份服务器,在主服务器池都不可达时才会被启用,提高了系统的可用性。
三、进阶配置:负载均衡算法与健康检查
Nginx四层代理支持多种负载均衡算法,让你可以根据业务特点灵活选择。
1. 负载均衡算法:
在 upstream {} 块中,通过指令指定:
round-robin:默认算法。轮询分发连接,每个后端按顺序接收新连接。least_conn:最少连接数。将新连接分发给当前活跃连接数最少的后端服务器。适合处理长连接(如数据库、SSH)且后端服务器性能不均的场景。hash $remote_addr:基于客户端IP的哈希。同一个客户端的连接总是被转发到同一台后端服务器。这对于需要会话保持(虽然四层本身无状态)或连接绑定的场景很有用。random:随机选择。可以指定参数,如random two,表示随机挑选两台后端,然后在这两台中使用least_conn算法。
2. 健康检查:
这是生产环境至关重要的一环。基础配置中的 max_fails 和 fail_timeout 是一种被动的健康检查方式。当Nginx在 fail_timeout 时间内,尝试与某后端建立连接失败次数达到 max_fails,则会将该后端标记为不可用,并在接下来的 fail_timeout 时间内不再向其分发新连接。
然而,对于更可靠的服务发现,我们通常需要主动健康检查。遗憾的是,Nginx官方开源版本的 stream 模块不直接支持类似 http 模块中 health_check 的主动探测指令。实现主动健康检查通常有以下几种关联技术方案:
- 使用Nginx Plus:Nginx商业版本提供了强大的主动健康检查功能。
- 结合第三方服务发现:使用Consul、etcd等,并通过
nginx-upsync-module等第三方模块动态更新Nginx的upstream配置。 - 使用OpenResty:基于Nginx的Lua脚本能力,可以编写自定义的健康检查逻辑。OpenResty可以看作是“增强版的Nginx”,它集成了LuaJIT,允许你在Nginx的各个处理阶段注入Lua代码。
下面是一个使用OpenResty技术栈,通过Lua脚本实现简易TCP端口主动健康检查的示例。这个例子展示了如何动态管理后端服务器列表。
# 技术栈:OpenResty (Nginx + Lua)
# 此配置需要OpenResty环境,并可能需要额外的Lua库支持
stream {
lua_shared_dict health_check 10m; # 定义一个共享内存字典,用于存储健康状态
upstream backend_dynamic {
zone backend_dynamic 64k; # 为upstream分配共享内存,用于动态解析
# 初始后端列表,可以由Lua脚本动态更新
server 192.168.1.201:6379;
server 192.168.1.202:6379;
}
server {
listen 6379;
proxy_pass backend_dynamic;
proxy_timeout 2s;
proxy_connect_timeout 1s;
}
# 一个独立的、不对外服务的server块,用于定时执行健康检查
server {
listen 127.0.0.1:9999; # 监听一个内部管理端口
# 当有请求到达此端口时,执行Lua脚本进行健康检查
content_by_lua_block {
local sock = ngx.socket.tcp()
local timeout = 1000 -- 1秒超时
local healthy_servers = {}
local backend_list = {
"192.168.1.201:6379",
"192.168.1.202:6379",
"192.168.1.203:6379", -- 一个可能新增或故障的服务器
}
local dict = ngx.shared.health_check
for _, addr in ipairs(backend_list) do
sock:settimeout(timeout)
local ip, port = string.match(addr, "([^:]+):(%d+)")
local ok, err = sock:connect(ip, port)
if ok then
sock:close()
ngx.log(ngx.INFO, "Health check passed for: ", addr)
dict:set(addr, "up", 60) -- 标记为‘up’,状态缓存60秒
table.insert(healthy_servers, addr)
else
ngx.log(ngx.WARN, "Health check FAILED for ", addr, ": ", err)
dict:set(addr, "down", 60) -- 标记为‘down’
end
end
-- 这里可以添加逻辑:根据healthy_servers动态更新upstream配置
-- 例如调用OpenResty的`ngx.balancer`模块API,或者更新共享字典供其他逻辑读取
ngx.say("Health check completed. Healthy: ", table.concat(healthy_servers, ", "))
}
}
}
这个示例中,我们创建了一个内部服务(监听9999端口),当访问它时,会触发一个Lua脚本,该脚本依次尝试连接预定义的后端服务器列表中的每一个。根据连接成功与否,将服务器的健康状态记录到共享字典中。在实际生产环境中,你可以通过crontab定时调用curl http://127.0.0.1:9999来触发检查,或者编写更复杂的Lua脚本,利用共享字典的状态来动态决定proxy_pass的目标。
四、实战示例:构建高可用SSH跳板机与内部服务代理
让我们通过两个更贴近实际生产的例子,来巩固对Nginx四层代理的理解。
示例一:SSH负载均衡跳板机 公司有多台开发/测试服务器,希望通过一个统一的入口进行SSH访问,并实现负载分担和故障转移。
# 技术栈:Nginx
stream {
upstream ssh_backends {
least_conn; # SSH是长连接,使用最少连接算法更合理
server 10.0.1.10:22 weight=3; # 性能较好的服务器,权重高
server 10.0.1.11:22 weight=2;
server 10.0.1.12:22 weight=1;
server 10.0.1.13:22 backup; # 备用机
}
server {
listen 2222; # 对外暴露的SSH端口,非标准的22端口可增强安全性
proxy_pass ssh_backends;
proxy_timeout 1h; # SSH连接可能持续很久,超时设长
proxy_connect_timeout 10s;
# 注意:SSH协议有自己的一套密钥交换和认证,四层代理只负责透明传输TCP包。
}
}
使用方式:开发者使用 ssh -p 2222 user@nginx-proxy-ip 即可连接。Nginx会自动将连接导向当前连接数最少的一台后端服务器。
示例二:内部微服务TCP接口代理 假设有一个用Golang编写的内部微服务,使用自定义的TCP协议,监听在8081端口。现在需要部署多个实例并做负载均衡。
# 技术栈:Nginx
stream {
# 定义一个哈希负载均衡,确保同一客户端的请求落到同一后端(如需会话粘滞)
upstream custom_tcp_service {
hash $remote_addr consistent; # consistent参数确保哈希一致性,后端增减时影响最小
server 172.18.0.10:8081 max_fails=2 fail_timeout=10s;
server 172.18.0.11:8081 max_fails=2 fail_timeout=10s;
server 172.18.0.12:8081 max_fails=2 fail_timeout=10s;
}
server {
listen 18081 so_keepalive=on; # 监听18081,并启用TCP keepalive
proxy_pass custom_tcp_service;
proxy_connect_timeout 3s;
proxy_timeout 30s;
# 可以传递客户端真实IP(如果后端服务需要记录)
proxy_bind $remote_addr transparent;
# 注意:使用transparent模式需要复杂的网络配置(如策略路由),通常用于高级场景。
}
}
这个配置通过hash $remote_addr实现了基于客户端IP的会话保持。consistent参数使得在增加或减少后端服务器时,只有少量客户端的连接会重新映射到不同的后端,减少了连接抖动。
五、应用场景、技术优缺点与注意事项
应用场景:
- 数据库负载均衡:MySQL、PostgreSQL、Redis等读写分离或集群的访问入口。
- 邮件服务代理:SMTP、POP3、IMAP服务的负载均衡与高可用。
- 游戏服务器网关:移动游戏或在线游戏常使用TCP/UDP长连接,四层代理是理想的网关。
- DNS负载均衡:对UDP协议的DNS查询请求进行分发。
- SSH/SFTP跳板机:统一访问入口,实现后端服务器的负载与安全隔离。
- 自定义协议服务:任何基于TCP/UDP的私有协议中间件或微服务。
- 物联网(IoT)接入:海量设备通过TCP/UDP接入的汇聚点。
技术优点:
- 性能高效:工作在传输层,数据转发开销极小,接近线速转发。
- 协议无关:对上层应用协议透明,通用性强。
- 配置简单:沿用Nginx清晰的配置语法,学习成本低。
- 功能丰富:支持多种负载均衡算法、被动健康检查、日志记录等。
- 高可用基石:与Keepalived等工具结合,可实现代理层自身的高可用。
技术缺点与局限:
- 无内容感知:无法根据数据包内容做高级路由(如根据SQL类型分库)。
- 会话保持局限:标准的四层代理本身是无状态的。虽然可以通过哈希算法实现简单粘滞,但在后端宕机时,该后端上的所有客户端连接都会中断。
- 主动健康检查缺失(开源版):如前所述,需要借助第三方方案补足。
- SSL/TLS终结复杂:如果想在代理层做SSL解密,需要配置
ssl_preread模块,且只能基于SNI等信息进行路由,无法像七层那样完全解密。
重要注意事项:
- 资源消耗:虽然四层代理本身开销小,但它维护的是完整的TCP连接。当并发连接数巨大时(如十万级),对Nginx服务器的文件描述符数量、内存和CPU仍有一定要求,需要调整系统参数(如
net.core.somaxconn,fs.file-max)和Nginx的worker_connections。 - UDP代理的可靠性:UDP本身是无连接的、不可靠的。Nginx的UDP代理尽力转发数据包,但不保证顺序和可达性。对于要求可靠传输的场景,应优先考虑TCP或在应用层处理。
- 网络拓扑:确保Nginx代理服务器与后端服务器之间的网络延迟低、带宽足,避免代理成为新的瓶颈。
- 安全考虑:四层代理暴露了后端服务的端口。务必结合防火墙(如iptables, firewalld)或安全组,严格限制只有代理服务器可以访问后端服务的真实端口。
- 监控与日志:充分利用Nginx的
stream访问日志(通过log_format和access_log指令配置),监控连接数、转发延迟、后端状态等关键指标。
六、总结
Nginx的四层代理功能,成功地将这款强大的Web服务器扩展为全能的网络流量分发器。它填补了在TCP/UDP协议栈上实现简单、高效、可靠负载均衡的空白。通过清晰的配置,我们可以轻松地为数据库、缓存、长连接服务等构建起坚固的流量入口。
虽然它在内容感知和主动健康检查方面存在局限,但通过将其与Nginx Plus、OpenResty或外部服务发现工具(如Consul)结合,可以构建出功能极其强大的现代网络基础设施。理解四层代理的工作原理和适用边界,能帮助我们在系统架构设计中做出更合适的技术选型,让Nginx继续在微服务、云原生和传统架构中发挥核心作用。
记住,没有一种工具是万能的。四层代理是工具箱里的一把利刃,专精于传输层的流量操控。当你的需求是“快速、透明地转发连接”时,它就是那个值得信赖的解决方案。
评论