MSA/Istio

2. [envoy-internals] 쓰레딩 모델과 데이터 공유

cla9 2023. 5. 17. 14:09

1. 서론

 

이전 포스팅에서는 envoy를 이해하는데 아주 기초적인 컴포넌트 종류에 대한 설명을 진행했습니다. 이번 포스팅에서는 envoy 내부 여러 쓰레드가 존재하는데, 쓰레드간에 데이터는 어떻게 이루어지는지 그리고 쓰레딩 모델은 어떻게 구성되어있는지에 대해서 살펴보겠습니다. 본격적으로 진행하기 앞서 envoy는 멀티 쓰레딩 모델이지만 내부적으로 Lock을 통해 데이터 동기화를 수행하지 않습니다. 어떻게 Lock 없이 데이터 동기화가 가능할까요? envoy 쓰레딩 모델을 학습함으로써 궁금증을 해소해보는 시간을 가져보겠습니다.

 


2. Threading Model

 

Envoy 내부에는 전체를 관장하는 Main 쓰레드와 Network 요청과 라우팅을 처리하기 위한 여러 Worker 쓰레드가 존재합니다. 또한 그 외에도 파일 동기화를 위해 별도 File Flush 쓰레드가 별도로 존재합니다.

 

정리하자면 envoy 내부에는 Main, Worker, File Flush 쓰레드 총 3가지로 구성되었다고 볼 수 있습니다.

 

 

 

여기서 Main 쓰레드는 envoy 내부의 xDS 정보를 관리하고, Admin Server, Health Check 등 Envoy 프로세스가 수행하는데 있어 중추적인 데이터를 관리하고 부가적인 Admin 기능을 제공합니다. 따라서 Envoy를 기동하는데 있어 주요 Metadata 정보는 Main 쓰레드에서 생성되며, 필요 시 다른 쓰레드로 해당 정보를 공유합니다.

 

그렇다면 envoy 내부에서 Main 쓰레드와 다수로 생성되는 Worker 쓰레드간의 데이터 공유가 필요할 경우에는 어떻게 해야할까요?

 

 

첫 번째로 생각해볼 수 있는 방법은 쓰레드간 공유 Data를 생성한 다음에 Main 쓰레드에서 데이터 변경이 필요한 경우 Worker 쓰레드가 해당 공유 Data 접근이 모두 끝나는 시점에 Exclusive Lock을 걸어 Worker 쓰레드의 접근을 일시적으로 차단한 다음 데이터를 변경하여 쓰레드 안정성을 확보하는 방법입니다. 하지만 위와 같은 방식은 동시성이 떨어지는 단점이 존재합니다.

 

 

두 번째 방법은 모든 쓰레드에 걸쳐 동일한 데이터를 가지고 있는 상태에서 Data 변경이 발생하면, Main 쓰레드에서 데이터를 변경한 다음 모든 Worker 쓰레드에 수정된 Data를 전달하는 것입니다. 이 경우에는 특정 Worker 쓰레드가 바라보고 있는 원본 Data에 대해 Main 쓰레드에서 수정을 가해도 Worker 쓰레드가 바라보는 원본 Data는 해당 시점에 변경되지 않기 때문에 쓰레드 안정성을 확보할 수 있습니다.

 

위와 같이 설계된 디자인에서는 쓰레드 외부간에는 데이터가 공유되지 않지만, 내부에서는 해당 데이터를 전역적으로 공유해서 사용할 수 있습니다. 이러한 기법을 Thread Local이라고 부릅니다.

 

 

다만 위 예시에서는 1개의 데이터에 대한 동기화 과정을 표현했는데, envoy 내부에는 Main과 Worker 쓰레드 간의 xDS 업데이트를 위해 사용되는 데이터 항목이 많습니다. 따라서 실제로는 위 그림과 같이 하나의 데이터 인스턴스가 아니라 여러개의 Vector 자료 구조로 개별 쓰레드 별로 관리되어야 할 것입니다.

 

