[转]HHVM动态语法的性能问题分析

作者: 空气 分类: HHVM 发布时间: 2014-12-23 21:19 ė183 6没有评论

背景

在某业务线使用HHVM的过程中,发现有一些机器的HHVM CPU使用率异常于其他机器,使用率高出了一倍多,上线和流量高时CPU高出更多,所以针对此问题定位和分析是哪里造成了此问题。

线上问题分析定位

首先我们通过hhvm的监控接口(HHVM 的admin server访问,check-health和vm-tcspace)获取了unit、funcs、tcspace、load和queued等信息; 但是发现这类机器都有统一的问题,那就是在不上线的时候unit、func、tcspace涨幅过快,一般情况虽然有未触发文件,在请求时会编译+翻译小部分文件但是并未有过这么大幅度,几乎一天tcspace就超过了91%阀值,所以分析应该是动态函数的调用引起了此问题,由于线上使用了smarty模板,smarty中会使用eval和create_function,所以可能造成此问题,然后跟进线下的代码尝试是否可以复现此问题。

首先通过线上的datablock造成的core的栈来跟进问题栈内容如下:

而且上面的core查看hhvm的栈一般都是jit cache溢出的栈,而且这种栈很多而且都暴露到了一个位置;

首先通过业务线RD定位是某个模板触发后才会出现此种场景,而非所有模板,就是我们上面看到的栈的内容:xxxx.file.page.tpl.php

每次当落到这个栈的时候会出现这个问题,然后涨jit cache每次query都能命中肯定是和eval和createfunction有关; 后来通过栈中发现其中一个位置中有调用eval: xxx/libs/Smarty/sysplugins/smartyinternal_template.php:432 (renderTemplate函数)

smarty

然后将eval注释掉后发现jit cache不涨了,再打开继续增长,可以定位是此处造成了hhvm的jit cache增长; 然后打印了$this->compiled_template的值发现是如下内容:

template

红框位置是动态内容,所以每次调用请求是$this->compiled_template都会生成一个新的值,所以hhvm就判断了这个内容是有修改了,所以就重新编译+翻译了,这样jit cache就涨了上去,这段代码在每次query调用了7次,也会影响每次访问的性能,所以会造成请求变慢和cpu增加(由于有多次编译+翻译,这样比解释执行都要慢了),所以此种用法应该也和cpu使用率过高有关系;

解决方式:

$this->compiled_template里面的动态值,如时间,hash内容通过变量代替,变量在请求的调用入口处声明,可以让eval内的值识别到,这样就可以做到eval的值是固定的,并且可以做到jit也提高了执行效率; 但是php语法中其实不建议使用eval,因为此语法本来就会影响性能;

线上验证:

线上验证,去掉eval后,从之前的200ms-600ms的请求降低到了0.8ms,提升了约1000倍的性能(代码中多处调用了eval)

下面我们手动构造动态函数从源码的角度上分析此问题;

动态函数分析

首先我们不建议使用eval和create_function这类动态函数,因为这些动态函数不仅会影响php的性能,而且在hhvm中会造成编译+翻译每次都进行,这样就跟解释语言一样或者更差;

eval

Eval主要是构造一段php代码,然后直接执行,如:

上面的2中构造的eval的内容其实实现的结果是一致的,但是第一种就会造成jit cache上涨,而第二种则不会这是为什么呢?

我们将上面的代码展开看一下

第一种展开

上面的1413185434.4592这个值并不是固定的,而是每次调用php时获取的$time的动态值,所以每次eval的值就一样了,而这个中会构造一个伪主函数,而代码出现了不一致,则判断有了修改,所以就重新编译+翻译了,所以就造成了jit cache的上涨a.code和astub.code上涨;(由于jit 翻译是通过判断func是否有修改进行判定的,而编译阶段则是通过文件是否修改进行判断的)

第二种展开:

第二种执行的结果和第一种情况是一致的,而第二种则不会造成jit cache的上涨,这是未什么呢?因为第二种虽然首次进行eval也会翻译,但是其中的内容是固定的,所以每次判定时就不需要重新编译和翻译了;

解决方式:

所以我们在使用时需要注意,不要在eval中产出会动态出现的值,这样很危险,如果想使用动态值,最好在eval使用的上面比如时间类的动态内容通过变量的方式放在eval语句中,这样既可以使动态语法jit 又不上涨jit cache;

eval实现

下面是eval的执行时的调用栈:

iopEval实现:

eval1

evalFilename是eval的包含文件+行号作为eval 的文件名,如:

然后通过compileEvalString函数进行eval的编译;

compileEvalString实现:

eval2

上面篮框处是执行eval编译的代码; EvaledUnitsMap 是eval unit map类型(key是eval的代码,value是unit) s_evaledUnits 是保存已经eval 的unit 的静态变量;

然后每次编译前会去s_evaledUnits中进行insert如果insert成功则表明之前没有这段code,所以进入分支进行编译环节;

注: 如果是动态代码就每次都进入此环节进行编译,这样是相当影响性能的;

create_function

createfunction 和 eval是类似的都是动态语法,但是createfunction其实比eval使用起来更加可怕,如果不加使用限制不仅仅影响性能,而且还会造成内存泄露这样的问题,除了内存泄露还会造成jit cache的猛烈增长; 下面我们看下create_function的实现:

create_function

首先我们看下createfunction,这里只要进入函数,那么每次都会进行编译,这块还不如eval函数,至少eval函数每次还会在sevaledUnits中判断是否有此code,但是其实就算把createfunction 的内容也放入到一个map中去保存起来,只要是动态的构造都会出现问题的; 其实如果只是createfunction的话只是构造函数,但是只要调用话就会增加jit cache了,因为这里的调用每次的函数都是新的functionid,所以在jit判断时就找不到这个函数了,认为就更改了,所以就每次都翻译了(因为判断函数是否变更是和funcId和offset有关,而这里每次都会构造新的func,所以 jit也会每次增加),这样的调用是很危险的(每次都编译+翻译),性能会很低,比eval还要危险,所以不建议用createfunction; 其实createfunction也可以按照eval那种模式,将code和unit放到一个静态全局变量中,但是那样也防不住动态内容的,所以还是不建议用的

createfunction测试代码:

当我们每次调用上面的函数时,我们通过监控的几个指标: check-health 监控下funcs每次执行都会上涨 vm-tcspace 监控中会发现a.code和astubs.code 每次都会上涨 从上面观察看,其实每次function都编译,都在涨,而且jit也每次都在翻译,所以从原理和监控上来讲这个都是十分危险的;

综上结论

虽然eval和create_function方便了我们的使用,但是动态函数的动态性却极大的影响了性能; 但是从hhvm的角度上来讲eval 如果不是动态值,其实是可以用的(只要内容相同就不重新编译+翻译),但是createfunction建议大家放弃使用,因为create_function每次都会进行编译+翻译(不管内容是否相同);

后面我们会对eval和create_function加上监控和warning方便大家进行trace问题和监控此问题的使用;

原文链接:http://lamp.baidu.com/2014/10/17/dong-tai-yu-fa-de-xing-neng-wen-ti-fen-xi/

百度Lamp技术博客 原创作品,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。

分享此文到:

本文出自 空气的时光记事本,非注明转载皆为原创,转载时请注明出处及相应链接。

本文永久链接: http://www.liujingze.com/%e8%bd%achhvm%e5%8a%a8%e6%80%81%e8%af%ad%e6%b3%95%e7%9a%84%e6%80%a7%e8%83%bd%e9%97%ae%e9%a2%98%e5%88%86%e6%9e%90.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*