LY Corporation Tech Blog

LY Corporation과 LY Corporation Group(LINE Plus, LINE Taiwan and LINE Vietnam)의 기술과 개발 문화를 알립니다.

가상 머신의 성능을 높이는 것도 지구 온난화에 도움이 될까요?

서비스 인프라 확장 비용을 줄이기 위한 선택 - 가상 머신

서비스 인프라를 확장하는 방법은 두 가지가 있습니다. 하나는 서버를 더 도입하는 스케일아웃이고, 또 하나는 서버에 하드웨어 자원을 추가하는 스케일업입니다. 요즘은 쿠버네티스의 유행과 관리의 편의성 때문에 스케일아웃에 더 무게를 두는 추세인데요. 스케일 아웃은 비용이 많이 든다는 문제가 있기 때문에 확장하려는 인프라의 규모가 크지 않다면 스케일업을 먼저 생각해 볼 수도 있습니다.

현재 LY는 전 세계적으로도 손에 꼽을 만큼 거대한 서비스를 운영하는 회사이며, 인프라 확장에 드는 비용도 일반적으로 쉽게 상상할 수 없는 규모입니다. 매년 사용자가 늘어나고 사용하는 데이터도 커지고 있으며, 이에 맞춰 매년 새로운 데이터 센터를 세우고 대규모로 서버를 구매하고 있는데요. 새로 서버를 도입하고 비용을 들일 때마다 과연 이 비용을 사용하는 만큼 서비스 인프라가 제대로 확장되고 있는지 의문이었고, 혹시 비용을 들이지 않고 지금 사용하고 있는 서버를 더 잘 이용하는 방법은 없는지를 고민했습니다. 

그렇다 보니 물리적으로 서버를 추가로 도입하는 것보다는 이미 있는 서버에 가상 머신을 추가로 생성해서 서비스를 운영하는 것을 우선적으로 고려하게 됩니다. 가상 머신을 사용하면 비용 절감뿐 아니라 스케일업과 스케일아웃이 모두 간편해지는 장점이 있습니다. 새로운 서버를 단순히 클릭 몇 번으로 생성하거나 제거할 수 있고, 하드웨어 스펙을 바꿀 수도 있으니까요.

그런데 가상 머신으로 서비스를 운영하다 보면 물리 서버로 운영할 때에는 마주치지 못한 문제와 마주칠 때가 있습니다. 일례로 저는 작년에 저희 팀에서 가상 머신으로 운영하고 있는 네트워크 게이트웨이 서버가 제가 예상했던 수치보다 너무 낮은 성능을 내고 있다는 것을 발견했습니다. 원인을 분석해 보니 오픈스택이 제공하는 가상 머신의 성능이 CPU 개수나 메모리 용량 등이 비슷한 물리 서버에 비해 너무 낮다는 것을 알게 됐고, 이런 이유로 많은 서비스가 가상 머신이 아닌 물리 서버에서 실행되고 있다는 것도 알게 됐습니다.

저는 이런 상황에 의문이 들었습니다. 커널 개발을 10여 년 동안 해오면서 거의 항상 가상 머신에서 개발을 하고 서비스를 출시해 왔던 제 경험에 비춰보면, 가상 머신도 거의 대부분의 서비스를 실행하기에 문제가 없었기 때문입니다. 이에 몇 가지 실험을 진행했고, 실험 결과 가상 머신이 실행되는 하이퍼바이저 서버의 성능은 충분한데 가상 머신들이 하어퍼바이저 서버의 성능을 제대로 활용하지 못하고 있다는 것을 발견했습니다.

이 문제를 해결하기 위해 1년간 연구와 실험을 진행했습니다. 그 결과 가상 머신에 몇 가지 옵션을 추가해서 기존 가상 머신보다 높은 성능을 발휘하는, 유사한 환경(CPU 수와 메모리 크기 등)에서 물리 서버 대비 70~80%의 성능을 내는 가상 머신을 생성할 수 있었습니다. 또한 이를 통해 추후 회사에서 도입했어야 할 물리 서버의 수를 줄일 수 있었습니다.

이 글에서는 제가 이 문제를 발견하고 분석한 과정과 어떤 방법으로 해결했는지 이야기하려고 합니다.

