如何使用同一个域名访问多地部署的同一个服务? | nginx应用实战-1

3,300 阅读5分钟

0 | 前言

最近,大模型很火,国内国外都在争相推出自己的大语言模型(LLM),有些可以公开调用 API,有些只能面向 B 端用户,各家 API 调用范式还不一致,如果单个去接的话,会花费大量的时间和经历去阅读API文档和配置 token。

由于业务需要,我们需要提供一个统一的大模型 API 聚合入口服务,对使用者屏蔽底层接入细节,统一接口规范,以及配置对应大模型的 token。

然而,大模型的提供方既有国内的(minimax,spark,文心一言等等),也有国外的(openai,claude,google-palm等等),如果将我们的聚合服务放到一个地方部署,就会有一个问题:如果部署在国内,那么很多大模型API调用的连通性就有障碍;如果部署在国外,那访问国内大模型API接口就会很慢。

所以为了解决连通性和连接速度慢的问题,就需要将服务在多地进行部署(国内国外),然后给予不同的调用入口来区分不同属地的服务。但是,这样会带来一些不便:

  1. 调用入口过多,会增加用户使用成本,徒增心智负担;
  2. 需要配套的物料也越多,如ssl证书(针对没有泛域名证书场景)
  3. 当有新属地部署时,需要新域名新证书,扩展性不够便利

所以针对这个痛点,提出了统一域名下的跨地域联合部署方案。

1 | 方案

在最初的版本,是一套代码分别在A地和B地同时部署,通过在接口文档里,显式规定用不同的域名来区分调用A地的模型还是B地的模型API。能解决问题,但是往往使用者有概率会忽略这部分内容,导致沟通成本的提升;同时,这种方式也不利于后续的扩展(如果有C地部署,那就需要新增一个新的域名访问)。 image.png

因此,在升级方案里,通过 nginx配置转发 + header自定义字段 的形式,来实现统一域名下的跨地域服务调用。 基本原理是:统一域名指向特定的 nginx 服务器,通过在请求的 header 里的自定义字段,将请求转发到不同地域。

image.png

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;
      }
    }
}

说明:

  1. proxy_set_header Connection "upgrade"; 如果需要支持 websocket 链接,需要配置此项;
  2. http_region 表示 http 请求的 header 里的 key=region 的值;
  3. 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来做判断:

image.png

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;
      }
    }
}

代码逻辑解释:

  1. server模块监听来自 server.com:80 的所有请求;

  2. 设置一个nginx的环境变量 the_region;

  3. 在lua脚本里,首先获得请求的subpath;

  4. 指定不同api供应商所在的区域,如minimax在cn,openai在us

    • local cn_vendors = {"minimax", "xunfei"}
    • local us_vendors = {"openai", "claude", "google"}
  5. 根据请求的subpath,循环遍历区域cn下的所有供应商,如果有包含关系,则设置 the_region = cn;(这里只有两个区域cn和us,所以只需要遍历一个区域即可)

  6. 然后通过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;
}