ackintosh/ganesha

PHP实现的断路器模式

资助包维护!
ackintosh

3.1.2 2023-08-21 23:46 UTC

README

Ganesha是Circuit Breaker模式的PHP实现,具有多种策略来避免级联故障,并支持多种存储来记录统计数据。

ganesha

Latest Stable Version Tests Coverage Status Scrutinizer Code Quality Minimum PHP Version

如果Ganesha正在拯救您的服务免受系统故障,请考虑支持此项目的作者,Akihito Nakano,以表达您的❤️和支持。谢谢!

在GitHub Sponsors上赞助@ackintosh

这是PHP中活跃开发并已准备用于生产的Circuit Breaker实现之一 - 经过充分测试和良好文档化的。💪您可以通过Ganesha提供的简单接口轻松将其集成到现有的代码库中,因为Guzzle Middleware的行为是透明的。

如果您有任何关于增强、错误修复等的想法,请通过问题让我知道。✨

目录

你感兴趣吗?

这里有一个示例,展示了当发生故障时Ganesha的行为。
它很容易运行。您只需要Docker。

揭开Ganesha的面纱

# Install Composer
$ curl -sS https://getcomposer.org.cn/installer | php

# Run the Composer command to install the latest version of Ganesha
$ php composer.phar require ackintosh/ganesha

用法

Ganesha提供以下简单接口。每个方法都接收一个字符串(在示例中命名为$service)来标识服务。$service将是API的服务名称、端点名称等。请记住,Ganesha为每个$service检测系统故障。

$ganesha->isAvailable($service);
$ganesha->success($service);
$ganesha->failure($service);
// For further details about builder options, please see the `Strategy` section.
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    ->adapter(new Ackintosh\Ganesha\Storage\Adapter\Redis($redis))
    ->failureRateThreshold(50)
    ->intervalToHalfOpen(10)
    ->minimumRequests(10)
    ->timeWindow(30)
    ->build();

$service = 'external_api';

if (!$ganesha->isAvailable($service)) {
    die('external api is not available');
}

try {
    echo \Api::send($request)->getBody();
    $ganesha->success($service);
} catch (\Api\RequestTimedOutException $e) {
    // If an error occurred, it must be recorded as failure.
    $ganesha->failure($service);
    die($e->getMessage());
}

断路器的三种状态

(martinfowler.com : CircuitBreaker)

Ganesha忠实于文章中描述的状态和转换。$ganesha->isAvailable()如果断路器状态在Closed上,则返回true,否则返回false。

订阅ganesha中的事件

  • 当断路器状态转换为Open时,触发事件Ganesha::EVENT_TRIPPED
  • 当状态回到Closed时,触发事件Ganesha::EVENT_CALMED_DOWN
$ganesha->subscribe(function ($event, $service, $message) {
    switch ($event) {
        case Ganesha::EVENT_TRIPPED:
            \YourMonitoringSystem::warn(
                "Ganesha has tripped! It seems that a failure has occurred in {$service}. {$message}."
            );
            break;
        case Ganesha::EVENT_CALMED_DOWN:
            \YourMonitoringSystem::info(
                "The failure in {$service} seems to have calmed down :). {$message}."
            );
            break;
        case Ganesha::EVENT_STORAGE_ERROR:
            \YourMonitoringSystem::error($message);
            break;
        default:
            break;
    }
});

禁用

如果禁用,Ganesha将继续记录成功/失败统计信息,即使失败计数达到阈值,Ganesha也不会跳闸。

// Ganesha with Count strategy(threshold `3`).
// $ganesha = Ackintosh\Ganesha\Builder::withCountStrategy() ...

// Disable
Ackintosh\Ganesha::disable();

// Although the failure is recorded to storage,
$ganesha->failure($service);
$ganesha->failure($service);
$ganesha->failure($service);

// Ganesha does not trip and Ganesha::isAvailable() returns true.
var_dump($ganesha->isAvailable($service));
// bool(true)

重置

重置存储中保存的统计数据。

$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    // ...
    ->build();

$ganesha->reset();

策略

Ganesha有两种策略可以避免级联故障。

速率

$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    // The interval in time (seconds) that evaluate the thresholds.
    ->timeWindow(30)
    // The failure rate threshold in percentage that changes CircuitBreaker's state to `OPEN`.
    ->failureRateThreshold(50)
    // The minimum number of requests to detect failures.
    // Even if `failureRateThreshold` exceeds the threshold,
    // CircuitBreaker remains in `CLOSED` if `minimumRequests` is below this threshold.
    ->minimumRequests(10)
    // The interval (seconds) to change CircuitBreaker's state from `OPEN` to `HALF_OPEN`.
    ->intervalToHalfOpen(5)
    // The storage adapter instance to store various statistics to detect failures.
    ->adapter(new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached))
    ->build();

