最近写了个 小玩具 ,用来去掉 goscon 相关的握手细节,这样压测程序可以透明的连接游戏服务器了。单连接测试的时候没问题,但是当连接数上去后,会出现莫名其妙的数据解析错误。感觉是出现了竞态条件了,于是用 go build -race 来重新构建,再测试的时候就有异常栈抛出可供分析了,还是蛮方便的

贴一段报错的堆栈:

==================
WARNING: DATA RACE
Write at 0x00c00015f400 by goroutine 82:
  syscall.Read()
      /usr/local/go/src/internal/race/race.go:49 +0x9a
  internal/poll.(*FD).Read()
      /usr/local/go/src/internal/poll/fd_unix.go:165 +0x1c7
  net.(*netFD).Read()
      /usr/local/go/src/net/fd_unix.go:202 +0x65
  net.(*conn).Read()
      /usr/local/go/src/net/net.go:184 +0xa0
  net.(*TCPConn).Read()
      <autogenerated>:1 +0x69
  github.com/ejoy/goscon/scp.(*cipherConnReader).Read()
      /home/spin6lock/work/2003VB12/go/src/github.com/ejoy/goscon/scp/conn.go:48 +0xf6
  github.com/ejoy/goscon/scp.(*Conn).Read()
      /home/spin6lock/work/2003VB12/go/src/github.com/ejoy/goscon/scp/conn.go:441 +0xcc
  main.handleRequest.func2()
      /home/spin6lock/work/2003VB12/go/src/goscon_proxy/main.go:77 +0x9a

Previous read at 0x00c00015f400 by goroutine 85:
  syscall.Write()
      /usr/local/go/src/internal/race/race.go:45 +0xab
  internal/poll.(*FD).Write()
      /usr/local/go/src/internal/poll/fd_unix.go:268 +0x1f8
  net.(*netFD).Write()
      /usr/local/go/src/net/fd_unix.go:220 +0x65
  net.(*conn).Write()
      /usr/local/go/src/net/net.go:196 +0xa0
  net.(*TCPConn).Write()
      <autogenerated>:1 +0x69
  main.writePipe()
      /home/spin6lock/work/2003VB12/go/src/goscon_proxy/main.go:112 +0x79

Goroutine 82 (running) created at:
  main.handleRequest()
      /home/spin6lock/work/2003VB12/go/src/goscon_proxy/main.go:75 +0x451

Goroutine 85 (running) created at:
  main.handleRequest()
      /home/spin6lock/work/2003VB12/go/src/goscon_proxy/main.go:90 +0x4dc
==================

先看修复前的代码:

 75     go func() {
 76         for {
 77             size, err := scon.Read(scp_buf)
 78             if err != nil {
 79                 log.Printf("scon read error:%s\n", err.Error())
 80                 break
 81             }
 82             data := scp_buf[:size]
 83             log.Println("Read data from scp:", data)
 84             writeCh <- data
 85         }
 86         ch <- ""
 87         log.Println("scon.Read exit")
 88     }()
 89     go readPipe(readCh, ch, scon)
 90     go writePipe(writeCh, ch, conn)

108 func writePipe(writeCh chan []byte, ctrlCh chan string, conn net.Conn) {
109     for {
110         select {
111         case data := <-writeCh:
112             conn.Write(data)
113         case <-ctrlCh:
114             log.Println("writePipe got close")
115             return
116         }
117     }
118 }

golang 的 data race 检测提示 77 行的 scon.Read 与 112 行的 conn.Write 有竞态,112 行正在读的时候,77 行写了,导致数据异常。想了一会终于想明白了。。。82 行的赋值并没有拷贝过程,只是创建了一个 Slice,这个 Slice 指向 scp_buf 这个字节数组。当 writePipewriteCh 中读出 data 后,75 行的 go 协程就能继续跑了,于是 scon.Read 继续修改 scp_buf,而 conn.Write 在读指向 scp_bufdata,导致数据异常。

针对这种情况,大致想到3种解决办法:

  1. 可以对 scp_buf 加读写锁
  2. 可以多复制一份 data 数据出来,供 writePipe
  3. 或者通过 Ring 环形链表来实现两个 buf 替换,scon.Read 写其中一个的时候,writePipe 读另外一个,Ring 的大小就是缓冲区的大小,从 goscon 读和写入客户端的速度相差较大的话,可以多搞点缓冲区

目前采取的是方案2来解决,GC压力可能会比较大,到时候再酌情更换方案3或者方案1