如何構建“高效能”“大小無限”(磁碟)佇列?

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

假設場景:

  1. 針對一個高併發的應用,你是否會選擇列印訪問日誌?
  2. 針對分散式的應用,你是否會選擇將所有日誌列印到日誌中心?

解決方案:

  1. 如果如果你選擇為了效能,不列印日誌,那無可厚非。但是你得考慮清楚,出問題的時候是否能夠做到快速排查?
  2. 你覺得日誌分佈在各臺機器上很方便,那不用日誌中心也行!

  如果,你還是會選擇列印大量的訪問日誌,如果你還是會選擇列印日誌到日誌中心,那麼本文對你有用!

  如果自己實現一個日誌中心,不說很難吧,也還是要費很大力氣的,比如效能,比如容量大小!

  所以,本文選擇阿里雲的 loghub 作為日誌中心,收集所有日誌!

loghub 常規操作:

  在提出本文主題之前,我們們要看看loghub自己的方式,以及存在的問題!
  在官方接入文件裡,就建議我們們使用 logProducer 接入。

  其實 logProducer 已經做了太多的優化,比如當日志包資料達到一定數量,才統一進行傳送,非同步傳送等等!

  至於為什麼還會存在本篇文章,則是由於這些優化還不夠,比如 這些日誌傳送仍然會影響業務效能,仍然會受到記憶體限制,仍然會搶佔大量cpu。。。

  好吧,接入方式:

  1. 引入maven依賴:

        <dependency>
            <groupId>com.aliyun.openservices</groupId>
            <artifactId>aliyun-log-logback-appender</artifactId>
            <version>0.1.13</version>
        </dependency>

 

  2. logback中新增appender:

    <appender name="LOGHUB-APPENDER" class="appender:com.aliyun.openservices.log.logback.LoghubAppender">
        <endpoint>${loghub.endpoint}</endpoint>
        <accessKeyId>${loghub.accessKeyId}</accessKeyId>
        <accessKey>${loghub.accessKey}</accessKey>
        <projectName>${loghub.projectName}</projectName>
        <logstore>test-logstore</logstore>
        <topic>${loghub.topic}</topic>
        <packageTimeoutInMS>1500</packageTimeoutInMS>
        <logsCountPerPackage>4096</logsCountPerPackage>
        <!-- 4718592=4M, 3145728=3M, 2097152=2M -->
        <logsBytesPerPackage>3145728</logsBytesPerPackage>
        <!-- 17179869184=2G(溢位丟棄) , 104857600=12.5M, 2147483647=2G, 536870912=512M-->
        <memPoolSizeInByte>536870912</memPoolSizeInByte>
        <retryTimes>1</retryTimes>
        <maxIOThreadSizeInPool>6</maxIOThreadSizeInPool>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>
    <root level="${logging.level}">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="LOGHUB-APPENDER" />
    </root>

 

  3. 在程式碼中進行日誌列印:

    private static Logger logger = LoggerFactory.getLogger(MyClass.class);
    logger.warn("give me five: {}", name);

 

看似高效接入,存在的問題:

  1. loghub日誌的傳送是非同步的沒錯,但是當傳送網路很慢時,將會出現大量記憶體堆積;
  2. 堆積也不怕,如上配置,當堆積記憶體達到一定限度時,就不會再大了。他是怎麼辦到的?其實就是通過一個鎖,將後續所有請求全部阻塞了,這想想都覺得可怕;
  3. 網路慢我們可以多開幾個傳送執行緒嘛,是的,這樣能在一定程度上緩解傳送問題,但是基本也無補,另外,日誌傳送執行緒開多之後,執行緒的排程將會更可怕,而這只是一個可有可無的功能而已啊;

 

針對以上問題,我們能做什麼?

  1. 去除不必要的日誌列印,這不是廢話嘛,能這麼幹早幹了!
  2. 在網路慢的時候,減少日誌列印;這有點牽強,不過可以試試!
  3. 直接使用非同步執行緒進行日誌接收和傳送,從根本上解決問題!
  4. 如果使用非同步執行緒進行傳送,那麼當日志大量堆積怎麼辦?
  5. 使用本地檔案儲存需要進行傳送的日誌,解決大量日誌堆積問題,待網路暢通後,快速傳送!

 

  考慮到使用非同步執行緒傳送日誌、使用本地磁碟儲存大量日誌堆積,問題應該基本都解決了!
  但是具體怎麼做呢?
  如何非同步?
  如何儲存磁碟?

  這些都是很現實的問題!

  如果看到這裡,覺得很low的同學,基本可以撤了!

 

