Java中命令行调用大坑 java 命令行调用
liebian365 2024-10-17 14:05 28 浏览 0 评论
Java中命令行调用大坑
背景
我司有一个查询服务接口机,QPS大概40~50,调用方式是Java调用Shell命令行的方式,核心代码如下:
Process ps = Runtime.getRuntime().exec("your command");
ps.getInputStream();//处理输入流
以前用的好好的,最有一次直接把接口机堵死了,ssh都很难登陆上,登陆上去之后发现全是同一个java进程,起码得上百个,而且还有很多defunct僵尸进程,没办法只有重启服务器等了20分钟才恢复服务
后来才知道是接口机调用量上升了,因为有新的服务在调用,现在QPS大概在70~100左右,也就是说接口机只能承受住40~50 QPS,超过就会导致接口机堵住,后来经常堵住,从发现问题到不断尝试解决问题整个过程花了几个月的时间
承受不了更高的QPS意味着接口机该扩容了,但是有一个十分关键的问题是:接口机资源还够!每次刚开始堵住的时候都发现内存还剩5~6个G,而且随着流量不断打过来,内存100M,100M的降,最后降到只有几百M内存,而且全是同一个java进程,接口机便动弹不得
这明显是程序有问题而不是接口机资源不够的问题嘛,于是就开始排查程序
第一版
第一版程序是这么写的
try {
Process ps = Runtime.getRuntime().exec(vCmd);
InputStream in = ps.getInputStream();
in = new BufferedInputStream(in);
StringBuffer buffer = new StringBuffer();
while ((ptr = in.read()) != -1) {
buffer.append((char) ptr);
}
return buffer.toString();
} catch (IOException e) {
log.error(e);
}
return null;
就是很朴实无华的调用命令行而已,不要问我为什么非得调用命令行,因为这个服务很老旧,只能调命令行,调用命令行本来就是开销很大的操作,大量调用要尽量避免或改成其他调用
第二版(加线程池)
第一版出问题之后想的是加一个线程池,让调用shell的代码放到线程池里面去执行,让线程池来控制资源,于是诞生了第二版
- 定义线程池
public static ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
static {
// 线程池维护线程的最少数量
pool.setCorePoolSize(100);
// 线程池维护线程的最大数量
pool.setMaxPoolSize(600);
pool.setQueueCapacity(50);
pool.setKeepAliveSeconds(20);//除核心线程外的线程存活时间
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
pool.setThreadNamePrefix("THREAD-POOL-");
pool.initialize();
}
- 用线程池调用
Future<String> future = pool.submit(()-> {
try {
Process ps = Runtime.getRuntime().exec(vCmd);
InputStream in = ps.getInputStream();
in = new BufferedInputStream(in);
StringBuffer buffer = new StringBuffer();
while ((ptr = in.read()) != -1) {
buffer.append((char) ptr);
}
return buffer.toString();
} catch (IOException e) {
log.error(e);
}
return null;
});
try {
return future.get();
} catch (Exception e) {
log.error("thread pool excetion -> {}",e.getMessage());
future.cancel(true);
return null;
}
运行了一段时间发现调用量一上来接口机还是会堵住,于是便放弃了线程池(后来想想其实线程池应该是可以的,只是我们设置的线程数量不对,QPS最大100,每个查询都算做1s,那么设置100个线程就应该足够了,大多数时候每个查询可能就200ms~500ms左右,线程数肯定要小于100的,上面设置的最大600,等于没限制住)
第三版(加超时)
线程池方案失败之后我们一度认为是Java调用shell命令行不能设置超时导致的,如果某条查询超过了1s,那么就直接返回了,不继续查,于是有了第三版
这一版的核心在于调用ps.exitValue()这个非阻塞方法,它可以告诉我们shell命令是否执行完成,于是下面的while循环每隔100ms就去调用ps.exitValue()得知是否调用完成,如果超过了1s,则直接返回给调用者了,不进行调用
Process ps = Runtime.getRuntime().exec(vCmd);
long start = System.currentTimeMillis();
BufferedReader inputReader = null;
try {
inputReader = new BufferedReader(new InputStreamReader(ps.getErrorStream()));
boolean processFinished = false;
StringBuilder sb = new StringBuilder();
int cnt = 0;
while (System.currentTimeMillis() - start < 1000 && !processFinished) {
cnt ++;
processFinished = true;
long cuStart = System.currentTimeMillis();
try {
ps.exitValue();
} catch (IllegalThreadStateException e) {
// process hasn't finished yet
processFinished = false;
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
logger.error("Process, failed [" + e.getMessage() + "]", e);
}
}
}
if (!processFinished) {
logger.error(" timeout used " +(System.currentTimeMillis() - start));
return null;
}
String line;
StringBuilder rtn = new StringBuilder();
while (inputReader.ready() && ( (line = inputReader.readLine()) != null) ){
rtn.append(line);
}
return rtn.toString();
} catch (Exception e) {
String error = "Command process, failed [" + e.getMessage() + "]";
logger.error(error, e);
} finally {
if (inputReader != null) {
try {
inputReader.close();
} catch (IOException e) {
//ignore
}
}
}
return null;
后来发现这样这这是换汤不换药,接口机照常堵住,因为就算调用超时了返回给了调用者,但是命令行的操作还是在执行,一样会耗费系统资源,此时还加了shell脚本来监控进程数,如果太多就杀进程,但是一样效果不明显,shell脚本如下
#!/usr/bin/env bash
# 主线程ID
mainid=23105
# 最大线程阈值
max_size=5
while true;do
pids=(`ps aux | grep '/serviceShell/conf/logging.properties' | grep -v 'grep' | grep -v $mainid | awk '{print $2}'`)
pids_size=${#pids[@]}
if [ $pids_size -gt $max_size ];then
echo $(date +%F%n%T)
# 打印内存信息
free -m
echo "当前主id是 $mainid"
echo "当前线程个数为$pids_size 大于$max_size个,杀死线程:"
for pid in ${pids[@]}
do
if [[ "$pid" != "$mainid" ]];then
echo $pid
kill -9 $pid
fi
done
# 打印内存信息
free -m
fi
sleep 1
done
而且在堵住的时候发现这句话Process ps = Runtime.getRuntime().exec(vCmd);竟然要调用100多秒!后来想想这应该是调用量一上来,超过系统的负载,就会导致命令行调用很难申请到资源,一直在等操作系统排队处理
第四版(找到真正的瓶颈:缓冲区)
经过上面几版的瞎折腾,我们似乎遗忘了最不正常的问题:接口机资源还够为什么调用命令行会卡住?究竟什么才是Java调用命令行的瓶颈?经过网上搜寻,发现JDK文档上关于Process有这么一段说明
By default, the created subprocess does not have its own terminal or console. All its standard I/O (i.e. stdin, stdout, stderr) operations will be redirected to the parent process, where they can be accessed via the streams obtained using the methods getOutputStream(), getInputStream(), and getErrorStream(). The parent process uses these streams to feed input to and get output from the subprocess. Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, or even deadlock.
中文翻译:
默认情况下,创建的子进程没有自己的终端或控制台,子进程所有的标准IO操作会被重定向到父进程(也就是JVM),JVM里可以用getOutputStream()、getInputStream()和getErrorStream()来获取子进程的标准输出、输入和错误流
下面重点来了
由于有些本机平台仅针对标准输入和输出流提供有限的缓冲区大小,当标准输出或者标准错误输出写满缓存池时,程序没法继续写入,子进程没法正常退出。读写子进程的输出流或输入流迅速出现失败,则可能致使子进程阻塞,甚至产生死锁。
所以瓶颈很有可能就是缓冲区太小!
后来还发现了Java命令行的框架Apache Commons Exec:https://commons.apache.org/proper/commons-exec/,它可以在命令行执行之后新开线程去及时消费子进程输入、输出和错误流里的数据,避免缓冲区阻塞或死锁
基于上面两点,第四版改动如下
使用Apache Commons Exec框架调用命令行
long s = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
CommandLine commandline = CommandLine.parse(vCmd);
//看门狗,可设置超时
ExecuteWatchdog watchdog = new ExecuteWatchdog(1000);
DefaultExecutor exec = new DefaultExecutor();
exec.setExitValues(null);
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream,errorStream);
exec.setStreamHandler(streamHandler);
exec.setWatchdog(watchdog);
//调用命令行
exec.execute(commandline);
sb.append(" execute used " + (System.currentTimeMillis() - s + /*后面为0 赋值*/((s = System.currentTimeMillis())-s) ) );
//消费数据
String out = outputStream.toString("gbk");
String error = errorStream.toString("gbk");
sb.append(" stream used " + (System.currentTimeMillis() - s + /*后面为0 赋值*/((s = System.currentTimeMillis())-s) ) );
return out;
} catch (Exception e) {
logger.error(e.getMessage(),e);
sb.append(" exception info "+e.getMessage()+" used " + (System.currentTimeMillis() - s + /*后面为0 赋值*/((s = System.currentTimeMillis())-s) ) );
return "F";
} finally {
logger.info(" exec info " + sb.toString());
}
通过查看Apache Commons Exec的源码发现它就是每次调用就新开线程去处理三个流的
try {
streams.setProcessInputStream(process.getOutputStream());
streams.setProcessOutputStream(process.getInputStream());
streams.setProcessErrorStream(process.getErrorStream());
} catch (final IOException e) {
process.destroy();
throw e;
}
protected Thread createPump(final InputStream is, final OutputStream os, final boolean closeWhenExhausted) {
//此处新开线程
final Thread result = new Thread(new StreamPumper(is, os, closeWhenExhausted), "Exec Stream Pumper");
result.setDaemon(true);
return result;
}
通过上面的操作解决了单个JVM的缓冲区可能出现的阻塞/死锁问题
使用Nginx负载均衡
这一步很简单但是也很重要,这是解决瓶颈的根本,一个JVM缓冲区太小,咱来两个JVM,来三个JVM不就行了?Nginx负载均衡核心配置如下:
#user nobody;
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
worker_rlimit_nofile 65536;
error_log logs/error.log;
events {
use epoll;
worker_connections 65536;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
access_log logs/access.log main;
//后端三个JVM
upstream app_backend {
server 127.0.0.1:8081 weight=1 max_fails=3 fail_timeout=300;
server 127.0.0.1:8082 weight=1 max_fails=3 fail_timeout=300;
server 127.0.0.1:8083 weight=1 max_fails=3 fail_timeout=300;
}
server {
listen 80;
server_name localhost;
location ~ \/(app) {
proxy_set_header X-real-ip $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_connect_timeout 300;
proxy_read_timeout 300;
proxy_send_timeout 300;
proxy_pass http://app_backend;
client_max_body_size 10m;
proxy_http_version 1.1;
proxy_set_header Connection "";
limit_conn addr 100;
limit_rate 10000k;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 400 404 413 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
总结
如果是调用量很大的服务一般不建议采用Java调命令行,如果费要调用的话注意下面两个问题
- 单个JVM注意处理好子进程的输出、输入和错误三个流,避免单JVM缓冲区阻塞或者死锁,使用Apache Commons Exec
- 如果单个JVM支撑不了调用,并且服务器资源剩余很多的话可以考虑用Nginx负载均衡,将单机单JVM变成单机多JVM
相关推荐
- “版本末期”了?下周平衡补丁!国服最强5套牌!上分首选
-
明天,酒馆战棋就将迎来大更新,也聊了很多天战棋相关的内容了,趁此机会,给兄弟们穿插一篇构筑模式的卡组推荐!老规矩,我们先来看10职业胜率。目前10职业胜率排名与一周前基本类似,没有太多的变化。平衡补丁...
- VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符"
-
首先,程序中头文件的选择,要选择头文件,在文件中是没有对M_PI的定义的。选择:项目——>”XXX属性"——>配置属性——>C/C++——>预处理器——>预处理器定义,...
- 东营交警实名曝光一批酒驾人员名单 88人受处罚
-
齐鲁网·闪电新闻5月24日讯酒后驾驶是对自己和他人生命安全极不负责的行为,为守护大家的平安出行路,东营交警一直将酒驾作为重点打击对象。5月23日,东营交警公布最新一批饮酒、醉酒名单。对以下驾驶人醉酒...
- Qt界面——搭配QCustomPlot(qt platform)
-
这是我第一个使用QCustomPlot控件的上位机,通过串口精确的5ms发送一次数据,再将读取的数据绘制到图表中。界面方面,尝试卡片式设计,外加QSS简单的配了个色。QCustomPlot官网:Qt...
- 大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写
-
老友相聚,仗剑江湖!《大话西游2》2021全民PK季4月激燃打响,各PK玩法鏖战齐开,零门槛参与热情高涨。PK季期间,不仅各种玩法奖励丰厚,参与PK趣闻录活动,投稿自己在PK季遇到的趣事,还有机会带走...
- 测试谷歌VS Code AI 编程插件 Gemini Code Assist
-
用ClaudeSonnet3.7的天气测试编码,让谷歌VSCodeAI编程插件GeminiCodeAssist自动编程。生成的文件在浏览器中的效果如下:(附源代码)VSCode...
- 顾爷想知道第4.5期 国服便利性到底需优化啥?
-
前段时间DNF国服推出了名为“阿拉德B计划”的系列改版计划,截至目前我们已经看到了两项实装。不过关于便利性上,国服似乎还有很多路要走。自从顾爷回归DNF以来,几乎每天都在跟我抱怨关于DNF里面各种各样...
- 掌握Visual Studio项目配置【基础篇】
-
1.前言VisualStudio是Windows上最常用的C++集成开发环境之一,简称VS。VS功能十分强大,对应的,其配置系统较为复杂。不管是对于初学者还是有一定开发经验的开发者来说,捋清楚VS...
- 还嫌LED驱动设计套路深?那就来看看这篇文章吧
-
随着LED在各个领域的不同应用需求,LED驱动电路也在不断进步和发展。本文从LED的特性入手,推导出适合LED的电源驱动类型,再进一步介绍各类LED驱动设计。设计必读:LED四个关键特性特性一:非线...
- Visual Studio Community 2022(VS2022)安装图文方法
-
直接上步骤:1,首先可以下载安装一个VisualStudio安装器,叫做VisualStudioinstaller。这个安装文件很小,很快就安装完成了。2,打开VisualStudioins...
- Qt添加MSVC构建套件的方法(qt添加c++11)
-
前言有些时候,在Windows下因为某些需求需要使用MSVC编译器对程序进行编译,假设我们安装Qt的时候又只是安装了MingW构建套件,那么此时我们该如何给现有的Qt添加一个MSVC构建套件呢?本文以...
- Qt为什么站稳c++GUI的top1(qt c)
-
为什么现在QT越来越成为c++界面编程的第一选择,从事QT编程多年,在这之前做C++界面都是基于MFC。当时为什么会从MFC转到QT?主要原因是MFC开发界面想做得好看一些十分困难,引用第三方基于MF...
- qt开发IDE应该选择VS还是qt creator
-
如果一个公司选择了qt来开发自己的产品,在面临IDE的选择时会出现vs或者qtcreator,选择qt的IDE需要结合产品需求、部署平台、项目定位、程序猿本身和公司战略,因为大的软件产品需要明确IDE...
- Qt 5.14.2超详细安装教程,不会来打我
-
Qt简介Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开库,主要用来开发图形用户界面(GraphicalUserInterface,GUI)程序。Qt是纯C++开...
- Cygwin配置与使用(四)——VI字体和颜色的配置
-
简介:VI的操作模式,基本上VI可以分为三种状态,分别是命令模式(commandmode)、插入模式(Insertmode)和底行模式(lastlinemode),各模式的功能区分如下:1)...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- “版本末期”了?下周平衡补丁!国服最强5套牌!上分首选
- VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符"
- 东营交警实名曝光一批酒驾人员名单 88人受处罚
- Qt界面——搭配QCustomPlot(qt platform)
- 大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写
- 测试谷歌VS Code AI 编程插件 Gemini Code Assist
- 顾爷想知道第4.5期 国服便利性到底需优化啥?
- 掌握Visual Studio项目配置【基础篇】
- 还嫌LED驱动设计套路深?那就来看看这篇文章吧
- Visual Studio Community 2022(VS2022)安装图文方法
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)