YAML 파일 하나로 발생한 서버 장애

Hyunta·2024년 1월 21일
0

장애 발생 배경

최근 저희 팀에서는 예상치 못한 배포 이슈를 만났습니다. 배포 과정 자체는 CI 툴을 통해 문제없이 진행되었음에도 불구하고, 실제 웹에 접속하려 하면 502 Bad Gateway 가 발생했습니다.

이 문제를 해결하기 위해 배포를 재시도하였지만, 오히려 상황은 더 악화되었습니다. 배포는 실패하였고, CPU 사용률은 갑자기 100%로 치솟아버렸습니다.

다행히 이 문제는 Dev 서버에서만 발생하여 실서버에는 전혀 영향을 주지 않았지만, 이로 인해 Dev 환경이 필요한 테스트 진행이 전부 중단되어야 했습니다.

해결 과정

저희는 먼저 nginx 환경을 검토하였습니다. AWS ElasticBeanstalk에서는 nginx를 이용하여 healthCheck를 진행하고 있었는데, 이 과정에서 지속적으로 50x 오류 응답이 발생하였습니다. 이로 인해 웹서버의 복구 과정이 끊임없이 반복되었습니다. 저희 팀은 RubyOnRails 프레임워크, Bundle 버전 관리 도구, 그리고 Puma 웹서버를 사용하여 서버를 운영하고 있었습니다.

첫번째 이슈

CPU 사용률이 급증하여 Puma를 종료하려고 시도하였지만, ElasticBeanstalk에서 계속해서 복구 작업을 시도하여 끝내 Puma를 종료하지 못하였습니다. 이 문제는 서버를 재시작함으로써 해결할 수 있었습니다. 서버를 재시작한 후 로그를 확인해보니 parse 에러가 발생하고 있었습니다.

ruby/2.7.0/psych.rb:456:in `parse': (<unknown>): found character that cannot start any token while scanning for the next token at line 4 column 13 (Psych::SyntaxError)

Psych는 Ruby 1.9.2 버전부터 기본적으로 포함된 라이브러리로, YAML 파서 및 에미터의 역할을 합니다. 이를 통해 YAML 파일 파싱 과정에서 문제가 발생했음을 알 수 있었습니다. 장애가 발생한 시점의 git commit 내역을 확인해보니, 제가 YAML 파일에 추가한 내용이 있었습니다. 이 내용은 환경변수를 읽어오는 부분이었습니다.

NAME = <%= ENV["VALUE"]>

이 부분에서 값을 환경변수를 통해 주입받지 않고 직접 값을 입력한 상태로 배포하였더니 새로운 에러가 발생했습니다.

두번째 이슈

ruby/gems/2.7.0/gems/zeitwerk-2.5.4/lib/zeitwerk/loader/callbacks.rb:25:in `on_file_autoloaded'

Zeitwerk는 모듈 및 클래스 로딩 라이브러리입니다. RubyOnRails6에서는 기본 코드 로더로 사용하고 있습니다. require로 작성하는 부분을 자동으로 로드해주기 때문에 개발자가 수동으로 작성하지 않아도 됩니다. 개발 중에 코드를 변경하더라도 해당 라이브러리가 자동으로 reload하기 때문에 피드백 루프가 빨라집니다.

해당 문제에는 자세한 로그가 남지 않아서 파악하는데 어려웠습니다. 로컬 환경에서 dev 환경변수를 주입하고 dev 환경으로 서버를 실행시켜보니 더 자세한 로그를 확인할 수 있었습니다.

expected file server/app/errors/message_not_found_error.rb to define constant Error::MessageNotFoundError, but didn't (Zeitwerk::NameError)

message_not_found_error 파일에 클래스를 찾지 못해서 발생하는 에러였습니다. 확인해보니 해당 파일에 클래스 이름이 파일명과 다르게 작성되어 있었습니다. 이는 제가 인텔리제이의 리팩터링 기능을 사용하면서 자동으로 변경되었는데 확인을 못해서 발생한 문제였습니다.

클래스 이름을 올바르게 변경하고 다시 배포를 해보니 문제없이 정상적으로 배포가 되는 것을 확인할 수 있었습니다.

원인 분석

YAML Parse 에러

YAML에 추가한 항목이 5개정도 있었는데, 하나씩 확인해보면서 문제가 발생하는 부분을 찾았습니다. 문제가 되는 부분은 @Name 이런식으로 입력을 받아야하는 부분에서 문제가 발생했습니다.

YAML에도 버전이 존재하는데요, 2001년에 첫 1.0 버전이 출시되었고, 이후 2005년에 1.1 버전이, 2009년에는 1.2 버전이 출시되었습니다. 그리고 2021년 10월에는 12년 만에 1.2.2 버전이 나와 일부 에러를 개선하였습니다.

Java 5에서는 SnakeYAML이 YAML 1.1을 지원하고, Java 8 이상에서는 YAML 1.2를 지원합니다.
Ruby의 Psych 라이브러리는 Ruby 2.x에서 YAML 1.1을, Ruby 3.x에서는 YAML 1.2를 지원합니다.
저희는 Ruby 2.x를 사용하고 있으므로 YAML 1.1 버전을 사용하고 있었습니다.

YAML에서 @ 문자는 예약어로, 특별한 기능이나 용도는 없지만, 향후 버전에서 사용될 수 있도록 예약되어 있습니다. 최신 버전인 1.2.2에서도 여전히 예약어로 남아 있습니다. 따라서 @ 문자를 문자열로 사용하려면 따옴표로 묶어 사용하는 것이 바람직합니다.

<출처: https://yaml.org/spec/1.1/>

NAME = "<%= ENV["VALUE"]>"

위와 같이 코드를 수정하면, ENV["VALUE"] 를 통해 읽어온 환경 변수 값이 @NAME이지만, 밖에 큰따옴표로 감싸 있기 때문에 "@NAME" 즉, 문자열로 인식하게 됩니다. 이렇게 수정함으로써 원하는 결과를 얻을 수 있었습니다.

클래스 로딩 에러

클래스 이름을 바꾸면서 인텔리제이의 리팩터링 기능을 사용했는데, 관련된 이름을 전부 바꿔버리는 바람에 클래스가 존재하지 않게되어 발생한 문제였습니다.
클래스 이름을 변경해주니 해결됐습니다.

반성 및 개선 방향

우선, YAML에 버전이 존재하는지도 잘 몰랐고 Ruby에서 어떻게 Parsing이 이뤄지는지 몰랐습니다. 이번 기회를 통해서 Ruby, Psych, Puma, Zeitwerk 등 Rails 환경에 필요한 라이브러리들을 공부할 수 있는 계기가 되었습니다. 비슷한 문제가 일어나지 않도록 YAML에 환경변수를 주입받을 때는 항상 String을 주입하는 형태를 취하도록 해야겠습니다. 그리고 YAML 파일을 관리할 때 String의 값들은 모두 작은 따옴표로 감싸서 문자열 그자체로 바라볼 수 있도록 구성해놔야할 것 같습니다.

profile
세상을 아름답게!

0개의 댓글