502 문제를 해결하는 방법은 무엇입니까?

이 글은 너겟 커뮤니티의 첫 사인 글입니다. 14일 이내 전재 금지, 14일 이후 무단 전재 금지, 침해 여부는 반드시 조사해야 합니다!

제가 일을 하고 있을 때 업스트림에 전화를 걸어온 형님이 서비스에서 "502 오류, 가서 이유를 확인하세요"라고 보고한 적이 있습니다.

당시 해당 서비스에 콜로그가 발생했는데, 일반적으로 다양한 200,4xx 상태 코드에 대한 정보를 기록합니다. 그래서 번호 502를 찾기 위해 서비스 로그에 갔지만 아무 것도 찾지 못했습니다. 그래서 동생에게 " 서비스 로그에 502 기록이 없는데, 잘못 알고 계시나요?"라고 물었 습니다.

지금 생각하면 조금 부끄럽네요.

1665968138227

그땐 나와 같은 형이 몇이나 되는지 모르겠는데, 이 글에서는 502뭐가 잘못된 건지 이야기해 볼까요?

상태 코드가 무엇인지부터 시작하겠습니다.


HTTP 상태 코드

우리가 일반적으로 브라우저에서 탐색하는 특정 보물과 특정 정도는 실제로 프론트 엔드 웹 페이지입니다.

일반적으로 프론트엔드는 많은 데이터를 저장하지 않으며 대부분 백엔드 서버에서 데이터를 가져와야 합니다.

따라서 TCP 프로토콜을 통해 프런트 엔드와 백 엔드 사이에 연결을 설정한 다음 TCP 기반으로 데이터를 전송해야 합니다.

그러나 TCP는 데이터 스트림을 기반으로 하는 프로토콜로, 데이터를 전송할 때 각 메시지에 데이터 경계를 추가하지 않으며, Bare TCP를 데이터 전송에 직접 사용하면 "sticky packet" 문제가 발생합니다.

따라서 데이터를 구문 분석하기 위해 특수한 프로토콜 형식을 사용해야 합니다. 그래서 이를 기반으로 HTTP 프로토콜이 설계되었습니다. 자세한 내용 은 앞서 작성한 " HTTP 프로토콜이 있으므로 RPC가 필요한 이유 "를 참조하십시오 .

예를 들어 특정 상품의 특정 정보를 보고 싶다면 실제로 Front End에서 보낸 HTTP 요청 에 전달된 상품의 id와 상품 의 가격, 매장명, 배송지 정보 등이다. 백엔드에서 반환 HTTP 응답 에서 반환됩니다.

ID로 제품 세부 정보 가져오기

이런 식으로 표면적으로는 다양한 웹 페이지를 스와이핑하고 있지만 실제로는 배후에서 많은 HTTP 메시지가 송수신되고 있습니다.

사용자가 온라인에서 제품을 검색합니다.


但问题就来了,上面提到的都是正常情况,如果有异常情况呢,比如前端发的数据,根本就不是个商品id,而是一张图片,这对于后端服务端来说是不可能给出正常响应的,于是就需要设计一套HTTP状态码,用来标识这次HTTP请求响应流程是否正常。通过这个可以影响浏览器的行为。

比方说一切正常,那服务端返回个200状态码,前端收到后,可以放心使用响应的数据。但如果服务端发现客户端发的东西异常,就响应个4xx状态码,意思是这是个客户端的错误,4xx里头的xx可以根据错误的类型,再细分成各种码,比如401是客户端没权限,404是客户端请求了一个根本不存在的网页。反过来,如果是服务器有问题,就返回5xx状态码。

4xx와 5xx의 차이점


但问题就来了。

服务端都有问题了,搞严重点,服务器可能直接就崩溃了,那它还怎么给你返回状态码?

是的,这种情况,服务端是不可能给客户端返回状态码的。所以说,一般情况下5xx的状态码其实并不是服务器返回给客户端的

它们是由网关返回的,常见的网关,比如nginx


nginx的作用

回到前后端交互数据的话题上,如果前端用户少,那后端处理起请求来,游刃有余。但随着用户越来越多,后端服务器受资源限制,cpu或者内存都可能会严重不足,这时候解决方案也很简单,多搞几台一样的服务器,这样就能将这些前端请求均摊给几个服务器,从而提升处理能力。

但要实现这样的效果,前端就得知道后端具体有哪些个服务器,并一一跟他们建立TCP连接。

프런트 엔드와 여러 서버 간의 연결 설정

也不是不行,但就是麻烦。

但这时候如果能有个中间层挡在它们中间就好了,这样客户端只需要跟中间层连接,中间层再和服务器建立连接。

于是,这个中间层就成了这帮服务器的一个代理人一样,客户端有啥事都找代理人,只管发出自己的请求,再由代理人去找某个服务器去完成响应。整个过程下来,客户端只知道自己的请求被代理人帮忙搞定了,但代理人具体找了那个服务器去完成,客户端并不知道,也不需要知道。

像这种,屏蔽掉具体有哪些服务器的代理方式就是所谓的反向代理

