26-8-3 스팀잇의 포스팅 오류와 이에 대한 @ety001의 답변

in AVLE 일상15 hours ago

steemit 홈페이지에 This page doesn't exist 라는 오류가 발생했다.
포스트를 했는데 steemit에 올라오지 않는 것이다. 신고를 했더니 @ety001이 수정하고 포스트를 올렸다.

https://steemit.com/steem/@ety001/hivemind-connection-pool-exhaustion-fix-released

기술적인 문제인데 한글 번역은 아래와 같습니다.

게시해주신 공지글의 한글 번역입니다.


날짜: 2026년 8월 1일

이미지: steemit/hivemind:latest, steemit/hivemind:latest-c89b5f5

PR: https://github.com/steemit/hivemind/pull/375 (c89b5f5)

일부 노드 운영자가 겪었던 반복적인 가용성 문제를 해결하는 신규 Hivemind 이미지가 출시되었습니다. 기존에는 고부하 상황에서 API 서버의 HTTP 4xx 에러율이 상승하면서 서비스가 주기적으로 저하되었고, 로드밸런서에 의해 인스턴스가 재시작되는 문제가 있었습니다. 본 글에서는 당시 발생했던 원인과 업그레이드 방법에 대해 설명합니다.


문제 원인 (What was wrong)

Hivemind는 고정된 크기의 PostgreSQL 커넥션 풀(기본값 20개, 운영 환경에서는 주로 25개로 설정)을 사용하여 API 요청을 처리합니다. 그런데 중첩 깊이가 깊은 댓글 스레드에서 단 한 번의 bridge.get_discussion 호출이 발생해도 여러 개의 커넥션을 장시간 점유할 수 있었습니다. 깊이의 각 단계마다 자식 ID 조회를 순차적으로 실행하므로, 범위가 넓거나 오래된 스레드는 수백 단계의 깊이를 가질 수 있기 때문입니다. 이러한 요청이 동시에 여러 건 실행되면 커넥션 풀이 완전히 고갈되었습니다.

문제는 헬스체크 엔드포인트인 /health 역시 동일한 커넥션 풀의 커넥션을 필요로 했다는 점입니다. 느린 discussion 쿼리로 인해 커넥션 풀이 모두 고갈되면 /health 요청이 커넥션을 획득하지 못하게 되고, 로드밸런서는 해당 인스턴스를 비정상(unhealthy) 상태로 간주하여 인스턴스를 교체해 버렸습니다. 이로 인해 사용자들은 get_profile / get_discussion 호출 시 간헐적인 "Internal error" 응답을 받거나, steemit.com에서 "Sorry! This page doesn't exist."라는 오류를 접하게 되었습니다. 인스턴스가 교체된 후에는 서비스가 자체적으로 복구되었습니다(보통 15~30분 소요).

관찰된 최악의 쿼리 실행 시간은 160초로, 이는 정상적인 요청 처리 범위를 크게 벗어난 수치였습니다.


변경 사항 (What changed)

PR #375에서는 단일 요청이 커넥션 풀을 독점하지 못하도록 보장하기 위해 4가지 관점에서 문제를 해결했습니다.

  1. 쿼리별 실행 타임아웃 도입 (statement_timeout=30s)
    PostgreSQL이 30초 이상 실행되는 모든 쿼리를 강제 취소하고, 커넥션을 즉시 풀로 반환합니다. 기존에는 쿼리가 무한정 실행될 수 있었습니다(기존 커넥션 풀의 acquire timeout은 빈 커넥션을 기다리는 시간만 제어할 뿐, 쿼리 자체의 실행 시간은 제어하지 못했습니다).
  2. 격리된 헬스체크 엔진 구축
    /health/head_age 엔드포인트가 메인 풀과 분리된 별도의 단일 커넥션 엔진에서 실행됩니다. 이를 통해 메인 커넥션 풀이 완전히 포화 상태이더라도 헬스체크 응답이 유지되므로, 로드밸런서가 단지 처리량이 많을 뿐 다운되지 않은 인스턴스를 강제로 종료하는 일이 더 이상 발생하지 않습니다.
  3. Discussion 재귀 탐색 제한
    _load_discussion 함수에 최대 깊이 제한(MAX_DEPTH=50) 및 최대 총 포스트 수 제한(MAX_THREAD_POSTS=500)을 도입했습니다. 기존에 수천 개의 댓글을 무제한으로 탐색하던 스레드 조회가 이제 안전한 범위 내에서 멈추게 됩니다. 제한 범위를 초과하는 대형 스레드는 서버를 위험에 빠뜨리는 대신 부분 트리(루트 포스트 + 최초 500개 포스트)를 반환합니다.
  4. 숨김 상태(Hide-status) 조회 캐싱
    요청당 실행되던 2개의 DB 조회(_get_author_hide_id, _check_posts_hide_id)가 300초 동안 캐싱됩니다. 이를 통해 모든 get_discussion 호출 시 2개의 DB 커넥션 사용을 줄였습니다.

