Envoy

개요

image.png
2016년 Lyft에서 개발하고 2017년 CNCF로 기증된 오픈소스 네트워크 프록시.
| The network should be transparent to applications. When network and application problems do occur it should be easy to determine the source of the problem.
어플리케이션의 네트워크를 투명하게, 문제가 발생했을 때 원인을 진단하는 것을 편하게 하는 것을 주목표로 개발됐다.[1]
C++을 이용해 개발되었고, 높은 부하를 견디며 좋은 성능을 가진 서버로서 기능하게 하는 것이 주된 목표이다.
여기에 프록시로서의 기능에 충실하기 위해 엔보이는 다양한 기능을 추가적으로 제공하고 있다.
현재 이스티오의 데이터 플레인으로서 톡톡히 역할을 수행하고 있다.

문서를 조금 보는데, 거의 백서 느낌으로 상세하게 돼있다..

디자인

엔보이를 처음 공부해보면, 생각보다 다양한 기능에 있다는 것에 놀라고, 무엇보다 생각 이상으로 설정 방법이 광대하고 복잡하다는 것에 놀란다.
엔보이를 조금 더 쉽게 이해하기 위해서는 먼저 엔보이가 무엇을 염두하여 디자인됐는지 아는 것이 도움이 된다.
내가 이해한 수준에서 엔보이는 다음의 요소들을 기반으로 디자인됐다.

이건 내가 엔보이를 공부하면서 막히거나 신기하게 느꼈던 부분들을 개념화시켜서 정리한 것이다.
다른 사람이 엔보이를 공부하면 이 범주화가 부족하다고 느껴질 수도 있으니 참고하자.

기능

기본적으로 프록시가 그러하듯이 엔보이는 L7 서버이며, 응용 계층의 정보를 이해하고 이에 대한 기능을 수행할 수 있다.
매우 다양한 기능을 수행할 수 있는데, 대략 보자면 다음과 같다.


기능들이 굉장히 많은데, 엔보이는 기본적으로 서비스 메시 환경을 기반으로 동작할 수 있도록 설계됐다.
어떤 네트워크 토폴로지 속에서도 동작할 수 있도록 가능한 많은 기능을 수행하면서 유연하게 동작할 수 있도록 개발됐다.
그래서 위의 설명만으로 엔보이를 이해하는 것은 매우 부족하고, 자세한 개념들과 설정법을 아는 것이 중요하다.

용어

엔보이에서 사용하는 용어들을 먼저 간단하게 짚어보자.

구조

컴포넌트 구조

일단 사용자가 엔보이를 설정할 때를 기준으로, 논리적인 엔보이의 전체 구조는 다음과 같다.

간략하게 봤을 때, 엔보이의 동작 흐름은 이렇게 된다.
한 마디로 요약하면, 리스너에서 트래픽을 받아 여러 필터를 거치고 클러스터로 라우팅된다고 정리할 수 있겠다.
사용자가 실질적으로 설정하게 되는 주된 영역은 필터 체인쪽에 있으며, 이쪽에서 다양한 엔보이의 기능을 활용할 수 있다.
유의할 점으로, 흐름을 도식화하고자 리스너와 필터를 전부 분리해 그렸으나, 필터 설정들은 사실 전부 리스너 안 속에 있다.

프로세스 구조

엔보이가 실제 실행되는 프로세스 차원에서 구조도를 보면 이런 식으로 생겼다.

프로세스 상으로 보자면, 엔보이는 싱글 프로세스 멀티 쓰레딩 구조를 가지고 있다.
nginx처럼 마스터-워커 개념이 있는데, 각 워커 스레드가 이벤트 기반으로 이벤트 루프(libevent)를 이용해 작업을 처리한다.
그래서 프라이머리 쓰레드(메인)가 워커 쓰레드를 여럿 만들고 초기 트래픽이 왔을 때 워커 스레드를 초기화하는 등의 작업을 수행한다.
동적 설정을 가할 때 이것을 처리하는 것도 바로 메인 스레드이다.

재밌는 특징 중 하나는 워커 스레드가 락을 걸지 않는다는 것인데, 이로 인해 엔보이는 자원 경합이나 데이터 정합성을 깨지 않고 효율적으로 작업을 처리한다.
이를 위해 매우 정교한 테크닉이 내장돼있는데, 자세한 내용은 여기를 보면 굉장히 도움될 것이다.[2]
기본적으로 각 워커 쓰레드는 독립적으로 트래픽을 작업하여 포워딩하는 작업을 수행한다.
보통의 트래픽은 stateless해 워커 쓰레드 간 독립성이 크게 문제되지 않으며, 커널 단의 컨트랙만으로 세션 유지등의 작업은 수행되도록 개발됐다.
다만 grpc와 같이 긴 연결이 필요한 요청을 처리하기 위해 커넥션 밸런싱을 통해 각 스레드로 가는 트래픽을 제어하기도 한다.