역 프록시

反过来,屏蔽掉具体有哪些客户端的代理方式,就是所谓的正向代理。

而这个中间层的角色,一般由nginx这类网关来充当。

另外,由于背后的服务器可能性能配置各不相同,有些4核8G,有些2核4G,nginx能为它们加上不同的访问权重,权重高的多转发点请求,通过这个方式实现不同的负载均衡策略。


nginx返回5xx状态码

有了nginx这一中间层后,客户端从直连服务端,变成客户端直连nginx,再由nginx直连服务端。从一个TCP连接变成两个TCP连接。

于是,当服务器发生异常时,nginx发送给服务器的那条TCP连接就不能正常响应,nginx在得到这一信息后,就会返回5xx错误码给客户端,也就是说5xx的报错,其实是由nginx识别出来,并返回给客户端的,服务端本身,并不会有5xx的日志信息。所以才会出现文章开头的一幕,上游收到了我服务的502报错,但我在自己的服务日志里却搜索不到这一信息。


产生502的常见原因

rfc7231中有关于502错误码的官方解释是

502 Bad Gateway
   The 502 (Bad Gateway) status code indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request.
复制代码

翻译一下就是,502 (Bad Gateway) 状态代码表示服务器在充当网关或代理时,在尝试满足请求时从它访问的入站服务器接收到无效响应。

汝听,人言否?

这对于大部分编程小白来说,不仅没解释到问题,反而只会冒出更多的问号。比如,这上面提到的无效响应到底指的是什么?

我来解释下,它其实是说,502其实是由网关代理(nginx)发出的,是因为网关代理把客户端的请求转发给了服务端,但服务端却发出了无效响应,而这里的无效响应,一般是指TCP的RST报文或四次挥手的FIN报文。

四次挥手估计大家背的很熟了,所以略过,我们来重点说下RST报文是什么。


RST是什么?

我们都知道TCP正常情况下断开连接是用四次挥手,那是正常时候的优雅做法。

异常情况下,收发双方都不一定正常,连挥手这件事本身都可能做不到,所以就需要一个机制去强行关闭连接。

RST 就是用于这种情况,一般用来异常地关闭一个连接。它是TCP包头中的一个标志位,在收到置这个标志位的数据包后,连接就会被关闭,此时接收到 RST的一方,在应用层会看到一个 connection resetconnection refused 的报错。

TCP 헤더 RST 비트

而之所以发出RST报文,一般有两个常见原因


服务端过早断开连接

nginx与服务端之间有一条TCP连接,在nginx将客户端请求转发给服务端时,他两之间按道理会一直保持这条连接,直到服务端将结果正常返回后,再断开连接。

但如果服务端过早断开连接,而nginx却还继续发消息过去,nginx就会收到服务端内核返回的RST报文四次挥手的FIN报文,迫使nginx那边的连接结束。

过早断开连接的原因常见的有两个

第一个是,服务端设置的超时时间过短。不管是用的哪种编程语言,一般都有现成的HTTP库,服务端一般都会有几个timeout参数,比如golang的HTTP服务框架里有个写超时(WriteTimeout),假设设置了2s,那它的含义就是,服务端在收到请求后需要在2s内处理完并将结果写到响应中,如果等不到,就会将连接给断掉。

比如你的接口处理时间是5s,而你的WriteTimeout却只有2s,在没等到响应写完之前,HTTP框架就会主动将连接给断开。nginx此时就有可能收到四次挥手的FIN报文(有些框架也可能发RST报文),然后断开连接,于是客户端就会收到一个502报错。

遇到这种问题,将WriteTimeout的时间调大一些就好了。

FIN과 502의 관계


第二个原因,也是造成502状态码最常见的原因,就是服务端应用进程崩了(crash)。

服务端崩了,也就是当前没有一个进程在监听服务器端口,而此时你却尝试向一个不存在的端口发数据,服务器的linux内核协议栈就会响应一个RST数据包。同样,这时候nginx也会给客户端一个502。

RST 및 502

在开发过程中,这种情况是最常见的

现在我们大部分的服务器都会将挂掉的服务重启,因此我们需要判断下服务是否曾经崩溃过

如果你有对服务端的cpu或者内存做过监控,可以看下CPU或内存的监控图是否出现过断崖式的突然下跌。如果有,十有八九百,就是你的服务端应用程序曾经崩溃过。

cpu가 갑자기 튕김

除此之外你还通过下面的命令,看下进程上次的启动时间是什么时候。

ps -o lstart {pid}
复制代码

比如我要看的进程id是13515,命令就需要像下面这样。

# ps -o lstart 13515
                 STARTED
Wed Aug 31 14:28:53 2022
复制代码

可以看到它上次的启动时间是8月31日,这个时间如果跟你印象中的操作时间有差距,那说明进程可能是崩了之后被重新拉起了。

遇到这种问题,最重要的是找出崩溃的原因,崩溃的原因就多种多样了,比如,对未初始化的内存地址进行写操作,或者内存访问越界(数组arr长度明明只有2,代码却读arr[3])。

