基于Dubbo实现定向调度

背景

     Dubbo目前支持了以下4种的负载均衡策略:

       1.一致性Hash均衡算法 (ConsistentHashLoadBalance)

        2.随机调用法 (RandomLoadBalance)

        3.轮询法 (RoundRobInLoadBalance)

        4.最少活动调用法 (LeastActiveLoadBalance)

     因为业务本身是基于长链接架构,用户创建的长链接只在指定的某一台机器上,如果Dubbo调用后的目标对象不在当前服务器上,此时该服务器向RocketMQ发送一条消息,集群服务通过广播模式进行消费处理。

方案弊端:
  1. 依赖于消息队列的组件,额外造成网络开销
  2. 每次请求都是盲盒行为,最少一次,最多2次请求,也会占用其他机器的性能开销,虽然开销不大。
  3. 链路被无形中拉长了,针对于分布式服务而言,链路越短,出错的概率越小。

RocketMQ集群消费模式

自定义实现策略

 Dubbo本身的 LoadBalance 就是一个被 SPI 定义的接口类,官方本身是支持LoadBalance的重写的(一般针对远程调用服务而言,例如Spring Cloud也是支持自定义Ribbon的)。实现的方式也较为简单

1.继承 AbstractLoadBalance.class 实现select方法

public class TestLoadBalance extends AbstractLoadBalance {

    private static final Logger log = LoggerFactory.getLogger(TestLoadBalance.class);

    public static final String NAME = "testLoadBalance";

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        log.info("load for test balance");
        return invokers.get(0);
    }
}

2.在/resources/MET-INF 下注入自己实现的 LoadBalance,使得 Dubbo能够通过 ExtensionLoader 加载到对应的实现

testLoadBalance=com.star.game.main.server.provider.TestLoadBalance

3.在Dubbo的 loadBalance 配置里增加自己定义的 Name 即可。

代码运行截图:

至此就完成了初步的LoadBalance重写,但是很明显,该方案并不能解决我们说的问题,我们诉求的点是:

1.根据请求的字段获取到对应的服务器

2.该服务器地址并不是一个策略,可能需要通过三方获取,也有可能需要通过其他事件监听获取并且缓存记录下来。

实现思路

1.通过添加注解的方式,将需要定向调度的RPC管理起来,并设置定向调度规则

2.添加本地缓存,由于长链接的状态行为,用户在服务器上的行为保持一般都会比较久,无需频繁调用规则

3.添加 三方查询 能力 (RPC请求,Http请求,Redis查询等)

具体实现逻辑

实现架构:

1.定义注解,该注解包含需要定向调度的Key,例如 roomId, userId等等。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface DubboDirectRequest {

    /**
     * roomId 作为缓存
     */
    String roomId() default "";

    /**
     * userId 作为缓存数据
     */
    long userId() default -1L;
}

2.基于Spring的AOP监听注解 ,监听注解中的值,并将该值填入到RpcContext中,使得当前Rpc的调度方式只针对于最近的一个RPC请求

@Aspect
public class LoadBalanceAnnotationAspect {

    private static final Logger log = LoggerFactory.getLogger(LoadBalanceAnnotationAspect.class);

    @Before("@annotation(com.star.game.dubbo.base.annotation.DubboDirectRequest)")
    public void before(JoinPoint joinPoint) throws Throwable{
        Signature signature = joinPoint.getSignature();
        log.info("aop for direct load balance by method {}", signature.getName());
        DubboDirectRequest annotation = Class.forName(signature.getDeclaringTypeName()).getMethod(signature.getName()).getAnnotation(DubboDirectRequest.class);
        RpcContext.getContext().setAttachment("roomId", annotation.roomId());
        RpcContext.getContext().setAttachment("userId", String.valueOf(annotation.userId()));
    }
}

3.实现自定义LoadBalance策略

抽象类的作用主要是能够在其他同学使用我们的SDK包时,能自定义定向调度策略,使得可扩展性更高。

public abstract class AbstractDirectBalance implements InitializingBean {

    private static final Logger log = LoggerFactory.getLogger(AbstractDirectBalance.class);

    LoadingCache<String, Invoker<Object>> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES).build(new CacheLoader<String, Invoker<Object>>() {
                @Override
                public Invoker<Object> load(String key) {
                    return null;
                }
            });

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("init AbstractDirectBalance bean success!");
    }

    public final Invoker<Object> getCache(String key) {
        return cache.getIfPresent(key);
    }

    public final void putCache(String key, Invoker<Object> invoker) {
        cache.put(key, invoker);
    }

    public final void removeCache(String key) {
        cache.invalidate(key);
    }

    public abstract <T> Invoker<T> selectDefaultStrategy(List<Invoker<T>> invokers, URL url, Invocation invocation);

    public abstract  <T> Invoker<T> selectByDirectStrategy(List<Invoker<T>> invokers, URL url, Invocation invocation, String cacheKey);
}

 解析RpcContext携带的定向调度参数,通过自定义的调度规则找到合适的Invoker,每次执行完以后清除当前携带的参数,缓存的实现可以自定义操作,比如什么时候删除缓存,缓存的过期策略等等,缓存的更新机制等等,都可以根据自己的需要定制化。

RpcContext.getContext().clearAttachments();  会清除RpcContext中的所有参数,因为业务中无携带其他参数因此可以直接调用该方法,如果有的同学本身业务中有其他参数在RPC中传递调用,建议还是清除指定的Key

