10
头图

swoole异步任务使用教程
学习swoole的总体流程先梳理一下。

1.swoole异步能解决什么样的问题?

2.swoole异步任务要如何使用?

3.使用时应注意什么细节?


1.swoole异步能解决什么样的问题?

相信大家在现在或未来的工作中,都会在遇到下面的一些问题。

问题1.测试同事说,哇这个接口好慢呀,平均响应时间都超过2秒了。能不能优化一下啊?然后自己一个,原来这里有个发邮件或者发短信验证码的操作。仔细一排查原来是因为第三方接口响应慢,导致自己的接口响应也慢。

这时swoole异步任务就很适合解决这种问题了。可以把这个操作投递到 TaskWorker 进程池中执行,投递后程序会立即返回,程序继续向下执行代码,不会拖慢当前的进程的处理速度。

这种场景只是举例,业务中如果还有类似的操作,都可以丢到异步任务中执行。

2.swoole异步任务要如何使用?

知道能解决什么问题了,那要怎么去使用呢?

首先先捋清楚使用的大概逻辑。逻辑:我们需要创建一个swoole服务,然后通过客户端把任务send到服务端,最后服务端接受到数据后,执行对应的操作,结束。

我结合自己的使用经历,总结了下面3个步骤。PS:自己先安装好swoole扩展哦

1.创建一个swoole服务

2.将任务投递到task进程池中

3.在task中编写自己的要执行的操作

2.1 创建一个swoole服务,其实就是初始化swoole\server对象

因为task任务是swoole\server对象的方法,所以我们需要先new 一个 swoole\server对象,并设置对象的一些配置和注册一些方法,就能调用了。

初始化时

必须要设置的点

  1. task_worker_num task进程的数量
  2. 注册onTask和onFinish这两个方法

下面是我封装的初始化swoole\server对象代码,可供参考。其中自行添加类对应的命名空间即可。

class Server
{
    private $serv;

    /**
     * Server constructor.
     */
    public function __construct()
    {
        //new一个swoole\server对象,可以new Swoole\Server('127.0.0.1', 9501)这样写,也可以new \swoole_server("127.0.0.1", 9501)这样写,别名而已,其实都是一样的。
        //其中127.0.0.1是指定监听的 ip 地址。IPv4 使用 127.0.0.1 表示监听本机,0.0.0.0 表示监听所有地址一般监听本机即可
        //$port监听的端口,如9501 0-1024之间,是系统默认保留的,所在建议从5000开始,一般使用默认的9501
        $this->serv = new \swoole_server("127.0.0.1", 9501);

        //配置参数
        $this->serv->set(array(
            'task_worker_num' => 200, //task进程的数量

            //下面是一些常用的配置参数说明
            //'worker_num' => 32, //worker进程数量一般设置为服务器CPU数的1-4倍1
            //'daemonize' => 1, //111以守护进程执行11
            //'max_request' => 2000,
            //'dispatch_mode' => 3,//抢占模式,主进程会根据Worker的忙闲状态选择投递,只会投递给处于闲置状态的Worker
            //"task_ipc_mode " => 1, // 使用Unix Socket通信,默认模式
            //"log_file" => "log/taskqueueu.log" ,//日志
        ));

        // bind事件对应的方法。

        //当客户端发送数据时,会触发Receive函数。此方法中能接受到客户端send的参数
        $this->serv->on('Receive', array($this, 'onReceive'));

        //在task方法中,执行对应的操作
        $this->serv->on('Task', array($this, 'onTask'));

        //当任务执行完成后,会触发在finish事件。
        $this->serv->on('Finish', array($this, 'onFinish'));

        //启动swoole服务
        $this->serv->start();
    }


    /**
     * Notes: 接收数据时触发的回调函数
     * User: 闻铃
     * @param $serv swoole\server对象
     * @param $fd $连接的文件描述符
     * @param $from_id TCP 连接所在的 Reactor 线程 ID
     * @param $data  收到的数据内容,可能是文本或者二进制内容
     */
    public function onReceive($serv, $fd, $from_id, $data)
    {
        //把数据转发到task中
        $serv->task($data);
    }

    /**
     * Notes: 执行task任务
     * User: 闻铃
     * @param $serv swoole\server对象
     * @param $task_id task进程id
     * @param $from_id TCP 连接所在的 Reactor 线程 ID
     * @param $data 收到的数据内容,可能是文本或者二进制内容
     */
    public function onTask($serv, $task_id, $from_id, $data)
    {
        //暂时留空 2.3部分会完善
    }

    /**
     * Notes: task任务完成的回调
     * User: 闻铃
     * @param $serv swoole\server对象
     * @param $task_id  ask进程id
     * @param $data
     */
    public function onFinish($serv, $task_id, $data)
    {
        //echo "Task {$task_id} finish\n";
        //echo "Result: {$data}\n";
    }

