第11章 遥测技术
# 第11章 遥测技术
在本章中,我们将探索遥测技术的实际应用领域。在这里,Go语言编程模型的优雅之处与应用程序可观测性的关键需求完美契合。我们将为你介绍日志记录、跟踪和指标等工具,帮助你深入了解Go应用程序的内部运行机制,确保这些应用程序高效、可靠地运行。
本章是提升应用程序遥测技术艺术与科学的指南。从结构化日志记录的全面实践(它能让应用程序日志变得有序、清晰),到跟踪技术提供的详细洞察,再到指标分析实现的深入剖析,本章将涵盖所有内容。
本章将涉及以下关键主题:
- 日志(Logs)
- 跟踪(Traces)
- 指标(Metrics)
- 开放遥测(OpenTelemetry,OTel)项目
在本章结束时,你将掌握观察、理解并积极提升应用程序性能和可靠性的技能,这会让你在工作中更有参与感和动力。
# 技术要求
确保你的计算机上安装了Docker。你可以从Docker官方网站(https://www.docker.com/get-started (opens new window))下载。
# 日志
日志记录,作为系统编程领域中默默无闻的英雄,常常像软件更新时的“条款与条件”复选框一样被忽视。大多数开发者对待日志记录的态度,就像青少年对待打扫房间:理论上是个不错的主意,但不知为何,除非出了问题,否则永远不会成为优先事项。这里常见的误解是什么呢?人们认为日志记录只是事后的想法,仅仅是代码偶尔用来随意记录的“日记”。剧透警告:事实并非如此!
想象一下,软件开发就像是一场考古挖掘。每一条日志记录都是一件精心挖掘出的文物,为曾经繁荣的“文明”(代码库)提供线索。现在,想象一下在这场挖掘中有一些开发者,他们用推土机(糟糕的日志记录实践)来挖掘这些脆弱的宝藏。结果会怎样呢?大量破碎的“陶器”和一张张困惑的脸。朋友们,当Go语言中的日志记录没有得到应有的重视和精确处理时,就会发生这样的情况。
在Go语言中,尤其是在系统编程的背景下,日志记录是了解应用程序行为的重要工具。它为系统提供了可见性,使开发者能够追踪错误、监控性能并了解流量模式。Go语言作为一门务实的语言,通过标准库中的log
包提供了对日志记录的内置支持。但当涉及到系统级编程时,情况就变得更加复杂了。
在系统编程中,性能和资源优化至关重要,标准的log
包可能并不总是能满足需求。这时,结构化日志记录就成为了焦点。与纯文本日志记录不同,结构化日志记录将日志条目组织成结构化格式,通常是JSON格式。这种格式使日志更易于查询、分析和理解,尤其是当你在海量数据中寻找关键信息时。
我们不仅要说,还要做。下面通过一段代码示例来展示Go语言中的结构化日志记录:
package main
import (
"os"
"log/slog"
)
func main() {
handler := slog.NewJSONHandler(os.Stdout)
logger := slog.New(handler)
logger.Info("A group of walrus emerges from the ocean", slog.Attr("animal", "walrus"), slog.Attr("size", 10))
}
2
3
4
5
6
7
8
9
10
11
12
这段代码使用了Go 1.21中引入的实验性slog
包。它位于log
子包(log/slog
)中,好处是无需外部依赖,简化了项目管理。
让我们来探究一下这段代码的关键点:
handler := slog.NewJSONHandler(os.Stdout)
:这一行代码创建了一个slog.Handler
,负责格式化并可能对日志条目进行路由。这里,slog.NewJSONHandler
生成了一个JSON格式化器,os.Stdout
指定标准输出作为目标。logger := slog.New(handler)
:这一行创建了一个slog.Logger
实例。新创建的JSON处理器用于配置日志记录器的输出格式和目标。- 带属性的结构化日志记录:
logger.Info("A group of walrus emerges from the ocean", slog.Attr("animal", "walrus"), slog.Attr("size", 10))
:这一行使用Info
方法记录一条信息性消息。slog.Attr("animal", "walrus"), slog.Attr("size", 10)
:这些代码利用slog.Attr
创建键值对(属性),用结构化数据丰富日志消息。这使得工具或下游应用程序更容易解析和分析日志。
在Go语言中进行日志记录,不仅仅是为了做记录,更是为了理解应用程序的“故事”,一次一条日志。记住——就像任何精彩的故事一样,细节(或者在这种情况下,数据)才是关键。
在软件开发领域,日志记录是理解、诊断和跟踪应用程序行为的基石。它就像著名童话中汉塞尔和格蕾特留下的面包屑路径,引导你穿越复杂的代码森林,了解发生了什么、何时发生以及为什么发生。
从本质上讲,日志记录涉及在程序执行过程中记录事件和数据。这些事件可以涵盖从应用程序运行的一般信息,到错误和系统特定消息,这些消息有助于了解应用程序的健康状况和性能。日志记录的重要性可以类比为航空领域中的飞行记录器或“黑匣子”;它捕获关键信息,事后可以对这些信息进行分析,以了解导致事件发生的原因,或优化未来的性能。
有效的日志记录实践通过以下方式赋予开发者能力:
- 调试和故障排除:当出现问题时,日志是主要的查找方向之一。它们可以帮助确定错误发生的位置和环境,减少解决问题所需的时间。
- 安全审计:记录访问和事务数据有助于检测未经授权的访问尝试、数据泄露和其他安全威胁,便于迅速采取行动。
- 合规性和记录保存:在许多行业中,出于合规目的,详细记录日志是一项监管要求,可作为正确处理数据和其他操作的证明。
- 了解用户行为:日志记录可以深入了解用户与应用程序的交互方式、哪些功能最受欢迎,以及用户可能遇到困难的地方。
尽管日志记录起着关键作用,但它并非没有挑战。需要谨慎平衡,以确保捕获适量的信息——信息太少可能会遗漏重要线索;信息太多,则会像在干草堆里找针一样困难。日志记录的艺术与科学在于确定记录什么、如何记录,以及如何理解收集到的数据,同时将对应用程序性能的影响降至最低。
如今,在追求高性能日志记录时,uber/zap
是最快的日志记录库之一。让我们来探究一下使用slog
和zap
的主要区别。
# Zap与slog的对比
在slog
和zap
之间做选择时,要考虑应用程序的特定需求。
对于那些性能至上,且需要对日志记录进行细粒度控制的应用程序,zap
提供了经过验证的速度和可配置性。
如果你正在寻找一种现代、高效的日志记录解决方案,它能与Go语言的context
包很好地集成,并且强调简单性和灵活性,那么slog
可能是正确的选择。下面是相同示例的zap
版本:
package main
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
encoderConfig := zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "level",
EncodeLevel: zapcore.CapitalLevelEncoder,
TimeKey: "time",
EncodeTime: zapcore.ISO8601TimeEncoder,
CallerKey: "caller",
EncodeCaller: zapcore.ShortCallerEncoder,
}
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
consoleSink := zapcore.AddSync(os.Stdout)
core := zapcore.NewCore(consoleEncoder, consoleSink, zap.InfoLevel)
logger := zap.New(core)
sugar := logger.Sugar()
sugar.Infow("A group of walrus emerges from the ocean",
"animal", "walrus",
"size", 10,
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这个示例故意做得更复杂一些,以展示zap
库的可配置性。让我一步一步解释这里发生了什么:
- 导入:我们导入
go.uber.org/zap
以获取zap
的核心功能,并导入go.uber.org/zap/zapcore
用于底层配置。 - 编码器配置:
zap
使用编码器来格式化日志条目。我们设置了一个适用于生产环境的encoderConfig
配置,用于生成具有人类可读键的JSON输出。 - 控制台日志记录:我们创建了一个控制台编码器(
consoleEncoder
)和一个输出目标(consoleSink
),它将日志写入标准输出。 - 核心创建:
zapcore.NewCore
函数构建了我们日志记录器的核心,它将编码器、输出目标和配置的日志级别(zap.InfoLevel
)组合在一起。 - 日志记录器创建:使用
zap.New
,我们基于核心构建了一个zap
日志记录器。 - 结构化日志记录器:
zap
的结构化日志记录器提供了诸如Infow
之类的便捷方法用于日志记录。它使得向日志消息中添加结构化数据更加容易(但运行速度比非结构化版本慢) 。
然而,slog
和zap
都增强了Go语言的日志记录功能,超越了标准库,提供结构化、高效且灵活的日志记录解决方案。两者之间的选择取决于应用程序的特定需求,包括性能考量、对结构化日志记录的需求以及所需的定制程度。
# 用于调试还是监控的日志记录?
调试日志主要在开发阶段或诊断系统问题时使用。其主要目的是在特定时刻为开发者提供有关应用程序行为的详细上下文信息,尤其是在出现错误或意外行为时。
调试日志具有以下特点:
- 粒度:调试日志通常非常详细,包含有关应用程序状态、变量值、执行路径和错误消息的详细信息。
- 临时性:这些日志可能在开发环境中生成,或者在生产环境中临时启用,以追踪特定问题。由于其详细程度较高,通常不会在实际运行环境中长期保持启用状态。
- 面向开发者:调试日志的受众通常是熟悉应用程序代码库的开发者。这些信息具有技术性,需要对应用程序的内部机制有深入理解。
最常见的调试日志示例是堆栈跟踪信息和特定检查点的关键变量。当我们进行用于监控的日志记录时,日志是为了在生产环境中持续观察应用程序而设计的。它们有助于了解应用程序随时间的健康状况和使用模式,便于进行主动维护和优化。监控日志具有以下特点:
- 便于聚合:监控日志的结构设计便于被监控工具聚合和分析。它们通常遵循一致的格式,更易于提取指标和趋势。
- 持续性:这些日志在生产环境中作为应用程序正常运行的一部分持续生成和收集。为了在信息量和性能开销之间取得平衡,它们比调试日志的详细程度要低。
- 提供运营洞察:重点在于与应用程序运行、用户活动和错误率相关的信息。受众不仅包括开发者,还包括系统管理员和运维团队。
例如,我们可以在HTTP请求日志中看到这种日志记录策略,其中包含请求方法、URL和状态码。
这两种日志记录方法的主要区别在于目标、详细程度、受众和生命周期。
本质上,用于调试和监控的日志记录在应用程序的生命周期中起着互补但又截然不同的作用。有效的日志记录策略会认识到这些差异,并采用定制化的方法来满足调试和监控的独特需求。
在进行日志记录时,你选择的日志格式会显著影响日志数据的可读性、处理速度和整体实用性。两种流行的格式是JSON日志和结构化文本日志。在两者之间做出选择,需要了解它们的差异、优势,以及应用程序或环境的特定需求。让我们列出一个框架,帮助我们做出明智的决策。
首先,我们应该考虑日志消费工具:
- JSON日志:如果你使用现代日志管理系统或专为摄取和查询JSON数据而设计的工具(如Elasticsearch、Logstash、Kibana(ELK)或Splunk),JSON日志会非常有优势。这些工具可以原生解析JSON,实现更高效的查询、过滤和分析。
- 结构化文本日志:如果你的日志主要用于直接读取以进行调试,或者使用的工具不能原生解析JSON,那么结构化文本日志可能更合适。结构化文本日志对人类来说更易于阅读,尤其是在控制台中实时查看日志时。
此外,我们还要评估日志数据的复杂性:
- JSON日志:JSON非常适合记录复杂和嵌套的数据结构。如果你的应用程序日志包含多种数据类型,或者包含受益于分层组织的结构化数据,JSON日志可以更有效地封装这种复杂性。
- 结构化文本日志:对于更简单的日志记录需求,即日志主要是包含几个键值对的简单消息,结构化文本日志就足够了,而且使用起来更简单。
完成上述评估后,我们可以评估性能和开销:
- JSON日志:以JSON格式写入日志会因序列化成本而带来额外的计算开销。对于性能至关重要的高吞吐量应用程序,要评估系统是否能够承受这种开销而不受显著影响。
- 结构化文本日志:一般来说,生成结构化文本日志比JSON序列化的CPU开销更小。如果性能是首要考虑因素,并且你的日志数据相对简单,那么结构化文本日志记录可能是更高效的选择。
然后,我们可以制定日志分析和故障排除计划:
- JSON日志:在需要对日志进行广泛分析以深入了解应用程序行为、用户操作,或排查复杂问题的场景中,JSON日志提供了更结构化、更易于“查询”的格式。它们便于进行更深入的分析,并且可以被许多工具自动处理。
- 结构化文本日志:如果你的日志分析需求很简单,或者你主要使用日志进行实时故障排除,而无需复杂的查询,那么结构化文本日志可能就足够了。
最后,我们可以评估开发和维护环境:
- JSON日志:要考虑开发团队是否熟悉JSON格式和解析,以及日志记录框架和基础设施是否能够有效地支持JSON日志记录。
- 结构化文本日志:对于追求简单易用的团队来说,结构化文本日志可能更受青睐,特别是当他们不使用高级日志处理工具时。
一般的指导原则如下:
- 日志消费工具:对于高级处理工具,选择JSON格式;为了简单或直接消费日志,选择结构化文本格式。
- 数据复杂性:对于复杂、嵌套的数据,使用JSON格式;对于更简单的数据,使用结构化文本格式。
- 性能考量:当性能至关重要时,选择结构化文本格式;使用JSON格式时要考虑对性能的影响。
- 分析和故障排除:对于深入分析需求,选择JSON格式;对于基本的故障排除,选择结构化文本格式。
- 团队和基础设施:考虑团队的熟悉程度和基础设施的能力。
最终,在JSON日志和结构化文本日志之间的选择,取决于平衡应用程序的特定需求、日志处理基础设施的能力,以及团队的偏好和技能。在不同的上下文或应用程序层中同时使用这两种日志格式,以优化人类可读性和机器处理能力,这种情况并不少见。
了解记录什么和不记录什么,对于保持高效、安全且有用的日志记录实践至关重要。
# 记录什么内容?
合理的日志记录有助于调试问题、监控系统性能以及了解用户行为。然而,过度或不当的日志记录可能会导致性能下降、存储问题以及安全漏洞。
以下指南可帮助你做出相关决策:
- 错误信息:记录发生的任何错误,并附上堆栈跟踪信息,以方便调试。
- 系统状态变更:记录应用程序内重大的状态变化,例如系统启动或关闭、配置更改以及关键组件的状态变化。
- 用户操作:记录关键的用户操作,尤其是那些修改数据或触发应用程序中重要流程的操作。这有助于了解用户行为并诊断问题。
- 在没有指标服务器的情况下:
- 性能指标:记录与性能相关的指标,如响应时间、吞吐量和资源利用率。这些信息对于监控系统的健康状况和性能至关重要。
- 安全事件:记录与安全相关的事件,如登录尝试、访问控制违规和其他可疑活动。这些日志对于安全监控和事件响应至关重要。
- API调用:当应用程序通过API与外部服务交互时,记录这些调用有助于跟踪依赖关系并解决问题。
- 在没有审计系统来发送系统事件的情况下:
- 关键业务交易:记录重要的业务交易,以提供审计跟踪记录,可用于合规性检查、报告和商业智能分析。
# 不要记录什么内容?
有一系列信息不适合记录,如下所示:
- 敏感信息:避免记录敏感信息,如密码、个人身份信息(PII)、信用卡号和安全令牌。这些信息的泄露可能会导致安全漏洞和合规性违规。
- 生产环境中的详细或调试信息:虽然详细或调试级别的日志在开发过程中非常有用,但它们可能会使生产系统不堪重负。应使用适当的日志级别,并考虑动态调整日志级别。
- 冗余或无关信息:多次记录相同信息或捕获无关细节会使日志杂乱无章,并占用不必要的存储空间。
- 大型二进制数据:避免记录大型二进制对象,如文件或图像。这会显著增加日志文件的大小并降低性能。
- 未经过滤的用户输入:记录未经处理的原始用户输入可能会带来安全风险,如注入攻击。在记录之前务必对输入进行过滤处理。
最佳实践总结如下:
- 使用结构化日志记录:结构化日志使数据的搜索和分析更加容易。在所有日志中使用一致的格式,如JSON。
- 实施日志轮转和保留策略:自动轮转日志并定义保留策略,以管理磁盘空间并符合数据保留要求。
- 保护日志数据安全:确保日志存储安全,控制访问权限,并对日志数据的传输进行加密。
- 监控日志文件中的异常情况:定期检查日志文件,查看是否存在可能表明操作或安全问题的异常活动或错误。
遵循这些准则,可确保日志记录实践对应用程序的维护、性能和安全产生积极作用。请记住,目标是捕获足够有用的信息,同时又不影响系统性能或安全性。
通常,我们需要更多有关程序执行的信息,但现有的一系列记录(日志)并不够。这时,我们就要依靠跟踪(tracing)技术。
# 跟踪
你是不是听说在Go语言中进行跟踪易如反掌?可别自欺欺人了;在系统编程领域,跟踪更像是在微波炉里烤舒芙蕾——当然,最后你可能也能弄出点能吃的东西,但这可拿不到米其林星级。
打个比方,你可能会感兴趣:想象你是软件开发“谋杀案”中的一名侦探。受害者是系统性能,嫌疑人则是一群形形色色的协程(goroutine),一个比一个可疑。破解此案的唯一希望在于复杂的跟踪分析技术。但要注意,这可不是儿戏。你需要运用所有的智慧,还要带上几分自嘲,才能在堆栈跟踪和执行线程的泥潭中找到头绪。
对于那些不太了解系统编程细节的人来说,Go语言中的跟踪就像是福尔摩斯式的调试工具。它能让开发者在程序执行过程中观察其行为,对于找出性能瓶颈和那些难以捉摸的漏洞极有帮助,要是没有跟踪技术,这些漏洞就像在摆满摇椅的房间里一只安静乖巧的猫一样,让人难以发现。
本质上,Go语言的跟踪框架借助runtime/trace
包,让你能够深入了解正在运行的应用程序。通过收集与协程、堆分配、垃圾回收等相关的各种事件,为深入研究代码的内部工作机制奠定基础。
跟踪分析的强大之处在go tool trace
这样的工具中得以体现。它会解析Go应用程序生成的跟踪文件,并在一个网页界面中展示这些信息,这个界面既直观又吸引人。在这里,你可以直观地看到协程的执行情况,追踪延迟问题,并揭开那些让你夜不能寐的性能谜团。
让我们通过一个简单的代码示例来实际了解一下。假设你像这样在关键部分添加了跟踪调用:
package main
import (
"os"
"runtime/trace"
)
func main() {
trace.Start(os.Stderr)
defer trace.Stop()
// Your code here. Let's pretend it's something impressive.
}
2
3
4
5
6
7
8
9
10
11
12
运行这个程序时,它会输出一些不太美观的内容,对吧?
这段代码片段启动了跟踪过程,并将输出定向到标准错误输出(stderr),之后你可以尽情地对其进行分析。请记住,这只是冰山一角。
让我们退一步,学习如何在程序中添加跟踪功能,并正确查看输出结果。
如你所见,要开始跟踪,需要导入runtime/trace
包。这个包提供了启动和停止跟踪的功能:
import (
"os"
"runtime/trace"
)
2
3
4
我们需要在代码中想要开始跟踪的位置调用trace.Start
。同样,当你想结束跟踪时,通常是在完成某个你感兴趣的特定操作之后,应该调用trace.Stop
:
func main() {
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
// Your program logic here
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
像往常一样运行你的Go程序。程序执行后会在当前目录生成一个名为trace.out
的跟踪文件(或者是你为文件取的任何名字):
go run your_program.go
程序运行结束后,可以使用go tool trace
命令分析跟踪文件。这个命令会启动一个Web服务器,提供一个基于网页的用户界面来分析跟踪信息:
go tool trace trace.out
运行这个命令时,它会在控制台打印一个URL。在网页浏览器中打开这个URL,就可以查看跟踪信息查看器。该查看器提供了各种视图,用于分析程序执行的不同方面,比如协程分析、堆分析,以及我们在第9章“性能分析”中探讨过的其他方面。
对于包含HTTP服务器的程序,方法略有不同。让我们为这个简单的程序添加跟踪功能:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", handler)
fmt.Println("Server is listening on :8080")
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Tracing!")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
要跟踪HTTP端点,我们需要用一个函数来包装处理程序(handler),在处理程序执行前后启动和停止跟踪。可以使用runtime/trace
包进行跟踪,使用net/http/httptrace
包进行更详细的HTTP跟踪。
首先,像前面的代码片段一样,修改main
包,导入runtime/trace
包。然后,为HTTP处理程序创建一个跟踪包装器:
import (
"net/http"
"runtime/trace"
)
func TraceHandler(inner http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, task := trace.NewTask(r.Context(), r.URL.Path)
defer task.End()
trace.Log(ctx, "HTTP Method", r.Method)
trace.Log(ctx, "URL", r.URL.String())
inner(w, r.WithContext(ctx))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
然后,用TraceHandler
包装HTTP处理程序:
func main() {
http.HandleFunc("/", TraceHandler(handler))
fmt.Println("Server is listening on :8080")
http.ListenAndServe(":8080", nil)
}
2
3
4
5
按照前面程序中的相同步骤启动和停止跟踪,然后运行应用程序。向服务器发送一些请求,确保有可跟踪的活动。
停止跟踪并生成跟踪文件后,使用go tool trace
命令分析跟踪数据。特别注意与网络I/O和HTTP请求相关的部分,以了解端点的性能。
# 高效跟踪
不要对整个程序进行跟踪,而是专注于性能关键的部分。这种方法可以减小跟踪文件的大小,使分析更加容易。
花些时间探索跟踪查看器中提供的不同视图。每个视图都能让你深入了解程序执行的特定方面。此外,在分析跟踪数据时,留意异常模式或异常情况,比如长时间阻塞的协程或频繁的垃圾回收暂停。
确保在请求处理过程中,包含跟踪信息的上下文传递到所有下游调用。这样可以实现更全面的跟踪,涵盖整个请求生命周期。
如果可能的话,使用中间件进行跟踪。对于更复杂的应用程序,可以考虑在HTTP服务器中将跟踪功能实现为中间件。这种方法在应用程序的不同部分提供了更大的灵活性和可重用性。
回想起我在Go语言跟踪方面的种种经历,我记得有一个项目就像一辆豪华轿车陷在了泥沼里。在花了几个小时仔细研究跟踪输出后,我有了一个重大发现,就像在冰箱里找到了车钥匙一样令人惊讶。我意识到,跟踪就像一位技艺精湛的品酒师,能够分辨出出色性能和灾难性瓶颈之间的细微差别。最后,解决方案很简单,只是重新调整了一些数据库调用,但这也凸显了Go语言跟踪功能的精细和强大。
跟踪主要用于性能分析和调试。它对于识别并发问题、了解系统在负载下的行为,以及确定分布式系统中的延迟源特别有用。与日志记录相比,它能提供更细致的程序执行视图。日志记录离散的事件或状态,而Go语言中的跟踪可以提供程序执行的连续、详细记录,包括系统级事件。
日志记录与跟踪 此外,这两种方式都需要考虑性能问题。日志记录和跟踪都会影响Go应用程序的性能,但跟踪的影响通常更大,尤其是在生产环境中使用执行跟踪时。开发者需要在捕获的详细程度和性能开销之间取得平衡。 |
---|
总而言之,把Go语言中的跟踪想象成拆解一台复杂的机器。没有合适的工具和知识,你就像拿着扳手的猴子,无从下手。但要是掌握了Go语言的跟踪包,你就能变成一位技术娴熟的机械师,让应用程序像在温暖膝盖上的小猫一样平稳运行。记住,细节决定成败,有时候,这些细节就隐藏在代码的跟踪信息中。
# 分布式跟踪
分布式跟踪是指监控一个请求在分布式系统中各个相互连接的服务之间的完整路径。想象一个复杂的电子商务应用程序,它有独立的产品搜索、购物车、支付处理和订单履行服务。一个用户请求可能会触发与所有这些服务的交互。
你可能会问,它是如何工作的呢?有四个关键概念:唯一标识符、传播、跨度(span)以及收集和分析。
一个唯一标识符(跟踪ID)会被分配给初始请求。这个ID就像一条线,将与该特定请求相关的所有后续日志和事件串联起来。
然后,跟踪ID会在处理该请求涉及的所有服务之间传播。这可以通过HTTP请求头、队列中的消息,或者任何适合服务间通信协议的机制来实现。
每个服务都会创建一个“跨度”,用于捕获其在处理请求过程中的相关信息。这个跨度可能包括时间戳、服务名称、函数调用以及遇到的任何错误等详细信息。
这些跨度会被一个中央跟踪系统收集,然后该系统根据跟踪ID将它们拼接在一起。这样就能全面了解整个请求流程,涵盖涉及的所有服务。
分布式跟踪的主要优点如下:
- 增强可观测性:分布式跟踪能让你清楚地了解请求在系统中的流动情况,揭示潜在的瓶颈和低效环节。
- 根源分析:当出现错误时,跟踪有助于准确找出导致问题的服务或组件,即使错误在请求流程的较晚阶段才显现出来。
- 性能优化:通过分析跟踪数据,你可以识别出速度较慢的服务或服务之间的通信问题,从而进行性能优化。
- 调试微服务:借助分布式跟踪提供的上下文,调试微服务之间复杂的交互变得更加容易。
有许多开源和商业工具可用于实现分布式跟踪。一些受欢迎的选择包括Zipkin、Jaeger、Honeycomb和Datadog。
但是,如何在不大量修改应用程序代码的情况下,自由切换可观测性工具和后端提供商呢?在本章后面的内容中,我们会看到开放遥测(OTel)项目正试图填补这一空白。
让我们继续学习遥测(telemetry)的下一个支柱:指标(metrics)。
# 指标
在一门设计得像在下雨天看油漆变干一样无趣的语言里,热衷于性能数据,这可太能体现 “我是个厉害的程序员” 了。但此刻,我们准备一头扎进Go语言指标这个激动人心的世界,带着像吃了镇静剂的树懒一样的热情。这是一场穿越数字和图表迷宫的奇妙之旅,在这里,你面对的 “弥诺陶洛斯”(Minotaur,古希腊神话中牛头人身的怪物)就是你自己的代码,它神秘地吞噬着资源,相比之下,量子物理都显得简单易懂了。
现在,对于那些还跟着我、没被即将到来的厄运阴影吓退的勇敢之人,咱们严肃一会儿。在Go语言的环境中,指标是理解应用程序行为和性能的重要工具。它们能让你深入了解系统的各个方面,比如内存使用情况、CPU负载和协程(goroutine)数量。Go语言凭借其简约的魅力和并发模型,给系统程序员在性能方面 “挖坑” 创造了大量机会。好在,它也提供了 “创可贴”,即用于收集、报告和分析这些指标的内置库和第三方库。例如,Go运行时通过runtime
和net/http/pprof
包公开了丰富的性能数据,让程序员能够实时监控他们的应用程序。
其中一个比较受欢迎的第三方库是Prometheus,它的Go客户端库提供了一套丰富的工具来定义和收集指标。它能无缝集成到Go应用程序中,不仅为监控系统级指标,还为监控特定于应用程序的指标提供了强大的解决方案,这有助于诊断性能瓶颈和了解用户行为。
为了让你有个初步感受,咱们来看一个使用Prometheus在Go Web服务中收集HTTP请求计数的简单示例:
package main
import (
"fmt"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
requestsProcessed = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_processed",
Help: "Total number of processed HTTP requests.",
},
[]string{"status_code"},
)
)
func init() {
// Register metrics with Prometheus
prometheus.MustRegister(requestsProcessed)
}
func main() {
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond)
code := http.StatusOK
if time.Now().Unix()%2 == 0 {
code = http.StatusInternalServerError
}
requestsProcessed.WithLabelValues(fmt.Sprintf("%d", code)).Inc()
w.WriteHeader(code)
fmt.Fprintf(w, "Request processed.")
})
fmt.Println("Starting server on port 8080...")
http.ListenAndServe(":8080", nil)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
这段代码片段使用prometheus/client_golang
库与Prometheus进行交互。一个名为http_requests_processed
的计数器指标用于跟踪HTTP请求的数量,并按状态码进行标记。/metrics
端点会公开指标,供Prometheus抓取。在HTTP处理程序中,计数器指标会根据适当的状态码标签进行递增。
简单性 这只是一个基本示例。在实际应用中,会涉及更丰富的指标和检测手段。 |
---|
下面我们按以下步骤运行Prometheus服务器。
- 创建一个Prometheus配置文件:
- 创建一个新文件,并命名为
prometheus.yml
。 - 将以下基本配置粘贴到文件中:
- 创建一个新文件,并命名为
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
2
3
4
5
6
- 拉取Prometheus的Docker镜像:
- 打开终端,运行以下命令来下载最新的Prometheus Docker镜像:
docker pull prom/prometheus
- 运行Prometheus容器:
- 使用以下命令运行Prometheus,并将
prometheus.yml
映射到容器中:
- 使用以下命令运行Prometheus,并将
docker run -p 9090:9090 -v <path_to_your_prometheus.yml>:/etc/prometheus/prometheus.yml prom/prometheus
- 将
<path_to_your_prometheus.yml>
替换为你配置文件的实际路径。
- 访问Prometheus的Web界面:
- 打开网页浏览器,访问
http://localhost:9090
。 - 此时你应该能看到Prometheus的用户界面。
- 打开网页浏览器,访问
- 探索Prometheus:
- 在表达式浏览器(“Graph”选项卡)中,输入一个基本查询,如
up
,然后点击“Execute”。这会显示Prometheus本身是否正在运行。 - 探索其他内置指标,试用查询语言,感受一下Prometheus。
- 在表达式浏览器(“Graph”选项卡)中,输入一个基本查询,如
现在,我们可以执行代码并查看指标了。首先,我们需要保存代码并进行构建:
go build app.go &&./app
探索指标:
- 访问指标端点:打开浏览器,访问
http://localhost:8080/metrics
。你应该能看到原始的Prometheus指标输出。 - 查询指标:在Prometheus用户界面(
http://localhost:9090
)中,尝试以下查询:http_requests_processed
:查看按状态码分类的请求总数。rate(http_requests_processed[1m])
:查看过去一分钟内的请求速率。
现在我们能看到指标了,但我们可以使用哪些指标,又应该使用哪些指标呢?咱们来探讨一下!
# 应该使用什么指标?
在应用程序中选择正确的指标类型进行监控,就好比为一项工作选择合适的工具 —— 用锤子钉钉子,而不是拧螺丝。在监控和可观测性的领域里,主要的指标类型(计数器(Counter)、仪表盘(Gauge)、直方图(Histogram)和摘要(Summary))各有不同的用途。理解这些用途对于有效衡量和分析应用程序的行为和性能至关重要。 计数器
计数器是一种简单的指标,它只会随着时间增加(递增),并且在重启时会重置为零。它非常适合跟踪事件的发生次数。当你想要统计一些事情,比如处理的请求数、完成的任务数或发生的错误数时,就可以使用计数器。例如,统计用户在你的网站上执行特定操作的次数。
以下是一些使用场景:
- 事件计数:非常适合统计特定事件的发生次数。比如,你可以用计数器跟踪用户注册的数量、完成的任务数量或遇到的错误数量。
- 速率测量:虽然计数器本身只会增加,但你可以测量它随时间的增加速率,这使得它适合用于了解事件发生的频率,比如每秒的请求数。
仪表盘
仪表盘是一种表示单个数值的指标,这个数值可以随意上升和下降。它就像一个测量当前温度的温度计。
对于随时间波动的值,比如当前内存使用量、并发会话数或机器的温度,你可以使用仪表盘。仪表盘非常适合监控那些特定时间点的当前状态比变化速率更重要的资源。
以下是一些使用场景:
- 资源水平:仪表盘很适合测量那些可以增加和减少的数量,比如当前内存使用量、剩余磁盘空间或活跃用户数量。
- 传感器读数:任何随时间波动的实时测量值,比如温度传感器数据、CPU负载或队列长度。
直方图
直方图对观测值(通常是请求持续时间或响应大小之类的)进行采样,并将它们统计到可配置的区间(bucket)中。它还会提供所有观测值的总和。
当你需要了解一个指标的分布情况,而不仅仅是它的平均值时,就可以使用直方图。直方图非常适合跟踪应用程序中请求的延迟或响应的大小,因为它不仅能让你看到平均值,还能看到这些值是如何分布的,比如第95百分位数的延迟。
以下是一些使用场景:
- 分布测量:当你需要捕获指标值随时间的分布时,直方图就很出色。这对于理解数据中的平均值、变异性和异常值至关重要。
- 性能分析:非常适合测量请求延迟或响应大小。直方图有助于识别那些可能对平均值影响不大,但对用户体验有显著影响的长尾延迟。
摘要
和直方图一样,摘要也对观测值进行采样。不过,它计算的是滑动窗口分位数(比如第50、90和99百分位数),而不是提供区间。摘要可能比直方图计算量更大,因为它会实时计算这些分位数。
当你需要在滑动时间窗口内获取精确的分位数,尤其是对于那些长期准确性不如近期趋势重要的指标时,就可以使用摘要。当你需要动态了解请求持续时间和响应大小的准确分布时,摘要特别有用。
以下是一些使用场景:
- 动态分位数:当你需要在滑动时间窗口内获得准确的分位数时,摘要就是最佳选择。它能提供更详细的指标分布视图,并随着新数据的到来进行调整。
- 近期趋势分析:适用于近期性能比长期平均值更重要的场景,让你能够快速响应模式的变化。
选择正确的指标
选择的关键在于你要测量的内容的性质以及你打算如何使用这些数据:
- 统计事件发生次数?选择计数器。
- 测量会增加和减少的值?仪表盘是你的好帮手。
- 需要了解分布情况?直方图在这方面表现出色。
- 需要近期数据的动态分位数?摘要就是答案。
记住,目标不仅仅是收集指标,还要从中得出可行的见解。因此,选择正确的指标类型对于有效的监控和分析至关重要。这能确保你不是为了收集数据而收集数据,而是在收集真正能为有关应用程序性能和设计的决策提供依据的信息。
要了解更多关于指标以及如何查询它们的信息,请查看Prometheus文档(https://prometheus.io/docs/concepts/metric_types/)。
总之,在Golang中使用指标就像是乘坐潜艇开启一场伟大的冒险。你身处代码的深海之下,在性能的浑浊水域中航行。你的指标就像声纳,探测潜在问题,指引你穿越深渊,驶向高效、可扩展软件的乐土。记住,在系统编程的广阔海洋里,重要的不是船的大小,而是你的指标的力量,它能为你规划通向成功的航线。
# OTel项目
OTel是云原生计算基金会(Cloud Native Computing Foundation,CNCF)下的一个开源、中立于供应商的项目。它提供了一套标准、应用程序编程接口(API)和软件开发工具包(SDK),用于检测、生成、收集和导出遥测数据。
这些数据包括跟踪信息(请求在系统中的流动情况)、指标(关于系统行为的测量数据)和日志(结构化的事件记录)。此外,它旨在规范应用程序的检测方式,让采用可观测性工具变得更加容易,同时避免供应商锁定。
从成熟度的角度来看,Golang是OTel主要支持的语言之一。基本上,它提供了一个全面的SDK,包含以下方面的库:
- 跟踪:
go.opentelemetry.io/otel/trace
- 指标:
go.opentelemetry.io/otel/metric
- 上下文传播:
go.opentelemetry.io/otel/propagation
OTel的Go SDK能与流行的库和框架无缝集成,让你轻松为现有的Golang应用程序添加检测功能。
此外,该SDK支持各种导出器,使你能够将遥测数据发送到不同的分析后端。OTel网站(https://opentelemetry.io/ecosystem/vendors/ (opens new window))上有一份详尽的供应商列表。
在Go项目中采用OTel的主要好处如下:
- 供应商中立:你可以自由在不同的可观测性工具和后端供应商之间切换,而无需对应用程序代码进行大量修改。
- 简化检测:OTel让检测你的Golang服务变得更加轻松,不再那么繁琐。
- 统一数据格式:它提供标准化的数据格式,确保你的跟踪和指标数据能被多个平台和工具理解。
- 强大的社区支持:Go SDK有一个活跃的社区作为后盾,提供支持并不断推动其改进。
随着OTel的应用越来越广泛,它很可能成为Golang应用程序可观测性的事实上的标准。这种标准化通过促进供应商中立性、可移植性以及更轻松地采用最佳实践,让整个生态系统都受益。
# 开放遥测(OTel,OpenTelemetry)
所以,你觉得在程序里添加开放遥测就像把一些酷炫的乐高积木拼在一起,是吧?搞点神奇的配置,再来点自动检测(auto-instrumentation),然后瞧——瞬间就能实现可观测性了!这么说吧,朋友,你可要有心理准备,事情没这么简单。
在你气得把键盘扔出去之前,咱们先来详细讲讲开放遥测是什么。把它想象成一个通用工具包,用于从你的应用程序中收集遥测数据。开放遥测就像是你应用程序的内心独白——记录它的执行轨迹、性能指标、日志,以及其他能揭示其内部运行情况的信息。开放遥测能让你照亮代码库中最隐蔽的角落,让你知道哪里运行速度变慢了、哪里出现了错误,以及用户是如何与你开发的应用进行交互的。
Go语言的日志软件开发工具包(SDK,Software Development Kit )仍在开发中,我们可以在其官方状态页面(https://opentelemetry.io/status/ )上查看进展情况。因此,以下示例将使用uber/zap库进行日志记录。 |
---|
开放遥测本身是一组规范、应用程序编程接口(API,Application Programming Interface )和软件开发工具包。它并不会神奇地让你的应用变得具有可观测性。你得在代码各处有策略地放置传感器(把它们想象成高级探针)。这就涉及到手动检测的“乐趣”了,而且你首先得决定要收集哪些数据。
下面我们从头开始创建一个使用开放遥测的程序。步骤如下:
- 创建Go项目:为你的项目创建一个新目录,并初始化一个Go模块:
mkdir telemetry-example
cd telemetry-example
go mod init telemetry-example
2
3
- 安装依赖项:安装开放遥测和zap日志记录所需的软件包:
go get go.uber.org/zap
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
go get go.opentelemetry.io/otel/sdk/resource
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
go get go.opentelemetry.io/otel/semconv/v1.7.0
2
3
4
5
6
7
- 初始化zap日志记录器:在项目目录中创建一个新的main.go文件。首先,我们用zap来设置高级日志记录:
package main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync() // Flushes buffer, if any
sugar := logger.Sugar()
sugar.Infow("This is an example log message", "location", "main", "type", "exampleLog")
}
2
3
4
5
6
7
8
9
10
11
12
这段代码使用zap初始化了一个生产级别的日志记录器,它提供了结构化日志记录功能。 4. 配置开放遥测追踪:接下来,在你的应用程序中添加开放遥测追踪功能,将数据发送到开放遥测收集器(OTel Collector):
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
// Previous Zap logger setup...
ctx := context.Background()
traceExporter, err := otlptrace.New(ctx, otlptracehttp.NewClient())
if err != nil {
sugar.Fatal("failed to create trace exporter: ", err)
}
tp := trace.NewTracerProvider(
trace.WithBatcher(traceExporter),
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("ExampleService"),
)),
)
otel.SetTracerProvider(tp)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这部分添加了追踪功能,并配置为通过开放遥测协议(OTLP,OpenTelemetry Protocol )导出追踪数据。 5. 添加一个示例HTTP处理程序:为了进行演示,添加一个简单的HTTP处理程序,它会为每个请求发出追踪信息和日志:
func exampleHandler(w http.ResponseWriter, r *http.Request) {
_, span := otel.Tracer("example-tracer").Start(r.Context(), "handleRequest")
defer span.End()
zap.L().Info("Handling request")
w.Write([]byte("Hello, World!"))
}
func main() {
// Previous setup...
http.Handle("/", otelhttp.NewHandler(http.HandlerFunc(exampleHandler), "Example"))
sugar.Fatal(http.ListenAndServe(":8080", nil))
}
2
3
4
5
6
7
8
9
10
11
12
- 运行你的应用程序:在运行应用程序之前,在终端中使用位于ch11/otel/目录下的文件运行docker-compose:
docker-compose up
收集器应该已设置为在默认的开放遥测协议端口接收追踪数据,并将其路由到你的追踪后端。 运行你的应用程序:
go run main.go
- 从浏览器或使用curl访问应用程序(例如,http://localhost:8080/ ):
curl http://localhost:8080/
瞧!我们利用开放遥测的无锁定特性制作了一个应用程序!
以前,我们调试系统时,靠的是打印语句,偶尔还会气得骂几句。开放遥测则文明多了。把它想象成在你的代码里构建一个错综复杂的“情报网络”。这些“情报员”会汇报每一个细节,让你不仅能更快地找出问题,有时候甚至能在问题造成严重破坏之前就发现它们。
这不是比传统的调试方式好多了吗?现在,是时候结束这部分内容了。
# 总结
在结束关于Go语言遥测的这一章时,我们探索了一些关键实践和工具,这些实践和工具揭示了Go应用程序的内部机制,提高了它们的可观测性。这次探索从深入研究日志记录开始,我们学会了超越基本的日志消息,采用结构化日志记录,因为它更清晰、更便于分析。接着,我们进入了追踪这个复杂但至关重要的领域,揭示了应用程序错综复杂的执行路径,以识别和解决性能瓶颈。此外,我们还涉足了指标领域,通过定量数据测量,我们能够监控和优化应用程序,使其达到最佳性能。最后,我们将所有这些知识整合到一个由开放遥测支持的、不依赖特定供应商的解决方案中。
在下一章,我们将开始探讨如何分布式部署我们的应用程序。