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.us

评论(0) 引用(0) 浏览(4)

python 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也可以

评论(0) 引用(0) 浏览(4)

502 问题一例

作者:antidote 发布于:2012-4-26 17:55 Thursday

今天帮微盘查一问题,说是有502,我测试的是出来500。。。

也有请求日志。。错误日志里面并没有报错,开始跟踪进程,看到有读mc,读数据库的操作。应该是正常的请求,看到strace里面读到的mysql数据好像很多。。。

 

看他源码,一行一行开始看,开始调试。。。

最后看到了这个。。。

 

function fetchAll()
{
         
$arr = array();
              
               error_reporting(E_ALL);
                ini_set("display_errors",True);
$count=0;
while($this->record = mysql_fetch_array($this->queryID,$this->fetchMode))
{
       $arr[] = $this->record;
                    
}
 ;
mysql_free_result($this->queryID);
return $arr;

 

代码语法肯定是没问题。。

 

后来加入

               error_reporting(E_ALL);
                ini_set("display_errors",True);
错误明显了。。

尼玛。。。

<b>SAE_Fatal_error</b>:  Allowed memory size of 67108864 bytes exhausted (tried to allocate 72 bytes) in <b>2/inc/Sys/Mysql.php</b> on line <b>256</b><br />
* Closing connection #0 


只会写代码的工人伤不起....





评论(0) 引用(0) 浏览(10)

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)

评论(0) 引用(0) 浏览(9)

apache 禁止path中特殊字符的请求

作者:antidote 发布于:2012-4-10 13:39 Tuesday

apache 禁止path中特殊字符的请求,比较恶心的需求.

 

 <LocationMatch  ".*/\.svn/?.*">

    Order Deny,Allow

    Deny from all

</LocationMatch>

评论(0) 引用(0) 浏览(8)

linux 2.6 监控进程的IO情况

作者:antidote 发布于:2012-4-6 13:27 Friday

 

1 , 打开文件系统的监控选项

 sysctl vm.block_dump=1

 使用dmesg -c 查看监控日志

#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
    117 oracle(28754)
    117 kjournald(452)
     50 sqlplus(28751)
     16 oracle(18907)
     15 oracle(28779)
      6 oracle(28745)
      4 oracle(28747)
      2 tnslsnr(28747)
      2 cut(28799)
      1 sort(28800)

转自:http://blog.sina.com.cn/s/blog_5d3a81a70100xzxp.html

评论(0) 引用(0) 浏览(10)

apache 内存池

作者:antidote 发布于:2012-3-27 16:39 Tuesday

通常情况下,内存的分配和释放通常都是mallloc和free显式进行的。这样做显得单调无味,同时也可能充满各种令人厌恶的问题。对同一块内存的多次释放通常会导致页面错误,而一直不释放又导致内存泄露,并且使得服务器性能大大下降。而apache基于池概念进行内存管理。

Apache中的内存分配的基本结构都是资源池,包括线程池,套接字池等等。内存池通常是一块很大的内存空 间,一次性被分配成功,然后需要的时候直接去池中取,而不需要重新分配,这样避免的频繁的malloc操作,而且另一方面,即时内存的使用者忘记释放内存 或者根本就不想分配,那么这些内存也不会丢失,它们仍然保存在内存池中,当内存池被销毁的时候这些内存将自动的被销毁。

Apache中的内存池并不是仅仅一个内存池,相反而是存在多个内存池,这些内存池之间形成层次结构。如果Apache中仅仅存在一个内存池的 话,潜在的问题是所有的内存分配都来自这个池,而且最要命的这些内存必须在整个Apache关闭时候才被释放,这一点显然不是那么合情合理,为此 Apache中根据处理阶段的周期长短又引出了子内存池的概念,与之对应的是父内存池以及根内存池的概念,它们的唯一区别就是存在的周期的不同而已。。比如 对于HTTP连接而言,包括两种内存池:连接内存池和请求内存池。由于一个连接可能包含多个请求,因此连接的生存周期总是比一个请求的周期长,为此连接处 理中所需要的内存则从连接内存池中分配,而请求则从请求内存池中分配。而一个请求处理完毕后请求内存池被释放,一个连接处理后连接内存池被释放。根内存池 在整个Apache运行期间都存在。


