一次JVM引起的服务器load过高问题排查经历

2016-11-08早上9:25分,公司运维的同事给我报警,称我们团队的一台服务器CPU的压力很大,zabbix的图显示:

1接到警报后,我在该服务器上使用w和top命令查看服务器的load和CPU情况,1分钟的load average在3-5之间,系统在高负荷运转,查看了我们的线上应用系统发现网页打开正常,但一个別列表的查询缓慢,chrome浏览器的network查看http接口的timing在十几秒。这部分列表走的是搜索引擎,我第一反应是会不会是搜索引擎出问题了?

于是我在服务器上查看tomcat日志,获取我们的应用调用搜索引擎的接口和参数,拼装后使用

curl -v '$url'

命令访问,发现访问的速度没有问题。那说明搜索引擎没有问题,那一定是我们的应用从搜索引擎返回值到展示给用户的部分处理有问题。

由于有大量用户反馈系统速度慢,于是我使用jps获取java进程的pid,随后使用:

jmap -dump:format=b,file=crm.bin [pid]

命令dump jvm的信息“保留犯罪现场”,然后重启线上应用。

启动应用后检测到服务器的load渐渐越来越高,最后保持在重启前的样子,于是使用

jstat -gc [pid] 3000

查看服务器每隔3秒的gc情况,发现3秒钟的full gc能够达到两次,jvm的状况十分异常。在启动服务器的这个时间里我利用eclipse MAT工具分析jmap dump文件信息,未诊断出异常。

后来我把原来jvm的Xms参数配置由1024m提高到3072m,发现双机的full gc次数分别稳定在0和2后再也没有full gc过了,但是young GC却十分频繁,3秒钟young GC次数可以达到15-20次。

