You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

650 lines
32 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 44数据处理应用程序和数据如何打交道
你好,我是陈天。
我们开发者无论是从事服务端的开发,还是客户端的开发,和数据打交道是必不可少的。
对于客户端来说,从服务端读取到的数据,往往需要做缓存(内存缓存或者 SQLite 缓存),甚至需要本地存储(文件或者 SQLite
对于服务器来说跟数据打交道的场景就更加丰富了。除了数据库和缓存外还有大量文本数据的索引比如搜索引擎、实时的消息队列对数据做流式处理或者非实时的批处理对数据仓库data warehouse中的海量数据进行 ETLExtract、Transform and Load
![图片](https://static001.geekbang.org/resource/image/1c/eb/1c42e693f0848b4a389870f848ffaeeb.png?wh=1482x807)
今天我们就来讲讲如何用 Rust 做数据处理,主要讲两部分,如何用 Rust 访问关系数据库,以及如何用 Rust 对半结构化数据进行分析和处理。希望通过学习这一讲的内容,尤其是后半部分的内容,能帮你打开眼界,对数据处理有更加深刻的认识。
## 访问关系数据库
作为互联网应用的最主要的数据存储和访问工具,关系数据库,是几乎每门编程语言都有良好支持的数据库类型。
在 Rust 下,有几乎所有主流关系数据库的驱动,比如 [rust-postgres、rust-mysql-simple](https://github.com/sfackler/rust-postgres) 等,不过一般我们不太会直接使用数据库的驱动来访问数据库,因为那样会让应用过于耦合于某个数据库,所以我们会使用 ORM。
Rust 下有 [diesel](https://diesel.rs/) 这个非常成熟的 ORM还有 [sea-orm](https://github.com/SeaQL/sea-orm) 这样的后起之秀。diesel 不支持异步,而 sea-orm 支持异步,所以,有理由相信,随着 sea-orm 的不断成熟,会有越来越多的应用在 sea-orm 上构建。
如果你觉得 ORM 太过笨重,繁文缛节太多,但又不想直接使用某个数据库的驱动来访问数据库,那么你还可以用 [sqlx](https://github.com/launchbadge/sqlx)。sqlx 提供了对多种数据库Postgres、MySQL、SQLite、MSSQL的异步访问支持并且不使用 DSL 就可以对 SQL query 做编译时检查,非常轻便;它可以从数据库中直接查询出来一行数据,也可以通过派生宏自动把行数据转换成对应的结构。
今天,我们就尝试使用 sqlx 处理用户注册和登录这两个非常常见的功能。
### sqlx
构建下面的表结构来处理用户登录信息:
```sql
CREATE TABLE IF NOT EXISTS users
(
id INTEGER PRIMARY KEY NOT NULL,
email VARCHAR UNIQUE NOT NULL,
hashed_password VARCHAR NOT NULL
);
```
特别说明一下在数据库中存储用户信息需要非常谨慎尤其是涉及敏感的数据比如密码需要使用特定的哈希算法存储。OWASP 对密码的存储有如下[安全建议](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
1. 如果 Argon2id 可用,那么使用 Argon2id需要目标机器至少有 15MB 内存)。
2. 如果 Argon2id 不可用,那么使用 bcrypt算法至少迭代 10 次)。
3. 之后再考虑 scrypt / PBKDF2。
Argon2id 是 Argon2d 和 Argon2i 的组合Argon2d 提供了强大的抗 GPU 破解能力,但在特定情况下会容易遭受[旁路攻击](https://zh.wikipedia.org/wiki/%E6%97%81%E8%B7%AF%E6%94%BB%E5%87%BB)side-channel attacks而 Argon2i 则可以防止旁路攻击,但抗 GPU 破解稍弱。所以只要是编程语言支持 Argo2id那么它就是首选的密码哈希工具。
Rust 下有完善的 [password-hashes](https://github.com/RustCrypto/password-hashes) 工具,我们可以使用其中的 [argon2](https://github.com/RustCrypto/password-hashes/tree/master/argon2) crate用它生成的一个完整的包含所有参数的密码哈希长这个样子
```plain
$argon2id$v=19$m=4096,t=3,p=1$l7IEIWV7puJYJAZHyyut8A$OPxL09ODxp/xDQEnlG1NWdOsTr7RzuleBtiYQsnCyXY
```
这个字符串里包含了 argon2id 的版本19、使用的内存大小4096k、迭代次数3 次、并行程度1 个线程),以及 base64 编码的 salt 和 hash。
所以,当新用户注册时,我们使用 argon2 把传入的密码哈希一下,存储到数据库中;当用户使用 email/password 登录时,我们通过 email 找到用户,然后再通过 argon2 验证密码。数据库的访问使用 sqlx为了简单起见避免安装额外的数据库就使用 SQLite来存储数据如果你本地有 MySQL 或者 PostgreSQL可以自行替换相应的语句
有了这个思路,我们创建一个新的项目,添加相关的依赖:
```rust
[dev-dependencies]
anyhow = "1"
argon2 = "0.3"
lazy_static = "1"
rand_core = { version = "0.6", features = ["std"] }
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite"] }
tokio = { version = "1", features = ["full" ] }
```
然后创建 examples/user.rs添入代码你可以对照详细的注释来理解
```rust
use anyhow::{anyhow, Result};
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString},
Argon2, PasswordVerifier,
};
use lazy_static::lazy_static;
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
use std::env;
/// Argon2 hash 使用的密码
const ARGON_SECRET: &[u8] = b"deadbeef";
lazy_static! {
/// Argon2
static ref ARGON2: Argon2<'static> = Argon2::new_with_secret(
ARGON_SECRET,
argon2::Algorithm::default(),
argon2::Version::default(),
argon2::Params::default()
)
.unwrap();
}
/// user 表对应的数据结构,处理 login/register
pub struct UserDb {
pool: SqlitePool,
}
/// 使用 FromRow 派生宏把从数据库中读取出来的数据转换成 User 结构
#[allow(dead_code)]
#[derive(Debug, sqlx::FromRow)]
pub struct User {
id: i64,
email: String,
hashed_password: String,
}
impl UserDb {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
/// 用户注册:在 users 表中存储 argon2 哈希过的密码
pub async fn register(&self, email: &str, password: &str) -> Result<i64> {
let hashed_password = generate_password_hash(password)?;
let id = sqlx::query("INSERT INTO users(email, hashed_password) VALUES (?, ?)")
.bind(email)
.bind(hashed_password)
.execute(&self.pool)
.await?
.last_insert_rowid();
Ok(id)
}
/// 用户登录:从 users 表中获取用户信息,并用验证用户密码
pub async fn login(&self, email: &str, password: &str) -> Result<String> {
let user: User = sqlx::query_as("SELECT * from users WHERE email = ?")
.bind(email)
.fetch_one(&self.pool)
.await?;
println!("find user: {:?}", user);
if let Err(_) = verify_password(password, &user.hashed_password) {
return Err(anyhow!("failed to login"));
}
// 生成 JWT token此处省略 JWT token 生成的细节)
Ok("awesome token".into())
}
}
/// 重新创建 users 表
async fn recreate_table(pool: &SqlitePool) -> Result<()> {
sqlx::query("DROP TABLE IF EXISTS users").execute(pool).await?;
sqlx::query(
r#"CREATE TABLE IF NOT EXISTS users(
id INTEGER PRIMARY KEY NOT NULL,
email VARCHAR UNIQUE NOT NULL,
hashed_password VARCHAR NOT NULL)"#,
)
.execute(pool)
.await?;
Ok(())
}
/// 创建安全的密码哈希
fn generate_password_hash(password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
Ok(ARGON2
.hash_password(password.as_bytes(), &salt)
.map_err(|_| anyhow!("failed to hash password"))?
.to_string())
}
/// 使用 argon2 验证用户密码和密码哈希
fn verify_password(password: &str, password_hash: &str) -> Result<()> {
let parsed_hash =
PasswordHash::new(password_hash).map_err(|_| anyhow!("failed to parse hashed password"))?;
ARGON2
.verify_password(password.as_bytes(), &parsed_hash)
.map_err(|_| anyhow!("failed to verify password"))?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
let url = env::var("DATABASE_URL").unwrap_or("sqlite://./data/example.db".into());
// 创建连接池
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&url)
.await?;
// 每次运行都重新创建 users 表
recreate_table(&pool).await?;
let user_db = UserDb::new(pool.clone());
let email = "tyr@awesome.com";
let password = "hunter42";
// 新用户注册
let id = user_db.register(email, password).await?;
println!("registered id: {}", id);
// 用户成功登录
let token = user_db.login(email, password).await?;
println!("Login succeeded: {}", token);
// 登录失败
let result = user_db.login(email, "badpass").await;
println!("Login should fail with bad password: {:?}", result);
Ok(())
}
```
在这段代码里,我们把 argon2 的能力稍微包装了一下,提供了 `generate_password_hash``verify_password` 两个方法给注册和登录使用。对于数据库的访问,我们提供了一个连接池 SqlitePool便于无锁访问。
你可能注意到了这句写法:
```rust
let user: User = sqlx::query_as("SELECT * from users WHERE email = ?")
.bind(email)
.fetch_one(&self.pool)
.await?;
```
是不是很惊讶,一般来说,这是 ORM 才有的功能啊。没错,它再次体现了 Rust trait 的强大:我们并不需要 ORM 就可以把数据库中的数据跟某个 Model 结合起来,只需要在查询时,提供想要转换成的数据结构 T: FromRow 即可。
看 query\_as 函数和 FromRow trait 的定义([代码](https://docs.rs/sqlx-core/0.5.9/src/sqlx_core/query_as.rs.html#157-160)
```rust
pub fn query_as<'q, DB, O>(sql: &'q str) -> QueryAs<'q, DB, O, <DB as HasArguments<'q>>::Arguments>
where
DB: Database,
O: for<'r> FromRow<'r, DB::Row>,
{
QueryAs {
inner: query(sql),
output: PhantomData,
}
}
pub trait FromRow<'r, R: Row>: Sized {
fn from_row(row: &'r R) -> Result<Self, Error>;
}
```
要让一个数据结构支持 FromRow很简单使用 sqlx::FromRow 派生宏即可:
```rust
#[derive(Debug, sqlx::FromRow)]
pub struct User {
id: i64,
email: String,
hashed_password: String,
}
```
希望这个例子可以让你体会到 Rust 处理数据库的强大和简约。我们用 Rust 写出了 Node.js / Python 都不曾拥有的直观感受。另外sqlx 是一个非常漂亮的 crate有空的话建议你也看看它的源代码开头介绍的 sea-orm底层也是使用了 sqlx。
**特别说明**,以上例子如果运行失败,可以去[GitHub](https://github.com/tyrchen/geektime-rust/tree/master/44_data_processing/data)上把 example.db 拷贝到本地 data 目录下,然后运行。
## 用 Rust 对半结构化数据进行分析
在生产环境中,我们会累积大量的半结构化数据,比如各种各样的日志、监控数据和分析数据。
以日志为例,虽然通常会将其灌入日志分析工具,通过可视化界面进行分析和问题追踪,但偶尔我们也需要自己写点小工具进行处理,一般,会用 Python 来处理这样的任务,因为 Python 有 pandas 这样用起来非常舒服的工具。然而pandas 太吃内存,运算效率也不算高。有没有更好的选择呢?
在第 6 讲我们介绍过 [polars](https://github.com/pola-rs/polars),也用 polars 和 [sqlparser](https://github.com/sqlparser-rs/sqlparser-rs) 写了一个处理 csv 的工具,其实 polars 底层使用了 [Apache arrow](https://arrow.apache.org)。如果你经常进行大数据处理,那么你对列式存储([columnar datastore](https://en.wikipedia.org/wiki/Column-oriented_DBMS))和 [Data Frame](https://pandas.pydata.org/pandas-docs/stable/user_guide/dsintro.html#dataframe) 应该比较熟悉arrow 就是一个在内存中进行存储和运算的列式存储,它是构建下一代数据分析平台的基础软件。
由于 Rust 在业界的地位越来越重要Apache arrow 也构建了完全用 [Rust 实现的版本](https://github.com/apache/arrow-rs),并在此基础上构建了高效的 in-memory 查询引擎 [datafusion](https://github.com/apache/arrow-datafusion) ,以及在某些场景下可以取代 Spark 的分布式查询引擎 [ballista](https://github.com/apache/arrow-datafusion/blob/master/ballista/README.md)。
Apache arrow 和 datafusion 目前已经有很多重磅级的应用,其中最令人兴奋的是 [InfluxDB IOx](https://github.com/influxdata/influxdb_iox),它是[下一代的 InfluxDB 的核心引擎](https://www.influxdata.com/blog/announcing-influxdb-iox/)。
来一起感受一下 datafusion 如何使用:
```rust
use datafusion::prelude::*;
use datafusion::arrow::util::pretty::print_batches;
use datafusion::arrow::record_batch::RecordBatch;
#[tokio::main]
async fn main() -> datafusion::error::Result<()> {
// register the table
let mut ctx = ExecutionContext::new();
ctx.register_csv("example", "tests/example.csv", CsvReadOptions::new()).await?;
// create a plan to run a SQL query
let df = ctx.sql("SELECT a, MIN(b) FROM example GROUP BY a LIMIT 100").await?;
// execute and print results
df.show().await?;
Ok(())
}
```
在这段代码中,我们通过 CsvReadOptions 推断 CSV 的 schema然后将其注册为一个逻辑上的 example 表,之后就可以通过 SQL 进行查询了,是不是非常强大?
下面我们就使用 datafusion来构建一个 Nginx 日志的命令行分析工具。
### datafusion
在这门课程的 [GitHub repo](https://github.com/tyrchen/geektime-rust/tree/master/44_data_processing/fixtures) 里,我放了个从网上找到的样本日志,改名为 nginx\_logs.csv注意后缀需要是 csv其格式如下
```bash
93.180.71.3 - - "17/May/2015:08:05:32 +0000" GET "/downloads/product_1" "HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
93.180.71.3 - - "17/May/2015:08:05:23 +0000" GET "/downloads/product_1" "HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
80.91.33.133 - - "17/May/2015:08:05:24 +0000" GET "/downloads/product_1" "HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)"
```
这个日志共有十个域,除了几个 “-”,无法猜测到是什么内容外,其它的域都很好猜测。
由于 nginx\_logs 的格式是在 Nginx 配置中构建的,所以,日志文件,并不像 CSV 文件那样有一行 header没有 header就无法让 datafusion 直接帮我们推断出 schema也就是说**我们需要显式地告诉 datafusion 日志文件的 schema 长什么样**。
不过对于 datafusuion 来说,创建一个 schema 很简单,比如:
```rust
let schema = Arc::new(Schema::new(vec![
Field::new("ip", DataType::Utf8, false),
Field::new("code", DataType::Int32, false),
]));
```
为了最大的灵活性,我们可以对应地构建一个简单的 schema 定义文件,里面每个字段按顺序对应 nginx 日志的字段:
```yaml
---
- name: ip
type: string
- name: unused1
type: string
- name: unused2
type: string
- name: date
type: string
- name: method
type: string
- name: url
type: string
- name: version
type: string
- name: code
type: integer
- name: len
type: integer
- name: unused3
type: string
- name: ua
type: string
```
这样,未来如果遇到不一样的日志文件,我们可以修改 schema 的定义,而无需修改程序本身。
对于这个 schema 定义文件,使用 [serde](https://github.com/serde-rs/serde) 和 [serde-yaml](https://github.com/dtolnay/serde-yaml) 来读取,然后再实现 From trait 把 SchemaField 对应到 datafusion 的 Field 结构:
```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum SchemaDataType {
/// Int64
Integer,
/// Utf8
String,
/// Date64,
Date,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct SchemaField {
name: String,
#[serde(rename = "type")]
pub(crate) data_type: SchemaDataType,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct SchemaFields(Vec<SchemaField>);
impl From<SchemaDataType> for DataType {
fn from(dt: SchemaDataType) -> Self {
match dt {
SchemaDataType::Integer => Self::Int64,
SchemaDataType::Date => Self::Date64,
SchemaDataType::String => Self::Utf8,
}
}
}
impl From<SchemaField> for Field {
fn from(f: SchemaField) -> Self {
Self::new(&f.name, f.data_type.into(), false)
}
}
impl From<SchemaFields> for SchemaRef {
fn from(fields: SchemaFields) -> Self {
let fields: Vec<Field> = fields.0.into_iter().map(|f| f.into()).collect();
Arc::new(Schema::new(fields))
}
}
```
有了这个基本的 schema 转换的功能,就可以构建我们的 nginx 日志处理结构及其功能了:
```rust
/// nginx 日志处理的数据结构
pub struct NginxLog {
ctx: ExecutionContext,
}
impl NginxLog {
/// 根据 schema 定义,数据文件以及分隔符构建 NginxLog 结构
pub async fn try_new(schema_file: &str, data_file: &str, delim: u8) -> Result<Self> {
let content = tokio::fs::read_to_string(schema_file).await?;
let fields: SchemaFields = serde_yaml::from_str(&content)?;
let schema = SchemaRef::from(fields);
let mut ctx = ExecutionContext::new();
let options = CsvReadOptions::new()
.has_header(false)
.delimiter(delim)
.schema(&schema);
ctx.register_csv("nginx", data_file, options).await?;
Ok(Self { ctx })
}
/// 进行 sql 查询
pub async fn query(&mut self, query: &str) -> Result<Arc<dyn DataFrame>> {
let df = self.ctx.sql(query).await?;
Ok(df)
}
}
```
仅仅写了 80 行代码,就完成了 nginx 日志文件的读取、解析和查询功能,其中 50 行代码还是为了处理 schema 配置文件。是不是有点不敢相信自己的眼睛?
datafusion/arrow 也太强大了吧?这个简洁的背后,是 10w 行 arrow 代码和 1w 行 datafusion 代码的功劳。
再来写段代码调用它:
```rust
#[tokio::main]
async fn main() -> Result<()> {
let mut nginx_log =
NginxLog::try_new("fixtures/log_schema.yml", "fixtures/nginx_logs.csv", b' ').await?;
// 从 stdin 中按行读取内容,当做 sql 查询,进行处理
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
while let Some(Ok(line)) = lines.next() {
if !line.starts_with("--") {
println!("{}", line);
// 读到一行 sql查询获取 dataframe
let df = nginx_log.query(&line).await?;
// 简单显示 dataframe
df.show().await?;
}
}
Ok(())
}
```
在这段代码里,我们从 stdin 中获取内容,把每一行输入都作为一个 SQL 语句传给 nginx\_log.query然后显示查询结果。
来测试一下:
```sql
echo "SELECT ip, count(*) as total, cast(avg(len) as int) as avg_len FROM nginx GROUP BY ip ORDER BY total DESC LIMIT 10" | cargo run --example log --quiet
SELECT ip, count(*) as total, cast(avg(len) as int) as avg_len FROM nginx GROUP BY ip ORDER BY total DESC LIMIT 10
+-----------------+-------+---------+
| ip | total | avg_len |
+-----------------+-------+---------+
| 216.46.173.126 | 2350 | 220 |
| 180.179.174.219 | 1720 | 292 |
| 204.77.168.241 | 1439 | 340 |
| 65.39.197.164 | 1365 | 241 |
| 80.91.33.133 | 1202 | 243 |
| 84.208.15.12 | 1120 | 197 |
| 74.125.60.158 | 1084 | 300 |
| 119.252.76.162 | 1064 | 281 |
| 79.136.114.202 | 628 | 280 |
| 54.207.57.55 | 532 | 289 |
+-----------------+-------+---------+
```
是不是挺厉害?我们可以充分利用 SQL 的强大表现力,做各种复杂的查询。不光如此,还可以从一个包含了多个 sql 语句的文件中,一次性做多个查询。比如我创建了这样一个文件 analyze.sql
```sql
-- 查询 ip 前 10 名
SELECT ip, count(*) as total, cast(avg(len) as int) as avg_len FROM nginx GROUP BY ip ORDER BY total DESC LIMIT 10
-- 查询 UA 前 10 名
select ua, count(*) as total from nginx group by ua order by total desc limit 10
-- 查询访问最多的 url 前 10 名
select url, count(*) as total from nginx group by url order by total desc limit 10
-- 查询访问返回 body 长度前 10 名
select len, count(*) as total from nginx group by len order by total desc limit 10
-- 查询 HEAD 请求
select ip, date, url, code, ua from nginx where method = 'HEAD' limit 10
-- 查询状态码是 403 的请求
select ip, date, url, ua from nginx where code = 403 limit 10
-- 查询 UA 为空的请求
select ip, date, url, code from nginx where ua = '-' limit 10
-- 复杂查询,找返回 body 长度的 percentile 在 0.5-0.7 之间的数据
select * from (select ip, date, url, ua, len, PERCENT_RANK() OVER (ORDER BY len) as len_percentile from nginx where code = 200 order by len desc) as t where t.len_percentile > 0.5 and t.len_percentile < 0.7 order by t.len_percentile desc limit 10
```
那么,我可以这样获取结果:
```sql
cat fixtures/analyze.sql | cargo run --example log --quiet
SELECT ip, count(*) as total, cast(avg(len) as int) as avg_len FROM nginx GROUP BY ip ORDER BY total DESC LIMIT 10
+-----------------+-------+---------+
| ip | total | avg_len |
+-----------------+-------+---------+
| 216.46.173.126 | 2350 | 220 |
| 180.179.174.219 | 1720 | 292 |
| 204.77.168.241 | 1439 | 340 |
| 65.39.197.164 | 1365 | 241 |
| 80.91.33.133 | 1202 | 243 |
| 84.208.15.12 | 1120 | 197 |
| 74.125.60.158 | 1084 | 300 |
| 119.252.76.162 | 1064 | 281 |
| 79.136.114.202 | 628 | 280 |
| 54.207.57.55 | 532 | 289 |
+-----------------+-------+---------+
select ua, count(*) as total from nginx group by ua order by total desc limit 10
+-----------------------------------------------+-------+
| ua | total |
+-----------------------------------------------+-------+
| Debian APT-HTTP/1.3 (1.0.1ubuntu2) | 11830 |
| Debian APT-HTTP/1.3 (0.9.7.9) | 11365 |
| Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21) | 6719 |
| Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.16) | 5740 |
| Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.22) | 3855 |
| Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17) | 1827 |
| Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.7) | 1255 |
| urlgrabber/3.9.1 yum/3.2.29 | 792 |
| Debian APT-HTTP/1.3 (0.9.7.8) | 750 |
| urlgrabber/3.9.1 yum/3.4.3 | 708 |
+-----------------------------------------------+-------+
select url, count(*) as total from nginx group by url order by total desc limit 10
+----------------------+-------+
| url | total |
+----------------------+-------+
| /downloads/product_1 | 30285 |
| /downloads/product_2 | 21104 |
| /downloads/product_3 | 73 |
+----------------------+-------+
select len, count(*) as total from nginx group by len order by total desc limit 10
+-----+-------+
| len | total |
+-----+-------+
| 0 | 13413 |
| 336 | 6652 |
| 333 | 3771 |
| 338 | 3393 |
| 337 | 3268 |
| 339 | 2999 |
| 331 | 2867 |
| 340 | 1629 |
| 334 | 1393 |
| 332 | 1240 |
+-----+-------+
select ip, date, url, code, ua from nginx where method = 'HEAD' limit 10
+----------------+----------------------------+----------------------+------+-------------------------+
| ip | date | url | code | ua |
+----------------+----------------------------+----------------------+------+-------------------------+
| 184.173.149.15 | 23/May/2015:15:05:53 +0000 | /downloads/product_2 | 403 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:30 +0000 | /downloads/product_2 | 200 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:33 +0000 | /downloads/product_2 | 403 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:34 +0000 | /downloads/product_2 | 403 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:52 +0000 | /downloads/product_2 | 200 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:43 +0000 | /downloads/product_2 | 200 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:42 +0000 | /downloads/product_2 | 200 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:46 +0000 | /downloads/product_2 | 200 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:18:05:10 +0000 | /downloads/product_2 | 200 | Wget/1.13.4 (linux-gnu) |
| 184.173.149.16 | 24/May/2015:18:05:37 +0000 | /downloads/product_2 | 403 | Wget/1.13.4 (linux-gnu) |
+----------------+----------------------------+----------------------+------+-------------------------+
select ip, date, url, ua from nginx where code = 403 limit 10
+----------------+----------------------------+----------------------+-----------------------------------------------------------------------------------------------------+
| ip | date | url | ua |
+----------------+----------------------------+----------------------+-----------------------------------------------------------------------------------------------------+
| 184.173.149.15 | 23/May/2015:15:05:53 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:33 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 5.153.24.140 | 23/May/2015:17:05:34 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 184.173.149.16 | 24/May/2015:18:05:37 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 195.88.195.153 | 24/May/2015:23:05:05 +0000 | /downloads/product_2 | curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3 |
| 184.173.149.15 | 25/May/2015:04:05:14 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 87.85.173.82 | 17/May/2015:14:05:07 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 87.85.173.82 | 17/May/2015:14:05:11 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 194.76.107.17 | 17/May/2015:16:05:50 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
| 194.76.107.17 | 17/May/2015:17:05:40 +0000 | /downloads/product_2 | Wget/1.13.4 (linux-gnu) |
+----------------+----------------------------+----------------------+-----------------------------------------------------------------------------------------------------+
select ip, date, url, code from nginx where ua = '-' limit 10
+----------------+----------------------------+----------------------+------+
| ip | date | url | code |
+----------------+----------------------------+----------------------+------+
| 217.168.17.150 | 01/Jun/2015:14:06:45 +0000 | /downloads/product_2 | 200 |
| 217.168.17.180 | 01/Jun/2015:14:06:15 +0000 | /downloads/product_2 | 200 |
| 217.168.17.150 | 01/Jun/2015:14:06:18 +0000 | /downloads/product_1 | 200 |
| 204.197.211.70 | 24/May/2015:06:05:02 +0000 | /downloads/product_2 | 200 |
| 91.74.184.74 | 29/May/2015:14:05:17 +0000 | /downloads/product_2 | 403 |
| 91.74.184.74 | 29/May/2015:15:05:43 +0000 | /downloads/product_2 | 403 |
| 91.74.184.74 | 29/May/2015:22:05:53 +0000 | /downloads/product_2 | 403 |
| 217.168.17.5 | 31/May/2015:02:05:16 +0000 | /downloads/product_2 | 200 |
| 217.168.17.180 | 20/May/2015:23:05:22 +0000 | /downloads/product_2 | 200 |
| 204.197.211.70 | 21/May/2015:02:05:34 +0000 | /downloads/product_2 | 200 |
+----------------+----------------------------+----------------------+------+
select * from (select ip, date, url, ua, len, PERCENT_RANK() OVER (ORDER BY len) as len_percentile from nginx where code = 200 order by len desc) as t where t.len_percentile > 0.5 and t.len_percentile < 0.7 order by t.len_percentile desc limit 10
+----------------+----------------------------+----------------------+-----------------------------+------+--------------------+
| ip | date | url | ua | len | len_percentile |
+----------------+----------------------------+----------------------+-----------------------------+------+--------------------+
| 54.229.83.18 | 26/May/2015:00:05:34 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.4.3 | 2592 | 0.6342190216041719 |
| 54.244.37.198 | 18/May/2015:10:05:39 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.4.3 | 2592 | 0.6342190216041719 |
| 67.132.206.254 | 29/May/2015:07:05:52 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.2.29 | 2592 | 0.6342190216041719 |
| 128.199.60.184 | 24/May/2015:00:05:09 +0000 | /downloads/product_1 | urlgrabber/3.10 yum/3.4.3 | 2592 | 0.6342190216041719 |
| 54.173.6.142 | 27/May/2015:14:05:21 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.4.3 | 2592 | 0.6342190216041719 |
| 104.156.250.12 | 03/Jun/2015:11:06:51 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.2.29 | 2592 | 0.6342190216041719 |
| 115.198.47.126 | 25/May/2015:11:05:13 +0000 | /downloads/product_1 | urlgrabber/3.10 yum/3.4.3 | 2592 | 0.6342190216041719 |
| 198.105.198.4 | 29/May/2015:07:05:34 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.2.29 | 2592 | 0.6342190216041719 |
| 107.23.164.80 | 31/May/2015:09:05:34 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.4.3 | 2592 | 0.6342190216041719 |
| 108.61.251.29 | 31/May/2015:10:05:16 +0000 | /downloads/product_1 | urlgrabber/3.9.1 yum/3.2.29 | 2592 | 0.6342190216041719 |
+----------------+----------------------------+----------------------+-----------------------------+------+--------------------+
```
## 小结
今天我们介绍了如何使用 Rust 处理存放在关系数据库中的结构化数据,以及存放在文件系统中的半结构化数据。
虽然在工作中,我们不太会使用 arrow/datafusion 去创建某个“下一代”的数据处理平台,但拥有了处理半结构化数据的能力,可以解决很多非常实际的问题。
比如每隔 10 分钟扫描 Nginx / CDN以及应用服务器过去 10 分钟的日志,找到某些非正常的访问,然后把该用户/设备的访问切断一阵子。这样的特殊需求一般的数据平台很难处理需要我们自己撰写代码来实现。此时arrow/datafusion 这样的工具就很方便。
### 思考题
1. 请你自己阅读 diesel 或者 sea-orm 的文档,然后尝试把我们直接用 sqlx 构建的用户注册/登录的功能使用 diesel 或者 sea-orm 实现。
2. datafusion 不但支持 csv还支持 ndJSON / parquet / avro 等数据类型。如果你公司的生产环境下有这些类型的半结构化数据,可以尝试着阅读相关文档,使用 datafusion 来读取和查询它们。
感谢你的收听。恭喜你完成了第44次Rust学习打卡之旅马上就要结束啦我们下节课见。