원본은 https://laced-river-472.notion.site/Concurrency-in-Go-7f97812aee104adc8cc6d60591f01316 에 요약 해놓음.
2024년 3월 27일 ~ 2024년 3월 29일
Concurrency in go 책 검토
https://github.com/SK-Jin/concurrency-in-go-src
-
1장 동시성 소개
- 동시성이 어려운 이유
-
2장 코드 모델링: 순차적 프로세스간의 통신(CSP)
- 동시성과 병렬성의 차이
- 동시성을 지원하는 언어의 장점
- Go의 동시성의 철학
-
3장 동시성 구성요소
- goroutine
- Sync Package
- Channels
- select-statement
- defer-statment
-
4장 동시성 패턴
- 제한(Confinement)
- Ad-hoc
- 내부적인 규칙, 암묵적인 규칙 등 시간적인 문제로 깨트려지는 경우가 있을 수 있음.
- lexical
- 도구에서 문제 상황에 대해 통보, 이를 개선 하는 형태로 접근
- 동기화를 사용할 수 있다면, 제한을 추구해야 함.
- 성능을 향상시키고, 개발자의 인지 부하를 줄이기 위함
- 동기화에는 비용이 들며, 이를 피할 수 있으면, 임계 염역이 없기 때문에 동기화 비용을 지불할 필요가 없음.
- 동기화로 인해 발생 가능한 모든 문제도 막을 수 있음
- Ad-hoc
- for-select 루프
- 채널에서 반복 변수 보내기
- 멈추기를 기다리면서 무한히 대기
- 고루틴 누수 방지
- 고루틴 작지만 리소스를 필요로 함.
- 가비지 콜렉터에 의해 수집 되지 않음. -> 원인은 제각각 이겠지만, 여전히 활성화 되어 있을 것이므로
- 고루틴 종료 조건
-
- 작업 완료
-
- 복구할 수 없는 에러로 인해 더 이상 작업을 계속할 수 없을 때
-
- 작업을 중단하라는 요청을 받았을 떄 -> 네트워크 효과(연관된 고루틴 존제)가 있으므로 제일 중요
-
- 접근 1
- Cancel chan 생성 하여 기존 for~range 구조에셔 for~select 구조 변경 cancel 요청 감시 하도록 수정
- 접근 2
- Cancel chan 생성, 기존 for 구조에서 for~select 구조 변경 cancel 요청 감시 하도록 수정
- or-channel
-
- 때로는 하나 이상의 done 채널을 하나의 done 채널로 결합해, 그 중 하나의 채널이 닫힐때 결합된 채널이 닫히도록 해야할 수 있음.
-
- select 로 처리하면 좋으나, 때때로 런타임 작업 중인 done 채널 갯수를 알지 못하는 경우가 있음
- 재귀 및 고루틴들을 통해 복합done 채널 생성
-
- 에러처리
- error path 에 주의?
- 에러 처리의 책임자는 누구인가?
- 호출한 함수에게 오류 상황을 반환하여 알린다.
- type Result struct { // <1> Error error Response *http.Response } checkStatus := func(done <-chan interface{}, urls ...string) <-chan Result { // <2>
- 호출한 함수에게 오류 상황을 반환하여 알린다.
- 파이프라인
-
- 긴 하나의 함수? 대신에 파이프라인 처리가 향후 관리에서도 효율적
-
- 어떤 시스템을 변경하고, 하나의 논리적인 변화를 위해 여러 영역을 변경하였다면, 잘못된 추상화 가능성 있음.
-
- 시스템에서 추상화를 구성하는 데 사용할 수 있는 또 다른 도구
- 특히, 프로그램이 스트림이나 데이터에 대한 일괄 처리 작업들을 처리해야 할 때 매우 강력한 도구
-
- 파이프라인의 각각의 단계를 스테이지(Stage) 라고 함.
-
- 하나의 단계를 데이터를 가져와서 변환을 수행하고, 데이터를 다시 전송하는 것.
-
- 파이프 라인 중 팬 인, 팬 아웃 가능, 속도를 제한 하는 것도 가능.
-
- 함수형 프로그램 과 연관, 모나드 가 생각 날 수 있음.
-
- 단계를 수정하지 않고, 높은 수준에 수정으로 원하는 바를 얻을 수 있음.
-
- pipeline []int 입력, multiply, add 형태로 구현, 출력이 chan int 형태가 되도록 generator, multiply, Add 구현 하여 호출
-
- 기존 예제와 차이점
- = 입력 및 출력이 동시에 실행되는 컨텍스트에서 안전하기 때문에 각 단계에서 안전하게 동시에 실행 가능
- = 파이프 라인의 각 단계가 동시에 실행된다는 점. 즉 모든 단계가 입력만을 기다리며, 출력을 보낼수 있어야 함.
- = 이로 인해 각 단계들로 하여금 특정 타임 슬라이스에서 상호 돌립적으로 실행 될 수 있도록 허용한다는 점.
-
- 기존 함수형 호출은 모든 단계가 완료되어야 처음부터 입력이 가능하지만, 파이프라인은 출력이 비워지는 순간 입력이 채워지게 되므로 사실상 여러 단계가 동시에 동작하는 효과가 있음.
-
- 파이프라인 생성기
- 이산 값의 집합을 채널의 값 스트림으로 변환하는 함수
-
- 유용한 생성기들
- repeat 주어진 값들을 순차적으로 반복
- 함수에서 주어진 값들을 반복
-
-
- Pan-out, Pan-in
-
- 파이프라인의 특정 단계에서 시간이 많이 걸리는 경우, 다른 단계에 영향을 미침.
-
- 파이프라인의 흥미로운 점, 개별 단계를 조합해 데이터 스트림에서 연산 할 수 있고, 파이프라인을 여러번 재사용 가능.
-
- 여러개의 고루틴을 통해, 파이프라인의 상류 단계로 부터 데이터를 가져오는 것을 병렬화하면서 파이프라인 한단계를 재사용 가능
-
- 팬 아웃 : 파이프라인의 입력을 처리하기 위해 여러 개의 고루틴 들로 시작하는 프로세스
-
- 팬 인 : 여러 결과를 하나의 채널로 결합하는 프로세스
- = 여러 소비자가 읽어 들이는 하나의 다중화된 채널을 생성하는 것,
- = 하나의 입력 채널마다 고루틴을 동작 시키는 것
- = 입력 채널이 모두 닫혔을때 다중화된 채널을 닫는 하나의 고루틴과 관련돼 있음.
- = 결과가 도착하는 순서가 중요하지 않은 경우 사용.
-
- 팬인, 팬아웃 적용 가능한 곳
- = 단계가 이전에 계산한 값에 의존하지 않는다. 즉 순서 독립성을 가진다.
- = 단계를 실행하는데 시간이 오래 걸린다.
-
- 8.or-done 채널
- Or 채널에 Done 채널 처리가 추가된 형태, Done 입력시 연관 채널 종료 처리
- 9.tee 채널
- 입력 하나에 대해 2개의 출력을 처리 하는 채널,
- 10.bridge 채널
- genval 에서 채널집합(채널당 값 이 들어간 채널)을 하나씩 넣고 닫는다.
- bridge 패턴에서는 각각 전달 받은 채널을 받아서 하나의 입력으로 처리해 준다.
- chanStream <-chan <-chan interface{}, intChanStream := make(chan (<-chan interface{})) // <2>
- 대기열 사용(queuing)
-
- 파이프라인이 아직 준비 되지 않았더라도, 파이프라인에 작업을 받아들이는 것이 유용할 때가 있다.
-
- 대기열을 널리 사용하지 않는 이유.
- = 프로그램 최적화 마지막 단계에 적용함.
- = 대기열을 성급하게 도입하면 데드락이나 라이브락 과 같은 동기화 문제가 드라나지 않을 수 있음
- = 프로그램이 정확해짐에 따라 대기열이 얼마나 필요한지 알게 될 수 있음.
-
- 대기열이 프로그램의 총 실행 속도를 높여주지는 않음.
- = 단지, 프로그램이 다른 방식으로 동작하도록 도와줌.
- = 대신에 좀 더 CPU 유휴시간을 활용할 수 있게 도와줌.
-
- 대기열 사용이 시스템의 전반적인 성능을 향상시킬 수 있는 상황.
- = 특정 단계에서 일괄 처리 요청이 시간을 절약하는 경우
- = 특정 단계의 지연으로 인해 시스템에 피드백 루프가 생성되는 경우
-
- 파일 쓰기시 정킹 단위로 한번에 수행 처리, IO 처리 속도 개선 효과
-
- 상위단계의 지연이 파이프라인 전체에 영향을 미침,
- = 보통 상위단계에서 단계별로 영향을 발생, 파악이 어려울수 있음.
-
- 파이프 라인의 효율성이 특정 임꼐 값 아래로 떨어지면 파이프 라인 상류 단계의 시스템이
- = 파이프라인으로의 입력을 늘리기 시작하고, 파이프라인 효율의 저하로 이어짐... 죽음의 나선 시작
- = 실패에 대한 안전장치(fail-safe) 가 없으면 파이프라인을 이용하는 시스템은 결코 회복 되지 않음.
-
- 시간 초과 처리는 필수, 죽은 요청 처리하다가 피드백 루프를 생성해 파이프 라인의 효율을 떨어뜨림.
-
- 대기열의 상황은 몇몇 상황에서만 파이프라인의 실행시간을 단축시킴.
-
- 여기저기 파이프라인 도입시 오히려 늦어질 수 있음.
-
- 리틀의법칙(little's law) : 대기열 사용 이론에서 충분한 샘플링을 통한 파이프라인의 처리량을 예측하는 법칙
- = 진입 속도와 탈출 속도가 같은 경우에만 도입해야 함.
-
- Context 패키지
-
- 고루틴 취소를 위한 Done 처리는 잘 동작하지만, 단순하면서 제한 적이다.
-
- 취소되었다는 알림 대신, 취소 이유가 무엇인지, 또는 함수가 마감 시한이 있는지 추가적인 정보가 있다면 도움이 됨.
-
- 시스템 크기에 상관없이 일반적으로 이들 정보를 Done 채널로 감쌀 필요가 있다는 것이 확실함.
-
- 그래서, Done 을 표준 패턴화 하고 패키지 한 것이 context 패키지.
-
- 두가지 주요 목적
- = 호출 그래프상의 분기를 취소하기 위한 AIP 제공
- = 호츨 그래프를 따라 요청 범위request-scope 데이터를 전송하기 위한 데이터 저장소(data-bag) 제공
-
- 함수의 취소 한다는 것의 의미 -> context 패키지에서 관리 가능함.
- = 고루틴의 부모가 해당 고루틴을 취소하고자 할 수 있다.
- = 고루틴이 자신의 자식을 취소하고자 할 수 있다.
- = 고루틴 내의 모든 대기 중인 작업은 취소 될 수 있도록 선점 가능할 필요가 있다.
-
- Context 에 타입을 다르게 선언하면 값은 같아도 추가 할 수 있다.
-
type foo int type bar int func main() { m := make(map[interface{}]int) m[foo(1)] = 1 m[bar(1)] = 2 fmt.Printf("%v", m) //map[1:2 1:1] } //- Context 의 인스턴스에 값을 저장해야 함. type ctxKey int const ( ctxUserID ctxKey = iota ctxAuthToken ) func UserID(c context.Context) string { return c.Value(ctxUserID).(string) } func AuthToken(c context.Context) string { return c.Value(ctxAuthToken).(string) } func ProcessRequest(userID, authToken string) { ctx := context.WithValue(context.Background(), ctxUserID, userID) ctx = context.WithValue(ctx, ctxAuthToken, authToken) HandleResponse(ctx) }
- 제한(Confinement)
-
5장 확장에서의 동시성
-
에러전파
-
- 분산 등의 처리 쉽게 오류가 발생할 수 있음. 하지만 문제의 원인을 파악하기 쉽지 않음.
-
- 크고 복잡한 시스템에서 오류를 어떻게 전달해야 할까?
-
- 오류 처리를 자신의 것으로 하는 방안이 필요.
-
- 에러에서 취해야 하는 정보
- = 발생한 사전
-
- 디스크가 찼다,
-
- 소켓이 닫혔다.
-
- 자격이 만료되었다.
-
- = 발생한 장소 와 시점
-
- 호출이 시작된 메서드 부터, 에러가 발생한 위치가 있는 전체 스택 트레이스 포함.
-
- 실행중인 콘텍스트 관련 정보 포함
-
- 분산 시스템인 경우에는 발생한 시스템 정보 표시 해야 함.
-
- 에러 시간은 UTC 기준 시간으로 표시
-
- = 사용자 친화적인 메세지
-
- 시스템 및 시스템의 사용자에 맞춰 조정해야 함
-
- 친화적인 메세지는 인간 중심적이며 문제가 일시적인지 여부를 포함해야 함.
-
- = 사용자가 추가적인 정보를 얻는 방법
-
- 어떤 시점에 누군가는 에러가 발생했을때 일어난 일을 자세히 알고 싶어 할 것
-
- 사용자에게 표시되는 에러는 에러가 기록된 시간이 아닌 발생한 시간.
-
- 에러가 생성 됐을때의 전체 스택 트레이스 등 에러의 전체 정보를 표시하는 로그에 상호 참조할수 있는 방법(또는 ID) 제공 해야 함.
-
- 버그 추적 시스템에서 비슷한 문제를 집계하는 데 도움이 되는 스택 트레이스의 해시 값을 포함하는 방식도 유용.
-
- = 아무런 정보 없이 오류만 노출 되는 것은 실수 이며 버그.
에러에 대해 생각할 수 있는 범용적인 프레임워크 필요.
-
- 모든 에러는 2가지로 나뉨.
- ++ 버그 - 사용자가 시스템에 맞춰 정의하지 않은 에러 또는 "처리되지 않은" 에러
- ++ 알려진 예외적인 경우(네트워크 단전, 디스크 쓰기 실패)
-
- 모든 오류를 다 감싸서 호출한 곳으로 전달할 필요는 없음.
- ++ 자기 모듈 혹은 코드에 유용한 콘텍스트를 추가할 수 있는 경우에만 에러를 감싸서 전달.
- ++ 나저지는 처리 하지 않음. 처음 발생한 곳에서 이미 오류 처리를 하고 있을 것이므로
-
-
-
2.시간 초과 및 취소
-
- 동시성 코드로 작업할 떄, 시간 초과(timeout) 이나 취소(cancellation)가 자주 발생함.
-
- 시간 초과는 동작을 이해할 수 잇는 시스템을 만드는 여러 요소 가운데 결정적인 요소
-
- 취소는 시간 초과에 따르는 자연스러운 반응.
-
- 동시 프로세스가 시간 초과를 지원하기를 원하는 이유
- = 시스템 포화
-
- 시스템 경계에 잇는 요청이 오랜 시간 후에 처리되는 것 보다는 시간 초과 되는 것을 원할 수 있음.
- ++ 시간 촉화 되었다고 해도 요청이 반복될 가능서잉 낮음
- ++ 메모리 내 요청을 저장하기 위한 메모리, 영속적인 대기열을 저장하기 위한 디스크 공간등 요청을 저장할 수 있는 리소스가 부족함.
- ++ 시간이 지날수록 요청 또는 전송 중인 데이터의 필요성이 줄어듬.
-
- = 오래된 데이터
-
- 어떤 데이터는 적절한 데이터가 있을 수 있음.
-
- 데이터가 만료되기 전에 처리 해야 함.
-
- context.WithDeadline, context.WithTimeout, context.Cancel 등을 이용
-
- = 데드락을 막으려는 시도
-
- 모든 동시 작업에 시간 초과를 설정하는 것은 불합리 하다.
-
- 시간 초과 기간을 설정하는 목적은 데드락을 막기 위한 것, 적당한 사간 내에 차단 해제될 만큼만 짧으면 됨.
-
- 데드락을 파하기 위해 시간 초과를 설정하려는 시도는 잠재적으로 시스템에서 데드락을 유발하는 문제를 라이브락을 유발하는 문제로 바꿀 수 있음.
- ++ 대형 시스템은 구성품이 훨씬 많으므로 재부팅을 통한 시스템 복구를 통한 데드락 보단, 시간이 허락되면 복구가 가능한 라이브락이 나음.
-
-
- 동시 프로세스가 취소 되는 이유
- = 시간 초과 - 암묵적인 취소
- = 사용자 개입
- = 부모 프로세스의 취소
- = 복제된 요청 - 여러곳에 동시 요청 빠른 응답 온곳 빼고 나머지는 취소..
-
-
3.하트비트
-
- 2가지 유형의 하트비트(HeartBit)
- = 일정 시간 간격으로 발생하는 하트비트
- = 작업 단위 시작 부분에서 발생하는 하트비트
-
- 데드락 또는 무한 또한 장시간 대기 같은 장애 시에 짧은 시간내에 원인 파악 가능
-
-
4.복제된 요청
-
- 일부 어픞리케이션의 경우, 가능한 빨리 응답을 받는 것이 최우선 과제.
-
- 성능상의 이점을 위해 일종의 교환을 할수 있음.
- = 여러 핸들러에게 요청을 복제할수 있고, 그중 하나는 다른 핸들러보다 빠르게 리턴 될 것.
- = 리소스를 소모를 해서 여러개를 돌리는 단점이 있으나, 빨리 응답 온것중 하나를 답하고, 나머지는 취소
- = 응답 속도가 비슷한 경우 리소스 소모하여 요청하는 것이 의미 없으므로 적용하면 안됨.
- = 설치, 유지보수 등의 비용이 비싸지만, 속도가 목적인 경우 사용.
-
-
5.속도 제한
-
- 속도 제한이란?
- = 리소스에 대한 접근을 단위 시간당 특정 횟수로 제한 하는 것.
- = 여기서 리소스는 API 연결, 디스크 읽기/쓰기, 네트워크 패킷, 에러 등
-
- 속도 제한을 적용하는 이유
- = 시스템이 사전에 정한 속도 경계 이상 벗어나지 않게 하므로 시스템의 성능, 안정성을 확보하고자 하는 것
- = 시스템 속도 제한을 도입하여 시스템의 전체 벡터 공격에 대한 차단을 하는 것.
- = 리소스가 허용하는 한 빠르게 시스템에 접근 할 수 있으면 악의적인 사용자는 모든 종류의 작업을 할 수 있음.
- = 시스템에 대한 요청의 속도를 제한하지 않으면, 쉽게 보안을 설정할 수 없음.
- = 악의적인 사용이 속도 제한의 유일한 이유는 아님.
-
- 분산 시스템에서 합법적인 사용자가 너무 많은 양의 작업을 수행하거나,
-
- 버그가 있는 코드를 수행하므로 인해 다른 사용자의 시스템 성능을 저하시킬수 있다.
-
-
- 증상
- = 분산 데이터베이스에서 쿠럼(Quorum:완료되었다고 여겨지는 작업에 반드시 응답해야 하느 서버의 수)을 잃어 버리고, 쓰기를 허용하지 않게 돼, 기존 요청이 실패하게 되며, 결국 뭔가 잘못된 일이 일어날 수 있음을 알 수 있음.
- = 결국 시스템이 스스로에게 Ddos 같은 공격을 수행하게 됨.
-
- Go 에서 속도 제한 알고리즘
-
= 토큰버켓(Token Bucket)
-
- 리소스를 이용하려면, 접근 토콘이 있어야 하며, 접근 토큰이 없는 경우, 사용이 제한된다.
-
- 토큰의 깊이는 d 이며, 한번에 d 개까지 토큰은 사용가능하다.
-
- 토큰이 사용가능할 때까지 대기열에서 기다리던가, 요청을 거부 한다.
- 참고 코드
import "[golang.org/x/time/rate](<http://golang.org/x/time/rate>)" func Open() *APIConnection { return &APIConnection{ apiLimit: MultiLimiter( rate.NewLimiter(Per(2, time.Second), 2), rate.NewLimiter(Per(10, time.Minute), 10), ), diskLimit: MultiLimiter( rate.NewLimiter(rate.Limit(1), 1), ), networkLimit: MultiLimiter( rate.NewLimiter(Per(3, time.Second), 3), ), } }
- 참고 코드2
type APIConnection struct { networkLimit, diskLimit, apiLimit RateLimiter } func (a *APIConnection) ReadFile(ctx context.Context) error { if err := MultiLimiter(a.apiLimit, a.diskLimit).Wait(ctx); err != nil { return err } // Pretend we do work here return nil } func (a *APIConnection) ResolveAddress(ctx context.Context) error { if err := MultiLimiter(a.apiLimit, a.networkLimit).Wait(ctx); err != nil { return err } // Pretend we do work here return nil }
-
-
-
6.비정상 고루틴의 치료
-
- 데몬과 같이 수명이 긴 프로세스에서는 수명이 긴 고루틴의 집합을 사용하는 것이 일반적
-
- 요점은 고루틴이 외부의 도움없이 복구 할수 없는 나쁜 상태에 빠지기 쉽다는 점.
-
- 장시간 사용하는 고루틴이 건강한 상태인지 확인하고, 건강한 상태가 아니면, 재시작하도록 하는 것이 도움이 됨.
-
- 고루틴을 관리하는 스튜어트(steward: 관리인)
-
- 모니터링 되는 고루틴을 와드(ward: 피후견인)
-
- 스튜어트는 와드의 고루틴이 비정상적인 상태가 되면 해당 고루틴을 재시작하도록 한다.
-
- 이를 위해 해당 고류틴을 재시작할 수 함수의 참조가 필요함.
-
-
-
6장 고루틴과 Go 런타임
댓글
댓글 쓰기