# 配置持久化

# 1.Ignite持久化

# 1.1.概述

Ignite持久化,或者说原生持久化,是旨在提供持久化存储的一组功能。启用后,Ignite会将所有数据存储在磁盘上,并将尽可能多的数据加载到内存中进行处理。例如,如果有100个条目,而内存仅能存储20个,则所有100个都存储在磁盘上,而内存中仅缓存20个,以获得更好的性能。

如果关闭原生持久化并且不使用任何外部存储时,Ignite就是一个纯内存存储。

启用持久化后,每个服务端节点只会存储整个数据的一个子集,即只包含分配给该节点的分区(如果启用了备份,也包括备份分区)。

原生持久化基于以下特性:

  • 在磁盘上存储数据分区;
  • 预写日志;
  • 检查点;
  • 变更数据捕获
  • 操作系统交换的使用。

启用持久化后,Ignite会将每个分区存储在磁盘上的单独文件中,分区文件的数据格式与保存在内存中的数据格式相同。如果启用了分区备份,则也会保存在磁盘上,除了数据分区,Ignite还存储索引和元数据。

可以在配置中修改数据文件的默认位置。

# 1.2.启用持久化存储

原生持久化是配置在数据区上的。要启用持久化存储,需要在数据区配置中将persistenceEnabled属性设置为true,可以同时有纯内存数据区和持久化数据区。

