关于udp:化虹为桥-Nginx-如何代理-UDP-连接

家喻户晓,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_timeoutproxy_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_requestsproxy_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 会话的。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理