标签归档:稳定性

稳住!AIGC 架构中的排队系统与限流策略

 

我们见过那么多 AIGC 的酷炫功能,文生图、图生视频,……,是不是觉得 AI 特别强大?但你想过没,当成千上万的用户同时涌进来,都想让 AI 帮他们画画、写诗、做视频时,后台那些强大的模型和昂贵的计算资源(比如 GPU)会发生什么?

如果不加管理,它们很可能瞬间就被「挤爆」了!服务器宕机、用户请求失败、体验直线下降,甚至可能因为资源滥用导致成本失控。就像一家超火的餐厅,没有排队叫号系统,也没有门口保安/服务员控制人流,那里面肯定乱成一锅粥,谁也吃不好饭,对吧?

比如年初 DeepSeek 不能用的场景。

这就是为什么在设计 AIGC 架构时,排队系统和限流是绝对不能少的「定海神针」。它们就像餐厅的叫号系统和门口保安,确保整个服务流程既高效又有序,还能保护好咱们宝贵的「厨房」(计算资源)。

那当排队系统或限流策略出现的时候,我们的产品可能会有哪些表现呢?或者作为一个用户我们能看到哪些?

1. 产品表现

1.1 排队系统的「产品表现」

排队系统主要是为了处理「来不及马上做」的请求,但让用户知道「我已经收到你的指令了,正在处理中,请稍候」。所以它的表现形式通常和 等待、状态更新、异步通知 有关。

  1. 「转圈圈」与进度条:

    • 表现: 你提交一个请求(比如让 AI 生成一张图片),界面上出现一个加载动画或者一个不太精确的进度条,告诉你「正在处理中」。
    • 背后逻辑: 这时候你的请求很可能已经进入了后台的队列,正在等待 GPU 资源。这个动画就是前端在告诉你:「别急,后台在排队/处理呢!」 对于耗时较短、用户愿意在线等待的任务,这是最常见的表现。
  2. 明确的「排队中」或「处理中」状态 :

    • 表现: 对于耗时较长的任务(比如生成一个几分钟的视频),产品界面可能会更明确地显示任务状态,比如在一个「我的任务」列表里看到:「排队中」 (排队位置 5)”、「处理中 (还剩 3 minutes )」、「已完成」。
    • 背后逻辑: 这直接反映了后台队列和处理单元的状态。用户可以离开页面,稍后再回来看结果。
  3. 异步通知:

    • 表现: 你提交任务后,系统提示「任务已提交,处理完成后会通知您」。然后你该干嘛干嘛去。过了一会儿,你收到一个 App 内推送、邮件、短信或者其他形式的通知,告诉你「你的图片/视频生成好了,快来看看吧!」
    • 背后逻辑: 这是典型的异步处理 + 队列的应用。请求入队后,用户界面就响应了,处理完成后,通过通知机制把结果推给用户。用户体验非常好,不用傻等。
  4. 预估等待时间:

    • 表现: 有些产品会根据当前队列的长度和系统的处理速度,给你一个大概的等待时间估计,比如「预计等待时间:约 5 分钟」。
    • 背后逻辑: 系统监控着队列状态,并基于历史数据或当前负载进行预测,用来管理用户的预期。
  5. 暂时无法提交新任务:

    • 表现: 在极少数极端高峰期,如果队列积压得实在太严重,产品可能会暂时禁止提交新的同类型任务,并提示「系统繁忙,请稍后再试」。
    • 背后逻辑: 这是一种保护机制,防止队列无限增长导致系统崩溃或等待时间过长。虽然体验不好,但有时是必要的。

第 5 点的暂时无法提交新任务其实就是触发了限流策略。

1.2 限流的「产品表现」

限流是为了保护系统不被过多请求压垮,所以它的表现形式通常和 拒绝服务、错误提示、配额显示 有关。

  1. 「太快了,请稍后再试」:

    • 表现: 你在短时间内疯狂点击某个按钮,或者一个脚本高频调用某个 API,突然收到一个错误提示,比如「操作过于频繁,请稍后再试」、「API 调用次数已达上限」、「429 Too Many Requests」。
    • 背后逻辑: 你触发了限流规则(比如每分钟只能调用 10 次)。服务器拒绝了你超额的请求,并明确告诉你原因。
  2. 需要人机验证:

    • 表现: 在你进行某些敏感操作(如登录、发帖)过于频繁时,突然弹出一个图片验证码、滑动验证或者 reCAPTCHA,要求你证明自己是人类。
    • 背后逻辑: 这是一种常见的反爬虫、反刷量的限流手段。系统怀疑可能是机器人在高频操作,用人机验证来增加门槛,降低请求频率。
  3. 功能暂时不可用或降级:

    • 表现: 比如在一个免费的 AIGC 工具里,你可能发现每天只能生成 5 张图片,超过之后「生成」按钮就变灰了,或者提示你需要升级到付费版。或者,高峰期时,免费用户生成的图片分辨率会降低。或者,我们在使用一些大模型频繁的时候,会出现降智的情况。
    • 背后逻辑: 这是基于用户身份或套餐的限流策略。通过限制使用次数或降低服务质量来保证核心资源的可用性,并引导用户付费。
  4. 明确的配额/用量显示:

    • 表现: 在你的账户设置、API 控制台或者产品界面上,能清楚地看到你的使用额度,比如「本月 API 调用剩余次数:850/1000」、「今日图片生成次数:3/5」、「并行队列:5」。
    • 背后逻辑: 透明地展示限流规则和当前用量,让用户心里有数,可以合理规划自己的使用,避免突然被拒。

1.3 产品表现小结

  • 排队系统主要通过管理等待预期提供状态反馈来影响产品表现,目标是让耗时任务的处理过程更平滑、用户体验更好(即使需要等待)。
  • 限流则主要通过明确的拒绝或限制来影响产品表现,目标是保护系统、保证公平性、控制成本,有时也作为商业模式的一部分(区分不同用户等级)。

