///
Search

05_공통 패턴

참고 코드 자료 링크

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와 같은 보조적인 캐시에 캐싱 하는 것이다.
캐시는 사전에 컴파일된 객체를 빠르게 접근할 수 있는 데이터 저장소에 저장함으로써 대용량 처리를 제공하도록 설계돼 있다.