【最后203篇系列】027 基于消息队列的处理架构

起因

之所以写这篇文章,主要是梳理一下进展。因为同时研究好几块内容,切换起来要点时间。这次也是因为协作的同事们把各自的分工都搞定了,瓶颈反而在我自己这里,哈哈。

除了帮自己思路恢复过来,我觉得这方法可能也有些借鉴意义,所以写下来。

场景

一直以来,我这边总是以AI服务的方式交付成果。微服务是一种很好的形式,实现了不同语言之间的无缝配合。后来随着时间推移,微服务越来越多,我都已经迷失了。最近完成了集中和迁移,主要的服务大概20个,还有一些零散的就不管了。

每一个AI服务的定位是解决复杂问题,例如实体识别,或者agent flow这样的。所以每个服务实际上都非常复杂,然后开发时复杂,运维时特别困难。

很多时候为了快速开发和实现,都采用了紧耦合的方式封在了一个服务中。

问题分析

之所以采取紧耦合,大抵是时间紧,任务急,且属于开放式任务–不确定实现路径,不确定效果,甚至无法确定商业价值。过去有过一些简单而实用的服务,比如实体识别,语义向量计算等,这些服务的确在对外时已经封装到只需要一层了。

但是训练模型的过程本身是比较复杂的,我曾经做过流程梳理和简化,但太久不用,也都忘的七七八八了。所以这些服务本身没有啥问题,但是要再次量身定制新的模型需要很长的时间才能再启动。这种紧耦合真的是过于紧了,好在这种基础服务一旦完成,很多时候是可以不用改动的。而且如果一开始没做好,也就没法用。

后来还有一些服务,因为涉及到的数据处理流程比较多,工具也比较多,以及加入了大模型这样的元素(不那么稳定,效率也不高),导致了一个服务要维护真的要同时追溯非常多流程,比较头疼。

解决方案

我决定采用微批次服务+消息队列的方式来解决问题,具体论证和实验的过程比较繁琐,此处略过。

大致的处理流程【伪实时服务】:

  • 1 用户向服务(MicroBatchService)发起请求(Requests)
  • 2 服务收到请求后,发到消息队列(Kafka),同时使用循环(异步等待 asyncio.sleep):如果从内存从获得了消息则返回
  • 3 待处理的数据进入消息队列,然后由若干道工序处理(Step)
  • 4 每个工序都盯着In队列,然后进行处理,结果送到Out队列
  • 5 最后一个工序将结果进行处理,回写到服务(MicroBatchService)的缓存(Redis)

这样的结果是:

  • 1 请求端感觉和之前没啥差别,就是每个请求等的稍微久了一点。例如原来大概等5秒,现在可能等1分钟。(数据允许10分钟的处理时长)
  • 2 服务端似乎并发能力变大了很多。(微批次服务只是简单的写入队列并等待,当然并发变大了)
  • 3 处理端的独立脚本数量多了,但是处理效率提升了。(想想for和apply的效率差异)

这样的处理流程其实还不是最优的,只是为了方便对接,对用户屏蔽了内部变化。更好的方式是请求的数据直接送到队列 ,处理流程直接和队列对接。然后请求时给到webhook,处理完之后直接回写到目标(而不必再到内存中缓存)。

还有一些增强方案,可以让整个处理更加智能。kafka确实挺好的。

实例

需要从新闻中抽取事件。
在这里插入图片描述

step1: 从微批次服务队列中获取原始的输入,然后按照处理流程的规划,进行格式调整,规范化。对启用缓存查询的请求(默认),会直接到mysql中查询,如果已经查询到数据了,就打上exit_point。
step2: 对文本内容进行实体识别
step3: 根据识别到的实体进行原文的过滤抽取。(before, current, next)
step4: 对一篇新闻中抽取出来的若干段落进行相似性去重
step5: 对若干篇新闻进行处理,提取出所有的段落进行相似性判定:新闻间的重复段落不会去重,但是不会被重复处理,而是做一个“like whom”的标记,单独抽出来;如果抽完之后,已经没有新的unique段落了,那么就打上exit标记。这样后续的step6,step7不需要对这个进行处理。注意,这里在判定重复时并不完全根据段落内容,而是同时考虑被识别到的实体。所以,先根据段落向量相似性(并行)查询找到是否有近似段落,然后再判定实体是否相同。
step6: 对需要处理的新事件执行大模型判定
step7: 对初步的结果再进行一次大模型反向筛查
step99:对生成的新事件,分别插入Milvus和Mysql。其中插入Milvus是主要的,它是以段落为核心存储段落,语义向量和实体列表。同时将事件以规范方式存入mysql,用于重复请求的缓存。另外就是处理step5中标记的like_whom,因为新事件经过step6已经生成,且写入了milvus和mysql,可以将对应的结果映射到,然后返回。最后,结果输出到结果队列。

这样就完成了整体的生产过程,我自己的感觉是有点复杂的,但这样才能满足业务的需求,同时又能够在很有限的硬件资源下具备非常高的吞吐。

我也反思了一下,为啥做的这么没有美感:流程太长了,很多逻辑太复杂了,真的不太好管理。

大概是既要、又要、还要这样的要求之下的产物。

  • 业务上,需要的粒度非常细。需要锁定到段落-实体级别,而且需要对重复进行累加。这点对业务有好处, 同时也避免了无谓的调用大模型。该模式未来也是我的标配模式,每段文本对应了若干“知识”。
  • 技术上,需要可以有很高的吞吐量,还希望有很好的可维护性。所以采用了多个队列进行流转,每个步骤的开发和维护都是独立的、并行的。这就要求能提前规划好步骤,以及约定好输入-输出的数据格式。
  • 管理上,需要可以看到“漏斗”(数据从输入到输出的比例),以及可以追溯。因此在队列上还有一些其他规范和约束。
  • 运营上,希望能够对输出的结果进行精细控制。所以除了step6的输出,还有step7的检查(会产生过滤效果)。
  • 调用上,尽量维持和原来相似的体验(这样其他部门不用修改,或者仅少量修改)

总结

这个项目下周就要交付了,挺有意思的。

一方面,这是对于设计理念的实证。就一个要求很高的既要又要还要需求,我们是否可以通过良好的设计来保证实现。有些场景我已经进行了初步验证,接下来就要进行大规模检验了。

另一方面,这次实现的范式应该是一种更优解,应该可以使未来的同类工作大幅简化。所有的任务都可以规约到task_for,function,rec_id,data这样的统一输入,而且入口可以是同一服务(微批次),这样就不必为每个新的任务单独开辟新的微服务。在处理模式上,都是队列-队列-结果,因此,每次新的任务就是改一下对接的队列参数,确定好输入输出模式,然后专心的开发worker就可以了。这样,可以将业务上的需求有效分解到团队(每个人都可以开发worker,而不必去管架构)。维护上,也很容易锁定问题环节并修改。

最后,这种方式还可以进一步升级,称为agent方式,也是AI方式:通过“门”的设计,使得优化和迭代在长期的服务过程中得到平滑的迭代。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值