<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Worker on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/worker/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Fri, 13 Mar 2026 13:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/worker/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>多 Worker 分布式渲染：从 HTTP 轮询到 RocketMQ</title>
      <link>https://ftmao.com/blog/posts/roketmq/</link>
      <pubDate>Fri, 13 Mar 2026 13:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/roketmq/</guid>
      <description>&lt;h1 id=&#34;多-worker-分布式渲染从-http-轮询到-rocketmq&#34;&gt;多 Worker 分布式渲染：从 HTTP 轮询到 RocketMQ&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;我开发的方糖猫平台的核心链路最后一步是视频渲染。用户提交生成任务后，系统需要把故事板、配音、素材合成为最终的视频文件。这个步骤 CPU/GPU 密集、耗时长、失败率高，单机渲染很快成为瓶颈。这篇文章会复盘我们从 HTTP 轮询架构迁移到 RocketMQ Push 架构的全过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一渲染任务的特性&#34;&gt;一、渲染任务的特性&lt;/h2&gt;
&lt;p&gt;AI 视频平台的渲染任务有几个典型特征：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;特性&lt;/th&gt;
					&lt;th&gt;影响&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;耗时长&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;一个 60 秒视频渲染可能需要 30 秒到 5 分钟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;资源密集&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;CPU/GPU 占用高，单机并发有限&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;失败率高&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;素材缺失、字体缺失、内存不足、网络超时都可能失败&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;不均衡&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;不同分辨率、不同时长任务耗时差异大&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;需要重试&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;失败后最好换一台 Worker 重试，而不是一直卡在原机&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些特性决定了渲染不能放在主服务里同步执行，必须用异步任务队列 + 多 Worker 架构。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二第一代架构http-轮询&#34;&gt;二、第一代架构：HTTP 轮询&lt;/h2&gt;
&lt;h3 id=&#34;21-设计思路&#34;&gt;2.1 设计思路&lt;/h3&gt;
&lt;p&gt;最早为了快速验证产品，我们用了最简单的方案：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant A as API 服务
    participant W as Worker 服务
    A-&amp;gt;&amp;gt;W: POST 任务
    A-&amp;gt;&amp;gt;W: 查询状态
    W--&amp;gt;&amp;gt;A: 渲染完成&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;API 服务把渲染任务通过 HTTP POST 发给某个 Worker&lt;/li&gt;
&lt;li&gt;Worker 开始渲染，API 服务定时轮询状态&lt;/li&gt;
&lt;li&gt;渲染完成后，Worker 回调 API 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-代码示例&#34;&gt;2.2 代码示例&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; requests
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; time
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RenderService&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;submit&lt;/span&gt;(self, task_id, render_params):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        worker_url &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pick_worker()  &lt;span style=&#34;color:#75715e&#34;&gt;# 简单轮询选一个&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        requests&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;post(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;worker_url&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/render&amp;#34;&lt;/span&gt;, json&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;render_params, timeout&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 轮询等待完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; _ &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;300&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sleep(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; requests&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;worker_url&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/status/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;task_id&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;json()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; status[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; status[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;23-问题很快暴露&#34;&gt;2.3 问题很快暴露&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Worker 负载不均&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;轮询分配无法感知 Worker 当前负载&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;单点故障&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;某个 Worker 挂了，上面的任务就卡住&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;扩缩容麻烦&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;新增 Worker 需要改 API 服务配置&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;失败重试复杂&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;需要 API 服务记录每个任务发给了哪个 Worker&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Windows 开发环境问题&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;开发环境只有一台机器，无法模拟多 Worker&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这个方案支撑了产品早期验证，但当日均任务量超过几百条后，稳定性和扩展性都跟不上了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三第二代架构rocketmq-push&#34;&gt;三、第二代架构：RocketMQ Push&lt;/h2&gt;
&lt;h3 id=&#34;31-为什么选-rocketmq&#34;&gt;3.1 为什么选 RocketMQ&lt;/h3&gt;
&lt;p&gt;我们对比了几种消息队列：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;th&gt;缺点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;HTTP 轮询&lt;/td&gt;
					&lt;td&gt;简单&lt;/td&gt;
					&lt;td&gt;负载不均、失败处理差&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RabbitMQ&lt;/td&gt;
					&lt;td&gt;功能全面&lt;/td&gt;
					&lt;td&gt;高可用配置复杂&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Kafka&lt;/td&gt;
					&lt;td&gt;吞吐高&lt;/td&gt;
					&lt;td&gt;消费端管理重，延迟不如 RocketMQ&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RocketMQ&lt;/td&gt;
					&lt;td&gt;负载均衡好、顺序消息、延迟消息、监控完善&lt;/td&gt;
					&lt;td&gt;部署稍重&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选择 RocketMQ 主要是因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原生支持 Push Consumer，自动负载均衡&lt;/li&gt;
