http://nginx.org/cn/docs/http/ngx_http_proxy_module.html
http://bbs.linuxtone.org/thread-19302-1-1.html
http://blog.csdn.net/zl8762385/article/details/5946325
proxy也是nginx最有名的功能之一了,当然要了解一下这强大的功能了
listen 8080;
#location / { //例子1
# proxy_pass http://www.nginxcn.com/;
#}
location /home/ { //例子2
proxy_pass http://www.nginxcn.com/;
}
server_name proxy;
}
例子1就是表示监听8080端口,只要所有访问这个端口的就代理过去
例子2就是表示8080端口下的home目录才代理过去
############################################################
http://tengine.taobao.org/book/chapter_05.html
upstream模块 (100%)
nginx模块一般被分成三大类:handler、filter和upstream。前面的章节中,读者已经了解了handler、filter。利用这两类模块,可以使nginx轻松完成任何单机工作。而本章介绍的upstream模块,将使nginx跨越单机的限制,完成网络数据的接收、处理和转发。
数据转发功能,为nginx提供了跨越单机的横向处理能力,使nginx摆脱只能为终端节点提供单一功能的限制,而使它具备了网路应用级别的拆分、封装和整合的战略功能。在云模型大行其道的今天,数据转发是nginx有能力构建一个网络应用的关键组件。当然,鉴于开发成本的问题,一个网络应用的关键组件一开始往往会采用高级编程语言开发。但是当系统到达一定规模,并且需要更重视性能的时候,为了达到所要求的性能目标,高级语言开发出的组件必须进行结构化修改。此时,对于修改代价而言,nginx的upstream模块呈现出极大的吸引力,因为它天生就快。作为附带,nginx的配置系统提供的层次化和松耦合使得系统的扩展性也达到比较高的程度。
言归正传,下面介绍upstream的写法。
upstream模块接口
从本质上说,upstream属于handler,只是他不产生自己的内容,而是通过请求后端服务器得到内容,所以才称为upstream(上游)。请求并取得响应内容的整个过程已经被封装到nginx内部,所以upstream模块只需要开发若干回调函数,完成构造请求和解析响应等具体的工作。
这些回调函数如下表所示:
create_request | 生成发送到后端服务器的请求缓冲(缓冲链),在初始化upstream 时使用。 |
reinit_request | 在某台后端服务器出错的情况,nginx会尝试另一台后端服务器。 nginx选定新的服务器以后,会先调用此函数,以重新初始化 upstream模块的工作状态,然后再次进行upstream连接。 |
process_header | 处理后端服务器返回的信息头部。所谓头部是与upstream server 通信的协议规定的,比如HTTP协议的header部分,或者memcached 协议的响应状态部分。 |
abort_request | 在客户端放弃请求时被调用。不需要在函数中实现关闭后端服务 器连接的功能,系统会自动完成关闭连接的步骤,所以一般此函 数不会进行任何具体工作。 |
finalize_request | 正常完成与后端服务器的请求后调用该函数,与abort_request 相同,一般也不会进行任何具体工作。 |
input_filter | 处理后端服务器返回的响应正文。nginx默认的input_filter会 将收到的内容封装成为缓冲区链ngx_chain。该链由upstream的 out_bufs指针域定位,所以开发人员可以在模块以外通过该指针 得到后端服务器返回的正文数据。memcached模块实现了自己的 input_filter,在后面会具体分析这个模块。 |
input_filter_init | 初始化input filter的上下文。nginx默认的input_filter_init 直接返回。 |
memcached模块分析
memcache是一款高性能的分布式cache系统,得到了非常广泛的应用。memcache定义了一套私有通信协议,使得不能通过HTTP请求来访问memcache。但协议本身简单高效,而且memcache使用广泛,所以大部分现代开发语言和平台都提供了memcache支持,方便开发者使用memcache。
nginx提供了ngx_http_memcached模块,提供从memcache读取数据的功能,而不提供向memcache写数据的功能。作为web服务器,这种设计是可以接受的。
下面,我们开始分析ngx_http_memcached模块,一窥upstream的奥秘。
Handler模块?
初看memcached模块,大家可能觉得并无特别之处。如果稍微细看,甚至觉得有点像handler模块,当大家看到这段代码以后,必定疑惑为什么会跟handler模块一模一样。
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
clcf->handler = ngx_http_memcached_handler;
因为upstream模块使用的就是handler模块的接入方式。同时,upstream模块的指令系统的设计也是遵循handler模块的基本规则:配置该模块才会执行该模块。
{ ngx_string("memcached_pass"),
NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,
ngx_http_memcached_pass,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL }
所以大家觉得眼熟是好事,说明大家对Handler的写法已经很熟悉了。
Upstream模块!
那么,upstream模块的特别之处究竟在哪里呢?答案是就在模块处理函数的实现中。upstream模块的处理函数进行的操作都包含一个固定的流程。在memcached的例子中,可以观察ngx_http_memcached_handler的代码,可以发现,这个固定的操作流程是:
1. 创建upstream数据结构。
if (ngx_http_upstream_create(r) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
2. 设置模块的tag和schema。schema现在只会用于日志,tag会用于buf_chain管理。
u = r->upstream;
ngx_str_set(&u->schema, "memcached://");
u->output.tag = (ngx_buf_tag_t) &ngx_http_memcached_module;
3. 设置upstream的后端服务器列表数据结构。
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_memcached_module);
u->conf = &mlcf->upstream;
4. 设置upstream回调函数。在这里列出的代码稍稍调整了代码顺序。
u->create_request = ngx_http_memcached_create_request;
u->reinit_request = ngx_http_memcached_reinit_request;
u->process_header = ngx_http_memcached_process_header;
u->abort_request = ngx_http_memcached_abort_request;
u->finalize_request = ngx_http_memcached_finalize_request;
u->input_filter_init = ngx_http_memcached_filter_init;
u->input_filter = ngx_http_memcached_filter;
5. 创建并设置upstream环境数据结构。
ctx = ngx_palloc(r->pool, sizeof(ngx_http_memcached_ctx_t));
if (ctx == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
ctx->rest = NGX_HTTP_MEMCACHED_END;
ctx->request = r;
ngx_http_set_ctx(r, ctx, ngx_http_memcached_module);
u->input_filter_ctx = ctx;
6. 完成upstream初始化并进行收尾工作。
r->main->count++;
ngx_http_upstream_init(r);
return NGX_DONE;
任何upstream模块,简单如memcached,复杂如proxy、fastcgi都是如此。不同的upstream模块在这6步中的最大差别会出现在第2、3、4、5上。其中第2、4两步很容易理解,不同的模块设置的标志和使用的回调函数肯定不同。第5步也不难理解,只有第3步是最为晦涩的,不同的模块在取得后端服务器列表时,策略的差异非常大,有如memcached这样简单明了的,也有如proxy那样逻辑复杂的。这个问题先记下来,等把memcached剖析清楚了,再单独讨论。
第6步是一个常态。将count加1,然后返回NGX_DONE。nginx遇到这种情况,虽然会认为当前请求的处理已经结束,但是不会释放请求使用的内存资源,也不会关闭与客户端的连接。之所以需要这样,是因为nginx建立了upstream请求和客户端请求之间一对一的关系,在后续使用ngx_event_pipe将upstream响应发送回客户端时,还要使用到这些保存着客户端信息的数据结构。这部分会在后面的原理篇做具体介绍,这里不再展开。
将upstream请求和客户端请求进行一对一绑定,这个设计有优势也有缺陷。优势就是简化模块开发,可以将精力集中在模块逻辑上,而缺陷同样明显,一对一的设计很多时候都不能满足复杂逻辑的需要。对于这一点,将会在后面的原理篇来阐述。
回调函数
前面剖析了memcached模块的骨架,现在开始逐个解决每个回调函数。
1. ngx_http_memcached_create_request:很简单的按照设置的内容生成一个key,接着生成一个“get $key”的请求,放在r->upstream->request_bufs里面。
2. ngx_http_memcached_reinit_request:无需初始化。
3. ngx_http_memcached_abort_request:无需额外操作。
4. ngx_http_memcached_finalize_request:无需额外操作。
5. ngx_http_memcached_process_header:模块的业务重点函数。memcache协议将头部信息被定义为第一行文本,可以找到这段代码证明:
for (p = u->buffer.pos; p < u->buffer.last; p++) {
if ( * p == LF) {
goto found;
}
如果在已读入缓冲的数据中没有发现LF(‘n’)字符,函数返回NGX_AGAIN,表示头部未完全读入,需要继续读取数据。nginx在收到新的数据以后会再次调用该函数。
nginx处理后端服务器的响应头时只会使用一块缓存,所有数据都在这块缓存中,所以解析头部信息时不需要考虑头部信息跨越多块缓存的情况。而如果头部过大,不能保存在这块缓存中,nginx会返回错误信息给客户端,并记录error log,提示缓存不够大。
process_header的重要职责是将后端服务器返回的状态翻译成返回给客户端的状态。例如,在ngx_http_memcached_process_header中,有这样几段代码:
r->headers_out.content_length_n = ngx_atoof(len, p - len - 1);
u->headers_in.status_n = 200;
u->state->status = 200;
u->headers_in.status_n = 404;
u->state->status = 404;
u->state用于计算upstream相关的变量。比如u->status->status将被用于计算变量“upstream_status”的值。u->headers_in将被作为返回给客户端的响应返回状态码。而第一行则是设置返回给客户端的响应的长度。
在这个函数中不能忘记的一件事情是处理完头部信息以后需要将读指针pos后移,否则这段数据也将被复制到返回给客户端的响应的正文中,进而导致正文内容不正确。
u->buffer.pos = p + 1;
process_header函数完成响应头的正确处理,应该返回NGX_OK。如果返回NGX_AGAIN,表示未读取完整数据,需要从后端服务器继续读取数据。返回NGX_DECLINED无意义,其他任何返回值都被认为是出错状态,nginx将结束upstream请求并返回错误信息。
6. ngx_http_memcached_filter_init:修正从后端服务器收到的内容长度。因为在处理header时没有加上这部分长度。
7. ngx_http_memcached_filter:memcached模块是少有的带有处理正文的回调函数的模块。因为memcached模块需要过滤正文末尾CRLF “END” CRLF,所以实现了自己的filter回调函数。处理正文的实际意义是将从后端服务器收到的正文有效内容封装成ngx_chain_t,并加在u->out_bufs末尾。nginx并不进行数据拷贝,而是建立ngx_buf_t数据结构指向这些数据内存区,然后由ngx_chain_t组织这些buf。这种实现避免了内存大量搬迁,也是nginx高效的奥秘之一。
本节回顾
这一节介绍了upstream模块的基本组成。upstream模块是从handler模块发展而来,指令系统和模块生效方式与handler模块无异。不同之处在于,upstream模块在handler函数中设置众多回调函数。实际工作都是由这些回调函数完成的。每个回调函数都是在upstream的某个固定阶段执行,各司其职,大部分回调函数一般不会真正用到。upstream最重要的回调函数是create_request、process_header和input_filter,他们共同实现了与后端服务器的协议的解析部分。
负载均衡模块 (100%)
负载均衡模块用于从”upstream”指令定义的后端主机列表中选取一台主机。nginx先使用负载均衡模块找到一台主机,再使用upstream模块实现与这台主机的交互。为了方便介绍负载均衡模块,做到言之有物,以下选取nginx内置的ip hash模块作为实际例子进行分析。
配置
要了解负载均衡模块的开发方法,首先需要了解负载均衡模块的使用方法。因为负载均衡模块与之前书中提到的模块差别比较大,所以我们从配置入手比较容易理解。
在配置文件中,我们如果需要使用ip hash的负载均衡算法。我们需要写一个类似下面的配置:
upstream test {
ip_hash;
server 192.168.0.1;
server 192.168.0.2;
}
从配置我们可以看出负载均衡模块的使用场景: 1. 核心指令”ip_hash”只能在upstream {}中使用。这条指令用于通知nginx使用ip hash负载均衡算法。如果没加这条指令,nginx会使用默认的round robin负载均衡模块。请各位读者对比handler模块的配置,是不是有共同点? 2. upstream {}中的指令可能出现在”server”指令前,可能出现在”server”指令后,也可能出现在两条”server”指令之间。各位读者可能会有疑问,有什么差别么?那么请各位读者尝试下面这个配置:
upstream test {
server 192.168.0.1 weight=5;
ip_hash;
server 192.168.0.2 weight=7;
}
神奇的事情出现了:
nginx: [emerg] invalid parameter "weight=7" in nginx.conf:103
configuration file nginx.conf test failed
可见ip_hash指令的确能影响到配置的解析。
指令
配置决定指令系统,现在就来看ip_hash的指令定义:
static ngx_command_t ngx_http_upstream_ip_hash_commands[] = {
{ ngx_string("ip_hash"),
NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,
ngx_http_upstream_ip_hash,
0,
0,
NULL },
ngx_null_command
};
没有特别的东西,除了指令属性是NGX_HTTP_UPS_CONF。这个属性表示该指令的适用范围是upstream{}。
钩子
以从前面的章节得到的经验,大家应该知道这里就是模块的切入点了。负载均衡模块的钩子代码都是有规律的,这里通过ip_hash模块来分析这个规律。
static char *
ngx_http_upstream_ip_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_upstream_srv_conf_t *uscf;
uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;
uscf->flags = NGX_HTTP_UPSTREAM_CREATE
|NGX_HTTP_UPSTREAM_MAX_FAILS
|NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
|NGX_HTTP_UPSTREAM_DOWN;
return NGX_CONF_OK;
}
这段代码中有两点值得我们注意。一个是uscf->flags的设置,另一个是设置init_upstream回调。
设置uscf->flags
- NGX_HTTP_UPSTREAM_CREATE:创建标志,如果含有创建标志的话,nginx会检查重复创建,以及必要参数是否填写;
- NGX_HTTP_UPSTREAM_MAX_FAILS:可以在server中使用max_fails属性;
- NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:可以在server中使用fail_timeout属性;
- NGX_HTTP_UPSTREAM_DOWN:可以在server中使用down属性;
此外还有下面属性:
- NGX_HTTP_UPSTREAM_WEIGHT:可以在server中使用weight属性;
- NGX_HTTP_UPSTREAM_BACKUP:可以在server中使用backup属性。
聪明的读者如果联想到刚刚遇到的那个神奇的配置错误,可以得出一个结论:在负载均衡模块的指令处理函数中可以设置并修改upstream{}中”server”指令支持的属性。这是一个很重要的性质,因为不同的负载均衡模块对各种属性的支持情况都是不一样的,那么就需要在解析配置文件的时候检测出是否使用了不支持的负载均衡属性并给出错误提示,这对于提升系统维护性是很有意义的。但是,这种机制也存在缺陷,正如前面的例子所示,没有机制能够追加检查在更新支持属性之前已经配置了不支持属性的”server”指令。
设置init_upstream回调
nginx初始化upstream时,会在ngx_http_upstream_init_main_conf函数中调用设置的回调函数初始化负载均衡模块。这里不太好理解的是uscf的具体位置。通过下面的示意图,说明upstream负载均衡模块的配置的内存布局。

从图上可以看出,MAIN_CONF中ngx_upstream_module模块的配置项中有一个指针数组upstreams,数组中的每个元素对应就是配置文件中每一个upstream{}的信息。更具体的将会在后面的原理篇讨论。
初始化配置
init_upstream回调函数执行时需要初始化负载均衡模块的配置,还要设置一个新钩子,这个钩子函数会在nginx处理每个请求时作为初始化函数调用,关于这个新钩子函数的功能,后面会有详细的描述。这里,我们先分析IP hash模块初始化配置的代码:
ngx_http_upstream_init_round_robin(cf, us);
us->peer.init = ngx_http_upstream_init_ip_hash_peer;
这段代码非常简单:IP hash模块首先调用另一个负载均衡模块Round Robin的初始化函数,然后再设置自己的处理请求阶段初始化钩子。实际上几个负载均衡模块可以组成一条链表,每次都是从链首的模块开始进行处理。如果模块决定不处理,可以将处理权交给链表中的下一个模块。这里,IP hash模块指定Round Robin模块作为自己的后继负载均衡模块,所以在自己的初始化配置函数中也对Round Robin模块进行初始化。
初始化请求
nginx收到一个请求以后,如果发现需要访问upstream,就会执行对应的peer.init函数。这是在初始化配置时设置的回调函数。这个函数最重要的作用是构造一张表,当前请求可以使用的upstream服务器被依次添加到这张表中。之所以需要这张表,最重要的原因是如果upstream服务器出现异常,不能提供服务时,可以从这张表中取得其他服务器进行重试操作。此外,这张表也可以用于负载均衡的计算。之所以构造这张表的行为放在这里而不是在前面初始化配置的阶段,是因为upstream需要为每一个请求提供独立隔离的环境。
为了讨论peer.init的核心,我们还是看IP hash模块的实现:
r->upstream->peer.data = &iphp->rrp;
ngx_http_upstream_init_round_robin_peer(r, us);
r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;
第一行是设置数据指针,这个指针就是指向前面提到的那张表;
第二行是调用Round Robin模块的回调函数对该模块进行请求初始化。面前已经提到,一个负载均衡模块可以调用其他负载均衡模块以提供功能的补充。
第三行是设置一个新的回调函数get。该函数负责从表中取出某个服务器。除了get回调函数,还有另一个r->upstream->peer.free的回调函数。该函数在upstream请求完成后调用,负责做一些善后工作。比如我们需要维护一个upstream服务器访问计数器,那么可以在get函数中对其加1,在free中对其减1。如果是SSL的话,nginx还提供两个回调函数peer.set_session和peer.save_session。一般来说,有两个切入点实现负载均衡算法,其一是在这里,其二是在get回调函数中。
peer.get和peer.free回调函数
这两个函数是负载均衡模块最底层的函数,负责实际获取一个连接和回收一个连接的预备操作。之所以说是预备操作,是因为在这两个函数中,并不实际进行建立连接或者释放连接的动作,而只是执行获取连接的地址或维护连接状态的操作。需要理解的清楚一点,在peer.get函数中获取连接的地址信息,并不代表这时连接一定没有被建立,相反的,通过get函数的返回值,nginx可以了解是否存在可用连接,连接是否已经建立。这些返回值总结如下:
返回值 | 说明 | nginx后续动作 |
NGX_DONE | 得到了连接地址信息,并且连接已经建立。 | 直接使用连接,发送数据。 |
NGX_OK | 得到了连接地址信息,但连接并未建立。 | 建立连接,如连接不能立即建立,设置事件, 暂停执行本请求,执行别的请求。 |
NGX_BUSY | 所有连接均不可用。 | 返回502错误至客户端。 |
各位读者看到上面这张表,可能会有几个问题浮现出来:
Q: | 什么时候连接是已经建立的? |
---|---|
A: | 使用后端keepalive连接的时候,连接在使用完以后并不关闭,而是存放在一个队列中,新的请求只需要从队列中取出连接,这些连接都是已经准备好的。 |
Q: | 什么叫所有连接均不可用? |
A: | 初始化请求的过程中,建立了一张表,get函数负责每次从这张表中不重复的取出一个连接,当无法从表中取得一个新的连接时,即所有连接均不可用。 |
Q: | 对于一个请求,peer.get函数可能被调用多次么? |
A: | 正式如此。当某次peer.get函数得到的连接地址连接不上,或者请求对应的服务器得到异常响应,nginx会执行ngx_http_upstream_next,然后可能再次调用peer.get函数尝试别的连接。upstream整体流程如下: |

本节回顾
这一节介绍了负载均衡模块的基本组成。负载均衡模块的配置区集中在upstream{}块中。负载均衡模块的回调函数体系是以init_upstream为起点,经历init_peer,最终到达peer.get和peer.free。其中init_peer负责建立每个请求使用的server列表,peer.get负责从server列表中选择某个server(一般是不重复选择),而peer.free负责server释放前的资源释放工作。最后,这一节通过一张图将upstream模块和负载均衡模块在请求处理过程中的相互关系展现出来。
=============================================
http://blog.csdn.net/poechant/article/details/7256184
转载请注明来自“柳大的CSDN博客”:http://blog.csdn.net/poechant
更多文章请浏览CSDN专栏《Nginx高性能Web服务器》或 服务器后端开发系列——《实战Nginx高性能Web服务器》
Nginx 的 HttpUpstreamModule 提供对后端(backend)服务器的简单负载均衡。一个最简单的 upstream 写法如下:
upstream backend {
server backend1.example.com;
server backend2.example.com;
server.backend3.example.com;
}
server {
location / {
proxy_pass http://backend;
}
}
1、后端服务器
通过 upstream 可以设定后端服务器,指定的方式可以是 IP 地址与端口、域名、UNIX 套接字(socket)。其中如果域名可以被解析为多个地址,则这些地址都作为 backend。下面举例说明:
upstream backend {
server blog.csdn.net/poechant;
server 145.223.156.89:8090;
server unix:/tmp/backend3;
}
第一个 backend 是用域名指定的。第二个 backend 是用 IP 和端口号指定的。第三个 backend 是用 UNIX 套接字指定的。
2、负载均衡策略
Nginx 提供轮询(round robin)、用户 IP 哈希(client IP)和指定权重 3 种方式。
默认情况下,Nginx 会为你提供轮询作为负载均衡策略。但是这并不一定能够让你满意。比如,某一时段内的一连串访问都是由同一个用户 Michael 发起的,那么第一次 Michael 的请求可能是 backend2,而下一次是 backend3,然后是 backend1、backend2、backend3…… 在大多数应用场景中,这样并不高效。当然,也正因如此,Nginx 为你提供了一个按照 Michael、Jason、David 等等这些乱七八糟的用户的 IP 来 hash 的方式,这样每个 client 的访问请求都会被甩给同一个后端服务器。(另外,由于最近发现很多网站以不留原文链接的方式盗取本博博文,所以我就在这插一下本博的地址“http://blog.csdn.net/poechant”)具体的使用方式如下:
upstream backend {
ip_hash;
server backend1.example.com;
server backend2.example.com;
server.backend3.example.com;
}
这种策略中,用于进行 hash 运算的 key,是 client 的 C 类 IP 地址(C 类 IP 地址就是范围在 192.0.0.0 到 223.255.255.255 之间,前三段号码表示子网,第四段号码为本地主机的 IP 地址类别)。这样的方式保证一个 client 每次请求都将到达同一个 backend。当然,如果所 hash 到的 backend 当前不可用,则请求会被转移到其他 backend。
再介绍一个和 ip_hash 配合使用的关键字:down。当某个一个 server 暂时性的宕机(down)时,你可以使用“down”来标示出来,并且这样被标示的 server 就不会接受请求去处理。具体如下:
upstream backend {
server blog.csdn.net/poechant down;
server 145.223.156.89:8090;
server unix:/tmp/backend3;
}
还可以使用指定权重(weight)的方式,如下:
upstream backend {
server backend1.example.com;
server 123.321.123.321:456 weight=4;
}
默认情况下 weight 为 1,对于上面的例子,第一个 server 的权重取默认值 1,第二个是 4,所以相当于第一个 server 接收 20% 的请求,第二接收 80% 的。要注意的是 weight 与 ip_hash 是不能同时使用的,原因很简单,他们是不同且彼此冲突的策略。
3、重试策略
可以为每个 backend 指定最大的重试次数,和重试时间间隔。所使用的关键字是 max_fails 和 fail_timeout。如下所示:
upstream backend {
server backend1.example.comweight=5;
server 54.244.56.3:8081max_fails=3 fail_timeout=30s;
}
在上例中,最大失败次数为 3,也就是最多进行 3 次尝试,且超时时间为 30秒。max_fails 的默认值为 1,fail_timeout 的默认值是 10s。传输失败的情形,由 proxy_next_upstream 或 fastcgi_next_upstream 指定。而且可以使用 proxy_connect_timeout 和 proxy_read_timeout 控制 upstream 响应时间。
有一种情况需要注意,就是 upstream 中只有一个 server 时,max_fails 和 fail_timeout 参数可能不会起作用。导致的问题就是 nginx 只会尝试一次 upstream 请求,如果失败这个请求就被抛弃了 : ( ……解决的方法,比较取巧,就是在 upstream 中将你这个可怜的唯一 server 多写几次,如下:
upstream backend {
server backend.example.com max_fails fail_timeout=30s;
server backend.example.com max_fails fail_timeout=30s;
server backend.example.com max_fails fail_timeout=30s;
}
4、备机策略
从 Nginx 的 0.6.7 版本开始,可以使用“backup”关键字。当所有的非备机(non-backup)都宕机(down)或者繁忙(busy)的时候,就只使用由 backup 标注的备机。必须要注意的是,backup 不能和 ip_hash 关键字一起使用。举例如下:
upstream backend {
server backend1.example.com;
server backend2.example.combackup;
server backend3.example.com;
}
转载请注明来自“柳大的CSDN博客”:http://blog.csdn.net/poechant
更多文章请浏览CSDN专栏《Nginx高性能Web服务器》或 服务器后端开发系列——《实战Nginx高性能Web服务器》
=============================
http://blog.sina.com.cn/s/blog_534ccf6d0100u7ez.html
nginx upstream的几种配置方式
文章分类:Web前端
nginx 的upstream目前支持4种方式的分配
1、轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器 ,如果后端服务器down掉,能自动剔除。
2、weight
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
例如:
upstream bakend {
server 192.168.0.14 weight=10;
server 192.168.0.15 weight=10;
}
2、ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session 的问题。
例如:
upstream bakend {
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}
3、fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
upstream backend {
server server1;
server server2;
fair;
}
4、url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
例:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法
upstream backend {
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}
tips:
upstream bakend{#定义负载均衡 设备的Ip及设备状态
ip_hash;
server 127.0.0.1:9090 down;
server 127.0.0.1:8080 weight=2;
server 127.0.0.1:6060;
server 127.0.0.1:7070 backup;
}
在需要使用负载均衡的server中增加
proxy_pass http://bakend/ ;
每个设备的状态设置为:
1.down 表示单前的server暂时不参与负载
2.weight 默认为1.weight越大,负载的权重就越大。
3.max_fails :允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误
4.fail_timeout:max_fails次失败后,暂停的时间。
5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
nginx支持同时设置多组的负载均衡,用来给不用的server来使用。
client_body_in_file_only 设置为On 可以讲client post过来的数据记录到文件中用来做debug
client_body_temp_path 设置记录文件的目录 可以设置最多3层目录
location 对URL进行匹配.可以进行重定向或者进行新的代理 负载均衡
-------------------------------------------------------------------------------------------------------
#############这个不错#####################3
转:http://salogs.com/2010/05/nginx-http负载均衡反向代理的相关参数测试/
upstream test {
server 192.168.108.163 ;
server 192.168.108.164:80;
}
·weight = NUMBER - 设置服务器权重,默认为1。
·max_fails = NUMBER - 在一定时间内(这个时间在fail_timeout参数中设置)检查这个服务器是否可用时产生的最多失败请求数,默认为1,将其设置为0可以关闭检查,
这些错误在proxy_next_upstream或fastcgi_next_upstream(404错误不会使max_fails增加)中定义。
·fail_timeout = TIME - 在这个时间内产生了max_fails所设置大小的失败尝试连接请求后这个服务器可能不可用,
同样它指定了服务器不可用的时间(在下一次尝试连接请求发起之前),默认为10秒,fail_timeout与前端响应时间没有直接关系,
不过可以使用proxy_connect_timeout和 proxy_read_timeout来控制。
·down - 标记服务器处于离线状态,通常和ip_hash一起使用。
·backup - (0.6.7或更高)只用于本服务器,如果所有的非备份服务器都宕机或繁忙。backup只有当该组中所有的主机都不可用时才会转到backup上来
关于max_fails参数的理解:根据上面的解释,max_fails默认为1,fail_timeout默认为10秒,也就是说,
默认情况下后端服务器在10秒钟之内可以容许有一次的失败,如果超过1次则视为该服务器有问题,将该服务器标记为不可用。
等待10秒后再将请求发给该服务器,以此类推进行后端服务器的健康检查。但如果我将max_fails设置为0,则代表不对后端服务器进行健康检查,
这样一来fail_timeout参数也就没什么意义了。那若后端服务器真的出现问题怎么办呢?上文也说了,可以借助proxy_connect_timeout和proxy_read_timeout进行控制。
下面介绍http proxy模块中的相关指令:
proxy_next_upstream
语法: proxy_next_upstream [error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_404|off]
确定在何种情况下请求将转发到下一个服务器。转发请求只发生在没有数据传递到客户端的过程中。
proxy_connect_timeout
后端服务器连接的超时时间_发起握手等候响应超时时间
proxy_read_timeout
连接成功后_等候后端服务器响应时间_其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间)
proxy_send_timeout
后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据
proxy_pass
这个指令设置被代理服务器的地址和被映射的URI
-------------------------------------------------------------------------------------------------------------
mbillion 注
max_fails=0 关闭后端服务器健康检查,不会标记服务器不可用,fail_timeout的值也失去意义。
proxy_next_upstream error timeout http_500 http_502 http_504
server 192.168.108.163 max_fails = 0;
server 192.168.108.164 max_fails = 0;
关闭了后端服务器的健康检查(max_fails=0)因此判断后端服务器情况的唯一依据便是proxy_read_timeout参数
1、如果用户请求到192.168.108.163,只有在proxy_read_timeout超时才转到192.168.108.164服务器,但不会标记192.168.108.163不可用,因为max_fails=0
2、如果用户请求到192.168.108.163有报错,报错信息在proxy_next_upstream 中有定义的话nginx还会跳到下一台服务器 192.168.108.164,但不会标记192.168.108.163不可用,因为max_fails=0
3、后端服务器正常但执行超时的情况下nginx根据proxy_read_timeout和proxy_next_upstream的值来选择下一个服务器,那如果我后端服务器直接报错的情况呢?
可以想到如果报错信息在proxy_next_upstream 中有定义的话nginx还会跳到下一台服务器。否则直接将保存信息返回给nginx从而最终呈献给用户
max_fails=1 打开后端服务器健康检查,会标记服务器不可用,根据fail_timeout的值不可有时间
proxy_next_upstream error timeout http_500 http_502 http_504
server 192.168.108.163 max_fails = 1 fail_timeout=10s;
server 192.168.108.164 max_fails = 1 fail_timeout=10s;
1、如果用户请求到192.168.108.163,只有在proxy_read_timeout超时才转到192.168.108.164服务器,同时标记192.168.108.163不可用,同时标记192.168.108.163不可用,在fail_timeout=10s设置为10s(默认也是10S)
当在这10S内再有用户请求nginx就会直接转到192.168.108.164服务器,当过10S后会标记192.168.108.163可用
2、如果用户请求到192.168.108.163有报错,报错信息在proxy_next_upstream 中有定义的话nginx还会跳到下一台服务器 192.168.108.164,同时标记192.168.108.163不可用,在fail_timeout=10s设置为10s(默认也是10S)
当在这10S内再有用户请求nginx就会直接转到192.168.108.164服务器,当过10S后会标记192.168.108.163可用
总结:
nginx的健康检查就是通过访问,如果访问出现超时,或出错时就会标记为不可用,如max_fails = 1,如果在后端192.168.108.163出现请求不可用,在后端192.168.108.163就会看到一条请求(当然大型网站可能在短时间内在大量的请求过来),在10S内不会有第二条请求过来,因为max_fails = 1设置为1
用户所花的访问时间,程序执行时间+proxy_read_timeout 时间,proxy_read_timeout设置为60s 如果在请求第一台时出现超时,就会跳到第二台,第台机器程序如果执行交时间为10s
那么用户花费的时间就60s
不管开不开启后端服务器健康检查,一次请求只能轮一遍服务器,不会出现重复。
如果多台后端服务器,难道要轮一遍吗,nginx还有一个参数client_body_timeout的值,客户端请求内容超时时间,默认是60秒,出现超时就会返回request time out,错误信息(http 状态码:408)
注意:
max_fails有几次就会看到几个请求,一直达到max_fails的次数才会认为192.168.108.163是不可用的,此时请求才会到 192.168.108.164上。
这里要说明的是404错误,因为按照官方的文档404并不会使得max_fails次数增加如果设置这样一条规则:
proxy_next_upstream http_404;
upstream edt{
server 192.168.16.80:81 max_fails=2 fail_timeout=60s; #规则一
server 192.168.16.196:80 backup; #规则三
}
当出现404时候你会发现这是一个死循环,在16.80的日志中会不停的循环出现该404请求,直至资源耗尽。所以要注意。
但是当出现404就是想跳到另外的机器怎么实现呢?
可以用这样的办法error_page 404 = @backup;
然后设置一个location backup的方法来迂回实现。具体配置如上。
转:http://hi.baidu.com/171892549/blog/item/5e9754ee496f10e3b2fb957f.html
------------------------------------------------------------------------------------------------------------------------
设备状态:
Weight=NUMBER
设置服务器的权重,权重数值越高,被 分配到客户端的请求次数越多,默认值为1
Max_fails=BUMBER
在参数fail_timeout指定时间内对后端服务器请求失改的次数,如果检测到后端服务器无法连接及发生服务器错误(404错误除外),则标记为失败。如果没有设置,则默认值为1,设置为0将关闭这项检查。
Tail_timeout=TIME
在经历参数max_fails设置的失改次数后,暂停的时间。
Down
标记服务器为永久离线状态,用于ip_hash命令。
Backup
仅在非backup服务器全部宕机或繁忙的时候才启用,这台服务器的负载会最轻。
如:
server 127.0.0.1:8080 max_fails=2 fail_timeout=30s;
server 127.0.0.1:6060 max_fails=2 fail_timeout=30s;
默认情况下后端服务器在30秒钟之内可以容许有两次的失败,如果超过两次则视为该服务器有问题,将该服务器标记为不可用。
在之后的30秒之内,所有请求不会再发送给有问题的服务器,等待30秒后再将请求发给该服务器,以此类推进行后端服务器的健康检查
------------------------------------------------------------------------------------------------------------------------------------------
nginx中,ip_hash和url_hash的区别
最近看nginx的负载均衡,发现为了解决nginx的session问题,有两种方法,就是ip_hash和url_hash,ip_hash是根据ip来维持session的,
而url_hash是根据url地址的,url_hash的优点是能够提高后端缓存服务器的效率,比如提高squid的效率,但是缺点是当后端服务器宕机的时候,
url_hash不会自动跳转的其他缓存服务器,而是返回给用户一个503错误,我想问的是,那ip_hash有没有解决这个问题,是不是会跳转到其他机器上,
还是一样会返回一个503错误,那ip_hash和url_hash有什么区别啊,还有就是nginx能不能即解决session问题又解决后端服务器的健康检查问题。
我记得squid是可以健康检查和session保持的。
答:
你可以使用memcached来保持session,实现session共享,无需担心session掉线
怎么做呢
================================================================
http://blog.csdn.net/lizhiqiang50/article/details/7551292
1. 实现原理
Nginx 1.1.14版本以前upstream连接建立和获取的机制如下图所示,Nginx会在一开始创建connection pool(进程间不共享,可以避免锁),提供给所有向前/后的连接。
如果要实现upstream长连接,则每个进程需要另外一个connection pool,里面都是长连接。一旦与后端服务器建立连接,则在当前请求连接结束之后不会立即关闭连接,而是把用完的连接保存在一个keepalive connection pool里面,以后每次需要建立向后连接的时候,只需要从这个连接池里面找,如果找到合适的连接的话,就可以直接来用这个连接,不需要重新创建socket或者发起connect()。这样既省下建立连接时在握手的时间消耗,又可以避免TCP连接的slow start。如果在keepalive连接池找不到合适的连接,那就按照原来的步骤重新建立连接。假设连接查找时间可以忽略不计,那么这种方法肯定是有益而无害的(当然,需要少量额外的内存)。
2. 相关配置
Nginx Upstream长连接由upstream模式下的keepalive指令控制,并指定可用于长连接的连接数,配置样例如下:
upstream http_backend {
server 127.0.0.1:8080;
keepalive 16;
}
server {
...
location /http/ {
proxy_pass http://http_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
...
}
}
目前Nginx只支持反向代理到upstream下配置的server,不支持直接由proxy_pass指令配置的server,更不支持proxy_pass参数中包含变量的情况。此外,为支持长连接,需要配置使用HTTP1.1协议(虽然HTTP 1.0可通过设置Connection请求头为“keep-alive”来实现长连接,但这并不推荐)。
此外,由于HTTPPROXY模块默认会将反向代理请求的connection头部设置成Close,因此这里也需要清除connection头部(清除头部即不发送该头部,在HTTP 1.0中默认为长连接)。
3. 现实细节
3.1. 数据结构
Nginx upstream keepalive的具体实现在文件src/http/modules/ngx_http_upstream_keepalive_module.c中,该文件中包含以下三个数据结构:
typedef struct {
//最大缓存连接个数,由keepalive参数指定(keepaliveconnection)
ngx_uint_t max_cached;
ngx_uint_t single; /* unsigned:1 */
//长连接队列,其中cache为缓存连接池,free为空闲连接池。初始化时根据keepalive
//指令的参数初始化free队列,后续有连接过来从free队列
//取连接,请求处理结束后将长连接缓存到cache队列,连接被断开(或超时)再从cache
//队列放入free队列
ngx_queue_t cache;
ngx_queue_t free;
ngx_http_upstream_init_pt original_init_upstream;
ngx_http_upstream_init_peer_pt original_init_peer;
}ngx_http_upstream_keepalive_srv_conf_t;
typedef struct {
ngx_http_upstream_keepalive_srv_conf_t *conf;
ngx_http_upstream_t *upstream;
void *data;
//保存原始获取peer和释放peer的钩子,它们通常是ngx_http_upstream_get_round_robin_peer和ngx_http_upstream_free_round_robin_peer,nginx负载均衡默认是使用轮询算法
ngx_event_get_peer_pt original_get_peer;
ngx_event_free_peer_pt original_free_peer;
#if (NGX_HTTP_SSL)
ngx_event_set_peer_session_pt original_set_session;
ngx_event_save_peer_session_pt original_save_session;
#endif
ngx_uint_t failed; /* unsigned:1 */
}ngx_http_upstream_keepalive_peer_data_t;
typedef struct {
ngx_http_upstream_keepalive_srv_conf_t *conf;
ngx_queue_t queue;
ngx_connection_t *connection;
//缓存连接池中保存的后端服务器的地址,后续就是根据相同的socket地址来找出
//对应的连接,并使用该连接
socklen_t socklen;
u_char sockaddr[NGX_SOCKADDRLEN];
}ngx_http_upstream_keepalive_cache_t;
3.2. 初始化操作
该模块中只有keepalive一个指令。在nginx解析配置文件遇到upstream模式下的keepalive时,会执行ngx_http_upstream_keepalive函数:
static char *
ngx_http_upstream_keepalive(ngx_conf_t*cf, ngx_command_t *cmd, void *conf)
{
……
// 获取upstream模块的server conf
uscf =ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
kcf = ngx_http_conf_upstream_srv_conf(uscf,
ngx_http_upstream_keepalive_module);
// 保存原来的初始化upstream的钩子,并设置新的钩子
kcf->original_init_upstream =uscf->peer.init_upstream
? uscf->peer.init_upstream
:ngx_http_upstream_init_round_robin;
uscf->peer.init_upstream =ngx_http_upstream_init_keepalive;
// 设置指令配置的最大缓存连接数
/* read options */
value = cf->args->elts;
n = ngx_atoi(value[1].data, value[1].len);
kcf->max_cached = n;
……
return NGX_CONF_ERROR;
}
在upstream模块初始化main conf时,就会根据设置的钩子进行upstream的初始化,该操作是在ngx_http_upstream_init_main_conf函数中执行的:
static char *
ngx_http_upstream_init_main_conf(ngx_conf_t*cf, void *conf)
{
ngx_http_upstream_main_conf_t *umcf = conf;
……
uscfp = umcf->upstreams.elts;
//遍历upstream数组。upstream的来源包括显性的配置upstream模式(ngx_http_upstream)
//和隐性的proxy_pass指令(ngx_http_proxy_pass),upstream的添加是通过
//ngx_http_upstream_add函数,需要注意的是proxy_pass指令携带变量参数时不会添加
//upstream
for (i = 0; i <umcf->upstreams.nelts; i++) {
//取钩子,并执行钩子。若该upstream来源于包含keepalive指令的upsteam模式,
//则钩子为前面设置的ngx_http_upstream_init_keepalive;其他情况均为空,则会
//使用ngx_http_upstream_init_round_robin
init = uscfp[i]->peer.init_upstream? uscfp[i]->peer.init_upstream:
ngx_http_upstream_init_round_robin;
if (init(cf, uscfp[i]) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
}
再来看upstreamkeepalive初始化函数ngx_http_upstream_init_keepalive:
static ngx_int_t
ngx_http_upstream_init_keepalive(ngx_conf_t*cf,
ngx_http_upstream_srv_conf_t *us)
{
……
kcf = ngx_http_conf_upstream_srv_conf(us,
ngx_http_upstream_keepalive_module);
// 先执行原始初始化upstream函数(即ngx_http_upstream_init_round_robin),该函数
// 会根据配置的后端地址解析成socket地址,用于连接后端。并设置us->peer.init钩子
// 为ngx_http_upstream_init_round_robin_peer
if (kcf->original_init_upstream(cf, us) != NGX_OK) {
return NGX_ERROR;
}
// 保存原钩子,并用keepalive的钩子覆盖旧钩子,初始化后端请求的时候会调用这个
// 新钩子
kcf->original_init_peer =us->peer.init;
us->peer.init =ngx_http_upstream_init_keepalive_peer;
/* 申请缓存项,并添加到free队列中,后续用从free队列里面取 */
cached = ngx_pcalloc(cf->pool,
sizeof(ngx_http_upstream_keepalive_cache_t)* kcf->max_cached);
if (cached == NULL) {
return NGX_ERROR;
}
ngx_queue_init(&kcf->cache);
ngx_queue_init(&kcf->free);
for (i = 0; i < kcf->max_cached; i++){
ngx_queue_insert_head(&kcf->free, &cached[i].queue);
cached[i].conf = kcf;
}
return NGX_OK;
}
至此,与upstreamkeepalive相关的初始化操作已经全部完毕,接下来反向代理如何使用keepalive连接。
3.3. 连接的建立/获取
在nginx收到HTTP请求时,准备将这个请求转发给后端之前,先要与后端建立TCP连接,这些准备操作是在ngx_http_upstream_init_request函数中:
static void
ngx_http_upstream_init_request(ngx_http_request_t*r)
{
……
// 不需要解析地址的情况,包括配置upstream模式下的静态server,静态proxy_pass
// 指令,即初始化过程中已经能将地址解析好的情况(静态IP后端或者可通过
//gethostbyname获取到地址的域名后端)
if (u->resolved == NULL) {
uscf = u->conf->upstream;
} else {
// 需要解析地址,并且已经解析完毕的情况(已经有socket地址),直接连接后端
if (u->resolved->sockaddr) {
if(ngx_http_upstream_create_round_robin_peer(r, u->resolved)
!= NGX_OK)
{
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
ngx_http_upstream_connect(r, u);
return;
}
// 需要解析地址,但socket地址还未解析
host = &u->resolved->host;
umcf = ngx_http_get_module_main_conf(r,ngx_http_upstream_module);
//先找当前访问的host是否在配置的upstream数组中
uscfp = umcf->upstreams.elts;
for (i = 0; i <umcf->upstreams.nelts; i++) {
uscf = uscfp[i];
if (uscf->host.len ==host->len
&& ((uscf->port == 0&& u->resolved->no_port)
|| uscf->port ==u->resolved->port)
&&ngx_memcmp(uscf->host.data, host->data, host->len) == 0)
{
goto found;
}
}
if (u->resolved->port == 0) {
ngx_log_error(NGX_LOG_ERR,r->connection->log, 0,
"no port inupstream \"%V\"", host);
ngx_http_upstream_finalize_request(r,u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
temp.name = *host;
// 开始进行域名解析
ctx =ngx_resolve_start(clcf->resolver, &temp);
if (ctx == NULL) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
if (ctx == NGX_NO_RESOLVER) {
ngx_log_error(NGX_LOG_ERR,r->connection->log, 0,
"no resolverdefined to resolve %V", host);
ngx_http_upstream_finalize_request(r, u, NGX_HTTP_BAD_GATEWAY);
return;
}
ctx->name = *host;
ctx->type = NGX_RESOLVE_A;
// 设置DNS解析钩子,解析DNS成功后会执行
ctx->handler =ngx_http_upstream_resolve_handler;
ctx->data = r;
ctx->timeout =clcf->resolver_timeout;
u->resolved->ctx = ctx;
// 开始处理DNS解析域名
if (ngx_resolve_name(ctx) != NGX_OK) {
u->resolved->ctx = NULL;
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
return;
}
found:
//调用peer.init初始化对端,若访问请求为配置了keepalive指令的upstream列表中的
//server,该钩子并初始化成ngx_http_upstream_init_keepalive_peer,否则是ngx_http_upstream_init_round_robin_peer。根据上述的代码,可以知道只有不需要解析地址或者能找到upstream的情况才会走到这里
if (uscf->peer.init(r, uscf) != NGX_OK){
ngx_http_upstream_finalize_request(r,u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
ngx_http_upstream_connect(r, u);
}
可以看出,在不需要解析地址的情况下,初始化peer完毕后(uscf->peer.init)就开始连接后端(ngx_http_upstream_connect),先来看下初始化keepalive peer函数(ngx_http_upstream_init_keepalive_peer):
static ngx_int_t ngx_http_upstream_init_keepalive_peer(ngx_http_request_t*r,
ngx_http_upstream_srv_conf_t *us)
{
……
kcf = ngx_http_conf_upstream_srv_conf(us,
ngx_http_upstream_keepalive_module);
kp = ngx_palloc(r->pool,sizeof(ngx_http_upstream_keepalive_peer_data_t));
if (kp == NULL) {
return NGX_ERROR;
}
// 先执行原始的初始化peer函数,即ngx_http_upstream_init_round_robin_peer。该
// 函数内部处理一些与负载均衡相关的操作并分别设置以下四个钩子:
// r->upstream->peer.get和 r->upstream->peer.free
// r->upstream->peer.set_session和 r->upstream->peer.save_session
if (kcf->original_init_peer(r, us) !=NGX_OK) {
return NGX_ERROR;
}
// keepalive模块则保存上述原始钩子,并使用新的各类钩子覆盖旧钩子
kp->conf = kcf;
kp->upstream = r->upstream;
kp->data = r->upstream->peer.data;
kp->original_get_peer =r->upstream->peer.get;
kp->original_free_peer =r->upstream->peer.free;
r->upstream->peer.data = kp;
r->upstream->peer.get =ngx_http_upstream_get_keepalive_peer;
r->upstream->peer.free =ngx_http_upstream_free_keepalive_peer;
#if(NGX_HTTP_SSL)
kp->original_set_session =r->upstream->peer.set_session;
kp->original_save_session =r->upstream->peer.save_session;
r->upstream->peer.set_session =ngx_http_upstream_keepalive_set_session;
r->upstream->peer.save_session =ngx_http_upstream_keepalive_save_session;
#endif
return NGX_OK;
}
在连接后端的函数(ngx_http_upstream_connect)中,会调用ngx_event_connect_peer去建立TCP连接:
ngx_int_t ngx_event_connect_peer(ngx_peer_connection_t*pc)
{
int rc;
ngx_int_t event;
ngx_err_t err;
ngx_uint_t level;
ngx_socket_t s;
ngx_event_t *rev, *wev;
ngx_connection_t *c;
//调用pc->get钩子。如前面所述,若是keepalive upstream,则该钩子是
//ngx_http_upstream_get_keepalive_peer,此时如果存在缓存长连接该函数调用返回的是
//NGX_DONE,直接返回上层调用而不会继续往下执行获取新的连接并创建socket,
//如果不存在缓存的长连接,则会返回NGX_OK.
//若是非keepalive upstream,该钩子是ngx_http_upstream_get_round_robin_peer。
rc = pc->get(pc, pc->data);
if (rc != NGX_OK) {
return rc;
}
// 非keepalive upstream或者keepalive upstream为找到缓存连接,则创建socket
s = ngx_socket(pc->sockaddr->sa_family,SOCK_STREAM, 0);
ngx_log_debug1(NGX_LOG_DEBUG_EVENT,pc->log, 0, "socket %d", s);
if (s == -1) {
ngx_log_error(NGX_LOG_ALERT,pc->log, ngx_socket_errno,
ngx_socket_n "failed");
return NGX_ERROR;
}
c = ngx_get_connection(s, pc->log);
……
}
下面看下keepaliveupstream是如何保存缓存连接并当后续使用时如何获取的,这是就是在ngx_http_upstream_get_keepalive_peer函数中完成:
static ngx_int_t
ngx_http_upstream_get_keepalive_peer(ngx_peer_connection_t*pc, void *data)
{
……
/* single pool ofcached connections,只要cache非空,直接从cache里面取,并返回NGX_DONE*/
if (kp->conf->single &&!ngx_queue_empty(&kp->conf->cache)) {
q =ngx_queue_head(&kp->conf->cache);
item = ngx_queue_data(q,ngx_http_upstream_keepalive_cache_t, queue);
c = item->connection;
ngx_queue_remove(q);
ngx_queue_insert_head(&kp->conf->free, q);
……
pc->connection = c;
pc->cached = 1;
return NGX_DONE;
}
// 先调用原始getpeer钩子(ngx_http_upstream_get_round_robin_peer)选择后端
rc = kp->original_get_peer(pc,kp->data);
if (kp->conf->single || rc != NGX_OK){
return rc;
}
/* search cache for suitable connection */
// 根据socket地址查找连接cache池,找到直接返回NGX_DONE,上层调用就不会获取新的连接
cache = &kp->conf->cache;
for (q = ngx_queue_head(cache);
q != ngx_queue_sentinel(cache);
q = ngx_queue_next(q))
{
item = ngx_queue_data(q,ngx_http_upstream_keepalive_cache_t, queue);
c = item->connection;
if (ngx_memn2cmp((u_char *)&item->sockaddr, (u_char *) pc->sockaddr,
item->socklen,pc->socklen)
== 0)
{
ngx_queue_remove(q);
ngx_queue_insert_head(&kp->conf->free, q);
……
pc->connection = c;
pc->cached = 1;
return NGX_DONE;
}
}
return NGX_OK;
}
上述分析了不需要进行解析地址的情况下如何使用keepalive的,当访问的地址需要进行域名解析的情况下(比如proxy_pass中存在变量,变量的值为一个域名,任何静态域名均会在初始化时候被解析),是无法使用keepalive连接的,请看nginx在解析域名成功后是怎么处理的(即ngx_http_upstream_init_request函数中设置ctx->handler = ngx_http_upstream_resolve_handler):
static void ngx_http_upstream_resolve_handler(ngx_resolver_ctx_t*ctx)
{
……
// 创建轮询后端,设置获取peer和释放peer的钩子等(与ngx_http_upstream_init_round_robin_peer的功能有点类似)
if (ngx_http_upstream_create_round_robin_peer(r,ur) != NGX_OK){
ngx_http_upstream_finalize_request(r,u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
ngx_resolve_name_done(ctx);
ur->ctx= NULL;
// 直接开始连接后端,注意该函数中的获取后端的钩子是上述调用中设置的ngx_http_upstream_get_round_robin_peer而不会是ngx_http_upstream_get_keepalive_peer
ngx_http_upstream_connect(r, u);
}
经过上面分析,若想支持任何域名请求都使用keepalive,则需要修改上述函数,需要在这个函数里面调用ngx_http_upstream_init_keepalive_peer改变获取/释放连接的接口,然后由于当前nginxkeepalive实现完全依赖于在upstream模式下配置,需要upstream server conf的支持,因此直接调用该初始化函数是无法实现的。
3.4. 连接的关闭/释放
在一个HTTP请求处理完毕后,通常会调用ngx_http_upstream_finalize_request结束请求,并调用释放peer的操作:
static void ngx_http_upstream_finalize_request(ngx_http_request_t*r,
ngx_http_upstream_t *u, ngx_int_t rc)
{
……
u->finalize_request(r, rc);
// 如果有设置peer.free钩子,则调用释放peer
// 非keepalive的情况下该钩子是ngx_http_upstream_free_round_robin_peer
// keepalive的情况下该钩子是ngx_http_upstream_free_keepalive_peer,该钩子会将连接
// 缓存到长连接cache池,并将u->peer.connection设置成空,防止下面代码关闭连接。
if (u->peer.free) {
u->peer.free(&u->peer,u->peer.data, 0);
}
// 若与对端的连接为关闭(或未被缓存起来),则关闭连接
if (u->peer.connection) {
#if(NGX_HTTP_SSL)
if (u->peer.connection->ssl) {
u->peer.connection->ssl->no_wait_shutdown= 1;
(void)ngx_ssl_shutdown(u->peer.connection);
}
#endif
if (u->peer.connection->pool) {
ngx_destroy_pool(u->peer.connection->pool);
}
ngx_close_connection(u->peer.connection);
}
//关闭连接后置空
u->peer.connection = NULL;
……
}
接着分析设置keepalive情况下的free peer的函数,即ngx_http_upstream_free_keepalive_peer:
static void ngx_http_upstream_free_keepalive_peer(ngx_peer_connection_t*pc, void *data,
ngx_uint_t state)
{
……
//通常设置keepalive后连接都是由后端web服务发起的,因此需要添加读事件
if (ngx_handle_read_event(c->read, 0) !=NGX_OK) {
goto invalid;
}
//如果free队列中可用cache items为空,则从cache队列取一个最近最少使用item,
//将该item对应的那个连接关闭,该item用于保存当前需要释放的连接
if(ngx_queue_empty(&kp->conf->free)) {
q =ngx_queue_last(&kp->conf->cache);
ngx_queue_remove(q);
item = ngx_queue_data(q,ngx_http_upstream_keepalive_cache_t, queue);
ngx_http_upstream_keepalive_close(item->connection);
} else {
//free队列不空则直接从队列头取一个item用于保存当前连接
q= ngx_queue_head(&kp->conf->free);
ngx_queue_remove(q);
item = ngx_queue_data(q,ngx_http_upstream_keepalive_cache_t, queue);
}
//缓存当前连接,将item插入cache队列,然后将pc->connection置空,防止上层调用
//ngx_http_upstream_finalize_request关闭该连接(详见该函数)
item->connection = c;
ngx_queue_insert_head(&kp->conf->cache, q);
pc->connection = NULL;
if (c->read->timer_set) {
ngx_del_timer(c->read);
}
if (c->write->timer_set) {
ngx_del_timer(c->write);
}
//设置连接读写钩子。写钩子是一个假钩子(keepalive连接不会由客户端主动关闭)
//读钩子处理关闭keepalive连接的操作(接收到来自后端web服务器的FIN分节)
c->write->handler =ngx_http_upstream_keepalive_dummy_handler;
c->read->handler =ngx_http_upstream_keepalive_close_handler;
……
// 保存socket地址相关信息,后续就是通过查找相同的socket地址来复用该连接
item->socklen = pc->socklen;
ngx_memcpy(&item->sockaddr,pc->sockaddr, pc->socklen);
……
}
分析完ngx_http_upstream_free_keepalive_peer函数后,在回过头去看ngx_http_upstream_get_keepalive_peer就更能理解是如何复用keepalive连接的,free操作将当前连接缓存到cache队列中,并保存该连接对应后端的socket地址,get操作根据想要连接后端的socket地址,遍历查找cache队列,如果找到就使用先前缓存的长连接,未找到就重新建立新的连接。
当free操作发现当前所有cache item用完时(即缓存连接达到上限),会关闭最近未被使用的那个连接,用来缓存新的连接。Nginx官方推荐keepalive的连接数应该配置的尽可能小,以免出现被缓存的连接太多而造成新的连接请求过来时无法获取连接的情况(一个worker进程的总连接池是有限的)。
3.5. 实现总结
nginx upstream keepalive实现主要通过当解析到upstream模式下的keepalive命令时,为该upstream改变初始化钩子(ngx_http_upstream_init_keepalive),而在初始化upstream时又再次改变初始化对端的钩子(ngx_http_upstream_init_keepalive_peer),在初始化对端时,再一次改变获取对端和释放对端的钩子(ngx_http_upstream_get_keepalive_peer和ngx_http_upstream_free_keepalive_peer)。在获取对端时,会先查找所访问的对端是否已经在cache池内(通过对端的socket地址),如果在cache池内则直接使用cache的连接,否则需要建立新的连接;在释放对端连接时,不直接释放连接,而是将连接保存在cache池中,同时使用对端的socket地址标识该对端,方便后续获取时查找。
可以看到,nginxupstream keepalive在缓存连接(free操作)和获取缓存的连接(get操作)时,只是查找匹配后端服务器的地址,而对前端没有任何感知。这就会造成一个问题,当用户1访问某站点后,会建立一个TCP连接,在后端web服务器没有关闭该连接之前,用户2同样访问该站点时,则不用建立TCP连接即可直接访问,也就是说nginx与后端的keepalive连接对前端来说是共享的,这就造成一个性能问题,当几万个用户同时访问同一站点时,这几万个用户与nginx建立了几万条TCP连接,而nginx与后端服务器确有可能只有一条连接,这一条连接需要服务前端的几万个用户,这就大大的影响了系统的性能!
可以在释放对端连接时添加前端IP地址(获取其他标识信息)来标识前端,在获取连接遍历连接cache池时增加前端IP地址查找匹配,这样方能携带前端标识,避免多个前端共用一个后端连接从而影响性能的问题。
4. 参考文档
http://bollaxu.iteye.com/blog/900424
http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
###############################################################
http://cnbeta.blog.51cto.com/1234897/1032903
我的要求:
外网访问xxx.homeserver.com时,我要求代理自动将请求转向内网IP为192.168.1.20(我的Home Server的IP)的服务器。
访问xxx.homeserver.com,可以是http的请求,也可以是https,但服务器是要求加密的,所以需要同时监听80和443两个端口,并都指向192.168.1.20这个IP
其他请求,如mail.DIYPCs.com,则根据关键字mail转向Exchange服务器(还没开启,不过这是未来的一个部署),此服务器IP肯定不是192.168.1.20
具体操作:
执行以下命令:
sudo vi /etc/nginx/sites-enabled/whs.conf
其中whs.conf为配置文件的文件名(自己随便改,但后缀必须是.conf),以下为我的whs.conf文件内容:
server {
listen 80;
server_name xxx.homeserver.com;
location / {
proxy_pass http://192.168.1.20:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
}
}
server {
listen 443;
server_name xxx.homeserver.com;
ssl on;
ssl_certificate /etc/nginx/ssl/whs.pem;
ssl_certificate_key /etc/nginx/ssl/whs.key;
ssl_session_timeout 5m;
ssl_protocols SSLv3 TLSv1;
ssl_ciphers HIGH:!ADH:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass https://192.168.1.20:443;
proxy_set_header host xxx.homeserver.com;
}
}
以上加亮部分为需要特别注意和修改的地方,其中xxx.homeserver.com为你自己的服务器域名,其他部分还有IP的修改和域名证书存放位置的修改。
修改完成后,保存退出就可以了。其他任何别的文件都不需要做更改(这也是我选择nginx的主要原因,简单!)
上面只是Home Server的配置,我说了,Exchange邮箱服务器还没弄好,还有别的服务器等等,每一个服务器可以有自己的配置,也可以在上面的whs.conf后面添加……格式就跟上面一样,更具体的内容可以另行搜索nginx的使用
经过上述简单配置,我的“反向代理”就完成了。最后一步工作:在路由器上将80和443端口映射指向该反向代理服务器的IP(我的是192.168.1.15)
补充一下:上述过程完成了,需要重启NginX,或服务器。指令为:
/etc/init.d/nginx restart
最后测试一下:
和原先一样,没有任何问题……此反向代理的架设就算完成了。
本文出自 “www.DIYPCs.com” 博客,请务必保留此出处http://cnbeta.blog.51cto.com/1234897/1032903
===========================================
http://www.sudone.com/nginx/nginx_gzip_always.html
一般来说,gzip压缩是否启用,除了服务器支持外,客户端也要支持。当客户端发送Accept-Encoding:gzip这个request header,服务器即认为其能接受gzip压缩,就响应一个Content-Encoding:gzip,并发送压缩内容;假如客户端没有发送 Accept-Encoding,那么服务器就把源代码老老实实地打印出去。
但这里就有个怪点子,能不能让客户端无论有没有发送Accept-Encoding,服务器都会发送压缩内容呢?
这有几个好处:
1、进一步节省带宽。
2、防止水平一般的爬虫抓页面偷数据。
经测试,此种做法并不会影响普通用户,因为他们都是用先进的浏览器上网的;另外,也不会影响主流的搜索引擎,收录仍然会正常。
要做到这点,需要有两个nginx,但也有办法配置两个虚拟主机就可以,不用启动两个nginx主进程。为了方便,我就以前后来区分它们。
前端nginx:
gzip压缩不在前端nginx进行,前端主要是用来强制修改request header,即写上:
1. proxy_set_header Accept-Encoding 'gzip';
这样,后台的nginx无论如何都将接收到Accept- Encoding:gzip,而不管客户端有没有发。
完整的测试样本:
1. upstream www.backend.sudone.com{
2. server 127.0.0.1:80;
3. }
4. server {
5. server_name www.sudone.com;
6. listen 80;
7.
8. location / {
9. proxy_pass http://www.backend.sudone.com;
10. include proxy.conf;
11. proxy_set_header Accept-Encoding 'gzip';
12. }
13. }
注意proxy_pass到的upstream是www.backend.sudone.com,这是在一台机器上配置两个虚拟主机所必需的,否则不就是个死循环吗?如果还是想用www.sudone.com,可以将前端的listen改成外网ip,后端就用127.0.0.1。
另外一个要注意proxy.conf里最好没有写过proxy_set_header Accept-Encoding,我的proxy.conf默认有将Accept-Encoding设为空的,这会造成配置重复。但 proxy_set_header不会冲突,可以按配置先后顺序生效,我一时忘了是前生效还是后生效,动手测一下便知。
后端nginx:
后端nginx才是负责压缩的,这里要注意gzip的版本,因为nginx是用http1.0方式作代理,因此gzip的版本就不能是默认的1.1版,改成1.0。
1. server {
2. server_name www.backend.sudone.com;
3. listen 80;
4.
5. location / {
6. root /html/;
7. gzip on;
8. gzip_http_version 1.0;
9. }
10. }
这里就简单点了,gzip的其他参数我就不贴上来,想必大家都有现成的配置,留意下version就好。
配好后,测试一下:
1. curl -I http://www.sudone.com
发现返回了Content-Encoding:gzip
不加-I参数呢?
1. curl http://www.sudone.com
打印出一堆乱码,把SecureCRT的字符都给弄坏了。
--------------------------------------------------------------------------------------------------
http://blog.csdn.net/sdomain/article/details/6046016
因公司将所有音乐的请求都封掉了,没法在线听音乐了,
找代理又麻烦,想了一下,为什么不自己做呢,
于是就找了台服务器,上面正好装了nginx,增加配增如下:
server{
resolver 8.8.8.8; #代理服务器要用的DNS服务器
listen 8080; #代理所用的端口,用一个没用过的即可
access_log logs/proxy/access.log; #记录访问日志
error_log logs/proxy/error.log; #出错的日志
location / {
proxy_pass http://$http_host$request_uri; #这句不要变实现代理
}
}
为防止被别人用,我限制了IP,每次使用时,把我当前IP允许
也可以增加服务器认证的方式,
即在location内增加:
auth_basic 'proxyServer';
auth_basic_user_file /var/nginx/auth/passwd;
然后将用户写到passwd内。这样就需要每次使用时,输入用户名及密码!
-===========^-^ 开心听歌中...
==============================================
http://baohua.me/system-architecture/nginx-configure-reverse-proxy/
有时候在访问国外被墙的网站的时候是非常郁闷的,比如某搜索引擎,虽然可以访问香港的站点,但会出现间歇性的中断,在条件允许的情况下是可以将国外的VPS做反向代理,方便自己搜索。
Nginx做反向代理是非常不错的选择,虽然Apache也有对应的功能,但现在最常用的还是使用Nginx来做反向代理。
自己弄的VPS是采用LNMP自动安装的程序环境,并没有对其做过多的配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
server
{
listen
80
;
server_name
yotodo
.
net
www
.
yotodo
.
net
;
index
index
.
html
index
.
htm
index
.
php
default
.
html
default
.
htm
default
.
php
;
root
/
home
/
www
.
yotodo
.
net
;
include
none
.
conf
;
# location ~ .*\.(php|php5)?$
# {
# fastcgi_pass unix:/tmp/php-cgi.sock;
# fastcgi_index index.php;
# include fcgi.conf;
# }
#
# location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
# {
# expires 30d;
# }
#
# location ~ .*\.(js|css)?$
# {
# expires 12h;
# }
location
/
{
proxy_pass
http
:
//www.google.com/;
# proxy_pass https://twitter.com/sessions;
proxy_redirect
off
;
proxy_set_header
X
-
Real
-
IP
$
remote_addr
;
proxy_set_header
X
-
Forwarded
-
For
$
proxy_add_x_forwarded_for
;
}
log_format
www
.
yotodo
.
net
&
#39;$remote_addr - $remote_user [$time_local] $request '
&
#39;$status $body_bytes_sent $http_referer '
&
#39;$http_user_agent $http_x_forwarded_for';
access_log
/
home
/
wwwlogs
/
www
.
yotodo
.
net
.
log
www
.
yotodo
.
net
;
}
|
以上是我直接建立的Nginx虚拟机,并配置好对应的代理服务。由于默认建立的虚拟机配置存在一些静态文件缓存配置,我们使用反向代理的话,就没有必要用到缓存,直接把对应的行注释掉即可。
实际新增的代码如下:
1
2
3
4
5
6
7
|
location
/
{
proxy_pass
http
:
//www.google.com/;
# proxy_pass https://twitter.com/sessions;
proxy_redirect
off
;
proxy_set_header
X
-
Real
-
IP
$
remote_addr
;
proxy_set_header
X
-
Forwarded
-
For
$
proxy_add_x_forwarded_for
;
}
|
–ENDOF–
===============================================
http://feilong.me/2011/06/nginx-reverse-proxy
server { # 监听80端口,通常可以省略。 listen 80; # 要配置的站点域名,即用来曲线访问GAE上的原站点的域名。 server_name www.tornadoweb.cn; # Ben老大要求tornadoweb.cn有说明是镜像站点以区别于原站tornadoweb.org, # 因此我走巧将头上的LOGO采用修改过的本地文件,在原logo上加了mirror的声明 location /static/tornado.png { # LOGO文件tornado.png所在文件夹目录 root /mnt/ebs/sites/tornadoweb; } location / { # 配置反向代理到 www.tornadoweb.org,对于没有绑定域名的GAE运用来说,会是http://python-tornado.appspot.com这样的。 proxy_pass http://www.tornadoweb.org; # 关闭重定向跳转 proxy_redirect off; # 转发IP等HTTP头信息 proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } # 下面几个是将www.tornadoweb.com.cn也跳转到www.tornadoweb.cn,并且将域名根目录tornadoweb.cn这样的访问也跳转到www下, # 这个反之亦然(现在多是这种,即www跳转到域名根目录)。 server { server_name tornadoweb.com.cn; rewrite ^(.*)$ http://www.tornadoweb.cn$1 permanent; } server { server_name www.tornadoweb.com.cn; rewrite ^(.*)$ http://www.tornadoweb.cn$1 permanent; } server { server_name tornadoweb.cn; rewrite ^(.*)$ http://www.tornadoweb.cn$1 permanent; }
配置好后reload/restart一下nginx就妥了,本例的结果即是访问tornadoweb.cn将看到tornadoweb.org一样网站,除了采用了本地文件的那个LOGO不同外。
转载请注明出处:http://feilong.me/2011/06/nginx-reverse-proxy
======================================
http://blog.csdn.net/dingyujie/article/details/8531857
网上搜过upstream相关分析的同学可能都已经有了大概的了解了,而且很多大牛分析的也很棒,我这里为什么还要多说几句呢?一来是总结自己的一些理解,二来是对前辈分析的一些补充,希望能带给大家更多启发。
先给出阿里技术牛们的总结和分析,供大家参考:
http://tengine.taobao.org/book/chapter_05.html
http://www.pagefault.info/?p=251
看nginx官方wiki给出的配置例子(稍作修改):
- upstream backend {
- server 211.20.10.11 weight=5;
- server 198.172.10.9:8080 weight=10;
- }
- server {
- location / {
- proxy_pass http://backend;
- }
- }
(事先声明下,upstream中的后端连接选择算法不是这里讨论的重点,我们仅仅会涉及round robbin算法,像ip_hash或者更新的机制如keepalive不在讨论之列)
对于上述的upstream配置,在系统初始化阶段会创建一个结构来保存它,注意是该结构是初始化阶段分配的内存,它的生命周期不跟随一个具体的request。这里必须强调的一点就是,在分析nginx代码时,我们要记清楚什么结构的生存期是跟随request的,什么结构不是。这个对你从总体上把握系统的框架很有用。参考函数ngx_http_upstream_init_round_robin。
其实upstream模块的运作,主要的驱动是xxx_pass,比如上面的proxy_pass。除此之外,还有memcache_pass,fastcgi_pass等,upstream相关配置结构的建立,并不一定是非要配置upstream这个指令才会去做,有时proxy_pass直接就是跟一个可解析的域名,这个时候upstream的初始化工作也会正常运转起来,这里我们不讨论这个情况。
那么一个具体的request如何跟这个upstream配置系统相关联?一般是通过peer.init函数指针,在这里round robbin使用的是ngx_http_upstream_init_round_robin_peer,当然ip_hash有自己的处理函数。从配置上看,凡是请求到location /的request,他们都关联到同一个upstream配置,这点没有问题,我们设计也会这么做,但是既然大家公用一个结构,那么需不需要互斥呢?如果大家都要修改其中的某个成员。。。实际上,nginx中一个请求不会中多个进程中同时处理,一个request生老病死都在一个worker内。其次,由于在单个进程内,nginx非阻塞对各个请求请求进行异步处理,具体来讲,在一个请求处理在发生EAGAIN(一些系统调用,或者主动放弃情况)之前,是不会转去处理另一个请求的,所以也就不存在互斥的问题。好像扯远了。。。我们继续讨论。
通过示例配置,可以看出upstream结构管理了两个后端服务器,那么在使用时,通过选择优先级最高(round robbin)的一个后端,来发起连接。算法如何去选择,大家可以直接去读源码。这里我们关心的是,连接异常的后续处理,毕竟异常和及其细节处理的好坏直接决定一个服务是否稳定。
情况1:当前选择的后端机器,连接异常(超时或者出错)
因为这里的socket都是非阻塞的,所以我们直接connect往往不会立即成功,一般会返回经典的EINPROGRESS错误码,这种情况下,你需要做的就是加一个定时器,并且如果之前没有向epoll添加读事件的话,还要加一个read event。为什么这么做是必要的(事实上nginx就是这么做的)?首先,如果连接成功或者出错,我们注册的读事件会被epoll上报,我们在事件处理函数中,会做处理;如果连接超时了(如对端物理断网了),那么我们的超时定时器就会触发。这样,无论失败还是成功,这个连接我们都可以合理的处理掉,面对异常你不能置之不理。
那么超时或者出错之后,upstream需要做什么呢?nginx使用ngx_http_upstream_next来处理,超时和出错分别用状态NGX_HTTP_UPSTREAM_FT_TIMEOUT和NGX_HTTP_UPSTREAM_FT_ERROR表示。在ngx_http_upstream_next中,如果确实是某个后端连接出了问题,一般nginx会再次调用ngx_http_upstream_connect,来找其他可用的后端尝试连接。
这里值得的一提的是,一个后端连接出问题,会被nginx记小本本的。当所以的后端都出问题的时候(获取后端连接返回NGX_BUSY),nginx只好找备胎来用了,如果有的话。如果没有备胎,或者备胎也出问题了,那没辙, 502给你好了。
情况2:后端连接成功,但是nginx收到的响应头是异常的
这里的异常我们简单的认为成不是我们期望的状态码,比如我们想得到“200 OK”,但是却得到了 “404 Not found”或者其他。那nginx后续的处理怎样?我认为可以有两种处理,第一就是直接将这个响应发给客户端。这样做的问题就是,nginx无法感知后端机器上内容,也就是说当前连接的后端上面根本没有客户端想要的文件,但是其他机器上可能有。面对的后端是一个内容上面的集群,在内容上这样处理显然不合适。所以比较合适的处理是,让nginx再次去后端尝试。。。但是这样也不是办法,如果你有很多后端机器,只有一个有客户端想要的内容,那么运气差的话。。。
先不讨论集群方面的优化,我们看nginx是怎么处理的。
当upstream收到异常响应时,不得不提一个指令“proxy_next_upstream”,看官方介绍吧。
关于用法,描述的很明确。再看一些相应的代码:
- static ngx_conf_bitmask_t ngx_http_proxy_next_upstream_masks[] = {
- { ngx_string("error"), NGX_HTTP_UPSTREAM_FT_ERROR },
- { ngx_string("timeout"), NGX_HTTP_UPSTREAM_FT_TIMEOUT },
- { ngx_string("invalid_header"), NGX_HTTP_UPSTREAM_FT_INVALID_HEADER },
- { ngx_string("http_500"), NGX_HTTP_UPSTREAM_FT_HTTP_500 },
- { ngx_string("http_502"), NGX_HTTP_UPSTREAM_FT_HTTP_502 },
- { ngx_string("http_503"), NGX_HTTP_UPSTREAM_FT_HTTP_503 },
- { ngx_string("http_504"), NGX_HTTP_UPSTREAM_FT_HTTP_504 },
- { ngx_string("http_404"), NGX_HTTP_UPSTREAM_FT_HTTP_404 },
- { ngx_string("updating"), NGX_HTTP_UPSTREAM_FT_UPDATING },
- { ngx_string("off"), NGX_HTTP_UPSTREAM_FT_OFF },
- { ngx_null_string, 0 }
- };
上述的信息告诉我们proxy模块中支持的特殊响应码处理有哪些,当然fastcgi和memcache等这些用到upstream的模块都有类似的数组。对于proxy_next_upstream指令配置中的给出的http_xxx,意思是让nginx在后端返回这些响应时,去尝试其他的后端(函数ngx_http_upstream_test_next)。当然nginx原生支持的就这些,如果大家有自己的需求,可以尝试去改这块代码。不过个人的建议是,改nginx的核心代码要慎重。
================================================
http://blog.taorenjia.com/?p=154
一、nginx框架结构说明
....... 定义运行nginx worker进程的用户/组,worker进程数,可以打开的最大文件描述符数.Log/pid path
events{ 事件模块,控制nginx处理连接的方式
……………….
}
http { nginx处理http的核心模块
……………….
upstrearm 负载组名{ 负载均衡模块,可以有多个均衡组.
……………………. nginx作反向代理方式有:轮询,权重,ip_hash,url_hash,fair
}
Server{ 定义虚拟主机模块,可以有多个
………………
}
Server{
………………….
Location ~ /purge(/.*) { location匹配访问目录/文件的控制模块
………..
}
Location ~ .*\.(ico\mp3\avi\jpg\jpeg\swf\.......) {
………..
}
Location ~ .*\.(thtml|thtm)$ {
………..
}
Location ./ {
………..
}
Location ~ .*\.(php|jsp|cgi)$ {
………..
}
Location /unlimit/ {
………..
}
………………….
}
Server{
……………….
Location ~ .*\.(ico\mp3\avi\jpg\jpeg\swf\.......) {
………..
}
Location ./ {
………..
}
…………………….
}
}
二、nginx作反向代理,利用upstream负载均衡模块说明参考:
user nginx nginx;
worker_processes 10; 工作进程数一般是cpu核数*2
error_log /var/log/nginx/error.log crit;
pid /var/run/nginx.pid;
worker_rlimit_nofile 51200; 文件描述符,每个进程所能打开的最大文件数量/socket.
events
{
use epoll; 使用的I/O网络模型
worker_connections 51200;
}
http
{
includ mime.types;
default_type application/octet-stream;
#charset uft-8; 如果有多个字符类型,最好在程序中定义
log_format main '$remote_addr - $remote_user [$time_local] $request '
'"$status" $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"' ' up_ip: $upstream_addr '
'up_type $upstream_http_content_type' ' gzip "$gzip_ratio"';
注:添加header头信息x_forwarded_for获取客户端真实IP。 $remote_addr.....nginx内部变量
access_log /var/log/nginx/access.log main;
server_names_hash_bucket_size 128;
client_header_buffer_size 32k;
large_client_header_buffer 4 32k;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on; 开启gzip压缩
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
limit_zone myzone $binary_remote_addr 10m; nginx限制并发各速率。 将myzone区域分配10M,1M可含约16000个会话状态。
client_max_body_size 50m; 允许客户端请求的最大单个字节数
client_body_buffer_size 256k; 缓冲区代理缓冲客户端请求的最 大字节数
proxy_connect_timeout 190; 后端server超时时间
proxy_send_timeout 290; 后端server回传时间
proxy_read_timeout 290; 后端server响应时间
proxy_buffer_size 4k; 缓存区保护的用户头信息大小,供nginx进行规则处理.
proxy_buffers 4 32k; 几个buffer最大用多大空间
proxy_busy_buffers_size 64k; 忙时可申请更大的空间
proxy_temp_file_write_size 64k; proxy缓存临时文件的大小
proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; 如遇500,503,404状态码转发到后端另一服务器。
proxy_max_temp_file_size 128m;
upstream mysvr { 定义负载均衡池,可以有多个,这里后端是squid-server组
server 192.168.1.10;
server 192.168.1.11;
hash $http_host$request_uri;
} 根据url来hash,后端为缓存server的如squid,可提高命中率
upstream websvr{ 定义后端websrv的均衡池,这里后端是web-server组
server 192.168.1.20;
server 192.168.1.21;
} 采用轮询
注:nginx负载均衡方式:轮询,权重,ip_hash,url_hash,fair.具体解释下面有说明。
upstream message_server{ 定义后端message_server的均衡池
server 192.168.1.30;
server 192.168.1.31;
}
server { 定义虚拟主机,第一个虚拟主机,反向代理mysrv负载组
listen 80;
server_name www.domain.com
location ~.*\.(ico|mp3|avi|wma|wmv|asf|mpg|mov|mid|js|css|jpg|ping|gif|swf|flv)$
{
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwaded_for;自定义变量获取原客户端真实IP。
proxy_pass http://mysrv; 反向代理到mysrv负载池。
root html; 网页文件根目录,后端server 默认路径。
}
location ~.*\.(php|jsp|cgi)?${
动态可直接proxy_pass到后端web池.http://websrv;
}
location / 匹配以/开头的url
{
proxy_next_upstream http_502 http_504 error timeout invalid_header;
如果后端服务器返回502,504,执行超时错误,自动请求转发到后端另一台server,故障切换。
proxy_pass http://mysrv;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
limit_conn myzone 100; 限制单个IP的100个并发访问数.
access_log /var/log/nginx/access.log main;
}
server{ 第二个虚拟主机,反向代理websrv负载组
listen 80;
server_name www1.domain.com;
访问http://www1.domain.com/message/...地址,反向代理到message_server池
location /message/
{
proxy_pass http://message_server;
proxy_set_header Host $host;
}
访问除了/message/之外的http://www1.domain.com/.....地址,反向代理到websrv
location /
{
proxy_pass http://websrv;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
access_log /var/log/nginx/message.access;
}
}
Nginx作为反向代理,利用upstream模块支持多组负载均衡,方式:
a) 轮询:每个请求按时间顺序依次分配到不同后端server.如何后端server down掉,能自动排除。
b) 权重: 根据weight值越大的后端server优先。指定轮询机率。
c) Ip_hash:如客户端IP第一次访问后端A-server,输入用户名密码后第二次可能分发到后端B-server,ip_hash利用hash_key值按访问ip的hash结果分配即进行哈希算法使其同一IP第二次会话保持访问至同一后端server. 此种方式能解决session问题,每个访客固定访问一个后端server.
d) 利用hash算法此种方式无法进行负载均衡。
e) url_hash:按访问url的hash结果分配,使每个url定向到同一后端server.如果后端是缓存server比较长有效,,如squid,可以增加squid命中率。
f) Fair:按后端服务器响应时间来分配请,响应时间短的优先分配。
Location 对url进行匹配,可以进行重定向或新的代理,负载均衡。
三、nginx缓存模块proxy_cache说明参考(这里省去全局配置部分,只列http标签部分):
http{
....
proxy_temp_path /data/proxy_temp_path;
proxy_cache_path /data/proxy_cache_path levels=1:2 keys_zone=cache_one:200m inactive=1d
max_size=30g;
proxy_temp_path,proxy_cache_path路径须在同一分区下。
设置web缓存区名cache_one,内存缓存空间大小200M,自动清除超过1天没有访问的缓存数据,硬盘缓存空间大小30G。
.....
upstream my_srv{
server ....
server ....
}
server{
........
location /
{
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http:mysrv;
location ~.*\.(gif|jpg|jpeg|png|bmp|swf|js|css)$
{
proxy_cache cache_one; 使用缓存区
proxy_cache_valid 200 304 12h;
proxy_cache_valid 301 302 1m;
proxy_cache_valid any 1m;
对不同状态码缓存时间不一样。
proxy_cache_key $host$uri$is_args$args;
nginx根据key值md5哈希存储缓存,一般根据域名,URL,参数组成key.
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http:mysrv;
反向代理,访问后端.
location ~/purge(/.*) 清除缓存http://.../purge/..
{
allow 127.0.0.1; 允许执行清除的IP段
allow 192.168.1.0/16;
deny all;
proxy_cache_purge cache-one $host$1$is_args$args;
}
access_log /var/log/nginx/purge.access main;
}
}
四、nginx作web服务器,相关网上nginx+php(利用fastcgi技术)实现高性能WEB服务器的博文已经很多,不作总结了....
==========================================
http://www.pagefault.info/?p=259
原创文章,转载请注明: 转载自pagefault
本文链接地址: nginx中upstream的设计和实现(二)
这次主要来看upstream的几个相关的hook函数。
首先要知道,对于upstream,同时有两个连接,一个时client和nginx,一个是nginx和upstream,这个时候就会有两个回调,然后上篇blog中,我们能看到在upstream中,会改变read_event_handler和write_event_handler,不过这里有三个条件,分别是
1 没有使用cache,
2 不忽略client的提前终止
3 不是post_action
1
2
3
4
5
6
|
//条件赋值
if
(!u->store && !r->post_action && !u->conf->ignore_client_abort) {
//然后给读写handler赋值
r->read_event_handler = ngx_http_upstream_rd_check_broken_connection;
r->write_event_handler = ngx_http_upstream_wr_check_broken_connection;
}
|
然后我们来看这个两个函数,这两个都会调用ngx_http_upstream_check_broken_connection,因此我们就先来详细分析这个函数。
这个函数主要是用来检测client的连接是否完好。因此它使用了MSG_PEEK这个参数,也就是预读,然后通过recv的返回值来判断是否连接已经断开。
这里的代码分为两部分,第一部分是本身连接在进入这个回调函数之前连接都已经有错误了,这个时候如果是水平触发,则删除事件,然后finalize这个upstream(没有cache’的情况下),否则就直接finalize这个upstream。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
c = r->connection;
u = r->upstream;
//如果连接已经出现错误。
if
(c->error) {
//如果是水平触发
if
((ngx_event_flags & NGX_USE_LEVEL_EVENT) && ev->active) {
event = ev->write ? NGX_WRITE_EVENT : NGX_READ_EVENT;
//删除事件
if
(ngx_del_event(ev, event, 0) != NGX_OK) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
}
if
(!u->cacheable) {
//清理upstream request
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_CLIENT_CLOSED_REQUEST);
}
return
;
}
|
紧接着就是第二部分,这部分的工作就是预读取1个字节,然后来判断是否连接已经被client断掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
//读取1个字节
n = recv(c->fd, buf, 1, MSG_PEEK);
err = ngx_socket_errno;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ev->
log
, err,
"http upstream recv(): %d"
, n);
if
(ev->write && (n >= 0 || err == NGX_EAGAIN)) {
return
;
}
//如果水平触发则删除事件
if
((ngx_event_flags & NGX_USE_LEVEL_EVENT) && ev->active) {
event = ev->write ? NGX_WRITE_EVENT : NGX_READ_EVENT;
if
(ngx_del_event(ev, event, 0) != NGX_OK) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
}
//如果还有数据,则直接返回
if
(n > 0) {
return
;
}
if
(n == -1) {
if
(err == NGX_EAGAIN) {
return
;
}
ev->error = 1;
}
else
{
/* n == 0 */
err = 0;
}
//到达这里说明有错误产生了
ev->eof = 1;
//设置错误,可以看到这个值在函数一开始有检测.
c->error = 1;
//如果没有cache,则finalize upstream request
if
(!u->cacheable && u->peer.connection) {
ngx_log_error(NGX_LOG_INFO, ev->
log
, err,
"client closed prematurely connection, "
"so upstream connection is closed too"
);
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_CLIENT_CLOSED_REQUEST);
return
;
}
ngx_log_error(NGX_LOG_INFO, ev->
log
, err,
"client closed prematurely connection"
);
//如果有cache,并且后端的upstream还在处理,则此时继续处理upstream,忽略对端的错误.
if
(u->peer.connection == NULL) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_CLIENT_CLOSED_REQUEST);
}
|
然后我们来看nginx如何连接后端的upstream,在上篇blog的结束的时候,我们看到最终会调用ngx_http_upstream_connect来进入连接upstream的处理,因此我们来详细分析这个函数以及相关的函数。
函数一开始是初始化请求开始事件一些参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
..........................................................
//取得upstream的状态
u->state = ngx_array_push(r->upstream_states);
if
(u->state == NULL) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
ngx_memzero(u->state,
sizeof
(ngx_http_upstream_state_t));
tp = ngx_timeofday();
//初始化时间
u->state->response_sec = tp->sec;
u->state->response_msec = tp->msec;
|
然后是调用ngx_event_connect_peer开始连接后端upstream.并且对返回值进行处理,等下会详细分析ngx_event_connect_peer这个函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//连接后端
rc = ngx_event_connect_peer(&u->peer);
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->
log
, 0,
"http upstream connect: %i"
, rc);
if
(rc == NGX_ERROR) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
//这个是很关键的一个结构peer,后面的blog会详细分析
u->state->peer = u->peer.name;
if
(rc == NGX_BUSY) {
ngx_log_error(NGX_LOG_ERR, r->connection->
log
, 0,
"no live upstreams"
);
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_NOLIVE);
return
;
}
if
(rc == NGX_DECLINED) {
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);
return
;
}
|
当返回值为NGX_OK或者NGX_AGAIN的话,就说明连接成功或者暂时异步的连接还没成功,所以需要挂载upstream端的回调函数.这里要注意就是NGX_AGAIN的情况,因为是异步的connect,所以可能会连接不成功。所以如果返回NGX_AGAIN的话,需要挂载写函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
/* rc == NGX_OK || rc == NGX_AGAIN */
c = u->peer.connection;
c->data = r;
c->write->handler = ngx_http_upstream_handler;
c->read->handler = ngx_http_upstream_handler;
//开始挂载回调函数,一个是读,一个是写。
u->write_event_handler = ngx_http_upstream_send_request_handler;
u->read_event_handler = ngx_http_upstream_process_header;
c->sendfile &= r->connection->sendfile;
u->output.sendfile = c->sendfile;
c->pool = r->pool;
c->
log
= r->connection->
log
;
c->read->
log
= c->
log
;
c->write->
log
= c->
log
;
/* init or reinit the ngx_output_chain() and ngx_chain_writer() contexts */
u->writer.out = NULL;
u->writer.last = &u->writer.out;
u->writer.connection = c;
u->writer.limit = 0;
|
然后时对request_body的一些处理以及如果request_sent已经设置,也就是这个upstream已经发送过一部分数据了,此时需要重新初始化upstream.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
if
(u->request_sent) {
//重新初始化upstream
if
(ngx_http_upstream_reinit(r, u) != NGX_OK) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
}
//如果request_body存在的话,保存request_body
if
(r->request_body
&& r->request_body->buf
&& r->request_body->temp_file
&& r == r->main)
{
/*
* the r->request_body->buf can be reused for one request only,
* the subrequests should allocate their own temporay bufs
*/
u->output.
free
= ngx_alloc_chain_link(r->pool);
if
(u->output.
free
== NULL) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
//保存到output
u->output.
free
->buf = r->request_body->buf;
u->output.
free
->next = NULL;
u->output.allocated = 1;
//重置request_body
r->request_body->buf->pos = r->request_body->buf->start;
r->request_body->buf->last = r->request_body->buf->start;
r->request_body->buf->tag = u->output.tag;
}
|
最后则是先判断rc的返回值,如果是NGX_AGAIN,则说明连接没有返回,则设置定时器,然后返回,否则说明连接成功,这时就需要发送请求到后端。
1
2
3
4
5
6
7
|
if
(rc == NGX_AGAIN) {
//添加定时器
ngx_add_timer(c->write, u->conf->connect_timeout);
return
;
}
ngx_http_upstream_send_request(r, u);
|
紧接着我们来看最后的两个函数,分别是上面的ngx_event_connect_peer和ngx_http_upstream_send_request,我们来一个个看。
先来看ngx_event_connect_peer。它主要是用来连接后端,函数比较长,一部分一部分来看。
下面这部分主要是建立socket,然后设置属性,从连接池取出来connection.这里后面的一部分和我们前面client请求上来之后,我们初始化connect类似.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
//取得我们将要发送的upstream对端
rc = pc->get(pc, pc->data);
if
(rc != NGX_OK) {
return
rc;
}
//新建socket
s = ngx_socket(pc->sockaddr->sa_family, SOCK_STREAM, 0);
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pc->
log
, 0,
"socket %d"
, s);
if
(s == -1) {
ngx_log_error(NGX_LOG_ALERT, pc->
log
, ngx_socket_errno,
ngx_socket_n
" failed"
);
return
NGX_ERROR;
}
//取得连接
c = ngx_get_connection(s, pc->
log
);
if
(c == NULL) {
if
(ngx_close_socket(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, pc->
log
, ngx_socket_errno,
ngx_close_socket_n
"failed"
);
}
return
NGX_ERROR;
}
//设置rcvbuf的大小
if
(pc->rcvbuf) {
if
(setsockopt(s, SOL_SOCKET, SO_RCVBUF,
(
const
void
*) &pc->rcvbuf,
sizeof
(
int
)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->
log
, ngx_socket_errno,
"setsockopt(SO_RCVBUF) failed"
);
goto
failed;
}
}
//设置非阻塞
if
(ngx_nonblocking(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, pc->
log
, ngx_socket_errno,
ngx_nonblocking_n
" failed"
);
goto
failed;
}
if
(pc->local) {
if
(bind(s, pc->local->sockaddr, pc->local->socklen) == -1) {
ngx_log_error(NGX_LOG_CRIT, pc->
log
, ngx_socket_errno,
"bind(%V) failed"
, &pc->local->name);
goto
failed;
}
}
//开始挂载对应的读写函数.
c->recv = ngx_recv;
c->send = ngx_send;
c->recv_chain = ngx_recv_chain;
c->send_chain = ngx_send_chain;
c->sendfile = 1;
c->log_error = pc->log_error;
if
(pc->sockaddr->sa_family != AF_INET) {
c->tcp_nopush = NGX_TCP_NOPUSH_DISABLED;
c->tcp_nodelay = NGX_TCP_NODELAY_DISABLED;
#if (NGX_SOLARIS)
/* Solaris's sendfilev() supports AF_NCA, AF_INET, and AF_INET6 */
c->sendfile = 0;
#endif
}
rev = c->read;
wev = c->write;
rev->
log
= pc->
log
;
wev->
log
= pc->
log
;
pc->connection = c;
c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
#if (NGX_THREADS)
/* TODO: lock event when call completion handler */
rev->lock = pc->lock;
wev->lock = pc->lock;
rev->own_lock = &c->lock;
wev->own_lock = &c->lock;
#endif
if
(ngx_add_conn) {
//添加读写事件
if
(ngx_add_conn(c) == NGX_ERROR) {
goto
failed;
}
}
|
等socket设置完毕,nginx就开始连接后端的upstream,这段代码可以学习一个好的代码是如何处理错误的,
下面这段主要是处理当返回值为-1,并且err不等于NGX_EINPROGRESS的时候,而NGX_EINPROGRESS表示非阻塞的socket,然后connect,然后连接还没有完成,可是提前返回,就回设置这个errno.这个error不算出错,因此需要过滤掉.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
rc = connect(s, pc->sockaddr, pc->socklen);
if
(rc == -1) {
err = ngx_socket_errno;
//判断错误号
if
(err != NGX_EINPROGRESS
#if (NGX_WIN32)
/* Winsock returns WSAEWOULDBLOCK (NGX_EAGAIN) */
&& err != NGX_EAGAIN
#endif
)
{
if
(err == NGX_ECONNREFUSED
#if (NGX_LINUX)
/*
* Linux returns EAGAIN instead of ECONNREFUSED
* for unix sockets if listen queue is full
*/
|| err == NGX_EAGAIN
#endif
|| err == NGX_ECONNRESET
|| err == NGX_ENETDOWN
|| err == NGX_ENETUNREACH
|| err == NGX_EHOSTDOWN
|| err == NGX_EHOSTUNREACH)
{
level = NGX_LOG_ERR;
}
else
{
level = NGX_LOG_CRIT;
}
ngx_log_error(level, c->
log
, err,
"connect() to %V failed"
,
pc->name);
//返回declined
return
NGX_DECLINED;
}
}
|
然后就是下面的部门就是处理连接成功和错误号为NGX_EINPROGRESS的情况,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
//如果当前的事件模型支持add_conn,则事件在开始已经加好了,因此如果rc==-1则直接返回
if
(ngx_add_conn) {
if
(rc == -1) {
/* NGX_EINPROGRESS */
return
NGX_AGAIN;
}
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, pc->
log
, 0,
"connected"
);
wev->ready = 1;
return
NGX_OK;
}
..............................................
//添加可读事件
if
(ngx_add_event(rev, NGX_READ_EVENT, event) != NGX_OK) {
goto
failed;
}
if
(rc == -1) {
/* NGX_EINPROGRESS */
//如果错误号是 EINPROGRES 添加可写事件
if
(ngx_add_event(wev, NGX_WRITE_EVENT, event) != NGX_OK) {
goto
failed;
}
return
NGX_AGAIN;
}
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, pc->
log
, 0,
"connected"
);
wev->ready = 1;
return
NGX_OK;
..............................................
|
最后我们来看下ngx_http_upstream_send_request的实现,这个函数是用来发送数据到后端的upstream,然后这里有一个需要注意的地方,那就是在linux下当非阻塞的connect,然后没有连接完成,如果挂载写事件,此时如果写事件上报上来,并不代表连接成功,此时还需要调用getsockopt来判断SO_ERROR,如果没有错误才能保证连接成功。
SOL_SOCKET
to determine whether connect() completed successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual error codes listed
here, explaining the reason for the failure).
这里我看了下内核的代码,就是如果连接失败,比如对端不可达,内核会设置sock->sk_soft_err,而在tcp_poll中只会检测sk_err , 对应的SO_ERROR会检测这两个错误。在内核里面的注释是这样子的
* @sk_err: last error
* @sk_err_soft: errors that don’t cause failure but are the cause of a
* persistent failure not just ‘timed out’
这个按照我的理解,内核里面的sk_err 表示4层的错误,而sk_err_soft下层的错误.
在nginx中是在ngx_http_upstream_test_connect中对连接是否断开进行判断的(调用getsockopt).
然后发送数据则是调用ngx_output_chain,不过这里我们知道在ngx_output_chain中会依次调用filter链,可是upstream明显不需要调用filter链,那么nginx是怎么做的呢,是这样子的,在upstream的初始化的时候,已经讲u->output.output_filter改成ngx_chain_writer了:
1
|
u->output.output_filter = ngx_chain_writer;
|
最后就是一些对错误的处理,我们来看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
static
void
ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
ngx_int_t rc;
ngx_connection_t *c;
c = u->peer.connection;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->
log
, 0,
"http upstream send request"
);
//如果test connect失败,则说明连接失败,于是跳到下一个upstream,然后返回
if
(!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) {
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);
return
;
}
c->
log
->action =
"sending request to upstream"
;
//发送数据,这里的u->output.output_filter已经被修改过了
rc = ngx_output_chain(&u->output, u->request_sent ? NULL : u->request_bufs);
u->request_sent = 1;
........................................................
//和request的处理类似,如果again,则说明数据没有发送完毕,此时挂载写事件.
if
(rc == NGX_AGAIN) {
ngx_add_timer(c->write, u->conf->send_timeout);
if
(ngx_handle_write_event(c->write, u->conf->send_lowat) != NGX_OK) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
return
;
}
/* rc == NGX_OK */
//设置tcp_cork,远离和前面的keepalive部分的处理类似
if
(c->tcp_nopush == NGX_TCP_NOPUSH_SET) {
if
(ngx_tcp_push(c->fd) == NGX_ERROR) {
ngx_log_error(NGX_LOG_CRIT, c->
log
, ngx_socket_errno,
ngx_tcp_push_n
" failed"
);
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return
;
}
c->tcp_nopush = NGX_TCP_NOPUSH_UNSET;
}
ngx_add_timer(c->read, u->conf->read_timeout);
#if 1
//如果读也可以了,则开始解析头
if
(c->read->ready) {
/* post aio operation */
/*
* TODO comment
* although we can post aio operation just in the end
* of ngx_http_upstream_connect() CHECK IT !!!
* it's better to do here because we postpone header buffer allocation
*/
ngx_http_upstream_process_header(r, u);
return
;
}
#endif
...........................................
}
|
在下一篇blog里面,我会详细的分析nginx对后端来的数据如何解析以及如何发送数据到client.
http://gcoder.blogbus.com/logs/212170707.html
nginx 是优秀的反向代理服务器,这里主要讲它的健康检查和负载均衡机制,以及这种机制带来的问题。所谓健康检查,就是当后端出现问题(具体什么叫出现问题,依赖于具体实现,各个实现定义不一样),不再往这个后端分发请求,并且做后续的检查,直到这个后端恢复正常。所谓负载均衡,就是选择后端的方式,如何(根据后端的能力)将请求均衡的分发到后端。此外,当请求某个后端失败时,要将该请求分发到其它后端(redispatch)。这里以ngx_http_upstream_round_robin(简称RR)做为负载均衡模块,以ngx_http_proxy_module(检查proxy)作为后端代理模块。
nginx 的健康检查和负载均衡是密切相关的,它没有独立的健康检查模块,而是使用业务请求作为健康检查,这省去了独立健康检查线程,这是好处。坏处是,当业务复杂时,可能出现误判,例如后端响应超时,这是可能是后端宕机,也可能是某个业务请求自身出现问题,跟后端无关。如果后端宕机,nginx还要在将它标记为不可用之后,仍不时的将业务请求分发给它,以检查后端是否恢复。
nginx 完成客户端请求Header部分的解析,upstream 调用RR模块的peer.get 选择具体的后端。当请求结束,upstream 调用RR模块的peer.free,向RR反馈后端的健康情况。当upstream和后端通信时,出现错误会调用 ngx_http_upstream_next,
void ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t ft_type); 第三个参数指明错误类型,共有如下错误类型
#define NGX_HTTP_UPSTREAM_FT_ERROR 0x00000002
#define NGX_HTTP_UPSTREAM_FT_TIMEOUT 0x00000004
#define NGX_HTTP_UPSTREAM_FT_INVALID_HEADER 0x00000008
#define NGX_HTTP_UPSTREAM_FT_HTTP_500 0x00000010
#define NGX_HTTP_UPSTREAM_FT_HTTP_502 0x00000020
#define NGX_HTTP_UPSTREAM_FT_HTTP_503 0x00000040
#define NGX_HTTP_UPSTREAM_FT_HTTP_504 0x00000080
#define NGX_HTTP_UPSTREAM_FT_HTTP_404 0x00000100
#define NGX_HTTP_UPSTREAM_FT_UPDATING 0x00000200
#define NGX_HTTP_UPSTREAM_FT_BUSY_LOCK 0x00000400
#define NGX_HTTP_UPSTREAM_FT_MAX_WAITING 0x00000800
#define NGX_HTTP_UPSTREAM_FT_NOLIVE 0x40000000
ngx_http_upstream_next,只要错误类型不是 NGX_HTTP_UPSTREAM_FT_HTTP_404,都认为后端有问题(NGX_PEER_FAILED)
if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404) {
state = NGX_PEER_NEXT;
} else {
state = NGX_PEER_FAILED;
}
ngx_http_upstream_next 调用RR的peer.free,RR根据state判断刚才接受请求的后端是否健康。
if (ft_type != NGX_HTTP_UPSTREAM_FT_NOLIVE) {
u->peer.free(&u->peer, u->peer.data, state);
}
ngx_http_upstream_next 如果超过最大重试次数(默认为后端的个数,每试过一个,就减1),或者proxy设置不允许redispatch,则向客户端返回响应status。
if (u->peer.tries == 0 || !(u->conf->next_upstream & ft_type)) {
ngx_http_upstream_finalize_request(r, u, status);
}
proxy 模块的 proxy_next_upstream 配置,在何种情况下将请求redispatch到下一个后端。
刚刚谈到,只要错误类型不是 NGX_HTTP_UPSTREAM_FT_HTTP_404,都认为后端有问题。这里的错误类型包括,连接后端失败,连接,读写后端超时,后端返回了500,502,504等。这个策略是有待商榷的,尤其是读写后端超时也判断为后端不可用。因为某个业务请求,可能因为自身的原因而导致读写超时。注意,在proxy_next_upstream 中指定timeout,http_504 是不同的,前者表示upstream连接,读写后端超时,后者表示后端返回的http code 是504。
实际上健康检查不是必须的,因为redispatch的存在保证了,就算有后端宕机,客户端仍将收到正确的响应。那么我们考虑关掉健康检查。通过upstream 的server配置的max_fails 参数
RR 的peer.get,如果max_fails 为0,则该后端总是可用的(就算它真有问题)。
if (peer->max_fails == 0
|| peer->fails < peer->max_fails)
{
break;
}
因为redispatch的次数,取决于后端的个数,所以后端的个数稍微多一点是有好处的。
下面是一些佐证分析的测试。
upstream test {
server 127.0.0.1:8060 max_fails=0;
server 127.0.0.1:8070 max_fails=0;
server 127.0.0.1:8080 max_fails=0;
server 127.0.0.1:8090 max_fails=0;
}
只有8060,8070是存活的,8080,8090处于不可用状态,这里max_fails=0,关闭了健康检查。
proxy_read_timeout 2;
读超时设为2S。
proxy_next_upstream error timeout;
默认当 error 和 timeout发生时,redispatch。
测试请求的sleep参数指定后端的sleep时间,code参数指定后端返回的http code。根据time和sleep时间的对比,判断重试了几个后端。
time curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
real 0m4.014s
sleep=3,读超时,重试了2个后端。
修改配置 proxy_next_upstream error;
time curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
real 0m2.018s
读超时,不再redispatch,重试了1个后端。
修改配置 proxy_next_upstream error http_504;
time curl "http://127.0.0.1:8099/index.php?sleep=1" -vv
real 0m1.022s
这个是正常请求。
time curl "http://127.0.0.1:8099/index.php?sleep=1&code=504" -vv
real 0m2.023s
让后端返回504,此时nginx会做redispatch,重试了2个后端
但是nginx返回给客户端的是502,不是504,因为所有的后端都返回504,nginx认为后端不可用,返回502.
测试健康检查,关掉redispatch。proxy_next_upstream off;
curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
返回了两次502,两次504。存活的后端返回504,有问题的返回502。
修改 max_fails server 127.0.0.1:8060 max_fails=1; 对8060开启健康检查。
curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
第一轮4次请求,返回两次502,两次504
8080和8090有问题,返回502,8060和8070响应超时,返回504,因为8060开启了健康检查,并且返回了504,所以被标记为不可用。
第二轮4次请求,返回三次502,一次504。8070没有开启健康检查,所以仍然返回504。
根据测试分析,业务请求(sleep 3s,或者 输出 http 504)可以让nginx误以为后端宕了,而这时后端活得好好的。在私有云平台,这个通常不是问题,把超时设大点,不返回5XX错误,可以避免这个问题。但是在公有云平台,这是致命的,因为业务可以编程输出5XX错误。有两种方法应对,一种是关闭健康检查,一种是修改nginx的代码,仅对 NGX_HTTP_UPSTREAM_FT_ERROR 判定为后端有问题。