这种情况几乎都是程序有代码逻辑问题,崩溃一般也会留下代码堆栈,可以根据堆栈报错去排查问题,修复之后就好了。比如下面这张图是golang的报错堆栈信息,其他语言的也类似。

오류 스택


不打印堆栈的情况

但有一些情况,有时候根本不留下堆栈

比如内存泄露导致进程占用内存越来越多,最后导致超过服务器的最大内存限制,触发OOM(out of memory), 进程直接就被操作系统kill掉。

还有更隐蔽的,代码逻辑里隐藏了主动退出进程的操作。比如golang的日志打印里有个方法叫log.Fatalln(),打印完日志还会顺便执行os.Exit()直接退出进程,对源码不了解的新手很容易犯这个错。

그런데 인쇄 후 프로세스를 종료

如果你很明确,你的服务没有崩过。那继续往下看。


网关将请求打到了一个不存在的IP上

nginx是通过配置的形式来代理多个服务器。这个配置一般是放在 /etc/nginx/nginx.conf 中。

打开它,你可能会看到类似下面这样的信息。

upstream xiaobaidebug.top {
    server 10.14.12.19:9235 weight=2;
    server 10.14.16.13:8145 weight=5;
    server 10.14.12.133:9702 weight=8;
    server 10.14.11.15:7035 weight=10;
}
复制代码

上面配置的含义是,如果客户端访问xiaobaidebug.top域名,nginx就会将客户端的请求转发到下面的4个服务器ip上,ip边上还有个weight权重,权重越高,被转发到的次数就越多。

可以看出,nginx具有相当丰富的配置能力。但要注意的是,这些个文件是需要自己手动配置的。对于服务器少,且不怎么变化的情况,这当然没问题。

但现在已经是云原生时代了,很多公司内部都有自己的云产品,服务自然也会上云。一般来说每次更新服务,都可能会将服务部署到一台新的机器上。而这个ip也会随着改变,难道每发布一次服务,都需要手动去nginx上改配置吗?这显然不现实。

如果能在服务启动时,让服务主动将自己的ip告诉nginx,然后nginx自己生成这样的一个配置并重新加载,那事情就简单多了。

为了实现这样一个服务注册的功能,不少公司都会基于nginx进行二次开发。

但如果这个服务注册功能有问题,比方说服务启动后,新服务没注册上,但老服务已经被销毁了。这时候nginx还将请求打到老服务的IP上,由于老服务所在的机器已经没有这个服务了,所以服务器内核就会响应RST,nginx收到RST后回复502给客户端

인스턴스가 파괴되었지만 구성이 IP를 삭제하지 않았습니다.

要排查这种问题也不难。

这个时候,你可以看下nginx侧是否有打印相关的日志,看下转发的IP端口是否符合预期。

如果不符合预期,可以去找找做这个基础组件的同事,进行一波友好的交流


总结

  • HTTP状态码用来表示响应结果的状态,其中200是正常响应,4xx是客户端错误,5xx是服务端错误。
  • 客户端和服务端之间加入nginx,可以起到反向代理和负载均衡的作用,客户端只管向nginx请求数据,并不关心这个请求具体由哪个服务器来处理。
  • 백엔드 서버 애플리케이션이 충돌하면 nginx는 서버에 액세스할 때 서버에서 반환된 RST 메시지를 수신한 다음 클라이언트에 502 오류를 반환합니다. 502는 서버 응용 프로그램에서 발급한 것이 아니라 nginx에서 발급한 것입니다. 따라서 502가 발생하면 백엔드 서버에 관련 502 로그가 없을 가능성이 높으며 이 502 로그는 nginx 측에서 볼 필요가 있습니다.
  • 502가 발견되면 먼저 서버 응용 프로그램이 충돌하고 다시 시작되었는지 여부를 모니터링하고 확인하십시오. 그렇다면 충돌 스택 로그가 남아 있는지 확인하십시오.로그가 없으면 프로세스를 일으키는 oom 또는 기타 이유가 있는지 확인하십시오. 적극적으로 퇴장.. 프로세스가 충돌하지 않은 경우 nginx 로그를 확인하여 요청이 알 수 없는 IP 포트로 전송되었는지 확인하십시오.

마침내

최근에는 오리지널 업데이트의 독서량이 꾸준히 줄어들고 있는데, 곰곰이 생각하다 밤에 뒤척였다.

미숙한 부탁이 있습니다.


내가 광동을 떠난 지 오랜 시간이 지났고 오랫동안 아무도 나를 Pretty Boy라고 부르지 않았습니다.

댓글창 에 예쁜남자라고 불러주실수 있나요?

내 이런 친절하고 단순한 소원이 이루어질 수 있을까?

정말 말씀드리기 어려우시면 우측 하단 의 팔로우 와 좋아요 + 즐겨찾기 를 눌러주시면 안될까요 ?


그만 말하자 함께 지식의 바다에 질식하자

추천

출처juejin.im/post/7155280646112280613