聊一聊限流、降级、熔断

梦康 2021-05-23 14:46:07 353

熔断

小时候村里一到夏天,全村都开空调,村里总闸的保险丝就会因为用电量太大,自动熔断了,直接停服。股市也有一些极端情况开启熔断处理,不到万不得已,不会熔断。在 Web 工程中熔断的最小单元,也不一定是整个应用,可能只是某个服务。这里不深究学术名词定义。

限流

限流场景我们经常遇到,有时候地铁里就被保安人员给我限流了,双十一抢购也被爸爸限流了。坐地铁之所以能限流是因为我们都要安检,有这个统一的地铁入口;浏览网站被限流是因为访问有统一的域名入口。

当我们需要根据路由规则进行限流,只要把握好网关就很方便的实现限流了,以下方案均可行

  • 布点在类似于 WAF(Web 应用防火墙)中,具体见 阿里云WAF手册
  • 如果不想花钱,也可以安装 nginx 限流插件来做类似的工作,自己部署,总之是在 web 应用前面布点拦截操作
  • 也可以在 web 应用里面结合路由组件开发一个限流组件,只要代码有统一入口,就可以方便控制

降级

生活中我也是消费降级的小伙,原来天猫,后来淘宝,现在拼多多和淘宝特价版。消费降级真香,话说回来,重点是又不是不能用。Web 项目中降级的案例,比如微博 feed 流中,用户基本信息压力比较大,而用户的勋章也在该接口中对前端输出,服务降级的重点是又不是不能用

如果是按照现在微服务的理念,勋章查询可能是一个独立的服务,所以降级对应的颗粒度可以是服务降级。既然是独立的服务单元,请求的拦截就又回到了和限流一样的场景;
如果不是微服务架构,接口依赖的用户的勋章输出只是一个独立的函数或者方法,如何进行拦截呢?

方案1. 紧急发布

如果后端服务是 PHP 的脚本语言,我们可以快速的单独发布需要修改的文件,达到快速降级的目的。
如果后端服务是 JAVA 需要编译的,对于这种简单场景的修改,也是支持热部署,单独发布一个 class 文件,也不需要重启也 OK,比如 arthas 就提供类似的功能。

如果发布系统不支持热部署,也不支持单文件发布,只支持发布软件包的方式,那么快速降级就需要 15 ~ 30分钟(业务复杂一点的 Java 应用)才能部署完成,这是互联网应用不能接受的。

方案2. 限流降级中间件

在 Java 生态目前比较成熟,知名的产品有 hystrix,我用的比较多的是 sentinel。支持从路由、方法去做单机的 qps 去限流,只需要在sentinel管控台做配置变更,然后发布推送到各个机器,机器则以最后收到的限流规则单机闭环操作,中间不再需要和中间件服务进行交互。

PHP 能不能像 sentinel 一样对用户态的函数和方法进行拦截控制呢,所以弄了这个 https://github.com/zhoumengkang/php-sentinel PHP 7.2.5 线上运行OK

安装使用

编译安装

$ phpize
$ ./configure --with-php-config=/usr/local/php/bin/php-config
# 如果需要调试
# ./configure --with-php-config=/usr/local/php/bin/php-config --enable-debug
$ make && make install

配置php.ini,在其后面追加

[sentinel]
extension=sentinel.so
sentinel.api_url=https://mengkang.net/sentinel.html
sentinel.api_cache_ttl=120
sentinel.api_cache_file=/tmp/sentinel.rule
sentinel.log_enabled=1
sentinel.log_file=/tmp/sentinel.log
  • sentinel.api_url 限流查询接口
  • sentinel.api_cache_ttl 接口查询结果缓存 2 分钟
  • sentinel.api_cache_file 接口查询缓存路径
  • sentinel.log_enabled 是否开启用户自定义的方法和函数日志记录,可以用日志处理工具收集比如阿里云 SLS
  • sentinel.log_file 日志记录路径

原理

PHP_MINIT阶段,通过zend_set_user_opcode_handler注册在opcode运行环节,对用户态的方法和函数(ZEND_DO_UCALL)的运行做处理

zend_set_user_opcode_handler(ZEND_DO_UCALL, php_sentinel_call_handler)

PHP_RINIT阶段,获取中间件配置。对于缓存时长内的不发起网络请求

php_sentinel_fetch()

限流策略闭环

php_sentinel_call_handler

当已经已经在限流列表中的方法或者函数进行拦截,同时对通过的方法和函数进行日志记录(当日志功能开启的情况)然后进行类似 sls 上报,然后中心通过分析 sls 日志再决策是否进行限流,通过接口通知到各个 web 服务器。

注册自定义的异常类SentinelException,当检测到执行的方法或者函数需要被限流,则抛出该异常

zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "SentinelException", NULL);
php_sentinel_exception_class_entry =  zend_register_internal_class_ex(&ce, zend_ce_exception);
php_sentinel_exception_class_entry->ce_flags |= ZEND_ACC_FINAL;

业务层可以对该异常在最外层进行捕获,按照业务协议做标准错误输出。

try{
    // 项目 dispatcher 调度入口
}catch (\SentinelException $exception){
    // 标准的错误输出 可以是 json 可以 html 页面
}

目前的方案更偏向于一个熔断思路,只要运行到指定的方法或者函数就抛出异常,比如(以函数演示,方法也支持)。可惜 PHP set_exception_handler 不能对运行当前行进行捕获处理,并且接管异常。所以只能算是熔断 ,除非被拦截的方法,在一些逻辑下没有进入。

function demo(){
    $ res[] = aa();
    
    if (条件 1) {
        $res[] = bb(); // bb 函数被降级
    } else {
        $resp[] = cc();
    }
    
    return $res;
}