前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TCP?HTTP? 不同类型探测的引发的坑

TCP?HTTP? 不同类型探测的引发的坑

作者头像
richard.xia_志培
发布2022-06-14 14:24:14
8510
发布2022-06-14 14:24:14
举报
文章被收录于专栏:PT运维技术PT运维技术

背景:

nginx-gateway部署在公有云 A, 业务测试服务器部署在办公区机房B, 公有云region A 和 办公区机房 B通过soft V**互连。B机房中有不同类型的应用服务器【nodejs,java(tomcat)】做nginx-gateway的后端upstream节点。nginx-gateway编译安装了ngx_http_upstream_check_module插件,ngx_http_upstream_check_module用于做后端upstream节点的健康监测, healthcheck为每个upstream的后端节点配置有一个raise_counts/fall_couts状态的计数器。业务方同事反馈:从外部访问内部某些应用有概率出现超时, 经观察, nodejs,java(tomcat)的raise_counts计数器概率性地重置为0,

并且概率不一样(前者概率低,后者概率高)。

nodejs的healthcheck配置:

代码语言:javascript
复制
server xx.xxx.xx.xx:yy;
server yyy.yyy.yyy.yyy:xxx;
check interval=3000 fall=5 rise=2 timeout=3000 default_down=true type=tcp;
keepalive 300;

Java的healtchcheck配置:

代码语言:javascript
复制
server x.xx.x.x:y;
server y.y.y.y:xx;
check interval=3000 rise=2 fall=5 timeout=5000 type=http;
check_http_send "GET / HTTP/1.1 Host:xx.xx.com \r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
keepalive 300;

现象:

观察了一段时间后(抽象出2个现象):

1. 办公区机房 B中的nodejs, java服务器过一段时间就会出现raise_count重置为0, nodejs出现的概率比Java应用低。

2. zabbix监控显示网络存在少量icmp丢包的迹象,丢包的时间和nodejs healthcheck raise_counts重置为0的时间并不完全吻合(zabbix icmp ping探测), 但跟java(tomcat) healthcheck raise_counts重置为0的时间较为吻合。

由于先前有过类似的故障:(原因是: 操作系统windows/linux的TCP协议栈实现有所不同:默认TCP RTO不同,导致TCP重传失败无法建连)。 但这次出现的情况不一样, 所以先前的经验并不能迅速定位到问题。

问题的分析和定位:

整个过程,有2个关键点需要确认:

关键点1. healthcheck的tcp/http类型的raise_counts重置为0判断条件是什么?

关键点2. 故障时刻点TCP/http发生了什么?(为啥同机房不同应用有这种现象?为啥nodejs/java和丢包时刻吻合度存在差异?)

关键点1:

在没有梳理代码逻辑前,脑海一直认为healthcheck插件是这样的: 如果是TCP类型的探测,则每个work进程都发起TCP短连接探测upstream后端节点的存活,每个nginx work进程独立工作; 如果是HTTP类型的探测,则是通过upstream keepalive长连接发起的healtcheck。通过看代码,debug, 抓包,发现实际上的情况跟想象中的存在较大差异。healtchcheck代码文件为ngx_http_upstream_check_module.c , 该插件使用了共享内存作为nginx work进程通信ipc手段,共享内存同时用来维护各个后端节点的healtchheck status状态/每个upstream节点和work进程关系/任务更新时间等。 每个upstream 后端节点只能由一个work进程heathcheck探测(第一次随机nginx work来执行healthcheck,如果某个upstream 后端节点较长的没有healchcheck,则由另外的work进程绑定该节点healtchcheck任务)。

nginx work进程通过定时事件触发执行healthcheck任务,各个nginx work进程通过共享内存+锁的方式来保证单个upstream后端节点只有一个nginx work对其探测;每个后端节点都有添加了一个定时器回调,nginx work 通过时间事件触发执行回调函数。

