前面我们介绍了Skywalkingphp是如何安装的。在本文中,我们将分析Skywalkingphp是如何实现拦截的。一、OpenTracing在分析代码之前,我们先来了解一下OpenTracing规范。OpenTracing规范用于解决分布式跟踪规范的问题。这样就保证了不管用什么语言开发,只要遵循规范,你写的程序就可以被跟踪。在这里我不打算过多谈论这方面的理论。有兴趣的同学可以百度下载。SkywalkingPhp也是按照OpenTracing规范实现的。贴个实际的例子:如果有下面的PHP代码$redis=newRedis();$redis->connect('127.0.0.1',6379);$redis->get('ok');得到的跟踪数据如下:{"application_instance":207,"pid":1639,"application_id":4,"version":6,"uuid":"bc12912e-1791-47b9-abe2-4dabda363d4a","段”:{“traceSegmentId”:“3062_15767727610001”,“isSizeLimited”:0,“跨度”:[{“spanId”:0,“parentSpanId”:-1,“startTime”:1576772761044,“operationName”:“cli”,"peer":"","spanType":0,"spanLayer":3,"componentId":2,"refs":[],"endTime":1576772761062,"isError":0},{"spanId":1,"parentSpanId":0,"startTime":1576772761059,"spanType":1,"spanLayer":5,"tags":[{"key":"db.type","value":"Redis"},{"key":"db.statement","value":""}],"componentId":30,"endTime":1576772761061,"operationName":"connect","peer":"127.0.0.1:6379","isError":0,"refs":[]},{"spanId":2,"parentSpanId":0,"startTime":1576772761061,"spanType":1,"spanLayer":5,"tags":[{"key":"db.type","value":"Redis"},{"key":"db.statement","value":"getok"}],"componentId":30,"endTime":1576772761061,"operationName":"get","peer":"127.0.0.1:6379","isError":0,"refs":[]}]},"globalTraceIds":["3062_15767727610001"]}拦截Redis的connect和get操作二、重点代码分析1、初始化任何PHP扩展都有模块启动函数和请求启动/关闭函数。我们可以从这里开始。先看模块启动函数://data_register_hashtable();REGISTER_INI_ENTRIES();/*如果你有INI条目,取消注释这些行*/if(SKY)({//Cli模式下没有处理if(strcasecmp("cli",sapi_module.name)==0&&cli_debug==0){returnSUCCESS;}//拦截用户执行函数ori_execute_ex=zend_execute_ex;zend_execute_ex=sky_execute_ex;//拦截内部执行函数ori_execute_internal=zend_execute_internal;zend_execute_internal=sky_execute_internal;//拦截curlzend_function*old_function;if((old_function=zend_hash_str_find_ptr),CG(cG(CG)"curl_exec",sizeof)c("curl_=)){orig_curl_exec=old_function->internal_function.handler;old_function->internal_function.handler=sky_curl_exec_handler;}如果((old_function=zend_hash_str_find_ptr(CG(function_table),"curl_setopt",sizeof("curl_setopt")-1))!=NULL){orig_curl_setopt=old_function->internal_function.handler;old_function->internal_function.handler=sky_curl_setopt_handler;}if((old_function=zend_hash_str_find_ptr(CG(function_table),"curl_setopt_array",sizeof("curl_setopt_array")-1))!=NULL){orig_curl_setopt_array=old_function->internal_function.handler;old_function->internal_function.handler=sky_curl_setopt_array_handler;}if((old_function=zend_hash_str_find_ptr(CG(function_table),"curl_close",sizeof("curl_close")-1))!=NULL){orig_curl_close=old_function->internal_function.handler;old_function->internal_function.handler=sky_curl_close_handler;}}returnSUCCESS;}/*}}}*/模型大概做了2件事情:1)、抓取执行次数//抓取用户执行执行次数ori_execute_ex=zend_execute_ex;zend_execute_ex=sky_execute_ex;//拦截内部执行函数ori_execute_internal=zend_execute_internal;zend_execute_internal=sky_execute_internal;PHP函数有两种,一种是用户态函数,即.php文件中的函数会执行zend_execute_ex;另一个是internalFunction,也就是PHP扩展写的函数,这个会通过zend_execute_internal来执行,这两个是函数指针,让每个模块自己拦截,这样Skywalking就可以拦截所有的函数调用。2)拦截curl调用zend_function*old_function;if((old_function=zend_hash_str_find_ptr(CG(function_table),"curl_exec",sizeof("curl_exec")-1))!=NULL){orig_curl_exec=old_function->internal_function.handler;old_function->internal_function.handler=sky_curl_exec_handler;}这里只有一个函数:curl_exec,先在函数表中搜索这个函数,保存起来,方便做统计信息后执行原来的逻辑,然后动态替换成自己的实现。我们看一下sky_curl_exec_handler的实现逻辑。这里的代码比较详细,大致思路是:获取当前执行的一些参数,然后按照格式组装OpenTraceing规范数据。zval函数名,curlInfo;zval参数[1];ZVAL_COPY(¶ms[0],zid);ZVAL_STRING(&function_name,"curl_getinfo");call_user_function(CG(function_table),NULL,&function_name,&curlInfo,1,params);zval_dtor(&函数名);zval_dtor(¶ms[0]);//得到url信息zval*z_url=zend_hash_str_find(Z_ARRVAL(curlInfo),ZEND_STRL("url"));char*url_str=Z_STRVAL_P(z_url);如果(strlen(url_str)<=0){zval_dtor(&curlInfo);is_send=0;}php_url*url_info=NULL;如果(is_send==1){url_info=php_url_parse(url_str);如果(url_info->scheme==NULL||url_info->host==NULL){zval_dtor(&curlInfo);php_url_free(url_info);is_send=0;}}接下来是按OpenTracking规范组数据:if(is_send==1){array_init(&temp);add_assoc_long(&temp,"spanId",Z_LVAL_P(span_id)+1);add_assoc_long(&temp,"parentSpanId",0);l_millisecond=get_millisecond();millisecond=zend_atol(l_millisecond,strlen(l_millisecond));efree(l_millisecond);add_assoc_long(&temp,"startTime",毫秒);add_assoc_long(&temp,"spanType",1);add_assoc_long(&temp,"spanLayer",3);add_assoc_long(&temp,"componentId",COMPONENT_HTTPCLIENT);}然后执行原逻辑:`orig_curl_exec(INTERNAL_FUNCTION_PARAM_PASSTHRU);`2.sky_execute_ex的实现先了解类和函数信息:zend_function*zf=execute_data->func;constchar*class_name=(zf->common.scope!=NULL&&zf->common.scope->name!=NULL)?ZSTR_VAL(zf->common.scope->name):NULL;constchar*function_name=zf->common.function_name==NULL吗?NULL:ZSTR_VAL(zf->common.function_name);`然后根据不同的类拦截不同的函数方法:`if(class_name!=NULL){if(strcmp(class_name,"Predis\\Client")==0&&strcmp(function_name,"executeCommand")==0){//参数uint32_targ_count=ZEND_CALL_NUM_ARGS(execute_data);if(arg_count){zval*p=ZEND_CALL_ARG(execute_data,1);zval*id=(zval*)emalloc(sizeof(zval));zend_call_method(p,Z_OBJCE_P(p),NULL,ZEND_STRL("getid"),id,0,NULL,NULL);如果(Z_TYPE_P(id)==IS_STRING){operationName=(char*)emalloc(strlen(class_name)+strlen(Z_STRVAL_P(id))+3);componentId=COMPONENT_JEDIS;strcpy(操作名,类名);strcat(operationName,"->");strcat(operationName,Z_STRVAL_P(id));}efree(id);`3.总结SkywalkingPhp模块在启动时,会拦截PHP函数执行的几个函数指针,然后判断是否与Predis等几个类相关,如果是则拦截;SkywalingPhp也拦截了curl,但是这个是在模块启动的时候拦截的,以后每次请求都不会改变。为什么只需要在模块启动时拦截一次curl模块,而其他函数需要拦截动态执行函数的方式?因为其他一些类在PHP脚本中,加载模块的时候可能没有加载,所以不能静态拦截,只好动态拦截,每次执行一个函数,判断是否是我们关心的功能。kywalkingPhp系统一:分布式文件系统FastDFS介绍及安装RabbitMQ网络框架代码解析二:命令分发
