乐趣区

关于nginx:Nginx-Connection-Reset-问题排查

网校研发部 – 施洪宝

一. 背景介绍

1.1 业务背景

网校服务正在向 K8S 迁徙,咱们有两个服务之前是绑定到一台机器上部署的,二者之间通过 IP 间接拜访,如下图所示,

调用关系非常简单,服务 A 调用了 服务 B ,这里简略阐明下 服务 A 服务 B

  • 服务 A 基于 GolangGin框架开发,应用 Http 长连贯拜访 服务 B
  • 服务 B 基于 C++BRPC开发

咱们想把两个服务进行拆分,通过域名拜访。拆分后,拜访链路变成了下图,

在拆分之后,咱们发现 服务 A 呈现了大量的 connection reset by peer 的谬误,而且这些谬误根本都是集中呈现,呈现的工夫点也没有什么法则,本文是对排查过程的简略总结。

1.2 Tcp Reset 简介

Tcp发送 Reset 包有很多种状况,比如说:服务端的全连贯队列已满,无奈承受新的连贯申请;服务端曾经敞开连贯,客户端依然向其发送数据;服务端没有解决完客户端发送的所有数据。还有很多其余的状况,咱们这里就不再一一列举。本文次要介绍其中的 2 种。

  1. 服务端曾经敞开连贯,客户端依然发送数据,这种状况比拟容易模仿,也比拟容易了解。
  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 8211

int 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 1024

int 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 的一些配置,网关 Nginx1.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 = 100
transport.MaxIdleConnsPerHost = 100
transport.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的长连贯和短连贯各有利弊,咱们要依据本人的理论场景进行抉择。排查问题的时候,多思考,任何一个问题背地都可能有很丰盛的知识点。

退出移动版