0 | 前言
最近,大模型很火,国内国外都在争相推出自己的大语言模型(LLM),有些可以公开调用 API,有些只能面向 B 端用户,各家 API 调用范式还不一致,如果单个去接的话,会花费大量的时间和经历去阅读API文档和配置 token。
由于业务需要,我们需要提供一个统一的大模型 API 聚合入口服务,对使用者屏蔽底层接入细节,统一接口规范,以及配置对应大模型的 token。
然而,大模型的提供方既有国内的(minimax,spark,文心一言等等),也有国外的(openai,claude,google-palm等等),如果将我们的聚合服务放到一个地方部署,就会有一个问题:如果部署在国内,那么很多大模型API调用的连通性就有障碍;如果部署在国外,那访问国内大模型API接口就会很慢。
所以为了解决连通性和连接速度慢的问题,就需要将服务在多地进行部署(国内国外),然后给予不同的调用入口来区分不同属地的服务。但是,这样会带来一些不便:
- 调用入口过多,会增加用户使用成本,徒增心智负担;
- 需要配套的物料也越多,如ssl证书(针对没有泛域名证书场景)
- 当有新属地部署时,需要新域名新证书,扩展性不够便利
所以针对这个痛点,提出了统一域名下的跨地域联合部署方案。
1 | 方案
在最初的版本,是一套代码分别在A地和B地同时部署,通过在接口文档里,显式规定用不同的域名来区分调用A地的模型还是B地的模型API。能解决问题,但是往往使用者有概率会忽略这部分内容,导致沟通成本的提升;同时,这种方式也不利于后续的扩展(如果有C地部署,那就需要新增一个新的域名访问)。
因此,在升级方案里,通过 nginx配置转发 + header自定义字段 的形式,来实现统一域名下的跨地域服务调用。 基本原理是:统一域名指向特定的 nginx 服务器,通过在请求的 header 里的自定义字段,将请求转发到不同地域。
2 | 实操
从上面的方案原理,可以看到,关键点就在 nginx 的配置:如何配置 nginx.conf 文件,来实现根据 header 里的特定 kv,来实现请求的转发。 这里直接上 nginx.conf 上的相关配置(提供了开箱即用的全量配置文件,但是重点在最后十几行,可以直接看最后的说明):
worker_processes 4;
error_log /error.log info;
events {
accept_mutex on;
multi_accept on;
use epoll;
worker_connections 4096;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$time_local $remote_user $remote_addr $host $request_uri $request_method $http_cookie '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time "$upstream_cache_status"';
log_format browser '$time_iso8601 $cookie_km_uid $remote_addr $host $request_uri $request_method '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time "$upstream_cache_status" $http_x_requested_with $http_x_real_ip $upstream_addr $request_body';
log_format client '{"@timestamp":"$time_iso8601",'
'"time_local":"$time_local",'
'"remote_user":"$remote_user",'
'"http_x_forwarded_for":"$http_x_forwarded_for",'
'"host":"$server_addr",'
'"remote_addr":"$remote_addr",'
'"http_x_real_ip":"$http_x_real_ip",'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"status":$status,'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_response_status":"$upstream_status",'
'"request":"$request",'
'"http_referer":"$http_referer",'
'"http_user_agent":"$http_user_agent"}';
access_log /access.log main;
sendfile on;
keepalive_timeout 120s 100s;
keepalive_requests 500;
send_timeout 60000s;
client_header_buffer_size 4k;
proxy_ignore_client_abort on;
proxy_buffers 16 32k;
proxy_buffer_size 64k;
proxy_busy_buffers_size 64k;
proxy_send_timeout 60000;
proxy_read_timeout 60000;
proxy_connect_timeout 60000;
proxy_cache_valid 200 304 2h;
proxy_cache_valid 500 404 2s;
proxy_cache_key $host$request_uri$cookie_user;
proxy_cache_methods GET HEAD POST;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
server_tokens off;
client_max_body_size 50G;
add_header X-Cache $upstream_cache_status;
autoindex off;
resolver 127.0.0.1:53 ipv6=off;
map $http_region $proxy_upstream {
cn1 server_cn_1;
us1 server_us_1;
default server_us_1;
}
upstream server_us_1 {
server server_us_1.com;
}
upstream server_cn_1 {
server server_cn_1.com;
}
server {
listen 80;
location / {
proxy_pass http://$proxy_upstream;
}
}
}
说明:
proxy_set_header Connection "upgrade";
如果需要支持 websocket 链接,需要配置此项;http_region
表示 http 请求的 header 里的 key=region 的值;map $http_region $proxy_upstream
表示是一个映射(可以理解成if else),即当 region=cn1 的时候, 转向 upstream=server_cn_1 所对应的 server=server_cn_1.com;当region=us1 的时候,转向 upstream=server_us_1 所对应的 server=server_us_1.com。
因此,以上 nginx 配置表示:监听80端口,通过判断 header 里的 region 值,将请求转发给不同的后端服务。
3 | 方案升级
在上面方案中,是通过用户在header里指定自定义字段region来实现请求的转发。这个方案有个比较大的痛点是:用户需要清楚调用的API和服务部署地域的mapping关系,然后设定region值。对用户来说,用不同的域名访问不同地域的同一个服务没有本质区别,依旧需要用户自己去决定如何调用。
所以,为了解决这个问题,我们对旧方案再做一次升级,目标是:用户无需自定义region字段,也无需掌握API和地域的mapping关系,只需要调用同一个域名,服务端自动去判断和转发请求。
这个可以通过请求的subpath来做判断:
4 | 实操升级
这里,还是借助nginx来转发请求。那么,问题就聚焦成了:
如何配置nginx.conf,来实现:同一个域名,根据不同的subpath来访问不同的服务地址?
这里还是需要借助lua脚本来实现这个业务逻辑,nginx.conf配置如下:
http {
...
map $the_region $proxy_upstream {
cn1 server_cn_1;
us1 server_us_1;
default server_us_1;
}
upstream server_us_1 {
server server_us_1.com;
}
upstream server_cn_1 {
server server_cn_1.com;
}
server {
listen 80;
server_name server.com;
location / {
set $the_region '';
access_by_lua_block {
local subpath = ngx.var.uri
ngx.log(ngx.ERR, "subpath=", subpath)
local cn_vendors = {"minimax", "xunfei"}
local us_vendors = {"openai", "claude", "google"}
for i, vendor in ipairs(cn_vendors) do
if string.find(subpath, vendor) then
ngx.var.the_region = 'cn1'
break
end
end
ngx.log(ngx.ERR, "the_region=",ngx.var.the_region)
}
proxy_pass http://$proxy_upstream;
}
}
}
代码逻辑解释:
-
server模块监听来自 server.com:80 的所有请求;
-
设置一个nginx的环境变量 the_region;
-
在lua脚本里,首先获得请求的subpath;
-
指定不同api供应商所在的区域,如minimax在cn,openai在us
- local cn_vendors = {"minimax", "xunfei"}
- local us_vendors = {"openai", "claude", "google"}
-
根据请求的subpath,循环遍历区域cn下的所有供应商,如果有包含关系,则设置 the_region = cn;(这里只有两个区域cn和us,所以只需要遍历一个区域即可)
-
然后通过proxy_pass转发请求,通过map方法选择对应的后端服务:
map $the_region $proxy_upstream {
cn1 server_cn_1;
us1 server_us_1;
default server_us_1; #兜底方案: 如果the_region字段为空,则默认走某一个后端服务
}
upstream server_us_1 {
server server_us_1.com;
}
upstream server_cn_1 {
server server_cn_1.com;
}