写点什么

高并发系统设计之负载均衡

作者:码农BookSea
  • 2023-09-04
    浙江
  • 本文字数:7428 字

    阅读完需:约 24 分钟

高并发系统设计之负载均衡

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


说回到计算机科学的世界里,负载均衡这个概念也发挥着类似的作用。它就像是网络世界的导游,引导来自用户的请求,确保每个服务器不会因为过多的请求而过载。


通过负载均衡,我们能提高系统的可用性,提升响应速度,同时也能防止任何单一的资源过度使用。总的来说,好的负载均衡让整个系统运行得更加平稳,效率更高,就像是一个良好运转的机器,每个零件都在承担适合自己的工作量。


当我们的应用单实例不能支撑用户请求时,此时就需要扩容,从一台服务器扩容到两台、几十台、几百台。此时我们就需要负载均衡,进行流量的转发。


本篇文章介绍几种常用的负载均衡方案,希望对大家能够有所启发。

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;
@SpringBootApplicationpublic class Application {
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }}
复制代码


接着,你可以在需要的地方通过 RestTemplate 调用其他服务,例如:


@RestControllerpublic 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-appdubbo.registry.address=zookeeper://127.0.0.1:2181dubbo.consumer.check=false
复制代码


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


public interface GreetingsService {    String sayHi(String name);}
复制代码


使用@DubboReference注解来引用远程服务。并且可以通过loadbalance属性设置负载均衡策略(例如 roundrobin、random、leastactive 等)。


@RestControllerpublic 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;
复制代码


以上代码仅作为基本示例,你可以根据你的具体需求修改和扩展这个代码。

总结

总的来说,负载均衡技术在确保网络或系统稳定运行中起着举足轻重的作用。


它通过分散请求流量,不仅提高了服务的可用性和冗余,还优化了用户体验。然而,技术始终在变化,我们应持续研究和掌握新的负载均衡策略,以满足未来更大的规模和更复杂的需求。不论是云计算、微服务架构还是边缘计算,负载均衡都将持续发挥其至关重要的作用。


本篇是高并发系统设计三部曲中的负载均衡,下篇会跟大伙聊聊「限流」,希望本文能够给你带来收获和思考,下篇再见。

发布于: 刚刚阅读数: 4
用户头像

码农BookSea

关注

Java开发工程师 2021-12-26 加入

Java开发菜鸟工程师,写博客的初衷是为了沉淀我所学习,累积我所见闻,分享我所体验。希望和更多的人交流学习。

评论

发布
暂无评论
高并发系统设计之负载均衡_Java_码农BookSea_InfoQ写作社区