트랜잭션 안에서 외부 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

float64를 float32로 낮춰도 되나 — 오차는 여기서 드러납니다

float64를 float32로 낮추면 정밀도가 정확히 절반으로 줄어든다고 생각하기 쉽지만, 실제로는 비트 수가 줄면서 생기는 반올림 오차가 특정 지점에서만 두드러지게 나타납니다. 결과부터 말하자면, 값의 크기가 커지거나 같은 연산을 수없이 반복해 오차가 누적되는 구간에서만 문제가 생기고, 평범한 연산 대부분에서는 체감되지 않습니다. float64를 float32로 낮춰도 되나 고민하고 계시다면, 아래 몇 가지 지점만 확인하면 판단이 쉬워집니다. float32와 float64, 비트 … Read more

numpy 브로드캐스팅에서 메모리가 터지는 거리 행렬, 쪼개서 계산하는 법

결론부터 말씀드리면, 거리 행렬을 구할 때 X[:, None, :] – X[None, :, :] 식으로 한 번에 브로드캐스팅하면 (N, N, D) 크기의 중간 배열이 그대로 메모리에 올라가서 터집니다. 해결책은 ① 제곱합 전개(내적 트릭)로 중간 배열 자체를 없애거나, ② 행 단위로 쪼개서 필요한 블록만 메모리에 올리는 것입니다. 데이터가 커질수록 numpy 브로드캐스팅에서 메모리가 터지는 지점은 거의 항상 이 … Read more

pandas 대신 DuckDB를 쓸 시점, 조인·집계·메모리 기준으로 정리

결론부터 말씀드리면, pandas 대신 DuckDB를 쓸 시점은 데이터 크기가 아니라 작업의 종류로 판단하는 게 정확합니다. 단일 파일을 훑어보는 수준이라면 pandas로 충분하지만, 여러 테이블을 조인하거나 그룹 집계를 반복하는 순간, 혹은 데이터가 메모리 용량에 슬슬 닿기 시작하는 시점부터는 DuckDB 쪽이 코드도 짧아지고 처리도 안정적입니다. 이 글에서는 조인, 집계, 메모리라는 세 기준으로 전환 시점을 구체적인 코드와 함께 짚어보겠습니다. … Read more