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

本文探讨了在开发中如何实现对后端服务的负载均衡,包括随机选择服务器、处理故障服务器以及重试机制。策略包括:随机选择服务器、标记和避免故障服务器、在重试时间后恢复服务器。通过两步请求过程,保证了服务的可用性和请求的响应时间。

在开发过程中,经常需要请求后端某个服务,而这个服务通常会部署在一个集群上,维护这个服务的同事一般都会给我们一个服务器地址列表,让我们请求这个列表中的服务器。

最简单也是最偷懒的方法,就是任选一个服务器,在代码中只使用这个服务器,每次都请求这个服务器。这么做的问题有两个,一是万一这个服务器出故障了,在故障恢复前所有请求都会失败;二是所有的请求都发送到这个服务器上,当请求量小时还好,请求量大时会这把这个服务器压垮。

稍微好一点的做法是每次都随机从列表中选取一个服务器,然后向该服务器发送请求。这种做法可以保证将请求均匀地分布在集群内的各服务器上。但这种做法也存在一定的问题,当集群内的服务器发生故障的数量多于一台时,如果某次请求随机选取到的服务器无法访问,再次随机选取到的服务器也无法访问,这样就会导致这次请求的时间过长,从而影响用户体验。

由于软硬件故障或网络抖动,集群中的服务器随时可能下线,故障修复后又重新上线。因此,我们的代码应当能做到:
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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值