위 그림과 같이 데이터를 관리하면, Lock 없이 쓰레드 안정성을 다소 확보하는 것을 확인할 수 있었습니다. 그렇다면 여기에는 비효율이 없을까요?

 

가만 생각해보면, 개별 쓰레드 별로 동일한 데이터를 모두 가지고 있기 때문에 쓰레드 안정성은 확보할 수 있으나 똑같은 데이터가 모든 쓰레드에서 독립적으로 존재하기 때문에 메모리 낭비가 커지는 단점이 존재합니다.

 

따라서 envoy에서는 이를 해결 하기 위해서 Shared Pointer와 자체 TLS(Thread Local Storage)를 구축하였습니다. 이와 관련해는 조금 뒤에 더 자세히 다루어보겠습니다.


3. Distpatcher

 

Envoy의 TLS(Thread Local Storage)를 살펴보기 위해서는 내부의 중추적인 역할을 수행하는 Dispatcher에 대해서 알아야합니다. 따라서 TLS를 다루기 이전에 Dispatcher에 대해서 알아보고 넘어가겠습니다.

 

이전에 Threading Model을 통해서 메인 쓰레드와 Worker 쓰레드 사이에 데이터 공유가 이루어졌음을 확인했습니다. 그렇다면 Worker 쓰레드 입장에서는 메인 쓰레드가 보낸 데이터를 어떻게 확인하고 후속 작업을 처리할까요?

 

 

envoy에서는 Dispatcher가 중간 매개체 역할을 해줍니다. 여기서 Dispatcher는 Main 쓰레드에서 보낸 데이터를 수신받으면, 이를 감지하고 Worker 쓰레드에게 통지하여 후속 작업이 진행될 수 있도록 가교 역할을 수행합니다. 

 

 

 

해당 과정을 조금 더 자세히 살펴보면 위 그림과 같습니다.

 

envoy에서 Main 쓰레드를 포함하여 모든 쓰레드에는 Dispatcher가 존재합니다. 해당 Dispatcher는 자체적인 event loop를 가지고 있어서 별도의 life cycle이 존재합니다. 

 

이 경우 가령 위 그림과 같이 Main 쓰레드에 Event가 발행할 경우 Main 쓰레드는 개별 Worker 쓰레드에 존재하는 Dispatcher에게 Event의 Callback 내용을 전달합니다. Worker 쓰레드에는 전달된 Event를 내부 Buffer에 저장하며, 저장된 Buffer에서 하나씩 Event를 추출하여 작업을 수행합니다.

 

그렇다면 Dispatcher의 내부는 어떻게 구성되어있을까요?

 

 

envoy Dispatcher 내부에는 자체적인 event loop가 있다고 했는데, 해당 기능은 자체 구현하지 않고 C언어의 라이브러리인 libevent를 사용합니다. 다만 libevent가 C언어로 구성되어있기 때문에 메모리 관리를 위해 libevent에서 제공하는 자료구조를 unique_ptr 스마트 포인터로 감싸서 자동으로 관리할 수 있도록 Wrapping 하였습니다. 따라서 Dispatcher의 핵심은 libevent라고 볼 수 있습니다.

 

libevent 라이브러리는 다양한 기능을 제공하는데, 핵심 기능은 event가 발생되었을 때 이를 감지하고 알림을 전달해주는 역할을 수행합니다. libevent 공식 홈페이지를 보면 제공하는 기능에 대해서 보다 상세하게 소개되어있으며, 이에 대해서는 아래 공식 홈페이지를 참조하시기 바랍니다.

 

https://libevent.org/

 

libevent

The libevent API provides a mechanism to execute a callback function when a specific event occurs on a file descriptor or after a timeout has been reached. Furthermore, libevent also support callbacks due to signals or regular timeouts. libevent is meant t

libevent.org

 

 

