본문 바로가기
CNCF Projects/Envoy

Envoy architecture - Introduction + Threading model

by cscscs 2023. 11. 20.

Envoy architecture - Introduction + Threading model

Introduction

envoy architecture에서는 공식문서 (Envoy architecture overview)와 envoy 레포지토리 (v1.28), lyft 엔지니어셨던 Matt Klein님이 작성해 주신 envoy 블로그 글을 번역, 참고해서 architecture를 이해해 볼 예정입니다.

 


 

envoy 레포 구조

envoy의 레포지토리 구성은 REPO_LAYOUT.md에 정리되어 있습니다. architecture를 이해하기 위해서는 아래 폴더들에 집중합시다.

  • api/: envoy의 dataplane API
  • envoy/: core Envoy를 위한 "Public" 인터페이스 헤더들. 100% abstract class들이 있도록 구성했습니다. public header에 추상화되지 않은(not-abstract) class들이 있긴 합니다 (*보통 성능 이슈).  "core"들은 HTTP connection manager filter 같은 몇 "extension" 들과 컴파일 관점에서 optional 할 수 없는 엔보이의 기반이 되는 연관된 기능들을 포함합니다. 
  • source/:  extensions를 포함한 core Envoy를 위한 소스 코드들이 포함됩니다. 자세한 레이아웃은 별도 기술되어 있습니다. 우선 용어 정리 (ex. bootstrap...)등이 안되어 있는 상태여서, 각각 architecture 소개 시에 같이 소개하도록 하겠습니다.
  • test/:  core Envoy와 extension들에 대한 테스트코드들이 포함됩니다. 테스트코드도 마찬가지로 하위 구조가 복잡한데, 차차 보도록 하겠습니다.

 


Terminology (*용어 정리)

우선 envoy에서 기본적으로 사용되는 용어들에 대해 먼저 정리해 보도록 하겠습니다.

  • Host: network communication을 가능하게 하는 entity (모바일폰의 어플리케이션이나 서버 등). 여기서 host는 logical network application을 뜻합니다. 하드웨어의 물리적 여러 호스트들을 가질 수 있습니다. (단, 각 호스트들은 독립적으로 address 되어야 함)
  • Downstream: Envoy와 연결되고, 요청을 보내고, resopnse를 받는 downstream host를 뜻합니다.
    (=엔보이에 붙어있는 서버)
  • Upstream: envoy로부터 connection과 request들을 받고 response를 반환하는 upstream host를 뜻합니다.
    (=엔보이에 붙어있는 서버가 요청을 보내려는 서버)
  • Listener:  listener은 이름이 지정된 network location (ex. port, unix domain socket 등)으로, downstream client로부터 연결될 수 있습니다. Envoy는 downstream host가 연결하는 하나 이상의 listener들을 노출합니다. 
  • Cluster: envoy가 연결하는 논리적으로 비슷한 upstream host들의 그룹을 뜻합니다. envoy는 service discovery를 통해 cluster member을 discover 합니다. active health checking을 통해 optional 하게 cluster 멤버들의 health 상태를 결정할 수 있습니다. envoy가 요청을 라우팅 하는 cluster member는 load balancing policy로부터 결정됩니다.
  • Mesh: 일관된 network topology를 제공하기 위해 협력하는 host group을 뜻합니다. 이 문서에서는 "Envoy mesh"는 다양한 서비스와 애플리케이션 플랫폼으로 구성된 분산 시스템의 메세지 전달 기반을 형성하는 Envoy proxy들의 집합을 뜻합니다.
  • Runtime configuration: out of band 실시간 configuration 시스템은 envoy와 함께 배포됩니다. Configuration 세팅을 변경해서 envoy를 restart 하거나 주요한 설정을 변경하는 것 없이 운영에 영향을 줄 수 있습니다. 

 


 

Threading model

Envoy는 multithread, single process architecture을 사용합니다.

몇몇의 다른 worker thread들이 listening, filtering, forwarding을 수행하면서, single primary 스레드는 다양한 간헐적인 coordination 작업을 컨트롤합니다.

connection들이 listener로부터 accept 되었을 때, connection의 남은 lifetime은 single worker thread에 바인딩됩니다.  envoy의 대부분이 대체로 단일 스레드로 운영될 수 있고, 소량의 더 복잡한 코드가 워커 스레드 간의 coordination 처리를 합니다. 

일반적으로 Envoy는 100% non-blocking 하도록 작성되었습니다.

Listener connection balancing

기본적으로, worker thread 간에 coordination은 아예 없습니다. 즉, 모든 worker thread는 독립적으로 각 리스너의 커넥션을 받으려 시도하고, 스레드 간에 알맞은 벨런싱을 수행하는 것을 kernel에 의존합니다.

