一个 Rust 小白发布生产级 Rust 应用的进阶之路
一、引 言
在流量日益增长的今天,随着用户需求的不断增加和性能要求的提升,一个能够更好地处理高并发、低延迟和资源有效利用的计算层是十分重要的。尽管在过去我们平台使用Java开发的计算层提供了稳定的服务支撑,但面对日益增长的流量和低延迟的需求,Java不可避免地开始显现局限性:
- 垃圾回收:Java 的自动内存管理依赖于垃圾回收机制,而垃圾回收虽然简化了开发工作,却可能引入不可预测的延迟。
- 内存使用效率:Java 的内存管理通常比手动管理的语言消耗更多的内存,因为它必须保留足够的空间来处理对象分配和回收。
- 异步处理瓶颈:虽然Java近年来强化了异步编程支持,但在极限性能优化方面,仍存在不可忽视的不足。
在此背景下,经过调研和实验验证,我们发现了Rust这个计算层改造升级的语言选型。Rust语言以其出色的内存管理、安全性和高效性能而闻名。Rust的所有权模型可以在编译时捕捉大多数内存错误,从而减少运行时错误,这对需要高可靠性和稳定性的系统尤为重要。此外,Rust没有垃圾回收机制,这意味着我们可以更好地预测和控制内存使用,提高应用程序的性能和资源利用率。
通过使用Rust对计算层改造升级,我们的系统获得了如下的提升:
- 相比于Java,减少了30%的CPU核数。
- 高效内存管理,减少了70%的内存使用。
- 服务更稳定,Bug少。
二、Rust核心特性
Rust 能够突破传统编程语言的瓶颈,主要得益于其独特的所有权、借用和生命周期机制。这些特性使 Rust 在编译阶段就能够确保内存安全和线程安全,从而最大程度地减少运行时错误和不确定性。接下来,我们将深入探讨 Rust 在并发模型、所有权、生命周期和借用方面的优势。
所有权
Rust 的所有权 (Ownership)是该语言独特的内存管理机制,它确保内存安全性和并发性而不需要垃圾回收器。所有权机制通过编译时检查来保证安全性,避免绝大多数的运行时错误,例如空指针或数据竞争。
Rust所有权规则
Rust的所有权有三个主要规则:
- 所有值(除Copy类型)有且只有一个拥有者。
- 当所有者离开作用域,值会被自动释放,不需要手动回收。
- 值的所有权可以被移动或者借用。
为了方便理解,这里展示Rust、C++和Java对象赋值的异同来理解所有权的运行机制。
可以看到,将a赋值给b时,Java会将a指向的值的引用传递给b,而C++则会产生一个新的副本。从某种意义来说,在内存管理上,Java和C++选择了相反的权衡。代价是Java需要垃圾回收来管理内存,而C++的赋值会消耗更多的内存。不同于Java和C++,Rust选择了另一种方案:移动所有权。即将a指向的堆内存地址"移动到b上",这时只有b可以访问这段内存,a则成为了未初始化状态并禁止使用。
Rust的所有权概念内置于语言本身,在编译期间对所有权和借用规则进行检查。这样,程序员可以在运行之前解决错误,提高代码的可靠性。
共享所有权
尽管Rust规定大多数值会有唯一的拥有者,但在某些情况下,我们很难为每个值都找到具有所需生命周期的单个拥有者,而是希望某个值在每个拥有者使用完后就自动释放。简单来说,就是可以在代码的不同地方拥有某个值的所有权,所有地方都使用完这个值后,会自动释放内存。对于这种情况,Rust提供了引用计数智能指针:Rc和Arc。
Rc和Arc非常相似,唯一的区别是Arc可以在多线程环境进行共享,代价是引入原子操作后带来的性能损耗。Rc和Arc实现共享所有权的原理是,Rc和Arc内部包含实际存储的数据T和引用计数,当使用clone时不会复制存储的数据,而是创建另一个指向它的引用并增加引用计数。当一个Rc或Arc离开作用域,引用计数会减一,如果引用计数归零,则数据T会被释放。这种机制也叫共享所有权机制。

