gprc从启动到运行 - 在生产中运行gRPC

我们将讨论如何为gRPC服务和客户端开发单元测试或集成测试,以及如何将它们与持续集成工具集成。 然后我们将进入gRPC应用程序的连续部署,我们将探索一些在虚拟机(vm)、Docker和Kubernetes上的部署模式。 最后,要在生产环境中运行gRPC应用程序,您需要有一个可靠的可观察平台形式。 在这里,我们将讨论用于gRPC应用程序的不同可观察性工具,并探索gRPC应用程序的故障排除和调试技术。 让我们从测试这些应用程序开始讨论。

测试gRPC应用程序

您开发的任何软件应用程序(包括gRPC应用程序)都需要与应用程序相关联的单元测试。 由于gRPC应用程序总是与网络交互,所以测试还应该涵盖服务器和客户机gRPC应用程序的网络RPC方面。 我们将从测试gRPC服务器开始。

测试gRPC服务器

gRPC服务测试通常使用gRPC客户端应用程序作为测试用例的一部分来完成。 服务器端测试包括使用所需的gRPC服务启动一个gRPC服务器,然后使用实现测试用例的客户机应用程序连接到服务器。 让我们看一下为我们的ProductInfo服务的Go实现编写的示例测试用例。 在Go中,gRPC测试用例的实现应该通过testing包作为Go的通用测试用例来实现。

func TestServer_AddProduct(t *testing.T) {
	initGRPCServerHTTP2() // 在HTTP2上启动常规的gRPC服务器
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("无法连接: %v", err)
	}
	defer conn.Close()
	c := pb.NewProductInfoClient(conn)

	name := "张三"
	description := "张三的个人描述"
	price := float32(700.0)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.AddProduct(ctx, &pb.Product{Name: name, Description: description, Price: price})
	if err != nil {
		log.Fatalf("无法添加产品: %v", err)
	}
	log.Printf("Res %s", r.Value)
}

