ceph网络通信分析--基于源码simple_server与simple_client

一、ceph网络通信框架

1.1 概述

分布式存储系统需要稳定的底层网络通信模块,用于集群内各节点间的通信以及与外界用户的通信。ceph的网络通信模块源码位于/src/msg之下,三个子目录中分别对应了三种不同的网络通信方式,包括simple,Async、XIO。
simple是稳定的方式,目前广泛应用于生产环境,simple方式为每一个链接都要创建接收线程与发送线程,但是随着集群规模的增大,这种方式会消耗大量的CPU资源,已经逐渐开始下滑。Async方式自从K版之后已经稳定,成为了默认的通信方式,这是一种异步多路IO复用的方式。XIO的方式基于第三方网络通信库accelio,目前仍处于实验阶段。本文主要以/src/test/中的simple_server和simple_client两个测试demo分析simple方式的实现与使用。

1.2 设计模式

订阅发布模式又名观察者模式,它意图是“定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新”。以simple方式为例,simple通信需要messenger作为发布者,dispatcher及其子类作为订阅者。所以在创建messenger的时候,都会在其构造函数中调用ms_deliver_handle_fast_connect()来告知dispatcher有新的connection建立,无论是新建或是重连。

void SimpleMessenger::init_local_connection()
{
  local_connection->peer_addrs = *my_addrs;
  local_connection->peer_type = my_name.type();
  local_connection->set_features(CEPH_FEATURES_ALL);
  ms_deliver_handle_fast_connect(local_connection.get());
}

1.3 通信的基础类

1.4 simple的通信机制

simple对于每一个连接,都会创建两个线程,其中一个用于监听和读取该终端的读事件,另一个用于写事件。读线程得到请求以后会解析网络流并开始构建消息,然后派发到后面的 Dispatcher。写线程在大部分时候会处于 Sleep 状态,直到有新的消息需要发送才会被唤醒。
Messenger是网络模块的核心数据结构,负责接收/发送消息。OSD主要有两个Messenger:ms_public处于与客户端的消息,ms_cluster处理与其它OSD的消息。下图是simple通信机制的框架,包括一个OSD server和一个client 

simple通信机制
对其中的各模块作简介

SimpleMessager

该类实现了Messager的接口。这只是三种实现接口方式中的一种。Messager是整个网络模块的抽象类,可以看成是一个顶层的控制模块,它定义了网络模块的基本API接口,实现发送和接收消息。

Connection

用来连接,发送和接收消息

Pipe

对于每一个连接,都会在Pipe内部创建一个读线程、一个写线程用来处理接收消息和发送消息。它的层次位于Connetion和Dispatcher的中间,其中拥有读写线程pipe:: reader_thread和pipe::writer_thread,他们的入口函数分别为Pipe::reader和Pipe::writer函数。

Dispatcher

是所有订阅者的基类,这个类用于消息的分发,在Pipe中接收、发送队列中有很多请求,他就是负责把Message的请求分发给具体的应用层。

Accecpter

该类用于listen,封装了系统的socket中的多种方法,是建立连接的主要实现模块。

二、simple_server

在server端,需要一个messenger来完成数据的收发,dispatcher来将数据请求分发到具体处理的类,需要accepter来进行监听。server端通信框图为
在这里插入图片描述

  • Accepter负责listen, 调用 SimpleMessenger::add_accept_pipe() 创建新的 Pipe 到 SimpleMessenger::pipes 来处理该请求。
  • Pipe用于消息的读取和发送。该类主要有两个组件,分别在不同线程中,Pipe::Reader,Pipe::Writer用来处理消息读取和发送。
  • Messenger作为消息的发布者, 各个 Dispatcher 子类作为消息的订阅者, Messenger 收到消息之后,通过 Pipe 读取消息,然后转给 Dispatcher 处理。
  • Dispatcher是订阅者的基类,具体的订阅后端(比如OSD、monitor和MDS等)继承该类,初始化的时候通过 Messenger::add_dispatcher_tail/head 注册到 messenger::dispatchers. 收到消息后,通知该类处理。
  • DispatchQueue该类用来缓存收到的消息, 然后唤醒 DispatchQueue::dispatch_thread 线程找到后端的 Dispatch 处理消息。

