【问题标题】:Some fundamental but important questions about web development?关于 Web 开发的一些基本但重要的问题?
【发布时间】:2026-02-02 19:05:01
【问题描述】:

到目前为止,我已经使用 PHP、Python 和 Java 开发了一些基于 Web 的应用程序。但是一些基本但非常重要的问题仍然超出我的知识范围,所以我发了这篇文章来寻求你们的帮助和澄清。

假设我使用某种编程语言作为我的后端语言(PHP/Python/.Net/Java 等),并使用 Web 服务器(apache/lighttpd/nginx/IIS 等)部署我的应用程序。假设在时间 T,我的一个页面同时收到了来自不同用户的 100 个请求。所以我的问题是:

  1. 我的 Web 服务器如何处理这 100 个并发请求? Web 服务器会为每个请求生成一个进程/线程吗? (如果是,进程还是线程?)
  2. 后端语言的解释器是怎么做的?它将如何处理请求并生成正确的 html?解释器会为每个请求生成一个进程/线程吗?(如果是,是进程还是线程?)
  3. 如果解释器会为每个请求生成一个进程/线程,那么这些进程(线程)呢?他们会共享一些代码空间吗?他们会互相交流吗?如何处理后端代码中的全局变量?或者它们是独立的进程(线程)?进程/线程的持续时间是多久?当请求被处理并返回响应时,它们会被销毁吗?
  4. 假设 Web 服务器只能同时支持 100 个请求,但现在它得到了 1000 个并发请求。它如何处理这种情况?它会像队列一样处理它们并在服务器可用时处理请求吗?还是其他方法?
  5. 这些天我读了一些关于彗星的文章。而且我发现长连接可能是处理实时多用户用例的好方法。那么长连接呢?它是某些特定 Web 服务器的功能还是适用于每个 Web 服务器?长连接需要一个长期存在的解释器进程吗?

编辑: 最近看了一些关于CGI和fastcgi的文章,让我知道fastcgi的方式应该是一个比较典型的handle request方式。

该协议在多个独立的 FastCGI 请求之间多路复用单个传输连接。这支持能够使用事件驱动或多线程编程技术处理并发请求的应用程序。

引自fastcgi spec,其中提到connection可以处理多个请求,并且可以在多线程技术中实现。我想知道这个 connection 可以被视为 process 并且它可以为每个请求生成多个 threads。如果这是真的,我会更加对如何处理每个线程中的共享资源感到困惑?

P.S 感谢 Thomas 关于将帖子拆分为多个帖子的建议,但我认为这些问题是相关的,最好将它们组合在一起。

感谢 S.Lott 的精彩回答,但每个问题的一些答案都过于简短或根本没有涵盖。

感谢大家的回答,让我更接近真相了。

【问题讨论】:

  • 请拆分这些问题并搜索重复项。它们很好而且有效,但对许多人来说都是一个问题。
  • 仅供参考。尽管 FASTCGI 规范支持连接多路复用,但是嵌入在 Web 服务器中的 FASTCGI 的主要实现都没有支持它。多路复用虽然有助于减少正在使用的文件描述符的数量,但根据实现和操作系统,实际上会降低请求处理吞吐量,因为您试图将许多连接推向有限的单个资源,随后可能会产生交叉影响处理慢速 HTTP 客户端时的请求之间。

标签: webserver cpu-usage


【解决方案1】:

我的网络服务器如何处理这 100 个并发请求? Web 服务器是否为每个请求生成一个进程/线程? (如果是,进程还是线程?)

它因人而异。 Apache 有处理请求的线程和进程。 Apache 启动多个并发进程,每个进程都可以运行任意数量的并发线程。您必须配置 Apache 来控制每个请求的实际执行方式。

后端语言的解释器是怎么做的?它将如何处理请求并生成正确的 html?解释器会为每个请求生成一个进程/线程吗?(如果是,是进程还是线程?)

