乐趣区

关于java:高并发系统设计之负载均衡

本文已收录至 GitHub,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

原创不易,重视版权。转载请注明原作者和原文链接

在咱们日常生活中,尤其是在拥挤的公共场所,咱们会看到很多排队等待的状况 —— 无论是在票房购票,超市结账,还是在银行期待服务。而为了防止让人们因过长的队伍和等待时间而感到焦躁,管理者往往会采取一种策略:开设更多的窗口或者柜台,将期待的人们平均地散布到各个地位去,这就是咱们生存中的「负载平衡」。

说回到计算机科学的世界里,负载平衡这个概念也施展着相似的作用。它就像是网络世界的向导,疏导来自用户的申请,确保每个服务器不会因为过多的申请而过载。

通过负载平衡,咱们能进步零碎的可用性,晋升响应速度,同时也能避免任何繁多的资源适度应用。总的来说,好的负载平衡让整个零碎运行得更加安稳,效率更高,就像是一个良好运行的机器,每个整机都在承当适宜本人的工作量。

当咱们的利用单实例不能撑持用户申请时,此时就须要扩容,从一台服务器扩容到两台、几十台、几百台。此时咱们就须要负载平衡,进行流量的转发。

本篇文章介绍几种罕用的负载平衡计划,心愿对大家可能有所启发。

DNS 负载平衡

一种是应用 DNS 负载平衡,行将域名映射多个 IP。

DNS 负载平衡是一种应用 DNS(域名零碎)来扩散达到特定网站的流量的办法。

基本上,它是通过将一个域名解析到多个 IP 地址来实现的。当用户试图接入这个域名时,DNS 服务器会依据肯定的策略抉择一个 IP 地址返回给用户,以此来实现网络流量的平衡调配。

举个例子来阐明:

假如你是一个大型电子商务网站的管理员,你的网站叫做 www.myshop.com。因为你的业务正在快速增长,每天有数百万的用户拜访你的网站进行购物。如果所有的流量都集中在一台服务器上,那么可能会导致服务器过载,从而升高网站的性能甚至使其宕机。

为了解决这个问题,你决定采纳 DNS 负载平衡。你将运行网站的任务分配给三台不同的服务器(服务器 A,服务器 B,服务器 C)。而后你设置你的 DNS 记录,以便当用户输出 www.myshop.com 时,他们能够被路由到任何一台服务器上。

例如,第一个用户可能被路由到服务器 A,下一个用户可能被路由到服务器 B,第三个用户可能被路由到服务器 C,而后反复这个模式。这样,你就把流量平均分配到了所有的服务器上,从而加重了每台服务器的负载,并进步了网站的总体性能和可靠性。

DNS 负载平衡蕴含多种策略:

  • 轮询(Round Robin):轮询是一种最简略的办法,它将申请按程序调配到服务器上。例如,如果你有三个服务器 A,B 和 C,那么第一个申请会发送到 A,第二个发送到 B,第三个发送到 C,而后再从 A 开始。
  • 加权轮询(Weighted Round Robin):这是轮询的加强版本,在此策略中,每个服务器都调配有一个权重,权重较大的服务器接管更多的申请。
  • 起码连贯(Least Connections):在此策略中,新的申请会被发送到以后领有起码沉闷连贯的服务器。
  • 源地址哈希(IP Hash):依据源 IP 地址确定申请的服务器,能够保障同一用户的申请总是拜访同一个服务器。
  • 响应工夫:依据服务器的响应工夫来调配申请,响应工夫短的服务器会接管到更多的申请。

能够依据理论的场景须要,抉择最合适的负载平衡策略。

然而 DNS 负载平衡存在一些问题,DNS 负载平衡最大的问题在于它「无奈实时地响应后端服务器的状态变动」。

如果一个服务器忽然宕机,DNS 负载平衡可能依然会将申请发送到这个曾经宕机的服务器上,直至 DNS 记录刷新,这可能导致用户体验降落和服务中断。

举个例子:

DNS 缓存了域名和 IP 的映射关系,假如我 A 服务器呈现了故障须要下线,即便批改了缓存记录,要使其失效也须要较长的工夫,这段时间,DNS 依然会将域名解析到已下线的 A 服务器上,最终导致用户拜访失败,影响用户体验。

对于 DNS 缓存多久工夫失效,能够参考阿里云的帮忙文档:https://help.aliyun.com/document_detail/39837.html。

总结一下 DNS 负载平衡的优缺点:

  • 长处:配置简略,将负载平衡的工作交给了 DNS 服务器,省去了治理的麻烦。
  • 毛病:DNS 会有肯定的缓存工夫,故障后切换工夫长。

Nginx 负载平衡