下面我們來看看具體實施方案:

1. 如何非同步?

  能想像到的,基本就是使用一個佇列來接收日誌寫請求,然後,開另外的消費執行緒進行消費即可!

  但是,這樣會有什麼問題?因為外部請求訪問進來,都是併發的,這個佇列得執行緒安全吧!用 synchronized ? 用阻塞佇列?

  總之,看起來都會有一個並行轉序列的問題,這會給應用併發能力帶去打擊的!

  所以,我們得減輕這鎖的作用。我們可以使用多個佇列來解決這個問題,類似於分段鎖!如果併發能力不夠,則增加鎖數量即可!

  說起來還是很抽象吧,現成的程式碼擼去吧!

  1. 覆蓋原來的 logProducer 的 appender, 使用自己實現的appender, 主要就是解決非同步問題:

    <appender name="LOGHUB-APPENDER" class="com.test.AsyncLoghubAppender">
        <endpoint>${loghub.endpoint}</endpoint>
        <accessKeyId>${loghub.accessKeyId}</accessKeyId>
        <accessKey>${loghub.accessKey}</accessKey>
        <projectName>${loghub.projectName}</projectName>
        <logstore>apollo-alarm</logstore>
        <topic>${loghub.topic}</topic>
        <packageTimeoutInMS>1500</packageTimeoutInMS>
        <logsCountPerPackage>4096</logsCountPerPackage>
        <!-- 4718592=4M, 3145728=3M, 2097152=2M -->
        <logsBytesPerPackage>3145728</logsBytesPerPackage>
        <!-- 17179869184=2G(溢位丟棄) , 104857600=12.5M, 2147483647=2G, 536870912=512M-->
        <memPoolSizeInByte>536870912</memPoolSizeInByte>
        <retryTimes>1</retryTimes>
        <maxIOThreadSizeInPool>6</maxIOThreadSizeInPool>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>

 

  2. 接下來就是核心的非同步實現: AsyncLoghubAppender

import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.CoreConstants;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.util.IOUtils;
import com.aliyun.openservices.log.common.LogItem;
import com.aliyun.openservices.log.logback.LoghubAppender;
import com.aliyun.openservices.log.logback.LoghubAppenderCallback;
import com.test.biz.cache.LocalDiskEnhancedQueueManager;
import com.test.biz.cache.LocalDiskEnhancedQueueManagerFactory;
import com.test.model.LoghubItemsWrapper;
import com.taobao.notify.utils.threadpool.NamedThreadFactory;
import org.joda.time.DateTime;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 非同步寫loghub appender, 解決框架的appender 無法承受高併發寫的問題
 *
 */
public class AsyncLoghubAppender<E> extends LoghubAppender<E> {

    /**
     * put 執行緒,從業務執行緒接收訊息過來
     */
    private ExecutorService puterExecutor;

    /**
     * 佇列搬運執行緒執行器
     */
    private ExecutorService takerExecutor;

    /**
     * mapdb 操作腳手架
     */
    private LocalDiskEnhancedQueueManager localDiskEnhancedQueueManager;

    /**
     * 日誌訊息傳球手
     */
    private List<LinkedBlockingQueue<LoghubItemsWrapper>> distributeLogItemPoster;

    // puter 的執行緒數,與cpu核數保持一致
    private final int puterThreadNum = 4;

    // taker 的執行緒數,可以稍微少點
    private final int takerThreadNum = 1;

    @Override
    public void start() {
        super.start();
        // 開啟單個put 執行緒
        doStart();
    }

    private void doStart() {
        initMapDbQueue();
        initPosterQueue();
        startPutterThread();
        startTakerThread();
    }

    /**
     * 初始化 mapdb 資料庫
     */
    private void initMapDbQueue() {
        localDiskEnhancedQueueManager = LocalDiskEnhancedQueueManagerFactory.newMapDbQueue();
    }