这取决于您的 Apache 配置和您的语言。对于 Python,一种典型的方法是让守护进程在后台运行。每个 Apache 进程都拥有一个守护进程。这是通过 mod_wsgi 模块完成的。它可以配置为以几种不同的方式工作。

如果解释器会为每个请求生成一个进程/线程,那么这些进程(线程)呢?他们会共享一些代码空间吗?他们会互相交流吗?如何处理后端代码中的全局变量?或者它们是独立的进程(线程)?进程/线程的持续时间是多久?处理请求并返回响应时它们会被销毁吗?

线程共享相同的代码。根据定义。

进程将共享相同的代码,因为这就是 Apache 的工作方式。

他们不会 - 故意 - 相互交流。您的代码无法轻松确定其他情况。这是设计使然。你无法分辨自己在哪个进程中运行,也无法分辨这个进程空间中还有哪些其他线程正在运行。

这些进程是长期运行的。它们不会(也不应该)动态创建。您可以将 Apache 配置为在它开始时派生多个并发副本,以避免创建进程的开销。

线程创建的开销要少得多。 Apaches 如何在内部处理线程并不重要。但是,您可以将 Apache 视为每个请求启动一个线程。

假设 Web 服务器只能同时支持 100 个请求,但现在它得到了 1000 个同时请求。它如何处理这种情况?它会像队列一样处理它们并在服务器可用时处理请求吗?还是其他方法?

这是“可扩展性”问题。简而言之——随着负载的增加,性能将如何下降。一般的答案是服务器变慢。对于某些负载级别(比如说 100 个并发请求),有足够的进程可用,它们都运行得相当快。在某个负载级别(例如 101 个并发请求)它开始变慢。在其他一些负载级别(谁知道有多少请求)它变得如此缓慢,您对速度不满意。

有一个内部队列(通常作为 TCP/IP 工作方式的一部分),但没有将工作负载限制为 100 个并发请求的调控器。如果您收到更多请求,则会创建更多线程(而不是更多进程)并且运行速度会更慢。

【讨论】:

  • 关于 mod_wsgi 您的声明'对于 Python,一种典型的方法是让守护进程在后台运行。每个 Apache 进程都拥有一个守护进程。这是通过 mod_wsgi 模块完成的。它可以配置为以几种不同的方式工作。是错的。在 mod_wsgi 的情况下,每个 Apache 进程不拥有一个守护进程。 Apache 父进程是所有 mod_wsgi 守护进程的所有者/管理者。任何 Apache 服务器子进程都可以代理到任何 mod_wsgi 守护进程。他们代理的对象将取决于 WSGIProcessGroup 的配置。
  • 有关 mod_wsgi 进程/线程模型的详细信息,请参阅“code.google.com/p/modwsgi/wiki/ProcessesAndThreading”。
【解决方案2】:

2018 年春季更新

我在 2010 年写了这篇回复,从那时起,Web 后端开发人员的世界发生了很多变化。也就是说,将一键式负载均衡器和自动缩放等“云”服务转变为商品的出现,使扩展应用程序的实际机制更容易上手。

也就是说,我在 2010 年的这篇文章中所写的内容在今天大部分仍然适用,并且了解您的 Web 服务器和语言托管环境实际工作的机制以及如何对其进行调整可以为您节省大量托管成本.出于这个原因,我将这篇文章保留在下面,供那些开始深入调整筹码的人使用。


