码上面试(程序员牛肉开源AI项目)——分布式single-flight落地简要解析
前言
这里是我基于牛肉哥的文档包括看到的牛肉哥的文档写的码上面试这个项目的这个single-flight项目的总结,这个项目是我心中的乔丹啊woc,等你真的啃了一下这个项目,你才发现什么是工程能力,什么是后端工程师的素养,我才啃了一口,就这受益匪浅,我将在这个文章的结尾附上牛肉哥的项目文档,推荐再看那个文档里面的技术,我这个是压缩版
正文
设计分布式Single—flight
解决需求
这里解决的就是同一个请求AI调用多次,并且AI调用结果不一样的问题,并且处理多个用户的不同请求,这里提高对并发的处理能力
Single—flight是什么?
Single-flight(单飞模式)是一种并发请求合并技术,它的核心思想是:当多个 goroutine / 线程同时请求同一个资源时,确保只有一个 goroutine / 线程真正执行实际操作,其他所有请求都等待并共享这个结果。
在这个项目里面Single—flight的设计模式
设计概述
- 在这里有基于Redis的分布式Single-flight
- 还有基于JVM并发组件ConcurrentHashMap和CompletableFuture的本地Single—flight
- 而且不仅是Single—flight减少并发,通过Redis构建的幂等缓存也减少并发,如果缓存命中直接返回命中结果,缓存定时储存在Redis中
关于本地Single—flight
设计概述
- 总结
- 这里的设计就比较简化,这里直接使用ConcurrentHashMap和CompleteFuture
- 关于ConcurrentHashMap的作用
- 按照这里的设计,这里ConcurrentHashMap的位置就是锁,这里通过获取这里ConcurrentHashMap的位置来获取锁来成为owner节点
- 关于CompleteFuture的作用
- 这里这个工具的作用就是共享结果,所有的follower都在get()方法上门阻塞等待,AI结果获取完成,这里直接调用complete()放入结果,所有的follower都将获取结果
问题解决
- 如果这里owner线程阻塞了怎么办?
这里follower有65秒的线程等待时间,如果到时间也没收到,则会抛异常到用户端,用户重试,owner也有65秒的过期时间,新线程发现同一个key过期了,这个时候直接覆盖 - 对于过期key的清理?
这里对于过期key有懒清理的逻辑,在owner完成逻辑的时候,发现有256个过期entry的时候就执行清理,这里其实有点谨慎了 - 这里的幂等缓存如何实现?
这里通过在原来的键里面加入用户的回答内容来实现幂等缓存,键匹配就直接返回
关于分布式Single—flight
解决需求
单机对于并发的处理有点弱了,所以这里引入了分布式集群,但是这里如果想要用分布式集群,就必须用一个跨集群的锁来进行资源控制,而不可靠这个JVM的锁监视器来锁住一个JVM,也就是牛肉哥说的—跨机器的请求去重和结果复用
设计概述
这里的设计采用经典的Redis设计分布式系统
- 抢锁的设计:这里通过状态机来实现,这里先抢一个Pending(等待状态),然后再通过Pending来进入Running状态。这里Owner通过心跳来告诉Follower我还活着,并且定期续心跳。
- 关于这里结果共享:这里就是基于Redis来实现结果共享
- 幂等缓存:这里也是基于Redis,和上面的实现思路一模一样
问题解决
- Owner的脑裂问题:
- 咱就是说上次在学习Redis的分布式设计的时候就无意了解到这个脑裂问题,没想到现在就看到了牛肉哥的解决方案
- 问题出现原因
这里因为Owner可能因为网络阻塞,没有同步心跳给follower集群,导致这里Owner被认为已经不存在,所以这里又选取一个Owner,但是原先的Owner的逻辑没有离开,完成逻辑之后直接把现在的Owner停掉了 - 解决方案
这里的解决方案就是设置唯一key,这里Redis维护了一个递增的数(就是这里的owner token唯一版本号),这里每次进入Pending状态的时候这个数都会被获取然后递增,保证这里的Owner唯一
- 问题出现原因
- Owner的心跳bug
- 问题出现原因:这里的Owner可能已经阻塞好久,但是JVM没有阻塞,所以这里就是一直在续心跳,然后就是导致业务没有进展,这里的Owner也占着茅坑不拉屎,所以这里我们就不能仅仅根据心跳来判断,因为这个的语意是JVM还活着而不是Owner正在推进进度
- 解决方法:这里没有实现但是给了解决方法:就是通过维护一个字段来记录最近的进度推进程度,然后这里每次判断有没有超时来看看这里任务推进的进度
- 分布式带来的节点状态问题
- 问题出现原因:这里可能就不是像单机一样只关心key和result了,这里可能更多关心一些节点的状态问题
- 关于owner
- 谁是owner
- owner现在是否在处理任务,是否超时。
- owner处理的是新任务还是刚刚处理出现异常的旧任务,现在请求是什么状态,失败了哪个机器做owner接管。
- 这里owner是不是也要复用结果
- 关于follower
- 谁是follower
- 是否在正常等待owner的处理。
- 是否应该复用结果还是直接返回
- 关于owner
- 解决方法:通过设计状态机和相应的行为来让节点明白什么时候该做什么事
除去Single—flight的链路上的其他问题
对于上传和识别的并发安全
问题描述
之所以要有 Session 重锁,是因为在这个项目里,有些流程不是单纯调一次 AI”这么简单,而是一整条重任务流水线。比如 interview-extraction 和 interview-demeanor,在真正调用模型之前和之后,还会有文件上传、图片上传、URL 生成、解析、结构化提取、落库等操作。如果同一个面试会话里,这类重任务被用户重复点击、前端重试、或者多节点并发打进来,那么问题不是只会“多调一次 AI”,而是整条重流程都可能重复执行。
对应现实
这个问题其实就是你用那些AI对话软件(现在的还是这样啊),就是一次只能生成一个请求,解析一份数据,你可以在对话框预备好,但是一次只能处理一个
解决方案
- 其实这里只要意识到问题,这里的解决方案也就很明晰了,直接使用一个锁就行(锁的粒度就是会话级)
- 为什么不用single-flight包裹这个逻辑(牛肉哥的考虑)
- 这里single-flight强调语意复用,而这里强调会话级阻塞
- 而且这里key不好覆盖,这里上传两份不同的简历这里就会进行两次操作,这个不解决实际问题
对于不同模块的隔离
问题描述
- 这里有四个模块
- interview-evaluation:面试回答评分
- interview-followup:追问问题生成
- interview-extraction:简历题目抽取
- interview-demeanor:神态分析
- 如果我们不进行模块的隔离,这个会话就会乱成一锅粥,不能进行独立监控,你只知道AI慢了,但是不知道是哪一块慢了
- 而且这里不同模块理应不能互相复用和互相拦截
- 并且在这里的设计希望这里的flight设计解耦
- 不是所有请求都用同一套超时、续租、缓存策略,而是按 stage 读取对应配置。比如评分是高频、文本类请求,就适合较短 heartbeat 和较积极的 L1 回放;简历抽题是长耗时重任务,就需要更长的 runningTtl 和更长的结果保留时间;神态分析则可能不适合开本地 L1 缓存。也就是说,stage 决定的不是“名字”,而是这次请求该套用哪套治理参数。(牛肉哥原话)
总结
https://wcnqkg2gb7ln.feishu.cn/wiki/Rcplw5BEAiSePAk3VsjcmmMKn2b,这个就是牛肉哥的文档,我建议这里的学习方式就按照牛肉哥文档来就行,不用死啃源码,这里牛肉哥文档为主,源码为辅助论证的手段,这个太牛逼了这个文档写的,牛肉哥太伟大了,就如此。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)