1.介绍

我们知道,随着网站应用访问量的增加,一台服务器很难满足需求,所以往往需要对服务器进行集群处理。这么多的服务器,我们每次网络请求要怎么分配、以何种方式分配到每台服务器?这时候就需要负载均衡了。所以从这一点可以看出负载均衡的主要用途了——对请求进行分发,保证服务器资源的合理利用。

2.分类

总体上来说。有三种负载均衡架构,分别是链路负载均衡、集群负载均衡、操作系统负载均衡。

  • 链路负载均衡:通过DNS解析成不同的IP,然后根据得到的IP来访问不同的服务器
  • 集群负载均衡:一般分为硬件和软件负载均衡,硬件如F5,软件如nginx、HAProxy等
  • 操作系统负载均衡:利用操作系统级别的软中断或者硬件中断来达到负载均衡

集群负载均衡一般使用的比较多,我们可以按照不同的维度对其进行大概的分类归纳如下:

2.1 硬件和软件

硬件负载均衡:直接在服务器和外部网络间安装负载均衡设备,这种设备我们通常称之为负载均衡器。由于专门的设备完成专门的任务,独立于操作系统,整体性能得到大量提高,但是通常成本较高。

软件负载均衡:在一台服务器的操作系统上,安装一个附加软件来实现负载均衡,如Nginx负载均衡。基于特定环境、配置简单、使用灵活、成本低廉,可以满足大部分的负载均衡需求。

2.2 服务端和客户端

服务端负载均衡:先发送请求,请求到达负载服务器后通过负载均衡算法,在多个服务器之间选择一个进行访问。

客户端负载均衡:客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问。

2.3 2层、3层、4层和7层

这里说的层是指OSI的七层协议,从下往上依此是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

2层负载均衡:即在数据链路层进行负载均衡,通过mac地址。

3层负载均衡:即在网络层进行负载均衡,通过ip地址

4层负载均衡:即在传输层(TCP/SSL)进行负载均衡,通过ip+port。

7层负载均衡:即在应用层(http/https)进行负载均衡,通过url。

参考:

http://blog.csdn.net/dlf123321/article/details/52131542

http://blog.csdn.net/shendeguang/article/details/75003509

https://www.thegeekstuff.com/2016/01/load-balancer-intro/

https://www.jianshu.com/p/fa937b8e6712

3.常用负载均衡介绍

3.1 F5

一个负载均衡器专用设备,架设在应用服务器之前。主要有如下功能:

  1. 提供12种灵活的算法将所有流量均衡的分配到各个服务器
  2. 可以确认应用程序能否对请求返回对应的数据,即可以知道服务器的存活情况而确定是否分发请求。
  3. 具有动态Session的会话保持功能。
  4. 提供的iRules功能可以做HTTP内容过滤,根据不同的域名、URL,将访问请求传送到不同的服务器。
3.2 Apache

Apache是使用很广泛的一种web server,它以跨平台,高效和稳定而闻名,可以通过加载模块的方式将其变成一个反向代理服务器,实现负载均衡。

https://yq.aliyun.com/articles/38759

http://blog.sina.com.cn/s/blog_150e503430102wt7m.html

Tomcat+Apache实现负载均衡的三种连接方式

  1. apache通过mod_proxy(http_proxy)模块,使用HTTP协议与Tomcat连接,并通过route唯一标识关联后端Tomcat
  2. apache通过mod_jk模块,使用AJP协议与Tomcat连接,这种方式使用广泛
  3. apache通过mod_proxy(ajp_proxy,Apache2.2中才有)模块,只能用AJP协议代理

环境配置

配置


<VirtualHost *:80> ServerName 192.168.18.113 ProxyPass /images ! ProxyPass /css ! ProxyPass /js ! ProxyPass / balancer://cluster/ stickysession=JSESSIONID nofailover=Off ProxyPassReverse / balancer://cluster/ ErrorLog "/var/log/apache2/cluster-error.log" CustomLog "/var/log/apache2/cluster-access.log" common <proxy balancer://cluster> BalancerMember http://192.168.18.109:8084 loadfactor=1 route=node1 BalancerMember http://192.168.18.109:80 loadfactor=1 route=node2 </proxy> </VirtualHost>

Apache的server为process-based server ,也就是基于多进程的HTTPServer,它需要对每个用户请求创建一个子进程进行响应,如果并发的请求非常多,就会需要非常多的进程,从而占用极多的cpu资源和内存。因此对于并发处理不是Apache的强项,而这就和负载均衡的最初目的有点矛盾——负载均衡就是为了处理高并发而架设的,所以虽说apache可以负载均衡的功能,但是一般使用的比较少。

3.3 Nginx

Nginx不光可以实现Web Server,还因为有着占有内存少,并发能力强的特点,常常被用作为HTTP负载均衡来分发流量给后端的应用程序服务器,以此来提高性能。

Nginx的负载均衡功能依赖于ngx_http_upstream_module模块,所支持的代理方式有proxy_pass,fastcgi_pass,memcached_pass。

Nginx有如下负载方式:

  • 轮询(默认算法)——每个请求会依次分配给后端不同的应用程序服务器,不理会后端服务器的实际压力
  • 加权轮询——权重越大的服务器,被分配到的次数就会越多,通常用于后端服务器性能不一致的情况
  • IP HASH——当同IP进行重复访问时会被指定到上次访问到的服务器,可以解决动态网站SESSION共享问题

