4

作者:学而思网校-黄桃

http1.1与http1.0最大的区别是什么?

答案是http1.1协议是默认开启keep-alive的,如图http1.1的请求头:
image.png

那什么是keepalive?作用是什么?

keepalive是在TCP中一个可以检测死连接的机制,可以保持tcp长连接不被断开,属于tcp层功能。http协议使用keepalive保持长连接,主要作用是提高对tcp连接的复用率,减少创建连接过程给系统带来的性能损耗。

TCP层怎么做到保持长连接的呢?

先看keepalive的用法:有三个参数,开放给应用层使用:

1. sk->keepalive_probes:探测重试次数,超过次数则close连接;
2. sk->keepalive_time  探测的心跳间隔,TCP连接在间隔多少秒之后未进行数据传输,则启动探测报文;
3. sk->keepalive_intvl 探测间隔,发送探活报文,未收到回复时,重试的时间间隔;

linux系统对这三个参数有默认配置,查看:

[***@*** ~]$ $  cat/proc/sys/net/ipv4/tcp_keepalive_time
300
[***@*** ~]$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
[***@*** ~]$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
9

应用层使用示例:

  1. int keepalive = 1; // 开启keepalive属性
  2. int keepidle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测
  3. int keepinterval = 5; // 探测时发包的时间间隔为5 秒
  4. int keepcount = 3; // 探测尝试的次数。如果第1次探测包就收到响应了,则后2次的不再发。并且清零该计数
  5. setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive , sizeof(keepalive ));
  6. setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepidle , sizeof(keepidle ));
  7. setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepinterval , sizeof(keepinterval ));
  8. setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepcount , sizeof(keepcount ));

应用层这么设置后,会把Linux默认配置覆盖,走手动设置的配置

  • keepcount: 覆盖 tcpkeepaliveprobes
  • keepidle: 覆盖 tcpkeepalivetime
  • keepinterval: 覆盖 tcpkeepalive_intvl
    对于一个经过三次握手已建立好的tcp连接,如果在keepalive_time时间内双方没有任何的数据包传输,则开启keepalive功能,一端将发送keepalive数据心跳包,若没有收到应答,则每隔keepalive_intvl时间间隔再发送该数据包,发送keepalive_probes次,一直没有收到应答,则发送rst包关闭连接,若收到应答,则将计时器清零。

抓包看看keepalive的探活过程
image.png
根据抓包继续分析keepalive发送及回复的心跳包内容:
先看tcp头的结构为:

typedef struct _TCP_HEADER
{
 short m_sSourPort;              // 源端口号16bit
 short m_sDestPort;              // 目的端口号16bit
 unsigned int m_uiSequNum;         // req字段 序列号32bit
 unsigned int m_uiAcknowledgeNum;  //ack字段  确认号32bit
 short m_sHeaderLenAndFlag;        // 前4位:TCP头长度;中6位:保留;后6位:标志位
 short m_sWindowSize;            //win字段  窗口大小16bit
 short m_sCheckSum;              // 检验和16bit
 short m_surgentPointer;           // 紧急数据偏移量16bit
}__attribute__((packed))TCP_HEADER, *PTCP_HEADER;

看发送的心跳包内容 :

0000 d4 6d 50 f5 02 7f f4 5c    89 cb 35 29 08 00        //mac头 14字节:
                                                  45 00  // ip头 20字节 :
0010 00 28 10 f4 00 00 40 06    5b dd ac 19 42 76 0a b3
0020 14 bd
           e4 4a 1f 7c 32 7e    7a cb 4c bc 55 08 50 10   // tcp头 20字节
0030 10 00 3f 00 00 00
//分析tcp头部内容
e4 4a //源端口号16bit  10进制为:58442
1f 7c  //目的端口号16bit 10进制为 : 8060
32 7e 7a cb // req字段 序列号32bit  10进制为 : 
4c bc 55 08 // ack字段  确认号32bit
5 // 前4位:TCP头长度 5*4 =20 字节 没问题
0 10  /// 中6位:保留;后6位:标志位 10 代表倒数第5位为1, 标识改tcp包为 ACK 确认包 
0030 10 00 3f 00 00 00