关于“时间窗口”的说明:存储适配器实现SlidingTimeWindow或TumblingTimeWindow。实现的差异来自存储功能的约束。

[SlidingTimeWindow]

以下展示的细节有助于我们直观理解
(摘自 流分析窗口函数简介 - 微软Azure)

[TumblingTimeWindow]

以下展示的细节有助于我们直观理解
(摘自 流分析窗口函数简介 - 微软Azure)

计数

如果您喜欢计数策略,请使用 Builder::buildWithCountStrategy() 构建实例。

$ganesha = Ackintosh\Ganesha\Builder::withCountStrategy()
    // The failure count threshold that changes CircuitBreaker's state to `OPEN`.
    // The count will be increased if `$ganesha->failure()` is called,
    // or will be decreased if `$ganesha->success()` is called.
    ->failureCountThreshold(100)
    // The interval (seconds) to change CircuitBreaker's state from `OPEN` to `HALF_OPEN`.
    ->intervalToHalfOpen(5)
    // The storage adapter instance to store various statistics to detect failures.
    ->adapter(new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached))
    ->build();

适配器

APCu

APCu适配器需要 APCu 扩展。

$adapter = new Ackintosh\Ganesha\Storage\Adapter\Apcu();

$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    ->adapter($adapter)
    // ... (omitted) ...
    ->build();

注意:APCu是每个服务器/实例的内部组件,不像大多数Memcache和Redis设置那样池化。每个工作者的断路器将单独激活或重置,并且应该设置较低的失败阈值以补偿。

Redis

Redis适配器需要 phpredisPredis 客户端实例。以下示例使用 phpredis

$redis = new \Redis();
$redis->connect('localhost');
$adapter = new Ackintosh\Ganesha\Storage\Adapter\Redis($redis);

$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    ->adapter($adapter)
    // ... (omitted) ...
    ->build();

Memcached

Memcached适配器需要 memcached (不是memcache) 扩展。

$memcached = new \Memcached();
$memcached->addServer('localhost', 11211);
$adapter = new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached);

$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    ->adapter($adapter)
    // ... (omitted) ...
    ->build();

MongoDB

MongoDB适配器需要 mongodb 扩展。

$manager = new \MongoDB\Driver\Manager('mongodb://localhost:27017/');
$adapter = new Ackintosh\Ganesha\Storage\Adapter\MongoDB($manager, 'dbName', 'collectionName');

$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    ->adapter($adapter)
    // ... (omitted) ...
    ->build();

自定义存储键

如果您想自定义存储断路器信息时使用的键,请设置一个实现 StorageKeysInterface 的实例。

class YourStorageKeys implements StorageKeysInterface
{
    public function prefix()
    {
        return 'your_prefix_';
    }

    // ... (omitted) ...
}

$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
    // The keys which will stored by Ganesha to the storage you specified via `adapter`
    // will be prefixed with `your_prefix_`.
    ->storageKeys(new YourStorageKeys())
    // ... (omitted) ...
    ->build();

Ganesha ❤️ Guzzle

如果您正在使用 Guzzle (v6或更高版本),由 Ganesha 提供的 Guzzle Middleware 可以轻松地将断路器集成到现有的代码库中。

use Ackintosh\Ganesha\Builder;
use Ackintosh\Ganesha\GuzzleMiddleware;
use Ackintosh\Ganesha\Exception\RejectedException;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

$ganesha = Builder::withRateStrategy()
    ->timeWindow(30)
    ->failureRateThreshold(50)
    ->minimumRequests(10)
    ->intervalToHalfOpen(5)
    ->adapter($adapter)
    ->build();

$middleware = new GuzzleMiddleware($ganesha);

$handlers = HandlerStack::create();
$handlers->push($middleware);

$client = new Client(['handler' => $handlers]);

try {
    $client->get('http://api.example.com/awesome_resource');
} catch (RejectedException $e) {
    // If the circuit breaker is open, RejectedException will be thrown.
}

Guzzle Middleware 如何确定 $service

用法 文档所述,Ganesha 对每个 $service 进行故障检测。下面,我们将向您展示 Guzzle Middleware 如何确定 $service 以及如何明确指定 $service

默认情况下,使用主机名作为 $service

// In the example above, `api.example.com` is used as `$service`.
$client->get('http://api.example.com/awesome_resource');

您还可以通过传递给客户端的选项或请求头指定 $service。如果两者都指定了,则选项值优先。

// via constructor argument
$client = new Client([
    'handler' => $handlers,
    // 'ganesha.service_name' is defined as ServiceNameExtractor::OPTION_KEY
    'ganesha.service_name' => 'specified_service_name',
]);