지금까지 Dispatcher를 통한 Event 전달 과정과 해당 기능을 수행하는 libevent에 대해서 알아봤습니다. 이번에는 Dispatcher에서 Event를 전달하는 부분을 코드를 살펴보면서, 조금 더 수행 과정에 대해서 들여다보겠습니다.

 

 

 

 

 

먼저 쓰레드간에 데이터 공유 시, 쓰레드 별로 작업 부하가 다를 수 있기 때문에 전달된 Event가 즉시 처리될 수 없습니다. 따라서 Dispatcher 내부에는 Buffer를 위해 위와 같이 post_callbacks_ 라는 자료 구조에 Event Callback을 저장합니다.

 

 

위 코드를 통해서 방금 전 설명한 내용처럼 Dispatcher의 post를 통해서 Callback Event가 전달되면, 바로 처리하지 않고 내부에 존재하는 post_callbacks_ 자료구조에 해당 callback 함수를 저장하는 것을 확인할 수 있습니다.

 

그렇다면, 해당 callback은 언제 실행될까요?

 

 

위 코드는 Dispatcher의 생성자 부분입니다. 여기서 Dispatcher를 구성하기 위해 다양한 속성들이 Member Initializer에 의해서 매핑되고 있지만, Event 전달 관점에서 주목할 부분은 빨간색으로 밑줄 친 영역입니다. 

 

Dispatcher 내부에는 Scheduler가 존재하는데, post callback을 처리하기 위해서 별도 Scheduler를 통해 수행할 수 있습니다. 이때 위와 같이 runPostCallbacks()라는 메소드를 호출하는 것을 확인할 수 있습니다.

 

 

해당 메소드의 역할을 살펴보면, post_callbacks_에 저장된 내용이 하나도 없을 때까지 Event 내용을 하나씩 꺼내서 실행시키는 역할을 수행합니다. 이후 모든 작업이 완료되면, 향후 Post 메소드를 통해 Event가 유입될 때까지 작업을 진행하지 않습니다.

 

지금까지 Dispatcher 내부 동작 과정에 대해서 살펴봤습니다. 이번에는 Dispatcher가 취급하는 Event 종류에 대해서 알아보겠습니다.

 

 

 

 

Dispatcher를 통해 전달되는 Event는 모두 위 클래스를 상속받습니다. 해당 클래스 내용을 잠시 살펴보면, event 타입의 프로퍼티를 기본적으로 가지고 있으며, 해당 클래스가 소멸할 때 event를 release 하도록 되어있습니다.

 

 

해당 event를 상속하는 Event 타입은 위 그림과 같이 크게 3가지로 나뉩니다.

 

첫 번째는 Signal Event입니다. 이는 생성 즉시 Event를 발행할 때 사용되는 Event 타입입니다.

 

두 번째는 Timer Event입니다. 해당 Event는 내부 속성에 Timer가 존재하여, 지정된 시간이 지난 이후 Event를 발행할 수 있으며, Timer를 활성화하거나 비 활성화 등을 통해서 Timer를 조정할 수 있습니다.

 

세 번째는 File Event입니다. 해당 Event 타입은 File에 읽기 혹은 쓰기등의 내부 Event가 발생했을 때, 해당 Event를 전달하는 것을 목적으로 합니다. 또한 Linux의 경우 Socket은 File로 취급됩니다. 따라서 Socket을 쓰거나 읽을 때, 해당 Event를 감지하여 전달할 수 있습니다.

 

envoy에서는 위 세가지 Event 타입을 폭넓게 활용하여 여러 쓰레드간에 데이터 공유에 활용합니다. 

 

 


4. TLS(Thread Local Storage)

 

Threading Model을 설명하면서, 쓰레드마다 동일한 데이터를 보관할 경우 메모리 낭비로 인해 비효율이 발생함을 설명하고 이를 위해서 envoy에서는 TLS를 사용한다고 언급했습니다. envoy의 TLS에 대해서 알아보면서, envoy가 어떻게 메모리 낭비를 줄이기 위해 최적화 기법을 사용했는지를 살펴보겠습니다.

 

 