검증 결과 (Verified)

해당 수정 사항은 개발 환경 및 프로덕션 beta-hivemind에 배포되어 엔드투엔드 검증을 완료했습니다.

  • /health 엔드포인트가 정상적인 최신 블록 연령(head-block age)과 함께 200 OK를 반환합니다.
  • 실제 포스트를 대상으로 bridge.get_discussion이 정상 작동함을 확인했습니다.
  • 850개의 댓글이 달린 스레드의 경우, 제한 조건이 정상 작동하여 약 2초 만에 497개의 포스트를 반환합니다(기존에는 무제한 탐색이 수행됨).
  • 메인 커넥션 풀이 15개의 무거운 동시 요청으로 포화된 상태에서도 /health는 1초 미만의 빠른 응답을 유지하며 커넥션 획득 타임아웃이 발생하지 않습니다.

업그레이드 방법 (How to upgrade)

최신 이미지를 받고 Hivemind 서버 컨테이너를 재시작하세요.

docker pull steemit/hivemind:latest      # 특정 빌드로 고정하려면 :latest-c89b5f5 사용
# 이후 hive 서버 컨테이너/프로세스 재시작

별도의 데이터베이스 마이그레이션은 필요하지 않으며, DB_VERSION도 변경되지 않았습니다.


운영자 참고 사항 (Notes for operators)

  • statement_timeout은 30초로 설정됩니다. 이 서버를 통해 Hivemind DB에 의도적으로 긴 분석용 커스텀 쿼리를 실행할 경우 30초 후 실패하게 됩니다. 이는 가용성을 위협하는 요인을 차단하기 위해 의도된 동작입니다. 임시 분석용 쿼리가 필요한 경우 API 서버를 거치지 않고 PostgreSQL에 직접 연결하여 사용하세요.
  • 매우 큰 스레드는 일부 결과만 반환됩니다. 포스트 수가 500개를 초과하는 스레드는 자려서 반환됩니다. 이는 커넥션 풀 고갈 위험을 방지하기 위한 합리적인 절충안이며, 루트 포스트와 상위 500개 댓글은 항상 반환됩니다.
  • 인스턴스당 DB 커넥션이 1개 추가됩니다. 격리된 헬스체크 엔진이 maxsize=1을 사용하므로, 총 커넥션 수가 $N$개에서 $N+1$개로 증가합니다. 이는 실제 운영 환경에서 무시할 수 있는 수준입니다.
  • Slow 쿼리가 의존하던 하위 인덱스들은 이미 올바르게 존재하고 있었습니다. 기존의 160초 쿼리는 인덱스 누락이 아닌 경합(Contention)으로 인해 발생한 현상이었습니다. 따라서 스키마나 인덱스 변경은 필요하지 않습니다.

감사 인사 (Thanks)

간헐적인 오류를 제보해 주신 모든 분께 감사드립니다. 업그레이드 이후에도 예상치 못한 4xx 응답이나 인스턴스 재시작 현상이 지속되는 경우, 발생 시간과 관련 API 메서드를 명시하여 GitHub Issue에 등록해 주시기 바랍니다.


이 수정은 다시 문제를 일으켜 추가 작업을 하고 있습니다.
그래서 현재의 작업 상황을 정리하면 다음과 같습니다.