참고로 '가상 머신의 성능이 문제라면 컨테이너를 사용하면 될 텐데?'라고 생각하는 분이 계실 것입니다. 물론 컨테이너는 물리 서버에서 중간 계층 없이 실행되므로 물리 서버의 성능에 버금가게 실행됩니다. 하지만 문제는 많은 인프라 시스템이 컨테이너가 실행되는 노드를 물리 서버가 아닌 가상 머신으로 사용하고 있다는 것입니다. 관리하기 용이하기 때문이지요. 따라서 가상 머신의 성능을 개선하지 않으면 아무리 컨테이너를 사용한다고 해도 서비스 성능을 높일 수가 없습니다.

가상 머신의 성능 문제를 알게 된 계기

먼저 사내에서 사용하는 가상 머신의 성능에 뭔가 문제가 있다는 것을 알게 된 계기를 말씀드리겠습니다.

제가 속한 팀에서는 Ceph 클러스터를 운영하면서 RGW(Rados GateWay)라는 게이트웨이 서비스를 사용하고 있습니다. 보통 실 서비스를 제공하는 환경에서는 물리 서버에 RGW를 설치해서 사용하고, 테스트 환경에서는 가상 머신에 RGW를 설치해서 사용합니다. 이때 가상 머신을 운영하면서 나름 기대했던 성능 예상치가 있었는데요. 예를 들어 CPU가 40개인 물리 서버에서 RGW가 처리하는 네트워크 데이터의 성능이 100이라고 하면, CPU가 20개인 가상 머신의 성능은 50까지는 아니더라도 적어도 30 이상은 나올 것이라고 생각하고 가상 머신을 운영했습니다. 

그런데 테스트 환경의 RGW에서 장애가 너무 자주 발생했습니다. 다른 컴포넌트를 확인해 봤지만 장애가 발생할 이유가 없었고, RGW를 통해 들어오는 트래픽의 양이 너무 적었으므로 RGW의 문제라는 것을 알 수 있었습니다. 당시 CPU 20개, 메모리 약 200GB인 가상 머신에서 RGW를 실행했는데요. 각종 모니터링 툴로 확인해 보니 CPU와 메모리의 사용률은 낮은 편이었습니다.

그렇다면 왜 네트워크 트래픽을 처리하지 못했는지 파악하기 위해 좀 더 세밀하게 모니터링하던 도중 실마리를 하나 발견했습니다. Prometheus라는 툴로 가상 머신의 각 CPU 사용률을 확인해 보니 CPU 하나만 100%의 사용률을 보이고 있었던 것입니다. 그래서 저는 각 CPU가 몇 개의 인터럽트를 처리했는지 보여주는 /proc/interrupts 파일을 확인했습니다. 아래는 제가 확인한 당시의 파일 내용을 복사해 놓은 것입니다.

           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7       CPU8       CPU9       CPU10      CPU11      CPU12      CPU13      CPU14      CPU15      CPU16      CPU17      CPU18      CPU19      CPU20      CPU21      CPU22      CPU23      CPU24      CPU25      CPU26      CPU27
  0:         79          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge      timer
  1:         10          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge      i8042
  3:          4          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge
  4:        521          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0        819          0          0          0          0   IO-APIC-edge      serial
  6:          3          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge      floppy
  8:          1          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge      rtc0
  9:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-fasteoi   acpi
 10:          2          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0     279196       3484          0          0          0          0          0          0          0   IO-APIC-fasteoi   virtio2
 11:         32          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-fasteoi   uhci_hcd:usb1
 12:         15          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge      i8042
 14:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge      ata_piix
 15:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC-edge      ata_piix
 24:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   PCI-MSI-edge      virtio1-config
 25:       9177          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0  112894163    1340489          0          0          0          0          0          0          0          0   PCI-MSI-edge      virtio1-req.0
 26:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   PCI-MSI-edge      virtio0-config
 27:    9176864          0          0          0          0          0          0          0          0          0          0          0          0          0          0 3676790525  401551873          0          0          0          0          0          0          0          0          0          0          0   PCI-MSI-edge      virtio0-input.0
 28:          2          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0    1285361      18874          0          0          0          0          0          0          0          0          0          0   PCI-MSI-edge      virtio0-output.0
NMI:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   Non-maskable interrupts 

여기서 이더넷 카드가 네트워크 트래픽을 처리하면서 발생하는 인터럽트는 virtio0-input.0과 virtio0-input.1입니다. 이더넷 카드가 네트워크 트래픽을 받거나 전송을 완료하면 CPU에게 인터럽트 신호를 보내는데 virtio0-input.0과 virtio0-input.1 수치를 살펴보면 각 CPU가 이더넷 카드의 인터럽트 신호를 몇 개 받았는지 알 수 있습니다.

