使用以太网到 WiFi 桥接器时,是什么原因导致大型 TCP 消息在一个方向上的吞吐量较低?

使用以太网到 WiFi 桥接器时,是什么原因导致大型 TCP 消息在一个方向上的吞吐量较低?

我有两台计算机,Orin 和 NUC,它们通过以太网电缆直接连接。NUC 通过 WiFi 连接到带互联网的路由器(为了完整性,我希望与问题无关),并通过源文件中的以下几行向 Orin 提供网络访问:

sudo nmcli c up id orin ifname enp88s0
sudo iptables -t nat -A POSTROUTING -o enp88s0 -j MASQUERADE

Orin 的网络是通过 GUI 配置的,地址为 192.168.3.2、网络掩码为 255.255.255.0,网关为 192.168.3.1(NUC 的 IP)。此配置至少使 Orin 可以访问互联网(并使其能够通过无线网络上的其他计算机进行 SSH 连接)。

Orin 通过以太网连接到 NUC 的网络图

我最初使用 ROS 从 Orin 向 NUC 发送 3MB 图像,结果丢失了其中的大部分;如果我以 30Hz 的频率发送,那么 NUC 上接收图像的频率为 20Hz。如果我以 15Hz 的频率发送,那么 NUC 上接收图像的频率为 10Hz。如果我尝试使用无线网络上的另一台计算机接收图像,接收速率将下降到 5Hz 甚至 1Hz。此外,接收图像的速率是随机的;例如,它不会持续丢失每一条消息。如果我在 Orin 上发送和接收图像,那么接收速度将达到完整的 30Hz,即大约 100MB/s。

我先用 NUC 运行 iperf,然后用 Orin 作为服务器,在这两种情况下,它都会提供 2.36gb/s 的带宽(两者都有 2.5gb 以太网)。这意味着 ROS 的 30Hz 全速发送速率为 100MB/s,肯定不会使连接饱和。

为了排除 ROS 的罪魁祸首,我编写了两个脚本,使用 Python 中的套接字通过 TCP 尽可能快地反复发送图像;我已将它们附加在本文底部。运行套接字脚本并将 4MB 图像从 Orin 发送到 NUC 可获得 56MB/s。如果我发送 80KB 图像,我得到 300MB/s,因此完全连接饱和。如果我以相反的方向(从 NUC 到 Orin)发送 4MB 图像,我也能获得 300MB/s。

因此,最终的问题陈述似乎是:从 Orin 向 NUC 发送大型 TCP/TCPROS 消息会导致吞吐量明显低于其连接可以处理的水平。

是以太网到 WiFi 桥接配置的问题吗?是我/ROS 发送 TCP 数据包的方式有问题吗?

编辑:我已重命名此帖子以反映消息没有丢失的事实,因为发送和接收的消息数量是相同的。

编辑2:我将 Orin 和 NUC 上的 MTU 大小从 1500(实际上是 1466 ifconfig)减小到 966,然后减小到 466,将 Orin 到 NUC 的 4MB 图像的吞吐量分别提高到 83MB/s 和 100MB/s。将 MTU 大小减小到 216 后,吞吐量为 65MB/s,收益递减,并且不会使硬件饱和。不过,我不确定这是暗示还是无关紧要。

sudo ifconfig eth0 down
sudo ifconfig eth0 mtu 1500 # ends up being 1466
sudo ifconfig eth0 up

编辑3:我通过 rync 在 Orin 和 NUC 之间发送了一个 500MB 的 .zip 文件;从 Orin 到 NUC 的速度为 18MB/s,从 NUC 到 Orin 的速度为 40MB/s。但不确定这是否提供了更多信息。

rsync -azvhPr test.zip [email protected]:/home/orin

编辑4nmcli:我通过不运行上述和命令来移除 Orin 和 NUC 之间的以太网到 wifi 桥接器iptables,然后将两个设备配置为将以太网视为自己的网络,方法是将这些接口上的地址设置为10.0.0.x,网络掩码仍为255.255.255.0,无网关。现在,使用 4MB 图像运行套接字脚本可使 Orin 到 NUC 的速度达到 300MB/s,从而有效地饱和了它们的连接。我认为通过 NUC 为 Orin 提供互联网的网络桥接器导致了低吞吐量问题,尽管我不知道为什么会这样。

最后再来个小惊喜:最初的用例是通过 ROS 发送图像。我们后来从 RGB 图像切换到单色图像,这样总消息大小就从 4MB 减少到了 0.9MB,这样就可以在原始的、不知为何损坏的网络配置下以 15Hz 或 30Hz 的频率读取完整的消息。

