在QtRO中实现分布式计算的任务调度

这是本专栏对QtRO的第四篇介绍。前三篇分别是:

需求说明

用过QtRO的小伙伴应该对Source/Replica这套东西很熟悉了吧?Source是功能端,Replica是调用代理;一个Source可以和无数个Replica相连,而一个Replica只能连接特定的一个Source。Source实际上对Replica一无所知。所以从拓扑结构上看,QtRO网络是一对多的结构。

一般情况下,这种结构基本满足我们的开发需要。但是有时候,我们有这样一种需求:我们有一个任务调度的总结点,工作节点的个数不确定,可能动态变化,那如何使用QtRO技术来实现这样一种网络拓扑结构呢?

我们把需求明确下:

  1. 一个调度节点,不定个数、可动态变化的多个工作节点;
  2. 调度节点能够调用工作节点的功能,完成计算,最后获得计算结果。
  3. 计算任务只需要一个工作节点计算即可,不需要广播。

做过分布式计算开发的小伙伴应该接触过这种需求吧?其实用其他网络技术来实现这个需求也不难,比如ZMQ里就可以用REQ/REP的模式构建。但如果我们的系统已经用了QtRO,不想引入其他通讯库的话,如何实现这个需求呢?

问题分析

如果要用QtRO来做,首先要转变一个思维:我们一般把一对多中的“一”想当然地认为应该是Source端,而把“多”认为是多个Replica端。这么做是无法实现上面的任务调度的第二个和第三个需求的,因为正如我们开篇时说的,Source端对Replica一无所知,那它怎么对Replica进行一对一的发送计算任务、等待计算结果?

所以我们这次需要将调度节点认为是Replica端,而各个工作节点认为是Source端。Source和Replica是一对多的关系,但这并没有限制一个进程中拥有多个Replica。我们的拓扑结构如下图所示:

现在问题的关键变成了:调度节点如何自动获得工作节点的Replica?如果这个问题解决了,那任务调度的三个需求也就能满足了:

  1. 因为调度节点自动获得工作节点的Replica,所以工作节点可以随意增删;
  2. 因为每个工作节点都有对应的Replica,调度节点只要根据调度策略,选取特定的Replica,然后向对应的Source发任务即可;
  3. Replica调用Source槽函数,可以通过QRemoteObjectPendingCall来等待槽函数返回值,也可以通过等待特定信号来获得返回值。

OK,核心问题分析出来了,那具体该怎么办?

讲了那么多,终于要祭出我们的核心代码了。QtRO中如果有Registry节点,那么每个接入的QRemoteObjectNode以及它的派生类节点都有个QRemoteObjectRegistry类型的registry属性。而它有两个关键信号:

void remoteObjectAdded(const QRemoteObjectSourceLocation &entry);
void remoteObjectRemoved(const QRemoteObjectSourceLocation &entry);

当带有Registry的QtRO网络中有新的Source被enableRemoting出来,则每个接入的Node的registry属性都会发出remoteObjectAdded这个信号;当有Source被disableRemoting之后,每个接入的Node的registry都会发出remoteObjectRemoved信号。这两个信号都带有一个参数,让我们看看这个参数里都有些什么信息。由于Qt帮助文档中没有对这个类型的说明,我们只能翻看QtRO的源码。在qtremoteobjectglobal.h这个文件中我们找到了定义:

struct QRemoteObjectSourceLocationInfo
{
// 省略一些构造函数之类的代码
    QString typeName;
    QUrl hostUrl;
};
typedef QPair<QString, QRemoteObjectSourceLocationInfo> QRemoteObjectSourceLocation;
typedef QHash<QString, QRemoteObjectSourceLocationInfo> QRemoteObjectSourceLocations;

所以这个参数带上了必要的、能让我们在调度节点上acquire到Source 的Replica的信息。

最终方案

我们最终的方案步骤如下:

调度节点需要做以下几步:

  1. 使用QRemoteObjectHostRegistry,确保QtRO网络中有Registry引入。
  2. 监听该节点的registry属性的 remoteObjectAddedremoteObjectRemoved信号。当remoteObjectAdded信号来时,通过参数acquire到对应的Replica;当remoteObjectRemoved信号来时,删掉对应的Replica;
  3. 当需要派发任务时,根据调度算法,从所有的Replica中取出空闲的Replica,然后调用远程和工作节点协商好的函数,使用QRemoteObjectPendingCallwaitForFinish同步等待结果,或者通过Replica的相关信号异步接收计算结果。

工作节点需要做以下几步:

  1. 使用QRemoteObjectHost接入QtRO网络,同时设置好Registry的URL;
  2. 编写实现具有协商好的函数接口的Source类,能够处理计算任务;
  3. 实例化Source类,将对象用Host节点enableRemoting出去;
  4. 如果需要退出QtRO网络,调用Host节点的disableRemoting函数。

总的来看,思路还是比较清晰的,多亏QtRO底层支持,我们所需要做的都是业务相关的,其他都由QtRO网络自动帮我们处理了。

注意事项

上述实现有几点注意事项:

  1. QRemoteObjectHostQRemoteObjectHostRegistry本身也有remoteObjectAddedremoteObjectRemoved这两个信号,但它只有当它本身使用enableRemotingdisableRemoting时才会发射;当QtRO网络中其他节点有Source上线或下线时它是不会发送的。而我们文中用的是它们的registry属性发出的这两个信号。千万别搞混了!
  2. 只有网络中有Registry的时候,才有我们上面的这套逻辑。否则registry的属性是无效的,自然没有了那两个信号。
  3. 良好的系统架构设计中,我们可以引入一个工作节点的Source基类(接口类),然后让工作节点通过它实例化接口。
编辑于 2019-06-08 10:30