返回 登录
16

老司机乱谈『代码之美』 ——不要束缚了自己前进的脚步

Seastar是一个优秀的c++网络框架,代码量低,注释详细,可读性高,在一些基础部门已经开始应用。框架之中有很多美的地方值得我们学习,本文主要介绍了Seastar框架的代码之美和一些关键特性。对于代码之美是站在C++程序员的角度来看的,比如haskell程序员看到这样的代码,也许会感叹:“Pieceofsh**!”。学习不同的语言,不同的框架,用不同的方法解决问题,可以让我们扩宽思路,打破思维局限,丰富自己思考问题的方式,提高自己解决问题的效率。比如用haskell实现一个快速排序,是这样的:

q_sort n=case n of

[]->[]

(x:xs)->q_sort [a|a<-xs,a<=x]++[x]++q_sort [a|a<-xs,a>x]

再对比一下你桌子上C语言版的《数据结构》,对,蓝色的那本。所以再次强调本文中提到的美,请从C++语言的角度去看,不要跟其他语言比☺。

《程序员修炼之道》中有一段话“Invest Regularly in Your Knowledge Portfolio”。对于技术人员来说,如果长期停留在自己的舒适区间你的优势将变成你的劣势。随时保持自己对技术的敏锐,停留在学习区间,光阴荏苒,日月如梭,多年之后,蓦然回首,你将发现自己已经蜕变。保持学习的态度,切莫辜负了岁月。

代码之美

先看一下这个框架以简驭繁的美。用一个EchoServer举例(省略了main函数)。
图片描述

可以看到代码很简洁清晰。该代码监听了一个1234的端口,只要accept,就发送一个固定字符串,然后关闭连接。这里面使用里future/promise模式还作为代码的胶合剂,把整个代码组织起来。同样整个框架也是用这个模式组织起来的。Future/promise(可以理解为常用的Callback,只是换了种方式重新组织了起来)模式后面再详细介绍一下,公司的taf框架里也有这个模式可以参照对比一下。另外这里出现的do_with 和 keep_doing都是配合该模式的一些函数(看名字也可以大概猜出来干啥的),有兴趣可以参考源文件future-util.hh(业界良心有注释)。

测试一下,注意这只是一个测试的例子,所以限定使用单线程否则会出错,如果正式使用应该使用sharded模板,使得实例运行在每一个core上面,这个后面再讲。
图片描述

使用这个框架当然是可以提高开发效率,但是这是一把大威力的加特林机枪,并自带了炸膛功能,如果对框架理解的不够对C++理解不够,那后果真是,不可限量。当然好的一面是,这个框架的代码量不大,注释比较详细,短时间就可以阅读所有核心代码。

性能之美

Seastar框架在代码上是美的,在性能方面也毫不含糊。对基于Seastar 的httpd来测试,已经具备在单节点(服务器)上每秒处理700万请求的能力,突破千万也指日可待。鱼(开发效率)和熊掌(运行效率)可以兼得了。
图片描述

硬件配置

CPU:2x Xeon E5-2695v3
网卡:2x Intel Ethernet CNA XL710-QDA1 (Transfer rate :40 Gbps,每cpu对应一个网卡)

使用Seastar开发的应用层代码和底层网络模式是隔离的,应用无需编译,可以通过配置选择使用Linux的tcp协议栈,还是使用Seastar native协议栈(用户态的tcp协议栈,zero-copy, zero-lock, and zero-context-switch,通过DPDK直接存取物理网络设备。DPDK处理一个包只需要不多于80个cpu时钟周期)

架构之美
对于多线程程序,理论是完美的,现实是残酷的,下图就体现了理论和现实的差别。

图片描述

Seastar在架构的上都是一些成熟经验,每个core运行一个线程(称之为shard),每个core上运行的线程只处理自己所负责的数据,把异步事件通过Promises的模式调度起来。这样的设计达到了shared-nothing的效果,所以避免了多核之间的锁,理论等于现实。当然这样设计当然避免不了core之间的交互,这个后面会提到。

下面给一个官方的配图。
图片描述

我们看一下关键代码,每个线程运行的主循环,是reactor::run(),是一个典型的reactor事件驱动模式,这个模块称之为引擎。
图片描述