Apache中使用了内存结点的概念来 描述每次分配的内存块。其结构类型则描述为apr_memnode_t,该结构定义在文件
Apr_allocator.h中,其定义如下:
/** basic memory node structure */
struct apr_memnode_t {
apr_memnode_t *next; /**< next memnode */
apr_memnode_t **ref; /**< reference to self */
apr_uint32_t index; /**< size */
apr_uint32_t free_index; /**< how much free */
char *first_avail; /**< pointer to first free
memory */
char *endp; /**< pointer to end of free
memory */
};
该结点类型是整个Apache内存管理的基石,在后面的部分我们将其称之为“内存结点类型”或者简称为“内存结点”或者“结点”。在该结构中,不同的结点之间通过next指针形成结点链表;另外当在结点内部的时候为了方便引用结点本身,成员变量中还引入了ref,该变量主要用来记录当前结点的首地址,即使身在结点内部,也可以通过ref指针得到该结点并对该结点进行操作。
从上面的结构中可以看出事实上在apr_memnode_t结构内部没有任何的“空闲空间”来容纳实际分配的内存,事实上,它从来不单独存在, 总是依附于具体的分配的内存单元。通常情况下,一旦分配了实际的空间之后,Apache总是将该结构置于整个单元的最顶部,如图所示。

\

在上图中,我们可能调用malloc函数分配了16K大小的空间,为了能够将该空间用Apache的结点进行记录,我们将 apr_memnode_t置于整个空间的头部,此时剩下的可用空间大小应该为16K-sizeof(apr_memnode_t),同时结构中还提
供了 first_avail和end_p指针分别指向这块可用空间的首部和尾部。当这块可用空间被不断利用时,first_avail和end_p指针也不断 随之移动,不过(end_p-first_avail)之间则永远是当前的空闲空间。上图的右边部分演示了这种布局。
通常情况下,其分配语句大致如下:
apr_memnode_t* node;
node=(apr_memnode_t*)malloc(size);
node->next = NULL;
node->index = index;
node->first_avail = (char *)node + APR_MEMNODE_T_SIZE;
node->endp = (char *)node + size;
Apache中对内存的分配大小并不是随意的,随意的分配可能会造成更多的内存碎片。为此Apache采取的则是“规则块”分配原则。Apache所支持的分配的最小空间是8K,如果分配的空间达不到8K的大小,则按照8K去分配;如果需要的空间超过8K,则将分配的空间往上调整为4K的倍数。为此我们在程序中很多地方会看到下面的宏APR_ALIGN,其定义如下:
/* APR_ALIGN() is only to be used to align on a power of 2 boundary
*/
#define APR_ALIGN(size, boundary) \
(((size) + ((boundary) - 1)) & ~((boundary) - 1))
该宏所做的无非就是计算出最接近size的boundary的整数倍的整数。通常情况下size大小为整数即可,而boundary则必须保证 为2的倍数

评论(0) 引用(0) 浏览(15)

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)


评论(0) 引用(0) 浏览(9)

[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                      */
};

评论(0) 引用(0) 浏览(9)

linux异步IO浅析(装)

作者:antidote 发布于:2012-3-1 22:24 Thursday

 

知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成。使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情)。
假此机会,也顺便研究了一下linux下的异步IO的实现。

linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本)、一套是由linux内核实现,并由libaio来封装调用接口(以下称之为linux版本)。


glibc版本

接口
glibc版本主要包含如下接口:
int aio_read(struct aiocb *aiocbp);  /* 提交一个异步读 */
int aio_write(struct aiocb *aiocbp); /* 提交一个异步写 */
int aio_cancel(int fildes, struct aiocb *aiocbp); /* 取消一个异步请求(或基于一个fd的所有异步请求,aiocbp==NULL) */
int aio_error(const struct aiocb *aiocbp);        /* 查看一个异步请求的状态(进行中EINPROGRESS?还是已经结束或出错?) */
ssize_t aio_return(struct aiocb *aiocbp);         /* 查看一个异步请求的返回值(跟同步读写定义的一样) */
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout); /* 阻塞等待请求完成 */

其中,struct aiocb主要包含以下字段:
int               aio_fildes;        /* 要被读写的fd */
void *            aio_buf;           /* 读写操作对应的内存buffer */
__off64_t         aio_offset;        /* 读写操作对应的文件偏移 */
size_t            aio_nbytes;        /* 需要读写的字节长度 */
int               aio_reqprio;       /* 请求的优先级 */
struct sigevent   aio_sigevent;      /* 异步事件,定义异步操作完成时的通知信号或回调函数 */

 

