从SPserver到BRPC

开发 后端
本文是之家广告引擎团队结合工作中遇到的超时现象及问题分析过程,对服务框架升级背后的思考做出的一个总结与沉淀。

公众号转载自:汽车之家技术委员会

1.背景 

性能优化是后端服务优化的一个重要课题。尤其在广告业务中,服务超时不但会引发广告客户的预算消耗顾虑,更会直接影响C端用户的浏览体验。而一个服务程序的性能往往是覆盖了编程语言特性、业务需求逻辑,甚至是操作系统底层原理等多方面因素的综合性外在表现。面对超时问题,不论是对其进行量化分解、问题复现,还是异常监控,以及后续的超时优化,对后端开发同学而言都极富挑战。如果变换思路,转而升级后端服务框架或许会成为最为彻底的解决方案。

本文是之家广告引擎团队结合工作中遇到的超时现象及问题分析过程,对服务框架升级背后的思考做出的一个总结与沉淀。

2.问题回顾 

之家广告检索服务从建立之初采用的一直是SPserver服务框架。Spserver是一个C++实现的半同步异步的网络框架,名气虽然不大,但在当年并发编程尤其是协程概念尚不广泛的年代,相比较代码风格各异、封装独立性参差不齐的诸多自研网络框架,它确实不失为一个比较好的选择。特别是在之家广告系统搭建之初,其以良好的性能、稳定性和简便的使用方式,在引擎内部曾得到广泛应用,可以说在推进广告业务发展上,SPserver立下了汗马功劳。

随着需求的不断迭代和升级,广告系统运行中碰到的问题也越来越凸显,包括:内存消耗、并发、超时、生态融合等等,其中又以超时问题为重。

说起超时,恐怕会让人抓狂,因为它来的太突然而且几乎不留任何痕迹。没错,监控可以抓到它,但问题是通过分析全链路日志你可能不会有任何收获,因为你所划分的各个业务逻辑阶段的耗时并没有超过预先设定的超时阈值,而且进程的CPU利用率也不高(可以说很低),让人诡异的是上游请求方的超时却是实实在在发生了的。

3.问题分析 

应用服务不是孤立的,它所发生的问题也应该是有关联的。

图片

(图1:来自网络)

通过图1知道,一个业务应用在处理网络请求之前以及发送网络应答之后,数据会流经网络、系统内核。那么,超时会不会是由它们引起的呢?为此,我们通过压测环境复现了超时发生的系统上下文场景:

图片

(图2)

可以发现,业务应用与其上下游之间经由多个tcp连接通信,而部分tcp连接在对应socket的接收缓冲区和发送缓冲区却存在着数据积压的现象,且两个队列的积压数据量分别固定在4xx和2xx。

不同于grpc倡导的单通道通信模式,spserver原生支持多tcp连接服务,这点无可厚非。基于tcp协议的拥塞控制机制,网络数据进入应用层前,会在操作系统内核态的fd缓冲区稍作停留,因此短时间内存在一定量的数据积压也是正常的;既然是缓冲区,自然有大小之分。那么,会不会是缓冲区太小或者网络拥堵导致的超时呢?又该如何判断呢?

图片

(图3)

可见,系统对socket收发缓冲区的默认值都在80k以上,远大于图2中的431字节。而且spserver源代码中也没有通过setsockopt系统调用对socket的收发缓冲区重新设值。所以,图2中缓冲区大小是合理的,而其中接收队列中的数据量431则可能是有问题的(实际上,431是压测场景中单次请求的数据长度;在生产环境中,这个队列长度会随着C端请求的不同而呈现不同的值,毫无规律可言)。

连续刷新netstat,对比目标tcp连接的发送缓冲队列或接收队列长度,发现此场景下收发队列的长度并不能立即消失,也没有减小,而是持续了1秒左右才有所变化。由此可以推测2种可能:1)网络拥堵;2)服务端cpu繁忙,不能及时将数据从缓冲区读走。第一种猜测通过网络检测工具很容易验证,是否定的;第二种猜测,也通过查看系统以及进程cpu的利用率也很容易排除掉。分析排查到此,似乎走到了死胡同。真的是这样吗?

