在微服务设计场景, 当系统负载较高时, 可以通过抛弃部分非核心服务来保证核心服务高可用.

网上的文章都是讲降级的策略, 基本没有讲到如何具体去实现. 我自己用python为验证码服务写了一个例子.

验证码通常服务是独立出去的(废话), 工作流程通常分为几步:

  • 判断是否需要进行验证

  • 获取验证码图片

  • 校验用户输入结果

  • 更新/清空错误次数

上面每一步, 都需要应用服务器与验证码服务交互, 通常只需在第一步进行降级. 这里截取部分代码

@coroutine
def service_down_captcha_img(*args, **kwargv):
    result = ''
    with open('./assets/captcha_unreachable.png', 'rb') as f:
        result = f.read()
    raise Return(result)

@coroutine
def service_down_captcha_check(*args, **kwargv):
    raise Return(None)

service_run_time_stat = {}
def async_sevice_downgrade(service_name, side_effect):
    def downgrade(func):
        @coroutine
        @wraps(func)
        def wrapped(*args, **kwargv):
            service = service_run_time_stat.get(service_name, {'t':[], 'f':[], 'd': False})
            now = int(time.time())
            window = 300
            down = False
            if (service['f'] and len(service['t']) + len(service['f']) > 10) or len(service['t']) > 10000:
                service['t'] = [i for i in service['t'] if i  > now - window]
                service['f'] = [i for i in service['f'] if i  > now - window]
                if len(service['f']) * 100 / (len(service['t']) + len(service['f'])) > 10:
                    down = True
                    if not service['d']:
                        log.exception('service %s down', service_name)
            service['d'] = down
            if down:
                service_run_time_stat[service_name] = service
                ret = yield side_effect(*args, **kwargv)
                raise Return(ret)
            else:
                try:
                    ret = yield func(*args, **kwargv)
                except RetValueNotExpected:
                    service['t'].append(now)
                    service_run_time_stat[service_name] = service
                    raise
                except Exception:
                    service['f'].append(now)
                    service_run_time_stat[service_name] = service
                    raise
                else:
                    service['t'].append(now)
                    service_run_time_stat[service_name] = service
                    raise Return(ret)
        return wrapped
    return downgrade

@async_sevice_downgrade('captcha', service_down_captcha_check)
@coroutine
def check_captcha(user, code, client, validate=False):
    """验证码校验

    Arguments:
        user {string} -- 用户名
        code {string} -- 输入的验证码字符
        client {string} -- 用户客户端标识

    Keyword Arguments:
        validate {bool} -- 是否强制校验 (default: {False})

    Raises:
        RetValueNotExpected -- 校验不通过

    远程api返回结果:
        error = 0 通过
        error = 1 参数错误
        error = 2 验证码错误或没有输入
    """

    comment = '验证码校验'
    url = base_url + 'captcha'
    params = {
        'project': project_name,
        'user': user,
        'pysessid': client,
        'code': code
    }
    if validate:
        params.update({'validate': True})
    ret = yield get(url, params=params, comment=comment)
    if ret['error'] != 0:
        raise RetValueNotExpected(ret['msg'])

在上面的例子中, 主要起作用的是async_sevice_downgrade这个装饰器. 因为我们使用的是tornado和tornado的asynchttpclient, 所以代码中可以见到coroutine.

主要原理就是维护一个全局变量, 当captcha这个服务的请求报了异常, 则将其添加到f列表内, 当登录函数调用验证码服务时, 装饰器会先判断最近错误请求是否达到阈值. 当达到阈值时, 直接调用side_effect 函数. 如果接口功能是验证用户输入, 则直接返回用户验证码正确, 如果接口是请求验证码图片, 则返回一个”验证码服务不可以, 请重新登录”的图片, 引导用户继续操作.