來!自己動手實現一個loghub(或kafka)分片消費負載均衡器

等你歸去來發表於2019-07-01

  一般地,像kafka之類的訊息中介軟體,作為一個可以保持歷史訊息的元件,其消費模型一般是主動拉取方式。這是為了給消費者足夠的自由,回滾或者前進。

  然而,也正是由於將消費訊息的權力交給了消費者,所以,消費者往往需要承擔更多的責任。比如:需要自行儲存消費偏移量,以便後續可以知道從哪裡繼續。而當這一點處理不好時,則可能帶來一些麻煩。

  不管怎麼樣,解決方案也都是現成的,我們們也不用擔心。

 

  今天我們要談論的是一個場景: 如何讓n個機器消費m個分片資料?

 

  這在訊息中介軟體的解決方案裡,明白地寫著,使用消費者群組就可以實現了。具體來說就是,每個分片至多會被一機器消費,每個機器則可以消費多個分片資料。即機器資料小於分片數時,分片會被均衡地分配到消費者中。當機器數大於分片數時,多餘的機器將不做任何事情。

  好吧,既然官方已經說明白了,那我們們應該就不再需要自己搞一個輪子了吧。

  但是,我還有個場景:如果我要求在機器做負載重平衡時,需要保證被抽取出去的機器分片,至少保留一段時間,不允許任何機器消費該分片,因為可能還有資料需要備份。

  針對這種場景,我想官方也許是有提供回撥函式之類的解決方案的吧。不管了,反正我沒找到,只能自己先造個輪子了。

 

本文場景前提:

  1. 使用loghub作為訊息中介軟體(原理同kafka);
  2. 整個資料有m個分片shard;
  3. 整個消費者叢集有n臺機器;
  4. 每個分片的資料需要集中到一機器上做有狀態處理;
  5. 可以藉助redis儲存有狀態資料,以便消費者機器做優雅停機;

  最簡單的方案是,使 n=m, 每臺機器消費一個shard, 這樣狀態永遠不會錯亂。

  但是這樣明顯可擴充套件能力太差了!

    比如有時資料量小了,雖然分片還在,但是完全不用那麼多機器的時候,如何縮減機器?
    比如由於資料壓力大了,我想增加下分片數,以提高傳送者效能,但是消費者我還不想理他,消費慢點無所謂?

  其實,我們可以使用官方的消費者群組方法,可以動態縮減機器。

  但是這個有狀態就比較難做到了。

  以上痛點,總結下來就是,可擴充套件性問題。

 

想象中的輪子是怎麼樣的?

  1. 需要有個註冊中心,管理機器的上下線監控;
  2. 需要有負載均衡器,負載將shard的負載均衡的分佈到線上機器中;
  3. 需要有每個機器自己消費的分片記錄,以使機器自身有據可查;
  4. 需要有每個分片的消費情況,以判定出哪些分片已分配給哪些機器;

 

我們來細看下實現:

【1】平衡協調器主框架:

import com.aliyun.openservices.log.Client;
import com.aliyun.openservices.log.common.Shard;
import com.aliyun.openservices.log.exception.LogException;
import com.aliyun.openservices.log.response.ListShardResponse;
import com.test.common.config.LogHubProperties;
import com.test.utils.RedisPoolUtil;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static com.test.dispatcher.work.RedisKeyConstants.MAX_CONSUMER_SHARD_LOAD;


/**
 * loghub動態消費者 shard分配shard 協調器
 *
 */
