多节点单进程Nodelet vs Composition


ROS通信是基于Node(节点)的,Node使用方便、易于扩展,可以满足ROS中大多数应用场景,但是也存在一些局限性,由于一个Node启动之后独占一根进程,不同Node之间数据交互其实是不同进程之间的数据交互,当传输类似于图片、点云的大容量数据时,会出现延时与阻塞的情况。

在ROS1中,nodelet软件包旨在提供在同一进程中运行多个算法(节点)的方式,不同算法之间通过传递指向数据的指针来代替了数据本身的传输(类似于编程传值与传址的区别),从而实现零成本的数据拷贝。在ROS2中,nodelet升级为更强大的Components。

nodelet和Components都是基于class_loader实现的。nodelet是对pluginlib的进一步封装,Components本质是有ROS2 接口的plugins。

Nodelet

nodelet能够实现以下功能:

  • 不同算法被封装进插件类,可以像单独的节点一样运行;
  • 在该功能包中提供插件类实现的基类nodelet:Nodelet,它将用于动态加载。所有的节点都将继承这个基类,并且可以使用pluginlib动态加载
  • 它将自动提供命名空间、重新映射arguments和parameters,就像它们是一个节点一样
  • 提供nodelet_manager进程,可以使用加载插件类的类加载器nodelet:NodeletLoader加载一个或多个节点。它们之间的任何通信都可以使用boost共享指针实现零拷贝roscpp发布调用,如果希望no-copy pub/sub工作,必须将消息发布为shared_ptr
ros::Publisher pub = nh.advertise<std_msgs::String>("topic_name", 5);
std_msgs::StringPtr str(new std_msgs::String);
str->data = "hello world";
pub.publish(str);

注意,当用这种方式发布,你和roscpp之间的隐性契约:你不可以修改你发送后的消息,因为指针会直接传递到用户的任何进程内。如果你想发送另一条消息,你必须分配一个新的。

bug清单

  • nodelet派生类的onInit()函数中使用std::shared_ptr而非boost::shared_ptr导致
    [FATAL] [1673255629.474497531]: Failed to load nodelet '/fastlio_plugin_loader` of type `fast_lio/fastlio_plugin` to manager `nodelet_manager'
    [nodelet_manager-1] process has died [pid 1170639, exit code -6, cmd /opt/ros/noetic/lib/nodelet/nodelet manager __name:=nodelet_manager __log:=/home/long/.ros/log/1496afea-8ffa-11ed-913a-07d0641c7442/nodelet_manager-1.log].
    
  • plugins.xml中没有加name属性导致
    [ERROR] [1673261540.209231901]: Failed to load nodelet [/fastlio_plugin_loader] of type [fast_lio/fastlio_plugin] even after refreshing the cache: According to the loaded plugin descriptions the class fast_lio/fastlio_plugin with base class type nodelet::Nodelet does not exist.
    

命令行工具

# Launch a nodelet manager node
nodelet manager               
# Launch a nodelet of type pkg/Type on m_name manager
nodelet load pkg/Type m_name 
# Launch a nodelet of type pkg/Type in a standalone node
nodelet standalone pkg/Type
# Unload a nodelet a nodelet by name from manager
nodelet unload name manager   

nodelet调用案例

rosrun nodelet nodelet manager __name:=mymanager

__name:= 用于设置管理器名称。

添加第一个节点

rosrun nodelet nodelet load nodelet_tutorial_math/Plus mymanager __name:=n1 _value:=100
  • nodelet 的节点名称是: /n1

  • 添加的参数名称是: /n1/value。

添加第二个节点

rosrun nodelet nodelet load nodelet_tutorial_math/Plus mymanager __name:=n2 _value:=-50 /n2/in:=/n1/out
  • /n2/in 重映射为 /n1/out

将独立节点改造为nodelet节点

源代码编写

原始节点程序改造

原始独立节点的main函数在main.cpp文件中,将其main函数更改名称(除main外的任意名称),比如mainFunction,同时还需新建头文件main.hmain.h中声明mainFunction函数

