基於GSoap/protobuf的服務性能優化

一、業務場景

前段時間,在做CS服務化的事情,其中有一個業務場景是這樣的:
CS在啓動時,需要一次性向服務端請求各種地理圖數據,該部分數據來源於將近200張表。起初爲了方便,所有表使用同一個protobuf結構,且所有字段類型統一定義爲bytes。
在120G內存服務器上的測試結果:
1、時間上:相比直接從數據庫加載數據,服務化後單個CS的啓動時間(將近3分鐘)要超出一倍。若同時啓動10個CS,所有CS全部啓動完畢需3-5分鐘不等。

時間都去哪了?
1)直接從數據庫加載:sql語句的執行、執行結果的遍歷、數據的最終展示。
2) 服務化後:(Server)sql語句的執行、執行結果的遍歷(逐個塞入protobuf)、protobuf的序列化、報文傳輸;(CS)請求傳輸、報文的解析/反序列化、protobuf的遍歷、數據的最終展示。
3)Server對請求的處理採用的是多線程形式,原則上同時啓動一個或多個CS的時常應該一致,爲什麼實際差別顯著?這與下面要將的內存有關。

2、內存上:單個CS啓動時,Server內存最多可達2350M,10個CS同時啓動時,Server內存最多超過10G。(該部分內存,在響應結束後會最終釋放)

內存都去哪了?
protobuf將repeated標記的字段劃分爲對象類型(諸如message 、Bytes、string等)和原始類型(諸如int32、int64、float)兩類。
protobuf在兩類的處理方式上存在顯著差別。

// -------------------定義DBRecordData
message DBRecordData
{
    repeated bytes              fieldData                   =       1;
    repeated int32              int32Data                   =       2;
}

// -------------------fieldData中新增一個元素
inline void DBRecordData::add_fielddata(const void* value, size_t size) 
{
    // Add()先獲取一個fieldData指針 然後再進行內存分配、數據拷貝
    fielddata_.Add()->assign(reinterpret_cast<const char*>(value), size);
}

template <typename Element>
inline Element* RepeatedPtrField<Element>::Add() 
{
    return RepeatedPtrFieldBase::Add<TypeHandler>();
}

template <typename TypeHandler>
inline typename TypeHandler::Type* RepeatedPtrFieldBase::Add() 
{
    // 1、檢查數組elements_中當前有效的元素個數和已分配空間的元素個數
    // current_size_:數組elements_當前有效的元素個數
    // allocated_size_:數組elements_當前已分配空間的元素個數
    //
    // 此處之所以要進行檢查,是因爲:RepeatedPtrFieldBase::RemoveLast()中
    // 存在操作“TypeHandler::Clear(cast<TypeHandler>(elements_[--current_size_]))”
    // 即釋放數組elements_最後一個元素的數據,但並不會空間進行回收
    if (current_size_ < allocated_size_) 
    {
        return cast<TypeHandler>(elements_[current_size_++]);
    }

    // 2、數組elements_無空閒位置時 以兩倍增長elements_的size
    if (allocated_size_ == total_size_) Reserve(total_size_ + 1);

    // 3、新建一個fieldData對象 將對象指針存入數組elements_
    typename TypeHandler::Type* result = TypeHandler::New();
    ++allocated_size_;
    elements_[current_size_++] = result;
    return result;
}

// -------------------int32Data中新增一個元素
inline void DBRecordData::add_int32data(::google::protobuf::int32 value) 
{
    int32data_.Add(value);
}

template <typename Element>
inline void RepeatedField<Element>::Add(const Element& value) 
{
    // 1、判斷數組中是否有空閒位置 以容納值value
    // current_size_:數組elements_中已佔用的元素個數
    // total_size_:數組elements_的總大小
    if (current_size_ == total_size_) Reserve(total_size_ + 1);

    // 2、將值value存入數組
    elements_[current_size_++] = value;
}

template <typename Element>
void RepeatedField<Element>::Reserve(int new_size) 
{
    if (total_size_ >= new_size) return;

    Element* old_elements = elements_;

    // 1、重新分配內存 數組elements_的size以原大小的2倍增長
    total_size_ = max(google::protobuf::internal::kMinRepeatedFieldAllocationSize,
                    max(total_size_ * 2, new_size));
    elements_ = new Element[total_size_];

    // 2、將數組elements_內的舊數據拷貝到新內存 並釋放舊空間
    if (old_elements != NULL) 
    {
        MoveArray(elements_, old_elements, current_size_);
        delete [] old_elements;
    }
}

