第二章【NGINX 开源功能】—— 四层反向代理

本节目录

处理 TCP/UDP 的七个阶段

nginx 自 1.9.0 起引入了 stream 模块(ngx_stream_core_module),用于实现 TCP/UDP 层的反向代理(四层反向代理),与 HTTP 模块不同,stream 模块并不解析应用层,而是直接转发原始字节流。


nginx 四层反向代理 7 个阶段的结构体

Post-Accept

此阶段(NGX_STREAM_POST_ACCEPT_PHASE)是 nginx 四层反向代理处理引擎中的第一个阶段。当 nginx 的监听进程成功接收到一个新的 TCP 连接或 UDP 数据包,并初步创建了 ngx_stream_session_t 会话结构后(对于 TCP nginx在 accept() 之后创建绘画结构,对于 UDP nginx 会基于客户端五元组虚拟创建会话结构),程序立将即进入此阶段。

此阶段的核心任务就是,在握手之后、正式决策之前,对连接进行最基础的元数据预处理(ip 和 端口等),它的存在也主要是为了解决一个矛盾 ------ 拿到某些必须支业务处理之前的信息。(典型场景 proxy protocol)

在标准 nginx stream 的模块中,挂载在此阶段的常用指令非常少,常用到的就俩(set_real_ip_fromproxy_protocol)。所以如果你想要获得更多指令,就需要使用第三方写好的模块或者自己定制化开发。下面就是官方的 ngx_stream_realip_module 模块的注册案例:

将 realip 模块注册到此阶段

如果你想在此阶段开发三方模块,必须遵守如下规矩:

  • 绝对不能执行任何会导致进程阻塞的操作(如同步磁盘 I/O 、数据库查询),因为这会卡住整个 nginx 事件循环;
  • 此阶段数据尚未就绪,preread 缓冲区基本是空的,所以你无法在此阶段读取业务数据(如 header 头或 mysql 登录包等);
  • 三方模块通常返回 NGX_DECLINED,以便让后续模块(如 realip)继续运行。除非检测到致命错误(返回 NGX_ERROR)或者你明确此模块执行后中止当前 handler 进入下一阶段(返回 NGX_OK)。

Pre-Access

此阶段(NGX_STREAM_PREACCESS_PHASE)是 nginx 四层反向代理处理引擎中的第二个阶段。

其主要任务是进行访问控制前的初步检查,由于它处于 Access 阶段之前,所以通常用于处理那些不需要读取应用层数据、仅根据连接本身属性就能决定的限制逻辑。