{
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 锁定 Mutex 以安全地访问数据
let mut num = counter_clone.lock().unwrap();
*num += 1; // 修改数据
});
handles.push(handle);
}
// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}
// 获取最终计数值
println!("Final count: {}", *counter.lock().unwrap());
}
生命周期和引用
在 Rust 中,生命周期 (lifetimes )和引用 (references)是两个密切相关的概念,它们共同构成了 Rust 的所有权系统的重要组成部分。生命周期用于确保引用在使用时是有效的,从而防止悬空引用和数据竞争等问题。
引用
前面提到,Rust值的所有权可以被借用,它允许在不获取数据所有权的情况下访问数据。Rust中有两种类型的引用:
- 不可变引用 (&T):允许你读取数据,但不允许修改。
- 可变引用 (&mut T):允许你修改数据。
在使用引用的时候需要满足以下规则:
- 在同一时间只能有一个可变引用。
- 多个不可变引用可以同时存在,但在可变引用存在时,不能有不可变引用。
- 每个引用都有一个生命周期,表示该引用在程序中的有效范围,且引用的生命周期不能超过被借用的值的生命周期。
生命周期
在 Rust 编程语言中,生命周期用于确保引用在使用时是有效的。生命周期的存在使得 Rust 能够在编译时检查引用的有效性,从而防止悬空引用。如下是一个Rust编译器检查生命周期的例子:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
}
这里编译器将r的生命周期记为'a,x的生命周期记为'b。可以明显看出,内部块的'b比外部块的'a生命周期小,当x离开作用域被释放时,r仍然持有x的引用。所以当把生命周期为'a的r想引用生命周期为'b的x时,编译器发现了这个问题,并拒绝通过编译,保证了程序不会出现悬垂引用。
生命周期标注
正如我们看到的,Rust的引用代表对值的一次借用,它们有着种种限制,所以,在函数中、在结构体中等等位置上使用引用时,你都要给Rust编译器一些关于引用的提示,这种提示,就是生命周期标记。对于简单的情况,聪明的Rust编译器可以自动推断出引用的生命周期。对于一些模棱两可的情况,编译器也无法推断引用是否在程序运行期间始终有效,这时就需要我们提供生命周期标注来提示编译器我们的代码是正确的,放我过去吧。
生命周期标注并没有改变传入的值和返回的值的生命周期,我们只是向借用检查器指出了一些用于检查非法调用的一些约束而已,而借用检查器并不需要知道 x、y 的具体存活时长。而事实上如果函数引用外部的变量,那么单靠 Rust 确定函数和返回值的生命周期几乎是不可能的事情。因为函数传递什么参数都是我们决定的,这样的话函数在每次调用时使用的生命周期都可能发生变化,正因如此我们才需要手动对生命周期进行标注。
相信第一次看到生命周期的小伙伴们都感觉概念非常难理解,且写出的代码非常丑,简直要逼死强迫症。但是有得就有舍,要写出安全且高效的Rust代码,就要学会理解和使用生命周期。如果实在不想用,那就多用Rc和Arc吧。
三、用Rust构建生产级应用
了解了Rust最核心的基本知识和特性后,你已经成为了一个合格的Rust练习生,可以开始用Rust愉快的进行开发工作了。但是要使用Rust开发高性能的生产级应用,只了解到这种程序是不行的。当初笔者信心满满地将第一个Rust应用发布到测试环境后,竟然发现效率比Java版本还低,于是开始了长期的瓶颈排查和调优,且调优时间远大于编码时间。最终我们的应用在相同吞吐量的条件下,CPU使用率从高于Java 20%优化到低于Java 40%。在这个过程中,也总结了一些经验进行分享。
合理利用引用减少数据拷贝
相信很多刚接触Rust的小伙伴在面对同一份数据需要在多处使用的情况时,为了逃避复杂的生命周期问题,会倾向于使用Clone来创建数据副本。如果这样做的话,一份数据在内存中重复出现多次,带来的cpu和内存消耗会让你会怀疑人生,为什么这么相信Rust的性能而不相信自己能啃下生命周期这块硬骨头呢?
有一个应用场景,我们从数据源得到若干个源数据,根据业务逻辑聚合成batch并存储到远端或者本地。聚合的逻辑可以有两种方式:
- 将源数据的所有权移动到batch。
- 将源数据拷贝一份到batch。
然而这两种方式都不可取。第一种方式的问题是,我们不知道一份源数据是不是只会被使用一次。而使用第二种方式则会消耗更多的CPU,且占用内存成倍上升。
前面提到,Rust的值是可以借用的,如果在batch中不获得所有权,而是存储引用,那么可以几乎零消耗的实现需求。以上述应用场景为例,这里介绍我们是怎么解决这个问题的。
首先给出源数据Data和Batch的定义:
struct Data {
condition: bool,
num: i32,
msg: String
}
struct Batch<'a> {
msgList: Vec<&'a str>
}
假设需求是将Data的msg字段在Batch里存储num次,我们很容易写出这样的代码:
fn main() {
let batch: Batch = Batch:new(); // 初始化Batch
loop {
let data:Data = dataSource.getData(); // 从数据源获得data
recordData(batch, &data);
if (batch.len() > 100) { // batch存储的数据大于100条时,存储并清空
save(batch);
batch.clear();
} // ------------------- data的生命周期到此结束
} // ------------------- batch的生命周期到此结束
}
fn record_data(batch: Batch, data: Data) {
if(condition) { // 根据条件将msg保存num次
for i in 0..data.num {
batch.msgList.push(&data.msg);
}
}
}
看起来是不是很合理,和其他语言也没有什么区别,当信心满满按下编译后,会发现天空飘来五个字:编译不通过。原因很简单,因为编译器发现被引用对象data的生命周期小于batch,data的在当前循环结束后就会销毁,batch存储的引用就变成了野指针。我们可以做如下修改:
fn() {
let batch: Batch = Batch:new(); // 初始化Batch
let dataList: Vec<Data> = Vec::new(); // dataList的生命周期和batch一样
loop {
let data: Data = dataSource.getData(); // 从数据源获得data
dataList.push(data); // 将data保存在dataList,提升生命周期
if(batch.len() > 100) {
for data_ref: &Batch in dataList.iter() {
record_data(batch, data_ref); // 此时data的生命周期和batch相等
}
save(batch);
batch.clear();
dataList.clear();
}
}
}
fn record_data<'a>(batch: Batch<'a>, data: &'a Data) {
if(condition) { // 根据条件将msg保存num次
for i in 0..data.num {
batch.msgList.push(&data.msg);
}
}
}
可以看到,我们对代码做了一些小改动:
- 在循环外初始化了一个Vec,并保存每次得到的data。
- record_data函数上增加了生命周期标注。
为什么这么做呢?我们已经知道最初版本是因为data的生命周期小于batch,导致batch不能存储data的引用。解决这个问题的思路很简单,提升data的生命周期不就完了。假设batch的生命周期是'a,data的生命周期是'b,很明显'a是大于'b的,因为batch的生命周期是整个main函数,而data的生命周期仅仅在loop内。我们在batch同样的作用域内定义一个容器,它的生命周期也是'a。在每次得到data后把它存入容器中,那data就不会在循环结束的时候被销毁了。
同时,在record_data函数定义上,我们也要使用标注告诉编译器batch和data的生命周期是相等的。如果data的生命周期大于batch,我们也可以在参数中定义data的生命周期为'a,因为实际的生命周期和参数生命周期标注无需一致,只需要实际的生命周期大于参数生命周期就行了。如果你有强迫症,也可以在参数中标注实际的生命周期,只需要加上适当的生命周期约束就行了:
// 'b: 'a表示'b的生命周期能够覆盖'a
fn record_data<'a>(batch: Batch<'a>, data: &'b Data) where 'b: 'a {
......
}
经过这些小改动,你的应用会比粗暴的使用拷贝提升许多性能并且节约大量内存使用。经过我们的测试,在类似需求中将需要大量拷贝的操作替换成引用,可以节省一倍的内存,CPU使用率也下降了20%。
FFI(Foreign Function Interface)
在一些情况下,我们项目使用的编程语言在实现一些功能时,想使用现成的依赖库来实现复杂的逻辑,但是因为生态不完善,导致缺少此类库或者现存的依赖库不成熟。在使用Rust时,这种现象尤其普遍。很多热门组件没有为Rust提供官方API,非官方实现功能和性能又得不到保证,且更新不稳定。难道Rust进阶之路就要到此为止?
Rust很贴心地提供了跨语言交互能力,对FFI的良好支持可以让开发者方便的在Rust代码中调用C程序。如果我们需要的依赖库刚好有C/C++的实现,就能使Rust完成主要逻辑,把一些Rust不完善的功能通过C/C++实现,而且性能也不会受到影响。在Rust程序调用C代码也非常简单:
- 声明外部函数
extern "C" {
fn c_add(a: i32, b: i32) -> i32;
}
- 在RUST中调用C函数
fn main() {
unsafe {
c_add(1, 2);
}
}
- 将C程序编译打包为静态/动态链接库
g++ -std=c++17 -shared -fPIC -o libhello.so hello.cpp
- 然后编译 Rust 文件并链接到链接库
rustc main.rs hello.o
尽管用Rust调用C程序已经非常方便,但是仍需要注意这些问题:
- 处理数据类型:在 Rust FFI 中,需要特别注意数据类型的转换和处理。Rust 和其他语言的数据类型可能存在差异,需要进行适当的转换。例如,Rust的i32和C的int可以直接相互转换。而字符串的传递之所以需要特殊处理,是因为Rust的字符串实现和C/C++不一样。C/C++的字符串指针只包含地址,且字符串后有"\0"作为结尾,而Rust字符串的指针不仅包含地址,还包含字符串长度,且末尾没有"\0"作为结尾。
- 内存管理:尽管Rust是内存安全的语言,但是在使用FFI的情况下,Rust无法保证调用的外部语言的安全性。作为开发者,我们要自己管理外部语言的内存。
- 线程安全:在多线程环境下使用 Rust FFI 时,需要注意线程安全问题。某些外部函数可能不是线程安全的,需要在调用时进行适当的同步操作。
- 性能优化:在使用 Rust FFI 时,需要注意性能优化问题。由于涉及跨语言调用,可能会导致一定的性能损失。因此,需要对 FFI 调用的性能进行评估和优化。
Tokio
如果你想构建一个高性能的Rust服务器应用,那么Tokio绝对是你的首选框架。Tokio 是一个用 Rust 编写的异步运行时,旨在提供高性能的 I/O、任务调度和并发支持。虽说Tokio提供了强大的异步支持,要用好Tokio也不是一件容易得事,首先要了解"异步"的概念。在计算机编程中,"异步"是指一种不阻塞的操作方式,允许程序在等待某些操作(如 I/O 操作、网络请求等)完成时继续执行其他代码。
Tokio 通过使用协程和 Future 机制来实现高效的并发处理。它将异步任务封装为Future对象,并通过运行时的调度器管理这些任务的执行状态。当任务被调用时,运行时通过poll方法检查其状态,如果任务无法继续执行(返回 Poll::Pending),则将其挂起并注册一个Waker来在后续的某个时刻唤醒任务。一旦相关的I/O操作完成,Waker会通知运行时重新调度该任务,从而实现非阻塞的并发执行。Tokio支持多线程运行,可以充分利用多核CPU的能力,提高应用程序的性能和响应性。