    /**
     * 初始化訊息傳球手資料
     */
    private void initPosterQueue() {
        distributeLogItemPoster = new ArrayList<>();
        for(int i = 0; i < puterThreadNum; i++) {
            distributeLogItemPoster.add(new LinkedBlockingQueue<>(10000000));
        }
    }

    /**
     * 開啟 putter 執行緒組,此執行緒組不應慢於業務執行緒太多,否則導致記憶體溢位
     */
    private void startPutterThread() {
        puterExecutor = Executors.newFixedThreadPool(puterThreadNum,
                new NamedThreadFactory("Async-LoghubItemPoster"));
        for(int i = 0; i < puterThreadNum; i++) {
            puterExecutor.execute(new InnerQueuePuterThread(distributeLogItemPoster.get(i)));
        }
    }

    /**
     * 初始化取數執行緒組,此執行緒組可以執行慢
     */
    private void startTakerThread() {
        takerExecutor = Executors.newFixedThreadPool(takerThreadNum,
                new NamedThreadFactory("Async-LoghubAppender"));
        for(int i = 0; i < takerThreadNum; i++) {
            takerExecutor.execute(new InnerQueueTakerThread());
        }
    }

    @Override
    public void stop() {
        super.stop();
        localDiskEnhancedQueueManager.close();
    }
    
    // copy from parent
    @Override
    public void append(E eventObject) {
        try {
            appendEvent(eventObject);
        } catch (Exception e) {
            addError("Failed to append event.", e);
        }
    }