healthcheck的类型有TCP/HTTP/mysql/ssl/ajp等,我们案例中使用的是TCP和HTTP, 先以TCP为例分析,

模块初始化后进入主逻辑:

ngx_http_upstream_check_begin_handler --> ngx_http_upstream_check_connect_handler

主逻辑在ngx_http_upstream_check_connect_handler:

代码语言:javascript
复制
ngx_http_upstream_check_connect_handler(ngx_event_t *event)
{
    ngx_int_t                            rc;
    ngx_connection_t                    *c;
    ngx_http_upstream_check_peer_t      *peer;
    ngx_http_upstream_check_srv_conf_t  *ucscf;
    ##是否收到退出信号,是否需要gracefully exit
    if (ngx_http_upstream_check_need_exit()) {
        return;
    }
    peer = event->data;
    ucscf = peer->conf;
    ###如果有可用的连接
    if (peer->pc.connection != NULL) {
        c = peer->pc.connection;
        if ((rc = ngx_http_upstream_check_peek_one_byte(c)) == NGX_OK) {
            goto upstream_check_connect_done;
        } else {
       ##异常连接,需要关闭
            ngx_close_connection(c);                                                                                                                      
            peer->pc.connection = NULL;                                                                                                                   
        }                                                                                                                                                 
    }                                                                                                                                                     
    ngx_memzero(&peer->pc, sizeof(ngx_peer_connection_t));                                                                                                                                                                                                                                                          
    peer->pc.sockaddr = peer->check_peer_addr->sockaddr;                                                                                                  
    peer->pc.socklen = peer->check_peer_addr->socklen;                                                                                                    
    peer->pc.name = &peer->check_peer_addr->name;                                                                                                                                                                                                                                                                
    peer->pc.get = ngx_event_get_peer;                                                                                                                    
    peer->pc.log = event->log;                                                                                                                            
    peer->pc.log_error = NGX_ERROR_ERR;                                                                                                                                                                                                                                                                          
    peer->pc.cached = 0;                                                                                                                                  
    peer->pc.connection = NULL;                                                                                                                                                                                                                                                                                    
    //没有可用的长连接,则创建新的连接
    rc = ngx_event_connect_peer(&peer->pc);
    //如果创建连接失败,计数器清0
    if (rc == NGX_ERROR || rc == NGX_DECLINED) {                                                                                                          
        ngx_http_upstream_check_status_update(peer, 0);                                                                                                   
        ngx_http_upstream_check_clean_event(peer);                                                                                                        
        return;                                                                                                                                           
    }  
    c = peer->pc.connection;                                                                                                                              
    c->data = peer;                                                                                                                                       
    c->log = peer->pc.log;                                                                                                                                
    c->sendfile = 0;                                                                                                                                      
    c->read->log = c->log;                                                                                                                                
    c->write->log = c->log;                                                                                                                               
    c->pool = peer->pool;                                                                                                                                 
                                                                                                                                                          
upstream_check_connect_done:                                                                                                                              
    peer->state = NGX_HTTP_CHECK_CONNECT_DONE;                                                                                                            
     //配置TCP类型连接的回调函数配置 ---> TCP类型对应的handle:ngx_http_upstream_check_peek_handler
     //读写事件都触发:ngx_http_upstream_check_peek_handler回调函数                                                                                                                                                    
    c->write->handler = peer->send_handler;                                                                                                               
    c->read->handler = peer->recv_handler;                                                                                                                                                                                                                                                                      
    ngx_add_timer(&peer->check_timeout_ev, ucscf->check_timeout);                                                                                         
                                                                                                                                                          
    /* The kqueue's loop interface needs it. */                                                                                                           
    if (rc == NGX_OK) {                                                                                                                                   
        c->write->handler(c->write);                                                                                                                      
    }                                                                                                                                                     
}

}

TCP类型探测的回调函数ngx_http_upstream_check_peek_handler:

代码语言:javascript
复制
ngx_http_upstream_check_peek_handler(ngx_event_t *event)
{
    ngx_connection_t               *c;
    ngx_http_upstream_check_peer_t *peer;
    ####当前work进程的pid和共享内存中的pid是否一致,不一致则退出(由其他nginx work来完成探测)
    if (ngx_http_upstream_check_need_exit()) {
        return;
    }
    c = event->data;
    peer = c->data;
    ####获取已经存在的tcp长连接读取1个字节
    if (ngx_http_upstream_check_peek_one_byte(c) == NGX_OK) {
        ngx_http_upstream_check_status_update(peer, 1);
    } else {
        c->error = 1;
        ##upstream tcp长连接recv读取错误,则重置计数器
        ngx_http_upstream_check_status_update(peer, 0);
    }
    ngx_http_upstream_check_clean_event(peer);
    ngx_http_upstream_check_finish_handler(event);                                                                                                        
}

//读去长连接的1个字节
ngx_http_upstream_check_peek_one_byte(ngx_connection_t *c)
{
    char                            buf[1];
    ngx_int_t                       n;
    ngx_err_t                       err;
    n = recv(c->fd, buf, 1, MSG_PEEK);
    err = ngx_socket_errno;
    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, err,
                   "http check upstream recv(): %i, fd: %d",
                   n, c->fd);
    if (n == 1 || (n == -1 && err == NGX_EAGAIN)) {
        return NGX_OK;
    } else {
        return NGX_ERROR;
    }
}

如果后端upstream节点无可用长连接, 则调用ngx_event_connect_peer创建TCP长连接,判断是否正常。

如果存在可用长连接, 取出连接,读取1个字节,判断是否正常。

综合上面代码逻辑:

TCP类型探测 2种情况计数器清0:

1. 建立新TCP连接失败,计数器清0

2. 可用TCP长连接读取异常(只读取1字节),计数器清0

HTTP类型探测,分析过后, 也是2种情况:

1. 建立新HTTP连接失败,计数器清0

2. check_module keepalive可用长连接中, http send 请求返回的http code不是预期配置中的状态码,计数器清0。

ngx_http_upstream_check_module自己维持/创建长连接, 跟ngx_http_upstream_module的keepalive 长连接没有关系(跟keepalive 300这个配置参数无关)。如果是http探测类型,http长连接还受到

check_keepalive_requests这个参数控制,如果在upstream healtcheck中没有该参数,则使用默认值1,

tcp类型的healthcheck不受check_keepalive_requests影响,能够影响到TCP healthcheck行为的是后端WEB服务器新连接的空闲超时时间(类似nginx client_header_timeout, tomcat connectionTimeout参数), 达到超时时间后端WEB服务器将主动关闭TCP连接,下一次healthcheck探测, 模块会重新创建新的TCP连接。

至此关键点1中的疑问: healthcheck的底层机制和判断条件已经梳理清楚了。

关键点2:

nodejs, java(tomcat) 服务器都在同一区域,同样的系统版本,同样的内核参数, 按照道理,应该不会出现先前案例中由于TCP内核参数差异导致的问题。

当前案例中nodejs和java(tomcat)唯一差异在于TCP和http探测的协议不同。

通过nc/telnet探测出nodejs创建连接后的空闲等待时间为120s

[类似nginx client_header_timeout:60s), java(tomcat)的connectionTimeout时间为20s], 所以nginx-gateway和办公区机房的nodejs每隔120s会重新创建一次TCP长连接。java(tomcat)使用的是http类型探测,由于在upstream中没有显示配置 check_keepalive_requests,则使用该参数的默认值1, 也就是每次建立的连接都需释放,因此,无论在http探测请求头中是否带keepalive/或者指定HTTP 1.1, http探测都会退化为http短连接方式。综上所述: http的探测类型和TCP的探测类型最大的差异在于: tcp探测类型重新新建TCP连接的概率远低于HTTP类型探测