func initGRPCServerHTTP2() {
	lis, err := net.Listen("tcp", "9988")

	if err != nil {
		log.Fatalf("监听失败: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterProductInfoServer(s, &server{})
	// 在gRPC服务器上注册反射服务。
	reflection.Register(s)
	go func() {
		if err := s.Serve(lis); err != nil {
			log.Fatalf("服务开启失败: %v", err)
		}
	}()
}

//skips ....

由于gRPC测试用例是基于标准语言测试用例的,所以您执行它们的方式与标准测试用例没有什么不同。 服务器端gRPC测试的一个特殊之处在于,它们要求服务器应用程序打开客户机应用程序连接到的一个端口。 如果你不愿意这样做,或者你的测试环境不允许这样做,你可以使用一个库来帮助避免启动一个使用真实端口号的服务。 在Go中,您可以使用bufconn包,它提供了一个网络。 由缓冲区和相关的拨号和监听功能实现的Conn

// google.golang.org/grpc/test/bufconn
// 包bufconn提供了一个网络。 由缓冲区和相关的拨号和监听功能实现的Conn  
func initGRPCServerBuffConn() {
	listener = bufconn.Listen(bufSize)
	s := grpc.NewServer()
	pb.RegisterProductInfoServer(s, &server{})

	reflection.Register(s)
	go func() {
		if err := s.Serve(listener); err != nil {
			log.Fatalf("failed to serve: %v", err)
		}
	}()
}

func TestServer_AddProductBufConn(t *testing.T) {
	ctx := context.Background()
	initGRPCServerBuffConn()
	conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(getBufDialer(listener)), grpc.WithInsecure())
	if err != nil {
		log.Fatalf("无法连接: %v", err)
	}
	defer conn.Close()
	c := pb.NewProductInfoClient(conn)

	name := "张三"
	description := "张三的个人描述"
	price := float32(700.0)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.AddProduct(ctx, &pb.Product{Name: name, Description: description, Price: price})
	if err != nil {
		log.Fatalf("无法添加产品: %v", err)
	}
	log.Printf("Res %s", r.Value)
}

测试gRPC客户端

当我们为gRPC客户端开发测试时,一种可能的测试方法是启动一个gRPC服务器并实现一个模拟服务。 然而,这并不是一个非常简单的任务,因为它需要打开端口并连接到服务器。 因此,要测试客户端逻辑而不需要连接到真实服务器,您可以使用模拟框架。 对gRPC服务器端的模仿使开发人员能够编写轻量级单元测试来检查客户端的功能,而无需调用RPC调用到服务器。

如果使用Go开发gRPC客户端应用程序,则可以使用Gomock来模拟客户端接口(使用生成的代码),并通过编程设置其方法以期望和返回预先确定的值。 使用Gomock,您可以使用以下方法为gRPC客户端应用程序生成模拟接口。

负载测试

使用传统工具对gRPC应用程序进行负载测试和基准测试是很困难的,因为这些应用程序或多或少被绑定到特定的协议,比如HTTP。因此,对于gRPC,我们需要定制的负载测试工具,这些工具可以通过向服务器生成虚拟的rpc负载来对gRPC服务器进行负载测试。

ghz就是这样一个负载测试工具; 它使用Go实现为一个命令行实用程序。 它可以用于本地测试和调试服务,也可以用于自动持续集成环境中进行性能回归测试。 例如,使用ghz,您可以使用以下命令运行负载测试:

# https://ghz.sh
ghz --insecure --proto ./proto/order.proto --call pd.OrderService.SearchOrder -d '{"value":"123"}' -n 2000 -c 20 127.0.0.1:9988

Summary:
  Count:        2000
  Total:        1.06 s
  Slowest:      56.17 ms
  Fastest:      0.16 ms
  Average:      6.78 ms
  Requests/sec: 1891.87

Response time histogram:
  0.162  [1]    |
  5.762  [1077] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  11.362 [690]  |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  16.963 [143]  |∎∎∎∎∎
  22.563 [27]   |28.163 [30]   |33.764 [8]    |
  39.364 [14]   |44.964 [0]    |
  50.565 [7]    |
  56.165 [3]    |

Latency distribution:
  10 % in 1.69 ms 
  25 % in 2.98 ms 
  50 % in 5.33 ms 
  75 % in 8.73 ms 
  90 % in 11.96 ms 
  95 % in 15.44 ms 
  99 % in 35.79 ms 

Status code distribution:
  [OK]   2000 responses   

这里,我们不安全地调用了OrderService服务的SearchOrder 远程方法。 我们可以指定请求总数(-n 2000)和并发数(20个线程)。 结果还可以以各种输出格式生成。 一旦具备了所需的服务器端和客户端测试,就可以将它们与所使用的持续集成工具集成起来。

部署

现在,让我们研究一下我们开发的gRPC应用程序的不同部署方法。如果你打算在本地或虚拟机上运行一个gRPC服务器或客户端应用程序,部署仅仅取决于你为相应的gRPC应用程序编程语言生成的二进制文件。对于本地或基于虚拟机的部署,gRPC服务器应用程序的可伸缩性和高可用性通常是通过标准部署实践来实现的,例如使用支持gRPC协议的负载平衡器。

大多数现代应用程序现在都部署为容器。因此,了解如何在容器上部署gRPC应用程序是非常有用的。Docker是基于容器的应用部署的标准平台。

部署在Docker

Docker是一个用于开发、发布和运行应用程序的开放平台。使用Docker,您可以将应用程序从基础设施中分离出来。它提供了在称为容器的隔离环境中打包和运行应用程序的能力,这样您就可以在同一主机上运行多个容器。容器比传统的vm轻得多,可以直接在主机的内核中运行。让我们看一些将gRPC应用程序部署为Docker容器的例子。

一旦你开发了一个gRPC服务器应用程序,你可以为它创建一个Docker容器。下面例子为基于go的gRPC服务器的Dockerfile。在Dockerfile中有许多特定于grpc的构造。在这个例子中,我们使用了多阶段Docker构建,我们在阶段1构建应用程序,然后在阶段2运行应用程序,作为一个更轻量级的运行时。生成的服务器端代码也会在构建应用程序之前添加到容器中。

FROM golang AS build

ENV location /home/zhangyong/go/src/server
ENV GOPROXY https://goproxy.cn,direct
ENV GO111MODULE on


WORKDIR ${location}

ADD ./pd ${location}/pd
ADD ./proto ${location}/proto
ADD ./go.mod ${location}/go.mod
ADD ./main.go ${location}/main.go

RUN go get -u ./...

RUN CGO_ENABLED=0 go build -o /bin/grpc-order-server


FROM scratch
COPY --from=build /bin/grpc-order-server /bin/grpc-order-server


ENTRYPOINT ["/bin/grpc-order-server"]
EXPOSE 9988

创建Dockerfile后,您可以使用以下方法构建Docker映像:

sudo docker run --name=order-server -p 9988:9988 grpc-order-server 

以使用相同的方法创建gRPC客户机应用程序。这里的一个例外是,由于我们在Docker上运行服务器应用程序,客户机应用程序用来连接到gRPC的主机名和端口现在不同了。

当我们在Docker上同时运行服务器和客户端gRPC应用程序时,它们需要相互通信,并通过主机与外部世界通信。所以必须有一层网络。Docker支持不同类型的网络,每种类型都适合特定的使用情况。因此,当我们运行服务器端和客户端Docker容器时,我们可以指定一个公共网络,以便客户端应用程序可以根据主机名发现服务器应用程序的位置。这意味着必须更改客户机应用程序代码,以便它连接到服务器的主机名。例如,我们的Go gRPC应用程序必须修改为调用服务主机名,而不是localhost:

conn, err := grpc.Dial("orderserver:50051", grpc.WithInsecure())

您可以从环境中读取主机名,而不是在客户机应用程序中硬编码它。一旦你完成了对客户端应用程序的更改,你需要重建Docker映像,然后运行服务器和客户端映像,如图所示:

docker run -it --network=mynet --name=order-server --hostname=orderserver -p 9988:9988 grpc-server-server

docker run -it --network=mynet --hostname=orderclient grpc-order-client

当启动Docker容器时,您可以指定给定容器运行的Docker网络。如果服务共享相同的网络,那么客户机应用程序可以使用docker run命令提供的主机名来发现主机服务的实际地址。

当您运行的容器数量较少且它们的交互相对简单时,那么您就可以完全在Docker上构建您的解决方案。然而,大多数实际场景都需要管理多个容器及其交互。仅仅基于Docker构建这样的解决方案是相当乏味的。这时就要用到容器编排平台了。

可观察性

正如我们在前一节中讨论的,gRPC应用程序通常在容器化环境中部署和运行,其中有多个这样的容器通过网络运行并相互通信。然后是如何跟踪每个容器并确保它们实际工作的问题。这就是可观测性的作用。正如维基百科的定义所述,“可观察性是一种衡量系统内部状态从其外部输出的知识推断得有多好的方法。”基本上,拥有系统可观测性的目的是为了回答这个问题:“系统中现在是否有什么东西出了问题?”如果答案是肯定的,我们还应该能够回答一系列其他问题,比如“出了什么问题?”以及“为什么会这样?”如果我们能在任何给定的时间和系统的任何部分回答这些问题,我们就可以说我们的系统是可观察的。同样需要注意的是,可观察性是系统的一个属性,它与效率、可用性和可靠性同样重要。因此,在构建gRPC应用程序时,必须从一开始就考虑到这一点。当谈到可观察性时,我们通常会谈到三个主要支柱:度量日志记录跟踪。这些是用来获得系统可观测性的主要技术。让我们在接下来的小节中分别讨论它们。

度量

度量是在一段时间间隔内测量的数据的数字表示。当谈到度量时,我们可以收集两种类型的数据。一个是系统级指标,如CPU使用率、内存使用率等。另一个是应用层满足的指标,如入站请求率、请求错误率等。

系统级指标通常在应用程序运行时获取。现在,有很多工具可以获取这些指标,而且通常由DevOps团队获取。但是应用程序级别的指标在应用程序之间是不同的。因此,在设计一个新的应用程序时,应用程序开发人员的任务是决定需要捕获什么样的应用程序级指标,以了解系统的行为。在本节中,我们将关注如何在应用程序中启用应用程序级度量。

OpenCensus

对于gRPC应用程序,OpenCensus库提供了一些标准度量。通过向客户机和服务器应用程序添加处理程序,我们可以轻松地启用它们。我们还可以添加我们自己的度量收集器(示例7-8)。

OpenCensus是一组用于收集应用程序度量和分布式跟踪的开源库;它支持多种语言。它从目标应用程序收集指标,并实时地将数据传输到您选择的后端。目前支持的后端包括Azure Monitor、Datadog、Instana、Jaeger、SignalFX、Stackdriver和Zipkin。我们也可以写我们自己的出口商为其他后端。

func main() {
    //启动z-Pages服务器。HTTP端点以端口8081中的/debug上下文开始,用于度量可视化。
	go func() {
		mux := http.NewServeMux()
		zpages.Handle(mux, "/debug")
		log.Fatal(http.ListenAndServe("127.0.0.1:8081", mux))
	}()

    //注册统计出口商以导出收集的数据。这里我们添加了PrintExporter,它将导出的数据记录到控制台。这只是为了演示的目的;通常不建议记录所有的生产负载。
	view.RegisterExporter(&exporter.PrintExporter{})

    //注册视图以收集服务器请求计数。这些是预定义的默认服务视图,它们收集每个RPC接收的字节、每个RPC发送的字节、每个RPC延迟和完成的RPC。我们可以编写自己的视图来收集数据。
	if err := view.Register(ocgrpc.DefaultServerViews...); err != nil {
		log.Fatal(err)
	}

	listen, err := net.Listen("tcp", ":9988")
	if err != nil {
		panic(err)
	}
	
    //创建一个带有stats处理程序的gRPC服务器。
	server := grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{}))
	pd.RegisterOrderServiceServer(server, &OrderServiceImpl{})

	log.Println("order service is starting....")
	err = server.Serve(listen)
	if err != nil {
		panic(err)
	}
}
func main() {
	view.RegisterExporter(&exporter.PrintExporter{})

	if err := view.Register(ocgrpc.DefaultServerViews...); err != nil {
		log.Fatal(err)
	}

	conn, err := grpc.Dial(":9988", grpc.WithStatsHandler(&ocgrpc.ClientHandler{}), grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	client := pd.NewOrderServiceClient(conn)
	ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancelFunc()
    //skip....
}

一旦我们运行服务器和客户机,我们就可以通过创建的HTTP端点访问服务器和客户机指标(例如,http://localhost:8081/debug/ rpcz上的RPC指标和http://localhost:8081/debug/tracez上的跟踪)。

如前所述,我们可以使用预定义的导出器将数据发布到支持的后端,或者我们可以编写自己的导出器将跟踪和指标发送到任何能够使用它们的后端。在下一节中,我们将讨论另一种流行的技术,Prometheus,它通常用于为gRPC应用程序启用指标。

Prometheus

Prometheus是一个用于系统监视和警报的开放源码工具包。您可以使用Prometheus来使用gRPC Prometheus库为您的gRPC应用程序启用度量。我们可以通过在客户机和服务器应用程序中添加拦截器来轻松实现这一点,我们还可以添加我们自己的指标收集器。

通过调用以上下文/指标开始的HTTP端点,Prometheus从目标应用程序收集指标。它存储所有收集到的数据,并对这些数据运行规则,以从现有数据汇总和记录新的时间序列,或生成警报。我们可以使用像Grafana这样的工具来可视化这些汇总结果。

启用Prometheus对gRPC Go服务器的监控:(省略客户端监控代码)

var (
    //创建一个度量注册表。它保存系统中注册的所有数据收集器。如果我们需要添加一个新的收集器,我们需要在这个注册表中注册它。
	reg                 = prometheus.NewRegistry()
    //创建标准的客户指标。这些是库中定义的预定义指标。
	grpcMetrics         = grpc_prometheus.NewServerMetrics()
    //order_mgt_server_handle_count的自定义度量计数器
	customMetricCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "order_mgt_server_handle_count",
		Help: "order number help",
	}, []string{"name"})
)

