PHP中使用fsock实现伪异步

在Web开发中,我们经常会遇到这样一个场景:用户发起一个请求,处理这个请求的业务很简单,可能几毫秒或者几十毫秒就能完成;但是这个请求会影响另外一个业务系统,所以我们在处理完用户的请求后,不得不发起一个新的后台请求到相关的业务系统,而相关的业务系统响应我们的后台请求可能非常慢,这样就会延长我们的整个响应时间。

  实例:我们的用户头像会保存到CDN,当用户更换头像的时候,上传写入可以很快的完成,但是调用CDN的purge接口可能要很久(好几秒),这样就会导致整个上传过程耗时很长。

当然,要解决这个问题有很多办法;比如把purge接口的请求丢给消息队列,后台计划任务不停地消费消息队列发起purge请求;如果这么做确实可以解决问题,但是把整体架构复杂化了。

不用消息队列,很多人就会想到用异步;但是项目使用的是PHP,它是顺序执行,不支持异步的,怎么办?

拜读Laruence早期的文章:

  1. 《PHP实现异步调用方法研究》
  2. 《使用fscok实现异步调用PHP》

根据文章中的介绍,果断采用fsockopen的方法:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

<?php

function asyncGet($url)

{

$parts = parse_url($url);

$fp = fsockopen(

$parts['host'],

isset($parts['port']) ? $parts['port'] : 80,

$errno, $errstr, 3

);

$out = "GET " . $parts['path'] . " HTTP/1.1\r\n";

$out .= "Host: " . $parts['host'] . "\r\n";

$out .= "Connection: Close\r\n\r\n";

fwrite($fp, $out);

fclose($fp);

}

$start = microtime(true);

asyncGet(

'http://cdn.ljf.me/purge.php'

);

$end = microtime(true);

$cost = $end - $start;

echo "purge cdn cost: $cost s\n";

问题来了,无论如何发送请求,服务器都没有正常地清洗缓存(直接输入url访问是可以正常清洗的,排除purge接口本身的问题)。

用WireShark抓到数据包来看,请求确实是有发出,也是正常的。

为什么请求有发出,但是服务器并没有进行清洗CDN工作,问题可能出现在哪里?

本地搭出一个测试环境。使用相同的脚本发完请求后,查看php-fpm的access日志没有找到刚才的请求,但是nginx的access是有这个请求的,状态码是499。

?

1

127.0.0.1 - - [18/Dec/2013:11:57:57 +0800] "GET /purge.php HTTP/1.1" 499 0 "-" "-" "-"

在HTTP协议中没有直接定义499的状态码,这个状态码是nginx指定的。

?

1

2

3

4

5

6

7

8

/* from ngx_http_request.h */

/*

* HTTP does not define the code for the case when a client closed

* the connection while we are processing its request so we introduce

* own code to log such situation when a client has closed the connection

* before we even try to send the HTTP header to it

*/

#define NGX_HTTP_CLIENT_CLOSED_REQUEST     499

nginx对499的定义是”client has closed connection”,并且在这些情况下会返回这个状态码:

  1. upstream 在收到读写事件处理之前时发现连接不可用。
  2. server处理请求未结束,而client提前关闭了连接。
  3. upstream出错,执行next_upstream时发现连接不可用。

现在的问题是:我们要等nginx的upstream处理完并且把请求交给fastcgi之后,才能主动关闭连接,否则就不能正常的清洗CDN的缓存了。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

function asyncGet($url)

{

$parts = parse_url($url);

$fp = fsockopen(

$parts['host'],

isset($parts['port']) ? $parts['port'] : 80,

$errno, $errstr, 3

);

$out = "GET " . $parts['path'] . " HTTP/1.1\r\n";

$out .= "Host: " . $parts['host'] . "\r\n";

$out .= "Connection: Close\r\n\r\n";

fwrite($fp, $out);

usleep(10000);

fclose($fp);

}

一个不安全的做法是在fclose之前,让当前的进程先睡眠一段时间;我这里设置为10毫秒,这10毫秒的延迟对我完成整个请求的影响不大,同时我也认为nginx一定能在10毫米内把请求转到fastcgi去执行。这个时间间隔很难把握,不能保证php一定有执行到。

这种方式并不是真正的异步,只是很取巧的强制关闭连接而不等待服务器端响应。所以在Laruence的那2篇文章中,有2个问题:

  • PHP使用fsock不能叫做异步,只是伪异步。
  • fwrite之后马上执行fclose,nginx会直接返回499。

Laruence这2篇博文都是08年写的,不知道当时是不是用apache做的测试。因为没有使用apache的场景,所以也就不打算用apache再验证一次。

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s