sonumb

TiDB Internals 1,2,3 본문

개발자 이야기/DBMS_Development

TiDB Internals 1,2,3

sonumb 2021. 11. 2. 12:01

 

https://pingcap.com/blog/tidb-internal-data-storage

https://pingcap.com/blog/tidb-internal-computing

https://pingcap.com/blog/tidb-internal-scheduling

의 한글 번역본

 

https://sonhyunwoong.tistory.com/9

번역본 내용을 참고

TiDB 소개

TiDB는  오픈 소스 분산 확장형 하이브리드 처리 및 분석 처리 데이터베이스 입니다. 수평 확장성 및 지속성, 높은 가용성을 지원한다.(ACID)

TiDB는 MySQL 호환 되면 OLTP 및 OLAP 작업 부하를 처리 하는 원스톱 데이터워어 하우스 역활을 한다.

특징

  • Horizontal scalability
    • TiDB는 새로운 노드를 추가 함으로써 수평 확장성을 제공한다. 인프라 용량에 대하여 걱정하지 마라.
  • MySQL compatibility
    • 대부분의 경우 단일 코드 라인을 변경 하지 않고도 MySQL을 TiDB로 쉽게 대체하여 어플리케이션에 전력을 공급 할 수 있으며 MySQL 에코 시스템의 헤텍을 누릴 수 있다.
  • Distributed transaction
    • TiDB는 ACID 보장하며, 너의 데이터는 언제 어디서는 정확하고 신뢰 할 수 있을 것이다.
  • Cloud Native
    • TiDB는 public, privite, 병렬 작업 개발 및 공급이 클라우드 환경에서 작동 하도록 설계 되었다. 그리고 유지보수가 간단하다.
  • No more ETL
    • TiDB는 하이브리드 OLTP/OLAP 아키텍처 이므로 더이상 ETL이 필요하지 않다 더쉽고 빠르게 사용자를 위한 새로운 가치를 창충 할 수 있다.
  • High availability
    • 너의 데이터 및 어플리케이션은 항상 켜져 있으며 지속적으로 이용이 가능하다. 

TiDB는 OLTP 및 OLAP 시나리오를 지원하도록 설계 되었다. 

TiDB Internals

Data Storage

Foreword

데이터베이스, 운영 체제 및 컴파일러는 3대 시스템으로 알려져 있으며 전체 컴퓨터 소프트웨어의 기초로 간주 됩니다. 데이터베이스는 비즈니스를 지원하며 어플리케이션 계층에 더 가깝습니다. 수십년의 발전을 거쳐 이 분야에서 발전이 계속 되고 있다.

많은 사람들이 다양하고 각기 다른 종류의 데이터베이스를 사용해왔지만, 분산 데이터베이스 개발 경험이 있는 사람은 적습니다. 데이터베이스 구현의 원칙과 세부 사항을 알면 자신의 기술 수준을 향상 시키는데 도움이 되며, 데이터베이스를 만들고 사용하는데 도움이 됩니다.

기술쪽에서 일할 때 가장 좋은 방법은 오픈소스 프로젝트에 깊이 참여 하는 것이라 나는 믿습니다. 데이터베이스도 예외가 아닙니다. 독립형(standalone) 데이터베이스 분야에 많은 훌륭한 오픈 소스 프로젝트가 있습니다. 그 중 MySQL 과 PostgreSQL이 가장 유명하며, 많은 사람들이 소스 코드를 읽고 있습니다. 그러나 분산 데이터베이스의 경우, 좋은 오픈 소스 프로젝트가 많지 않습니다. 그리고 TiDB는 그중 하나이다. 많은 사람들은,  특히 기술 자들은, 이 프로젝트에 참여 하기를 희망합니다. 그러나 분산형 데이터베이스의 복장성 때문에 많은 사람들이 전체 프로젝트를 이해하기가 어렵습니다. 그래서, 나는 TiDB의 원리를 설명하는 몇개의 기고를 작성하기를 계획하였고, 이는 SQL 인터페이스 이면에 있어서 보이지 않아 사용자들이 알고 싶어하는 많은 기술을 포함하고 있습니다.

Storing Data

데이터베이스를 저장하는 기장 기본적인 기능부터 시작하겠습니다. 데이터를 저장하는데 여러 가지 방법이 있으며 가장 쉬운 방법은 사용자가 보낸 데이터를 저장하기 위해 메모리에 데이터 구조를 만드는 것입니다. 예를 들면, 배열을 사용하여 데이터를 저장하고 데이터를 수신할 때 배열에 새항목을 추가합니다. 이 솔루션은 간단하고 기본 요구사항을 충족하며 우수한 성능을 제공합니다. 그러나 그것은 결점이 장점을 능가합니다. 그리고 가장 큰 문제는 모든 데이터를 메모리에 저장한다는 것입니다. 그러므로 서버가 중지되거나 다시 시작된다면 데이터가 손실됩니다. 데이터의 지속성을 달성하기 위해 예를 들면 비휘발성 저장 매체에 데이터를 저장할 수 있습니다. 우리는 디스크에 파일을 만들고 데이터를 받을 때, 파일에 새 레코드를 추가합니다. 이것은 내구성이 있는 저장소 솔루 션이다. 그러나 이것으로 충분치 않습니다. 디스크가 고장난 경우 어떻게 해야 합니까? 디스크의 불량 트랙을 피하기 위해 독립형 디스크에 RAID 구성 하여 사용 할 수 있습니다. 그러나 전체 기계가 다운 된다면 ? RAID는 안전하지 않습니다. 또 다른 솔루션은 저장 및 복제를 위해 네트워크로 데이터를 저장하는 하드웨어 또는 소프트웨어를 사용하는 것입니다. 그러나 문제는 레플리카 간의 일관성을 보장하는 방법입니다. 데이터의 온전함과 정확성을 확보하는 것이 기본 요구사항 입니다.  다음과 같은 문제들의 훨씬 더 까다로와 집니다.

  • 데이터베이스가 다중 데이터 센터의 재해 복구를 지원합니까?
  • 쓰기 속도가 충분히 빠릅니까?
  • 데이터가 저장 될 때 데이터를 읽는 것이 편리 합니까?
  • 저장된 데이터를 업데이트 하는 방법 ? 동시에 revision(record/mvcc)를 어떻게 처리 합니까?
  • 여러 개의 레코드를 atomic하게 수정 하는 방법?

이 모든 문제를 해결하기 어렵습니다. 그러나 우수한 데이터베이스 스토리지 시스템은 각각의 모든 것을 처리할 수 있어야 합니다. 이것을 위해, 우리는 TiKV를 개발하였습니다. TiKV에 대해 이야기 할 때 SQL에 대한 개념은 잊어버리고 성능과 안정성이 뛰어난 거대한 분산순차맵(distributed ordered Map)인 TiKV를 구현하는 방법에 중점을 두시기 바랍니다.

Key-Value

