Elixir 및 OTP의 프로세스 지향 프로그래밍 가이드

게시 됨: 2022-03-11

사람들은 프로그래밍 언어를 패러다임으로 분류하는 것을 좋아합니다. 객체지향(OO) 언어, 명령형 언어, 함수형 언어 등이 있습니다. 이것은 어떤 언어가 유사한 문제를 해결하고 어떤 유형의 문제를 해결하려는 언어인지 파악하는 데 도움이 될 수 있습니다.

각각의 경우에 패러다임은 일반적으로 해당 언어군의 원동력이 되는 하나의 "주요" 초점과 기술을 가지고 있습니다.

  • 객체지향 언어에서 상태(메소드)를 조작하여 상태(데이터)를 캡슐화하는 방법으로 클래스 또는 객체 입니다.

  • 함수형 언어에서는 함수 자체를 조작 하거나 함수 간에 전달되는 변경할 수 없는 데이터 일 수 있습니다.

Elixir(및 그 이전의 Erlang)는 기능적 언어에 공통적인 불변 데이터를 나타내기 때문에 종종 기능적 언어로 분류되지만, 저는 그것들 이 많은 기능적 언어와 별개의 패러다임을 나타낸다고 제출하고 싶습니다. OTP의 존재로 인해 존재하고 채택되고 있으므로 프로세스 지향 언어 로 분류합니다.

이 포스트에서 우리는 이러한 언어를 사용할 때 프로세스 지향 프로그래밍이 무엇인지 의미를 포착하고, 다른 패러다임과의 차이점과 유사점을 탐색하고, 교육 및 채택 모두에 대한 의미를 확인하고, 짧은 프로세스 지향 프로그래밍 예제로 끝낼 것입니다.

프로세스 지향 프로그래밍이란 무엇입니까?

정의부터 시작하겠습니다. 프로세스 지향 프로그래밍 은 원래 1977년 Tony Hoare의 논문에서 나온 Communicating Sequential Processes를 기반으로 하는 패러다임입니다. 이것은 동시성의 행위자 모델이라고도 널리 알려져 있습니다. 이 원본 작업과 관련이 있는 다른 언어에는 Occam, Limbo 및 Go가 있습니다. 공식 문서는 동기식 통신만 다룹니다. 대부분의 액터 모델(OTP 포함)도 비동기식 통신을 사용합니다. 비동기 통신 위에 동기 통신을 구축하는 것은 항상 가능하며 OTP는 두 가지 형식을 모두 지원합니다.

이 역사에서 OTP는 순차 프로세스를 통신하여 내결함성 컴퓨팅을 위한 시스템을 만들었습니다. 내결함성 기능은 감독자 형태의 견고한 오류 복구와 액터 모델에 의해 활성화된 분산 처리 사용과 함께 "실패하도록 놔두는" 접근 방식에서 비롯됩니다. 전자가 수용하기 훨씬 쉽고 OTP에서 후자보다 훨씬 더 신뢰할 수 있는 것으로 입증되었기 때문에 "실패하도록 두십시오"는 "실패하지 않도록 방지"와 대조될 수 있습니다. 그 이유는 장애를 방지하는 데 필요한 프로그래밍 노력(Java 확인 예외 모델에서 볼 수 있듯이)이 훨씬 더 복잡하고 까다롭기 때문입니다.

따라서 프로세스 지향 프로그래밍은 시스템의 프로세스 간의 프로세스 구조와 통신이 주요 관심사인 패러다임 으로 정의할 수 있습니다.

객체 지향 대 프로세스 지향 프로그래밍

객체 지향 프로그래밍에서 데이터와 함수의 정적 구조는 주요 관심사입니다. 동봉된 데이터를 조작하는 데 필요한 메서드는 무엇이며 개체 또는 클래스 간의 연결은 무엇이어야 합니다. 따라서 그림 1에서 볼 수 있듯이 UML의 클래스 다이어그램은 이러한 초점의 대표적인 예입니다.

프로세스 지향 프로그래밍: 샘플 UML 클래스 다이어그램