参考:

https://www.linuxidc.com/Linux/2016-04/130350.htm

https://www.cnblogs.com/youzhibing/p/7327342.html

3.4 LVS

LVS是 Linux Virtual Server 的简称,是一个虚拟的服务器集群系统。利用LVS的主要作用可以说是将内网服务器映射到公网。LVS集群中有三种IP负载均衡技术:VS/NAT、VS/TUN、VS/DR。

LVS主要组成部分为:

  • 负载调度器(load balancer/Director),它是整个集群对外面的前端机,负责将客户的请求发送到一组服务器上执行,而客户认为服务是来自一个IP地址(我们可称之为虚拟IP地址)上的。

  • 服务器池(server pool/ Realserver),是一组真正执行客户请求的服务器,执行的服务一般有WEB、MAIL、FTP和DNS等。

  • 共享存储(shared storage),它为服务器池提供一个共享的存储区,这样很容易使得服务器池拥有相同的内容,提供相同的服务。

VS/NAT

其实,按照名称来理解很明白,利用NAT,将内网IP映射到公网IP地址,由NAT路由器进行转发。所有的RealServer只需要将自己的网关指向Director即可。客户端可以是任意操作系统,但此方式下,一个Director能够带动的RealServer比较有限,因为无论NAT路由器是转发还是返回请求都必须对内容进行处理加工。

img

VS/TUN

负载调度器只将请求调度到不同的后端服务器,后端服务器将应答的数据直接返回给用户。相比较于VS/NAT而言,少了将返回内容进行加工的一步。

img

VS/DR

调度器根据各个服务器的负载情况,动态地选择一台服务器,不修改也不封装IP报文,而是将数据帧的MAC地址改为选出服务器的MAC地址,再将修改后的数据帧在与服务器组的局域网上发送。

img

VS/NAT VS/TUN VS/DR
操作系统 任意 Linux(IP tunnel) 大多数
服务器网络 私有 局域网/广域网 局域网
服务器可见 不可见 可见 可见

参考:

http://soft.chinabyte.com/25/13169025.shtml

http://blog.sina.com.cn/s/blog_7e76ec510102we9v.html

3.5 HAProxy

HAProxy是一个免费的负载均衡软件,可以运行于大部分主流的Linux操作系统上。HAProxy提供了L4(TCP)和L7(HTTP)两种负载均衡能力,具备丰富的功能,具备媲美商用负载均衡器的性能和稳定性。

HAProxy的核心功能

  • 负载均衡:L4和L7两种模式,支持RR/静态RR/LC/IP Hash/URI Hash/URL_PARAM Hash/HTTP_HEADER Hash等丰富的负载均衡算法
  • 健康检查:支持TCP和HTTP两种健康检查模式
  • 会话保持:对于未实现会话共享的应用集群,可通过Insert Cookie/Rewrite Cookie/Prefix Cookie,以及上述的多种Hash方式实现会话保持
  • SSL:HAProxy可以解析HTTPS协议,并能够将请求解密为HTTP后向后端传输HTTP请求重写与重定向
  • 监控与统计:HAProxy提供了基于Web的统计信息页面,展现健康状态和流量数据。基于此功能,使用者可以开发监控程序来监控HAProxy的状态

参考

https://www.jianshu.com/p/c9f6d55288c0

3.6 Ribbon

Ribbon是一个基于HTTP的客户端负载工具。所谓客户端负载均衡,指的是客户端在发起请求的时候会先自行选择一个服务端,向该服务端发起请求,从而实现负载均衡。所以客户端就需要知道某一个服务由哪些服务端提供,这就要求客户端节点都有一份自己要访问的服务端清单,这些清单统统都是从Eureka服务注册中心获取的,在消费某个服务时按照某种轮询方式选择服务端以实现均衡。

3.6.1 开发使用

启用Ribbon也很简单,创建一个RestTemplate Bean,然后在bean上使用@LoadBalanced注解。

@Configuration
public class RestConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

3.6.2 @LoadBalanced

为什么在RestTemplate 上加一个@LoadBalanced注解就可以实现负载均衡了呢,我们还是根据源码来详细了解一下。通过源码可以看到,这个注解本质上就是一个@Qualifier注解。

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

我们再看下org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration这个客户端负载的自动配置类,当我们为RestTemplate 加上@LoadBalanced注解后,究竟做了哪些工作。

这里面也有用到@LoadBalanced注解,用在List restTemplates = Collections.emptyList()语句之上,这就说明我们前面创建的RestTemplate实例被注入到这个list中,然后通过定制器被添加上了LoadBalancerInterceptor拦截器。所以现在可以明白@LoadBalanced的作用就是通过指定哪一个真正的RestTemplate bean 将会被装配,这也就是说@Loadbalanced完全可以被替换成@Qualifier。

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
            final List<RestTemplateCustomizer> customizers) {
        return new SmartInitializingSingleton() {
            @Override
            public void afterSingletonsInstantiated() {
                for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                    for (RestTemplateCustomizer customizer : customizers) {
                        customizer.customize(restTemplate);
                    }
                }
            }
        };
    }

    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(
            LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
    }

    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient,
                LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

        @Bean
        @ConditionalOnMissingBean
         // RestTemplate定制器
        public RestTemplateCustomizer restTemplateCustomizer(
                final LoadBalancerInterceptor loadBalancerInterceptor) {
            return new RestTemplateCustomizer() {
                @Override
                public void customize(RestTemplate restTemplate) {
                    List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    // 为restTemplate添加loadBalancerInterceptor拦截器
                      list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                }
            };
        }
    }
}

