基于 SPP 模块的优化实践

作者:袁浩

导语

SPP框架的微线程模式在网络密集型Server开发中优势明显,用同步的方式写异步的代码真的很爽。QQ消息系统这边目前也有若干模块都在使用SPP框架,新增模块也首选SPP。在使用过程中,也遇到过一些性能问题,下面跟大家分享下解决思路。

proxy的性能瓶颈

SPP是单proxy + 多worker架构,随着CPU的核心数越来越多,M1是24核心,M10是48核心,为了充分利用机器的计算资源,就必须扩展越来越多的worker,而proxy只能有一个,所以proxy会成为业务瓶颈。下面来聊聊怎么解决proxy瓶颈。

1. worker代替proxy回包

图:创建msg的时候,设置来源地址

图:回包时,由worker直接回包,并调用SendToClient断开proxy连接

2. 优化proxy路由函数spp_handle_route

一般来讲,proxy的路由函数只需随机选一个worker保持worker负载均衡即可。对于某些业务,需要对uin分shard处理,则proxy需要解析出请求的uin来计算shard。以oidb协议为例,对于proxy只关心uin和command,就可以把其他字段删除,用简化版的oidb head,其他字段在PB解析时,则直接放到unknown字段(PB的解析可以参考PB解析原理)。如果使用的PB版本支持lazy字段就更简单了,把不需要的字段设置lazy选项,proxy和worker就可以使用同样的协议,同时保持proxy的高效了。

图:proxy和worker协议对比

下图是我们群系统消息存储模块的CPU占用情况,单proxy CPU占用率25%,而每个worker则最多只占了14%。这是在经过1,2方法优化之后,优化前proxy CPU占用35%以上,差不多是每个worker的3倍。

图:单proxy + 23个worker的CPU占用情况

3. 绕过proxy,worker直接监听收包

如果以上方法仍然不能解决proxy的瓶颈,那么可以绕过proxy,由worker直接监听收包。这样,既解决proxy瓶颈问题,也减少了大量内存拷贝和共享队列锁的抢占,一举多得。可参考thomas同学的文章《一种SPP性能改良方法》

图:spp_handle_init启动监听微线程

图:监听函数处理收包,并创建微线程和msg处理请求

不过这种方式,有一个不爽的地方就是不能批量监听端口,SPP没有提供mt_select方法,因为微线程底层的就是用select来实现的。

这种去proxy化,有两个弊端:

a. SPP的proxy具有防雪崩的设计,去proxy就意味着没有防雪崩; b. proxy和worker之间的共享队列,可以缓存请求,在模块发布时,使用热重启,可以减少甚至避免丢包。去proxy化,重启进程必然会丢失请求; c. 同group下的worker在proxy + worker模式下,具有容灾功能,即一个worker挂了,同一group下其他worker可以顶上;在去proxy化的情况下,注意同一端口多worker共同监听。 总之,去proxy化慎用。

worker性能优化

1. 缓存action等对象

使用SPP框架时,处理每个msg,难以避免会new很多对象出来,最明显的就是action的创建,甚至有时候,一个msg请求,就会有数个action的创建,内存new和delete消耗了大量的性能。由于同类action有大量的相同信息,我们能不能把action缓存起来,每次需要变化的东西,重新传入?

图:对象池类

需要缓存的对象,只需继承CObjectPool即可。使用智能指针操作对象,在智能指针释放时,则自动调用FreeObj,把对象放回对象池。每个对象都有自己的对象池,使用者不必关心对象池的存在,也不用自己释放对象,简单易用,居家旅行必备。其他类似的对象,都可以用这种方式进行优化。

图:对象池使用方法

2. 缓存msg,由用户自己管理msg,而不是托管给框架

既然action可以缓存,那么msg可不可以呢?答案是肯定的,msg相对复杂,每个msg占用的内存可能达到几k或几十k以上,不用重复创建和释放,肯定能得到更大的收益。但有以下几个问题: a. msg比较复杂,里面脏数据比较难以控制; b. msg是由用户创建,spp框架释放,我们怎么回收到对象池中?

