我有两台计算机,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 连接)。
我最初使用 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
,并且不设置网关。我也没有运行nmcli
和iptables
命令。
这样做让我在运行套接字脚本时能够充分利用他们的直接 2.5gb/s 以太网连接。这似乎表明网桥导致了低吞吐量问题,至少在使用套接字发送图像时是如此。尽管即使在这种配置下,ROS 消息吞吐量仍然有限,但我愿意将其归咎于 ROS 无法很好地处理大消息。