天天看點

mysql cluster ndb的索引性能ndb存儲引擎的索引批量操作,一次執行性能測試btree索引的查詢優化btree索引的多程序并發查詢總結Reference

本文連結:http://blog.csdn.net/njnu_mjn/article/details/52882402,謝絕轉載

ndb存儲引擎的索引

ndb存儲引擎支援兩種索引(見CREATE INDEX Syntax): hash和btree

Storage Engine Permissible Index Types
InnoDB BTREE
MyISAM BTREE
MEMORY/HEAP BTREE
NDB HASH, BTREE

hash

mysql實作的hash是一種key-value,一一對應,不允許存在相同的hash值,是以隻能在primary key和unique字段上指定使用hash索引,且需要顯示指定

using hash

, 如:

create table test
(
    id bigint,
    seq int,
    description varchar(),
    primary key (id, seq) using hash
) engine = ndb
partition by key(id);
           

檢視建立的索引

show index from test;

mysql cluster ndb的索引性能ndb存儲引擎的索引批量操作,一次執行性能測試btree索引的查詢優化btree索引的多程式并發查詢總結Reference

unique字段使用hash的例子:

create table test2
(
    id int,
    unique (id) using hash
) engine = ndb;
           

btree

ndb存儲引擎使用T-tree資料結構來實作btree索引的(見CREATE INDEX Syntax):

BTREE indexes are implemented by the NDB storage engine as T-tree indexes.

如果primary key和unique不顯示使用hash, 則預設使用btree:

For indexes on NDB table columns, the USING option can be specified only for a unique index or primary key. USING HASH prevents the creation of an ordered index; otherwise, creating a unique index or primary key on an NDB table automatically results in the creation of both an ordered index and a hash index, each of which indexes the same set of columns.

create index建立的索引都是btree, ndb存儲引擎不支援在建立索引的時候使用using關鍵字來指定索引類型

例如:

create index idx_test_id on test
(
    id
);
           

檢視建立的索引

show index from test;

mysql cluster ndb的索引性能ndb存儲引擎的索引批量操作,一次執行性能測試btree索引的查詢優化btree索引的多程式并發查詢總結Reference

批量操作,一次執行

mysql ndb支援多次操作,一次性執行(見The NdbTransaction Class)

Several operations can be defined on the same NdbTransaction object, in which case they are executed in parallel. When all operations are defined, the execute() method sends them to the NDB kernel for execution.

可以通過多次調用NdbTransaction::getNdbOperation()擷取多個NdbOperation,針對NdbOperation的增删改隻修改本地的資料,當執行NdbTransaction::execute()時,NdbTransaction下的多個NdbOperation被同時發到伺服器端,并行執行。

性能測試

hash

使用上面建立的

test

表,插入一條測試資料:

insert into test(id, seq, description)
values(, , 'this is a description');
           

根據主鍵(即hash索引)查詢記錄,以下是測試結果:

事物數 操作數 總耗時(s)
150 20 0.14578
300 10 0.20860
600 5 0.35560
3000 1 1.47742

以第一行資料為例,總共150個事物,每個事物有20個操作,這20個操作是一起發送到伺服器端的,以下是每個函數所消耗的時間:

Func:[close          ] Total:[    0.04425s] Count:[       150] 0~0.005s[       150]
Func:[execute        ] Total:[    0.09601s] Count:[       150] 0~0.005s[       150]
Func:[readTuple      ] Total:[    0.00031s] Count:[      3000] 0~0.005s[      3000]
Func:[getNdbOperation] Total:[    0.00171s] Count:[      3000] 0~0.005s[      3000]
Func:[all            ] Total:[    0.14578s] Count:[         1] 0~0.005s[         0]
           

Total表示該函數總耗時,Count表示執行的次數,最後一行記錄是總耗時。

時間主要消耗在NdbTransaction::execute()和NdbOperation::close()。

根據hash查詢,NdbTransaction::execute()函數在送出操作的同時,阻塞等待傳回的結果。

以下是測試代碼的核心部分:

void test_pk(Ndb* pNdb, int argc, char* argv[])
{
    int cnt_transaction = ;
    int cnt_query = ;
    if (argc >= )
    {
        cnt_transaction = atoi(argv[]);
    }
    if (argc >= )
    {
        cnt_query = atoi(argv[]);
    }

    const char* tableName = "test";
    const NdbDictionary::Dictionary* pDict= pNdb->getDictionary();
    const NdbDictionary::Table *pTable= pDict->getTable(tableName);
    if (pTable == NULL)
        APIERROR(pDict->getNdbError());

    Int64 id = ;
    Int32 seq = ;

    NdbOperation** operations = new NdbOperation*[cnt_query];

    extern char g_progName[];
    strncpy(g_progName, argv[], sizeof(g_progName)-);

    TPerfStatMgr::Instance().SetSwitch(true);
    PS_START(all);

    for (int i = ; i < cnt_transaction; ++i)
    {
        NdbTransaction *pTransaction = pNdb->startTransaction();
        if (pTransaction == NULL)
            APIERROR(pNdb->getNdbError());

        for (int j = ; j < cnt_query; ++j)
        {
            PS_START(getNdbOperation);
            NdbOperation *pOperation= pTransaction->getNdbOperation(pTable);
            if (pOperation == NULL)
                APIERROR(pTransaction->getNdbError());
            PS_END(getNdbOperation);

            operations[j] = pOperation;

            PS_START(readTuple);
            if (pOperation->readTuple() == -)
                APIERROR(pNdb->getNdbError());
            PS_END(readTuple);

            if (pOperation->equal("id", id) == -)
                APIERROR(pOperation->getNdbError());
            if (pOperation->equal("seq", seq) == -)
                APIERROR(pOperation->getNdbError());

            GetColumns(pTransaction, pOperation);
        }

        PS_START(execute);
        if(pTransaction->execute(NdbTransaction::NoCommit) == -)
            APIERROR(pTransaction->getNdbError());
        PS_END(execute);

        for (int j = ; j < cnt_query; ++j)
        {
            //ignore result
        }

        PS_START(close);
        pTransaction->close();
        PS_END(close);
    }

    PS_END(all);

    delete[] operations;
}

void GetColumns(NdbTransaction *pTransaction, NdbOperation *pOperation)
{
    GetColumn(pTransaction, pOperation, "id");
    GetColumn(pTransaction, pOperation, "seq");
}

void GetColumn(NdbTransaction *pTransaction, NdbOperation *pOperation, const char* column_name)
{
    const NdbRecAttr *pRecAttr = pOperation->getValue(column_name, NULL);
    if (pRecAttr == NULL)
        APIERROR(pTransaction->getNdbError());
}
           

btree

根據btree索引字段id查詢記錄,以下是測試結果:

事物數 操作數 總耗時(s)
150 20 0.77634
300 10 0.90511
600 5 0.93458
3000 1 1.54747

分析第一行測試結果(150個事物)的詳細記錄:

Func:[close                   ] Total:[    0.00571s] Count:[      3000] 0~0.005s[      3000]
Func:[nextResult              ] Total:[    0.73533s] Count:[      3000] 0~0.005s[      3000]
Func:[execute                 ] Total:[    0.01163s] Count:[       150] 0~0.005s[       150]
Func:[readTuples              ] Total:[    0.00039s] Count:[      3000] 0~0.005s[      3000]
Func:[getNdbIndexScanOperation] Total:[    0.01603s] Count:[      3000] 0~0.005s[      3000]
Func:[all                     ] Total:[    0.77634s] Count:[         1] 0~0.005s[         0]
           

時間主要消耗在NdbIndexScanOperation::nextResult()函數,NdbTransaction::execute()和NdbIndexScanOperation::close()所占的比重很小,而hash使用的NdbOperation類沒有nextResult函數,執行NdbTransaction::execute()即得到了唯一的結果。

以下是btree的測試代碼:

void test_btree(Ndb* pNdb, int argc, char* argv[])
{
    int cnt_transaction = ;
    int cnt_query = ;
    if (argc >= )
    {
        cnt_transaction = atoi(argv[]);
    }
    if (argc >= )
    {
        cnt_query = atoi(argv[]);
    }

    const char* tableName = "test";
    const NdbDictionary::Dictionary* pDict= pNdb->getDictionary();
    const NdbDictionary::Table *pTable= pDict->getTable(tableName);
    if (pTable == NULL)
        APIERROR(pDict->getNdbError());
    const NdbDictionary::Index *pIndex= pDict->getIndex("idx_test_id",pTable->getName());
    if (pIndex == NULL)
        APIERROR(pDict->getNdbError());

    Int64 id = ;

    NdbIndexScanOperation** operations = new NdbIndexScanOperation*[cnt_query];

    extern char g_progName[];
    strncpy(g_progName, argv[], sizeof(g_progName)-);

    TPerfStatMgr::Instance().SetSwitch(true);
    PS_START(all);

    for (int i = ; i < cnt_transaction; ++i)
    {
        NdbTransaction* pTransaction = pNdb->startTransaction();
        if (pTransaction == NULL)
            APIERROR(pNdb->getNdbError());

        for (int j = ; j < cnt_query; ++j)
        {
            PS_START(getNdbIndexScanOperation);
            NdbIndexScanOperation* pIndexOperation = pTransaction->getNdbIndexScanOperation(pIndex);
            if (pIndexOperation == NULL)
                APIERROR(pTransaction->getNdbError());
            PS_END(getNdbIndexScanOperation);

            operations[j] = pIndexOperation;

            PS_START(readTuples);
            //attention: not readTuple
            pIndexOperation->readTuples();
            PS_END(readTuples);

            pIndexOperation->equal("id", id);

            GetColumns(pTransaction, pIndexOperation);
        }

        PS_START(execute);
        if(pTransaction->execute(NdbTransaction::NoCommit, NdbOperation::AbortOnError, ) == -)
            APIERROR(pTransaction->getNdbError());
        PS_END(execute);

        for (int j = ; j < cnt_query; ++j)
        {
            NdbIndexScanOperation* pIndexOperation = operations[j];

            PS_START(nextResult);
            while (pIndexOperation->nextResult(true) == )
            {
                //ignore result
            }
            PS_END(nextResult);

            PS_START(close);
            pIndexOperation->close();
            PS_END(close);
        }

        pTransaction->close();
    }

    PS_END(all);

    delete[] operations;
}
           

NdbScanOperation::nextResult()函數的第一個參數表示是否到伺服器擷取記錄。如果置為true,将擷取一批資料,當本地資料周遊完,将從伺服器端再次擷取。如果第一次調用就置為false将擷取不到記錄(已測試過)。

最後一次nextResult(true)調用,會進行一次網絡互動嗎?本例中,nextResult隻會調用兩次,現在對這兩次分别統計耗時,代碼修改:

PS_START(nextResult1);
            pIndexOperation->nextResult(true);
            PS_END(nextResult1);

            PS_START(nextResult2);
            pIndexOperation->nextResult(true);
            PS_END(nextResult2);

            PS_START(close);
            pIndexOperation->close();
            PS_END(close);
           

測試結果:

Func:[close                   ] Total:[    0.00476s] Count:[      3000] 0~0.005s[      3000]
Func:[nextResult2             ] Total:[    0.62985s] Count:[      3000] 0~0.005s[      3000]
Func:[nextResult1             ] Total:[    0.09145s] Count:[      3000] 0~0.005s[      3000]
Func:[execute                 ] Total:[    0.01087s] Count:[       150] 0~0.005s[       150]
Func:[readTuples              ] Total:[    0.00031s] Count:[      3000] 0~0.005s[      3000]
Func:[getNdbIndexScanOperation] Total:[    0.01447s] Count:[      3000] 0~0.005s[      3000]
Func:[all                     ] Total:[    0.75860s] Count:[         1] 0~0.005s[         0]
           

時間主要消耗在第二次nextResult(true),本例中隻有一條記錄,第一次nextResult(true)已經取到記錄(測試過,即使有62條記錄,一次nextResult調用也可以全部取到),那麼第二次的nextResult參數傳false是不是可以減少一次網絡互動而減少耗時呢?修改代碼:

PS_START(nextResult1);
            pIndexOperation->nextResult(true);
            PS_END(nextResult1);

            PS_START(nextResult2);
            pIndexOperation->nextResult(false);
            PS_END(nextResult2);

            PS_START(close);
            pIndexOperation->close();
            PS_END(close);
           

測試結果:

Func:[close                   ] Total:[    0.63609s] Count:[      3000] 0~0.005s[      3000]
Func:[nextResult2             ] Total:[    0.00053s] Count:[      3000] 0~0.005s[      3000]
Func:[nextResult1             ] Total:[    0.09279s] Count:[      3000] 0~0.005s[      3000]
Func:[execute                 ] Total:[    0.01139s] Count:[       150] 0~0.005s[       150]
Func:[readTuples              ] Total:[    0.00032s] Count:[      3000] 0~0.005s[      3000]
Func:[getNdbIndexScanOperation] Total:[    0.01554s] Count:[      3000] 0~0.005s[      3000]
Func:[all                     ] Total:[    0.76394s] Count:[         1] 0~0.005s[         0]
           

根據結果,nextResult(false)幾乎不耗時間了,現在轉移到NdbIndexScanOperation::close(),總的性能沒有改變

猜測(待驗證):最後一次調用的是nextResult(true),進行了一次網絡互動,釋放伺服器資源,再次調用close()不需要網絡互動;最後一次調用的是nextResult(false),釋放伺服器資源的動作由close來做。

後續又做了其他的嘗試,對性能的提升,沒有實質的影響:

1. 因為隻涉及到查詢操作,是以可以隻使用一個事物(這種情況下,NdbTransaction::execute()的execType參數不能是Commit)

2. 隻使用一個NdbIndexScanOperation,不執行NdbIndexScanOperation::close(這種情況下,NdbIndexScanOperation::nextResult的fetchAllowed參數必須是true)

btree索引的查詢優化

NdbIndexScanOperation::readTuples的函數原型為:

/**
   * readTuples using ordered index
   * This method is used to specify details for an old Api Index Scan
   * operation.
   * 
   * @param lock_mode Lock mode
   * @param scan_flags see @ref ScanFlag
   * @param parallel No of fragments to scan in parallel (0=max)
   */ 
  virtual int readTuples(LockMode lock_mode = LM_Read, 
                         Uint32 scan_flags = , 
             Uint32 parallel = ,
             Uint32 batch = );
           

第一個參數的類型是枚舉LockMode,定義為:

/**
   * Lock when performing read
   */

  enum LockMode {
    LM_Read                 ///< Read with shared lock
#ifndef DOXYGEN_SHOULD_SKIP_INTERNAL
    = 
#endif
    ,LM_Exclusive           ///< Read with exclusive lock
#ifndef DOXYGEN_SHOULD_SKIP_INTERNAL
    = 
#endif
    ,LM_CommittedRead       ///< Ignore locks, read last committed value
#ifndef DOXYGEN_SHOULD_SKIP_INTERNAL
    = ,
    LM_Dirty = ,
#endif
    LM_SimpleRead =        ///< Read with shared lock, but release lock directly
  };
           

預設值

LM_Read

表示加讀共享鎖,這就解釋了為什麼最後一次

pIndexOperation->nextResult(true)

或者

pIndexOperation->close()

耗時較長了,可能是因為進行了一次網絡互動,到伺服器端将鎖的持有者數目減一。另一個枚舉值

LM_CommittedRead

符合我的需求:不加鎖,讀上次送出的資料。

修改代碼:

pIndexOperation->readTuples(NdbOperation::LM_CommittedRead);
           

再次進行測試,結果為:

耗時大幅降低了,最後一次

pIndexOperation->nextResult(true)

耗時減少。

btree索引的多程序并發查詢

如果多個程序同時查詢test表,單程序的性能降低地厲害嗎?

寫一個shell腳本

run_test1.sh

,在背景啟動10個程序通路

test

表,每個程序150個事物,每個事物20次操作:

#!/bin/sh
#file: run_test1.sh
counter=
while [ $counter -gt  ]; do
    ndbTest_test_btree   &
    counter=`expr $counter - `
done
           

以下為各程序的耗時:

總耗時(s)
TODO: 待貼出結果

總結

mysql ndb接口的查詢,支援一次執行多次操作(一次網絡互動)

按hash索引查詢,支援多次操作的結果,一次性傳回(一次網絡互動)

按btree索引查詢,每個操作自己發消息取資料,釋放資源需要進行另外一次網絡互動(此結論是根據本文測試的結果,推理出來的,不是從源碼或者官方文檔中獲得的)

Reference

  1. CREATE INDEX Syntax
  2. Comparison of B-Tree and Hash Indexes
  3. The NdbTransaction Class
  4. NdbScanOperation::nextResult()

本文連結:http://blog.csdn.net/njnu_mjn/article/details/52882402,謝絕轉載