2.1 server端代码

  1. 将地址字符串解析为entity_addr_t类的地址
	string dest_str = "tcp://";
	dest_str += addr;
	dest_str += ":";
	dest_str += port;
	entity_addr_from_url(&bind_addr, dest_str.c_str()); //将字符串地址解析为实体地址
  1. 创建simplemessenger,并进行magic和policy设置
	messenger = Messenger::create(g_ceph_context, g_conf().get_val<std::string>("ms_type"), //type决定是simple方式还是async方式
				      entity_name_t::MON(-1),
				      "simple_server",
				      0 /* nonce */,
				      0 /* flags */);
	// enable timing prints
	messenger->set_magic(MSG_MAGIC_TRACE_CTR);
	messenger->set_default_policy(Messenger::Policy::stateless_server(0));

create()函数根据传入的type确定具体返回的messenger类型,messenger类是父类,在simple中是返回SimpleMessenger,其构造函数中调用了

	init_local_connection();  //初始化connection,并且一旦有新连接,需告知所有的dispatcher
void SimpleMessenger::init_local_connection()
{
  local_connection->peer_addrs = *my_addrs;
  local_connection->peer_type = my_name.type();
  local_connection->set_features(CEPH_FEATURES_ALL);
  ms_deliver_handle_fast_connect(local_connection.get());//告知所有的dispatcher
}
  1. 为messenger来bind IP
	r = messenger->bind(bind_addr);

messenger.h中定义了bind的接口,具体实现在SimpleMessenger中

 int SimpleMessenger::bind(const entity_addr_t &bind_addr)
{
  lock.Lock();
  if (started) {
    ldout(cct,10) << "rank.bind already started" << dendl;
    lock.Unlock();
    return -1;
  }
  ldout(cct,10) << "rank.bind " << bind_addr << dendl;
  lock.Unlock();

  // bind to a socket
  set<int> avoid_ports;
  int r = accepter.bind(bind_addr, avoid_ports);	//实际是将地址和端口bind到了accepter上
  if (r >= 0)
    did_bind = true;
  return r;
}

分析可知,最终完成bind的实际是Accepter,该类继承自thread类,是用于在指定的地址和端口上监听即将出现的connection。accepter是封装的Linux的socket中的 方法,包括创建socket,bind,listen,创建pipe,是建立连接实际完成的模块
参考tcp通信流程

在这里插入图片描述

int Accepter::bind(const entity_addr_t &bind_addr, const set<int>& avoid_ports)
{
	...
	listen_sd = socket_cloexec(family, SOCK_STREAM, 0); //创建socket,封装了Linux的socket创建方法
	...
	...
    // 设置地址
  entity_addr_t listen_addr = bind_addr;
  if (listen_addr.get_type() == entity_addr_t::TYPE_NONE) {
    listen_addr.set_type(entity_addr_t::TYPE_LEGACY);
  }
  listen_addr.set_family(family);
  ...
  ...
  //指定端口
  if (listen_addr.get_port())
  ...
  ...
  //开始bind,用的还是Linux的socket的bind方法
  rc = ::bind(listen_sd, listen_addr.get_sockaddr(), listen_addr.get_sockaddr_len());
  ...
  ...
   // listen!,Linux的方法,貌似并没有开启线程,只是准备好监听连接
  rc = ::listen(listen_sd, msgr->cct->_conf->ms_tcp_listen_backlog);
  ...
  ...
  //此处又执行了connection初始化,通知所有dispatcer?为啥两次?
  //因为不是同一个messenger了,这里是accepter自己的messenger
  msgr->init_local_connection();
  //创建pipe
  rc = create_selfpipe(&shutdown_rd_fd, &shutdown_wr_fd);
}

