elasticsearch 구성 참고사항

elasticsearch 는 워낙 유명하고 안정적으로 성장한 오픈소스라 공식 문서, 커뮤니티, 블로그, 소스코드 등 참고할 자료들이 넘쳐난다. 굳이 그 넘쳐나는 자료에 한 숟가락을 더 얹을 필요가 있겠냐마는, 프로덕션 환경에 elasticsearch 를 처음 도입하려는 사람에게 찾는 수고를 덜 수 있는 best practice 를 제공하는 것이 이 문서의 목적이므로 누군가에게는 도움이 될 것으로 생각한다. elk 스택이나 관리용 플러그인, 키바나 사용법 등은 필자가 잘 모르기도 하고 워낙 좋은 문서가 많기 때문에 다른 문서를 참조하는 편이 낫다. 이 문서는 elasticsearch cluster 를 구성할 때 참고할 만한 사항들을 정리하였다.

ECK(Elastic Cloud on k8s)

ECK는 k8s 에 거부감이 없다면 elasticsearch 운영환경으로 선택할 수 있는 좋은 옵션으로, elasticsearch 개발사인 Elastic 에서 공식 배포하는 k8s operator pattern 이다. k8s 환경에 빠르게 적용할 수 있으며, elasticsearch 특성을 고려한 리소스 / lifecycle 관리가 가능하다.

Operator pattern이란?

k8s에서 기본 제공하는 방식으로 서비스 운영하기엔 도메인 특성이 적합하지 않아 반복적인 설정/변경작업이 잦을 경우 직접 custom resource 를 작성할 수 있는데, 이 custom resource 를 관리하는 확장 인터페이스가 operator 이며, 이렇게 custom resource 와 opertor를 제작하여 k8s 를 사용하는 패턴을 operator pattern 이라 한다.

elasticsearch 적정 메모리

elasticsearch 는 메모리의 크기가 매우 중요하다. 다큐먼트로부터 인덱스를 생성할 때에도 메모리를 사용하고, 검색 속도 향상을 위한 캐시에도 메모리를 사용하기 때문에 elasticsearch 가 빠른 성능을 내기 위해선 메모리 리소스가 넉넉해야 한다. 그럼 그 넉넉한 메모리의 적정 규모는 얼마일까?

jvm compressed oop

대부분 elasticsearch 와 적정 메모리 관련 문서를 찾아보면 jvm heap 에 32Gbytes 이상 할당하지 말라는 내용이 많을 것이다. compressed oop 라는 기능으로 메모리를 효율적으로 쓸 수 있는 최대치가 32Gbytes 이기 때문이다. 절반은 맞고 절반은 틀렸다.

Compressed OOPs in the JVM | Baeldung
compressed oop란 0으로 padding 되어 실질적으로 사용하지 않는 주소 영역 3bit 도 addressing 에 활용하는 기술이다. 2^3 만큼 이득이므로 32bit(=4G) x 8 = 32Gbytes 까지 addressing 이 가능하다.

64bit 주소를 사용하면 메모리 공간 내에서 데이터 저장영역이 그만큼 줄어들기 때문에 효율성 측면에서 heap size는 32Gbytes를 넘지 않는 편이 좋았다. 과거에는 말이다.

zgc

zgc는 java11 부터 사용할 수 있는 신규 gc 이다. 벤치마크를 확인해 보면 성능이 매우 뛰어남을 알 수 있다. 허나 zgc를 사용하려면 compressed oop 를 쓰면 안된다. 0으로 채워지는 padding 영역을 zgc에서 사용하기 때문이다.

64bit 주소체계에서 18bit 는 사용되지 않는다. zgc는 저 사용되지 않는 공간을 활용하여 gc 의 성능을 높였다. 기회가 되면 zgc에 대해 따로 다뤄보겠다.
gc 벤치마크이다. zgc의 성능이 매우 높음을 알 수 있다. 또한 gc 에서 발생하는 latency 가 낮기 때문에 체감 성능은 기존 mark&sweep 이나 g1gc보다 월등히 높을 것으로 보인다.

