微服务化之----熔断和隔离

背景:

 

最近在负责一个数据中心的搭建工作,业务场景较多,比如:历史、实时数据计算和查询,汇总分析数据,以及基于数据与业务相结合提供智能推荐服务。对于每个不同的业务场景,所需要存储介质以及容量都不尽相同。比如:实时数据依赖redis,历史数据依赖hbase、mysql、mongdo,用户画像数据依赖es,redis等等。如果不进行微服务化拆分,其中任何一个组件出现问题,整个数据服务工程将都会有瘫痪的风险。

 

面临的问题:所有功能都被柔和在一个工程里,牵一发动全身,业务扩展难度大;相互干扰,每次新业务上线不仅需要验证新功能,还需要验证以前的功能是否受影响;另外 在大促期间也不易于做灾备、熔断和隔离。

 

由于以上原因,最近对整个系统架构做了微服务化拆分。首先看下老系统架构。

 

老系统架构:

 

 

 

 

可以看到这里典型的MVC架构(springmvc),业务代码彼此交织在一起,不便于扩展;各种服务相互依赖,假如hbass或redis整个数据中心将无法正常工作。

 

微服务化架构:

 

再看下微服务拆分后的系统架构

 

 

名称解释jsf:京东自己开发的RPC框架,类似于淘宝的duboo(图中虚线箭头表示jsf接口调用)

 

可以看到这里已经将原来的一个大工程,拆成8个独立的子工程:5个jsf工程 + 3个web服务工程。每个子工程只负责自己独立的业务逻辑,有自己独立的redis集群,数据彼此隔离。

 

Jsf服务子工程集(5个):目前第一期是根据存储介质的不同进行拆分。后续还可以无限扩展,比如即将接入的ES jsf服务、mogodb jsf 服务等。对于每种不同的存储介质,还可以按照业务进行垂直和水平拆分。

 

 

mysql jsf工程(目前1个):mysql主要用于存放数据量不大的汇总数据,以及用户信息数据。目前数据量还比较小,这里只需要一个mysql服务即可满足需求。如果有新业务加入,可以按照对mysql进行垂直拆分,拆分成几个业务独立的数据库,对每个数据库再新建jsf工程,最终形成多个mysql jsf工程集对外提供服务。如下:

 

 

 

 

如果某个业务mysql表数据量暴增,再进行水平拆分,根据实际情况拆分为多库多表。借助淘宝开源的mycat、DRDS分库分表中间件也很容易实现。

 

 

 

 

hbase jsf工程集(4个):为每个不同的业务申请不同的hbase实例(一个实例可以简单理解成mysql的一个数据库)。这里以实例为单位进行服务化,如果一个实例挂掉,也不会影响到其他实例。做到业务数据完全隔离。

 

其他 jsf工程:ES或mongodb等,待接入。

 

至此,简单的服务化拆分已经完成。下面来看微服务怎么做 隔离、熔断、限流。

 

隔离:

 

按照上述方法进行微服务拆分,已经在不同的数据存储介质和不同业务级别完成了服务隔离。保证其中某个服务坏掉,不会影响到其他服务。

 

利用jsf提供的filter功能可以很方便的为各个子服务做自动熔断、限流功能(主流的RPC框架都具备filter功能,如dubbo)。filter一般都采用“责任链模式”,其实"代理模式"也可以做,但"责任链模式"更方便扩展,不同的任务可以分配到不同的层级的filter。下面主要对自动熔断、限流进行讲解。

 

自动熔断:

 

要做自动熔断,我们首先实现的是统一异常捕获。先来看下我们以前jsf服务工程service实现代码:

 

/**
 * 定义服务接口
 * Created by gantianxing on 2017/5/18.
 */
public interface TestMysqlJsf {
 
    ResultModel getById(int id); //根据用户id获取用户信息
 
    ResultModel getByPage();//分页获取用户信息
}
 
/**
 * 服务实现类
 * Created by gantianxing on 2017/5/18.
 */
