网站的流量越来越多大,对于使用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
其实有很多的开源框架都在采用该压缩算法:
- Avro为了对int、long类型数据压缩
 - Protocol Buffers的ZigZag编码
 - Thrift 也采用了ZigZag来压缩整数
 
6. 总结
如果确定压缩,务必挑选那些底层用 C 语言实现的算法库,其中Google 的 snappy 算法足以胜任java领域的大多数应用。据说阿里的 SOFA RPC 就使用了 snappy 作为协议层压缩算法。










