一、业务场景
前段时间,在做CS服务化的事情,其中有一个业务场景是这样的:
CS在启动时,需要一次性向服务端请求各种地理图数据,该部分数据来源于将近200张表。起初为了方便,所有表使用同一个protobuf结构,且所有字段类型统一定义为bytes。
在120G内存服务器上的测试结果:
1、时间上:相比直接从数据库加载数据,服务化后单个CS的启动时间(将近3分钟)要超出一倍。若同时启动10个CS,所有CS全部启动完毕需3-5分钟不等。
时间都去哪了?2、内存上:单个CS启动时,Server内存最多可达2350M,10个CS同时启动时,Server内存最多超过10G。(该部分内存,在响应结束后会最终释放)
1)直接从数据库加载:sql语句的执行、执行结果的遍历、数据的最终展示。
2) 服务化后:(Server)sql语句的执行、执行结果的遍历(逐个塞入protobuf)、protobuf的序列化、报文传输;(CS)请求传输、报文的解析/反序列化、protobuf的遍历、数据的最终展示。
3)Server对请求的处理采用的是多线程形式,原则上同时启动一个或多个CS的时常应该一致,为什么实际差别显著?这与下面要将的内存有关。
内存都去哪了?
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个操作),区别在于:二、Server优化过程
前者“预先分配和动态增长的空间“是用来存储fieldData对象的地址,每次add_fielddata()时都会调用一次TypeHandler::New()来创建一个新的fieldData对象, 同时在assign()中还需为真正的数据进行内存分配;
而int32Data,“预先分配和动态增长的空间“是用来存储真正数据的地址。
1、GSoap启用Zlib压缩。优化后结果:可能是在内网测试的原因,优化效果不明显。
2、QT线程池QThreadPool。从数据的独立性上,将加载过程划分6部分,分别将6个任务添加到线程池中执行。优化后效果:性能显著提升。
相比多线程,采用QT线程池的好处:起初,这6个任务分别属于同一个类SLoadMainGISData的private成员函数,为了尽可能少改动,我们将SLoadMainGISData实例的地址作为SLoadDataTask构造函数的第一个参数, 6个private函数指针作为构造函数的第二个参数。
1)任务结束后,QT线程池会QRunnable对象的run()运行结束后,自动释放Qrunnable对象所有数据;
2)统一管理。不需要逐个线程遍历,判断线程是否执行完毕。
// 定义回调函数类型
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();
// 等待所有任务完成
注意:3、数据库连接的互斥。在步骤2中开启线程池后,必须为每个任务启用新的数据库连接,否则优化效果可能事与愿违。
SLoadDataTask构造函数第4个参数必须定义为智能指针的引用形式或指针,否则线程池结束之后,将无法获得所需要的数据。因为QT线程池会在QRunnable对象的run()运行结束后,自动释放Qrunnable对象所有数据。
4、protobuf结构调整。到目前为止,Server在内存消耗上没有任何改观。注意到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):性能无明显改观。
进一步优化,待续。
推荐阅读
- linux|linux下安装protoc-c的方法
- #|gRPC 在 Java 中的入门实例
- linux安装protobuf3.11.2
- grpc|在python中使用grpc和protobuf
- java|idea中配合protobuf使用生成Java文件
- Protobuf(一)[环境搭建]
- win10|win10下golang使用protobuf
- protobuf|使用 gitlab 实现 proto 文件的 semantic version 管理(2) - 配置篇
- 服务器|c++服务器protobuf使用