&lt;li&gt;NameServer 架构适合弹性扩缩容&lt;/li&gt;
&lt;li&gt;消息重试、死信队列机制完善&lt;/li&gt;
&lt;li&gt;团队对阿里系中间件比较熟悉&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-新架构设计&#34;&gt;3.2 新架构设计&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;API 服务&amp;#34;] -- &amp;#34;发送渲染任务&amp;#34; --&amp;gt; T[&amp;#34;RocketMQ Topic: video-render&amp;#34;]
    T --&amp;gt; W1[&amp;#34;Worker-1&amp;#34;]
    T --&amp;gt; W2[&amp;#34;Worker-2&amp;#34;]
    T --&amp;gt; WN[&amp;#34;Worker-N&amp;#34;]
    W1 --&amp;gt; D[&amp;#34;渲染完成后更新数据库&amp;#34;]
    W2 --&amp;gt; D
    WN --&amp;gt; D
    D --&amp;gt; S[&amp;#34;SSE 推送进度给用户&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 服务只负责发消息，不关心任务被哪个 Worker 消费&lt;/li&gt;
&lt;li&gt;Worker 从 RocketMQ 拉取任务，自动负载均衡&lt;/li&gt;
&lt;li&gt;渲染完成后 Worker 更新数据库，并通过 SSE 通知前端&lt;/li&gt;
&lt;li&gt;新增 Worker 只需启动新实例，自动加入消费组&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-生产者实现&#34;&gt;3.3 生产者实现&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; rocketmq.client &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Producer, Message
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Producer(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;render-producer-group&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_name_server_address(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;localhost:9876&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;submit_render_task&lt;/span&gt;(task_id: str, params: dict):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    body &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;: task_id,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;params&amp;#34;&lt;/span&gt;: params,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;submitted_at&amp;#34;&lt;/span&gt;: time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;time()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Message(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video-render&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_keys(task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_tags(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;normal&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_body(body)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    producer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;send_sync(msg)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;34-消费者实现&#34;&gt;3.4 消费者实现&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; rocketmq.client &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; PushConsumer, ConsumeStatus
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RenderConsumer&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; PushConsumer(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;render-consumer-group&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_name_server_address(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;localhost:9876&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;subscribe(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video-render&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;, self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;on_message)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;on_message&lt;/span&gt;(self, msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;body)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            task_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            params &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;params&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 幂等检查&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;is_task_processed(task_id):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 更新任务状态为处理中&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update_task_status(task_id, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;processing&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 执行渲染&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;render(params)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 更新任务状态为完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update_task_status(task_id, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            logger&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;error(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Render failed: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;RECONSUME_LATER
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四关键设计细节&#34;&gt;四、关键设计细节&lt;/h2&gt;
&lt;h3 id=&#34;41-幂等处理&#34;&gt;4.1 幂等处理&lt;/h3&gt;
&lt;p&gt;消息队列可能重复投递，渲染任务又是耗时操作，必须做幂等：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;is_task_processed&lt;/span&gt;(task_id: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;基于数据库状态做幂等判断&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    task &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; db&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_task(task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; task&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;status &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;processing&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;如果任务已经是 &lt;code&gt;processing&lt;/code&gt; 状态，说明可能被其他 Worker 在处理，需要进一步判断心跳时间，避免任务永远卡住。&lt;/p&gt;
&lt;h3 id=&#34;42-心跳与任务认领&#34;&gt;4.2 心跳与任务认领&lt;/h3&gt;
&lt;p&gt;每个 Worker 在处理任务时会定时更新心跳：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;update_task_status&lt;/span&gt;(task_id, status, result&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    db&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;execute(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        UPDATE render_task
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        SET status = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;            result = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;            worker_id = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;            heartbeat_at = NOW()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        WHERE task_id = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;, (status, result, WORKER_ID, task_id))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;如果某个 Worker 挂了，心跳超过阈值的任务会被标记为失败，重新进入队列重试。&lt;/p&gt;
&lt;h3 id=&#34;43-失败重试与死信队列&#34;&gt;4.3 失败重试与死信队列&lt;/h3&gt;
&lt;p&gt;RocketMQ 支持消费失败自动重试，默认 16 次，间隔递增。如果还是失败，消息进入死信队列：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 消费失败时返回重试&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;RECONSUME_LATER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;对于死信消息，我们单独处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分析失败原因&lt;/li&gt;
&lt;li&gt;通知运营或用户&lt;/li&gt;
&lt;li&gt;部分场景自动降级（比如降低分辨率重试）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-并发控制&#34;&gt;4.4 并发控制&lt;/h3&gt;
&lt;p&gt;每个 Worker 同时能处理几个任务，取决于机器配置。我们通过线程池控制：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; concurrent.futures &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; ThreadPoolExecutor
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 根据 CPU/GPU 核心数设置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;executor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ThreadPoolExecutor(max_workers&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;on_message&lt;/span&gt;(self, msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    future &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; executor&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;submit(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;handle_render, msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 注意：PushConsumer 是阻塞回调，这里需要等待结果&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; future&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;result()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; result&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;实际部署中，我们更推荐一个 Worker 进程只消费一个任务，通过启动多个 Worker 进程来提升并发。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五windows-开发环境兼容&#34;&gt;五、Windows 开发环境兼容&lt;/h2&gt;
&lt;h3 id=&#34;51-问题&#34;&gt;5.1 问题&lt;/h3&gt;
&lt;p&gt;RocketMQ 在 Windows 上部署比较麻烦，开发环境有时候只有一台机器。为了不影响开发效率，我们设计了一个降级策略：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;生产环境：RocketMQ Push Consumer
开发环境：HTTP 轮询模拟（单 Worker）&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-抽象层设计&#34;&gt;5.2 抽象层设计&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TaskConsumer&lt;/span&gt;(ABC):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RocketMQConsumer&lt;/span&gt;(TaskConsumer):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;生产环境使用&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;HttpPollConsumer&lt;/span&gt;(TaskConsumer):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;开发环境使用&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 根据配置切换&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;consumer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; RocketMQConsumer() &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; env &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;production&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; HttpPollConsumer()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这样业务逻辑完全一致，只是消息消费方式不同。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六监控与告警&#34;&gt;六、监控与告警&lt;/h2&gt;
&lt;h3 id=&#34;61-关键指标&#34;&gt;6.1 关键指标&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指标&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;队列堆积量&lt;/td&gt;
					&lt;td&gt;判断是否需要扩容 Worker&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;任务平均处理时间&lt;/td&gt;
					&lt;td&gt;评估渲染性能和资源需求&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;任务成功率&lt;/td&gt;
					&lt;td&gt;健康度核心指标&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Worker 在线数量&lt;/td&gt;
					&lt;td&gt;消费组健康状态&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重试次数分布&lt;/td&gt;
					&lt;td&gt;发现系统性问题&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;62-告警规则&#34;&gt;6.2 告警规则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;队列堆积超过 100 条，持续 5 分钟 → 扩容提醒&lt;/li&gt;
&lt;li&gt;任务成功率低于 95% → 人工介入&lt;/li&gt;
&lt;li&gt;Worker 数量异常减少 → 服务异常告警&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七演进效果&#34;&gt;七、演进效果&lt;/h2&gt;
&lt;p&gt;迁移到 RocketMQ 后，渲染系统的稳定性明显提升：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指标&lt;/th&gt;
					&lt;th&gt;HTTP 轮询&lt;/th&gt;
					&lt;th&gt;RocketMQ Push&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;平均等待时间&lt;/td&gt;
					&lt;td&gt;X 分钟&lt;/td&gt;
					&lt;td&gt;Y 分钟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;任务成功率&lt;/td&gt;
					&lt;td&gt;X%&lt;/td&gt;
					&lt;td&gt;Y%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Worker 利用率&lt;/td&gt;
					&lt;td&gt;不均&lt;/td&gt;
					&lt;td&gt;均衡&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扩容成本&lt;/td&gt;
					&lt;td&gt;需要改配置&lt;/td&gt;
					&lt;td&gt;启动实例即可&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;失败重试&lt;/td&gt;
					&lt;td&gt;自建逻辑&lt;/td&gt;
					&lt;td&gt;原生支持&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;（待补充真实数据）&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八踩过的坑&#34;&gt;八、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;81-消息体过大&#34;&gt;8.1 消息体过大&lt;/h3&gt;
&lt;p&gt;早期的渲染参数里包含完整的素材 URL 列表，消息体很大。RocketMQ 默认单条消息限制 4MB，我们遇到过消息发送失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：消息体只传 &lt;code&gt;task_id&lt;/code&gt; 和关键参数，Worker 消费后自己从数据库/redis 拉取完整参数。&lt;/p&gt;
&lt;h3 id=&#34;82-消费组订阅不一致&#34;&gt;8.2 消费组订阅不一致&lt;/h3&gt;
&lt;p&gt;不同 Worker 用同一个消费组但订阅了不同 Tag，导致消息分发混乱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：严格规范消费组命名和订阅规则，每个消费组只负责一个 Topic。&lt;/p&gt;
&lt;h3 id=&#34;83-任务卡住没有超时释放&#34;&gt;8.3 任务卡住没有超时释放&lt;/h3&gt;
&lt;p&gt;某个 Worker 处理到一半进程挂了，任务状态一直是 &lt;code&gt;processing&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：增加心跳检测，心跳超时的任务自动重置为 &lt;code&gt;pending&lt;/code&gt;，重新入队。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;视频渲染是 AI 视频平台里最典型的「重任务」场景，必须用异步队列 + 分布式 Worker 来解决。几个关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不要过早优化，但也不能过度将就&lt;/strong&gt;。HTTP 轮询能验证产品，但流量起来后必须升级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RocketMQ 的 Push Consumer 很适合这种场景&lt;/strong&gt;，负载均衡和重试机制省去很多自建成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幂等和心跳是必须的&lt;/strong&gt;，否则任务会丢或卡住。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控要跟上&lt;/strong&gt;，队列堆积和成功率是最核心的两个指标。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做类似的异步任务处理系统，欢迎交流消息队列选型和 Worker 调度经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