public class LoghubConsumerShardCoWorker implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(LoghubConsumerShardCoWorker.class);

    private LogHubProperties logHubProperties;

    private RedisPoolUtil redisPoolUtil;

    private Client mClient;

    private ShardAssignMaster shardAssignMaster;

    private String HOST_NAME;

    public LoghubConsumerShardCoWorker(RedisPoolUtil redisPoolUtil, LogHubProperties logHubProperties) {
        this(redisPoolUtil, logHubProperties, null);
    }

    public LoghubConsumerShardCoWorker(RedisPoolUtil redisPoolUtil, LogHubProperties logHubProperties, String hostName) {
        this.redisPoolUtil = redisPoolUtil;
        this.logHubProperties = logHubProperties;
        this.HOST_NAME = hostName;

        initSharedVars();
        initConsumerClient();
        initShardAssigner();
        getAllShardList();
        registerSelfConsumer();
        startHeartBeatThread();
    }

    /**
     * 開啟心跳執行緒,保活
     */
    private void startHeartBeatThread() {
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        executorService.scheduleAtFixedRate(() -> {
            String serverConsumeCacheKey = RedisKeyConstants.SERVER_CONSUME_CACHE_KEY_PREFIX + HOST_NAME;
            redisPoolUtil.expire(serverConsumeCacheKey, 30);
            shardAssignMaster.sendHeartbeat(HOST_NAME);
        }, 30, 25, TimeUnit.SECONDS);
    }

    /**
     * 初始化客戶端例項
     */
    private void initConsumerClient() {
        this.mClient = new Client(logHubProperties.getEndpoint(),
                logHubProperties.getAccessKeyId(), logHubProperties.getAccessKey());
    }

    /**
     * 初始化分片分配控制器
     */
    private void initShardAssigner() {
        shardAssignMaster = new ShardAssignMaster(redisPoolUtil);
    }

    /**
     * 初始化公共變數
     */
    private void initSharedVars() {
        try {
            if(HOST_NAME != null) {
                return;
            }
            HOST_NAME = InetAddress.getLocalHost().getHostName();
        }
        catch (UnknownHostException e) {
            logger.error("init error : 獲取伺服器主機名失敗", e);
            throw new RuntimeException("init error : 獲取伺服器主機名失敗");
        }
    }

    /**
     * 將自己作為消費者註冊到消費者列表中,以判定後續可以進行消費
     */
    private void registerSelfConsumer() {
        shardAssignMaster.registerConsumer(HOST_NAME);
        shardAssignMaster.sendHeartbeat(HOST_NAME);
    }

    @Override
    public void run() {
        try {
            checkConsumerSharding();
        }
        catch (Exception e) {
            logger.error("動態分配shard 發生異常", e);
        }
    }

    /**
     * job 只做一件事,即檢查 shard 的消費情況,不平衡則處理
     */
    private void checkConsumerSharding() {
        try {
            if (tryCoWorkerLock()) {
                // step1. 檢查是否需要進行shard分配
                // 叢集消費loghub資料動態伸縮策略
                // 1. 啟動時先去獲取部分片數,備用;
                // 2. 應用啟動後,把自己註冊到註冊中心或redis中;
                // 3. 根據註冊上來的機器列表,按平均分配策略分配shard(只能由一個機器來分配,其他機器處理分散式鎖競爭失敗,等待狀態);
                // 4. 分配好後,釋放鎖,各機器開始消費,如機器A消費shard 0/3,則機器1以輪詢的方式依次從shard 0/3 摘取資料消費;
                // 5. 分配好的資料結構為:prefix+ip儲存具體資料,另外將自己的key新增到另一個zset中,標識自己存活;自己的key有效期為30秒;使用另一維度 shard,儲存每個shard被佔用情況,使用hash儲存,key為shard,value為當有佔用時為機器ip或主機名,當無佔用時為null或空串;
                // 6. 以上資料刷入,將在機器搶佔到shard更新資料;shard總數資訊暫時不允許在執行期間進行變更;(即如果變理shard必須重啟伺服器)
                // 7. 機器下線時,佔用的key將自動過期;(考慮是否主動刪除)
                // 8. 各機器上啟動一個後臺掃描執行緒,每隔30秒掃描一次。掃描zset,取出所有值後檢視是否存在相應的key,如果不存在說明機器已下線,需要重新分配其佔用的shard;
                // 9. 重新分配策略,使用一致性hash演算法實現;
                // 10. 機器上線時,使用一致性hash演算法重新平衡shard;
                // 11. 使用分散式鎖保證分配程式只有一個;
                CheckConsumerShardingResultContainer resultContainer = checkShardConsumerReBalanceStatus();
                if(resultContainer.getStatusResultType() != ReBalanceStatusResultEnum.OK) {
                    reBalanceConsumerShard(resultContainer);
                }
            }
        }
        finally {
            releaseCoWorkerLock();
        }
    }

    /**
     * 確認機器和shard是否需要再平衡
     *
     * @return 結果狀態集
     */
    private CheckConsumerShardingResultContainer checkShardConsumerReBalanceStatus() {
        // step1. 檢查自身是否存在shard, 不存在則立即進行一次重分配(消費者機器數大於分片數時,重平衡動作將是無效動作)
        // step2. 檢查所有shard列表,是否有未被分配的shard,如有,立即觸發一次重分配
        // step3. 檢查是否有負荷比較高的機器,如有觸發平衡(功能預留,此功能需要基於統計資訊)
        CheckConsumerShardingResultContainer resultContainer = new CheckConsumerShardingResultContainer();

        final List<String> activeServersList = shardAssignMaster.getAllOnlineServerList();
        final List<String> allShardList = getAllShardList();

        // 計算空閒機器
        Map<String, Integer> hostConsumeLoadCountMap = new HashMap<>();
        List<String> idleServerList = filterIdleServerList(activeServersList, hostConsumeLoadCountMap);

        // 計算未被分配的shard
        List<String> unAssignedShardList = filterUnAssignedShardList(allShardList);

        // 根據資源資訊,得出目前的負載狀態
        ReBalanceStatusResultEnum statusResult = computeReBalanceStatusOnResources(
                                            unAssignedShardList, idleServerList, hostConsumeLoadCountMap);

        resultContainer.setAllServerList(activeServersList);
        resultContainer.setAllShardList(allShardList);
        resultContainer.setIdleServerList(idleServerList);
        resultContainer.setUnAssignedShardList(unAssignedShardList);
        resultContainer.setServerConsumeShardLoad(hostConsumeLoadCountMap);
        resultContainer.setStatusResultType(statusResult);
        return resultContainer;
    }

    /**
     * 根據給定資源資訊,計算出目前的負載狀態
     *
     * @param unAssignedShardList 未分配的shard列表
     * @param idleServerList 空閒機器列表
     * @param hostConsumeLoadMap 機器消費計數容器(負載情況)
     * @return 狀態值
     */
    private ReBalanceStatusResultEnum computeReBalanceStatusOnResources(
                                            List<String> unAssignedShardList,
                                            List<String> idleServerList,
                                            Map<String, Integer> hostConsumeLoadMap) {
        // 沒有未分配的shard,檢測是否平衡即可
        // 0. 有空閒機器,則直接分配給空閒機器即可
        // 1. 最大消費shard-最小消費shard數 >= 2, 則說明有機器消費過多shard,需重分配
        // 2. 機器負載平衡,無須調整
        if(unAssignedShardList.isEmpty()) {
            int minConsume = MAX_CONSUMER_SHARD_LOAD;
            int maxConsume = 0;
            for (Map.Entry<String, Integer> entry : hostConsumeLoadMap.entrySet()) {
                int gotCount = entry.getValue();
                if(gotCount > maxConsume) {
                    maxConsume = gotCount;
                }
                if(gotCount < minConsume) {
                    minConsume = gotCount;
                }
            }

            // 因有未分配的機器,假如現有的機器消費都是2,則需要重分配的大壓力的機器 shard 給空閒機器
            if(!idleServerList.isEmpty()) {
                if (maxConsume > 1) {
                    return ReBalanceStatusResultEnum.HEAVY_LOAD_WITH_CONSUMER_IDLE_BALANCE_NEEDED;
                }
            }

            // 有消費相差2的機器,重新分配,從大數上借調到小數上
            if(maxConsume > minConsume + 1) {
                return ReBalanceStatusResultEnum.HEAVY_LOAD_BALANCE_NEEDED;
            }
            return ReBalanceStatusResultEnum.OK;
        }

        // 有可用shard
        // 3. 有空閒機器,直接讓空閒shard分配給這些空閒機器就ok了
        // 4. 沒有空閒機器,須將空閒shard 分配給負載小的機器
        if(idleServerList.isEmpty()) {
            return ReBalanceStatusResultEnum.UNASSIGNED_SHARD_WITHOUT_CONSUMER_IDLE_EXISTS;
        }
        return ReBalanceStatusResultEnum.UNASSIGNED_SHARD_WITH_CONSUMER_IDLE_EXISTS;
    }

    /**
     * 過濾出空閒的機器列表
     *
     * @param activeServersList 所有機器列表
     * @return 空閒機器集, 且將各自消費數放入計數容器
     */
    private List<String> filterIdleServerList(List<String> activeServersList, Map<String, Integer> hostConsumeCountMap) {
        List<String> idleServerList = new ArrayList<>();
        for (String hostname1 : activeServersList) {
            if(!shardAssignMaster.isConsumerServerAlive(hostname1)) {
                shardAssignMaster.invalidateOfflineServer(hostname1);
                continue;
            }
            int consumeCount;
            Set<String> consumeShardSet = shardAssignMaster.getServerDutyConsumeShardSet(hostname1);
            if(consumeShardSet == null || consumeShardSet.isEmpty()) {
                idleServerList.add(hostname1);
                consumeCount = 0;
            }
            else {
                consumeCount = consumeShardSet.size();
            }
            hostConsumeCountMap.put(hostname1, consumeCount);
        }
        return idleServerList;
    }


    /**
     * 過濾出未分配的shard列表
     *
     * @param allShardList 所有shard
     * @return 未分配的shard
     */
    private List<String> filterUnAssignedShardList(List<String> allShardList) {
        List<String> unAssignedShardList = new ArrayList<>();
        for (String shardId1 : allShardList) {
            String consumeHostname = shardAssignMaster.getShardAssignedServer(shardId1);
            // 如果不為空,則之前分配過,檢查機器是否下線
            // 如果為空,則是第一次分配
            if(!StringUtils.isBlank(consumeHostname)) {
                if(!shardAssignMaster.isConsumerServerAlive(consumeHostname)) {
                    // 清除下線機器資訊,將當前shard置為空閒
                    shardAssignMaster.invalidateOfflineServer(consumeHostname);
                    shardAssignMaster.invalidateShardAssignInfo(shardId1);
                    unAssignedShardList.add(shardId1);
                }
            }
            else {
                unAssignedShardList.add(shardId1);
            }
        }
        return unAssignedShardList;
    }

    /**
     * 嘗試獲取協調者協調鎖
     *
     *         在叢集環境中,只允許有一個協調器在執行
     *
     * @return true:成功, false:失敗,不得進行協調分配工作
     */
    private boolean tryCoWorkerLock() {
        return redisPoolUtil.getDistributedLock("distributedLock", HOST_NAME, 30);
    }

    /**
     * 釋放協調鎖,以便下次再競爭
     */
    private void releaseCoWorkerLock() {
        redisPoolUtil.releaseDistributedLock("distributedLock", HOST_NAME);
    }

    /**
     * 重新平衡消費者和shard的關係
     *
     * @param resultContainer 待重平衡狀態
     */
    private void reBalanceConsumerShard(CheckConsumerShardingResultContainer resultContainer) {

        // 叢集消費loghub資料動態伸縮策略,根據負載狀態,呼叫相應策略進行重平衡
        StatusReBalanceStrategy strategy = StatusReBalanceStrategyFactory.createStatusReBalanceAlgorithm(resultContainer, shardAssignMaster);
        strategy.loadBalance();
    }


    /**
     * 獲取分片列表
     *
     * @return 分片列表,如: 0,1,2,3
     */
    private List<String> getAllShardList() {
        // 實時讀取列表
        List<String> shardList = Lists.newArrayList();
        try {
            ListShardResponse listShardResponse = mClient.ListShard(logHubProperties.getProjectName(),
                    logHubProperties.getEventlogStore());
            ArrayList<Shard> getShards = listShardResponse.GetShards();
            for (Shard shard : getShards) {
                shardList.add(shard.GetShardId() + "");
            }
        }
        catch (LogException e) {
            logger.error("loghub 獲取shard列表 error :", e);
        }
        return shardList;
    }

}

  如上,就是協調均衡主框架。主要邏輯如下:

    1. 啟動時初始化各種端,分配器,註冊自己到控制中心等等;
    2. 以執行緒的形式,被外部以定時任務執行的方式呼叫;
    3. 檢查任務前,須獲得檢查鎖,否則直接返回;
    4. 先獲得目前機器的所有消費情況和shard的分配情況,得出資源負載資料;
    5. 根據得到的資料資訊,推算出目前的平衡狀態;
    6. 根據平衡狀態,呼叫相應的平衡策略進行重平衡;
    7. 等待下一次排程;

