点击上方蓝字
关注大侠之运维
后台回复99.99% 获取运维干货物
大家好,这里是大侠之运维,每天分享各类干货。
两者的区别,cloudflarre有做介绍,下面为原文译文:
How we built Pingora, the proxy that connects Cloudflare to the Internet
今天,我们很高兴向大家介绍Pingora,这是我们内部使用Rust构建的全新HTTP代理。它每天处理超过1万亿个请求,提升了我们的性能,并为Cloudflare的客户提供了许多新功能,同时只需要我们之前代理基础架构的三分之一的CPU和内存资源。
随着Cloudflare的规模扩大,我们已经超越了NGINX的能力。多年来,NGINX一直非常出色,但随着时间推移,在我们的规模上,它的限制意味着构建新的代理系统是有意义的。我们无法再获得所需的性能,而且NGINX没有我们在非常复杂的环境中所需的功能。
许多Cloudflare的客户和用户使用Cloudflare全球网络作为HTTP客户端(如Web浏览器、应用程序、物联网设备等)和服务器之间的代理。过去,我们曾经详细讨论过浏览器和其他用户代理如何连接到我们的网络,并且我们开发了许多技术并实施了新的协议(如QUIC和针对http2的优化),以使连接的这一环节更加高效。
今天,我们将关注方程式的另一部分:在我们的网络和互联网上的服务器之间代理流量的服务。这个代理服务支持我们的CDN、Workers Fetch、Tunnel、Stream、R2以及许多其他功能和产品。
让我们深入了解为什么选择替换我们的传统服务以及我们如何开发Pingora,这是我们专为Cloudflare的客户使用情况和规模而设计的新系统。
为什么要构建另一个代理
多年来,我们使用NGINX时遇到了一些限制。对于一些限制,我们进行了优化或绕过处理。但是其他一些限制要克服起来就困难得多。
架构限制影响性能
NGINX的工作进程架构对我们的使用情况存在操作上的缺点,影响了我们的性能和效率。
首先,在NGINX中,每个请求只能由一个工作进程处理。这导致CPU核心之间的负载不均衡,从而导致速度变慢。
由于这种请求-进程绑定效应,执行CPU密集型或阻塞IO任务的请求会减慢其他请求的速度。正如那些博文所证明的,我们花了很多时间来解决这些问题。
对于我们的使用情况来说,最严重的问题是连接复用不佳。我们的机器与源服务器建立TCP连接来代理HTTP请求。连接复用通过重用之前建立的连接从连接池中加速请求的TTFB(首字节到达时间),避免了在新连接上所需的TCP和TLS握手。
然而,NGINX的连接池是每个工作进程独立的。当一个请求落在某个特定的工作进程上时,它只能在该工作进程内复用连接。当我们添加更多的NGINX工作进程以进行扩展时,连接复用比率变得更差,因为连接分散在更多孤立的进程池中。这导致TTFB变慢,需要维护更多的连接,这消耗了我们和我们的客户的资源(和资金)。