public class TestMysqlJsfImpl implements TestMysqlJsf{
    private static final Log log = LogFactory.getLog(TestMysqlJsfImpl.class);
 
    @Override
    public ResultModel getById(int id) {
        ResultModel resultModel = new ResultModel();
 
        try {
            DataUser userinfo = null;
            //省略各种计算代码
            resultModel.addAttribute("userInfo",userinfo);
        } catch (Exception e){
            resultModel.fail("xxxxxxxxxxx异常");
            log.error("xxxxxxxxxxx异常",e);
        }
        return resultModel;
    }
 
    @Override
    public ResultModel getByPage() {
        ResultModel resultModel = new ResultModel();
        try {
            List<DataUser> userList = null;
            //省略各种计算代码
            resultModel.addAttribute("userList",userList);
        } catch (Exception e){
            resultModel.fail("xxxxxxxxxxx异常");
            log.error("xxxxxxxxxxx异常",e);
        }
        return resultModel;
    }
}

 

 

以mysql对应的jsf服务工程为例,在该工程里有无数个类似TestMysqlJsf的服务接口。可以看到实现类TestMysqlJsfImpl里为了保证返回给接口调用方的信息足够友好,每个方法体里都加了try{} catch catch (Exception e),捕获所有的异常(避免抛出给调用方)。

这种重复的try catch 遍布所有的接口实现类的每个方法,费时费力,而且也不雅观。

 

怎么能更优雅的实现呢,这里采用的是在服务入口处统一添加一条filter list。首先来看我们第一个filter,统一异常处理filter:

/**
 * Created by gantianxing on 2017/5/18.
 */
public class HandleExceptionFilter extends AbstractFilter {
    private final static Log log = LogFactory.getLog(HandleExceptionFilter.class);
 
    /**
     * 统一异常处理过滤器
     * @param request jsf接口请求信息
     * @return response 如果出现异常,动态构造错误返回信息。
     */
    @Override
    public ResponseMessage invoke(RequestMessage request) {
        ResponseMessage response = null;
        try {
            response = getNext().invoke(request); // 调用链自动往下层执行,直到真实的服务接口被调用
 
        }catch (Exception e){
            String methodName = request.xxxxx().getMethodName(); //获取调用方法名
            String clazzName = request.xxxxx().getClazzName();//获取调用类名
            response = MessageBuilder.buildResponse(request); // 自己构造返回对象
            ResultModel resultModel = new ResultModel();// 动态构造异常返回
            resultModel.fail("接口调用异常:" + e.getMessage());
            response.setResponse(resultModel);
            log.error("接口调用异常:" + clazzName + ":" + methodName, e);
        }
        return response;
    }
}

 

通过配置保证,这层filter在所有业务方法调用之前,首先被调用,其中的response = getNext().invoke(request) 会根据配置再去调用到真实的接口实现方法。

这样所有的“运行时异常”都可以在统一这个过滤器中处理,所有service实现类都不再需要再添加类似try{} catch catch (Exception e) ("非运行时异常"除外)这样的代码,只需处理"非运行时异常"即可。

 

另外,方法性能监控也可以做到这层filter,统一对每个方法性能监控进行埋点,防止出现整个工程到处都是监控埋点的情况(牵涉公司业务太多,代码里没有体现)。

 

有了HandleExceptionFilter这个统一异常处理过滤器之后,上面的TestMysqlJsfImpl可以改为:

 

/**
 * Created by gantianxing on 2017/5/18.
 */
public class TestMysqlJsfImpl implements TestMysqlJsf{
    private static final Log log = LogFactory.getLog(TestMysqlJsfImpl.class);
 
    @Override
    public ResultModel getById(int id) {
        ResultModel resultModel = new ResultModel();
        DataUser userinfo = null;
        //省略各种计算代码
        resultModel.addAttribute("userInfo",userinfo);
        return resultModel;
    }
 
