You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cloud Run部署后ReadableStream取消入队延迟5分钟原因咨询

问题:Google Cloud Run上SvelteKit流式API取消请求延迟5分钟触发cancel()

我基于adapter-node搭建了SvelteKit API端点,用ReadableStream流式传输行情数据。退出客户端路由时,流式传输应该停止。在SvelteKit开发环境(npm run dev,即vite dev)或Windows桌面容器(node build)中,以下代码运行正常:

onDestroy(async () => {
  await reader.cancel();   // 停止流式传输
  controller.abort();      // 中止fetch请求
});

但将Node容器构建部署到Google Cloud Run后,流式传输功能正常,但退出客户端路由时,API端点会持续流数据5分钟,之后才延迟触发ReadableStream的cancel()。

API代码(+server.js)

import { YahooFinanceTicker } from "yahoo-finance-ticker";

/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
  const { logging, symbols } = await request.json();

  const controller = new AbortController();
  const ticker = new YahooFinanceTicker();
  ticker.setLogging(logging);

  if (logging) console.log("api ticker", symbols);
  const stream = new ReadableStream({
    start(controller) {
      (async () => {
        const tickerListener = await ticker.subscribe(symbols);
        tickerListener.on("ticker", (quote) => {
          if (logging) console.log("api", JSON.stringify(quote, ["id", "price", "changePercent"]));
          controller.enqueue(JSON.stringify(quote, ["id", "price", "changePercent"]));
        });
      })().catch(err => console.error(`api listen exeption: ${err}`));
    },
    cancel() {   // 5分钟后才触发!!!
      console.log("api", "cancel: unsubscribe ticker and abort");
      ticker.unsubscribe();
      controller.abort();
    },
  });

  return new Response(stream, {
    headers: {
      'content-type': 'text/event-stream',
    }
  });
}

路由代码(+page.svelte)

const controller = new AbortController();
let reader = null;
const signal = controller.signal;

async function streaming(params) {
  try {
    const response = await fetch("/api/yahoo-finance-ticker", {
      method: "POST",
      body: JSON.stringify(params),
      headers: {
        "content-type": "application/json",
      },
      signal: signal,
    });

    const stream = response.body.pipeThrough(new TextDecoderStream("utf-8"));
    reader = stream.getReader();

    while (true) {
      const { value, done } = await reader.read();
      if (logging) console.log("resp", done, value);
      if (done) break;
      // ... 处理行情数据的逻辑
    }
  } catch (err) {
    if (!["AbortError"].includes(err.name)) throw err;
  }
}
// ...

原因分析

核心:Google Cloud Run的闲置连接超时机制

Google Cloud Run的负载均衡器默认会对闲置超过5分钟的HTTP连接强制断开,这就是你看到5分钟延迟的直接原因。本地/Windows容器环境没有这种平台级强制闲置超时,客户端取消请求后TCP连接会立即断开,服务器能快速感知并触发cancel()。

具体流程:

  1. 客户端调用reader.cancel()和controller.abort()后,浏览器发送TCP FIN包终止连接,但Cloud Run的负载均衡器不会立即将断开信号转发给容器实例。
  2. 服务器端的ReadableStream仍在持续enqueue数据,但数据无法送达客户端,连接进入"闲置"状态。
  3. 直到负载均衡器检测到连接闲置满5分钟,才会主动断开与容器的连接,此时服务器的ReadableStream才会触发cancel()回调。

代码层面的放大因素

你的API代码还存在两个细节问题,加剧了延迟:

  1. 未监听请求的中止信号:服务器端Request对象自带signal属性,你没有利用它主动监听客户端的中止请求,完全依赖ReadableStream的cancel()回调,而该回调仅在连接被断开时触发。
  2. 混淆了两个controller:ReadableStream的cancel()回调里的controller是流控制器,此时调用abort()已无意义,且你混淆了流控制器和自己创建的AbortController。

解决办法

1. 监听请求中止信号,主动停止数据推送

修改+server.js,监听请求的signal,一旦客户端中止请求,立即取消订阅并终止流:

export async function POST({ request }) {
  const { logging, symbols } = await request.json();

  const ticker = new YahooFinanceTicker();
  ticker.setLogging(logging);

  if (logging) console.log("api ticker", symbols);

  // 监听客户端的请求中止信号
  request.signal.addEventListener('abort', () => {
    console.log("api", "request aborted: unsubscribe ticker");
    ticker.unsubscribe();
  });

  const stream = new ReadableStream({
    start(controller) {
      (async () => {
        const tickerListener = await ticker.subscribe(symbols);
        tickerListener.on("ticker", (quote) => {
          // 提前检查请求是否已中止,避免无效入队
          if (request.signal.aborted) {
            ticker.unsubscribe();
            controller.close();
            return;
          }
          if (logging) console.log("api", JSON.stringify(quote, ["id", "price", "changePercent"]));
          controller.enqueue(JSON.stringify(quote, ["id", "price", "changePercent"]));
        });
      })().catch(err => console.error(`api listen exception: ${err}`));
    },
    cancel() {
      console.log("api", "stream canceled");
      ticker.unsubscribe();
    },
  });

  return new Response(stream, {
    headers: {
      'content-type': 'text/event-stream',
      // 可选:设置更短的Keep-Alive超时,帮助Cloud Run更快检测闲置连接
      'Connection': 'keep-alive',
      'Keep-Alive': 'timeout=30'
    }
  });
}

2. 优化客户端取消逻辑

调整客户端取消顺序,先中止请求再取消阅读器:

onDestroy(async () => {
  controller.abort(); // 先中止fetch请求
  if (reader) {
    await reader.cancel(); // 再取消阅读器
  }
});

3. 可选:调整Cloud Run连接超时

你可以在Cloud Run服务配置中设置更短的闲置超时(最小1分钟),让负载均衡器更快断开闲置连接。但更推荐通过代码监听中止信号的方式主动处理,比依赖平台超时更可靠。


内容的提问来源于stack exchange,提问作者voscausa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 16:35:20