데이터 스토리지 시스템은 무엇보다 데이터 스토리지 모델을 결정해야 합니다. 즉, 데이터를 저장하는 포맷. TiKV는 Key-Value 모델을 선택하고 순서대로 순회(traverse)하는 솔루션을 제공합니다. 간단히 말하면, 키와값이 원본 바이트 배열인 거대한 Map으로 TiKV를 볼 수 있습니다. 이 Map에서, Key는 바이트 배열의 원시 이진 비트에 따라 비교 순서로 정렬됩니다. 다음 사항을 염두해 두어야 합니다.

  • Key-Value 쌍에 대한 거대한 맵이다.
  • 이 맵의 Key-Value 쌍은 키의 이진 시퀀스따라 정렬됩니다. 우리는 키의 위치를 찾고 다른 Key-Value 쌍에 Next 메서드를 사용할 수 있으며, 이 Key-Value 쌍은 모든 이전 값보다 큽니다.

내가 말하는 스토리지 모델과 SQL의 테이블 사이의 관계를 궁금해 할 것입니다. 하지만, 여기에서 나는 강조하고 싶습니다 : 그것들은 관계가 없습니다.

RocksDB

내구성이 있는 스토리지 엔진은 디스크에 데이터를 저장해야 하고, TiKV도 예외는 아닙니다. 그러나 TiKV는 디스크에 바로 데이터를 쓰지 않습니다. 대신에,  RockDB에 데이터를 저장하며, RocksDB가 데이터 스토리지를 책임집니다. 그 이유는 독립형 스토리지 엔진, 특히 고성능 독립형 엔진을 개발하는데 많은 비용이 소요되기 때문입니다. 당신은 모든 종류의 세세한 최적화를 필요로 합니다. 다행히 RocksDB는 우리의 모든 요구 사항을 충족시키는 뛰어난 오픈 소스 독립형 스토리지 엔진이라는 것을 알게 되었습니다. 게다가 페이스북 팀이 최적화를 유지함에 따라 많은 노력을 하지 않고도 강력하고 발전하는 독립형 엔진을 즐길 수 있습니다. 그러나 우리는 RocksDB에 몇 줄의 코드를 제공합니다. 이 프로젝트가 더 잘 진행 되기를 바랍니다.

RocksDB를 독립형 Key-Value 맵으로 간주 할 수 있습니다.

Raft

효과적이고 안정적인 로컬 스토리지 솔루션을 찾는 것이 전체 구현의 중요한 첫 번째 단계입니다. 이제는 더 어려운 문제에 직면해 있습니다. 단일 시스템이 고장났을 때 데이터의 온전함과 정확성를 확보하는 방법은 무엇입니까? 좋은 방법은 데이터를 여러 머신으로 복제하는 것입니다. 그 다음 한 대의 컴퓨터가 고장나면, 우리는 다른 기계에 레플리카를 가지고 있습니다. 그러나 복제 솔루션은 신뢰할 수 있고 효과적이어야 하며 유효하지 않은 레플리카의 상황을 처리 할 수 있어야 합니다. 그것은 어렵지만, Raft는 가능케합니다. Raft는 컨센서스 알고리즘이자 Paxos와 동급인데, Raft가 좀 더 이해하기 쉽습니다. 우리는 Raft을 구현하기 위해 수많은 최적화 작업을 수행 했으며, 자세한 내용은 수석 설계자인 Tang LIu가 작성한 이 블로그 글을 참조하십시요.

Raft는 컨세서스 알고리즘이며 세가지 중요한 기능을 제공 한다. 

  • Leader election (리더 선출)
  • Membership change (회원 변경)
  • Log replicaiton  (로그 복제)

TiKV는 Raft를 사용하여 데이터를 복제하며, 각 데이터 변경 사항은 Raft 로그로 기록될 것입니다. Raft 로그 복제 기능을 통해 데이터는 안전하고 신뢰성 있게 Raft 그룹의 여러 노드와 동기화됩니다.

요약하자면, 독립형 RockDB를 통하여, 우리는 디스크에 데이터를 빠르게 저장할 수 있습니다. Raft을 통하여 우리는 데이터를 기계 고장에 대비하여 여러 대의 기계로 복제할 수 있습니다. 데이터는 RocksDB가 아닌 Raft의 인터페이스를 통해 기록됩니다. Raft 구현 덕분에, 우리는 분산 Key-Value를 가지면 더 이상 기계 고장에 대하여 걱정할 필요가 없습니다.

Region

이 장에서 Region(Region)이라는 매우 중요한 개념을 소개하려고 합니다. Region은 뒤이어 설명할 메커리즘을 이해하는데 있어서 기초입니다. 시작하기 전에, Raft에 대해 잊어버리세요. 일단 모든 데이터가 하나의 레플리카만 가지고 있다고 가정해보세요.
앞서 언급했듯, TiKV는 거대하지만 순차 Key-Value Map으로 보입니다. 스토리지의 수평 확장성을 구현하기 위해, 우리는 여러 대의 컴퓨터에 데이터를 분산해야 합니다. Key-Value 시스템의 경우 여러 대의 컴퓨터에 데이터를 분산하는 두 가지 대표적인 솔루션이 있습니다. 하나는 해시를 만들어 해시값에 따라 상응하는 스토리지 노드를 선택합니다. 다른 하나는 Range를 사용하여 스토리지 노드에 연속적인 키의 세그먼트를 저장하는 것입니다. TiKV는 두 번째 솔루션을 선택하고 전체 Key-Value 공간을 여러 세그먼트로 나누었습니다. 각각의 세그먼트는 일련의 인접한 키로 구성되며 이런한 세그먼트를 "Region" 이라고 부릅니다. 데이터를 저장할 각 Region의 크기 제한이 있습니다.(기본값은 64M이며 크기를 조정할 수 있다.) 각 Region은 Startkey에서 EndKey까지의 왼쪽-종료-오른쪽-오픈 간격으로 설명 할 수 있습니다.

내가 말하는 Region은 SQL의 테이블과 아무런 관련이 없음을 상기하세요. 지금은 SQL을 잊고 Key-Value에 집중하세요.

데이터를 Region으로 나눈 후, 우리는 두 개의 작업을 수행할 것입니다.

  • Region은 데이터 이동의 기본 단위이며, 클러스터의 모든 노드에 데이터를 분산합니다. 또한 각 노드의 Region 수는 대략 동일해야 합니다.
  • Raft 복제Region 내 구성원(membership) 관리.

이 두 가지 작업은 매우 중요합니다.

첫번째 작업은, 데이터는 Key에 따라 여러 Region으로 분할되고, 각 Region의 모든 데이터는 하나의 노드에 저장됩니다. 시스템의 한 컴포넌트는 클러스터의 모든 노드에 Region을 고르게 분배할 책임이 있습니다. 한편으로는, 스토리지 용량의 수평 확장을 구현합니다. (언젠가 새로운 노드가 추가되면 시스템은 다른 노드에 Region을 자동으로 스케줄링 할 것이다.).  한편, 로드 밸런싱을 완료합니다.(한 노드가 많은 데이터를 갖고 있지만 다른 노드들이 적은 데이터를 갖고 있는 상황은 발생하지 않을 것이다). 이와 동시에, 상위 클라이언트의 데이터 접근을 보장하기 위해, 다른 컴포넌트는 노드들의 Region의 분포를 기록합니다. 다른 말로, 키의 정확한 Region과 키를 통해 배치된 Region의 노드를 질의할 수 있습니다. 위 두 컴포넌트에 대한 정보는 밑에서 설명하겠습니다.

