워커를 늘렸는데 캐시 적중률이 떨어지는 이유

새벽에 트래픽이 몰려서 워커 프로세스를 4개로 늘렸는데, 정작 대시보드의 캐시 적중률이 뚝 떨어지는 그래프를 보고 당황한 적이 있으신가요. 원인은 메모리 부족이나 캐시 로직 버그가 아니라, 워커마다 캐시가 따로 만들어지는 구조 때문인 경우가 많습니다. 이 글에서는 프로세스별 메모리가 어떻게 캐시 적중률을 갉아먹는지, 그리고 어떤 조건에서 공유 캐시로 옮기는 게 맞는지를 구체적으로 짚어보겠습니다. 워커 수를 늘렸는데 캐시 … Read more

캐시가 만료되는 순간 DB가 무너지는 이유, 스탬피드를 막는 세 가지 방법

이 글에서는 캐시가 만료되는 순간 DB가 무너지는 정확한 원인과, 현장에서 실제로 쓰는 방어 기법 세 가지를 코드와 함께 확인하실 수 있습니다. 핵심만 먼저 말씀드리면, 캐시 미스 요청이 한순간에 몰리는 ‘캐시 스탬피드’ 현상은 분산 락, TTL 지터, 확률적 조기 재계산 중 하나만 제대로 적용해도 크게 줄어듭니다. 다만 세 방법은 구현 난이도와 적용 조건이 서로 다르기 때문에, … Read more

어느 쿼리가 느린지 모를 때, pg_stat_statements와 EXPLAIN 읽는 순서

결론부터 말씀드리면, 어느 쿼리가 느린지 모를 때는 감으로 찾지 말고 pg_stat_statements로 범위를 좁힌 다음 EXPLAIN으로 원인을 확인하는 순서를 지키는 것이 가장 빠릅니다. 로그를 뒤지거나 “이 쿼리가 느릴 것 같다”는 추측으로 튜닝을 시작하면 시간만 쓰고 원인은 못 찾는 경우가 많습니다. 이 글은 PostgreSQL 13 이상 환경을 기준으로, 통계 확장을 켜는 설정부터 EXPLAIN 출력에서 실제로 봐야 할 … Read more

트랜잭션 안에서 외부 API를 부르면 idle in transaction이 쌓입니다

트랜잭션 안에서 외부 API를 부르면, 그 응답이 돌아올 때까지 DB 커넥션은 커밋도 롤백도 못 한 채로 열려 있습니다. 이 대기 시간이 PostgreSQL의 pg_stat_activity에 idle in transaction 상태로 그대로 쌓입니다. API가 평소보다 느려지거나 타임아웃이 나면 그 시간만큼 커넥션 풀이 잠기고, 뒤에 들어온 요청들은 커넥션을 못 받아 줄줄이 대기하게 됩니다. 원인은 코드 한 줄짜리 습관인데, 결과는 서비스 … Read more

Alembic 자동생성이 놓치는 변경들 — head가 둘로 갈렸을 때 합치는 법

alembic revision –autogenerate를 돌렸는데 마이그레이션 파일이 텅 비어 있던 경험, 한 번쯤 있으실 거예요. 결론부터 말씀드리면 Alembic 자동생성이 놓치는 변경들은 따로 정해져 있고, 이 범위를 모르고 쓰면 스키마와 모델이 조용히 어긋나 버립니다. 여기에 더해 여러 명이 동시에 마이그레이션을 만들면 head가 두 개로 갈라지는 문제까지 겹치는데, 이 글에서는 Alembic 1.13 기준으로 두 문제를 실제 명령어와 함께 … Read more

새벽 첫 요청만 DB 오류가 나는 이유, pool_recycle·pre_ping으로 잡기

새벽 첫 요청만 DB 오류가 뜨는데 도대체 왜 그런 걸까요? 답은 거의 항상 같습니다. MySQL 같은 DB 서버가 밤새 가만히 있던 연결을 서버 쪽에서 먼저 끊어버렸는데, SQLAlchemy 커넥션 풀은 그 사실을 모른 채 죽은 연결을 그대로 꺼내 쓰기 때문입니다. 이 글에서는 pool_recycle 값을 얼마로 잡아야 하는지, pool_pre_ping을 언제 같이 써야 하는지를 구체적인 숫자와 코드로 정리합니다. … Read more

PgBouncer 뒤에서 asyncpg가 터지는 prepared statement 충돌, 이렇게 풉니다

PgBouncer를 transaction 풀링 모드로 쓰면서 asyncpg를 붙이면, 커넥션 풀 뒤에서 asyncpg가 언제 터질지 모르는 상태가 됩니다. 원인은 asyncpg가 기본값으로 모든 쿼리를 서버사이드 prepared statement로 만들어 캐싱하는데, PgBouncer가 트랜잭션마다 실제 백엔드 연결을 바꿔버리기 때문입니다. 해결의 핵심은 asyncpg의 statement_cache_size를 0으로 낮추거나, SQLAlchemy를 쓴다면 드라이버 레벨 캐시를 별도로 꺼주는 것입니다. PgBouncer 뒤에서 asyncpg가 막히는 진짜 원인 asyncpg는 쿼리를 … Read more

n_jobs를 올렸는데 더 느려지는 이유, BLAS 스레드 중첩 문제

결론부터 말씀드리면, n_jobs를 올렸는데 더 느려지는 현상은 대부분 joblib(또는 scikit-learn)이 만드는 프로세스 병렬화와 NumPy·SciPy가 내부적으로 쓰는 BLAS 스레드 풀이 같은 CPU 코어를 동시에 두 번 점유하기 때문에 생깁니다. 코어가 8개인데 워커 프로세스 8개가 각자 BLAS 스레드를 4개씩 더 띄우면, 실제로는 32개의 스레드가 8개 코어를 나눠 쓰게 되고 컨텍스트 스위칭 비용만 늘어납니다. 이 글에서는 이 현상이 … Read more

to_datetime이 전체 시간의 절반을 먹을 때 — format 지정의 효과

배치 스크립트 실행 시간을 cProfile로 떠보면, CSV 몇 개 읽고 날짜 컬럼 하나 변환했을 뿐인데 pd.to_datetime 한 줄이 전체 시간의 절반 가까이를 차지하는 경우가 있습니다. 원인은 대부분 포맷 추론입니다. format 인자를 명시적으로 넣어주면 pandas가 행마다 날짜 형식을 다시 추측하는 과정을 건너뛰기 때문에 같은 데이터라도 체감될 정도로 빨라집니다. 다만 데이터 안에 포맷이 섞여 있으면 이 방법이 … Read more

del 했는데 메모리가 안 줄어들 때: RSS와 tracemalloc이 다른 이유

del 했는데 메모리가 줄어들지 않는 이유는 간단합니다. del이 지우는 건 변수와 객체 사이의 참조 하나일 뿐, 객체가 차지한 메모리 블록 자체가 아니기 때문입니다. 게다가 그 블록이 실제로 해제되더라도 운영체제에 돌려주는 건 또 다른 단계라서, tracemalloc이 보여주는 수치와 OS가 보고하는 RSS(Resident Set Size)가 서로 다르게 움직입니다. 이 글은 CPython(표준 파이썬 구현체), 리눅스 + glibc 환경을 기준으로 … Read more