当前位置: 首页 > 文档资料 > Swoole 中文文档 >

使用问题

优质
小牛编辑
137浏览
2023-12-01

Swoole性能如何

QPS对比

使用 Apache-Bench工具(ab) 对Nginx静态页、Golang Http程序、PHP7+Swoole Http程序进行压力测试。在同一台机器上,进行并发100共100万次Http请求的基准测试中,QPS对比如下:

软件QPS软件版本
Nginx164489.92nginx/1.4.6 (Ubuntu)
Golang166838.68go version go1.5.2 linux/amd64
PHP7+Swoole287104.12Swoole-1.7.22-alpha
Nginx-1.9.9245058.70nginx/1.9.9

!> 注:Nginx-1.9.9的测试中,已关闭access_log,启用open_file_cache缓存静态文件到内存

测试环境

  • CPU:Intel® Core™ i5-4590 CPU @ 3.30GHz × 4
  • 内存:16G
  • 磁盘:128G SSD
  • 操作系统:Ubuntu14.04 (Linux 3.16.0-55-generic)

压测方法

ab -c 100 -n 1000000 -k http://127.0.0.1:8080/

VHOST配置

server {
    listen 80 default_server;
    root /data/webroot;
    index index.html;
}

测试页面

<h1>Hello World!</h1>

进程数量

Nginx开启了4个Worker进程

htf@htf-All-Series:~/soft/php-7.0.0$ ps aux|grep nginx
root      1221  0.0  0.0  86300  3304 ?        Ss   12月07   0:00 nginx: master process /usr/sbin/nginx
www-data  1222  0.0  0.0  87316  5440 ?        S    12月07   0:44 nginx: worker process
www-data  1223  0.0  0.0  87184  5388 ?        S    12月07   0:36 nginx: worker process
www-data  1224  0.0  0.0  87000  5520 ?        S    12月07   0:40 nginx: worker process
www-data  1225  0.0  0.0  87524  5516 ?        S    12月07   0:45 nginx: worker process

Golang

测试代码

package main

