您的位置:首页 > 其它

访问后端服务负载均衡的方法

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类用来管理服务器列表。以下是具体代码。

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);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息