위 표에서 virtio0-input.0 인터럽트의 CPU별 개수를 보면 CPU15가 3,676,790,525개, CPU16가 401,551,873개의 인터럽트를 처리했습니다. 그 외에는 CPU0이 9,176,864개의 인터럽트를 처리했을 뿐, 나머지 CPU는 모두 0입니다. 결국 CPU15와 16 두 개의 CPU가 거의 모든 처리를 다 하고 있는 것을 알 수 있습니다. 네트워크 처리를 하는 애플리케이션을 실행하기 위해 여러 개의 CPU와 많은 메모리를 탑재한 가상 머신을 만들어서 사용하고 있는데, 겨우 두 개의 CPU만 바쁘게 일하고 나머지는 놀고 있었던 것입니다. 그러니 아무리 많은 CPU와 메모리로 가상 머신을 만들어도 항상 기대했던 성능의 10~20%만 나올 수밖에 없었습니다.

이와 같은 상태를 확인한 저는 본격적으로 저희가 사용하는 가상 머신의 전체적인 성능을 측정하기 시작했습니다.

가상 머신의 성능 측정 후 비교

저는 앞서 살펴본 바와 같이, 네트워크 게이트웨이를 아무리 많이 스케일아웃해도 기대한 만큼의 성능이 나오지 않는 이유는 각 가상 머신이 할당된 자원을 제대로 활용하지 못하기 때문이라고 추측했습니다. 이에 가상 머신의 성능을 아래 네 가지 카테고리로 나눠 측정해 봤습니다.

  • CPU
  • 메모리
  • 네트워크
  • 디스크

성능 측정은 sysbench와 iperf, fio 등의 벤치마킹 툴을 사용했습니다. 각 툴을 이용해 하이퍼바이저의 성능을 측정하고 하이퍼바이저에 가상 머신 한 개를 실행했을 때의 성능을 측정해서 비교해 가상 머신을 사용할 때 어느 정도의 성능 저하가 발생하는지 알아봤습니다. 실험에 사용한 가상 머신은 OpenStack 기반의 환경에서 Qemu를 이용해서 생성했습니다. 

CPU 성능 비교

컴퓨터 성능의 기본은 CPU 성능일 것입니다. 다음은 하이퍼바이저에서 CPU 성능을 측정한 결과입니다.

# sysbench cpu --threads=12 run
sysbench 1.0.17 (using system LuaJIT 2.0.4)

Running the test with following options:
Number of threads: 12
Initializing random number generator from current time


Prime numbers limit: 10000

Initializing worker threads...

Threads started!

CPU speed:
    events per second: 29062.92

General statistics:
    total time:                          10.0004s
    total number of events:              290695

Latency (ms):
         min:                                    0.41
         avg:                                    0.41
         max:                                    0.66
         95th percentile:                        0.42
         sum:                               119937.14

Threads fairness:
    events (avg/stddev):           24224.5833/15.06
    execution time (avg/stddev):   9.9948/0.00

이번에는 가상 머신에서 실행한 CPU 성능 실험 결과입니다.

# sysbench cpu --threads=12 run
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
 
Running the test with following options:
Number of threads: 12
Initializing random number generator from current time
 
 
Prime numbers limit: 10000
 
Initializing worker threads...
 
Threads started!
 
CPU speed:
    events per second: 28395.51
 
General statistics:
    total time:                          10.0004s
    total number of events:              284026
 
Latency (ms):
         min:                                    0.42
         avg:                                    0.42
         max:                                   13.38
         95th percentile:                        0.42
         sum:                               119933.05
 
Threads fairness:
    events (avg/stddev):           23668.8333/80.23
    execution time (avg/stddev):   9.9944/0.00

events per second의 값을 비교해 보면 가상 머신의 성능이 2% 정도 낮은데요. 이 정도의 성능 차이는 유의미하다고 판단하기 어려울 것 같았습니다. 소프트웨어 실행에 큰 문제가 되지 않을 것 같아 무시해도 좋을 것이라고 판단했습니다.

메모리 성능 비교

다음은 하이퍼바이저에서 실행한 메모리 성능 실험 결과입니다. 초당 6184MiB의 메모리를 기록할 수 있다고 나왔습니다.

