zipkin原理解析和php代码实现(基于zipkin-api)
zipkin跟踪原理解析
- 主要原理: 核心参数为 trace_id , id(当前的span的id) ,和 parent_id(前一个span的id组成)。
- trace_id : 整个请求链的唯一id,只要请求的trace_id 相同,不管相隔多长时间,zipkin都会归类到同一个链路中
- id(span_id): 在同一个请求链路下的单个请求(跟踪)的唯一id,span_id一般是不相同的
- parent_id(parent_span_id): 本次请求的上一个请求,这个是用于跟踪请求链中的每个请求之间的联系,zipkin可根据这个上下级的id关系生成服务依赖关系图
- 可以通过下面的结构查看到跟踪关系(测试数据的111 调用 222 ,222 调用 333,此段数据可以在下面的示例代码中生成)
zipkin中存储的数据结构示例
[
{
"traceId": "b7794163e2432bf0",
"id": "d4976a1d6eb10c24",
"name": "service_aaaa",
"timestamp": 1559123832737078,
"duration": 100,
"localEndpoint": {
"serviceName": "1111"
},
"remoteEndpoint": {
"serviceName": "2222"
},
"debug": true
},
{
"traceId": "b7794163e2432bf0",
"parentId": "d4976a1d6eb10c24",
"id": "ba7f99539c4e2b53",
"name": "service_aaaa",
"timestamp": 1559123832791081,
"duration": 100,
"localEndpoint": {
"serviceName": "2222"
},
"remoteEndpoint": {
"serviceName": "3333"
},
"debug": true
}
]
zipkin-api的使用
-
zipkin官方提供了好多sdk,有java,go,php等,里面涉及到很多header传播必要参数,可是,我一直没有接入成功过
-
既然sdk无法成功,我们可以采用最原始的方法,调用zipkin的sdk去进行跟踪,还好,官方项目提供了详细的api: api地址,全部复制后放入在线swagger编辑器即可查看,有了api,我们只要按照api的要求,把供zipkin跟踪的必要参数通过http的形式发送到zipkin,就算大功告成了。
-
sdk 帮我们处理了相关参数的传播,主要包括 trace_id ,parent_span_id 的传播和自动组装,遇到问题很难调试(本人愚钝,一直没接入成功过)。当然,在理解zipkin的数据结构和原理的时候,这些自己处理也不是很难的事情
-
下面的示例代码简单的模拟向zipkin发送跟踪点的数据,基于,可参考下,生产环境能不能用不知道
<?php
namespace http;
/**
* 文档 https://raw.githubusercontent.com/apache/incubator-zipkin-api/master/zipkin2-api.yaml
* 用 https://editor.swagger.io/?_ga=2.121438216.1860594245.1559118783-1964203828.1559118783 打开
* Class Span
* @package http
*/
// 时区需要设置准确,否则zipkin 跟踪错误
ini_set('date.timezone',"Asia/Shanghai");
class Span{
/**
* 必须是16位或32位
* @var string
*/
private $spanId;
/**
* 名称,随便取,便于区分即可
* @var string
*
*/
private $name;
/**
* 必须是16位
* @var string
*/
private $traceId;
/**
* 必须是16位
* @var string
*/
private $parentId;
/**
* 必须是16位的数字,不能是科学计数法
* @var bool|string
*/
private $timestamp;
/**
* 消耗时间,不写系统会计算,但是需要提供 kind
* @var int
*/
private $duration;
private $debug;
/**
* [
* serviceName: "",
* ipv4: "",
* ipv6: "",
* port: ""
* ]
* @var array
*/
private $localEndpoint;
/**
* [ CLIENT, SERVER, PRODUCER, CONSUMER ]
* @var string
*/
private $kind;
/**
* 同 $localEndpoint
* @var array
*/
private $remoteEndpoint;
/**
*
* [
* timestamp: 16位数字, php的int没有16位,所以需要转化为string
* value: string
* ]
* @var array
*/
private $annotations;
/**
* tags 记录任何数组
* 满足 [string,string] 即可
* @var array
*/
private $tags;
/**
* Span 数据结构示例.
*/
// id: "352bff9a74ca9ad2"
// traceId: "5af7183fb1d4cf5f"
// parentId: "6b221d5bc9e6496c"
// name: "get /api"
// timestamp: 1556604172355737
// duration: 1431
// kind: "SERVER"
// localEndpoint:
// serviceName: "backend"
// ipv4: "192.168.99.1"
// port: 3306
// remoteEndpoint:
// ipv4: "172.19.0.2"
// port: 58648
// tags:
// http.method: "GET"
// http.path: "/api"
/**
* Span constructor.
*/
function __construct()
{
$this->name = 'default_service';
/**
* 时间,统一生成
*/
$this->timestamp = $this->decimalNotation(microtime(true)*1000*1000);
//$this->duration = 100;
$this->debug = true;
}
/**
* 将科学计数法转化为string型的数字
* @param $num
* @return bool|string
*/
function decimalNotation($num){
$parts = explode('E', $num);
if(count($parts) != 2){
return $num;
}
$exp = abs(end($parts)) + 3;
$decimal = number_format($num, $exp);
$decimal = rtrim($decimal, '0');
return substr(str_replace(",","", rtrim($decimal, '.')),0,16);
}
/**
* @param mixed $spanId
* @return Span
*/
public function setSpanId($spanId)
{
$this->spanId = $spanId;
return $this;
}
/**
* @param mixed $name
* @return Span
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* @param mixed $traceId
* @return Span
*/
public function setTraceId($traceId)
{
$this->traceId = $traceId;
return $this;
}
/**
* @param mixed $parentId
* @return Span
*/
public function setParentId($parentId)
{
$this->parentId = $parentId;
return $this;
}
/**
* @param mixed $timestamp
* @return Span
*/
public function setTimestamp($timestamp)
{
$this->timestamp = $timestamp;
return $this;
}
/**
* @param mixed $duration
* @return Span
*/
public function setDuration($duration)
{
$this->duration = $duration;
return $this;
}
/**
* @param mixed $debug
* @return Span
*/
public function setDebug($debug)
{
$this->debug = $debug;
return $this;
}
/**
* @param mixed $localEndpoint
* @return Span
*/
public function setLocalEndpoint($localEndpoint)
{
$this->localEndpoint = $localEndpoint;
return $this;
}
/**
* @param mixed $kind
* @return Span
*/
public function setKind($kind)
{
$this->kind = $kind;
return $this;
}
/**
* @param mixed $remoteEndpoint
* @return Span
*/
public function setRemoteEndpoint($remoteEndpoint)
{
$this->remoteEndpoint = $remoteEndpoint;
return $this;
}
/**
* @param mixed $annotations
* @return Span
*/
public function setAnnotations($annotations)
{
$this->annotations = $annotations;
return $this;
}
/**
* @param mixed $tags
* @return Span
*/
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
public function toArray()
{
$spanAsArray = [
'id' => (string) $this->spanId,
'name' => $this->name,
'traceId' => (string) $this->traceId,
'parentId' => $this->parentId ? (string) $this->parentId : null,
'timestamp' => $this->timestamp,
'duration' => $this->duration,
'debug' => $this->debug,
'localEndpoint' => $this->localEndpoint,
];
if ($this->kind !== null) {
$spanAsArray['kind'] = $this->kind;
}
if ($this->remoteEndpoint !== null) {
$spanAsArray['remoteEndpoint'] = $this->remoteEndpoint;
}
if (!empty($this->annotations)) {
$spanAsArray['annotations'] = $this->annotations;
}
if (!empty($this->tags)) {
$spanAsArray['tags'] = $this->tags;
}
return $spanAsArray;
}
/**
* @param $payload
* @param array $options
* @return bool
* @throws \Exception
*/
function send($url,$payload,$options=[])
{
$handle = curl_init($url);
curl_setopt($handle, CURLOPT_POST, 1);
curl_setopt($handle, CURLOPT_POSTFIELDS, $payload);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
$requiredHeaders = [
'Content-Type' => 'application/json',
'Content-Length' => strlen($payload),
];
$additionalHeaders = (isset($options['headers']) ? $options['headers'] : []);
$headers = array_merge($additionalHeaders, $requiredHeaders);
$formattedHeaders = array_map(function ($key, $value) {
return $key . ': ' . $value;
}, array_keys($headers), $headers);
curl_setopt($handle, CURLOPT_HTTPHEADER, $formattedHeaders);
if (isset($options['timeout'])) {
curl_setopt($handle, CURLOPT_TIMEOUT, $options['timeout']);
}
if (curl_exec($handle) !== false) {
$statusCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);
if ($statusCode !== 202) {
throw new \Exception(
sprintf('Reporting of spans failed, status code %d', $statusCode)
);
}
return true;
} else {
throw new \Exception(sprintf(
'Reporting of spans failed: %s, error code %s',
curl_error($handle),
curl_errno($handle)
));
}
}
}
/**
* 使用方式
*/
//
// const ENDPOINT_URL = 'http://127.0.0.1:9411/api/v2/spans';
//function sendSpan(){
// $trace_id = substr(md5(time()),0,16); //trace_id 一次请求的标志,traceid相同表示为同一个请求
// $span1_id = substr(md5(time()-3500),0,16); // 当前span 的id
// $span = new Span();
// $span->setSpanId($span1_id);
// $span->setTraceId($trace_id)->setLocalEndpoint(
// ["serviceName"=>"1111"] // 设置当前的entpoint
// )->setRemoteEndpoint(
// ["serviceName"=> "2222"] // 设置远程的entpoint
// )->setTags(
// ['http.code'=>200] // 设置tag
// );
// $resp = $span->send(self::ENDPOINT_URL,json_encode([$span->toArray()]),[]); //第一个跟踪的上传,本次span结束
//
//
// usleep(500); // 假设去处理其他的事情了
// $span2_id = substr(md5(time()-3400),0,16); // 第二个span的id
// $span2 = new Span(); // 模拟依赖关系,进行第二个跟踪的上传
// $span2->setSpanId($span2_id); // 第二个跟踪的id,注意和第一个不同
// $span2->setTraceId($trace_id)->setParentId($span1_id)->setLocalEndpoint(
// ["serviceName"=>"2222"] // 设置服务绑定第一个span
// )->setRemoteEndpoint(
// ["serviceName"=> "3333"]
// );
// $res = json_encode([$span2->toArray()]); // 发送第二个跟踪
// $resp = $span->send(self::ENDPOINT_URL,$res,[]);
// return $resp;
//} // 发送完成后可以去zipkin查看
zipkin截图
- 请求时间图
- 服务依赖关系图