Category rust

一个用rust写的类似于Skywalking/CAT的迷你trace PHP扩展

Category rust
Tag rust
Tag PHP
Posted on
View

1. 简介

最近在学习rust,恰好看到了skywalking的php扩展采用了rust编写。有用过Skywalking/CAT之类监控系统的同学应该知道,这类系统对我们开发工作帮助非常大,能够非常快的帮我们定位到问题的关键,比如说现在有一个api的请求响应非常慢,那我们就可以从系统提供的web ui中查询这个api请求的链路各个节点的耗时,从而精准的定位慢的关键。

image.png

但是这类系统搭建起来还是比较繁琐的,对于个人开发者或者一些小公司来说成本比较高,因此我在apache/skywalking-php的基础上对其进行精简和部分增强,去掉其上报到skywalking server的部分,将trace log写入到本地文件,在这个本地文件中会记录以下内容:

1. 调用CURL时,记录开始结束时间以及耗时,如果发生错误会将错误信息记录下来

{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "CURL",
	"name": "https://error.blog.fanscore.cn/a/57/",
	"payload": {
		"http_code": "0",
		"query": "k1=v1&k2=k2&k3=v3",
		"curl_error": "Could not resolve host: error.blog.fanscore.cn"
	},
	"start_time": "10:19:03.596", // 时间格式%H:%M:%S%.3f
	"end_time": "10:19:03.602",
	"duration_in_micro": 5988 // 耗时
}

{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "CURL",
	"name": "https://blog.fanscore.cn/a/57/",
	"payload": {
		"http_code": "200",
		"curl_error": "",
		"query": "k1=v1&k2=k2&k3=v3"
	},
	"start_time": "10:19:03.602",
	"end_time": "10:19:03.969",
	"duration_in_micro": 366647
}

2. 调用PDO函数时,记录开始结束时间以及耗时,如果发生错误会将错误信息记录下来

{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "PDO",
	"name": "__construct",
	"payload": {
		"result": "unknown",
		"dsn": "mysql:host=127.0.0.1;dbname=blog;charset=utf8mb4"
	},
	"start_time": "10:19:03.969",
	"end_time": "10:19:03.980",
	"duration_in_micro": 11175
}
{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "PDO",
	"name": "query",
	"payload": {
		"statement": "select * from article",
		"result": "object(PDOStatement)"
	},
	"start_time": "10:19:03.980",
	"end_time": "10:19:03.985",
	"duration_in_micro": 5471
}
{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "PDO_STATEMENT",
	"name": "fetchAll",
	"payload": {
		"query_string": "select * from article",
		"result": "array(3)"
	},
	"start_time": "10:19:03.985",
	"end_time": "10:19:03.985",
	"duration_in_micro": 25
}

3. 捕获PHP代码中的错误

{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "ERROR",
	"name": "E_WARNING: Undefined variable $undefined_value in /Users/orlion/workspace/nginx/www/ptrace/index.php on line 32",
	"payload": {},
	"start_time": "10:19:03.986",
	"end_time": "10:19:03.986",
	"duration_in_micro": 2
}

4. 捕获PHP代码中未捕获的异常

{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "EXCEPTION",
	"name": "Exception: test exception in /Users/orlion/workspace/nginx/www/ptrace/index.php on line 34",
	"payload": {
		"trace": "#0 {main}"
	},
	"start_time": "10:19:03.986",
	"end_time": "10:19:03.986",
	"duration_in_micro": 1
}

5. 请求结束后会记录请求开始结束时间、状态码、GET/POST参数

{
	"trace_id": "b89143d7-0fda-43d5-a688-397aef0ee3ef",
	"kind": "URL",
	"name": "/index.php",
	"payload": {
		"$_GET": "{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\"}",
		"$_POST": "[]",
		"method": "GET",
		"status_code": "200"
	},
	"start_time": "10:19:03.595",
	"end_time": "10:19:03.992",
	"duration_in_micro": 397178
}

2. 安装

  1. Requirement

很遗憾,目前只提供mac arm64版本,后续会编译出linux版本,但因为依赖的phper-framework/phper的库不支持windows,因此短期内恐怕不能提供windows版本了。

  1. 进入https://github.com/Orlion/minitrace/releases 下载编译好的扩展二进制文件到本地

  2. 假设第一步将扩展下载到了/tmp/minitrace-v0.1.0-macos-arm64.dylib,编辑php.ini配置文件加入以下配置

    [minitrace]
    ;加载我们的扩展
    extension=/tmp/minitrace-v0.1.0-macos-arm64.dylib
    ;将trace数据输出到/tmp/minitrace.log
    minitrace.log_file = /tmp/minitrace.log
    
  3. 重启fpm