# sysbench memory --threads=12 run
sysbench 1.0.17 (using system LuaJIT 2.0.4)

Running the test with following options:
Number of threads: 12
Initializing random number generator from current time


Running memory speed test with the following options:
  block size: 1KiB
  total size: 102400MiB
  operation: write
  scope: global

Initializing worker threads...

Threads started!

Total operations: 63331630 (6331933.79 per second)

61847.29 MiB transferred (6183.53 MiB/sec)


General statistics:
    total time:                          10.0001s
    total number of events:              63331630

Latency (ms):
         min:                                    0.00
         avg:                                    0.00
         max:                                    0.26
         95th percentile:                        0.00
         sum:                               106914.24

Threads fairness:
    events (avg/stddev):           5277635.8333/254591.12
    execution time (avg/stddev):   8.9095/0.06

이번에는 같은 테스트를 가상 머신에서 실행했습니다. 초당 5249MiB 메모리를 기록했다는 결과가 나왔습니다.

# sysbench memory --threads=12 run
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
 
Running the test with following options:
Number of threads: 12
Initializing random number generator from current time
 
 
Running memory speed test with the following options:
  block size: 1KiB
  total size: 102400MiB
  operation: write
  scope: global
 
Initializing worker threads...
 
Threads started!
 
Total operations: 53767542 (5375465.94 per second)
 
52507.37 MiB transferred (5249.48 MiB/sec)
 
 
General statistics:
    total time:                          10.0002s
    total number of events:              53767542
 
Latency (ms):
         min:                                    0.00
         avg:                                    0.00
         max:                                   13.02
         95th percentile:                        0.00
         sum:                               107665.70
 
Threads fairness:
    events (avg/stddev):           4480628.5000/378369.07
    execution time (avg/stddev):   8.9721/0.08

가상 머신이 메모리 쓰기에서 하이퍼바이저 대비 84%의 성능을 보인다는 결과를 얻었습니다. 그리 나쁘지 않다고 생각할 수도 있겠지만, 저에게는 놀라운 결과였습니다. 가상 머신이 실행되는 것이나 하이퍼바이저에서 애플리케이션이 실행되는 것이나 사실상 가상 메모리에 접근해 물리 메모리에 접근하는 과정에서는 크게 다를 게 없기 때문입니다. Linux 커널에서 가상 메모리 주소를 물리 메모리 주소로 변환하는 페이지 테이블 접근 과정이 두 번 반복되긴 하겠지만, 최근의 CPU는 캐시 용량이 워낙 크기 때문에 그런 주소 변환에 들어가는 시간이 10%가 넘는다는 것은 이상했습니다. 어쩌면 개선의 여지가 있을 것 같았습니다.

네트워크 성능 비교

다음으로 네트워크 성능을 비교했습니다. 네트워크의 최대 성능이 얼마나 나올지 확인하기 위해 iperf를 이용해서 서로 다른 하이퍼바이저에 있는 가상 머신 간 네트워크 대역폭이 얼마나 나오는지 확인했습니다.

# iperf -c <다른 하이퍼바이저에 있는 가상 머신> -P 12

하이퍼바이저 간은 25Gbits/sec 이더넷으로 연결돼 있었고, 각 하이퍼바이저에 다른 가상 머신은 없는 상황이었는데요. 2.0~2.2Gbits/sec 정도만 나오고 있었습니다. 확실히 네트워크 성능이 기대한 것에 비해 너무 낮게 나오고 있는 상황이었습니다.

디스크 성능 비교

하이퍼바이저에서 벤치마크 테스트를 한 결과와 가상 머신에서 벤치마크 테스트를 한 결과를 비교해서 가상 머신을 사용하면서 디스크 성능이 얼마나 저하되는지 확인했습니다. 디스크 성능은 sysbench보다 조금 더 많이 사용되는 fio를 이용했으며, 다음과 같이 세 가지 유형의 I/O 성능을 측정했습니다.

  • 읽기 75%, 쓰기 25%
  • 읽기 100%
  • 쓰기 100%

테스트는 4KB 단위로 디스크에 읽기/쓰기를 발생시켰을 때 초당 몇 번의 입출력이 실행되는지 IOPS(I/O operations per second)를 측정하는 방식으로 진행했습니다.