    /**
     * 優雅停機
     */
    public void shutdown() {
        puterExecutor.shutdown();
        try {
            puterExecutor.awaitTermination(60, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            addError("【日誌appender】loghub shutdown interupt", e);
            Thread.currentThread().interrupt();
        }
    }

    // modify from parent
    private void appendEvent(E eventObject) {
        //init Event Object
        if (!(eventObject instanceof LoggingEvent)) {
            return;
        }
        LoggingEvent event = (LoggingEvent) eventObject;

        List<LogItem> logItems = new ArrayList<>();
        LogItem item = new LogItem();
        logItems.add(item);
        item.SetTime((int) (event.getTimeStamp() / 1000));

        DateTime dateTime = new DateTime(event.getTimeStamp());
        item.PushBack("time", dateTime.toString(formatter));
        item.PushBack("level", event.getLevel().toString());
        item.PushBack("thread", event.getThreadName());

        StackTraceElement[] caller = event.getCallerData();
        if (caller != null && caller.length > 0) {
            item.PushBack("location", caller[0].toString());
        }

        String message = event.getFormattedMessage();
        item.PushBack("message", message);

        IThrowableProxy iThrowableProxy = event.getThrowableProxy();
        if (iThrowableProxy != null) {
            String throwable = getExceptionInfo(iThrowableProxy);
            throwable += fullDump(event.getThrowableProxy().getStackTraceElementProxyArray());
            item.PushBack("throwable", throwable);
        }

        if (this.encoder != null) {
            // 框架也未處理好該問題,暫時忽略
//            item.PushBack("log", new String(this.encoder.encode(eventObject)));
        }

        LoghubItemsWrapper itemWrapper = new LoghubItemsWrapper();
        itemWrapper.setLogItemList(logItems);
        putItemToPoster(itemWrapper);

    }

    /**
     * 將佇列放入 poster 中
     *
     * @param itemsWrapper 日誌資訊
     */
    private void putItemToPoster(LoghubItemsWrapper itemsWrapper) {
        try {
            LinkedBlockingQueue<LoghubItemsWrapper> selectedQueue = getLoadBalancedQueue();
            selectedQueue.put(itemsWrapper);
        } catch (InterruptedException e) {
            addError("【日誌appender】放入佇列中斷");
            Thread.currentThread().interrupt();
        }
    }

    /**
     * 選擇一個佇列進行日誌放入
     *
     * @return 佇列容器
     */
    private LinkedBlockingQueue<LoghubItemsWrapper> getLoadBalancedQueue() {
        long selectQueueIndex = System.nanoTime() % distributeLogItemPoster.size();
        return distributeLogItemPoster.get((int) selectQueueIndex);
    }

    // copy from parent
    private String fullDump(StackTraceElementProxy[] stackTraceElementProxyArray) {
        StringBuilder builder = new StringBuilder();
        for (StackTraceElementProxy step : stackTraceElementProxyArray) {
            builder.append(CoreConstants.LINE_SEPARATOR);
            String string = step.toString();
            builder.append(CoreConstants.TAB).append(string);
            ThrowableProxyUtil.subjoinPackagingData(builder, step);
        }
        return builder.toString();
    }

    // copy from parent
    private String getExceptionInfo(IThrowableProxy iThrowableProxy) {
        String s = iThrowableProxy.getClassName();
        String message = iThrowableProxy.getMessage();
        return (message != null) ? (s + ": " + message) : s;
    }

    class InnerQueuePuterThread implements Runnable {

        private LinkedBlockingQueue<LoghubItemsWrapper> queue;

        public InnerQueuePuterThread(LinkedBlockingQueue<LoghubItemsWrapper> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            // put the item to mapdb
            while (!Thread.interrupted()) {
                LoghubItemsWrapper itemsWrapper = null;
                try {
                    itemsWrapper = queue.take();
                } catch (InterruptedException e) {
                    addError("【日誌appender】poster佇列中斷");
                    Thread.currentThread().interrupt();
                }
                if(itemsWrapper != null) {
                    flushLogItemToMapDb(itemsWrapper);
                }
            }
        }

        /**
         * 將記憶體佇列儲存到 mapdb 中, 由消費執行緒獲取
         *
         * @param itemsWrapper 日誌資訊
         */
        private void flushLogItemToMapDb(LoghubItemsWrapper itemsWrapper) {
            byte[] itemBytes = JSONObject.toJSONBytes(itemsWrapper.getLogItemList());
            localDiskEnhancedQueueManager.push(itemBytes);
        }
    }

    /**
     * for debug, profiler for mapdb
     */
    private static final AtomicLong takerCounter = new AtomicLong(0);

    class InnerQueueTakerThread implements Runnable {

        @Override
        public void run() {
            long startTime = System.currentTimeMillis();
            while (!Thread.interrupted()) {
                //item = fullLogQueues.take();      // take items without lock
                try {
                    while (localDiskEnhancedQueueManager.isEmpty()) {
                        Thread.sleep(100L);
                    }
                }
                catch (InterruptedException e) {
                    addError("【日誌appender】中斷異常", e);
                    Thread.currentThread().interrupt();
                    break;
                }
                byte[] itemBytes = localDiskEnhancedQueueManager.pollFirstItem();
                try {
                    if(itemBytes != null
                            && itemBytes != localDiskEnhancedQueueManager.EMPTY_VALUE_BYTE_ARRAY) {
                        List<LogItem> itemWrapper = JSONObject.parseArray(
                                new String(itemBytes, IOUtils.UTF8),
                                LogItem.class);
                        if(itemWrapper != null) {
                            doSend(itemWrapper);
                        }
                    }
                    else {
                        // 如果資料不為空,且一直在迴圈,說明存在異常,暫時處理為重置佇列,但應從根本上解決問題
                        localDiskEnhancedQueueManager.reset();
                    }
                }
                catch (Exception e) {
                    addError("【日誌appender】json解析異常", e);
                }
                // for debug test, todo: 上線時去除該程式碼
                if(takerCounter.incrementAndGet() % 1000 == 0) {
                    System.out.println(LocalDateTime.now() + " - "
                            + Thread.currentThread().getName() + ": per 1000 items took time: "
                            + (System.currentTimeMillis() - startTime) + " ms.");
                    startTime = System.currentTimeMillis();
                }
            }
        }

        /**
         * 傳送資料邏輯,主要為 loghub
         *
         * @param item logItem
         */
        private void doSend(List<LogItem> item) {
            AsyncLoghubAppender.this.doSendToLoghub(item);
        }
    }

    /**
     * 傳送資料邏輯,loghub
     *
     * @param item logItem
     */
    private void doSendToLoghub(List<LogItem> item) {
        producer.send(projectConfig.projectName, logstore, topic, source, item,
                new LoghubAppenderCallback<>(AsyncLoghubAppender.this,
                        projectConfig.projectName, logstore, topic, source, item));
    }

}

  如上實現,簡單說明下:

  1. 開啟n個消費執行緒的 distributeLogItemPoster 阻塞佇列,用於接收業務執行緒發來的日誌請求;
  2. 開啟n個消費執行緒, 將從業務執行緒接收過來的請求佇列,放入磁碟佇列中,從而避免可能記憶體溢位;
  3. 開啟m個taker執行緒,從磁碟佇列中取出資料,進行loghub的傳送任務;

  如上,我們已經完全將日誌的傳送任務轉移到非同步來處理了!

  但是,這樣真的就ok了嗎?磁碟佇列是什麼?可靠嗎?效能如何?執行緒安全嗎?

 

2. 如何儲存磁碟佇列?

  好吧。我們們這裡使用的是 mapdb 來實現的磁碟佇列, mapdb 的 github star數超3k, 應該還是不錯了!

  但是,它更多的是用來做磁碟快取,佇列並沒有過多關注,不管怎麼樣,我們還是可以選擇的!

  mapdb專案地址: https://github.com/jankotek/mapdb

  其實mapdb有幾個現成的佇列可用: IndexTreeList, TreeSet. 但是我們仔細看下他的官宣,看到這些資料結構只支援少量資料時的儲存,在資料量巨大之後,效能完全無法保證,甚至 poll 一個元素需要1s+ 。

  所以,還得拋棄其佇列實現,只是自己實現一個了,其 HashTree 是個不錯的選擇, 使用 HashTree 來實現佇列,唯一的難點在於,如何進行元素迭代;(大家不仿先自行思考下)

  下面我們來看下我的一個實現方式:

import com.test.biz.cache.LocalDiskEnhancedQueueManager;
import com.taobao.notify.utils.threadpool.NamedThreadFactory;
import org.mapdb.BTreeMap;
import org.mapdb.DB;
import org.mapdb.DBMaker;
import org.mapdb.Serializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.NavigableSet;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * MapDb 實現的記憶體佇列工具類
 *
 */
public class LocalDiskEnhancedQueueManagerMapDbImpl implements LocalDiskEnhancedQueueManager {

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

    /**
     * 預設儲存檔案
     */
    private final String DEFAULT_DB_FILE = "/opt/mapdb/logappender.db";

    /**
     * 佇列名
     */
    private final String LOG_ITEM_LIST_TABLE = "hub_log_appender";
    private final String LOG_ITEM_TREE_SET_TABLE = "hub_log_appender_tree_set";
    private final String LOG_ITEM_HASH_MAP_TABLE = "hub_log_appender_hash_map";
    private final String LOG_ITEM_BTREE_TABLE = "hub_log_appender_btree";

    private final String QUEUE_OFFSET_HOLDER_BTREE_TABLE = "queue_offset_holder_btree_table";

    /**
     * db 例項
     */
    private final DB mapDb;

//    private IndexTreeList<byte[]> indexTreeListQueue;

    /**
     * 假裝是個佇列
     */
    private NavigableSet<byte[]> treeSetQueue;
    private BTreeMap<byte[], Byte> bTreeQueue;
    private ConcurrentMap<Long, byte[]> concurrentMapQueue;

    /**
     * 佇列偏移量持有器, 對於小容量的節點使用 btree 處理很好
     */
    private BTreeMap<String, Long> queueOffsetDiskHolder;

    /**
     * 讀佇列偏移器, jvm 執行時使用該值, 該值被定時重新整理到 mapdb 中
     *
     *      會有部分資料重複情況
     *
     */
    private AtomicLong readerOfQueueOffsetJvmHolder;

    /**
     * 寫佇列偏移器, jvm 執行時使用該值, 該值被定時重新整理到 mapdb 中
     *
     *      會有部分資料重複情況
     */
    private AtomicLong writerOfQueueOffsetJvmHolder;

    private final String readerOffsetCacheKeyName = "loghub_appender_queue_key_read_offset";
    private final String writerOffsetCacheKeyName = "loghub_appender_queue_key_write_offset";

    /**
     * mapdb 構造方法,給出佇列持有者
     *
     */
    public LocalDiskEnhancedQueueManagerMapDbImpl() {
        mapDb = DBMaker.fileDB(getDbFilePath())
                .checksumHeaderBypass()
                .closeOnJvmShutdown()
                .fileChannelEnable()
                .fileMmapEnableIfSupported()
                // 嘗試修復刪除元素後磁碟檔案大小不變化的bug
                 .cleanerHackEnable()
                .concurrencyScale(128)
                .make();
        initQueueOffsetHolder();
        initQueueOwner();
        initCleanUselessSpaceJob();
    }