限流是入口保安,决定「你能不能进」 以及 「进来的速度有多快」。

排队系统是等候区管理员,负责管理「已经进来了但需要等待的人(任务)该怎么排队」。

2. 设计考量

咱们已经知道用户可能会看到「转圈圈」或者「稍后再试」了。但作为产品的设计者和开发者,在决定用什么样的排队和限流策略时,背后有一大堆门道需要考虑。这就像规划一场大型活动,既要保证大家玩得开心(用户体验),又要控制好场地容量和资源消耗(系统稳定性和成本),还得考虑 VIP 客人是不是有特殊通道(公平性与商业模式)。

2.1 目标第一:想达到什么效果?

这绝对是第一步,也是最重要的一步。我们引入排队和限流,到底是为了解决什么核心问题?

  • 保命要紧: 是不是首要目标就是防止系统在高并发下崩溃?比如像 DeepSeek 那样,突然涌入大量用户,如果没有任何防护,服务器可能直接就「躺平」了。这时候,强力的限流和能有效缓冲的队列就是救命稻草。
  • 控制成本: AIGC 服务,尤其是 GPU 推理,那是「吞金兽」。是不是想确保资源使用不超预算?限流可以直接控制调用总量,排队也能让我们更平稳地调度昂贵资源,避免为了应对短暂高峰而过度配置。
  • 用户体验: 我们希望用户等待多久是可接受的?是希望尽量快地给结果(可能需要更多资源),还是可以接受一定等待但保证任务最终完成?排队系统的设计(比如优先级、等待时间预估)和限流策略(是直接拒绝还是友好提示)都直接影响用户感受。
  • 公平性与差异化服务: 是不是所有用户都一视同仁?还是说付费用户、高等级用户应该有更高的请求频率、更短的等待时间?这就需要在限流和排队策略里体现出差异化。比如,给 VIP 用户更高的 QPS 限制和专属的优先队列。
  • 防止滥用: 是不是担心有人恶意刷接口、爬数据,或者用脚本进行大规模、低价值的调用?限流(特别是基于 IP、用户 ID 的精细化限流)和人机验证就是重要的防御手段。

想清楚了主要目标,后面的设计才有了方向。

2.2 量体裁衣:系统和业务长啥样?

没有放之四海而皆准的完美方案,我们的设计必须契合自身的特点:

  • 任务特性:

    • 耗时: AIGC 任务耗时差异很大。文生图可能几秒到几十秒,训练一个模型可能几小时甚至几天。耗时长的任务更适合异步队列处理。
    • 资源消耗: 不同任务对 CPU、GPU、内存的需求不同。瓶颈在哪里?是 GPU 显存容易爆,还是 CPU 计算跟不上?这决定了我们的限流和队列应该重点保护哪些资源。
    • 可并行度: 某些任务能很好地并行处理,而有些则不行。这影响了你可以同时从队列中取出多少任务来处理。
  • 流量模式:

    • 峰值与均值: 我们的应用流量是比较平稳,还是有明显的潮汐效应(比如白天高峰,晚上低谷)或者突发尖峰(比如搞活动、上热搜)?应对突发尖峰,限流的令牌桶算法和有足够缓冲能力的队列就比较有用。
    • 用户构成: 主要用户是 C 端普通用户,还是 B 端开发者通过 API 调用?他们的行为模式和容忍度是不同的。
  • 技术栈与基础设施:

    • 用的是云服务(AWS, Azure, GCP)还是自建机房?云服务通常自带成熟的队列(如 SQS, Pub/Sub)和网关限流功能,用起来方便。
    • 系统是单体架构还是微服务?微服务架构下,限流可能需要在网关层和具体服务层都做考虑。
  • 商业模式 (Business Model):

    • 免费增值模式?那免费用户的限流策略和付费用户的肯定不一样。
    • 按量付费?那精确的用量统计和限额就非常重要。

2.3 队列怎么玩:策略与选择

如果决定用排队系统,具体怎么设计呢?

  • 队列类型:

    • 先进先出 (FIFO): 最简单公平,按顺序处理。适合大部分场景。
    • 优先级队列: 可以让付费用户或紧急任务插队。实现起来复杂些,但能满足差异化服务需求。并且可以作为商业化的重要组成。
    • 延迟队列: 可以让任务在指定时间后才被处理,比如用于定时任务或重试。
  • 队列数量: 是所有任务都进一个大队列,还是按任务类型(文生图、文生文)、用户等级(免费、付费)分成多个队列?分队列可以实现更精细的控制和资源隔离,但管理更复杂。
  • 消息持久化: 请求(消息)进入队列后,是否需要保证即使系统重启也不会丢失?对于重要任务,需要选择支持持久化的消息队列(如 Kafka, RabbitMQ 持久化模式, SQS 标准队列)。
  • 死信队列: 如果一个任务处理失败(比如代码 bug、资源问题),尝试几次后还是不行,总不能一直卡在队列里吧?可以把它移到一个特殊的「死信队列」,后续再人工分析处理。
  • 消费者逻辑: 从队列里取任务来处理的「消费者」程序,它的并发数怎么控制?怎么处理任务失败和重试?怎么向用户更新状态?

2.4 限流怎么限:策略与选择

