可能您听过nginx的tcp代理和负载均衡,那想了解它的来龙去脉,想知道如何使用,想知道它的实现原理吗?这正是本文的内容。

1、民间传闻

nginx因为在http服务的优异表现被大众认可,但是它不仅仅是一个http服务器,也是mail代理服务器。现在这个家庭加入了新的成员tcp。其实它在nginx官网的说辞里叫stream,之所以呈现在大家面前是tcp的原因,我猜测是这样的:

a:它解决了代理需求,而这需求就是tcp代理,但是它的源码里命名是stream,可以说看不到tcp。

b:确实有第三方模块(ngx_tcp_proxy_module)做了同样的事,这个优秀的模块早好些时间,有兴趣的可以去github上看看。

c:tcp比stream容易上口,这好像有点废话,反正就是这样了。

为什么强调stream呢,stream和http是同级的,http本身有proxy功能,现在stream的核心功能也是proxy。但预料以后stream的功能将更加丰富强大,所以重申nginx有个stream的东西,因为它才具备tcp代理功能。

2、stream是什么样子?

worker_processes auto; 
 
 error_log /var/log/nginx/error.log info; 
 
 stream { 
 
     upstream backend { 
 
         hash $remote_addr consistent; 
 
         server backend1.example.com:12345 weight=5; 
 
         server 127.0.0.1:12345            max_fails=3 fail_timeout=30s; 
 
         server unix:/tmp/backend3; 
 
     } 
 
     server { 
 
         listen 12345; 
 
         proxy_connect_timeout 1s; 
 
         proxy_timeout 3s; 
 
         proxy_pass backend; 
 
     } 
 
     server { 
 
         listen [::1]:12345; 
 
         proxy_pass unix:/tmp/stream.socket; 
 
     } 
 
 }

这是官网上的例子,我们发现stream和http极其相似。是的,这是我强调的nginx不仅仅是http服务器。我们知道http有三层,main, server, location。但是stream只有main和server。这是因为http的业务更加可变,而stream没必要这么复杂,所以作者只抽象到二层而已。但这不影响它的使用。注意stream也支持ssl。

3、stream的原理和实现

stream的源码可以说是http的简化版,细心的读者应该发现源码的作者是Roman Arutyunyan,这个也是rtmp模块的作者,他还有个模块叫mysql handlersocket。我将在模块系列里专门分析handlersocket模块,敬请关注。尤其是web(api)工程师会觉得这模块是很值得关注的。

首先main和server的构造跟http完全一样,这个不讨论。

在init里 ls->handler = ngx_stream_init_connection;

在处理请求里 

ngx_stream_init_connection(ngx_connection_t *c)
{ 
    ...
    ngx_stream_init_session(c);
}

static void
ngx_stream_init_session(ngx_connection_t *c)
{
    ngx_stream_core_srv_conf_t  *cscf;

    cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);

    cscf->handler(s); /* 请注意这里 */
}
ngx_stream_init_connection(ngx_connection_t *c)
{ 
    ...
    ngx_stream_init_session(c);
}

static void
ngx_stream_init_session(ngx_connection_t *c)
{
    ngx_stream_core_srv_conf_t  *cscf;

    cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);

    cscf->handler(s); /* 请注意这里 */
}

上面提过stream未来将更强大,因为cscf->handler这个钩子可以随意扩展。而现在注册的是ngx_stream_proxy_handler这个模块,所以大家清楚为什么nginx具备tcp代理功能了吧。正是这样的机制,分析stream基本就是分析ngx_stream_proxy_handler模块了。

static void
ngx_stream_proxy_handler(ngx_stream_session_t *s)
{
    u = ngx_pcalloc(c->pool, sizeof(ngx_stream_upstream_t));

    s->upstream = u;

    u->peer.log = c->log;

    uscf = pscf->upstream;

    uscf->peer.init(s, uscf);

    p = ngx_pnalloc(c->pool, pscf->downstream_buf_size);

    u->downstream_buf.start = p;
    u->downstream_buf.end = p + pscf->downstream_buf_size;
    u->downstream_buf.pos = p;
    u->downstream_buf.last = p;

    c->write->handler = ngx_stream_proxy_downstream_handler;
    c->read->handler = ngx_stream_proxy_downstream_handler;

    ngx_stream_proxy_process(s, 0, 0);

    ngx_stream_proxy_connect(s);
}
static void
ngx_stream_proxy_handler(ngx_stream_session_t *s)
{
    u = ngx_pcalloc(c->pool, sizeof(ngx_stream_upstream_t));

    s->upstream = u;

    u->peer.log = c->log;

    uscf = pscf->upstream;

    uscf->peer.init(s, uscf);

    p = ngx_pnalloc(c->pool, pscf->downstream_buf_size);

    u->downstream_buf.start = p;
    u->downstream_buf.end = p + pscf->downstream_buf_size;
    u->downstream_buf.pos = p;
    u->downstream_buf.last = p;

    c->write->handler = ngx_stream_proxy_downstream_handler;
    c->read->handler = ngx_stream_proxy_downstream_handler;

    ngx_stream_proxy_process(s, 0, 0);

    ngx_stream_proxy_connect(s);
}

