19년 차 개발자가 실무에서 맞닥뜨린 기술 이슈들
F-Lab : 상위 1% 개발자들의 멘토링
📌 글 작성
성장에 관심이 많은 F-Lab 백앤드 멘토 Elkein
스타트업 여럿과 NHN, 넷마블, 크래프톤 등을 거쳤으며
게임 산업, 클라우드 플랫폼 개발, 웹 개발 등을 두루 경험한 19년 차 엔지니어
개요
최근에는 많은 회사가 기술 블로그를 운영하면서, 많은 기술 사례가 공개되고 있다. 이를 통해 많이 배울 수 있고, 많은 성장을 할 수 있는 계기고 되고, 비슷한 장애가 발생했을 때의 힌트가 되기도 한다.
내가 직접 경험했던 몇 가지 기술 이슈를 통해서 개발자가 네트워크, DBMS, 운용 환경 등에 대해서도 왜 알아야 하는지 도움이 될 것이며, 또한 어떠한 내용을 미리 알고 있을 때 다양한 서비스 상황의 이슈를 이해하고 해결하는 데에 도움을 주는지 알아볼 수 있는 계기도 될 수 있을 것이다.
소켓 라이브러리 성능 저하 이슈
2010년 즈음 소켓 라이브러리 개발 중에 발생한 이슈이다. 한데 인접한 버퍼에 여러 스레드가 동시에 액세스 할 때 급격히 느려지는 로그를 발견했다.
- 성능 측정을 위한 profiler
- 자체적인 로그 모두에서 이상 현상이 보였다.
정상적인 상황에서 ms 이하의 시간이 소요되는데 초단위 이상의 시간이 소요되는 현상으로 인해 latency가 발생하고 있었고, 이로 인한 간헐적 지연에 최우선 대응 과제가 되었다. 관련한 상황에 대해서 사내 여러 개발자분들께 물어보게 되었으며, 동료분들과 코드와 로그를 같이 보게 됐다.
그러던 중 한 개발자분이 False Sharing 아니냐며 링크를 던져주셨다.(해당 블로그는 당시 링크는 아니지만 잘 설명된 글이라 가져와보았다.) C++ false sharing이란?(거짓 공유) (tistory.com)
요약하자면 캐시간 데이터 공유를 위해 시간을 소모하는 과정이라고 볼 수 있다. 멀티 스레드간 인접 메모리 접근이 안되게끔 메모리 영역에 패딩을 주어 크게 잡으니 해결되었고, 같은 버퍼에 대해서 접근이 있을 경우에는 순차 접근하도록 sequencial한 동작이 이뤄지게끔 큐로 처리했다. 당시 C++을 이용했기 때문이기도 하지만, 소켓 라이브러리 자체를 직접 구현하게 되면서 컴퓨터 구조나 운영체제에 대한 높은 이해가 필요했다.
CPU에 대한 이해도, 그리고 캐시가 어떤 의미를 가지는지 좀 더 잘 알았어야 했는데, 이러한 부분에 대한 학습이 부족해서 생긴 이슈라고 볼 수 있겠다.
이때 컴퓨터 구조와 운영체제에 대한 높은 이해가 low level 프로그래밍 (운영체제나 메모리를 직접 다루는 쪽에 근접한 작업)을 하는 데에 중요하다고 느끼고 조금 더 학습에 매진했는데, 이러한 부분이 high level 프로그래밍(이미 개발된 기술을 활용하거나, 잘 만들어진 패키지나 라이브러리를 가져다가 비즈니스 로직을 구현하는 일)을 하게 된 이후에도, 각종 장애 현상이나, 동작 원리를 이해하는 데에 큰 도움이 되었다.
나는 low level 프로그래밍 과정에서 니즈를 느꼈지만, high level 프로그래밍을 주로 한다고 해도 컴퓨터 구조나 운영체제에 대한 높은 이해가 큰 도움이 된다.
소켓 서버 앞단에 L4 스위치 도입 시 생겼던 이야기
System Engineer 관점에서 트래픽 폭발을 대응하고자 하는 차원에서 L4 스위치가 점검 시간에 도입이 됐다.
- 네트워크 트래픽 제어는 기존 라우터에서 중간 L4 스위치를 하나 더 거치게 됐다.
- 브로드 캐스팅 대상을 좁히기 위함이라고 생각하면 그럴 수는 있을 것 같았다. 나중에야 알게 된 사실이지만, 개발팀 누구도 L4 스위치 도입 시 검토해야 될 이슈나, 설정값 검토, QA 서버에 선 도입해 볼 생각을 하지 못했고, 그렇게 위험을 감지도 불안정한 상태에서 L4는 도입됐다.
- 도입되자마자 이슈가 터졌고, 커넥션 유실이 두 종류가 발생했다.
디비 커넥션 유실
하나는 디비 커넥션이 끊어지면서, 나약하게 짜여 있던 ODBC로 작성된 코드 중 커넥션 유실 시 재접속 하는 코드에서 무한루프가 돈 것이다. 장애가 나고 나서야 덤프를 떠서 확인하고, 처리했다.
커넥션이 유실 된 이유는 커넥션풀을 이용하고 있었는데 해당 풀에서 꺼내 사용하는 방식이 최근 반환됐던 커넥션 위주로 재 사용되는 큐구조였고 그렇다 보니 트래픽이 지속 유입되고 DB 쿼리를 수행했음에도 큐 끝 쪽에 있는 사용되지 않는 상황이 벌어졌다.
이로 인해 해당 커넥션이 alive check에걸리면서 L4가 유효하지 않은 커넥션으로 판별해 연결을 해제 한 것이다.
이로 인해 당시 ODBC 라이브러리는 범용이 아닌 사내에서 작성된 코드였는데 ODBC의 재접속 시도 처리에 대한 로직에서 while 문의 종료 조건이 잘못되어서 다시 연결에 성공했음에도 루프를 빠져나오지 못하고 무한 루프가 발생해, 시스템 장애로 이어지게 되었다.
사용자와의 커넥션 유실
L4 스위치 자체에서 일정 시간 이상 지난 커넥션을 자동으로 제거했기 때문이다. 제거 원인은 배포된 클라이언트나 서버에서 지정된 KeepAlive 패킷을 주고받는 주기보다 L4 스위치에서의 KeepAlive 유효성 판단 주기가 짧아 발생했다.
발견이 조금 늦은 이슈였는데, 사용자가 액티브한 동작을 하지 않으면 Alive Check 패킷만 주고받게 되는데, 이 경우에만 커넥션이 끊어졌고, 이로 인한 사용자 불편으로 인한 이슈가 발생했다. 그나마 다행인 것은 위 이슈와는 다르게 해당 이슈는 장애로 이어지지는 않았으며, 서비스를 지속적으로 사용하는 유저에게는 발생하지 않는 이슈였다는 점이다.
L4 스위치의 KeepAlive 유효성 판단에 대한 설정값을 늘림으로써 임시적으로 해결됐지만, 과연 이 L4 도입 이슈가 소켓 서버의 경우에도 적절했는가에 대한 생각을 떨칠 수 없었다. 물론 현재는 온프라미스로 구축된 서비스를 잘 하지 않고, 심지어 이것이 레거시 환경이라고 불리기도 한다. 왜냐면 트래픽 폭발에 유연한 대응이 잘 안되기 때문이다.
하지만 이 이슈와 같은 케이스는 L7 혹은 ALB, NLB라고 해도 상황은 달라지지 않는다. 그리고 중요한 것은, 기존 환경에서 변화가 일어날 때 어떠한 영향이 있을지 충분히 검증이 되지 않았다는 점이다. 대부분 기본 옵션으로 도입되었고, 사내에 다른 서비스에 도입된 장비라고 해도 충분한 검토와 검증을 거쳐야 된다는 점, 그리고 소켓 서버의 특수성을 이해하는 것, 웹 서버가 자체적으로 해주는 작업 (Keep Alive), Stateless 기반의 환경의 유연함 등이 주는 차이도 체크했어야 했다.
DB 실행 계획의 함정
2010년쯤이었다. 갑자기 올라오는 데드락 로그 및 각종 쿼리 지연, API 응답 속도 지연 로그로 메신저 오류 로그 채널이 범람했다. 모든 쿼리는 검수를 했고, 실행 계획을 주기적으로 확인했다.
EXPLAIN SELECT * FROM customers WHERE customer_id = 1;
문제가 된 쿼리들을 들여다봤다. 실행 계획상에는 전혀 문제가 없었다. 쿼리 실행 계획과 오차가 크고, 조회하는 데이터 범위가 커서 테이블 풀 스캔이 일어났고, 이로 인해 테이블 잠금이 일어나면서 연관된 쿼리들도 같이 느려짐을 알 수 있었다.
실행 계획과 실제 쿼리 수행이 다를 수 있다는 것을 알게 됐다. 실제 계획을 맹신하면 안 된다. 결국 다양한 명령어를 통해서 DB의 상황을 파악해야하고, 슬로우 쿼리를 지속적으로 추적해야 한다.
SHOW STATUS
SHOW STATUS LIKE 'Handler%';
SHOW STATUS 명령어는 MySQL 서버의 상태 정보를 보여줍니다. 이 중에서 일부는 쿼리 실행에 대한 정보를 제공한다. 예를 들어, 아래 명령을 사용하면 인덱스 레코드의 개수, 테이블 풀 스캔 횟수 등을 확인할 수 있다.
MySQL에서는 쿼리 실행 로그를 통해 쿼리 실행에 대한 상세한 정보를 확인할 수 있다. 쿼리 실행 로그는 MySQL 서버의 설정 파일에서 로그 레벨을 설정하거나, 쿼리 실행 시 명령어 옵션을 사용하여 활성화할 수 있다.
*slow query log**는 쿼리 실행 시간이 지정한 값보다 오래 걸린 경우 해당 쿼리를 로그로 남기는 기능이다. 이 로그는 long_query_time 설정값 이상의 시간이 소요된 쿼리에 대한 정보를 기록하며,
slow_query_log_file 설정값에 지정한 파일에 기록된다. 이 로그는 쿼리의 실행 계획과 함께 실제 쿼리 실행 결과도 기록하므로, 실행 계획과 실제 동작의 차이점을 확인하는 데 유용하다.
또한, general query log는 MySQL 서버에서 발생하는 모든 쿼리 실행에 대한 로그를 남기는 기능이다. 이 로그는 general_log_file 설정값에 지정한 파일에 기록된다. general query log는 모든 쿼리에 대한 로그를 기록하기 때문에 로그 파일 크기가 매우 커질 수 있으므로, 필요한 경우에만 활성화하고 사용하는 것이 좋다.
이 외에도 MySQL에서는 performance_schema라는 내장된 통계 정보를 제공한다.
performance_schema를 사용하면 데이터베이스의 성능 및 활동에 대한 다양한 통계 정보를 조회할 수 있다. performance_schema는 MySQL 서버의 설정 파일에서 활성화할 수 있으며,
performance_schema를 사용하여 쿼리 실행에 대한 상세한 통계 정보를 조회할 수 있다.
DB 처리 속도 급감 이슈
위 이슈가 있고 몇 년 후인 2014년 초의 이야기다.
몇 번의 장애를 겪고 나니, 장애에 대한 우려가 커서 내부 테스트 서버에서 다양한 스트레스 테스트를 진행했다. 물론 DBMS도 내부에 설치했고, DB를 이용하는 로직도 검증하면서 DBMS 쓰루풋도 측정했다.
이렇게 검증된 상태에서 Production 서비스를 시작하게 되는데…
서비스에 트래픽이 몰리면서 다양한 쿼리가 느려지게 된다.
테이블 구조나, 쿼리 방식을 보았을 때 전혀 문제가 될 것이 없는데 지연 로그가 있으므로, 어떻게든 조금 더 성능이 빠른 방식으로 바꾸기 시작했다. 정규화를 포기하고 최대한 Key-Value DB처럼 사용하는 방식으로 바꾼다든지, 집계 연산을 replica로 대체하는 처리를 빠르게 처리한다든지, 캐시 히트율을 높이기 위해 cache evict 조건을 섬세하게 조정한다든지 등으로 수정해나가기 시작했다.
개선책들이 유효한 것처럼 보이면서 지연 및 쿼리 실행 실패, 슬로우 쿼리 로그가 줄어드는 듯 보였으나 얼마 가지 않아 다른 쿼리에 대한 로그도 보이기 시작했다. 이러한 반복에도 계속 다른 로그가 보이며, 결국 DB 성능이 서비스에 영향을 준다는 것을 확인할 수 있었다.
Cloud Vendor에서 제공해 주던 DBMS Managed 서비스를 이용하던 상황이었으므로, 이에 대한 Vendor의 지원을 받고자 시도했는데, 이 과정에서 IOPS 임계치를 초과했다는 것은 알 수 있었다.
우리는 IOPS (입출력 작업 수 제한)을 상향하면서 다양한 시도를 해보았지만, 정확히 어떠한 자원쪽이 문제가 있는 것인지 파악할 수 없었다.
DISK인지, CPU인지, RAM인지
당시 Cloud Vendor의 서포트 담당해 주시던 분도 IOPS 자체가 넘친 것은 알 수 있지만, 어떠한 자원이 영향을 크게 준 것인지는 알기 어렵다고 했다.
RDBMS의 특성상 Disk를 의심해 Disk에 대한 IO만 높이고 싶었지만, 당시 Cloud Vendor에서는 IOPS의 전체 단위만 높일 수 있다는 답변만 받게 됐다. 성능 문제인 것은 확실해진 상황에서 Disk가 병목인지에 대한 확신(로그)도 필요했고, 성능을 부분적으로 높이기 위해서는 DBMS Managed 서비스가 아닌 컴퓨팅 서비스에 올려야 한다는 점이 아쉽긴 했으나, 온 프라미스에서의 다양한 측정 도구를 이용할 수 있다는 점이 유효했다.
결국 서비스를 컴퓨팅 서비스에 올리고 난 뒤, 쿼리가 지연이 있던 상황과 유사한 스펙에서 현상을 재현 한 뒤, Disk Queue Length를 확인한 뒤, Disk IO가 밀린다는 것을 확인했고, Disk 성능을 높여서 문제를 해결할 수 있었다.
주요 쿼리가 필요한 자원, 사용된 DBMS의 종류, 사용자 시나리오에 대한 부하 테스트 등이 이루어졌음에도 실제 운용될 DBMS Managed 환경에서의 처리량 검증 (내부 테스트 서버에서 테스트했던 시나리오를 그대로 테스트만 했어도 훨씬 빨리 발견이 가능했을 것이다), 그리고 Managed 환경에서의 제한적인 설정 (제공해 주는 것만 가능한 상황), IOPS가 임계치에 달했을 때 어떠한 현상이 벌어지며, 서버 로그, 앱의 동작에서 어떠한 현상들이 벌어지는지에 대한 검증이 미리 되었으면 좋았을 것이다.
배포 자동화 시스템 오류
배포 자동화 플랫폼 관련 이슈였다.
사내 서버를 수십~수백 대를 자동화 배포를 지원하는 도구였는데, 이 과정에서 특정 몇몇 서버에서만 실패가 나는 것을 확인했다. 아쉽게도 이 과정은 플랫폼을 사용하는 서비스 담당자께서, 몇몇 서버가 패치가 안되었다는 것을 30분 이상 지난 후에 확인하고 요청 주셨다. 점검 중인 만큼 시간이 한정되어 있었는데, 이로 인해 점검 연장했다.
해당 몇몇 개 서버의 공통분모를 찾지 못했다. 실패 로그도 없었고 정확히는 시도 로그도 제대로 잡히지 않았다.
메시지가 전달 안된 것도 있었는데, 메시징으로 사용된 HTTP 호출 로그가 당시엔 집계되지 않고 있었다. 이벤트 로그, 감사 (Audit) 로그가 없었던 것이다. 해당 응답 Log를 보강하고 재배포해 보았으나, 웹 호출이 들어오지 않았다. 처음부터 안 들어온 것은 아니고, 해당 점검 날짜부터 해당 서버들로 메시지가 전달 안되었다. endpoint 정보의 변경 이력을 살펴봤는데, 정상적으로 동작 한 날 이후 없었고 배포 서비스의 코드 변경 이력 또한 확인해 봤는데 변경 이력이 없었다.
다른 메시징 기능도 오류가 있는지 확인했다.
이 과정에서 다른 것은 IP와 도메인 (내부 도메인) 도메인이 아닌 IP로 endpoint 정보를 바꾸고 배포 시도를 하니 정상 동작했다. 네트워크 플랫폼에서 DNS 정보가 일부 날라가서, DNS를 복구했는데 이 과정에서 누락 정보가 발생했던 것인데 이런 경우는 원래 찾기 어렵긴 하다.
하지만, 기본적은 heartbeat 기능이 동작하고, 연결되어 있는 서비스 간 실시간 모니터링이 됐었으면 이런 이슈가 발생하지 않았을 것이다.
또한 로그도 부족했다. 언제든 이벤트 로그나 감사 로그를 수집해, 디버깅에 활용할 수 있어야 한다. 상시 기록이 어렵다면 최소한 오류가 발생한 시점 이후에라도 빠른 확인이 되게끔 기록되어야 한다.
마지막으로 웹 호출 실패 로그도 부족했다. 웹 호출이 실패가 나고, 해당 HttpStatus 코드가 잘 기록되고 확인되었다면 연결 이슈라는 것과 문제가 있다는 것을 좀 더 빠르게 캐치 할 수 있다.
마치며
내 기준에서는 프로그래밍 언어나, 프레임워크에서 발생한 이슈에 대한 경험보다 DBMS, 네트워크, 운용 환경 등에서 이슈가 생겼을 때가 더 기억에 남는다. 왜냐하면 내 경우에 주니어고 서비스 경험이 적었을 땐 이러한 부분의 중요성을 잘 몰랐기 때문에 더욱 더 충격이 컸고, 그 니즈를 몸으로 느꼈다고 볼 수 있다.
만약 이러한 이해도가 필요하다는 것을 미리 알았다면 이슈를 더 빨리 더 잘 해결할 수 있지 않았을까?
또 개발자가 알아야 될 범주가 넓다는 것을 알고 학습하게 된다면, 조금 더 잠재력이 큰 개발자가 될 수 있지 않을까?
이 컨텐츠는 F-Lab의 고유 자산으로 상업적인 목적의 복사 및 배포를 금합니다.