객체 지향 프로그래밍에 대한 일반적인 비판은 가시적인 제어 흐름이 없다는 것입니다. 시스템은 개별적으로 정의된 많은 수의 클래스/객체로 구성되기 때문에 경험이 적은 사람이 시스템의 제어 흐름을 시각화하는 것이 어려울 수 있습니다. 이것은 추상 인터페이스를 사용하거나 강력한 유형이 없는 상속이 많은 시스템에 특히 해당됩니다. 대부분의 경우 개발자가 많은 양의 시스템 구조를 효과적으로 암기하는 것이 중요해집니다(어떤 클래스에 어떤 메서드가 있고 어떤 방식으로 사용되는지).

객체 지향 개발 접근 방식의 강점은 새로운 객체 유형이 기존 코드의 기대치를 충족하는 한 기존 코드에 제한적인 영향을 미치는 새로운 유형의 객체를 지원하도록 시스템을 확장할 수 있다는 것입니다.

함수형 대 프로세스 지향 프로그래밍

많은 함수형 프로그래밍 언어는 다양한 방식으로 동시성을 해결하지만 주요 초점은 함수 간에 전달되는 변경할 수 없는 데이터 또는 다른 함수(함수를 생성하는 고차 함수)에서 함수 생성입니다. 대부분의 경우 언어의 초점은 여전히 ​​단일 주소 공간 또는 실행 파일이며 이러한 실행 파일 간의 통신은 운영 체제별 방식으로 처리됩니다.

예를 들어, Scala는 Java Virtual Machine에 구축된 기능적 언어입니다. 통신을 위해 Java 기능에 액세스할 수 있지만 언어의 고유한 부분은 아닙니다. Spark 프로그래밍에서 사용되는 공용 언어이지만 다시 언어와 함께 사용되는 라이브러리입니다.

기능적 패러다임의 강점은 최상위 기능이 주어진 시스템의 제어 흐름을 시각화하는 능력입니다. 제어 흐름은 각 함수가 다른 함수를 호출하고 모든 데이터를 한 함수에서 다음 함수로 전달한다는 점에서 명시적입니다. 기능적 패러다임에는 부작용이 없으므로 문제를 더 쉽게 결정할 수 있습니다. 순수 기능 시스템의 문제는 "부작용"이 지속 상태를 유지해야 한다는 것입니다. 잘 설계된 시스템에서 상태 지속은 제어 흐름의 최상위 수준에서 처리되므로 대부분의 시스템에서 부작용이 없습니다.

Elixir/OTP 및 프로세스 지향 프로그래밍

Elixir/Erlang 및 OTP에서 통신 기본 요소는 언어를 실행하는 가상 머신의 일부입니다. 프로세스 간 및 기계 간 통신 기능은 언어 시스템에 내장되어 있으며 핵심입니다. 이것은 이 패러다임과 이러한 언어 시스템에서 의사소통의 중요성을 강조합니다.

Elixir 언어는 언어로 표현된 논리 측면에서 주로 기능적 이지만 프로세스 지향적 입니다.

프로세스 지향적이라는 것은 무엇을 의미합니까?

이 포스트에서 정의한 프로세스 지향적이라는 것은 먼저 어떤 프로세스가 존재하고 어떻게 통신하는지 형태로 시스템을 설계하는 것입니다. 주요 질문 중 하나는 정적 프로세스와 동적 프로세스, 요청에 대한 요구에 따라 생성되는 프로세스, 장기 실행 목적에 봉사하는 프로세스, 공유 상태 또는 시스템 공유 상태의 일부를 보유하는 프로세스, 시스템은 본질적으로 동시적입니다. OO에 객체의 유형이 있고 기능에 함수 유형이 있는 것처럼 프로세스 지향 프로그래밍에는 프로세스 유형이 있습니다.

따라서 프로세스 지향 설계는 문제를 해결하거나 요구 사항을 해결하는 데 필요한 일련의 프로세스 유형을 식별하는 것입니다 .

시간의 측면은 설계 및 요구 사항 노력에 빠르게 들어갑니다. 시스템의 수명 주기는 무엇입니까? 어떤 사용자 정의 요구 사항이 비정기적이며 어떤 것이 지속적입니까? 시스템의 하중은 어디에 있으며 예상 속도와 부피는 얼마입니까? 이러한 유형의 고려 사항을 이해한 후에야 프로세스 지향 설계가 각 프로세스의 기능 또는 실행할 논리를 정의하기 시작합니다.

교육의 의미

