🚀 요약
SUMMARY
사내 클러스터의 DB를 백업본에서 복구하는데 임시파일 하나 만드는 데 수 분씩 걸려 복구가 사실상 멈췄다. 원인은 NFS 서버의
syncexport와 동시 쓰기 과부하였다. exports를async(+no_wdelay)로 바꾸고nfsd스레드를 올린 뒤, 클라이언트 마운트 옵션(rsize/wsize/hard/timeo)까지 손봐서 쓰기 지연을 걷어냈다. 대신async가 지는 데이터 손실 리스크는 UPS·애플리케이션fsync·백업으로 눈 뜨고 받아들였다.
우리 온프레미스 클러스터는 스토리지를 NFS 한 대에 몰아 쓰고 있었다. DB 데이터를 NFS에 얹는 게 정석이 아니라는 건 알지만(랜덤 I/O가 많은 DB와 네트워크 파일시스템은 궁합이 나쁘다) 당장 별도 블록 스토리지가 없는 환경이라 그렇게 굴러가던 상황이었다. 문제는 MariaDB Galera를 물리 백업본에서 복구하던 날 터졌다.
1. 복구 중 임시파일 하나에 수 분
mariadb-backup --copy-back으로 백업 데이터를 데이터 디렉토리에 밀어넣는 단계에서 복구 Job이 진행이 안 됐다. 죽은 건 아니고 그냥 느렸다. 수십 GB짜리 데이터 파일을 푸는 과정에서 파일 하나를 만들고 채우는 데 체감상 수 분이 걸렸다. 복구가 언제 끝날지 가늠이 안 되는 속도였다.
서버에 붙어 상태를 봤더니 그림이 명확했다.
vmstat에서 CPU의 I/O 대기(wa)가 10%대로 붙어 있고, 로드 애버리지가 20을 넘겼다.- 커널 로그(
dmesg)에ext4_buffered_write_iter에서 블록되는 로그가 계속 찍혔다. 쓰기가 디스크 I/O를 기다리다 막히고 있다는 뜻이다. - 하필 그 시점에 Elasticsearch와 MariaDB의 데이터가 모두 같은 NFS 볼륨을 쓰고 있었다. 쓰기 요청이 한 디스크로 몰렸다.
즉 디스크가 못 버티는 것도 있었지만, 그 위에서 NFS가 쓰기를 처리하는 방식이 지연을 증폭시키고 있었다.
2. 왜 느렸나
NFS export에는 sync와 async 두 모드가 있다. 당시 서버는 안전한 기본값인 sync로 물려 있었는데, sync는 클라이언트가 쓰기를 보내면 그 데이터가 디스크에 실제로 안착한 뒤에야 완료 응답을 돌려준다. 데이터 안전성은 좋지만, 작은 쓰기가 수없이 쏟아지는 워크로드에서는 매 요청이 디스크 flush를 기다리며 줄을 선다.
DB 복구는 정확히 그런 워크로드였다. 백업을 푸는 동안 작은 파일 생성·메타데이터 갱신·데이터 쓰기가 폭발적으로 일어나는데, sync에서는 그 하나하나가 디스크 확정을 기다렸다. 거기에 디스크는 이미 다른 워크로드로 포화 상태였다. 지연이 곱해진 것이다.
정리하면 손댈 곳이 세 군데였다. 서버가 쓰기를 확정하는 방식(sync), 요청을 받아주는 nfsd 스레드 수, 그리고 클라이언트가 요청을 쪼개고 재시도하는 방식. 하나만 고쳐서는 부족했다.
3. 서버 쪽: export async와 nfsd 스레드 상향
가장 큰 병목이 sync였으니 여기부터 바꿨다. exports를 async로 전환한다.
# /etc/exports — 마스킹된 예시 경로/대역
# 변경 전: 쓰기를 디스크에 확정한 뒤 응답 (안전하지만 느림)
/srv/nfs 10.10.20.0/24(rw,sync,no_subtree_check,no_root_squash)
# 변경 후: 쓰기를 메모리에 받은 즉시 응답, 디스크 반영은 뒤로 (빠름)
/srv/nfs 10.10.20.0/24(rw,async,no_wdelay,no_subtree_check,no_root_squash)async는 클라이언트 쓰기를 서버 메모리(페이지 캐시)에 받는 순간 완료로 응답하고, 실제 디스크 기록은 커널이 알아서 뒤로 미룬다. 매 요청이 디스크를 기다리던 줄이 사라지니 쓰기 응답이 즉시 돌아온다. no_wdelay도 같이 붙였는데, 사실 이건 async가 켜져 있으면 이미 쓰기 지연 병합을 하지 않으므로 기능적으로는 중복에 가깝다. “쓰기를 모아뒀다 내보내지 않겠다”는 의도를 명시해두는 용도로 남겨뒀다.
바꾼 뒤 반영한다.
exportfs -ra # /etc/exports 재적용두 번째로 nfsd 스레드를 올렸다. NFS 서버는 커널 스레드 풀로 요청을 처리하는데 기본값이 낮으면(배포판에 따라 8 안팎) 동시 연결이 늘 때 요청이 스레드를 기다린다. 당시 활성 연결이 10개 안팎이었어서 여유를 둬 16으로 잡았다.
# 즉시 반영 (런타임)
echo 16 > /proc/fs/nfsd/threads
# 영구 반영 — Rocky 9 기준 /etc/nfs.conf 의 [nfsd] 섹션
# [nfsd]
# threads=16여기에 서버의 더티 페이지가 한꺼번에 몰려 쓰이며 다시 멈추는 걸 막으려고 vm.dirty_background_ratio를 낮춰(백그라운드 flush를 더 일찍·자주 시작) 쓰기를 잘게 흘려보내도록 했다. async로 캐시에 쌓이는 데이터가 늘어난 만큼, 이 부분을 방치하면 주기적으로 대량 flush가 터지며 지연이 되돌아온다.
4. 클라이언트 쪽 마운트 옵션
서버만 고친다고 끝이 아니다. 클라이언트가 요청을 어떻게 쪼개 보내고, 응답이 늦을 때 어떻게 구는지도 지연에 직접 영향을 준다. 마운트 옵션을 이렇게 잡았다.
# 클라이언트 마운트 옵션 예시
mount -t nfs -o rsize=65536,wsize=65536,hard,timeo=14,retrans=2 \
10.10.20.10:/srv/nfs /mnt/nfsrsize/wsize를 65536으로 키웠다. 한 번에 주고받는 읽기·쓰기 블록이 커지면 대용량 순차 전송에서 왕복 횟수가 줄어 처리량이 올라간다. 백업을 푸는 것처럼 큰 파일을 쭉 쓰는 작업에 특히 유리하다.timeo=14(1.4초)·retrans=2로 재전송 타이밍을 명시했다.
hard는 일부러 그대로 뒀다. 초기 분석에서는 타임아웃을 허용하는 soft로 바꾸자는 얘기도 나왔다. 응답이 늦으면 클라이언트가 포기하고 넘어가니 프로세스가 안 멈추긴 한다. 그런데 soft는 서버가 잠깐 느려졌을 때 쓰기가 조용히 실패할 수 있고, 그게 DB 데이터 위에서 일어나면 파일이 깨진다. 성능이 아니라 데이터 무결성 문제라, DB 워크로드에서는 hard를 지키는 게 맞다고 봤다. 느려도 실패로 위장하지는 않는 쪽을 택한 것이다.
5. async의 대가
async는 공짜가 아니다. 클라이언트에게는 “다 썼다”고 응답했지만 실제로는 아직 서버 메모리에만 있는 데이터가 생긴다. 이 상태에서 NFS 서버가 정전이나 커널 패닉으로 갑자기 죽으면, 클라이언트는 성공했다고 아는데 디스크에는 없는 데이터가 날아간다. sync가 원래 막아주던 게 바로 이거였다.
그래서 성능만 챙기고 리스크를 모른 척하지는 않았다. 감당 가능한 선까지 완화책을 뒀다.
- UPS:
async의 손실 창(window)은 결국 “캐시에 있고 디스크엔 없는 그 짧은 시간”이라, 급작스러운 전원 차단만 막아도 현실적 위험이 크게 준다. 그래서 NFS 서버에 무정전 전원을 두는 걸 대비책 1순위로 삼았다. - 애플리케이션 레벨
fsync: DB는 커밋마다fsync로 데이터를 flush한다. 다만 서버 export가async면 서버는 이 flush(NFS COMMIT) 요청조차 디스크에 확정하기 전에 완료로 응답한다. 즉async는 앱fsync의 내구성 보장까지 깎아내므로, 서버가 그 순간 죽으면 커밋한 데이터도 잃을 수 있다. 그래도 클라이언트에 고인 쓰기를 서버까지는 밀어내니 손실 창을 줄이는 효과는 있다. 진짜 안전망은 아래 백업이다. - 백업: 애초에 이 사달이 백업본 복구 중에 났다. 정기 물리 백업 체계가 최후의 보루다. 최악의 경우에도 마지막 백업 시점으로 되돌릴 수 있으면,
async가 잃을 수 있는 건 그 사이의 증분으로 한정된다.
결국 sync↔async는 “얼마나 안전한가”와 “얼마나 빠른가” 사이의 다이얼이다. 우리 환경에서는 복구가 현실적인 시간 안에 끝나는 게 당장 급했고, 손실 리스크는 위 세 가지로 감당 가능한 수준까지 눌렀다고 판단해 async로 다이얼을 돌렸다. 다른 환경이었으면 답이 달랐을 수도 있다.
NOTE
근본적으로 DB 데이터를 NFS에 얹는 구성 자체가 임시방편이다. 지금은 튜닝으로 버티지만, 결국은 DB는 로컬 SSD 기반 블록 스토리지로, NFS는 공유 파일·백업 같은 순차 워크로드로 역할을 나누는 게 맞다. 이 글은 그 이전(以前)의, 있는 자원으로 버텨낸 기록에 가깝다.