使用问题
Swoole性能如何
QPS对比
使用 Apache-Bench工具(ab) 对Nginx静态页、Golang Http程序、PHP7+Swoole Http程序进行压力测试。在同一台机器上,进行并发100共100万次Http请求的基准测试中,QPS对比如下:
软件 | QPS | 软件版本 |
---|---|---|
Nginx | 164489.92 | nginx/1.4.6 (Ubuntu) |
Golang | 166838.68 | go version go1.5.2 linux/amd64 |
PHP7+Swoole | 287104.12 | Swoole-1.7.22-alpha |
Nginx-1.9.9 | 245058.70 | nginx/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_keepalive和heartbeat,使用方法和注意事项参考 Swoole官方视频教程
Swoole如何正确的重启服务
在我们修改了PHP代码后经常需要重启服务让代码生效,一台繁忙的后端服务器随时都在处理请求,如果管理员通过kill
进程方式来终止/重启服务器程序,可能导致刚好代码执行到一半终止,没法保证整个业务逻辑的完整性。
Swoole
提供了柔性终止/重启的机制,管理员只需要向Server
发送特定的信号或者调用reload
方法,工作进程就可以结束,并重新拉起。具体请参考reload()
但有几点要注意:
首先要注意新修改的代码必须要在OnWorkerStart
事件中重新载入才会生效,比如某个类在OnWorkerStart
之前就通过composer的autoload载入了就是不可以的。
其次reload
还要配合这两个参数max_wait_time和reload_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\Run
或go
来创建协程;
是否可以共用1个redis或mysql连接
绝对不可以。必须每个进程单独创建Redis
、MySQL
、PDO
连接,其他的存储客户端同样也是如此。原因是如果共用1个连接,那么返回的结果无法保证被哪个进程处理,持有连接的进程理论上都可以对这个连接进行读写,这样数据就发生错乱了。
所以在多个进程之间,一定不能共用连接
- 在Swoole\Server中,应当在onWorkerStart中创建连接对象
- 在Swoole\Process中,应当在Swoole\Process->start后,子进程的回调函数中创建连接对象
- 此问题所述信息对使用
pcntl_fork
的程序同样有效
示例:
$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
属性, 但实际上并不可靠, 连接可能会在你检查后马上就断开了