到底是什么东西这么占内存???我使用jmap -histo pid查看,发现排在第一位的[C对象经常飙升到1个多G然后降到200MB,就是young GC频繁的原因,这通常是String类型的对象存在问题。

随后我使用

top -H -p [pid]

看到了如下结果:

2这一排的PID都占用了大量的CPU资源,于是随机取了一个PID为1077的进程,使用

printf "%x\n" 1077

转换成16进制的数字为435

随后使用jstack工具:

jstack [pid] | less

输出当前的堆栈信息,然后搜索435,发现如下内容:

3这块代码就是调用搜索引擎的代码,它处于RUNNABLE的状态,一直在执行中,说明这段代码一定是追魁祸首!打开代码看看!原来是这样的一段代码:

while ((line = in.readline()) != null) {
    result += line;
}

真相就是:循环里每次对String的操作都会生成新的String对象,不仅效率低下,而且大量浪费有限的内存空间。

将String改成StringBuilder,发布上线后load过高的问题解决。这就解释了为什么一般的页面打开速度正常,而搜索客户列表接口速度缓慢以及为什么char类型的数组占用大量内存。

TypeScript Error TS2304: Cannot find name ‘require’的解决方法

最近在研究ionic framework,在编译项目的时候出现一个报错:

TS2304: Cannot find name 'require'

原因是我们使用了ECharts,在引用它的使用写了这样的一句代码:

// 引入 ECharts 主模块
var echarts = require('echarts/lib/echarts');

require是在ES5的语法里常用的方式,现在我们写typescript了,没有这个玩意儿怎么办?
我找了很久,在github上找到了一个项目:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/requirejs/require.d.ts
把项目里的require.d.ts放到ionic的app目录下就解决了这个报错。

TypeScript Error TS2304: Cannot find name ‘Set’、’Promise’、’Map’.解决方法

最近在研究ionic framework,希望使用hybrid app开发我们团队的新版app报表模块。

在使用命令:

ionic serve

启动应用的时候,发现www/build目录下没有app.bundle.js这个文件,这个文件是typescript编译之后生成的一个js文件。看了命令行启动日志,发现有大量的typescript编译报错,以下就展示部分典型报错信息:

TypeScript error: /Users/wangzhiang/IdeaProjects/crm-ionic/node_modules/@angular/common/src/directives/ng_class.d.ts(81,35): Error TS2304: Cannot find name 'Set'.
TypeScript error: /Users/wangzhiang/IdeaProjects/crm-ionic/node_modules/@angular/common/src/facade/async.d.ts(34,33): Error TS2304: Cannot find name 'Promise'.
TypeScript error: /Users/wangzhiang/IdeaProjects/crm-ionic/node_modules/@angular/common/src/facade/lang.d.ts(11,17): Error TS2304: Cannot find name 'Map'.

经过stackoverflow和github的指点,我找到的解决方法如下:
先安装angular2依赖,–save-dev目的是把angular2依赖写入到package.json里:

npm install angular2 --save-dev

然后在项目的app目录下找到app.ts文件,在文件顶部加上:

///<reference path="../node_modules/angular2/typings/browser.d.ts"/>

这样之后就解决了typescript报错的问题。

利用gitlab的web hook和钉钉做代码review工作

     还记得在我读大学的时候有一门课叫做计算机安全,我们的教材《信息安全原理与应用(第四版)》中提到代码检查工作每千行代码发现的错误数量是10个,而其他的程序系统检查方法:需求复查为2.5个/1000行、设计复查为5个/1000行、集成测试为3个/1000行,接受测试为2个/1000行。
     那时候看完这个研究结果之后,代码检查的观念就根深蒂固地植入我的脑海里。工作了这么多年,一直不忘记代码review。团队也制定了很多的代码review工作的要求,但总是因为各种各样的原因做不好。最大的问题就是业务工作太忙,没有时间review。
     还记得上高三的时候,准备高考可谓是争分夺秒,我们都随身携带小笔记本,把重要的知识点记录在里面,蹲厕所、排队吃饭的时候掏出来复习,将细碎的时候利用起来也是一笔不小的财富。那我们就可以利用零碎的时间来做代码review工作,将bug扼杀在摇篮之中!
     webhook功能的作用是当开发者向git服务器push代码完成时,git会触发webhook里配置的url,即向我们配置的url用post的方式发送一个json格式的内容,这个内容里包含本次push的所有信息。众所周知,github和oschina这些代码管理的平台的项目设置里都提供webhook功能,我们公司使用的是gitlab来管理我们的代码,gitlab也拥有webhook功能。webhook功能给了我们无限的想象空间,可以用来自动部署代码,可以自动集成测试,当然,还有本文的主题,review提交的代码。
     我在公司里独自一人维护了一个叫做superb的thinkphp项目,这个系统提供项目管理、日报周报、接口文档、运维信息等功能,我觉得这个功能做到superb里是最合适不过了!
实现步骤:
第一步:根据gitlab的文档中描述的web hook post的json格式编写一个接受post内容的http接口。
第二步:接口处理完post的内容之后发送钉钉消息给指定人员。
具体实现:

1.接口代码(thinkphp框架):

<?php
namespace Home\Controller;

use Think\Controller;
use Think\Log;

class GitController extends Controller
{
    public function index()
    {
        $requestBody = file_get_contents('php://input');
        if (empty($requestBody)) {
            echo '发送失败';
            return false;
        }
        $content = json_decode($requestBody, true);

        $message = "";
        if ($content['total_commits_count'] > 0) {
            $message .= $content['user_name'] . '向' . $content['repository']['name'] . '项目的' . $content['ref'] . '分支push了' . $content['total_commits_count'] . '个commit:' . "\n";
            $count = 0;
            foreach ($content['commits'] as $commit) {
                $count++;
                $message .= $count . '. ' . $commit['author']['name'] . '在' . date('Y年m月d日 h:i:s', strtotime($commit['timestamp'])) . ' 提交的:' . $commit['message'] . "\n";
                $message .= '点击 ' . $commit['url'] . ' 查看本次commit diff' . "\n";
            }
            echo $message;
            Log::write($message);
        } else {
            $message .= $content['user_name'] . '在' . $content['repository']['name'] . '项目创建或者删除了一个分支:' . $content['ref'] . "\n";
        }
        echo D('Dingding', 'Service')->sendTextMsg('@all', $message);
    }
}
2.发送钉钉的代码:
<?php
namespace Home\Service;

use \Think\Model;
use \Think\Log;

class DingdingService extends Model
{
    private $_url = 'https://oapi.dingtalk.com/gettoken?corpid=你的&corpsecret=你的';

    public function sendTextMsg($touser, $text, $agentid = 你的)
    {
        $accessToken = $this->_getAccessToken();
        if ($accessToken == null) {
            return '获取accessToken失败';
        }
        $content = array(
            'touser' => $touser,
            'agentid' => $agentid,
            'msgtype' => 'text',
            'text' => array('content' => $text)
        );
        $url = 'https://oapi.dingtalk.com/message/send?access_token=' . $accessToken;
        $result = $this->_sendPost($url, $content);
        Log::write($result);
        return $result;
    }

    private function _getAccessToken()
    {
        $content = $this->_sendGet($this->_url);
        $json = json_decode($content, true);
        if ($json['errcode'] == 0) {
            $accessToken = $json['access_token'];
            return $accessToken;
        } else {
            Log::write('获取accessToken失败');
            return null;
        }
    }

    private function _sendPost($url, $data)
    {
        $ch = curl_init($url);
        $payload = json_encode($data);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }

    private function _sendGet($url)
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }
}
完成之后,把接口的url配置到gitlab的web hook上就可以坐等你的push消息了。结果是这样的:
HMHAWQ5%}[I]L(${HK6LNI2
点击最下方的diff url就可以打开gitlab的代码diff页面进行代码review工作。
其实这是个非常简单的功能,但是它给我带来的是完全不同的体验:
  1. 团队成员在push代码之后我第一时间收到钉钉的通知,我可以打开diff页面,立刻查看或者等会儿有时间了再看这个网页,我完全可以利用编译代码的零碎时间来做这项工作。记得这个功能上线第一天我就快速review出两个组员的代码bug,他们都万万没想到。
  2. 让团队中的初级程序员养成提交代码的时候自己先diff的习惯,并且写好comment。以前初级程序员常常乱提交代码,对代码分支造成严重的破坏,现在我告诉他们,只要他们提交代码boss就会看到,他们都非常小心谨慎了。
  3. 和其他团队的协作开发变得更加透明,其他团队的同学提交什么代码,功能说明我就可以看到。一起联调代码,他提交后我第一时间知道他接口搞定了;要求对方修改的bug对方提交了代码我就知道代码修复了。这两个体验不是我随口说的,现实中我真的遇到,感觉很爽!
这个功能的未来:
我希望将来在superb里面增加一个订阅功能,所有开发人员可以订阅自己关注的项目,订阅自己关心的人,这样对团队的代码管理将带来非常大的帮助。

-Dmaven.multiModuleProjectDirectory system propery is not set. Check $M2_HOME environment variable and mvn script match.[ERROR] Maven execution terminated abnormally (exit code 1)解决方法

今天给同事的Mac配置JAVA项目IDEA开发环境的时候遇到了一个问题:
-Dmaven.multiModuleProjectDirectory system propery is not set. Check $M2_HOME environment variable and mvn script match.[ERROR] Maven execution terminated abnormally (exit code 1)
Check了电脑的M2_HOME系统环境变量,都没有错误,于是在IDEA中File->Maven->Runner的VM options中增加配置:
-Dmaven.multiModuleProjectDirectory=$M2_HOME
保存解决问题。