    //调用这个runStart方法,即可创建一个swoole服务。我这边是在TP中啦,在public目录下,执行php index.php api/server/runStart 这段命令,即启动swoole。通过netstat -tunlp 看是否启动成功
    public function runStart()
    {
        
    }
}

2.2 将任务投递到task进程池中

通过客户端类的send方法即可投递到swoole服务中

class SwooleClient
{  
    private $client;

    public function __construct()  
    {  
        $this->client = new Swoole\Client(SWOOLE_SOCK_TCP);
    }  
  
    public function connect()  
    {  
       //9501要和swoole服务监听的端口号一致
        if (!$this->client->connect("127.0.0.1", 9501, -1)) {
            throw new \Exception(sprintf('Swoole Error: %s', $this->client->errCode));
        }  
    }  
  
  //投递一个数据到swoole服务中
    public function send($data)  
    {  
        if ($this->client->isConnected()) {  
            if (!is_string($data)) {  
                $data = json_encode($data);  
            }
          //拼接"\r\n",是解决在循环场景下,投递任务可能会出现的tcp粘包问题。
            return $this->client->send($data."\r\n");  
        } else {  
            throw new \Exception('Swoole Server does not connected.');
        }  
    }  
  
    public function close()  
    {  
        $this->client->close();  
    }  
} 
在需要投递的场景时,new 一个客户端类,把数据投递到swoole服务中
$client = new Swoolecli();
$client->connect();
if ($client->send($value)) {
   //成功,关闭链接
        $client->close();
} else {
  //异常处理
}

如果是在swoole的协程框架中。可以直接投递。因为是常驻内存的,内存中有初始化好的swoole对象。可以不用通过客户端的方式投递,直接$server->task($data)即可

比如

在think-swoole中可以通过下面的方式直接投递

        $get = $this->request->get();
        $code = mt_rand(1111,9999);
        $phoneNum = $get['phone_num'];
        $taskData = [
            'method' => 'sendSms',
            'data'   => [
                'code'       => $code,
                'phone_num'  => $phoneNum
            ]
        ];
        //投递任务
        $this->app->swoole->task(json_encode($taskData));

在easyswoole中可以通过下面的方式直接投递

$server = ServerManager::getInstance()->getSwooleServer()->task(json_encode($task_data));

2.3 在task中编写自己的要执行的业务操作

      //执行异步任务
      public function onTask($serv, $task_id, $from_id, $data)
    {
          //一般发送过来的数据是json数据。
        $data = json_decode($data,true);
  
        //接受到task数据,进程业务逻辑处理。我一般喜欢新建一个执行异步任务的类,统一管理对应的操作。这种这个task里面,只写3句代码就行了。
        $method = $data['method'];
        return SwooleTask::getInstance()->$method($data['data'], $serv);
    }
      
      
class SwooleTask
{
    private static $obj = null;

    public static function getInstance()
    {
        if (is_null(self::$obj)) {
            self::$obj = new self();
        }
        return self::$obj;
    }

    /**
     * Notes: 发送短信验证码
     * User: 闻铃
     * DateTime: 2021/9/9 下午4:06
     * @param array $data
     * @return bool
     */
    public function sendSms($data = [])
    {
        if (empty($data)) {
            return false;
        }
        return MsgCode::send($data['phone_num'], $data['code']);
    }

    /**
     * 通过task机制推送数据给全部客户端
     * @param $data
     * @param $serv swoole server对象
     */
    public function pushLive($data, $serv) {
        //获取redis中的有序集合。
        $clients = get_redis_obj()->set();
                
        foreach($clients as $fd) {
            $serv->push($fd, json_encode($data,JSON_UNESCAPED_UNICODE));
        }
    }
}

3.使用时应注意什么细节?

1.swoole服务的配置有很多,应根据业务场景和环境设置合适的配置。具体配置的话请参考官方文档,如下

全部的配置:https://wiki.swoole.com/#/ser...

下面是我总结了常用的配置

1.'worker_num' => 32, //一般设置为服务器CPU数的1-4倍 lscpu命令,cpus参数,即cpu核数
描述:指定启动的worker进程数。
说明:swoole是master-> n * worker的模式,开启的worker进程数越多,server负载能力越大,但是相应的server占有的内存也会更多。同时,当worker进程数过多时,进程间切换带来的系统开销也会更大。因此建议开启的worker进程数为cpu核数的1-4倍。
  
2.'ipc_mode' => 1
  描述:设置进程间的通信方式。
说明:共有三种通信方式,参数如下
  1 => 使用unix socket通信
2 => 使用消息队列通信
3 => 使用消息队列通信,并设置为争抢模式
  
