nginx源码分析之变量

nginx中的变量在nginx中的使用非常的多,正因为变量的存在,使得nginx在配置上变得非常灵活。

我们知道,在nginx的配置文件中,配合变量,我们可以动态的得到我们想要的值。最常见的使用是,我们在写access_log的格式时,需要用到多很多变量。
而这些变量是如何工作的呢?我们可以输出哪些变量?我们又怎么才能输出自己想要的内容呢?当然,我们可能还想知道,如何在我们的模块里面去使用变量,如何添加变量,获取变量的值,以及设置变量的内容?如何使用,以及需要注意些什么?

问题一大堆,那接下来,就让我们一起去一探nginx源码的秘密。

我要讲的内容
1. 变量的分类
2. 相关结构
3. 模块中操作变量的函数
4. 变量的实现源码及流程

1. 变量的分类
站在使用者的角度来看,我们在配置文件中可以看到:
1) set添加的变量(变量名由用户设定)
2) nginx功能模块中添加的变量,如geo模块(变量名由用户设定)
3) nginx内建的变量(变量名已由nginx设定好,可以看ngx_http_core_variables结构)
4) 有一定规则的变量,如”http_host”等(有相同前缀,表示某一类变量),我们就称为规则变量吧
从这里,也解决我们的问题,在配置access_log时,我们可以配置哪些变量,是否是用户添加的变量,是否是内建变量在ngx_http_core_variables中有,其次,是否是规则变量,另外,如果想输出自己的内容,那只能写模块自己添加一个变量了,或者hack nginx在ngx_http_core_variables中添加一个变量。

从nginx内部实现上来看,变量可分为:
1) hash过的变量
2) 未hash过的变量,变量有设置NGX_HTTP_VAR_NOHASH
2) 未hash过的变量,但有一定规则的变量,如以这些串开头的:”http_”,”sent_http_”,”upstream_http_”,”cookie_”,”arg_”
我们在模块里面可以通过ngx_http_add_variable来添加一个变量,在后面的介绍中我们可以看到。而我们添加的变量,最好不要是以这些规则开头的变量,否则就有可能会覆盖掉这些规则的变量。

从变量获取者来看,可以分为索引变量与未索引的变量。
1)索引变量,我们通过ngx_http_get_variable_index来获得一个索引变量的索引号。然后可以通过ngx_http_get_indexed_variable与ngx_http_get_flushed_variable来获取索引过变量的值。如果要索引某个变量,则只能在配置文件初始化的时候来设置。ngx_http_get_variable_index不会添加一个真正的变量,在配置文件初始化结束时,会检查该变量的合法性。索引过的变量,将会有缓存等特性(缓存在r->variables中)。
2)未索引过的变量,则只能通过ngx_http_get_variable来获取变量的值。

2. 相关结构
接下来,我们就要开始进入源码的世界了,先看看几个关键结构:

// ngx_variable_value_t即变量的结果,变量的值
typedef struct {
    unsigned    len:28;		

    unsigned    valid:1;	// 当前变量是否合法
    unsigned    no_cacheable:1;	// 当前变量是否可以缓存,缓存过的变量将只会调用一次get_handler函数
    unsigned    not_found:1;// 变量是否找到
    unsigned    escape:1;

    u_char     *data;		// 变量的数据
} ngx_variable_value_t;

// 变量本身的信息
struct ngx_http_variable_s {
    ngx_str_t                     name;		// 变量的名称
    ngx_http_set_variable_pt      set_handler;	// 变量的设置函数
    ngx_http_get_variable_pt      get_handler;	// 变量的get函数
    uintptr_t                     data;		// 传给get与set_handler的值
    ngx_uint_t                    flags;	// 变量的标志
    ngx_uint_t                    index;	// 如果有索引,则是变量的索引号
};

// 在ngx_http_core_module的配置文件中保存了所使用的变量信息
typedef struct {
 ngx_hash_t                 variables_hash;		// 变量的hash表
 ngx_array_t                variables;			// 索引变量的数组
 ngx_hash_keys_arrays_t    *variables_keys;		// 变量的hash数组
} ngx_http_core_main_conf_t;

// 变量在每个请求中的值是不一样的,也就是说变量是请求相关的
// 所以在ngx_http_request_s中有一个变量数组,主要用于缓存当前请求的变量结果
// 从而可以避免一个变量的多次计数,计算过一次的变量就不用再计算了
// 但里面保存的一定是索引变量的值,是否缓存,也要由变量的特性来决定
struct ngx_http_request_s {
 ngx_http_variable_value_t        *variables;
}

3. 模块中操作变量的函数
那么,在模块中,我们要如何使用一个变量呢?在前面讲分类的时候,我们也提到过了,这里再总结并细说一下:
首先,如果要添加一个变量,我们需要调用ngx_http_add_variable函数来添加一个变量。添加时需要指明变量的名称就行了。

