浅析tsf

2016/07/01

简介

        “作为世界上最好的开发语言”,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进程模型

model

  • TSF运行架构图

framework

  • TSF协程

coroutine

  • 支持start,stop,reload,restart,shutdown, status

server

  • 性能揭秘

perform

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及服务器的处理能力,但是对自己来说不够完美的点:

  1. 调试不方便:因为是常驻内存的,所以当你修改代码后,如果要进行调试,需要先把代码上传到服务器,但是此时执行的还是之前的代码,新修改的还没生效;还需要进行重启服务reload才能生效;另外因为习惯了var_dump();exit()进行调试,但是在tsf中这种方式却不行,一般通过记录log,然后查看log进行调试。

  2. reload问题: 有时reload后,却未成功,需要收到stop后再start。并且reload不是完全平滑的,当reload时,可能有的正在等待IO响应,而reload后,之前的监听已经停止了,所以IO响应时,可能会找不到回调地方。

Post Directory