swoole / rfc

Swoole 提案
116 stars 3 forks source link

底层服务治理框架 #82

Open matyhtf opened 2 years ago

matyhtf commented 2 years ago

Swoole 底层服务治理框架

在 swoole 中提供了一套服务器治理的解决方案,有以下几点优势:

服务发现

支持ConsulNacos等常见的服务注册发现组件,底层可自动替换DNS,无需应用层更改代码。

file_get_contents

// GET
$json = file_get_contents("http://user.service/profile/?uid=1");

// POST
$opts = array('http' =>
  array(
    'method'  => 'POST',
    'header'  => "Content-Type: application/x-www-form-urlencoded\r\n",
    'content' => http_build_query(['mobile' => 19900000000, 'name' => 'Rango']),
  )
);

$context  = stream_context_create($opts); 
$json = file_get_contents("http://user.service/profile/?uid=1", false, $context);

curl

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://user.service/profile/?uid=1");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$rs = curl_exec($ch);

SWOOLE 协程客户端

$cli = new Swoole\Coroutine\Http\Client('user.service');
$cli->get('/profile/?uid=1');
$json = $cli->getBody();

这里的user.service就是服务的名称,在服务端需要注册服务到对应的ConsulNacos服务管理中心。 客户端会自动获取user.service所有集群的节点,未指定端口时,会自动获取服务器节点注册的端口。

服务注册

const SERVICE_NAME = 'user.service';
$ns = new Swoole\NameResolver\Consul('http://127.0.0.1:8500');
$serv = new Swoole\Http\Server('127.0.0.1', 9501);
$serv->on("start", function ($serv) use ($ns) {
    $ns->join(SERVICE_NAME, '127.0.0.1', $serv->port);
});
$serv->on('request', function ($req, $resp) {
    $resp->end(json_encode(['code' => 0,]));
});
$serv->on('beforeShutdown', function ($serv) use ($ns) {
    $ns->leave(SERVICE_NAME, '127.0.0.1', $serv->port);
});
$serv->start();

负载均衡

服务有多个节点时,客户端会获得所有节点列表。底层会使用随机+权重的算法进行负载均衡,将请求发送到服务集群内的其中一个节点。 除了随机+权重的模式外,未来还会支持多种负载均衡模式:

故障转移

当客户端发起请求时,TCP连接到一个节点发生超时和连接被拒绝,底层会切换到集群的另外一个节点进行连接,直到成功连接或者达到 最大重试次数时,将返回失败。

请求重试

当客户端请求出现失败时,某些情况下是可以安全重试的。如TCP连接失败,或者服务器端返回503服务不可用, 客户端可以切换到另外一个节点进行重试。

可安全重试的情况

  1. HTTP request failed! HTTP/1.1 503
  2. HTTP request failed! HTTP/1.1 502
  3. Failed to open stream: Connection timed out
  4. Failed to open stream: No route to host
  5. Failed to open stream: Connection refused

超时控制

由于 php streamcurl的默认超时时间非常长,如果出现某些请求无响应可能会导致整个进程被长期阻塞,导致服务雪崩。 底层会自动将超时时间统一修改为1秒。

php stream 的默认超时

default_socket_timeout  "60"    PHP_INI_ALL

参考: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout

curl 的默认超时

CURLOPT_TIMEOUT: Indefinite
CURLOPT_TIMEOUT_MS: Indefinite
CURLOPT_CONNECTTIMEOUT: 300 seconds
CURLOPT_CONNECTTIMEOUT_MS: Indefinite

参考: https://stackoverflow.com/questions/10308915/php-default-curl-timeout-value

服务熔断

当客户端发起一个请求之后,底层首先会从名字服务(naming service)获得可用节点列表, 之后根据负载均衡策略挑选出一个节点作为实际访问的节点。当某个节点出现故障或者服务器高负载无法正常处理请求时, 底层的重试机制能够自动跳过这些节点,将请求发送到其他节点。

底层发现某个节点出现连续多次连接超时、服务不可用、延时过高时,会判断认为该节点短时间内不可用,将当前节点熔断, 一定时间内将自动跳过此节点。如果节点再次可用会重新向此节点发送请求。这样可以避免:

  1. 降低客户端请求出错重试引起的资源浪费以及延时的增加
  2. 减少对高负载节点的请求数量,降低服务端压力,避免雪崩

服务限流

一个服务器程序受制于硬件性能、软件算法和逻辑复杂度、后端存储的吞吐能力,因此处理能力是有限的。 当请求速度超过服务的处理速度时,服务就会过载。

如果服务持续过载,会导致越来越多的请求积压,最终所有的请求都必须等待较长时间才能被处理,从而使整个服务处于瘫痪状态。 这时如果服务器可以直接主动拒绝掉一部分请求,能够让服务能够"及时"处理更多的请求。

可以通过设置服务器的最大并发 (max_concurrency) 来设置限流,防止服务过载引起雪崩 。

$serv = new Swoole\Http\Server('127.0.0.1', 9501);
$serv->set(['max_concurrency' => 500]);

如何设置最大并发

可以通过一个简单的公式计算出合适的max_concurrency设置,

max_concurrency = 最大 QPS(请求个数/秒) * 平均响应延时(秒)

原理请参考:little's law

通过对服务器程序进行压测可以观察到程序在能够及时并成功处理所有请求时的最大 QPS 和平均响应延时。

自适应限流

理论上如果服务器程序所在机器的硬件性能、程序未变更、依赖的后端数据库或其他服务的吞吐能力没有变化, 当前服务的并发是可以保持不变的,这种情况下要让服务不过载,只需在上线前进行压力测试,并通过little's law 计算出最佳的max_concurrency并设置即可。

而在复杂多变的环境下,静态设置的max_concurrency可能并不能适应所有情况。因此需要一种能够根据 服务器程序运行时产生的实时性能,自动计算并动态设置max_concurrency,可称之为自适应限流。

可将 max_concurrency 设为 SWOOLE_MAX_CONCURRENCY_AUTO 即可启用自适应限流。

$serv = new Swoole\Http\Server('127.0.0.1', 9501);
$serv->set(['max_concurrency' => SWOOLE_MAX_CONCURRENCY_AUTO]);

弹性伸缩

当服务器产生服务不可用时主动拒绝掉一部分请求,这些拒绝掉的请求与接受并正常处理的请求数量达到一定比例时, 说明当前集群可能需要扩容。

通过计算并汇总集群内所有节点拒绝请求、正常处理请求的次数,达到高水位阈值后可通知K8s等服务器容器编排系统, 将对应的服务集群进行扩容。下降到低水位阈值后,可进行缩容。

Plus 配置

php.ini

NameResovler 配置

路径: /opt/swoole/plus/name_resolver.json,格式为json,必须为数组,可同时设置多个名字服务,通过suffix 后缀区分使用哪个NameResolver

[
    {
        "class": "Swoole\\NameResolver\\Consul",
        "server_url": "http://127.0.0.1:8500",
        "suffix": ".service"
    }
]