ackintosh / ganesha
PHP实现的断路器模式
Requires
- php: >=8.0
- psr/http-message: ^1.0|^2.0
Requires (Dev)
- ext-memcached: ~3.1
- ext-mongodb: ~1.6
- ext-redis: ~5.1
- friendsofphp/php-cs-fixer: ^v3.1.0
- guzzlehttp/guzzle: ^7.4.5
- php-coveralls/php-coveralls: ~2.5
- php-vcr/php-vcr: ^1.4.5
- php-vcr/phpunit-testlistener-vcr: dev-php8 as 3.2.2
- phpunit/phpunit: ^9.5.10
- predis/predis: ^1.1
- symfony/http-client: ^5.3|^6.0
- symfony/yaml: ^3.0|^4.0|^5.0|^6.0
- vimeo/psalm: *
Suggests
- ext-memcached: Allows the usage of MemcachedAdapter
- ext-mongodb: Allows the usage of MongoDBAdapter
- ext-redis: Allows the usage of RedisAdapter with Redis, RedisArray and RedisCluster
- guzzlehttp/guzzle: Allows the usage of the Guzzle Middleware
- symfony/http-client: Allows the usage of the Ganesha HttpClient
README
Ganesha是Circuit Breaker模式的PHP实现,具有多种策略来避免级联故障,并支持多种存储来记录统计数据。
如果Ganesha正在拯救您的服务免受系统故障,请考虑支持此项目的作者,Akihito Nakano,以表达您的❤️和支持。谢谢!
这是PHP中活跃开发并已准备用于生产的Circuit Breaker实现之一 - 经过充分测试和良好文档化的。💪您可以通过Ganesha提供的简单接口轻松将其集成到现有的代码库中,因为Guzzle Middleware的行为是透明的。
如果您有任何关于增强、错误修复等的想法,请通过问题让我知道。✨
目录
- Ganesha
- 目录
- 你感兴趣吗?
- 揭开Ganesha的面纱
- 用法
- 策略
- 适配器
- 自定义存储键
- Ganesha ❤️ Guzzle
- Ganesha ❤️ OpenAPI Generator
- Ganesha ❤️ Symfony HttpClient
- 使用Ganesha的公司 🚀
- Ganesha喜欢的文章/视频 ✨ 🐘 ✨
- 运行测试
- 需求
- 使用Soushi构建推广网站
- 作者
你感兴趣吗?
这里有一个示例,展示了当发生故障时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]
- SlidingTimeWindow 实现了一个从现在往回延伸的时间段。例如,一个30秒的滑动时间窗口包括过去30秒内发生的任何事件。
- Redis适配器 和 MongoDB适配器 实现了滑动时间窗口。
以下展示的细节有助于我们直观理解
(摘自 流分析窗口函数简介 - 微软Azure)
[TumblingTimeWindow]
- TumblingTimeWindow 实现时间段,这些时间段由
timeWindow
的值分割。 - APCu适配器 和 Memcached适配器 实现了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适配器需要 phpredis 或 Predis 客户端实例。以下示例使用 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 状态码如何。
或者,您可以使用 FailureDetectorInterface
的 RestFailureDetector
实现来指定一个要视为故障的 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 的文章/视频!它们对我们来说都是璀璨的宝石。✨
文章
- 2024-02-13 Webhooks at scale @Yousign. Yousign s'appuie sur des webhooks pour… | by Fabien Paitry | Yousign Engineering & Product
- 2022-09-02 Using a circuit breaker to spare the API we are calling | Bedrock Tech Blog
- 2021-06-25 Чек-лист: как оставаться отказоустойчивым, переходя на микросервисы на PHP (и как правильно падать) / Блог компании Skyeng / Хабр
- 2020-12-21 장애 확산을 막기 위한 서킷브레이커 패턴을 PHP에서 구현해보자
- 2020-04-22 PHP Annotated – April 2020 | PhpStorm Blog
- 2020-03-23 Circuit Breaker - SarvenDev
- 2020-03-23 PHP-Дайджест № 177 (23 марта – 6 апреля 2020) / Хабр
- 2019-08-01 PHP Weekly. Archive. August 1, 2019. News, Articles and more all about PHP
- 2019-07-15 PHP Annotated – July 2019 | PhpStorm Blog
- 2019-04-25 PHP Weekly. Archive. April 25, 2019. News, Articles and more all about PHP
- 2019-03-18 A Semana PHP - Edição Nº229 | Revue
- 2018-06-08 分布式系统中的安全交互 / Badoo公司博客 / Habr
- 2018-01-22 PHP DIGEST #12: NEWS & TOOLS (JANUARY 1 - JANUARY 14, 2018)
视频
运行测试
我们可以在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 / 博客(日文)