檢查結果將作為後續選擇均衡策略的依據,所以需要相應的狀態容器儲存。如下:

/**
 * 叢集狀態預檢查 結果容器
 */
class CheckConsumerShardingResultContainer {

    /**
     * 所有shard列表
     */
    private List<String> allShardList;

    /**
     * 未被分配的shard列表
     */
    private List<String> unAssignedShardList;

    /**
     * 所有機器列表
     */
    private List<String> allServerList;

    /**
     * 空閒的機器列表(未被分配shard)
     */
    private List<String> idleServerList;

    /**
     * 機器消費shard的負載計數容器
     */
    private Map<String, Integer> serverConsumeShardLoad;

    /**
     * 狀態檢查結果型別
     */
    private ReBalanceStatusResultEnum statusResultType;

    public Map<String, Integer> getServerConsumeShardLoad() {
        return serverConsumeShardLoad;
    }

    public void setServerConsumeShardLoad(Map<String, Integer> serverConsumeShardLoad) {
        this.serverConsumeShardLoad = serverConsumeShardLoad;
    }

    public List<String> getAllShardList() {
        return allShardList;
    }

    public void setAllShardList(List<String> allShardList) {
        this.allShardList = allShardList;
    }

    public List<String> getUnAssignedShardList() {
        return unAssignedShardList;
    }

    public void setUnAssignedShardList(List<String> unAssignedShardList) {
        this.unAssignedShardList = unAssignedShardList;
    }

    public List<String> getAllServerList() {
        return allServerList;
    }

    public void setAllServerList(List<String> allServerList) {
        this.allServerList = allServerList;
    }

    public List<String> getIdleServerList() {
        return idleServerList;
    }

    public void setIdleServerList(List<String> idleServerList) {
        this.idleServerList = idleServerList;
    }

    public ReBalanceStatusResultEnum getStatusResultType() {
        return statusResultType;
    }

    public void setStatusResultType(ReBalanceStatusResultEnum statusResultType) {
        this.statusResultType = statusResultType;
    }
}

 

  針對多個平衡策略演算法,使用一個工廠類來生產各種策略例項。如下:

/**
 * 再平衡演算法工廠類
 */
class StatusReBalanceStrategyFactory {

    /**
     * 無需做平衡的控制器
     */
    private static final StatusReBalanceStrategy EMPTY_BALANCER = new EmptyReBalancer();

