老司机乱谈『代码之美』
老司机乱谈『代码之美』
——不要束缚了自己前进的脚步
Seastar是一个优秀的c++网络框架,代码量低,注释详细,可读性高。框架之中有很多美的地方值得我们学习,本文主要介绍了Seastar框架的代码之美和一些关键特性。对于代码之美是站在C++程序员的角度来看的,比如haskell程序员看到这样的代码,也许会感叹:“Piece of sh**!”。学习不同的语言,不同的框架,用不同的方法解决问题,可以让我们扩宽思路,打破思维局限,丰富自己思考问题的方式,提高自己解决问题的效率。比如用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”。对于技术人员来说,如果长期停留在自己的舒适区间你的优势将变成你的劣势。随时保持自己对技术的敏锐,停留在学习区间 ,光阴荏苒,日月如梭,多年之后,蓦然回首,你将发现自己已经蜕变。保持学习的态度,切莫辜负了岁月。
代码之美
先看一下这个框架以简驭繁的美。用一个Echo Server举例(省略了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 CNAXL710-QDA1 (Transfer rate :40Gbps,每cpu对应一个网卡)
使用Seastar开发的应用层代码和底层网络模式是隔离的,应用无需编译,可以通过配置选择使用Linux的tcp协议栈,还是使用Seastarnative协议栈(用户态的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中的回调函数会在对promise set value的时候立即调用,seastar还存在一个统一调度的过程(在第19行后才被调度)。同样后面的sleep函数也会返回一个future,也绑定了一个回调函数。
执行结果,注意秩序顺序。
虽然只是一个小小的机制,但是带来的收益确是巨大的,在整个框架中这种机制把各个模块连接成了一个整体。回调那种丑陋的代码到处都有就不再举例了。
Programsmust be written for people to read, and only incidentally for machines toexecute.
— Harold Abelson and Gerald Jay Sussman
Message passing
前面说到了shared-nothing,但是没有跨核通讯是不太可能的,对于跨核的通讯,框架中使用了无锁队列来实现。要达到无锁,不能出现多读多写,所以这里任意2核心之间都需要一个队列,对于16 core的cpu,就需要16*16-16个队列(自己无需跟自己通讯)。
队列的定义与初始化
队列的轮询处理
smp_message_queue的定义
这里看到了内存洁癖,工匠精神。这里有2块数据,发送端的统计(A core使用),和接收端的统计(B core使用),中间插入了一个其他的数据结构。作者解释为了避免cpu的prefecther 造成Acore的Cache line和 B core 的Cache line加载了相同的内容 。这里多解释一下,如果多加载了会造成什么现象。我们以Intel处理器来举例,如果prefecth失败了,数据块2(这里已经按照cache line的大小64字节对齐)只存在于B core的cacheline中,状态可能为E(Exclusive),如果中间没有其他数据结构A core prefecth成功了。数据块2还存在于A core 中,cache line的状态可能为 S(Shared)。S状态的写需要操作到InvalidateQueue,必然在硬件层面上造成了性能上的负担。如果想了解相关细节,可以阅读《Memory Barriers:a Hardware View for Software Hackers》,注意这里不必深究,不同cpu的实现都不太一样。
后记
回到《程序员修炼之道》这句话“Invest Regularly in Your Knowledge Portfolio”,有一个关键的问题,如何持之以恒的呢?其实这根本不是一个问题,如果你学习一个东西的过程感觉跟看《天龙八部》一样很精彩,那这会是一个问题吗?如果你的学习过程味同嚼蜡,那这也不是一个问题,因为可能已经走错路了。另外学习的过程中肯定有些东西无法完全理解,学会囫囵吞枣,不妨先放下,过一段时间回来看看,可能有不同的认识。学习是一个持续的过程,在这个过程中没有终点,但在过程之中自己会慢慢变得强大。
Jaco于2016年10月29日