CGI到底是个啥?从原理到演进,一次讲透
CGI全称 Common Gateway Interface通用网关接口。注意这几个字——通用说明不绑定任何编程语言网关说明是个中间人角色接口说明它是一套规范协议不是什么具体的软件。说白了CGI就是一份合同规定了Web服务器跟外部程序怎么通信。没有这份合同之前Web服务器只能干一件事你请求什么文件我就把那个文件原样扔回去。全是静态HTML写死的内容谁访问都一样。有了CGI之后就不一样了。Web服务器可以外包了——收到一个请求自己处理不了就按照CGI协议把请求数据打包交给外部脚本去跑脚本跑完把结果送回来服务器再转交给浏览器。页面内容就是动态生成的不同用户不同时间看到的都不一样。打个比方Web服务器就像一个前台接待平时就负责发发宣传册静态文件。来了个客户问我上个月的订单啥情况前台自己答不上来按照CGI这个转接规范把客户问题记下来转给后边的业务部门外部脚本业务部门查完系统把答案给前台前台再告诉客户。CGI就是那个转接规范。CGI在Web服务器里干了啥核心就三个词处理、传递、扩展。处理HTTP请求。静态文件服务器干不了的事——查数据库、做计算、根据用户身份返回不同内容——这些活儿全得靠CGI调用的外部程序来完成。你在网页上点了个查询按钮后端真正去数据库里捞数据的就是那个被CGI调起来的脚本。数据传递。CGI最核心的角色就是桥梁。浏览器发过来的数据表单参数、URL查询串、Cookie什么的Web服务器自己不处理通过CGI协议把这些数据喂给外部脚本脚本跑完输出的内容一般是HTML也可以是JSON、图片二进制再由Web服务器端回浏览器。整个过程中Web服务器不关心脚本内部怎么实现的脚本也不关心自己是在Apache下面跑还是Nginx下面跑大家只认CGI这个协议。这就是接口的威力——解耦。扩展服务器功能。这点特别关键。你想给Web服务器加个新功能不用去改它的源码重新编译写个脚本就行。今天想用Perl处理请求写个Perl脚本明天想用Python写个Python脚本后天想用C写个编译型程序跑也行。Web服务器一视同仁只要你的程序符合CGI规范它就调用。这种可插拔的架构设计放到今天看也是相当优雅的。一次CGI请求的完整流程光说概念不够咱们把一次完整的请求流程走一遍。假设用户在浏览器里提交了一个表单表单的action指向一个CGI脚本比如/cgi-bin/query.pl。浏览器发请求。用户点了提交浏览器往服务器发一个HTTP请求可能是GET也可能是POSTURL里带着脚本路径和参数。Web服务器识别CGI请求。服务器收到请求一看这个路径在/cgi-bin/下面按照配置规则得交给CGI处理。服务器开始准备环境——把HTTP请求头里的信息变成环境变量QUERY_STRING、REQUEST_METHOD、CONTENT_TYPE这些如果是POST请求还得把请求体通过标准输入stdin传给脚本。这里插一句CGI的数据传递机制就两种环境变量和标准输入。URL参数走环境变量QUERY_STRINGPOST数据走stdin。脚本那边通过读环境变量和stdin就能拿到所有请求数据简单粗暴。CGI脚本执行。Web服务器fork出一个新进程在这个进程里执行对应的CGI脚本。脚本拿到数据后开始干活——可能查数据库可能做一堆计算可能调别的API。处理完了脚本把结果输出到标准输出stdout一般第一行是Content-Type: text/html这样的HTTP头空一行之后是HTML正文。Web服务器返回响应。服务器拿到脚本的stdout输出封装成HTTP响应发回浏览器。用户就看到了动态生成的页面。整个流程走下来最要命的一步就是fork新进程那一步。每次请求都要fork请求完了进程就销毁。这个后面细说。实际搭一个CGI环境看看光说不练假把式。我之前在一台CentOS 7上搭过CGI环境拿Apache做的过程很简单。装Apacheyuminstallhttpd-ysystemctl start httpdApache默认就带了mod_cgi模块CGI脚本的存放目录一般是/var/www/cgi-bin/。去/etc/httpd/conf/httpd.conf里能看到这么一段ScriptAlias /cgi-bin/ /var/www/cgi-bin/ Directory /var/www/cgi-bin AllowOverride None Options None Require all granted /Directory这表示/cgi-bin/路径下的请求都会被当作CGI脚本处理。写个最简单的Perl脚本存成/var/www/cgi-bin/hello.pl#!/usr/bin/perlprintContent-Type: text/html\n\n;printhtmlbodyh1Hello from CGI!/h1/body/html;给执行权限chmodx /var/www/cgi-bin/hello.pl浏览器访问http://你的IP/cgi-bin/hello.pl就能看到页面了。再写个能接收参数的Python版本存成/var/www/cgi-bin/greet.py#!/usr/bin/python3importos queryos.environ.get(QUERY_STRING,)namequery.split()[1]ifinqueryelseStrangerprint(Content-Type: text/html\n\n)print(fhtmlbodyh1Hello,{name}!/h1/body/html)访问http://你的IP/cgi-bin/greet.py?name张三页面上就会显示Hello, 张三!。你看CGI的编程模型就是这么直白——读环境变量和stdin往stdout写内容。没有任何框架没有任何魔法。这跟现在动不动就Spring Boot、Django一大套框架的画风完全不一样但正是这种朴素让你能看清楚Web服务器跟后端程序之间到底是怎么交互的。CGI的致命问题性能前面说了CGI每次请求都要fork一个新进程。这个事有多严重算一笔账就知道了。假设一个CGI脚本用Python写的Python解释器启动一次大概50-100ms取决于机器性能和加载的模块。并发100个请求服务器就得同时fork出100个Python进程每个进程启动都要50ms内存占用一个进程少说30MB100个就是3GB。还只是一个简单的脚本要是脚本里还import了一堆第三方库启动时间更长内存更大。我之前维护过一个老系统用的Perl写的CGI脚本跑在Apache上。平时访问量不大几十QPS还能撑住。有一次搞活动流量突然涨到几百QPS服务器直接load飙到几十SSH都登不上去。最后只能临时加机器硬扛。问题就出在fork进程这个机制上。进程创建和销毁的系统开销太大了而且每个进程都是独立的不能共享数据库连接、不能共享内存里的缓存什么资源都得从头初始化一遍。这就好比一个餐厅每来一个客人就临时雇一个厨师厨师做完这道菜就解雇。来100个客人就雇100个厨师厨房根本塞不下。正常做法难道不是雇几个常驻厨师客人来了排队做菜吗而且还有个问题进程fork出来的时间是不确定的。系统负载低的时候可能几毫秒就完事负载一高fork本身就可能卡住。再加上Python/Perl这些解释型语言的启动开销一个请求光在准备阶段就花掉大几百毫秒用户体验能好才怪。FastCGICGI的进化形态上面那个常驻厨师的思路就是FastCGI。FastCGI的核心改进就一句话进程不销毁常驻内存处理完一个请求接着处理下一个。FastCGI的工作模式是这样的启动的时候预先fork出一组worker进程数量可配这些进程一直活着通过Unix Socket或TCP连接跟Web服务器通信。请求来了Web服务器把数据通过这个连接发给workerworker处理完把结果发回来连接不断继续等下一个请求。对比一下维度CGIFastCGI进程生命周期请求来时创建请求完销毁常驻处理多个请求进程间通信环境变量 stdin/stdoutSocket连接数据库连接每次重新建立可复用并发处理每请求一进程固定数量进程池内存占用随并发线性增长相对固定拿Nginx PHP-FPM来说PHP-FPM就是一个FastCGI进程管理器。它管理着一组PHP-CGI worker进程Nginx收到PHP请求后通过FastCGI协议转发给PHP-FPMPHP-FPM分配一个空闲的worker处理处理完归还到进程池。这套架构到现在还是PHP世界的主流方案。Nginx配置FastCGI的写法大概长这样location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; include fastcgi_params; }fastcgi_pass就是告诉NginxPHP请求往127.0.0.1:9000这个地址转PHP-FPM就监听在这个端口上。FastCGI还有个好处是worker进程可以部署在不同的机器上。Web服务器跟FastCGI进程之间是Socket通信那就意味着可以是TCP Socket天然支持远程部署。CGI就不行脚本必须在Web服务器本机因为它是靠fork本地进程来执行的。这点在需要做水平扩展的时候差别就很大了。不过FastCGI也不是银弹。worker进程数是有限的请求量突然暴增超过worker处理能力请求就会排队响应时间变长。PHP-FPM的max_children参数调不好要么资源浪费要么502报错这个踩过坑的都懂。ServletJava的方案Java走的是另一条路。Servlet不fork进程而是在Web服务器或者Servlet容器比如Tomcat的进程内部用线程来处理请求。线程比进程轻量得多创建和切换的开销小很多。Servlet的运行机制是这样的Tomcat启动的时候加载Servlet类实例化之后常驻内存。请求来了Tomcat从线程池里拿一个线程调用Servlet的service()方法内部根据HTTP方法分发给doGet()或doPost()处理完线程归还。Servlet实例始终在内存里所有线程共享同一个实例所以Servlet要注意线程安全。对比CGIServlet省掉了进程创建的开销还能共享内存状态数据库连接池、缓存性能优势非常明显。这也是Java在企业级Web开发中长期占据主导地位的原因之一。我之前做过一个项目从CGI迁移到Servlet的改造。原来那个系统是C写的CGI程序每次请求fork进程查数据库QPS上到200就扛不住了。改成Servlet之后连接池复用数据库连接线程处理并发同样的硬件配置QPS直接翻了十倍不止。当然这个对比不完全公平因为连接池的贡献很大但进程模型跟线程模型的差距确实是数量级的。WSGIPython的方案Python世界有自己的规范叫WSGIWeb Server Gateway Interface。跟CGI的思想有点像都是定义一个接口标准但实现方式完全不同。WSGI不是基于进程fork的它是基于函数调用的。写一个Python函数接收两个参数environ字典和start_response回调返回可迭代对象这就是一个WSGI应用。Web服务器或者中间件直接在进程内调用这个函数没有进程创建的额外开销。defapplication(environ,start_response):status200 OKheaders[(Content-Type,text/html)]start_response(status,headers)return[bh1Hello from WSGI!/h1]你看这个environ字典跟CGI的环境变量是不是很像其实WSGI设计的时候就参考了CGI的规范environ里面那些REQUEST_METHOD、QUERY_STRING、CONTENT_TYPE的key跟CGI环境变量基本一一对应。可以说WSGI是CGI在Python世界里脱胎换骨的产物——保留了接口标准化的思想抛弃了fork进程的包袱。Gunicorn、uWSGI这些应用服务器都是基于WSGI规范实现的它们用多worker多进程或多线程的方式处理并发每个worker常驻跟FastCGI的思路类似。Gunicorn默认用的是pre-fork模型启动时先fork好worker进程请求来了分配给空闲worker跟FastCGI简直是一个模子刻出来的。Node.js更激进的方案Node.js又走了一条不同的路——单线程事件循环。不用多进程也不用多线程一个进程一个线程靠异步I/O和事件驱动来处理并发。这个模型跟CGI的差距就更大了。CGI是一个请求一个进程Node.js是所有请求一个线程。但本质上解决的问题是一样的怎么高效地处理大量并发请求。Node.js的方案在I/O密集型场景下表现特别好因为I/O操作是异步的不会阻塞事件循环。但CPU密集型任务就会卡住整个进程所以得靠cluster模块或者拆分成多个进程来兜底。CGI现在还在哪出现说了这么多替代方案CGI是不是彻底没用了也不完全是。在一些嵌入式设备、路由器的管理界面里CGI还在用。那些环境资源有限跑不动什么应用服务器一个轻量的CGI脚本反而最合适。我之前帮人调试过一个OpenWrt路由器它的Web管理界面就是用Lua写的CGI脚本跑在uhttpd上的简单高效占用的内存小到可以忽略不计。还有一些遗留系统九十年代和2000年代初用C或Perl写的CGI程序现在还在跑着。你没法轻易迁移因为业务逻辑都写死在那些脚本里了没人敢动。这种系统的运维就很头疼性能差但又不能停只能靠前面加缓存、加负载均衡来缓解。另外CGI的环境变量传递模型其实影响很深远。你去看Nginx的fastcgi_params文件里面定义的那些参数跟CGI标准里的环境变量几乎一模一样。FastCGI协议本质上就是CGI协议的网络版——把环境变量和stdin/stdout搬到了Socket上传输。所以理解了CGIFastCGI的配置参数你一看就懂不用死记硬背。从CGI到现代Web架构的演进脉络把这条线串起来看就很清晰了CGI每次请求fork进程 → 性能太差 → FastCGI用常驻进程池解决 → 但进程还是重 → Servlet用线程替代进程 → 更轻量 → WSGI用函数调用替代协议通信 → 更Pythonic → Node.js用单线程事件循环 → 另一种思路每一步演进都是在解决上一步的性能瓶颈或者易用性问题。理解了这个演进逻辑不管以后出现什么新技术你都能快速理解它为什么这么设计。CGI这个东西说它过时吧确实过时了说它没用吧还真不是。它是Web动态内容处理这条线上的起点从CGI到FastCGI到WSGI到Servlet一脉相承。把这些串起来理解不管后面技术怎么迭代底层逻辑你都能抓得住。觉得这篇有帮助的话转发给需要的朋友让更多人少走弯路。有问题欢迎留言讨论咱们下期见。