    /**
     * 根據當前的負載狀態,建立對應的負載均衡演算法
     *
     * @param resultContainer 負載狀態集
     * @param shardAssignMaster 分片分配管理者例項
     * @return 演算法例項
     */
    public static StatusReBalanceStrategy createStatusReBalanceAlgorithm(CheckConsumerShardingResultContainer resultContainer, ShardAssignMaster shardAssignMaster) {
        ReBalanceStatusResultEnum balanceStatus = resultContainer.getStatusResultType();
        switch (balanceStatus) {
            case OK:
                return EMPTY_BALANCER;
            case UNASSIGNED_SHARD_WITH_CONSUMER_IDLE_EXISTS:
                return new UnAssignedShardWithConsumerIdleReBalancer(shardAssignMaster,
                                    resultContainer.getUnAssignedShardList(), resultContainer.getIdleServerList());
            case UNASSIGNED_SHARD_WITHOUT_CONSUMER_IDLE_EXISTS:
                return new UnassignedShardWithoutConsumerIdleReBalancer(shardAssignMaster,
                                    resultContainer.getUnAssignedShardList(), resultContainer.getServerConsumeShardLoad());
            case HEAVY_LOAD_BALANCE_NEEDED:
                return new HeavyLoadReBalancer(shardAssignMaster, resultContainer.getServerConsumeShardLoad());
            case HEAVY_LOAD_WITH_CONSUMER_IDLE_BALANCE_NEEDED:
                return new HeavyLoadWithConsumerIdleReBalancer(shardAssignMaster,
                                    resultContainer.getServerConsumeShardLoad(), resultContainer.getIdleServerList());
            default:
                break;
        }
        return EMPTY_BALANCER;
    }
}

/**
 * 負載均衡策略統一介面
 */
interface StatusReBalanceStrategy {

    /**
     * 執行負載均衡方法
     */
    public void loadBalance();
}

 

  針對各種場景的負載均衡,各自實現如下。其中,無需操作時,將返回一個空操作例項!

1. 空操作例項

/**
 * 無需做平衡的控制器
 *
 * @see ReBalanceStatusResultEnum#OK 狀態列舉
 */
class EmptyReBalancer implements StatusReBalanceStrategy {
    @Override
    public void loadBalance() {
        // ignore ...
    }
}

 

2. 分配剩餘shard給空閒的機器控制器

/**
 * 為所有空閒的其他空閒機器分配可用 shard 的控制器
 *
 * @see ReBalanceStatusResultEnum#UNASSIGNED_SHARD_WITHOUT_CONSUMER_IDLE_EXISTS 狀態列舉
 */
class UnAssignedShardWithConsumerIdleReBalancer implements StatusReBalanceStrategy {

    /**
     * 未被分配的分片列表
     */
    private List<String> unAssignedShardList;

    /**
     * 分片分配管理者例項
     */
    private ShardAssignMaster shardAssignMaster;

    /**
     * 空閒的機器列表
     */
    private List<String> idleServerList;

    public UnAssignedShardWithConsumerIdleReBalancer(
                    ShardAssignMaster shardAssignMaster,
                    List<String> unAssignedShardList,
                    List<String> idleServerList) {
        this.shardAssignMaster = shardAssignMaster;
        this.unAssignedShardList = unAssignedShardList;
        this.idleServerList = idleServerList;
    }

    @Override
    public void loadBalance() {
        // 1. 找出還未被消費的shard
        // 2. 依次分配給各空閒機器,每個空閒機器只至多分配一個shard
        int serverIndex = 0;
        for (String shard1 : unAssignedShardList) {
            // 輪詢分配shard, 先只給一個機器分配一個shard
            if(serverIndex >= idleServerList.size()) {
                break;
            }
            String serverHostname = idleServerList.get(serverIndex++);
            shardAssignMaster.assignShardToServer(shard1, serverHostname);
        }
    }
}

 

3. 分配剩餘shard給負載低的機器的控制器

/**
 * 有空閒shard場景 的控制器 , 須找出負載最低的機器塞入shard到現有的機器中(可能是有機器下線導致)
 *
 * @see ReBalanceStatusResultEnum#UNASSIGNED_SHARD_WITHOUT_CONSUMER_IDLE_EXISTS 狀態列舉
 */
class UnassignedShardWithoutConsumerIdleReBalancer implements StatusReBalanceStrategy {

    /**
     * 未被分配分片列表
     */
    private List<String> unAssignedShardList;

    /**
     * 分片管理者例項
     */
    private ShardAssignMaster shardAssignMaster;

    /**
     * 消費者負載情況
     */
    private Map<String, Integer> consumerLoadCount;

    public UnassignedShardWithoutConsumerIdleReBalancer(
                        ShardAssignMaster shardAssignMaster,
                        List<String> unAssignedShardList,
                        Map<String, Integer> consumerLoadCount) {
        this.shardAssignMaster = shardAssignMaster;
        this.unAssignedShardList = unAssignedShardList;
        this.consumerLoadCount = consumerLoadCount;
    }

    @Override
    public void loadBalance() {
        // 1. 找出負載最低的機器
        // 2. 依次分配shard給該機器
        // 3. 分配的後負載數+1, 迴圈分配
        // 先根據空閒數,計算出一個可以接受新shard的機器的shard負載最低值,然後依次分配給這些機器
        for (String shard1 : unAssignedShardList) {
            // 按負載最小分配原則 分配shard
            Map.Entry<String, Integer> minLoadServer = getMinLoadServer(consumerLoadCount);
            String serverHostname = minLoadServer.getKey();

            // 分配shard給機器
            shardAssignMaster.assignShardToServer(shard1, serverHostname);

            // 負載數 +1
            minLoadServer.setValue(minLoadServer.getValue() + 1);
        }
    }

    /**
     * 獲取負載最小的機器名備用
     *
     * @param loadCount 負載資料
     * @return 最小負載機器
     */
    private Map.Entry<String, Integer> getMinLoadServer(Map<String, Integer> loadCount) {
        int minCount = MAX_CONSUMER_SHARD_LOAD;
        Map.Entry<String, Integer> minLoadServer = null;
        for(Map.Entry<String, Integer> server1 : loadCount.entrySet()) {
            if(server1.getValue() < minCount) {
                minCount = server1.getValue();
                minLoadServer = server1;
            }
        }
        return minLoadServer;
    }
}

 

4. 將現有機器消費情況做重分配,從而使各自負載相近控制器

/**
 * 負載不均衡導致的 重新均衡控制器,將消費shard多的機器的 shard 拆解部分到 消費少的機器上 (須上鎖)
 *
 * @see ReBalanceStatusResultEnum#HEAVY_LOAD_BALANCE_NEEDED 狀態列舉
 */
class HeavyLoadReBalancer implements StatusReBalanceStrategy {

    /**
     * 分片分配管理者例項
     */
    private ShardAssignMaster shardAssignMaster;

    /**
     * 機器消費負載情況
      */
    private Map<String, Integer> consumerLoadCount;

    public HeavyLoadReBalancer(ShardAssignMaster shardAssignMaster, Map<String, Integer> consumerLoadCount) {
        this.shardAssignMaster = shardAssignMaster;
        this.consumerLoadCount = consumerLoadCount;
    }

