일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- conda base 활성화
- 티스토리챌린지
- window netstat time wait 제거
- 려려
- conda base 기본 설정
- conda 기초 설정
- time wait port kill
- 3000 port kill
- 오블완
- 실행중인 포트 죽이기
- conda 가상환경 설정 오류
- Today
- Total
모도리는 공부중
[DB] 2006 mysql server has gone away brokenPipeError - db 연결 끊긴 후 요청 시 발생하는 문제 (feat. ChatGPT) 본문
[DB] 2006 mysql server has gone away brokenPipeError - db 연결 끊긴 후 요청 시 발생하는 문제 (feat. ChatGPT)
공부하는 모도리 2025. 1. 31. 17:46📌 상황 설명
- 1월 24일 이후 일주일 동안 api 요청이 없어 db connection pool이 끊어진 상황
- 1월 31일 로그인 시도 결과 502 응답이 나왔고, 재시도하자 정상적으로 로그인 및 서비스 이용이 가능해짐
- 세션 연결에 관하여 pool_pre_ping=True 설정은 되어 있으나, pool_recycle이나 pool_wait_timeout 등의 값은 별도로 설정하지 않았으므로 기본값 상태
🤖
에러 메시지를 보면 MySQL server has gone away (BrokenPipeError(32, 'Broken pipe')) 가 발생한 것으로 보아, DB 연결이 끊어졌거나 세션이 닫힌 상태에서 쿼리를 실행하려 했을 가능성이 큽니다.
🔍 원인 분석
- DB 연결 끊김 (Connection Lost)
- DB 연결이 오래 유지되지 않아 서버가 닫힘.
- MySQL의 wait_timeout, interactive_timeout 설정이 낮을 가능성이 있음.
- 잘못된 세션 또는 커넥션 풀 문제
- SQLAlchemy 세션이 유효하지 않은 상태에서 접근했을 가능성이 있음.
- Session이 오래 유지되면서 재사용할 때 DB 연결이 끊어졌을 수도 있음.
- 트랜잭션 문제
- 세션이 닫힌 상태에서 ORM 객체의 속성을 접근하려 할 경우 발생 가능.
🛠 해결 방법
1. SQLAlchemy에서 연결이 끊어진 세션을 자동 복구하기
pool_pre_ping=True 옵션을 설정하여 DB 연결이 끊겼을 때 자동으로 재연결되도록 설정하세요.
from sqlalchemy.engine import create_engine
DATABASE_URL = "mysql+pymysql://user:password@host:port/dbname"
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # 연결이 유효한지 체크 후 사용
pool_recycle=3600 # 일정 시간 후 연결 재사용 방지
)
이렇게 하면 DB 연결이 끊어졌을 때 자동으로 재연결됩니다.
2. 커넥션 타임아웃 값 조정
MySQL 서버 설정에서 wait_timeout과 interactive_timeout 값을 확인하고 적절히 늘리세요.
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
기본값이 너무 낮다면 60초 → 3600초 정도로 늘리는 것을 추천합니다.
SET GLOBAL wait_timeout = 3600;
SET GLOBAL interactive_timeout = 3600;
(설정이 유지되려면 my.cnf 파일에도 반영해야 합니다.)
3. SQLAlchemy 세션을 항상 새로운 세션으로 가져오기
DB 연결이 끊어졌을 경우, 기존 세션을 버리고 새로운 세션을 만들어야 합니다.
✅ session.commit() 또는 session.close() 사용 후 새로운 세션을 생성하세요.
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import OperationalError
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
except OperationalError: # MySQL 연결이 끊어졌을 때 재시도
db.rollback()
finally:
db.close()
기존 session이 끊어졌다면 rollback() 후 close()를 호출해야 새로운 세션을 올바르게 사용할 수 있습니다.
4. ORM 객체 접근 방식 수정
에러 메시지를 보면 ORM 객체에서 place_user_rights 속성을 가져오는 과정에서 문제가 발생한 것 같습니다.
✅ 해결책: ORM 객체를 사용할 때, joinedload() 같은 방법을 사용해 미리 데이터를 가져오세요.
from sqlalchemy.orm import joinedload
db = SessionLocal()
member = db.query(Member).options(joinedload(Member.place_user_rights)).filter(Member.id == 27).first()
이렇게 하면 DB 세션이 닫힌 후에도 place_user_rights 속성에 접근할 수 있습니다.
5. pydantic 모델에서 default_factory 사용
FastAPI가 ORM 객체를 JSON으로 변환할 때 속성을 조회하는데, 연결이 끊긴 상태에서 접근하면 문제가 발생할 수 있습니다.
Pydantic 모델에서 default_factory를 설정하여 예외가 발생하지 않도록 방어 코드 추가 가능.
from pydantic import BaseModel, Field
class MemberSchema(BaseModel):
place_user_rights: list = Field(default_factory=list) # 기본값을 빈 리스트로 설정
이렇게 하면 속성 조회 시 에러를 방지할 수 있습니다.
🔥 최종 정리
- pool_pre_ping=True 추가하여 자동 재연결 설정
- MySQL wait_timeout, interactive_timeout 값 증가
- SQLAlchemy 세션을 rollback() 후 close() 하도록 변경
- joinedload()를 사용하여 필요한 속성 미리 로드
- FastAPI 모델에서 default_factory 설정하여 에러 방지
위 방법을 적용하면 OperationalError (MySQL server has gone away) 문제를 해결할 수 있을 것입니다. 🚀
🤔
🤖
맞아요! 정확한 문제 원인은 DB 세션이 일정 시간 사용되지 않아 MySQL 서버에서 연결을 종료했기 때문입니다. 그리고 joinedload()를 사용하면 미리 데이터를 불러와서 해결할 수 있지만, 성능을 고려했을 때 적절하지 않을 수 있죠.
🔍 세션 예외 처리 시 rollback()이 필요 없는 이유
기본적으로 MySQL 연결이 닫힌 후(세션 만료) 발생한 예외를 처리할 때 rollback()이 필요하지 않습니다.
왜냐하면, 이미 세션이 닫힌 상태에서 rollback()을 호출하면 또 다른 예외가 발생할 가능성이 있기 때문입니다.
즉, OperationalError가 발생했다는 것은 이미 세션이 유효하지 않은 상태이므로, rollback()을 호출할 필요 없이 새로운 세션을 생성하는 것이 맞습니다.
🔥 그러면 어떻게 해결해야 할까?
기존 get_db() 함수를 다음과 같이 수정하면 됩니다.
✅ 올바른 예외 처리 방식 (rollback 없이 새로운 세션 생성)
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import OperationalError
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db # 세션 제공
except OperationalError: # MySQL 연결이 끊어졌다면?
db.close() # rollback 없이 닫고 새로운 세션을 생성하도록 유도
raise # 예외를 다시 발생시켜 FastAPI가 적절한 응답을 반환하도록 함
finally:
db.close() # 세션 정리
🔥 왜 rollback() 없이 close()만 해야 할까?
- 이미 연결이 끊겼으므로 rollback이 불가능함
- rollback()을 시도하면 "This session is inactive" 같은 추가적인 예외가 발생할 수 있음.
- 중간에 예외 발생 시 새로운 세션을 생성하는 것이 더 안전함
- rollback()을 시도하는 대신 예외를 다시 발생시키면 FastAPI에서 적절한 HTTP 응답(500 에러 등)을 보낼 수 있음.
- 운영 환경에서 MySQL 연결을 재사용하기 위해 pool_pre_ping=True 설정이 더 중요함
- pool_pre_ping=True를 사용하면 세션이 죽었는지 확인하고 자동 복구할 수 있음.
🔍 추가 해결 방법: 세션이 만료되지 않도록 pool_pre_ping=True 설정하기
이전에도 설명했듯이, DB 세션이 만료되지 않도록 pool_pre_ping=True를 설정하면 대부분의 문제를 해결할 수 있습니다.
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # 연결이 유효한지 체크 후 사용
pool_recycle=3600 # 1시간마다 연결을 새로 만듦 (MySQL wait_timeout보다 짧게 설정)
)
- pool_pre_ping=True: MySQL 연결이 죽었는지 확인 후, 자동 복구
- pool_recycle=3600: 1시간마다 새로운 연결을 생성하여 MySQL 세션 만료 방지
🔥 정리
- 이 문제는 세션이 만료되었기 때문에 발생한 것
- rollback()은 이미 닫힌 세션에서 실행하면 안됨 → close()만 실행
- pool_pre_ping=True를 설정해서 MySQL 연결을 자동 복구하는 것이 최우선 해결책
- 세션이 끊겼을 때 예외를 다시 발생시켜 FastAPI가 정상적으로 처리하도록 유도
이렇게 하면 성능 저하 없이도 DB 연결 문제가 해결될 거예요. 🚀
🤔
pool_pre_ping=True는 이미 되어 있어. 한 번 로그인 안되고 나서 다시 시도하니 정상적으로 로그인 됐거든. 의문은, 사용자 입장에서는 한 번 로그인이 안됐었으니 서버에 문제 있다고 생각하게 된다는 거야. 실제로 운영중인 서버에서 이런 문제는 발생하면 안되잖아? 물론 일주일간 사용 없다가 사용하게 돼서 닫혀있었다는 건 알 거 같긴 해. 근데 닫혀 있는 세션이면 그 세션에 시도해서 502 띄우는 것보다, 곧바로 자동복구부터 시도해서 사용자에겐 정상적인 로그인 200을 보여줬어야 되는 거 아니야? 내 생각은 그러한데 실제로는 아닌가? 그리고 pool_recycle에 대한 설정을 따로 걸어주진 않았어. 이걸 하게 되면 무슨 장단점이 있어?
🤖
사용자가 말한 것이 맞아요. 운영 서버에서는 이런 문제가 발생하면 안 되고, 끊어진 세션을 자동으로 복구해서 사용자 입장에서는 문제없이 동작하는 것처럼 보이도록 처리하는 것이 이상적이죠.
🔥 왜 한 번 실패한 후 다시 시도하면 되는가?
현재 pool_pre_ping=True가 적용되어 있지만, 첫 번째 요청에서 실패하는 이유는:
- SQLAlchemy가 연결 풀에서 끊긴 커넥션을 꺼내옴
- pool_pre_ping=True가 있더라도, 첫 번째 요청 시 이미 끊어진 연결이 반환되었을 경우 예외 발생
- 두 번째 요청에서는 pool_pre_ping이 작동하여 유효한 연결을 가져옴
- 즉, 한 번 에러 발생 후 다음 요청부터는 정상적으로 동작
- 자동 복구가 첫 번째 요청에서 이루어지지 않음
- SQLAlchemy의 pool_pre_ping=True는 사용하기 전에 핑을 보내는 것인데, 끊긴 세션을 처음 가져올 때까지는 작동하지 않을 수도 있음.
- 즉, 연결이 끊긴 후 첫 번째 요청이 실패해야 pool에서 재연결이 수행됨.
✅ 운영에서 이 문제를 방지하려면?
운영 서버에서 이런 문제가 발생하지 않게 하려면 끊어진 연결을 자동 복구하는 메커니즘을 추가해야 합니다.
1️⃣ pool_recycle 설정 추가 (끊기기 전에 미리 새로운 연결 사용)
현재 pool_recycle을 설정하지 않았기 때문에, 연결이 끊기기 전까지 계속 같은 커넥션을 사용하고 있음.
→ MySQL wait_timeout보다 짧은 값으로 pool_recycle을 설정하면 끊어지기 전에 미리 새로운 연결을 사용하도록 할 수 있음.
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # 연결이 유효한지 확인 후 사용
pool_recycle=28000 # MySQL wait_timeout보다 짧게 설정 (ex. 30,000 → 28,000)
)
✅ 장점
- 연결이 끊어지기 전에 미리 새로운 연결을 생성하기 때문에 첫 번째 요청 실패 없이 바로 처리됨.
- 서버가 장시간 유휴 상태였다가 다시 사용될 때도 끊긴 세션을 반환받지 않고 새로운 연결을 사용할 가능성이 높아짐.
🚨 단점
- 너무 짧게 설정하면 불필요한 커넥션 재사용이 많아질 수 있음. (적절한 값 조정 필요)
2️⃣ 첫 번째 요청에서 실패하지 않고 자동 재시도하도록 예외 처리 추가
FastAPI에서 DB 연결이 끊어졌을 때 자동으로 한 번 더 재시도하는 방식을 추가할 수도 있음.
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import OperationalError
import time
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
except OperationalError: # 연결이 끊겼을 때 자동 재시도
db.close() # 기존 세션 닫기
time.sleep(0.1) # 짧은 대기 후 다시 연결 시도
db = SessionLocal() # 새로운 세션 생성
yield db
finally:
db.close()
✅ 이렇게 하면?
- 첫 번째 요청에서 MySQL 연결이 끊겨도 바로 복구됨.
- time.sleep(0.1)로 짧은 대기 후 다시 요청을 보내 502 없이 정상적으로 처리 가능.
🚨 단점
- OperationalError가 발생할 때마다 새로운 세션을 생성하므로, 만약 DB 자체가 다운되었다면 재시도를 반복하는 대신 적절한 오류 메시지를 반환해야 함.
- MySQL 서버가 죽어있는 상태에서는 지속적인 재시도로 인해 불필요한 부하 발생 가능.
📌 pool_recycle을 추가하면 장점과 단점
✅ 장점
- 끊긴 커넥션을 미리 감지하고 재사용 방지
→ 즉, 사용하지 않는 커넥션이 너무 오래되면 recycle이 발생하여 새로운 연결을 생성함. - 첫 번째 요청에서 실패 없이 즉시 응답 가능
→ pool_recycle이 없으면 끊긴 연결을 사용할 가능성이 높아 첫 번째 요청이 실패함.
🚨 단점
- 너무 짧게 설정하면 연결이 자주 재생성됨
→ MySQL은 새로운 연결을 만들 때마다 약간의 부하가 발생할 수 있음. - 적절한 설정이 중요함
→ MySQL의 wait_timeout보다 살짝 짧게(-2000~-5000 정도) 설정하는 것이 좋음.
🚀 결론
- 운영 서버에서는 끊어진 세션을 감지하고 자동 복구해야 함
- pool_recycle=28000 (MySQL wait_timeout=30000일 때) 추가하면 끊기기 전에 새로운 연결을 사용하므로 첫 번째 요청 실패가 방지됨.
- 추가적으로 get_db()에서 OperationalError 발생 시 자동 재시도하는 로직을 추가하면 사용자에게 502 없이 정상적인 200 응답을 제공 가능.
- 지금처럼 pool_pre_ping=True만 사용하면 첫 번째 요청에서 여전히 실패할 가능성이 있음.
🔥 운영 서버라면 pool_recycle을 반드시 설정하고, 필요하다면 get_db()에서 자동 재시도를 추가하는 것이 가장 안전한 방법!
🤔
✅ 이렇게 하면? 첫 번째 요청에서 MySQL 연결이 끊겨도 바로 복구됨. time.sleep(0.1)로 짧은 대기 후 다시 요청을 보내 502 없이 정상적으로 처리 가능. 🚨 단점 OperationalError가 발생할 때마다 새로운 세션을 생성하므로, 만약 DB 자체가 다운되었다면 재시도를 반복하는 대신 적절한 오류 메시지를 반환해야 함. MySQL 서버가 죽어있는 상태에서는 지속적인 재시도로 인해 불필요한 부하 발생 가능. 단점을 극복할 방법
🤖
🔥 단점 극복 방법
현재 OperationalError가 발생하면 자동 재시도하는 로직을 추가하면 502 에러를 방지할 수 있지만, DB가 완전히 다운된 경우 무한 재시도로 인해 불필요한 부하가 발생할 수 있습니다.
이를 방지하려면 다음과 같은 개선 방법을 적용하면 됩니다.
✅ 1. 최대 재시도 횟수 제한 (max_retries)
DB가 다운되었을 경우 무한 재시도를 방지하기 위해 최대 시도 횟수를 지정해야 합니다.
🔹 개선된 get_db() 함수 (최대 3번 재시도)
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import OperationalError
import time
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
max_retries = 3 # 최대 3번 재시도
attempt = 0
while attempt < max_retries:
try:
yield db # 정상적인 DB 세션 반환
return
except OperationalError as e: # MySQL 연결 문제 발생
db.close() # 기존 세션 닫기
attempt += 1
if attempt < max_retries:
time.sleep(0.2 * attempt) # 재시도 전 지수형 대기 시간 증가 (0.2s → 0.4s → 0.6s)
db = SessionLocal() # 새로운 세션 생성
else:
raise e # 최대 재시도 횟수를 초과하면 예외 발생
finally:
db.close()
✅ 개선점
- max_retries=3 → 최대 3번까지 재시도하여 DB가 일시적으로 끊어진 경우 자동 복구
- time.sleep(0.2 * attempt) → 재시도 간격을 점점 증가시켜 불필요한 부하를 줄임
(첫 번째 0.2s, 두 번째 0.4s, 세 번째 0.6s 대기 후 재시도) - 재시도를 초과하면 예외를 다시 발생시켜 FastAPI가 적절한 에러 응답을 반환할 수 있도록 함
✅ 2. DB 다운 여부를 체크하여 예외 반환 (is_db_alive)
DB 자체가 완전히 다운된 경우 연결 체크 후 바로 에러 응답을 반환하도록 하면 불필요한 재시도를 방지할 수 있습니다.
🔹 DB가 살아있는지 확인하는 함수
from sqlalchemy.sql import text
from sqlalchemy.exc import OperationalError
def is_db_alive():
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1")) # 간단한 쿼리 실행
return True # 정상 작동 중
except OperationalError:
return False # DB가 다운됨
🔹 get_db()에서 DB 상태 체크 추가
def get_db():
db = SessionLocal()
max_retries = 3
attempt = 0
# DB가 아예 다운된 경우 즉시 에러 반환
if not is_db_alive():
raise OperationalError("Database is down.", None, None)
while attempt < max_retries:
try:
yield db
return
except OperationalError as e:
db.close()
attempt += 1
if attempt < max_retries:
time.sleep(0.2 * attempt)
db = SessionLocal()
else:
raise e
finally:
db.close()
✅ 개선점
- is_db_alive()로 DB 상태를 먼저 체크하여, 완전히 다운된 경우 불필요한 재시도 없이 즉시 에러 반환
- DB가 일시적으로 끊긴 경우만 재시도하도록 설정
- 무조건 재시도하는 것이 아니라 DB가 복구될 가능성이 있는 경우에만 재시도
✅ 3. FastAPI에서 사용자 친화적인 에러 응답 반환
기본적으로 FastAPI는 500 에러를 반환하지만, 사용자 친화적인 메시지를 제공하는 것이 더 좋음
→ DB가 다운된 경우 "서버 점검 중입니다. 잠시 후 다시 시도해주세요." 같은 메시지를 반환하도록 설정
🔹 FastAPI 예외 처리 추가
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.exc import OperationalError
app = FastAPI()
@app.exception_handler(OperationalError)
async def db_exception_handler(request, exc):
return JSONResponse(
status_code=503, # 서비스 불가 상태
content={"message": "서버 점검 중입니다. 잠시 후 다시 시도해주세요."},
)
@app.get("/healthcheck")
def healthcheck():
"""DB 상태 확인용 엔드포인트"""
if is_db_alive():
return {"status": "ok"}
else:
raise HTTPException(status_code=503, detail="Database is down")
@app.get("/users")
def get_users(db: Session = Depends(get_db)):
return db.query(User).all()
✅ 개선점
- OperationalError 발생 시 503(Service Unavailable) 응답을 반환
→ 502 같은 원인 불명 에러보다 사용자가 이해할 수 있는 메시지를 제공 - /healthcheck 엔드포인트 추가하여 DB 상태를 확인할 수 있음
🔥 최종 정리
적용 방법 해결되는 문제 추가 장점
pool_recycle=28000 | DB 연결이 끊기기 전에 새로운 연결을 사용 | 첫 번째 요청 실패 없이 안정적으로 연결 |
max_retries=3 | 첫 요청 실패 후 자동 복구 | 사용자는 문제를 못 느끼고 정상 로그인 |
time.sleep(0.2 * attempt) | 연속 재시도로 인한 부하 감소 | 시스템 안정성 향상 |
is_db_alive() | DB가 완전히 죽었을 때 재시도 방지 | 불필요한 재시도를 최소화 |
FastAPI 예외 핸들러 | 사용자 친화적인 503 응답 반환 | "서버 점검 중입니다" 등의 메시지 제공 |
🚀 결론
- 연결이 끊기기 전에 pool_recycle=28000 설정
- 첫 번째 요청 실패 시 최대 3번까지 재시도 (max_retries)
- DB가 완전히 다운된 경우 is_db_alive()로 즉시 오류 반환
- FastAPI에서 사용자에게 503 응답 제공하여 UX 개선
이렇게 하면 끊긴 세션을 자동 복구하면서도, DB가 완전히 죽었을 때는 불필요한 재시도를 방지하고 사용자에게 적절한 메시지를 제공할 수 있습니다! 🚀
🤔
db 연결 세션은 얼마나 사용되지 않으면 닫히는 거야? 따로 설정하지 않았을 때 기본값이 어떻게 돼?
🤖
DB 연결 세션이 얼마나 사용되지 않으면 닫히는지는 MySQL 서버 설정과 SQLAlchemy의 커넥션 풀 설정에 따라 달라집니다.
1️⃣ MySQL 서버 측 기본 설정 (wait_timeout, interactive_timeout)
MySQL에서는 **세션이 일정 시간 동안 유휴 상태(사용되지 않음)**이면 자동으로 연결을 종료합니다.
🔹 기본값
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
- wait_timeout : 기본값 28800초 (8시간)
- **일반적인 연결(비-인터랙티브)**에서 사용되지 않으면 세션이 닫힘.
- interactive_timeout : 기본값 28800초 (8시간)
- MySQL CLI 같은 인터랙티브 세션에서 적용됨.
✅ 즉, 기본값 기준으로 MySQL 연결이 8시간 동안 사용되지 않으면 자동으로 닫힘.
✅ 하지만, 운영 환경에서는 보통 더 짧게 설정 (300~3600초 정도)하여, 오래된 연결이 계속 유지되지 않도록 함.
2️⃣ SQLAlchemy의 커넥션 풀 기본 설정
SQLAlchemy의 기본 설정을 사용하면 MySQL 서버에서 연결을 닫기 전까지 계속 유지합니다.
따라서 MySQL의 wait_timeout이 초과되면, SQLAlchemy는 끊어진 커넥션을 가져와서 OperationalError를 발생시키게 됩니다.
🔹 SQLAlchemy의 기본 커넥션 풀 설정
engine = create_engine(DATABASE_URL)
- 기본적으로 사용하지 않는 연결을 자동으로 닫지 않음 → MySQL wait_timeout이 초과되면 세션이 끊어짐.
- pool_recycle을 설정하지 않으면, 연결이 8시간 동안 유지될 수도 있음.
✅ 따라서 pool_recycle을 MySQL wait_timeout보다 짧게 설정해야 세션이 끊기기 전에 자동으로 갱신됨.
3️⃣ SQLAlchemy pool_recycle을 설정하지 않았을 때 동작
(1) pool_recycle이 없는 경우
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
- SQLAlchemy는 MySQL 연결을 풀에서 계속 유지하려고 함.
- 하지만, MySQL wait_timeout이 초과되면 해당 연결이 끊어짐.
- SQLAlchemy가 이 끊어진 연결을 가져오면 OperationalError 발생.
✅ 이것이 "첫 번째 요청에서 MySQL server has gone away" 오류가 발생하는 이유!
(2) pool_recycle을 설정한 경우 (예: pool_recycle=28000)
engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_recycle=28000)
- pool_recycle=28000 설정 시, 연결이 28,000초(약 7시간 46분) 동안 유지된 후 새로운 연결로 갱신됨.
- MySQL의 wait_timeout=28800(기본값 8시간)보다 짧기 때문에, SQLAlchemy가 끊긴 연결을 가져오는 문제가 발생하지 않음.
- 즉, 연결이 끊기기 전에 새로운 연결을 만들도록 유도하는 역할을 함.
✅ MySQL wait_timeout보다 짧게 설정해야 끊어지기 전에 새로운 연결을 받을 수 있음.
✅ pool_recycle=3600(1시간)로 설정하면, 최대 1시간 이상 사용되지 않은 연결은 자동으로 갱신됨.
4️⃣ 최적의 pool_recycle 설정 값은?
운영 환경에서는 wait_timeout을 고려하여 pool_recycle을 설정해야 합니다.
🔹 적절한 pool_recycle 값 추천
MySQL wait_timeout 값 추천 pool_recycle 값
28800초 (8시간, 기본값) | pool_recycle=28000 (약 7시간 46분) |
3600초 (1시간) | pool_recycle=3500 (약 58분) |
600초 (10분) | pool_recycle=550 (약 9분) |
✅ wait_timeout보다 약간 짧게 설정하는 것이 중요!
✅ 너무 짧게 설정하면 불필요하게 새로운 연결을 많이 만들게 되어 성능이 저하될 수 있음.
🔥 최종 정리
설정 기본값 설명
MySQL wait_timeout | 28800초 (8시간) | 일정 시간 사용되지 않으면 MySQL이 자동으로 세션을 닫음 |
SQLAlchemy 기본 설정 (pool_recycle 없이 사용) | 설정 없음 | MySQL이 세션을 닫을 때까지 유지됨 (8시간 후 끊길 수 있음) |
SQLAlchemy pool_recycle=28000 설정 | 28000초 (약 7시간 46분) | 8시간이 되기 전에 새로운 연결을 생성하여 끊기지 않도록 방지 |
✅ 운영 환경에서는 pool_recycle=3600(1시간) 정도로 설정하는 것이 일반적.
✅ MySQL wait_timeout을 낮춘 경우(3600초 이하), pool_recycle도 비슷하게 맞춰줘야 함.
🚀 운영 서버에서 안전한 DB 연결 유지 설정
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # 연결이 죽었는지 확인 후 자동 복구
pool_recycle=3600 # 1시간마다 새로운 연결을 생성하여 MySQL 세션 만료 방지
)
이렇게 설정하면:
- 끊어진 세션을 자동으로 감지하고 복구 (502 오류 방지)
- 연결이 닫히기 전에 미리 새로운 세션을 생성 (첫 요청 실패 방지)
- 사용자가 서버 문제라고 인식하지 않도록 정상 응답 유지 🚀
✅ 운영 환경에서 해야 할 최적화
1️⃣ MySQL의 wait_timeout 확인
SHOW VARIABLES LIKE 'wait_timeout';
→ 값이 3600초(1시간)보다 크면, SQLAlchemy pool_recycle을 설정하지 않으면 끊어진 세션을 사용할 가능성이 있음.
2️⃣ SQLAlchemy pool_recycle 설정
engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_recycle=3600)
→ 세션이 끊기기 전에 자동으로 새로운 연결을 사용하도록 설정.
3️⃣ FastAPI에서 세션 예외 처리 추가
@app.exception_handler(OperationalError)
async def db_exception_handler(request, exc):
return JSONResponse(status_code=503, content={"message": "서버 점검 중입니다. 잠시 후 다시 시도해주세요."})
→ 사용자가 502 에러를 경험하지 않고, "서버 점검 중" 같은 친절한 메시지를 받을 수 있도록 설정.
🔥 운영 환경에서 "MySQL server has gone away" 문제를 방지하려면?
✅ pool_pre_ping=True → 끊어진 세션 감지 후 자동 복구
✅ pool_recycle=3600 (1시간) → 오래된 세션을 자동으로 교체
✅ FastAPI에서 OperationalError 예외 처리 → 502 대신 친절한 메시지 제공
이렇게 하면 운영 환경에서도 첫 번째 요청에서 실패하지 않고 정상적인 응답을 받을 수 있습니다. 🚀
'내 지식 정리 > 날것 그 자체' 카테고리의 다른 글
2025.02. 개발 궁금증 링크 일기 (0) | 2025.02.04 |
---|---|
[GithubAction] Github 제공 러너 vs Github Actions 자체 호스팅 러너 (feat.ChatGPT) (0) | 2025.01.17 |
[URL링크하기] QR코드 생성 (feat. ChatGPT) (0) | 2025.01.10 |
[DB 보안] db ↔ 애플리케이션 연결 SSL 구성 관련 (feat. ChatGPT) (0) | 2025.01.07 |
2025.01. 개발 궁금증 링크 일기 (0) | 2025.01.02 |