func init() {
    //将标准服务器指标和自定义指标收集器注册到创建的注册中心。
	reg.MustRegister(grpcMetrics, customMetricCounter)
}

func main() {
    //为Prometheus创建一个HTTP服务器。HTTP端点以9092端口上的上下文文本/度量开始,用于度量收集。
	httpServer := &http.Server{
		Handler: promhttp.HandlerFor(reg, promhttp.HandlerOpts{}),
		Addr:    fmt.Sprintf("0.0.0.0:%d", 9092)}

	listen, err := net.Listen("tcp", ":9988")
	if err != nil {
		panic(err)
	}

    //创建一个带有度量拦截器的gRPC服务器。这里我们使用grpcMetrics.aryServerInterceptor,因为我们有一
    元服务。还有另一个名为grpcMetrics.StreamServerInterceptor()的拦截器用于流服务。
	server := grpc.NewServer(grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()))
	pd.RegisterOrderServiceServer(server, &OrderServiceImpl{})
    //初始化所有标准度量。
	grpcMetrics.InitializeMetrics(server)

	// 为prometheus启动你的http服务器
	go func() {
		if err := httpServer.ListenAndServe(); err != nil {
			log.Fatal("Unable to start a http server.")
		}
	}()

	log.Println("order service is starting....")
	err = server.Serve(listen)
	if err != nil {
		panic(err)
	}
}