    @Override
    public void loadBalance() {
        // 1. 找出所有機器的消費數的平均線值
        // 2. 負載數大於均線1的,直接抽出多餘的shard, 放到待分配容器中
        // 3. 從大到小排序負載機器
        // 4. 從大的負載上減少shard到最後的機器上,直到小的機器達到平均負載線最貼近的地方,或者小的機器到達平均負載線最貼近的地方
        // 5. ++大負載機器 或者 --小負載機器,下一次迴圈
        double avgLoadCount = computeAliveServersAvgLoadCount(consumerLoadCount);
        List<Map.Entry<String, Integer>> sortedLoadCountList = sortLoadCountByLoadWithSmallEndian(consumerLoadCount);
        int bigLoadIndex = 0;
        int smallLoadIndex = sortedLoadCountList.size() - 1;
        for (;;) {
            // 首先檢測是否已遍歷完成,完成後不再進行分配
            if(isRoundRobinComplete(bigLoadIndex, smallLoadIndex)) {
                break;
            }
            Map.Entry<String, Integer> bigLoadServerEntry = sortedLoadCountList.get(bigLoadIndex);
            double canTakeCountFromBigLoad = bigLoadServerEntry.getValue() - avgLoadCount;
            if(canTakeCountFromBigLoad < 1) {
                bigLoadIndex += 1;
                continue;
            }
            for (int reAssignShardIndex = 0;
                     reAssignShardIndex < canTakeCountFromBigLoad; reAssignShardIndex++) {
                if(isRoundRobinComplete(bigLoadIndex, smallLoadIndex)) {
                    break;
                }
                Map.Entry<String, Integer> smallLoadServerEntry = sortedLoadCountList.get(smallLoadIndex);
                double canPutIntoSmallLoad = avgLoadCount - smallLoadServerEntry.getValue();
                if(canPutIntoSmallLoad < 1) {
                    smallLoadIndex -= 1;
                    continue;
                }
                // 此處可以使用管道操作,更流暢, 或者更準確的說,使用事務操作
                // 從 bigLoad 中移除shard 0
                // 將移除的 shard 上鎖,以防後續新機器立即消費,導致資料異常
                // 新增新shard到 smallLoad 中
                String firstLoadSHardId = shardAssignMaster.popServerFirstConsumeShardId(bigLoadServerEntry.getKey());
                bigLoadServerEntry.setValue(bigLoadServerEntry.getValue() - 1);

                // 上鎖分片,禁用消費
                shardAssignMaster.lockShardId(firstLoadSHardId);

                // 新增shard到 smallLoad 中
                shardAssignMaster.assignShardToServer(firstLoadSHardId, smallLoadServerEntry.getKey());
                smallLoadServerEntry.setValue(smallLoadServerEntry.getValue() + 1);
            }
        }
    }

    /**
     * 判定輪詢是否完成
     *
     * @param startIndex 開始下標
     * @param endIndex 結束下標
     * @return true: 輪詢完成, false: 未完成
     */
    private boolean isRoundRobinComplete(int startIndex, int endIndex) {
        return startIndex == endIndex;
    }

    /**
     * 從大到小排序 負載機器
     *
     * @param consumerLoadCount 總負載情況
     * @return 排序後的機器列表
     */
    private List<Map.Entry<String, Integer>> sortLoadCountByLoadWithSmallEndian(Map<String, Integer> consumerLoadCount) {
        List<Map.Entry<String, Integer>> sortedList = new ArrayList<>(consumerLoadCount.entrySet());
        sortedList.sort(new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                return o2.getValue() - o1.getValue();
            }
        });
        return sortedList;
    }

    /**
     * 計算平均每臺機器的消費shard負載
     *
     * @param loadCount 總負載指標容器
     * @return 負載均線
     */
    private double computeAliveServersAvgLoadCount(Map<String, Integer> loadCount) {
        int totalServerCount = loadCount.size();
        int totalShardCount = 0;
        for(Integer consumeShardCount : loadCount.values()) {
            totalShardCount += consumeShardCount;
        }
        return (double) totalShardCount / totalServerCount;
    }
}

 

5. 從負載重的機器上剝奪shard,分配給空閒的機器 控制器

/**
 *  負載不均衡,且存在空閒的機器, 此時應是 均值與最大值之間相差較小值,但是至少有一個 消費2 的機器,可以剝奪其1個shard給空閒機器 的控制器
 *
 * @see ReBalanceStatusResultEnum#HEAVY_LOAD_WITH_CONSUMER_IDLE_BALANCE_NEEDED 狀態列舉
 */
class HeavyLoadWithConsumerIdleReBalancer implements StatusReBalanceStrategy {

    /**
     * 分片分配管理者例項
     */
    private ShardAssignMaster shardAssignMaster;

    /**
     * 空閒的機器列表
     */
    private List<String> idleServerList;

    /**
     * 機器消費負載情況
     */
    private Map<String, Integer> consumerLoadCount;

    public HeavyLoadWithConsumerIdleReBalancer(
            ShardAssignMaster shardAssignMaster,
            Map<String, Integer> consumerLoadCount,
            List<String> idleServerList) {
        this.shardAssignMaster = shardAssignMaster;
        this.consumerLoadCount = consumerLoadCount;
        this.idleServerList = idleServerList;
    }

    @Override
    public void loadBalance() {
        // 1. 找出還未被消費的shard
        // 2. 分配一個給自己
        // 3. 如果還有其他機器也未分配,則同樣進行分配
        for (String idleHostname1 : idleServerList) {
            Map.Entry<String, Integer> maxLoadEntry = getMaxLoadConsumerEntry(consumerLoadCount);
            // 本身只有一個則不再分配負擔了
            if(maxLoadEntry.getValue() <= 1) {
                break;
            }
            String maxLoadServerHostname = maxLoadEntry.getKey();

            // 此處可以使用管道操作,更流暢, 或者更準確的說,使用事務操作
            // 從 bigLoad 中移除shard 0
            // 將移除的 shard 上鎖,以防後續新機器立即消費,導致資料異常
            // 新增新shard到 smallLoad 中
            String firstLoadSHardId = shardAssignMaster.popServerFirstConsumeShardId(maxLoadServerHostname);
            maxLoadEntry.setValue(maxLoadEntry.getValue() - 1);

            // 上鎖解除安裝下來的shard,鎖定50s
            shardAssignMaster.lockShardId(firstLoadSHardId);

            // 新增shard到 smallLoad 中
            shardAssignMaster.assignShardToServer(firstLoadSHardId, idleHostname1);
            consumerLoadCount.put(idleHostname1, 1);
        }
    }

