分布式系统很复杂
在 Hootsuite,我们在复杂的软件架构中运行,因为我们广泛使用微服务。在这样一个错综复杂的连接世界中,了解正在发生的事情以处理单个请求至关重要。在运行微服务基础架构时,处理外部请求通常会导致多个不同微服务的参与和编排。如果链中出现故障,检测问题所需的时间可能会增加一个数量级,因为您需要找到故障服务以及故障背后的原因。
上下文传播使分布式系统成为一个具有挑战性的话题的原因在于,观察整个应用程序并了解幕后发生的事情的能力变得更加复杂。上下文传播是一种基本工具,可在执行操作时在整个基础架构中传播信息。这是服务于不同目的的通用机制。通过在上下文中传播元数据,您可以,例如:
-
标记不同服务产生的日志,稍后重新聚合
-
衡量不同服务的应用性能和细分
我们广泛使用 golang 来构建微服务,并使用上下文传播。以下示例将展示如何在分布式环境中跨微服务边界传播上下文信息。请记住,golang 中有一个对象被传递给每个函数调用,并且具有一些跨越服务边界的本机机制,即上下文。
func doSomething(ctx context.Context, params …string) { }
上下文包提供了存储请求范围值、管理请求截止日期和取消的功能。请记住,如果您要在上下文中存储某些内容,它应该是一个与您正在处理的请求严格相关的值。之后,应该从内存中安全地删除上下文和所有包含的值。上下文本身应该在应用程序的顶层创建,并且不应该存储在任何结构类型中,只是作为第一个参数传递给其他函数调用。
r.Context()
免责声明
如果你想深入兔子洞,这里有一个关于上下文的有趣资源列表:
-https://go.dev/blog/context
-https://go.dev/blog/context-and-structs
-https://www.ardanlabs.com/blog/2019/09/context-package-semantics-in-go.html
假设您要实现一个 gRPC 服务来获取用户信息。在您的服务前面有一个 API 网关,它提供一些元数据,例如 user-id 和 request-id。您的业务逻辑涉及其他几个服务来检索完成响应所需的所有信息(您知道,微服务基础架构),我们也希望将元数据传播到所有这些其他服务。
如果您不熟悉 gRPC 拦截器,它们就像一个中间件,但用于 gRPC 堆栈。只需一个函数来实现这个signature. 在 gRPC 服务之间传播元数据非常容易。我们将元数据存储到上下文中,然后将它们传播到下游 gRPC 调用。为此,我们只需要一个拦截器从传入请求中读取元数据并将它们写入传出上下文。这样,gRPC 堆栈将自动在您要调用的服务中传播所有元数据,如果它们这样做,元数据将传播到整个基础设施。
代码示例:
将传入元数据传播到传出上下文的 gRPC 拦截器
func ContextPropagationUnaryServerInterceptor() grpc.UnaryServerInterceptor { return func(
ctx context.Context,
请求接口{},
信息*grpc.UnaryServerInfo,
处理程序 grpc.UnaryHandler,
) (interface{}, error) { if md, ok :u003d metadata.FromIncomingContext(ctx);好的 {
ctx u003d metadata.NewOutgoingContext(ctx, md)
} 返回处理程序(ctx,req)
}
}
在上面的示例中,我们从 API 网关收到了 user-id 和 request-id。通过在所有服务中使用该拦截器,元数据将通过整个微服务星座,这只是初始部分;我们现在可以开始用这些数据做很多有趣的事情。
现在我们已经通过上下文传播了元数据,我们可以开始使用它了。回到我的第一个陈述:“_让分布式系统成为具有挑战性的事情是能够观察整个应用程序并了解幕后发生的事情。”_当整个应用程序由不同的应用程序组成时,观察应用程序日志可能具有挑战性微服务,但通过元数据传播,您现在可以将这些信息附加到日志中,以便以后过滤和聚合它们。丰富日志的关键是使用结构化记录器。使用结构化记录器,我们将定义一致的日志格式,让其他工具从中提取数据集以供以后分析。
纯文本日志条目示例
[info] 2022–07–01T15:48:02+02:00 127.0.0.1:53165 hoosuite.User.GetUserInfo "验证器通行证"
结构日志条目示例
{
“级别”:“信息”,
"message": "Authenticator Pass",
"grpc.start_time": "2022-07-01T15:48:02+02:00",
“系统”:“grpc”,
"span.kind": "服务器",
"grpc.service": "hoosuite.User",
"grpc.method": "GetUserInfo",
"peer.address": "127.0.0.1:53165",
“用户_id”:“e46f6c78-d365-465e-b92c-effd33f10a32”,“请求_id”:“05fd0f39-af14-4191-b26d-3569237de4a7”
}
Zap是普遍使用的 golang 日志库之一,我将提供一些用它实现的示例。使用 zap 记录结构化数据非常容易
记录器,_ :u003d zap.NewProduction()
延迟 logger.Sync()logger.Info(
“身份验证通行证”,
zap.String(“端点”,“GetUserInfo”),
zap.String(“服务”, “hoosuite.User”),
)
使用包含传播元数据的结构化日志,您可以执行以下操作:
-
过滤单个用户产生的所有日志,按_user-id_字段过滤
-
过滤单个请求产生的所有日志,按_request-id_字段过滤
回到代码示例,元数据附加到上下文对象,那么我们需要什么才能达到这种可观察性水平?我们要:
-
将元数据放入 logger 以自动将它们附加到每个日志条目
-
有一个特定于我们正在处理的请求的记录器实例,以便它已经填充了字段
如果你正在考虑另一个拦截器,你是对的,我们可以使用拦截器。我假设使用作为记录器库,但您可以调整代码以与您选择的记录器一起使用。
首先我们将收到的所有元数据注入zap,使用github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap
func ContextToZapFieldsUnaryServerInterceptor() grpc.UnaryClientInterceptor {return func(
ctx context.Context,
方法字符串,
要求,
回复界面{},
cc *grpc.ClientConn,
调用者 grpc.UnaryInvoker,
选择...grpc.CallOption,
) 错误 {
如果 md, ok :u003d metadata.FromIncomingContext(ctx);好的 {
对于 k, v :u003d 范围 md {
ctxzap.AddFields(ctx,zap.Strings(k,v))
}
} 返回调用者(ctx,方法,请求,回复,抄送,选择...)}
}
接下来我们使用grpc-ecosystem/go-grpc-middleware/logging/zap日志中间件从上下文中获取请求范围。我们现在拥有瑞士军刀中的所有工具来执行以下操作:
-
读取元数据并将它们传播到我们依赖的任何其他微服务
-
设置请求范围的记录器,自动将元数据记录为记录字段
我们只需要将所有拦截器粘合在一起,让奇迹发生。
func createServer() *grpc.Server { opts :u003d []grpc.ServerOption{
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
ContextPropagationUnaryServerInterceptor(),
grpc_zap.UnaryServerInterceptor(zapLogger),
ContextToZapFieldsUnaryServerInterceptor(),
)),
} return grpc.NewServer(opts...)}func (s *Server) MyRPCFunction(ctx context.Context, req *pb.MyRequest) (*pb.Response, error) { logger :u003d ctxzap.Extract(ctx)
logger.Info(“我的日志条目”) . . .}
堆栈中的拦截器将合作执行以下操作:
-
将元数据推送到传出上下文(准备传播到传出请求)
-
将传入的上下文元数据推送到日志字段
-
将记录器注入上下文,准备提取为请求范围的记录器
在以下日志示例中,我们可以看到 user_id 和 request _id 通过不同的服务传播
{
“级别”:“信息”,
"message": "Authenticator Pass",
"grpc.start_time": "2022–07–01T15:48:02+02:00",
“系统”:“grpc”,
"span.kind": "服务器",
"grpc.service": "hoosuite.User",
"grpc.method": "GetUserInfo",
"peer.address": "127.0.0.1:53165",
“用户_id”:“e46f6c78-d365–465e-b92c-effd33f10a32”,
“请求_id”:“05fd0f39-af14–4191-b26d-3569237de4a7”
}{
“级别”:“错误”,
"message": "用户信息无效",
"grpc.start_time": "2022–07–01T15:48:02+02:00",
“系统”:“grpc”,
"span.kind": "服务器",
"grpc.service": "hoosuite.User",
"grpc.method": "GetUserInfo",
"peer.address": "127.0.0.1:53165",
“用户_id”:“e46f6c78-d365–465e-b92c-effd33f10a32”,
“请求_id”:“05fd0f39-af14–4191-b26d-3569237de4a7”
"error": "获取订阅时出错"
}{
“级别”:“错误”,
"message": "获取用户订阅时出错",
"grpc.start_time": "2022–07–01T15:48:02+02:00",
“系统”:“grpc”,
"span.kind": "服务器",
"grpc.service": "hoosuite.Subscription",
"grpc.method": "GetUserSubscription",
"peer.address": "127.0.0.1:53165",
“用户_id”:“e46f6c78-d365–465e-b92c-effd33f10a32”,
“请求_id”:“05fd0f39-af14–4191-b26d-3569237de4a7”
“错误”:“未找到用户订阅”
}
结论
在本文中,我们演示了使用 Go gRPC 中间件的上下文传播实现。这种技术使我们能够轻松轻松地在我们的分布式基础架构中实现请求跟踪。通过添加一些简单的拦截器,我们能够传播元数据并开始观察整个应用程序,即使它被分割成不同的微服务。这开辟了从多个方向观察我们的代码的新方法,在前面的示例中,我们使用 request-id 和 user-id 标记了请求。同样,您可以选择更适合您的用例的粒度级别,例如,您可以用服务名称装饰请求,您可以增加一个标头值来计算您的服务链中的最长路径,没有限制到您可以附加和传播的信息。