可能您听过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,
如果只是做简单的代理而已,而且性能上会更优异。这也是支持开源的一种小小方式,不是吗 ^-^。
本文结束!