    /**
     * 初始化佇列偏移器
     */
    private void initQueueOffsetHolder() {
        queueOffsetDiskHolder = mapDb.treeMap(QUEUE_OFFSET_HOLDER_BTREE_TABLE,
                                    Serializer.STRING, Serializer.LONG)
                                    .createOrOpen();
        initQueueReaderOffset();
        initQueueWriterOffset();
    }

    /**
     * 初始化讀偏移資料
     */
    private void initQueueReaderOffset() {
        Long readerQueueOffsetFromDisk = queueOffsetDiskHolder.get(readerOffsetCacheKeyName);
        if(readerQueueOffsetFromDisk == null) {
            readerOfQueueOffsetJvmHolder = new AtomicLong(1);
        }
        else {
            readerOfQueueOffsetJvmHolder = new AtomicLong(readerQueueOffsetFromDisk);
        }
    }

    /**
     * 初始化寫偏移資料
     */
    private void initQueueWriterOffset() {
        Long writerQueueOffsetFromDisk = queueOffsetDiskHolder.get(writerOffsetCacheKeyName);
        if(writerQueueOffsetFromDisk == null) {
            writerOfQueueOffsetJvmHolder = new AtomicLong(1);
        }
        else {
            writerOfQueueOffsetJvmHolder = new AtomicLong(writerQueueOffsetFromDisk);
        }
    }

    /**
     * 刷入最新的讀偏移
     */
    private void flushQueueReaderOffset() {
        queueOffsetDiskHolder.put(readerOffsetCacheKeyName, readerOfQueueOffsetJvmHolder.get());
    }

    /**
     * 刷入最新的讀偏移
     */
    private void flushQueueWriterOffset() {
        queueOffsetDiskHolder.put(writerOffsetCacheKeyName, writerOfQueueOffsetJvmHolder.get());
    }

    /**
     * 初始化佇列容器
     */
    private void initQueueOwner() {
//        indexTreeListQueue = db.indexTreeList(LOG_ITEM_LIST_TABLE, Serializer.BYTE_ARRAY).createOrOpen();
//        bTreeQueue = mapDb.treeMap(LOG_ITEM_BTREE_TABLE,
//                                            Serializer.BYTE_ARRAY, Serializer.BYTE)
//                                            .counterEnable()
//                                            .valuesOutsideNodesEnable()
//                                            .createOrOpen();
//        treeSetQueue = mapDb.treeSet(LOG_ITEM_TREE_SET_TABLE, Serializer.BYTE_ARRAY)
//                                            .createOrOpen();
        concurrentMapQueue = mapDb.hashMap(LOG_ITEM_HASH_MAP_TABLE, Serializer.LONG, Serializer.BYTE_ARRAY)
                                        .counterEnable()
                                        // 當處理能力很差時,就將該日誌列印丟掉
                                        .expireMaxSize(100 * 10000 * 10000L)
                                        // 3小時後還沒消費就過期了
                                        .expireAfterCreate(3L, TimeUnit.HOURS)
                                        .expireAfterGet()
                                        .createOrOpen();
    }

