Exclamate EOS! : RAM

Jeeyong Um
GameXCoin
Published in
9 min readOct 15, 2018

# commit-id: 1e9ca55cc

본 시리즈는 개발자를 대상으로 하고 있으므로 친절하지 않을 수 있습니다.
소스 코드는 변경될 수 있으므로 매 글 상단에 분석 시점 기준 마지막 commit-id를 기록합니다.
이 글은 스마트 컨트랙트에서 멀티 인덱스 테이블 eosio::multi_index 의 사용법을 알고 있는 독자를 전제로 작성하였습니다.

EOS 사용에 있어 RAM은 필수적인 자원이다. 당장 계정 생성부터 RAM이 필요하며 스마트 컨트랙트 배포와 컨트랙트 내 데이터 저장에도 RAM을 사용한다. 그런데 RAM 사용량이 단순 예측치보다 훨씬 큰 경우가 많아 DApp 개발자들이 사전에 확보해야 하는 RAM 크기를 가늠하기 어려운 면이 있다. 이번 글에서는 실제 RAM 사용량을 어떤 방식으로 계산하는지 분석한다.

멀티 인덱스 테이블과 RAM

네이티브 액션에서 고정적으로 소모하는 양을 제외하면 대부분의 RAM은 스마트 컨트랙트에서 멀티 인덱스 테이블을 쓸 때 사용된다. 멀티 인덱스 테이블 사용에 RAM이 얼마나 소모되는지 정확히 이해하기 위해서는 Chainbase에 대한 이해가 선행되어야 한다.

Chainbase는 메모리 맵 파일 방식의 데이터베이스로 EOS와 Steem에서 지속성(persistence)을 띤 모든 데이터를 저장하는 장소이다. boost::multi_index_container 기반으로 만들어져 멀티 인덱스를 지원하고 여러 타입의 오브젝트를 하나의 Chainbase DB에 저장할 수 있다.

eosio::chain 라이브러리는 object_type 이란 enumeration을 이용하여 오브젝트 타입을 구분한다. types.hpp 파일에 정의되어 있는데 개발 도중 deprecated 되어 현재 사용하지 않는 타입의 경우 UNUSED_ 란 접두사가 붙는다.

이 중 스마트 컨트랙트의 멀티 인덱스 테이블과 관련이 있는 대표적인 항목은 table_id_object_typekey_value_object_type 이다. 간단히 정리하면 테이블 자체에 대한 정보는 table_id_object 로 저장하고, 그 테이블에 추가되는 각 행(row)은 key_value_object 로 저장한다.

스마트 컨트랙트와 eosio::chain 라이브러리가 연결되는 지점을 분석해보자. 스마트 컨트랙트에서 멀티 인덱스 테이블에 emplace() 함수를 사용하여 데이터를 저장하면 내부적으로는 db_store_i64() 라는 C API를 호출하는 것을 알 수 있다. 여기서 i64는 64비트 정수를 의미하는데 64비트 정수를 저장하겠다는 것이 아니라 64비트 정수를 인덱스로 사용하여 데이터를 저장하겠다는 뜻이다. eosio::multi_index 테이블이 primary key로 64비트 정수를 필요로 하는 이유가 여기에 있다.

스마트 컨트랙트의 WASM 인터페이스는 eosio::chain 라이브러리 내의 함수로 연결된다.

context.db_store_i64() 에서 contexteosio::apply_context 의 인스턴스이므로 다음 함수가 호출된다.

find_or_create_table() 은 컨트랙트에서 multi_index 의 인스턴스를 생성할 때 입력하는 code , scope 값과 테이블명 table 을 이용하여 테이블을 검색한다. 조건에 맞는 테이블이 발견되면 이를 반환하고 그렇지 않은 경우 새로운 테이블을 생성하여 반환한다.

참고로 이 함수는 db_store_i64() 에 의해 호출되고 있음을 기억하라. 스마트 컨트랙트에서 multi_index 테이블을 선언하는 것만으로는 실제 Chainbase 내에 테이블을 생성하지 않는다.

예를 들어 eosio.token 컨트랙트에서 토큰을 생성하는 create() 액션을 살펴보자.

토큰 정보를 저장하기 위한 테이블 stats statstable( _self, sym.name() ); 을 선언하고 있지만 이는 컨트랙트에서 테이블 접근을 위한 핸들러를 만든 것이지 실제 테이블을 생성한 것이 아니다. 만약 여기서 다른 작업 없이 액션을 종료하였다면 Chainbase 상에는 아무 변경도 일어나지 않는다.

테이블에 새로운 행을 삽입하는 statstable.emplace() 가 실행되면 내부적으로 db_store_i64() 가 호출되어 새로운 테이블이 생성된다.

테이블 생성에 부과되는 RAM 사용량은 config::billable_size_v<table_id_object> 이다. billable_size_v<T>billable_size<T> 를 16바이트 단위로 정렬(align)한 값이다. (정렬 결과는 16바이트 단위로 올림한 값이 된다)

table_id_object 가 위와 같이 구성되어 있으므로 44바이트가 필요한데 여기에 인덱스가 차지하는 오버헤드 용량이 추가된다. table_id_object 의 경우 2개의 인덱스를 사용하는데 하나는 Chainbase DB 사용에 기본적으로 필요한 내부 인덱스 id_type 이며 다른 하나는 테이블 검색에 사용하는 composite key {code, scope, table}인덱스이다. 인덱스당 필요한 용량 overhead_per_row_per_index_ram_bytes 는 32바이트이다.

따라서 테이블 생성에 필요한 RAM 크기 billable_size<table_id_object> 는 44 + 2 × 32 = 108바이트지만 실제 사용량은 16바이트 단위로 정렬한 billable_size_v<table_id_object> 로 부과되므로 112바이트가 소모된다.

마찬가지 방식으로 테이블에 행 삽입시 필요한 RAM 크기도 계산할 수 있다.

key_value_object 생성에 필요한 상수 크기의 필드 사이즈 32바이트, 데이터 저장 공간을 가리키는 포인터 8바이트, 데이터 크기 저장에 4바이트, 인덱스 2개(내부 인덱스 id_type 과 행 검색에 사용하는 composite key {table_id, primary_key} 인덱스)를 포함하여 합이 32 + 8 + 4 + 2 × 32 = 108바이트가 되며 16바이트 단위로 정렬하면 112바이트가 된다.

여기에 테이블에 저장하는 구조체에 따라 필요한 크기가 달라지므로 이를 더해주어야 한다. db_store_i64() 함수에서 RAM 사용량을 업데이트하는 부분을 보면 실제 데이터 크기buffer_size 를 추가로 부과하고 있음을 알 수 있다.

eosio.token 에서 토큰 정보를 저장하는 currency_stats 구조체의 경우 asset (16바이트) 2개와 account_name(8바이트) 1개, 합이 40바이트이므로 새로운 토큰을 생성할 때마다 112 + 40 = 152바이트의 RAM을 사용할 것을 예상할 수 있다.

RAM 사용에 있어 추가적인 변수는 secondary index를 사용하는 경우이다. 스마트 컨트랙트의 멀티 인덱스 테이블은 64비트 정수의 primary key 이외에도 추가로 16개까지의 secondary index를 정의할 수 있는데 이 때 사용 가능한 key 타입은 uint64_t, uint128_t, uint256_t, double, long double 5종류가 있다. secondary index를 사용하는 경우 행마다 추가로 RAM을 차지하며 이 때 인덱스 정보를 저장하는 object_type 은 secondary key의 타입에 따라 index64_object_type, index128_object_type, index256_object_type, index_double_object_type, index_long_double_object_type 이 된다.

  • billable_size_v<index64_object> : 128바이트
  • billable_size_v<index128_object> : 144바이트
  • billable_size_v<index256_object> : 160바이트
  • billable_size_v<index_double_object> : 128바이트
  • billable_size_v<index_long_double_object> : 144바이트

eosio.token 컨트랙트에서 토큰 정보를 저장하는 테이블에 128비트 정수를 key로 사용하는 secondary index를 설정하였다면 매 토큰 저장에 112 + 40 + 144 = 296바이트씩 사용하게 된다. 따라서 secondary index를 여러 개 설정한 테이블에 새로운 행을 삽입하면 RAM 사용량이 빠르게 증가하게 된다.

RAM 절약 팁

멀티 인덱스 테이블에 새로운 행을 삽입하는 것은 매우 높은 비용이 드는 작업이다. 하나의 행에 std::vector 타입의 컨테이너를 만들고 데이터를 추가할 때는 멀티 인덱스 테이블에 새로운 행을 삽입하는 것이 아니라 vector 컨테이너 안에 데이터를 추가하면 인덱스에 따른 오버헤드 없이 저장하는 데이터 크기만큼만 RAM 사용량이 증가한다. 단, 이 방식에서는 key를 이용한 검색이 되지 않으므로 외부에서 인덱스 관리를 따로 하면서 블록체인에는 기록만 하는 경우 등에 제한적으로 사용할 수 있는 방법이다.

결론

EOS에서 RAM은 매우 비싼 자원이다. 사용량 계산이 쉽지 않고 CPU나 NET bandwidth와 달리 한 번 사용하면 저장된 데이터를 삭제하지 않는 한 가용량이 회복되지 않는다. 또 계속해서 변하는 가격 때문에 사용 비용을 예측하기도 어렵다. 자원 임대 시장 도입으로 이 문제를 해결하고자 논의가 진행중이나 확정된 것은 아니며 확실히 해결될 것인지도 미지수이다.

현재의 블록체인 기술 수준에서는 모든 데이터를 블록체인에 기록하기보다 무결성이 중요하거나 투명하게 공개해야 하는 데이터에 한정하여 블록체인을 활용하고 그렇지 않은 경우 중앙화 된 방식과 결합하여 사용하는 것이 실제 가능한 서비스 제공 방식이 될 것이다.

이번 글에서는 RAM 사용량의 계산 원리에 집중하여 설명하였다. 다만 코드 레벨의 분석이다보니 사용자 또는 DApp 개발자 입장에서는 개별 상황에서의 구체적인 RAM 사용량에 대해서는 답을 얻지 못했을 수 있다. 다음 글에서는 상황별 RAM 사용량에 대한 목록을 정리할 예정이다.

--

--