限流策略的设计同样关键:

  • 限流算法:

    • 令牌桶: 最常用,允许一定的突发流量(只要桶里有令牌),控制平均速率。比较灵活。
    • 漏桶: 强制平滑请求速率,不管来多少请求,处理速度是恒定的。对于需要严格控制下游压力的场景有用。
    • 固定窗口/滑动窗口计数器: 实现相对简单,但固定窗口有边界问题,滑动窗口更精确但实现和存储开销稍大。
  • 限流维度:

    • 按用户/API Key: 最常见,实现差异化服务和防止单一用户滥用。
    • 按 IP 地址: 可以限制匿名用户或来自特定 IP 的恶意流量,但可能误伤使用共享 IP(如 NAT、VPN)的正常用户。
    • 按接口/服务: 对不同的 API 或服务设置不同的限制,保护核心或资源消耗大的接口。
    • 按模型: 模型云厂商中最常见,不同的模型,资源不同,限制的大小也不同。
    • 全局限流: 对整个系统设置一个总的入口限制。
  • 限流位置:

    • 网关层: 统一入口,实现方便,可以拦截大量非法请求。
    • 服务层: 更靠近业务逻辑,可以做更精细的控制。
    • 中间件/库: 在代码层面集成限流逻辑。
  • 超出限制后的行为:

    • 直接拒绝: 返回错误码(如 429 Too Many Requests)。最简单直接。
    • 排队等待: 把超出的请求放入一个短暂的等待队列(注意,这和我们前面说的主要用于异步任务的队列不同,这里的队列更像是限流器内部的一种缓冲机制),如果短时间内有处理能力空出来就处理。体验稍好,但增加了复杂性。
    • 降级处理: 比如返回一个缓存的、旧的结果,或者调用一个计算量更小的备用模型(一般称为降智)。

2.5 用户体验:别让用户一脸懵

技术实现很重要,但最终是为用户服务的。怎么让这些机制不那么「讨人嫌」?

  • 透明度: 尽可能让用户知道发生了什么。

    • 等待时: 显示明确的状态(排队中、处理中)、进度条(即使不精确)、预估时间。
    • 被限流时: 返回清晰、友好的错误信息,说明原因(「操作太频繁」、「今日额度已用完」)以及何时可以重试。提供文档说明 API 的限流规则。
    • 配额显示: 在用户界面或账户中心清晰展示当前用量和总额度。
  • 预期管理: 通过预估等待时间、明确的配额等,让用户对可能发生的等待或限制有心理准备。
  • 友好的错误处理: 即使必须拒绝,也要给用户明确的指引,而不是冷冰冰的错误代码。

2.6 监控与迭代:持续观察与调整

最后,别忘了,这些都不是一成不变的。

  • 监控指标: 你需要实时监控关键指标:

    • 队列: 队列长度、消息平均等待时间、消息积压数量、消费者处理速率、死信队列数量。
    • 限流: 请求总数、被限流的请求数、触发限流的规则/用户/IP 分布、响应时间。
    • 系统: CPU/GPU 使用率、内存占用、网络带宽、错误率。
  • 告警: 当指标超过阈值时(比如队列长度过长、限流拒绝率过高),需要及时收到告警。
  • 调优: 根据监控数据和业务变化,不断调整限流阈值、队列优先级、消费者数量等参数。可能需要进行 A/B 测试来验证不同策略的效果。

3. 技术实现

聊完了「是什么」和「怎么想」,现在就到了「怎么做」的环节了。要把排队系统和限流策略落地,咱们得选对工具、用对方法。市面上成熟的方案很多,就像工具箱里的各种扳手和螺丝刀,得挑顺手的、合适的才行。

3.1 排队系统的技术选型与实现

要搞个靠谱的排队系统,我们通常不会自己从零开始造轮子(那太复杂了!),而是会选用一些成熟的消息队列中间件。这些中间件就像是专业的「排队调度中心」。

常见的「排队调度中心」有哪些?

  1. RabbitMQ:

    • 特点: 老牌劲旅,功能非常全面,支持多种消息协议(比如 AMQP),路由规则特别灵活(可以搞得很复杂,比如根据消息内容决定发给哪个处理单元),社区成熟,文档丰富。
    • 适合场景: 需要复杂的消息路由逻辑、任务分发、对消息可靠性要求高的场景。比如,不同类型的 AIGC 任务(文生图、图生文)需要交给不同的处理集群。
    • 上手: 相对来说,配置和管理比 Kafka 简单一些。
  2. Kafka:

    • 特点: 设计目标就是超高吞吐量!它更像是一个「可持久化的日志流」,数据来了就顺序写盘,消费者可以从任意位置开始读。天生适合分布式、高并发写入和读取。
    • 适合场景: 需要处理海量请求(比如用户行为日志、实时数据流)、对消息顺序有要求、能容忍稍高一点的延迟(相比内存队列)、需要消息回溯(重新消费)的场景。AIGC 的请求量如果巨大,或者需要记录详细的请求日志流,Kafka 是个好选择。
    • 上手: 集群部署和运维相对复杂一些。
  3. Redis:

    • 特点: Redis 本身是个内存数据库,速度飞快!可以用它的 List 数据结构(LPUSH/RPOP)模拟简单队列,或者用更现代的 Streams 数据类型(5.0 版本后的功能,功能更强,支持消费组、持久化等,有点像迷你版 Kafka)。
    • 适合场景: 对性能要求极高、队列逻辑相对简单、可以接受一定的数据丢失风险(如果 Redis 挂了且没做持久化或主从)、或者你系统里已经重度使用 Redis,不想引入新组件。很多限流实现也会用到 Redis。
    • 上手: 如果你熟悉 Redis,用起来非常方便。
  4. 云服务商提供的 MQ:

    • 特点: 云平台提供的托管服务。我们不用关心服务器运维、扩容缩容,按量付费,和云上其他服务(如 Lambda 函数、云存储)集成得非常好。
    • 适合场景: 应用部署在云上,想省心省力,快速搭建排队系统。它们通常提供标准队列(保证至少一次送达)和 FIFO 队列(保证顺序)。
    • 上手: 非常简单,控制台点几下或者几行 SDK 代码就能用起来。