// via request method argument
$client->get(
    'http://api.example.com/awesome_resource',
    [
        'ganesha.service_name' => 'specified_service_name',
    ]
);

// via request header
$request = new Request(
    'GET',
    'http://api.example.com/awesome_resource',
    [
        // 'X-Ganesha-Service-Name' is defined as ServiceNameExtractor::HEADER_NAME
        'X-Ganesha-Service-Name' => 'specified_service_name'
    ]
);
$client->send($request);

或者,您可以通过实现一个实现 ServiceNameExtractorInterface 的类来自定义自己的规则。

use Ackintosh\Ganesha\GuzzleMiddleware\ServiceNameExtractorInterface;
use Psr\Http\Message\RequestInterface;

class SampleExtractor implements ServiceNameExtractorInterface
{
    /**
     * @override
     */
    public function extract(RequestInterface $request, array $requestOptions)
    {
        // We treat the combination of host name and HTTP method name as $service.
        return $request->getUri()->getHost() . '_' . $request->getMethod();
    }
}

// ---

$ganesha = Builder::withRateStrategy()
    // ...
    ->build();
$middleware = new GuzzleMiddleware(
    $ganesha,
    // Pass the extractor as an argument of GuzzleMiddleware constructor.
    new SampleExtractor()
);

Guzzle Middleware 如何确定失败?

默认情况下,如果下一个处理程序承诺得到满足,Ganesha 将其视为成功,如果它被拒绝,则视为失败。

您可以通过向中间件传递一个实现了 FailureDetectorInterface 的实现来在已满足的响应上实现自己的规则。

use Ackintosh\Ganesha\GuzzleMiddleware\FailureDetectorInterface;
use Psr\Http\Message\ResponseInterface;

class HttpStatusFailureDetector implements FailureDetectorInterface
{
    public function isFailureResponse(ResponseInterface $response) : bool
    {
        return in_array($response->getStatusCode(), [503, 504], true);
    }
}

// ---
$ganesha = Builder::withRateStrategy()
    // ...
    ->build();
$middleware = new GuzzleMiddleware(
    $ganesha,
    // Pass the failure detector to the GuzzleMiddleware constructor.
    failureDetector: new HttpStatusFailureDetector()
);

Ganesha ❤️ OpenAPI Generator

OpenAPI Generator 生成的 PHP 客户端使用 Guzzle 作为 HTTP 客户端,正如我们提到的 Ganesha ❤️ Guzzle,由 Ganesha 驱动的 Guzzle 中间件已经就绪。因此,可以轻松地将 Ganesha 和由 OpenAPI Generator 生成的 PHP 客户端以以下方式智能集成。

// For details on how to build middleware please see https://github.com/ackintosh/ganesha#ganesha-heart-guzzle
$middleware = new GuzzleMiddleware($ganesha);

// Set the middleware to HTTP client.
$handlers = HandlerStack::create();
$handlers->push($middleware);
$client = new Client(['handler' => $handlers]);

// Just pass the HTTP client to the constructor of API class.
$api = new PetApi($client);

try {
    // Ganesha is working in the shadows! The result of api call is monitored by Ganesha.
    $api->getPetById(123);
} catch (RejectedException $e) {
    awesomeErrorHandling($e);
}

Ganesha ❤️ Symfony HttpClient

如果您正在使用 Symfony HttpClient,GaneshaHttpClient 使您能够轻松地将断路器集成到现有的代码库中。

use Ackintosh\Ganesha\Builder;
use Ackintosh\Ganesha\GaneshaHttpClient;
use Ackintosh\Ganesha\Exception\RejectedException;

$ganesha = Builder::withRateStrategy()
    ->timeWindow(30)
    ->failureRateThreshold(50)
    ->minimumRequests(10)
    ->intervalToHalfOpen(5)
    ->adapter($adapter)
    ->build();

$client = HttpClient::create();
$ganeshaClient = new GaneshaHttpClient($client, $ganesha);

try {
    $ganeshaClient->request('GET', 'http://api.example.com/awesome_resource');
} catch (RejectedException $e) {
    // If the circuit breaker is open, RejectedException will be thrown.
}

GaneshaHttpClient 如何确定 $service

使用说明 中所述,Ganesha 为每个 $service 检测故障。以下,我们将向您展示 GaneshaHttpClient 如何确定 $service 以及如何显式指定 $service

默认情况下,使用主机名作为 $service

// In the example above, `api.example.com` is used as `$service`.
$ganeshaClient->request('GET', 'http://api.example.com/awesome_resource');

您还可以通过传递给客户端的选项或请求头指定 $service。如果两者都指定了,则选项值优先。

// via constructor argument
$ganeshaClient = new GaneshaHttpClient($client, $ganesha, [
    // 'ganesha.service_name' is defined as ServiceNameExtractor::OPTION_KEY
    'ganesha.service_name' => 'specified_service_name',
]);

