搭建第一个Ignite集群时的注意事项

开发者在搭建第一个Ignite集群时,常常会遇到各种障碍,社区在收集了各种常见问题后,整理了一份检查清单帮助开发者,总之,本文的目的是帮助开发者在一开始就搭建一个正常的集群,走在正确的道路上。

配置日志

准备启动:设置日志

首先,需要日志,因为在解决很多问题的时候,需要每个节点的日志。

虽然Ignite默认是开启日志记录的,但是默认为QUIET模式,会忽略掉INFODEBUG级别的日志输出,如果系统属性IGNITE_QUIET配置为false,则Ignite将以正常的、没有限制的日志记录模式运行,注意,所有QUIET模式的日志都会输出到标准输出(STDOUT)。

只有严重的日志才会出现在控制台中,其它的日志都会记录在文件中,默认位置为${IGNITE_HOME}/work/log,注意不要删除它,以后可能有用。

Ignite以默认方式启动时的标准输出

搭建第一个Ignite集群时的注意事项

对于一些简单问题的处理,不需要做单独的监控,只需要在命令行中以详细模式启动即可:

搭建第一个Ignite集群时的注意事项

然后,系统会将所有事件与其它一些应用日志信息一起,输出到标准输出中。

这时,再有问题就可能从日志中找到解决方案,比如如果集群崩溃,可能会发现"在某某配置处增加某某超时配置"之类的信息,这就说明,该配置太小了,网络质量很差。

禁用组播

很多人遇到的第一个问题是,集群中出现了预期之外的节点,即启动一个节点后,在集群的拓扑快照中,不是一个节点,而是2个或者多个,怎么回事呢?

比如下图,指出集群中有2个节点:

搭建第一个Ignite集群时的注意事项

出现这种情况的可能原因是,Ignite启动时默认使用组播,并在一个子网中查找位于同一组播组中的其它Ignite节点,找到后会尝试与其建立连接,如果连接失败,整个启动就会失败。

为了防止这种情况发生,最简单的做法就是使用静态IP地址,不是默认的TcpDiscoveryMulticastIpFinder而是TcpDiscoveryVmIpFinder,然后指定所有要连接的IP和端口,这会规避很多问题,尤其是在测试和开发环境中。

IP地址太多

另一个问题就是IP地址过多。禁用组播后再次启动集群,这时已经在配置中指定了来自不同环境的大量IP,有时第一个节点的启动需要5到10分钟,尽管后续每个节点的启动只需要5到10秒。

以IP地址列表为3个IP为例,对于每个地址,指定了10个端口的范围,这样总共得到了30个TCP地址,Ignite在创建新集群之前,默认会尝试连接到已有的集群,这样就会依次检查每个IP。

如果只是在自己的电脑上工作,这样问题不大,但是在云环境或者企业网络中,通常会启用端口扫描保护,这意味着如果要访问某IP上的专用端口时,可能在超时到期之前没有任何反馈,这个超时时间默认为10秒。

解决方案很简单,不要指定过多的端口,在生产上,一个端口就够了,当然也可以禁用内部网络的端口扫描保护。

IPv4和IPv6

第三个常见问题与IPv6有关,开发者有可能遇到一些奇怪的网络错误信息,比如:failed to connect或者failed to send a message, node segmented.等,如果已经从集群中分离,则会发生这种情况。通常来说这种问题是由IPv4和IPv6的异构环境造成的,Ignite虽然支持IPv6,但是目前存在一些问题。

最简单的解决方案是为JVM增加如下的参数:

搭建第一个Ignite集群时的注意事项

之后Java和Ignite将不再使用IPv6,问题解决。

序列化/反序列化

集群搭建完成之后,业务代码和Ignite之间的主要交互点之一是Marshaller(序列化)。要将任何内容写入内存、持久化或通过网络发送,Ignite首先会将对象序列化,这时可能会看到类似cannot be written in binary formatcannot be serialized using BinaryMarshaller.这样的消息,这时就需要稍微调整一下代码以将其与Ignite结合使用。

