2016年2月

在JAVA中实现PHP的gzuncompress

背景

在系统改造的时候,从php迁移到java;由于php中为了节省redis的内存,对缓存的数据做了 gzcompress 处理;为了能读取出数据,有两套方案:

  1. 刷数据,将redis中老的数据清理掉,去掉 gzcompress 的步骤(缺点:刷数据的时间,和读取代码上线的时间点无法吻合;数据的写入入口比较多,容易遗漏)

  2. java中读取的时候可以进行 gzuncompress

一些知识

知道这些知识候就能避免我在实现过程中遇到的很多问题。

PHP中的 gzcompress

This function compresses the given string using the ZLIB data format.
Note:
This is not the same as gzip compression, which includes some header data. See gzencode() for gzip compression.

一直以为 gzcompress 就是 gz 的压缩,php中使用的 zlib 来压缩,压缩完的结果中携带了头信息,直接使用 gz 解压是不认这种格式的。

JAVA中的 new String(byte[])

java.lang.StringCoding.StringDecoder 当在编码 byte[] 不能处理的时候会进行一些处理;所以说 (new String(compressedByte)).getBytes()compressedByte 并不一定会完全一样。

说到这里就可以看下 jedis 提供的接口了,刚开始我是使用的 String get(String key),于是由于上面的原因,当我用这个返回值 getBytes() 的时候就已经发生了变化。正确的使用方法应该是使用 byte[] get(byte[] key),由于比较繁琐,封装一下。

实现

    public static String get(Jedis jedis, String key) {
        byte[] byteKey = key.getBytes();
        byte[] element = jedis.get(byteKey);
        return new String(gzuncompress(element));
    }

    public static List<String> mget(Jedis jedis, List<String> keys) {
        byte[][] byteKeys = new byte[keys.size()][];
        for (int i = 0; i < keys.size(); i++) {
            byteKeys[i] = keys.get(i).getBytes();
        }
        List<byte[]> elements = jedis.mget(byteKeys);
        List<String> result = new ArrayList<>();
        for (byte[] element : elements) {
            result.add(new String(gzuncompress(element)));
        }
        return result;
    }

    public static byte[] gzuncompress(byte[] data) {
        byte[] unCompressed = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
        Inflater deCompressor = new Inflater();
        try {
            deCompressor.setInput(data);
            final byte[] buf = new byte[1024];
            while (!deCompressor.finished()) {
                int count = deCompressor.inflate(buf);
                bos.write(buf, 0, count);
            }
            unCompressed = bos.toByteArray();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            deCompressor.end();
        }

        return unCompressed;
    }

记一次内存泄露的debug过程

在压测 代码在线运行 工具的时候,发现当并发比较高的时候程序占用的内存会飙升,而且在中断压测之后,内存占用并没有回落。

第一个能想到的办法就是去看代码,但是大多数时候,自己写的代码,很难review出太多的问题;于是就借助golang的pprof来定位问题。

在程序中嵌入 pprof

package main

import (
    "tool.lu/sandbox-server/app"
    "net/http"
    _ "net/http/pprof"
    "strconv"
    "runtime"
)

func main() {
    debug()
    server := app.NewApp()
    server.Run(":9090")
}

func debug() {
    go func() {
        // 这边是由于通过pprof发现问题之后,加的一段debug代码;后面会讲到
        http.HandleFunc("/go", func(w http.ResponseWriter, r *http.Request) {
            num := strconv.FormatInt(int64(runtime.NumGoroutine()), 10)
            w.Write([]byte(num))
        })
        http.ListenAndServe("localhost:6060", nil)
    }()
}

通过 go tool 工具,查看内存分配最多的 top 5

go tool pprof http://localhost:6060/debug/pprof/heap
top 5

Screenshot 2016-02-07 at 17.55.39.png

查看代码,发现是 goroutine, ioPipe 的问题,一定是使用姿势出了问题:

Screenshot 2016-02-07 at 17.56.40.png

于是便有了上面的那段代码,curl http://localhost:6060/go,查看当前 go routine 的数量;于是猜测是因为 ioPipe 没有正确的关闭,引起 go routine 大量的产生,但是没有退出,耗费大量的内存;于是在异常退出前,主动关闭 ioPipe 的Reader,至此问题解决。

压测验证

本机

wrk -t5 -c20 -d10000s -s post.lua http://tool.lu

服务器

Screenshot 2016-02-07 at 16.24.24.png

curl http://localhost:6060/go

总结

这是一个很小的bug,由于写代码的时候不仔细,return之前没有关闭资源造成,但却要花费不少的力气去解决;对语言自己提供的工具链需要熟悉在熟悉,这样不管在解决问题或者避免问题的时候,都能节省很多的时间。