有一个细节似乎被忽略了:作为多线程服务,进程的cpu利用率≈线程cpu利用率之和,但进程cpu利用率低并不意味着某些或某个线程cpu利用率也低。换句话说,个别线程所在cpu的高利用率同样会使上面第二种猜测成立。

图片

(图4)

如图4,反复施加不同量级的请求压力,会发现除了18194号线程的cpu使用率保持在99%以上,其他线程的cpu使用率最大都不过40%左右。面对高压力,服务不能平摊cpu压力,这就是问题!

4.根本原因 

上帝总是吝啬造就一个十全十美的东西,对待SPserver自然也不例外。其实排查走到线程的cpu利用率时,已经接近真实原因,但还不是真相。为此,撸了一遍spserver的源码,得出如下线程模型示意图。

图片

(图5)

不出所料,SPserver采用的是单线程reactor网络模型,即单线程负责事件监听、socket读写,多线程负责业务逻辑处理。单线程io的优劣显而易见,可以很好的利用thread local加速,也没有cpu cache bounding的问题;但问题是一旦某个socket上待读取或待发送的数据量较大时,就会阻塞其他socket上数据的收发,这就比较致命。退一步讲,即便每个socket fd上的数据量大小均匀,在上述单线程的cpu吃满时,整个框架的数据收发效率同样会成为所在服务的性能瓶颈。

真相大白了,原来SPserver框架的设计机制决定了它不适合高并发、高吞吐业务场景的事实。但如果是在业务初期又或是业务流量比较小,个人觉得它仍可视为一个不错的选择(SPserver框架的c++代码风格还是很不错的,很简洁,封装性也很好,值得借鉴)。

5.选择BRPC 

如今rpc框架林立,我们的选择是百度研发的brpc框架,主要是基于以下考虑:

1)高并发高性能

个人理解后面会着重介绍一下。这里我只贴一个brpc和其他rpc框架性能的对比图:

图片

(图6:来自brpc官网)

是的,你没有看错,在跨机多client请求单server的场景下,brpc框架的性能已经绝对领先国外知名的rpc框架,尤其是grpc,用碾压一词来形容也不为过。

2)文档资料丰富

brpc有着丰富的中英文文档,丰富程度让人有点难以置信,曾一度有人认为是百度内部的技术资料无意中被公开了,呵呵。

3)apache 的顶级项目,目前有数千个企业级应用

说直白点,已经在生产中经历过千锤百炼了,质量有保证。

当然,brpc还有诸多特性,这里不再赘述。详见brpc官网或移步到github incubator-brpc项目。

6.性能初探 

经历了spserver框架,免不了要有一番对比。在介绍brpc的线程模型前,先了解一个bprc中的概念:bthread。我们看一下官方的解释:

“bthread是brpc使用的M:N线程库,目的是在提高程序的并发度的同时,降低编码难度,并在核数日益增多的CPU上提供更好的scalability和cache locality。”M:N“是指M个bthread会映射至N个pthread,一般M远大于N。由于linux当下的pthread实现[NPTL]是1:1的,M个bthread也相当于映射至N个[LWP]。bthread的前身是Distributed Process(DP)中的fiber,一个N:1的合作式线程库,等价于event-loop库,但写的是同步代码。”

个人理解bthread其实是一个运行在系统pthread之上的可以低成本、灵活调度的任务(队列),而这个任务载有自身运行时的上下文信息(比如:栈、寄存器、signal等),使它能够随意切换在不同的pthread上运行。其低成本体现在几个方面:1)bthread实现了多种同步原语,可以和系统线程实现相互等待,我们可以像使用pthread一样使用bthread;2)纳秒级建立bthread,耗时远低于pthread;3)几乎没有上下文切换,且将cpu cache locality和thread local发挥到了极致,这部分后面会有实践说明。