LoadBalancerInterceptor的intercept拦截了RestTemplate请求,然后将交给LoadBalancerClient去处理请求内容。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        // for backwards compatibility
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

LoadBalancerClient是个接口,其实现类是RibbonLoadBalancerClient,请求被拦截器拦截之后,会交由RibbonLoadBalancerClient的execute(String serviceId, LoadBalancerRequest request)方法处理请求内容,首先会根据某种负载策略选择一个服务器,然后将请求重新交付给InterceptingRequestExecution处理。

public class RibbonLoadBalancerClient implements LoadBalancerClient {

    private SpringClientFactory clientFactory;

    public RibbonLoadBalancerClient(SpringClientFactory clientFactory) {
        this.clientFactory = clientFactory;
    }

    @Override
    public URI reconstructURI(ServiceInstance instance, URI original) {
        Assert.notNull(instance, "instance can not be null");
        String serviceId = instance.getServiceId();
        RibbonLoadBalancerContext context = this.clientFactory
                .getLoadBalancerContext(serviceId);
        Server server = new Server(instance.getHost(), instance.getPort());
        IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
        ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
        URI uri = RibbonUtils.updateToHttpsIfNeeded(original, clientConfig,
                serverIntrospector, server);
        return context.reconstructURIWithServer(server, uri);
    }

    @Override
    public ServiceInstance choose(String serviceId) {
        Server server = getServer(serviceId);
        if (server == null) {
            return null;
        }
        return new RibbonServer(serviceId, server, isSecure(server, serviceId),
                serverIntrospector(serviceId).getMetadata(server));
    }

    @Override
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        // 根据某种策略选择一个服务器
         ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        Server server = getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }
         // 封装到RibbonServer
        RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
                serviceId), serverIntrospector(serviceId).getMetadata(server));

        return execute(serviceId, ribbonServer, request);
    }

    @Override
    public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
        Server server = null;
        if(serviceInstance instanceof RibbonServer) {
            server = ((RibbonServer)serviceInstance).getServer();
        }
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }

        RibbonLoadBalancerContext context = this.clientFactory
                .getLoadBalancerContext(serviceId);
        RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

        try {
             // 将请求重新交付给InterceptingRequestExecution
            T returnVal = request.apply(serviceInstance);
            statsRecorder.recordStats(returnVal);
            return returnVal;
        }
        // catch IOException and rethrow so RestTemplate behaves correctly
        catch (IOException ex) {
            statsRecorder.recordStats(ex);
            throw ex;
        }
        catch (Exception ex) {
            statsRecorder.recordStats(ex);
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }

    private ServerIntrospector serverIntrospector(String serviceId) {
        ServerIntrospector serverIntrospector = this.clientFactory.getInstance(serviceId,
                ServerIntrospector.class);
        if (serverIntrospector == null) {
            serverIntrospector = new DefaultServerIntrospector();
        }
        return serverIntrospector;
    }

    private boolean isSecure(Server server, String serviceId) {
        IClientConfig config = this.clientFactory.getClientConfig(serviceId);
        ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
        return RibbonUtils.isSecure(config, serverIntrospector, server);
    }

    protected Server getServer(String serviceId) {
        return getServer(getLoadBalancer(serviceId));
    }

    protected Server getServer(ILoadBalancer loadBalancer) {
        if (loadBalancer == null) {
            return null;
        }
        return loadBalancer.chooseServer("default"); // TODO: better handling of key
    }

    protected ILoadBalancer getLoadBalancer(String serviceId) {
        return this.clientFactory.getLoadBalancer(serviceId);
    }

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

        public RibbonServer(String serviceId, Server server) {
            this(serviceId, server, false, Collections.<String, String> emptyMap());
        }

        public RibbonServer(String serviceId, Server server, boolean secure,
                Map<String, String> metadata) {
            this.serviceId = serviceId;
            this.server = server;
            this.secure = secure;
            this.metadata = metadata;
        }

    }
}

RibbonLoadBalancerClient对请求内容处理完以后,调用LoadBalancerRequest的apply方法,将请求重新归还给ClientHttpRequestExecution。ClientHttpRequestExecution对请求拦截器继续遍历执行拦截,直到所有拦截器都执行完毕最后将请求发出。

public class LoadBalancerRequestFactory {

    private LoadBalancerClient loadBalancer;
    private List<LoadBalancerRequestTransformer> transformers;

    public LoadBalancerRequestFactory(LoadBalancerClient loadBalancer,
            List<LoadBalancerRequestTransformer> transformers) {
        this.loadBalancer = loadBalancer;
        this.transformers = transformers;
    }

    public LoadBalancerRequestFactory(LoadBalancerClient loadBalancer) {
        this.loadBalancer = loadBalancer;
    }

