Skip to content
aimingoo edited this page Feb 24, 2015 · 2 revisions

ngx_cc是一个实现“nginx可通信集群”的框架。它基于lua,因此它需要在编译nginx时加入HttpLuaModule模块,它使用ngx.location.capture*来实现通信,并不依赖(通常的)cosocket库。

我会在后面进一步解释选择capture*而不是cosocket的原因。

ngx_cc构建自我在豌豆荚(Wandoulabs)负责的一个称为kwaf的风控产品,是kwaf的一个基础服务。

一点背景

我曾经在一个小圈子的聚会上被问到一个问题:nginx + lua在实现企业级的应用服务器(business application server,类似EJB或其具体实现JBOSS、Jetty等)上有什么限制呢?我想了想,觉得是“nginx不提供集群间的通信能力”。

换言之,用nginx搭建起来的集群,都是“不可通信的集群”。这是nginx原本的设计所决定的,它在网络架构中主要承载两种角色,即负载均衡(Load Balance)和Web服务器(HTTP Server)。加入了Lua之后,它具备了Web应用服务(Application Service)的基本能力,但也仅是解决了JSP这个层面上的问题。这个方向上比较成熟的项目很多,例如hveem, lapis, remy, ophal, bamboo,还有国内的alilua、moochine也是相当成熟的作品。一些较早或较旧的推荐可以参考这里,或这里。个人非常推荐alilua,走轻快路线的可选moochine

但是再往企业级应用集群方向上走一走,你就会发现集群通信是个致命问题。因为这与nginx的最初设计完全是背道而驰的:nginx作为Load Balance时,显然是希望各个服务器之间是没有通讯、互不依赖的,这样上游才能简单地通过活跃探查来决定如何均衡。而且nginx是使用multi worker processes and connections的方式来实现单机上的超大连接数+多核支持的,这也意味着即使在同一台nginx服务器内部,多个http request/session之间,也是无法(也不应当)通信的。

于是,在通常的nginx+lua方案中,如果要实现集群通信或者进一步地向企业级应用服务集群上探索,解决问题的方法就是cosocket,或者依赖后端通信(例如基于mysql集群或memcache集群来建立通信/数据交换机制)。

采用cosocket的问题在于,它相当于在nginx上面外挂了一层通信。这层通信的有效性需要自己来确保(例如心跳,又例如连接的复用等)。而且,它事实上跟具体的nginx http request上下文之间没有关系,所以还需要特殊的机制来构建多个请求之间的会话(session)或保持状态(status)。至于采用后端通信的问题,架构上就更加复杂了,并且效率上也大打折扣。

ngx_cc选择了另外的一条道路:直接使用ngx.location.capture*来实现。这样,在单个worker process内部,它相当于C语言级别上的内部调用;在worker processes之间,它相当于跨进程的、端口间的WebServices调用;在集群之间,它相当于在服务/服务器之间的Remote WebService API访问。尤其重要的是,ngx_cc在这三者之间提供完全一致的接口与平滑迁移的框架。

最后,我确信一点:让nginx干它自己该干的事——响应HTTP请求而不是实现基于socket的全新服务,一定是更好的选择。

ngx_cc所需的运行环境

首先,你需要部署支持Lua的nginx服务器。在标准的nginx上去集成HttpLuaModule是可行的,请参考官方文档:http://wiki.nginx.org/HttpLuaModule#Installation。我个人的建议是选择OpenResty(在这里,或这里),或者Tengine(在这里),这两个都出自阿里系,前者是agentzh(章亦春)大神的作品,后者目前仍然主要由淘宝的团队在维护着。

不过在编译、安装之前,暂请先打住,我们还需要一点Patch。这是一个本框架中关键的Patch,它来自于一个很“另类的”项目(Streaming with nginx-rtmp-module),即“基于nginx提供rtmp媒体流数据服务”。这个服务需要解决一个问题:在一个work process上稳定、持续的端口连接。因此出现了这样的一个名为 “per-worker-listener”的patch。这个patch可以在这里下载: https://github.com/arut/nginx-patches。相关的介绍在这里:http://nginx-rtmp.blogspot.jp/2013/06/multi-worker-statistics-and-control.html

请在编译nginx之前打入这个patch。以 http://wiki.nginx.org/HttpLuaModule#Installation介绍的过程:

    wget 'http://nginx.org/download/nginx-1.7.7.tar.gz'
    tar -xzvf nginx-1.7.7.tar.gz
    cd nginx-1.7.7/
    ... 

    # Here we assume Nginx is to be installed under /opt/nginx/.
    ./configure --prefix=/opt/nginx \
            --with-ld-opt='-Wl,-rpath,/path/to/luajit-or-lua/lib" \
            --add-module=/path/to/ngx_devel_kit \
            --add-module=/path/to/lua-nginx-module
    ...

为例,请在./configure执行前,进入nginx目录做如下操作:

    cd nginx-1.7.7/
    wget https://github.com/arut/nginx-patches/raw/master/per-worker-listener -O per-worker-listener.patch
    patch -p1 < per-worker-listener.patch

对于OpenResty(或其它的nginx或发布版)来说,上述步骤是一样的,只是在具体的操作目录上不同,例如ngx_openresty-1.7.2.1是在:

    cd ngx_openresty-1.7.2.1/bundle/nginx-1.7.2
    ...

然后再使用./configure来配置你需要加载的模块,然后编译、安装即可。

基本配置与测试

接下来测试一下nginx是否成功打入上面的patch。你需要如下的一个简单配置:

## your_demo_directory/nginx.conf
user  nobody;
worker_processes  4;

events {
    worker_connections  10240;
    accept_mutex off; ## 关键配置1
}

