PHP程序员内功心法-趣说进程内连接池的作用和实现

在一个月黑风高的夜晚,郭靖冒着严寒登上了山顶,只见马钰道长已然等候多时,今日的课程还是练习打坐吐纳的功夫,仿佛耳边又传来的了熟悉的声音……

连接池是什么

我们常见的池很多,比如内存池,线程池,对象池,连接池等。顾名思义,池子干的事情都是一样的,把一类相同的事物放到一个池里面,已备不时之需,好比我们的蓄水池一样,把平日多余的水储蓄起来,一方面防止洪水到来时候对下游造成洪涝灾害,另一方面还可以合理灌溉资源利用,比如还可以水力发电。同样连接池是把已经已经建立好的连接放入一个池子,当请求到来可以直接拿来使用,这样就即解决了频繁的创建关闭连接带来的开销,也保护了后端服务,防止同时有大量连接涌入,造成危害。

连接池的种类

其实也就是连接池的使用场景

  • 可以是一个独立部署的服务,通过套接字提供代理服务。例如我们的常用的mysqlproxy。
  • 可以是一个服务内部进程间共享的连接池,这种相对更加轻量,可以理解为项目级别,只对内提供服务。
  • 进程内的连接池,更加轻量,当前进程内的线程或者协程可以使用。
  • 今天我们这里要介绍的是进程内的连接池,我们以PHP为例,使用协程并发的场景来观察连接池的作用效果。首先我们要心里琢磨,我们连接池的连接作用

  • 减少客户端使用连接时,创建和销毁连接的时间和系统资源开销,这里涉及到TCP的三次握手也四次挥手,还有TCP的慢启动预热。
  • 避免极端情况大量连接直接涌入后端服务,对整个系统服务造成危害。
  • 但同时也有一些缺点,比如空闲状态下也要维护一定数量的连接,占用客户端和服务端的资源,这里可以根据实际需求动态调配连接数,达到效率和资源利用的平衡。哪有一点资源不占用,还想系统高效稳定的事情,建个水坝还得占片地,护坝人间断性的职守呢。

    心中的明镜

    又进入我们的提前思考环节,例如我们要提供100QPS的服务用户查询服务,后端DB是Redis(也可以是mysql,我们这里只是假设,实际上redis的单机处理能力是10w/s这个数量级),我可以先事先创建好100个redis连接,每个请求到来拿一个连接使用,请求结束后再归还到连接池中。但是万一有超过预期并发量的连接应该怎么办呢,一般可以排队处理或者降级处理。排队时等待当前服务进程空闲后再处理,当然这会增加客户端的响应时间。降级处理是返回其他的数据,不走DB请求。

    下面秀出我们的基础代码,这里只是演示功能,没有对模块做进一步封装。

    Step 1

    最简单的http服务器

      
      
      
      
    1. class MyServer
    2. {
    3. public $server;
    4. function __construct()
    5. {
    6. $server = new Swoole\Http\Server("127.0.0.1", 9501);
    7. $this->server = $server;
    8. }
    9. function request($request, $response)
    10. {
    11. $redis = new redis;
    12. $redis->connect("127.0.0.1", 6379);
    13. $val = $redis->get("key");
    14. $response->end("

      Hello Swoole redis val $val #" . rand(1000, 9999) . "");

    15. }
    16. function start()
    17. {
    18. $this->server->on('request', [$this, "request"]);
    19. $this->server->set([
    20. 'worker_num' => 1
    21. ]);
    22. $this->server->start();
    23. }
    24. }
    25. (new MyServer())->start();

    我们使用 swoole process 多进程模式,只开启一个进程为方便调试,运行脚本后

      
      
      
      
    1. $ ps -ef | grep -v grep |grep server1.php
    2. shiguan+ 30587 8251 0 20:37 pts/11 00:00:00 php server1.php
    3. shiguan+ 30588 30587 0 20:37 pts/11 00:00:00 php server1.php
    4. shiguan+ 30590 30588 0 20:37 pts/11 00:00:00 php server1.php

    我们可以发现三个进程,熟悉swoole的同学都知道30590进程是工作进程.

  • 我们在命令行执行 curl 'http://127.0.0.1:9501' 可以得到服务器反馈 

    Hello Swoole redis val value2 #6642

  • 然后我们通过lsof -p 30590 查看工作进程打开的文件描述符, 发现并没有redis的连接.这是为什么呢?自问自答一波,因为在php的执行流程中,所有局部变量在退出当前作用域时,都会进行释放,也就是16行建立连接的$redis对象,在执行完毕当前请求后进行了释放,我们可以通过strace进一步验证
       
       
       
       
    1. $ sudo strace -s 1000 -p 30590
    2. strace: Process 30590 attached
    3. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    4. read(4, "\2\0\0\0N\0\0\0\0\0\0\0\3\0\0\0GET / HTTP/1.1\r\nHost: 127.0.0.1:9501\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n", 425952) = 94
    5. brk(0x55bdabaae000) = 0x55bdabaae000
    6. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 7
    7. fcntl(7, F_GETFL) = 0x2 (flags O_RDWR)
    8. fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK) = 0
    9. connect(7, {sa_family=AF_INET, sin_port=htons(6379), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
    10. poll([{fd=7, events=POLLIN|POLLOUT|POLLERR|POLLHUP}], 1, 60000) = 1 ([{fd=7, revents=POLLOUT}])
    11. getsockopt(7, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
    12. fcntl(7, F_SETFL, O_RDWR) = 0
    13. setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
    14. setsockopt(7, SOL_SOCKET, SO_KEEPALIVE, [0], 4) = 0
    15. poll([{fd=7, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 0 (Timeout)
    16. sendto(7, "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n", 22, MSG_DONTWAIT, NULL, 0) = 22
    17. poll([{fd=7, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 0 (Timeout)
    18. poll([{fd=7, events=POLLIN|POLLERR|POLLHUP}], 1, 60000) = 1 ([{fd=7, revents=POLLIN}])
    19. recvfrom(7, "$6\r\nvalue2\r\n", 8192, MSG_DONTWAIT, NULL, NULL) = 12
    20. close(7) = 0
    21. sendto(4, "\2\0\0\0\305\0\0\0\0\0\0\0\0\0\0\0HTTP/1.1 200 OK\r\nServer: swoole-http-server\r\nConnection: keep-alive\r\nContent-Type: text/html\r\nDate: Tue, 22 Oct 2019 12:43:10 GMT\r\nContent-Length: 44\r\n\r\n

      Hello Swoole redis val value2 #4823", 213, 0, NULL, 0) = 213

    22. brk(0x55bdab88e000) = 0x55bdab88e000
    23. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    24. read(4, "\2\0\0\0\0\0\0\0\0\0\4\0\3\0\0\0", 425952) = 16
    25. sendto(4, "\2\0\0\0\0\0\0\0\0\0\4\0\0\0\0\0", 16, 0, NULL, 0) = 16
    26. epoll_wait(3,
    我们可以发现连接到redis的fd,在执行完recv以后close(7)关闭了连接.
    话外音: 行走江湖重要招式,lsof -p pid, strace -p pid
  • 到这里和连接池没有半毛钱关系,因为这个服务是短连接,每次处理请求需要创建连接,关闭连接,对应有tcp的三次握手和四次挥手等老生长谈的问题,具体可以参考我们郭新华老师在Swoole微课程中的视频教程.

    Step 2

    感受一下长连接,我们可以通过将连接对象的变量赋值给类属性的简单操作,增加其引用计数,从而使得请求结束后不能对对象进行释放.

      
      
      
      
    1. class MyServer
    2. {
    3. public $server;
    4. public $pool;
    5. function __construct()
    6. {
    7. $server = new Swoole\Http\Server("127.0.0.1", 9501);
    8. $this->server = $server;
    9. }
    10. function request($request, $response)
    11. {
    12. $redis = new redis;
    13. $redis->connect("127.0.0.1", 6379);
    14. $this->pool[] = $redis;
    15. $val = $redis->get("key");
    16. $response->end("

      Hello Swoole redis val $val #" . rand(1000, 9999) . "");

    17. }
    18. function start()
    19. {
    20. $this->server->on('request', [$this, "request"]);
    21. $this->server->set([
    22. 'worker_num' => 1
    23. ]);
    24. $this->server->start();
    25. }
    26. }
    27. (new MyServer())->start();

    通过简单的代码修改,然后通过lsof -p 查看工作进程打开的文件描述符

      
      
      
      
    1. ...
    2. php 31598 shiguangqi 4u unix 0x0000000000000000 0t0 6577793 type=DGRAM
    3. php 31598 shiguangqi 5u unix 0x0000000000000000 0t0 6577794 type=DGRAM
    4. php 31598 shiguangqi 6u a_inode 0,13 0 11932 [signalfd]
    5. php 31598 shiguangqi 7u IPv4 6579223 0t0 TCP localhost:48048->localhost:6379 (ESTABLISHED)

    我们可以发现在最下方真的有打开的redis连接,同样也可以strace来跟踪请求的系统调用,这里我们省去.这个代码是我们每次请求都去创建新的连接,没有任何复用,基本无法使用.

    Step 3

    渐入佳境,我们想要的是可以重复利用的一个连接池,有几种选择

  • 当请求到来的时候,尝试从连接池中获取连接对象,如果连接池为空,创建连接对象,请求结束的时候,归还至连接池.
  • 进程启动的时候,创建固定数量的连接对象,当请求到来的时候,尝试从连接池中获取连接对象,如果连接池为空,继续等待或者服务降级; 不为空的话正常服务,请求结束的时候,归还至连接池.
  • 我们这里选择第一种方式,每个方式都各有优势,我们可根据自己情况进行取舍,下面是动态创建连接的实例代码

      
      
      
      
    1. class MyServer
    2. {
    3. public $server;
    4. public $pool;
    5. function __construct()
    6. {
    7. $server = new Swoole\Http\Server("127.0.0.1", 9501);
    8. $this->server = $server;
    9. $this->pool = new \SplQueue();
    10. }
    11. function request($request, $response)
    12. {
    13. if ($this->pool->count() > 0) {
    14. $redis = $this->pool->pop();
    15. } else {
    16. $redis = new redis;
    17. $redis->connect("127.0.0.1", 6379);
    18. }
    19. $val = $redis->get("key");
    20. $response->end("

      Hello Swoole redis val $val #" . rand(1000, 9999) . "");

    21. $this->pool->push($redis);
    22. }
    23. function start()
    24. {
    25. $this->server->on('request', [$this, "request"]);
    26. $this->server->set([
    27. 'worker_num' => 1
    28. ]);
    29. $this->server->start();
    30. }
    31. }
    32. (new MyServer())->start();

    我们这里实现了连接的动态创建和复用,可以通过strace来验证发现,两次连续的请求,第一次会创建连接,第二次会复用我们的fd

      
      
      
      
    1. $ sudo strace -s 1000 -p 1001
    2. [sudo] shiguangqi 的密码:
    3. strace: Process 1001 attached
    4. brk(0x556c8955b000) = 0x556c8955b000
    5. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    6. read(4, "\1\0\0\0N\0\0\0\0\0\0\0\3\0\0\0GET / HTTP/1.1\r\nHost: 127.0.0.1:9501\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n", 425952) = 94
    7. mmap(NULL, 2101248, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f61bd028000
    8. socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP) = 7
    9. close(7) = 0
    10. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 7
    11. fcntl(7, F_GETFL) = 0x2 (flags O_RDWR)
    12. fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK) = 0
    13. connect(7, {sa_family=AF_INET, sin_port=htons(6379), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
    14. poll([{fd=7, events=POLLIN|POLLOUT|POLLERR|POLLHUP}], 1, 60000) = 1 ([{fd=7, revents=POLLOUT}])
    15. getsockopt(7, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
    16. fcntl(7, F_SETFL, O_RDWR) = 0
    17. setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
    18. setsockopt(7, SOL_SOCKET, SO_KEEPALIVE, [0], 4) = 0
    19. poll([{fd=7, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 0 (Timeout)
    20. sendto(7, "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n", 22, MSG_DONTWAIT, NULL, 0) = 22
    21. poll([{fd=7, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 1 ([{fd=7, revents=POLLIN}])
    22. recvfrom(7, "$", 1, MSG_PEEK, NULL, NULL) = 1
    23. poll([{fd=7, events=POLLIN|POLLERR|POLLHUP}], 1, 60000) = 1 ([{fd=7, revents=POLLIN}])
    24. recvfrom(7, "$6\r\nvalue2\r\n", 8192, MSG_DONTWAIT, NULL, NULL) = 12
    25. getpid() = 1001
    26. getpid() = 1001
    27. fcntl(4, F_GETFL) = 0x802 (flags O_RDWR|O_NONBLOCK)
    28. fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0
    29. sendto(4, "\1\0\0\0\305\0\0\0\0\0\0\0\0\0\0\0HTTP/1.1 200 OK\r\nServer: swoole-http-server\r\nConnection: keep-alive\r\nContent-Type: text/html\r\nDate: Tue, 22 Oct 2019 13:17:45 GMT\r\nContent-Length: 44\r\n\r\n

      Hello Swoole redis val value2 #6094", 213, 0, NULL, 0) = 213

    30. munmap(0x7f61bd028000, 2101248) = 0
    31. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    32. read(4, "\1\0\0\0\0\0\0\0\0\0\4\0\3\0\0\0", 425952) = 16
    33. sendto(4, "\1\0\0\0\0\0\0\0\0\0\4\0\0\0\0\0", 16, 0, NULL, 0) = 16
    34. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    35. read(4, "\2\0\0\0N\0\0\0\0\0\0\0\3\0\0\0GET / HTTP/1.1\r\nHost: 127.0.0.1:9501\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n", 425952) = 94
    36. brk(0x556c8977b000) = 0x556c8977b000
    37. poll([{fd=7, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 0 (Timeout)
    38. sendto(7, "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n", 22, MSG_DONTWAIT, NULL, 0) = 22
    39. poll([{fd=7, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 0 (Timeout)
    40. poll([{fd=7, events=POLLIN|POLLERR|POLLHUP}], 1, 60000) = 1 ([{fd=7, revents=POLLIN}])
    41. recvfrom(7, "$6\r\nvalue2\r\n", 8192, MSG_DONTWAIT, NULL, NULL) = 12
    42. sendto(4, "\2\0\0\0\305\0\0\0\0\0\0\0\0\0\0\0HTTP/1.1 200 OK\r\nServer: swoole-http-server\r\nConnection: keep-alive\r\nContent-Type: text/html\r\nDate: Tue, 22 Oct 2019 13:17:46 GMT\r\nContent-Length: 44\r\n\r\n

      Hello Swoole redis val value2 #6308", 213, 0, NULL, 0) = 213

    43. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    44. read(4, "\2\0\0\0\0\0\0\0\0\0\4\0\3\0\0\0", 425952) = 16
    45. sendto(4, "\2\0\0\0\0\0\0\0\0\0\4\0\0\0\0\0", 16, 0, NULL, 0) = 16
    46. epoll_wait(3,

    可以验证在38行,第二次请求的时候,并没有重新创建连接,完全符合我们程序预期

    Step 4

    重点来了,通过观察系统调用我们可以发现,以上的例子我们使用的是单进程同步模式,也就是不支持单进程的并发处理.到这里协程的威力要出来了,我们可以支持单进程并发(php对多线程的支持不好,几乎没人使用php的ZTS版本)
    重点又又来了,我们需要做的只需要在(new MyServer())->start();前增加一行代码

      
      
      
      
    1. Swoole\Runtime::enableCoroutine();
    2. (new MyServer())->start();

    接下来观察工作进程两次请求的系统调用

      
      
      
      
    1. $ sudo strace -s 1000 -p 1747
    2. strace: Process 1747 attached
    3. brk(0x55f6d8c9e000) = 0x55f6d8c9e000
    4. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    5. read(4, "\1\0\0\0N\0\0\0\0\0\0\0\3\0\0\0GET / HTTP/1.1\r\nHost: 127.0.0.1:9501\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n", 425952) = 94
    6. mmap(NULL, 2101248, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3edce28000
    7. socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 7
    8. fcntl(7, F_GETFL) = 0x2 (flags O_RDWR)
    9. fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK) = 0
    10. setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
    11. setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
    12. connect(7, {sa_family=AF_INET, sin_port=htons(6379), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
    13. brk(0x55f6d8cc0000) = 0x55f6d8cc0000
    14. epoll_ctl(3, EPOLL_CTL_ADD, 7, {EPOLLOUT, {u32=7, u64=25769803783}}) = 0
    15. brk(0x55f6d8ca0000) = 0x55f6d8ca0000
    16. epoll_wait(3, [{EPOLLOUT, {u32=7, u64=25769803783}}], 4096, 60000) = 1
    17. epoll_ctl(3, EPOLL_CTL_DEL, 7, NULL) = 0
    18. getsockopt(7, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
    19. setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
    20. setsockopt(7, SOL_SOCKET, SO_KEEPALIVE, [0], 4) = 0
    21. recvfrom(7, 0x7f3ee07b69ef, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
    22. sendto(7, "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n", 22, 0, NULL, 0) = 22
    23. recvfrom(7, 0x7f3ee07b69ef, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
    24. recvfrom(7, "$6\r\nvalue2\r\n", 8192, 0, NULL, NULL) = 12
    25. getpid() = 1747
    26. getpid() = 1747
    27. fcntl(4, F_GETFL) = 0x802 (flags O_RDWR|O_NONBLOCK)
    28. fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0
    29. sendto(4, "\1\0\0\0\305\0\0\0\0\0\0\0\0\0\0\0HTTP/1.1 200 OK\r\nServer: swoole-http-server\r\nConnection: keep-alive\r\nContent-Type: text/html\r\nDate: Tue, 22 Oct 2019 13:27:04 GMT\r\nContent-Length: 44\r\n\r\n

      Hello Swoole redis val value2 #3338", 213, 0, NULL, 0) = 213

    30. munmap(0x7f3edce28000, 2101248) = 0
    31. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    32. read(4, "\1\0\0\0\0\0\0\0\0\0\4\0\3\0\0\0", 425952) = 16
    33. sendto(4, "\1\0\0\0\0\0\0\0\0\0\4\0\0\0\0\0", 16, 0, NULL, 0) = 16
    34. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    35. read(4, "\2\0\0\0N\0\0\0\0\0\0\0\3\0\0\0GET / HTTP/1.1\r\nHost: 127.0.0.1:9501\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n", 425952) = 94
    36. brk(0x55f6d8ec0000) = 0x55f6d8ec0000
    37. recvfrom(7, 0x7f3ee07b69ef, 1, MSG_PEEK, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
    38. sendto(7, "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n", 22, 0, NULL, 0) = 22
    39. recvfrom(7, "$", 1, MSG_PEEK, NULL, NULL) = 1
    40. recvfrom(7, "$6\r\nvalue2\r\n", 8192, 0, NULL, NULL) = 12
    41. sendto(4, "\2\0\0\0\305\0\0\0\0\0\0\0\0\0\0\0HTTP/1.1 200 OK\r\nServer: swoole-http-server\r\nConnection: keep-alive\r\nContent-Type: text/html\r\nDate: Tue, 22 Oct 2019 13:28:50 GMT\r\nContent-Length: 44\r\n\r\n

      Hello Swoole redis val value2 #1576", 213, 0, NULL, 0) = 213

    42. brk(0x55f6d8ca0000) = 0x55f6d8ca0000
    43. epoll_wait(3, [{EPOLLIN, {u32=4, u64=12884901892}}], 4096, -1) = 1
    44. read(4, "\2\0\0\0\0\0\0\0\0\0\4\0\3\0\0\0", 425952) = 16
    45. sendto(4, "\2\0\0\0\0\0\0\0\0\0\4\0\0\0\0\0", 16, 0, NULL, 0) = 16
    46. epoll_wait(3,

    可以发现是通过epoll_wait来监听redis句柄的读写事件.但是有个问题,如果当前时间有大量的请求涌入,会建立大量的redis连接,对后端服务造成杀伤,我们来通过ab压测演示一下

      
      
      
      
    1. $ ab -c 1000 -n 10000 'http://127.0.0.1:9501/'
    2. This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
    3. Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    4. Licensed to The Apache Software Foundation, http://www.apache.org/
    5. Benchmarking 127.0.0.1 (be patient)
    6. Completed 1000 requests
    7. Completed 2000 requests
    8. Completed 3000 requests
    9. Completed 4000 requests
    10. Completed 5000 requests
    11. Completed 6000 requests
    12. Completed 7000 requests
    13. Completed 8000 requests
    14. Completed 9000 requests
    15. Completed 10000 requests
    16. Finished 10000 requests
    17. Server Software: swoole-http-server
    18. Server Hostname: 127.0.0.1
    19. Server Port: 9501
    20. Document Path: /
    21. Document Length: 44 bytes
    22. Concurrency Level: 1000
    23. Time taken for tests: 0.512 seconds
    24. Complete requests: 10000
    25. Failed requests: 0
    26. Total transferred: 1920000 bytes
    27. HTML transferred: 440000 bytes
    28. Requests per second: 19528.96 [#/sec] (mean)
    29. Time per request: 51.206 [ms] (mean)
    30. Time per request: 0.051 [ms] (mean, across all concurrent requests)
    31. Transfer rate: 3661.68 [Kbytes/sec] received
    32. Connection Times (ms)
    33. min mean[+/-sd] median max
    34. Connect: 0 3 5.8 2 39
    35. Processing: 1 5 6.3 4 61
    36. Waiting: 1 4 6.3 3 61
    37. Total: 4 7 9.2 5 64
    38. Percentage of the requests served within a certain time (ms)
    39. 50% 5
    40. 66% 6
    41. 75% 6
    42. 80% 6
    43. 90% 7
    44. 95% 38
    45. 98% 44
    46. 99% 56
    47. 100% 64 (longest request)

    然后通过查看通过压测建立了多少连接

      
      
      
      
    1. $ lsof -p 2323 | grep 'localhost:6379 (ESTABLISHED)' | wc -l
    2. 129

    这里只是本地使用redis执行最简单的操作,如果请求IO时间较长,连接不能及时释放,会建立更多的连接.这里会对后端造成不可预估的杀伤.有没有什么办法可以限制并发数,对服务资源进行控制呢,答案是肯定的.我们可以使用channel来限制并发.具体channel的使用和原理请参考Twosee的课

    Step 5

      
      
      
      
    1. class MyServer
    2. {
    3. public $server;
    4. public $pool;
    5. public $chan;
    6. function __construct()
    7. {
    8. $server = new Swoole\Http\Server("127.0.0.1", 9501);
    9. $this->server = $server;
    10. $this->pool = new \SplQueue();
    11. }
    12. function request($request, $response)
    13. {
    14. $this->chan->push(true);
    15. if ($this->pool->count() > 0) {
    16. $redis = $this->pool->pop();
    17. } else {
    18. $redis = new redis;
    19. $redis->connect("127.0.0.1", 6379);
    20. }
    21. $val = $redis->get("key");
    22. $response->end("

      Hello Swoole redis val $val #" . rand(1000, 9999) . "");

    23. $this->pool->push($redis);
    24. $this->chan->pop();
    25. }
    26. function workerStart($server, $worker_id)
    27. {
    28. echo "worker start $worker_id\n";
    29. Swoole\Runtime::enableCoroutine();
    30. $this->chan = new Swoole\Coroutine\Channel(10);
    31. }
    32. function start()
    33. {
    34. $this->server->on('request', [$this, "request"]);
    35. $this->server->on('workerStart', [$this, "workerStart"]);
    36. $this->server->set([
    37. 'worker_num' => 1
    38. ]);
    39. $this->server->start();
    40. }
    41. }
    42. (new MyServer())->start();

    我们对上面的代码进行多次压测,

      
      
      
      
    1. $ ab -c 1000 -n 10000 'http://127.0.0.1:9501/'
    2. This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
    3. Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    4. Licensed to The Apache Software Foundation, http://www.apache.org/
    5. Benchmarking 127.0.0.1 (be patient)
    6. Completed 1000 requests
    7. Completed 2000 requests
    8. Completed 3000 requests
    9. Completed 4000 requests
    10. Completed 5000 requests
    11. Completed 6000 requests
    12. Completed 7000 requests
    13. Completed 8000 requests
    14. Completed 9000 requests
    15. Completed 10000 requests
    16. Finished 10000 requests
    17. Server Software: swoole-http-server
    18. Server Hostname: 127.0.0.1
    19. Server Port: 9501
    20. Document Path: /
    21. Document Length: 44 bytes
    22. Concurrency Level: 1000
    23. Time taken for tests: 0.477 seconds
    24. Complete requests: 10000
    25. Failed requests: 0
    26. Total transferred: 1920000 bytes
    27. HTML transferred: 440000 bytes
    28. Requests per second: 20949.87 [#/sec] (mean)
    29. Time per request: 47.733 [ms] (mean)
    30. Time per request: 0.048 [ms] (mean, across all concurrent requests)
    31. Transfer rate: 3928.10 [Kbytes/sec] received
    32. Connection Times (ms)
    33. min mean[+/-sd] median max
    34. Connect: 4 17 4.0 18 24
    35. Processing: 8 28 5.5 29 43
    36. Waiting: 5 23 6.0 23 39
    37. Total: 27 46 3.9 46 56
    38. Percentage of the requests served within a certain time (ms)
    39. 50% 46
    40. 66% 47
    41. 75% 48
    42. 80% 49
    43. 90% 50
    44. 95% 52
    45. 98% 53
    46. 99% 53
    47. 100% 56 (longest request)

    发现最多只会有10个连接,这里的连接数是我们进行硬编码设置.

      
      
      
      
    1. $ lsof -p 5093 | grep 'localhost:6379 (ESTABLISHED)' | wc -l
    2. 10

    总结

    需要说明的是,我们的实例代码只是演示,很多地方并不严谨而且没有模块化的封装,例如redis连接的建立没有检查成功,也没有处理redis请求的失败重连,还有很多细节需要完善.在生产环境当中,需要对外部的每一项资源保持警惕,不信任.连接很可能被服务端切断.这里也没有涉及到用户提交的参数等过程.
    我们通过渐进式的演进,来验证长连接的作用和使用方式,并且学会通过channel掌控并发能力,保护当前服务的资源,包括后端的资源,使我们的服务稳定,健壮.

    注:文章来源于网络,如有侵权请告知删除。

    你可能感兴趣的:(PHP程序员内功心法-趣说进程内连接池的作用和实现)