继续看回复的心跳包内容 :

0000 f4 5c 89 cb 35 29 d4 6d 50 f5 02 7f 08 00 45 00
0010 00 34 47 28 40 00 36 06 ef 9c 0a b3 14 bd ac 19 
0020 42 76 // 前面数据不解读
1f 7c
e4 4a
4c bc 55 08
32 7e 7a cc
8// TCP头长度为8 * 4 = 32  除了头部 还有 选项数据 12字节
0 10   // 中6位:保留;后6位:标志位 10 代表倒数第5位为1, 标识该tcp包为 ACK 确认包 
0030 01 3f //win字段  窗口大小16bit
4e 0d // 检验和16bit
00 00 // 紧急数据偏移量16bit
01 01 08 0a 00 59 be 1c 39 13 
0040 cf 12  // 选项数据 12字节

由上(可直接抓包工具看,此处只是一笔带过,让大家知道如何分析包的内容)可以看出,tcp长连接是由客户端(浏览器)与服务器通过发送ACK心跳包来维持,大致了解keepalive后,我们再看nginx是如何处理。

keepalive与keep-alive区别?

keepalive是tcp层长连接探活机制;
keep-alive是应用层http协议使用,在其头部Connection字段中的一个值,只是代表客户端与服务之间需要保持长连接,可以理解为开启tcp层长连接探活机制。

nginx的keepalive会做哪些事情?

当使用nginx作为代理服务器时,这两点必然要满足:

  • client到nginx的连接是长连接
  • nginx到server的连接是长连接

image

配置

场景1,配置TCP层探活机制的三个参数

case1:
http {
server {
    listen 127.0.0.1:3306 so_keepalive=on;//开启keepalive探活,不管系统默认配置开没开,探测策略走系统默认
    }
}
case2:
http {
server {
    listen 127.0.0.1:3306 so_keepalive=7m:75s:9;//把空闲时长有系统默认的5分钟改为了7分钟
    }
}

其中so_keepalive有如下选择配置,官方文档:so_keepalive

so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]
*   on: 开启,探测参数更加系统默认值
*   off: 关闭
*   keepidle: 连接空闲等待时间 
*   keepintvl: 发送探测报文间隔时间
*   keepcent: 探测报文重试次数

每个参数主要是覆盖linux系统针对keepalive的默认配置,如果nginx未设置so_keepalive配置,则走系统默认的探活策略

场景2、nginx与客户端(一般为浏览器、APP等)保持的长连接进行限制管理;

http {
    keepalive_timeout  120s 120s;
    keepalive_requests 100;
}

客户端请求header头:

GET /uri  HTTP/1.1      #版本为1.1及以上,Connection:为空也开启长连接,但Connection:close时不开启
Host: www.baidu.com
Connection: keep-alive  #Connection:keep-alive 时均开启长连接,HTTP是否为1.1以上无影响
  • keepalive_timeout: 第一个参数:客户端连接在服务器端空闲状态下保持的超时值(默认75s);值为0会禁用keep-alive,也就是说默认不启用长连接;第二个参数:响应的header域中设置“Keep-Alive: timeout=time”;告知浏览器对长连接的维持时间;官方文档:keepalive_timeout
  • keepalive_requests:默认100,某个长连接连续处理请求次数限制,超过次数则该长连接被关闭;如果需要释放某个连接占用的内存,必须关闭该链接,内存不大的情况下,不建议开大该配置;在QPS较高的场景,则有必要加大这个参数;官方文档:keepalive_requests

场景3、nginx与上游server保持长连接

http {
    upstream  BACKEND {
        server 127.0.0.1:8000;
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
        keepalive 300; //空闲连接数   
        keepalive_timeout  120s;//与上游空闲时间
        keepalive_requests 100;//与上游请求处理最大次数
    }
    server{
        listen 8080;
        location /{
            proxy_pass http://BACKEND;
        }
    }
}
  • keepalive:限制nginx某个worker最多空闲连接数,此处不会限制worker与上游服务长连接的总数,官方文档:keepalive
  • keepalive_timeout:nginx与上游长连接最大空闲时间,默认值为60s;官方文档: keepalive_timeout
  • keepalive_requests:nginx与上游长连接最大交互请求的次数,默认值为100;官方文档: keepalive_requests

除此之外,nginx与上游通信,http协议默认是走的http1.0,对客户端header头不会直接转发,且会把头部中Connection字段置为默认的"close",要与上游保持长连接还需要加如下配置:

http {
    keepalive_timeout  120s 120s;
    keepalive_requests 100;
    server {
        location /  {
            proxy_http_version 1.1;         //设置与上游通信的
            proxy_set_header Connection ""; 
            proxy_pass http://BACKEND;
        }
    }
}

nginx的内部实现

1、so_keepalive 配置后对系统默认的tcp探活策略进行覆盖

第一步:nginx启动阶段,读取配置文件配置,解析listen关键字时执行该关键字对应的回调函数:ngx_http_core_listen函数,ngx_http_core_listen函数中会读取so_keepalive配置项,并赋值:

*   lsopt.so_keepalive = 1;//开启长连接探活机制,上文中场景1配置的case1与case2,都会置为1;
*   lsopt.tcp_keepidle = ngx_parse_time(&s, 1);//根据场景1配置的case2,此处值为 7*60 = 420
*   lsopt.tcp_keepintvl = ngx_parse_time(&s, 1);//根据场景1配置的case2,此处值为 75 
*   lsopt.tcp_keepcnt = ngx_atoi(s.data, s.len);//根据场景1配置的case2,此处值为 9

第二步:解析配置完成后,会循环监听每个listen对应的端口,产生listen_fd,并把配置文件解析出来的keepalive相关配置,赋值给每个listen_fd对应监听池中的ngx_listening_s 结构体:

    ls->keepalive = addr->opt.so_keepalive;
    ls->keepidle = addr->opt.tcp_keepidle;
    ls->keepintvl = addr->opt.tcp_keepintvl;
    ls->keepcnt = addr->opt.tcp_keepcnt;

第三步:监听完所有端口后,还会继续初始化根据listen对应配置设置listen_fd的属性,主要在ngx_configure_listening_sockets函数中进行,与keepalive相关的设置主要如下,具体属性值在前文已介绍,此处不再说明:

    if (ls[i].keepalive) {
        value = (ls[i].keepalive == 1) ? 1 : 0;
        setsockopt(ls[i].fd, SOL_SOCKET, SO_KEEPALIVE,(const void *) &value, sizeof(int);
    }
    if (ls[i].keepidle) {
        value = ls[i].keepidle;
        setsockopt(ls[i].fd, IPPROTO_TCP, TCP_KEEPIDLE,(const void *) &value, sizeof(int);
    }
    if (ls[i].keepintvl) {
        value = ls[i].keepintvl;
        setsockopt(ls[i].fd, IPPROTO_TCP, TCP_KEEPINTVL,(const void *) &value, sizeof(int));
    }

    if (ls[i].keepcnt) {
        setsockopt(ls[i].fd, IPPROTO_TCP, TCP_KEEPCNT, (const void *) &ls[i].keepcnt, sizeof(int);
    }

此时想比大家有一个疑问,为什么设置属性时是对listen_fd进行操作,而不是对客户端与nginx的connect_fd进行设置。
主要原因为:这些属性是sockt继承的,即listen的套接字设置该属性后,后面建连接后调用accept函数获取的connect_fd套接字同样继承该属性(心跳属性)。
通过以上设置之后,nginx与客户端的链接就可以通过tcp探活保持长连接,并且探活策略是可配置的;

2、nginx与客户端什么时候断开长连接?

在nginx通过 setsockopt(ls[i].fd, SOL_SOCKET, SO_KEEPALIVE,(const void *) &value, sizeof(int))开启keepalive后,accept后的connect_fd会始终和客户端保持长连接,如此会出现一个很严峻的问题,每个woker进程能保持的连接数是有限的,见如下代码:

ep = epoll_create(cycle->connection_n / 2);  //cycle->connection_n / 2 为epoll能管理的fd上限

如此一来,连接数很快就被耗尽,这时候nginx应该怎么处理 ?
答案显而易见,通过 keepalive_timeout keepalive_requests 来管理长连接,
也就是上文中场景2的配置,实际是nginx与客户端(一般为浏览器、APP等)保持长连接进行的限制配置;

1、当一个tcp连接存活时间超过 keepalive_timeout  时则会被close掉,nginx的具体实现,是通过定时器来做的
2、当一个tcp连接最大请求数超过 keepalive_requests  时则也会被close掉

通过这两个机制来保证每个worker的连接数不会超过epoll所能管理的数目。

对应源码实现:
第一步:解析对应配置赋值

 ngx_http_core_keepalive(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    clcf->keepalive_timeout = ngx_parse_time(&value[1], 0);
    clcf->keepalive_header = ngx_parse_time(&value[2], 1);
}
    conf->keepalive_requests,prev->keepalive_requests, 100);

第二步:根据客户端请求头中的参数,及服务端配置,对客户端的连接存活进行管理:
1、读取客户端Connection: keep-aliveent | close

ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h,
    ngx_uint_t offset)
{
    if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) {
        r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE;
    } else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1)) {
        r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE;
    }
    return NGX_OK;
}