제시해주신 공지글(PR #375 배포 내용)과 ety001 증인의 제보(실제 운영 현장의 피드백)를 종합하면, 현재 상황은 "새로 도입한 디비 타임아웃 해결책이 예상치 못한 부작용을 일으켜, 또 다른 형태의 릴레이 장애를 유발하고 있는 비상 상황"으로 정리할 수 있습니다.

두 내용을 연결하여 구조화하면 다음과 같습니다.


1. 개발팀의 원래 의도 (PR #375)

스팀잇 팀은 무거운 쿼리가 DB 커넥션 풀을 독점하여 서버가 마비되는 것을 막기 위해 statement_timeout=30s(30초 초과 쿼리 강제 종료)를 도입하고 공지했습니다.

"이제 30초가 지나면 쿼리가 취소되고 커넥션이 즉시 풀로 반환되므로 안전합니다."


2. 현장에서 터진 실제 문제 (부작용 발생)

그러나 ety001 증인이 확인한 바에 의하면, DB(PostgreSQL) 레이어와 앱(Hivemind) 레이어 간의 비동기 처리 미흡으로 인해 의도와 다른 악순환이 시작되었습니다.

  1. 앱 레이어의 방치: PG가 30초 만에 쿼리를 끊고 커넥션을 회수했지만, Hivemind 애플리케이션의 비동기(Async) 요청은 자기가 끊긴 줄 모르고 여전히 작업을 대기하며 메모리를 잡고 있습니다.
  2. 증폭 루프 (Amplified Loop): 반환된 커넥션을 다른 요청이 즉시 낚아채서 또 30초 동안 돌다 터집니다. 쿼리 실행 → 30초 타임아웃 → 커넥션 재획득 → 다시 타임아웃이 끊임없이 반복됩니다.
  3. 결과적인 서버 마비:
  • RDS DB: 타임아웃 및 재연결 트래픽 폭주로 I/O가 치솟고 전체 쿼리가 느려짐.
  • Hivemind 서버: 완료되지 못한 비동기 컨텍스트(Async context)가 메모리에 쌓여 메모리 점유율 99% 달성.
  • 착시 현상: 커넥션 풀은 정작 '비어 있음'으로 표시되지만 실제로는 모든 커넥션이 잼(Jam) 상태.

3. 문제를 더 악화시킨 범인 (condenser_api.get_state)

이 와중에 스팀 지갑 페이지(/@user/transfers, /@user/curation-rewards 등)를 이용하는 사용자들이 여전히 수년 전 사용 중단(Deprecated)된 condenser_api.get_state를 계속 호출하면서, 안 그래도 위태로운 RDS에 엄청나게 무거운 DB 쿼리를 지속적으로 들이붓는 불에 기름을 붓는 역할을 하고 있습니다.


4. 현재 진행 중인 대응 상황 (Summary)

  • ety001 증인: DB가 완전히 다운되는 것을 막기 위해 API 게이트웨이인 Jussi 단에서 condenser_api.get_state 호출에 대한 임시 타임아웃 제한을 걸어 RDS로 들어가는 무거운 트래픽을 차단(방화벽 역할)하고 있습니다.
  • 근본적 해결책 추진 중:
  1. Hivemind: DB 쿼리가 취소될 때 앱 레이어의 비동기 요청도 함께 즉시 취소/정리되도록 코드 수정 작업 진행 중.
  2. 지갑(Wallet): 문제의 원인인 레거시 API(get_state)를 더 이상 사용하지 않는 신규 리팩토링 지갑으로의 전환 가속화.

일단 hivemind의 수정과 보안작업이 더 진행되어야 하는 상황입니다.
현재는 "Hivemind의 Async cancellation 버그가 수정된 추가 핫픽스 PR/이미지 출시"를 기다리며, 그동안 Jussi 설정으로 RDS 입력 트래픽을 제어하는 응급처치 단계에 있다고 보시면 됩니다.

Sort:  

High-Yield Curation by @steem-seven

Your content has been supported!


Maximize your passive income!
Delegate your SP to us and earn high rewards

Click here to see our Tiered Reward System

Vote Proposal 100Vote Witness @seven.witMeet Speak on Steem

We are the hope!

S7VEN Banner

This post has been upvoted by @italygame witness curation trail


If you like our work and want to support us, please consider to approve our witness




CLICK HERE 👇

Come and visit Italy Community



Steemtoon이 가끔씩 멈출때가 있는데요.
원인을 보면 API 접속 오류더군요.

사실,
자동화를 해놓긴 했는데
불안불안 합니다.

빠르게 안정화 되길
기원할게요.