我认为我们中的一些人已经这样做了一段时间,在向那些新手展示什么是可能的方面做得很糟糕。我得到的印象是,我们中的很多人(包括我自己)都达到了某种程度,厌倦了这些废话,只想摆脱它。有时我在想,如果我忘记自己对系统的了解,消失在这个世界上……那会是什么样子呢?我不知道,养金鱼什么的。
但是,只要我在这里写这些东西,我不妨分享一下这些年来学到的一些东西。并不是所有的都是好的,其中一些可能是完全错误的或过时的,但还有其他事情需要人们知道。
首先,为相当数量的客户提供服务并不需要那么多的机器。自从几个月前我写了整个Python/Gunicorn/Gevent乱七八糟的事情以来,人们一直在问我,如果不是这样,那么是什么。这让我开始思考替代方案,最后我开始编写代码。
结果对我来说有些不同寻常:一台还没有真正用途的服务器。在这一点上,它更多的是一种示范性的东西。
我拥有的是一个服务器,它启动并启动一个侦听器线程,除了监视传入的流量之外什么都不做。当它获得新的连接时,它会启动一个serviceworker线程来处理它。侦听器线程拥有所有文件描述符(侦听器和客户端都拥有),并管理单个EPOLL集来监视它们。
当客户端FD激活时,它用condvar戳那个serviceworker线程,然后唤醒并从网络进行读取。如果有一条完整和可用的消息,它会将其发送到我的古怪的小RPC情况,它就坐在它后面。然后,它再次将结果推送到网络,并返回等待另一次唤醒。
如果你在计分,这意味着我每次连接时都会产生一个完整的操作系统级线程。我围绕着这个概念设计了这个东西,如果我们不怕创建线程会怎么样,这就是结果。
我还编写了一个负载测试工具。它将创建任意数量的工作线程,每个工作线程都会打开一个回服务器的TCP连接。它们中的每一个都将沿管道发出一个请求,等待响应,休眠一段可配置的时间,然后再次执行。
假设我在同一台机器上安装了服务器和一个loadgen实例。在这种情况下,它是我运行Slackware64的九年前的工作站机箱。我告诉负载生成器访问服务器(在localhost上),运行2000个工作进程,并在两次查询之间等待200毫秒。
通过计算,每个工作者每秒应该运行大约5个查询。它不完全是5,因为请求本身需要一些时间才能完成。这意味着我应该看到大约每秒10000次。
我启动它。您认为服务器会发生什么情况?它现在有2,000个客户端连接和10000 QPS需要担心。它会吃掉整台机器吗?不,远非如此。
服务器进程有大约90MB的RSS--即物理内存。对于有2000多根头发的人来说,这已经不坏了!现在,当然,进程的虚拟大小(VSZ)大约是20 GB,但是谁在乎呢?这就是拥有那么多线程的所有开销,但是它们实际上并没有在物理上使用它,所以它是有效的。
延迟是什么样子的?由于这一切都在环回上运行,它看起来非常出色。大多数情况下,所有请求都在不到一毫秒的时间内完成。每隔一段时间,就会有一小部分人进入1个以上、不到2个地区。那很酷,对吧?
但是,好吧,那是不现实的。查询不是来自本地主机,而是来自同一网络上的其他计算机。很好。那么,当我保持这2000个客户端运行,然后从我的Mac笔记本电脑通过本地千兆以太网再添加4000个客户端时,会发生什么情况呢?
嗯,首先,潜伏期会扩散。那些通过环回的请求现在都不会在一毫秒内发生。现在,他们中的一些人实际上可以达到40毫秒。喘口气!
与此同时,Mac也出现了很好的延迟蔓延。我让它做了一些非常糟糕的分析来得出百分位数。如果我没有把这件事搞得太糟的话,从滚动控制台抓取的一段5秒的数字是这样的:
如果您不熟悉,这意味着一半(50%)的请求在24毫秒或更短的时间内完成。75%的人在31毫秒或更短的时间内完成,以此类推。
这似乎比提供所有服务的Linux机器要落后我的笔记本电脑多了很多。我在运行X的Linux机器上,在X终端上,跑进Mac,输入这篇帖子……。而它现在却落在了后面。它也不是Linux盒子,因为我可以在上面做更多互动的事情,而且它乏味、乏味、老一套。
与此同时,服务器占用了大约256MB的物理内存,并且正在处理6000多个线程。整个机器(运行服务器、负载生成器和其他一些不相关的东西)上的平均负载大约是4.4。
那么,如果我往它身上扔更多的东西,会发生什么呢?在我的本地网络上,我还有一个模糊统一的盒子:2011款Mac Mini。那另外4000个客户呢?
服务器现在拥有大约422MB的物理内存和刚刚超过10000个线程。现在整台机器的平均负载大约是7.8。现在整个服务器的运行速度约为32000 QPS。
这对时间有什么影响?回到我的笔记本电脑上,事情变慢了一点。下面是另一个5秒的快照:
老实说,笔记本电脑上的数字到处都是。它还有Firefox和至少一个愚蠢的聊天客户端,它实际上是一个正在运行的巨大的小猪网络浏览器,再加上苹果决定在后台启动的任何其他功能。尽管如此,P99仍然保持在250ms以下。从理论上讲,我可以运行更长的时间,并从整个业务中获得一些错误条。我也可以使用合理的系统来做这项测试,这样就不会有大量的其他垃圾在上面运行,这些垃圾会给我的结果带来噪音。但是,嘿,我正在用我手头上的东西。
现在,需要明确的是,这一切都取决于服务器到底在做什么工作。它不是在做TLS。它处理的是我自己的小傻瓜线协议,将其反序列化为协议Buf,弄清楚它的含义,将其内部反序列化为另一个(依赖于请求的)协议Buf,然后将其分派给处理程序。处理程序会做一些工作(稍后会详细介绍),然后服务器必须序列化响应,将其塞进外部消息中,序列化它,将其包装在我的GOFFY WIRE协议中,然后将其发送到套接字。
那么,工作量是多少呢?为了这次测试,我写了一个非常愚蠢的东西叫LavaLamp。这是对SGI Lavarand项目的致敬,该项目使用摄像头指向熔岩灯来生成随机数字。
我的不用熔岩灯。这个名字的意思是具有讽刺意味。不过,它确实会产生伪随机数。我们的想法是在CPU上只做一些工作,而不是做任何太有趣的事情。这是故意不优化的。每次运行时,它都会创建一个随机设备、一个随机引擎和一个约束在';A';和';Z';之间的统一int分布。是的,没错,这玩意儿只吐出ASCII的大写字母。
它会运行16次,并将结果发送给您。这里没有高速缓存,所以可以说,每个客户都能从烤架上新鲜地得到他们的小斑点。
这有点好笑,因为它确实每次都会打开/dev/urandom。事实上,如果您不给这个东西一个足够高的文件描述符限制,那么当它走到这一步时,它可能会爆炸。毕竟,连接到10000个客户机每个都会消耗一个FD,侦听套接字会吃几个,epoll有一个,那里有stdin/out/err,现在您想在每次收到请求时打开/dev/urandom吗?那可真是一大堆苦差事!
注意:当发生这种情况时,它不会崩溃。我捕捉到随机的设备故障并终止请求。为什么在一个请求失败时关闭整个服务器?那简直是自找麻烦。
我只是想要一些能给盒子带来些许震撼的东西,而不是像Echo端点那样真正愚蠢的东西。这样做的工作量微乎其微,并且使它变得更加有趣。
因此,我2011年的愚蠢小盒子可以处理10000个持久连接,每个连接每秒5次地向管道中发送一个请求,用随机数字做一些真正愚蠢的事情。它使用大约四分之一GB的物理内存和大约75%的CPU可用能力来做到这一点。在平衡来自NIC的IRQ、拆分接受/EPOLL/分派负载或流水线请求方面,我没有做任何特殊的工作。这几乎是你能想象到的最愚蠢的事了。
唯一应该稍微有趣的是,我是经过深思熟虑地决定不害怕线程来构建它的。我想,我会准备好它们,然后让内核决定什么时候调度它们。到目前为止,它一直运行得很好。
这只是一个可以做到的例子。这不是魔术。这只是个工程学问题。