高并发系统设计之负载均衡
本文已收录至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 中,有以下几种常见的负载均衡策略:
- 轮询(Round Robin):按顺序循环,如果服务器 A、B、C,那么第一次请求发送到服务器 A,第二次请求发送到服务器 B,第三次请求发送到服务器 C,然后再回到服务器 A。
- 随机(Random Rule):根据产生的随机数选择服务器,随机数生成的范围就是服务列表的大小。
- 重试(Retry Rule):在一个配置时间段内当选择服务失败,则进行重试。
- 最少并发调用数(Best Available Rule):选择并发最小的服务。
- 响应时间加权(Response Time Weighted Rule):根据平均响应时间计算所有服务的权值,越小的响应时间权值越大,被选中的可能性越高。刚启动时如果统计信息不足,则使用 Round Robin 策略。
- 区域亲和性(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中用于负载均衡的一个组件。如果你需要更复杂的功能,可能还需要查看其它的接口,如IPing
和ServerListFilter
。
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技术干货,文章持续更新,可以关注公众号第一时间阅读。
一起交流学习,期待与你共同进步!