3. 测试使用

编辑以下php文件

<?php

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://error.blog.fanscore.cn/a/57/?k1=v1&k2=k2&k3=v3#aaa');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://blog.fanscore.cn/a/57/?k1=v1&k2=k2&k3=v3#aaa');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

$host = '127.0.0.1';
$db   = 'blog';
$user = 'root';
$pass = '123456';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];
$pdo = new PDO($dsn, $user, $pass, $options);
$stm = $pdo->query('select * from article');
$rows = $stm->fetchAll();
foreach($rows as $row) {
    print_r($row);
}


var_dump($undefined_value);

throw new Exception('test exception');
?>

然后在浏览器中请求该文件,打开/tmp/minitrace.log就能看到如下输出:

{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"CURL","name":"https://error.blog.fanscore.cn/a/57/","payload":{"http_code":"0","query":"k1=v1&k2=k2&k3=v3","curl_error":"Could not resolve host: error.blog.fanscore.cn"},"start_time":"10:19:03.596","end_time":"10:19:03.602","duration_in_micro":5988}
{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"CURL","name":"https://blog.fanscore.cn/a/57/","payload":{"http_code":"200","curl_error":"","query":"k1=v1&k2=k2&k3=v3"},"start_time":"10:19:03.602","end_time":"10:19:03.969","duration_in_micro":366647}
{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"PDO","name":"__construct","payload":{"result":"unknown","dsn":"mysql:host=127.0.0.1;dbname=blog;charset=utf8mb4"},"start_time":"10:19:03.969","end_time":"10:19:03.980","duration_in_micro":11175}
{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"PDO","name":"query","payload":{"statement":"select * from article","result":"object(PDOStatement)"},"start_time":"10:19:03.980","end_time":"10:19:03.985","duration_in_micro":5471}
{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"PDO_STATEMENT","name":"fetchAll","payload":{"query_string":"select * from article","result":"array(3)"},"start_time":"10:19:03.985","end_time":"10:19:03.985","duration_in_micro":25}
{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"ERROR","name":"E_WARNING: Undefined variable $undefined_value in /Users/orlion/workspace/nginx/www/ptrace/index.php on line 32","payload":{},"start_time":"10:19:03.986","end_time":"10:19:03.986","duration_in_micro":2}
{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"EXCEPTION","name":"Exception: test exception in /Users/orlion/workspace/nginx/www/ptrace/index.php on line 34","payload":{"trace":"#0 {main}"},"start_time":"10:19:03.986","end_time":"10:19:03.986","duration_in_micro":1}
{"trace_id":"b89143d7-0fda-43d5-a688-397aef0ee3ef","kind":"URL","name":"/index.php","payload":{"$_GET":"{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\"}","$_POST":"[]","method":"GET","status_code":"200"},"start_time":"10:19:03.595","end_time":"10:19:03.992","duration_in_micro":397178}

...

阅读全文 »

rust所有权和借用中的一些case

Category rust
Tag rust
Posted on
View

前言

学习rust有一段时间了,也用rust写了两个小项目,过程中发现一些rust教程在所有权和引用这一章节的讲解还是不够丰富,有很多case没有讲到,对所有权和引用的理解不够深入,这就导致实际应用时经常卡在所有权和引用,后面查阅一些资料在社区请教一些大佬后才理解,因此将最近练习过程中遇到的一些所有权和引用方面的问题总结成本文,分享给大家,帮大家踩踩坑。

1. 所有权

let a = 1;
let b = a; // a拷贝给b
println!("{}", a); // 不会报错

a的值被拷贝给了b,a和b被存储在栈上,无需在堆上分配内存

let a = String::from("a");
let b = a;
println!("{}", a); // 会报错,上一行a的所有权转移给了b,a不能再使用了

新手在这里可能会产生疑问?当执行形如let b = a;这样的代码时,到底什么情况下发生拷贝,什么情况下转移所有权呢?问题的答案其实非常简单:

只要a实现了Copy trait,那么就会拷贝,如果没有实现则转移所有权

那么为什么不能拷贝呢?我们可以以String这个类型为例,String是一个复杂类型,由存储在栈上的堆指针、字符串长度、字符串容量组成。

我们假设这里也是拷贝,那么a和b都会持有这个堆指针,当变量离开作用域后,rust会自动清理堆内存,由于a和b都指向了同一位置,那么会释放两次,这就导致了bug。