下面再来看看它的线程模型示意图:

图片

(图7)

由图7可见:1)在brpc框中系统级线程池pthread是高效运行的基础设施,不再与具体的业务逻辑直接绑定,取而代之的是bthread;2)bthread按不同责任分成不同类型,且不同类型的bthread有着不同的数量。比如:网络事件的监听和驱动由一个bthread专职处理,当然也可以通过命令行启动服务时配置,或者服务启动后通过web入口更新;而处理具体请求的bthread数量则是动态计算的;3)brpc支持不忙的线程“偷”繁忙线程的bthread任务来提升系统整体效能。

那么问题来了,某一个socket fd上大量数据的接收或发送,会发生类似spserver那样的阻塞吗?答案是不会。首先,每个fd有两个bthread,分别负责接收和发送,这就能保证收发互不影响;其次,bthread作为被调度的任务,会被分派给不同系统线程(也就是pthread),而一个系统线程同一时刻只能执行一个bthread任务,加上bthread 支持的stealing机制,就保证了进程中所有线程都是有事可做的,不会存在空闲的pthread(除非请求量非常小,不足以给pthread平分)。因此,基本上是不可能发生“一处阻塞处处阻塞”的情况的。

使用更大量的压测请求对brpc服务发压,得到各pthread的cpu消耗如下:

图片

(图8)

由图8可见,系统的多核cpu得到充分的调动,且随着压力的增大cpu的扩展性表现良好;再来看一下网络队列:

图片

(图9)

可见,(同机grpc单通道压测,仅有一个tcp连接)tcp fd的收发缓冲区得到了充分的利用,且队列长度很快能减少至0。

Socket缓冲区不再是摆设,整个系统活了起来!

另外,值得一提的是线程间的上下文切换。因为过多的上下文切换,会把cpu时间消耗在寄存器、线程栈的保存和恢复上,从而降低服务的整体性能。brpc框架的m:n线程库,在这方面做的比较好,它使用固定的系统线程调度运行大量用户态的bthread,将所有的切换基本上都限制在了用户态,这就避免了内核态和用户态的数据交换(用户态之间切换耗时在100~200ns,而内核态和用户态切换则在微秒级)。通过命令也可以证明这一点:

图片

(图10)

如图10,我们的服务在使用brpc后上下文切换频次基本保持在每秒1次。再回头来看看spserver框架,由于没有用户态任务的概念,只是单纯依赖系统级的线程池,就不可避免的使cpu不断地游离于多线程调度和任务执行上,内核态和用户态间的上下文切换开销肯定少不了:

图片

(图11)

用户态线程切换的另一个好处是,可以将内核态线程与cpu核心很好的绑定到了一起,这就能够尽量避免cpu不同核心间cacheline的数据同步,从而提升性能,这也是brpc框架高性能的一个原因。

7.应用实践 

brpc的编译安装及基本使用,在官方文档都有较为详细的说明,比较简单。这里再分享一下我们在brpc应用过程中遇到过的几个值得注意的地方:

1)thread local.

它是多线程程序常用的加速手段。比如tcmalloc就充分利用了这个技术,通过在线程内部设置局部缓存来加速小额空间的申请效率。之家广告引擎服务毫无例外的也使用了这项技术。但需要注意的是,在引入brpc框架后,原有的pthread id可能将不再有效,如果你执意为之,就可能会在程序运行期间遇到莫名其妙的段错误。这是因为我们的业务代码是托管在bthread中的,而bthread是在系统pthread之间随机游走的,使用pthread1的标识信息到pthreadN的线程栈中读取缓存数据自然是读不到的。我们的临时解决办法是,暂时剥离掉thread local缓存,在控制锁粒度的前提下改为全局cache,暴力、简单、有效。

2)cpu profiler

