协程的一些特性和优点我就不说了,网上很多文章都讲述的很透彻。
协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。
1、golang::goroutine
最近在学习 go,一些高大上的特性果然是为高并发而生,自带的 net/http 包对请求的处理也透明的放在了协程上下文中,真开箱即用。
server.go
package main
import (
"fmt"
"net/http"
"time"
"log"
"runtime"
"bytes"
"strconv"
)
func main() {
// 注册请求 handler
http.HandleFunc("/", func (responseWrite http.ResponseWriter, request *http.Request) {
log.Println("goroutine: ", GetGID(), "start")
// 模拟2秒的IO耗时操作 会发生协程切换
time.Sleep(2 * time.second)
// 协程继续执行
log.Println("goroutine: ", GetGID(), "end")
// 结束请求
fmt.Fprintln(responseWrite, "hello world!
")
})
log.Println("server start ...")
http.ListenAndServe(":8081", nil)
}
// 获取 goroutine 的协程 id
func GetGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
golang 自带的 net/http 包是会自行的创建一个协程去处理请求的,所以阻塞的 2秒 并不会将主进程挂起,主进程仍在不停的接收请求并创建一个协程去处理请求。
2、swoole::coroutine
Swoole在2.0开始内置协程(Coroutine)的能力,提供了具备协程能力IO接口(统一在命名空间SwooleCoroutine*)。
server.php
set([
'worker_num' => 1,
]);
$server->on("start", function ($server) {
echo "server start ..." . PHP_EOL;
});
// 处理请求
$server->on('request', function ($request, $response) {
echo "goroutine: " . Swoole\Coroutine::getuid() . " start" . PHP_EOL;
/**
* 这里我们需要使用 Swoole\Coroutine::sleep() 来模拟IO耗时 触发协程切换
* 真实的业务场景中你可能使用的是以下协程客户端
* Coroutine\Client Coroutine\Http\Client Coroutine\Http2\Client
* Coroutine\Redis Coroutine\Socket Coroutine\MySQL Coroutine\PostgreSQL
* php 的 sleep() 是没办法触发协程切换的 会被同步阻塞
* 这也是为什么 swoole 的协程中要使用指定的 IO 客户端才能发挥协程的性能
*/
Swoole\Coroutine::sleep(2);
echo "goroutine: " . Swoole\Coroutine::getuid() . " end" . PHP_EOL;
$response->end("Hello world!
");
});
$server->start();
swoole 的 Server 同 golang 的 net/http 一样的将协程透明化,不过我们在 swoole 中需要使用一些预先提供的方法或客户端才能触发协程切换,进而发挥协程的高性能。
swoole 的各协程客户端:https://wiki.swoole.com/wiki/...
3、ab压测
服务器配置略渣,vsphere上的一台小小自用服务器 1 核 2G,不要在意
200 并发 5000 请求
ab -c 200 -n 5000 -k http://127.0.0.1:8081
ab -c 200 -n 5000 -k http://127.0.0.1:8082
500 并发 5000 请求
ab -c 500 -n 5000 -k http://127.0.0.1:8081
ab -c 500 -n 5000 -k http://127.0.0.1:8082
go
swoole
1000 并发 5000 请求
ab -c 1000 -n 5000 -k http://127.0.0.1:8081
ab -c 1000 -n 5000 -k http://127.0.0.1:8082
go
swoole
2000 并发 5000 请求
ab -c 2000 -n 5000 -k http://127.0.0.1:8081
ab -c 2000 -n 5000 -k http://127.0.0.1:8082
go
swoole
可以通过以上几组数据看出,golang::goroutine 和 swoole::coroutine 两个协程的性能基本没有什么差距,而且协程在高并发下才能更直观的体现出优异的性能。
同样 5000 的请求,200 - 500 - 1000 - 2000 处理耗时越来越小,说明不是我吃不掉,是你发的太慢。当 2000 并发时,我这台 1核 2G 的服务器都能在阻塞 2 秒的场景下跑到 600- 并发。
但 swoole 还是要占更多些的内存,我测试机内存比较小,没办法用 swoole 完美的跑 c10k,内存不够用,没办法创建更多的协程去并发处理请求,但 go 还可以,后面在看下 go::goroutine 是不是占用内存更小吧,先跑一个看看。
go 10k 并发 200k请求
go 20k 并发 1000k请求
ab 最大并发模拟量为 20k,仅供参考
在 C20K 的并发量下,go http server 依然可以游刃有余的提供服务,qps 可以达到 5K,要知道我模拟的2s的IO耗时可以说是比较大的IO开销了,单机1核2G的配置能压出这样的性能指数,很可以了。