Ignite支持3种序列化机制:

  • JdkMarshaller:常见的Java序列化;
  • OptimizedMarshaller:优化的Java序列化,但与JdkMarshaller基本一致;
  • BinaryMarshaller:一个专门为Ignite实现的序列化机制。它有很多优点,有时需要摆脱过多的序列化/反序列化,有时甚至可以为无法序列化的对象提供一个API接口,并以二进制格式处理它,就像JSON一样。

BinaryMarshaller能够对除了字段和简单方法之外什么都没有的POJO对象进行序列化和反序列化。但是如果通过readObject()writeObject()方法自定义了序列化,并且使用了Externalizable接口,那么BinaryMarshaller将无法对对象进行序列化,它会回归到OptimizedMarshaller

如果发生了这种情况,就必须实现Binarylizable接口。这非常简单。

例如有一个Java中的标准TreeMap,它通过readObject()writeObject()方法自定义了序列化和反序列化,首先它描述了一些字段,然后向OutputStream写入了长度和数据:

TreeMapwriteObject()实现:

搭建第一个Ignite集群时的注意事项

BinarylizablewriteBinary()readBinary()工作方式类似:BinaryTreeMap将自身包装到简单的TreeMap中并将其写入OutputStream,这种方式对于编码来说微不足道,但是性能提升非常明显。

BinaryTreeMapwriteBinary()实现:

搭建第一个Ignite集群时的注意事项

正确地使用Ignite实例

Ignite是支持分布式计算的,那么如何在所有服务器上执行lambda表达式呢?

首先看下下面的代码有什么错误:

搭建第一个Ignite集群时的注意事项

或者这样:

搭建第一个Ignite集群时的注意事项

熟悉lambda表达式和匿名类缺陷的人都知道,当引用外部变量时会出现问题,匿名类更复杂。

还有一个示例,这里再次使用Ignite的API执行lambda。

在闭包中使用Ignite实例的错误方式:

搭建第一个Ignite集群时的注意事项

这个代码段的逻辑基本上是从Ignite中获取缓存并在本地执行一个SQL查询,当只需要处理远程节点上的本地数据时,这会很有用。

那么问题是什么呢?Lambda又引用了外部资源,但这次不是一般的对象,而是发送Lambda的节点上的本地Ignite实例。这样做可能可以运行,因为Ignite对象有一个readResolve()方法,它可以通过反序列化将通过网络传输的Ignite对象替换为目标节点上的本地对象,但这样做也可能产生预期之外的结果。

从本质上讲,虽然只要想要,可以通过网络传输尽可能多的数据,但是获取Ignite对象或者它的任意接口的最简单方法,是使用Ignition.localIgnite()。可以从Ignite创建的任何线程调用它,并获得对本地对象的引用。在Lambda、服务等等中,如果需要Ignite对象,都建议这样做。

在闭包中使用Ignite的正确方式:通过localIgnite()

搭建第一个Ignite集群时的注意事项

这部分的最后一个示例,Ignite中有一个服务网格,它允许在集群中部署微服务,Ignite可以永久保持在线所需的实例数。想象一下,假如也需要在此服务中引用Ignite实例,怎么弄呢?其实也可以使用localIgnite(),但这时需要在字段中保留此引用。

搭建第一个Ignite集群时的注意事项

它接收一个Ignite实例的引用作为构造函数的参数,这是错误的。

这个问题有更简单的解决办法,即使用@IgniteInstanceResource注释该字段,创建服务后,该实例会自动注入,然后就可以用了。建议开发者这样做,而不要传递Ignite对象或者它的子对象。

服务使用@IgniteInstanceResource后:

搭建第一个Ignite集群时的注意事项

控制基线拓扑

现在集群已经搭建好,代码也有了。

考虑下下面的场景:

  • 一个REPLICATED模式的缓存:每个节点上都有数据副本;
  • 打开了原生持久化:写入磁盘。

先启动一个节点,因为开启了原生持久化,所以必须先**才能用,**之后,再启动一些其它的节点。