1. 取决于网络服务器(有时是这样的配置)。各种型号的说明:

  • 带有 mpm_prefork 的 Apache(unix 上的默认值):按请求处理。为了最大限度地缩短启动时间,Apache 保留了一个空闲进程池,等待处理新请求(您可以配置其大小)。当一个新的请求进来时,主进程将它委托给一个可用的工作者,否则会产生一个新的。如果有 100 个请求进来,除非你有 100 个空闲的工作人员,否则需要进行一些分叉来处理负载。如果空闲进程的数量超过 MaxSpare 值,则在完成请求后会收获一些,直到只有这么多空闲进程。

  • 具有 mpm_event、mpm_worker、mpm_winnt 的 Apache:每个请求的线程。同样,apache 在大多数情况下都会保留一个空闲线程池,也是可配置的。 (一个小细节,但功能相同:mpm_worker 运行多个进程,每个进程都是多线程的)。

  • Nginx/Lighttpd:这些是轻量级的基于事件的服务器,它们使用 select()/epoll()/poll() 来多路复用多个套接字,而不需要多个线程或进程。通过非常仔细的编码和非阻塞 API 的使用,它们可以扩展到商品硬件上的数千个同时请求,提供可用带宽和正确配置的文件描述符限制。需要注意的是,在服务器上下文中实现传统的嵌入式脚本语言几乎是不可能的,这将抵消大部分好处。两者都支持 FastCGI,但是对于外部脚本语言。

2. 取决于您使用的部署模型所使用的语言或某些语言。某些服务器配置仅允许某些部署模型。

  • Apache mod_php、mod_perl、mod_python:这些模块为每个 apache worker 运行一个单独的解释器。其中大多数不能很好地与 mpm_worker 一起使用(由于客户端代码中的线程安全问题),因此它们大多仅限于分叉模型。这意味着对于每个 apache 进程,你都有一个 php/perl/python 解释器在里面运行。这严重增加了内存占用:如果给定的 apache 工作人员通常会在您的系统上占用大约 4MB 的内存,那么对于一个普通的应用程序来说,使用 PHP 的可能需要 15mb,而使用 Python 的可能需要 20-40MB。其中一些将是进程之间的共享内存,但一般来说,这些模型很难扩展到非常大的规模。

  • Apache(支持的配置)、Lighttpd、CGI:这主要是一种濒临灭绝的托管方法。 CGI 的问题在于,您不仅要创建一个新进程来处理请求,而且还要为每个请求执行此操作,而不仅仅是在您需要增加负载时。由于当今的动态语言具有相当长的启动时间,这不仅为您的网络服务器创建了大量工作,而且显着增加了页面加载时间。一个小的 perl 脚本可以作为 CGI 运行,但一个大型的 python、ruby 或 java 应用程序相当笨拙。在 Java 的情况下,您可能要等待一秒钟或更长时间才能启动应用程序,但必须在下一次请求时再次执行所有操作。

  • 所有网络服务器,FastCGI/SCGI/AJP:这是运行动态语言的“外部”托管模型。有很多有趣的变化,但要点是您的应用程序侦听某种套接字,Web 服务器处理 HTTP 请求,然后通过另一个协议将其发送到套接字,仅适用于动态页面(静态页面是通常由网络服务器直接处理)。

    这带来了许多优势,因为与处理连接的能力相比,您需要更少 个动态工作者。如果每 100 个请求中,有一半用于静态文件,例如图像、CSS 等,而且如果大多数动态请求都很短,那么您可能需要 20 个动态工作人员同时处理 100 个客户端。也就是说,由于给定网络服务器保持活动连接的正常使用是 80% 空闲,您的动态解释器可以处理来自其他客户端的请求。这比 mod_php/python/perl 方法要好得多,在这种方法中,当您的用户加载 CSS 文件或根本不加载任何内容时,您的解释器会使用内存而不做任何工作。

  • Apache mod_wsgi:这特别适用于托管 python,但它具有 web 服务器托管应用程序(易于配置)和外部托管(进程多路复用)的一些优点。当您在守护程序模式下运行它时,mod_wsgi 仅在需要时将请求委托给您的守护程序工作人员,因此 4 个守护程序可能能够同时处理 100 个用户(取决于您的站点及其工作负载)

  • Phusion Passenger:Passenger 是一个 apache 托管系统,主要用于托管 ruby​​ 应用程序,与 mod_wsgi 一样,它提供了外部托管和网络服务器托管托管的优势。

