성장은 회수가 아니다

서버는 8천 커맨드에서 두 단어를 남기고 죽었다: region OOM. 언어의 메모리 모델은 「끝나는 프로그램」을 축으로 설계되어 있었다 —— 반환되는 일 없는 고정 용량 region. 단명한 도구는 모두 견뎠고, 서버는 견딜 수 없었다. 즉각의 수정은 region을 abort가 아니라 성장하게 하는 것, 그리고 byte arena의 thread 간 레이스를 멈추는 것. 정직했던 것은, 수정이 하지 않은 것을 적어 남긴 점이다: 서버는 살아남게 되었지만, 그 메모리는 여전히, 응한 모든 커맨드에 비례해 계속 성장하고 있었다.

merememory-modelregionsserverlanguage-design

크래시 메시지는 두 단어, region OOM이었고, 그것을 둘러싼 시스템에는 없는 정직함이 있었다. 언어의 메모리 모델은 region 기반이다: 값은 bump region에 살고, 스코프 있는 region 블록은 exit에서 해방하고, 그 외의 모든 것은 프로세스의 끝에 회수되는 하나의 program-lifetime region에 착지한다. 이 설계는 일관되어 있다 —— 끝나는 프로그램에게는. 서버가 커맨드마다 만지는 모든 값 —— parse된 인자 문자열, 조립 중인 reply, 그것들을 잇는 cons cell —— 은 그 program-lifetime region에 착지했고, 거기에는 고정 4 메가바이트의 용량과, 넘치면 즉시 abort라는 거동이 있었다. 작은 문자열 약 8천 커맨드 분이 그것을 채웠고, 프로세스는 멈췄다. 여기에 수수께끼는 아무것도 없다. 메모리 모델의 핵심 가정 —— 프로세스는 곧 끝난다 —— 이, 끝나지 않는 것이야말로 본분인 단 하나의 프로그램과 만났을 뿐이다.

버그는 abort이지 용량이 아니다

첫 수리는, 크래시가 녹여서 하나로 만들고 있던 두 가지를 분리했다: region의 고정 크기와, 넘쳤을 때의 거동이다. region은 bump block의 체인이 되었다 —— 현재 block이 차면, 기하급수적으로 큰 block을 잇는다. block은 결코 이동하지 않으므로, 이미 배부된 모든 포인터는 유효한 채다. 스코프 블록은 종전대로 exit에서 체인 전체를 해방한다. 성장은 값싸고 결정적이며, 당면한 문제를 풀었다: 8천 커맨드에서 죽던 같은 부하 시험이 8만을 넘어 달렸고, 메모리는 벽에 부딪히는 대신 매끄럽게 넓어졌다. 같은 수리는 바로 옆의, 더 조용한 위험도 고쳤다: descriptor 수준 FFI가 쓰는 평탄한 byte arena는, 하나의 bump pointer를 모든 thread가 무방비・무검사로 공유하고 있었다 —— spawn된 handler들은 거기서 경주했고, 끝을 조용히 써서 넘을 수 있었다. mutex와, 큰 소리로 실패하는 bounds check가 붙었다. 어느 변경도 영리하지 않다. 둘 다, 「단명한 호출자가 한 명」을 가정하는 runtime과, 기대어지기를 상정하는 runtime의 차이일 뿐이다.

수정이 하지 않는 것을 적어 남긴다

그 릴리스의 changelog에서 중요한 한 문장은, 고친 것에 대한 것이 아니었다. 이렇게 적었다: 성장은 회수가 아니다. 서버는 더는 크래시하지 않지만, 커맨드는 여전히 하나마다 수백 바이트를, 결코 줄지 않는 region에 두고 간다 —— 메모리는 총 트래픽에 비례해, 영원히 성장한다. 데모에게는 각주지만, 「이 언어로 실물 서버를 쓴다」는 생각에게는 실격 선고이고, abort보다 도화선이 길 뿐이다. 통과한 부하 시험으로 회를 맺고, 곡선에게는 나중에 말하게 할 수도 있었을 것이다. 프로젝트의 규율 —— 모든 dogfood repo에 PAIN 파일을 두는 것과 같은 것 —— 은, 수정과 같은 숨으로 잔여 문제에 이름을 붙이기를 요구했고, 이름 불린 그 문제는, 미래의 프로덕션 이야기에서 매복하는 놀람이 아니라, 언어의 작업 큐의 선두가 되었다.

진짜 문제의 형태

정밀하게 이름 짓는 것은, 문제의 구조도 밝혔다. 커맨드마다의 값은, reply가 쓰인 순간 논리적으로는 죽는다 —— 아무것도 그것들을 참조하지 않는다 —— 하지만 runtime에 그것을 알 방법은 없다. 할당의 99 퍼센트와, 살아남아야 하는 1 퍼센트(map에 실제로 store된 값, thread 사이를 비행 중인 메시지)가, 같은 region에 섞여 쌓이기 때문이다. 언어에는 스코프의 exit에서 해방하는 region 블록이 이미 있었다. 빠져 있던 것은, 첫째, 보통의 값 —— 문자열, 리스트 —— 이 거기에 사는 방법. 둘째, 스코프보다 오래 살아야 하는 값이 안전하게 밖으로 나가는 길의 설명이다. 이것은 앨러케이터의 문제가 아니다. 소유의 문제다. 그리고 그것은 깨끗하게 두 단계로 갈라졌다 —— 먼저 container가 내용물을 소유하게 하고, 그다음 스코프가 자신의 할당을 회수하게 한다 —— 는 것이 다음 두 화다. 청구서를 배달한 서버는, 수리가 언젠가 들이밀어질 잣대로서, 무변경인 채 기다리고 있었다.

← Back to Mere: 언어를 만들다