Ignite的数据网格是围绕着基于内存的分布式key/value存储能力打造的。当初技术选型的时候,决定用Ignite也是因为虽然同样是key/value存储,它有着和其他key/value存储系统不同的特性。根据官网的介绍,Ignite在设计之初,就是为了能方便的水平扩展而设计的。Ignite将数据分片,每个节点只存储数据的一部分,这样每当有新节点加入时,整个集群可以存储更多的数据。为了提高可用性,Ignite也支持用不同的策略对数据分片进行冗余备份,这样保证数据不会因为集群中一两个节点失效而丢失。另外,和其他key/value缓存系统最大的不同是,Ignite支持用SQL语句对缓存数据进行查询。正是由于有对SQL 99的支持,我们甚至可以把Ignite当做一个分布式内存数据库来使用。
从这篇文章开始,我们先聚焦在Ignite提供的数据网格服务上,看看同样是基于key/value存储,Ignite的key/value缓存又提供了哪些能力。
消失的数据
在介绍Ignite不同的缓存冗余备份模式之前,我们先用上一篇文章的代码来模拟一下在默认配置下,如果Ignite集群中有节点失效,我们的数据是否还完整有效。我们在同一台虚拟机按以下的顺序分别启动server和client实例:
- 启动一个server实例,该实例会创建一个“TEST”缓存并写入三条缓存数据。
- 再启动一个server实例,该实例会自动加入之前启动的server节点,组成一个Ignite集群。因为该实例创建的缓存名字和写入的缓存数据都一样,所以"TEST"缓存里的数据保持不变。
- 启动一个client实例,查询“TEST”缓存里的数据,此时该client应该可以查询到三条之前写入的缓存数据:
Montreal is in Quebec
Edmonton is in Alberta
Markham is in Ontario
Toronto is in null
- 关闭一个server实例,再启动一个client实例做查询,此时我们会发现某些缓存数据消失了。比如在我的环境里,就查不到Markham这条数据了:
Montreal is in Quebec
Edmonton is in Alberta
Markham is in null
Toronto is in null
因为Ignite将缓存数据分片存储的,即同一缓存中的不同数据存到不同的server节点上,Ignite通过一个哈希算法计算出某个数据分片所属的节点。除了原生的哈希算法,用户也可实现自己的哈希算法来决定数据分片对应的节点。在上面的步骤1中,缓存数据全部存在仅有的唯一一个server实例中。在步骤2中,当有新的server实例加入集群,Ignite通过默认的哈希算法决定哪部分的数据分片应该存到新加入的server实例中,然后将数据在两个实例间重新平衡分布。由于我们采用的是默认的配置,所以每个数据分片只有一份拷贝,这就是为什么当我们关了一个server实例后会发生数据丢失的情况。因此,为了保证数据的高可用,我们必须调整数据分片拷贝的数量。接下来,我们就来看看Ignite提供了哪些对数据分片进行冗余备份的策略,以便用户根据实际需求在性能和数据高可用性之间做选择。
Ignite缓存数据分片冗余策略
Local模式
我们先从简单的模式说起,Local模式,顾名思义就是缓存的所有数据只保存在本地节点,数据不会分布到其他节点上,也就没有数据分片和数据拷贝这么一说。Local模式的最大好处是它的轻量化,因为没有了数据分片和冗余备份的负担,其非常适合于数据只读模式和需要定期刷新的场景,也适合于作为一个read-through的缓存。除了数据分布不同,采用local模式的缓存和分布式缓存有着相同的功能,比如数据自动清除,过期失效,磁盘交换,数据查询以及事务等特性。
Replicated模式
Replicated模式下,缓存数据虽然被均分为多个数据分片,但每个节点上都有该缓存的全部数据分片。下面这张官网图很好的展示了replicated模式的数据分布:
在replicated模式下,缓存数据被平分为4个数据分片A、B、C、D。在节点1(JVM1)上有分片A的primary拷贝和B、C、D的backup拷贝。在节点2(JVM2)上有分片C的primary拷贝和A、B、D的backup拷贝。节点3(JVM3)和节点4(JVM4)的情况也类似。关于数据分片的primary和backup拷贝的概念我们在下一篇介绍,这里只要记住当primary拷贝失效了,Ignite可以用backup拷贝恢复数据,保证了数据的高可靠性。所以在replicated模式下,每个节点其实有缓存的所有数据分片拷贝,即便集群里其他节点都失效,Ignite还是可以通过仅存的一个节点提供数据读写服务
Partitioned模式
Partition模式下,缓存数据被均分为多个数据分片,数据分片的拷贝均等的分布在集群的某些节点上。换句话说,和replicated模式最大的不同就是,一个节点上没有全部的缓存数据分片拷贝。让我们借用官网的图来解释一下partitioned模式:
如上图所示,在partitioned模式下,缓存数据被平分为4个数据分片A、B、C、D,每个数据分片有一份primary拷贝和backup拷贝,所以每个节点只保存两个数据分片的拷贝,比如节点1(JVM1)有分片A的primary分片和分片B的backup分片,节点2(JVM2)有分片C的primary分片和分片A的backup分片。Backup拷贝的数量是用户可配置的,如果配置为0时,代表着一个数据分片没有副本,一旦某个节点挂了,数据就会丢失。如果配置为(集群节点数量-1),代表着集群的每个节点上都有一份该数据分片的拷贝,这就相当于一种特殊的replicated模式。拷贝数量越多,代表数据约可靠,但也会带来额外的开销,所以我们还是要根据实际的场景和需求来调整拷贝数量。
Replicated V.S. Partitioned
让我们简单的比较下两种模式的优缺点以及它们适合的场景:
- 首先,从数据的可靠性来说当然是replicated模式占优势,毕竟每个节点都有缓存的所有数据分片,只要集群中有一个节点还能工作,就能从该节点恢复数据。而partitioned模式下,数据的可靠性是和backup数量N相关的,在partitioned模式下,一旦有N+1个节点失效,集群就有可能出现丢失数据的情况。
- 其次,从扩展性上看,是partitioned模式优于replicated模式。因为每个节点需要有所有数据分片的拷贝,在replicated模式下,集群所能容纳的数据大小是受单个节点的内存和硬盘(如果启用了Ignite原生的持久化功能)限制的,即便新增节点,也不能提高集群的数据容量。反观partitioned模式,新增加一个节点就可以给集群增加更多的存储能力,容纳更多的数据。
- 再次,从读写性能上看,replicated模式适合多读少写的场景,因为每写一份数据,就要同步到集群中所有的节点上,如果节点数量多了,同步的开销还是很可观的。对于读数据,因为每个节点上都有缓存数据的拷贝,所以在replicated模式下的读可以充分利用所有节点的带宽,提供更好的读性能。而Partitioned模式更适合多写少读的场景,因为写数据时需要同步的节点数量要少,所以写性能更好。对于读场景,因为一份数据的拷贝只在集群的几台节点上,所以读性能势必会受影响。
配置缓存Replicated/Partitioned模式
好了,在了解完Ignite缓存不同的数据分片冗余策略后,让我们通过一个实际的例子看看如何在代码或是xml配置文件中配置不同的数据分片冗余策略。我们在上一篇文章的server节点代码上进行改造,大部分逻辑都保持不变,重点注意一下第26行~34行加入的新代码:
public class IgniteCacheOpModeExample {
public static void main(String[] args) {
Ignite ignite;
// 创建一个TEST缓存并写入一些数据, key是城市的名字,value是省的名字
IgniteCache<String, String> cityProvinceCache;
if(args.length == 1 && !args[0].isEmpty())
{
//如果启动时指定了配置文件,则用指定的配置文件
System.out.println("Use " + args[0] + " to start.");
ignite = Ignition.start(args[0]);
//配置文件中,我们将缓存设置为partitioned模式,backup数量为1
cityProvinceCache = ignite.getOrCreateCache("TEST");
}
else
{
//如果启动时没指定配置文件,则生成一个配置文件
System.out.println("Create an IgniteConfiguration to start.");
TcpDiscoverySpi spi = new TcpDiscoverySpi();
TcpDiscoveryMulticastIpFinder ipFinder = new TcpDiscoveryMulticastIpFinder();
ipFinder.setMulticastGroup("224.0.0.251");
spi.setIpFinder(ipFinder);
IgniteConfiguration cfg = new IgniteConfiguration();
cfg.setDiscoverySpi(spi);
ignite = Ignition.start(cfg);
CacheConfiguration<String, String> cacheCfg = new CacheConfiguration("TEST");
// 如果不用配置文件启动,缓存模式被设置为replicated
cacheCfg.setCacheMode(CacheMode.REPLICATED);
/* 下面的配置将"TEST"缓存设为partitioned模式,并且设置了backup数量为1,这样保证即使有一个node出现
故障的情况下,缓存数据还是完整可用的
cacheCfg.setCacheMode(CacheMode.PARTITIONED);
cacheCfg.setBackups(1);
*/
cityProvinceCache = ignite.getOrCreateCache(cacheCfg);
}
cityProvinceCache.put("Edmonton", "Alberta");
cityProvinceCache.put("Markham", "Ontario");
cityProvinceCache.put("Montreal", "Quebec");
}
}
在调用ignite.getOrCreateCache()函数之前,我们为"TEST"先生成一个CacheConfiguration,然后调用setCacheMode()将其模式设置为REPLICATED模式(在29~33行被注释掉的代码中,是如何设置PARTITIONED模式以及backups数量的代码),最后再交由Ignite根据configuration生成"TEST"缓存。 当然,和上一篇一样,也可以通过XML文件来配置缓存模式:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
<property name="discoverySpi">
<bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
<property name="ipFinder">
<bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
<property name="multicastGroup" value="224.0.0.251"/>
</bean>
</property>
</bean>
</property>
<property name="cacheConfiguration">
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<!-- 设置缓存名字. -->
<property name="name" value="TEST"/>
<!-- 设置缓存模式. -->
<property name="cacheMode" value="PARTITIONED"/>
<property name="backups" value="1"/>
<!-- 下面将缓存设置为replicated模式 -->
<!--property name="cacheMode" value="REPLICATED"/-->
</bean>
</property>
</bean>
</beans>
在XML文件中,我们加入了cacheConfiguration的配置,为了和代码里创建的缓存名字保持一致,配置里也使用了"TEST"作为缓存名字,"cacheMode"设为PARTITIONED,"backups"值设为了1(每个数据分片除了primay拷贝外,还有额外的一份backup拷贝,即缓存可以允许有一个节点故障而保证缓存数据的完整性)。
更新了代码和配置文件后,server节点如果制定了XML配置文件启动,生成的缓存为带一个backup的PARTITIONED模式,如果不用XML配置文件,则生成的缓存为REPLICATED模式。无论用哪种方式启动server节点,我们再重复这篇文章开头的那个实验,就会发现即使在一个节点失效的情况下,client节点还是可以访问到缓存中的所有数据,不会再出现丢数据的情况了。在实际使用过程中,正确的配置缓存的冗余模式直接影响到Ignite集群数据的高可用性。
总结
这篇文章我们介绍了Ignite集群中数据分片的不同冗余策略,在实际的使用过程中,不同的策略会直接影响集群中数据的高可用性和读写性能,所以理解不同的策略的优缺点,是使用好Ignite数据网格集群的第一步。 这篇文章里用到的例子的完整代码和maven工程可以在这里找到。 Server对应的xml配置文件在src/main/resources目录下。
下一篇,我们将继续了解一下Ignite针对不同的冗余策略提供的功能,比如数据分片由于节点失效出现丢失时的行为,primary拷贝和backup拷贝之前的同步等。