5.6 KiB
5.6 KiB
为什么要写这一篇
因为当时 vLLM 部署的 gpt-oss-120b 的模型,总是造成 vLLM 宕机,分析宕机时的崩溃日志是由于 vLLM 根据模型的回复内容调用结构化输出的工具,然后模型回复的内容跟结构后输出函数不兼容所以抛了 ValueError 导致 vLLM 的引擎进程退出,APIServer 与引擎的进程有心跳机制,发现引擎宕机了,所以自杀了。但是没有请求参数日志,不知道啥样的请求参数触发了结构化输出的功能。在 Open AI API 层面是有校验 response_format 的参数合法性的,不合法会直接拒绝。所以当务之急是先捕获请求参数,结合 vLLM 的宕机时间,尝试复现宕机的参数。
踩了很多坑
- 第一反应是看看能不能调整 vLLM 有没有开启打印请求参数的能力,vLLM 的版本是 v0.11.0,查看源码发现是没有的。
- 第二种就是使用抓包工具去实时抓包,选择了 tshark 它是 wireshark 的命令行版本。
- 在服务器后台运行了一晚上,天塌了!这叼东西会一直写临时文件,存储路径:/tmp/*.pcap。
- 它是抓包网卡的流量,在服务器部署了好几个大模型和 OpenAI API 端点。
- 而且有其他同事在压测,一直在并发调用 Open AI API,所以一晚上写了 300G+ 的临时文件,服务器直接告警了,运维挨批了。
终极方案
不修改 vLLM 的源码,也不用抓包工具,针对 vLLM 部署的 gpt-oss-120b 的 Open AI API 加一层反向代理,把请求参数写到 access_log 并轮转。
OpenResty
用这玩意儿是因为它可以在 nginx.conf 里面写 Lua 脚本,还提供了一系列的增强能力,比直接用 nginx 更省心。
nginx.conf
worker_processes auto;
error_log stderr warn;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
lua_need_request_body on;
log_escape_non_ascii off;
# 注意:这里不再使用 $time_local,而是用自定义变量 $log_time
log_format llm_audit '[$log_time] | $request_uri | $raw_body';
upstream llm_backend {
server 127.0.0.1:8080; # 修改成你的服务地址
keepalive 32;
}
server {
listen 8000;
server_name _;
client_max_body_size 100M;
client_body_buffer_size 128k;
client_body_in_single_buffer on;
location /v1/chat/completions {
set $log_time "";
set $raw_body "";
rewrite_by_lua_block {
local now = ngx.time()
local tm = os.date("*t", now)
ngx.var.log_time = string.format(
"%04d-%02d-%02d %02d:%02d:%02d",
tm.year, tm.month, tm.day,
tm.hour, tm.min, tm.sec
)
local body = ngx.var.request_body or ""
ngx.var.raw_body = body
-- 判断是否为流式请求
if body ~= "" then
local cjson = require "cjson.safe"
local ok, json = pcall(cjson.decode, body)
if ok and type(json) == "table" and json.stream == true then
ngx.exec("@stream")
return
end
end
ngx.exec("@normal")
}
}
# ========== 流式响应 ==========
location @stream {
internal;
access_log /hook/request.log llm_audit;
proxy_pass http://llm_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_cache off;
send_timeout 600s;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 600s;
proxy_socket_keepalive on;
}
# ========== 非流式响应 ==========
location @normal {
internal;
access_log /hook/request.log llm_audit;
proxy_pass http://llm_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering on;
proxy_cache off;
send_timeout 600s;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 600s;
proxy_socket_keepalive on;
}
}
}
/hook/request.log
/hook/request.log {
daily
rotate 5
size 20M
compress
delaycompress
missingok
notifempty
copytruncate
su root root # 如果 nginx 以非 root 用户运行,需匹配权限
}
docker-compose.yml
version: "3.8"
services:
openresty:
image: docker.1ms.run/openresty/openresty:jammy
container_name: openresty
environment:
- TZ=Asia/Shanghai
- http_proxy=
- https_proxy=
- HTTP_PROXY=
- HTTPS_PROXY=
- no_proxy=
- NO_PROXY=
ports:
- "28000:8000"
volumes:
# 宿主机这个目录可以查看 request.log 也必须包含 nginx.conf
- ./hook:/hook
# 日志轮转配置文件
- ./hook/hook-nginx:/etc/logrotate.d/hook-nginx
command: ["/usr/local/openresty/bin/openresty", "-c", "/hook/nginx.conf", "-g", "daemon off;"]
restart: on-failure:3