了解连接和池

2021-01-05 21:20:50

连接是计算机系统相互通信的一种隐藏机制,它们之间的联系已变得非常重要,以至于我们忽略了它们的重要性,工作方式以及何时发生故障。在出现问题之前,我们通常不了解它们,通常在我们的系统执行最大工作量时会出现严重故障。但是由于它们无处不在,并且在几乎每个系统中都非常重要,因此值得花一点时间来理解它们。

连接是两个系统之间的链接,它使它们可以按零和一的序列交换信息,以发送和接收字节。

根据系统之间相对运行的位置,底层软件和硬件的组合将努力处理信息的物理移动,从而将其抽象化。例如,如果通信系统是两个Unix进程,则IPC系统将为交换的数据处理分配的内存,并在两侧处理字节的提取和传递。如果系统在不同的计算机上运行,​​则它们可能会通过TCP进行通信,TCP会处理在计算机之间通过有线或无线系统移动数据的问题。计算机如何协同工作以可靠地处理,传输和接收数据的细节更多是一个标准化问题,并且大多数系统使用UDP和TCP协议提供的构造块。在应用程序开发中,如何在每一端处理这些连接是一个更相关的问题,这就是我们现在要看的问题。

您现在正在使用它们。您的浏览器与托管此博客的Web服务器建立了连接,并使用该服务器获取构成您正在查看的HTML,CSS,JavaScript和图像的字节。如果您使用的是HTTP / 1.1协议,则浏览器将与服务器建立多个连接,每个文件建立一个连接。如果您使用HTTP / 2,则使用多路传输,许多文件可能是通过同一连接提供的。在这些情况下,您的浏览器是客户端,而博客服务器是...服务器。

但是服务器也建立了自己的连接以显示此页面。它使用一种连接来与数据库对话,在查询中通过该页面的URL发送并返回该页面的内容。在这种情况下,应用程序服务器是客户端,数据库服务器是服务器。应用程序服务器可能还建立了与其他第三方服务的连接,例如订阅或付款服务或位置服务。

对于静态文件,例如JS,CSS和图像,在浏览器和博客服务器之间有一个CDN系统。您的浏览器(客户端)与离您最近的CDN服务器(服务器)之间建立了连接,如果您附近的缓存中没有可用的文件,则CDN可能还会有另一个连接服务器(客户端)到博客服务器(服务器)。

如果您仔细考虑所使用或构建的所有系统,就会发现到处都是连接,但是它们经常被隐藏起来,并且不了解它们的无形存在和局限性会再次咬住您。您最不希望在何时何地。

了解连接的处理方式非常重要,因为连接的成本是不对称的-客户端和服务器上的成本不同。在点对点(P2P)系统中,例如洪流云,这是错误的,并且两端的连接成本相同,但是这种情况很少发生。连接的常见用法有一个客户端和一个服务器,服务器的成本与客户端的成本不同。

在研究如何处理连接之前,我们需要快速查看计算机运行程序的不同方式以及程序并行运行的方式。当您运行程序时,操作系统会将您的代码作为进程的一个实例来运行。一个进程在运行时会占用一个CPU内核和一些内存,并且不会与任何其他进程共享其内存。进程可以启动线程,就像可以同时运行的进程的子级一样。线程与产生它们的进程共享内存,并可能分配更多内存供其使用。或者该流程可能使用事件循环,该事件循环是一个单进程系统,可以跟踪其必须执行的任务,并连续且无限地循环遍历所有任务,每次执行它可以执行的任务,或者在执行任务时跳过它们39;被阻止。或者,该过程可能使用称为纤维,绿线,协程或参与者的内部构造-这些每个的工作方式略有不同,成本也有所不同-但它们都由该过程及其线程在内部进行管理。

回到连接的处理方式,让我们首先看一下数据库连接-从应用程序服务器(在本例中为客户端),您看到一个TCP连接已用较小的内存缓冲区和一个端口分配付费了。在服务器端,如果您使用的是PostgreSQL,则通过产生一个新进程来处理服务器上的每个连接,该进程处理通过该连接发送的所有查询。此过程占用一个CPU内核,RAM中约有10MB或更多的内存。 MySQL通过在进程内部产生一个线程来处理每个连接。在线程模型中,RAM /内存要求要低得多,但这是以上下文切换这些线程为代价的。 Redis将每个连接作为事件循环中的迭代进行处理,这降低了资源成本-但要付出代价,必须循环遍历每个连接的查询并严格地一次为它们提供服务。

考虑对应用程序服务器的请求。您的浏览器以客户端身份启动TCP连接,这种连接很便宜(较小的内存缓冲区和一个端口)。在服务器上,情况有所不同。如果服务器使用的是Ruby on Rails,则每个连接都由固定数量的正在运行的进程(Puma Web服务器)中产生的一个线程或一个进程(Unicorn)处理。如果使用的是PHP,则CGI系统会为每个连接启动一个新的PHP进程,而更流行的FastCGI系统会保持一些进程运行,从而更快地处理下一个连接。如果您使用的是Go,则会生成一个goroutine(一种廉价且轻便的线程状结构,由Go运行时在内部进行调度),以处理每个连接。如果您使用的是NodeJS / Deno,则在事件循环中通过遍历传入的连接并一次响应一个请求来处理传入的连接。在类似Erlang / Elixir的系统中,每个连接都将由一个actor处理,该actor是另一个内部调度的轻量级类似线程的构造。

有关如何处理连接的示例具有一些可以确定的常见策略:

进程:每个连接由一个单独的进程处理,该进程要么专门为该连接启动(CGI,PostgreSQL),要么作为一组可用进程(Unicorn,FastCGI)的一部分进行维护。