3.同样,我将根据适用的托管模型来拆分问题。

  • mod_php、mod_python、mod_perl:通常只有应用程序的 C 库会在 apache 工作人员之间共享。这是因为 apache 首先分叉,然后加载您的动态代码(由于微妙之处,大多数情况下无法使用共享页面)。口译员在此模型中不相互交流。通常不共享全局变量。在 mod_python 的情况下,您可以让全局变量留在进程内的请求之间,但不能跨进程。这可能会导致一些非常奇怪的行为(浏览器很少永远保持相同的连接,并且大多数浏览器会打开几个到给定网站的连接)所以要非常小心如何使用全局变量。使用 memcached 或数据库或文件之类的东西来存储会话存储和其他需要共享的缓存位。

  • FastCGI/SCGI/AJP/Proxied HTTP:因为您的应用程序本身本质上是一个服务器,这取决于编写服务器的语言(通常与您的代码相同,但并非总是如此)以及各种因素。例如,大多数 Java 部署使用每个请求的线程。 Python 及其“flup”FastCGI 库可以在 prefork 或线程模式下运行,但由于 Python 及其 GIL 受到限制,您可能会从 prefork 获得最佳性能。

  • mod_wsgi/passenger:在服务器模式下的 mod_wsgi 可以配置它处理事情的方式,但我建议你给它一个固定个进程数。您希望将您的 python 代码保存在内存中,并准备好运行。这是保持可预测和低延迟的最佳方法。

在上面提到的几乎所有模型中,进程/线程的生命周期都比单个请求长。大多数设置都遵循 apache 模型的一些变化:保留一些备用工人,在需要时产生更多,当有太多时收获,基于一些可配置的限制。大多数这些设置 - 不会 - 在请求后破坏进程,尽管有些可能会清除应用程序代码(例如 PHP fastcgi 的情况)。

4. 如果您说“网络服务器只能处理 100 个请求”,这取决于您是指实际的网络服务器本身还是网络服务器的动态部分。实际限制和功能限制之间也存在差异。

以 Apache 为例,您将配置最大工作线程数(连接数)。如果此连接数为 100 并且已达到,则 apache 将不再接受任何连接,直到有人断开连接。启用 keep-alive 后,这 100 个连接可能会长时间保持打开状态,比单个请求长得多,而等待请求的其他 900 人可能会超时。

如果您的限制足够高,您可以接受所有这些用户。然而,即使使用最轻量级的 apache,每个工作人员的成本也约为 2-3mb,因此仅使用 apache,您可能会谈论 3gb+ 的内存来处理连接,更不用说其他可能有限的操作系统资源,如进程 ID、文件描述符、和缓冲区,这是在考虑您的应用程序代码之前。

对于 lighttpd/Nginx,它们可以在很小的内存占用中处理大量连接(数千个),通常每千个连接只有几兆(取决于缓冲区等因素以及异步 IO api 的设置方式)。如果我们继续假设您的大多数连接都保持活动状态并且 80%​​(或更多)空闲,那么这非常好,因为您不会浪费动态处理时间或大量内存。

在任何外部托管模型 (mod_wsgi/fastcgi/ajp/proxied http) 中,假设您只有 10 个工作人员和 1000 个用户发出请求,您的网络服务器会将请求排队到您的动态工作人员。这是理想的:如果您的请求快速返回,您可以继续处理更大的用户负载,而无需更多的工作人员。通常溢价是内存或数据库连接,通过排队,您可以使用相同的资源为更多用户提供服务,而不是拒绝某些用户。

小心:假设您有一个页面构建报告或进行搜索并需要几秒钟,而很多用户会为此占用工作人员:想要加载您的首页的人可能会排队等待几秒钟而所有那些长期运行的请求都完成了。替代方案是使用单独的工作人员池来处理报告应用程序部分的 URL,或者单独进行报告(如在后台作业中),然后稍后轮询其完成情况。那里有很多选项,但需要您在应用程序中考虑一下。

5. 大多数使用 apache 的人需要同时处理大量用户,因为内存占用高,请关闭 keep-alive。或者启用了保持活动的 Apache,保持活动时间限制很短,比如 10 秒(这样你就可以在单个页面加载中获取首页和图像/CSS)。如果您确实需要扩展到 1000 个或更多连接并希望保持活动状态,您将需要查看 Nginx/lighttpd 和其他基于事件的轻量级服务器。