TLS는 말 그대로 Thread Local 객체들을 저장하는 저장소이므로 가장 먼저 해야할 일은 TLS에 저장되는 타입에 대한 정의가 필요합니다.

 

 

 

그에 따라 위와 같이 TLS에 저장하려는 데이터 타입에는 envoy에서 정의한 ThreadLocalObject 타입을 필수적으로 상속 받아야합니다. 그렇다면 ThradLocalObject 클래스는 어떻게 정의되어있을까요?

 

 

ThreadLocalObject 클래스를 살펴보면, 그 안에 어떠한 속성도 정의되어있지 않으며 asType 메소드가 존재하여 지정된 타입으로 캐스팅하여 반환하는 기능만을 제공하는 것을 확인할 수 있습니다.

 

또한 위 코드에서 주목할 부분은 ThreadLocalObject를 Shared Pointer로 감싼 타입을 새로 정의했다는 것입니다.

 

 

 

위 코드는 각 쓰레드별로 저장되는 TLS 구조의 모습입니다. 해당 구조체에는 각 쓰레드별로 구동되는 Dispatcher 정보를 저장할 수 있는 속성과, 이전 코드에서 확인한 ThreadLocalObject를 Shared Pointer로 감싼 타입을 Vector Container로 가지고 있는 것을 확인할 수 있습니다.

 

그렇다면 위와 같이 Shared Pointer 형태로 데이터를 관리하면 어떤 이점이 있을까요?

 

 

 

첫 번째로 얻을 수 있는 이점은 데이터 자체를 저장하는 것이 아니라 Pointer를 지니고 있으므로 데이터 중복을 줄일 수 있으며, 메모리 주소 값을 저장하므로 비용이 많이 절감됩니다.

 

두 번째는 Shared Pointer로 데이터를 가르키는 것이기 때문에 메모리 관리를 자동으로 수행할 수 있습니다. 

 

 

가령 위와 같이 Shared Pointer로 가르키는 오브젝트에 대한 링크가 모두 해제되었을 경우 참조 카운트가 0이므로 자동으로 메모리를 해제할 수 있습니다.

 

그렇다면 위와 같이 Shared Pointer를 사용하게 되면 모든 문제가 해결될까요? 정답은 그렇지 않습니다. 위와 같이 구성하게 되면 결국 처음에 야기되었던 문제점이 다시 발생합니다. 그 이유는 결국 공유 데이터를 가르키는 것이기 때문에 결론적으로는 쓰레드 안정성에 대한 이슈가 다시 불거지기 때문입니다.

 

따라서 envoy에서는 기존에 살펴본 Dispatcher와 각 쓰레드별로 Shared Pointer를 지정하도록 구성함으로써 이러한 문제를 해결하고자 했습니다. 이와 관련해서는 다음 시나리오를 통해서 확인해보겠습니다.

 

 

위와 같이 두 개의 쓰레드에서 서로 동일한 Thread Local Object를 가르키고 있다고 가정해봅시다. 이러한 상황에서 Thread Local Object의 업데이트가 필요할 경우 다음과 같은 과정을 수행합니다.

 

 

 

 

1. 변경을 수행해야하는 쓰레드에서 Thread Local Object를 새로 만들고 기존 Thread Local Object를 가르키던 포인터를 신규 Object를 가르키도록 변경합니다.

 

2. 변경을 수행한 쓰레드에서 다른 쓰레드에게 변경 내용을 전파합니다. 따라서 해당 쓰레드의 Dispatcher에게 post 요청을 통해 변경 Event를 전달합니다.

 

3. Dispatcher는 전달된 Event 내역을 자신의 buffer에 저장하였다가 쓰레드에게 데이터 변경 내용을 전달합니다.

 

 