bind完成,在bind过程的最后一步创建了pipe

int Accepter::create_selfpipe(int *pipe_rd, int *pipe_wr) {
  int selfpipe[2]; //接收和发送各一个
  if (pipe_cloexec(selfpipe) < 0) { //执行Linux的pipe创建函数
    int e = errno;
    lderr(msgr->cct) << __func__ << " unable to create the selfpipe: "
                    << cpp_strerror(e) << dendl;
    return -e;
  }
  //为pipe的每个文件设置特性
  for (size_t i = 0; i < std::size(selfpipe); i++) {
    int rc = fcntl(selfpipe[i], F_GETFL);
    ceph_assert(rc != -1);
    rc = fcntl(selfpipe[i], F_SETFL, rc | O_NONBLOCK);
    ceph_assert(rc != -1);
  }
  *pipe_rd = selfpipe[0];
  *pipe_wr = selfpipe[1];
  return 0;
}
  1. 初始化守护进程貌似会开启log的线程和守护进程
	// Set up crypto, daemonize, etc.
	//global_init_daemonize(g_ceph_context, 0); 
	//log和守护进程开始,若已经存在是否就不需要创建?
	common_init_finish(g_ceph_context);
  1. 创建dispatcher并添加到messenger的dispatcher队列头
	dispatcher = new SimpleDispatcher(messenger);
	messenger->add_dispatcher_head(dispatcher); // should reach ready()
  void add_dispatcher_head(Dispatcher *d) {
    bool first = dispatchers.empty();
    dispatchers.push_front(d);
    if (d->ms_can_fast_dispatch_any())
      fast_dispatchers.push_front(d);
      //需要ready后才能执行之后的程序
    if (first)
      ready();
  }

同样的,ready函数在simplemessenger中实现,继承自messenger中的接口,在ready函数中会开启多个线程

  void SimpleMessenger::ready()
{
  ldout(cct,10) << "ready " << get_myaddr_legacy() << dendl;
  dispatch_queue.start(); 	//开启了 dispatch_thread和 local_delivery_thread两个线程
  lock.Lock();
  //bind成功,开启accepter线程
  if (did_bind)
    accepter.start();
  lock.Unlock();
}
  1. 开始simplemessenger
  reaper_started = true;  //开启reaper线程,用于回收通信关闭后的pipe资源
  reaper_thread.create("ms_reaper");
  1. 开始工作,阻塞住main函数 的线程,直到messenger shutdown
messenger->wait(); // can't be called until ready()
...
...
void SimpleMessenger::wait()
{
...
  if (!stopped)
    stop_cond.Wait(lock);  //阻塞住主线程,等待messenger的shutdown
...
  // 结束开启的线程,没有见到log和守护进程,所以这俩不归messenger管?
    accepter.stop();
    ...
  dispatch_queue.shutdown();
 ...

  if (reaper_started) {
    ldout(cct,20) << "wait: stopping reaper thread" << dendl;
    lock.Lock();
    reaper_cond.Signal();
    reaper_stop = true;
    lock.Unlock();
    reaper_thread.join();
    reaper_started = false;
    ldout(cct,20) << "wait: stopped reaper thread" << dendl;
  }
...
  // close+reap all pipes
  lock.Lock();
  {
    ldout(cct,10) << "wait: closing pipes" << dendl;
    while (!rank_pipe.empty()) {
      Pipe *p = rank_pipe.begin()->second;
      p->unregister_pipe();
      p->pipe_lock.Lock();
      p->stop_and_wait();
      ...
    }
    reaper(); //利用reaper来删除pipe
    ldout(cct,10) << "wait: waiting for pipes " << pipes << " to close" << dendl;
    while (!pipes.empty()) {
      reaper_cond.Wait(lock);
      reaper();
    }
  }
...
}

用流程图总结上述代码,可以表示为下图所示,此图从上述第五步的ready开始,描述了配置完成之后,simple通信方式的工作流程
在这里插入图片描述

2.2 开启的线程分析

三、simple_client

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值