동시성

조성열·4일 전

eBPF

목록 보기
5/5
post-thumbnail

들어가며

Kernel Level에서 동작하는 eBPF Program들이 수집한 정보를 eBPF Map에 저장하여 상태를 공유한다고 했다. eBPF Program이 정보를 수집하고 Map에 기록할 때 복수의 프로그램이 공유 자원에 접근을 시도하면 Race Condition, Dead Lock과 같은 상황에 놓일 수 있는데, 이번 포스트에선 이러한 문제를 어떻게 해결하는지 정리 해보도록 하겠다.

eBPF Map

eBPF Map에 대해 정리한 포스트입니다.

Atomic Operation

Atomic Operation은 멀티 스레드 환경에서 데이터 무결성 유지를 위한 연산이다. 예를 들어 일반적인 연산은 초기 i 값이 있고 1을 더하는 연산을 하면 i+1을 반환하는 형식이다. 하지만 Atomic Operation을 통한 연산을 수행하면 같은 연산을 해도 i라는 초기값이 그대로 반환된다.

Atomic Operation은 외부에서 아예 시작되지 않은 것처럼 보여야하고, 연산이 수행되는 동안 InterruptContext Switching에 의해 중단되지 않는다는 특성을 갖는다. 이를 통해 여러 Program이 연산을 동시에 수행할 수 있는 것이다. 이런 연산은 다음과 같은 함수들을 이용해서 수행할 수 있다.

  • __sync_fetch_and_add(*a, b)
    주소 a 에서 값을 읽어와 b 를 더한 후 a 위치의 원래 값을 반환
  • __sync_fetch_and_sub(*a, b)
    위에서 add 함수와 마찬가지로 동작하고 뺄셈 연산
  • __sync_fetch_and_or(*a, b)
    주소 a의 값을 읽고, 여기에 특정 숫자를 비트 OR 연산한 뒤 다시 저장하며, 원래 a 값을 반환
  • __sync_fetch_and_xor(*a, b)
    주소 a의 값을 읽고, 특정 숫자를 비트 XOR 연산한 뒤 다시 저장하며, 원래 a 값을 반환
  • __sync_val_compare_and_swap(*a, b, c)
    주소 a의 값을 읽어 b, c에 대해 swap 연산을 한 뒤 원래 a의 값을 반환
  • __sync_lock_test_and_set(*a, b)
    주소 a의 값을 읽어 b로 설정하고 원래 a의 값을 반환

위와 같이 공유 자원에 대해 각각의 연산을 수행해도 반환 값은 항상 동일한 값으로 유지되어 무결성이 유지되고, Race Condition과 같은 문제 상황이 발생하지 않는 것이다.

Spin Lock

동시성을 위한 또다른 해결 방법으로 Spin Lock 사용이 있다. Spin LockSemaphore, Mutex와 같이 상호 배제를 위한 방법으로 공유 자원에 대한 lock을 획득할 때까지 Thread가 멈추지 않고 루프를 돌며(busy-waiting) 재시도하는 동기화 기법이다.

struct concurrent_element {
    struct bpf_spin_lock semaphore;
    int count;
}

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, int);
    __type(value, struct concurrent_element);
    __uint(max_entries, 100);
} concurrent_map SEC(".maps");

Spin Lock을 사용하려면 먼저 Map 값 맨 위에 struct bpf_spin_lock이 포함되어야 한다.

...
    bpf_spin_lock(&read_value->semaphore);
    read_value->count += 1;
    bpf_spin_unlock(&read_value->semaphore);
    return 0;
}

한 번에 두 개 이상의 Lock을 점유할 수 없고, 사용 후에는 Deadlock 방지를 위해 반드시 Lock을 해제 해야한다. 해제하지 않으면 Verifier에 통과할 수 없게 된다.

CPU 별 Map 할당

이 방법은 각 logical CPU마다 Map의 복사본을 부여하는 것이다. 각 CPU에 자체 메모리를 할당하므로 공유 메모리 접근이 없어 메모리 접근 동기화 문제를 해결할 수 있다. 하지만 logical CPU가 많아지면 그만큼 메모리 할당량도 증가하는 문제가 있다. 또한 할당된 메모리만큼 더 많은 데이터를 읽고 처리해야 하기 때문에 User Level의 복잡도가 증가한다.

MAP RCU(Read, Copy, Update)

이 방법은 Map 값을 직접 수정하는 것이 아니라, 헬퍼 함수를 통해 값을 업데이트 하는 것이다.
bpf_map_lookup_elem 함수를 통해 얻은 포인터를 사용해서 BPF 스택에 Map 값을 복사해 와서 수정하고, bpf_map_update_elem을 통해 수정된 복사본을 호출하는 방식으로 동작한다.
여러 업데이트가 동시에 발생하면 누락이 될 수 있다는 문제가 있지만 동시성 문제는 해결할 수 있다. 또한, 메모리 복사 과정에서 오버헤드가 존재하지만, Spin lock 방식에서 사용하는 block이나 동기화를 하지 않기 때문에 값의 크기에 따라 더 효율적일 수 있다.
User Level에서 이뤄지는 Map 업데이트는 이 방식을 따른다.

Map-in-Map

User Level에서 Map을 조회할 때는 모든 key 값을 순회 해야한다. 하지만 이런 과정에서 중간에 Map값이 변하는 문제가 발생하게 된다. 이런 문제를 해결하는 Map-in-Map은 Map에 대한 스냅샷 기능을 제공한다.
Map의 key 값을 담은 외부 Map에서 내부 Map에 대한 포인터 정보를 얻어 내부 Map에 저장된 정보에 접근한다. 이때 접근 과정에서 값이 변하면 안되므로 새로운 Map을 생성하여 교체한다.

마치며

공유 자원에 대한 동시성 문제를 어떤 방식들로 해결하는지에 대해 알아봤다. 다음 포스트에선 Pinning에 대해 알아보도록 하겠다.

profile
Blue Team

0개의 댓글