[트러블슈팅] 서버를 터뜨린 쟈그마한 함수

개똥이개똥이
4 min read

개발된 기능을 개발서버에서 QA한 후 모든 사항을 수정하고 이상 없음을 확인한 후 운영서버에 배포했다. 당일 할 일을 끝냈기에 맘편하게 배포를 기다리며 코드를 보고 있는 다를 바 없는 하루였다. 수십 분 후 슬랙에 멘션이 여럿 달리기 시작했다. 현재 웹 사이트가 접속이 안된다는 얘기였다. 순간 식은 땀이 나기 시작했다. 무슨 일이지? 분명 개발서버에서는 이상이 없었는데..? 당장 장애가 발생했으니 문제를 파악해야했다. 하지만 나는 서버를 터뜨린 경험이 없었고, 처음 겪는 사태에 머리는 새하얘졌다.

구원자로 등장한 건 백엔드 개발자였다. 여느 때처럼 일하고 있었을 백엔드 개발자에게 갑작스레 떨어진 서버 장애. 동료 개발자로서 미안함 한가득이었다. 머리를 맞대 문제를 파악했고 힌트는 백엔드 개발자가 확인한 로그에서 나왔다. 로그를 통해 장애를 발생시킨 코드를 유추할 수 있었고 빠르게 롤백을 해 큰 이슈없이 사이트를 복구시킬 수 있었다. 식은땀이 흐르는 경험이었다. 날이 덥지 않았음에도.

개발했던 것

당시 개발했던 기능은 포스팅 내 마지막 문단의 링크를 추출해 단순히 하이퍼링크로 되어있던 링크 뭉치 대신 커스텀 컴포넌트를 만드는 것이었다. 언뜻 봤을 때 크게 어렵지 않은 기능이었다. 단순히 마지막 문단(h2)에 속한 링크 뭉치를 추출해내고 이 링크들의 메타데이터를 가지고 링크 유형에 따라 컴포넌트를 만들어내는 것이다.

그래서 생각대로 개발을 하기 시작했다. 원본의 HTML 문자열의 가장 마지막 h2 태그를 확인하고 만약 a태그를 포함하고 있다면 slug에 따라 a서비스 URL 배열과 b서비스 URL 배열 그리고 나머지 HTML 문자열을 반환하는 함수를 개발했다. 그 후 반환된 URL을 스크래핑해 데이터를 반환하고 이를 통해 각 타입별 컴포넌트를 그려주는 크게 복잡할 것 없는 개발사항이었다.

문제의 원인

로컬에서도 개발서버에서도 순조로웠다. 문제는 배포가 된 이후였는데 배포 이후 얼마 안있어 평소의 10배가 넘는 요청이 발생했다. 서버는 인스턴스를 계속 증설시키다가 결국엔 리미트를 맞이하며 장렬히 전사했다.

서버의 방문자 수는 늘지 않았으므로 자연스럽지 않은 상태였다. 시간으로 확인했을 때 개발/운영서버로 배포한 이후였다. 즉 내가 배포한 코드에 문제가 있는 상태로 유추됐다. 개발서버에서 문제가 발생하지 않았다고 생각했기 때문에 이 이슈의 원인을 쉽사리 떠올리기 쉽지 않았다. 그 때 백엔드 개발자가 확인해준 로그가 강력한 실마리가 되었다. 끊임없이 올라오는 metadata-fetcher…. 문제의 원인이었다. 우선 문제가 되는 코드를 모두 롤백했고 문제는 곧 해소되었다. (백엔드 개발자의 피땀눈물로서…)

왜 이런 문제가 생겼을까?

장애가 해소되고나서 곰곰히 생각해봤다. 왜 이런 문제가 발생했을까? 조금 생각해보니 너무 당연한 결과라는 판단이 섰다. 문제가 됐던 스크래핑 함수가 어떤 URL을 참조하는 지를 생각했다. A서비스의 내용에서 URL을 추출하는데, 이 때 추출되는 URL 중 A서비스의 URL이 포함되었다. 즉 A서비스 내에 렌더링되는 커스텀 컴포넌트를 만들기 위해 A서비스의 컨텐츠에서 URL을 추출하는데 이 추출된 URL 역시 A서비스의 URL 이었다. 메타데이터를 확인하기 위해 페이지를 열어보게 되고 열린 페이지는 다시 한번 스크래핑 함수를 실행하게 되며 재귀적으로 무한히 자기참조를 하는 상황이 발생하는 것이었다. 운좋게 아무런 링크가 없는 컨텐츠가 걸릴 때 요청이 조금 줄어들기도 하겠지만 결국 무한히 반복되어 요청되며 서버를 죽여버린 것이다.