怎么选? 简单说:

  • 要灵活路由、功能全面?考虑 RabbitMQ。
  • 要超高吞吐、能接受一定复杂性?考虑 Kafka。
  • 要简单快速、或者已有 Redis?试试 Redis Streams/List。
  • 在云上、想省事?用云厂商的 MQ 服务。

实现时要注意啥?

  • 生产者: 就是你接收用户请求的那部分服务(比如你的 Web API)。它需要把用户的请求(比如“画一只猫”)包装成一个**消息 (Message)**,扔进选好的队列里。这个消息里得包含足够的信息,比如任务类型、用户输入的提示词 (Prompt)、用户 ID、可能还有优先级等。
  • 消费者 这是真正干活的「工人」(比如运行 AIGC 模型的 GPU 服务器上的程序)。它会不断地从队列里拉取(Pull)或接收推送(Push)过来的消息,然后根据消息内容执行任务(比如调用模型生成图片)。

    • 并发控制: 你可以启动多个消费者实例来并行处理队列里的任务,提高效率。但要控制好数量,别把 GPU 资源撑爆了。
    • 任务确认: 消费者处理完一个任务后,一定要告诉队列:“这个活我干完了(Ack)!”这样队列才会把这个消息彻底删除。如果消费者处理失败或者挂了,没来得及确认,队列通常会把这个消息重新交给其他消费者处理(保证任务不丢失)。处理不了的坏消息,可以考虑扔进死信队列
  • 消息体设计: 消息里具体放啥内容得设计好。是直接把图片数据放进去(不推荐,太大),还是放一个指向存储的链接?用户 ID 要不要带上,方便后续通知?

3.2 限流的技术选型与实现

限流的实现方式也很多样,可以在不同的地方「设卡」。

在哪儿「设卡」?

  1. 网关层: 这是最常见的做法。在所有请求进入你系统的大门口(比如 API Gateway)就进行拦截。

    • 工具: Nginx (自带 limit_req 模块)、Kong、Apigee、AWS API Gateway、Google Cloud API Gateway 等。这些网关通常都内置了限流功能,配置一下就行,对后端服务是透明的。
    • 优点: 统一管理,效率高,能把大量不合规的请求挡在外面,保护后端服务。
    • 缺点: 可能不够灵活,无法基于非常复杂的业务逻辑来限流。
  2. 应用层/代码层: 直接在你的后端服务代码里加入限流逻辑。

    • 工具:

      • 各种语言的库/框架: 几乎每种流行的编程语言都有现成的限流库。比如 Java 的 Guava RateLimiter,Go 的 golang.org/x/time/rate,Python 的 ratelimiter 或集成在 Web 框架(如 Django/Flask)的插件,Node.js 的 express-rate-limit 等。
      • Web 框架中间件 (Middleware): 很多 Web 框架允许你插入中间件,在处理请求前后执行逻辑,非常适合放限流代码。
    • 优点: 最灵活,可以根据任意业务逻辑(比如用户等级、请求参数)来定制限流策略。
    • 缺点: 需要在每个需要限流的服务里都实现或引入,可能有点重复工作;性能开销比网关层高一点。

限流状态存哪儿?(尤其是在分布式系统里)

限流算法(比如令牌桶、滑动窗口)需要记录当前的状态(比如桶里还有多少令牌、窗口内有多少请求)。在分布式环境下(你有多个服务实例),这个状态必须是共享的。

  • Redis: 绝对的主力! 因为它:

    • 快: 基于内存,读写速度非常快,对限流这种高频操作很友好。
    • 原子操作: Redis 提供了像 INCR (原子加一)、EXPIRE (设置过期时间) 这样的原子命令,这对于并发环境下的计数和状态更新至关重要,避免了竞态条件。很多复杂的限流逻辑可以通过 Lua 脚本 在 Redis 服务端原子执行,保证一致性。
    • 适合分布式: 所有服务实例都可以访问同一个 Redis 来读写限流状态。
  • 内存 如果你的服务是单实例部署,或者限流逻辑不要求跨实例共享状态,那么用内存记录状态是最快的。但服务一重启状态就没了,也不适用于分布式系统。
  • 数据库: 理论上也可以,但数据库通常比 Redis 慢,对于限流这种需要快速响应的操作,可能会成为性能瓶颈,所以不太常用。

算法怎么用代码大概实现一下?(概念性)

  • 令牌桶:

    1. 每个用户/API Key 在 Redis 里对应一个 Key,存当前令牌数 (token count) 和上次添加令牌的时间戳 (last refill timestamp)。
    2. 请求来了,先根据时间差计算需要补充多少令牌(不能超过桶容量),更新令牌数和时间戳。
    3. 检查当前令牌数是否大于 0。
    4. 如果大于 0,令牌数减 1,允许请求通过。
    5. 如果等于 0,拒绝请求。
    • 关键: 上述步骤最好用 Lua 脚本在 Redis 里原子执行,防止并发问题。
  • 滑动窗口日志:

    1. 每个用户/API Key 在 Redis 里对应一个 Sorted Set。
    2. 请求来了,用当前时间戳作为 score,请求 ID (或时间戳+随机数) 作为 member,添加到 Sorted Set (ZADD)。
    3. 移除窗口之外的旧记录 (ZREMRANGEBYSCORE,移除时间戳小于 “当前时间 – 窗口大小” 的记录)。
    4. 获取当前窗口内的记录数量 (ZCARD)。
    5. 如果数量小于阈值,允许请求;否则拒绝。
    • 关键: 同样,这些操作最好也封装在 Lua 脚本里保证原子性。

3.3 整合到 AIGC 流程