    @Override
    public ResultModel getByPage() {
        ResultModel resultModel = new ResultModel();
        List<DataUser> userList = null;
        //省略各种计算代码
        resultModel.addAttribute("userList",userList);
        return resultModel;
    }
}

 

 

没有了千篇一律的try catch是不是舒服了很多:-D

 

有人要说了,你这跟“自动熔断”有什么关系。别急,我们先看下,为什么要熔断,无非就是服务内部大范围出现异常时自动断开服务,快速的告诉接口调用方“目前服务不可用”。比如:连接数据库超时,或者服务内部调用其他外部接口调用超时等,当某一类异常达到一定的阀指时进行熔断。

 

我们的做法是,在过滤器filter中捕获这些异常,并对某一类异常进行计数,当异常到达一定阀值修改熔断开关为开启状态。将HandleExceptionFilter改造如下:

 

/**
 * Created by gantianxing on 2017/5/18.
 */
public class HandleExceptionFilter extends AbstractFilter {
    private final static Log log = LogFactory.getLog(HandleExceptionFilter.class);
 
    private static AtomicInteger errorcount = new AtomicInteger(0);//错误计数器
 
    @Resource
    private Redis redis;
 
    public void reLoadCount(){
        errorcount.set(0); //故障解除后,手动置0计数器。
    }
 
    /**
     * 统一异常处理过滤器
     * @param request jsf接口请求信息
     * @return response 如果出现异常,动态构造错误返回信息。
     */
    @Override
    public ResponseMessage invoke(RequestMessage request) {
        ResponseMessage response = null;
        try {
            response = getNext().invoke(request); // 调用链自动往下层执行,直到真实的服务接口被调用
        }catch (Exception e){
 
            if (e instanceof ConnectTimeoutException){
                errorcount.getAndIncrement();//原子 +1
                if(errorcount.get() > 50){ //错误次数大于50 熔断器开启
                    redis.setStr("circuit_breaker","on");
                }
            }
            String methodName = request.xxxxx().getMethodName(); //获取调用方法名
            String clazzName = request.xxxxx().getClazzName();//获取调用类名
            response = MessageBuilder.buildResponse(request); // 自己构造返回对象
            ResultModel resultModel = new ResultModel();// 动态构造异常返回
            resultModel.fail("接口调用异常:" + e.getMessage());
            response.setResponse(resultModel);
            log.error("接口调用异常:" + clazzName + ":" + methodName, e);
        }
        return response;
    }
}

每次失败都对异常计数器+1,每次真实接口调用前 先检查异常次数是否到达阀值。

 

在HandleExceptionFilter这层过滤器中只是把熔断开关开启,我们还需要新建一个熔断过滤器filter 添加到HandleExceptionFilter的上层:取名为CircuitBreakerFilter。 它的主要职责:如果熔断开关已经开启,直接返回错误提示;否则继续调用责任链往下执行HandleExceptionFilter。

 

 

/**
 * Created by gantianxing on 2017/5/18.
 */
public class CircuitBreakerFilter extends AbstractFilter {
    private final static Log log = LogFactory.getLog(HandleExceptionFilter.class);
 
    @Resource
    private Redis redis; //redis服务
 
    /**
     * 熔断 过滤器
     * @param request
     * @return
     */
    @Override
    public ResponseMessage invoke(RequestMessage request) {
        ResponseMessage response = null;
        if ("on".equals(redis.get("circuit_breaker"))){
            ResultModel resultModel = new ResultModel();
            resultModel.fail("请求已熔断,请联系服务通过方");
            response.setResponse(resultModel); // 返回结果
            log.info("请求已熔断");
        }else{
            response = getNext().invoke(request); // 调用链往下层执行:HandleExceptionFilter
        }
        return response;
    }
}

 

至此自动熔断实现已经讲完。这里是全熔断实现,如果要实现半熔断,可以再添加“状态模式”实现。

 

自动限流:

 

