微服务优化实战指南
1. 微服务性能测试工具
可以通过发送根据脚本规则构建的 HTTP 请求来对微服务进行负载测试,并输出如下报告:
Threads 4
Iterations 5
Rampup 2
Base URL http://localhost:8080
Index Page http://localhost:8080/ 200 OK 7ms
Index Page http://localhost:8080/ 200 OK 8ms
...
Index Page http://localhost:8080/ 200 OK 1ms
Concurrency Level 4
Time taken for tests 0.2 seconds
Total requests 20
Successful requests 20
Failed requests 0
Requests per second 126.01 [#/sec]
Median time per request 1ms
Average time per request 3ms
Sample standard deviation 3ms
有两个工具适合测试微服务性能:
- Welle:若要优化指定处理程序,它适合测量单一请求类型的性能。
- Drill:适合产生复杂负载,以衡量应用程序可服务的用户数量。
2. 性能测量与优化示例
我们将测量一个示例微服务在两种编译选项下的性能:无优化和编译器优化。该微服务会向客户端发送渲染后的索引页面,使用 Welle 工具来测量性能,看是否能提升。
2.1 基础示例
在基于 actix - web 包的新包中创建微服务,在
Cargo.toml
中添加以下依赖:
[dependencies]
actix = "0.7"
actix-web = "0.7"
askama = "0.6"
chrono = "0.4"
env_logger = "0.5"
futures = "0.1"
[build-dependencies]
askama = "0.6"
构建一个异步渲染包含当前时间(精确到分钟)的索引页面的小型服务器,有一个快捷函数:
fn now() -> String {
Utc::now().to_string()
}
使用 askama 包渲染索引页面模板,并插入从共享状态获取的当前时间。为使用内存堆中的值,时间值使用
String
类型:
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
time: String,
}
共享状态使用一个包含
last_minute
值的结构体,该值用
Mutex
包装成
String
类型:
#[derive(Clone)]
struct State {
last_minute: Arc<Mutex<String>>,
}
索引页面使用以下处理程序:
fn index(req: &HttpRequest<State>) -> HttpResponse {
let last_minute = req.state().last_minute.lock().unwrap();
let template = IndexTemplate { time: last_minute.to_owned() };
let body = template.render().unwrap();
HttpResponse::Ok().body(body)
}
通过
main
函数启动应用程序,准备
Server
实例并派生一个单独的线程来更新共享状态:
fn main() {
let sys = actix::System::new("fast-service");
let value = now();
let last_minute = Arc::new(Mutex::new(value));
let last_minute_ref = last_minute.clone();
thread::spawn(move || {
loop {
{
let mut last_minute = last_minute_ref.lock().unwrap();
*last_minute = now();
}
thread::sleep(Duration::from_secs(3));
}
});
let state = State {
last_minute,
};
server::new(move || {
App::with_state(state.clone())
.middleware(middleware::Logger::default())
.resource("/", |r| r.f(index))
})
.bind("127.0.0.1:8080")
.unwrap()
.start();
let _ = sys.run();
}
2.2 性能测试
-
无优化构建运行
:使用标准命令
cargo run构建并运行代码,生成包含大量调试信息的二进制文件。使用以下命令进行性能测试:
welle --concurrent-requests 10 --num-requests 100000 http://localhost:8080
测试报告如下:
| 指标 | 值 |
| ---- | ---- |
| 总请求数 | 100000 |
| 并发数 | 10 |
| 完成请求数 | 100000 |
| 错误请求数 | 0 |
| 5XX 请求数 | 0 |
| 总耗时 | 29.883248121s |
| 平均耗时 | 298.832µs |
| 飞行总时间 | 287.14008722s |
| 平均飞行时间 | 2.8714ms |
| 50%请求响应时间 | 3.347297ms |
| 66%请求响应时间 | 4.487828ms |
| 75%请求响应时间 | 5.456439ms |
| 80%请求响应时间 | 6.15643ms |
| 90%请求响应时间 | 8.40495ms |
| 95%请求响应时间 | 10.27307ms |
| 99%请求响应时间 | 14.99426ms |
| 100%请求响应时间 | 144.630208ms |
-
优化构建运行
:使用
cargo run --release命令重新编译,该命令会向rustc编译器传递-C opt - level = 3优化标志。再次使用相同参数的 Welle 工具测试,报告如下:
| 指标 | 值 |
| ---- | ---- |
| 总请求数 | 100000 |
| 并发数 | 10 |
| 完成请求数 | 100000 |
| 错误请求数 | 0 |
| 5XX 请求数 | 0 |
| 总耗时 | 8.010280915s |
| 平均耗时 | 80.102µs |
| 飞行总时间 | 63.961189338s |
| 平均飞行时间 | 639.611µs |
| 50%请求响应时间 | 806.717µs |
| 66%请求响应时间 | 983.35µs |
| 75%请求响应时间 | 1.118933ms |
| 80%请求响应时间 | 1.215726ms |
| 90%请求响应时间 | 1.557405ms |
| 95%请求响应时间 | 1.972497ms |
| 99%请求响应时间 | 3.500056ms |
| 100%请求响应时间 | 37.844721ms |
可以看到,请求平均耗时减少了 70% 以上。
3. 代码优化
尝试对代码应用以下三种优化:
- 减少共享状态的阻塞。
- 通过引用重用状态中的值。
- 添加响应缓存。
在
Cargo.toml
文件中有如下特性配置:
[features]
default = []
cache = []
rwlock = []
borrow = []
fast = ["cache", "rwlock", "borrow"]
-
cache:激活请求缓存。 -
rwlock:对State使用RwLock而非Mutex。 -
borrow:通过引用重用值。
3.1 无阻塞状态共享
将
Mutex
替换为
RwLock
,因为
Mutex
在读写时都会锁定,而
RwLock
允许单个写入者或多个读取者。修改
State
结构体:
#[derive(Clone)]
struct State {
last_minute: Arc<RwLock<String>>,
}
修改
last_minute
引用计数器的创建:
let last_minute = Arc::new(RwLock::new(value));
在工作线程中使用
write
方法锁定写入:
let mut last_minute = last_minute_ref.write().unwrap();
在处理程序中使用
read
方法锁定读取:
let last_minute = req.state().last_minute.read().unwrap();
3.2 通过引用重用值
修改
IndexTemplate
结构体:
struct IndexTemplate<'a> {
time: &'a str,
}
使用引用:
let template = IndexTemplate { time: &last_minute };
3.3 缓存
在
State
结构体中添加新字段:
cached: Arc<RwLock<Option<String>>>
初始化:
let cached = Arc::new(RwLock::new(None));
let state = State {
last_minute,
cached,
};
修改索引处理程序:
let cached = req.state().cached.read().unwrap();
if let Some(ref body) = *cached {
return HttpResponse::Ok().body(body.to_owned());
}
let mut cached = req.state().cached.write().unwrap();
*cached = Some(body.clone());
3.4 优化编译与测试
-
部分优化编译
:使用
cargo run --release --features rwlock,borrow编译代码并测试,报告如下:
| 指标 | 值 |
| ---- | ---- |
| 总请求数 | 100000 |
| 并发数 | 10 |
| 完成请求数 | 100000 |
| 错误请求数 | 0 |
| 5XX 请求数 | 0 |
| 总耗时 | 7.94342667s |
| 平均耗时 | 79.434µs |
| 飞行总时间 | 64.120106299s |
| 平均飞行时间 | 641.201µs |
| 50%请求响应时间 | 791.554µs |
| 66%请求响应时间 | 976.074µs |
| 75%请求响应时间 | 1.120545ms |
| 80%请求响应时间 | 1.225029ms |
| 90%请求响应时间 | 1.585564ms |
| 95%请求响应时间 | 2.049917ms |
| 99%请求响应时间 | 3.749288ms |
| 100%请求响应时间 | 13.867011ms | -
全部优化编译
:使用
cargo run --release --features fast编译代码并测试,报告如下:
| 指标 | 值 |
| ---- | ---- |
| 总请求数 | 100000 |
| 并发数 | 10 |
| 完成请求数 | 100000 |
| 错误请求数 | 0 |
| 5XX 请求数 | 0 |
| 总耗时 | 7.820692644s |
| 平均耗时 | 78.206µs |
| 飞行总时间 | 62.359549787s |
| 平均飞行时间 | 623.595µs |
| 50%请求响应时间 | 787.329µs |
| 66%请求响应时间 | 963.956µs |
| 75%请求响应时间 | 1.099572ms |
| 80%请求响应时间 | 1.199914ms |
| 90%请求响应时间 | 1.530326ms |
| 95%请求响应时间 | 1.939557ms |
| 99%请求响应时间 | 3.410659ms |
| 100%请求响应时间 | 10.272402ms |
可以看到,应用全部优化后,比无优化版本快了 2% 以上。
4. 其他优化技术
4.1 链接时优化
在
Cargo.toml
文件中添加以下内容激活链接时优化(LTO):
[profile.release]
lto = true
4.2 用中止代替恐慌
在
Cargo.toml
文件中添加:
[profile.release]
panic = "abort"
4.3 减小二进制文件大小
使用
strip
命令:
strip <path_to_your_binary>
优化流程 mermaid 图
graph LR
A[创建微服务] --> B[无优化性能测试]
B --> C[优化编译]
C --> D[优化性能测试]
D --> E[应用代码优化]
E --> F[部分优化编译测试]
F --> G[全部优化编译测试]
G --> H[其他优化技术应用]
综上所述,微服务优化是一个逐步推进的过程,通过合理运用各种优化技术,可以显著提升微服务的性能和效率。但也要注意避免过度优化,以免增加开发难度和影响代码可维护性。
微服务优化实战指南
5. 优化技术的综合考量
在进行微服务优化时,需要综合考虑各种优化技术的优缺点和适用场景。
5.1 链接时优化的权衡
链接时优化(LTO)虽然可以帮助减小二进制文件的大小,但编译时间会显著增加。而且,并非在所有情况下都能提升性能。例如,在一些简单的微服务场景中,激活 LTO 可能并不会带来明显的性能提升,反而会因为编译时间的增加而影响开发效率。因此,在决定是否使用 LTO 时,需要进行充分的测试和比较,权衡编译时间和性能提升之间的关系。
5.2 中止代替恐慌的风险
使用中止(abort)代替 Rust 的恐慌(panic)可以减少错误处理代码的空间占用,从而可能提升性能。但这种方式也存在很大的风险,因为程序在中止时无法正确记录日志或传递跟踪信息。对于微服务来说,这可能会导致在出现问题时难以进行故障排查。因此,在使用这种优化技术时,需要确保有其他机制来处理日志记录和跟踪信息,例如创建一个单独的线程来处理跟踪信息。
5.3 减小二进制文件大小的适用场景
减小二进制文件大小的优化技术(如使用
strip
命令)通常适用于分布式应用中硬件空间有限的场景。但需要注意的是,剥离调试信息后,将无法对二进制文件进行调试。因此,在开发和测试阶段,不建议使用该优化技术,而应该在生产环境中,当性能和空间占用成为关键因素时再考虑使用。
6. 优化的注意事项
在进行微服务优化时,还需要注意以下几点:
6.1 避免过度优化
过度优化可能会使代码变得复杂,增加开发和维护的难度。例如,在一些简单的微服务中,为了追求微小的性能提升而进行复杂的优化,可能会导致代码难以理解和修改。因此,应该根据实际需求进行优化,避免为了不必要的性能提升而投入过多的时间和精力。
6.2 性能测试的重要性
在进行任何优化之前和之后,都需要进行充分的性能测试,以确保优化确实带来了性能提升。不同的优化技术可能会相互影响,因此需要对各种优化组合进行测试,找到最适合的优化方案。例如,在应用链接时优化和代码优化的同时,需要测试不同组合下的性能,以确定最佳的配置。
7. 优化效果总结
为了更直观地展示不同优化阶段的效果,我们将之前的测试结果汇总成以下表格:
| 优化阶段 | 总耗时(s) | 平均耗时(µs) | 50%请求响应时间(µs) | 99%请求响应时间(ms) |
|---|---|---|---|---|
| 无优化 | 29.883248121 | 298.832 | 3347.297 | 14.99426 |
| 编译器优化(–release) | 8.010280915 | 80.102 | 806.717 | 3.500056 |
| 部分代码优化(rwlock, borrow) | 7.94342667 | 79.434 | 791.554 | 3.749288 |
| 全部代码优化(fast) | 7.820692644 | 78.206 | 787.329 | 3.410659 |
从表格中可以看出,通过逐步应用各种优化技术,微服务的性能得到了显著提升。特别是在应用全部代码优化后,请求平均耗时进一步降低,响应时间也更加稳定。
8. 优化流程回顾
为了更好地理解微服务优化的整个流程,我们再次回顾之前的 mermaid 图:
graph LR
A[创建微服务] --> B[无优化性能测试]
B --> C[优化编译]
C --> D[优化性能测试]
D --> E[应用代码优化]
E --> F[部分优化编译测试]
F --> G[全部优化编译测试]
G --> H[其他优化技术应用]
这个流程清晰地展示了微服务优化的步骤:从创建微服务开始,进行无优化性能测试,然后进行编译器优化和性能测试,接着应用代码优化并进行部分和全部优化编译测试,最后考虑应用其他优化技术。每个步骤都需要进行性能测试,以确保优化的有效性。
9. 总结与建议
微服务优化是一个复杂而又重要的过程,需要综合运用各种优化技术,并根据实际情况进行权衡和选择。以下是一些总结和建议:
- 逐步优化 :从简单的优化开始,如编译器优化,然后逐步应用代码优化和其他优化技术。这样可以更好地控制优化的效果和风险。
- 性能测试贯穿始终 :在每个优化阶段都进行性能测试,确保优化确实带来了性能提升。同时,通过测试可以发现不同优化技术之间的相互影响。
- 避免过度优化 :根据实际需求进行优化,避免为了微小的性能提升而使代码变得复杂。保持代码的简洁和可维护性是非常重要的。
- 综合考虑各种因素 :在选择优化技术时,需要综合考虑性能提升、编译时间、开发难度、空间占用等因素,找到最适合的优化方案。
通过遵循这些建议,可以有效地提升微服务的性能和效率,同时保持代码的质量和可维护性。
超级会员免费看
847

被折叠的 条评论
为什么被折叠?



