Geth 클라이언트 스냅샷 가속 메커니즘을 이해하기 위한 기사
편집자 주: 이 기사의 출처는이더리움 애호가(ID: ethfans)편집자 주: 이 기사의 출처는
이더리움 애호가(ID: ethfans)
이더리움 애호가(ID: ethfans)
, 저자: Péter Szilágyi, 번역: A Jian, 승인을 받아 Odaily에서 재인쇄함.
이 글은 Geth Client Answers 시리즈의 첫 번째 글입니다. 이번 주에 가장 많이 요청된 질문은 플랫 데이터베이스 구조와 레거시 구조의 주요 차이점에 대해 말씀해 주시겠습니까?
이더리움 상태
가속 구조(가속 구조)에 대해 알아보기 전에 이더리움의 "상태" 개념과 다양한 추상화 수준에서 상태가 저장되는 방식을 검토해 봅시다.
이더리움에는 계정 모음과 계약당 계정 슬롯 모음이라는 두 가지 유형의 상태가 있습니다. 완전히 추상적인 관점에서 보면 두 종류의 데이터 모두 키-값 쌍입니다. 계정 컬렉션은 주소를 해당 주소의 논스, 잔액 등에 매핑합니다. 계약의 저장 필드는 모든 값(계약에서 정의 및 사용)을 일부 값에 매핑합니다.
불행하게도 이러한 키-값 쌍을 플랫 데이터로 저장하는 것은 매우 효율적일 수 있지만 정확성을 확인하는 것은 계산적으로 어려워집니다. 데이터가 수정될 때마다 모든 데이터를 상향식으로 해시해야 합니다.
항상 전체 데이터베이스를 해시할 필요가 없도록 데이터베이스를 연속 조각으로 분할하고 일종의 트리 구조를 만들 수 있습니다! 가장 독창적이고 유용한 데이터는 리프 노드에 배치되며 트리의 각 내부 노드는 노드 아래 콘텐츠의 해시 값입니다. 이런 식으로 어떤 값을 수정하고 싶을 때 대수 해시 연산만 수행하면 됩니다. 이러한 종류의 데이터 구조에는 실제로 "메르켈 트리"라는 잘 알려진 이름이 있습니다.
그러나 아직 끝나지 않았습니다. 이 방법은 여전히 계산 복잡성이 부족합니다. Merkle 트리 구조는 기존 데이터를 수정할 때 매우 효율적이지만 데이터를 삽입하고 삭제하면 기본 작은 데이터 블록의 경계가 변경되면 계산된 모든 해시 값이 무효화됩니다.
이때 맹목적으로 데이터베이스를 그룹화하는 대신 키 자체를 사용하여 데이터를 구성하고 공통 접두사를 기반으로 트리와 같은 형식으로 데이터를 정렬할 수 있습니다! 이러한 방식으로 삽입 및 삭제 작업은 모든 노드에 영향을 미치지 않고 트리의 루트에서 리프까지의 경로에 있는 (로그) 노드에만 영향을 미칩니다. 이 데이터 구조를 "Patricia 트리"라고 합니다.
위의 두 가지 방법, 즉 Patricia 트리의 트리 계층 구조와 Merkle 트리의 해시 알고리즘을 결합한 것이 소위 "Merkel-Patricia 트리"이며 실제로도 사용되는 이더리움의 상태를 나타내는 데이터 구조입니다. 수정, 삽입, 삭제 또는 확인 여부에 관계없이 로그 복잡성만 있습니다! 유일한 작은 예외는 시도의 균형을 맞추기 위해 삽입 전에 일부 키가 해시된다는 것입니다(작은 추가 사항은 시도의 균형을 맞추기 위해 삽입 전에 키가 해시된다는 것입니다).
이더리움의 상태 저장소
위에서 Ethereum이 상태를 저장하기 위해 Merkle Patricia 트리 구조를 사용하는 이유를 설명합니다. 불행히도 필요한 작업은 빠르지만 각 옵션에는 희생이 따른다. 업데이트 및 확인 작업의 로그 복잡성은 각 개별 키에 대한 읽기 및 저장이 로그임을 의미합니다(로그 읽기 및 로그 저장). 이는 트리 구조의 각 내부 노드가 하드 디스크에 별도로 저장되어야 하기 때문입니다.
지금은 계정 트리가 얼마나 깊은지 정확히 알 수 없지만 약 1년 전에 계정 상태가 7레벨 트리를 채웠습니다. 즉, 모든 트리 작업(예: 읽기 균형, 난스 쓰기)은 최소 7-8개의 내부 노드에 영향을 미치므로 최소 7-8개의 영구 데이터베이스 액세스를 수행합니다. LevelDB는 최대 7계층까지 데이터를 구성하므로 추가 승수가 있습니다. 최종 결과는 단일 상태 액세스가 25-50개의 임의 디스크 액세스로 증폭될 것으로 예상된다는 것입니다. 여기에 블록에 있는 모든 트랜잭션의 모든 상태 읽기 및 쓰기를 곱하면 무서운 숫자가 됩니다.
[물론 모든 클라이언트 구현은 오버헤드를 가능한 한 낮게 유지하려고 합니다. Geth는 더 큰 메모리 영역을 사용하여 여러 노드를 캐시하고, 메모리 내 가지치기 메커니즘을 사용하여 몇 블록 후에 삭제될 데이터를 디스크에 쓰는 것을 방지합니다. 그러나 이를 명확히 하기 위해서는 다른 기사가 필요합니다. ]
또한 무서운 것은 이 수치가 모든 상태를 24/7 검증하도록 보장된 이더리움 노드를 실행하는 비용이라는 것입니다.
더 잘할 수 있을까요?
모든 방문이 동일하게 생성되는 것은 아닙니다.
이더리움의 작동은 암호화된 상태 증명에 의존합니다. 모든 데이터를 검증하는 능력을 유지하려는 한 하드 디스크 읽기 및 쓰기 증폭 문제를 피할 수 없습니다. 즉, 우리는 우리가 확인한 데이터를 신뢰할 수 있고 신뢰할 수 있습니다.
각 상태 개체를 반복적으로 확인하는 것은 의미가 없지만 하드 디스크에서 데이터를 가져올 때마다 확인해야 한다면 그런 의미 없는 일을 하고 있는 것입니다. Merkle Patricia 트리 구조는 본질적으로 쓰기 작업을 위해 설계되었지만 읽기 작업에는 부담이 됩니다. 우리는 그것을 제거할 수 없고, 그것을 슬림하게 만들 수는 없지만, 그것이 우리가 그것을 모든 경우에 사용해야 한다는 것을 의미하지는 않습니다.
Ethereum 노드 액세스 상태 시나리오는 크게 다음 세 가지 범주로 나눌 수 있습니다.
새 블록을 가져올 때 EVM 코드 실행은 다소 균형 잡힌 상태 읽기 및 쓰기 수를 생성합니다. 그러나 서비스 거부 공격에 사용되는 블록은 쓰기 작업보다 훨씬 더 많은 읽기 작업을 생성할 수 있습니다.
노드 운영자가 상태를 검색할 때(예: ethcall 및 유사한 작업 호출) EVM 코드 실행은 읽기 작업만 생성합니다(물론 쓰기 작업이 있을 수 있지만 이러한 작업으로 생성된 데이터는 결국 폐기되고 지속되지 않습니다. 하드 드라이브 내부).
노드가 블록체인을 동기화할 때 동기화 장치는 원격 노드에 상태를 요청하고 요청자는 데이터를 마이닝하여 네트워크를 통해 동기화 장치에 전파합니다.
위의 액세스 패턴을 기반으로 상태 트리를 건드리지 않고 읽기 작업을 단락시킬 수 있다면 많은 노드 작업이 훨씬 빨라질 수 있습니다. 이것은 심지어 이전에는 너무 비싸서 실행 불가능했던 일부 새로운 액세스 패턴(예: 상태 반복)을 열 수 있습니다.
물론 희생은 불가피하다. 트리 구조를 제거하지 않고 새로운 가속 구조는 추가 오버헤드를 발생시킵니다. 유일한 질문은 다음과 같습니다. 추가 오버헤드가 시도를 보증하기에 충분합니까?
그것을 따르십시오
우리는 모든 문제를 해결하기 위해 마법의 Merkle Patricia 트리 구조를 개발했으며 이제 읽기 작업이 이를 우회하도록 만들고 싶습니다. 그렇다면 읽기 작업을 다시 빠르게 하려면 어떤 종류의 가속 구조를 사용해야 할까요? 분명히 트리 구조가 필요하지 않으면 트리 구조와 함께 제공되는 모든 복잡성을 제쳐두고 원래 상태로 바로 돌아갈 수 있습니다.
이 글의 서두에서 언급한 바와 같이 이론적으로 이상적인 상태의 이더리움 상태의 데이터 저장 방식은 간단한 키-값 쌍이어야 하며 Merkel Patricia 트리의 제한 없이는 우리를 막을 것이 없다. 이 이상적인 계획을 실현하기 위해!
얼마 전에 Geth는 스냅샷 가속 구조를 도입했습니다(기본적으로 활성화되지 않음). 스냅샷은 주어진 블록에서 이더리움 상태의 전체 보기입니다. 구현 세부 사항을 추상화하면 플랫 키-값 쌍으로 표시되는 모든 계정 및 계약 스토리지 슬롯을 함께 쌓을 수 있습니다.
계정이나 특정 스토리지 슬롯에 액세스하려고 할 때마다 각 트리에서 7-8번 쿼리하는 대신 LevelDB의 쿼리 작업을 한 번만 지불하면 됩니다. 이론적으로 스냅샷 업데이트도 매우 간단합니다.블록을 처리한 후 업데이트할 각 스토리지 슬롯에 대해 추가로 1개의 LevelDB 쓰기 작업만 수행하면 됩니다.
스냅샷 가속 구조는 쓰기 작업의 복잡성을 O(log n)에서 O( 1 + log n)(LevelDB 오버헤드를 곱함), 디스크 스토리지가 O(n log n)에서 O(n + n log n)으로 증가했습니다.
악마는 디테일에 있다
이더리움 상태 스냅샷의 가용성을 유지하는 것도 쉽지 않습니다. 블록이 계속해서 생성되고 마지막 블록 위에 쌓이는 한 최신 변경 사항을 스냅샷에 병합하는 조잡한 방법이 제대로 작동합니다. 그러나 아주 작은 블록체인 재구성(단지 하나의 블록)으로도 실행 취소 작업이 전혀 설계되지 않았기 때문에 스냅샷 메커니즘이 무너집니다. 플랫 데이터 표현 패턴의 경우 영구 쓰기는 단방향 작업입니다. 설상가상으로 이전 상태에 액세스할 방법이 없습니다(예: 일부 dApp은 3블록 전 상태 또는 빠른/스냅 동기화 모드에서 64블록 상태에 액세스해야 함).
이러한 제한을 극복하기 위해 Geth 클라이언트 스냅샷은 두 부분, 즉 이전 블록 상태의 완전한 스냅샷인 영구 하드 디스크 계층(예: 상위 128개 블록)과 메모리 내 diff 계층 A로 구성됩니다. 가장 최근 쓰기 작업을 수집하는 데 사용되는 구성 요소 트리입니다.
새 블록을 처리할 때 이러한 쓰기를 하드 디스크 계층에 직접 병합하지 않고 이러한 변경 사항을 포함하는 새로운 인메모리 diff 계층을 생성합니다. 메모리 내부의 diff 레이어가 충분한 수의 레이어로 누적되면 맨 아래 레이어가 업데이트 병합을 시작하여 하드 디스크 레이어로 푸시합니다. 상태 개체를 읽어야 하는 경우 상위 diff 계층에서 검색을 시작하고 diff 계층 또는 하드 디스크 계층에서 찾을 때까지 아래로 이동합니다.
이 데이터 표현은 매우 강력하며 많은 문제를 해결합니다. 메모리 내부의 diff 레이어가 트리를 형성하기 때문에 128 블록 내의 체인 재구성은 부모 블록에 속한 diff 레이어만 꺼내고 빌드를 시작하면 됩니다. 이전 상태가 필요한 dApp 및 farsyncer는 최근 128개 상태에 액세스할 수 있습니다. 오버헤드는 128개의 매핑 조회가 되지만 128개의 메모리 내 조회는 8개의 하드 디스크 읽기 및 레벨 DB의 4~5배 증폭보다 몇 배 더 빠릅니다.
물론 여전히 많은 함정이 있습니다. 너무 깊이 들어가지는 않겠지만 다음 목록을 간단히 열거해 보겠습니다.
자체 파괴(계약 자체 파괴 작업)(및 삭제 작업)는 diff 레이어의 하강을 단락시켜야 하기 때문에 특히 다루기가 어렵습니다.
영구 하드 디스크 계층보다 더 깊은 체인 재구성이 있는 경우 현재 스냅샷이 완전히 삭제되고 재생성됩니다. 전체 작업 세트는 매우 비쌉니다.
노드가 종료되면 메모리의 diff 계층을 로그에 유지하고 백업을 위해 로드해야 합니다. 그렇지 않으면 다시 시작한 후 스냅샷이 쓸모 없게 됩니다.
특정 메모리 사용량을 초과하는 경우에만 디스크로 플러시되는 누산기로 가장 낮은 diff 계층을 사용합니다. 이를 통해 청크 전체에서 동일한 슬롯에 대한 쓰기 중복을 제거할 수 있습니다.
계약이 동일한 이전 스토리지 슬롯에 반복적으로 액세스하는 경우 하드 디스크가 손상되지 않도록 하드 디스크 계층에 읽기 캐시를 할당해야 합니다.
메모리 내 diff 레이어에서 누적 블룸 필터를 사용하여 상태 개체가 diff 레이어에 존재할 가능성이 있는지 또는 디스크로 직접 이동해야 하는지 빠르게 감지합니다.
원본 데이터(계정 주소, 컨트랙트 저장 키)는 키로 설정되지 않지만 이 데이터의 해시 값을 키로 사용하여 스냅샷의 반복 순서가 Merkle Patricia 트리의 반복 순서와 동일하도록 합니다.
영구 하드 디스크 계층을 생성하는 시간은 상태 트리 창을 가지치기하는 시간보다 훨씬 길기 때문에 생성기조차 체인의 작동을 동적으로 추적해야 합니다.
아름다움과 추함의 공존
Geth의 스냅샷 가속 구조는 상태 읽기의 복잡성을 한 단계 줄입니다. 이것은 읽기 기반 DoS 공격이 실행하기 훨씬 더 어렵고 ethcall 호출도 훨씬 더 빠르다는 것을 의미합니다(CPU 병목 현상이 없다고 가정).
물론 이 모든 것이 비용이 들지 않는 것은 아닙니다. 초기 동기화가 완료된 후 메인 네트워크에 참여하는 노드들은 초기 스냅샷을 구성(이후 가용성 유지)하기 위해 9~10시간이 필요하며 15GB 이상의 추가 하드 디스크가 필요합니다.
발문
나쁜 부분은 어디입니까? 스냅샷 메커니즘을 출시하기에 충분한 신뢰를 구축하는 데 6개월이 걸렸으며 여전히 기본 기능이 아니며 --snapshot 플래그를 사용하여 활성화해야 하며 메모리 사용 및 충돌 복구에 대한 일부 연마 작업이 아직 남아 있습니다. 할 것.
전반적으로 우리는 이 개선 사항을 매우 자랑스럽게 생각합니다. 엄청난 양의 작업이 관련되어 있었고 어둠 속에서 더듬으며 모든 것을 직접 구현하고 작동하기를 기도했습니다. 또 다른 흥미로운 점은 리프 싱크의 첫 번째 버전이 2년 반 전에 작성되었지만 이를 구동하는 데 필요한 가속 구조가 부족하여 차단되었다는 것입니다.


