访问后端服务负载均衡的方法
2017-02-10 18:24
197 查看
在开发过程中,经常需要请求后端某个服务,而这个服务通常会部署在一个集群上,维护这个服务的同事一般都会给我们一个服务器地址列表,让我们请求这个列表中的服务器。
最简单也是最偷懒的方法,就是任选一个服务器,在代码中只使用这个服务器,每次都请求这个服务器。这么做的问题有两个,一是万一这个服务器出故障了,在故障恢复前所有请求都会失败;二是所有的请求都发送到这个服务器上,当请求量小时还好,请求量大时会这把这个服务器压垮。
稍微好一点的做法是每次都随机从列表中选取一个服务器,然后向该服务器发送请求。这种做法可以保证将请求均匀地分布在集群内的各服务器上。但这种做法也存在一定的问题,当集群内的服务器发生故障的数量多于一台时,如果某次请求随机选取到的服务器无法访问,再次随机选取到的服务器也无法访问,这样就会导致这次请求的时间过长,从而影响用户体验。
由于软硬件故障或网络抖动,集群中的服务器随时可能下线,故障修复后又重新上线。因此,我们的代码应当能做到:
1)将请求均匀地分布到集群中的各服务器上(即实现负载均衡);
2)当某台服务器下线时(即无法访问时)能将该服务器从可用服务器列表中剔除,不再向该服务器发送请求;
3)当某台服务器故障修复重新上线后,应当能及时发现,并将请求分发到该服务器上。
为了实现这3个目标,我的基本策略是:
1)每次请求时都随机选取一台服务器;
2)当发现某台服务器无法访问时将其标记为不可用,同时为它设置重试时间,在重试时间到达前不再发送请求到该服务器;
3)被标记为不可用的服务器在重试时间到达后允许再次访问,若发现它已恢复服务,则重新将其标记为可用,否则将重试时间延长。
上述策略的第1点保证了请求可以均匀分布;第2点保证了不会将请求发送到出现故障的服务器上;第3点则保证了当服务器故障修复重新上线后可以及时被发现。第3点是基于这样一个假设,即服务器无法访问是暂时的,等待一段时间后它就可以恢复服务。
从上面的策略可以看出,某次请求可能会向被标记为不可用的服务器发送请求,以探测该服务器是否已经从故障中恢复。为了控制单次请求的延时时间,防止在一次请求中多次尝试访问不可用的服务器,向后端服务器集群发送请求的具体步骤分为以下2步:
第1步:从服务器列表中随机选取一个服务器。
获取到的服务器分3种情况:
a.被标记为可用的服务器,返回该服务器;
b.被标记为不可用的服务器,但是已经到达重试时间,返回该服务器;
c.被标记为不可用的服务器,且还没有到达重试时间,返回空。
第1步获取到服务器后,向其发送请求,若请求成功,则请求结束(若该服务器被标记为不可用,还需重新将其标记为可用);若请求失败,则将其标记为不可用,然后进入第2步。
当然,如果第1步没有获取到服务器,则直接进入第2步。
第2步:从服务器列表中强制获取一个可用的服务器。
到这一步,说明第1步可能已经请求过一次服务了,但是失败了,所以这一步要尽可能地确保请求可以成功。这一步的策略就是,尽可能获取标记为可用的服务器,即只要有可用的服务器,就返回一个可用的服务器;如果没有可用的服务器了,那就退而求其次,返回最早到达重试时间的那个服务器;最后如果所有的服务器都不可用且都还没有到达重试时间,那天王老子也没办法了,只能返回空,表示没有服务器可用。具体实现上来说,为了保证请求尽可能均匀的分布在各服务器上,这一步也是先随机若干次获取可用的服务器,若多次随机获取的都是不可用的服务器,则遍历整个服务器列表,将第一个可用的服务器返回。
第2步获取到服务器后,向其发送请求,若请求成功,则请求结束(若该服务器被标记为不可用,还需重新将其标记为可用);若请求失败,则将其标记为不可用,然后循环执行第2步,直到请求成功,或者获取不到服务器为止。
不论是第1步还是第2步,如果某个服务器本就处于不可用的状态,在重试后发现其仍然无法访问,还需要将其重试时间加倍。
向服务器集群发起请求分成两步走是有必要的,第一步是为了保证被标记为不可用的服务器能有重试的机会,如果每次都只获取可用的服务器,则那些标记为不可用的服务器在故障恢复后将迟迟不能被发现。第二步强制获取可用的服务器,又确保了在有可用服务器的情况下,在一次请求中不会多次尝试访问不可用的服务器,从而保证了请求的响应时间。
对后端服务器集群进行访问是个通用的功能,我使用了2个类来实现这项功能。ServerInfo类用来描述一个服务器,ServerMgr类用来管理服务器列表。以下是具体代码。
其中isValid字段用来标记该服务器是否可用。lastTryTime是服务器不可用时,上一次尝试访问它的时刻;waitTime则是再次重试前应该等待的时间。lastTryTime和waitTime合起来,就可以判断标记为不可用的服务器是否到达重试时间。比如重试时间的初始值为10秒,如果在2017-2-1 18:00:00发现某服务器无法访问,则将其lastTryTime字段设置为2017-2-1 18:00:00(自1970年以来经过的秒数),waitTime设置为10,那么在18:00:10之后就可以再次尝试访问该服务器。
服务器列表管理类:
最简单也是最偷懒的方法,就是任选一个服务器,在代码中只使用这个服务器,每次都请求这个服务器。这么做的问题有两个,一是万一这个服务器出故障了,在故障恢复前所有请求都会失败;二是所有的请求都发送到这个服务器上,当请求量小时还好,请求量大时会这把这个服务器压垮。
稍微好一点的做法是每次都随机从列表中选取一个服务器,然后向该服务器发送请求。这种做法可以保证将请求均匀地分布在集群内的各服务器上。但这种做法也存在一定的问题,当集群内的服务器发生故障的数量多于一台时,如果某次请求随机选取到的服务器无法访问,再次随机选取到的服务器也无法访问,这样就会导致这次请求的时间过长,从而影响用户体验。
由于软硬件故障或网络抖动,集群中的服务器随时可能下线,故障修复后又重新上线。因此,我们的代码应当能做到:
1)将请求均匀地分布到集群中的各服务器上(即实现负载均衡);
2)当某台服务器下线时(即无法访问时)能将该服务器从可用服务器列表中剔除,不再向该服务器发送请求;
3)当某台服务器故障修复重新上线后,应当能及时发现,并将请求分发到该服务器上。
为了实现这3个目标,我的基本策略是:
1)每次请求时都随机选取一台服务器;
2)当发现某台服务器无法访问时将其标记为不可用,同时为它设置重试时间,在重试时间到达前不再发送请求到该服务器;
3)被标记为不可用的服务器在重试时间到达后允许再次访问,若发现它已恢复服务,则重新将其标记为可用,否则将重试时间延长。
上述策略的第1点保证了请求可以均匀分布;第2点保证了不会将请求发送到出现故障的服务器上;第3点则保证了当服务器故障修复重新上线后可以及时被发现。第3点是基于这样一个假设,即服务器无法访问是暂时的,等待一段时间后它就可以恢复服务。
从上面的策略可以看出,某次请求可能会向被标记为不可用的服务器发送请求,以探测该服务器是否已经从故障中恢复。为了控制单次请求的延时时间,防止在一次请求中多次尝试访问不可用的服务器,向后端服务器集群发送请求的具体步骤分为以下2步:
第1步:从服务器列表中随机选取一个服务器。
获取到的服务器分3种情况:
a.被标记为可用的服务器,返回该服务器;
b.被标记为不可用的服务器,但是已经到达重试时间,返回该服务器;
c.被标记为不可用的服务器,且还没有到达重试时间,返回空。
第1步获取到服务器后,向其发送请求,若请求成功,则请求结束(若该服务器被标记为不可用,还需重新将其标记为可用);若请求失败,则将其标记为不可用,然后进入第2步。
当然,如果第1步没有获取到服务器,则直接进入第2步。
第2步:从服务器列表中强制获取一个可用的服务器。
到这一步,说明第1步可能已经请求过一次服务了,但是失败了,所以这一步要尽可能地确保请求可以成功。这一步的策略就是,尽可能获取标记为可用的服务器,即只要有可用的服务器,就返回一个可用的服务器;如果没有可用的服务器了,那就退而求其次,返回最早到达重试时间的那个服务器;最后如果所有的服务器都不可用且都还没有到达重试时间,那天王老子也没办法了,只能返回空,表示没有服务器可用。具体实现上来说,为了保证请求尽可能均匀的分布在各服务器上,这一步也是先随机若干次获取可用的服务器,若多次随机获取的都是不可用的服务器,则遍历整个服务器列表,将第一个可用的服务器返回。
第2步获取到服务器后,向其发送请求,若请求成功,则请求结束(若该服务器被标记为不可用,还需重新将其标记为可用);若请求失败,则将其标记为不可用,然后循环执行第2步,直到请求成功,或者获取不到服务器为止。
不论是第1步还是第2步,如果某个服务器本就处于不可用的状态,在重试后发现其仍然无法访问,还需要将其重试时间加倍。
向服务器集群发起请求分成两步走是有必要的,第一步是为了保证被标记为不可用的服务器能有重试的机会,如果每次都只获取可用的服务器,则那些标记为不可用的服务器在故障恢复后将迟迟不能被发现。第二步强制获取可用的服务器,又确保了在有可用服务器的情况下,在一次请求中不会多次尝试访问不可用的服务器,从而保证了请求的响应时间。
对后端服务器集群进行访问是个通用的功能,我使用了2个类来实现这项功能。ServerInfo类用来描述一个服务器,ServerMgr类用来管理服务器列表。以下是具体代码。
public class ServerInfo { private String ip; // 服务器ip private int port; // 服务器端口 private boolean isValid; // 服务器是否可用 private long lastTryTime; // 上次尝试访问时间,单位:秒 private long waitTime; // 再次尝试访问该服务器前应等待的时间 public ServerInfo(String ip, int port) { this.ip = ip; this.port = port; isValid = true; lastTryTime = 0; waitTime = 0; } 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; } public boolean isValid() { return isValid; } public void setValid(boolean valid) { isValid = valid; } public long getLastTryTime() { return lastTryTime; } public void setLastTryTime(long lastTryTime) { this.lastTryTime = lastTryTime; } public long getWaitTime() { return waitTime; } public void setWaitTime(long waitTime) { this.waitTime = waitTime; } @Override public String toString() { return "ServerInfo{" + "ip='" + ip + '\'' + ", port=" + port + ", isValid=" + isValid + ", lastTryTime=" + lastTryTime + ", waitTime=" + waitTime + '}'; } }
其中isValid字段用来标记该服务器是否可用。lastTryTime是服务器不可用时,上一次尝试访问它的时刻;waitTime则是再次重试前应该等待的时间。lastTryTime和waitTime合起来,就可以判断标记为不可用的服务器是否到达重试时间。比如重试时间的初始值为10秒,如果在2017-2-1 18:00:00发现某服务器无法访问,则将其lastTryTime字段设置为2017-2-1 18:00:00(自1970年以来经过的秒数),waitTime设置为10,那么在18:00:10之后就可以再次尝试访问该服务器。
服务器列表管理类:
public class ServerMgr { // 当服务器不可用时,重试等待时间(默认值) private static final long WAIT_TIME_DEFAULT = 10; // 最长重试等待时间(默认值) private static final long WAIT_TIME_MAX_DEFAULT = 5 * 60; // 获取服务器时的随机次数(默认值) private static final int RANDOM_TRY_TIMES_DEFAULT = 3; // 服务器列表 private List<ServerInfo> serverInfolist; // 重试等待时间 private long waitTimeBeforeRetry; // 最长重试等待时间 private long maxWaitTimeBeforeRetry; // 获取时随机次数 private int randomCount; // 服务器个数 private int serverCount; // 随机数生成器 private Random random; public ServerMgr(List<ServerInfo> serverInfos) { serverInfolist = new ArrayList<>(); for (ServerInfo serverInfo : serverInfos) { serverInfolist.add(new ServerInfo(serverInfo.getIp(), serverInfo.getPort())); } waitTimeBeforeRetry = WAIT_TIME_DEFAULT; maxWaitTimeBeforeRetry = WAIT_TIME_MAX_DEFAULT; randomCount = RANDOM_TRY_TIMES_DEFAULT; serverCount= serverInfolist.size(); random = new Random(); // 随机尝试次数不超过服务器数量 // 特别的,若服务器只有1台,则随机尝试次数为1 checkRandomCount(); } /** * 随机获取一个服务器,获取的服务器: * 1)要么是可用的(即serverInfo.isValid()返回为true); * 2)要么是不可用的(即serverInfo.isValid()返回为false)但是已经到达重试时间; * 3)要么是不可用的且尚未到达重试时间,此时返回null。 * * 获取服务器时,调用方应当首先调用本函数获取,对返回值的三种情况,调用方应该做如下处理: * (1)返回可用服务器的,如果使用时发现该服务器实际不可用, * 则应当调用invalidateServer()函数使之失效,然后再次调用getServerByForce()函数强制获取一个服务器; * (2)返回不可用服务器的,若重试后发现该服务器恢复可用了,则应当调用validateServer()函数使之生效; * 若重试后依然不可用,再应当再次调用invalidateServer()函数使之失效; * 然后调用getServerByForce()强制获取一条连接; * (3)返回null的,直接调用getServerByForce()函数强制获取一个服务器。 * * 获取服务器时,调用方正确的做法为,首先调用本函数获取服务器,若获取不到或者获取的服务器无法访问,则再调用getServerByForce()获取。 * 之所以要求首先调用本函数,是为了重试标记为不可用的服务器,以便短暂不可用的服务器恢复正常后可以及时被我们发现。 * * @return */ public ServerInfo getServerByRandom() { if (serverInfolist.isEmpty()) { return null; } // 随机获取一个服务器 int index = random.nextInt(this.serverCount); ServerInfo serverInfo = serverInfolist.get(index); if (serverInfo.isValid()) { return serverInfo; } long now = new Date().getTime() / 1000; if (now > serverInfo.getLastTryTime() + serverInfo.getWaitTime()) { // 服务器已到达重试时间,给它一次机会,重试下 return serverInfo; } // 很不幸,获取的服务器既不可用,也没有达到重试时间,返回null return null; } /** * 强制获取一个服务器。本函数保证: * 在有可用服务器的情况下,肯定返回一个可用服务器; * 若所有服务器都不可用,则返回最早到达重试时间的那个服务器; * 若所有服务器都没有到达重试时间,则返回null。 * * 调用方应当首先调用getServerByRandom()函数获取服务器,获取不到或者获取的服务器使用后发现确实不可用,再调用本函数获取服务器, * 否则,当某台服务器暂时不可用又恢复后,将导致该服务器长期饥饿。 * * 函数首先尝试随机获取一个可用服务器,若尝试若干次都获取不到, * 则遍历服务器列表池,获取第一个可用服务器; * 若所有服务器都不可用,则返回最早到达重试时间的那个服务器; * 若所有服务器都没有到达重试时间,则返回null。 * * 函数保证了随机性。 * * @return */ public ServerInfo getServerByForce() { // 避免无休止的随机 int randomCnt = this.randomCount; while (randomCnt-- > 0) { ServerInfo svrInfo = getServerByRandom(); if (svrInfo != null && svrInfo.isValid()) { return svrInfo; } } // 运气真不好,连续随机几次,获取的都是不可用的连接 // 用来记录最早到达重试时间的连接 ServerInfo earliestServerInfo = null; long now = new Date().getTime() / 1000; // 遍历服务器列表 for (ServerInfo serverInfo : serverInfolist) { if (serverInfo.isValid()) { // 返回第一个可用的服务器 return serverInfo; } // 判断服务器是否已达到重试时间 long tryAt = serverInfo.getLastTryTime() + serverInfo.getWaitTime(); if (now >= tryAt) { if (earliestServerInfo == null || tryAt < earliestServerInfo.getLastTryTime() + earliestServerInfo.getWaitTime()) { earliestServerInfo = serverInfo; } } } // 当所有的服务器都不可用时,返回最早到达重试时间的那个服务器 return earliestServerInfo; } /** * 标记服务器不可用 * @param serverInfo */ public void invalidateServer(ServerInfo serverInfo) { long waitTime = serverInfo.getWaitTime(); if (waitTime == 0) { // 从可用变为不可用 waitTime = this.waitTimeBeforeRetry; } else { // 重试后依然不可用,延长等待时间 waitTime *= 2; } // 重试等待时间不超过最大值 if (waitTime > this.maxWaitTimeBeforeRetry) { waitTime = this.maxWaitTimeBeforeRetry; } serverInfo.setWaitTime(waitTime); serverInfo.setLastTryTime(new Date().getTime() / 1000); serverInfo.setValid(false); } /** * 标记服务器可用 * @param serverInfo */ public void validateServer(ServerInfo serverInfo) { serverInfo.setWaitTime(0); serverInfo.setValid(true); } public void setWaitTimeBeforeRetry(long waitTimeBeforeRetry) { this.waitTimeBeforeRetry = waitTimeBeforeRetry; } public void setMaxWaitTimeBeforeRetry(long maxWaitTimeBeforeRetry) { this.maxWaitTimeBeforeRetry = maxWaitTimeBeforeRetry; } public void setRandomCount(int randomCount) { this.randomCount = randomCount; checkRandomCount(); } private void checkRandomCount() { // 随机尝试次数不超过服务器数量 // 特别的,若服务器只有1台,则随机尝试次数为1 if (randomCount > serverCount) { randomCount = serverCount; } } }
// 测试程序,伪代码 public class Test { public static void main(String[] args) throws Exception { List<ServerInfo> list = new ArrayList<>(); list.add(new ServerInfo("10.94.0.1", 8000)); list.add(new ServerInfo("10.94.0.2", 8000)); list.add(new ServerInfo("10.94.0.3", 8000)); list.add(new ServerInfo("10.94.0.4", 8000)); ServerMgr serverMgr = new ServerMgr(list); // 第1步,随机获取一个服务器 ServerInfo serverInfo = serverMgr.getServerByRandom(); if (serverInfo == null) { // 第2步,强制获取一个可用服务器 serverInfo = serverMgr.getServerByForce(); if (serverInfo == null) { throw new Exception("无可用服务器"); } } // 请求内容 String requestMsg = "/get/1"; do { try { // 发起请求,requestServer()函数由业务方实现 String res = requestServer(requestMsg, serverInfo.getIp(), serverInfo.getPort()); // 请求成功,发生故障的服务器恢复服务,将其重置为可用状态 if (!serverInfo.isValid()) { serverMgr.validateServer(serverInfo); } System.out.println("请求成功:" + res); return; } catch (Exception e) { System.out.println("服务器不可用:" + serverInfo); // 服务器无法访问, 将服务器标记为不可用 serverMgr.invalidateServer(serverInfo); } // 第2步强制获取可用的服务器 serverInfo = serverMgr.getServerByForce(); if (serverInfo == null) { throw new Exception("无可用的服务器"); } } while (true); } }
相关文章推荐
- nginx做负载均衡的时候,检验后端服务健康状态的方法
- 使用keepalived加lvs做负载均衡,访问后端的服务器,2分钟后超时,需要重新登录
- 使用多个tomcat实现负载均衡后,tomcat端口不对外开放的情况下,实现精确访问tomcat的方法
- Alteon 4层负载均衡服务应用
- 浅谈IDC机房的负载均衡服务
- 减压分流:谈IDC机房的负载均衡服务(转)
- web集群服务的负载均衡方案选择与实现
- 浅谈IDC机房的负载均衡服务
- 使用JQuery中ajax方法访问web服务。
- 两类负载均衡的实现方法
- 【服务配置】apache+tomcat配置负载均衡的网站
- web集群服务的负载均衡方案选择与实现
- 无法访问windows安装服务 解决方法
- 蛙蛙推荐:作一个支持过载自适应和动态扩容的负载均衡服务
- 企业实现服务器负载均衡常见的四种方法
- 【服务配置】apache+tomcat配置负载均衡的网站 【摘】
- 减压分流:谈IDC机房的负载均衡服务(转)
- web集群服务的负载均衡方案选择与实现