如前所述,gRPC 应用程序正常部署和运行在容器化的环境中,其中会有多个这样的容器在运行,并通过网络进行彼此交流。这就带来了一个问题:该如何跟踪每个容器并确保它们真正在运行?这就是可观察性能够发挥作用的地方了。
按照维基百科的定义,“可观察性是一种度量指标,衡量系统的内部状态是否能够由其对外输出的知识推断出来”。简单地说,系统的可观察性是为了回答这样一个问题:“现在系统中有问题吗?”如果答案是肯定的,则我们应该能够回答后续的一系列问题,比如“发生了什么问题”以及“为什么会发生这种问题”。如果我们在任意时间针对系统的任何组成部分都能回答这些问题,那么就可以说该系统是可观察的。
需要注意的要点是,可观察性是系统的一个属性,与效率、可用性和可靠性同等重要。因此,在构建 gRPC 应用程序之初就必须考虑到它。
在讨论可观察性时,我们通常会涉及其三个主要方面:度量指标、日志和跟踪。这是实现系统可观察性的主要技术。以下几个小节将分别介绍它们。
7.3.1 度量指标
度量指标是一段时间内测量数据的数字形式表示。在讨论度量指标时,我们会收集两种类型的数据:一种是系统级的指标,如 CPU 使用情况、内存使用情况等;另一种是应用级的指标,如入站请求率、请求错误率等。
系统级的指标通常是在应用程序运行期间捕获的。现在,有很多工具可以捕获这些指标,它们通常都是由 DevOps 团队去捕获的。但是,应用级的度量指标因应用程序不同而有所不同。因此,在设计一个新的应用程序时,应用程序开发人员的任务是决定要捕获哪些应用级的度量指标才能了解系统的行为。本节将关注如何在应用程序中启用应用级的度量指标。
01. 在 gRPC 中使用 OpenCensus
OpenCensus 库为 gRPC 应用程序提供了标准的度量指标。通过在客户端应用程序和服务器端应用程序中添加 handler,可以很容易地启用它们。我们还可以添加自己的度量指标收集器(见代码清单 7-8)。
OpenCensus 是一组开源库,用来实现应用程序度量指标的收集和分布式跟踪,它支持各种语言。它会从目标应用程序收集度量指标,并将数据实时转移到所选择的后端。目前所支持的后端包括 Azure Monitor、Datadog、Instana、Jaeger、SignalFX、Stackdriver 和 Zipkin 等。我们还可以为其他后端编写自己的导出器(exporter)。
代码清单 7-8 为 Go gRPC 服务器端启用 OpenCensus 监控
package main
import (
"errors"
"log"
"net"
"net/http"
pb "productinfo/server/ecommerce"
"google.golang.org/grpc"
"go.opencensus.io/plugin/ocgrpc" ➊
"go.opencensus.io/stats/view"
"go.opencensus.io/zpages"
"go.opencensus.io/examples/exporter"
)
const (
port = ":50051"
)
// 用来实现ecommerce/product_info的服务器
type server struct {
productMap map[string]*pb.Product
}
func main() {
go func() { ➐
mux := http.NewServeMux()
zpages.Handle(mux, "/debug")
log.Fatal(http.ListenAndServe("127.0.0.1:8081", mux))
}()
view.RegisterExporter(&exporter.PrintExporter{}) ➋
if err := view.Register(ocgrpc.DefaultServerViews...); err != nil { ➌
log.Fatal(err)
}
grpcServer := grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{})) ➍
pb.RegisterProductInfoServer(grpcServer, &server{}) ➎
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
if err := grpcServer.Serve(lis); err != nil { ➏
log.Fatalf("failed to serve: %v", err)
}
}
- ❶ 为了启用监控,指明需要添加的外部库。gRPC OpenCensus 提供了一组预先定义好的 handler 以支持 OpenCensus 监控。这里会使用这些 handler。
- ❷ 注册统计导出器以导出收集的数据。这里添加了PrintExporter,它会将导出数据以日志的形式打印到控制台上。这只是为了展示功能,正常情况下,不推荐日志记录所有的生产环境负载。
- ❸ 注册视图以收集服务器请求的数量。这些是预定义的默认服务视图,会收集每个 RPC 所接收的字节、每个 RPC 发送的字节、每个 RPC 的延迟以及完成的 RPC。我们可以编写自己的视图来收集数据。
- ❹ 使用数据统计 handler 来创建 gRPC 服务器端。
- ❺ 注册 ProductInfo 服务到服务器端上。
- ❻ 开始在端口(50051)上监听传入的消息。
- ❼ 启动一台 z-Pages 服务器。在端口 8081 的 /debug 上下文中启动一个 HTTP 端点,实现度量指标的可视化。
与 gRPC 服务器端类似,可以使用客户端的 handler 在 gRPC 客户端启用 OpenCensus 监控。代码清单 7-9 提供了使用 Go 语言添加度量指标 handler 到 gRPC 客户端的代码片段。
代码清单 7-9 为 Go gRPC 客户端启用 OpenCensus 监控
package main
import (
"context"
"log"
"time"
pb "productinfo/server/ecommerce"
"google.golang.org/grpc"
"go.opencensus.io/plugin/ocgrpc" ➊
"go.opencensus.io/stats/view"
"go.opencensus.io/examples/exporter"
)
const (
address = "localhost:50051"
)
func main() {
view.RegisterExporter(&exporter.PrintExporter{}) ➋
if err := view.Register(ocgrpc.DefaultClientViews...); err != nil { ➌
log.Fatal(err)
}
conn, err := grpc.Dial(address, ➍
grpc.WithStatsHandler(&ocgrpc.ClientHandler{}),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("Can't connect: %v", err)
}
defer conn.Close() ➏
c := pb.NewProductInfoClient(conn) ➎
... // 省略了RPC方法的调用
}
- ❶ 声明为了启用监控需要添加的外部库。
- ❷ 注册统计数据和跟踪的导出器,以导出收集的数据。这里添加了 PrintExporter,它会将导出数据以日志的形式打印到控制台上。这只是为了展示功能,正常情况下,不推荐日志记录所有的生产环境负载。
- ❸ 注册视图以收集服务器请求的数量。这些是预定义的默认服务视图,会收集每个 RPC 所接收到的字节、每个 RPC 发送的字节、每个 RPC 的延迟以及完成的 RPC。我们可以编写自己的视图来收集数据。
- ❹ 使用客户端统计数据的 handler 建立到服务器端的连接。
- ❺ 使用服务器端连接创建客户端存根。
- ❻ 在所有的事情完成后关闭连接。
运行服务器端和客户端之后,我们可以通过创建的 HTTP 端点访问服务器端和客户端的度量指标。
如前所述,我们可以使用预先定义的导出器将数据发布到支持的后端,也可以编写我们自己的导出器,将跟踪数据和度量指标发送给任意能够消费它们的后端。
下一小节将讨论另外一项流行的技术——Prometheus,它经常用来为 gRPC 应用程序启用度量指标功能。
02. 在 gRPC 中使用 Prometheus
Prometheus 是一个用于系统监控和警告的开源工具集。可以通过gRPC Prometheus 库为 gRPC 应用程序实现基于 Prometheus 的度量指标功能。通过为客户端应用程序和服务器端应用程序添加拦截器,可以很容易地实现这一点,并且还可以添加自己的收集器。
Prometheus 通过调用一个“/metrics”上下文开头的HTTP 端点来收集目标应用程序的度量指标。它会存储所有收集到的数据,并且基于这些数据运行规则,要么基于已有的数据进行聚合并记录新的时序数据,要么生成警告。可以使用像Grafana 这样的工具对聚合结果进行可视化。
代码清单 7-10 展示了如何为 Go 语言编写的商品管理服务器端添加度量指标拦截器和自定义指标收集器。
代码清单 7-10 为 Go gRPC 服务器端启用 Prometheus 监控
package main
import (
...
"github.com/grpc-ecosystem/go-grpc-prometheus" ➊
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
reg = prometheus.NewRegistry() ➋
grpcMetrics = grpc_prometheus.NewServerMetrics() ➌
customMetricCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "product_mgt_server_handle_count",
Help: "Total number of RPCs handled on the server.",
}, []string{"name"}) ➍
)
func init() {
reg.MustRegister(grpcMetrics, customMetricCounter) ➎
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
httpServer := &http.Server{
Handler: promhttp.HandlerFor(reg, promhttp.HandlerOpts{}),
Addr: fmt.Sprintf("0.0.0.0:%d", 9092)} ➏
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()), ➐
)
pb.RegisterProductInfoServer(grpcServer, &server{})
grpcMetrics.InitializeMetrics(grpcServer) ➑
// 为Prometheus启动HTTP服务器
go func() {
if err := httpServer.ListenAndServe(); err != nil {
log.Fatal("Unable to start a http server.")
}
}()
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
- ❶ 声明要启用监控功能所需的外部库。gRPC 提供了预定义的一组拦截器以支持 Prometheus 监控。在这里将使用这些拦截器。
- ❷ 创建度量指标的注册中心。它会持有系统中所有注册的数据收集器。如需添加新的收集器,就要在这个注册中心中对其进行注册。
- ❸ 创建标准的服务器端度量指标。这是在库中预先定义好的度量指标。
- ❹ 创建名为
product_mgt_server_handle_count
的自定义度量指标计数器。 - ❺ 将标准的服务器度量指标和自定义的度量指标收集器注册到第 2步所创建的注册中心里。
- ❻ 为 Prometheus 创建 HTTP 服务器。在端口
9092
上以上下文/metrics
开头的 HTTP 端点用来进行度量指标收集。 - ❼ 使用度量指标拦截器创建 gRPC 服务器。这里使用了
grpcMetrics.UnaryServerInterceptor
,因为我们具有一元服务。还有另一个适用于流服务的拦截器,名为grpcMetrics.StreamServerInterceptor
。 - ❽ 初始化所有的标准度量指标。
借助在第 4 步添加的自定义度量指标计数器,能够为监控添加更多的度量指标。假设我们想收集相同名称的商品向商品管理系统中添加的次数,如代码清单 7-11 所示,可以在 AddProduct
方法中添加名为 customMetricCounter
的新度量指标。
代码清单 7-11 添加新的度量指标到自定义的度量指标收集器
// AddProduct实现了ecommerce.AddProduct
func (s *server) AddProduct(ctx context.Context,
in *pb.Product) (*wrapper.StringValue, error) {
customMetricCounter.WithLabelValues(in.Name).Inc()
...
}
与 gRPC 服务器端类似,可以通过客户端的拦截器为 gRPC 客户端启用 Prometheus 监控功能。代码清单 7-12 提供了在 Go 语言中为gRPC 客户端添加度量指标拦截器的代码片段。
代码清单 7-12 为 Go gRPC 客户端启用 Prometheus 监控
package main
import (
...
"github.com/grpc-ecosystem/go-grpc-prometheus" ➊
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
address = "localhost:50051"
)
func main() {
reg := prometheus.NewRegistry() ➋
grpcMetrics := grpc_prometheus.NewClientMetrics() ➌
reg.MustRegister(grpcMetrics) ➍
conn, err := grpc.Dial(
address,
grpc.WithUnaryInterceptor(grpcMetrics.UnaryClientInterceptor()), ➎
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 为Prometheus创建HTTP服务器端
httpServer := &http.Server{
Handler: promhttp.HandlerFor(reg, promhttp.HandlerOpts{}),
Addr: fmt.Sprintf("0.0.0.0:%d", 9094)} ➏
// 启动Prometheus的HTTP服务器端
go func() {
if err := httpServer.ListenAndServe(); err != nil {
log.Fatal("Unable to start a http server.")
}
}()
c := pb.NewProductInfoClient(conn)
...
}
- ❶ 声明要启用监控功能所需要的外部库。
- ❷ 创建度量指标的注册中心。与服务器端代码类似,它会持有系统中所有注册的数据收集器。如果要添加新的收集器,就需要在这个注册中心中对其进行注册。
- ❸ 创建标准的客户端度量指标,这是在库中预定义好的度量指标。
- ❹ 注册标准的客户端度量指标到第 2 步所创建的注册中心里。
- ❺ 使用度量指标拦截器创建到 gRPC 服务器端的连接。这里使用了
grpcMetrics.UnaryClientInterceptor
,因为具有一元客户端。还有另一个适用于流客户端的拦截器,叫作grpcMetrics.StreamClientInterceptor
。 - ❻ 为 Prometheus 创建 HTTP 服务器。在
9094
端口上以上下文/metrics
开头的 HTTP 端点用来进行度量指标收集。
运行服务器端和客户端之后,我们就可以通过 HTTP 端点访问服务器端和客户端的度量指标了。例如,服务器端度量指标在http://localhost:9092/metrics
上,客户端度量指标在http://localhost:9094/metrics
上。
如前所述,Prometheus 能够通过访问上述的 URL 收集度量指标。
Prometheus 将所有的度量指标数据存储在本地,并使用一组规则聚合和创建新的记录。另外,使用 Prometheus 作为数据源,我们还可以使用像 Grafana 这样的工具在一个仪表盘中可视化度量指标。
Grafana 是一个开源的度量指标仪表盘和图编辑器,适用于 Graphite、Elasticsearch 和 Prometheus。它能够让我们查询、可视化和理解度量指标数据。
在系统中使用基于度量指标的监控有一项显著的优势,那就是处理度量指标数据的成本并不会随着系统的活动而增加。例如,应用程序流量的增加不会增加磁盘利用率、处理复杂性、可视化速度、运维等方面的处理成本,它具有固定的开销。同时,收集完度量数据之后,我们可以进行大量的数学和统计转换,并得出关于系统状况的有价值的结论。
可观察性的另一个重要方面是日志,下一节会讨论。
7.3.2 日志
日志是不可变的、带时间戳的记录,描述了一段时间内所发生的离散事件。作为应用程序的开发人员,我们通常会将数据转储到日志中,以判断在给定的时间点上系统的位置和内部状态。日志的好处在于,它们很容易生成,而且比度量指标更加细粒度。可以为其附加特定的操作或一些上下文信息,比如唯一 ID、我们要做什么,以及栈跟踪信息等。日志的不足之处在于,它们代价高昂,因为需要存储它们并为其建立索引,这样才能更容易地搜索和使用它们。
在 gRPC 应用程序中,可以使用拦截器来启用日志功能。如第 5 章所述,可以在客户端和服务器端添加新的日志拦截器,并记录每个远程调用的请求和响应消息。
gRPC 生态系统为 Go 应用程序提供了一组预定义的日志拦截器,包括
grpc_ctxtags
、grpc_zap
和grpc_logrus
。其中,grpc_ctxtags
库会添加一个 Tag map 到上下文中,其数据来源于请求体;grpc_zap 将 zap 日志库集成到了 gRPC handler中;grpc_logrus
则将logrus
日志库集成到了 gRPC handler中。关于这些拦截器的更多信息,请参阅 gRPC Go 的中间件仓库。
在将日志功能添加到 gRPC 应用程序中之后,它们就会被打印到控制台上或日志文件中,这取决于如何对日志进行配置。日志如何配置则依赖于所使用的日志框架。
我们已经讨论了可观察性的两个重要方面:度量指标和日志。它们对于理解单个系统的性能和行为已经足够了,但对于理解跨多个系统的请求生命周期还不够。分布式跟踪技术使跨多个系统的请求生命周期可见。
7.3.3 跟踪
trace 是对一系列相关事件的描述,这些事件组成了分布式系统中端到端的请求流。如 3.5 节所述,在真实场景中,我们会有多个微服务,分别用来实现不同的业务功能。因此,在将响应返回给客户端之前,客户端所发起的请求要经历多个服务和不同的系统。所有的这些中间事件都是请求流的一部分。借助跟踪技术,我们能够看见请求所遍历的路径以及请求的结构。
在跟踪技术中,trace 是 span 所组成的一棵树,在分布式跟踪中,span是最基础的构造。span 包含与任务相关的元数据、延迟(完成该任务所耗费的时间)以及该任务的其他属性。trace 有自己的 ID,叫作traceID,这是独一无二的字节序列。traceID 会对 span 进行分组和区分。接下来在 gRPC 应用程序中启用跟踪功能。
与度量指标类似,OpenCensus
库提供了在 gRPC 应用程序中启用跟踪功能的支持。在商品管理应用程序中,会使用 OpenCensus
来启用跟踪。
如前所述,可以插入任意的导出器将跟踪数据导出到不同的后端。这里将使用 Jaeger 进行分布式跟踪的采样。
在默认情况下,gRPC Go 就是启用跟踪功能的。因此,只需要注册导出器,从而借助 gRPC Go 集成功能收集跟踪数据就可以了。接下来,将Jaeger 导出器应用到客户端和服务器端的应用程序中。代码清单 7-13 阐述了如何使用 Jaeger 库初始化 OpenCensus Jaeger 导出器。
代码清单 7-13 初始化 OpenCensus Jaeger 导出器
package tracer
import (
"log"
"go.opencensus.io/trace" ➊
"contrib.go.opencensus.io/exporter/jaeger"
)
func initTracing() {
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
agentEndpointURI := "localhost:6831"
collectorEndpointURI := "http://localhost:14268/api/traces" ➋
exporter, err := jaeger.NewExporter(jaeger.Options{
CollectorEndpoint: collectorEndpointURI,
AgentEndpoint: agentEndpointURI,
ServiceName: "product_info",
})
if err != nil {
log.Fatal(err)
}
trace.RegisterExporter(exporter) ➌
}
- ❶ 导入 OpenTracing 和 Jaeger 库。
- ❷ 使用收集器端点、服务名和代理端点创建 Jaeger 导出器。
- ❸ 使用 OpenCensus tracer 注册导出器。
在服务器端注册完导出器后,我们就可以对服务器端安装(instrument)跟踪功能了。代码清单 7-14 展示了如何在服务方法中安装跟踪功能。
代码清单 7-14 为 gRPC 服务方法启用跟踪功能
// GetProduct实现了ecommerce.GetProduct
func (s *server) GetProduct(ctx context.Context, in *wrapper.StringValue) (*pb.Product, error) {
ctx, span := trace.StartSpan(ctx, "ecommerce.GetProduct") ➊
defer span.End() ➋
value, exists := s.productMap[in.Value]
if exists {
return value, status.New(codes.OK, "").Err()
}
return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
}
- ❶ 使用 span 名称和上下文启动新的 span。
- ❷ 当所有的事情完成后,停止该 span。
与 gRPC 服务器端类似,我们可以为客户端安装跟踪功能,如代码清单7-15 所示。
代码清单 7-15 为 gRPC 客户端启用跟踪功能
package main
import (
"context"
"log"
"time"
pb "productinfo/client/ecommerce"
"productinfo/client/tracer"
"google.golang.org/grpc"
"go.opencensus.io/plugin/ocgrpc" ➊
"go.opencensus.io/trace"
"contrib.go.opencensus.io/exporter/jaeger"
)
const (
address = "localhost:50051"
)
func main() {
tracer.initTracing() ➋
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewProductInfoClient(conn)
ctx, span := trace.StartSpan(context.Background(),"ecommerce.ProductInfoClient") ➌
name := "Apple iphone 11"
description := "Apple iphone 11 is the latest smartphone,launched in September 2019"
price := float32(700.0)
r, err := c.AddProduct(ctx, &pb.Product{Name: name,Description: description, Price: price}) ➎
if err != nil {
log.Fatalf("Could not add product: %v", err)
}
log.Printf("Product ID: %s added successfully", r.Value)
product, err := c.GetProduct(ctx, &pb.ProductID{Value: r.Value}) ➏
if err != nil {
log.Fatalf("Could not get product: %v", err)
}
log.Printf("Product: ", product.String())
span.End() ➍
}
- ❶ 导入 OpenTracing 和 Jaeger 库。
- ❷ 调用 initTracing 函数,初始化 Jaeger 导出器实例,并使用 trace进行注册。
- ❸ 使用 span 名称和上下文启动新的 span。
- ❹ 当所有的事情完成后,停止该 span。
- ❺ 通过传递新的商品详情来调用 AddProduct 远程方法。
- ❻ 通过传递 ProductID 来调用 GetProduct 远程方法。
运行服务器端和客户端之后,trace span 就会发送到 Jaeger 代理上,会有一个守护进程作为缓冲,它将批处理和路由从客户端抽象了出来。
Jaeger 代理接收到来自客户端的 trace 日志之后,它就会将日志转发到收集器。收集器处理日志并将它们存储起来。在 Jaeger 服务器端,我们就可以可视化跟踪了。
到此为止,我们完成了对可观察性的讨论。日志、度量指标和跟踪都有其特定的用途,在你的系统中,最好全部启用这三项功能,以便于获取内部状态的最大可见性。
基于 gRPC 的可观察应用程序运行在生产环境中后,就可以持续观察它的状态,并且能够很容易地随时发现问题或系统不可用的状况。当诊断系统中的问题时,很重要的一点就是要尽快找到解决方案、对方案进行测试并部署到生产环境中。为了实现这个目标,就要有好的调试和问题排查机制。接下来详细了解 gRPC 应用程序的这些机制。