现在我们有了排队和限流的工具,怎么把它们串到咱们的 AIGC 服务流程里呢?想象一下一个典型的流程:

  1. 用户请求抵达: 比如用户在网页上点了“生成图片”按钮,请求发往后端。
  2. 入口限流 (网关/服务): 请求首先经过限流器。检查这个用户/IP 的请求频率是否超标。

    • 超标: 直接返回错误(如 429 Too Many Requests),流程结束。
    • 未超标: 请求继续往下走。
  3. 请求处理与任务提交: 后端服务(比如 Web API)接收到请求,进行一些基本校验,然后把需要执行的 AIGC 任务(包含提示词、参数等)封装成一个消息。
  4. 进入队列: 这个消息被发送到消息队列 (MQ) 中。此时可以告诉用户「任务已提交,正在排队/处理中」。
  5. 任务排队等待: 消息在队列里按照策略(FIFO、优先级等)排队,等待有空闲的「工人」。
  6. 工人处理任务 (消费者): 后台的 GPU 工作节点(消费者)从队列里拉取消息。
  7. (可选) 资源访问限流: 如果这个工人需要访问外部资源(比如调用另一个受限的 API),它内部可能也需要遵守相应的限流规则。
  8. 执行 AIGC 任务: 工人调用模型,执行计算密集型的生成任务。
  9. 存储结果: 生成结果(比如图片 URL、生成的文本)被存储到数据库或对象存储中。
  10. 任务完成确认: 工人向消息队列发送确认信号 (Ack)。
  11. 通知用户: 通过某种方式(比如 WebSocket 推送、回调 URL、或者用户主动查询状态)告知用户任务已完成,并提供结果。

从整个流程来看,限流主要作用在入口处(步骤 2),有时也可能在资源消耗端(步骤 7)。而排队系统则承担了削峰填谷、异步解耦(步骤 4-6, 10) 的核心作用。

技术实现这块,选型和细节非常多,但核心思路就是这样:根据我们的需求(性能、可靠性、成本、复杂度)选择合适的 MQ 和限流工具/库,然后把它们合理地嵌入到服务流程中,再配上完善的监控。这样,我们的 AIGC 应用就能更从容地应对用户的热情啦!=

看到这里,你可能会问:排队和限流是不是有点像?它们都管理请求,但侧重点不同,而且经常一起工作:

  • 限流是「准入控制」:决定一个请求能不能进入系统处理流程。它关注的是「速率」和「总量」,防止系统被瞬间打垮。
  • 排队是「流量整形」和「缓冲」:处理那些已经被允许进入,但暂时无法立即处理的请求。它关注的是「平滑度」、「异步性」和「可靠性」。

想象一下:

  1. 请求先到达限流器(保安)。保安检查你的「票」(比如 API Key)以及当前人流速度,决定是否放你进去。
  2. 如果你被允许进入,但「处理台」(GPU)正忙,你就被引导到排队系统(等候区)等待。
  3. 等处理台空闲了,就从队列里叫下一个号来处理。

这样一套组合拳下来,AIGC 系统就能在汹涌的请求浪潮中保持稳定、高效、公平地运行啦!

4. 小结

对于 AIGC 架构而言,排队系统和限流策略并非「可选件」,而是保障系统稳定性可用性公平性成本效益 的核心组件。在设计阶段就必须充分考虑:

  • 识别瓶颈: 哪些环节是资源密集型的?(通常是模型推理)
  • 定义策略: 基于业务目标(用户体验、成本、公平性)设定合理的限流阈值和排队机制(如优先级)。
  • 选择工具: 根据技术栈、性能需求、运维复杂度选择合适的限流组件和消息队列。
  • 监控与调优: 持续监控队列长度、等待时间、限流触发次数、系统负载等指标,并根据实际运行情况不断调整策略。

以上。

关于复杂系统的思考:从语雀服务中断想到的

在 10 月 23 日,语雀服务出现了一次重大故障,中断时间超过 7 小时。具体来说,这次故障的起因是在执行系统升级操作期间,新的运维升级工具出现了 bug,导致华东地区生产环境的存储服务器被误下线。由于存储系统使用的机器类别过于陈旧,无法直接将其上线,这导致了恢复过程时间延长。尽管语雀的工程师们立刻进行了恢复工作,但由于数据量大,恢复方案有限,恢复过程耗时 4 小时,数据检验耗时耗时 2 小时。这次故障给用户带来了严重的影响,也让我们重新审视关于本次故障带来的思考。

我将整个复盘报告输入给到 chatGPT,其给我的回复如下:

从语雀的这次事故中,我们可以学到以下几个关键的教训:

  1. 重视运维工具的测试和质量保证:在这次事故中,新的运维升级工具的 bug 是导致故障的主要原因。这说明,即使是运维工具,也需要进行足够的测试和质量保证。在实际使用之前,应该在相似的环境中进行充分的测试,确保其稳定性和安全性。
  2. 灾备和恢复系统的重要性:即使有了数据备份,恢复仍然可能需要花费大量时间,这会导致服务中断时间过长。因此,除了定期备份数据外,还需要建立有效的灾备和恢复系统,以便在灾难发生时,能够快速恢复服务。
  3. 灾难恢复演练的必要性:定期的灾难恢复演练可以帮助我们在真实的灾难发生时,更快地恢复服务。通过演练,我们可以发现并解决恢复过程中可能存在的问题,提高恢复的效率。
  4. 高可用架构的重要性:高可用架构可以保证在部分系统组件发生故障时,整个系统仍能继续运行。在这次事故中,由于存储系统的故障,导致了整个服务的中断。如果有高可用架构,即使某个组件发生故障,也可以通过其他的组件提供服务,从而减少服务中断的可能性。
  5. 灾难通信的重要性:在灾难发生后,应该及时向用户通报情况,说明故障的原因,修复的进度,以及预计的恢复时间。这样可以增加用户的信任度,减少用户的不安。在这次事故中,语雀做了很好的灾难通信,及时向用户报告了故障的详情,以及修复的进度。
  6. 持续改进和学习的重要性:每次故障都是一次学习的机会。通过分析故障的原因,我们可以找出问题,改进系统,避免类似的故障再次发生。在这次事故中,语雀提出了一系列的改进措施,这表明他们有持续改进和学习的意识。

