从LNMP到LORMP的高性能实践

By 周晶@新浪

LNMP简介

LNMP是目前互联网世界使用最广泛的技术架构之一,1998 年,当时 Michael Kunze 为德国计算机杂志 《c’t》 写作的一篇关于自由软件如何成为商业软件替代品的文章时,创造了LAMP这个名词,用来指代Linux操作系统、 Apache Web 服务器、MySQL数据库和PHP(Perl 或 Python) 脚本语言的组合(由 4 种技术的首字母组成)。得益于开源社区众多优秀工程师多年来的不懈努力,LAMP 这组开源技术发展的越发成熟,日积月累发展成了如今一个功能强大、使用广泛的 Web 应用程序平台,并持续对互联网世界产生深远影响;2004 年 Nginx 0.1.0 版本发布的带来了全新的服务器技术,Nginx 极高的稳定性、极低的资源消耗和高并发等能力使其迅速被大家所关注、认可。现今 Nginx 逐步替代了 Apache 霸主地位,并形成了如今更优的 LNMP 架构,且正逐步的取代 LAMP 架构。

Web服务器与后端动态服务的交互

LNMP 作为如今使用最广泛的 Web 应用服务架构,其中包含的优秀的开源技术每项都特别值得深入研究,高性能更是久谈不衰的主题,本文主要关注 Web 服务器(Nginx 及其其衍生版 OpenResty)与后端技术(主要以 PHP 为主进行论述)之间交互所涉及的性能问题。

Web 服务器与 PHP 的交互发展到今天大致经历了 CGI 、 FastCGI 、 PHP-CGI 、 PHP-FPM 四个阶段,其中前两阶段有大量 CGI 、 FastCGI 协议的实现,后两种 PHP-CGI 、 PHP-FPM 可看做对这些实现的升级优化,虽然目前可能还有一些老的系统并没有用到 PHP-FPM ,但是可预见 PHP-FPM 才是 PHP FastCGI 管理的未来。 那这些交互方式都有什么关系和特性呢?

互联网早期你可能看到过这样的 URL: http://example.com/cgi-bin/script.php,这就是 CGI 程序提供 Web 服务的一般 URL,CGI (Common Gateway Interface / 通用网关接口),CGI 作为一种语言无关的协议,它规定了 Web 服务器需要传递给后端哪些数据,以什么样的格式传递给后方处理,是后端语言与 Web 服务器信息交换的桥梁。CGI 可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。 Web 服务器收到用户请求,就会把请求提交给 CGI 程序(如 PHP-CGI),CGI 程序根据请求提交的参数作应处理(解析 PHP),然后输出标准的 HTML 语句,返回给 Web 服服务器,WEB服务器再返回给客户端,这就是普通 CGI 的工作原理。 CGI 的优点在于它可以完全独立于服务器,作为 Web 服务器和后端语言交互的独立通道。但是它致命的弱点在于每次请求处理它都需要重新启动,执行完了直接退出,这种 fork-and-execute 的模式成为它性能低下的硬伤,无法支撑高并发的请求。

FastCGI 的出现打破了 CGI fork-and-execute 的困局。FastCGI 是基于 CGI 协议的扩展,他的核心思想就是在 Web 服务器和具体 CGI 程序之间常驻(long-live)一个智能的可持续的中间层,统管 CGI 程序的运行,这样 Web服务器只需要将请求提交给这个层,这个层再派生出几个可复用的 CGI 程序实例,然后再把请求分发给这些实例,这些实例是可控的,可持续,可复用的, 因此一方面避免了进程反复 fork,另一方面又可以通过中间层的控制和探测机制来监视这些实例的运行情况,根据不同的状况 fork 或者回收实例,达到灵活 性和稳定性兼得的目的。Apache 的 mod_fastcgi 模块就是这种协议的一个代表性实践,这种技术允许把 Web 服务器和动态语言运行在不同的主机上,以大规模扩展和改进安全性而不损失生产效率。

FastCGI 请求处理流程简述如此:当客户端请求到达 Web 服务器时,FastCGI 进程管理器选择并连接到一个 CGI 解释器。Web 服务器将 CGI 环境变量和标准输入发送到 FastCGI 子进程 PHP-CGI;FastCGI 子进程完成处理后将标准输出和错误信息从同一连接返回 Web 服务器。当 FastCGI 子进程关闭连接时,请求便告处理完成。FastCGI 子进程接着等待并处理来自 FastCGI 进程管理器(运行在 Web 服务器中)的下一个连接。在 CGI 模式中,PHP-CGI 在此便退出了。

PHP-CGI 是 PHP 官方实现的 FastCGI 管理器。它致命的弱点是不支持平滑重启,而且杀死 PHP-CGI 子进程会导致 PHP 结束运行。这些缺点导致 PHP-CGI 并没有被大量使用,大家更多的还是使用 Apache 的 mod_php 或者 mod_fastcgi,PHP-CGI 虽然是官方出品,但还是被后来的 PHP-FPM 急速替代。

从 PHP 5.3.3 版本开始,PHP 官方集成了 PHP-FPM,而不再以第三方包发布。此举认可了 PHP-FPM 在 FastCGI 进程管理中的绝对地位。PHP-FPM 也确实解决了 PHP-CGI 的已有问题,支持平滑重启、有效的控制了内存和进程,优雅的动作和更高的性能促使 PHP-FPM 一路高歌猛进,迅速占据绝对位置。

到此我们可以明确目前 Web 服务器都是通过 FastCGI 的各种实现在与后端脚本交互,如Apache 通过 mod_fastcgi、 Nginx 通过 PHP-FPM 与 PHP 的交互。

LNMP Web 服务架构的发展

Nginx 作为现今性能最好的 Web 服务器,一切都得益于 Nginx 优雅的架构体系设计,分层、别类的模块化设计,核心、配置、事件、HTTP、Mail 五大核心模块相互有机结合提供丰富稳定的功能;基于事件驱动的异步请求处理,处理过程分阶段进行,所有操作由事件收集、分发器触发,单进程轻松处理成千上万并发请求;基于 Master、Worker 模式的进程管理模式,最大程度的发挥多核 CPU 处理的优势;由此众多特性带来的高并发、大吞吐特性将请求高效的转发给后端 FastCGI 处理,而 PHP-FPM 不论在资源控制、处理性能以及可扩展性方面都优于其他 FastCGI 的实现。Nginx 直接对接 PHP-FPM 这种最优的交互模式,LNMP 这种高性能的 Web 服务架构显得自然而水到渠成。

Nginx 与 PHP-FPM 如此轻量的对接,相比老牌的 Apache,同样可以与 FastCGI 进行对接,但 Nginx 往往能达到比 Apache 数倍的效能。关键在于 Nginx 和 Apache 二者的请求处理模型。Nginx 通过 一个 http 类型的模块 ngx_http_fastcgi_module 来处理与 PHP-FPM 的交互。它为不同的平台实现了各自最适合的事件处理模块,比如 ngx_select_module 、 ngx_epoll_module 等,并根据内核所支持的事件模块选用最适合最高效的事件处理方式,另外它还实现了定时器事件专门处理超时控制, 把各种操作都封装成一系列的事件,将这些事件加入事件队列,由事件处理模块来高效的处理各种事务,达到高并发的目的。比如 Nginx 会将监听连接的读事件设为 ngx_event_accept 方法,然后将监听连接的读事件添加到 ngx_epoll_module 事件驱动模块中,如此当有新连接事件出现,则会调用 ngx_event_accept 方法来建立连接。诸如此类,所有操作都是基于事件来异步处理的,所以一个进程可以同时非阻塞的处理很多并发的请求。

而 Apache请求的处理有 prefork 和 worker 两种工作模式,prefork 模式下,一个单独的控制进程(父进程)负责产生子进程,这些子进程用于监听请求并作出应答。Apache 总是试图保持一些备用的 (spare)或是空闲的子进程用于迎接即将到来的请求。这样客户端就无需在得到服务前等候子进程的产生,编译安装 Apache 时,如果不显示指定 “--with-mpm” 编译选项,prefork 就是 Unix 平台上默认的 MPM。 worker 模式下,每个进程能够拥有的线程数量是固定的。服务器会根据负载情况增加或减少进程数量。一个单独的控制进程(父进程)负责子进程的建立。每个子进程能够建立 ThreadsPerChild 数量的服务线程和一个监听线程,该监听线程监听接入请求并将其传递给服务线程处理和应答。Apache总是试图维持一个备用(spare)或是空闲的服务线程池。这样,客户端无须等待新线程或新进程的建立即可得到处理。相对于 prefork 模式,worker 模式是2.0 版中全新的支持多线程和多进程混合模型的 MPM。由于使用线程来处理,所以可以处理相对海量的请求,而系统资源的开销要小于基于进程的服务器。但是,worker 模式也使用了多进程,每个进程又生成多个线程,以获得基于进程服务器的稳定性。这种 MPM 的工作方式将是 Apache 2.0 的发展趋势。在请求处理阶段,Apache 将请求处理循环分为 Post-Read-Request、URI Translation、Header Parsing、Access Control、Authentication、Authorization、MIME Type Checking、FixUp、Response、Logging、CleanUp11个阶段,prefork 模式下每接到一个请求连接后,当前请求独占处理进程,直到请求处理完成,所以这种模式下不具备处理大并发请求的能力。而 worker 模式,虽然通过线程的方式想达到处理大并发请求的目的,但是整个请求模型还是线程独占,尽管使用了多进程来提升了稳定性,但是进程中线程池的维护等也需要消耗资源。不过我们应该以发展的眼光看问题 worker 模式任然是 Apache 的发展方向。

面对现实,Nginx 和 Apache,LNMP 和 LAMP 该如何抉择呢?综上所述,其实 Nginx 和 Apache 各有长处。不过在大多数场景下可能 Nginx 确实更胜一筹。怎么讲?Nginx 使用异步事件处理的方式来达到处理高并发的目的,这种方式最擅长做短小精干的请求,最害怕阻塞式请求。如果每次请求都是比较耗时的请求,则 Nginx 不再能保持其高并发特性。比如在 CPU 密集型计算的处理中。每个请求都会占用大量的 CPU 时间片,在计算量上可见并发就不太能上去这时候选用 Apache,让它每个请求踏踏实实的处理,稳定性更高。但是在 I/O 密集型计算中,比如经常有请求要去连接后端读取数据资源,这个时候就可以用 Nginx 的异步事件来处理,可能连接读取数据事件后,Nginx 不需要阻塞等待结果返回,Nginx 进程可以继续处理其他准备好的事件。这样能更好的利用好 CPU 、内存等系统资源,能最大限度榨取服务器性能。

随着分布式计算技术、缓存技术以及计算机硬件的发展,目前生产环境更多的将计算分布的更扁平,更多的内存缓存的使用,使各种阻塞的 CPU 密集型的计算得以很优雅的消化和封装,一个系统往往有多个子系统互相配合组合而成,由更多的 I/O 密集型的计算取代,这也就是为什么更多的 Web 服务架构在不断从 LAMP 迁移到 LNMP 的根本原因。Nginx 优秀的并发能力,消耗极少的资源就能带来极高的性能,促使这场行业升级在持续广泛的进行。

LNMP高性能Web服务

Nginx 可以通过简单的配置来提供非常高性能的 Web 服务,有丰富的模块体系,提供各种常见的功能,也提供了特别方便的模块开发方式,可以定制化的开发各种类型的功能模块,这里我们通过几个有代表性的模块来阐述几种常见的提高性能操作。

下面给出一个常见的配置(中间略去了部分更细节的配置,仅为表述执行流程):

server {

listen *:80;

server_name domain.com;

charset utf-8;

index index.html index.htm index.php;

root /var/vhosts/domain.com/public;

location / {

try_files $uri $uri/ index.php$is_args$args;

}

location ~ .php$ {

set $nocache "";

if ($http_cookie ~ (cookie_name)) {

set $nocache "Y";

}

set $cachekey $request_method$scheme://$host$request_uri;

fastcgi_cache_bypass $nocache;

fastcgi_cache dynamic;

fastcgi_cache_key $scheme$request_method$host$request_uri;

... ...

fastcgi_cache_use_stale error timeout invalid_header http_500;

fastcgi_catch_stderr "PHP Fatal error";

add_header X-Cache-Status $upstream_cache_status;

try_files $uri /index.php =404;

fastcgi_pass unix:/var/run/php5-fpm.sock;

fastcgi_index index.php

fastcgi_split_path_info ^(.+.php)(.*)$;

fastcgi_param PATH_INFO $fastcgi_path_info;

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_scriptName;

}

}

首行的 “server” 指令来自于 ngx_http_core_module 模块,请求处理阶段 Nginx 先根据请求头中的 Host 字段确定提供服务的 Server 段。

“location /” 作为保底请求处理段,当请求通过正则匹配(“location ~” 类型)、前缀匹配(“location /prefix 类型)或者完全匹配(“location = /favicon.ico” 类型)均无法确定时,会交由此段进行处理。

“try_files $uri $uri/ index.php$is_args$args;” 将请求转向 index.php location 段处理,此时必定会成功完全正则匹配 “location ~ .php$” 段,则请求交由此段处理。

被 “location ~ .php$” 正则表达式 Location 段匹配的请求,会先确定 “$nocache” 的值,据此来最终确定是否 fastcgicache_bypass(不走 FastCGI Cache),这里的 fastcgi_cache* 序列指令来自于 ngx_http_fastcgi_module 模块,这是 Nginx 同后端 CGI 通信的桥梁,这里的后端则是 PHP-FPM。如果需要走缓存,则根据 key 获取缓存数据,走相关的缓存逻辑,否则直接走到后面的 fastcgi_pass,将请求交由后端的 PHP-FPM 处理。fastcgi_split_path_info 指令用来配置 PATH_INFO 支持,而 fastcgi_param 指令则是 Nginx 通向 PHP-FPM 的窗口,在 Nginx 环境中获取的数据可以此方式传递给 PHP-FPM,比如在 PHP 中,我们将可以通过 $SERVER['PATH_INFO'] 获取到 Nginx 配置中 $fastcgi_path_info 的值。这种操作如果是加上上面的 set 指令(来自于 ngx_http_rewrite_module 模块)能给我们带来无尽的想象空间,尽管 set 指令只能在 server, location, if 这三个配置段使用。

上面仅仅3个 http 类型的模块,简单组合,即可完成各种复杂的 Web 服务,提高性能我们可以开启 fastcgi_cache(当然 Nginx 同时也是高性能反向代理服务器,ngx_http_proxy_module 也提供了相关的缓存技术,但是这并不在我们讨论之列,这里主要关注如何使 Nginx 与 PHP-FPM 结合的更优雅,发挥更大的效能),这样在缓存命中的情况下,能轻松处理几万甚至十万级别的请求,将动态服务静态化。通过一些判断,变量赋值(set 指令),我们可以将一些简单的操作放到 Nginx 本身来处理,省去与后端 PHP-FPM 的交互成本,或者将某些信息在 Nginx 环境获取后,将结果直接放入 PHP 的 $SERVER 变量中,或者是一些配置信息等等。这里有条原则,就是把能不放到后端处理的,尽量提前到 Nginx 环境中处理。提供更高性能的服务,有 Nginx 模块开发能力的公司这时一般会选择开发一些自定义的模块来满足自己的高性能需求。但是有没有更亲民的做法呢?

为什么是LORMP

LORMP 是使用近年来 Nginx 社区兴起的一个衍生版 OpenResty 代替 Nginx 得来的系统架构平台。如其官网的简介所述:OpenResty 是一个基于 Nginx 和 LuaJIT 的 Web 平台。OpenResty 只做了两件事,其一是通过 ngx_http_lua_module 将 LuaVM 嵌入 Nginx 使得可以在 Nginx 运行环境中直接执行 Lua 代码,这提供了一个全新的、更亲民的 Nginx 模块开发解决方案,使其能在 Nginx 运行上下文中使用 Lua 来处理各种请求,其二是由 OpenResty 社区强大活力支撑且日趋完整的 Resty 生态。Openrestry 对 HTTP 请求的进行分阶段的处理,这种执行阶段来自于 Nginx,Nginx 的 HTTP 核心模块依据常见的处理流程将处理阶段划分为11个阶段,其中每个处理阶段可以由任意多个 HTTP 模块流水式地处理请求,ngx_http_lua_module 属于一个 Nginx 的 HTTP 模块,它基于 Nginx 的11个阶段,封装了如下7个相应HTTP请求处理阶段:set_by_lua(block,file等,后面6个阶段也相同,故略去)、 content_by_lua、 rewrite_by_lua、 access_by_lua、 header_filter_by_lua、 body_filter_by_lua、 log_by_lua。并且该模块提供了丰富的 Lua API 能很便捷的同 Nginx 通信。比如可在 server、server if、location、location if 配置段使用的 set_by_lua 指令其实可以看做是 set 指令的 lua 实现,我们可以将一个 lua 代码块或者一个 lua 文件的运行返回结果 set 到 Nginx 配置变量中以供业务配置判断等使用,或者直接通过 fastcgi_param 传递到 PHP-FPM,供 PHP 处理请求使用。再如 access_by_lua 指令,我们可以在请求的 access 阶段通过 lua 代码来做访问鉴权,这样非法的请求根本就没机会走到 PHP-FPM,极大的减少了系统开销,提升了系统的吞吐能力。或者我们可以使用 content_by_lua 指令,作为请求的路由层,将请求处理都先经过 content_by_lua* 指定的一个 lua 逻辑中,在这里决定将请求分发给那些 PHP 脚本,甚至我们可以将 PHP 执行的结果缓存到 Nginx 的共享内存中(ngx_http_lua_module 模块提供了共享字典 API 可以轻松实现这样的功能),这种缓存的效率比 fastcgi_cache 类的文件缓存性能更高,可控性更强(fastcgi_cache 的缓存清理必须单独安装 purge 扩展)。这里给出一个大致的配置以及代码片段:

相关配置:

location / {

content_by_lua_file $document_root/pub/index.lua;

}

location ~ .php$ {

root $rs_root;

fastcgi_pass localhost:9000;

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

include fastcgi_params;

}

相关代码片段:

local function call_php(request)

local index = '/index.php'

local rs = ngx.location.capture(index,{args = ngx.var.args, always_forward_body = true})

if rs.status == 302 then

redirect(rs.header.Location)

end

return rs

end

配置将所有请求都指向 “$document_root/pub/index.lua”,在 lua 逻辑中确定何时调用 call_php 将请求转发给 “location ~ .php$”,交由后端 PHP-FPM 处理。

比起前面的 set 指令、 fastcgi_cache、 fastcgi_param 等来说,ngx_http_lua_module 带来了全所未有的想象空间。我们甚至可以直接在 Lua 中完成所有的业务逻辑,但是现实中可能由于历史遗留问题,我们还有很多系统运行在 LNMP 的架构体系下,所以使用 LORMP 给原有的 LNMP 体系加入 Lua 支持显得更简单直接且有必要。

LORMP未来展望

从 LNMP 到 LORMP 的高性能实践这条路足够的宽,因为这些技术结合到一起给我们的想象空间实在太大,我们有很多方向可以尝试,可以探索。开源社区源源的动力也为此提供无限支持,LORMP 目前处于一个高速爆发的阶段,OpenResty 的高效与简单吸引了大批优秀的工程师,我们有理由相信未来一片光明。本文所述高性能实践其意不止于实践,而在于一些开发过程中一些解决问题的思路,思路这种东西往往都不是唯一的,永远也找不到一条最好的思路,没有最好只有更适合,高性能实践是一个永恒的话题,本文论述的观点在于如何将各种技术更有机的结合,让它们产生效能多倍叠加的反应。请求不会拘泥于都交给 PHP 或者都用 Lua,我们应该在最合适的场景下使用最适合的技术。

参考资料:

OpenResty 文档 [https://github.com/openresty/lua-nginx-module]

陶辉.《深入理解 Nginx:模块开发与架构解析》[J].北京:机械工业出版社,2013.3:256-347

results matching ""

    No results matching ""