2、根据客户端请求头配置,对request的keepalive进行标识,需要保持长连接则标识为1,否则为0

ngx_http_handler(ngx_http_request_t *r)
{
    if (!r->internal) {
        switch (r->headers_in.connection_type) {//客户端的Connection: 为空时,只要HTTP协议>1.0,也开启keepalive
        case 0:
            r->keepalive = (r->http_version > NGX_HTTP_VERSION_10);
            break;

        case NGX_HTTP_CONNECTION_CLOSE://客户端的Connection: close时
            r->keepalive = 0;
            break;

        case NGX_HTTP_CONNECTION_KEEP_ALIVE://客户端的Connection: keep-alive时
            r->keepalive = 1;
            break;
        }

    } 
 }

3、根据nginx自身配置,判断keepalive_timeout是否为0 或 该连接的请求次数已达上限值,也把客户端的keepalive标识改为0

ngx_http_update_location_config(ngx_http_request_t *r)
{
    if (r->keepalive) {
        if (clcf->keepalive_timeout == 0) {//keepalive_timeout配置关闭了长连接
            r->keepalive = 0;

        } else if (r->connection->requests >= clcf->keepalive_requests) {//请求次数已达上限,默认为100 
            r->keepalive = 0;

        }
    }
}

4、请求处理结束时,根据keepalive标识是否为1,为0则直接关闭与客户端的连接,否则把连接加入到时间事件中,保活该连接,等待下一次请求到来;

