一、業務場景
前段時間,在做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):性能無明顯改觀。
進一步優化,待續。