身为前端的我做了一次“集群部署”😄?

事情是这样的,公司自研了一个大模型服务,想要在内部试用,但问题是模型一次只能响应一个请求。为了能多人同时使用,就需要同时部署多个服务。于是乎,我拿到了5台8卡机器(3090),要把这个服务集群部起来。

最后花了两天时间解决了这个问题,在不同机器上部署了服务,并写了一个代理服务器来做负载均衡。下面是解决问题的过程,如果大家有更好的方案,欢迎评论指导交流~

服务示意图:

如何充分利用一台机器?

普通的Web服务可以通过多线程或多进程的方式并发处理多个请求,以充分利用服务器的CPU与内存资源。而本文中的服务不同,它的主要资源瓶颈在GPU。经过测试发现,运行这样一个模型需要2张卡,而整台机器一共是8卡。只运行单个服务时,通过nvidia-smi命令可以发现GPU根本用不满。

解决办法是在启动服务时指定GPU。比如CUDA_VISIBLE_DEVICES=0,1 python server.py可以限制服务只使用0和1两块GPU。这样我们就能在同一台机器上同时启动4个服务。

如何在多台机器上部署?

这个服务是一个使用Gradio开源工具部署的Python服务。最简单也是最笨的方法就是分别登录每台机器,下载模型,安装环境。

由于我自身对这个服务的环境还不太熟悉,只能照猫画虎先在另一台机器上尝试把这个服务运行起来,并把过程步骤记录下来。推荐使用miniconda来安装Python环境,对新手比较友好。

如果对安装过程比较熟悉了,就推荐封装成Docker镜像,这样可以方便在新机器上部署,避免繁琐的重复安装过程。

如何做负载均衡?

现在多个服务已经部署起来了,我们需要的是设置一个统一的入口,并将流量分发到各个服务上。

如果是简单的负载均衡,Nginx做起来还是很方便的。像这样就可以将请求按时间顺序依次分发给列表中的服务器。

nginx 复制代码
upstream backend {
  server backend1.example.com;
  server backend2.example.com;
}

但问题是我们的服务是有状态的 :前后的多个HTTP请求间有关联关系。这样一来上述的负载均衡方案就行不通了。比如第一个请求发送给了backend1,而第二个请求是依赖第一个请求的,却发送给了backend2,那么第二个请求就失败了。就像老生常谈的依赖Session的登录方案,如果多个服务的Session不是共享的,那么它们就无法一起工作。

然而Nginx还有其他的负载均衡策略来处理有状态服务问题,比如ip_hash策略,即通过计算将不同的IP映射到不同的服务地址,这样同一个IP地址的请求就只会打到同一个服务上。可问题是这个策略在内网服务上也行不通,一个原因是内网IP地址的前面部分都相同,Nginx默认的计算规则会把它们都映射到同一个服务地址上(需要修改默认计算规则)。另一个原因是公司的网关并没有把请求的真实地址暴露出来,这样我们就没法拿到客户端的真实IP地址。

那么如何标识各个客户端呢?我最后用的方法是在前端生成一个唯一ID,暂且叫它SessionID,并携带在每个请求上。使用Node.js实现一个反向代理服务器,它根据SessionID判断将请求分发到哪个服务上。

实现反向代理使用的是http-proxy库,主要的是如何根据sessionid来设置转发规则。

js 复制代码
const http = require("http");
const httpProxy = require("http-proxy");
const proxy = httpProxy.createProxyServer();

http
  .createServer(function (req, res) {
    // ... 根据sessionid获取服务地址
    const serverUrl = loadBalancer.newRequest(sessionid);

    // 代理请求到serverUrl
    proxy.web(req, res, { target: serverUrl });
  })
  .listen(7070);

转发规则为:在一个请求到来时,如果SessionID记录存在,就将请求发到对应的服务上 ;如果不存在,就挑选出一个最空闲的服务 (当前绑定会话数最少),并把SessionID与该服务地址记录下来。同时每个会话的有效期为5分钟,如果相同的sessionID超过5分钟没有新的请求,就将这个会话记录删除,避免不活跃的会话长时间占据某个服务。这样就能满足我们的业务需求了:相同客户端的请求打到同一个服务,同时不同客户端的请求均匀分配到各个服务上。

还有一个问题,浏览器如何让每个请求都携带上SessionID呢?由于我们是用的一个SDK来对接后端服务,所以没法直接修改请求代码。一个方法是使用Cookie,在请求时自动携带,但问题是前端服务与后端服务地址并不相同,没法实现。另一个方法就是重写请求方法。SDK里用到了fetch与EventSource,EventSource是不支持设置请求头的,所以我们可以把SessionID放到URL的查询参数里。重写请求的逻辑如下:

js 复制代码
  const originalFetch = window.fetch;
  window.fetch = function (url: RequestInfo | URL, options) {
    // 修改默认的url,将sessionID添加到查询参数里
    return originalFetch.call(this, addSessionIDToURL(url), options);
  };

  class CustomEventSource extends EventSource {
    constructor(url: string | URL, options?: EventSourceInit) {
      super(addSessionIDToURL(url), options);
    }
  }
  window.EventSource = CustomEventSource;

总结

提高请求并发量的一种方法就是把服务部署到多台机器上,然后用一个代理服务器做负载均衡。简单场景可以直接用Nginx默认的负载均衡策略,遇到更复杂的场景时就需要自定义转发逻辑了,前端也可以自己用Node.js来实现一个代理服务器。当然,本文中的方法还是很简单的,还有很多问题要处理,比如如何一键更新服务版本?如果运行过程中其中一个服务挂了怎么办?如何自动扩展?对于生产场景的集群部署,还是需要更专业的工具。

相关推荐
小屁不止是运维5 分钟前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构
求知若饥5 分钟前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
ZJ_.10 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营15 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood41 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端42 分钟前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248942 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5