안녕하세요 🙋‍♂️ 하이퍼커넥트 미디어 서버 팀에서 Software Engineer로 일하고 있는 Redi입니다.

여러분들은 하이퍼커넥트에서 수많은 미디어 서버가 동작하고 있다는 것을 알고 계셨나요?
저도 입사한 후에야 알게 되었는데 하이퍼커넥트의 라이브 스트리밍 서비스하쿠나, 아자르 라이브에서 호스트들의 영상을 전 세계에 퍼져 있는 다수의 시청자에게 실시간으로 제공하기 위해 엄청나게 많은 미디어 서버가 24시간 내내 동작하고 있습니다!

그런데 만약 이 미디어 서버에 문제가 생긴다면 어떻게 될까요? 특히나 모두가 퇴근한 이후, 새벽에 단 꿈을 꾸고 있는 상황이라면? 아마 방송을 하지 못한다거나 재미있는 방송을 아무도 시청하지 못하여 수많은 컴플레인이 발생할 것이고 미디어 서버 팀원들은 붉어진 눈을 비비며 문제 해결을 위해 밤을 지새우게 될 것입니다.

다행히도 미디어 서버팀에서는 이런 너무나도 슬픈 상황을 사전에 차단하기 위해 미디어 서버 테스터를 만들어 사용하고 있습니다.

이 포스트에서는 테스터를 개발하게 된 구체적인 배경과 기능들을 간단히 소개해 드릴까 합니다!

자동화 테스트의 필요성

하이퍼커넥트의 미디어 서버 팀에서는 정확히 어떤 일을 하고 있을까요?

저희는 실시간 미디어 스트리밍을 위한 새로운 기능을 구현하거나 더 좋은 품질의 미디어를 유저분들께 제공하기 위해 운영 중인 서버의 성능을 개선하고 서버에 문제가 발생했을 때를 위한 모니터링 개선 작업운영등의 업무를 하고 있습니다.

운영 작업을 제외한 모든 업무가 미디어 서버에 새로운 코드를 추가하거나 수정하는 작업이기 때문에 미디어 서버가 정상적으로 동작함을 보장하도록 하는 것은 매우 중요한 일입니다.

func average(values []int) int {
    return sum(values) / len(values)  // divide by zero
}

func main() {
    empty := []int{}
    average(empty)
}

신입 redi는 나누기만으로도 서버를 죽일 수 있습니다.

코드 수정 및 추가로 인해 생성되는 버그들은 코드 리뷰 시스템과 유닛 테스트의 도입으로 대부분 개발 과정에서 잡을 수 있습니다. 하지만 큰 변경 사항이 있어 코드 수정량이 많거나, 테스트 케이스가 누락되면 버그들을 알아채지 못하는 경우가 발생하기도 합니다. 때로는 너무도 당연해보여 모두의 리뷰를 피해 살아남은 devide by zero 같은 (어이없는) 케이스가 발생할 수 있습니다.

scale in & out

미디어 서버들은 효율적인 운영을 위해 분산 구조 형태로 구현되어 서버를 자동으로 늘리거나(scale-out) 줄이며(scale-in) 동작을 하게 됩니다. 이러한 구조에서는 유닛 테스트 등으로 파악하기 힘든 문제가 발생할 수 있어 꼼꼼한 개발자 테스트가 필수적인데 종종 파악하지 못하고 배포까지 버그가 넘어가는 경우 또한 발생할 수 있습니다.

이처럼 갖가지 원인으로 인해 버그를 확인하지 못하는 상황이 발생할 수 있기 때문에 배포 전까지 최대한 많은 테스트가 필요합니다.

서버를 부수는 테스터의 개발

배포한 서버에 버그가 있다면 당연히 서비스에 영향을 줄 것이고 이는 24시간 운영되어야 하는 서비스 특성상 매우 큰 문제가 될 수 있습니다. 그렇기 때문에 실제 서비스에 올라가기 전 최대한 많은 테스트를 통해 버그를 찾아낼 수 있어야 합니다. (어차피 터질 서버라면 우리 손으로..)

sfu server 구조