fieldData與int32Data都會預先分配空間和動態增長空間(這個過程包括內存的重新分配,把舊數據拷貝到新內存,再釋放舊內存3個操作),區別在於:
前者“預先分配和動態增長的空間“是用來存儲fieldData對象的地址,每次add_fielddata()時都會調用一次TypeHandler::New()來創建一個新的fieldData對象, 同時在assign()中還需爲真正的數據進行內存分配;
而int32Data,“預先分配和動態增長的空間“是用來存儲真正數據的地址。

二、Server優化過程

1、GSoap啓用Zlib壓縮。優化後結果:可能是在內網測試的原因,優化效果不明顯。

2、QT線程池QThreadPool。從數據的獨立性上,將加載過程劃分6部分,分別將6個任務添加到線程池中執行。優化後效果:性能顯著提升。

相比多線程,採用QT線程池的好處:
1)任務結束後,QT線程池會QRunnable對象的run()運行結束後,自動釋放Qrunnable對象所有數據;
2)統一管理。不需要逐個線程遍歷,判斷線程是否執行完畢。

起初,這6個任務分別屬於同一個類SLoadMainGISData的private成員函數,爲了儘可能少改動,我們將SLoadMainGISData實例的地址作爲SLoadDataTask構造函數的第一個參數, 6個private函數指針作爲構造函數的第二個參數。

// 定義回調函數類型
typedef bool(SLoadMainGISData::*pMainGISCallBack)(/* 回調函數的參數列表 */);

// ---------------------------------------
// 定義SLoadDataTask,表示要放入線程池的任務
class SLoadDataTask : public QRunnable
{
public:
    /// @param  pLoadMainGISData    [in]    SLoadMainGISData實例的指針
    /// @param  pCallBack           [in]    回調函數
    /// @param  pSLoadDBDataParam   [in]    加載數據需要的參數
    /// @param  pTaskDBInfo         [out]   加載的數據
    SLoadDataTask(SLoadMainGISData *pLoadMainGISData, 
    pMainGISCallBack pCallBack, 
    SLoadDBDataParam *pSLoadDBDataParam,        
    QSharedPointer<LoadDBDataProtobuf::ClassGAndNetData> &pTaskDBInfo);

......

}   

// ---------------------------------------
// 將任務添加至線程池

QThreadPool pQThreadPool;
pQThreadPool.setMaxThreadCount(6); 

// 跟線程類似,也存在run()
// 並可通過“pQThreadPool.start(pLoadGadgetsTask)”方式,將任務添加到線程池中
SLoadDataTask *pLoadKnotsTask = 
new SLoadDataTask(this, &SLoadMainGISData::_loadKnots, pSLoadDBDataParam, pKnots_DBInfo);
pQThreadPool.start(pLoadKnotsTask);

......

pQThreadPool.waitForDone(); // 等待所有任務完成

注意:
SLoadDataTask構造函數第4個參數必須定義爲智能指針的引用形式或指針,否則線程池結束之後,將無法獲得所需要的數據。因爲QT線程池會在QRunnable對象的run()運行結束後,自動釋放Qrunnable對象所有數據。

3、數據庫連接的互斥。在步驟2中開啓線程池後,必須爲每個任務啓用新的數據庫連接,否則優化效果可能事與願違。

4、protobuf結構調整。到目前爲止,Server在內存消耗上沒有任何改觀。注意到protobuf使用手冊上有如下說明:

這裏寫圖片描述

5、包拆分。由於公司的CS通常只對外發布32位版本,而32位CS在一次性反序列化整個protobuf時,string內部會由於new()失敗而拋出異常。於是決定,將整個protobuf包拆分成若干個小包,CS每次在反序列化一個小包之後,立即將不需要的內存釋放。

三、優化前後效果對比

1、優化前:CS啓動時長:84秒,內存增長:1100M;

2、行(單線程):CS啓動時長:3分鐘左右,報文大小(壓縮前353M,壓縮後58M),單個CS啓動時,Server內存增長2350M,10個CS啓動時,Server內存增長超過10G;

3、列(線程池):CS啓動時長:50秒左右,報文大小(壓縮前281M,壓縮後58M),單個CS啓動時,Server內存增長1300M,10個CS啓動時,Server內存增長約3.5G;

4、列(線程池,Reserve):性能無明顯改觀。


進一步優化,待續。

發佈了69 篇原創文章 · 獲贊 20 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章