4. Event를 전달받은 쓰레드에서는 Event를 통해 전달된 Callback을 수행하면서 자신이 가르키고 있는 Thread Local Object를 신규로 변경합니다. 이 과정을 통해 모든 쓰레드에서 새로운 Thread Local Object로 변경이 완료되면, 기존의 Thread Local Object의 참조 카운트는 0이 되므로 삭제할 수 있습니다.

 

위와 같이 4가지 과정을 수행하면, 쓰레드 안정성을 확보할 수 있습니다. 그 이유는 첫 번째 단계를 살펴보면, 두 개의 쓰레드가 공유 데이터를 바라보고 있다가 특정 쓰레드에서 데이터 변경이 수행되었을 경우 다른 쓰레드에 영향을 미치지 않기 때문입니다. 이후 다른 쓰레드의 event loop 수행 과정에서 변경 내용을 통지받으면 그제서야 변경된 데이터를 바라볼 수 있도록 적용됩니다.

 

이는 데이터 변경이 발생되었을 때 동일 시점에 모든 쓰레드에서 공유 데이터를 바라볼 수 있는 Strict Consistency는 포기하더라도 Eventually Consistency를 적용함으로써 쓰레드 안정성, 메모리 효율성과 성능을 얻는 것이 이점이 크기 때문입니다.

 

지금까지 TLS에 대해서 기본적인 동작 과정에 대해서 개략적으로 살펴봤습니다. 이번에는 조금 더 구체적으로 Thread Local Data를 활용해서 Envoy내 쓰레드 끼리 어떻게 동기화를 수행하는지에 대해서 살펴보겠습니다.

 


4-1 Envoy TLS 동작 과정

 

Envoy 내부에는 TLS를 관장하기 위해 InstanceImpl 인스턴스가 존재합니다. 이를 위해 해당 클래스 내부에는 TLS와 쓰레드간 상호작용을 위한 여러 메소드와 속성이 포함되어있습니다. 코드를 토대로 해당 클래스에 대해서 자세하게 살펴보겠습니다.

 

 

위 코드에서 먼저 눈에 띄는 것은 TLS를 위한 자료구조인 ThreadLocalData 구조체가 해당 클래스내에 포함된 것을 확인할 수 있습니다. 또한 ThreadLocalData는 C++의 thread_local 키워드가 붙어있어 쓰레드내 전역적으로 공유되는 데이터임을 알 수 있습니다.

 

 

 

그 다음 눈 여겨 볼 점은 dispatcher를 저장하는 점과 registered_threads_ 속성을 통해 해당 쓰레드와 연결되는 쓰레드들을 관리한다는 점입니다.

 

void InstanceImpl::registerThread(Event::Dispatcher& dispatcher, bool main_thread) {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);

  if (main_thread) {
    main_thread_dispatcher_ = &dispatcher;
    thread_local_data_.dispatcher_ = &dispatcher;
  } else {
    ASSERT(!containsReference(registered_threads_, dispatcher));
    registered_threads_.push_back(dispatcher);
    dispatcher.post([&dispatcher] { thread_local_data_.dispatcher_ = &dispatcher; });
  }
}

 

실제 Thread 등록은 Worker 쓰레드가 생성되는 시점에 registerThread 메소드를 호출하여 InstanceImpl에 등록됩니다. 이때 호출되는 코드를 살펴보면, 등록되는 Thread가 Main 쓰레드인 경우와 그렇지 않은 경우가 분리되어서 저장되는 것을 확인할 수 있습니다.

 

 

그밖에 InstanceImpl 코드를 더 살펴보면,  그 전에는 보지 못했던 Slot 구조체와 이를 저장하는 slots_ Vector Container가 존재하는 것을 알 수 있습니다. Slot은 무엇이고 어떤 역할을 수행할까요?

 

 