fio --name=test --directory=. --size=20g --direct=1 --bs=4KB --ioengine=libaio --iodepth=32 --thread --numjobs=4 --runtime=300 --rw=randrw  --rwmixread=75 --group_reporting  --time_based=1
fio --name=test --directory=. --size=20g --direct=1 --bs=4KB --ioengine=libaio --iodepth=32 --thread --numjobs=4 --runtime=300 --rw=randrw  --rwmixread=100 --group_reporting  --time_based=1
fio --name=test --directory=. --size=20g --direct=1 --bs=4KB --ioengine=libaio --iodepth=32 --thread --numjobs=4 --runtime=300 --rw=randrw  --rwmixread=0 --group_reporting  --time_based=1

아래는 테스트 결과입니다. 숫자가 높을수록 좋습니다.

가상 머신하이퍼바이저(RAID-10 SSD)
읽기 75%, 쓰기 25%읽기 17.5k, 쓰기 5k읽기 18k, 쓰기 6k
읽기 100%231k284k
쓰기 100%8.6k8k

가상 머신의 성능은 읽기만 실행한 경우에만 성능이 조금 낮을 뿐 그 외의 결과는 모두 비슷한 수준으로 나왔습니다. 이에 따라 가상 머신의 최대 디스크 성능은 하이퍼바이저와 근접한 것으로 판단했습니다.

다만, 이 실험에는 다음과 같은 한계가 있습니다.

  1. 하이퍼바이저 디스크의 절대적인 성능이 좋지 못했기에 가상 머신의 최대 성능을 측정하기에 적합하지 않았습니다. 만약 하이퍼바이저에 최신 NVMe 디스크가 있었다면 성능 차이가 크게 벌어졌을 수도 있습니다.
  2. 이 결과는 하이퍼바이저에 하나의 가상 머신만 실행한 상황에서 측정한 결과입니다. 하지만 실제 서비스를 운영할 때에는 하나의 하이퍼바이저에 여러 개의 가상 머신을 실행해야 물리 서버의 개수를 줄이고 자원을 절약할 수 있습니다.

여러 개의 가상 머신을 실행해서 디스크 성능을 측정하니, 가상 머신 개수에 비례해서 디스크 성능이 떨어지는 것을 확인했습니다. 디스크의 물리적인 대역폭은 한계가 있고, 하나의 디스크를 여러 가상 머신이 나눠서 사용하기 때문에 물리적으로 어쩔 수 없는 상황입니다. 이런 디스크 대역폭의 한계를 극복할 수 있는 방안을 찾아야 합니다.

성능 비교 결과 고찰

하이버바이저와 가상 머신의 성능을 비교한 결과를 요약하자면 이렇습니다.

  • 가상 머신의 CPU와 메모리 성능은 하이퍼바이저의 성능과 유사하다.
  • 가상 머신의 네트워크와 디스크 성능은 하이퍼바이저에 비해 많이 낮다.

제가 얻은 결론은 이렇습니다.

  • 가상 머신의 오버헤드는 크지 않지만, 하이퍼바이저의 하드웨어를 여러 개의 가상 머신이 공유해서 사용할 때에는 성능 저하를 피할 수 없다.
  • 네트워크 성능은 가상 머신의 오버헤드 문제가 아니라, 커널/드라이버의 설정 문제로 보인다.

따라서 종합적으로 고려해 볼 때, 기존에 물리 서버에서 실행하던 네트워크 게이트웨이 서비스를 가상 머신으로 옮기는 것은 어려운 수준이었습니다. 성능을 개선할 필요가 있었습니다.

가상 머신의 성능 개선

LINE 서비스를 제공하기 위해 사용하는 가상 머신은 Qemu라는 소프트웨어를 이용해서 생성합니다. Qemu는 오픈소스 소프트웨어로, 소스 코드가 공개돼 있고 아주 다양한 옵션을 제공하고 있습니다. 저는 문제를 해결하기 위해 Qemu의 매뉴얼을 정독했습니다. 또한 Qemu뿐 아니라 Red Hat 계열 운영 체제에서 가상 머신을 생성하고 관리하는 virt나 libvirtd 등의 매뉴얼과 그 밖에 Red Hat에서 제공하는 다양한 자료를 조사했습니다(참고).