如前面的博客文章中提到的,我们对其中一些问题进行了解决方案。但是,如果我们能够解决根本问题:工作进程模型,我们将自然而然地解决所有这些问题。
有些类型的功能很难添加
NGINX是一个非常好的Web服务器、负载均衡器或简单的网关。但是,Cloudflare的功能远不止于此。我们过去在NGINX周围构建了所有需要的功能,但是在努力不过多偏离NGINX上游代码库的同时,这并不容易做到。
例如,当重试/故障切换请求时,有时我们希望将请求发送到不同的源服务器,并使用不同的请求头集。但这不是NGINX允许我们做的事情。在这种情况下,我们花费时间和精力解决NGINX的限制问题。
与此同时,我们使用的编程语言并未提供帮助来缓解困难。NGINX完全使用C语言,而C语言在设计上并不具备内存安全性。与这样的第三方代码库一起工作很容易出现错误。即使对于有经验的工程师来说,也很容易遇到内存安全问题,我们希望尽量避免这些问题。
我们用来补充C的另一种语言是Lua。它的风险较低,但性能较低。此外,当处理复杂的Lua代码和业务逻辑时,我们经常发现自己缺少静态类型检查。
此外,NGINX社区活跃度不高,开发往往是“闭门造车”的。
选择自建
在过去的几年中,随着我们不断扩大客户群和功能范围,我们一直在评估三种选择:
继续投资于NGINX,可能派生出一个完全符合我们需求的分支版本。我们具备所需的专业知识,但鉴于上述架构限制,需要大量工作来重新构建以完全支持我们的需求。
迁移到其他第三方代理代码库。确实有一些很好的项目,比如envoy等。但这条路意味着几年后可能会重复相同的循环。
从零开始构建一个内部平台和框架。从工程方面来看,这种选择需要最多的前期投资。
过去几年中,我们每个季度都评估了这些选项。没有明显的公式可以告诉我们哪种选择最好。几年来,我们继续选择最低阻力的道路,不断增强NGINX。然而,在某个时刻,建立自己的代理的回报似乎是值得的。我们决定从头开始构建一个代理,并开始设计我们梦想中的代理应用程序。
Pingora项目
设计决策
为了创建一个每秒处理数百万请求的代理,快速、高效且安全,我们首先需要做出一些重要的设计决策。
我们选择了Rust作为项目的编程语言,因为它可以以安全的方式实现与C相同的功能,同时不影响性能。
虽然有一些很棒的现成的第三方HTTP库,比如hyper,但我们选择自己构建一个,因为我们希望最大限度地灵活处理HTTP流量,并确保我们可以按照自己的节奏进行创新。
在Cloudflare,我们处理整个互联网的流量。我们面临许多奇怪和非RFC兼容的HTTP流量案例,我们必须对其进行支持。这是HTTP社区和Web界面的共同困境,严格遵循HTTP规范与适应可能是传统客户端或服务器的广泛生态系统的微妙之处之间存在着紧张关系。在其中选择一方可能是一项艰巨的任务。
HTTP状态码在RFC 9110中定义为三位数的整数,通常预期在100到599的范围内。Hyper是其中一个实现。然而,许多服务器支持在599到999之间使用状态码。为此特性创建了一个问题,探讨了辩论的各个方面。虽然hyper团队最终接受了这个改变,但他们拒绝这样的要求也有合理的理由,而且这只是我们需要支持的许多不兼容行为案例之一。
为了满足Cloudflare在HTTP生态系统中的要求,我们需要一个强大、宽容和可定制的HTTP库,它可以在互联网的复杂环境中生存,并支持各种不兼容的使用情况。保证这一点的最好方法是自己实现。
下一个设计决策是关于我们的工作负载调度系统。我们选择多线程而不是多进程,以便轻松共享资源,特别是连接池。我们还决定需要工作窃取来避免上述某些性能问题类别。Tokio异步运行时非常适合我们的需求。
最后,我们希望我们的项目直观且对开发人员友好。我们所构建的不是最终产品,而是作为一个平台的可扩展性,在其之上可以构建更多功能。我们决
定实现了一个基于事件的“请求生命周期”编程接口,类似于NGINX/OpenResty。例如,“请求过滤器”阶段允许开发人员在接收到请求头时运行代码以修改或拒绝请求。通过这种设计,我们可以将业务逻辑和通用代理逻辑清晰地分离。之前在NGINX上工作的开发人员可以轻松切换到Pingora,并迅速提高生产力。
Pingora在生产环境中更快
让我们来快进到现在。Pingora处理几乎所有需要与源服务器进行交互的HTTP请求(例如,缓存未命中),在此过程中我们收集了大量的性能数据。
首先,让我们看看Pingora如何加速我们客户的流量。Pingora整体流量显示中位数TTFB减少了5毫秒,第95个百分位数减少了80毫秒。这不是因为我们的代码运行得更快。即使是我们旧的服务也可以处理毫秒级别的请求。
节省的原因在于我们的新架构可以在所有线程之间共享连接。这意味着更好的连接重用比率,从而减少了在TCP和TLS握手上花费的时间。

