dev.to

Valkey 9.2's forkless BGSAVE cuts my memory spike from 350MB to 10MB

Valkey 9.2의 forkless BGSAVE, 메모리 급증을 350MB에서 10MB로 줄였습니다

Valkey 9.2.0-rc1의 forkless BGSAVE는 쓰기 부하가 있는 1.1GB 데이터셋에서 저장 중 메모리 증가량을 평균 354MB에서 9.9MB로 줄였습니다. 대신 저장 시간은 3초에서 5~6초로 늘고 쓰기 처리량은 약 15% 감소해, 메모리와 성능 사이의 실제 비용을 보여줍니다.

AI 요약

Valkey 9.2.0-rc1은 자식 프로세스를 fork하지 않고 RDB 스냅샷을 저장하는 forkless BGSAVE를 추가했습니다. 글쓴이는 기본 fork 방식과 forkless 방식을 같은 데이터셋과 부하 조건에서 비교해 메모리 사용량뿐 아니라 저장 시간, 쓰기 처리량, 지연 시간도 측정했습니다. 결과는 forkless가 메모리 급증을 크게 줄이는 대신 저장이 느려지고 쓰기 처리량도 떨어지는 선택지임을 보여줍니다.

비교 방법

Docker Hub에 올라온 valkey/valkey:9.2.0-rc1 이미지로 컨테이너 두 개를 실행했습니다. 기본 컨테이너는 bgsave-default-method fork를 사용했고, 비교 대상은 시작 옵션에 --forkless-infrastructure-enabled yes --bgsave-default-method forkless를 지정했습니다. 각 인스턴스에 300바이트 크기의 키 300만 개를 넣어 used_memory 기준 1.04GiB, 약 1.1GB 데이터셋을 만들었습니다.

저장 중에도 부하가 이어지도록 8개 스레드가 기존 키에 무작위 SET을 반복했습니다. 9번째 연결에서는 2ms 간격으로 PING을 보내 왕복 지연 시간을 쟀습니다. 12초 동안 부하를 주고 2초가 지난 시점에 BGSAVE를 실행했으며, 각 방식으로 일곱 차례 반복했습니다. 이 조건이 중요한 이유는 copy-on-write 비용이 fork 이후 실제 페이지에 쓰기가 발생할 때 커지기 때문입니다. 글쓴이의 테스트에서는 fork 방식에 초당 약 2만 7천 건의 쓰기가 들어갔습니다.

측정값은 INFO persistence의 rdb_last_cow_size와 저장 중 150ms 간격으로 수집한 컨테이너의 cgroup 메모리 사용량입니다. 두 지표의 평균은 각각 fork에서 354.7MB와 354.4MB, forkless에서 0MB와 9.9MB였습니다. forkless의 COW 크기는 모든 실행에서 0으로 보고됐고, cgroup 메모리 증가량도 10.0MB를 넘지 않았습니다. 반면 fork 방식의 메모리 증가는 일곱 차례에 걸쳐 319.6~367.5MB 범위에 있었습니다. 서로 다른 방식으로 측정한 두 메모리 지표가 비슷한 결과를 낸 점도 확인했습니다.

메모리를 줄이는 대신 저장과 쓰기가 느려집니다

같은 데이터셋을 저장하는 데 fork 방식은 매번 3초가 걸렸지만 forkless는 5~6초가 걸렸습니다. 약 70% 더 오래 걸린 셈입니다. 저장 전후 12초 동안 8개 쓰기 스레드가 낸 처리량도 fork의 초당 평균 27,202건에서 forkless의 23,034건으로 약 15% 줄었습니다. 글쓴이는 forkless에서 직렬화 작업이 메인 스레드와 같은 잠금 및 CPU 자원을 놓고 경쟁하는 점을 원인으로 설명합니다.

지연 시간 측정에서는 forkless가 항상 더 나쁘지는 않았습니다. 7회 실행에서 각 실행의 최악의 PING 왕복 시간은 forkless가 5.1~14.2ms, 평균 7.2ms였습니다. fork 방식은 11.5~25.7ms, 평균 20.1ms로 더 거칠었습니다. 다만 forkless도 한 차례 14.2ms까지 튀었습니다. 저장 백그라운드 스레드가 아직 처리하지 않은 키에 메인 스레드가 쓰기를 시도하면 요청이 잠시 멈출 수 있다는 문서의 주의사항과 맞아 보였다고 글쓴이는 덧붙입니다. 지연 꼬리를 좁히지만 없애지는 못합니다.

따라서 선택 기준은 운영 환경의 병목입니다. 메모리 여유가 적고 BGSAVE 중 실제 OOM이나 메모리 압박을 겪는 환경이라면 forkless를 검토할 만합니다. 반대로 쓰기 처리량이 중요하고 짧은 메모리 급증을 감당할 수 있다면, 저장 시간이 길어지고 처리량이 줄어드는 비용을 먼저 측정해야 합니다. idle 데이터셋으로 저장하면 COW 크기가 약 12MB에 그쳤습니다. 동시 쓰기가 있어야 fork 방식의 메모리 비용이 뚜렷해진다는 점도 글쓴이가 강조합니다.

