负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件,目标是尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。
负载均衡的主要作用如下:
高并发:负载均衡通过算法调整负载,尽力均匀的分配应用集群中各节点的工作量,以此提高应用集群的并发处理能力(吞吐量)。
伸缩性:添加或减少服务器数量,然后由负载均衡进行分发控制,这使得应用集群具备伸缩性。
高可用:负载均衡器可以监控候选服务器,当服务器不可用时,自动跳过,将请求分发给可用的服务器,这使得应用集群具备高可用的特性。
安全防护:有些负载均衡软件或硬件提供了安全性功能,如:黑白名单处理、防火墙,防 DDos 攻击等。
目前支持负载均衡的技术很多,我们可以通过不同维度去进行分类。
从支持负载均衡的载体来看,可以将负载均衡分为两类:硬件负载均衡、软件负载均衡
硬件负载均衡一般是在定制处理器上运行的独立负载均衡服务器,价格昂贵。硬件负载均衡的主流产品有:F5 和 A10。
优点:
功能强大:支持全局负载均衡并提供较全面的、复杂的负载均衡算法。
性能强悍:硬件负载均衡由于是在专用处理器上运行,因此吞吐量大,可支持单机百万以上的并发。
安全性高:往往具备防火墙,防 DDos 攻击等安全功能。
缺点:
成本昂贵:购买和维护硬件负载均衡的成本都很高。
扩展性差:当访问量突增时,超过限度不能动态扩容。
软件负载均衡,应用最广泛,无论大公司还是小公司都会使用。软件负载均衡从软件层面实现负载均衡,一般可以在任何标准物理设备上运行。软件负载均衡的主流产品有:Nginx、HAProxy、LVS。
其中,LVS 可以作为四层负载均衡器,其负载均衡的性能要优于 Nginx;HAProxy 可以作为 HTTP 和 TCP 负载均衡器;Nginx、HAProxy 均可作为四层或七层负载均衡器;
优点:
扩展性好:适应动态变化,可以通过添加软件负载均衡实例,动态扩展到超出初始容量的能力。
成本低廉:软件负载均衡可以在任何标准物理设备上运行,降低了购买和运维的成本。
缺点:
性能略差:相比于硬件负载均衡,软件负载均衡的性能要略低一些。
软件负载均衡从通信层面来看,又可以分为四层和七层负载均衡。
七层负载均衡就是可以根据访问用户的 HTTP 请求头、URL 信息将请求转发到特定的主机。实现方式有:DNS 重定向、HTTP 重定向和反向代理。
四层负载均衡基于 IP 地址和端口进行请求的转发。实现方式有:修改 IP 地址和修改 MAC 地址。
Zookeeper 可以实现负载均衡的方式有多种,以下是一种常见的实现方式:
(1)注册服务:每个服务实例在启动时向 Zookeeper 注册自己的信息,包括服务地址、端口等。注意:这里使用 ZooKeeper 的临时节点来存储服务器信息,当客户端连接断开后,临时节点将自动被删除。
(2)服务发现:客户端向 Zookeeper 查询可用的服务实例列表,并选择其中一个进行访问。
(3)监听节点变化:客户端可以注册监听器,当服务实例列表发生变化时,Zookeeper 会通知客户端,客户端可以及时更新可用的服务实例列表。
(4)负载均衡策略:客户端根据负载均衡策略选择一个合适的服务实例进行访问,常见的负载均衡策略有轮询、随机、权重等等。
我们通过以上步骤,Zookeeper 可以实现服务的注册、发现和负载均衡,从而帮助客户端实现对服务实例的动态调度和负载均衡。
下面将通过 ZooKeeper 的 Java API 简单实现一个负载均衡策略,允许客户端注册服务,拉取服务列表,服务发生变化时通知客户端。完整代码如下:
package com.hxstrive.zookeeper.load_balance; import com.alibaba.fastjson.JSONObject; import org.apache.zookeeper.*; import org.apache.zookeeper.data.Stat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Scanner; import java.util.concurrent.CountDownLatch; /** * 使用 ZooKeeper 实现负载均衡 * 使用 -Dname= -Dip= -Dport= 设置服务器信息 * @author hxstrive.com */ public class ZkLoadBalance { private static ZooKeeper zookeeper; /** 服务实例列表 */ private static final List<Instance> instanceList = Collections.synchronizedList(new ArrayList<>()); public static void main(String[] args) throws Exception { ZkLoadBalance instance = new ZkLoadBalance(); instance.init(); // 初始化 instance.register(); // 注册服务 instance.loadInstanceList(); // 加载服务实例列表 while (true) { try { System.out.println("请输入 list 查看所有实例,输入 lb 任意选择一个服务实例:"); Scanner scanner = new Scanner(System.in); String command = scanner.nextLine(); if ("list".equals(command)) { instanceList.forEach(e -> { System.out.println(JSONObject.toJSONString(e)); }); } else if ("lb".equals(command)) { // 笔者这里演示随机算法 if (instanceList.isEmpty()) { System.out.println("没有可用实例..."); } else { synchronized (instanceList) { Instance ins = instanceList.get((int) (Math.random() * instanceList.size())); System.out.println(JSONObject.toJSONString(ins)); } } } } catch (Exception e) { e.printStackTrace(); } } } /** * 初始化 */ private void init() throws Exception { CountDownLatch countDownLatch = new CountDownLatch(1); zookeeper = new ZooKeeper("127.0.0.1:2181", 2000, new Watcher() { public void process(WatchedEvent watchedEvent) { System.out.println("触发了 " + watchedEvent.getType() + " 事件"); if(watchedEvent.getState() == Event.KeeperState.SyncConnected) { countDownLatch.countDown(); // 连接成功 } // 监听子节点变化 if(watchedEvent.getType() == Event.EventType.NodeChildrenChanged) { try { ZkLoadBalance.this.loadInstanceList(); } catch (Exception e) { e.printStackTrace(); } } } }); System.out.println(zookeeper); countDownLatch.await(); // 等待连接创建成功 } /** * 注册服务 */ private void register() throws Exception { Stat stat = zookeeper.exists("/instances", false); if(null == stat) { zookeeper.create("/instances", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } // 初始化服务信息,创建一个临时节点 Instance instance = new Instance(); instance.setName(System.getProperty("name", "default_server")); instance.setIp(System.getProperty("ip", "127.0.0.1")); instance.setPort(Integer.parseInt(System.getProperty("port", "8080"))); zookeeper.create("/instances/" + instance.getName(), JSONObject.toJSONString(instance).getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); System.out.println("服务注册成功..."); } /** * 加载实例列表 */ private void loadInstanceList() throws Exception { Stat stat = zookeeper.exists("/instances", false); if(null != stat) { List<Instance> tmpInstanceList = new ArrayList<>(); // 监听 /instances 路径 List<String> childrenList = zookeeper.getChildren("/instances", true); childrenList.forEach(path -> { try { byte[] data = zookeeper.getData("/instances/" + path, false, null); Instance instance = JSONObject.parseObject(new String(data), Instance.class); tmpInstanceList.add(instance); } catch (Exception e) { e.printStackTrace(); } }); synchronized (instanceList) { instanceList.clear(); instanceList.addAll(tmpInstanceList); } } } /** * 实例类 */ static class Instance { /** 服务名称 */ private String name; /** 服务IP */ private String ip; /** 服务端口 */ private int port; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } } }
使用 IDEA 运行上面示例,创建三个实例,如下图:
注意,上图中通过在运行程序时添加 JVM 参数来设置服务信息,三个服务的 JVM 参数如下:
# ZkLoadBalance1 -Dname=server1 -Dip=192.168.1.101 -Dport=8080 # ZkLoadBalance2 -Dname=server2 -Dip=192.168.1.102 -Dport=8080 # ZkLoadBalance3 -Dname=server3 -Dip=192.168.1.103 -Dport=8080
运行上面三个服务,如下图:
此时,我们使用可是化工具(PrettyZoo)查看注册到 ZooKeeper 中的实例节点,如下图:
上面成功启动了三个服务实例,并且成功注册到 ZooKeeper,现在将进行演示:
正常启动三个服务实例,然后在每个服务实例下查看服务列表以及通过随机负载均衡算法获取一个服务实例信息,如下图:
先正常启动三个服务实例,然后逐一关闭服务,查看每次关闭一个服务后,其他未关闭服务中服务列表信息以及通过随机负载均衡策略获取一个实例信息,如下图: