Tuesday, March 8, 2011

ngx_openresty系列之 ngx_lua vs. node.js

ngx_lua 是由 chaoslawful 和 agentzh 开发的用于 web 开发的 nginx 扩展,
其主要特点在于利用了 nginx 的非阻塞 IO 模型以及 lua VM 的灵活性。

node.js 是 ry 主导开发的基于强大高效的 v8 引擎,
提供了事件模型和各种基础设施的 web 开发平台,
其主要特点是内部各种 IO 操作是非阻塞的,为高并发提供了很好的基础。
同时,因为基于 js 语法,和 web 前端融合较紧密,
可以提供前后一致的编程体验(当然,客户端还比较难达到服务器端的爽)。

在传统的 php 编程中,当需要查询数据库时,当前的
apache 进程(或线程)(或 php-fpm 之类)在数据结果返回前一直处于等待状态,
如果大量的请求都在做数据库查询操作,那么服务器就没法处理更多请求。
因为 apache 能开启的进程(或线程)数是有限的(受内存限制)。

<code>
<?php
// 连接数据库时,进程是在等待的
$c = mysql_connect('mysql_host', 'user', '****');
mysql_select_db('my_database');

$query = 'SELECT col1 FROM my_table LIMIT 10';
// 查询时,进程是在等待的
$result = mysql_query($query);

while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
    echo " * " . $line[0] . "\n";
}

mysql_free_result($result);
mysql_close($c);
?>
</code>

如上面的代码所示,在跟外部系统交互的时候,当前进程啥都没干,净等着了,
你还不能抱怨人家不干活,因为理由很充分:数据库忙着呢,我得等!
真的这样么?
我们看看 node.js 怎么做的。

<code>
// 仅仅给出部分代码,不能直接运行
// resp 是响应体对象

var c = new require('mysql').Client();
c.user = 'user';
c.password = '****';

// 开始连接数据库,同时注册了一个在连接成功后要执行的函数
// 注意,这时候 node.js 做了这件事情以后代码还在继续往下执行
c.connect(function (err, results) {
    if (err) {
        resp.end("ERROR: " + err.message);
        return;
    }

    // 这里发了一个数据库请求,并且注册了一个在请求完成后要
    // 执行的函数,注册完毕后就跟刚才一样干其他事情去了
    c.query(
        'SELECT col1 FROM my_table LIMIT 10',
        function (err, results, fields) {
            if (err) {
                resp.end("ERROR: " + err.message);
                return;
            }

            for (var i = 0, iM = results.length; i < iM; ++i) {
                resp.write(' * ' + results[i][0] + '\n');
            }

            resp.end();
            c.end();
        });
});
</code>

我们可以看到,node.js 对 IO 的做法是当要发生可能等待的事情时,
注册个函数在那里,然后继续做其他事情,当实际的数据到达或者事件完成时,
再调用之前注册的函数来处理。
整个 node.js 环境内漂浮着许多事件和函数,
在底层,有一些机制来保证这些事件的正确、准确触发。

在这种思路下,业务处理代码被切片,然后被注册到各种事件上面去,
这些由 node.js 统一管理,因此实现了非阻塞的处理。

对于 php 来说,一旦 apache 把控制权转交给 php 以后,
他们之间就很难转让控制权了,我们不可能把 php 代码切分到这样细致的地步,
因为即使切分了也没法注册给 apache ,而在 php 内部注册也是没有意义的:
一个 php 进程一般只处理一个客户端请求而已,在一个请求内添加事件的概念,
没法提升系统整体的并发能力。

node.js 还有一个很大的优势是 连接池。
在 php 里,连接可能仅仅是复用而已,连接池的意义应该不大。
而在 node.js 里,连接是可以在多个请求之间共享的,
只要 node.js 服务器不重启或关闭,那么这些连接便可以一直复用。

看到这里,你有没有激动或者兴奋呢?
是否要抛弃执着等待 IO 的 apache + php 这一对黄金搭档呢?
不过,我们稍等一下,如果为了非阻塞的特性,而要我人肉的拆分我的代码
成为很多函数(代码片段)然后注册到各种事件上去,会不会写起来很恶心?
或者,有没有从天上掉下来的什么东西,能够让我写像 php 那样从上到下的代码,
而又能够像 node.js 那样非阻塞呢?

ngx_lua 就是这样的东西。
那,ngx_lua 到底是神马东西呢?我们先看段代码。

<code>
upstream db {
    drizzle_server mysql_host:3306 protocol=mysql
                   dbname=my_database user=user password=****;
}

http {
    server {
        location = /i-mysql {
            internal;

            drizzle_query $echo_request_body;
            drizzle_pass db;

            rds_json on;
        }

        location /test {
            content_by_lua '
                local yajl = require("yajl")

                local sql = "SELECT col1 FROM my_table LIMIT 10"
                local res = ngx.location.capture("/i-mysql",
                    { method = ngx.HTTP_POST, body = sql })

                if res.status ~= 200 then
                    ngx.say("error")
                    ngx.exit()
                end

                local result = yajl.to_value(res.body)

                for i, v in ipairs(result) do
                    ngx.say(" * " .. v)
                end
            ';
        }
    }
}
</code>

这是神马东西!!!!!!
好吧,其实这是一段 nginx 配置,
同时也是一段业务逻辑处理代码。

这里的写法跟 php 是类似的,从上到下,
但是呢,它对待 IO 的态度跟 node.js 是一样的,
当开始 IO 操作的时候,它会暂停当前代码的执行并发起数据请求,
当请求完成后,恢复之前暂停的代码,并把结果返回。

ngx_lua 的模型是一个 nginx 进程内可以同时处理不限数量(几乎)的请求,
他们按照上面描述的逻辑执行。

你可能会说,
那为什么要用 lua 这个没听说过的东西来搞,
而不用 php 或者 js 搞呢?

刚才说了,当发起请求的时候,需要暂停代码的执行,
暂时只有 lua 运行时支持这个特性(或者有其他什么主流语言支持,赶快说来听听)。
并且 lua 里面创建的进程(其实是协程)非常轻量,占用内存非常少,可以让服务器同时处理更多请求。

这篇文章是个开头,
后续会更多的介绍 ngx_lua 的周边和现状,
以及更多的使用示例。