문제의 해결책은?

해결을 위해서는 저 무한한 순환고리를 끊어내야했다. 처음에는 스크래핑 함수를 통해서만 해결할 것을 생각했다. 라이브러리를 찾아보기도 했고 갖가지 방어코드로 무장해서 다시 작성하는 것을 방법으로 떠올렸다.

  1. 윈도우 객체를 통해 최초 실행(부모) 외에는 스크래핑 함수가 실행되지 않도록 한다.

  2. 실행플래그를 활용해 실행플래그가 false일 때는 스크래핑 함수가 실행되지 않도록 한다.

직접 만든 스크래핑 함수를 사용하게 될 때는 가장 합리적인 방법 아닐까 생각이 들었다. 결국 문제는 최초 실행 시점 이후에 계속해서 자기참조가 발생하는 것이므로 그 부분만 해결하면 될 일이었다.

하지만 결국 이 방법을 사용하지는 않았다. 이유는 더 좋은 방법을 떠올려서 였는데, 우선 스스로 메타데이터를 활용한다는 생각에만 매몰되어있었다는 점 때문에 떠올리지 않았던 방법이었다. 맞다. 어차피 자사 서비스의 정보를 불러오는 것이므로 url의 slug(or id)를 통해 해당 데이터를 직접 호출하면 될 일이었다.

이는 서버에 대한 부담도 당연스럽게 줄일 수 있고 무한참조에 빠지는 일을 미연에 방지할 수 있으며 구조가 변경이 될 일이 메타데이터에 비해 비교적 적으므로 훨씬 더 합리적인 방법이었다.

어찌보면 키워드에 사로잡혀 눈앞에 있는 것을 좇다보니 더 합리적이고 단순한 방법을 놓친 격이라 살짝 부끄러웠다.

예방을 위해서

본질적인 예방은 당연히도 개발을 “잘”하는 것이다. 하지만 인간은 실수를 하기 마련이고 시야는 자주 좁아질 수 있다. 그렇기에 이런 일이 발생하지 않도록 예방하는 방법을 생각할 필요가 있었다. 몇가지 떠오르는 것은

  1. 시나리오를 다양하게 상상하고 설계해 개발을 하자! 좁은 시야를 넓히는 건 더 많은 생각 뿐이다.

  2. 테스트를 도입해보자! 코드 상에서 방지할 것들이 있다면 방지하자.

  3. 개발서버에서 이상없다고 운영서버에 무조건 이상없다고 생각하지 말자. 아무리 동일한 환경을 최대한 맞춰놨다고 해도 엄연히 다른 환경이다.

  4. 마지막으로 문제가 발생했을 때 최대한 빠르게 대처하자. 문제를 확인하고 로그를 확인하고 상황을 빠르게 대처하자. 더 큰 문제가 발생하는 것을 예방하는 것은 훌륭한 대처다.

마치며…

정말 처음 겪어봤다. 내가 잘해서 처음 겪어본 게 아니라 그냥 운이 좋아서 처음 겪어본 것이었고, 운이 좋아서 좋은 동료들과 함께 빠르게 문제를 해결해낼 수 있었다. 역시 개발은 내가 아닌 모두 함께가 중요하단 것을 다시 한번 깨달았고 장애가 발생했을 때 새하얗게 얼기보단 적극적으로 해결해나가고자 하는 자세가 중요하다는 것을 알았다. 장애는 발생시키지 않는 것도 중요하지만 어떻게 대처하는 지가 정말 중요하다는 것을 깨닫게 된 하루였다.

0
Subscribe to my newsletter

Read articles from 개똥이 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

개똥이
개똥이

어제보다 더 나은 서비스를 만들어내는 사람이 되고자 노력하며, 내일의 나를 위해 기록합니다.