1. 程式人生 > >MongoDB導出場景查詢優化 #1

MongoDB導出場景查詢優化 #1

ans ext 什麽 會有 exti noop 配置 簡單的 .bat

原始鏈接:https://github.com/aCoder2013/blog/issues/1 轉載請註明出處

引言

前段時間遇到一個類似導出數據場景,觀察下來發現速度會越來越慢,導出100萬數據需要耗費40-60分鐘,從日誌觀察發現,耗時也是越來越高。

原因

從代碼邏輯上看,這裏采取了分批次導出的方式,類似前端的分頁,具體是通過skip+limit的方式實現的,那麽采用這種方式會有什麽問題呢?我們google一下這兩個接口的文檔:

The?cursor.skip()?method is often expensive because it requires the server to walk from the 
beginning of the collection or index to get the offset or skip position before beginning to return 
results. As the offset (e.g.?pageNumber?above) increases,?cursor.skip()?will become slower and 
more CPU intensive. With larger collections,?cursor.skip()?may become IO bound.

簡單來說,隨著頁數的增長,skip()會變得越來越慢,但是具體就我們這裏導出的場景來說,按理說應該沒必要每次都去重復計算,做一些無用功,我的理解應該可以拿到一個指針,慢慢遍歷,簡單google之後,我們發現果然是可以這樣做的。

我們可以在持久層新增一個方法,返回一個cursor專門供上層去遍歷數據,這樣就不用再去遍歷已經導出過的結果集,從O(N2)優化到了O(N),這裏還可以指定一個batchSize,設置一次從MongoDB中抓取的數據量(元素個數),註意這裏最大是4M.

/**
     * <p>Limits the number of elements returned in one batch. A cursor 
     * typically fetches a batch of result objects and store them
     * locally.</p>
     *
     * <p>If {@code batchSize} is positive, it represents the size of each batch of objects retrieved. It can be adjusted to optimize
     * performance and limit data transfer.</p>
     *
     * <p>If {@code batchSize} is negative, it will limit of number objects returned, that fit within the max batch size limit (usually
     * 4MB), and cursor will be closed. For example if {@code batchSize} is -10, then the server will return a maximum of 10 documents and
     * as many as can fit in 4MB, then close the cursor. Note that this feature is different from limit() in that documents must fit within
     * a maximum size, and it removes the need to send a request to close the cursor server-side.</p>
*/

比如說我這裏配置的8000,那麽mongo客戶端就會去默認抓取這麽多的數據量:

技術分享圖片

經過本地簡單的測試,我們發現性能已經有了飛躍的提升,導出30萬數據,采用之前的方式,翻頁到後面平均要500ms,總耗時60039ms。而優化後的方式,平均耗時在100ms-200ms之間,總耗時16667ms(中間包括業務邏輯的耗時)。

使用

DBCursor cursor = collection.find(query).batchSize(8000);
while (dbCursor.hasNext()) {
  DBObject nextItem = dbCursor.next();
  //業務代碼
  ... 
  //
}

那麽我們再看看hasNext內部的邏輯好嗎?好的.

    @Override
    public boolean hasNext() {
        if (closed) {
            throw new IllegalStateException("Cursor has been closed");
        }

        if (nextBatch != null) {
            return true;
        }

        if (limitReached()) {
            return false;
        }

        while (serverCursor != null) {
            //這裏會向mongo發送一條指令去抓取數據
            getMore();
            if (nextBatch != null) {
                return true;
            }
        }

        return false;
    }

    private void getMore() {
        Connection connection = connectionSource.getConnection();
        try {
            if(serverIsAtLeastVersionThreeDotTwo(connection.getDescription()){
                try {
//可以看到這裏其實是調用了`nextBatch`指令        
initFromCommandResult(connection.command(namespace.getDatabaseName(),
                                                             asGetMoreCommandDocument(),
                                                             false,
                                                             new NoOpFieldNameValidator(),
                                                             CommandResultDocumentCodec.create(decoder, "nextBatch")));
                } catch (MongoCommandException e) {
                    throw translateCommandException(e, serverCursor);
                }
            } else {
                initFromQueryResult(connection.getMore(namespace, serverCursor.getId(),
                                                       getNumberToReturn(limit, batchSize, count),
                                                       decoder));
            }
            if (limitReached()) {
                killCursor(connection);
            }
        } finally {
            connection.release();
        }
    }

最後initFromCommandResult 拿到結果並解析成Bson對象

總結

我們平常寫代碼的時候,最好都能夠針對每個方法、接口甚至是更細的粒度加上埋點,也可以設置成debug級別,這樣利用log4j/logback等日誌框架動態更新級別,可以隨時查看耗時,從而更能夠針對性的優化,對於本文說的這個場景,我們首先看看是不是代碼的邏輯有問題,然後看看是不是數據庫的問題,比如說沒建索引、數據量過大等,再去想辦法針對性的優化,而不要上來就擼代碼。

MongoDB導出場景查詢優化 #1