http {
    server {
        listen     80;
        listen     8010 per_worker; ## 关键配置2
        server_name  localhost;

        location /test {
            content_by_lua '
                ngx.say("pid: " .. ngx.var.pid .. ", port: " .. ngx.var.server_port .. "\tOkay.")';
        }
    }
}

两处关键配置中,per_worker是说明当前的nginx除了建立在80号的端口上的侦听之外,还需要建立8010开始的4个(由worker_processess配置决定的)端口。

然后,启动nginx,再在命令行上测试(假设当前在nginx的安装目录下,另外由于需要80号端口,所以要使用sudo):

    ## sudo sbin/nginx -c your_demo_directory/nginx.conf
    for port in 80 {8010..8013}; do curl "http://127.0.0.1:$port/test"; done

显示类似如下,即检测成功:

pid: 24017, port: 80    Okay.
pid: 24016, port: 8010  Okay.
pid: 24017, port: 8011  Okay.
pid: 24018, port: 8012  Okay.
pid: 24019, port: 8013  Okay.

其中,port:80与port:8011显示相同的pid(24017),表明在该次访问80端口时,master proess动态地将请求转到了worker process pid:24017处理。

ngx_cc: 安装

首先下载ngx_cc:

> cd ~
> git clone https://github.com/aimingoo/ngx_cc

或:

> wget https://github.com/aimingoo/ngx_cc/archive/master.zip -O ngx_cc.zip
> unzip ngx_cc.zip -d ngx_cc/
> mv ngx_cc/ngx_cc-master/* ngx_cc/
> rm -rf ngx_cc/ngx_cc-master

然后查看ngx_cc目录位置,并修改演示用的nginx.conf配置文件:

> cd ~/ngx_cc/
> pwd
/Users/aimingoo/work/ngx_cc

用上面显示的目录位置替换nginx.conf中如下两行中的路径即可:

> grep -n 'aimingoo' nginx.conf # 修改如下配置到你的ngx_cc下载目录的完整路径
26:     lua_package_path '/Users/aimingoo/work/ngx_cc/?.lua;;';
27:     init_worker_by_lua_file '/Users/aimingoo/work/ngx_cc/init_worker.lua';

一般来说,到这里就OK了。不过如果你用的是macos,那么你需要再多做一点操作(因为macos不支持procfs所以在DEMO中需要另外的方法来获得parent process id)。这种情况下,请参考这里(https://github.com/mah0x211/lua-process)所示的方法安装lua-process,因为luarocks会将二进制包安装到lua的默认目录中,所以无需另行配置Lua的搜索路径。

最后,我们启动一个nginx来做测试(如果你还有nginx实例没有停掉,请先shutdown之)。

> # 设当前目录位于nginx安装目录下, ngx_cc安装于当前用户的Home目录下
> sudo ./sbin/nginx -c ~/ngx_cc/nginx.conf
> curl http://127.0.0.1/test/hub?getServiceStat
{"clients":[],"ports":"8012,8011,8010,8013","routePort":"8011","service":"127.0.0.1:80"}
> curl http://127.0.0.1/kada/hub?showMe
Hi, Welcome to the ngx_cc cluster!

ngx_cc: 一般说明

上面的测试中,getServiceStat是ngx_cc内部实现的提供的一个简单请求处理/响应(invokes,你可以理解为请求处理,或响应逻辑),它显示了服务集群的组网信息(这里是单台服务器),它的返回是一个标准的JSON。其中ports显示per-worker-listener所开启的端口,而routePort指明在这组端口中用来做master/route的端口。

注意上面的测试使用了两个通道(channels),一个是test,另一个是kada。在不同的通道上能发生的通信是不一样的,这取决于该通道能响应哪些请求(invoke)。例如,kada通道能响应showMe,而test就不能。反之,事实上kada/test都能响应getServiceStat。

ngx_cc并没有约定“所有通道都需要响应getServiceStat”,如何实现invokes是由具体的用户代码来决定的。

每个通道内置的响应有3个,声明在module/ngx_cc_core.lua中:

    kernal_actions = {
        reportClient = reportClient,
        reportHubPort = reportHubPort,
        registerWorker = registerWorker
    },

reportHubPort是一个关键的invoke,它用于client向super汇报自己的HubPort,这个决定了super下发消息时如何调用'/channel_name/hub'这个接口(interfaces)。

ngx_cc在nginx.conf中声明了三个接口,其中两个外部接口,一个内部接口:

# caster, internal only
location ~ ^/([^/]+)/cast { ...

# invoke at worker listen port
location ~ ^/([^/]+)/invoke { ...

# hub at main listen port(80), will redirect to route listen port
location ~ ^/([^/]+)/hub { ...

在本说明文档中,上面的正则表达式

^/([^/]+)

我们将换作‘/channel_name/’来替代。亦即是:

/channel_name/cast  ## 内部接口(下面暂先不讨论)
/channel_name/invoke
/channel_name/hub

对外,invoke提供直接访问端口的响应能力,通常它应该以

http://127.0.0.1:8010/channel_name/invoke?...

这样的形式调用。尽管ngx_cc不做强制约定,但它事实上是由ngx_cc在集群间做内部通信时使用的,建议用户代码不要直接使用之。

而hub提供类似中继的作用,它应该以

http://127.0.0.1:80/channel_name/hub?...

这样的形式调用(注意如果是80,则也可以省略不写)。发送到hub的请求会被再次转发到ngx_cc内部决定的某个路由端口(routePort),并由具体的invoke代码来决定如何转发/处理这次通信。

ngx_cc提供整个集群间、全方向的通信转发能力,而用户代码通过invoke来决定如何具体处理通信的内容。这也是ngx_cc这个项目名称的由来,'cc'取自邮件中的抄送。当然,正式文档中它总是解释为这样:

a framework of Nginx Communication Cluster.