한 가지 꼭 기억해야 할 점은, 하이퍼바이저에 가상 머신 하나를 실행했을 때의 성능은 큰 의미가 없다는 것입니다. 가상 머신 하나를 실행했을 때 성능이 좋다면 관리에 편리할 수는 있어도 비용은 물리 머신을 추가한 것과 차이가 나지 않기 때문입니다. 따라서 가상 머신 두 개 이상을 동시에 실행할 때의 성능이 서비스 운용에 허용 가능한 수준인지가 성능 개선의 기준이 되어야 합니다. 위 실험에서 가상 머신 자체의 오버헤드는 크지 않지만, 물리 하드웨어를 가상 머신들이 공유해서 사용할 때의 성능 저하는 피할 수 없다는 것을 확인했습니다. 이를 해결하기 위해 다음 두 가지 해결 방안을 적용했습니다. 

  1. 하나의 하이퍼바이저에 실행하는 가상 머신의 개수를 제한하는 정책 도입
  2. 새로운 미디어 도입

첫 번째 해결 방안은 CPU 성능을 개선하면서, 두 번째 해결 방안은 디스크 성능을 개선하면서 적용했는데요. 그 외 메모리와 네트워크 성능을 개선한 방법까지 하나씩 차례로 살펴보겠습니다. 

CPU 성능 개선

앞서 살펴본 것처럼, 가상 머신의 CPU 성능은 크게 나쁘지 않아 보입니다. 하지만 여러 개의 가상 머신을 실행하면 어떻게 될까요? 실험해 본 결과 당연히 가상 머신의 개수에 비례해서 성능이 낮아졌습니다.

물론 물리적으로 정해진 CPU를 나눠 사용하는 것이기에 하나의 CPU 코어를 여러 개의 가상 CPU가 나눠 사용할 때 성능이 낮아지는 것은 어쩔 수 없습니다. 이에 저희는 하나의 하이퍼바이저에 소수의 가상 머신만 실행해서 물리 CPU를 여러 가상 머신이 공유해서 사용하지 못하도록 정책을 세웠습니다. 구체적으로 말씀드리면, 먼저 가상 머신을 고성능용과 범용으로 나눈 뒤 고성능 가상 머신의 경우 CPU 개수를 20개로 고정했습니다. 그리고 물리 CPU가 40개인 하이퍼바이저에서는 가상 CPU가 20개인 가상 머신이 두 개까지만 실행되도록 정책을 정했습니다. 그렇게 되면 가상 머신이 물리 CPU를 독점할 수 있어서, 물리 CPU의 성능이 곧 가상 머신의 CPU 성능이 됩니다. 반면 범용 가상 머신은 CPU 개수를 다양하게 할당할 수 있고, 물리 CPU가 40개인 하이퍼바이저에서 여러 개를 실행할 수 있습니다.

물론 이렇게 정책을 설정하면 하나의 하이퍼바이저에 많은 수의 가상 머신을 실행해 비용을 극적으로 절감하는 효과는 얻을 수 없습니다. 하지만 현재 물리 서버를 사용하는 대부분의 서비스가 고성능을 필요로 하거나 혹은 일정하게 유지되는 성능이 필요한 서비스이기 때문에 이런 서비스도 가상 머신을 사용하도록 유도할 수 있습니다.

이와 같이 CPU 성능 개선은 새로운 정책 수립으로 해결했습니다.

메모리 성능 개선

기업용 엔터프라이즈 서버는 NUMA(Non-Uniformed Memory Access)라는 메모리 설계 방식을 사용해서 대용량 메모리를 사용합니다. 예를 들어 인텔 플랫폼에서 물리 메모리가 500GB이고 NUMA 노드가 두 개 존재한다고 가정해 보겠습니다. 이때 0번부터 19번까지의 CPU는 0번 노드에, 20번부터 39번까지의 CPU는 1번 노드에 속하는데요. 이와 마찬가지로 500GB의 메모리도 두 개의 NUMA 노드에 나눠 연결됩니다. 따라서 0번 NUMA 노드에 속한 250GB의 메모리는 0번 노드(0~19번)에 속한 CPU에서 빠르게 접근할 수 있는 반면, 1번 노드(20~39번)에 속한 CPU에서 0번 노드에 속한 메모리에 접근하면 상대적으로 속도가 느립니다.

그렇다면 하나의 가상 머신이 같은 노드에 속한 CPU와 메모리만 사용한다면 어떻게 될까요? 이를 확인하기 위해 NUMA 노드가 두 개인 하이퍼바이저에 가상 머신(CPU 20개) 두 개를 생성하고, 각 가상 머신이 하나의 NUMA 노드에서만 실행되도록 옵션을 추가한 뒤 sysbench를 이용해서 성능 실험을 진행했습니다. 

