家喻户晓,UDP 并不像 TCP 那样是基于连贯的。但有些时候,咱们须要往一个固定的地址发送多个 UDP 来实现一个 UDP 申请。为了保障服务端可能晓得这几个 UDP 包形成同一个会话,咱们须要在发送 UDP 包时绑定某个端口,这样当网络栈通过五元组(协定、客户端 IP、客户端端口、服务端 IP、服务端端口)进行辨别时,那几个 UDP 包可能分到一起。通常咱们会把这种景象称之为 UDP 连贯。
但这样又有了一个新的问题。不同于 TCP 那样有握手和挥手,UDP 连贯仅仅意味着应用固定的客户端端口。尽管作为服务端,因为当时就跟客户端约定好了一套固定的协定,能够晓得一个 UDP 连贯该当在何处终止。但如果两头应用了代理服务器,那么代理是如何辨别某几个 UDP 包是属于某个 UDP 连贯呢?毕竟没有握手和挥手作为分隔符,一个中间人是不分明某个会话该当在何处放下句号的。
通过上面的试验,咱们会看到 Nginx 是如何解决这个问题的。
试验
在接下来的几个试验中,我都会用一个固定的客户端。这个客户端会向 Nginx 监听的地址建设 UDP“连贯”,而后发送 100 个 UDP 包。
// save it as main.go, and run it like `go run main.go`
package main
import (
"fmt"
"net"
"os"
)
func main() {conn, err := net.Dial("udp", "127.0.0.1:1994")
if err != nil {fmt.Printf("Dial err %v", err)
os.Exit(-1)
}
defer conn.Close()
msg := "H"
for i := 0; i < 100; i++ {if _, err = conn.Write([]byte(msg)); err != nil {fmt.Printf("Write err %v", err)
os.Exit(-1)
}
}
}
根底配置
上面是试验中用到的 Nginx 根底配置。后续试验都会在这个根底上做些改变。
这个配置中,Nginx 会有 4 个 worker 过程监听 1994 端口,并代理到 1995 端口上。谬误日志会发往规范谬误,而拜访日志会发往规范输入。
worker_processes 4;
daemon off;
error_log /dev/stderr warn;
events {worker_connections 10240;}
stream {log_format basic '[$time_local]'
'received: $bytes_received'
'$session_time';
server {
listen 1994 udp;
access_log /dev/stdout basic;
preread_by_lua_block {ngx.log(ngx.ERR, ngx.worker.id(), " ", ngx.var.remote_port)
}
proxy_pass 127.0.0.1:1995;
proxy_timeout 10s;
}
server {
listen 1995 udp;
return "data";
}
}
输入如下:
2023/01/27 18:00:59 [error] 6996#6996: *2 stream [lua] preread_by_lua(nginx.conf:48):2: 1 51933 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
2023/01/27 18:00:59 [error] 6995#6995: *4 stream [lua] preread_by_lua(nginx.conf:48):2: 0 51933 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
2023/01/27 18:00:59 [error] 6997#6997: *1 stream [lua] preread_by_lua(nginx.conf:48):2: 2 51933 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
2023/01/27 18:00:59 [error] 6998#6998: *3 stream [lua] preread_by_lua(nginx.conf:48):2: 3 51933 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:01:09 +0800] received: 28 10.010
[27/Jan/2023:18:01:09 +0800] received: 27 10.010
[27/Jan/2023:18:01:09 +0800] received: 23 10.010
[27/Jan/2023:18:01:09 +0800] received: 22 10.010
能够看出,全副 100 个 UDP 包扩散到了每个 worker 过程上。看来 Nginx 并没有把来自同一个地址的 100 个包当作同一个会话,毕竟每个过程都会读取 UDP 数据。
reuseport
要想让 Nginx 代理 UDP 连贯,须要在 listen 时指定 reuseport:
...
server {
listen 1994 udp reuseport;
access_log /dev/stdout basic;
当初全副 UDP 包都会落在同一个过程上,并被算作同一个会话:
2023/01/27 18:02:39 [error] 7191#7191: *1 stream [lua] preread_by_lua(nginx.conf:48):2: 3 55453 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:02:49 +0800] received: 100 10.010
多个过程在监听同一个地址时,如果设置了 reuseport 时,Linux 会依据五元组的 hash 来决定发往哪个过程。这样一来,同一个 UDP 连贯外面的所有包就会落到一个过程上。
顺便一提,如果在 1995 端口的 server 上打印承受到的 UDP 连贯的客户端地址(即 Nginx 跟上游通信的地址),咱们会发现同一个会话的地址是一样的。也即是 Nginx 在代理到上游时,默认就会应用 UDP 连贯来传递整个会话。
proxy_xxx directives
置信读者也曾经留神到,在谬误日志中记录的 UDP 拜访开始工夫,和在拜访日志中记录的完结工夫,正好差了 10 秒。该时间段对应了配置里的 proxy_timeout 10s;
。因为 UDP 连贯中没有挥手的说法,Nginx 默认依据每个会话的超时工夫来决定会话何时终止。默认状况下,一个会话的持续时间是 10 分钟,只是因为我不足急躁,所以特定配成了 10 秒。
除了超时工夫,Nginx 还会依附什么条件决定会话的终止呢?请往下看:
...
proxy_timeout 10s;
proxy_responses 1;
在新增了 proxy_responses 1
后,输入变成了这样:
2023/01/27 18:07:35 [error] 7552#7552: *1 stream [lua] preread_by_lua(nginx.conf:48):2: 2 36308 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:07:35 +0800] received: 62 0.003
2023/01/27 18:07:35 [error] 7552#7552: *65 stream [lua] preread_by_lua(nginx.conf:48):2: 2 36308 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:07:35 +0800] received: 9 0.000
2023/01/27 18:07:35 [error] 7552#7552: *76 stream [lua] preread_by_lua(nginx.conf:48):2: 2 36308 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:07:35 +0800] received: 7 0.000
2023/01/27 18:07:35 [error] 7552#7552: *85 stream [lua] preread_by_lua(nginx.conf:48):2: 2 36308 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:07:35 +0800] received: 3 0.000
2023/01/27 18:07:35 [error] 7552#7552: *90 stream [lua] preread_by_lua(nginx.conf:48):2: 2 36308 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:07:35 +0800] received: 19 0.000
咱们看到 Nginx 不再被动等待时间超时,而是在收到上游发来的包之后就终止了会话。proxy_timeout
和 proxy_responses
两者间是“或”的关系。
和 proxy_responses
绝对的有一个 proxy_requests
:
...
proxy_timeout 10s;
proxy_responses 1;
proxy_requests 50;
在配置了 proxy_requests 50
后,咱们会看到每个申请的大小都稳固在 50 个 UDP 包:
2023/01/27 18:08:55 [error] 7730#7730: *1 stream [lua] preread_by_lua(nginx.conf:48):2: 0 49881 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
2023/01/27 18:08:55 [error] 7730#7730: *11 stream [lua] preread_by_lua(nginx.conf:48):2: 0 49881 while prereading client data, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:08:55 +0800] received: 50 0.002
[27/Jan/2023:18:08:55 +0800] received: 50 0.001
留神让会话终止所需的上游响应的 UDP 数是 proxy_requests * proxy_responses
。在下面的例子中,如果咱们把 proxy_responses
改成 2,那么要过 10 秒才会终止会话。因为这么做之后,对应每 50 个 UDP 包的申请,须要响应 100 个 UDP 包才会终止会话,而每个申请的 UDP 包只会失去一个 UDP 作为响应,所以只能等超时了。
动静代理
在大多数时候,UDP 申请的包数不是固定的,咱们可能要依据结尾的某个长度字段来确定会话的包数,抑或通过某个包的包头是否有 eof 标记来判断什么时候终结以后会话。目前 Nginx 的几个 proxy_*
指令都只反对固定值,不反对借助变量动静设置。
proxy_requests
和 proxy_responses
实际上只是设置了 UDP session 上的对应计数器。所以实践上咱们能够批改 Nginx,暴露出 API 来动静调整以后 UDP session 的计数器的值,实现按上下文决定 UDP 申请边界的性能。那是否存在不批改 Nginx 的解决方案呢?
换个思路,咱们能不能通过 Lua 把客户端数据都读出来,而后在 Lua 层面上由 cosocket 发送给上游?通过 Lua 实现上游代理这个思路的确挺富裕想象力,惋惜它目前是行不通的。
应用如下代码代替后面的 preread_by_lua_block
:
content_by_lua_block {local sock = ngx.req.socket()
while true do
local data, err = sock:receive()
if not data then
if err and err ~= "no more data" then
ngx.log(ngx.ERR, err)
end
return
end
ngx.log(ngx.WARN, "message received:", data)
end
}
proxy_timeout 10s;
proxy_responses 1;
proxy_requests 50;
咱们会看到这样的输入:
2023/01/27 18:17:56 [warn] 8645#8645: *1 stream [lua] content_by_lua(nginx.conf:59):12: message received: H, udp client: 127.0.0.1, server: 0.0.0.0:1994
[27/Jan/2023:18:17:56 +0800] received: 1 0.000
...
因为在 UDP 上面,ngx.req.socket:receive
目前只反对读取第一个包,所以即便咱们设置了 while true
循环,也得不到全副的客户端申请。另外因为 content_by_lua
会笼罩掉 proxy_*
指令,所以 Nginx 并没有走代理逻辑,而是认为以后申请只有一个包。把 content_by_lua
改成 preread_by_lua
后,尽管 proxy_*
指令这下子失效了,但因为拿不到全副客户端申请,仍然无奈实现 Lua 层面上的代理。
总结
如果 Nginx 代理的是 DNS 这种只有一个包的基于 UDP 的协定,那么应用 listen udp
就够了。但如果须要代理蕴含多个包的基于 UDP 的协定,那么还须要加上 reuseport。另外,Nginx 目前还不反对动静设置每个 UDP 会话的大小,所以没方法精确辨别不同的 UDP 会话。Nginx 代理 UDP 协定时能用到的性能,更多集中于像限流这种不须要关注单个 UDP 会话的。