병렬화, 병렬화, 더 많은 병렬화

Junhan Kim
NodeONE
Published in
10 min readJan 10, 2023

ENF 의 수석 엔지니어인 버키 키팅어(Bucky Kittinger)가 작년에 작성했던 프로젝트 아이디어 노트를 하나 더 소개해 드리려 합니다. Antelope Leap(구 Mandel) 소프트웨어에 현존하는 문제점과 더불어 Antelope 기반 블록체인의 확장성을 해결할 수 있는 방법에 대하여 그가 어떠한 생각을 하고 있는지 엿볼 수 있습니다.

enf-project-ideas/project.md at main · eosnetworkfoundation/enf-project-ideas (github.com)

잘 정리된 정식 아티클이 아닌 아이디어 노트 정도의 느낌이기 때문에 의미가 불명확한 부분들이 다소 존재하고 상황이 달라짐에 따라 임의로 의역한 부분도 존재합니다. 혹 심각한 오역이나 오타가 있다면 노드원 텔레그램 채널에 제보해 주시면 가능한 빨리 확인후 조치하도록 하겠습니다.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

왜 병렬화가 필요할까?

현재, Antelope Leap 는 고성능 클럭 스피드의 싱글 스레드 프로세서를 타겟으로 동작하도록 설계되었습니다. 이는 시대의 흐름에 엇나간 방법으로, 현존 CPU 제조사들이 지향하는 방향은 아닙니다. 제조사들은 상대적으로 클럭 속도가 낮지만 더 많은 하드웨어 코어/스레드를 도입하는 방법을 택했습니다.

이 때문에 Antelope Leap 는 기본 시스템의 활용률이 떨어지게 되었고 동시에 상당히 고가의 하드웨어를 사용해야 하는 문제에 봉착했습니다. 때문에 새로 누군가가 참여하기에는 비용이 너무 많이 들고 확장성도 낮아 어느 시점이 되면 현재 하드웨어가 한계에 도달하게 되는 등 여러 가지 문제가 발생하게 될 것입니다.

개인적으로 확장성은 스마트 컨트랙트 평가 수준과 노드 수준의 병렬화에서 부터 시작된다고 생각합니다. 확장성을 해결하기 위해 IBC(Inter-Blockchain Communication) 솔루션을 사용하거나 체인을 분할하자는 제안이 있었지만 이는 문제를 해결하기 보다는 더 많은 문제를 불러들여올 수도 있습니다.

일반적으로 IBC 와 같은 모델이 강력하게 지지받고 있는 것은 이더리움을 위시한 다른 체인들이 확장성을 이루기 위한 방법으로 도입하고 있기 때문이라 생각합니다. 문제는 이더리움은 Antelope 이 아니며 본질적으로 완전히 다르다는 것입니다. 이더리움은 사용할 수만 있다면 가능한 모든 하드웨어에서 실행되도록 설계 되었으며, 또한 합의 메커니즘 때문에 실질적으로 네트워크에서 성능 좋은 하드웨어가 절대 다수가 되도록 최적화할 수 없습니다. 환경이 크게 다르기 때문에 노드 레벨에서 확장성을 최적화할 수 있다는 것은 전혀 다른 문제입니다.

IBC를 사용하면, 단일 상태(mono-state)체인을 분할할 때 하위 체인 전체에 데이터가 복제되지 않게 하는 않는 방법, 두 체인 간에 동기화하는 방법, 암호화 방식으로 처리하는 방법 등등에 대해 고민해야 합니다. 이는 비용이 많이 들고 구현하기 어렵다는 것을 의미합니다. 블록체인 간 커뮤니케이션과 메시지 전달을 위한 솔루션으로서의 IBC가 절실히 필요하지만, 확장성에 대한 솔루션으로서는 다소 적합하지 않은 방법이라 생각합니다.

비동기 DB(Asynchronous DB)

병렬 처리를 위해 가장 먼저 고려해야 하는 분야는 다중인덱스(multi-index) 를 지원하는 기반 데이터베이스이며, 향후 지원될 데이터베이스 백엔드는 현재와 동일한 인터페이스를 쓸 수 있어야 할 것입니다.

현재 기반 데이터베이스인 체인베이스는 싱글 스레드 솔루션입니다. 이는 어떠한 데이터를 조회하거나 DB에 데이터를 쓰는 작업을 완료하기 전까지는 의미있는 연산 작업을 수행할 수 없어 결과적으로 시간을 낭비하기 때문에 이상적인 솔루션과는 거리가 매우 멉니다.

첫 번째는 비동기식 쓰기(Asynchronous Writes)로, 이를 도입하면 스마트 컨트랙트 의 중요한 실행 경로를 방해하지 않을 수 있습니다.

쓰기 출력을 버퍼링하기 위해 쓰기 대기열(슈퍼스칼라 아키텍처의 스토어 대기열과 유사)을 사용합니다.

비동기식 쓰기 작업은 반드시 트랜잭션을 완료하기 전에 수행할 필요가 없으므로 적용할 시간이 충분합니다.

다음은 비동기식 읽기(Asynchronous Reads)입니다. 나중에 비동기식 액션 및 추측성 병렬 처리에서의 동기화에 대해 논의할 것입니다.

읽기를 비동기식으로 만들면 다음과 같은 작업으로 성능을 높일 수 있습니다:

  • 미리 가져오기(Perfetch) 반복, 즉, 증가분(Increment)을 얻으면 다음 데이터를 가져오고, 감소분(decrement)을 얻으면 이전 데이터를 가져옵니다.
  • CDT/ANTLER에서는 함수를 호출하고 그 결과를 사용하기 전에, 중요한 작업을 수행할 수 있도록 최적화 가능한 충분한 데이터가 있다면, 즉시 비동기식 읽기가 발생하도록 일정을 조정할 수 있습니다. 이는 ILP(Instruction Level Parallelism, 명령어 수준 병렬성) 수준에서 병렬로 평가할 수 있기 때문에 컴파일러가 로드 명령을 다시 예약하는 것과 유사합니다.
  • 클러스터 읽기(Cluster Reads), 즉 하나를 읽은 다음에 다량의 읽기 프로세스를 시작합니다. 첫 번째 읽기 결과가 완료될 때 쯤 혹은 그 직후에 나머지 결과도 나올 가능성이 높습니다.

초기 호출시 데이터가 불분명하게 처리될 수 있습니다. 별도의 호스트 함수를 사용하여 결과를 얻는 방법도 있습니다.

대략적인 예제는 다음과 같습니다.

int64_t get_row(…);

int64_t get_data_size(int64_t handle);

int get_data(int64_t handle, char* buff, uint32_t size);

위 코드에서 get_row는 비동기식 읽기를 설정합니다. get_data_size는 데이터가 준비되었는지 확인하고 준비되지 않았다면 처리를 중단하고 오류를 나타내는 음수 값 또는 데이터 크기를 반환합니다. 마지막으로 get_data는 데이터를 검색하고 가능한 오류 코드를 반환합니다.

비동기 액션

ANTLER에서 언급했듯이 이것들은 꽤나 유용할 수 있습니다.

비동기 액션을 지원하는 API는 위의 비동기 데이터베이스 API와 같은 종류입니다.

첫 번째 비동기 액션 버전의 경우 읽기 전용(read-only) 액션 또는 순수(Pure) 액션만을 지원할 것입니다. 따라서 시스템 상태(System State)에 대한 부작용은 없습니다.

순수 액션만 비동기식 실행에 사용할 수 있도록 하면 액션 간에 반드시 동기화 해야 할 필요성을 줄일 수 있습니다.

나중에 비순수(Non-pure) 액션도 지원할 수 있습니다.

위의 예제와 유사한 API를 사용합니다:

int64_t async_send(…);

int64_t get_action_result_size(int64_t handle);

int get_action_result(int64_t handle, char* buff, uint32_t size);

다른 액션을 호출할 때 발생하는 성능 문제를 해결하기 위해 각 실행 스레드에 대해 선형 메모리에 대한 버퍼를 이중으로 구현합니다. 이렇게 하면 스레드에서 실행되는 각 액션이 즉시 메모리에 로드될 수 있게 되며 덕분에 먼저 상태를 정리하고 메모리 보호를 재설정할 필요가 없습니다.

다음은 추가적인 호스트 함수입니다.

void prefetch_contract(name contract, bool discard);

void discard_contract(name contract);

이렇게 하면 실행될 컨트랙트를 스레드에 미리 가져올 수 있으며 폐기(discard) 플래그로 스레드가 해당 컨트랙트의 상태를 종료할지 여부를 지정합니다.

컴파일러가 Prefetch 된 컨트랙트가 너무 많아 문제를 일으키지 않도록 비용을 최소화하고 실행 흐름에서 가장 효율적인 지점에 이러한 Prefetch 를 추가하기 때문에 실제로 직접 사용해서는 안 됩니다.

추측성 병렬화(Speculative Parallelism)

정기적으로 수행되는 액션을 병렬화할 수 있게 되면 Antelope 은 CPU 비용을 N배 절감할 수 있으며, 기존 서버급 하드웨어 시스템 활용도를 높일 수 있습니다.

이러한 유형의 병렬 처리를 통해 얻을 수 있는 주요 이점은, 액션을 병렬로 실행하고 데이터 해저드(Data Hazard)가 발생할 경우 액션의 실행을 중지하고 해당 데이터를 “소유”하고 있는 스레드에 맞게 다시 스케줄링 하는 것입니다.

쓰기 대상 테이블 행의 맵은 다음과 같이 소유 스레드 값과 함께 저장됩니다.

[키(Key): 테이블 행 수정 값(Value): 소유하는 스레드 값]

실행 중에 DB 테이블 행을 읽을 때 누가 소유하고 있는지 모니터를 확인하고, 만약 누군가 소유하고 있다면 실행을 중지하고 데이터를 소유하는 스레드로 다시 예약합니다.

실행 중에 DB 테이블 행이 기록되면 모니터에서 해당 테이블 행의 소유자가 있는지 확인하고, 소유하지 않은 경우 소유권과 함께 데이터를 기록합니다. 소유자 있으면 실행을 중지하고 데이터를 소유하는 스레드로 다시 예약합니다.

이렇게 하면 세밀한 동기화 및 트랜잭션 리플레이를 위해 인코딩하는 방법에 대해 걱정할 필요가 없습니다.

다양한 액션을 수행하는 트랜잭션에게 이 솔루션이 가장 효과가 좋을 것입니다.

다만, 단일 상태(Mono-State) 스마트 컨트랙트는 이 방법으로 얻는 혜택이 별로 없을 것입니다. 즉, 단순한 카운터, 싱글톤 등이 강제로 직렬화 될 것입니다. 이에 대한 솔루션으로는 CDT와 같은 항목에서 모범 사례 패턴으로 추상화 할 수 있는 상태 분할 패턴(state fracture pattern)이 있습니다.

트랜잭션 리플레이를 최대한 활용하기 위해 실행 스케줄링 트리를 각 블록에 커밋합니다.

트랜잭션 생성 시점에 종속성이 발견되면 액션의 순서를 정의하는 방법도 있습니다.

분산 노드(Distributed Nodes)

위의 아이디어를 기반으로 함으로써 결국 노드 수준에서의 병렬화를 이룰 수 있을 것입니다. 이를 통해 크기가 방대한 데이터베이스를 확장할 때 발생할 수 있는 많은 우려를 해소할 수 있습니다.

데이터베이스 상태를 세분화함으로써 각 노드에서 요구되는 RAM 용량의 압박을 낮출 수 있으며 트랜잭션의 개념에 대해 동일한 “투기적” 모델을 사용할 수 있습니다.

이렇게 하면 맵은 동기화된 맵으로서 모든 노드로 확장되며, 소유권이 로컬 에 있는지 외부에 있는지를 보여주는 목록을 보여줄 수 있습니다.

또한 “Lead” 노드 같은 새로운 노드 모델이 필요합니다. 이는 입력 트랜잭션을 가져와 대기열에 저장한 다음 분산 노드로 리디렉션하는, 상태가 없는(Stateless) 노드입니다.

분산 노드는 이러한 트랜잭션을 가져가서 추측성 실행(Speculative Execution)을 수행하고 블록 구성을 위한 트랜잭션 정보를 반환합니다.

비동기 DB를 가지고 있기 때문에 가능한 한 읽기 성능을 최적화할 수 있는, 동일한 API와 개념을 활용할 수 있습니다.

트랜잭션의 비동기 액션이 DB 소유권 때문에 실패하면 액션을 포함은 해당 트랜잭션은 실패하고 소유한 노드로 다시 예약됩니다.

트랜잭션 리플레이 성능을 위해 트랜잭션 스케줄은 블록에 커밋됩니다.

추측성 병렬화 및 분산 노드 최적화

스마트 컨트랙트를 구축할 때 “상징 실행 슬라이스(symbolic execution slice)”를 생성하여 일종의 확률적 분석을 수행할 수 있습니다. 그 다음 트랜잭션이 대기열에 들어가면 이를 활용할 수 있습니다.

이 확률 함수를 통해 값을 걸러냄으로써, 초기에 예약할 스레드 또는 노드에 대한 더 나은 선호도를 얻고, 발생 가능한 데이터 해저드를 찾아볼 수 있습니다. 완벽할 필요는 없고, 답이 틀리면 그저 스마트 컨트랙트 실행에 영향을 끼칠 뿐 노드나 블록체인 네트워크에는 문제가 발생하지 않습니다.

--

--