InstainceImpl은 ThreadLocalData를  thread_local 속성으로 가지고 있지만, 사용자가 ThreadLocalData에 데이터를 할당하고자 할때 직접 할당하는 것을 방지하도록 설계되어있습니다.

 

대신 Client는 ThreadLocalData에 대한 추가,수정,삭제 작업을 희망할 경우 InstanceImpl로 부터 Slot을 할당 받아야합니다. 여기서 Slot은 ThreadLocalData에 작업 수행 및 ThreadLocalData를 공유하는 모든 쓰레드에게 명령을 요청하기 위한 일종의 interface라고 볼 수 있습니다.

 

SlotPtr InstanceImpl::allocateSlot() {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);

  if (free_slot_indexes_.empty()) {
    SlotPtr slot = std::make_unique<SlotImpl>(*this, slots_.size());
    slots_.push_back(slot.get());
    return slot;
  }
  const uint32_t idx = free_slot_indexes_.front();
  free_slot_indexes_.pop_front();
  ASSERT(idx < slots_.size());
  SlotPtr slot = std::make_unique<SlotImpl>(*this, idx);
  slots_[idx] = slot.get();
  return slot;
}

 

Slot을 할당받기 위해서는 Client는 InstanceImpl에게 allocateSlot() 메소드를 호출할 수 있습니다. 이때 내부적으로는 비어있는 Slot을 관리하는 free_slot_indexes가 존재하는데, free한 Slot이 비었을 경우 Slot을 추가로 생성해서 반환하는 것을 볼 수 있습니다. 반면 비어있는 Slot이 존재한다면, 해당 Slot의 index를 기반으로 Slot을 반환합니다.

 

 

위 코드에서 주목할 부부은 Slot을 생성할 때, 두 번째 인자에 비어있는 Slot의 index를 지정한다는 점입니다. 따라서 Client 입장에서 전달받은 Slot에서는 Instance 내부에 존재하는 slots_  list에서 몇 번째 index에 Slot이 생성되었는지 알 수 있습니다.

 

void InstanceImpl::SlotImpl::set(InitializeCb cb) {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!parent_.shutdown_);

  for (Event::Dispatcher& dispatcher : parent_.registered_threads_) {
    // See the header file comments for still_alive_guard_ for why we capture index_.
    dispatcher.post(wrapCallback(
        [index = index_, cb, &dispatcher]() -> void { setThreadLocal(index, cb(dispatcher)); }));
  }

  // Handle main thread.
  setThreadLocal(index_, cb(*parent_.main_thread_dispatcher_));
}

 

Client는 Slot을 할당 받으면, Thread Local에 저장하기 위한 Callback Event를 등록할 수 있습니다. 이때 위 코드와 같이 set 메소드를 통해 저장할 수 있으며, 코드 내용을 자세히 살펴보면, 등록된 Thread를 모두 순회하면서 dispatcher에게 post 메소드를 호출하여, 각자 쓰레드가 보유한 setThreadLocal을 호출하도록 요구하는 것을 볼 수 있습니다.

 

해당 과정을 통해서 쓰레드 내의 변경 사항을 다른 쓰레드에 전파하게 됩니다.

 

void InstanceImpl::setThreadLocal(uint32_t index, ThreadLocalObjectSharedPtr object) {
  if (thread_local_data_.data_.size() <= index) {
    thread_local_data_.data_.resize(index + 1);
  }

  thread_local_data_.data_[index] = object;
}

 

setThreadLocal 코드는 위와 같습니다. 해당 코드 내용을 보면 ThreadLocalData 구조체 내에있는 data_에 접근하여 Slot에 지정된 index에 해당하는 위치에 Callback을 저장하는 것을 볼 수 있습니다.

 

 

 

Client에서 수행할 수 있는 두 번째 요청은 runOnAllThreads 입니다. InstanceImpl로 부터 전달받은 Slot에는 index 값과 더불어 InstanceImpl에 대한 parent_ 속성에 저장되어있습니다.

 

