네이버 오픈소스를 활용하여 확장성 있는 서버 아키텍처를 구축하고 성능 개선해보기 - 2편

1월 12일, 2018

1편에 이어서 작성한 내용입니다. 1편을 읽지 않으신 분은 1편을 먼저 보신 후 이 글을 보시면 이해에 도움이 될 수 있습니다.

[1편]

  1. 확장성 있는 서버 아키텍처
  2. 성능 개선 방법

[2편]

  1. 확장성에 도움을 주는 네이버 오픈소스
  2. 성능 개선을 위한 작업

1편의 요약

1편에서는 1. 확장성 있는 서버 아키텍처2. 성능 개선 방법에 대해 다뤘습니다. 1. 확장성 있는 서버 아키텍처 파트에서는 기본 서버는 어떻게 구성하는지 먼저 알아보았고, 사용자 증가에 따른 대응 방법으로 Scale-up과 Scale-out을, 여러 개의 서버를 구성하기 위한 방법으로 Shared Everything과 Shared Nothing에 대해 알아보았습니다.

2. 성능 개선 방법 파트에서는 멀티 노드를 구현하기 위한 방법을 웹 서버 / 서버사이드 언어 / 데이터베이스 관점에서 각각 살펴보았고, 캐시를 사용해서 성능을 개선하는 방법을 알아보았습니다.

이번 포스트에는 위의 이론들을 실제 오픈소스를 활용해서 구현해 보는 내용을 담았습니다.

셋째. 확장성에 도움을 주는 네이버 오픈소스

Arcus

1. Arcus란

Arcus

아커스 (Arcus)는 memcachedZooKeeper를 기반으로 네이버 (NAVER) 서비스들의 요구 사항을 반영해 개발한 메모리 캐시 클라우드 입니다.

- 아커스 공식 홈페이지

설명을 읽어보면 네이버 서비스에서 성능 개선을 위해 자체적으로 만든 프로젝트임을 유추할 수 있습니다. “캐시 클라우드”라 했는데, 캐시는 1부에서 말했던 데이터베이스로의 캐시이며, 클라우드라 함은 이런 캐시들 여러 개가 모여서 하나의 캐시처럼 동작하는 것이라고 보면 됩니다.

구체적으로는 캐시 클라우드를 구현하기 위해 “memcached”라는 것과 “ZooKeeper”라는 것을 사용하는 것 같네요. 하나씩 살펴봅시다.

2. Memcached

Memecached

Memcached는 Facebook, Twitter, Reddit 및 YouTube와 같은 클라우드 및 웹 서비스 제공 회사에서 사용하는 key-value 메모리 캐시로, 웹 데이터를 소비자에게 서비스하는 데 있어 지연 시간을 줄이고 데이터베이스 및 컴퓨팅 서버에 대한 증설을 줄여주게 한다.

Memcached는 key-value 쌍으로 이뤄진 간단한 데이터 타입을 저장하며, NoSQL 데이터베이스와 유사하지만 NoSQL처럼 영구적 (persistent) 이지는 않다. Memcached는 모든 key-value 쌍을 메모리에 저장하므로 서버 장애나 오류가 발생했을 때 저장된 데이터가 모두 손실된다. 이때 키는 고유한 값이다.

- Memcached의 확장성 개선, 네이버 D2 (http://d2.naver.com/helloworld/151047)

Memcached는 오픈소스로 배포되었고 다양한 기업에서 쓰고 있는 전형적인 캐시 데이터베이스 중 하나입니다. 그중에서도 “메모리” 상에 위치하여 더 빠른 속도를 자랑하는 대신 데이터가 보존됨을 보장하지는 않습니다.

2000년대부터 사용된 꽤나 오래된 프로젝트인데, 그만큼 안정적이기는 하지만 최신의 요구를 반영하지는 못한다는 문제도 있습니다. 그렇기 때문에 기업에서는 memcached를 원하는 용도에 맞게 개선해서 사용하거나, 혹은 비슷한 기능을 하는 다른 캐시를 사용하기도 하죠. Arcus는 전자에 속한다고 볼 수 있습니다.

3. ZooKeeper

ZooKeeper

아파치 주키퍼(Apache ZooKeeper)는 아파치 소프트웨어 재단 프로젝트중의 한 소프트웨어 프로젝트로서 공개 분산형 구성 서비스, 동기 서비스 및 대용량 분산 시스템을 위한 네이밍 레지스트리를 제공한다. 주키퍼는 하둡의 한 하위 프로젝트이었으나 지금은 독립적인 상위 프로젝트이다. 주키퍼의 아키텍처는 중복 서비스를 이용한 고가용성을 제공한다. 클라이언트는 주키퍼 마스터가 응답을 하지 않으면 다른 주키퍼 마스터에게 요청을 한다. 주키퍼 노드들은 파일 시스템이나 trie 데이터구조와 비슷한 구조의 네임 스페이스안에 데이터들을 저장한다. 클라이언트들은 이 노드들에게서 읽거나 쓴다.

- 위키백과

설명이 참 어렵습니다. 무언가 분산 시스템을 위해서 사용하는 소프트웨어인 것은 알겠는데, 구체적으로 어떤 역할을 하는지 의문입니다. 네이버의 글을 인용해서 다시 한번 살펴봅시다.

분산 처리 환경에서 필수로 언급되는 ZooKeeper란 무엇일까? 한 마디로 정의하면 “분산 처리 환경에서 사용 가능한 데이터 저장소“라고 말할 수 있겠다. 기능은 매우 단순하지만 분산 서버 환경에서는 활용 분야가 넓다. 예를 들어 분산 서버 간의 정보 공유, 서버 투입/제거 시 이벤트 처리, 서버 모니터링, 시스템 관리, 분산 락 처리, 장애 상황 판단 등 다양한 분야에서 활용할 수 있다.

ZooKeeper는 데이터를 디렉터리 구조로 저장하고, 데이터가 변경되면 클라이언트에게 어떤 노드가 변경됐는지 콜백을 통해서 알려준다. 데이터를 저장할 때 해당 세션이 유효한 동안 데이터가 저장되는 Ephemeral Node라는 것이 존재하고, 데이터를 저장하는 순서에 따라 자동으로 일련번호(sequence number)가 붙는 Sequence Node라는 것도 존재한다. 조금 과장하면 이러한 기능이 ZooKeeper 기능의 전부다. 이런 심플한 기능을 가지고 자신의 입맛에 맞게 확장해서 사용하면 된다.

(중략)

첫 번째로 Persistent Node는 한 번 저장되고 나면 세션이 종료되어도 삭제되지 않고 유지되는 노드다. 즉, 명시적으로 삭제하지 않는 한 해당 데이터는 삭제 및 변경되지 않는다.

두 번째로 Ephemeral Node는 특정 노드를 생성한 세션이 유효한 동안 그 노드의 데이터가 유효한 노드다. 좀 더 자세히 설명하면 ZooKeeper Server에 접속한 클라이언트가 특정 노드를 Ephermeral Node로 생성했다면 그 클라이언트와 서버 간의 세션이 끊어지면, 즉 클라이언트와 서버 간의 Ping을 제대로 처리하지 못한다면 해당 노드는 자동으로 삭제된다. 이 기능을 통해 클라이언트가 동작하는지 여부를 쉽게 판단할 수 있다.

세 번째로 Sequence Node는 노드 생성 시 sequence number가 자동으로 붙는 노드다. 이 기능을 활용해 분산 락 등을 구현할 수 있다.

- 네이버 D2 (http://d2.naver.com/helloworld/294797)

Arcus에서 ZooKeeper를 어떤 식으로 활용하는지 정확히 알기는 어렵지만, 그래도 다음과 같은 작업을 처리하는데 사용한다고 유추할 수는 있을 것 같습니다.

  • 분산된 캐시 서버 간의 통신
  • 새로운 서버를 열었을 때, 혹은 기존 서버를 제거했을 때 다른 노드들에게 알리기
  • 연결이 끊어진 (오류가 났을 것으로 예상되는) 노드 삭제
  • 노드 간의 Lock 처리

Arcus에서는 이런 분산 시스템에서의 작업을 처음부터 손수 작업하는 대신, 이미 검증된 소프트웨어인 ZooKeeper를 이용하여 캐시 클라우드를 만들었다고 할 수 있을 것 같습니다.


nBase-ARC

1. nBase-ARC란

nBase-ARC

nBase-ARC는 네이버에서 사용하는 일련의 분산 저장 플랫폼 중에 하나입니다. 2013년 7월 사내에 배포된 이후 밴드, 카페, 게임 등의 서비스에 폭넓게 사용되고 있습니다. Redis의 장점을 그대로 가져가며, 대규모 서비스 운용에 필요한 고가용성, 확장성 요구사항을 구현했습니다.

nBase는 분산 저장 플랫폼의 브랜드 이름이고, ARC는 autonomous Redis cluster의 약자다. 다른 nBase 플랫폼과 기술적인 내용은 전부 다르지만, 안정적인 분산 저장소를 제공한다는 본연의 의미를 잘 따르고 있다. Autonomous Redis cluster란 이름의 의미를 살펴보면 다음과 같다.

  • Autonomous: 사람이 직접 운영할 필요 없이 소프트웨어에서 자동으로 클러스터를 진단하고 형상을 변경할 수 있다는 의미다.
  • Redis: 데이터의 기본 저장소다.
  • Cluster: 전체 데이터가 하나의 장비에 있지 않고 분산되어 저장되고 처리된다는 의미다.

- 네이버 D2 (http://d2.naver.com/helloworld/614607)

Arcus와 비슷하게 캐시 클라우드 기능을 하는 오픈소스지만, 베이스로 memcached 대신 redis를 사용합니다. 네이버의 소개 글을 읽어보면 “호환성”과 “클러스터”를 강조하는데요, 다수의 redis 노드를 구성하는 것을 클러스터라 하며, 이렇게 클러스터로 구성된 노드들을 사용할 때, redis만 사용하던 방법과 같은 방법으로 쓸 수 있도록 호환성을 제공하는 것이 nBase-ARC의 가장 큰 특징이라고 할 수 있습니다.

2. redis란

redis

Redis는 memcached 같은 메모리 기반의 key-value 스토리지입니다. Hash Set, Set, List, Strings, Sorted Set 같은 여러 데이터 구조를 지원하며, 현재 가장 많이 사용되고 있는 오픈소스죠. Redis를 잘 활용한다면 굉장히 빠르게 캐시를 서비스에 적용할 수 있고, 상당한 성능 향상도 시킬 수 있습니다. 네이버는 이런 좋은 오픈소스를 이용하되, 자신들의 추가적인 요구 사항을 덧붙여서 redis를 감싸는 형태로 프로젝트를 하나 만들었고, 그 것이 nBase-ARC라고 할 수 있습니다.



NGrinder

ngrinder

1. NGrinder란

nGrinder is a platform for stress tests that enables you to execute script creation, test execution, monitoring, and result report generator simultaneously. The open-source nGrinder offers easy ways to conduct stress tests by eliminating inconveniences and providing integrated environments.

- NGrinder 공식 홈페이지

NGrinder는 스트레스 테스트를 해주는 플랫폼이라고 합니다. 플랫폼에서는 스트레스 테스트를 위한 스크립트를 생성하고, 실행하고, 모니터링하고, 리포트까지 즉각적으로 뽑을 수 있다고 말하는데요, 쉽게 말해 이 서버는 몇 명의 사용자가 들어올 때까지 버틸 수 있는지를 확인하는 용도의 소스입니다. 그리고 이를 확인하기 위해 스크립트를 작성하고, 실행하며 모니터링 툴을 통해서 몇 번째 가상의 사용자가 접속했을 때 뻗어버리는지를 확인할 수 있죠.

스트레스 테스트는 주어진 시스템이나 실체의 안정성을 결정하기 위해 진행되는 테스트로, 소프트웨어에서는 막대한 부하를 통해 튼튼함, 이용 가능성, 오류 관리를 강조한다고 합니다.

이를 통해 신규 서비스를 런칭하기 전이나, 서버를 미리 구비하기 전에 어느 정도 비용을 들이면 되는지를 미리 확인하여, 비용을 최적화하거나 서비스가 중단되지 않도록 미리 준비할 수 있습니다.

2. 성능 지표들

NGrinder Screenshot
사진: http://cloud.syncrofusion.com

nGrinder 이용하려 스트레스 테스트를 수행하고 나면 다음과 같은 리포트를 받을 수 있습니다. 리포트에 쓰여있는 지표는 각각 다음을 의미합니다.

  • TPS(Transaction Per Second) : 초당 트랜젝션의 양
  • Peak TPS: 서버가 순간적으로 가장 부하를 많이 받는 순간의 TPS
  • Mean Test Time: 평균 테스트 시간
  • Executed Tests: 총 수행 테스트 수
  • Scucessful Tests: 성공적으로 수행된 테스트 (2xx Response를 받은 테스트)
  • Errors: 오류가 발생한 테스트 수

이런 지표를 통해 어떤 구조가 가장 효율적인지 확인할 수 있습니다. 예상되는 사용자에 맞춰서 서버를 미리 증설할 수도 있죠.


넷째. 성능 개선을 위한 작업

사실 지금까지의 섹션들은 이 섹션을 위한 초석이었습니다. 수업에서의 목표는 네이버 오픈소스를 활용하여 직접 성능 개선을 해보는 것이지, 그저 알아보기만 해서는 점수를 받을 수 없죠. (^^;) 어쨌든 성능 개선을 하기 위해서는 기준이 있어야 합니다. 기존의 무언가를 수정해서 성능이 좋아지면 그게 개선이고, 과제의 목표는 이런 개선 작업을 진행하는 것이었죠. 하지만 과제에서는 별다른 기준 프로젝트나 코드를 제공하지 않았습니다. 그래서 처음 해야 할 작업은 개선의 여지가 있는 프로젝트부터 먼저 만드는 것이었습니다.

기준 프로젝트의 개발

이왕 기준 프로젝트를 만들어야 하는 거, 제대로 만들어 보기로 했습니다. 기준 성능을 낮추기 위해 일부러 나쁜 알고리즘을 썼다가, 이걸 좋은 알고리즘으로 바꾸는 식의 성능 개선은 별로 의미가 없다고 생각했습니다. 그래서 일반적인 방법으로는 잘 짜도 성능이 느린 대신, 캐시를 썼을 때 성능 증가 폭이 큰 요구 사항이 무엇이 있을지를 고민해 보았고, 이를 토대로 부탁하냥(ASKHY)이라는 프로젝트를 만들었습니다.

askhy

심플하게 생긴 이 사이트는 Flask로 개발된 웹 애플리케이션입니다. 웹 서버로는 nginx를 사용하고, 데이터베이스는 MySQL을 사용합니다. 배포가 쉽도록 Docker라는 기술을 통해 패키징 하기도 했습니다.

이 웹 애플리케이션은 일반적인 게시판과 느낌이 비슷한데, 게시글 대신 “부탁“이라는 특별한 이름을 사용하고, 댓글 대신 “응원“이라는 이름을 사용합니다. “부탁”마다 “응원”이 달려있고, 각각에 대해서는 작성 시간, 메시지, IP 주소 등이 함께 저장되죠. 이 웹 애플리케이션의 데이터베이스 스키마는 아래와 같습니다.

askhy_scheme

2개의 릴레이션 밖에 사용하지 않는 아주 간단한 프로젝트입니다. 이 스키마를 이용해서 View에서 사용할 데이터를 가져와야 하는데, 메인 화면만 우선 고려해 보도록 합시다. 메인 화면에서 요구하는 데이터는 아래와 같습니다.

  • 부탁(ask) 고유 ID
  • 부탁 메시지
  • 부탁 등록 시간
  • 부탁 별 응원(cheer) 수
  • 부탁 별 순수 응원 수: IP 하나 당 중복되는 응원을 제거하여, 순수하게 몇 명이 해당 부탁에 응원을 했는지를 보여주는 숫자

“부탁”과 “응원”의 정보가 모두 필요하니 두 테이블을 조인하거나 프로젝션 해야 합니다. 다만 “응원”에 대해서는 모든 정보가 필요한 것이 아닌, 개수 정보만 필요하니 COUNT 함수와 함께 Nested Sub Query 정도만 써도 될 것 같습니다. 아래는 위 정보들을 가져오는 쿼리입니다.

SELECT
	*,
	(SELECT COUNT(*) FROM `cheer` WHERE ask_id = ask.id) AS cheer_cnt,
	(SELECT COUNT(DISTINCT ip_address) FROM `cheer` WHERE ask_id = ask.id) AS pure_cheer_cnt
FROM `ask`

이 쿼리는 “응원(cheer)” 개수가 많아지면 속도가 매우 느려지게 됩니다. cheer.ask_idcheer.ip_address에 인덱스가 걸려있지 않다면 O(n*m)의 수행 시간을 가질 수도 있죠.

굉장히 간단한 애플리케이션지만, 내부적으로는 시간이 꽤 걸리는 쿼리를 사용하고 있습니다. 캐시를 사용하여 프로젝트를 개선한다면 상당한 성능 향상이 있을 것으로 보입니다.

캐시로 데이터 조회를 빠르게 하기

그럼 캐시 데이터베이스를 사용해서 이 웹 애플리케이션을 개선해 봅시다. 우리는 key-value 스토리지를 사용하기 때문에 직접 위의 관계 모델을 적합한 key-value 형태로 변형하여 캐싱 해야 합니다.

1. 캐시 사용 결정

어떤 것을 저장할지는 완전히 개발자의 몫입니다. 자주 사용하는 데이터만 캐시에 저장할 수도 있고, 모든 데이터를 캐시에 저장할 수도 있고, 가져오는데 오래 걸리는 정보만 캐시에 저장할 수도 있습니다.

저는 그중에서도 “가져오는데 오래 걸리는 정보만 캐시에 저장”하기로 했습니다. 바로 부탁 별 응원 수 (cheer_cnt)부탁 별 순수 응원 수 (pure_cheer_cnt)죠. Nested Sub Query를 사용하는 대신에 아래 쿼리만 사용해서 MySQL에서는 전체 부탁에 대한 정보만 가져옵니다. 응원 개수는 일단 가져오지 않구요.

SELECT * FROM `ask`

다음으로는 전체 부탁에 대해 iteration을 돌아 하나하나마다 cheer_cntpure_cheer_cnt캐시에서 찾는 방법을 사용합니다. 아래처럼 말이죠.

for id, message, ip_address, register_time in result :
	cheer_cnt = cache_client.get('askhy:cheer_cnt_' + str(id)).get_result()
	pure_cheer_cnt = cache_client.get('askhy:pure_cheer_cnt' + str(id)).get_result()

여기서 캐시의 cheer_cnt_{id}, 혹은 pure_cheer_cnt_{id}로 이름 지었습니다. 아는 데이터 유형(cheer_cnt 혹은 pure_cheer_cnt)에 데이터의 고유한 값(id)을 붙인 것인데요, 이렇게 키 이름을 설정하면 같은 키 이름에 대해 항상 하나의 데이터만 가리키게 되고, 나중에 다른 데이터 유형을 저장할 때도 쉽게 확장할 수 있게 됩니다.

캐시를 조회했는데 발견하지 못했다면 원래처럼 MySQL에서 데이터를 조회해야 합니다. 조회 이후에는 그 결과를 캐시에 올려두어 다음 실행부터는 빠르게 데이터를 가져올 수 있게 해줄 수 있죠.

결과적으로 이 웹 애플리케이션에 대한 캐시 프로세스는 아래로 정리할 수 있습니다.

  1. ask 테이블에 대해서만 쿼리 조회
  2. ask.id를 바탕으로 캐시에서 cheer_countpure_cheer_count 조회
  3. 있다면 캐시를 사용하고 6으로 이동
  4. 없다면 askcheer을 중첩으로 조회하는 무거운 쿼리 재 조회
  5. MySQL에서 받아온 데이터를 다시 캐시에 저장
  6. HTML 렌더링

2. Arcus를 코드에 추가하기

네이버에서 Arcus를 사용하기 편하도록 제작한 client가 있습니다. 저는 기준 프로젝트인 “부탁하냥”을 python으로 만들었기에, arcus-python-client를 사용하여 Arcus와 연결을 하였습니다.

아래는 Arcus를 이용해서 위의 프로세스를 동작시킨 코드입니다. arcusdriver를 별도로 만들어 캐시 사용을 일관성 있게 처리했으며, dataset에 최종적인 결과를 담도록 했죠. 위의 프로세스처럼 ask 하나하나마다 cheer_cnt, pure_cheer_cnt를 캐시에서 가져오는 것 까지는 똑같은데, 약간의 차이가 있다면 만약 하나의 캐시라도 찾지 못했다면 그 ask에 대해서 SQL 쿼리를 재 조회하는 방법 대신, 전체 ask에 대해서 재 조회하는 방법을 사용한다는 것입니다. 캐시가 하나도 없을 때, 전체 ask의 수만큼 SQL 쿼리가 수행되는 비 효율적인 상황을 막기 위해서죠.

arcus_client = arcusdriver.get_client()
dataset = []

with get_db().cursor() as cursor :
		# Get data in `ask` only (not with cheer count)
		cursor.execute("SELECT * FROM `ask`")
		result = cursor.fetchall()

		success = True

		for id, message, ip_address, register_time in result :
			cache = arcus_client.get('askhy:cheer_count_' + str(id)).get_result()
			cache2 = arcus_client.get('askhy:pure_cheer_count_' + str(id)).get_result()

			if cache == None or cache2 == None :
				# Re-run query with count(*)
				success = False
				break
			else :
				dataset.append((id, message, ip_address, register_time, cache, cache2))
		
	if not success :
		with get_db().cursor() as cursor :
			# Get data with cheer count
			cursor.execute("""SELECT *,
				(SELECT COUNT(*) FROM `cheer` WHERE ask_id = ask.id) AS cheer_cnt,
				(SELECT COUNT(DISTINCT ip_address) FROM `cheer` WHERE ask_id = ask.id) AS cheer_cnt_pure
				FROM `ask`
			""")
			result = cursor.fetchall()

			dataset = []

			for id, message, ip_address, register_time, cheer_cnt, cheer_cnt_pure in result :
				dataset.append((id, message, ip_address, register_time, cheer_cnt, cheer_cnt_pure))
				arcus_client.set('askhy:cheer_count_' + str(id), cheer_cnt)
				arcus_client.set('askhy:pure_cheer_count_' + str(id), cheer_cnt_pure)

위는 캐시 사용에 있어 핵심적인 부분이었으며, 전체 코드는 https://github.com/Prev/askhy/tree/1.1-arcus-combined 에서 확인할 수 있습니다. 결과적으로 Arcus를 추가한 서버의 아키텍처는 아래처럼 됩니다.

architecture_arcus

3. nBase-ARC를 코드에 추가하기

nBase-ARC는 redis와 완전히 호환성을 갖습니다. 이 말은 redis를 위해 만들어진 client를 사용해도 nBase-ARC 서버에 접속할 수 있다는 소리죠. redis는 굉장히 유명하고 많이 쓰이는 오픈소스입니다. 클라이언트가 굉장히 잘 만들어졌고 관리되고 있죠. PyPi를 통해서도 설치할 수 있습니다.

성능 개선 로직은 Arcus 사용 시와 완전히 동일합니다. 코드는 https://github.com/Prev/askhy/tree/1.1-redis-combined 에서 확인할 수 있고, 아키텍처는 아래처럼 됩니다.

architecture_nbasearc

멀티 노드 구성을 통해 트래픽 분산시키기

지금까지 캐시를 통해 데이터 조회를 빠르게 하는 작업을 진행해봤습니다. 그럼 이제 멀티 노드 구성을 통해서 트래픽을 분산시키고, 이를 통해 다수의 사용자에 대응할 수 있도록 아키텍처를 개선해 봅시다.

1. 웹 애플리케이션의 멀티 노드 구성

1부에서 웹 애플리케이션(서버사이드 언어)을 멀티노드로 구성하려면 세션 간에 메모리를 공유하지 않고 상호 독립적으로 동작해야 한다고 했습니다. (보러 가기) “부탁하냥”에서 사용한 프레임워크인 Flask는 기본적으로 이를 보장합니다. 제가 개발을 할 때에도 메모리를 공유하는 로직은 짜지 않았죠. 즉, 여러 웹 애플리케이션을 띄우고 중앙에 Load Balancer만 넣게 된다면 멀티 노드 구성을 바로 할 수 있었습니다.

2. 로드밸런싱

가장 기본적인 Load Balancer는 연결된 서버 중 하나에 랜덤으로 데이터를 전송할 것입니다. 조금 더 똑똑한 Load Balancer는 트래픽에 여유가 있는 서버에 데이터를 전송할 수도 있죠. Load Balancer는 소프트웨어로 만들어질 수도 있고, 라우터처럼 소프트웨어 없이 하드웨어로만 구성할 수도 있습니다.

- 1부

1부에서 Load Balancer를 구현하는 방법을 간단하게 설명했었습니다. 하지만 우리는 직접 이것까지 구현할 필요는 없습니다. 사실 이미 꽤 잘 만들어진 Load Balancer들이 공개되어 있으니까요. 최근 Apache 대신 많이 쓰이고 있는 웹 서버인 nginx에서도 Load Balancing 기능을 제공합니다.

서버 노드 하나를 추가로 띄우고, nginx를 설치해서 Load Balancer 역할을 하도록 설정해 봅시다. 아래처럼 config 파일을 적어주고 nginx를 시작하면 이 노드는 Load Balancer가 됩니다.

upstream askhyapp {
	server 172.17.0.3;
	server 172.17.0.4;
	server 172.17.0.5;
}
server {
	listen 80;
	location / {
		proxy_pass http://askhyapp;
	}
}

그럼 멋지게도, 서버의 아키텍처는 다음 그림처럼 됩니다. 여기에 캐시를 다시 추가하면 더 나은 구조가 되겠죠.

architecture_multinode

NGrinder를 이용하여 성능 측정하기

캐시 데이터베이스를 사용하고, 멀티 노드 구성을 통해 성능이 향상되도록 프로젝트를 수정해보았습니다. 하지만 실제로 성능 개선이 되었는지는 확인을 해봐야겠죠.

NGrinder 노드를 띄우고, 웹 애플리케이션이나 Load Balancer에 부하 테스트를 진행합니다. 서버 성능에 따라서 얼마나 많은 요청을 보낼지 정할 수 있겠죠. 저는 총 4가지 버전에서의 성능 테스트를 진행 해 보았습니다. MySQL만 사용한 버전(1), MySQL과 Arcus를 사용한 버전(2), MySQL과 nBase-ARC를 사용한 버전(3), MySQL만 사용하지만 서버 애플리케이션을 멀티노드로 구성한 버전(4)으로 각각 테스트해보았고, 그 결과는 아래 사진과 같습니다.

TPS 측정값

TPS (Transaction Per Second)의 경우 약 4~5배 차이가 남을 확인할 수 있었고, MTT (Mean Test Time)의 경우에도 마찬가지로 약 4~5배 차이가 남을 확인할 수 있었습니다. 상당한 개선이 있었죠. 아마 캐시와 멀티 노드 구성을 함께 한다면 더 큰 성능 향상이 있을 것 같습니다.

MTT 측정값

결론

처음 이 프로젝트가 던져졌을 때, 굉장히 막막했습니다. 위 내용은 학부과정에서 다루는 일반적인 내용이 아니며, 그렇기 때문에 강의자료도 턱없이 부족했습니다. 네이버 D2의 hello world 글을 찾아가고 SlideShare를 뒤져가면서 공부했고, 이를 토대로 개발을 진행해나갔죠.

project_objectives

약간의 과장을 보태면 거의 이 PPT 한 장을 보고 과제를 진행했습니다. 왜 이 프로젝트를 진행해야 하는지는 거의 듣지 못했고, 대신 “어떤어떤 오픈소스를 써봐라” 정도의 기준만 내려와있었죠. 채점 기준에 맞춰서 이 오픈소스들이 있는 이유를 해석해가며 공부한 결과로 “네이버 오픈소스를 활용하여 확장성 있는 서버 아키텍처를 구축하고 성능 개선해보기”라는 주제를 찾아낼 수 있었습니다. 원래 이런 주제가 명확하게 나온 것은 아니었구요.

그래도 어찌어찌 프로젝트는 성공적으로 끝마쳤습니다. 실제로 스트레스 테스트를 돌려보니 캐시를 사용했을 때 더 성능이 잘 나왔고, 뿌듯하기도 했죠. (ㅎㅎ)


Ngrinder의 실제 성능 측정 결과

이 프로젝트의 목표 중 하나가 오픈소스 생태계에 기여하는 것인데, 생태계에 기여하지는 못했어도 같은 수업을 듣는 다른 친구들에게 기여하기도 했습니다. 웹 백엔드/프론트엔드 개발 경험이 없는 친구들을 위해서 기준 프로젝트로 만든 부탁하냥(ASKHY)이라는 웹 애플리케이션을 오픈소스로 배포하고, Docker 이미지로 만들고 Docker Hub에 업로드하며 쉽게 웹앱을 띄워보고 개선할 수 있게 만들기도 했습니다. 그 결과 Star와 Fork 수를 꽤나 받았습니다. (^^;)

askhy_github

결론적으로 이 프로젝트는 오픈소스를 활용하여 다양한 실험을 진행해보며 개선을 위해 어떤 방법을 시도할 수 있는지, 또 무엇이 괜찮은 방법인지를 체크해보는 목적은 만족시킨 것 같습니다. 네이버는 서버 성능을 위해 어떤 시도들을 하는지도 간접적으로 배울 수 있었죠. 한 학기동안 여러모로 고생을 많이 했지만, 많은 삽질만큼 남은것도 꽤 많은 프로젝트였습니다.

그럼 지금까지 한양대학교 컴퓨터공학부의 3학년 2학기 수업인 소프트웨어스튜디오2(ITE3068) 의 과제를 진행하며 학습하고 구현한 내용을 담은 포스트였습니다. 감사합니다.