一行代码,为何使 24 核服务器比笔记本还慢(2)
时间:2023-06-27 01:44 来源:网络整理 作者:墨客科技 点击:次
因为有 4 个核心,所以直到 4 个线程,吞吐量随着线程数的增加线性增长。然后,由于超线程技术使每个核心中可以再挤出一点性能,所以在 8 个线程时,吞吐量略有增加。显然,在 8 个线程之后,性能没有任何提升,因为此时所有的 CPU 资源都已经饱和。 我对获取的绝对数值感到满意。几百万个空调用在笔记本上每秒听起来像基准测试循环足够轻量,不会在真实测量中造成重大开销。同一笔记本上,如果请求足够简单且所有数据都在内存中,本地 Cassandra 服务器在全负载情况下每秒只能做大约 2 万个请求。当我在函数体中添加了一些实际的数据生成代码,但没有对数据库进行调用时,一如预期性能变慢 , 但不超过 2 倍,仍在 " 百万 OPS" 范围。 我本可以在这里停下来,宣布胜利。然而,我很好奇,如果在一台拥有更多核心的大型服务器上运行,它能跑多快。 在 24 核上运行空循环 一台配备两个 Intel Xeon CPU E5-2650L v3 处理器的服务器,每个处理器有 12 个运行在 1.8GHz 的内核,显然应该比一台旧的 4 核笔记本电脑快得多,对吧?可能单线程会慢一些,因为 CPU 主频更低(3 GHz vs 1.8 GHz),但是它应该可以通过更多的核心来弥补这一点。 用数字说话: 你肯定也发现了这里不太对劲。两个只是线程比一个线程好一些而已,随着线程的增加吞吐量增加有限,甚至开始降低。我无法获得比每秒约 200 万次调用更高的吞吐量,这比我在笔记本上得到的吞吐量差了近 4 倍。要么这台服务器有问题,要么我的程序有严重的可扩展性问题。 查问题 当你遇到性能问题时,最常见的调查方法是在分析器下运行代码。在 Rust 中,使用 cargo flamegraph 生成火焰图非常容易。让我们比较在 1 个线程和 12 个线程下运行基准测试时收集的火焰图: 我原本期望找到一个瓶颈,例如竞争激烈的互斥锁或类似的东西,但令我惊讶的是,我没有发现明显的问题。甚至连一个瓶颈都没有!Rune 的 VM::run 代码似乎占用了大约 1/3 的时间,但剩下的时间主要花在了轮询 futures 上,最有可能的罪魁祸首可能已经被内联了,从而在分析中消失。 无论如何,由于 VM::run 和通往 Rune 的路径 rune::shared::assert_send::AssertSend,我决定禁用调用 Rune 函数的代码,并且我只是在一个循环中运行一个空的 future,重新进行了实验,尽管仍然启用了计时和统计代码: // Executes a single iteration of a workload.// This should be idempotent – // the generated action should be a function of the iteration number.// Returns the end time of the query.pub async fn run ( &self, iteration: i64 ) -> Result<Instant, LatteError> { let start_time = Instant::now ( ) ; let session = SessionRef::new ( &self.session ) ; // let result = self // .program // .async_call ( self.function, ( session, iteration ) ) // .await // .map ( |_| ( ) ) ; // erase Value, because Value is !Send let end_time = Instant::now ( ) ; let mut state = self.state.try_lock ( ) .unwrap ( ) ; state.fn_stats.operation_completed ( end_time - start_time ) ; // ... Ok ( end_time ) } 在 48 个线程上,每秒超过 1 亿次调用的扩展表现良好!所以问题一定出现在 Program::async_call 函数下面的某个地方: // Compiled workload programpub struct Program { sources: Sources, context: Arc<RuntimeContext>, unit: Arc<Unit>,} // Executes given async function with args.// If execution fails, emits diagnostic messages, e.g. stacktrace to standard error stream.// Also signals an error if the function execution succeeds, but the function returns// an error value. pub async fn async_call ( &self, fun: FnRef, args: impl Args + Send, ) -> Result<Value, LatteError> { let handle_err = |e: VmError| { let mut out = StandardStream::stderr ( ColorChoice::Auto ) ; let _ = e.emit ( &mut out, &self.sources ) ; LatteError::ScriptExecError ( fun.name, e ) }; let execution = self.vm ( ) .send_execute ( fun.hash, args ) .map_err ( handle_err ) ?; let result = execution.async_complete ( ) .await.map_err ( handle_err ) ?; self.convert_error ( fun.name, result ) } // Initializes a fresh virtual machine needed to execute this program.// This is extremely lightweight.fn vm ( &self ) -> Vm { Vm::new ( self.context.clone ( ) , self.unit.clone ( ) ) } async_call 函数做了几件事: (责任编辑:admin) |
