Go和Rust是最近几年非常火的语言,经常有人问到底该怎么选择,特别是谁更适合搭建网络后台服务,哪一个性能更好,稳定性更高。
网络上Go和Rust的比较文章很多,大体上是做一个测试或写几段测试代码,根据运行的时长来比较哪个性能更好,但这种测试可能会陷入误区:
1)比来比去,比的是网络IO,因为这种测试中语言特性在PK中占比很小,小到可以忽略。
2)无法模拟业务环境的重负荷下对性能和稳定性的影响。
这种测试也不符合实际情况,原因:
1)很少有业务场景需要极高的并发性能,因为业务负荷重,并发就做不高,高并发时服务的稳定性更重要。
2)如果性能确实不够了,优先去重构流程或架构,或多加几台机器做负载均衡。
当年做电信服务时(还在使用j2ee-ejb)out of memory是难以挥去的噩梦,所以本文是从内存角度来比较Go和Rust,测试在高并发下Go和Rust的内存使用情况。为了更好的做横向比较,将Java作为陪练一起PK。
先说一下测试环境:虚机环境做服务端,宿主机做客户端,使用这个环境主要是以下考虑:
1)每次测试都是重启虚机,这样可以保证所有测试的环境是稳定一致的。
2)宿主机(客户端)访问虚机(服务端),也可以保证网络环境是稳定一致的。
测试采样使用了图形化SSH工具软件OnTheSS( 下载 )。先看下虚机空载时的系统状态:
- 系统:CentOS 7.9
- CPU:AMD R7 4800U (笔记本CPU) 4个线程核(虚机),空载时CPU使用率很低且稳定。
- 内存:总量3.77G(单位不同,实际是4G),已使用0.62G
- 网络:ens33网卡每秒有11k的流量,虚机是CentOS带桌面的系统,所以本身有一定的网络流量。
- TCP:只有22端口有连接,这2个连接是测试的shell终端和OnTheSSH工具的。
先把客户端代码贴出来,比较简单,即使你没有学过Rust语言,也不影响理解:
use std::net::TcpStream; use std::io::{Read, Write}; fn main() { let msg = "abcdefg0123456789".as_bytes(); let mut buf: [u8; 1024] = [0; 1024]; for _ in 0..50000 { let mut socket = TcpStream::connect("192.168.152.130:9000").unwrap(); //发送 socket.write_all(msg).unwrap(); //接收 let len = socket.read(&mut buf).unwrap(); let recv_msg = std::str::from_utf8(&buf[0..len]).unwrap(); println!("{}", recv_msg); } }
客户端进行了5万次循环,每次循环都是从创建socket连接开始,发送一小段文字,再接收服务端返回的信息并打印出来。注意每次创建的socket并没有close,因为在rust语言中socket变量随生命周期的结束(循环结束时),会自动释放连接。
1、基准测试
基准测试目的是证明在服务端轻负荷下,性能测试是区分不了语言特性的,三种语言的服务端代码如下:
1)Go
package main import ( "fmt" "net" ) func main(){ listen, _ := net.Listen("tcp", ":9000") fmt.Printf("侦听端口 9000") buf := make([]byte, 1024) for { conn, _ := listen.Accept() //接收 size, _ := conn.Read(buf)
//发送 size, _ = conn.Write(buf[0:size]) conn.Close() } }
服务端是简单的ECHO服务,创建TCP侦听端口9000,接收客户端发来的信息,再将信息发回,注意每次应答后立即关闭socket(短连接服务)。下面的Rust和Java语义相同。
2)Rust
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; use std::io::{Read, Write}; fn main() { let ip = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); let port = 9000; let addr = SocketAddr::new(ip, port); println!("侦听端口: 9000"); let mut buf: [u8; 1024] = [0; 1024]; let listener = TcpListener::bind(addr).unwrap(); loop { let (mut socket, _) = listener.accept().unwrap(); //接收 let len = socket.read(&mut buf).unwrap(); //发送 let send_msg = &buf[0..len]; socket.write_all(send_msg).unwrap(); } }
3)Java
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket;public class Echo { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(9000); System.out.println("listen port 9000"); byte[] buf = new byte[1024]; while (true) { Socket socket = server.accept(); //接收 InputStream in = socket.getInputStream(); int len = in.read(buf); //发送 OutputStream out = socket.getOutputStream(); out.write(buf, 0, len); //close socket.close(); } } }
【测试结果】
用OnTHeSSH的网络监测来反馈基准测试的结果,5万次socket调用开始到结束(如下图)。和预想的结果一样,没有多大差别。基准测试中性能主要体现在socket读写,即使再换几种编程语言,结果应该也是差不离的。
2、模拟业务环境测试
基准测试非常特殊,而一般的业务环境不可能搭建这种“串行”的服务,另外在基准测试中服务没有载荷,纯粹是拼网络IO,考验的是Linux系统网络吞吐(TCP内核),和语言关系不大。
所以第二轮测试模拟了业务场景,改动有两处:第一是改为并行(多线程),第二是增加了5次UUID的获取来模拟任务负荷。
1)Go
package main import ( "fmt" "net" "github.com/google/uuid" ) func main(){ listen, _ := net.Listen("tcp", ":9000") fmt.Printf("侦听端口 9000") buf := make([]byte, 1024) for { conn, _ := listen.Accept() go func(){ //接收 size, _ := conn.Read(buf) //业务负载,5次uuid的生成 uuid.New().String() uuid.New().String() uuid.New().String() uuid.New().String() uuid.New().String() //发送 size, _ = conn.Write(buf[0:size]) conn.Close() }() } }
Go语言的并发使用了协程(用户态线程),理论上比内核管理的传统线程效率高。
2)Rust
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; use std::io::{Read, Write}; use std::thread; use uuid::Uuid; fn main() { let ip = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); let port = 9000; let addr = SocketAddr::new(ip, port); println!("侦听端口: 9000"); let mut buf: [u8; 1024] = [0; 1024]; let listener = TcpListener::bind(addr).unwrap(); loop { let (mut socket, _) = listener.accept().unwrap(); thread::spawn(move ||{ //接收 let len = socket.read(&mut buf).unwrap(); //业务负载,5次生成uuid let _uuid = Uuid::new_v4().to_string(); let _uuid = Uuid::new_v4().to_string(); let _uuid = Uuid::new_v4().to_string(); let _uuid = Uuid::new_v4().to_string(); let _uuid = Uuid::new_v4().to_string(); //发送 let send_msg = &buf[0..len]; socket.write_all(send_msg).unwrap(); }); } }
Rust并发使用普通的线程机制,并没有使用Tokio异步库。
3)Java
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.UUID; public class Echo { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(9000); System.out.println("listen port 9000"); byte[] buf = new byte[1024]; while (true) { Socket socket = server.accept(); new Thread(){ @Override public void run() { try { //接收 InputStream in = socket.getInputStream(); int len = in.read(buf); //业务负载,5次uuid生成 UUID.randomUUID().toString(); UUID.randomUUID().toString(); UUID.randomUUID().toString(); UUID.randomUUID().toString(); UUID.randomUUID().toString(); //发送 OutputStream out = socket.getOutputStream(); out.write(buf, 0, len); //close socket.close(); } catch (Exception e) { e.printStackTrace(); } } }.start(); } } }
Java并发也是使用普通线程机制。
【测试结果】
1)性能:
为了避免陷入到谁家的UUID库的效率高的争论,本轮PK的重点不在效率上,但对比图还是贴一下。OnTheSSH提供的网络吞吐图没有反馈出时间长短的显著差异(横轴跨度),Go相对更平滑一些,可能是和用户态线程有关,但不影响大局。因此性能PK的结果和基准测试一样:还是差不多。
2)内存:
再看OnTheSSH提供的内存使用状态:
内存差异性就非常明显了:物理内存使用最少的是Rust,700多K,其次是Go,11M,最多的是Java,300多M。虚存总量差异也是极大:Rust用了82M,Go用了912M,Java用了3.5G。
下图是OnTheSSH提供的进程状态对比,图太大有点看不清,注意划红线的两处对比,靠上红线处是进程运行过程中分配虚存的峰值的大小,靠下红线处是进行运行过程中分配到底物理内存峰值大小:
从内存使用角度PK,Rust是绝对领先的,差不多领先Go一个数量级,这点在测试前和我的预计相同,毕竟Go和Java都是带垃圾回收的语言,在高并发下内存回收有个过程。出乎预料的是Go和Java同是GC,但GC效果相差这么大,难怪当年经常碰到 Out of Memory。
在测试中只是用了5次UUID的生成来模拟业务负荷,实际应用中往往业务负荷要比这重得多,因此少用内存节省资源,是服务能长期可靠运行的必要条件。
3、测试结论
经过两轮测试,总结一下:
1、以绝对任务时间长短来比较语言的并发性能,没多大意义。
2、高并发下要更关注内存资源,从内存使用角度:Rust >> Go >> Java
标签:socket,vs,let,Go,buf,Rust,uuid From: https://www.cnblogs.com/dyf029/p/17768680.html