实现
glibc的aio实现是比较通俗易懂的:
1、异步请求被提交到request_queue中;
2、request_queue实际上是一个表结构,"行"是fd、"列"是具体的请求。也就是说,同一个fd的请求会被组织在一起;
3、异步请求有优先级概念,属于同一个fd的请求会按优先级排序,并且最终被按优先级顺序处理;
4、随着异步请求的提交,一些异步处理线程被动态创建。这些线程要做的事情就是从request_queue中取出请求,然后处理之;
5、为避免异步处理线程之间的竞争,同一个fd所对应的请求只由一个线程来处理;
6、异步处理线程同步地处理每一个请求,处理完成后在对应的aiocb中填充结果,然后触发可能的信号通知或回调函数(回调函数是需要创建新线程来调用的);
7、异步处理线程在完成某个fd的所有请求后,进入闲置状态;
8、异步处理线程在闲置状态时,如果request_queue中有新的fd加入,则重新投入工作,去处理这个新fd的请求(新fd和它上一次处理的fd可以不是同一个);
9、异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建;

看起来,换作是我们,要在用户态实现一个异步IO,似乎大概也会设计成类似的样子……


linux版本

接口
下面再来看看linux版本的异步IO。它主要包含如下系统调用接口:
int io_setup(int maxevents, io_context_t *ctxp);  /* 创建一个异步IO上下文(io_context_t是一个句柄) */
int io_destroy(io_context_t ctx);  /* 销毁一个异步IO上下文(如果有正在进行的异步IO,取消并等待它们完成) */
long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp);  /* 提交异步IO请求 */
long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result);  /* 取消一个异步IO请求 */
long io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout)  /* 等待并获取异步IO请求的事件(也就是异步请求的处理结果) */

其中,struct iocb主要包含以下字段:
__u16     aio_lio_opcode;     /* 请求类型(如:IOCB_CMD_PREAD=读、IOCB_CMD_PWRITE=写、等) */
__u32     aio_fildes;         /* 要被操作的fd */
__u64     aio_buf;            /* 读写操作对应的内存buffer */
__u64     aio_nbytes;         /* 需要读写的字节长度 */
__s64     aio_offset;         /* 读写操作对应的文件偏移 */
__u64     aio_data;           /* 请求可携带的私有数据(在io_getevents时能够从io_event结果中取得) */
__u32     aio_flags;          /* 可选IOCB_FLAG_RESFD标记,表示异步请求处理完成时使用eventfd进行通知(百度一下) */
__u32     aio_resfd;          /* 有IOCB_FLAG_RESFD标记时,接收通知的eventfd */

其中,struct io_event主要包含以下字段:
__u64     data;               /* 对应iocb的aio_data的值 */
__u64     obj;                /* 指向对应iocb的指针 */
__s64     res;                /* 对应IO请求的结果(>=0: 相当于对应的同步调用的返回值;<0: -errno) */

 

实现
io_context_t句柄在内核中对应一个struct kioctx结构,用来给一组异步IO请求提供一个上下文。其主要包含以下字段:
struct mm_struct*     mm;             /* 调用者进程对应的内存管理结构(代表了调用者的虚拟地址空间) */
unsigned long         user_id;        /* 上下文ID,也就是io_context_t句柄的值(等于ring_info.mmap_base) */
struct hlist_node     list;           /* 属于同一地址空间的所有kioctx结构通过这个list串连起来,链表头是mm->ioctx_list */
wait_queue_head_t     wait;           /* 等待队列(io_getevents系统调用可能需要等待,调用者就在该等待队列上睡眠) */
int                   reqs_active;    /* 进行中的请求数目 */
struct list_head      active_reqs;    /* 进行中的请求队列 */
unsigned              max_reqs;       /* 最大请求数(对应io_setup调用的int maxevents参数) */
struct list_head      run_list;       /* 需要aio线程处理的请求列表(某些情况下,IO请求可能交给aio线程来提交) */
struct delayed_work   wq;             /* 延迟任务队列(当需要aio线程处理请求时,将wq挂入aio线程对应的请求队列) */
struct aio_ring_info  ring_info;      /* 存放请求结果io_event结构的ring buffer */

