釋放你九成的帶寬和內存:GZIP在解決Redis大Key方面的應用
引言
目前主流HTTP協議接口都是使用JSON格式做數據交換的,JSON數據格式有著結構簡單、可讀性高、跨平臺,易解析等優點,同時也存在著冗余數據會占用非常多的儲存空間的問題,這大大增加了JSON格式數據在存儲、傳輸過程中的性能消耗。所以對JSON格式數據壓縮后再傳輸、存儲就變的非常的有價值,如對JSON格式數據使用GZIP壓縮算法可以實現90%左右的壓縮率,更小的空間可以節省存儲成本和降低傳輸帶寬成本,本文介紹GZIP壓縮算法在優化Redis使用大KEY字段中的應用,通過簡單壓縮可以節省88%的內存空間和帶寬資源。
HTTP協議開啟GZIP
HTTP協議標準中是直接支持GZIP壓縮算法的,通過響應頭Content-Encoding: gzip來表明響應內容使用了GZIP壓縮,當客戶端收到數據后會使用GZIP算法對Body內容進行解壓。
★
RFC 1952 - IETF(互聯網工程任務組)標準化的Gzip文件格式規范,
★
RFC 2616 - HTTP 1.1 協議規范,其中包括對 Content-Encoding 頭的定義
在Nginx中可以通過 gzip on開啟GZIP壓縮功能:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
在Springboot中可以通過server.compression.enabled開啟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,開啟或關閉
- mime-types,壓縮的數據類型
- min-response-size,最小壓縮大小
測試GZIP
為了測試開啟GZIP前后的對比效果我們寫一個簡單的接口:
@GetMapping("/list")
public ResponseEntity<ApiResult> list() {
return renderOk(getData());
}
我們返回1000條JSON格式的用戶信息:
private List<UserVo> getData() {
return IntStream.range(1, 1000).mapToObj(x -> new UserVo(x,x+"+email@q63.com",x+"_公眾號",x+"_趙俠客")).collect(Collectors.toList());
}
@Data
@AllArgsConstructor
public class UserVo {
private Integer id;
private String username;
private String email;
private String trueName;
}
在未開啟GZIP前接口返回數據的大小是92.8KB, Content-Encoding為空,在開啟GZIP后接口返回的數據大小為11.5KB,Content-Encoding為gzip,接口返回數量降低了88%。
當然我們也可以在接口中通過手動添加content-encoding響應頭,然后通過手動調用GZIPOutputStream對返回數據進行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緩存壓縮
為了增加接口的響應速度我們通常會使用Redis當緩存,基本邏輯是先查Redis有沒有數據如果有直接返回,如果沒有會查數據庫,然后再存入Redis,以下是一個簡單的使用Redis當緩存的接口:
@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());
}
我們分析一下這樣個接口的基本數據流:
- 第一次從數據庫服務器查出92.8KB的數據傳輸到WEB服務器中
- 將92.8KB的數據從WEB服務器傳輸到Redis服務器中
- 后面如果命中緩存將92.8KB數據從Redis服務器傳輸到WEB服務器
- 最后將92.8KB數據從WEB服務器返回給用戶瀏覽器
使用Redis當緩存加速接口
使用ZIP優化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壓縮后的緩存接口
我們再分析一下以上使用GZIP壓縮后的數據傳輸:
- 第一次從數據庫服務器查出92.8KB的數據傳輸到WEB服務器中
- 將11.5KB的GZIP數據從WEB服務器傳輸到Redis服務器中
- 后面命中緩存將11.5KB數據從Redis服務器傳輸到WEB服務器
- 最后將11.KB數據從WEB服務器返回給用戶瀏覽器
GZIP壓縮后的Redis緩存
單次接口請求好像感覺不到這個 GZIP壓縮帶來的好處,接下來我們壓測一下看看會不會有差距。
壓力測試
壓測可以使用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 表示總共請求 100 次。
- -c 10 表示并發 10 個請求。
未壓縮走Redis壓縮結果:
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緩存壓測結果:
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)
總結
對比使用GZIP壓縮我們可以得出以下幾點:
- 測試中10萬請求在194S完成,緩存時間是100S,服務器端只做了二次查數據庫和GZIP壓縮然后存數Redis
- 兩次GZIP和之后的數據傳輸消耗資源可以忽略不計
- 未壓縮10萬請求從Redis傳輸了8.6GB數據到WEB服務器,又從WEB服務器傳輸8.6GB給用戶瀏覽器,
- 壓縮10萬請求從Redis傳輸了1GB數據到WEB服務器,又從WEB服務器傳輸1GB給用戶瀏覽器,節省數據傳輸15.2GB,節省率88%
- 未壓縮數據傳輸速度達到45M/S,壓縮后5.4M/S,節省帶寬88%
- 如果Redis中大JSON都使用GZIP壓縮理論上可以節省Redis內存達到88%
- 因為直接使用gzip返回,所有解壓計算在用戶瀏覽器端完成,不消耗服務器CPU資源
請求10萬次數據傳輸流程
綜合上所述如里你的Redis緩存中存在大量的大Key,可能先達到瓶頸的不是Redis的讀寫性能,很可能是你的帶寬,此時只需要簡單的使用GZIP壓縮就能你給不僅節省88%的Redis內存空間還大大減少了數據的傳輸量和節省了帶寬資源,而且還能使用的C端用戶的資源來解壓,這個ROI是非常高的。