以大见小 - Rust快速实践(一)


本文摘自网络,作者,侵删。


目标

被内存安全与极致性能的特性所吸引,前段时间简单了解了一下Rust 语言,然后就更加好奇起来。元旦也出不去就决定进一步学习一下。不想纠结于过多的语言细节,给自己定了一个明确的目标:开发出一个小工具(开源:Ratchet http/https 服务监控)。

这次完成了一些基本功能,对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              // 程序入口

先写到这里,本来想写一篇就可以了,结果写了这么多还没到主题。





本文来自:简书

感谢作者:卷边芝士

查看原文:以大见小 - Rust快速实践(一)


相关阅读 >>

slice 自身的指针为什么会变

Golang 为什么高并发

[系列] Go - 常用签名算法的基准测试

Go语言happens-before原则及应用

Golang中的defer关键字

Go语言如何判断变量是slice还是array

Go是强类型语言么

iota在Go中怎么使用

Golana语言社区】window应该开发之--cmd杀进程

[译]Go语言内存布局

更多相关阅读请进入《Go》频道 >>




打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

评论

管理员已关闭评论功能...