Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

另外一个cluster的消息顺序异常问题 #587

Closed
zhust2003 opened this issue Mar 13, 2017 · 14 comments
Closed

另外一个cluster的消息顺序异常问题 #587

zhust2003 opened this issue Mar 13, 2017 · 14 comments

Comments

@zhust2003
Copy link
Contributor

zhust2003 commented Mar 13, 2017

目前我的issue比较特别,与上一位仁兄的情况不太一致,我所以我重开了一个issue,主要的问题点我们排查下来在socketchannel与wakeup_session结构中。

我描述下问题,假设A通过cluster.call调用B的某个lua api,为了简介起见,我假设api是这样的
在A服务中写了这样的代码
cluster.call(B, "xx_actor", "xx_function", arg)
此时B服务中的xx_function在某个参数为特定值的情况下,会skynet.wait当前协程暂缓发送(假设这个逻辑为u),然后在参数为另外一个特地值的时候,与之前暂缓发送的协程重新wakeup并处理当前逻辑(假设这个逻辑为r)。
比如是这样的

 function xx_function(arg)
     if arg == 1 then
            skynet.wait(coroutine.running())
     end
     if arg == 2 then
            skynet.wakeup刚才的co
            skynet.yield()
            返回此次逻辑的msg
     end
 end

这里特地增加了一个yield是希望最终执行的顺序是先真正wakeup,再执行本次返回逻辑(也就是说会有两条B->A的socket消息发送到A中,而且时间上很接近),目前从socket层抓到A中消息的顺序确实是u->r,不过最终函数处理顺序却变成了r->u。

目前我们通过日志进行分析,发现是这样的情况,在socket_channel的dispatch_by_session(self)函数中,会进行sock读取,这里的日志打印是顺序的,但是当wakeup后,情况就不一定是如此了,目前查看wakeup代码,发现是wakeup是个table结构,由next函数返回下一个键值,顺序是无法保证的,那么当接收包比较相近或者网络层阻塞的情况下,是有可能导致接收函数处理乱序的。

目前暂时我想的一个方案是否需要修改wakeup结构为先入先出的队列,或者在socketchannel中yield一次,保证每次read的wakeup队列只有一个,测试结构才是有序的。但是这些方法都不太好,修改了源码,而且socketchannel在很多第三方api中都有使用,按道理不应该出现这个问题。

希望我描述的细节云风能理解,不吝赐教。

@cloudwu
Copy link
Owner

cloudwu commented Mar 13, 2017

这里有几个框架没有承诺保证次序的地方:

  1. skynet.wakeup (issue 里写的 skynet.resume 不知道是不是笔误) 并不保证什么次序唤醒。也就是说,并不承诺 skynet.yield() 后的流程一定在 wakeup 的后面。虽然目前实现的可以保证。

  2. wakeup 本身不保证次序。

我认为这种有序需求可以看成是一种多线程并行状态,不应该依赖隐式的次序来保证。比较简单的作为是使用 skynet.response ,把它放在一个自己维护的队列中。

如果要保持上面的结构,那么应该把 skynet.yield() 改为 skynet.wait 挂起,由前面的流程结束后再 wakeup 。即,第一步完成后再延续下一步。

另外还有一个方法,就是利用 https://github.com/cloudwu/skynet/wiki/CriticalSection 创建一个队列,这个队列可以按持续执行一系列步骤。那么,你可以在 arg == 1 的时候,把整个后续流程(包括第一行 skynet.wait 推进队列放进队列。然后在 arg == 2 时,先 wakeup ,再把后续流程放进队列。

local queue = require "skynet.queue"
local cs = queue()
local delay_co

function xx_function(arg)
  if arg == 1 then
    cs(function()
      if delay_co == nil then
        -- 如果之前没有 delay 就在这里挂起。
        -- 如果之前已经有了则不需要,因为 cs 队列本身就有排队的功能。
        delay_co = coroutine.running()
        table.insert(q, delay_co)
        skynet.wait(delay_co)
      -- 后续流程需要打包在和 wait 在同一个函数中
    end)
  elseif arg == 2 then
      if delay_co then
          -- 如果前面有 delay 就唤醒,并清除记录。
          skynet.wakup(delay_co)
          delay_co = nil
     end
     cs(function()
        -- 后续流程,这里会由 cs 队列来排序。
      end)
 end

上面的代码是随手写的,用的时候要理解一下,排查可能的潜在 bug 。

