스택 전체를 소유할 때 물리 패브릭, 프레임 크기, 오프로드, CPU 배치, 플로우 병렬성, IPv6 같은 각 계층을 조정해 온프레미스 인프라의 성능을 어떻게 끌어올릴 수 있는지 설명합니다.
스택 전체를 구축하는 일의 가장 큰 장점 중 하나는 동시에 가장 어려운 점 중 하나이기도 합니다. 모든 문제 역시 직접 떠안게 된다는 것입니다. 패킷이 고객의 게스트를 떠나 가상 NIC를 지나고, 우리의 소프트웨어 스위치를 통과하고, OPTE data path에 닿고, 물리 패브릭을 타고, 다른 게스트에 도착할 때까지 여러 개의 서로 다른 구성 요소를 지나갑니다.
그리고 우리처럼 그 모든 것을 직접 만든다면, 그 구성 요소 하나하나가 시스템을 더 빠르게 만들 수 있는 지점이 됩니다. 서로 다른 벤더의 지원 팀과 다섯 군데 통화하는 것보다는 훨씬 낫습니다.
하지만 그 구성 요소들을 빠르게 만들 수 있다고 해서, 어떤 단일 계층 하나가 성능 이야기의 “전부”가 되는 것은 아닙니다. 각 계층은 서로 다른 종류의 워크로드를 돕고, 특정한 형태의 병목으로 나타나며, 그것이 해소되면 또 다른 대기 계층이 드러납니다.
그러니 이러한 계층들 가운데 몇 가지를 이야기해 보겠습니다. 각 계층이 무엇을 하는지, 그리고 이 모든 것을 우리가 직접 소유하고 있다는 사실이 어떻게 고성능 온프레미스 인프라를 계속 구축할 수 있게 해주는지 살펴보겠습니다.
지난 몇 번의 릴리스 동안 우리는 네트워크에 특히 집중해 왔습니다. 구체적으로는 종단 간 IPv6 배선과 점보 프레임 도입입니다. 이것들을 하나의 독립적인 랙에 도입하는 일에는 많은 결정이 필요했고, 예상되는 결과를 철저히 조사하는 과정도 뒤따랐습니다. 우리는 먼저 영향을 줄 수 있는 계층과 조절 지점을 살펴보았습니다. 와이어에서 위로 올라가면 다음과 같습니다.
물리 패브릭 랙 사이의 링크와 그것을 전달하는 스위치입니다.
프레임 크기 하나의 패킷에 실리는 바이트 수입니다. (Maximum Transmission Unit, 즉 MTU)
오프로드 세그먼테이션과 체크섬이 게스트에서 수행되는지, 아니면 그것을 더 효율적으로 처리할 수 있는 하부 인프라에 맡겨지는지입니다.
CPU 배치 가상 NIC의 워커 스레드가 게스트 자신의 vCPU에 비해 어디에서 실행되는지입니다.
플로우 병렬성 작업이 몇 개의 독립적인 연결에 분산되는지입니다.
프로토콜 IPv6는 랙의 기본 언더레이다. 문제는 게스트가 그 위에서 IPv6를 말하는지 IPv4를 말하는지뿐입니다.
이 각각의 조절 지점은 하나 이상의 워크로드 유형에 도움이 됩니다. 이 글의 나머지 부분에서는 패킷이 마주치게 되는 순서와 대체로 비슷한 순서로 이를 따라가며, 각각이 어떤 점에 유용한지 설명하겠습니다.
가장 눈에 잘 띄는 조절 지점이자, 이를 바꾸는 데 꽤 많은 작업이 필요했던 것부터 시작해 보겠습니다. 바로 패킷의 크기입니다.
크기가 중요한 이유는 네트워크가 패킷 하나당 대체로 고정된 양의 작업을 수행하기 때문이며, 이는 패킷이 몇 바이트를 싣고 있는지와는 거의 무관합니다. 모든 패킷은 여러 계층에서 헤더가 파싱됩니다. 모든 패킷은 방화벽 규칙, 라우트 조회, 주소 해석, 연결 추적 상태라는 관문을 통과하며, 그중 많은 부분이 데이터 플레인에서 소프트웨어로 처리됩니다. 그래서 1메가바이트를 옮기고 싶을 때 패킷 하나가 1500바이트를 담는다면, 그 패킷당 세금을 약 700번 치르게 됩니다. 패킷을 9000바이트로 키우면 약 120번만 치르면 됩니다. 데이터는 같고, 오버헤드는 6분의 1입니다.
랙 내부에서는 우리는 언제나 이것을 활용해 왔습니다. 게스트는 TCP segmentation offload (TSO)를 광고하므로, 게스트는 virtio를 통해 한 번에 최대 64 KB 버퍼를 우리에게 넘깁니다. 우리는 그것을 9000바이트 언더레이 위로 운반하고, 캡슐화를 위한 공간을 남기도록 세그먼트 크기를 설정한 뒤, 썰매의 물리 NIC가 나가는 길에 큰 버퍼를 다시 와이어 크기의 패킷으로 잘라내도록 합니다. 이것이 tunneled TSO이며, 그래서 VPC 내부 트래픽은 사실상 오랫동안 점보 프레임을 타고 다녔고, 내부 처리량이 언제나 높았던 이유이기도 합니다.
보수적이었던 부분은 랙의 가장자리였습니다. 랙을 벗어나는 트래픽은 우리가 통제하지 않는 네트워크로 나가며, 임의의 외부 경로에 대해 안전한 가정은 인터넷 표준인 1500바이트 MTU입니다. 그래서 외부 트래픽에 대해서는 게스트에 그것을 광고했습니다.
주요 퍼블릭 클라우드들도 같은 선택을 합니다. 하지만 왜 그런지 정확히 짚어둘 필요가 있습니다. 그들에게 “외부”라는 말이 뜻하는 바는 우리에게 그것이 뜻하는 바와 매우 다르기 때문입니다. 하이퍼스케일러에게 내부 네트워크는 전체 가용 영역입니다. 건물 하나, 혹은 여러 개의 건물이 그들이 소유한 패브릭으로 엮여 있습니다. 워크로드가 대화하는 거의 모든 대상은 그 경계 안에 존재하며, 진정으로 “외부”인 것은 공용 인터넷뿐입니다. 그 경계에서 1500바이트 MTU로 되돌아가는 비용은 그들에게 거의 들지 않습니다. 그 경계는 멀리 떨어져 있고, 빠른 경로 위에 있는 경우가 드물기 때문입니다.
하나의 Oxide 랙에서는 그 경계가 훨씬 더 가까이 있습니다. “외부”는 한 랙 너머의 모든 것이며, 그곳에 있는 것 중 상당수는 공용 인터넷이 전혀 아닙니다. 같은 운영자가 소유하고 프로비저닝한 패브릭을 통해 도달하는 옆 줄의 랙일 뿐입니다. 그 운영자는 종종 업스트림을 실제로 통제하며, 그것이 무엇을 운반할 수 있는지도 정확히 알고 있습니다. 그 링크에 하이퍼스케일러의 보수적인 기본값을 적용하는 것은, 알려져 있고 두터우며 운영자가 소유한 경로를 열린 인터넷처럼 취급하는 셈이고, 상당한 성능을 그냥 버려두는 일입니다.
외부 점보 프레임은 그러한 운영자가 그 보수적인 기본값을 벗어날 수 있게 해줍니다. 최종 사용자가 보게 되는 부분은 인스턴스의 단일 불리언 값입니다. 흥미로운 부분은 그 불리언 값 아래에서 조정되어야 하는 모든 것들입니다. 프레임 크기는 스택의 모든 계층이 그것에 동의할 때만 현실이 되기 때문입니다.
게스트는 virtio (VIRTIO_NET_F_MTU)를 통해 자신의 MTU를 알게 되며, Propolis는 viona가 붙는 데이터 링크로부터 그 값을 도출합니다. 따라서 제어 플레인은 애초에 더 큰 MTU로 OPTE/xde 데이터 링크를 만들어야 합니다.
xde 드라이버는 이전에는 1500을 하드코딩하고 있었습니다. 이것은 create-xde ioctl, opteadm, 그리고 커널 모듈까지 전달되는 매개변수로 바뀌었습니다.
게스트는 또한 라우터 광고를 통해 자신의 IPv6 MTU를 알게 되는데, 이것은 OPTE가 생성하며 그 생성기 역시 1500을 하드코딩하고 있었습니다. 따라서 광고 자체가 더 큰 숫자를 알게 되기 전까지는, 게스트가 8500으로 구성되어 있어도 IPv6에서는 조용히 1500을 계속 사용할 수 있었습니다.
그리고 Nexus에는 두 개의 플래그가 추가되었습니다. 플릿 수준(운영자)과 인스턴스 수준(사용자)이며, 둘 다 필요합니다. 따라서 주변 네트워크가 이를 감당할 수 있다고 운영자가 확인한 뒤에야 그 기능이 최종 사용자에게 열립니다.
표면 위에는 하나의 불리언 값이 있지만, 그 아래에서는 수십 가지의 조정이 함께 돌아가야 합니다. 이 기능을 고객에게 제공할 수 있었던 것은 매우 좋았습니다. 동시에 거친 가장자리도 우리가 직접 발견했다는 뜻이기도 했습니다. 예를 들어, 점보 경로 프로토타입을 테스트하는 과정에서 지나치게 큰 프레임이 도착했을 때 가상 NIC의 인터럽트 처리 문제를 드러냈고, 드라이버가 우리 소유였기 때문에 우리는 기능 출시 전에 그 문제를 추적해 고칠 수 있었습니다. 벤더에게 티켓을 열고 기다릴 필요가 없었습니다.
언더레이는 9000바이트이지만, 우리는 여유 공간을 두기 위해 게스트에게 8500을 제공합니다. 모든 게스트 프레임은 언더레이를 위해 한 번 더 감싸집니다. Ethernet, IPv6, UDP, Geneve 헤더, 그리고 세그먼트 크기 같은 것을 실어 나르는 데 쓰는 Geneve 옵션이 붙습니다. 이 오버헤드의 엄격한 하한은 대략 70~78바이트 수준입니다. 우리는 500을 예약해 둡니다. 이 추가 여유는 의도적인 것입니다. 앞으로 더 많은 Geneve 기능이 들어올 예정이고, 라우팅 프로토콜도 결국 데이터 플레인에 extension-header 오버헤드를 더하게 될 것이기 때문입니다. 또한 지켜야 할 하드웨어 한계도 있습니다. 스위치는 패킷 헤더를 고정된 깊이까지만 파싱하는데, 보통 처음 256바이트나 512바이트까지만 볼 수 있으며, 그 창 너머에 있는 헤더는 볼 수 없습니다. 캡슐화 스택을 그 안쪽에 충분히 유지하면 스위치는 전달 결정을 내리는 데 필요한 필드를 계속 읽을 수 있습니다. 반대로 스택이 너무 깊어지면 그 필드들은 시야에서 사라집니다. 8500은 나중에 늘리기는 쉽고, 고객이 그 숫자에 대한 기대를 형성한 뒤에는 줄이기가 매우 어려운 값입니다.
전통적인 스위치는 모든 포트에 설정 가능한 MTU를 둡니다. 우리는 의도적으로 그렇게 하지 않으며, 그 이유는 박스가 아니라 클라우드를 위한 설계가 무엇인지 잘 보여주는 깔끔한 예입니다. 우리 네트워킹 엔지니어인 Ryan Goodfellow의 표현을 빌리면, “MTU는 포트의 속성이 아니라 경로의 속성”입니다. 하나의 switch port는 동시에 여러 업스트림 경로의 관문이 될 수 있습니다. 점보 프레임을 운반하는 경로 하나, 1420바이트로 제한된 경로 하나, 그리고 1500바이트 공용 인터넷이 그것입니다. 이 셋 모두에 올바른 단일 포트 MTU란 존재하지 않습니다. 포트를 가장 작은 값에 맞춰 고정하는 것은 더 큰 경로들에 불이익만 줄 뿐 작은 경로에는 도움이 되지 않습니다. 작은 경로로 가는 너무 큰 패킷은 어차피 버려지고 신호가 돌아오기 때문입니다. 많은 경우 사용자는 운영체제의 path MTU discovery (PMTUD)가 통신 경로의 실질적인 MTU를 동적으로 결정해 주기를 원할 것입니다.
그래서 우리는 포트 다이얼 대신, 결정이 있어야 할 자리에 그것을 두었습니다. 운영자가 제어하는 플릿 수준 스위치로 보호되는 인스턴스 위입니다. 이것은 전통적인 네트워크가 포트 MTU와 MSS 클램핑으로 얻는 보호를, 클라우드에 맞는 방식으로 표현한 것입니다.
| 경로 | MTU 1500 | MTU 8500 | 변화 |
|---|---|---|---|
| 내부 VPC, A에서 B | 52.44 | 55.73 | +6% |
| 내부 VPC, B에서 A | 51.70 | 55.57 | +7% |
| 외부, A에서 B | 7.70 | 32.67 | +324% |
| 외부, B에서 A | 7.96 | 34.19 | +329% |
(40G 외부 업링크에서, 쌍당 Gbps.)
내부 VPC 트래픽은 거의 변하지 않습니다. 이미 큰 프레임을 타고 있었기 때문에 남아 있는 비용은 패킷당이 아니라 바이트당 비용이기 때문입니다. 1500에서 패킷당 세금을 내고 있었던 외부 트래픽은 대략 4배 향상되며, 내부와 외부 처리량의 격차는 약 7배에서 약 1.7배로 줄어듭니다.
더 큰 프레임은 그것이 펼쳐질 더 많은 와이어도 원합니다. 우리 실험실 패브릭에서는 외부 트래픽을 단일 40G 업링크에서 여러 개의 100G 링크 세트로 옮겼고, 트래픽은 equal-cost multipath (ECMP) 라우팅으로 그 링크들에 분산되었습니다. 그 패브릭에서 단일 쌍은 패킷당 세금 감소와 함께 60 Gbps에 도달했습니다.
한 가지 덧붙일 점이 있습니다. 이것 역시 Ryan의 표현을 빌리면, 점보 프레임은 네트워크를 무조건 빠르게 해주는 버튼이 아닙니다. 실제로 1500바이트보다 큰 메시지를 많이 보내는 플로우에만 도움이 됩니다. iperf3 같은 벤치마크가 과장된 이점을 보여주는 이유는, 가능한 한 가장 큰 프레임을 가능한 한 빠르게 보내도록 만들어졌기 때문이며, 이는 최상의 경우에 가깝습니다. 평균 메시지 크기가 작은 실제 워크로드는 이점이 작을 수도 있고, 전혀 없을 수도 있습니다. 위의 4배라는 숫자를 읽는 올바른 방법은 “천장이 크게 올라갔다”이지, “모든 워크로드가 4배 빨라진다”가 아닙니다.
자세히 설명할 만한 운영상의 날카로운 모서리가 하나 있습니다. 이것은 점보가 도움이 되는지 해가 되는지를 가르는 차이입니다.
점보 프레임은 두 종단점 사이의 모든 홉이 그것을 운반할 수 있을 때만 동작합니다. 패킷이 경로 어딘가의 링크에 비해 너무 클 경우, 그 링크는 그 패킷을 버리고 ICMP “fragmentation needed” (IPv6에서는 “packet too big”) 메시지를 되돌려 보내야 하며, 송신자의 path-MTU discovery는 이에 반응해 프레임 크기를 낮춥니다. 문제는 이런 신호가 놀랄 만큼 자주 드롭된다는 점입니다. 방화벽 때문일 수도 있고, ICMP를 필터링하는 라우터 정책 때문일 수도 있고, 과부하된 홉 때문일 수도 있습니다. 그렇게 되면 송신자는 줄여야 한다는 사실을 영영 배우지 못합니다. 그러면 고전적인 블랙홀이 생깁니다. TCP 핸드셰이크는 완료되지만, 그다음 애플리케이션은 멈춰 서고 아무것도 전송하지 못합니다. 이는 IPv6에서 더 심각한데, 관련 ICMPv6가 더 불안정하게 처리되고, 라우터는 너무 큰 패킷을 무조건 드롭하기 때문입니다.
따라서 규칙은 이렇습니다. 점보를 활성화하기 전에 전체 경로를 점검하십시오. 모든 홉이 더 큰 프레임을 운반하는지, 그리고 또는 fragmentation needed 신호를 되돌려 보내는지 확인하십시오. 구성만 믿지 말고 실제 연결에서 협상된 MTU를 검증하십시오. 그리고 게스트에서 packetization-layer path-MTU discovery를 활성화하는 것도 고려하십시오. (Linux에서는 net.ipv4.tcp_mtu_probing) 이것은 커널이 ICMP에 전혀 의존하지 않고도 올바른 프레임 크기를 찾을 수 있게 해주는 두 번째 인밴드 방식입니다. 점보는 외부 트래픽에 대해 큰 지렛대이며, 그 아래 경로가 일관되기만 하다면 안전한 지렛대이기도 합니다.
프레임 크기가 작업량 자체에 관한 것이라면, 이 계층은 그 작업이 어디에서 일어나는지에 관한 것입니다. 여기에는 두 개의 조절 지점이 있으며, 둘 다 고객이 직접 만지는 것은 아닙니다.
첫 번째는 위에서 잠시 언급한 오프로드입니다. 오프로드는 패킷당 무거운 작업을 어느 계층이 맡을지 결정합니다. segmentation offload는 게스트가 큰 버퍼를 넘기고, 하위 계층이 그것을 한 번에 와이어 크기의 패킷으로 잘라내도록 합니다. 게스트가 패킷 하나씩 처리하는 것이 아닙니다. 이것을 IPv4와 IPv6 모두에서 올바르게 만드는 일은 어떤 프로토콜을 완전한 동급으로 끌어올린다는 것이 무엇을 의미하는지의 일부입니다.
두 번째는 CPU 배치입니다. 실제로 네트워크 스택의 작업을 어느 코어들이 실행하는가 하는 문제입니다. 많은 vCPU를 가진 게스트도, 패킷 처리를 코어들에 분산시키는 스택의 능력만큼만 빨라질 수 있습니다. 우리는 가상 NIC의 워커 스레드가 게스트의 vCPU들에 비해 어떻게 배치되는지를 다듬었고, 그 결과 이제 처리량은 게스트 크기가 커질수록 깔끔하게 확장됩니다. 우리가 테스트하는 64코어, 128스레드 부품에서는 96-vCPU 게스트가 물리 코어 너비를 넘어 많은 코어의 두 번째 하드웨어 스레드까지 사용합니다. 워커 스레드가 정확히 그 상황을 처리하도록 배치되면, 96-vCPU 게스트는 64-vCPU 게스트만큼, 혹은 그 이상으로, 외부 처리량을 유지합니다.
단일 연결에는 한계가 있습니다. 100G 패브릭에서 수행한 많은 테스트에서는 그 한계가 VM 쌍당 단방향 약 60 Gbps 근처에서 나타났고, 하나의 연결에 32개, 64개, 128개 스트림을 밀어 넣든, 여러 개의 iperf3 프로세스를 나란히 실행하든 그 값은 안정적으로 유지되었습니다.
그 한계를 넘는 방법은 더 큰 단일 플로우가 아닙니다. 더 많은 플로우입니다. 서로 다른 5-튜플을 가진 여러 연결에 작업을 분산시키고, 그것들이 서로 다른 썰매에 도착하게 하면 ECMP가 자동으로 그것들을 패브릭에 펼쳐 놓습니다. 그러면 총 처리량은 어떤 하나의 천장에 부딪히는 대신 독립적인 플로우 수에 따라 확장됩니다. 이것은 실제 프로덕션 시스템의 동작 방식과도 잘 맞습니다. 거대한 단일 연결 하나가 아니라, 많은 연결입니다.
이 계층에 대한 실질적인 지침은 다음과 같습니다.
단일 연결이 아니라 여러 개의 병렬 플로우를 기준으로 설계하십시오.
처리량이 중요한 쌍은 공유되지 않는 썰매에 배치하십시오. 100G 링크에 연결된 단일 썰매는 몇 개의 VM이 그것을 만들어내는지와 무관하게, 입출력을 합친 총 트래픽이 대략 90 Gbps 근처에서 상한에 도달합니다. 따라서 어떤 중요한 플로우의 수신 측을 또 다른 중요한 플로우의 송신 측과 같은 썰매에 두지 마십시오. 이 작업에는 Oxide의 anti-affinity rules를 사용할 수 있습니다.
IPv6는 호환성을 위해 나중에 덧붙인 프로토콜이 아닙니다. 랙의 언더레이는 IPv6입니다. 모든 썰매 간 홉, 모든 Geneve 캡슐화 게스트 프레임은 기본적으로 IPv6 위를 탑니다. 이것이 랙의 모국어입니다. 따라서 이번 몇 번의 릴리스에서 한 일은 랙에게 IPv6를 말하게 가르치는 것이 아니었습니다. 그 일급 지위를 게스트 내부까지, 종단 간으로 끝까지 확장하고, 그것을 사용하는 데 드는 세금이 없어질 때까지 조정하는 일이었습니다.
40G 업링크에서 같은 MTU, 같은 경계 경로로 수행한 우리의 테스트에서는 이제 IPv4와 IPv6가 동일한 처리량을 제공합니다. 점보 프레임에서 쌍당 IPv4는 약 34.5 Gbps, IPv6는 약 34.9 Gbps입니다. 본질적인 IPv6 페널티는 없습니다. 처음부터 패브릭이 IPv6였던 랙에서는 그래서는 안 됩니다. IPv6 우선 환경을 설계하고 있다면, 그리고 실제로 우리 고객 중 다수가 그렇다면, 이 차이는 중요합니다.
IPv6에 대한 한 가지 주의점이 있습니다. path-MTU discovery는 IPv4보다 IPv6에서 덜 관대하므로, 프레임 섹션에서 말한 점보 경로 점검 규칙은 IPv6를 운용할 때 조금 더 중요합니다.
한 걸음 물러서서 보면, 핵심 줄거리는 어떤 단일 숫자가 아닙니다. 바닥부터 구축된 플랫폼은 모든 계층마다 하나씩 조절 지점을 갖고 있고, 모든 계층을 소유한 팀은 그것들을 계속 돌릴 수 있다는 점입니다. 패브릭, 프레임 크기, 오프로드, CPU 배치, 플로우 분산, 프로토콜 처리. 이것들은 따로따로 붙여 놓은 별개의 제품이 아닙니다. 하나의 시스템이며, 다른 계층들에 대한 완전한 지식을 바탕으로 어떤 하나를 조정하는 일은 스택 전체의 소유자만이 할 수 있는 일입니다.
외부 점보 프레임은 가장 분명한 예입니다. 사용자가 보는 것은 하나의 불리언 값입니다. 하지만 그것을 가능하게 하려면 가상 NIC, OPTE 데이터 경로, 커널 드라이버, 라우팅 프로토콜, 제어 플레인 전반에 걸친 조정된 변경이 필요했습니다. 여기에 여유 공간에 대한 논리를 담아 의도적으로 선택한 프레임 크기, 그리고 그 조절 지점을 시스템 어디에 두어야 하는지에 대한 결정까지 포함됩니다.
그래서 이것은 일회성 발표가 아닙니다. 네트워크 스택의 최종 버전 같은 것은 없고, 조정의 끝도 없습니다. 다음 계층이 있고, 그 아래 계층이 있으며, 핵심에 이를 때까지 계속됩니다. 우리는 계속 벗겨 나갈 것입니다.