ngx_http_finalize_connection(ngx_http_request_t *r)
{
  if (!ngx_terminate
         && !ngx_exiting
         && r->keepalive
         && clcf->keepalive_timeout > 0)
    {
        ngx_http_set_keepalive(r);
        return;
    }
    ngx_http_close_request(r, 0);
}
ngx_http_set_keepalive(r){//下游连接有keepalive机制 直接保活,又重新把下游fd监听起来,保持长连接   
     if (ngx_handle_read_event(rev, 0)//继续监听读写事件
     rev->handler = ngx_http_keepalive_handler;//设置回调函数,如果在keepalive_timeout时间内,有新请求过来,则处理请求,且删除时间事件,继续保活客户端连接
     ngx_add_timer(rev, clcf->keepalive_timeout);//添加到时间事件中,在keepalive_timeout时间后如果被时间事件触发,则直接关闭客户端连接
     }

3、nginx与上游server开启长连接及上游的长连接管理

第一步:针对场景3中的配置进行解析

ngx_http_upstream_keepalive(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
   n = ngx_atoi(value[1].data, value[1].len);//读取keepalive 300的值
   kcf->max_cached = n;
}
ngx_http_upstream_init_keepalive(ngx_conf_t *cf,ngx_http_upstream_srv_conf_t *us)
{
    ngx_conf_init_msec_value(kcf->timeout, 60000);//初始化kcf->timeout,即keepalive_timeout  120s的值
    ngx_conf_init_uint_value(kcf->requests, 100);//初始化kcf->requests,即keepalive_requests 100的值
}

第二步:初始化空闲长连接队列,用于存储空闲的长连接,大小为max_cached,即与场景3中的"keepalive 300;" 配置的值相同

ngx_http_upstream_init_keepalive(ngx_conf_t *cf,ngx_http_upstream_srv_conf_t *us)
{
    for (i = 0; i < kcf->max_cached; i++) {
     ngx_queue_insert_head(&kcf->free, &cached[i].queue);
        cached[i].conf = kcf;
    }
}

第三步:根据场景3中配置,初始化connection_close标识,当http协议低于1.1或Connection值配置为"close",则设置上游连接的keepalive值为0,代码如下:

ngx_http_upstream_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h,ngx_uint_t offset)
{
    r->upstream->headers_in.connection = h;

    if (ngx_strlcasestrn(h->value.data, h->value.data + h->value.len,(u_char *) "close", 5 - 1)!= NULL)
    {
        r->upstream->headers_in.connection_close = 1;
    }

    return NGX_OK;
}
 ngx_http_proxy_process_status_line(ngx_http_request_t *r)
{
if (ctx->status.http_version < NGX_HTTP_VERSION_11) {
        u->headers_in.connection_close = 1;
}
ngx_http_proxy_process_header(ngx_http_request_t *r)
{
 u->keepalive = !u->headers_in.connection_close;//给上游连接的keepalive标记为0;
}

第四步:在上游响应数据接收完后,nginx调用ngx_http_upstream_finalize_request函数释放上、下游的连接,在上游也开启了keepalive的情况下,释放上游连接会执行ngx_http_upstream_free_keepalive_peer函数,此时,若上游连接的keepalive为0 或 上游连接的请求处理次数达到了上限值 或 空闲长连接队列已满,则关闭上游连接,具体代码如下:


ngx_http_upstream_free_keepalive_peer(ngx_peer_connection_t *pc, void *data, ngx_uint_t state)
{
    if (c->requests >= kp->conf->requests) {//请求处理次数已达上限值
        goto invalid;
    }

    if (!u->keepalive) {//上游连接未启用keepalive,场景3中proxy_http_version 1.1;proxy_set_header Connection ""; 决定了此值
        goto invalid;
    }
    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 {
        q = ngx_queue_head(&kp->conf->free);//
        ngx_queue_remove(q);

        item = ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);
    }
    ngx_add_timer(c->read, kp->conf->timeout);//添加到时间事件中,在keepalive_timeout时间后如果被时间事件触发,则直接关闭客户端连接
    c->read->handler = ngx_http_upstream_keepalive_close_handler; //设置回调函数,如果在keepalive_timeout时间内,有新请求过来使用了此连接,则删除时间事件,继续保活上游连接,否则该连接被close
}

nginx的开启长连接会带来什么问题?

nginx上下游针对请求处理的超时时间配置不合理,导致报connection reset by peer问题,即低频502,如图:

image.png

此类问题主要原因为,客户端在对上游长连接fd读写时,正好此fd被上游服务器关闭了,此时会报connection reset by peer,所以需要尽量避免上游服务器主动断开连接;
详见张报写的分析文章:https://note.youdao.com/ynote...

小结

本文是一篇科普文,写这篇文章的缘由是,因为曾经有朋友去面试的时候被问到keepalive是什么,http1.1有什么特性,然后答不上,被刷了,所以写一篇nginx的keepalive科普文章,后续针对nginx的知识点会陆续出一系列的科普文。

团队在大量招聘go工程师,感兴趣可发邮箱:huangtao3@tal.com


AI及LNMPRG研究
7.2k 声望12.8k 粉丝

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人