클라우드플레어, 메모리 100TB 절감 성공

BBetter Stack
Computing/SoftwareInternet Technology

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다음 영상에서 뵙겠습니다.

Key Takeaway

클라우드플레어는 DNS 캐시 시스템의 데이터 구조와 메모리 패킹 방식을 개선하여 100테라바이트의 메모리를 확보하고 조회 성능을 향상시켰다.

Highlights

  • 클라우드플레어는 공용 DNS 리졸버 '빅 파인애플'의 캐시 시스템 최적화를 통해 100테라바이트의 메모리를 절감했다.

  • 2,500억 개가 넘는 DNS 캐시 항목에서 벡터를 박스로 교체하여 과도하게 할당된 힙 공간을 제거했다.

  • 세 개의 DNS 응답 레코드 목록을 구분선이 포함된 단일 목록과 2바이트 오프셋으로 통합하여 필드 크기를 줄였다.

  • 열거형 변인을 박싱하는 대신 원시 바이트로 저장하는 방식을 도입하여 메모리 지역성을 되찾고 조회 속도를 19% 향상시켰다.

  • 프로덕션 환경 배포 결과 P99 상주 메모리가 43% 감소하고 삽입 처리량이 43% 증가했다.

Timeline

DNS 캐시 최적화 개요와 배경

  • 클라우드플레어의 공용 DNS 리졸버 '빅 파인애플'은 러스트로 작성되었다.
  • 이 시스템은 언제든 2,500억 개가 넘는 DNS 캐시 항목을 저장한다.
  • 항목당 단 1바이트만 낭비되어도 전체 플릿에서 250기가바이트 이상의 메모리가 소모된다.

클라우드플레어는 거대한 규모의 DNS 리졸버를 운영하며, 작은 메모리 낭비도 전체 인프라 비용에 큰 영향을 미친다. 이들은 캐시 시스템에 간단한 변경 사항들을 적용하여 메모리를 절감하고 속도를 높였다.

데이터 타입 변경과 벡터 최적화

  • 캐시 항목의 수정되지 않는 벡터와 문자열 필드를 박스로 교체했다.
  • 미래 확장을 위한 여유 용량 예약 공간과 용량 필드 8바이트를 제거했다.
  • 이 변경만으로 2,500억 개 항목 기준 15테라바이트 이상의 메모리를 절약했다.

캐시에 기록된 데이터는 다시 수정되지 않기 때문에 힙 공간이 과도하게 할당된 채 낭비되고 있었다. 크기가 고정된 박스를 사용함으로써 불필요한 용량 필드와 여유 공간을 없애고 메모리를 대폭 아꼈다.

레코드 목록 통합과 소유자 중복 제거

  • 세 개의 별도 레코드 목록을 두 개의 작은 오프셋이 포함된 하나의 목록으로 통합했다.
  • 도메인 이름이 쿼리 도메인과 일치하는 경우 소유자를 중복 저장하지 않고 none을 저장했다.
  • 불필요한 헤더와 힙 할당을 제거하여 구조체 크기를 극적으로 줄였다.

DNS 응답의 여러 레코드 목록은 항상 함께 읽히고 기록된다는 특성이 있다. 이를 하나의 목록으로 합치고 16비트 오프셋을 도입했으며, 쿼리 도메인과 겹치는 소유자 필드를 생략하여 추가적인 힙 할당을 방지했다.

열거형 박싱과 원시 바이트 저장 방식

  • 가장 큰 변인 크기에 맞춰지던 열거형의 패딩 문제를 해결하기 위해 박싱과 원시 바이트 저장 방식을 도입했다.
  • 레코드 데이터를 단 하나의 박스형 u8 배열로 합쳐 jemalloc 빈 올림 문제를 해결했다.
  • 메모리 지역성을 회복하고 대부분의 레코드를 와이어 형식 그대로 복사하여 조회 속도를 높였다.

대다수를 차지하는 A 및 AAAA 레코드가 큰 변인들로 인해 과도한 공간을 차지하는 문제를 개선하기 위해 원시 바이트 배열 방식을 적용했다. 이를 통해 할당 낭비와 캐시 미스 문제를 동시에 해결하고 파싱 부하를 줄였다.

최적화 성과와 전사 배포 결과

  • 엔트리당 용량이 953바이트에서 420바이트로 56% 감소했다.
  • 실제 트래픽에서 P99 상주 메모리가 43% 줄어 총 100테라바이트의 메모리를 확보했다.
  • 삽입 처리량은 43% 증가하고 조회 속도는 19% 빨라졌다.

다섯 가지 저수준 변경 사항을 프로덕션 환경에 배포한 결과, 서버 130대 분량의 RAM이 절감되는 성과를 거두었다. 클라우드플레어는 확보된 여유 공간을 활용해 더 큰 캐시를 운영할 계획이다.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video