Ignite的计算网格可以将比如一个计算这样的逻辑片段,可选地拆分为多个部分,然后在不同的节点并行地执行,这样就可以并行地利用所有节点的资源,来减少计算任务的整体执行时间。并行执行的最常见设计模式就是MapReduce。但是,即使不需要对计算进行拆分或者并行执行,计算网格也会非常有用,因为它可以通过将计算负载放在多个可用节点上,提高整个系统的扩展性和容错能力。
计算网格的API非常简单,可以将计算和数据处理发布到集群的多个节点上,还可以配置失败策略来控制特定作业失败时的行为。
计算网格的关键特性包括
IgniteCompute
接口提供了在集群节点或者一个集群组中运行很多种类型计算的方法,这些方法可以以一个分布式的形式执行任务或者闭包(内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后)。
只要至少有一个节点有效,所有的作业和闭包就会保证得到执行,如果一个作业的执行由于资源不足被踢出,它会提供一个故障转移的机制。如果发生故障,负载平衡器会选择下一个有效的节点来执行该作业,下面的代码显示了如何获得IgniteCompute
实例:
Ignite ignite = Ignition.ignite();
// Get compute instance over all nodes in the cluster.
IgniteCompute compute = ignite.compute();
也可以通过集群组来限制执行的范围,这时,计算只会在集群组内的节点上执行。
Ignite ignite = Ignitition.ignite();
ClusterGroup remoteGroup = ignite.cluster().forRemotes();
// Limit computations only to remote nodes (exclude local node).
IgniteCompute compute = ignite.compute(remoteGroup);
Ignite计算网格可以对集群或者集群组内的任何闭包进行广播和负载平衡,包括纯Java的runnables
和callables
。
IgniteRunnable 分别继承了Runnable, Serializable接口;
所有的broadcast(...)
方法会将一个给定的作业广播到所有的集群节点或者集群组。
广播
IgniteCompute compute = ignite.compute();
compute.broadcast(new IgniteRunnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Hello Node: " + ignite.cluster().localNode().id());
}
});
所有的call(...)
和run(...)
方法都可以在集群或者集群组内既可以执行单独的作业也可以执行作业的集合。
/**
* call 和 run 类型任务(单个任务或者任务集合)执行
*
* IgniteCallable接口继承了Callable, Serializable接口
*/
Collection> calls = new ArrayList<>();
// Iterate through all words in the sentence and create callable jobs.
for (final String word : "Count characters using callable".split(" ")) {
calls.add(new IgniteCallable() {
@Override public Integer call() throws Exception {
return word.length(); // Return word length.
}
});
}
// Execute collection of callables on the cluster.
Collection res = ignite.compute().call(calls);
int total = 0;
// Total number of characters.
// Looks much better in Java 8.
for (Integer i : res){
total += i;
}
System.out.println("分布式计算字符串字符长度总和为="+total);
闭包是一个代码块,它是把代码体和任何外部变量包装起来然后以一个函数对象的形式在内部使用它们,然后可以在任何传入一个变量的地方传递这样一个函数对象,然后执行。所有的apply方法都可以在集群内执行闭包。
/**
* apply()执行闭包(有异步执行方法)
* 闭包是一个代码块,它是把代码体和任何外部变量包装起来然后以一个函数对象的形式在内部使用它们,
* 然后可以在任何传入一个变量的地方传递这样一个函数对象,然后执行。
*/
IgniteCompute cmpute2 = ignite.compute();
//IgniteClosure的两个参数分别为闭包的参数类型和闭包的返回类型
Collection rets = cmpute2.apply(new IgniteClosure() {
@Override
public Integer apply(String e) {
// 返回当前字符串的长度
return e.length();
}
}, Arrays.asList("Count characters using closure".split(" ")));
//遍历并整合分布式计算结果
int sum = 0;
for (int len : res)
sum += len;
IgniteCompute提供了一个方便的API以在集群内执行计算。虽然也可以直接使用JDK提供的标准ExecutorService
接口,但是Ignite还提供了一个ExecutorService
接口的分布式实现然后可以在集群内自动以负载平衡的模式执行所有计算。该计算具有容错性以及保证只要有一个节点处于活动状态就能保证计算得到执行,可以将其视为一个分布式的集群化线程池。
// 从Ignite集群中获取 executor service.
ExecutorService exec = ignite.executorService();
// Iterate through all words in the sentence and create jobs.
for (final String word : "Print words using runnable".split(" ")) {
// Execute runnable on some node.
exec.submit(new IgniteRunnable() {
@Override public void run() {
System.out.println(">>> Printing '" + word + "' on this node from grid job.");
}
});
也可以限制作业在一个集群组中执行:
// Cluster group for nodes where the attribute 'worker' is defined.
ClusterGroup workerGrp = ignite.cluster().forAttribute("ROLE", "worker");
// Get cluster-enabled executor service for the above cluster group.
ExecutorService exec = ignite.executorService(workerGrp);
ComputeTask
是Ignite对于简化内存内MapReduce的抽象,这个也非常接近于ForkJoin范式(分而治之),纯粹的MapReduce从来不是为了性能而设计,只适用于处理离线的批量业务处理(比如Hadoop MapReduce)。不过当对内存内的数据进行计算时,实时性低延迟和高吞吐量通常具有更高的优先级。同样,简化API也变得非常重要。考虑到这一点,Ignite推出了ComputeTask
API,它是一个轻量级的MapReduce(或ForkJoin)实现。
注意 只有当需要对作业到节点的映射做细粒度控制或者对故障转移进行定制的时候,才使用
ComputeTask
。对于所有其它的场景,都需要使用分布式闭包中介绍的集群内闭包执行来实现。
ComputeTask
定义了要在集群内执行的作业以及这些作业到节点的映射,它还定义了如何处理作业的返回值(Reduce)。所有的IgniteCompute.execute(...)
方法都会在集群上执行给定的任务,应用只需要实现ComputeTask
接口的map(...)
和reduce(...)
方法即可。
任务是通过实现ComputeTask
接口的2或者3个方法定义的。
map方法
map(...)
方法将作业实例化然后将它们映射到工作节点,这个方法收到任务要运行的集群节点的集合还有任务的参数,该方法会返回一个map,作业为键,映射的工作节点为值。然后作业会被发送到工作节点上并且在那里执行。
result方法
result(...)
方法在每次作业在集群节点上执行时都会被调用,它接收计算作业返回的结果,以及迄今为止收到的作业结果的列表。该方法会返回一个ComputeJobResultPolicy
的实例,说明下一步要做什么。
WAIT
:等待所有剩余的作业完成(如果有)REDUCE
:立即进入Reduce阶段,丢弃剩余的作业和还未收到的结果FAILOVER
:将作业转移到另一个节点(参照容错章节),所有已经收到的作业结果也会在reduce(...)
方法中有效reduce方法
当所有作业完成后(或者从result(...)
方法返回REDUCE结果策略),reduce(...)
方法在Reduce阶段被调用。该方法接收到所有计算结果的一个列表然后返回一个最终的计算结果。
4.2中ComputeTask的实现稍显繁琐,4.3的适配器是对4.2中task的简化
定义计算时每次都实现ComputeTask
的所有三个方法并不是必须的,有一些帮助类使得只需要描述一个特定的逻辑片段即可,剩下的交给Ignite自动处理。
ComputeTaskAdapter
ComputeTaskAdapter
定义了一个默认的result(...)
方法实现,它在当一个作业抛出异常时返回一个FAILOVER
策略,否则会返回一个WAIT
策略,这样会等待所有的作业完成,并且有结果。
ComputeTaskSplitAdapter
ComputeTaskSplitAdapter
继承了ComputeTaskAdapter
,它增加了将作业自动分配给节点的功能。它隐藏了map(...)
方法然后增加了一个新的split(...)
方法,其只需要一个待执行的作业集合(这些作业到节点的映射会被适配器以负载平衡的方式自动处理)。
这个适配器对于所有节点都适于执行作业的同质化环境是非常有用的,这样映射阶段就可以隐式地完成。
任务触发的所有作业都实现了ComputeJob
接口,这个接口的execute()
方法定义了作业的逻辑然后返回一个结果。cancel()
方法定义了当一个作业被丢弃时的逻辑(比如,当任务决定立即进入Reduce阶段或者被取消)。
ComputeJobAdapter
这是一个提供了无操作的cancel()
方法的方便的适配器类。
ComputeTaskAdapter:
IgniteCompute compute = ignite.compute();
// Execute task on the clustr and wait for its completion.
int cnt = grid.compute().execute(CharacterCountTask.class, "Hello Grid Enabled World!");
System.out.println(">>> Total number of characters in the phrase is '" + cnt + "'.");
/**
* Task to count non-white-space characters in a phrase.
*/
private static class CharacterCountTask extends ComputeTaskAdapter {
// 1. Splits the received string into to words
// 2. Creates a child job for each word
// 3. Sends created jobs to other nodes for processing.
@Override
public Map extends ComputeJob, ClusterNode> map(List subgrid, String arg) {
String[] words = arg.split(" ");
Map map = new HashMap<>(words.length);
Iterator it = subgrid.iterator();
for (final String word : arg.split(" ")) {
// If we used all nodes, restart the iterator.
if (!it.hasNext())
it = subgrid.iterator();
ClusterNode node = it.next();
map.put(new ComputeJobAdapter() {
@Override public Object execute() {
System.out.println(">>> Printing '" + word + "' on this node from grid job.");
// Return number of letters in the word.
return word.length();
}
}, node);
}
return map;
}
@Override
public Integer reduce(List results) {
int sum = 0;
for (ComputeJobResult res : results)
sum += res.getData();
return sum;
}
}
ComputeTaskSplitAdapter:
IgniteCompute compute = ignite.compute();
// 在集群中执行task,并等待知道结果返回
int cnt = compute.execute(CharacterCountTask.class, "Hello Grid Enabled World!");
System.out.println(">>> Total number of characters in the phrase is '" + cnt + "'.");
/**
* 计算非空字符数
*/
private static class CharacterCountTask extends ComputeTaskSplitAdapter {
// 1. Splits the received string into to words
// 2. Creates a child job for each word
// 3. Sends created jobs to other nodes for processing.
@Override
public List split(int gridSize, String arg) {
String[] words = arg.split(" ");
List jobs = new ArrayList<>(words.length);
for (final String word : arg.split(" ")) {
jobs.add(new ComputeJobAdapter() {
@Override public Object execute() {
System.out.println(">>> Printing '" + word + "' on from compute job.");
// Return number of letters in the word.
return word.length();
}
});
}
return jobs;
}
@Override
public Integer reduce(List results) {
int sum = 0;
for (ComputeJobResult res : results)
sum += res.getData();
return sum;
}
}
每个任务执行时都会创建分布式任务会话,它是由ComputeTaskSession
接口定义的。任务会话对于任务和其产生的所有作业都是可见的,因此一个作业或者一个任务设置的属性也可以被其它的作业访问。任务会话也可以在属性设置或者等待属性设置时接收通知。
在任务及其相关的所有作业之间会话属性设置的顺序是一致的,不会出现一个作业发现属性A在属性B之前,而另一个作业发现属性B在属性A之前的情况。
在下面的例子中,让所有的作业在步骤1移动到步骤2之前是同步的:
@ComputeTaskSessionFullSupport注解
注意由于性能的原因分布式任务会话默认是禁用的,可以任务类上加注
@ComputeTaskSessionFullSupport
注解启用。
IgniteCompute compute = ignite.commpute();
compute.execute(new TaskSessionAttributesTask(), null);
/**
* Task demonstrating distributed task session attributes.
* Note that task session attributes are enabled only if
* @ComputeTaskSessionFullSupport annotation is attached.
*/
@ComputeTaskSessionFullSupport
private static class TaskSessionAttributesTask extends ComputeTaskSplitAdapter
通常来说在不同的计算作业或者服务之间共享状态是很有用的,为此Ignite在每个节点上提供了一个共享并发node-local-map。
IgniteCluster cluster = ignite.cluster();
ConcurrentMap nodeLocalMap = cluster.nodeLocalMap();
节点局部变量类似于线程局部变量,只不过节点局部变量不是分布式的,它只会保持在本地节点上。节点局部变量可以用于计算任务在不同的执行中共享状态,也可以用于部署的服务。
作为一个示例,创建一个作业,每次当它在某个节点上执行时都会使节点局部的计数器增加,这样,每个节点的节点局部计数器都会告诉我们一个作业在那个节点上执行了多少次。
/**
* 节点内状态共享--ignite每个节点都有一个“共享并发node-local-map”
* @param ignite
*/
private static void localState(Ignite ignite){
IgniteCallable job = new IgniteCallable() {
/**
* 将上下文中的Ignite实例注入到当前变量中
*/
@IgniteInstanceResource
private Ignite ignite2;
@Override
public Long call() {
// 从集群中获取nodeLocalMap(为jdk中的ConcurrentMap实例)
ConcurrentMap nodeLocalMap = ignite2.cluster().nodeLocalMap();
//获取计数器
AtomicLong cntr = nodeLocalMap.get("counter");
if (cntr == null) {
//当前新建的计数器放入localMap中同时返回原localMap
AtomicLong old = nodeLocalMap.putIfAbsent("counter", cntr = new AtomicLong());
if (old != null)
cntr = old;
}
return cntr.incrementAndGet();
}
};
ClusterGroup random = ignite.cluster().forRandom();
IgniteCompute compute = ignite.compute(random);
// The first time the counter on the picked node will be initialized to 1.
Long res = compute.call(job);
System.out.println( res == 1);
// Now the counter will be incremented and will have value 2.
res = compute.call(job);
System.out.println( res == 2);
}
计算和数据的并置可以最小化网络中的数据序列化,以及可以显著地提升应用的性能和可扩展性。只要可能,都应尝试将计算和存储待处理数据的节点并置在一起。
affinityCall(...)
和affinityRun(...)
方法使作业和缓存着数据的节点位于一处,换句话说,给定缓存名字和关联键,这些方法会试图在指定的缓存中定位键所在的节点,然后在那里执行作业。
一致性保证
从Ignite1.8版本开始,可以保证在执行由
affinityCall(...)
或者affinityRun(...)
触发的作业时,关联键所属的分区是不会从作业执行所处的节点退出的。而分区的再平衡通常是由拓扑变更事件触发的,比如新节点加入集群或者旧节点离开。 这个保证使得可以执行复杂的业务逻辑,因为作业执行的全过程中让数据一直位于同一个节点至关重要。比如,这个特性可以将执行本地SQL查询作为affinityCall(...)
或者affinityRun(...)
触发的作业的一部分,不用担心因为数据再平衡导致本地查询返回部分结果集。
final IgniteCache cache = ignite.cache(CACHE_NAME);
IgniteCompute compute = ignite.compute();
for (int i = 0; i < KEY_CNT; i++) {
final int key = i;
// This closure will execute on the remote node where
// data with the 'key' is located.
compute.affinityRun(CACHE_NAME, key, new IgniteRunnable() {
@Override public void run() {
// Peek is a local memory lookup.
System.out.println("Co-located [key= " + key + ", value= " + cache.peek(key) +']');
}
});
}
注意
affinityCall(...)
或者affinityRun(...)
方法都有重载的版本,可以锁定分区,避免作业跨多个缓存执行时,分区的退出,要做的仅仅是将缓存的名字传递给上述方法。
Ignite支持作业的自动故障转移,当一个节点崩溃时,作业会被转移到其它可用节点再次执行。不过在Ignite中也可以将任何作业的结果认为是失败的。工作节点可以仍然是在线的,但是它运行在一个很低的CPU、I/O、磁盘空间等资源上,在很多情况下会导致应用的故障然后触发一个故障的转移。此外,也有选择一个作业故障转移到那个节点的功能,因为同一个应用内部不同的程序或者不同的计算也会是不同的。
FailoverSpi
负责选择一个新的节点来执行失败作业。FailoverSpi
检查发生故障的作业以及该作业可以尝试执行的所有可用的网格节点的列表。它会确保该作业不会再次映射到出现故障的同一个节点。故障转移是在ComputeTask.result(...)
方法返回ComputeJobResultPolicy.FAILOVER
策略时触发的。Ignite内置了一些可定制的故障转移SPI实现。
只要有一个节点是有效的,作业就不会丢失。
Ignite默认会自动对停止或者故障的节点上的所有作业进行故障转移,如果要定制故障转移的行为,需要实现ComputeTask.result()
方法。下面的例子显示了当一个作业抛出任何的IgniteException
(或者它的子类)时会触发故障转移。
public class MyComputeTask extends ComputeTaskSplitAdapter {
...
@Override
public ComputeJobResultPolicy result(ComputeJobResult res, List rcvd) {
IgniteException err = res.getException();
if (err != null)
return ComputeJobResultPolicy.FAILOVER;
// If there is no exception, wait for all job results.
return ComputeJobResultPolicy.WAIT;
}
...
}
闭包的故障转移是被ComputeTaskAdapter
管理的,它在一个远程节点或者故障或者拒绝执行闭包时被触发。这个默认的行为可以被IgniteCompute.withNoFailover()
方法覆盖,它会创建一个设置了无故障转移标志的IgniteCompute
实例,下面是一个示例:
IgniteCompute compute = ignite.compute().withNoFailover();
compute.apply(() -> {
// Do something
...
}, "Some argument");
Ignite将任务拆分成作业然后为了加快处理的速度将它们分配给多个节点执行,如果一个节点故障了,AlwaysFailoverSpi
会将一个故障的作业路由到另一个节点,首先会尝试将故障的作业路由到该任务还没有被执行过的节点上,如果没有可用的节点,然后会试图将故障的作业路由到可能运行同一个任务中其它的作业的节点上,如果上述的尝试都失败了,那么该作业就不会被故障转移然后会返回一个null。
下面的配置参数可以用于配置AlwaysFailoverSpi
:
setter方法 | 描述 | 默认值 |
---|---|---|
setMaximumFailoverAttempts(int) |
设置尝试将故障作业转移到其它节点的最大次数 | 5 |
java方式配置ignite最大故障作业转移次数
AlwaysFailoverSpi failSpi = new AlwaysFailoverSpi();
IgniteConfiguration cfg = new IgniteConfiguration();
// Override maximum failover attempts.
failSpi.setMaximumFailoverAttempts(5);
// Override the default failover SPI.
cfg.setFailoverSpi(failSpi);
// Start Ignite node.
Ignition.start(cfg);