Go 与 Rust:Web 服务性能对比

科技   2024-12-08 15:46   广东  

Go和Rust是当前技术领域中备受关注的两种编程语言,尤其是在构建高性能Web服务方面。本文将通过实现一个CPU密集型Web服务,比较这两种语言的性能表现。


简介

Go的特点

Go在Web开发领域已经确立了其高效强大的地位。凭借其简洁性、卓越的性能以及对并发的强大支持,Go成为构建可扩展分布式系统(尤其是云原生环境)的一种流行选择。Go的设计注重实用性和开发者的生产力,使其成为高吞吐量应用和微服务架构团队的常用语言。

Rust的特点

Rust作为一门高性能的系统编程语言,近年来也获得了广泛关注。Rust以内存安全、高效的资源利用和零成本抽象(Zero-Cost Abstraction)著称,逐渐被应用于对性能要求极高的领域,包括Web服务。虽然Rust传统上主要用于系统级编程,但随着其生态系统的扩展和框架(如Actix)的发展,Rust在Web开发中的地位也日益提升。

本文目标

本文将通过在Go和Rust中分别实现一个Web服务,比较两段文本的相似性,并评估它们的性能表现。我们将使用词频-逆文档频率(TF-IDF)余弦相似度(Cosine Similarity)算法,这两种算法在自然语言处理和信息检索领域被广泛用于衡量文本数据的相关性和相似性。


算法简介

词频-逆文档频率(TF-IDF)

TF-IDF是一种统计方法,用于评估一个词在文档中的重要性,相对于整个文档集合的权重。它结合了以下两个指标:

  • 词频(Term Frequency, TF):表示一个词在文档中出现的频率,相对于该文档的总词数。
  • 逆文档频率(Inverse Document Frequency, IDF):降低在多个文档中普遍出现的常见词(如“the”或“and”)的权重。

通过结合这两个指标,TF-IDF能够突出那些在特定文档中频繁出现但在整个语料库中较少见的词,从而更好地捕捉文本的特征。

余弦相似度(Cosine Similarity)

余弦相似度是一种用于衡量两个向量之间相似性的指标,与向量的大小无关。在文本比较的上下文中,向量表示文本中各词的TF-IDF分值。其计算公式如下:

  • 取值范围
    • 1 表示文本完全相同。
    • 0 表示文本完全不同。
    • 介于0和1之间的值表示不同程度的相似性。

TF-IDF和余弦相似度相辅相成:TF-IDF将原始文本转换为数值表示,捕捉词的重要性,而余弦相似度则量化这些数值表示之间的关系。两者结合构成了一个强大的文本相似性分析基础。


开发过程

接下来,我们将在Rust和Go中分别实现一个Web服务,使用上述算法比较两段文本的相似性。

Rust实现

以下是基于Rust的实现代码,使用了Actix-web框架:

use std::{collections::{HashMap, HashSet}, env};
use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
   let addr = env::var("ADDR").unwrap_or("127.0.0.1".to_string());
   let port = env::var("PORT").unwrap_or("8081".to_string()).parse().unwrap();

   HttpServer::new(|| App::new().service(similarity))
       .bind((addr.as_ref(), port))?
       .run()
       .await
}

#[derive(Deserialize)]
struct SimilarityRequest {
   text1: String,
   text2: String,
}

#[derive(Serialize)]
struct SimilarityResponse {
   similarity: f64,
   interpretation: String,
}

#[post("/similarity")]
pub async fn similarity(data: web::Json<SimilarityRequest>) -> impl Responder {
   let normalized1 = normalize_text(&data.text1);
   let normalized2 = normalize_text(&data.text2);
   let words1: Vec<&str> = normalized1.split_whitespace().collect();
   let words2: Vec<&str> = normalized2.split_whitespace().collect();

   let freq_map1 = generate_frequency_map(&words1);
   let freq_map2 = generate_frequency_map(&words2);

   let uniq: Vec<&str> = freq_map1
       .keys()
       .chain(freq_map2.keys())
       .cloned()
       .collect::<HashSet<&str>>()
       .into_iter()
       .collect();

   let total1 = words1.len();
   let total2 = words2.len();
   let tf1 = calculate_tf(&uniq, &freq_map1, total1);
   let tf2 = calculate_tf(&uniq, &freq_map2, total2);

   let idf = calculate_idf(&uniq, &freq_map1, &freq_map2);

   let tf_idf1 = calculate_tf_idf(&tf1, &idf);
   let tf_idf2 = calculate_tf_idf(&tf2, &idf);

   let similarity = calculate_similarity(&tf_idf1, &tf_idf2);
   let similarity = (similarity * 1000.0).round() / 1000.0;
   let interpretation = interpret_similarity(similarity);

   HttpResponse::Ok().json(SimilarityResponse {
       similarity,
       interpretation,
   })
}

/// Normalize text by converting to lowercase, removing punctuation, and collapsing whitespace.
fn normalize_text(text: &str) -> String {
   static RE_PUNCT: Lazy<Regex> = Lazy::new(|| Regex::new(r"[^\w\s]").unwrap());
   static RE_WHITESPACE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s+").unwrap());
   let lower = text.to_lowercase();
   let no_punct = RE_PUNCT.replace_all(&lower, "");
   let clean_text = RE_WHITESPACE.replace_all(&no_punct, " ");
   clean_text.trim().to_string()
}

// Additional helper functions omitted for brevity

此外,为了将该服务容器化,以下是对应的Dockerfile:

FROM rust:latest as builder
WORKDIR /text-similarity-rust
COPY . .
RUN cargo build --release

FROM gcr.io/distroless/cc-debian12
WORKDIR /app
COPY --from=builder /text-similarity-rust/target/release/text-similarity-rust .
CMD ["./text-similarity-rust"]

Go实现

以下是基于Go的实现代码,使用了Fiber框架:

package main

import (
   "math"
   "regexp"
   "strings"
   "github.com/gofiber/fiber/v2"
   "golang.org/x/exp/maps"
)

// Additional code omitted for brevity

同样,为了将该服务容器化,以下是对应的Dockerfile:

FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/app main.go

FROM alpine
WORKDIR /out
COPY --from=builder /out/app /out/app
CMD ["./app"]

性能测试

为了测试两种服务的性能,我编写了一个简单的k6脚本,模拟高并发负载。以下是测试脚本:

import { check } from 'k6';
import http from 'k6/http';

// Additional code omitted for brevity

测试结果

Rust性能

  • 请求总数:1,865,618
  • 吞吐量:3,887请求/秒
  • 响应时间
    • 平均:90.77ms
    • 中位数:74.79ms
    • 95分位:219.01ms
  • 失败率:~0.00%

Go性能

  • 请求总数:960,483
  • 吞吐量:2,001请求/秒
  • 响应时间
    • 平均:177.35ms
    • 中位数:22.28ms
    • 95分位:905.77ms
  • 失败率:0%

总结

Rust和Go各有优劣:

  • 性能:Rust在吞吐量和响应时间上表现更优。
  • 开发效率:Go代码更简洁,开发速度更快。

如果追求极致性能,Rust是更好的选择;如果需要快速开发并保持较好的性能,Go则更为合适。

两者各擅胜场,选择取决于具体需求。

点击关注并扫码添加进交流群
免费领取「Go 语言」学习资料

源自开发者
专注于提供关于Go语言的实用教程、案例分析、最新趋势,以及云原生技术的深度解析和实践经验分享。
 最新文章