目标
被内存安全与极致性能的特性所吸引,前段时间简单了解了一下Rust 语言,然后就更加好奇起来。元旦也出不去就决定进一步学习一下。不想纠结于过多的语言细节,给自己定了一个明确的目标:开发出一个小工具(开源:Ratchet http/https 服务监控)。
这次完成了一些基本功能,对Rust语言有了初步理解,也收集了一些资料,本文做一个整理。然后继续深入学习和迭代完善。
文章难免有错误或疏漏,欢迎指正。如果对内容有意见或建议,或者新的参考资料或信息也希望能留下评论或发给我,我会进一步更新和完善,在这里先谢谢了。
最后,希望能够帮到所有想要初步认识Rust的人。
以大见小 - Rust快速实践(一)- 主观感受
以大见小 - Rust快速实践(二)- 语言部分细节
主观感受
好像同时在接触三种语言;
细节概念较复杂,学习曲线较陡且长;
编译很容易报错,但报错很清晰,便于查询解决问题;
编译较慢,发布程序较小;
三种语言
示例代码
...
let metrics = vec![
Gauge::new(grabber.name(), grabber.help()).unwrap(),
];
let descs = metrics
.iter()
.map(|c| c.desc().into_iter().cloned())
.fold(
Vec::new(),
|mut acc, ds| {
acc.extend(ds);
acc
},
);
...
这是项目中的一段代码,写(Copy)这段代码的时候,有一种三种语言混在一起的感觉。感觉信息量也比较多,不用深究,体会一下即可。虽然很容易区分,但是总有几种语言写在一起的感觉。
// 声明与赋值代码!
let metrics = ...
...
let descs = metrics
.iter()
...
... Vec::new()
// vec! 是宏,以叹号'!'结尾,
// 编译时会被展开为代码再进行最终编译!
... = vec![
// |c| ... 和 |mut acc, ds| {} 都是闭包,
// 当闭包中只有一条语句时{} 可以省略;
// 对了,因为没有分号,所以概念上好像应该叫表达式,
// 函数中最后一行没有分号表示这是函数返回值的表达式;
... |c| c.desc().into_iter().cloned()
... |mut acc, ds| {
acc.extend(ds);
acc
}
...
学习曲线
这几天学习下来,感觉上手还算容易,但学习曲线还是比较陡且长的。很多重要的概念要学习要理解,可以慢慢来,也只能慢慢来。比如(按照我的开发使用顺序以及自认为的优先级进行的排序):
- 包和crate;
- 表达式和语句;
- 宏和闭包;
- 所有权,借用与引用;
- 泛型、trait 和生命周期;
- 智能指针;
- 面向对象,并发,unsafe Rust;
- 等等......
本文不会也无法涉及如此多的概念,仅做一些简要的介绍。
编译
Rust会在编译时进行大量的静态检查,以尽可能确保运行时安全。也可能是因此编译速度不是很快,并且很容易报错。不过报错基本都很清晰,根据报错的信息线索,基本都能够较快的解决问题。当然,有一些涉及到对语言概念理解的问题则需要多一些时间,不过也更能帮助深入理解Rust这门语言。
error[E0277]: the size for values of type `(dyn Grabber + 'static)` cannot be known at compilation time
--> common/exporter/src/collector.rs:10:17
|
10 | pub fn register(grabber: Grabber)
| ^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `(dyn Grabber + 'static)`
= help: unsized locals are gated as an unstable feature
help: function arguments must have a statically known size, borrowed types always have a known size
最终令人比较欣慰的是,程序编译比Java和Golang可运行程序小很多。当然这会由于实现功能的不同而有所不同,但应该只是小多少的问题。
- Ratchet http/https 服务监控 Rust 实现版本
- Net_watcher http/https 服务监控 Golang 实现版本
$ ll -lh ./target/release/ratchet
... 13M ... ./target/release/ratchet*
$ ll -lh build/net_watcher
... 16M ... build/net_watcher*
至于性能,CPU和内存占用问题,以后进一步学习之后再整理一篇文章单独介绍。
初识Rust
内存安全与极致性能
对Rust预研引起兴趣,不仅仅是因为其生态中已经有了丰富的开源项目(GitHub 上有哪些值得关注的 Rust 项目?),甚至已经有使用Rust实现的操作系统!更吸引我的是,很多文章所说的Rust的内存安全与机制性能是否属实?是如何实现的?
我初步了解到有以下几点:
第一:编译时的静态检查:Rust编译时有严格的检查,以增强程序内存使用的安全性;
第二:泛型的单太化:泛型代码在编译时会被展开为静态的特定类型代码;因此如果调用了三个类型的泛型,编译时会被展开为静态的三套类型的特定代码,应该属于空间换时间的一种方法;
第三:Rust运行时没有GC:因此也就完全没有运行时GC进行回收时的任何开销;
运行时比较
Java 分代GC
Golang 三色GC
Rust 无GC
看网上很多的讨论就是关于Rust运行时没有GC的。Java的分代GC与Golang的三色GC虽然一直在尽可能优化,但是仍然需要在一些情况下完全暂停。这可能就是Rust性能高的很重要一点,Rust没有通常理解的独立的GC机制,所以也就不存在暂停的问题。
没有运行时GC并不是完全没有垃圾回收。而是通过语言级概念和语法的严格定义实现了可预定义的回收(Rust有GC,并且速度很快)。Rust要求开发人员对所有权以及生命周期等概念有清晰的理解,并在代码中也要有明确的定义和使用,也就不需要通过运行时GC而直接可以更安全的处理内存(语言级GC?);
- 通过生命周期管理(释放)栈内存
- 通过所有权管理(释放)堆内存(释放堆内存,Rust是怎么做的?所有权!)
以大见小,开发稳定并且高性能的程序
在进一步学习具体语法和开发等细节之前,我觉得更应该思考一下学习和实践应该从哪些方面入手。也是偶然一次,因为我曾研究过一段时间Seaweedfs(Golang开发的高并发的分布式文件系统),一个朋友问我Seaweedfs是如何实现高并发的。我讲了很多,却并没有让朋友听明白,因为我当时并没有足够简练的总结出重点。也因此在我重新思考了Seaweedfs的高并发关键因素之后,我有了现在的想法:技术在某种程度上都是相通的,可以以小见大,同样也可以以大见小。对于分布式服务系统的重要因素,同样适用于开发一个独立的服务程序!
Seaweedfs 分布式文件服务稳定和高并发的关键因素
一、可观测:是服务稳定的保证,测试用例,日志输出,监控接口;
二、算法实现:数据结构与索引搜索算法,快速定位数据;
三、资源优化:存储卷,合并小文件,优化IO从而提高整体的吞吐量;
四、分布式:分布式集群,数据副本冗余分发,进一步提高稳定性和并发能力,使服务能够并发访问和故障转移;
稳定高性能的服务程序同样需要关注以下类似的几个因素:
一、可观测:测试用例,日志输出,监控接口;
二、数据结构与算法实现:功能的实现,数据结构和算法的选择;
三、资源优化:语言的资源(比如:锁,内存,引用包提供的数据结构等),调用的资源(比如:依赖包,依赖服务,底层资源,CPU,内存,IO磁盘,网络等等);
四、并发:多线程,协程等
参考项目
为了更快的接触掌握最佳实践,挑选了一个社区活跃的开源项目。参考学习它的项目结构,文件配置以及包的选择。
Lighthouse
项目结构
$ tree
├──lighthouse/ // Package
│ ├── Cargo.toml
│ ├── environment // Package
│ │ ├── Cargo.toml
│ │ ├── src
│ │ │ └── lib.rs
│ │ └── ...
│ │ ...
│ ├── src
│ │ └── main.rs
│ └──...
├──account_manager/ // Package
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ ├── common.rs
│ ├── lib.rs
│ ├── validator
│ │ └── *.rs
│ ...
├──beacon_node/ // Package
├──book/ // Package
├──boot_node/ // Package
├──Cargo.toml
...
├──Makefile
├──README.md
...
src/main.rs 是可执行程序crate的根,即可执行程序的构建入口。
src/lib.rs 是库crate的根,即库的构建入口。
一个包中至多只能包含一个库 crate(library crate);
包中可以包含任意多个可执行程序(二进制)crate(binary crate);
包中至少包含一个 crate,无论是库的还是可执行程序的。
实践项目Ratchet 参考了Lighthouse 的项目结构,规划为多个包、箱和模块组成项目,以便功能划分和以后的维护迭代。
./Ratchet
├── Cargo.toml
├── log4rs.yaml // 日志配置文件
├── ratchet.yaml // 服务程序配置文件
├── README.md // 项目说明文件
├── Makefile // 构建脚本
├── common // 通用工具
│ ├── exporter // 监控接口
│ │ ├── Cargo.toml
│ │ └── src
│ │ ├── collector.rs
│ │ ├── grabber.rs
│ │ └── lib.rs
│ ├── logger // 日志
│ │ ├── Cargo.toml
│ │ └── src
│ │ └── lib.rs
│ └── ratchet_version // 版本信息
│ ├── Cargo.lock
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── watcher // 检测服务功能实现
│ ├── Cargo.toml
│ └── src
│ ├── config.rs
│ └── lib.rs
└── ratchet // 二进制运行程序
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs // 程序入口
先写到这里,本来想写一篇就可以了,结果写了这么多还没到主题。