为大型系统和长时间运行的后台任务构建。
图片来源:Ilias Chebbi 于 Unsplash几个月前,我承担了一个需要为媒体(音频)流构建基础设施的角色。但除了将音频作为可流式块提供外,还有长时间运行的媒体处理任务和一个广泛的RAG管道,用于转录、转码、嵌入和顺序媒体更新。以生产思维构建MVP使我们不断迭代,直到实现了一个无缝系统。我们的方法是整合功能和优先级的底层堆栈。
在构建过程中,每次迭代都是对即时且通常"全面"需求的响应。最初关注的是队列任务,Redis就足够了;我们只需发送并忘记。NEST JS框架中的Bull MQ让我们对重试、积压和死信队列有了更好的控制。在本地和生产环境中使用少量负载,我们正确处理了媒体流。我们很快就被可观察性的重担所困扰:
日志 → 任务记录(请求、响应、错误)。
指标 → 这些任务运行、失败、完成的频率/数量等。
追踪 → 任务在服务之间的路径(流程路径中调用的函数/方法)。
你可以通过设计API并构建自定义仪表板来解决其中一些问题,但可扩展性问题将会出现。事实上,我们确实设计了API。
面对管理复杂、长时间运行的后端工作流的挑战,其中故障必须可恢复,状态必须持久,Inngest成为了我们架构上的救星。它从根本上重塑了我们的方法:每个长时间运行的后台任务变成了一个后台函数,由特定事件触发。
例如,Transcription.request 事件将触发TranscribeAudio 函数。这个函数可能包含以下步骤运行:fetch_audio_metadata、deepgram_transcribe、parse_save_trasncription和notify_user。
核心持久性原语是步骤运行。后台函数在内部被分解为这些步骤运行,每个步骤包含最小的、原子级的逻辑块。
Inngest函数摘要:
import { inngest } from 'inngest-client';
export const createMyFunction = (dependencies) => {
return inngest.createFunction(
{
id: 'my-function',
name: 'My Example Function',
retries: 3, // 失败时重试整个运行
concurrency: { limit: 5 },
onFailure: async ({ event, error, step }) => {
// 在这里处理错误
await step.run('handle-error', async () => {
console.error('Error processing event:', error);
});
},
},
{ event: 'my/event.triggered' },
async ({ event, step }) => {
const { payload } = event.data;
// 步骤1:定义第一步
const step1Result = await step.run('step-1', async () => {
// 步骤1的逻辑
return `Processed ${payload}`;
});
// 步骤2:定义第二步
const step2Result = await step.run('step-2', async () => {
// 步骤2的逻辑
return step1Result + ' -> step 2';
});
// 步骤N:根据需要继续
await step.run('final-step', async () => {
// 最终逻辑
console.log('Finished processing:', step2Result);
});
return { success: true };
},
);
};
Inngest的事件驱动模型提供了对每个工作流执行的精细洞察:
依赖纯事件处理的缺点是,虽然Inngest有效地队列化函数执行,但事件本身并不在传统消息代理意义上内部队列化。在高流量场景中,由于潜在的竞争条件或如果摄取端点不堪重负而导致的事件丢失,这种明确事件队列的缺失可能会有问题。
为了解决这个问题并强制执行严格的事件持久性,我们实现了一个专用队列系统作为缓冲。
AWS简单队列系统(SQS)是我们的选择(尽管任何强大的队列系统都可行),考虑到我们在AWS上的现有基础设施。我们设计了一个双队列系统:主队列和死信队列(DLQ)。
我们建立了一个专门配置为直接从主队列消费消息的Elastic Beanstalk(EB)工作环境。如果主队列中的消息在EB工作器处理一定次数后失败,主队列会自动将失败的消息移至专用DLQ。这确保了如果事件无法触发或被Inngest拾取,不会永久丢失。这个工作环境与标准EB Web服务器环境不同,因为它的唯一责任是消息消费和处理(在这种情况下,将消费的消息转发到Inngest API端点)。
构建企业级基础设施的一个被低估且相当重要的部分是它消耗资源,而且它们是长时间运行的。微服务架构为每个服务提供可扩展性。存储、RAM和资源超时将发挥作用。例如,我们的AWS实例类型规格很快从t3.micro转移到t3.small,现在固定在t3.medium。对于长时间运行、CPU密集型的后台任务,使用微小实例进行水平扩展会失败,因为瓶颈是处理单个任务所需的时间,而不是进入队列的新任务数量。
任务或函数如转码、嵌入通常是CPU受限和内存受限的。CPU受限是因为它们需要持续、密集的CPU使用,而内存受限是因为它们通常需要大量RAM来加载大型模型或有效处理大文件或负载。
最终,这种增强架构,将SQS的持久性和EB工作环境的受控执行直接放在Inngest API的上游,提供了基本的弹性。我们实现了严格的事件所有权,消除了流量高峰期间的竞争条件,并获得了非易失性死信机制。我们利用Inngest进行工作流编排和调试功能,同时依靠AWS原语实现最大消息吞吐量和持久性。由此产生的系统不仅可扩展,而且高度可审计,成功地将复杂、长时间运行的后台任务转化为安全、可观察和容错的微步骤。
为布道构建Spotify。最初发表在Medium上的Coinmonks,人们通过突出显示和回应这个故事继续对话。