网校研发部--施洪宝
一. 背景介绍
1.1 业务背景
网校服务正在向K8S
迁徙,咱们有两个服务之前是绑定到一台机器上部署的,二者之间通过IP
间接拜访,如下图所示,
调用关系非常简单,服务A
调用了服务B
,这里简略阐明下服务A
和服务B
,
服务A
基于Golang
的Gin
框架开发,应用Http
长连贯拜访服务B
服务B
基于C++
的BRPC
开发
咱们想把两个服务进行拆分,通过域名拜访。拆分后,拜访链路变成了下图,
在拆分之后,咱们发现服务A
呈现了大量的connection reset by peer
的谬误,而且这些谬误根本都是集中呈现,呈现的工夫点也没有什么法则,本文是对排查过程的简略总结。
1.2 Tcp Reset简介
Tcp
发送Reset
包有很多种状况,比如说:服务端的全连贯队列已满,无奈承受新的连贯申请;服务端曾经敞开连贯,客户端依然向其发送数据;服务端没有解决完客户端发送的所有数据。还有很多其余的状况,咱们这里就不再一一列举。本文次要介绍其中的2种。
- 服务端曾经敞开连贯,客户端依然发送数据,这种状况比拟容易模仿,也比拟容易了解。
- 咱们这里介绍下,服务端没有解决完客户端数据的状况。对于客户端发送的数据, 服务端应用层没有读取完, 就敞开了连贯, 服务端会发送
Reset
。这里是为什么呢?思考之后,不难发现, 这是Tcp
可靠性的保障,Tcp
须要保障客户端发送的数据, 服务端应用层都能收到,如果服务端应用层没有读取数据,就应该告诉客户端,怎么告诉呢,就是通过Tcp Reset
包。上面给出一个简略的示例程序,
#include <sys/time.h>#include <stdlib.h>#include <stdio.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#define PORT 8211int main(int argc, char** argv){ int send_sk = socket(AF_INET, SOCK_STREAM, 0); if(send_sk == -1) { perror("socket failed "); return -1; } struct sockaddr_in s_addr; socklen_t len = sizeof(s_addr); bzero(&s_addr, sizeof(s_addr)); s_addr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &s_addr.sin_addr); s_addr.sin_port = htons(PORT); if(connect(send_sk, (struct sockaddr*)&s_addr, len) == -1) { perror("connect fail "); return -1; } char pcContent[1028]={0}; write(send_sk, pcContent, 1028); sleep(1); close(send_sk); return 0;}
- 编译
gcc client.c -o client
- 以上是客户端程序, 客户端发送了1028个字节给服务端,期待1s之后,敞开连贯。
#include <sys/time.h>#include <stdlib.h>#include <stdio.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#define PORT 8211#define BACKLOG 10#define MAXRECVLEN 1024int main(int argc, char *argv[]){ char buf[MAXRECVLEN]; int listenfd, connectfd; /* socket descriptors */ struct sockaddr_in server; /* server's address information */ struct sockaddr_in client; /* client's address information */ socklen_t addrlen; if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket() error. Failed to initiate a socket"); exit(1); } /* set socket option */ int opt = SO_REUSEADDR; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1) { perror("Bind() error."); exit(1); } if(listen(listenfd, BACKLOG) == -1) { perror("listen() error. \n"); exit(1); } addrlen = sizeof(client); printf("wait connect\n"); if((connectfd=accept(listenfd,(struct sockaddr *)&client, &addrlen))==-1) { perror("accept() error. \n"); exit(1); } printf("connectfd is %d\n", connectfd); int ans = recv(connectfd, buf, MAXRECVLEN, 0); printf("read data size: %d\n", ans); close(listenfd); /* close listenfd */ return 0;}
- 编译
gcc server.c -o server
- 服务端启动后, 期待客户端的连贯。客户端连贯之后,服务端只接管了1024个字节, 就敞开了连贯。
1.3 Nginx长连贯的一些设置参数
这里给出网关Nginx
的一些配置,网关Nginx
是1.15.8
版本,这里只列了其中一部分配置,
http{ upstream backend{ keepalive 100; #Nginx 1.15.3之后能够设置,这里是默认配置 keepalive_timeout 60s; #Nginx 1.15.3之后能够设置,这里是默认配置 keepalive_requests 100; ... } server{ keepalive_timeout 20s; keepalive_requests 100; ... }}
server
中的keepalive_timeout
, 意思是Nginx
作为服务端,对于客户端的长连贯申请,如果20s内,没有收到新的申请,就会敞开这个连贯。server
中的keepalive_requests
,意思是对于客户端的单个长连贯,最多解决100
个申请,解决完100
个申请后,就敞开这个连贯,不再接管新的申请。upstream
中的keepalive
,意思是这个upstream
最多的闲暇长连接数upstream
中的keepalive_timeout
,意思是Nginx
作为客户端,与upstream
建设长连贯后,如果60s
内没有应用,就敞开这个长连贯,不再应用。upstream
中的keepalive_requests
,意思是Nginx
作为客户端,与upstream
建设的长连贯,单个连贯最多发送100
个申请,超过之后,就敞开连贯。
1.4 Golang服务的长连贯设置
Golang
服务在拜访网关Nginx
时,充当客户端的角色,作为客户端的配置如下,咱们这里只列出其中一部分,
//transport = http.Transport{}transport.MaxIdleConns = 100transport.MaxIdleConnsPerHost = 100transport.IdleConnTimeout = 60 * time.Second//client = http.Client{Timeout: 300 * time.Millisecond}client.Transport = &transport
这里重点讲下transport.IdleConnTimeout
的含意,它的意思是单个长连贯,如果60s
内没有被应用,就不再应用这个长连贯发送申请。
二. 问题排查
服务拆分之后,Golang
服务呈现了大量的connection reset by peer
谬误,很显著,这个谬误是网关Nginx
发送给Golang
服务的。问题的排查能够分为3个阶段,这里咱们一一介绍。
2.1 超时设置
之前始终认为,网关Nginx server
中的keepalive_timeout
设置的是75s
,也就是说Nginx
过了75s
才敞开这个长连贯。问题排查的时候,问了下网关那边的具体配置,通知我是20s
。这时候就想,应该是因为咱们Golang
服务设置的超时工夫是60s
导致的。当单个长连贯超过20s
没有被应用后,Golang
服务认为这个长连贯还能够应用,然而网关Nginx
认为曾经超时,所以Golang
服务再次发送申请,Nginx
会发送Tcp Reset
包。
将transport.IdleConnTimeout
设置为15s
后, 本认为这个问题肯定能解决,然而发现还是会呈现connection reset by peer
。
2.2 长连贯解决申请数设置
批改了超时工夫之后,发现并没有解决connection reset by peer
的问题。之后,又想到是不是Nginx server
中的keepalive_requests
设置导致的。Golang
服务的长连贯,并没有设置单个长连贯能够发送的申请数,然而Nginx
在单个长连贯上,只会解决100
个申请,超过100
申请后,Nginx
会敞开连贯,这时候如果Golang
服务发送了申请,Nginx
就会发送Tcp Reset
包。
之后,想要设置Golang Http Client
的长连贯解决申请数,找了半天,并没有找到哪个配置项能够配置这个值(这里如果有晓得的,能够和我说下)。
没找到配置项后,就想先抓包看下吧,抓到包之后就发现了问题不在这里。
2.3 找到起因并解决
想要设置Golang
服务作为客户端,单个长连贯最多解决的申请数无果后,进行了抓包,抓包结果显示,
- 单个长连贯并没有解决到
100
个申请,Nginx
就发送了Tcp Reset
包。 - 收到
Tcp Reset
包的工夫点,还有很多Tcp Fin
包。
之后,排查网关日志发现,咱们呈现connection reset by peer
谬误的时候,网关也呈现了大量的init_worker()
的谬误。这时候就感觉是网关Nginx Reload
导致的connection reset by peer
。
而后,咱们复现了这个景象,早晨的时候,让网关Nginx
更改配置,Reload
服务,发现咱们果然又呈现了connection reset by peer
的谬误,到这里基本上就定位了问题起因。
咱们思考下,为什么网关Nginx
重启,会导致咱们的服务收到Tcp Reset
包,不是说Nginx
是平滑重启,不影响服务的嘛?思考之后,起因如下,
Nginx Reload
在创立新的Worker
过程之后,须要向老的Worker
发送信号,要求其不再接管新的连贯申请,并解决完以后连贯的申请后,敞开连贯。- 很显著,咱们
Golang
服务是与Nginx
老的Worker
建设的长连贯,当老的Worker
解决完一个申请后,发送后果给Golang
服务,Golang
服务收到后果之后,可能会持续发送申请,然而这个时候,Nginx
老的Worker
可能曾经敞开了连贯,故而发送Tcp Reset
包给Golang
服务。 - 这里留个问题,有没有某种状况是,
Nginx Worker
收到申请后,没有解决就敞开了连贯,进而导致服务端Nginx
发送Tcp Reset
包给客户端?
找到起因之后,就须要解决这个问题。解决方案有两种,
Golang
服务的长连贯改为短连贯,咱们应用的就是这一种Golang
服务持续应用长连贯,当呈现connection reset by peer
谬误后,进行重试。这里须要留神的是,因为Nginx
正在Reload
,大量的长连贯都会生效,所以可能须要重试很屡次。
三. 总结
Nginx
是十分优良的负载平衡、反向代理工具,业界应用的十分宽泛,Nginx
的高性能、模块化设计也被很多软件所模拟。咱们在应用Nginx
的时候,能够多思考,多总结,加深对Nginx
的了解。Http
的长连贯和短连贯各有利弊,咱们要依据本人的理论场景进行抉择。排查问题的时候,多思考,任何一个问题背地都可能有很丰盛的知识点。