교육에 대한 이러한 분류의 의미는 교육이 언어 구문이나 "Hello World" 예제가 아니라 시스템 엔지니어링 사고와 프로세스 할당에 대한 설계 초점으로 시작해야 한다는 것입니다.

코딩 문제는 상위 수준에서 가장 잘 처리되고 수명 주기, QA, DevOps 및 고객 비즈니스 요구 사항에 대한 교차 기능적 사고를 포함하는 프로세스 설계 및 할당에 부차적입니다. Elixir 또는 Erlang의 모든 교육 과정은 OTP를 포함해야 하며(일반적으로 포함합니다) "이제 Elixir에서 코딩할 수 있으므로 동시성을 수행합시다" 유형 접근 방식이 아니라 처음부터 프로세스 지향성을 가져야 합니다.

채택의 의미

채택의 의미는 언어와 시스템이 통신 및/또는 컴퓨팅 배포가 필요한 문제에 더 잘 적용된다는 것입니다. 단일 컴퓨터의 단일 워크로드 문제는 이 분야에서 덜 흥미롭고 다른 언어로 더 잘 해결할 수 있습니다. 수명이 긴 연속 처리 시스템은 처음부터 내결함성이 내장되어 있기 때문에 이 언어의 주요 대상입니다.

문서화 및 디자인 작업의 경우 그래픽 표기법을 사용하는 것이 매우 유용할 수 있습니다(OO 언어의 경우 그림 1과 같이). UML의 Elixir 및 프로세스 지향 프로그래밍에 대한 제안은 프로세스 간의 시간적 관계를 표시하고 요청 서비스에 관련된 프로세스를 식별하는 시퀀스 다이어그램(그림 2의 예)입니다. 라이프 사이클 및 프로세스 구조를 캡처하는 UML 다이어그램 유형은 없지만 프로세스 유형과 그 관계에 대한 간단한 상자 및 화살표 다이어그램으로 표현할 수 있습니다. 예를 들어, 그림 3:

프로세스 지향 프로그래밍 샘플 UML 시퀀스 다이어그램

프로세스 지향 프로그래밍 샘플 프로세스 구조 다이어그램

프로세스 지향의 예

마지막으로 프로세스 방향을 문제에 적용하는 간단한 예를 살펴보겠습니다. 글로벌 선거를 지원하는 시스템을 제공해야 한다고 가정해 보겠습니다. 이 문제는 많은 개별 활동이 버스트로 수행되지만 결과를 실시간으로 집계하거나 요약하는 것이 바람직하고 상당한 부하가 발생할 수 있다는 점에서 선택됩니다.

초기 프로세스 설계 및 할당

우리는 처음에 각 개인이 투표를 하는 것이 많은 개별 입력에서 시스템으로의 트래픽 버스트이고 시간 순서가 없으며 높은 부하를 가질 수 있음을 알 수 있습니다. 이 활동을 지원하기 위해 우리는 이러한 입력을 모두 수집하고 표 작성을 위해 보다 중앙 프로세스로 전달하는 많은 프로세스를 원할 것입니다. 이러한 프로세스는 투표를 생성할 각 국가의 인구 근처에 위치하여 짧은 대기 시간을 제공할 수 있습니다. 그들은 로컬 결과를 유지하고, 입력을 즉시 기록하고, 대역폭과 오버헤드를 줄이기 위해 일괄적으로 표로 전달합니다.

결과가 제시되어야 하는 각 관할 구역의 투표를 추적하는 프로세스가 필요하다는 것을 처음에 알 수 있습니다. 이 예에서 각 국가에 대한 결과를 추적해야 하고 각 국가 내에서 지방/주별로 결과를 추적해야 한다고 가정해 보겠습니다. 이 활동을 지원하기 위해 우리는 계산을 수행하고 현재 합계를 유지하는 국가당 하나 이상의 프로세스와 각 국가의 각 시/도에 대한 또 다른 세트를 원할 것입니다. 이는 국가 및 시/도에 대한 총계에 실시간으로 또는 짧은 대기 시간에 응답할 수 있어야 한다고 가정합니다. 데이터베이스 시스템에서 결과를 얻을 수 있는 경우 임시 프로세스에 의해 합계가 업데이트되는 다른 프로세스 할당을 선택할 수 있습니다. 이러한 계산에 전용 프로세스를 사용하는 이점은 결과가 메모리 속도로 발생하고 짧은 대기 시간으로 얻을 수 있다는 것입니다.