    /**
     * 獲取負載最大的機器例項作
     *
     * @param consumerLoadCount 所有機器的負載情況
     * @return 最大負載機器例項
     */
    private Map.Entry<String, Integer> getMaxLoadConsumerEntry(Map<String, Integer> consumerLoadCount) {
        Integer maxConsumeCount = 0;
        Map.Entry<String, Integer> maxEntry = null;
        for (Map.Entry<String, Integer> server1 : consumerLoadCount.entrySet()) {
            if(server1.getValue() > maxConsumeCount) {
                maxConsumeCount = server1.getValue();
                maxEntry = server1;
            }
        }
        return maxEntry;
    }
}

  如上,各個平衡策略,實現各自的功能,就能掌控整個叢集的消費控制了!

除了上面的主料,還有一些附帶的東西!

【2】均衡狀態列舉值如下:

/**
 * 再平衡檢測結果型別列舉
 *
 */
public enum ReBalanceStatusResultEnum {

    /**
     * 一切正常,無須操作
     */
    OK("一切正常,無須操作"),

    /**
     * 有新下線機器,可以將其分片分配給其他機器
     */
    UNASSIGNED_SHARD_WITHOUT_CONSUMER_IDLE_EXISTS("有未分配的分片,可以分配給其他機器"),

    /**
     * 有未分配的分片,且有空閒機器,直接將空閒shard分配給空閒機器即可(最好只分配1個,以便其他機器啟動後可用)
     */
    UNASSIGNED_SHARD_WITH_CONSUMER_IDLE_EXISTS("有未分配的分片,且有空閒機器"),

    /**
     * 負載不均衡,鬚生平衡
     */
    HEAVY_LOAD_BALANCE_NEEDED("負載不均衡,鬚生平衡"),

    /**
     * 負載不均衡,且存在空閒的機器, 此時應是 均值與最大值之間相差較小值,但是至少有一個 消費2 的機器,可以剝奪其1個shard給空閒機器
     */
    HEAVY_LOAD_WITH_CONSUMER_IDLE_BALANCE_NEEDED("負載不均衡,且存在空閒的機器"),

    ;

    private ReBalanceStatusResultEnum(String remark) {
        // ignore
    }
}

 

【3】RedisKeyConstants 常量定義

/**
 * redis 相關常量定義
 */
public class RedisKeyConstants {

    /**
     * 線上機器快取key.與心跳同時作用
     *
     * @see #SERVER_ALIVE_HEARTBEAT_CACHE_PREFIX
     */
    public static final String ALL_ONLINE_SERVER_CACHE_KEY = "prefix:active.servers";

    /**
     * 機器消費shard情況 快取key字首
     */
    public static final String SERVER_CONSUME_CACHE_KEY_PREFIX = "prefix:log.consumer:server:";

    /**
     * 分片被分配情況 快取key字首
     */
    public static final String SHARD_ASSIGNED_CACHE_KEY_PREFIX = "prefix:shard.assigned:id:";

    /**
     * 分片鎖 快取key字首, 當上鎖時,任何機器不得再消費
     */
    public static final String SHARD_LOCK_CONSUME_CACHE_PREFIX = "prefix:consume.lock.shard:id:";

    /**
     * 存活機器心跳,與上面的機器形成呼應
     *
     * @see #ALL_ONLINE_SERVER_CACHE_KEY
     */
    public static final String SERVER_ALIVE_HEARTBEAT_CACHE_PREFIX = "prefix:log.consumer:server.heartbeat:";

    /**
     * 單個消費者最大消費負載數 (一個不可能達到的值)
     */
    public static final Integer MAX_CONSUMER_SHARD_LOAD = 9999;
}

 

【4】shard分配控制器負責所有shard分配

import com.test.utils.RedisPoolUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * shard分配管理者 (儘量使用介面表達)
 *
 */
public class ShardAssignMaster {

    private RedisPoolUtil redisPoolUtil;

    public ShardAssignMaster(RedisPoolUtil redisPoolUtil) {
        this.redisPoolUtil = redisPoolUtil;
    }

    /**
     * 註冊消費者到 控制中心(註冊中心)
     */
    public void registerConsumer(String serverHostname) {
        // 註冊server到 redis zset 中,如有條件,可以使用 zk 進行操作,也許更好
        redisPoolUtil.zadd(RedisKeyConstants.ALL_ONLINE_SERVER_CACHE_KEY, (double)System.currentTimeMillis(), serverHostname);
    }

    /**
     * 心跳傳送資料
     */
    public void sendHeartbeat(String serverHostname) {
        String heartbeatCacheKey = RedisKeyConstants.SERVER_ALIVE_HEARTBEAT_CACHE_PREFIX + serverHostname;
        redisPoolUtil.set(heartbeatCacheKey, "1", 30);
    }

    /**
     * 檢測指定消費者伺服器還存活與否
     *
     * @param consumeHostname 機器名
     * @return true: 存活, false: 當機
     */
    public boolean isConsumerServerAlive(String consumeHostname) {
        String aliveValue = redisPoolUtil.get(RedisKeyConstants.SERVER_ALIVE_HEARTBEAT_CACHE_PREFIX + consumeHostname);
        return aliveValue != null
                && "1".equals(aliveValue);
    }
    /**
     * 獲取並刪除指定server的所屬消費的第一個 shardId
     *
     * @param serverHostname 機器名
     * @return 第一個shardId
     */
    public String popServerFirstConsumeShardId(String serverHostname) {
        String bigLoadConsumerServerCacheKey = RedisKeyConstants.SERVER_CONSUME_CACHE_KEY_PREFIX + serverHostname;
        Set<String> firstLoadShardSet = redisPoolUtil.zrange(bigLoadConsumerServerCacheKey, 0, 0);
        String firstLoadSHardId = firstLoadShardSet.iterator().next();
        redisPoolUtil.zrem(bigLoadConsumerServerCacheKey, firstLoadSHardId);
        redisPoolUtil.expire(bigLoadConsumerServerCacheKey, 60);
        return firstLoadSHardId;
    }

    /**
     * 對shard進行上鎖,禁止所有消費行為
     *
     * @param shardId 分片id
     */
    public void lockShardId(String shardId) {
        String shardLockCacheKey = RedisKeyConstants.SHARD_LOCK_CONSUME_CACHE_PREFIX + shardId;
        redisPoolUtil.set(shardLockCacheKey, "1", 50);
    }