可能需要注意的是,如果您确实想要 apache(为了便于配置使用,或者需要托管某些设置),您可以使用 HTTP 代理将 Nginx 放在 apache 前面。这将允许 Nginx 处理 keep-alive 连接(最好是静态文件),而 apache 只处理 grunt 工作。有趣的是,Nginx 在编写日志文件方面也恰好比 apache 好。对于生产部署,我们对 apache 前面的 nginx 非常满意(在本例中使用 mod_wsgi)。 apache 不做任何访问日志记录,也不处理静态文件,允许我们禁用 apache 内部的大量模块以使其占用空间小。

我基本上已经回答了这个问题,但是不,如果您的连接很长,则它不必对解释器运行多长时间有任何影响(只要您使用的是外部托管应用程序,现在应该清楚是非常优越的)。因此,如果您想使用彗星,并且要长时间保持活动状态(如果可以处理的话,这通常是一件好事)考虑使用 nginx。

Bonus FastCGI 问题您提到 fastcgi 可以在单个连接中进行多路复用。协议确实支持这一点(我相信这个概念被称为“通道”),因此理论上单个套接字可以处理大量连接。但是,它不是 fastcgi 实现者的必需功能,实际上我不相信有一个服务器使用它。大多数 fastcgi 响应者也不使用此功能,因为实现此功能非常困难。大多数网络服务器一次只会通过给定的 fastcgi 套接字发出一个请求,然后通过该套接字发出下一个请求。因此,每个进程/线程通常只有一个 fastcgi 套接字。

您的 fastcgi 应用程序是使用处理还是线程(以及您是通过接受连接和委派的“主”进程来实现它,还是通过大量进程来实现它,每个进程都在做自己的事情)取决于您;并且也会根据您的编程语言和操作系统的功能而有所不同。在大多数情况下,库使用的默认值应该没问题,但要准备好进行一些基准测试和参数调整。

关于共享状态,我建议您假装不存在任何传统的进程内共享状态使用:即使它们现在可以工作,您以后可能不得不将动态工作器拆分到多台机器上。对于购物车等状态; db 可能是最好的选择,会话登录信息可以保存在 securecookies 中,对于临时状态,类似于 memcached 的东西非常简洁。您对共享数据的功能(“无共享”方法)的依赖越少,您未来的规模就越大。

后记:我在上面的整个设置范围内编写和部署了大量动态应用程序:上面列出的所有网络服务器,以及 PHP/Python/Ruby/ 范围内的所有内容爪哇。我已经对这些方法进行了广泛的测试(使用基准测试和实际观察),结果有时令人惊讶:少即是多。一旦您不再在网络服务器进程中托管您的代码,您通常可以使用极少数的 FastCGI/Mongrel/mod_wsgi/etc 工作人员。这取决于您的应用程序在数据库中停留的时间,但通常情况下,多于 2*数量的 CPU 的进程实际上不会为您带来任何好处。

【讨论】:

【解决方案3】:

首先,恕我直言,要求对所有观点的详细回答有点过分。

无论如何,关于您的问题的一些简短答案:

#1

这取决于服务器的架构。 Apache 是一个多进程服务器,也可选择多线程服务器。有一个主进程监听网络端口,并管理一个工作进程池(在“工作”mpm 的情况下,每个工作进程都有多个线程)。当一个请求进来时,它被转发给一个空闲的工作人员。 master 通过根据负载和配置设置启动和终止工作人员来管理工作人员池的大小。