#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() {
let h = tokio::spawn(async {
let (tx, rx) = std::sync::mpsc::channel::<String>();
tokio::spawn(async move{
let _ = tx.send("send message".to_string());
});
let ret = rx.recv().unwrap();
println!("{}", ret)
});
h.await;
}
代码结构很简单,但是运行后会发现代码似乎hang住了,检查代码结构也没有发现问题。要解释这个卡死的问题,要从Tokio的任务调度机制来分析:

Processor 运行完当前 task 后,会尝试按照以下顺序获取新的 Task 并继续运行:
- LIFO Slot.
- Local Run Queue.
- Global Queue.
- 其他 Processor 的 Local Run Queue。
如果 Processor 获取不到 task 了,那么其对应的线程就会休眠,等待下次唤醒。
在上面的例子中,我们首先Spawn了一个异步任务Task-1,Task-1被分配给了Processor-1执行。然后在Task-1里Spawn了另一个异步任务Task-2,Task-2被放到了Processor-1的LIFO Slot中。
因为Task-1继续运行的条件依赖于Task-2,所以Task-1被阻塞了。而且Tokio的协程是非抢占式的,在Task-1没有遇到.await前无法让出CPU,Processor-1无法去执行Task-2。又因为Task-2在Processor-1的LIFO Slot中,其他的Processor也无法偷取Task-2执行。于是,Task-2永远也不会有机会被执行,这两个Task在循环等待中就永远卡死了。
要解决这个问题,我们要将阻塞型的数据结构替换成Tokio的非阻塞式的:
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() {
let handler = tokio::spawn(async {
let (tx, mut rx) = tokio::sync::mpsc::channel(2);
tokio::spawn(async move{
let _ = tx.send("send message".to_string()).await;
});
let ret = rx.recv().await.unwrap();
println!("{}", ret)
});
handler.await;
}
将channel替换成Tokio的非阻塞数据结构后,Task-1在提交完Task-2后遇到await让出了CPU,Processor-1就可以从LIFO Slot取出Task-2执行了,循环等待也就被打破了。
由这个例子可以看出,Tokio 的轻量级线程之间的关系是一种合作式的。合作式的意思就是同一个 CPU 核上的任务大家是配合着执行(不同 CPU 核上的任务是并行执行的)。我们可以设想一个简单的场景,A 和 B 两个任务被分配到了同一个 CPU 核上,A 先执行,那么,只有在 A 异步代码中碰到 .await 而且不能立即得到返回值的时候,才会触发挂起,进而切换到任务 B 执行。也就是说,在一个 task 没有遇到 .await 之前,它是不会主动交出这个 CPU 核的,其他 task 也不能主动来抢占这个 CPU 核。
所以在使用Tokio时,我们要注意两点:
- 不要在异步代码中执行阻塞操作,不然这个OS线程中的其他任务都会被阻塞。
- Tokio 虽然适合网络 I/O 型并发,但是也要在 I/O 任务里小心地控制计算型代码的时间,否则会导致运行时任务调度不均,从而长时间阻塞其它任务的运行。
四、Rust应用发布
通过 Cargo,开发者可以轻松创建、构建和共享 Rust 项目。但是因为发布系统只支持Java和Golang应用,要在发布系统发布Rust应用还是需要一些工作的。以下是我们发布Rust应用的流程。
上传镜像
因为公司平台是没有Rust应用的,所以我们需要自己制作镜像并上传,这样才能在发布平台发布我们的代码。我们需要创建两个 Docker 镜像:一个用于构建(CI 镜像),另一个用于运行(运行时镜像)。

