GSoap|基于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 bytesfieldData=1; repeated int32int32Data=2; }// -------------------fieldData中新增一个元素 inline void DBRecordData::add_fielddata(const void* value, size_t size) { // Add()先获取一个fieldData指针 然后再进行内存分配、数据拷贝 fielddata_.Add()->assign(reinterpret_cast(value), size); }template inline Element* RepeatedPtrField::Add() { return RepeatedPtrFieldBase::Add(); }template inline typename TypeHandler::Type* RepeatedPtrFieldBase::Add() { // 1、检查数组elements_中当前有效的元素个数和已分配空间的元素个数 // current_size_:数组elements_当前有效的元素个数 // allocated_size_:数组elements_当前已分配空间的元素个数 // // 此处之所以要进行检查,是因为:RepeatedPtrFieldBase::RemoveLast()中 // 存在操作“TypeHandler::Clear(cast(elements_[--current_size_]))” // 即释放数组elements_最后一个元素的数据,但并不会空间进行回收 if (current_size_ < allocated_size_) { return cast(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 inline void RepeatedField::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 void RepeatedField::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: /// @parampLoadMainGISData[in]SLoadMainGISData实例的指针 /// @parampCallBack[in]回调函数 /// @parampSLoadDBDataParam[in]加载数据需要的参数 /// @parampTaskDBInfo[out]加载的数据 SLoadDataTask(SLoadMainGISData *pLoadMainGISData, pMainGISCallBack pCallBack, SLoadDBDataParam *pSLoadDBDataParam, QSharedPointer &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使用手册上有如下说明:
GSoap|基于GSoap/protobuf的服务性能优化
文章图片

【GSoap|基于GSoap/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):性能无明显改观。
进一步优化,待续。

    推荐阅读