系统中基于指标的监视的一个优点是,处理指标数据的成本不会随着系统活动的增加而增加。例如,应用程序流量的增加不会增加磁盘利用率、处理复杂性、可视化速度、运营成本等处理成本。它有固定的开销。此外,一旦我们收集了指标,我们就可以进行大量的数学和统计转换,并得出关于系统的有价值的结论。另一个可观测性的支柱是日志,我们将在下一节中讨论它。

日志

日志是不可变的,带有时间戳的,对一段时间内发生的离散事件的记录。作为应用程序开发人员,我们通常将数据转储到日志中,以确定在给定的点上系统的内部状态和位置。日志的好处是它们是最容易生成的,而且比指标粒度更细。我们可以给它附加特定的操作或一堆上下文,比如唯一id,我们要做什么,堆栈跟踪,等等。缺点是它们非常昂贵,因为我们需要以一种便于搜索和使用的方式存储和索引它们。在gRPC应用程序中,我们可以使用拦截器启用日志记录。我们可以在客户端和服务器端附加一个新的日志拦截器,并记录每个远程调用的请求和响应消息。一旦你在你的gRPC应用程序中添加日志,它们将打印在控制台或日志文件中,这取决于你如何配置日志。如何配置日志记录取决于您使用的日志记录框架。

调试及故障排除

调试和故障排除是找出问题的根本原因并解决应用程序中出现的问题的过程。为了调试和排除问题,我们首先需要在较低的环境(称为开发或测试环境)中重现相同的问题。因此,我们需要一组工具来生成类似于生产环境的请求负载。

这个过程在gRPC服务中要比在HTTP服务中更加困难,因为工具需要同时支持基于服务定义的消息编码和解码,并且能够支持HTTP/2。通常用于测试HTTP服务的curl或Postman等工具不能用于测试gRPC服务。

但是有很多有趣的工具可用于调试和测试gRPC服务。您可以在强大的gRPC存储库中找到这些工具的列表。它包含了大量可用于gRPC的资源。调试gRPC应用程序最常用的方法之一是使用额外的日志记录。

我们可以提供额外的日志和跟踪,以诊断您的gRPC应用程序的问题。在gRPC Go应用程序中,我们可以通过设置以下环境变量来启用额外的日志:

GRPC_GO_LOG_VERBOSITY_LEVEL=99 #冗长意味着每五分钟要打印多少次信息信息。默认情况下,冗长度设置为0。
GRPC_GO_LOG_SEVERITY_LEVEL=info #设置日志级别为info。所有的信息信息将被打印出来。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值