第一个问题,定义reset方法,由对象池调用,清空脏数据; 第二个问题,看过SPP源码的同学可能知道,框架处理msg其实只是调用msg->HandleProcess, 然后delete msg;那我们自己启动微线程,处理msg即可。

那么带来另一个问题,智能指针对象无法通过微线程函数传递;我们搞一个裸的对象池类,不使用智能指针:

图:msg的对象池类, msg类直接继承即可

图:启动微线程处理msg

图:创建msg智能指针时,设置删除器,删除器的作用是把msg放归对象池

3. 避免socket的重复创建

看spp源码的时候发现,mt_udpsendrcv的实现是,不断创建和释放socket,根据缓存的思想,那么我们可不可以把socket缓存起来呢?把socket收归到action里面,缓存action的同时,socket也会被缓存起来。

图:spp的sendrecv实现

接下来自己实现一个sendrecv函数。

图:自己实现一个sendrecv

别急还没完:测试发现,缓存socket的方式是有问题的。假设msg A使用一个socket发出去一个请求,恰好下游超时,A处理完毕后,msg B又通过这个socket发出去请求,从该socket的读到的就是A的脏数据。这样形成错位效应,后面所有请求读到的都是脏数据。怎么解决呢?

串包校验,请求和回复不符的,继续处理socket里面的后续数据:

图:解决脏数据问题

图:优化前

图:优化后

利用以上worker优化方法1, 2,3对我们消息上行模块进行优化,优化前单worker压测值为325k/min,优化后压测值385k/min,大约提升18%。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大宽宽的碎碎念

为什么DB连接管理一般不采用IO多路复用?

2818
来自专栏desperate633

深入理解Redis的Set类型的使用及应用

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。 Redis 中 集合是通过哈希表实现的,所以添加,删除...

801
来自专栏JAVA高级架构

阿里和vivo面试题目汇集 【文末有福利】

1202
来自专栏企鹅号快讯

PHP7.2有哪些新特性?

我们知道php被称为“世界最好的语言“,可见人们对其是又爱又恨。我是其中一位开发者,但我对php是绝对地喜爱。我对php 了如指掌。自从php7.2发布以来,我...

3479
来自专栏Java技术

一步步带你了解ID发号器是什么、为什么、如何做!

上一篇文章《面试必备:如何将一个长URL转换为一个短URL?》中谈到如何将长地址URL转换为短地址URL,其中谈到了一个比较理想的解决方案就是使用发号器生成一个...

982
来自专栏张善友的专栏

SQL Server 2008 压缩

执行SQL查询时,主要的几个瓶颈在于:CPU运算速度、内存缓存区大小、磁盘IO速度。而对于大数据量数据的查询,其瓶颈则一般集中于磁盘IO,以及内存缓存。那么为了...

18710
来自专栏开发与安全

socket 请求接收完整的一个http响应(设置recv 接收超时选项SO_RCVTIMEO)

在前面的系列网络编程文章中,我们都是使用socket 自己实现客户端和服务器端来互相发数据测试,现在尝试使用socket 客户端发 送http 请求给某个网站,...

6230
来自专栏魏琼东

基于DotNet构件技术的企业级敏捷软件开发平台 - AgileEAS.NET平台开发指南 - 数据层开发

对象关系映射          AgileEAS.NETORM并没有采用如NHibernate中映射文件的文件的模式,而是采用了直接硬编码的模式实现,ORM体系...

2049
来自专栏犀利豆的技术空间

Redis 数据库、键过期的实现

之前的文章讲解了 Redis 的数据结构,这回就可以看看作为内存数据库,Redis 是怎么存储数据的以及键是怎么过期的。

1062
来自专栏小黄人打代码

在Java中如何解析JSON格式数据?

1055

扫码关注云+社区