•
참고 코드 자료 링크
Martin Kleppman 의 Design Data-Intensive Applications
•
시스템이 커질수록 구성 요소 중 하나가 고장날 가능성이 커진다.
•
시간이 지나면서 고장났던 것들은 고쳐지고 또 새로운 것들이 고장나게 되지만 수천 개의 노드가 있는 시스템에서는 항상 무언가가 고장났다고 가정하는 것이 합리적이다.
•
고장난 부분은 포기하는 것으로만 에러 처리 전략이 구성돼 있다면 큰 시스템은 결코 동작하지 않을 것이다.
•
피할 수 없는 실패를 처리하도록 하기위해 좋은 패턴을 사용하는 방법을 소개한다.
•
느슨한 연결을 통한 시스템의 전체적인 계단식 에러를 예방하는 방법을 소개한다.
•
항상 지속적으로 로그를 남기고 서비스를 모니터링 해야한다.
예시 상황
•
새로운 주문이 들어오면 주문서비스로 들어간다.
•
주문서비스에서 데이터 저장소(DB)로 주문을 저장한다.
•
주문서비스에서 이메일 서비스를 통해 이메일을 발생한다.
최소 한 번은 전달되는 이벤트 처리
•
이벤트 처리는 메시지 큐를 사용해 마이크로서비스를 분리할 수 있는 모델이다.
•
기존의 서비스에 직접 연결하는 대신, Redis, AmazonSQS 같은 큐나 다른 소스의 호스트로 이벤트를 브로드태스트로 보내고 수신을 대기할 수 있다.
•
즉 새로운 주문이 주문 서비스에 들어오면 메시지 큐에 new_order를 추가한다.
•
그리고 이메일 서비스는 new_order 메시지를 읽어오는 작업이다.
•
메시지 큐는 고도로 분산된 확장 가능한 시스템이다.
에러 처리
•
분산 시스템에서는 에러가 당연히 발생하기에 소프트웨어 설계에 반영해야 한다.
•
에러가 발생한 시점에서의 메시지를 바탕으로 메시지에 추가적인 정보를 보완해 큐에 다시 추가할 수 있다.
처리 불가 큐
•
일반적으로 처리불가(데드 레터 dead letter)큐라고 하며 이 처리 불가 큐는 메시지가 처음 시작된 큐에 의해 지정된다.
아토믹 트랜잭션
•
시스템 모델(system model), 로그 기반 복구(log based recovery), 검사점(checkpoint)
•
임계 구역의 상호 배제는 임계 구역이 원자적으로 수행된다는 것을 보장한다. 두 개의 임계 구역이 동시에 병렬로 수행된다고 하더라도 그 결과는 어떤 순서인지는 지정할 수 없지만 마치 두 개를 한번에 하나씩 순차적으로 수행시킨 것과 같게 된다.
•
가장 대표적인 예는 은행 이체 시스템이다. 자금 이체의 경우 한 통장에서 돈이 출금되어 다른 통장으로 입금된다. 이때 일관성을 위해서는 출금과 입금이 둘 다 발생하던지 둘 다 발생하지 않는 것이다.
•
마이크로서비스에서 아토믹한 특성의 문제를 해결하기 위한 작업
◦
주문 프로세스의 두 부분을 큐에서 처리하도록 배포한다.
◦
이 패턴을 사용하면 프로세스가 성공한 경우에만 큐에서 메시지를 제거하므로 무언가 실패하는 부분이 있으면 다시 시도한다.
타임 아웃
•
타임 아웃은 다른 서비스나 데이터 저장소와 통신할 때 매우 유용한 패턴이다.
•
기본적인 아이디어는 서버 응답에 대한 제한을 설정하고, 주어진 시간 내에 응답을 받지 못하면 다시 시도하거나 업스트림 서비스에 실패 메시지를 다시 보내는 등 실패를 처리할 비즈니스 논리를 작성한다는 것이다.
•
예시 코드
package main
import (
"fmt"
"os"
"time"
"github.com/eapache/go-resiliency/deadline"
)
func main() {
switch os.Args[1] {
case "slow":
makeNormalRequest()
case "timeout":
makeTimeoutRequest()
}
}
func makeNormalRequest() {
slowFunction()
}
func makeTimeoutRequest() {
dl := deadline.New(1 * time.Second)
err := dl.Run(func(stopper <-chan struct{}) error {
slowFunction()
return nil
})
switch err {
case deadline.ErrTimedOut:
fmt.Println("Timeout")
default:
fmt.Println(err)
}
}
func slowFunction() {
for i := 0; i < 100; i++ {
fmt.Println("Loop: ", i)
time.Sleep(1 * time.Second)
}
}
Go
복사
백 오프
•
일반적으로 연결이 실패하면 네트워크 또는 서버에 요청이 넘치지 않게 하기 위해서 다시 시도를 안 한다.
•
그래도 다시 재시도를 하기 위해서는 백 오프 접근 방식으로 구현을 해야한다.
•
처음에 연결 실패후 다시 설정된 시간동안 대기하는데, 성공하기 전까지 실패할때마다 점점 늘어 최대 시간까지 늘어나는 시간을 말한다.
•
예시 코드
package main
import (
"fmt"
"time"
"github.com/eapache/go-resiliency/retrier"
)
func main() {
n := 0
r := retrier.New(retrier.ConstantBackoff(3, 1*time.Second), nil)
err := r.Run(func() error {
fmt.Println("Attempt: ", n)
n++
return fmt.Errorf("Failed")
})
if err != nil {
fmt.Println(err)
}
}
Go
복사
회로 차단
•
계단식 장애로부터 시스템을 보호하는데 도움이 되는 타임 아웃과 백오프와 같은 일부 패턴이 있다.
•
여기서 새롭게 소개하는 회로 차단은 빠른 실패에 관한 것이다.
•
Michael Nygard의 Release It
회로 차단기는 시스템이 스트레스를 받고 있을 때 자동으로 기능을 축소(degrade)시키는 방법이다.
Go
복사
•
서비스 호출을 중단하고 웹사이트를 정상 작동 속도로 되돌리고 프로필 페이지의 기능을 조금 축소시켜야한다.
•
여기에서의 효과는 3가지가 있다.
◦
사이트 내 다른 사용자들은 브라우징 환경을 원래대로 되돌린다.
◦
한 영역에서 경험을 약간 떨어뜨린다.
◦
이 기능은 시스템의 비즈니스에 직접적인 영향을 미치기 때문에, 구현하기 전에 이해 관계자와 논의해야 한다.
•
회로 차단기를 구성하기 위해서는 세가지 매게 변수를 사용해 회로 차단기를 구성해야한다.
•
첫 번째 매개 변수 eroorThreshold의 횟수 만큼 에러가 발생하면 회로를 차단한다.
•
반 개방 상태에서 두 번째 매개 변수 SuccessThreshold의 횟수 만큼 요청 처리에 성공하면 닫힌 상태로 변경한다.
•
세 번째 매개 변수 timeout은 회로를 반 개방으로 변경되기 전에 열린 상태로 대기해야 하는 시간이다.
•
예시 코드
package main
import (
"fmt"
"time"
"github.com/eapache/go-resiliency/breaker"
)
func main() {
b := breaker.New(3, 1, 5*time.Second)
for {
result := b.Run(func() error {
// Call some service
time.Sleep(2 * time.Second)
return fmt.Errorf("Timeout")
})
switch result {
case nil:
// success!
case breaker.ErrBreakerOpen:
// our function wasn't run because the breaker was open
fmt.Println("Breaker open")
default:
fmt.Println(result)
}
time.Sleep(500 * time.Millisecond)
}
}
Go
복사
•
회로 차단이 잘 구현된 예시 넷플릭스 Netflix/Hystrix
•
원격 시스템, 서비스 및 서드 파티 라이브러리에 대한 접속 지점을 격리 시켜 계단시 오류를 방지한다.
•
장애가 발생하기 쉬운 분산 시스템의 시스템 복원 능력이 활성화 되도록 설계된 지연 및 장애에 대한 내결함성을 갖춘 라이브러리
•
Go로 만들어진 Hystrix-go
상태 점검
•
상태 점검(Health Check)은 마이크로서비스 설정에서 반드시 필요한 사항
•
상태 점검시 기록해야할 목록들
1.
데이터 저장소 연결 상태(일반 연결 상태, 연결 풀 상태)
2.
현재 응답 시간(이동 평균)
3.
현재 연결
4.
잘못된 요청(평균 실행 소요시간)
•
예를 들어 DB에 연결되지 않은것은 서비스가 작동이 안됨을 의미하므로, 비정상적인 상태로써 오케스트레이터(orchestrator)(여러 개의 컨테이너로 구성된 마이크로서비스 사이에서 프로세스의 처리 흐름을 관리 및 조율하는 서비스)가 컨테이너를 다시 활용할 수 있도록 해야한다.
쓰로틀링
•
쓰로틀링은 (Throttling)은 서비스가 처리할 수 있는 연결 수 를 제한하고 임계치를 초과하면 HTTP 에러코드를 리턴하는 패턴
•
예시 코드
package throttling
import "net/http"
// LimitHandler is middleware which limits the current number of active
// connections that this handler can sustain.
// Once the current connections equal the max http.StatusTooManyRequests is
// returned
type LimitHandler struct {
connections chan struct{}
handler http.Handler
}
// NewLimitHandler creates a new instance of the LimitHandler for the
// given parameters.
func NewLimitHandler(connections int, next http.Handler) *LimitHandler {
cons := make(chan struct{}, connections)
for i := 0; i < connections; i++ {
cons <- struct{}{}
}
return &LimitHandler{
connections: cons,
handler: next,
}
}
func (l *LimitHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
select {
case <-l.connections:
l.handler.ServeHTTP(rw, r)
l.connections <- struct{}{} // release the lock
default:
http.Error(rw, "Busy", http.StatusTooManyRequests)
}
}
Go
복사
서비스 탐색
•
모놀리식(Monolithic), 일체형 즉 마이크로서비스의 정 반대의 개념으로 모든 서비스의 기능이 하나의 컴포넌트로 구성되어있는 상태를 의미한다.
•
일체형 애플리케이션에서 서비스는 언어 수준이나 메서드나 프로시저 호출을 통해 서로를 호출한다.
마이크로서비스는 쉽다.
하지만 마이크로서비스 시스템을 구축하는 일은 어렵다.
Go
복사
•
서비스 탐색에는 두가지의 주요 패턴이 있다.
◦
서버측 탐색
▪
클라이언트에서 서비스 라우터로 접근한다.
▪
서비스 라우터는 서비스 레지스트리(DB)에 서비스 위치 탐색을 한다.
▪
서비스 라우터는 서비스 A, 서비스 B, 서비스 C에 다운스트림 호출을 하달한다.
◦
클라이언트측 탐색
▪
서비측 탐색과 유사하다.
▪
클라이언트가 서비스 탐색 및 부하 분산을 담당한다.
부하 분산
•
내부적인 호출을 위해 클라이언트측을 살펴보는 것이 좋다.
•
이렇게 하면 상황에 따른 재시도 논리를 더 강력하게 제어할 수 있다.
•
클라이언트 측 부하 분산을 선호하는 이유는 다음과 같다.
•
수년 동안 서버측 탐색이 유일한 선택지였으며 성능 문제로 인해 로드 밸런서에서 SSL을 종료하는 것을 선호한다.
캐싱
•
서비스의 성능을 향상할 수 있는 방법은 매번 DB를 사용하는 대신, 데이터베이스 및 다른 다운스트림 호출을의 결과를 인-메모리 캐시나 Redis와 같은 보조적인 캐시에 캐싱 하는 것이다.
•
캐시는 사전에 컴파일된 객체를 빠르게 접근할 수 있는 데이터 저장소에 저장함으로써 대용량 처리를 제공하도록 설계돼 있다.