在定位过程中,已经在nginx-gateway, nodejs, java(tomcat)抓了一段时间包,经过仔细对比TCP上下文, 发现了问题所在。先查看centos7当前TCP 重传相关的内核参数:

代码语言:javascript
复制
[root@nginx-gateway0 ~]#sysctl -a 2>/dev/null|grep retri
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 2
net.ipv4.tcp_orphan_retries = 0

以下分析都是参考当前服务器的配置而言的(不是centos7的默认值)。

其中net.ipv4.tcp_retries1,net.ipv4.tcp_retries2 是在认定出错并向网络层提交错误报告之前,重试的最大次数(该参数影响的是长连接)。

在RFC1122中有两个门限R1和R2,当重传次数超过R1的时候,TCP向IP层发送negative advice,指示IP层进行MTU探测、刷新路由等过程,以防止由于网络链路发生变化而导致TCP传输失败。当重传次数超过R2的时候,TCP放弃重传并关闭TCP连接。其中R1和R2也可以表述为时间,

即总重传时间超过R1或者R2的时候触发响应的操作。在linux中对于普通数据报文状态下的TCP,R1对应/proc/sys/net/ipv4/tcp_retries1,R2对应/proc/sys/net/ipv4/tcp_retries2参数。

总重传包遵循指数回退。所以对于已经存在的TCP连接的超时时间至少> 2^0+2^1+2^2+2^3 =15s【不是很精准,参看RFC1122】。(^为指数运算)

所以对于已经存在的TCP长连接可以承受15s时间内多次丢包(15s内完成重传即可)。

对于SYN报文,则是由tcp_syn_retries和tcp_synack_retries这两个参数控制(该参数影响新建tcp连接),tcp_syn_retries为syn的重试次数, tcp_synack_retries为synack的重试次数,遵循指数回退, syn的最大超时时间: 2^0 +2^1=3s, syn_ack的最大超时时间:2^0 +2^1=3s。

所以对于新建的TCP连接承受3秒内的丢包(3秒内完成1次重传即可)

从上面描述,结合抓包的数据分析:

nodejs 针对客户端设置连接超时时间为120s, 故upstream healthcheck创建nodejs的tcp长连接是最大可用时间为120s, java(tomcat)的http healthcheck没有配置check_keepalive_requests, 故healthcheck使用http短连接(每次需要重新建立TCP连接), 由于TCP长连接丢包容忍度远高于新建TCP连接,所以nodejs的raise_counts计数器重置为0的概率远低于JAVA 应用。

过程回溯:

nginx heathcheck维护的TCP长连接(已经存在的),在网络短时丢包的情况下,TCP通过指数回退方式进行重发,由于tcp_retries1/tcp_retries2的默认值较大(次数较多),所以对网络丢包容忍度较高(15s), 所以这个场景下nodejs TCP healthcheck较少概率出现rise_count置0的情况(和zabbix ping丢包时间并不完全吻合的原因)。由于java(tomcat)类型http的healthcheck已经退化为短连接,每次需要建立新连接,在网络状况不好的情况下,失败的概率远高于前者,从而导致java(tomcat) rise_count计数器的值重置为0的概率远大于nodejs应用。

解决办法:

1. 改用带宽相对较为充裕的联通线路作为soft v**线路, 采用网络层openswan ipsec替换当前soft v**软件。(根除途径)

2. 调整java(tomcat)的connecttimeout参数,将探测类型http调整为tcp(缓解途径)

3. 根据自身的业务场景(用户端/服务器端),优化TCP相关参数(缓解途径)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PT运维技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
测试服务
测试服务 WeTest 包括标准兼容测试、专家兼容测试、手游安全测试、远程调试等多款产品,服务于海量腾讯精品游戏,涵盖兼容测试、压力测试、性能测试、安全测试、远程调试等多个方向,立体化安全防护体系,保卫您的信息安全。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档