基于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::Add() 
{
    return RepeatedPtrFieldBase::Add();
}

template <typename TypeHandler>
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 <typename Element>
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 <typename Element>
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:
    /// @param  pLoadMainGISData    [in]    SLoadMainGISData实例的指针
    /// @param  pCallBack           [in]    回调函数
    /// @param  pSLoadDBDataParam   [in]    加载数据需要的参数
    /// @param  pTaskDBInfo         [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/protobuf的服务性能优化_第1张图片

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):性能无明显改观。


进一步优化,待续。

你可能感兴趣的:(GSoap,protobuf)