详解Nginx如何代理UDP连接

2023-12-07 0 989
目录
  • UDP 连接
  • 实验
    • 基础配置
    • reuseport
    • proxy_xxx directives
    • 动态代理
  • 总结

    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:19942023/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:19942023/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:19942023/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.0032023/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.0002023/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.0002023/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.0002023/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:19942023/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 会话的。

    以上就是详解Nginx如何代理UDP连接的详细内容,更多关于Nginx代理UDP连接的资料请关注悠久资源其它相关文章!

    收藏 (0) 打赏

    感谢您的支持,我会继续努力的!

    打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
    点赞 (0)

    悠久资源 Nginx服务器 详解Nginx如何代理UDP连接 https://www.u-9.cn/server/nginx/114234.html

    常见问题

    相关文章

    发表评论
    暂无评论
    官方客服团队

    为您解决烦忧 - 24小时在线 专业服务