在标准版 nginx stream 中,可用于此阶段的指令也几乎没有,常用的就 limit_conn 它一个,用于限制连接数量,当超过规定数量 nginx 会关闭连接。如果需要开发第三方模块请参照 nginx/src/stream/ngx_stream_limit_conn_module.c 这个内置模块,将你的模块追加到 &cmcf->phases[NGX_STREAM_PREACCESS_PHASE].handlers[1](#1) 这个动态数组中即可。

Access

此阶段(NGX_STREAM_ACCESS_PHASE)是紧随 pre-access 之后的第三个阶段,如果说 pre-access 是初步安检(如限制并发连接数),那么 Access 阶段就是正式准入审查。

其主要任务是根据预设的规则(ip 黑白名单、认证状态等)来决定当前连接是否允许继续向后转发到上游服务器。

在 nginx stream 模块中,此阶段主要由 ngx_stream_access_module 负责,通过指令 allowdeny 来允许或拒绝特定 ip 或网段访问。

如果你要开发一个复杂的准入控制模块(对接 redis 校验 token、动态防火墙等),则此阶段是最佳落脚点。强烈建议参考 src/stream/ngx_stream_access_module.c 模块的实现。

如果你想在此阶段开发三方模块,必须遵守如下规矩:

  • 此阶段具有局限性,你拿不到 HTTP 的内容,只能通过 s->connection->sockaddr 拿到客户端的 ip 和 端口号;
  • 此阶段如果要拒绝访问,可以在此阶段设置标志变量,并在 content 阶段由 ngx_stream_return_module 统一返回提示信息(ngx_stream_access_module 默认打印一条错误日志后就会直接关闭 socket),这样对用户相对友好些;
  • 此阶段为每个连接的必经之路,还是老生常谈的问题,务必使用非阻塞的方式来实现它。

SSL

此阶段(NGX_STREAM_SSL_PHASE)是紧随 access 之后的第四个阶段,也是一个极其特殊的阶段,它的核心任务只是完成 TLS/SSL 握手即可。在四层代理中,此阶段要处理加密流量的解密(如果 nginx 作为 SSL 的终点)或简单的属性提取。

注意:

  • 如果 nginx 没有配置证书,它通常是不会进入此阶段的,而是直接将加密包透传给后端;
  • 此阶段是四层代理中计算量最大的,非对称加密握手会消耗大量 CPU 资源,所以建议在全局配置中开启 ssl_session_cache 以复用会话,减少重复握手,提高性能。

与之前的 access 阶段不同,很少人会在 NGX_STREAM_SSL_PHASE 上注册自定义 handler 来实现业务,这其中涉及到了 openssl 库和SSL 核心处理逻辑等。如果非要在此阶段挂 handler,请挂载到 SSL_set_verify 的回调函数上,或者使用 ngx_stream_ssl_conf_t 提供的钩子,不要直接在 cmcf->phase 数组里塞函数[2](#2)

如果你想在此阶段开发三方模块,必须遵守如下规矩:

  • 此阶段完成之前,你无法读取到加密报文中的任何业务数据,如果你需要根据原始报文内容来决定是否开启 SSL,你需要去下一阶段利用变量判断;
  • 一旦进入 SSL 阶段并握手成功,大量的 $ssl_* 变量(如 $ssl_protocol, $ssl_cipher, $ssl_client_s_dn)才会变得可用;
  • 对于高性能场景,nginx 支持异步 openssl 操作。如果你的模块涉及外部硬件加速器,需要特别处理 NGX_AGAIN 返回值,确保握手挂起时不会阻塞 Worker。

Preread

此阶段(NGX_STREAM_PREREAD_PHASE)是紧随 ssl 之后的第五个阶段,也是最核心、最具有技术含量的一个阶段。在四层代理中,nginx 默认是不会读取应用层数据的,它只是简单地把包从左手倒右手,但此阶段是允许 nginx 在不真正消耗数据的情况下,偷摸的看一眼客户端发送的前几个字节。

此阶段的核心意义在于判断流量是 HTTP、数据库还是 SSH 并进行只能路由转发,它读取的数据会保留在缓冲区中,当连接发往后端时,这些原始数据依然会完整的发送。

在 nginx stream 模块中,此阶段主要是由 ngx_stream_ssl_preread_module 等模块支持。其指令 ssl_preread 可以实现根据域名转发 TCP 流量,指令 preread_buffer_size 可以设置预读缓冲区的大小用于分析结果,指令 preread_timeout 可以设置预读数据到达的超时时间,当客户端只建立连接而不发送数据时,避免 nginx 死等着。

作为开发者来说,这里将是重灾区,推荐参考 src/stream/ngx_stream_ssl_preread_module.c 模块来进行开发。

如果你想在此阶段开发三方模块,必须遵守如下规矩:

  • 想要获取 peek 数据,请通过 c->recv(需谨慎,避免多读)或检查 s->preread_buf(优先)来获取;
  • 因为 TCP 是流式数据,所以你必须要遵循状态机机制,正确处理 handler 的 NGX_DONENGX_AGAINNGX_DECLINED
  • 对于缓冲区指针的操作,你只能移动指针或读取内容,坚决不能把数据从缓冲区中删除,否则后端收到的包将是不完整的;
  • 预读会增加首包延迟 TTFB(nginx 必须等缓冲区填满或匹配到特征码才往下走),所以建议将 preread_buffer_size 设置得尽量小且精准;
  • 注意前一阶段是否存在 SSL 解密,如果存在则本阶段读到的是明文,反之是密文。

Coutent

此阶段(NGX_STREAM_CONTENT_PHASE)是紧随 preread 之后的第六阶段,也是 nginx 四层代理的终点站,其唯一任务就是产生响应或将流量转发到上游服务器。 在四层代理中,这通常意味着建立与后端服务器的 TCP/UDP 连接,并在客户端和后端之间开启双向数据透明传输。

此阶段具有独占性,在一个 server 块中只能有一个模块负责此阶段,一旦某个模块接管了内容处理,后续的 content 处理程序将不会被触发。此外,此阶段不止负责数据的发送,还管理着整个连接的生命周期。

在 nginx stream 模块中,此阶段主要是由 ngx_stream_proxy_modulengx_stream_return_module 两模块支持,前一个负责将流量转发到后端服务器,后一个负责维护状态或非法访问的友好提示(向客户端发送指定字符串并关闭连接)。

如果你想在此阶段开发三方模块,有下列注意事项:

  • 与上述的其他阶段不同,此阶段通常是通过配置指令直接设置 ngx_stream_core_srv_conf_t 中的 handler 指针,让你的模块来接管内容处理,而不是推入全局的 phase 数组;(参见下图)
  • 如果你实现的 handler 只是想发点数据就行了,那么可以参见 nginx/src/stream/ngx_stream_return_module.c 模块;
  • 如果你实现的 handler 还想要做代理,那么你需要自行调用 ngx_stream_proxy_handler 和管理上游连接;
  • 作为内容的产生者,你必须确保在连接结束时释放所有分配的资源,不过一般不需要自己管理,而是通过内存池 s->connection->pool 自动管理;
  • 你自定义的 handler 一旦被调用,就必须负责整个会话的生命周期,且禁止返回 NGX_DECLINED,否则会导致未定义的行为。


return 模块的单点挂载

Log

此阶段(NGX_STREAM_LOG_PHASE)是最后一个阶段,当上一阶段处理完成、连接准备关闭或已经关闭的时候,nginx 会进入此阶段记录这次会话的所有相关信息。

此阶段是事后审计的阶段,无论连接是正常结束、被中途拒绝(access 直接跳过来的),还是因为超时断开,nginx 都会调用该阶段的处理器。

如果你需要将日志实时推送到 kafka、发送到特定的监控系统或者进行自定义的流量审计,你需要在此阶段注册 handler。可以参考 src/stream/ngx_stream_log_module.c 模块来写。

Nginx Stream 如何串联七个阶段

在 nginx stream 源码中,串联这七个阶段的核心逻辑被称为阶段引擎,它并不是靠简单的函数嵌套,而是通过一个连续的函数数组(阶段数组)和一个索引指针来回拨动状态机实现的。下面是一个连接的全流程追踪:(基于 nginx-1.26.x 及以后的最新源码)

  1. 打开工厂大门,连接从 event 核心进入 stream 模块的第一个函数,需要分配 ngx_stream_session_t 会话结构,先判断连接是否是 SSL,以决定是否进入 NGX_STREAM_SSL_PHASE;(本次假设不是 SSL,不进入 SSL 阶段)

收到货物,送入工厂准备启动阶段引擎处理它

  1. 阶段引擎启动,这是阶段引擎运行起点,调用 ngx_stream_core_run_phases(s) 正式让连接进入流水线;

引擎启动函数:拉闸开电,准备干活

  1. 阶段驱动引擎,这是整个流水线的核心,它根据 s->phase_handler 查找对应阶段的 checker,并循环调用每个阶段 checker 函数。只有 checker 能决定是停下来等事件(返回 NGX_OK),还是继续走下一个(索引自增并继续循环);

引擎驱动函数:状态机逐步推进,去找牛马们(handler)来处理流水线上的货物

  1. 阶段 python,ngx_stream_core_generic_phase() 函数就像胶水一样,负责调用用户注册的函数。如果你的 handler 返回 NGX_OK,它会在当前阶段完成后直接进入下一阶段,忽略此阶段后续 handler;如果返回 NGX_DECLINED,它会放弃处理当前 handler,并自增 s->phase_handler 在循环中立即执行下一模块;如果返回 NGX_AGAIN,它会等待当前 handller 被事件触发,此时阶段引擎暂时挂起(等待事件回调再次驱动);

包工头函数:接收状态机找的牛马们,然后有序组织它们干活

  1. 阶段货仓,这是整个流水线的终点站,它不再寻找下一个阶段,而是查找 cscf->handler,如果存在(比如 proxy_pass),就直接跳过去执行,阶段引擎的循环到此终止;

快递员函数:根据货物目的地发送出去,独占性发货,后续 handler 不再触发

  1. 统计员,无论连接成功还是失败最后都会手动调用此函数,它会触发 NGX_STREAM_LOG_PHASE 进入 log 阶段。

    log 阶段不在 run_phases 的主循环里跑,而是由这个销毁函数在关闭 socket 前最后拉起来跑一次。

记录所有货物的处理情况

四层反向代理配置

在生产环境中,nginx 四层代理常用于数据库集群(MySQL/Redis)、中继加速(SSH/RDP)、DNS 转发以及非 HTTP 协议的负载均衡。

nginx 复制代码
limit_conn_zone $binary_remote_addr zone=addr:10m;

stream {
    log_format proxy '$remote_addr [$time_local] '		# 定义日志格式,记录连接耗时、流量和后端响应情况
                     '$protocol $status $bytes_sent $bytes_received '
                     '$session_time "$upstream_addr" '
                     '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';
    access_log /var/log/nginx/stream_access.log proxy;

    upstream mysql_backend {
        hash $remote_addr consistent; 					# 采用通用 hash,确保同一 ip 尽量落到同一台库,减少 session 漂移
        server 192.168.1.10:3306 max_fails=3 fail_timeout=30s;
        server 192.168.1.11:3306 max_fails=3 fail_timeout=30s;
        server 192.168.1.12:3306 backup;
    }

    server {
        listen 33060;
        
        allow 10.0.0.0/8;    				# 仅允许内网网段访问(access 阶段)
        deny all;            				# 拒绝其他所有连接
        limit_conn addr 100;				# 限制单个 ip 同时最多 100 个连接(pre-access 阶段)
        proxy_connect_timeout 5s;  			# 连接后端超时,生产建议设短一点,快速重试
        proxy_timeout 10m;         			# 数据传输超时,对于长连接(数据库),设长一些防止被意外切断
        proxy_buffer_size 16k;     			# 存放后端响应数据的缓冲区
        proxy_pass mysql_backend;			# 转发
        proxy_bind 192.168.100.100:3306;	# 通过本地接口 192.168.100.100 的端口 3306 转发到上游
    }

	server {
		listen 12345;
		...
	}
}

配置 nginx 四层反向代理的注意事项:

  • nginx 作为中转,作为客户端去连后端时会消耗临时端口,可以根据需要去调优内核参数 net.ipv4.ip_local_port_range 扩大端口范围,并开启 net.ipv4.tcp_tw_reuse 以快速回收端口;

    /etc/sysctl.conf 复制代码
    net.ipv4.ip_local_port_range = 1024 65535			# 扩大临时端口范围
    net.ipv4.tcp_tw_reuse = 1							# 开启快速回收和重用处于 time_wait 的连接
    net.core.somaxconn = 4096							# 调高全连接队列大小,防止高并发下丢包
  • 上游服务器(如 mysql)看到的连接 ip 全是 nginx 的内网 ip 时是无法进行审计的,可以设置 proxy_protocol on(需要上游软件支持,如 mysql 8.0 或 pgsql);

    nginx 复制代码
    stream {
    	server {
    		listen 3306;
    		proxy_pass mysql_backend;
    		proxy_protocol on;					# 开启 proxy protocol 发送,将客户端真实 ip 封装在报文头传给后端
    	}
    }
  • 四层代理一个会话要占两个 socket(一个客户端,一个后端),所以必须修改系统 ulimit -n 和 nginx 的 worker_rlimit_nofile(如果单机并发 10 万,句柄数至少要设为 20 万以上);

    nginx 复制代码
    user nginx;									
    worker_processes auto;
    worker_rlimit_nofile 100000; 				# 限制每个 worker 能打开的最大文件数
    
    events {
     	worker_connections 40000;				# 每个 worker 允许的并发连接
     	use epoll;								# 开启 epoll 事件模型提高效率
     	multi_accept on;			 			# 允许一个 worker 同时接收多个新连接
    }
  • UDP 是无连接的,如果代理 UDP,必须配置 proxy_responses 指令告知 nginx 收到多少个包后就认为本次会话结束,否则会话会一直占用直到超时。

    nginx 复制代码
    stream {
     	server {
         	listen 53 udp;
         	proxy_pass dns_backends;
         	proxy_responses 1; 					# 期望从后端收到 1 个包后就关闭会话(适用于 DNS 查询)
         	proxy_timeout 10s; 					# 如果后端没回包,10秒后强制释放连接资源
     	}
    }

附加

下面是一个 nginx stream 中各阶段的 handler 开发模板:(不适用于内容处理阶段)

c 复制代码
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_stream.h>

/**
 * 业务逻辑处理的模板
 * @param s: 当前 stream 会话上下文,包含连接、内存池、变量等所有信息
 */
static ngx_int_t ngx_stream_my_filter_logic_handler(ngx_stream_session_t *s) {
    ngx_connection_t  *c;
    c = s->connection;

    // 1. 记录一条调试日志
    ngx_log_error(NGX_LOG_DEBUG, c->log, 0, "my filter: f*`*k nginx");

    // 2. 处理业务逻辑
    // 在四层代理中,获取客户端 ip 地址进行判断(c->sockaddr 包含了原始的二进制地址信息)
    if (c->sockaddr->sa_family == AF_INET) {
        // 如果是 ipv4,执行一些逻辑
	   	// ...
    }

    // 3. 决定连接命运
    // 验证通过,允许进入下一个 handler 或下一个阶段
    // return NGX_DECLINED; 
    // 验证通过,且不需要再执行本阶段后续的 handler,直接跳到下一阶段
    // return NGX_OK;
    // 验证失败,出现异常,立即中断并关闭连接
    // return NGX_ABORT;
    // 默认行为:交给下一个处理器
    return NGX_DECLINED;
}

/**
 * 在 nginx 内存池中申请内存的模板
 * @param s: 当前 stream 会话上下文,包含连接、内存池、变量等所有信息
 */
static ngx_int_t ngx_stream_my_filter_memory_handler(ngx_stream_session_t *s) {
    ngx_connection_t  *c;
    void              *my_data;
    ngx_str_t         *custom_msg;

    c = s->connection;

    // 1:申请一块普通的内存(类似 malloc)
    // 使用 s->connection->pool,这块内存在 TCP 连接断开时自动释放
    // ngx_pallo 分配对齐的内存,用于性能要求极高的结构体分配
    my_data = ngx_palloc(c->pool, 1024); 
    if (my_data == NULL) {
        return NGX_ERROR; // 内存分配失败,返回错误
    }

    // 2:申请并清零内存(类似 calloc)
    // ngx_pcalloc	分配并初始化为 0,防止读到内存中的脏数据
    custom_msg = ngx_pcalloc(c->pool, sizeof(ngx_str_t));
    if (custom_msg == NULL) {
        return NGX_ERROR;
    }

    // 3:拷贝字符串
    // nginx 的字符串不是以 \0 结尾的,手动拷贝容易出错
    // ngx_pnalloc 分配不对齐的内存,可以节省内存对齐造成的空隙
    u_char *src = (u_char *) "cnmd nginx";
    size_t len = ngx_strlen(src);
    custom_msg->data = ngx_pnalloc(c->pool, len);
    if (custom_msg->data == NULL) {
        return NGX_ERROR;
    }
    ngx_memcpy(custom_msg->data, src, len);
    custom_msg->len = len;

    ngx_log_error(NGX_LOG_DEBUG, c->log, 0, "fenpei memory for: %V", custom_msg);

    return NGX_DECLINED;
}

/**
 * 模块初始化注册函数的模板(不适用于内容处理阶段)
 * 通常赋值给 ngx_stream_module_t 中的 postconfiguration 钩子
 */
static ngx_int_t ngx_stream_my_filter_init(ngx_conf_t *cf) {
    ngx_stream_handler_pt        *h;
    ngx_stream_core_main_conf_t  *cmcf;

    // 1:获取 stream 核心模块的 main 配置
    cmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_core_module);

    // 2:在对应的阶段(以 preaccess 为例)推入一个新的 handler 指针
    h = ngx_array_push(&cmcf->phases[NGX_STREAM_PREACCESS_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    // 3:将占位符指向我们编写的函数地址
    *h = ngx_stream_my_filter_logic_handler;
    return NGX_OK;
}

下面是一份适用于内容处理阶段的模板:

c 复制代码
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_stream.h>

static void ngx_stream_my_echo_handler(ngx_stream_session_t *s);
static void ngx_stream_my_echo_event_handler(ngx_event_t *ev);

/**
 * 注册函数,在配置解析时将本模块设为内容处理阶段的处理器
 * 函数统一这样签名,否则编译器报错
 */
static char *ngx_stream_my_echo_directive(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {
    ngx_stream_core_srv_conf_t  *cscf;

    // 获取当前 server 块的核心配置
    cscf = ngx_stream_conf_get_module_srv_conf(cf, ngx_stream_core_module);

    // 直接将 handler 指针挂载到核心配置上(第六阶段独有)
    cscf->handler = ngx_stream_my_echo_handler;

    return NGX_CONF_OK;
}

/**
 * 入口 handler,当连接经过前五个阶段后,会跳转到这里
 */
static void ngx_stream_my_echo_handler(ngx_stream_session_t *s) {
    ngx_connection_t  *c;

    c = s->connection;
    ngx_log_error(NGX_LOG_INFO, c->log, 0, "enter my echo content phase");

    // 设置读写事件的回调函数(接管状态机)
    c->read->handler = ngx_stream_my_echo_event_handler;
    c->write->handler = ngx_stream_my_echo_event_handler;

    // 将 session 指针存入 connection 的 data,方便在事件触发时找回 session
    c->data = s;

    // 立即触发一次读逻辑
    ngx_stream_my_echo_event_handler(c->read);
}

/**
 * 事件循环,处理实际的数据收发
 */
static void ngx_stream_my_echo_event_handler(ngx_event_t *ev) {
    ngx_connection_t      *c;
    ngx_stream_session_t  *s;
    u_char                 buf[1024];
    ssize_t                n;
    c = ev->data;
    s = c->data;

    // 如果连接已经出错或超时,直接终结
    if (ev->timedout) {
        ngx_stream_finalize_session(s, NGX_STREAM_OK);
        return;
    }

    // 读循环逻辑
    for ( ;; ) {
        n = c->recv(c, buf, 1024);
        if (n == NGX_AGAIN) {
            // 内核缓冲区暂时没数据,等待下一次 epoll 通知
            if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
                ngx_stream_finalize_session(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
            }
            return;
        }

        if (n == NGX_ERROR || n == 0) {
            // n=0 表示客户端主动断开连接
            ngx_stream_finalize_session(s, NGX_STREAM_OK);
            return;
        }

        // 业务逻辑:将读到的内容原样发回
        // 实际开发中应检查 c->send 的返回值和缓冲区状态
        c->send(c, buf, n);
      
        break;
    }
}

  1. &cmcf->phases 是一个超级数组,其中每个元素都是 stream 的一个处理阶段;
    NGX_STREAM_PREACCESS_PHASE 是具体的阶段名称;
    .handlers 是一个阶段下的动态数组,存储了一个阶段下所有需要执行的拓展。 ↩︎

  2. 因为 SSL 阶段的执行并非严格的 phase-handler 顺序,而是被 openssl 状态机驱动,直接插入 phase handler 可能导致未初始化的 SSL 结构被访问。 ↩︎

相关推荐
sinovoip10 分钟前
香蕉派开源社区联合进迭进空重磅打造: BPI‑SM10(K3-Com260) 和 K3 Pico‑ITX 计算机将于5月11日全球发货
人工智能·开源·risc-v
星恒讯工业路由器12 分钟前
配网自动化多网融合应用解决方案
运维·自动化
智慧物业老杨18 分钟前
智慧物业收费系统的数智化落地实践:从人工硬扛到自动化闭环
运维·自动化
FIT2CLOUD飞致云27 分钟前
飞致云开源社区月度动态报告(2026年4月)
开源·飞致云·月度报告·开源报告
techdashen44 分钟前
Cloudflare 为何抛弃 NGINX,用 Rust 自研了一个代理
运维·nginx·rust
南城猿1 小时前
保姆级 Ubuntu 部署 禅道
linux·运维·ubuntu
胖虎喜欢静香1 小时前
从零到一快速实现 Mini DeepResearch
人工智能·python·开源
花间相见1 小时前
【大模型推理01】—— 初探VLLM:高性能LLM推理引擎,让开源模型跑起来更快更省
开源·vllm
珠海西格电力1 小时前
零碳园区产业园管理系统的全场景源网荷储氢协同调度功能是如何实现的
大数据·运维·人工智能·物联网·能源