基于 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 条评论
登录 后参与评论

相关文章

来自专栏落影的专栏

iOS电商类APP的研发

前言 本文是研发一个在线超市的电商类APP过程中,对架构的整理。 ? 功能: 1、浏览商品、购买商品、切换商店; 2、查看订单、订单投诉、意见反馈; 3、...

67310
来自专栏阮一峰的网络日志

处理Apache日志的Bash脚本

去年一年,我写了将近100篇网络日志。 现在这一年结束了,我要统计"访问量排名",看看哪些文章最受欢迎。(隆重预告:本文结尾处将揭晓前5名。) ? 以往,我用的...

3535
来自专栏Golang语言社区

Golang-简洁的并发

转载原文:http://www.yankay.com/go-clear-concurreny/ 多核处理器越来越普及。有没有一种简单的办法,能够让我们写的软件释...

3634
来自专栏游戏杂谈

关于seajs

虽然已经有很长时间没写JavaScript,但很多时候看到一些应用还是会带着好奇心去研究一下。之前是看腾讯的朋友网,它的webchat做的很不错(虽然ff下有b...

1863
来自专栏JackieZheng

Nodejs学习笔记(四)——支持Mongodb

前言:回顾前面零零碎碎写的三篇挂着Nodejs学习笔记的文章,着实有点名不副实,当然,这篇可能还是要继续走着离主线越走越远的路子,从简短的介绍什么是Nodejs...

2055
来自专栏芋道源码1024

从客户端的角度设计后端的接口

2.请求Path,http://www.online.com/api/ [path]

1323
来自专栏PHP在线

2018最新PHP学习路线整合

PHP是一种通用开源脚本语言。语法吸收了C语言、Java和Perl的特点,利于学习,使用广泛,主要适用于Web开发领域。

2385
来自专栏ChaMd5安全团队

DedeCMS的两个小trick

0x00 前言 昨天晚上做了一个神奇的梦,梦到了我高中时候晚自习在偷偷的看《黑客攻防技术宝典》,当年的事情无论是苦是乐,回忆起来总是感觉非常的美好。但是,现实就...

3009
来自专栏Java帮帮-微信公众号-技术文章全总结

大文件拆分方案的Java实践【面试+工作】

大文件拆分问题涉及到io处理、并发编程、生产者/消费者模式的理解,是一个很好的综合应用场景,为此,花点时间做一些实践,对相关的知识做一次梳理和集成,总结一些共性...

1924
来自专栏杨建荣的学习笔记

如果理解Python web开发技术

首先来问一个问题,如何来看待Python web开发技术?如果不知道如何回答,我们换个问题:如何理解Python web的本质,这个我先用了三个程序来说明。 首...

3654

扫码关注云+社区