网站的流量越来越多大,对于使用nginx的优化变得非常重要,经过gzip(GNU-ZIP)压缩后页面大小可以变为原来的30%甚至更小,当用户浏览页面的时候速度会块得多。gzip 是规定的三种标准HTTP压缩格式之一,目前绝大多数的网站都在使 gzip 传 HTML
、CSS
、JavaScript
等资源文件。
目录
1. gzip交互流程
2. 任何内容都可以开启gzip压缩?
3. nginx 开启 gzip
3.1 客户端是否支持
3.2 HTTP1.0的keep-alive不支持gzip
3.3 参数说明
4. 其他使用场景
4.1 Snappy
4.2 GZIPInputStream和Eureka
5. 整数压缩编码
5.1 zigzag
6. 总结
1. gzip交互流程
gzip 的压缩页面需要浏览器和服务器双方都支持,实际上就是服务器端压缩,传到浏览器后浏览器解压并解析,浏览器那里不需要我们担心,因为目前的巨大多数浏览器 都支持解析gzip过的页面。
2. 任何内容都可以开启gzip压缩?
如果消息的内容太大,就要考虑对消息进行压缩处理,这可以减轻网络带宽压力。但是这同时也会加重 CPU 的负担,因为压缩算法是 CPU 计算密集型操作,会导致操作系统的负载加重。所以,最终是否进行消息压缩,一定要根据业务情况加以权衡。
- 大文件,会消耗大量的cpu资源,且不一定有明显的效果
- 图片类型,原因:图片如jpg、png本身就会有压缩,所以就算开启gzip后,压缩前和压缩后大小没有多大区别,所以开启了反而会白白的浪费资源。(虽然zip和gzip算法不一样,但是可以看出压缩图片的价值并不大)
3. nginx 开启 gzip
Nginx实现资源压缩的原理是通过ngx_http_gzip_module模块拦截请求,并对需要做gzip的类型做gzip,ngx_http_gzip_module是Nginx默认集成的,不需要重新编译,直接开启即可。
3.1 客户端是否支持
并不是每个浏览器都支持gzip
的,如何知道客户端是否支持gzip
呢,请求头中的Accept-Encoding
来标识对压缩的支持。
如果客户端支持gzip
的解析,那么只要服务端能够返回gzip
的文件就可以启用gzip
了,通过nginx
的配置来让服务端支持gzip
。
下面就是服务端开启了gzip
的压缩方式:http节点中添加
http {
gzip on;
gzip_proxied any;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 4;
gzip_types application/javascript text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
gzip_disable 'MSIE [1-6]\.';
}
3.2 HTTP1.0的keep-alive不支持gzip
http1.0中默认是关闭的,需要在http头加入”Connection: Keep-Alive”,才能启用Keep-Alive。目前大部分浏览器都是用http1.1协议,http 1.1中默认启用Keep-Alive,如果加入”Connection: close “才关闭。
这对启用gzip后有什么影响呢?
在HTTP/1.0 中浏览器严重依赖Content-Length,浏览器通过这个字段来判断当前请求的数据是否已经全部接收。
在HTTP/1.1 中新增的 Transfer-Encoding: chunked
所对应的分块传输机制可以完美解决这类问题(无法知道实体内容的长度时),chunked表明实体内容不仅是gzip压缩的,还是分块传递的(边压缩边响应,这样可以显著提高 TTFB【
Time To First Byte
,首字节时间,WEB 性能优化重要指标】),当浏览器接收到一个长度为0的chunked时, 知道当前请求内容已全部接收。Nginx有对应的配置chunked的
属性chunked_transfer_encoding
,这个属性是默认开启的。
所以,在HTTP1.0中如果利用Nginx 启用了GZip,是无法获得 Content-Length 的,这导致HTTP1.0中开启持久链接和使用GZip只能二选一,故gzip_http_version默认值1.1。
3.3 参数说明
gzip | 开启或者关闭gzip模块,默认值为 off |
gzip_proxied | Nginx做为反向代理的时候启用:
|
gzip_min_length | 设置允许压缩的页面最小字节数, |
gzip_buffers | 设置用于处理请求压缩的缓冲区数量和大小,比如:32 4K表示按照内存页大小以4K为单位申请32倍的内存空间 |
gzip_http_version | 启用GZip所需的HTTP最低版本,默认值为 HTTP/1.1 |
gzip_comp_level | 压缩级别【1-9】,级别越高压缩率越大,当然压缩时间也就越长(传输快但比较消耗cpu),建议设置在4左右。 |
gzip_types | 需要压缩哪些响应的文件类型(MIME类型),多个空格隔开,不建议压缩图片 |
gzip_disable | 配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持) |
gzip_vary | 是否添加“Vary: Accept-Encoding”响应头 |
4. 其他使用场景
除了Nginx 利用gzip开启压缩外,在很多大型的rpc框架等通讯框架都有使用数据压缩技术。Java领域中也有非常多的压缩算法的实现:
- JDK GZIP ——下面重点介绍
- JDK deflate ——这是JDK中的又一个算法(zip文件用的就是这一算法)。它与gzip的不同之处在于,你可以指定算法的压缩级别,这样你可以在压缩时间和输出文件大小上进行平衡。可选的级别有0(不压缩),以及1(快速压缩)到9(慢速压缩)。它的实现是java.util.zip.DeflaterOutputStream / InflaterInputStream。
- LZ4压缩算法的Java实现——这是本文介绍的算法中压缩速度最快的一个,与最快速的deflate相比,它的压缩的结果要略微差一点
- snappy 下面重点介绍
4.1 Snappy
snappy 是Google开发的一个非常流行的数据压缩库,它旨在提供速度与压缩比都相对较优的压缩算法。它被Google用于许多内部项目,其中就包括BigTable,MapReduce和RPC。Google宣称它在这个库本身及其算法做了数据处理速度上的优化,作为代价,并没有考虑输出大小以及和其他类似工具的兼容性问题。Snappy特地为64位x86处理器做了优化,在单个Intel Core i7处理器内核上能够达到至少每秒250MB的压缩速率和每秒500MB的解压速率。
Snappy不是压缩率最高的,但是离最优的差距已经不是很大,但是速度和性能表现很优秀!
4.2 GZIPInputStream和Eureka
这是一个压缩比高的慢速算法,压缩后的数据适合长期使用。JDK中的java.util.zip.GZIPInputStream / GZIPOutputStream便是这个算法的实现。比如我们熟悉的Eureka的client side的Node的注册、更新、取消、过期和状态变化都有在使用gzip(GZIPInputStream ),通讯交互的实体是PeerEurekaNode。
//集群中的节点
public class PeerEurekaNode {
//HttpReplicationClient 可以视为一个EurekaHttpClient
private final HttpReplicationClient replicationClient;
public void heartbeat(final String appName, final String id,
final InstanceInfo info, final InstanceStatus overriddenStatus,
boolean primeConnection) throws Throwable {
if (primeConnection) {
// We do not care about the result for priming request.
replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
return;
}
......
}
}
//帮助管理维护集群节点
public class PeerEurekaNodes {
/**
* 集群节点集合
*/
private volatile List<PeerEurekaNode> peerEurekaNodes = Collections.emptyList();
/**
* 集群节点URL集合
*/
private volatile Set<String> peerEurekaNodeUrls = Collections.emptySet();
/**
* 定时任务线程池
*/
private ScheduledExecutorService taskExecutor;
}
/**
* Eureka specific GZIP content filter handler.
* EurekaHttpClient的过滤器
*/
public class DynamicGZIPContentEncodingFilter extends ClientFilter {
private static final String GZIP_ENCODING = "gzip";
private final EurekaServerConfig config;
public DynamicGZIPContentEncodingFilter(EurekaServerConfig config) {
this.config = config;
}
@Override
public ClientResponse handle(ClientRequest request) {
// If 'Accept-Encoding' is not set, assume gzip as a default
if (!request.getHeaders().containsKey(HttpHeaders.ACCEPT_ENCODING)) {
request.getHeaders().add(HttpHeaders.ACCEPT_ENCODING, GZIP_ENCODING);
}
if (request.getEntity() != null) {
Object requestEncoding = request.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
if (GZIP_ENCODING.equals(requestEncoding)) {
request.setAdapter(new GzipAdapter(request.getAdapter()));//看这里
} else if (isCompressionEnabled()) {
request.getHeaders().add(HttpHeaders.CONTENT_ENCODING, GZIP_ENCODING);
request.setAdapter(new GzipAdapter(request.getAdapter()));//看这里
}
}
ClientResponse response = getNext().handle(request);
String responseEncoding = response.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
if (response.hasEntity() && GZIP_ENCODING.equals(responseEncoding)) {
response.getHeaders().remove(HttpHeaders.CONTENT_ENCODING);
decompressResponse(response);//看这里
}
return response;
}
private boolean isCompressionEnabled() {
return config.shouldEnableReplicatedRequestCompression();
}
private static void decompressResponse(ClientResponse response) {
InputStream entityInputStream = response.getEntityInputStream();
GZIPInputStream uncompressedIS;
try {
uncompressedIS = new GZIPInputStream(entityInputStream);
} catch (IOException ex) {
try {
entityInputStream.close();
} catch (IOException ignored) {
}
throw new ClientHandlerException(ex);
}
response.setEntityInputStream(uncompressedIS);
}
private static final class GzipAdapter extends AbstractClientRequestAdapter {
GzipAdapter(ClientRequestAdapter cra) {
super(cra);
}
@Override
public OutputStream adapt(ClientRequest request, OutputStream out) throws IOException {
return new GZIPOutputStream(getAdapter().adapt(request, out));
}
}
}
5. 整数压缩编码
优秀开源的 RPC 消息协议往往对消息流量优化到了极致,比如:对于一个Integer型,一般使用 4 个字节来表示一个整数值。
经过研究发现,消息传递中大部分使用的整数值都是很小的非负整数(并非4 个字节,减少浪费),为此发明了一种叫变长整数varint。数值非常小时只需要使用一个字节,数值稍微大一点可以使用 2 个字节,再大一点就是 3 个字节。
实现原理是怎样的?
其原理就是保留每个byte的最高位的那个bit 来标识是否后面还有字节,1 表示需要继续读,0 表示到读到当前字节就结束。
5.1 zigzag
若按上面的实现,那如果是负数该怎么办呢?于是 zigzag 编码来了,专门用来解决负数问题。zigzag 编码将整数范围(包含负数)全部映射到自然数范围,然后再进行 varint 编码。
实现规则:zigzag 将负数编码成正奇数,正数编码成偶数。解码的时候遇到偶数直接除 2 就是原值,遇到奇数就加 1 除 2 再取负就是原值。
0 => 0
-1 => 1
1 => 2
-2 => 3
2 => 4
-3 => 5
3 => 6
其实有很多的开源框架都在采用该压缩算法:
6. 总结
如果确定压缩,务必挑选那些底层用 C 语言实现的算法库,其中Google 的 snappy 算法足以胜任java领域的大多数应用。据说阿里的 SOFA RPC 就使用了 snappy 作为协议层压缩算法。