釋放你九成的帶寬和內(nèi)存:GZIP在解決Redis大Key方面的應(yīng)用
引言
目前主流HTTP協(xié)議接口都是使用JSON格式做數(shù)據(jù)交換的,JSON數(shù)據(jù)格式有著結(jié)構(gòu)簡(jiǎn)單、可讀性高、跨平臺(tái),易解析等優(yōu)點(diǎn),同時(shí)也存在著冗余數(shù)據(jù)會(huì)占用非常多的儲(chǔ)存空間的問(wèn)題,這大大增加了JSON格式數(shù)據(jù)在存儲(chǔ)、傳輸過(guò)程中的性能消耗。所以對(duì)JSON格式數(shù)據(jù)壓縮后再傳輸、存儲(chǔ)就變的非常的有價(jià)值,如對(duì)JSON格式數(shù)據(jù)使用GZIP壓縮算法可以實(shí)現(xiàn)90%左右的壓縮率,更小的空間可以節(jié)省存儲(chǔ)成本和降低傳輸帶寬成本,本文介紹GZIP壓縮算法在優(yōu)化Redis使用大KEY字段中的應(yīng)用,通過(guò)簡(jiǎn)單壓縮可以節(jié)省88%的內(nèi)存空間和帶寬資源。
HTTP協(xié)議開(kāi)啟GZIP
HTTP協(xié)議標(biāo)準(zhǔn)中是直接支持GZIP壓縮算法的,通過(guò)響應(yīng)頭Content-Encoding: gzip來(lái)表明響應(yīng)內(nèi)容使用了GZIP壓縮,當(dāng)客戶(hù)端收到數(shù)據(jù)后會(huì)使用GZIP算法對(duì)Body內(nèi)容進(jìn)行解壓。
★
RFC 1952 - IETF(互聯(lián)網(wǎng)工程任務(wù)組)標(biāo)準(zhǔn)化的Gzip文件格式規(guī)范,
★
RFC 2616 - HTTP 1.1 協(xié)議規(guī)范,其中包括對(duì) Content-Encoding 頭的定義
在Nginx中可以通過(guò) gzip on開(kāi)啟GZIP壓縮功能:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
在Springboot中可以通過(guò)server.compression.enabled開(kāi)啟GZIP壓縮功能:
server:
port: 80
compression:
enabled: true
mime-types: application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain
min-response-size: 2KB
- enabled,開(kāi)啟或關(guān)閉
- mime-types,壓縮的數(shù)據(jù)類(lèi)型
- min-response-size,最小壓縮大小
測(cè)試GZIP
為了測(cè)試開(kāi)啟GZIP前后的對(duì)比效果我們寫(xiě)一個(gè)簡(jiǎn)單的接口:
@GetMapping("/list")
public ResponseEntity<ApiResult> list() {
return renderOk(getData());
}
我們返回1000條JSON格式的用戶(hù)信息:
private List<UserVo> getData() {
return IntStream.range(1, 1000).mapToObj(x -> new UserVo(x,x+"+email@q63.com",x+"_公眾號(hào)",x+"_趙俠客")).collect(Collectors.toList());
}
@Data
@AllArgsConstructor
public class UserVo {
private Integer id;
private String username;
private String email;
private String trueName;
}
在未開(kāi)啟GZIP前接口返回?cái)?shù)據(jù)的大小是92.8KB, Content-Encoding為空,在開(kāi)啟GZIP后接口返回的數(shù)據(jù)大小為11.5KB,Content-Encoding為gzip,接口返回?cái)?shù)量降低了88%。
當(dāng)然我們也可以在接口中通過(guò)手動(dòng)添加content-encoding響應(yīng)頭,然后通過(guò)手動(dòng)調(diào)用GZIPOutputStream對(duì)返回?cái)?shù)據(jù)進(jìn)行GZIP壓縮:
@GetMapping("/gzip")
public void gzip(HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.setHeader("content-encoding", "gzip");
try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(response.getOutputStream())) {
IOUtils.write(JsonUtils.toJson(getData()), gzipOutputStream);
}
}
Redis緩存壓縮
為了增加接口的響應(yīng)速度我們通常會(huì)使用Redis當(dāng)緩存,基本邏輯是先查Redis有沒(méi)有數(shù)據(jù)如果有直接返回,如果沒(méi)有會(huì)查數(shù)據(jù)庫(kù),然后再存入Redis,以下是一個(gè)簡(jiǎn)單的使用Redis當(dāng)緩存的接口:
@Resource
private RedissonClient redissonClient;
public static final String REDIS_KEY = "REDIS_KEY";
@GetMapping("/redis")
public void redis(HttpServletResponse response) throws IOException {
RBucket<String> bucket = redissonClient.getBucket(REDIS_KEY);
String data = bucket.get();
if (data == null) {
data=JsonUtils.toJson(getData());
redissonClient.getBucket(REDIS_KEY).set(data,100L, TimeUnit.SECONDS);
}
response.setContentType("application/json");
IOUtils.write(data, response.getOutputStream());
}
我們分析一下這樣個(gè)接口的基本數(shù)據(jù)流:
- 第一次從數(shù)據(jù)庫(kù)服務(wù)器查出92.8KB的數(shù)據(jù)傳輸?shù)絎EB服務(wù)器中
- 將92.8KB的數(shù)據(jù)從WEB服務(wù)器傳輸?shù)絉edis服務(wù)器中
- 后面如果命中緩存將92.8KB數(shù)據(jù)從Redis服務(wù)器傳輸?shù)絎EB服務(wù)器
- 最后將92.8KB數(shù)據(jù)從WEB服務(wù)器返回給用戶(hù)瀏覽器
使用Redis當(dāng)緩存加速接口
使用ZIP優(yōu)化Redis緩存:
public static final String GZIP_REDIS_KEY = "GZIP_REDIS_KEY";
@GetMapping("/gzipRedis")
public void gzipRedis(HttpServletResponse response) throws IOException {
RBucket<byte[]> bucket = redissonClient.getBucket(GZIP_REDIS_KEY);
byte[] data = bucket.get();
if (data == null) {
String jsnotallow=JsonUtils.toJson(getData());
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
IOUtils.write(json, gzipOutputStream, String.valueOf(StandardCharsets.UTF_8));
gzipOutputStream.finish();
data= byteArrayOutputStream.toByteArray();
redissonClient.getBucket(GZIP_REDIS_KEY).set(data,100L, TimeUnit.SECONDS);
}
}
response.setContentType("application/json");
response.setHeader("content-encoding", "gzip");
IOUtils.write(data, response.getOutputStream());
}
使用GZIP壓縮后的緩存接口
我們?cè)俜治鲆幌乱陨鲜褂肎ZIP壓縮后的數(shù)據(jù)傳輸:
- 第一次從數(shù)據(jù)庫(kù)服務(wù)器查出92.8KB的數(shù)據(jù)傳輸?shù)絎EB服務(wù)器中
- 將11.5KB的GZIP數(shù)據(jù)從WEB服務(wù)器傳輸?shù)絉edis服務(wù)器中
- 后面命中緩存將11.5KB數(shù)據(jù)從Redis服務(wù)器傳輸?shù)絎EB服務(wù)器
- 最后將11.KB數(shù)據(jù)從WEB服務(wù)器返回給用戶(hù)瀏覽器
GZIP壓縮后的Redis緩存
單次接口請(qǐng)求好像感覺(jué)不到這個(gè) GZIP壓縮帶來(lái)的好處,接下來(lái)我們壓測(cè)一下看看會(huì)不會(huì)有差距。
壓力測(cè)試
壓測(cè)可以使用ab (Apache Benchmark) 工具,ab工具是 Apache HTTP server 的一部分,在 macOS使用Homebrew包管理器可以快速安裝上ab :
brew install httpd
ab -V
ab -n 100 -c 10 http://localhost/list
其中:
- -n 100 表示總共請(qǐng)求 100 次。
- -c 10 表示并發(fā) 10 個(gè)請(qǐng)求。
未壓縮走Redis壓縮結(jié)果:
ab -n 100000 -c 10 http://localhost/redis
Finished 100000 requests
Document Length: 92476 bytes
Concurrency Level: 10
Time taken for tests: 194.917 seconds
Complete requests: 100000
Failed requests: 0
Total transferred: 9258100000 bytes
HTML transferred: 9247600000 bytes
Requests per second: 513.04 [#/sec] (mean)
Time per request: 19.492 [ms] (mean)
Time per request: 1.949 [ms] (mean, across all concurrent requests)
Transfer rate: 46384.34 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 8 249.5 0 19514
Processing: 4 12 19.8 10 754
Waiting: 4 11 19.8 10 754
Total: 4 19 250.4 10 19525
Percentage of the requests served within a certain time (ms)
50% 10
66% 11
75% 11
80% 12
90% 12
95% 15
98% 27
99% 134
100% 19525 (longest request)
使用GZIP壓縮后走Redis緩存壓測(cè)結(jié)果:
ab -n 100000 -c 10 http://localhost/gzipRedis
Finished 100000 requests
Document Length: 11091 bytes
Concurrency Level: 10
Time taken for tests: 194.927 seconds
Complete requests: 100000
Failed requests: 0
Total transferred: 1122000000 bytes
HTML transferred: 1109100000 bytes
Requests per second: 513.01 [#/sec] (mean)
Time per request: 19.493 [ms] (mean)
Time per request: 1.949 [ms] (mean, across all concurrent requests)
Transfer rate: 5621.09 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 12 410.4 0 19608
Processing: 3 7 20.0 4 802
Waiting: 3 7 19.9 4 801
Total: 3 19 410.9 4 19613
Percentage of the requests served within a certain time (ms)
50% 4
66% 9
75% 9
80% 9
90% 10
95% 10
98% 11
99% 19
100% 19613 (longest request)
總結(jié)
對(duì)比使用GZIP壓縮我們可以得出以下幾點(diǎn):
- 測(cè)試中10萬(wàn)請(qǐng)求在194S完成,緩存時(shí)間是100S,服務(wù)器端只做了二次查數(shù)據(jù)庫(kù)和GZIP壓縮然后存數(shù)Redis
- 兩次GZIP和之后的數(shù)據(jù)傳輸消耗資源可以忽略不計(jì)
- 未壓縮10萬(wàn)請(qǐng)求從Redis傳輸了8.6GB數(shù)據(jù)到WEB服務(wù)器,又從WEB服務(wù)器傳輸8.6GB給用戶(hù)瀏覽器,
- 壓縮10萬(wàn)請(qǐng)求從Redis傳輸了1GB數(shù)據(jù)到WEB服務(wù)器,又從WEB服務(wù)器傳輸1GB給用戶(hù)瀏覽器,節(jié)省數(shù)據(jù)傳輸15.2GB,節(jié)省率88%
- 未壓縮數(shù)據(jù)傳輸速度達(dá)到45M/S,壓縮后5.4M/S,節(jié)省帶寬88%
- 如果Redis中大JSON都使用GZIP壓縮理論上可以節(jié)省Redis內(nèi)存達(dá)到88%
- 因?yàn)橹苯邮褂胓zip返回,所有解壓計(jì)算在用戶(hù)瀏覽器端完成,不消耗服務(wù)器CPU資源
請(qǐng)求10萬(wàn)次數(shù)據(jù)傳輸流程
綜合上所述如里你的Redis緩存中存在大量的大Key,可能先達(dá)到瓶頸的不是Redis的讀寫(xiě)性能,很可能是你的帶寬,此時(shí)只需要簡(jiǎn)單的使用GZIP壓縮就能你給不僅節(jié)省88%的Redis內(nèi)存空間還大大減少了數(shù)據(jù)的傳輸量和節(jié)省了帶寬資源,而且還能使用的C端用戶(hù)的資源來(lái)解壓,這個(gè)ROI是非常高的。