SSE 消费变慢以后:队列限 100 条,为什么内存仍可能涨?

本文探讨SSE流式传输中队列限100条却仍导致内存上涨的原因,指出关键在于事件字节预算而非条数限制。强调UTF-8编码下中文和emoji对字节数的影响,提出基于已编码字节的有界队列设计、预算归还验证机制,以及上游暂停、取消请求等背压策略。分析了传输层缓冲、TLS开销等隐性内存占用,并指出需分层明确所有者与预算边界。

流式网关按字节限制积压

大模型输出是一条长连接,客户端却不一定一直快。移动网络暂停几秒,网关如果继续接收上游输出,就必须有人存放这些还没发走的内容。把队列容量写成 100 条并不能限制内存:一条事件可能只有几十字节,也可能装着整段工具结果。

先确定队列里放的是什么

SSE 标准定义的是 UTF-8 事件流,以及 data: 等字段和事件分隔规则。它没有替业务方规定缓冲上限。网络读到的一段字节、一个模型增量和一条完整 SSE 事件也不是同一个单位。

我的做法是先在编码层生成完整事件字节,再交给有界发送队列。预算按已编码字节计,不能用 Java 字符串的 length() 代替。中文和 emoji 会让“字符数等于字节数”的估算失真。

事件入队与释放字节预算的流程

一个能检查预算归还的队列

下面只演示单连接的待发送队列。调用者拿走一项代表这部分缓冲预算已经移交给下一层;下一层仍需有自己的上限。

import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;

public class Demo {
    static class Queue {
        final int limit;
        int used;
        final ArrayDeque<byte[]> pending = new ArrayDeque<>();
        Queue(int limit) {
            if (limit <= 0) throw new IllegalArgumentException();
            this.limit = limit;
        }
        synchronized boolean offer(String event) {
            byte[] bytes = event.getBytes(StandardCharsets.UTF_8);
            if (bytes.length > limit - used) return false;
            pending.add(bytes);
            used += bytes.length;
            return true;
        }
        synchronized byte[] take() {
            byte[] bytes = pending.poll();
            if (bytes != null) used -= bytes.length;
            return bytes;
        }
    }
    public static void main(String[] args) {
        Queue q = new Queue(4);
        if (!q.offer("😀") || q.used != 4) throw new AssertionError();
        if (q.offer("a")) throw new AssertionError("overflow admitted");
        if (q.take().length != 4 || q.used != 0) throw new AssertionError();
        if (!q.offer("中文".substring(0, 1))) throw new AssertionError();
        if (q.offer("中文")) throw new AssertionError("oversized event admitted");
        System.out.println("UTF-8 byte cap enforced and budget returned");
    }
}

保存为 Demo.java,执行 javac --release 11 Demo.java、java Demo。用 emoji 检查四字节占用,再检查取出后预算归还以及大事件拒绝,比只往队列里塞十个短字符串更容易发现口径错误。

满了以后,谁来做决定

入队失败不能只记日志然后继续读。可以暂停上游读取,在可取消的 API 上请求取消,或在协议仍允许时返回明确的流中断状态。对 HTTP 客户端、代理和浏览器的具体实现,需要分别确认流控与取消接口;offer=false 本身不会取消任何模型计费。

还应给连接数、单事件大小、积压时间和全局缓冲设上限。上例在转换字符串时会临时分配字节数组,它限制的是队列内持有的有效载荷,不是 JVM 总堆内存。传输层写缓冲、TLS 缓冲和重复编码都可能额外占内存。本文没有进行浏览器慢读或十万连接压测,不能从这个队列推导并发容量。

背压的关键是把“暂时发不出去”变成有界状态,并能沿调用链停下来。先把每一层的所有者和预算写清楚,再谈吞吐量。

本地运行环境为 Java/javac 12.0.1,使用 --release 11 编译并执行了文中的检查;没有安装新依赖。该结果只覆盖示例列出的逻辑,不代表真实模型、浏览器或生产链路已经验证。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值