【性能】性能比较:REST vs gRPC vs 异步通信

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
简介: 【性能】性能比较:REST vs gRPC vs 异步通信

微服务之间的通信方式对微服务架构内的各种软件质量因素有重大影响(有关微服务网络内通信的关键作用的更多信息)。沟通方式会影响软件的性能和效率等功能性需求,以及可变性、可扩展性和可维护性等非功能性需求。因此,有必要考虑不同方法的所有优缺点,以便在具体用例中合理选择正确的沟通方式。

本文比较了以下样式:REST、gRPC 和使用消息代理 (RabbitMQ) 的异步通信,在微服务网络中了解它们对软件的性能影响。沟通方式的一些最重要的属性(反过来会影响整体表现)是:

  • 数据传输格式
  • 连接处理
  • 消息序列化
  • 缓存
  • 负载均衡

数据传输格式

虽然使用 AMQP 协议(高级消息队列协议)的异步通信和 gRPC 通信使用二进制协议进行数据传输,但 REST-API 通常以文本格式传输数据。与基于文本的协议相比,二进制协议的效率要高得多 [1,2]。因此,使用 gRPC 和 AMQP 进行通信会导致较低的网络负载,而使用 REST API 时可以预期更高的网络负载。

连接处理

REST-API 通常建立在 HTTP/1.1 协议之上,而 gRPC 依赖于 HTTP/2 协议的使用。HTTP/1.1、HTTP/2 以及 AMQP 都在传输层使用 TCP 来确保稳定的连接。要建立这样的连接,需要在客户端和服务器之间进行详细的通信。这些性能影响同样适用于所有沟通方式。但是,对于 AMQP 或 HTTP/2 连接,通信连接的初始建立只需要执行一次,因为这两种协议的请求都可以多路复用。这意味着可以将现有连接重用于使用异步或 gRPC 通信的后续请求。另一方面,使用 HTTP/1.1 的 REST-API 为与远程服务器的每个请求建立新连接。


Necessary communication to establish a TCP-Connection

消息序列化

通常,在通过网络传输消息之前,使用 JSON 执行 REST 和异步通信以进行消息序列化。另一方面,gRPC 默认以协议缓冲区格式传输数据。协议缓冲区通过允许使用更高级的序列化和反序列化方法来编码和使用消息内容 [1] 来提高通信速度。然而,选择正确的消息序列化格式取决于工程师。关于性能,protocol buffers 有很多优势,但是当必须调试微服务之间的通信时,依赖人类可读的 JSON 格式可能是更好的选择。

缓存

有效的缓存策略可以显着减少服务器的负载和必要的计算资源。由于其架构,REST-API 是唯一允许有效缓存的通信方式。REST-API 响应可以被其他服务器和缓存代理(如 Varnish)缓存和复制。这减少了 REST 服务的负载并允许处理大量的 HTTP 流量 [1]。但是,这只有在基础架构上部署更多服务(缓存代理)或使用第三方集成后才有可能。gRPC 官方文档和 RabbitMQ 文档都没有介绍任何形式的缓存。

负载均衡

除了临时存储响应之外,还有其他技术可以提高服务速度。负载均衡器(例如 mod_proxy)可以高效透明的方式在服务之间分配 HTTP 流量 [1]。这可以实现使用 REST API 的服务的水平扩展。Kubernetes 作为容器编排解决方案,无需任何调整即可对 HTTP/1.1 流量进行负载均衡。另一方面,对于 gRPC,需要在网络上提供另一个服务(linkerd)[3]。异步通信无需进一步的帮助即可支持负载平衡。消息代理本身扮演负载均衡器的角色,因为它能够将请求分发到同一服务的多个实例。消息代理为此目的进行了优化,并且它们的设计已经考虑到它们必须具有特别可扩展性的事实[1]。

实验

为了能够评估各个通信方法对软件质量特性的影响,开发了四个微服务来模拟电子商务平台的订单场景。

