微服务架构之自实现RPC

Posted 数天技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务架构之自实现RPC相关的知识,希望对你有一定的参考价值。

作者介绍
覃波: 数字天空《茜色》项目组服务器工程师

导语

《茜色的世界中与君咏唱》(以下简称茜色)是由数字天空与日本知名游戏厂商GCREST株式会社针合作推出的一款针对国内玩家优化的女性向恋爱养成游戏,原作《茜さすセカイでキミと咏う》是知名日系手游《梦王国与100位王子殿下》开发团队的最新力作,台湾、日本女性玩家皆给与该游戏高度的好评。目前茜色已经完成封测,正在进行性能的优化以及功能的完善。本文主要是对在项目功能完善的过程中遇到的架构限制以及解决过程的整理。


服务器框架

服务器由多个模块组成,每个模块负责不同的功能。模块主要分为两类:一类是服务器框架的基础模块,比如日志模块、数据库模块等;另一类是业务逻辑模块,比如兑换模块、关卡模块等。最后这些模块都被编译成动态库,根据配置加载到服务器中提供功能。


遇到的问题

随着新功能的不断增加以及对原有功能的多次调整,游戏内各个功能需要操作的数据越来越多,使用到的模块也相应的增加,模块之间的耦合度也越来越高,导致对模块进行改动和维护也越来越难。比如在游戏结束时的战斗结算功能,需要经过战斗模块和关卡模块处理,而这两个模块同时又使用了其他很多的模块,这些模块中分别操作了与玩家相关的不同数据:

1) 属性数据:通关消耗和战斗结算奖励等;

2) 队伍数据:获取出战队伍卡牌ID的列表等;

3) 卡牌数据:增加出战卡片的技能经验以及通关给与新的卡牌等;

4) 道具数据:通关给与道具奖励等;

5) 活动数据:累计参加活动的次数、累计活动积分、给与活动积分奖励等;

6) 公会数据:活动积分累加到公会上、给与公会活动积分奖励等;

7) 关卡数据:通关记录以及通关奖励的给与等。

原有逻辑功能的代码是直接操作数据库,将数据从数据库取出、反序列化、修改、序列化后再存入数据库。在项目的后期,这种方式不仅效率低下,需要频繁的访问数据库,而且不同服务器之间操作相同数据时,数据之间的同步也很麻烦,需要使用分布式锁来保证。


如何重构

针对上述出现的模块之间耦合过高,数据同步复杂的问题,我们决定对服务器进行重构,采取的方法是将数据库进行垂直拆分,将不同业务的数据拆分到不同的数据库,同时保证,指定的业务数据只能由指定的服务器来访问和操作。比如:玩家的公会数据就只能由公会服务器来操作。而原有代码中,需要访问其他业务数据的代码则改用RPC来完成对数据的增删改查。这样就能大大的降低各个功能模块之间的耦合,同时避免了使用低效的分布式锁来保证数据的同步。

RPC库的选择

既然确定了重构的方向,那么就可以开始实施重构了。在RPC库的选择上,我们主要的考虑有两个方面:

1) 支持C++并且支持在协程中使用。我们的服务器使用libgo (版本2.6)协程库来提升服务器的承载。所有的消息都会在协程中处理,所以,处理消息时需要使用到的库也必须要支持协程;

2) 代码库要成熟稳定且能够商用的。这样在服务器的稳定性和吞吐量上才能有比较可靠的保障。根据上面的需求,再结合目前支持C++开发的RPC框架库中,我们选择了目前比较成熟的gRPC框架,同时使用protobuf作为消息的序列化库。gRPC的版本选的是1.14.1,protobuf的版本选择是3.5.1。

在服务器接入完gRPC后,我们首先在Windows下,进行了基本的功能测试,但是发现了两个明显的问题:

1) gRPC的同步调用会阻塞协程所在的线程,直到gRPC调用超时或者结果返回时,才取消阻塞;

2) gRPC结果返回后,程序无法读取返回的结果。

对于上面的第一个问题,我们深入查看了gRPC的代码发现,gRPC的同步调用,底层实现并不是同步的。同步调用时,gRPC实际上是将gRPC的调用请求交给gRPC的工作线程,让工作线程去和远程gRPC服务器通信。而调用线程和gRPC的工作线程之间的同步是靠线程锁来实现的,所以,只有在gRPC调用有结果时或超时后,调用线程才会取得结果从而取消阻塞继续执行后面的逻辑代码。

然而对于上面出现第二个问题,却比第一个问题麻烦得多。我们检查了功能测试的代码,gRPC的使用方法和官方的例子相同,但是gRPC调用的结果却无法读取, 而在本地抓取网络消息包时发现gRPC服务器的响应是回复给了gRPC客户端的。但是我们将功能测试的代码从服务器的框架中提取出来单独执行时,发现结果又正常了。通过这样的对比测试,我们确定是服务器框架中的某些代码导致gRPC无法正常工作。最后通过多次的对比测试后发现,当服务器加载了协程库后,gRPC的调用结果就不能正常的读取到了。

我们服务器使用的协程库libgo在程序启动时会hook与网络IO有关的API,这其中就包含了接收网络消息的API(代码如下)。

