Please enable JavaScript to view the comments powered by Disqus.왜 Elixir를 공부해야 할까? Erlang/OTP BEAM으로 살펴보는 엘릭서
Search

왜 Elixir를 공부해야 할까? Erlang/OTP BEAM으로 살펴보는 엘릭서

태그
Elixir
Erlang
OTP
BEAM
공개여부
작성일자
2024/06/09
이전 포스팅에서 Gleam을 다뤘었다. 하지만 2024년 4월에 만들어진 gleam을 공부하다 보니 github issue 에서 발견되는 버그들을 경험하기도 했고 아직 성숙해지는데 시간이 필요하다 생각되어 Elixir를 공부하게 되었다.
이 Elixir를 이야기 하려면 먼저 Erlang 에 대해서 이야기 해야 한다.

Erlang 이란?

Erlang은 서비스가 매우 작거나 아예 down time이 없게 만들 수 있는 개발 플랫폼이다.
Erlang은 통신 회사에서 자사 서비스를 위해 만들어진 언어로 reliability, responsive, scalability, availability 와 같은 특징을 언어 수준에서 제공하낟.
이러한 특징은 비기능 요구사항으로 개발자들에게 반복적으로 요구되어 왔다.
Erlang이 각광 받는 이유는 Whatsapp, Discord, RabbitMQ와 같은 이미 익숙한 곳에서 충분히 사용되고 있다.

고가용성 High availability

Erlang은 예상치 못한 버그에도 고가용상을 제공하기 위해 다음의 특징을 갖는다.
Fault-tolerance
시스템은 예상치 못한 일이 발생하더라도 계속 실행되어야 한다.
예상치 못한 에러를 최대한 격리하고 회복한 후 서비스를 제공해야 한다.
Scalability
시스템은 어떠한 부하(load)에도 견뎌야 한다.
세상의 모든 사람이 내가 만든 시스템에 아침마다 접속한다 했을 때 장비를 추가하고 S/W 적인 변화 없이 빠르게 추가하는대로 부하를 견딜 수 있어야 한다.
Distribution
여러 기계에서 어플리케이션이 동작한다 하더라도 시스템은 절대로 멈추면 안된다. (horizontal)
만약 기계가 멈춘다면 다른 기계가 이 부하를 감당할 수 있어야 한다.
Responsiveness
항상 빠르게 응답해야 한다.
특히 때때로 매우 긴 태스크가 있어서 나머지 다른 process를 block 하도록 해선 안된다.
Live update
새로운 버전의 S/W 를 업그레이드 할 때 재시작을 하지 않아야 한다.
전화 서비스는 전화 통화 도중에 끊어지면 안된다.
Erlang은 이러한 도전을 concurrency tools에서 찾았다.

Erlang의 동시성 (Concurrency)

Erlang의 심장과도 같은 동시성은 high concurrency를 제공하기 때문인데 이는 Erlang process(OS의 process, thread가 아니다)에서 지원하는 기능 때문이다.
Elixir in action
Erlang은 가상 머신 BEAM에서 실행되는데 이 프로세스가 수천~수백만개가 실행되며 여기에 특화된 scheduler가 process에 task를 할당한다. 이 스케쥴러 덕분에 multi-cord 환경답게 CPU를 사용학 수 있게 만들어준다.
이러한 특징이 위에서 언급한 항목에 대해 기술적으로 대응한다.

Fault-tolerance

Erlang 프로세스는 다른 프로세스와 완전하게 격리되어 있다.
프로세스는 memory를 공유하지 않고, 하나의 프로세스로 인해 다른 프로세스에 crash가 발생하지 않는다.
예상치 못한 에러가 격리되도록 한다.
Erlang은 이러한 crash를 찾아내는 기능과 그와 관련된 기능을 제공한다.
새로운 process를 실행하여 crashed를 대체하면 된다.

Scalability

Memory를 공유하지 않기 때문에 비동기 메시지로 프로세스간 통신을 한다.
이 뜻은 복잡한 동기화 과정인 locks, mutexes, semaphores가 없다는 뜻이다.
연속적으로 시분할된 entity가 상호작용하여 이 모든 과정을 더 단순하게 만들어준다.
가상머신은 프로세스의 실행을 가능한 한 많이 병렬화할 수 있다.
이 점이 Erlang 시스템이 사용 가능한 모든 CPU 코어를 최대한 활용할 수 있기 때문에 확장 가능하게 구성할 수 있다.

Distribution

같은 BEAM 안에 있던, 원격 서버의 다른 BEAM안에 있건 상관없이 두 프로세스의 통신 방법은 동일하다.
일반적인 고 동시성의 Erlang 기반의 시스템은 두 개 이상의 기계에서 서비스 될 준비가 되어있다.
이 뜻은 자연스럽게 여러 기계에서 부하를 공동으로 감당할 준비가 된다는 뜻이며 하나의 기계가 재난으로 고장난다 하더라도 다른 기계에서 이를 대신할 수 있다.

Responsiveness

응답성은 하나의 프로세스가 오래 실행되어 다른 프로세스를 block 하는 일이 없도록 하는것을 의미한다.
BEAM은 전반적인 시스템의 응답 속도를 낮추기 위해 조정되어 있다.
BEAM에 특화된 scheduler를 사용하기 때문인데 이 스케쥴러는 선점적으로 작은 실행 윈도우를 각각의 process에 할당하고 일시정지 하면서 다른 프로세스를 실행한다.
Execution windows가 작기 때문에 실행 시간이 긴 process가 오랫동안 자원을 소비하여 다른 프로세스가 차단되는 것을 막는다.
그리고 GC 역시 responsiveness를 개선하도록 설계되어 있다.
Process가 메모리를 공유하지 않는다.
이는 GC로 인해 JVM과 같이 STW가 발생하는 것이 아니라 process 각각에 GC가 발생하기 때문에 시스템이 멈추지 않음을 의미한다.
멀티 코어 시스템에서 CPU는 짧은 GC를 실행하고 나머지 core들은 자신의 일을 할 수 있다.
이러한 특징이 Erlang에서 수천~수 백만의 process를 사용해 동시성을 갖추게 되고 server-side 개발에 알맞으며 Erlang으로 충분히 구현할 수 있다.

Server side 에선 왜 Erlang이 알맞을까?

우리가 만드는 RESTful API에서 가장 중요한 것은 stateless 이다. HTTP method, URI 가 갖는 그 의미보다도 먼저 언급되어야 하는것이 stateless 이다.
상태가 없기 때문에 얼마든지 scale-out 이 가능하고 이 서비스가 여러 client의 요청을 감당한다.
Elixir in action
그런데 Erlang의 강점은 이것이 전부가 아니다.
1.
Nginx의 web server 역할을 수행할 수 있다.
2.
프로세스간 데이터 전송을 위한 redis 용도의 캐시 기능이 존재한다.
3.
Background job또한 Erlang으로 대체할 수 있다.
물론, 생태계에 존재하는 redis, air flow 등을 직접 사용하는 것도 가능하다.

Elixir는 무엇인가?

Elixir는 Erlang의 가상 머신 BEAM에서 동작하는 대체언어로 더 간단하고 개발자의 의도를 드러내기 적합하다.
마치 JVM에서 java 뿐 아니라 kotlin이 실행될 수 있는 것과 같다.
Elixir는 Erlang의 runtime을 목표로 만들어진 오픈소스로 Elixir 코드가 BEAM에서 동작하는 bytecode로 컴파일 되고 Erlang 코드와 함께 사용할 수 있다.
이 두 코드의 간결함은 굳이 설명할 필요를 느끼지 못한다. 왜냐하면 Erlang의 괴랄한 코드는 이미 알고 있을 것이다.

Elixir의 특징

Elixir엔 |> (파이프라인) 연산자가 존재한다.
파이프라인 연산자가 하는 일은 앞의 return(output)을 다음 함수의 첫 번째 argument에 할당하는 것이다.
def monthly_rate(hourly_rate, discount) do rate = apply_discount(daily_rate(hourly_rate) * 22, discount) rate = Float.ceil(rate) rate = trunc(rate) end
Elixir
복사
여기서 보면 rate 라는 변수가 여러 함수를 거치면서 할당되는 값이 바뀐다.
def monthly_rate(hourly_rate, discount) do trunc( Float.ceil( apply_discount( daily_rate(hourly_rate) * 22, discount ) ) ) end
Elixir
복사
Staircase 로 만든 코드 스타일
임시 변수를 없애기 위해 위와 같이 만들 수 있으며, |> 를 사용한다면
def monthly_rate(hourly_rate, discount) do hourly_rate |> daily_rate |> Kernel.*(22) |> apply_discount(discount) |> Float.ceil() |> trunc end
Elixir
복사
더 간단하게 표현할 수 있다.
물론 실제로 compile time에 |> 는 staircase 로 코드가 치환된다.

Server Side 의 숙명

Out of memory Error

필자는 Spring, Java를 오랜기간 사용해왔다.
가장 고통스러운 문제는 OutOfMemory 가 발생하여 어플리케이션 자체가 죽어버리는 일이다.
그래서 이 블로그엔 Heap dump를 분석하는 글 까지 작성하였다.
언젠가 다시 heap dump를 분석할 때 매뉴얼을 만들어두고 싶었기 때문이다.
하지만, elixir 에선 문제가 생긴 process 하나만 죽고 나머지는 그대로 동작한다. 하나의 문제가 커져서 다른 요청에 영향을 주는 일이 없는 것이다.(responsiveness)

확장성, 가용성

새로운 코드를 배포한다고 해서 기존의 코드를 shutdown 할 필요가 없다.
임의의 새로운 요청은 새로운 process가 생성하고, 요청이 종료되어 응답을 반환한다면, process가 끝난다.
새로운 기계에 코드를 실행시키면 되고, 버전이 바뀐다 하더라도 기존의 요청을 끝낼 필요도 없다.(Graceful shutdown 으로 draining 되는 것을 기다릴 필요가 없다.)
이러한 특징으로 봤을때 왜 지금까지 java, spring이 우리의 삶 속에 있는지 의아할 때가 많다.
물론 silver bullet 은 없기 때문에 elixir에도 단점이 존재하지만, 기존에 나의 야근과 내 무지의 좌절들이 elixir를 만나면서 탈출구를 찾은 것 같다.