高性能服务器架构 第二篇

转自:http://www.doserv.com/article/2012/0831/5299117.shtml

上下文切换Context Switches

相对于数据拷贝影响的明显,非常多的人会忽视了上下文切换对性能的影响. 在我的经验里,比起数据拷贝,上下文切换是让高负载应用彻底完蛋的真正杀手. 系统更多的时间都花费在线程切换上,而不是花在真正做有用工作的线程上. 令人惊奇的是, (和数据拷贝相比)在同一个水平上,导致上下文切换原因总是更常见. 引起环境切换的第一个原因往往是活跃线程数比CPU个数多. 随着活跃线程数相对于CPU个数的增加,上下文切换的次数也在增加,如果你够幸运,这种增长是线性的,但更常见是指数增长. 这个简单的事实解释了为什么每个连接一个线程的多线程设计的可伸缩性更差. 对于一个可伸缩性的系统来说,限制活跃线程数少于或等于CPU个数是更有实际意义的方案. 曾经这种方案的一个变种是只使用一个活跃线程,虽然这种方案避免了环境争用,同时也避免了锁,但它不能有效利用多CPU在增加总吞吐量上的价值,因此除非程序无CPU限制(non-CPU-bound), (通常是网络I/O限制 network-I/O-bound), 应该继续使用更实际的方案.

一个有适量线程的程序首先要考虑的事情是规划出如何创建一个线程去管理多连接. 这通常意味着前置一个select/poll, 异步I/O,信号或者完成端口,而后台使用一个事件驱动的程序框架。关于哪种前置API是最好的有很多争论. Dan Kegel的C10K在这个领域是一篇不错的论文. 个人认为,select/poll和信号通常是一种丑陋的方案,因此我更倾向于使用AIO或者完成端口,但是实际上它并不会好太多. 也许除了select(),它们都还不错. 所以不要花太多精力去探索前置系统最外层内部到底发生了什么.

对于最简单的多线程事件驱动服务器的概念模型, 其内部有一个请求缓存队列,客户端请求被一个或者多个监听线程获取后放到队列里,然后一个或者多个工作线程从队列里面取出请求并处理. 从概念上来说,这是一个很好的模型,有很多用这种方式来实现他们的代码. 这会产生什么问题吗? 引起环境切换的第二个原因是把对请求的处理从一个线程转移到另一个线程. 有些人甚至把对请求的回应又切换回最初的线程去做,这真是雪上加霜,因为每一个请求至少引起了2次环境切换. 把一个请求从监听线程转换到成工作线程,又转换回监听线程的过程中,使用一种"平滑"的方法来避免环境切换是非常重要的. 此时,是否把连接请求分配到多个线程,或者让所有线程依次作为监听线程来服务每个连接请求,反而不重要了.

即使在将来, 也不可能有办法知道在服务器中同一时刻会有多少激活线程. 毕竟,每时每刻都可能有请求从任意连接发送过来,一些进行特殊任务的"后台"线程也会在任意时刻被唤醒. 那么如果你不知道当前有多少线程是激活的,又怎么能够限制激活线程的数量呢?根据我的经验,最简单同时也是最有效的方法之一是:用一个老式的带计数的信号量,每一个线程执行的时候就先持有信号量. 如果信号量已经到了最大值,那些处于监听模式的线程被唤醒的时候可能会有一次额外的环境切换, (监听线程被唤醒是因为有连接请求到来, 此时监听线程持有信号量时发现信号量已满,所以即刻休眠), 接着它就会被阻塞在这个信号量上,一旦所有监听模式的线程都这样阻塞住了,那么它们就不会再竞争资源了,直到其中一个线程释放信号量,这样环境切换对系统的影响就可以忽略不计. 更主要的是,这种方法使大部分时间处于休眠状态的线程避免在激活线程数中占用一个位置,这种方式比其它的替代方案更优雅.

