nginx 健康检查和负载均衡机制分析(转)
作者:antidote 发布于:2012-5-15 22:54 Tuesday
转自: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 判定为后端有问题。 收藏到:Del.icio.uspython traceback
作者:antidote 发布于:2012-5-2 18:48 Wednesday
今天在多线程里面调式,恶心死我了... 错误信息不全 就一行。。坑爹
try:
dosthing
except Exception,e:
logging.info("except")
import StringIO
fp = StringIO.StringIO() #创建内存文件对象
traceback.print_exc(file=fp)
message = fp.getvalue()
logging.info(message) // print message也可以
502 问题一例
作者:antidote 发布于:2012-4-26 17:55 Thursday
今天帮微盘查一问题,说是有502,我测试的是出来500。。。
也有请求日志。。错误日志里面并没有报错,开始跟踪进程,看到有读mc,读数据库的操作。应该是正常的请求,看到strace里面读到的mysql数据好像很多。。。
看他源码,一行一行开始看,开始调试。。。
最后看到了这个。。。
代码语法肯定是没问题。。
后来加入
python 加载指定文件
作者:antidote 发布于:2012-4-24 17:52 Tuesday
以前经常是sys.append("path")
再import,怪恶心的。。
import imp
m=imp.load_source("m","/root/syntax_check.py")
m.main()
dir(m)
apache 禁止path中特殊字符的请求
作者:antidote 发布于:2012-4-10 13:39 Tuesday
apache 禁止path中特殊字符的请求,比较恶心的需求.
<LocationMatch ".*/\.svn/?.*">
Order Deny,Allow
Deny from all
</LocationMatch>
linux 2.6 监控进程的IO情况
作者:antidote 发布于:2012-4-6 13:27 Friday
1 , 打开文件系统的监控选项
2
#dmesg -c
oracle(18604): WRITE block 5513688 on cciss/c0d0p5
oracle(18604): WRITE block 5513696 on cciss/c0d0p5
oracle(18604): WRITE block 5513704 on cciss/c0d0p5
oracle(18604): WRITE block 5513712 on cciss/c0d0p5
oracle(18604): WRITE block 5513720 on cciss/c0d0p5
oracle(18604): WRITE block 5513728 on cciss/c0d0p5
oracle(18604): WRITE block 5513736 on cciss/c0d0p5
oracle(18604): WRITE block 5513744 on cciss/c0d0p5
oracle(18604): WRITE block 5513752 on cciss/c0d0p5
oracle(18604): WRITE block 5513760 on cciss/c0d0p5
oracle(18604): WRITE block 5513768 on cciss/c0d0p5
oracle(18604): WRITE block 5513776 on cciss/c0d0p5
oracle(18604): WRITE block 5513784 on cciss/c0d0p5
oracle(18604): WRITE block 5513792 on cciss/c0d0p5
oracle(18604): WRITE block 5513928 on cciss/c0d0p5
oracle(18604): WRITE block 5513936 on cciss/c0d0p5
dmesg(26407): dirtied inode 2855455 (locale-archive) on cciss/c0d0p2
说明:oracle(18604): WRITE block 5513928 on cciss/c0d0p5
日志显示18604这个进程正把5513928 这个block写入磁盘cciss/c0d0p5
3 统计当前最消耗IO的进程
#dmesg -c | cut -d: -f1 | sort | uniq -c | sort -rn| head
apache 内存池
作者:antidote 发布于:2012-3-27 16:39 Tuesday