import (
    "log"
    "net/http"
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU() - 1)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Add("Last-Modified", "Thu, 18 Jun 2015 10:24:27 GMT")
        w.Header().Add("Accept-Ranges", "bytes")
        w.Header().Add("E-Tag", "55829c5b-17")
        w.Header().Add("Server", "golang-http-server")
        w.Write([]byte("<h1>\nHello world!\n</h1>\n"))
    })

    log.Printf("Go http Server listen on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

PHP7+Swoole

PHP7已启用OpCache加速器。

测试代码

$http = new Swoole\Http\Server("127.0.0.1", 9501, SWOOLE_BASE);

$http->set([
    'worker_num' => 4,
]);

$http->on('request', function ($request, Swoole\Http\Server $response) {
    $response->header('Last-Modified', 'Thu, 18 Jun 2015 10:24:27 GMT');
    $response->header('E-Tag', '55829c5b-17');
    $response->header('Accept-Ranges', 'bytes');    
    $response->end("<h1>\nHello Swoole.\n</h1>");
});

$http->start();

全球Web框架权威性能测试 Techempower Web Framework Benchmarks

最新跑分测试结果地址: techempower

Swoole领跑动态语言第一

数据库IO操作测试, 使用基本业务代码无特殊优化

性能超过所有静态语言框架(使用MySQL而不是PostgreSQL)

Swoole如何维持TCP长连接

关于TCP长连接维持有2组配置tcp_keepaliveheartbeat,使用方法和注意事项参考 Swoole官方视频教程

Swoole如何正确的重启服务

在我们修改了PHP代码后经常需要重启服务让代码生效,一台繁忙的后端服务器随时都在处理请求,如果管理员通过kill进程方式来终止/重启服务器程序,可能导致刚好代码执行到一半终止,没法保证整个业务逻辑的完整性。

Swoole提供了柔性终止/重启的机制,管理员只需要向Server发送特定的信号或者调用reload方法,工作进程就可以结束,并重新拉起。具体请参考reload()

但有几点要注意:

首先要注意新修改的代码必须要在OnWorkerStart事件中重新载入才会生效,比如某个类在OnWorkerStart之前就通过composer的autoload载入了就是不可以的。

其次reload还要配合这两个参数max_wait_timereload_async,设置了这两个参数之后就能实现所谓的异步安全重启特性。

如果没有此特性,Worker进程收到重启信号或达到max_request时,会立即停止服务,这时Worker进程内可能仍然有事件监听,这些异步任务将会被丢弃。设置上述参数后会先创建新的Worker,旧的Worker在完成所有事件之后自行退出,即reload_async。

如果旧的Worker一直不退出,底层还增加了一个定时器,在约定的时间( max_wait_time秒)内旧的Worker没有退出,底层会强行终止。

示例:

<?php
$serv = new Swoole\Server("0.0.0.0", 9501, SWOOLE_PROCESS);
$serv->set(array(
    'worker_num' => 1,
    'max_wait_time' => 60,
    'reload_async' => true,
));
$serv->on('receive', function (Swoole\Server $serv, $fd, $reactor_id, $data) {

    echo "[#" . $serv->worker_id . "]\tClient[$fd] receive data: $data\n";

    Swoole\Timer::tick(5000, function () {
        echo 'tick';
    });
});

$serv->start();

例如上面的代码 如果没有 reload_async 那么 onReceive 中创建的定时器将丢失,没有机会处理定时器中的回调函数。

进程退出事件

为了支持异步重启特性,底层新增了一个onWorkerExit事件,当旧的Worker即将退出时,会触发onWorkerExit事件,在此事件回调函数中,应用层可以尝试清理某些长连接Socket,直到事件循环中没有fd或者达到了max_wait_time退出进程。

$serv->on('WorkerExit', function (Swoole\Server $serv, $worker_id) {
    $redisState = $serv->redis->getState();
    if ($redisState == Swoole\Redis::STATE_READY or $redisState == Swoole\Redis::STATE_SUBSCRIBE)
    {
        $serv->redis->close();
    }
});

同时我们在Swoole Plus中增加了检测文件变化的功能,可以不用手动reload或者发送信号,文件变更自动重启worker。

为什么不要send完后立即close就是不安全的

send完后立即close就是不安全的,无论是服务器端还是客户端。

send操作成功只是表示数据成功地写入到操作系统socket缓存区,不代表对端真的接收到了数据。究竟操作系统有没有发送成功,对方服务器是否收到,服务器端程序是否处理,都没办法确切保证。

close后的逻辑请看下面的linger设置相关

这个逻辑和电话沟通是一个道理,A告诉B一个事情,A说完了就挂掉电话。那么B听到没有,A是不知道的。如果A说完事情,B说好,然后B挂掉电话,就绝对是安全的。

linger设置

一个socket在close时,如果发现缓冲区仍然有数据,操作系统底层会根据linger设置决定如何处理

struct linger
{
     int l_onoff;
     int l_linger;
};
  • l_onoff = 0,close时立刻返回,底层会将未发送完的数据发送完成后再释放资源,也就是优雅的退出。
  • l_onoff != 0,l_linger = 0,close时会立刻返回,但不会发送未发送完成的数据,而是通过一个RST包强制的关闭socket描述符,也就是强制的退出。
  • l_onoff !=0,l_linger > 0, closes时不会立刻返回,内核会延迟一段时间,这个时间就由l_linger的值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,close会返回正确,socket描述符优雅性退出。否则close会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。如果socket描述符被设置为非堵塞型,则close会直接返回值。

client has already been bound to another coroutine

对于一个TCP连接来说Swoole底层允许同时只能有一个协程进行读操作、一个协程进行写操作。也就是说不能有多个协程对一个TCP进行读/写操作,底层会抛出绑定错误:

Fatal error: Uncaught Swoole\Error: Socket#6 has already been bound to another coroutine#2, reading or writing of the same socket in coroutine#3 at the same time is not allowed 

重现代码:

Co\run(function() {
    $cli = new Swoole\Coroutine\Http\Client('www.xinhuanet.com', 80);
    go(function () use ($cli) {
        $cli->get("/");
    });
    go(function () use ($cli) {
        $cli->get('/');
    });
});

!> 此限制对于所有多协程环境都有效,最常见的就是在onReceive等回调函数中去共用一个TCP连接,因为此类回调函数会自动创建一个协程, 那有连接池需求怎么办?Swoole内置了连接池可以直接使用,或手动用channel封装连接池。

Call to undefined function Co\Run()

本文档中的大部分示例都使用了Co\run()来创建一个协程容器,了解什么是协程容器

如果遇到如下错误:

PHP Fatal error:  Uncaught Error: Call to undefined function Co\Run()

PHP Fatal error:  Uncaught Error: Call to undefined function go()

说明你的Swoole扩展版本小于v4.4.0或者手动关闭了协程短名称,提供三种解决方法

  • 如果是版本过低,则请升级扩展版本至>= v4.4.0或使用go关键字替换Co\Run来创建协程;
  • 如果是关闭了协程短名称,则请打开协程短名称
  • 使用Coroutine::create方法替换Co\Rungo来创建协程;

是否可以共用1个redis或mysql连接

绝对不可以。必须每个进程单独创建RedisMySQLPDO连接,其他的存储客户端同样也是如此。原因是如果共用1个连接,那么返回的结果无法保证被哪个进程处理,持有连接的进程理论上都可以对这个连接进行读写,这样数据就发生错乱了。

所以在多个进程之间,一定不能共用连接

示例:

$server = new Swoole\Server("0.0.0.0", 9502);

//必须在onWorkerStart回调中创建redis/mysql连接
$server->on('workerstart', function($server, $id) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    $server->redis = $redis;
});

$server->on('receive', function (Swoole\Server $server, $fd, $from_id, $data) { 
    $value = $server->redis->get("key");
    $server->send($fd, "Swoole: ".$value);
});

$server->start();

连接已关闭问题

如以下提示

NOTICE  swFactoryProcess_finish (ERRNO 1004): send 165 byte failed, because connection[fd=123] is closed
NOTICE  swFactoryProcess_finish (ERROR 1005): connection[fd=123] does not exists

服务端响应时, 客户端已经切断了连接导致

常见于:

  • 浏览器疯狂刷新页面(还没加载完就刷掉了)
  • ab压测到一半取消
  • wrk基于时间的压测 (时间到了未完成的请求会被取消)

以上几种情况均属于正常现象, 可以忽略, 所以该错误的级别是NOTICE

如由于其它情况无缘无故出现大量连接断开时, 才需要注意

connected属性和连接状态不一致

4.x协程版本后, connected属性不再会实时更新, isConnect方法不再可靠

原因

协程的目标是和同步阻塞的编程模型一致, 同步阻塞模型中不会有实时更新连接状态的概念, 如PDO, curl等, 都没有连接的概念, 而是在IO操作时返回错误或抛出异常才能发现连接断开

Swoole底层通用的做法是, IO错误时, 返回false(或空白内容表示连接已断开), 并在客户端对象上设置相应的错误码, 错误信息

注意

尽管以前的异步版本支持"实时"更新connected属性, 但实际上并不可靠, 连接可能会在你检查后马上就断开了