设计了五个要求越来越高的工作负载:
- 并发连接处理:管理多达 100 万个同时连接
- 内存压力:处理需要几乎全部系统内存的数据结构
- CPU 饱和:在所有可用核心上执行复杂计算
- I/O 轰炸:处理数千个同时的文件和网络操作
- 错误级联模拟:从故意引起的部分系统故障中恢复
用七种语言实现了相同的算法:Go、Rust、C++、Java、Python、JavaScript(Node.js)和 Erlang。
使用截至 2025 年 5 月的最新稳定版本进行了优化:
- Go 1.23
- Rust 1.78
- C++(使用具有 C++23 功能的 GCC 14.1)
- Java 21.0.2
- Python 3.13
- Node.js 22.3(JavaScript)
- Erlang/OTP 27
令人惊讶的幸存者
其他语言在我们最极端的测试场景下最终都遭遇了各种形式的故障,但 Erlang 依然保持了正常运行——虽然速度有时会很慢,但从未完全崩溃。即使我们故意引入通常会导致系统崩溃的级联故障,这种韧性依然保持了下来。
每种语言的表现如何
Go 1.23
Go 在高连接负载下表现出色,能够以相对较低的开销管理数十万个 goroutine。它的垃圾收集器能够高效地处理内存压力,直至达到系统极限。
// Go handled concurrent connections impressively
func handleConnections() {
listener, _ := net.Listen("tcp", ":8080")
for {
conn, _ := listener.Accept()
go func(c net.Conn) {
defer c.Close()
// Connection handling logic
processRequest(c)
}(conn)
}
}主要故障模式:在极端内存压力和 I/O 轰炸的情况下,Go 的垃圾收集器最终落后,导致延迟增加并最终导致系统不稳定。
Rust 1.78
Rust 的内存安全性无需垃圾回收机制,使其在内存压力下表现出色。其性能可预测,即使在极端负载下也能保持高效的资源利用率。
// Rust's thread pool handled CPU saturation well
fn main() -> Result<(), Box<dyn Error>> {
let pool = ThreadPool::new(num_cpus::get());
for task_id in 0..1_000_000 {
let task = Task::new(task_id);
pool.execute(move || {
process_complex_calculation(task);
});
}
pool.join();
Ok(())
}主要故障模式:在我们的错误级联模拟下,不安全代码区域中的一些低级故障以最终导致资源管理死锁的方式传播。
C++(GCC 14.1)
C++ 的原始性能在 CPU 密集型工作负载下最初超越了所有其他语言。精心实施后,内存管理极其高效。
// C++ memory management required careful implementation
void process_large_dataset(const std::vector<DataPoint>& data) {
// Using custom memory pool to avoid fragmentation
MemoryPool pool(1024 * 1024 * 1024); // 1GB pool
std::vector<ProcessedResult, PoolAllocator<ProcessedResult>> results(
data.size(), PoolAllocator<ProcessedResult>(&pool));
// Process data with careful memory management
#pragma omp parallel for
for (size_t i = 0; i < data.size(); ++i) {
results[i] = process_item(data[i]);
}
}主要故障模式:在内存压力和错误条件的共同作用下,出现内存管理问题,导致分段错误和系统崩溃。
Java 21.0.2
Java 成熟的 JVM 处理内存压力的能力超出了我们的预期,其最新的垃圾收集算法也表现出色。在 CPU 饱和的情况下,其性能表现也十分出色。
// Java's virtual threads handled concurrent workloads efficiently
public void handleMassiveRequests() throws Exception {
try (var server = ServerSocket.open()) {
server.bind(new InetSocketAddress(8080));
while (true) {
Socket socket = server.accept();
// Virtual threads (Project Loom) feature
Thread.startVirtualThread(() -> {
try (socket) {
processClientRequest(socket);
} catch (IOException e) {
logger.error("Error processing request", e);
}
});
}
}
}主要故障模式:在极端内存压力下,垃圾收集暂停最终增长到不可接受的长度,从而有效地停止应用程序。
Python 3.13
Python 的性能超出预期,尤其在 3.13 版本中并发能力的提升,让我们惊喜不已。对于解释型语言来说,Asyncio 能够很好地处理连接负载。
# Python's asyncio performed better than expected
async def handle_connections():
server = await asyncio.start_server(
process_client, '0.0.0.0', 8080)
async with server:
await server.serve_forever()
async def process_client(reader, writer):
try:
data = await reader.read(100)
# Process request data
result = await process_request(data)
writer.write(result)
await writer.drain()
finally:
writer.close()
await writer.wait_closed()主要故障模式:Python 的全局解释器锁(GIL)在 CPU 饱和下成为瓶颈,并且在极端负载下内存使用量呈指数增长,最终导致系统故障。
Node.js 22.3(JavaScript)
正如预期的那样,Node.js 在 I/O 密集型工作负载方面表现出色。它的事件循环能够以最小的开销高效地处理大量连接。
// Node.js excelled at I/O tasks
const server = http.createServer(async (req, res) => {
if (req.url === '/api/data') {
// Worker threads for CPU-intensive operations
const worker = new Worker('./process-data.js', {
workerData: { requestId: crypto.randomUUID() }
});
worker.on('message', (result) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
});
}
});
// Handle many concurrent connections
server.maxConnections = 100000;
server.listen(8080);主要故障模式:在内存压力下,Node.js 最终经历了级联故障,因为其事件循环被阻塞,并且垃圾收集暂停时间变得更长。
Erlang/OTP 27
Erlang 并非在任何单一类别中速度最快,但在所有测试场景中都展现了卓越的弹性。其“任其崩溃”的理念,结合监督树模式,使其能够从其他语言无法应对的故障中恢复。
%% Erlang's supervision trees provided exceptional fault tolerance
start() ->
{ok, Supervisor} = supervisor:start_link({local, main_sup}, ?MODULE, []),
{ok, Supervisor}.
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 10, period => 10},
ChildSpecs = [
#{id => connection_pool,
start => {connection_pool, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [connection_pool]},
#{id => task_scheduler,
start => {task_scheduler, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [task_scheduler]}
],
{ok, {SupFlags, ChildSpecs}}.生存之道:Erlang 的架构允许隔离并重启故障进程,而不会影响整个系统。即使系统的大部分功能都面临压力,其他部分也能继续独立运行。
测试的关键见解
- 架构设计胜过性能 Erlang 的胜利并非在于速度,而在于架构。它的 Actor 模型和监督层级结构专为容错和高可用性而设计。虽然 Rust 和 C++ 等语言提供了卓越的原始性能,但它们的架构模型却无法提供同等程度的抵御级联故障的弹性。
- 内存管理是最终的瓶颈在所有语言中,内存相关问题成为最常见的故障点。具有自动内存管理功能的语言(Java、Go、Python、JavaScript)最终都会遭遇垃圾回收机制的冲击。即使是 Rust 和 C++,在极端压力下也会遇到内存相关问题,尽管原因各不相同。
- 并发模型比我们想象的更重要在负载下,每种语言如何处理并发至关重要。Erlang 的轻量级进程、Go 的 goroutine 和 Java 的虚拟线程比每个连接一个线程的模型具有更好的扩展性。但 Erlang 的进程间隔离提供了其他并发模型所缺乏的关键故障控制能力。
- 恢复能力被低估大多数性能讨论都集中在速度和资源使用率上,但我们的测试表明,恢复能力对于系统可靠性同样重要。Erlang 的“任其崩溃”理念,结合其监督树,使其能够自动从其他语言中致命的故障中恢复。
实际意义
这些发现并不意味着每个人都应该立即切换到 Erlang。相反,它们强调了可以跨语言应用的重要架构原则:
- 针对故障进行设计:假设组件会发生故障并构建可以自动恢复的系统。
- 隔离至关重要:一个组件的故障不应该影响整个系统。
- 监督层次结构发挥作用:拥有清晰的监控和重启故障组件的模式可以提高弹性。
- 资源管理至关重要:在极端负载下,如何管理内存和系统资源最终决定了系统的生存。
转载自
https://mp.weixin.qq.com/s/EDrkVs_vMVmRUSxlGsbXcg