구축 후 이슈 (Neo4j)
GDB 업데이트 후 저희 팀은 다양한 이슈를 경험했습니다. 그리고 이슈 해결 과정에서 기반 서비스의 변경도 두 차례나 겪었죠. 이번 편부터는 GDB 구축 후 발생했던 이슈들과 이를 해결하는 과정에서의 이야기를 들려드리고자 합니다. 잡힐 듯 잡히지 않았던 문제의 이슈는 무엇이었고, 이를 해결하고자 어떤 시도들을 했었는지, GDB 구축 후 있었던 애증의 에피소드들을 5편과 6편에 걸쳐 생생히 보여드릴게요 :D
※ 투덥 GDB 기반 서비스 변천사
구축 직후 : Neo4j Aura, Cloud 형태 → 1차 변경 : Neo4j Self-Hosted → 2차 변경 : Memgraph
대부분의 GDB는 스키마-프리이며 Neo4j 또한 같은 특성을 갖고 있습니다. 덕분에 유연성이라는 장점을 활용할 수 있지만 경우에 따라 이 부분이 문제의 원인으로도 작용할 수 있기 때문에 주의해야 합니다.
투덥의 알림과 피드는 하나의 메시지 노드를 공유하며 보이는 위치에 따라 서로 다른 정렬과 필터가 적용되게끔 설정되어 있습니다. 그런데 간헐적으로 목록에서 정렬이나 필터 기준에 맞지 않는 아이템이 보이는 이슈가 발생했습니다. 확인 결과, 메시지 노드의 시간정보가 Integer로 입력되어야 했으나, 일부 특정 상황에서 string 타입으로 입력됨에 따라 정렬과 필터링이 정상 동작하지 않아 발생한 이슈였습니다. RDB 였다면 입력 즉시 에러가 발생하여 빠르게 알아차릴 수 있었겠지만, 그러지 않고 의도한 정렬 및 필터 조건에만 어긋나고 똑같이 보여지는 까닭에 저희도 곧바로 인지하기 어려웠던 부분이었습니다.
다만, 본 이슈와 관련해 수정 자체가 어려운 부분은 아니었기 때문에 관련 쿼리 및 기존 GDB 내 해당 프로퍼티에 대한 검사/변경 작업을 통해 큰 어려움 없이 문제를 해결할 수 있었습니다.
구축 초기의 경우 사소한 이슈들은 있었지만 별다른 큰 문제 없이 안정적인 상황이 유지되었습니다. 그러나 어느 정도 시간이 지남에 따라 GDB 내 데이터양이 증가했고, 유저 수 또한 늘어나게 되면서 GDB에 걸리는 부하도 갈수록 커지기 시작했는데요. 이러한 부담은 결국 GDB 프리징(멈추는 현상) 이슈의 원인이 되고 맙니다.
투덥의 소셜 시스템은 피드 형태인 투덥 서비스 첫 페이지와 연결되어 있었습니다. 그로 인해 프리징 이슈는 홈 화면이 필요한 데이터를 정상적으로 불러오지 못하고 있다는 것을 의미하기도 했죠. 프리징 발생 후 다행히 몇 분 만에 다시 정상으로 돌아오는 경우도 있었지만, 이와 반대로 심할 경우에는 해당 현상이 몇 시간 이상 지속되는 케이스도 있었기 때문에 투덥 서비스 자체의 원활한 운영을 위해서라도 근본적인 이슈 해결이 필요한 상황이었습니다.
(근본적이진 않지만 일단) 다시 정상적인 서비스가 될 수 있도록 가장 먼저 선택할 수 있는 옵션은 재기동이었습니다. 그런데 클라우드 서비스인 Neo4j Aura의 인스턴스를 재기동 하는 일이 쉽지만은 않았습니다. 재기동을 위해서는 기본 제공되는 WebConsole을 사용해야 했는데, 중지 및 재시작 명령을 내리더라도 곧바로 중지되거나 재시작되지 않았기 때문입니다. 해결 방법을 모색하던 중 이전 경험에서 Neo4j Aura 인스턴의 스펙을 조정했을 때 인스턴스가 재시작 됐던 기억이 떠올랐고, 이를 활용해 스펙을 조정함으로써 강제 재기동이 이루어질 수 있도록 조치했습니다.
프리징 이슈가 언제 해결될 수 있을지 섣불리 예측하기 어려운 상황 속에서 우선 서비스 자체에 미치는 영향을 최소화하기 위해 RDB를 통한 우회 로직을 구현했습니다. 그리고 저희가 사용하고 있던 에러 관리 툴인 Sentry의 Metric Alert을 통해 일정 시간 내 에러 현상이 지속되는 경우 알림 및 우회 로직으로 자동 변경되도록 만들었습니다.
프리징 증상의 근본 원인을 찾고자 관련 리서치를 진행하며 에러 정보를 찾고자 노력했습니다. 또한 추가 정보 수집을 위해 Neo4j 측에 현재 저희 팀이 사용 중인 클라우드 서비스의 상세 로그를 제공해 줄 수 있는지도 문의했습니다. 그러나 돌아온 답변은 불가능하다는 내용이었고, 결과적으로 리서치를 통해서도 문제의 정확한 원인을 파악하기는 어려웠습니다.
다시 주어진 상황으로 돌아와 문제의 원인을 처음부터 정리해 보기로 했습니다. 구축 초기에는 괜찮았으나 몇 개월 후 이러한 문제들이 발생한 것에서 미루어 볼 때 급격히 증가한 데이터에 의한 문제라고 가정할 수 있었고, 이에 GDB에 부하가 크게 걸릴 것 같은 쿼리들을 찾아 수정하는 계획을 세웠습니다. 즉, 문제가 발생할 수 있다고 예상되는, 무거울 것으로 보이는 쿼리들의 로직과 실행 시기를 조절했습니다. 당시 확인한 쿼리들 중 특히 큰 변화가 있었던 쿼리로는 호출 횟수가 많았던 신규 메시지 개수 확인 쿼리와 전체 유저 노드를 대상으로 하여 많은 부하가 걸릴 것으로 예상되었던 유저별 추천 더비Pool 갱신 쿼리 두 가지였습니다.
유저의 읽지 않은 메시지의 수를 반환해 주는 쿼리
● 초기 수신 메시지 목록 쿼리를 그대로 사용하던 것에서 카운트 쿼리로 변경
● 유저의 최종 확인 시간 정보를 저장하여 신규 메시지 카운트를 해당 시간으로 필터링한 후 동작시킴으로써 부하를 줄임
추천 로직을 통해 유저별 추천 관계를 생성
● 기존에 전체 유저 노드에 한 번에 작업하던 형태에서 전체 유저 그룹을 5개로 나누어 일정 시간 간격을 두고 동작하도록 함으로써 부하를 분산시킴
이러한 노력에도 불구하고 프리징 이슈는 여전히 계속되었습니다. 결국 이대로는 에러의 정확한 원인 파악이 어려울 것이라 판단하게 되었고, 이후 기반 서비스 자체를 초기의 Cloud 형태에서 config 설정이 자유롭고 서버 로그도 확인 가능한 Self-Hosted 형태로 변경하게 되었습니다.
Self-Hosted 변경 후, 기존의 Aura보다 메모리 설정도 높여서 설정함에 따라(Aura의 경우 100MB로 고정) 변경 초기에는 프리징 이슈도 해결된 것처럼 보였습니다. 그러나 얼마 안가 프리징 증상은 또다시 발생했고, 이에 서버 로그를 확인해 본 결과 Java의 stop-the-world 에러 코드를 확인할 수 있었습니다.
문제 해결을 위해 또 한 번 리서치를 시작했습니다. 하지만 인터넷상에서 찾을 수 있는 정보 중 Neo4j 관련 내용은 거의 없었고, Java에서의 해결 방법 또한 메모리 관리를 확인하여 코드를 수정하거나 메모리 사이즈 조정을 통해 해결하라는 것뿐이었습니다. 엔진 내부의 코드에서 발생하는 에러를 코드의 수정으로 해결할 수는 없었기 때문에, 이번에는 저희 팀 나름대로 메모리 조정 시도를 통한 문제 해결을 마음먹었습니다.
우선 확인한 내용으로는 stop-the-world 에러는 가비지컬렉션으로 인해 발생한다는 사실이었습니다. 메모리 크기가 커질수록 가비지컬렉션의 주기가 길어져 좋을 수 있지만 프리징 되는 시간도 함께 커지게 되고, 반대로 메모리 크기가 작아질수록 주기는 짧아지지만 프리징 되는 시간도 함께 줄어든다는 것을 알 수 있었습니다. 따라서 가비지컬렉션 주기가 길어지거나 충분한 메모리가 확보되어 프리징이 거의 일어나지 않는 형태로 만들거나, 프리징 되는 시간이 충분히 짧아져 순간적인 정도로 줄어들게 만드는 것이 중요했습니다.
그리고 이맘때쯤 GDB에서는 2~3일에 한번 정도의 주기로 프리징이 발생하고 있었습니다. 이에 저희 팀은 4일을 주기로 잡고 현재의 메모리 값에서 변화시키면서 그 영향을 확인하기로 했습니다. 변화할 때는 2배 혹은 절반의 크기로 변화시키고 변화량이 달라지는 곳이 있다면 그곳을 기점으로 근처의 세부 값들을 확인하는 것을 계획했습니다.
하지만 이러한 노력을 통해서도 에러 해결에 필요한 의미 있는 결과를 얻을 수는 없었습니다. Sentry의 알림 간격 또한 5분이 최소였기 때문에 잠시 발생하고 1~2분 내에 정상 가동될 경우 우회 로직이 제대로 작동하지 않는다는 부분도 문제였습니다. 결국 문제의 원인 자체를 해결하지 못한 상황이었기 때문에 또다시 다른 방법을 찾아야만 하는 상황에 놓이게 되었습니다.
직접적인 문제 해결이 어렵다면, 여러 개의 GDB를 만들어 프리징의 영향을 회피할 수 있도록 하는 간접적 해결 방법이 가능한지 확인해 보기로 했습니다. 여러 개의 GDB 인스턴스를 만드는 경우, GDB 간 데이터 싱크를 맞추기 어려운 부분이 있었기 때문에 이와 관련한 대안을 찾는 것이 필요했습니다. 먼저 Neo4j에서 지원하는 것이 있는지 알아보았으나 Neo4j에서의 지원은 Enterprise 버전에서만 해당되어 현재 저희 버전에서는 어렵다는 것을 확인했습니다.
차선책을 찾던 중 Kafka 플러그인을 통해 여러 개의 GDB를 구성하는 방법이 있음을 알게 되었습니다. 하지만 Kafka 플러그인이 4.1 버전 이하에서만 지원이 됐고 저희는 4.2 버전부터 구축을 시작한 관계로 버전 변경이 필요한 상황이었습니다. 다만, 메이저 버전이 다른 것은 아니었기에 일단은 '4.1 버전 + Kafka' 방식으로 테스트해 볼 것을 계획했습니다.
그러나 4.1 버전과 4.2 버전의 차이는 생각보다 컸습니다. 그뿐만 아니라 이전에 문제가 없었던 쿼리들에서도 프리징 현상이 나타나는 부작용이 있었습니다. 쿼리를 조금씩 바꿔가며 테스트를 지속했지만 너무 잦은 프리징 이슈에 결국은 테스트를 중단하게 되었습니다. Kafka로 인한 프리징 이슈는 기존의 GDB 프리징을 피하기 위해 고민했던 방법이 맞는지를 무색하게 할 정도로 너무나 빈번히 발생했습니다.
결국 저희는 또다시 다른 방법을 찾아야만 했습니다. 그렇게 반복된 리서치 도중 Memgraph를 발견하게 되었고, In-Memory를 기반으로 하는 Memgraph의 빠른 데이터 조회라는 장점은 RDB를 기반으로 GDB를 보조적으로 사용하는 저희의 형태와 잘 맞는다고 판단할 수 있었습니다. 이에 또 한 번 서비스 변경을 결정했습니다.
(Memgraph 변경 후 에피소드들은 다음 편에서 이어질 예정입니다.)