微服务部署在由三个不同服务器组成的自托管 Kubernetes 集群上。服务器通过千兆 (1000 Mbit/s) 网络连接,位于同一数据中心,服务器之间的平均延迟为 0.15 毫秒。每次实验运行时,各个服务都部署在相同的服务器上。这种行为是通过 pod 亲和性来实现的。

所有微服务都是用 GO 编程语言实现的。个别服务的实际业务逻辑,例如与数据库的通信,为了不被选择的通信方法之外的其他影响,故意不实现。因此,收集的结果不能代表这种类型的微服务架构,但可以使实验中的通信方法具有可比性。相反,业务逻辑的实现是通过将程序流程延迟 100 毫秒来模拟的。因此,在通信中,总延迟为 400 毫秒。

开源软件k6用于实现负载测试。

实现

Golang 标准库中包含的 net/http 模块用于提供 REST 接口。使用标准库中也包含的 encoding/json 模块对请求进行序列化和反序列化。所有请求都使用 HTTP POST 方法。

“谈话很便宜。给我看看密码。”

package main
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "github.com/google/uuid"
    "gitlab.com/timbastin/bachelorarbeit/common"
    "gitlab.com/timbastin/bachelorarbeit/config"
)
type restServer struct {
    httpClient http.Client
}
func (server *restServer) handler(res http.ResponseWriter, req *http.Request) {
    // only allow post request.
    if req.Method != http.MethodPost {
        bytes, _ := json.Marshal(map[string]string{
            "error": "invalid request method",
        })
        http.Error(res, string(bytes), http.StatusBadRequest)
        return
    }
    reqId := uuid.NewString()
    // STEP 1 / 4
    log.Println("(REST) received new order", reqId)
    var submitOrderDTO common.SubmitOrderRequestDTO
    b, _ := ioutil.ReadAll(req.Body)
    err := json.Unmarshal(b, &submitOrderDTO)
    if err != nil {
        log.Fatalf(err.Error())
    }
    checkIfInStock(1)
    invoiceRequest, _ := http.NewRequest(http.MethodPost, 
    fmt.Sprintf("%s/invoices", config.MustGet("customerservice.rest.address").
     (string)), bytes.NewReader(b))
    // STEP 2
    r, err := server.httpClient.Do(invoiceRequest)
    // just close the response body
    r.Body.Close()
    if err != nil {
        panic(err)
    }
    shippingRequest, _ := http.NewRequest(http.MethodPost, 
    fmt.Sprintf("%s/shipping-jobs", config.MustGet("shippingservice.rest.address").
     (string)), bytes.NewReader(b))
    // STEP 3
    r, err = server.httpClient.Do(shippingRequest)
    // just close the response body
    r.Body.Close()
    if err != nil {
        panic(err)
    }
    handleProductDecrement(1)
    // STEP 5
    res.WriteHeader(201)
    res.Write(common.NewJsonResponse(map[string]string{
        "state": "success",
    }))
}
func startRestServer() {
    server := restServer{
        httpClient: http.Client{},
    }
    http.HandleFunc("/orders", server.handler)
    done := make(chan int)
    go http.ListenAndServe(config.MustGet("orderservice.rest.port").(string), nil)
    log.Println("started rest server")
    <-done
}

RabbitMQ 消息代理用于异步通信,部署在同一个 Kubernetes 集群上。消息代理和各个微服务之间的通信使用 github.com/spreadway/amqp 库进行。该库是 GO 编程语言官方文档推荐的。