以下是如何为默认数据区启用持久化存储的示例:

    # 1.3.配置持久化存储目录

    启用持久化之后,节点就会在{IGNITE_WORK_DIR}/db目录中存储用户的数据、索引和WAL文件,该目录称为存储目录。通过配置DataStorageConfigurationstoragePath属性可以修改存储目录。

    每个节点都会在存储目录下维护一个子目录树,来存储缓存数据、WAL文件和WAL存档文件。

    子目录名 描述
    {WORK_DIR}/db/{consistentId} 该目录中包括了缓存的数据和索引
    {WORK_DIR}/db/wal/{consistentId} 该目录中包括了WAL文件
    {WORK_DIR}/db/wal/archive/{consistentId} 该目录中包括了WAL存档文件

    这里的nodeId要么是节点的一致性ID(如果在节点配置中定义)要么是自动生成的节点ID,它用于确保节点目录的唯一性。如果多个节点共享同一工作目录,则它们将使用不同的子目录。

    如果工作目录包含多个节点的持久化文件(存在多个具有不同nodeId{consistentId}子目录),则该节点将选择第一个未使用的子目录。为了确保节点即使重启也始终使用固定的子目录,即指定数据分区,需要在节点配置中将IgniteConfiguration.setConsistentId设置为集群范围内的唯一值。

    修改存储目录的代码如下所示:

      还可以将WAL和WAL存档路径指向存储目录之外的目录。详细信息后面章节会介绍。

      # 1.4.预写日志

      预写日志是节点上发生的所有数据修改操作(包括删除)的日志。在内存中更新页面时,更新不会直接写入分区文件,而是会附加到WAL的末尾。

      预写日志的目的是为单个节点或整个集群的故障提供一个恢复机制。如果发生故障或重启,则可以依靠WAL的内容将集群恢复到最近成功提交的事务。

      WAL由几个文件(称为活动段)和一个存档组成。活动段按顺序填充,然后循环覆盖。第一个段写满后,其内容将复制到WAL存档中(请参见下面的WAL存档章节)。在复制第一段时,第二段会被视为激活的WAL文件,并接受来自应用端的所有更新,活动段默认有10个。

      # 1.4.1.WAL模式

      WAL模式有几种,每种模式对性能的影响方式不同,并提供不同的一致性保证:

      WAL模式 描述 一致性保证
      FSYNC 保证每个原子写或者事务性提交都会持久化到磁盘。 数据更新不会丢失,不管是任何的操作系统或者进程故障,甚至是电源故障。
      LOG_ONLY 默认模式,对于每个原子写或者事务性提交,保证会刷新到操作系统的缓冲区缓存或者内存映射文件。默认会使用内存映射文件方式,并且可以通过将IGNITE_WAL_MMAP系统属性配置为false将其关闭。 如果仅仅是进程崩溃数据更新会保留。
      BACKGROUND 如果打开了IGNITE_WAL_MMAP属性(默认),该模式的行为类似于LOG_ONLY模式,如果关闭了内存映射文件方式,变更会保持在节点的内部缓冲区,缓冲区刷新到磁盘的频率由walFlushFrequency参数定义。 如果打开了IGNITE_WAL_MMAP属性(默认),该模式提供了与LOG_ONLY模式一样的保证,否则如果进程故障或者其它的故障发生时,最近的数据更新可能丢失。
      NONE WAL被禁用,只有在节点优雅地关闭时,变更才会正常持久化,使用Ignite.cluster().state(ClusterState.INACTIVE)可以冻结集群然后停止节点。 可能出现数据丢失,如果节点在更新操作期间突然终止,则磁盘上存储的数据很可能出现不同步或损坏。

      # 1.4.2.WAL存档

      WAL存档用于保存故障后恢复节点所需的WAL段。存档中保存的段的数量应确保所有段的总大小不超过WAL存档的既定大小。

      WAL存档的最大大小(在磁盘上占用的总空间)定义为检查点缓冲区大小的4倍,可以在配置中更改该值。

      警告

      将WAL存档大小配置为小于默认值可能影响性能,用于生产之前需要进行测试。

      # 1.4.3.修改WAL段大小

      在高负载情况下,默认的WAL段大小(64MB)可能效率不高,因为它会导致WAL过于频繁地在段之间切换,并且切换/轮转是一项昂贵的操作。更大的WAL段大小有助于提高高负载下的性能,但代价是增加WAL文件和WAL存档文件的总大小。

      可以在数据存储配置中更改WAL段文件的大小,该值必须介于512KB和2GB之间。

        # 1.4.4.禁用WAL

        警告

        禁用或启用WAL只能在稳定的拓扑上进行:即所有基线节点都应该在线,在此操作期间不应该有节点加入或离开集群。否则,缓存可能会陷入不一致状态。如果发生这种情况,建议销毁受影响的缓存。

        在某些情况下,禁用WAL以获得更好的性能是合理的做法。例如,在初始数据加载期间禁用WAL并在预加载完成后启用WAL就是个好的做法。

          警告

          如果禁用WAL并重启节点,则将从该节点上的持久化存储中删除所有数据。之所以这样实现,是因为如果没有WAL,则无法保证节点故障或重启时的数据一致性。

          # 1.4.5.WAL存档压缩

          可以启用WAL存档压缩以减少WAL存档占用的空间。WAL存档默认包含最后20个检查点的段(此数字是可配置的)。启用压缩后,则将所有1个检查点之前的已存档段压缩为ZIP格式,如果需要这些段(例如在节点之间再平衡数据),则会将其解压缩为原始格式。

          关于如何启用WAL存档压缩,请参见下面的配置属性章节。

          # 1.4.6.WAL记录压缩

          设计文档中所述,在确认用户操作之前,代表数据更新的物理和逻辑记录已写入WAL文件,Ignite可以先将WAL记录压缩到内存中,然后再写入磁盘以节省空间。

          WAL记录压缩要求引入ignite-compress模块,具体请参见启用模块

          WAL记录压缩默认是禁用的,如果要启用,需要在数据存储配置中设置压缩算法和压缩级别:

          IgniteConfiguration cfg = new IgniteConfiguration();
          
          DataStorageConfiguration dsCfg = new DataStorageConfiguration();
          dsCfg.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
          
          //WAL page compression parameters
          dsCfg.setWalPageCompression(DiskPageCompression.LZ4);
          dsCfg.setWalPageCompressionLevel(8);
          
          cfg.setDataStorageConfiguration(dsCfg);
          Ignite ignite = Ignition.start(cfg);
          

          DiskPageCompression中列出了支持的压缩算法。

          # 1.4.7.禁用WAL存档

          有时可能想要禁用WAL存档,比如减少与将WAL段复制到存档文件有关的开销,当Ignite将数据写入WAL段的速度快于将段复制到存档文件的速度时,这样做就有用,因为这样会导致I/O瓶颈,从而冻结节点的操作,如果遇到了这样的问题,就可以尝试关闭WAL存档。

          通过将WAL路径和WAL存档路径配置为同一个值,可以关闭存档。这时Ignite就不会将段复制到存档文件,而是只是在WAL文件夹中创建新的段。根据WAL存档大小设置,旧段将随着WAL的增长而删除。

          # 1.5.检查点

          检查点是一个将脏页面从内存复制到磁盘上的分区文件的过程,脏页面是指页面已经在内存中进行了更新但是还没有写入对应的分区文件(只是添加到了WAL中)。

          创建检查点后,所有更改都将保存到磁盘,并且在节点故障并重启后将生效。

          检查点和预写日志旨在确保数据的持久化和节点故障时的恢复能力。

          这个过程通过在磁盘上保持页面的最新状态而节省更多的磁盘空间,检查点完成后,就可以在WAL存档中删除检查点执行前创建的WAL段。

          具体请参见相关的文档:

          # 1.6.配置属性

          下表列出了DataStorageConfiguration的主要参数:

          属性名 描述 默认值
          persistenceEnabled 将该属性配置为true可以开启原生持久化。 false
          storagePath 数据存储路径。 ${IGNITE_HOME}/work/db/node{IDX}-{UUID}
          walPath WAL活动段存储路径。 ${IGNITE_HOME}/work/db/wal/
          walArchivePath WAL存档路径。 ${IGNITE_HOME}/work/db/wal/archive/
          walCompactionEnabled 将该属性配置为true可以开启WAL存档压缩 false
          walSegmentSize WAL段文件大小(字节)。 64MB
          walMode 预写日志模式 LOG_ONLY
          walCompactionLevel WAL压缩级别,1表示速度最快,9表示最高的压缩率。 1
          maxWalArchiveSize WAL存档占用空间最大值(字节)。 检查点缓冲区大小的4倍

          # 2.外部存储

          # 2.1.概述

          Ignite可以做为已有数据库之上的一个缓存层,包括RDBMS或者NoSQL数据库,比如Apache Cassandra或者MongoDB等,该场景通过内存计算来对底层数据库进行加速。

          Ignite可以与Apache Cassandra直接集成,但是暂时还不支持其他NoSQL数据库,但是开发自己的CacheStore接口实现

          使用外部存储的两个主要场景是:

          • 作为已有数据库的缓存层,这时可以通过将数据加载到内存来优化处理速度,还可以为不支持SQL的数据库带来SQL支持能力(数据全部加载到内存);
          • 希望将数据持久化到外部数据库(而不是单一的原生持久化)。

          CacheStore接口同时扩展了javax.cache.integration.CacheLoaderjavax.cache.integration.CacheWriter,相对应的分别用于通读通写。也可以单独实现每个接口,然后在缓存配置中单独配置。

          提示

          除了键-值操作,Ignite的通写也支持SQL的INSERT、UPDATE和MERGE,但是SELECT查询语句不会从外部数据库通读数据。

          # 2.1.1.通读和通写

          通读是指如果缓存中不存在,则从底层持久化存储中读取数据。注意这仅适用于通过键-值API进行的get操作,SELECT查询不会从外部数据库查询数据。要执行SELECT查询,必须通过调用loadCache()方法将数据从数据库预加载到缓存中。

          通写是指数据在缓存中更新后会自动持久化。所有的通读和通写操作都参与缓存事务,然后作为整体提交或回滚。

          # 2.1.2.后写缓存

          在一个简单的通写模式中每个缓存的putremove操作都会涉及一个持久化存储的请求,因此整个缓存更新的持续时间可能是相对比较长的。另外,密集的缓存更新频率也会导致非常高的存储负载。

          对于这种情况,可以启用后写模式,它会以异步的方式执行更新操作。这个方式的主要概念是累积更新操作然后作为一个批量异步刷入持久化存储。数据的刷新可以基于时间的事件(数据条目驻留在队列中的时间是有限的)来触发,也可以基于队列大小的事件(如果队列大小达到限值,会被刷新)触发,或者两者(先发生者优先)。

          性能和一致性

          启用后写缓存可以通过异步更新来提高性能,但这可能会导致一致性下降,因为某些更新可能由于节点故障或崩溃而丢失。

          对于后写的方式只有数据的最后一次更新会被写入底层存储。如果键为key1的缓存数据分别被依次更新为值value1value2value3,那么只有(key1,value3)对这一个存储请求会被传播到持久化存储。

          更新性能

          批量的存储操作通常比按顺序的单一操作更有效率,因此可以通过开启后写模式的批量操作来利用这个特性。简单类型(putremove)的简单顺序更新操作可以被组合成一个批量操作。比如,连续地往缓存中写入(key1,value1)(key2,value2)(key3,value3)可以通过一个单一的CacheStore.putAll(...)操作批量处理。

          # 2.2.RDBMS集成

          要将RDBMS作为底层存储,可以使用下面的CacheStore实现之一:

          • CacheJdbcPojoStore:使用反射将对象存储为一组字段,如果在现有数据库之上添加Ignite并希望使用底层表中的部分字段或所有字段,请使用此实现;
          • CacheJdbcBlobStore:将对象以Blob格式存储在底层数据库中,当将外部数据库作为持久化存储并希望以简单格式存储数据时,可以用此实现。

          下面是CacheStore两种实现的配置示例:

          # 2.2.1.CacheJdbcPojoStore

          使用CacheJdbcPojoStore,可以将对象存储为一组字段,并可以配置表列和对象字段之间的映射。

          1. CacheConfiguration.cacheStoreFactory属性设置为org.apache.ignite.cache.store.jdbc.CacheJdbcPojoStoreFactory并提供以下属性:

            • dataSourceBean:数据库连接凭据:URL、用户、密码;
            • dialect:实现与数据库兼容的SQL方言的类。Ignite为MySQL、Oracle、H2、SQLServer和DB2数据库提供了现成的实现。这些方言位于org.apache.ignite.cache.store.jdbc.dialect包中;
            • types:此属性用于定义数据库表和相应的POJO之间的映射(请参见下面的POJO配置示例)。
          2. (可选)如果要在缓存上执行SQL查询,请配置查询实体

          以下示例演示了如何在MySQL表之上配置Ignite缓存。该映射到Person类对象的表有2列:id(INTEGER)name(VARCHAR)

          可以通过XML或Java代码配置CacheJdbcPojoStore

            Person类:

            class Person implements Serializable {
                private static final long serialVersionUID = 0L;
            
                private int id;
            
                private String name;
            
                public Person() {
                }
            
                public String getName() {
                    return name;
                }
            
                public void setName(String name) {
                    this.name = name;
                }
            
                public int getId() {
                    return id;
                }
            
                public void setId(int id) {
                    this.id = id;
                }
            }
            

            # 2.2.2.CacheJdbcBlobStore

            CacheJdbcBlobStore将对象以Blob格式存储于底层数据库中,它会创建一张表名为ENTRIES,有名为keyval的列(类型都为binary)。

            可以通过提供自定义的建表语句和DML语句,分别用于加载、更新、删除数据来修改默认的定义,具体请参见CacheJdbcBlobStore的javadoc。

            在下面的示例中,Person类的对象存储于单一列的字节数组中。

              # 2.3.加载数据

              缓存存储配置完成并启动集群后,就可以使用下面的代码从数据库加载数据了:

              // Load data from person table into PersonCache.
              IgniteCache<Integer, Person> personCache = ignite.cache("PersonCache");
              
              personCache.loadCache(null);
              

              # 2.4.NoSQL数据库集成

              通过实现CacheStore接口,可以将Ignite与任何NoSQL数据库集成。

              警告

              虽然Ignite支持分布式事务,但是并不会使NoSQL数据库具有事务性,除非数据库本身直接支持事务。

              # 2.4.1.Cassandra集成

              Ignite通过CacheStore实现,直接支持将Apache Cassandra用作持久化存储。其利用Cassandra的异步查询来提供loadAll()writeAll()deleteAll()等高性能批处理操作,并自动在Cassandra中创建所有必要的表和命名空间。

              具体请参见Cassandra集成章节的介绍。

              # 3.交换空间

              # 3.1.概述

              如果使用纯内存存储,随着数据量的大小逐步达到物理内存大小,可能导致内存溢出。如果不想使用原生持久化或者外部存储,还可以开启交换,这时Ignite会将内存中的数据移动到磁盘上的交换空间,注意Ignite不会提供自己的交换空间实现,而是利用了操作系统(OS)提供的交换功能。

              打开交换空间之后,Ignite会将数据存储在内存映射文件(MMF)中,操作系统会根据内存使用情况,将其内容交换到磁盘,但是这时数据访问的性能会下降。另外,还没有数据持久性保证,这意味着交换空间中的数据只在节点在线期间才可用。一旦存在交换空间的节点停止,所有数据都会丢失。因此,应该将交换空间作为内存的扩展,以留出足够的时间向集群中添加更多的节点让数据重新分布,并避免集群未及时扩容导致内存溢出的错误(OOM)发生。

              注意

              虽然交换空间位于磁盘上,但是其不能替代原生持久化。交换空间中的数据只有在节点在线时才有效,一旦节点关闭,数据将丢失。为了确保数据一直可用,应该启用原生持久化或使用外部存储

              # 3.2.启用交换

              数据区的maxSize定义了区域的整体最大值,如果数据量达到了maxSize,然后既没有使用原生持久化,也没有使用外部存储,那么就会抛出内存溢出异常。使用交换可以避免这种情况的发生,做法是:

              • 配置maxSize的值大于内存大小,这时操作系统就会使用交换;
              • 启用数据区的交换,如下所示。

                # 4.实现自定义CacheStore

                可以实现自己的自定义CacheStore并将其作为缓存的底层数据存储,IgniteCache中读写数据的方法将会调用CacheStore实现中相应的方法。

                下表描述了CacheStore接口中的方法:

                方法 描述
                loadCache() 调用IgniteCache.loadCache(…​)时,就会调用该方法,通常用于从数据库预加载数据。此方法在驻有缓存的所有节点上执行,要加载单个节点的数据,需要在该节点上调用IgniteCache.localLoadCache()方法。
                load()write()delete() 当调用IgniteCache接口的get()put()remove()方法时,会分别调用这3个方法,这些方法用于单条数据的通读通写
                loadAll()writeAll()deleteAll() 当调用IgniteCache接口的getAll()putAll()removeAll()方法时,会分别调用这3个方法,这些方法用于处理多条数据的通读通写,通常以批量的形式实现以提高性能。

                # 4.1.CacheStoreAdapter

                CacheStoreAdapterCacheStore的扩展,提供了批量操作的默认实现,如loadAll(Iterable)writeAll(Collection)deleteAll(Collection),其会迭代所有条目并在每个条目上调用对应的load()write()delete()方法。

                # 4.2.CacheStoreSession

                CacheStoreSession用于持有多个操作之间的上下文,主要用于提供事务支持。一个事务中的多个操作是在同一个数据库连接中执行的,并在事务提交时提交该连接。通过@GridCacheStoreSessionResource注解可以将其注入CacheStore实现中。

                关于如何实现事务化的CacheStore,可以参见GitHub上的示例

                # 4.3.示例

                下面是一个CacheStore的非事务化实现的示例:

                public class CacheJdbcPersonStore extends CacheStoreAdapter<Long, Person> {
                    // This method is called whenever the "get(...)" methods are called on IgniteCache.
                    @Override
                    public Person load(Long key) {
                        try (Connection conn = connection()) {
                            try (PreparedStatement st = conn.prepareStatement("select * from PERSON where id=?")) {
                                st.setLong(1, key);
                
                                ResultSet rs = st.executeQuery();
                
                                return rs.next() ? new Person(rs.getInt(1), rs.getString(2)) : null;
                            }
                        } catch (SQLException e) {
                            throw new CacheLoaderException("Failed to load: " + key, e);
                        }
                    }
                
                    @Override
                    public void write(Entry<? extends Long, ? extends Person> entry) throws CacheWriterException {
                        try (Connection conn = connection()) {
                            // Syntax of MERGE statement is database specific and should be adopted for your database.
                            // If your database does not support MERGE statement then use sequentially
                            // update, insert statements.
                            try (PreparedStatement st = conn.prepareStatement("merge into PERSON (id, name) key (id) VALUES (?, ?)")) {
                                Person val = entry.getValue();
                
                                st.setLong(1, entry.getKey());
                                st.setString(2, val.getName());
                
                                st.executeUpdate();
                            }
                        } catch (SQLException e) {
                            throw new CacheWriterException("Failed to write entry (" + entry + ")", e);
                        }
                    }
                
                    // This method is called whenever the "remove(...)" method are called on IgniteCache.
                    @Override
                    public void delete(Object key) {
                        try (Connection conn = connection()) {
                            try (PreparedStatement st = conn.prepareStatement("delete from PERSON where id=?")) {
                                st.setLong(1, (Long) key);
                
                                st.executeUpdate();
                            }
                        } catch (SQLException e) {
                            throw new CacheWriterException("Failed to delete: " + key, e);
                        }
                    }
                
                    // This method is called whenever the "loadCache()" and "localLoadCache()"
                    // methods are called on IgniteCache. It is used for bulk-loading the cache.
                    // If you don't need to bulk-load the cache, skip this method.
                    @Override
                    public void loadCache(IgniteBiInClosure<Long, Person> clo, Object... args) {
                        if (args == null || args.length == 0 || args[0] == null)
                            throw new CacheLoaderException("Expected entry count parameter is not provided.");
                
                        final int entryCnt = (Integer) args[0];
                
                        try (Connection conn = connection()) {
                            try (PreparedStatement st = conn.prepareStatement("select * from PERSON")) {
                                try (ResultSet rs = st.executeQuery()) {
                                    int cnt = 0;
                
                                    while (cnt < entryCnt && rs.next()) {
                                        Person person = new Person(rs.getInt(1), rs.getString(2));
                                        clo.apply(person.getId(), person);
                                        cnt++;
                                    }
                                }
                            }
                        } catch (SQLException e) {
                            throw new CacheLoaderException("Failed to load values from cache store.", e);
                        }
                    }
                
                    // Open JDBC connection.
                    private Connection connection() throws SQLException {
                        // Open connection to your RDBMS systems (Oracle, MySQL, Postgres, DB2, Microsoft SQL, etc.)
                        Connection conn = DriverManager.getConnection("jdbc:mysql://[host]:[port]/[database]", "YOUR_USER_NAME", "YOUR_PASSWORD");
                
                        conn.setAutoCommit(true);
                
                        return conn;
                    }
                }
                

                # 5.快照目录

                # 5.1.配置快照目录

                快照的一段默认存储在相应Ignite节点的工作目录中。该段使用与Ignite持久化保存数据、索引、WAL和其他文件相同的存储介质。由于快照会占用与持久性文件已经占用的空间一样多的空间,并且会通过与Ignite持久化进程共享磁盘I/O来影响应用的性能,因此建议将快照和持久化文件存储在不同的介质上。

                可以通过更改持久化文件的存储目录或覆盖默认快照的位置来避免Ignite原生持久化和快照之间的这种干扰,如下所示:

                  # 6.磁盘压缩

                  磁盘压缩是指将数据页面写入磁盘时对其进行压缩的过程,以减小磁盘空间占用。这些页面在内存中是不压缩的,但是当将数据刷新到磁盘时,将使用配置的算法对其进行压缩。这仅适用于开启原生持久化的数据页面并且不会压缩索引或WAL记录的数据页,WAL记录压缩是可以单独启用的。

                  磁盘页面压缩是在每个缓存的配置中设定的,缓存必须在持久化的数据区中。目前没有选项可全局启用磁盘页面压缩,此外,还必须必须满足以下的条件:

                  • 将持久化配置中的pageSize属性设置为文件系统页面大小的至少2倍,这意味着页面大小必须为8K或16K;
                  • 启用ignite-compress模块。

                  要为某个缓存启用磁盘页面压缩,需要在缓存配置中提供一种可用的压缩算法,如下所示:

                    # 6.1.支持的算法

                    支持的压缩算法包括:

                    • ZSTD:支持从-131072到22的压缩级别(默认值:3);
                    • LZ4:支持从0到17的压缩级别(默认值:0);
                    • SNAPPY
                    • SKIP_GARBAGE:该算法仅从半填充页面中提取有用的数据,而不压缩数据。

                    # 7.持久化调优

                    本章节总结了Ignite原生持久化调优的最佳实践。

                    # 7.1.调整页面大小

                    Ignite的页面大小(DataStorageConfiguration.pageSize)不要小于存储设备(SSD、闪存、HDD等)的页面大小以及操作系统缓存页面的大小,默认值为4KB。

                    操作系统的缓存页面大小很容易就可以通过系统工具和参数获取到。

                    存储设备比如SSD的页面大小可以在设备的说明上找到,如果厂商未提供这些信息,可以运行SSD的基准测试来算出这个数值,如果还是难以拿到这个数值,可以使用4KB作为Ignite的页面大小。很多厂商为了适应4KB的随机写工作负载不得不调整驱动,因为很多标准基准测试都是默认使用4KB,来自英特尔的白皮书也确认4KB足够了。

                    选定最优值之后,可以将其用于集群的配置:

                      # 7.2.单独保存WAL

                      考虑为数据文件以及预写日志(WAL)使用单独的磁盘设备。Ignite会主动地写入数据文件以及WAL文件,下面的示例会显示如何为数据存储、WAL以及WAL存档配置单独的路径:

                        # 7.3.增加WAL段大小

                        WAL段的默认大小(64MB)在高负载情况下可能是低效的,因为它导致WAL在段之间频繁切换,并且切换/轮转是昂贵的操作。将段大小设置为较大的值(最多2GB)可能有助于减少切换操作的次数,不过这将增加预写日志的占用空间。

                        具体请参见修改WAL段大小

                        # 7.4.调整WAL模式

                        考虑其它WAL模式替代默认模式。每种模式在节点故障时提供不同程度的可靠性,并且可靠性与速度成反比,即,WAL模式越可靠,则速度越慢。因此,如果具体业务不需要高可靠性,那么可以切换到可靠性较低的模式。

                        具体可以看WAL模式的相关内容。

                        # 7.5.禁用WAL

                        有时禁用WAL也会改进性能。

                        # 7.6.页面写入限流

                        Ignite会定期地启动检查点进程,以在内存和磁盘间同步脏页面。脏页面是已在内存中更新但是还未写入对应的分区文件的页面(更新只是添加到了WAL)。这个进程在后台进行,对应用没有影响。

                        但是,如果计划进行检查点的脏页面在写入磁盘前被更新,它之前的状态会被复制进某个区域,叫做检查点缓冲区。如果这个缓冲区溢出,那么在检查点处理过程中,Ignite会停止所有的更新。因此,写入性能可能降为0,直至检查点过程完成,如下图所示:

                        当检查点处理正在进行中时,如果脏页面数达到阈值,同样的情况也会发生,这会使Ignite强制安排一个新的检查点执行,并停止所有的更新操作直到第一个检查点执行完成。

                        当磁盘较慢或者更新过于频繁时,这两种情况都会发生,要减少或者防止这样的性能下降,可以考虑启用页面写入限流算法。这个算法会在检查点缓冲区填充过快或者脏页面占比过高时,将更新操作的性能降低到磁盘的速度。

                        页面写入限流剖析

                        要了解更多的信息,可以看相关的Wiki页面

                        下面的示例显示了如何开启页面写入限流:

                          # 7.7.调整检查点缓冲区大小

                          前述章节中描述的检查点缓冲区大小,是检查点处理的触发器之一。

                          缓冲区的默认大小是根据数据区大小计算的。

                          数据区大小 默认检查点缓冲区大小
                          < 1GB MIN (256 MB, 数据区大小)
                          1GB ~ 8GB 数据区大小/4
                          > 8GB 2GB

                          默认的缓冲区大小并没有为写密集型应用进行优化,因为在大小接近标称值时,页面写入限流算法会降低写入的性能,因此在正在进行检查点处理时还希望保持写入性能,可以考虑增加DataRegionConfiguration.checkpointPageBufferSize,并且开启写入限流来阻止性能的下降:

                            在上例中,默认数据区的检查点缓冲区大小配置为1GB。

                            # 7.8.启用直接I/O

                            通常当应用访问磁盘上的数据时,操作系统拿到数据后会将其写入一个文件缓冲区缓存,写操作也是同样,操作系统首先将数据写入缓存,然后才会传输到磁盘,要消除这个过程,可以打开直接IO,这时数据会忽略文件缓冲区缓存,直接从磁盘进行读写。

                            Ignite中的直接I/O插件用于加速检查点进程,它的作用是将内存中的脏页面写入磁盘,建议将直接IO插件用于写密集型负载环境中。

                            注意

                            注意,无法专门为WAL文件开启直接I/O,但是开启直接I/O可以为WAL文件带来一点好处,就是WAL数据不会在操作系统的缓冲区缓存中存储过长时间,它会在下一次页面缓存扫描中被刷新(依赖于WAL模式),然后从页面缓存中删除。

                            要启用直接I/O插件,需要在二进制包中将{IGNITE_HOME}/libs/optional/ignite-direct-io文件夹上移一层至libs/optional/ignite-direct-io文件夹,或者也可以作为一个Maven构件引入,具体请参见这里的介绍。

                            通过IGNITE_DIRECT_IO_ENABLED系统属性,也可以在运行时启用/禁用该插件。

                            相关的Wiki页面有更多的细节。

                            # 7.9.购买产品级SSD

                            限于SSD的操作特性,在经历几个小时的高强度写入负载之后,Ignite原生持久化的性能可能会下降,因此需要考虑购买快速的产品级SSD来保证高性能,或者切换到非易失性内存设备比如Intel Optane持久化内存。

                            # 7.10.SSD预留空间

                            由于SSD预留空间的原因,50%使用率的磁盘的随机写性能要好于90%使用率的磁盘,因此需要考虑购买高预留空间比率的SSD,然后还要确保厂商能提供工具来进行相关的调整。

                            Intel 3D XPoint

                            考虑使用3D XPoint驱动器代替常规SSD,以避免由SSD级别上的低预留空间设置和恒定垃圾收集造成的瓶颈。具体可以看这里

                            # 8.变更数据捕获

                            # 8.1.概述

                            变更数据捕获(CDC)是一种数据处理模式,用于异步接收本地节点上已更新的条目,以便在变更的数据上执行某些动作。

                            警告

                            CDC是一个实验性特性,API和架构设计未来可能发生变更。

                            下面是CDC的一些使用场景:

                            • 数据仓库的流式更新;
                            • 更新检索索引;
                            • 计算统计数据(流式查询);
                            • 审计日志;
                            • 与外部系统的异步交互:审核、业务流程调用等。

                            Ignite通过ignite-cdc.sh应用和Java API实现了CDC。

                            下图展示了CDC应用如何与Ignite节点的WAL存档段集成:

                            CDC启用之后,Ignite服务端节点会在特定的db/cdc/{consistency_id}目录中创建指向每个WAL存档段的硬链接。ignite-cdc.sh应用在不同的JVM上运行并处理新归档的WAL段。当某个段被ignite-cdc.sh完全处理过后,会被删除。删除两个链接(存档和CDC)后,实际磁盘空间会被释放。

                            消费状态是指向最后处理的事件的指针。消费者可以告诉ignite-cdc.sh保存消费状态。启动时事件处理将从上次保存的状态继续。

                            # 8.2.配置

                            # 8.2.1.Ignite节点

                            属性名 描述 默认值
                            DataRegionConfiguration#cdcEnabled 服务端节点的CDC启用标志 false
                            DataStorageConfiguration#cdcWalPath CDC WAL目录路径 db/wal/cdc
                            DataStorageConfiguration#walForceArchiveTimeout 即使未完成也强制存档WAL段的超时时间 -1,禁用

                            # 8.2.2.CDC应用

                            CDC与Ignite节点是同样的配置方式,即通过Spring的配置文件。

                            • ignite-cdc.sh:需要Ignite和CDC的配置才能启动;
                            • IgniteConfiguration:用于确定公共的选项,比如CDC目录的路径、节点的一致性ID和其他的一些参数;
                            • CdcConfiguration:包含ignite-cdc.sh特有的一些参数。
                            属性名 描述 默认值
                            lockTimeout 启动时CDC锁定目录的锁等待超时时间,确保ignite-cdc.sh不会并发地处理相同的目录 10000毫秒
                            checkFrequency 当没有新文件可用时,应用在后续检查之间休眠的时间量 10000毫秒
                            keepBinary 二进制标志,即变更数据的键和值是否以二进制形式提供 true
                            consumer org.apache.ignite.cdc.CdcConsumer接口的实现,用于处理变更数据 null
                            metricExporterSpi 导出CDC指标的SPI数组,具体看指标的相关文档 null

                            # 8.2.3.全局参数

                            下表列出的全局参数可以在运行时对CDC进行配置:

                            cdc.disabled 禁用集群的CDC可以避免磁盘过载,注意禁用CDC可能导致缓存变更的丢失,如果CDC程序需要停用很长时间,这个做法会很有用。 false

                            # 8.3.API

                            org.apache.ignite.cdc.CdcEvent

                            下面是CdcEvent反映的单个数据变更:

                            方法名 描述
                            key() 变更数据的主键
                            value() 变更数据的值,如果事件对应的是删除操作,该方法返回null
                            cacheId() 触发变更的缓存ID,该值等于SYS.CACHESCACHE_ID
                            partition() 变更数据所属的分区
                            primary() 用于区分变更发生在主节点还是备份节点的标志
                            version() 变更数据可比较的版本号,在内部,Ignite会为每个条目维护一个有序的版本号,因此可以对相同条目的任意变更进行排序

                            org.apache.ignite.cdc.CdcConsumer

                            变更事件的消费者,开发者应实现该接口。

                            方法名 描述
                            void start(MetricRegistry) CDC应用启动的时候会调用一次,MetricRegistry可用于导出消费者特有的指标
                            boolean onEvents(Iterator<CdcEvent> events) 处理变更的主方法,当该方法返回true,状态会被保存到磁盘,状态指向最后读取事件的下一个,如果发生故障,消费将从上次保存的状态继续
                            void stop() CDC应用停止时会调用一次

                            # 8.4.指标

                            ignite-cdc.sh使用和Ignite节点相同SPI来导出指标,应用提供了下面的指标(消费者可提供其他的指标)。

                            指标名 描述
                            CurrentSegmentIndex 当前正在处理的WAL段的序号
                            CommittedSegmentIndex 包含最后已提交状态的WAL段的序号
                            CommittedSegmentOffset WAL段内已提交的偏移量(字节)
                            LastSegmentConsumptionTime 最后一次段开始处理的时间戳(毫秒)
                            BinaryMetaDir 应用读取的二进制元数据目录
                            MarshallerDir 应用读取的编组器目录
                            CdcDir 应用读取的CDC目录

                            # 8.5.日志

                            ignite-cdc.sh与Ignite节点使用相同的日志配置,唯一的区别是日志会计入ignite-cdc.log文件。

                            # 8.6.生命周期

                            提示

                            ignite-cdc.sh实现了Fail-Fast(快速失败)方法,任何错误都会导致故障,应配置其他的工具(比如OS)来快速重启该应用。

                            1. 通过IgniteConfiguration拿到所需的共享目录;
                            2. 锁定CDC目录;
                            3. 加载保存的状态;
                            4. 开始消费;
                            5. 无限等待新的可用段并处理之;
                            6. 在发生故障或者收到停止信号后停止消费。

                            # 8.7.处理被忽略的段

                            CDC可以通过手动或者配置目录的大小上限来禁用,这时会忽略硬链接的创建。

                            警告

                            被忽略的段中的变更都会丢失!

                            CDC被禁用期间段之间会出现空档,比如0000000000000002.wal0000000000000010.wal0000000000000011.wal。这时ignite-cdc.sh程序会发生故障,错误信息大致是:Found missed segments. Some events are missed. Exiting! [lastSegment=2, nextSegment=10]

                            提示

                            在重启CDC程序之前,需要进行数据同步,同步数据可以使用resend命令,快照或者其他的方法。

                            要解决这个错误,可以通过控制脚本执行下面的命令:

                            # Delete lost segment CDC links in the cluster.
                            control.sh|bat --cdc delete_lost_segment_links
                            
                            # Delete lost segment CDC links on a node.
                            control.sh|bat --cdc delete_lost_segment_links --node-id node_id
                            

                            该命令会删除最后一个空挡之前的所有段链接。

                            比如,CDC被禁用了若干次:000000000000002.wal000000000000003.wal000000000000008.wal0000000000000010.wal0000000000000011.wal,执行该命令之后,下面的段链接会被删除:000000000000002.wal000000000000003.wal000000000000008.wal,然后CDC程序启动后会从0000000000000010.wal开始然后继续。

                            # 8.8.强制向CDC重新发送全部缓存数据

                            当CDC程序停用一段时间之后,这段时间内的缓存更新会被忽略,这时是有必要重新发送已有的缓存数据的。比如在复制重新开始之前,确认缓存数据的一致性,非常重要。

                            提示

                            如果集群还没有被平衡,或者拓扑已被变更(节点加入/离开,基线变更等),该命令会被取消。

                            如果要向CDC程序重新发送所有的缓存数据,可以通过控制脚本执行下面的命令:

                            # Forcefully resend all cache data to CDC. Iterates over caches and writes primary copies of data entries to the WAL to get captured by CDC:
                            control.sh|bat --cdc resend --caches cache1,...,cacheN
                            

                            该命令会在缓存中迭代,然后将数据条目的主拷贝写入WAL,已被CDC程序捕获。

                            提示

                            无法保证缓存的并发更新会通知到CDC消费者,可以使用CdcEvent#version来解决版本问题。

                            # 8.9.cdc-ext

                            Ignite的扩展项目有一个cdc-ext模块,其提供了基于CDC进行跨集群数据复制的两种方式,具体文档请参见变更数据捕获

                            18624049226

                            最后更新时间:: 11/25/2023, 3:51:28 PM