环境:
java version "1.8.0_271"
1. 现象
JsSanitizer.java
/**
* The name of the variable which holds reference to interruption checking
* class. To prevent collisions random suffix is added.
*/
final static String JS_INTERRUPTED_TEST = "__it";
private final static List<PoisonPil> POISON_PILLS = Arrays.asList(
// every 10th statements ended with semicolon put interrupt checking function
new PoisonPil(Pattern.compile("(([^;]+;){9}[^;]+(?<!break|continue);\\n(?![\\W]*(\\/\\/.+[\\W]+)*else))"),
JS_INTERRUPTED_FUNCTION + "();\n"),
// every (except switch) block start brace put interrupt checking function
new PoisonPil(Pattern.compile("(\\s*for\\s*\\([^\\{]+\\)\\s*\\{)"), JS_INTERRUPTED_FUNCTION + "();"), // for
// with
// block
new PoisonPil(Pattern.compile("(\\s*for\\s*\\([^\\{]+\\)\\s*[^\\{]+;)"), JS_INTERRUPTED_FUNCTION + "();"), // for
// without
// block
//
new PoisonPil(Pattern.compile("(\\s*([^\"]?function)\\s*[^\"}]*\\([\\s]*\\)\\s*\\{)"),
JS_INTERRUPTED_FUNCTION + "();"), // function except when enclosed in quotes
new PoisonPil(Pattern.compile("(\\s*while\\s*\\([^\\{]+\\{)"), JS_INTERRUPTED_FUNCTION + "();"),
new PoisonPil(Pattern.compile("(\\s*do\\s*\\{)"), JS_INTERRUPTED_FUNCTION + "();"));
String injectInterruptionCalls(final String str) {
String current = str;
for (final PoisonPil pp : POISON_PILLS) {
final StringBuffer sb = new StringBuffer();
final Matcher matcher = pp.pattern.matcher(current);
while (matcher.find()) {
matcher.appendReplacement(sb, ("$1" + pp.replacement));
}
matcher.appendTail(sb);
current = sb.toString();
}
return current;
}
POISON_PILLS中的正则表达式,在处理特殊js脚本时,会出现死循环,线程CPU飙升:
"http-nio-8103-exec-26" #898 daemon prio=5 os_prio=0 tid=0x00007f1370068000 nid=0x18a runnable [0x00007f125f3ec000]
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$CharProperty.match(Pattern.java:3790)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4274)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
...... (循环出现)
at delight.nashornsandbox.internal.JsSanitizer.injectInterruptionCalls(JsSanitizer.java:231)
at delight.nashornsandbox.internal.JsSanitizer.secureJsImpl(JsSanitizer.java:279)
at delight.nashornsandbox.internal.JsSanitizer.secureJs(JsSanitizer.java:258)
at delight.nashornsandbox.internal.NashornSandboxImpl.eval(NashornSandboxImpl.java:221)
at delight.nashornsandbox.internal.NashornSandboxImpl.eval(NashornSandboxImpl.java:203)
可供测试的js代码:
var PROPERTY_SET = 0x11;
function system_getString(view, position) {
var length = view.getUint16(position);
var strValue = '';
for (var i = 0; i < length; i++) {
strValue += String.fromCharCode(view.getUint8(position + 2 + i));
}
return strValue;
}
function system_getString(view, length, position) {
var length1 = length
var strValue = '';
for (var i = 0; i < length1; i++) {
strValue += String.fromCharCode(view.getUint8(position + i));
}
return strValue;
}
function decode(bytes) {
var uint8Array = new Uint8Array(bytes.length);
for (var i = 0; i < bytes.length; i++) {
uint8Array[i] = bytes[i] & 0xff;
}
var dataView = new DataView(uint8Array.buffer, 0);
var jsonMap = {};
var fHead = uint8Array[0]; // command
if (fHead == 0x01) {
var params = {};
var data = [];
var property = {};
property['key'] = 'testAdd';
property['value'] = system_getString(dataView, 7, 1); //test
//property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
data.push(property);
property = {};
property['key'] = 'testReading';
property['value'] = dataView.getInt32(15).toString(); //
//property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
data.push(property);
// property = {};
// property['key'] = 'test';
// property['value'] = dataView.getInt8(19).toString(); //
// //property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
// data.push(property);
// property = {};
// property['key'] = 'test';
// property['value'] = dataView.getInt8(20).toString(); //
// //property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
// data.push(property);
params['data'] = data;
jsonMap['params'] = params; //json
}
return JSON.stringify(jsonMap);
}
function encode(jsonObj) {
var bytes = [];
return bytes;
}
2 . 分析原因
jdk8问题,在jdk9中修复
TODO 还没使用jdk9验证
(另外,发现正则匹配耗时很长,如果在后端,需要提升TPS,可以考虑把注入函数环节去掉)
3. 优化方法
去掉注入函数环节
3.1. 在JsSanitizer的派生类中,直接返回格式化后的js脚本,不再注入函数调用即可。
@Override
protected String injectInterruptionCalls(final String str) {
return str;
}
3.2. 上述方法已经可以解决问题,另外,js脚本中不再注入检测中断的代码,那么,Java代码中,调用interrupt方法发出中断的代码,也可以优化下
在ThreadMonitor.java中,
void run() { try { // wait, for threadToMonitor to be set in JS evaluator thread synchronized (monitor) { if (threadToMonitor == null) { monitor.wait((maxCPUTime + 500) / MILI_TO_NANO); } if (threadToMonitor == null) { timedOutWaitingForThreadToMonitor = true; throw new IllegalStateException("Executor thread not set after " + maxCPUTime / MILI_TO_NANO + " ms"); } } final long startCPUTime = getCPUTime(); final long startMemory = getCurrentMemory(); while (!stop.get()) { final long runtime = getCPUTime() - startCPUTime; final long memory = getCurrentMemory() - startMemory; if (isCpuTimeExided(runtime) || isMemoryExided(memory)) { cpuLimitExceeded.set(isCpuTimeExided(runtime)); memoryLimitExceeded.set(isMemoryExided(memory)); threadToMonitor.interrupt(); // 在js中没有了注入的检测中断代码,此处在Java中调用interrupt方法,也不起作用,后面的等待50ms,也应该是无效的等待,降低解析效率 synchronized (monitor) { monitor.wait(50); } if (stop.get()) { return; } if (!scriptFinished.get()) { LOG.error(this.getClass().getSimpleName() + ": Thread hard shutdown!"); threadToMonitor.stop();// 可以考虑直接调用stop方法(虽然stop是deprecated) scriptKilled.set(true); } return; } else { } synchronized (monitor) { long waitTime = getCheckInterval(runtime); if (waitTime == 0) { waitTime = 1; } monitor.wait(waitTime); } } } catch (final Exception e) { throw new RuntimeException(e); } }
TODO Java中调用interrupt,在执行js代码时没有捕获到该异常,可能要看下nashorn源码了吧
参考:
https://blog.csdn.net/yhtppp/article/details/118785609 接上一篇
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6988218 JDK-6988218 : RegEx matcher loops ,在jdk9中修复
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=5050507 JDK-5050507 : Pattern.matches throws StackOverFlow Error
https://blog.csdn.net/shixing_11/article/details/5997567