Spring Cloud客户端负载均衡之Ribbon详解

SpringCloud通过Ribbon实现客户端负载均衡,通过在 RestTemplateBean上注解 LoadBalance实现

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
public @interface LoadBalanced {
}

通过上面的注解我们找到LoadBalancerClient这个类
这是SpringCloud定义的一个接口

image.png

LoadBalancerClient这个类的所属包中我们可以看到
image.png

整理出如下关系,


image.png

从该类的解释

Auto configuration for Ribbon (client side load balancing).

可以看到LoadBalancerAutoConfiguration为自动化配置Ribbon客户端负载均衡的类

LoadBalancerInterceptor拦截器会调用LoadBalancerClientreconstructURI 方法转变成可以发送的实际URI(将服务名替换成Host:Port格式的请求)

我们继续找到LoadBalancerClient的实现类RibbonLoadBalancerClient

image.png

查看RibbonLoadBalancerClient的源码可以看到
image.png

执行总共有分下面几步:

1. 得到客户端负载均衡器
2. 得到Server,并封装成RibbonServer
3. 回调LoadBalancerRequest执行请求 request.apply

1. 得到客户端负载均衡器
ILoadBalancer 是选择哪个客户端负载均衡器的接口
具体实现有:

image.png

ILoadBalancer (com.netflix.loadbalancer)
AbstractLoadBalancer (com.netflix.loadbalancer)
NoOpLoadBalancer (com.netflix.loadbalancer)
BaseLoadBalancer (com.netflix.loadbalancer)
DynamicServerListLoadBalancer (com.netflix.loadbalancer)
ZoneAwareLoadBalancer (com.netflix.loadbalancer)
可以看到都是来自netflix 原生的代码,默认的Ribbon配置的是哪个LoadBalancer呢?
通过RibbonClientConfiguration我们可以看到

    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
            ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
            IRule rule, IPing ping) {
        ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder()
                .withClientConfig(config).withRule(rule).withPing(ping)
                .withServerListFilter(serverListFilter).withDynamicServerList(serverList)
                .buildDynamicServerListLoadBalancer();
        return balancer;
    }

Ribbon默认是 配置ZoneAwareLoadBalancer 这个负载均衡器

2. 得到Server,并封装成RibbonServer
继续分析 RibbonLoadBalancerClient.execute
根据loadBalancer得到Server ,将Server封装成RibbonServer
RibbonServer 比Server多了一些信息,并且是一个 ServiceInstance实例


    protected static class RibbonServer implements ServiceInstance {
        private final String serviceId;
        private final Server server;
        private final boolean secure;
        private Map<String, String> metadata;

3. 回调LoadBalancerRequest执行请求 request.apply
在执行Request.apply的时候会执行 LoadBalancerInterceptor中定义的方法如下

image.png

其中ServiceRequestWrapper里有个方法通过调用负载均衡reconstructURI的方法实现了服务名替换URI的功能

        @Override
        public URI getURI() {
            URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI(
                    this.instance, getRequest().getURI());
            return uri;
        }

execution.execute(serviceRequest, body); 在哪里执行的呢?
查看其实现类可以发现是在InterceptingClientHttpRequest这个类里面的内部类InterceptingRequestExecution implements ClientHttpRequestExecution
源码如下:

        @Override
        public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
            if (this.iterator.hasNext()) {
                ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
                return nextInterceptor.intercept(request, body, this);
            }
            else {
                ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());
                delegate.getHeaders().putAll(request.getHeaders());
                if (body.length > 0) {
                    StreamUtils.copy(body, delegate.getBody());
                }
                return delegate.execute();
            }
        }

