@liyuj
2017-04-03T20:57:47.000000Z
字数 11904
阅读 4657
Apache-Ignite-1.9.0-中文开发手册
本章涵盖了在准备将系统迁移到生产环境时的主要考虑因素。
对系统容量进行编制和规划是设计的一个必要组成部分。理解需要的缓存大小有助于决定需要多少物理内存、多少JVM、多少CPU和服务器。本章节会讨论有助于规划和确定一个部署的最小硬件需求的各种技术。
IgniteSystemProperties.IGNITE_ATOMIC_CACHE_DELETE_HISTORY_SIZE
为比默认值小的值,这个值可以减小)。GridGain通常会为每个条目增加200个字节。
内存容量规划示例
以如下的场景举例:
总对象数量 x 对象大小 x 2(每个对象一主一备)
2,000,000 x 1,024 x 2 = 4,096,000,000 字节
考虑到索引:
4,096,000,000 + (4,096,000,000 x 30%) = 5,078 MB
平台大约需要的额外内存:
300 MB x 4 = 1,200 MB
总大小:
5,078 + 1,200 = 6,278 MB
因此,预期的总内存消耗将超过 ~6GB
如果在没有代码的情况下规划计算通常来说很难进行估计,理解应用中要执行的每个操作要花费的成本是非常重要的,然后再乘以各种情况下预期要执行的操作的数量。为此一个非常好的起点就是Ignite的基准测试,它详细描述了标准操作的执行结果,以及提供这种性能所必要的粗略的容量需求。
在32核4.large的AWS实例上,性能测试结果如下:
PUT/GET: 26k/s
PUT (事务): 68k/s
PUT (事务 - 悲观): 20k/s
PUT (事务 - 乐观): 44k/s
SQL查询: 72k/s
这里提供更多的信息。
当迁移到生产环境时,为了提高性能和稳定性的若干高级JVM和系统级调整场景。
本章节包含有关将所有的或者重要的缓存条目放在堆内的集群的基本建议。
基本JVM配置
下面的JVM配置已经证明可以提供相当平滑的吞吐量,没有大的波动。
-server
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseTLAB
-XX:NewSize=128m
-XX:MaxNewSize=128m
-XX:MaxTenuringThreshold=0
-XX:SurvivorRatio=1024
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=60
-XX:+DisableExplicitGC
高级JVM配置
下面是对应用的一套高级JVM配置,他会产生大量的临时对象,因此会因为垃圾收集活动而触发长时间的暂停。
对于JDK1.7/1.8(以32核CPU,8GB堆举例)
-server
-Xms8g
-Xmx8g
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseTLAB
-XX:NewSize=128m
-XX:MaxNewSize=128m
-XX:MaxTenuringThreshold=0
-XX:SurvivorRatio=1024
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=40
-XX:MaxGCPauseMillis=1000
-XX:InitiatingHeapOccupancyPercent=50
-XX:+UseCompressedOops
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=8
-XX:+DisableExplicitGC
请注意,这些设置可能不总是理想的,所以一定确保在生产部署之前严格测试。
本章节包含有关将所有的或者重要的缓存条目放在堆外的集群的基本建议。
JVM配置
下面是对应用的一套高级JVM配置,他会产生大量的临时对象,因此会因为垃圾收集活动而触发长时间的暂停。
对于JDK1.8,建议使用G1垃圾收集器,下面可以看到对于开启G1的64核CPU的机器的10GB堆举例:
-server
-Xms10g
-Xmx10g
-XX:NewSize=512m
-XX:SurvivorRatio=6
-XX:+AlwaysPreTouch
-XX:+UseG1GC
-XX:MaxGCPauseMillis=2000
-XX:GCTimeRatio=4
-XX:InitiatingHeapOccupancyPercent=30
-XX:G1HeapRegionSize=8M
-XX:ConcGCThreads=16
-XX:G1HeapWastePercent=10
-XX:+UseTLAB
-XX:+ScavengeBeforeFullGC
-XX:+DisableExplicitGC
如果决定使用G1,请使用最新版的Oracle JDK8或者OpenJDK8,因为它在不断地改进。
如果G1无法满足应用场景或者使用的是JDK7,那么可以参照下面的基于CMS的配置,对于开始JVM调整是比较合适的(64核CPU的机器的10GB堆举例):
-server
-Xms10g
-Xmx10g
-XX:NewSize=512m
-XX:SurvivorRatio=6
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+AlwaysPreTouch
-XX:CMSInitiatingOccupancyFraction=30
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSClassUnloadingEnabled
-XX:+CMSPermGenSweepingEnabled
-XX:ConcGCThreads=16
-XX:+UseTLAB
-XX:+ScavengeBeforeFullGC
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC
请注意,这些设置可能不总是理想的,所以一定确保在生产部署之前严格测试。
在Linux环境下,因为I/O或者内存饥饿或者其他特定内核的设定的原因,可能发生一个基于IMDG的应用面临长时间的GC暂停的情况。本章节会给出一些指导,关于如何修改内核设定来避免因为Linux内核导致的长时间GC暂停。
下面给出的所有的Shell脚本命令都在RedHat7上做了测试,这些可能和实际的Linux发行版不同。
在应用任何基于内核的设定之前,还要确保检查系统统计数据、日志,使其对于实际场景的一个问题确实有效。
最后,在生产环境中针对Linux内核级做出改变,与IT部门商议也是明智的。
I/O问题
如果GC日志显示:“low user time, low system time, long GC pause”,那么一个原因就是GC线程因为内核等待I/O而卡住了,发生的原因基本是日志提交或者因为日志滚动的gzip导致改变的文件系统刷新。
作为一个解决方案,可以增加页面刷新到磁盘的频率,从默认的30秒到5秒。
sysctl –w vm.dirty_writeback_centisecs=500
sysctl –w vm.dirty_expire_centisecs=500
内存问题
如果GC日志显示“low user time, high system time, long GC pause”,那么最可能的是内存的压力触发了空闲内存的交换和扫描。
sysctl –w vm.swappiness=10
sysctl –w vm.zone_reclaim_mode=0
echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled
echo never > /sys/kernel/mm/redhat_transparent_hugepage/defrag
页面缓存
当应用与底层文件系统有大量的交互时,会导致内存大量使用页面缓存的情况,如果kswapd
进程无法跟上页面缓存使用的页面回收,在后台应用就会面临当需要新页面时的直接回收导致的高延迟,这种情况不仅影响应用的性能,也可能导致长时间的GC暂停。
要避免内存页面直接回收导致的长时间GC暂停,在Linux的最新内核版本中,可以通过/proc/sys/vm/extra_free_kbytes
设置在wmark_min
和wmark_low
之间增加额外的字节来避免前述的延迟。
sysctl -w vm.extra_free_kbytes=1240000
要获得有关本章节讨论的话题的更多信息,可以参考这个幻灯片。
当需要调试和解决与内存使用或者长时间GC暂停有关的问题时,本章节包括了一些可能有助于解决这些问题的信息。
内存溢出时获得堆现场
当JVM抛出OutOfMemoryException
并且JVM进程应该重启时,需要给JVM配置增加如下的属性:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/heapdump
-XX:OnOutOfMemoryError=“kill -9 %p” JROCKIT
-XXexitOnOutOfMemory
详细的垃圾收集统计
为了捕获有关垃圾收集的详细信息以及它的性能,可以给JVM配置增加如下的参数:
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M
-Xloggc:/path/to/gc/logs/log.txt
对于G1,建议设置如下的属性,他提供了很多符合人体工程学的、明确地保持-XX:+PrintGCDetails的详细信息:
-XX:+PrintAdaptiveSizePolicy
确保修改相应的路径和文件名,并且确保对于每个调用使用一个不同的文件名来避免从多个进程覆盖日志文件。
FlightRecorder设置
当需要调试性能或者内存问题,可以依靠Java的Flight Recorder工具,他可以持续地收集底层的详细的运行时信息,来启用事后的事故分析,要开启Flight Recorder,可以使用如下的设定:
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
-XX:+UnlockDiagnosticVMOptions
-XX:+DebugNonSafepoints
要开始记录衣蛾特定的Java进程,可以使用下面的命令作为一个示例:
jcmd <PID> JFR.start name=<recordcing_name> duration=60s filename=/var/recording/recording.jfr settings=profile
关于Java的Flight Recorder的完整信息,可以查看Oracle的官方文档。
系统级文件描述符限制
对于大规模的服务端应用,当运行大量的线程访问网格时,可能最终在客户端和服务端节点上打开大量的文件,因此建议增加默认值到默认的最大值。
错误的文件描述符设置会影响应用的稳定性和性能,为此,需要设置系统级文件描述符限制
和进程级文件描述符限制
,以root用户分别按照如下步骤操作:
fs.file-max = 300000
cat /proc/sys/fs/file-max
验证这个设置可以用:
sysctl fs.file-max
进程级文件描述符限制
默认情况下,Linux操作系统有一个相对较少的文件描述符和最大用户进程(1024)设置。重要的是,使用一个用户账户,它有他自己的最大打开文件描述符(打开文件数)和最大的用户进程配置为一个合适的值。
对于打开文件描述符,一个合理的最大值是32768。
使用如下命令来设置打开文件描述符的最大值和用户进程的最大值。
ulimit -n 32768 -u 32768
或者,也可以相应地修改如下文件:
/etc/security/limits.conf
- soft nofile 32768
- hard nofile 32768
/etc/security/limits.d/90-nproc.conf
- soft nproc 32768
可以参照增加打开文件数限制了解更多细节。
Ignite内存数据网格的性能和吞吐量很大程度上依赖于使用的功能以及配置,在几乎所有的场景中都可以通过简单地调整缓存的配置来优化缓存的性能。
Ignite有丰富的事件系统来向用户通知各种各样的事件,包括缓存的修改,退出,压缩,拓扑的变化以及很多其他的。因为每秒钟可能产生上千的事件,他会对系统产生额外的负载,这会导致显著地性能下降。因此,强烈建议只有应用逻辑必要时才启用这些事件。默认的话,事件通知是禁用的:
XML:
<bean class="org.apache.ignite.configuration.IgniteConfiguration">
...
<!-- Enable only some events and leave other ones disabled. -->
<property name="includeEventTypes">
<list>
<util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_STARTED"/>
<util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FINISHED"/>
<util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FAILED"/>
</list>
</property>
...
</bean>
就大小和容量而言,Ignite的内部缓存映射行为非常像正常的Java HashMap。他有一些初始的容量(默认都非常小),是持有数据的两倍,内部缓存映射的大小调整过程是CPU密集以及费时的,如果往缓存中加载大量的数据集(这是典型的使用场景),这个映射就会频繁地调整大小。要避免这个问题,可以通过比对预期的数据集大小来指定初始的缓存映射容量,这会在加载期间节约大量的CPU资源,因为映射不需要重新调整大小了。比如如果希望在缓存中加载一亿的数据,可以用如下的配置:
XML:
<bean class="org.apache.ignite.configuration.IgniteConfiguration">
...
<property name="cacheConfiguration">
<bean class="org.apache.ignite.configuration.CacheConfiguration">
...
<!-- Set initial cache capacity to ~ 100M. -->
<property name="startSize" value="#{100 * 1024 * 1024}"/>
...
</bean>
</property>
</bean>
上面的配置可以节省log₂(10⁸) − log₂(1024) ≈ 16次缓存映射调整大小(1024是初始映射容量的默认值)。记住,一般来说,后续的每一次调整大小都需要前一次两倍长的时间。
如果使用了分区
缓存,而且数据丢失并不是关键(比如,当有一个备份缓存存储时),可以考虑禁用分区
缓存的备份。当备份启用时,缓存引擎会为每个条目维护一个远程拷贝,这需要网络交换和而且是耗时的。要禁用备份,可以使用如下的配置:
XML:
<bean class="org.apache.ignite.configuration.IgniteConfiguration">
...
<property name="cacheConfiguration">
<bean class="org.apache.ignite.configuration.CacheConfiguration">
...
<!-- Set cache mode. -->
<property name="cacheMode" value="PARTITIONED"/>
<!-- Set number of backups to 0-->
<property name="backups" value="0"/>
...
</bean>
</property>
</bean>
可能的数据丢失
如果没有启用分区
缓存的备份,会丢失缓存在故障节点的所有数据,这对于缓存临时数据或者数据可以通过某种方式重建可能是可以接受的。禁用备份之前一定要确保对于应用来说丢失数据不是关键的。
如果打算为了缓存数据为JVM分配大量的内存(通常大于10G的内存),那么应用很可能会遭遇长时间的GC暂停,他会显著地增加延时。要避免GC暂停可以使用堆外内存来缓存数据-实际上数据仍然换存在内存中,但是JVM并不知道他而且GC也不起作用。要启用不限大小的堆外存储,可以使用如下的配置:
XML:
<bean class="org.apache.ignite.configuration.IgniteConfiguration">
...
<property name="cacheConfiguration">
<bean class="org.apache.ignite.configuration.CacheConfiguration">
...
<!-- Enable off-heap storage with unlimited size. -->
<property name="offHeapMaxMemory" value="0"/>
...
</bean>
</property>
</bean>
交换存储默认是禁用的。然而在配置中是可以启用的。如果启用,记住使用交换存储会显著影响性能。要显式地禁用交换存储,可以使用如下的配置:
XML:
<bean class="org.apache.ignite.configuration.IgniteConfiguration">
...
<property name="cacheConfiguration">
<bean class="org.apache.ignite.configuration.CacheConfiguration">
...
<!-- Disable swap. -->
<property name="swapEnabled" value="false"/>
...
</bean>
</property>
</bean>
退出默认是禁用的。如果确实需要使用退出来确保缓存的数据不会增长到超过允许的内存限额,可以考虑选择正确的退出策略。下面是一个设置最大数据条目是100000的LRU退出策略的例子:
XML:
<bean class="org.apache.ignite.cache.CacheConfiguration">
...
<property name="evictionPolicy">
<!-- LRU eviction policy. -->
<bean class="org.apache.ignite.cache.eviction.lru.LruEvictionPolicy">
<!-- Set the maximum cache size to 1 million (default is 100,000). -->
<property name="maxSize" value="1000000"/>
</bean>
</property>
...
</bean>
不论使用哪个退出策略,缓存的性能取决于退出策略允许的缓存内数据条目的最大量-如果缓存大小增长到这个限值,就会触发退出。
当有一个新节点加入网络时,已有的节点会放弃一些键的或主或备的所有权给新的节点以使整个集群中的数据一直保持均衡,这可能需要额外的资源以及影响缓存的性能。要处理这个可能的问题,可以考虑调整如下的参数:
下面是一个在缓存配置中设置上述所有参数的例子:
XML:
<bean class="org.apache.ignite.configuration.IgniteConfiguration">
...
<property name="cacheConfiguration">
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<!-- Set rebalance batch size to 1 MB. -->
<property name="rebalanceBatchSize" value="#{1024 * 1024}"/>
<!-- Explicitly disable rebalance throttling. -->
<property name="rebalanceThrottle" value="0"/>
<!-- Set 4 threads for rebalancing. -->
<property name="rebalanceThreadPoolSize" value="4"/>
...
</bean
</property>
</bean>
默认,Ignite的主线程池大小设置为2倍于可用CPU数量。大多数情况下保持每核心两个线程会使应用有个好的性能,因为这会有更少的上下文切换以及CPU缓存也会工作得更好。然而,如果希望作业因为I/O或者其他任何原因而阻塞,增加线程池的大小可能也是有意义的,下面是一个如何配置线程池的例子:
XML:
<bean class="org.apache.ignite.configuration.IgniteConfiguration">
...
<!-- Configure internal thread pool. -->
<property name="publicThreadPoolSize" value="64"/>
<!-- Configure system thread pool. -->
<property name="systemThreadPoolSize" value="32"/>
...
</bean>
Ignite可以在内存内执行MapReduce计算。然而,通常很多计算都会使用缓存在远程节点上的一些数据,大多数情况下从远程节点加载那些数据是很昂贵的而将计算发送到数据所在的节点就要便宜得多。最简单的实现方式就是使用IgniteCompute.affinityRun()
方法或者@CacheAffinityMapped
注解,也有其他的方式, 包括Affinity.mapKeysToNodes()
方法。关于并置计算主题在3.11.关系并置
章节有详细的描述,还有相关的代码样例。
如果需要往缓存中加载大量的数据,可以使用IgniteDataStreamer
来实现,数据流处理器在将数据发送到远程节点之前会将数据正确地形成批次然后会正确地控制发生在每个节点的并发操作的数量来避免颠簸。通常他会比一堆单线程的操作有十倍的性能提升。可以在3.13.数据加载
章节看到更详细的描述和样例。
如果能发送10个比较大的作业代替100个小些的作业,那么应该选择发送大些的作业,这会降低网络上传输作业的数量以及显著地提升性能。类似的,对于缓存的条目应该想办法使用API方法,他会使用键和值的集合,而不是一个一个地传递。
如果发现由于垃圾收集(GC)导致吞吐量产生了波动,那么就需要像下面这样对JVM的参数进行调整以提供较为平滑的吞吐量表现:
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseTLAB
-XX:NewSize=128m
-XX:MaxNewSize=128m
-XX:MaxTenuringThreshold=0
-XX:SurvivorRatio=1024
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=60
JCache标准要求缓存供应商支持基于值存储的语义,这意味着当从缓存中读取一个值时,无法得到真实存储的对象的引用,而是这个对象的一个副本。Ignite默认遵守这个方式,但是可以通过CacheConfiguration.copyOnRead
这个配置属性覆盖这个行为。
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<!--
Force cache to return the instance that is stored in cache
instead of creating a copy.
-->
<property name="copyOnRead" value="false"/>
</bean>
如果使用jul-to-slf4j桥,确保日志的正确配置
Ignite使用java.util.logging.Logger (JUL),如果使用了jul-to-slf4j桥,那么需要特别注意为Ignite配置的JUL日志级别,如果因为某种原因将org.apache配置为DDEBUG级别,最终的日志级别可能是INFO,这意味着Ignite会花费十倍的负载来生成日志消息,它们在之后跨桥时会被丢弃,JUL的默认日志级别为INFO。在org.apache.ignite.logger.java.JavaLogger#isDebugEnabled
中打上断点,当JUL系统的日志级别配置为DEBUG时,就会暴露这一点。