2023. 4. 5. 00:42ㆍ기술적 이슈 정리
예전에 취업 준비를 하던 시절, 개발 선배 혹은 면접에서 자꾸 CS의 중요성을 강조하다보니, 그냥 하라니까 하는.. 다소 막연한 이유로, CS 공부를 병행했었다. 그런데, 이번에 실무를 진행하면서, 원인을 알 수 없는 버그를 고치기 위해, 여러 가지 시나리오를 가정하고 해결해 가는 경험을 갖게 되었다.
이 과정에서 CS에 대한 이해도의 중요성을 새삼 깨닫게 되어, 이렇게 글로 정리해보기로 한다.
1. 상황
사내 프로젝트로 제2 금융권 커머스 앱에 적용할 NFT 마켓 api 개발중, 이더리움 노드 모니터링 에이전트가 제대로 동작하지 않는 이슈가 발생했다. 개발 환경에서 돌아가는 노드는 총 4개로 각 노드별로 서버의 리소스 점유율 및 블록체인 정보를 모니터링하는 에이전트가 설치되어 있다.
정상적이라면 총 4개 노드의 블록체인 정보를 가져다주어야 하는데, 4개 중에 단 1개의 노드의 정보만 모니터링하고 있는 상황이었다.
또한, 노드별 발생하는 트랜잭션에 대한 정보를 관계형 데이터베이스에 insert를 하는 기능도 제대로 동작하고 있지 않다. 일반적으로는 에러로그를 보고 분석하면 열에 아홉은 해결되는데, 이번 이슈의 경우.. 콘솔창은 암흑 그 자체였다.. 마치 깜깜해진 내 눈처럼..