其中 requestFactory.createRequest(request.getURI() 的request.getURI() 会调用ServiceRequestWrappergetURI方法,getURI又是调用RibbonLoadBalancerClient.reconstructURI方法

负载均衡器

ILoadBalancer (com.netflix.loadbalancer)
netflix原生的接口
AbstractLoadBalancer (com.netflix.loadbalancer)
定义了 ServerGroup 枚举类
和另外两个方法
NoOpLoadBalancer (com.netflix.loadbalancer)
啥都是空的
BaseLoadBalancer (com.netflix.loadbalancer)
这个是负载均衡的基础实现类

  1. 定义了两个易变的变量以维护服务器清单,一个是所有的,一个是可用的
    @Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
    protected volatile List<Server> allServerList = Collections
            .synchronizedList(new ArrayList<Server>());
    @Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
    protected volatile List<Server> upServerList = Collections
            .synchronizedList(new ArrayList<Server>());
  1. 定义了用来存储负载均衡器各服务实例属性和统计信息的LoadBalancerStats对象
    protected LoadBalancerStats lbStats;
  2. 定义了心跳检测的相关变量,IPing是要构造的时候传入,IPingStrategy
    默认是用SerialPingStrategy 实现类,

    protected IPingStrategy pingStrategy = DEFAULT_PING_STRATEGY;

    protected IPing ping = null;

SerialPingStrategy 代码如下,注意其中的注释说明,当IPing速度不理想时应该怎么办...

    /**
     * Default implementation for <c>IPingStrategy</c>, performs ping
     * serially, which may not be desirable, if your <c>IPing</c>
     * implementation is slow, or you have large number of servers.
     */
    private static class SerialPingStrategy implements IPingStrategy {

        @Override
        public boolean[] pingServers(IPing ping, Server[] servers) {
            int numCandidates = servers.length;
            boolean[] results = new boolean[numCandidates];

            if (logger.isDebugEnabled()) {
                logger.debug("LoadBalancer:  PingTask executing ["
                             + numCandidates + "] servers configured");
            }

            for (int i = 0; i < numCandidates; i++) {
                results[i] = false; /* Default answer is DEAD. */
                try {
                    // NOTE: IFF we were doing a real ping
                    // assuming we had a large set of servers (say 15)
                    // the logic below will run them serially
                    // hence taking 15 times the amount of time it takes
                    // to ping each server
                    // A better method would be to put this in an executor
                    // pool
                    // But, at the time of this writing, we dont REALLY
                    // use a Real Ping (its mostly in memory eureka call)
                    // hence we can afford to simplify this design and run
                    // this
                    // serially
                    if (ping != null) {
                        results[i] = ping.isAlive(servers[i]);
                    }
                } catch (Throwable t) {
                    logger.error("Exception while pinging Server:"
                                 + servers[i], t);
                }
            }
            return results;
        }
    }
  1. 定义了负载均衡的处理规则IRule对象
    protected IRule rule = DEFAULT_RULE;
    默认是用RoundRobinRule,实现了线性负载均衡规则
    private final static IRule DEFAULT_RULE = new RoundRobinRule();
  2. 启动Ping任务,默认每隔十秒对Server做一次健康检查
    image.png

    protected int pingIntervalSeconds = 10;
  3. markServerDown 标记一个Server暂停服务

DynamicServerListLoadBalancer (com.netflix.loadbalancer)
BaseLoadBalancer的扩展,实现了实例清单在运行期动态更新的能力,还具备对服务清单的过滤功能

  1. ServerList
    image.png

    image.png

    默认配置的是DomainExtractingServerList,从EurekaRibbonClientConfiguration中可得到
    @Bean
    @ConditionalOnMissingBean
    public ServerList<?> ribbonServerList(IClientConfig config) {
        DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
                config);
        DomainExtractingServerList serverList = new DomainExtractingServerList(
                discoveryServerList, config, this.approximateZoneFromHostname);
        return serverList;
    }

DiscoveryEnabledNIWSServerList

  1. ServerListUpdater获取服务清单以及更新本地服务清单
    protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };

    protected volatile ServerListUpdater serverListUpdater;

image.png

ServerListFilter
image.png

ZoneAwareLoadBalancer (com.netflix.loadbalancer)
解决了多区域部署时,DynamicServerListLoadBalancer 可能会产生的性能问题,因为跨区域会产生更高的延迟

public class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {

负载均衡策略

IRule 的实现类有

image.png

RandomRule
随机取一个实例,如果取不到可能会有BUG
RoundRobinRule
线性轮询的方式去,超过10次取不到就报警告退出

    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

RetryRule
该类内部定义了个IRule对象,如下

public class RetryRule extends AbstractLoadBalancerRule {
    IRule subRule = new RoundRobinRule();
    long maxRetryMillis = 500;

默认是用 RoundRobinRule

    public Server choose(ILoadBalancer lb, Object key) {
        long requestTime = System.currentTimeMillis();
        long deadline = requestTime + maxRetryMillis;

        Server answer = null;

        answer = subRule.choose(key);

        if (((answer == null) || (!answer.isAlive()))
                && (System.currentTimeMillis() < deadline)) {

            InterruptTask task = new InterruptTask(deadline
                    - System.currentTimeMillis());

            while (!Thread.interrupted()) {
                answer = subRule.choose(key);

                if (((answer == null) || (!answer.isAlive()))
                        && (System.currentTimeMillis() < deadline)) {
                    /* pause and retry hoping it's transient */
                    Thread.yield();
                } else {
                    break;
                }
            }

            task.cancel();
        }

        if ((answer == null) || (!answer.isAlive())) {
            return null;
        } else {
            return answer;
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容