V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
mawen0726
V2EX  ›  程序员

求教,有状态的分布式系统应该如何设计

  •  
  •   mawen0726 · 36 天前 · 2595 次点击
    这是一个创建于 36 天前的主题,其中的信息可能已经有所发展或是发生改变。

    应对业务扩张,将服务拆分并支持横向扩展,现在拆了共 a,b,c 三个服务出来,然后这三个服务要设计成可以随时横向扩展。其中有如下调用链路

    • a -> b -> c
    • a -> c

    然后 c 是主要处理业务的服务,会上分布式锁,加上处理时间长,会后台执行并执行完通知上游来做其他业务操作,业务操作之后才能解锁。

    因为一个业务只能在一个 c 中执行,所以解锁要找到对应的 c 。比如 a 执行业务分配到节点 1 的 c,后续交互都要找节点 1 的 cb是批量执行业务,这一批都要到同一个c中执行完。

    目前设计方案是将 c的 IP 和业务 id 写到 redis ,然后通过 redis 确定业务在哪个 c 节点上面跑着。然后要在业务处理过程中,查看状态都通过 redis 查对应 ip 再去调用服务。

    比如批量任务中,a 要确定哪个 b 节点承接了这个业务,b 节点确定哪个 c 节点正在处理这批次业务。

    感觉这种设计不是很好,想请教下还有没有更优雅的做法

    可能描述的有点复杂,大概意思是

    怎么记录在多个节点时,知道这个任务在哪个节点上,并且应用之间相对不耦合

    第 1 条附言  ·  23 天前

    补充个最终设计的大概原型图 foo.png

    46 条回复    2024-12-26 17:30:41 +08:00
    k9982874
        1
    k9982874  
       36 天前 via Android
    为什么要把问题搞这么复杂,为什么不用消息队列
    tairan2006
        2
    tairan2006  
       36 天前 via Android
    这很简单,你改成 pub/sub ,比如 mqtt 。接到任务的节点会自己订阅相关 topic ,其他的节点不会订阅。你发布任务的时候也无需关心到底目的地在哪。
    JasonGrass
        3
    JasonGrass  
       36 天前
    我的理解:a 执行业务分配到节点 1 的 c ,执行完成之后,C1 就把中间结果保存到数据库或者 Redis ,得到一个 key,
    后面 a 要继续处理这个业务,随便找一个 c 的节点,把 key 给 c ,然后继续处理。

    感觉是业务拆得还不够彻底?理想情况是,把业务拆成一步一步执行的,
    执行完第一步,丢到消息队列,中间结果(如果有比较大的中间结果)保存到 redis 或者数据库,下一个服务从消息队列中拿到任务,然后继续跑,这样才好真正的横向扩展。
    mawen0726
        4
    mawen0726  
    OP
       36 天前
    @k9982874
    消息队列用了,但是用来处理业务中产出的各种数据实时推送...
    没想好记录任务在哪个节点这个层面怎么用...
    mawen0726
        5
    mawen0726  
    OP
       36 天前
    @tairan2006 貌似跟 1 楼提的消息队列方式很像,我思考一下怎么设计
    k9982874
        6
    k9982874  
       36 天前 via Android
    不是,你为什么关心认为在哪个节点
    a 有任务扔进队列,c 去捡了执行,执行完了再扔个消息,a 受到走完后面流程,就这么简单的事
    mawen0726
        7
    mawen0726  
    OP
       36 天前
    @k9982874
    因为 c 在执行任务的时候,上了锁,锁定了某个资源,因为处理时间长,锁定时间久
    代码在写的时候,不是常规的写法....
    lock.lock
    try{
    dosomething()
    }
    finally{
    lock.unlock
    }


    而是起了个线程来上锁,等 a 确定完业务执行完,再去通知持锁的 c 解锁。因为用的 redisson 的 lock ,所以要找回对应的节点 c 才能解锁...
    mawen0726
        8
    mawen0726  
    OP
       36 天前
    @JasonGrass
    感觉再拆就不好维护了...
    不过可能是我没拆好,感觉还得思考下
    mawen0726
        9
    mawen0726  
    OP
       36 天前
    @k9982874
    不过感觉可以按这种方式再想下怎么改,确实可能设计复杂了
    1402851639
        10
    1402851639  
       36 天前 via Android
    感觉你这个耦合这么重是不是拆的就有问题呢。。
    1402851639
        11
    1402851639  
       36 天前 via Android   ❤️ 1
    我觉得你更应该解释一下是什么条件限制了必须在同一个实例上,去考虑能否在请求中将关键信息传递过去,又或者 redis 暂时存一下,而不是 b 服务去循环将请求打到同一个 c 上面?
    yeqizhang
        12
    yeqizhang  
       36 天前 via Android
    @1402851639 我也觉得,这样拆到底解决了啥问题,我感觉瓶颈都在数据库了,那还要解决分布式的事务问题
    wateryessence
        13
    wateryessence  
       36 天前
    一致性哈希?
    cowcomic
        14
    cowcomic  
       35 天前
    1 ,当时为什么要拆,从描述看 abc 的耦合很严重,而且是双向依赖,这不拆更合理
    2 ,如果一定要拆,那我理解可以把 C 看成一些工作节点,把 C 向上依赖的部分再拆出来变成 C 的下游,首先保证链条是单向的,AB 变成纯上游,C 变成总业务入口,这样看能不能消除 C 的状态变成无状态的
    3 ,如果消除不了,那就 C 前面架消息队列,由 C 决定要哪些请求,如果并发不是很大的话,就直接表记录,如果并发很大,那这个整体的系统功能就很有问题,重新梳理吧
    czsas
        15
    czsas  
       35 天前
    可以借鉴一下 temporal/Cadence 这种 workflow 框架
    nuk
        16
    nuk  
       35 天前
    给每个任务加一个路径栈
    server
        17
    server  
       35 天前
    temporal
    wenjun19931112
        18
    wenjun19931112  
       35 天前
    想办法把有状态的服务,转换为无状态的服务。
    而不是优化有状态服务的分布式处理逻辑。
    Plutooo
        19
    Plutooo  
       35 天前
    @mawen0726 #7 redisson 的 lock 不是基于线程的吗,当你的 a 通知 c 可以解锁以后相当于一个新的请求,新的请求怎么去解锁之前那个线程的锁
    RightHand
        20
    RightHand  
       35 天前 via Android
    有状态的,去做复杂均衡啊。干嘛非要上无状态的微服务
    THESDZ
        21
    THESDZ  
       35 天前
    1.状态带着走;
    2.更进一步,状态单独存,状态唯一 id 带着走。
    mark2025
        23
    mark2025  
       35 天前
    分布锁? 这是给自己掘墓吧……
    mark2025
        24
    mark2025  
       35 天前
    @mawen0726 锁是 C 加上的,为啥要用 A 去解锁而不是 C 自己解锁?
    cheng6563
        25
    cheng6563  
       35 天前
    “会后台执行并执行完通知上游来做其他业务操作,业务操作之后才能解锁。”

    所以你的实际调用链应该是 a->b->c->b->a
    huzhizhao
        26
    huzhizhao  
       35 天前
    消息队列最合适,特定的队列处理特定的数据就好了
    pangzipp
        27
    pangzipp  
       35 天前
    上一个分布式调度框架例如
    例如 airflow
    阿里云的 SchedulerX
    sujin190
        28
    sujin190  
       35 天前
    @Plutooo #19 分布式锁底层都是加锁方生成一个只有自己指定的 ID 来防止他人异常解锁,那换句话说你加锁后主动把这个 ID 告诉别人,或者提前生成一个 ID 要求加锁方使用这个 ID ,,那别人就能解锁了啊,和线程啥的无关

    而且从实现来看,如果执行时间很长,那这个执行中的状态应该保存在数据库中,不应该单纯加锁,加锁范围只到查询修改这个数据库中状态的过程,否则直接在入口加锁同步调用就好了啊,没必要异步吧

    有状态逻辑可比把这个状态写入数据库需要的地方读取判断复杂多了,存入数据库需要的地方读取判断,那整个系统还是无状态的,简单多了,想要性能好一点那存入 redis 也不是不行啊,干嘛非要纠结使用加锁的方式
    ychost
        29
    ychost  
       35 天前
    每个任务 ID 创建一个 topic ,后续只需要往这个 Topic 推数据就行了,处理的那台机器拿到任务之后去监听它,后续都是此机器来处理任务
    luofuchuan668
        30
    luofuchuan668  
       35 天前
    看得不是很明白,但是这样设计大概是有问题的
    Plutooo
        31
    Plutooo  
       35 天前
    @sujin190 #28 对啊拥有线程 id 是可以解锁,那还跟请求到哪个 c 的节点有什么关系呢,既然 a 需要再次请求解锁,那么这个重新发送的请求必然跟先前的请求大概率不同线程,那些跟请求到了其他 c 节点有什么不同吗
    ConkeyMonkey1024
        32
    ConkeyMonkey1024  
       34 天前
    b 和 c 之间再加一层,专门负责调度
    aarontian
        33
    aarontian  
       34 天前
    我不认为有必要查询到具体是在哪个实例上执行。把服务实现为有状态的,并假定它在释放资源前不会挂是个很糟糕的设计,你记录了 c1 的 IP ,c1 挂了呢?你是不是又要有更多复杂的“业务逻辑”去处理实例状态变更导致的问题。


    你这里 c 锁定资源的正确姿势或许是把锁定的凭证存放到某个分布式存储(比如 redis )中,等解锁时任意一个本服务的实例都应该能拿到凭证去解锁。(但我甚至怀疑这个设计有根本性的问题,或许不需要加这种资源锁,或者可能可以有一个统一的调度器负责一个流程的资源上锁与解锁)
    mawen0726
        34
    mawen0726  
    OP
       24 天前
    @1402851639
    这几天忙忘记回了,后面重新拆了下。
    将所有要执行的东西都提交到消息队列中,让下游去抢。如果执行的要中断,上游也是发消息队列,下游监听自己有没有在处理这个业务,没有就忽略。
    将所有相关场景都改成这样了,不知道这样的设计好不好...
    mawen0726
        35
    mawen0726  
    OP
       24 天前
    @cowcomic
    这里的最底层的资源其实是 jupyter ,用来运行一些大型算法得出结果。然后印个 jupyter 同时只能跑一个算法,所以需要资源锁定。c 服务就是干这个事的。然后 a 和 b 其实就是一个算法只跑单次和按时间纬度跑多次的区别,因为跑多次且结果有上下关联,所需内存大,所以也抽两个了(原本放一块)

    然后之前 c 跟业务耦合了,最初拆的就来回解锁了。后面把 c 拆得只跟 jupyter 交互,是个无状态的。a 、b 服务发算法任务让 c 自己去抢,中断执行 ab 也是发消息队列,c 监听到检查自己有没有正在运行对应的业务(算法 id 唯一),没有则忽略。

    我之前纠结要维护 ip 是因为觉得中断,暂停等操作要通知对应的 c 服务。但是后面捋了下,一个业务只会在一个 c 里面执行,要做什么操作直接广播所有 c (通过消息队列),c 来判断有没有在运行这个就可以了。

    然后上锁放在上游( a 、b 服务)里面,在让 c 运行前上锁,c 运行完通知后解锁。
    mawen0726
        36
    mawen0726  
    OP
       24 天前
    @czsas
    @server
    感谢感谢,现在回头看看这些框架。
    之前纠结要维护 ip 是因为觉得中断,暂停等操作要通知对应的 c 服务。但是后面捋了下,一个业务只会在一个 c 里面执行,要做什么操作直接广播所有 c (通过消息队列),c 来判断有没有在运行这个就可以了。没有运行则不做任何操作
    mawen0726
        37
    mawen0726  
    OP
       23 天前
    @mark2025
    因为业务场景锁有业务在生命周期内固定使用某个资源的场景,感觉自己写一套去维护资源的状态,还不如用分布式锁来的直接...
    mawen0726
        38
    mawen0726  
    OP
       23 天前
    @mark2025
    当时没设计好,后面重新捋了下,将上锁和解锁都放在同一端了
    当时不想大动代码(屎山),就想着 c 上锁,a 、c 解锁
    mawen0726
        39
    mawen0726  
    OP
       23 天前
    @huzhizhao
    现在的设计是消息队列中:
    主题 1 关于任务发布( a 、c 发布的任务,类型字段区分开)
    主题 2 关于任务接收成功的响应,让 a 、c 知道任务在执行了
    主题 3 关于执行中的数据
    主题 4 关于任务的中断、取消

    所有的 c 实例对于主题都属于同一个消费组,共同监听主题 1 抢任务执行,发布到主题 2 响应上游正在执行,同时将结果发布到主题 3 。主题 4 则是不同消费组,收到取消的消息判断自身有没有在执行对应业务,没有则忽略

    所有的 a 、b 实例都是一个单独的消费组,监听主题 2 确定任务发布成功(没响应则重试 5 次),监听主题 3 获取结果(结果实体包含消费组信息,消费组不一致忽略消息)

    感觉这样设计强依赖了消息队列,但是可以不管下游的 ip 了,也不知道好不好,但是也没机会再改了 - -
    mawen0726
        40
    mawen0726  
    OP
       23 天前
    @luofuchuan668
    大佬能看下我新回复的,评价一下吗
    mawen0726
        41
    mawen0726  
    OP
       23 天前
    @Plutooo
    其实正是这个线程的问题,才让我发帖问的,原本我以为 redisson 只是简单的 key 相同解锁...
    后面稍微深入了一下,其实就是分了两个类型
    1. 线程 id 必须一样
    2. lockasync ,指定一个 id ,使用相同 id 上锁解锁即可(可以联想场景 reactor java ,每个场景都可能是不同的线程执行的)

    但是抛开这个不谈,原本要维护下游 ip 还是很垃圾的设计...
    mawen0726
        42
    mawen0726  
    OP
       23 天前
    @aarontian
    当时认为 c 去锁定资源,就认为 c 去上锁了,其实还说漏了一些,c 和资源会建立 ws 连接,所以当时就更认为要在 c 上锁...
    希望可以看看我新的回复,看看这种设计怎么样,算是第一次做种比较复杂的设计
    huzhizhao
        43
    huzhizhao  
       22 天前
    @mawen0726 #39 没有绝对好的,适合就好。得看你从什么角度考量。
    但从复杂度上来说,整个系统的结构是复杂了的。
    mark2025
        44
    mark2025  
       22 天前
    @mawen0726 感觉 主题 2 、3 、4 都可以合并,用执行状态键值表示 完成、终端、取消等状态
    mark2025
        45
    mark2025  
       22 天前
    @mawen0726 你这个场景使用任务队列来做应该还算简单了:
    1. 发布任务消息到消息队列(带执行)
    2. 收到任务消息,上锁,发布任务状态消息(已上锁)
    3. 收到已上锁的行任务消息,开始执行任务。根据任务执行结果(成功、失败),发布任务状态消息(执行状态,可以包含任务结果数据)
    4. 收到包含执行状态的任务消息,解锁

    抽象为 4 个步骤,使用任务状态值在消息队列中来流转,可以忽略 a 、b 、c 具体服务。
    mark2025
        46
    mark2025  
       22 天前
    a -> b -> c
    a -> c
    两种调用链路可以设计为两个队列 topic ,以上面 4 步为基础对 步骤 3 进行扩展就行了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1078 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 23:27 · PVG 07:27 · LAX 15:27 · JFK 18:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.