    public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request,
            final byte[] body, final ClientHttpRequestExecution execution) {
        return new LoadBalancerRequest<ClientHttpResponse>() {

            @Override
            public ClientHttpResponse apply(final ServiceInstance instance)
                    throws Exception {
                HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
                if (transformers != null) {
                    for (LoadBalancerRequestTransformer transformer : transformers) {
                        serviceRequest = transformer.transformRequest(serviceRequest, instance);
                    }
                }
                return execution.execute(serviceRequest, body);
            }

        };
    }

}

那么,RestTemplate在发起请求时是怎么被拦截器拦截下来的呢,实际上RestTemplate在发起请求是通过ClientHttpRequestExecution的实例InterceptingRequestExecution发起请求的,请求过程中,如果有拦截器,请求会交由拦截器处理。

class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {



    private class InterceptingRequestExecution implements ClientHttpRequestExecution {

        private final Iterator<ClientHttpRequestInterceptor> iterator;

        public InterceptingRequestExecution() {
            this.iterator = interceptors.iterator();
        }

        @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());
                for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
                    List<String> values = entry.getValue();
                    for (String value : values) {
                        delegate.getHeaders().add(entry.getKey(), value);
                    }
                }
                if (body.length > 0) {
                    StreamUtils.copy(body, delegate.getBody());
                }
                return delegate.execute();
            }
        }
    }

}

现在我们来梳理一下整个客户端负载均衡的思路:首先带有@LoadBalanced注解的RestTemplate实例被注入LoadBalancerAutoConfiguration配置类,RestTemplate被添加上了LoadBalancerInterceptor拦截器。后续在使用RestTemplate发起RESTful请求过程中,请求的实际执行者InterceptingRequestExecution会把请求先交付给LoadBalancerInterceptor拦截器,拦截器拦截到请求内容后,RibbonLoadBalancerClient将请求做进一步的处理,首先根据serviceId从符合条件的服务器中按照某个负载策略选择一台合适的服务器,然后对请求作进一步的封装——RibbonServer,将请求内容改成IP加端口的形式。在对请求内容处理完毕后,请求又被重新交付给InterceptingRequestExecution,InterceptingRequestExecution将重复上述动作直到所有拦截器都已执行完毕,然后将请求发出。

3.7 DNS

DNS负载均衡技术的实现原理是在DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。

下面是全局负载均衡(GSLB)解析示意图:

  1. 用户向本级配置的本地DNS服务器发出查询请求,如果本地DNS服务器有该域名的缓存记录,则返回给用户,否则进行第2步;
  2. 本地DNS服务器进行递归查询,最终会查询到域名服务商商处的授权DNS服务器,这里可能有多个步骤,图中只反映最后一步;

  3. 授权DNS服务器返回一条NS记录给本地DNS服务器。根据授权DNS服务器上的不同设置,这条NS记录可能是指向随机一个GSLB设备的接口地址或者是所有GSLB设备的接口地址;
  4. 本地DNS服务器向其中一个GSLB地址发出域名查询请求,如果请求超时会向其它地址发出查询;
  5. GSLB设备选出最优解析结果,返回一条A记录给本地DNS服务器。根据全局负载均衡策略设定的不同可能返回一个或多个VIP地址;
  6. 本地服务器将查询结果通过一条A记录返回给用户,并将缓存这条记录。

参考:

https://www.nginx.com/resources/glossary/dns-load-balancing/

https://www.zhihu.com/question/29787004/answer/62775195

3.8 CDN

CDN负载均衡技术是整个CDN系统的核心,它的作用是将某一用户的请求导向整个CDN网络中距离该用户最近最稳定加速节点,保障互联网站终端用户能够与网站之间建立稳定高效的。

img

通常情况下,CDN负载均衡技术分为两个层次:全局负载均衡和本地负载均衡

1、全局均衡负载的主要目的是在整个网络范围内实现将用户的请求定向到最近的节点。因此,全局负载均衡的主要功能是实现就近性判断。目前,全局均衡负载系统一般采用DNS来实现。对于网络系统而言,执行全局负载均衡可以对用户的请求进行初步的定向,以减轻本地负载均衡的负担。

2、和全局负载均衡相比,本地负载均衡一般局限于一定的区域范围内。本地负载均衡侧重于根据CDN节点的健康性、负载情况、策略等进行精细的负载均衡。因此本地负载均衡设备一般需要了解CDN节点的具体运行状况作为执行本地负载均衡的依据。

参考:

https://www.zhihu.com/question/36514327

3.9 方案比较

这里对上述提供的负载均衡发难进行下比较,便于以后在实际中更好的选择出最佳的方案:

主要作用 工作层数 抗负载能力 后端服务器健康检查 正则表达式 配置复杂性 功能拓展 分类功能拓展 成本分类
F5 负载均衡 4-7层 支持 支持 复杂 硬件 高昂硬件
Apache web服务器 7 支持 简单 有,通过拓展模块 软件/服务端
Nginx web、email服务器,反向代理服务器 7 端口 支持 简单 有,通过拓展模块 软件/服务端
Ribbon 客户端负载均衡 / / 会剔除不可达应用 可自行拓展负载算法实现 简单 软件/客户端
LVS 构成高性能的、高可用的虚拟服务器 4 很强 端口 不支持 复杂 有,keepalived 软件/服务端有
HAProxy 负载均衡、应用代理 4 很强 支持端口、url、请求头检查 支持 简单 软件/服务端
DNS 域名解析 7 / 简单 软件/服务端 本地使用成本低廉
CDN 内容分发加速 7 很强 / 购买服务 软件/服务端
类型 优点 缺点 适用场景
F5 1.系统无关 2.负载性能强更适用于一大堆设备、大访问量、简单应用 1.价格高昂 2.难以集群 3.一般都不管实际系统与应用的状态,而只是从网络层来判断 实力雄厚的企业用户
Apache 1.可轻易通过拓展模块实现 2. 支持Session的直接保持 高并发导致进程数太多,性能不高 较少使用
Nginx 1.可以针对http应用做一些分流的策略 2.Nginx对网络稳定性的依赖非常小 3.可以承担高负载压力且稳定 4.Nginx可以通过端口检测到服务器内部的故障 5.拓展模块丰富,比如通过sysguard限流 1.Nginx仅能支持http、https和Email协议 2.对后端服务器的健康检查,只支持通过端口来检测,不支持通过url来检测 大部分场景
Ribbon 1.节省服务器负载安装配置成本 2.请求只与最终的服务器有关,不会有负载均衡服务器过载宕机造成服务不可用的风险 3.节省请求转发时间 1.只能用于微服务应用 2.需要客户端自身提供负载方案 微服务场景
LVS 1.抗负载能力强、是工作在网络4层之上仅作分发之用,没有流量的产生 2.应用范围比较广,因为LVS工作在4层,所以它几乎可以对所有应用做负载均衡,包括http、数据库、在线聊天室等等 1.软件本身不支持正则表达式处理,不能做动静分离 对负载均衡有比较高的要求而又不想购置硬件负载均衡器
HAproxy 1.HAProxy的优点能够补充Nginx的一些缺点,比如支持Session的保持,Cookie的引导; 2.HAProxy支持TCP协议的负载均衡转发,可以对MySQL读进行负载均衡
DNS dns系统本身是一个分布式的网络,它是相对可靠的 1.不能够按照Web服务器的处理能力分配负载 2.没有容错机制,后端服务器出现错误DNS可能仍会向其分配请求 3.DNS使用UDP协议,有网络封锁时无法保证其可用性 在不同地区设立了多个数据中心,业务已经做了分布式部署的规划,比如百度、CDN
CDN 1.提升网站加载速度 2.降低网站压力 3.提升用户体验 1.数据有缓存,不会实时更新 2.成本较高 站点或者应用中大量静态资源的加速分发

参考:

http://www.cnblogs.com/siodoon/articles/5325462.html

4.负载策略与算法

4.1 常见负载均衡策略

4.1.1轮询法

将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

加权轮询法

不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

4.1.2随机法

通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

加权随机法

与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

4.1.3源地址哈希法

源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

4.1.4最小连接数法

最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

参考:

http://blog.csdn.net/yjw757174266/article/details/51066163

4.2 策略比较
名称 优点 缺点 适用场景
轮询法 一般为服务器带上权重,针对服务器的性能差异可分配不同的负载 请求到目的结点的不确定,造成其无法适用于有写的场景(缓存,数据库写) 数据库或应用服务层中只有读的场景;
随机法 实现简单、易水平扩展 请求到目的结点的不确定,造成其无法适用于有写的场景(缓存,数据库写) 数据库负载均衡,也是只有读的场景;
源地址哈希法 相同key一定落在同一个结点上,这样就可用于有写有读的缓存场景;session问题可以得到解决 在某个结点故障后,会导致哈希键重新分布,造成命中率大幅度下降 不想进行session同步或者集中式管理
最小连接数法 充分利用服务器的资源,保证个结点上负载处理均衡 实现起来复杂,真实使用较少

参考:

https://www.cnblogs.com/data2value/p/6107653.html

5.实际问题的思考与解决方案

5.1 过载问题的处理

过载问题对于运营者来说是不希望看到的,但是有时候确实会发生,比如遭遇了DDoS或者说某个促销活动导致用户数激增。排除这些不常见的情况外,运营者得对软件或者网站的运营情况有个大致的了解,合理的预测出正常的流量范围和服务器的承受能力,以便合理的对网站进行管理配置,尽量避免过载的发生。比如,限制在线用户人数或者限制连接数,当超过限制时直接对后续请求拒绝服务或者让后续服务排队等候,等待服务器恢复。

5.1.1nginx+sysguard

如果nginx被攻击或者访问量突然变大,nginx会因为负载变高或者内存不够用导致服务器宕机,最终导致站点无法访问。今天要谈到的解决方法来自淘宝开发的模块nginx-http-sysguard,主要用于当负载和内存达到一定的阀值之时,会执行相应的动作,比如直接返回503,504或者其他的.一直等到内存或者负载回到阀值的范围内,站点恢复可用。简单的说,这几个模块是让nginx有个缓冲时间,缓缓。

sysguard只要有以下几个重要的配置参数:

  • sysguard [on | off] 开启或者关闭
  • sysguard_load load=number [action=/url] 指定负载阀值,当系统的负载超过这个值,所有的请求都会被重定向到action定义的uri请求中.如果没有定义URL action没有定义,那么服务器直接返回503
  • sysguard_mem swapratio=ratio% [action=/url] 定义交换分区使用的阀值,如果交换分区使用超过这个阀值,那么后续的请求全部被重定向到action定义的uri请求中.如果没有定义URL action没有定义,那么服务器直接返回503
  • sysguard_interval time 定义更新频率

参考:

https://www.52jbj.com/yunying/461010.html

5.1.2 HAProxy

对于HAProxy来说,比nginx更加简单,因为HAXproxy的主要功能就包括负载均衡,所以其原生就支持请求的限流,包括限制连接数、session数量、每秒钟最大请求数量等等。

maxconn <number>:设定单haproxy进程的最大并发连接数;
maxconnrate <number>:设定单haproxy进程每秒接受的连接数;
maxsslconn <number>:设定单haproxy进程的ssl连接最大并发连接数;
maxsslrate <number>:单haproxy进程的ssl连接的创建速率上限;
spread-checks <0..50, in percent>:避免对于后端检测同时并发造成
5.2 负载均衡的高可用性

对于负载均衡器而言,其作用可谓相当之重要。因为其作为客户端与服务端之间的重要连接点,一旦发生故障将直接导致服务不可用。同时,负载均衡器还得实时监听后端服务器的健康情况,以便更好的对请求进行转发。

负载均衡器通常可以和后端服务器一样进行集群,做主备集群,这样可以在很大程度上降低负载均衡器不可用的情形。这里介绍下keepalived,keepalived是集群管理中保证集群高可用的一个服务软件,其功能类似于heartbeat,用来防止单点故障。

keepalived通常有两种作用:

  1. 实现负载均衡器Master和backup的failover
  2. 检测后端web服务器的健康状态

这里介绍下几个名词:

  • failover:失败自动切换,当出现失败,重试其它服务器。
  • failfast:快速失败,只发起一次调用,失败立即报错。
  • failsafe:失败安全,出现异常时,直接忽略,通常用于写入审计日志等操作。
  • failback:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作 不可靠,重启丢失。 可用于生产环境 Registry。
  • Forking:并行调用多个服务器,只要一个成功即返回,通常用于实时性要求较高的读操作。 需要浪费更多服务资源 。

  • Broadcast :广播调用,所有提供逐个调用,任意一台报错则报错。通常用于更新提供方本地状态 速度慢,任意一台报错则报错 。

顺带回顾一下Java并发中对容器的修改:java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。当发现容器在迭代过程中被修改时,快速失败的迭代器会抛出ConcurrentModificationException异常,而fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException。

Keepalived高可用服务对之间的故障切换转移,是通过 VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议)来实现的。

在 Keepalived服务正常工作时,主 Master节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备Backup节点自己还活看,当主 Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主 Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的 IP资源及服务。而当主 Master节点恢复时,备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。

下面就是nginx+keepadlived+tomcat主备架构图

img

这里需要注意下的是,使用keepalived也会和zookeeper一样,可能会产生脑裂。当联系2个节点的“心跳线”断开时,本来为一整体、动作协调的HA系统,就分裂成为2个独立的个体。由于相互失去了联系,都以为是对方出了故障。两个节点上的HA软件像“裂脑人”一样,争抢“共享资源”、争起“应用服务”,就会发生严重后果——或者共享资源被瓜分、2边“服务”都起不来了;或者2边“服务”都起来了,但同时读写“共享存储”,导致数据损坏(常见如数据库轮询着的联机日志出错)。

参考:

http://www.cnblogs.com/jony413/articles/2697384.html

https://www.cnblogs.com/clsn/p/8052649.html

https://www.jianshu.com/p/bc34f9101c5e

5.3 负载服务器如何知晓后台服务器的服务情况

首先默认情况下nginx、HAProxy是支持对后端服务器的健康情况进行检查的,但是LVS默认是不支持的。下面我们将一一介绍:

5.3.1 nginx后端服务器健康检查

1.nginx可以通过默认自带的 ngx_http_proxy_module 模块 和ngx_http_upstream_module模块中的相关指令来完成当后端节点出现故障时,自动切换到健康节点来提供访问。

这里列出这两个模块中相关的指令:

ngx_http_proxy_module 模块中的 proxy_connect_timeout 指令、 proxy_read_timeout指令和roxy_next_upstream指令;

ngx_http_upstream_module模块中的server指令。

2.除了自带的上述模块,还有一个更专业的模块,来专门提供负载均衡器内节点的健康检查的。这个就是淘宝技术团队开发的 nginx 模块 nginx_upstream_check_module,通过它可以用来检测后端 realserver 的健康状态。如果后端 realserver 不可用,则所以的请求就不会转发到该节点上。

在nginx.conf配置文件里面的upstream加入健康检查,如下:

upstream name {
       server 192.168.0.21:80;
       server 192.168.0.22:80;
       check interval=3000 rise=2 fall=5 timeout=1000 type=http;

}

5.3.2 HAProxy后端服务器健康检查

1、通过监听端口进行健康检测 。这种检测方式,haproxy只会去检查后端server的端口,并不能保证服务的真正可用。

配置示例:

listen http_proxy 0.0.0.0:80
        mode http
        cookie SERVERID
        balance roundrobin
        option httpchk
        server web1 192.168.1.1:80 cookie server01 check
        server web2 192.168.1.2:80 cookie server02 check inter 500 rise 1 fall 2

2、通过URI获取进行健康检测 。检测方式,是用过去GET后端server的的web页面,基本上可以代表后端服务的可用性。

配置示例:

listen http_proxy 0.0.0.0:80
        mode http
        cookie SERVERID
        balance roundrobin
        option httpchk GET /index.html
        server web1 192.168.1.1:80 cookie server01 check
        server web2 192.168.1.2:80 cookie server02 check inter 500 rise 1 fall 2

3、通过request获取的头部信息进行匹配进行健康检测 。这种检测方式,则是基于高级,精细的一些监测需求。通过对后端服务访问的头部信息进行匹配检测。

配置示例:

listen http_proxy 0.0.0.0:80
        mode http
        cookie SERVERID
        balance roundrobin
        option httpchk HEAD /index.jsp HTTP/1.1\r\nHost:\ www.xxx.com
        server web1 192.168.1.1:80 cookie server01 check
        server web2 192.168.1.2:80 cookie server02 check inter 500 rise 1 fall 2

5.3.3 keepalived

keepalived具有很强大、灵活的后端检测方式,其具有HTTP_GET、SSL_GET、TCP_CHECK、SMTP_CHECK、MISC_CHECK 几种健康检测方式。

  1. HTTP_GET:基于返回值的状态码
  2. SSL_GET:基于返回页面的hash值
  3. TCP_CHECK:基于TCP协议三次握手,看能否正常建立连接
  4. SMTP_CHECK:基于邮件协议SMTP
  5. MISC_CHECK:通过调用外部配置名脚本进行检测确认后端主机是否正常

注:LVS不能直接检查后端的RS的健康状态,但是可以借助keepalived

参考:

http://blog.csdn.net/moqiang02/article/details/42846221

https://www.cnblogs.com/breezey/p/4680418.html

https://www.cnblogs.com/clsn/p/8052649.html

http://blog.csdn.net/wtswjtu/article/details/53008348

http://www.361way.com/keepalived-health-check/5218.html

https://www.linuxidc.com/Linux/2017-01/139365.htm

5.4 集群环境与分布式环境中的负载均衡

先来理解下几个概念:

集群:将一套系统或者一个业务服务部署在不同的机器上一起对外提供访问

分布式:将一套系统中的不同业务部署在不同的机器上,当前业务不依赖于其他业务,可独自对外提供访问

微服务:将分布式中的业务进一步拆分成小的逻辑单元,逻辑单元依赖于其他逻辑单元,合起来才能对外提供一个完整的业务服务

先抛开微服务不谈,从上面的理解我们可以看到,通常在集群情况下才需要负载均衡,如果单独的一个服务器肯定没必要了吧。当然,分布式里面也是可以包含集群的,比如将某一个繁重的业务进行集群,那么对这个业务就需要进行负载,再加上其他服务器上的其他业务,就可以算分布式了。

5.5 传统应用与微服务中的负载均衡

5.5.1 传统应用

在传统应用中,我们是通过session来进行登录校验和权限验证。但是session是保存在服务器的内存中,当对服务器进行集群之后,同一个用户的请求每次可能会被分发到不同的服务器上,这是就需要对session进行管理,不然可能的情况就是,我们每次请求都得重新登录。

session处理方案主要有三种:

1.粘性session

在没有负载均衡设备之前,因为我们只有一台应用服务器,我们的每次请求都是落在同一个服务器上,session也是都在这个服务器,我们的每一次会话请求都在这一个服务器上进行处理。

沿用这个简单思路,如果我们在负载均衡设备上,设置这样的规章:让每次请求响应均被负载均衡设备发送到相同的服务器进行处理,这样,我们的session就能得到保证了。前面我们也提到过相应的算法——源地址哈希法。这样就可以保证每个人的请求会落在相同服务器上。

2.session同步

这种方式就是说每次有一个用户登录完以后,就把session同步到其他的服务器,这样无论以后请求被分发到哪一台服务器,都存在用户的session数据。

3.session统一管理

我们把session单独放到一个地方存储起来,可以是单独数据库,或其他分布式存储系统,前面负载均衡器还是无状态转发,应用服务器每次读写session,都到相同的地方来取。比如说我们可以用redis、memcache。

方案有多种,我们还是来比较以下各自的优缺点:

优点 缺点
粘性 配置简单 1.一旦后台服务器宕机,用户需要重新登录 2.对于经常换IP的用户不太友好
同步 tomcat原生支持 1.网络开销大 2.每一台服务器都保证整个集群的session数据,浪费内存
统一管理 便于管理 1.存取都得经过序列化与反序列化 2.得通过网络、IO,没有内存速度快

5.5.2 微服务

微服务通常基于云平台的,不采用负载均衡的原因有:

  1. 单点故障。如果负载均衡器挂了,所有服务都不能被访问。就算负载均衡器是高可用的,它也会成为整个应用的瓶颈。
  2. 限制了水平扩展。单节点的负载均衡器能力是有限的。负载均衡器有两点制约: 冗余模型和许可证费用。大部分的负载均衡器采用热交换的冗余模型,只有一台服务器处理负载,另一台只有在主负载均衡器失效的情况下才工作。另外商用负载均衡器也受许可证费用的制约。
  3. 静态化管理。大多数传统的负载均衡器使用一个中央数据库保存路由信息,只有通过提供商特殊的API才能添加新的路由规则。不适合快速注册和快速取消注册。
  4. 复杂。负载均衡器作为服务的代理,需要将客户发来的请求映射到具体的服务。这需要在负载均衡器上手工定义服务的映射规则。如果有新的服务提供者加入,也需要手工修改负载均衡器添加映射规则。