其中,这个aio_ring_info结构比较值得一提,它是用于存放请求结果io_event结构的ring buffer。它主要包含了如下字段:
unsigned long   mmap_base;       /* ring buffer的地始地址 */
unsigned long   mmap_size;       /* ring buffer分配空间的大小 */
struct page**   ring_pages;      /* ring buffer对应的page数组 */
long            nr_pages;        /* 分配空间对应的页面数目(nr_pages * PAGE_SIZE = mmap_size) */
unsigned        nr, tail;        /* 包含io_event的数目及存取游标 */

这个数据结构看起来有些奇怪,直接弄一个io_event数组不就完事了么?为什么要维护mmap_base、mmap_size、ring_pages、nr_pages这么复杂的一组信息,而又把io_event结构隐藏起来呢?
这里的奇妙之处就在于,io_event结构的buffer是在用户态地址空间上分配的。注意,我们在内核里面看到了诸多数据结构都是在内核地址空间上分配的,因为这些结构都是内核专有的,没必要给用户程序看到,更不能让用户程序去修改。而这里的io_event却是有意让用户程序看到,而且用户就算修改了也不会对内核的正确性造成影响。于是这里使用了这样一个有些取巧的办法,由内核在用户态地址空间上分配buffer。(如果换一个保守点的做法,内核态可以维护io_event的buffer,然后io_getevents的时候,将对应的io_event复制一份到用户空间。)
按照这样的思路,io_setup时,内核会通过mmap在对应的用户空间分配一段内存,mmap_base、mmap_size就是这个内存映射对应的位置和大小。然后,光有映射还不行,还必须立马分配物理内存,ring_pages、nr_pages就是分配好的物理页面。(因为这些内存是要被内核直接访问的,内核会将异步IO的结果写入其中。如果物理页面延迟分配,那么内核访问这些内存的时候会发生缺页异常。而处理内核态的缺页异常又很麻烦,所以还不如直接分配物理内存的好。其二,内核在访问这个buffer里的信息时,也并不是通过mmap_base这个虚拟地址去直接访问的。既然是异步,那么结果写回的时候可能是在另一个上下文上面,虚拟地址空间都不同。为了避免进行虚拟地址空间的切换,内核干脆直接通过kmap将ring_pages映射到高端内存上去访问好了。)

然后,在mmap_base指向的用户空间的地址上,会存放着一个struct aio_ring结构,用来管理这个ring buffer。其主要包含了如下字段:
unsigned         id;                /* 等于aio_ring_info中的user_id */
unsigned         nr;                /* 等于aio_ring_info中的nr */
unsigned         head,tail;         /* io_events数组的游标 */
unsigned         magic,compat_features,incompat_features;
unsigned         header_length;     /* aio_ring结构的大小 */
struct io_event  io_events[0];      /* io_event的buffer */
终于,我们期待的io_event数组出现了。

看到这里,如果前面的内容你已经理解清楚了,你一定会有个疑问:既然整个aio_ring结构及其中的io_event缓冲都是放在用户空间的,内核还提供io_getevents系统调用干什么?用户程序不是直接就可以取用io_event,并且修改游标了么(内核作为生产者,修改aio_ring->tail;用户作为消费者,修改aio_ring->head)?我想,aio_ring之所以要放在用户空间,其原本用意应该就是这样的。
那么,用户空间如何知道aio_ring结构的地址(aio_ring_info->mmap_base)呢?其实kioctx结构中的user_id,也就是io_setup返回给用户的io_context_t,就等于aio_ring_info->mmap_base。
然后,aio_ring结构中还有诸如magic、compat_features、incompat_features这样的字段,用户空间可以读这些magic,以确定数据结构没有被异常篡改。如果一切可控,那么就自己动手、丰衣足食;否则就还是走io_getevents系统调用。而io_getevents系统调用通过aio_ring_info->ring_pages得到aio_ring结构,再将相应的io_event拷贝到用户空间。
下面贴一段libaio中的io_getevents的代码(前面提到过,linux版本的异步IO是由用户态的libaio来封装的):
int io_getevents_0_4(io_context_t ctx, long min_nr, long nr, struct io_event * events, struct timespec * timeout){
    struct aio_ring *ring;
    ring = (struct aio_ring*)ctx;
    if (ring==NULL || ring->magic != AIO_RING_MAGIC)
        goto do_syscall;
    if (timeout!=NULL && timeout->tv_sec == 0 && timeout->tv_nsec == 0) {
        if (ring->head == ring->tail)
            return 0;
    }
do_syscall:
    return __io_getevents_0_4(ctx, min_nr, nr, events, timeout);
}
其中确实用到了用户空间上的aio_ring结构的信息,不过尺度还是不够大。