단일 요청 흐름 예시

그렇다면 조금 더 예시를 들어서 하나의 트래픽이 어떤 식으로 흘러가는지 살펴보자.[3]
다음의 상황을 가정하겠다.

엔보이에서 이걸 설정할 때, 아래와 같이 세팅할 수 있다.

# 엔보이가 처음 가동될 때 설정되는 필드.
static_resources:
  # 리스터 지정 필드
  listeners:
  - name: listener_https
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 443
    # tls 통신을 위해 sni를 추출하는 필터
    listener_filters:
    - name: "envoy.filters.listener.tls_inspector"
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
    # 특정 도메인에 적용될 필터 체인 정의
    filter_chains:
    - filter_chain_match:
        # tls 필터를 통해 이 이름이 추출된다.
        server_names: ["acme.com"]
      # 다운스트림과의 tls 통신을 위한 설정
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            tls_certificates:
            - certificate_chain: {filename: "certs/servercert.pem"}
              private_key: {filename: "certs/serverkey.pem"}
      filters:
      # HTTP connection manager 필터
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          use_remote_address: true
          http2_protocol_options:
            max_concurrent_streams: 100
          # 파일시스템에 로깅
          access_log:
          - name: envoy.access_loggers.file
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
              path: "/var/log/envoy/access.log"
          # 라우팅할 클러스터 지정
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["acme.com"]
              routes:
              - match:
                  path: "/foo"
                route:
                  cluster: some_service
		  # HCM(http connection manager)일 때 존재하는 필드.
          # 이 필터가 라우팅을 수행하기 위한 필터라는 명시하는 원소를 넣었다.
          # 여기에 커스텀 필터를 더 넣어서 기능을 추가할 수도 있다.
          http_filters:
          # - name: some.customer.filter
          - name: envoy.filters.http.router
		    # 이 http 필터는 라우팅을 담당하며, 이걸 아예 라우터 필터라고 부른다.
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  # 클러스터 지정 필드
  clusters:
  # 트래픽을 받게 될 클러스터
  - name: some_service
    # 업스트림과의 tls 통신을 위한 설정
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
	# 로드밸런서 세팅으로, 클러스터 내 하위 엔드포인트들을 지정한다.
    load_assignment:
      cluster_name: some_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 10.1.2.10
                port_value: 10002
        - endpoint:
            address:
              socket_address:
                address: 10.1.2.11
                port_value: 10002
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: 
            max_concurrent_streams: 100
  # stats 통계를 내기 위한 클러스터
  - name: some_statsd_sink
# 위 클러스터에 대한 설정이 담긴 필드
stats_sinks:
- name: envoy.stat_sinks.statsd
  typed_config:
    "@type": type.googleapis.com/envoy.config.metrics.v3.StatsdSink
    tcp_cluster_name: some_statsd_sink

딱 봐도 굉장히 복잡하다..
복잡한 만큼 다양한 기능을 넣고 커스텀하는 것이 가능하다.
이제 이 상황에서 요청 트래픽이 어떻게 흐름을 탈지 정리해보자.

내가 제대로 정리한 것이 맞는지 확신이 없는데, 조금 더 개념이 익숙해지면 다시금 정리해야겠다.
전체 흐름을 상세 도식으로 다시금 이해해보자.

리스너의 TCP 수락


일단 리스너 매니저는 리스너 설정을 관리하며, 트래픽이 들어올 때 해당하는 IP, 포트, 연결 프로토콜을 기준으로 리스너 인스턴스를 초기화한다.
각 리스너 인스턴스는 아래의 상태를 가질 수 있다.

이렇게 초기화된 인스턴스는 각 워커 스레드에 할당되며, 이때부터 본격적으로 설정을 통한 엔보이의 트래픽 작업이 수행된다.
한번 TCP 연결이 수락되면 이후 패킷들에 대해서, 커널 단에서 어떤 워커 스레드가 해당 연결을 받고 있는지, 어떤 리스너 인스턴스에 매핑되는지 추적하여 패킷을 보낸다.

리스너 필터 체인 생성


워커 스레드의 리스너 인스턴스는 리스너 필터 체인을 만든다.
이 필터 체인은 각 필터의 필터 팩토리(그냥 팩토리 패턴 말하는 거 같은데, 지피티왈 이 팩토리 자체는 메인 스레드가 가진다는 듯.)로부터 만들어지며, 이 팩토리는 필터의 설정 정보를 기반으로 각 연결에 대한 필터 인스턴스를 만들어 반환해준다.
이 예시에서는 TLS inspector 필터가 만들어지고, 이 필터가 초기 TLS 핸드쉐이킹에서 SNI값을 꺼낸다.
이제 SNI 값은 이후 필터 체인에서 사용될 수 있게 된다.
리스너 필터 체인은 이후에 네트워크 필터 체인(본격적으로 TCP 설정, HTTP 설정 진행)으로 연결된다.