我们写的一个应用比如httpd,通过sharded模板,然后跑到不同的core上去,这个模块称之为服务。
图片描述

Parallel_for_each 循环smp::count(可以通过启动参数进行重置少于物理core的数量)次,通过smp::submit_to 把服务创建到各个core的实例上去。这段代码也很美。

这里Cpu可以看作工厂,每一个core是一个车间,Reactor可以看作为引擎,我们写的服务(例如httpd)看作为机床,引擎驱动机床运转,每个车间互不干扰。

我们看一下httpd,限定为3个车间(shard),可以看到有3个同样的引擎在处理同样的事情,互不干扰。这里底层使用了SO_REUSEPORT(注意需要3.9以上的内核支持)。
图片描述

Future/promise

这是框架的胶合剂。Future,一个数据结构代表了尚未决议的结果,并且可以绑定一个Continuations(理解为回调函数)。Promise,就是这个结果的Provider。

这里需要强调的是,各种框架里实现的Promise包括标准库里提供的,细节上都是有所不同的,需要注意其中的差别。

我们先看一个示例,第11行我们创建了一个promise,并且它的future绑定了一个回调函数。当第16行对这个promise进行赋值的时候(比如异步结果从服务器返回),我们绑定的回调函数将会被调度,比如这里就跟公司的taf框架的实现有些不同,taf中的回调函数会在对promisesetvalue的时候立即调用,seastar还存在一个统一调度的过程(在第19行后才被调度)。同样后面的sleep函数也会返回一个future,也绑定了一个回调函数。
图片描述

执行结果,注意秩序顺序。

图片描述

虽然只是一个小小的机制,但是带来的收益确是巨大的,在整个框架中这种机制把各个模块连接成了一个整体。回调那种丑陋的代码到处都有就不再举例了。

Programs must be written for people to read, and only incidentally for machines to execute.
— Harold Abelson and Gerald Jay Sussman

Message passing

前面说到了shared-nothing,但是没有跨核通讯是不太可能的,对于跨核的通讯,框架中使用了无锁队列来实现。要达到无锁,不能出现多读多写,所以这里任意2核心之间都需要一个队列,对于16 core的cpu,就需要16*16-16个队列(自己无需跟自己通讯)。
图片描述
队列的定义与初始化
图片描述

队列的轮询处理
图片描述

smp_message_queue的定义

这里看到了内存洁癖,工匠精神。这里有2块数据,发送端的统计(Acore使用),和接收端的统计(Bcore使用),中间插入了一个其他的数据结构。作者解释为了避免cpu的prefecther造成A core的Cacheline和 B core的Cacheline加载了相同的内容 。这里多解释一下,如果多加载了会造成什么现象。我们以Intel处理器来举例,如果prefecth失败了,数据块2(这里已经按照cacheline的大小64字节对齐)只存在于B core的cacheline中,状态可能为E(Exclusive),如果中间没有其他数据结构A coreprefecth成功了。数据块2还存在于Acore中,cacheline的状态可能为 S(Shared)。S状态的写需要操作到InvalidateQueue,必然在硬件层面上造成了性能上的负担。如果想了解相关细节,可以阅读《Memory Barriers: a Hardware View for Software Hackers》,注意这里不必深究,不同cpu的实现都不太一样。

后记

回到《程序员修炼之道》这句话“Invest Regularly in Your Knowledge Portfolio”,有一个关键的问题,如何持之以恒的呢?其实这根本不是一个问题,如果你学习一个东西的过程感觉跟看《天龙八部》一样很精彩,那这会是一个问题吗?如果你的学习过程味同嚼蜡,那这也不是一个问题,因为可能已经走错路了。另外学习的过程中肯定有些东西无法完全理解,学会囫囵吞枣,不妨先放下,过一段时间回来看看,可能有不同的认识。学习是一个持续的过程,在这个过程中没有终点,但在过程之中自己会慢慢变得强大。

腾讯WeTest是腾讯游戏官方推出的一站式游戏测试平台,用十年腾讯游戏测试经验帮助广大开发者对游戏开发全生命周期进行质量保障。 腾讯WeTest提供:兼容适配测试;云端真机调试;安全测试;耗电量测试;服务器压力测试;舆情监控等服务。点击链接:http://wetest.qq.com/gaps/立即体验

评论