但是从复杂系统的角度来看这个事件:复杂系统的性质使得它们在面临故障时,能够以我们意料之外的方式表现出来。尽管我们可能已经在设计和实施阶段做了很多工作来确保系统的稳定性,但是在实际运行中,仍然会出现我们没有预见到的问题。

高尔在讨论复杂系统时提到:「具备故障防备能力的系统出故障的原因恰恰就是故障防备能力的失灵」。这是因为在设计防备系统时,我们往往会基于我们对系统的理解来设计,但是实际上,系统的行为可能会超出我们的理解范围。当系统的行为超出我们的预期时,我们的防备措施就可能会失效。

这与这次语雀服务故障的情况非常相似。尽管语雀设计了一套备份恢复机制,但是在实际的故障中,由于对系统的理解不足,这套机制并没有发挥出应有的效果,反而延长了恢复时间。

在许多情况下,一些复杂系统的故障并不是单一的、孤立的事件,而是由一系列的因素相互作用、相互影响的结果。这通常包括了系统设计、运维策略、人为错误等多个方面。

具备故障防备能力的系统,其设计和运营目标是提供连续、稳定的服务,即使在面临内部错误或外部攻击等影响时也能持续运行。这种系统通常包含冗余的组件、自动故障切换机制、灾难恢复策略等措施,以保证系统的正常运行。

然而,这些故障防备能力本身可能成为系统故障的源头,原因可能有以下几点:

  1. 复杂性故障防备能力会增加系统的复杂性,复杂性本身就可能带来故障。例如,冗余的组件需要保持同步,如果同步机制出现问题,可能导致数据不一致,进而产生故障。

  2. 人为错误故障防备能力的维护和操作需要人工参与,人为操作错误可能导致防备能力失灵。如语雀故障案例中,运维工具的 bug 就是人为因素导致的。

  3. 预期和实际的差距:故障防备能力的设计和实施都是基于对可能故障的预期,但实际发生的故障可能超出了预期,导致防备能力无法应对。

面对一个每天都在演化的复杂系统,我们应该做些什么呢?

面对复杂,我们第一个能想到的原则应该就是 KISS 原则了。

「KISS」 原则是 「Keep It Simple, Stupid」 的缩写,这是一种广泛应用于工程、设计和决策过程中的设计原则。其最主要的理念就是保持事物的简单性。

KISS 原则主张:

  • 避免不必要的复杂性
  • 优先选择简单、清晰和直观的解决方案
  • 不要添加不必要的特性或功能
  • 简单的设计更易于维护、理解和修改

这个原则并不是说所有的事物都必须以最简单的方式来完成,而是鼓励我们在设计和决策过程中,优先考虑最简单和最直接的方法,除非有充分的理由去选择更复杂的方案。

简单性是一个非常重要的设计目标,因为它可以降低错误的可能性,提高系统的可靠性,减少维护的难度,提高效率,以及其他许多好处。

我们如果应用了以下的一些建议,应该可以往 KISS 原则靠近一些:

  1. 明确需求:首先明确我们试图解决的问题是什么。这有助于避免在设计和实现过程中添加不必要的功能或复杂性。

  2. 简化设计:在设计过程中,始终问自己是否可以把事情做得更简单。避免过度设计,只实现真正需要的功能。

  3. 模块化:把大问题分解成小问题,每个小问题都可以用简单的方式解决。这样,每个模块都保持简单,而整个系统则由这些简单模块组成。

  4. 避免不必要的优化:过早的优化可能会增加复杂性。除非确定优化会带来显著的好处,否则最好先实现功能,然后再考虑优化。

  5. 使用已有的解决方案:如果有已经存在的工具或库可以满足我们的需求,那就使用它,而不是从头开始创建。这样可以降低复杂性,同时节省时间。

  6. 代码和设计的清晰性:代码应该易于理解,设计应该直观。这不仅提高了可维护性,也让其他团队成员更容易理解你的工作。

  7. 定期回顾和重构:随着时间的推移和需求的变化,我们可能需要调整或简化现有的设计。定期回顾我们的代码和设计可以帮助我们找出可以简化的地方。

作为一个技术管理者应该作些什么呢?我认为最应该做的是拒绝那些让系统变得复杂的不合理需求,守好技术实现的大门。

面对复杂系统,我们需要对系统有深入的理解,尽可能地简化系统,避免不必要的复杂性。通过模块化设计,将复杂系统分解为相对独立的部分,每个部分都可以单独理解和测试。并且设计出健壮的错误处理和恢复机制,同时也需要进行持续的监控和回顾。只有这样,才能提升系统的稳定性,减少服务中断的可能性。

研发管理之生产环境的变更管理

2017 年,Amazon S3 服务在美国东部区域发生了大规模的故障,影响了许多依赖于 S3 的服务和应用。这次故障的根本原因是维护人员在执行一个操作时,错误地将更多的服务器脱机,这超过了系统设计的冗余容量,导致了该区域S3的部分子系统开始备份,进一步扩大了故障的影响。

2018 年 10 月 31 日 GitHub 通过官方博客发布了 2018 年 10 月 21 日「挂掉」的事件分析。GitHub 指出此次事件发生的原因是在 10 月 21 日 22:52UTC 进行日常维护——更换发生故障的 100G 光学设备时导致美国东海岸网络中心与美国东海岸数据中心之间的连接断开。更具体地 GitHub 分析,虽然两地的连接在 43 秒内恢复,但这次短暂的中断引发了一系列事件,这才导致了长达 24 小时 11 分钟的服务降级。

2020 年 7 月,Cloudflare 的 DNS 服务遭受了大规模的中断,影响了许多依赖其服务的网站。该故障的原因是 Cloudflare 的路由器中的一个错误配置。

