作者:学而思网校-黄桃
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_time300[***@*** ~]$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl75[***@*** ~]$ cat /proc/sys/net/ipv4/tcp_keepalive_probes9
应用层应用示例:
- int keepalive = 1; // 开启keepalive属性
- int keepidle = 60; // 如该连贯在60秒内没有任何数据往来,则进行探测
- int keepinterval = 5; // 探测时发包的工夫距离为5 秒
- int keepcount = 3; // 探测尝试的次数。如果第1次探测包就收到响应了,则后2次的不再发。并且清零该计数
- setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive , sizeof(keepalive ));
- setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepidle , sizeof(keepidle ));
- setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepinterval , sizeof(keepinterval ));
- 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 b30020 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进制为:584421f 7c //目标端口号16bit 10进制为 : 806032 7e 7a cb // req字段 序列号32bit 10进制为 : 4c bc 55 08 // ack字段 确认号32bit5 // 前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 000010 00 34 47 28 40 00 36 06 ef 9c 0a b3 14 bd ac 19 0020 42 76 // 后面数据不解读1f 7ce4 4a4c bc 55 0832 7e 7a cc8// TCP头长度为8 * 4 = 32 除了头部 还有 选项数据 12字节0 10 // 中6位:保留;后6位:标记位 10 代表倒数第5位为1, 标识该tcp包为 ACK 确认包 0030 01 3f //win字段 窗口大小16bit4e 0d // 测验和16bit00 00 // 紧急数据偏移量16bit01 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.comConnection: 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