package main
import (
    "encoding/json"
    "log"
    "github.com/streadway/amqp"
    "gitlab.com/timbastin/bachelorarbeit/common"
    "gitlab.com/timbastin/bachelorarbeit/config"
    "gitlab.com/timbastin/bachelorarbeit/utils"
)
func handleMsg(message amqp.Delivery, ch *amqp.Channel) {
    log.Println("(AMQP) received new order")
    var submitOrderRequest common.SubmitOrderRequestDTO
    err := json.Unmarshal(message.Body, &submitOrderRequest)
    utils.FailOnError(err, "could not unmarshal message")
    checkIfInStock(1)
    handleProductDecrement(1)
    ch.Publish(config.MustGet("amqp.billingRequestExchangeName").(string), "", 
     false, false, amqp.Publishing{
        ContentType: "application/json",
        Body:        message.Body,
    })
}
func getNewOrderChannel(conn *amqp.Connection) (*amqp.Channel, string) {
    ch, err := conn.Channel()
    utils.FailOnError(err, "could not create channel")
    ch.ExchangeDeclare(config.MustGet("amqp.newOrderExchangeName").
    (string), "fanout", false, false, false, false, nil)
    queue, err := ch.QueueDeclare(config.MustGet("orderservice.amqp.consumerName").
    (string), false, false, false, false, nil)
    utils.FailOnError(err, "could not create queue")
    ch.QueueBind(queue.Name, "", config.MustGet("amqp.newOrderExchangeName").
    (string), false, nil)
    return ch, queue.Name
}
func startAmqpServer() {
    conn := common.NewAmqpConnection(config.MustGet("amqp.host").(string))
    defer conn.Close()
    orderChannel, queueName := getNewOrderChannel(conn)
    msgs, err := orderChannel.Consume(
        queueName,
        config.MustGet("orderservice.amqp.consumerName").(string),
        true,
        false,
        false,
        false,
        nil,
    )
    utils.FailOnError(err, "could not consume")
    forever := make(chan bool)
    log.Println("started amqp server:", queueName)
    go func() {
        for d := range msgs {
            go handleMsg(d, orderChannel)
        }
    }()
    <-forever
}

gRPC 客户端和服务器使用 gRPC 文档推荐的 google.golang.org/grpc 库。数据的序列化是使用协议缓冲区完成的。

package main
import (
    "log"
    "net"
    "context"
    "gitlab.com/timbastin/bachelorarbeit/common"
    "gitlab.com/timbastin/bachelorarbeit/config"
    "gitlab.com/timbastin/bachelorarbeit/pb"
    "gitlab.com/timbastin/bachelorarbeit/utils"
    "google.golang.org/grpc"
)
type OrderServiceServer struct {
    CustomerService pb.CustomerServiceClient
    ShippingService pb.ShippingServiceClient
    pb.UnimplementedOrderServiceServer
}
func (s *OrderServiceServer) SubmitOrder(ctx context.Context, 
    request *pb.SubmitOrderRequest) (*pb.SuccessReply, error) {
    log.Println("(GRPC) received new order")
    if s.CustomerService == nil {
        s.CustomerService, _ = common.NewCustomerServiceClient()
    }
    if s.ShippingService == nil {
        s.ShippingService, _ = common.NewShippingServiceClient()
    }
    checkIfInStock(1)
    // call the product service on each iteration to decrement the product.
    _, err := s.CustomerService.CreateAndProcessBilling(ctx, &pb.BillingRequest{
        BillingInformation: request.BillingInformation,
        Products:           request.Products,
    })
    utils.FailOnError(err, "could not process billing")
    // trigger the shipping job.
    _, err = s.ShippingService.CreateShippingJob(ctx, &pb.ShippingJob{
        BillingInformation: request.BillingInformation,
        Products:           request.Products,
    })
    utils.FailOnError(err, "could not create shipping job")
    handleProductDecrement(1)
    return &pb.SuccessReply{Success: true}, nil
}
func startGrpcServer() {
    listen, err := net.Listen("tcp", config.MustGet("orderservice.grpc.port").(string))
    if err != nil {
        log.Fatalf("could not listen: %v", err)
    }
    grpcServer := grpc.NewServer()
    orderService := OrderServiceServer{}
    // inject the clients into the server
    pb.RegisterOrderServiceServer(grpcServer, &orderService)
    // start the server
    log.Println("started grpc server")
    if err := grpcServer.Serve(listen); err != nil {
        log.Fatalf("could not start grpc server: %v", err)
    }
}

收集数据

