Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TCP 서버 구현으로 알아보는 golang 내부 동작

Avatar for flangdu flangdu
September 04, 2024

TCP 서버 구현으로 알아보는 golang 내부 동작

golang에서 제공하는 걸 활용하여 빠르게 TCP Server를 만들어보고 내부를 탐험해보는 것을 목표로 합니다.

Avatar for flangdu

flangdu

September 04, 2024
Tweet

More Decks by flangdu

Other Decks in Technology

Transcript

  1. - golang에서 제공하는 걸 활용하여 빠르게 TCP Server를 경험해보자!!(TCP 서버는

    golang 내부를 보기 위한 구현 정도입니다…) - golang에서 제공하는 것들의 내부를 탐험해보자!! 발표 목표!
  2. TCP server를 만들면서 필요한 golang의 synchronization primitive 학습(실습) - golang에서

    제공하는 동시성 제어를 위한 자료구조를 TCP 서버 구현에 넣어보자! - 서버를 구현하면서 발생하는 동시성 문제를 해결해보기 golang 내부 동작원리 학습(이론 + 시간되면 golang/go 코드 분석) - 해당 자료구조 내부는 어떻게 동작할까? - golang 내부는 어떻게 동작할까? 대략적인 발표 흐름
  3. - golang runtime scheduler 동작 과정과 설계 원리 - golang의

    synchronization primitive를 많이 활용하기(sync, atomic, goroutine, chan, …) - golang의 synchronization primitive 내부 동작 주로 다룰 것
  4. - golang 기초(기초 없이 바로 코드짜기부터 시작합니다) - TCP 자체에

    대한 이해(동작 원리 및 네트워크) - 메모리와 GC 다루지 않을 것
  5. 목차 1부: TCP 서버 구현 1. net 패키지를 이용한 서버

    구현 2. TCP 서버 라이브러리화 3. select문 순위 지정 & 이벤트루프 구현 4. broadcast 서버 구현 2부: golang runtime 내부 1. golang runtime 스케줄링 2. 컨테이너 자료형의 가용성과 일관성 3. 채널 송수신 및 select문 동작 원리
  6. 아래 interface를 이용하면 서버가 동작할 수 있도록 구현해보기! TCP 서버

    라이브러리화 type TCPServerHandler interface { OnOpen(conn *Conn) OnClose(conn *Conn) OnRead(conn *Conn, b []byte) (n int) OnReadError(conn *Conn, err error) OnWriteError(conn *Conn, err error) }
  7. 다음 4개의 구조체 구현 TCP 서버 라이브러리화 type TCPServer struct

    type TCPServerConfig struct type Conn struct type ConnConfig struct TCP conn entrypoint https://github.com/atgane/tcpgo/tree/tcp_conn
  8. Write가 비동기적으로 처리되어야 하는 이유: 만약 여러 커넥션에 쓰는 상황에

    하나의 커넥션의 장애로 인해 다른 커넥션이 대기하고 있게 된다면? write pending -> read pending -> conn read pending 밀림이 전파된다!! TCP 서버 라이브러리화 eventloop를 블로킹하면 안되는 이유(라인) https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part3
  9. Eventloop[T] 구현할 메서드 대상 이벤트루프 구현: sync 패키지 활용 type

    Eventloop[T any] struct { queue chan T handler func(T) dispatchCount int closeCh chan struct{} closed atomic.Bool state atomic.Int32 } eventloop entrypoint https://github.com/atgane/tcpgo/tree/eventloop
  10. select문 랜덤 실행 select문 랜덤 실행에서 순서 지정하기(2중 select, early

    return) 순서지정에서도 완벽하게 해결 불가능한 문제 소개: 만약 2중 select 사이에서 마지막 select의 case 통과조건을 모두 갖추었다면? select문의 실행 우선순위 지정
  11. Eventloop[T] 구현할 메서드 이벤트루프 구현: sync 패키지 활용 func NewEventloopConfig()

    *EventloopConfig func NewEventloop[T any](handler Handler[T], config *EventloopConfig) *Eventloop[T] func (e *Eventloop[T]) Run() func (e *Eventloop[T]) Send(event T) (err error) func (e *Eventloop[T]) Close() func (e *Eventloop[T]) ForceClose() func (e *Eventloop[T]) dispatch()
  12. 일반적인 구현에서 문제점 - select에서 어떤 case가 실행될 지 모름

    - 닫힌 채널 send로 인한 패닉 가능성 - Send에서 err가 nil인 이벤트에 대한 실행을 보장하지 못함 이벤트루프 구현: sync 패키지 활용
  13. epoll wait: conn배열에 대한 read 이벤트 처리 eventloop를 이용한 react

    pattern: 고루틴 개수 최적화 (reuseport를 이용한 backlog queue 다중화?) 만약 TCPServer에서 성능을 더 올리고 싶다면? reuseport: https://github.com/libp2p/go-reuseport
  14. N:M 스케줄링 - 고루틴과 쓰레드의 대응 관계는 N:M - 스케줄러는

    고루틴을 실행할 쓰레드에 할당하는 스케줄링 작업 진행 go scheduler M M g M M g g g g g
  15. golang runtime의 내부 구현 GMP 모델: golang에서 쓰레드를 관리하는 방법

    구성 요소 - P(processor): 가상의 프로세서. 각 P마다 고루틴을 실행할 큐를 가짐 - M(machine): 쓰레드 - G(goroutine): 고루틴. 애플리케이션에서 쓰레드 역할을 수행하는 대상
  16. go scheduler global runnable queue Lock P P M M

    g local runnable queue local runnable queue main goroutine
  17. go scheduler global runnable queue Lock P P M M

    g local runnable queue local runnable queue main goroutine 논리적 프로세서: goroutine 실행 관리
  18. go scheduler global runnable queue Lock P M g local

    runnable queue main goroutine 1. newproc으로 goroutine 생성 후 queue에 삽입 g g
  19. local runnable queue FIFO LIFO g0 P M G main

    goroutine 1. LIFO에 먼저 삽입 runqput https://github.com/golang/go/blob/77cc7fbc842f221d54a635757eaf88d17fe67971/src/runtime/proc.go#L6684
  20. local runnable queue FIFO LIFO g1 P M G main

    goroutine 1. LIFO에 먼저 삽입 g0 2. LIFO 고루틴이 FIFO로 이동
  21. local runnable queue FIFO LIFO g99 P M G main

    goroutine g0 g1 g97 g98 … 1. LIFO에 먼저 삽입 2. LIFO 고루틴이 FIFO로 이동
  22. local runnable queue FIFO LIFO g99 P M g99 main

    goroutine g0 g1 g97 g98 … 1. LIFO에서 먼저 삭제
  23. local runnable queue FIFO LIFO P M g0 main goroutine

    g0 g1 g97 g98 … 2. 다음으로 FIFO 실행
  24. go scheduler global runnable queue Lock P P M M

    g local runnable queue local runnable queue main goroutine
  25. go scheduler global runnable queue Lock P P M M

    g local runnable queue local runnable queue main goroutine g 1. newproc으로 goroutine 생성 후 queue에 삽입
  26. go scheduler global runnable queue Lock P P M M

    g local runnable queue local runnable queue main goroutine g 1. newproc으로 goroutine 생성 후 queue에 삽입 2. wakep 호출로 다른 p 활성화
  27. go scheduler global runnable queue Lock P P M M

    g local runnable queue local runnable queue main goroutine g 1. newproc으로 goroutine 생성 후 queue에 삽입 2. wakep 호출로 다른 p 활성화 3. p에서 실행할 작업 탐색(findrunnable)
  28. 61번째 스케줄마다 확인 P grq lrq netpoller P lrq 비었으면

    grq 확인 lrq 확인 netpoller 확인(네트워크) 다른 p에 대한 작업 훔치기 findrunnable 내부 findrunnable https://github.com/golang/go/blob/beaf7f3282c2548267d3c894417cc4ecacc5d575/src/runtime/proc.go#L3249
  29. g go scheduler global runnable queue Lock P P M

    M g local runnable queue local runnable queue main goroutine g 1. newproc으로 goroutine 생성 후 queue에 삽입 2. wakep 호출로 다른 p 활성화 4. stealWork로부터 작업 훔치기 3. p에서 실행할 작업 탐색(findrunnable) golang runtime scheduler 내부 구조
  30. g go scheduler global runnable queue Lock P P M

    M g local runnable queue local runnable queue main goroutine g 1. newproc으로 goroutine 생성 후 queue에 삽입 2. wakep 호출로 다른 p 활성화 4. stealWork로부터 작업 훔치기 g 5. 고루틴 실행 3. p에서 실행할 작업 탐색(findrunnable) golang runtime scheduler 내부 구조
  31. local runnable queue FIFO P M g0 g1 g98 g99

    … G FIFO에서 고루틴을 빼내어 실행
  32. cpu가 메모리 접근 속도를 높이기 위해 캐시를 이용하기 때문! 따라서

    가장 마지막에 삽입한 고루틴을 먼저 실행하여 캐시 적중을 높인다! 1. 왜 1 LIFO + n FIFO 조합인가?
  33. local runnable queue FIFO P M g0 g1 g97 g98

    … G LIFO g99 가장 마지막에 저장한 고루틴을 빼내어 실행
  34. 고루틴 실행 순서 보장: 고루틴을 생성된 순서로 실행? 캐시를 이용한

    실행 성능 향상: 가장 최근 생성한 고루틴을 먼저 사용하여 메모리 재사용! vs
  35. … go scheduler global runnable queue g g g g

    M M M M M Lock grq 접근을 위해 lock을 사용하면?
  36. 2. local runnable queue는 왜 있을까? global queue만 존재할 때

    고루틴을 선점하기 위해 여러 쓰레드가 락을 획득해야 한다. 따라서 락을 최소화하기 위해 쓰레드 별 로컬 큐를 이용한다!
  37. … go scheduler global runnable queue g g g g

    M M M Lock g g g g g g g g 만약 쓰레드가 블록되면?
  38. … go scheduler global runnable queue g g g g

    M M M Lock M M M M 여유로운 쓰레드가 작업훔치기를 시도해야 한다!
  39. M M M M M M M M M M

    M M M 그러나 블로킹이 너무 많이 된다면?
  40. 3. 그럼 p는 왜 존재하는가? 큐에 고루틴이 없는 쓰레드가 대기하고

    있는 수 많은 쓰레드를 확인해야 한다. 따라서 쓰레드와 로컬 큐를 분리하고 실행 가능한 쓰레드를 일정히 유지한다!
  41. 런타임 내부 구현 정리 1. 왜 lrq는 1 LIFO +

    n FIFO? FIFO만 쓰면 되는거 아닌가? > 최근 생성된 고루틴 메모리를 재사용하여 캐시 히트를 높이기 위해 2. 왜 lrq쓰는지? grq만 쓰면 되는 거 아닌가? > grq에 대한 락 경합을 줄이기 위해 3. 왜 p가 존재하는지? 쓰레드 + 고루틴으로 충분하지 않나? > 실행 가능한 쓰레드의 블로킹 쓰레드에 대한 작업 훔치기 횟수를 줄이기 위해
  42. 만약 Map에 Mutex를 이용하면? callback이 loop로 실행되는 동안 Lock을 점유하고

    있어 g2의 접근에 대기가 발생한다. 그럼 RWMutex라면?
  43. 1 2 3 4 Map element 1 callback function g1

    loop function g2 Load(4) X
  44. 1 2 3 4 Map element 1 callback function g1

    loop function g2 Store(5)로 바꾸면? 5
  45. 만약 Map에 RWMutex를 이용하면? callback이 loop로 실행되는 동안 RLock을 점유하고

    있어 Store명령이 실행되지 않는다. CRUD에서 CUD는 방어하는 게 맞나? 일단 방어해보자 !
  46. 1 2 3 4 Map element 1 callback function g1

    loop function error! delete(1)
  47. 1 2 3 4 Map element 1 callback function g1

    loop function error! X delete(1) 데드락 발생!
  48. 만약 Map에 RWMutex를 이용하면? CUD를 방어하면 RLock을 획득하는 callback 내부에서

    Map에 대한 CUD를 진행했을 때 Lock을 획득하려고 시도하기 때문에 데드락이 발생할 수 있다. 그럼 락을 안잡아야 하나?
  49. CAP theorem 일관성(C): 모든 읽기 요청은 가장 최근의 쓰기 요청을

    수신하거나 일관성을 보장할 수 없는 경우 오류 발생 가용성(A): 모든 요청은 노드가 다운되거나 사용할 수 없는 경우에도 오류 없는 응답을 받음 파티션 내성(P): 노드 간 임의의 수의 메세지가 손실되더라도 시스템 지속 작동 https://docs.aws.amazon.com/ko_kr/whitepapers/latest/availability-and-beyond-improving-resilience/cap-theorem. html
  50. 1 2 3 4 Map Replica 1 callback function g1

    loop function 1 2 3 4 error!
  51. 1 2 3 4 Map Replica 1 callback function g1

    loop function 1 2 3 4 error! delete(1)
  52. 1 2 3 4 Map Replica 1 callback function g1

    loop function 2 3 4 error! delete(1)
  53. Map 1 callback function g1 g2 1 2 3 4

    Replica loop function 1 2 3 4
  54. Map 1 callback function g1 g2 some CUD에도 동작 이상

    X 1 2 3 4 Replica loop function 1 2 3 4
  55. 복제를 이용한 가용성을 증가시킨 방법 map의 loop function이 실행되기 전

    replica를 구성하고 락을 획득하지 않고 callback을 실행시킨다! 실제 sync.Map은 내부에 read와 write를 분리 (*sync.Map)Range https://github.com/golang/go/blob/4e548f2c8e489a408033c8aab336077b16bc8cf7/src/sync/map.go#L449
  56. chansend: recvq에 고루틴이 존재하는 경우 (비어있음) buf g g recvq

    chansend 고루틴 & 데이터 g g 1. recvq에서 고루틴 pop 2. chansend 고루틴에서 데이터를 recv 고루틴에게 전달 runnable queue 3. 대기하는 고루틴 상태 변경
  57. chansend: 대기가 필요한 경우 buf runnable queue g g g

    sendq & 데이터 1. 꽉 찼으니 sendq에 저장, 고루틴 상태 변경
  58. chanrecv: sendq에 고루틴이 존재하는 경우 buf g 2. buf 내부의

    데이터 recv 고루틴에 전달 1. sendq에서 고루틴 pop runnable queue 5. 대기하는 고루틴 상태 변경 g sendq & 데이터 chanrecv 고루틴 g 3. send 고루틴 데이터 이동 4. recvx, sendx 인덱스 증가 및 회전 g
  59. chanrecv: 대기가 필요한 경우 runnable queue g (비어있음) buf g

    g 1. 비었으니 recvq에 저장, 고루틴 상태 변경
  60. for { m1 := sync.Mutex{} m2 := sync.Mutex{} go func()

    { m1.Lock() defer m1.Unlock() m2.Lock() defer m2.Unlock() fmt.Println("m1m2") }() go func() { m2.Lock() defer m2.Unlock() m1.Lock() defer m1.Unlock() fmt.Println("m2m1") }() } 1 2
  61. for { m1 := sync.Mutex{} m2 := sync.Mutex{} go func()

    { m1.Lock() defer m1.Unlock() m2.Lock() defer m2.Unlock() fmt.Println("m1m2") }() go func() { m2.Lock() defer m2.Unlock() m1.Lock() defer m1.Unlock() fmt.Println("m2m1") }() } 1. 상호 배제 1 2 하나의 고루틴에서만 접근 가능
  62. for { m1 := sync.Mutex{} m2 := sync.Mutex{} go func()

    { m1.Lock() defer m1.Unlock() m2.Lock() defer m2.Unlock() fmt.Println("m1m2") }() go func() { m2.Lock() defer m2.Unlock() m1.Lock() defer m1.Unlock() fmt.Println("m2m1") }() } 1. 상호 배제 2. 점유 대기 1 2 하나의 자원을 획득하고 다른 자원 획득 대기
  63. for { m1 := sync.Mutex{} m2 := sync.Mutex{} go func()

    { m1.Lock() defer m1.Unlock() m2.Lock() defer m2.Unlock() fmt.Println("m1m2") }() go func() { m2.Lock() defer m2.Unlock() m1.Lock() defer m1.Unlock() fmt.Println("m2m1") }() } 1. 상호 배제 2. 점유 대기 1 2 X 3. 비선점 서로의 자원을 강탈할 수 없음
  64. for { m1 := sync.Mutex{} m2 := sync.Mutex{} go func()

    { m1.Lock() defer m1.Unlock() m2.Lock() defer m2.Unlock() fmt.Println("m1m2") }() go func() { m2.Lock() defer m2.Unlock() m1.Lock() defer m1.Unlock() fmt.Println("m2m1") }() } 1. 상호 배제 2. 점유 대기 1 2 4. 상호 대기 X 3. 비선점 1 -> 2 점유 자원 대기 2 -> 1 점유 자원 대기
  65. for { c1 := make(chan bool) c2 := make(chan bool)

    go func() { select { case <-c1: fmt.Println("g1 c1") case c2 <- true: fmt.Println("g1 c2") } }() go func() { select { case <-c2: fmt.Println("g2 c1") case c1 <- true: fmt.Println("g2 c2") } }() }
  66. for { c1 := make(chan bool) c2 := make(chan bool)

    go func() { select { case <-c1: fmt.Println("g1 c1") case c2 <- true: fmt.Println("g1 c2") } }() go func() { select { case <-c2: fmt.Println("g2 c1") case c1 <- true: fmt.Println("g2 c2") } }() } 안전한 코드일까? case가 순서대로 실행: 데드락 가능성 존재 case가 랜덤으로 실행: 데드락 가능성 존재 실제는 case가 랜덤으로 실행
  67. for { c1 := make(chan bool) c2 := make(chan bool)

    go func() { select { case <-c1: fmt.Println("g1 c1") case c2 <- true: fmt.Println("g1 c2") } }() go func() { select { case <-c2: fmt.Println("g2 c1") case c1 <- true: fmt.Println("g2 c2") } }() } 안전한 코드일까? 그럼 select마다 채널 어떻게 대기하는지 다 고려해줘야 하나!!
  68. 안전하다 왜? 락을 획득하는 채널의 순서를 채널의 메모리 순으로 정렬하여

    데드락을 회피한다! selectgo https://github.com/golang/go/blob/3959d54c0bd5c92fe0a5e33fedb0595723efc23b/src/runtime/select.go#L121
  69. for { c1 := make(chan bool) c2 := make(chan bool)

    go func() { select { case <-c1: fmt.Println("g1 c1") case c2 <- true: fmt.Println("g1 c2") } }() go func() { select { case <-c2: fmt.Println("g2 c1") case c1 <- true: fmt.Println("g2 c2") } }() } 두 select 구문은 내부 케이스에서 c1, c2의 메모리가 같은 순서로 정렬되어 락을 획득하기 때문에 데드락X 1 2
  70. select 동작 요약 1. 탐색할 케이스 섞기 2. 락은 채널

    주소에 따라 힙 정렬 3. 실행이 바로 가능한 케이스 탐색 -> 있으면 실행 4. 없으면 순서에 따라 채널에 대기로직 등록 5. 깨어나면 나머지 케이스 정리 및 실행
  71. golang runtime schedule gophercon 2021: queues, fairness, and the go

    scheduler https://youtu.be/wQpC99Xu1U4?si=BntJmR8q884EoLYv gophercon 2018: scheduler saga https://youtu.be/YHRO5WQGh0k?si=N10D4l-SV9xyAF8k gophercon 2017: understanding channel https://youtu.be/KBZlN0izeiY?si=jaFL9AFaM3LBLpaI diving deep into the golang channels https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8 추가자료
  72. 메모리 & GC Golang GC 튜닝 가이드(kakao) https://tech.kakao.com/posts/618 naver 개발

    생산성 10배 높이기: from c++ to Golang(naver) https://tv.naver.com/v/16972079 Go의 메모리 과다 사용 문제 해결해보기(Akita) https://blog.insane.pe.kr/1511 추가자료
  73. 이외 이벤트루프를 블로킹하면 안되는 이유(라인) https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part3 CAP 정리(AWS) https://docs.aws.amazon.com/ko_kr/whitepapers/latest/availability-and-beyond-improving-resilience/cap-theorem.html CAP

    이론을 통한 네트워크 동기화 기법(넥슨) https://youtu.be/j3eQNm-Wk04?si=ABZoagbk6ECTVp0C why discord is switching from Go to Rust(discord) https://discord.com/blog/why-discord-is-switching-from-go-to-rust CNCF landscape(CNCF) https://landscape.cncf.io/ 추가자료