antlabs/greatws

Conn无锁的一致性问题

lesismal opened this issue · 14 comments

扫了几眼,可能会有窜号问题,假设:

step 1: 应用模块A有close:
https://github.com/antlabs/bigws/blob/master/conn_unix.go#L45
或者event loop收到对端EOF后close了fd

step 2: 此时Conn被应用模块B协程池持有,并且正执行Write、马上要syscall但尚未syscall:
https://github.com/antlabs/bigws/blob/master/conn_unix.go#L61

step 3: 新的连接到来,被分配了这个fd

step 4: step 2中执行到了syscall write,数据被写入到了新的fd中

感谢,Write加锁,漏了Close要加锁。

@lesismal 改好了。

不是简单加锁的问题,还要有状态判定,否则这块加锁close,但其他地方正准备执行的是syscall.Write(fd)还是乱的。

而且Conn.Write这里没加锁,并发写的for循环过程中都可能循环多次、每次写入了一部分,这期间可能多个并发各自写half-packet也会乱:
https://github.com/antlabs/bigws/blob/master/conn_unix.go#L69

不知要考虑io协程和逻辑协程(处理每个message的协程)的一致性问题,还要考虑业务自己持有、随时可能对Conn进行操作,所以至少是三类协程池/并发的一致性处理

暂时还没看ws部分是否自己加锁了,只看了四层这块,但是四层自己处理好自己这一层比较好

而且Conn.Write这里没加锁,并发写的for循环过程中都可能循环多次、每次写入了一部分,这期间可能多个并发各自写half-packet也会乱: https://github.com/antlabs/bigws/blob/master/conn_unix.go#L69

不知要考虑io协程和逻辑协程(处理每个message的协程)的一致性问题,还要考虑业务自己持有、随时可能对Conn进行操作,所以至少是三类协程池/并发的一致性处理

在WriteMessage的地方加锁了。你看下WriteFrame函数。
https://github.com/antlabs/bigws/blob/master/conn.go#L473

writeOrAddPoll就两个地方调用,一个是Write,上游是WriteMessage这块有锁。一块是flush的地方也有锁https://github.com/antlabs/bigws/blob/master/conn_unix.go#L126。

不是简单加锁的问题,还要有状态判定,否则这块加锁close,但其他地方正准备执行的是syscall.Write(fd)还是乱的。

Close之后,fd 就置为-1.这块就没有串号问题。状态判断不加不会影响目前的正确性。close状态位是预留的。后面入口的地方都会判断的。

只做ws的话,4层不处理也可以。

只做ws的话,4层不处理也可以。

bigws的4层处理不会开放出去,没必要。只是自用。

@lesismal nbio的Write语义是保证写入成功,还是尽量保证写入成功的?

*nix syscall write EINTR/EAGAIN 会:

  1. 判断是否之前已经写失败并且正在等待可写,如果是则判断把新数据添加到缓存后总量是否超过配置的限制(默认为0、不限制),没超过限制就缓存起来: https://github.com/lesismal/nbio/blob/master/conn_unix.go#L387
  2. 添加可写事件,conn自带了是否添加过写事件的标记,如果之前已经添加过、尚未可写并清除标记,则不需要再syscall去重复添加:https://github.com/lesismal/nbio/blob/master/conn_unix.go#L367

因为是非阻塞的,所以返回err==nil也不代表数据是被发送出去了,不过问题不大,因为即使write成功了也只是写入到内核缓冲区,tcp是否发送到对端也没给我们保证。只有7层协议交互自己才能判断成功

*nix syscall write EINTR/EAGAIN 会:

  1. 判断是否之前已经写失败并且正在等待可写,如果是则判断把新数据添加到缓存后总量是否超过配置的限制(默认为0、不限制),没超过限制就缓存起来: https://github.com/lesismal/nbio/blob/master/conn_unix.go#L387
  2. 添加可写事件,conn自带了是否添加过写事件的标记,如果之前已经添加过、尚未可写并清除标记,则不需要再syscall去重复添加:https://github.com/lesismal/nbio/blob/master/conn_unix.go#L367

因为是非阻塞的,所以返回err==nil也不代表数据是被发送出去了,不过问题不大,因为即使write成功了也只是写入到内核缓冲区,tcp是否发送到对端也没给我们保证。只有7层协议交互自己才能判断成功

我在想要不要提供两个写口

  1. WriteMessage,模拟同步方案的WriteMessage。在内核的sendbuf满了,Write会失败。通过一个chan做事件通知。
    这时候再在poll加个write事件,poll write可写,直接通知chan,这时候卡住的WriteMessage继续写。
  2. AsyncWriteMessage就是现在WriteMessage的实现也和nbio Write类似,Write失败就加到poll里面管理并写入。

nbio的sendfile量大失败时候是有用到chan实现阻塞的机制,跟你说的这个差不多,只存储文件offset等待可写后继续sendfile也行,但是实现起来有点麻烦,所以我偷懒用阻塞了

我觉得同一个连接只支持一种模式比较好,Write内部根据不同的模式用不同的机制去write。如果同一个conn支持不同的模式、两个接口,就要考虑并发写的时序。为了保证时序,阻塞写可能就需要在等待期间一直上锁,这时候即使AsyncWrite的syscall或者加入到队列后队列的syscall也是要上锁等待,否则就可能两边有half-packet乱序之类的,要解决会比较麻烦。

提供两种方法确认容易出bug,那就选择提供一种方法WriteMessage。模拟同步方案的WriteMessage的形为。考虑下一种常见写法

c.AsyncWriteMessage()
c.Close()

如果一次AsyncWriteMessage撑满fd的SendBuf,可能会造成数据只有一部分写入成功就被关闭了。

如果tcp send buf + 框架层自己设置的buf size都满了,被关闭也不冤枉。另外就是提供SetWriteDeadline,即使框架层自己维护的待写入buf没满,超时也应该关闭

nbio里目前是默认不限制buf,用户自己可配置,四层的write deadline也是上层用户自己调用