开发者在搭建第一个Ignite集群时,常常会遇到各种障碍,社区在收集了各种常见问题后,整理了一份检查清单帮助开发者,总之,本文的目的是帮助开发者在一开始就搭建一个正常的集群,走在正确的道路上。
配置日志
准备启动:设置日志
首先,需要日志,因为在解决很多问题的时候,需要每个节点的日志。
虽然Ignite默认是开启日志记录的,但是默认为QUIET
模式,会忽略掉INFO
和DEBUG
级别的日志输出,如果系统属性IGNITE_QUIET
配置为false
,则Ignite将以正常的、没有限制的日志记录模式运行,注意,所有QUIET
模式的日志都会输出到标准输出(STDOUT)。
只有严重的日志才会出现在控制台中,其它的日志都会记录在文件中,默认位置为${IGNITE_HOME}/work/log
,注意不要删除它,以后可能有用。
Ignite以默认方式启动时的标准输出
对于一些简单问题的处理,不需要做单独的监控,只需要在命令行中以详细模式启动即可:
然后,系统会将所有事件与其它一些应用日志信息一起,输出到标准输出中。
这时,再有问题就可能从日志中找到解决方案,比如如果集群崩溃,可能会发现"在某某配置处增加某某超时配置"之类的信息,这就说明,该配置太小了,网络质量很差。
禁用组播
很多人遇到的第一个问题是,集群中出现了预期之外的节点,即启动一个节点后,在集群的拓扑快照中,不是一个节点,而是2个或者多个,怎么回事呢?
比如下图,指出集群中有2个节点:
出现这种情况的可能原因是,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增加如下的参数:
之后Java和Ignite将不再使用IPv6,问题解决。
序列化/反序列化
集群搭建完成之后,业务代码和Ignite之间的主要交互点之一是Marshaller
(序列化)。要将任何内容写入内存、持久化或通过网络发送,Ignite首先会将对象序列化,这时可能会看到类似cannot be written in binary format
或cannot 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
写入了长度和数据:
TreeMap
的writeObject()
实现:
Binarylizable
的writeBinary()
和readBinary()
工作方式类似:BinaryTreeMap
将自身包装到简单的TreeMap
中并将其写入OutputStream
,这种方式对于编码来说微不足道,但是性能提升非常明显。
BinaryTreeMap
的writeBinary()
实现:
正确地使用Ignite实例
Ignite是支持分布式计算的,那么如何在所有服务器上执行lambda表达式呢?
首先看下下面的代码有什么错误:
或者这样:
熟悉lambda表达式和匿名类缺陷的人都知道,当引用外部变量时会出现问题,匿名类更复杂。
还有一个示例,这里再次使用Ignite的API执行lambda。
在闭包中使用Ignite实例的错误方式:
这个代码段的逻辑基本上是从Ignite中获取缓存并在本地执行一个SQL查询,当只需要处理远程节点上的本地数据时,这会很有用。
那么问题是什么呢?Lambda又引用了外部资源,但这次不是一般的对象,而是发送Lambda的节点上的本地Ignite实例。这样做可能可以运行,因为Ignite对象有一个readResolve()
方法,它可以通过反序列化将通过网络传输的Ignite对象替换为目标节点上的本地对象,但这样做也可能产生预期之外的结果。
从本质上讲,虽然只要想要,可以通过网络传输尽可能多的数据,但是获取Ignite对象或者它的任意接口的最简单方法,是使用Ignition.localIgnite()
。可以从Ignite创建的任何线程调用它,并获得对本地对象的引用。在Lambda、服务等等中,如果需要Ignite对象,都建议这样做。
在闭包中使用Ignite的正确方式:通过localIgnite()
这部分的最后一个示例,Ignite中有一个服务网格,它允许在集群中部署微服务,Ignite可以永久保持在线所需的实例数。想象一下,假如也需要在此服务中引用Ignite实例,怎么弄呢?其实也可以使用localIgnite()
,但这时需要在字段中保留此引用。
它接收一个Ignite实例的引用作为构造函数的参数,这是错误的。
这个问题有更简单的解决办法,即使用@IgniteInstanceResource
注释该字段,创建服务后,该实例会自动注入,然后就可以用了。建议开发者这样做,而不要传递Ignite对象或者它的子对象。
服务使用@IgniteInstanceResource
后:
控制基线拓扑
现在集群已经搭建好,代码也有了。
考虑下下面的场景:
- 一个
REPLICATED
模式的缓存:每个节点上都有数据副本; - 打开了原生持久化:写入磁盘。
先启动一个节点,因为开启了原生持久化,所以必须先激活才能用,激活之后,再启动一些其它的节点。
看上去是正常的:可以按照预期读取和写入数据,每个节点都有数据副本。关闭一个节点也是可以的,但是如果关闭第一个节点,就不行了,会发现出现了数据丢失,集群无法操作,这是由基线拓扑(存储持久化数据的节点集)引起的,因为其它节点并不持久化数据。
这个节点集在第一次激活时定义,后续添加的节点默认是不会包含在基线拓扑中的,因此目前的基线拓扑只包含最初的第一个节点,这个节点故障整个集群就会故障。为了避免这种情况,正确的做法是首先就要启动所有的节点,然后再激活集群,如果要往基线拓扑中添加或者删除节点,可以使用下面的命令:
此脚本还可以对基线进行刷新,使其保持最新状态。
control.sh的使用:
调整数据并置
现在已经明确,数据已经持久化,下面就要对其进行读取。因为Ignite支持SQL,所以可以像Oracle一样执行SELECT查询,并且还支持线性扩展,因为数据是分布式的。考虑下面的模型:
SQL查询:
但是上面的代码并没有返回所有的数据,为什么呢?
这里Person
通过orgId
与Organization
进行关联,这是一个典型的外键。但是仅仅这样做,将两个表关联之后执行SQL查询,如果集群中有几个节点,那么并不会返回正确的结果。
这是因为默认的SQL关联仅适用于单个节点,如果SQL遍历整个集群以收集数据并返回,会非常低效,这样会失去分布式系统的优势,所以Ignite默认只会在本地节点上检索数据。
如果要获得正确的数据,需要对数据进行并置。要正确地关联Person
和Organization
,相关的数据应该存储在同一个节点上。
最简单的做法是声明一个关系键,该键会针对给定的值确定实际的节点和分区。如果将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:节点数。
则需要满足的条件是:
这里>>
是远大于,如果满足上图的条件,数据就会均匀分布。另外要指出的是,默认的分区数(P)为1024。
可能分布仍然不是很均匀,这是Ignite 1.x系列版本的问题。当时的算法为FairAffinityFunction
,虽然运行正常但是节点间的流量过多,现在的算法是RendezvousAffinityFunction
,但是也不是绝对公平,误差在正负5-10%。
总结
总结一下,在搭建第一个Ignite集群时,对于不熟悉Ignite的新人来说,要注意以下的注意事项:
- 设置日志;
- 禁用组播,仅指定实际使用的IP和端口;
- 禁用IPv6;
- 熟悉序列化/反序列化;
- 控制基线拓扑;
- 调整关系并置。