먼저 CPU 성능 실험 결과입니다. 

스래드 개수14816
하이퍼바이저920.623678.617357.4114485.46
가상 머신1916.183663.937313.3713096.31
가상 머신2916.533665.517324.9513094.76

테스트 스레드가 많아질수록 약간의 성능 저하가 발생했지만 그럼에도 가상 머신이 90% 정도의 성능을 발휘한다면 사용하는 데에는 문제가 없다고 생각했습니다.

CPU 성능은 괜찮다는 것은 확인했으니 이제 메모리 성능 측정 결과를 살펴보겠습니다.

스레드 개수14816
하이퍼바이저(읽기/쓰기)1500.35 / 1454.695160.60 / 262.269498.14 / 372.0917527.66 / 485.41
가상 머신1(읽기/쓰기)1447.44 / 1413.535522.02 / 459.2210926.20 / 574.2213752.94 / 772.64
가상 머신2(읽기/쓰기)1449.13 / 1414.715489.58 / 468.1410963.66 / 556.2814386.05 / 776.93

메모리 성능은 제 예상보다 훨씬 좋게 나왔습니다. 실제 서비스 환경과 비교적 유사하다고 생각하는 스레드가 1~8개인 상황에서 물리 서버보다 더 좋은 결과가 나왔기 때문입니다. 특히 쓰기 성능은 스레드가 4~8개일 때 거의 두 배에 가까운 결과가 나왔습니다. 스레드가 16개일 때도 읽기 성능은 80% 정도로 나와서 사용 가능한 수준이었습니다. 이 실험을 통해 메모리 접근 속도가 NUMA 노드에 상당한 영향을 받는다는 것을 알게 됐습니다.

여기서 한 가지 특이한 점을 꼽자면 읽기보다 쓰기에서 훨씬 더 좋은 결과가 나왔다는 것인데요. 왜 이런 결과가 나왔을까요? 저는 단순히 DRAM 메모리의 성능뿐 아니라 L3 캐시의 성능도 더해져서 이런 결과가 나왔다고 생각합니다. L1 혹은 L2 캐시는 보통 CPU 내부에 존재하기 때문에 NUMA와 상관 없지만 L3 캐시는 NUMA 노드별로 나뉘는데요. 쓰기 성능은 캐시에 더 영향을 많이 받기 때문에 L3 캐시 성능이 높아지니 읽기와 비교해 월등히 더 좋은 성능이 나왔다고 생각합니다. 이와 관련해서 추후 여건이 된다면 캐시별 히트율을 조사해 볼 계획입니다.

다음은 참고 자료 링크입니다.

네트워크 성능 개선

네트워크 성능을 개선하기 위해 가상 머신의 모든 CPU가 네트워크 인터럽트를 처리하도록 만든 뒤 다시 성능을 측정했습니다.

스레드 개수14816
하이퍼바이저 간 측정7.7823.023.523.5
가상 머신12.6711.012.310.4
가상 머신22.598.757.5212.6

물리적으로 25Gbps의 네트워크로 연결된 상황에서 두 개의 가상 머신이 각자 최대로 네트워크를 사용하면 각각 10~12Gbps 정도를 사용합니다. 만약 하나의 가상 머신만 네트워크를 사용한다면 20Gbps까지도 올라갈 것입니다. 기존에 2~2.2Gbps가 나오던 것을 생각하면 압도적인 발전입니다.

게다가 위 결과는 최악의 상황에서 10Gbps라는 것이고, 보통 서비스는 오래 지속적으로 네트워크 처리를 바쁘게 진행하지는 않으므로 평균적으로는 그 이상의 성능을 발휘할 것입니다. 결국 처음 문제가 되었던 게이트웨이 서비스를 가상 머신으로 작동시킬 수 있다는 결과를 얻었습니다.

다음은 참고 자료 링크입니다.

디스크 성능 개선

앞서 성능 측정 실험을 하면서 각 가상 머신이 물리 디스크를 공유하기 때문에 성능 문제가 발생하는 것을 확인했습니다. 따라서 이 문제는 각 가상 머신에게 독점해서 사용할 수 있는 디스크를 할당하면 해결할 수 있으며, 그 해결 방법은 이미 많은 클라우드 회사는 물론 LY에서도 활용하고 있는 네트워크 블록 장치입니다. 네트워크 블록 장치는 각 가상 머신이 독점적으로 사용할 수 있는 블록 장치인데요. 블록 장치의 대역폭보다 더 큰 네트워크 대역폭을 공유하므로 여러 개의 가상 머신이 하나의 하이퍼바이저에서 작동할 때도 좀 더 좋은 성능을 낼 수 있습니다. 특히 최근 개발된 NVMe-over-fabric이나 NVMe-over-tcp 등의 네트워크 블록 장치들은 일반 SSD 디스크 이상으로 좋은 성능을 갖추고 있습니다.

이 글의 목적이 가상 머신의 성능 개선이므로 네트워크 블록 장치의 성능에 대해서 길게 이야기하지는 않겠습니다만, NVMe-over-tcp 블록 장치를 도입함으로써 하이퍼바이저에 물리적으로 연결된 장치보다 훨씬 더 좋은 성능을 제공할 수 있게 됐습니다.

빠른 가상 머신은 지구 환경에 도움이 될까요?

벤치마크 툴을 이용해 실험한 결과, 적절히 설정하면 하나의 하이퍼바이저에 두 개의 가상 머신을 실행해도 각 가상 머신이 물리 서버와 어느 정도 유사한 성능을 낼 수 있다는 것을 알게 됐습니다. 현재 LY에서는 수십에서 수백 대의 물리 서버로 실행하고 있는 서비스들을 가상 머신으로 옮겨서 물리 서버의 사용을 줄이기 위해 활발히 연구하고 있습니다. 특히 Redis나 MongoDB, OpenSearch 등의 대규모 서비스의 성능을 테스트한 결과, 가상 머신으로도 충분히 제공할 수 있다는 실험 결과를 얻어서 가상 머신 도입을 진행하고 있습니다.

물론 가상 머신은 분명히 한계가 있습니다. 당연히 물리 서버의 성능을 100% 대체할 수는 없습니다. 어떤 서비스나 애플리케이션이 물리 서버의 하드웨어 자원을 50% 정도 사용하고 있다면, 해당 물리 서버에 두 개의 가상 머신을 생성해서 하나는 원래 실행 중인 서비스를 실행하고, 다른 하나는 다른 서비스에 사용할 수 있다는 계산이 나옵니다. 하지만 실제로는 가상 머신을 실행하는 데 필요한 자원도 있으므로 기존 성능보다는 낮은 성능을 얻을 수밖에 없습니다. 따라서 최대로 사용하는 자원이 물리 서버의 50%라면, 가상 머신을 생성할 때 그보다 더 많은 자원을 가진 가상 머신을 생성해야 안정적으로 운영할 수 있습니다. 또한 물리 서버의 자원을 많이 사용하지 않는 것처럼 보여서 가상 머신으로 옮겼는데 생각지 못한 문제를 발견하는 경우도 있습니다. 예를 들어 데이터베이스 시스템이나 VoIP 같이 블록 장치나 네트워크 지연 시간(latency)에 민감한 서비스는 가상 머신으로 운영하는 게 간단하지 않습니다.

따라서 물리 서버에서 운영하고 있는 서비스를 가상 머신으로 이전하려면 성능과 안정성을 세심하게 모니터링해야 하고, 서비스에 필요한 각 컴포넌트를 충분히 이해할 필요가 있습니다. 단순히 동일한 소프트웨어를 가상 머신에 설치하고 실행하기만 하면 되는 일이 아니며, 경우에 따라서 서비스의 운영 정책까지도 바뀔 수 있는 긴 호흡이 필요한 작업이라고 생각합니다.

LY와 같이 다양한 국가에서 수억 명 이상이 사용하는 거대한 서비스를 안정적으로 제공하려면 그에 맞게 절대적으로 높은 성능의 서버가 필요합니다. 하지만 환경을 위해서 앞으로 더 적은 전기를 사용하고, 더 적은 쓰레기를 배출하면서도 더 좋은 서비스를 제공하기 위해 계속 노력하겠습니다.

참고 자료

Red Hat에서는 가상 머신 성능에 관해 다음과 같은 자료를 공개해 놓았습니다. 가상 머신 성능에 대한 자료이지만, 컴퓨터가 어떻게 작동하는지도 설명하고 있는 좋은 자료이므로 꼭 성능에 관심이 없더라도 한 번 읽어보면 많은 도움이 될 것이라고 생각합니다.