现在,lighthttpd 和 nginx 是不同的;它们是所谓的基于事件的架构,通过使用操作系统对事件多路复用的支持,例如 POSIX 中的经典 select()/poll(),多个网络连接被多路复用到一个或多个工作进程/线程上,或者更具可扩展性但不幸的是,特定于操作系统的机制,例如 Linux 中的 epoll。这样做的好处是每个额外的网络连接可能只需要几百字节的内存,允许这些服务器保持打开数以万计的连接,这对于像 apache 这样的每进程请求/线程架构通常是禁止的.但是,这些基于事件的服务器仍然可以使用多个进程或线程来利用多个 CPU 内核,并且还可以并行执行阻塞系统调用,例如正常的 POSIX 文件 I/O。

有关详细信息,请参阅有些过时的C10k page by Dan Kegel

#2

同样,这取决于。对于经典 CGI,每个请求都会启动一个新进程。对于带有 apache 的 mod_php 或 mod_python,解释器嵌入到 apache 进程中,因此无需启动新进程或线程。然而,这也意味着每个 apache 进程都需要相当多的内存,并且结合我上面针对 #1 解释的问题,限制了可扩展性。

为了避免这种情况,可以让一个单独的重量级进程池运行解释器,并在需要生成动态内容时由前端 Web 服务器代理到后端。这本质上是 FastCGI 和 mod_wsgi 采用的方法(尽管它们使用自定义协议而不是 HTTP,所以从技术上讲它可能不是代理)。这也是使用基于事件的服务器时通常选择的方法,因为生成动态内容的代码很少是可重入的,为了在基于事件的环境中正常工作,它需要这样做。如果动态内容代码不是线程安全的,多线程方法也是如此;例如,可以将前端 apache 服务器与线程工作者 mpm 代理到后端 apache 服务器,使用单线程 prefork mpm 运行 PHP 代码。

#3

根据您要求的级别,它们将通过操作系统缓存机制共享一些内存,是的。但通常,从程序员的角度来看,它们是独立的。请注意,这种独立性本身并不是一件坏事,因为它可以直接水平扩展至多台机器。但是很遗憾,通常需要一些沟通。一种简单的方法是通过数据库进行通信,假设由于其他原因需要一个,通常是这样。另一种方法是使用一些专用的分布式内存缓存系统,例如memcached

#4

视情况而定。它们可能会排队,或者服务器可能会回复一些合适的错误代码,例如 HTTP 503,或者服务器可能一开始就拒绝连接。通常,以上所有情况都可能发生,具体取决于服务器的负载情况。

#5

这种方法的可行性取决于服务器架构(请参阅我对 #1 的回答)。对于基于事件的服务器,保持连接打开不是什么大问题,但对于 apache 来说,这肯定是因为每个连接都需要大量内存。是的,这当然需要一个长时间运行的解释器进程,但如上所述,除了经典 CGI 之外,这几乎是理所当然的。

【讨论】:

    【解决方案4】:

    Web 服务器是多线程环境;除了使用应用程序范围的变量外,用户请求不与其他线程交互。

    所以:

    1. 是的,将为每个用户创建一个新线程
    2. 是的,将为每个请求处理 HTML
    3. 您需要使用应用程序范围的变量
    4. 如果您收到的请求超出您的处理能力,它们将被放入队列中。如果他们在配置的超时期限之前得到服务,用户将收到他的响应,或类似“服务器忙”的错误。
    5. Comet 并不特定于任何服务器/语言。您可以通过每 n 秒查询一次服务器来获得相同的结果,而无需处理其他令人讨厌的线程问题。

    【讨论】:

    • Web 服务器不一定是多线程的。可以是基于进程的、基于线程的或基于参与者的。这取决于所使用的软件堆栈的架构。
    • Apache 并没有为每个请求创建一个线程,而是一个进程 差别很大。
    • 只有非常简单的实现才会为到达的用户创建一个线程。这太贵了。
    • HTML 不会被处理,它是一个 HTTP 请求。
    • @Itay。除非您专门讨论 CGI,否则 Apache 也不会为每个请求创建一个进程。对于在 Apache 服务器子进程内部处理请求的 Apache 模块,该进程在请求和线程之间持续存在,在 prefork MPM 的情况下为 1 个线程,在 worker 或 winnt MPM 的情况下为 n 个线程,从一个请求到下一个请求重用。