这里做了两点:选择upstream和连接读写操作。不管是upstream还是读写,相对http可以说简化非常多,熟悉http部分的同学阅读起来非常简单。我写过一篇http upstream的来龙去脉,

点这里。这里有两个词downstream、upstream。相对nginx而言,客户端就是downstream,后端服务器就是upstream,nginx夹在中间。ngx_stream_proxy_connect 做的是负载均衡的事,上面的文章已经分析过了,在此略过。

接着看处理读写请求的代码:

static ngx_int_t
ngx_stream_proxy_process(ngx_stream_session_t *s, ngx_uint_t from_upstream, ngx_uint_t do_write)
{
    ...
    c = s->connection;
    pc = u->upstream_buf.start ? u->peer.connection : NULL;

    if (from_upstream) {
        src = pc;
        dst = c;
        b = &u->upstream_buf;

    } else {
        src = c;
        dst = pc;
        b = &u->downstream_buf;
    }

    for ( ;; ) {
        if (do_write) {
            size = b->last - b->pos;
            if (size && dst && dst->write->ready) {
                n = dst->send(dst, b->pos, size);

                if (n == NGX_ERROR) {
                    ngx_stream_proxy_finalize(s, NGX_DECLINED);
                    return NGX_ERROR;
                }

                if (n > 0) {
                    b->pos += n;

                    if (b->pos == b->last) {
                        b->pos = b->start;
                        b->last = b->start;
                    }
                }
            }
        }

        size = b->end - b->last;

        if (size && src->read->ready) {
            n = src->recv(src, b->last, size);
            if (n == NGX_AGAIN || n == 0) {
                break;
            }
            if (n > 0) {
                if (from_upstream) {
                    u->received += n;

                } else {
                    s->received += n;
                }
                do_write = 1;
                b->last += n;
                continue;
            }

            if (n == NGX_ERROR) {
                src->read->eof = 1;
            }
        }
        break;
    }
    ...

    return NGX_OK;
}
static ngx_int_t
ngx_stream_proxy_process(ngx_stream_session_t *s, ngx_uint_t from_upstream, ngx_uint_t do_write)
{
    ...
    c = s->connection;
    pc = u->upstream_buf.start ? u->peer.connection : NULL;

    if (from_upstream) {
        src = pc;
        dst = c;
        b = &u->upstream_buf;

    } else {
        src = c;
        dst = pc;
        b = &u->downstream_buf;
    }

    for ( ;; ) {
        if (do_write) {
            size = b->last - b->pos;
            if (size && dst && dst->write->ready) {
                n = dst->send(dst, b->pos, size);

                if (n == NGX_ERROR) {
                    ngx_stream_proxy_finalize(s, NGX_DECLINED);
                    return NGX_ERROR;
                }

                if (n > 0) {
                    b->pos += n;

                    if (b->pos == b->last) {
                        b->pos = b->start;
                        b->last = b->start;
                    }
                }
            }
        }

        size = b->end - b->last;

        if (size && src->read->ready) {
            n = src->recv(src, b->last, size);
            if (n == NGX_AGAIN || n == 0) {
                break;
            }
            if (n > 0) {
                if (from_upstream) {
                    u->received += n;

                } else {
                    s->received += n;
                }
                do_write = 1;
                b->last += n;
                continue;
            }

            if (n == NGX_ERROR) {
                src->read->eof = 1;
            }
        }
        break;
    }
    ...

    return NGX_OK;
}

它所做的就是读client,发送到后端,接收后端响应,响应给client,是不是很有意思。mail的代理也是这样的。它没有http的解析,chain, buf重用,没有event pipe。可以说阅读起来一点不费劲。

4、可能会踩到的小坑

代理做的事情很简单,把client发过来的数据一一转发给后端服务器。所以不要指望代理能帮你做任何数据上的处理,这点也适用于http代理。代理的配置里只关注到port:ip,看下面的例子。

location / { 
 
     proxy_pass   http://127.0.0.1:8082/xxx; #这样是不正确的,虽然可以运行 
 
 }

经常有人这样使用,包括我,后面的/xxx是没有任何效果的。

5、小结:

现在nginx 1.9是开发版,目前稳定版没有stream的功能,但在下个的稳定版发布时,这功能就会集成进来。因此推荐以后用http proxy的同学可以考虑换成tcp proxy,

如果只是做简单的代理而已,而且性能上会更优异。这也是支持开源的一种小小方式,不是吗 ^-^。

本文结束!