virtual FilterStatus onAccept(ListenerFilterCallbacks& cb) PURE;

모든 필터들은 인터페이스로 위의 메서드를 구현한다고 한다.
이 메서드는 필터가 작업을 수행하는 중에 호출될 수 있는 콜백 메서드로, 이걸 기반으로 필터 작업을 도중에 멈추거나 재개하는 것이 가능하다.

TLS 복호화 후 네트워크 체인 필터 수행


중간에 TLS tranport socket 층이 생성된 것이 보인다.
이것 자체는 필터는 아닌데, TCP 연결 로직 간에 SslSocket::doRead() 함수로 실행되는 메서드라고 한다.
이 친구는 TLS 핸드쉐이킹을 수행하고, 복호화된 데이터를 이후 필터에 넘기는 역할을 한다.
워커 스레드는 이벤트 기반으로 동작하기 때문에, tls 통신 과정으로 인해 추가 통신이 오고가며 이 필터 파이프라인이 수행되지 못하는 순간에도 다른 커넥션을 처리할 수 있다.

리스너 필터 체인이 그러했듯이, 네트워크 필터 체인도 각각 팩토리로부터 필터 인스턴스가 만들어진다.
네트워크 필터는 커넥션으로부터 읽기, 쓰기 작업이 일어날 때만 호출되는 것도 가능하다.
이 예시에서 마지막 필터는 Http Connection Manager, 줄여서 HCM이다.
이 필터는 먼저 HTTP/2 코덱(http/2에서 헤더를 압축)을 복호화한다.
HTTP/2에서는 하나의 TCP 연결에 여러 스트림이 있을 수 있고, 이 스트림이 각각 하나의 요청과 응답을 나타내기 때문에 이를 분류하는 작업을 먼저 한다.

그리고 HTTP 필터 체인을 만들어 작업을 수행한다.
이때 만들어지는 필터는 다운스트림 HTTP 필터라고 부르는데, HTTP 관련 설정만 진행한다.
가령 위의 네트워크 체인 필터는 데이터 읽기 쓰기 작업에 호출될 수 있었는데, 여기에서는 요청이냐 응답이냐에 따라 호출될 수 있다.
각 독립적인 스트림에 대해 각각 체인이 만들어지고 작업이 진행된다.

HTTP 필터 중 마지막인 라우터 필터가 동작할 때 내부에 decodeHeaders()가 발동되면 라우팅 결정이 완료되며 클러스터가 선택된다.
HTTP 필터 체인이 초기에 경로 설정에 대한 정보가 있어 이를 기반으로 HCM은 최종 클러스터를 선택하게 된다.
근데 체인을 거치며 헤더 변경이나 각종 작업들이 수행될 수 있기에, HCM은 초기 필터 진행 당시 정보 이외에도 다시 경로 평가를 진행하기도 한다.
아무튼 이때 라우팅은 무조건 업스트림 클러스터 이름으로 설정된다.

여기에서 라우터 필터가 하는 일이 굉장히 많다.

라우팅 결정이 완료된 이후 업스트림 필터 체인이 동작하는 것이기에, 이쪽 필터는 라우팅에 변경을 가하는 조작은 불가능하다.
여기에서는 재시도 전략, 혹은 업스트림과의 연결 정보를 꺼내오는 정도의 필터를 넣을 수 있다.

업스트림으로 트래픽이 전달되는 과정


라우터 필터가 커넥션 풀에 정보를 요청할 때를 조금 더 상세하게 들어가보면, 먼저 건강한 상태이며 조건이 맞는 엔드포인트에 대해 로드밸런싱이 수행된다.
이 예시에서는 로드밸런싱 알고리즘을 명시하지 않았는데, 기본값은 라운드 로빈 방식이다.
업스트림과도 HTTP/2 통신을 하므로 들어온 요청에 대해 코덱 디코딩을 했듯이 여기에서는 다시금 코덱 인코딩이 수행된다.
마지막으로 TLS 암호화가 이뤄진다.

요청 작업 수행

이제 위의 과정을 계속 수행하면서 업스트림으로 트래픽을 전달한다.
돌아오는 응답은 여기에 필터 체인 부분에서 코덱 디코딩이나 TLS 암복호화가 역순으로 수행된다고 보면 된다.
HTTP/2에서는 통신을 하는 양측이 연결을 종료하는 게 가능한데, independent half-close 기능이 활성화됐다면 엔보이 내부의 스트림은 양쪽 연결이 완벽하게 종료됐을 때만 삭제된다.
다양한 방식으로 요청은 도중에 종료될 수 있다.

