letscodehu/php-circuit-breaker

PHP 电路断路器组件

0.2.9 2017-02-18 12:52 UTC

This package is auto-updated.

Last update: 2024-09-13 20:18:03 UTC


README

Build Status

一个帮助您优雅处理外部服务(通常是远程的第三方服务)中断和超时的组件。

这是一个提供极其易于使用电路断路器组件的库。它不需要外部依赖,并为APC和Memcached提供默认存储实现,但可以通过多种方式扩展。

框架支持

此库不需要任何特定的PHP框架,您只需要PHP 5.4或更高版本。

Symfony 2

如果您使用Symfony 2框架,则应使用php-circuit-breaker-bundle。这是一个我创建的包装php-circuit-breaker并与Symfony 2组件和依赖注入集成的捆绑包。

其他框架

如果您使用其他框架并且想要使用php-circuit-breaker,请告诉我,我们可以尝试构建一个开源集成,就像为Symfony 2所做的那样。

动机和好处

  • 允许应用程序检测故障并适应其行为,而无需人工干预。
  • 通过向模块中添加故障安全功能来提高服务的鲁棒性。

安装

您可以下载源代码并使用它们与您的自动加载器一起使用,或者您可以使用Composer,在这种情况下,您只需要一个像这样的require:

"require": {
    "ejsmont-artur/php-circuit-breaker": "*"
},

之后,您应该更新Composer依赖项,然后就可以开始使用了。

用例 - 非关键功能

  • 您的应用程序有一个非关键功能,例如:用户跟踪、统计、推荐等
  • 该可选功能使用远程服务,这会导致您的应用程序中断。
  • 您希望在“非关键功能”失败时保持应用程序和核心进程可用。

您的应用程序的代码可能如下所示

    $factory = new Ejsmont\CircuitBreaker\Factory();
    $circuitBreaker = $factory->getSingleApcInstance(30, 300);

    $userProfile = null;
    if( $circuitBreaker->isAvailable("UserProfileService") ){
        try{
            $userProfile = $userProfileService->loadProfileOrWhatever();
            $circuitBreaker->reportSuccess("UserProfileService");
        }catch( UserProfileServiceConnectionException $e ){
            // network failed - report it as failure
            $circuitBreaker->reportFailure("UserProfileService");
        }catch( Exception $e ){
            // something went wrong but it is not service's fault, dont report as failure
        }
    }
    if( $userProfile === null ){
        // for example, show 'System maintenance, you cant login now.' message
        // but still let people buy as logged out customers.
    }

用例 - 支付网关

  • Web应用程序依赖于第三方服务(例如支付网关)。
  • Web应用程序需要跟踪第三方服务何时不可用。
  • 应用程序不能变慢/不可用,它必须告诉用户功能有限或只是隐藏它们。
  • 应用程序在渲染结账页面之前使用电路断路器,如果特定的支付网关不可用,则从用户那里隐藏支付选项。

如您所见,这是一个在运行时选择性禁用功能但仍然允许核心业务流程不间断的非常强大的概念。

后端与支付服务通信可能如下所示

    $factory = new Ejsmont\CircuitBreaker\Factory();
    $circuitBreaker = $factory->getSingleApcInstance(30, 300);

    try{
        // try to process the payment
        // then tell circuit breaker that it went well
        $circuitBreaker->reportSuccess("PaymentOptionOne");
    }catch( SomePaymentConnectionException $e ){
        // If you get network error report it as failure
        $circuitBreaker->reportFailure("PaymentOptionOne");
    }catch( Exception $e ){
        // in case of your own error handle it however it makes sense but
        // dont tell circuit breaker it was 3rd party service failure
    }

由于您正在记录失败和成功的操作,您现在可以在前端使用它们来隐藏失败的支付选项。

渲染可用支付选项的前端可能如下所示

    $factory = new Ejsmont\CircuitBreaker\Factory();
    $circuitBreaker = $factory->getSingleApcInstance(30, 300);

    if ($circuitBreaker->isAvailable("PaymentOptionOne")) {
        // display the option
    }

动态代理

为了轻松包装服务类并用电路断路器保护它,有一个工厂类

$client = new Predis\Client("tcp://127.0.0.1:6379?read_write_timeout=0");
$factory = new Ejsmont\CircuitBreaker\Factory();
$cb = $factory->getRedisInstance($client, 30, 3600);

$proxy = CircuitBreakerProxyFactory::create('SomeSlowService', $circuitBreaker, ["constructor", "arguments"]);

$proxy->remoteCalls(); // the call is protected by the circuit breaker

默认的包装功能如下

        $oldTimeout = ini_get("default_socket_timeout"); // as we are talking about remote calls, we get the old timeout

        if ($this->circuitBreaker->isAvailable($this->serviceName)) { // we are checking if the service is available
            try {
                ini_set("default_socket_timeout", $this->timeout); // override the timeout
                $returnValue = $method->invokeArgs($proxy, $args); // the original methodcall
                $this->circuitBreaker->reportSuccess($this->serviceName); // at the happy path we report the success
            } catch(\Exception $e) {
                $this->circuitBreaker->reportFailure($this->serviceName); // or we report the error
            }

        }
        ini_set("default_socket_timeout", $oldTimeout); // and set the timeout to the original value

您可以通过向create工厂方法提供第四个参数来覆盖此功能

$proxy = CircuitBreakerProxyFactory::create('SomeSlowService', $circuitBreaker, ["constructor", "arguments"], $ownMethodHook);

第四个参数必须实现'guymers\proxy\MethodHook'接口。

功能

  • 通过单个电路断路器实例跟踪多个服务。
  • 可插拔的后端适配器,默认提供APC、Redis和Memcached。
  • 可自定义的服务阈值。您可以为服务被认为是down所需多少次失败定义。
  • 可自定义的重试超时。您不希望永久禁用服务。在提供的超时后,电路断路器将允许单个进程尝试
  • 使用断路器创建围绕服务类的动态代理。

性能影响

断路器的开销可以忽略不计。

APC 实现执行 isAvailable() 约需 0.0002 秒,然后报告成功或失败。

与本地 memcached 进程通信时,Memcache 适配器的范围在 0.0005 秒左右。

与本地 Redis 通信时,Redis 适配器的范围在 0.0002 秒左右。

唯一可能影响性能的是网络连接时间。如果您选择使用远程 memcached 服务器或实现自己的自定义 StorageAdapter。

代理创建大约需要 0.0007 秒。后续调用没有性能影响。

运行测试

  • 测试通过 PHPUnit 运行,假设是通过 PEAR 安装的。
  • 可以使用 phpunit 独立运行测试,或通过 ant 构建目标运行。
  • “ci”目标生成代码覆盖率报告,“phpunit”目标不生成。

您可以通过以下任一方式运行所有测试

ant
ant phpunit
ant ci

您可以通过以下方式运行所选测试用例

cd tests
phpunit Unit/Ejsmont/CircuitBreaker/Storage/Adapter/DummyAdapterTest.php

详细信息

在文档更新之前,您可以在我的博客上了解更多关于断路器及其实现的概念:http://artur.ejsmont.org/blog/circuit-breaker

一些实现细节已更改,但核心逻辑仍然相同。

作者