마지막으로 많은 사람들이 결과를 보고 있음을 알 수 있습니다. 이러한 프로세스는 여러 가지 방법으로 분할할 수 있습니다. 우리는 해당 국가의 결과에 책임이 있는 각 국가에 프로세스를 배치하여 부하를 분산하고자 할 수 있습니다. 프로세스는 계산 프로세스의 결과를 캐시하여 계산 프로세스에 대한 쿼리 로드를 줄일 수 있으며, 계산 프로세스는 결과가 상당한 양만큼 변경되거나 계산 프로세스가 유휴 상태가 되어 느린 변화 속도를 나타냅니다.

세 가지 프로세스 유형 모두에서 프로세스를 서로 독립적으로 확장하고 지리적으로 분산할 수 있으며 프로세스 간의 데이터 전송에 대한 능동적 승인을 통해 결과가 손실되지 않도록 할 수 있습니다.

논의한 바와 같이, 우리는 각 프로세스의 비즈니스 로직과 독립적인 프로세스 설계로 예제를 시작했습니다. 비즈니스 논리에 프로세스 할당에 반복적으로 영향을 미칠 수 있는 데이터 집계 또는 지역에 대한 특정 요구 사항이 있는 경우. 지금까지의 프로세스 설계는 그림 4에 나와 있습니다.

프로세스 지향 개발 사례: 초기 프로세스 설계

투표를 수신하기 위해 별도의 프로세스를 사용하면 각 투표가 다른 투표와 독립적으로 수신되고 수신 시 기록되며 다음 프로세스 세트로 일괄 처리되므로 해당 시스템의 부하가 크게 줄어듭니다. 많은 양의 데이터를 소비하는 시스템의 경우 프로세스 계층을 사용하여 데이터 볼륨을 줄이는 것이 일반적이고 유용한 패턴입니다.

격리된 프로세스 집합에서 계산을 수행하여 해당 프로세스의 부하를 관리하고 안정성과 리소스 요구 사항을 보장할 수 있습니다.

결과 프레젠테이션을 격리된 프로세스 집합에 배치하여 시스템의 나머지 부분에 대한 부하를 제어하고 부하에 따라 프로세스 집합을 동적으로 확장할 수 있습니다.

추가 요구 사항

이제 복잡한 요구 사항을 추가해 보겠습니다. 각 관할 구역(국가 또는 주)에서 투표 표를 작성하면 비례 결과, 승자 독식 결과가 나타나거나 해당 관할 구역의 인구에 비해 투표가 충분하지 않은 경우 결과가 없을 수 있다고 가정해 보겠습니다. 각 관할 구역은 이러한 측면을 제어합니다. 이 변경으로 인해 국가의 결과는 원시 투표 결과의 단순한 집계가 아니라 시/도 결과의 집계입니다. 이렇게 하면 시/도 프로세스의 결과가 국가 프로세스에 공급되도록 원래의 프로세스 할당이 변경됩니다. 투표수집과 시/도 및 도-국가 프로세스 간에 사용되는 프로토콜이 동일하면 집계 논리를 재사용할 수 있지만 결과를 보유하는 별개의 프로세스가 필요하고 그림과 같이 통신 경로가 다릅니다. 5.

프로세스 지향 개발 사례: 수정된 프로세스 설계

코드

예제를 완료하기 위해 Elixir OTP에서 예제 구현을 검토합니다. 일을 단순화하기 위해 이 예제에서는 실제 웹 요청을 처리하는 데 Phoenix와 같은 웹 서버가 사용되고 이러한 웹 서비스가 위에서 식별한 프로세스에 요청을 하는 것으로 가정합니다. 이것은 예제를 단순화하고 Elixir/OTP에 초점을 맞추는 이점이 있습니다. 프로덕션 시스템에서 이러한 프로세스를 분리하면 몇 가지 이점이 있을 뿐만 아니라 문제를 분리하고 유연한 배포를 허용하고 로드를 분산하고 대기 시간을 줄일 수 있습니다. 테스트가 포함된 전체 소스 코드는 https://github.com/technomage/voting에서 찾을 수 있습니다. 이 포스트에서는 가독성을 위해 소스를 축약했습니다. 아래의 각 프로세스는 실패 시 프로세스가 다시 시작되도록 OTP 감독 트리에 맞습니다. 예제의 이 측면에 대한 자세한 내용은 소스를 참조하십시오.