compressed oop 는 32Gbytes 제약이 있으나 32bit 주소체계이기 때문에 메모리 공간을 효율적으로 사용할 수 있다. 반면 zgc는 64bit 주소체계이기 때문에 메모리 공간 사용률이 효율적이지 않지만 gc 성능이 빠르다. 그럼 둘 중에 어느 것이 효율적일까? 정해진 답은 없으나 heap size 가 32Gbytes 이하라면 compressed oop, 그 이상으로 heap을 크게 써야 한다면 zgc를 사용하는 편이 이득일 것으로 보인다.

lucene mmaped io

elasticsearch에 메모리를 할당할 때 주의할 점이 하나 더 있는데, 서버의 모든 메모리 리소스를 jvm에 할당하면 오히려 성능상 불리해질 수 있다. lucene은 memory mapped io 를 사용하는데, virtual memory 의 원활한 paging 을 위해 시스템 영역에 충분한 메모리를 남겨두어야 한다.

The Generics Policeman Blog: Use Lucene's MMapDirectory on 64bit ...
lucene 의 MMapDirectory 는 virtual memory를 사용하는데, virtual memory 의 성능을 위해서 시스템 영역에 충분히 메모리를 남겨둘 필요가 있다.

Maximum shards in node

elasticsearch 를 default 설정으로 사용하였을 때, index 하나 당 shard 갯수는 5개, node 당 maximum shard 갯수는 1000개이다. 필자는 이걸 모르고 index를 많이 만들었다가 shard 갯수가 1000 개를 초과하여 더이상 index를 생성하지 못하는 문제를 겪었다. node 당 shard 갯수는 설정으로 조절이 가능하므로 shard 를 늘리고 싶다면 클러스터 설정을 바꾸면 된다.

그렇다면 node 하나 당 적절한 shard 갯수는 얼마일까? elasticsearch 공식 블로그에 따르면 적절한 heap 1Gbyte 당 shard 20개가 적절하다고 하니 참고하길 바란다. (https://www.elastic.co/kr/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster)

Hot-Warm architecture

elasticsearch 는 저장 비용이 큰 편이다. 주어진 리소스가 데이터 양에 비해 충분히 넉넉하다면 별 문제가 없겠으나, 리소스를 효율적으로 사용하려면 아키텍처를 적절히 구성하여야 할 것이다. 일반적으로 최근 데이터는 검색을 자주하는 반면 오래된 데이터는 그렇지 않은 경우가 많은데, hot-warm 모델은 index 를 고성능 data node(hot) -> 저성능 data node(warm) -> 삭제 의 순서로 이동시키며 lifecycle을 관리하는 아키텍처이다.

hot-warm_cluster.png
Hot node 는 고성능의 저장장치와 높은 메모리로 구성, 빈번한 읽기/쓰기 작업을 감당할 수 있도록 한다. 최초에 Hot node에서 생성한 index는 일정 시간이 경과하면 Warm node 로 이동시킨다. Warm node 는 고용량의 저가 저장장치로 구성, 참조 빈도가 낮은 데이터를 보관하는 용도로 사용한다.

opendistro ISM

opendistro의 ISM(Index State Management) 기능을 사용하면 Hot-Warm architecture 의 index lifecycle 을 쉽게 관리할 수 있다. opendistro에는 다른 여러 부가기능들이 있으므로 관심있으면 공식 문서를 참조 바란다.

OSX 환경에서 CI/CD 자동화

현 시점에선 이 구성을 사용 안하지만 조사해 둔 내용이 아까워서 osx 환경에서 CI / CD 자동화 관련 참고할만한 문서 링크들을 정리해 둔다.

결론부터…

  • 여러 버전의 ios sdk 관리는 xcode-select / xserver 사용
  • unreal 이나 unity 같이 gpu / 하드웨어 특성을 타는 빌드 환경이 아니라면 vagrant
  • 하드웨어 종속성 탓에 물리적인 빌드환경이 필요하다면 ansible

ios sdk 빌드 관련 문서

원칙적으로 sdk는 하위 호환성을 보장하여야 한다. 허나 현실에서 반드시 최신 버전의 빌드도구가 하위 버전을 지원한다는 보장은 없다. 여러 프로젝트를 하나의 머신에서 빌드해야 하는데 sdk나 서드파티 라이브러리 버전이 서로 배타적일 때, 모든 프로젝트의 빌드환경을 만족하는 구성을 관리하는건 손이 많이 가는 일이다. 특히 ios sdk 의 패키지 / 종속성 관리는 애플에서 제공하는 대로 따르는 수 밖에 없기 때문에 더욱 귀찮다.애플에서 제공하는 문서를 잘 찾아보고 읽어야 한다.

인프라 자동화 관련 문서

osx가 리눅스나 윈도우처럼 docker 환경을 지원한다면 빌드환경을 구성하는게 편했을텐데 아쉽게도 osx 는 docker를 지원하지 않는다. vm 환경 또한 구성하는게 편하지는 않다. 공식적으로 os image 다운로드 링크를 제공해주지도 않을 뿐더러, osx 이외에 다른 host os 에서 osx vm을 실행하는건 불법이기 때문이다. 하지만 어떻게든 vm 환경을 구성할 수 있으며, vagrant 같은 vm 관리도구로 인프라를 자동화할 수 있다.

하지만 unreal 이나 unity 처럼 gpu 하드웨어에 종속적인 개발환경을 병행하여야 한다면 vm으로 빌드 인프라를 구성할 수는 없다. 이런 경우에 쓸 수 있는 좋은 솔루션으로 ansible을 추천한다. ansible galaxy 에는 ios 개발환경을 쉽게 자동화할 수 있는 양질의 레시피들이 제공되므로 참고해 보길 바란다.

겜섭알못의 게임서버 아키텍처 101 #1

서론

별다른 언급 없이 서버라 하면 당연히 웹서버를 떠올릴만큼, 웹기술은 서버-클라이언트 모델의 대세다. 웹 서버 기술은 상당히 많은 연구와 응용이 수십년간 진행되었기에 표준, 프레임워크, 아키텍처 등이 정형화되었고, 따라서 서버 개발자는 거의 도메인 모델 구현만 신경쓰면 되는 수준에 이르렀다. 허나 게임 서버는 웹과는 많은 것들이 다르다. 게임 장르와 대상, 목적에 따라 기술이나 아키텍처가 달라지기 때문에 정형화할 만한 영역이 많지 않다. 또한 웹과는 다르게 게임 진행중 서버-클라이언트는 연결성을 유지하고 있어야 한다. 이러한 요구사항, 기술적 차이 외에도 규모의 차이가 있다. 시장 규모, 매출을 기준으로 하면 게임 서버 시장이 결코 작지 않으나 개발자 인력 풀 관점에서 게임 개발자는 다른 도메인에 비해 소수다. 웹서버 개발 10년차 베테랑이라 할지라도 멀티쓰레드 환경에서 발생하는 다양한 동시성 문제나 소켓 통신에 따른 네크워크 문제, 위상 변화에 따른 위치문제 등을 현업에서 겪었을 가능성은 높지 않다. 때문에 게임 업계는 다른 업계 종사자가 전직하기엔 진입장벽이 높은 고인물들의 닫힌 시장인 경향이 있다.

본인의 커리어에 게임서버 백엔드 경험이 없기 때문에 이쪽 분야에 대한 기술적 특징이나 아키텍처를 짚고 넘어가야 할 필요를 느낀다. 다행스럽게도(?) 본인의 커리어가 워낙 잡스러웠던 탓에 다른 사람들 보다 게임 서버 기술을 이해하는데 조금은 낫지 않나 싶다. 이 글에선 게임 서버 기술 및 아키텍처의 일반론을 서술하고, 필요한 경우 웹 기술과 비교하면서 그 특징의 이해도를 높이도록 하겠다. 본인도 겜섭알못이기 때문에 글에 깊이가 없을 수 있음은 감안해 달라. 게임 개발에 흥미가 있는 주니어나 전업을 고민하는 개발자에게 도움이 되었으면 좋겠다.

Stateless vs Stateful

일반적으로 웹 컨텐츠는 모두에게 동일한 결과를 제공해준다.

웹은 기본적으로 stateless 하다.  100명의 사용자에게 하나의 동일한 페이지를 제공하는 것이다. 반면 게임은 기본적으로 이전의 상태를 저장하고 있어야 한다. 같은 아이템을 사용해도 사용자 레벨이나 능력치에 따라 다르게 적용되는 경우도 있고, PvP 상황에선 누가 먼저 공격을 했는지에 따라 결과가 달라질 수 있다.

물론 현재의 웹은 로그인도 필요하고 장바구니도 필요하고 결제, 배송 내역을 확인할 필요도 있기 때문에 이전의 상태값을 저장해야 한다. 비즈니스 모델의 요구사항이 사용자 개개인의 상태를 저장하도록 발전해왔기 때문에 웹에도 다양한 방법으로 이전 상태를 저장하는 기술이 추가되었다.

허나 어떤 특정 상황에서 웹기술로 이전 상태를 저장하는 데에는 근본적인 한계가 있다. 1) 아주 짧은 시간 동안, 2) 상태가 계속 바뀌는데, 3) 상태변화의 순서가 나와 타인에게 영향을 주는, mmorpg에선 일반적으로 발생하는 그런 상황은 웹기술로 처리하기가 곤란하다. 웹프로토콜이 http이기 때문이다.

가장 흔한 http 클라이언트인 웹브라우저로 예를 들어보자. 웹브라우저는 사용자가 브라우저를 사용하는 대부분의 시간동안 서버와의 연결이 끊어져 있다. 어떤 페이지에 접근을 하기 위해 요청을 하면 그때 서버와 연결을 하고, 응답을 받으면 바로 서버와 연결을 끊는다. 간단하게 원하는 리소스를 가져올 수 있는 장점이 있지만 리소스를 가져올 때 마다 매번 새로 연결 / 연결 해제하므로 아무리 응답이 빨라도 연결 수립을 위한 시간이 소요된다. 요즘 흔한 웹기반 모바일 게임 장르에 fps 나 mmorpg가 없거나, PvP 관련 컨텐츠가 존재하지 않는 이유가 웹기술의 이러한 한계 때문이다.

Realtime event

반면 게임서버는 빠르게 요청되는 유저 간의 이벤트를 순서대로 처리하기 위해 클라이언트/서버 간에 연결을 유지한다.  위치의 이동, 스킬 또는 아이템의 사용, 공격과 회피에 따른 상태 변경 등을 게임월드에 요청하면 게임서버는 이를 순서대로 적용하여 결과를 사용자에게 돌려주는데 이때 소요되는 시간은 게임 진행에 무리가 없도록 충분히 짧아야 한다.

Key pain points of web application development
웹페이지 컨텐츠를 기다려 주는 시간은 일반적으로 3초 정도라고 한다.

일반적으로 웹 컨텐츠의 응답시간은 3초 이내로 상정한다. 반면 게임 서버는 100ms 이내에 응답이 와야 원활한 플레이가 가능하다. 네트워크에서 가장 코스트가 큰 작업이 연결이므로 연결 비용을 줄이려면 게임 클라이언트/서버는 연결을 유지하여야 한다.

게임에서 요구되는 빠른 응답성과 연결성, 이 두 가지 특징은 서버 운영 및 아키텍처에 큰 도전 과제다. 후술할 게임 서버의 다른 특징들은 상기 두 가지 특징으로부터 파생되었다 하여도 과언이 아니다.

Logical / physical topology – Load balancing

다수의 사용자를 처리하기 위한 서버 운영 기술 중 여러 개의 서버를 로드밸런싱하는 방법이 있는데, 게임 서버에서는 이런 구성을 하기 전에 한 번 더 고민하여야 한다. 사용자 간에 데이터 교환이 일어나기 때문이다. 쿠팡이나 11번가 같은 쇼핑몰 사용자들은 서로 통신할 일이 없기 때문에 다른 사용자가 어느 서버에 접속하던 전혀 상관할 필요가 없다. 반면 게임 서버는 사용자 간 논리적 거리가 가까우면 물리적으로도 가까워야 한다. 최소한 서버간 데이터 교환에 걸리는 시간이 응답성에 영향을 주지 않을 정도로 짧아야 한다. 때문에 게임서버는 같이 게임을 진행하는 유저들을 물리적으로도 같은 서버에 위치시키도록 유도한다.

오버워치 서버 탐색기 A to Z : 이런 것도 가능해?! : 네이버 포스트
어떤 게임은 함께 플레이하는 유저들을 물리적으로 같은 서버에 위치시키도록 대기실에 입장하는 단계를 거치기도 한다.

이러한 토폴로지적 특징(특정 유저군을 물리적/논리적으로 동일한 위치에 모아야만 하는) 때문에 게임 서버는 로드밸런싱이 어렵다. 웹 서버의 로드밸런서 – 백엔드 구성을 떠올려보자. 웹의 특징 – connectionless / stateless – 에 따라 모든 서버는 독립적이다. 사용자는 어느 서버에 접속하여도 동일한 리소스를 제공받는다. 따라서 로드밸런서는 부하 분산의 역할에 충실하여 접속하는 사용자들을 골고루 분산시키기만 하면 된다. 허나 게임서버의 부하분산은 기술적 도전과제다.

Long connection – Scalability

컴퓨터 네트워크에서 가장 코스트가 높은 작업은 커넥션이다. 바꿔말하면 최초로 커넥션을 생성할 때 걸리는 시간이 가장 길다는 뜻이기도 하다. 물론 최근의 웹소켓이나 스트리밍 컨텐츠 같은 예외들이 있지만, 일반적으로 웹은 요청 시 마다 새로 연결을 생성하고 응답을 받으면 연결을 종료한다. 이러한 특성 때문에 웹서비스는 서버의 사용량에 맞추어 수평적 스케일링을 하기 용이하다. 모든 요청 시 마다 새로 커넥션을 생성하는 구조이기 때문에 기존 유저가 신규 서버에 접속하여 리소스를 요청하여도 문제가 없다.

반면 게임 서버 같은 경우는 앞서 말한 빠른 응답성을 보장해야 하기 때문에 커넥션을 매번 새로 맺는 비용을 감당할 수 없다. 따라서 게임 서버는 일반적으로 커넥션을 유지한 상태로 데이터 교환을 한다. 이러한 연결성을 유지해야 하는 제약조건 때문에 게임 서버는 스케일링이 웹서비스처럼 용이하지가 않다. 높은 연결성과 관련한 문제들은 물리적인 한계이기 때문에 게임 디자인 / 서버 아키텍처를 잘 구성하여 게임 서비스 중에도 스케일링을 하기 좋은 형태로 처음부터 설계하여야 한다.

게임의 성격에 따라 서버-클라이언트 네트워킹 외에 서버-서버 간 통신이 이루어져야 하는 경우도 많다. 다수의 월드 서버로 구성된 MMORPG 게임이 있다고 가정하자. 월드 내에서 일어나는 이벤트 중에선 모든 서버에 공유되어야만 하는 사건들도 존재한다. 이러한 니즈 때문에 게임 서버들은 각각의 게임 서버들 끼리 거미줄처럼 서로 통신하게 된다.

game_server_arch
좌측의 웹서버는 서버간 연결이 불필요한 반면, 우측의 게임서버는 거미줄처럼 n대n 통신을 하여야 한다. – 출처 NetEase/pomelo

하지만 이러한 n대n 연결 구조에서 큰 고민 없이 아키텍처를 구성하면 가용성 향상을 위해 서버를 늘렸을 때 오히려 커뮤니케이션 비용이 올라가버린다. 서버를 추가하였으나 도리어 성능이 저하되는 원치 않는 결과를 초래하기 때문에 주의하여야 한다.

다음 글에선 일반적인 게임 아키텍처 패턴, 그리고 게임서버와 관련된 기술들을 열거해 보도록 하겠다.