一旦处理请求的过程被分成两个阶段(监听和工作),那么更进一步,这些处理过程在将来被分成更多的阶段(更多的线程)就是很自然的事了. 最简单的情况是一个完整的请求先完成第一步,然后是第二步(比如回应). 然而实际会更复杂: 一个阶段可能产生出两个不同执行路径,也可能只是简单的生成一个应答(例如返回一个缓存的值). 由此每个阶段都需要知道下一步该如何做,根据阶段分发函数的返回值有三种可能的做法:

请求需要被传递到另外一个阶段(返回一个描述符或者指针)

请求已经完成(返回ok)

请求被阻塞(返回"请求阻塞")。这和前面的情况一样,阻塞到直到别的线程释放资源

应该注意到在这种模式下,对阶段的排队是在一个线程内完成的,而不是经由两个线程中完成. 这样避免不断把请求放在下一阶段的队列里,紧接着又从该队列取出这个请求来执行。这种经由很多活动队列和锁的阶段很没必要.

这种把一个复杂的任务分解成多个较小的互相协作的部分的方式,看起来很熟悉,这是因为这种做法确实很老了. 我的方法,源于CAR在1978年发明的"通信序列化进程" (Communicating Sequential Processes CSP),它的基础可以上溯到1963时的Per Brinch Hansen and Matthew Conway--在我出生之前! 然而,当Hoare创造出CSP这个术语的时候,“进程”是从抽象的数学角度而言的,而且,这个CSP术语中的进程和操作系统中同名的那个进程并没有关系. 依我看来,这种在操作系统提供的单个线程之内,实现类似多线程一样协同并发工作的CSP的方法,在可扩展性方面让很多人头疼.

一个实际的例子是,Matt Welsh的SEDA,这个例子表明分段执行的(stage-execution) 思想朝着一个比较合理的方向发展. SEDA是一个很好的 "server Aarchitecture done right" 的例子,值得把它的特性评论一下:

1. SEDA的批处理倾向于强调一个阶段处理多个请求,而我的方式倾向于强调一个请求分成多个阶段处理.

2. 在我看来SEDA的一个重大缺陷是给每个阶段申请一个独立的在加载响应阶段中线程"后台"重分配的线程池. 结果,原因1和原因2引起的环境切换仍然很多.

3. 在纯技术的研究项目中,在Java中使用SEDA是有用的,然而在实际应用场合,我觉得这种方法很少被选择.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++棋牌游戏服务器架构通常包括以下几个主要组件: 1. 游戏逻辑层:负责处理游戏规则、游戏状态的更新和管理,以及处理玩家的操作请求。这一层通常由C++编写,包括游戏逻辑的实现和相关算法。 2. 网络通信层:负责处理客户端和服务器之间的网络通信。这一层通常使用TCP或UDP协议进行数据传输,并提供网络连接的建立、断开、数据收发等功能。常用的C++网络库有Boost.Asio、libevent等。 3. 数据库层:负责存储和管理游戏数据,如玩家信息、游戏记录等。这一层通常使用关系型数据库(如MySQL)或NoSQL数据库(如Redis)来存储数据,并提供相应的读写接口。 4. 多线程/多进程管理:为了提高服务器的并发处理能力,可以采用多线程或多进程的方式来处理客户端请求。多线程可以使用C++标准库中的std::thread或第三方库如pthread来实现,多进程可以使用fork或者第三方库如boost.process来实现。 5. 负载均衡与高可用性:为了提高服务器的性能和可用性,可以采用负载均衡技术将客户端请求分发到多台服务器上进行处理,同时可以使用集群和备份机制来实现高可用性。常用的负载均衡软件有Nginx、HAProxy等。 6. 安全认证与防作弊:为了保证游戏的公平性和安全性,可以在服务器端进行安全认证和防作弊处理。常见的技术包括数据加密、防止外挂和作弊程序的检测与防御等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值