检查成功和失败的订单处理的数量,以确认它们所经过的时间。如果直到确认的持续时间超过 900 毫秒,则订单流程被解释为失败。选择此持续时间是因为在实验中可能会出现无限长的等待时间,尤其是在使用异步通信时。每次试验都会报告失败和成功订单的数量。

每种架构总共进行了 12 次不同的测量,每种情况下同时请求的数量不同,传输的数据量也不同。首先,在低负载下测试每种通信方式,然后在中等负载下,最后在高负载下测试。低负载模拟 10 个,中等负载模拟 100 个,高负载模拟 300 个同时向系统发出的请求。在这六次测试运行之后,要传输的数据量会增加,以了解各个接口的序列化方法的效率。数据量的增加是通过订购多个产品来实现的。

结果

gRPC API 架构是实验中研究的性能最佳的通信方法。在低负载下,它可以接受的订单数量是使用 REST 接口的系统的 3.41 倍。此外,平均响应时间比 REST-API 低 9.71 毫秒,比 AMQP-API 低 9.37 毫秒。

相关文章
|
24天前
|
自然语言处理 负载均衡 API
gRPC 一种现代、开源、高性能的远程过程调用 (RPC) 可以在任何地方运行的框架
gRPC 是一种现代开源高性能远程过程调用(RPC)框架,支持多种编程语言,可在任何环境中运行。它通过高效的连接方式,支持负载平衡、跟踪、健康检查和身份验证,适用于微服务架构、移动设备和浏览器客户端连接后端服务等场景。gRPC 使用 Protocol Buffers 作为接口定义语言,支持四种服务方法:一元 RPC、服务器流式处理、客户端流式处理和双向流式处理。
|
Dubbo Java 测试技术
分布式RPC框架性能大比拼 dubbo、motan、rpcx、gRPC、thrift的性能比较
Dubbo 是阿里巴巴公司开源的一个Java高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。不过,略有遗憾的是,据说在淘宝内部,dubbo由于跟淘宝另一个类似的框架HSF(非开源)有竞争关系,导致dubbo团队已经解散(参见http://www.oschina.net/news/55059/druid-1-0-9 中的评论),反到是当当网的扩展版本仍在持续发展,墙内开花墙外香。
7259 0
|
1月前
|
网络协议 算法
RPC为何比较高效?
RPC为何比较高效?
58 0
|
3月前
|
人工智能 缓存 安全
Golang 搭建 WebSocket 应用(七) - 性能、可用性
Golang 搭建 WebSocket 应用(七) - 性能、可用性
53 1
|
5月前
|
Dubbo 前端开发 Java
Dubbo3 服务原生支持 http 访问,兼具高性能与易用性
本文展示了 Dubbo3 triple 协议是如何简化从协议规范与实现上简化开发测试、入口流量接入成本的,同时提供高性能通信、面向接口的易用性编码。
16643 14
|
4月前
|
网络协议 Dubbo Java
什么是RPC?RPC和HTTP对比?RPC有什么缺点?市面上常用的RPC框架?
选择合适的RPC框架和通信协议,对于构建高效、稳定的分布式系统至关重要。开发者需要根据自己的业务需求和系统架构,综合考虑各种因素,做出适宜的技术选型。
461 1
|
6月前
|
Dubbo 网络协议 Java
性能基础之常见RPC框架浅析
【4月更文挑战第23天】性能基础之常见RPC框架浅析
252 1
性能基础之常见RPC框架浅析
|
JSON Cloud Native 网络协议
gRPC简介: Google的高性能RPC框架
gRPC简介: Google的高性能RPC框架
258 0
|
XML JSON 自然语言处理
gRPC系列 :RPC 框架原理是?gRPC 是什么?gRPC设计原则
gRPC系列 :RPC 框架原理是?gRPC 是什么?gRPC设计原则
1713 0
gRPC系列 :RPC 框架原理是?gRPC 是什么?gRPC设计原则
|
运维 监控 Dubbo
Dubbo协议异步单一长连接原理与优势
Dubbo协议异步单一长连接原理与优势
568 0