void InstanceImpl::SlotImpl::runOnAllThreads(const UpdateCb& cb) {
  parent_.runOnAllThreads(dataCallback(cb));
}

 

따라서 Client 입장에서 전달받은 Slot에 runOnAllThreads 명령을 요청하면, 내부적으로는 InstanceImpl에게 runOnAllThreads를 실행시키도록 위임합니다.

 

void InstanceImpl::runOnAllThreads(Event::PostCb cb) {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);

  for (Event::Dispatcher& dispatcher : registered_threads_) {
    dispatcher.post(cb);
  }

  // Handle main thread.
  cb();
}

 

요청을 전달받은 InstanceImpl에서는 이전과 마찬가지로 registered_threads_에 등록된 dispatcher 들을 순회하면서 사용자가 전달한 Callback을 전달하고 Main 쓰레드 내에서 해당 Callback 또한 실행시킴으로써 연관된 모든 쓰레드에서 사용자 요청을 처리하도록 작업을 수행합니다.

 

지금까지 InstanceImpl을 통해 쓰레드를 등록, Thread Local Object 할당 및 Dispatcher를 활용한 데이터 공유 과정에 대해서 살펴봤습니다. 이번에는 Envoy가 종료될 때 TLS 초기화가 어떻게 이루어지는지 살펴보겠습니다.

 

 

void InstanceImpl::shutdownGlobalThreading() {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);
  shutdown_ = true;
}

 

Envoy가 종료될 때, InstanceImpl에 suhtdownGlobalThreading() 메소드를 호출하여 종료를 요청합니다. 다만 해당 메소드의 역할은 InstanceImpl 내부에 존재하는 shutdown_ bool 값을 true로 변경하는 것입니다.

 

void InstanceImpl::shutdownThread() {
  ASSERT(shutdown_);
  for (auto it = thread_local_data_.data_.rbegin(); it != thread_local_data_.data_.rend(); ++it) {
    it->reset();
  }
  thread_local_data_.data_.clear();
}

 

이후 개별적인 Worker 쓰레드는 shutdownThread() 메소드를 호출함으로써 쓰레드 별로 종료 과정이 이루어집니다. 이때 위 코드에서 주목할 부분은 ThreadLocalData 내부 데이터를 종료할 때, 뒤에서부터 reset 과정이 진행되는 점입니다.

 

그 이유는 ThreadLocalData를 구성할 때 가장 먼저 적재되는 데이터는 ClusterManager에서 생성되는 ThreadLocalObject이고 해당 데이터들은 종료될 때까지 수정이 이루어지지 않는 데이터이면서, 여러 Object에서 참조될 수 있는 가장 중요한 데이터일 수 있습니다.

 

따라서 중요도가 가장 낮은 Object 부터 종료를 시작해서 가장 높은 Object를 나중에 종료시킴으로써 의존성이 강한 Object가 먼저 삭제되어 발생될 수 있는 문제를 예방합니다.

 


5. 마치며

 

지금까지 Envoy 쓰레드 모델과, TLS에 대해서 살펴봤습니다. TLS를 이해하기 위해서는 Dispatcher의 기능 중 일부에 이해해야하기 때문에 이 부분에 대해서도 다루어봤습니다.

 

또한 코드를 살펴보면서 Envoy에서 Lock을 사용하지 않고도 공유 데이터를 사용하기 위해 많은 고민이 반영되었음을 확인할 수 있었습니다.

 

Envoy의 쓰레딩 모델과 TLS를 이해하는 것은 istio를 이해하는데 있어서도 중요합니다. Envoy 내부에서 xDS API 업데이트가 발생되었을 때 내부적으로 TLS를 통해서 데이터 변경을 전파하고 반영하기 때문입니다. 그러한 측면에서 이번 포스팅 내용은 어렵지만 중요하다고 볼 수 있습니다.