以上就是异步IO的context的结构。那么,为什么linux版本的异步IO需要“上下文”这么个概念,而glibc版本则不需要呢?
在glibc版本中,异步处理线程是glibc在调用者进程中动态创建的线程,它和调用者必定是在同一个虚拟地址空间中的。这里已经隐含了“同一上下文”这么个关系。
而对于内核来说,要面对的是任意的进程,任意的虚拟地址空间。当处理一个异步请求时,内核需要在调用者对应的地址空间中存取数据,必须知道这个虚拟地址空间是什么。不过当然,如果设计上要想把“上下文”这个概念隐藏了也是肯定可以的(比如让每个mm隐含一个异步IO上下文)。具体如何选择,只是设计上的问题。

struct iocb在内核中又对应到struct kiocb结构,主要包含以下字段:
struct kioctx*       ki_ctx;           /* 请求对应的kioctx(上下文结构) */
struct list_head     ki_run_list;      /* 需要aio线程处理的请求,通过该字段链入ki_ctx->run_list */
struct list_head     ki_list;          /* 链入ki_ctx->active_reqs */
struct file*         ki_filp;          /* 对应的文件指针 */
void __user*         ki_obj.user;      /* 指向用户态的iocb结构 */
__u64                ki_user_data;     /* 等于iocb->aio_data */
loff_t               ki_pos;           /* 等于iocb->aio_offset */
unsigned short       ki_opcode;        /* 等于iocb->aio_lio_opcode */
size_t               ki_nbytes;        /* 等于iocb->aio_nbytes */
char __user *        ki_buf;           /* 等于iocb->aio_buf */
size_t               ki_left;          /* 该请求剩余字节数(初值等于iocb->aio_nbytes) */
struct eventfd_ctx*  ki_eventfd;       /* 由iocb->aio_resfd对应的eventfd对象 */
ssize_t (*ki_retry)(struct kiocb *);   /*由ki_opcode选择的请求提交函数*/

调用io_submit后,对应于用户传递的每一个iocb结构,会在内核态生成一个与之对应的kiocb结构,并且在对应kioctx结构的ring_info中预留一个io_events的空间。之后,请求的处理结果就被写到这个io_event中。
然后,对应的异步读写(或其他)请求就被提交到了虚拟文件系统,实际上就是调用了file->f_op->aio_read或file->f_op->aio_write(或其他)。也就是,在经历磁盘高速缓存层、通用块层之后,请求被提交到IO调度层,等待被处理。这个跟普通的文件读写请求是类似的。
在《linux文件读写浅析》中可以看到,对于非direct-io的读请求来说,如果page cache不命中,那么IO请求会被提交到底层。之后,do_generic_file_read会通过lock_page操作,等待数据最终读完。这一点跟异步IO是背道而驰的,因为异步就意味着请求提交后不能等待,必须马上返回。而对于非direct-io的写请求,写操作一般仅仅是将数据更新作用到page cache上,并不需要真正的写磁盘。page cache写回磁盘本身是一个异步的过程。可见,对于非direct-io的文件读写,使用linux版本的异步IO接口完全没有意义(就跟使用同步接口效果一样)。
为什么会有这样的设计呢?因为非direct-io的文件读写是只跟page cache打交道的。而page cache是内存,跟内存打交道又不会存在阻塞,那么也就没有什么异步的概念了。至于读写磁盘时发生的阻塞,那是page cache跟磁盘打交道时发生的事情,跟应用程序又没有直接关系。
然而,对于direct-io来说,异步则是有意义的。因为direct-io是应用程序的buffer跟磁盘的直接交互(不使用page cache)。

