클라우드 네이티브 Go 책을 참고했습니다.
go를 이용하여 안정성 패턴을 구현해봅니다.
(분산 애플리케이션에서의)
서킷 브레이커
서비스가 실패할 경우, 장애가 퍼지는 걸 막을 수 있습니다.
예를 들면, db lock 이나 리소스 부족 등으로 인해 timeout이 발생할 수 있습니다. time out이 계속해서 발생하게 될 경우에는 뒤의 요청에 대해서도 처리를 하지 못합니다. 만약 MSA 라면, 모든 서비스들이 영향을 받게 되고 이는 전체 서비스 장애와 이어 집니다.
또한 클라이언트가 retry까지 하게 되면 네트워크 단에 부하가 엄청나게 발생합니다.
서킷 브레이커는 이러한 장애를 막을 수 있습니다.
에러가 몇 번 이상 발생했을 경우 서킷을 open 합니다. 그러면 기본 로직은 동작하지 않고 바로 error를 클라이언트에 반환합니다.
시간이 지난 후에는 half open으로 요청 중 일부만 받아들입니다.
만약 이 과정에서 에러가 발생하지 않았다면 서킷이 close되고 기존 로직이 동작하게 됩니다.
이를 go로 구현해보겠습니다.
package circuit
// context는 thread safe.
// 즉, 다수의 go routine이 접근해도 괜찮음.
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
type Circuit func(context.Context) (string, error)
func Breaker(circuit Circuit, threshold int) Circuit {
var last = time.Now()
var m sync.RWMutex
failures := 0
return func(ctx context.Context) (string, error) {
m.Lock() //mutex 를 이용한 read lock(기존 코드) -> Read lock 을 하면 서킷에 오픈이 안됨..?
count := failures - threshold
// 실패 개수가 더 많을 경우
if count >= 0 {
retryAt := last.Add(40 * time.MilliSecond)
if !time.Now().After(retryAt) {
m.Unlock()
fmt.Println("open")
return "open", errors.New("open")
}
}
m.Unlock()
res, err := circuit(ctx)
m.Lock()
defer m.Unlock() //끝날 때 lock
last = time.Now() //이를 위해 lock이 필요함.
if err != nil {
fmt.Println(err)
failures++ // need lock
return res, err
}
if failures > 0 {
failures -= 1
}
fmt.Println("200")
return res, nil
}
}
테스트 코드 입니다.
package circuit
import (
"context"
"errors"
"fmt"
"math/rand"
"sync"
"testing"
"time"
)
func failAfter(threshold int) Circuit {
count := 0
// Service function. Fails after 5 tries.
return func(ctx context.Context) (string, error) {
count++
if count > threshold {
return "", errors.New("INTENTIONAL FAIL!")
}
return "Success", nil
}
}
func waitAndContinue() Circuit {
return func(ctx context.Context) (string, error) {
time.Sleep(time.Second)
if rand.Int()%2 == 0 {
return "success", nil
}
return "Failed", fmt.Errorf("forced failure")
}
}
func TestCircuitBreakerFailAfter5(t *testing.T) {
circuit := failAfter(5)
ctx := context.Background()
for count := 1; count <= 10; count++ {
message, err := circuit(ctx)
t.Logf("attempt %d: %v, %s", count, err, message)
switch {
case count <= 5 && err != nil:
t.Error("expected no error; got", err)
case count > 5 && err == nil && message != "open":
t.Error("expected err and open")
}
}
}
func TestCircuitBreakerDataRace(t *testing.T) { //테스트 코드!
ctx := context.Background()
circuit := failAfter(5)
breaker := Breaker(circuit, 1)
wg := sync.WaitGroup{}
for count := 1; count <= 20; count++ {
wg.Add(1)
go func(count int) {
defer wg.Done()
message, err := breaker(ctx)
time.Sleep(10 * time.Millisecond)
t.Logf("attempt %d: err=%v, message=%s", count, err, message)
}(count)
}
wg.Wait()
}
5번 이상이 넘어가면 error가 발생하는 코드 입니다.
그리고 1번 에러가 발생하면, 그 다음에 circuit이 open 됩니다.
여기서 breaker에서는 40 밀리세컨드 동안 open이 됩니다. 그리고 다시 close 되고 error 발생한 후에 open이 되는 것을 볼 수 있습니다.
* half open 등이 세부 사항은 구현되지 않았습니다.
* read lock을 이용해도 잘 됩니다.
'BackEnd > go' 카테고리의 다른 글
[Go] 리소스 상태 유지 (0) | 2024.07.09 |
---|---|
[GO] Rest API 구현 (0) | 2024.07.06 |
[Go] 동시성 패턴 future (0) | 2024.06.29 |
[Go] struct 와 포인터(자바 클래스와 비교) (0) | 2024.06.25 |