为什么逐出数据?
如果有多于JVM内存的数据想放入Region,数据逐出是可能使用的一种解决方案。 一种备选方案是对数据进行分区。然而分区Region也可能无法将所有数据放入内存,所以有可能需要对分区Region采用数据逐出。 另一种备选方案是让数据过一定期限后从内存删除,这是基于时间而不是空间的一种方案。
数据逐出如何工作?
当使用数据逐出时需要决定所采用的算法和动作。 算法规定了需要检查的所耗费资源的最大值,可为基于条目数量、内存消耗字节数和可用堆消耗百分比的LRU(最近最少使用)算法。
- 条目数量和绝对内存消耗量完全由GemFire逐出控制器基于Region级别进行管理。EntryLRU是最简单的算法,适用于每个条目消耗相同大小的内存。Memory LRU则适用于每个条目消耗不同大小的内存。
- Heap消耗百分比由GemFire逐出控制器基于缓存级别进行管理。最大值设置在管理器配置的缓存下配置。当管理器断定需要进行数据逐出时,它命令逐出控制器对所有逐出算法设为lru-heap-percentage的region采取数据逐出,直到管理器停止这一命令。注意的是,当其他非LRU资源甚至非GemFire缓存消耗内存时,这一算法也会导致HeapLRUregion的数据逐出。
对于一个region,数据逐出操作会逐出最近最少使用到的条目。几乎所有操作(包括读写)都认为是对条目的使用,除了以下的操作:
- Region.containsKey
- Region.containsValue
- Region.getEntry
当使用MemoryLRU或HeapLRU算法时需要实现ObjectSizer接口。这让GemFire可以调用自己的代码来计算条目的字节大小。让条目大小估算精确很重要,但同时需要注意的是复杂ObjectSize实现会花费较长时间并导致性能下降。如果条目的所有值都是String或byte[]类型,GemFire会自动计算内存大小,无须实现ObjectSizer。
分区Region在数据逐出的不同之处
对于分区region,基于条目数量和内存消耗量的逐出行为当节点数据超过本地缓存主副本和冗余副本组合的限制后发生。对于基于堆消耗百分比的逐出行为由管理器驱动。 因为维护整个分区region或者同一节点所有桶(bucket)的LRU条目信息代价太大,GemFire是基于桶来维护LRU条目信息的。此外,对分区region的所有桶施行数据组出会导致数据分布失衡。 因此,对分区region进行的数据逐出可能会保留相对本地节点其他桶或其他分布系统节点相对更老的条目。它可能在主副本中保留条目而在第二副本中逐出条目,或者相反。 LRU逐出对每个桶单独进行:
- 对基于内存和条目数的数据逐出,LRU逐出在操作新条目时有可能执行,直到Region的桶组合整体内存下降到门限下结束。对于内存逐出,分区region最大内存门限会忽略lru-memory-size设置,始终是local-max-memory。
- 对于基于堆的数据逐出,每个分区region桶被当作单独region来处理,每个逐出动作仅考虑桶内的LRU,而不是整体分区region。
动作为本地删除条目的数据逐出无法用于复制region,因为不允许对复制分区进行本地写操作,这会违反所有数据在复制分区都可见的契约。如果需要使用本地删除条目的数据逐出,可以考虑使用预加载数据策略,其行为在初始化时与复制分区相同并允许动作为本地删除条目的数据逐出。
数据无持久化 | 数据持久化 | |
---|---|---|
EvictionAction.NONE | 条目将在内存中一直保存。 | 条目将在内存和磁盘中一直保存。 |
EvictionAction.LOCAL_DESTROY | 条目(键和值两部分)将从内存中释放。仅当被逐出数据可从外部数据源加载时可用。 | |
EvictionAction.OVERFLOW_TO_DISK | 条目将被逐出到磁盘但是不会持久化 (当缓存关闭时磁盘文件将被删除),条目的键部分始终在内存中保存。 | 条目(键和值两部分)一直在磁盘中保存。条目的值部分将被逐出,键部分始终在内存中保存。 |
磁盘存储文件名和扩展名
磁盘存储文件包括存储管理、访问控制文件和操作日志(oplog,记录了删除和其他所有操作)。下面的表描述了文件名和扩展名及示例。
文件名
文件名包括三部分:
第一部分: 使用标识
值 | 用途 | 示例 |
---|---|---|
OVERFLOW | 仅为溢出region和队列的操作日志数据。 | OVERFLOWoverflowDS1_1.crf |
BACKUP | 持久化、持久化+溢出rgion和队列操作日志数据。 | BACKUPoverflowDS1.if, BACKUPDEFAULT.if |
DRLK_IF | 访问控制 - 对磁盘存储上锁。 | DRLK_IFoverflowDS1.lk, DRLK_IFDEFAULT.lk |
第二部分: 磁盘存储名
值 | 用途 | 示例 |
---|---|---|
<磁盘存储名> | 非默认磁盘存储。 | name=“overflowDS1” DRLK_IFoverflowDS1.lk, name=“persistDS1” BACKUPpersistDS1_1.crf |
DEFAULT | 默认磁盘存储名,当对region或队列指定持久化或溢出但没有命名磁盘存储时使用。 | DRLK_IFDEFAULT.lk, BACKUPDEFAULT_1.crf |
第三部分: 操作日志序列号
值 | 用途 | 示例 |
---|---|---|
序列号格式为_n | 仅用于操作日志。编码从1开始。 | OVERFLOWoverflowDS1_1.crf, BACKUPpersistDS1_2.crf, BACKUPpersistDS1_3.crf |
文件扩展名
文件扩展名 | 用途 | 注释 |
---|---|---|
if | 数据存储元数据 | 存放在存储所列的第一个目录。文件很小可以忽略不计-在文件大小控制中不考虑。 |
lk | 磁盘存储访问控制 | 存放在存储所列的第一个目录。文件很小可以忽略不计-在文件大小控制中不考虑。 |
crf | Oplog: 创建、更新和“使无效”操作 | 在创建时其文件大小预分配为max-oplog-size的90%。 |
drf | Oplog: 删除操作 | 在创建时其文件大小预分配为max-oplog-size的90%。 |
krf | Oplog: 键和crf偏移量信息 | 当操作日志文件大小达到max-oplog-size后创建。用于增强启动时的性能。 |
数据持久化
GemFire在持久化配置后,确保放入region的所有数据将写入磁盘,以用于下次创建region时恢复数据。这让机器或进程故障后或按特定次序重启GemFire后,数据可以恢复。 GemFire缓存可以驻留多个region,任何region都可以设为持久化。与传统数据库管理系统不同,应用设计师使用GemFire可以考虑哪些数据集放入内存和哪些放入磁盘,数据在任何时候在分布系统中应该有多少可用数据副本。这样的颗粒度控制可以让应用设计师在基于内存的性能和基于磁盘的持久性之间进行权衡。 GemFire使用无共享磁盘存储模型。任意两个缓存节点在写操作时不会共享磁盘文件。这让GemFire应用可以部署在商品级硬件中并获得高的吞吐量。 所有对oplog文件的持久化写操作都是通过向oplog文件附加完成的。如果配置了同步持久化则在写操作完成前将从附加内容从JVM堆刷新到文件系统缓冲区。为了提供更好的性能,不会完全刷新到磁盘。通过使用GemFire复制,多个数据副本保留在内存中,所以完全刷新到磁盘对大多数用例而言是不需要的。 对于同步持久化的一个用例是当存储在GemFire的数据集在其他地方没有被管理(至少在一段时间内)。例如,在金融交易应用中,来自客户的订单以高于数据库可以处理的速度到达,GemFire是管理数据持久性的唯一数据存储库。数据可能会被复制到数据仓库,但多个应用需要数据集在数据中心任何时候都可用。应此,将数据同步持久化到分布系统中至少一个节点的磁盘上是有意义的。 持久化也可被配置为异步的。在此模式下,任何数据变动都会缓冲到内存,知道可被写入磁盘。这意味着当系统崩溃时数量可配置的数据可能会丢失,但是对于可以容忍数据丢失的应用可以带来更大的性能。对于异步持久化的一个用例是当GemFire被用于会话状态管理。上千用户的会话状态可能变化非常快,所以需要异步写提高速度。
持久化如何工作?
当持久化region被创建后,它会检查持久化文件是否在配置的磁盘目录中存在以用于数据恢复。如果没有发现存在持久化文件,它会创建新的;否则,通过这些文件的数据初始化region的内容。一旦恢复过程结束,region就已经被创建并可被应用和客户端使用。任何对region的写操作都会将条目写入磁盘。 条目写入oplog,oplog包含所有对缓存的逻辑操作。每个跟新会附加到当前oplog的尾部。在某一时刻,oplog会被认为已满,新的oplog会被创建。对oplog的更新可以同步或异步完成。
使用的磁盘和目录
默认情况下,JVM当前目录会用于存放持久化数据。可以配置不同磁盘下的多个目录,这可以突破文件系统空间限制并带来性能优化。
Oplog创建
当oplog被创建,会试图在磁盘上预留max-oplog-size90%的文件大小。这仅仅是设置oplog的文件大小而不是真正写入内容。如果预留请求失败,会记录告警日志并尝试以没有预留空间的方式创建oplog文件。告警提示用户磁盘空间将满。 max-oplog-size_是以兆字节为单位的oplog文件最大长度。如果长度小于_max-oplog-size,操作可以写入;否则会创建新的oplog。默认为1GB。 forceRoll方法会强制创建新的oplog文件。这让引用有机会强制GemFire考虑当前oplog长度达到最大值。注意的是该方法未必会真正导致oplog文件滚动,因为这取决于系统参数roll-oplogs是否设为true。 通过设置max-oplog-size足够大以防止当前oplog在使用高峰时变满,防止滚动发生可以增加数据处理吞吐量。一旦使用高峰结束,可以调用forceRoll()来触发滚动。
持久化region恢复
每次启动JVM,都必须通过cache.xml或JavaAPI重新创建持久化region。当持久化region被创建,就会对磁盘上已有的持久化文件进行检查。如果找到文件,则用来进行数据恢复。然而在此之前会检查是否有含有此region的其他GemFire节点存在。如果有这样的节点存在,由于其他节点可能含有比磁盘上更新的数据,就会试图从其他节点获得初始数据进行恢复。原有持久化文件会被备份,并在使用其他节点初始数据恢复成功后删除备份的持久化数据,或者在使用其他节点初始数据恢复失败后用于数据恢复。 默认下条目仅含有从磁盘发序列化获得的键对象并指导如何从磁盘找到值对象。但是值对象在数据恢复阶段并不放入内存,会在应用读取条目时懒惰加载入内存。 如果期望所有的值在数据恢复期间加载到内存,需要设置系统属性gemfire.disk.recoverValues为true。这会降低region创建速度但会给应用读数据带来更好的性能。另一个优点是region创建时会分配所需的全部堆内存,因此在缓存创建时可知是否给JVM提供了足够多的堆。如果同时配置了溢出磁盘,最好在数据恢复时不加载值,除非有很高百分比的数据能装入内存。
何时数据写入磁盘?
当任何在持久化region上的写操作完成后,数据会写入磁盘。下边描述了region写操作:
写操作 | 写入的数据 | 方法 |
---|---|---|
条目创建 | 一个包含键、值和条目id的oplog记录 | create, put, putAll, get due toload, region creation due to initialization frompeer |
条目更新 | 一个包含新值和条目id的oplog记录 | put(), putAll(), invalidate(), localInvalidate(),Entry.setValue() |
条目删除 | 一个条目id的oplog记录 | remove(), destroy(), localDestroy() |
region关闭 | 关闭所有文件但是保留在磁盘 | close(), Cache.close() |
region删除 | 关闭并从磁盘删除所有文件 | destroyRegion(), localDestroyRegion() |
region清空 | 从磁盘删除所有文件并创建新的空文件 | clear(), localClear() |
region使无效 | 对每个条目付为null的新值 | invalidateRegion(), localInvalidateRegion() |
即使配置了同步磁盘写,GemFire仅同步写入文件系统缓冲区,而不是磁盘本身。这意味着在文件系统缓冲区的数据有可能在机器崩溃时无法写入磁盘。但这能让持久化region所在的JVM崩溃时数据得以保护。
注意 可以配置为同步oplog写刷新到磁盘,但是通常会造成显著性能下降。如果使用告诉磁盘或固态内存,可以通过设置系统参数gemfire。syncWrites为true让同步oplog写刷新到磁盘,该参数仅在启动GemFire节点之前可改。
异步写
使用异步写可以获得更好的性能,其代价是使用更多的内存(用于缓冲)和写操作完成后数据仍存在内存而非写入磁盘的风险。 不是想同步写那样立即附加到当前oplog文件,异步写将当前操作加入异步缓冲。当缓冲区满(基于_bytes-threshold_设置)或当时间超时(基于_time-interval_设置),或者当强制刷新(通过调用DiskStore.flush),将会将异步缓冲的内容刷新到当前oplog文件。 当操作加入异步缓冲区,有可能会对内存上的更新操作进行合并。例如,异步缓冲区已经包含了对键为X的条目的创建,此后完成了对键为X的条目的删除,则缓冲区在刷新到磁盘时无需操作。如果在异步缓冲区刷新到磁盘之前有五次对键为X的条目的更改,则缓冲区中最新的修改需要写入磁盘。 DiskStore.flush()方法强制将异步缓冲区刷新到当前oplog。这允许应用在DiskStore.flush()调用前的异步缓冲区内容已被移出JVM内存并放入文件系统缓冲区。
业务数据如何被写入磁盘
持久化region将每个条目的键和值进行序列化,并写入磁盘。
Oplog滚动(Rolling)
当操作日志满,GemFire会自动关闭日志并创建一个带有下一个序列号的新日志。这叫做oplog滚动。也可以调用APIDiskStore.forceRoll来请求oplog滚动。可在压缩磁盘存储之前进行调用,这样最新的oplog也可以被压缩。
注意 日志压缩能改变磁盘存储文件的名称。一些已有日志被删除或者被更高序列号的文件替换,文件序列号也经常改变。GemFire始终为新日志用比已有数字更高的数字。
处理磁盘错误
如果region发生致命磁盘异常,它将记录错误日志并关闭region。磁盘上的文件会被保留用以恢复。取决于磁盘错误,用户需要删除或归档这些文件。如果由于文件毁坏导致恢复失败,需要删除这些数据文件并重新创建region。由于磁盘空间不足导致的错误,可以保留这些文件用于解决磁盘空间问题后重新创建region的数据恢复。
溢出到磁盘
溢出region可以将数据驱逐到磁盘但是不会持久化。 但溢出region被通过cache.xml或API创建时,它会检查在配置的目录下是否存在溢出文件。如果存在且被其他进程锁定,则溢出region创建失败。如果没有锁定,则会删除,然后创建自己的溢出文件。 在region上的操作可能导致数据从内存删除并写入磁盘。写操作跟持久化region一样:附加记录到当前oplog,即可以同步也可以异步,业务数据也是通过序列化写入磁盘。 如果读一个值被逐出到硬盘的条目,值会重新读入内存,同时其他条目的值被逐出到硬盘。 老的oplog文件在内容全部废弃后被删除。
溢出分区region
分区region可以配置为溢出到磁盘。用于存储JVM中分区region数据的桶有自己的文件和统计。对分区region的max-oplog-size会除以最大桶个数来决定每个桶的实际max-oplog-size。 因为所有桶的oplog都存放在相同目录,在相同JVM中对不同桶的并发写会导致额外的磁盘头移动。当溢出使能时,需要降低分区region的桶总个数来获得更好的性能。
何时数据写入磁盘?
条目仅值部分会被逐出到磁盘,键始终保留在内存里。 当超出溢出region逐出控制器门限,数据将写入磁盘。当对region进行写操作或者资源管理检测到JVM内存过低时,发生溢出。 如果溢出由写操作导致,操作在数据从内存溢出到磁盘、region的资源下降到门限值后完成。 注意下列写操作释放资源而不是消耗资源,所以它们不会导致数据逐出:
- 对条目进行更新,新的值对象大小比老的值对象小
- 删除条目
- 不会导致数据逐出的region级操作
写操作也有可能导致数据写入磁盘。一旦达到逐出控制器门限,读操作需要从磁盘加载数据到内存,其他条目的值对象需要被逐出以释放空间。 注意:由于溢出非持久化region不会进行数据恢复,可以安全配置异步写操作,缺点就是需要使用更多额外的内存。 对于溢出持久化Regina,所有值在写操作时已经立即写入磁盘,因此需要被逐出的值对象仍可以在内存被间接引用。
装载值对象
当读值对象被逐出到磁盘的条目时,需要读取包含当前值对象的oplog文件将其加载回内存。该oplog文件可能是非活跃oplog,默认最多7个非活跃oplog可以保持打开用来进行读取。系统属性gemfire.MAX_OPEN_INACTIVE_OPLOGS定义了可以同时打开的非活跃oplog文件最大个数。如果需要对已关闭的非活跃oplog进行读取,可能需要关系最近最少读的已打开的非活跃oplog。
滚动
Oplog滚动是压缩不再写入的oplog过程,基本上是垃圾回收过程。对溢出非持久化region的滚动与持久化region不同,它从非活跃oplog拷贝记录到当前的活跃oplog。如果所有非活跃oplog的值都拷贝到当前活跃oplog,即没有节省空间又浪费CPU和磁盘IO。所以默认如果少于一半的值需要从非活跃oplog复制到活跃oplog,才会考虑。可以设置系属性gemfire.OVERFLOW_ROLL_PERCENTAGE(浮点数)来设置这个百分比。例如设置其为25%,则配置为‘0.25’。这意味着当非活跃oplog含有25%的有效值对象(75%的垃圾)才会滚动到当前活跃oplog。 即使禁止滚动,溢出非持久化region在oplog不包含有效值对象时仍将删除非活跃oplog。因此如果可知加入溢出非持久化region的数据有相对短的生存时间,可以安全禁止滚动。 被驱逐到磁盘的值如仍被缓存使用就一直有效,直到条目被修改或删除。
注:以上文档是通过GemFire手册和多个社区文档整合而成,由于原文档版本不同而且没有源码对照,加上个人理解有限,难免有误,敬请谅解。