    /**
     * 分配shard分片資料給 指定server
     *
     * @param shardId 分片id
     * @param serverHostname 分配給的消費者機器名
     */
    public void assignShardToServer(String shardId, String serverHostname) {
        String smallLoadConsumerServerCacheKey = RedisKeyConstants.SERVER_CONSUME_CACHE_KEY_PREFIX + serverHostname;
        redisPoolUtil.zadd(smallLoadConsumerServerCacheKey, (double)System.currentTimeMillis(), shardId);
        redisPoolUtil.expire(smallLoadConsumerServerCacheKey, 60);

        // 更新新的shard消費者標識
        String shardIdAssignCacheKey = RedisKeyConstants.SHARD_ASSIGNED_CACHE_KEY_PREFIX + shardId;
        redisPoolUtil.set(shardIdAssignCacheKey, serverHostname);
    }

    /**
     * 獲取被分配了shardId的server資訊
     *
     * @param shardId 要檢查的分片id
     * @return 被分配了shardId 的機器名
     */
    public String getShardAssignedServer(String shardId) {
        String shardAssignCacheKey = RedisKeyConstants.SHARD_ASSIGNED_CACHE_KEY_PREFIX + shardId;
        return redisPoolUtil.get(shardAssignCacheKey);
    }

    /**
     * 刪除shard的分配資訊,使無效化
     *
     * @param shardId 要刪除的分片id
     */
    public void invalidateShardAssignInfo(String shardId) {
        String shardAssignCacheKey = RedisKeyConstants.SHARD_ASSIGNED_CACHE_KEY_PREFIX + shardId;
        redisPoolUtil.del(shardAssignCacheKey);
    }

    /**
     * 清理下線機器
     *
     * @param hostname 下線機器名
     */
    public void invalidateOfflineServer(String hostname) {
        redisPoolUtil.zrem(RedisKeyConstants.ALL_ONLINE_SERVER_CACHE_KEY, hostname);
    }

    /**
     * 獲取機器消費的shard列表
     *
     * @param serverHostname 機器主機名
     * @return shard列表 或者 null
     */
    public Set<String> getServerDutyConsumeShardSet(String serverHostname) {
        String serverDutyConsumeShardCacheKey = RedisKeyConstants.SERVER_CONSUME_CACHE_KEY_PREFIX + serverHostname;
        return redisPoolUtil.zrange(serverDutyConsumeShardCacheKey, 0, -1);
    }

    /**
     * 獲取所有線上機器列表
     *
     * @return 線上機器列表
     */
    public List<String> getAllOnlineServerList() {
        Set<String> hostnameSet = redisPoolUtil.zrange(RedisKeyConstants.ALL_ONLINE_SERVER_CACHE_KEY, 0, -1);
        return new ArrayList<>(hostnameSet);
    }

}

  以上是協同負載均衡器程式碼實現。

 

【5】當然你還需要一個消費者

  接下來我們還要看下消費者如何實現消費。

import com.test.utils.RedisPoolUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * 消費者業務執行緒
 *
 */
public class LoghubConsumeWorker implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(LoghubConsumeWorker.class);

    private RedisPoolUtil redisPoolUtil;

    private String HOST_NAME;

    /**
     * 因消費者數目不一定,所以使用 CachedThreadPool
     */
    private ExecutorService consumeExecutorService = Executors.newCachedThreadPool();

    public LoghubConsumeWorker(RedisPoolUtil redisPoolUtil) {
        this(redisPoolUtil, null);
    }

    public LoghubConsumeWorker(RedisPoolUtil redisPoolUtil, String hostName) {
        this.redisPoolUtil = redisPoolUtil;
        // 為測試需要新增的 hostName
        HOST_NAME = hostName;
        initSharedVars();
    }

    /**
     * 初始化公共變數
     */
    private void initSharedVars() {
        try {
            if(HOST_NAME != null) {
                return;
            }
            HOST_NAME = InetAddress.getLocalHost().getHostName();
        }
        catch (UnknownHostException e) {
            throw new RuntimeException("init error : 獲取伺服器主機名失敗");
        }
    }


    @Override
    public void run() {
        while (!Thread.interrupted()) {
            // 先獲取所有分配給的shard列表,為空則進入下一次迴圈(注意此時阻塞鎖不能起作用)
            Set<String> shardsSet = blockingTakeAvailableConsumeShardList();
            try {
                // 消費所有給定shard資料
                consumeLogHubShards(shardsSet);
            } catch (Exception e) {
                logger.error("消費loghub, error", e);
            }

        }

    }

    /**
     * 獲取可用的分片列表(沒有則阻塞等待)
     *
     * @return 分片列表
     */
    private Set<String> blockingTakeAvailableConsumeShardList() {
        while (!Thread.interrupted()) {
            String serverConsumeCacheKey = RedisKeyConstants.SERVER_CONSUME_CACHE_KEY_PREFIX + HOST_NAME;
            Set<String> shardsSet = redisPoolUtil.zrange(serverConsumeCacheKey, 0, -1);
            if (shardsSet != null && !shardsSet.isEmpty()) {
                return shardsSet;
            }
            logger.warn(" =========== 當前主機[hostname:{}]未查詢到任何shard =========", HOST_NAME);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                logger.error("LogHubClientWork run 未獲取到該主機的shard時,每隔1秒鐘獲取 ,error : {}", e);
            }
        }
        return null;
    }

    /**
     * 消費loghub 分片資料
     *
     * @param shardsSet 被分配的分片列表
     */
    public void consumeLogHubShards(Set<String> shardsSet) throws InterruptedException {
        if(shardsSet == null || shardsSet.isEmpty()) {
            return;
        }
        // 此處使用 CountdownLatch, 保證至少有一個任務完成時,才開始下一次任務的調入
//        Semaphore semaphoreLock = new Semaphore(shardsSet.size());
        CountDownLatch openDoorLatch = new CountDownLatch(1);
        boolean startNewJobAtLeastOnce = false;
        for (String shard : shardsSet) {
            // 檢測當前shard是否處於鎖定狀態,如果鎖定則不能消費, 注意鎖情況
            if(isShardLocked(shard)) {
                logger.info("=============== shard:{} is locked, continue... ======", shard);
                continue;
            }
            int shardId = Integer.parseInt(shard);
            LoghubConsumerTaskExecutor consumer = getConsumerExecutor(shardId);
            // consumer 應保證有所消費,如果沒有消費,則自行等待一個長週期,外部應只管調入請求
            // consumer 應保證所有消費,在上一個任務未完成時,不得再開啟下一輪提交消費
            boolean startNewJob = consumer.startNewConsumeJob(openDoorLatch);
            if(startNewJob) {
                // start failed, prev job is running maybe
                // ignore job, no blocking
                startNewJobAtLeastOnce = true;
            }
        }
        // 任意一個任務完成,都將開啟新的分配週期,且後續 countDown 將無效,此處可能導致死鎖
        if(startNewJobAtLeastOnce) {
            openDoorLatch.await();
        }
        else {
            // 當本次分配排程一個任務都未提交時,則睡眠等待
            // (一般此情況為 消費者被分配了上了鎖的shard時,即搶佔另的機器的shard, 需要給別的機器備份資料時間鎖)
            Thread.sleep(200);
        }
    }

    /**
     * 檢測分片是否被鎖定消費了
     *
     * @param shardId 分片id
     * @return true:鎖定, false:未鎖定可用
     */
    private boolean isShardLocked(String shardId) {
        String shardCacheKey = RedisKeyConstants.SHARD_LOCK_CONSUME_CACHE_PREFIX + shardId;
        String lockValue = redisPoolUtil.get(shardCacheKey);
        return !StringUtils.isBlank(lockValue)
                    && "1".equals(lockValue);
    }

    /**
     * 獲取消費者例項,針對一個shard, 只建立一個例項
     */
    private Map<Integer, LoghubConsumerTaskExecutor> mShardConsumerMap = new ConcurrentHashMap<>();
    private LoghubConsumerTaskExecutor getConsumerExecutor(final int shardId) {
        LoghubConsumerTaskExecutor consumer = mShardConsumerMap.get(shardId);
        if (consumer != null) {
            return consumer;
        }
        consumer = new LoghubConsumerTaskExecutor(new SingleShardConsumerJob(shardId));
        mShardConsumerMap.put(shardId, consumer);
        logger.info(" ======================= create new consumer executor shard:{}", shardId);
        return consumer;
    }

    /**
     * 消費者排程器
     *
     *      統一控制消費者的執行狀態管控
     */
    class LoghubConsumerTaskExecutor {

        private Future<?> future;

        private ConsumerJob consumerJob;

        public LoghubConsumerTaskExecutor(ConsumerJob consumerJob) {
            this.consumerJob = consumerJob;
        }

        /**
         * 啟動一個新消費任務
         *
         * @return true: 啟動成功, false: 啟動失敗有未完成任務在前
         */
        public boolean startNewConsumeJob(CountDownLatch latch) {
            if(future == null
                    || future.isCancelled() || future.isDone()) {
                //沒有任務或者任務已取消或已完成 提交任務
                future = consumeExecutorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            consumerJob.consumeShardData();
                        }
                        finally {
                            latch.countDown();
                        }
                    }
                });
                return true;
            }
            return false;
        }

    }

}

