达成千万级别并发连接的秘密——内核正是问题所在,而非解决方案
译者:孙薇
原文链接:http://highscalability.com/blog/2013/5/13/the-secret-to-10-million-concurrent-connections-the-kernel-i.html
小象科技原创作品,欢迎大家疯狂转发;
机构、自媒体平台转载务必至后台留言,申请版权。
如今C10K问题(同时承载过万并发连接的问题)已经得以解决,那么想要升级到支持千万级别的并发连接,该怎么做呢?你也许会认为办不到,事实并非如此,现在的系统是能够支持千万级别的并发连接的,只需用到一些不怎么常见的先进技术。
为了了解具体实现方法,我们采访了Errata Security的CEO Robert Graham,他在 Shmoocon 2013上曾经发表过一篇精彩绝伦的演讲,题为“C10M Defending The Internet At Scale”。
Robert解决这个问题的办法十分巧妙,是我之前闻所未闻的。他先讲了一些历史原因:Unix最开始不是按照通用的服务器OS设计的,而是作为电话网络的控制系统存在。而在电话网络中,由于实际传输的是数据,因此控制面与数据面是明确独立分开的。现在的问题在于,如今我们将Unix服务器作为数据面的一部分来使用,这是不合适的做法。如果在内核设计之时,就按照各个服务器运行各自单独的应用程序这样的方式,那么就会跟如今的多用户内核这样的方式完全不同了。
这正是他所认为的关键:需要了解到这一点:
n 内核并非解决方案,而是问题所在。
也就是说:
n 不要将所有繁重的工作都丢给内核来做,将数据包处理、内存管理还有处理器调度从内核中单另出来放置到应用中,这样会使得工作更高效。让Linux处理控制面,应用程序处理数据面。
这样就能构建出一个足以处理千万级别并发连接的系统,其中200个时钟周期用以处理数据包,14万个时钟周期用以处理应用逻辑。其中由于主内存访问需花费300个时钟周期,因此关键在于设计出一种方法,让代码和缓存的丢失率降到最低。
一个面向数据面的系统能够处理的数据包高达千万每秒,而一个面向控制面的系统所能处理的数据包却只有百万每秒。
如果这看起来只是极端状况,还有一句老话需要我们铭记在心,那就是:可扩展性是专业化的。要作出优秀的系统,不能把性能外包给OS,而必须亲自来完成。现在,我们来了解一下Robert是怎样构建出这样一个能够支持千万级别并发连接的系统的。
C10K 问题——过去十年
十年前,C10K的可扩展性问题终于被解决,它曾一度阻碍服务器处理超过1万的并发连接。工程师们通过修复OS内核,并逐渐从诸如Apache之类的多线程服务器转向Nginx与Node之类的事件驱动服务器上来解决这个问题。从Apache迁移到可扩展性服务器的过程耗费了十年之久,在过去几年中,正像我们看到的那样,可扩展性服务器的普及速度更快了。
Apache的问题
n Apache的问题在于:随着连接数的增加,性能随之下降。
n 关键的认知:性能与可扩展性及正交概念,这些并非是一回事。人们在谈及可扩展性时,通常是在谈论性能,不过可扩展性与性能之间是有区别的。我们在Apache中能够看到,
n 维持数秒的短期连接被称为一个快速事务(quick transaction),而每秒执行1000个这样的事务(1000TPS),代表着只有1000个与服务器相连的并发连接存在。
n 将单个事务的连接时间调整为10秒,现在1000TPS意味着有1万个连接了。会出现DoS攻击而导致Apache面临性能陡降,因此大量的下载就会把Apache拖垮。
n 如果原本每秒处理5000个连接的服务器,现在希望处理的连接数增到1万个,该如何执行?假设硬件升级后,处理器速度达到原本的两倍,结果就是:性能达到了之前的两倍,但是可扩展性却没有变化,可能只达到每秒6000连接的能力。即使持续升级硬件,情况依旧如此,就算性能达16倍,却仍旧无法达成1万的连接数。可见,性能是不同于可扩展性的。
n 问题在于Apache会分离一个CGI进程,然后再干l掉它,结果无法达成扩展。
n 为什么呢?服务器达不到1万个并发连接的原因在于:内核使用了O(n^2)算法。
n 在内核中有两个基本问题:
n 连接数=线程数/进程数:每当一个数据包进入内核,都需要将1万个进程走一遍,找出该处理数据包的线程。
n 连接数=选择数/轮询数(单线程):同样的可扩展性问题,每个数据包都要走一遍socket列表。
n 解决方案:修复内核,使其查找时间为常数。
n 现在无论线程数有多少,线程的context切换时间是常数。
n 使用新的可扩展epoll()/IOCompletionPort常数时间来进行socket查找。
n 这时线程调度仍不具备可扩展性,因此服务器对socket使用epoll来进行扩展,导致用到在Node和Nginx中常有体现的异步编程模型,从而将软件提升到了另一个层次的性能级别。在增加连接数时,即便使用较慢的服务器,性能也不会陡然下降了,换台笔记本,连接数达到1万,速度都比16核的服务器要快。
C10M的问题——今后十年
要不多久,服务器就得能够处理百万级别的并发连接了。在IPV6协议下,每个服务器的潜在连接数都有百万级别,因此我们需要达到可扩展性的下一个层次。
n 需要这样的可扩展性应用包括:IDS/IPS——与服务器的骨干相连。其他应用:DNS根服务器、TOR节点、互联网的Nmap、视频流、银行业、运营商级别的NAT、网络电话交换机、负载均衡器、web缓存、防火墙、邮件接收与垃圾邮件过滤。
n 通常来说,人们将互联网可扩展性问题归咎于应用程序,而不会去责怪服务器,因为应用卖的是硬件+软件。人们购买设备,放入数据中心使用,这些设备可能包含一块Intel主板或者网络处理器,还有专门用作加密、数据包检测等用途的芯片。
n 在新蛋网上,2013年2月份一台X86/40G/32核/256RAM的服务器硬件卖到5千美元,它能处理的连接数就超过了1万。如果达不到这个数字,那是因为选用了错误的软件设计,而不是硬件本身导致的问题。此类硬件扩展到支持千万级别的并发连接轻而易举。
千万级别的并发连接这一挑战意味着:
1、1千万的并发连接数;
2、每秒100万的连接数——每个连接保持这个速率达10秒左右;
3、每秒10G的连接数——快速连接到互联网;
4、每秒1千万的数据包——预计当前的服务器能够处理每秒5万的数据包,以后还会发展到更高的层次:每个数据包造成一个中断,服务器每秒可处理10万个中断。
5、10微秒的延迟——可扩展的服务器可以解决可扩展性的问题,不过延迟的问题仍旧存在。
6、10微秒的抖动——限制最大延迟值
7、10核CPU——软件应当可以扩展到更大数量级的内核。通常来说,软件只能简单扩展到四核,服务器可以扩展到更多内核,因此需要重写软件以支持更多核的机器。
我们已经知道,Unix并非网络编程
n 整个一代的程序员都是通过阅读W. Richard Stevens. 所作的“Unix Networking Programming”一书来学习网络编程的。问题在于,这本书是关于Unix的,而不只是网络编程的,它让程序员将所有繁重的工作交给Unix,只要在Unix顶端编写一个小型服务器就行了,但是内核是不能扩展的。解决办法就是将繁重的工作从内核剥离,自己来做。
n 影响之一就是Apache每个连接模型的线程,用线程调度器根据收到的数据包类型来决定调用哪一个read(),将线程调度系统作为数据包的调度系统使用。(我真的很喜欢这一点,以前从未想过还能这样做)。
n Nginx号称不会将线程调度器作为数据包调度系统来用,而是自行调度数据包。使用select寻找socket,拿到数据立即读取,这样就会马上处理数据,而不会造成阻塞。
n 教训:让Unix处理网络堆栈,之后的一切都自己来。
如何编写可扩展的软件
如何修改软件才能让它具有可扩展性?很多有关硬件所能完成的工作有多少这样的经验之谈都是错的,我们需要了解性能的真实情况。
要想更进一步,我们需要解决如下问题:
1、数据包的可扩展性
2、多核的可扩展性
3、内存的可扩展性
数据包扩展——编写自定义驱动绕开堆栈
n 数据包的问题在于它们需穿过Unix内核,网络堆栈复杂缓慢,在你的应用中,数据包的路径应当更直接一些,别让OS来处理数据包。
n 解决问题的办法就是自行编写驱动,驱动只需将数据包直接发给应用,无需经过堆栈。可以找到以下驱动:PF_RING, Netmap, Intel DPDK(数据面开发工具)。Intel是不开源的,但是有很多相关的支持。
n 速度有多快?Intel有一个基准,在一台相当轻量级的服务器上每秒处理8千万个数据包(每个数据包200时钟周期),这也是通过用户模式来实现的。数据包向上传输,到达用户模式,再返回。在Linux中,用这样的方式处理UDP数据包最多每秒只能处理100万个,而自定义驱动的性能是Linux的80倍。
n 为了达成每秒1千万数据包的目标,假如获取数据包花费200个时钟周期,会有1400个时钟周期可以用来实现类似DNS/IDS的功能。
n 使用PF_RING获取原始数据包,那么就必须完成自己的TCP堆栈,大家在做的正是用户模式堆栈。对Intel来说,有可用的TCP堆栈能提供真正的可扩展性能。
多核可扩展性
多核可扩展性与多线程可扩展性并非一回事。我们都很了解这一概念:处理器不会变得更快。不过我们可以获得更多的处理器。大多数代码的扩展不会超过4核,我们添加更多的核时,不止会造成性能水平下降,还会让处理速度更慢,这是因为软件编写的不好。我们想要这样的软件:能够随着核数增加而接近线性地扩展,同时处理速度越来越快。
多线程编程并非多核编程。
n 多线程:
n 每个CPU核有不止一个线程
n (通过系统调度)锁定来协调线程
n 每个线程负责不同的任务
n 多核
n 每个CPU核一个线程
n 当两个线程/核访问同一个数据时,无法停止作而相互等待
n 所有线程在做同一个任务
n 我们的问题在于,如何将应用分布到多个核上。
n 在Unix中,锁在内核之中实现。在4核中使用锁时,大多软件都在等待其他线程解锁,所以从更多CPU中,所获得的性能提升还不如内核占用资源所造成的性能下降要多。
n 我们需要的架构是高速公路式的,而不是由红绿灯控制的十字路口式的。我们想要所有线程按部就班继续运行,同时开销极小。
n 解决方案:
n 保持每个核中的数据架构,然后聚合读取所有读数。
n 原子性:CPU支持可以从C语言中调用的指令,保证原子性,不要发生冲突。开销很大,所以不要到处都用。
n 无锁的数据结构:线程无需停止等待即可访问,不要自行实现,在不同架构中这项工作非常复杂。
n 线程模型:流水线 vs worker线程模型。不仅是同步的问题,还有线程如何构建的问题。
n 处理器关联:让OS使用前两个核,将线程布置到其他核上,可以用中断达到同样的目的。所以是设计者控制CPU,而不是Linux。
内存可扩展性
n 问题是如果你有20G的内存,假设每次连接使用2k,并且只有20M的L3缓存,那么缓存里是不会有连接数据的。这样CPU需要消耗300时钟周期将数据拿到主内存中,期间无事可做。
n 想一下我们每个数据包1400时钟周期的预算吧,要记得每个数据包有200时钟周期的开销,每个数据包只有4个cache miss,这就是问题。
n 将数据放在一起
n 不要用指针胡乱引用整个内存中的数据,每次用一个指针,就会造成一个cache miss:[hash pointer] -> [Task Control Block] -> [Socket] -> [App],这就是四个。
n 将所有数据放在内存的一个大块里: [TCB | Socket | App],通过预分配所有块来预订缓存,将会把cache miss从4减少到1。
n 分页
n 32G数据的分页表需占用64MB的资源,不适合存在cache中,这样就有两个cache miss了。一个是分页表格的,一个是指针的。这些都是可扩展软件所不能忽视的细节。
n 解决方案:压缩数据,并用高效缓存架构来代替有很多内存访问的二叉搜索树。
n NUMA架构使得访问主内存的时间加倍。内存可能不在一个本地socket上,而是在另一个socket上。
n 内存池
n 启动时立即预分配所有内存。
n 基于每个对象、每个线程和每个socket进行分配。
n 超线程
n 在网络处理器中,每个处理器至多可运行4个线程,Intel只能运行2个。
n 掩盖延迟,比如内存访问时,一个线程等待,另一个全速执行。
n 大页(Hugepages)
n 减少页面表的大小,从开始就保存内存,然后让应用管理内存。
结论
n NIC
n 问题:经过内核,效率不高。
n 解决方案:通过使用自定义驱动、自行管理,取消OS中的适配器。
n CPU
n 问题:如果使用传统的方法用内核协调应用,效率并不高。
n 解决方案:将前两个CPU留给Linux,让应用管理剩下的CPU。在未经允许的CPU上是不会发生中断的。
n 内存
n 问题:需要特别注意才能提高效率。
n 解决方案:在系统启动时,将大多数内存分配给自行管理的大页。
把控制面留给Linux,由于数据面运行在应用代码中,它从不与内核交互,线程调度、系统调用、中断这些情况都不存在。而你拥有的还是运行在Linux上的代码,可以正常debug的,而不是什么需要工程师专门定制的奇特硬件系统。这样用熟悉的编程和开发环境就能达到定制硬件所能达到的数据面性能了。
浙公网安备 33010602011771号