顾名思义,你的程序可能会因此得以优化并加速运行。但事实并非完全如此。如果你们业务程序的CMakeList来自某个demo或网络程序,则最好要注意这个编译选项。它的原理是通过调用对应的库函数采集活跃线程中的线程函数信息,并根据栈体现的函数调用关系生成调用图,进而进行调用优化。所以,它会加速但不是立即,因为它要先采集数据、再分析,最后才能优化运行。我们实践初期,肉眼可见其性能相比较不加此编译选项要低10%以上,所以,对待cpu profiler要慎用。

3)关于grpc

前面提到过,gprc默认是基于单通道的通信方式,这是既是google官方的建议也是微软的实践建议。下面截图来自微软的” Performance best practices with gRPC”一文:

图片

(图12:截自微软官网)

不能总是人云亦云。结合具体业务场景,我们的实践结论是:多通道数据传输效率要优于单通道。受限于tcp速率限制,单通道(连接)情况下,一旦遇到高吞吐的数据传输业务场景,会明显阻塞此网络连接。特别对于广告业务来说,我们允许有大数据块传输,但不允许大数据块的传输影响其他正常的广告数据响应。

因此,我们的建议是:要么使用多通道grpc或者其他协议方式,要么就放弃grpc吧(事实上,grpc在生产中还有其他问题)。

4)并发

早在spserver时期,我们曾在其中实现了并发线程库(确切说是一个并发线程类),但效果并未达到预期,因为它一定程度上加重了多线程调度成本。而如今的brpc则直接提供了较为简单的并发线程 api,我们可以直接使用,无需造轮。

然而,会面临新的选择:用bthread_start_background,还是用bthread_start_urgent。使用后者启动bthread后会在当前pthread立即执行任务,而前者则会让新生成的bthread任务排队等待调度。在我们的广告检索过滤场景中,适合使用后者;而在执行http请求时,则更适合使用前者。建议brpc开发者,一定要根据自己业务的实际情况再做决定。

8.最后 

通过从SPserver框架升级到BRPC框架,在相同的业务场景下,之家广告服务qps从5w+提升到了10w左右,服务实例数也因此下降了一半以上,收益明显。

另外,Brpc提供了相对丰富的内置服务,这里贴两个具有代表性功能的web界面,都比较实用,推荐大家尝试。

图13:我们可以看到服务的qps、latency分布等数据,方便把握服务的运行时信息。

图片

(图13)

而从下图,可以看到服务运行期间在等待锁上所消耗的时间及发生等待的函数,从而支持我们有针对性的开展性能优化。

图片

(图14)

note

限于作者水平,难免会有理解和描述上的疏漏或者错误,欢迎共同交流、指正。文章供于学习交流,转载注明出处,谢谢!

作者简介

图片

汽车之家

主机厂事业部-技术部

杨明哲

2018年加入汽车之家,目前任职于主机厂事业部-技术部-广告技术及系统团队,负责之家广告引擎架构的设计与研发等相关工作。

图片

责任编辑:武晓燕 来源: 汽车之家技术委员会
相关推荐

2023-11-16 21:20:13

ListWatchkube

2021-03-03 08:18:54

Service组件

2022-05-09 08:35:43

面试产品互联网

2015-09-17 13:09:48

预装软件毒瘤国产手机

2013-06-06 13:42:48

OSPF入门配置

2023-10-12 15:38:50

FreeDOS命令

2017-06-26 09:15:39

SQL数据库基础

2022-09-04 21:46:12

数据信息风险

2013-04-07 10:10:23

2010-02-06 15:31:18

ibmdwAndroid

2009-07-22 14:55:16

ibmdwAndroid

2021-02-21 22:53:01

CanvasHTML5JavaScript

2016-11-28 16:23:23

戴尔

2010-12-22 12:00:48

软件保护软件授权

2019-07-02 14:17:18

API网关网关流量

2012-12-10 13:24:15

回归分析数据挖掘

2023-10-18 18:32:07

2023-12-27 06:48:49

KubernetesDevOpsHTTP

2020-10-27 06:39:14

智慧城市智慧旅游物联网

2016-12-08 22:39:40

Android
点赞
收藏

51CTO技术栈公众号