1. 先来看下问题:
    l82hfdd9.png
    测试的时候发现三个执行体只有php的执行体所在的板卡CPU占用率居高不下,java和python的则正常,由于服务正在被多人测试,导致登录界面加载很慢,验证码刷新不出等问题。
    l82hke0g.png
  2. 排查:
    l82hj8wl.png
    l82hkzst.png
    l82hl4dx.png
    通过排查发现,左括号输出日志,报了8080超时,8080端口为php的服务,而此时拟态大屏在频繁发送数据请求,而右括号服务没有正常响应,导致执行体的客户端一直在等待接收数据,因为执行体作为TCP客户端跟右括号建立通信时需要接收指定的长度才会断开连接,但是使用了socket_set_nonblock非阻塞模式设置,所以如果没有接收到数据就会跳出外层for循环,一直持续4分钟的时间,这样就会导致cpu资源被for循环全部占用。我们先来看下有问题的代码:
    l82hrx6q.png
    我们知道socket_read,socket_recv,socket_accept均为默认的阻塞模式,什么是阻塞模式,就是当程序运行到此函数时,会一直读取服务端发来的数据,直到接收满2048个字节的(最大设置65535)长度才会继续往下运行,否则就停留在此处,交由操作系统和底层网络接管程序,一直监听,直到超时为止,所以问题就来了,在这个代码中由于把read设置成了socket_set_nonblock($socket);非阻塞模式,也就是说当运行到这里时,由于右括号服务已经挂了所以不会发送数据,自然而然也不会读到数据,由于又没有超时设置,所以很快就往下继续运行了,跳出本次循环,继续下次循环,这个时间是很短暂的,也就是说当有括号不发数据时,执行体一直在运行for循环,自然而然cpu就飙上去了,直到手动设置的超时4分钟结束才中断连接,也就是这4分钟内其他服务都不能访问!!虽然CPU升高是由于右括号的服务异常,继而导致php执行体服务运行不正常最终导致的,但是我们还是要尽量避免由于别的模块或者服务不正常导致自己的服务出问题继而瘫痪整个系统的情况。要尽量把异常缩小在可控范围内。
  3. 解决
    知道问题所在了,那么就很好解决了,可以通过以下方式进行修改:
  • 设置为非阻塞模式,并增加读取数据超时时间设置;

    socket_set_option($socket,SOL_SOCKET,SO_RCVTIMEO,array("sec"=>10, "usec"=>0 ) );

    l82i5lj2.png
    记住:一定要注释掉非阻塞改为阻塞,不然即使加了阻塞超时设置也不会生效,因为本质是非阻塞,非阻塞就是死循环。

  • 增加多路复用阻塞:
    假如我一定要用非阻塞设置该怎么办呢?
    办法很多,那就是在read前面加多路复用阻塞:
    l82iaqji.png
    参数 描述
    read 指向一组等待可读性检查的套接字
    write 指向一组等待可写性检查的套接字
    except 指向一组等待错误检查的套接字
    tv_sec 用来设置select()的等待时间,秒
    tv_usec两者组成了 用来设置select()的等待时间,微妙
    这种方式也很好理解,就相当于在read非阻塞模式前加了阻塞判断,select来监听read的数据。当读取出来数据为空时会一直读,直到10s为止,可以理解为比sleep()更高级点,PHP的socket_select函数也是调用系统的select函数实现的。PHP中socket_select()函数传入的read和write数组是引用传入的,所以每次调用socket_select()后read和write或者except数组中会包含最新的可以使用的资源数组。传入的是要监视的,而调用socket_select后得到的是可以用的。
    多路是指多个客户端连接socket,复用就是指复用少数几个进程,多路复用本身依然隶属于同步通信方式,只是表现出的结果看起来像异步,这点值得注意.目前多路复用有三种常用的方案,依次是:
    select,最早的解决方案
    poll,算是select的升级版
    epoll,目前的最终解决版,解决c10k问题的功臣
  • 使用其他方式
    其实该问题的本质还是解决死循环过程中CPU过高的问题,只不过我们改为阻塞,就避免了程序死循环过程中被过多占用CPU资源的问题,我们其实可以直接暴利一点使用sleep(),判断当接收不到数据时 就sleep来释放资源,不过这种方式过于暴利,会影响性能,你要知道循环一次sleep1毫秒 循环10000次就是10s 不可想象!!

    总结
    通过以上总结,我们在使用socket通信时,如果对并发量要求不高,尽量使用阻塞模式,由于我们使用了负载均衡,所以对单机并发量不是特别要求。
    阻塞的socket函数在调用send,recv,connect,accept等函数时,如果特定的条件不满足,就会阻塞其调用线程直至超时,非阻塞的socket恰恰相反。
    非阻塞模式一般用于需要支持高并发多QPS的场景(如服务器程序),但是正如前文所述,这种模式让程序的执行流和控制逻辑变得复杂;相反,阻塞模式逻辑简单,程序结构简单明了,常用于一些特殊场景中。

  • 应用场景一:某程序需要临时发送一个文件,文件分段发送,每发送一段,对端都会给予一个响应,该程序可以单独开一个任务线程,在这个任务线程函数里面,使用先send后recv再send再recv的模式,每次send和recv都是阻塞模式的。
  • 应用场景二:A端与B端之间的通信只有问答模式,即A端每发送给B端一个请求,B端比定会给A端一个响应,除此之外,B端不会向A端推送任何数据,此时A端就可以采用阻塞模式,在每次send完请求后,都可以直接使用阻塞式的recv函数接收应答包。