설정 범위와 운영상 제약

forkless-infrastructure-enabled는 실행 중 CONFIG SET으로 바꿀 수 없는 설정입니다. 서버 시작 시 명령행 옵션이나 설정 파일에 넣어야 하므로 기존 인스턴스에 적용하려면 재시작이 필요합니다. 이 설정 없이 bgsave-default-method forkless를 지정하면 서버가 선택을 거부합니다. 실행 중인 BGSAVE가 있을 때 두 번째 요청을 보내도 대기열에 쌓이지 않고 ERR Background save already in progress 오류가 납니다.

또한 forkless 설정은 RDB 스냅샷에만 적용됩니다. AOF를 켜면 자동 초기 AOF rewrite가 실행되는데, 글쓴이가 확인한 aof_last_cow_size는 약 10MB였습니다. AOF rewrite는 여전히 fork를 사용하므로, 문제가 BGSAVE가 아니라 AOF rewrite에 있다면 이 기능으로 해결되지 않습니다. 저장 진행 상황을 보여주는 current_save_keys_processed, current_save_keys_total, forkless_estimated_seconds_remaining 값은 저장 도중 실제로 갱신되는 것도 확인했습니다.

측정 과정에서 확인한 점

글쓴이는 첫 번째 지연 시간 기록에서 서로 기준점이 다른 time.time()과 time.perf_counter()를 빼는 실수를 발견했습니다. 두 시계 값을 직접 비교한 탓에 CSV의 시각이 잘못됐지만, 지연 시간 값만 집계한 결과에는 영향이 없었습니다. 이후 실행 시작 시각을 한 번 기록하고 monotonic clock의 경과 시간을 더하는 방식으로 고친 뒤 해당 실험을 다시 수행했습니다. 측정 도구의 시계 기준도 결과를 믿기 전에 점검해야 한다는 설명입니다.

dev.to 반응

  • @anh_nguynvn_0478e614ba — 스냅샷 시간이 70% 늘어나는 건 상당한 비용입니다. 쓰기 작업이 이미 IOPS 한계에 가까우면 특히 그렇습니다. 제가 고처리량 Redis 환경에서 겪은 바로는 일반적인 fork()의 메모리 급증이 더 큰 문제인 경우가 많았습니다. OOM 종료나 심한 스와핑을 일으켜 애플리케이션 전체를 멈추게 하기 때문입니다. 작은 컨테이너나 공유 인스턴스처럼 메모리가 빠듯하다면 350MB에서 10MB로 줄어드는 이점이 지연 시간 비용을 감수할 만큼 클 수 있습니다. 하지만 예측 가능한 꼬리 지연 시간이 중요한 프로덕션 데이터베이스라면 쓰기 처리량 15% 감소를 조심스럽게 봐야 합니다. 쓰기 부하가 몰릴 때 잠금 경합이 늘어나는지도 함께 테스트해 볼 만합니다.
    • @alexgeorgiev17 — fork 쪽에 관한 말씀에 동의합니다. 예상하지 못한 RSS 350MB 증가는 메모리가 빠듯한 컨테이너를 OOM으로 몰아넣을 수 있고, 서버 용량을 산정할 때도 잘 드러나지 않습니다. 다만 놀랍게도 꼬리 지연 시간은 forkless가 더 나았습니다. 일곱 차례 실행에서 최악의 PING 왕복 시간은 forkless 평균 7.2ms, fork 평균 20.1ms였습니다. forkless의 비용은 처리량 15% 감소와 더 긴 저장 시간이지 p99 지연 시간은 아니었습니다. fork 방식의 일시 정지가 더 거칠었습니다. forkless도 한 번 14.2ms까지 튀었습니다. 백그라운드 스레드가 아직 직렬화하지 않은 키에 쓰기가 들어올 때 발생하는 문서상 정지일 가능성이 높아 보입니다. 꼬리 지연은 줄어도 완전히 없어지지는 않습니다. IOPS는 측정하지 않았지만 RDB 파일 크기는 양쪽이 같으므로 2~3초가 더 걸리는 원인이 디스크라고 보기는 어렵습니다. 직렬화 스레드가 메인 스레드와 잠금을 두고 경쟁하는 쪽으로 보이며, 말씀하신 잠금 경합과도 맞습니다. 저장 시간과 쓰기 부하 급증의 관계를 살펴보는 건 좋은 다음 실험입니다. 저는 12초 전체의 평균만 봤습니다. 일정한 부하 대신 쓰기가 몰리는 조건으로 후속 테스트를 해볼 수도 있겠습니다. 아직 9.2.0-rc1이니 프로덕션에 바로 켜기보다는 각자 부하에서 시험해 보세요.
  • @wrobeltomasz — 비동기 I/O가 Valkey의 BGSAVE 성능에 미치는 영향도 설명하나요?

원문: dev.to / 번역·요약: Trawling