Klaytn 성능 향상 대장정 - 1000만 account 극복기
블록체인은 네트워크 상에서 상태가 합의를 이루고 복제되어야하는 제한 조건 때문에 전통적인 데이터베이스에 비해 떨어지는 성능을 보인다. 또한 Klaytn에서는 각 어카운트의 정보를 저장하기 위해 Merkle Partricia Trie를 사용하는데, Merkle Partricia Trie의 성능은 전체 어카운트 개수가 늘어날수록 떨어진다. 1000만개 이상의 어카운트가 존재할 때에도 성능 저하를 최소화 하기 위해 1) 일반적인 성능 향상과 더불어 2) 어카운트 개수 증가에 의한 성능 저하를 최소화하는 작업을 동시에 진행하였다. 본 세션에서는 블록체인의 성능 병목 구간에 대한 설명과 더불어 1) Consensus Layer, 2) In-memory Layer, 3) Persistent Layer 의 각 레이어 별로 적용한 최적화 기법들, 그리고 그 사이에 마주친 문제들에 대해 공유하는 시간을 갖는다.
1. Klaytn 성능 향상 대장정
: 1,000만 어카운트 극복기
우준희 melvin.gx
Ground X
2. About Me
우준희 (Melvin)
Platform and SDK Team, Ground X
• SAP 의 인메모리 데이터베이스 HANA SQL Optimizer 팀에 있었습니다.
• 공부도 더 해보고, 유학도 가볼까하고 퇴사해서 대학원에 진학했습니다.
그런데 연구랑 잘 안맞아서 진로를 고민하던 중에, Ground X 에 대한 이야기를 듣고
재밌을 것 같아서 지원했고, 작년 5월부터 플랫폼 팀에서 일하고 있습니다.
• (못 믿으시겠지만) 올해 초까지만 해도 플랫폼 팀에서 제일 어렸습니다. (ㅋ)
• 퍼포먼스, 블록 다운로드, 스토리지 레이어 관련된 작업을 많이 하고 있습니다.
3. 무엇을, 왜 최적화 했나
무엇을, 어떻게 최적화 했나
최적화에 대해 얻은 교훈
앞으로 해야할 일
01
02
03
04
5. 왜 1000만 어카운트?
카카오톡 Monthly Active Users = 국내 4400만, 글로벌 5000만
사용자의 20%가 클레이튼에 온보딩 한다면? = 1000만 사용자
6. 왜 1000만 어카운트가 문제인가?
계정 데이터를 저장 및 관리하는 Merkle Patricia Tree 와 StateDB 때문
• 계정 데이터 하나는 Merkle Patricia Tree(=MPT) 의 하나의 leaf-node로 존재하는데,
MPT는 leaf-node가 많을수록 업데이트 비용이 커지는 구조.
State Trie
(=MPT)
어카운트 데이터가 들어있는 leaf-node
7. 왜 1000만 어카운트가 문제인가?
계정 데이터를 저장 및 관리하는 Merkle Patricia Tree 와 StateDB 때문
• StateDB는 State Trie의 일부를 메모리에서 관리하면서 어카운트에 대한 업데이트를 반영하는데,
노드가 많고, 업데이트가 빈번한 상황에서 비효율적으로 동작하고 있었음.
StateDB
State Trie
(=MPT)
어카운트 데이터가 들어있는 leaf-node
9. StateDB랑 MPT 디자인을 새로하면 되지 않을까요?
“네,,, 그러면 좋은데,,, 시간이 없어서,,,”
• StateDB + MPT 구조보다는 새로운 자료 구조를 디자인해서 사용하는 것이 더 좋을 수 있음.
• 하지만 메인넷 출시 전까지 주어진 시간이 많지 않았음. (=3달)
• 이전까지는 네트워크의 성능보다는 안정성에 초점을 두고 개발을 했기 때문.
• 따라서, 새로운 디자인을 고민하기보다는 현재 구조 개선에 초점을 맞추고 개발을 진행.
17. Account Data Cache
어카운트 데이터에 캐시를 적용해서 성능을 올려보자!
• State Object Cache
• State Trie Node Cache
18. State Object Cache
배경 설명 - StateDB와 stateObject(s)
• 각 어카운트는 stateObject 라는 구조체로 표현되고, 일련의 stateObject는 StateDB에 의해 관리됨.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
19. State Object Cache
배경 설명 - 어카운트 데이터 읽기
• 어떤 어카운트의 데이터가 StateDB에 존재하지 않는 경우,
State Trie(=MPT)에서 디코딩해서 갖고오거나 LevelDB에서 가져옴.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
“decoding” persistent access
20. State Object Cache
배경 설명 - 어카운트 데이터 쓰기
• 하나의 블록이 만들어지면, 업데이트 된 stateObject 들이 인코딩되어 State Trie를 업데이트 함.
• State Trie는 매 블록마다 LevelDB에 쓰는 것이 아니라, 128블록마다 씀.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
“encoding” persistent access
21. State Object Cache
문제 상황 - 매 블록마다 버려지는 StateDB와 stateObjects
• 그런데 StateDB는 매 블록마다 사용 이후에 버려져서, stateObjects도 버려짐.
• 따라서 블록마다 State Trie 혹은 LevelDB에서 읽어와야 하기 때문에,
디코딩 비용 혹은 데이터베이스 접근 비용 발생.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
“decoding” persistent access
22. State Object Cache
문제 해결 - 만들어 놓은 stateObject를 버리지 말고 재사용하자!
• 재사용하는 경우, 디코딩 비용과 데이터베이스 접근 비용을 줄일 수 있다!
• State Object Cache는 [1] 블록이 쓰일 때 State Trie와 같이 업데이트가 되거나,
[2] StateDB에 없는 stateObject를 State Trie나 LevelDB로 읽어올 때 업데이트 됨.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
StateObject
Cache
23. State Object Cache
싱글 노드 테스트 기준으로 15% 정도의 성능 향상
하지만 멀티 노드 테스트에서 여러가지 문제가 발생
• 싱글 노드 테스트는 노드를 직접 띄우지 않고, 트랜잭션 실행 및 블록 쓰기에 대해서만 테스트하는데
• [1] 고려되지 않은 execution path에서 문제 발생
• [2] 싱글 노드 테스트만큼 성능 향상이 보이지 않음
stateObjects
State Trie
(MPT)
LevelDB
StateDB
StateObject
Cache
24. State Object Cache
[1] 고려되지 않은 execution path에서 문제 발생
• StateDB와 MPT는 변하지 않는 특정 시점에서 어카운트들의 상태를 의미하는데,
State Object Cache는 특정 “최신” 시점에서 어카운트들의 상태를 저장하고 있음.
• “최신” 시점은 N번 블록에서 N+1번 블록으로 변경될 수 있는데, 이에 대한 고려가 되지 않았음 = 버전 컨트롤 필요.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
StateObject
Cache
25. State Object Cache
[2] 싱글 노드 테스트만큼 성능 향상이 보이지 않음
• stateObject는 State Trie의 leaf-node들이고, StateObject Cache는 이 leaf-node들만 캐시.
• Leaf node 읽기는 조금 빨라졌으나, 전체 StateDB+MPT 관리 비용은 줄어들지 않았는데, 이 비용의 비중이 컸음.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
StateObject
Cache
26. State Trie Node Cache
문제 의식 = LevelDB 접근을 줄일 수 없을까?
• StateDB 혹은 State Trie에 어카운트가 존재하지 않는 경우 LevelDB 접근을 해야함.
• LevelDB 접근은 persistent layer access 이기 때문에 비싼 작업.
stateObjects
State Trie
(MPT)
LevelDB
StateDB
persistent access
27. State Trie Node Cache
배경 설명 - MPT와 어카운트 데이터
State Trie
(=MPT)
어카운트 데이터가 들어있는 leaf-node
leaf-node를 바탕으로 구성된 MPT
(=메모리에 존재)
28. State Trie Node Cache
배경 설명 - MPT와 어카운트 데이터
State Trie
(=MPT)
어카운트 데이터가 들어있는 leaf-node
leaf-node를 캐싱하려고 한 것이
방금 설명한 State Object Cache
leaf-node를 바탕으로 구성된 MPT
(=메모리에 존재)
29. State Trie Node Cache
배경 설명 - MPT에서 LevelDB로 = 스토리지 영역에 “쓰기”
State Trie
(=MPT)
State Trie 관리 메커니즘에 의해
일부 노드들이 LevelDB로 저장됨.
LevelDB
persistent access
어카운트 데이터가 들어있는 leaf-node
leaf-node를 바탕으로 구성된 MPT
(=메모리에 존재)
30. State Trie Node Cache
배경 설명 - LevelDB에서 MPT로 = 스토리지 영역에서 “읽기”
State Trie
(=MPT)
어카운트 데이터가 들어있는 leaf-node
이 중에서 일부 노드가 다시 필요해지면
LevelDB에서 필요한 노드를 가져옴.
LevelDB
persistent access
leaf-node를 바탕으로 구성된 MPT
(=메모리에 존재)
31. State Trie Node Cache
문제 해결 - 빠지는 걸 막을 수는 없지만, 다시 가져올 때 빠르게 가져올 수는 있다!
State Trie
(=MPT)
캐시 레이어를 추가해서, LevelDB가 아닌,
캐시 레이어에서 노드를 가져올 수 있게 변경
LevelDB
State Trie
Node Cache
in-memory access
32. State Trie Node Cache
문제 해결 - 빠지는 걸 막을 수는 없지만, 다시 가져올 때 빠르게 가져올 수는 있다!
• go-ethereum PR #18087 (=add trie read caching layer) 적용 및 최적화
• go-ethereum에서는 LevelDB에서 데이터를 읽어올 때에만 캐시에 넣어줬는데,
• Klaytn에서는 LevelDB에 데이터를 써줄 때에도 캐시에 넣어줘서 추가적인 성능 이득을 가져감.
stateObjects
State Trie
Node Cache
LevelDB
StateDB
State Trie
(MPT)
33. Account Data Management Optimization
어카운트 데이터 관리 메커니즘을 효율적으로 개선시켜보자!
• Concurrent Commit With Partitioning
• Tries in Memory from 128 to 4
34. Concurrent Commit with Partitioning
• go-ethereum의 경우, 하나의 LevelDB 인스턴스에 모든 key-value 데이터를 저장.
• 쓰기 작업이 병렬적으로 처리되지 못하고 직렬화되어 처리할 수 밖에 없음.
Before Concurrent Commit with Partitioning
35. Concurrent Commit with Partitioning
• go-ethereum의 경우, 하나의 LevelDB 인스턴스에 모든 key-value 데이터를 저장.
• 쓰기 작업이 병렬적으로 처리되지 못하고 직렬화되어 처리할 수 밖에 없음.
Before Concurrent Commit with Partitioning
36. Concurrent Commit with Partitioning
1. Database Partitioning / Parallel Write
= LevelDB를 하나만 쓰지 말고, 여러 개 써버리자!
2. Concurrent Commit
= Commit 작업을 serial하게 처리하지 말고, parallel하게 처리하자!
두 가지 방법으로 문제를 해결해보았습니다.
37. Concurrent Commit with Partitioning
• go-ethereum의 경우, 하나의 LevelDB 인스턴스에 모든 key-value 데이터를 저장.
• [1] 쓰기 작업이 병렬적으로 처리되지 못함.
• [2] LevelDB는 주기적으로 파일을 정리해서 새로 만드는 compaction 작업을 수행하는데,
compaction 작업을 처리하는 동안 WriteLock이 걸리기 때문에 쓰기 작업을 할 수 없음.
1. Database Partitioning / Parallel Write
38. Concurrent Commit with Partitioning
• go-ethereum의 경우, 하나의 LevelDB 인스턴스에 모든 key-value 데이터를 저장.
• [1] 쓰기 작업이 병렬적으로 처리되지 못함.
• [2] LevelDB는 주기적으로 파일을 정리해서 새로 만드는 compaction 작업을 수행하는데,
compaction 작업을 처리하는 동안 WriteLock이 걸리기 때문에 쓰기 작업을 할 수 없음.
• Klaytn의 경우, 여러 개의 LevelDB 인스턴스에 key-value 데이터를 저장
• [1] 쓰기 작업을 병렬적으로 처리할 수 있음.
• [2] 하나의 LevelDB를 사용하는 것보다 compaction이 덜 발생하고,
따라서 WriteLock에 의한 쓰기 딜레이가 감소.
1. Database Partitioning / Parallel Write
39. Concurrent Commit with Partitioning
2. Concurrent Commit
• Commit은 주기적(=128블록)으로 State Trie의 노드들을 LevelDB에 써주는 작업
• 기존의 Commit은 하나의 고루틴에서 모든 노드들을 순회하며 쓰기 작업을 수행
• 자식 노드 A, B, C가 있을 때, A가 처리되고 있다면 B, C는 처리되지 못하고 기다려야 함
40. Concurrent Commit with Partitioning
2. Concurrent Commit
• Commit은 주기적(=128블록)으로 State Trie의 노드들을 LevelDB에 써주는 작업
• 기존의 Commit은 하나의 고루틴에서 모든 노드들을 순회하며 쓰기 작업을 수행
• 자식 노드 A, B, C가 있을 때, A가 처리되고 있다면 B, C는 처리되지 못하고 기다려야 함
• 이를 해결하기 위해 루트 노드의 자식 노드들에게 개별 고루틴을 할당, 동시적으로 처리되게 함.
41. Concurrent Commit with Partitioning
• go-ethereum의 경우, 하나의 LevelDB 인스턴스에 모든 key-value 데이터를 저장.
• 쓰기 작업이 병렬적으로 처리되지 못하고 직렬화되어 처리할 수 밖에 없음.
After Concurrent Commit with Partitioning
3 sec
42. Concurrent Commit with Partitioning
• go-ethereum의 경우, 하나의 LevelDB 인스턴스에 모든 key-value 데이터를 저장.
• 쓰기 작업이 병렬적으로 처리되지 못하고 직렬화되어 처리할 수 밖에 없음.
Before Concurrent Commit with Partitioning
44. Tries in Memory from 128 to 4
결과는 강력합니다 - [1] 스토리지 레이어 접근 감소
• LevelDB (read/write)
TriesInMemory=128 (before) TriesInMemory=4 (after)
45. Tries in Memory from 128 to 4
결과는 강력합니다 - [2] 캐시 미스 감소
• State Trie Node Cache Miss Count
TriesInMemory=128 (before) TriesInMemory=4 (after)
46. Tries in Memory from 128 to 4
triesInMemory가 무엇이길래?
• State Trie는 블록 마다 업데이트가 되는데, triesInMemory는 메모리 상에서 접근 가능한 State Trie를 의미
• triesInMemory=128인 경우 최근 128블록, 4인 경우 최근 4블록의 State Trie에 접근 가능
• 이더리움의 경우 포크나 롤백이 가능하기 때문에 과거 State Trie를 보관하는 것이 필요
N-1 N-2 N-3N N-4 … N-128 N-129 …
블록
블록 N-1의 State Trie
47. Tries in Memory from 128 to 4
triesInMemory가 무엇이길래?
• 하지만 triesInMemory 범위에 포함되었다고 모두 메모리에 들고 있는 것은 아니고
들고 있는 노드들의 크기가 일정 값보다 커지면, 이 초과분에 대해서 정리가 들어가게 됨.
• 그리고 정리되는 노드들은 접근 가능성이 낮은, 오래된 노드들.
N-1 N-2 N-3N N-4 … N-128 N-129 …
StateTrie
NodeCache
LevelDB
접근 가능성 낮은, 오래된 노드들이 정리된다
48. Tries in Memory from 128 to 4
문제 상황 - 쓸모없는 노드들을 정리하면서 리소스가 낭비됨.
• 정리된 노드들은 [1] State Trie Node Cache와 [2] LevelDB에 저장되는데
• [1] State Trie Node Cache에 쓰이게 되면서, 다른 노드들이 캐시에서 빠지게 되고, 캐시 미스가 발생.
• [2] LevelDB에 쓰이면서 persistent layer access가 발생하고, 이로 인해서 퍼포먼스 감소.
• 정리되는 노드들은 오래되고, 접근 가능성이 낮은 노드들이라는게 문제.
N-1 N-2 N-3N N-4 … N-128 N-129 …
StateTrie
NodeCache
LevelDB
접근 가능성 낮은, 오래된 노드들이 정리된다
49. Tries in Memory from 128 to 4
해결책 = 관리 범위를 128블록에서 4블록으로 줄여서, 관리 범위와 관리 작업을 줄이자!
• 관리 범위가 줄어들면서 초과분에 대한 정리 작업이 덜 발생하게 됨.
• 따라서 [1] State Trie Node Cache에서 캐시 미스 감소, [2] LevelDB 접근 감소로 퍼포먼스 증가
N-1 N-2 N-3N N-4 … N-128 N-129 …
StateTrie
NodeCache
LevelDB
51. Consensus Message Separation
문제 상황 - 컨센서스 메시지가 다른 메시지와 동일한 고루틴에서 처리되고 있었음
• 컨센서스 노드(=CN)들은 새로운 블록을 만들기 위해 컨센서스 메시지를 교환하는데,
컨센서스 메시지도 다른 메시지와 동일한 고루틴에서 처리되고 있었음.
컨센서스 메시지
트랜잭션 메시지
메시지 처리 고루틴
52. Consensus Message Separation
문제 상황 - 부하 상황에서는 컨센서스 메시지 처리가 늦어짐
• 부하 상황에서는 네트워크가 처리할 수 있는 트랜잭션보다 더 과도한 트랜잭션이 유입되고,
따라서 노드 사이의 트랜잭션 전달 메시지도 증가하게 됨.
• 트랜잭션 전달 메시지와 컨센서스 메시지가 동일한 고루틴으로 처리되기 때문에,
부하 상황에서는 컨센서스 메시지가 제때 처리되지 않고, 블록 생성이 2.5초 정도로 늦어지게 됨.
트랜잭션 메시지
컨센서스 메시지
53. Consensus Message Separation
문제 해결 = 컨센서스 메시지 처리를 별도의 고루틴으로 분리하자!
• 컨센서스 메시지에 별도의 고루틴을 할당해서, 부하 상황에서도 컨센서스 메시지 처리 영향 최소화.
• 부하 상황에서 블록 생성 시간이 2.5초에서 1.1초로 낮아짐.
컨센서스 메시지 처리 고루틴
기타 메시지 처리 고루틴
컨센서스 메시지
트랜잭션 메시지
57. 디자인은 구조적 문제 해결 = 한 번
최적화란 무엇인가?
최적화는 지역적 문제 해결 = 여러 번
58. 최적화란 무엇인가? 노가다 입니다,,,
“지역적 문제 해결의 반복을 통한 점진적 개선이 최적화의 본질”
이라고 멋지게 포장할 수 있지만 결론은 노가다입니다.
59. • 문제를 빠르게 파악하기 위해서는 테스트 시스템 구축 및 성능 측정 & 프로파일링이 빠르게 이뤄져야 함.
• 따라서 "테스트 시스템 구축 및 성능 측정, 프로파일링” 을 빠르게 해야 함.
• 본인의 경우 어떤 부분에서는 앞선 고민을 통해서 이를 미리 적용, 시간을 줄일 수 있었는데
다른 부분에서는 고민을 많이 하지 않고 접근해서 시간을 낭비한 부분이 있었음.
• 여러분들은 꼭! 최적화 작업에 들어가기 전에, “최적화의 최적화”에 대해서 먼저 고민해보시길 바랍니다.
피할 수 없는 노가다, 잘 하려면?
최적화 작업의 루틴 = 문제 파악 + 문제 해결
“문제 해결”은 나의 통제가 불가능한 영역이니, “문제 파악”에 집중합시다.
61. 최적화로는 답이 안나오는 문제는
디자인이 필요합니다.
앞으로 해야할 일
아직 희망이 보이는 문제는
최적화를 시도해봅니다.
62. 앞으로 해야할 일 - 디자인이 필요한 문제
개선시키는 것으론 답이 안나오는 문제들
• MPT 재설계 혹은 대안 찾기 - MPT를 효율적인 구조로 재설계 하거나, 새로운 자료 구조를 찾는다
• 트랜잭션 풀 재설계 - 각 노드가 들고있는 트랜잭션들의 검증, 관리 및 전파를 담당하는 트랜잭션 풀의 재설계
• Concurrent State Trie - State Trie를 concurrent access가 가능한 구조로 변경하기
• LevelDB 대안 찾기 - 블록체인 데이터 구조와 맞지 않는 compaction이 주기적으로 발생하는 문제
63. 앞으로 해야할 일 - 최적화가 더 필요한 문제
개선할 수 있다는 희망이 보이는 문제들
• LevelDB 셋팅 개선 - 다양한 셋팅 값이 존재하는데, 이들에 대한 실험을 통한 최적 파라미터 값 찾기
• 트랜잭션 풀 크기 최적화 - 작으면 관리 오버헤드가 줄어들지만, 그만큼 처리할 수 있는 트랜잭션이 줄어들기도 함.
64. 앞으로 해야할 일은 여러분과 같이 할 일입니다.
Klaytn과 SDK는 오픈소스니까요!
입사해서 하시는 것도 환영합니다 같이 해요
• Github organization
• https://github.com/klaytn
• Github repositories
• Klaytn: https://github.com/klaytn/klaytn
• Caver-js: https://github.com/klaytn/caver-js
• Caver-java: https://github.com/klaytn/caver-java
• Links
• Klaytn Homepage: https://www.klaytn.com
• KlaytnDocs: https://docs.klaytn.com
• Klaytnscope: https://scope.klaytn.com
65. Ground X 부스 - Connecting Bar B
1. Klaytn Tech Talk
Ground X 부스에서 Klaytn 개발팀과 함께 Klaytn 기술에 대해 이야기 나눠보세요.
2. Ground X Survey
Ground X와 Klaytn에 대한 간단한 설문 참여하고, Klaytn 굿즈를 받아가세요.
3. Day 2 Ground X 세션
• 11:00-11:45 블록체인 제품 생태계를 구성해보자 (chofa.gx)
• 12:00-12:45 블록체인 플랫폼 및 SDK 소개 (sam.gx)
• 14:00-14:45 Klyatn을 이용한 블록체인 서비스/DX 개발기 (liam.gx)
• 15:00-15:45 Klaytn 성능 향상 대작전 - 1000만 어카운트 극복기 (melvin.gx)