简介
“作为世界上最好的开发语言”,php其高效的开发效率一直被人津津乐道,就像孪生兄弟一样总是如影随形,它的性能问题也一直被人诟病。平时开发过程中,一言不合就直接var_dump然后exit(),调试起来极其方便;还记得上份工作,web侧是用php开发、后台api用的是java,基本每次发布,都是等后台api发布,一个小时候,问他们怎么样了,“已经编译好了一半”,等api OK了,web侧一分钟就上线完成;也正因为如何,不需要每次都进行编译后执行,所以其性能问题也没有C、java等表现优异。
问题的出现也是进步最大的动力,于是乎php的优化、各种扩展的出现就是推动下的产物。最近表现比较优异的一个就是swoole:PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写;其号称 简单易用开发效率高、并发百万TCP连接……
说了这么多,要说的重点终于要登场了,那就是tsf,全称“Tencent Server Framework”;网络层用的是上面提及的swoole,并结合php的协程,实现php的异步、高并发性能。
DEMO
- Swoole进程模型
- TSF运行架构图
- TSF协程
- 支持start,stop,reload,restart,shutdown, status
- 性能揭秘
TSF的优势
传统的php一般在有数据交互、网络请求的时候(比如db读取、调用后台接口)时都是串行处理的;比如当整个执行过程中有3次接口调用时,总是按顺序执行的:先发送第一次接口调用请求,此时cpu处于等待接口响应状态,等到接口返回数据时,再进行第二次接口调用、然后第三次;可以看出,这种方式下,每次进行网络请求,cpu总会有段时间处于等待状态,不继续往下执行,也不处理其他请求,明显是占着茅坑不拉屎,感觉服务处理请求的效率很低。当接口调用又比较耗时时,整个服务的处理效率更是惨不忍睹。
就好比说,一个人带着一本书去钓鱼。他的打算是钓到一条鱼(假如钓到一条鱼的等待时间是10分钟),然后看10分钟书。于是他下好钩,然后就什么也不做,闭目养神等着鱼儿上钩;10分钟后,鱼上钩了,他就暂停钓鱼,接下来看了10分钟的书;然后再钓鱼、看书……两个小时的时间内,他一共钓了6条鱼,看了一个小时的书。此时,你也许禁不住“噗哧”一笑道“这个人是不是傻,他可以边钓鱼边看书”。确实这样!但一般的php执行过程就是这样的!
基于这个痛点,tsf进行了很大改善,实现了网络请求的并行处理;比如和上面一样的情况,整个执行过程有3次接口调用;同样,也会先发送第一次调用请求,接下来不同的是,tsf会保存此时执行的上下文,然后让出cpu给系统进行其他任务的处理;当接口响应时,再切回到之前保存的上下文继续执行。可以看出,和上面不同的是,tsf发送网络请求后,会让出cpu,而不是让cpu无所事事地等待响应,实现了网络调用的异步请求。可以进行更多地任务处理,大幅地提高了服务处理的QPS
上面的钓鱼例子中,此时相当于按照你想的的来做了,下好钩,然后看书,等鱼上钩……忽略鱼儿上钩后收鱼的时间,两个小时的时间内,一共钓到了12条鱼,同时也看了2个小时的书,效率是不是一下子就上去了呢!
执行流程的分析
tsf属于常驻内存的服务,一直监听某个端口来进行响应处理。可以理解把swoole理解为是一个事件驱动的,类似于nodejs,在服务初始化函数中,有这么一段代码
tsf/core/Server.php
if ($this->servType == 'http')
{
$this->sw->on('Request', array($this, 'onRequest'));
}
所以当过来一个httpd请求时,就会出发 server.php 中onRequest()方法,然后又调用 protocal中的onRequest()方法,经过路由后,转发到对应的class->controller->action后,进行协程调度
tsf/core/Event.php->onRequest()
$request->scheduler->newTask($obj->run($fun));
$request->scheduler->run();
把上述class->controller->action的执行创建为一个新的task任务,放进队列,然后再去扫描队列,如果队列中还有任务,则继续执行;task的执行会放在下面进行描述
tsf/coroutine/Scheduler.php
<?php
namespace tsf\coroutine;
class Scheduler
{
protected $maxTaskId = 0;
protected $taskQueue;
public function __construct()
{
$this->taskQueue = new \SplQueue();
}
public function newTask($coroutine)
{
if ($coroutine instanceof \Generator)
{
$taskId = ++ $this ->maxTaskId;
$task = new Task($taskId, $coroutine);
$this ->taskQueue ->enqueue($task);
}
}
public function schedule(Task $task)
{
$this->taskQueue->enqueue($task);
}
public function run()
{
while (!$this->taskQueue->isEmpty())
{
$task = $this->taskQueue->dequeue();
$task->run($task->getCoroutine());
}
}
}
上面是把class->controller->action当作一个整体塞进队列中,而action中又会有N多代码、方法的执行,task就是处理action的具体执行。task的初始化函数对把上面入队列产生的$taskId、$coroutine传进来,另外会新建一个栈用于保存中断
tsf/coroutine/Task.php
public function __construct($taskId, \Generator $coroutine)
{
$this->taskId = $taskId;
$this->coroutine = $coroutine;
$this->corStack = new \SplStack();
}
对于同步调用的执行就不描述,主要介绍下yield中断时的执行,如果中断是异步IO时,则入栈、然后发包;入栈相当于保存此时执行的上下文,然后交出cpu的控制权,等待IO的返回,cpu继续干其他的活。
tsf/coroutine/Task.php->run()
$value = $gen->current();
...
if (is_subclass_of($value, 'tsf\client\Base'))
{
//async send push gen to stack
Log::debug(__METHOD__ . " value is IO ", __CLASS__);
$this->corStack->push($gen);
$value->send(array($this, 'callback'));
return;
}
上面发包处代码一般是 $ret = new /tsf/clien/Tcp($ip, $port, $data, $timeout)
,在代码执行处是没有看到实际发包操作的;其实实际发包操作是在Task.php->run()中执行的,即上面代码中的 $value->send(array($this, 'callback'))
此时调用Tcp.php 中的send()方法
tcp/client/Tcp.php
public function send(callable $callback)
{
$client = new \swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
$client->on("connect", function ($cli) {
$cli->send($this->data);
});
$client->on('close', function ($cli) {
});
$client->on('error', function ($cli) use ($callback) {
$cli->close();
$this->calltime = microtime(true) - $this->calltime;
call_user_func_array($callback, array('r' => 1, 'key' => $this->key, 'calltime' => $this->calltime, 'error_msg' => 'conncet error'));
});
$client->on("receive", function ($cli, $data) use ($callback) {
$cli->close();
$this->calltime = microtime(true) - $this->calltime;
call_user_func_array($callback, array('r' => 0, 'key' => $this->key, 'calltime' => $this->calltime, 'data' => $data));
});
if ($client->connect($this->ip, $this->port, $this->timeout, 1))
{
$this->calltime = microtime(true);
if (floatval(($this->timeout)) > 0)
{
Timer::add($this->key, $this->timeout, $client, $callback, array('r' => 2, 'key' => $this->key, 'calltime' => $this->calltime, 'error_msg' => $this->ip . ':' . $this->port . ' timeout'));
}
}
}
对的,就是在上面的on(connect)事件来触发send()发包的,当receive到数据后,就执行call_user_func_array($callback, array('r' => 0, 'key' => $this->key, 'calltime' => $this->calltime, 'data' => $data));
,其中$callback是Task.php->run()处理IO中断时传过来的,所以此时就回调Task.php中的callback方法
tsf/coroutine/Task.php
public function callback($r, $key, $calltime, $res)
{
$gen = $this->corStack->pop();
$this->callbackData = array('r' => $r, 'calltime' => $calltime, 'data' => $res);
Log::debug(__METHOD__ . " data back and corStack pop and send ", __CLASS__);
try {
$value = $gen->send($this->callbackData);
}catch (\Exception $e) {
$this->setException($e);
}
$this->run($gen);
}
出栈,相当于恢复之前保存的上下文,回到中断出,把数据回传回去,拿回cpu的控制权,然后继续往下执行;这样是实现了网络IO的异步,最大化地利用了cpu。
总结
总的来说,tsf更有效地利用了cpu,提升了QPS及服务器的处理能力,但是对自己来说不够完美的点:
-
调试不方便:因为是常驻内存的,所以当你修改代码后,如果要进行调试,需要先把代码上传到服务器,但是此时执行的还是之前的代码,新修改的还没生效;还需要进行重启服务reload才能生效;另外因为习惯了
var_dump();exit()
进行调试,但是在tsf中这种方式却不行,一般通过记录log,然后查看log进行调试。 -
reload问题: 有时reload后,却未成功,需要收到stop后再start。并且reload不是完全平滑的,当reload时,可能有的正在等待IO响应,而reload后,之前的监听已经停止了,所以IO响应时,可能会找不到回调地方。