はじめに
ソリューションプロフェッショナルグループのみよしです。
ROSソリューションの担当をしています。
今回はROSでrosbridge_serverの処理速度改善を試した結果についてご紹介します。
- はじめに
- intdash ROS Bridge
- ROS Bridge Server
- ROS(Melodic)でrosbridge_tcpの速度改善
- ROS(Noetic)でrosbridge_tcpを試す
- rosbridge_websocketを試す
- まとめ
intdash ROS Bridge
弊社が提供する DX Functions の遠隔制御のユースケースとして、ROSを利用したロボット開発が挙げられます。
これらのユースケースに対して、弊社ではROSメッセージの遠隔リアルタイムデータ伝送を行うintdash Bridge / intdash ROS2Bridgeというプロダクトを提供しています。
intdash Bridge / intdash ROS2Bridgeを使うことで、遠隔地のROS1 / ROS2空間をつなぎ、ROSのメッセージをやり取りすることによる遠隔制御やモニタリングなどのユースケースが実現できます。
弊社の過去Blogでも、intdash Bridge / intdash ROS2Bridge、rosbridge_serverに関してのご紹介をしてきました。
tech.aptpod.co.jp tech.aptpod.co.jp tech.aptpod.co.jp
intdash Bridge / intdash ROS2Bridgeは、intdash Edge Agentを介してintdash Serverに接続することが可能です。
(下記は一例です)
ROS Bridge Server
これらの弊社製品とは別にrosbridge_suiteのrosbridge_serverを使用する場合もあります。
rosbridge_serverは、rosbridge protocolを使用するクライアントであれば、接続することが可能です。
rosbridge protocolを使用するクライアントからrosbridge_serverへ接続するので、LANやVPN等でつながっている、もしくはrosbridge_serverが動く環境でグローバルIPを有している場合に使用可能です。
(下記は一例です)
rosbridge_serverの通信方式として、ROS1では、TCP・UDP・WebSocketが提供されています。
- rosbridge_tcp
- rosbridge_udp
- rosbridge_websocket
また、ROS2では、現在、WebSocketのみ提供されています。 *1
- rosbridge_websocket
高頻度データに対する問題点
過去のROS環境向けの開発でも、rosbridge_serverを度々使用していますが、高頻度のデータでは処理速度が遅くなり、その影響がrosbridge_serverの後段にあるノードやクライアント側にも生じるという問題がありました。
例えば、下記計測で行なっている環境(条件)では、テスト用クライアントでs.send()の後にtime.sleep()をして、送信する間隔を調整した場合、time.sleep()が0.0001secよりsleep時間が短くなると、rosbridge_serverを起動している側で行うrostopic echoの表示が明らかに遅れ、クライアント側でも送信待ち状態が生じます。
(rosbridge_serverで受信するメッセージ(サイズや要素数)によって、送信する間隔がどれぐらいであれば影響を受けないかは異なります。)
ROS(Melodic)でrosbridge_tcpの速度改善
rosbridge_tcp
最初に結論を述べると、Melodic(Python2.7)ではProtocolクラスのincoming()内で、文字列の連結を + から join() とすることで処理速度が改善できました。
Pythonの文字列連結については、こちらの “String Concatenation”に記載されています。
RosBridgeTcpSocketクラスのhandle()内で、recv()したデータの処理が終わるまで次recv()が行なわれないので、一度にrecv()するデータのサイズが大きくなると、recv()後に行なう処理で時間が掛かる為、遅くなります。
構成として
- SocketServerのTCPServerを使用
- TCPServerでrequest_queue_size=5と設定
- RosbridgeTcpSocketでincoming_buffer=65536byte(64k)と設定
となっているので、MAX 64k x 5 = 320k 分のデータが溜まる可能性があります。
incoming_bufferは起動時にパラメーターとして変更可能です。
改善策
rosbridge_library/src/rosbridge_library/protocol.py で文字列の連結をしている箇所を変更します。(下記diff参照)
rosbridge_tcpが動く環境のスペックによってはincoming_bufferの値を小さくします。
diff --git a/rosbridge_library/src/rosbridge_library/protocol.py b/rosbridge_library/src/rosbridge_library/protocol.py index c9424ad..3ca2c47 100644 --- a/rosbridge_library/src/rosbridge_library/protocol.py +++ b/rosbridge_library/src/rosbridge_library/protocol.py @@ -124,10 +124,11 @@ class Protocol: message_string -- the wire-level message sent by the client """ - if self.bson_only_mode: - self.buffer.extend(message_string) - else: - self.buffer = self.buffer + str(message_string) + if message_string != "": + if self.bson_only_mode: + self.buffer.extend(message_string) + else: + self.buffer = ''.join([self.buffer, str(message_string)]) msg = None # take care of having multiple JSON-objects in receiving buffer
計測と結果
実際に、改善策の効果を計測します。
計測の方法として
- DockerのContainerで動かすrosbridge_serverに対して、Hostからテスト用のクライアントでメッセージ 100,000 件を送信する。
- rosbridge_serverで受信したメッセージはpublishされるので、rostopic echoで表示する。
ということを行ないます。
環境
DockerのContainerを使用(下記イメージを使用) *2
$ docker pull ros:melodic-robot
計測する時間
- メッセージ送信開始(クライアント起動)から、メッセージ送信終了(クライアント終了)まで。
- メッセージ送信開始(クライアント起動)から、rosbridge_serverを起動している側(DockerのContainer)でrostopic echoを行い、送信したメッセージの表示が終了するまで。
テスト用のクライアント (test_client_tcp.py)
#!/usr/bin/env python3 import sys import socket import time import json def advertise_msg(topic, type): return json.dumps( dict(op='advertise', topic=topic, type=type)).encode('utf-8') def publisher_msg(topic, data): return json.dumps(dict(op='publish', topic=topic, msg=data)).encode('utf-8') def publish(host=None, port=None, topic=None): if not host or not port or not topic: print('invalid params host={0} port={1} topic=({2}, {3})'.format( host, port, topic['name'], topic['type'])) try: s = socket.socket() s.connect((host, port)) s.send(advertise_msg(topic=topic['name'], type=topic['type'])) time.sleep(0.5) cnt_max = 100000 for cnt in range(cnt_max): data = dict(data='hello, {0}, {1}'.format(cnt, time.time())) s.send(publisher_msg(topic['name'], data)) print(data) except KeyboardInterrupt: pass except Exception as e: print(e) finally: s.close() if __name__ == '__main__': host = '127.0.0.1' port = 9090 topic = { 'name': '/chatter', 'type': 'std_msgs/String' } if len(sys.argv) > 1: host = sys.argv[1] publish(host, port, topic)
手順
DockerのContainerでrosbridge_serverを起動する。
incoming_bufferの設定なし $ roslaunch rosbridge_server rosbridge_tcp.launch incoming_buffer=1024 $ roslaunch rosbridge_server rosbridge_tcp.launch incoming_buffer:=1024
DockerのContainerでrostopic echoを行なう。
$ rostopic echo /chatter
Hostでテスト用のクライアントを起動する。
$ python3 test_client_tcp.py 172.17.0.2
結果 - incoming_bufferの設定なし
文字列の連結 | メッセージ送信開始~メッセージ送信終了 | メッセージ送信開始~rostopic echoの表示終了 |
---|---|---|
変更なし | 01h 12m 14.94s | 02h 45m 22.59s |
変更あり | 00h 01m 30.16s | 00h 04m 15.50s |
結果 - incoming_buffer=1024
文字列の連結 | メッセージ送信開始~メッセージ送信終了 | メッセージ送信開始~rostopic echoの表示終了 |
---|---|---|
変更なし | 00h 03m 26.07s | 00h 03m 37.23s |
変更あり | 00h 00m 13.99s | 00h 00m 16.06s |
文字列の連結をしている箇所の変更で大きく変わりました。
計測した環境がDockerのContainerで非力な為、incoming_bufferを小さくすることで更に効果が得らました。
ROS(Noetic)でrosbridge_tcpを試す
先のMelodicで行なった変更を試してみました。
テスト用のクライアント、手順はMelodicと同様です。
環境
DockerのContainerを使用(下記イメージを使用)
$ docker pull ros:noetic-robot
結果 - incoming_bufferの設定なし
文字列の連結 | メッセージ送信開始~メッセージ送信終了 | メッセージ送信開始~rostopic echoの表示終了 |
---|---|---|
変更なし | 00h 01m 59.37s | 00h 04m 15.79s |
変更あり | 00h 01m 28.01s | 00h 04m 09.26s |
結果 - incoming_buffer=1024
文字列の連結 | メッセージ送信開始~メッセージ送信終了 | メッセージ送信開始~rostopic echoの表示終了 |
---|---|---|
変更なし | 00h 00m 10.53s | 00h 00m 11.87s |
変更あり | 00h 00m 10.45s | 00h 00m 11.90s |
Melodicでの結果とは異なり、文字列の連結をしている箇所の変更をしても大した違いは見られませんでした。
rosbridge_suiteはPythonで作られているので、PythonのVersionの違いが影響していると思われます。(MelodicはPython2.7、NoeticはPython3)
incoming_bufferを小さくすることについては、効果が得られました。
rosbridge_websocketを試す
構成として
- autobahnのWebSocketServerFactoryを使用
- TornadoのWebSocketHandlerを使用
となっています。
環境
DockerのContainerを使用
テスト用のクライアント (test_client_ws.py)
#!/usr/bin/env python3 import sys import json import time import asyncio from autobahn.asyncio.websocket import (WebSocketClientProtocol, WebSocketClientFactory) class TestClientProtocol(WebSocketClientProtocol): def onOpen(self): self._sendDict({ 'op': 'advertise', 'topic': '/chatter', 'type': 'std_msgs/String', }) time.sleep(0.5) self._sendDictLoop() def _sendDict(self, msg_dict): msg = json.dumps(msg_dict).encode('utf-8') self.sendMessage(msg) cnt = 0 cnt_max = 100000 def _sendDictLoop(self): data = 'hello, {0} {1}'.format(self.cnt, time.time()) msg_dict = { 'op': 'publish', 'topic': '/chatter', 'msg': { 'data': data } } msg = json.dumps(msg_dict).encode('utf-8') self.sendMessage(msg) print(data) self.cnt += 1 if self.cnt >= self.cnt_max: return self.factory.loop.call_later(0, self._sendDictLoop) def onMessage(self, payload, binary): self.__class__.received.append(payload) if __name__ == '__main__': host = '127.0.0.1' port = 9090 if len(sys.argv) > 1: host = sys.argv[1] url = 'ws://{0}:{1}'.format(host, port) factory = WebSocketClientFactory(url) factory.protocol = TestClientProtocol loop = asyncio.get_event_loop() coro = loop.create_connection(factory, host, port) loop.run_until_complete(coro) loop.run_forever() loop.close()
手順
DockerのContainerでrosbridge_serverを起動する。
$ roslaunch rosbridge_server rosbridge_websocket.launch
DockerのContainerでrostopic echoを行なう。
$ rostopic echo /chatter
Hostでテスト用のクライアントを起動する。
$ python3 test_client_tcp.py 172.17.0.2
結果 - Melodic
文字列の連結 | メッセージ送信開始~メッセージ送信終了 | メッセージ送信開始~rostopic echoの表示終了 | |
---|---|---|---|
WebSocket | 変更なし | 00h 00m 05.18s | 00h 00m 16.20s |
WebSocket | 変更あり | 00h 00m 04.99s | 00h 00m 15.50s |
TCP | 変更あり(incoming_buffer=1024) | 00h 00m 13.99s | 00h 00m 16.06s |
結果 - Noetic
文字列の連結 | メッセージ送信開始~メッセージ送信終了 | メッセージ送信開始~rostopic echoの表示終了 | |
---|---|---|---|
WebSocket | 変更なし | 00h 00m 04.63s | 00h 00m 06.17s |
WebSocket | 変更あり | 00h 00m 04.49s | 00h 00m 06.05s |
TCP | 変更あり(incoming_buffer=1024) | 00h 00m 10.45s | 00h 00m 11.90s |
WebSocketが、文字列の連結をしている箇所の変更あり・なしに関わらず速いのは、常に1メッセージずつ処理を行なっているからだと思います。
まとめ
本記事では、Melodicでのrosbridge_serverの処理速度改善と、NoeticやWebSocketとの比較を行いました。
rosbridge_tcpを使用する場合、Melodicでは文字列の連結をしている箇所を変更することで大きく処理速度を改善できました。
同様の変更をNoeticで行っても、大きな効果は得られませんでしたが、それはPythonのVersionの違いによるものだと思われます。
rosbridge_tcpが動く環境のスペックによって、incoming_bufferを小さくすることはMelodic・Noeticともに効果はありました。(潤沢なスペックの場合、大きな効果は得られない可能性があります。)
文字列の連結をしている箇所の変更は、rosbridge_tcp・rosbridge_websocketともに使用している箇所ですが、変更なしのMelodicでもrosbridge_websocketが速いのは、常に1メッセージずつ処理を行なうからだと思います。
可能であるなら、rosbridge_websocketを使用するのが良いと思います。
弊社ではROSコミュニティへの貢献もしていきたいと思っております。
上記も含めて弊社製品、またはROSに関連した開発に興味を持って頂けた方は是非、こちらの弊社採用ページもご覧ください。
製品に関するお問い合わせはこちらへ!
最後までご覧いただきありがとうございました。