多进程不能共用连接
其实这个是所有多进程、多线程程序都需要注意的问题,当进程or线程共享资源的时候,一定要考虑资源冲突,否则会出现各种诡异的问题(死锁、数据返回异常、连接被关闭等等等)。
如下代码,在Swoole中,在server启动时创建了一个redis连接,在onRequest
中使用。
代码看上去没什么问题,但是实际使用时,如果压力很大,就会出现多进程抢占连接导致的问题。原因是因为创建的redis连接实际上是一个全局对象,每个work进程都在使用同一个连接。
1 | class Server |
解决方法
- 不在server中创建,而在
onRequest
中,接收到请求时创建简单粗暴的解决方案,每次收到请求时独立创建局部的redis连接,请求结束后释放。这种方式缺点很明显,连接没有复用,影响性能。 - 在
onWorkerStart
中创建连接,并按workerId
索引每个worker进程的redis连接。
代码如下(主体代码参考上面),在Server中增加一个redisPool
,worker启动时创建连接后注册到pool中。这样能保证每次请求时,使用的都是各进程独立的redis连接。
1 | //在Server类中增加$redisPool变量,初始化为空数组 |
tcp协议包完整性
在默认情况下,使用swoole-server时(TCP协议
),swoole不对包的进行完整性校验,在onReceive
中接收到的包可能是不完整的,也有可能是多份数据。这是由于TCP协议的原理所造成的:
TCP是一个流式协议。客户端向服务器发送的一段数据,可能并不会被服务器一次就完整的收到;客户端向服务器发送的多段数据,可能服务器一次就收到了全部的数据
如下代码,在onReceive
中接收到数据后,转给task进程进行处理,task进程处理结束后返回。
1 | //请求处理(接收到客户端发送的数据) |
执行时(发送的数据比较大),有可能出现这样的日志。从日志里可以看到
同一个server_fd(53)接收到了两份数据,两份数据来自同一个client_fd(22)
第一个task向客户端发送数据成功了,但是第二个发送失败(
sendRes:false
)
实际打印出数据(在onReceive中),会发现客户端发送的数据被拆成了两份,因此触发了两次onReceive
。
第二个task中sendRes失败,是因为在处理第一份数据时,task中已经把客户端连接给关闭了。
解决办法
open_eof_check
使用Swoole提供的open_eof_check
,保证数据包的完整性。
此选项将检测客户端连接发来的数据,当数据包结尾是指定的字符串时才会投递给Worker进程。否则会一直拼接数据包,直到超过缓存区或者超时才会中止
EOF即为数据的结束标记,具体由客户端使用的发送方式而定,比如Memcache协议以”\r\n
”结尾,Java中BuffWriter.newLine()发送的数据在有可能是”\n
“结尾,也有可能是”\r\n
“。
注意:==swoole的EOF检测不会从数据中间查找eof字符串,所以Worker进程可能会同时收到多个数据包,需要在应用层代码中自行explode(“\n”, $data) 来拆分==,1.7.15版本增加了open_eof_split
,支持从数据中查找EOF,并切分数据。
1 | 'open_eof_check' => true, //打开EOF检测 |
手动拼接请求数据
默认情况下,同一个客户端fd会被分配到同一个worker
中处理,所以数据可以拼接起来,当发现结尾是EOF字符时才进行处理。
例如可以在全局数据中保存一个数组buff
,接收到数据后进行拼接和判断。
1 | $buff[$fd] .= $data; |