    /**
     * 清理無用空間,如磁碟檔案等等
     */
    private void initCleanUselessSpaceJob() {
        ScheduledExecutorService scheduledExecutorService =
                Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("Async-MapDbSpaceCleaner"));
        // 每過10分鐘清理一次無用空間,看情況調整
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            mapDb.getStore().compact();
        }, 0L, 10L, TimeUnit.MINUTES);

        // 每過10s刷入一次讀寫偏移,允許重複和丟失
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            flushQueueWriterOffset();
            flushQueueReaderOffset();
        }, 30L, 10L, TimeUnit.SECONDS);
    }

    /**
     * 獲取檔案儲存位置,考慮後續擴充套件被子類覆蓋
     *
     * @return db檔案地址
     */
    protected String getDbFilePath() {
        return DEFAULT_DB_FILE;
    }

    /**
     * 獲取下一個佇列讀編號 (確保準確性可承受,效能可承受)
     *
     * @return 佇列編號
     */
    private long getNextReaderId() {
        return readerOfQueueOffsetJvmHolder.incrementAndGet();
    }

    /**
     * 獲取下一個佇列寫編號 (確保準確性可承受,效能可承受)
     *
     * @return 佇列編號
     */
    private long getNextWriterId() {
        return writerOfQueueOffsetJvmHolder.incrementAndGet();
    }

    @Override
    public boolean push(byte[] itemBytes) {
//        return indexTreeListQueue.add(itemBytes);
//        bTreeQueue.put(itemBytes, (byte)1 );
//        treeSetQueue.add(itemBytes);
        concurrentMapQueue.put(getNextWriterId(), itemBytes);
        return true;
    }

    @Override
    public byte[] pollFirstItem() {
        // 使用時不得使用修改元素方法
//        return indexTreeListQueue.remove(index);
//         Map.Entry<byte[], Byte> entry = bTreeQueue.pollFirstEntry();
//        return treeSetQueue.pollFirst();
        return concurrentMapQueue.remove(getNextReaderId());
    }

    @Override
    public boolean isEmpty() {
        // 佇列為空,不一定代表就沒有可供讀取的元素了,因為 counter 可能落後於併發寫操作了
        // 佇列不為空,不一定代表就一定有可供讀取的元素,因為 counter 可能落後於併發 remove 操作了
        // 當讀指標等於寫指標時,則代表所有元素已被讀取完成,應該是比較準確的空判定標準
        return concurrentMapQueue.isEmpty()
                    || readerOfQueueOffsetJvmHolder.get() == writerOfQueueOffsetJvmHolder.get();
    }

    @Override
    public void close() {
        flushQueueWriterOffset();
        flushQueueReaderOffset();
        mapDb.close();
    }

    @Override
    public void reset() {
        concurrentMapQueue.clear();
        // 同步兩個值,非準確的
        readerOfQueueOffsetJvmHolder.set(writerOfQueueOffsetJvmHolder.get());
        logger.error("【mapdb快取】讀寫指標衝突,強制重置指標,請注意排查併發問題. reader:{}, writer:{}",
                            readerOfQueueOffsetJvmHolder.get(), writerOfQueueOffsetJvmHolder.get());
    }

}

  如上,就是使用 mapdb的hashMap 實現了磁碟佇列功能,主要思路如下:

  1. 使用一個long的自增資料作為 hashMap 的key,將佇列存入value中;
  2. 使用另一個 long 的自增指標做為讀key, 依次讀取資料;
  3. 讀寫指標都定期刷入磁碟,以防出異常crash時無法恢復;
  4. 當實在出現了未預料的bug時,允許直接丟棄衝突日誌,從一個新的讀取點開始新的工作;

  最後,再加一個工廠類,生成mapdb佇列例項: LocalDiskEnhancedQueueManagerFactory

import com.test.biz.cache.impl.LocalDiskEnhancedQueueManagerMapDbImpl;

/**
 * 本地磁碟佇列等例項工廠類
 *
 */
public class LocalDiskEnhancedQueueManagerFactory {

    /**
     * 生產一個mapDb實現的佇列例項
     *
     * @return mapdb 佇列例項
     */
    public static LocalDiskEnhancedQueueManager newMapDbQueue() {
        return new LocalDiskEnhancedQueueManagerMapDbImpl();
    }

    /**
     * 生產一個使用 ehcache 實現的佇列例項
     *
     * @return ehcache 佇列例項
     */
    public static LocalDiskEnhancedQueueManager newEhcacheQueue() {
        // 有興趣的同學可以實現下
        return null;
    }

    /**
     * 生產一個使用 fqueue 實現的佇列例項
     *
     * @return fqueue 佇列例項
     */
    public static LocalDiskEnhancedQueueManager newFQueueQueue() {
        // 有興趣的同學可以實現下, 不過不太建議
        return null;
    }


    /**
     * 生產一個使用 自己直接寫磁碟檔案 實現的佇列例項
     *
     * @return file 佇列例項
     */
    public static LocalDiskEnhancedQueueManager newOwnFileQueue() {
        // 有興趣的同學可以挑戰下
        return null;
    }

}

  這樣,我們就實現了一個既能滿足高併發場景下的日誌列印需求了吧。業務執行緒優先,日誌執行緒非同步化、可丟棄、cpu佔用少、記憶體不限制。

 

老話: 優化之路,道阻且長!

相關文章