这里,在使用direct-io的情况下,file->f_op->aio_{read,write}提交完IO请求就直接返回了,然后io_submit系统调用返回。(见后面的执行流程。)
通过linux内核异步触发的IO调度(如:被时钟中断触发、被其他的IO请求触发、等),已经提交的IO请求被调度,由对应的设备驱动程序提交给具体的设备。对于磁盘,一般来说,驱动程序会发起一次DMA。然后又经过若干时间,读写请求被磁盘处理完成,CPU将收到表示DMA完成的中断信号,设备驱动程序注册的处理函数将在中断上下文中被调用。这个处理函数会调用end_request函数来结束这次请求。这个流程跟《linux文件读写浅析》中所说的非direct-io读操作的情况是一样的。
不同的是,对于同步非direct-io,end_request将通过清除page结构的PG_locked标记来唤醒被阻塞的读操作流程,异步IO和同步IO效果一样。而对于direct-io,除了唤醒被阻塞的读操作流程(同步IO)或io_getevents流程(异步IO)之外,还需要将IO请求的处理结果填回对应的io_event中。
最后,等到调用者调用io_getevents的时候,就能获取到请求对应的结果(io_event)。而如果调用io_getevents的时候结果还没出来,流程也会被阻塞,并且会在direct-io的end_request过程中得到唤醒。

linux版本的异步IO也有aio线程(每CPU一个),但是跟glibc版本中的异步处理线程不同,这里的aio线程是用来处理请求重试的。某些情况下,file->f_op->aio_{read,write}可能会返回-EIOCBRETRY,表示需要重试(只有一些特殊的IO设备会这样)。而调用者既然使用的是异步IO接口,肯定不希望里面会有等待/重试的逻辑。所以,如果遇到-EIOCBRETRY,内核就在当前CPU对应的aio线程添加一个任务,让aio线程来完成请求的重新提交。而调用流程可以直接返回,不需要阻塞。
请求在aio线程中提交和在调用者进程中提交相比,有一个最大的不同,就是aio线程使用的地址空间可能跟调用者线程不一样。需要利用kioctx->mm切换到正确的地址空间,然后才能发请求。(参见《浅尝异步IO》中的讨论。)
 
内核处理流程
最后,整理一下direct-io异步读操作的处理流程:
io_submit。对于提交的iocbpp数组中的每一个iocb(异步请求),调用io_submit_one来提交它们;
io_submit_one。为请求分配一个kiocb结构,并且在对应的kioctx的ring_info中为它预留一个对应的io_event。然后调用aio_rw_vect_retry来提交这个读请求;
aio_rw_vect_retry。调用file->f_op->aio_read。这个函数通常是由generic_file_aio_read或者其封装来实现的;
generic_file_aio_read。对于非direct-io,会调用do_generic_file_read来处理请求(见《linux文件读写浅析》)。而对于direct-io,则是调用mapping->a_ops->direct_IO。这个函数通常就是blkdev_direct_IO;
blkdev_direct_IO。调用filemap_write_and_wait_range将相应位置可能存在的page cache废弃掉或刷回磁盘(避免产生不一致),然后调用direct_io_worker来处理请求;
direct_io_worker。一次读可能包含多个读操作(对应于类readv系统调用),对于其中的每一个,调用do_direct_IO;
do_direct_IO。调用submit_page_section;
submit_page_section。调用dio_new_bio分配对应的bio结构,然后调用dio_bio_submit来提交bio;
dio_bio_submit。调用submit_bio提交请求。后面的流程就跟非direct-io是一样的了,然后等到请求完成,驱动程序将调用 bio->bi_end_io来结束这次请求。对于direct-io下的异步IO,bio->bi_end_io等于dio_bio_end_aio;
dio_bio_end_aio。调用wake_up_process唤醒被阻塞的进程(异步IO下,主要是io_getevents的调用者)。然后调用aio_complete;
aio_complete。将处理结果写回到对应的io_event中;


比较

从上面的流程可以看出,linux版本的异步IO实际上只是利用了CPU和IO设备可以异步工作的特性(IO请求提交的过程主要还是在调用者线程上同步完成的,请求提交后由于CPU与IO设备可以并行工作,所以调用流程可以返回,调用者可以继续做其他事情)。相比同步IO,并不会占用额外的CPU资源。
而glibc版本的异步IO则是利用了线程与线程之间可以异步工作的特性,使用了新的线程来完成IO请求,这种做法会额外占用CPU资源(对线程的创建、销毁、调度都存在CPU开销,并且调用者线程和异步处理线程之间还存在线程间通信的开销)。不过,IO请求提交的过程都由异步处理线程来完成了(而linux版本是调用者来完成的请求提交),调用者线程可以更快地响应其他事情。如果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

评论(0) 引用(0) 浏览(30)

Powered by emlog