// name: 即变量的名字
// flags: 如果同一个变量要多次添加,则flags应该设置NGX_HTTP_VAR_CHANGEABLE
// 否则,多次添加将会提示重复
// flags表示可以是:NGX_HTTP_VAR_CHANGEABLE
//                 NGX_HTTP_VAR_NOCACHEABLE
//                 NGX_HTTP_VAR_INDEXED
//                 NGX_HTTP_VAR_NOHASH
ngx_http_variable_t *ngx_http_add_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags);

然后,要获取变量,如果要高效一点,我们可以先将该变量放到索引数组里面,通过ngx_http_get_variable_index来添加一个变量的索引:

// name: 即nginx支持的任意变量名
// 返回该变量的索引
ngx_int_t ngx_http_get_variable_index(ngx_conf_t *cf, ngx_str_t *name);

不过,要注意的是,添加的变量必须是nginx支持的已存在的变量。即如果是hash过的变量,则一定是通过ngx_http_add_variable添加的变量,否则,一定是规则变量,如”http_host”。当然,在解析配置文件的时候,变量不一定是要先通过ngx_http_add_variable然后才能获取索引,这个是不需要有顺序保证的。nginx会将在最后配置文件解析完成后,去验证这些索引变量的合法性,在ngx_http_variables_init_vars函数中可以看到,我们在后面具体再分析。
所以,可以看到,获取索引的操作,一定是要在解析配置文件的过程是进行的, 一旦配置文件解析完成后,索引变量不能再添加。在获取索引号后,我们需要保存该索引号,以便在后面通过索引号来获取变量。
那么,索引变量的获取,可以通过ngx_http_get_indexed_variable与ngx_http_get_flushed_variable来获取,两个函数间的区别,我们后面再介绍:

ngx_http_variable_value_t *ngx_http_get_indexed_variable(ngx_http_request_t *r, ngx_uint_t index);
ngx_http_variable_value_t *ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index);

而如果没有索引过的变量,则只能通过ngx_http_get_variable函数来获取了。

// key 由ngx_hash_strlow来计算
ngx_http_variable_value_t *ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key);

可以看到,key是通过ngx_hash_strlow来计算的,所以变量名是没有大小写区分的。
最后,通过获取变量的函数,我们可以看到,变量是与请求相关的,也就是获取的变量都是与当前请求相关的。

4. 变量的实现源码及流程
那接下来,我们就来看看nginx在源码中的实现吧!
初始化:
首先,在数据结构中,我们知道ngx_http_core_main_conf_t中保存了变量相关的一些信息,我们添加的变量key放在cmcf->variables_keys中,而cmcf->variables保存变量的索引结构,cmcf->variables_hash则保存着变量hash过的结构。
ngx_http_add_variable添加变量的时候,会先放到cmcf->variables_keys中,然后在解析完后,再生成hash结构体。
那么,ngx_http_core_module的preconfiguration阶段,调用ngx_http_variables_add_core_vars初始化变量的数据结构,然后再添加ngx_http_core_variables结构中的变量。所以可以看出,nginx中内建的变量是在这个数组里面的。
然后在解析其它模块的配置文件时,会通过ngx_http_add_variable函数来添加变量:

ngx_http_variable_t *
ngx_http_add_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags)
{
	// 先检查变量是否在已添加
    key = cmcf->variables_keys->keys.elts;
    for (i = 0; i < cmcf->variables_keys->keys.nelts; i++) {
        if (name->len != key[i].key.len
            || ngx_strncasecmp(name->data, key[i].key.data, name->len) != 0)
        {
            continue;
        }

        v = key[i].value;

		// 如果已添加,并且是不可变的变量,则提示变量的重复添加
		// 其它NGX_HTTP_VAR_CHANGEABLE就是为了让变量的重复添加时不出错,都指向同一变量
        if (!(v->flags & NGX_HTTP_VAR_CHANGEABLE)) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "the duplicate \"%V\" variable", name);
            return NULL;
        }
		// 如果变量已添加,并且有NGX_HTTP_VAR_CHANGEABLE表志,则直接返回
        return v;
    }

	// 添加这个变量
    v = ngx_palloc(cf->pool, sizeof(ngx_http_variable_t));

    v->name.len = name->len;

	// 注意,变量名不区分大小写
    ngx_strlow(v->name.data, name->data, name->len);

    rc = ngx_hash_add_key(cmcf->variables_keys, &v->name, v, 0);

    if (rc == NGX_ERROR) {
        return NULL;
    }

    return v;
}

在添加完变量后,我们需要设置变量的get_handler与set_handler。get_handler是当我们在获取变量的时候调用的函数,在该函数中,我们需要设置变量的值。而在set_handler则是用于主动设置变量的值。get_handler与set_handler的区别是:get_handler是在变量使用时获取值,而set_handler则是变量会主动先设置好,在使用的时候就不用再算了。目前,set指令,设置一个变量的值是用的set_handler。
在需要获取变量的模块中,可以通过ngx_http_get_variable_index来得到变量的索引,这个函数工作很简单,就是在ngx_http_core_main_conf_t的variables中添加一个变量,并返回该变量在数组中的索引号。源码就不展示了。
然后,在解析配置文件之后,在ngx_http_block中通过ngx_http_variables_init_vars函数来初始化变量,在ngx_http_variables_init_vars中,会做两个事情,检查索引变量,以及初始化变量的hash表。首先,对索引数组中的每一个元素,会先检查是否在ngx_http_core_main_conf_t的variables_keys中出现,即是否是添加过的,然后再检查是否是有特定规则的变量,如”http_host”,如果都不是,则说明该变量是不存在的,该索引会对应于一个不存在的变量,所以就会提示错误,程序无法启动。然后,如果变量有设置NGX_HTTP_VAR_NOHASH,则会跳过该变量,不进行hash,再对hash过的变量建立hash表。

在请求中:
当一个请求过来时,在ngx_http_init_request函数中,即请求初始化的时候,会建立一个与ngx_http_core_main_conf_t中的变量索引数组variables大小一样的数组。r->variables有两个作用,一是为了缓存变量的值,二是可以在创建子请求时,父请求给子请求传递一些信息。注意,变量的值是与当前请求相关的,所以每个请求里面会不一样。
然后在模块里面ngx_http_get_indexed_variable和ngx_http_get_flushed_variable,这两个函数的代码还是要小讲一下:

ngx_http_variable_value_t *
ngx_http_get_indexed_variable(ngx_http_request_t *r, ngx_uint_t index)
{
    ngx_http_variable_t        *v;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

	// 变量已经获取过了,就不再计算变量的值,直接返回
    if (r->variables[index].not_found || r->variables[index].valid) {
        return &r->variables[index];
    }

	// 如果变量是初次获取,则调用变量的get_handler来得到变量值,并缓存到r->variables中去

    v = cmcf->variables.elts;

    if (v[index].get_handler(r, &r->variables[index], v[index].data)
        == NGX_OK)
    {
        if (v[index].flags & NGX_HTTP_VAR_NOCACHEABLE) {
            r->variables[index].no_cacheable = 1;
        }

        return &r->variables[index];
    }

	// 变量获取失败,设置为不合法,以及未找到
	// 注意我们在调用完本函数后,需要检查函数的返回值以及这两个属性
	r->variables[index].valid = 0;
	r->variables[index].not_found = 1;
    return NULL;
}

ngx_http_variable_value_t *
ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index)
{
    ngx_http_variable_value_t  *v;

    v = &r->variables[index];

    if (v->valid) {
		// 变量已经获取过了,而且是合法的并且可缓存的,则直接返回
        if (!v->no_cacheable) {
            return v;
        }

		// 否则,清除标志,并再次获取变量的值
        v->valid = 0;
        v->not_found = 0;
    }

    return ngx_http_get_indexed_variable(r, index);
}

注意:ngx_http_get_flushed_variable会考虑到变量的cache标志,如果变量是可缓存的,则只有在变量是合法的时才返回变量的值,否则重新获取变量的值。
而ngx_http_get_indexed_variable则不管变量是否可缓存,只要获取过一次了,不管是否成功,则都不会再获取了。
最后,如果是未索引的变量,我们可以通过ngx_http_get_variable函数来得到变量的值。ngx_http_get_variable做的工作:
1)变量是hash过的,而且变量有索引过,则调用ngx_http_get_flushed_variable来得到变量值。
2)变量hash过,未索引过,则调用变量的get_handler来获取变量,注意,此时每次调用变量,都将会调用get_handler来计算变量的值,然后返回该值。注意因为只有索引过的变量的值才会缓存到ngx_http_request_t的variables中去,所以变量的添加方要注意,如果当前变量是可缓存的,要将该变量建立索引,即调用完ngx_http_add_variable后,再调用ngx_http_get_variable_index来将该变量建立索引。
3)特定规则的变量,”http_”开头的会调用ngx_http_variable_unknown_header_out函数,”upstream_http_”开头的会调用ngx_http_upstream_header_variable函数,”cookie_”开头的会调用ngx_http_variable_cookie函数,”arg_”开头的会调用ngx_http_variable_argument函数。
4)变量未找到,设置变量

至此,变量的整个流程差不多就完了,另外还有一个要注意的是,在创建子请求时候的变量。在ngx_http_subrequest函数中,我们可以看到,子请求的variables是直接指向父请求的variables数组的,所以子请求与父请求是共享variables数组的,这样父子请求就可以传递变量的值。但正因为如此,我们在使用父子请求的时候会产生一些问题,如果一个父请求创建多个子请求,他们之间获取同一个变量时,会有很明显的干扰,因为每个请求的环境是不一样的,这样获取的值也是不一样的。

好吧,变量也简单的介绍了一下。

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:nginx源码分析之变量

nginx关于域名解析的源码分析

在nginx中,nginx需要频繁进行域名解析的过程做了自己的优化,使用了自己的一套域名解析过程,并做了缓存处理。我们可以设置DNS解析服务器的地址,即通过resolver指令来设置DNS服务器的地址,由此来启动nginx的域名解析。
本文,我们来看看nginx是如何做的,这里我们只选出重要的代码进行分析,完整代码请参考nginx源代码,本文基于nginx-1.0.6版本进行的分析。
首先,来看看resolver的初始化。
在ngx_http_core_loc_conf_s的声明中,可以看到对reolver:

struct ngx_http_core_loc_conf_s {
    ngx_resolver_t  *resolver;             /* resolver */
}

resolver中保存了与域名解析相关的一些数据,它保存了DNS的本地缓存,通过红黑树的方式来组织数据,以达到快速查找。

typedef struct {
    ngx_event_t              *event;
    // 用于连接dns服务器
    ngx_udp_connection_t     *udp_connection;
    // 保存了本地缓存的DNS数据
    ngx_rbtree_t              name_rbtree;
    ngx_rbtree_node_t         name_sentinel;
} ngx_resolver_t;

在nginx初始化的时候,通过ngx_resolver_create来初始化这一结构体,如果有设置resolver,则在ngx_http_core_resolver中有调用:

static char *
ngx_http_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf = conf;
    // 初始化,第二个参数是我们设置的域名解析服务器的IP地址
    clcf->resolver = ngx_resolver_create(cf, &u.addrs[0]);
    if (clcf->resolver == NULL) {
        return NGX_OK;
    }
    return NGX_CONF_OK;
}

来看看ngx_resolver_create做了些什么:

ngx_resolver_t *
ngx_resolver_create(ngx_conf_t *cf, ngx_addr_t *addr)
{
    ngx_resolver_t        *r;
    ngx_udp_connection_t  *uc;
    r = ngx_calloc(sizeof(ngx_resolver_t), cf->log);
    if (r == NULL) {
        return NULL;
    }
    // 省略了其它数据的初始化过程
    r->event = ngx_calloc(sizeof(ngx_event_t), cf->log);
    if (r->event == NULL) {
        return NULL;
    }
    // 设置事件的句柄
    r->event->handler = ngx_resolver_resend_handler;
    r->event->data = r;
    // 设置dns服务器的地址
    if (addr) {
        uc = ngx_calloc(sizeof(ngx_udp_connection_t), cf->log);
        if (uc == NULL) {
            return NULL;
        }
        r->udp_connection = uc;
        uc->sockaddr = addr->sockaddr;
        uc->socklen = addr->socklen;
        uc->server = addr->name;
    }
    return r;
}

阅读更多…

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:nginx关于域名解析的源码分析

nginx共享内存与锁的实现

本文,我们来分析下nginx中对共享内存和锁的使用。
在nginx中,很多地方使用到了共享内存,在我们的应用中,往往有一些数据需要在多进程间进行共享,了解了共享内存的实现与使用,对我们写程序可以提供很多帮助。在我之前的博文中也有介绍到共享内存的使用与slab分配器,以及红黑树的使用。本文,我将从底层实现上简单介绍下nginx共享内存的实现与锁的利用。
由于nginx不同版本间会有一些差异,我这里是按照nginx-1.0.6版本来分析。

1. 共享内存
nginx在共享内存操作相关的诉在src/os/unix/ngx_shmem.c与src/os/unix/ngx_shmem.h中。
我们先来看看这个结构体

typedef struct {
    u_char      *addr;		// 共享内存首地址
    size_t       size;   	// 共享内存大小
    ngx_str_t    name;      // 共享内存名称
    ngx_log_t   *log;		// 日志
    ngx_uint_t   exists;   /* unsigned  exists:1;  */
} ngx_shm_t;

在ngx_shmem.c中我们可以看到根据不同的宏,会有不同的共享内存实现方式。我们看看NGX_HAVE_MAP_ANON的这种方式,它很简单就是使用mmap的方式来创建共享内存。但从代码中,我们可以看出,它创建出来的共享内存是不与文件相关的,也就是不会映射到文件,当然,当nginx重启后,共享内存里面的内容就消失了。从代码中,我们可以看到,这里没有用到name与exists。

ngx_int_t
ngx_shm_alloc(ngx_shm_t *shm)
{
    shm->addr = (u_char *) mmap(NULL, shm->size,
                                PROT_READ|PROT_WRITE,
                                MAP_ANON|MAP_SHARED, -1, 0);
    if (shm->addr == MAP_FAILED) {
        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                      "mmap(MAP_ANON|MAP_SHARED, %uz) failed", shm->size);
        return NGX_ERROR;
    }
    return NGX_OK;
}
void
ngx_shm_free(ngx_shm_t *shm)
{
    if (munmap((void *) shm->addr, shm->size) == -1) {
        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
                      "munmap(%p, %uz) failed", shm->addr, shm->size);
    }
}

阅读更多…

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:nginx共享内存与锁的实现

端口重用引发的悲剧

最近做个性能测试,需要在一台机器上启动很多客户端,连接到同一台服务器,我在一台机器上启动了六万个连接,于是,端口被占用完了。按照我的理解,因为我作用端口是作为客户端,应该不会影响到其它进程,于是我放心大胆地去做测试,结果就引发了悲剧。有服务器程序要用到5191端口,却显示端口被占用了,lsof看了下,居然只有我的进程占用了,完全颠覆我的惯性思想。服务端与客户端都有打开SO_REUSEADDR。

我们先来看看SO_REUSEADDR的说明:

SO_REUSEADDR可以用在以下四种情况下。
(摘自《Unix网络编程》卷一,即UNPv1)
1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启
动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但
每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可
以测试这种情况。
3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个soc
ket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的
多播,不用于TCP。

分析了一下,第二种情况比较相似,但第二种情况是针对同一服务器的多个实例(多个进程),很显然跟我的情况不一样。我是在客户端与服务端间争用端口。 所以,我的情况根本就不适合以上任何一种情况,所以设置SO_REUSEADDR为1是无用的。

再接下来分析:
一个TCP连接需要由四元组来形成,即(src_ip,src_port,dst_ip,dst_port)。
如果有客户端建立了连接(src_ip1,src_port1,dst_ip1,dst_port1),那么,如果我们还有listen在(src_ip1,src_port1),那么当(dst_ip1,dst_port1)发送消息过来,系统应该把消息给谁?所以就说明了客户端占用了某一端口时,该端口就不能被其它进程listen了。

那么,对于有些童鞋,可能还有这样的疑问,是否一台机器就只能建立65535个连接了(端口16位限制)?非也,一个连接由四元组(src_ip,src_port,dst_ip,dst_port)形式,那么当(src_ip,src_port)一定时,变化的(dst_ip,dst_port)就可以建立更多连接了。

可能有些童鞋还有疑问,作为一个服务器监控一个端口,比如80端口,它为什么可以建立上百万个连接?首先要明白一点,当accept出来后的新socket,它所占用的本地端口依然是80端口,很多新手都以为是一个新的随机端口。由四元组就很容易分析到了,同一个(src_ip,src_port),它所对应的(dst_ip,dst_port)可以无穷变化,这样就可以建立很多个客户端的请求了。

好吧,真的是很悲剧。Have fun!!:)

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:端口重用引发的悲剧

显示网卡流量

显示网卡流量的方法蛮多,一般我们可以通过dstat来查看,但dstat不一定所有的机器都有安装。而我们知道,通过ifconfig可以看到某一网卡发送与接收的字节数,所以我们可以写一个脚本来统计一下。
先看ifconfig:

$ ifconfig eth0
eth0      Link encap:Ethernet  HWaddr A4:BA:DB:43:BA:B1
          inet addr:10.232.4.34  Bcast:10.232.4.255  Mask:255.255.255.0
          inet6 addr: fe80::a6ba:dbff:fe43:bab1/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:301707081 errors:0 dropped:1346358 overruns:0 frame:0
          TX packets:296718885 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:28485042645 (26.5 GiB)  TX bytes:35887266717 (33.4 GiB)
          Interrupt:98 Memory:d6000000-d6012800

我们可以看到rx与tx两个数据,于是我们的脚本出来了:

#!/bin/bash
alias ifconfig="/sbin/ifconfig"
eth=eth0
while true; do
RXpre=$(ifconfig ${eth} | grep bytes | awk '{print $2}'| awk -F":" '{print $2}')
TXpre=$(ifconfig ${eth} | grep bytes | awk '{print $6}' | awk -F":" '{print $2}')
sleep 1
RXnext=$(ifconfig ${eth} | grep bytes | awk '{print $2}'| awk -F":" '{print $2}')
TXnext=$(ifconfig ${eth} | grep bytes | awk '{print $6}' | awk -F":" '{print $2}')
echo RX ----- TX
echo "$((((${RXnext}-${RXpre})/1024)/1024))MB/s $((((${TXnext}-${TXpre})/1024/1024)))MB/s"
done

脚本暂时比较简单,可以添加一些参数判断,比如多长时间显示一次等等,先看看执行结果:

$ ./a
RX ----- TX
5MB/s 7MB/s
RX ----- TX
5MB/s 7MB/s
RX ----- TX
4MB/s 6MB/s
RX ----- TX
4MB/s 6MB/s
RX ----- TX

好吧,先介绍到这,have fun :)

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:显示网卡流量

http长连接200万尝试及调优

对于一个server,我们一般考虑他所能支撑的qps,但有那么一种应用, 我们需要关注的是它能支撑的连接数个数,而并非qps,当然qps也是我们需要考虑的性能点之一。这种应用常见于消息推送系统,也称为comet应用,比如聊天室或即时消息推送系统等。comet应用具体可见我之前的介绍,在此不多讲。对于这类系统,因为很多消息需要到产生时才推送给客户端,所以当没有消息产生时,就需要hold住客户端的连接,这样,当有大量的客户端时,就需要hold住大量的连接,这种连接我们称为长连接。