2. 이슈 시나리오 선정하기
에러로그가 전혀 없었기 때문에, 어쩔 수 없이 해당 이슈가 발생할 수 있는 모든 상황을 가정해야 했는데, 총 3가지를 가정해볼 수 있었다.
- 해당 에이전트는 실행시, 바라보고 있는 이더리움 네트워크에 쌓인 블록을 제일 첫 번째부터, 최근 블록까지 상태를 조회하고 나서, 주요 리소스 모니터링을 시작한다. 사내 개발용 이더리움 네트워크는 프라이빗으로 운영되고 있는데, 블록을 쌓는 속도가 빨라서, 에이전트의 조회 속도가 이더리움 블록을 쌓는 주기를 쫓아가지 못하고 있을지도 모른다.
- NFT 마켓플레이스의 아키텍처 정의서에 따르면, 모든 api 서버 및 노드 서버는 전부 L4 스위치 장비에 의해 로드밸런싱이 되어있는데, 여기에 sticky session 정책이 적용되어, 모니터링 에이전트가 단 하나의 노드 서버만 바라보는 현상이 생겼을 것이다.
- 동작하고 있는 노드 서버가 어떤 함수를 실행하는 과정에서 스레드를 점유하고 락을 풀어주질 않아서, 나머지 노드 서버가 동작을 못하고 있는건 아닐까?
이렇게 대략 3가지의 시나리오를 구성해보고 하나씩 차근차근 살펴보았다.
2.1. 블록을 쌓는 속도와 블록을 조회하는 속도의 차이
사실 이 부분은 시나리오를 가정하자마자 기각하였다.
에이전트가 블록을 조회하는 기능은 모든 에이전트가 최초 실행시 수행하는 기능이다. 따라서, 조회 속도로 인해 문제가 발생했다면, 모든 에이전트에 동일한 현상이 일어나야 하는데, 단 하나의 에이전트는 정상적으로 수행하는 것은 적어도 하나는 블록을 전부 조회하고 다음 기능을 수행하고 있다는 뜻이다. 따라서, 해당 시나리오는 현재 이슈의 원인이 아니라 판단하였다.
2.2. L4 스위치의 세션 관리 정책으로 인해 모니터링 결과가 하나의 서버만 조회되는 경우
결론부터 이야기하면, 이 경우도 이번 이슈의 원인은 아니었다.
우선 dev 환경에서는 L4 스위치 자체를 적용하지 않았기 때문에, 로드 밸런서의 세션 관리정책을 고려할 이유가 없다.
게다가 설령 L4 스위치가 적용되어 있어도, 에이전트는 별도의 네트워크를 타지 않고, 노드가 설치된 동일한 서버에 동작하여, localhost로 노드를 모니터링하고 있기 때문에, 모니터링 결과가 클라이언트 엔드포인트에 반영이 안 되더라도, 세션 관리 정책에 영향을 받지 않기 때문에, 에이전트 자체는 잘 동작하고 있어야 한다.
2.3. 동시성 이슈
위의 2가지 경우를 겪고나서, 모든 에이전트를 끄고 하나씩 번갈아가며 실행을 해보았는데, 단 하나의 에이전트만 실행하는 경우에는 전부 정상적으로 동작하고 있었다.
이러한 상황을 보고, 이번 이슈는 스레드 간의 경합으로 인해 발생한 문제라고 판단할 수 있었다.
그런데, 스레드가 관여하는 분야가 에이전트 내부에서 관리하는 스레드만의 문제인지 아니면, 에이전트와 통신하는 DB에서 관리하는 스레드와의 문제인지 구분할 필요가 있어, 여기에 시나리오를 2개로 세분화해보았다.
2.3.1. case1: 애플리케이션 스레드 문제
에이전트는 GO 언어로 구성되어 있는데, 해당 소스 코드를 분석해 보았을 때, 세션 별로 스레드를 따로 생성하여 관리하고 있었다. 이러한 부분이 스레드의 경합을 일으킬 수 있겠지만, 생성된 스레드 간의 애플리케이션 자체의 자원을 공유하는 코드(JAVA로 치면 가변적인 변수를 클래스 단위로 공유하는 경우)는 없었다. 그래서 마치 tomcat was처럼 요청 별로 스레드를 생성하고 관리하듯이, 각 스레드는 대체로 무상태 혹은 불변의 공유 자원을 활용하여, 경합이 웬만하면 발생하지 않아서 마치 단일 스레드 애플리케이션처럼 관리할 수 있게 구성되어 있었다.
물론, 이렇게 이론적으로만 살펴보는 것은 크게 의미가 없으니, 사내에서 관리하는 NCP를 활용하여, 진행 중인 프로젝트랑 동일한 환경의 아키텍처를 구성하여, 동작을 시켜보았다.
그런데, 이 경우에는 이슈와 동일한 현상은 전혀 일어나지 않고, 모든 게 제대로 동작하고 있었다.
이러한 현상을 보고, 우선은 에이전트 자체에는 스레드 경합 관련해서는 이슈가 없는 것으로 판단하고 바로 다음 시나리오를 가정하여 진행해 보았다.
2.3.2. case2: DBMS 스레드 문제
사내 솔루션의 아키텍처에 의하면, 아무리 여러 에이전트를 설치하더라도, 같은 네트워크에서 동작한다면, 단 하나의 DBMS를 의존하도록 설계가 되어있다.
따라서, 아키텍처의 관점에서 봤을 때, 에이전트 4개가 관여하는 공유자원은 'DBMS'이고, 'DBMS'에서 관리하는 스레드, 즉 트랜잭션 처리와 관련된 문제가 아닐까 하는 가정을 시작으로 디버깅을 진행하였다. 먼저 insert를 하는 과정에서 스레드를 관리하는 과정을 되짚어보면...
innoDB 기반의 RDBMS에서 insert가 발생할 경우 해당 쿼리를 수행하기 위해 추가하고자 하는 row에 exclusive lock을 건다.
만약 여러 insert 쿼리가 발생했는데, 각 쿼리로 인해 추가되는 레코드의 인덱스가 중복이 안된다면, 서로를 기다리지 않게 하기 위해, table level lock의 일종인 insert intention gap lock 을 먼저 걸어둔다.
만약에 중복된 인덱스의 레코드가 추가되려고 하면, 가장 먼저 수행하는 세션의 insert 쿼리에 먼저 exclusive lock을 걸고, 나머지 세션에는 shared lock을 걸어둔다.
그리고 exclusive lock의 insert 구문이 처리되고 커밋을 하게 된다면, 나머지 shared lock이 걸린 세션에는 duplicated key 에러를 던진다.
에이전트에서는 동시성 제어를 편하게 하기위해, pk를 블록번호로 지정하고 있고 해당 insert 쿼리를 1초에 한 번씩 수행하고 있다. 따라서 만약 DB와의 세션연결 및 db 트랜잭션 처리가 제대로 처리되었다면, db에는 이더리움 내에서 발생한 모든 트랜잭션이 기록되어 있어야 한다. 그런데 db에 해당 테이블을 조회하였지만, 단 하나의 레코드도 찾아볼 수 없었다.
이 현상의 원인을 파악하기 위해, dbms 서버에 접속하여, 트랜잭션 상태를 모니터링하고 있었는데 알고 보니, 트랜잭션은 발생하는데 커밋을 전혀 수행하고 있지 않았다.
혹시나 해서 DBMS의 기본 옵션값들을 살펴보니, NFT 마켓플레이스 프로젝트는 금융권에 속하여서, 데이터의 정합성을 강화하기 위해 auto-commit을 false로 지정해두고 있었다. 즉, 단일 쿼리를 날리더라도, 인위적으로 커밋을 하지 않는다면, 레코드에 반영하지 않는다는 뜻이다.
에이전트의 경우 go 언어로 되어있는데, go 언어에서 제공하는 트랜잭션 처리 관련 라이브러리 문서를 보니, GO 진영에서 제공하는 트랜잭션 처리 라이브러리의 경우, 마치 jdbc를 직접 사용하는 경우와 마찬가지로 롤백 및 커밋 처리 등을 코드로 인위적으로 구현을 해주어야 한다.
그런데, 에이전트를 구성하는 소스코드를 보니, 트랜잭션을 커밋하거나 롤백하는 코드는 전혀 없었다. 그렇다면 왜 다른 업체에서는 이러한 문제가 없었을까?
답은 단순하다. 대부분의 DBMS는 auto-commit이 기본적으로 true로 설정되어 있기 때문이다.
따라서, 그동안 납품한 업체에서 이러한 이슈가 발생하지 않았던 이유는 auto-commit이 true 이기 때문에 에이전트 내에서 단건 쿼리로 insert를 할 경우, DBMS에서 자동으로 커밋을 수행해주고 있었던 것이다.
3. 성과
가장 큰 성과는 '작업 공정 단축'이라 할 수 있겠다. 이 원인을 알아내기 전까지 해당 에이전트 솔루션 담당 개발자는 아예 리뉴얼을 하여 코드를 다시 짜야하는 상황이었다. 기간을 넉넉히 잡아도 5-6주 정도 걸릴 거라고 했는데, 해당 개발자는 다른 업무로 시간을 제대로 할애하지 못하는 상황이었다.
그런데, 원인을 알아내고 나서는 트랜잭션을 처리하는 코드에 커밋, 롤백하는 코드만 추가하거나, DBMS의 auto-commit을 true로 활성화하기만 하면, 되었기에 원인을 알아내는 데 걸리는 시간 5일 그리고, 작업 시간 5분 합해서 대략 5일 안에 이슈를 처리할 수 있었으니, 해당 개발자에게 다른 업무에 집중할 수 있는 시간 5주를 확보해 주었다고 할 수 있다.
경우에 따라 다르겠지만, 개발자의 시급은 다른 직종에 비해 꽤 비싼 편이기에 이렇게 시간을 확보해 주었다는 점만으로도, 꽤 괜찮은 성과라고 어필했으면 좋겠다.
그리고 또 하나의 성과는 아무래도 '잠재적 결함에 대한 명시적인 해결책'을 마련해준 것이라 할 수 있겠다. 앞서 서술했듯이, 해당 솔루션은 이미 다른 업체에도 납품이 되어있는 상황이다. 어떤 업체든 상황에 따라 DBMS의 auto-commit 옵션을 바꿀 수 있기 때문에, 이번 이슈랑 비슷한 경우로 결함이 발생하지 않는다는 보장은 없다. 만약 같은 이슈로 다른 업체에서 수정을 요청한다면, 이미 명확한 레퍼런스가 있기 때문에 좀 더 신속한 대응이 가능하다.
'기술적 이슈 정리' 카테고리의 다른 글
payment-lab 기술적 이슈 -3- 결제 이력 및 복구, Logger를 그대로 사용해도 되는걸까? (0) | 2023.11.17 |
---|---|
payment-lab 기술적 이슈 -2- 중복 결제를 막기위한 멱등키 생성.. 사용자가 결제를 확정짓는 시점은 언제일까? (0) | 2023.11.17 |
payment-lab 기술적 이슈 -1- 로깅 중 비밀번호 노출의 위험성 및 대비책 (1) | 2023.11.12 |
정확한 결제 상태 추적 이슈 -1-, StateMachine 에 대해 알아보자 (1) | 2023.08.23 |
이벤트는 무엇이고 언제 사용해야할까 (0) | 2023.08.22 |