두번째 작업을 봅시다. TiKV는 Region들의 데이터를 복제합니다. 이는 하나의 Region의 데이터는 "레플리카(Replica)"라는 이름을 가진 여러 레플리카를 갖게됨을 의미합니다. Raft는 레플리카간의 데이터 일관성을 유지하는데 사용됩니다. 한 Region의 여러 레플리카는 다른 노드에 저장되며, Raft Group으로 구성됩니다. 한 레플리카는 그룹 리더(Leader of Group) 역할을 하고 다른 레플리카는 팔로어(Follower:추종자) 역할을 합니다. 모든 읽기와 쓰기는 리더를 통해 수행되고, 그런 다음 리더는 팔로어에게 (쓰기를) 복제합니다.

다음 다이어그램은 지역 및 Raft Group에 대한 전체 그림을 보여 줍니다.

Region에서 데이터를 분산하고 복제할 때, TiDB는 어느 정도까지 재난 복구 능력을 가진 분산 Key-Value 시스템입니다. 사용자는 더 이상 저장 용량과 디스크 고장으로 인한 데이터 손실에 대해 걱정할 필요가 없습니다. 이것은 멋지지만 완벽하지 않습니다. 우리는 더 많은 기능이 필요합니다.

MVCC

많은 데이터베이스가 다중 버전 동시성 제어(MVCC)를 제공합니다. 그리고 TiKV도 마찬가집니다.  두 클라이언트가 MVCC 없이 Value를 업데이트한다고 가정하면, 데이터는 lock 되어야 할 것입니다. 분산환경 시나리오에서, 이로 인해 성능하락 및 교착 상태가 발생합니다. TiKV는 Key에 Version을 추가하여 MVCC를 구현하였습니다. TiKV의 데이터 레이아웃은 다음과 같이 볼 수 있습니다:

~~ Key1 -> Value
~~ Key2 -> Value
~~ ......
~~ KeyN -> Value

 

Value ~~ Key2 -> Value ~~ ...... ~~ KeyN -> Value]]>

MVCC에서 TiKV의 Key 배열은 다음과 같습니다.

~~ Key1-Version3 -> Value
~~ Key1-Version2 -> Value
~~ Key1-Version1 -> Value
~~ ......
~~ Key2-Version4 -> Value
~~ Key2-Version3 -> Value
~~ Key2-Version2 -> Value
~~ Key2-Version1 -> Value
~~ ......
~~ KeyN-Version2 -> Value
~~ KeyN-Version1 -> Value
~~ ......
Value ~~ Key1-Version2 -> Value ~~ Key1-Version1 -> Value ~~ ...... ~~ Key2-Version4 -> Value ~~ Key2-Version3 -> Value ~~ Key2-Version2 -> Value ~~ Key2-Version1 -> Value ~~ ...... ~~ KeyN-Version2 -> Value ~~ KeyN-Version1 -> Value ~~ ......]]>

Key의 여러 버전에 대해서, 우리는 더 큰 숫자의 버전을 먼저 쓴다.(키는 순차적인 배열(ordered array)). 이런 방법으로, 사용자 Key + 값으로 Value를 획득하면, Key와 version으로 Mvcc의 키를 만들 수 있다. 사용자는 직접 Seek(Key-Version)을 호출할 있다. 그리고 Key-version 보다 큰 위치 혹은 같은 위치를 찾을 수 있다.  For more detail, see MVCC in TiKV.

Transaction

TiKV의 처리는 퍼콜레이터 모델을 체택하고 많은 최적화를 하였다.  내가 말하고 싶은 것은 TiKV의 트랜젝션은 optimisitc lock 사용한다는 것이다. 실행 프로세스중에 쓰기 충돌을 감지 하지 못합니다. 오직 커밋 단계에서만 충돌이 날 것이다. 이전 커밋을 완료한 트랙젼션은 성공적으로 기록되고 다른 트랜잭션도 성공적으로 기록된다. 비즈니스의 쓰기 충돌이 심하지 않다면 이 모델에서의 성능을 좋다. 예를 들면, 큰 테이블에서 데이터의 일부 행을 무작위로 업데이트 하는데 효과적이다. 그러나 쓰기 충돌이 발생 한다면 성능은 좋지 않을 것이다. 많은 클라이언트가 동시에 몇 행을 Update하는 상황은 심각한 문제를 초래한다.

⚠️ 역주) 비관적 락킹 기법으로 바뀜.

 

ℹ️ 참고
낙관적 잠금(Optimistic locking)

낙관적인 잠금은 레코드를 업데이트 할때에 버전을 보고, 충돌나면 그 에러를 처리하고, 충돌나지 않으면 그냥 업데이트 한다.  비관적인 잠금은 레코드를 갱신하는 동시에 해당 컬럼을 잠그고, 트랜 잭션이 커밋되면 잠금을 푼다.
낙관적이라함은... 하면하고 말면 말고 이런거고,,비관적이라함은... 충돌할테니 먼저 잠그는 그런느낌... 왠지 데자뷰가 떠오른다..낙관적인 자신감, 비관적인 자괴감.. 아무튼..
Pessimistic Locking 은 비관적 잠금, Optimistic Locking 은 낙관적인 잠금으로 보통 번역된다. 저 두 가지의 용어가 쓰이는 경우는 보통 Multi-User, Multi-Transaction 상황에서 쓰이곤 한다. 두 가지의 용어가 어떠한 차이점을 갖는 지 다음의 경우를 생각해보자.
User1, User2 가 동시에 Table1의 Record1을 동시에 Update하는 상황을 가정하자.
1. Locking이 없는 경우
  • User1이 Record1을 읽는다.(read)
  • User2가 Record1을 읽는다.(read)
  • User1이 Record1을 수정한다.(update)
  • User2가 Record1을 수정한다.(update)
User2는 User1의 수정이 끝나기 전 데이터를 읽어들였기 때문에 최종적으로는 User1의 update는
무시되고 User2의 update만 남게된다. 이런 경우를 lost update라고 부른다.

2. Pessimistic Locking의 경우
  • User1이 Record1을 읽고 있는 동안 해당 record에 lock을 걸어둔다.(update 때문에)
  • User2가 Record1를 읽으려고 시도하지만 lock이 걸려있기 때문에 User1의 Update가 수행이 완료될 때 까지 기다린다.
  • User1이 Record1을 수정한다.(update)
  • User2는 이제 Record1을 읽을 수 있고 User1의 Update가 이루어진 데이터를 읽을 수 있다.
  • User2가 Record1을 수정한다.(update)
이로써 lost update문제는 해결된다. 이와 같은 방식의 문제는 동시성(concurrency)이다. User1의 데이터 update 작업이 끝나지 않으면 User2는 Record1을 읽을 수 조차 없다. 이러한 접근법은 실제로는 구현된 적은 없다.
3. Optimistic Locking의 경우
  • User1이 Record1을 읽는다. (read) 그와 동시에 timestamp(1)를 남긴다.
  • User2가 Record1을 읽는다. (read) 그와 동시에 timestamp(1)를 남긴다.
  • User1이 Record1을 수정하려고 시도한다.(update). timestamp(1)이 데이터 베이스의 timestamp(1)와 일치한다.   따라서 데이터 수정 작업을 수행하고 timestamp(1)을 timestamp(2)로 수정한다.
  • User2가 Record2를 수정하려고 시도한다.(update). User2가 가지고 있는 timestamp(1)을 현재 데이터베이스의 timestamp(2)와 일치하지 않는다. 따라서 error를 리턴받는다.
  • User2는 반드시 Record1을 다시 읽어서(re-read) User1이 변경했던 데이터와 timestamp의 변경사항을 읽어들인다. 그 후에 다시 update를 재시도 한다.(re-update)