3.'max_request' => 2000,
描述:每个worker进程允许处理的最大任务数。
说明:设置该值后,每个worker进程在处理完max_request个请求后就会自动重启。设置该值的主要目的是为了防止worker进程处理大量请求后可能引起的内存溢出。    
  
4.'max_conn' => 10000
  描述:服务器允许维持的最大TCP连接数
说明:设置此参数后,当服务器已有的连接数达到该值时,新的连接会被拒绝。另外,该参数的值不能超过操作系统ulimit -n的值,同时此值也不宜设置过大,因为swoole_server会一次性申请一大块内存用于存放每一个connection的信息。
 
  5.'dispatch_mode' => 3
  描述:指定数据包分发策略。
说明:共有三种模式,参数如下:
1 => 轮循模式,收到会轮循分配给每一个worker进程
2 => 固定模式,根据连接的文件描述符分配worker。这样可以保证同一个连接发来的数据只会被同一个worker处理
3 => 抢占模式,主进程会根据Worker的忙闲状态选择投递,只会投递给处于闲置状态的Worker
  
  
  
  6.'task_worker_num' => 8
  描述:服务器开启的task进程数。
说明:设置此参数后,服务器会开启异步task功能。此时可以使用task方法投递异步任务。

  7.'task_max_request' => 10000
设置此参数后,必须要给swoole_server设置onTask/onFinish两个回调函数,否则启动服务器会报错。
  描述:每个task进程允许处理的最大任务数。
说明:参考max_request task_worker_num
示例:

8.'task_max_request' => 10000
  描述:每个task进程允许处理的最大任务数。
说明:参考max_request task_worker_num
  
9.'task_ipc_mode' => 2
  描述:设置task进程与worker进程之间通信的方式。
说明:参考ipc_mode
  
  
  10.'daemonize' => 1, //以守护进程 1或0
设置程序进入后台作为守护进程运行。
说明:长时间运行的服务器端程序必须启用此项。如果不启用守护进程,当ssh终端退出后,程序将被终止运行。启用守护进程后,标准输入和输出会被重定向到 log_file,如果 log_file未设置,则所有输出会被丢弃。
  
  11.'log_file' => '/data/log/swoole.log'
  指定日志文件路径
说明:在swoole运行期发生的异常信息会记录到这个文件中。默认会打印到屏幕。注意log_file 不会自动切分文件,所以需要定期清理此文件。
  12.'heartbeat_check_interval' => 60
  设置心跳检测间隔
说明:此选项表示每隔多久轮循一次,单位为秒。每次检测时遍历所有连接,如果某个连接在间隔时间内没有数据发送,则强制关闭连接(会有onClose回调)。
  13.'heartbeat_idle_time' => 600
  设置某个连接允许的最大闲置时间。
说明:该参数配合heartbeat_check_interval使用。每次遍历所有连接时,如果某个连接在heartbeat_idle_time时间内没有数据发送,则强制关闭连接。默认设置为heartbeat_check_interval * 2。

2.投递任务时,如何在同一个链接,短时间内多次投递数据,可能会有数据粘包问题,解决方式如下

客户端
在send时,拼接"\r\n"       
比如 $this->client->send($data."\r\n");  

服务端新增配置
  
 1.自动分包
$server->set(array(
    'open_eof_split' => true, //swoole底层实现自动分包。比较消耗cpu资源
    'package_eof' => "\r\n", //设置后缀,一般为"\r\n"
));


或者
   2.手动分包
$server->set(array(
    'open_eof_check' => true,   //打开EOF检测,每次包以EOF结尾,才send给服务端
    'package_eof'    => "\r\n", //设置EOF
))
需要在应用层代码中手动 explode("\r\n", $data) 来拆分数据包
效率比较高
  
如果对效率比较看重则使用手动分包,看场景选择合适的即可

4.项目实战截图

swoole异步任务的执行效率非常的高。在数据量较大的情况下,效率依然很高。

swoole的异步任务进程池效率有多高呢?手上刚好有个项目就是用swoole的异步任务的,每天异步处理6000w的新增数据量,也就6个小时左右,而且速度的瓶颈其实是卡在mysql层面。

学会了后多使用swoole异步任务,提高自己的接口响应效率吧。
image.png

如果觉得有帮助到你,就给我点个赞鼓励一下让我有更多的动力继续写文章吧

本文为夜雨闻铃原创文章,转载无需和我联系,但请注明文章出处。文章出处:夜雨闻铃的思否文章(https://segmentfault.com/u/ye...)


夜雨闻铃
50 声望12 粉丝

分享和记录自己成长之路遇到学习到的经验和遇到的坑(偏php和go)。为了更好更活跃的社区,尽一份自己的微薄之力。