网站的流量越来越多大,对于使用nginx的优化变得非常重要,经过gzip(GNU-ZIP)压缩后页面大小可以变为原来的30%甚至更小,当用户浏览页面的时候速度会块得多。gzip 是规定的三种标准HTTP压缩格式之一,目前绝大多数的网站都在使 gzip 传 HTMLCSSJavaScript 等资源文件。

目录

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过的页面。

nginx 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来标识对压缩的支持。

nginx gzip带来的启示_Nginx_02

如果客户端支持gzip的解析,那么只要服务端能够返回gzip的文件就可以启用gzip了,通过nginx的配置来让服务端支持gzip

nginx gzip带来的启示_nginx_03

下面就是服务端开启了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 “才关闭。

nginx gzip带来的启示_压缩算法_04

这对启用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做为反向代理的时候启用:

  • off – 关闭所有的代理结果数据压缩
  • expired – 如果header中包含”Expires”头信息,启用压缩
  • no-cache – 如果header中包含”Cache-Control:no-cache”头信息,启用压缩
  • no-store – 如果header中包含”Cache-Control:no-store”头信息,启用压缩
  • private – 如果header中包含”Cache-Control:private”头信息,启用压缩
  • no_last_modified – 启用压缩,如果header中包含”Last_Modified”头信息,启用压缩
  • no_etag – 启用压缩,如果header中包含“ETag”头信息,启用压缩
  • auth – 启用压缩,如果header中包含“Authorization”头信息,启用压缩
  • any – 无条件压缩所有结果数据

gzip_min_length

设置允许压缩的页面最小字节数,Content-Length小于该值的请求将不会被压缩,默认值为0

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 表示到读到当前字节就结束。

nginx gzip带来的启示_nginx_05

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 作为协议层压缩算法。