跨所有客户,与旧服务相比,Pingora每秒只建立三分之一的新连接。对于一个重要的客户,它将连接重用比率从87.1%提高到99.92%,将与源服务器的新连接减少了160倍。为了更直观地表示这个数字,通过切换到Pingora,我们每天为我们的客户和用户节省了434年的握手时间。
更多功能
拥有开发人员友好的界面,让工程师们熟悉,并消除了以前的限制,使我们能够更快地开发更多的功能。核心功能如新协议成为我们可以为客户提供更多产品的基石。
例如,我们能够在Pingora中添加对HTTP/2上游的支持,没有遇到重大障碍。这使我们能够在不久之后向客户提供gRPC。如果要将同样的功能添加到NGINX中,需要投入更多的工程力量,而且可能无法实现。
最近,我们宣布了Cache Reserve,Pingora使用R2存储作为缓存层。随着我们为Pingora添加更多功能,我们能够提供以前不可行的新产品。
更高效
在生产环境中,与旧服务相比,Pingora的CPU消耗减少了约70%,内存消耗减少了约67%。节省来自几个因素。
我们的Rust代码与旧的Lua代码相比运行更高效。此外,它们的架构也存在效率差异。例如,在NGINX/OpenResty中,当Lua代码想要访问HTTP头时,它必须从NGINX C结构中读取它,分配一个Lua字符串,然后将其复制到Lua字符串中。之后,Lua还必须对其新字符串进行垃圾回收。在Pingora中,它只是直接访问字符串。
多线程模型还使得跨请求共享数据更加高效。NGINX也有共享内存,但由于实现限制,每个共享内存访问都必须使用互斥锁,并且只能将字符串和数字放入共享内存。在Pingora中,大多数共享项目可以通过共享引用直接访问,并在原子引用计数器后面进行。
正如上面提到的,CPU节省的另一个显著部分来自于建立较少的新连接。与通过已建立的连接仅发送和接收数据相比,TLS握手是昂贵的。
更安全
快速而安全地发布功能是困难的,尤其是在我们的规模下。很难预测在每秒处理数百万个请求的分布式环境中可能出现的每个边界情况。模糊测试和静态分析只能起到一定的缓解作用。Rust的内存安全语义保护我们免受未定义行为的困扰,并让我们有信心我们的服务将正确运行。
有了这些保证,我们可以更多地关注我们的服务变更将如何与其他服务或客户的源进行交互。我们可以以更高的节奏开发功能,而不受内存安全和难以诊断的崩溃的限制。
当发生崩溃时,工程师需要花时间诊断发生的原因。自从Pingora诞生以来,我们已经处理了数万亿个请求,但由于我们的服务代码而导致的崩溃几乎是非常罕见的。
事实上,Pingora的崩溃是如此罕见,以至于当我们遇到一个崩溃时,通常会发现与之无关的问题。最近,我们发现了一个内核错误,就在我们
可以说,作为对比,我们现在着重介绍另一个方面:连接我们的网络与Internet上的服务器之间的流量的代理服务。这个代理服务为我们的CDN、Workers fetch、Tunnel、Stream、R2以及许多其他功能和产品提供支持。
让我们详细了解为什么我们选择替换我们的传统服务以及我们如何开发Pingora,这是一个专门为Cloudflare的客户使用场景和规模而设计的全新系统。
架构限制影响了性能
在我们的使用过程中,NGINX的工作进程(进程)架构存在一些操作上的缺点,这影响了我们的性能和效率。
首先,在NGINX中,每个请求只能由一个工作进程处理。这导致CPU核心之间的负载不平衡,从而导致速度变慢。
由于这种请求-进程固定效应,执行CPU密集型或阻塞IO任务的请求可能会减慢其他请求的处理速度。正如前面的博文所述,我们花了很多时间解决这些问题。
对于我们的使用场景来说,最关键的问题是连接复用较差。我们的机器与源服务器建立TCP连接以代理HTTP请求。连接复用通过从连接池中重用之前建立的连接,跳过新连接上所需的TCP和TLS握手,加快了请求的TTFB(时间到第一个字节的时间)。
然而,NGINX的连接池是每个工作进程的独立的。当一个请求落在某个特定的工作进程上时,它只能复用该工作进程内的连接。当我们添加更多的NGINX工作进程以扩展规模时,我们的连接复用比率变得更差,因为连接分散在更多的孤立进程的池中。这导致TTFB变慢,并且需要维护更多的连接,这会消耗我们和客户的资源(和资金)。
尽管我们针对一些问题进行了优化或绕过,但有些问题很难解决。
结论
总结起来,我们建立了一个自己的代理,它更快、更高效、更多功能,成为我们当前和未来产品的平台。
我们将会提供更多关于我们面临的问题、应用的优化和从构建Pingora以及将其应用于驱动互联网的重要部分中所学到的教训的技术细节。我们还将提供我们开源计划的详细信息。
Pingora是我们重新构建系统的最新尝试,但不会是最后一次。它也只是我们系统重新架构的一个构建模块。
end
大侠之运维,相识便是缘
收集不易,点赞、留言、分享就是大侠🦸♀️写下去的动力!

👆点击查看更多内容👆
推荐阅读
记得星标记一下,下次更容易找到我

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!