因此rust这样解决问题:当a赋值给b后,rust认为a不再有效,因此a离开作用域之后不会二次释放,这就是把所有权从a转移到了b。a被赋值给b之后就失效了,因此不能再使用。

如果String实现了Copy trait,拷贝a给b时,把堆指针指向的数据也复制一遍,同时将新的堆指针给b,那么a和b就不会指向同一个位置,就不会二次释放,自然就不会发生二次释放的bug了。

以下类型实现了Copy trait * 所有整数类型,比如 u32 * 布尔类型,bool,它的值是 true 和 false * 所有浮点数类型,比如 f64 * 字符类型,char * 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是 * 不可变引用 &T,注意: 可变引用 &mut T 是不可以 Copy的(如果Copy相当于两个指针指向一个位置,又会出现上面的二次释放的问题了)

1.1 结构体

结构体所有权问题比较复杂,这里单独拿出来分析。

先看一个简单的

struct User {
    age:
}

let user1 = User {
    age: 100,
};

let user2 = user1;
println!("{:}", user1); // 会报错,因为User没有实现Copy trait,所以user1的所有权转移给了user2
println!("{:}", user1.sign_in_count); // 会报错,user1已经无法使用了

这里要注意,虽然user1分配在栈上,但它没有实现Copy trait,仍然会发生所有权的转移

再看看一个复杂的

struct User {
    username: String,
    age: i128,
}

let user1 = User {
    username: String::from("user1"),
    age: 100,
};

let user2 = User {
    username: user1.username,
    age: user1.age
};

println!("{}", user1.age); // 不会报错,age发生了copy,而非所有权转移,可以继续使用
println!("{}", user1.username); // 会报错,username发生了所有权的转移
println!("{:}", user1); // 会报错

这里需要注意的是结构体内部的字段发生所有权转移后,会导致结构体本身也无法继续使用。但是其内部发生copy的值还是可以继续使用的,也就是user1.age还能继续使用不会报错的原因。

1.2 Option 所有权转移问题

我们先明确一个规则: 只要Option<T>中的T实现了Copy trait,那么Option<T>就实现了Copy trait

let a = Some(String::from("hello world!"));
let b = a.unwrap();
let c = a.unwrap(); // 这里会报错

我们分析下报错的原因,首先看unwrap的源码

pub const fn unwrap(self) -> T {
    match self {
        Some(val) => val,
        None => unwrap_failed(),
    }
}

从上面可以看到,调用unwrap时,因为Option<String>没有实现Copy trait,所以a发生了所有权转移,a的所有权转移到了unwrap里,所以第二次调用unwrap时就会报错。

解决办法就是调用as_ref/as_mut或者将Option<String>换成Option<&String>,rust中引用默认实现了Copy trait,所以Opiton<&String>不会发生所有权转移 看下as_ref的源码:

pub const fn as_ref(&self) -> Option<&T> {
    match *self {
        Some(ref x) => Some(x),
        None => None,
    }
}

2. 引用

2.1 可变引用

只能可变的引用一个可变变量

let a = 1;
let b = &mut a; // 会报错,无法可变引用一个不可变变量

同一时刻只能存在一个可变引用

let mut a = 1;
let b = &mut a;
*b = 2;
println!("{}", a); // 会报错,可以将a理解成1的一个引用,因为下一行println!("{}", b);所以b这个可变引用的生命周期还未结束,那么此时如果使用a,则违反了可变引用与不可变引用不能同时存在的规则
println!("{}", b);

2.2 解引用

结构体解引用

let user = String::from("user");
let user_ref = &user;
let _user_1 = *user_ref; // 报错

第三行会报错:

error[E0507]: cannot move out of `*user_ref` which is behind a shared reference
  --> src/main.rs:30:19
   |
30 |     let _user_1 = *user_ref;
   |                   ^^^^^^^^^ move occurs because `*user_ref` has type `String`, which does not implement the `Copy` trait

这个报错看到有解释说不能解引用获取到所有权(String没有实现Copy trait只能将user的所有权转移给_user_1),但是这里将user的所有权转移给_user_1也并不会造成什么错误,所以我猜测是rust编译器限制了不能通过解引用间接转移所有权,只能直接转移。

这里还有个case:let _user_1 = &(*user_ref); 这种写法可以编译通过,猜测是编译器优化直接拷贝的引用,而不是先转移所有权再取引用。

3. 参考资料

...

阅读全文 »