代码

注意:主机地址是使用子网地址给出的。因此,对于 NUC,我使用的是 192.168.3.1(其子网与 Orin 的 IP 地址),而不是 192.168.1.243(路由器给出的 IP)。对于编辑 4,IP 为 10.0.0.1。

发送者.py

import socket
import struct
import pickle
import logging
import time
from wrapper import Wrapper

logging.getLogger().setLevel(logging.DEBUG)

# ip address and port of this device
# if sender and receiver both on this device, localhost is fine
HOST = '192.168.3.1'
PORT = 50007

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
print('Connected by ' + str(addr))

messages_received = 0
total_message_bytes = 0
end_of_messages = False
first_message = True

try:
    while not end_of_messages:
        dsp = conn.recv(4)
        if first_message:
            start_time = time.time()
            first_message = False
        if len(dsp) != 4:
            logging.info('End of messages')
            end_of_messages = True
            continue
        data_size = struct.unpack('>I', dsp)[0]
        data_id = struct.unpack('>I', conn.recv(4))[0]
        recv_payload = b''
        recv_remaining = data_size

        while recv_remaining != 0:
            recv_payload += conn.recv(recv_remaining)
            recv_remaining = data_size - len(recv_payload)
        wrapper = pickle.loads(recv_payload)

        messages_received += 1
        total_message_bytes += data_size

        #print(wrapper.rostopic)
        #print(wrapper.timestamp)
        #cv2.imshow("Display window", wrapper.msg)
except KeyboardInterrupt:
    logging.info('Closing program')

end_time = time.time()
total_time = end_time - start_time
mb = total_message_bytes / 1000000

logging.info(f'Total time: {total_time}')
logging.info(f'Total messages: {messages_received}')
logging.info(f'Messages per second: {messages_received / total_time}')
logging.info(f'MB received: {mb}')
logging.info(f'MB per second: {mb / total_time}')
logging.info(f'MB per message: {mb / messages_received}')

conn.close()

接收方.py

import socket
import struct
import pickle
import time
import logging
from wrapper import Wrapper

logging.getLogger().setLevel(logging.DEBUG)

with open('image.jpg', 'r+b') as image:
    img = image.read()
logging.info(f'Image, when opened as pure bytes, is {len(pickle.dumps(img))/1000000}MB')

wrapper = Wrapper(rostopic='sensor_msgs/Image', msg=img, timestamp=1999999)

# should be IP and port of server sending messages to
# if sender and receiver both on this device, localhost is fine
HOST = '192.168.3.1'
PORT = 50007
CONNECT_RETRY = 5

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

start = time.time()
connected = False
while connected == False:
    try:
        s.connect((HOST, PORT))
    except ConnectionRefusedError:
        logging.info(f'Could not connect to server, will try again in {CONNECT_RETRY} seconds')
        time.sleep(CONNECT_RETRY)
    else:
        connected = True
        logging.info("Connected to server")

messages_sent = 0

try:
    while True:
        # Pickle the object and send it to the server
        data_pickled = pickle.dumps(wrapper)
        s.sendall(struct.pack('>I', len(data_pickled))) # I = unsigned long, > = big endian
        s.sendall(struct.pack('>I', 1)) # maybe use long unsigned int, just in case
        s.sendall(data_pickled)
        messages_sent += 1
except KeyboardInterrupt:
    logging.info('Closing program')
    s.shutdown(socket.SHUT_RDWR)
except ConnectionError:
    logging.info('Connection closed/reset/aborted closing program')

s.close()
logging.debug(f'Messages send: {messages_sent}')

包装器.py

class Wrapper:
    """
    Wraps a ROS message, and additionally specifies the rostopic
    and timestamp.
    """
    def __init__(self, rostopic, msg, timestamp):
        self.rostopic = rostopic
        self.msg = msg
        self.timestamp = timestamp

答案1

我将网络配置从以太网到 WiFi 桥接器切换为简单地将以太网视为自己的网络。从askubuntu 回答:对于 Orin 和 NUC 的以太网接口,我将其地址设置为10.0.0.x,网络掩码设置为255.255.255.0,并且不设置网关。我也没有运行nmcliiptables命令。

这样做让我在运行套接字脚本时能够充分利用他们的直接 2.5gb/s 以太网连接。这似乎表明网桥导致了低吞吐量问题,至少在使用套接字发送图像时是如此。尽管即使在这种配置下,ROS 消息吞吐量仍然有限,但我愿意将其归咎于 ROS 无法很好地处理大消息。

相关内容