BumpAllocator: 엔진 밖에서 자라던 메모리를 엔진 안으로
GritDB 분석 엔진이 값마다 하던 malloc을, 엔진이 세는 큰 블록 안에서 잘라 쓰는 bump 할당으로 바꿨습니다. 엔진 밖 할당(glibc 힙)은 63 % 줄고, 오래 돌 때 메모리가 늘어나는 폭은 1/6이 됐습니다.
GritDB는 PostgreSQL 엔진에 열 기반 분석 엔진을 더한 이중 엔진 데이터베이스입니다. 이 글은 분석 엔진이 메모리를 다루는 방식을 바꾼 이야기입니다.
엔진이 세지 못하던 메모리(glibc 힙)가 63 % 줄었고, 오래 돌 때 메모리가 늘어나는 폭은 1/6이 됐습니다. 재기동 없이 200라운드를 돌려도 최대 메모리는 83 GiB 아래에 머뭅니다.
| 한눈에 보기 | 적용 전 | 적용 후 | 변화 |
|---|---|---|---|
| 엔진 밖 할당 (glibc 힙) | 86.7 GiB | 31.9 GiB | -63 % |
| 오래 돌 때 최대 메모리가 늘어나는 폭 | 한 번에 +0.64 GiB | 한 라운드에 +0.10 GiB | 1/6 |
| 엔진이 한도 안에서 관리하는 몫 | 엔진 메모리 한도의 15 % | 96 % | 넓어짐 |
| 최대 메모리 (VmHWM) | 118.3 GiB | 74.1 GiB | -37 % |
상황: 엔진이 세지 못하는 메모리가 자랐다
행 하나를 메모리에 담을 때 malloc을 2 ~ 8번 불렀습니다. 분석 엔진의 UNIFY는 등록한 테이블의 입력, 수정, 삭제를 메모리에 행으로 쌓았다가 열 기반 파일로 옮깁니다. 행에는 길이가 바뀌는 값이 여럿 있어서, 값마다 C 라이브러리(glibc)의 malloc을 따로 불렀습니다.
그 메모리는 엔진의 메모리 한도 밖에 있었습니다. 엔진은 정해진 한도 안에서 메모리를 세고 관리합니다. 그런데 glibc 힙에서 받은 행 메모리는 엔진이 세지도, 한도로 막지도 못했습니다.
쓴 메모리가 운영체제로 돌아가지 않았습니다. 행을 만드는 스레드와 지우는 스레드가 다릅니다. glibc는 스레드마다 힙을 따로 두고, 돌려받은 메모리를 바로 운영체제에 돌려주지 않습니다. 그래서 오래 돌수록 프로세스 메모리가 올라갔습니다.
| 문제 | 결과 |
|---|---|
| 행마다 malloc 여러 번 | 할당과 반납이 행 수의 몇 배로 일어난다 |
| 엔진 한도 밖 메모리 | 엔진이 세지도, 막지도 못한다 |
| 돌아가지 않는 메모리 | 오래 돌수록 프로세스 메모리가 오른다 |
과제: 모든 행 메모리를 엔진이 세고 한도 안에 둔다
목표는 세 가지였습니다.
- 행 하나에 할당 한 번: 행의 값들을 한 조각에 모은다.
- 엔진이 세는 메모리: 메모리를 엔진의 메모리 관리에서 받아 종류별로 세고, 한도 안에 둔다.
- 오래 돌아도 평탄하게: 같은 부하를 수백 번 되풀이해도 메모리가 오르지 않는다.
해결: 큰 블록에서 잘라 쓰고, 블록째 돌려준다
bump 할당은 큰 블록 안의 커서만 앞으로 밀어 메모리를 내줍니다. 할당 하나가 덧셈과 비교 한 번으로 끝납니다. 대신 가운데 조각 하나만 따로 돌려줄 수 없어서, 수명이 비슷한 데이터끼리 같은 블록에 담습니다. PostgreSQL도 정렬할 때 행을 담는 메모리에 같은 방식(Bump 메모리 컨텍스트)을 씁니다.
수명에 따라 블록을 두 가지로 나눴습니다.
| 블록 | 담는 것 | 돌려주는 때 |
|---|---|---|
| 행 블록 | 메모리에 쌓인 행 | 행이 열 기반 파일로 옮겨져 지워질 때 조각마다, 블록의 조각이 다 돌아오면 블록째 |
| 묶음 블록 | 열 기반 파일로 옮기는 행 묶음 | 그 묶음을 다 쓰면 블록째 한 번에 |
블록은 엔진의 메모리 관리에서 받습니다. 그래서 엔진이 블록을 종류별로 세고, 메모리 한도 안에 둡니다.
표마다 미리 잡던 메모리도 줄였습니다. 전에는 표를 하나 등록할 때마다 색인용 메모리를 40 MiB씩 미리 잡았습니다. 이제는 약 1 KB로 작게 시작해 파일이 늘 때 키웁니다. 지운 표의 메모리도 파일 정리를 기다리지 않고 돌려받습니다.
디스크를 기다리는 동안에도 메모리 정리가 이어집니다. 열 기반 파일을 디스크에 쓰고 기다리는 동안에도, 파일을 쓰는 쪽이 메모리 정리를 막지 않게 했습니다.
| 행 하나 | 적용 전 | 적용 후 |
|---|---|---|
| 메모리 할당 | glibc 힙에서 malloc 2 ~ 8번 | 엔진에서 받은 블록 안에서 1번 (긴 문자열 값은 아직 따로) |
| 엔진이 세는가 | 세지 못한다 | 종류별로 세고 한도 안에 둔다 |
| 돌려주는 단위 | 값마다 free | 조각 · 블록 단위 |
결과: 메모리가 엔진 한도 안으로 들어왔다
같은 서버에서 적용 전과 후의 엔진에 같은 부하를 오래 되풀이했습니다. 적용 후 시험은 부하를 더 얹은 라운드라 적용 전보다 무거운 조건입니다.
| 시험 조건 | 내용 |
|---|---|
| 서버 | AMD Ryzen 7 9800X3D (8코어 16스레드) · 메모리 186.3 GiB · 적용 전 · 후 같은 서버 |
| 시험 | ① 긴 쓰기 부하: 대량 입력 · 수정 · 삭제를 되풀이 ② 표 만들고 지우기: 한 번에 표 500개를 만들고 지움 ③ 라운드: ① · ②에 다른 부하 둘을 더한 묶음 · 재기동 없이 200번 |
| 날짜 | 적용 전 2026-10-06 ~ 14 · 적용 후 2026-10-15 ~ 23 (개발 빌드) |
| 잰 값 | 분석 엔진 프로세스 · RSS: 지금 쓰는 메모리 · VmHWM: 그때까지 가장 많이 쓴 메모리 · 엔진 밖 할당: 프로세스 메모리에서 엔진이 관리하는 메모리를 뺀 값 |
| 맨 위 표의 견준 값 | 적용 전: ①을 10번 돌고 전체 회귀 시험을 이어 돈 엔진 · 적용 후: 100라운드를 돈 엔진 (200라운드 값은 따로 적음) |
1. 오래 돌려도 최대 메모리가 오르지 않는다
적용 전에는 33번 만에 127 GiB까지 올랐고, 적용 후에는 200라운드를 돌아도 83 GiB 아래입니다.
| 값 | 적용 전 | 적용 후 |
|---|---|---|
| 처음 기록 | 4번째 109.2 GiB | 1라운드 63.2 GiB |
| 마지막 | 33번째 127.2 GiB | 100라운드 74.1 GiB · 200라운드 82.6 GiB |
| 늘어나는 폭 | 한 번에 +0.64 GiB | 한 라운드에 +0.10 GiB |
| 되풀이한 결과 | 39번째에 시간 초과로 멈춤 | 100번 모두 통과 · 이어서 200라운드 |
2. 표를 만들고 지워도 메모리가 돌아온다
적용 전에는 한 번에 12.3 GiB씩 늘어 8번째에 멈췄고, 적용 후에는 100번을 돌아도 21 ~ 25 GiB입니다.
| 값 | 적용 전 | 적용 후 |
|---|---|---|
| 시작 | 73.4 GiB | 20.8 GiB |
| 한 번에 늘어난 양 | +12.3 GiB | +0.013 GiB (4 ~ 100번째) |
| 되풀이한 결과 | 8번째에 175 GiB에서 서버 메모리가 바닥나 멈춤 | 100번 모두 통과 · 24.0 GiB |
3. 늘어난 메모리는 엔진 밖 할당이었다
적용 전에 늘어난 것은 모두 엔진 밖 할당(glibc 힙)이었습니다. 엔진이 관리하는 메모리는 그동안 41 GiB에 머물렀습니다.
| 값 | 적용 전 (0 → 5번째) | 적용 후 (0 → 100번째) |
|---|---|---|
| 엔진 밖 할당 (glibc 힙) | 31.8 → 100.1 GiB | 0.1 → 2.9 GiB |
| 엔진이 관리하는 메모리 | 41.0 → 41.2 GiB | 20.7 → 21.0 GiB |
4. 그 밖에 좋아진 것
| 항목 | 적용 전 | 적용 후 | 변화 |
|---|---|---|---|
| 행 하나의 할당 | malloc 2 ~ 8번 | 1번 | 1/2 ~ 1/8 |
| 표 하나를 등록할 때 잡는 메모리 | 40 MiB | 약 1 KB | 1/40,000 |
| 메모리가 모자라 캐시를 비운 양 | 시간당 5.8 TB | 시간당 6 ~ 11 GB | -99 % |
| 적재 뒤 분석 엔진에 다 반영되기를 기다린 시간 (가장 길 때) | 180.9초 (시간 초과) | 23.1초 | -87 % |
| 되풀이할수록 늘던 처리 시간 | 17번에 +8.0분 | 100번에 +2.0분 | 기울기 약 1/20 |
대가와 남은 것
나빠진 것
- 엔진 메모리 한도의 기본값이 서버 메모리의 30 %에서 50 %로 올랐습니다. 엔진이 세는 메모리의 범위가 넓어졌기 때문입니다. 메모리 설정은 전과 같이 한도 하나뿐입니다. 메모리가 작은 서버(16 GiB)에서는 엔진 몫과 PostgreSQL 메모리를 함께 봅니다.
- 엔진 밖에 남은 몫이 아직 조금씩 늡니다. 열 기반 파일의 키 색인 일부가 아직 엔진 한도 밖에 있어서, 100라운드에 약 6 GiB씩 늡니다(200라운드에 11.6 GiB). 오래 돌 때 늘어나는 몫의 대부분이 여기에 있습니다.
그대로인 것
- 시험 동안 데이터 유실은 없었습니다. 전체 회귀 시험에서 실패한 시험도 없었습니다.
- 같은 부하를 한 번 처리하는 시간은 그대로였습니다(처음 10번 평균 12.6분 → 12.8분).
- 새로 생긴 메모리 설정은 없습니다.
다음
- 엔진 밖에 남은 키 색인 메모리를 엔진이 관리하는 메모리로 옮깁니다.
- 긴 문자열 값처럼 아직 따로 받는 할당도 같은 블록으로 옮깁니다.
- 서버 메모리 크기에 맞는 메모리 한도 기본값을 정합니다.
UNIFY가 메모리의 행을 열 기반 파일로 옮기는 흐름은 아키텍처에 있습니다. GritDB가 무엇을 하는지는 제품 홈에, 실제 업무로 잰 결과는 사례에 있습니다.
이 글의 용어
- bump 할당: 큰 블록 안의 커서를 요청 크기만큼 밀어서 메모리를 내주는 방식. 조각 하나만 따로 돌려줄 수 없고, 블록 단위로 돌려준다.
- 엔진 밖 할당 (glibc 힙): C 라이브러리(glibc)의 malloc이 쓰는 메모리. 엔진의 메모리 한도 밖에 있다.
- 엔진 메모리 한도: 분석 엔진이 쓸 수 있는 메모리의 상한. 서버 메모리의 비율로 정해진다.
- RSS · VmHWM: RSS는 프로세스가 지금 쓰는 메모리, VmHWM은 그때까지 가장 많이 쓴 메모리다. Linux가 프로세스마다 기록한다.
- 라운드: 이 글의 시험에서 부하 넷을 차례로 한 번씩 돈 묶음.
- 커서: 이 글에서는 블록 안에서 다음에 내줄 자리를 가리키는 표시. SQL의 커서와는 다른 뜻이다.
- UNIFY: 분석 엔진에서 등록한 테이블의 변경을 메모리에 모았다가 열 기반 파일로 옮기는 부분.
고친 기록
- 2026-10-29: 측정 그래프 셋(오래 돌 때 최대 메모리 · 표 만들고 지우기 · 엔진 밖 할당)과 그 밖에 좋아진 것, 해결의 블록 두 가지를 더했습니다.
- 2026-10-28: 처음 글의 숫자는 개발 중에 잰 값(2026-10-12 ~ 14)이었습니다. 마지막 측정의 숫자와 시험 조건으로 바꾸고, 나빠진 것과 그대로인 것을 더했습니다.