李子的博客

想想写写

Ngx_lua与go高并发性能对比

language | Comments

nginx在处理高并发能力上非常出色,而go作为新时代互联网语言,在设计之初就为实现高并发。

ngx_lua由nginx来处理网络事件,并使用协程来实现非阻塞,从而实现高并发。 go语言级别提供非阻塞的api,同样使用协程来提供高并发处理。

我们来测试对比一下两者的性能。

ngx_lua:Tengine/1.4.3+luajit+ngx_lua
go:go1.0.3

分别实现512字节的内容的输出,对比在不同并发下的qps。

测试机器:

16core Intel(R) Xeon(R) CPU E5520  @ 2.27GHz  
Linux localhost 2.6.18-164.el5 #1 SMP Tue Aug 18 15:51:48 EDT 2009 x86_64 x86_64 x86_64 GNU/Linux

使用ab进行测试,测试结果如下:

短连接 100 200 500 1000 2000
ngx_lua qps:17329 us:2.6% sy:2.2% 17744 16443 15852 13589
go qps:16538 us:9.1% sy:3.6% 16546 15988 15032 13757
长连接 100 200 500
ngx_lua qps:72274 us:13.8% sy:8.5 61204 61983
go qps:39072 us:29% sy:15% 38688 38238

从结果中,可以看出短连接时,两者qps相差不大,而长连接时,两者相差较大。go的cpu占用比ngx_lua要高不少。另外,go在并发数增加的情况下,性能依然出色。

相关测试代码。

lua代码:

ngx.print("aaaaa...512...aaa")

go 代码:

package main

import (
    "net/http"
    "log"
    "fmt"
    "runtime"
)

func handler512(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Connection", "keep-alive")
    a := []byte("aaaaa...512...aaa")
    w.Header().Set("Content-Length", fmt.Sprintf("%d", len(a)))
    w.Write(a)
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    http.HandleFunc("/512b", handler512)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Nginx问题定位之监控进程异常退出

nginx | Comments

nginx在运行过程中是否稳定,是否有异常退出过?这里总结几项平时会用到的小技巧。

1. 在error.log中查看是否有signal项,如果有,看看signal是多少。

比如,这是一个异常退出的情况:

$grep signal error.log

2012/12/24 16:39:56 [alert] 13661#0: worker process 13666 exited on signal 11

如果在进程退出后,有coredump文件产生,则会打出如下日志:

$grep signal error.log

2012/12/24 16:39:56 [alert] 13661#0: worker process 13666 exited on signal 11 (core dumped) 

2. 简单方式,看进程号是否连续

一般来说,在worker进程启动时,其进程号都是连续的(至少相差不是很远),如果有进程退出,其进程号就不一定连续。

$ps aux | grep nginx

lizi      7223  0.0  0.0  74844  2024 ?        Ss   13:32   0:00 nginx: master process ./nginx
lizi      7292  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7293  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7294  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7295  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7296  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7297  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7298  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7299  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7300  0.0  0.0  78856  5468 ?        S    13:33   0:00 nginx: worker process
lizi      7301  0.0  0.0  78856  5452 ?        S    13:33   0:00 nginx: worker process

可以看到,10个worker进程,基本从7292到7301,进程号连续。
如下:

$ps aux | grep nginx

nobody    9492 16659 26 09:18 ?        01:10:41 nginx: worker process
root      16659     1  0 Dec24 ?       00:00:00 nginx: master process ./nginx
nobody   16663 16659 11 Dec24 ?        02:41:38 nginx: worker process
nobody   19344 16659 24 10:18 ?        00:50:54 nginx: worker process
nobody    25447 16659 28 07:41 ?        01:43:56 nginx: worker process 

进程号已不再连续,说明nginx可能有工作进程异常退出。

3. 查看dmesg系统消息。

在man手册里面是这么描述dmesg的:

DESCRIPTION
dmesg is used to examine or control the kernel ring buffer.

查看dmesg是检测系统运行状态的常用手段,通常可以帮我们排查很多问题。当然,如果有进程异常退出,dmesg也可以看到。

$dmesg

nginx[24721]: segfault at 0000000000000001 rip 0000000000000001 rsp 00007ffff58d8180 error 14
nginx[1729]: segfault at 0000000000000190 rip 00000000004c2d27 rsp 00007ffff58d8340 error 4
nginx[22002]: segfault at ffffffffffffffff rip 000000001c959744 rsp 00007fff43caac18 error 6

rip表示程序退出时的ip寄存器内容,当没有core文件可用时,可根据此值以及反汇编来查找程序core的位置。

4. 打开coredump文件。

一般我们在程序启动前,通过ulimit -c ulimited来设置core文件的大小,也可以修改/etc/security/limits.conf文件,添加如下信息:

admin               soft    core            1000000
admin               hard    core            1000000

也可以直接修改nginx的配置文件,添加如下配置项:

worker_rlimit_core 10000m;

而此时,在limit系统中,默认coredump文件会写在启动nginx时的目录,如果nginx在启动时worker进程的用户没有权限写到这个目录,进程在异常退出时,就无法产生coredump文件。由于nginx启动后,或者是由别人启动,我们无法知道nginx在启动时的目录,也就无法知道core文件的目录。我曾经碰到过这样的问题,通过日志查看,是coredump出来了,但却找不到coredump的文件。

这里有一个小技巧,查看/proc/pid/cwd可以看到进程的工作目录,而core文件会产生在工作目录。

nginx可以配置工作目录来改变默认的工作目录,于是,我们需要配置working_directory为目的工作目录,我们的core文件也会产生在这个目录。

working_directory /path/to/core;

working_directory与编译时指定的--prefix=/path不同,后者表示在配置文件中所用的相对路径所生产的绝对路径。所以,working_directory不会影响到配置的引用路径,而仅仅是为了改变core文件的路径,当然nginx必须有写这个目录的权限,否则无法core出来。

所以,这里,我推荐的做法是,配置worker_rlimit_coreworking_directory这两个指令,这样,就不需要修改操作系统的参数就可以正常core出来了。

以上这些是平时用到的一些技巧的总结,大家玩得开心!

Vim字符串替换

vim | Comments

经常使用vim进行字符串替换,每次却只使用全量替换,特地总结一下常用的几个替换字符串的使用。

当前行进行替换
:s/abc/efg/
:s/abc/efg/g

所有行进行替换
:%s/abc/efg/
:%s/abc/efg/g

从第n行开始向下的所有行进行替换,当n为"."时,表示从当前行开始
:n,$s/abc/efg/
:n,$s/abc/efg/g

上面命令中,最后没有g表示只替换一行中第一次出现的字符串abc为efg。而后面带g的表示当前行的所有abc替换efg。

Nginx中slab分配大内存的陷阱

nginx | Comments

我们在开发nginx模块时,需要很小心,nginx里面有很多陷阱是我们需要注意的。之前有人提到过slab分配器在使用时,不适合大内存分配,否则会出现分配不出内存的现象。

nginx一般使用slab来管理共享内存,在程序启动时,很分配好需要共享的内存,然后使用slab来进行初始化,之后就交给slab来管理这段内存。slab的源码分析与合适,在我之前的博客里面有分析过。这次我们针对性的看看,为什么会出现分配不出内存的现象。

分配内存时,会调用ngx_slab_alloc_locked,在这个函数里面会先判断size是否大于ngx_slab_max_size,代码如下。

void *
ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size)
{
    size_t            s;
    uintptr_t         p, n, m, mask, *bitmap;
    ngx_uint_t        i, slot, shift, map;
    ngx_slab_page_t  *page, *prev, *slots;

    /* 判断大小 */
    if (size >= ngx_slab_max_size) {

        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0,
                       "slab alloc: %uz", size);

        /* 直接分配页 */
        page = ngx_slab_alloc_pages(pool, (size + ngx_pagesize - 1)
                >> ngx_pagesize_shift);
        if (page) {
            p = (page - pool->pages) << ngx_pagesize_shift;
            p += (uintptr_t) pool->start;

        } else {
            p = 0;
        }

        goto done;
    }

    ...

}

Nginx源码分析之变量

nginx | Comments

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中添加一个变量。

端口重用引发的悲剧

linux | Comments

最近做个性能测试,需要在一台机器上启动很多客户端,连接到同一台服务器,我在一台机器上启动了六万个连接,于是,端口被占用完了。按照我的理解,因为我作用端口是作为客户端,应该不会影响到其它进程,于是我放心大胆地去做测试,结果就引发了悲剧。有服务器程序要用到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)可以无穷变化,这样就可以建立很多个客户端的请求了。

统计网卡流量

linux | Comments

显示网卡流量的方法蛮多,一般我们可以通过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  

Http长连接200万尝试及调优

http | Comments

对于一个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问题及其影响
升级内核后,继续我们的调优,如下:

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的监控得到数据:
data
两百万连接时系统内存情况:
mem