首先,我们分析一下,对于这类服务,需消耗的系统资源有:cpu、网络、内存。所以,想让系统性能达到最佳,我们先找到系统的瓶颈所在。这样的长连接,往往我们是没有数据发送的,所以也可以看作为非活动连接。对于系统来说,这种非活动连接,并不占用cpu与网络资源,而仅仅占用系统的内存而已。所以,我们假想,只要系统内存足够,系统就能够支持我们想达到的连接数,那么事实是否真的如此?如果真能这样,内核来维护这相当大的数据结构,也是一种考验。

要完成测试,我们需要有一个服务端,还有大量的客户端。所以需要服务端程序与客户端程序。为达到目标,我的想法是这样的:客户端产生一个连接,向服务端发起一个请求,服务端hold住该连接,而不返回数据。

1. 服务端的准备
对于服务端,由于之前的假想,我们需要一台大内存的服务器,用于部署nginx的comet应用。下面是我用的服务端的情况:

Summary:        Dell R710, 2 x Xeon E5520 2.27GHz, 23.5GB / 24GB 1333MHz
System:         Dell PowerEdge R710 (Dell 0VWN1R)
Processors:     2 x Xeon E5520 2.27GHz 5860MHz FSB (16 cores)
Memory:         23.5GB / 24GB 1333MHz == 6 x 4GB, 12 x empty
Disk-Control:   megaraid_sas0: Dell/LSILogic PERC 6/i, Package 6.2.0-0013, FW 1.22.02-0612,
Network:     eth0 (bnx2):Broadcom NetXtreme II BCM5709 Gigabit Ethernet,1000Mb/s
OS:             RHEL Server 5.4 (Tikanga), Linux 2.6.18-164.el5 x86_64, 64-bit

服务端程序很简单,基于nginx写的一个comet模块,该模块接受用户的请求,然后保持用户的连接,而不返回。Nginx的status模块,可直接用于监控最大连接数。

服务端还需要调整一下系统的参数,在/etc/sysctl.conf中:

net.core.somaxconn = 2048
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 4096 16777216
net.ipv4.tcp_wmem = 4096 4096 16777216
net.ipv4.tcp_mem = 786432 2097152 3145728
net.ipv4.tcp_max_syn_backlog = 16384
net.core.netdev_max_backlog = 20000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_max_orphans = 131072

/sbin/sysctl -p 生效

这里,我们主要看这几项:
net.ipv4.tcp_rmem 用来配置读缓冲的大小,三个值,第一个是这个读缓冲的最小值,第三个是最大值,中间的是默认值。我们可以在程序中修改读缓冲的大小,但是不能超过最小与最大。为了使每个socket所使用的内存数最小,我这里设置默认值为4096。
net.ipv4.tcp_wmem 用来配置写缓冲的大小。
读缓冲与写缓冲在大小,直接影响到socket在内核中内存的占用。
而net.ipv4.tcp_mem则是配置tcp的内存大小,其单位是页,而不是字节。当超过第二个值时,TCP进入pressure模式,此时TCP尝试稳定其内存的使用,当小于第一个值时,就退出pressure模式。当内存占用超过第三个值时,TCP就拒绝分配socket了,查看dmesg,会打出很多的日志“TCP: too many of orphaned sockets”。
另外net.ipv4.tcp_max_orphans这个值也要设置一下,这个值表示系统所能处理不属于任何进程的socket数量,当我们需要快速建立大量连接时,就需要关注下这个值了。当不属于任何进程的socket的数量大于这个值时,dmesg就会看到”too many of orphaned sockets”。

另外,服务端需要打开大量的文件描述符,比如200万个,但我们设置最大文件描述符限制时,会遇到一些问题,我们在后面详细讲解。

2. 客户端的准备
由于我们需要构建大量的客户端,而我们知道,在一台系统上,连接到一个服务时的本地端口是有限的。由于端口是16位整数,也就只能是0到65535,而0到1023是预留端口,所以能分配的只是1024到65534,也就是64511个。也就是说,一台机器只能创建六万多个长连接。要达到我们的两百万连接,需要大概34台客户端。
当然,我们可以采用虚拟ip的方式来实现这么多客户端,如果是虚拟ip,则每个ip可以绑定六万多个端口,34个虚拟ip就可以搞定。而我这里呢,正好申请到了公司的资源,所以就采用实体机来做了。
由于系统默认参数,自动分配的端口数有限,是从32768到61000,所以我们需要更改客户端/etc/sysctl.conf的参数:

net.ipv4.ip_local_port_range = 1024 65535

/sbin/sysctl -p

客户端程序是基于libevent写的一个测试程序,不断的建立新的连接请求。

3. 由于客户端与服务端需要建立大量的socket,所以我们需要调速一下最大文件描述符。
客户端,需要创建六万多个socket,我设置最大为十万好了,的在/etc/security/limits.conf中添加:

admin    soft    nofile  100000
admin    hard    nofile  100000

服务端,需要创建200万连接,那我想设置nofile为200万,好,问题来了。
当我设置nofile为200万时,系统直接无法登陆了。尝试几次,发现最大只能设置到100万。在查过源码后,才知道,原来在2.6.25内核之前有个宏定义,定义了这个值的最大值,为1024*1024,正好是100万,而在2.6.25内核及其之后,这个值是可以通过/proc/sys/fs/nr_open来设置。于是我升级内核到2.6.32。ulimit详细介绍见博文:老生常谈: ulimit问题及其影响: http://blog.yufeng.info/archives/1380
升级内核后,继续我们的调优,如下:

sudo bash -c 'echo 2000000 > /proc/sys/fs/nr_open'

现在再设置nofile就可以了

admin    soft    nofile  2000000
admin    hard    nofile  2000000

4. 最后,在测试的过程中,根据dmesg的系统打出的信息不断调整服务端/sbin/sysctl中的配置,最后我们的测试完成了200万的长连接。
为了使内存占用尽量减少,我将Nginx的request_pool_size从默认的4k改成1k了。另外,net.ipv4.tcp_wmem与net.ipv4.tcp_rmem中的默认值也设置成4k。

两百万连接时,通过nginx的监控得到数据:
两百万连接数

两百万连接时系统内存情况:
内存占用

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:http长连接200万尝试及调优

TCPDUMP工具的简单介绍PPT

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:TCPDUMP工具的简单介绍PPT

linux下ss工具简介

在实际工作中,熟练使用工具,可以为我们提高不少效率。今天我们简单了解下ss工具的使用。ss即socket state,也就是说,是可以查看系统中socket的状态的。我们可以用netstat,但为什么还要用ss这个工具呢,当然ss也是有好处的。当我们打开的socket数量很多时,netstat就会变得慢了。

我们先来看看ss的使用格式:

$ ss [ OPTIONS ] [ STATE-FILTER ] [ ADDRESS-FILTER ]

options我从man手册里摘了过来:

-h – show help page
-? – the same, of course
-v, -V – print version of ss and exit
-s – print summary statistics. This option does not parse socket lists obtaining summary from various sources. It is useful when amount of sockets is so huge that parsing /proc/net/tcp is painful.
-D FILE – do not display anything, just dump raw information about TCP sockets to FILE after applying filters. If FILE is – stdout is used.
-F FILE – read continuation of filter from FILE. Each line of FILE is interpreted like single command line option. If FILE is – stdin is used.
-r – try to resolve numeric address/ports
-n – do not try to resolve ports
-o – show some optional information, f.e. TCP timers
-i – show some infomration specific to TCP (RTO, congestion window, slow start threshould etc.)
-e – show even more optional information
-m – show extended information on memory used by the socket. It is available only with tcp_diag enabled.
-p – show list of processes owning the socket
-f FAMILY – default address family used for parsing addresses. Also this option limits listing to sockets supporting given address family. Currently the following families are supported: unix, inet, inet6, link, netlink.
-4 – alias for -f inet
-6 – alias for -f inet6
-0 – alias for -f link
-A LIST-OF-TABLES – list of socket tables to dump, separated by commas. The following identifiers are understood: all, inet, tcp, udp, raw, unix, packet, netlink, unix_dgram, unix_stream, packet_raw, packet_dgram.
-x – alias for -A unix
-t – alias for -A tcp
-u – alias for -A udp
-w – alias for -A raw
-a – show sockets of all the states. By default sockets in states LISTEN, TIME-WAIT, SYN_RECV and CLOSE are skipped.
-l – show only sockets in state LISTEN

ss的强大之处,大于可以设定过滤条件,我们可以根据socket的状态来进行过滤,也可通过端口与ip地址进行过滤。也就是我们在命令格式里面看到的STATE-FILTER与ADDRESS-FILTER。

首先看看STATE-FILTER,STATE-FILTER可用的过滤条件有:
1. 所有的TCP状态,包含:established, syn-sent, syn-recv, fin-wait-1, fin-wait-2, time-wait, closed, close-wait, last-ack, listen and closing.
2. all,包含所有的状态。
3. connected,除了listen与closed的所有其它状态。
4. synchronized,除了syn-sent的所有connected的状态。
5. bucket
6. big
使用时,如:

$ ss state connected

再看看ADDRESS-FILTER,ADDRESS-FILTER用于过滤端口与地址。而且可以进行表达式组合。可用的子表达式有:
1. dst ADDRESS_PATTERN
2. src ADDRESS_PATTERN
3. dport RELOP PORT
4. sport RELOP PORT
5. autobound
其中ADDRESS_PATTERN为ip地址与端口匹配,ip:port,可以用*代替。RELOP为<= >=或==。
如:

$ ss dst 192.168.0.1:80
      $ ss dport == 80

多个子表达式之间可以组合,当然跟tcpdump一样,可以用or and not来组合。但括号要用转义符号表示。
如:

$ ss -o state fin-wait-1 \( sport = :http or sport = :https \) dst 193.233.7/24

看看几个例子:
查看系统总体信息:

$ ss -s
Total: 85 (kernel 108)
TCP:   15 (estab 4, closed 0, orphaned 0, synrecv 0, timewait 0/0), ports 12

Transport Total     IP        IPv6
*	  108       -         -
RAW	  0         0         0
UDP	  10        7         3
TCP	  15        12        3
INET	  25        19        6
FRAG	  0         0         0

想看当前机器的8088端口被谁占用了:

$ ss -lp src :8088
   Recv-Q Send-Q                                                       Local Address:P
0      0                                                                        *:8ers:(("nginx",2942,5),("nginx",2943,5))

我们可以看到,是一个叫nginx的进程,进程id是2942。
当然,用lsof工具也可以看到,还会更简单呢。lsof -i :80

好吧,就先简单介绍到这了。

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:linux下ss工具简介

Nginx中的location匹配的几点说明

location匹配的原型是这样的:location [=|~|~*|^~|@] /uri/ { … }

“=”是精确匹配
“@”是命名的location,在正常的location匹配中不会使用,仅仅在内部跳转中才会使用到。
“~”是区分大小写的匹配
“~*”是不区分大小写的匹配
“^~”表示中止正则匹配(这个平时没太注意)

在一个请求中,匹配的顺序是这样的。先使用所有location来匹配URI的开始部分,最精确匹配的(形象点说,就是即配置字符数最多的)为最后匹配结果;然后进行正则表达式的匹配,按照配置文件中的顺序来进行匹配,如果有一个匹配成功,则结束正则匹配,且最后匹配结果为此location,否则,最后结果为先前最精确匹配的的那个location

之前有提到过”^~”,它配置在非正则匹配中,表示,如果最精确匹配的loction为此location,则立即返回该location作为结果,而不进行下一步的正则匹配,这样,就此可以不必要进入到正则匹配当中,以加快匹配速度。

还有”=”,它是最精确的匹配,而且优先级最高。最先进行带”=”的匹配,如果匹配成功,立马返回。

最后总结下匹配的过程,有四步:
1. 带”=”前缀的先进行匹配,如果找到了,中止查找。
2. 所有其它location进行非正则的匹配,找到最精确匹配的那个,如果匹配到带”^~”前缀的,则中止查找。
3. 正则查找,按照我们配置文件中配置的location顺序进行查找。
4. 如果正则查找匹配成功,则使用此正则匹配的location,否则,使用第二步查找的结果。

这里要特别说明下”=”与”^~”的区别:
“=”在匹配时,则匹配带”=”的location。而”^~”,则会匹配所有非”=”的非正则location,只有在确认它是最精确匹配的location后,才生效。

 

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:Nginx中的location匹配的几点说明

nginx共享内存(二):共享内存的实现

nginx中, 作者为我们提供了方便共享内存的使用的接口,关于共享内存的使用在我之前的文章中有介绍。这次我们来研究一下nginx是如何实现的。
我们知道,如果我们的模块中要使用一个共享内存,需要调用ngx_shared_memory_add来创建共享内存。而ngx_shared_memory_add不会马上创建一个共享内存,它是先登记一下共享内存的使用信息,比如名称、大小等,然后在进程初始化的时候再进行共享内存的创建与初始化。
那么,ngx_shared_memory_add这个函数是将共享内存的分配登记在哪的呢?在ngx_cycle_s这个结构体中有一个成员,即ngx_cycle_s->shared_memory,它是一个list,用来保存所有登记的共享内存,这个list中保存的是ngx_shm_zone_t类型的数据。下面是ngx_shm_zone_t这个结构体的实现源码:

struct ngx_cycle_s {
     ****
     ngx_list_t                shared_memory; //  共享内存 ngx_shm_zone_t
     ****
};
struct ngx_shm_zone_s {
    // 这里可以指向自定义的一个数据结构,主要是为了在数据初始化的时候使用到,或通过共享内存直接拿到与共享内存相关的数据,它不一定指向共享内存中的地址
    void                     *data;
    // 实际的共享内存
    ngx_shm_t                 shm;
    // 共享内存的初始化函数
    ngx_shm_zone_init_pt      init;
    // 标记
    void                     *tag;
};

我们再看看ngx_shared_memory_add这个函数的实现,该函数先检查要添加的共享内存是否已存在,如果已存在,则直接返回,否则,创建一个新的。
1. 两个相同名字的共享内存大小要一样。
2. 两个相同名字的共享内存tag要一样。
3. 如果当前共享内存已经存在,则不需要再次添加。会返回同一个共享内存
4. 如果此共享内存不存在,则添加一个新的ngx_shm_zone_t
添加完后,会返回ngx_shm_zone_t,然后再设置init函数与data数据。
阅读更多…

>>原创文章,欢迎转载。转载请注明:转载自程序非主流,谢谢!
>>原文链接地址:nginx共享内存(二):共享内存的实现