以上是在网上搜索各大平台的故障描述,可以看到这些故障都是由于生产环境的变更导致的,有些是网络设备变更,有些是配置变更,有些是维护人员在线上执行了某个操作…… 如此种种。

这些问题最终都是开发人员通过系统化的建设,一个坑一个坑的填完了,但是当我们带着一个团队急速前进时,可能来不及做这些系统化的建设,此时通过流程对生产环境的变更进行管理,快速解决或规避一些问题以控制线上故障的出现。流程能保证的是我们做事的下限

在做生产环境变更管理流程之前一定要明晰生产环境的概念和范围,在团队内达成共识,然后再去做流程,以规避因为对生产环境的概念和范围不一致,导致的误解和乌龙。

1 生产环境的概念

生产环境,也称为「产品环境」或「线上环境」,是指实际运行并对外提供服务的环境。这个环境中的软件版本、配置和数据都应该是最新的、经过充分测试的,以保证系统的稳定性和性能。线上环境需要提供24小时不间断的服务。

一个应用或环境是否属于线上环境,主要取决于它是否直接对外提供服务。例如,如果一个应用接收并处理来自最终用户的请求,那么它就是线上环境的一部分。同样,如果一个环境中的数据被用于生产服务,那么这个环境也应该被视为线上环境。

通常,生产环境包括以下 4 个部分:

  • 硬件资源:例如服务器、网络设备、云服务中的硬件部分等;
  • 软件资源:包括操作系统、数据库、中间件、云服务中的软件部分等;
  • 应用程序:实际运行的业务代码和与之相关联的部分,如 CI/CD 工具;
  • 数据:实际的用户数据和业务数据。

定义了生产环境,从生产环境衍生出生产环境的变更。

2 生产环境变更的概念和分类

生产环境的变更是指在生产环境中对任何一部分进行的修改,包括应用程序的更新、配置的修改、硬件设施的更换等。而线上故障大多来源于生产环境的变更,对生产环境的变更进行管理和控制,在较大程度上可以减少对系统稳定性产生影响。

生产环境的变更从其组成出发,再加上外部流量,可以分为 5 类:

2.1 硬件资源变更

硬件资源变更主要包含所有与物理硬件和云服务硬件配置相关的更换、升级或维护。

  • 硬件规格调整:例如,升级处理器(CPU)、扩充内存(RAM)、更换硬盘等。
  • 网络设备更新:包括替换路由器、交换机或进行固件更新等。
  • 存储设备变动:磁盘扩容、存储设备更换等场景会包含在内。
  • 云服务硬件调整:如云计算服务中服务器规格的调整、增减虚拟机实例、增减 POD 数、网络设备变更等。

2.2 软件配置变更

软件配置变更涵盖了所有与操作系统、数据库、中间件以及云服务软件设置的修改。

  • 操作系统参数调整:比如,优化操作系统性能通过调整系统参数等。
  • 数据库设置变动:例如,数据库参数调整或修改索引,导致数据库负载提升甚至锁表导致的无法读写等线上事故。
  • 中间件配置更新:如修改消息队列的设置,调整缓存策略导致缓存穿越或者缓存雪崩等线上问题时有发生。
  • 云服务软件配置调整:包括了云服务的安全规则更新、网络配置变动等。

2.3 应用程序变更

应用程序变更主要包含了所有与业务代码和将业务代码发布到生产环境的 DevOps 工具的更改。

  • 代码变更:代码变更是我们最最常见的变更类型,主要是通过修改代码改变应用程序并通过发布系统发布到生产环境。这也是我们变更管理中风险最大的地方,因为变更的人,变更的位置和逻辑等都是不确定的。除了正常的发布变更,应用的回滚也是应用变更的回滚,因为其改动了线上的应用。实际中,代码变更在逻辑上包括了修复 bug、优化性能、增加新功能等,都需要对应用程序代码进行更新。
  • 配置变更:指应用系统的配置变更,一般是通过配置系统来变更,触发线上应用的热更新或滚动,配置如果是写死在代码中,会变为代码变更。
  • 依赖库更新:实际业务中需要对应用程序所依赖的库或框架进行更新,有些更新可能需要改代码,或者代码本身已经是这么个逻辑,在构建的时候就会带出去。
  • DevOps 工具变更:例如,升级工具版本,或者对某些功能进行调整。
  • DevOps 工具配置变更:如发布脚本中对于 dev 或 prod 环境的配置修改等等,都是高风险操作,线上有着血淋淋的故障。

2.4 数据变更

数据变更很少被人当作变更处理,因为很多时候就是正常的业务操作,如管理后台的批量操作这些,但是这些批量操作如果发生在高峰时期,可能会对线上业务带来较大的影响,轻则速度变慢,重则线上事故。数据变更可以分为线上数据的清理、迁移、更新等操作。

  • 数据清理:如定期删除过期数据,清理无效数据节省成本等等,基于不同的目的,将数据清除,除了可能会影响性能,如果清理错了,将会导致用户丢失,以至用户资产的损坏,这将会是很大的线上事故。
  • 数据迁移:如将数据从一个数据库迁移到另一个数据库,或者因为业务升级,数据需要从一种逻辑迁移到另一种逻辑,除了负载压力,更多的可能是数据错乱或者数据丢失,这两种情况都会引发用户投诉。
  • 数据更新:如前面说的管理后台批量更新,或者上线新模块在已有的数据库上初始化数据等等,这种最多的情况是其引发的 DB/ES 等存储类中间件的高负载导致服务的异常或引发线上事故。

2.5 流量变更

