内容简介:公司业务由于广告以及导流的影响,对搜索服务的稳定以及性能提出了更苛刻的要求,为了应对可能的流量突增,特进行了大规模,全方位的性能测试。摸清线上服务的能力,以及定位瓶颈所在。搜索服务采用了Master-Slave的Redis服务来缓存用户的搜索结果,过期时间在5~10分钟,通过缓存来提升响应时间和并发。key是用户的请求实体json字符串,value自然是搜索结果json串,并没有使用set,hash等类型。经过一轮的性能测试,最终的瓶颈点落在了Redis上,如下图Grafana监控所示:
公司业务由于广告以及导流的影响,对搜索服务的稳定以及性能提出了更苛刻的要求,为了应对可能的流量突增,特进行了大规模,全方位的性能测试。摸清线上服务的能力,以及定位瓶颈所在。
搜索服务采用了Master-Slave的 Redis 服务来缓存用户的搜索结果,过期时间在5~10分钟,通过缓存来提升响应时间和并发。key是用户的请求实体json字符串,value自然是搜索结果json串,并没有使用set,hash等类型。
Redis瓶颈
经过一轮的性能测试,最终的瓶颈点落在了Redis上,如下图Grafana监控所示:
在请求和并发上去之后,redis item迅速增长到300W左右,每秒command数达到近10W。由于redis存储做了限制20G,图中可以看到内存已经满了,item肯定会被频繁置换出去,总数提升不上去。从network上看,output已经达到了近200MB/s,远超运维的限制的100MB/s(千兆网卡极限约100MB,万兆约1000MB)。
核心瓶颈点:
- 存储
- 网络IO
Redis bigkey解决
所谓的BigKey,大概有以下的情况:
- String本身比较大
- set,hash内个数比较大
在我们的这个场景下,主要是json串太大。这涉及到key, value的如果减小的问题。
先来看key如何减小:
我们是使用fastjson来对Request对象做序列化的,key就是这个json串。
Request request = (Request) obj; String json = JSON.toJSONString(request);
直接想到的就是对key做编码,减小长度。使用了commons-codec里的md5加密摘要,作为key。
DigestUtils.md5Hex(json)
这样就保证了key的长度缩小,并且长度一致。需要注意的是MD5有一定的冲突率,在本场景下可以基本忽略。
接下来,看value如何缩小:
value的序列化,我们使用的是jackson2来做的,反序列化的时候直接就是对象,方便配合Spring Cache使用。核心代码如下:
private RedisSerializer getValueSerializer() { Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); return jackson2JsonRedisSerializer; }
redis里存储的就是jackson2转化后的字符串,其中包含了class等相关信息。
当然,从业务上减小每次查询的搜索结果条数是一个办法,比如每次最多只能查询10条。但是,如果10条的数据仍旧比较大,我们想到的直接方法就是 压缩 。
直接使用JDK自带的GZIP压缩来实现如下:
public class GzipSerializer implements RedisSerializer<Object> { public static final int BUFFER_SIZE = 4096; // 这里组合方式,使用到了一个序列化器 private RedisSerializer<Object> innerSerializer; public GzipSerializer(RedisSerializer<Object> innerSerializer) { this.innerSerializer = innerSerializer; } @Override public byte[] serialize(Object graph) throws SerializationException { if (graph == null) { return new byte[0]; } ByteArrayOutputStream bos = null; GZIPOutputStream gzip = null; try { // 先序列化 byte[] bytes = innerSerializer.serialize(graph); bos = new ByteArrayOutputStream(); gzip = new GZIPOutputStream(bos); // 在压缩 gzip.write(bytes); gzip.finish(); byte[] result = bos.toByteArray(); return result; } catch (Exception e) { throw new SerializationException("Gzip Serialization Error", e); } finally { IOUtils.closeQuietly(bos); IOUtils.closeQuietly(gzip); } } @Override public Object deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) { return null; } ByteArrayOutputStream bos = null; ByteArrayInputStream bis = null; GZIPInputStream gzip = null; try { bos = new ByteArrayOutputStream(); bis = new ByteArrayInputStream(bytes); gzip = new GZIPInputStream(bis); byte[] buff = new byte[BUFFER_SIZE]; int n; // 先解压 while ((n = gzip.read(buff, 0, BUFFER_SIZE)) > 0) { bos.write(buff, 0, n); } // 再反序列化 Object result = innerSerializer.deserialize(bos.toByteArray()); return result; } catch (Exception e) { throw new SerializationException("Gzip deserizelie error", e); } finally { IOUtils.closeQuietly(bos); IOUtils.closeQuietly(bis); IOUtils.closeQuietly(gzip); } } }
通过这个GzipSerializer实现了压缩功能,并且最大可能的减少了对原始代码的改动。
@Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory); // 在这里进行了功能增强,提供了压缩机制。 template.setValueSerializer(new GzipSerializer(getValueSerializer())); template.afterPropertiesSet(); return template; }
更改之后,进行新一轮的性能压测。grafana监控效果如下:
可以看出,同样的压力情况下。内存占用不到5G,网络IO也在14M/S上下, 效果显著 。顺利的解决了Redis的瓶颈所在。
至此压力测试到一个阶段,下面来看一下,功能在生产环境的使用情况如何。
从上面看出,在1.16上线后,在整体流量基本不变的情况下,内存以及网络IO大幅度下降,经受住了生产环境的考验。
有没有更好的方案
以上,为了解决Redis瓶颈,通过MD5摘要以及压缩快速而小改动的解决了问题,如果从长远来看,如何才能做到更好呢?
压缩方式-性能&压缩比兼顾
GZIP能取得不错的压缩比,但是对CPU要求较高,相当于把redis的存储瓶颈转移到了 Java 程序的解压缩上。是否有一个性能和压缩比兼得的压缩方案呢?
从参考1中,可以一探究竟。
文中以一个445M的文件对常见的压缩方式进行了比较。
压缩后的大小:
压缩比:
压缩耗时:
解压缩耗时:
从以上的benchmark对比来看,不同压缩方式优劣不一样,大家可以根据实际情况来选择。
个人推荐来看,也许 LZ4 是一个不错的选择,压缩比高一些,但是解压缩速度都比gzip要高。
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import org.apache.commons.io.IOUtils; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import net.jpountz.lz4.LZ4BlockInputStream; import net.jpountz.lz4.LZ4BlockOutputStream; import net.jpountz.lz4.LZ4Compressor; import net.jpountz.lz4.LZ4Factory; import net.jpountz.lz4.LZ4FastDecompressor; /** * LZ4 压缩 Serializer * @author shenyanchao */ public class LZ4Serializer implements RedisSerializer<Object> { private static final int BUFFER_SIZE = 4096; private RedisSerializer<Object> innerSerializer; private LZ4FastDecompressor decompresser; private LZ4Compressor compressor; public LZ4Serializer(RedisSerializer<Object> innerSerializer) { this.innerSerializer = innerSerializer; LZ4Factory factory = LZ4Factory.fastestInstance(); this.compressor = factory.fastCompressor(); this.decompresser = factory.fastDecompressor(); } @Override public byte[] serialize(Object graph) throws SerializationException { if (graph == null) { return new byte[0]; } ByteArrayOutputStream byteOutput = null; LZ4BlockOutputStream compressedOutput = null; try { byte[] bytes = innerSerializer.serialize(graph); byteOutput = new ByteArrayOutputStream(); compressedOutput = new LZ4BlockOutputStream(byteOutput, BUFFER_SIZE, compressor); compressedOutput.write(bytes); compressedOutput.finish(); byte[] compressBytes = byteOutput.toByteArray(); return compressBytes; } catch (Exception e) { throw new SerializationException("LZ4 Serialization Error", e); } finally { IOUtils.closeQuietly(compressedOutput); IOUtils.closeQuietly(byteOutput); } } @Override public Object deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) { return null; } ByteArrayOutputStream baos = null; LZ4BlockInputStream lzis = null; try { baos = new ByteArrayOutputStream(BUFFER_SIZE); lzis = new LZ4BlockInputStream(new ByteArrayInputStream(bytes), decompresser); int count; byte[] buffer = new byte[BUFFER_SIZE]; while ((count = lzis.read(buffer)) != -1) { baos.write(buffer, 0, count); } Object result = innerSerializer.deserialize(baos.toByteArray()); return result; } catch (Exception e) { throw new SerializationException("LZ4 deserizelie error", e); } finally { IOUtils.closeQuietly(lzis); IOUtils.closeQuietly(baos); } } }
序列化方案
前面提到,我们的序列化方案是基于json的,那有没有更好的序列化方案,能够有更好的性能,更低的内存占用。
序列化方案无论是在redis,还是在各种RPC解决方案里都是大家热烈讨论的话题。那我们来看一下主要序列化方案的一些对比。
在参考2和3里都有提及。
各种序列化方案字节大小对比(越小越好):
耗时对比(越小越好):
综合以上文章所述,再加上易用性的使用角度,个人推荐倾向于:
- kryo
- FST
当然关于Java对象的序列化,不同方案也有一些差异,比如是否实现Serializable接口,是否支持嵌套内部类在选择的时候,也都是需要注意的。
下面给一个kryo的实现:
import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; /** * kryo 序列化 * @author shenyanchao */ public class KryoSerializer<T> implements RedisSerializer<T> { private static final int BUFFER_SIZE = 2048; private Kryo kryo = new Kryo(); @Override public byte[] serialize(T t) throws SerializationException { byte[] buffer = new byte[BUFFER_SIZE]; Output output = new Output(buffer); kryo.writeClassAndObject(output, t); return output.toBytes(); } @Override public T deserialize(byte[] bytes) throws SerializationException { Input input = new Input(bytes); T t = (T) kryo.readClassAndObject(input); return t; } }
使用Redis Cluster
前面提到的,无论是压缩还是序列化都是基于Redis无法扩容,以及单机网络IO限制的。如果为了支持更大的并发和更快的响应,我们需要使用Redis Cluster,当然也可以从客户角度进行拆分,使用多个Redis实例,这里不予表述。
Redis Cluster在3.0之后推出,用于解决Redis的分布式需求。自动的在不同节点之间均衡数据,充分利用多机的优势。
参考:
1. Quick Benchmark: Gzip vs Bzip2 vs LZMA vs XZ vs LZ4 vs LZO
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
从界面到网络空间
(美)海姆 / 金吾伦/刘钢 / 上海科技教育出版社 / 2000-7 / 16.40元
计算机急剧改变了20世纪的生活。今天,我们凭借遍及全球的计算机网络加速了过去以广播、报纸和电视形式进行的交流。思想风驰电掣般在全球翻飞。仅在角落中潜伏着已完善的虚拟实在。在虚拟实在吕,我们能将自己沉浸于感官模拟,不仅对现实世界,也对假想世界。当我们开始在真实世界与虚拟世界之间转换时,迈克尔·海姆问,我们对实在的感觉如何改变?在〈从界面到网络空间〉中,海姆探讨了这一问题,以及信息时代其他哲学问题。他......一起来看看 《从界面到网络空间》 这本书的介绍吧!