Transcript
00:00:001바이트가 실제로 얼마나 많은 비용을 초래할까요?
00:00:02글쎄요, 클라우드플레어 규모에서 항목당 단 1바이트를 낭비해도
00:00:04전체 플릿에 걸쳐 250기가바이트가 넘는 메모리가 소모되며,
00:00:06비용이 발생합니다.
00:00:08최근에 그들은 항목당 메모리를 실제로 절반으로 줄여
00:00:11100테라바이트의 메모리를 확보했습니다.
00:00:13요즘 같은 경제 상황에서는 엄청난 금액입니다.
00:00:15그들은 캐싱 시스템에 매우 간단한 다섯 가지
00:00:17변경 사항을 적용하여 이를 달성했으며,
00:00:18동시에 DNS 속도도 더 빠르게 만들었습니다.
00:00:21어떻게 이런 작업을 해냈는지 자세히 살펴보겠습니다.
00:00:27여러분도 1.1.1.1에 대해 들어보셨을 것입니다.
00:00:30클라우드플레어의 공용 DNS 리졸버로,
00:00:32betterstack.com과 같이 접속하고 싶은 도메인을 받아
00:00:34betterstack.com,
00:00:35해당 도메인의 실제 IP가 무엇인지 찾아내는 방식으로 작동합니다.
00:00:39이 작업을 실제로 수행하는 주체는
00:00:40'빅 파인애플(Big Pineapple)'이라고 불리며 러스트로 작성되었습니다.
00:00:42예상하시다시피,
00:00:43이 프로그램은 엄청나게 바쁘게 돌아가는 소프트웨어로,
00:00:46언제든 2,500억 개가 넘는 DNS 캐시 항목을 저장합니다.
00:00:50이를 통해 누군가 betterstack.com에 접속한 후
00:00:522초 뒤에 다른 사람이 또 접속하더라도
00:00:53전체 DNS 계층 구조를 다시 거칠 필요 없이
00:00:56답변을 메모리에 유지하다가 바로 반환할 수 있습니다.
00:00:59하지만 오프닝에서 언급했듯이,
00:01:012,500억 개의 캐시 항목이 있다는 것은 항목당 1바이트만 낭비되어도
00:01:04250기가바이트의 램이 낭비된다는 뜻입니다.
00:01:07이러한 다섯 가지 변경 사항이 그토록 큰 영향을 미친 이유가 바로 여기에 있습니다.
00:01:10첫 번째는 용량 비용입니다.
00:01:12과거 캐시 항목의 구조는 다음과 같았습니다.
00:01:14타임스탬프, 유효 생존 시간(TTL), 적중 카운터,
00:01:16그리고 여러 개의 벡터가 있었습니다.
00:01:17응답(Answer) 레코드 벡터,
00:01:18권한(Authority) 레코드 벡터,
00:01:20추가(Additional) 레코드 벡터,
00:01:21오류 벡터,
00:01:22그 외에도 수많은 항목이 있었죠.
00:01:23러스트에 익숙하지 않은 분들을 위해 설명하자면,
00:01:25벡터는 크기가 조절 가능한 목록을 위한 기본 데이터 유형이며,
00:01:27메모리 상에서 세 가지 요소로 구성됩니다.
00:01:29포인터, 길이, 그리고 용량입니다.
00:01:31확장을 위해 얼마나 많은 공간이 예약되어 있는지를 나타내므로,
00:01:32데이터를 추가할 때마다 메모리를 재할당할 필요가 없습니다.
00:01:35실제로 데이터가 늘어나는 경우에는 매우 유용합니다.
00:01:38하지만 그들은 실제 DNS 캐시 항목들을 살펴보았고,
00:01:40모든 데이터가 캐시에 기록된 후
00:01:42다시는 수정되지 않는다는 점을 깨달았습니다.
00:01:44읽기만 할 뿐이므로,
00:01:45이 벡터들은 실제로 커지지 않았습니다.
00:01:47즉, 힙 공간이 과도하게 할당된 채로
00:01:49그냥 낭비되고 있었다는 뜻입니다.
00:01:50항목 8개를 담을 용량의 벡터가 있는데
00:01:525개만 저장되어 있다면
00:01:533개의 슬롯이 사용되지 않고 남게 되며,
00:01:55용량 필드 자체는 usize 타입으로
00:01:578바이트의 메모리를 차지하는데,
00:01:58이들에게는 전혀 필요 없는 공간이었습니다.
00:02:00이 문제를 해결하는 방법은 정말 간단했습니다.
00:02:02벡터를 박스(Box)로 바꾸기만 하면 됩니다.
00:02:04박스의 크기는 생성된 시점의 크기로 고정되므로
00:02:07용량 필드가 필요하지 않으며,
00:02:08있는 그대로의 데이터만 정확히 저장합니다.
00:02:10미래의 확장을 위해 여유 용량을 미리 할당할 필요가 없습니다.
00:02:13또한 이들은 이와 동일한 절감 효과를
00:02:15문자열 필드에도 적용할 수 있음을 깨달았습니다.
00:02:16문자열도 본질적으로는 u8의 벡터이기 때문에
00:02:18텍스트가 늘어나거나 줄어들 수 있지만,
00:02:20그럴 필요가 없다면
00:02:21박스형 문자열 슬라이스를 사용할 수 있습니다.
00:02:23즉, '이 문자열은 변하지 않는다'는 뜻입니다.
00:02:26결과적으로 하나의 캐시 항목에는
00:02:278개의 벡터 및 문자열 필드가 있었으므로,
00:02:29이를 박스로 교체함으로써 필드당 8바이트를 절약하여
00:02:31항목당 64바이트를 아낄 수 있었고,
00:02:33벡터가 향후 확장을 위해 예약해 두는
00:02:35초과 힙 공간도 없앨 수 있었습니다.
00:02:36이를 2,500억 개의 캐시 항목으로 환산하면 총 절감량이 15테라바이트를
00:02:392,500억 개의 캐시 항목으로 확장했을 때 말입니다.
00:02:42데이터 타입 변경 하나로 이 모든 것을 이뤄낸 것입니다.
00:02:45다음 변경 사항으로 넘어가서,
00:02:46그들은 그러한 목록 중 일부를 살펴보고는
00:02:48'우리가 과연 이 목록들이 필요하기는 한가?'라고 물었습니다.
00:02:50DNS 응답에는 세 가지 항목이 포함됩니다.
00:02:52answer, authority, additional인데요,
00:02:54앞서 보았듯이
00:02:55이들은 들어온 그대로 세 개의
00:02:57별도 목록으로 캐시되었습니다.
00:02:58비록 첫 번째 변경에서 용량 필드를 제거하긴 했지만,
00:03:01각 목록은 여전히 포인터와 길이로 이루어져 있으므로
00:03:03(8바이트 + 8바이트) 곱하기 3,
00:03:05즉 총 48바이트와
00:03:07세 번의 별도 힙 할당이 필요했습니다.
00:03:09하지만 이 목록들이 실제로 어떻게
00:03:10사용되고 있는지 살펴보았을 때,
00:03:12항상 함께 읽히고, 항상 함께 기록되며,
00:03:13항상 정확히 같은 순서로
00:03:14존재한다는 사실을 깨달았습니다.
00:03:16따라서 세 개의 목록 대신
00:03:17구분선 두 개가 포함된 하나의 목록으로 처리할 수 있었습니다.
00:03:20즉, 세 개의 목록이 하나의 레코드 목록과
00:03:22'여기서 답변이 끝난다', '여기서 권한이 끝난다'를 나타내는
00:03:25두 개의 작은 오프셋으로 바뀌었습니다.
00:03:26DNS 응답에 40억 개의 레코드가 들어갈 일은 절대 없으므로,
00:03:2916비트 부호 없는 정수면 오프셋 용도로 충분하며,
00:03:33각각 단 2바이트만 소모됩니다.
00:03:34이 말은 결과적으로
00:03:3516바이트짜리 헤더 두 개를 2바이트 숫자 두 개로 교체하여
00:03:39항목당 28바이트를 절약했고,
00:03:41이를 통해 러스트가 필드 사이의 여유 공간을 제거할 수 있게 되어
00:03:44구조체 크기가 단순히 제거된 필드 그 이상으로 작아졌습니다.
00:03:47그들은 이 개념을 한 단계 더 발전시켜
00:03:49여러 개의 불리언 필드를 단일 비트 플래그로 통합했습니다.
00:03:52세 번째 변경 사항으로 넘어가서,
00:03:53같은 이름을 두 번 말하지 않으면 어떻게 될까요?
00:03:55모든 DNS 레코드에는 소유자가 포함되어 있으며,
00:03:57이는 해당 레코드가 속한 도메인 이름일 뿐입니다.
00:04:00따라서 betterstack.com을 조회하고 레코드를 받아올 때
00:04:03각각의 레코드마다 betterstack.com이 적혀 있습니다.
00:04:05하지만 그 정보는 다소 중복적입니다.
00:04:08질문을 던진 사람이 바로 당신이므로
00:04:09이미 도메인 이름을 알고 있기 때문입니다.
00:04:11클라우드플레어는 이미 해당 쿼리 도메인을 캐시 키로 저장하고 있었는데,
00:04:14왜 소유자로서 다시 저장해야 할까요?
00:04:17그럴 필요가 없습니다.
00:04:17그래서 클라우드플레어는 이 필드를 선택적 박스 이름으로 간단히 변경하여,
00:04:20소유자가 쿼리 도메인과 일치하면
00:04:22none을 저장하고,
00:04:23진짜로 다를 경우에만
00:04:24(CNAME 체인 같은 몇 안 되는 예외 상황처럼 말이죠)
00:04:27이전처럼 소유자를 저장하도록 했습니다.
00:04:29이렇게 함으로써 두 값이 일치하는 일반적인 경우에
00:04:32레코드당 통째로 힙 할당 하나를 절약할 수 있었습니다.
00:04:34즉, 이 절감은 캐시에 어떤 데이터가 있는지 살펴보고
00:04:37중복된 값이 존재한다는 사실을 깨달은 데서 비롯되었습니다.
00:04:39네 번째 절감은 IP 주소별 144바이트 문제입니다.
00:04:43이들의 레코드 데이터는 A 레코드, AAAA 레코드, 텍스트,
00:04:47SVCB, NAPTR이 포함된 러스트 열거형(enum)으로, 모든 타입을 하나로 아우르고 있었습니다.
00:04:51하지만 열거형에는 문제가 있습니다.
00:04:52항상 가장 큰 변수만큼의 크기를 차지한다는 점입니다.
00:04:54해당 타입의 모든 값은 실제로 그 공간이 필요하든 아니든
00:04:57동일한 양의 공간을 차지합니다.
00:04:58이 경우 NAPTR이 가장 큰 값으로 136바이트를 차지하며,
00:05:03열거형에 태그와 패딩을 더하면 144바이트가 됩니다.
00:05:07이를 단순한 IPv4 주소인 A 레코드에 필요한 용량과 비교해 보면,
00:05:12단 4바이트만 필요합니다.
00:05:13즉 캐시에 있는 모든 A 레코드가 144바이트짜리 상자에 들어 앉아 있으면서
00:05:17그중 4바이트만 쓰고 있던 셈이며,
00:05:19실제 트래픽의 대다수는 A 레코드와 AAAA 레코드가 차지합니다.
00:05:22클로드가 자체 구성한 벤치마크에서는 A 레코드가 56%, AAAA가 25%이므로 캐시의 대부분이 그냥 패딩이었던 것입니다.
00:05:29이를 해결한 방법이 바로 우리가 늘 쓰던 박싱입니다.
00:05:32크기가 크고 흔한 A 및 AAAA 레코드는 인라인으로 두고, 큰 변인들은 박스 처리를 했는데요.
00:05:36이제 text, SVCB, NAPTR은 포인터 뒤에 숨게 되므로
00:05:40열거형은 남은 것 중 가장 큰 것인 16바이트짜리 IPv6 주소만큼만 크기를 가지면 됩니다.
00:05:45결과적으로 A 또는 AAAA 레코드 하나당 120바이트씩 절약된 셈입니다.
00:05:50하지만 이 변경에는 실제로 대가가 따릅니다.
00:05:52변인을 박싱하면 그 데이터는 캐시 엔트리 내부가 아니라
00:05:55힙의 완전히 다른 별도 영역에 위치하게 되며,
00:05:58그로 인해 두 가지 새로운 비용이 발생합니다.
00:06:00첫 번째는 할당기입니다.
00:06:02클라우드플레어는 jemalloc을 사용하는데, jemalloc은 요청한 바이트 수를 정확히 주는 것이 아니라
00:06:06할당을 고정 크기 빈으로 그룹화하여 가장 가까운 크기로 올림 처리합니다.
00:06:10따라서 32바이트를 요청하는 text 레코드는 32바이트 빈에 들어가 낭비가 없지만,
00:06:15MX 레코드는 40바이트를 요청해 48바이트로 올림 되면서 8바이트를 조용히 날리게 됩니다.
00:06:21두 번째 비용은 지역성입니다.
00:06:22박싱 전에는 한 엔트리의 모든 레코드 데이터가 연속된 메모리 블록 하나에 들어 있었지만,
00:06:27박싱 후에는 각각 딴 곳에 살게 되므로 읽으려면 포인터를 따라가야 합니다.
00:06:31만약 그 포인터가 엔트리의 나머지 부분과 먼 곳을 가리키고 있다면
00:06:34CPU는 그것을 읽기 위해 완전히 새로운 캐시 라인을 가져와야 합니다.
00:06:37따라서 박싱이 패딩 문제를 해결하긴 했지만 새로운 문제를 만들어낸 셈입니다.
00:06:41이때 클라우드플레어는 '러스트 타입으로 저장하지 않으면 어떨까?'라는 생각을 하게 됩니다.
00:06:45자, 이것이 다섯 번째 변경 사항입니다.
00:06:46클라우드플레어는 이를 절충안으로 설명했습니다. 캐시 엔트리의 나머지는 일반적인 구조화된
00:06:50필드로 유지하되, 레코드 데이터 자체는 원시 바이트로 저장하는 것입니다.
00:06:54따라서 레코드마다 열거형이나 박스를 쓰는 대신, 엔트리 전체에 단 하나의 박스형 u8 배열을 두고
00:07:00각 레코드를 2바이트 길이 접두사 뒤에 데이터가 오는 형식으로 작성합니다.
00:07:03이렇게 하면 방금 이야기한 두 가지 비용이 모두 사라집니다.
00:07:06그 수많은 개별 박스 할당이 모든 레코드 데이터를 위한 단 하나의 할당으로 합쳐지므로
00:07:10레코드별로 jemalloc 빈에 맞춰 올림 되는 일이 더 이상 없고,
00:07:13다시 연속해서 촘촘히 패킹되므로 박싱이 빼앗아 갔던 캐시 지역성을 되찾을 수 있습니다.
00:07:18게다가 보너스로 조회 속도도 더 빨라집니다.
00:07:21예전에는 캐시 조회 때마다 메모리에 파싱된 레코드가 있어서
00:07:25어디로든 전송하기 전에 필드별로 다시 DNS 와이어 형식으로 직렬화해야 했습니다.
00:07:30하지만 이제는 이미 DNS 와이어 형식으로 되어 있어서 대부분의 레코드 타입은 버퍼에서 발신 메시지로
00:07:35바로 복사됩니다.
00:07:37여전히 파싱이 필요한 것은 도메인 이름을 포함하는 레코드들,
00:07:40즉 CNAME, NS, MX, SOA뿐이며
00:07:43그것도 DNS 이름 압축 때문에 어차피 그 이름들을 다시 써야 하기 때문입니다.
00:07:47따라서 이 최종 변경은 메모리를 줄이고 핵심 경로상의 엄청난 작업 부하를 없애줍니다.
00:07:51자, 이렇게 캐시에 대한 5가지 저수준 변경 사항을 거쳤으며,
00:07:54이를 결합하니 엔트리당 용량이 953바이트에서 420바이트로 줄어들었습니다.
00:08:00즉 56% 더 작아졌고, 엔트리당 실제로 할당되는 메모리도 1.1킬로바이트에서
00:08:05461바이트로 줄어들어 58% 감소했습니다.
00:08:09게다가 삽입 처리량도 43% 증가하여
00:08:12초당 62만 5천 개의 엔트리에서 89만 3천 개로 늘어났고,
00:08:17조회 속도도 828나노초에서 670나노초로 19% 더 빨라졌습니다.
00:08:23이들은 올해 프로덕션 환경 전반에 이 변경 사항을 배포했으며,
00:08:25P99 상주 메모리가 9.3기가바이트에서 5.3기가바이트로 줄어
00:08:29실제 트래픽에서 43%가 절감되었습니다.
00:08:32이를 전사적으로 환산하면 약 100테라바이트의 메모리가 확보된 것이며,
00:08:351세대 13세대 서버 130대에 맞먹는 RAM 분량입니다.
00:08:38이제 이들은 남은 여유 공간을 활용해 더 큰 캐시를 운영함으로써
00:08:41속도를 훨씬 더 높일 계획입니다.
00:08:43저는 이 블로그 글이 우리가 앞으로 내려야 할 결정을 잘 보여주기 때문에 아주 마음에 듭니다.
00:08:46이런 걸 만들기 전에 미리 앉아서 모든 최적화를 다 계산해 뒀어야 했을까요,
00:08:50아니면 그건 너무 이른 최적화였을까요?
00:08:52생각해 보면 이 시스템이 처음 만들어진 2018년 당시에는
00:08:55클라우드플레어에게 RAM 비용이 지금처럼 그리 크지 않았을 것입니다.
00:08:59전체 글 내용이 아주 훌륭한 글이므로 아래에 링크를 남겨두겠습니다.
00:09:02여기에 대해 어떻게 생각하시는지 댓글로 남겨주시고,
00:09:03구독도 부탁드리며,
00:09:04늘 그렇듯 다음 영상에서 뵙겠습니다.
00:09:05다음 영상에서 뵙겠습니다.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video