流量变更和上面四个类别不同,其是从外部来看的,主要包含了流量变化的情况。这里不考虑攻击类的流量。流量一般是带来高负载,或者由高负载引发的链路异常或雪崩,从而导致整体服务异常或线上事故。

  • 负载调整:如对调整负载均衡策略,更改流量路由等由于考虑不周引发某些节点过热或流量过大,引发级联反应,从而出现异常或事故。
  • 后台投放或大型促销活动:如没有提前通知的后台投放或大型促销活动、特殊事件导致的流量激增,可能需要进行负载调整或资源扩容等,如果某些链路存在容量上限,或者达到扩容的上限,就会引起线上异常或事故。

以上 5 种类型画成简单的脑图,如下:

图片

3 变更管理

变更管理是指以可控的方式对线上的服务、配置或基础设施进行变更,从而减少变更对业务和服务质量的影响,快速处理变更可能带来的问题,提升系统的稳定性。

变更管理,咱们从组织和流程机制两个方面来看。

3.1 组织

一个事情要想有力的执行下去,一定是有一个组织来保障事情的整体节奏和推进。

从组织的角度,整个变更管理的组织成员角色可以分为以下几种:

  • 变更管理主导者:一般来说,这个角色通常由技术团队的高级管理者来担任,并且这个事情它本身是一个从上向下的事情,需要更上层的负责人来推进事项,一般是 CTO 或 VP,或质量的负责人。他们需要确保变更管理策略和流程的成功实施,对整个变更管理过程负责,并需要对所有的变更决策拥有最后的决定权。

  • 变更管理委员会:这是一个跨部门的团队,包括来自业务、开发、运维、质量保证等部门的代表。他们的任务是评审即将进行的变更,评估其对业务的影响,以及是否符合公司的战略目标。他们还负责改进变更管理流程,并对变更管理的效果进行监督和评估。在实际的实施过程中可能没有正式的名称叫委员会,可能叫 XXX 质量小组,或者就是某个研发中心的管理团队兼任。

  • 变更经理:这个角色负责确保变更管理流程的日常运行,是实际的变更控制推进者,他们需要协调变更的执行,确保所有的变更都通过了必要的评审,已经准备好了回滚计划,并且变更后的效果已经得到了验证。在实际的实施过程中,变更经理大概率是某个 Leader 或者质量的负责人,或者 PM。

  • 变更执行人:这个角色负责协调变更的具体实施,例如安排变更的时间,通知相关人员,收集反馈,等等,一般这种变更由一线的开发,SRE 来做,也有大一些的公司有专门的职位。

3.2 流程机制

变更管理有一个理想状态的标准流程,其大概如下:

  1. 变更申请:在我们的流程中可能是创建发布记录,或者申请紧急发布
  2. 变更评审:变更评审主要是检查变更过程是否完备,以降低变更的风险,其包括如下内容:
    1. 就绪分析:材料是否完备,人员、设备、软件、网络是否就绪,测试是否达到上线要求等。
    2. 风险分析:架构、性能、业务、合规等方面的风险评估,变更内容是否属于需求范围,变更是否可控。
    3. 重要程度:变更属于一般、重要、紧急、标准哪一种。
    4. 变更审查:内容是否满足业务需求,内容是否通过测试,测试是否全面、有效。
    5. 应急管理:变更步骤、应急方案、回滚方案、应急预案是否完备。
    6. 变更实施:变更计划时间如何安排,发布及回退操作步骤是否完备,自动化步骤情况。
    7. 变更验证:变更涉及的业务、技术验证方法与时间安排。
  3. 变更审批:相关负责人对于变更评审的结果进行确认,并审批通过。
  4. 变更执行
    1. 根据发布计划执行发布操作,一般应该有一个灰度的过程;
    2. 验证线上功能并回归主流程;
    3. 持续灰度,观察用户直到灰度完成。
  5. 变更验收
    1. 对发布的功能进行验收,对于影响范围内的功能进行验收,对业务主流程进行回归验收;
    2. 留守,并观察日志、监控服务负载等,这个操作是为了及时发现验收检查漏掉的问题,或者及时处理隐藏的问题,以减少变更后产生的问题对线上业务的影响。

我们做这个流程是形式上的安慰,还是僵化的惯性,还是能真正地解决问题,是我们在做这个流程以及执行这个流程中需要着重思考的问题。

在变更前,即我们变更线上环境前需要自己做 Code Review,以及交叉的检查,以尽量减少问题流转到后面的操作中,节省问题的处理成本。

在标准流程之外,另外还有两个特别重要的点,一个是周知,一个是盘点。

周知在形式上可以是邮件、群通知、群消息,通过这些方式,将研发自己做的前面所定义的线上变更周知给相关方:「我们做了 XXX 操作,可能会影响 XXX ,你们看下对你们自己有没有影响,如果有相关告警可以找我。」

变更虽然有流程,但是流程保证的是过程,对于过程中的问题通过变更盘点的方式,阶段性回顾问题和成果,在变更委员会中达成共识并继续迭代。在每次迭代的时候我们可以问自己如下的一些问题:

  • 与上次回顾相比,变更对线上的影响有更严重吗?有影响到稳定性吗?
  • 变更流程是否有什么问题,是否需要专项来解决?是否应该解决?
  • 上次回顾安排的事项落实了吗,对应的情况如何,是否有更新到流程或系统中?

以上的回顾操作我们建议以某个管理系统来承载,并且这个管理系统是带有通知等功能,以更好的将变更相关的信息周知出去。当然,也可能直接共享文档+群通知来搞。

4 小结

上面的变更管理只是流程方面的,对于实际中变更管理最好是能在类似于 DevOps 系统中的落地,最少也是在项目管理或流程系统中落地。

生产环境的变更管理是一项复杂而重要的任务。通过对生产环境的良好理解,结合有效的组织、流程和系统工具,我们可以实现对生产环境变更的有效管理,保证业务的稳定运行,提升用户的使用体验,同时也提升了我们自身的运维效率和质量。这也是我们做研发管理必须要完成的任务之一。