前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于 SPP 模块的优化实践

基于 SPP 模块的优化实践

原创
作者头像
serena
修改2021-08-03 14:56:08
2.1K0
修改2021-08-03 14:56:08
举报
文章被收录于专栏:社区的朋友们社区的朋友们

作者:袁浩

导语

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%。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导语
  • proxy的性能瓶颈
    • 1. worker代替proxy回包
      • 2. 优化proxy路由函数spp_handle_route
        • 3. 绕过proxy,worker直接监听收包
        • worker性能优化
          • 1. 缓存action等对象
            • 2. 缓存msg,由用户自己管理msg,而不是托管给框架
              • 3. 避免socket的重复创建
              相关产品与服务
              负载均衡
              负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
              http://www.vxiaotou.com