看上去是正常的:可以按照预期读取和写入数据,每个节点都有数据副本。关闭一个节点也是可以的,但是如果关闭第一个节点,就不行了,会发现出现了数据丢失,集群无法操作,这是由基线拓扑(存储持久化数据的节点集)引起的,因为其它节点并不持久化数据。

这个节点集在第一次**时定义,后续添加的节点默认是不会包含在基线拓扑中的,因此目前的基线拓扑只包含最初的第一个节点,这个节点故障整个集群就会故障。为了避免这种情况,正确的做法是首先就要启动所有的节点,然后再**集群,如果要往基线拓扑中添加或者删除节点,可以使用下面的命令:

搭建第一个Ignite集群时的注意事项

此脚本还可以对基线进行刷新,使其保持最新状态。

control.sh的使用:

搭建第一个Ignite集群时的注意事项

调整数据并置

现在已经明确,数据已经持久化,下面就要对其进行读取。因为Ignite支持SQL,所以可以像Oracle一样执行SELECT查询,并且还支持线性扩展,因为数据是分布式的。考虑下面的模型:

搭建第一个Ignite集群时的注意事项

SQL查询:

搭建第一个Ignite集群时的注意事项

但是上面的代码并没有返回所有的数据,为什么呢?

这里Person通过orgIdOrganization进行关联,这是一个典型的外键。但是仅仅这样做,将两个表关联之后执行SQL查询,如果集群中有几个节点,那么并不会返回正确的结果。

这是因为默认的SQL关联仅适用于单个节点,如果SQL遍历整个集群以收集数据并返回,会非常低效,这样会失去分布式系统的优势,所以Ignite默认只会在本地节点上检索数据。

如果要获得正确的数据,需要对数据进行并置。要正确地关联PersonOrganization,相关的数据应该存储在同一个节点上。

最简单的做法是声明一个关系键,该键会针对给定的值确定实际的节点和分区。如果将Person中的orgId做为关系键,则具有相同orgId的人将位于相同的节点上。

如果由于某种原因无法做到这一点,那么还有另外一个比较低效的解决方案,即启用分布式关联。这是在API层面实现的,该过程取决于使用的是Java、JDBC还是其它的什么接口,虽然速度会变慢,但是至少会返回正确的结果。

下面要考虑如何定义关系键,即如何确认一个特定的ID和一个关联的字段适合定义关联性呢?如果定义所有具有相同orgId的人都被并置,那么orgId就是一个不可分割的独立的块,就不能再将其分布在不同的节点上。如果数据库中有10个组织,那么就可以在10个节点上有10个不可分割的块,如果集群中有更多的节点,那么就会有部分"多余"的节点不属于任何块,这在运行时很难确定,所以要提前规划好。

如果有1个大的组织和9个小的组织,那么块的大小就会有所不同。但是Ignite在节点间分发数据时并不会考虑某个关系组中有多少记录,它不会在一个节点上放1个大的块,然后在另一个节点上放9个块来平衡负载,更可能的分配比例是5:5(或者6:4,甚至于7:3)。

如何让数据分布均匀呢?可以看下面的维度:

  • K:键数量(即数据量);
  • A:关系键数量;
  • P:分区数,即Ignite在节点间分配的大量数据组;
  • N:节点数。

则需要满足的条件是:

搭建第一个Ignite集群时的注意事项

这里>>是远大于,如果满足上图的条件,数据就会均匀分布。另外要指出的是,默认的分区数(P)为1024。

可能分布仍然不是很均匀,这是Ignite 1.x系列版本的问题。当时的算法为FairAffinityFunction,虽然运行正常但是节点间的流量过多,现在的算法是RendezvousAffinityFunction,但是也不是绝对公平,误差在正负5-10%。

总结

总结一下,在搭建第一个Ignite集群时,对于不熟悉Ignite的新人来说,要注意以下的注意事项:

  • 设置日志;
  • 禁用组播,仅指定实际使用的IP和端口;
  • 禁用IPv6;
  • 熟悉序列化/反序列化;
  • 控制基线拓扑;
  • 调整关系并置。

转载于:https://my.oschina.net/liyuj/blog/3028514