本文从GC影响C端接口性能的场景展开,讲述了使用堆外本地缓存降低对JVM影响的解决方案,达到了接口Max降低10倍的效果。通过本文可以了解OHC本地缓存优化实战,一招Max降低10倍,提升系统吞吐量。
【本文目录】
性能优化是一场永无止境的旅程。
到家门店系统,作为到家核心基础服务之一,门店C端接口有着调用量高,性能要求高的特点。
C端服务经过演进,核心接口先查询本地缓存,如果本地缓存没有命中,再查询Redis。本地缓存命中率99%,服务性能比较平稳。
随着门店数据越来越多,本地缓存容量逐渐增大到3G左右。虽然对垃圾回收器和JVM参数都进行调整,由于本地缓存数据量越来越大,本地缓存数据对于应用GC的影响越来越明显,YGC平均耗时100ms,特别是大促期间调用方接口毛刺感知也越来越明显。
由于本地缓存在每台机器上容量是固定的,即便是将机器扩容,对与GC毛刺也没有明显效果。
本地缓存位于应用程序的内存中,读取和写入速度非常快,可以快速响应请求,无需额外的网络通信,但是一般本地缓存存在JVM内,数据量过多会影响GC,造成GC频率、耗时增加;如果用Redis的话有网络通信的开销。
通过对本地缓存的调研,堆外缓存可以很好兼顾上面的问题。堆外缓存把数据放在JVM堆外的,缓存数据对GC影响较小,同时它是在机器内存中的,相对与Redis也没有网络开销,最终选择OHC。
talk is cheap, show me the code! OCH是骡子是马我们遛一遛。
【3.1 引入POM】
OHC 存储的是二进制数组,需要实现OHC序列化接口,将缓存数据与二进制数组之间序列化和反序列化。
这里使用的是Protostuff,当然也可以使用kryo、Hession等,通过压测验证选择适合的序列化框架。
<!--OHC相关-->
<dependency>
<groupId>org.caffinitas.ohc</groupId>
<artifactId>ohc-core</artifactId>
<version>0.7.4</version>
</dependency>
<!--OHC 存储的是二进制数组,所以需要实现OHC序列化接口,将缓存数据与二进制数组之间序列化和反序列化-->
<!--这里使用的是protostuff,当然也可以使用kryo、Hession等,通过压测验证选择适合的-->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.6.0</version>
</dependency>
OHC缓存创建
OHCache<String, XxxxInfo> basicStoreInfoCache = OHCacheBuilder.<String, XxxxInfo>newBuilder()
.keySerializer(new OhcStringSerializer()) //key的序列化器
.valueSerializer(new OhcProtostuffXxxxInfoSerializer()) //value的序列化器
.segmentCount(512) // 分段数量 默认=2*CPU核数
.hashTableSize(100000)// 哈希表大小 默认=8192
.capacity(1024 * 1024 * 1024) //缓存容量 单位B 默认64MB
.eviction(Eviction.LRU) // 淘汰策略 可选LRU\W_TINY_LFU\NONE
.timeouts(false) //不使用过期时间,根据业务自己选择
.build();
value-自定义对象序列化器,这里用Protostuff实现,也可以自己选择使用kryo、Hession等实现;
//key-String 序列化器,这里直接复用OCH源码中测试用例的String序列化器
public class OhcStringSerializer implements CacheSerializer<String> {
public int serializedSize(String value) {
return writeUTFLen(value);
}
public void serialize(String value, ByteBuffer buf) {
// 得到字符串对象UTF-8编码的字节数组
byte[] bytes = value.getBytes(Charsets.UTF_8);
buf.put((byte) ((bytes.length >>> 8) & 0xFF));
buf.put((byte) ((bytes.length >>> 0) & 0xFF));
buf.put(bytes);
}
public String deserialize(ByteBuffer buf) {
int length = (((buf.get() & 0xff) << 8) + ((buf.get() & 0xff) << 0));
byte[] bytes = new byte[length];
buf.get(bytes);
return new String(bytes, Charsets.UTF_8);
}
static int writeUTFLen(String str) {
int strlen = str.length();
int utflen = 0;
int c;
for (int i = 0; i < strlen; i++) {
c = str.charAt(i);
if ((c >= 0x0001) && (c <= 0x007F)){
utflen++;}
else if (c > 0x07FF){
utflen += 3;}
else{
utflen += 2;
}
}
if (utflen > 65535) {
throw new RuntimeException("encoded string too long: " + utflen + " bytes");
}
return utflen + 2;
}
}
//value-自定义对象序列化器,这里用Protostuff实现,可以自己选择使用kryo、Hession等实现
public class OhcProtostuffXxxxInfoSerializer implements CacheSerializer<XxxxInfo> {
/**
* 将缓存数据序列化到 ByteBuffer 中,ByteBuffer是OHC管理的堆外内存区域的映射。
*/
public void serialize(XxxxInfo t, ByteBuffer byteBuffer) {
byteBuffer.put(ProtostuffUtils.serialize(t));
}
/**
* 对堆外缓存的数据进行反序列化
*/
public XxxxInfo deserialize(ByteBuffer byteBuffer) {
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
return ProtostuffUtils.deserialize(bytes, XxxxInfo.class);
}
/**
* 计算字序列化后占用的空间
*/
public int serializedSize(XxxxInfo t) {
return ProtostuffUtils.serialize(t).length;
}
}
为了方便调用和序列化封装为工具类,同时对代码通过FastThreadLocal进行优化,提升性能。public class ProtostuffUtils {
/**
* 避免每次序列化都重新申请Buffer空间,提升性能
*/
private static final FastThreadLocal<LinkedBuffer> bufferPool = new FastThreadLocal<LinkedBuffer>() {
@Override
protected LinkedBuffer initialValue() throws Exception {
return LinkedBuffer.allocate(4 * 2 * LinkedBuffer.DEFAULT_BUFFER_SIZE);
}
};
/**
* 缓存Schema
*/
private static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();
/**
* 序列化方法,把指定对象序列化成字节数组
*/
@SuppressWarnings("unchecked")
public static <T> byte[] serialize(T obj) {
Class<T> clazz = (Class<T>) obj.getClass();
Schema<T> schema = getSchema(clazz);
byte[] data;
LinkedBuffer linkedBuffer = null;
try {
linkedBuffer = bufferPool.get();
data = ProtostuffIOUtil.toByteArray(obj, schema, linkedBuffer);
} finally {
if (Objects.nonNull(linkedBuffer)) {
linkedBuffer.clear();
}
}
return data;
}
/**
* 反序列化方法,将字节数组反序列化成指定Class类型
*/
public static <T> T deserialize(byte[] data, Class<T> clazz) {
Schema<T> schema = getSchema(clazz);
T obj = schema.newMessage();
ProtostuffIOUtil.mergeFrom(data, obj, schema);
return obj;
}
@SuppressWarnings("unchecked")
private static <T> Schema<T> getSchema(Class<T> clazz) {
Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
if (Objects.isNull(schema)) {
schema = RuntimeSchema.getSchema(clazz);
if (Objects.nonNull(schema)) {
schemaCache.put(clazz, schema);
}
}
return schema;
}
}
GC时间对比降低10倍
优化后
hitCount
:缓存命中次数,表示从缓存中成功获取数据的次数。
missCount
:缓存未命中次数,表示尝试从缓存中获取数据但未找到的次数。
evictionCount
:缓存驱逐次数,表示因为缓存空间不足而从缓存中移除的数据项数量。
expireCount
:缓存过期次数,表示因为缓存数据过期而从缓存中移除的数据项数量。
size
:缓存当前存储的数据项数量。
capacity
:缓存的最大容量,表示缓存可以存储的最大数据项数量。
free
:缓存剩余空闲容量,表示缓存中未使用的可用空间。
rehashCount
:重新哈希次数,表示进行哈希表重新分配的次数。
put(add/replace/fail)
:数据项添加/替换/失败的次数。
removeCount
:缓存移除次数,表示从缓存中移除数据项的次数。
segmentSizes(#/min/max/avg)
:段大小统计信息,包括段的数量、最小大小、最大大小和平均大小。
totalAllocated
:已分配的总内存大小,表示为负数时表示未知。
lruCompactions
:LRU 压缩次数,表示进行 LRU 压缩的次数。
对象:{"paramID":1,"paramName":"John Doe"} 正常JSON字符串:{"paramID":1,"paramName":"John Doe"} 压缩字段名JSON字符串:{"a":1,"b":"John Doe"}
- END -