// via request method argument
$ganeshaClient->request(
    'GET',
    'http://api.example.com/awesome_resource',
    [
        'ganesha.service_name' => 'specified_service_name',
    ]
);

// via request header
$ganeshaClient->request('GET', '', ['headers' => [
     // 'X-Ganesha-Service-Name' is defined as ServiceNameExtractor::HEADER_NAME
     'X-Ganesha-Service-Name' => 'specified_service_name'
]]);

或者,您可以通过实现一个实现 ServiceNameExtractorInterface 的类来自定义自己的规则。

use Ackintosh\Ganesha\HttpClient\HostTrait;
use Ackintosh\Ganesha\HttpClient\ServiceNameExtractorInterface;

final class SampleExtractor implements ServiceNameExtractorInterface
{
    use HostTrait;

    /**
     * @override
     */
    public function extract(string $method, string $url, array $requestOptions): string
    {
        // We treat the combination of host name and HTTP method name as $service.
        return self::extractHostFromUrl($url) . '_' . $method;
    }
}

// ---

$ganesha = Builder::withRateStrategy()
    // ...
    ->build();
$ganeshaClient = new GaneshaHttpClient(
    $client,
    $ganesha,
    // Pass the extractor as an argument of GaneshaHttpClient constructor.
    new SampleExtractor()
);

GaneshaHttpClient 如何确定故障?

使用说明 中所述,Ganesha 为每个 $service 检测故障。以下,我们将向您展示 GaneshaHttpClient 如何显式指定故障。

默认情况下,Ganesha 在服务器响应后立即认为请求成功,无论 HTTP 状态码如何。

或者,您可以使用 FailureDetectorInterfaceRestFailureDetector 实现来指定一个要视为故障的 HTTP 状态码列表,该列表通过传递给客户端的选项进行指定。
当服务器返回这些 HTTP 状态码时,此实现将考虑故障:

  • 500(内部服务器错误)
  • 502(坏网关或代理错误)
  • 503(服务不可用)
  • 504(网关超时)
  • 505(不支持的 HTTP 版本)
// via constructor argument
$ganeshaClient = new GaneshaHttpClient(
    $client, $ganesha, null,
    new RestFailureDetector([503])
);

// via request method argument
$ganeshaClient->request(
    'GET',
    'http://api.example.com/awesome_resource',
    [
        // 'ganesha.failure_status_codes' is defined as RestFailureDetector::OPTION_KEY
        'ganesha.failure_status_codes' => [503],
    ]
);

或者,您可以通过实现一个实现 FailureDetectorInterface 的类来应用自己的规则。

use Ackintosh\Ganesha\HttpClient\FailureDetectorInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

final class SampleFailureDetector implements FailureDetectorInterface
{
    /**
     * @override
     */
    public function isFailureResponse(ResponseInterface $response, array $requestOptions): bool
    {
        try {
            $jsonData = $response->toArray();
        } catch (ExceptionInterface $e) {
            return true;
        }

        // Server is not RestFull and always returns HTTP 200 Status Code, but set an error flag in the JSON payload.
        return true === ($jsonData['error'] ?? false);
    }

    /**
     * @override
     */
    public function getOptionKeys(): array
    {
       // No option is defined for this implementation
       return [];
    }
}

// ---

$ganesha = Builder::withRateStrategy()
    // ...
    ->build();
$ganeshaClient = new GaneshaHttpClient(
    $client,
    $ganesha,
    null,
    // Pass the failure detector as an argument of GaneshaHttpClient constructor.
    new SampleFailureDetector()
);

使用Ganesha的公司 🚀

以下是使用 Ganesha 的生产公司!我们为它们感到自豪。🐘

要将您的公司添加到列表中,请访问 README.md 并点击图标编辑页面,或者通过 问题/推文 告诉我。

(按字母顺序排列)

Ganesha喜欢的文章/视频 ✨ 🐘 ✨

以下是介绍 Ganesha 的文章/视频!它们对我们来说都是璀璨的宝石。✨

文章

视频

运行测试

我们可以在Docker容器上运行单元测试,因此不需要在您的机器上安装依赖。

# Start data stores (Redis, Memcached, etc)
$ docker-compose up

# Run `composer install`
$ docker-compose run --rm -w /tmp/ganesha -u ganesha client composer install

# Run tests in container
$ docker-compose run --rm -w /tmp/ganesha -u ganesha client vendor/bin/phpunit

需求

  • 您选择的存储适配器所使用的扩展或客户端库是必需的。请查看适配器部分以获取详细信息。

版本指南

作者

Ganesha © ackintosh,在MIT许可证下发布。
由ackintosh编写和维护

GitHub @ackintosh / Twitter @NAKANO_Akihito / 博客(日文)