7层完整性校验
作者:antidote 发布于:2012-3-21 14:06 Wednesday
TimYang:貌似碰到一个TCP/IP CRC出错的问题,IP包的CRC并不是用CRC32算法(CRC32比较消耗CPU资源),而是用一种弱的checksum算法,此算法可以检测到大部分单个bit的error,但是在海量数据下错误的包还是有漏网的可能 http://t.cn/zOqCgbN
为了防止这种漏网的可能,可以自己做个七层校验
import traceback,time,logging
import socket, errno ,zlib, struct
logging.basicConfig(
level = logging.DEBUG,
format = '%(message)s',
filename = 'codefs.log',
)
class Client:
def __init__(self, addr):
self.wbuf = ''
self.readEvent = None
self.writeEvent = None
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setblocking(1)
self.addr = addr
try:
print "connection starting:"
self.sock.connect(addr)
except Exception,e:
traceback.print_exc()
def set(self, accesskey,type, key, value):
crc_num = zlib.crc32(value)
print crc_num
crc_num = zlib.crc32(value)+1
print crc_num
crc = struct.pack("I", crc_num)
self.accesskey = accesskey;
self.type = type
self.key = key
self.value = value + crc
self.len = len(value) + 4
if self.type == "32":
msg="set %(ak)s_%(key)s %(type)s 0 %(len)d\r\n%(value)s\r\n" % {"ak":self.accesskey, "key":self.key, "type":self.type, "len": self.len, "value":self.value}
else:
msg="set %(ak)s_%(key)s %(type)s 0 %(len)d\r\n%(value)s\r\n" % {"ak":self.accesskey, "key":self.key, "type":self.type, "len": len(value), "value":value}
print msg
print "send......"
self.sock.send(msg)
return_value=self.sock.recv(10)
print return_value
return return_value
if __name__ == "__main__":
import os,sys
if len(sys.argv) != 5:
print "eg:python self.py akey type[0/32] key value"
else:
ak=sys.argv[1]
type=sys.argv[2]
key=sys.argv[3]
value=sys.argvp[4]
addr=("xxxxx",xxxx)
client=Client(addr)
print client.set(ak, type, key, value)
[linux] C语言扩展Apache模块开发入门篇
作者:antidote 发布于:2012-3-19 21:49 Monday
转载请说明该文章来自smallfish博客 http://hi.baidu.com/smallfish7788
chinaunix地址:http://www.chinaunix.net/index.php?uid=386791&url=http://bbs.chinaunix.net/viewthread.php?tid=1255594 前言: 扩展Apache模块开发网上大部分教程都是围绕Perl语言记性,老外的《Writing Apache Modules with Perl and C》可以算是经典之作了,可惜一直都是针对老版本开发,而且主力语言是Perl,C语言部分只是略有介绍。不过相比较而言用Perl来扩展模块功能确实比C语言来的快速以及便捷多了,也简单容易。我自己也在工作里应用了一部分,主要是在防盗链上面写了两个简单都模块,可以参考我写的另外两篇文章:apache+mod_perl防盗链以及apache+mod_perl实现url rewrite。说了那么多题外话,回到正题,这里只是用C语言实现一个简单的hello模块,模块功能是查询MySQL自带mysql数据库里都user表。 系统环境: ArchLinux Apache2.2 MySQL 5.0 具体开发步骤: 1.利用Apache自带都apxs建立hello模块: [root#localhost] apxs -g -n hello 这样就会在当前目录下新建一个hello模块的文件目录,可以看到里面有:Makefile mod_hello.c modules.mk这样的文件,具体apxs路径查询下本机apache/bin目录。 2.预览下mod_hello.c,可以看到里面apxs自动帮你生成一堆代码了,我们需要的只是修改里面的代码部分,先简单都介绍下里面的函数说明。 include 部分就是引入了一些必要都头文件 hello_handler 这个就是hello模块都主体部分,所有的显示、处理请求什么的都在这里。 hello_register_hooks hello_module 这俩个是需要导出的函数所必须的,先可以不管他们,按照生成的不动即可。 3.修改hello_handler函数,里面可以看到request_rec *r,r有很多函数和变量,具体要参见文档了。里面的ap_rputs是输出,可以简单的理解为把字符串输出到r。 static int hello_handler(request_rec *r) { if (strcmp(r->handler, "hello")) { // 判断apache配置文件里handler是否等于hello,不是就跳过 return DECLINED; } r->content_type = "text/html"; // 设置content-type if (!r->header_only) ap_rputs("The sample page from mod_hello.c\n", r); // 输出一段文字 return OK;// 返回 200 OK状态 } 增加#include "mysq.h",查询需要用到这个头文件。 具体代码参见本文结尾部分。 4.编译模块 [root#localhost] apxs -c -a -i -I/usr/include/mysql/ -lmysqlclient mod_hello.c 可以看到一堆编译指令,加上-I和-l是编译mysql必须的,编译完会自动在httpd.conf加上 LoadModule hello_module modules/mod_hello.so 5.修改httpd.conf <Location /hello> SetHandler hello </Location> 6.重启apache,访问http://localhost/hello,看是否成功。 ================================================================================= 完整代码: #include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "ap_config.h" /* 头文件,本文用到了ap_rprintf函数 */ #include "apr.h" #include "apr_lib.h" #include "apr_strings.h" #include "apr_want.h" #include "mysql.h" /* 定义mysql数据变量 */ const char *host = "localhost"; const char *user = "root"; const char *pass = "smallfish"; const char *db = "mysql"; /* The sample content handler */ static int hello_handler(request_rec *r) { if (strcmp(r->handler, "hello")) { return DECLINED; } r->content_type = "text/html"; /* 定义mysql变量 */ MYSQL mysql; MYSQL_RES *rs; MYSQL_ROW row; mysql_init(&mysql); /* 初始化 */ if (!mysql_real_connect(&mysql, host, user, pass, db, 0, NULL, 0)) {/* 连接数据库 */ ap_rprintf(r, "<li>Error : %d %s</li>\n", mysql_errno(&mysql), mysql_error(&mysql)); return OK; } char *sql = "select host,user from user order by rand()"; if (mysql_query(&mysql, sql)!=0) { /* 查询 */ ap_rprintf(r, "<li>Error : %d %s</li>\n", mysql_errno(&mysql), mysql_error(&mysql)); return OK; } rs = mysql_store_result(&mysql); /* 获取查询结果 */ while ((row = mysql_fetch_row(rs))) { /* 获取每一行记录 */ ap_rprintf(r, "<li>%s - %s</li>\n", row[0], row[1]); } mysql_free_result(rs); /* 释放结果集 */ mysql_close(&mysql); /* 关闭连接 */ return OK; } static void hello_register_hooks(apr_pool_t *p) { ap_hook_handler(hello_handler, NULL, NULL, APR_HOOK_MIDDLE); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA hello_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ NULL, /* table of config file commands */ hello_register_hooks /* register hooks */ }; |
linux异步IO浅析(装)
作者:antidote 发布于:2012-3-1 22:24 Thursday
知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成。使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情)。 linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本)、一套是由linux内核实现,并由libaio来封装调用接口(以下称之为linux版本)。
接口 其中,struct aiocb主要包含以下字段:
实现 看起来,换作是我们,要在用户态实现一个异步IO,似乎大概也会设计成类似的样子……
接口 其中,struct iocb主要包含以下字段: 其中,struct io_event主要包含以下字段:
实现 其中,这个aio_ring_info结构比较值得一提,它是用于存放请求结果io_event结构的ring buffer。它主要包含了如下字段: 这个数据结构看起来有些奇怪,直接弄一个io_event数组不就完事了么?为什么要维护mmap_base、mmap_size、ring_pages、nr_pages这么复杂的一组信息,而又把io_event结构隐藏起来呢? 然后,在mmap_base指向的用户空间的地址上,会存放着一个struct aio_ring结构,用来管理这个ring buffer。其主要包含了如下字段: 看到这里,如果前面的内容你已经理解清楚了,你一定会有个疑问:既然整个aio_ring结构及其中的io_event缓冲都是放在用户空间的,内核还提供io_getevents系统调用干什么?用户程序不是直接就可以取用io_event,并且修改游标了么(内核作为生产者,修改aio_ring->tail;用户作为消费者,修改aio_ring->head)?我想,aio_ring之所以要放在用户空间,其原本用意应该就是这样的。 以上就是异步IO的context的结构。那么,为什么linux版本的异步IO需要“上下文”这么个概念,而glibc版本则不需要呢? struct iocb在内核中又对应到struct kiocb结构,主要包含以下字段: 调用io_submit后,对应于用户传递的每一个iocb结构,会在内核态生成一个与之对应的kiocb结构,并且在对应kioctx结构的ring_info中预留一个io_events的空间。之后,请求的处理结果就被写到这个io_event中。 这里,在使用direct-io的情况下,file->f_op->aio_{read,write}提交完IO请求就直接返回了,然后io_submit系统调用返回。(见后面的执行流程。) linux版本的异步IO也有aio线程(每CPU一个),但是跟glibc版本中的异步处理线程不同,这里的aio线程是用来处理请求重试的。某些情况下,file->f_op->aio_{read,write}可能会返回-EIOCBRETRY,表示需要重试(只有一些特殊的IO设备会这样)。而调用者既然使用的是异步IO接口,肯定不希望里面会有等待/重试的逻辑。所以,如果遇到-EIOCBRETRY,内核就在当前CPU对应的aio线程添加一个任务,让aio线程来完成请求的重新提交。而调用流程可以直接返回,不需要阻塞。
从上面的流程可以看出,linux版本的异步IO实际上只是利用了CPU和IO设备可以异步工作的特性(IO请求提交的过程主要还是在调用者线程上同步完成的,请求提交后由于CPU与IO设备可以并行工作,所以调用流程可以返回,调用者可以继续做其他事情)。相比同步IO,并不会占用额外的CPU资源。 还有一点,当调用者连续调用异步IO接口,提交多个异步IO请求时。在glibc版本的异步IO中,同一个fd的读写请求由同一个异步处理线程来完成。而异步处理线程又是同步地、一个一个地去处理这些请求。所以,对于底层的IO调度器来说,它一次只能看到一个请求。处理完这个请求,异步处理线程才会提交下一个。而内核实现的异步IO,则是直接将所有请求都提交给了IO调度器,IO调度器能看到所有的请求。请求多了,IO调度器使用的类电梯算法就能发挥更大的功效。请求少了,极端情况下(比如系统中的IO请求都集中在同一个fd上,并且不使用预读),IO调度器总是只能看到一个请求,那么电梯算法将退化成先来先服务算法,可能会极大的增加碰头移动的开销。 最后,glibc版本的异步IO支持非direct-io,可以利用内核提供的page cache来提高效率。而linux版本只支持direct-io,cache的工作就只能靠用户程序来实现了。
转自:http://hi.baidu.com/_kouu/blog/item/e225f67b337841f42f73b341.html |