위의 경우 락을 잡는 과정이 생략되어 있다. 락을 잡고 다시 타임스탬프(버전)를 비교후 같다면, 업데이트 한다. 말그대로 낙관적 '락킹'이다. 팁으로써, 효율성 및 안정성을 위해서 타임스탬프를 변경하는 작업이 선행되어야 한다.


다시 생각해보면 비관적인 잠금(Pessimistaic Locking) 말그대로 동시에 데이터 수정 작업이 진행될 경우를 가정하고, 아예 데이터가 꼬이는 것을 방지한다. 마치 “이 데이터는 다른 사람이 수정할 지도 몰라. 그러니깐 반드시 잠궈놔야 해” 라고 접근하는 방식이라 비관적인 방식인 것이다. 반대로 낙관적인 잠금(Optimistic Locking)의 경우는 “이 데이터가 혹시 꼬일지라도 일단은 다 update하는 게 맞아. 혹시 동시에 작업이 수행되면 User에게 노티해주지 뭐..”라고 접금하는 방식이기에 낙관적인 방식인 것이다.
✅ 참고자료

Miscellaneous

지금까지, 나는 TiKV 기본 컨셉 및 세부사항에 대하여 설명하였고, 분산 및 트랜잭션 Key-Value 엔진의 계층 구조 및 다중 데이터 센터의 재해 복구를 어떻게 구현하는지에 대해 설명하였다. 

Computing(Query Processing)

이전 절(스토리지)에서 TiDB가 데이터를 저장하는 방식을 소개했습니다. 또한 TiKV의 기본 개념이기도 합니다. 이 기고는, TiDB가 데이터를 저장하기 위해 최하위 계층 Key-Value을 어떻게 사용하는지, 또한 관계형 모델과 Key-Value 모델을 어떻게 매핑하는지, 어떻게 SQL 컴퓨팅을 수행하는지 자세히 설명할 것입니다

Mapping the Relational Model to the Key-Value Model

관계형 모델을 테이블과 SQL 문으로만 단순화 합시다. 우리가 생각해야할 것은 Key-Value 구조상에서 테이블을 저장하고 SQL문을 실행하는 방법입니다. 

다음과 같은 테이블이 있다고 가정합시다:

CREATE TABLE User
{
    ID int,
    Name varchar(20),
    Role varchar(20),
    Age int,
    PRIMARY KEY (ID),
    Key idxAge (age)
};

 

SQL 과 Key-Value 구조 사이의 큰 차이점을 감안한다면, SQL을 Key-Value에 편리하고 효율적으로 매핑하는 방법은 매우 중요합니다. 기사는 가장먼저, 1) 데이터 컴퓨팅의 요구사항 및 특징을 살펴보는데, 이것은 2) 매핑 솔루션이 좋은지 아닌지를 결정하는데 필수적입니다.

테이블에는 세 부분의 데이터가 있습니다.

  1. 테이블의 메타데이터
  2. 테이블의 로우
  3. 인덱스 데이터

이 문서에서는 메타데이터에 대해 설명하지 않습니다.

데이터는 row-oriented나 column-oriented로 저장될 수 있으며, 둘 다 장점과 단점이 있습니다. TiDB의 기본 목표는 온라인 트랜잭션 처리(OLTP) 입니다. OLTP는 한 로우의 데이터를 신속하게 읽기, 저장, 갱신 및 삭제합니다. 그러므로 로우 저장 방식이 더 좋습니다.

TiDB는 Primary Index 와 Secondary Index 지원합니다. Index는 더 빠른 쿼리, 높은 쿼리 성능 및 제약 조건을 위한 것입니다. 두 가지 형식의 쿼리가 있습니다.

  • Point Query, 쿼리에서 Pimary Key, Unique Key에 이퀄 조건식을 사용 하는 경우,
    • ex: select name from user where id=1;, 인덱스를 통해 로우를 찾습니다.
  • Range Query,
    • ex :  select name from user where age > 30 and age < 35;, IndexAge 통해 나이가 20에서 30사이인 데이터를 쿼리 합니다.
    • 인덱스의 두가지 타입이 있습니다. unique index 와 non-unique index. 둘 다  TiDB에서는 지원된다.

저장된 데이터의 특징을 분석 후, 데이터를 조작하기 위한 수행하기 위해 무엇을 해야해야 하는 가에 대한 작업으로 넘어 갑니다. Insert/Upate/Delete/Select 문은 각각 무엇을 해야 할까요?

  • Insert 문은 Row를 Key-Value에 쓰고 인덱스 데이터를 만듭니다.
  • Update 문은 Row를 갱신 및 필요한 경우 Index Data를 갱신합니다.
  • Delete 문은 Row 및 Index 데이터를 삭제합니다.
  • 4가지 중, Select 문장은 가장 복잡한 상황을 처리합니다.
    • 한줄의 데이터를 쉽고 빠르게 읽기. 이 경우 각 행에는 ID(명시적 또는 암시적)가 있어야 합니다.
    • 연속적으로 여러 행을 읽기. (select * from user;)
    • 인덱스를 통한 데이터 읽기. Point Query 또는 Range Query에서 인덱스를 활용합니다.

전역적으로 순차적이며 분산된 Key-Value 엔진은 위의 요구 사항을 충족시킵니다. '전역적으로 순차화되어 존재한다'는 특징은 몇 가지 문제를 해결하는데 도움이 됩니다.

두가지 예를 들면 다음과 같습니다:

  • 데이터의 행을 빠르게 획득하기: 특정 또는 일부 키를 만들 수 있다고 가정하면, 해당 행을 지정할 때 TiKV에서 제공하는 Seek() 메소드를 사용하여 이 데이터 행을 빠르게 찾을 수 있습니다.
  • 전체 테이블 스캔하기: 테이블을 키 범위에 매핑할 수 있으면 StartKey에서 EndKey까지 스캔하여 모든 데이터를 가져올 수 있습니다. 인덱스 데이터를 조작하는 방법은 비슷합니다.

TiDB에서 이 작업이 어떻게 동작하는지 봅시다.

TiDB는 각각의 테이블에 TableID를, 각각의 인덱스에는 IndexID를, 그리고 각각의 로우에는 RowID를 할당합니다. 테이블에 Integer Primary Key을 가지고 있다면, 이 값이 RowID 로 사용될 것입니다. TableID는 전체 클러스터에서 고유하며, IndexID 및 RowID는 테이블 안에서 고유합니다. 이 ID는 모두 int64입니다.

각 데이터 행은 다음 규칙에 따라 Key-Value 쌍으로 인코딩 됩니다.

Key: tablePrefix{tableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]

 

Key의 tablePrifix 및 rowPrefixSep는 특정 문자열 상수이며 Key-Value 공간에서 다른 데이터를 구별하는데 사용됩니다.

인덱스 데이터는 다음 규칙에 따라 Key-Value으로 인코딩 됩니다.:

Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: RowID

 

위의 인코딩 규칙은 고유 인덱스에 적용되지만, 고유하지 않은 인덱스에 대해서는 고유키를 생성할 수 없습니다. 그 이유는 인텍스의 tablePrefix{tableID}_indexPrefixSep{indexID} 가 동일하기 때문입니다. 따라서, 우리는 non-unique index를 인코딩하기 위해 몇가지 변경을 했습니다.

Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_RowID
Value: null

 

이 방법은, 각 데이터 행의 고유키를 작성할 수 있습니다. 위의 규칙에서,  Key의 모든 xxPrefix는 다른 유형의 데이터 간의 충동을 피하기 위해 네임스페이스를 구별하는 기능이 있는 문자열 상수입니다.

var(
  tablePrefix     = []byte{'t'}
  recordPrefixSep = []byte("_r")
  indexPrefixSep  = []byte("_i")
)

 

로우 또는 Index Key 인코딩 솔루션에는 동일한 prefix가 있습니다. 특히 테이블의 모든 행은 동일한 prefix를 가지고 있습니다(인덱스의 데이터도 마찬가지). 동일한 prefix를 가진 데이터들은 TiKV의 Key 공간에 함께 정렬(인접하게 배치)됩니다. 다른 말로, 비교 관계를 변경하지 않은 채, suffix의 인코딩 솔루션을 신중하게 설계하면, 로우 또는 인덱스 데이터는 순서대로 TiKV에 저장될 수 있습니다. 인코딩 전후에 관계가 변함없이 유지하는 솔루션을 Memcomparable 이라고 합니다. 모든 value의 타입에 대해서는, 인코딩 전의 두 객체의 비교 결과는 인코딩 후의 바이트 배열의 결과와 일치합니다. (참고: TiKV의 Key, Value 둘 다 메모리상의 바이트 배열이다). (상세한 TiDB의 인코딩은 codec package를 참조하라). 이 인코딩 솔루션을 채택할 때, 테이블의 모든 행 데이터는 RowID 순서에 따라 TiKV의 공간에 정렬됩니다. 인덱스의 ColumnValue 순서에 따르는 인덱스 데이터도 마찬가지 입니다.

이제 이전 요구 사항과 TiDB의 매핑 솔루션을 고려하여 솔루션의 실현 가능성을 검증합니다.

  1. 첫째로, 매핑 솔루션을 통해 Row 및 Index 데이터를 Key-Value 데이터로 변환한 다음, 각 행과 각 인덱스 데이터에 고유한 Key가 있는지 확인합니다.
  2. 두번째로, 우리는 Point query 와 Range  Query 모두 적합하게, 이 매핑 솔루션을 사용하여 일부 행 또는 일부 Index에 해당하는 key를 쉽게 만들 수 있어야 합니다.
  3. 마지막, 테이블 일부 제약 조건을 보장 할 때, 상응하는 제약 조건을 만족 여부를 결정할 수 있는 특정 Key를 생성하고 그 특정 Key 존재를 확인할 수 있어야 합니다.

지금까지, 우리는 테이블 Key-Value에 매핑하는 방법을 다루었습니다. 여기에 동일한 테이블 구조를 가진 사례가 하나 더 있습니다. 테이블에 3개의 데이터 행이 있다고 가정합시다.

1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 20
3, "PD", "Manager", 30

첫번째, 각 데이터의 Row는 Key-Value 맵핑되었을 것이다. 이 테이블에는 int 기본 키가 있으므로 RowID 값이 이 기본 키의 값입니다. 이 테이블의 TableID가 10이고 다음과 같다고 가정합니다.

1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 20
3, "PD", "Manager", 30

 

Primary Key외에 이 테이블에 Index가 있다고 합시다. index id가 1이라고 가정하면 그 데이터는 다음과 같습니다. (✅ 역주: 10, 20, 30의 값이 있는 것으로 보아, 마지막 컬럼에 대한 인덱스다.)

t10_i1_10_1 --> null
t10_i1_20_2 --> null
t10_i1_30_3 --> null

앞서 설명한 인코딩 규칙은 위의 예를 이해하는데 도움이 됩니다. 우리는 이 매핑솔루션을 채택한 이유와 목적을 깨닫기 바랍니다.

Metadata Management

테이블의 데이터 및 인덱스가 Key-Value 와 매핑되는 방법을 설명하였고, 이 절에서는 메타데이터 스토리지에 대하여 소개합니다.

데이터베이스와 테이블 둘 다 메타데이터를 가지며, 그것의 정의와 다양한 속성을 참조하고 있습니다. 이 모든 정보는 TiKV에 영속적으로 저장되어야 합니다. 각각의 데이터베이스/테이블은 고유 ID를 가지며, ID는 고유한 식별자로 사용됩니다. Key-Value로 인코딩 할 때, 이 ID는 접두사는 m_Key로 인코딩 됩니다. 이 방법으로 Key가 생성되고, 상응하는 Value는 직렬화된 메타데이터를 저장합니다.

이와 별개로, 특별한 Key-Value는 현재 스키마 정보의 버전을 저장합니다. TiDB는 google F1의 온라인 스키마 변경 알고리즘을 사용합니다. 백그라운드 스레드는 TiKV에 저장된 스키마 버전이 변경되었는지 지속적으로 체크한다. 만일 변경되었다면, 특정 기간내에 갱신내역을 받을 수 있도록 관리합니다. 자세한 내용은 를 참조하세요.

Architecture of SQL on Key-Value

다음 다이어그램은 TiDB의 전체 아키텍처를 보여줍니다.

TiKV 클러스터의 주요 기능은 데이터를 저장하는 Key-Value 엔진이며, 마직막 컬럼에서 철저히 소개합니다. 이 기사에서는 SQL 레이어, 즉 TiDB 서버에 중점을 둡니다. 이 계층의 노드는 상태를 저장하지 않으며(stateless), 데이터를 저장 하지 않으며, 그리고 노드들의 각각은 완전히 동형입니다. TiDB Server는 사용자 요청을 처리하고 SQL 연산 로직을 실행합니다.

SQL Computing

SQL과 Key-Value 와의 매핑 솔루션은 관계형 데이터를 저장하는 방법을 보여 줍니다. 우리는 쿼리 요청이 있을때 이 데이터를 어떻게 사용하는지를 이해해야 합니다. 다른 말로, 쿼리문이 맨 아래 계층에 저장된 데이터에 액세스 하는 방법을 이해해야 합니다.

가장 쉬운 방법은 내가 이전에 소개한 매핑 솔루션을 통해 SQL 쿼리를 Key-Value 쿼리에 매핑한 다음, Key-Value 인터페이스를 통해 해당 데이터를 가져옵니다. 그런 다음, 연산을 실행합니다.

SQL문, Select count(*) from user where name="TiDB";에 관해서, 우리는 테이블의 모든 데이터를 읽고 name 필드의 값이 "TiDB"인지 아닌지 확인합니다. 그렇다면 그 행을 반환한다. 연산이 아래에 나열된 Key-Value 연산 프로세스로 전송됩니다.

  • Create Key Range: Table의 모든 RowsID는 [0,MaxInt64) 범위 안에 있다, 0과 MaxInt64 및 Row의 Key 인코딩 규칙을 사용하여 왼쪽-닫음 오른쪽-열림 간격[StartKey,EndKey)을 만든다.
  • Scan Key Range: 이전에 생성 된 키 범위에 따라 TiKV에서 데이터를 읽는다.
  • Filter the data: 각각의 데이터 행을 읽을 때 name="TiDB" 조건으로 평가한다. 만약 결과가 참이라면 이 행을 반환한다. 만약 거짓이라면 이 행을 SKIP한다.
  • Evaluate Count: 요구 사항을 충족시키는 각 행에 대해 Count 값을 더하다.
step1. TiDB에서 start key, end key 만든다.
step2. start key 따라 데이터를 읽는다.
step3. 가져온 데이터의 행을 읽고 name="TiDB" 만 참이라고 행을 반환 
step4. count() 행을 더한다.

위 프로세스에 대해, 아래 다이어그램을 참조하세요:

이 솔루션은 여전히 다음과 같은 단점이 있지만 작동합니다.

  1. 데이터를 스캔할 때 각 행은 Key-Value 연산을 통해 TiKV에서 읽어야 합니다. 그러므로, 최소한 하나의 PRC 오버헤드가 있습니다. 많은 양의 데이터를 스캔하는 경우 오버헤드가 커집니다.
  2. 모든 행에 적용할 수 없습니다. 조건을 만족하지 않는 데이터는 읽을 필요가 없습니다.
  3. 조건을 만족시키는 행의 값은 의미가 없습니다. 여기에서 필요한 것은 단지 행의 수입니다.

Distributed SQL Operation

위의 단점을 피하는 것은 단순하다. 

  1. 첫번째, 막대한 RPC 호출을 막기 위해, 스토리지 노드 가까이에서 연산해야 합니다.
  2. 그런 다음 필터를 스토리지 노드로 푸시 다운 해야 합니다. 이 경우, 유효한 행만 반환될 것입니다. 그리고 무의미한 네트워크 전송은 피할 수 있습니다.
  3. 마지막으로, 우리는 사전 집계를 위해 aggregation 함수 및 group by를 푸시 다운할 수 있습니다. 각 노드는 단지 하나의 카운트 값을 반환하며, TiDB-Server가 모든 값을 합산 합니다.

다음의 스케치는 데이터가 레이어별로 어떻게 반환 되는지 보여준다.

 

Architecture of the SQL Layer

이전 절에서는 SQL 레이어의 일부 기능을 소개하였습니다. SQL 문을 처리하는 방법에 대한 기본 개념을 갖기를 바랍니다. 사실은, TiDB의 SQL 레이어는 매우 복잡하고, 많은 모듈과 레이어를 가지고 있습니다. 다음 다이어그램은 모든 중요한 모듈과 호출 관계를 보여줍니다.

SQL의 요청은 TiDB-Server에 직접 또는 로드 밸런서(Load Balancer)를 통해 전송 됩니다. 이후, 요청 내용을 확인하기 위해 MySQL 프로토콜 패킷의 구문을 분석합니다(✅ 역주: Protocol Decode). 그 다음, syntax parsing하고, 쿼리 플랜을 만들고 최적화 한 후, 데이터에 엑세스하고 처리하기 위해 쿼리 플랜을 실행합니다. 모든 데이터는 TiKV Cluster 안에 저장됩니다. 이러한 과정중에, TiDB-Server는 데이터에 액세스 하기 위해 TiKV-Server와 상호 작용해야 합니다. 마지막으로, TiDB-Server는 질의 결과를 사용자에게 반환합니다.

ℹ️ 참고

 

실제 실행은 executor 디렉토리 하부에 있는 자료구조에서 실행된다.

executor/compiler.go

48 // Compile compiles an ast.StmtNode to a physical plan.
49 func (c *Compiler) Compile(ctx context.Context, stmtNode ast.StmtNode) (*ExecStmt, error) {
50     if span := opentracing.SpanFromContext(ctx); span != nil && span.Tracer() != nil {
51         span1 := span.Tracer().StartSpan("executor.Compile", opentracing.ChildOf(span.Context()))
52         defer span1.Finish()
53         ctx = opentracing.ContextWithSpan(ctx, span1)
54     }
55
56     infoSchema := infoschema.GetInfoSchema(c.Ctx)
57     if err := plannercore.Preprocess(c.Ctx, stmtNode, infoSchema); err != nil {
58         return nil, err
59     }
60     stmtNode = plannercore.TryAddExtraLimit(c.Ctx, stmtNode)
61
62     finalPlan, names, err := planner.Optimize(ctx, c.Ctx, stmtNode, infoSchema)
63     if err != nil {
64         return nil, err
65     }
66
67     CountStmtNode(stmtNode, c.Ctx.GetSessionVars().InRestrictedSQL)
68     var lowerPriority bool
69     if c.Ctx.GetSessionVars().StmtCtx.Priority == mysql.NoPriority {
70         lowerPriority = needLowerPriority(finalPlan)
71     }
72     return &ExecStmt{
73         GoCtx:         ctx,
74         InfoSchema:    infoSchema,
75         Plan:          finalPlan,
76         LowerPriority: lowerPriority,
77         Text:          stmtNode.Text(),
78         StmtNode:      stmtNode,
79         Ctx:           c.Ctx,
80         OutputNames:   names,
81     }, nil
82 }

 

Summary

지금까지는 SQL의 관점에서 데이터를 저장하고 조작에 사용하는 방법을 알았습니다. 최적화 원칙 및 분산 실행 프레임워크의 세부 사항과 같은 SQL 레이어에 대한 정보가 앞으로 소개 될 것입니다. 다음 기사에서는 PD(Placement Driver)에 대한 정보, 특히 클러스터 관리 및 일정에 대해 설명합니다. 이것은 TiDB를 사용할 때 보이지 않지만 전체 클러스터에 중요한 것을 볼 수 있기 때문에 흥미로운 부분입니다.

Scheduling

why scheduling

TiDB internals의 첫번째 블로그(storage)에서, 우리는 TiKV 클러스터가 TiDB 데이터베이스의 분산 KV 스토리지 엔진이라는 것을 알고 있습니다. 데이터는 Region에서 복제 및 관리 되며(✅ 역주: 복제는 Region 단위로), 각 Region은 다른 TiKV 노드들에 분산된 여러 개의 레플리카가 있습니다. 이 레플리카 사이에서, (Raft) 리더는 읽기/쓰기를 담당하고 팔로어는 리더가 전송한 Raft 로그를 동기화합니다. 이제, 다음의 질문에 대하여 생각해봅시다.

  • 같은 Region의 여러 레플리카가 다른 노드에 분산되도록 하는 방법은 무엇입니까? 또한, 하나의 머신에서 여러 TiKV 인스턴스를 시작하면 어떻게 됩니까?
  • TiKV 클러스터가 (재해 복구에 대비하여) 다중 사이트 배포를 수행할 때, 한 데이터 센터가 정전이 발생하더라도 Raft Group의 여러 레플리카가 손실되지 않도록 어떻게 보장합니까?
  • TiKV 클러스터에 있는 다른 노드의 데이터를 새로 추가된 노드로 이동하는 방법은?
  • 노드가 실패하면 어떻게 됩니까? 전체 클러스터는 무엇을 해야 합니까? 만약 노드가 일시적으로 실패했다면(ex: service restarting) 어떻게 취급해야 하나요? 오랫동안 실패한 것은 어떨까요?(예, disk failure 또는 data 손실) (⚠️ 중요: ✅ 역주: 실패의 정의를 묻고 있다. CN 환경에서는 의도된 장애도 장애라 취급할 수 있으며, 기간에 따른 장애를 분류하고 각각의 처리방법을 개발자에게 요구하고 있다.)
  • Raft Group에 N개의 레플리카가 요구된다고 가정합시다. 하나의 Raft Group은 레플리카가 불충분 하거나(e.g 노드 실패 혹은 레플리카 손실) 혹은 너무 많을 수 있습니다(예 : 한 번 실패한 노드 기능이 다시 작동하여 자동으로 클러스터에 추가). (레플리카) 개수를 스케줄링하는 방법은?
  • 리더에 의해 읽기/쓰기가 수행되는데, 모든 리더가 특정 몇몇 노드에 액세스하는 경우 클러스터에 어떤 일이 발생합니까? (✅ 역주: 워크로드를 밸런싱하는게 필요하다)
  • 모든 Region에 접근할 필요가 없으며, 그리고 핫스팟은 아마 몇몇 Region일 것입니다. 이 경우 우리는 무엇을 해야 합니까?
  • 클러스터는 로드 밸런싱 중에 데이터 마이그레이션을 해주어야 합니다(✅: 핫백업과 비슷한 개념) 이러한 종류의 데이터 마이그레이션은 실질적인 네트워크 대역폭, disk IO 및 CPU 와 온라인 서비스에 영향을 미치는가?

위의 질문을 하나씩 해결 하는 것은 쉽습니다. 그러나 한번 섞여지면 해결하기 어렵습니다. 몇몇의 질문은 하나의 Raft 그룹의 내부 상황을 고려할 필요가 있어 보입니다. 예를 들면, 레플리카를 추가할지 말지 여부는 레플리카 개수에 의해 결정됩니다. 그러나, 실제로 어디에 레플리카를 추가하는 것은 전역 뷰(global view)가 필요합니다. 전체 시스템은 동적으로 변하고 있습니다. Region 분리, 노드 조인, 노드 실패 그리고 핫스팟 접근과 같은 상황은 지속적으로 발생합니다. 또한 스케줄링 시스템은 최상의 상태로 나아가야 합니다. 스케줄링하고, 전역 정보를 구성하고, 지배(master)할 수 있는 컴포넌트가 없으면, 이러한 요구를 충족하기가 어렵습니다. 그래서, 우리는 시스템을 모든 상황을 제어하고 조정할 수 있는 중심 노드가 필요합니다. 따라서 PD(Placement Driver: 배치 드라이버)모듈이 제공됩니다.

The Requirements of scheduling

나는 이전에 언급한 질문목록을 분류 및 정렬 하였습니다. 일반적으로, 두 타입이 있습니다.

  • 분산 및 고가용성 스토리지 시스템은 다음 요구 사항을 만족시켜야 합니다.
    • 정확한 개수의 레플리카
    • 레플리카는 다른 머신으로 분산되어야 합니다.
    • 노드를 추가한 후, 다른 노드들의 레플리카는 마이그레이션 될 수 있다.
    • 노드가 오프라인이면, 해당 노드의 데이터는 마이그레이션 되어야 한다.
  • 좋은 분산 시스템은 다음과 같은 최적화가 필요합니다.
    • 클러스터에 균형 잡힌 리더 분포.
    • 각 노드의 균형된 스토리지 용량.
    • 핫스팟 접속에 대한 균형된 분산.
    • 온라인 서비스에 영향을 주지 않기 위해 균형의 속도를 제어.
    • 노드의 상태를 관리
      • 노드를 수동으로 온라인/오프라인 전환
      • 실패한 노드를 자동으로 오프라인 전환.

첫 번째 유형의 요구 사항이 충족되면, 시스템은 다중 레플리카 재해 복구, 동적 확장성, 노드 장애 허용, 자동 재해 복구를 지원합니다.  두 번째 유형의 요구 사항이 충족되면, 시스템 부하가 좀더 균형을 맞추며, 관리되기 쉬워집니다. 이러한 요구를 충족시키기 위해서 일단 충분한 정보를 수집해야하는데, 각  노드의 상태, 각 Raft 그룹의 정보 및 비즈니스 액세스 및 연산의 통계 같은 정보들입니다. 그런 다음, PD가 이 정보와 스케줄 정책에 따라 스케줄 플랜을 공식화(formulate)할 수 있도록 우리는 몇가지 정책을 설정해야 합니다.

ℹ️ 역주
스케줄 플랜을 공식화하여 도출하는 주체는 PD라 여겨지며, 이를 기저로 원문을 번역하였음. 원문은 아래와 같다.

Then we should set some policies for PD to formulate a schedule plan to meet the previous requirements according to this information and the schedule policy.

The Basic Operations of Scheduling

스케줄링의 기본 동작은 정말 간단합니다. 다른 말로, 스케줄 정책을 충족시키기 위해 무엇을 할까 입니다. 이것은 전체 스케줄러의 핵심입니다. 앞서 언급한 스케줄러 요구사항은 복잡해 보인다. 그러나 3개의 조작으로 일반화할 수 있습니다.

  • 레플리카 추가.
  • 레플라카 삭제
  • Raft Group의 다른 레플리카들에게 리더의 역할을 전송.

Raft 포로토콜은 이러한 요구 사항들에 부합합니다:
 AddReplica, RemoveReplica, 그리고 TransferLeader 명령은 세가지 기본 오퍼레이션을 지원한다.

Information Collection

스케줄링은 전체 클러스터의 정보 수집에 달려있습니다. 간달히 말해서, 우리는 각 TiKV 및 각 Regioin 상태를 알아야 합니다. TiKV 클러스터는 두 종류의 정보를 PD에 보고 합니다.

  • 각 TiKV 노드는 정기적으로 노드의 전반적인 정보를 PD에 레포팅 합니다.

TiKV Store와 PD 사이에는 하트비트가 있습니다. 한편으로는, PD는 하트비트를 통해 각 Store가 활성 상태인지 또는 새로 추가 된 Store가 있는지 확인합니다. 또 한편으로, 하트비트는 스토어의 상태 정보를 전송하는데, 포함된 정보중 핵심은,

  • 총 disk 용량
  • free disk 용량
  • Region의 개수
  • 데이터 기록 속도
  • 송수신된 스냅샵의 개수. (레플리카는 스냅샵을 통해 데이터를 동기화 한다)
  • 과부하인지 여부
  • 라벨 정보 (레이블은 계층적 관계가 있는 일련의 태그들입니다)
  • 각 Raft Group의 리더는 PD에게 정기적으로 레포팅합니다.

각 Raft 그룹의 리더와 PD는 하트비트와 연결되어 있으며, 아래 사항이 포함되어 있는 Region의 상태를 보고합니다.

  • Leader의 위치
  • 팔로어의 위치
  • 오프라인 레플리카의 수
  • 데이터 읽기/쓰기 속도

이 두 종류의 하트비트를 통해 PD는 전체 클러스터 정보를 수집한 다음 의사 결정을 내립니다. 또한 PD는 관리 인터페이스를 통해 추가 정보를 획득하므로 보다 정확한 의사 결정을 내릴 수 있습니다. 예를들어, Store의 하트비트가 중단되면, PD는 이것이 일시적인지 또는 영구적인지 여부를 알 수 없습니다. PD는 일정 기간 동안만 기다릴 수 있습니다 (기본적으로 30 분). 여전히 하트비트가 없다면, PD는 Store가 오프라인이고 Store의 모든 Region을 다른 Region으로 옮겨야한다고 간주 합니다. 그러나 만일 운영 스태프가 기계를 수동으로 오프라인으로 전환한다면, 그는 관리 인터페이스를 통해 Store를 사용할 수 없다고 PD에 알려야 합니다. 이 경우 PD는 즉시 저장소의 모든 Region을 이동시킵니다.

The Policy of Scheduling

정보를 수집 한 후, PD는 구체적인 일정 계획을 작성하는 몇 가지 정책을 필요로 합니다.

1. 한 Region 에 있는 레플리카의 수는 정확해야 합니다.

PD는 Region의 레플리카 수가 Region Leader의 하트비트를 통해 요구 사항을 충족시키지 못하는 것을 발견하면, 레플리카의 Add/Remove 오퍼레이션을 통해 그 개수를 수정합니다. 다음과 같은 경우에 발생할 수 있습니다.

  • 노드가 드랍되고 모든 데이터가 손실되어, 일부 Region에서 레플리카가 부족한 경우.
  • 드랍된 노드가 다시 기능하고, 자동으로 클러스터에 조인하는 경우. 이 경우 중복된 레플리카가 있어 제거해야합니다.
  • 관리자가 레플리카 정책과 max-replicas 설정을 수정한 경우.

2. Raft Group의 여러 레플리카는 같은 위치에 있으면 안됩니다.

동일한 노드가 아니라 동일한 위치임을 주의하십시오. 일반적으로 PD는 다수의 레플리카가 동일한 노드에 있지 않을 것을 보장하여, 노드가 실패할 때 많은 레플리카가 손실되는 문제를 피할 수 있습니다. 실제 배포 시나리오에서는 다음 요구 사항이 발생할 수 있습니다.

  • 여러 노드가 동일한 머신에 배치된다.
  • TiKV 노드는 여러 서버에 분산된다. 서버의 전원이 꺼지더라도 시스템을 계속 사용할 수 있다.
  • TiKV 노드는 여러 IDC에 분산된다. 데이터 센터가 정전되더라도, 시스템을 계속 사용할 수 있다.

본질적으로, 당신이 필요로하는 것은 공통적인 위치 속성을 가지는 노드, 최소한의 결함 허용 단위를 구성하는 노드입니다. 이 노드 단위 내부에는 Region의 여러 레플리카가 공존하지 않기를 바랍니다. 이 때, 위치 식별자가 될 라벨을 지정하여, 노드에 라벨을 설정하고 PD안에 위치 라벨을 설정할 수 있습니다. 레플리카를 분산할 때, Region의 여러 레플리카를 저장할 노드는 동일한 위치 식별자를 갖지 않습니다.

3.  레플리카는 여러 Store에 걸쳐 균등하게 분산됩니다.

각 레플리카의 데이터 저장 용량이 고정되어 있으므로, 각 노드의 레플리카 수의 균형을 유지하면 전체 부하가 균형을 이루게 됩니다.

4. 리더의 수는 여러 Store에 걸쳐 균등하게 분산됩니다.

Raft 프로토콜은 리더를 통해 읽고 씁니다. 따라서 컴퓨팅 부하는 주로 리더에 배치됩니다. 따라서 PD는 여러 저장소에 리더를 배포하는 작업을 관리합니다.

5. 핫스팟의 수는 여러 Store에 걸쳐 균등하게 분산됩니다.

(스케줄링) 정보를 전송할 때, 각 Store 및 Region 리더는 Key의 읽기/쓰기 속도와 같은 현재 액세스 부하 정보를 전달합니다. PD는 핫스팟을 검사하여 노드에 분산시킵니다.

6. 각 Store의 저장 공간 점유율은 대략(roughly) 동일해야 합니다.

각 Store는 시작할 때 Capacity 매개 변수를 지정하는데, 이 Store의 저장 공간 제한을 나타냅니다. 스케줄링 할 때, PD는 노드의 나머지 공간을 고려합니다.

7. 온라인 서비스에 영향을 미치지 않도록 스케줄링 속도를 제어해야 합니다.

스케줄링 작업은 CPU, 메모리, 디스크 I / O 및 네트워크 대역폭을 소비합니다. 따라서 온라인 서비스에 큰 영향이 없도록 해야 합니다. PD는 진행중인 작업의 수를 제어하며, 기본 속도는 보존적(conservative)입니다. 스케줄링 속도를 높이려면 (서비스 업그레이드를 중지하고, 새 노드를 추가하고, 가능한 한 빨리 스케줄링이 되길 바란다면 등), pd-ctl을 통해 수동으로 가속화 할 수 있습니다.

8. 수동으로 오프라인 노드 지원

pd-ctl을 통해 수동으로 노드를 오프라인 상태로 만들 때, PD는 특정 속도제한(rate control) 내에서 노드의 데이터를 다른 노드로 이동시킵니다. 그런 다음 노드를 오프라인으로 설정합니다.

The implementation of scheduling

이제 스케줄링 과정을 봅시다.

PD는 Store 또는 (Region) 리더의 하트비트를 통해 지속적으로 정보를 수집하여 클러스터의 세부 데이터를 획득합니다. 이 정보와 일정 정책에 기반하여, PD는 일련의 운영 작업를 생성한 다음, Region 리더가 보낸 하트비트를 수신 할 때, 이 Region에서 수행할 연산이 있는지 체크합니다. PD는 예정된 연산을 "하트비트에 대한 응답메시지"을 통해 Region 리더에게 반환합니다. 그런 뒤, PD는 다음 하트비트에서 그 연산 실행 결과를 모니터링합니다. 이러한 연산은 Region 리더에게만 제안되는 것이며, 실행이 보장되지는 않습니다. 현재 상태에 따라 실행할 것인지 여부와 언제 실행할지 결정하는 것은 Region 리더입니다.

summary

이 블로그는 귀하가 다른 곳에서 찾을 수 없는 정보를 공개합니다. 우리는 여러분들이 분산 스토리지 시스템을 구축할 수 있도록 스케줄링에 대해 고려해야할 사항정책의 유연한 확장을 지원하기 위해 정책과 구현을 분리하는 방법에 대해 더 잘 이해 하였기를 바랍니다.

우리는이 세 가지 블로그 (데이터 스토리지, 컴퓨팅 및 스케줄링)가 TiDB의 기본 개념과 구현 원칙을 이해하는 데 도움이되기를 바랍니다. 앞으로 TiDB에 관하여 코드에서 아키텍처로 가는 더 많은 블로그가 있을 것입니다!

 

반응형