fix: fix the issue of During ultra-high concurrency (5000 req, 200 concurrent) URL Params testing, stability issues were encountered (success rate only 0.24%).
This commit is contained in:
+45
-23
@@ -1,40 +1,62 @@
|
|||||||
# 已知问题
|
# 已知问题
|
||||||
|
|
||||||
## 1. HTTP Keep-Alive 连接复用不支持
|
## 1. HTTP Keep-Alive 连接复用优化
|
||||||
|
|
||||||
**状态**: 等待 V 语言官方修复
|
**状态**: 已优化 (2025-12-30)
|
||||||
|
|
||||||
**问题描述**:
|
**问题描述**:
|
||||||
每次 HTTP 请求响应时间约 300ms,其中大部分时间消耗在 TCP 连接建立上。即使响应头中包含 `Connection: keep-alive`,连接也无法复用。
|
在超高并发(5000 req, 200 concurrent)的 URL Params 测试中出现稳定性问题(成功率仅 0.24%)。
|
||||||
|
|
||||||
**现象**:
|
|
||||||
- 快速连续发送请求:每次都慢(~300ms)
|
|
||||||
- 慢速发送请求:快慢交替(复用时快,超时后重建连接时慢)
|
|
||||||
|
|
||||||
**根本原因**:
|
**根本原因**:
|
||||||
V 语言标准库 `net.http.Server` 的 `handle_conn` 函数在每次请求处理完成后都会立即关闭连接:
|
1. picoev 的 `max_fds = 1024` 限制了并发连接数
|
||||||
|
2. 虽然设置了 `Connection: keep-alive` 头,但连接管理不够优化
|
||||||
|
3. 高并发下客户端端口耗尽(TCP TIME_WAIT 状态累积)
|
||||||
|
4. 缺少 `Content-Length` 头导致客户端无法正确解析响应边界
|
||||||
|
|
||||||
|
**已实施的优化**:
|
||||||
|
|
||||||
|
### 1. 增强 Keep-Alive 支持
|
||||||
|
- 添加 `Keep-Alive` 响应头,包含 `timeout` 和 `max` 参数
|
||||||
|
- 检测客户端 Keep-Alive 请求头
|
||||||
|
- 根据客户端请求决定是否保持连接
|
||||||
|
|
||||||
|
### 2. 响应头优化
|
||||||
|
- 始终设置 `Content-Length` 头,帮助客户端正确解析响应边界
|
||||||
|
- 优化 `Content-Type` 默认值
|
||||||
|
|
||||||
|
### 3. 高并发配置
|
||||||
|
新增 `HighConcurrencyConfig` 和 `listen_high_concurrency()` 方法:
|
||||||
```v
|
```v
|
||||||
// F:\v\vlib\net\http\server.v
|
// 使用高并发优化配置启动
|
||||||
fn (mut w HandlerWorker) handle_conn(mut conn net.TcpConn) {
|
app.listen_high_concurrency(8081)
|
||||||
defer {
|
|
||||||
conn.close() or { eprintln('close() failed: ${err}') } // 每次请求后关闭连接
|
// 或使用自定义配置
|
||||||
}
|
config := hono.HighConcurrencyConfig{
|
||||||
// ... 处理单个请求 ...
|
port: 8081
|
||||||
|
timeout_secs: 5 // 较短超时
|
||||||
|
keepalive_timeout: 3 // Keep-Alive 超时
|
||||||
|
max_keepalive_req: 500 // 单连接最大请求数
|
||||||
}
|
}
|
||||||
|
app.listen_high_concurrency_with_config(config)
|
||||||
```
|
```
|
||||||
|
|
||||||
**影响**:
|
### 4. 服务器配置优化
|
||||||
- 无法实现 HTTP/1.1 Keep-Alive 连接复用
|
- 降低默认超时时间(8秒 -> 5秒)以更快释放连接
|
||||||
- 每次请求都需要重新建立 TCP 连接(三次握手)
|
- 增大写缓冲区(8KB -> 64KB)
|
||||||
- 高并发场景下性能受限
|
- 添加 TCP_NODELAY 配置选项
|
||||||
|
|
||||||
**临时解决方案**:
|
**使用建议**:
|
||||||
- 在服务器前部署支持 Keep-Alive 的反向代理(如 nginx)
|
1. 高并发场景使用 `--high-concurrency` 或 `-hc` 参数启动服务器
|
||||||
- 或等待 V 语言官方修复此问题
|
2. 在服务器前部署支持 Keep-Alive 的反向代理(如 nginx)
|
||||||
|
3. 使用压力测试工具验证: `v run examples/stress_test.v`
|
||||||
|
|
||||||
|
**picoev 底层限制**:
|
||||||
|
- `max_fds = 1024` 是 picoev 的硬编码限制
|
||||||
|
- 真正的连接复用需要 picoev 底层支持
|
||||||
|
- 建议关注 V 语言官方对 picoev 的更新
|
||||||
|
|
||||||
**跟踪**:
|
**跟踪**:
|
||||||
- V 语言 GitHub: https://github.com/vlang/v
|
- V 语言 GitHub: https://github.com/vlang/v
|
||||||
- 相关文件: `vlib/net/http/server.v`
|
- 相关文件: `vlib/picoev/picoev.v`
|
||||||
|
|
||||||
**发现日期**: 2025-12-26
|
**更新日期**: 2025-12-30
|
||||||
|
|||||||
@@ -57,6 +57,5 @@ fn main() {
|
|||||||
println('║ 特性: picoev 事件驱动 + Keep-Alive ║')
|
println('║ 特性: picoev 事件驱动 + Keep-Alive ║')
|
||||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||||
|
|
||||||
// 使用 picoev 启动服务器
|
|
||||||
app.listen_picoev(8081)
|
app.listen_picoev(8081)
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-51
@@ -11,16 +11,19 @@ pub:
|
|||||||
port int = 8080
|
port int = 8080
|
||||||
host string
|
host string
|
||||||
family net.AddrFamily = .ip6
|
family net.AddrFamily = .ip6
|
||||||
timeout_secs int = 30
|
timeout_secs int = 8
|
||||||
max_headers int = 100
|
max_headers int = 100
|
||||||
max_read int = 8192
|
max_read int = 8192
|
||||||
max_write int = 8192
|
max_write int = 65536
|
||||||
|
keepalive_timeout int = 5
|
||||||
|
max_keepalive_req int = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
// picoev 请求上下文
|
// picoev 请求上下文
|
||||||
struct PicoevRequestContext {
|
struct PicoevRequestContext {
|
||||||
mut:
|
mut:
|
||||||
app &Hono = unsafe { nil }
|
app &Hono = unsafe { nil }
|
||||||
|
config PicoevConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 picoev 启动服务器
|
// 使用 picoev 启动服务器
|
||||||
@@ -34,6 +37,7 @@ pub fn (mut app Hono) listen_picoev(port int) {
|
|||||||
pub fn (mut app Hono) listen_picoev_with_config(config PicoevConfig) {
|
pub fn (mut app Hono) listen_picoev_with_config(config PicoevConfig) {
|
||||||
mut ctx := &PicoevRequestContext{
|
mut ctx := &PicoevRequestContext{
|
||||||
app: unsafe { &app }
|
app: unsafe { &app }
|
||||||
|
config: config
|
||||||
}
|
}
|
||||||
|
|
||||||
mut pico := picoev.new(
|
mut pico := picoev.new(
|
||||||
@@ -67,26 +71,17 @@ fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picoht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析路径和查询参数
|
|
||||||
path, query_map := parse_path_and_query(req.path)
|
path, query_map := parse_path_and_query(req.path)
|
||||||
|
keepalive := check_keepalive_request(req)
|
||||||
// 路由匹配
|
|
||||||
method_str := req.method
|
method_str := req.method
|
||||||
|
|
||||||
// 优先使用快速路由器
|
// 优先使用快速路由器
|
||||||
if ctx.app.use_fast_router {
|
if ctx.app.use_fast_router {
|
||||||
if route_match := ctx.app.fast_router.match_route(method_str, path) {
|
if route_match := ctx.app.fast_router.match_route(method_str, path) {
|
||||||
// 构造 Context
|
|
||||||
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
||||||
|
|
||||||
// 获取中间件
|
|
||||||
middlewares := get_middlewares_for_path_picoev(ctx.app, path)
|
middlewares := get_middlewares_for_path_picoev(ctx.app, path)
|
||||||
|
|
||||||
// 执行中间件和处理器
|
|
||||||
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
||||||
|
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||||||
// 发送响应
|
|
||||||
send_picoev_response(mut res, hono_ctx, response)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,40 +89,50 @@ fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picoht
|
|||||||
// 回退到混合路由器
|
// 回退到混合路由器
|
||||||
if route_match := ctx.app.context_hybrid_router.match_route(method_str, path) {
|
if route_match := ctx.app.context_hybrid_router.match_route(method_str, path) {
|
||||||
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
||||||
|
|
||||||
// 获取中间件
|
|
||||||
middlewares := get_middlewares_for_path_picoev(ctx.app, path)
|
middlewares := get_middlewares_for_path_picoev(ctx.app, path)
|
||||||
|
|
||||||
// 执行中间件和处理器
|
|
||||||
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
||||||
|
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||||||
send_picoev_response(mut res, hono_ctx, response)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 404 Not Found - 检查自定义 notFound 处理器
|
// 404 Not Found
|
||||||
mut hono_ctx := create_picoev_context(req, map[string]string{}, query_map)
|
mut hono_ctx := create_picoev_context(req, map[string]string{}, query_map)
|
||||||
|
|
||||||
if handler := ctx.app.not_found_handler {
|
if handler := ctx.app.not_found_handler {
|
||||||
response := handler(mut hono_ctx)
|
response := handler(mut hono_ctx)
|
||||||
send_picoev_response(mut res, hono_ctx, response)
|
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认 404 响应
|
// 默认 404 响应
|
||||||
hono_ctx.status(404)
|
|
||||||
res.raw('HTTP/1.1 404 Not Found\r\n')
|
res.raw('HTTP/1.1 404 Not Found\r\n')
|
||||||
res.header('Content-Type', 'text/plain')
|
res.header('Content-Type', 'text/plain')
|
||||||
|
res.header('Content-Length', '9')
|
||||||
|
if keepalive {
|
||||||
res.header('Connection', 'keep-alive')
|
res.header('Connection', 'keep-alive')
|
||||||
|
res.header('Keep-Alive', 'timeout=${ctx.config.keepalive_timeout}, max=${ctx.config.max_keepalive_req}')
|
||||||
|
} else {
|
||||||
|
res.header('Connection', 'close')
|
||||||
|
}
|
||||||
res.body('Not Found')
|
res.body('Not Found')
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取路径对应的所有中间件(全局 + 路由前缀匹配的)
|
// 检查客户端是否请求 Keep-Alive
|
||||||
|
fn check_keepalive_request(req picohttpparser.Request) bool {
|
||||||
|
for i in 0 .. req.num_headers {
|
||||||
|
h := req.headers[i]
|
||||||
|
if h.name.to_lower() == 'connection' {
|
||||||
|
return h.value.to_lower().contains('keep-alive')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true // HTTP/1.1 默认 Keep-Alive
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取路径对应的所有中间件
|
||||||
fn get_middlewares_for_path_picoev(app &Hono, path string) []ContextMiddleware {
|
fn get_middlewares_for_path_picoev(app &Hono, path string) []ContextMiddleware {
|
||||||
mut middlewares := app.context_middlewares.clone()
|
mut middlewares := app.context_middlewares.clone()
|
||||||
|
|
||||||
// 按前缀长度排序,确保更具体的前缀后执行
|
|
||||||
mut prefixes := app.route_middlewares.keys()
|
mut prefixes := app.route_middlewares.keys()
|
||||||
prefixes.sort(a.len < b.len)
|
prefixes.sort(a.len < b.len)
|
||||||
|
|
||||||
@@ -149,9 +154,8 @@ fn exec_middlewares_picoev(idx int, middlewares []ContextMiddleware, mut ctx Con
|
|||||||
return mw(mut ctx, fn [idx, middlewares, handler] (mut c Context) http.Response {
|
return mw(mut ctx, fn [idx, middlewares, handler] (mut c Context) http.Response {
|
||||||
return exec_middlewares_picoev(idx + 1, middlewares, mut c, handler)
|
return exec_middlewares_picoev(idx + 1, middlewares, mut c, handler)
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
return handler.handle(mut ctx)
|
|
||||||
}
|
}
|
||||||
|
return handler.handle(mut ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析路径和查询参数
|
// 解析路径和查询参数
|
||||||
@@ -162,15 +166,11 @@ fn parse_path_and_query(full_path string) (string, map[string]string) {
|
|||||||
path := full_path[..idx]
|
path := full_path[..idx]
|
||||||
query_str := full_path[idx + 1..]
|
query_str := full_path[idx + 1..]
|
||||||
|
|
||||||
// 解析查询参数
|
|
||||||
for part in query_str.split('&') {
|
for part in query_str.split('&') {
|
||||||
if eq_idx := part.index('=') {
|
if eq_idx := part.index('=') {
|
||||||
key := part[..eq_idx]
|
query_map[part[..eq_idx]] = part[eq_idx + 1..]
|
||||||
value := part[eq_idx + 1..]
|
|
||||||
query_map[key] = value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return path, query_map
|
return path, query_map
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,6 @@ fn parse_path_and_query(full_path string) (string, map[string]string) {
|
|||||||
|
|
||||||
// 创建 picoev 上下文
|
// 创建 picoev 上下文
|
||||||
fn create_picoev_context(req picohttpparser.Request, params map[string]string, query map[string]string) Context {
|
fn create_picoev_context(req picohttpparser.Request, params map[string]string, query map[string]string) Context {
|
||||||
// 将 picohttpparser.Request 转换为需要的格式
|
|
||||||
return Context{
|
return Context{
|
||||||
req: convert_picoev_request(req)
|
req: convert_picoev_request(req)
|
||||||
params: params
|
params: params
|
||||||
@@ -219,34 +218,27 @@ fn convert_picoev_request(req picohttpparser.Request) http.Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发送 picoev 响应
|
// 发送 picoev 响应
|
||||||
fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response http.Response) {
|
fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response http.Response, keepalive bool, config PicoevConfig) {
|
||||||
// 设置状态码
|
|
||||||
status_code := if ctx.status_code != 0 { ctx.status_code } else { response.status_code }
|
status_code := if ctx.status_code != 0 { ctx.status_code } else { response.status_code }
|
||||||
|
|
||||||
if status_code == 200 {
|
if status_code == 200 {
|
||||||
res.http_ok()
|
res.http_ok()
|
||||||
} else {
|
} else {
|
||||||
// 手动构建状态行
|
res.raw('HTTP/1.1 ${status_code} ${get_status_text(status_code)}\r\n')
|
||||||
status_text := get_status_text(status_code)
|
|
||||||
res.raw('HTTP/1.1 ${status_code} ${status_text}\r\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置响应头
|
|
||||||
mut has_content_type := false
|
mut has_content_type := false
|
||||||
mut has_connection := false
|
mut has_connection := false
|
||||||
|
mut has_content_length := false
|
||||||
|
|
||||||
// 从 Context 的 headers 中获取
|
|
||||||
for key, value in ctx.headers {
|
for key, value in ctx.headers {
|
||||||
res.header(key, value)
|
res.header(key, value)
|
||||||
if key.to_lower() == 'content-type' {
|
key_lower := key.to_lower()
|
||||||
has_content_type = true
|
if key_lower == 'content-type' { has_content_type = true }
|
||||||
}
|
if key_lower == 'connection' { has_connection = true }
|
||||||
if key.to_lower() == 'connection' {
|
if key_lower == 'content-length' { has_content_length = true }
|
||||||
has_connection = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 http.Response 的 header 中获取
|
|
||||||
if content_type := response.header.get(.content_type) {
|
if content_type := response.header.get(.content_type) {
|
||||||
if !has_content_type {
|
if !has_content_type {
|
||||||
res.header('Content-Type', content_type)
|
res.header('Content-Type', content_type)
|
||||||
@@ -254,19 +246,25 @@ fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response h
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认 Content-Type
|
|
||||||
if !has_content_type {
|
if !has_content_type {
|
||||||
res.header('Content-Type', 'text/plain')
|
res.header('Content-Type', 'text/plain; charset=utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_content_length {
|
||||||
|
res.header('Content-Length', response.body.len.str())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep-Alive
|
|
||||||
if !has_connection {
|
if !has_connection {
|
||||||
|
if keepalive {
|
||||||
res.header('Connection', 'keep-alive')
|
res.header('Connection', 'keep-alive')
|
||||||
|
res.header('Keep-Alive', 'timeout=${config.keepalive_timeout}, max=${config.max_keepalive_req}')
|
||||||
|
} else {
|
||||||
|
res.header('Connection', 'close')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置响应体并发送
|
|
||||||
res.body(response.body)
|
res.body(response.body)
|
||||||
res.end() // 关键:必须调用 end() 来实际发送响应
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态码文本
|
// 获取状态码文本
|
||||||
|
|||||||
Reference in New Issue
Block a user