关于memcache client CPU占用率过高的问题排查和解决

4 十一月, 2010 (12:18) | 性能, 架构设计 繁体 English    DeliciOus    分享到新浪微博
作者: H.E. | 您可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明
网址: http://www.javabloger.com/article/about-memcache-client-cpu.html
豆瓣读书 向你推荐有关 性能架构设计、 类别的图书。

现象:
    在 手机浏览器的网页代理(类似 UCWeb) 项目运行的这段日子里,客户抱怨服务器在使用一段时间后会越来越慢,导致网页抓取和转换的运行的非常慢,到系统里面一查CPU的资源使用情况,被占用了90%以上。客户提供的硬件 可是 16G的内存 和 8核cpu ,这明显是代码 太不给力了。

 

排查:
    首先想到的是 代码流程中分块、排序和频繁的数据库操作 导致cpu占用过高,但是使用jmeter渗透测试后发现 存在缓存的流程也是不给力,于是将不相干代码剥离,最后定位到一句代码
        CacheObject cacheObject = cacheManager.get(key);  
    这句代码 核心是调用memcached 客户端的Memcache 的get方法于是精确定位到问题所在:Memcache 在取元素出的问题!!!

 

定位:
   1.一开始偷个懒 检查版本 server端为最新1.4.5版本 客户端为whalin 1.xx 版本 我觉得是客户端版本过老(依赖于jdk 1.4),所以切换客户端为最新的2.5.1 whalin以及net.spy版本的客户端 依然不给力。
   2.于是怀疑是memcached 分布式导致,但是现在是单一memcached server环境,又怀疑是网络延迟,把memcached server移到本地测试,不给力啊!!!
   3.继续排查,老老实实跟踪代码,下面简单介绍 get时 客户端做的事:
          // get SockIO obj using cache key
    SockIOPool.SockIO sock = pool.getSock(key, hashCode);   //根据 key和 hashcode 到sock池中取得sock
    String cmd = "get " + key + "\r\n";
    sock.write(cmd.getBytes());
    sock.flush();

底层操作还是 sock写入命令方式写入命令后 memcached server 会返回一些信息
正常情况是:

VALUE 1755631828 10 359137

内容

END

第一行 分别为关键字“VALUE key  flag  内容字节长度

第二行 为内容字节

第三行 为关键字 END

客户端代码 的流程是在一个死循环中 ,先读取第一行,拿到内容字节长度然后根据这个长度去读取内容碰到 end 时 再退出死循环。仔细检查并且通过jmeter测试多线程log记录循环次数发现这个流程没问题并没有发现死循环。嗯!问题不在此处?

 

4.              继续排查,发现代码如下

if ((flag & F_COMPRESSED) == F_COMPRESSED) {

                     try {

// read the input stream, and write to a byte array

                         // output stream since

                         // we have to read into a byte array, but we don't

                         // know how large it

// will need to be, and we don't want to resize it a

                         // bunch

                         GZIPInputStream gzi = new GZIPInputStream(

                                new ByteArrayInputStream(buf));

ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.length);

 

                         int count;

                         byte[] tmp = new byte[2048];

                         while ((count = gzi.read(tmp)) != -1) {

                            bos.write(tmp, 0, count);

                         }

 

                         // store uncompressed back to buffer

                         buf = bos.toByteArray();

                       gzi.close();

 

原来是压缩,这个压缩 是通过 实例memcacheclient时设置的

 

 mcc = new MemCachedClient(POOL_NAME);

            mcc.setCompressEnable(true);

   mcc.setCompressThreshold(8 * 1024);

 

默认是压缩的 而且是在 8*1024大小时进行压缩在get元素时如果为压缩的元素 则Flag即为 10 ,则通过gizinputstream拿到数据流测试了下 百度的首页 发现点问题

 

 

是否压缩

大小

10线程渗透测试 cpu占用率

Baidu

29.5K

37%-51%

Baidu

8.3K

71%-82%

  这边有一定的影响考虑加大 压缩临界点 ,
  由于客户的服务器的内存很大 16G 所以改代码为mcc.setCompressThreshold(640 * 1024);这样只有元素大于640K时才进行压缩,但是cpu占用还是很大!!!

5.  查了资料,发现序列化也是影响cpu占用的因素之一,于是检查代码,发现压缩代码后面的代码紧跟着就是 序列化/反序列化:

// we can only take out serialized objects

            if ((flag & F_SERIALIZED) != F_SERIALIZED) {

                if (primitiveAsString || asString) {

                   // pulling out string value

                  log.info("++++ retrieving object and stuffing into a string.");

                   o = new String(buf, defaultEncoding);

                   } else {

                         // decoding object

                         try {

                            o = NativeHandler.decode(buf, flag);

                         } catch (Exception e) {

 

                            // if we have an errorHandler, use its hook

                       if (errorHandler != null)

                        errorHandler.handleErrorOnGet(this, e, key);

 

              log.error("++++ Exception thrown while trying to deserialize for key: "

                                          + key, e);

                   throw new NestedIOException(e);

                         }

                   }

                  } else {

                     // deserialize if the data is serialized

                ContextObjectInputStream ois = new ContextObjectInputStream(

                new ByteArrayInputStream(buf), classLoader);

                try {

                         o = ois.readObject();

                   } catch (ClassNotFoundException e) {

 

                         // if we have an errorHandler, use its hook

                if (errorHandler != null)

                            errorHandler.handleErrorOnGet(this, e, key);

 

     log.error("++++ ClassNotFoundException thrown while trying to deserialize for key: "

                                       + key, e);

     throw new NestedIOException("+++ failed while trying to deserialize for key: "

               + key, e);

                     }

                  }

 

             }

 

为了证明序列化影响的程度测试了一个38K的字符串 一个 37.6K的对象,测试结果:

 

大小

10线程渗透测试 cpu占用率

对象

37.6K

22%-39%

字符串

38K

5%-11%

 

很给力,明白了,字符串不会反序列化,只是简单的转码,还是传输字符串效率高!!!!

 

 

6.              随后发现memcache client read内容的时候,是这样写的:

            int count = 0;

           while ( count < b.length ) {

              int cnt = in.read( b, count, (b.length – count) );

              count += cnt;

         }

 

一眼看上去 没什么问题 但是 我们再循环中加入一个计数标识,然后10个线程循环10次测试发现问题了 10*10 get元素时 应该是 执行100但是却执行了 1934 即每次取元素时 要读将近20次!!!

于是修改了下代码在循环中加入

if(a%3==0 || (b.length>2000 && cnt<1000)){

                  try {

                     Thread.sleep(20);

                  } catch (InterruptedException e) {

                     e.printStackTrace();

                  }

      }

3 或者 读取量小于1000B sleep 20 ms

这样做的好处是 让出CPU占用 减少循环计算的次数 留出更多的时间client一次能read更多的bytes最后对比测试了下

 

10线程渗透cpu占用率

Client源码

77%-91%

Client修改的代码

34%-45%

 

以上内容纯属本人虚构,如果上述有相同的事情,纯属巧合。

–end–

豆瓣读书  向你推荐有关 性能 架构设计、 类别的图书。



Creative Commons License
本文由J2ee企业顾问-黄毅创作,并已采用创作共用署名2.5中国大陆版许可证授权。

评论

评论也是有版权的!




5264