/**
 * 消費者任務介面定義
 */
interface ConsumerJob {

    /**
     * 消費資料具體邏輯實現
     */
    public void consumeShardData();
}

/**
 * 單個shard消費的任務實現
 */
class SingleShardConsumerJob implements ConsumerJob {

    /**
     * 當前任務的消費 shardId
     */
    private int shardId;

    public SingleShardConsumerJob(int shardId) {
        this.shardId = shardId;
    }

    @Override
    public void consumeShardData() {
        System.out.println(LocalDateTime.now() + " - host -> consume shard: " + shardId);
        try {
            // do complex biz
            // 此處如果發現shard 不存在異常,則應回撥協調器,進行shard的移除
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}

 

【6】當然你還需要一個demo

  看不到效果,我就是不信!
  所以來看個 demo 吧!
  我們使用單機開多個 單元測試用例,直接測試就好!

測試程式碼:.

import com.test.common.config.LogHubProperties;
import com.test.utils.RedisPoolUtil;
import org.junit.Test;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 臨時測試 負載均衡
 *
 */
public class ShardConsumerLoadBalanceTest {

    public static void main(String[] args) throws IOException {
        startAConsumer();
        System.in.read();
    }

    // 啟動一個單元測試,就相當於啟動一個消費者應用
    @Test
    public void mainMock() throws IOException {
        startAConsumer();
        System.in.read();
    }

    // 啟動一個單元測試,就相當於啟動一個消費者應用
    @Test
    public void startNewConsumer() throws IOException {
        startAConsumer();
        System.in.read();
    }

    // 啟動一個單元測試,就相當於啟動一個消費者應用
    @Test
    public void startNewConsumer2() throws IOException {
        startAConsumer();
        System.in.read();
    }


    private static void startAConsumer() {
        RedisPoolUtil redisPoolUtil = new RedisPoolUtil();
        redisPoolUtil.setIp("127.0.0.1");
        redisPoolUtil.setMaxActive(111);
        redisPoolUtil.setMaxIdle(1000);
        redisPoolUtil.setPort(6379);
        redisPoolUtil.setMaxWait(100000);
        redisPoolUtil.setTimeout(100000);
        redisPoolUtil.setPassWord("123");
        redisPoolUtil.setDatabase(0);
        redisPoolUtil.initPool();

        LogHubProperties logHubProperties = new LogHubProperties();
        logHubProperties.setProjectName("test");
        logHubProperties.setEndpoint("cn-shanghai-finance-1.log.aliyuncs.com");
        logHubProperties.setAccessKey("xxxx");
        logHubProperties.setAccessKeyId("11111");

        // 使用隨機 hostname 模擬多臺機器呼叫
        Random random = new Random();
        String myHostname = "my-host-" + random.nextInt(10);

        // 啟動管理執行緒
        LoghubConsumerShardCoWorker shardCoWorker = new LoghubConsumerShardCoWorker(redisPoolUtil, logHubProperties, myHostname);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        scheduledExecutorService.scheduleAtFixedRate(shardCoWorker, 5, 30, TimeUnit.SECONDS);

        // 啟動業務執行緒
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        LoghubConsumeWorker worker = new LoghubConsumeWorker(redisPoolUtil, myHostname);

        executorService.submit(worker);

    }
}

 

  如上,就可以實現自己的負載均衡消費了。

  比如: 總分片數為4。

    1. 最開始啟動1個機器時,將會被分配 0,1,2,3。
    2. 啟動兩個後,將分為 0,1; 2,3;
    3. 啟動3個後,將分為 0; 1; 2,3;
    4. 反之,關閉一個機器後,將把壓力分擔到原機器上。
    當做負載重分配時,將有50秒的鎖定時間備份。

 

【7】待完善的點

      本文是基於loghub實現的分片拉取,其實在這方面loghub與kafka是如出一轍的,只是loghub更商業產品化。

  當shard縮減時,應能夠自動發現,從而去除原有的機器消費分配。而不是讓消費者報錯。

 

老話: 可以適當造輪子!

相關文章