//main.h
#ifndef MAIN_H
#define MAIN_H
int mainFunction();
#endif 
//main.h
//main.cpp
#define NODELET //切换独立节点与Nodelet节点的宏定义
#ifdef NODELET
#include "main.h"
#endif

#endif

#ifdef NODELET
int mainFunction()
{
    int argc; char** argv;
    ros::init(argc, argv, "laserMapping");
#else
int main(int argc, char** argv)
{
    ros::init(argc, argv, "laserMapping");
#endif
//main.cpp
插件类编写

新建文件plugin.hplugin.cpp

//plugin.h
#ifndef PLUGIN_H
#define PLUGIN_H
#include <nodelet/nodelet.h> #继承Nodelet基类
namespace custom
{
	class Plugin: public nodelet::Nodelet
	{
	public:
	  Plugin();
	  virtual void onInit();
	};
}
#endif 
//plugin.h
//plugin.cpp
#include "plugin.h"
#include "main.h"
#include <boost/thread.hpp>
#include <pluginlib/class_list_macros.h>
namespace custom
{
	Plugin::Plugin(){
	}
	
	void Plugin::onInit(){
	  NODELET_INFO("plugin - %s", __FUNCTION__);
	  boost::shared_ptr<boost::thread> spinThread;
	  spinThread = boost::make_shared<boost::thread>(&mainFunction);
	}
}
PLUGINLIB_EXPORT_CLASS(custom::Plugin,nodelet::Nodelet);
//plugin.cpp

编译插件库

在CMakeLists.txt文件中添加库

# CMakeLists.txt
cmake_minimum_required(VERSION 2.8.3)
project(sample_nodelet)
 
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  nodelet
)
 
catkin_package(
 INCLUDE_DIRS include
 LIBRARIES ${PROJECT_NAME}
 CATKIN_DEPENDS nodelet roscpp rospy
 # DEPENDS system_lib
)
 
include_directories(
    include
    ${catkin_INCLUDE_DIRS}
)

add_library(main_function src/main.cpp)
target_link_libraries(main_funtion ${catkin_LIBRARIES})

add_library(custom_plugin
  src/plugin.cpp
)
target_link_libraries(custom_plugin main_function ${catkin_LIBRARIES})
if(catkin_EXPORTED_LIBRARIES)
  add_dependencies(custom_plugin ${catkin_EXPORTED_LIBRARIES})
endif()

插件描述文件

插件描述文件是存储插件所有重要信息的机器可读文件,包含插件所在库的名字,插件的名字,插件的类型及其继承的基类名。

ROS1中功能包生成的库保存在devel/lib目录下,ROS2中功能包生存的库保存在install/<package-name>/lib目录下。

创建plugins.xml文件

<class_libraries>
	<library path="lib/libcustom_plugin"><!--lib/lib插件类的库名-->
	  <!--class name="load时填写的nodelet插件名称" type="插件命名空间::插件类名" base_class_type="基类命名空间::基类名"-->
	  <class name="custom/Plugin" type="custom::Plugin" base_class_type="nodelet::Nodelet">
	    <description>This is a nodelet plugin.</description>
	  </class>
	</library>
</class_libraries>

class name必须与nodelet load <class name>相同,否则会报错。

在ROS包系统中注册插件

为了让pluginlib能够找到ROS包中的所有可用的插件,每个包必须显式指定其导出的插件和包含这些插件的库。

ROS1是通过package.xml导出的。

在package.xml中添加以下语句,其中polygon_base是基类的包名,plugin= xml描述文件

<export>
    <nodelet plugin="${prefix}/plugins.xml">
</export>

为了使上述导出命令正常工作,提供插件的包必须直接依赖于包含插件基类的包,还要在package.xml文件中加油

<build_depend>nodelet</build_depend>
<run_depend>nodelet</run_depend><depend>nodelet</depend>

launch文件运行

<launch>
  <node pkg="nodelet" type="nodelet" name="nodelet_manager"  args="manager" output="screen">
  	<param name="num_worker_threads" value="4"/>
  </node>
      
  <node pkg="nodelet" type="nodelet" name="SampleNodelet" args="load custom/Plugin nodelet_manager" output="screen">
  </node>
</launch>

load 后跟的名称必须与class name相同,否则会报错。

Components

ROS2 Wiki
Components可以实现以下功能:

  • 在不同的进程中运行多个节点。这样可以使不同的进程独立开。一个崩溃其他可以正常运行。也更方便调试各个节点。
  • 在同一个进程中运行多个节点。这样可以使得通信更加高效。

对于传统插件,基类可以是用户定义的任何东西。唯一的约束是派生类必须可以默认构造(因此,根据C++定律,基类也必须可以默认构造)。通常对于Components,插件类通常会继承rclcpp::Node类,但这不是必须的。实际上,将类作为Components导出的要求是:

  • 具有一个构造函数,该构造函数将单个参数是rclcpp::NodeOptions实例。
  • 具有方法:
    rclcpp::node_interfaces::NodeBaseInterface::SharedPtr get_node_base_interface(void)
    

满足要求的有rclcpp::Noderclcpp_lifecycle::LifecycleNode等。这意味着Components可以从满足这些要求的任何类中继承,但它也可以自身满足要求并完全跳过继承。

传统上,插件在注册到class_loader factory scheme时必须声明他们的基类。这允许工厂将派生对象实例化为基类的智能指针

#include <pluginlib/class_list_macros.hpp>
// Registering a plugin
PLUGINLIB_EXPORT_CLASS(BaseClass, DerivedClass)
//
...
// Instantiating a plugin
// We have access to all of BaseClass API
std::shared_ptr<BaseClass> base_ptr = plugin_loader.createSharedInstance("DerivedClass");

Components基类注册依赖于void(智能)指针的类型擦除,这样就可以将一个简单的包装器类作为所有组件的公共基类。这种设计意味着开发人员不需要自己为新实例查询工厂。

#include <rclcpp_components/register_node_macro.hpp>
// Registering a component
RCLCPP_COMPONENTS_REGISTER_NODE(DerivedNode)
//
...
// NodeInstanceWrapper is a wrapper around both a
// std::shared_ptr<void> and a
// rclcpp::node_interfaces::NodeBaseInterface::SharedPtr
// Not much to do with those!
rclcpp_components::NodeInstanceWrapper component_wrapper = component_loader->create_node_instance(options);

在大多数情况下,Component继承自rclcpp::Node,因为它是满足上述需求的最简单方法。因此,我们假设Component是一个ROS 2节点,包含它所涉及的一切,可能包括parameters, listeners/publishers, services, action等等。

CMakeLists.txt

命令行工具

查看已经注册过的components

ros2 component types

启动component container,重映射容器名和命名空间

ros2 run rclcpp_components component_container --ros_args --remap __node:=MyContainer --remap __ns:=/ns

component container的名字将会是/ns/MyContainer
容器的命名空间不会影响加载的components

加载component到容器

# ros2 component load [component_container_name] [package_name] [class_name]
ros2 component load /ns/MyContainer composition composition::Listener

return the unique ID of the loaded component as well as the node name:

终端输出

Loaded component 1 into '/ns/MyContainer' container node as '/listener'`

重映射component的节点名和命名空间

ros2 component load /ns/MyContainer composition composition::Talker --node-name talker3 --node-namespace /ns1

终端输出

Loaded component 2 into '/ns/MyContainer' container node as '/ns1/talker3'`

使用component id卸载组件

ros2 component unload /ns/MyContainer 1 2

终端输出

Unloaded component 1 from '/ns/MyContainer' container
Unloaded component 2 from '/ns/MyContainer' container

编译插件库

与ROS1 nodelet相似,将节点编译成共享库,与ROS1在package.xml中导出插件不同,ROS2在CMakeLists.txt中使用rclcpp_components_register_nodes注册组件节点。

find_package(rclcpp_components REQUIRED)
add_library([shared_lib_name] SHARED [cppFile...])
rclcpp_components_register_nodes([shared_lib_name] "component_class_name")

launch文件运行

import launch
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode


def generate_launch_description():
    """Generate launch description with multiple components."""
    container = ComposableNodeContainer(
            name='my_container',
            namespace='',
            package='rclcpp_components',
            executable='component_container',
            composable_node_descriptions=[
                ComposableNode(
                    package='composition',
                    plugin='composition::Talker',
                    name='talker'),
                ComposableNode(
                    package='composition',
                    plugin='composition::Listener',
                    name='listener')
            ],
            output='screen',
    )

    return launch.LaunchDescription([container])

在C++代码中运行时加载组件

#include "class_loader/class_loader.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_components/node_factory.hpp"
int main(int argc, char * argv[])
{
  if (argc < 2) {
    fprintf(stderr, "Requires at least one argument to be passed with the library to load\n");
    return 1;
  }
  rclcpp::init(argc, argv);
  rclcpp::Logger logger = rclcpp::get_logger("composition");
  rclcpp::executors::SingleThreadedExecutor exec;
  rclcpp::NodeOptions options;
  std::vector<std::unique_ptr<class_loader::ClassLoader>> loaders;
  std::vector<rclcpp_components::NodeInstanceWrapper> node_wrappers;

  std::vector<std::string> libraries;
  for (int i = 1; i < argc; ++i) {
    libraries.push_back(argv[i]);
  }
  for (auto library : libraries) {
    RCLCPP_INFO(logger, "Load library %s", library.c_str());
    std::unique_ptr<class_loader::ClassLoader> loader = std::make_unique<class_loader::ClassLoader>(library);
    auto classes = loader->getAvailableClasses<rclcpp_components::NodeFactory>();
    for (auto clazz : classes) {
      RCLCPP_INFO(logger, "Instantiate class %s", clazz.c_str());
      rclcpp_components::NodeFactory::SharedPtr node_factory = loader->createInstance<rclcpp_components::NodeFactory>(clazz);
      rclcpp_components::NodeInstanceWrapper wrapper = node_factory->create_node_instance(options);
      node_wrappers.push_back(wrapper);
      // 获取节点基类指针
      rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node = wrapper.get_node_base_interface();
      // 将component节点加到执行器中
      exec.add_node(node);
    }
    loaders.push_back(std::move(loader));
  }

  exec.spin();
  // 卸载components节点
  for (auto wrapper : node_wrappers) {
    exec.remove_node(wrapper.get_node_base_interface());
  }
  node_wrappers.clear();

  rclcpp::shutdown();

  return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
MinIO是一个开源的对象存储服务器,它允许您在分布式环境中构建高性能的云存储。MinIO支持节点多磁盘配置,这意味着您可以将多个磁盘驱动器连接到个MinIO节点,以增加存储容量和性能。 在MinIO的节点多磁盘配置中,您可以将多个磁盘驱动器挂载到MinIO节点的不同目录下。MinIO会自动将数据分布在这些磁盘上,以实现数据的冗余和负载均衡。这种配置方式可以提高存储容量和读写性能,并且可以通过添加更多的磁盘来扩展存储能力。 要配置MinIO的节点多磁盘,您需要按照以下步骤进行操作: 1. 挂载磁盘驱动器:将每个磁盘驱动器挂载到MinIO节点的不同目录下。您可以使用操作系统提供的工具来完成这一步骤。 2. 启动MinIO服务:使用MinIO提供的命令行工具或API启动MinIO服务,并指定每个挂载点的路径作为参数。 3. 配置存储分布:在MinIO的配置文件中,您可以指定数据的分布策略。例如,您可以选择将数据均匀地分布在所有磁盘上,或者将数据复制到多个磁盘以实现冗余。 4. 测试和监控:一旦配置完成,您可以使用MinIO提供的工具来测试和监控存储性能。您可以使用MinIO的命令行工具或API来上传、下载和删除对象,并查看性能指标和日志信息。 通过使用MinIO的节点多磁盘配置,您可以轻松地扩展存储容量和性能,以满足不断增长的数据需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shilong Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值