线程:每个连接由单独的线程处理,该线程专门为该连接生成,或在生成后保留在备用线程中。线程可能分布在多个进程中,但是所有线程都是等效的(Puma / Ruby,Tomcat / Java,MySQL)。

事件循环:每个连接都是事件循环中的一个任务,具有要读取的数据的连接通过对其进行迭代(节点,Redis)进行处理。这些系统通常是单进程和单线程的,但是它们有时可能是多进程的,其中每个进程都充当具有独立事件循环的半独立系统。

协程/绿色线程/光纤/角色:每个连接都由轻量级结构处理,该结构的内部调度(Go,Erlang,Scala / Akka)。

了解服务器如何处理连接对于了解其限制和扩展模式至关重要。即使是基本用法或配置,也需要了解连接处理的工作原理:例如,Redis和PostgreSQL提供不同的事务和服务。受各自连接处理机制影响的锁定语义。流程与如果未将最大线程数的最大计数设置为合理的限制,则基于线程的服务器可能会由于资源耗尽而崩溃,并且当设置了限制时,由于限制太低,它们可能会被严重利用不足。基于事件循环的系统可能不会完全受益于在64核CPU上运行,除非将它们的64个副本配置为同时运行-这对于Web服务器非常有用,但对于数据库却很少使用。

由于每个系统的分布式或集中式性质,当在应用程序服务器和数据库中使用这些处理连接的方式时,每种方式的执行都会有所不同。例如,应用程序服务器往往具有水平可伸缩性,无论您是1台服务器还是10台或10,000台服务器,它们都可以正常工作并且以相同的方式工作。在这些情况下,远离进程/线程模型往往会导致更高的性能,因为我们希望以最少的内存使用量和CPU上下文切换来完成很多工作。像Node这样的事件循环在单核服务器上工作得很好,但是需要正确地集群以使用多核服务器。诸如Go或Erlang之类的基于协程/ actor的系统将更轻松地利用CPU的每个内核,因为它们被设计为以这种方式工作,同时在一台计算机上同时运行数千个goroutine或actor。

另一方面,集中式数据库从流程/线程/事件循环处理中受益更多,因为基于系统的事务保证,我们不希望多个连接同时对相同数据进行操作。发生在多个连接上的操作将必须在其事务中对事务敏感的部分锁定,或者使用其他策略(例如MVCC),并且连接处理程序越少越好。这些系统在一台机器上支持几个连接。在大型服务器上,PostgreSQL可以管理数百个连接,MySQL可以处理数千个连接。 Redis可以处理最多数量的连接(可能数以万计),因为它使用事件循环设法使数据保持一致-但这意味着一次只能进行一次操作,因此这不是一次免费的午餐。

分布式数据库可以并且将尝试脱离基于进程和线程的模型:因为它们将数据分布在多台计算机上,所以它们通常放弃锁定,拥抱分区,并为大量服务器上的高连接量而设计。例如,AWS DynamoDB或Google Datastore或许多用Go编写的分布式数据库,将很乐意接受数百万或数十亿个并发连接。但是,这些决定会产生后果-它们会牺牲大量的操作(联接,临时查询)和由集中式/单服务器数据库提供的一致性保证。作为这种牺牲的回报,他们可以以分区的,水平可伸缩的,几乎不受限制的方式处理连接,从而允许他们选择支持多台计算机上许多连接的设计。这使得连接成为问题,每个人都必须担心其连接处理问题,但是总的来说,在具有智能连接路由功能的成千上万台机器上,这些系统的行为通常就像它们是无限可伸缩的。

我们需要通过昂贵的连接来高效而节俭。由于不对称,通常不容易意识到它们的昂贵程度:从客户端的角度来看,它们看起来很便宜-通常是服务器连接过多的问题。

Warning: Can only detect less than 5000 characters

既然我们已经了解了连接的一般处理方式,那么我们可以讨论几种不同的应用程序服务器+数据库组合,以及在最大程度地减少数据库连接数的同时如何最大化应用程序服务器上可以处理的请求数量的原因。尽管此处未涵盖所有组合,但大多数系统将具有与我们所涵盖的示例之一相同的特征-因此,即使此处未涵盖,了解这些系统的工作原理也将有助于您了解自己的系统。如果您要我添加更多组合或系统,请告诉我。

Puma是一种流行的应用程序服务器,它运行Ruby应用程序,并为传入HTTP请求的处理程序提供两种池。第一个杠杆是要启动的进程数,由worker配置指令表示。服务器的每个进程都是不同的,并且将整个应用程序堆栈独立地加载到内存中-因此,如果您的应用程序占用N MB或RAM,则需要确保至少有工人* N MB的RAM才能运行那么多它的副本。有一种方法可以减轻这种情况:Ruby 2+支持写时复制功能,该功能允许多个进程作为单个进程启动并分叉到多个进程中,而不必复制所有内存,因此很多公共内存区域将被复制。共享,直到对其进行某种方式的修改。使用preload_app激活写时复制!指令可能会帮助您使用比应用程序大小和工作程序总数的整数倍更少的内存,但不要过多地依靠它,而无需测试它在持续负载下给您带来的优势。

像Unicorn这样的纯粹基于进程的服务器会停止在此配置级别,Python,PHP和其他使用全局锁或假定执行单线程单进程执行的语言的流行服务器也是如此。每个进程一次只能处理一个请求,但这不能保证完全利用-如果该进程正在等待数据库查询或对另一服务的网络请求,则不会选择新的请求。请求,并且CPU内核保持空闲状态。为了避免这种浪费,您启动的进程可能比拥有CPU内核的进程更多(这导致上下文切换成本)或使用线程。

这将Puma带给您的第二个杠杆-您配置的每个进程/工作程序中运行的线程数。使用threads指令可以配置最小值和最大值

......