LB health check 실패.. 삽질기..
로드밸런서 health check를 확인하는데 계속 Error라고 떴습니다.. 이로 인한 삽질 여행기 들어보시렵니까..?
기존에 로드밸런서 하나에 Flask로 구성된 API server 3대를 붙여놨었고, python-alpine 이미지를 이용하여 flask 서버 컨테이너를 구성했었습니다. 포트는 8080 포트를 사용했었습니다. 이 때까지만 해도 로드밸런서를 연결하여 http health check를 잘 하고 있던 상태였어요.
이후 api-server 하나 구성하는데 docker를 이용하는거는 너무 무겁기도 하고, 또.. python-alpine 이미지가 빌드도 오래 걸리고.. 간혹 발생하는 런타임 에러 문제가 있다는 것을 알게되어 도커 사용 대신 systemd 데몬으로 api server를 띄우기로 했습니다. journalctl 로 로그보기도 좋고 같은 기능을 하지만 가볍다는 이유로 사용하게 되었지요..
그런데.. 이게 웬일.. 갑자기 헬스체크가 실패하기 시작했습니다. LB에 연결된 서버들이 전부 Error가 나기 시작하더군요.. 아래처럼요!
그래서 혹시 포트가 제대로 열려있지 않을 수도 있을까? 하여 포트 listen 상태를 확인해보았습니다. 하지만 아래처럼 listen 상태였어요.
지금 여기에는 보이지 않지만 time_wait 상태인 프로세스들이 여러개가 뜨기도 했었는데 그것만으로는 이유를 잘 모르겠더라구요.
TCP time_wait이 제가 알기로는.. tcp 연결을 끊을 때 fin, ack 보내고 그 상대방이 완전히 끊기기를 기다리기 위해 time_wait을 따로 두는 걸로 알고 있거든요.. 근데 이미 잘 응답도 전달해줬으니 큰 문제가 없다 생각했어요.. 그리고.. http 연결도 결국 tcp 연결을 맺으니까 아래 처럼 나오는게 맞다고 생각했습니다.
~# netstat -nap | grep 8080
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 10054/python3
그래서 혹시 docker port binding하는거와 systemd로 port listen하는거는 뭔가 다른가? 하고 찾아봤는데 그건 아닌거 같더라구요.. 또.. 심지어 그런 고민을 하는 사람도 없어서 그런지 자료도 안나왔습니다. ㅎㅎ..
그렇다면 혹시 LB쪽에서 아직 이전에 도커 사용했던거 관련해서 정보를 가지고 있어서 생기는 문제는 아닐까? 하고 생각도 해봤고..
또, tcpdump 를 떠보기까지 해봤어요!
흐름상 틀린게 없다고 생각했습니다. 하지만.. 제가 여기에서 놓치는 부분이 있었죠.. 저는 정말 눈을 뜨고 있지만 아무것도 보지 못하는 바보일지도 모른다고 생각했어요.. 댕청댕청..
정답은... 바로 응답 코드쪽이 좀 다르다는거죠... 아 저걸 왜 못봐서 이렇게 삽질을 했을까요? LB 쪽에 설정된 헬스 체크 부분을 보시면 http 버전이 1.1로 되어있는 것을 알 수 있죠.. 하지만 tcpdump를 떠서 본 응답의 http 버전은 바로 1.0 이었습니다. 그래서 제대로 응답을 주고 받았지만 이걸 제대로 살아있다고 LB는 판단할 수 없고, 그래서 Error를 내뿜는 거였어요..
그럼 또 여기에서 궁금한 점이 분명 같은 서버에서 도커를 띄웠고, 그 다음 systemd 데몬을 띄웠는데 왜 http 버전이 다르게 떴을까? 라는 의문이 들었습니다. 하지만.. 중요한 차이가 있었습니다.
apiserver container의 python 버전은 3.11.1 이었습니다. 그리고 Flask 버전은 2.2.2였어요..
하지만 systemd 데몬을 사용하면서 서버 로컬 환경을 이용했는데요. 이 때의 python 버전은 3.6.9 , Flask 버전은 2.0.3 이었습니다.
python 3.6.9의 최대 Flask 버전은 2.0.3 이어서 pip3 install Flask 명령어로 설치된 버전이 달랐던 것을 알게 되었습니다.
그런데.. Flask 버전이 달라도 http version 설정을 그렇게 다르게 해놨을까? 라는 생각이 들었고 관련해서 확인을 해보았습니다.
Flask 는 WSGI 유틸리티인 Werkzeug를 사용하고 있습니다. Flask 2.2.x 대의 HTTP 프로토콜 버전 디폴트 값이 아래 링크처럼 1.1로 설정되어 있는 것을 볼 수 있었습니다.
handler.protocol_version = "HTTP/1.1"
https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L681
GitHub - pallets/werkzeug: The comprehensive WSGI web application library.
The comprehensive WSGI web application library. Contribute to pallets/werkzeug development by creating an account on GitHub.
github.com
하지만, Flask 2.0.x 대의 HTTP 프로토콜 버전은 달랐습니다. 아래 링크에 달린 함수 일부인데요. 보시면 http request version이 HTTP/0.9가 아니면 기본적으로 설정된 protocol version을 설정하도록 되어 있습니다. 이 self.protocol_version 값이 무엇인지 확인을 해보았더니... 충격..
def send_response(self, code: int, message: t.Optional[str] = None) -> None:
"""Send the response header and log the response code."""
self.log_request(code)
if message is None:
message = self.responses[code][0] if code in self.responses else ""
if self.request_version != "HTTP/0.9":
hdr = f"{self.protocol_version} {code} {message}\r\n"
self.wfile.write(hdr.encode("ascii"))
https://github.com/pallets/werkzeug/blob/2.0.x/src/werkzeug/serving.py#L381
GitHub - pallets/werkzeug: The comprehensive WSGI web application library.
The comprehensive WSGI web application library. Contribute to pallets/werkzeug development by creating an account on GitHub.
github.com
럴수 럴수..
protocol_version = "HTTP/1.0" 로 설정되어 있더라구요..
GitHub - python/cpython: The Python programming language
The Python programming language. Contribute to python/cpython development by creating an account on GitHub.
github.com
응답 프로토콜 버전을 변경 후에 아래와 같이 LB health check 가 성공하는 것을 확인했습니다 얏호!!!
결국 정리하면..
LB error의 이유는 http 응답이 잘못 가서였고 (정확하게는 protovol version), http 응답이 다르게 갔던 이유는 Flask 버전 때문이었다.
Flask 버전 2.0.x 대는 http 1.0 버전을 이용하고 Flask 2.2.x 대는 http 1.1 버전을 사용한다.
입니다.
이를 해결하기 위한 방법은 2가지 정도로 정리할 수 있겠습니다.
1. python 3.6대 버전에서 설치할 수 있는 Flask 최대 버전이 2.0.3 이므로 python 버전을 3.8정도 대로 올려서 Flask 2.2.x 버전을 설치할 수 있도록 합니다.
2. Flask 버전을 그대로 쓰되 아래의 코드를 넣어서 http 응답을 변경합니다.
WSGIRequestHandler.protocol_version = "HTTP/1.1" 이 부분을 추가해주면 protocol version을 변경할 수 있다고 하네요.
from flask import Flask, make_response, Response
from werkzeug.serving import WSGIRequestHandler
from flask import jsonify
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def hello_world():
resp = make_response("{'123':'aaa'}")
return resp
if __name__ == '__main__':
WSGIRequestHandler.protocol_version = "HTTP/1.1"
app.run()
https://stackoverflow.com/questions/55746305/flask-keep-alive-connection-request-failed
Flask Keep Alive Connection Request Failed
I want to crate Keep-Alive http connection, but i failed. I build a demo app. from flask import Flask, make_response, Response from flask import jsonify try: from http.server import
stackoverflow.com
이번에 파보면서 깨달은 거는.. 어떤 버전을 쓸지 정확히 명시해주지 않으면 나중에 라이브러리들 버전 때문에 고생할 수 있겠다는 생각을 했고.. 명시해놓은 파일을 따로 생성해보려고 합니다.
그래도 코드도 보고 재밌었네요 ㅎㅎㅎ 그럼 안녕히 계세요옹