FROM repoin.shizhuang-inc.net/ci-build/rust:1.79.0
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 871920D1991BC93C
# 创建 /etc/apt/sources.list
RUN echo "deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse" > /etc/apt/sources.list && \
echo "deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse" >> /etc/apt/sources.list && \
echo "deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse" >> /etc/apt/sources.list && \
echo "deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse" >> /etc/apt/sources.list && \
echo "deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse" >> /etc/apt/sources.list
# 更新包列表并安装必要的工具
RUN apt-get install -y \
protobuf-compiler \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 验证安装
RUN protoc --version
RUN pwd
RUN ls -alh .
RUN ls -alh workspace
发布
建好集群后,还需要对集群进行一些配置:
- 修改编译配置的镜像为自己上传的镜像。
- 将编译命令设为cargo build --release。
- 修改运行时镜像。
- 修改发布配置,改为自己应用所需要的。
还需要注意的是,发布平台的编译环境和运行环境是不同的,编译完成后发布平台会将可执行文件移动到/opt/apps目录下进行执行,而配置文件不会被打包。遇到这种情况可以使用rust-embed库,它允许将静态文件(如 Yaml、Json、图像等)打包到您的二进制文件中,从而简化文件管理和部署。
上监控
虽说Rust应用主打的是稳定,但是发布后持续对应用进行监控也是必须的,不然晚上能睡得着吗。和发布一样,Rust应埋的指标要被监控采集,需要额外的配置。在KubeOne平台找到自己的集群,在发布配置里加上这两项,监控平台就可以采集到指标了。
labels:
- key: http://dewu.com/qos
value: LS
- key: http://duapp.kubernetes.io/metrics-scraped
value: metrics
containerPorts:
- containerPort: "2892"
name: http-metrics
protocol: TCP
通过上监控,可以实时观察Rust服务的运行情况,并且根据自己的埋点分析系统的瓶颈。可以看到,Rust应用运行非常平稳。相比于有GC的Java应用,Rust明显毛刺很少,非常平滑,而且内存占用相比Java减少了70%。
五、结 论
通过迁移到Rust,我们的计算层能够在处理高并发请求时显著提高系统的吞吐量和响应能力,同时减少服务器资源的浪费。这不仅能降低运营成本,还能为我们的用户提供更流畅、更快速的体验。
但是,如果要持续地拥抱Rust生态,目前仍然面临如下挑战:
1. 生态不完善 尽管 Rust 已经有一些非常优秀的库和工具,但某些特定领域仍然缺乏成熟且广泛使用的库。这意味着开发者可能需要花费更多的时间来构建自己的解决方案或者整合不同语言的库。
2. 学习曲线陡峭 Rust 语言引入了许多独特的概念和特性,对于初学者和来自其他语言的开发者来说,这些特性可能需要一段时间来彻底掌握。
3. 开发进度 相比于自动内存管理类型语言的开发任务,Rust严格的编译检查会让开发进度一度阻塞。
尽管开发Rust生产级应用有那么多阻碍,我们目前已经发布的Rust应用已经证明了,相比于付出,迁移Rust带来的收益更大。希望大家都可以探索Rust的可行性,为节能减排和世界和平出一份力,也欢迎各位对Rust有兴趣的同学一起交流。
文 / 小新
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
关注公众号
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
全国人大代表雷军:建议加快推进人工智能终端产业高质量发展
全国人大代表、小米集团创始人、董事长兼CEO雷军在2025全国两会上共提交了 5 项建议,包括: 关于加快推进自动驾驶量产的建议 关于发展智能网联新能源汽车产业生态的建议 关于加快推进人工智能终端产业高质量发展的建议 关于优化新能源汽车号牌设计的建议 关于加强“AI换脸拟声”违法侵权重灾区治理的建议 其中,围绕加快推进人工智能终端产业高质量发展,雷军建议: 一、健全人工智能终端标准体系,引导产业良性发展。人工智能终端步入创新发展快车道,但行业内在产品形态、范畴界定、智能功能与性能等方面未达成共识,相关标准体系尚未完善,不同品牌,不同类型、不同产品之间的用户体验参差不齐,一定程度上影响了消费者对人工智能终端的整体认知,也阻碍了行业健康长远发展。 建议尽快完善人工智能终端标准体系,编制以用户体验为导向的智能化分级等系列标准,研究制定人工智能终端产品认定方法,强化国际国内标准有效衔接。力争2027年内初步建成人工智能终端标准体系,2030年率先形成全球领先的人工智能终端标准体系。积极推进行业内人工智能终端优秀产品、典型案例遴选,树立行业标杆,助力企业创新发展。 二、强化人工智能终端产业协作,...
-
下一篇
Canonical 正在改进 CLA 流程,让开发者更便捷参与 Ubuntu 贡献
Canonical宣布,他们正在努力提升其贡献者许可协议流程,该流程多年来一直主要依靠人工操作。基于他们的新流程,贡献者许可协议处理变得更加简便,开发者参与贡献也更加迅速。 请注意,Canonical为其开发的围绕Ubuntu Linux的各种项目所使用的贡献者许可协议(CLA)本身并未改变,但签署CLA的流程正在得到改进。 新的Canonical CLA流程将允许通过GitHub的OAuth身份验证,或通过带有Ubuntu SSO单点登录的Launchpad更轻松地签署协议。对于签署CLA的组织,还有一个改进的协议流程。 如需了解更多关于新Canonical CLA流程的信息,请访问Ubuntu Discourse。
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS6,CentOS7官方镜像安装Oracle11G
- MySQL8.0.19开启GTID主从同步CentOS8
- Crontab安装和使用
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8编译安装MySQL8.0.19



微信收款码
支付宝收款码