弹性部署服务-Serverless 基础认识
共绩算力 suanli.cn 中的“弹性部署服务”产品属于一种 Serverless 产品。本文档将介绍和 Serverless 产品相关的基本概念,和在 Serverless 产品中部署和运行应用程序的最佳实践。
具体而言,本文将介绍无状态服务和有状态服务、负载均衡、API 交互、容器化等关键概念,说明选择此种实践方式对确保服务的可扩展性、可靠性和可维护性的价值。
1. 初步理解 Serverless
Serverless,直译为“无服务器”,指的是一种与服务器租赁不同的云产品形态。传统的云产品,不论是裸金属租赁,还是各大云厂商提供的云服务器(一般为虚拟机)租赁,又或是如 AutoDL 等平台提供的本质上是容器的云服务器租赁,都以机器的出租为核心。客户向平台采购设备,平台需快速响应用户订单,开出机器,同时保障机器本身的稳定性。客户拿到机器后,在上面自行部署应用程序,满足自身的业务需要。
Serverless 则不再以机器为核心,而是以应用程序为核心。在 Serverless 模式中,用户将应用程序托管至 Serverless 平台,由平台自身负责保障程序的运行环境。此时,用户只需关注应用程序本身,而无需关注服务器运维、资源调配、网络链路等事宜。一个最为理想化的 Serverless 平台界面上只应当有 2 个按钮:选择(上传)应用和发布应用。应用发布后,平台自动根据业务需要的设备条件匹配对应机器,自动根据业务流量进行无限制的自动弹性扩缩容。
当然,限于目前的条件,我们的产品相较理想 Serverless 平台仍多了许多和选择资源的种类、数目有关的面板和选项,但即使如此也能看到和 AutoDL 等云服务器租赁平台的一个明显区别:AutoDL 等平台直接向用户展示自己的机器列表以供选购,而我们只对机器的型号配置进行区别,单个选项卡背后是庞大的、同质的具体机器。
2. 无状态服务和有状态服务
2.1. 核心概念
几乎所有的应用程序都有维护业务状态的需求,而一个凭直觉编写的程序,常常会通过程序进程自身的内存或本地磁盘维护和用户业务相关的许多状态。例如,一个文生图的 WebUI 程序会维护一个和用户之间的会话页面同时将生成的图片文件保存于本地,一个网关身份鉴权程序会维护各个用户的登录状态,等等。对于这种通过程序进程自身内存或本地磁盘维护业务状态的应用程序,我们称为有状态服务。
编写有状态服务是自然而直观的,但是有状态服务存在一个核心的弊端:难以简单地以多副本(在我们平台中为多节点)方式运行。因为一旦以多副本方式运行,各个副本之间维护的状态互不相通,很可能导致业务逻辑出错。例如,如果一个副本保存有用户的登录状态,另一个副本未保有,那么当用户的请求发向另一个副本时,会发现自己的登录状态突然丢失,影响体验。而难以以多副本模型运行会进一步带来两个问题:
(1)难以实现高可靠性(高可用性)。只要进程异常退出,业务立即中断。
(2)难以便捷扩容缩容,业务流量超过单台服务器承载能力时难以继续扩容。
无状态服务解决了这一问题。所谓无状态服务,是指将业务中有状态的部分剥离至专业的有状态中间件(例如数据库,Redis,消息队列,云存储等)中,自身只保留无状态的部分。例如,对于网关身份鉴权程序,可以用 redis 维护各个登录会话的信息;对于许多业务程序,都会采用数据库存储用户状态。AI 推理程序一般也可以以无状态服务方式运行,例如 Ollama 的 11434 端口暴露的服务即为无状态服务。
但是,有些场合我们确实也需要使用有状态服务。用户需要进行密集 GUI 交互的服务常常不得不成为有状态服务,例如 ComfyUI 8188 端口暴露的调试工作流用的 UI 界面,又例如 JupyterLab 提供的在线编程调试环境。
我们平台最适合运行无状态服务,也为运行有状态服务提供了一些条件。后续将分别进行说明。
2.2. 无状态服务实践 A:无状态 API 和负载均衡
无状态服务的一个典型实践方式为提供无状态的 API,再结合适当的负载均衡策略。所谓无状态的 API,可以理解为这些 API 在单次调用中即可完成其业务功能,输入信息和结果信息均通过单次 http 的请求和返回信息来承载,各次请求之间不存在业务逻辑上的依赖性。例如,在文生图业务中通过单次调用生成一张图片;在语言模型中通过单次调用完成一轮对话等。
以典型的 ComfyUI 应用为例(可从平台的基础镜像直接启动),它在 8188 端口暴露了一个可供交互的 WebUI 服务,还在 3000 端口暴露了一个 API 服务。WebUI 服务的正常运行需要一系列具有前后依赖关系的请求,属于有状态服务,而原生的 API 服务由于在一次完整业务流程中需要按一定顺序调用/upload、/prompt 等多个接口,因此同样属于有状态服务。将其调整为无状态服务需要一些改造,我们提供了一个例子,详见容器化部署 Flux.1-dev 文生图模型应用。
负载均衡,是指将传入的网络流量或计算任务按一定规则分配到多个服务的副本上。在我们的平台中您会注意到,即使您为您的服务设定了多个副本(多个节点),对于服务监听的每个端口仍然只会产生 1 个链接,而不是和副本(节点)数目对应的链接。实际上,我们会把访问这一个链接的请求负载均衡到各个副本(节点)上。对于无状态服务,可以简单地采用轮询,随机或最小连接数等负载均衡策略。我们平台目前提供轮询策略。
有些平台(例如 runpod)还提供了一种更高级的负载均衡策略,即将请求放入一个队列进行缓存,以一个预定的并发数发送给后端服务。这种做法缓解了请求并发较高时服务程序被大并发打爆的问题。
2.3. 无状态服务实践 B:“拉模式”计算程序和消息队列
无状态服务的另一个典型实践方式为采用“拉模式”结合消息队列进行计算。在这种模式下,执行计算的程序并不需要暴露一个 http 服务的 API,而是主动从云上的消息队列中拉取任务,并将结果推送回云上的存储中。此种模式使得每个计算程序能完全自主控制自身负载,根本上杜绝了高并发时服务程序被打爆的问题,同时保障了每个任务一定能被完成。这个模式也是我们平台非常推崇的实践方式。
2.4. 有状态服务实践 A:单副本运行
有些时候我们不得不运行有状态服务,例如通过 jupyterLab 调试程序或训练 AI 模型,或使用 ComfyUI 的原生 API。此时,最简单的实践方式为让服务以单副本(节点数选择为 1)方式运行。此时所有的请求都将被发送到该副本,也就不存在之前所说的各种多副本引起的业务逻辑混乱问题。
对于通过 jupyterLab 调试程序或训练 AI 模型,或使用 ComfyUI 调试工作流等开发调试场景,一般需要保存开发调试得到的程序或者数据。值得注意的是,我们平台的节点本身并没有数据持久化功能,节点关闭、迁移、重启(如修改配置后节点重启)的过程中并不会保存在节点自身路径下保存的数据。为此,当采用本平台进行开发调试时,务必及时将数据下载回您的计算机上,或推送至云存储中,以免数据丢失。
我们同时提供了带有阿里云存储挂载功能的镜像容器化部署 JupyterLab可供使用。我们计划短期内上线节点镜像保存功能(与 AutoDL 的“保存镜像”功能一致,保存您的整个系统盘并推入镜像仓库)。同时,我们也计划上线云存储和 S3 挂载功能,这可以让您在节点内的特定路径下挂载带有持久化的云存储,或者访问您已经保存在各大云对象存储服务上的文件(我们会采用集群内的 P2P 分布式缓存技术,使得在启动缓存时读写速度接近本地磁盘)。
2.5. 有状态服务实践 B:自主搭建负载均衡和一致性哈希负载均衡
如果您的有状态服务只是在业务逻辑上存在先后的依赖关系,您可以考虑自主搭建负载均衡。具体的操作方法为,您同时发布多个单副本任务(推荐采用 API 以便捷的批量发布),每个任务的每个端口都会产生一个唯一的回传链接。您通过架设一个负载均衡,按业务逻辑关系的约束来把相关的请求全部转发至同一个任务中,由此实现有状态服务的负载均衡。
实际上,我们也在调研是否应该在平台上加入这种有可能与您的业务耦合的负载均衡功能,这被称为一致性哈希负载均衡。您可以配置它根据 HTTP 请求头的哈希、HTTP Cookie 的哈希、源 IP 地址的哈希或 HTTP 指定请求参数的哈希四种之一来把哈希值相同的请求全部负载均衡至同一个节点。
3. 容器化最佳实践
在 Serverless 弹性部署环境下,将应用封装为无状态的 Docker 镜像,乃是最佳实践的核心要点。无状态设计能够确保容器可在任意时刻创建、销毁或迁移,且不会对服务的连续性造成影响。尤其在多节点负载均衡部署场景中,无状态设计显得尤为关键。这是因为有状态服务有可能致使用户请求被路由至不同节点,进而引发状态信息的不一致或丢失,对服务的可靠性与数据准确性产生不良影响。
设计关键要点如下:
- 数据与应用分离:所有需要持久化的数据,均应存储于外部存储服务,而非容器内部。
- 配置外部化:借助环境变量或配置文件挂载的方式,达成配置的动态注入。
- 会话无关性:不依赖本地会话状态,所有状态信息均通过外部缓存或数据库进行管理。
- 健康检查:构建完善的健康检查机制,以支持自动恢复功能。