투표 녹음기

이 프로세스는 투표를 받아 영구 저장소에 기록하고 결과를 집계기에 일괄 처리합니다. VoteRecoder 모듈은 Task.Supervisor를 사용하여 각 투표를 기록하는 단기 작업을 관리합니다.

 defmodule Voting.VoteRecorder do @moduledoc """ This module receives votes and sends them to the proper aggregator. This module uses supervised tasks to ensure that any failure is recovered from and the vote is not lost. """ @doc """ Start a task to track the submittal of a vote to an aggregator. This is a supervised task to ensure completion. """ def cast_vote where, who do Task.Supervisor.async_nolink(Voting.VoteTaskSupervisor, fn -> Voting.Aggregator.submit_vote where, who end) |> Task.await end end

투표 집계기

이 프로세스는 관할 구역 내에서 투표를 집계하고 해당 관할 구역에 대한 결과를 계산하며 투표 요약을 다음 상위 프로세스(상위 수준 관할 구역 또는 결과 발표자)로 전달합니다.

 defmodule Voting.Aggregator do use GenStage ... @doc """ Submit a single vote to an aggregator """ def submit_vote id, candidate do pid = __MODULE__.via_tuple(id) :ok = GenStage.call pid, {:submit_vote, candidate} end @doc """ Respond to requests """ def handle_call {:submit_vote, candidate}, _from, state do n = state.votes[candidate] || 0 state = %{state | votes: Map.put(state.votes, candidate, n+1)} {:reply, :ok, [%{state.id => state.votes}], state} end @doc """ Handle events from subordinate aggregators """ def handle_events events, _from, state do votes = Enum.reduce events, state.votes, fn e, votes -> Enum.reduce e, votes, fn {k,v}, votes -> Map.put(votes, k, v) # replace any entries for subordinates end end # Any jurisdiction specific policy would go here # Sum the votes by candidate for the published event merged = Enum.reduce votes, %{}, fn {j, jv}, votes -> # Each jourisdiction is summed for each candidate Enum.reduce jv, votes, fn {candidate, tot}, votes -> Logger.debug "@@@@ Votes in #{inspect j} for #{inspect candidate}: #{inspect tot}" n = votes[candidate] || 0 Map.put(votes, candidate, n + tot) end end # Return the published event and the state which retains # Votes by jourisdiction {:noreply, [%{state.id => merged}], %{state | votes: votes}} end end

결과 발표자

이 프로세스는 애그리게이터로부터 투표를 수신하고 결과를 제시하기 위한 서비스 요청에 해당 결과를 캐시합니다.

 defmodule Voting.ResultPresenter do use GenStage … @doc """ Handle requests for results """ def handle_call :get_votes, _from, state do {:reply, {:ok, state.votes}, [], state} end @doc """ Obtain the results from this presenter """ def get_votes id do pid = Voting.ResultPresenter.via_tuple(id) {:ok, votes} = GenStage.call pid, :get_votes votes end @doc """ Receive votes from aggregator """ def handle_events events, _from, state do Logger.debug "@@@@ Presenter received: #{inspect events}" votes = Enum.reduce events, state.votes, fn v, votes -> Enum.reduce v, votes, fn {k,v}, votes -> Map.put(votes, k, v) end end {:noreply, [], %{state | votes: votes}} end end

테이크아웃

이 게시물은 프로세스 지향 언어로서의 Elixir/OTP의 잠재력을 탐구하고 이를 객체 지향 및 기능적 패러다임과 비교하고 이것이 교육 및 채택에 미치는 영향을 검토했습니다.

이 게시물에는 이 방향을 샘플 문제에 적용하는 짧은 예도 포함되어 있습니다. 모든 코드를 검토하려는 경우 GitHub의 예제에 대한 링크가 있습니다. 다시 스크롤하여 코드를 찾을 필요가 없습니다.

핵심은 시스템을 통신 프로세스의 모음으로 보는 것입니다. 먼저 프로세스 설계 관점에서 시스템을 계획하고 두 번째로 논리 코딩 관점에서 시스템을 계획합니다.