ribbon 配置 动态更新_Ribbon的应用

fef68280df63fc9364f756ecce92e532.png

1 背景

19年笔者一直在跟进一个NFC相关的项目(这个会在以后的文章中详细介绍),年关临近,项目即将产品化,许多第三方提供的能力、服务都准备验收,其中有一个关键的供应商的验收出了些问题。具体来说,该供应商的交付物是web应用,已docker容器的方式部署在我公司的服务器上,由于不满足公司容器集群的相关要求,无法纳入k8s统一管理,于是成为了“野生”docker,关于此类docker的运维部署方案却着实难住了我。

难点有很多,这次我们只说高可用这一件事,简单来讲高可用就是可侦测,可切换,可恢复三个大方向。落实到编程技术方面就是拨测、负载均衡和重试(这可能是最简陋的实现了),接下来说说我是怎么思考和实现这三方面的。

2 方案

两个方案:

  • 服务端负载均衡,创建几个docker,前置一个nginx;或者搞一个docker-swarm
  • 客户端负载均衡,ribbon,eureka这一套,访问一个自定义的域名,由域名指向不同的server

先说方案1,对于服务调用方,也就是我,这是最省心的方案了。代码修改几乎为0,运维监控也完全不需要我操心,相当于所有的权限责任都划归运维团队就好了。但是,由于docker不能纳入集群管理,运维团队对野生docker投入的资源能有多少呢?而且这样做客户端丧失了个性化配置的主动权。另一方面,本良部署几个docker就可以的事,现在还要swarm,nginx增加了组件也就增加了运维成本和监控点。本着自己的服务自己负责的原则,决定用客户端负载均衡

3 实现

客服端负载均衡,笔者公司用的spring-cloud这一套架构,从前往后看,将服务注册到euraka,这需要服务端开发,所以不行,其余两个还可以试一下。于是决定用ribbon。

ribbon的基本用法google一下随处可见,原理也很清晰,对请求做拦截,实现不同的负载均衡算法(说句题外话,很多spring的架构都逃不开动态代理,反射)具体可以看这篇文章。下面说一下思路,配置自定义ribbon,先看看默认的实现是否符合要求,如果符合直接用(自己实现的肯定没有源码nb,我还是很有自知之明的),不符合的自己写个简单的

@Configuration
@RibbonClient(name = "test-server", configuration = CustomizeRibbonConfig.class)
public class CustomizeRibbonClient {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        // 用okhttpclient做访问,简单实现
        OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory(OkHttpBuilderFactory.getTrusClient());
        return new RestTemplate(factory);
    }

}

当扫描到@LoadBalanced时,会默认生成一个DynamicServerListLoadBalancer,这个是核心配置,ribbon的配置会注册到DynamicServerListLoadBalancer上。ribbon的自定义配置是从@RibbonClient(name ="test-server", configuration = CustomizeRibbonConfig.class)注解开始的,这里的name就是你访问的域名,当然也支持配制的,CustomizeRibbonConfig 这个类就是各个功能组件的配置类,下面的restTemplate就是访问的客户端对象,此处可以配置用OkHttp3Client,ribbon是支持OkHttp的。

注意,很多博客会说CustomizeRibbonConfig这个类不能被spring扫描到,否则会覆盖全局配置,其实想想,既然是做自定义ribbon配置,肯定要有自定义的配置项啊,如果怕用@ComponentScan避免加载的方式影响项目其他模块的加载,CustomizeRibbonConfig类上可以不写@Configuration。

继续来看,当启动扫描到@RibbonClient注解时,它会创建spring子上下文,寻找如下七个对象:

  • IClientConfig 自定义ribbon的所有配置项,默认读取yml文件中@RibbonClient(name ="test-server") name的value对应的配置,
  • IRule 定义负载均衡策略
  • IPing 定义如何ping目标服务实例来判断是否存活,默认返回true,不处理
  • ServerList定义如何获取服务实例列表. 有两个默认实现,一个是基于配置文件,一个基于eureka
  • ServerListFilter用来使用期望的特征过滤静态配置动态获得的候选服务实例列表
  • ServerListUpdater 用来定义服务器列表的更新方式,默认有定时读取的和注册监听eureka事件的

如果没找到就会默认创建,其中超时、重试等功能可以通过IClientConfig 配置,通过 IPing 可以实现一些简单的拨测,如下:

@Bean   
public IPing getIPing(){
        return server -> {
            try (Socket socket = new Socket()) {
                socket.connect(new InetSocketAddress(server.getHost(), server.getPort()), Config.getPingConnectionTimeoutMs());
            } catch (IOException e) {
                log.warn("Failed to connect to {} in zone {} via {}", server.getId(), server.getZone(), server.getHostPort());
                return false;
            }
            return true;
        };
    }

下面介绍下serverList的更新机制

我们先看ServerList接口

public interface ServerList<T extends Server> {

    public List<T> getInitialListOfServers();
    
    /**
     * Return updated list of servers. This is called say every 30 secs
     * (configurable) by the Loadbalancer's Ping cycle
     * 
     */
    public List<T> getUpdatedListOfServers();   

}

嗯,没什么说的,再看ServerListUpdater

public interface ServerListUpdater {

public interface UpdateAction {
        void doUpdate();
    }


    /**
     * start the serverList updater with the given update action
     * This call should be idempotent.
     *
     * @param updateAction
     */
    void start(UpdateAction updateAction);

    /**
     * stop the serverList updater. This call should be idempotent
     */
    void stop();

    // continue ...

}

可以看到真正的更新动作在UpdateAction 中,而更新的trigger在start,我们看看ribbon的默认实现:

//LoadBalancer默认实现
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer{
    protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };
    @VisibleForTesting
    public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            // 调用serverList的update接口
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);

            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
        updateAllServerList(servers);
    }
}
// ServerListUpdater 默认实现
public class PollingServerListUpdater implements ServerListUpdater {
@Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };
            // 系统启动定时任务,默认30s更新一次
            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }
}

可以看到,默认30s读一次serverList,其实这里如果觉得缺少实时性,可以基于事件修改的,现在的互联网中台都有配置中心,可以监听配置中心的配置更新事件,来动态更新serverList这里给一个示例:

 @Slf4j
public class CustomizeServerListUpdater implements ServerListUpdater, ServerListUpdater.UpdateAction {

    private ConfigChangeListener listener;
    private UpdateAction updateAction;

    private final AtomicBoolean isActive = new AtomicBoolean(false);
    private volatile long lastUpdated = System.currentTimeMillis();

    public CustomizeServerListUpdater (ConfigChangeListener listener) {
        this.listener = listener;
    }

    // 装饰器
    @Override
    public void doUpdate(){
        if (isActive.get() && updateAction!=null){
            updateAction.doUpdate();
        }
    }

    
    @Override
    public void start(UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            try {
                this.updateAction = updateAction;
                // 初始化操作先执行一次
                listener.init(this);
                updateAction.doUpdate();
                lastUpdated = System.currentTimeMillis();
            } catch (Exception e) {
                log.warn("Failed one update cycle", e);
            }
        }
        else {
            log.info("Already active, no-op");
        }
    }

    @Override
    public void stop() {
        if (isActive.compareAndSet(true, false)) {
            this.updateAction = null;
            this.listener.destroy();
        }
        else {
            log.info("Not active, no-op");
        }
    }

    @Override
    public String getLastUpdate() {
        return new Date(lastUpdated).toString();
    }

    @Override
    public long getDurationSinceLastUpdateMs() {
        return System.currentTimeMillis() - lastUpdated;
    }

    @Override
    public int getNumberMissedCycles() {
        return 0;
    }

    @Override
    public int getCoreThreads() {
        return 0;
    }
}


// 各位可以依据各自公司的架构来实现哈
@Configuration
public class ConfigChangeListener implements ConfigChangeListener {

    private ServerListUpdater.UpdateAction updater;

    private boolean isActive = false;

    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
        if (isActive && changeEvent.changedKeys().stream()
                .anyMatch(changedKey->changedKey.startsWith(Config.PREFIX))){
            updater.doUpdate();
        }

    }

    public void init(ServerListUpdater.UpdateAction updater){
        this.updater = updater;
        this.isActive = updater!=null;
    }

    public void destroy(){
        this.updater = null;
        this.isActive = false;
    }
}

这样就可以了,再说几点注意事项哈,监听配置变化时要注意,切面是在配置修改前还是后,还有哈,ribbon默认是懒加载的,不访问接口,连IPing都不执行,所以最好改成起动加载。

4 思考

现在saas流行,不知道其他公司如何应对这种野生docker,欢迎留言哈。

最后啰嗦一点,我觉得网上的博客真是多啊,但脱离应用场景就讲代码有点耍流氓,我想写一些和实际应用结合的东西。但是笔者就是一普通程序员,能力有限,希望批评指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值