云基础设施管理与结构化日志实践
1. 基础设施管理中的依赖关系
在管理云基础设施时,常常会遇到资源之间存在依赖关系的情况。比如,在一个
google_sql_database_instance
类型的资源中,若要创建用户,前提是该数据库实例已经存在。若在数据库未准备好时就创建用户,显然是不合理的。
为确保Terraform能遵循这些依赖关系,我们需要使用引用(References)来告知它。引用是一种向Terraform说明资源依赖关系的方式。例如,访问控制列表(ACL)依赖于云存储桶(Cloud Storage bucket),这也是一种依赖关系的体现。云存储是Google Cloud的产品,用于存储二进制对象(blobs),而存储桶则是组织这些对象的方式。
在设置引用时,可以使用资源暴露的属性。通常会引用资源的名称属性,但像Cloud Run服务的URL这类生成的属性也非常有用。这些生成的属性存储在Terraform状态中,可以使用
terraform console
来确定正确的引用,以建立依赖关系。
1.1 学习Terraform的补充资源
- Google Cloud Platform Provider文档 :该文档列出了与Google Cloud协作的特定资源,有助于理清可创建的各种资源之间的关系,可作为官方Google Cloud文档的补充,因为Terraform的资源模型更便于理解各资源的组合方式。
- Cloud Foundation Fabric :这是GitHub上的一个有价值的资源,适用于构建更高级的Google Cloud基础设施。它是一组经过维护的资源,通常以特定的方式将底层原语打包在一起。
2. 基础设施即代码的优势
基础设施即代码(Infrastructure as Code)是一种以声明方式描述云资源的方法。它有助于轻松创建可重现的环境,并且Terraform配置文件作为源代码可以进行版本控制。这使得系统更易于理解和维护,尤其是在系统规模不断扩大时。
使用Terraform,需要了解一些核心概念,包括Terraform状态、配置语言和工作流程。具体操作步骤如下:
1. 编辑配置文件以进行更改。
2. 使用
plan
命令评估更改的结果。
3. 使用
apply
命令对云基础设施进行修改。
即使在使用无服务器架构时,如果仅通过一次性命令或脚本设置所有内容,也可能会遇到挑战。
3. 结构化日志与跟踪概述
结构化日志是指在应用程序的日志中添加元数据,以便在阅读日志时能获取更多上下文信息,从而将相关日志分组或进行过滤。元数据可以包括日志的严重级别和相关业务属性等。
在Google Cloud上,日志管理由Cloud Logging负责。它允许创建仪表盘、交互式构建查询以查找日志,并显示日志活动的直方图。在生产环境中,需要将日志与请求关联起来,以便轻松查看在处理单个请求过程中生成的所有日志。同样,当处理一个请求并需要调用另一个Cloud Run服务时,也希望将第一个请求和第二个请求的日志分组显示。这可以通过将跟踪头(trace header)传播到下游服务来实现。
3.1 Cloud Run的日志捕获
Cloud Run会捕获容器的日志,并将其转发到Cloud Logging。具体捕获内容如下:
- 容器输出流:包括容器进程的标准输出(standard out)和标准错误(standard error)。
-
/var/log
目录(或子目录)中文件的每一行写入内容。
- 系统日志(Syslog):如果在应用程序中使用了syslog库,这些日志也会被捕获。
此外,Cloud Run还会将请求日志转发到Cloud Logging,其中包括请求路径、响应状态码和延迟数据。建议使用容器输出流进行日志记录,因为它是从容器进行日志记录的一种可移植且标准的方式。
3.2 日志查看方式
3.2.1 在Web控制台查看日志
Cloud Run的Web控制台提供了一种快速便捷的方式来访问服务的日志。只需导航到服务页面,即可看到“Logs”标签。不过,Cloud Logging的完整界面提供了更多功能,可以通过点击小方框箭头按钮直接访问服务的日志。
3.2.2 在终端查看日志
虽然在本书的大多数示例中使用了基于文本的命令行界面,但对于日志查看,可视化界面可能更适合用于在大量数据中查找模式。不过,在开发服务时,能够在终端快速跟踪日志(tail logs)会很方便。截至2020年9月,Cloud Run CLI尚未具备此功能,但App Engine和Cloud Functions有简单的方式来跟踪日志,预计Cloud Run也会跟进。
可以使用以下命令查看项目中任何Cloud Run服务的最后一条日志:
$: gcloud logging read "resource.type=cloud_run_revision" --limit 1
---
insertId: 5f27e14900040088e8728110
labels:
instanceId: 00bf4bf02d8c07f620...
logName: projects/my-project/logs/run.googleapis.com%2Fstdout
receiveTimestamp: ’2020-08-03T10:04:57.429391709Z’
resource:
labels:
configuration_name: hello-world
location: europe-west1
project_id: my-project
revision_name: hello-world-00001-yed
service_name: hello-world
type: cloud_run_revision
textPayload: Hello from "Hello World Logging"
timestamp: ’2020-08-03T10:04:57.262280Z’
该命令的输出完整,便于使用脚本进行处理。
3.3 查找隐藏日志
有时,需要额外的工作来查找应用程序的日志,并确保它们被转发到Cloud Logging。例如,某些PHP应用程序默认将日志写入应用程序目录中的文件,这使得Cloud Run无法捕获这些日志。另外,如果容器中运行多个进程,且进程管理器未聚合日志输出流,也会导致日志不可见。在这种情况下,可以将应用程序配置为将相关日志写入
/var/log
目录。需要注意的是,无需转发Web服务器日志(如NGINX),因为Cloud Run已经单独保留了请求日志。
3.4 Operations Suite相关服务
Cloud Logging会捕获Google Cloud项目中每个服务的所有日志,包括平台日志、审计日志和其他应用程序的日志。它是Google Cloud Operations Suite的一部分。此外,还涉及Cloud Monitoring、Error Reporting和Cloud Trace,它们与Cloud Logging集成。
-
Cloud Monitoring
:跟踪指标,可用于创建图表和设置警报。
-
Error Reporting
:在日志中查找堆栈跟踪,并记录其发生频率。
-
Cloud Trace
:收集请求的延迟数据,帮助查找性能问题。
4. 日志级别与结构化日志
4.1 纯文本日志的局限性
从Cloud Run容器发送日志的默认体验较为基础,只能逐行查看容器的日志。Cloud Run在转发日志之前会进行一些处理,例如将常见的多行日志(如堆栈跟踪)合并为一个日志事件,但除此之外,日志内容基本不变。纯文本日志缺少日志级别(如debug、info、warning、error、fatal或panic),每行日志的优先级默认为“default”,这使得查找实际错误变得困难。而结构化日志则可以解决这个问题,它不仅可以添加日志级别,还能提供更多信息。
4.2 演示应用程序
为了更好地说明结构化日志,提供了一个演示应用程序。这是一个API,用于从包含宝可梦角色(日本虚构生物)的开放数据集中返回随机项目。宝可梦数据集丰富的数据为结构化日志提供了良好的素材。该应用程序的仓库可以在GitHub上找到。
4.3 结构化日志的实现
结构化日志是指在日志中添加结构化的元数据,通常以JSON格式记录。以下是演示应用程序中在生成宝可梦时记录元数据的示例代码:
p, _ := pokeapi.Pokemon("1")
log.Info().
Int("Weight", p.Weight).
Str("Name", p.Species.Name).
Msg("Spawning pokemon")
/* Output: {
"severity": "INFO",
"Weight": 69,
"Name": "bulbasaur",
"time": 1601626008,
"message": "Spawning pokemon"
}*/
Cloud Logging会自动识别JSON格式的日志,并使额外的字段可用。可以使用自定义属性搜索日志,例如查看与同一宝可梦相关的所有日志,或仅查看处理极重宝可梦的日志。
4.4 客户端库的使用
除了记录JSON格式的消息,还可以使用Cloud Logging客户端库直接将日志发送到Cloud Logging API。其工作方式是:使用库编写日志,库会将日志缓存在内存中,并定期将其发送到Cloud Logging API。
然而,使用客户端库会绕过容器输出流,直接连接到Google Cloud Logging API,这会影响应用程序的可移植性,降低在其他环境(如Google Cloud之外或本地机器)部署的能力。由于Cloud Logging已经集成到Cloud Run中,因此只需以正确的格式编写日志即可,这里将介绍如何从容器中编写结构化JSON日志。
4.5 不同语言的结构化日志
示例代码使用Go语言的
zerolog
包,但其他编程语言也可以实现结构化日志。
zerolog
是一个零内存分配的Go包,用于编写JSON格式的日志,性能非常高。其他语言也有类似的库,结构化日志已经存在了一段时间。需要确保JSON消息的结构符合Cloud Logging的期望。如果搜索相关插件,要注意Cloud Logging产品以前称为Stackdriver。由于消息结构并不复杂,也可以自行开发插件。
4.6 日志级别的使用
如果谨慎且有规律地使用不同的日志级别,将能从日志中获取更多价值。以下是不同日志级别及其使用场景的列表:
| Cloud Logging级别 | Zerolog函数 | 使用场景 |
| — | — | — |
| DEBUG | Debug() | 调试日志非常详细,仅用于本地调试,通常在生产环境中禁用。 |
| INFO | Info() | 常规状态消息,如“Updated record.” |
| WARNING | Warn() | 警告可能表示存在错误,如“Record not found”、“Configuration not found, using default.” |
| ERROR | Error() | 发生异常,如“Failed to save record.” 应尽量使日志中不出现错误,避免将错误作为正常流程的一部分进行记录。 |
| CRITICAL | Fatal() | 关键错误需要纠正,例如数据库在重试后仍然不可用。调用
Fatal()
会停止容器,用户将收到“Service Unavailable - HTTP 503”响应。 |
| ALERT | Panic() | 当应用程序严重损坏,需要立即有人采取行动时使用。
zerolog
会调用Go的内置
panic
函数,停止程序执行并输出堆栈跟踪。 |
4.7 捕获恐慌(Panics)
如果使用
zerolog.Panic()
记录事件,它会记录消息并调用Go的内置
panic
函数。当程序遇到越界索引或空指针时,也可能发生恐慌。恐慌的程序会输出纯文本的堆栈跟踪并退出。
尽管堆栈跟踪以纯文本形式输出,但Cloud Logging会检测到并添加一个默认日志级别(“unspecified”)的日志事件,Error Reporting也会注意到并开始跟踪该事件,无需进行额外配置,并且Error Reporting也能检测其他语言的堆栈跟踪。
4.8 日志中的敏感信息
在日志中添加更多元数据时,需要注意安全问题。建议避免在日志中包含个人身份信息或其他敏感信息。可以在网上搜索“plain text password in logs”,了解在日志中包含敏感信息可能导致的问题,这不仅限于结构化日志。
4.9 本地开发中的日志处理
在本地开发过程中,阅读JSON格式的日志消息可能不太方便。不过,
zerolog
包提供了
zerolog.ConsoleWriter
,可以输出可读(且带颜色)的日志,方便开发使用。
4.10 请求上下文的重要性
在生产环境中,如果能够点击一个日志事件并查看在处理同一请求过程中生成的所有其他日志,将非常有用。当服务同时处理大量请求时,这是一项不可或缺的功能,因为应用程序在处理单个请求时可能会生成大量日志消息。如果多个请求同时发生,所有消息会交织在一起,难以确定哪些日志属于同一个请求。
Google Frontend(GFE)会为所有传入请求添加跟踪头(X-Cloud-Trace-Context),其中包含唯一的跟踪ID。如果将该跟踪ID添加到与处理请求相关的所有日志中,这些日志将在Cloud Logging中一起显示。
以下是使用Yuki Furuyama的
crzerolog
包将跟踪ID添加到日志的示例代码:
func main() {
rootLogger := zerolog.New(os.Stdout)
// Create an HTTP handler that adds request context to logs
loggingHandler := crzerolog.InjectLogger(&rootLogger)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Retrieve the logger from the request
logger := log.Ctx(r.Context())
logger.Debug().Msg("1st")
logger.Info().Msg("2nd")
logger.Error().Msg("3rd")
})
// Wrap the logging handler around the http handler
handler := loggingHandler(mux)
if err := http.ListenAndServe(":8080", handler); err != nil {
log.Fatal().Msg("Can’t start service")
}
}
该包允许从请求上下文中提取日志记录器。使用该日志记录器时,跟踪ID会自动添加到日志中,使用Cloud Logging期望的属性名称。示例输出如下:
{
"severity": "DEBUG",
"logging.googleapis.com/trace": "[trace ID]",
"time": "2020-10-02...",
"message": "1st"
}
logging.googleapis.com/trace
属性保存跟踪ID,Cloud Logging使用该ID将日志与请求关联起来。通过部署上述代码到Cloud Run并同时发送1000个请求的测试,可以看到请求上下文的实用性。在同一毫秒内记录了来自三个不同请求的九个日志事件,如果没有请求上下文,很难理解这些日志的含义。使用
crzerolog
包还会在日志中添加源位置信息,例如可以知道消息“1st”是在
main.go
的第21行记录的(点击“Expand nested fields”可查看所有属性)。
综上所述,合理管理云基础设施的依赖关系、运用基础设施即代码的方法,以及正确使用结构化日志和跟踪技术,能够提升系统的可维护性和可调试性,为开发和运维工作带来便利。
5. 结构化日志在云环境中的应用流程
为了更清晰地展示结构化日志在云环境中的应用过程,下面通过一个 mermaid 流程图来呈现:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B(应用程序生成日志):::process
B --> C{日志格式}:::decision
C -->|JSON格式| D(Cloud Logging自动处理):::process
C -->|非JSON格式| E(使用客户端库转换):::process
D --> F(Cloud Logging存储日志):::process
E --> F
F --> G(用户查询日志):::process
G --> H{查询方式}:::decision
H -->|Web控制台| I(使用Cloud Run Web控制台或Cloud Logging界面):::process
H -->|终端| J(使用gcloud命令):::process
I --> K(查看日志结果):::process
J --> K
K --> L([结束]):::startend
这个流程图展示了从应用程序生成日志到用户查看日志的完整过程。应用程序生成的日志可以是 JSON 格式,也可以是非 JSON 格式。如果是 JSON 格式,Cloud Logging 会自动处理;如果是非 JSON 格式,则需要使用客户端库进行转换。处理后的日志会存储在 Cloud Logging 中,用户可以通过 Web 控制台或终端进行查询。
6. 结构化日志的最佳实践总结
6.1 日志级别使用原则
- DEBUG :仅在本地开发和调试时使用,避免在生产环境中启用,以免产生过多无用日志。
- INFO :用于记录常规的状态信息,帮助了解系统的正常运行情况。
- WARNING :记录可能存在问题的情况,但不一定是错误,提醒开发人员关注。
- ERROR :记录异常情况,应尽量减少错误日志的出现,确保系统的稳定性。
-
CRITICAL
:当系统出现关键错误,如数据库无法连接时使用,调用
Fatal()会停止容器。 -
ALERT
:当应用程序严重损坏,需要立即采取行动时使用,会调用
panic函数。
6.2 避免敏感信息
在日志中避免记录个人身份信息、密码等敏感信息,防止信息泄露。
6.3 利用请求上下文
在生产环境中,使用请求上下文将相关日志关联起来,方便调试和问题排查。例如,通过传播跟踪头,将不同服务的日志关联到同一个请求。
6.4 选择合适的日志记录方式
优先使用容器输出流进行日志记录,因为它是一种可移植且标准的方式。如果需要使用客户端库,要考虑其对应用程序可移植性的影响。
6.5 结合其他服务
将 Cloud Logging 与 Cloud Monitoring、Error Reporting 和 Cloud Trace 等服务结合使用,全面监控系统的性能和健康状况。
7. 示例总结与回顾
7.1 基础设施管理示例
在基础设施管理中,通过引用告知 Terraform 资源之间的依赖关系,确保资源的正确创建顺序。例如,在创建数据库用户之前,确保数据库实例已经存在。
7.2 结构化日志示例
通过演示应用程序,展示了如何使用结构化日志添加元数据,如日志级别、宝可梦的名称和重量等。以下是回顾的示例代码:
p, _ := pokeapi.Pokemon("1")
log.Info().
Int("Weight", p.Weight).
Str("Name", p.Species.Name).
Msg("Spawning pokemon")
/* Output: {
"severity": "INFO",
"Weight": 69,
"Name": "bulbasaur",
"time": 1601626008,
"message": "Spawning pokemon"
}*/
7.3 请求上下文示例
使用
crzerolog
包将跟踪头添加到日志中,实现请求上下文的关联。示例代码如下:
func main() {
rootLogger := zerolog.New(os.Stdout)
// Create an HTTP handler that adds request context to logs
loggingHandler := crzerolog.InjectLogger(&rootLogger)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Retrieve the logger from the request
logger := log.Ctx(r.Context())
logger.Debug().Msg("1st")
logger.Info().Msg("2nd")
logger.Error().Msg("3rd")
})
// Wrap the logging handler around the http handler
handler := loggingHandler(mux)
if err := http.ListenAndServe(":8080", handler); err != nil {
log.Fatal().Msg("Can’t start service")
}
}
8. 总结
通过本文的介绍,我们了解了云基础设施管理中资源依赖关系的处理方法,以及结构化日志在云环境中的重要性和应用实践。合理管理资源依赖关系可以确保云基础设施的正确部署,而结构化日志则可以提供更多的上下文信息,帮助我们更好地监控和调试系统。
在实际应用中,我们应该遵循结构化日志的最佳实践,选择合适的日志级别,避免记录敏感信息,利用请求上下文关联日志,并结合其他相关服务进行全面监控。通过这些方法,可以提高系统的可维护性和可靠性,为云应用的开发和运维提供有力支持。
超级会员免费看

被折叠的 条评论
为什么被折叠?