public class DirectLoadBalance extends AbstractLoadBalance {

    private static final Logger log = LoggerFactory.getLogger(DirectLoadBalance.class);

    public static final String NAME = "directIp";

    private AbstractDirectBalance directBalance;

    @SuppressWarnings("all")
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        try {
            if (directBalance == null) {
                directBalance = ApplicationContextUtil.getBean(AbstractDirectBalance.class);
            }
            Map<String, String> attachments = RpcContext.getContext().getAttachments();
            String s = attachments.remove("roomId");
            String a = attachments.remove("userId");
            if (StringUtils.isBlank(s) && StringUtils.isBlank(a)) {
                log.info("method not need direct for rpc");
                return directBalance.selectDefaultStrategy(invokers, url, invocation);
            }
            String cacheKey = buildKey(s, a);
            Invoker<T> invoker = (Invoker<T>) directBalance.getCache(cacheKey);
            log.info("cache invoker is {}", invoker != null);
            if (invoker == null) {
                Invoker<T> select = directBalance.selectByDirectStrategy(invokers, url, invocation, cacheKey);
                log.info("select invoker for ip:{}", select.getUrl().getHost());
                directBalance.putCache(cacheKey, (Invoker<Object>)select);
            }
            return (Invoker<T>) directBalance.getCache(cacheKey);
        } catch (Exception e){
            log.error("DirectLoadBalance error", e);
        }finally {
            RpcContext.getContext().clearAttachments();
            return invokers.get(0);
        }
    }

    private String buildKey(String roomId, String userId) {
        return roomId + "-" + userId;
    }
}

4.注入Spring Bean的获取工具类,方便在服务中自定义获取Bean

public class ApplicationContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextUtil.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static <T> T getBean(Class<T> clz) {
        return applicationContext.getBean(clz);
    }
}

5.自定义注入所需要的Bean即可。

@ConditionalOnMissingBean 在于兜底,使用SDK的业务方如果未自己继承实现抽象类,则有默认的实现机制,默认取第一个invoker。

@ConditionalOnMissingBean(AbstractDirectBalance.class)
    @Bean
    public AbstractDirectBalance directBalance() {
        return new AbstractDirectBalance() {
            @Override
            public <T> Invoker<T> selectDefaultStrategy(List<Invoker<T>> invokers, URL url, Invocation invocation) {
                log.info("default balance select invoker:{}", invokers.get(0).getUrl().getHost());
                return invokers.get(0);
            };
            @Override
            public <T> Invoker<T> selectByDirectStrategy(List<Invoker<T>> invokers, URL url, Invocation invocation, String cacheKey) {
                log.info("direct balance select invoker:{}", invokers.get(0).getUrl().getHost());
                return invokers.get(0);
            }
        };
    }

    @Bean
    public ApplicationContextUtil applicationContextUtil() {
        return new ApplicationContextUtil();
    }

    @Bean
    public LoadBalanceAnnotationAspect loadBalanceAnnotationAspect() {
        return new LoadBalanceAnnotationAspect();
    }

6.Spring stater统一注入配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.star.game.dubbo.auto.config.DubboConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration=\
com.star.game.dubbo.auto.aspect.LoadBalanceAnnotationAspect

运行效果

1.使用SDK抽象类默认实现方式

第一次请求携带注解的方法:

第二次请求该方法:

请求未携带注解的方法,可以看到未被AOP检测到,因此走默认方法:

2.在业务中实现SDK抽象类自定义注入负载策略 

实现代码:

@Slf4j
@Component
public class TestDirectBalance extends AbstractDirectBalance {

     /**
     * 可以自定义注入Spring Bean,通过RPC/Redis或者其他方式去编码自己的负载均衡策略
     */

    @Override
    public <T> Invoker<T> selectDefaultStrategy(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        log.info("business for balance strategy for invoker ip:{}", invokers.get(0).getUrl().getHost());
        return invokers.get(0);
    }

    @Override
    public <T> Invoker<T> selectByDirectStrategy(List<Invoker<T>> invokers, URL url, Invocation invocation, String cacheKey) {
        log.info("business cacheKey {} for strategy for invoker ip:{}", cacheKey, invokers.get(0).getUrl().getHost());
        return invokers.get(0);
    }
}

第一次请求携带注解的方法: 

第二次请求该方法:

 手动clear缓存数据:再次调用该方法:

至此就完成了Dubbo基于业务自己实现的定向调度策略

总结

该实现的优势在于以下几点:
  1. 业务方不需要熟悉 Dubbo 的 SPI 调用方式,基于抽象类可以自己实现自己的负载均衡策略,包括 Dubbo 本身支持的  一致性Hash均衡算法 (ConsistentHashLoadBalance)、随机调用法 (RandomLoadBalance)、轮询法 (RoundRobInLoadBalance)、最少活动调用法 (LeastActiveLoadBalance)  等均可以自己实现
  2. 通过Spring stater的方式将Dubbo以及Spring 的 Bean配置自定义注入,业务方使用SDK可以开箱即用
  3. 基于cache的方式,提高Dubbo调用性能,减少不必要的IO操作
  4. 基于注解的方式,针对Dubbo的调用策略方法精确到方法,对业务代码入侵极少
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值