Concepts/Cloud Native

Fan-out, Fan-in

ocwokocw 2023. 11. 12. 23:34

- 출처: https://go.dev/blog/pipelines

 

Go Concurrency Patterns: Pipelines and cancellation - The Go Programming Language

Go Concurrency Patterns: Pipelines and cancellation Sameer Ajmani 13 March 2014 Introduction Go’s concurrency primitives make it easy to construct streaming data pipelines that make efficient use of I/O and multiple CPUs. This article presents examples o

go.dev

 

- 개요

Fan-out은 하나의 데이터소스가 닫힐때까지 다중 함수가 읽을 수 있는 것을 말한다. 때문에 CPU 사용이나 I/O를 병렬화 하기 위해 worker 그룹들에 작업을 분산시키는 방법으로 사용되기도 한다.

 

반면 Fan-in 은 그 반대라고 생각하면 되는데 다중 입력으로 부터 읽고 모든 입력 소스가 닫힐때까지 하나의 출력으로 다중화(multiplexing - 2개 이상의 저수준의 채널을 하나의 고수준의 채널로 통합하는 과정)하여 처리하는것을 말한다.


- Go example

Go의 동시성 지원은 강력하다. 이 때문에 Pipeline을 손쉽게 구성하여 다중 CPU나 I/O를 효율적으로 사용할 수 있다. Go에서 Pipeline은 channel로 연결된 stage 들의 나열이며, 각 stage는 같은 함수가 실행되는 goroutine 그룹이라고 할 수 있다. 하나의 예제 시나리오를 통해 fan-out과 fan-in을 사용하면 어떤 이점이 있는지 알아본다. 시나리오는 아래와 같다.

  • N을 입력받아 1부터 N까지 숫자를 차례로 생성한다.
    • 이는 실제 업무에서 N개의 작업이 생성되는것을 비유했다고 할 수 있다.
  • 생성된 숫자를 제곱처리한다. 이때 1초가 걸린다고 가정한다.
    • 시간이 조금 걸리는 상황을 위해 예제에서는 time.Sleep을 사용한다. 실제 업무에서는 계산이 오래 걸리나 I/O 작업, 다른 서비스를 호출하는 작업으로 생각할 수 있다.
  • 제곱된 수를 출력한다.

물론 하나씩 정성들여 처리할수도 있지만 이렇게 되면 자원을 효율적으로 사용할 수 없다. 시나리오를 하나씩 구현해가면서 Fan-out, Fan-in의 강력함을 느껴보자.

 

- N을 입력받아 1부터 N까지 숫자를 차례로 생성

const number = 10
genNumber1ToN := func(n int) <-chan int {
    out := make(chan int)
    go func() {
        for i := 0; i < n; i += 1 {
            out <- i + 1
        }
        close(out)
    }()
    return out
}
numChan := genNumber1ToN(number)

 

위의 코드는 1부터 10까지 숫자를 차례대로 생성하여 channel로 보낸 후, channel을 닫는 코드이다. channel로 전송하는 for loop가 goroutine 인 이유는 1~10까지 모두 보낸 후 다음 과정을 처리하는것이 아니라 10까지 모두 전송하지 않았어도 이미 전송된 숫자는 다음 과정인 "생성된 숫자를 제곱처리한다"는 단계를 동시에 수행할 수 있도록 하기 위함이다.

 

- 생성된 숫자를 제곱처리

const processDuration = time.Second
longAsyncProcessSQ := func(source <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for v := range source {
            time.Sleep(processDuration)
            out <- v * v
        }
        close(out)
    }()
    return out
}

 

 

데이터소스로 부터 데이터를 받아서 숫자를 제곱하고 channel에 전송하는 함수 "longAsyncProcessSQ"를 정의하였다. 이때 오래 걸린다는것을 모방하기 위해 Sleep 함수를 넣었다.

 

- Fan-out

const fanOutSize = 1
fanOut := make([]<-chan int, 0, fanOutSize)
for i := 0; i < fanOutSize; i += 1 {
    out := longAsyncProcessSQ(numChan)
    fanOut = append(fanOut, out)
}

 

함수 "longAsyncProcessSQ"을 실행하면 인자로는 하나의 데이터소스인 "numChan" channel을 사용하고, 해당 함수가 실행될때마다 결과를 반환하는 channel인 "out"이 생성된다. fanOutSize의 수에 따라 몇개의 goroutine이 병렬적으로 이를 수행하는지 결정된다. 코드 예시값인 1은 순차처리의 속도를 보기위함이다.

 

- Fan-in

func FanIn(sources []<-chan int) <-chan int {
	out := make(chan int)
	go func() {
		wg := sync.WaitGroup{}
		wg.Add(len(sources))
		for _, source := range sources {
			source := source
			go func() {
				for v := range source {
					out <- v
				}
				wg.Done()
			}()
		}
		wg.Wait()
		close(out)
	}()
	return out
}

...

result := FanIn(fanOut)
for resultV := range result {
    fmt.Println("result: ", resultV)
}

 

"FanIn" 함수는 Fan-out 단계에서 생성된 channel들을 인자로 받는다. 해당 channel들이 데이터를 모두 전송하기까지 기다린 후, 단일 채널에 결과를 취합한다.


- 결과 확인

결과를 확인해보기 위해 fanOutSize를 1, 5로 두번 수행해보자. fanOutSize가 1인 경우 순차처리 이므로 10초 가량이 걸린다. 하지만 5로 설정하면 5개의 goroutine이 수행되므로 2초 가량내에 업무를 끝낼 수 있다.

=== RUN   TestFan
result:  1
result:  4
result:  9
result:  16
result:  25
result:  36
result:  49
result:  64
result:  81
result:  100
--- PASS: TestFan (10.01s)

...

=== RUN   TestFan
result:  9
result:  16
result:  25
result:  1
result:  4
result:  100
result:  64
result:  36
result:  49
result:  81
--- PASS: TestFan (2.00s)