Monitoring and Tuning the Linux Networking Stack: Receiving Data

IRQ

NIC는 DMA을 이용해서 패킷들을 메모리에 보내고, 메모리에 있는 패킷들을 발신한다. DMA가 완료된 후에 IRQ로 정보가 수발신이 완료되었다고 통보한다. 그러면 패킷을 수·발신 할 때 마다 매번 IRQ를 만들어서 CPU를 괴롭힐것이다. (IRQ가 발생하면 CPU는 강제로 Context-switching 되어서 interrup handler를 실행한다) 이것을 줄이기 위해 NAPI가 만들어졌다. NAPI는 Interrupt과 polling을 동시에 사용한다.

  1. 패킷이 수신되면 DMA를 통해 NIC에서 메모리로 데이터를 전송한다
  2. NIC에서 IRQ로 CPU(커널)에 데이터를 수신했다고 통보한다 → 이때 드라이버가 개입된다
  3. 드라이버가 IRQ를 끄고 NAPI를 진입한다. NAPI에서 polling으로 데이터를 직접 수집(처리)한다
  4. NAPI가 동작하는 동안 IRQ는 발생하지 않는다.
    1. 패킷이 매우많이 발생하는 서버라면 IRQ를 처리하는 동안에도 새 IRQ가 발생할 것이다. NAPI를 이용하면 처리하는 동안에는 IRQ가 발생하지 않게 한다
  5. NAPI에서 모든것을 처리하면 (더 이상 NIC에서 가져올게 없어지면) NAPI를 종료하고 IRQ를 다시 활성화한다

NAPI를 지원하지 않으면 매번 패킷을 커널로 보낼때 마다 Interrupt이 발생하니까 전체적인 처리량이 줄어든다.

Receive Side Scailing / Multi Queue

NIC에서는 DMA로 데이터를 메모리로 전송한다. 이때 DMA를 사용할 메모리 공간을 미리 확보 해야한다. 또한 Ring Buffer를 사용하기 때문에 Buffer 처리도 매번 해 줘야한다. 패킷량이 많으면 단일 Core에서 처리하기 버거울수 있다. 이 작업을 여러 코어에 나눠서 할 수 있으면 좋다. 이때 사용되는게 receive side scailing 또는 multi queue이다. 이때 까지는 메모리의 동시 접근 문제 때문에 어쩔수 없이 단일코어 밖에 처리하지 못했다. 동일한 메모리 공간에 여러 코어가 접근하면 Data-racing을 비롯해서 동기화 문제가 발생한다. Multi-Queue 또는 RSS는 NIC단에서 (수신단에서) 가상으로 큐(메모리 공간)를 나눈다. 메모리 공간이 다르면 다른 코어가 붙어도 된다. 수신된 패킷은 hash값이나 (사용자가 설정하면) 가중치 값에 따라서 지정된 큐에 들어간다.

<aside> 💡 드라이버단에서 여러 코어로 작업을 분산시키는 기법도 있다. Receive Packet Steering이라고 불리는 기법이다. 큐 자체는 동일함으로 NAPI에서 poll 을 부르는것 까지는 단일 코어에서 돌아간다. Poll 이후 패킷의 실제 처리는 여러 코어에 나눠서 돌아간다. RPS는 /sys/class/net/DEVICE_NAME(ens-3)/queues/QUEUE(rx-0, tx-0)/rps_cpus 에서 설정할 수 있다. 여기서 affinity 처럼 CPU를 설정하면 해당 코어로 패킷 처리를 분산시킨다. softirq로 옆의 CPU에 넘기는 방법이기 때문에 /proc/softirqs 에서 NET_RX가 올라감을 확인할 수 있으며, htop 등에서는 si(sitime)이 올라감을 볼 수 있다. RPS는 다른 코어로 데이터를 보내는 과정이 동반된다. 이때 각 코어에서 받을 수 있는 최대 패킷수 (큐 길이)가 존재한다. 이것을 넘어서면 뒤의 데이터는 다 짤린다. (처리된 패킷 수(input_pkt_queue)가 netdev_max_backlog 와 비교된다. 만약 netdev_max_backlog 보다 처리된 패킷수가 크다면 뒷부분은 짤린다.) 이 경우 netdev_max_backlog를 늘려야한다. → drop 됨은 /proc/net/softnet_stat에서 dropped 에서 관측할 수 있다.

</aside>

<aside> 💡 더 나아가서 Receive Flow Steering 기법도 있다. 위의 RPS와 비슷하지만 "해당 연결을 사용하는 코어에서 패킷을 처리"하도록 한다. 즉, pypy [worker.py](<http://worker.py>) 5001 의 접속들은 모두 해당 pypy가 돌고있는 코어에서 작동하게 된다. 같은 코어에서 바로바로 처리할 수 있기 때문에 추가적인 성능 향상을 꽤할수 있다. 하지만 어느 코어에서 처리해야할지를 저장해야 하기에 별도의 cost가 필요하다. CPU 내부 캐시가 충분히 크다면 효과를 노려봄직 하다. echo 32768 > proc/sys/net/core/rps_sock_flow_entries 또는 sysctl -w net.core.rps_sock_flow_entries=32768 로 테이블(엔트리)를 만들 수 있다. 각 큐마다 엔트리를 만든다면 /sys/class/net/eth0/queues/rs-0/rps_flow_cnt 에 값을 쓰면 된다. RFS가 동작하기 위해선 RPS가 우선 켜져있어야 한다

</aside>

<aside> 💡 하드웨어단에서 RFS를 지원하기도 한다. aRFS로 불리는 기법으로, ntuple filter가 되는 NIC에서 지원한다. 이것을 사용하면 NIC가 프레임을 받을때 최대한 비슷한 코어에 큐를 넘긴다. IRQ affinity를 프로그램이 도는 코어에 물려야 한다. 예를들어 4코어중 3, 4번 코어에서 프로그램이 돌고 있다면, 1번 Queue는 affinity → 3, 2번 Queue는 affinity → 4로 걸어야 한다.

</aside>

Queue Count

<aside> 💡 ethtool -l을 이용하면 큐 정보를 볼 수 있다.

</aside>

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4d43e3f8-8310-413a-8a66-3ac7597bce03/Untitled.png

Oracle에서 ethtool -l을 실행한 결과. 큐를 최대 2개 사용할 수 있고 (pre-set maxium) 현재 2개를 사용하는 중이다. (current). 이러면 패킷 처리에 2개의 코어를 사용할 수 있다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6d5481ee-54fd-4bae-81c9-e5c3ce05d463/Untitled.png

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/09ba450c-cb07-44e2-9cf6-4940c5e27c97/Untitled.png

Iwinv, OVH VPS (KVM 계열)에서 ethtool -l을 실행한 결과. 큐를 최대 1개밖에 못쓰고, 현재도 1개를 쓰고있다. 이러면 패킷이 아무리 많이 와도 단일 코어만을 사용할수 있다. 특히 RX, TX가 분리되어 있지않고 Combined 되어 있기 때문에 수신·발신 처리를 다 단일코어에서 해야한다.

virtio(숫자)-input.0하고 virtio(숫자)-output.0 만 체크하면 된다. 만약 멀티큐를 지원한다면 input.1, input.2 같이 큐 길이처럼 늘어날 것이다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b0bc5b90-e588-4519-a844-cb0fd0a82037/Untitled.png

큐 변경은 ethtool -L ens3 2 ethtool -L eth0 1 과 같이 할 수 있다.