🕸️ 第二章 Web特有的架构
📄 一句话概括

CGI 是【每来一次请求就起一个新程序】的做法,它最大的问题不在能不能用,而在把最贵的那一步放在了每次请求里。

📝 本节和前面几节的关系

2-5 说动态页面要【起一个程序】,但没说起程序这件事本身要花多少代价。这一条讲的就是这个代价,以及为什么后来大家换掉了它。它是本章的一个转折点:前面讲的都是【看到什么】,从这里开始讲【后台怎么把它做出来】。

一、CGI 的做法

CGI 是一套很直白的规矩:服务器把请求的内容变成一组环境变量,启动一个程序,程序把结果从标准输出送回来,服务器再转发给浏览器。

它好懂、好写、语言无关——这正是它当年能流行起来的原因。

问题出在它的执行方式上:每来一次请求,就起一个新进程,干完就退出。

二、动手 Demo:把点火的成本算出来

parts = [("读取脚本文件", 1), ("启动语言解释器", 30), ("建立数据库连接", 20)]
for name, ms in parts:
    print("%-16s 约 %2d 毫秒" % (name, ms))
total = sum(ms for _, ms in parts)
print("合计            约 %d 毫秒" % total)
print("每秒 100 次请求:光启动就要 %d 毫秒 -> 需要 %.0f 个 CPU 核心"
      % (100 * 30, 100 * 30 / 1000.0))

实跑输出:

--------------------------------------------------------------------
D9  程序的启动:CGI
--------------------------------------------------------------------
  静态页面是服务器【读文件】,动态页面要服务器【起程序】。
  CGI 是干这件事最早的一套规矩,做法很直白:
    服务器把请求变成环境变量,启动一个程序;
    程序把生成的结果从标准输出吐回来,服务器再转发给浏览器。

  它的问题不在能不能用,在【每来一次请求就起一个新进程】:
  ----------------------------------------------------------------------------------------
  环节                      开销            说明
  ----------------------------------------------------------------------------------------
  读取脚本文件              约 1 毫秒       从磁盘把代码读进内存
  启动语言解释器            约 30 毫秒      Python 这类要先起虚拟机
  建立数据库连接            约 20 毫秒      每次连接都要握手
  合计                      约 51 毫秒      每次请求都要重来一遍
  ----------------------------------------------------------------------------------------

  51 毫秒里,真正在处理业务的大概只有几毫秒,其余都花在【点火】上。

  把它放大到每秒 100 条请求来看:
    每秒要起 100 个进程,光启动就是 100 x 30 = 3000 毫秒。
    意思是:需要 3 个 CPU 核心什么都不干,专门负责启动进程。

  FastCGI 换了个做法:进程先起好、一直待着,来一条请求接一条。
  ----------------------------------------------------------------------------------------
  做法          每次请求的启动成本        一台机器的吞吐
  ----------------------------------------------------------------------------------------
  CGI           约 30 毫秒                每秒数百次
  FastCGI       约 0.5 毫秒(复用)       每秒数千次以上
  ----------------------------------------------------------------------------------------

  同样一台机器,吞吐差出一个数量级。这就是 CGI 从主流退场的原因——
  不是因为不好用,是因为它把最贵的那一步放在了每次请求里。

  ⚠️ 2026 现状:CGI 没有死,但它基本只活在三个地方——
    遗留系统(还能跑就先不动)、低流量站点(简单胜过高效)、
    路由器与摄像头这类嵌入式设备(资源少,够用就行)。
    Apache 2.4 至今仍带着 mod_cgi 模块。

  今天接替它的,是一整套按语言分家的接口:
  ----------------------------------------------------------------------------------------
  接口        属于哪个语言        为什么快
  ----------------------------------------------------------------------------------------
  FastCGI     语言无关            进程常驻,请求复用
  WSGI        Python              应用对象常驻,只调一次函数
  ASGI        Python              同上,还支持异步与长连接
  Servlet     Java                由容器管理线程池
  mod_php     PHP                 解释器直接嵌在服务器里
  ----------------------------------------------------------------------------------------

  这些名字看着多,说的其实是同一件事:
  别为每条请求都重新点一次火。

读这个输出:51 毫秒里真正在处理业务的大概只有几毫秒,其余都花在启动解释器和建连接上。再往下看最后一行——每秒一百次请求,就要拿三个 CPU 核心专门负责启动进程。这个成本不是【有点浪费】,而是把机器的大部分力气用在了重复的准备工作上。

三、后来怎么改的

FastCGI 的思路很简单:别每次重启,让进程一直待着。请求来了就接一条,处理完不退出。这样一来,启动成本从【每次 30 毫秒】降到【几乎为零】。

同样一台机器,吞吐能差出一个数量级——不是因为程序写得更好了,是因为最贵的那一步被挪走了。

今天接替它的是一整套按语言分家的接口:FastCGI(语言无关)、WSGI 与 ASGI(Python)、Servlet(Java)、mod_php(PHP 直接嵌进服务器)。名字看着多,说的其实是同一件事:别为每条请求都重新点一次火。

四、⚠️ 2026 现状

CGI 没有死,但它基本只活在三个地方:遗留系统(还能跑就先不动)、低流量站点(简单胜过高效)、路由器与摄像头这类嵌入式设备(资源少,够用就行)。Apache 2.4 至今仍带着 mod_cgi 模块。

顺带说一句安全上的老账:CGI 的经典漏洞是把用户输入当成命令去执行。这套规矩把【程序】和【输入】混在一起传递,本身就给命令注入留了口子——这也是它被更结构化的接口取代的原因之一。

30 秒实验

  1. 打开你常用的一个网站,想象它每来一个访问者就要重新启动一遍程序,估一下它每天要启动多少次。
  2. 再想想这个过程里,哪一部分是每次都一样的准备工作。
  3. 最后回答一个问题:如果这些准备工作能只做一次,省下来的是时间还是钱?

常见误区

  1. 错误:CGI 是过时的技术,不用了解。——纠正:它是理解【后台怎么跑起来】这套逻辑的起点。今天用的 FastCGI、WSGI、Servlet,全都是在回答 CGI 提出的同一个问题——只是把答案换成了更省的做法。不了解它,就不知道现在这套做法在省什么。
  2. 错误:慢一点没关系,反正是后台。——纠正:后台慢会直接变成用户的等待。而且这个慢是按访问次数累加的——一万次访问就是多花一万次启动时间。后台的每一次浪费,都会被访问量放大。
  3. 错误:只要换成更快的语言就行了。——纠正:问题不在语言快不快,在【每请求起一次进程】这个做法。换成任何语言,只要还是每次重启,那 30 毫秒的启动开销都跑不掉。先改做法,再谈换语言。
  4. 错误:CGI 只是性能问题。——纠正:它还留下一批经典的安全教训。把用户输入直接交给一个能被外部触发的程序,稍不注意就变成命令注入——这条教训今天仍然有效,只是换了地方出现。
自测三问(点开对答案) 1. CGI 最根本的问题是什么? —— **把最贵的那一步放在了每次请求里**。启动解释器、建数据库连接这些准备工作,每条请求都要重来一遍。**它本身没有做错事,只是把一个应该只做一次的动作,做成了每次都做。** 2. FastCGI 改了什么? —— **让进程常驻**。进程先起好、一直待着,请求来了接一条、处理完不退出。这样启动成本只在最开始付一次。**同样一台机器,吞吐因此差出一个数量级**——改的是结构,不是代码优化。 3. 为什么说这个问题和 2-1 讲的【改得起】是一条线? —— 因为**动态页面要能快速迭代,就不能让启动开销拖住每一次请求**。后台从 CGI 换到常驻进程,本质上和低代码、前后端分离在解决同一件事:**把重复的准备工作从【每次】变成【一次】**,让【改】这件事变得更便宜。

相关笔记