采用类似"自动熔断"的做法,也可以实现"自动限流",新建CurrentLimitFilter:

/**
 * Created by gantianxing on 2017/5/19.
 */
public class CurrentLimitFilter extends AbstractFilter {
 
    private final static Log log = LogFactory.getLog(CurrentLimitFilter.class);
 
    private static AtomicInteger count = new AtomicInteger(0);
 
    /**
     * 自动限流器
     * @param request
     * @return
     */
    @Override
    public ResponseMessage invoke(RequestMessage request) {
        ResponseMessage response = null;
 
        if (count.get() > 100) { //最高并发超过100 自动限流
            ResultModel resultModel = new ResultModel();
            resultModel.fail("请求到到上限");
            response = MessageBuilder.buildResponse(request); // 自己构造返回对象
            response.setResponse(resultModel); // 返回结果
            log.info("请求到到上限");
        }else{
            count.getAndIncrement(); //进入方法调用,并发计数器+1
            response = getNext().invoke(request); // 自动往下层执行
            count.getAndDecrement();//结束方法调用,并发计数器-1
            return null;
        }
        return response;
    }
}

 

主要采用AtomicInteger做计数器,当进入方法调用时+1,当结束方法调用时-1. 计数器get()方法获取的值即为 该服务器的并发量,如果并发量超过100(根据自己的业务、服务器性能自己配置),则进行限流。当并发量小于100,又自动恢复正常。我们暂且称之为:“丢弃式限流”。

 

这种限流措施,会对调用方产生一定负面影响。有人说,为什么不使用MQ(消息队列)进行限流,还可以保证数据不丢失。其实这是两种不同的手段,针对不同的业务场景。

 

MQ异步限流:适用于后端逻辑处理业务,无需及时向客户端返回处理结果,允许处理请求暂时积压,延迟处理(重要数据要求必须被处理)。比如 订单积压,一般都是采用MQ。

 

丢弃式限流:适用于需及时返回的前端业务,比如一个状态查询,前端页面要求及时返回查询结果(哪怕是错误的也行),否则页面就会被卡住,是一种大促常用降级手段。当服务端并发到达上限时,及时返回一条提示信息,用户再次刷新页面,有可能会得到正常结果。做到保护服务端的同时(预防调用链“雪崩”),让前端也能及时的得到响应。

 

采用“丢弃式限流”、或者直接熔断,可以避免“雪崩”问题。

 

 

 

最后 再把三个过滤器串联起来(注意顺序),限流过滤器放在调用链第一层,熔断过滤器放在第二层,统一异常处理过滤器放在第三层。最终调用流程如下:

 

 

 

简单总结下:

隔离:要做隔离,一种好的方法就是微服务拆分,让每个小业务成为孤岛,即便是其中一个业务挂掉,其他服务依然可以正常运行。

熔断、限流:在微服务化的基础上可以很方便的做熔断和限流。采用“责任链模式”在服务入口处进行统一封装即可。如果你采用的RPC框架不原生支持filter,可以自己实现一个“责任链模式”融合进去。

扩容:在微服务化后,只需对压力大的子服务进行针对性扩容;对重要的服务数据采用主从备份。总之,可以对不同的自服务灵活的采用不同的灾备手段。

 

最后说下,微服务的缺点:

1、不方便联调测试,需要启动多个服务。解决办法,在开发环境部署一整套服务,开发本机只启动一个正在开发的服务与之进行联调。

2、不方便事务控制,各个子服务构成了一个分布式环境,在需要事务的地方,必须做分布式事务控制。解决办法,对于mysql等关系型分库分布数据库,可以采用mycat等中间件;对于跨服务的,可以采用MQ的事务机制;其他办法,如日志+人工干预等。总之:分布式事务根据业务场景做到“最终一致性”即可。

 

以后再总结下分布式事务,这次就到这里吧。

猜你喜欢

转载自moon-walker.iteye.com/blog/2375240
今日推荐