小规模的情况下,可以使用负载均衡器。另外,负载均衡器还可以集中处理SSL协议。

但是在云平台上,需要处理大量的交易,一个集中式的网络基础设施不能有效的扩展,所以就不合适了。

在基于云的微服务环境中,采用服务发现机制:

  1. 高可用, 基于“服务发现集群”
  2. 点对点。服务发现集群下的每个节点都共享服务实例的状态。
  3. 负载均衡。服务发现也需要负载均衡。采用客户端负载均衡。
  4. 弹性。客户端会缓存服务的信息。如果“服务发现”失效,客户端可以使用本地缓存的信息找到服务提供者。
  5. 容错性。当“服务发现”侦测到某一个服务不可用时,可以将他从可用服务列表删除。可以自动检测到服务故障,无需人工参与。

我们由此可以推测Ribbon的设计理念应该是为了:1.解决服务端负载均衡延时、瓶颈效应;2.解决服务器负载可能导致的服务不可达风险;3.与Eureka结合,在本地缓存服务器数据,并定时剔除不可达应用,保证服务调用的高可用性。

参考:

https://mp.weixin.qq.com/s/x0CZpovseOuofTA_lw0HvA

http://blog.csdn.net/hxg117/article/details/77529996

5.6 不同类型应用负载均衡的应用

说到应用,那肯定得联系实际,之前我们可以不加任何限制的单纯考虑负载均衡,但是到了实际环境,我们的一切活动都得围绕我们的最终目的展开——系统的健壮可用性。想要达到目的,方案可以有很多种,但是最优的方案并不多。

这里列举下设计过程中需要考虑到的一些因素,性能与可伸缩性、可用性、可扩展性、成本、当然还有软件配置的复杂性等等。

我们可以先来看看常用的一些负载均衡架构方案,从中学习学习:

动静分离。可以需要开发人员配合(把静态资源放独立站点下),也可以不需要开发人员配合(利用7层反向代理来处理,根据后缀名等信息来判断资源类型)。有了单独的静态文件服务器之后,存储也是个问题,也需要扩展。多台服务器的文件怎么保持一致,买不起共享存储怎么办?分布式文件系统也派上用场了。

img

还有一项目前国内外用的非常普遍的技术CDN加速。目前该领域竞争激烈,也已经比较便宜了。国内南北互联网问题比较严重,使用CDN可以有效解决这个问题。CDN的基本原理并不复杂,可以理解为智能DNS+Squid反向代理缓存 ,然后需要有很多机房节点提供访问。

img

这里我们得重点关注下CDN,因为我们经常在无形之中就使用到了,比如开发vue应用,会引入其js文件,官网上推荐的方式就是通过CDN的方式去取的,还有我们购物使用的淘宝的图片、直播的视频流等等。

参考:

http://geek.csdn.net/news/detail/208040

http://blog.csdn.net/truelove12358/article/details/49685903

5.7 负载均衡协作使用

强强结合,这是我们很喜欢的一个词,这个也可以用在负载均衡之中,用来确保负载均衡器的可靠性,这里仅作为一种思路提供。

5.7.1 客户端+服务端

如果单单从名称上看,可能觉得客户端服务端结合的方式应该很好,这样两头都有负载均衡了。但是深入考虑下,设计客户端的原因就是想摆脱传统服务端的负载带来的束缚,比如响应延迟,负载会成为服务调用瓶颈等。每个客户端都会保存一份完整的后端服务器的信息,这样可以保证客户端与服务器端之间的直连,这就完全规避了因为服务端负载均衡器不可用导致的后端服务也不可用的问题,而且客户端会定时剔除不可达的应用,保证请求的有效性。

5.7.2 服务端+服务端

nginx /Haproxy作为lvs的节点。

5.7.3 软件+硬件

array+nginx/Haproxy。

总的来说,上面的方案也是在网络上有人所提供出来的,单单从技术上实现起来是没有问题的,但是实际上是否真的有必要就值得考证,上面的方案总给人一种画蛇添足的感觉,因为做多次负载一方面会加重请求的延迟,另外一方面也加重了配置的复杂性。所以如果想提高负载均衡的能力,还不如单纯使用某一种负载均衡来的痛快。

5.8 内网系统与公网系统负载均衡异同

内网系统就是内部网络部署的系统,公网系统就是在公网部署的系统,本质上来说这两者对负载均衡设计本身并无影响,上面说的所有方案在两种情况下都可以使用。但是二者还是有些许差别:

首先想一下访问公网的情况,这种情况下无论是上述哪种负载均衡,都是可以访问负载均衡访问的到的。为了安全,公网系统可能会仅仅将负载均衡器暴露在公网之中,然后RS隐藏在内网之中,有没有很熟悉的感觉?对,很快就会想到VS/NAT,这就好比局域网内部机器通过路由器NAT协议访问公网。

内网系统访问一般不会互通,得打破网络隔离,简单一点的就是两个局域网内所有机器可以相互访问,安全点的仅可访问另一局域网的部分机器,比如只可以访问另一内网的负载服务器,而且对于内网系统来讲,访问量用户数量往往没有不大,用nginx做个负载通常就可以了,当然实力雄厚的也可以选择F5等硬件设备。

发表评论

电子邮件地址不会被公开。 必填项已用*标注