Nginx 是一种高效的 Web 服务器 / 反向代理服务器,它也能够作为一个负载均衡器应用。在负载平衡配置中,Nginx 能够将接管到的申请散发到多个后端服务器上,从而进步响应速度和零碎的可靠性。Nginx 是负载平衡比拟罕用的计划。

负载平衡算法

Nginx 负载平衡是通过「upstream」模块来实现的,内置实现了三种负载策略,配置还是比较简单的。

  • 轮循(默认):Nginx 依据申请次数,将每个申请平均调配到每台服务器。
  • 起码连贯:将申请调配给连接数起码的服务器。Nginx 会统计哪些服务器的连接数起码。
  • IP Hash:每个申请按拜访 IP 的 hash 后果调配,这样每个访客固定拜访一个后端服务器,能够解决 session 共享的问题。
  • fair(第三方模块):依据服务器的响应工夫来调配申请,响应工夫短的优先调配,即负载压力小的优先会调配。须要装置「nginx-upstream-fair」模块。
  • url_hash(第三方模块):按拜访的 URL 的哈希后果来调配申请,使每个 URL 定向到一台后端服务器,如果须要这种调度算法,则须要装置「nginx_upstream_hash」模块。
  • 一致性哈希(第三方模块):如果须要应用一致性哈希,则须要装置「ngx_http_consistent_hash」模块。

负载平衡配置

Nginx 负载平衡配置示例如下:

http {
    upstream myserve {
        server 192.168.0.100:8080 weight=1 max_fails=2 fail_timeout=10;
        server 192.168.0.101:8080 weight=2;
        server 192.168.0.102:8080 weight=3;
      # server 192.168.0.102:8080 backup; 
      # server 192.168.0.102:8080 down;
      # server 192.168.0.102:8080 max_conns=100;
    }
    
    server {
        listen 80;
        location / {proxy_pass http://myserve;}
    }
}
  • weight:weight 是权重的意思,上例配置,示意 6 次申请中,调配 1 次,2 次和 3 次。
  • max_fails:容许申请失败的次数,默认为 1。超过 max_fails 后,在 fail_timeout 工夫内,新的申请将不会调配给这台机器。
  • fail_timeout:默认为 10 秒,上诉代码配置示意失败 2 次之后,10 秒内 192.168.0.100:8080 不会解决新的申请。
  • backup:备份机,所有服务器挂了之后才会失效,如配置文件正文局部,只有 192.168.0.100 和 192.168.0.101 都挂了,才会启用 192.168.0.102。
  • down:示意某一台服务器不可用,不会将申请调配到这台服务器上,该状态的应用场景是某台服务器须要停机保护时设置为 down,或者公布新性能时。
  • max_conns:限度调配给某台服务器解决的最大连贯数量,超过这个数量,将不会调配新的连贯给它。默认是 0,示意不限度最大连贯。它所起到的作用是避免服务器因连贯过多而导致宕机,限度同时解决的最大连贯数量。

超时配置

  • proxy_connect_timeout:后端服务器连贯的超时工夫,默认是 60 秒。
  • proxy_read_timeout:连贯胜利后等待后端服务器响应工夫,也能够说是后端服务器解决申请的工夫,默认是 60 秒。
  • proxy_send_timeout:发送超时工夫,默认是 60 秒。

被动健康检查与被动健康检查

Nginx 负载平衡有个毛病,Nginx 的服务查看是惰性的,Nginx 只有当有拜访时后,才发动对后端节点探测。

如果本次申请中,节点正好呈现故障,Nginx 仍然将申请转交给故障的节点,而后再转交给衰弱的节点解决。所以不会影响到这次申请的失常进行。然而会影响效率,因为多了一次转发,而且自带模块无奈做到预警。

也就是说 Nginx 自带的健康检查是被动的。

如果咱们想被动的去进行健康检查,能够应用淘宝开源的第三方模块:「nginx_upstream_check_module」。

加载了这个模块后,Nginx 会定时被动地去 ping 后端的服务列表,当发现某服务出现异常时,把该服务从衰弱列表中移除,当发现某服务复原时,又可能将该服务加回衰弱列表中。

示例配置如下:

upstream myserver {    
        server 127.0.0.1:8080;
        server 127.0.0.2:8080;
        check interval=5000 rise=2 fall=5 timeout=1000 type=http;    
        check_http_send"HEAD / HTTP/1.0\r\n\r\n";   check_http_expect_alive http_2xx http_3xx;
    }

解释一下:

  • upstream myserver: 定义一个上游服务器组,名为 ”myserver”。
  • server 127.0.0.1:8080; server 127.0.0.2:8080;:定义两台上游服务器的 IP 地址和端口号,服务器地址别离为 127.0.0.1 和 127.0.0.2,都在 8080 端口运行。
  • check interval=5000 rise=2 fall=5 timeout=1000 type=http;:配置健康检查参数。每隔 5000 毫秒(5 秒)进行一次健康检查,如果间断 2 次健康检查通过,则将该服务器标记为可用;如果间断 5 次健康检查失败,则将该服务器标记为不可用。每次健康检查的超时工夫为 1000 毫秒(1 秒),健康检查的形式为 http 申请。
  • check_http_send"HEAD / HTTP/1.0\r\n\r\n"; check_http_expect_alive http_2xx http_3xx;:对上游服务器执行的健康检查的具体细节。发送一个 HTTP HEAD 申请到服务器,而后期待响应状态码为 2xx 或 3xx,如果失去这些响应,则认为服务器是衰弱的。

LVS/F5+Nginx

Nginx 个别用于七层负载平衡,其吞吐量是有肯定限度的,如果网站的申请量十分高,还是存在性能问题。

为了晋升整体吞吐量,会在 DNS 和 Nginx 之间引入接入层,如应用 LVS(软件负载均衡器)、F5(硬负载均衡器)能够做四层负载平衡,即首先 DNS 解析到 LVS/F5,而后 LVS/F5 转发给 Nginx,再由 Nginx 转发给后端实在服务器。

比拟现实的架构是这样的:

不过能上这种架构的都是超高流量了,在国内也得是大厂级别。Nginx 目前提供了 HTTP (ngx_http_upstream_module)七层负载平衡,而 1.9.0 版本也开始反对 TCP(ngx_stream_upstream_module)四层负载平衡。一般利用个别咱们一个 Nginx 间接就能够搞定

对于个别业务开发人员来说,咱们只须要关怀到 Nginx 层面就够了,LVS/F5 个别由零碎 / 运维工程师来保护。

个别能上 F5 的状况不多见,绝大部分 LVS+Nginx 就能够搞定

另外我抱着好奇心去谷歌了下 F5 设施的价格

好家伙,这玩意要几十万一台,原来不是玩不起,而是没这个实力啊。

利用级负载平衡

下面咱们说的都是零碎级的负载平衡,上面来谈谈利用级别的负载平衡,利用级别的负载平衡大都是一些框架自带的。

介绍两个具备代表性的:Ribbon 和 Dubbo。

Ribbon 负载平衡

首先,确保你的我的项目中增加了 Spring Cloud Starter Netflix Ribbon 的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

而后,在你的 Spring Boot 利用中应用 @LoadBalanced 注解来开启 Ribbon 的负载平衡性能。

例如,上面的代码示例创立一个能够执行负载平衡的 RestTemplate 实例:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;

@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);
  }

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

