关于nginx:nginx的keepalive

10次阅读

共计 11263 个字符,预计需要花费 29 分钟才能阅读完成。

作者:学而思网校 - 黄桃

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

答案是 http1.1 协定是默认开启 keep-alive 的,如图 http1.1 的申请头:

那什么是 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 的探活过程

依据抓包持续剖析 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 的连贯是长连贯

配置

场景 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,如图:

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

小结

本文是一篇科普文,写这篇文章的原因是,因为已经有敌人去面试的时候被问到 keepalive 是什么,http1.1 有什么个性,而后答不上,被刷了,所以写一篇 nginx 的 keepalive 科普文章,后续针对 nginx 的知识点会陆续出一系列的科普文,很多内容是间接看源码总结的,但存在了解谬误的可能,有谬误之处还请间接分割我更正。

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

正文完
 0