@zhust2003
Copy link
Contributor Author

skynet.resume即是wakeup,我们理解上相同,实际上调用的确实是wakeup,只不过最终是协程resume,所以我就这么写了,一会我修改一下。
后面的部分云风应该理解错了,目前的无序并不是在B的XX_function中,而是A中的u,r方法,这里我并没有写出代码。
目前主要的情况是B发送给A的socket顺序是u->r,但是由于socketchannel的wakeup中的乱序,导致了虽然我socket消息是u->r顺序,但是由于socket处理刚好在没有任何挂起的多次socket.read中,那么最终的处理函数顺序可能变成了r处理函数->u处理函数,由于A中的处理函数刚好就是往外发消息,那么就完全无序了。

目前的情况就是在B服务中的调用有序,那么在A中按道理应该也是有序的,但目前的处理函数无序。

@zhust2003
Copy link
Contributor Author

目前我B中复杂的wait跟wakeup应该也是可以用skynet.response替代的,不过这个不影响A中的协程wakeup乱序问题。

@zhust2003
Copy link
Contributor Author

zhust2003 commented Mar 13, 2017

A

function u()
    local msg = cluster.call(B, "xx_actor", "xx_function", 1)
    print 1
end

function r()
    local msg = cluster.call(B, "xx_actor", "xx_function", 2)
    print 2
end

B xx_actor

 function xx_function(arg)
     if arg == 1 then
            skynet.wait(coroutine.running())
            返回此次逻辑的msg(u)
     end
     if arg == 2 then
            skynet.wakeup刚才的co
            skynet.yield()
            返回此次逻辑的msg(r)
     end
 end

可以看到我在b中刻意希望能保证u,r顺序,但是a中实际u,r唤醒的顺序是不固定的。

@cloudwu
Copy link
Owner

cloudwu commented Mar 13, 2017

我大概明白了。是说 socketchannel 的 dispatch_by_session 里面调用了 skynet.wakeup 来唤醒已经收到的 response 包来延续回应流程,这里由于 wakeup 没有保证次序,导致了后收到的回应包,先被返回了?

@zhust2003
Copy link
Contributor Author

zhust2003 commented Mar 13, 2017

那么,核心问题是否是:

socket.read 被唤醒的次序和 socket 上的数据输入次序不同?

对,是这个意思,socket我打印了,read出来顺序是对的,但是唤醒的co是偶尔是顺序是反的,我觉得这个应该会影响到所有使用socketchannel的。

修改了上述代码,大致就是偶尔 1 2 ,但是偶尔打印又是 2 1
但是B方发送顺序是保证的,从抓包,打印日志都能得出结论。

@cloudwu
Copy link
Owner

cloudwu commented Mar 13, 2017

ok 那我把 skynet.wakeup 改成保证次序的。你看一下。

@zhust2003
Copy link
Contributor Author

我测试过,保证有序是可以的,但是我不太敢修改项目源码,毕竟影响比较大,所以在这里发issue跟你商量看看有没有应用层可以解决的方案。

@cloudwu
Copy link
Owner

cloudwu commented Mar 13, 2017

确保次序比无序的保证更强,应该没有兼容性问题。如果不改 skynet.wakeup ,也需要修改 socket channel 来保证这点。dispatch_by_session 乱序倒是可以接受,但是 dispatch_by_order 也会有同样问题,那么就是 bug 了。

@zhust2003
Copy link
Contributor Author

dispatch_by_session 乱序倒是可以接受,但是 dispatch_by_order 也会有同样问题,那么就是 bug 了。

也是,都忘记了还有by order的场景

@zhust2003
Copy link
Contributor Author

原来你已经提交了commit的,我没有发现,我修改下项目源码测试一下,另外我看了下源码,是用默认的lua table remove,会有后续元素的移动,是否有考虑用
https://www.lua.org/pil/11.4.html
不过这样项目代码会比较复杂,不知道这里的数据量是否需要引入这个结构。

@cloudwu
Copy link
Owner

cloudwu commented Mar 14, 2017

没有必要,O(n) 取决于 n 有多大。这里 n 和 同时 wakeup 的数量相关。

@zhust2003
Copy link
Contributor Author

ok

@zhust2003
Copy link
Contributor Author

hi,目前测试下来新代码没有问题,解决了我们这里socket消息与wakeup协程乱序的问题。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants