Go와 Raft 합의를 이용한 탄력적인 분산 시스템 구축
Emily Parker
Product Engineer · Leapcell

소개: 분산 합의의 중요성
오늘날 상호 연결된 세상에서 수평적으로 확장하고, 장애를 견디며, 높은 가용성을 유지할 수 있는 애플리케이션을 구축하는 것은 더 이상 사치가 아니라 기본적인 필수 사항입니다. 관리하기는 더 간단하지만 중앙 집중식 시스템은 단일 실패 지점을 제시하며 사용자 수요가 증가함에 따라 종종 성능 병목 현상을 일으킵니다. 대조적으로, 분산 시스템은 여러 기계에 걸쳐 계산 및 데이터를 분산하여 향상된 복원력과 확장성을 제공합니다. 그러나 이러한 독립 노드 간의 상태를 관리하고 데이터 일관성을 보장하는 것은 상당한 복잡성을 야기합니다. 이것이 바로 분산 합의 알고리즘이 작동하는 곳입니다. 이는 네트워크 분할이나 기계 충돌이 발생하더라도 기계 그룹이 단일 값 또는 일련의 작업에 동의할 수 있는 메커니즘을 제공합니다. 이러한 알고리즘 중에서 Raft는 특히 명확성과 안전 보장으로 유명하여 매우 인기 있고 이해하기 쉬운 선택으로 부상했습니다. 이 기사에서는 Go의 동시성 기본 요소와 강력한 생태계를 활용하여 Raft 프로토콜을 사용하여 간단하면서도 강력한 분산 합의 시스템을 구축하는 방법을 자세히 살펴봅니다.
Go에서의 Raft 및 구현 이해
코드를 살펴보기 전에 Raft 합의 알고리즘의 기반이 되는 핵심 개념을 명확하게 이해하고 이를 실용적인 Go 구현으로 어떻게 변환할 수 있는지 알아보겠습니다.
Raft의 핵심 개념
Raft는 복제된 로그를 관리하기 위해 설계된 합의 알고리즘입니다. 다수 서버가 사용 가능한 경우 시스템이 진행되고 일관성을 유지할 수 있음을 보장합니다. Raft는 여러 핵심 역할과 단계를 통해 이를 달성합니다.
- 리더, 팔로워 및 후보: Raft 클러스터의 모든 서버는 이 세 가지 역할 중 하나를 맡습니다.
- 팔로워: 수동적입니다. 리더 및 후보의 요청에 응답합니다.
- 후보: 팔로워에서 리더로 전환하는 서버입니다. 새로운 리더가 되기 위한 선거를 시작합니다.
- 리더: 활성 서버입니다. 모든 클라이언트 요청을 처리하고 로그 항목을 팔로워에게 복제합니다. 주어진 시점에는 항상 하나의 리더만 존재합니다.
- 기간: Raft는 기간이라는 단조롭게 증가하는 정수 시퀀스로 시간을 나눕니다. 각 기간은 선거로 시작됩니다.
- 로그 복제: 리더는 클라이언트 명령을 수신하고 로컬 로그에 추가한 다음 팔로워에게 복제합니다. 항목이 다수 서버에 복제되면 커밋된 것으로 간주되어 상태 기계를 적용할 수 있습니다.
- 하트비트: 리더는 리더십을 유지하고 새로운 선거를 방지하기 위해 모든 팔로워에게 정기적으로 AppendEntries RPC(빈 것도 하트비트라고 함)를 보냅니다.
- 선거 타임아웃: 팔로워는 무작위 선거 타임아웃을 기다립니다. 이 타임아웃 내에 리더로부터 하트비트 또는 AppendEntries RPC를 수신하지 못하면 후보로 전환하고 선거를 시작합니다.
- RequestVote RPC: 후보는 다른 서버에 RequestVote RPC를 보내 투표를 수집합니다. 서버는 자신의 로그만큼 최신이거나 더 최신인 로그를 가진 후보에게 투표합니다.
- AppendEntries RPC: 리더는 이 RPC를 사용하여 로그 항목을 복제하고 하트비트를 보냅니다.
Go에서의 빌딩 블록
Go의 goroutine 및 채널과 같은 내장 동시성 기능은 Raft 구현에 매우 적합합니다. 이러한 기능을 활용하여 동시 작업, 노드 간 통신 및 상태 전환을 관리합니다.
Go Raft 구현의 필수 구성 요소를 개략적으로 설명해 보겠습니다.
-
서버 상태: 각 Raft 서버는 현재 기간, 투표 대상(현재 기간), 로그 항목, 선거/하트비트 타이머를 포함한 상태를 유지해야 합니다.
undefined
type LogEntry struct { Term int Command []byte }
type RaftServer struct { mu sync.Mutex // 공유 상태 보호용 id int // 서버 ID peers []string // 다른 서버의 주소 isLeader bool currentTerm int votedFor int // currentTerm에서 이 서버가 투표한 피어 ID log []LogEntry
commitIndex int // 커밋된 것으로 알려진 가장 높은 로그 항목의 인덱스
lastApplied int // 상태 기계에 적용된 가장 높은 로그 항목의 인덱스
nextIndex []int // 각 피어에 대해 해당 피어에 보낼 다음 로그 항목의 인덱스
matchIndex []int // 각 피어에 대해 해당 피어에 복제된 것으로 알려진 가장 높은 로그 항목의 인덱스
// 통신 및 이벤트 트리거용 채널
electionTimeoutC chan time.Time
heartbeatC chan time.Time
applyC chan LogEntry // 상태 기계에 커밋된 항목을 적용하기 위한 채널
shutdownC chan struct{}
}
2. **RPC 및 통신:** 서버 간 통신에는 Go의 `net/rpc` 패키지를 사용합니다. 이를 위해서는 `RequestVote` 및 `AppendEntries` 메시지에 대한 RPC 메서드를 정의해야 합니다.
```go
// RequestVote RPC 인수 및 응답
type RequestVoteArgs struct {
Term int // 후보의 현재 기간
CandidateId int // 투표를 요청하는 후보의 ID
LastLogIndex int // 후보의 마지막 로그 항목의 인덱스
LastLogTerm int // 후보의 마지막 로그 항목의 기간
}
type RequestVoteReply struct {
Term int // 후보가 업데이트할 현재 기간
VoteGranted bool // 후보가 투표를 받은 경우 참
}
// AppendEntries RPC 인수 및 응답
type AppendEntriesArgs struct {
Term int // 리더의 현재 기간
LeaderId int // 팔로워가 클라이언트를 리디렉션할 수 있도록
PrevLogIndex int // 새로운 항목 직전의 로그 항목 인덱스
PrevLogTerm int // PrevLogIndex 항목의 용어
Entries []LogEntry // 저장할 로그 항목 (하트비트의 경우 비어 있음)
LeaderCommit int // 리더의 commitIndex
}
type AppendEntriesReply struct {
Term int // 리더가 업데이트할 현재 기간
Success bool // 팔로워가 PrevLogIndex 및 PrevLogTerm과 일치하는 항목을 포함한 경우 참
}
-
상태 기계 로직: Raft 서버의 핵심은 이벤트를 처리하고 역할 간의 전환을 관리하는 연속 실행되는 goroutine이 될 것입니다.
undefined
func (rs *RaftServer) Run() { // 초기 상태는 팔로워 rs.becomeFollower()
for {
select {
case <-rs.shutdownC:
return // 서버 종료
case <-rs.electionTimeoutC:
rs.mu.Lock()
// 하트비트를 받지 못했다면 후보로 전환
if rs.isFollower() && time.Since(lastHeartbeat) > rs.electionTimeout {
rs.becomeCandidate()
}
rs.mu.Unlock()
case <-rs.heartbeatC:
rs.mu.Lock()
if rs.isLeader {
// 모든 팔로워에게 하트비트 보내기
rs.sendHeartbeats()
}
rs.mu.Unlock()
// ... RPC 처리 및 커밋된 항목 적용을 위한 다른 경우 ...
}
}
}
4. **RPC 핸들러:** `RequestVote` 및 `AppendEntries` RPC 호출에 대한 논리를 구현합니다. 이러한 핸들러는 들어오는 요청을 기반으로 서버의 상태를 업데이트하고 적절한 응답을 반환합니다.
```go
func (rs *RaftServer) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) error {
rs.mu.Lock()
defer rs.mu.Unlock()
reply.Term = rs.currentTerm
reply.VoteGranted = false
// 투표에 대한 Raft 규칙...
if args.Term < rs.currentTerm {
return nil // 후보의 기간이 오래되었습니다
}
if args.Term > rs.currentTerm {
rs.becomeFollower() // 기간 업데이트 및 필요한 경우 단계 강등
rs.currentTerm = args.Term
rs.votedFor = -1 // 투표 재설정
}
if (rs.votedFor == -1 || rs.votedFor == args.CandidateId) &&
rs.isLogUpToDate(args.LastLogIndex, args.LastLogTerm) {
reply.VoteGranted = true
rs.votedFor = args.CandidateId
// 유효한 후보에게 투표하므로 여기에서 선거 타이머 재설정
rs.resetElectionTimeout()
}
return nil
}
func (rs *RaftServer) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) error {
rs.mu.Lock()
defer rs.mu.Unlock()
reply.Term = rs.currentTerm
reply.Success = false
if args.Term < rs.currentTerm {
return nil // 리더의 기간이 오래되었습니다
}
// 새 리더 또는 더 높은 기간의 현재 리더로부터 RPC인 경우 단계 강등
if args.Term > rs.currentTerm || rs.isCandidate() {
rs.becomeFollower()
rs.currentTerm = args.Term
rs.votedFor = -1
}
// 유효한 AppendEntries RPC 시 항상 선거 타이머 재설정
rs.resetElectionTimeout()
// 로그 일관성 검사 및 항목 추가 로직...
// 이것은 단순화된 표현입니다. 실제 로직에는
// PrevLogIndex/PrevLogTerm 확인 및 로그 자르기/추가가 포함됩니다.
if len(rs.log) > args.PrevLogIndex && rs.log[args.PrevLogIndex].Term == args.PrevLogTerm {
reply.Success = true
// 새 항목 추가 및 잠재적으로 충돌하는 항목 자르기
// args.LeaderCommit > rs.commitIndex인 경우 commitIndex 업데이트
}
return nil
}
- 클라이언트 상호 작용: 클라이언트는 새 명령을 제안하기 위해 현재 Raft 리더에게 연결합니다. 클라이언트가 팔로워에게 연결하면 팔로워는 클라이언트를 리더에게 리디렉션해야 합니다.
애플리케이션 시나리오
Raft 기반 시스템은 상태 저장 서비스에 대한 강력한 일관성과 장애 내성이 필요한 시나리오에 이상적입니다. 일반적인 애플리케이션은 다음과 같습니다.
- 분산 키-값 저장소:
etcd
또는ZooKeeper
와 같이 클러스터 메타데이터 및 구성을 관리하기 위해 Paxos 또는 Raft의 변형을 사용하는 것을 생각해 보세요. - 분산 데이터베이스: 복제본 간에 트랜잭션 로그의 일관성을 보장합니다.
- 분산 잠금: 공유 리소스에 대한 상호 배타적 액세스를 위한 안정적인 메커니즘을 제공합니다.
- 고가용성을 위한 리더 선출: 클러스터에서 기본 서비스 인스턴스를 선택합니다.
결론: 합의를 통한 안정성
간단한 시스템조차도 Go와 Raft를 사용하여 분산 합의 시스템을 구축하는 것은 강력한 프로그래밍 언어와 잘 정의된 알고리즘의 조합의 힘을 보여줍니다. Raft의 명확한 설계와 Go의 동시성 모델은 내결함성과 고가용성을 갖춘 인프라 구성 요소를 만드는 데 탁월한 조합입니다. 우리의 예시가 표면을 긁는 것에 불과하지만, 모든 Raft 구현의 기반이 되는 리더 선출, 로그 복제 및 안전 보장이라는 기본 원칙을 강조합니다. 이러한 개념을 숙달하는 것은 불가피한 실패에 직면하여 회복력 있는 분산 시스템을 구축하려는 모든 사람에게 중요합니다. Go와 Raft는 함께 강력한 분산 시스템 안정성을 달성하기 위한 설득력 있는 경로를 제공합니다.