아무튼 요청이 완전히 종료된 이후에는 후속 작업이 진행된다.
일단 요청에 대한 통계 정보가 업데이트된다.
몇몇 통계는 요청 수행 중에 업데이트되나, 바로 실제 정보로 반영되지는 않고 주기적으로 메인 쓰레드에서 배치 처리로 동기화를 수행한다.
이후 액세스 로그가 파일시스템에 쓰여진다.
또한 트레이스 스팬이 종료된다.

XDS api

엔보이는 동적으로 서버 설정할 수 있도록 api를 노출하고 있는데, 이들을 통틀어 XDS라고 부른다.
왜 이리 부르는지는 종류를 보면 바로 알 수 있다.

이래서 xDS라고 부르는 것인데, 그럼 왜 Discovery Service인가?
위 api들은 기본적으로 흔히 api 요청을 날리는 방식으로 동작하는 것이 아니기 때문이다.
기본적으로 엔보이에서는 gRPC를 이용해 api를 정의하는데, 대략적인 동작 흐름은 다음과 같다.

이 설정들은 기본적으로 단일 요청으로 보내서 설정하는데, 각 api 설정 간 긴밀한 결합도를 가진 경우도 존재한다.
그래서 이럴 때 발생할 수 있는 문제를 막고자 ADS라고 트랜잭션형 api도 개발된 것이다.
또 하나, 이 설정은 비동기로 이뤄져서 설정의 순서가 반드시 보장되지는 않는다.
엔보이는 결국에는 적용된다는 관점, 즉 궁극적 일관성을 추구하는 식으로 설계됐기 때문이다.

엔보이는 애초에 서비스 메시 환경에서 운용할 것을 기본으로 만들어졌기 때문에, 설정할 때 각 엔보이에 통신을 하는 식이 아니라 엔보이가 설정 서버로부터 설정을 긁어오는 식으로 api가 개발됐다.
그래서 X에 대한 설정을 네트워크에서 탐색하여 가져와 적용하겠다, 해서 X Discovery Service인 것이다.

참고로 이 api를 적용하는 방법은 크게 3가지가 있다.[4]
가장 권장되는 방식은 당연히 gRPC를 이용하는 것으로, 각 api에 대한 proto 파일이 문서에 정리돼있다.
그래서 이스티오나, 엔보이에서 개발된 Go ControlPlane[5]과 같은 설정용 grpc api를 이용해주면 된다.
일반 rest endpoint에 대한 래퍼도 제공하긴 한다고 한다.
파일시스템의 변경을 기반으로 설정 파일을 적용하는 방법도 존재한다.
각 설정 방법들은 일단 초기 설정 파일에 명시적으로 설정돼있어야 적용되니 참고하자!

image.png
엔보이는 이번에 만들어진 v3 이후로 더 이상 메이저 버전을 업데이트하지 않겠다고 한다..

관련 문서

EXPLAIN - 파생 문서

이름3related생성 일자
이스티오 가상머신 통합이스티오 확장성2025-06-01 13:32
이스티오에서 엔보이 기능 확장하기이스티오 스케일링2025-06-01 14:06
이스티오의 데이터 플레인 트래픽 세팅 원리E-이스티오 가상머신 통합2025-05-27 21:55

기타 문서

Z0-연관 knowledge, Z1-트러블슈팅 Z2-디자인,설계, Z3-임시, Z5-프로젝트,아카이브, Z8,9-미분류,미완
이름6코드타입생성 일자
T-엔보이 실습 with solo.ioZ3topic/temp2025-04-14 23:15
8W - 엔보이와 iptables 뜯어먹기Z8published2025-06-01 12:14
엔보이에 와즘 플러그인 적용해보기Z8topic2025-06-09 02:29
6W - 이스티오 설정 트러블슈팅Z8published2025-05-18 01:31
7W - 엔보이 필터를 통한 기능 확장Z8published2025-06-09 02:30
8W - 가상머신 통합하기Z8published2025-06-01 12:11

참고

그나저나 엔보이 api 직접 만지려고 시도하다보니까, 새삼 이스티오가 선녀 같다..
쿠버네티스를 공부한 이래로 어떤 툴을 공부하는데 3일 이상 쓴 건 엔보이가 처음이다..


  1. https://www.envoyproxy.io/docs/envoy/v1.33.2/intro/what_is_envoy ↩︎

  2. https://cla9.tistory.com/191 ↩︎

  3. https://www.envoyproxy.io/docs/envoy/v1.33.2/intro/life_of_a_request#configuration ↩︎

  4. https://github.com/envoyproxy/envoy/blob/v1.33.2/source/docs/xds.md ↩︎

  5. https://github.com/envoyproxy/go-control-plane ↩︎