대부분의 workload에 대해, 커널은 들어오는 connection을 잘 벨런싱 합니다. 다만, 적은 수의 매우 긴 long-lived connection (eg. HTTP2/gRPC egress인 경우)과 같은 몇 workload에 대해서는, 커널만으로 의존된 스케줄링은 잘 동작하지 않을 수 있습니다.
이런 경우  worker thread 간 connection을 강제로 벨런싱 하는 것이 이상적입니다. 위와 같은 상황을 처리할 수 있도록, envoy는 각 리스너에 설정될 수 있는 다양한 connection balancing type을 지원합니다.

 


 

Threading overview

https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Envoy는 세 개의 다른 종류의 스레드를 사용합니다.

Main

이 스레드는 다음 역할을 담당합니다.

  • server의 startup, shutdown
  • 모든 xDS API handling (DNS, health checking, 일반적인 cluster management를 포함)
  • runtime
  • stat flushing
  • admin
  • 일반적인 프로세스 관리 (signal, hot restart 등

해당 스레드에서 일어나는 모든 것은 비동기(asynchronous)하고, "non-blocking"합니다.
보통, 메인 스레드는 처리하는데 많은 양의 CPU가 들지 않는 모든 중요한 process functionality를 조정합니다.
이는 대부분의 관리 코드가 단일 스레드인 것처럼 작성될 수 있게 해 줍니다.

Worker

기본적으로, Envoy는 시스템의 모든 하드웨어 스레드마다  worker thread를 생성합니다.  (이는 --concurrency 옵션을 통해 제어될 수 있습니다.)
각 worker thread는 "non-blocking" event loop를 실행합니다. (현재 구현에서는 listner의 sharding은 없습니다.). 새로운 연결을 수락하고, 연결에 대한 filter stack을 인스턴스화하며, connection의 lifetime에 대한 모든 IO를 처리합니다.
즉, 대부분의 connection handling code들을 단일 스레드인 것처럼 작성될 수 있게 해 줍니다.

File flusher

엔보이가 작성하는 모든 파일은 현재 독립적인 blocking flush thread를 갖고 있습니다. 이는 filesystem 캐시 된 파일에 O_NONBLOCK을 사용하더라도, 종종 block 당할 수 있기 때문입니다(?!). 워커 스레드가 파일을 쓰려할 때, 데이터는 실제로 in-memory 버퍼로 이동합니다. 여기서 file flush thread를 통해 최종적으로 flush 됩니다. 이는 기술적으로 모든 워커가 메모리 buffer을 채우려 할 때 같은 lock에 의해 block 될 수 있는 코드 중 하나입니다.


 

Connection handling

위에서 짧게 언급했듯이, 모든 worker thread는 임의의 sharding 없이 모든 리스너들을 listen 합니다. 따라서 커널은 지능적으로 승인된 소켓을 worker thread에 분배하는 데 사용됩니다. 현대의 커널은 이를 매우 잘 수행합니다. 현대의 커널은 IO priority boosting과 같은 feature를 사용해서 한 스레드의 작업을 먼저 완전히 수행하려 시도하고, 그 후 같은 소켓을 listen 하고 있는 다른 스레드들을 활용합니다. 또한 각 accept를 처리하기 위해 단일 spin-lock으로 처리하지 않습니다.

connection이 worker로부터 accept 되면, 해당 connection은 accept 된 worker에서 처리됩니다 (위에서 언급). 모든 connection의 추가적인 처리는 forwarding을 포함해서 워커 스레드 내에서 완전히 처리됩니다. 이는 몇 중요한 의미를 갖고 있습니다.

  • Envoy의 모든 connection pool은 각 worker thread당 하나씩 있습니다.. 따라서 HTTP/2 connection pool이 각 upstream host에 대해 단 하나의 연결만 만들더라도, 네 개의 워커가 있는 경우, 안정 상태에서 각 upstream host당 4개의 HTTP/2 connection이 있게 됩니다. 
  • Envoy가 이런 방식으로 작동하는 이유는 모든 것을 단일 워커 스레드 내에 유지함으로써 거의 모든 코드를 lock 없이 마치 단일 스레드인 것처럼 작성할 수 있습니다. 이 디자인은 대부분의 코드를 작성하기 쉽게 만들고, 거의 무제한 워커 수에 대해서도 놀라울 정도로 잘 확장됩니다.
  • 하지만, 주요한 교훈 중 하나는, 메모리 및 connection pool의 효율성 측면에서 --concurrency 옵션을 조정하는 것은 매우 중요하다는 것입니다. 필요한 것보다 많은 워커를 가질 경우, 메모리 낭비가 발생하고, 더 많은 idle 연결을 생성하며, connection pool의 hitrate를 낮출 수 있습니다. Lyft에서는 Envoy 사이드카를 매우 낮은 concurrency로 운영해서, 그들 옆에 있는 서비스들의 성능과 대략적으로 일치하도록 합니다. 반면 엣지 Envoy는 최대 동시성으로만 운영됩니다.

 


 

What non-blocking means

"non-blocking"이라는 용어는 지금까지 메인 스레드와 워커 스레드가 어떻게 동작하는지에 대해 논의하면서 몇 번 사용되었습니다. 작성된 모든 코드는 아무것도 block 되지 않음을 가정하고 있습니다. 하지만, 해당 명제는 완벽히 참이 아닙니다. Envoy는 몇 프로세스 전체에 걸친 lock을 사용합니다.  

  • 이미 논의된 것처럼, access log가 작성된다면, 모든 worker들은 in-memory access log buffer을 채우기 전에 같은 lock을 얻습니다.  Lock이 잡는 시간은 매우 작아야 합니다. 하지만 이 lock이 high concurrency와 high throughput 상황에서는 경쟁 상태에 빠질 수 있습니다.
  • Envoy는 thread local 통계를 처리하는 매우 정교한 시스템을 사용합니다. 스레드 로컬 통계 처리의 일부로, 때때로 중앙 "stat store"에 lock을 획득할 필요가 있습니다. 이 lock은 절대 흔히 경쟁돼서는 안 됩니다.
  • 메인 스레드는 주기적으로 모든 워커 스레드와 coordinate 될 필요가 있습니다. 이는 메인스레드에서 worker thread로 "posting"함으로써 수행됩니다. (종종 반대로도 합니다.) Posting은 발송된 메세지가 큐에 안전하게 저장하고, 나중에 전달될 수 있도록  Lock을 필요로 합니다. 이 Lock들도 또한 절대 자주 경쟁되어서는 안 됩니다. 다만, 이 lock들은 기술적으로 block 할 수 있습니다.
  • Envoy가 스스로 standard error에 대해 로깅할 때, 이는 process-wide lock을 얻습니다. 일반적으로 Envoy의 로컬 로깅은 끔찍한 퍼포먼스를 수행할 것이 예상됩니다. 따라서 이를 개선하기 위해 많은 생각을 하지 않았습니다.
  • 몇 다른 임의의 lock들이 있습니다. 하지만 그중 어느 것도 퍼포먼스에 심각한 영향을 주지 않고, 그들끼리 경쟁하지 않습니다.

 


 

Thread local storage

Envoy가 메인 스레드의 책임들을 워커스레드의 책임들로부터 분리하는 방법 때문에, 메인 스레드에서 복잡한 처리가 수행되고, 고도로 concurrent 한 방식으로  각 워커 스레드가에 제공될 필요가 있습니다. Envoy high level에서의 Thread Local Storage (TLS) 시스템에 대해 다뤄보겠습니다. 이는 cluster 관리를 핸들링하는 데 사용됩니다.

https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

이미 논의했던 것처럼, 메인 스레드는 필수적으로 Envoy process 내의 모든 management/control plane functionality를 다룹니다. (Control plane은 약간 과부하 상태이지만, envoy process 자체와, 다른 worker들이 수행하는 forwarding과 비교할 때 적절해 보입니다.) 메인 스레드가 약간의 작업을 처리하는 것은 흔한 패턴입니다. 작업의 결과로 각 work thread를 업데이트할 필요가 있습니다. worker thread는 모든 접근에 대해 lock을 얻을 필요가 없습니다.

Envoy의 TLS 시스템은 다음과 같이 동작합니다:

  • 메인스레드에서 도는 코드는 process-wide TLS slot에 할당될 수 있습니다. 추상화되어 있지만, 실제로는 O(1) 접근을 허용하는 vector로의 index입니다.
  • 메인 스레드는 자신의 slot으로 임의의 데이터를 설정할 수 있습니다. 이것이 완료되었을 때, 데이터는 각 worker로 일반 이벤트루프 이벤트인 것처럼 post 됩니다.
  • 워커스레드는 자신의 TLS slot을 읽을 수 있고, 거기에서 사용가능한 스레드 로컬 데이터를 검색할 수 있습니다.

매우 단순하지만, 이는 RCU locking concept와 매우 비슷한 굉장히 강력한 패러다임입니다. (필수적으로, worker thread는 작업을 처리하는 동안, TLS slot의 데이터의 변화를 절대 보지 않습니다. change는 오직 work event 간의 고요한 기간 동안만 발생합니다.) Envoy는 TLS 시스템을 두 다른 방식으로 사용합니다.

  • lock을 걸지 않고 각 워커가 접근 가능한 다양한 데이터를 저장하는 데에
  • 각 워커에 읽기 전용 글로벌 데이터에 대한 공유 포인터를 저장하는 데에.
    따라서 각 워커는 데이터에 대한 작업 수행 동안 감소될 수 없는 ref count를 갖습니다. 오직 모든 워커들이 고요해지고, 새로운 공유 데이터를 로드한 후에야 이전 데이터가 destroy 됩니다. 이는 RCU와 동일합니다.

 

 


 

Cluster update threading

https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

위 그림은 전체 플로우를 보여줍니다.

  1. Cluster manager은 모든 알려진 upstream cluster, CDS API, SDS/EDS API, DNS, active(out-of-band) health checking을 관리하는 Envoy 내부 컴포넌트입니다. Cluster manager은 모든 upstream cluster의 일관된 관점을 만드는 역할을 합니다. (upstream cluster은 발견된 host 뿐 아니라 health status도 포함) 
  2. Health checker은 active health checking을 수행하고, health state의 변화를 cluster manager에게 보고합니다.
  3. CDS/SDS/EDS/DNS는 cluster membership을 결정하기 위해 수행됩니다. 상태의 변화는 cluster manager에게 보고됩니다.
  4. 모든 worker 스레드는 지속적으로 이벤트루프를 실행합니다.
  5. cluster manager이 cluster의 상태 변화를 결정할 때, Post handler / TLS update는 새로운 cluster state의 read-only snapshot을 만들고, 이를 모든 worker thread에 보냅니다.
  6. 다음 조용한 기간 동안, worker thread는 할당된 TLS slot에 snapshot을 업데이트합니다.
  7. host를 어디로 loadbalance 할지 결정이 필요한 IO 이벤트동안 load balancer은 host information을 얻기 위해 TLS slot에 쿼리 합니다. 어떤 lock도 이를 수행하기 위해 필요하지 않습니다. 

이전에 말한 절차들을 사용함으로써, envoy는 어떤 lock도 걸지 않고 모든 요청들을 처리할 수 있습니다. TLS code 자체의 복잡성을 넘어서, 대부분의 코드는 어떻게 스레드가 동작하는지 알 필요 없고, single threaded 되도록 작성할 수 있습니다. 이는 대부분의 코드를 작성하기 쉽고 엄청난 퍼포먼스를 낼 수 있도록 만들어주었습니다.

 


 

Other subsystem that make use of TLS

TLS와 RCU는 Envoy에서 확장성 있게 사용됩니다. 아래는 몇 예시입니다.

  • Runtime override lookup: 현재 feature flag over ride map은 main thread에서 계산됩니다. read-only snapshot은 각 worker에게 RCU 시멘틱을 사용해 제공됩니다.
  • Route table swapping: RDS로부터 제공되는 route table들에 대해 route table들은 메인 스레드에서 인스턴트화되어있습니다. read-only snapshot은 각 worker들에게 RCU 시멘틱을 사용해서 제공합니다. 이는 route table을 효율적으로 atomic 하게 바꿔줍니다.
  • HTTP data header caching: 모든 요청에 대한 HTTP 날짜 header을 계산하는 것은 매우 비쌉니다. 중앙화된 Envoy는 날짜 헤더를 0.5초에 대해 계산하고, 각 worker에게 TLS와 RCU를 통해 제공합니다.

 


 

Known performance pitfalls

전체적인 envoy의 퍼포먼스는 좋지만, high concurrency와 throughput으로 운영하는 경우 주의를 기울여야 하는 몇 알려진 부분들이 있습니다.

  • in-memory buffer에 대한 access log를 작성할 때, 현재 모든 worker들은 lock을 얻어야 합니다. 높은 concurrency와 높은 throughput에서, 최종 파일에 기록할 때 순서가 뒤바뀔 수 있지만, 작업자 별 액세스 로그의 배치 처리가 필요합니다. 대안적으로 access log들은 worker thread 별로 될 수 있습니다.
  • stats가 매우 심히 최적화되어 있지만, 매우 큰 concurrency와 throughput 상황에서, 각 stats에 대해 atomic 한 경쟁이 있을 수 있습니다. 이에 대한 해결책은 중앙 카운터로 flushing 되는 per-worker counter입니다.
  • 기존 아키텍처는 Envoy가 매우 적은 수의 connection이 많은 자원을 요구하는 시나리오에 배포되는 경우 잘 작동하지 않습니다. 이는 connection이 worker들 사이에 고르게 분포될 것이라는 보장이 없기 때문입니다. 이 문제는 worker이 connection을 다른 worker에게 forwarding 해 처리할 수 있는 worker connection balancing을 구현함으로써 해결될 수 있습니다.

 

'CNCF Projects > Envoy' 카테고리의 다른 글

Envoy architecture - Listener + Listener/Network Filter  (3) 2023.11.22
Envoy proxy란?  (0) 2023.11.16