void coroutine_hook_init(){ ... // recv-like functions ok &= XHookAttach((PVOID*)&recv_f, &hook_recv) == NO_ERROR; ok &= XHookAttach((PVOID*)&recvfrom_f, &hook_recvfrom) == NO_ERROR; ok &= XHookAttach((PVOID*)&WSARecv_f, &hook_WSARecv) == NO_ERROR; ok &= XHookAttach((PVOID*)&WSARecvFrom_f, &hook_WSARecvFrom) == NO_ERROR; ...}

所以,当协程库加载后,gRPC调用Windows底层网络API读取RPC结果时,实际上会调用到协程库中hook的函数。而协程库在Windows下在判断一个网络套接字是否是阻塞时有BUG,从而导致了gRPC无法读取到响应结果。这一BUG也在与协程库作者联系后得到了确认。

基于上面使用gRPC库遇到的问题,我们意识到现成的RPC库难以在项目中使用,并且支持C++开发的RPC库的并不多。最后,结合项目实际的使用情况,我们决定自己实现RPC库。

实现自己的RPC库-cRPC

RPC的原理还是很简单的:应用程序调用RPC客户端提供的RPC函数,RPC函数内部将调用的参数封装成一个RPC请求,RPC客户端通过网络将RPC请求发送给RPC服务器进行处理,然后等待RPC服务器返回RPC结果(如图1)。


微服务架构之自实现RPC

图1 RPC调用的时序图

实现RPC时,网络库和序列化库对RPC性能影响比较大。我们目前项目中使用的网络库是公司技术储备库clib,它提供的网络功能能够很好的在协程中工作,所以cRPC网络库我们决定使用公司技术储备库clib。而对于序列库,我们决定使用protobuf,因为其在易用性和性能上都有不错的表现。

而在实现的细节上,需要注意的地方是一定要保证不管是RPC服务器宕机还是网络卡断,RPC调用都能在规定的时间内返回结果。另外,对于我们自己实现的cRPC库来说,还需要注意的是RPC的调用和请求的处理都是在协程中进行的。整个cRPC实现过程中主要需要注意的有下面几点:

1) 阻塞cRPC的调用方时,只能阻塞调用的协程,不能阻塞调用协程所在的线程,否则将会导致其他协程没有线程可供执行。在这一点上,我们最初尝试了协程库中提供的Channel用于在等待结果时阻塞协程。但是后来压力测试时发现,当每秒RPC调用上千时,少数Channel的调用即使在超时后也依然在阻塞。后来我们改用了原子变量加协程睡眠的方式来模拟限时锁的方式来解决这个问题。

2) 关于cRPC请求的超时处理。因为协程也会有调度。所以,cRPC请求可能到达cRPC服务器时,就已经超时了。这时可以不用处理cRPC请求而直接返回错误。另一个就是cRPC处理结果到达cRPC客户端时,处理此cRPC结果的是与调用cRPC不同的另一个协程,而调用cRPC的协程可能还在等待,也可能已经超时返回了。两种情况,都需要正确的处理或丢弃cRPC的结果,并释放之前用于同步cRPC结果的锁。

3) 关于cRPC调用失败时的处理方式。cRPC失败时处理的方式一般有两种:重试和放弃。重试就是再次调用RPC,直到调用成功或者失败次数达到限制。放弃就是当cRPC调用失败时,就向调用方返回错误,而不进行任何重试。因为cRPC调用失败时,不确定再次调用会不会破坏原本正确的数据,所以我们采取了保守的策略,即放弃策略。

实现完成后的cRPC由客户端模块和服务端模块组成,服务器中的其他模块可以方便的集成使用这两个模块。cRPC不仅提供了RPC必备的调用超时控制,而且还能完美的和现有服务器中的协程库协同工作。同时我们也对cRPC进行了细致性能测试和分析。

性能测试

在实现了cRPC后我们对其进行了性能测试。测试平台的硬件以及测试的参数如下:

微服务架构之自实现RPC

图2 RPC性能测试的平台及测试的部分参数

测试方法如下:

cRPC服务器和cRPC客户端之间只有一个网络套接字连接,并且他们都在同一台物理机上。性能测试程序每秒调用cRPC客户端向cRPC服务器发送20000个RPC请求,总共持续30秒。每秒内将20000个RPC请求分100批次发送。并且设置RPC请求的最大处理时间(即cRPC服务器开始处理RPC请求的时间与RPC请求创建时间之间差的最大值)为60秒,RPC请求响应的最大时间(即cRPC客户端发送RPC请求到收到回复的时间差的最大值)为120秒。cRPC服务器收到RPC请求时,将数据原封不动的返回给cRPC客户端。分别测试了RPC请求时附带的数据大小分别为0字节、64字节和128字节时这3种情况。下面是某一次测试的结果:

微服务架构之自实现RPC

图3 一次RPC性能测试的结果(表格)

微服务架构之自实现RPC

图4 一次RPC性能测试的结果(图表)

从性能测试的结果来看,单套接字连接情况,cRPC的TPS能够到达7000以上。各个请求的响应时间变化不大,比较稳定,且未出现超时或错误等情况。

结论

综合上面的数据及分析,我们自己实现的RPC库cRPC能够满足项目目前对RPC使用的功能需求和性能需求。


微服务架构之自实现RPC


数天技术

让技术·更有趣



点击下方“阅读全文”留言互动哟

以上是关于微服务架构之自实现RPC的主要内容,如果未能解决你的问题,请参考以下文章

微服务架构之RPC-client序列化细节

微服务架构下RPC IDL及代码如何统一管理?

离不开的微服务架构,脱不开的RPC细节(值得收藏)!!!

详解为什么微服务架构绕不开RPC

一文带你实现RPC框架

漫谈微服务RPC架构