接着,你能够在须要的中央通过 RestTemplate 调用其余服务,例如:

@RestController
public class TestController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/test")
    public String test() {
        // "service-name" 是你须要调用的服务名称
        String result = this.restTemplate.getForObject("http://service-name/test", String.class);
        return "Return :" + result;
    }
}

在这个例子中,「service-name」为你心愿调用服务的名称。Ribbon 会主动解决服务发现并对申请进行负载平衡。

在 Ribbon 中,有以下几种常见的负载平衡策略:

  1. 轮询(Round Robin):按程序循环,如果服务器 A、B、C,那么第一次申请发送到服务器 A,第二次申请发送到服务器 B,第三次申请发送到服务器 C,而后再回到服务器 A。
  2. 随机(Random Rule):依据产生的随机数抉择服务器,随机数生成的范畴就是服务列表的大小。
  3. 重试(Retry Rule):在一个配置时间段内当抉择服务失败,则进行重试。
  4. 起码并发调用数(Best Available Rule):抉择并发最小的服务。
  5. 响应工夫加权(Response Time Weighted Rule):依据均匀响应工夫计算所有服务的权值,越小的响应工夫权值越大,被选中的可能性越高。刚启动时如果统计信息有余,则应用 Round Robin 策略。
  6. 区域亲和性(Zone Avoidance Rule):复合判断 server 所在区域的性能和 server 的可用性抉择服务器。

自定义配置负载平衡

在 Ribbon 中,你能够自定义你的负载平衡策略。以下是进行自定义的根本步骤:

首先,你须要创立一个实现了 com.netflix.loadbalancer.IRule 接口的类。这个接口有一个次要的办法choose(Object key),你应该在这个办法中编写你的负载平衡逻辑。

public class MyCustomRule implements IRule {

    private ILoadBalancer lb;

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {this.lb = lb;}

    @Override
    public ILoadBalancer getLoadBalancer() {return lb;}

    @Override
    public Server choose(Object key) {
        // 在这里实现你的负载平衡逻辑
        // 返回你抉择的服务器
    }
}

之后,你须要在你的 Ribbon Client 配置中应用新的规定。例如,如果你正在应用 Spring Cloud,而后你能够在你的配置文件中增加:

serviceId:
  ribbon:
    NFLoadBalancerRuleClassName: com.example.MyCustomRule

其中 serviceId 是你的服务 ID,com.example.MyCustomRule是你的自定义规定的全类名。

留神:IRule只是 Ribbon 中用于负载平衡的一个组件。如果你须要更简单的性能,可能还须要查看其它的接口,如 IPingServerListFilter

Dubbo 负载平衡

在 Spring Boot 中应用 Dubbo 进行负载平衡大抵须要以下几个步骤:

增加依赖到你的 pom.xml 文件,也就是 Spring Boot 我的项目的配置文件。

<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.8</version>
</dependency>

在 Spring Boot 的 application.properties 配置文件中设置 Dubbo 的相干参数,包含提供者和消费者的地址、接口版本等。

dubbo.application.name=consumer-of-helloworld-app
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.consumer.check=false

建服务接口。Dubbo 是一个基于接口的 RPC 框架,所以你须要创立一个服务接口。

public interface GreetingsService {String sayHi(String name);
}

应用 @DubboReference 注解来援用近程服务。并且能够通过 loadbalance 属性设置负载平衡策略(例如 roundrobin、random、leastactive 等)。

@RestController
public class GreetingsController {@DubboReference(loadbalance="roundrobin")
    private GreetingsService greetingsService;

    @GetMapping("/greet")
    public String greet(String name) {return greetingsService.sayHi(name);
    }
}

这里的 GreetingService 是一个近程的 Dubbo 服务接口,Spring Boot 利用作为消费者会调用这个服务。

留神:在理论环境中,你须要正确配置 Zookeeper 地址,服务提供者和消费者的地址等信息。

上述代码中的负载平衡策略设定为「roundrobin」,即轮询形式。当然,Dubbo 还反对其余负载平衡策略,如随机 (random)、最小沉闷数(leastactive) 等。

Dubbo 提供了多种负载平衡策略,这些策略能够在服务消费者端进行配置。常见的负载平衡策略有:

  • Random:随机抉择调用服务。
  • RoundRobin:轮询调用服务。
  • LeastActive:起码沉闷调用数,即优先调用服务的响应工夫短且正在解决的申请数量少的服务。
  • ConsistentHash:一致性哈希。

要扭转默认的负载平衡策略,你能够在「dubbo:reference」或「dubbo:service」标签中设置 loadbalance 属性为你想要的策略名称。

例如,如果你想应用 LeastActive 策略,你的配置可能会像这样:

<dubbo:reference id="demoService" interface="com.example.DemoService" loadbalance="leastactive" />

具体应用哪种负载平衡策略须要依据理论的服务状况和需要进行抉择和配置。

自定义负载平衡

Dubbo 同样也反对自定义负载平衡策略。你能够实现 org.apache.dubbo.rpc.cluster.LoadBalance 接口并将其注册到 ExtensionLoader 中,以创立本人的负载平衡策略。

在服务援用时,你能够通过 @Reference(loadbalance = "myLoadBalance") 注解指定应用你的负载平衡策略。

上面是一个简略的示例:

首先,你须要创立一个类实现 LoadBalance 接口。例如,假如你想要创立一个随机抉择提供者的负载均衡器:

package com.example;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.cluster.LoadBalance;

import java.util.List;
import java.util.Random;

public class CustomLoadBalance implements LoadBalance {private final Random random = new Random();

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {int size = invokers.size();
        return invokers.get(random.nextInt(size));
    }
}

而后,在 Dubbo 配置文件中应用全限定类名来应用你的自定义负载平衡策略:

<dubbo:reference id="xxxService" interface="com.example.XxxService" loadbalance="com.example.CustomLoadBalance"/>

或者,你能够在 @Reference 注解中指定它:

@Reference(loadbalance = "com.example.CustomLoadBalance")
private XxxService xxxService;

以上代码仅作为根本示例,你能够依据你的具体需要批改和扩大这个代码。

总结

总的来说,负载平衡技术在确保网络或零碎稳固运行中起着无足轻重的作用。

它通过扩散申请流量,不仅进步了服务的可用性和冗余,还优化了用户体验。然而,技术始终在变动,咱们应继续钻研和把握新的负载平衡策略,以满足将来更大的规模和更简单的需要。不论是云计算、微服务架构还是边缘计算,负载平衡都将继续施展其至关重要的作用。

本篇是高并发零碎设计三部曲中的负载平衡,下篇会跟大伙聊聊「限流」,心愿本文可能给你带来播种和思考,下篇再见。


感激浏览,如果本篇文章有任何谬误和倡议,欢送给我留言斧正。

老铁们,关注我的微信公众号「Java 随想录」,专一分享 Java 技术干货,文章继续更新,能够关注公众号第一工夫浏览。

一起交流学习,期待与你共同进步!

退出移动版