一招MAX降低10倍,现在它是我的了| 京东到家门店系统OHC本地缓存优化实战
本文从GC影响C端接口性能的场景展开,讲述了使用堆外本地缓存降低对JVM影响的解决方案,达到了接口Max降低10倍的效果。通过本文可以了解OHC本地缓存优化实战,一招Max降低10倍,提升系统吞吐量。
【本文目录】
背景 初识此物心已惊-OHC初识 习得技能心自安-OHC使用 剖析根源见真谛-OHC原理 总结
性能优化是一场永无止境的旅程。
到家门店系统,作为到家核心基础服务之一,门店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> {@Overridepublic int serializedSize(String value) {return writeUTFLen(value);}@Overridepublic 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);}@Overridepublic 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管理的堆外内存区域的映射。*/@Overridepublic void serialize(XxxxInfo t, ByteBuffer byteBuffer) {byteBuffer.put(ProtostuffUtils.serialize(t));}/*** 对堆外缓存的数据进行反序列化*/@Overridepublic XxxxInfo deserialize(ByteBuffer byteBuffer) {byte[] bytes = new byte[byteBuffer.remaining()];byteBuffer.get(bytes);return ProtostuffUtils.deserialize(bytes, XxxxInfo.class);}/*** 计算字序列化后占用的空间*/@Overridepublic int serializedSize(XxxxInfo t) {return ProtostuffUtils.serialize(t).length;}}
public class ProtostuffUtils {/*** 避免每次序列化都重新申请Buffer空间,提升性能*/private static final FastThreadLocal<LinkedBuffer> bufferPool = new FastThreadLocal<LinkedBuffer>() {@Overrideprotected 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;}}
【3.3 压测及参数调整】
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 -