미디어 서버에서는 방송하는 호스트들의 영상을 시청자들에게 전송해주고 있습니다. 서버에서는 크게 두 가지 데이터를 처리하고 있는데 하나는 방송 시청, 오디오와 비디오의 on-off 등의 요청을 위한 제어 요청이고, 다른 하나는 영상 및 음성 등의 미디어 데이터입니다. 호스트들이 방송을 시작한다는 제어 요청을 보낸 다음 미디어 서버에 미디어 데이터를 전송해주면 서버에서 방송 시청을 요청한 시청자들에게 미디어 데이터를 전송해주는 방식입니다.

만약 미디어 서버가 정상적으로 동작하는지를 확인하고 싶다면 제어 요청에 대한 처리만이 아닌 미디어 데이터가 제대로 전송되고 있는지도 확인해야 합니다.

옛날 옛적의 미디어 서버 테스터

미디어 서버 테스트를 위한 장치가 미디어 서버팀에 처음부터 없었던 것은 아닙니다.

최초 버전의 테스터는 미디어 서버와 연동이 가능한 테스트 웹 페이지를 JavaScript로 구현한 방식이었는데 이 웹 페이지로 테스트하기 위해선 미디어 서버의 방송과 시청에 관련된 기능을 직접 클릭해가며 기능을 검증할 수 밖에 없었습니다.

이 방식은 사람이 직접 웹페이지를 클릭해가며 확인하는 방법이다보니 시간이 매우 오래 걸리며, 실수로 테스트하지 않는 부분이 생길 수도 있다는 단점이 발생할 수 있었습니다. 이를 보완하기 위해서 테스트를 자동화 하기로 하였고 창을 띄우지 않고도 크롬 브라우저의 동작을 실행할 수 있는 Headless Chrome과 Node를 사용해 자동화하는데 성공했습니다.

하지만 여전히 치명적인 단점이 있었습니다.

첫째로 단순히 미디어를 보내고 받는 지표만 계산하면 되는 상황임에도 불구하고 Browser가 하는 모든 동작을 (미디어 인코딩, 디코딩) 수행해 컴퓨팅 자원을 많이 소모한다는 문제입니다. 이 경우 서버 한 대의 Capability 를 알아보기 위한 부하 테스트를 하려면 아주 많은 테스터 자원이 필요했습니다. 두번째로 미디어 서버팀이 사용하지 않던 Node와 Headless Chrome을 이용해 만들었기 때문에 관리 측면에서의 비용 증가가 발생한다는 문제가 있었습니다. 유지보수를 위해 팀원들이 새로운 개발 환경에 익숙해지길 강요 받게 되며 기능 추가나 수정을 위해 소모되는 비용이 클 수 밖에 없었습니다.

Brand New 미디어 서버 테스터

GoLang과 pion

미디어 서버팀에서 사용 중인 Go 언어와 Go 언어에서 미디어 데이터를 처리하기 위한 pion 라이브러리

레거시 미디어 서버 테스터를 사용하면서 발생한 클라우드 비용 💸 청구서의 압박, 사용성과 수정으로 골머리를 앓던 미디어 서버 팀에서는 그 모든 단점들을 해결한 Brand New 미디어 서버 테스터를 개발하기로 했습니다.

미디어 서버와의 통신을 위해 pion이라는 라이브러리를 사용해 제작된 새 미디어 서버 테스터미디어 데이터의 필요한 지표만 수집하고 다른 작업을 하지 않아 컴퓨팅 자원 소모를 줄일 수 있을 것이라 예상했고 미디어 서버 팀 전부에게 익숙한 Go 언어 기반으로 pion과 테스터가 제작되었기 때문에 코드를 공부하거나 유지 보수를 위해 투자하는 시간 역시 줄어들 것을 기대할 수 있었습니다.

새로운 미디어 서버 테스터는 다음의 주요한 목적을 가지고 있습니다.

  1. 서버가 제대로 동작하는지 여부 테스트
  2. 서버가 터졌을 때 버그 재현
  3. 서버가 어디까지 트래픽을 감당할 수 있는지 부하 테스트
  4. scale in, scale out 상황 테스트

주로 테스트 코드만으로는 문제를 확인하기 어렵거나 어떤 부분에서 이슈가 생겼는지 알기 어려울 때 테스터를 돌려 확인하려는 의도가 있었습니다. 이 목적을 만족시키기 위해 저희는 크게 2가지 테스트 방법을 사용하고 있습니다.

시퀀스 테스트

시퀀스 테스트는 가장 기본적인 테스트입니다. 미리 작성해 둔 시나리오대로 서버에 요청을 보냈을 때 서버가 제대로 동작하는지를 확인하는 테스트입니다. 기본적으로 수행해야 할 동작이나 버그가 발생할 것으로 예상되는 시나리오를 작성한 후에 테스터가 시나리오대로 요청을 보냈을 때 서버가 제대로 동작하는지 확인합니다.

{
  "subscriber_count": 500,
  "type": "sequence_test",
  "repeat_count": 1,
  "description": "basic sequence test (subscriber count: 500)",
  "host": {
    "host_count": 3,
    "host_scenario": [
      {
        "command": "video_off",
        "wait_time": 50
      },
      {
        "command": "video_on",
        "wait_time": 50
      }
    ]
  },
  "sequence": [
    {
      "command": "start_subscribe",
      "wait_time": 120,
      "command_id_when_failed": 3,
      "fail_wait_time": 10
    },
    {
      "command": "video_off",
      "wait_time": 60,
      "command_id_when_failed": 3,
      "fail_wait_time": 1
    },
    {
      "command": "video_on",
      "wait_time": 60,
      "command_id_when_failed": 3,
      "fail_wait_time": 1
    },
    {
      "command": "exit",
      "wait_time": 1
    }
  ]
}

방송 송출과 방송 시청 시나리오를 각각 작성할 수 있다.

테스트를 위해서는 테스트하는 상황(시나리오)을 JSON 형식의 파일로 작성해야 합니다.

위의 예제는 세 명이 방송하고 있고 500명의 시청자가 각 방송을 시청하는 시나리오를 보여주고 있습니다. 세 명이 음성 전용 방송이나 비디오 방송 중 랜덤하게 미디어를 전송하고 500명의 시청자가 비디오를 끄고 켜는 등의 실제 유저들이 할 법한 동작을 정의하여 테스트할 수 있습니다.

이 테스트를 실행한 후 서버의 로그와 지표, 테스터에서의 지표를 확인해보며 서버가 제대로 동작했는지를 확인할 수 있습니다. 이 외에 여러 시나리오를 미리 작성해두어 테스터에서 다양한 케이스에 대한 테스트를 쉽게 확인할 수 있습니다.

임의 동작 테스트

하지만 유저가 어떤 동작을 할 것인지 개발자가 항상 예측하지는 못합니다. 시퀀스 테스트는 이미 작성된 시나리오에 대해서는 테스트할 수 있지만 예상하지 못한 버그는 확인하지 못합니다. 그때 사용할 수 있는 것이 임의 동작 테스트입니다. 정해진 시나리오대로 실행되는 시퀀스 테스트와 달리 임의 동작 테스트는 커맨드 후에 어떤 동작이 실행될지 정해져 있지 않습니다. 매번 테스트할 때마다 랜덤하게 다음 동작이 정해지는 방법입니다.

{
  "subscriber_count": 500,
  "type": "behavior_test",
  "repeat_count": 100,
  "description": "basic behavior test (subscriber count: 500)",
  "host": {
    "host_count": 3
  },
  "behavior": [
    {
      "command": "start_subscribe",
      "next_command": [
        "start_subscribe", "subscribe_other_host", "exit",
        "video_on", "video_off",
        "audio_on", "audio_off"
      ]
    },
    {
      "command": "video_off",
      "next_command": [
        "start_subscribe", "subscribe_other_host", "exit",
        "video_on", "video_off",
        "audio_on", "audio_off"
      ]
    },
    ...
    {
      "command": "subscribe_other_host",
      "next_command": [
        "start_subscribe", "subscribe_other_host", "exit",
        "video_on", "video_off",
        "audio_on", "audio_off"
      ]
    },
    {
      "command": "exit",
      "next_command": [
        "start_subscribe"
      ]
    }
  ],
  "start_command": "start_subscribe",
  "min_wait_time": 100,
  "max_wait_time": 5000
}

커맨드별로 다음에 올 수 있는 커맨드를 지정해두면 실행할 때 랜덤하게 선택합니다.

임의 동작 테스트에서는 다음 커맨드를 실행하기 위한 대기시간도 최소 대기시간과 최대 대기시간 중 랜덤하게 선택합니다. 랜덤한 시간 간격에 랜덤한 커맨드를 실행해보는 것입니다. 실행 결과로 테스터에 수집된 지표를 개발자가 파악하기 어렵지만, 개발자가 보기에 이상한 동작을 해도 서버에 문제가 생기지 않는지 확인해 보는 의도였습니다.

결과적으로는 시퀀스 테스트는 서버의 동작 여부 확인이나 부하 테스트에는 효과적이었지만 원인을 파악하지 못한 버그를 재현하는 데에는 어려움이 있었습니다. 반면에 임의 동작 테스트는 서버에 문제가 발생하는지 여부 확인하는 것에 가장 효과적이었습니다.

정해져 있는 순서대로 실행되는 방식이 아닌 비교적 마구잡이로 요청을 보내다 보면 어느 순간부터 서버에 문제가 있는 상태가 되는 경우를 발견할 수 있어, 실제 서버에서 가끔가다 한 번씩 일어나는 찾기 어려운 문제를 해결하는 데 도움이 되었습니다.

현재는 서버를 배포하기 전 임의 동작 테스트를 실행해보고 문제가 발생하지 않으면 배포를 진행하면서 실제 서비스에 문제가 발생하기 전 미리 확인해보는 용도로도 사용하고 있습니다.

결과

새로운 미디어 서버 테스터의 구현으로 많은 개선을 이룰 수 있었습니다. 먼저 서버의 Capability 테스트를 위한 작업에서 Headless Chrome과 Node 를 이용한 방식의 단지 1/5 정도의 컴퓨팅 자원 소모하는 획기적인 개선이 있었습니다. 앞서 언급했던 바와 같이 pion을 직접 다루면서 Headless Chrome의 내부에서 수행되던 많은 일들을 제거할 수 있었던 것이 가장 컸다고 생각합니다.

또한 서버 수정 후 배포 전까지 시나리오 테스트를 주기적으로 돌려 여러 상황에서 정상적인 미디어 서버 동작을 체크할 수 있어 안정성을 확보하였고 임의 동작 테스트를 돌리면서 미처 체크하지 못한 동시성 이슈나 버그로 인한 서버 크래시를 미연에 방지하는 케이스도 여러번 경험할 수 있었습니다.

보너스로, 밑바닥부터 전부 만들게 되었기 때문에 미디어 서버에서 사용되는 WebRTC 기술에 대해 한 번쯤 공부할 수 있는 좋은 계기가 되기도 했습니다.

마치며

지금까지 미디어 서버 테스트를 위해 사용하고 있는 미디어 서버 테스터에 대해 소개해 드렸습니다. 사실 미디어 서버 테스터를 입사한 후부터 개발하기 시작해 처음에는 미디어 서버 테스터가 서버보다 먼저 죽기도 했었지만, 이제는 서버를 더 많이 죽이게 되었습니다.(?)

입사하고서 개발하게 된 코드가 합법적으로 서버를 터뜨리고, 서버가 터지면 오히려 기뻐하게 되는(?) 아이러니한 상황이 되었습니다. (물론 실 서버에서 터지면 눈물이 앞을 가립니다) 앞으로도 서버를 배포 전에 터뜨리는 다크 나이트 미디어 서버 테스터를 통해 미디어 서버 안전성을 올리는 데 노력하겠습니다.

마지막으로 저희가 채용 중이라는 소식을 전하면서 글을 마칩니다.

🙌 Hyperconnect 채용공고 바로가기 🙌

🚀 Media Server Engineer 채용공고 바로가기 🚀

reference


[1] https://tecoble.techcourse.co.kr/post/2021-10-12-scale-up-scale-out/

[2] https://medium.com/pplink/1000명이서-실시간-커뮤니케이션을-하는-가장-효과적인-방법-ca039fbcaedb

[3] https://go.dev/

[4] https://github.com/pion