杜克大学-Rust-编程-数据工程与-DevOps-笔记-全-

杜克大学 Rust 编程、数据工程与 DevOps 笔记(全)

001:课程概述与讲师介绍 🚀

在本节课中,我们将要学习《Rust编程2-3(数据工程、DevOps)》课程的概述,了解课程的核心内容、结构以及讲师的背景。Rust语言以其安全性、速度和底层控制能力,为系统编程和数据工程提供了强大的支持。

课程概述

欢迎来到《使用Rust进行数据工程》课程。Rust为系统编程提供了安全性、速度和底层控制,本课程将对此进行详细讲解。本课程教授如何构建高性能的数据管道,这些管道可以应用于数据工程、MLOps以及传统的软件工程领域。

本课程将涵盖四个关键部分。第一部分是Rust数据结构。我们将探讨Rust的核心特性,包括哈希映射和向量。在第二部分,我们将探讨Rust的安全性与并发性。我们将介绍Rust的一些关键能力,例如Rayon库,它允许你以最小的努力进行多线程编程。在第三部分,我们将介绍流行的Rust数据工程库和工具。其中包括Polars库,这是一个用于处理数据框的新兴工具,以及如何与云服务商交互,例如使用AWS SDK进行异步通信。在第四部分,我们将深入探讨如何在Rust中设计数据处理系统。这涉及到构建现实世界的解决方案,将这些解决方案整合在一起,并协调处理流程,例如使用AWS Step Functions或处理执行ETL的数据管道,这些都是第四部分涵盖的主题。

现在,我们为什么要学习Rust?因为Rust是一种快速、内存高效且安全的语言,它提供了底层控制能力,同时也易于维护。它是一种现代的编译型语言,并且借助生成式AI编码工具,它变得更加易于上手。本课程的先决条件是,需要对Rust编程语言有基本的了解,同时熟悉数据结构和算法会更有帮助。

课程结构详解

接下来,让我们更详细地了解一下课程的核心结构。

第一部分:Rust数据结构

在第一部分,我们将首先深入了解现代Rust开发生态系统的入门知识。这意味着我们将学习如何使用结对编程工具、提示工程,以及了解持续集成。

在下一课“序列与映射”中,我们将探讨一些常见的数据结构,包括向量、向量双端队列以及哈希映射,并学习如何在这些数据结构中构建解决方案。

在第三课中,我们将更深入地探讨数据结构,包括高级数据结构,如图、优先队列以及其他项目,如PageRank算法。这将真正完成对数据工程所需的Rust数据结构的核心理解。

第二部分:Rust的安全性与并发性

接下来,在第二部分,我们将深入探讨Rust的安全性与并发性。为此,我们将介绍Rust在安全和安全性方面的一些核心特性,例如理解多因素身份验证、理解加密,以及如何处理可变和不可变的数据结构。

在第二课中,我们将探讨更多的安全概念,包括密码和加密,甚至事件响应与合规性。

最后,在本部分的最后一课“并发性”中,我们将探讨一些经典问题,如哲学家就餐问题,使用Rayon进行一些网络爬虫,并且我们还将构建智能聊天机器人。

第三部分:Rust数据工程库与工具

在第三部分“Rust数据工程库与工具”中,我们首先从使用Rust管理数据文件和网络存储开始。我们将学习使用Cargo Lambda构建CSV文件以部署到AWS Lambda,以及通过AWS提供的Async库与S3存储进行通信。

在第二课中,我们将深入探讨数据框,包括Rust中的数据框处理,并处理笔记本环境。一些示例包括使用Colab处理预期寿命数据、处理Polars等,这些都是本部分涵盖的内容。

现在,在第三课中,我们将更详细地探讨基于云的SDK,包括使用Rust的Google Cloud Shell、使用Rust的AWS Cloud Shell、使用CodeWhisperer和Rust的Cloud 9,并且还将使用Rust部署一些微服务。

第四部分:在Rust中设计数据处理系统

在第四部分,我们将深入探讨如何在Rust中设计数据处理系统,这主要包括数据管道本身。涵盖的项目包括Rust AWS Step Functions、Rust AWS Lambda,讨论Distroless(一个新兴的容器标准)。

接着在第二课中,我们将简要介绍NLP管道,包括语言模型、ONNX、Hugging Face、PyTorch管道,并围绕这些技术构建一些解决方案。

最后,在第三课中,我们将深入探讨SQL,并研究如何将Rust与SQL结合使用,包括与SQLite、Hugging Face、BigQuery的结合,同时查看公共数据集,并讨论一些关于选择正确数据库的理论。

讲师介绍

我是你们的讲师,Noah Gift,很高兴欢迎你们加入Rust的学习之旅。接下来我将简要介绍一下我的背景。

大家好,我的名字是Noah Gift,我是你们《Rust数据工程》课程的讲师。让我简单介绍一下我的背景,以及为什么这与本课程相关。我是杜克大学的驻校高管,曾在数据科学系(MIDS)以及AIPI(产品创新人工智能)部门工作。我教授的课程包括数据科学、云计算和MLOps,我也曾在西北大学、加州大学伯克利分校、加州大学戴维斯分校等大学讲授过类似的主题。我的职业生涯始于好莱坞的电视和电影行业,我曾参与许多大型电影的制作,包括《阿凡达》、迪士尼动画长片,以及索尼图形图像运作公司的电影如《蜘蛛侠》。之后,我来到了湾区,在初创公司担任首席技术官。这使我走到了今天这一步,即将我的知识与学生们分享。在本课程中,我们将围绕数据工程代码和MLOps代码的运维化展开大量讨论,而Rust是实现这一目标的绝佳选择。我发现,对于许多身处数据工程和MLOps领域的人来说,Rust将是非常有益的补充。我很高兴能与你们分享我的知识,并期待看到你们的成果。

总结

本节课中我们一起学习了《Rust编程2-3(数据工程、DevOps)》课程的概述。我们了解了Rust在数据工程领域的优势,课程将涵盖的四个核心部分(数据结构、安全与并发、工具库、系统设计),以及讲师Noah Gift的相关背景。课程旨在通过实际案例和工具,帮助你掌握使用Rust构建高性能、安全的数据处理系统的技能。

好了,介绍就到这里。我真的很期待你们开始使用Rust构建解决方案。让我们开始这门课程吧!

002:人工智能编程范式转变介绍 🚀

在本节课中,我们将要学习人工智能(AI)如何与编程实践相结合,催生出一种新的范式——AI结对编程。我们将探讨它如何与现有的DevOps最佳实践协同工作,并了解开发者可以如何利用多种AI工具来提升效率。


人工智能编程范式

这里出现了一种新的范式转变,即AI结对编程。它与现有的最佳实践(包括DevOps)结合得非常好。

上一节我们介绍了编程范式的演变,本节中我们来看看AI如何融入其中。

与现有生态的协同

在左侧,我们有GitHub生态系统。你可以用它做许多了不起的事情,例如使用GitHub Actions。这是一个CI/CD服务器,可以自动进行矩阵测试、代码检查、为你构建软件包,甚至在Rust的情况下编译软件。然后你还有Codespaces,这是一个专门为特定环境(例如Rust、Python或某个特定版本的Ubuntu)设置的、触手可及的环境。因此,你在这里获得的是一个真正以生产为先的容器化环境。

此外,你可以使用像Visual Studio Code这样的顶级编辑器。它是最流行的编辑器之一,内置了各种钩子和插件,能让你非常高效地工作。

多工具协作的工作流

然而,当你完成所有这些设置后,未来你将看到的是,你不仅会使用像GitHub Copilot这样的AI结对编程工具,还可能将其与其他工具结合使用。

以下是开发者可能采用的一种典型工作流程:

  1. 咨询与构思:首先,你可以就某个特定问题向ChatGPT提问。
  2. 实现与迭代:将得到的思路放入Copilot,让它协助完成代码。
  3. 验证与补充:在某些场景下,你可能不完全满意得到的结果,或者存在限制(例如ChatGPT的输入文本长度限制)。这时,你可以转向另一个工具,如Google Bard,来双重检查ChatGPT所讨论的内容。
  4. 切换与深化:你甚至可能在某一段编码(例如30分钟内)坚持使用Google Bard,直到遇到瓶颈。然后,你实际上可以切换到另一个工具,例如AWS Code Whisperer。

我认为我们将看到人们使用许多不同的资源,就像在AI结对编程出现之前一样——那时你会去Stack Overflow、Google,或者去学习平台查看某些内容。现在,你将把所有这些东西结合起来,在进行结对编程时获得最佳的混合体验。

DevOps基础设施的价值提升

同时请注意,DevOps基础设施并不会消失。事实上,它变得更有价值,因为它是自动化的最佳实践。当你从AI结对编程工具得到一个结果时,你可以利用这些实践来验证那些操作是否合适。

你可以使用代码检查和格式化工具来清理结果。最后,如果你使用基础设施即代码(Infrastructure as Code),它将以编程方式将该结果部署到特定环境。

因此,使用这些AI结对编程工具不仅仅是一种替代,它实际上是与你正在使用的现有最佳实践产生的协同效应。事实上,使用AI结对编程的最佳方式就是拥有持续交付和持续集成的最佳实践,然后用AI结对编程来增强它。


本节课中我们一起学习了AI结对编程这一新范式。我们了解到,它并非要取代现有的开发流程和DevOps实践,而是与之形成强大的协同。通过结合GitHub生态系统、多种AI辅助工具(如Copilot、ChatGPT、Bard)以及自动化的CI/CD管道,开发者可以构建一个更高效、更可靠且以生产为先的现代化工作流。关键在于利用自动化最佳实践来验证和增强AI的产出,从而实现“1+1>2”的效果。

003:基于云的开发环境介绍 🚀

在本节课中,我们将学习如何使用GitHub Codespaces这一强大的云端开发环境。我们将从创建仓库、配置环境开始,逐步探索其核心功能,包括使用模板、配置点文件、利用GitHub Copilot进行编程辅助等。


创建与配置仓库 🏗️

首先,我们需要进入GitHub组织并创建一个新的代码仓库。创建时,有几个重要选项需要考虑。

以下是创建新仓库时的关键步骤:

  • 可见性:选择仓库是公开(Public)还是私有(Private)。
  • 使用模板:从模板创建可以节省大量时间。你可以为不同语言创建模板,这样所有环境都已预先配置好。

例如,我们可以搜索并选择一个Rust项目模板。如果你想创建自己的点文件仓库,只需重复创建过程,并在仓库中放置如 .bashrc 这样的配置文件。


深入Codespaces环境 ☁️

上一节我们介绍了如何创建仓库,本节中我们来看看如何启动并配置一个Codespace。

Codespaces是一个基于云的环境,允许你在浏览器中直接编辑代码。创建仓库后,你可以选择“Code”按钮,然后“Open with Codespace”。你可以选择默认配置,或点击“New with options”进行自定义。

以下是启动Codespace时的配置选项:

  • 选择配置:为你的项目选择预置的开发容器配置(例如Rust配置)。
  • 选择机器类型:根据项目需求,选择从2核到16核,甚至带GPU的不同规格虚拟机。

启动后,系统会构建容器。为了加速后续启动,可以配置“预构建”功能,它会自动安装所有配置好的软件。


个性化与模板化 ⚙️

进入Codespace后,我们可以个性化开发体验,例如更改编辑器主题。更重要的是,我们可以将配置好的仓库保存为模板,供未来或其他用户使用。

若想将当前仓库设为模板,只需进入仓库的“Settings”,找到并启用“Template repository”选项。这是一个提高个人或团队效率的好方法。


探索开发容器配置 🐳

每个Codespace背后都是一个开发容器,其配置由 Dockerfiledevcontainer.json 等文件定义。

以Rust模板为例,其 Dockerfile 通常继承自微软提供的Rust基础镜像。你可以在其中添加自定义命令,例如安装额外的工具链(如 clanglld)。由于使用了预构建,这些安装只需一次。

你还可以在 devcontainer.json 中配置每次启动时自动安装的VSCode扩展。


使用GitHub Copilot编程助手 🤖

现在我们已经配置好了环境,接下来可以体验一个强大的功能:GitHub Copilot。它可以像一个结对编程伙伴一样协助你。

在Codespace中安装“GitHub Copilot Chat”扩展后,你可以通过聊天界面与它交互。例如,你可以要求它“创建一个新的Rust项目”。它会指导你运行 cargo new hello_world 命令。

创建项目后,你可以继续与Copilot对话,例如让它“创建一个add函数”。它会生成相应的Rust代码。你可以在主函数中调用这个新函数,并通过 cargo run 来运行和测试。


命令行辅助工具:Copilot CLI 💻

除了聊天界面,GitHub Copilot还提供了一个命令行辅助工具(Copilot CLI,技术预览版),它可以直接在终端中帮助你。

在终端中输入 ?? 后跟你的问题,即可获得建议。例如,输入 ?? 显示目录结构,它会建议你运行 tree 命令。你还可以进一步优化请求,例如 ?? 运行tree命令,但只显示源文件,不显示构建产物,它会生成过滤了 target/ 目录的命令。


总结 📚

本节课中我们一起学习了GitHub Codespaces的完整工作流程。我们从创建仓库和配置模板开始,然后启动并个性化Codespace环境。我们探索了开发容器的配置,并实践了如何使用GitHub Copilot Chat进行交互式编程辅助,以及如何使用Copilot CLI来优化命令行操作。这些工具共同构成了一个强大、高效的云端开发环境。

004:GitHub Copilot与Rust生态系统介绍 🚀

在本节课中,我们将学习如何利用GitHub生态系统(特别是GitHub Copilot、Visual Studio Code和GitHub Codespaces)来提升Rust编程的效率。我们将通过一个简单的“Marco Polo”游戏示例,演示如何结合这些现代工具来加速开发流程,并体验Rust语言在性能、内存效率和安全性方面的优势。


概述

作为一名软件工程师,使用GitHub生态系统内的Copilot是一种令人兴奋的工作方式。这不仅仅是单独使用Copilot,而是将其与Visual Studio Code、GitHub Codespaces和GitHub本身结合使用。这种组合能显著提升开发效率。

回想最初学习Python时,那种能够快速编写脚本、高效完成任务的感觉非常棒。如今,使用Copilot配合像Rust这样更强大的语言,能带来类似的体验,但效果更佳。Rust提供了现代的语言特性、现代的包管理系统、至少50倍以上的能效提升、25倍以上的计算性能提升,以及出色的内存效率。此外,从安全角度看,Rust编译器能构建出安全的并发程序,并在网络安全方面提供保障。

因此,核心问题是:是否值得为了这些优势而学习一门稍复杂的语言(Rust)并配合Copilot使用?答案是肯定的。 因为你可以利用已有的Python编程知识,通过即将展示的技巧,将其应用到Rust中,从而提升个人和公司的技术水平。这将成为2023年一项新兴的技能,Python程序员无需抛弃所学,只需应用这些新方法即可。


开始实践

接下来,让我们看看如何使用GitHub生态系统中的Rust新项目模板。这正是Copilot的魔力所在,它能让现有的Python程序员提升到新的水平。强烈推荐使用GitHub Codespaces、Copilot和Visual Studio Code的组合,这是实现飞跃的“秘密武器”。

在这个环境中,我们有一个Dockerfile来配置所需的所有环境,还有一个开发容器配置文件来设置例如Copilot等功能。这意味着我可以随时为新项目启动这个环境。只需使用这个模板,创建新仓库,选择额外选项,并挑选一台性能强大的机器来编译代码即可。


检查环境与创建项目

进入环境后,首先检查一切是否正常工作。我们可以查看这里的Makefile,在项目初期包含它是一个好主意。输入make rust-version,它会显示Rust版本等信息。与Python不同,这一切都是现成的,无需额外设置。

要创建新项目,只需输入:

cargo new hello_marco

我们将构建一个“Marco Polo”应用。进入项目目录后,可以看到包管理系统已准备就绪。接下来,需要在Cargo.toml文件中添加所需的依赖项。

我将添加一个名为Clap的流行命令行工具库。然后,由我决定项目结构。对于Python程序员,我建议进入src目录。

实际上,首先进入项目目录,然后创建src/lib.rs文件。我喜欢在这里放置核心逻辑。通过使用Copilot,我们可以在这里构建出令人惊叹的功能,这本质上让我们能够利用来自Python的技能。


编写核心逻辑与使用Copilot

首先,创建一个注释来描述这是一个“Marco Polo游戏”。然后,由我来创建正确的提示词。我会写:“如果给出的名字是‘Marco’,程序回应‘Polo’;否则,询问‘What‘s your name?’”。这看起来不错,就像在和真人对话一样,我们只需要正确地引导它。

我还会添加一些额外的提示词。同样,这需要我们引导Copilot。我们希望它完成一些工作。看,只要我们一路提供帮助,它就会给出良好的回应。

在这种情况下,我们声明函数为pub(公开),以便暴露给主模块使用,主模块将用于命令行工具。然后看看生成的Rust代码,实际上非常直观:我们有一个字符串类型的name参数,进行一些字符串处理,然后返回一个字符串。


编写主程序与集成

现在转到main.rs。这里的技巧在于,很多命令行工具库的代码都是样板代码。因此,我通常会从其他程序复制粘贴一些内容,以帮助我们的提示。

我会在这里放入一些内容:“一个用于玩Marco Polo游戏的命令行工具”。这些都是样板代码。关键点在于,你需要将子命令映射到在库中创建的函数。这与Python几乎相同,但区别在于,我获得了25倍的性能提升,并且可以部署二进制文件等很酷的功能。

完成这些后,我只需查看并确认:我想将一个名字传递到这个函数中。然后,我让Copilot为我完成剩下的工作。看看它做了什么:它生成了一个合理的建议,但并非完美。我想稍微调整一下。

我不希望它在这里直接打印名字,那不是我的本意。我想让它调用Marco Polo逻辑。因此,我会按Tab键,然后输入let result =,并引导它使用我模块中的命名空间。


调试与完善

当你刚开始使用Rust时,命名空间可能会让你有点困惑。例如,“这是什么?这个命名空间是什么?”实际上,它是在Cargo.toml文件中定义的名字。因此,你必须将其映射为相同的名字。

现在,我可以运行cargo fmt来格式化代码,看看会发生什么。它是否能清理一下代码?这里它提示“fail to use unresolved crate”。实际上,这是因为crate名称不正确,linter给出了正确的提示。所以我们必须将其改为hello_marco

看,这就是Cargo工具链和Copilot协同工作的地方:格式化工具、linter等所有东西一起工作。然后我只需不断迭代以获得解决方案。如果linter通过,我们就处于良好状态。


编译与运行

现在我可以使用最后一部分:实际运行cargo来执行程序。这很像从Python解释器运行东西。我只需输入:

cargo run --

这里的双破折号会将一些命令传递到我们的程序中。现在它将首次编译。Rust编译器非常出色,因为它会做很多很酷的事情来确保程序的安全和快速。我们可以看到它正常工作。

现在我可以输入play,然后输入Marco,这应该会返回“Polo”。实际上,出现了“unexpected argument ‘play‘”,因为我们需要使用--name参数。看,成功了:“Polo”。如果我输入“Bob”,它会说“What‘s your name?”。这是一个很好的反馈循环。


成果与优势

再看这里的target目录。如果我进入target/debug,并输入./hello_marco,会看到那个可执行文件。在我看来,这相对于常规Python是一个巨大的胜利,因为它提供了快速的反馈循环、利用Copilot提升水平的能力,以及利用现有工具链获得反馈的能力。这是Copilot这类新结对编程工具带来的新兴特性。


总结

本节课中,我们一起学习了如何将GitHub Copilot与Rust生态系统结合使用,以加速开发流程并提升代码质量。我们通过构建一个简单的命令行应用,体验了从项目初始化、依赖管理、代码编写(借助Copilot)、调试到最终编译运行的完整流程。关键在于利用现代工具链(如Cargo、GitHub Codespaces)和AI辅助编程,将已有的编程知识(例如来自Python的经验)高效地应用到Rust开发中,从而获得性能、安全性和开发效率的多重提升。

005:GCP BigQuery SQL提示工程 🚀

在本节课中,我们将学习如何利用提示工程(Prompt Engineering)来辅助编写和优化Google BigQuery中的SQL查询。我们将通过一个具体的例子,展示如何与大型语言模型(如ChatGPT)交互,逐步改进一个查询,使其满足更复杂的需求。

概述

提示工程的核心思想是,通过向大型语言模型提供清晰的指令或示例,引导其生成、解释或修改代码。在数据工程领域,这可以极大地帮助我们理解现有查询、调整查询逻辑,甚至构建更复杂的分析语句。本节我们将以Google BigQuery中的Google Trends数据集为例,演示这一过程。

从解释查询开始

当你面对一个不熟悉的SQL查询时,一个很好的起点是让AI模型为你解释它。这有助于你理解查询的结构和目的。

以下是具体步骤:

  1. 首先,在BigQuery中找到一个你想了解的查询。例如,一个从Google Trends数据集中提取特定日期范围内前100个上升搜索词的查询。
  2. 将该查询的代码复制到ChatGPT等工具中。
  3. 向模型提出解释请求,例如:“请为我解释这个Google BigQuery查询。”

模型会返回查询的详细分解,说明每个部分(如SELECTFROMWHEREGROUP BY)的作用。这是理解复杂查询的快速方法。

修改查询:获取前10条结果

理解了基础查询后,你可能需要对其进行调整。假设我们只需要前10条结果,而不是原来的100条。

以下是具体步骤:

  1. 将原始查询再次提供给模型。
  2. 给出明确的修改指令,例如:“请修改这个查询,只获取前10条结果。”
  3. 模型会生成修改后的代码,通常是将LIMIT子句的值从100改为10。
  4. 将新生成的SQL代码复制回BigQuery并运行,验证结果是否符合预期。

通过这种方式,你可以快速获得一个可工作的代码变体,而无需手动查找语法。

进阶调整:格式化结果

有时,我们需要对查询结果进行格式化。例如,将结果中的所有文本转换为大写字母。

以下是具体步骤:

  1. 基于上一步修改后的查询,继续向模型提出新要求。
  2. 给出指令,例如:“现在我需要所有结果都显示为大写字母。”
  3. 模型会建议使用UPPER()函数来包装相应的列,并生成新的查询代码。
  4. 在BigQuery中测试新查询,确认结果已全部转为大写。

这就像身边有一位数据库专家,可以随时回答你如何实现特定数据转换的问题。

更复杂的转换:替换字符

格式化可以更进一步。例如,我们可能希望将结果中的空格替换为下划线。

以下是具体步骤:

  1. 继续向模型提出请求:“请将结果中的所有空格替换为下划线。”
  2. 模型会建议使用REPLACE()函数,例如 REPLACE(term, ‘ ‘, ‘_’)
  3. 这是一个很好的起点。你可以直接使用模型提供的代码,也可以在此基础上自行调整,比如将下划线换成其他字符。
  4. 运行修改后的查询,验证转换是否成功。

增量构建复杂查询

提示工程最有效的方式之一是进行增量式修改。我们可以在已有查询的基础上,逐步添加新的功能。

上一节我们修改了结果的格式,本节我们来看看如何为查询添加更多数据维度。例如,原始查询可能包含国家代码(country_code),我们希望将对应的国家名称(country_name)也加入结果中。

以下是具体步骤:

  1. 向模型说明需求:“之前的修改很完美。现在我还需要在查询结果中加入country_name列。”
  2. 模型会指导你需要在SELECT子句中添加该列,并在GROUP BY子句中也包含它(如果使用了聚合函数)。
  3. 将整合了所有修改(如前10条、大写转换、空格替换、添加国家名)的新查询代码复制到BigQuery中执行。

最终挑战:添加自定义聚合列

最后,让我们尝试一个更复杂的请求:扩展查询以返回更多结果,并添加一个自定义的计算列。

以下是具体步骤:

  1. 向模型提出综合性请求:“现在,请将查询扩展为返回前50条结果,并添加一个我们自己生成的列。该列用于统计每个结果(搜索词)在不同国家出现的次数,并将这个计数包含在输出中。”
  2. 这是一个相当复杂的自然语言指令。模型需要理解并执行以下操作:将LIMIT改为50,使用COUNT()聚合函数按国家和词条进行计数,并将该计数作为新列输出。
  3. 模型生成的查询可能会使用窗口函数或子查询来实现此计数。将最终代码粘贴到BigQuery中运行。

如果成功,你将得到一个包含国家名称、处理后的搜索词(大写、下划线连接)以及该词条在不同国家出现次数的结果集。这些处理后的数据非常适合为后续的机器学习模型训练做准备。

总结

本节课中,我们一起学习了如何将提示工程应用于Google BigQuery的SQL开发。我们从解释一个现有查询开始,逐步学会了如何修改查询以限制结果数量格式化输出结果(如转大写、替换字符),并通过增量式交互添加了新的数据列。最后,我们尝试了构建一个包含自定义聚合列的复杂查询。整个过程表明,结合BigQuery这样的强大数据平台与提示工程,可以显著提高数据查询和探索的效率,使你能够更灵活地与数据对话。

006:AWS CodeWhisperer Rust功能介绍 🚀

在本节课中,我们将学习如何使用AWS CodeWhisperer这一AI编程工具来辅助Rust开发。我们将了解其基本功能、如何在AWS Cloud9环境中进行设置,并通过一个简单的计算器项目演示如何与AI结对编程,以高效地编写和调试Rust代码。


Amazon CodeWhisperer是一个AI驱动的编程工具,它能以编程方式帮助你构建代码。

我们来看一下它的文档。你可以看到它是一个AI编码伴侣,并且能与IDE集成。它既可以在Visual Studio Code中使用,也可以在AWS Cloud9环境中工作。

首先,让我们看看Cloud9。我喜欢Cloud9的一点是,它可以轻松创建新的、一次性的环境来测试代码。

如果我在这里选择“创建环境”,并命名为“test”,我可以从许多不同的选项中进行选择,包括一些性能强大的机器。例如,这里有一个包含大型机器(甚至裸金属服务器)的庞大列表。如果我想进行实验,我也可以在Amazon Linux上构建环境,这有助于确保我编写的代码能在亚马逊环境中良好运行。

你还可以在这里设置超时时间,这是一种很好的配置方式。你甚至可以通过SSH进入环境进行控制,或者将其放入VPC中。

我已经有一个正在运行的环境,并且已经集成了AWS CodeWhisperer。让我们看看它是如何工作的。

如果我们点击这里的AWS图标,可以看到关于AWS的一些信息,我可以控制不同的服务。我也可以转到“开发者工具”,在这里我已经打开了CodeWhisperer,你可以看到它正在为我的代码提供建议。


让我们来看一个具体的例子。注意,在Rust中,我可以先输入一个注释。在这个例子中,我写道“构建一个打印1+1的函数”,我成功地做到了这一点,然后我添加了这段代码。让我们找到它在哪里,它在hello项目中。让我们进入hello目录。

从这里,如果我输入cargo run,你会看到我能够使用这些建议并修改我的代码。

这里需要指出的一点是,为了让Rust在Cloud9上运行,你需要运行rustup命令。我们按照屏幕上的说明操作,只需复制并粘贴,非常容易设置。它会询问你是否继续安装、自定义安装或取消。我已经完成了这个步骤,所以我会取消,不需要再次操作。


现在,让我们从头开始构建更多内容。我们先向上退一个目录,然后输入cargo new cw_project来创建一个名为“cw_project”的CodeWhisperer项目。

创建完成后,我们进入这个目录。在这里,如果我查看src目录下创建的main.rs文件,这个文件包含了所有入门级的内容,它给了我们一个“Hello, world!”程序。让我们运行cargo run来执行它。

现在,让我们开始使用CodeWhisperer。我要做的是,先构建一个多行Rust注释,这是开始的好方法。我会写:“这是一个充当基本计算器的脚本。”

这是在为我们的程序设置上下文。在使用AI结对编程工具时,首先设置上下文非常重要。

接下来,我将开始引导它。例如,我会输入“使用标准输入输出库”,然后开始编写代码,它会给我们建议。我们不一定非要接受这些建议。实际上,在这个案例中,我不打算接受,我会输入自己的引导。我会说:“创建一个加法函数。”

这完全取决于你如何引导它,就像你与同事协作一样。看,它给出了一个看起来不错的函数。现在,它会完成这个特定函数的最后部分。

完美。从这里,我还可以继续整合。例如,我可以说“将两个数字相加”。我们可以这样做:定义一个变量来存储结果,然后打印出来。注意,它会进行替换以使代码工作。

现在,如果我运行cargo run,我们会执行它。哦,看起来有个问题,它提示“未闭合的分隔符”。实际上,这确实是个问题。

使用像Rust这样强大的语言,结合CodeWhisperer,好处之一就体现出来了。编译器告诉我哪里错了,然后AI结对编程工具帮助我构建代码。它会怎么做?它会补全那个代码块。

现在,让我们再次运行cargo run,它会构建并执行。


这里还有几个有趣的地方值得指出。如果我转到另一个已经设置好的项目,查看其中的Makefile,我注意到我喜欢做代码格式化。cargo fmt的好处是它能帮我整理代码。

所以,如果我们回到这个环境,输入cargo fmt,注意它实际上清理了代码。因此,结合请求构建正确的东西(即设置提示)、逐步引导、然后运行(cargo run)和格式化(cargo fmt),你真正获得了AI结对编程的所有优势,以及编译器带来的好处。


最后,让我们做一件事:扩展功能。如果我输入“创建一个减法函数”,它很聪明,知道我想做什么。它会给出相应的代码。完美。

然后,我们可以添加第二个结果。我们可以定义let res2,然后将其改为result2。接着,我们运行cargo fmt进行格式化,再运行cargo run。看,我们得到了-1和5。

我认为,利用结对编程环境的方法之一就是:在某些情况下,选择确切的部署目标(这里是Cloud9,因为它运行Amazon Linux),请求建议,运行编译器,但同时使用结对编程工具来真正创建一个反馈循环,以获得最佳可能的结果。


总结

本节课中,我们一起学习了AWS CodeWhisperer在Rust开发中的应用。我们了解了如何在AWS Cloud9环境中设置Rust和CodeWhisperer,并通过构建一个基础计算器项目,实践了如何通过注释设置上下文、逐步引导AI生成代码、利用编译器反馈调试,以及使用cargo fmt保持代码整洁。这种结合AI辅助编程与Rust强大编译器的工作流,能有效提升开发效率与代码质量。

007:使用Google Bard提升生产力 🚀

概述

在本节课中,我们将学习如何利用现代AI结对编程工具,特别是Google Bard,来辅助软件开发和学习。我们将通过一个具体的例子,展示如何向Bard提问以获取知识、生成代码,并学习如何调试和迭代AI生成的代码,最终完成一个数据可视化任务。


知识获取:向Bard提问 📚

现代软件工程的一个令人兴奋的方面是能够使用AI结对程序员来帮助构建项目,甚至为认证考试学习。

让我们先来看一个向Bard提问的例子。第一步,假设我正在为Google云平台的认证考试学习,我可以向Bard提问:“Google云平台安全的三个关键方面是什么?”

以下是Bard给出的关键方面:

  • 数据安全
  • 身份和访问管理
  • 合规性

此外,Bard还提供了一些其他功能。这为我如何学习提供了很好的思路。这主要是从知识获取的角度出发。


代码生成:获取编程助手 💻

那么,如果我想进行一些编码工作呢?

我可以向Bard提出更具体的编程请求。例如,我说:“构建一个Python Colab笔记本,用于从pandas导入一个样本数据集并进行图表绘制。”

此时,我可以从Bard那里获得一个入门工具包。我甚至可以直接获得代码。例如,代码可能包含 import pandas as pdimport matplotlib.pyplot as plt 这样的片段。

如果我需要,我还可以将这个响应直接导出到笔记本中。例如,我可以说“导出到Colab笔记本”。这样做的好处是,我既能获得Bard的结对编程协助,又能直接深入到一个示例笔记本中。


实践与调试:迭代完善代码 🔧

上一节我们介绍了如何从Bard获取代码,本节中我们来看看如何运行和调试这些代码。

让我们在Colab中更改运行时类型,并分配更多内存。然后,我们可以尝试运行这个示例。这是快速上手特定库的一个好方法。

然而,我们遇到了一个问题:代码报错,提示某个数据集未找到。像这样的错误很常见。那么,如何解决这个问题呢?

我们可以进行调试。一个方法是打开新标签页搜索正确的数据集加载方法。例如,搜索“iris dataset pandas”,我们可能会找到使用 from sklearn.datasets import load_iris 的正确调用方式。

于是,我回到Bard的提示框,要求它更改代码以使用这个新的导入方式。理解如何迭代是重要的一部分,即要能接受代码可能存在问题的现实。

我们再次导出响应,并在新的Colab笔记本中打开。现在代码变成了 from sklearn.datasets import load_iris。我们更改运行时设置并保存,然后再次运行。

重要的是要明白,你不能指望生成的代码是完美的。你必须具备来回调试的能力。现在,我们又遇到了一个新问题:某个列名可能有问题。


深入调试:检查数据与修正错误 🐛

那么,我们如何修复这个问题呢?其实很简单。

我们可以通过深入代码来调试。通常,我会先尝试单独加载数据,看看是否成功。我新建一个代码单元格,粘贴数据加载部分的代码并运行。这一步成功了。

接着,我在下面再添加一个代码单元格,查看 iris.datairis.feature_names。我们发现,实际的特征名(例如 sepal length (cm))与代码中预期的列名(例如 sepal_length)略有不同。

这个问题很容易修复。我只需要回到绘图代码中,将 sepal_length 改为 sepal length (cm),将 sepal_width 改为 sepal width (cm)

修改之后,我们再次运行代码,现在图表成功显示了。使用结对编程助手确实需要一点耐心,但同时进行编码。

在这个具体的例子中,我们坚持了下来,通过将结对编程工具视为协作者,并深入处理Iris数据和进行可视化,我们获得了相当大的进展。


总结

本节课中,我们一起学习了如何利用Google Bard作为AI结对编程工具来提升生产力。我们从向Bard提问获取知识开始,然后学习了如何让它生成初始代码片段。更重要的是,我们实践了运行、调试和迭代AI生成代码的过程,包括处理导入错误和数据列名不匹配等常见问题。这个过程表明,将AI助手作为协作工具,并结合开发者自身的调试能力,可以高效地完成编程和学习任务。

008:Rust与GitHub Actions持续集成 🚀

在本节课中,我们将学习如何为一个Rust项目设置GitHub Actions持续集成(CI)流程。我们将从创建一个新项目开始,配置必要的构建和测试步骤,并最终在GitHub Actions中自动运行这些步骤,以确保代码质量。


概述

持续集成是DevOps的核心实践之一,它允许我们自动测试和验证代码。通过使用GitHub Actions,我们可以为Rust项目设置一个自动化的构建、代码检查、格式化和测试流程。这有助于在开发早期发现问题,并确保代码库始终保持高质量。


创建新项目与仓库

首先,我们需要创建一个新的Rust项目,并将其设置为使用GitHub Actions进行持续集成。以下是具体步骤。

我们将使用一个项目模板来创建一个新的GitHub仓库。这个模板已经预配置了CI所需的基础结构。

  1. 使用模板创建新仓库。
  2. 将仓库命名为 github-actions-rust-example
  3. 描述为“构建和测试一个Rust项目”。

设置新项目的目标是,从一开始就为其配置持续集成。这是DevOps的最佳实践,能帮助你更快地构建微服务,因为质量控制流程会随着时间的推移不断改进你的代码。


配置开发环境

为了高效工作,我们将在GitHub Codespaces中配置一个强大的Rust开发环境。

在GitHub Codespaces内部,环境已经为我们准备好了Rust工具链。我们可以通过输入 which cargo 来验证。接下来,我们将初始化一个新的Rust项目。

我们将执行以下命令来创建项目结构:

cargo new test_rust

这将创建一个名为 test_rust 的新项目目录。

我们还需要创建一些额外的文件来组织我们的代码和测试。
以下是需要创建的文件列表:

  • src/lib.rs:库文件。
  • Makefile:用于定义构建命令。
  • tests/test_lib.rs:测试文件。

使用 touch 命令创建这些文件:

touch src/lib.rs
mkdir -p tests
touch tests/test_lib.rs

编写与测试代码

现在,项目结构已经搭建完成,我们需要向其中添加一些代码并进行本地测试。

我将从一个现有项目中复制一些示例代码,粘贴到我们的新文件中。

  1. 将代码粘贴到 src/main.rs 中。
  2. 将库代码粘贴到 src/lib.rs 中。
  3. 将测试代码粘贴到 tests/test_lib.rs 中。

粘贴完成后,我们需要对代码做一点小修改,例如将某个结构体或函数重命名以符合当前项目上下文。Rust的工具链非常智能,我们可以使用编辑器的查找替换功能轻松完成这个任务。

现在,我们可以利用 Makefile 来运行各种质量检查命令。Makefile 中预定义了多个任务。

以下是 Makefile 中可用的关键命令:

  • make format:使用 rustfmt 自动格式化代码。
  • make lint:使用 clippy 进行代码检查,发现潜在问题。
  • make test:运行项目中的所有测试。

我们在本地依次运行这些命令,以确保一切正常。例如,运行 make test 后,应该能看到所有测试都成功通过。


配置GitHub Actions工作流

上一节我们验证了代码在本地可以正常工作,本节中我们来看看如何将这些步骤自动化。核心是配置GitHub Actions工作流文件。

GitHub Actions的工作流定义在 .github/workflows 目录下的YAML文件中。我们需要确保工作流中的步骤与我们本地运行的 make 命令一致。

检查现有的工作流文件,我们发现它已经配置了在Ubuntu环境下运行,并会拉取Rust工具链。它计划执行 make lint 等步骤。由于我们已经在本地测试过,可以确信这些步骤会成功。

不过,工作流中有一个步骤调用了 make format-check,而我们的 Makefile 中没有定义这个命令。因此,我们需要将其改为我们已经定义好的 make format

修改完成后,我们就可以提交更改并触发GitHub Actions了。


提交更改并触发CI

所有配置都已完成,现在我们将更改提交到仓库,并观察GitHub Actions的自动执行过程。

首先,我们使用Git命令添加所有更改的文件并提交。

git add .
git commit -m “Ready for continuous integration”
git push

提交信息“Ready for continuous integration”会触发GitHub Actions开始运行。

提交后,我们可以转到GitHub仓库的“Actions”选项卡查看工作流的执行情况。


验证CI结果与创建状态徽章

工作流触发后,GitHub Actions会自动执行我们定义的所有步骤。我们需要查看每个步骤的执行结果是否成功。

在Actions页面,我们可以看到工作流被分解为多个独立的步骤,例如构建二进制文件、代码检查(linting)、代码格式化(formatting)和运行测试(testing)。每个步骤旁边会显示成功(✓)或失败(✗)的状态。

如果所有步骤都显示为成功,则表明我们的持续集成管道配置正确。这是一个重要的里程碑。

为了让项目的质量状态一目了然,我们可以创建一个状态徽章(Status Badge)并添加到项目的README文件中。

在GitHub Actions页面,可以找到“Create status badge”的选项。复制生成的Markdown代码,将其粘贴到项目的 README.md 文件中。例如:

![CI Status](https://github.com/your-username/your-repo/actions/workflows/rust.yml/badge.svg)

我们还可以更新README的标题,例如改为“一个Rust CI管道示例”,并说明本项目使用GitHub Actions测试Rust代码。


总结

本节课中我们一起学习了为Rust项目设置GitHub Actions持续集成的完整流程。

持续集成的核心在于:你可以在本地进行开发,并通过一个统一的命令(如 make all)验证所有构建和测试步骤。然后,这些相同的步骤可以在构建系统(如GitHub Actions、GCP Cloud Run等)中自动复现。最后,通过状态徽章向所有人展示你的代码处于高质量且可用的状态。

这是实现DevOps的基础组件——持续集成,并且设置过程相当简单直接。在此基础上,你可以进一步构建持续交付(CD)流程。

009:Rust序列与映射结构介绍 🦀

在本节课中,我们将要学习Rust中的两种核心集合类型:序列(Sequence)和映射(Map)。我们将探讨它们的基本概念、与Python中类似结构的异同,以及Rust在类型安全和可变性方面的独特设计。

概述

Rust的集合库提供了多种数据结构来存储和操作数据。其中,序列和映射是最常用的两种。它们与Python中的列表(List)和字典(Dictionary)有相似之处,但在可变性、类型安全和迭代顺序等方面存在关键差异。理解这些差异对于编写安全、高效的Rust代码至关重要。

序列(Sequence)

序列类似于Python中的列表。在Rust中,一个常见的序列实现是向量(Vec),它是一种动态数组。

以下是一个创建不可变序列的例子:

let fruits = ["apple", "banana", "cherry"];

这行代码创建了一个名为fruits的不可变数组。这意味着一旦创建,你就无法修改其内容(例如,添加、删除或更改元素)。这是Rust的一项安全特性,旨在防止意外的数据修改。如果你打印这个数组,输出将是:

["apple", "banana", "cherry"]

映射(Map)

映射类似于Python中的字典,是一种键值对(Key-Value)存储结构。Rust标准库中常用的映射是HashMap

默认情况下,Rust中的映射也是不可变的。例如,如果你尝试向一个默认创建的HashMap中插入值,但没有将其声明为可变的(mut),操作将会失败:

use std::collections::HashMap;

let mut scores = HashMap::new(); // 注意这里的 `mut` 关键字
scores.insert(String::from("Apple"), 10);
scores.insert(String::from("Banana"), 8);
scores.insert(String::from("Oranges"), 15);

只有使用mut关键字声明后,scores这个映射才是可变的,才能进行插入操作。打印这个映射,输出类似于键值对的形式:

{"Apple": 10, "Banana": 8, "Oranges": 15}

上一节我们介绍了Rust中序列和映射的基本创建方式,本节中我们来看看它们与Python中对应结构的详细对比。

与Python的对比

Rust的序列和映射与Python的列表和字典在概念上相似,但在实现和行为上有重要区别。

以下是它们之间的一些核心异同点:

  • 数据结构类比

    • Python的列表(List)是动态数组,类似于Rust中的向量(Vec<T>)。
    • Python的字典(Dictionary)和Rust的映射(如HashMap<K, V>)都是键值存储。
  • 访问模式

    • 在Python列表和Rust序列中,元素都通过它们在集合中的位置(索引) 来访问。
  • 重复值

    • Python列表和Rust序列都允许存储重复的值。
    • Python字典和Rust映射都不允许有重复的键。每个键必须是唯一的。
  • 迭代顺序

    • 这是一个重要区别。在较新版本的Python中,字典默认会保持元素的插入顺序。
    • 在Rust的HashMap中,迭代顺序是不保证的,它可能因哈希函数和内部布局而不同。
  • 类型安全

    • 这是使用Rust的一个关键原因。Python的列表和字典是动态类型的,意味着你可以在一个列表中放入字符串、整数、对象等任何类型的数据。
    • Rust的序列和映射是静态类型的。你必须在编译时明确指定它们所能包含的数据类型。例如,一个Vec<i32>只能包含整数,一个HashMap<String, i32>的键必须是字符串,值必须是整数。这为程序提供了强大的类型安全保障,能在编译期捕获许多错误。

总结

本节课中我们一起学习了Rust中两种基础的集合类型:序列和映射。

我们了解到,Rust的序列(如数组和向量)类似于Python的列表,而映射(如HashMap)类似于Python的字典。然而,Rust通过默认的不可变性、不保证顺序的映射迭代以及最重要的静态类型系统,与Python区分开来。这种类型安全特性要求开发者在编码时更明确地定义数据结构,从而在程序运行前就避免了大量的潜在错误,这是Rust追求内存安全和并发安全的核心体现。

理解这些集合的特性和它们与动态语言中对应结构的区别,是掌握Rust编程的重要一步。

010:数据结构打印演示 🖨️

在本节课中,我们将学习如何在控制台中打印Rust的各种数据结构。我们将逐一介绍Rust中流行的数据结构,并通过代码示例展示如何打印它们。涵盖的数据类型包括整数、浮点数、字符串、数组、向量、哈希映射和结构体。

项目初始化与运行

首先,我们使用Cargo创建一个新的Rust项目。在终端中执行以下命令:

cargo new print_datastructs

这将在当前目录下创建一个名为print_datastructs的新项目。进入项目目录后,可以看到默认生成的src/main.rs文件,其中包含一个简单的main函数。

以下是项目的基本结构:

fn main() {
    println!("Hello, world!");
}

为了运行这个项目,我们使用cargo run命令。在项目根目录下执行:

cargo run

这将编译并运行项目,在控制台输出“Hello, world!”。

常用Rust集合概览

在深入打印具体数据结构之前,我们先简要了解Rust中常用的集合类型。根据官方文档,这些集合可以分为以下几类:

  • 序列Vec(向量)、VecDeque(双端队列)、LinkedList(链表)
  • 映射HashMap(哈希映射)、BTreeMap(B树映射)
  • 集合HashSet(哈希集合)、BTreeSet(B树集合)
  • 其他BinaryHeap(二叉堆)

这些集合类型将在后续的演示中逐一介绍。

使用Makefile简化开发流程

在Rust项目开发中,使用Makefile可以简化常见的开发命令,如格式化、代码检查和运行。以下是一个示例Makefile的内容:

format:
    cargo fmt

lint:
    cargo clippy

test:
    cargo test

run:
    cargo run

将上述内容保存为项目根目录下的Makefile文件后,可以通过以下命令执行相应操作:

  • make format:使用rustfmt工具格式化代码。
  • make lint:使用clippy工具进行代码检查,发现潜在问题。
  • make test:运行项目中的所有测试。
  • make run:编译并运行项目。

例如,如果代码中有多余的空格,执行make format可以自动清理并格式化代码。同样,执行make lint可以帮助发现代码中的常见错误或不良实践。

打印基本数据类型

上一节我们介绍了项目设置和开发工具,本节中我们来看看如何打印Rust的基本数据类型。以下是几种基本数据类型的打印示例:

fn main() {
    // 整数
    let integer: i32 = 42;
    println!("整数: {}", integer);

    // 浮点数
    let float: f64 = 3.14;
    println!("浮点数: {}", float);

    // 字符串
    let string: &str = "Hello, Rust!";
    println!("字符串: {}", string);
}

执行上述代码,将在控制台输出相应的值。

打印集合类型

接下来,我们学习如何打印Rust中的集合类型。以下是几种常见集合的打印示例:

use std::collections::{HashMap, HashSet, VecDeque, BinaryHeap};

fn main() {
    // 数组
    let array: [i32; 3] = [1, 2, 3];
    println!("数组: {:?}", array);

    // 向量
    let vector: Vec<i32> = vec![4, 5, 6];
    println!("向量: {:?}", vector);

    // 哈希映射
    let mut hash_map = HashMap::new();
    hash_map.insert("key1", "value1");
    hash_map.insert("key2", "value2");
    println!("哈希映射: {:?}", hash_map);

    // 哈希集合
    let hash_set: HashSet<i32> = [7, 8, 9].iter().cloned().collect();
    println!("哈希集合: {:?}", hash_set);
}

注意,打印集合类型时使用了{:?}格式化占位符,这是因为这些类型实现了Debug trait,允许以调试格式输出。

打印结构体

最后,我们看看如何打印自定义的结构体。为了使结构体能够打印,需要为其派生Debug trait。以下是一个示例:

#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let person = Person {
        name: String::from("Alice"),
        age: 30,
    };
    println!("结构体: {:?}", person);
}

通过#[derive(Debug)]属性,我们为Person结构体自动实现了Debug trait,从而可以使用{:?}占位符打印其内容。

总结

本节课中我们一起学习了如何在Rust中打印各种数据结构。我们从项目初始化开始,介绍了使用Cargo创建和运行项目的基本步骤。然后,我们探讨了如何使用Makefile简化开发流程,包括代码格式化、检查和运行。接着,我们通过示例代码演示了如何打印基本数据类型、集合类型以及自定义结构体。掌握这些打印技巧对于调试和理解Rust程序中的数据流非常有帮助。

011:向量数据结构演示 🍇

在本节课中,我们将学习如何使用Rust中的向量数据结构。向量是Rust中最常用、最灵活的数据结构之一,类似于Python中的列表。我们将通过一个“水果沙拉”的示例程序,演示如何创建向量、动态添加/移除元素、访问元素,并展示其核心特性和优势。

概述

向量是一个可增长的数组,其大小可以在运行时动态调整。它是Rust标准库提供的一个核心集合类型,对于处理一系列同类型数据非常有用。本节我们将通过一个具体的例子,直观地理解向量的基本操作。

代码结构与依赖

首先,我们来看示例程序的基本结构和外部依赖。这个程序使用了rand库来生成随机数,以实现对水果列表的随机打乱。

在Rust项目中,如果需要使用标准库之外的功能,我们需要在Cargo.toml文件中声明依赖。以下是本示例的依赖项:

[dependencies]
rand = "0.8"

创建与初始化向量

上一节我们介绍了程序的基本结构,本节中我们来看看如何创建和初始化一个向量。在Rust中,使用vec!宏可以方便地创建一个包含初始元素的向量。

main函数中,我们定义了一个可变的字符串向量来存放各种水果:

let mut fruit = vec!["Orange", "Fig", "Pomegranate", "Cherry", "Apple", "Pear", "Peach"];

这里的关键点:

  • let mut 声明了一个可变变量fruit,这意味着我们后续可以修改这个向量。
  • vec! 是一个宏,用于便捷地初始化向量。
  • 如果不需要修改向量,可以声明为let fruit,使其成为不可变的。

操作向量元素

创建向量后,我们可以对其进行各种操作。以下是本示例中演示的几个核心操作:

打乱向量顺序
我们使用rand库中的shuffle方法来随机打乱向量中元素的顺序。这体现了向量内容可以被修改的特性。

fruit.shuffle(&mut rng);

遍历与访问元素
为了打印出所有水果,我们需要遍历向量。Rust的向量是可迭代的,可以使用iter()方法获取迭代器,并结合enumerate()同时获得元素的索引和值。

for (i, item) in fruit.iter().enumerate() {
    println!("{}: {}", i + 1, item);
}

运行与工具使用

现在,让我们运行这个程序并介绍一些常用的Rust开发工具。

要编译并运行程序,只需在项目根目录下执行:

cargo run

Cargo会处理依赖、编译代码并运行生成的可执行文件,输出随机打乱后的水果沙拉列表。

此外,保持代码整洁规范很重要,Rust提供了强大的工具链:

代码格式化
使用rustfmt工具可以自动格式化代码,使其符合Rust风格指南。可以通过Cargo执行:

cargo fmt

代码检查
clippy是一个静态分析工具,能发现代码中的常见错误并提出改进建议。运行命令如下:

cargo clippy

如果代码没有问题,它将不会输出任何警告或错误。

总结

本节课中我们一起学习了Rust中向量数据结构的基本用法。我们通过“水果沙拉”程序,实践了如何:

  1. 使用vec!宏创建和初始化向量。
  2. 通过let mut声明可变向量以修改其内容。
  3. 使用shuffle方法打乱向量顺序。
  4. 使用iter()enumerate()遍历并访问向量元素。
  5. 使用cargo run运行程序,并用cargo fmtcargo clippy来格式化和检查代码。

向量是解决许多编程问题的首选数据结构,掌握其用法是学习Rust的重要一步。其语法和概念对于有Python等语言经验的开发者来说也相当直观。

012:VecDeque水果沙拉演示 🍇

在本节中,我们将学习Rust标准库中的VecDeque(双端队列)数据结构。我们将通过一个制作“水果沙拉”的示例程序,来理解VecDeque的基本操作,包括从队列两端高效地添加元素,以及如何与普通向量(Vec)进行转换和配合使用。


概述

VecDeque是一个双端队列,类似于Python collections模块中的deque。它的一个显著优点是,在队列的前端后端进行添加或删除操作时,都具有O(1)的时间复杂度。这意味着无论从哪一端操作,效率都非常高。这种特性可以为你的代码引入新的、非常有用的编程模式。

接下来,让我们详细解析示例代码。该代码的流程是:首先初始化一个VecDeque,将其转换为Vec以便进行随机打乱(shuffle),然后再转换回VecDeque。随后,它将石榴(pomegranate)添加到队列前端,将无花果(fig)和樱桃(cherry)添加到队列后端。最后,它打印出最终的水果沙拉列表。


代码解析与步骤

以下是实现上述逻辑的Rust代码核心步骤。

首先,我们需要引入必要的依赖。代码中使用了rand库来提供随机性,用于打乱水果的顺序。

# Cargo.toml 依赖项
[dependencies]
rand = "0.8"

main函数中,我们开始构建水果沙拉。

第一步:创建并初始化一个可变的VecDeque

我们创建一个VecDeque<String>,并向其后端依次添加三种水果。

use std::collections::VecDeque;

let mut fruit: VecDeque<String> = VecDeque::new();
fruit.push_back("Apple".to_string());
fruit.push_back("Banana".to_string());
fruit.push_back("Strawberry".to_string());
  • push_back方法将元素添加到队列的末尾。

第二步:将VecDeque转换为Vec并进行打乱。

为了随机打乱水果的顺序,我们需要先将VecDeque转换为普通的Vec,因为Vec有更丰富的切片操作方法。

use rand::seq::SliceRandom;
use rand::thread_rng;

// 将 VecDeque 转换为 Vec
let mut fruit_vec: Vec<String> = fruit.into_iter().collect();

// 使用随机数生成器打乱 Vec 中的元素顺序
let mut rng = thread_rng();
fruit_vec.shuffle(&mut rng);
  • into_iter().collect()是一个常见的模式,用于将一种集合类型转换为另一种。
  • shuffle方法来自rand::seq::SliceRandom trait,它随机重新排列切片中的元素。

第三步:将打乱后的Vec转换回VecDeque

打乱完成后,我们再将Vec转换回VecDeque,以便后续进行双端操作。

// 将打乱后的 Vec 转换回 VecDeque
let mut fruit: VecDeque<String> = fruit_vec.into_iter().collect();

第四步:向VecDeque的前端和后端添加新水果。

现在,我们演示VecDeque的双端操作能力。将石榴添加到队列前端,将无花果和樱桃添加到队列后端

fruit.push_front("Pomegranate".to_string()); // 添加到前端
fruit.push_back("Fig".to_string());         // 添加到后端
fruit.push_back("Cherry".to_string());      // 添加到后端
  • push_front方法将元素添加到队列的开头,这正是VecDeque相比普通Vec的优势所在(Vec在头部插入是O(n)操作)。

第五步:打印最终的水果沙拉。

最后,我们遍历并打印最终队列中的所有水果。

println!("Final Fruit Salad:");
for (i, item) in fruit.iter().enumerate() {
    println!("{}: {}", i + 1, item);
}
  • iter().enumerate()会同时产生元素的索引(i)和值(item)。

运行结果

运行程序(cargo run),每次输出可能类似如下(因为包含随机打乱):

Final Fruit Salad:
1: Pomegranate
2: Banana
3: Strawberry
4: Apple
5: Fig
6: Cherry

再次运行,由于打乱顺序不同,输出也会变化:

Final Fruit Salad:
1: Pomegranate
2: Apple
3: Strawberry
4: Banana
5: Fig
6: Cherry

总结

本节课我们一起学习了Rust中的VecDeque双端队列。我们通过一个制作水果沙拉的生动例子,掌握了以下核心操作:

  1. 使用push_backpush_front在队列两端高效添加元素。
  2. 利用into_iter().collect()VecDequeVec之间进行转换。
  3. 结合rand库对集合中的元素进行随机打乱。

VecDeque在需要频繁在序列头部和尾部进行操作的场景下(如实现缓存、任务队列等)非常有用,它能提供比普通Vec更优的性能。你可以根据实际需求,在此基础上添加更复杂的逻辑或优化。

013:链表水果沙拉演示 🥗

在本节课中,我们将学习Rust标准库中的LinkedList数据结构。我们将通过一个制作“水果沙拉”的示例,来了解链表的基本操作、其特性以及适用场景。

概述

链表是一种线性数据结构,由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针。与Vec(动态数组)和VecDeque(双端队列)相比,链表在中间插入或删除元素时可能更高效,但它也带来了更高的内存开销和更差的缓存局部性。

链表特性分析

上一节我们介绍了链表的基本概念,本节中我们来看看它的核心特性。

链表的一个优点是,在不关心随机访问元素,而只关注从列表中间插入或删除元素时,它能提供关键的性能效率。

然而,在实际应用中,链表是一种使用频率相对较低的数据结构。与VecVecDeque相比,它具有更高的内存开销更差的缓存局部性

代码示例:水果沙拉

现在,让我们通过一个具体的代码示例,来看看如何在Rust中使用LinkedList。这个例子与之前VecDeque的例子非常相似。

以下是创建、修改和遍历链表的步骤:

  1. 创建并填充链表:首先,我们创建一个LinkedList,并将一系列水果名称添加到链表的尾部。

    use std::collections::LinkedList;
    let mut fruit_list = LinkedList::new();
    fruit_list.push_back("apple");
    fruit_list.push_back("banana");
    fruit_list.push_back("cherry");
    
  2. 转换为Vec并打乱:为了与之前的示例保持一致,这里做了一个不常见的操作:将链表转换回Vec以便打乱顺序。在实际开发中,可能不会这样做。

    let mut fruit_vec: Vec<_> = fruit_list.into_iter().collect();
    // 使用rand库打乱fruit_vec的顺序
    // ... shuffle操作 ...
    
  3. 移回链表并添加元素:将打乱后的Vec重新转换为LinkedList。然后,可以向链表的头部和尾部添加新的水果。

    fruit_list = fruit_vec.into_iter().collect();
    fruit_list.push_front("dragonfruit"); // 添加到头部
    fruit_list.push_back("elderberry");   // 添加到尾部
    
  4. 遍历并打印:最后,遍历链表并打印出所有水果,制作成我们的“水果沙拉”。

    for fruit in &fruit_list {
        println!("{}", fruit);
    }
    

运行cargo run后,我们将看到与使用Vec时非常相似的操作结果,一系列水果被打印出来。

总结

本节课中我们一起学习了LinkedList的使用。我们了解到,链表是一种具有特定属性的专用数据结构,在需要频繁从中间插入或删除元素的场景下可能有用。但在大多数情况下,使用VecVecDeque通常是更优的选择,因为它们能提供更好的内存布局和缓存性能。

014:水果沙拉命令行工具演示 🍇

在本节课中,我们将学习如何使用Rust构建一个简单的命令行工具。我们将通过一个名为“水果沙拉”的示例项目,演示如何组织代码、使用外部库解析命令行参数,并最终生成一个可执行的二进制程序。

项目结构概述

首先,我们来看一下这个Rust命令行工具的项目结构。它与Python的argparse模块非常相似。使用命令行界面可以让我们在构建应用时获得更大的灵活性。

项目默认包含src目录,其中有一个库文件lib.rs和一个主程序文件main.rs。在Rust中,如果你想将一些代码组织成库,只需在src目录下创建一个lib.rs文件即可。

库文件 (lib.rs) 解析

上一节我们介绍了项目结构,本节中我们来看看库文件lib.rs的具体实现。这个文件包含了我们工具的核心逻辑。

pub fn create_fruit_salad() -> Vec<String> {
    let mut fruits = vec![
        "apple".to_string(),
        "banana".to_string(),
        "cherry".to_string(),
        "date".to_string(),
        "elderberry".to_string(),
    ];
    fruits.shuffle(&mut rand::thread_rng());
    fruits
}

在这段代码中,我们公开(pub)了一个名为create_fruit_salad的函数。该函数创建一个包含多种水果名称的字符串向量(Vec<String>)。随后,它使用shuffle方法随机打乱向量中元素的顺序,使得每次调用都能获得一个独特的水果沙拉组合。

主程序 (main.rs) 与命令行解析

了解了核心逻辑后,我们现在转向主程序文件main.rs。这里我们将集成命令行参数解析功能。

在文件的顶部,我们引入了clap库,它类似于Python的argparse。同时,我们使用use ci_salad::create_fruit_salad;语句来引入我们刚刚在库中定义的函数。

这个命名空间ci_salad来自哪里?它来自于项目Cargo.toml文件中定义的包名。Rust会自动将包名中的连字符转换为下划线,因此ci-salad在代码中就变成了ci_salad。这是一种非常便捷的模式,能节省大量时间。

以下是main.rs中命令行设置的核心部分:

use clap::Parser;

#[derive(Parser)]
#[command(version, author, about)]
struct Args {
    #[arg(short, long, default_value_t = 5)]
    number: usize,
}

fn main() {
    let args = Args::parse();
    let num_fruits = args.number;
    // ... 调用 create_fruit_salad 并根据 num_fruits 处理逻辑
}

我们定义了一个Args结构体,并使用clap的宏为其派生Parser特性。我们为它添加了版本、作者和描述信息。结构体中有一个number字段,用户可以通过-n--number来指定所需的水果数量,默认值为5。

工具使用演示

现在,让我们将所有这些部分结合起来,看看如何实际运行这个工具。

首先,我们需要在项目根目录下执行命令。一个关键点是,当使用cargo run并希望将参数传递给我们的命令行工具时,必须在工具参数前使用双连字符--

以下是运行示例:

运行帮助命令查看用法:

cargo run -- --help

创建一个包含5种水果的沙拉:

cargo run -- --number 5

创建一个包含10种水果的沙拉:

cargo run -- --number 10

执行后,程序会根据用户请求的数量生成相应大小的随机水果沙拉列表。

优势总结

本节课中我们一起学习了如何使用Rust和clap库构建命令行工具。通过“水果沙拉”这个简单示例,我们掌握了:

  1. 代码组织:如何将核心逻辑分离到lib.rs库文件中。
  2. 依赖管理:如何在Cargo.toml中声明依赖(如clap)。
  3. 参数解析:如何使用clap定义和解析命令行参数。
  4. 项目构建与运行:如何使用cargo run --来运行并传递参数。

这种方式的巨大优势在于,一旦你习惯了用Rust编写脚本,就可以轻松地使用clap来扩展功能。更重要的是,你可以将项目编译成独立的二进制文件进行部署,轻松地将你的命令行工具分享给他人使用。🚀

015:哈希映射频率计数器演示 🧮

在本节课中,我们将学习如何使用Rust中的哈希映射(HashMap)来构建一个频率计数器。这是一种非常实用的数据结构,常用于统计元素出现的次数。

哈希映射与Python中的字典非常相似,它对于插入、删除和访问操作都具有O(1)的时间复杂度,因此性能优异。它特别适合需要快速查找的任务,例如统计项目中各项的出现频率。

代码结构与逻辑

接下来,我们通过一个具体的代码示例来演示其用法。在main函数中,我们定义了一个数字向量,并调用了一个名为logic的函数来处理它。

以下是logic函数的核心逻辑:

let mut frequencies = HashMap::new();
for &num in &numbers {
    let count = frequencies.entry(num).or_insert(0);
    *count += 1;
}

这段代码遍历输入的数字向量,并使用哈希映射的entryAPI来计数。or_insert(0)方法确保如果键不存在,则将其值初始化为0,然后我们对计数进行递增。

为了便于输出结果,函数最后将哈希映射转换成了一个元组向量并返回。

主函数与数据

在主函数中,我们创建了一个包含一些重复项的数字列表,用于验证计数器的正确性。

let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 3];

然后,我们调用logic函数并打印出每个数字的频率。清晰的输出信息有助于理解程序的功能。

运行结果与分析

运行程序(cargo run)后,控制台会输出类似以下的结果:

The frequency of each number in the vector is:
1 occurs 2 times
2 occurs 1 time
3 occurs 2 times
...

从结果中可以看到,数字1和3各出现了两次,而其他数字均出现一次,这与我们的输入数据完全吻合,证明了频率计数器的正确性。

总结与核心要点

本节课中,我们一起学习了Rust哈希映射在构建频率计数器中的应用。

  • 核心数据结构:哈希映射(HashMap)是Rust中用于快速键值查找的通用数据结构,其时间复杂度为O(1)
  • 常用APIentry(key).or_insert(default_value)是进行计数或初始化操作的惯用模式。
  • 代码组织:将核心逻辑封装在独立的函数中(如本例的logic函数),可以使main函数保持清晰易读。
  • 实用价值:结合向量(Vec)和哈希映射,你就能在Rust中高效处理各种各样的数据任务,这与Python中使用列表和字典的思路非常相似。

哈希映射因其出色的性能和灵活性,是Rust编程中需要重点掌握的数据结构之一。

016:哈希映射语言对比演示

概述

在本节课中,我们将学习如何使用Rust的哈希映射(HashMap)来创建一个自定义的编程语言权重计算器。这个例子非常适合数据科学家或数据工程师,他们经常需要创建自定义指标,这些指标可以输入到仪表板或数据库中。我们将通过计算编程语言的活跃年限来为其分配权重,从而展示Rust在数据处理方面的简洁性和高效性。


创建语言哈希映射

首先,我们创建一个名为 knit_languages 的函数。在这个函数内部,我们初始化一个可变的哈希映射。这个映射将编程语言名称(字符串)与其首次出现的年份(i32整数)关联起来。

以下是该函数的代码实现:

fn knit_languages() -> HashMap<String, i32> {
    let mut languages = HashMap::new();
    languages.insert("JavaScript".to_string(), 1995);
    languages.insert("HTML".to_string(), 1993);
    languages.insert("Python".to_string(), 1991);
    languages.insert("SQL".to_string(), 1974);
    languages.insert("TypeScript".to_string(), 2012);
    languages.insert("Bash".to_string(), 1989);
    languages.insert("Java".to_string(), 1995);
    languages.insert("C#".to_string(), 2000);
    languages.insert("R".to_string(), 1993);
    languages.insert("C++".to_string(), 1985);
    languages.insert("C".to_string(), 1972);
    languages.insert("Rust".to_string(), 2010);
    languages
}

计算语言权重

上一节我们创建了包含语言和年份的映射,本节中我们来看看如何根据语言的活跃年限计算其权重。

我们创建一个名为 calculate_weights 的函数。它接收语言的哈希映射作为参数,并根据每种语言的“年龄”(当前年份减去其诞生年份)计算一个从1到100的权重值。最新的语言权重为1,最古老的语言权重为100。

以下是权重计算的逻辑:

fn calculate_weights(languages: HashMap<String, i32>) -> HashMap<String, i32> {
    let current_year = 2023; // 假设当前年份
    let mut weighted_languages = HashMap::new();

    for (lang, year) in languages {
        let age = current_year - year;
        // 将年龄映射到1-100的区间,这里使用简化线性映射
        // 注意:实际映射逻辑可能需要根据最大和最小年龄调整
        let weight = (age as f32 / 100.0 * 99.0 + 1.0) as i32;
        weighted_languages.insert(lang, weight);
    }
    weighted_languages
}

运行与结果分析

现在,我们将两个函数结合起来,运行程序并查看结果。

在主函数中,我们首先调用 knit_languages 获取基础数据,然后将其传递给 calculate_weights 函数进行计算。最后,我们打印出带权重的语言列表。

运行程序(cargo run)后,我们可以看到类似以下的输出:

TypeScript: 1
Rust: 2
C#: 30
Java: 28
JavaScript: 28
Python: 32
HTML: 30
Bash: 34
R: 30
C++: 38
SQL: 49
C: 51

根据这个权重排名(1代表最新,100代表最古老),我们可以得出一些观察:

  • TypeScript和Rust是相对较新的语言。
  • C语言是最古老的语言之一。
  • 这种排名方式为技术选型提供了另一个维度:追求稳定可选择Python等历史悠久的语言;从事前沿项目可考虑Rust或TypeScript;而像C#这样排名居中的语言则是一个折中的选择。

总结

本节课中我们一起学习了如何利用Rust的哈希映射进行数据处理。我们创建了一个编程语言年限与权重的映射,并演示了如何根据自定义逻辑(活跃年限)计算和排序数据。这个例子虽然简单,但清晰地展示了Rust在构建高效、自定义数据管道方面的能力,这正是数据工程和DevOps工作中的常见任务。得益于Rust的编译特性和高效的哈希映射实现,此类计算性能表现卓越。

017:使用图中心性分析UFC选手网络 🥊

在本节课中,我们将学习如何使用Rust的petgraph库进行图网络分析。具体来说,我们将通过分析UFC选手之间的对战关系,来计算并理解“接近中心性”这一图度量指标。这是一种衡量网络中节点(此处指选手)与其他所有节点平均距离的方法。

概述

许多数据科学从业者都熟悉描述性统计,例如计算中位数、平均值或识别异常值。对于图数据结构,我们也可以进行类似的分析,但使用的是完全不同的度量标准。本节我们将重点探讨“中心性”,特别是“接近中心性”。它衡量的是一个节点到网络中所有其他节点的平均距离。在本例中,我们将以UFC选手对战网络为例,这项分析能够揭示一名选手与其他选手的交战紧密程度。

这里的“距离”指的是两个节点之间的最短路径。在我们的上下文中,即两名选手之间需要经过的最少对战场次。接下来,让我们先看看将要使用的库。

引入库与数据结构

我们首先需要使用petgraph这个图处理库。在Cargo.toml文件中,可以看到已经添加了该依赖。

现在,让我们查看代码。首先定义了一个结构体。初次看到结构体及其实现可能会有些困惑,但它实际上与以下Python代码的功能是相同的:在Python中为对象添加name属性,在这里我们为选手添加fighter_name属性。此外,我们还实现了一些用于显示的功能。

use petgraph::graph::{NodeIndex, UnGraph};
use std::collections::HashMap;

// 定义表示选手的节点数据
#[derive(Debug)]
struct Fighter {
    name: String,
}

impl Fighter {
    fn new(name: &str) -> Self {
        Fighter {
            name: name.to_string(),
        }
    }
}

构建对战关系图

上一节我们定义了选手节点,本节中我们来看看如何构建他们之间的对战关系图。以下是添加边(即对战关系)的代码逻辑。

// 辅助函数:添加边到图中
fn add_edge(graph: &mut UnGraph<Fighter, ()>, nodes: &HashMap<String, NodeIndex>, a: &str, b: &str) {
    let a_index = nodes[a];
    let b_index = nodes[b];
    graph.add_edge(a_index, b_index, ());
}

然后,我们在主函数中将所有部分组合起来。首先创建一个可变的图数据结构,然后添加选手节点。

fn main() {
    // 创建一个无向图
    let mut graph = UnGraph::<Fighter, ()>::new_undirected();
    let mut nodes = HashMap::new();

    // 定义一组知名的UFC选手
    let fighters = vec![
        "Dustin Poirier",
        "Khabib Nurmagomedov",
        "Jose Aldo",
        "Conor McGregor",
        "Nate Diaz",
    ];

    // 将选手作为节点加入图中,并记录其索引
    for fighter in &fighters {
        let node_index = graph.add_node(Fighter::new(fighter));
        nodes.insert(fighter.to_string(), node_index);
    }

定义对战关系

节点添加完毕后,我们需要定义他们之间的对战关系。这类似于社交媒体或真实世界关系中的任何网络分析。

以下是具体的对战关系,我们将它们作为边添加到图中:

    // 根据真实对战历史添加边(关系)
    add_edge(&mut graph, &nodes, "Dustin Poirier", "Khabib Nurmagomedov");
    add_edge(&mut graph, &nodes, "Khabib Nurmagomedov", "Conor McGregor");
    add_edge(&mut graph, &nodes, "Conor McGregor", "Dustin Poirier");
    add_edge(&mut graph, &nodes, "Conor McGregor", "Jose Aldo");
    add_edge(&mut graph, &nodes, "Conor McGregor", "Nate Diaz");
    add_edge(&mut graph, &nodes, "Dustin Poirier", "Nate Diaz");
    add_edge(&mut graph, &nodes, "Jose Aldo", "Nate Diaz");

计算与输出中心性

关系定义好后,接下来的代码会计算每位选手的度、接近中心性并打印结果。作为一名数据工程师、机器学习工程师或数据科学家,创建此类自定义度量或图表是非常常见的。

我们特别匹配了选手“Conor McGregor”,可以看到他的中心性分数最低,因为他与网络中所有其他选手都有过对战。

    // 计算并打印每位选手的接近中心性
    for fighter in &fighters {
        let index = nodes[*fighter];
        // 此处应使用图算法库计算接近中心性,为演示我们使用模拟值
        // 实际项目中,您需要使用`petgraph::algo`或类似库进行计算
        let closeness_centrality = match *fighter {
            "Conor McGregor" => 0.25,
            "Dustin Poirier" => 0.33,
            "Nate Diaz" => 0.33,
            "Khabib Nurmagomedov" => 0.5,
            "Jose Aldo" => 0.5,
            _ => 0.0,
        };

        println!("{}: {:.2}", fighter, closeness_centrality);

        // 根据中心性分数提供解释
        match *fighter {
            "Conor McGregor" => println!("  -> Conor McGregor has the lowest centrality score, indicating he has fought all other fighters in this network."),
            "Khabib Nurmagomedov" | "Jose Aldo" => println!("  -> {} has a higher score, indicating fewer direct fight connections within this group.", fighter),
            _ => println!("  -> {} has an intermediate score.", fighter),
        }
    }
}

运行结果与分析

现在,让我们运行程序查看结果。在终端输入cargo run,程序将输出计算结果。

从输出中我们可以看到:

  • Dustin Poirier的接近中心性是0.33。
  • 中心性最低(即最佳,表示交战范围最广)的是Conor McGregor,分数为0.25。分数越低,意味着该选手与网络中其他选手的平均距离越短,在本例中即直接对战过的对手越多。
  • 分数最差(即最高)的是Khabib Nurmagomedov和Jose Aldo,均为0.5,这大约是Conor McGregor分数的两倍,表明他们在此特定关系网络中的直接连接相对较少。
  • Dustin Poirier和Nate Diaz并列第二。

这种方法非常适用于计算那些对内部关系、体育关系或社交网络真正独特的自定义指标。

总结

本节课中我们一起学习了如何利用Rust的petgraph库进行图网络分析。我们以UFC选手对战网络为案例,定义了节点和边,并理解了“接近中心性”这一概念——它衡量的是节点到网络中所有其他节点的平均距离。通过这个案例,我们看到了如何将图论度量应用于实际关系数据,从而得出有意义的洞察。当然,使用Rust可以轻松高效地完成这类分析。

018:使用HashSet存储唯一水果

在本节课中,我们将学习如何使用Rust标准库中的HashSet来存储唯一元素。我们将通过一个生成随机水果列表并计算其中唯一水果数量的程序来演示其用法。


概述

我们将创建一个Rust程序,该程序会生成一个包含100个随机水果的列表。由于水果是从一个固定的列表中随机选择的,因此列表中很可能包含重复项。我们的目标是利用HashSet的特性,自动过滤掉重复项,从而计算出最终生成了多少种不同的水果。

上一节我们介绍了Rust的基本数据结构,本节中我们来看看如何使用HashSet来处理唯一性数据。

引入外部依赖

程序使用了rand外部crate来生成随机数。具体来说,SliceRandomthread_rng这两个工具帮助我们从一个水果列表中随机选择元素。

以下是引入依赖和定义水果列表的代码:

use rand::seq::SliceRandom;
use rand::thread_rng;
use std::collections::HashSet;

fn main() {
    let fruits = vec![
        "apple", "banana", "orange", "grape", "kiwi",
        "mango", "pineapple", "strawberry", "blueberry", "peach",
    ];
    // ... 后续代码
}

理解HashSet

HashSet是Rust标准库collections模块中的一个集合类型。它的一个关键特性是只存储唯一的元素。当我们向HashSet中插入一个已经存在的元素时,插入操作不会产生任何效果。这使得它非常适合用于需要去重或检查唯一性的场景。

在程序中,我们声明一个HashSet来存储出现过的水果:

let mut fruit_set = HashSet::new();

生成随机水果列表

接下来,我们需要生成指定数量的随机水果。以下是实现这一功能的核心循环:

for _ in 0..100 {
    let fruit = fruits.choose(&mut rng).unwrap();
    fruit_set.insert(fruit);
}

这段代码循环100次。在每次循环中:

  1. 使用choose方法从fruits列表中随机选取一个水果。
  2. 使用insert方法将这个水果放入fruit_set中。如果水果已存在,HashSet会自动忽略。

运行程序并分析结果

现在,让我们运行程序并查看结果。在终端中输入cargo run

程序输出可能类似于:

生成了100个随机水果。
生成的唯一水果数量是:8。

关键点:即使我们生成了100个水果,唯一水果的数量也可能远少于100。这是因为随机选择过程允许同一水果被多次选中,而HashSet确保了每个种类只被计数一次。

为了更直观地理解随机性,我们可以将生成数量从100改为10,并多次运行程序:

for _ in 0..10 { // 将循环次数改为10
    // ... 生成水果
}

多次运行可能会得到不同的结果,例如6、4或更少。这证明了在少量尝试中,由于随机性,很难覆盖列表中的所有水果。即使生成了10次,也可能只得到4种不同的水果。

实际应用场景

掌握HashSet和随机数生成在数据工程等领域非常实用。例如,你可以用类似的方法:

  • 从日志文件中找出唯一的用户ID。
  • 在数据清洗过程中去除重复的记录。
  • 为仪表盘或分析工具生成去重后的数据视图。

Rust作为一门编译型语言,其强大的类型安全和性能特性,使得处理这类任务既安全又高效。


总结

本节课中我们一起学习了如何使用Rust的HashSet来存储唯一值。我们通过一个生成随机水果的示例程序,演示了如何结合rand crate生成随机数据,并利用HashSet自动去重的特性来统计唯一项的数量。你学会了HashSet的基本操作,并理解了随机性对结果的影响。这些是构建更复杂数据处理工具的基础技能。

019:使用BTreeSet维护有序唯一水果 🍎🍌🍇

在本节课中,我们将学习Rust标准库中的另一个有趣的数据结构——BTreeSet。我们将通过一个生成随机水果集合的示例程序,来理解BTreeSet如何维护元素的唯一性和排序。

概述

BTreeSet是一种集合数据结构,它与HashSet类似,都能保证元素的唯一性。但BTreeSet的独特之处在于,它会自动将元素保持在一个有序的状态。这对于某些数据工程问题,例如收集自定义指标或生成有序报告,非常有帮助。接下来,我们将通过一个具体的例子来探索它的用法。

项目依赖与配置

首先,我们需要了解程序使用了哪些外部依赖。在Rust项目中,这非常简单,只需查看Cargo.toml文件即可。

以下是本程序使用的核心依赖项:

  • rand:用于生成随机数。
  • randthread_rngSliceRandom特性:用于从列表中随机选择元素。

定义数据

程序的核心是两组数据:水果名称和数量。我们使用Vec(向量)来存储它们,这是Rust中一个灵活的动态数组。

以下是程序中定义的数据:

  • fruits:一个包含多种水果名称的字符串向量。
  • amounts:一个包含不同数量的整数向量。

生成随机水果集合

现在,我们进入程序的主要逻辑部分。目标是生成多个随机的水果集合,每个集合包含不同数量的、不重复的水果,并且这些水果是按字母顺序排列的。

以下是生成随机有序水果集合的步骤:

  1. 导入必要的随机数生成器。
  2. 将水果列表随机打乱。
  3. 根据amounts中指定的数量,从打乱后的列表中取出前N个水果。
  4. 将这些水果放入一个BTreeSet中。BTreeSet会自动去重并排序。
  5. 打印出生成的有序唯一水果集合。

运行结果分析

运行程序(cargo run)后,我们可以看到类似以下的输出:

第一组:{“无花果”}
第二组:{“香蕉”, “樱桃”, “枣”, “接骨木莓”, “无花果”}
第三组:{“苹果”, “香蕉”, “枣”, “接骨木莓”, “无花果”, “葡萄”, “蜜瓜”}

从输出可以看出,BTreeSet成功地为每一组随机选择的水果创建了唯一的集合,并且集合内的元素是按照字母顺序(字符串的默认排序)有序排列的。

总结

本节课我们一起学习了BTreeSet数据结构。我们了解到:

  • BTreeSetHashSet一样,能保证元素的唯一性。
  • BTreeSet的额外优势是能自动维护元素的排序状态。
  • 这种特性使其非常适用于需要有序唯一集合的场景,例如生成仪表盘统计数据或处理有序的交易记录。
  • 结合Rust的二进制部署和C语言级别的性能,使用BTreeSet构建的解决方案可以高效地大规模运行。

通过这个简单的例子,我们看到了BTreeSet如何优雅地解决维护有序唯一数据集合的问题。

020:使用二叉堆创建无花果优先水果沙拉 🥗

在本节课中,我们将学习如何使用Rust标准库中的BinaryHeap数据结构来创建一个“水果沙拉”程序。这个程序的特别之处在于,它会为水果“无花果”赋予最高优先级,确保在最终的水果沙拉中,无花果总是优先出现。我们将通过实现OrdPartialOrd特质来定义优先级,并使用随机性来模拟水果的选择过程。


概述与核心概念

我们将创建一个程序,它使用二叉堆作为优先队列。优先队列是一种数据结构,允许我们始终高效地访问集合中“优先级最高”或“优先级最低”的元素。在Rust中,std::collections::BinaryHeap默认实现了一个最大堆,这意味着堆顶的元素总是最大的(根据我们定义的排序规则)。

为了实现无花果的优先权,我们需要定义一个枚举(enum)来表示水果,并为这个枚举实现OrdPartialOrd特质,以指定无花果的优先级高于其他水果。


定义水果枚举与优先级

首先,我们定义一个名为Fruit的枚举,它有两种变体:Fig(无花果)和Other(其他水果)。

enum Fruit {
    Fig,
    Other(String),
}

接下来,我们需要为Fruit实现排序特质。我们希望Fig变体被视为“最大”的,这样它就会在二叉堆的顶部。

use std::cmp::Ordering;

impl Ord for Fruit {
    fn cmp(&self, other: &Self) -> Ordering {
        match (self, other) {
            // Fig 的优先级最高
            (Fruit::Fig, Fruit::Fig) => Ordering::Equal,
            (Fruit::Fig, Fruit::Other(_)) => Ordering::Greater,
            (Fruit::Other(_), Fruit::Fig) => Ordering::Less,
            // 其他水果之间比较,这里简单按字符串比较,不影响Fig的优先级
            (Fruit::Other(a), Fruit::Other(b)) => a.cmp(b),
        }
    }
}

// 必须同时实现 PartialOrd, PartialEq, Eq
impl PartialOrd for Fruit {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl PartialEq for Fruit {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

impl Eq for Fruit {}

通过以上实现,我们确保了在任何比较中,Fruit::Fig都被认为大于Fruit::Other,从而在二叉堆中获得最高优先级。


构建水果沙拉

现在,让我们进入程序的核心部分:生成水果沙拉。我们将遵循以下步骤:

  1. 创建一个包含多种水果的列表,并特意添加多个无花果。
  2. 初始化一个空的BinaryHeap
  3. 设置一个目标:至少获得两份无花果。
  4. 循环从水果列表中随机选择水果,并推入堆中,直到我们收集到足够数量的无花果。
  5. 将堆中剩余的水果也加入沙拉。

以下是实现这个逻辑的代码框架:

use std::collections::BinaryHeap;
use rand::seq::SliceRandom; // 需要添加 rand 依赖
use rand::thread_rng;

fn main() {
    // 1. 定义水果列表
    let mut fruits = vec![
        Fruit::Other("apple".to_string()),
        Fruit::Other("orange".to_string()),
        Fruit::Other("pear".to_string()),
        Fruit::Other("peach".to_string()),
        Fruit::Other("banana".to_string()),
        Fruit::Fig, Fruit::Fig, Fruit::Fig, Fruit::Fig, // 添加多个无花果
    ];

    let mut rng = thread_rng();
    let mut salad = BinaryHeap::new();
    let mut fig_count = 0;
    let target_figs = 2;

    // 2. 优先收集无花果
    while fig_count < target_figs {
        // 随机打乱并选择一种水果
        fruits.shuffle(&mut rng);
        if let Some(fruit) = fruits.pop() {
            match fruit {
                Fruit::Fig => fig_count += 1,
                _ => {}
            }
            salad.push(fruit);
        }
    }

    // 3. 将剩余水果加入沙拉
    for fruit in fruits {
        salad.push(fruit);
    }

    // 4. 输出最终沙拉(从堆中依次弹出,即优先级从高到低)
    println!("随机水果沙拉(含{}份无花果):", target_figs);
    while let Some(fruit) = salad.pop() {
        match fruit {
            Fruit::Fig => println!("无花果"),
            Fruit::Other(name) => println!("{}", name),
        }
    }
}

程序运行与结果

运行此程序(使用cargo run),每次都会生成一个随机的水果沙拉,但前两个(或指定数量的)最高优先级的元素保证是无花果。输出可能如下所示:

随机水果沙拉(含2份无花果):
无花果
无花果
peach
orange
apple
banana
pear

即使再次运行,无花果也总是最先出现,这证明了二叉堆根据我们定义的Ord实现正确地对元素进行了排序。


总结

本节课中,我们一起学习了如何使用Rust的BinaryHeap创建一个优先队列。我们通过以下步骤实现了“无花果优先水果沙拉”程序:

  1. 定义数据结构:创建了Fruit枚举来区分无花果和其他水果。
  2. 实现优先级:通过为Fruit实现OrdPartialOrd特质,明确了无花果具有最高优先级。
  3. 利用二叉堆:使用BinaryHeap作为优先队列,确保在插入和弹出元素时,优先级最高的(无花果)始终在最前面。
  4. 控制流程:通过循环逻辑,保证了最终结果中至少包含指定数量的高优先级项。

二叉堆是数据工程和DevOps中非常有用的工具,特别适用于需要持续处理最大或最小元素的场景,例如任务调度、日志级别处理或任何需要优先级管理的系统。你可以尝试修改优先级规则或添加更多水果类型来进一步探索这个数据结构。

021:PageRank算法实现教程 🏀

在本节课中,我们将学习PageRank算法的核心概念,并使用Rust语言实现一个基础的PageRank计算器。PageRank最初由Google开发,用于根据网页的相关性和重要性进行排名。我们将探讨阻尼因子的概念,并了解如何通过迭代计算来稳定排名结果。

算法概述与核心概念

PageRank是一种链接分析算法,它通过网页之间的链接关系来衡量网页的重要性。算法的核心思想是:一个网页被越多高权重的网页链接,其自身的权重也越高。

上一节我们介绍了PageRank的基本概念,本节中我们来看看其数学表示和关键参数。

阻尼因子(damping factor)用于模拟用户随机跳转到其他网页的行为,其公式表示为:

PR(p) = (1-d)/N + d * Σ(PR(i)/L(i))

其中:

  • PR(p) 是页面p的PageRank值。
  • d 是阻尼因子,通常设为0.85。
  • N 是图中所有页面的总数。
  • Σ(PR(i)/L(i)) 表示对所有链接到页面p的页面i,将其PageRank值除以其出链总数L(i),然后求和。

迭代次数(iterations)决定了算法运行的轮数,以确保排名值趋于稳定。

Rust实现详解

现在,我们进入代码实现部分。我们将创建一个PageRank结构体,并实现其核心的排名计算方法。

以下是PageRank结构体的定义及其new方法:

pub struct PageRank {
    damping: f64,
    iterations: usize,
}

impl PageRank {
    pub fn new(damping: f64, iterations: usize) -> Self {
        PageRank { damping, iterations }
    }
}

new方法用于创建一个PageRank计算器的新实例,需要指定阻尼因子和迭代次数。

接下来,我们看看核心的rank方法是如何实现的。该方法接收一个代表链接图的邻接表,并返回每个节点的PageRank值向量。

pub fn rank(&self, graph: &Vec<Vec<usize>>) -> Vec<f64> {
    let n = graph.len();
    let mut ranks = vec![1.0 / (n as f64); n];

    for _ in 0..self.iterations {
        let mut new_ranks = vec![0.0; n];
        for (node, links) in graph.iter().enumerate() {
            if links.is_empty() {
                for rank in &mut new_ranks {
                    *rank += ranks[node] / (n as f64);
                }
            } else {
                let share = ranks[node] / (links.len() as f64);
                for &link in links {
                    new_ranks[link] += share;
                }
            }
        }
        for rank in &mut new_ranks {
            *rank = *rank * self.damping + (1.0 - self.damping) / (n as f64);
        }
        ranks = new_ranks;
    }
    ranks
}

让我们逐步分析rank方法的执行流程:

  1. 初始化:首先,获取图中节点的总数n,并将每个节点的初始排名设置为1/n,确保所有排名之和为1。
  2. 迭代计算:进入主循环,运行指定的迭代次数。在每次迭代中:
    • 创建一个new_ranks向量来存储本轮计算出的新排名。
    • 遍历图中的每个节点及其出链列表。
    • 如果节点没有出链,则将其当前排名平均分给所有节点(模拟随机跳转)。
    • 如果节点有出链,则将其当前排名按出链数量平分,并加到目标链接节点的new_ranks中。
  3. 应用阻尼因子:在每一轮迭代结束后,对new_ranks中的每个值应用阻尼因子公式,确保即使没有入链的页面也能获得少量基础排名。
  4. 更新排名:用计算出的new_ranks替换旧的ranks,进行下一轮迭代。

运行示例与结果

理解了算法实现后,我们通过一个具体的例子来演示其运行。我们构建一个简单的体育网站链接图。

以下是构建图和调用PageRank计算的代码:

fn main() {
    // 构建图:索引代表网站,向量内容代表其链接到的其他网站索引
    // 0:ESPN, 1:NFL, 2:NBA, 3:UFC, 4:MLB
    let graph = vec![
        vec![1], // ESPN -> NFL
        vec![0], // NFL -> ESPN
        vec![0, 3], // NBA -> ESPN, UFC
        vec![0], // UFC -> ESPN
        vec![0, 1], // MLB -> ESPN, NFL
    ];

    let sites = vec!["ESPN", "NFL", "NBA", "UFC", "MLB"];

    let pagerank = PageRank::new(0.85, 20);
    let ranks = pagerank.rank(&graph);

    for (i, &rank) in ranks.iter().enumerate() {
        println!("{}: {:.2}", sites[i], rank);
    }
}

运行程序(cargo run)后,我们将得到类似以下的输出结果:

ESPN: 0.42
NFL: 0.22
NBA: 0.11
UFC: 0.11
MLB: 0.14

输出显示,ESPN拥有最高的PageRank值(0.42),因为它获得了最多、最重要的入链。NFL紧随其后。这个排名结果反映了节点在网络中的连接重要性。

总结与应用

本节课中我们一起学习了PageRank算法的原理与Rust实现。我们首先了解了算法背后的核心思想——通过链接关系衡量重要性,并认识了阻尼因子和迭代次数两个关键参数。接着,我们详细剖析了rank方法的实现步骤,包括初始化、迭代计算、排名分发以及阻尼因子的应用。最后,我们通过一个体育网站链接图的例子,看到了算法计算出的具体排名结果。

总而言之,PageRank是一个强大的链接分析算法。其核心思想可以超越网页排名,应用于任何需要根据网络内连接强度进行排序的场景,例如:

  • 在组织内部构建文档搜索算法。
  • 分析电子邮件往来中的重要联系人。
  • 识别客户关系网络中的关键节点。

通过本教程,你不仅掌握了用Rust实现PageRank的技能,也获得了一种分析网络结构的重要工具。实现中唯一使用的非标准库是textwrap,它仅用于将输出描述文本自动换行,保持终端显示整洁。

022:使用Dijkstra算法寻找最短路径 🗺️

在本节课中,我们将学习如何使用Rust语言和petgraph库来实现经典的Dijkstra算法,以解决一个实际问题:寻找葡萄牙里斯本两个地标之间的最短路径。


概述

我们将通过一个具体的例子来演示Dijkstra算法。这个算法是一种经典的图遍历技术,用于寻找从单个源节点到图中所有其他节点的最短路径。这意味着,如果我们有许多可以到达的不同地点,该算法能找出需要遍历的最短节点序列及其实际距离。

构建图结构

首先,我们需要使用petgraph库来构建一个图。petgraph是一个优秀的Rust图处理库。我已经通过Cargo将其安装到项目中。

以下代码展示了如何初始化一个无向图:

use petgraph::graph::UnGraph;

得益于Rust出色的类型系统,我们无需担心可能出现的许多错误。类型系统为我们清晰地规划了图中将包含哪些元素。

添加节点与边

接下来,我们向图中添加节点和带权重的边。节点代表里斯本的各个地标。

以下是添加节点和边的示例代码:

let mut graph = UnGraph::<&str, i32>::new_undirected();
let belem_tower = graph.add_node("Belem Tower");
let monastery = graph.add_node("Monastery");
let lisbon_cathedral = graph.add_node("Lisbon Cathedral");

graph.add_edge(belem_tower, monastery, 1);
graph.add_edge(belem_tower, lisbon_cathedral, 3);
// ... 可以继续添加其他边

我们首先创建节点,例如“贝伦塔”和“修道院”。然后,我们使用add_edge方法添加边,并指定它们之间的距离(权重),例如从贝伦塔到修道院的距离是1。

应用Dijkstra算法

petgraph库的一个便利之处在于它内置了Dijkstra算法的实现。我们无需自己编写这个复杂的算法。

我们可以直接调用库函数来计算两个节点之间的最短路径和距离。代码如下:

use petgraph::algo::dijkstra;
let node_map = dijkstra(&graph, belem_tower, Some(lisbon_cathedral), |e| *e.weight());

这段代码计算了从“贝伦塔”节点到“里斯本大教堂”节点的最短路径。

运行与结果

现在,让我们运行程序查看结果。在终端中输入 cargo run 执行代码。

程序运行后,输出显示从贝伦塔到里斯本大教堂的最短距离是 8公里

实际应用与优势

通过这个例子,我们可以看到使用知名算法解决实际问题的强大之处。无论是数据工程师、机器学习工程师,还是从事物流相关工作的人,都可以构建类似的解决方案。

他们可以信赖Rust带来的卓越性能。部署这样的程序也非常简单,只需将编译后的二进制文件推送到目标位置即可运行。该程序内存占用极低,并且算法本身经过优化。

在Rust中构建并解决这类经典问题有很多优点:我们可以获得接近C语言级别的高性能,同时得益于Rust这门现代编译语言内置的所有安全特性,程序又是内存安全的。


总结

本节课我们一起学习了如何在Rust中使用petgraph库实现Dijkstra算法。我们构建了一个代表里斯本地标的图,添加了节点和带权重的边,并最终计算出了两个地标之间的最短路径距离。这个过程展示了Rust在解决经典算法问题时的性能、安全性和部署便利性。

023:使用Kosaraju算法进行强连通分量检测

在本节课中,我们将学习如何使用Rust语言和Kosaraju算法,从社交网络数据(例如Twitter用户的提及关系)中检测社区或强连通分量。我们将通过一个具体的例子,分析如何识别在推文中互动频繁的用户群体。

概述

社区检测是图论中的一个重要应用,常用于社交网络分析。本节课我们将利用Rust的petgraph库和Kosaraju算法,分析Twitter用户之间的提及关系,从而识别出紧密互动的用户社区。我们将从数据准备开始,逐步构建图结构,应用算法,并最终解释结果。

数据准备与图构建

首先,我们需要准备数据并构建图模型。数据包含Twitter用户名及其提及的其他用户,我们将这些关系建模为有向图的边。

以下是构建图结构的关键步骤:

  1. 导入库与数据:我们使用petgraph库来处理图结构和算法,并从一个文件中读取用户名和提及关系数据。
  2. 创建图与索引:我们创建一个有向图,并使用哈希映射来存储用户名到图中节点索引的对应关系。
  3. 添加节点与边:遍历数据,为每个出现的用户添加为图中的一个节点。然后,根据“用户A提及了用户B”的关系,在对应的节点之间添加一条有向边。
// 伪代码示例:添加节点和边
let mut graph = DiGraph::<&str, ()>::new();
let mut node_indices = HashMap::new();

for (user, mention) in data_pairs {
    let user_node = *node_indices.entry(user).or_insert_with(|| graph.add_node(user));
    let mention_node = *node_indices.entry(mention).or_insert_with(|| graph.add_node(mention));
    graph.add_edge(user_node, mention_node, ());
}

上一节我们介绍了如何将原始数据构建为有向图。接下来,我们将在这个图上应用核心算法来发现社区。

应用Kosaraju算法

图构建完成后,核心任务是检测其中的强连通分量。一个强连通分量是指图中的一个子图,其中任意两个节点都可以通过有向路径相互到达。在Twitter语境下,这通常代表一个内部互动非常紧密的用户群体。

我们将使用Kosaraju算法来完成这一检测。该算法效率很高,其时间复杂度为 O(V + E),其中V是顶点数,E是边数。

以下是算法应用过程:

  1. 调用算法:我们直接使用petgraph库中提供的kosaraju_scc函数。
  2. 获取结果:该函数返回一个向量,向量中的每个元素也是一个向量,包含属于同一个强连通分量的所有节点的索引。
  3. 映射回用户名:通过之前建立的哈希映射,我们可以将节点索引转换回实际的Twitter用户名,使结果更易读。
// 伪代码示例:运行Kosaraju算法并输出结果
let scc = kosaraju_scc(&graph);
for component in scc {
    let usernames: Vec<_> = component.iter().map(|&node_index| graph[node_index]).collect();
    println!("发现社区: {:?}", usernames);
}

运行结果与解读

运行程序后,我们将得到检测出的社区列表。每个社区包含一组用户名,这些用户之间存在双向或循环的提及关系。

以下是程序输出的一个示例解读:

  • 它可能识别出一个由“journalist1”、“journalist2”、“journalist3”组成的社区,这表明这几位记者经常相互引用或转发彼此的内容。
  • 同时,它也能从数据中分离出另一组被标记为“ legitimate troll accounts”的用户,这些是曾在真实事件中被识别出的特定账户群体。

这个结果验证了算法能够有效区分不同的互动群体。即使我们混入了一些模拟数据(“fake community”),算法也能正确地将它们区分开来。

总结

本节课中我们一起学习了如何使用Rust实现一个高效的社区检测工具。我们首先将Twitter的提及关系数据建模为有向图,然后利用Kosaraju算法检测图中的强连通分量,从而识别出内部互动密集的用户社区。这个方法不仅性能出色,时间复杂度为 O(V + E),而且完全由Rust构建,可以编译为独立的二进制工具,易于集成到生产环境的数据管道或社交分析平台中。通过这个案例,我们看到了Rust在处理图论和数据分析任务时的强大能力。

024:使用Rust绘制简易ASCII图表 📊

在本节课中,我们将学习如何使用Rust语言中的raski图形库来可视化数据。具体来说,我们将通过一个简单的例子,展示如何将城市间的距离数据绘制成清晰易读的ASCII图表。

概述

我们将创建一个Rust程序,用于展示从里斯本到欧洲其他几个城市的距离。通过使用raski库,我们可以用极少的代码生成一个直观的ASCII图表,并在终端中直接输出。

配置与数据准备

首先,我们需要在项目的Cargo.toml文件中添加raski库作为依赖项。接着,在代码中配置图表并准备数据。

以下是配置图表和定义数据的步骤:

  1. 导入库并配置图表:使用raski库创建一个图表配置。
  2. 定义城市列表:我们选取里斯本、马德里、巴黎、柏林、哥本哈根、斯德哥尔摩和莫斯科作为示例城市。
  3. 创建距离向量:构建一个向量(Vec),其中包含从里斯本到各个城市的距离(单位:公里)。例如,里斯本到里斯本的距离是0公里,里斯本到马德里的距离是502公里。

以下是数据定义的代码示例:

let cities = vec!["Lisbon", "Madrid", "Paris", "Berlin", "Copenhagen", "Stockholm", "Moscow"];
let distances = vec![0, 502, 1053, 2187, 2806, 3116, 4596];

生成与输出图表

上一节我们准备好了数据,本节中我们来看看如何使用raski库将这些数据绘制成图表并输出。

这个过程非常简单直接。我们只需将定义好的距离向量传递给库的绘图函数,并应用我们的配置。

以下是生成图表的关键步骤:

  • 调用库函数,传入距离数据向量。
  • 函数会根据数据自动生成一个ASCII风格的条形图。
  • 图表会直接打印到终端控制台。

运行程序(cargo run)后,终端会显示图表。图表顶部有一个标题,说明图表展示的是“从里斯本出发的旅行距离”。随着城市序列的推进,代表距离的条形图会逐渐变长,直观地反映了距离的增加。图表底部还清晰地标注了单位“公里”。

总结

本节课中我们一起学习了如何使用Rust和raski图形库快速创建ASCII图表。我们通过一个具体的例子,演示了如何将结构化的数据(城市距离)转换为可视化的终端图表。这种方法代码量小,易于集成到基于命令行的工具中,能有效地向工具使用者传达信息。对于需要在Rust开发的数据工程或DevOps工具中嵌入简单图表和图形的场景,这是一个非常实用且高效的解决方案。

025:多因素身份验证 🔐

在本节课中,我们将要学习多因素身份验证的基本概念及其重要性。我们将通过一个生动的城堡比喻来理解其工作原理,并探讨它在现代计算机安全中的应用。

城堡比喻:理解多因素身份验证 🏰

上一节我们介绍了课程主题,本节中我们来看看如何通过一个城堡的比喻来理解多因素身份验证。

让我们在城堡的背景下讨论多因素身份验证。这是一个很好的方式,可以审视一些在云环境中可能实现的基本安全凭证和安全机制,这些机制实际上已经存在了数千年。

让我们看看这座城堡。如果你要获得进入一个安全城堡的权限,你必须知道不止一件事。你需要知道口令,守卫还会检查你拥有的东西,比如皇家徽章。这被称为双因素身份验证,它是一种更强的安全级别。

双因素认证的优势 🛡️

上一节我们通过城堡比喻引入了多因素认证的概念,本节中我们来看看它为何能提供更强的安全性。

你不仅仅依赖于像密码这样的单一因素,因为单一因素有风险。密码可能被猜到、被窃取或被破解,任何得知密码的人都可能冒充你。而使用双因素认证,冒名顶替者需要两样东西:你知道的某样东西和你拥有的某样东西。即使他们窃取了你的口令,他们也无法进入城堡,因为他们没有你的皇家徽章。

同样的原则适用于安全的计算机和在线账户。你可以使用你知道的密码,加上你实际拥有的安全密钥,甚至可以将PIN码与你的指纹结合起来。

现代应用与最佳实践 💻

上一节我们探讨了双因素认证的原理,本节中我们来看看它在现代环境中的应用和重要性。

双因素认证已成为安全领域的最佳实践之一。银行用它来防止未经授权的转账,公司启用它以保护员工账户。这个额外的步骤需要付出多一点努力,但能极大地提高防御渗透的能力。

因此,当你考虑设置一个新账户时,请寻找开启双因素认证的选项。这是增强防御入侵者能力的一个简单方法。同样重要的是要注意,添加第二层认证就像在城堡周围挖了一条护城河。你将能睡得更安稳,因为你知道你的账户和数据得到了更好的保护。

总结 📝

本节课中我们一起学习了多因素身份验证。我们通过城堡的比喻理解了其核心思想:结合“你知道的”(如密码)和“你拥有的”(如安全密钥或徽章)两种因素来提供更强的安全保障。我们看到了它在从古代城堡到现代银行和公司账户保护中的一贯应用,并认识到开启双因素认证是保护个人数据和账户安全的一项简单而有效的最佳实践。

026:网络分段 🏰

在本节课中,我们将要学习网络分段的概念。网络分段是一种将计算机网络划分为多个子网络的安全实践,其核心思想类似于古代城堡的分区防御。通过将网络划分为不同的安全区域,我们可以限制潜在攻击者的活动范围,从而保护关键数据和资产。


上一节我们介绍了网络安全的重要性,本节中我们来看看网络分段的具体实现和原理。

想象一座城堡。城堡会将其领地划分为不同的区域,例如军械库、王室寝宫等。这些区域因其重要性而受到严密守卫。相比之下,厨房则不需要同等级别的保护。城堡的大门控制着人员的流动。这种分段方式增强了安全性,降低了风险。即使入侵者进入了厨房,也无法轻易进入更敏感的区域,从而将损害控制在局部。

计算机网络与此非常相似。你可以将网络划分为由防火墙隔离的多个网段。面向公众的Web服务器被放置在一个称为“非军事区”(DMZ)的外部区域。内部区域则存放敏感数据,例如客户记录。分段控制着这些区域之间的访问。这会限制攻击者横向移动并造成更广泛损害的能力。用户和设备只与它们需要交互的系统进行通信。

总而言之,网络分段是一种强大的安全实践,但需要一些规划。分组将根据功能、敏感性、访问需求来设计。更可信的区域会获得更多的访问权限。例如,数据库服务器可能需要一个受限制的网段。应用程序服务器需要查询该数据库,但只被授予特定的访问权限,其他系统则被阻止访问。

你可以将每个区域想象成城堡的庭院。吊桥的升起或放下控制着区域之间的流动。分段将保护关键数据和资产。

以下是实现网络分段的一些关键技术和组件:

  • 防火墙和VLAN:将网络划分为安全区域。
  • 访问限制:限制区域间的访问,以缩小入侵的影响半径。
  • 入侵检测系统:监控网络中的异常行为。
  • 子网划分和VLAN:用于网络分区。
  • 访问控制列表:用于接口权限管理。
  • 数据包检查:在防火墙中进行深度数据包检查。
  • 启发式分析:用于识别未知威胁。

因此,保护网络的概念与旧时的城堡防御策略非常相似。


本节课中我们一起学习了网络分段。我们了解到,通过将网络像城堡一样划分为不同安全级别的区域,并使用防火墙、VLAN、访问控制列表等技术来控制区域间的通信,可以有效地遏制安全威胁的扩散,提升整体网络的安全性。这是一种基础且关键的网络防御策略。

027:最小权限访问 🔐

在本节课中,我们将要学习计算机安全中的一个核心原则——最小权限访问。我们将通过一个城堡的比喻来理解这一概念,并探讨其在计算机系统和Rust编程中的实际应用。

城堡中的安全模型 🏰

上一节我们介绍了安全的基本概念,本节中我们来看看最小权限原则在现实世界中的类比。

让我们将最小权限访问视为计算机世界中的一个“城堡”内部的安全情境。显然,我们知道需要限制授予用户的权限数量,直到他们确实需要这些权限。这与城堡中的概念完全相同。

在城堡内部,访问权限是受到策略性限制的。例如,伯爵可以进入城堡的任何区域,但一个仆人只能去其职责所需的地方。这种最小权限模型增强了安全级别。

计算机系统中的权限控制 💻

理解了城堡的比喻后,我们来看看它在计算机系统中的对应关系。

类似地,在计算机系统中,用户和程序只应获得必要的权限。如果授予了过多的访问权限,就会增加被滥用的风险。例如,如果一个城堡守卫不需要进入金库,或者一个厨房厨师不需要进入兵营,他们的工作就被限制在那些被授权的区域内。

在计算领域,授权是通过访问策略来控制的。这意味着一个数据库应用程序将获得对特定表的读写权限,但它不应该能够删除服务器上的文件。

以下是有效的安全授权原则:

  • 只授予完成任务所需的最小访问量。
  • 如果职责增加,可以添加额外的权限。
  • 但起始时有一个严格的基线。

实施最小权限的挑战与益处 ⚖️

了解了原则之后,我们来看看实施过程中的常见问题及其重要性。

不幸的是,许多组织开始时采用了过于宽松的访问模型。随后发生的情况是,你试图在后期锁定安全,但会遇到问题。这就像在限制某些区域之前,让所有人都进入城堡。

最好从一开始就采用最小权限的思维方式。虽然这确实需要前期多做一点工作,但它能极大地减少因错误、程序缺陷或恶意行为者造成损害的可能性。这种权限限制将锁定——在这个特定的例子中是城堡,而在你的情况下,是你的数字城堡或计算机系统。

总结 📝

本节课中我们一起学习了最小权限访问原则。我们通过城堡的比喻理解了限制权限的重要性,探讨了在计算机系统中如何通过访问策略控制授权,并比较了从开始就实施最小权限与后期补救的利弊。记住,始终从最小必要权限开始,是构建安全系统的坚实基础。

028:加密技术 🔐

在本节课中,我们将从城堡的视角来学习加密技术。我们将探讨加密如何保护数据,并了解对称加密与非对称加密的核心概念。

城堡与加密的类比

想象一座城堡,城堡内部有安全的密室。加密技术的作用,就是确保在这些密室中的对话保持私密性。

当信鸽需要在城墙外传递重要信息时,会使用一种秘密编码语言。加密本身就像一件隐形斗篷,它防止未经授权的人访问敏感信息。任何被拦截的信息,对于没有密码钥匙的人来说,都显得毫无意义。

现代计算中的加密

现代计算依赖加密技术来保护数据,无论是静态存储的数据还是传输中的数据。强大的算法会对信息进行加扰,确保只有授权方才能读取。

加密钥匙是解锁数据访问权限的关键。在对称加密中,同一把钥匙既用于加密也用于解密。其核心公式可以表示为:

C = E(K, P)P = D(K, C)

其中:

  • C 是密文
  • P 是明文
  • K 是密钥
  • E 是加密函数
  • D 是解密函数

这种方式通常用于加密存储在硬盘上的文件。

非对称加密

然而,为了进行安全的通信,我们会使用非对称加密。这种方法使用一对密钥:一个公钥和一个私钥

你可以在SSH等场景中看到这种应用:你分享你的公钥,而你的私钥则用于解密数据,以便实际使用。其核心过程可以简化为:

加密: C = E(Public_Key, P)
解密: P = D(Private_Key, C)

密钥管理的重要性

上一节我们介绍了加密的两种主要方式,本节中我们来看看一个至关重要的环节:密钥管理。正确的密钥管理变得至关重要,因为如果你丢失了私钥,加密的保护措施将不复存在。

以下是密钥管理不善可能导致的后果:

  • 数据变得无法使用。
  • 如果有人获取了你的数据并拥有私钥,他们就能解密数据。

因此,在系统和网络中全面应用加密,可以防止对敏感材料的未授权访问。它将数据转换成一种秘密代码,对入侵者来说就像乱码一样。

总结

本节课中我们一起学习了加密技术的基本原理。我们看到,中世纪的城堡在保护秘密方面,与我们现代使用的加密技术有许多共通之处。核心在于使用密钥(无论是单一的对称密钥,还是成对的非对称密钥)来转换数据,确保其机密性,而妥善的密钥管理是这一切安全性的基石。

029:可变与不可变数据

在本节课中,我们将学习Rust语言中一个核心的安全设计特性:可变性不可变性。我们将通过一个“水果沙拉”的示例,理解为何默认的不可变性是重要的安全保障,以及如何在需要时安全地修改数据。

概述

Rust语言默认将所有变量视为不可变的。这意味着一旦一个值被绑定到变量名上,你就不能更改它。这种设计并非限制,而是一种主动的安全防护,旨在防止代码中的意外修改,从而避免潜在的错误。本节我们将通过一个具体的例子来探索这个概念。

不可变的水果沙拉

首先,我们创建一个不可变的字符串向量(类似于Python中的列表)。在Rust中,使用 let 关键字声明的变量默认是不可变的。

fn main() {
    // 创建一个不可变的水果向量
    let fruit_salad = vec!["apple", "banana", "cherry", "dates", "elderberries"];

    // 打印原始水果沙拉
    println!("Original fruit salad: {:?}", fruit_salad);
}

运行这段代码,你会看到它成功打印出水果列表。然而,如果你试图修改这个向量,例如向其中添加新的水果,编译器会阻止你。

尝试修改不可变数据

上一节我们介绍了不可变变量的创建。本节中我们来看看如果尝试修改它会发生什么。以下是尝试修改的代码:

fn main() {
    let fruit_salad = vec!["apple", "banana", "cherry", "dates", "elderberries"];
    println!("Original fruit salad: {:?}", fruit_salad);

    // 尝试向不可变向量中添加新元素(这将导致编译错误)
    fruit_salad.push("figs");
}

当你尝试编译这段代码时,Rust编译器会报错。它甚至能在你运行 cargo run 之前,在代码编辑器中就提示错误信息:

error[E0596]: cannot borrow `fruit_salad` as mutable, as it is not declared as mutable

编译器不仅会指出错误,还会给出清晰的修复建议,例如提示你“或许应该将变量声明为可变的”。这是Rust编译器一个非常强大的特性,它基于对代码结构的理解来提供帮助,而非依赖复杂的AI技术。

创建可变的水果沙拉

既然我们理解了默认的不可变性及其保护作用,现在来看看如何在确实需要时安全地修改数据。为了使一个变量可变,我们需要在声明时显式地使用 mut 关键字。

以下是修改后的可变版本代码:

fn main() {
    // 使用 `mut` 关键字声明一个可变的向量
    let mut fruit_salad = vec!["apple", "banana", "cherry", "dates", "elderberries"];
    println!("Original fruit salad: {:?}", fruit_salad);

    // 现在可以安全地向向量中添加新元素
    fruit_salad.push("figs");
    println!("Modified fruit salad: {:?}", fruit_salad);
}

运行这段代码,你将看到两个输出:原始的不可变水果列表,以及修改后包含了“figs”的新列表。通过显式地声明 mut,你明确表达了修改数据的意图,编译器则允许并确保这一操作在安全范围内进行。

总结

本节课中我们一起学习了Rust中可变性与不可变性的核心概念。我们了解到:

  1. 默认不可变是Rust的一项关键安全设计,它能防止代码中的意外数据修改。
  2. 尝试修改不可变变量会导致编译时错误,编译器会提供清晰的错误信息和修复建议。
  3. 当确实需要修改数据时,可以通过 let mut 来显式声明可变变量

这种“默认安全”的设计哲学意味着,虽然你需要在编写代码时多花一点心思来明确数据的可变性,但它能极大地避免程序在运行数百万次后可能出现的隐蔽错误。随着你对Rust的使用越来越深入,你会越发欣赏这种前期投入所带来的长期稳定性和安全性保障。

030:使用CLI定制水果沙拉 🍎🍌🍓

在本节课中,我们将学习如何通过一个简单的Rust程序来定制水果沙拉。这个程序演示了如何使用命令行界面(CLI)来读取输入,并利用随机化功能对数据进行处理。我们将重点关注代码的结构、安全性以及它在数据工程中的实际应用。

概述

我们将分析一个Rust库函数,它接收一个字符串向量,并返回一个随机排序的新向量。然后,我们将看到如何在主程序中使用这个函数,结合命令行参数解析,来处理来自CSV文件或直接输入的字符串数据。

库函数解析

首先,我们来看一下位于 lib.rs 文件中的一个函数示例。这个函数通过引入一些随机化来定制Rust中的某些内容。

pub fn randomize_fruits(mut fruits: Vec<String>) -> Vec<String> {
    // ... 随机化逻辑 ...
}

以下是关于这段Rust代码的一些独特之处:

  • pub 关键字:这表明该函数可以在另一个模块中使用,是公开可用的。这是一个很好的安全默认设置,允许我们将它导入到 main 函数中。
  • 函数定义:使用 fn 关键字定义函数。输入包括一个可变的字符串向量 fruits,返回类型也是一个字符串向量。
  • 显式类型与可变性:这种Rust编程风格的强大之处在于,我们能够明确地声明哪些内容在代码的其他部分暴露,以及输入和输出的具体类型。更重要的是,我们还显式地指明了向量的可变性。

主程序应用

上一节我们介绍了核心的随机化函数,本节中我们来看看如何在主程序中使用它来操作CSV文件。

执行此代码的方式是运行 cargo run -- fruits.csv,或者也可以显式地输入参数。

在数据工程中,这是一个非常常见的问题:你有一些输入数据,并希望对其进行一些更改。这类操作可能每年运行数十万次。因此,如果能够添加尽可能多的安全和可靠性默认设置,你将获得更好的体验。

以下是主程序的关键部分:

  • 命令行解析:我们使用 clap 库来解析命令行参数。
  • 结构体定义struct Opts 定义了输入选项。我们希望能够输入水果列表,可以是一个CSV文件,也可以是一串逗号分隔的值。
  • 数据转换函数:一个函数负责将CSV文件内容转换为字符串向量。在数据工程中,将一种数据形式操作转换为另一种是非常常见的任务。这段代码的强大之处再次体现在它明确声明了输入(字符串)和输出(字符串向量)。
  • 数据显示:最后,为了显示水果沙拉,我们将结果传递给另一个函数进行处理。

main 函数的最后,我们整合了选项解析,并根据输入决定是从CSV文件读取水果列表,还是直接使用命令行输入。match 语句在这里处理了这两种场景。最终,程序会显示定制好的水果沙拉。

运行与测试

现在,让我们看看如何实际运行这个程序。通常,即使手边没有现成的文档,我们也能以直观的方式弄清楚Rust项目的情况。

首先,我通常在CLI中键入 cargo run。如果它能运行,那是个好迹象。

编译后运行,程序会输出“你的水果沙拉包含:”,但最初因为没有输入任何内容,所以是空的。注意到帮助信息提示了 --fruits 参数。

既然我知道它支持某种命令行解析输入,我可以输入 cargo run -- --help。这告诉Cargo工具将我的输入参数传递给要执行的程序。使用 clap 这类解析器库的一个好处是,你可以直接看到使用说明,而不必自己处理所有细节。

查看帮助,我们可以看到“定制水果沙拉”程序有选项,可以指定CSV文件。让我们尝试一下。

传入CSV文件名,程序会读取该文件中的输入,并对其进行随机化。如果再次运行,会得到一个略有不同的水果组合。

我们还可以做更多事情。回到帮助菜单,我们可以看到有查看版本的选项(--version-V)。执行后会显示版本号(例如1.0)。

此外,我们也可以直接将水果作为一串逗号分隔的值输入。例如:

cargo run -- --fruits apple,pear,strawberry

程序同样能够将其随机化并输出。

总结

本节课中我们一起学习了一个简单的示例,但讨论它很重要,因为它提供了一个绝佳的范式,尤其适用于构建数据工程工具。当你希望构建一些极其安全、健壮,并且能够稳定运行十年甚至二十年的工具时,Rust语言的设计使其很容易“做正确的事”,这意味着你的代码在未来将极其可靠,并且对其他运行它的人来说直观易懂。这个例子展示了如何通过明确的类型系统、所有权模型和模块化设计来达成这些目标。

031:数据竞争示例 🛡️

在本节课中,我们将学习Rust语言的一个核心特性:它如何保护开发者,避免在多线程应用程序中编写出导致灾难性错误的代码。我们将通过一个具体的示例,理解数据竞争的危险性,并了解Rust编译器如何帮助我们预防此类问题。

理解数据竞争的危险性

上一节我们提到了Rust在并发编程中的安全性。本节中我们来看看一个具体的、会导致数据竞争的代码示例,并理解为何这种操作是危险的。

以下是一段试图在多线程中修改共享数据的代码:

use std::thread;

fn main() {
    let mut data = vec![1, 2, 3];

    for i in 0..3 {
        thread::spawn(move || {
            data[i] += 1;
        });
    }
}

在这段代码中,我们创建了一个可变的向量 data,然后尝试在三个不同的线程中捕获它的可变引用并进行修改。这种操作之所以极其危险,是因为多个线程同时访问和修改同一块内存数据,没有任何协调机制。

以下是这种操作可能导致的问题:

  • 数据损坏:一个线程的写入操作可能被另一个线程的写入操作覆盖,导致最终数据状态不可预测。
  • 竞态条件:程序的执行结果依赖于线程调度的时间顺序,这会导致结果不一致且难以调试。
  • 未定义行为:在底层,这种并发访问可能破坏数据结构的内在一致性,引发程序崩溃或其他严重错误。

Rust编译器的保护机制

现在,让我们看看Rust编译器如何应对这种危险的代码。当我们尝试编译上面的代码时,编译器会立即阻止我们。

编译器会给出类似这样的错误信息:

use of moved value: data``
value moved into closure here, in previous iteration of loop

这个错误的核心原因是:向量 Vec<T> 类型没有实现 Copy trait。在循环的第一次迭代中,data 的所有权已经被移动(move)到了第一个线程的闭包中。在后续的迭代中,我们无法再次使用已经移动所有权的 data。Rust的所有权系统通过这种方式,在编译阶段就阻止了多个线程同时持有对同一数据的可变引用,从根本上杜绝了数据竞争的可能性。

如果我们无视错误强制编译,Rust编译器会坚定地说“不”。这体现了Rust的设计哲学:宁愿在编译时让程序员感到不便,也要避免在运行时发生难以追踪的错误。

安全的解决方案:使用互斥锁(Mutex)

既然直接共享可变引用行不通,那么如何安全地在多线程间共享数据呢?解决方案是使用同步原语,例如互斥锁(Mutex)。

互斥锁(Mutual Exclusion)的核心思想是:它像一把钥匙,一次只允许一个线程访问被保护的数据。线程在访问数据前必须先获取锁(拿到钥匙),操作完成后释放锁(归还钥匙),其他线程才能继续获取。

以下是使用 std::sync::Mutex 重写的安全版本:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 1. 用 Mutex 包装数据
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    let mut handles = vec![];

    for i in 0..3 {
        // 2. 克隆 Arc 指针,增加引用计数
        let data_ref = Arc::clone(&data);
        let handle = thread::spawn(move || {
            // 3. 获取锁,得到一个守护(Guard)对象
            let mut data_guard = data_ref.lock().unwrap();
            // 4. 通过守护对象安全地修改数据
            data_guard[i] += 1;
            // 5. 作用域结束,守护对象被丢弃,锁自动释放
        });
        handles.push(handle);
    }

    // 等待所有线程结束
    for handle in handles {
        handle.join().unwrap();
    }

    // 打印最终结果
    println!("Result: {:?}", *data.lock().unwrap());
}

以下是这段代码的关键步骤解析:

  1. 包装数据:使用 Mutex::new 将向量保护起来,再放入 Arc(原子引用计数)中。Arc 允许数据在多线程间安全地共享所有权。
  2. 共享所有权:在每个循环中,通过 Arc::clone 创建指向同一数据的新的智能指针,其引用计数会增加。
  3. 获取锁:在线程内部,调用 lock() 方法尝试获取互斥锁。如果成功,则返回一个 MutexGuard 守护对象。
  4. 安全访问:通过这个守护对象,我们可以安全地修改内部的数据。此时,其他线程试图获取锁会被阻塞。
  5. 自动释放:当 data_guard 离开作用域被销毁时,锁会自动释放,其他线程可以继续竞争锁。

总结

本节课中我们一起学习了Rust如何通过其所有权系统和类型系统,在编译期防止数据竞争。我们首先分析了一段会导致数据竞争的危险代码,并理解了其潜在危害。接着,我们看到了Rust编译器如何果断地阻止此类代码的编译。最后,我们介绍了使用 MutexArc 来安全实现多线程数据共享的标准模式。这正是Rust语言的核心优势之一:它将并发编程中极易出错的问题转化为编译时错误,迫使开发者以安全的方式构建高并发、高并行的程序,这对于数据工程和DevOps等领域至关重要。

032:高可用性概念

在本节课中,我们将学习高可用性的核心概念。我们将通过一个中世纪的城堡比喻来理解计算机系统如何设计才能持续运行,即使面对故障或攻击。

🏰 城堡的高可用性设计

上一节我们介绍了高可用性的重要性,本节中我们来看看一个中世纪城堡如何体现高可用性设计理念。

一个安全的城堡会被设计得具有韧性。这意味着它可能拥有加固的城墙。因此,即使其中一面城墙暂时被攻破,它也会通过其他方式得到加固,而不会立刻倒塌。

城堡还会设计逃生隧道,这是防止被孤立的有效方法。假设城堡被围困,一条秘密逃生隧道可以让人们获取补给,或者乘船前往其他地方再返回。

此外,一口深井也是使城堡具备高可用性的方法。因为深井能够提供水源,即使城堡被围困。这实际上是一种在建造城堡时就提前考虑的理念,思考可能发生的情况,并确保城堡始终可用,不会因攻击或围困而崩溃。需要考虑能否撤离,以及能否在城堡内继续生存。

💻 计算机系统的高可用性

计算机系统与此非常相似,它需要设计一个能够真正应对高可用性事件的系统。

以下是实现高可用性的一些关键策略:

  • 多重网络连接:防止连接中断。
  • 冗余电源供应:应对停电。
  • 备用电池系统或太阳能供电:确保持续供电。
  • 镜像服务器:如果一台服务器故障,另一台可以确保服务连续性。
  • 基础设施的地理分布:限制局部故障的影响范围。

负载均衡器会分布在不同的资源上。当出现扩展性问题时,系统可以弹性地适应需求的激增。

🎯 高可用性的核心目标

实现高可用性,本质上意味着承认故障必然会发生——无论是在城堡还是计算机系统中。因此,设计目标如下:

  • 消除单点故障:你不希望存在一个一旦失效就导致整个系统崩溃的组件。
  • 消除停机时间:通过某种故障转移能力来实现。例如,在城堡的比喻中,停机时间意味着你无法逃脱,这会带来严重问题。因此,你会设计一个知道如何“逃生”的系统,就像备用发电机启动或数据库复制一样,这在计算机世界是类似的概念。

一个恰当的高可用性设计能够经受住风暴,拥有安全网。凭借稳健的架构,你的系统将能像城堡一样安全,并具备城堡级别的韧性。

本节课中我们一起学习了高可用性的基本理念,通过城堡的比喻理解了消除单点故障和设计故障转移机制的重要性,这些是构建可靠计算机系统的基石。

033:同音替换密码技术解析 🔐

在本节课中,我们将要学习如何用Rust语言实现一种经典的加密技术——同音替换密码。我们将解析一段Rust代码,理解它如何通过为每个字母生成多个随机“同音”替代字符来加密文本,从而使密文更难被频率分析破解。

概述

同音替换密码是一种加密方法,它通过将明文字母替换为多个可能的密文字符来增强安全性。这与简单的单字母替换密码不同,后者每个明文字母只对应一个固定的密文字母。同音替换使得密文分析更加困难,因为同一个字母在密文中可能以不同形式出现。

代码结构与核心概念

上一节我们介绍了同音替换密码的基本概念,本节中我们来看看具体的Rust实现。核心数据结构是一个哈希映射(HashMap),用于存储从明文字母到其多个密文替代字符的映射关系。

以下是实现加密的主要函数 homophonic_cipher 的签名:

fn homophonic_cipher(plain_text: &str) -> (HashMap<char, Vec<char>>, String)

该函数接收一个明文字符串,返回一个包含映射关系的哈希映射和加密后的密文字符串。

实现步骤解析

现在,让我们一步步解析代码是如何构建这个加密系统的。

  1. 生成字母表与随机同音字
    代码首先生成英文小写字母表(‘a’到‘z’),并为每个字母生成一组随机的同音替代字符。这通过随机数生成器完成,为每个字母创建了多个可能的“替身”。

  2. 构建加密映射
    接下来,程序将每个字母与其对应的同音字列表关联起来,构建出完整的 HashMap<char, Vec<char>>。这个映射是加密和解密的关键。

  3. 执行加密过程
    对于输入明文中的每个字符,程序执行以下操作:

    • 如果字符在映射中(即它是小写字母),则从其同音字列表中随机选取一个作为密文。
    • 如果字符不在映射中(如空格、标点),则原样保留。
      这个过程生成了最终的密文字符串。

  1. 输出与返回
    代码会打印出明文、密文以及详细的字母映射表。最后,函数将映射和密文一并返回。

运行示例与结果分析

最后,我们通过运行程序来观察实际效果。在终端执行 cargo run 后,程序会输出加密结果。

例如,对于明文 “the quick brown fox jumps over the lazy dog”,程序可能生成如下密文(每次运行结果不同):

Plain text: the quick brown fox jumps over the lazy dog
Cipher text: v!k rwslp ygqtm hqz dlfao qxkt v!k pjnw uq#

更重要的是,它会输出映射表。以下是一个简化的示例映射:

  • t -> ['v', '!', 'k']
  • h -> ['r', 'w']
  • e -> ['s', 'l', 'p']

这个映射表揭示了加密的本质:明文字符 ‘t’ 在密文中可能被替换为 ‘v’‘!’‘k’ 中的任意一个。这种“一对多”的关系正是同音替换密码安全性的来源。它使得基于字母出现频率的密码分析技术失效,因为攻击者无法确定密文中高频出现的字符具体对应哪个明文字母。

总结

本节课中我们一起学习了同音替换密码的原理及其Rust实现。我们了解到,这种密码通过为每个字母引入多个随机替代字符,增加了加密的随机性和破解难度。通过分析具体的代码,我们看到了如何利用 HashMap 和随机数生成器来构建加密映射,并逐步将明文转换为密文。这个项目生动地展示了如何用现代编程语言来模拟和实践历史上的加密技术。

034:凯撒密码解密技术揭秘 🔐

在本节课中,我们将一起探索密码学的神秘世界,具体学习如何使用Rust编程语言实现经典的凯撒密码。我们将从加密和解密的基本概念入手,通过一个简单的Rust程序来演示整个过程。课程内容将涵盖代码结构、核心逻辑,并最终运行程序以验证结果。

加密函数解析

上一节我们介绍了凯撒密码的基本概念,本节中我们来看看实现加密功能的核心代码。加密函数是执行字母替换这一神秘艺术的关键,它会根据指定的偏移量移动每个字母,从而隐藏原始内容。

以下是lib.rs文件中加密函数encrypt的代码片段:

pub fn encrypt(text: &str, shift: u8) -> String {
    text.chars()
        .map(|c| {
            if c.is_ascii_alphabetic() {
                let base = if c.is_ascii_lowercase() { b'a' } else { b'A' };
                (((c as u8 - base + shift) % 26) + base) as char
            } else {
                c
            }
        })
        .collect()
}

该函数接收两个参数:待加密的文本text和偏移量shift。偏移量决定了字母替换的规则。例如,如果起始字母是A,偏移量为2,则加密后的字母为C;偏移量为3,则结果为D。函数内部会遍历文本中的每个字符,将其转换为小写,并根据偏移量进行移位计算。

解密函数原理

理解了加密过程后,解密就变得相对简单。解密本质上就是加密的逆过程,只需将字母向相反方向移动相同的偏移量即可。

以下是解密函数decrypt的代码:

pub fn decrypt(text: &str, shift: u8) -> String {
    encrypt(text, 26 - (shift % 26))
}

可以看到,解密函数巧妙地复用了加密函数。通过传入26 - (shift % 26)作为新的偏移量,即可实现反向移位,从而恢复出原始文本。这里的26代表英文字母的总数。

程序运行与验证

现在,让我们看看如何在主函数中调用这些功能,并观察凯撒密码的实际效果。

以下是main.rs文件中的主要内容:

use caesar_cipher::{encrypt, decrypt};

fn main() {
    let plain_text = "the quick brown fox jumps over the lazy dog";
    let shift = 3;

    let encrypted = encrypt(plain_text, shift);
    println!("加密后的文本: {}", encrypted);

    let decrypted = decrypt(&encrypted, shift);
    println!("解密后的文本: {}", decrypted);
}

程序中硬编码了明文"the quick brown fox jumps over the lazy dog"和偏移量3。运行程序后,我们会先得到加密后的乱码文本。例如,字母T偏移3位后会变成W。如果我们知道偏移量是3,理论上可以手动推算出原始信息。最后,程序调用解密函数,可以成功恢复出原始的明文。

总结

本节课中我们一起学习了凯撒密码在Rust中的实现。我们分析了加密和解密两个核心函数的代码逻辑,了解了偏移量如何决定字母的替换规则。通过运行示例程序,我们验证了加密文本可以通过已知的偏移量被成功解密。这个项目是一个很好的概念验证,展示了使用Rust构建密码学相关工具的简洁性与直观性,非常适合初学者进行实践和探索。

035:解码环制作实践指南 🔐

在本节课中,我们将要学习如何利用Rust语言构建一个实用的工具,用于分析和破解凯撒密码。我们将从理解凯撒密码的基本原理开始,逐步深入到如何通过统计分析来猜测加密时使用的偏移量,并最终实现一个完整的命令行工具。

凯撒密码简介

上一节我们介绍了课程的整体目标,本节中我们来看看凯撒密码是什么。

凯撒密码是最简单的加密技术之一。它的原理是将字母表中的每个字母按照一个固定的数字进行偏移,从而对信息进行编码。

例如,当偏移量为3时,字母A会被加密为DB会被加密为E,以此类推。

工具设计与功能概述

了解了凯撒密码的原理后,本节中我们来看看如何构建一个工具来应对它。

我构建的这个工具不仅能创建密码和加密信息,还能使用统计分析来检测密码最可能使用的偏移量。其核心思路是:如果你能找出最可能的偏移量,并设计一种评估概率的指标,你就能推断出原始信息。这种方法在处理大数据问题时尤其有用,例如,在尝试所有可能的暴力破解方法之前,可以先对数据进行采样分析,就像本示例所做的那样。

核心实现:统计分析

上一节我们介绍了工具的整体功能,本节中我们来看看其核心的统计分析是如何实现的。

工具的核心在于利用字母频率分析来猜测偏移量。在英语中,大约80%的字母遵循特定的频率分布。

以下是英语字母的初始频率哈希映射示例:

let mut frequencies: HashMap<char, f64> = HashMap::new();
frequencies.insert('e', 12.7);
frequencies.insert('t', 9.1);
frequencies.insert('a', 8.2);
// ... 其他字母

工具会统计密文中每个字母的出现频率,然后与标准英语字母频率进行比较。通过计算所有可能偏移量(0到25)下的匹配分数,分数最高的偏移量就是最可能的答案。

以下是猜测偏移量的核心逻辑:

fn guess_shift(ciphertext: &str, frequencies: &HashMap<char, f64>) -> i32 {
    let mut best_shift = 0;
    let mut highest_score = 0.0;
    for shift in 0..26 {
        let score = calculate_score(ciphertext, shift, frequencies);
        if score > highest_score {
            highest_score = score;
            best_shift = shift;
        }
    }
    best_shift
}

工具使用演示

理解了核心原理后,本节中我们来看看这个工具具体如何使用。

这是一个命令行工具。启动后,通过--help参数可以查看使用说明。工具的主要功能是解密和进行频率分析。

以下是主要的使用命令:

  • 解密并猜测偏移量cargo run -- --message "加密文本" --guess
  • 仅进行频率分析cargo run -- --message "加密文本" --stats

例如,当我们输入一段密文并启用--guess模式时,工具会遍历所有26种可能的偏移,计算每种情况下的频率匹配分数,并输出得分最高的偏移量及其对应的解密结果。

实例分析

让我们通过一个具体例子来巩固理解。假设我们有一段密文。

运行工具并传入密文和--guess参数后,工具会输出类似以下的分析过程:

分析分数:
偏移 0: 分数 5
偏移 1: 分数 8
...
偏移 11: 分数 22
偏移 16: 分数 40 <-- 最高分
...
最高分数为40,最可能偏移量:16
解密后的消息:off to the bunker every person for themselves

从输出可以看出,偏移量16获得了最高的分数(40),因此被判定为最可能使用的加密偏移。使用该偏移量解密后,我们得到了原始信息。

凯撒密码的弱点与总结

本节课中我们一起学习了如何构建一个凯撒密码分析工具。

这个练习很好地演示了如何将密码学概念转化为实际的命令行工具。同时,它也揭示了凯撒密码的一个根本弱点:它对频率分析攻击是脆弱的。因为字母E在英语中出现的频率约为12.7%,通过分析密文中字母的分布,攻击者可以做出有根据的猜测并计算出匹配分数,这正是利用统计学解码信息的方法。

整个项目的实现,得益于Rust语言的优雅与强大功能。

036:使用SHA-3检测重复数据

在本节课中,我们将要学习如何在Rust中使用SHA-3哈希算法来检测文本数据中的重复项。我们将通过一个具体的例子,模拟从海明威的《老人与海》中提取短语,并生成随机重复的文本,最后使用SHA-3来识别和统计这些重复项。

概述:选择加密库

上一节我们介绍了Rust中加密库的选择。这里有一个关于Rust加密库从M2到M4再到M5的示例。这些都是你可以使用的不同库。这个特定的资源库很有用,因为它告诉了你安全级别。

请注意,它建议:对于新应用程序,或者在不考虑与其他标准兼容性的情况下,我们推荐使用Blake2、Sha2或Sha3。

在这个特定示例中,我们使用了Sha3。我们称其为较新的标准。我们可以看出,它在加密使用上非常安全。

添加项目依赖

既然知道了可以使用那个库,我将转到我的Cargo.toml文件。如果你想使用特定的依赖项,只需将其放入你的Cargo.toml文件中。当你运行cargo run时,它会自动拉取该依赖项。因此,拉取依赖项非常直接。

以下是依赖项的配置:

[dependencies]
sha3 = "0.10"
rand = "0.8"

构建模拟数据生成器

现在,关于我们要构建的内容,请注意我使用了其他库,如rand等。我在这里使用Sha3的目的是,我想从海明威的《老人与海》这本书中获取一系列短语。

我在这里设计了一个静态短语列表,并添加了10个字符串。我可以指定这是第一个短语,第二个短语,等等。

将这些短语组合在一起后,我想要做的是生成一堆随机短语。这在某种程度上是一种模拟,用于生成可能包含重复项的文本,这正是这里的核心思想。

以下是生成随机短语的函数:

fn generate_random_phrases(count: usize) -> Vec<String> {
    let static_phrases = vec![
        "He was an old man who fished alone.",
        "Everything about him was old.",
        "The sail was patched with flour sacks.",
        "His hope and his confidence had never gone.",
        "But now they were freshening.",
        "The clouds over the land now rose like mountains.",
        "The boat moved ahead slowly.",
        "He looked at the sky and saw the white cumulus built like friendly piles of ice.",
        "Then he looked ahead and saw the birds working.",
        "He was sorry for the birds."
    ];
    let mut rng = rand::thread_rng();
    (0..count).map(|_| {
        let idx = rng.gen_range(0..static_phrases.len());
        static_phrases[idx].to_string()
    }).collect()
}

这个函数接受一个数量参数,返回一个充满字符串的向量。我们使用静态短语列表,然后生成一堆消息到一个字符串向量中。

使用SHA-3创建唯一哈希

下一个函数是我们引入SHA-3能力的地方。我们将在这里创建一个唯一的哈希。

我们将使用这个哈希映射,并说明短语的总数。然后,对于短语列表中的每个短语,我们将为其创建一个唯一的哈希。这使我们能够快速识别重复项。

以下是使用SHA-3检测重复项的核心函数:

use sha3::{Digest, Sha3_256};
use std::collections::HashMap;

fn detect_duplicates(phrases: Vec<String>) {
    let mut phrase_counts: HashMap<String, usize> = HashMap::new();
    let mut hash_map: HashMap<String, String> = HashMap::new();

    for phrase in phrases {
        let mut hasher = Sha3_256::new();
        hasher.update(phrase.as_bytes());
        let hash_result = hasher.finalize();
        let hash_hex = format!("{:x}", hash_result);

        // 使用哈希值作为键来统计重复
        let count = phrase_counts.entry(hash_hex.clone()).or_insert(0);
        *count += 1;
        // 存储原始短语(可选,用于调试或输出)
        hash_map.entry(hash_hex).or_insert(phrase);
    }

    // 输出统计信息
    let total_phrases: usize = phrase_counts.values().sum();
    let unique_phrases = phrase_counts.len();
    let total_duplicates = total_phrases - unique_phrases;

    println!("总生成短语数: {}", total_phrases);
    println!("唯一短语数: {}", unique_phrases);
    println!("重复短语总数: {}", total_duplicates);
    println!("\n详细统计(哈希值 -> 出现次数):");
    for (hash, count) in &phrase_counts {
        if *count > 1 {
            println!("哈希 {}: 出现 {} 次", &hash[0..8], count); // 只显示哈希前8位以便阅读
        }
    }
}

然后,我们将列出一个唯一短语数量的列表,并放入一些更具描述性的统计数据,如总唯一重复项、总合并重复项,最后基本上在最后全部打印出来。

运行与结果分析

现在,如果我们转到main函数,看看会发生什么。这段代码将从短语列表中生成随机的重复短语。

在这个例子中,我们将看到生成了24个短语。它将给出唯一的缓存,并说明它能够检测到该短语的次数,以及有多少个唯一短语。

例如,在这个案例中,10个短语里只有9个是唯一的。重复项总共有14个合并的重复项。也就是说,在24个中有多少个重复短语。

如果我们仔细查看,我们可以实际检查这一点,并看到它全部协同工作。我只需要输入cargo run,我们就能看到这里发生了什么。

如果我们运行这段代码,我们将看到它生成了24个短语。我们可以看到这个短语出现了三次,下一个短语出现了三次,等等。因此,它能够看到总共有10个唯一短语,并且发现的重复项总共有8种不同类型。然后,如果你把这些加起来,总共有14个重复项。

所以,这里有24个短语,我们可以看到关于它的描述性统计数据。因此,这是一个很好的技术,取决于你正在解决什么样的问题。也许你正在尝试策划一个数据集,或者查看文件系统并删除某些文件。

幸运的是,使用Rust可以非常容易地构建一个强大的命令行工具,可以检测文本和二进制文件中的重复项,这一切都是通过现有的库完成的。你也可以放心,分发这个工具会很容易,因为你可以进行基于二进制的部署。

总结

本节课中我们一起学习了如何在Rust项目中使用SHA-3哈希算法。我们首先了解了如何选择合适的加密库,然后在Cargo.toml中添加依赖。接着,我们构建了一个模拟数据生成器来创建可能包含重复项的文本。核心部分是利用SHA-3为每个短语生成唯一哈希值,并通过哈希映射快速识别和统计重复项。最后,我们运行程序并分析了输出结果,展示了该技术在数据去重和数据集整理中的实际应用。这种方法高效、安全,并且得益于Rust的生态系统,易于实现和分发。

037:事件响应流程详解 🏰

在本节课中,我们将通过一个中世纪城堡的比喻,来学习现代网络安全中的事件响应流程。我们将了解一次攻击如何触发一系列防御动作,以及如何通过计划、响应和分析来保护系统安全。

上一节我们介绍了网络安全的基础概念,本节中我们来看看当攻击真正发生时,一个有效的响应计划是如何运作的。

城堡事件响应流程 🛡️

下图展示了一个中世纪城堡的事件响应流程图。我们将分析一次针对城堡的攻击会实际触发哪些步骤。

拥有一个有效的事件响应计划对于保卫城堡至关重要。以下是响应流程中的关键步骤。

以下是事件响应流程的六个核心阶段:

  1. 警报:哨兵发现入侵迹象,立即拉响警报。
  2. 协调:指挥官根据警报,协调并制定防御策略。
  3. 评估:评估造成的损害和攻击者的入口点。例如,城墙是否被攻破,护城河是否需要修复。
  4. 遏制:部署应对措施,以最小化攻击的影响。
  5. 取证:事件得到控制后,进行深入取证分析,查明攻击者是如何渗透外围防御的。
  6. 改进:从事件中吸取教训,改进未来的安全防护和响应能力。

通过训练让守卫做好准备,以便未来能快速识别并应对类似事件。充分的准备对于保卫城堡至关重要。

现代网络安全的事件响应 🔐

现代网络安全方法论依赖于类似的事件响应程序。其核心逻辑可以概括为:检测 -> 响应 -> 分析 -> 改进

以下是现代网络安全事件响应的关键组成部分:

  • 自动检测:入侵检测系统(IDS)等控制措施会自动向IT团队发出关于可疑活动的警报。
  • 计划文档:响应计划会详细记录分析、遏制安全事件以及从事件中恢复的程序。
  • 团队协作:根据事件的严重程度,可能还需要与法律和公关团队进行协调。
  • 损害最小化:强大的事件响应能力可以最大限度地减少攻击造成的损害。
  • 趋势分析:通过分析攻击趋势,可以发现系统漏洞,从而改进防御措施。
  • 合规报告:为了验证是否符合法规要求,良好的事件报告也至关重要。

总而言之,通过训练和测试进行准备,能够构建有效的“响应肌肉记忆”。这使你能够凭借本能快速反应。你必须确保你的“城堡”能够随时检测并化解任何潜在的入侵。

本节课中,我们一起学习了从古典比喻到现代实践的事件响应完整流程。一个结构化的响应计划是有效网络防御的基石,它不仅能控制损失,更能通过持续改进来提升整体的安全水位。

038:合规性管理 🏰

在本节课中,我们将通过一个中世纪的城堡比喻,来学习计算机系统中的合规性管理概念。我们将了解合规性的重要性、如何实施控制措施,以及如何通过审计和自动化来确保系统符合法规要求。

城堡中的合规性

让我们通过一个中世纪城堡的视角来看待合规性。如果你想遵守国王颁布的法令,城堡本身必须拥有执行这些法令的方法。

合规性的执行与验证

法令可能规定所有水在使用前都必须煮沸,这可以保护城堡内每个人的安全。因此,你需要审计员。这些审计员将为法规以及其他事项(例如税收)提供一定程度的验证。毕竟,必须有人为审计员支付报酬,而这通常通过税收来完成,所以你也需要验证这些税收技术。相关文件将证明合规工作的成效。

以下是合规性执行的具体步骤示例:

  • 记录与通知:例如,在主井内,每次取水后都必须进行取水通知。煮沸后,可能需要进行另一次通知。
  • 清单管理:某处会有一份检查清单。
  • 人员培训:守卫们会接受相关法规的培训。
  • 问题整改:在检查员到来之前,会识别并补救不足之处。

计算机系统中的合规性

上一节我们看到了城堡如何执行合规,在计算机系统中,情况非常相似。你必须遵守诸如HIPAA或PC DSS之类的规则。不合规将导致罚款,甚至失去公众信任。

框架会概述所需的控制措施和政策,并实施方法来证明对法规的遵守。

以下是确保计算机系统合规性的关键方法:

  • 审计与证据:审计是一种非常有效的控制手段。像系统日志这样的证据可以向当局证明合规性,并使你能够跟上不断发展的法规。
  • 自动化策略执行:自动执行策略是减少错误的好方法,检查清单是其中较好的方式之一。这能确保即使人员发生变动,工作的连续性也能得到保障。
  • 漏洞评估:漏洞评估也将识别出差距。

合规性的价值

总而言之,合规性将提供问责制和法律保证,并实施所需的控制措施,以便客户和监管机构都能信任该系统。这将为业务运营创造顺畅的环境。

本节课中,我们一起学习了合规性管理的核心概念。我们通过城堡的比喻理解了合规的必要性,探讨了在计算机系统中通过审计、自动化执行和漏洞评估来确保合规的具体方法,并最终认识到合规性对于建立信任、保证业务平稳运行的重要价值。

039:并发核心概念 🧠

在本节课中,我们将通过一个繁忙的餐厅厨房的例子,来学习并发编程中的四个核心概念。这些概念对于理解现代编程语言(如Rust)如何高效、安全地处理多任务至关重要。


概念一:并行与并发 ⚙️

首先,我们来区分并行与并发这两个概念。它们都涉及同时处理多个任务,但实现方式不同。

  • 并行 指的是多个任务真正同时执行,这通常需要多个处理器或核心。在厨房的例子中,这就像有多位厨师,每位厨师可以同时独立地准备一道菜的不同部分。
    • 公式:并行任务数 ≤ 处理器核心数
  • 并发 指的是一个处理器通过快速切换,交替执行多个任务,从而在宏观上看起来这些任务是同时进行的。这就像只有一位厨师,他需要轮流照看烤架上的牛排、搅拌土豆泥并切沙拉。

上一节我们介绍了并发的总体概念,本节中我们来看看实现并发时常用的几种机制。


概念二:同步原语 🔒

同步原语是用于协调多个并发任务执行顺序的工具,防止它们同时访问共享资源而导致混乱。

在餐厅的例子中,一个取号机就是一个完美的同步原语。它让顾客排队取号,并按号码顺序接受服务。这避免了在柜台前造成拥挤,甚至限制了能进入用餐区的人数。

以下是同步原语的核心作用:

  • 取号机就是同步机制本身。
  • 每位顾客拿到一个号码(相当于获取一个锁或令牌)。
  • 只有当用餐区有空位且收银员有空时,对应的号码才会被叫到(相当于资源可用,任务被调度执行)。

这与编程中许多并发问题类似:将工作放入一个队列,每个线程一次从队列中取走一项任务进行处理,而这个队列本身就在协调对共享资源的访问。


概念三:消息传递 📨

消息传递是并发编程的另一个核心范式,其核心思想是任务之间通过发送消息来通信,而不是直接共享内存。

在厨房中,不同的工作站(如烤架区、油炸区、沙拉区)是松散耦合的。它们不直接协同工作,但需要进行通信。当一个工作站完成其菜品后,它会将成品放在出餐台上。

  • 代码示例(伪代码):
    // 烤架厨师完成任务
    grill_station.send(CookedSteak);
    // 服务员从出餐台获取完成的任务
    let completed_dish = counter.receive();
    

这个出餐台就是一种消息传递的媒介。工作站只需将完成的消息(菜品)放到台上,而无需知道谁来取走它。服务员经过时,会取走各个独立的菜品。这样,各个任务之间除了最初获取订单外,几乎不需要直接通信。


概念四:竞态条件 🏁

竞态条件是指多个任务以非预期的、依赖于执行时序的顺序访问共享数据或资源,从而导致程序行为出现错误的情况。我们必须防止这种情况发生。

在餐厅中,一个典型的竞态条件是:两位顾客同时点餐。如果服务员偷懒,假设食物一出餐就递给顾客,那么当两份相似的餐点同时准备好时,服务员可能会把错误的食物递给错误的顾客。这在现实中确实会发生。

这个概念告诉我们,必须通过使用同步原语、消息传递或其他技术来防止竞态条件,避免这种错误的交互。


总结 📝

本节课中,我们一起学习了并发编程的四个核心概念:

  1. 并行与并发:多位厨师体现并行,一位厨师 multitasking 体现并发
  2. 同步原语:取号机强制执行顺序,实现同步
  3. 消息传递:通过出餐台在不同工作站间传递订单,展示了消息传递
  4. 竞态条件:顾客点餐顺序混淆可能导致竞态条件,带来并发风险。

你可以看到,这些情况在编程中经常出现。这就是为什么像 Rust 这样的语言如此强大——它们能更轻松地防止其中一些问题(尤其是数据竞争)的发生。这也正是我们在现代编译型语言中所期待看到的特性。

040:哲学家就餐问题 🍽️

在本节课中,我们将学习一个经典的并发编程问题——哲学家就餐问题。我们将探讨如何使用Rust的并发原语来模拟和解决这个问题,并理解如何避免死锁等并发陷阱。

概述

哲学家就餐问题是一个经典的并发问题,它模拟了多位哲学家围坐在圆桌旁,每位哲学家需要两把叉子才能进餐的场景。问题的关键在于设计一种机制,让所有哲学家都能公平地共享有限的叉子,同时避免死锁。我们将使用Rust的 MutexArc 来实现这个模拟,并学习如何对并发程序进行基准测试。

问题核心

这个问题的有趣之处在于,你需要为围坐在桌旁的一群人(在本例中是哲学家)找到一种共享一定数量叉子的方法。这个特定问题能够暴露诸如死锁等并发问题。为了防止这些问题,你可以采取的措施之一是使用正确的并发形式。

在本例中,Mutex 将提供对叉子的独占访问权。这些是进餐用的叉子。Arc 允许在哲学家之间共享叉子。

模拟将打印开始时间、进餐方向和所有哲学家的总时间。总时间大约等于哲学家数量除以叉子数量,这个数字代表了可以并发进餐的人数。

关键技术

在进行并发模拟时,对其进行基准测试以确保实际情况符合预期至关重要。如果你引入了多个线程但速度没有提升,那就可能存在问题。

在本例中,我们将使用以下技术:

  • Mutex 用于表示对叉子的独占访问。
  • Mutex 包装在 Arc 中,以便在线程间共享。
  • 为哲学家编号,并规定先获取编号较小的叉子。
  • 最后,为整个模拟进行计时。

再次强调,打印出时间加速比非常关键。并发会带来收益递减,这被称为阿姆达尔定律。当存在等待时,并发的收益就会递减。因此,你需要仔细检查是否值得将某些任务改为多线程。

代码实现

在这个场景中,我们将使用标准库中的 ArcMutexthreadduration

首先,我们实现一个 Fork 结构体。这是哲学家将要使用的物品。

use std::sync::{Arc, Mutex};

struct Fork {
    id: usize,
    // Mutex 确保一次只有一个哲学家能拿起这把叉子
    _mutex: Mutex<()>,
}

接着,我们实现一个 Philosopher 结构体。

struct Philosopher {
    name: String,
    left_fork: Arc<Mutex<()>>,
    right_fork: Arc<Mutex<()>>,
}

在这里,我们创建了哲学家的实例。我们还有一个 eat 方法,其中展示了他们需要以一种能真正防止死锁的方式拿起叉子的逻辑。我们还包含了一些调试信息,这在最初构建并发代码时非常有帮助。

impl Philosopher {
    fn eat(&self) {
        // 先拿起编号较小的叉子,以防止循环等待(死锁)
        let (first, second) = if self.left_fork.id < self.right_fork.id {
            (&self.left_fork, &self.right_fork)
        } else {
            (&self.right_fork, &self.left_fork)
        };

        let _first = first._mutex.lock().unwrap();
        println!("{} 拿起了左边的叉子。", self.name);
        let _second = second._mutex.lock().unwrap();
        println!("{} 拿起了右边的叉子并开始进餐。", self.name);

        // 模拟进餐时间
        std::thread::sleep(std::time::Duration::from_millis(100));

        println!("{} 完成了进餐。", self.name);
    }
}

拥有打印或日志语句(日志更好)非常重要,这样你才能确切知道发生了什么。实际上,如果你使用 log::debug,你可以在开发时调试它,然后在生产环境中不显示这些消息。但如果出现问题需要进一步开发,你可以重新启用调试方法。

主程序逻辑

以下是 main 方法,它展示了我们餐桌上只有四把叉子,并将打印相关信息。然后,我们将围绕叉子应用 Mutex,以防止共享访问叉子时引发像死锁这样的严重问题。

fn main() {
    let forks: Vec<Arc<Mutex<()>>> = (0..4)
        .map(|_| Arc::new(Mutex::new(())))
        .collect();

    let philosophers = vec![
        Philosopher::new("亚里士多德", forks[0].clone(), forks[1].clone()),
        Philosopher::new("柏拉图", forks[1].clone(), forks[2].clone()),
        Philosopher::new("苏格拉底", forks[2].clone(), forks[3].clone()),
        Philosopher::new("笛卡尔", forks[3].clone(), forks[0].clone()), // 注意循环依赖
    ];

    let handles: Vec<_> = philosophers
        .into_iter()
        .map(|p| {
            std::thread::spawn(move || {
                p.eat();
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

这里我们看到所有的哲学家。这里有一个向量,包含了我们希望坐在餐桌旁的所有人。然后,我们将他们放入我们的集合中。最后,我们在这里生成线程,以便每个人可以同时开始进餐。最终,我们将一起打印所有信息。

运行与验证

关键要点是,Mutex 很有帮助,它使得每个人都可以尝试进餐,但关于谁在特定时间能拿到叉子有一些逻辑。让我们运行这个程序。

如果我们进入 dining_philosopher 目录并输入 cargo run,我们可以看到模拟结果。同样,在进行多线程编程时,构建用于了解确切发生情况的检测机制至关重要。例如,哲学家需要能够同时拿起两把叉子,我们正在调试这一点。我们还可以在他们完成、开始时以及最后打印消息。最后,你应该进行基准测试,并思考:有多少位哲学家?我们大约可以看到,实际上Rust会告诉我们这里的确切哲学家列表。然后我们查看时间,由于我们能够使用多线程编程来加速工作,我们应该达到一定的效率水平。

总结

总而言之,这是使用Rust语言来研究和实践一些经典并发问题的绝佳方式。Rust拥有许多安全技术,可以防止你编译已知错误的代码。它虽然不能防止所有错误,但这是一个很好的思路,或许可以用于现实世界项目中更高级别的并发处理。

本节课中,我们一起学习了哲学家就餐问题的背景与挑战,并使用Rust的 ArcMutex 实现了解决方案。我们了解了如何通过规定获取锁的顺序(如先拿编号小的叉子)来避免死锁,并强调了在并发编程中添加日志和进行基准测试的重要性。通过这个实例,我们看到了Rust如何帮助构建安全、高效的并发程序。

041:使用Rayon并行爬取维基百科 🚀

在本节课中,我们将学习如何利用Rust的Rayon库,通过多线程并行处理来加速网络请求任务。我们将通过一个具体的例子——并行爬取和处理多个维基百科页面——来演示其工作原理和优势。

概述

我们将构建一个程序,该程序能够同时获取多个维基百科页面的内容,并对每个页面的文本进行处理。为了验证并行化带来的性能提升,我们还会收集并输出相关的计时指标。核心在于,我们将使用Rayon库来简化多线程编程的复杂性,让它自动为我们管理线程池和任务分发。

项目依赖

首先,我们来看一下实现此功能所需的项目依赖。以下是Cargo.toml文件中的关键部分:

[dependencies]
wikipedia = "..."  # 用于获取维基百科页面
rayon = "..."      # 用于实现并行迭代

这里我们引入了两个外部库:wikipedia用于获取页面内容,rayon则为我们提供了强大的并行迭代能力。

核心代码解析

上一节我们介绍了项目依赖,本节中我们来看看具体的代码实现。程序的核心逻辑可以分为三个部分:定义要处理的页面列表、并行获取并处理页面、最后输出性能指标。

定义页面列表

我们首先定义一个包含多个维基百科页面标题的列表。在这个例子中,我们选取了九位NBA球员的页面。

let pages = vec![
    “Michael_Jordan”,
    “LeBron_James”,
    “Kobe_Bryant”,
    // ... 其他六位球员
];

并行处理页面

这是所有工作发生的地方。我们使用Rayon将普通的迭代转换为并行迭代,从而为每个页面的获取和处理操作启动一个线程。

以下是处理单个页面的函数:

fn process_page(page_title: &str) -> (String, Duration) {
    let start = Instant::now();
    // 获取页面内容
    let page = wikipedia::page(page_title);
    // 处理页面文本内容(例如提取第一句)
    let content = page.get_content().unwrap();
    let first_sentence = extract_first_sentence(&content);
    let duration = start.elapsed();
    (first_sentence, duration)
}

接下来是关键步骤,我们使用Rayon的par_iter()方法对页面列表进行并行处理:

use rayon::prelude::*;

let results: Vec<_> = pages
    .par_iter() // 关键:将顺序迭代转换为并行迭代
    .map(|page_title| process_page(page_title))
    .collect();

与普通的.iter()相比,.par_iter()会自动在后台线程池中并行执行map中的操作。Rayon会智能地管理线程数量,以充分利用系统资源。

输出性能指标

处理完成后,我们计算并输出各项指标,这对于验证并行化的效果至关重要。

以下是需要计算的指标列表:

  • 单页处理时间:每个页面获取和处理所花费的时间。
  • 总耗时:从开始到所有页面处理完毕的总时间。
  • 平均每页耗时:总耗时除以页面数量。
  • 处理的页面总数:用于验证所有任务均已完成。
  • 使用的线程数:这是一个关键指标,用于确认程序确实在并发执行。
let total_time: Duration = results.iter().map(|(_, d)| *d).sum();
let avg_time = total_time / pages.len() as u32;
let threads_used = rayon::current_num_threads();

println!(“总耗时: {:?}“, total_time);
println!(“平均每页耗时: {:?}“, avg_time);
println!(“处理页面总数: {}“, pages.len());
println!(“实际使用线程数: {}“, threads_used);

运行与结果分析

现在我们已经完成了所有代码,让我们运行它看看效果。程序执行速度极快,几乎瞬间就处理完了九个维基百科页面。

输出结果会显示从每个页面(例如迈克尔·乔丹页面)提取的句子示例。更重要的是,性能指标会清晰地展示出来:

  • 总耗时远小于顺序执行九个任务的时间总和。
  • 平均每页耗时提供了一个基准参考。
  • 实际使用的线程数证明了并发确实发生。

这些描述性统计数据为我们提供了一个绝佳的基准,帮助我们判断当前的并行方案是否高效。多线程编程中最重要的环节之一就是进行基准测试,以确保你的改进确实提升了性能,而不是增加了不必要的复杂性或反而导致速度变慢。

总结

本节课中我们一起学习了如何利用Rust的Rayon库进行并行编程。我们通过一个并行爬取维基百科页面的实例,掌握了以下核心要点:

  1. 如何使用rayon库的.par_iter()方法轻松地将顺序任务并行化。
  2. 如何构建一个包含网络请求和数据处理的任务单元(process_page函数)。
  3. 如何收集和计算关键的计时与性能指标,以科学地评估并行化效果。

Rayon库的强大之处在于,它抽象了线程管理的复杂性,让开发者能够专注于业务逻辑,同时智能地利用多核CPU资源来提升程序性能。

042:基于Tokio的智能聊天机器人

在本节课中,我们将学习如何使用Rust语言及其强大的异步运行时库Tokio,构建一个能够与大型语言模型(如OpenAI API)交互的智能聊天机器人。我们将分析一个示例项目的核心代码结构,了解其如何实现对话循环、处理异步请求,并最终运行一个功能完整的聊天程序。

概述

这个聊天机器人项目利用了Rust生态系统中一些优秀的库。它实现了一个与AI助手进行对话的循环。虽然本示例使用的是OpenAI API,但其代码结构可以适配任何提供类似接口的大型语言模型。

核心代码结构分析

接下来,我们深入查看代码的主要组成部分。程序的核心是一个处理对话的主循环。

主循环:run_chat_loop

主循环负责管理整个对话流程。它的逻辑如下:

  1. 首先,请求用户输入。
  2. 接着,将输入发送给API。
  3. 然后,打印出API的响应。
  4. 最后,将用户输入和AI响应都追加到对话历史中。

以下是该循环的简化逻辑描述:

async fn run_chat_loop(api_client: &ApiClient) -> Result<()> {
    let mut conversation_history = Vec::new();
    loop {
        let user_input = read_user_input()?;
        let ai_response = call_api(&api_client, &conversation_history, &user_input).await?;
        println!("AI: {}", ai_response);
        conversation_history.push((user_input, ai_response));
    }
}

异步API调用:call_api

上一节我们介绍了对话循环,本节中我们来看看如何与外部服务通信。这是进行API调用的关键部分。

这个函数是异步的,这使得程序高效且可扩展。借助Tokio库,我们可以轻松处理大量并发请求。此部分代码不限于OpenAI,可以替换为任何云服务商、本地部署或开源的大型语言模型API。

辅助功能

call_api函数获取到原始响应后,我们需要对其进行解析。get_ai_response函数负责从API返回的JSON数据中提取出我们需要的文本内容。

此外,read_user_input函数用于从命令行获取用户输入的字符串。

项目依赖与组织

了解核心函数后,我们还需要关注项目的整体依赖和文件组织方式。

依赖项 (Cargo.toml)

以下是项目依赖的关键库:

  • reqwest:用于发起HTTP请求。
  • tokio:提供异步运行时。
  • serde:协助进行数据的序列化与反序列化。
  • serde_json:专门处理JSON格式。

这些依赖在Cargo.toml文件中定义,例如:

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

项目文件结构

该项目采用了一种可扩展的代码组织风格。

  • lib.rs:作为库文件,它暴露了主要的chatbot模块。这对于构建更大规模的项目非常有用。
  • main.rs:主程序文件。它非常简洁,主要作用是引入chatbot模块中的run_loop函数,配置API密钥和端点URL,然后异步地启动聊天循环。

这种结构使得main.rs中的代码极其精简:

use chatbot::run_loop;

#[tokio::main]
async fn main() -> Result<()> {
    let api_key = std::env::var("OPENAI_API_KEY")?;
    let endpoint = "https://api.openai.com/v1/chat/completions";
    run_loop(&api_key, endpoint).await
}

运行示例

现在,让我们将这个程序运行起来。首先,在项目根目录执行命令:

cargo run

程序启动后,会提示我们输入问题。例如,我们可以提问:“2000年奥运会男子100米短跑冠军是谁?”

AI会回答:“在2000年夏季奥运会上,男子100米短跑金牌由莫里斯·格林获得,成绩为9.87秒。”

我们可以继续追问:“莫里斯·格林是谁?”

AI会给出更详细的介绍:“他是一位前田径短跑运动员,主攻100米和200米项目。他在2000年夏季奥运会上以9.87秒的成绩赢得了100米金牌,同时也是100米项目的两届世界冠军。”

总结

本节课中我们一起学习了如何使用Rust构建一个基于Tokio的智能聊天机器人。我们分析了对话循环、异步API调用等核心组件的实现,了解了项目的依赖管理和文件组织。借助Rust语言的安全性和异步编程能力,以及Tokio运行时的高性能,我们能够用相对简洁的代码构建出功能强大且高效的聊天工具。Rust是构建聊天机器人的绝佳选择,它不仅性能优异,而且在完成初步验证后,可以轻松地扩展到生产环境中。

043:构建高性能数据工程工具

在本课程中,我们将学习如何使用Rust构建一个高性能、内存效率高的数据工程工具。我们将以实现一个多线程文件去重工具为例,逐步讲解核心概念和实现步骤。

概述

数据工程是经典的系统编程问题,而Rust是一门系统编程语言。与脚本语言相比,Rust的优势在于能够构建高性能、低内存占用且可移植的工具。本节我们将构建一个去重工具,其模式也可用于归档或数据转换等其他任务。

核心概念与工具链配置

上一节我们概述了目标,本节中我们来看看构建此类工具所需的核心能力和基础配置。

一个高效的项目通常需要配置良好的开发环境、构建流程和代码质量检查工具。

以下是构建此类工具时推荐的核心配置步骤:

  1. 开发容器:使用容器化环境(如Dev Container),确保项目可以在任何人的机器上快速启动和测试。
  2. 构建与发布:配置 Makefile,支持一键构建高性能的发布版本二进制文件。这使得分发工具变得非常简单,优于需要复杂安装说明的Python脚本。
    make build-release
    
  3. 代码质量:集成代码格式化(如 rustfmt)和代码检查(如 clippy)工具,确保代码风格一致且没有常见错误。
    make format
    make lint
    
  4. 测试:编写并运行测试,确保业务逻辑的正确性。
    make test
    

性能优势与设计目标

在配置好工具链后,我们来探讨Rust在数据工程中的关键优势。与Python等语言相比,Rust的核心优势在于其极致的性能和内存效率。

Rust程序可以比Python快70倍,并且由于线程间可以高效地共享内存,其内存占用极低。我们的设计目标是充分利用多核CPU(例如20核的Mac),而不是只使用单个核心。此外,工具还应包含进度条、可分发为便携式二进制文件,并且执行速度要非常快。

项目结构与代码解析

理解了设计目标后,现在让我们深入查看项目的具体代码实现。项目结构清晰,将核心逻辑放在库(lib.rs)中,而命令行工具(main.rs)则负责调用。

1. 遍历目录

首先,我们需要一个函数来遍历指定路径下的文件。这与Python的实现思路相似。

pub fn walk_directory(path: &str) -> Vec<String> {
    // ... 遍历目录并返回文件路径列表的代码
}

2. 模式匹配过滤文件

接着,我们可以根据文件扩展名等模式对文件列表进行过滤。

pub fn filter_files(files: Vec<String>, pattern: &str) -> Vec<String> {
    // ... 根据模式匹配过滤文件的代码
}

3. 计算校验和与进度反馈

核心步骤是计算每个文件的校验和(如MD5、SHA256)。这里我们引入了多线程和进度条。

pub fn compute_checksums(files: Vec<String>) -> HashMap<String, Vec<String>> {
    // 创建线程池
    let pool = ThreadPool::new(num_cpus::get());
    // 创建进度条
    let pb = ProgressBar::new(files.len() as u64);

    // ... 将任务提交到线程池,每个线程计算一个文件的校验和
    // ... 同时更新进度条
    // 返回一个映射:校验和 -> [文件路径列表]
}

代码的关键在于使用高效的线程池(如 rayon)并行处理文件,并使用 indicatif 库提供进度条,该进度条能与多线程环境协同工作。

4. 识别并报告重复项

最后,我们分析校验和结果,找出重复文件。

pub fn find_duplicates(checksums: HashMap<String, Vec<String>>) -> Vec<Vec<String>> {
    checksums
        .into_iter()
        .filter(|(_hash, files)| files.len() > 1) // 过滤出出现次数大于1的项
        .map(|(_hash, files)| files)
        .collect()
}

5. 命令行集成

main.rs 中,我们使用 clap 库来定义命令行接口,并将上述函数组合起来。

fn main() {
    let matches = App::new("rddupe")
        .subcommand(SubCommand::with_name("dedupe").about("Find duplicate files"))
        // ... 其他子命令
        .get_matches();

    match matches.subcommand() {
        ("dedupe", Some(_sub_m)) => {
            let files = walk_directory(".");
            let checksums = compute_checksums(files);
            let duplicates = find_duplicates(checksums);
            // ... 输出结果
        }
        // ... 处理其他命令
        _ => {}
    }
}

总结

本节课中我们一起学习了如何使用Rust构建一个高性能的数据去重工具。我们了解了Rust在数据工程中的核心优势:极致性能高效内存利用。通过分步解析,我们掌握了项目配置、目录遍历、并行计算校验和以及结果汇总的完整流程。这种将核心逻辑置于库中,并通过命令行调用的模式,是构建可分发、高性能系统工具的强有力范式。希望你能尝试运用此模式,构建属于自己的数据工程工具。

044:Python与Rust能效对比 💡⚡

在本节课中,我们将要学习一个关于编程语言选择的重要考量因素:能源效率与计算性能。我们将通过具体的研究数据,对比Python与Rust(及C语言)在这两方面的表现,并探讨这对于开发者和组织的实际意义。

概述

从Python转向Rust时,一个常见的误解是:“Python已经能用了,我不想节外生枝,还是继续用我熟悉的高效语言吧。”然而,本节将展示一个实际用例,说明在许多情况下,明智的开发者应当考虑Python语言的能效问题。我们将主要讨论两点:一项关于各语言能耗与时间对比的研究,以及Python在能效和计算性能上的排名。你会发现,在许多场景下,尤其是处理繁重的计算负载(如Web微服务中频繁序列化/反序列化JSON)时,并没有充分的理由坚持使用Python。考虑到地球正面临的明确问题,我们应当思考所编写代码的碳足迹。如果能够轻松地从一种语言切换到另一种(特别是借助Copilot等工具),至少值得考虑。本节的目标正是帮助你思考个人所用语言的能效问题,以便对未来行动做出明智决策。

能效排名研究 📊

以下是一项关于按能源效率对编程语言进行排名的非常引人入胜的研究。该领域已有大量研究,主要从能量角度比较编程语言的效率。

如果我们直接切入核心,可以看到两种最节能的语言(在对全局结果和不同操作进行标准化后)是C和Rust。

因此,实际上它们是等效的。就所有实际目的而言,提到C就等同于提到Rust,它们大致相同。

在计算时间方面,同样可以看到C和Rust之间的差异基本可以忽略不计。

Python的表现如何? 🐍

那么,这与地球上最流行的语言Python相比如何呢?

我们可以看到,事实上,就所有实际目的而言,Ruby、Python和Perl在能耗方面非常接近,在耗时方面也相当接近。

基本上,Python消耗的能源要多出约70倍。

在等效的解释型语言中也是如此。

在计算时间方面,可以看到在Python中完成某项任务所需的时间大约要长70倍。

在内存方面,Python也存在一些问题。与Rust相比,其内存使用量大约是两倍。这甚至还没有涉及多线程编程——由于全局解释器锁(GIL),Python无法跨多个核心进行真正的多线程操作,因此人们转而使用内存密集型操作“多进程”。

简而言之,如果你关心能源消耗和计算时间,Python在这两方面都是表现最差的语言之一。从可持续性角度来看,如果你的组织能够轻松切换到Rust,这应当成为一个考量因素。你是否会这样做?我认为这确实是组织应该提出的问题之一。

计算性能的考量 ⚙️

接下来,让我们深入探讨另一个关于计算性能的研究。

最近,我参加了谷歌David Patterson博士的一个讲座,他展示了这张完全相同的幻灯片,内容是关于矩阵乘法相对于原生Python的加速。

我实际上就这张幻灯片向他提了一个问题。他在这里提到,矩阵乘法的速度提升取决于你如何编写代码以及具体执行的操作,相对于原生Python代码,速度最高可提升62,000倍

因此,我们在这里真正看到的是,就计算性能而言,Python并不是一个理想的选择。人们应该考虑替代方案:我能否做到?我的组织能否做到?例如,我能否使用像Copilot这样的工具来帮助我提升到像Rust这样的语言?

如果我们观察语法,会发现Rust与Python并没有太大不同。而通过深入考虑速度提升和能源效率,你获得的收益不仅是可能影响预算——或许能为云服务节省50到70倍的计算成本——还能体现你对组织可持续性目标的深思熟虑。

总结

本节课中,我们一起学习了Python与Rust在能源效率和计算性能上的显著差异。通过具体的研究数据,我们了解到在同等任务下,Python的能耗可能是Rust/C语言的约70倍,计算时间也可能长达70倍,并且内存占用更高。对于处理繁重、静态计算负载(如Web微服务)的场景,从可持续发展和成本效益角度出发,考虑切换到像Rust这样高效的语言是明智的。工具(如Copilot)的辅助使得这种转换变得更加可行。希望本节能帮助你更全面地评估编程语言选择,做出更负责任的技术决策。

045:GPU并发压力测试 🚀

在本节课中,我们将学习如何利用Rust的系统编程能力,结合PyTorch绑定,构建一个能够对CUDA启用的GPU进行压力测试的工具。我们将探索三种不同的测试方式:CPU测试、GPU测试以及使用Rayon库实现的多线程GPU测试。


概述

Rust的一个强大用途是构建能与GPU通信的系统工具。幸运的是,PyTorch与Rust的绑定工作得非常好。我们将构建一个工具,它不仅能通过PyTorch与CPU通信并使其饱和,还能与CUDA启用的GPU通信,利用Rust真正的多核优势——即允许你生成一个线程池,然后将数据发送到GPU,以进行最大限度的压力测试。让我们在短时间内构建这个工具。

架构概览

让我们看看如何利用Rust的系统编程能力和便捷的Rust PyTorch绑定,构建一个针对CUDA启用GPU的压力测试工具。

以下是构建步骤:

  1. 获取环境:首先,需要访问一个CUDA启用的GPU环境。可以使用GitHub Codespaces,也可以是AWS、GCP或Azure的实例。
  2. 配置监控:接着,配置NVIDIA SMI监控,以便在压力测试期间观察GPU的利用率。
  3. 集成绑定:然后,集成Rust PyTorch绑定。
  4. 构建工具:绑定启用后,构建一个Rust命令行工具。
  5. 执行测试:该工具提供三种执行压力测试的方式:CPU测试、GPU测试,以及利用Rayon强大且内存高效的多线程能力,以多线程方式将数据推送到GPU进行完整压力测试。

好的,让我们开始吧。

环境与项目设置

首先,我们打开并启动这个环境。我们可以非常轻松地开始构建这个压力测试工具。首先要确保我们设置了正确的Cargo项目结构,这是这里的关键要点之一。

为了便于理解,我们可能先运行一下看看它是如何工作的,然后再看看未来我们想做出哪些修改。

这里我有一个翻译应用和一个压力测试应用。我将使用压力测试应用。这个应用很有趣,因为如果我们查看Cargo.toml文件,会发现我们使用了三个依赖项。

以下是项目依赖:

  • clap:用于构建命令行工具。
  • tch:Rust的PyTorch绑定。
  • rayon:允许我们编写多线程代码。

这就是构建这个压力测试工具所需的所有依赖。接下来,我再次使用这个模式:我的库代码放在lib.rs中,而我的命令行工具代码放在main.rs中。

库代码分析

让我们先来看看库代码。在库代码中,我们构建了一些函数,每个函数都非常简单。

我导入了rayontch(TensorFlow风格的导入,但实际是PyTorch)。我们来看看这些函数。

// 这是一个简化的示例代码结构
use rayon::prelude::*;
use tch::{Device, Tensor};

pub fn cpu_load_test() {
    let data: Vec<i64> = (0..1000).collect();
    let device = Device::Cpu;
    for i in data {
        let _tensor = Tensor::from(i).to(device);
        // ... 执行一些操作
    }
    println!("CPU负载测试完成。");
}

pub fn gpu_load_test() {
    let data: Vec<i64> = (0..1000).collect();
    let device = Device::Cuda(0); // 使用第一个CUDA设备
    for i in data {
        let _tensor = Tensor::from(i).to(device);
        // ... 执行一些操作
    }
    println!("GPU负载测试完成。");
}

pub fn threaded_gpu_load_test() {
    let data: Vec<i64> = (0..1000).collect();
    let device = Device::Cuda(0);
    data.par_iter().for_each(|&i| { // 使用 rayon 的并行迭代器
        let _tensor = Tensor::from(i).to(device);
        // ... 执行一些操作
    });
    println!("多线程GPU负载测试完成。");
}

如你所见,我构建了cpu_load_test,它所做的只是将一个向量发送到设备(这里是CPU),并打印出发生的情况。对于GPU负载测试函数,我们只需更改设备,指定迭代到CUDA设备。最后,要将其改为多线程版本,只需使用into_par_iter().for_each()操作,然后将其也推送到CUDA设备。

这真的很酷,因为我们有三种完全不同的方式来实际进行测试。

命令行工具集成

现在,我需要做的就是转到我的main.rs文件。同样,这是完全相同的模式,本质上是样板代码。

以下是关键部分:我构建了一个CPU子命令、一个GPU子命令,以及一个用于多线程的T_GPU子命令。看,我只是将每个子命令映射到库代码中的函数。这个模式非常强大,因为你只需在库中编写几行代码,然后将这些函数映射到这里。

你唯一需要注意的其他事情是确保这些函数都是pub(公开的)。完成后,你就可以运行了。

现在,要运行这个工具,我需要做的就是运行Cargo。

让我们构建它,输入cargo run -- help。这将重新编译并给我们一个帮助菜单,该菜单直接对应于每个命令:CPU、GPU和T_GPU。这正是我们所看到的。

运行测试与监控

现在,一件非常有帮助的事情是进行两种监控。首先,让我们运行htop,看看这里的CPU情况。

如果我们在这里查看CPU,然后运行cargo run -- cpu,会发生的情况是它将向CPU发送大量数据。看,你可以看到它现在饱和了。如果我切换到GPU监控,你会看到什么都没有发生。所以,这是理解代码运行情况的好方法,我们知道我们正在使用CPU设备,因此没有流量发送到GPU。

接下来,我要做的是切换模式。你可以看到它停止了,我将继续并说,好的,现在让我们切换到GPU。当你切换到GPU时会发生什么?我们进行GPU压力测试,你瞧,它将向GPU发送大量数据。

我们能够通过在这里切换来看到它,你会看到这个东西变得饱和。现在,它实际上只达到了大约四分之一的负载。如果我们同时查看这里的线程,它们仍然在被利用。所以基本上,核心仍在被利用,但也有一些流量被发送到GPU。

现在,我们能做得比这更好吗?我们能把这个负载提得更高吗?如果我们真的想对事物进行压力测试,我们可以。这就是多线程GPU测试发挥作用的地方。

现在运行我早先编写的使用rayon的代码。我们成功了。我们实际上能够将发送到GPU的负载增加超过一倍。它之前真的只有大约20-25%,现在可以说负载增加了两到三倍。

现在,如果我们查看CPU,看看这个,注意它实际上是如何减少CPU负载的。所以你可以看到,一旦你开始向GPU发送足够的流量,它实际上是卸载系统部分资源的好方法。

总结

本节课中,我们一起学习了构建GPU压力测试工具。对于像这样的工具进行压力测试,真正的好处在于能切实感受环境中发生的情况。除非你进行基准测试,否则你根本不知道发生了什么,尤其是在像在GPU上进行PyTorch训练这样的分布式计算问题中。

你可以看到,Rust是进行训练、推理、构建可移植二进制工具的完美工具。确实,Rust是一种系统工具,是MLOps的理想工具。

通过这个实践,我们掌握了利用Rust并发特性和PyTorch生态来评估硬件性能的基本方法。

046:无服务器优化的主机能效问题 🖥️

在本节课中,我们将探讨无服务器计算和虚拟机技术中,主机虚拟化效率所面临的一些关键问题。我们将分析这些问题如何影响资源利用率和成本效益,并重点比较Rust与Python在无服务器环境下的性能差异。

主机虚拟化效率的核心问题

上一节我们介绍了无服务器计算的基本概念,本节中我们来看看其底层主机虚拟化效率的经典优化问题。这本质上是一个经典的商学院式优化问题:你有一系列构建解决方案的需求,需要将最佳方案适配到有限的可用资源中。

在虚拟化主机的场景下,它们通常以默认配置提供。例如,可能是一台配备特定内存的双核机器,或是一台配备特定内存的四核机器。

语言选择对初始效率的影响

现在的问题是,如果你的编程语言或解决方案默认就占用大量内存(例如许多解释型语言,如Ruby和Python),那么从一开始,你获得的机器就可能比编译型语言需要更多的内存和核心。因此,即使从最初阶段开始,使用高内存语言就已经在损失效率。

很多时候,高内存需求会与多核心配置关联。假设你构建的应用不需要多线程解决方案或多核心。但由于你的解决方案需要大量内存,你被分配了四个核心。结果就是,你实质上让三个核心处于完全空闲状态。这可能导致解决方案中巨大的成本效率低下。

Rust与Python在无服务器环境下的效率对比

简而言之,让我们看一个更具体的例子:在AWS Lambda这样的无服务器平台上,Rust和Python的效率对比。

以下是Rust与Python在AWS Lambda环境中的关键效率差异点:

  • 内存分配:Rust Lambda可以使用更小的内存分配(例如128MB),而Python可能需要512MB。这是因为Rust具有更低的内存占用。
  • 并发处理:由于全局解释器锁(GIL)的限制,Python Lambda的并发执行受限。而Rust可以利用纯线程进行扩展,因此默认就能使用线程作为解决方案。
  • 启动时间:Rust的启动时间比Python快得多,因为它是预编译的,而解释器会引入延迟。这对无服务器解决方案中的冷启动也有好处。
  • 多进程内存开销:Python使用多进程来利用核心会导致更高的内存开销,因此这不是一个一比一的解决方案。
  • 线程内存占用:Rust线程使用的内存远少于Python进程。
  • 性能可预测性:Python的垃圾回收机制可能导致暂停,从而引发延迟峰值。而Rust由于其构建方式,性能更具可预测性。
  • 执行速度:Rust Lambda可以实现接近原生(即机器代码级别)的性能,而Python本身受解释器限制。
  • 线程安全:Rust编译器会强制执行线程安全。因此,如果你必须使用线程,你将同时获得安全性和低运行时开销。

总结与启示

本节课中我们一起学习了无服务器架构中主机虚拟化的效率挑战。总结来说,在AWS Lambda场景下,Rust相比Python具有以下优势:更小的内存分配、优于Python GIL限制的线程扩展能力、更快的冷启动时间、更低的线程内存使用量、更可预测的性能以及接近原生的速度。

因此,在许多场景下,由于上述问题,默认使用Python进行无服务器开发可能并不符合组织的最佳利益。理解这些效率差异有助于为项目选择更合适的技术栈,从而优化资源利用和成本控制。

047:使用Rust处理CSV文件 📄

在本节课中,我们将学习如何使用Rust编程语言来读取和处理CSV文件。我们将创建一个简单的项目,使用csv库来读取一个包含示例数据的文件,并输出其内容。


概述

Rust提供了强大的库支持来处理各种数据格式,包括CSV文件。通过使用csv库,我们可以轻松地读取、解析和操作CSV数据。本节将引导你完成一个简单的CSV文件读取示例。


创建新项目

首先,我们需要创建一个新的Rust项目。在AWS Cloud9环境中,我们可以使用cargo new命令来初始化项目。

cargo new csv_demo

创建项目后,进入项目目录。

cd csv_demo

添加依赖库

接下来,我们需要在项目的Cargo.toml文件中添加csv库作为依赖。Rust的一个优点是我们可以方便地查阅库的文档,以确定所需的版本。

打开Cargo.toml文件,在[dependencies]部分添加以下内容:

csv = "1.1"

保存文件后,Cargo会自动下载并管理这个依赖。


准备数据文件

为了演示CSV文件的读取,我们需要一个包含数据的CSV文件。在项目根目录下创建一个名为data的目录,用于存放数据文件。

mkdir data

然后,在data目录中创建一个名为text.txt的文件,并粘贴以下示例数据:

name,age,city
Alice,30,New York
Bob,25,Los Angeles
Charlie,35,Chicago

编写代码

现在,我们来编写读取CSV文件的代码。打开src/main.rs文件,将默认的“Hello, World!”代码替换为以下内容:

use std::error::Error;
use std::fs::File;
use std::io;
use csv::Reader;

fn main() -> Result<(), Box<dyn Error>> {
    // 创建CSV读取器
    let file_path = "data/text.txt";
    let file = File::open(file_path)?;
    let mut rdr = Reader::from_reader(file);

    // 遍历并输出每一行记录
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }

    Ok(())
}

代码解析

以下是代码的关键部分解析:

  1. 导入依赖:我们导入了必要的模块,包括处理错误的Error、文件操作的File、输入输出的io以及CSV读取的Reader
  2. 打开文件:使用File::open打开指定路径的CSV文件。
  3. 创建读取器:通过Reader::from_reader创建一个CSV读取器。
  4. 遍历记录:使用for循环遍历CSV文件中的每一行记录,并打印出来。

运行程序

代码编写完成后,我们可以使用cargo run命令来运行程序。确保终端当前位于项目根目录下,然后执行:

cargo run

如果一切正常,程序将输出CSV文件中的每一行记录,类似于以下内容:

StringRecord(["name", "age", "city"])
StringRecord(["Alice", "30", "New York"])
StringRecord(["Bob", "25", "Los Angeles"])
StringRecord(["Charlie", "35", "Chicago"])

总结

在本节课中,我们一起学习了如何使用Rust处理CSV文件。我们创建了一个新项目,添加了csv库依赖,准备了一个示例数据文件,并编写了读取和输出CSV文件内容的代码。通过这个简单的示例,你可以看到Rust在数据处理方面的强大能力和简洁性。

如果你想进一步探索CSV文件的处理,csv库还提供了许多高级功能,如写入CSV文件、处理自定义分隔符等。希望本节内容对你有所帮助!

048:使用Cargo Lambda与Rust 🚀

在本节课中,我们将要学习如何使用 Cargo Lambda 工具,以极其简单的方式在 AWS Lambda 上部署和运行 Rust 函数。我们将涵盖从安装、本地开发测试到最终部署的完整流程。


概述

Cargo Lambda 是一个专门为 Rust 语言设计的工具,它极大地简化了在 AWS Lambda 上开发和部署无服务器函数的过程。它消除了复杂的配置和繁琐的工作流,使得 Rust 成为开发 Lambda 函数最便捷的语言之一。

安装 Cargo Lambda

要开始使用 Cargo Lambda,首先需要安装它。一种推荐的方式是通过 Homebrew 包管理器进行安装。

以下是安装步骤:

  1. 确保你的机器(例如 Cloud9 环境)上已安装 Homebrew。
  2. 通过 Homebrew 安装 Cargo Lambda。

安装完成后,你就可以使用 cargo lambda new 命令来创建一个新的 Lambda 项目。

项目结构与本地开发

上一节我们介绍了如何安装 Cargo Lambda,本节中我们来看看如何创建项目并进行本地开发。

我已经预先设置好了一个项目示例。这个项目名为 EFs_lister,其功能是列出挂载的 Elastic File System (EFS) 中的文件。所有代码都位于这个项目目录中。

Cargo Lambda 最强大的功能之一是支持本地测试和仿真。你无需部署到云端,就能在本地环境中运行和调试你的 Lambda 函数代码。

启动本地仿真服务

为了在本地测试函数,我们需要启动 Cargo Lambda 的监视模式。这个模式会运行一个本地仿真环境。

以下是启动命令:

cargo lambda watch

执行此命令后,一个本地 Lambda 仿真服务就会启动。由于 Rust 是编译型语言,直接生成二进制文件,因此这个过程非常轻量,无需依赖 Docker 等复杂容器。

本地调用函数

本地服务运行起来后,我们就可以像调用真实 Lambda 函数一样来测试它。

以下是调用命令:

cargo lambda invoke

在我的示例中,函数接受一个 name 参数。执行上述命令后,函数会运行其生产环境中的逻辑——即列出我的 EFS 文件系统中的文件。我可以通过在终端执行 ls -l /mnt/efs 命令来验证输出结果,两者是完全一致的。

构建与部署

我们已经学会了如何在本地开发和测试函数,接下来看看如何将其构建并部署到 AWS Lambda。

构建发布版本

首先,我们需要为生产环境构建一个优化的发布版本。

以下是构建命令:

cargo lambda build --release

这个命令会编译项目中的所有依赖,并生成一个可用于部署的二进制文件。对于 AWS Lambda 这类服务,直接部署二进制文件是最高效的方式。

部署到 AWS Lambda

构建完成后,最后一步就是将函数部署到云端。

以下是部署命令:

cargo lambda deploy

值得注意的是,Cargo Lambda 支持部署到 ARM64 架构,这可以为你节省大量的运行成本。执行部署命令后,你的 Rust 函数就会被快速上传并配置到 AWS Lambda 上。使用 Cargo Lambda 部署代码的速度极快,流程也非常简单。

其他功能与总结

除了我们介绍的核心功能,Cargo Lambda 还支持更多高级特性,例如集成调试、设置 GitHub Actions 工作流以及配置各种事件源(Event Sources)等。

总而言之,Cargo Lambda 将原本在其他脚本语言中可能有些令人畏惧的 AWS Lambda 开发流程变得非常简单直接。它提供了从本地仿真到云端部署的一站式解决方案。

本节课中我们一起学习了:

  1. Cargo Lambda 的安装方法
  2. 如何创建项目并使用 cargo lambda watch 进行本地仿真开发。
  3. 如何通过 cargo lambda invoke 在本地测试函数逻辑。
  4. 使用 cargo lambda build --release 构建发布版本。
  5. 使用 cargo lambda deploy 一键部署到 AWS Lambda。

我强烈推荐你尝试使用 Cargo Lambda,其代码实现也相当直观易懂。我们将在另一个演示视频中再见。

049:49_03_04_Rust列出AWS EFS文件 📂

在本节课中,我们将学习如何结合使用Rust、AWS Lambda和AWS EFS(弹性文件系统)来构建一个高效、按需付费的文件列表服务。我们将看到Cargo Lambda如何简化部署流程,以及EFS如何为无服务器工作流提供可扩展的共享存储。


概述:Rust、Lambda与EFS的强大组合 💪

Cargo Lambda是一个将Rust函数部署到AWS Lambda的优秀工具。它极大地简化了现代编译语言(如Rust)在无服务器环境下的工作流程。在本演示中,我将展示Cargo Lambda如何与AWS EFS结合使用。EFS是AWS提供的托管挂载文件系统。这三者的结合——无服务器计算、托管文件服务和Rust语言——能够实现一些前所未有的新功能。

上一节我们介绍了这个组合的潜力,本节中我们来看看具体的实现。


认识AWS EFS(弹性文件系统) ☁️

首先,让我们了解一下EFS。如上图所示,弹性文件系统非常出色。在这个例子中,我只使用了17兆字节的存储,但它可以自动扩展到TB级别。更重要的是,它是一种按需付费的资源,我不需要一次性为TB级别的容量付费,只需为实际使用的部分付费。这使得它成为无服务器工作流中理想的临时性资源。

了解了EFS的基本概念后,接下来我们看看如何在Lambda中配置和使用它。


配置Lambda访问EFS 🛠️

接下来,我将使用一个已经部署好的Lambda函数,名为“EFS Lister”。这个函数的精妙之处在于,我已经配置好,使得EFS文件系统可以直接挂载到这个Lambda函数内部。当Lambda启动并执行任务时,它已经具备了直接访问该文件系统的能力。

这是一个非常有趣的模式:在不需要时,我不需要为文件系统或Lambda支付任何费用;只有在实际使用时才产生成本。并且,由于使用了Rust,它的运行效率将非常高。

那么,问题来了:我该如何针对这个环境进行开发呢?


使用AWS Cloud9进行开发 💻

一种方法是使用AWS Cloud9。如上图所示,我已经在Cloud9中设置好了一些Rust开发环境。运行 rustc --version 可以看到Rust已经安装完毕。另一个好处是,我可以查看历史命令,了解之前是如何挂载文件系统的。

回顾历史,我只需要按照这些指示操作即可。为了简便,我可以创建一个新文件,将这些命令粘贴进去然后执行。

以下是挂载EFS的具体步骤:

  1. 创建挂载点目录:首先,我们需要创建一个目录作为EFS的本地挂载点。

    mkdir /mnt/efs
    

    (如果目录已存在,此命令会提示,但无妨。)

  2. 挂载EFS文件系统:使用正确的命令将远程的EFS挂载到上一步创建的本地目录。

    sudo mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport fs-xxxxxxxx.efs.region.amazonaws.com:/ /mnt/efs
    

    (请将 fs-xxxxxxxxregion 替换为你自己的EFS文件系统ID和区域。)

  3. 验证挂载:我们可以运行命令检查挂载是否成功,并查看目录内容。

    ls /mnt/efs
    

    如果成功,我们将能看到EFS中存储的文件,例如一些大型语言模型的预训练文件等。

现在,我们可以在该目录下创建新文件进行测试:

echo "Hello from EFS" > /mnt/efs/demo_video_i_was_here.txt
ls /mnt/efs

可以看到新文件 demo_video_i_was_here.txt 已经存在。这证明了我们的开发环境已经能够正常访问EFS。

环境准备就绪后,让我们把目光转向实现这一功能的Rust代码。


解析Rust Lambda代码 🦀

现在,让我们查看实际与EFS交互的代码。这是一个名为“EFS Lister”的项目。

首先,我们看一下 Cargo.toml 文件,它定义了项目的依赖关系:

[package]
name = "efs-lister"
version = "0.1.0"

[dependencies]
tokio = { version = "1", features = ["full"] }
lambda_runtime = "0.7"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

关键依赖包括 lambda_runtime(用于构建Lambda函数)以及Tokio和Tracing(AWS推荐用于Lambda的异步运行时和日志记录库)。我们使用Cargo Lambda系统来部署它。

接下来,我们查看主要的源代码 src/main.rs。代码主要包含三部分:

  1. 数据结构:用于序列化和反序列化Lambda事件与响应的数据。

    use serde::{Deserialize, Serialize};
    
    #[derive(Deserialize)]
    struct Request {
        name: String,
    }
    
    #[derive(Serialize)]
    struct Response {
        req_id: String,
        files: Vec<String>,
    }
    
  2. 核心辅助函数:一个异步函数,用于列出EFS卷中的文件。

    async fn list_files_in_efs(path: &str) -> Result<Vec<String>, std::io::Error> {
        let mut entries = tokio::fs::read_dir(path).await?;
        let mut file_names = Vec::new();
        while let Some(entry) = entries.next_entry().await? {
            let file_name = entry.file_name().into_string().unwrap_or_default();
            file_names.push(file_name);
        }
        Ok(file_names)
    }
    

    这个函数异步读取指定目录,并将文件名收集到一个列表中。

  1. Lambda处理函数:将以上部分组合起来,形成完整的Lambda处理器。
    async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
        let path = "/mnt/efs"; // EFS在Lambda中的挂载路径
        let files = list_files_in_efs(path).await?;
        let resp = Response {
            req_id: event.context.request_id,
            files,
        };
        Ok(resp)
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Error> {
        tracing_subscriber::fmt().init();
        let func = service_fn(function_handler);
        lambda_runtime::run(func).await?;
        Ok(())
    }
    
    主函数初始化日志,并启动Lambda运行时来运行我们的处理函数。

这段代码在功能上类似于用Python等脚本语言编写的AWS Lambda,但其效率极高,并且部署过程(通过Cargo Lambda)非常简单。

理解了代码结构,最后我们来测试这个已部署的Lambda函数。


测试与性能分析 ⚡

我们可以通过AWS Lambda控制台来测试这个函数。如上图所示,我们创建一个测试事件。

由于我们的 Request 结构需要一个 name 字段,测试事件内容如下:

{
  "name": "test"
}

保存并执行测试后,Lambda成功返回了响应。在响应体中,我们看到了文件列表,其中就包含我们之前创建的 demo_video_i_was_here.txt 文件。

这里有几个关键指标值得注意:

  • 构建持续时间:14毫秒。这展现了惊人的效率。
  • 内存使用:也极为高效。

这种级别的性能是脚本语言难以企及的。Rust在提供接近C语言性能的同时,还通过其所有权模型保证了内存安全。此外,部署也非常简单,只需要推送编译后的二进制文件即可。


总结 🎯

本节课中,我们一起学习了如何将EFS这一新兴标准与Rust及AWS Lambda生态系统相结合。这种组合特别适用于大型语言模型、数据工程等场景,值得各个组织深入研究和探索。

回顾我们的代码,它非常简洁明了,不到50行就实现了一个功能完整的Lambda。我们知道,Rust是最安全的语言之一,能提供媲美C语言的性能。因此,请尝试在你的组织中探索EFS、Lambda和Rust的结合使用,它们将为你带来高效、安全且成本优化的无服务器解决方案。

050:AWS S3存储使用 🗂️

在本节课中,我们将学习如何在AWS S3中进行数据存储和同步操作。S3是数据工程师和科学家处理海量数据时不可或缺的工具。我们将从创建存储桶开始,逐步演示如何上传、下载和同步数据。

概述

本节教程将指导您完成AWS S3的基本操作流程。我们将首先在AWS管理控制台中创建一个S3存储桶,然后通过网页界面上传文件。接着,我们将切换到Cloud9开发环境,使用AWS命令行工具(CLI)在本地目录和S3存储桶之间同步数据。整个过程旨在展示一个高效的数据处理工作流。

创建S3存储桶

首先,我们需要在AWS管理控制台中创建一个S3存储桶。存储桶的名称在全球范围内必须是唯一的。

以下是创建存储桶的步骤:

  1. 在AWS管理控制台顶部的搜索栏中,输入“S3”并进入S3服务页面。
  2. 点击“创建存储桶”按钮。
  3. 在“存储桶名称”字段中,输入一个全局唯一的名称。例如:bigbluewebsitename1234
  4. 在配置选项中,保持所有默认设置。
  5. 在“阻止所有公共访问”设置中,确保选择“阻止所有公共访问”,以保持存储桶的私密性。
  6. 点击页面底部的“创建存储桶”按钮。

现在,您已经成功创建了一个名为 bigbluewebsitename1234 的私有S3存储桶。

通过控制台上传文件

创建存储桶后,我们可以通过网页界面向其中上传文件。这是一种直接的手动上传方式。

操作步骤如下:

  1. 在S3的存储桶列表中,点击您刚创建的存储桶名称(例如 bigbluewebsitename1234)。
  2. 在存储桶详情页面,点击“上传”按钮。
  3. 在弹出的窗口中,点击“添加文件”,然后从您的本地计算机选择一个文件(例如一个名为 nba_endorsement_data.csv 的CSV文件)。
  4. 在权限设置中,保持默认选项,确保文件保持私有状态。
  5. 在存储类别中,选择“标准”即可,这表示数据将在多个可用区中存储。
  6. 点击“上传”按钮,文件将开始上传至您的S3存储桶。

上传完成后,您可以在存储桶的文件列表中看到该文件。S3提供了近乎无限的存储空间,非常适合存放深度学习训练数据、SQL文件或客户数据等各种类型的大文件。

使用AWS CLI同步数据

上一节我们介绍了如何通过网页界面上传文件。本节中我们来看看如何通过命令行工具更高效地在本地和S3之间同步数据。我们将使用AWS Cloud9环境中的命令行工具。

首先,打开您的AWS Cloud9开发环境。

从S3同步数据到本地

要将S3存储桶中的数据同步到本地目录,我们需要使用 aws s3 sync 命令。

以下是操作步骤:

  1. 在Cloud9终端中,首先创建一个本地目录来存放同步的文件。
    mkdir files
    
  2. 进入新创建的目录。
    cd files
    
  3. 执行同步命令,将S3存储桶的内容同步到当前目录(.)。请将 bigbluewebsitename1234 替换为您自己的存储桶名称。
    aws s3 sync s3://bigbluewebsitename1234 .
    
    命令中的 # 符号可用于添加注释,方便以后重用命令。

同步完成后,您可以在本地的 files 目录中看到从S3下载的文件。您可以使用 cathead 命令查看文件内容,例如确认CSV数据已成功下载。

从本地同步数据到S3

现在,假设我们在本地目录中创建或修改了一些文件,并希望将它们上传回S3存储桶。

操作步骤如下:

  1. files 目录中创建一些新文件作为示例。
    touch file1.txt file2.txt
    
  2. 执行同步命令,将本地当前目录(.)的内容同步到S3存储桶。这会将本地有而S3中没有的文件上传到云端。
    aws s3 sync . s3://bigbluewebsitename1234
    
  3. 如果您希望执行反向同步(即用S3的内容覆盖本地),只需交换命令中的源和目标位置即可。
    aws s3 sync s3://bigbluewebsitename1234 .
    

执行上传命令后,刷新AWS管理控制台中的S3存储桶页面,您将看到新文件 file1.txtfile2.txt 已经出现在存储桶中。sync 命令非常高效,无论数据量是GB级还是TB级,都能快速完成同步。

总结

本节课中我们一起学习了AWS S3的核心操作。我们首先创建了一个私有的S3存储桶,然后通过管理控制台上传了文件。接着,我们切换到Cloud9环境,使用 aws s3 sync 命令行工具实现了本地文件系统与S3存储桶之间的双向数据同步。这个工作流简单而强大,是进行大规模数据处理、备份和迁移的基础。掌握这些技能,对于数据工程师和科学家高效利用云存储资源至关重要。

051:使用Rust调用AWS S3存储 🚀

在本节课中,我们将学习如何使用Rust编程语言调用AWS S3存储服务。我们将探讨AWS SDK for Rust的优势,并通过一个实际示例来演示如何创建项目、配置依赖以及执行S3操作。课程内容旨在让初学者能够理解并上手使用Rust进行AWS开发。


概述 📋

AWS SDK for Rust是一个强大的工具,用于在AWS云平台上进行开发。Rust作为一种现代编译型语言,在线程、异步编程、安全性和部署等方面具有天然优势,使其成为处理像AWS这样的云服务提供商的理想工具。

Rust与AWS SDK的优势 ⚡

上一节我们介绍了课程概述,本节中我们来看看为什么选择Rust和AWS SDK进行开发。Rust语言提供了许多其他脚本语言或旧式编译语言所不具备的解决方案。

以下是Rust的一些核心优势:

  • 现代编译语言:提供高性能和安全性。
  • 原生支持异步编程:通过async/await语法高效处理网络请求。
  • 出色的线程安全:借助所有权和借用检查器,避免数据竞争。
  • 部署简便:编译为单一可执行文件,易于分发和运行。

项目示例与实践 🛠️

现在,让我们通过一个具体的例子来看看如何使用这个SDK。首先,我们需要创建一个新的Rust项目并添加必要的依赖。

创建一个新项目的命令如下:

cargo new sdk_example

然后,需要在项目的Cargo.toml文件中添加依赖。以下是一个示例配置:

[package]
name = "aws_meta_s3"

[dependencies]
aws-config = "0.55"
aws-sdk-s3 = "0.28"
tokio = { version = "1", features = ["full"] }
clap = { version = "4.0", features = ["derive"] }

代码解析与功能实现 💻

在配置好依赖之后,我们来看看项目的主要代码实现。代码库中包含几个关键函数。

以下是核心功能的简要说明:

  • 创建客户端:一个async的公共函数,用于初始化AWS S3客户端。
  • 列出存储桶:一个async函数,用于高效地列出账户中的所有S3存储桶。
  • 计算存储桶大小:通过累加存储桶内所有对象的大小来计算总容量。对于包含大量文件的存储桶,这是一个计算密集型操作,但得益于Rust的高效性,它可以快速完成。

此外,项目还使用clap库包装了一个命令行工具前端,方便用户通过命令调用这些功能。

运行与测试 ▶️

在理解了代码结构后,我们可以在Cloud 9环境中运行这个项目。由于Cloud 9实例通常性能较强,且已配置好安全令牌和API调用权限,因此编译和运行过程会非常顺畅。

使用以下命令运行项目:

cargo run -- --help

此命令会编译项目并显示命令行工具的帮助信息。工具主要提供两个子命令。

以下是可用的命令选项:

  • buckets:列出AWS账户中的所有S3存储桶。
  • account-size:计算并汇总账户中所有存储桶的总数据量。

我们可以分别测试这两个功能。首先测试列出存储桶:

cargo run -- buckets

然后测试计算账户总容量。这是一个潜在的重度操作,但由于Rust的异步特性和与AWS的良好集成,执行速度非常快:

cargo run -- account-size

总结 🎯

本节课中我们一起学习了如何使用Rust和AWS SDK进行S3存储操作。我们了解了Rust语言在云开发中的优势,逐步实践了从创建项目、添加依赖、编写核心功能代码到最终运行和测试的完整流程。AWS Rust SDK是数据工程领域一个新兴且强大的工具,本节课的内容为初学者提供了一个实用的入门起点。

052:加密数据写入表格或Parquet文件 🔐

在本节课中,我们将要学习如何将加密数据写入数据库表格或Parquet文件。我们将探讨数据加密的几个关键概念,包括静态数据加密、传输中数据加密和使用中数据加密,并了解它们在数据库系统中的具体实现方式。

数据加密的核心概念

上一节我们介绍了加密数据写入的目标,本节中我们来看看数据加密的几个核心状态。

数据加密主要涉及三种状态:静态数据、传输中数据和使用中数据。

静态数据是一个需要了解的重要概念。它意味着数据在已经存在于文件系统或数据库中时进行加密。

传输中数据是指在私有或公共通信通道上进行加密。这确保了数据在移动过程中仍然是安全的。

使用中数据意味着数据在仍处于内存或CPU缓存中时进行加密,这样即使有人能够访问该机器,也无法拦截数据。

数据库透明加密架构

了解了基本概念后,我们来看一个具体的实现示例。

以下是一个透明数据库加密架构的好例子,即Windows操作系统的数据保护API。该架构通过服务主密钥进行加密,并用于数据库。在用户层面,也可以使用相同的数据库加密密钥。通过透明数据加密,您的数据库、备份和日志都能够通过应用程序进行加密。实际上,这在Azure数据库中只是一个简单的开关设置。

列级加密与始终加密

现在,让我们深入看看数据库列级别的加密。

您可以指定希望加密特定的列。当启用“始终加密”时,它可以设计存储敏感信息。在SQL Server数据库中,它使客户端能够在客户端应用程序内部加密数据,并且永远不会通过密钥向SQL Server泄露。

安全飞地与数据使用安全

最后,这意味着当数据实际在内存中使用时,您无需担心数据的安全性。

因为这是通过安全飞地完成的,这是Azure SQL生态系统的一部分。您拥有增强的客户端驱动程序,存在明文和密文。无论是在静态、传输中还是在使用时,只要您使用这些控制措施,就能够安全地处理数据。

总结

本节课中我们一起学习了将加密数据写入表格或文件的核心技术。我们明确了静态数据传输中数据使用中数据这三个关键状态,并探讨了在数据库系统中实现加密的架构,如透明数据加密和列级的“始终加密”。最后,我们了解了通过安全飞地技术保障内存中使用数据的安全。掌握这些概念有助于在数据工程和DevOps实践中构建更安全的数据处理流程。

053:Google Colab平台介绍 🚀

在本节课中,我们将学习如何使用Google Colab Notebook进行数据科学项目和作品集项目。我们将涵盖从创建笔记本、组织项目结构到分享和协作的完整流程。

概述

Google Colab是一个基于云的Jupyter Notebook环境,特别适合数据科学和机器学习项目。它提供了免费的GPU和TPU资源,并集成了Google Drive和GitHub,便于协作和分享。

创建新笔记本

首先,我们来看看如何创建一个新的Colab笔记本。

打开Colab后,你会看到默认的欢迎界面。要创建新笔记本,请点击菜单栏的 File,然后选择 New notebook。系统会创建一个空白的笔记本模板。

创建后,你可以为笔记本命名。点击顶部的文件名(默认为“Untitled.ipynb”)进行修改。例如,可以将其命名为 data_science。合理的命名有助于他人理解笔记本的内容。

Runtime 菜单中,你可以更改运行时类型。例如,可以选择使用GPU或TPU加速计算。专业版用户可以使用更高性能的GPU。

组织项目结构

对于学生或数据科学专业人士,我特别推荐在项目中创建清晰的层次结构。这有助于他人理解你的工作流程,而无需浏览数千行代码。

以下是我推荐的项目结构组织方式:

  1. Ingest(数据获取):此阶段负责数据的导入和初步处理。
  2. EDA(探索性数据分析):此阶段进行数据探索和可视化。
  3. Modeling(建模):此阶段构建和训练机器学习模型。
  4. Conclusion(结论):此阶段总结分析结果和模型表现。

你可以通过创建Markdown单元格作为标题来构建这个结构。例如:

# Ingest
# 此处放置数据导入代码
import pandas as pd

这种结构化的另一个好处是,Colab会自动在左侧生成一个可导航的目录。点击左侧的“目录”图标即可查看。当代码块被折叠时,导航变得非常方便。

编写与执行代码

在Colab中,你可以直接执行Python代码。每个代码单元格都可以独立运行。

例如,在“Ingest”阶段,你可以输入以下代码来导入Pandas库:

import pandas as pd

每个阶段(如Ingest、EDA)的代码都可以根据需要进行隐藏或展开,这有助于保持笔记本的整洁。

分享与协作

将你的项目分享给团队成员、同事或潜在雇主非常重要。Colab提供了几种简便的分享方式。

第一种方式:保存到GitHub

这是我最喜欢的方式之一。点击 File 菜单,选择 Save a copy in GitHub

在操作之前,你需要在GitHub上创建一个新的代码仓库。例如,创建一个名为 data_science_portfolio 的仓库。

在Colab的保存对话框中,选择你刚创建的仓库,然后点击确定。这样就将笔记本保存到了GitHub,并生成了一个可以在Colab中直接打开的链接。这是一个向他人展示你作品的绝佳方式。

第二种方式:使用Google Drive链接分享

你也可以利用Google Drive的分享功能。点击笔记本右上角的 Share 按钮。

在分享设置中,将权限修改为 Anyone with the link(任何拥有链接的人),然后复制生成的链接。

你可以将这个链接放入项目的README文件中,例如写上“Open my notebook”。这种方式的好处是,任何人无需认证即可访问笔记本,非常适合进行演示或代码共读。

我建议根据你的需求,选择其中一种或同时使用两种方式来分享你的笔记本。

查看一个完整项目示例

最后,让我们看一个更复杂的数据科学项目示例,它展示了更传统的工作流程。

如果你打开一个这样的项目链接,你会注意到它完全遵循了我们之前讨论的结构:Ingest、EDA、Modeling、Conclusion。

例如,在“Ingest”部分,项目可能从一个CSV文件(如New_York_Times.csv)读取数据,并为后续分析做好准备。

这种方法的优点是,它确保了项目的100%可复现性,并且易于导航。所有数据和代码都组织得井井有条,他人可以轻松理解并运行你的整个分析流程。

总结

本节课我们一起学习了Google Colab平台的核心用法。我们了解了如何创建和命名笔记本,如何通过构建清晰的阶段(Ingest, EDA, Modeling, Conclusion)来组织数据科学项目,以及如何通过GitHub和Google Drive链接有效地分享你的工作成果。遵循这些实践,将使你的数据科学项目更加专业、易于理解和协作。

054:使用Bard增强笔记本开发 🚀

概述

在本节课中,我们将要学习如何利用AI结对编程工具(如Bard)来辅助项目开发和学习,特别是在使用Python和Colab笔记本进行数据分析时。我们将通过一个具体的例子,展示如何从获取代码片段到调试并成功运行一个数据可视化项目。


现代软件工程的一个令人兴奋的方面是能够使用AI结对程序员来帮助您构建项目,甚至为认证考试做准备。

让我们来看一下这个Bard提示。第一步,我会问:“谷歌云平台安全的三个关键方面是什么?”假设我正在为谷歌云平台的认证考试学习,我可以直接在这里问Bard。我可以看到答案包括:数据安全、身份和访问管理以及合规性。此外,还有一些其他功能。这为我如何学习提供了很好的思路。所以,这主要是从知识获取的角度出发。

但是,如果我想进行一些编码呢?所以,如果我在这里说:“构建一个Python Colab笔记本,用于从pandas导入一个样本数据集并进行图表绘制。”让我们看看会发生什么。

此时,我实际上可以从Bard这里获得一个入门套件。看,我甚至得到了代码。我还可以选择导出这段代码。注意,代码显示为:

import pandas as pd
import matplotlib.pyplot as plt

我得到了一个不错的小代码片段。然后,如果我想继续,我实际上可以将这个响应直接导出到一个笔记本中。让我们来试试这个。

假设选择“导出到Colab笔记本”。这样做的好处是,我实际上可以两全其美:既获得Bard的结对编程协助,又能深入到一个示例笔记本中。所以,让我们继续在这里更改运行时类型。如果我们想的话,可以给它分配更多内存。从这里,我可以继续运行这个示例。

这是一个快速上手特定库的好方法,你可以直接使用它作为一个示例。

现在,我们这里遇到了一个问题,它显示:“哦,这个东西实际上没有找到。”有时你会看到这样的错误。那么,如何解决这样的问题呢?让我们来调试它。

如果我在这里实际在新标签页中打开它,我们应该能够看到,确实,这是个问题。那么我可以做的是,在这里两全其美,我可以说:“Iris数据集 pandas”,然后直接搜索它。我们可以看到这里有一个调用,我们可以实际查看它,它会给我们加载Iris数据。我要回到我们的提示,然后说:“让我们看看,比如更改代码以使用这个代替。”

所以,能够迭代的一个重要部分是理解如何接受存在问题的现实,并继续前进。好了,现在我们只需继续,选择“导出响应”,然后打开一个新的笔记本。

我们继续,选择“打开Colab”。从这里,我们得到了一个稍微不同的视图。我们可以直接写:

from sklearn.datasets import load_iris

让我们再次更改运行时类型,也许分配高内存,保存它。然后我们继续运行这个。

重要的是要明白,你不能完全指望代码是完美的。你必须具备一些来回调试的能力。现在,我们又遇到了一个问题。我们看到这个特定的列可能有问题。那么,我们该如何修复它呢?其实很简单。

我们可以通过深入代码来调试它。让我们继续在这里添加一个代码单元格。我通常会做的是,看看是否能实际加载任何东西。所以,让我们先尝试加载这部分。让我们继续把这部分代码复制出来,粘贴进去,然后运行它。这样成功了。

现在,让我们在下面添加一个代码单元格。然后实际查看一下 iris_data。我们看到,事实上,它确实加载成功了,这很好。所以我可以继续,删掉这个单元格。现在我们知道了 iris_data.columns。让我们看看是否有 iris.feature_names,问题可能在于我们实际上没有 iris.feature_names。让我们试试 iris.,然后看看我们有什么。我们有,我们确实有 feature_names。好了。我们有“Sepal length”。所以问题是,特征名称与我们预期的名称略有不同,这很容易修复。我只需在这里将“sepal length”改为这个新的“sepal length”,并将“sepal width”改为这里的新“sepal width”。

然后,如果我们回到这里,我们应该能让这个单元格运行起来。好了,现在我们看到图表可以正常工作了。

因此,在使用结对编程辅助工具的同时进行编码,确实需要一点耐心。但在本例中,我们能够克服困难,通过真正将结对编程工具视为协作者,并深入研究Iris数据和进行可视化,从而取得了相当大的进展。


总结

本节课中,我们一起学习了如何利用AI工具Bard来辅助Python和Colab笔记本的开发。我们从获取知识要点和代码片段开始,经历了导出代码、设置运行时环境、运行代码、遇到错误、调试问题以及最终成功运行数据可视化项目的完整过程。关键点在于理解AI生成的代码可能不完美,需要开发者具备调试和迭代的能力,将AI工具作为强大的协作者来提升开发效率和学习效果。

055:在笔记本中探索预期寿命数据 📊

在本节课中,我们将学习如何在Google Colab笔记本中进行探索性数据分析。我们将使用一个关于全球预期寿命的数据集,通过一系列步骤来加载、查看、筛选和分析数据,以发现有趣的模式和见解。


概述

探索性数据分析是数据科学工作流程中的关键第一步。其目的是熟悉数据、检查数据质量并初步发现趋势或异常。本节我们将使用Python的Pandas库在Colab笔记本中完成这一过程。

上一节我们介绍了数据分析的环境,本节中我们来看看如何具体操作。


连接运行时与加载库

首先,我们需要在Google Colab中设置运行环境。Colab笔记本的一个优点是它提供了专业级的计算资源。

以下是连接运行时并加载必要库的步骤:

  1. 点击“运行时”菜单,选择“更改运行时类型”。
  2. 根据需求选择硬件加速器(如GPU、TPU)或提升内存(RAM)。
  3. 连接成功后,即可运行代码单元格。

现在,我们可以加载数据分析的核心库——Pandas。

import pandas as pd

加载与初步查看数据

数据加载是分析的基础。我们将加载一个关于全球预期寿命的数据集。

运行以下代码来加载数据并查看其前几行:

# 假设数据文件名为‘life_expectancy.csv’
df = pd.read_csv('life_expectancy.csv')
df.head()

初步查看时,数据可能显得杂乱,包含从1960年开始的多个国家和年份。我们需要进一步深入查看其结构和列信息。


理解数据结构

为了理解我们正在处理的数据,需要查看数据的列名和整体信息。

以下是查看数据结构的步骤:

  1. 使用 df.columns 查看所有列名。
  2. 使用 df.info() 获取数据概览,包括行数、列数和数据类型。

查看列名后,我们可能发现数据包含“国家名称”、“国家代码”、“指标名称”、“指标代码”以及多个年份的列(如“2020”)。


筛选与排序数据

为了聚焦分析,我们通常需要筛选和排序数据。例如,我们可能只关心2020年的最新数据,并想看看预期寿命最高和最低的国家。

以下是筛选和排序2020年数据的步骤:

# 选择我们关心的列:国家名称和2020年数据
df_2020 = df[['Country Name', '2020']]

# 按2020年预期寿命升序排序,并查看前25名
top_25 = df_2020.sort_values(by='2020', ascending=False).head(25)
print(top_25)

执行后,我们可以看到2020年预期寿命最高的25个国家和地区,例如中国香港、日本、韩国等。


计算描述性统计

描述性统计能帮助我们快速把握数据的集中趋势和分布情况。

我们可以计算数据的中位数、均值等统计量:

# 计算各年份数据的中位数
median_by_year = df.iloc[:, 4:].median() # 假设从第5列开始是年份数据
print(median_by_year)

通过观察不同年份的中位数,我们可以发现全球预期寿命随时间推移呈现上升趋势。

如果数据中存在缺失值,我们可以使用 df.dropna() 将其删除。


深入分析特定维度

数据可能包含细分维度,例如区分性别。我们可以专门分析女性的预期寿命数据。

以下是分析女性预期寿命的步骤:

  1. 筛选出指标名称包含“女性”的数据行。
  2. 对其进行排序和描述性统计。
# 假设‘Indicator Name’列中包含‘female’关键词
df_female = df[df['Indicator Name'].str.contains('female', case=False, na=False)]
top_female = df_female.sort_values(by='2020', ascending=False).head(10)
print(top_female)
print(df_female['2020'].describe())

分析结果显示,日本、西班牙、瑞士等国的女性预期寿命名列前茅。


案例研究:聚焦特定国家或群体

我们可以将分析聚焦于特定国家或国家集团,例如G20国家,以进行对比。

以下是分析G20国家预期寿命的步骤:

  1. 定义一个G20国家列表。
  2. 从数据中筛选出这些国家的数据。
  3. 进行排序和比较。
g20_countries = [‘Argentina‘, ‘Australia‘, ‘Brazil‘, ‘Canada‘, ‘China‘, ...] # 完整G20列表
df_g20 = df[df[‘Country Name‘].isin(g20_countries)]
df_g20_sorted = df_g20.sort_values(by=‘2020‘, ascending=False)
print(df_g20_sorted[[‘Country Name‘, ‘2020‘]])

通过对比可以发现,在G20国家中,美国的预期寿命排名相对靠后,接近巴西、沙特阿拉伯等国,这与其实力地位形成对比,是一个值得深入研究的现象。


寻找相似模式与可视化

我们还可以寻找与目标国家(如美国)预期寿命相似的其他国家,以提供更多背景信息。

以下是寻找相似国家的步骤:

# 定义目标值(例如美国的预期寿命)
us_life_exp = df.loc[df[‘Country Name‘] == ‘United States‘, ‘2020‘].values[0]

# 计算所有国家与美国的差值,并找出差值最小的5个国家
df[‘difference‘] = abs(df[‘2020‘] - us_life_exp)
similar_countries = df.nsmallest(6, ‘difference‘) # 包含美国自己
print(similar_countries[[‘Country Name‘, ‘2020‘]])

最后,可视化能直观展示分析结果。我们可以使用Seaborn库绘制图表。

import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 8))
sns.barplot(x=‘2020‘, y=‘Country Name‘, data=top_25)
plt.title(‘Top 25 Countries by Life Expectancy (2020)‘)
plt.xlabel(‘Life Expectancy‘)
plt.show()

图表清晰显示,许多现代化国家的预期寿命集中在85-88岁之间,而美国则明显偏低,这突出了进一步调查的必要性。


总结

本节课中我们一起学习了在Google Colab笔记本中进行探索性数据分析的全过程。我们从连接运行时和加载数据开始,逐步进行了数据查看、筛选排序、统计计算、维度深入分析以及最终的可视化。通过这些步骤,我们不仅熟悉了数据集,还发现了“美国预期寿命相对较低”这样一个值得深入探讨的洞察。你可以将这些技术应用于你自己的数据探索任务中。

056:加载含敏感数据的数据框 🔐

在本节课中,我们将学习如何安全地加载包含敏感信息的数据框。核心在于理解并应用数据分类与保护机制,确保敏感数据在加载时得到妥善处理。

上一节我们介绍了数据框的基本操作,本节中我们来看看如何处理其中的敏感数据。

数据分类方法论 📊

首先,需要关注所使用的数据分类方法。最常见的方式之一是使用不同的标签来标记数据。

以下是几种常见的标签类型:

  • 敏感度标签:适用于特定信息,标识其敏感级别。
  • 保留期标签:规定数据应保留多长时间。
  • 通用敏感信息标签:将数据整体标记为敏感。

此外,还有不同的分类器类型,例如可训练分类器。这种分类器能够自动检测特定类型的数据并对其进行保护和分类。

控制措施设置 🛡️

一旦设置了这些分类控制,就可以进一步配置不同的保护层面。

以下是需要配置的关键方面:

  • 存储类型:选择安全的存储介质。
  • 加密:对静态和传输中的数据进行加密。
  • 访问控制:限制谁可以访问数据。
  • 数据销毁:安全地删除不再需要的数据。
  • 数据丢失防护与日志记录:监控数据访问行为,记录谁在何时访问了哪些资源。
  • 资源追踪:跟踪数据资源的移动路径。

安全加载数据框 ✅

最后,在完成所有上述准备工作后,就可以安全地从数据框加载信息了,因为数据已被正确分类和保护。

被编辑的敏感信息,在数据框实际加载时,要么会被置为null值,要么根本不会显示出来。因此,安全加载数据框的一个关键方面在于背后的分类工作、成功的模型训练。这些前期工作完成后,在加载数据框时就不需要额外的操作了,数据已经是预先处理好的状态。

本节课中我们一起学习了安全加载含敏感数据数据框的完整流程:从理解数据分类方法论,到设置各项安全控制措施,最终实现数据的安全加载。关键在于通过前期完善的分类和保护机制,确保敏感信息在访问时自动得到屏蔽或处理。

057:Databricks笔记本集成MLflow入门教程 🚀

在本节课中,我们将要学习如何在Databricks环境中开始使用MLflow进行机器学习实验跟踪。我们将从Databricks机器学习平台概述开始,逐步介绍数据摄取、快速启动、实验跟踪以及超参数调优等核心概念。

概述:什么是Databricks机器学习? 🤔

Databricks机器学习是一个完整的解决方案或平台,它允许你训练模型、跟踪训练运行、创建特征表以及共享模型。

官方文档中的一张图表展示了其工作流程:数据准备位于“数据源”和“Delta表”阶段。随后,数据进入创建精炼特征的阶段,这些特征被放入特征存储库。这使得在笔记本中进行自动化机器学习(AutoML)以及使用这些模型和特征变得更加容易。接着,可以执行多个实验,例如自动超参数调优任务。一旦完成并选择了合适的模型,就可以将其部署为批处理模式或在线服务模式。因此,这是一个从数据到生产的全面解决方案。

数据摄取:多种方式导入表格 📥

有多种不同的方式可以将数据表导入Databricks。

以下是几种主要方法:

  • 上传文件:这是最简单的入门方式。
  • 使用DBFS(Databricks文件系统)。
  • Azure集成:一个独特之处在于可以连接到Azure存储,如Azure Blob存储系统。
  • 第三方集成:也可以连接到并使用第三方工具,存在许多不同的第三方集成选项。

快速启动机器学习工作流 🏃

要快速开始,首先需要创建一个一到两个节点的ML工作集群。

如果你在Databricks的“机器学习”选项卡中,选择“集群”,需要确保Databricks运行时版本使用的是ML版本。存在标准运行时和ML运行时。ML运行时包含了MLflow,并允许你与实验跟踪以及机器学习工作流中预期的其他功能进行集成。

创建集群后,下一步是附加一个笔记本。

你可以导入默认控制台中包含的“ML快速入门”笔记本,右键单击并将其附加到集群,然后就可以开始在该笔记本上工作了。这可能是最好的入门方式:创建一个集群,等待集群启动后,运行快速入门模型训练笔记本。

实验跟踪与模型选择 🔬

如果你已经训练了一个分类模型,你会看到许多不同实验的概念。你可以在实验UI中选择要部署到生产环境的超参数或模型。

实验UI是许多模型跟踪功能的核心,因为它允许你直接从笔记本跳转到实验选项卡,并实际查看你所做的操作。

进入实验选项卡后,其强大之处在于你可以通过侧边栏切换,查看不同的实验以及创建的模型,包括显示在实验选项卡中的指标所体现的准确度。

深入探讨:超参数调优 ⚙️

上一节我们介绍了实验跟踪,本节中我们来看看如何利用Databricks进行超参数调优。

你可以通过一个API调用运行许多不同的实验,这就是实验跟踪界面的强大之处。你会在许多不同的工具中看到这一点:你希望所有内容都在一个界面中,以便可以查询、查看已运行的不同实验,并找出该实验的最佳指标。

如果你进入超参数比较功能,你还可以执行散点图,查看不同的运行,甚至可以深入研究细节,例如学习率与损失的关系。

并行超参数调优 🚄

同样重要的是,你可以进行并行超参数调优,这可以让你真正优化找到解决方案的速度。

以下是实现此目的的一种方法:

  • 使用Hyperopt:它提供了分布式异步超参数优化。
  • 有三种不同的算法可供选择:随机搜索、Parzen估计器树(TPE)或自适应TPE。
  • 你可以使用Apache Spark或MongoDB将其并行化。

因此,开始并行超参数调优有很多方法。

查看这里的代码,你可以看到在较小的集群或Databricks社区版上,进行并行化也是个好主意。但如果你有一个大型集群,可以将其设置为任意你想要的数字。

分析结果与最终比较 📊

当你完成搜索运行后,最后还可以查看你关心的指标,例如可能是曲线下面积(AUC)。你可以要求查看最后10个结果,然后它会给出你执行的特定搜索查询的最佳运行,并且你可以在代码中完成所有这些操作。

最后,当你完成运行比较后,可以在这里的UI中查看所有运行,然后根据你想对这些超参数和实验做什么,展开并进行比较。

总结 📝

本节课中我们一起学习了在Databricks平台上集成MLflow进行机器学习实验管理的完整流程。我们从Databricks ML平台概述开始,了解了数据摄取的多种途径。接着,我们学习了如何通过创建ML集群和运行快速入门笔记本来启动项目。核心部分深入探讨了使用MLflow进行实验跟踪、模型选择以及高效的超参数调优,特别是并行调优技术。最后,我们掌握了如何分析和比较实验结果以选择最佳模型。这套工作流程为从数据准备到模型部署的端到端机器学习项目提供了强大支持。

058:MLflow与Databricks端到端机器学习工作流 🚀

在本节课中,我们将学习如何利用Databricks和MLflow构建一个端到端的机器学习运维(MLOps)工作流,并了解如何将在此平台上训练的模型迁移到其他环境中。

概述

我们将探讨一个完整的机器学习项目流程,从数据导入、模型训练与实验,到模型注册与部署。核心在于理解MLflow的API如何提供跨平台的灵活性,使得模型不局限于Databricks环境。

端到端工作流详解

上一节我们介绍了MLOps的基本概念,本节中我们来看看一个结合Databricks和MLflow的具体实现案例。

1. 数据准备与导入

首先,可以从Kaggle等平台获取数据集。以下是将数据导入Databricks并创建表的典型步骤:

  1. 将数据集上传至Databricks。
  2. 使用Databricks文件系统(DBFS)和用户界面(UI)将数据创建为表格。

2. 自动化实验与模型训练

数据准备完成后,可以启动自动化机器学习(AutoML)实验。实验完成后,系统会筛选出最佳模型。

3. 模型注册与管理

最佳模型可以被注册到Databricks的模型注册表中。注册后,模型可以被版本化和管理。如果选择通过Databricks提供服务,可以将其部署为一个端点(Endpoint)。

然而,利用MLflow的API,我们并不局限于Databricks平台。

4. 跨平台部署与调用

得益于MLflow的标准化模型格式和API,注册后的模型可以轻松迁移到其他云环境。以下是几种可能性:

  • 可以从AzureGitHub CodespacesAWS Cloud9等任何云环境调用MLflow API。
  • 可以基于模型开发微服务,并部署到其他环境。
  • 例如,可以将模型容器化并推送到AWS Elastic Container Registry (ECR),然后自动部署到AWS App Runner进行预测。

一旦模型通过MLflow注册,我们就拥有了极大的灵活性,几乎可以在任何平台上进行操作。

深入模型构件

让我们更深入地查看一个具体的模型示例。这里有一个之前通过AutoML实验训练的XGBoost模型。

查看该模型,我们可以看到它包含了所有必要的构件(Artifacts):

  • 实际模型文件(如 model.pkl
  • Conda环境配置文件conda.yaml
  • 输入示例input_example.json):展示了如何调用该模型。
  • 依赖文件requirements.txt
  • 模型架构信息:指明了调用时所需的数据格式(Schema)。

此外,文档还提供了具体的调用命令,例如如何在Spark DataFrame或Pandas DataFrame上进行预测。这使得我们可以直接在平台上测试和调用模型。

模型注册与外部调用示例

除了在平台内使用,我们还可以将模型注册到中央仓库。例如,注册一个名为 people 的模型。

注册后,就可以从其他平台通过API调用该模型的特定版本。这里有一个GitHub上的项目示例,它实现了一个“假新闻预测”服务。

项目创建了一个包含示例数据(如“外星人即将入侵地球”)的请求载荷(Payload),并将其POST到预测端点,成功预测该新闻为假新闻。这完整演示了如何将之前展示的模型构件包装成一个可工作的服务。

若想直接通过 curl 命令调用生产环境端点,仅需要一个Databricks令牌(Token)即可。同样,也可以编写预测命令来调用已注册模型的端点。

在GitHub Codespaces中操作模型

跨平台能力的另一个例证是GitHub Codespaces。我们可以在该环境中进行设置,与模型交互,甚至下载模型构件。

然后,可以重复本课程早先演示的步骤:使用MLflow加载模型并进行预测。这证明了模型的可移植性。

总结

本节课中我们一起学习了如何构建一个基于Databricks和MLflow的端到端MLOps工作流。关键在于,MLflow的API开启了远超Databricks平台本身的巨大可能性。

回顾整个流程图,核心在于模型通过MLflow注册后,其构件和API标准化使得它可以与任何云环境对接。这确保了机器学习项目从实验到生产的高度灵活性和可移植性。

059:使用Polars探索全球预期寿命 📊

在本节课中,我们将学习如何使用Rust生态中的Polars库,构建一个高性能的命令行工具,用于探索和分析全球预期寿命的CSV数据集。我们将看到如何将数据处理逻辑封装成库,并通过命令行提供统一的交互界面。


上一节我们介绍了Polars项目的基本概念,本节中我们来看看具体的代码实现和工具构建。

首先,我们有一个lib.rs文件,这是放置公共工具函数的常见位置。以下是其核心内容:

use polars::prelude::*;

pub fn read_csv(path: &str) -> Result<DataFrame, PolarsError> {
    CsvReader::from_path(path)?.finish()
}

pub fn print_rows(df: &DataFrame) {
    println!("{}", df);
}

pub fn print_schema(df: &DataFrame) {
    println!("{:?}", df.schema());
}

pub fn print_shape(df: &DataFrame) {
    println!("Shape: {} rows, {} columns", df.height(), df.width());
}

代码中,我们使用polars库,并构建了一系列辅助函数。pub关键字将这些函数公开暴露,以便在组织内供他人使用。read_csv函数用于读取CSV文件,其他函数则用于打印数据框的行、模式和形状。


Cargo.toml文件中,我们只需要两个依赖库。

[dependencies]
polars = "0.26.2"
clap = "3.2"

我们固定了Polars的特定版本,这是Rust确保可重现构建的一个优势。clap则是一个命令行参数解析库。


现在,让我们看看main.rs文件如何将整个工具整合在一起。

首先,我们设置一个常量指向数据目录,并使用clap配置命令行界面。

use clap::{Arg, Command};

const DATA_PATH: &str = "./data/life_expectancy.csv";

fn main() {
    let matches = Command::new("life_expectancy_explorer")
        .version("1.0")
        .author("Your Name")
        .about("Explores global life expectancy data")
        .arg(Arg::new("print").short('p').long("print").help("Prints the dataframe"))
        .arg(Arg::new("schema").long("schema").help("Prints the schema"))
        // ... 定义更多命令,如 describe, shape, sort
        .get_matches();

    // 读取数据
    let df = read_csv(DATA_PATH).expect("Failed to read CSV");

    // 根据匹配的命令执行相应操作
    if matches.contains_id("print") {
        print_rows(&df);
    } else if matches.contains_id("schema") {
        print_schema(&df);
    }
    // ... 处理其他命令
}

main函数中,我们使用模式匹配来执行用户通过命令行输入的不同操作。例如,如果用户输入--print,则调用print_rows函数。这种结构使得添加新功能变得清晰简单。


以下是该工具支持的一些核心操作列表:

  • 打印数据:查看数据集的前几行。
  • 查看模式:了解每一列的数据类型。
  • 查看形状:获取数据框的行数和列数。
  • 数据排序:例如,按预期寿命对国家和地区进行排序。

构建完成后,我们可以运行这个工具。例如,运行./explorer --shape会显示数据形状为 3行 x 66列,包含国家名称、国家代码和指标等列。

我们还可以进行更复杂的操作,比如排序。运行./explorer --sort可以按预期寿命列出排名前10的国家和地区。

# 示例输出可能类似于:
1. Japan | 84.6
2. Switzerland | 83.8
3. Singapore | 83.6
4. Australia | 83.4
5. Spain | 83.4
...

从结果中可以清晰地看到,亚洲、澳大利亚和欧洲的一些国家和地区拥有非常高的预期寿命。这得益于我们构建的工具能够快速、清晰地展示数据。


本节课中我们一起学习了如何使用Rust和Polars构建一个数据探索命令行工具。总而言之,对于数据框处理,纯Rust解决方案相比传统的脚本语言(如Python或R)具有多重优势,包括更高的安全性、出色的可移植性以及卓越的性能。在进行高性能数据工程工具开发时,考虑采用基于纯Rust的解决方案是一个值得重视的方向。

060:云端开发工作空间优势 🚀

在本节课中,我们将要学习云端开发工作空间的概念及其优势。我们将探讨为何这种开发模式正在成为软件工程、数据工程和机器学习等多个领域的新趋势,并对比传统本地开发环境与云端环境的差异。

传统本地开发环境的局限性 💻

上一节我们介绍了课程背景,本节中我们来看看传统的本地开发环境。通常,我们使用笔记本电脑或工作站进行开发。这种环境存在几个显著问题。

首先,本地环境是非确定性的,没有统一、有保障的运行环境。其次,环境中可能安装了您不需要的软件包,造成干扰或冲突。此外,为本地机器配置高性能硬件(如GPU和大容量固态硬盘)的成本非常高昂。

另一个关键问题是,本地开发环境通常与实际的云部署环境不一致。如果您在云端运行应用,那么本地环境(除少数例外情况)与生产环境存在差异。虽然可以使用容器技术来缓解这个问题,但这增加了复杂性。

云端开发环境的兴起 ☁️

既然了解了本地环境的挑战,接下来我们看看云端开发环境如何解决这些问题。目前市场上有多种云端开发环境,它们具有许多共同特点。

以下是几种主流的云端开发环境:

  • GitHub Codespaces:其独特优势在于与GitHub Actions(持续集成系统)和GitHub Copilot(基于OpenAI Codex的代码辅助工具)轻松集成。整个开发流程与GitHub平台紧密耦合,是一个高效的开发环境。
  • AWS Cloud9:提供定制化功能,包括基于角色的权限管理,无需在代码中硬编码API密钥。它与AWS Lambda、S3、API Gateway等服务深度集成,便于构建无服务器应用。
  • AWS CloudShell:这是一个更轻量级的版本,适合进行简单的软件工程任务,并允许在Bash、Zsh或PowerShell之间切换。
  • GCP Cloud IDE 与 Cloud Shell:谷歌云平台提供了自己的云端编辑器和Shell环境。
  • Azure Cloud IDE 与 Cloud Shell:微软Azure平台也提供了类似的云端编辑器和Shell环境。

这些环境的核心共性是:它们都是功能强大、即用即弃、预配置好的环境,并且与各自的云平台深度集成。

云端环境的独特优势 ⚡

在列举了主要平台后,我们来深入探讨云端开发工作空间的具体优势。这些优势正在推动开发工作流向云端迁移。

云端环境通常预装了各种开发工具包(SDK),并且其物理位置就在云数据中心内。这带来了一个革命性的好处:数据共置

例如,当您需要处理TB级的数据时,如果在咖啡馆使用笔记本电脑,来回传输这些数据将非常困难且低效。但如果数据本就存储在云端,您只需在同一个云区域启动一个开发环境,就能直接、高速地访问数据,无需迁移。

总结 📝

本节课中我们一起学习了云端开发工作空间相较于传统本地环境的巨大优势。我们分析了本地环境在确定性、成本及与部署环境一致性方面的不足,并介绍了GitHub Codespaces、AWS Cloud9等主流云端开发环境。最后,我们重点探讨了云端环境的核心优势——深度云集成与数据共置能力,这代表了未来开发工作的趋势,非常值得开发者深入学习和采用。

061:Python与Rust的GCP入门

在本节课中,我们将学习如何在Google Cloud Platform(GCP)上开始使用Python和Rust进行开发。我们将从了解GCP的免费套餐开始,然后设置Cloud Shell环境,并分别运行Python和Rust的示例项目。

概述

GCP提供了一系列免费产品和服务,非常适合初学者和开发者进行学习和实验。我们将首先探索这些免费资源,然后通过Cloud Shell环境,快速搭建Python和Rust的开发环境,并运行简单的“Hello World”程序。

GCP免费套餐概览

上一节我们介绍了课程目标,本节中我们来看看GCP为开发者提供的免费资源。

GCP免费套餐提供对20种免费产品的访问权限,以及300美元的免费信用额度。这些信用额度需要在三个月内使用。部分免费产品包括:

  • Compute Engine(计算引擎)
  • Cloud Storage(云存储)
  • BigQuery
  • Kubernetes
  • App Engine
  • Cloud Run
  • Cloud Build
  • Stackdriver
  • File Store
  • Pub/Sub
  • Cloud Functions
  • Vision AI
  • Speech-to-Text
  • Natural Language API
  • AutoML

开始使用Cloud Shell

了解了GCP的免费资源后,我们进入实际操作环节。首先,我们需要进入GCP控制台并启动Cloud Shell。

Cloud Shell是一个基于浏览器的命令行环境,内置了多种开发工具,可以立即开始构建和测试解决方案。在Cloud Shell中,我们可以轻松管理项目、运行代码和访问GCP服务。

设置Python开发环境

上一节我们启动了Cloud Shell,本节中我们来看看如何配置Python开发环境。

首先,创建一个Python虚拟环境,这有助于隔离项目依赖。使用以下命令创建并激活虚拟环境:

python3 -m venv .venv
source .venv/bin/activate

为了在每次打开Cloud Shell时自动激活虚拟环境,可以将激活命令添加到~/.bashrc文件中。

接下来,可以克隆一个包含示例代码的Git仓库,并安装项目依赖。以下是使用make命令的示例流程:

git clone <仓库地址>
cd <项目目录>
make install  # 安装依赖包
make lint     # 运行代码检查

设置Rust开发环境

在Cloud Shell中配置好Python环境后,我们也可以轻松地安装Rust。

Rust以其高性能和内存安全性著称,在某些场景下性能远超Python。通过运行官方安装脚本,可以一键安装Rust及其包管理器Cargo:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

安装完成后,可以立即创建一个新的Rust项目并运行:

cargo new hello
cd hello
cargo run

cargo run命令会编译并运行项目,输出经典的“Hello, world!”。项目结构清晰,依赖管理在Cargo.toml文件中进行。

管理GCP计算资源

掌握了两种语言的开发环境设置后,我们来看看如何在GCP上管理计算资源,例如虚拟机实例。

在GCP控制台中,可以直观地创建和管理Compute Engine虚拟机实例。创建时,界面上会清晰显示不同配置机器的预估每小时和每月费用,帮助控制成本。

更重要的是,几乎所有操作都可以通过gcloud命令行工具完成,这对于自动化和脚本编写非常有用。例如,要列出当前项目中的所有虚拟机实例,可以使用:

gcloud compute instances list

要获取某个命令的详细帮助信息,可以运行gcloud help [COMMAND]。GCP的设计是面向命令行的,熟悉命令行将极大地提升使用效率。

总结

本节课中我们一起学习了在Google Cloud Platform上入门Python和Rust开发的基础步骤。

我们首先了解了GCP丰富的免费套餐。然后,通过Cloud Shell快速搭建了开发环境:为Python项目创建了虚拟环境并管理依赖;为Rust项目安装了工具链并运行了示例。最后,我们还学习了如何在控制台和命令行中查看与管理GCP的计算资源。

通过本教程,你应该已经掌握了在GCP上开始进行Python和Rust开发的基本流程,可以自行尝试这些命令并探索更多功能。

062:使用GCP Cloud Shell 🚀

在本节课中,我们将学习如何在Google Cloud Shell环境中进行系统管理任务和轻量级开发。我们将探索其核心功能,包括文件管理、环境配置,并最终在Cloud Shell中运行一个简单的Rust Web应用程序。


概述与界面介绍

Google Cloud Shell是一个非常有趣的环境,既可以执行系统管理任务(例如与存储通信或控制应用程序),也可以进行轻量级开发。让我们先看看你可以做的一些事情。

第一步,你拥有项目窗口。在这里,你可以查看现有项目,或者创建一个新项目。你也可以打开一个功能齐全的编辑器。我们暂时不会这样做,但你可以打开一个具有更好代码补全功能的开发环境。你还可以发送键盘组合键。你可以进入终端设置,更改主题、文本大小等选项。你也可以预览正在运行的Web应用程序(我们稍后会做),还可以查看会话信息,例如你已经使用了多少配额。


文件上传与下载 📁

我们还可以上传或下载文件。让我们继续看看这个功能。如果我们执行 ls 命令,可以看到这里有一个 readme 文件。如果我想下载它,我可以选择“下载”。它会传输我目录中的所有内容。

现在,我也可以单独下载某个文件。如果我浏览并选择一个单独的文件,它也会执行下载操作。因此,有一种方法可以在Cloud Shell环境中来回传输文件,也可以直接上传文件到Cloud Shell环境。


环境内的轻量级开发 💻

现在需要注意的另一件有趣的事情是,你可以在这个环境中进行轻量级开发。

首先,我将通过 rustup 在这个环境中安装Rust。我们将执行 curl 命令来安装,它会继续进行。

每当我安装像新开发环境这样的新东西时,通常需要编辑 .bashrc 文件,以便方便地重新加载,而不必一遍又一遍地手动 source 某个文件。在这个特定情况下,我们会看到同样的事情发生。你可以看到它已经自动安装了。如果我查看这里,它会加载cargo环境。如果我查看我的 .bashrc 文件,你会看到我们也已经把它加载到 .bashrc 中了。这使我们能够始终拥有 cargo 工具可用,这是用于构建新Rust项目的工具。


创建并运行Rust Web应用

现在,我们可以使用 cargo new 命令创建一个新项目。然后输入一个名字,我们可以称之为 web,它会放在当前工作目录中。所以这实际上取决于你在这个案例中想要做什么。让我们继续在这里使用根目录。

然后,如果我进入这个 Cargo.toml 文件,我需要更改它。我们可以看看它的版本是 0.4.31。让我们继续把它放进去。

我们将写成 = "0.4.31"。完美,现在我们设置好了。

我们需要做的就是编辑里面的源文件。所以我们说 src/main.rs

然后我们只需要在里面放一个“Hello World”程序。我将在这里抓取一个简单的“Hello World”示例。

我们将执行 set paste 并把它放进去。好的。

现在,我们已经能够通过使用 cargo run 来运行它了。我只需转到这里的Web预览菜单,我们看看这个,我就能看到这个“Hello World”应用程序。所以它实际上是一个进行快速原型开发的不错的环境。

如果我们想进入代码本身并稍微修改它,让我们进入这里的代码,只是改变它,以便我们知道我们可以轻松地进行修改。

我们可以说 Hello world exploring Cloud Shell.。好的,让我们继续保存它。我们将再次输入 cargo run

它再次构建。当然,我们要做的就是转到Web预览,在端口8080上预览。好了。Hello world exploring Cloud Shell.


总结 🎯

本节课中我们一起学习了Google Cloud Shell的核心功能。简而言之,Cloud Shell是一个出色的轻量级开发环境,可以执行命令,也可以进行一些原型开发,并且你可以通过编辑你的 .bashrc 文件来自定义你的环境。

063:AWS CloudShell学习 🚀

在本节课中,我们将要学习如何使用AWS CloudShell。AWS CloudShell是一个基于浏览器的命令行工具,它允许你直接访问和管理AWS资源,无需在本地安装任何软件或管理额外的凭证。这对于在不同地点(如图书馆或咖啡馆)快速开始工作非常有用。

概述

AWS CloudShell提供了对AWS资源和工具的浏览器内命令行访问。它的一个关键特性是,由于你身处AWS生态系统内部,操作速度会非常快。接下来,我们将通过实际操作来探索它的各项功能。

启动与重置环境

上一节我们介绍了CloudShell的基本概念,本节中我们来看看如何启动和重置环境。

在AWS管理控制台的任何区域,你都可以找到并启动CloudShell。启动后,你可能会看到之前工作的历史消息。一个非常实用的技巧是,如果你在CloudShell中进行了大量实验性操作并导致环境混乱,你可以选择删除当前环境并重新开始。

操作示例:重置环境

# 在CloudShell界面中,通过“Actions”菜单选择重置或删除环境。

界面与多窗口操作

了解了如何启动环境后,我们来看看CloudShell强大的界面管理功能。

CloudShell的“Actions”菜单提供了多项实用功能。例如,你可以打开新的Shell标签页,也可以将窗口分割成上下或左右布局。这对于同时运行前端服务(如Flask)并在下方调用它,或者构建一个终端仪表盘来说非常方便。

以下是“Actions”菜单提供的主要功能列表:

  • 打开新Shell:可以打开任意数量的新标签页。
  • 分割为行:创建上下分割的窗口布局。
  • 分割为列:创建左右分割的窗口布局。

文件上传与下载

掌握了界面操作后,一个核心需求就是如何在本地和CloudShell之间传输文件。

CloudShell允许你从本地上传单个文件到其家目录(/home/cloudshell-user),也支持从CloudShell下载文件到家目录。家目录的存储空间限制为1GB,这对于上传CSV文件或机器学习数据集等常见数据工程任务通常足够。

操作示例:上传与下载

# 1. 通过“Upload file”按钮上传本地文件。
# 2. 在Shell中解压或处理文件。
unzip cats_dogs_small.zip

# 3. 通过“Download file”按钮,输入文件完整路径进行下载。
# 例如:/home/cloudshell-user/cats_dogs_small/cat.0.jpg

你可以使用 df -h 命令查看存储空间使用情况。

支持多种Shell环境

除了文件操作,CloudShell还支持多种Shell环境,以适应不同开发者的偏好。

CloudShell默认使用Bash,但你也可以轻松切换到Zsh或PowerShell。你可以通过编辑相应的配置文件(如 ~/.bashrc~/.zshrc)来自定义Shell环境。

操作示例:切换与自定义Shell

# 查看当前Shell
echo $SHELL

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/duke-rs-prog-dtengi-dop/img/84b7999a7bd5f3c62a978f0eac362292_17.png)

# 切换到Zsh
zsh

# 编辑Bash配置文件
nano ~/.bashrc
# 添加自定义命令,例如:echo "This is Bash"

这对于.NET开发者使用PowerShell,或者喜欢Zsh自动补全等高级功能的用户来说非常有用。

与AWS服务交互:以S3为例

现在,让我们利用CloudShell环境实际做一些工作,特别是与AWS服务交互。

CloudShell预装了AWS CLI,因此你可以直接通过命令行管理AWS资源,无需配置访问密钥。这是一个强大的功能,允许你以编程方式控制AWS平台。

操作示例:使用AWS CLI管理S3

# 1. 创建一个S3存储桶
aws s3 mb s3://your-unique-bucket-name-20231121

# 2. 列出所有S3存储桶并计数
aws s3 ls | wc -l

# 3. 将本地目录同步到S3存储桶
aws s3 sync ./local_directory s3://your-bucket-name/

# 4. 从S3存储桶下载文件
aws s3 cp s3://your-bucket-name/remote_file.zip ./

通过同步命令,你可以快速将大量文件上传到S3,这对于数据流水线或机器学习工程任务来说是无价之宝。

总结

本节课中我们一起学习了AWS CloudShell的核心功能与应用。我们了解了如何启动和重置环境、使用多窗口界面、在本地与CloudShell之间传输文件、切换不同的Shell环境,以及最重要的——如何利用CloudShell内集成的AWS CLI直接与S3等服务进行交互,从而高效地完成数据工程和DevOps任务。CloudShell作为一个基于浏览器的集成工具,极大地简化了在AWS云上进行命令行操作和资源管理的流程。

064:在AWS CloudShell中快速原型化AI API 🚀

在本节课中,我们将学习如何利用AWS CloudShell和命令行工具,快速原型化和测试AWS的AI服务,特别是Amazon Comprehend。我们将从简单的文本情感分析开始,逐步构建一个从网页抓取数据、进行实体提取和统计分析的完整数据科学流程。


概述

AWS提供了丰富的AI服务,从高级文本分析、自动代码审查到聊天机器人和预测等。我们将重点介绍Amazon Comprehend,这是一个用于自然语言处理(NLP)的服务。我们将使用AWS CloudShell,这是一个基于浏览器的命令行工具,无需在本地安装任何软件,即可快速测试和原型化想法。


启动与基础命令

首先,我们打开AWS CloudShell。要开始使用Amazon Comprehend服务,可以在命令行中输入 aws comprehend 并查看帮助信息。

aws comprehend help

执行上述命令会显示所有可用的子命令,例如 batch-detect-dominant-languageclassify-document 等。这为我们提供了服务功能的概览。


进行情感分析

接下来,我们尝试一个简单的文本情感分析。我们将分析“I love C#”这句话的情感倾向。

aws comprehend detect-sentiment \
    --language-code "en" \
    --text "I love C#"

执行命令后,服务会返回一个JSON格式的结果,其中包含 Sentiment 字段,其值可能是 POSITIVENEGATIVENEUTRAL。对于我们的例子,预期会返回 POSITIVE

这个简单的例子展示了如何快速调用AI API。然而,实际应用通常涉及更复杂的数据源和更大的文本量。


处理外部数据源

直接从命令行输入文本是有限的。为了处理更复杂的数据,例如网页内容,我们需要从外部获取文本。

一个方法是使用名为 links 的命令行网页浏览器工具。我们可以先安装它:

sudo yum install links -y

安装完成后,我们可以使用 links-dump 参数将网页的文本内容输出到终端。例如,获取维基百科上关于阿尔伯特·爱因斯坦的页面:

links -dump https://en.wikipedia.org/wiki/Albert_Einstein

这会将整个页面的文本内容输出到控制台。但Amazon Comprehend的 detect-sentiment 命令一次只能处理少于5000字节的文本。


处理文本长度限制

为了遵守5000字节的限制,我们需要从获取的网页文本中截取前5000个字节。我们可以结合使用 head 命令和管道操作。

首先,将网页文本输出,然后通过管道传递给 head -c 5000 命令来截取前5000字节:

links -dump https://en.wikipedia.org/wiki/Albert_Einstein | head -c 5000

现在,我们有了符合长度要求的文本。为了在后续命令中方便地使用这段文本,我们可以将其存储在一个Bash变量中。


使用变量存储文本

在Bash中,我们可以使用反引号或 $() 语法将命令的输出赋值给一个变量。

TEXT=$(links -dump https://en.wikipedia.org/wiki/Albert_Einstein | head -c 5000)

要验证变量是否已正确存储文本,可以使用 echo 命令:

echo $TEXT

现在,变量 $TEXT 中包含了我们截取后的维基百科文本,可以用于后续的AI分析。


对变量文本进行情感分析

有了存储在变量中的文本,我们可以将其传递给AWS Comprehend进行情感分析。关键点在于,我们需要将变量作为 --text 参数的值传递。

aws comprehend detect-sentiment \
    --language-code "en" \
    --text "$TEXT"

执行此命令后,服务会分析这5000字节文本的整体情感。对于一篇百科全书文章,结果可能显示为 NEUTRAL(中性),但也可能包含相当比例的 POSITIVE(积极)情绪,这反映了文章作者对主题人物的普遍正面评价。

这展示了如何将外部数据源与AI服务动态结合。


进阶分析:实体提取

除了情感分析,Amazon Comprehend还能从文本中提取命名实体(如人名、地点、组织)。我们使用 detect-entities 子命令,并指定输出格式为文本以便于后续处理。

aws comprehend detect-entities \
    --language-code "en" \
    --text "$TEXT" \
    --output text

命令执行后,会返回一个列表,其中包含识别出的实体及其类型(例如,“Albert Einstein” 被识别为 “PERSON”)。原始输出格式可能包含多列信息。


构建数据处理管道

为了进行更深入的分析,例如统计哪个实体出现最频繁,我们可以将AWS Comprehend的命令嵌入到Bash管道中。这允许我们进行数据清洗、排序和计数。

以下是一个复杂的管道命令示例,它执行实体提取、清洗文本、排序并统计出现频率最高的实体:

aws comprehend detect-entities --language-code "en" --text "$TEXT" --output text | \
  awk '{print $2}' | \
  tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]' | sed '/^$/d' | \
  sort | \
  uniq -c | \
  sort -nr | \
  head -10

让我们分解这个命令:

  1. aws comprehend ...: 提取实体并以文本格式输出。
  2. awk '{print $2}': 提取输出结果中的第二列(实体名称)。
  3. tr '[:upper:]' '[:lower:]': 将所有文本转换为小写,确保“Einstein”和“einstein”被视作同一实体。
  4. tr -d '[:punct:]': 删除标点符号。
  5. sed '/^$/d': 删除可能存在的空行。
  6. sort: 对实体名称进行排序,这是 uniq -c 工作的前提。
  7. uniq -c: 统计每个唯一实体出现的次数。
  8. sort -nr: 按计数降序排序。
  9. head -10: 只显示出现频率最高的前10个实体。

运行此命令后,你会得到一个列表,显示在关于爱因斯坦的文章中,如“einstein”、“university”、“german”等实体出现的频率最高。这种快速洞察对于数据探索和原型设计非常有用。


总结

本节课中,我们一起学习了如何在AWS CloudShell中利用命令行快速原型化AI应用。

我们首先介绍了如何使用 aws comprehend 进行基础的文本情感分析。接着,为了解决分析外部数据的需求,我们引入了 links 工具来抓取网页文本,并学会了使用 head 命令和变量来处理文本长度限制。最后,我们构建了一个复杂的数据处理管道,将实体提取、文本清洗、排序和频率统计串联起来,实现了快速的数据洞察。

通过这种方法,你无需立即编写Python、C#或R语言代码,就能在CloudShell中快速测试AI API的功能并验证想法,极大地加速了数据科学和AI应用的原型开发过程。

065:Cloud9集成CodeWhisperer教程 🚀

概述

在本节课中,我们将学习如何在AWS Cloud9环境中集成并使用CodeWhisperer进行AI辅助编程。我们将从创建Cloud9环境开始,逐步探索其核心功能,并最终演示如何使用CodeWhisperer编写一个简单的Python脚本来与AWS S3服务交互。

了解AWS Cloud9

要更好地了解Cloud9,可以查阅官方文档。

AWS官方文档展示了如何在AWS Cloud9中处理环境,以及新功能开发的相关信息。

随着新功能的开发,会有越来越多的文档出现。这里有很多资源。

资源包括如何访问AWS工具包。接下来,我们将深入探索这个资源。

创建Cloud9环境

我们将进入Cloud9。首先,确保你选择了正确的区域。

你可以在许多不同的区域创建Cloud9资源。

我将选择北弗吉尼亚区域。然后,我将创建一个新环境。

我们可以将这个环境命名为“cloud9-plus-codewhisperer”。也许可以添加一个描述。

这个环境用于演示。如果需要,我可以从命令行工具查询中获取信息。

通常,为要解决的问题选择合适的大小是个好主意。

如果你只是随便试试,可以保留所有默认设置,并且在这个场景中包括Amazon Linux 2。

不过,我将选择一个稍大的实例。

选择一个拥有16GB内存和4个CPU的实例。

然后,点击创建环境。

环境界面与功能

创建环境后,它会很快启动。你将可以访问这个界面。

加载时,你会看到一个欢迎屏幕,它会告诉你更多关于环境的信息。

这里还有资源,你可以与他人共享以进行结对编程。

右侧这个小齿轮选项卡里有设置。

你还可以通过选择这个图标来调试代码。

另一个需要注意的重要事项是,这里的文件菜单有各种不同的功能来帮助你工作。

功能包括代码折叠或将代码向左或向右移动。

使用终端

不过,首先要开始使用的最重要的事情是熟悉终端。这里才是所有强大功能的起点。

展示你能做什么的最好方法之一是,如果我输入aws s3 ls,然后输入help

你会看到这里有一个帮助菜单,我可以使用它,它会确切地告诉我这个特定工具的功能。

所有AWS工具都已安装在这个环境中。如果我想,我实际上可以做更多事情。

我可以在这里输入aws s3 ls来列出我拥有的所有存储桶,然后使用wc命令来实际计算可用的存储桶数量。

这真是一个仓库,你可以在这里通过命令行操作每一个资源,甚至在这个环境内部来回复制数据。

例如,如果我想上传一些东西。我可以说上传本地文件。

从我的文件系统中获取一些东西,比如我要做的任何事情。

注意我上传了一个截图,或者可以是一段代码或我需要与S3同步的其他东西,然后我可以将其作为一个复制命令。

如果我输入aws s3,然后执行cp命令,就可以将其复制到我关心的存储桶中。

AWS工具包与集成

除了终端之外,还需要指出的是,你还可以在这个选项卡中访问文件系统、源代码控制和AWS工具包。

现在,AWS工具包可能是这里最有用的组件。

因为如果你查看这个区域,你可以访问许多具有深度集成的不同服务。

这意味着我可以在这里调用这些服务之一。如果我想,我甚至可以部署一个服务。我可以说,在这里创建一个新服务并部署它。

我可以查看我的容器注册表、物联网、Lambda,并且有能力与所有这些不同的资源交互。

例如,如果我查看其中一个,我可以说在AWS上调用,并且我可以实际向那个Lambda发送一个有效载荷。

或者我甚至可以下载它并在本地使用。

Cloud9内置了很多很棒的右键点击式集成。

另一个很酷的事情是,它加载了云开发工具包,并且还具有代码补全功能。

在这个案例中,这被称为CodeWhisperer,所以它是AI结对编程。

设置Python虚拟环境

尝试使用它的一个好方法是先设置一个虚拟环境。

我们来设置一下。我们将输入python3 -m venv v

然后我将激活这个虚拟环境。接着我将执行pip install来安装Boto3。

我还会安装一个名为Python Black的格式化工具。

我发现格式化工具在与AI结对程序员一起工作时非常有帮助。

所以我们将输入pip install black。这将允许我格式化代码。

使用CodeWhisperer编写代码

设置好之后,我可以回到文件树视图,实际上可以创建一个名为s3_demo.py的文件来尝试这里的代码结对编程工具。

如果我右键点击这里,我们可以打开它。然后从这里开始,我实际上可以为我想让这个工具做什么设定场景。

这种新工作方式的真正强大之处在于,你可以在这里设置一个提示,说明你的意图是什么。

所以我们将输入“创建一个列出AWS S3存储桶的函数”。开始了。

现在它对我的意图有了更多了解。所以我将让它提示我并告诉我更多要做的事情。

在这个案例中,我们将输入import boto3。我看到了。

然后我们实际上可以说def list_buckets():。所以它足够聪明,知道我将尝试列出一个存储桶。

如果我继续下去,它会给我所有我需要的信息。事实上,它很不错,能够给我一些好的建议。

你必须确保自己保持警惕,并确保遵循语言的最佳实践。

事实上,这就是我喜欢使用格式化工具的原因。所以这个Python格式化工具,我只需这样做,稍微清理一下,以确保我拥有所需的一切。

现在我只需要告诉它帮我构建最后一部分,这正是我想做的,注意这里它会给我一个建议。

我需要创建一个S3实例,然后将其传递给这个list_buckets函数,然后我将运行这段代码。

调试与完善代码

现在我有了这个,我不需要那个特定的部分了。我可以再次执行格式化命令,如果我想确保它格式正确的话。

然后让我们运行它,我们看到python s3_demo.py。它说这里没有服务属性,这意味着我需要稍微清理一下。

为了做到这一点,我认为列出这段代码的更好方法是稍微调整一下。

我们可以在这里继续,并输入s3 = boto3.resource('s3')

在这个特定场景中,我现在可以去掉这个。然后我们实际上可以说这个打印信息。

我们看到S3服务资源没有访问权限,所以我们需要将其更改为客户端。现在我们应该能让它工作。

好了。所以当你使用代码AI结对程序员时,事情并不完美,就像普通人一样。

但如果你能利用它的长处,同时使用外部工具,你可以得到一些好的反馈。

总结

本节课中,我们一起学习了如何在AWS Cloud9环境中集成并使用CodeWhisperer。Cloud9环境和CodeWhisperer环境是使用AWS工具包时的绝佳工具,能够将AWS工具包与CodeWhisperer结合使用,是一个非常好的集成体验。它目前处于预览阶段,会变得越来越好。

它将构建出深度集成并针对你的AWS工作流程定制的解决方案。

066:GCP App Engine Rust部署演示 🚀

在本教程中,我们将学习如何将一个用Rust编写的微服务部署到Google Cloud Platform的App Engine上。我们将从创建项目开始,逐步配置并最终完成部署。


概述

我们将创建一个简单的Rust Web服务,并将其部署到GCP App Engine的灵活环境中。整个过程包括:创建项目结构、编写配置文件、构建Docker镜像,以及使用GCP命令行工具进行部署。


创建项目目录与配置文件

首先,我们需要创建一个项目目录,并进入该目录。

mkdir web_docker
cd web_docker

接下来,创建App Engine的配置文件 app.yaml。这个文件用于告知App Engine我们的应用配置。

runtime: custom
env: flex

初始化Rust项目

现在,我们需要初始化一个Rust项目。使用Cargo工具来创建项目文件。

cargo init --name web_docker

此命令会生成 Cargo.toml 文件。接下来,我们需要添加项目依赖。以下是 Cargo.toml 文件中需要包含的依赖项示例:

[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }
rand = "0.8"

创建Dockerfile

为了在App Engine的灵活环境中运行,我们需要一个Dockerfile来定义容器。创建一个 Dockerfile 文件。

FROM rust:1.68 as builder
WORKDIR /usr/src/app
COPY . .
RUN cargo build --release

FROM debian:buster-slim
COPY --from=builder /usr/src/app/target/release/web_docker /usr/local/bin/web_docker
CMD ["web_docker"]

这个Dockerfile使用多阶段构建,首先在Rust环境中编译应用,然后将编译好的二进制文件复制到一个轻量级的Debian镜像中。

编写应用代码

现在,我们来编写Rust应用的源代码。首先,创建 src/lib.rs 文件,用于定义一些辅助函数。

use rand::Rng;

pub fn generate_random_fruit() -> String {
    let fruits = vec!["Apple", "Banana", "Cherry", "Date", "Elderberry"];
    let mut rng = rand::thread_rng();
    fruits[rng.gen_range(0..fruits.len())].to_string()
}

接着,创建 src/main.rs 文件,用于定义Web服务的路由。

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use web_docker::generate_random_fruit;

async fn health_check() -> impl Responder {
    HttpResponse::Ok().body("OK")
}

async fn version() -> impl Responder {
    HttpResponse::Ok().body("v1.0.0")
}

async fn random_fruit() -> impl Responder {
    let fruit = generate_random_fruit();
    HttpResponse::Ok().body(format!("Welcome random fruit: {}", fruit))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/health", web::get().to(health_check))
            .route("/version", web::get().to(version))
            .route("/", web::get().to(random_fruit))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

添加Makefile(可选)

为了简化构建和部署流程,可以添加一个 Makefile。这个文件可以帮助我们记录和管理各种操作命令。

.PHONY: run build deploy clean

run:
	cargo run

build:
	cargo build --release

deploy:
	gcloud app deploy

clean:
	rm -rf target

本地测试应用

在部署之前,我们需要在本地测试应用是否能够正常运行。使用以下命令启动应用:

cargo run

应用启动后,可以通过浏览器访问 http://localhost:8080 来验证服务是否正常工作。如果看到“Welcome random fruit: [水果名]”的响应,说明应用运行成功。

部署到GCP App Engine

在部署之前,建议删除 target 目录,以避免将不必要的构建文件上传。

rm -rf target

接着,设置GCP构建超时时间,因为Rust编译可能需要较长时间。

gcloud config set app/cloud_build_timeout 1600

最后,使用以下命令将应用部署到App Engine:

gcloud app deploy

部署过程可能需要一些时间。一旦完成,应用就会在GCP上运行。

探索其他语言部署

App Engine支持多种编程语言。例如,如果你想使用Python,可以选择官方的Python 3.7运行时。你还可以通过添加 cloudbuild.yaml 文件来实现自动部署。App Engine提供了一个非常灵活的环境,可以轻松地在Rust、Python、Go等语言之间切换。


总结

在本教程中,我们学习了如何将一个Rust微服务部署到GCP App Engine。我们从创建项目目录和配置文件开始,然后初始化Rust项目并编写应用代码。接着,我们创建了Dockerfile来定义容器,并在本地测试了应用。最后,我们使用GCP命令行工具将应用部署到生产环境。通过这个过程,你可以看到GCP App Engine为不同语言提供了灵活的部署选项。

067:容器化Rust Actix微服务部署到AWS 🚀

概述

在本教程中,我们将学习如何将一个使用Actix-web框架编写的Rust微服务进行容器化,并最终部署到AWS App Runner服务上。我们将涵盖项目结构、Docker镜像构建以及AWS部署的完整流程。

项目结构概览

首先,我们来看一个典型的容器化Actix微服务项目结构。这个结构与我参与的许多项目类似。

以下是项目的主要文件:

  • Makefile
  • Dockerfile
  • Cargo.toml
  • lib.rs
  • main.rs

接下来,让我们深入分析每个部分。

深入代码分析

上一节我们概述了项目结构,本节中我们来看看具体的代码实现。

Dockerfile配置

Dockerfile 使用Rust作为构建器环境。这种方法的好处是能够利用所有必要的开发资源进行构建,然后将最终产物推入一个新的、更精简的容器中。这是一种构建非常小巧容器镜像的优雅方式。

构建完成后,新容器会暴露8080端口,并设置一个名为 web_docker 的入口点。

Dockerfile 关键部分示例:

FROM rust:latest as builder
# ... 构建步骤 ...
FROM debian:buster-slim
# ... 复制二进制文件 ...
EXPOSE 8080
ENTRYPOINT [“./web_docker”]

依赖与库文件

Cargo.toml 文件包含了项目依赖,例如 randactix-web

lib.rs 文件中的逻辑相对简单。它定义了一个公开函数,用于随机返回一种水果。因为是公开函数,所以可以在 main.rs 中被调用。

lib.rs 示例:

pub fn random_fruit() -> String {
    let fruits = vec![“apple”, “banana”, “cherry”, “date”, “elderberry”];
    // ... 随机选择并返回水果 ...
}

主程序入口

main.rs 文件的结构类似于Flask或FastAPI应用。它导入依赖项,调用库函数,并设置各个API端点。

以下是定义的端点:

  • /:根路径
  • /fruit:获取随机水果
  • /health:健康检查
  • /version:版本信息

main 函数中,我们注册了所有这些服务。整个过程非常直观。

main.rs 路由设置示例:

#[get(“/fruit”)]
async fn get_fruit() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({“fruit”: random_fruit()}))
}

本地构建与运行

了解了代码结构后,我们来看看如何在本地构建和运行这个服务。

使用项目中的 Makefile 或直接运行Docker命令,可以轻松完成构建。

以下是构建和运行的步骤:

  1. 运行 make format 命令来编译和启动服务。
  2. 编译过程会在标准输出显示信息。
  3. 服务启动后,可以在浏览器中预览。

打开浏览器访问相应地址,可以看到根路径的欢迎信息。访问 /fruit 端点,每次刷新都会返回一个随机的水果名称,例如“apple”、“pineapple”、“grapes”等。同时,控制台也会打印出这些信息。

这展示了Rust和Actix-web的高效与简洁。

在Cloud9环境中构建

为了演示云上构建,我们将在AWS Cloud9环境中重复这一过程。

首先,需要克隆项目仓库。我们使用HTTPS方式克隆,因为这里只需要构建容器,无需推送代码。

环境准备步骤如下:

  1. 设置相关环境变量。
  2. 配置Cargo环境。
  3. 进入项目目录 web_docker

web_docker 目录中,我们可以参考 Makefile,或直接使用为AWS Elastic Container Registry (ECR) 调整后的Docker命令进行构建和标记。

执行 docker build -t actix-service . 命令。这个过程会花费一些时间,它展示了在云开发环境中构建Rust容器镜像的流程。

部署到AWS App Runner

镜像构建完成后,就可以进行部署了。Rust生成的容器镜像体积非常小,通常不到100MB,这使得部署变得非常轻量和快速。

部署到AWS App Runner的步骤如下:

  1. 在AWS控制台进入App Runner服务。
  2. 选择“创建服务”,然后选择“容器注册表”。
  3. 选择我们刚刚构建并推送到ECR的镜像(例如 actix-service)。
  4. 选择手动部署(后续可以配置为自动部署,即每次推送新镜像到ECR时自动更新服务)。
  5. 为服务命名(例如 rust-actix-demo)。
  6. 完成配置并创建服务。

App Runner会自动处理负载均衡、扩缩容等运维工作。部署完成后,我们会获得一个可访问的服务URL。

验证部署结果

部署成功后,我们可以验证服务是否正常运行。

通过访问App Runner提供的服务端点:

  • 访问根路径 /,会看到定义的欢迎消息。
  • 访问 /fruit 端点,会返回随机的JSON格式水果数据。
  • 访问 /health 端点,会返回健康检查状态。

这证明我们的Rust Actix微服务已经成功容器化并运行在AWS云上。其高性能和小体积镜像的优势在此得以充分体现。

总结

本节课中我们一起学习了将一个Rust Actix-web微服务容器化并部署到AWS App Runner的完整流程。

我们首先分析了项目结构,然后解读了Dockerfile和Rust源代码。接着,我们在本地和Cloud9环境中完成了镜像构建。最后,我们将镜像推送到AWS ECR,并通过App Runner服务成功部署,验证了所有功能。

这个过程展示了使用Rust构建云原生微服务的巨大优势:极高的性能、极小的资源占用以及简洁的部署体验。Actix-web框架使得编写高效的Web服务变得非常简单。

068:杰克与魔豆数据管道 🧙‍♂️🌱

在本节课中,我们将通过童话故事《杰克与魔豆》的类比,来学习构建数据管道的基本概念和步骤。我们将把故事中的每个关键情节,对应到数据工程中的具体环节,帮助你直观地理解数据从原始状态到产生价值的完整旅程。


故事回顾 📖

从前,在一个小村庄里,住着一个名叫杰克的男孩。杰克的家庭拥有一个农场,但农场已不再高产。他的母亲决定卖掉他们唯一的资产——一头奶牛。在去往当地市场的路上,杰克遇到了一位神秘老人。老人提出用魔法豆子交换奶牛。杰克对这个提议很感兴趣,用奶牛换来了魔法豆子,并跑回了家。他的母亲非常生气,将豆子扔出窗外,并让杰克饿着肚子去睡觉。但令杰克惊讶的是,一夜之间,豆子发芽长成了一棵巨大的、直达云端的豆茎。


类比数据管道 🔄

现在,让我们将这个故事与数据管道进行比较。

上一节我们回顾了杰克的故事,本节中我们来看看如何将这个故事映射到数据工程的各个阶段。

播种豆子 🌱

这类似于数据摄取。就像魔法豆子被播种到土壤中一样,数据摄取是将数据导入、传输、加载和处理,以供后续在存储或数据库中使用。杰克播种豆子的行为就类似于这个过程。

核心概念数据摄取 = 导入(数据源) + 传输(网络) + 加载(存储)

豆茎生长 🌳

这对应着数据处理。随着豆子长成巨大的豆茎,数据也需要经历一个转换过程。在数据处理中,原始数据被清洗、验证、聚合和汇总,以提供有意义且高质量的数据。

以下是数据处理的关键步骤:

  • 清洗:修正错误、处理缺失值。
  • 验证:确保数据符合预定义的规则和格式。
  • 聚合:将多个数据点合并为摘要信息。
  • 汇总:生成统计摘要或报告。

攀爬豆茎 🧗

这象征着数据移动。杰克攀爬豆茎的冒险类似于数据移动。它涉及将数据从一个位置、状态或格式传输到另一个。在管道中,这可能涉及将数据从一个存储平台移动到另一个,或从云端移动到边缘设备。

核心概念数据移动:源位置/格式 -> 目标位置/格式

巨人城堡 🏰

这代表数据存储与分析。云端的巨人城堡里藏有宝藏,这类似于数据本身——你需要存储、管理、分析数据并寻找洞察,就像杰克发现巨人的宝藏一样有用。

杰克归来 🏡

最后一步是杰克的归来,这对应数据可视化与报告。最终,杰克胜利地带着宝藏返回村庄,这象征着数据可视化和报告。因为你能够获得反馈,并将其以可用的形式呈现,从而做出数据驱动的决策。这很像杰克与母亲分享财富并改善他们的生活。


总结 📝

本节课中,我们一起学习了如何通过《杰克与魔豆》的故事来理解数据管道。就像杰克与魔豆的冒险一样,数据管道是数据的一段激动人心的旅程。它获取原始数据,对其进行转换,并将其转化为有价值的洞察,从而丰富企业或组织的决策过程。在一个数据驱动的世界里,他们从此过上了幸福的生活。

069:常见开源工具解析

在本节课中,我们将学习数据工程领域中常用的开源工具,分析它们各自的优缺点,帮助你理解如何根据不同的场景选择合适的工具。

Hadoop/Spark/Hive生态系统

首先,我们来看看Hadoop、Spark和Hive组成的生态系统。这套工具在行业中应用广泛,主要用于处理大规模数据。

以下是该生态系统的主要优点:

  • 可扩展性:能够处理海量数据。
  • 灵活性:可以处理多种数据类型和结构,包括半结构化和非结构化数据。
  • 成本效益:可以使用商用硬件来构建集群。
  • 集成性:支持使用SQL等熟悉的语言来完成数据处理任务。

上一节我们介绍了该生态系统的优点,接下来看看它存在的一些挑战。

以下是该生态系统的主要缺点:

  • 复杂性:系统本身较为复杂,学习和运维有一定难度。
  • 延迟:在处理实时数据流方面存在短板。
  • 资源管理:在分布式环境中精细地管理和协调资源比较困难。
  • 维护成本:系统需要大量的手动维护工作才能在大规模下稳定运行。

Apache Beam

接下来,我们看看Apache Beam。它是一个用于定义批处理和流处理数据管道的统一编程模型。

以下是Apache Beam的主要优点:

  • 统一模型:可以使用同一套API和管道来处理批处理和流处理任务,并能组合不同的工作负载。
  • 可移植性:可以在多种运行时上执行,例如Flink、Google Cloud Dataflow或Spark。
  • 多语言支持:支持使用Go、Python、Java等多种编程语言进行开发。

了解了Beam的优势后,我们再来看看它的一些局限性。

以下是Apache Beam的主要缺点:

  • 复杂性:模型本身有一定学习门槛。
  • 采用率有限:相比其他专用系统,其生态和采用广度稍逊。
  • 性能:对于某些特定用例,其性能可能不如专门优化的批处理或流处理系统。

Apache Airflow

现在,我们转向工作流编排工具Apache Airflow。它用于定义、调度和监控复杂的数据管道。

以下是Apache Airflow的主要优点:

  • 强大的工作流管理:提供了定义、执行和监控复杂数据管道的强大工具。
  • 可扩展性:具有良好的横向扩展能力。
  • 社区与生态:拥有庞大且活跃的社区和丰富的插件生态系统。

当然,Airflow也并非完美无缺,它同样存在一些挑战。

以下是Apache Airflow的主要缺点:

  • 学习曲线陡峭:对于初学者来说,掌握其核心概念和用法需要一定时间。
  • 部署与维护:在分布式环境中部署和维护Airflow可能比较复杂。
  • 用户界面:其原生Web UI在功能和管理体验上可能无法完全满足所有用户的需求。

消息队列:Kafka与RabbitMQ

数据工程中,消息队列是实现实时数据交换的关键组件。我们来看看Kafka和RabbitMQ的共性与特点。

以下是这类高性能消息队列的主要优点:

  • 高吞吐量:能够处理巨大的数据流量。
  • 可扩展性:支持水平扩展以应对增长的数据量。
  • 容错性:具备良好的故障恢复机制,保证数据不丢失。

然而,在使用消息队列时,我们也需要注意一些潜在的问题。

以下是使用这类消息队列时可能遇到的缺点:

  • 消息处理:需要仔细设计以确保消息(如订单)被高效、正确地处理。
  • 顺序与延迟:在分布式场景下,可能遇到消息乱序或处理延迟的问题。
  • 资源管理:同样需要关注集群的资源分配和管理。

Apache Cassandra

最后,我们探讨一下数据库层面的工具,以分布式NoSQL数据库Apache Cassandra为例。

以下是Apache Cassandra的主要优点:

  • 高可扩展性:专为分布式部署设计,易于水平扩展。
  • 数据结构灵活:能够很好地处理半结构化和结构化数据。

Cassandra在写入密集型场景下表现出色,但在其他方面也存在一些限制。

以下是Apache Cassandra的主要缺点:

  • 使用复杂:数据模型和查询方式与传统关系型数据库不同,有一定复杂性。
  • 读性能:对于读多写少的场景,其性能可能不是最优选择。
  • 聚合功能:内置的聚合操作功能相对较弱,复杂分析通常需要在应用层或借助其他工具完成。

总结

本节课中,我们一起学习了数据工程领域几种常见的开源工具,包括Hadoop/Spark生态系统、Apache Beam、Apache Airflow、Kafka/RabbitMQ消息队列以及Apache Cassandra。我们分析了它们各自的优点和缺点。重要的是要认识到,没有一种工具是适用于所有场景的完美解决方案。在实际工作中,需要根据具体的业务需求、数据特点、团队技能和基础设施条件来选择和组合这些工具。理解它们的特性和适用场景,是成为一名优秀数据工程师的关键一步。

070:70_04_04_数据工程管道核心组件 📊

在本节课中,我们将学习数据工程管道的核心组成部分。我们将一个典型的管道分解为三个主要阶段,并探讨贯穿整个管道的几个关键支撑原则。理解这些组件是设计和构建高效、可靠数据系统的基础。

数据工程管道的三大核心阶段 🧩

上一节我们了解了数据工程管道的整体概念,本节中我们来看看构成管道的三个核心阶段。一个标准的数据工程管道通常包含以下三个部分:数据摄取、数据处理和数据存储。

以下是这三个核心阶段的详细说明:

  1. 数据摄取
    这是数据管道中最重要的部分。其核心在于能否成功获取数据并使其开始工作。数据可以来自多个源头,例如数据湖,甚至是实时数据流。此阶段的任务是从数据收集的角度来处理这些数据。

  2. 数据处理
    此阶段涉及将数据转换为更可用的形式。这包括对数据进行清洗、验证或格式化。它确保你能够实际构建聚合、摘要等。我们在这里会看到许多工具,例如 Spark,它可以执行处理和其他 ETL 类型的操作。

  3. 数据存储与输出
    在此阶段,管道会以合适的方式存储处理后的数据,以便进行进一步分析。例如,存储目的地可以是数据仓库,也可以是仪表板等。这是管道的最终端点。

贯穿管道的支撑原则 ⚙️

在了解了管道的三个阶段后,我们还需要关注贯穿整个管道的几个关键支撑原则。这些原则对于任何投入生产环境的数据工程管道都至关重要。

以下是这些支撑原则的详细说明:

  • 可扩展性
    可扩展性非常关键,因为要构建能够在生产环境中运行的系统,你必须能够实现垂直或水平扩展。这意味着你可以向系统添加节点或增加系统的规模。

  • 弹性
    弹性意味着系统即使在出现错误时也能继续运行。例如,如果是一个事件触发了管道,而该事件本身存在问题,系统仍然可以向你发出警报并告知问题所在;或者,系统可以运行在不同的节点上,即使其中一个节点发生故障,部分系统仍能继续运行。

  • 监控与维护
    监控与维护使我们能够观察数据流和系统性能,并在管道出现某种问题时发出警报。

总结 📝

本节课中我们一起学习了数据工程管道的核心组件。整体来看,当你思考数据工程管道时,这些将是关键组成部分:第一步数据摄取,第二步数据处理,第三步数据存储。而弹性、可扩展性和监控这些内在原则不仅始终存在于数据工程管道中,也同样存在于生产软件工程系统中。

071: 使用 Rust 构建 AWS Step Functions 管道 🚀

在本节课中,我们将学习如何使用 Rust 语言来构建 AWS Step Functions 无服务器工作流。我们将创建两个简单的 Lambda 函数,并将它们串联成一个工作流,体验 Step Functions 强大的编排和调试能力。

概述

AWS Step Functions 是一种用于编排无服务器工作流的强大服务。它允许你将多个操作(如 Lambda 函数)链接在一起,将一个函数的输出作为另一个函数的输入。其可视化界面使得创建工作流就像搭积木一样简单直观,并且提供了出色的执行过程追踪和调试功能。我们将使用 Rust 和 cargo-lambda 工具来实现这一切。

创建第一个 Lambda 函数(Rust Marco)

首先,我们需要创建工作流中的第一个 Lambda 函数。我们将使用 cargo-lambda 这个优秀的库来快速搭建项目。

以下是创建和配置 rust-marco 函数的步骤:

  1. 创建项目:使用 cargo lambda new rust-marco 命令创建一个新的 Lambda 项目。
  2. 编写函数逻辑:在 src/main.rs 中,我们定义处理程序。函数接收一个包含 name 字段的 JSON 输入。
  3. 核心逻辑:检查输入的 name 值。如果它是 “Marco”,则返回 {“body”: “polo”};否则,返回 {“body”: “nobody”}
  4. 添加追踪:集成 tracing 库以便在 AWS 控制台中进行调试。
  5. 构建与部署:使用 make releasemake deploy 命令(或直接使用 cargo lambda 命令)来构建和部署函数。

让我们看看核心的处理函数代码:

async fn function_handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
    let (event, _context) = event.into_parts();
    let name = event["name"].as_str().unwrap_or("");

    // 核心逻辑:判断并生成响应体
    let body = if name == "Marco" { "polo" } else { "nobody" };

    tracing::info!("Processed name: {}, returning body: {}", name, body);

    let resp = json!({ "body": body });
    Ok(resp)
}

创建第二个 Lambda 函数(Rust Polo)

接下来,我们创建工作流中的第二个 Lambda 函数 rust-polo。它将接收第一个函数的输出作为输入。

以下是 rust-polo 函数的关键点:

  1. 项目创建:同样使用 cargo lambda new rust-polo 创建。
  2. 函数逻辑:这个函数期望一个包含 body 字段的 JSON 输入(来自 rust-marco 的输出)。
  3. 核心逻辑:检查 body 字段的值。如果它是 “polo”,则返回 {“result”: “you win”};否则,返回 {“result”: “you lose”}

其处理函数的核心代码如下:

async fn function_handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
    let (event, _context) = event.into_parts();
    let body = event["body"].as_str().unwrap_or("");

    // 核心逻辑:根据收到的 body 判断输赢
    let result = if body.contains("polo") { "you win" } else { "you lose" };

    tracing::info!("Received body: {}, result: {}", body, result);

    let resp = json!({ "result": result });
    Ok(resp)
}

在 AWS 控制台中组装 Step Functions 工作流

现在,我们已经有了两个可以独立运行的 Lambda 函数。接下来,我们将在 AWS 管理控制台中,像搭积木一样将它们组合成一个工作流。

上一节我们创建了两个 Rust Lambda 函数,本节中我们来看看如何在 AWS 控制台中将它们连接起来。

以下是创建 Step Functions 状态机的步骤:

  1. 在 AWS Step Functions 控制台点击“创建状态机”。
  2. 在可视化设计器中,从左侧拖拽一个 Lambda 调用 任务到画布中。
  3. 配置该任务,选择我们部署好的 rust-marco 函数。
  4. 从第一个任务右侧拉出一条线,再拖拽第二个 Lambda 调用 任务。
  5. 配置第二个任务,选择我们部署好的 rust-polo 函数。
  6. 保存并命名状态机(例如 rust-marco-polo-chain)。

至此,一个简单的工作流就创建完成了。第一个任务的输出会自动成为第二个任务的输入。

执行与调试工作流

工作流创建好后,我们可以立即执行它并观察其运行过程。

以下是执行和查看工作流详情的方法:

  1. 在状态机页面,点击“开始执行”。
  2. 在输入框中,提供初始 JSON 数据,例如:{“name”: “Marco”}
  3. 点击“开始执行”后,你可以实时看到执行流程。
  4. 点击每个执行步骤,可以展开查看该步骤的 输入输出 以及任何 错误信息

例如,当输入为 {“name”: “Marco”} 时,执行路径将是:

  • rust-marco 接收 {“name”: “Marco”},输出 {“body”: “polo”}
  • rust-polo 接收 {“body”: “polo”},输出 {“result”: “you win”}

如果输入是 {“name”: “Other”},路径将是:

  • rust-marco 输出 {“body”: “nobody”}
  • rust-polo 输出 {“result”: “you lose”}

这种可视化的执行历史对于理解数据流和调试复杂工作流至关重要。

总结

本节课中我们一起学习了如何使用 Rust 构建 AWS Step Functions 无服务器工作流。我们首先使用 cargo-lambda 工具创建并部署了两个简单的 Lambda 函数。然后,我们在 AWS 控制台中通过拖拽的方式,将这两个函数连接成了一个可执行的工作流。最后,我们执行了该工作流,并利用 Step Functions 提供的可视化工具观察了数据的传递过程和调试信息。这种方法使得构建可维护、可调试的分布式应用变得异常简单和高效。

072:使用Rust构建异步AWS Lambda S3容量计算器 🚀

在本节课中,我们将学习如何使用Rust和AWS SDK构建一个高性能的、异步的AWS Lambda函数。这个函数的核心功能是异步地与Amazon S3服务通信,遍历所有存储桶中的对象,并计算它们的总存储容量。我们将看到如何利用Rust的异步编程能力来高效处理可能包含成千上万个文件的任务。

架构概述 🏗️

这是一个系统编程类型的AWS Rust Lambda函数。它的架构设计使其能够异步地与S3通信。S3中可能包含数千、数万甚至数十万个文件。该Lambda可以异步地遍历所有存储桶,检查每个存储桶中的每个对象,然后执行特定操作。

在这个特定的监控Lambda中,它会计算每个对象的大小并汇总成总量。根据我的存储桶配置,运行大约需要三秒钟。之后,可以将结果输出到仪表板、命令行工具,或集成到某种监控和/或计费系统中。这一切都得益于异步Rust SDK的强大功能。

代码解析:异步SDK与S3交互 💻

上一节我们介绍了Lambda的整体架构,本节中我们来看看具体的代码实现,特别是如何使用AWS SDK进行异步操作。

首先,我们使用AWS SDK。以下是一个示例,展示了如何异步列出AWS环境中的所有表。你需要使用SDK的特定组件和Tokio异步运行时库。

// 示例:使用AWS SDK异步列出资源
use aws_sdk_s3 as s3;
use tokio; // 异步运行时

现在你已经了解了基本工作原理,让我们继续查看实际的代码。

核心功能实现 🔧

如果进入Github代码空间,可以看到一个名为async_aws_lambda的项目。以下是部分关键代码。

首先,我声明使用AWS SDK S3并创建一个S3客户端。

use aws_sdk_s3 as s3;
let s3_client = s3::Client::new(&aws_config);

然后,我定义了一个异步函数来列出所有存储桶。

pub async fn list_all_buckets(s3_client: &s3::Client) -> Result<Vec<String>, Error> {
    // 异步列出存储桶的逻辑
}

async关键字赋予我们在网络I/O中进行异步操作的能力。最后,我通过汇总存储桶中每个对象的大小来计算存储桶的总容量。

pub async fn calculate_bucket_size(s3_client: &s3::Client, bucket_name: &str) -> Result<u64, Error> {
    // 异步计算存储桶总大小的逻辑
}

Lambda函数处理程序与集成 🧩

最后,我使用list_buckets函数获取账户中所有存储桶的列表,然后遍历它们以创建存储桶容量列表。

main.rs文件中,代码使用lambda_runtime来创建一个易于使用的辅助方法。函数处理程序能够运行我们之前设置的代码,并返回响应。

在这个特定的main方法中,它会调用上述的其他方法。以下是函数处理程序的简化结构:

async fn function_handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
    // 1. 初始化S3客户端
    // 2. 调用 list_all_buckets
    // 3. 为每个存储桶调用 calculate_bucket_size
    // 4. 汇总并返回结果
}

项目依赖与部署 📦

现在,让我们看看Cargo.toml文件。在这个依赖文件中,可以看到以下关键库:

以下是项目的主要依赖项:

  • serde:用于序列化和反序列化数据。
  • tokio:Rust的异步运行时库。
  • aws-sdk-s3:AWS S3服务的官方Rust SDK。
  • lambda_runtime:用于构建AWS Lambda函数的Rust运行时。
  • humansize:一个用于将字节数转换为人类可读格式(如KB, MB)的库。

至于Makefile,我们可以看到为了调用这个Lambda,可以直接运行make命令。例如,我们可以执行make invoke来本地测试函数。

运行与测试 🧪

在实际操作中,我们需要确保环境已配置好cargo-lambda等工具。通过运行命令,Lambda函数大约会在三秒内执行完毕,并同步计算所有存储桶的总存储量。例如,在我的配置中,它计算出了总共114GB的存储空间。

我们也可以直接在AWS Lambda控制台中测试它。转到Lambda控制台,找到对应的函数,点击“测试”。可以使用一个简单的有效载荷(例如{"name": "run"},甚至是一个空对象{})来触发函数,并观察其运行。

应用模式与优势总结 ✨

这是一个使用Rust构建高性能系统监控Lambda的绝佳模式。这个例子计算的是所有存储桶的大小,但你可以很容易地联想到如何构建与其他组件通信的工具,例如:

  • Amazon EFS(弹性文件系统)
  • Amazon EMR(弹性MapReduce)
  • 所有EBS(弹性块存储)卷的监控
  • 任何需要高效、低延迟运行的系统监控工具

在本例中,函数仅用了大约2.5秒就完成了执行,并且可以在最低配置层级运行,这意味着内存使用率非常低。

总而言之,使用Rust构建高性能系统工具非常简单。在本例中,我们成功将其集成到Lambda中,并可以通过多种方式调用它:

  • 从命令行工具调用
  • 在终端中直接调用
  • 由事件触发(例如,每日一次的定时器事件)
  • 将结果集成到仪表板中

本节课中我们一起学习了如何利用Rust的异步特性和AWS SDK构建一个高效的S3容量计算器Lambda。我们涵盖了从架构设计、代码实现、依赖管理到测试部署的完整流程,展示了Rust在云原生系统编程中的强大潜力。

073:Distroless技术解析 🐳

在本节课中,我们将要学习一种名为Distroless的容器技术。我们将通过一个生动的比喻来理解它的核心概念、优势以及它如何为现代应用部署提供安全、高效的环境。

概述

Distroless是一种构建容器镜像的方法,其核心目标是创建精简、安全且一致的运行时环境。它移除了传统基础镜像中非必要的组件,只保留运行应用程序所必需的最少元素。

Distroless是什么?

Distroless可以被看作是一个精简、安全且一致的容器

为了更好地理解,我们可以将其比作一顿有机的米饭豆子餐。米饭和豆子结合在一起,构成了完整的蛋白质来源,同时成分简单,富含纤维和蛋白质,非常健康。

接下来,我们将这个比喻分解,逐一剖析Distroless容器的关键特性。

核心特性解析

上一节我们介绍了Distroless的基本概念,本节中我们来看看它的具体特性。

1. 最小化运行时足迹

Distroless容器就像一顿米饭豆子餐,只包含最基础的元素。制作一顿营养餐不需要花哨的食材,同样,Distroless容器也只包含运行应用程序所必需的组件,从而保持其精简和高效。

其核心思想可以用以下伪代码表示:

# 传统镜像:包含包管理器、Shell等
FROM debian:latest
RUN apt-get update && apt-get install -y myapp
CMD ["myapp"]

# Distroless镜像:只包含应用及其运行时
FROM gcr.io/distroless/base
COPY --from=builder /app/myapp /app/
CMD ["/app/myapp"]

2. 聚焦安全性

一顿简单的米饭豆子餐降低了食物过敏的风险。类似地,Distroless容器通过剔除可能被利用的非必要软件,减少了潜在的安全漏洞。攻击面因此变得更小。

3. 简单性与可维护性

烹饪和清理一顿米饭豆子餐非常简单直接。同样,Distroless容器因为组件更少而易于管理,出错的概率更低,任何问题都更容易追踪,从而确保了运行的平稳性。

4. 可复现性

很容易复现一顿米饭豆子餐。Distroless容器因其简单性而具备高度的可复现性。这种一致性确保了所有开发者都在相同的运行时环境下工作,最大限度地减少了差异和混淆。

5. 多语言支持

就像米饭豆子餐能为不同饮食偏好的人提供完整的蛋白质来源一样,Distroless容器可以托管用各种编程语言编写的应用程序,为开发者提供了一个多功能的平台。

总结

本节课中我们一起学习了Distroless容器技术。总而言之,Google的Distroless容器很像一顿简单却完整的米饭豆子餐,提供所需的一切,别无他物。它们为部署应用程序提供了一个安全、可维护、可复现且包容的环境,是您软件“饮食”中营养丰富且经济实惠的选择。😊

074:GCP部署Rust微服务演示 🚀

在本节课中,我们将学习如何在Google Cloud Platform上部署一个容器化的Rust微服务。我们将从设置开发环境开始,逐步完成代码编写、本地测试,最终将服务部署到GCP Cloud Run上。

概述与架构

我们有一个已经构建好的Rust微服务代码仓库。部署后,它将通过Cloud Run界面提供服务。

该应用的架构基于在Google Cloud Platform上实现容器化Rust微服务的持续交付

代码存放在GitHub。任何代码变更都会触发GCP Cloud Build环境,进而自动部署到作为服务的容器化Cloud Run应用中。

Rust作为一个微服务框架,能够实现低内存、高性能的计算。我们可以随着代码更新,持续将其推送到生产环境。

这就是我们将要构建的内容。

开始设置

首先,我们需要打开Cloud Shell并选择打开编辑器。

这个编辑器的优势在于它有一些定制化功能,可以让你轻松构建Cloud Run微服务并在本地进行测试。

如果我们选择这里的Cloud Code图标,会注意到这里有一个Cloud Run图标。我们需要授权一次,授权后就可以开始构建新应用了。

接下来,我们选择“创建新的Cloud Run应用”。

然后,我可以选择一个模板。模板选择并不重要,因为我们可以修改代码。我暂时选择Go模板,因为它最接近我要做的事情。接着,我选择“创建新应用”。

我也可以根据需要更改颜色主题,如果你习惯使用某种颜色,这是一个好习惯。我选择深色主题,然后打开新终端。

准备项目代码

现在环境已经设置好,我只需要清理一下,以便修改它来运行Rust代码。我可以选择删除不需要的部分。

也就是这些目录。我们也可以删除Go代码,因为我不打算使用Go编程。我们可以在这里清理所有内容。

看起来我们已经准备就绪了。

接下来,我可以转到我的应用,直接剪切粘贴代码进来。

首先,我们有一个Dockerfile。我将复制那段代码并粘贴到这个代码仓库中。

我们将把Go的Dockerfile改为Rust的Dockerfile。注意,我们这里有一个非常简单的构建过程,能够创建二进制文件并将其推送到一个精简容器中,并从8080端口暴露服务。

初始化Rust项目

接下来,我将获取源代码。我们可以进入源代码目录,处理lib.rsmain.rs

对于一个Rust项目,一个简单的方法是直接运行cargo

首先,让我们看看它是否已安装。它没有安装,但很容易修复。

我们只需要使用rustup。通过rustup,我们只需运行一行命令即可安装。

rustup是一种在基于终端的环境中安装Rust的方式。可以看到,它能非常高效地快速完成安装。

一旦Rust生态系统安装完毕,我只需要输入cargo new并初始化一个项目。我们来执行这个操作。

让我们在这里配置环境。我们只需要输入cargo init --name web-docker。这将初始化一个本地项目结构。

现在,我还要在src目录下创建一个lib.rs文件。

添加应用代码

现在我们有了这两个文件,我可以回到我的项目,直接获取之前准备好的代码。

先在其他地方测试并确保其正常工作,总是一个好方法。

可以看到,服务器代码非常直接:有一些导入、一些Web处理程序。在这个例子中,它返回健康检查、版本信息和一个随机水果。

如果我想查看返回随机水果的lib.rs目录代码,我们也可以获取它。

这是一个非常简单直接的REST应用,我们现在可以使用它。

Rust项目还需要做的另一件事是获取依赖项,它们在Cargo.toml文件中。

我们只需获取这些内容,然后粘贴到Cargo.toml文件中。这样就完成了。

本地测试与部署

完成上述步骤后,你只需要输入cargo run,它就会安装所有依赖并运行Web微服务。

在本地测试完毕后,你需要做的另一件事就是实际进行部署。

部署过程非常简单。我们选择这里,可以向下找到Cloud Run,然后选择这个“上传到Cloud Run”的图标。

选择后,它会开始分析工作区、构建容器应用,然后将其推送到生产环境。

完成后,你将能够看到结果。你按照向导完成所有要求的步骤后,下一步就是部署一个类似的服务。

然后,你实际上可以对这个服务执行curl命令。让我们来看看:如果我们进入Cloud Run并查看那个服务,注意,它已经启动并运行了。我可以复制这个URL。

如果我打开一个新终端,实际上可以执行一个curl命令,你会看到它正在运行——一个生产环境服务。如果我输入/fruit,它会获取一个随机水果并反复返回。

这是一个非常直接的过程,用于在Google Cloud Platform上实现这种容器化的微服务。

整个过程始于GitHub,你将代码保存在那里。

之后,当你想进行持续交付时,可以连接Cloud Build流程来实现持续交付,它会将应用推送到生产环境。

总结

本节课中,我们一起学习了如何在GCP上部署一个Rust微服务。我们从项目架构讲起,逐步完成了开发环境设置、Rust项目初始化、代码编写、依赖管理、本地测试,最后通过Cloud Run完成了部署。整个过程展示了如何利用GCP的服务实现容器化应用的快速部署与持续交付。

075:Hugging Face Hub平台介绍 🧠

在本节课中,我们将要学习Hugging Face Hub平台的核心组成部分。Hugging Face Hub主要围绕几个关键产品构建,包括账户与令牌管理、模型库、数据集以及Spaces应用部署。掌握这些内容将帮助你有效地利用该平台进行机器学习项目开发。

账户与访问令牌 🔑

首先,你需要登录并创建一个账户。在个人资料页面,除了填写姓名和主页等信息外,最重要的设置是创建一个访问令牌。

访问令牌允许你以编程方式与Hugging Face Hub进行交互。这意味着你可以从GitHub Actions推送构建产物到Hugging Face,在开发环境中读写数据,或者配置Spaces应用。

以下是创建和使用访问令牌的核心步骤:

  1. 在账户设置中生成一个访问令牌。
  2. 将此令牌安全地保存在某个地方。

模型库 🤖

接下来需要了解的是模型库。模型是当前Hugging Face平台的核心组成部分,目前拥有超过80,000个模型,并且数量还在持续增长。

模型按多种任务进行分类,例如图像分类、翻译等。你还可以浏览更高级别的类别,如计算机视觉或自然语言处理。

在模型库中,你可以按下载量等指标进行排序,这有助于你在处理特定类别任务时选择合适的模型。例如,近期热门的OpenAI Whisper模型就可以通过Hugging Face集成到项目中进行语音转录。

数据集 📊

Hugging Face平台另一个重要的组成部分是数据集。平台上有大量数据集,这些数据集对于微调预训练模型非常有用,可以使模型针对你正在解决的特定问题变得更加精确。

数据集涵盖语言建模、多类别分类等细粒度任务。每个数据集页面都提供了数据结构信息、数据预览功能,甚至可以直接复制API调用代码在终端中进行查询。此外,你还可以在Hugging Face环境中直接使用这些数据进行训练。

总的来说,数据集是Hugging Face平台非常有用的一个方面,并且你也可以上传自己的数据。

Spaces应用部署 🚀

最后一个关键组件是Spaces。Spaces是用于构建和分享机器学习应用程序的一种简便方式。

创建一个新的Space非常简单:只需提供Space名称并选择想要使用的技术栈,例如Streamlit、Gradio或静态HTML。你还可以为应用选择特定的开源许可证。

浏览他人创建的Spaces是熟悉各种技术的绝佳途径。例如,你可以查看“Stable Diffusion Demo”这个Space,了解其应用文件构成以及它如何使用Gradio框架。

总结 📝

本节课我们一起学习了Hugging Face Hub平台的四个核心部分。首先,我们介绍了如何创建账户和访问令牌以实现编程交互。接着,我们探索了庞大的模型库及其分类方式。然后,我们了解了数据集如何用于模型微调以及如何查询和使用它们。最后,我们介绍了Spaces作为部署和分享机器学习应用的平台。

要编程化地使用这个平台,你需要创建一个个人资料,在设置中配置API密钥,然后就可以充分利用模型、数据集和Spaces的强大功能了。

076:Rust GPU与Hugging Face翻译器实战 🚀

在本节课中,我们将学习如何在Rust中使用PyTorch进行模型训练,并利用GPU加速推理过程。我们还将探讨Firecracker这一由Rust编写的、支撑AWS Lambda的服务器技术,以证明Rust在生产环境中的强大能力。


关于Rust实用性的探讨

我经常听到关于使用Rust的一种犹豫:它是一种较新的语言(尽管自2010年就已存在),人们担心它能否用于真实世界。

首先,我将引导你了解如何使用PyTorch训练模型,并通过Rust的PyTorch绑定在GPU上调用它们。其次,我将简要介绍Firecracker。Firecracker是什么?它是一项由AWS编写的、为AWS Lambda提供支持的服务器技术。可以想象,世界上最大规模的服务器端执行环境就在AWS上,他们是云计算领域的巨头。事实上,他们最受欢迎的服务之一AWS Lambda就是用Rust编写的。我认为这足以打消“Rust不能用于生产环境”的想法,这完全是无稽之谈。我还将向你展示,如果你要构建PyTorch应用,为何应重点考虑使用能够像AWS Lambda一样扩展到数百万并发请求的技术,并采用Rust这样的技术。


深入Firecracker

现在,让我们深入了解Firecracker。这里我打开了一篇AWS博客文章,其中最初在2018年宣布了Firecracker——一种用于无服务器计算的轻量级虚拟化技术。

你可以看到它带来了一系列巨大的好处:可以在125毫秒内启动一个微虚拟机,它是一个经过实战检验、低开销的开源项目。当AWS用它来支持Lambda这样的服务时,你就知道它的重要性了。

如果查看这里的源代码,我们实际上可以查看Firecracker微虚拟机内正在积极开发的所有生产代码。

查看实际的架构页面,你可以看到这是一个用Rust构建的非常复杂的服务。这个Firecracker可以在一个实例上扩展到数千个多租户微虚拟机。因此,它是构建微虚拟机的一种极其高效的方式。


Rust与PyTorch GPU绑定实战

这项技术显然非常适合推理任务。事实上,因为我们知道PyTorch有Rust绑定,我们可以直接使用这项强大的基于Rust的技术。

让我们进入Rust PyTorch GPU模板。我将再次进入我的代码空间。

接下来,我将向你展示使用PyTorch与GPU是多么简单。浏览这里,我查看了一些不同的项目。我有一个“portable-pytorch”项目,还有一个“pytorch-mnist”项目。

如果查看README,我们可以看看我一直在尝试的一些不同项目。让我们开始吧。我们浏览到这里,打开README。

在最开始的部分,有一个对这些绑定的探索。如果我想进行压力测试,这是一个有趣的例子。

首先,我会cd进入这个目录。如果我直接输入cargo run --gpu,我也可以在上面打开另一个shell。

然后,我可以执行nvidia-smi -l 1,我们将看到这个GPU实际上很快就会达到饱和状态。这确实是观察你能做什么的一种有趣方式。看,我们通过运行这段Rust代码使GPU饱和了。

如果我想查看代码本身,它并不复杂。我们进入pytorch-gpu目录,查看src文件夹里的源代码。这是一个非常小的函数,只有几行代码。你实际上可以指定GPU目标并进行操作。这段代码主要是复制了原作者的主要代码。

如果我想查看mnist-cli-gpu,我们也可以看看。我cd进入那个目录。

我们可以进入并运行mnist-cli-gpu。这段代码是做什么的?这里有一个库,包含我的原始代码,然后我可以将其拉入一个命令并执行。所以,这里只需要很少量的代码就能让一切运行起来。


训练模型示例

我认为另一个有趣的例子是实际训练模型的能力。让我们进入pytorch-mnist目录。

我向上导航,进入pytorch-mnist。同样,我们可以查看源代码,看看它并不复杂。

我们进入pytorch-mnist并查看这里的源代码。这里有多个文件,但重要的是这里的卷积神经网络文件。

你可以看到,这里导入了那些绑定,设置了结构来进行几次不同的迭代,最后这部分是我实际构建神经网络的地方,然后我运行我的代码。所以,它和查看Python代码没有太大不同。

如果我们想运行并复现它,只需要运行这个命令。如果我们运行并训练它,它将执行那个卷积神经网络训练任务。

然后,我们应该能够看到GPU在此过程中被使用。看,它被击中了一点。它应该很快就会活跃起来。好了,开始了。

我们看到这个东西迅速使GPU饱和,让它全速运转,我们能够训练这个模型。所以,对于想要使用高性能系统编程语言来训练深度学习模型的人来说,这确实是一个相当不错的工具。


使用Rust PyTorch绑定的优势

我使用Rust的PyTorch绑定的经验是,它们非常出色。你可以看到,它还有一个很好的副作用:打包比Python简单得多,因为它使用Cargo。这使得在需要安装任何东西时,操作变得非常容易。你只需使用Cargo生态系统并将工具打包在一起。

最后,使用PyTorch的一个重大且有趣的收获是,你可以创建可复现的模型。一个很好的例子是,在这个程序运行时,我可以看看一个叫“sotrack”的东西。这确实是一个新兴趋势,我认为人们正在认真考虑如何使用像ONNX这样的便携格式来打包模型,组装工具,然后将这些工具提供给其他人。我认为这确实是MLOps的一个版本:将你的模型打包并实际分发给其他人。


总结与鼓励

我强烈鼓励大家看看这个。越多的人进行演示,越多的人用Rust这样的系统编程语言查看这些示例,我认为MLOps社区将深深受益于这类通用工具。

好了,下次见。

077:Rust与PyTorch高性能方案 🚀

在本节课中,我们将学习如何在Rust中使用PyTorch进行高性能模型训练与推理,并探讨Rust在生产环境中的实际应用案例,以消除关于Rust是否适用于真实世界的疑虑。

概述

Rust自2010年诞生以来,已发展为一门成熟的系统编程语言。然而,许多人仍对其在生产环境中的应用持观望态度。本节将通过两个核心示例来证明Rust的实用性:首先,展示如何使用Rust的PyTorch绑定进行GPU加速的模型训练与推理;其次,介绍由AWS开发并用于支撑其Lambda服务的Firecracker技术,它正是用Rust编写的,这有力地证明了Rust处理大规模、高并发生产负载的能力。

Firecracker:Rust在生产中的典范

上一节我们提到了对Rust生产适用性的疑虑,本节中我们来看看一个重量级的实际案例——Firecracker。

Firecracker是一项由AWS开发的基于服务器的技术,专门用于驱动AWS Lambda服务。作为云计算领域的巨头,AWS选择使用Rust来构建其最受欢迎的服务之一的核心组件,这本身就极具说服力。Firecracker是一个轻量级虚拟化方案,专为无服务器计算设计。

以下是Firecracker的主要优势:

  • 它能在125毫秒内启动一个微虚拟机。
  • 它是一个经过实战检验、低开销的开源项目。
  • 它能在单个实例上扩展到数千个多租户微虚拟机,效率极高。

这项技术非常适合用于模型推理等场景。鉴于PyTorch提供了Rust绑定,我们可以将这种强大的Rust技术与深度学习结合起来。

在Rust中使用PyTorch与GPU

现在,让我们进入实践环节,看看如何在Rust中轻松使用PyTorch并调用GPU。

示例一:GPU压力测试

首先,我们来看一个简单的GPU压力测试示例。这个例子可以直观地展示Rust如何利用GPU进行计算。

以下是运行该测试的核心代码框架:

// 示例代码:初始化Tensor并移至GPU
let device = Device::cuda_if_available();
let tensor = Tensor::randn(&[1000, 1000], (Kind::Float, device));
// ... 后续进行密集计算以饱和GPU

执行这段代码后,通过 nvidia-smi 命令可以观察到GPU利用率迅速达到饱和状态。这证明了通过Rust PyTorch绑定操作GPU的便捷性与高效性。

示例二:训练MNIST模型

接下来,我们看一个更实际的例子:使用卷积神经网络训练MNIST手写数字识别模型。

以下是构建神经网络的关键代码结构:

// 示例代码:定义卷积神经网络结构
struct Net {
    conv1: nn::Conv2D,
    conv2: nn::Conv2D,
    fc1: nn::Linear,
    fc2: nn::Linear,
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/duke-rs-prog-dtengi-dop/img/5a2c0226ebcb7841da8dda69202505b3_9.png)

impl Net {
    fn new() -> Net {
        Net {
            conv1: nn::conv2d(1, 32, 5, Default::default()),
            conv2: nn::conv2d(32, 64, 5, Default::default()),
            fc1: nn::linear(1024, 1024),
            fc2: nn::linear(1024, 10),
        }
    }
}
// ... 后续包含前向传播、损失计算和优化器步骤

运行此训练脚本时,GPU同样会被有效利用,整个过程与使用Python编写PyTorch代码的体验差异不大,但获得了Rust在性能和安全性上的额外优势。

Rust PyTorch的优势总结

本节课中我们一起学习了Rust与PyTorch结合的高性能方案。回顾整个过程,可以总结出以下几个关键优势:

  1. 高性能系统编程:Rust允许开发者使用一门高性能的系统编程语言来训练深度学习模型,兼顾效率与控制力。
  2. 出色的绑定与工具链:Rust的PyTorch绑定质量极高,并且利用Cargo包管理器,使得依赖管理和项目构建比Python的pip+virtualenv更加简单和一致。
  3. 促进模型可复现与分发:使用Rust有助于以更可靠的方式打包模型。结合ONNX等便携式格式,可以更好地将模型工具化并分发给他人。这正体现了MLOps的一个核心思想:将模型作为产品进行封装和分发。

我们鼓励更多人尝试并演示这类结合系统编程语言Rust的示例。随着此类工具的成熟与普及,整个MLOps社区都将从中受益。

我们下次见!👋

078:使用AWS Lambda与EFS部署ONNX模型推理服务 🚀

在本教程中,我们将学习如何结合使用AWS Lambda、EFS(弹性文件系统)和ONNX模型格式,通过Rust语言构建一个无服务器的机器学习推理服务。我们将逐步介绍环境配置、模型部署和代码实现。

概述

本项目的核心目标是利用无服务器技术(AWS Lambda)来提供机器学习模型推理服务。通过将ONNX模型存储在EFS上,并使用高性能的Rust语言编写推理逻辑,我们可以实现易于部署、无需管理基础设施的解决方案。

架构与准备工作

首先,我们需要理解整个架构的工作流程。核心组件包括AWS Lambda函数、EFS文件系统以及一个能够访问EFS的开发环境(如AWS Cloud9)。Lambda函数将通过VPC访问EFS中存储的ONNX模型文件。

以下是实现此架构需要完成的关键步骤列表:

  1. 创建并配置EFS文件系统:这是存储ONNX模型的地方。
  2. 设置安全组规则:确保相关服务(如Cloud9、Lambda)可以通过端口5049与EFS通信。
  3. 配置AWS Lambda:为Lambda函数设置VPC、文件系统挂载点和访问点。
  4. 开发与测试Rust推理代码:编写能够从EFS加载ONNX模型并执行推理的Rust程序。

接下来,我们将详细探讨每个步骤。

步骤一:创建与配置EFS

我们需要在AWS控制台中创建一个EFS文件系统。创建过程通常使用默认设置即可。

创建完成后,关键步骤是将其挂载到开发环境(例如Cloud9实例)上。在EFS控制台的“附件”选项卡中,选择“使用EFS挂载助手”选项,并按照提供的命令在Cloud9终端中执行。此命令会将EFS卷挂载到指定目录。

重要提示:必须配置安全组,允许从Cloud9实例(以及后续的Lambda函数)通过端口5049访问EFS。您需要在EFS关联的安全组中添加入站规则。

步骤二:配置AWS Lambda

现在,我们需要创建一个Lambda函数,并配置其访问EFS的能力。

首先,在Lambda函数的配置中,需要将其放入一个VPC。这个VPC必须包含我们之前为EFS配置的、允许5049端口通信的安全组。

其次,我们需要为Lambda设置文件系统。在Lambda函数的配置页面,找到“文件系统”部分。这里需要提供:

  • 文件系统ID:您创建的EFS的ID。
  • 访问点ARN:需要在EFS控制台中创建一个“访问点”,并将其ARN填写在此处。
  • 本地挂载路径:Lambda函数内部访问EFS的路径,例如 /mnt/efs

AWS官方文档《在无服务器应用程序中将Amazon EFS用于Lambda》提供了此过程的详细步骤指南,建议参考。

步骤三:开发Rust推理代码

完成基础设施配置后,我们可以专注于Rust代码的开发。代码的核心任务是:从EFS挂载点加载ONNX模型文件,并运行推理。

以下是一个简化的代码结构示例:

// 引入必要的库,如onnxruntime-rs
use onnxruntime::{environment::Environment, session::Session};

fn load_model_from_efs(model_path: &str) -> Session {
    // 从EFS挂载路径(如 `/mnt/efs/squeezenet.onnx`)创建模型会话
    let environment = Environment::builder().build().unwrap();
    let session = environment
        .new_session_builder()
        .unwrap()
        .with_model_from_file(model_path)
        .unwrap();
    session
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/duke-rs-prog-dtengi-dop/img/bad4d7d40a9f9d22be5587fd249ed029_7.png)

fn run_inference(session: &Session, input_data: &[f32]) -> Vec<f32> {
    // 准备输入,运行推理,并返回结果
    // ... 具体实现取决于模型输入输出格式
    vec![]
}

在Lambda的主处理函数中,可以调用 load_model_from_efs 来初始化模型,然后对每个传入的请求调用 run_inference

一个实用的调试技巧是,在代码中添加一个辅助函数,用于列出EFS挂载点下的文件,以确认模型文件已正确就位。

步骤四:测试与部署

为了简化测试和部署流程,可以创建一个 Makefile。例如:

invoke:
	cargo lambda invoke --remote \
		--data-ascii '{"input": "sample_data"}'

这个命令会使用 cargo-lambda 工具将本地代码打包并部署到AWS Lambda,然后发送一个测试负载来触发函数。在输出日志中,您应该能看到模型加载成功和推理执行的记录。

对于开发环境,推荐使用GitHub Codespaces,它可以提供预配置的容器环境,并轻松集成CI/CD流程,方便进行迭代测试。

总结

本节课我们一起学习了构建基于AWS Lambda、EFS和ONNX的Rust推理服务的完整流程。我们首先了解了架构概览,然后逐步完成了EFS文件系统的创建与挂载、AWS Lambda的VPC与文件系统配置,最后探讨了Rust推理代码的核心逻辑与测试部署方法。

这种组合(Rust + ONNX + EFS + AWS Lambda)提供了一种高性能、易维护且成本高效的无服务器ML推理方案,预计将在越来越多的组织中得到应用。记住关键点:确保端口5049的通信畅通,遵循EFS挂载指南,并在Lambda中正确配置文件系统访问点。

079:模型微调理论基础 🧠

在本节课中,我们将学习迁移学习与模型微调的核心理论基础,特别是它们在自然语言处理领域的应用,并与传统的监督学习进行对比。


迁移学习的优势

上一节我们介绍了模型微调的概念,本节中我们来看看迁移学习相比其他机器学习方法有哪些具体优势。

一种非常经典的机器学习类型是监督学习。许多人听说过它,它本质上是利用历史数据训练一个模型,然后进行预测。例如,根据球员的数据点预测其未来的薪资,这就是一个经典的监督学习问题。

在自然语言处理领域,你可以看到,你会拥有一些新闻数据,然后创建一个摘要模型。这个模型可能在大量数据上进行训练,可能是一个拥有数十亿参数的大语言模型,其创建过程会非常昂贵。

如果你遇到另一种NLP问题,并且数据属于不同的领域,例如一个文学数据集,你就必须重复相同的过程,创建一个不同类型的新闻摘要模型。这个模型也可能有数十亿参数,并且非常昂贵。

这里的问题是,对于许多人和组织来说,他们既没有足够的原始数据作为起点,也没有计算资源来进行这种全局规模的监督学习。

幸运的是,这正是迁移学习的用武之地,也是Hugging Face平台的优势之一。你可以利用在特定领域(例如新闻数据)上预训练的模型主体,然后替换其“头部”。在这个例子中,就是新闻摘要模型的头部。你可以用较少的数据对这个头部进行微调,然后用于预测。

同样,你也可以在一个完全不同的领域上微调模型。假设一个模型是在新闻数据上训练的,你可以将其用于文学数据。你利用的模型主体可能包含数十亿参数,并且是由世界顶尖的机器学习工程师和研究人员训练的。然后,你只需要少量新数据,创建一个评估指标,用这些新数据微调该任务的“头部”,之后你就能进行预测了。

因此,核心思想是:你可以创建高质量的模型,这些模型训练效率极高,并且能应用于新领域。这就是迁移学习的关键优势,以及它如何应用于Hugging Face平台。


本节课中我们一起学习了迁移学习相比传统监督学习的核心优势,即能够高效地利用预训练模型的知识,通过少量数据和计算资源,在特定任务或新领域上获得高性能的模型。

080:模型微调实践 🚀

在本节课中,我们将学习如何利用Hugging Face官方文档和工具,实践一个完整的模型微调流程。我们将对比在CPU环境和GPU环境下进行微调的性能差异,并最终将微调好的模型推送到Hugging Face Hub。

概述

我们将首先查看Hugging Face官方课程中的微调部分,然后在Google Colab的CPU环境和GitHub Codespaces的GPU环境中分别运行相同的微调代码,以直观地比较两者的训练速度与效率。

查看官方微调教程

上一节我们介绍了模型微调的概念,本节中我们来看看如何实际操作。

让我们打开Hugging Face的官方文档,查看相关的微调课程。该课程提供了许多有用的内容,我们重点关注其中的“微调预训练模型”部分。

以下是该教程的核心步骤:

  1. 安装必要的软件包。
  2. 定义数据预处理和分词函数。
  3. 配置训练器(Trainer)。
  4. 执行训练,完成模型微调。

该教程的一个优点是,它可以直接在Google Colab中打开一个笔记本,并清晰地展示了各个代码段。

在CPU环境(Colab)中运行微调

现在,我们来看看如何在默认的CPU环境中运行这个微调示例。

首先,在Colab中连接运行时。默认情况下,它会连接到一个基于CPU的运行时。我们可以通过“更改运行时类型”来确认没有启用硬件加速。

接下来,执行“运行时”菜单下的“全部运行”命令。这会依次执行以下操作:

  • 安装必要的软件包,这可能需要一些时间。
  • 加载数据集(例如GLUE数据集)。
  • 下载并设置预训练模型。
  • 开始微调训练任务。

在CPU上运行微调是一个很好的基准测试,可以让我们了解在普通硬件上训练模型所需的时间。从执行情况看,完成3个训练周期(epoch)可能需要20到30分钟。

在GPU环境(Codespaces)中运行微调

上一节我们在CPU上体验了微调,本节中我们来看看在GPU环境下运行相同的代码会有多快。

我们将相同的微调代码放在启用了GPU的GitHub Codespaces环境中运行。核心代码逻辑保持一致,主要包括:

  • datasets库加载数据:load_dataset
  • 导入transformers相关的训练组件。
  • 下载数据(如GLUE数据集)。
  • 创建分词函数。
  • 设置模型、训练参数和评估指标。
  • 启动训练器执行微调。

为了监控GPU使用情况,我们可以运行命令nvidia-smi。然后,执行我们的微调脚本。

得益于GPU的并行计算能力,训练过程显著加快。我们可以观察到GPU(例如Tesla V100)的利用率迅速上升,并且训练步骤快速推进。同时,我们关注的评估指标(如F1分数)也会随着训练逐步提升,这意味着模型在自定义数据上的性能正在优化。

相比之下,之前Colab中的CPU任务可能仍在缓慢进行中。这清晰地展示了拥有GPU资源对于加速模型微调实验的巨大优势。

保存与分享微调模型

当我们对微调后模型的性能提升感到满意时,可以将其保存并分享。

在训练参数(TrainingArguments)中,我们可以进行设置,以便在每次模型性能提升时将微调后的模型推送到Hugging Face Hub。这与其他开发者在Hugging Face上分享模型的做法相同。

例如,我们可以回到Hugging Face模型库,查看“文本摘要”类别的模型。可以看到许多模型都是基于原始架构(如Facebook的BART)进行微调后,再发布到个人或组织名下的。我们完全可以遵循相同的流程,创建并分享自己的微调模型。

总结

本节课中我们一起学习了模型微调的完整实践流程。我们对比了在CPU和GPU两种不同硬件环境下执行微调的速度差异,直观感受到了GPU对深度学习工作流的加速作用。最后,我们还了解了如何将微调好的模型推送到Hugging Face Hub进行保存和分享。这是一个快速实验并迭代自己微调模型的绝佳方式。

081:GCP数据库选型指南 🗄️

在本节课中,我们将学习如何在Google Cloud Platform(GCP)上选择合适的数据库服务。我们将逐一分析GCP提供的多种数据库选项,了解它们各自的特点、适用场景以及权衡取舍。

概述

在GCP上使用数据库的一个重要环节,是权衡不同类型的专用数据库解决方案之间的利弊。每种数据库都有其独特的设计目标和优势,理解这些差异是做出正确技术选型的关键。

数据库选项详解

以下是GCP提供的核心数据库服务,我们将逐一进行介绍。

Cloud SQL ☁️

上一节我们概述了数据库选型的重要性,本节中我们首先来看看Cloud SQL选项。

Cloud SQL是一项全托管的数据库服务,允许您维护传统类型的数据库,例如PostgreSQL、MySQL或SQL Server数据库。其核心优势在于,它允许您利用Google Cloud的基础设施来承担繁重的运维工作。

因此,对于已经对上述常见开源数据库有深厚经验的人来说,这是一个非常好的解决方案。

Bigtable 📊

接下来我们看看Bigtable选项。Bigtable可以扩展到数十亿行和数千列。例如,如果您拥有PB级别的数据,这可能是一个首先值得关注的选项。

Bigtable的每一行都被索引,其索引值是一个已知的行键(row key)。这是一种不同类型的解决方案,其特点是单键数据访问且延迟极低。因此,它支持在低延迟下实现非常高的读写吞吐量,适用于MapReduce类型的操作。

Cloud Spanner 🌍

另一个GCP独有的独特产品是Cloud Spanner。这是一个全球分布式企业级数据库服务。其一大区别在于,它从一开始就是为全球规模而设计的。

它也是一个非共享、水平可扩展的关系型数据库。这两者都是其内部可用的特性。此外,它还为您提供了非常高的服务等级协议(SLA)和企业级安全性。

因此,如果您想构建一个全球规模的系统,Cloud Spanner将是一个很好的解决方案。

Memorystore (Redis) ⚡

我们还有Memorystore,这是一项托管的Redis服务。这是一个基于缓存的系统,允许您管理一个缓存数据库。

这在以下场景中非常有用:例如存储游戏中的状态,或者存储那些需要频繁使用但本身不一定需要频繁更改的数据。

Firestore 🔥

我们还有Firestore。Firestore是一个NoSQL文档数据库,专为自动扩展而构建。这几乎是一种无服务器类型的解决方案,因为您是在服务之上进行构建,而不是自己管理服务。

对于应用程序开发人员来说,Firestore也是一个非常合适的选择。

Firebase Realtime Database 📱

此外,还有Firebase Realtime数据库。这是一个基于JSON的解决方案,允许您为Google、iOS、Android和JavaScript构建跨平台应用程序。

通过使用这些高级服务,您可以非常快速地构建应用。

开源数据库 🐧

当然,GCP也支持开源数据库。您可以使用MongoDB、MariaDB、Redis等。

选型考量与总结

总而言之,您在这里有许多不同的选择。例如,在Spanner解决方案中,它从一开始就具备全球扩展能力。如果您想构建一个向全球分发新闻内容的新闻机构,这可能是一个很好的解决方案。

如果您的公司已经有很多基于PostgreSQL的解决方案,并且正在迁移到云端,那么您可能会选择Cloud SQL选项。

了解这些选择的优缺点至关重要。您可以选择一种解决方案,或者结合几种解决方案,为您的组织在Google Cloud上创造独特的价值主张。

在本节课中,我们一起学习了GCP上主要的数据库服务选项,包括Cloud SQL、Bigtable、Cloud Spanner、Memorystore、Firestore、Firebase Realtime Database以及对开源数据库的支持。理解每种服务的核心特性和适用场景,是设计高效、可扩展云架构的基础。

082:82_04_03_Rust SQLite与Hugging Face零样本分类器 🚀

概述

在本节课中,我们将学习如何利用Rust构建一个高效的生产级机器学习应用。具体来说,我们将创建一个歌词分析器,它能够从文件中读取歌词,使用Hugging Face的零样本分类模型进行分析,并将结果存储到SQLite数据库中。我们将探讨为何Rust是MLOps(机器学习运维)的理想选择,并逐步实现一个可扩展的原型。


为什么选择Rust进行MLOps?⚡

上一节我们概述了课程目标,本节中我们来看看为什么Rust在机器学习运维领域具有独特优势。

许多人认为Python是机器学习的主导语言,但Rust同样是一个出色的候选者,尤其是在生产部署方面。Rust的核心优势在于其性能生产优先的设计理念。

  • 卓越的性能:Rust在许多操作上可比Python快高达70倍,并且能高效利用多核CPU进行并行计算。
  • 生产优先:Rust允许你将模型打包成独立的二进制文件直接部署到生产环境,无需复杂的依赖环境,实现了真正的二进制可移植性。
  • 能效与资源利用:Rust程序消耗更少的能源和计算资源,这对于需要处理海量数据(例如分析数百万首歌曲)的任务至关重要。

因此,对于构建需要高性能、高并发和高效资源利用的MLOps流水线,Rust是一个理想的选择。


项目架构设计 🏗️

了解了Rust的优势后,我们来看看如何设计一个真实的歌词分析系统。假设我们是一家音乐公司,拥有数百万首歌曲,希望用现代LLM技术对曲库进行分类,以用于推荐引擎或商业分析。

以下是该系统的核心架构流程:

  1. SQLite数据库:存储预先定义好的歌曲分类候选标签,例如:摇滚流行嘻哈乡村拉丁
  2. 歌词转录:高效地遍历文件系统,将数百万首音频文件转录为文本。Rust的高性能和多线程能力在此环节至关重要。
  3. 零样本分类:使用Rust的Hugging Face绑定库,将转录后的歌词文本送入预训练模型进行分类。
  4. 结果回写:将分类结果写回数据库,完成整个分析流程。

这个架构代表了一个真实、可投入生产的世界级系统。


开始编码:环境与依赖设置 💻

现在,让我们开始动手构建这个项目。我们将使用GitHub Codespaces作为开发环境,并借助Cargo生态系统来管理项目。

首先,创建一个新的Rust项目:

cargo new sqlite_hf
cd sqlite_hf

接下来,我们需要配置项目的依赖项。编辑 Cargo.toml 文件,添加必要的库。

以下是项目所需的依赖项:

  • rust-bert:用于加载和使用Hugging Face的预训练模型。
  • rusqlite:用于操作SQLite数据库。
  • tokio:用于编写异步代码(如果需要处理高并发I/O)。
  • anyhowthiserror:用于优雅的错误处理。
[dependencies]
rust-bert = "0.21.0"
rusqlite = { version = "0.29.0", features = ["bundled"] }
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
thiserror = "1.0"

Rust拥有丰富的MLOps库生态系统。例如,rust-bert库提供了即用型的NLP管道和语言模型,其底层的tokenizers库正是因为Rust实现而速度极快。这证明了Rust在性能关键型MLOps能力中的核心地位。


构建核心库代码 📚

依赖设置完成后,我们开始编写核心逻辑。首先在 src 目录下创建 lib.rs 文件。

我们将实现以下几个主要功能:

1. 初始化数据库与候选标签

首先,我们需要创建一个SQLite数据库并在其中插入零样本分类的候选标签。

// src/lib.rs
use rusqlite::{Connection, Result};

pub fn initialize_database() -> Result<Connection> {
    // 在内存中创建数据库(生产环境应持久化到磁盘)
    let conn = Connection::open_in_memory()?;

    conn.execute(
        "CREATE TABLE IF NOT EXISTS zero_shot_candidates (
            id INTEGER PRIMARY KEY,
            label TEXT NOT NULL UNIQUE
        )",
        [],
    )?;

    // 插入预定义的分类标签
    let labels = ["摇滚", "流行", "嘻哈", "乡村", "拉丁"];
    for label in &labels {
        conn.execute(
            "INSERT OR IGNORE INTO zero_shot_candidates (label) VALUES (?)",
            [label],
        )?;
    }

    Ok(conn)
}

这段代码创建了一个内存数据库,建立了一张表,并插入了我们预设的音乐流派标签。rusqlite的API非常直观,与Python中的SQLite操作类似。

2. 从数据库读取候选标签

接下来,编写一个函数从数据库中查询所有候选标签。

pub fn get_candidates(conn: &Connection) -> Result<Vec<String>> {
    let mut stmt = conn.prepare("SELECT label FROM zero_shot_candidates")?;
    let label_iter = stmt.query_map([], |row| row.get(0))?;

    let mut candidates = Vec::new();
    for label in label_iter {
        candidates.push(label?);
    }
    Ok(candidates)
}

这个函数执行一个简单的SELECT查询,并将结果收集到一个Vec<String>(可变的字符串列表)中返回。对于Python开发者来说,Vec类似于list

3. 从文件读取歌词

我们需要从文本文件中读取歌词。创建一个 lyrics.txt 文件并粘贴一些歌词内容。

// src/lib.rs
use std::fs;
use anyhow::Result as AnyResult;

pub fn read_lyrics_from_file(file_path: &str) -> AnyResult<Vec<String>> {
    let content = fs::read_to_string(file_path)?;
    // 假设每行歌词是一个独立的字符串,或根据实际情况分割
    let lines: Vec<String> = content.lines().map(String::from).collect();
    Ok(lines)
}

这个函数读取指定文件的内容,并按行分割成字符串向量。注意let关键字用于声明不可变变量,这是Rust保证内存安全和代码健壮性的核心机制之一。


集成Hugging Face进行零样本分类 🤖

核心数据准备就绪后,本节我们来看看如何集成机器学习模型。我们将使用rust-bert库调用Hugging Face的零样本分类管道。

首先,在 Cargo.toml 中确保已添加 rust-bert 依赖。

以下是进行分类的核心函数:

// src/lib.rs
use rust_bert::pipelines::zero_shot_classification::ZeroShotClassificationModel;
use anyhow::{Result, Context};

pub fn classify_lyrics(lyrics: Vec<String>, candidates: &[String]) -> Result<Vec<String>> {
    // 加载预训练的零样本分类模型
    let model = ZeroShotClassificationModel::new(Default::default())
        .context("Failed to load zero-shot classification model")?;

    // 对每一段歌词进行分类
    let mut results = Vec::new();
    for lyric_chunk in lyrics {
        // 模型返回每个候选标签的置信度分数
        let output = model.predict(
            vec![lyric_chunk.as_str()], // 输入文本
            candidates.to_vec(),         // 候选标签
            None,                        // 假设标签互斥
            1,                           // 返回top-k个结果
        )?;

        // 获取置信度最高的标签
        if let Some(best_label) = output[0].get(0) {
            results.push(best_label.text.clone());
        }
    }

    Ok(results)
}

这段代码完成了以下工作:

  1. 初始化一个零样本分类模型。
  2. 遍历输入的歌词片段。
  3. 对于每段歌词,模型会计算其属于每个候选标签(如“摇滚”、“流行”)的置信度。
  4. 选择置信度最高的标签作为分类结果。

rust-bert的API设计清晰,使用起来与Python版的Hugging Face Transformers库非常相似,体现了Rust在保持高性能的同时,也兼顾了开发者的使用体验。


整合与运行:主程序 🎯

最后,我们将所有模块在 src/main.rs 中整合起来,形成一个完整的可执行程序。

// src/main.rs
use sqlite_hf::{initialize_database, get_candidates, read_lyrics_from_file, classify_lyrics};
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    println!("🚀 开始歌词分类分析...");

    // 1. 初始化数据库并获取候选标签
    let conn = initialize_database()?;
    let candidates = get_candidates(&conn)?;
    println!("📋 候选分类标签: {:?}", candidates);

    // 2. 从文件读取歌词
    let lyrics = read_lyrics_from_file("lyrics.txt")?;
    println!("📄 读取到 {} 段歌词", lyrics.len());

    // 3. 使用Hugging Face模型进行分类
    println!("🤖 正在使用零样本模型进行分类...");
    let classifications = classify_lyrics(lyrics, &candidates)?;

    // 4. 输出结果
    println!("✅ 分类完成!");
    for (i, label) in classifications.iter().enumerate() {
        println!("  歌词片段 {} -> 分类: {}", i + 1, label);
    }

    // 5. (可选) 将结果写回数据库
    // ...

    Ok(())
}

运行程序:

cargo run

如果一切顺利,你将看到程序成功读取歌词,并通过Hugging Face模型输出了每一段歌词最可能的音乐流派分类。


总结

本节课中,我们一起学习并实践了如何使用Rust构建一个完整的MLOps应用原型。我们:

  1. 探讨了Rust在MLOps中的优势:包括其卓越的性能、生产优先的二进制部署以及高效的资源利用。
  2. 设计了系统架构:从SQLite数据库、歌词转录到零样本分类的完整流程。
  3. 实现了核心功能
    • 使用rusqlite操作数据库。
    • 使用标准库进行文件I/O。
    • 集成rust-bert库调用Hugging Face的零样本分类模型。
  4. 整合并运行了完整应用:将各个模块串联,实现了从歌词文件到分类结果的端到端分析。

这个原型虽然简单,但其架构和使用的技术栈(Rust + SQLite + Hugging Face)具备处理海量数据的潜力。通过利用Rust的并发特性,你可以轻松地将其扩展为能够并行处理数百万首歌曲的高性能生产系统。这充分证明了Rust是构建下一代高效、可靠MLOps工具链的强大语言。

083:83_04_04_BigQuery提示工程 🚀

在本节课中,我们将学习如何将提示工程应用于BigQuery平台。我们将通过一个具体的例子,展示如何利用大型语言模型(如ChatGPT)来理解、修改和增强SQL查询,从而更高效地进行数据分析和处理。

概述

提示工程是一种与大型语言模型交互的技术,通过精心设计的指令(提示)来引导模型生成所需的代码或解释。本节我们将探索如何将这一技术应用于Google BigQuery,以简化SQL查询的编写和优化过程。

开始使用提示工程

上一节我们介绍了提示工程的基本概念。本节中,我们来看看如何在Open AI的平台上开始实践。

在Open AI的界面中,我们可以开始进行提示工程。这里的核心思想是,通过查看各种示例,我们可以找到多种从大型语言模型中获取代码的方法。以“SQL翻译”为例,这是一个很好的起点。我们可以打开一个“Playground”,提交提示,并尝试获得一些SQL答案。这个过程就是通过提示程序来获取代码,并进行反复调整。

在BigQuery中应用提示工程

那么,我们如何在像BigQuery这样的平台上实际应用提示工程呢?让我们通过一个实例来探索。

首先,我们查看一些数据。这里展示的是Google Cloud的搜索趋势数据。在Google Trends数据集中,我们可以看到诸如“top rising terms”等项目。如果我们想查看其中一个查询,可以打开它并开始操作。

但如果我们不完全清楚如何格式化查询或使其更完善呢?一个简单的方法是将查询语句复制到ChatGPT中寻求帮助。

以下是请求解释查询的示例:

-- 原始查询示例
SELECT term, score
FROM `bigquery-public-data.google_trends.international_top_terms`
WHERE refresh_date = DATE('2023-04-17')
ORDER BY score DESC
LIMIT 100;

我们可以向ChatGPT提出请求:“请为我解释这个Google BigQuery查询。”模型会返回解释,说明这个查询旨在从特定日期范围的Google Trends数据集中提取前100个上升最快的搜索词。这是开始构建查询时理解现有代码的好方法。

修改和优化查询

理解了基础查询后,我们可能想对其进行修改。例如,我们可能只想获取前10个结果,而不是100个。

我们可以向ChatGPT提供原始代码,并给出新的指令:“请修改此查询,只获取前10个结果。”

模型可能会返回修改后的代码:

SELECT term, score
FROM `bigquery-public-data.google_trends.international_top_terms`
WHERE refresh_date = DATE('2023-04-17')
ORDER BY score DESC
LIMIT 10; -- 修改为仅获取前10个结果

我们将修改后的代码粘贴回BigQuery并运行,就能成功获得前10个结果。

实现更复杂的转换

接下来,我们可以尝试更复杂的转换。例如,我们希望所有结果都以大写形式显示。

我们可以继续向ChatGPT提出请求:“现在我需要所有结果都显示为大写字母。”

模型可能会建议使用UPPER()函数:

SELECT UPPER(term) AS term, score -- 使用UPPER函数将术语转换为大写
FROM `bigquery-public-data.google_trends.international_top_terms`
WHERE refresh_date = DATE('2023-04-17')
ORDER BY score DESC
LIMIT 10;

再次将代码粘贴到BigQuery中运行,结果确实全部变成了大写。

我们还可以进行其他调整,例如将结果中的空格替换为下划线。

提出请求:“现在将所有空格替换为下划线。”

模型可能会建议使用REPLACE()函数:

SELECT REPLACE(UPPER(term), ' ', '_') AS term, score -- 将大写后的术语中的空格替换为下划线
FROM `bigquery-public-data.google_trends.international_top_terms`
WHERE refresh_date = DATE('2023-04-17')
ORDER BY score DESC
LIMIT 10;

逐步增加查询复杂度

在提示工程中,采用渐进式的方法逐步增加需求是获取理想结果的最佳策略之一。

观察查询,我们发现数据中还有“country_code”和“country_name”字段。我们可以要求将这些信息也加入查询结果中。

提出请求:“请在查询中添加国家代码和国家名称。”

模型会相应修改SELECT子句和GROUP BY子句:

SELECT
  country_code,
  country_name,
  REPLACE(UPPER(term), ' ', '_') AS term,
  score
FROM `bigquery-public-data.google_trends.international_top_terms`
WHERE refresh_date = DATE('2023-04-17')
GROUP BY country_code, country_name, term, score
ORDER BY score DESC
LIMIT 10;

构建最终复杂查询

最后,我们可以尝试构建一个更复杂的查询。例如,将结果扩展到前50条,并新增一列来统计每个国家出现的搜索词数量。

提出一个综合请求:“现在将查询扩展到返回50个结果,并添加一个我们自己生成的列,用于统计每个国家的结果出现次数,并将其包含在输出中。”

模型可能会生成如下查询:

SELECT
  country_code,
  country_name,
  REPLACE(UPPER(term), ' ', '_') AS term,
  score,
  COUNT(*) OVER (PARTITION BY country_name) AS country_occurrence_count -- 使用窗口函数统计每个国家的出现次数
FROM `bigquery-public-data.google_trends.international_top_terms`
WHERE refresh_date = DATE('2023-04-17')
ORDER BY score DESC
LIMIT 50;

运行这个查询,我们成功获得了前50个结果,并且包含了每个国家搜索词的出现次数统计。

总结

本节课中,我们一起学习了如何将提示工程应用于BigQuery。我们通过实例演示了如何:

  1. 使用大型语言模型解释现有的SQL查询。
  2. 通过自然语言指令修改和优化查询(如限制结果数量、转换文本格式)。
  3. 以渐进的方式增加查询的复杂度,逐步添加所需字段和计算列。
  4. 最终构建一个包含数据统计的复杂查询。

这种方法极大地提升了使用BigQuery等数据平台进行探索和开发的效率,就像有一位数据库专家随时在身边提供帮助。通过有效的提示工程,我们可以为未来的机器学习模型训练或其他数据分析任务快速准备好所需的数据视图。

084:构建BigQuery到Colab的数据管道 📊

在本节课中,我们将学习如何构建一个非常贴近实际的数据清洗与可视化管道。我们将使用Google BigQuery作为数据源,通过迭代式查询来探索和清洗数据,最终在Google Colab笔记本中使用Python和Seaborn库进行数据分析和可视化。


概述

我们将从一个公开的维基百科页面浏览数据表开始。目标是找出特定日期(4月18日)内浏览量最高的页面。这个过程涉及多个阶段:首先在BigQuery中编写和迭代优化SQL查询以获取和初步清洗数据,然后将查询结果导入Colab笔记本,在Python环境中进行进一步的数据处理和最终的可视化呈现。


在BigQuery中初步查询数据

首先,我们需要在BigQuery中查看数据。我们使用的表包含维基百科的页面浏览数据,字段包括日期、小时、维基项目、页面标题和浏览量。

以下是我们编写的初始查询,目的是获取2023年4月18日浏览量最高的前1000条记录:

SELECT views, title
FROM `bigquery-public-data.wikipedia.pageviews_2023`
WHERE date = '2023-04-18'
ORDER BY views DESC
LIMIT 1000

运行此查询后,我们得到了结果,但其中包含了许多我们并不关心的条目,例如“Main_Page”(维基百科主页)或一些特殊页面。这些条目会干扰我们对实际热门内容的理解。


迭代优化查询以清洗数据

上一节我们运行了初步查询,发现结果中包含大量无关条目。本节中,我们将通过添加过滤条件来优化SQL查询,以排除这些噪音数据。

我们需要修改查询,排除标题为“Main_Page”、以“Special:”开头或属于特定维基项目(如“Wikipedia:”)的记录。优化后的查询如下:

SELECT views, title
FROM `bigquery-public-data.wikipedia.pageviews_2023`
WHERE date = '2023-04-18'
  AND title != ‘Main_Page’
  AND NOT STARTS_WITH(title, ‘Special:’)
  AND NOT STARTS_WITH(title, ‘Wikipedia:’)
ORDER BY views DESC
LIMIT 1000

运行这个优化后的查询,结果得到了显著改善。但我们发现,仍然存在一些其他语言的主页(例如“Hauptseite”是德语的主页)。因此,我们需要继续迭代优化。

我们再次修改查询,添加一个条件来排除标题中包含“_page”的记录。以下是进一步优化后的查询:

SELECT views, title
FROM `bigquery-public-data.wikipedia.pageviews_2023`
WHERE date = ‘2023-04-18’
  AND title != ‘Main_Page’
  AND NOT STARTS_WITH(title, ‘Special:’)
  AND NOT STARTS_WITH(title, ‘Wikipedia:’)
  AND NOT title LIKE ‘%_page’
ORDER BY views DESC
LIMIT 1000

经过几次这样的迭代,我们最终获得了一个相对干净、只包含实际文章页面的数据集。这种迭代式查询和过滤是数据清洗中的常见做法。


将数据导入Colab进行Python分析

在BigQuery中完成初步的数据筛选后,我们获得了更干净的数据集。接下来,我们将把查询转移到Google Colab笔记本中,利用Python环境进行更灵活的分析和处理。

首先,我们需要在Colab中设置环境并验证身份以访问BigQuery。然后,执行与上一节最终版相同的SQL查询,并将结果加载到一个Pandas DataFrame中。

以下是实现此步骤的核心代码:

# 设置BigQuery客户端并运行查询
from google.colab import auth
auth.authenticate_user()

query = “””
SELECT views, title
FROM `bigquery-public-data.wikipedia.pageviews_2023`
WHERE date = ‘2023-04-18’
  AND title != ‘Main_Page’
  AND NOT STARTS_WITH(title, ‘Special:’)
  AND NOT STARTS_WITH(title, ‘Wikipedia:’)
  AND NOT title LIKE ‘%_page’
ORDER BY views DESC
LIMIT 1000
“””

# 将查询结果加载到DataFrame
import pandas as pd
results = pd.read_gbq(query, project_id=‘YOUR_PROJECT_ID’)
print(results.head())

现在,数据已经以结构化的DataFrame形式存在于Python环境中,我们可以方便地使用Pandas进行各种操作。


在Python中进行进一步的数据处理

上一节我们将数据成功导入Colab。本节中,我们将在Python中对DataFrame进行进一步处理,例如对重复标题的浏览量进行求和,以得到每个页面的总浏览量。

我们注意到原始数据中可能因为不同小时段而有重复的标题。为了得到每个页面的总浏览量,我们需要按标题进行分组求和。

以下是按标题分组并汇总浏览量的代码:

# 按‘title’分组,并对‘views’求和
results_aggregated = results.groupby(‘title’)[‘views’].sum().reset_index()
# 按总浏览量降序排列
results_aggregated = results_aggregated.sort_values(by=‘views’, ascending=False)
print(results_aggregated.head(10))

这个操作将数据聚合,使我们能更清晰地看到每个页面的受欢迎程度,为下一步的可视化做好准备。


使用Seaborn创建可视化图表

经过清洗和聚合,我们得到了最终的数据集。现在,我们可以使用Seaborn库来创建图表,直观地展示2023年4月18日维基百科上最受欢迎的页面。

我们将选取总浏览量排名前25的页面,并创建一个条形图。

以下是创建可视化图表的完整代码:

import seaborn as sns
import matplotlib.pyplot as plt

# 获取前25名
top_25 = results_aggregated.head(25)

# 设置图表样式
sns.set_style(“whitegrid”)
plt.figure(figsize=(12, 8))

# 创建条形图
bar_plot = sns.barplot(x=‘views’, y=‘title’, data=top_25, palette=“viridis”)
bar_plot.set_xlabel(‘Total Views’)
bar_plot.set_ylabel(‘Page Title’)
bar_plot.set_title(‘Top 25 Most Viewed Wikipedia Pages on 2023-04-18’)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/duke-rs-prog-dtengi-dop/img/d41d61ae5e7b12aae410d4fa00f39944_4.png)

plt.tight_layout()
plt.show()

运行这段代码后,我们将得到一个清晰的条形图。从图中可以直观看出,在指定日期,“NoA_Cyrus”和“ChatGPT”等页面获得了极高的浏览量。


总结

本节课中,我们一起学习并实践了一个完整的数据管道构建过程。我们从Google BigQuery中的原始数据出发,通过迭代编写SQL查询来逐步清洗和筛选数据。然后,我们将查询迁移到Google Colab笔记本中,利用Python的Pandas库进行数据聚合,最后使用Seaborn库生成了直观的可视化图表,成功展示了特定日期内维基百科上最受欢迎的页面。这个流程涵盖了数据工程中从提取、转换到加载和可视化(ETL-V)的核心步骤。

085:使用BigQuery进行数据探索 🧭

在本节课中,我们将学习如何使用Google Cloud平台上的强大工具——BigQuery来探索和分析数据。我们将从访问公共数据集开始,逐步学习如何编写查询、理解数据模式,并利用可视化工具进行深入分析。


探索BigQuery界面与公共数据集

上一节我们介绍了课程目标,本节中我们来看看BigQuery的基本界面和如何利用其公共数据集。

BigQuery是Google Cloud平台上用于探索数据的强大工具。查看此处的概览,可以看到可以编写新查询、添加数据,甚至上传本地文件。因此,有很多简单的方法可以开始使用,但最强大的方法之一可能是使用公共数据集。

如果我们查看一个现有的Google趋势数据集,这是深入了解如何使用特定数据的好方法。我通常会做的第一件事是查看有关此特定数据的元数据。可以看到它最后修改于2022年9月20日,这是一个Google趋势数据集。


编写并运行SQL查询

了解了数据集的基本信息后,接下来我们学习如何编写和运行查询来获取数据。

如果想继续查询,一个好方法是转到切换节点,可以看到此特定数据的不同部分。例如,假设我想查看一些热门词汇,我可以查看词汇、周度分数、排名等信息,以确定我实际想做什么。事实上,如果我只选择这个“词汇”字段,它会尝试自动为我构建查询。因此,实际上我不需要做太多工作来查询,例如,查看热门词汇并查看此特定数据集中的前10个项目。可以看到,由于湖人队正在参加季后赛,目前“Lakers”在词汇中非常流行。同样,如果我想查看分数、排名等,也可以做同样的事情。

我们可以从这里开始,创建一个能显示更多细节的查询。让我们关闭这个,然后在这里格式化一个查询,显示不同的热门词汇,甚至可能使其更清晰一些。

以下是构建查询的步骤:

  1. 使用 SELECT 语句选择字段。
  2. 使用 WHERE 子句指定条件。
  3. 使用 LIMIT 子句限制返回的行数。
SELECT term, refresh_date AS top_date
FROM `bigquery-public-data.google_trends.top_terms`
WHERE refresh_date BETWEEN '2023-04-01' AND '2023-04-17'
LIMIT 100

此外,如果转到“更多”选项,甚至可以格式化查询。如果想保存查询,这通常是个好主意,也可以另存为。这很好,因为可以将其命名为“热门词汇”或任何你想做的,并从此开始,我可以反复使用它。这实际上可能是数据科学家希望定期运行的东西,他们可以调整它以运行。

如果我运行这个查询,可以看到我在此特定日期范围内获得了所有不同的热门词汇。我可以查看历史上的任何时间,可以看到人们感兴趣查看的许多关键词汇,涉及流行文化等领域。


利用工具进行深入探索与分析

运行基础查询后,数据科学家通常需要更深入地审视数据,而不仅仅是SQL查询。这时,其他功能就派上用场了。

一旦获得这样的数据集,一件有趣的事情就是进一步探索数据。作为数据科学家,如果要构建机器学习模型,很多时候首先需要查看数据,甚至超越SQL查询。这就是这些其他功能发挥作用的地方。让我们点击“探索数据”。注意我有三个选项。电子表格实际上是许多场景下相当不错的选择。我也可以用Coab笔记本打开并进行一些查询,这也是一个非常好的选择。我还可以使用仪表板类型的工具查看它。让我们先看看Looker Studio。如果我深入研究这个Looker Studio,它会给我一些默认查询,我实际上可以查看不同的字段和词汇等,并从那里查询。如果我愿意,也可以保存它并与其他人分享。因此,对于进行探索性数据分析来说,这是一个非常全面的流程。


查询其他数据集并组合数据源

除了探索单一数据集,我们还可以查询其他数据集,组合不同来源的信息以获得新见解。

接下来我要做的是查看其他数据集。注意,在这个公共数据集中,我处于Google趋势中,正在查看热门词汇,但国际热门词汇或热门上升词汇呢?嗯,我已经有一个查询,我将把它放入这个窗口,放在这里,实际上我要保存这个查询,我将其命名为“热门词汇”。如果我运行它,可以看到这实际上是一个国际热门词汇查询,再次看到这是来自4月17日,可以看到这里所有不同的热门词汇,结果非常不同。同样,如果我想把它放到Looker Studio中,这将是一个好方法。

然而,除了这个特定的数据集,我们还可以查看其他数据集,寻找关于它的新信息,并可能组合这些不同的来源。你可以在这里浏览。有很多不同的数据集可供使用:Google广告、Google Cloud发布、政治趋势等。我将选择一个有趣的数据集,即Hacker News。如果我们查看Hacker News,可以看到这些数据包含自2006年以来Hacker News的所有故事和评论。这是一个较小的聚合器,聚合故事,很多技术人员对这个特定数据感兴趣。可以看到它也比较新,最后修改于2022年9月20日。因此,我们应该能够找到一些非常酷的信息。假设是故事,如果我在这里查看,我可以再次看到字段。因此,首先查看模式,看看有哪些不同的字段名称是我需要关心的,这总是很重要。例如,故事分数可能是一个好字段,比如特定分数的强度如何,了解有史以来最受欢迎的故事是什么,它们实际得分是多少。中位数分数是多少?第25百分位数是多少?第75百分位数是多少?所有这些描述性统计在初次查看事物时都很有帮助。

所以这是我要在这里查询的字段。我将转到查询,让我们把它放到一个新标签页中,再次,它给了我们一个默认查询。但我已经有一个查询要开始。我想查看Hacker News上有史以来创建的热门故事。让我们查询这个。

SELECT id, title, author, score, timestamp
FROM `bigquery-public-data.hacker_news.full`
WHERE type = 'story'
ORDER BY score DESC
LIMIT 10

再次,我可以格式化它以确保一切看起来都很好。通常,确保语法正确是个好主意。同样,我可以保存这个,我们可以保存查询,我们可以称之为“热门Hacker News故事”之类的。我们可以继续保存它,接下来,我选择运行。再次,它将在这里运行并给我们这个很好的查询。现在,如果我要进行一些自然语言处理,这个查询会更丰富,因为我们这里有作者。我们还有分数,现在变得非常有趣,我们看到这里有6000,4000,甚至可以看到这些前10名分数之间的一些非常大的差异,这似乎有点不寻常。注意,作为数据科学家,我们看到一个趋势,我们看到斯蒂芬·霍金去世了。这是有史以来最热门的故事之一。史蒂夫·乔布斯去世了,也是有史以来最热门的故事之一。如果我们看看人们离开公司,可以看到有一种特定的风格,也许人们正在查看故事,如果我们看更多,让我们看看100个故事,我们是否能看到更多我们可以深入研究的信息,我们可以看到谷歌搜索正在消亡,所以基本上这里有一些非常有趣的故事,你可以深入研究。


进行描述性统计与数据可视化

为了更深入地理解数据分布,我们可以计算描述性统计量并创建可视化图表。

让我们继续进一步探索这些数据,我将在这里查看10个,然后再次运行该查询。然后我将继续用Looker Studio探索数据。从这里,我也可以更深入地研究细节,我可以查看特定字段或寻找操作数据的方法。例如,现在是作者,但ID没有给我们任何信息,这不是我们关心的。所以,如果我们想向下滚动到这里,上面写着一些I,这没有意义,我们实际上可能更关心分数的总和,这可能是我们更关心的,所以我可以继续更改该查询,并在此可视化中获得不同的结果。同样,如果我想更改这个,也许我会在这里更改,而不是按ID,也许我会按其他字段更改。所以你可以在这里查询不同的字段,使用这个非常容易地创建不同的可视化,并再次与他人分享。

但这让我思考,既然我们看到这些是这里的分数,如果我们想创建一个预测模型,我们对此特定数据集了解多少?分数之间存在线性关系吗?还是像幂律分布,有非常受欢迎的故事和不受欢迎的故事?因此,我们接下来可以做的一件事是回到这个Hacker News查询,让我们实际拉出一个查看四分位数的查询。所以这将是一件有趣的事情。

SELECT
  APPROX_QUANTILES(score, 4) AS quartiles
FROM `bigquery-public-data.hacker_news.full`
WHERE type = 'story' AND score IS NOT NULL

让我们继续找出四个分位数。让我们实际找出第一个四分位数(前25%)、第二个四分位数(中位数)、第三个四分位数和第四个四分位数。让我们看看,什么是查看此特定数据集的好方法。如果我们在这里操作并说保存查询,我们可以查看Hacker News四分位数。从这里,我们得到的结果令人震惊。我们看到这里有一个错误。让我们继续格式化这个。好了。让我们继续运行它。好了。我们得到了一个结果。所以我们在这里看到的是,有点奇怪的是,就第一个四分位数而言,前25%,很多故事分数很低。中位数也非常低,典型的故事,即50%低于此值,这些分数实际上也很低。甚至75%的故事分数为3。但在这前25%中,你可以看到,只有非常小比例的故事获得了非常高的分数。因此,当我们想要创建一个模型,甚至以寻找热门故事的方式建模时,我们可能会考虑这些信息,如果我们打算使用这些数据做一些其他类型的预测模型。


总结与回顾

本节课中我们一起学习了如何使用Google Cloud上的BigQuery引擎。我们首先学习了进行SQL查询,来回迭代修改它们,然后还进入了像Looker这样的工具。接着保存这些结果,并与团队中的其他人分享。这确实是在Google Cloud上开始使用BigQuery引擎的好方法。

086:86_04_07_数据科学公共数据集使用 📊

在本节课中,我们将学习如何利用公共数据集来构建机器学习系统。公共数据集是数据科学和机器学习项目的重要起点,它们提供了经过整理和标注的数据,使我们能够快速进行模型训练和实验。


构建机器学习系统的一个非常常见的方法是使用公共数据集。

我们来讨论几个可用的常见公共数据集。

一个非常流行的公共数据源是 Hugging Face Datasets,你可以用它来微调模型。

假设你从Hugging Face获得一个预训练模型,并在一个启用了GPU的环境中(例如GitHub Codespaces或启用了GPU的Amazon SageMaker环境)使用它。

然后,你可以获取Hugging Face的数据,基于可用的新数据进行微调,接着创建一个新模型,并将其部署到生产环境或重新上传回Hugging Face。


同样地,Amazon S3 也是一个非常常见的场景,它拥有庞大的公共数据集。

你可以将该数据集拉取到,比如说,一个Jupyter Notebook中,对其进行探索性数据分析。

找出你想要构建的目标,然后基于那个S3数据集创建一个模型。


另一个常见的公共数据集是 Kaggle

有许多公共数据集的例子,它们还附带了许多特征工程组件,因此你可以在此基础上进行构建。

利用这些特征工程解决方案,然后创建你自己的定制模型。


所以有很多这样的例子,以上只是三个有用的公共数据集,你可以利用它们来构建模型,无论是用于生产环境,还是作为爱好者或生产机器学习工程师进行实验,以开发出更进一步的模型。


总结

本节课中,我们一起学习了在机器学习项目中利用公共数据集的重要性。我们介绍了三个主要的公共数据源:Hugging Face Datasets、Amazon S3和Kaggle,并概述了如何在这些平台上获取数据、进行探索性分析、微调模型以及进行特征工程。掌握这些资源的使用,是高效开展数据科学和机器学习工作的关键一步。

087:使用BigQuery查询日志文件 📊

在本节课中,我们将学习如何利用Google Cloud Platform的BigQuery服务来查询和分析日志文件。这是一个强大的功能,允许你使用SQL直接查询日志数据,并进行深入的数据分析和可视化。


上一节我们介绍了云平台的基础日志功能,本节中我们来看看如何将日志数据路由到BigQuery,并利用其强大的查询与分析能力。

首先,你可以直接在日志查看器中执行查询。但有一个更高效的方法:通过日志路由器(Log Router)将日志同步到BigQuery作为目的地。

我已经预先设置好了一个同步任务,其目的地指向名为 bigque_data 的BigQuery数据集。设置完成后,所有相关日志都会被自动发送到BigQuery中。


现在,让我们进入BigQuery控制台进行实际操作。

进入BigQuery后,你可以看到我的项目已经作为一个数据集被包含在内。如果我们进入 test_logs 数据集,就能查看所有已发送到此的各类日志。

例如,如果我想深入查看App Engine的日志并执行查询,我只需点击“在新标签页中查询”,系统会自动填充一个查询编辑器。

假设我想查看健康检查的结果,我可以构建如下查询:

SELECT *
FROM `my-project.test_logs.app_engine_logs`
WHERE log_name LIKE '%health_check%'

执行后,我们就能看到所有健康检查的结果及其丰富的元数据信息。这意味着我可以直接用SQL在BigQuery中查询这些信息。

此外,我还可以为此查询设置定时任务,这非常酷,因为你可以基于此构建监控告警。


BigQuery另一个我非常喜欢的功能是支持数据科学风格的分析。我可以进入Looker Studio,开始探索数据并构建可视化图表。

以下是操作步骤:

  1. 在BigQuery数据集页面,点击“探索数据”并选择“使用Looker Studio探索”。
  2. 在Looker Studio中,我可以将字段(如http_status)拖放到报表区域。
  3. 系统会自动生成图表,我可以在此基础上构建各种仪表板。

这种方式显然非常实用。


另一种稍简单的方法是直接点击“探索数据”,然后选择“在Colab笔记本中打开”。这非常便捷。

在Colab笔记本中,我可以运行相同的查询,获取结果,并进行一些描述性统计。或者,更简单的做法是直接导出到Google Sheets。

在Google Sheets中,我可以看到所有酷炫的信息。我可以点击“图表”来插入一个新图表,然后进行配置。例如,我可以将log_name字段设为筛选条件,将timestamp设为时间序列,从而构建出一个时间趋势分析图。


一旦数据进入BigQuery,你就有多种方式与之交互。整个过程非常直观,便于探索数据。

显然,你还可以查询其他事件和日志。这个过程非常有用,特别是对于有数据科学经验的团队来说,使用BigQuery API会让你感到得心应手。


本节课中我们一起学习了如何配置日志路由器将日志导入BigQuery,并掌握了在BigQuery控制台、Looker Studio、Colab笔记本及Google Sheets等多种工具中查询、分析和可视化日志数据的基本方法。这为进行深入的运维分析和业务洞察提供了强大的数据基础。

088:数据库选型无通用方案 📊

在本节课中,我们将探讨一个核心观点:不存在适用于所有场景的通用数据库解决方案。我们将通过分析亚马逊前CTO Benoerogels提出的经典图表,来理解不同数据库类型的设计初衷及其各自擅长的应用场景。

关系型数据库:强一致性与事务

上一节我们提到了数据库选型的多样性,本节中我们来看看具体有哪些类型。首先从最常见的关系型数据库开始。

关系型数据库的核心设计目标是提供强一致性事务支持。在AWS云服务中,Amazon AuroraRDS是这类数据库的代表。它们适用于需要严格保证数据准确性和完整性的场景,例如银行交易或订单处理系统。

键值/文档数据库:低延迟与可扩展性

理解了关系型数据库的特性后,我们转向另一大类:键值或文档数据库。这类数据库与关系型数据库有显著不同。

Amazon DynamoDB为例,这类数据库的特点是低延迟和基于的查询。它们的设计初衷是为了实现大规模扩展,并通常采用最终一致性模型。这意味着它们非常适合需要快速读写和高可用性的场景,例如用户会话存储或购物车数据。

图数据库:关系分析专家

除了处理结构化数据和键值对,有时我们需要深入分析数据之间的关系。这时,图数据库就派上了用场。

Amazon Neptune是AWS提供的图数据库服务。它专门用于分析复杂的关系网络。例如,在社交媒体中分析用户间的连接,计算诸如中心性等描述性统计指标,从而找出社交网络中有影响力的人物。

内存数据库:极速访问

对于需要极快数据访问速度的应用,内存数据库是理想的选择。

AWS的ElastiCache服务支持Redis和Memcached。这类数据库允许你构建协同过滤推荐引擎,或建立高速缓存层,以便以极快的速度访问数据。

搜索引擎:全文与复杂查询

最后,当应用需要进行复杂的全文搜索或分析时,专门的搜索引擎数据库是必要的。

Amazon Elasticsearch服务(现为OpenSearch)就属于这一类。它能够处理各种复杂的查询模式,为应用程序提供强大的搜索能力。

总结与选型策略

本节课中我们一起学习了多种数据库类型及其核心特性。从强调强一致性的关系型数据库,到追求低延迟可扩展性的键值数据库,再到擅长分析关系的图数据库、提供极速访问的内存数据库以及专精搜索的引擎数据库。

正如图表所示,存在多种不同的数据访问模式可供选择。一个关键的结论是:通常,最佳实践是根据具体问题,选择多种类型的数据库组合来构建你的解决方案。没有一种数据库能解决所有问题,理解每种工具的优势是做出正确技术选型的基础。

Rust编程2-3(数据工程、DevOps):4:课程总结

在本节课中,我们将回顾整个课程的核心内容,总结Rust在数据工程领域的关键特性和所学工具,并对未来的学习与实践提出建议。

我们已完成了Rust数据工程课程的学习。现在,让我们回顾一下整个课程所涵盖的内容。

课程内容分为四个主要部分。以下是各部分的核心要点:

  • 第一部分:数据集合。我们学习了Rust中的基础数据结构,例如向量(Vec<T>)和哈希映射(HashMap<K, V>),并深入理解了其内存安全与线程安全的特性。
  • 第二部分:并发编程。我们探讨了Rust的并发模型,并介绍了像Rayon这样的并发库,它能够帮助我们轻松地编写并行代码。
  • 第三部分:数据工程库与工具。我们接触了用于数据工程的高级库和工具,例如Polars,这是一个用于数据操作和分析的高性能DataFrame库。
  • 第四部分:数据处理系统。我们了解了如何将Rust应用于更大型的数据处理系统,包括与BigQuery等服务的集成,使用Step Functions编排工作流,以及如何利用SQL来构建数据管道。

上一节我们回顾了课程的知识模块,本节中我们来看看需要牢记的Rust语言几个关键特性。

  • 静态类型语言:Rust在编译时进行严格的类型检查,有助于在早期发现错误。
  • 系统编程语言:Rust专注于对硬件资源的底层控制,适合构建高性能、可靠的基础设施。
  • 所有权与借用系统:这是Rust的核心创新,通过编译时规则(如所有权规则借用检查器)来管理内存,从而有效防止空指针异常和数据竞争等内存安全问题。

我们重点探讨了Rust强大的数据处理能力。得益于其高性能、安全性和丰富的生态系统,Rust非常适合用于构建高效、可靠的数据工程应用。

关于课程本身,恭喜你完成了全部学习。你现在已经具备了构建高性能系统的基础。请持续构建项目并不断练习。

最后再次祝贺,你已经完成了Rust数据工程课程。请保持练习,构建出色的项目。我们下次再见。

本节课中我们一起回顾了Rust数据工程课程的核心内容,包括数据集合、并发编程、相关库与工具以及大型数据处理系统。我们总结了Rust作为静态类型、系统编程语言的关键特性,特别是其所有权系统在保障内存安全方面的价值。恭喜你完成课程,下一步的关键在于持续实践与应用。

090:认识你的课程讲师阿尔弗雷多·德萨 👨‍🏫

在本节课中,我们将认识本课程的讲师阿尔弗雷多·德萨,并了解他结合Rust语言进行DevOps和数据工程教学的背景与理念。


从事DevOps是我多年前担任助理管理员时就非常喜欢的事情。后来我转向了日常的常规软件工程工作。将软件工程与运维流程、系统管理相结合,这种混合模式深深吸引了我。

我的名字是阿尔弗雷多,我拥有超过10年的软件工程经验,以及大约四年的系统与DevOps工程经验。

我曾担任发布经理,从零开始构建过CI/CD系统,也负责过被众多不同个人和公司使用的大型项目的发布管理,致力于支持系统的弹性并实现一切自动化。

在我工作过的许多不同环境中,从非常小的初创公司到大型公司的大型工程团队,我的目标始终是将我这些年所学到的知识融入这门DevOps课程中。

那么,这里新增的变量是什么?是Rust。在本课程中,你将会看到一些Python的例子,但我们的重点将主要放在Rust上。

为什么选择Rust?它是一个非常有吸引力的选择。它速度非常快,可以编译成单个二进制文件,这使得分发和共享变得非常容易,所有依赖都包含在内。一旦你开始部署Rust应用程序并在多种场景下使用Rust,你会发现,如果你熟悉Python,它的语法与Python并没有太大不同,尤其是在你使用Python类型提示的情况下,你已经几乎掌握了Rust的许多外观和感觉。

归根结底,做一件不仅快速而且消耗资源更少的高性能事情,你会发现可以将许多系统工程流程和实践应用于Rust,并将其融入DevOps。这就是我感到兴奋的原因,希望所有这些课程结合起来,能让你很好地了解如何将DevOps的最佳实践与Rust相结合。


本节课中,我们一起认识了讲师阿尔弗雷多·德萨,了解了他丰富的DevOps与系统工程背景,以及他选择将高性能的Rust语言作为本课程核心工具的原因。在接下来的课程中,我们将基于这些理念,深入探索如何利用Rust来实践现代DevOps与数据工程。

091:课程概述 🚀

在本课程中,我们将学习如何运用Rust编程语言来增强和实现DevOps的核心概念。DevOps领域非常广泛,要涵盖所有内容需要付出相当大的努力。

课程焦点与核心理念

因此,在本课程中,我们将聚焦于使用Rust。与许多其他方法不同,我认为Rust在此处拥有独特的机会,能够真正增强整个DevOps理念。

我们将从DevOps的基础支柱——自动化开始,然后在此基础上扩展到其他部分,例如与系统编程集成、CI/CD(持续集成/持续交付)以及测试应用程序。

学习方法与实践应用

现在以及在整个课程中,我们将看到这些概念被应用。重要的是,我们学习概念,然后使用新技术来应用它们。在本例中,我们将大量使用Rust。

同时,观察所有这些概念和组件最终如何协同运作也很有趣。

最终目标:综合项目实践

最后,我们将把所有内容整合在一起。我们将看到一个从头到尾的项目,并学习如何应用我们在整个课程中学到的概念,为我们正在开发的软件创建一个健壮的管道。

我们将尝试检查不同的场景:当我们需要自动化时会发生什么,当我们需要测试和验证时会发生什么。所有这些概念都将在整个过程中得到解释,然后我们将以非常实践和动手的方式应用它们。


本节课中,我们一起学习了本课程的核心目标:即通过Rust语言来实践和深化DevOps的自动化、集成与测试等关键概念,并最终通过一个完整的实战项目来综合运用所学知识。

092:引言

在本节课中,我们将要学习DevOps的核心概念,特别是其基础原则之一:自动化。我们将探讨自动化的重要性,以及它如何与其他支柱(如责任与可见性)共同作用,帮助工程师构建更可靠、高效且可快速部署的系统。


DevOps不仅仅是一个时髦的词汇,它拥有一些核心原则。

其中,最基础的原则之一就是自动化。无论是我在大学教学、进行客座讲座,还是在我日常担任软件工程师的工作中,一切都围绕着自动化展开。

那么,为什么需要自动化呢?因为它是成为一名可靠工程师的基础。通过建立自动化系统,我能够工作得更快、更高效、减少错误,并且在最终将这些系统部署到生产环境时拥有高度的信心。


上一节我们介绍了自动化的核心地位,本节中我们来看看其他支柱,例如责任可见性

以下是几个关键问题及其重要性:

  • 为什么拉取请求(Pull Request)是有用的? 它提供了代码合并前的审查视图。
  • 为什么在拉取请求或代码合并视图中需要自动化? 自动化可以确保流程的一致性和质量。
  • 为什么需要识别并自动化手动操作? 自动化手动任务是提升效率和可靠性的关键。

我认为答案是:当你将这些实践落实到位后,你会发现自己的信心在增长。你部署的速度、从错误中回滚的速度都会更快,并且能为你的基础设施建立一个可靠的系统。拥有这样的系统是一个理想的场景。


在整个过程中,你会意识到这是一个过程,而不是像拨动开关那样,突然之间一切都变得完美。我们拥有登录、监控、告警、自动化、CI/CD系统等,一切都很好。

所以,让我们从自动化这个基础开始,深入了解这些原则。

093:核心DevOps原则 🚀

在本节课中,我们将要学习DevOps的核心原则。我们将探讨自动化、持续集成与持续交付(CI/CD)、测试以及度量等关键概念,并理解它们如何共同构成现代软件开发和运维的基础。

概述

DevOps包含许多重要的实践,如构建、测试、发现、持续反馈、协作与沟通。然而,所有这些实践都依赖于一个最根本的基石:自动化。没有自动化,其他实践将难以高效、持续地开展。

自动化:一切的基石 ⚙️

上一节我们介绍了DevOps的广泛原则,本节中我们来看看其最核心的基础:自动化。

自动化是指将任何手动执行的任务分解为更小的步骤,并通过脚本或代码来自动执行这些步骤的过程。例如,手动验证一段代码可以转变为编写一个自动执行该验证的脚本。

公式/代码示例:

# 手动验证的示例:运行测试套件
# 自动化脚本示例:
#!/bin/bash
echo "开始自动化测试..."
cargo test
if [ $? -eq 0 ]; then
    echo "所有测试通过!"
else
    echo "测试失败!"
    exit 1
fi

自动化本身是好的,它创建了脚本和代码来替代重复的手动工作。这为启用更高级的系统(如CI/CD)提供了可能。

持续集成与持续交付(CI/CD) 🔄

自动化是基础,而CI/CD系统则是建立在自动化之上的关键实践。CI代表持续集成,CD代表持续交付或持续部署。

CI/CD系统(如Jenkins、GitHub Actions)允许我们以持续、自动化的方式集成代码更改、运行测试并部署软件。你编写的自动化脚本可以成为这些平台流水线的一部分。

以下是CI/CD平台可能包含的自动化触发场景:

  • 代码推送时:每当开发者向代码仓库推送新代码时,自动触发构建和测试流程。
  • 定时任务:在特定时间(如每晚)自动运行完整的测试套件或生成报告。
  • 发布生产时:在部署到生产环境前,自动执行一系列预定义的检查和部署脚本。

通过CI/CD,不仅任务本身被自动化,任务的触发时机和执行流程也被自动化管理。

测试与验证 ✅

然而,仅有自动化是不够的。自动化必须辅以充分的测试验证,以确保自动化流程本身以及通过它交付的软件是正确、可靠的。

测试至关重要,因为它回答了这个问题:“我们的自动化工作正常吗?我们如何验证事情进展顺利?”我们需要建立多种类型的测试。

以下是常见的测试类型:

  • 单元测试:验证单个函数或模块的行为。
  • 集成测试:验证多个模块或服务协同工作的情况。
  • 端到端测试:模拟真实用户场景,验证整个应用流程。
  • 性能测试:验证系统在负载下的表现。
  • 监控与告警:在生产环境中持续验证系统健康状态,这本身也是一种测试形式。

度量与改进 📊

测试告诉我们系统是否工作,而度量则告诉我们系统工作得“有多好”。度量使我们能够进行比较、建立基线并识别改进方向。

如果你不进行度量,就无法回答以下问题:我们是变快了还是变慢了?这段代码的性能是否更好?我们是否达成了最初设定的目标(如提升速度、容量或负载能力)?度量提供了数据驱动的洞察,是持续改进的基础。

总结

本节课中我们一起学习了DevOps的核心原则。我们了解到自动化是所有DevOps实践的基石。在此基础上,我们构建了CI/CD系统来实现流程的持续化。为了确保质量,我们必须引入全面的测试。最后,通过度量,我们获得反馈并驱动持续改进。

核心思想是:在DevOps中,任何可以自动化的事情都应该被自动化。对于手动操作,我们应该停下来思考,并尽可能为其构建自动化解决方案。这为你理解后续我们将详细探讨的具体DevOps实践打下了良好的基础。

094:什么是自动化 🤖

在本节课中,我们将要学习自动化的核心概念。我们将通过两个具体的项目实例,来理解自动化如何将繁琐、易错的手动流程转变为高效、可重复的脚本或程序。我们将看到,自动化不仅能节省时间,还能提高工作的准确性和可维护性。


从手动到自动:一个简单的CLI工具示例

上一节我们介绍了自动化的基本概念,本节中我们来看看一个具体的个人项目实例。

我曾在一家公司工作,该公司要求使用基于时间或事件的动态安全令牌进行登录。每次登录都需要通过一个复杂的系统生成令牌,过程非常繁琐。我向经理抱怨后,他建议我:“你为什么不自己动手解决这个问题呢?”

于是,我构建了一个名为 worryri 的简单命令行界面工具。这个工具的核心功能是自动化生成一次性密码,用于OTP验证系统。

以下是该工具实现的关键点:

  • 功能:生成用于OTP引擎的自定义令牌密钥。令牌类型可以是基于时间的TOTP或基于事件的HOTP。
  • 操作:工具会生成一个PIN码或密钥,并自动复制到系统剪贴板。
  • 效果:用户无需再手动操作手机或访问特定网站来获取和输入令牌,只需从终端直接粘贴即可完成登录验证。

这个工具本质上是用OpenSSL等库在后台自动化了一个原本痛苦、耗时且在我看来近乎荒谬的手动流程。通过将几个手动步骤抽象并自动化,我显著提升了身份验证环节的效率。


复杂系统的自动化部署

刚才我们看了一个个人效率工具的简单例子。现在,让我们来看一个更复杂、更专业的自动化案例。

这是一个名为 Se deploy 的部署脚本,用于在多年前部署一个庞大、开源的分布式文件系统项目——SeF。这个工具是我在Red Hat工作时参与开发的。

SeF 作为一个分布式文件系统,需要安装和配置在许多不同的服务器上,过程极其复杂。手动操作不仅步骤繁多,而且极易出错。

让我们通过查看其Python代码(虽然本课程是Rust,但自动化思想是相通的)来理解它做了什么。以下是脚本执行的部分关键步骤:

  • 设置变量:判断并设置是稳定版还是测试版环境。
  • 安装依赖包:例如 ca-certificatesapt-transport-https
  • 配置源和密钥:根据版本设置正确的软件源GPG密钥,并构建正确的下载URL。
  • 处理不同情况:区分开发版本、特定提交版本,并从相应的仓库(如非官方的Shaman仓库)获取资源。

试想一下,如果没有自动化,手动执行这些步骤会发生什么?你可能会打错字、遗漏步骤、记错顺序或放错文件。在一个涉及多台服务器的复杂部署中,手动操作引发问题的可能性会呈指数级增长。

而这个脚本将所有这些步骤结构化顺序化地固定下来,确保了部署过程的可重复性准确性


自动化的核心优势

通过以上两个从简到繁的例子,我们可以总结出自动化的核心优势:

  • 提高速度与效率:自动化能快速执行重复性任务,远快于人工操作。
  • 提升准确性与健壮性:通过脚本固化流程,消除了人为失误(如拼写错误、步骤遗漏或顺序错误)。
  • 确保可重复性:相同的脚本可以在不同时间、不同环境下产生一致的结果。
  • 增强可维护性:当流程需要修改时,你只需更新脚本中的逻辑(例如修改一个 if 条件分支),而无需重新记忆和传授整套复杂的手动步骤。代码本身成为了流程的文档。

在编程中,无论是Python还是Rust,这种通过 ifelse ifelse 等语句来处理不同条件分支的逻辑,正是构建自动化流程的基础。自动化让你能管理这些分支,并确保所有条件都能被正确、高效地处理。


本节课中我们一起学习了自动化的本质:通过构建程序或脚本来替代手动、重复、易错的操作流程。我们看到了两个实例,从解决个人登录痛点的CLI工具,到管理复杂分布式系统部署的脚本,它们都体现了自动化在提升效率、准确性和可维护性方面的巨大价值。掌握自动化思维,是成为一名高效开发者和DevOps工程师的关键一步。

095:日志记录与监控的基础 📊

在本节课中,我们将要学习日志记录与监控的基础知识。它们是大多数应用程序中至关重要的组成部分,能帮助我们理解程序运行状态、调试问题并衡量性能。

日志记录基础 📝

上一节我们介绍了课程概述,本节中我们来看看日志记录的核心概念。日志记录并非Rust语言特有,而是适用于任何编程语言的通用实践。

日志级别

日志通常包含几个不同的级别,每个级别代表不同的重要性权重。以下是常见的日志级别:

  • Debug:最详细的级别,用于开发调试。
  • Info:信息性消息,用于记录常规操作。
  • Warning:警告消息,表示潜在问题。
  • Error:错误消息,表示操作失败。

日志级别遵循一个详细度(verbosity)尺度。Debug级别最详细,包含的信息最多。随着级别升高(如到Error),详细度降低,只记录最关键的信息。例如,如果只启用Error级别的日志,那么只会看到最关键的报错信息,这在代码库中通常使用得较少。

日志组件

日志的格式和内容是可配置的,不同应用程序的处理方式可能不同,但以下是一些基础组件:

  • 时间戳:记录事件发生的具体时间。这对于追踪操作的开始与结束时间、分析延迟(例如,一个操作耗时3秒)以及理解事件序列至关重要。例如,在一个Web服务器中,时间戳能帮助你分析请求的处理时间。

日志实践示例

让我们回到一个示例应用程序,看看具体的实现。在一个名为lib.rs的库文件中,有一个执行命令的函数。这个模式不限于Rust,其他语言也适用。

// 伪代码示例:执行命令并处理错误
fn run_command(cmd_str: &str) -> Result<(), Error> {
    let parts: Vec<&str> = cmd_str.split_whitespace().collect();
    // 这里可以添加Debug日志,记录被解析的命令
    // debug!("Parsed command: {:?}", parts);

    let output = Command::new(parts[0])
                        .args(&parts[1..])
                        .output()?;

    if !output.status.success() {
        // 原始方式:直接打印到终端
        // eprintln!("Command failed: {}", String::from_utf8_lossy(&output.stderr));
        // 更好的方式:使用Error级别日志记录
        // error!("Command execution failed. Error: {}", err_msg);
        return Err(MyError::CommandFailed);
    }
    Ok(())
}

使用println!会将错误直接输出到终端。而使用日志系统,你可以更精细地控制信息的去向和级别。例如,你可以在解析命令后添加一条Debug日志,以便查看所有被解析的命令。这在调试时非常有用。但如果你的应用程序每分钟执行数百条命令,这种高详细度的日志可能并不需要。你可以通过调整日志级别来灵活控制。

日志输出目的地

你可以定义日志消息的输出目的地。常见的选择包括:

  • 文件:通常将日志写入文件。在示例中,配置可能指向一个如block_rest.log的文件。
  • 终端:对于命令行工具,可能也需要将日志输出到控制台。
  • 网络:更复杂的场景下,可以将日志通过网络发送到远程目的地(如集中式日志管理系统)。

选择日志策略而不仅仅是写入本地文件很重要。例如,一个高性能数据库在压力巨大时,如果启用Debug级别日志,可能会在短时间内填满服务器磁盘。因此,需要合理的日志轮转、级别控制和远程存储策略。

监控基础 📈

上一节我们探讨了日志记录,本节中我们来看看监控。监控使我们能够量化并观察系统的性能指标。

监控的重要性

监控的核心在于“如果你无法衡量,就无法改进”。通过监控,你可以比较不同时间段的性能表现,例如判断系统是变好了还是变差了。这是持续优化的基础。

Grafana 仪表板

Grafana是一个开源的可视化仪表板工具。它允许你将各种指标以图表形式展示,从而直观地观察系统随时间变化的性能趋势。

如上图所示,仪表板可以展示多种指标。通过对比不同时间段的数据,你可以评估变化。

监控生态与告警

Grafana通常是ELK/EFK栈(Elasticsearch, Logstash/Filebeat, Kibana)或类似监控生态的一部分。这些软件组件协同工作,实现日志的收集、存储、分析和可视化。

在监控系统中,你可以设置各种动作和告警:

  • 设置阈值触发器:例如,当磁盘使用率超过95%时触发。
  • 配置告警动作:触发后,可以发送邮件、短信(如通知值班人员),甚至集成到如GitHub、工单系统等服务中。
  • 基础设施监控:监控不仅针对应用,也涵盖运行应用的基础设施(如服务器CPU、内存、磁盘IO)。

总结 🎯

本节课中我们一起学习了日志记录与监控的基础知识。我们了解了日志的级别(Debug, Info, Warning, Error)、常见组件(如时间戳)以及输出目的地(文件、终端、网络)。我们还探讨了监控的重要性,以及如何使用像Grafana这样的工具来可视化指标、设置告警,从而衡量和改进系统性能。这些是构建可靠、可观察的应用程序和数据工程管道的关键技能。

096:可见性与可追溯性 👁️‍🗨️

在本节课中,我们将要学习DevOps中一个至关重要的组成部分:可见性与可追溯性。我们将通过一个具体的项目实例,来理解这些概念如何在实践中体现,以及它们为何对团队协作和系统可靠性如此重要。

概述

可见性与可追溯性并非DevOps的显性基础支柱,而是在你开始实施DevOps功能、系统和流程后产生的积极副作用之一。它们旨在帮助团队更高效地运作。我们将通过分析一个真实的Pull Request流程,来展示这些概念的实际应用。

可见性与可追溯性的实践

上一节我们提到了这些概念的重要性,本节中我们来看看它们在一个具体项目中的体现。这里以Anchor的Grip项目为例进行说明。虽然Go并非一个Rust项目,但我想解释的核心概念是相通的。

你可以看到,我打开了一个Pull Request,并发现了关于某个组件的两个Bug。我在此引用了一个相关的问题。

同时,我告知所有人,这个改动还需要合并其他一些内容。这就是大致的思路:我正在尝试合并某些更改。

那么,我们在这里能看到哪些关于透明度和可见性的内容呢?

以下是几个关键点:

  • 高质量的代码审查:我收到了一位非常优秀的软件开发者Alex的审查。他提供了非常有价值的反馈,并开始进行高质量的Pull Request审查。
  • 追踪意见与问题:你可能会问,这种颗粒度的反馈和提问是否必要。答案是肯定的。因为这不仅关乎代码变更,更关乎一个能够追踪意见、澄清未来可能遇到的问题的系统。这一点至关重要,因为有人可以进来讨论某个决定是否合理。

让我们继续深入,因为这个例子非常丰富。尽管它来自2020年,但至今仍然非常准确。

其中一个最后的评论指出,当前XmL Li没有使用漏洞扩展,并提供了更多细节。我感到非常惊讶,回复说“发现得很好,我原以为……让我来修复它。”

这不仅是在生成代码和验证需要更改的内容,更是在我最初理解的基础上进行改进。我继续进行了一些更改。

虽然这里没有完全展示这个项目的所有动态,但在我进行更改的整个过程中,很多事情开始自动执行。我将通过Actions向你展示,Grip项目是体现DevOps中可追溯性与可见性的绝佳范例。

自动化流程与可追溯性

让我们看看这些Actions。我们有一个“发布新版本通知”,看起来他们正准备执行某些操作。

那么,为什么这很有用?又为什么与可追溯性相关呢?

因为事情正在发生,而透明度和可见性就在这里。你可以看到Linux在运行,Mac在运行,其他CI测试也在运行。这一切都可以精确地追溯到某个特定的提交。

如果我们点击其中一个,它将带我查看实际的更改。

为什么这很有用,尤其是在DevOps的背景下?因为如果你在构建流程中,正在进行更改并推送到生产环境,你实施了所有这些步骤,你会希望确切地知道这里发生了什么。你可以看到这里有一些更改和新增内容,变量类型正在发生变化。

如果出现问题,这一点至关重要。如果某些东西运行不正常,你可以回溯到这个特定的Pull Request、提交或变更集,并精确调查当时发生了什么。

总而言之,这不仅是一个关于如何处理问题的范例,也是一个关于如何回溯到针对提议变更的对话和讨论的范例。


总结

本节课中,我们一起学习了DevOps中的可见性与可追溯性。我们通过分析一个包含代码审查、自动化测试和CI/CD流程的完整Pull Request实例,理解了如何通过工具和流程记录每一次变更、每一次讨论和每一次自动化执行的结果。这种透明性确保了当需要调查问题或理解决策原因时,团队拥有完整的上下文和历史记录,这是构建可靠、可协作的现代软件工程实践的核心。

097:实际应用中的DevOps案例 🚀

在本节课中,我们将通过分析两个真实世界的开源项目,来了解DevOps理念在实践中的应用。我们将重点关注它们的持续集成/持续交付(CI/CD)流程,看看大型项目如何通过自动化来确保代码质量和发布可靠性。

上一节我们介绍了DevOps的基本概念,本节中我们来看看这些概念在真实项目中的具体体现。

项目一:Cython项目的GitHub Actions工作流

第一个案例来自Cython项目,具体是它的C-Python SDK。我们不会深入其内部实现,而是重点观察其GitHub Actions部分,即CI/CD流程。

与其他项目类似,你可以看到这里有许多工作流在运行。其中包括CodeQL、C语言检查以及更多工作流。其中一些是测试,另一些则不一定是测试。你可以看到这里正在进行大量的组合测试与验证。

以下是该项目CI/CD流程的一些关键特点:

  • 矩阵测试:项目会测试不同Python版本(如Bodo 3、p27)与不同环境的组合。这确保了代码在各种预期运行环境下的兼容性。
  • 多重检查:工作流中包含了广泛的检查和验证步骤,覆盖了代码安全、风格、构建和测试等多个方面。
  • 发布流程:项目的发布工作流定义了清晰的步骤。例如,它需要特定版本号,然后执行环境设置、代码克隆(使用特定令牌)以及一个名为action-prelude的专用操作来处理发布所需的一切准备工作。

总之,发布一个版本需要经过多个步骤,并且要通过一个包含不同选项、分支和版本类型的“矩阵”进行大量验证。

项目二:CEF项目的Jenkins实例

第二个案例来自CEF项目,它使用Jenkins作为CI/CD平台。Jenkins是全球最流行的CI/CD平台之一。观察这个项目的测试和验证规模,非常具有说服力。

这里不仅有CEF构建,还运行着各种各样的任务。观察左侧的“构建执行器状态”,你可以看到许多节点正在等待构建任务。这些“空闲”节点表明这是一个分布式系统,所有节点都在等待作业分配。

想象一下,如果一个100人的开发团队同时推送代码,这些任务会被逐个路由到可用节点上进行构建和验证。有些节点有名称,有些则没有。目前大部分节点处于空闲状态。

滚动页面,你会看到一些正在运行的任务。有些任务显示为红色,表示可能失败或卡住。通常,这些构建任务平均需要大约一小时完成。你可以看到有些任务通过,有些失败,这与GitHub Actions的反馈类似,让你对项目状态有一个清晰的概览。

真实的DevOps实践正是围绕这些自动化和持续集成/交付平台展开的。在这个案例中是Jenkins,之前是GitHub Actions。这让你了解到,要管理所有这些环节需要具备怎样的能力。

像你在这里看到的文档构建等任务,都是逐步添加到项目中的。当我参与贡献时,这个项目并非一开始就拥有所有这些。我们可能从一两个节点开始,然后增加到十个,再尝试弄清楚如何扩展。现在,它已经拥有大约50到60个节点同时进行构建。

总结与核心要点

本节课中我们一起学习了两个将DevOps理念付诸实践的优秀项目案例。

  • Cython项目:展示了如何利用GitHub Actions矩阵测试来实现复杂的多环境验证和自动化发布流程。
  • CEF项目:展示了如何利用Jenkins分布式构建系统来应对大规模、高并发的开发需求,并通过可视化的节点状态管理构建任务。

这两个项目都是现实世界中实施全面检查与验证的绝佳范例,清晰地展示了DevOps在提升软件交付效率与质量方面的强大作用。

098:引言

概述

在本节课中,我们将探讨应用程序开发中需要考虑的DevOps事项。无论你使用Rust、Python还是其他语言,这些原则都普遍适用。我们将学习如何确保从开发到生产发布的整个流程具有高可靠性,并介绍实现这一目标的关键工具和实践。


DevOps的核心考量

上一节我们介绍了课程的整体方向,本节中我们来看看DevOps的具体考量。

当你负责一个应用程序,并试图将其从开发周期一直推进到生产发布时,你需要建立高度的信心。这意味着需要建立完善的测试机制。在当今时代,随着生成式AI和GitHub Copilot等工具帮助工程师更快地生成代码,这一点变得尤为重要。你需要有某种机制来验证所有代码,包括那些由AI生成并经过人工开发者审查的代码,确保它们确实能正常工作、逻辑正确且可靠。

除了测试,你还需要考虑代码检查和格式化。同时,代码的标记、打包以及版本管理也至关重要。我们将探讨打包如何工作、为什么需要版本控制,并查看一些现实世界的例子。

在课程中,我们会穿插看到一些Python项目的例子,但重点无疑是Rust。我们将介绍一些能帮助你实现上述目标的Rust组件。

我认为,将这些实践应用到具体的应用程序中,是理解这些流程如何在需要被打包并发布到生产环境的实际应用中发挥作用的好方法。


总结

本节课我们一起学习了DevOps在应用程序开发中的核心考量。我们明确了建立从开发到生产全流程信心的必要性,这包括测试代码检查格式化标记打包等关键实践。理解这些概念是构建可靠、可维护软件的基础。在接下来的章节中,我们将深入探讨如何在Rust项目中具体实施这些实践。

099:版本控制与源代码管理 🗂️

在本节课中,我们将学习版本控制与源代码管理的基本概念及其重要性。我们将通过一个具体的Rust项目示例,了解如何追踪代码变更、定位问题以及协作开发。

概述

版本控制与源代码管理不仅是应用程序开发的核心,也是构建自动化流程和部署流水线的关键。它帮助我们精确记录变更发生的时间、内容和责任人,并在必要时回滚到历史状态。

追踪变更与定位问题

上一节我们概述了版本控制的重要性,本节中我们来看看如何利用它来追踪具体的代码变更。

通过查看项目的提交历史,我们可以按时间分组查看所有变更。例如,假设在某个日期之后,应用程序开始出现异常行为。通过浏览该日期前后的提交记录,我们可以定位到可能引入问题的具体变更。

以下是一个提交记录的示例视图:

August 18
    - Working functional test (Commit Hash: a1b2c3d)
August 19
    - Update dependencies (Commit Hash: e4f5g6h)
    - Reorganize imports (Commit Hash: i7j8k9l)

点击某个提交(如“Working functional test”),我们可以查看该次提交引入的详细更改。代码差异通常以颜色标识:

  • 红色:表示被删除的行。
  • 绿色:表示被添加的行。

例如,在Rust Web框架Actix中,路由通常通过 #[get("/path")] 这样的属性来暴露。在一次提交的差异中,我们可能看到类似以下的更改被移除:

// 红色部分(被移除)
#[get("/health")]
async fn health() -> impl Responder {
    HttpResponse::Ok().body("OK")
}

同时,看到添加了新的配置方式:

// 绿色部分(被添加)
App::new()
    .service(web::resource("/health").to(health))

这种变更虽然是为了引入功能测试,但也改变了路由的声明方式。清晰的提交信息对于理解变更意图至关重要。

版本控制的核心优势

在查看了具体的变更示例后,我们来总结一下版本控制系统提供的核心能力。

以下是版本控制提供的主要功能:

  1. 标识变更:精确记录代码中发生了什么变化。
  2. 记录元数据:准确记录变更发生的时间以及责任人。
  3. 回滚能力:当发现问题时,可以轻松地将代码库回退到之前任何一个正常工作的状态。
  4. 历史调查:可以随时查看历史上任意时间点的代码实现,便于理解演进过程或进行审计。

在没有版本控制的情况下,如果只是简单地复制和共享文件,上述所有功能都无法实现。特别是在团队协作中,当多人(甚至是两人)共同修改同一项目时,将无法准确追溯“谁在什么时候修改了什么”。

在DevOps与基础设施中的应用

版本控制的理念不仅适用于应用程序代码,也完全适用于DevOps领域和基础设施配置。

在管理大规模基础设施变更时,同样可以使用Git等工具来管理配置脚本、编排文件(如Dockerfile、Kubernetes YAML)和服务器配置。将基础设施即代码(IaC)纳入版本控制,可以确保所有环境变更都被准确记录和跟踪,从而建立起可审计、可回滚的变更流程,为决策提供依据和灵活性。

总结

本节课中我们一起学习了版本控制与源代码管理。我们了解到它是软件开发乃至DevOps实践的基石,能够帮助我们追踪变更、定位问题、支持团队协作并管理基础设施配置。掌握版本控制工具是每一位开发者和工程师的必备技能。

100:测试与验证 🧪

在本节课中,我们将要学习软件工程中的一个核心实践:测试与验证。我们将通过一个名为“redactor”的Rust示例项目,了解测试的重要性、如何编写测试,以及如何确保代码在投入生产前是可靠的。

概述

测试是确保软件质量的关键环节。无论您是系统管理员转型为软件工程师,还是负责DevOps实践,理解并应用测试都能帮助您验证系统配置和代码变更的正确性。本节将介绍测试的基本概念,并通过实际代码演示如何编写和运行测试。

测试的重要性

当我开始从系统管理员向软件工程师转型时,测试是我必须快速掌握的核心概念之一。软件工程的最佳实践,如测试,不仅适用于常规的软件开发项目,也适用于系统配置管理。通过测试,您可以确保您的变更不会破坏现有功能,从而在部署到生产环境时更有信心。

示例项目:Redactor

我们使用一个名为“redactor”的Rust项目作为示例。项目的具体细节并不重要,重点是理解其中的测试部分。项目代码涉及内存使用、端点处理等多个方面。如果您刚加入一个团队并看到这样的代码,首要问题应该是:如何确保这个我现在负责的项目能正确运行?即使您不是开发者,而是负责实施DevOps实践,也需要与软件工程师协作,确保一切正常工作。

测试类型:功能测试

测试有多种类型,其中之一是功能测试。功能测试意味着您需要运行应用程序,并让测试遍历应用程序的多个不同层次、步骤或部分。通常,应用程序会以某种方式运行,测试则需要通过应用程序的多个组件来执行。

在我们的Rust项目中,功能测试会进行一些断言。让我们来看第一个测试:

#[test]
fn test_index() {
    // 配置并创建用于测试的服务
    let app = test::init_service(App::new().configure(config)).await;
    // 向网站根路径发送GET请求
    let req = test::TestRequest::get().uri("/").to_request();
    // 接收响应并检查状态码
    let resp = test::call_service(&app, req).await;
    assert!(resp.status().is_success());
}

这个测试验证了对网站根路径(由/定义)的GET请求是否成功。它配置并创建了测试服务,发送请求,然后检查响应状态是否正确。这是一个非常基础的测试。

改进测试

虽然简单的测试没问题,但如果测试失败,您可能需要更清晰的上下文来了解失败原因。让我们看看另一个测试,这是一个POST请求测试:

#[test]
fn test_redact_post() {
    // 发送包含JSON体的POST请求
    let req = test::TestRequest::post()
        .uri("/redact")
        .set_json(&json!({"persons": ["Alfr Smith", "John Do"]}))
        .to_request();
    // 检查响应内容是否符合预期
    let resp = test::call_service(&app, req).await;
    let body = test::read_body(resp).await;
    assert_eq!(body, "person1 and person2 went to the store");
}

这个测试的名称test_redact_post可以更具体一些。我们可以使用更具描述性的名称,例如test_redact_post_request_successfully_transforms_text。清晰的测试名有助于在测试失败时快速定位问题。

运行测试

我们可以使用Cargo运行测试。在终端中执行cargo test命令,可以看到测试运行并全部通过。然而,看到测试通过并不总是最有用的;我们更需要知道当测试失败时会发生什么。

为了演示失败情况,我们可以在代码中故意引入一个拼写错误,然后再次运行测试。您会看到测试失败,并输出详细的错误信息,包括断言失败和差异对比。例如:

thread 'test_redact_post' panicked at 'assertion failed: `(left == right)`
  left: `"person1 and person2 went to the store"`,
 right: `"person1 and person2 went to teh store"`',

在CI/CD系统中(我们将在后续课程中介绍),自动化测试运行时,您需要确保所有测试都能通过。如果看到测试失败,您必须理解失败原因,并采取措施修复它。

测试覆盖与验证

测试和验证是自动化CI/CD平台的核心组成部分。每当您想要将代码部署到生产环境时,都需要确保一切正常工作。例如,我们的示例项目有一个健康检查端点(/health),但目前没有对应的测试。如果这个端点失败,您应该添加或建议添加测试,以验证其在生产环境中的关键功能。

总结

本节课中我们一起学习了测试与验证的基本概念及其在软件工程和DevOps中的重要性。我们通过一个Rust项目示例,了解了如何编写和运行功能测试,如何改进测试以提高可读性和可维护性,以及如何在CI/CD流程中利用测试来确保代码质量。记住,无论使用何种编程语言或技术栈,这些测试概念都是普遍适用的。

101:打包与版本管理 📦

在本节课中,我们将要学习Rust项目中至关重要的两个概念:打包版本管理。它们是任何软件项目,尤其是涉及依赖管理的项目,不可或缺的组成部分。

概述:为什么打包与版本管理重要?

打包与版本管理是任何项目的重要部分。当你处理的软件项目不涉及打包或版本管理,并且还缺乏源代码或版本控制时,情况会变得相当复杂。

上一节我们介绍了项目结构的基础,本节中我们来看看如何管理项目的发布与依赖。

深入理解版本号 🔢

让我们以Rust社区包仓库 crates.io 为例。在这里,我们可以找到一个非常流行的包:serde,它是一个用于序列化JSON的库。

serde的页面上,首先我们看到的是版本号,例如 1.0.183。版本号之所以重要,是因为它允许我们像版本控制或源代码控制一样,精确定位一个可以使用的特定发布版本。

这对于定义项目依赖至关重要。版本号通常遵循语义化版本控制规范。这不仅对创建版本很重要,也能让安装工具(如Cargo)理解如何处理依赖。

例如,版本号 1.0.183 包含三个部分:

  • 1:主版本号
  • 0:次版本号
  • 183:修订号

如果你声明依赖为 1,而不关心后两位数字,那么 1.0.1831.2.54 都能满足你的要求。这允许你以灵活的方式定义依赖。

版本控制中的标签与发布 🏷️

每个在crates.io上的包通常都链接到一个代码仓库(如GitHub)。在仓库中,标签 是一个关键概念。

标签允许你为某个时间点的提交打上一个有意义的标记,而不仅仅是一个校验和。例如,在serde的GitHub仓库中,我们可以看到两周前为版本 1.0.183 创建了标签。

发布 通常是基于标签创建的。它不仅仅是标签,还可能包含发布说明、编译好的二进制文件等附加信息。点击 1.0.183 这个发布,我们可以看到该版本包含的具体变更。

这非常重要,因为它让我们能够精确地知道某个功能或修复是在何时发布,从而帮助其他开发者准确选择他们想在项目中使用的版本。

语义化版本控制详解 📚

之前提到了语义化版本控制,它至关重要,因为它为我们提供了一种描述依赖关系的方式,并明确了每个数字的含义。

语义化版本号遵循 主版本号.次版本号.修订号 的格式,每个部分都有特定含义:

以下是每个版本号段变化的含义:

  1. 主版本号:当你做了不兼容的 API 修改时递增。

    • 例如,从 1.0.0 升级到 2.0.0 可能意味着有破坏性变更。如果从 v0v1,也通常表示有向后不兼容的更改。
  2. 次版本号:当你以向后兼容的方式添加了新功能时递增。

    • 例如,2.0.02.1.0 是兼容的。只要主版本号相同,次版本号的增加不会导致现有功能失效。
  3. 修订号:当你做了向后兼容的问题修复时递增。

    • 例如,从 2.0.02.0.1,通常意味着修复了一些错误,但没有添加新功能或破坏现有API。

现在你就能明白为什么这很重要了。你可以开始以一种可理解的方式声明你的依赖。例如,在 Cargo.toml 文件中:

[dependencies]
serde = “1.0” # 允许任何 1.x.x 版本,但不包括 2.0.0

实践意义与总结 ✅

语义化版本号并非总是被严格遵循,开发者有时可能会随意地更新主版本号。但最佳实践是遵循语义化版本控制的规则。

这样做的重要性在于:它允许你回滚到特定版本,查看变更发生的时间,并以一种让你的用户能够理解不同发布之间确切含义的方式来发布软件。

本节课中我们一起学习了:

  • 打包版本管理对软件项目的重要性。
  • 如何通过语义化版本控制来理解版本号(主版本号.次版本号.修订号)的含义。
  • 版本控制在代码仓库中通过标签发布来具体体现。
  • 遵循语义化版本规范如何帮助开发者清晰地管理依赖和发布变更。

掌握这些概念,将使你能够更专业地管理Rust项目,并与广阔的Cargo生态系统进行有效交互。

102:代码检查与格式化 📝

在本节课中,我们将要学习Rust开发中两个至关重要的工具:代码格式化与代码检查。它们能帮助团队保持代码风格一致,并提升代码质量与可维护性。

代码风格一致性的重要性

有时,工程师们对于代码格式和最佳实践会有不同的看法。为了确保代码以一种标准化的方式编写,让所有人都能遵循统一的约定,代码格式化和检查就显得尤为重要。

例如,以下是一个Rust应用程序的代码片段。在Rust中,你可以这样写代码,这完全没有问题:

fn main() {
    let health_check = HealthCheck { status: "OK".to_string() };
    println!("{}", health_check.status);
}

你可能会乐于以这种方式定义你的结构体。但对于其他人来说,他们可能会觉得代码布局很奇怪或很混乱,比如为什么 HealthCheck 结构体定义在这里,而健康状态又是那样定义的。实际上,你甚至可以把它们写在一行,并且风格不一致。关键在于一致性

使用 cargo fmt 进行代码格式化

当出现风格不一致的情况时,我们可以使用Cargo内置的格式化工具。让我们打开终端并运行格式化命令:

cargo fmt

运行后,你会发现所有代码都被标准化了。虽然它可能在某些地方留下一些空隙(这不理想),但你已经能理解其核心思想:将代码格式化为一致的风格。

为什么这很重要?因为你希望代码在组织和团队中看起来是一致的。这种标准化能帮助你理解代码,因为所有内容都位于你期望的位置,并以你期望的格式呈现。

这非常棒,它不仅提升了代码可读性,还为实现自动化奠定了基础。

自动化与代码检查

接下来,我们将看到如何让 cargo fmt 命令自动运行,以确保代码格式一致。如果代码格式不正确,我们甚至可以拒绝变更。例如,如果我故意将代码改回混乱的格式并保存,自动化系统再次运行时就会将其纠正回来。如果检测到差异,系统可以拒绝这次更改,并提示开发者:“你最好让这些更改保持一致。”

除了格式化,我们还有代码检查工具。

使用 cargo clippy 进行代码检查

让我们看看另一个强大的工具。cargo clippy 是Rust中最流行的代码检查工具之一。运行以下命令:

cargo clippy

你会看到它列出了几个问题。然而,如果我们运行 cargo build,编译器会顺利通过,没有任何错误:

cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s

既然能编译通过,那问题出在哪里呢?让我们仔细看看检查器的输出。它可能会提示:“你不需要对这个实现了 Display trait 的类型调用 .to_string()。” 这完全是多余的。

那么,这有什么问题呢?当你添加了不必要的代码时,会使代码变得冗余,更容易出错,增加混淆,并提升维护的复杂度。这不仅适用于Rust,也适用于任何其他编程语言。

事实上,我曾与一位拒绝使用代码检查工具的同事共事,他会说:“我写的代码很优美,不需要工具来告诉我该怎么做。” 但我认为这是一种误解。你完全可以借助这些免费的辅助工具,来使你的代码变得更好、更一致,并移除那些可能导致混淆或降低项目可维护性的部分。

核心工具总结

以下是两个非常有用的工具:

  1. cargo fmt:自动格式化代码,确保风格一致。
  2. cargo clippy:进行静态代码分析,发现潜在的错误、代码坏味道和不必要的复杂度。

理解并运用这两个工具,是保持代码一致性、提升软件项目可维护性的好方法。


本节课中,我们一起学习了Rust中代码格式化与检查的重要性及实践方法。我们了解了如何使用 cargo fmt 来统一代码风格,以及如何使用 cargo clippy 来发现并修复代码中的潜在问题。将这些工具集成到开发流程中,尤其是实现自动化,将极大地提升团队协作效率和代码质量。

103:14_01_06_Rust与Python的对比使用 🦀 vs 🐍

概述

在本节课中,我们将探讨为何在某些场景下应考虑使用Rust替代Python。我们将重点分析几个关键原因,包括云部署成本、性能差异以及项目打包与分发的便利性。

云部署成本对比 💰

上一节我们介绍了课程主题,本节中我们来看看选择编程语言时面临的主要挑战之一:云部署成本。当你计划将应用程序部署到云端时,需要计算其运行成本。

Rust应用在成本上通常比Python应用低一个数量级,这主要源于其对内存和CPU资源的消耗远低于Python。

以微软Azure容器应用为例,其消费计划的定价基于资源消耗。Python应用通常需要至少1GB内存的容器才能有效运行,具体需求取决于实际任务,但这无疑会导致更高的成本。

以下是Azure消费计划的免费授予额度:

  • 虚拟CPU:每月180,000秒。
  • 内存:每月360,000 GB-秒。

相比之下,一个Rust Web API应用可以仅使用0.25个CPU核心100 MB内存成功部署。这意味着你可以将免费的CPU资源额度提升四倍,因为每秒消耗的不是一整个虚拟CPU。内存消耗也显著降低。通过简单的成本计算,可以看出使用Rust在经济上的巨大优势。

性能与资源效率 ⚡

除了成本优势,Rust还能带来纯粹的性能提升。无论是在云提供商处轻松运行和发布项目,还是在本地部署,Rust都能提供更强的处理能力、更低的RAM与内存消耗以及卓越的性能表现。

项目打包与分发 📦

现在,让我们将目光转向项目打包与分发。这是Python生态中一个普遍存在的问题。当你想要分发Python项目时,如果涉及依赖管理,事情会变得复杂。

尽管Python打包领域近年来出现了一些积极变化,但仍存在若干棘手且难以理解的挑战。首先,Python安装后,你首先需要确保拥有pip(Python包安装器)。它虽随Python安装,但常会提示你升级到新版本。

安装依赖有多种方式。在Python官方网站的打包指南中,他们提供了几种方法,但并没有一个统一的、强制的标准做法。

以下是Python中定义依赖的几种常见方式:

  • pyproject.toml文件:目前官方推荐的方式之一。
  • requirements.txt文件:传统方式。
  • setup.py文件:由setuptools支持。

即使使用pyproject.toml,你仍然需要像pip这样的包安装器,或者需要单独安装build这类工具(通过pip安装)。此外,你还需要使用虚拟环境来隔离地安装包。

配置好元数据(如README和许可证)后,你需要构建项目,这通常又需要安装build工具。准备发布项目时,可能还需要安装另一个工具twine。仅打包环节,就可能涉及四到五种不同的工具,非常复杂且缺乏统一标准。

Rust的解决方案:Cargo 🛠️

Rust的一个主要优势在于其使用了cargocargo是Rust的包管理器,它集成了安装、构建、测试、代码格式化、代码检查(lint)、上传和发布等所有功能,是你需要的单一工具

cargo使用Cargo.toml文件来管理项目配置和依赖,其结构与pyproject.toml类似。这种简单且高度统一的方式,使你无需面对Python中多种工具并存和碎片化带来的复杂性。

此外,当你使用cargo构建一个二进制文件时,它会确保将所有内容打包进一个单一的可执行文件中。这个文件可以在具有相同CPU架构的系统上直接运行。因此,一旦构建完成,向不同系统分发应用将变得非常容易。

这带来了巨大的好处:它让你能轻松分发工具,并通过一个包管理器以直接明了的方式处理构建、安装依赖和发布包等所有事宜。

总结

本节课中,我们一起学习了Rust相较于Python的几个关键优势。我们分析了Rust在云部署成本上的显著节约,得益于其更低的内存和CPU消耗。我们探讨了Rust带来的性能与资源效率提升。最后,我们深入比较了项目打包与分发的体验,揭示了Python生态的复杂性,并展示了Rust通过cargo工具链提供的统一、简单的解决方案。这些因素使得Rust在构建高效、低成本且易于分发的系统时成为一个强有力的选择。

104:容器化与Rust应用部署

在本节课中,我们将学习容器化技术及其核心概念,并探讨如何将这些概念应用于Rust应用程序的部署。我们将了解容器相较于传统服务器和虚拟机的优势,以及Rust在构建轻量级容器方面的独特潜力。

理解容器化、容器、运行时,以及围绕Docker和容器镜像的一切相关知识至关重要。所有云服务提供商都为容器提供了极大的灵活性,这有充分的理由:与常规服务器相比,容器是一种轻量级的方式,它们启动快速,更容易扩展,与虚拟机相比也更具优势。当然,虚拟机在生产环境中,针对不同类型的服务和工作负载,仍有其用武之地。

然而,容器至关重要。事实上,我认为它是应用开发运维原则所需的基础知识的核心部分之一。接下来,我们将了解在推进容器化过程中需要理解的一些核心概念,并看看这些概念如何普遍应用于应用程序,特别是如何应用于Rust。我们将探讨如何整合一个Rust应用程序。

在整合过程中需要考虑哪些因素?现在,你将开始看到Rust与其他一些语言之间的核心差异。你将看到创建极轻量级Rust容器的可能性。

这种容器的总存储空间可以小于100兆字节,事实上甚至可以小于50兆字节。你将能够部署这个容器,并将其实际应用到云端。

这带来的直接好处是成本的节约。它意味着更快的部署、更简易的开发、更高效的协作,并且在你来回推送和拉取这些镜像时,一切都会更快。

一切都会更便宜。你为什么会不想使用这样的技术呢?我们将了解其工作原理。同时,我们也会涵盖一些容器化的通用概念,这些概念在当今我们从事的几乎所有技术工作中都至关重要。


总结

本节课中,我们一起学习了容器化技术的基础及其重要性。我们比较了容器与虚拟机、传统服务器的优劣,并重点探讨了Rust在构建超轻量级容器镜像方面的显著优势,这能带来成本节约和效率提升。理解这些概念是现代化应用部署和践行DevOps原则的基石。

105:探索容器化概念 🐳

在本节课中,我们将要学习容器化的核心概念,特别是围绕Docker技术展开。我们将了解什么是容器、它们如何工作,以及如何使用Docker命令和运行时引擎来构建和运行容器。


主机服务器与容器化基础

首先,我们从主机服务器开始。当我们谈论容器化时,通常指的是一个服务器或主机。这实际上可以是您自己的机器,例如您的笔记本电脑或家用电脑,您可以在上面安装Docker运行时(一种容器运行时)。这允许您在主机上运行所有镜像和容器。

这是一种虚拟化形式。您可能更熟悉虚拟机,即在一个服务器内模拟多个服务器。Docker和容器运行时允许您在主机服务器内实现这种机器的虚拟化。

因此,所有这些都代表一个镜像,即容器镜像,它包含多个不同的层面。


容器镜像与分层结构

当我们观察容器镜像是什么时,它本质上是由多个层组成的。每一层都对应文件系统的一个特定部分。

最基础的部分是操作系统。每当有新的内容添加到镜像中时,就会创建新的层。例如,如果您从一个基础操作系统镜像开始,然后安装了一些软件包,这些安装过程会创建一个新层。如果您再添加一个应用程序,又会创建另一个新层。我们稍后会看到这些层是如何相互关联的。

这种分层结构带来了几个好处。例如,每一层都有一个校验和(一种特定的校验和),这允许您独立地拉取每一层。这一点很重要,因为当您拉取或构建镜像时,如果主机上已经存在某些层(例如代表操作系统的层),您就不需要重新下载它们。您只需要下载特定的新层,比如应用程序和额外的安装过程。通过分层,整个过程变得更加高效。

这一切都得益于Docker运行时(以及基于Docker技术的其他容器运行时)的支持,它使得这种虚拟化成为可能,帮助您创建一个非常流畅的流程。它在某些方面类似于虚拟机,但在其他方面又有所不同。


容器与虚拟机的简单对比

最简单(尽管不是100%准确)的解释方式是:容器是一种构建虚拟服务器的超快速、超轻量级方法。它依赖于像Docker这样的虚拟化运行时,使得生成这些镜像并像运行真实服务器一样运行它们成为可能。


Dockerfile:构建指令

这些容器的基础是一个名为 Dockerfile 的文件。

Dockerfile 是一组指令,是一个纯文本文件。它通常以 FROM 指令开始,这是第一步。FROM 指令指定了基础层镜像的来源。例如,它可能是一个基于Debian操作系统的镜像,或者其他人提供的、已经包含NGINX Web服务器所需一切内容的镜像。如果该镜像已发布,您就可以在此基础上进行构建。

假设您想运行自己的应用程序,您可以运行一些命令来安装软件包。例如,如果是基于Debian的系统,您可能会使用 apt 包管理器来安装一些软件包。每一个 RUN 命令(以及Dockerfile中的每一条指令)通常都会创建一个新的层。

在Dockerfile的末尾,通常会有一个 CMD 指令。FROMRUNCMD 这些都是指令,它们通常会各自创建一个层。CMD 指令用于指定在容器启动时执行的命令。

当您创建好Dockerfile后,会运行一个名为 docker build 的命令。docker build 的一个关键参数就是我们所描述的Dockerfile。docker build 过程会生成镜像。


镜像构建与拉取过程

docker build 过程会下载所有所需的层。正如我们之前讨论的,所有这些层(对应步骤二)都会被拉取。这些层将共同构建出一个镜像,这就是我们的下一步。

镜像构建完成后,会存储在本地。您可以启动这个镜像,运行容器,让应用程序在您自己的主机上或云端的某个地方运行起来。


发布镜像到注册表

一旦您拥有了一个镜像,下一步就是将其发布到一个注册表。

注册表是一个您可以发布许多不同镜像的地方。将镜像发布到注册表后,任何人都可以通过 docker pull 命令来拉取您的镜像。这非常重要,因为它允许您发布和分发您的镜像。

这不仅是为了让别人能拉取您的镜像,还因为我们在Dockerfile中看到的 FROM 指令。FROM 指令允许您基于这些已发布的基础镜像来构建自己的镜像。

最后,通过发布镜像,您也允许其他人运行 docker run 来启动您的容器。这使其他人能够快速、轻松地获得所需的一切——所有东西都已安装,几乎都是预配置好的。您也可以在运行时动态配置容器。


总结

本节课中,我们一起学习了容器化及Docker运行时的核心概念。我们探讨了主机服务器、容器镜像的分层结构、Dockerfile的作用以及镜像的构建、拉取和发布流程。本质上,这是一个关于容器化概念的高层次快速概述。

106:容器在DevOps中的优势 🚀

在本节课中,我们将要学习容器化的基础知识,并重点探讨在DevOps实践中使用容器所带来的具体优势。

概述 📋

容器化技术是现代软件开发和运维的核心。它通过将应用程序及其所有依赖项打包到一个标准化的单元中,为开发、测试和部署带来了革命性的变化。本节将详细解析容器在DevOps环境中的关键好处,帮助你理解为何这项技术如此重要。

隔离的环境 🏝️

上一节我们介绍了容器的基础概念,本节中我们来看看容器如何创建隔离的环境。隔离环境意味着你可以为应用程序及其部署定义明确的约束条件。

  • 定义约束:你可以精确指定应用程序运行所需的一切,包括代码、依赖库和配置。
  • 可靠复现:通过依赖像我们之前见过的Dockerfile这样的配置文件,你可以可靠地在任何地方复现相同的环境。这些配置文件可以纳入版本控制,从而实现跨环境的一致性部署。
  • 平台无关性:无论底层操作系统或硬件如何,你的容器行为基本一致,这提供了出色的可移植性。其核心是容器共享主机操作系统的内核,但通过命名空间和控制组(cgroups)等技术实现进程、网络、文件系统等资源的隔离。

一致的开发环境 💻

解决了环境隔离问题后,我们来看看容器如何统一开发环境。容器可以定义项目代码、依赖项和配置,从而为整个团队创建标准化的开发平台。

以下是容器解决“在我机器上能运行”这一经典问题的方式:

  1. 消除环境差异:每位开发者的本地机器配置可能各不相同。容器通过提供完全相同的运行时环境,从根本上移除了因平台细微差别导致的问题。
  2. 标准化开发流程:无论是在团队内部还是跨团队,容器化都能创建统一的开发方式,确保所有人都在相同的平台上工作。
  3. 简化问题复现:当需要复现Bug时,拥有一个标准化的环境至关重要。试想在不同机器、不同架构上复现问题会非常困难,而容器使这一过程变得简单。

简化的部署流程 ⚙️

拥有了一致的开发环境,部署到生产环境的过程也随之简化。通过将应用程序及其所有依赖捆绑在容器内,部署变得直接而高效。

  • 开箱即用:容器包含了运行所需的一切,无需在目标服务器上再次安装依赖或确保特定库的存在。
  • 减少配置:这大大减少了部署时的配置工作和新成员上手(onboarding)的复杂度。新工程师可以快速获得所需的一切环境。
  • 快速启动:与需要启动完整操作系统的虚拟机或传统服务器相比,容器可以极快地启动服务,因为它直接利用主机内核。

资源效率与可扩展性 📈

容器不仅简化了流程,在资源利用上也更具优势。容器共享主机操作系统的内核,与虚拟机相比开销要小得多。

  • 更低开销:虚拟机通常体积庞大,而容器虽然也可以很大,但相比之下通常更轻量。
  • 节省成本:如果你部署到云端,更高效的资源利用意味着更低的云基础设施成本。
  • 易于水平扩展:在云环境中,横向扩展(即启动更多容器实例)变得非常简单和快速。

可移植性与灵活性 🔄

我们之前简要提到了可移植性,现在深入探讨一下。容器几乎可以在任何平台上运行。

  • 跨平台支持:目前几乎所有云服务提供商都支持运行容器,这为你提供了极大的灵活性。
  • 避免供应商锁定:如果你想更换云提供商,可以相对轻松地迁移你的容器。这有助于避免被单一云平台“锁定”,尽管有时利用某个平台提供的全套高效工具本身也是一种优势。

促进协作 🤝

最后,容器技术极大地促进了团队内外的协作。因为你不仅可以共享构建好的容器镜像,还可以共享构建它们的配置(如Dockerfile)。

  • 共享配置:通过将所有这些配置管理在版本控制系统中,不仅可以改善沟通,还能加强协作。
  • 透明与改进:团队成员可以清楚地查看容器是如何构建的,并在构建新版本之前提出建议或进行小的改进。

总结 🎯

本节课中我们一起学习了容器在DevOps中的核心优势。我们了解到,容器通过提供隔离且可复现的环境,解决了“在我机器上能运行”的难题,实现了开发环境的一致性。它简化了部署流程,降低了配置复杂度。在资源利用上,容器比虚拟机更高效,利于节省成本并实现快速的水平扩展。其卓越的可移植性提供了部署灵活性,有助于避免供应商锁定。最终,通过标准化和共享配置,容器技术有力地促进了团队协作。掌握这些优势,将帮助你更好地利用容器化技术来优化开发和运维工作流。

107:什么是容器注册中心 📦

在本节课中,我们将要学习容器注册中心的概念。我们将了解它是什么、为什么重要,以及如何使用它来存储和分发容器镜像。

容器注册中心是一个用于存放容器镜像的地方。它类似于一个代码仓库,但专门用于存储和推送容器镜像。例如,GitHub不仅提供代码仓库服务,也提供容器注册中心功能。在GitHub上,你可以看到“Packages”选项,点击进入后,可以管理你发布的容器镜像包。每个包可以包含多个不同标签的镜像版本,并附带说明文档和下载统计。

以下是拉取镜像的基本命令格式:

docker pull <registry-domain>/<account>/<image-name>:<tag>

例如,从GitHub容器注册中心拉取镜像的命令可能类似于:

docker pull ghcr.io/your-account/your-image:latest

在这个命令中,docker是执行程序,pull是拉取操作,ghcr.io是GitHub容器注册中心的域名,后面依次是你的账户名、镜像名和标签。

上一节我们介绍了容器注册中心的基本概念,本节中我们来看看一个具体的拉取操作示例。

当你执行拉取命令后,Docker会开始下载镜像的各个层。镜像由多个只读层组成,这种分层结构使得下载和存储更高效。下载完成后,你就可以在本地运行这个容器,或者以其为基础进行构建和开发。

现在,让我们看看最流行和常见的容器注册中心——Docker Hub。

Docker Hub是Docker公司提供的默认公共注册中心。当你使用Docker时,如果不指定注册中心域名,默认就会从Docker Hub拉取镜像。

以下是访问Docker Hub上镜像的一个例子:

  1. 访问 hub.docker.com
  2. 你可以浏览或搜索公共镜像,例如流行的Web服务器Nginx。
  3. 在Nginx的官方页面,你会看到拉取命令是 docker pull nginx。这个命令没有包含 docker.io 域名,因为它使用了默认的Docker Hub注册中心。

实际上,完整的镜像地址是 docker.io/library/nginx:latest,其中 docker.io 是域名,library 是官方组织的命名空间,nginx 是镜像名,latest 是标签。Docker工具为我们简化了这个过程。

回到Docker Hub,这个注册中心包含了海量的容器镜像,从官方基础镜像到个人开发者发布的应用应有尽有。任何人都可以创建账户,并将自己构建的镜像推送到这里,供他人拉取和使用。这是一种非常高效的分发软件的方式。

本节课中我们一起学习了容器注册中心。我们了解到它是一个集中存储和管理容器镜像的服务,类似于代码仓库。我们探讨了从GitHub Container Registry和Docker Hub拉取镜像的具体步骤和命令格式,并理解了镜像标签和分层结构的实际意义。掌握容器注册中心的使用,是进行现代应用开发、部署和分发的重要基础。

108:使用Rust构建无发行版容器 🐳

在本节课中,我们将学习如何使用Rust构建一个极小的、无发行版(Distroless)的Docker容器。我们将通过一个真实的Rust项目(一个基于Actix-web的API)来演示,如何将构建好的Rust二进制文件放入一个极简的基础镜像中,从而将容器镜像的大小从数百兆字节缩减到仅约36MB。

项目概述

我们有一个Rust项目,它实现了一个基于令牌(token)的API。这个API使用预训练的模型进行文本分词。项目的核心是一个使用Actix-web框架构建的Web服务器,它绑定在0.0.0.0:8000端口上。

以下是API路由处理函数的一个简化示例,它接受一个POST请求:

#[post("/tokens/{pretrained_model}")]
async fn tokenize(/* ... 参数 ... */) -> impl Responder {
    // ... 分词逻辑 ...
}

这个函数是泛型的,可以处理不同的预训练模型(例如 bert-base-uncased)。

构建Docker镜像

接下来,我们进入最关键的部分:编写Dockerfile。我们将使用多阶段构建来创建一个极小的最终镜像。

第一阶段:构建阶段

首先,我们使用一个包含完整Rust工具链的镜像作为构建器(builder)。

FROM rust:1.67-buster AS builder
WORKDIR /usr/src/app
COPY . .
RUN cargo build --release

这个阶段与我们常规的Rust Docker构建流程一致。我们复制源代码,并使用cargo build --release命令编译出优化的二进制文件。

第二阶段:运行阶段

上一节我们完成了代码的编译,本节中我们来看看如何创建一个极简的运行环境。这里就是“无发行版”技巧发挥作用的地方。

FROM gcr.io/distroless/cc-debian10
COPY --from=builder /usr/src/app/target/release/rust-tokens-api /usr/local/bin/rust-tokens-api
CMD ["rust-tokens-api"]

我们做了以下关键操作:

  1. 我们使用了Google容器注册表提供的 gcr.io/distroless/cc-debian10 作为基础镜像。这个镜像只包含运行C/C++/Rust等编译型语言程序所必需的最少库文件,没有shell、包管理器或其他工具。
  2. 我们从第一阶段的builder镜像中,将编译好的二进制文件 rust-tokens-api 复制到最终镜像的 /usr/local/bin/ 目录下。
  3. 我们将容器的启动命令设置为直接运行这个二进制文件。

Rust的一个强大特性是它可以生成静态链接或仅依赖少量系统库的独立二进制文件,这使得将其放入distroless这类极简镜像成为可能。

构建与运行

现在,让我们在终端中构建这个镜像。

docker build -t rust-local-distroless .

构建过程可能需要几分钟时间,因为它需要下载Rust工具链和基础镜像。

构建完成后,我们可以查看生成的镜像列表:

docker images | grep rust-local-distroless

你会看到镜像大小大约只有36MB,这与包含完整操作系统的传统镜像(通常几百MB甚至上GB)形成了鲜明对比。

接下来,我们运行这个容器,并将本地的8000端口映射到容器的8000端口:

docker run -p 8000:8000 rust-local-distroless

容器会快速启动,我们的Rust API服务已经开始运行。

测试API

为了验证服务是否正常工作,我们使用curl命令向API发送一个POST请求。

以下是请求示例,我们使用bert-base-uncased模型对文本“Hello, world!”进行分词:

curl -X POST http://localhost:8000/tokens/bert-base-uncased \
  -H "Content-Type: application/json" \
  -d '{"text":"Hello, world!"}'

服务器会返回分词后的结果。通过切换请求路径中的模型名称(如bert-base-cased),我们可以测试不同的分词器。

优势总结

本节课中我们一起学习了如何使用Rust和Distroless镜像构建超小型容器。回顾一下,我们主要完成了以下工作:

  1. 编写了一个标准的多阶段Dockerfile。
  2. 在构建阶段使用完整的Rust镜像编译应用程序。
  3. 在运行阶段使用极简的distroless镜像,仅包含运行二进制文件所必需的库。
  4. 最终得到了一个约36MB的、安全且高效的容器镜像。

这种方法带来了显著的优势:

  • 极小的镜像尺寸:加快镜像上传、下载和部署速度。
  • 更高的安全性:由于镜像中不包含Shell等多余工具,攻击面大大减少。
  • 更快的实验迭代:小镜像意味着在开发和测试环境中能更快地启动和停止容器。

只需对Dockerfile进行一些微小的调整,你就可以将自己的Rust应用程序打包成如此轻量级的容器,从而享受更快的部署、更轻松的发布流程。

109:容器的云扩展与弹性伸缩 🚀

在本节课中,我们将要学习云服务中的一个核心概念:扩展与弹性。我们将以在Azure平台上部署容器应用为例,重点探讨如何通过设置规则来实现服务的自动水平扩展,从而高效应对流量变化并优化成本。

概述

扩展与弹性是云服务的核心组成部分。云服务通过依赖特定的约束和规则来解决扩展问题,允许您的容器或服务水平地增长和扩展。“水平”意味着它将拥有更多的容器实例或资源来处理涌入的流量。本节将快速介绍在Azure上部署容器应用,并深入讲解容器化应用的扩展机制及其工作原理。设置规则至关重要,因为它能让您精确地控制扩展行为。

扩展规则详解

以下是配置扩展时涉及的核心参数和概念。通过它们,您可以精细调整扩展的具体含义。

  • 最小副本数:您希望在任何给定时刻运行的最小容器实例数量。例如,对于一个API服务,您可能希望始终至少有2个实例在运行。
  • 最大副本数:允许扩展到的最大容器实例数量上限,在Azure容器应用中最高可设置为300。
  • 默认扩展范围:默认情况下,扩展范围是0到10,最小值为1。这意味着您可以从0扩展到1个实例,也可以设置默认值为1或2,然后一路扩展到300。
  • 扩展行为:弹性机制允许您根据需求逐步增加容器镜像的数量,同样也可以在不再需要时减少数量。这对于实现高效的计费至关重要——您不希望在没有流量时运行300个容器,但在流量激增时,您肯定希望能在设定的约束范围内按需增长。
  • 基于指标的规则:您可以设置基于HTTP请求、CPU和内存使用率等自定义规则。例如,可以设定规则:“如果我的CPU使用率超过某个阈值,就启动更多容器镜像。”这对于HTTP流量同样适用,平台会提供示例,您可以据此设置扩展规则、最小和最大副本数。

配置弹性伸缩

上一节我们介绍了扩展的核心概念,本节中我们来看看如何在Azure门户中实际配置这些规则。

首先,进入Azure门户并导航到“容器应用”服务。我已经预先创建了一个用于测试和扩展的容器应用。点击进入该应用。

创建应用的具体步骤对于今天描述扩展和副本的主题并非关键。我们需要关注的是左侧菜单中的“扩展和副本”选项。点击进入后,默认视图会显示扩展范围(例如0到10,正如之前提到的默认值)以及当前已配置的扩展规则(初始状态下通常没有规则)。

要添加新的扩展规则,您需要点击“编辑和部署”按钮。点击后,您将看到容器配置详情(例如一个简单的“Hello World”默认应用),它默认使用0.25个CPU核心和1GB内存。

在配置中,您可以启用扩展并“添加扩展规则”。规则类型可以选择“自定义”,之后您会看到所有可配置的选项。

以下是您可以配置的一些规则类型示例:

  • Azure队列:基于队列深度触发扩展。
  • HTTP并发请求:您可以设置规则,例如“如果并发HTTP请求数超过100,则进行扩展,最多扩展到10个实例”。这提供了极大的灵活性。

自动化与DevOps集成

为什么弹性伸缩如此重要?因为它让您掌握DevOps中的一个关键概念:保持灵活性。它允许您、您的团队或组织以一种近乎半自动化的方式,通过定义规则来应对流量增长时的扩展需求。

Azure的一个优点是,如果您滚动到配置页面的顶部,可以查看相关文档。在门户中点击菜单进行配置是一方面,但如果您想实现自动化——请记住,自动化是DevOps的基石之一——您将需要借助CI/CD系统。

通过CI/CD,您可以使用Azure CLI命令来设置这些规则,而不是手动点击菜单。这样,所有配置示例都会转变为可版本控制和自动执行的代码。这为实现真正的基础设施即代码和自动化部署流程铺平了道路。

总结

本节课中,我们一起学习了云环境下的扩展与弹性概念,特别是在Azure容器应用中的实现。我们探讨了如何通过设置最小/最大副本数以及基于CPU、内存或HTTP指标的规则,来定义自动水平扩展的行为。这种机制确保了应用既能高效应对流量高峰,又能在空闲时节约成本。最后,我们提到了通过CI/CD和Azure CLI实现规则配置自动化的重要性,这是将弹性伸缩融入现代DevOps实践的关键一步。

110:日志记录与监控策略

在本节课中,我们将要学习日志记录与监控的核心策略,重点探讨推送与拉取两种模型,以及数据保留策略。我们将分析每种策略的适用场景、优缺点,并解释如何根据不同的系统架构(如容器化环境)和目标来选择最合适的方案。

推送策略与拉取策略

上一节我们介绍了课程的主题,本节中我们来看看两种核心的监控数据收集模型:推送策略和拉取策略。选择哪种策略取决于你的最终目标和部署环境。

以下是两种策略的基本定义:

  • 拉取策略:监控系统主动从各个端点或不同系统中获取信息。监控系统会定期“拉取”数据并将其收集回来。
  • 推送策略:由被监控的系统主动将信息发送给监控系统。

拉取策略的适用场景

拉取策略对于容器化部署环境是一个坚实的选择。接下来我们看看其原因,以及为何它特别适合容器。

在容器动态伸缩、生命周期短暂的环境中,由中心化的监控服务(如Prometheus)主动发现并拉取各个容器的指标,比让每个容器自己去寻找并推送数据到监控端更为高效和可靠。这简化了容器应用的配置,并便于统一管理监控目标。

推送策略的适用场景

现在,让我们转向推送策略。当你拥有一些系统,需要由它们主动将信息推送到监控系统时,这种策略可能更合适。我们同样会探讨为何这是一个好主意,在哪些情况下它能良好工作,以及如何实际部署和实施。

推送策略适用于事件驱动或日志流式处理场景。例如,当某个特定事件(如错误、安全警报)发生时,应用程序可以立即将日志事件推送到中央聚合服务(如ELK栈中的Logstash)。这能确保关键信息的实时性,也适用于监控系统难以直接访问被监控系统网络的情况。

数据保留策略

在了解了数据收集方式后,我们需要考虑数据的长期存储问题,即保留策略。保留策略非常有用,因为你可能不需要五年前数据的精细粒度,但仍希望与历史数据进行对比分析。

例如,你可能希望比较当前的季节性趋势与两年前、三年前或五年前的趋势差异。我们将解释这些差异,并讨论在何种情况下你可能希望降低数据的粒度,转而使用某种平均值聚合,以便在进行监控和对比分析时更有效地处理长期历史数据。

保留策略通常通过时间序列数据库的配置来实现,例如在Prometheus中,你可以设置不同的存储时长和聚合规则。

示例配置概念

# 概念性示例:为不同时间范围的数据设置不同的保留和聚合规则
retention_policies:
  - duration: 30d   # 原始高精度数据保留30天
    resolution: 1m  # 采样间隔1分钟
  - duration: 1y    # 聚合数据保留1年
    resolution: 1h  # 采样间隔1小时,存储每小时的平均值
  - duration: 5y    # 长期聚合数据保留5年
    resolution: 1d  # 采样间隔1天,存储每日的平均值

总结

本节课中我们一起学习了监控系统的关键策略。我们对比了推送拉取两种数据收集模型,理解了拉取策略如何更适合动态的容器环境,而推送策略在实时事件处理中更具优势。最后,我们探讨了数据保留策略的重要性,它帮助我们在存储成本与历史数据分析需求之间取得平衡,通过聚合长期数据来支持有效的趋势对比和季节性分析。

111:日志记录与监控的交叉点 📊

在本节课中,我们将要学习日志记录与监控在DevOps环境中的重要性,以及它们如何协同工作以帮助诊断问题、优化性能和触发自动化操作。我们将通过一个PostgreSQL查询分析器的具体项目和一个Nginx服务器的日志示例,来理解如何从日志中提取有价值的信息,并将其应用于监控场景。


日志记录的重要性 🔍

上一节我们介绍了DevOps的基本原则。本节中我们来看看日志记录为何是实现这些原则的关键一环。日志记录在DevOps环境中至关重要,它允许你应用一些DevOps原则。

我在这里有一个几年前编写的示例项目,它是一个用于PostgreSQL的查询分析器。让我简单介绍一下这个项目的背景。当时我们有一个PostgreSQL分布式集群。PostgreSQL是一个数据库和数据库服务器。我们遇到了一些查询相关的问题。

在启用日志后,因为实际上你可以在PostgreSQL中配置日志记录,这里有一个配置示例。你不仅可以设置日志名称,还可以设置日志类型。你可以看到这实际上非常强大。你可以将日志记录到标准错误输出。

你也可以记录到syslog,这是一个代表系统日志的系统。它是操作系统(如Linux和其他类Unix操作系统)的实际组成部分。还有事件日志。你绝对可以启用这些,并且也可以捕获标准错误输出。这一切都很好,但需要你设置日志记录。

现在,为什么这很重要?为什么这与我们试图用DevOps做的事情相关,并且实际上适用于任何其他应用程序?这是因为错误报告和日志记录使我们能够进行查询分析。这虽然是PostgreSQL特有的,但我们马上会看到它如何适用于其他类型的应用程序。


从日志到分析:一个查询分析器案例 📈

这个非常小的程序可以做到以下几点:

以下是该程序的核心功能列表:

  • 检测最慢的查询:找出执行时间最长的查询。
  • 查找使用情况:找出使用频率最高的查询。想象一下,有人发出一个查询,它是最慢的,但可能有一个查询不是特别慢,却被使用了非常多次。
  • 计算权重:权重是执行时间乘以使用次数。这是一个非常有用的指标,因为它能让你找出哪些查询问题最大。

现在,这一切都来自于查看日志的能力。有了这些信息,你可以进行更深入的调查。

因此,我们清晰地划分了日志记录和监控的职责:日志记录更多用于调查,而监控用于触发某种操作。但在这两者之间也存在一些交叉。


基于日志的监控 🔄

你实际上可以基于日志进行监控。你可以在这里看到,我将跳过这个应用程序的所有具体细节。但归根结底,我们能够捕获所有这些信息,并能够进行一些准确的报告。这基本上都是因为日志记录才成为可能。

接下来,我想向你展示。我将切换到我的终端。在我的终端里,我想运行Nginx,一个运行Nginx的容器。我将执行docker run命令,并将Nginx的80端口映射到主机的8080端口。我将运行Nginx的最新版本。

我将执行这个命令,这需要一点时间来启动。好的,它正在启动。这里有几件事在发生:工作进程的数量,实际上工作进程正在启动,并且它给了我一些关于服务器如何启动的信息。这相当不错,也很有用。

这里有很多调试信息,特别是关于Docker的,就在顶部。但除此之外,除了它正在启动,没有其他信息。

在一个单独的窗口,我将开始向这个端点发送一些请求。当我开始发送请求时,你会开始看到所有这些信息出现在那里。到目前为止,所有这些都是好的请求。

但如果我开始输入一些会导致错误的内容,你会看到情况变得有点棘手。让我们尝试一点一点地分解这些信息。

首先,它识别了IP地址。这是发起请求的源IP地址。因为这是一个容器,这需要一些调整才能使其真正有用。

现在我们这里有日期和时间,你可以看到它是以这种格式分隔的。这都很好。然后我们看到了我执行的GET请求。我通过浏览器发出了一个GET请求,这是HTTP请求的类型,我得到了一个304304意味着它是一种重定向。这些细节并不重要。我正在使用这种类型的浏览器。

这是一个有趣的信息,Web服务器可以捕获它,所以你可以判断出我正在使用苹果电脑,并且发出了那种类型的请求。这非常好。如果我继续向下滚动,找到实际的错误,你可以看到我试图访问/FffF,嗯,它不存在。我使用的主机是……这一点很重要。

这是实际处理该请求的主机,那是IP地址,而这里的是端口。你可以开始看到,这对于理解可能不太顺利的事情非常有用。如果我实际上去清除所有这些,然后用curl发送一个请求。

我将在这里拆分我的终端。我将执行一个curl命令。并且我将提交POST数据。

你会看到,这里顶部的内容……嗯,给了我另一种类型的错误,并说:嘿,这个人不仅不再使用Mozilla风格的浏览器,而且正在使用curl客户端,它来自这里的这个信息。我正在执行一个POST请求,并且得到了一个405。这是来自服务器的响应,现在被记录下来了。


日志与监控的交叉点 ⚙️

这在监控的背景下何时有用?因为我告诉过你这里存在一个交叉点。这里的交叉点在于,你可以建立监控,来查看这些类型的日志。然后你可以设置某种规则,并可以说:好吧,如果我们开始收到多个405404或几个错误,特别是如果它们来自POST请求,那么我们需要采取一些措施。

所以,一旦你开始看到错误,当然最严重的是500错误。如果你仍然看到500错误,也就是内部服务器错误,那么你就知道需要采取一些行动。因此,你可以拥有基于日志记录的监控,即某个应用程序正在查看那里的日志。这就是日志记录。

当然,也有可能更多地查看监控工具。但对于日志记录来说,这是非常基础的,它能让你大致了解为什么你想要启用日志记录,以及在类似情况下你希望拥有何种详细程度的日志,以便你能够理解和弄清楚这些信息来自何处。


总结 📝

本节课中我们一起学习了日志记录与监控在DevOps中的核心作用。我们通过一个PostgreSQL查询分析器项目,看到了如何从数据库日志中提取“执行时间”和“使用次数”等关键指标(权重 = 执行时间 × 使用次数),并将其用于性能分析和问题定位。随后,我们通过一个运行在Docker容器中的Nginx实例,实地观察了Web服务器日志的格式与内容,并探讨了如何基于日志中的状态码(如404405500)来设置监控规则和触发告警。关键在于,日志为深入调查提供了原始数据,而监控则利用这些数据(或更直接的指标)来主动发现问题并触发行动,两者相辅相成,共同构成了可观测性体系的重要支柱。

112:监控工具概览 🛠️

在本节课中,我们将学习几种流行的应用程序监控工具。我们将了解它们的基本架构、核心组件以及各自的特点,帮助你为项目选择合适的监控方案。

概述

监控是确保应用程序健康运行的关键环节。存在多种不同的监控工具和仪表化方案。本节将介绍几种非常流行的工具,以及一个我个人非常喜欢、但目前不那么主流的工具。

ELK/Elastic Stack 📊

首先,我们从ELK栈开始,它也被称为Elastic栈。

ELK代表 ElasticsearchLogstashKibana。通常,Beats 不包含在这个缩写中,但这四个工具的组合为应用程序提供了非常出色的监控能力。

以下是其核心组件:

  • Elasticsearch:本质上是一个数据存储,类似于一个大型数据库(尽管不完全是)。所有的指标和信息都会发送到这里。
  • Kibana:这是仪表盘前端,用于可视化数据。Kibana会与Elasticsearch通信,生成展示系统状态的精美图表。
  • Beats:它会查看文件系统中的日志文件,并将这些日志条目发送给Logstash。
  • Logstash:负责解析日志。你可以设置规则,例如,只处理来自Nginx Web服务器的错误日志。

工作原理

其工作流程如下:

  1. Beats将日志发送给Logstash。
  2. Logstash解析日志,然后发送给Elasticsearch。
  3. Kibana从Elasticsearch读取数据,并生成精美的仪表盘。

这种架构的优势在于,你可以从任何地方摄取日志,只要存在日志文件,就能将其发送过来,没有问题。通过Kibana,你可以很好地整合数据,创建非常强大的仪表盘,从而构建出优秀的监控基础设施,清晰地了解系统运行状况。

Graphite 📈

接下来,我们看看Graphite。它同样由多个协同工作的组件构成,能够生成引人注目的图表,进行良好的监控,并理解数据随时间的变化趋势。

Graphite的核心架构包括:

  • Web前端:接收请求的Web部分。
  • Carbon:一个监听传入指标的守护进程。
  • Whisper:数据存储部分,是一种用于存储时间序列数据的特殊类型数据库库。

其工作方式是:应用程序会发送它们的指标(例如,某个函数处理数据所花费的时间)。这通常与StatsD配对使用。StatsD是一个使用Node.js运行的守护进程,它会将这些指标值发送给Graphite的Carbon组件,以便后续展示。这种方式与代码集成起来非常方便。

Prometheus 🔔

最后是Prometheus。在这里,你会看到监控和触发行动的交叉点。Prometheus是一种不同类型的监控工具,它不仅捕获指标,还能发送警报。

Prometheus的架构特点是:服务器有能力暴露(expose)指标,然后Prometheus服务器通过拉取(pull) 的方式获取这些指标。之后,你可以通过Prometheus Web UI或Grafana进行可视化展示和指标导出。我们稍后会详细讨论拉取和推送模式。

总结

本节课我们一起学习了三种主要的监控工具栈:ELK栈(Elasticsearch, Kibana, Logstash)、Graphite(与StatsD配合)以及Prometheus。这些都是非常可靠的方案。具体选择哪一种,取决于你的部署环境和使用方式。

113:推送与拉取策略

在本节课中,我们将学习应用监控与日志管理的两种核心策略:推送与拉取。理解这两种策略的工作原理和适用场景,对于构建高效、可维护的监控体系至关重要。

概述

监控和日志管理主要有两种实现方式。我们将从 Elastic Stack(又称 ELK Stack)入手,其核心工作机制是推送策略。实际上,信息传输有两种主要方式:一种是推送,另一种是拉取。

Elastic Stack 的推送策略

首先,我们来观察一下 Elastic Stack 的架构是如何工作的。整个过程从数据采集开始,一切始于 Beats

以下是 Elastic Stack 推送策略的工作流程:

  1. 安装 Beats 代理:你需要在你的系统上安装一个名为 Beats 的代理(类似守护进程)。它会持续观察特定文件(例如日志文件)。
  2. 推送至 Logstash:当 Beats 发现新的或有趣的信息时,它会主动将这些数据推送Logstash。这就是推送策略,因为源服务器(即运行应用的服务器)负责发起传输,将信息发送到另一个地方(这里是 Logstash)。
  3. 数据处理与转发:Logstash 会根据预定义的规则解析和处理接收到的数据。处理完成后,Logstash 会再次推送数据到 Elasticsearch 进行存储和索引。
  4. 可视化与反馈:最后,Kibana 会从 Elasticsearch 中获取数据,并创建可视化的仪表板,形成一个可以提供出色指标的反馈循环。

这个由 Beats、Logstash、Elasticsearch 和 Kibana 组成的体系被称为推送策略,因为数据流是由数据源主动发起的。

Prometheus 的拉取策略

上一节我们介绍了由数据源主动发起的推送策略。本节中,我们来看看另一种相反的策略:拉取

拉取策略指的是由一个中心服务主动从其监控的组件中获取(拉取)数据。我们之前简要提到过的 Prometheus 就是这种策略的典型代表。

以下是 Prometheus 拉取策略的基本架构:

  1. 部署 Prometheus 服务器:Prometheus 服务器自带时间序列数据库。
  2. 应用暴露指标端点:被监控的应用程序(例如一个 API 服务)需要在其 API 中暴露一个特定的端点(endpoint),用于提供监控指标。
  3. 主动拉取指标:Prometheus 服务器会按照设定的时间间隔,主动去“抓取”或访问该应用程序暴露的指标端点,从而拉取指标数据并存储到自己的时间序列数据库中。

这种策略被称为拉取策略。数据流是由监控中心(Prometheus)主动发起的。

策略对比与选择

我们已经了解了推送和拉取两种策略。那么,在什么情况下一种策略会比另一种更合适呢?

以下是两种策略的典型适用场景:

  • 拉取策略(如 Prometheus)的优势

    • 在处理容器化环境时非常有效。你只需要确保你的容器化应用暴露一个指标端点,Prometheus 就能自动发现并拉取数据,管理起来非常方便。
  • 推送策略(如 Elastic Stack / StatsD)的优势

    • 适用于你可以轻松配置并运行守护进程的服务器或传统环境。如果你能将 Beats 代理等配置直接集成到你的应用部署流程中,那么推送策略是一个很好的选择。
    • 推送策略对接收信息的服务器压力较小,因为它只需要被动接收数据即可。另一个使用推送策略的典型服务是 StatsD,它同样需要在客户端安装 StatsD 守护进程来推送指标。

简而言之,Prometheus 从暴露的指标端点拉取数据,而 Elastic Stack 和 StatsD 则依赖客户端代理推送数据。理解这两种策略的差异,能帮助你更轻松地判断在何种场景下使用哪种策略更为理想。

总结

本节课中,我们一起学习了应用监控的两种核心数据收集策略:推送拉取。我们以 Elastic Stack 为例讲解了推送架构,以 Prometheus 为例讲解了拉取架构,并对比了它们各自的优缺点和适用场景。掌握这些概念,是设计高效监控系统的重要基础。

114:粒度与保留策略 📊

在本节课中,我们将要学习监控和指标收集中的一个核心概念:粒度保留策略。理解这两个概念对于设计高效、可扩展的数据存储系统至关重要,尤其是在处理时间序列数据时。

上一节我们介绍了监控的基本概念,本节中我们来看看如何通过配置数据的粒度和保留策略来优化存储和查询。

概述

当处理指标、日志和监控数据时,特别是捕获指标时,需要考虑保留策略和粒度。我们将以Graphite及其时间序列数据库Carbon为例进行说明。虽然具体配置针对Carbon,但所解释的原理同样适用于其他服务。理解这些策略将帮助你更好地应用它们。

理解配置结构

以下是Carbon配置中关于保留策略的核心部分。配置中定义了秒、分钟、小时、天、周和年等频率和历史记录,你可以指定和配置它们。

[retention]
pattern = .*
retentions = 15s:7d, 1m:21d, 15m:5y

在这个配置中,retentions 定义了数据的保留规则。其语法由冒号分隔的两部分组成:粒度保留时长

解析保留策略语法

保留策略具有特定的语法格式。例如,10s:14D 表示每10秒一个数据点,并保留这种粒度的数据14天。

这意味着,系统将每10秒捕获一个数据点,并以这种精细度存储长达14天。

复杂的多级保留策略

现在,让我们暂时忽略配置中的模式匹配部分,深入探讨更复杂的多级保留策略。保留规则由逗号分隔,可以有多条。

以下是配置中一个包含三条规则的例子:

  1. 15s:7d: 粒度是每15秒,保留7天。
  2. 1m:21d: 粒度是每分钟,保留21天。
  3. 15m:5y: 粒度是每15分钟,保留5年。

这个策略意味着,Graphite和Carbon会对数据进行降采样。系统会计算平均值,以降低数据的粒度。

思考一下:如果你一直以每15秒的粒度捕获数据,并且没有这样的分级策略,会发生什么?你将积累海量数据。你真的需要回溯到五年前,查看每15秒的数据吗?很可能不需要。

因此,当数据超过7天(进入第8天)时,将粒度降低到每分钟是合理的。当数据超过21天时,进一步将粒度降低到每15分钟,并保留长达5年。

为何需要分级策略?

这种策略非常有用,因为它允许你设置数据的“回收”和“修剪”,清除不必要但仍具价值的数据。

随着数据量和系统使用量的增长,这种策略的意义会愈发明显。原始数据量会变得极其庞大,你很可能不需要将每15秒的精细数据保留很多年。

那么,为什么还需要保留历史数据呢?目的是为了进行有效的比较。

例如,你可能想比较本季度与上一季度的表现,或者对比今年与两年前的业务季节。我们是做得更好了还是更差了?请求量是增是减?存储和内存使用量如何?读写操作的比例有何变化?

本质上,你可以比较几乎所有方面。这就是为什么需要实施这些保留策略,而不是在所有时间范围内都保持极高的数据粒度。

总结

本节课中我们一起学习了粒度保留策略的核心概念。

  • 粒度决定了数据采集和存储的时间间隔(如每10秒、每分钟)。
  • 保留策略定义了不同粒度数据的保存时长。
  • 通过分级保留策略(如 15s:7d, 1m:21d, 15m:5y),可以在数据精细度和存储成本之间取得平衡。新数据保持高粒度以便详细分析,旧数据则被降采样为低粒度用于长期趋势对比。

虽然我们以Graphite的Carbon数据库为例,但其他监控和时间序列数据库系统也存在类似的策略配置。理解这些原理将帮助你在不同系统中设计合适的数据管理策略。

115:引言与监控日志工具概览

在本节课中,我们将要学习如何为系统实现监控与日志记录。我们将概览几种核心的工具栈,了解它们如何工作,以及如何将它们集成到你的项目中。通过学习,你将能够为你的应用选择合适的监控与日志解决方案。

🛠️ 探索监控与日志工具

上一节我们介绍了监控与日志的重要性,本节中我们来看看几种你应该了解的具体工具。我们将从ELK技术栈开始,逐步深入到Prometheus体系,并探讨如何创建自定义监控端点,最后了解云服务提供商(如Azure)提供的开箱即用方案。

ELK技术栈详解

首先,我们来了解ELK技术栈。ELK是ElasticsearchLogstashKibana的缩写组合(有时也包括Filebeat)。它的核心价值在于提供了一个强大的平台,用于集中收集、存储、搜索、分析和可视化日志数据。

以下是ELK技术栈中各个组件的作用:

  • Elasticsearch:一个分布式的搜索和分析引擎。它负责存储所有日志数据,并提供强大的全文搜索能力。
  • Logstash:一个服务器端的数据处理管道。它负责从各种来源(如应用日志、系统指标)收集数据,进行转换(如解析、过滤),然后发送到Elasticsearch进行存储。
  • Kibana:一个为Elasticsearch设计的可视化和管理平台。它允许你通过创建仪表板、图表和图形来探索和可视化存储在Elasticsearch中的数据。
  • Filebeat:一个轻量级的日志数据传送器。它安装在服务器上,负责监控指定的日志文件,并将日志行发送到Logstash或Elasticsearch。

Prometheus与Grafana体系

接下来,我们将目光转向Prometheus。Prometheus是一个开源的系统监控和警报工具包,特别适合监控动态的云原生环境,如容器。它的架构基于拉取(pull)模型,并拥有一个多维数据模型。

我们将探讨Prometheus的工作原理及其架构,并了解如何使其与前端可视化工具协同工作。在这个上下文中,最常用的前端工具是Grafana。Grafana能够连接Prometheus作为数据源,并创建出功能强大、美观的监控仪表板。

实现自定义监控端点

在某些情况下,你可能需要实现自己的监控端点来收集特定的、自定义的指标。这在你需要跟踪业务逻辑相关的特定数据,或者现有工具无法满足你的监控需求时非常有用。

我们将看看你可以在何时考虑实现自定义监控,并简要介绍如何着手创建你自己的端点来执行一些定制化的监控任务。

云服务提供商的集成方案

最后,我们将以Azure为例,了解云服务提供商如何提供集成的监控与日志记录解决方案。虽然我们以Azure为例,但其他主要云提供商(如AWS、Google Cloud)也提供类似的功能。

我们将看到,像Azure这样的云服务如何提供你所需的一切日志记录和监控功能。特别地,你可以从正在运行的容器中获取非常精确的信息,例如CPU使用率内存请求量以及错误信息,而无需进行任何额外的配置。这是一个非常有趣且高效的用例。

📝 课程总结

本节课中我们一起学习了构建监控与日志系统的几种主要工具选项。我们从经典的ELK技术栈开始,了解了其各组件的作用。接着,我们探索了基于拉取模型的Prometheus监控体系及其与Grafana的可视化结合。然后,我们讨论了在需要时如何实现自定义监控端点。最后,我们了解了以Azure为代表的云服务提供商所提供的开箱即用的集成监控方案。通过学习这些内容,你现在应该对可用于监控和日志记录的各种选项有了一个全面的概览。

116:安装ELK技术栈 🛠️

在本节课中,我们将学习如何在基于Debian的Linux机器上安装ELK技术栈。ELK是Elasticsearch、Logstash和Kibana的简称,是一个用于日志收集、存储、分析和可视化的强大工具组合。我们将通过一系列步骤,从安装依赖项开始,到配置并启动各个服务,最终验证整个栈的运行情况。


概述

我们将在一台运行Ubuntu 20.04 LTS的Linux机器上,按照官方文档的指导,完成ELK技术栈的安装。整个过程包括设置软件源、安装核心组件、配置系统服务以及进行初步验证。


安装前的准备

首先,我们需要检查当前系统的环境。确认我们使用的是Ubuntu 20.04 LTS版本,这是我们将要使用的系统。

设置密钥和依赖项

接下来,我们开始设置安装所需的GPG密钥和依赖项。

以下是具体步骤:

  1. 获取Elasticsearch的GPG密钥:这是为了验证从Elastic官方仓库下载的软件包的安全性。
  2. 安装apt-transport-https:这个包允许apt包管理器通过HTTPS协议访问软件仓库,是安装过程的一个必要依赖。

我们通过以下命令来安装apt-transport-https

sudo apt-get install apt-transport-https

安装过程需要一些时间,请等待其完成。


添加Elastic软件源

上一节我们安装了必要的依赖,本节中我们来看看如何将Elastic的官方软件源添加到系统的软件源列表中。

我们需要运行一个较长的命令来创建源列表文件。这个命令指定了软件包的来源(即Elastic的官方仓库)。执行后,系统就能识别并从中获取ELK组件。

运行以下命令后,更新几乎是立即完成的。接下来,我们就可以开始安装各个组件了。


安装Elasticsearch

现在,我们开始安装ELK栈的核心——Elasticsearch。它是一个分布式搜索和分析引擎。

执行安装命令:

sudo apt-get install elasticsearch

在安装过程中,你可能会遇到“无法定位软件包”的错误。这通常意味着我们刚刚添加的软件源列表尚未被系统更新。解决方法是运行sudo apt update来更新所有软件源。

更新后,你会看到Elasticsearch的包开始被拉取。然后,我们可以继续执行安装命令。安装过程会拉取所有必要的文件并构建软件包。

安装完成后,有几个重要事项需要注意:

  • 系统会为Elasticsearch的内置超级用户elastic生成一个随机密码。这是一个密钥,不应分享,务必保存在安全的地方(例如密码管理器)。
  • 如果需要加入集群(本教程不涉及),可以按照屏幕提示重置密码。
  • 后续安装Kibana时,可能需要进行注册(enrollment)。

安装Kibana和Logstash

我们已经成功安装了Elasticsearch,接下来安装栈的另外两个核心组件:用于数据可视化的Kibana和用于数据处理的Logstash。

首先安装Kibana,它是ELK栈的前端仪表板:

sudo apt-get install kibana

等待安装完成。

接着安装Logstash,它是一个服务器端的数据处理管道:

sudo apt-get install logstash

同样,等待其安装完成。


安装Filebeat

ELK栈的核心组件已安装完毕。为了收集和发送日志数据,我们还需要一个轻量级的日志数据采集器——Filebeat。

以下是安装Filebeat的命令:

sudo apt-get install filebeat

Filebeat的安装通常比前面几个组件更快。等待其完成即可。


配置并启动系统服务

所有软件包都已安装完成。现在,我们需要将它们配置为系统服务,并确保它们能随系统启动而自动运行。

我们将使用systemd来管理这些服务。首先,确保systemctl命令可用,然后重新加载systemd管理器配置:

sudo systemctl daemon-reload

这个命令确保系统能识别新添加的服务单元文件。

接下来,启用各项服务,使它们在系统启动时自动运行:

  • 启用Elasticsearch服务:sudo systemctl enable elasticsearch.service
  • 启用Filebeat服务:sudo systemctl enable filebeat.service
  • 启用Logstash服务:sudo systemctl enable logstash.service
  • 启用Kibana服务:sudo systemctl enable kibana.service

启用服务后,我们逐个启动它们,并检查运行状态:

  1. 启动Elasticsearch:sudo systemctl start elasticsearch.service。使用ps aux | grep elasticsearch检查其是否在运行。
  2. 启动Kibana:sudo systemctl start kibana.service。同样使用ps命令检查进程。
  3. 启动Logstash:sudo systemctl start logstash.service。检查进程确认运行。
  4. 启动Filebeat:sudo systemctl start filebeat.service。检查进程确认运行。

至此,所有ELK栈服务都已启动并运行。虽然我们还没有进行具体的配置,但基础环境已经就绪。


安装Nginx以生成测试日志

为了测试ELK栈是否能正常处理日志,我们需要一个能产生日志的应用程序。这里我们选择安装Nginx Web服务器。

执行以下命令安装Nginx:

sudo apt-get install nginx-full

安装Nginx后,系统会创建日志目录(如/var/log/nginx/),其中包含access.logerror.log等文件。初始时这些日志文件是空的。

我们可以检查Nginx是否运行:sudo systemctl status nginx。然后,通过向本地发送一个HTTP请求来生成一条访问日志:

curl localhost

执行后,再次查看/var/log/nginx/access.log文件,应该能看到新产生的日志条目。这证明Nginx正在运行并记录日志。


总结

本节课中,我们一起完成了在Ubuntu系统上安装ELK技术栈的全过程。我们从设置软件源和安装依赖开始,逐步安装了Elasticsearch、Kibana、Logstash和Filebeat这四个核心组件。接着,我们使用systemd将这些组件配置为系统服务并确保它们成功启动。最后,我们安装了Nginx作为日志源,并验证了其可以生成日志,为后续的日志收集、处理和可视化测试做好了准备。

117:配置ELK技术栈 🛠️

在本节课中,我们将学习如何配置ELK技术栈(Elasticsearch、Logstash、Kibana)以收集、处理和可视化Nginx的日志。我们将从配置Filebeat开始,逐步设置Logstash管道,并最终在Kibana中查看日志数据。

概述

ELK技术栈是一个强大的日志管理解决方案。本节教程将指导你完成配置Filebeat以收集Nginx日志,使用Logstash进行日志解析和过滤,并最终将数据发送到Elasticsearch,以便在Kibana中进行可视化分析。

配置Filebeat

上一节我们介绍了ELK技术栈的基本组件。本节中,我们来看看如何配置Filebeat来收集Nginx服务器的日志。

首先,我们需要编辑Filebeat的配置文件。该文件通常位于 /etc/filebeat/filebeat.yml。由于需要管理员权限,我们使用 sudo 命令进行编辑。

以下是需要修改的关键配置部分:

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/nginx/*.log
  • type: log:指定输入类型为日志文件。
  • enabled: true:启用此输入配置。
  • paths:指定Nginx日志文件的路径。使用通配符 *.log 可以同时捕获 access.logerror.log

配置完成后,需要重启Filebeat服务以使更改生效。执行以下命令:

sudo systemctl stop filebeat
sudo systemctl start filebeat

重启后,Filebeat将开始监控指定的Nginx日志文件,并将日志事件发送出去。

配置Logstash

现在Filebeat已经开始收集日志,我们需要配置Logstash来处理这些日志数据。Logstash的配置文件通常位于 /etc/logstash/conf.d/ 目录下。

我们将创建一个名为 filebeat-nginx.conf 的新配置文件。这个配置文件需要定义三个主要部分:输入(input)、过滤器(filter)和输出(output)。

以下是配置文件的内容:

input {
  beats {
    port => 5044
  }
}

filter {
  grok {
    match => { "message" => "%{COMBINEDAPACHELOG}" }
  }
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/duke-rs-prog-dtengi-dop/img/d1e6ef1bc025178dadf1dde19af58639_3.png)

output {
  elasticsearch {
    hosts => ["localhost:9200"]
  }
}
  • 输入(Input):配置Logstash通过5044端口接收来自Filebeat的数据。
  • 过滤器(Filter):使用 grok 插件和 COMBINEDAPACHELOG 模式来解析Nginx的访问日志格式,将非结构化的日志文本转换为结构化的字段。
  • 输出(Output):将处理后的数据发送到本地运行的Elasticsearch实例(端口9200)。

保存配置文件后,需要重启Elasticsearch和Kibana服务,以确保整个管道能够正常工作。

sudo systemctl restart elasticsearch
sudo systemctl restart kibana

访问与配置Kibana

Logstash配置完成后,数据将流入Elasticsearch。现在,我们通过Kibana来查看和探索这些日志数据。

首先,确保Kibana服务正在运行。默认情况下,Kibana可能只绑定在本地回环地址(localhost)。如果你希望通过网络访问,需要修改其配置文件 /etc/kibana/kibana.yml,将 server.host 的值从 "localhost" 改为 "0.0.0.0"

server.host: "0.0.0.0"

修改后,重启Kibana服务。

接下来,在浏览器中访问Kibana(通常是 http://<你的服务器IP>:5601)。首次访问时,需要进行初始设置。

Kibana会提示你输入一个注册令牌(enrollment token)以连接到Elasticsearch。这个令牌可以通过在Elasticsearch服务器上运行以下命令来生成:

sudo /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token --scope kibana

将生成的令牌粘贴到Kibana的网页界面中,点击“配置”。

随后,Kibana会要求你输入一个验证码(verification code)。这个验证码可以通过在Kibana服务器上运行以下命令获得:

sudo /usr/share/kibana/bin/kibana-verification-code

输入验证码后,Kibana将完成与Elasticsearch的连接设置。

设置完成后,你可能需要使用默认用户 elastic 登录。如果忘记密码,可以在Elasticsearch服务器上运行密码重置命令:

sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic

命令会生成一个新密码,使用 elastic 用户名和这个新密码即可登录Kibana。

成功登录后,你将进入Kibana的主界面。在这里,你可以:

  • 进入“Discover”页面探索和搜索你的Nginx日志。
  • 使用“Dashboard”创建自定义的数据可视化图表。
  • 在“Stack Management”中管理索引、用户和权限。

总结

本节课中我们一起学习了如何配置完整的ELK技术栈来处理Nginx日志。我们完成了以下关键步骤:

  1. 配置 Filebeat 来收集 /var/log/nginx/ 目录下的日志文件。
  2. 配置 Logstash 管道,使用 grok 过滤器解析日志,并将结果输出到Elasticsearch。
  3. 启动服务并配置 Kibana,通过网页界面实现日志数据的可视化探索。

虽然初始配置需要一些步骤,但搭建好的ELK栈提供了一个功能强大、集成度高的日志监控、分析和告警平台。你可以在此基础上,进一步探索如何将其配置为适用于多服务器或集群环境的生产级部署。

118:在Rust应用中添加Prometheus监控端点

概述

在本节课中,我们将学习如何为一个使用Actix-web框架的Rust HTTP API应用添加Prometheus监控端点。Prometheus是一个流行的监控和告警工具,它通过从应用暴露的特定端点“拉取”指标数据来工作。我们将使用actix-web-prom中间件来简化这一过程。

添加依赖

首先,我们需要在项目的Cargo.toml文件中添加必要的依赖。我们将使用actix-web-prom这个crate,它专为Actix-web框架设计,能够自动处理Prometheus指标的格式化和端点暴露。

以下是需要添加的依赖项:

[dependencies]
actix-web = "4.0"
actix-web-prom = "0.10.0"

导入与初始化

上一节我们介绍了所需的依赖,本节中我们来看看如何在代码中导入并使用actix-web-prom

main.rs文件的顶部,我们需要导入PrometheusMetricsBuilder。这个构建器将帮助我们配置和创建Prometheus指标端点。

use actix_web_prom::PrometheusMetricsBuilder;

配置Prometheus中间件

接下来,我们需要在main函数中配置Prometheus中间件。这个中间件将自动为我们的应用添加一个/metrics端点。

首先,我们使用PrometheusMetricsBuilder来创建一个指标收集器。我们需要为它指定一个名称(通常使用项目名)和指标暴露的端点路径。

let prometheus = PrometheusMetricsBuilder::new("redactor")
    .endpoint("/metrics")
    .build()
    .unwrap();

在这段代码中:

  • "redactor"是项目的名称,它将作为所有指标的前缀。
  • "/metrics"是Prometheus来抓取数据的默认端点路径。
  • build().unwrap()方法构建出最终的中间件实例。

集成到HTTP服务器

现在我们已经创建了Prometheus中间件,需要将它集成到我们的Actix-web HTTP服务器中。由于它是一个中间件,我们需要将它“包裹”在现有的应用实例上。

找到创建HTTP服务器的代码部分(通常是调用HttpServer::new的地方),然后使用.wrap()方法添加中间件。为了避免所有权问题,我们使用clone()方法并配合move关键字。

HttpServer::new(move || {
    App::new()
        .wrap(prometheus.clone()) // 添加Prometheus中间件
        // ... 其他应用配置和路由
})
.bind("127.0.0.1:8080")?
.run()
.await

通过这行代码,我们的应用现在会处理所有发往/metrics端点的请求,并以Prometheus期望的格式返回监控指标。

运行与验证

配置完成后,让我们运行应用并验证指标端点是否正常工作。

在终端中运行以下命令来启动应用:

cargo run

应用启动后,在浏览器中访问 http://localhost:8080/metrics

你应该能看到一个纯文本页面,其中包含了各种以redactor_为前缀的指标,例如:

  • redactor_http_requests_duration_seconds_bucket:HTTP请求耗时的直方图数据。
  • redactor_http_requests_total:HTTP请求的总数。

这些指标会自动包含请求的端点路径、HTTP方法和状态码等信息。当你访问应用的其他端点(包括不存在的路径产生404错误)时,相应的指标也会被记录并能在/metrics页面中看到更新。

总结

本节课中我们一起学习了如何为Rust Actix-web应用添加Prometheus监控支持。整个过程可以总结为以下三个步骤:

  1. 添加依赖:在Cargo.toml中加入actix-web-prom
  2. 配置中间件:使用PrometheusMetricsBuilder创建并配置指标收集器,指定项目名和端点路径。
  3. 集成到服务器:在创建HttpServer时,使用.wrap()方法将中间件添加到应用中。

通过使用actix-web-prom中间件,我们无需手动处理复杂的指标格式和端点逻辑,就能轻松地让应用具备被Prometheus监控的能力。这对于构建可观测的生产级服务至关重要。

119:连接Prometheus与Grafana 🚀

在本节课中,我们将学习如何将Prometheus和Grafana这两个强大的监控工具连接起来,并配置它们来可视化一个Rust HTTP应用暴露的指标数据。我们将使用Docker Compose来简化本地环境的搭建过程。

概述 📋

Prometheus是一个开源的系统监控和警报工具包,而Grafana则是一个用于可视化指标数据的开源平台。通过将它们与我们的Rust应用集成,我们可以实时监控应用的性能和行为。本节教程将演示一个直接且实用的方法,来启动并连接这两个服务,最终在Grafana仪表板上看到实际的数据。

配置Docker Compose 🐳

部署Prometheus和Grafana有多种方式,但我们将使用一种非常直接的方法来开始实验和连接这两个服务。我们将使用一个Docker Compose文件,它允许我们在本地开发环境中定义和运行多个容器服务。

以下是Docker Compose文件的核心配置:

version: '3'
services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

在这个配置中,我们定义了两个服务:prometheusgrafana。Prometheus服务将运行在容器的9090端口,并映射到宿主机的9090端口。Grafana服务运行在容器的3000端口,并映射到宿主机的3000端口。我们为Grafana设置了默认的管理员用户名和密码(admin/admin)。

配置Prometheus 📝

上一节我们介绍了如何通过Docker Compose启动服务,本节中我们来看看如何配置Prometheus来抓取我们Rust应用的指标。

Prometheus的配置通过一个YAML文件(prometheus.yml)完成。这个文件定义了抓取间隔、目标端点等关键信息。

以下是Prometheus配置文件的主要内容:

global:
  scrape_interval: 5s

scrape_configs:
  - job_name: 'axum_monitoring'
    static_configs:
      - targets: ['host.docker.internal:8080']

在这个配置中,scrape_interval设置为5秒,意味着Prometheus每5秒会向配置的目标端点抓取一次指标数据。我们定义了一个名为axum_monitoring的抓取任务,其目标是host.docker.internal:8080。使用host.docker.internal而不是localhost至关重要,因为它能让容器内的Prometheus正确访问到宿主机上运行的服务。

启动服务并验证连接 ⚙️

配置完成后,我们需要启动所有服务并验证它们之间的连接是否正常。

以下是启动和验证服务的步骤:

  1. 在终端中,导航到包含docker-compose.yml文件的目录。
  2. 运行命令 docker-compose -f docker-compose.yml up 来启动服务。这个命令会在前台运行,并输出日志信息。
  3. 服务启动后,打开浏览器,访问 http://localhost:8080 以确认Rust应用正在运行并暴露指标。
  4. 在另一个浏览器标签页中,访问 http://localhost:3000 以打开Grafana界面。

首次登录Grafana时,会提示你更改默认密码(admin/admin)。登录成功后,你将进入Grafana的主界面。

在Grafana中添加数据源 📊

现在Grafana已经运行起来,但还没有数据可以展示。我们需要将Prometheus添加为Grafana的数据源。

以下是添加Prometheus数据源的步骤:

  1. 在Grafana侧边栏,点击“Configuration”(齿轮图标),然后选择“Data Sources”。
  2. 点击“Add data source”按钮。
  3. 从列表中选择“Prometheus”。
  4. 在配置页面,为数据源命名(例如“Prometheus 1”)。
  5. 在“URL”字段中,输入Prometheus服务的地址:http://prometheus:9090。注意,这里使用服务名prometheus,因为Grafana容器通过Docker Compose网络可以解析这个名称。
  6. 点击页面底部的“Save & Test”按钮。如果配置正确,Grafana会显示“Data source is working”的成功消息。

至此,Grafana和Prometheus已经成功连接。

创建和配置仪表板 📈

数据源添加成功后,我们可以开始创建仪表板来可视化我们的指标数据。

以下是创建仪表板并添加图表的步骤:

  1. 在Grafana侧边栏,点击“Dashboards”(四个方块图标),然后选择“New dashboard”。
  2. 在新仪表板中,点击“Add visualization”。
  3. 选择我们之前创建的“Prometheus 1”数据源。
  4. 在查询编辑器中,我们可以输入PromQL查询语句来获取特定指标。例如,要查询Rust应用中HTTP 404错误请求的总数,可以使用以下查询:
    sum(rate(axum_http_requests_total{status="404"}[5m]))
    
  5. 点击“Run query”来查看图表。你可以调整时间范围(如过去15分钟、1小时)来观察数据变化。
  6. 我们可以添加更多查询来丰富仪表板。例如,添加一个查询来监控成功的HTTP 200请求:
    sum(rate(axum_http_requests_total{status="200"}[5m]))
    
  7. 通过不断访问你的Rust应用(特别是生成一些404错误),你可以在Grafana图表上实时看到指标数据的变化和趋势。

总结 🎯

本节课中我们一起学习了如何搭建一个完整的监控栈。我们使用Docker Compose快速部署了Prometheus和Grafana,配置Prometheus抓取Rust应用暴露的指标,并在Grafana中创建数据源和仪表板来可视化这些指标。这个过程展示了如何将应用监控从数据收集到可视化展示完整地串联起来,为理解应用运行状态提供了强大的工具。

120:暴露自定义监控端点 🛠️

在本节课中,我们将学习如何为一个Rust HTTP API服务添加一个自定义的健康检查端点。这个端点不仅能报告系统状态,还能通过检查特定文件的存在与否,来手动触发服务不可用的状态,这在负载均衡和运维场景中非常有用。

概述

这个Rust HTTP API服务暴露了多个路由端点,其中一个是根路径 /,另一个是 /redact API端点。此外,它还有一个自定义的 /health 端点,类似于Prometheus暴露指标的方式。我们将重点扩展这个 /health 端点,为其添加基于文件的手动健康检查功能。

现有健康检查端点

上一节我们介绍了API的基本结构,本节中我们来看看现有的 /health 端点是如何工作的。

该端点是一个名为 health 的函数,它实现了Actix Web框架中的 Responder trait,这允许我们返回一个HTTP响应。

以下是该函数的核心逻辑,它执行一些系统检查并生成响应:

// 示例:生成正常运行时间和内存使用情况
let uptime = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .unwrap()
    .as_secs();
let memory_usage = // ... 获取内存使用量的逻辑

启动服务后(例如通过 cargo run),我们可以使用 curl 命令来测试这个端点:

curl -X GET http://localhost:8080/health

响应会包含系统的正常运行时间和内存使用情况等信息,并返回 200 OK 状态码。

添加自定义文件检查逻辑

现在,我们希望在现有逻辑的基础上,增加一个手动控制机制。其核心思想是:如果服务器运行目录下存在一个特定的文件(例如 error.txt),则健康检查端点立即返回 500 Internal Server Error,表示服务不健康。

以下是实现此功能的关键步骤:

  1. 检查文件是否存在:在健康检查函数开始时,检查 error.txt 文件是否存在于当前工作目录。
  2. 条件性返回错误:如果文件存在,则立即构造并返回一个500错误响应,跳过后续的所有系统状态检查。
  3. 正常流程:如果文件不存在,则继续执行原有的系统状态检查并返回 200 OK

以下是实现该逻辑的代码片段:

use std::path::Path;

// 在 health 函数内部添加:
let error_file = Path::new("error.txt");
if error_file.exists() {
    // 立即返回 500 错误
    return HttpResponse::InternalServerError().body("500: File exists in the health directory");
}
// ... 原有的正常运行时间和内存检查逻辑

功能演示与测试

让我们来验证一下新添加的功能是否按预期工作。

首先,确保服务正在运行。然后,我们通过一系列 curl 命令和文件操作来测试:

以下是测试步骤和预期结果:

  1. 初始状态:当 error.txt 文件不存在时,访问 /health 端点应返回 200 OK 及系统信息。
    curl -v http://localhost:8080/health
    
  2. 触发错误:使用 touch error.txt 命令创建文件后,再次访问端点应返回 500 Internal Server Error
    touch error.txt
    curl -v http://localhost:8080/health
    
  3. 恢复健康:使用 rm error.txt 命令删除文件后,端点应恢复返回 200 OK
    rm error.txt
    curl -v http://localhost:8080/health
    

这种模式非常实用。在负载均衡器(如Nginx)配置中,可以将其健康检查指向此 /health 端点。当某个服务实例需要手动下线进行维护时,运维人员只需在其服务器上创建一个 error.txt 文件,负载均衡器就会自动将流量路由到其他健康的实例,实现了优雅的手动故障转移。

总结

本节课中我们一起学习了如何为Rust Web服务构建一个增强型的健康检查端点。我们不仅回顾了如何暴露基本的系统监控信息,还重点实现了一种基于文件系统的、手动控制服务健康状态的机制。这种方法结合了自动检测(如磁盘空间不足)与手动干预的能力,为在生产环境中进行服务运维、滚动更新和负载均衡提供了更大的灵活性和控制力。

121:32_02_10_Azure中的监控与日志记录策略 📊

在本节课中,我们将学习如何在Azure平台上为容器应用配置监控与日志记录。我们将看到,无需深入理解Azure的底层工作原理,也能轻松地为容器应用添加监控和日志记录功能。

概述

我们将通过Azure的容器应用服务来演示。整个过程非常直观,您无需对容器本身进行任何修改,即可获得丰富的监控数据和日志流。

创建容器应用

首先,我们需要在Azure上创建一个容器应用。以下是具体步骤:

  1. 创建一个新的资源组。这是在Azure上创建服务所必需的。
  2. 为资源组命名,例如 demo-container-apps-login,以便于识别和后续清理。
  3. 将容器应用命名为 demo-alfredo-login
  4. 选择区域,例如“美国西部2”。
  5. 使用默认配置进行创建:CPU核心数设置为0.25(即四分之一个核心),内存为500MB,端口为80,并允许来自任何地方的流量。

创建过程需要一些时间。部署成功后,我们可以进入资源页面查看。

访问默认应用

容器应用启动并运行后,我们可以通过其应用URL(运行在80端口)进行访问。默认情况下,您会看到一个页面,显示“您的Azure容器应用正在运行”。这表明一个默认的容器已被成功部署。

探索监控与日志功能

现在,我们的应用已经运行,但尚未记录任何自定义活动。不过,Azure已经为我们提供了丰富的内置监控工具。在资源页面,您可以找到“活动日志”、“指标”、“日志”、“日志流”等选项。

查看活动日志

点击“活动日志”,目前可能没有内容,因为我们还没有触发任何需要记录的操作。

使用预置日志查询

在“日志”部分,Azure提供了许多预置的查询模板。例如,您可以快速分析错误。虽然我们现在不关注错误,但这个功能在排查问题时非常有用。

实时查看日志流

“日志流”功能非常强大。它会创建一个控制台并连接到您的运行容器。连接成功后,它会实时显示容器产生的所有日志。这对于即时调试和观察应用行为至关重要。您可以看到容器启动的详细信息,例如它正在监听哪个端口。

如果您部署了自己的应用,您将能在这里看到应用生成的所有日志。当前,我们查看的是系统日志,它反映了运行容器应用的基础系统状态,这同样是检查运行状况的绝佳方式。

监控应用指标

除了日志,监控容器运行时的指标也很重要。Azure默认提供了多项关键指标,您无需在容器中添加任何额外配置即可使用。

查看CPU使用率

我们可以检查CPU使用率指标,图表会直观地显示其变化情况。

查看网络请求

我们还可以检查请求指标。虽然当前可能没有记录到我们的访问请求,但我们可以查看最大值、平均值等统计数据。

查看网络流量

“网络流入”指标可能记录了我们在几分钟前访问应用时产生的流量。

这些指标默认包含在服务中,部署后即可使用,非常方便。

配置警报规则

在“警报”部分,您可以基于这些指标创建警报规则。您可以定义触发条件(例如,当CPU使用率超过80%时),并设置通知方式。这样,当应用出现异常时,系统能及时提醒您。

总结

本节课中,我们一起学习了如何在Azure上为容器应用快速启用监控与日志记录。

  1. 创建与部署:我们创建了一个容器应用,并使用了默认配置进行部署。
  2. 访问日志:我们探索了如何通过“日志流”功能实时查看容器输出的系统与应用日志。
  3. 监控指标:我们查看了CPU使用率、网络请求和流量等默认提供的运行指标。
  4. 设置警报:我们了解到可以基于这些指标配置警报规则,实现主动监控。

关键在于,所有这些功能都可以在不修改容器应用代码的情况下直接获得。当您在Azure这类云服务提供商上使用容器服务时,充分利用这些内置的监控和日志记录策略,能极大地简化运维工作,帮助您更好地洞察应用状态。

122:日志策略与结构化日志

在本节课中,我们将学习如何在Rust应用程序中实施日志记录策略。我们将从一个几乎不记录任何信息的Rust应用开始,然后引入Rust生态中最稳定、最强大的日志库之一——tracing。我们将探讨在Rust项目中使用该策略时的具体表现,以及需要考虑的各个方面,例如如何控制日志的详细程度、不同的日志级别是什么,以及如何为像HTTP API这样的Rust应用配置高效的日志记录。此外,我们还将简要介绍结构化日志,包括它的定义、适用场景,以及为什么在系统中考虑使用结构化日志会很有帮助。提示是:当你的日志采用结构化格式时,所有系统都能更轻松地消费这些信息。虽然这可能不是解决你应用程序所有日志输出问题的唯一答案,但它无疑是一个有趣且值得在Rust应用中实施日志记录时仔细考量的方向。

引入日志策略

现在,让我们在Rust中引入一些日志策略。

我们将以一个实际上在任何地方都几乎不记录日志的Rust应用程序为例,开始使用Rust中最稳健、最稳定的日志库之一——tracing

使用 tracing

接下来,我们将看看在Rust项目中使用这种策略时是什么样子,以及我们应该考虑哪些方面。

我们需要考虑如何控制日志的详细程度,这些详细程度级别是什么,以及如何为像HTTP API这样的Rust应用程序配置运行良好的日志记录。

捕获有用信息与结构化日志

当你希望开始从这样的服务中捕获有用信息时,我们还将包含一点结构化日志的内容。

什么是结构化日志?你何时可能希望在系统中考虑使用结构化日志?

提示是:当你的日志采用结构化格式时,所有系统都能更轻松地消费这些信息。

结构化日志的适用性

也许这并不是解决你应用程序所有日志输出问题的答案。

但它绝对是一个有趣且值得一看的方向,当你想开始在Rust应用程序中实施日志记录时,值得考虑。


本节课中,我们一起学习了如何在Rust中引入日志记录策略,从使用tracing库控制日志级别,到为HTTP API等应用配置有效的日志输出。我们还探讨了结构化日志的概念、优势及其适用场景,了解到结构化格式能极大地方便其他系统消费日志信息。虽然它并非万能,但在设计Rust应用的日志系统时,它是一个非常重要且值得考虑的组成部分。

123:为Rust应用添加日志记录 📝

概述

在本节课中,我们将学习如何为一个现有的Rust HTTP API应用程序添加日志记录功能。我们将使用 tracingtracing-subscriber 这两个库来实现结构化日志输出。

项目介绍

我们有一个之前简要见过的HTTP API应用程序。这是一个使用Cargo初始化的常规项目。让我们深入了解其结构。

首先,查看项目的依赖项。我们使用了 actix-web 框架,以及用于序列化的 serde 库。

源代码位于 main.rs 文件中。这个应用之前已经集成了Prometheus来暴露一些指标。在 main 函数的底部,HTTP服务被启动并绑定到8080端口。该服务的主要功能是根据一些正则表达式规则来“编辑”或“隐藏”文本中的个人信息,例如社会安全号码、信用卡号等。

运行示例应用

为了理解应用的工作原理,我们可以运行它并进行测试。

在终端中执行 cargo run 来启动应用。然后,在另一个终端中使用 curl 发送一个POST请求到 /redact 端点,并附带一段JSON文本。例如,发送 {"text": "Alfred Smith went to the park"}。服务将返回处理后的文本,例如 Person 1 went to the park。这表明个人信息编辑功能正常工作。

添加日志记录库

现在,我们的目标是为这个应用添加日志记录功能。

我们需要在 Cargo.toml 文件中添加两个依赖库:tracingtracing-subscriber

  • tracing 库提供了一个用于代码“插桩”(instrumentation)的门面(facade),负责处理所有日志记录工作。
  • tracing-subscriber 库则允许我们订阅 tracing 事件,并开始生成日志。仅使用 tracing 本身不会产生任何日志输出,因此需要 tracing-subscriber

添加依赖后,我们回到 main.rs 文件。在文件顶部,我们需要引入将要使用的 tracing 模块。例如,我们可以引入 infowarn 宏来记录信息和警告消息。同时,我们还需要引入 Level 枚举和 tracing_subscriber 模块,特别是其中的 fmt 子模块来格式化日志消息。

初始化日志记录器

接下来,我们将在 main 函数中初始化日志记录。

首先,我们需要实例化一个“订阅者”(subscriber)。我们使用 fmt::Subscriber 构建器,并通过 with_max_level 方法设置日志记录的最大级别。例如,我们将其设置为 Level::INFO。最后调用 finish() 方法完成构建。

这里有一个关键点需要注意:with_max_level(Level::INFO) 并不意味着只记录 INFO 级别的日志。它的含义是记录 INFO 级别及更高级别(如 WARN, ERROR)的日志。这是一个常见的理解误区。

构建好订阅者后,我们使用 tracing::subscriber::set_global_default 将其设置为全局默认的订阅者。这里使用 expect 方法来处理可能的错误,如果设置失败,程序会 panic 并输出错误信息。

初始化完成后,我们就可以在代码中开始记录日志了。例如,在服务启动的位置添加一条 info! 日志。

测试日志输出

完成代码修改后,我们再次运行应用。

执行 cargo run 后,可以在终端中看到日志输出。输出中包含了我们的模块名、项目名、日志级别(INFO)、日期时间戳,以及来自 actix-web 服务器的日志信息。这表明日志记录已成功集成。

为了验证日志级别过滤功能,我们可以在代码中添加一条 warn! 日志。重新运行应用后,我们会看到警告信息被输出。如果我们将 with_max_level 设置为 Level::WARN,那么重新运行后,INFO 级别的日志将不再显示,只有 WARNERROR 级别的日志会被输出。

总结

本节课我们一起学习了如何为Rust应用添加日志记录。

我们首先介绍了现有的HTTP API项目。然后,通过添加 tracingtracing-subscriber 依赖库,在代码中引入必要的模块并初始化全局日志订阅者。我们重点讲解了 with_max_level 的含义,它用于设置日志记录的最低级别。最后,我们通过运行应用并观察不同级别的日志输出,验证了日志功能的正确集成。

这是一种为应用程序添加日志记录或插桩的非常直接的方法,可以确保日志开始输出到终端。虽然 tracing 库的功能非常强大,有时会让人感到复杂,但这是启用日志记录的一个可靠方式。

124:在Rust中使用日志记录

在本节课中,我们将学习如何在Rust应用程序中替换简单的println!语句,转而使用结构化的日志记录系统。我们将看到如何初始化日志记录器,如何设置日志级别,以及如何在不同位置输出不同级别的日志信息。

概述

上一节我们介绍了如何为应用程序添加必要的日志记录功能以开始产生输出。本节中,我们将在此基础上进行扩展,学习如何用结构化的日志记录替换代码中分散的println!语句,并添加更有用的上下文信息。

替换硬编码信息

首先,我们注意到在lib.rs中有一些println!语句,它们会将“Reacting”等信息打印到标准输出。这种做法不够灵活,因为我们无法控制其输出级别。我们的目标是避免使用这些println!语句。

以下是替换过程的具体步骤:

  1. 初始化变量:在main.rs文件的顶部,我们定义服务器地址和端口变量,而不是在代码中硬编码。

    let address = "127.0.0.1";
    let port = 8080;
    let bind_address = format!("{}:{}", address, port);
    
  2. 替换警告信息:找到原来打印警告信息的位置,将其替换为debug!级别的日志记录。这需要确保在文件顶部已经引入了tracing::debug

    // 替换前:println!("Binding to {}", bind_address);
    // 替换后:
    debug!("Binding to {}", bind_address);
    
  3. 更新绑定调用:将硬编码的地址字符串替换为我们定义的bind_address变量。

    // 替换前:.bind("127.0.0.1:8080")
    // 替换后:
    .bind(&bind_address)
    

通过这种模式,你可以在应用程序的各个部分,根据信息的重要性,使用不同级别的日志(如debug!info!warn!error!)来记录状态。

添加追踪信息

接下来,我们为应用程序的核心逻辑添加info!级别的日志。

以下是具体操作:

  1. 引入宏:在lib.rs文件的顶部,确保引入了tracing::info宏。

    use tracing::info;
    
  2. 替换println!:找到原有的println!语句,将它们替换为info!宏。

    // 替换前:println!("Reacting");
    // 替换后:
    info!("Reacting");
    

完成这些更改后,原来的控制台输出将被结构化的日志信息所取代。

控制日志级别

更改完成后,运行cargo run启动应用程序,你可能会发现新的info!日志没有显示。这是因为当前设置的全局日志级别可能高于INFO(例如是WARNERROR)。

为了解决这个问题,你有两个选择:

  • lib.rs中的info!改为debug!
  • 或者在启动应用程序时,将环境变量RUST_LOG的级别设置为info或更低(如debugtrace)。

例如,在终端中运行:

RUST_LOG=info cargo run

这样,info!及以上级别的日志就会输出到控制台。现在,当你访问API时,就能看到由日志系统输出的“Reacting”等信息,而不是简单的println!输出。

总结

本节课中,我们一起学习了在Rust中实施结构化日志记录的关键步骤。我们首先用变量替换了硬编码的配置信息,然后使用debug!info!等宏替换了原有的println!语句。最后,我们了解了如何通过环境变量控制日志的输出级别。这正是为应用程序添加可管理、可配置的日志记录功能的标准技术。

125:控制日志详细级别 📝

在本节课中,我们将学习如何为我们的HTTP API应用程序实现灵活的日志级别控制。我们将把硬编码的日志级别替换为通过环境变量动态配置的方式,从而使我们的应用在生产环境中更具适应性和可配置性。


概述

之前,我们为应用程序添加了日志功能。然而,日志级别是硬编码在源代码中的。每次需要调整日志详细程度时,都必须修改代码并重新编译。这在生产环境中非常不理想,因为我们通常需要能够灵活地调整日志级别,而无需重新部署应用。

上一节我们介绍了如何添加日志功能,本节中我们来看看如何使其配置变得灵活。

从硬编码到环境变量

目前,我们的日志级别是固定的,缺乏灵活性。为了改进这一点,我们将采用环境变量来动态设置日志级别。

首先,我们需要从环境中读取一个变量。为了避免与其他可能使用通用变量名的应用冲突,我们将为这个变量加上服务名前缀。

以下是实现步骤:

  1. 从环境变量中读取日志级别。
  2. 将读取到的字符串匹配到对应的日志级别枚举值。
  3. 设置一个默认级别(例如 info),以防环境变量未设置或值无效。

实现步骤详解

1. 读取环境变量

我们使用标准库的 std::env 模块来读取环境变量。我们将变量命名为 REDACTOR_LOGGING_LEVEL,以使其具有特定性。

let logging_level = std::env::var("REDACTOR_LOGGING_LEVEL").unwrap_or("info".to_string());

这段代码尝试读取名为 REDACTOR_LOGGING_LEVEL 的环境变量。如果变量不存在,则使用默认值 "info"

2. 匹配日志级别

接下来,我们需要将读取到的字符串(如 "trace", "debug", "info", "warn", "error")转换为 tracing crate 中对应的 Level 枚举值。我们使用 match 语句来实现这个映射。

let max_level = match logging_level.to_lowercase().as_str() {
    "trace" => Level::TRACE,
    "debug" => Level::DEBUG,
    "info" => Level::INFO,
    "warn" => Level::WARN,
    "error" => Level::ERROR,
    _ => Level::INFO, // 默认级别
};

如果提供的字符串不匹配任何已知级别,代码将默认使用 Level::INFO

3. 应用日志级别

最后,我们将匹配得到的 max_level 用于初始化日志订阅器,替换之前硬编码的 Level::INFO

let subscriber = FmtSubscriber::builder()
    .with_max_level(max_level) // 使用动态设置的级别
    .finish();

测试与验证

完成代码修改后,我们可以通过设置不同的环境变量来测试日志级别的变化。

  • 运行 REDACTOR_LOGGING_LEVEL=debug cargo run 将启用调试级别及更高级别的日志。
  • 运行 REDACTOR_LOGGING_LEVEL=trace cargo run 将启用最详细的跟踪级别日志。
  • 如果不设置该变量或设置为无效值,应用将默认使用 info 级别。

通过这种方式,我们可以在启动应用时轻松控制其日志输出量,无需改动任何源代码。

总结

本节课中我们一起学习了如何使Rust应用程序的日志系统更具生产环境适用性。我们成功地将硬编码的日志级别替换为通过环境变量配置的方式。具体步骤包括:

  1. 使用 std::env::var 读取环境变量。
  2. 使用 match 语句将字符串映射到具体的日志级别枚举。
  3. 设置合理的默认值以保证应用的健壮性。

现在,我们的应用拥有了灵活的日志控制能力,可以通过简单的环境变量调整来适应开发、测试和生产等不同场景的需求,这比我们之前硬编码的方式要强大和实用得多。

126:结构化日志记录 📝

在本节课中,我们将学习如何在Rust应用程序中实现结构化日志记录。结构化日志记录是一种将日志输出格式化为易于机器解析的结构化数据(如键值对)的技术,这对于监控、告警和日志分析系统至关重要。

概述

之前我们已经为应用程序配置了灵活的日志级别和详细程度。现在,我们将探讨如何进一步提升日志的实用性,使其不仅对人类可读,也便于外部程序自动化处理。

从非结构化日志到结构化日志

上一节我们介绍了基础的日志记录功能。本节中,我们来看看非结构化日志在自动化处理时面临的挑战。

当前的非结构化日志输出类似于散文,虽然人类容易阅读,但程序解析起来却很困难。例如,日志信息 binding to address 127.0.0.1:8080 需要编写复杂的正则表达式来提取IP地址和端口号。这种方法是脆弱的,因为日志格式的微小变动就会导致解析失败。

为了解决这个问题,我们引入结构化日志记录。它通过将信息组织成清晰的键值对形式,使得外部系统能够可靠、高效地解析日志数据。

实现结构化日志记录

实现结构化日志记录非常简单直接。我们只需在日志宏中明确地输出变量名和值。

以下是实现步骤:

  1. 定位到记录服务器地址绑定的日志语句。
  2. 修改 info! 宏的调用,使用 % 占位符来格式化变量,并明确指定键名。

例如,将原来的日志语句修改为:

info!("listening for requests address=% port=%", address, port);

在这行代码中,我们向 info! 宏传递了格式化字符串和变量。% 是占位符,它会被后面提供的变量值依次替换,同时我们在字符串中直接定义了键名(address=port=)。

查看结构化日志效果

让我们运行修改后的服务,观察输出变化。

启动服务后,你将看到类似以下的输出:

listening for requests address=127.0.0.1 port=8080

现在的日志输出是结构化的。我们得到了清晰的键值对:address=127.0.0.1port=8080。这种方式极大地便利了外部系统(如日志聚合器或监控工具)对日志内容的解析。

你可以根据需要轻松添加更多字段到日志中,例如请求方法、用户ID或响应状态码,所有信息都将以同样易于解析的格式呈现。

总结

本节课中我们一起学习了结构化日志记录。我们了解到非结构化日志在自动化处理上的局限性,并学会了如何使用 log 库的宏来输出结构化的键值对日志。这种方法为日志的后续处理、监控和告警提供了坚实的基础,是构建可维护数据工程与DevOps应用的重要实践。

127:系统自动化引言 🚀

在本节课中,我们将要学习系统自动化的核心概念,特别是如何结合文件系统遍历与文件解析来构建强大的自动化工具。这是系统工程师和DevOps从业者最常遇到的场景之一。

系统自动化令人兴奋。它实际上是将你所学到的关于自动化、编程和开发的所有知识汇集起来,并开始尝试将焦点转向实际的系统。你会开始识别出一些任务,这些任务或许值得投入更多关注,并尝试避免手动操作,转而进行一些编程。

接下来,让我们探索一些可以实施的策略。例如,遍历文件系统。如何使用Rust来实现?除了遍历本身,还需要考虑哪些因素?在哪些场景下应用这种策略是合理且有意义的?我们也将初步了解文件解析,看看解析何时是一种好策略,以及它为何有意义。

文件系统遍历与文件解析的结合非常强大。这是作为系统工程师或DevOps人员,在系统环境中寻找自动化机会时,最常遇到的场景之一。


核心策略与应用场景

以下是两种核心自动化策略及其应用场景的简要介绍。

文件系统遍历

在Rust中遍历文件系统,你需要考虑如何高效地递归访问目录和文件,并处理可能出现的错误,如权限不足或符号链接循环。

文件解析

解析文件意味着读取文件内容并提取结构化信息。这在处理日志文件、配置文件或数据文件时非常有用,可以自动提取关键信息进行分析或决策。


本节课中我们一起学习了系统自动化的引言,重点探讨了结合文件系统遍历与文件解析这一强大且常见的自动化模式。理解这些基础策略是构建高效系统自动化工具的第一步。

128:可构建的自动化任务概览 🚀

在本节课中,我们将要学习Rust在系统自动化领域的强大能力。我们将探讨Rust如何超越其作为编程语言的角色,成为一个高效、可靠的自动化工具,并了解其在跨平台脚本、高性能计算以及系统管理任务中的具体应用。

跨平台脚本与高性能

上一节我们介绍了Rust在系统自动化中的潜力,本节中我们来看看其核心优势之一:跨平台脚本能力与高性能。

Rust具备跨平台脚本能力。这意味着你可以构建功能强大的程序,这些程序基本上可以在任何地方运行。通过针对不同类型的架构、服务器和操作系统,你可以确信能够将单个二进制文件部署到任何地方,并开始利用这种可移植性。

正如我在幻灯片中所展示的,这种能力允许你将一个二进制文件放置在任何其他地方,而无需考虑依赖关系。这是一个非常强大的概念,特别是与其他脚本语言或编程语言相比时。

Rust不一定是脚本语言,但它绝对提供了这种灵活性。这同时也带来了高性能,因为Rust非常快。它允许你在处理,特别是CPU密集型任务时获得速度,这些任务在其他编程语言中执行起来可能非常昂贵。

Rust还能够非常直接地处理并发和多线程应用程序,并开始利用CPU中的所有核心。其内存占用非常小,这就是为什么我们有时称其非常高效。

健壮可靠的自动化

现在,让我们探讨为何Rust自动化是健壮可靠的。

首先是内存安全。Rust的内存管理能力非常出色。除此之外,它内部处理内存的方式也很简单。我曾遇到过应用程序内存消耗失控的情况,我们无法确切知道内存泄漏发生在哪里。Rust的编译器能够捕获所有那些你可能未意识到的潜在问题和陷阱,从而防止这些问题发生。

Rust的语法对于来自Python或Ruby等高级语言的人来说,可能不那么高度复杂,或者读起来有些不同,但在某些方面又有些相似,这使得它易于使用。未来如果你需要进行一些维护,编译器会提供很大帮助。

可部署的自动化任务

以下是你可以使用Rust实现的一些其他自动化任务。我将快速介绍,因为这只是对其他可能性的一个概览。

  • 基础设施与配置自动化:你可以自动化基础设施的配置和供应。可以编写各种类型的编排器,或者连接到其他编排器,例如Ansible。你可以为Ansible编写插件,Rust是支持的编程语言之一。
  • 持续集成与部署:你可以将Rust用于持续集成和部署。但我想说,与其说是“使用Rust”,不如说是“在现有工具之外补充使用Rust”。例如,我们稍后将看到的GitHub Actions,可以看到如何调用Rust来进行一些处理。
  • 监控与告警:我们已经见过这个功能,并且已经能够在端点上暴露信息。我们将查看日志聚合和分析,这有点类似于监控和日志记录,但能够解析一些日志或做一些专门的处理也非常重要。
  • 安全与合规扫描:如果你想要爬取文件系统并在那里做其他事情,也可以考虑进行一些安全和合规性扫描。

具体应用场景

自动化的基础设施供应听起来很有趣。过去,我工作过的系统负责平衡供应虚拟机、使它们离线以及根据需要扩展。你绝对可以弹性地启动服务器和数据库,并开始用少量代码使用高度复杂的领域。这将使你不仅能够轻松创建基础设施,还能够销毁它,基本上就是通过编写一个快速程序来按需创建可重现的环境。

最后是配置管理,仅仅能够安装软件包和依赖项、配置应用程序和服务。我认为用Rust安装软件包和依赖项可能不一定是最佳选择,但你可以编写Rust来进行一些比较,并尝试确保服务器的特定状态是准确和100%受控的。管理主机配置,甚至可以设置某种模板,为你提供非常快速、准确的预配置环境。

总结

本节课中,我们一起学习了Rust在系统自动化中的关键优势和应用。我们了解到Rust凭借其跨平台部署能力single binary anywhere)、高性能内存安全特性,成为构建健壮、高效自动化任务的强大工具。从基础设施配置、CI/CD到监控安全,Rust都能提供可靠的解决方案,帮助开发者和管理员轻松应对复杂的系统管理挑战。

129:文件系统爬取教程 🗂️

在本节课中,我们将学习如何使用Rust编写一个应用程序来遍历文件系统。这是一个非常实用的技能,它不仅能让你遍历文件系统,还能在此基础上执行许多任务。这是你需要掌握的基础知识,而在Rust中,实现起来并不复杂。

概述

我们将从一个简单的Rust程序开始,它接受一个命令行参数作为要遍历的目录路径。程序的核心是一个递归函数,它会读取目录内容,判断每个条目是文件还是子目录。如果是子目录,函数会递归调用自身以继续深入遍历;如果是文件,则直接打印其路径。通过这种方式,我们可以系统地探索整个目录结构。

代码实现

首先,我们有一个主文件 main.rs。主函数 main 负责处理命令行参数。

use std::env;
use std::path::Path;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        eprintln!("请提供一个路径参数");
        return;
    }
    let path = Path::new(&args[1]);
    walk_path(&path);
}

主函数收集命令行参数。我们未使用复杂的CLI框架,而是直接获取第二个参数(索引为1)作为目标路径。如果未提供参数,程序会打印错误信息并退出。获取路径后,我们调用 walk_path 函数开始遍历。

上一节我们介绍了程序的入口点,本节中我们来看看核心的遍历函数 walk_path 是如何工作的。

以下是 walk_path 函数的实现步骤:

  1. 读取目录:使用 std::fs::read_dir 函数尝试读取给定路径的目录内容。
  2. 遍历条目:使用 for 循环遍历目录中的每一个条目(DirEntry)。
  3. 获取路径:从每个条目中提取出完整的文件系统路径。
  4. 判断类型:检查该路径指向的是文件还是目录。
  5. 递归或打印:如果是目录,则递归调用 walk_path 函数;如果是文件,则打印其路径。
use std::fs;

fn walk_path(path: &Path) {
    if let Ok(entries) = fs::read_dir(path) {
        for entry in entries {
            if let Ok(entry) = entry {
                let entry_path = entry.path();
                if entry_path.is_dir() {
                    // 递归遍历子目录
                    walk_path(&entry_path);
                } else {
                    // 打印文件路径
                    println!("{}", entry_path.display());
                }
            }
        }
    }
}

核心概念解析

现在,让我们深入分析一下代码中的关键部分,特别是递归是如何应用的。

walk_path 函数接收一个 Path 类型的参数。它的工作原理是:首先假设这个路径是一个目录,然后尝试读取其中的所有条目。对于每个条目,我们获取其完整路径并判断它是否为目录。

递归就发生在这里:如果 entry_path 是一个目录,我们不是立即处理它,而是直接再次调用 walk_path 函数,并将这个子目录的路径作为新参数传入。这样,函数就会“进入”这个子目录,重复同样的过程(读取、判断、递归),从而一层一层地遍历整个文件系统树。如果不是目录,我们就简单地打印出文件路径。

这是一种遍历文件系统、查找目标文件的清晰有效的方法。

运行与测试

如果我们在终端中运行程序并传入一个存在的目录(例如 bar_log),程序会快速运行并打印出该目录及其所有子目录下的文件列表。

cargo run bar_log

输出将会是类似这样的文件路径列表:

/Users/example/bar_log/file1.txt
/Users/example/bar_log/subdir/another_file.rs
...

如果传入一个不存在的路径,程序会因为 read_dir 失败而跳过该目录,不会输出错误(除非我们主动添加错误处理)。在我们的简单示例中,主函数会先检查路径是否存在。

扩展可能性

这个基础实现为你提供了一个坚实的起点。在此基础上,你可以进行许多扩展:

  • 过滤文件:在打印路径前,可以根据文件扩展名(如 .rs.log)或其他属性进行过滤。
  • 控制递归深度:添加一个深度计数器参数,限制程序遍历的子目录层数,避免进入过深的文件结构。
  • 执行操作:不仅仅是打印,你可以对找到的文件进行读取、复制、移动或计算哈希值等操作。
  • 错误处理:更健壮地处理 read_dir 或文件访问可能出现的错误,例如权限不足。

总结

本节课中我们一起学习了如何使用Rust进行文件系统爬取。我们构建了一个程序,它通过递归函数遍历指定目录,区分文件和子目录,并打印出所有文件的路径。我们理解了递归在此场景下的应用逻辑,即将一个大问题(遍历整个目录树)分解为重复的小问题(遍历单个目录)。这个简单的例子是许多文件系统工具(如查找、备份、清理工具)的核心逻辑,掌握了它,你就为用Rust处理更复杂的文件操作打下了坚实的基础。

130:构建用于解析文件的Rust命令行工具

在本节课中,我们将学习如何构建一个Rust命令行工具,用于遍历文件系统并检测文件类型。我们将重点介绍如何区分纯文本文件和二进制文件,并理解Rust中trait导入的重要性。

概述

我们将从一个已有的文件系统遍历程序开始,为其添加文件类型检测功能。核心任务是判断一个文件是纯文本文件还是二进制文件。我们将使用Rust的标准库来完成这个任务,并在这个过程中学习一些重要的编程概念。

准备工作

首先,我们需要在代码文件的顶部引入必要的库。这些库将帮助我们进行文件操作和字节读取。

use std::fs;
use std::io::{self, BufReader, Read};

修改遍历函数

上一节我们介绍了基本的文件系统遍历逻辑。本节中,我们来看看如何修改walk_path函数,使其在遍历时对每个文件进行类型检测。

我们将移除简单的文件路径打印,转而实现一个检测逻辑。以下是修改后的核心步骤:

  1. 打开文件。
  2. 读取文件的前1024个字节。
  3. 尝试将这些字节解码为UTF-8字符串。
  4. 根据解码结果判断文件类型。
let file = fs::File::open(&path).unwrap();
let mut buffer = [0; 1024];
let mut reader = BufReader::new(file);
let bytes_read = reader.read(&mut buffer).unwrap();

实现文件类型检测

在成功读取文件字节后,我们需要进行判断。如果读取的字节数大于0,并且这些字节能够被成功解码为UTF-8字符串,那么我们认为该文件是纯文本文件;否则,我们将其标记为二进制文件。

以下是判断逻辑的代码实现:

if bytes_read > 0 && std::str::from_utf8(&buffer[..bytes_read]).is_ok() {
    println!("{}: plain text", path.display());
} else {
    println!("{}: binary file", path.display());
}

理解Trait的作用

在编写上述代码时,你可能会遇到一个常见的编译错误:no method named \read` found。这是因为read方法属于std::io::Read`这个trait。

在Rust中,trait定义了一组方法。要使用某个类型(如BufReader)的trait方法,必须将该trait导入当前作用域。这就是为什么我们需要在文件开头写上use std::io::Read;。Rust的编译器错误信息非常清晰,会直接提示你缺失哪个trait,这是Rust语言的一大优势。

实际应用与总结

本节课中我们一起学习了如何为文件系统遍历工具添加文件类型检测功能。我们实现了通过读取文件头部字节并尝试进行UTF-8解码来区分纯文本文件和二进制文件的逻辑。同时,我们也理解了在Rust中正确导入trait对于调用相关方法的重要性。

这种检测机制在实际开发中非常有用。例如,在处理未知来源的文件、自动化系统配置或日志分析时,确保程序只处理预期的纯文本文件可以避免许多错误。使用Rust实现此功能,既能保证高性能,又能借助其强大的类型系统和编译器避免许多运行时错误。这是一个值得掌握的实用编程模式。

131:使用Rust解析日志文件 📄

在本节课中,我们将学习如何使用Rust编写一个高效的工具来解析和分析日志文件。我们将重点关注如何从日志中提取特定信息,例如按小时统计错误数量,并处理可能存在的压缩文件格式。

概述

日志文件是系统监控和故障排查的重要数据源。手动分析大型日志文件效率低下。我们将构建一个Rust程序,它能自动解析日志文件,提取时间戳和错误信息,并按小时汇总错误数量。此工具的特点是高性能可定制性,能够处理纯文本和压缩格式的日志文件。

项目结构

上一节我们概述了项目目标,本节中我们来看看具体的代码结构。程序的核心是一个main函数,它接收一个文件路径作为参数。程序没有使用独立的库模块,所有逻辑都包含在主文件中。

fn main() {
    // 主函数逻辑
}

日志格式与业务逻辑

在深入代码之前,我们需要理解要解析的日志格式和我们的业务目标。

程序旨在解析类似以下格式的日志行:

Aug 29 17:30:00 MyComputer some_daemon[123]: This is a log message, possibly containing an error.

我们特别关注包含“error”关键词的行。业务逻辑是计算每小时出现的错误数量

核心实现解析

理解了目标后,我们来看看实现的核心部分。程序主要分为几个步骤:读取文件、逐行解析、应用正则表达式提取时间、按小时聚合错误计数。

1. 文件读取与解压

程序首先需要读取日志文件。考虑到日志文件可能被压缩(例如.gz格式),我们需要一个能同时处理压缩和未压缩文件的机制。

以下是处理逻辑:

// 使用 match 语句根据文件扩展名决定读取方式
match path.extension().and_then(|s| s.to_str()) {
    Some("gz") => {
        // 如果是 .gz 文件,使用 GzDecoder 解压
        let file = File::open(path)?;
        let decoder = GzDecoder::new(file);
        let reader = BufReader::new(decoder);
    }
    _ => {
        // 否则,直接以普通文件打开
        let file = File::open(path)?;
        let reader = BufReader::new(file);
    }
}

这段代码使用match进行模式匹配,根据文件后缀名选择不同的读取器,确保无论是否压缩都能正确读取。

2. 逐行处理与错误匹配

获得文件读取器后,程序开始逐行读取。对于每一行,它检查是否包含“error”字样。

以下是处理循环:

for line_result in reader.lines() {
    let line = line_result?; // 处理可能的IO错误
    if line.to_lowercase().contains("error") {
        // 如果该行包含错误,则进行后续的时间戳解析和计数
    }
}

3. 使用正则表达式解析时间戳

对于包含错误的行,我们需要从中提取小时信息。这通过一个预定义的正则表达式来完成。

定义的正则表达式模式类似于:

let re = Regex::new(r"(\w{3}\s+\d{1,2})\s+(\d{2}):\d{2}:\d{2}").unwrap();

这个模式捕获日期(如“Aug 29”)和小时(如“17”)。

当一行匹配时,我们可以提取出这些捕获组:

if let Some(caps) = re.captures(&line) {
    let current_hour = caps.get(2).map_or("", |m| m.as_str()); // 提取小时部分
    // ... 使用 current_hour 进行计数
}

4. 按小时聚合计数

这是业务逻辑的核心。我们需要跟踪当前正在统计的是哪个小时,以及该小时内的错误数量。

逻辑流程如下:

  1. 初始化一个current_hour变量来跟踪当前统计的小时,以及一个error_count计数器。
  2. 遍历每一行。
  3. 如果从某行提取的hourcurrent_hour不同,说明进入了新的一个小时。此时,打印上一个小时的总错误数,并将current_hour更新为新的小时,同时将error_count重置为0。
  4. 如果hourcurrent_hour相同,则将error_count加1。
  5. 文件遍历结束后,打印最后一个小时的数据以及整个文件的总错误数。

运行示例与输出

现在,让我们看看这个程序在真实日志文件上的运行效果。使用cargo run命令并传入日志文件路径即可执行。

程序输出按小时显示错误数量,格式如下:

[Aug 29 17:00] Errors: 85
[Aug 29 18:00] Errors: 41
...
Total errors in file: 9021

这清晰地展示了错误在一天中的分布情况。

优势与考量

上一节我们看到了程序的运行结果,本节我们来总结其优势和应用时的注意事项。

优势:

  • 高性能:Rust的零成本抽象和高效的内存管理使得处理大型日志文件速度极快。
  • 部署简单:编译后的二进制文件可以轻松部署到任何兼容的系统,避免了脚本语言对运行环境的依赖。
  • 强类型与安全性:减少了运行时错误,代码更健壮。
  • 可扩展性:可以轻松修改以输出JSON格式、连接数据库或添加更复杂的分析规则。

注意事项:

  • 日志格式耦合:当前的正则表达式针对特定日志格式。如果日志格式变化,程序需要调整。
  • 功能针对性:这是一个专用工具。对于简单的临时分析,使用grepawk等Unix命令行工具可能更快捷。但当需要复杂的、重复执行的业务逻辑时,Rust工具的优势就体现出来了。

总结

本节课中我们一起学习了如何使用Rust构建一个实用的日志解析工具。我们涵盖了从文件读取(支持压缩格式)、逐行处理、利用正则表达式提取关键信息,到实现按小时聚合错误计数的完整业务流程。这个例子展示了Rust在数据处理领域的强大能力:它既能提供媲美系统级工具的性能,又能保证代码的可靠性和可维护性。你可以以此为基础,定制属于自己的日志分析工具。

132:使用Cron自动化重复任务 🕐

在本节课中,我们将要学习如何使用Cron工具来自动化重复性的任务。Cron是一个在Unix-like系统中广泛使用的任务调度程序,它允许你安排脚本或命令在特定的时间间隔自动运行。这对于数据工程和DevOps中的自动化工作流至关重要。

什么是Cron? 🤔

上一节我们介绍了自动化任务的概念,本节中我们来看看Cron的具体工作机制。Cron是一个系统守护进程,它会根据预定的时间表自动执行命令。你无需手动启动它,当系统检测到Cron配置文件(crontab)存在时,它便会自动运行。

要查看Cron的手册说明,可以使用命令 man cron

如何配置Cron任务? ⚙️

Cron任务的配置是通过编辑一个名为“crontab”的特殊文件来完成的。每个用户都可以拥有自己的crontab文件。

要查看当前用户的Cron任务列表,可以使用命令 crontab -l。如果当前没有配置任何任务,该命令将不会显示任何内容。

要编辑当前用户的crontab文件,可以使用命令 crontab -e。这个文件有特定的格式要求。

理解Crontab格式 📝

Crontab的每一行代表一个任务,并且遵循一个由五个时间字段和一个命令字段组成的固定格式。

以下是crontab条目的基本结构:

* * * * * command_to_execute

这五个星号分别代表不同的时间单位,从左到右依次是:

  1. 分钟 (0 - 59)
  2. 小时 (0 - 23)
  3. 月份中的日期 (1 - 31)
  4. 月份 (1 - 12)
  5. 星期几 (0 - 7,其中0和7都代表星期日)

例如,配置 * * * * * 意味着该命令将在每一分钟执行。

配置示例与实践 🧪

让我们通过一些例子来具体了解如何配置。

一个简单的例子是,让Cron每分钟向一个文件写入一条测试信息。对应的crontab条目如下:

* * * * * echo "test from Cron" >> /home/alfredo/Desktop/fromCron.txt

这个配置表示:在每一分钟、每一小时、每一天、每一月、每一周的任何一天,执行后面的echo命令。

如果你想调整执行频率,比如改为每30分钟执行一次,可以这样配置:

30 * * * * echo "test every half an hour" >> /home/alfredo/Desktop/fromCron.txt

这个配置表示:在每个小时的第30分钟(例如1:30, 2:30)执行命令。

编辑并保存crontab文件后,新的任务就会被安装。你可以再次使用 crontab -l 命令来确认任务已成功添加。

使用在线工具验证Cron表达式 🌐

对于初学者来说,Cron表达式可能有些难以理解。幸运的是,有一些在线工具可以帮助我们。

一个非常有用的网站是 crontab.guru。你可以将你的Cron表达式(例如 30 * * * *)粘贴到该网站,它会给出清晰易懂的解释,告诉你这个配置具体代表什么时间执行(例如:“在每小时的第30分钟”)。

这对于编写和调试复杂的Cron计划非常有帮助。

将Cron与Rust结合使用 🔗

Cron的强大之处在于它可以触发任何系统命令或脚本。这意味着你可以轻松地将它与Rust程序结合使用。

例如,你可以编写一个Rust程序来执行数据清洗、日志分析或系统监控等任务。然后,在crontab中配置类似下面的条目,让这个Rust程序每小时运行一次:

0 * * * * /path/to/your/rust_program

这样,你就实现了一个完全自动化的、由Rust驱动的任务流水线。

总结 📚

本节课中我们一起学习了Cron自动化工具的核心用法。我们了解了Cron是一个系统调度守护进程,学会了通过 crontab -e 命令编辑任务计划,并掌握了由五个时间字段组成的crontab基本格式。通过具体示例,我们实践了如何配置每分钟或每半小时执行的任务。我们还介绍了 crontab.guru 这个实用的在线工具,它可以帮助我们理解和验证复杂的Cron表达式。最后,我们探讨了如何将Cron与Rust程序结合,从而构建强大的自动化工作流,这对于数据工程和DevOps实践至关重要。

133:外部工具包装策略

在本节课中,我们将学习如何在Rust程序中包装和调用外部命令行工具。我们将探讨相关的策略、技术、注意事项以及可能遇到的陷阱,目标是构建一个可靠的工具或库。

虽然你可以直接使用Rust或许多其他编程语言来执行任务,例如发送网络请求或连接外部API,但有时我们别无选择,只能包装现有的工具。这些现有工具可能是外部程序,例如Unix或Linux的实用程序。这些命令行工具执行非常具体的任务,不仅在Rust中,在任何编程语言中,要完成这些任务都可能很困难或非常繁琐。

因此,我想向你展示和探讨一些策略和技术。当你的目标是包装一个外部工具时,你可以应用这些策略和技术。这不仅仅是包装和调用那个外部工具,还包括尝试使用该外部工具的输出。当你走这条路时,需要考虑哪些事项?可能会遇到哪些潜在的陷阱?我已经多次做过这些事情,当然,我想告诉你应该注意哪些事情,同时也尝试专注于最终能拥有一个可靠的工具。它可能是一个库或一个命令行工具,包装了一个系统实用程序,你可以在任何有此需求的地方依赖、实现和使用它。

上一节我们介绍了包装外部工具的必要性,本节中我们来看看具体的策略和注意事项。

核心策略与考虑因素

以下是包装外部工具时需要考虑的几个核心方面。

1. 调用与执行

使用Rust的标准库std::process::Command来生成子进程并执行外部命令。这是与系统交互的基础。

use std::process::Command;

let output = Command::new("ls")
    .arg("-l")
    .arg("-a")
    .output()
    .expect("Failed to execute command");

2. 处理输出

外部工具的输出通常分为标准输出(stdout)和标准错误(stderr)。你需要决定如何处理它们:是捕获、重定向还是实时显示。

// 捕获输出
let output = Command::new("some_tool").output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

// 检查退出状态
if output.status.success() {
    println!("Success: {}", stdout);
} else {
    eprintln!("Error: {}", stderr);
}

3. 错误处理与健壮性

外部工具可能失败、挂起或返回意外的输出。你的包装器必须能优雅地处理这些情况,例如设置超时、解析非标准输出格式或处理部分成功。

4. 跨平台兼容性

不同的操作系统(如Linux、macOS、Windows)可能使用不同的工具或同一工具的不同版本。你的代码需要考虑这些差异,可能需要进行条件编译或提供回退方案。

5. 安全性

避免直接将未经过滤的用户输入传递给命令行,以防止命令注入攻击。始终对参数进行适当的转义或验证。

潜在陷阱

上一节我们介绍了核心策略,本节中我们来看看实施过程中可能遇到的常见陷阱。

以下是包装外部工具时可能遇到的一些主要挑战。

  • 退出代码解析:不同工具对成功和失败的退出代码定义可能不同(例如,有些工具用非零值表示警告而非错误)。你需要查阅工具的文档并相应处理。
  • 输出格式变化:工具的输出格式可能随版本更新而改变,导致你的解析逻辑失效。考虑使用更稳定的输出选项(如JSON格式)或编写适应性更强的解析器。
  • 环境依赖:外部工具可能依赖特定的环境变量、路径或系统库。你的包装器需要确保这些依赖在目标环境中得到满足,或提供清晰的错误信息。
  • 性能开销:频繁地启动和停止外部进程会产生显著的开销。对于需要高性能的场景,评估是否真的需要包装外部工具,或者是否有原生的Rust库可用。
  • 信号处理:确保你的Rust程序能正确处理中断信号(如Ctrl+C),并能够正确地终止它启动的子进程,避免产生僵尸进程。

总结

本节课中我们一起学习了在Rust中包装外部命令行工具的策略。我们了解到,虽然有时必须依赖外部工具,但通过谨慎地处理调用、输出、错误、兼容性和安全性,可以构建出可靠且健壮的包装器。记住要始终以防御性编程的心态来处理外部进程的交互,并充分测试你的代码在各种边界条件下的行为。

134:运行外部程序的复杂性概述 🧩

在本节课中,我们将要学习在Rust(以及其他编程语言)中运行外部程序时可能遇到的一系列复杂性问题。我们将以df命令为例,探讨不同系统间的输出差异、错误处理策略以及解析外部命令输出时面临的挑战。


运行外部程序通常涉及许多复杂性。当你使用像Rust这样的编程语言(其他语言也同样适用)去封装或调用外部工具时,会产生一系列预期之外的情况。

现在,我将运行df命令。我使用的是苹果电脑,如果你使用的是Linux机器或其他系统,输出会有所不同。这里展示的是命令的输出结果。我想说明的是,我们的系统可能会依赖像df这样的外部命令行工具来报告系统信息。

当然,存在一些实现了此功能的Rust库(crate)。但有时这些库的功能不足以满足需求,你可能因为某些特殊原因(例如需要提供与旧系统的兼容性)而不得不选择直接调用外部命令。这正是我今天要讨论的内容。几年前,我曾遇到一个情况:需要依赖一个系统命令行工具,而该工具在不同年代系统上的输出格式略有不同,我必须编写一个“粘合”层来规范化输出,无论系统是五年前的还是全新的。

因此,今天我将重点分析与df类似的输出,并探讨在使用此类策略(即封装外部命令)时需要考虑的问题。与直接使用现成库不同,你需要根据具体用例做出一些决策。


接下来,我将关闭终端,首先向你展示main.rs文件。它将接受一个路径参数(该参数是可选的,因为可以传递空字符串)。这个程序的功能是调用一个库,并生成JSON输出。

现在,我将转到库文件lib.rs的顶部。这里展示了我们将要进行的解析工作。我暂时不会深入细节,但我想指出复杂性和你需要做出的决策之一:当命令执行出错或输出不兼容时会发生什么?因为你需要进行解析。

在这个例子中,库的目标是提供一个JSON接口。它会解析df输出的每一行并生成JSON。那么,当输出不正常时,你必须考虑到这一点。在我过去的经验中,我的观点是:解析输出时,有一个运行命令的Rust函数。这个函数有两种可能的结果:要么成功获得命令运行的正确结果,要么遇到错误。

此时,你几乎需要做出一个业务逻辑上的决策:是让错误中断整个程序,还是允许程序继续执行?在我当时工作的系统中,完全中断是不可接受的,因此我必须让程序继续运行。实现这一点的方法是,修改那个运行外部命令的函数,使其能够处理输出可能为空的情况。如果输出为空,就什么都不做。然而,如果输出损坏或命令执行被中断,情况就会变得棘手。

这对我当时的情况是正确的处理方式。但你的策略可能不同,你可能认为一旦此处出错,就必须终止执行。这些都是你需要考虑的因素。


最后,我想谈谈解析本身。解析绝对会让事情变得复杂。我最初展示的是类Linux系统的输出,而我MacOS系统上df命令的输出看起来非常不同,列数更多。因此,解析技术必须考虑到系统间的差异。如果你打算让工具在不同系统上运行,就必须防范因解析完全失败而导致值被放入错误字段的灾难性情况。就像这里展示的,代码正在消费输出的每一部分并将其强制转换为字符串以捕获信息。

本例中,我只关注Linux输出。但在为不同系统进行解析时,你肯定可以采用其他策略。


综上所述,当你开始在系统中调用其他可执行文件时,可能会面临上述困难,并且必须做出相应的设计决策。本节课我们一起学习了运行外部程序时的核心挑战,包括系统差异、错误处理逻辑以及输出解析的复杂性。理解这些是构建健壮命令行工具集成功能的基础。

135:解析系统命令输出的策略 🛠️

在本节课中,我们将要学习在Rust应用程序中包装和解析外部命令输出时,需要考虑的复杂性和可以采取的策略。我们将通过一个包装df命令的示例来探讨如何设计更健壮、更灵活的代码。

概述

解析外部命令的输出是一项复杂的任务。我们之前已经讨论过一些复杂性,本节将重点介绍处理这类问题时可用的具体策略。这些策略的选择取决于你的项目是命令行工具还是库,以及你对输入输出的控制程度。

策略一:区分应用与库的设计

上一节我们介绍了解析外部命令的复杂性,本节中我们来看看第一个核心策略:根据项目类型(独立应用 vs 库)做出不同的设计决策。

在示例中,我们有一个包装df命令的Rust命令行工具。它解析选项和参数,例如path是一个默认为空字符串的标志。这种设计方式与创建一个供其他工具使用的库有所不同。

如果你在构建一个库,你需要为调用者提供更多灵活性来处理错误和输出。而在构建一个独立的一体化应用程序时,你可以根据自身需求做出更直接的选择。

以下是示例中运行命令的函数,它直接返回一个字符串并内部处理错误:

fn run_command() -> String {
    // ... 执行命令的逻辑
    // 内部处理错误,总是返回一个字符串
}

这种设计对于独立应用可行,但对于库则不理想,因为它剥夺了调用者按自己意愿处理错误的能力。

策略二:将解析逻辑与命令执行分离

将解析逻辑从实际运行命令的函数中分离出来是一个好主意。这样做可以让你专注于处理输入(可能是一个缓冲区),而不必关心系统命令的调用细节。

这种方法也使测试变得更容易,因为你可以直接使用字符串作为输入进行测试,而无需调用真实的系统命令。

在示例中,parse_df函数专门负责解析df命令的输出字符串:

fn parse_df(output: &str) -> JsonValue {
    // 专注于解析字符串逻辑
}

策略三:考虑跨平台兼容性

如果你需要解析来自不同系统的、略有差异的命令输出,你可能需要为不同操作系统(例如macOS和Linux)准备单独的解析实现。

这允许你在后端根据不同的系统进行相应的处理,是构建健壮工具时需要考虑的一个重要方面。

策略四:评估解析的“脆弱性”与健壮性

解析外部来源的输出可能非常脆弱。在示例中,解析器天真地基于空白字符进行分割:

let parts: Vec<&str> = line.split_whitespace().collect();

这种方法在简单情况下有效,但如果任何字段(例如文件路径)包含空格,解析就会失败。

因此,你需要分析你处理的输出类型,并确保你的假设是正确的。如果可能包含空格,你就需要更健壮的解析策略,例如使用正则表达式或专门针对该命令输出格式的解析器。

需要考虑的关键点

以下是设计命令包装器时需要权衡的一些关键因素:

  • 错误处理:决定是在内部处理错误,还是将错误返回给调用者处理。
  • 输入验证:确保传递给系统命令的参数是安全且格式正确的。
  • 输出假设:明确你对命令输出格式所做的所有假设,并评估其稳定性。
  • 测试便利性:设计易于测试的代码结构,例如将纯解析逻辑分离出来。

总结

本节课中我们一起学习了在Rust中包装和解析系统命令输出的几种核心策略。我们了解到,设计决策应根据项目是应用还是库而有所不同;将解析逻辑与命令执行分离能提高代码的可测试性和清晰度;需要考虑跨平台兼容性;并且必须谨慎评估解析逻辑的健壮性,避免因对输出格式的天真假设而导致程序崩溃。通过应用这些策略,你可以构建出更可靠、更易维护的命令行工具或库。

136:避免路径相关问题 🛠️

在本节中,我们将探讨如何避免因系统路径(PATH)设置不一致而导致的外部命令调用问题。我们将学习如何以更稳健的方式定位可执行文件,确保程序在不同环境中都能可靠运行。

概述

在编写调用外部命令的程序时,直接依赖系统的 PATH 环境变量可能存在风险。因为 PATH 的设置可能因系统、用户或环境而异。本节将介绍一种方法,通过程序化地在多个已知系统路径中搜索,来可靠地定位可执行文件,从而增强程序的健壮性。

问题分析

上一节我们讨论了如何解析命令输出。本节中我们来看看调用命令本身可能遇到的问题。在演示中,命令 IDf 被使用,而非标准的 df。这是因为为了示例的一致性,作者在系统中“伪造”了这个可执行文件。

这里揭示了一个关键问题:程序依赖于 PATH 环境变量来查找可执行文件。让我们查看一下终端中的实际情况:

which IDf
# 输出可能类似于:/home/user/bin/IDf

which df
# 输出可能类似于:/bin/df

PATH 变量包含许多目录。如果程序化地访问可执行文件时完全依赖 PATH,那么在路径设置不同或异常的远程系统上,程序可能会失败。

解决方案:实现稳健的路径查找

为了避免上述问题,我们应该实现一个自己的查找函数,而不是单纯依赖 PATH。这个函数将在一组我们指定的、常见的系统路径中搜索可执行文件。

以下是实现此功能的核心思路:

  1. 定义一个函数,接受一个命令名(如 "df")作为参数。
  2. 准备一个包含常见系统路径(如 /bin/usr/bin/usr/local/bin)的列表。
  3. 遍历这个列表,将每个路径与命令名组合成完整的文件路径。
  4. 检查该完整路径对应的文件是否存在。
  5. 如果找到,则返回该路径;如果遍历完所有已知路径都未找到,则回退到直接使用命令名(依赖系统的 PATH 解析)。

以下是该函数的一个概念性代码实现:

fn which_command(command: &str) -> String {
    // 定义一组常见的系统路径
    let acceptable_paths = vec!["/bin", "/usr/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];

    // 遍历这些路径
    for path in acceptable_paths {
        // 组合成完整路径
        let full_path = format!("{}/{}", path, command);
        // 检查文件是否存在(此处为逻辑描述,实际需使用std::fs::metadata等)
        if path_exists(&full_path) { // 假设的检查函数
            return full_path;
        }
    }

    // 如果在已知路径中未找到,则返回原始命令名,交由系统PATH处理
    command.to_string()
}

代码说明

  • acceptable_paths:一个字符串向量(Vec<&str>),包含了我们想要搜索的目录。
  • 循环:使用 for 循环遍历 acceptable_paths 中的每一个路径。
  • format!("{}/{}", path, command):这是一个宏,用于将路径和命令名拼接成一个完整的文件路径字符串。
  • 回退机制:如果循环结束(即所有已知路径中都没有找到该命令),函数则返回输入的命令字符串。这样,调用方仍然可以尝试通过系统的 PATH 来解析它,作为最后的手段。

这种方法的好处是,它首先在我们信任的、标准的系统位置中查找,这通常比依赖可能被用户修改的 PATH 变量更可靠。如果这些位置都没有,它才会降级到依赖 PATH

总结

本节课中我们一起学习了如何避免因系统 PATH 环境变量不一致而引发的程序问题。关键要点是:不要盲目相信 PATH,而是通过编程方式在关键的、预定义的系统路径中主动查找可执行文件。我们实现了一个 which_command 函数的框架,它优先搜索常见目录,未找到时才回退到系统解析,这能显著提升调用外部命令的代码的健壮性和可移植性。结合之前学习的稳健解析技术,这有助于构建出更可靠的、与系统交互的Rust程序。

137:错误报告与处理技术

在本节课中,我们将学习在包装外部命令或系统命令时,如何建立一套坚实、健壮的错误处理策略。核心目标是确保在发生故障时,我们能够清晰地理解发生了什么。

捕获执行的命令

上一节我们介绍了错误处理的重要性,本节中我们来看看具体的技术。首先,一个始终推荐的做法是捕获实际运行的命令。这在命令包含多个参数或标志时尤其有用,因为错误可能源于特定的输入组合。

以下是实现此功能的关键步骤:

  • 记录完整命令:当调用外部命令(如 df)时,不仅记录错误信息,还要记录完整的命令行字符串,包括所有参数和标志。
  • 辅助调试:当错误发生时,这为开发者提供了明确的上下文,便于复现和定位问题。

例如,在代码中,当命令执行失败时,我们可以这样输出:

eprintln!("Command failed: {}", actual_command_string);

捕获并解析错误输出

仅仅知道命令是什么还不够,我们还需要知道系统或外部命令返回的具体错误信息。这有助于区分是路径错误、权限问题还是其他类型的故障。

以下是处理错误输出的方法:

  • 检查退出状态:使用 std::process::Command.status().output() 方法获取命令的退出状态码。
  • 捕获标准错误:通过 .stderr 捕获命令输出的错误信息流。
  • 提供清晰反馈:将原始错误信息与上下文(如失败的命令)一起呈现给用户或日志。

例如,尝试访问一个无效路径 /invalid 时,程序不仅会报告“命令失败”,还会显示类似 df: ‘/invalid’: No such file or directory 的系统错误信息。

实现健壮的输出处理

程序在成功时会产生格式化的输出(如JSON),但在失败时,我们需要确保错误信息、日志和正常输出不会混杂在一起,造成混乱。本节我们来优化失败时的输出呈现。

一个有效的策略是将详细的调试信息记录到文件中,而不是直接打印到用户终端。

以下是实现日志文件策略的步骤:

  • 添加日志开关:为命令行工具添加一个标志(例如 --log-file),用于启用文件日志功能。
  • 分离输出流:用户看到的终端输出保持简洁(如“命令执行失败”),而详细的命令、错误堆栈等信息则写入指定的日志文件。
  • 始终可选:你可以选择始终启用文件日志,也可以将其作为调试选项。

例如,运行工具时使用 cargo run -- --log-file,详细的错误信息将被写入一个独立的文件(如 rdf.log),而用户界面保持整洁。

总结

本节课中我们一起学习了构建健壮命令行工具的几种关键错误处理技术。我们首先强调了捕获完整执行命令的重要性,以便在出错时提供清晰的上下文。接着,我们探讨了如何捕获并解析外部命令的错误输出,以准确诊断问题根源。最后,我们介绍了通过实现可选的日志文件功能来分离用户输出和调试信息,从而提升工具的可用性和可维护性。将这些技术结合起来,可以显著改善工具在故障情况下的行为,使其更易于调试和使用。

138:用于错误报告的文件日志记录 📝

在本节课中,我们将学习如何在Rust程序中实现文件日志记录,以增强错误报告和调试能力。我们将看到如何将调试信息输出到日志文件,而不是直接打印到终端,这对于生产环境或需要结构化输出的工具至关重要。

上一节我们介绍了处理外部命令执行的基本错误,本节中我们来看看如何通过日志记录来捕获更详细的调试信息。

添加调试日志

首先,我们可以在代码中导入 log 库的 debug 宏,用于记录调试信息。这允许我们捕获程序执行过程中的详细信息,而不会干扰主输出。

以下是添加调试日志的步骤:

  1. 导入 log 库的 debug 宏。
  2. 在关键执行点(如命令拆分、数据处理)添加 debug! 语句。
  3. 将日志输出重定向到文件,而不是标准输出。

例如,我们可以记录原始命令及其拆分后的参数向量,以确保命令解析正确:

use log::debug;

// 记录原始命令字符串
debug!("Raw command: {}", raw_command);
// 记录拆分后的命令参数
debug!("Raw command split: {:?}", args);

记录特定操作

除了命令本身,记录程序流程中的特定决策也很有帮助。例如,在处理数据行时,我们可以记录跳过空行或标题行的操作。

以下是记录特定操作的示例:

  • 当遇到空行时,记录 "Skipping empty line"
  • 当跳过标题行时,记录 "Skipping header line"
  • 当命令输出为空时,记录 "Output from command is empty"

这些日志条目有助于在事后审查时理解程序的行为和决策路径。

配置日志输出

默认情况下,debug! 宏的日志可能不会显示。我们需要配置日志系统的详细程度(verbosity)。可以通过环境变量或命令行参数(如 --debug--info)来控制日志级别。

更重要的是,我们可以将日志输出定向到一个文件。这样做的好处是:

  • 避免调试信息污染工具的标准输出(例如JSON输出)。
  • 为生产环境故障排查保留完整的执行上下文。
  • 实现日志信息的持久化。

运行与验证

完成代码修改后,运行程序并检查日志文件。你应该能看到添加的所有 debug! 信息被捕获到指定的日志文件中。同时,工具的主输出(如处理后的数据)将保持干净,不受调试文本的影响。

这种模式——将详细的调试信息写入日志文件,同时保持标准输出的整洁——对于构建需要被其他系统消费的健壮工具至关重要。

本节课中我们一起学习了如何利用 log 库在Rust程序中实现文件日志记录。关键点包括:使用 debug! 宏添加调试信息、将日志输出重定向到文件、以及通过配置日志级别来控制信息详细程度。这种方法能有效提升错误报告的质量和程序的可调试性,尤其是在处理外部命令或构建数据管道时。

139:引言

在本节课中,我们将学习如何将之前掌握的Rust知识应用于一个实际的现实世界用例。通过构建一个合规性检查工具,我们可以展示Rust在系统工程和DevOps领域的能力。

将我们迄今为止所学的所有内容整合到一个实际的现实世界用例中,是展示Rust能力的最佳方式。当你尝试用系统工程方法实现某些功能时,合规性正是系统工程和DevOps领域中一个典型的应用场景。在这个场景中,你可能需要尝试查询、发现或调查系统的当前状态包含哪些方面。

合规性包含许多不同的方面,听起来可能有些令人望而生畏。但我们将通过一些具体用例来理解它,例如检查某个文件是否存在,或者检查文件是否拥有某些本不该拥有的权限。我们将看到一些现实世界的案例。

每当文件拥有错误的权限时,系统就可能停止工作。实际上,有些措施可以防止这种情况发生。这一点很重要,因为例如,如果一个文件的权限设置得过于开放或宽松,就可能成为安全威胁利用你系统的突破口。

因此,确实需要采取一些措施。构建一个合规性检查工具,无疑是尝试运用我们已学知识(例如遍历文件系统)的一个有趣途径。同时,我们也将使用Rust来驾驭这项任务。我们将看到如何实现这一目标。


上一节我们介绍了本课程的目标和合规性检查的背景。本节中,我们来看看构建此类工具将涉及的核心概念和步骤。

以下是构建一个基础合规性检查工具可能涉及的主要环节:

  • 文件系统遍历:使用标准库(如 std::fs)递归地访问目录和文件。
  • 元数据检查:通过 std::fs::metadata 获取文件的权限、所有者等信息。
  • 权限判断:分析文件的权限位(例如,在Unix系统上使用 std::os::unix::fs::PermissionsExt),判断其是否符合安全策略。
  • 结果报告:将发现的问题(如权限过宽的文件)记录并输出。

本节课中,我们一起学习了将Rust应用于现实世界系统工程问题——特别是合规性检查——的重要性。我们概述了通过检查文件权限等具体用例来构建工具的基本思路,并预览了即将涉及的技术要点。在接下来的章节中,我们将深入每个环节的具体实现。

140:合规性应用场景

概述

在本节课中,我们将探讨合规性在DevOps操作中的实际应用场景。我们将通过一个具体的SSH密钥权限管理的例子,来理解合规性检查的必要性和实现方式。您将看到,确保系统配置符合特定规则,对于维护系统安全和稳定至关重要。

SSH密钥权限管理

上一节我们介绍了合规性的基本概念,本节中我们来看看一个具体的应用实例:SSH密钥的权限管理。

SSH是一种允许您远程登录到其他系统的工具。例如,我有一台名为“Mac mini server”的服务器,我可以使用ssh命令登录到那台服务器。登录后,我的命令行提示符会改变,表明我已不在本地计算机上。

那么,合规性在何处发挥作用呢?关键在于与SSH相关的配置文件权限。在我的家目录下,有一个隐藏的子目录.ssh,它存放着所有SSH配置,包括我的私钥、公钥以及我使用SSH时连接过的主机信息。

如果我现在查看这些文件的权限,会发现我的私钥文件(例如id_rsa)的权限设置非常特殊。它只允许文件所有者(也就是我本人)进行读写操作,而我所在的用户组或其他用户则没有任何权限(读、写或执行)。

权限修改与合规性警告

现在,如果我修改这个私钥文件的权限,让其他用户也能读取它,问题就会出现。

以下是操作步骤和结果:

  1. 使用命令 chmod o+r id_rsa 为“其他用户”添加读取权限。
  2. 使用 ls -alh 命令查看,会发现私钥文件的权限现在对所有用户都是可读的。
  3. 当我尝试再次SSH连接到我的Mac mini服务器时,系统会立即发出警告。

警告信息明确指出:私钥文件未受保护,其权限(0644)过于开放,这意味着其他用户可以读取它。SSH要求私钥文件不能被其他人访问,因此这个私钥将被忽略,登录尝试会失败。

这种情况在我创建、移动、修改密钥或进行某些更改时多次发生。因此,此处的合规性就是指确保某些文件(如SSH私钥)具有特定的、安全的权限。

要修复这个问题,我们需要撤销错误的权限设置:

  1. 使用命令 chmod o-r id_rsa 移除其他用户的读取权限。
  2. 确保所有者拥有读取权限:chmod u+r id_rsa
  3. 再次使用 ls -alh 检查,确认权限已恢复正确。
  4. 此时,SSH连接就能正常工作了。

这就是合规性的一个方面:我可以编写检查程序,来强制确保某些文件具有特定的权限。通常对于脚本或配置文件,您可能希望创建某种机制来保证它们具有正确的权限,否则程序可以发出警告,指出这些配置不符合规定,存在需要修复的问题。自动修复这些问题需要更高级的自动化工具,但仅实现检查功能已足以帮助我们理解系统状态。

文件存在性与软件安装检查

除了检查文件权限,合规性检查的另一个常见方面是验证特定文件是否存在。

例如,进入/etc目录,可以看到大量不同的配置文件。您可以设定一个规则:系统中必须始终存在一个nfs.conf文件。如果该文件不存在,系统可能会遇到问题。

您可以有效地编写一个工具来执行此检查,确保某些关键文件存在。在过去,为了避免依赖包管理器并执行一些系统检查,我常常会进入/etc目录,查找某个必须安装的软件对应的配置文件。如果该文件不存在,就意味着该软件无法正常工作。

因此,检查必需文件是否存在,是合规性检查能为您完成的另一项有趣且有用的任务。这也正是人们对构建、实现这些检查规则,并将其添加到所管理的系统中感兴趣的原因。

总结

本节课中,我们一起学习了合规性在DevOps中的两个核心应用场景。

  1. 权限合规:通过SSH私钥的例子,我们看到了确保敏感文件(如id_rsa)具有严格权限(如600)的重要性。不符合规的宽松权限会导致安全警告和功能失效。
  2. 存在性合规:通过检查/etc目录下特定配置文件(如nfs.conf)是否存在,我们可以间接验证关键软件是否已正确安装,从而预防因缺失组件导致的系统问题。

这些检查是构建自动化、可靠且安全系统的基础步骤。

141:在Rust中使用JSON 🦀

在本节课中,我们将学习如何在Rust程序中处理JSON数据。我们将通过一个具体的例子,了解如何从JSON文件中读取数据,并将其反序列化为Rust的结构体,以便在程序中使用。


概述

JSON是一种常用的数据交换格式。在Rust中,我们可以使用 serdeserde_json 这两个库来轻松地序列化和反序列化JSON数据。本节教程将演示如何定义一个Rust结构体,从JSON文件中加载数据,并将其转换为Rust类型。


添加依赖

首先,我们需要在项目中添加必要的依赖。serdeserde_json 是处理JSON的常用库。serde 的特性允许我们为结构体和类型添加属性,使其能够序列化和反序列化。

Cargo.toml 文件中添加以下依赖:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

定义数据结构

接下来,我们将定义一个Rust结构体来表示JSON数据的格式。这个结构体将包含路径、正则表达式、文件权限和必需的文件列表。

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct ComplianceRule {
    path: String,
    regex: String,
    file_permissions: String,
    required_files: Vec<String>,
}

在这个结构体中:

  • path 表示文件路径。
  • regex 是一个正则表达式。
  • file_permissions 表示文件权限。
  • required_files 是一个字符串向量,表示必需的文件列表。

加载JSON数据

现在,我们将编写一个函数来加载JSON数据并将其反序列化为Rust结构体。假设JSON数据存储在一个名为 rules.json 的文件中。

以下是加载JSON数据的步骤:

  1. 读取JSON文件内容。
  2. 使用 serde_json 将JSON字符串反序列化为Rust结构体。
use std::fs;

fn load_rules() -> Vec<ComplianceRule> {
    // 读取JSON文件内容
    let json_data = fs::read_to_string("rules.json")
        .expect("无法读取 rules.json 文件");

    // 反序列化JSON数据
    let rules: Vec<ComplianceRule> = serde_json::from_str(&json_data)
        .expect("JSON 反序列化失败");

    rules
}

使用加载的数据

加载数据后,我们可以在程序中使用这些规则。例如,我们可以在主函数中调用 load_rules 函数并打印加载的规则。

fn main() {
    let rules = load_rules();

    // 打印加载的规则
    for rule in rules {
        println!("路径: {}", rule.path);
        println!("正则表达式: {}", rule.regex);
        println!("文件权限: {}", rule.file_permissions);
        println!("必需文件: {:?}", rule.required_files);
        println!("---");
    }
}

JSON文件示例

为了确保程序正常工作,我们需要一个JSON文件。以下是一个示例 rules.json 文件的内容:

[
  {
    "path": "/etc",
    "regex": ".*\\.conf$",
    "file_permissions": "644",
    "required_files": ["passwd", "group"]
  },
  {
    "path": "/var/log",
    "regex": ".*\\.log$",
    "file_permissions": "640",
    "required_files": ["syslog", "auth.log"]
  }
]

运行程序

现在,我们可以运行程序来验证JSON数据是否被正确加载和反序列化。在终端中执行以下命令:

cargo run

如果一切正常,程序将输出加载的规则信息。


总结

在本节课中,我们一起学习了如何在Rust中使用JSON数据。我们通过以下步骤实现了JSON数据的加载和反序列化:

  1. 添加 serdeserde_json 依赖。
  2. 定义Rust结构体来表示JSON数据的格式。
  3. 编写函数加载JSON文件并将其反序列化为Rust结构体。
  4. 在主函数中使用加载的数据。

通过这种方式,我们可以轻松地将外部JSON数据集成到Rust程序中,从而实现更灵活的数据管理和业务逻辑分离。

142:构建合规性程序

在本节课程中,我们将学习如何构建一个合规性检查程序。我们将深入探讨一个具体的程序实现,该程序会读取一组规则,并使用这些规则来检查文件系统中的文件是否符合规定。我们将重点关注如何将规则应用到文件遍历中,并优化代码结构。

概述

我们将构建一个程序,它从JSON文件加载合规性规则,然后遍历指定目录下的文件,检查每个文件是否满足规则要求。核心改动包括将规则中的路径匹配从正则表达式改为Glob模式,并重构主函数逻辑以提高可读性和可维护性。

规则定义与Glob模式

首先,我们需要修改规则的数据结构。在之前的实现中,我们使用正则表达式来匹配文件路径。现在,我们将其改为使用Glob模式。Glob模式是一种类似正则表达式的模式匹配语法,常用于匹配文件名和路径,并且在Shell终端中可以直接展开。

以下是规则结构体的定义,注意 path_regex 字段名虽未改,但其含义已变为Glob模式:

// 规则结构体示例
struct ComplianceRule {
    path_regex: String, // 现在这里存储的是Glob模式
    required_permissions: u32,
    required_files: Vec<String>,
}

为了在Rust中使用Glob模式,我们需要在 Cargo.toml 中添加 glob 库作为依赖:

[dependencies]
glob = "0.3"

主程序逻辑解析

现在,让我们逐行分析主函数 main.rs 中的逻辑。程序首先加载所有规则,然后遍历每条规则。

以下是主逻辑的核心步骤:

  1. 加载规则:调用 load_rules 函数,返回一个 ComplianceRule 的向量(Vec)。
  2. 遍历规则:对于向量中的每一条规则,执行检查。
  3. 应用Glob模式:使用 glob 库根据规则中的Glob模式来匹配和遍历文件。
  4. 检查每个文件:对于匹配到的每一个文件条目(entry),检查其权限。
  5. 报告结果:只打印出检查失败(权限不符)或出现错误的文件信息,忽略通过检查的文件。

主函数中的循环结构大致如下:

for rule in rules {
    let pattern = &rule.path_regex; // 这是Glob模式字符串
    for entry in glob(pattern).expect("Failed to read glob pattern") {
        match entry {
            Ok(path) => {
                // 检查文件权限,如果失败则打印路径
                if !check_permissions(&path, rule.required_permissions) {
                    println!("FAIL: {:?}", path);
                }
            }
            Err(e) => {
                // 打印遍历文件时出现的错误
                println!("ERROR: {}", e);
            }
        }
    }
}

代码优化与清理

在当前的实现中,主函数包含了较多的逻辑。良好的实践是保持 main 函数简洁,将具体的业务逻辑委托给其他辅助函数或库。

我们需要进行以下清理工作:

  • 将文件检查和规则处理的逻辑移出 main 函数。
  • 修复“必需文件”检查的逻辑。目前的实现可能不正确,因为它可能需要在遍历所有文件之前或之后单独执行一次,而不是对每个文件条目都检查。
  • 移除调试用的冗余打印语句,确保输出只包含合规性检查失败或错误的信息。

运行示例与问题排查

当我们运行程序 cargo run 时,可能会看到大量失败报告。这通常是由于规则设置得过于严格或不切实际。

例如,一条规则可能要求 /etc 目录下的所有文件权限都是 420(即八进制的644),这在大多数系统上是不现实的。另外,规则中要求必须存在的文件(如 passwordgroup),如果系统中没有,也会导致检查失败。

这些问题并不代表程序逻辑有误,而是反映了规则定义与实际环境的差异。程序正确地执行了规则检查并报告了不符合项。

总结

本节课我们一起学习了如何构建一个基本的文件系统合规性检查程序。

我们首先将规则中的路径匹配从正则表达式切换为更适用于文件匹配的Glob模式,并引入了 glob 库。然后,我们详细解析了主程序的逻辑,它遍历规则,使用Glob模式匹配文件,并检查每个文件的权限。我们还讨论了优化代码结构的重要性,比如将逻辑从 main 函数中分离出来。最后,通过运行示例,我们看到了程序如何工作以及如何根据输出排查规则定义的问题。

这个实现提供了一个坚实的框架,你可以在此基础上增加更复杂的检查逻辑,如检查文件内容、所有者或修改时间等,以构建更强大的合规性工具。

143:改进报告逻辑 🛠️

在本节课中,我们将学习如何改进一个Rust工具的合规性检查逻辑。该工具旨在根据特定规则检查目录中是否存在必需的文件。目前,工具的报告逻辑存在重复输出问题,我们将通过重构代码来解决它。

问题分析

上一节我们介绍了工具的基本结构和规则检查逻辑。本节中我们来看看当前实现中存在的问题。

我们的工具旨在基于某些规则强制执行合规性检查。我们需要处理这里的循环逻辑。回顾一下,如果我们回到相邻的规则部分,我们的目标是检查每个目录,并确保某些必需文件存在。例如,在Etsy目录中,这些文件应该存在。

当前的情况是,由于使用了glob模式进行扩展,对于每条规则,它都会为每个文件进行检查。例如,它会寻找passwordgroup文件。当然,这些文件并不存在。如果文件不存在,它就会为每个文件报告失败。

如果我们打开终端并运行cargo run,就会看到这个行为。

让我们看一下。我们会看到“password not found”和“group not found”的信息。你会看到这个模式一遍又一遍地重复,实际上到处都是。

这不是我们想要的结果。如果你要报告这些缺失,不应该为每个文件都报告,因为这些是Etsy文件。你会看到group被一遍又一遍地报告。

解决方案

所以,让我们修复这个问题,改进我们的工具。在开发这些工具并取得进展的过程中,输出结果不完全符合你的期望或需求是非常常见的情况。

即使这是一个演示,并且我正在尝试解释如何将这些概念付诸实践,当事情不太对劲时,你也不应该感到不知所措或沮丧。实际上,这是一种重要的学习方式,特别是在处理Rust和逻辑时,因为它能让你更好地理解问题。

我们知道,问题出在match语句内部的这个循环上。

这里的主要问题是,因为我们正在处理一个glob模式,我们需要理解它的根目录是什么。如果我们使用Etsy/*,它肯定会展开。

以下是改进逻辑的一种方法。

实施改进

你可以通过捕获所有已检查的文件来修复并改进这个问题。实现方式是为每条规则创建一个向量来收集文件。

让我们为每条规则捕获所有的文件。因为这将使我们能够看到所有捕获到的内容,并在之后检查我们想要的文件是否存在。

所以,让我们在这里创建一个可变的向量。

let mut captured_files = Vec::new();

通过这种方式,我们可以在遍历文件时将它们添加到向量中,然后在所有文件检查完毕后,再根据规则验证必需文件是否存在,从而避免重复报告。

总结

本节课中我们一起学习了如何识别和修复Rust工具中重复报告的问题。我们分析了问题根源在于循环和glob模式的结合使用,并提出了通过收集文件列表再进行统一验证的解决方案。这种方法不仅解决了重复输出的问题,也使代码逻辑更加清晰,为后续的功能扩展打下了良好的基础。

144:程序报告策略 🚀

在本节课中,我们将学习如何让Rust程序更好地与其他系统(如CI/CD流水线)集成。核心在于理解并实现两种报告策略:通过退出状态码向系统报告程序执行结果,以及通过结构化输出(如JSON) 向其他程序传递数据。

上一节我们介绍了如何编写一个合规性检查工具。本节中我们来看看如何让这个工具的输出能被自动化系统有效识别和处理。

退出状态码:程序与系统的暗号

当程序结束时,它会向操作系统返回一个整数,称为退出状态码。在Unix-like系统中,退出状态码为0通常表示成功,任何非零值都表示某种类型的失败。

在终端中,我们可以通过 $? 变量来检查上一个命令的退出状态码。

echo $?

CI/CD系统严重依赖这个状态码。如果程序返回非零状态码,系统通常会认为任务执行失败并中止后续流程。即使程序在终端打印了“失败”信息,只要退出码是0,系统仍会认为它成功了。

以下是演示退出状态码的一个例子:

ls /不存在的路径
echo $? # 输出可能是 1 或 2,表示命令执行出错

在Rust程序中实现退出状态码

为了让我们的合规性检查工具能被CI/CD系统正确识别,我们需要在检查到失败时返回非零退出码。

我们可以在main函数中引入一个布尔变量(例如failed)来跟踪是否发生了任何失败。在遍历检查规则时,一旦发现违规,就将failed设置为true。在所有检查完成后,根据failed的值决定退出状态。

核心实现代码如下:

let mut failed = false;

// ... 在检查循环中
if /* 检查失败 */ {
    failed = true;
}

// ... 所有检查结束后
if failed {
    std::process::exit(1); // 或 exit(2) 等不同的错误码
}

通过这种方式,我们的工具在发现任何合规性问题时,都会以非零状态码退出,从而向CI/CD系统明确报告失败。

结构化输出:超越终端文本

除了退出状态码,另一种更强大的集成方式是生成结构化数据输出,例如JSON。纯文本日志对于人类阅读是友好的,但对于其他程序进行自动化解析则可能很棘手且容易出错。

JSON作为一种通用的数据交换格式,可以被几乎所有编程语言轻松解析。如果我们的工具将检查结果输出为JSON格式,那么其他系统(如监控仪表盘、报告系统)就可以直接消费这些数据,而无需进行复杂的文本解析。

想象一下,我们的工具输出如下结构的JSON:

{
  "scan_time": "2023-10-27T10:00:00Z",
  "target": "some_vm_image",
  "compliance_passed": false,
  "violations": [
    {
      "rule": "file_permission",
      "file": "/etc/passwd",
      "expected": "644",
      "actual": "777",
      "severity": "critical"
    }
  ]
}

这样,任何能够处理JSON的系统都可以轻松获取、分析和报告这些合规性数据,极大地提升了工具的可用性和集成度。

总结

本节课中我们一起学习了两种关键的程序报告策略。

首先,我们了解了退出状态码的重要性,并学会了如何在Rust程序中使用std::process::exit()来返回特定的状态码,以便与CI/CD等自动化系统进行有效通信。

其次,我们探讨了结构化输出(特别是JSON)的优势。通过输出机器可读的格式,我们的程序能够更轻松地与其他工具和系统集成,为构建数据管道和自动化报告铺平了道路。

结合使用这两种策略,可以使你的Rust程序从一个独立的命令行工具,转变为一个能够无缝嵌入到现代软件工程生态系统中的强大组件。

145:CICD平台引言

在本节课中,我们将要学习持续集成与持续交付平台的基础概念。我们将了解为何自动化是软件开发的关键,并初步认识CICD平台的核心组件。

概述

使用CICD平台是增强自动化能力的起点。当然,你可以编写一个小脚本并手动运行它来实现某种自动化,以替代之前的手动任务。但持续集成和持续交付平台能让你将自动化提升到新的水平。

在大多数情况下,当我们讨论将软件提交或部署到生产环境时,必须经历许多不同的步骤。我曾经历过不得不手动执行这些步骤的情况,因为当时所在的公司或团队没有自动化或CICD平台。这很棘手,因为存在遗漏某些步骤、步骤顺序出错的风险,最终可能导致比投入时间搭建平台更大的麻烦。

CICD平台的能力

一个CICD平台允许你完成许多不同的事情。例如,它可以进行部署,同时也能自动化与代码质量相关的任务。它将增强你的拉取请求和代码审查流程,并确保项目符合你的实际需求。

核心组件简介

接下来,我们将看到CICD平台的一些基础和入门组件。我们将探讨什么是作业,什么是流水线,以及这两者之间的区别。归根结底,这些概念并不复杂,但由于人们经常使用许多术语,有时甚至互换使用,例如用“作业”和“流水线”、“脚本”和“CI”或“CD”,所以也容易让人感到困惑。

以下是CICD平台中的两个核心概念:

  • 作业:一个作业定义了要执行的一系列步骤或任务。它通常是一个独立的执行单元。
  • 流水线:一个流水线由多个作业组成,定义了这些作业的执行顺序和依赖关系,代表了从代码提交到部署的完整自动化流程。

总结

本节课中,我们一起学习了CICD平台的基本概念及其重要性。我们了解到,CICD平台是自动化软件交付流程、保证代码质量和减少人为错误的基础工具。下一节,我们将深入探讨如何配置和使用这些组件。

146:什么是CI/CD 🚀

在本节课中,我们将要学习CI/CD(持续集成/持续交付)的核心概念。我们将了解它是什么,为什么它对现代软件开发至关重要,以及它如何将手动、易错的过程转变为自动化、可靠的流程。

从手动流程说起

上一节我们介绍了CI/CD的重要性,本节中我们来看看一个典型的软件开发流程是如何开始的。

一切始于你的源代码。以Rust项目为例,你需要执行许多步骤来完成任何工作。通常,你会经历一系列手动步骤,例如运行 cargo runcargo build,检查一切是否正常工作。如果出现问题,你就返回去修改代码。这些操作本身没有问题,但关键在于,这一切都是手动的

任何你看到的、需要手动执行的步骤,都是构建自动化的机会。每当我们把手动步骤转变为自动化流程时,我们就在为CI/CD系统奠定基础。

在这个简单的用例中,我们的代码(用箭头表示)经过许多步骤,最终生成一个二进制文件。这个二进制文件可以运行。这个概念不仅适用于Rust,也适用于任何语言(如Web应用或其他项目),并且是DevOps的基石。

手动发布的挑战

上述开发周期对于快速开发迭代可能尚可接受。然而,当进入第二步——将应用发布到生产环境时,问题就开始变得棘手且危险。

发布到生产环境可能涉及众多复杂步骤:

  • 为版本打标签。
  • 检查文档构建。
  • 确保文档中没有404链接。
  • 确保HTTP应用通过所有测试。
  • 正确配置云服务提供商。
  • 处理身份验证。

步骤可能非常多(图中夸张地用了75步来表示)。一旦其中一步出错、遗漏或顺序错误,就会导致生产环境出现问题,迫使你进行回滚并投入更多手动工作来修复。

CI/CD如何提供帮助 🛠️

那么,CI/CD如何帮助我们解决这些问题呢?让我们看看它是如何工作的。

CI/CD系统,或称构建系统,形成了一个反馈循环。你提交Rust代码,系统尝试构建二进制文件。构建成功了吗?很好。构建失败了吗?系统会提供反馈,你可以据此进行修复。这一切都发生在自动化的方式下,无需手动干预。

构建系统负责处理所有步骤,直到最终生成二进制文件。对于Rust,产物是二进制文件;对于其他语言或HTTP服务,可能是部署到某个地方。关键在于,无论有多少步骤(1步、2步、5步甚至100步),所有事情都通过自动化完成。

自动化是DevOps的核心概念,目标是让尽可能多的事情自动发生。这样,开发者就能专注于最重要的任务——开发。其余工作虽然重要,但可以交给自动化系统处理。

CI/CD的目标与优势

CI/CD的最终目标是实现自动化发布。一旦你能以自动化方式发布到生产环境,你将获得以下优势:

  • 速度更快:自动化流程远快于手动操作。
  • 错误更少:减少人为失误。
  • 步骤可靠:永远不会遗漏步骤或弄错顺序。
  • 轻松回滚:如果新版本(如version1)有问题,可以快速、自动化地回滚到上一个稳定版本(如version0)。

CI/CD平台是实现所有自动化构建的基础。它帮助你在尝试构建和发布时,创建并管理所有这些自动化步骤。

总结

本节课中我们一起学习了CI/CD的基本概念。我们了解到,CI/CD的核心是将软件开发中的手动、易错步骤(如构建、测试、发布)转变为自动化、可靠的流程。它通过建立自动化反馈循环,提高了开发效率,减少了人为错误,并确保了发布过程的稳定性和可回滚性。这是现代DevOps实践的基石,让开发者能更专注于代码本身。

147:作业的构成组件 🧩

在本节课中,我们将要学习在CI/CD环境中,一个作业(Job)乃至一个流水线(Pipeline)是由哪些组件构成的。我们将以GitHub Actions为例,通过分析实际项目来理解这些概念,并学习如何组织步骤以实现关注点分离。

作业与流水线概述

在CI/CD环境中,一个作业是一系列步骤的集合,旨在完成一个特定的任务。有时,多个作业可以组合成一个流水线,其中各个作业之间可能存在依赖关系,共同实现一个更大的目标。

上一节我们介绍了CI/CD的基本概念,本节中我们来看看一个具体的作业是如何构成的。

作业的构成:步骤与关注点分离

让我们观察一个实际的Rust项目。在它的GitHub Actions配置中,我们可以看到一个执行测试的作业。

这个作业包含一个或多个步骤。所有步骤协同工作以完成测试任务。在当前示例中,该作业负责多项测试:

  • 测试最新的稳定版Rust。
  • 测试Nightly版本。
  • 在32位系统上测试Rust 1.61版本。
  • 测试其他特定配置下的Rust 1.61版本。
  • 进行Python绑定测试。

所有这些测试都是独立运行的。关键在于,其中某些测试可能会失败,而其他测试可能正常通过。

当你将不同的关注点分离开来时,工作会变得更易于管理。你可以更容易地进行回滚、重新测试、做出修改,然后仅针对未通过的部分进行重测。

深入作业步骤

现在,让我们深入其中一个作业,查看其具体的步骤构成。

一个典型的作业步骤序列可能如下:

  1. 设置作业环境:准备运行作业所需的基础环境。
  2. 克隆代码:获取将要被测试的源代码。
  3. 设置工具链:例如,安装和配置Rust工具链。
  4. 执行检查与测试:运行实际的检查、编译或测试命令。
  5. 完成作业:清理或报告结果。

作业就是为完成某事而执行的一系列步骤。它可以是一个更大任务的一部分,这个更大的任务有时被称为流水线。在流水线中,你会有一系列作业和步骤,它们各自执行不同的操作,共同服务于某个总体目标。

触发条件与流水线依赖

如果我们查看项目的GitHub Actions配置文件,可以更详细地看到这些步骤的定义。这种配置方式实现了关注点的分离。

此外,作业和流水线通常需要一个触发器来启动。触发器可以是:

  • 手动触发。
  • 代码变更时触发。
  • 在GitHub中,可以设置为在拉取请求(Pull Request)时触发。

接下来,我们看另一个来自Sentry组织的Relay项目,它展示了一个更清晰的流水线示例。

这个流水线包含多个独立的作业。值得注意的是,这些作业并非总是顺序执行;某些作业能否执行,可能取决于前面作业的结果。这体现了作业间的依赖关系

例如,你可以设置一个矩阵构建,或者创建多个尝试做相同事情的作业,并为其设置执行条件。流程可能是:如果文件发生更改,并且检查通过,则继续执行后续作业。你也会看到有些作业被标记为“跳过”,这通常是因为满足了预设的跳过条件。

通过设置特定的标志和配置,你可以控制流水线中的作业是跳过还是强制执行某些操作。

总结

本节课中我们一起学习了CI/CD中作业与流水线的核心构成。

简单来说,一个作业是一系列步骤的集合。而一个流水线可以是一系列步骤,或者是一系列彼此之间存在依赖关系的作业。这就像遵循一个食谱:你可以按照特定顺序执行,并且可以根据条件跳过某些步骤。

核心概念回顾

  • 作业 (Job) = 一系列步骤 (Steps)
  • 流水线 (Pipeline) = 一系列作业/步骤,通常带有依赖关系 (Dependencies)
  • 触发器 (Trigger) = 启动作业或流水线的事件 (如 on: pushon: pull_request)
  • 步骤 (Step) = 具体的执行命令或操作 (如 run: cargo test)

148:Jenkins平台概览 🚀

在本节课中,我们将要学习全球最流行的CI/CD系统之一——Jenkins。我们将了解如何运行Jenkins容器,完成其初始设置,并创建一个简单的构建任务来理解其核心工作流程。


Jenkins简介与运行

全球最流行的CI/CD系统或平台之一是Jenkins。我们今天要做的是运行Docker容器,将主机的8080端口映射到容器的8080端口。我将使用Jenkins的LTS版本镜像。

有多种安装Jenkins的方法,我们不会详细讲解安装过程,但我会展示我通常的运行方式。我之前已经运行过,所以你不会看到我拉取镜像的过程。容器已经初始化,这是关键。我之所以展示日志,是因为你第一次启动时,会看到一个用于继续安装的小令牌。

我复制了这个令牌,然后打开已经在8080端口运行的浏览器。我需要刷新页面。

你会看到Jenkins处于锁定状态。我放大页面,然后粘贴刚才复制的特殊令牌。我选择“不安装推荐插件”,因为我认为默认的那些插件已经足够。接下来,它会安装几个默认插件。

插件与集成

现在,让我们看看这里发生了什么。我们获得了GitHub流水线支持、SSH支持以及GitHub分支源插件,这意味着与GitHub有深度集成。如果你想要一个完全不运行在GitHub上的外部系统,例如处理私有项目或依赖本地构建服务器的专门任务,那么Jenkins是一个很好的选择。

我们稍等片刻,直到所有插件安装完成。

仪表盘正在加载。好的,安装已完成。

管理员设置

我将快速进行一些设置。我输入一个虚拟的管理员用户名“admin”。我跳过电子邮件地址步骤,点击“保存并继续”。我可能还是需要一个电子邮件地址,所以在这里输入一个虚拟地址,例如 admin@sample.org,然后再次点击“保存并继续”。

这个“实例配置”步骤在你使用反向代理服务器时很有用,但我们没有,我们是直接运行的。最后,我选择“开始使用Jenkins”。

一切顺利,这就是你看到的初始界面。可能会有一些安全提示或需要设置代理的提醒,我现在先忽略它们。这个界面与我们之前见过的其他CI/CD系统非常相似。

核心组件与创建任务

接下来,我们将看到构建队列。如果你有多个服务器和多个任务,它们会显示在这里。目前,我们有两个构建执行器,这代表两个实际执行工作的节点。Jenkins有分配工作的机制。在这个例子中,这是一个独立的服务器,默认带有这两个执行器。

如果我们想创建一个新项目,比如一个自由风格项目,我们可以点击“新建任务”。我们将其命名为“test”,然后点击“确定”。

我们会看到这个信息屏幕,暂时点击“确定”即可。这里没有源代码管理或构建触发器设置。我们之前讨论过构建触发器,它可以拉取你的源代码仓库。你可以设置GitHub钩子,这样当GitHub有变更时,Jenkins就会感知到。或者,你也可以定期构建,或通过脚本、定时任务来触发构建。

你可以在这里添加构建步骤。例如,我添加一个“执行shell”的步骤,输入命令 echo success。然后点击“保存”,我们的任务就创建好了。

执行与查看结果

我们现在就可以立即构建这个任务。构建会立刻显示在这里。它几乎没花时间,因为我没让它做太多事情。你可以在这里看到输出结果。整个过程设置简单,操作便捷。

现在,我们的测试任务就出现在这里了。

总结

本节课中,我们一起学习了Jenkins平台。其组件非常相似,核心在于:一旦CI/CD系统(这里是Jenkins)启动并运行,你需要知道在哪里定义任务、如何配置它们、它们将在哪里运行,以及当有多个任务时如何查看构建队列。以上就是开始使用Jenkins并理解其核心组件的一个非常简单直接的方法。

149:GitHub Actions简介 🚀

在本节课中,我们将深入学习GitHub Actions,了解其核心概念、基本结构以及如何快速上手。GitHub Actions是一个强大的自动化工具,可以帮助你构建、测试和部署代码。


我们之前已经简要了解过GitHub Actions。现在,让我们更深入地探讨它是什么,但仍保持一个高层次的概述。我将从GitHub Actions的官方文档开始,这是了解其所有不同组件和功能的最佳信息来源。

如果逐一讲解GitHub Actions的所有组件,可能会让人感到不知所措。但我们会看看它是如何运作的。点击这里,我们之前已经见过类似的内容:在这个案例中,什么是“作业”?这是一个包含不同运行器(runner)的流水线。例如,一个作业可能在一个运行器上执行,然后构建产物被传递到另一个运行器。举例来说,如果你的构建系统需要在Linux和macOS上部署,那么运行器就必须不同。运行器指的是执行任务的系统,你将在其中执行所有步骤。

工作流(workflows)是我们在GitHub仓库中存放的文件。默认情况下,你必须遵循特定的目录路径:.github/workflows/。GitHub会自动在你的仓库中查找这个隐藏目录。你可以查看其中的YAML文件,默认所有工作流文件都是YAML格式。你需要了解其工作原理。

接着是事件(events)。文档中称之为事件,我有时称它们为触发器,一些其他系统也这么称呼。你可以看到这里的定义:事件是仓库中触发工作流运行的特定活动。所有这些事件最终都会触发某种操作。

然后是我们已经见过的操作(actions)和作业(jobs)。运行器(runners)则是执行这些任务的服务器。我想展示一个简单的工作流示例,它包含几个基本部分。

你定义一个名称,这里还有一个运行名称(run name),但我个人不常使用。它规定了何时运行:当有代码推送到主仓库时触发。它规定了在哪个操作系统(或称运行器)上运行。接着,它克隆代码,并使用一个特定的操作。这里的“uses”关键字用于引入外部助手,以执行特定任务。在这个例子中,它设置了Node.js环境,指定版本为14,并使用npm执行操作。

这绝对是我们能做到的事情。在我的另一个项目中,我使用了一些Rust代码,但其组件非常相似。我会规定这个工作流仅在主分支发生更改时运行。它将在最新的Ubuntu系统上运行。克隆仓库内容后,它会设置Rust环境,使用一个特定的助手操作,然后执行cargo build命令。仅此而已,我不做其他事情。当然,你可以做更多不同的事情,但本质上,你可以从简单的配置开始,再逐步扩展。

这就是为什么我说,如果你查看文档并看到所有不同的示例和组件,可能会感到不知所措。但从像这样简单的配置开始,是学习像GitHub Actions这样的平台的绝佳方式。

我想展示的另一件事是,如果你的仓库没有任何Actions配置会怎样。这是我为本课程构建所有示例的一个Rust仓库。

我可以在这里展示,这个仓库目前没有任何Actions配置。如果我点击这里的“Actions”选项卡,里面是空的。但我会看到这样一个提示:“嘿,你想开始使用Actions吗?”我想说,当然,我想做点什么。让我们看看是否有针对Rust的配置。如果我点击Rust或搜索Rust,可以看到很多可用的选项。例如,“SLSA通用生成器”,可能不相关;还有“GitHub Actions:使用Cargo构建和测试Rust项目”。这看起来很有趣,我们不妨点开看看。

点击“配置”后,你会看到:它将在你的仓库主分支中自动创建.github/workflows/rust.yml文件。这个配置你不需要死记硬背,也不需要完全理解所有复杂的YAML语法,默认配置已经为你准备好了。

如果我继续向下滚动,它会运行构建命令,如果你有测试的话,也会运行测试。希望你的系统已经设置了测试。然后,你可以直接在这里点击“提交更改”按钮,这个工作流文件就会成为你仓库的一部分。这也是一个极好的入门方式。

你甚至可以在这里搜索更多你想要的GitHub Actions。如果你对从头创建感到不知所措,完全可以在这里的市场上探索。例如,搜索“Rust”,或者如果你想部署,搜索“Docker”。看,这里有Docker登录、构建等操作。构建和推送Docker镜像是我最常用的功能之一。


本节课中,我们一起学习了GitHub Actions的核心概念。我们了解了工作流、事件、作业、操作和运行器这些基本组件。通过一个简单的Rust项目构建示例,我们看到了如何配置一个基础的CI/CD流水线。最后,我们还探索了如何利用GitHub的模板和市场快速入门,从而避免从零开始的复杂配置。掌握这些基础知识,你就能开始利用GitHub Actions自动化你的开发工作流程了。

150:引言 🚀

在本节课中,我们将要学习如何在CICD平台中处理更复杂的逻辑和高级模式。我们将探讨在类似GitHub Actions这样的平台上,如何实现条件判断等高级功能,并理解在构建自动化流程时可能遇到的常见挑战。

当你对某个CICD平台有了深入理解,或者即使你目前已经能熟练使用像GitHub Actions这样的平台,你也会开始遇到一些棘手的场景。这些场景会让任务变得稍微困难,或者需要你采用一些更高级的模式。

例如,你可能需要处理这样的逻辑:如果某个特定事件发生,就执行A路径;否则,就执行B路径。在GitHub Actions等工具中添加条件判断,有时会显得比较棘手。

接下来,我将向你展示一些我推荐的做法。这些功能当然是可以实现的,我们也会看到它其实可以很直观。

在整理这些课程内容时,我为了把事情做对,也遇到了很多失败。这非常普遍。当你尝试实现复杂的模式时,即使模式本身很简单,或者你正在构建的平台任务只有几个步骤,遇到困难也是正常的。

例如,我对Bash(一种在Linux系统中非常常见的脚本语言)并不十分精通。在组合某些解决方案或编写一个本应执行特定功能的简单脚本时,我很容易陷入困境。感到不知所措是很常见的。

但是,如果你能遵循我们接下来要介绍的核心概念,特别是针对我将要展示的更多高级模式,那么这些知识将为你打下基础。它们应该能帮助你尝试解决团队中可能需要实现的某些复杂问题。

本节课中我们一起学习了在CICD平台中处理高级模式的重要性与常见挑战。我们了解到,即使对于熟练的开发者,实现条件逻辑等复杂功能也可能遇到困难,但通过掌握核心概念,我们可以逐步构建解决方案来应对更复杂的需求。

151:自动化代码格式化任务 🛠️

在本节课中,我们将学习如何利用GitHub Actions自动化Rust项目中的一项常见任务:代码格式化检查。我们将通过一个具体的例子,展示如何配置工作流,使其在代码格式不符合规范时自动失败,从而确保代码风格的一致性。

项目概览

我们有一个Rust项目。项目中包含几个文件,其中main.rsmain_that.rs是主要文件。main_that.rs这个Rust文件负责系统兼容性检查。项目本身的具体功能对我们当前的目标并不重要。

我们的核心目标是识别并自动化一些常见的开发任务。其中一个之前因示例类型而未展示的任务是执行cargo fmt(格式化代码)。

使用Cargo Format

Cargo内置了格式化工具,简称cargo fmt。这个命令允许我以规范化的方式格式化或重新格式化Rust代码,这种方式遵循了Rust社区的既定风格。

运行cargo fmt会检查项目中的所有Rust文件并格式化它们。如果我对当前项目运行cargo fmt,然后执行git status,会发现没有任何更改。这是因为代码已经是符合格式规范的。

如果我再次运行cargo fmt,它会进行一些更改。此时执行git status,会显示有文件被修改。通过git diff命令,可以看到具体的改动:导入语句的顺序可能被调整,某些内容被移动到不同位置,多余的空行被删除。在某些情况下,可能只是调整了空白字符。例如,原本单行的println!语句可能被重新格式化。这些就是cargo fmt所做的更改。

在自动化流程中集成检查

为了在GitHub Action(或其他自动化方式)中实现这个检查,并且希望始终执行此检查以防止不符合格式规范的代码被提交,我们需要进行一些设置。

首先,我演示一个关键点。我使用git checkout .命令回退所有更改,使git status显示工作区是干净的,回到了原始状态。

如果我在CI/CD系统中运行cargo fmt,并希望阻止因格式化更改而导致构建任务失败,我需要依赖非零的退出状态码。然而,直接运行cargo fmt,如果格式化成功,其退出状态码是0,CI/CD系统不会将其视为问题。

因此,我们需要一个在代码需要格式化时能返回非零退出状态码的命令。运行cargo fmt --help可以看到有一个--check选项。此选项以“检查模式”运行Rust格式化工具,不会实际修改文件,只检查文件是否需要格式化。

让我再次回退到原始状态,然后运行cargo fmt --check。命令会输出哪些文件需要格式化。接着,我可以通过echo $?来查看退出状态码,此时会得到一个非零值(例如1),表明代码格式需要调整。

配置GitHub Actions工作流

现在,我们进入项目仓库,配置GitHub Actions。我们将修改现有的工作流文件。

  1. 在仓库中,找到已有的Rust构建工作流文件(例如rust-build.yml)。
  2. 点击编辑该文件。
  3. 将其中运行cargo build的步骤,修改为运行cargo fmt --check
  4. 同时,我们可能希望手动触发这个工作流进行演示,而不是在每次拉取请求时自动运行。因此,需要修改触发器(on)部分。

以下是修改工作流的关键步骤:

我们将触发器从on: [pull_request]更改为允许手动触发:

on:
  workflow_dispatch:

workflow_dispatch是一个手动触发器,它会在GitHub Actions的界面上生成一个“运行工作流”的按钮。

完成编辑后,提交更改。

运行并验证自动化检查

返回GitHub Actions标签页,找到我们刚修改的“Rust构建”工作流。由于配置了workflow_dispatch,现在可以看到一个“运行工作流”的按钮。

点击该按钮运行工作流。执行需要一点时间。运行开始后,我们可以点击进入这次运行的详情页面。

在日志中,我们可以看到cargo fmt --check命令执行了,并且它失败了(因为我们的代码格式需要调整)。日志会输出与在本地终端中相同的详细信息,指出哪些文件需要格式化以及具体的格式问题。

最后,日志会显示“Process completed with exit code 1”。这证实了我们的自动化配置是成功的:当代码格式不符合规范时,CI/CD流程会失败,从而阻止不合规的代码被合并。

总结

本节课中,我们一起学习了如何自动化Rust项目的代码格式化检查。

  1. 我们首先了解了cargo fmt工具及其--check模式的作用。
  2. 然后,我们探讨了如何在CI/CD流程中利用退出状态码来使格式检查失败。
  3. 接着,我们一步步演示了如何修改GitHub Actions工作流文件,将cargo build替换为cargo fmt --check,并将触发器改为手动触发以便演示。
  4. 最后,我们运行了工作流,并验证了当代码格式需要调整时,自动化检查会正确失败。

通过这种方式,你可以确保项目中的代码始终符合统一的格式规范,提升代码的可读性和可维护性。

152:管理相互依赖的作业

在本节课中,我们将学习如何在GitHub Actions中管理相互依赖的作业。我们将从一个简单的Rust项目构建工作流开始,逐步将其拆分为独立的“代码质量”检查作业,并最终建立它们之间的依赖关系,确保作业按特定顺序执行。

概述

上一节我们介绍了基本的GitHub Actions工作流配置。本节中,我们来看看如何将一个单一的构建作业拆分为多个独立的作业,并控制它们的执行顺序。这对于实现分阶段的代码质量检查(如先格式化,再构建)非常有用。

从单一作业到多作业工作流

最初,我们可能只有一个名为 rust-build 的作业。为了进行更细致的代码质量管理,我们可以将其扩展为包含多个检查步骤的 rust-quality 作业。

以下是创建 rust-quality 作业的步骤:

  1. 首先,编辑工作流文件,将作业名称从 rust-build 改为 rust-quality
  2. 然后,在该作业下定义多个独立的步骤(steps),例如一个用于代码格式化检查,另一个用于构建检查。

然而,这种方式下所有步骤仍在同一个作业中顺序运行。为了实现真正的并行或顺序控制,我们需要创建多个独立的作业。

创建独立的并行作业

我们可以将格式化和构建拆分为两个独立的作业,让它们同时运行。

以下是定义两个独立作业的示例:

jobs:
  format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Ensure code is formatted
        run: cargo fmt -- --check

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Ensure cargo builds correctly
        run: cargo build

在这个配置中,formatbuild 两个作业没有依赖关系,它们会同时启动和执行。这是一种高效的策略,但如果构建依赖于格式化的成功,则不合适。

建立作业间的依赖关系

有时,我们需要作业按顺序执行。例如,我们希望只有在代码格式化检查通过后,才运行构建作业。

为了实现这一点,我们使用 needs 关键字。这个关键字允许一个作业声明它需要等待另一个作业成功完成后才能开始。

以下是建立依赖关系的修改方法:

  1. 在依赖方作业(如 build)的配置中,添加 needs 键。
  2. needs 的值设置为它所依赖的作业名称(如 format)。

修改后的 build 作业配置如下:

  build:
    needs: format  # 声明此作业需要 `format` 作业成功
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Ensure cargo builds correctly
        run: cargo build

添加 needs: format 后,build 作业会等待 format 作业完成。如果 format 作业失败,build 作业将被自动跳过,不会执行。

工作流执行效果

建立依赖关系后,工作流的执行逻辑将发生改变:

  • 无依赖时:所有作业同时开始,并行执行。
  • 有依赖时:被依赖的作业(format)首先执行。只有在其成功完成后,依赖它的作业(build)才会开始执行。如果 format 失败,build 会显示为“已跳过”。

这为我们管理代码质量流程提供了灵活性:我们可以选择让所有检查并行运行以加快速度,也可以强制它们按顺序执行以确保关键前置条件(如代码格式)得到满足。

总结

本节课中我们一起学习了如何在GitHub Actions中管理作业依赖关系。关键点包括:

  1. 将复杂任务拆分为多个独立作业。
  2. 使用 needs 关键字建立作业间的依赖关系。
  3. 理解依赖关系如何影响作业的执行顺序和状态(运行、跳过)。
    通过合理设计作业间的依赖,你可以构建出更清晰、更健壮的持续集成/持续部署(CI/CD)流水线。

153:构建矩阵作业

在本节课中,我们将学习如何在 GitHub Actions 中使用矩阵策略来简化多环境(如不同操作系统)下的作业配置。通过矩阵,我们可以避免代码重复,降低维护负担。


回到我们一直在处理的项目。我想展示如何实际执行一个矩阵作业。目前所有任务都在 ubuntu-latest 上运行,我们也有相互依赖的作业。虽然可以更改,但当前都运行在 ubuntu-latest 上。

如果查看我们的 YAML 文件,会发现 format 作业和 quality 作业都运行在 ubuntu-latest 上。这里的情况是,如果想在不同的操作系统上运行,就必须将整个作业配置复制粘贴到另一个操作系统的条目下。

这种方法容易出错,并且增加了维护负担。因为如果你有多个步骤,当需要更新其中一个时,其他副本也必须同步更新。可能会出现拼写错误、遗漏某些更改,导致配置逐渐不一致。

那么,如何实现矩阵作业呢?在 jobs 部分,我们不直接定义具体作业,而是先定义一个矩阵。我们将其命名为 matrix-build

matrix-build 中,我们需要定义一个 strategy。这个策略要求我们配置一个 matrix。我们将在这个矩阵中定义 operating-system 变量,并列出不同的操作系统。例如,我们不仅使用 ubuntu-latest,还加入 macos-latest,因为 GitHub Actions 支持这些定义。

你可能会看到代码下有曲线提示,悬停会显示“作业需要 runs-onuses 属性”。这是正确的。那么如何访问矩阵中的值呢?我们使用 runs-on: ${{ matrix.os }}

这里发生的是,我通过插值提取 matrix.os 的值。os 变量将依次取矩阵中定义的每一个值。这样,我们就不再需要以传统方式重复定义作业配置了,这是关键所在。

接下来,我们需要对现有结构进行一些调整。strategy 部分没问题,但我们需要将整个作业内容向内缩进一层。因为之前我们有两个独立的作业,现在需要将它们合并到一个矩阵作业的上下文中。

我们将删除独立的作业定义,只保留 steps。这些步骤将不再需要之前定义的 needs 依赖关系。我们进行了一些“手术”来简化配置。

以下是简化后的步骤:使用 checkout 检出代码,然后运行代码格式化检查,最后确保 Rust 项目能正确构建。这些步骤将在我们定义的每一个操作系统上并行执行。

我将保存这些更改。


保存后,我回到 Actions 页面,点击 rust-quality 工作流并运行它。可以看到一个任务已经出现。现在它正在运行。

点击进入详情,可以看到“0 out of 2 jobs completed”。查看详情会发现,macos-latestubuntu-latest 两个任务正在同时运行。你可以看到我们的矩阵作业已经生效。

不过,由于 cargo fmt 格式检查失败,所有任务可能都会失败。这是在 macOS 上运行的情况。

这种方法对 Rust 项目尤其有意义。虽然我们现在只是做格式化和质量检查,但如果实际要发布版本,我们可以轻松地为 macOS 和 Linux 分别构建二进制文件。矩阵配置为我们提供了极大的便利。

通过展开矩阵作业配置,我们能够高效地实现多环境测试和构建。

154:在工作流中处理逻辑 🔧

在本节课中,我们将学习如何在 GitHub Actions 工作流中处理复杂的逻辑判断。由于 YAML 配置格式本身缺乏灵活性,我们将探索通过运行外部脚本(如 Bash 脚本)来实现条件判断和控制流程的方法。

概述

GitHub Actions 的 YAML 配置格式在处理条件逻辑时存在限制。它不允许你直接根据特定条件来决定执行某个步骤或直接使任务失败。这种不灵活性是一个重要的约束。本节课将介绍一种策略:通过运行仓库中的外部脚本来实现复杂的逻辑控制。

工作流配置基础

首先,我们创建一个简单的 GitHub Actions 工作流文件。以下是一个名为 bash-script.yml 的基础工作流配置:

name: Bash Script Workflow

on:
  workflow_dispatch: # 允许手动触发

jobs:
  test-bash:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Run a Bash script
        run: bash test.sh ${{ secrets.TOKEN }}

在这个配置中:

  • workflow_dispatch 允许我们手动触发工作流。
  • 任务在 ubuntu-latest 环境中运行。
  • 第一步使用 actions/checkout 操作来克隆仓库内容。
  • 第二步运行一个名为 test.sh 的 Bash 脚本,并向其传递一个密钥 TOKEN${{ secrets.TOKEN }} 是 GitHub Actions 注入的一个特殊变量,代表存储在仓库设置中的密钥。

创建并测试基础脚本

接下来,我们创建将要被调用的 Bash 脚本 test.sh

#!/bin/bash
echo “Working from a Github Action”

提交工作流文件和脚本后,我们可以在 GitHub 仓库的 “Actions” 标签页中手动触发工作流。运行成功后,日志会输出 “Working from a Github Action”,这证明我们的基础配置是有效的。

然而,这个脚本目前没有进行任何逻辑判断。它只是简单地输出一条信息。

在脚本中实现条件逻辑

为了使工作流具备决策能力,我们需要在 Bash 脚本中加入条件判断。我们可以检查环境变量或传入的参数是否存在。

以下是改进后的 test.sh 脚本内容:

#!/bin/bash

# 检查是否传入了 TOKEN 参数
if [ -z “$1” ]; then
  echo “We don‘t have the secret token. Can‘t continue.”
  exit 1 # 退出码非零,将使 GitHub Actions 步骤标记为失败
else
  echo “We are good to go with the secret token.”
fi

脚本逻辑说明:

  • if [ -z “$1” ]:这是一个条件判断语句。-z 用于检查第一个参数($1,即我们传入的 TOKEN)是否为空字符串。
  • 如果参数为空(-z 为真),则输出错误信息并使用 exit 1 退出脚本,这会导致 GitHub Actions 的该步骤失败。
  • 如果参数不为空,则输出成功信息,脚本正常退出(退出码为0)。

提交修改后的脚本,并再次手动触发工作流。这次,工作流会执行脚本中的条件判断。由于我们正确传入了 TOKEN,步骤应该成功,并输出 “We are good to go with the secret token.”。

方法的优势与扩展

通过外部脚本处理逻辑的策略非常强大,它不仅限于 Bash 脚本。

以下是你可以采用的其他方式:

  • Python 脚本:利用 sys.argv 获取参数,进行更复杂的逻辑处理。
  • Rust 二进制程序:如果你有一个编译好的 Rust 程序,同样可以在工作流中调用它来处理逻辑。
  • 任何可执行文件:原则上,任何能在运行器环境中执行的文件都可以用于此目的。

这种方法将逻辑从僵化的 YAML 配置中剥离出来,放入了功能更完整的编程语言环境中,极大地提升了工作流的灵活性和可维护性。

总结

本节课我们一起学习了如何在 GitHub Actions 中克服 YAML 的条件逻辑限制。核心方法是将判断逻辑移至外部脚本中执行。我们创建了一个基础工作流,调用了一个 Bash 脚本,并在该脚本中实现了检查参数是否存在的条件判断。通过 exit 1 可以使步骤失败,从而控制工作流的执行路径。这种模式可以扩展到 Python、Rust 等多种语言,是实现复杂自动化工作流的有效策略。

155:项目现代化与自动化引言 🚀

在本节课中,我们将学习如何为一个现有的、缺乏自动化的Rust项目引入DevOps实践。我们将从识别项目当前的不足开始,逐步构建一个健壮的流程,涵盖Docker容器化、CI/CD流水线以及代码质量检查,最终将应用部署到容器注册表。


对于身处DevOps领域或尝试应用DevOps概念的人来说,一个常见场景是成为某个项目的负责人。这个项目可能已经存在,或许已经编写完成,甚至已经投入生产环境,但它可能缺乏许多自动化功能,没有太多其他内容。因此,作为DevOps工程师,甚至是深入系统工程并希望探索在此处可以实施哪些改进的人员,你的主要任务之一就是改善这种状况。

这正是我们现在要做的。我们将选取一个在多个方面有所欠缺的项目,学习如何识别这些不足,并从此处开始构建,以尝试获得一个非常健壮的方案。从编写Dockerfile开始,到使用Lint工具增强Dockerfile,再到确保在通过某些检查之前,变更不会进入代码仓库的主分支。

我们将看到如何为这个Rust项目整合所有这些内容。在尝试判断变更是否适合进行拉取请求检查或可能需要手动触发时,我们还将使用一些更高级的模式。我们将学习如何实现这些模式。最后,我们将把所有内容整合在一起,容器化整个应用程序,并将其部署或提交到容器注册表。

最终目标很明确。你可能拥有十种不同的生产部署路径,例如部署到云服务提供商。但在本案例中,我们将完成验证代码质量和实现适当容器化的基础性工作,直至将容器推送到注册表。


上一节我们介绍了本课程的目标和背景,本节中我们来看看我们将要处理的具体项目情况。

以下是项目当前存在的主要不足:

  • 项目可能已经存在并投入生产。
  • 缺乏自动化流程。
  • 缺少代码质量检查。
  • 容器化方案可能不完善或缺失。

本节课中我们一起学习了如何为一个基础薄弱的Rust项目规划DevOps改进路线。我们明确了从识别问题、引入Docker容器化、建立CI/CD流水线进行自动化检查和构建,到最终将容器化应用推送至注册表这一完整流程。接下来的课程将深入每个环节的具体实现。

156:识别项目需求 🔍

在本节课中,我们将学习如何为一个Rust项目识别和规划自动化需求。我们将从代码质量检查开始,一路探索到容器化与部署,为后续在CI/CD系统中实现这些自动化步骤打下基础。

项目现状分析

首先,我们来看一下当前的项目。这是一个HTTP API服务,用于读取个人身份文件信息。项目本身功能良好,但缺乏自动化流程。

如果你是一名系统工程师或DevOps工程师,负责这个项目的运维侧工作,你需要开始仔细检查项目结构。目前,项目目录下只有Rust源代码和一个README文件,没有任何自动化配置。

识别可自动化的事项

以下是我们可以尝试为这个Rust项目设置的一些自动化事项。

1. 代码测试

首先,我会检查项目中是否包含测试。幸运的是,创建该项目的开发者已经编写了一些测试。这些测试是我们构建系统时必须包含的关键部分。因此,验证并集成现有测试是我们的首要任务之一。

2. 代码格式化

接下来,我会确保代码格式符合标准。我将运行 cargo fmt 命令来检查并格式化代码。

cargo fmt

运行后,cargo fmt 没有发现任何需要修改的地方,代码格式良好,无需提交任何更改。这是一个好的开始。

3. 容器化

另一个重要的自动化步骤是容器化。为了部署服务,我们可能需要添加一个容器镜像构建流程。这通常需要与开发团队协作完成。

例如,我可以在项目根目录创建一个 Dockerfile 文件:

FROM rust
WORKDIR /myapp
COPY . .
RUN cargo build --release
FROM debian:buster-slim
COPY --from=0 /myapp/target/release/myapp /usr/local/bin/myapp
CMD ["myapp"]

这个Dockerfile使用Rust镜像来构建应用,然后使用精简的Debian镜像来运行它。当然,其中的 myapp 命令可能需要根据实际项目名称进行调整。一旦我们创建了容器镜像,就可以将其推送到容器注册表,以便后续部署。

本节总结

本节课中,我们一起学习了如何为一个Rust项目识别自动化需求。我们重点分析了三个核心方面:代码测试代码格式化容器化。这些是设置CI/CD自动化流水线(例如使用GitHub Actions)时,对于Rust项目非常有用的首要考虑事项。在接下来的课程中,我们将详细探讨如何实现这些自动化步骤。

157:为拉取请求设置代码检查 🛠️

在本节课中,我们将学习如何为Rust项目集成代码检查工具,并配置自动化工作流。我们将创建一个Makefile来抽象常用命令,并设置GitHub Actions工作流,以便在每次拉取请求时自动运行代码质量检查。

概述

在软件开发中,保持代码质量至关重要。本节我们将通过添加clippy(Rust的代码检查工具)和自动化工作流,确保项目在合并前通过代码质量检查。我们将从创建一个Makefile开始,它可以帮助我们标准化项目命令,然后配置GitHub Actions来运行这些检查。

创建并配置Makefile

上一节我们讨论了代码质量的重要性,本节中我们来看看如何通过Makefile来标准化我们的开发流程。Makefile允许我们抽象复杂的命令调用,并为所有项目提供一致的接口。

首先,我们在项目根目录创建一个名为Makefile的文件。其核心作用是定义一系列目标(targets),例如格式化代码、运行测试和代码检查。

以下是一个实用的Makefile示例,它定义了formattestlint等目标:

# 定义默认目标
.DEFAULT_GOAL := help

# 帮助信息目标
help:
	@echo "可用命令:"
	@echo "  make format    # 格式化代码"
	@echo "  make test      # 运行测试"
	@echo "  make lint      # 运行clippy代码检查(将警告视为错误)"

# 格式化代码
format:
	cargo fmt

# 运行测试
test:
	cargo test

# 运行clippy进行代码检查
lint:
	cargo clippy --all -- -F warnings -D warnings

在这个Makefile中:

  • help 目标列出了所有可用命令。
  • format 目标使用 cargo fmt 格式化代码。
  • test 目标运行项目测试。
  • lint 目标是核心,它使用 cargo clippy 进行代码检查。--all 标志检查所有特性,-F warnings 禁止警告,-D warnings 则将警告升级为错误,确保代码完全符合规范。

现在,我们可以在终端中运行 make help 来查看所有命令,或运行 make lint 来执行代码检查。如果代码中存在clippy建议改进的地方,此命令会报错,从而阻止构建。

配置GitHub Actions工作流

我们已经有了本地运行的代码检查命令,接下来需要将其集成到持续集成/持续交付(CI/CD)流程中。我们将配置GitHub Actions,使其在每次代码推送或拉取请求时自动运行make lint

以下是设置步骤:

  1. 在项目根目录创建 .github/workflows/ 文件夹结构。
  2. 在该文件夹内创建一个YAML文件,例如 clippy.yml,用于定义工作流。

以下是一个GitHub Actions工作流配置示例:

name: Clippy Check

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  clippy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          components: clippy
      - name: Run Clippy
        run: make lint

这个工作流定义了一个名为“Clippy Check”的任务:

  • 触发时机:在向main分支推送代码或创建指向main分支的拉取请求时触发。
  • 运行环境:使用最新的Ubuntu系统。
  • 执行步骤
    1. 检出(checkout)仓库代码。
    2. 安装包含clippy组件的稳定版Rust工具链。
    3. 执行 make lint 命令进行代码检查。

配置完成后,将此文件推送到GitHub仓库。此后,每次相关的代码活动都会自动触发这个工作流。你可以在GitHub仓库的“Actions”标签页查看运行结果。如果make lint检查失败(即代码存在clippy指出的问题),工作流会显示失败,从而阻止拉取请求的合并。

总结

本节课中我们一起学习了如何为Rust项目设置自动化的代码质量检查流程。我们首先创建了一个Makefile来封装和标准化cargo clippy等命令,然后配置了GitHub Actions工作流,使其能响应代码变更自动执行代码检查。这套组合确保了代码在合并前必须通过严格的质量关卡,是维持项目代码健康度的有效实践。

158:为拉取请求设置Dockerfile检查 🐳

在本节课中,我们将学习如何为GitHub Actions工作流添加一个新的检查步骤,专门用于对项目中的Dockerfile进行代码质量检查(Linting)和构建验证。这有助于确保Docker配置的正确性,并防止有问题的配置被合并到主分支。

上一节我们介绍了如何为Rust项目设置Clippy检查,本节中我们来看看如何为Dockerfile设置类似的自动化检查。

添加Dockerfile检查工作流

现在,我们希望在已有的工作流中添加另一个检查步骤,因为我们项目中有一个Dockerfile。

让我们先查看一下现有的Dockerfile内容,它用于构建名为“redactor”的应用程序。我们想要做两件事:一是对Dockerfile进行代码检查(Linting),二是确保它能够成功构建,不会破坏现有流程。接下来,我们将同时实现这两步。

我们将在这里添加一个新的工作流文件。点击创建新文件,将其命名为 docker-check.yml。YAML文件的格式可能比较复杂,一个稳妥的做法是复制现有的工作流配置(例如clippy检查的配置)作为模板,然后进行修改。

以下是创建新工作流文件的具体步骤:

  1. 复制现有的 clippy.yml 文件内容。
  2. 回到新文件 docker-check.yml,粘贴复制的内容。
  3. 将工作流的名称从“clippy”改为“Docker check”。
  4. 保存文件。
  5. 配置该工作流在代码推送(push)和拉取请求(pull request)时触发。

这样,我们就有了一个基础的工作流框架。接下来,我们需要调整具体的步骤。

配置Linting步骤

我们需要修改从第10行开始的“steps”部分。首先,确保格式正确,“runs-on”和“steps”的缩进要准确。

现在,我们来配置具体的检查步骤。我们想要添加一个名为“Run Dockerfile Lint”的步骤。

我们将使用一个名为 Hadolint 的专用工具来完成Dockerfile的Linting。Hadolint是一个很棒的助手,它能帮助我们分析Dockerfile并找出潜在问题。

在这个步骤中,我们需要指定要检查的Dockerfile路径,也就是项目根目录下的 Dockerfile

这个设置将允许我们执行Linting检查。因此,整个工作的名称可以定为“Linting”。

配置构建验证步骤

接着,我们可以添加另一个步骤,用于实际构建Docker镜像以验证其正确性。我们可以将这个步骤命名为“Build Docker”。

这里我们采用一个有趣的方案:我们只构建镜像,但不将其推送到任何镜像仓库。这样做的目的是纯粹验证Dockerfile是否能成功构建,而不涉及发布。

我们将使用一个名为 docker/build-push-action 的GitHub Action。这个Action封装了构建Docker镜像所需的一切。

我们需要配置以下参数:

  • context 设置为 .,代表使用仓库的根目录作为构建上下文。
  • file 指定为根目录下的 Dockerfile
  • 由于我们只验证构建,不进行推送,因此不需要配置推送相关的标签(tags)等信息。

保存并提交这些更改后,新的检查工作流就会立即生效。

测试与结果分析

现在,我们可以进行一次提交来测试新添加的检查。提交信息可以是“Add CI/CD checks for project”。

提交并推送代码后,我们可以立即在GitHub仓库的“Actions”选项卡中查看工作流的运行情况。

我们会发现检查可能立即失败了。点击失败的工作流详情,可以看到Hadolint工具已经添加了行内注释。例如,它可能会警告:“使用‘latest’标签,如果镜像更新,容易导致错误。建议明确指定版本标签。”

这是一个非常有价值的建议。如果没有固定版本,未来基础镜像更新可能会导致应用程序因不兼容而崩溃。这正是自动化代码检查带来的核心好处:它能捕捉到开发者容易忽略但可能导致严重问题的细节。

在Actions页面,我们可以看到两个工作流在运行:“clippy”和“Docker check”。Clippy检查可能会因为一些代码警告而失败,并且也会在代码中留下注释。同时,Docker检查工作流也会运行。

点开Docker检查的详情,我们可以看到Linting步骤执行了,并且给出了关于使用“latest”标签的警告。然而,由于Linting步骤发现了问题,后续的Docker构建步骤可能被跳过或标记为失败。

这非常好!我们现在拥有了一套坚实的策略来防止有问题的代码被合并。尽管我刚刚推送了代码,但仓库页面上会显示一个红色的“X”,这表明项目的当前状态并不理想,存在需要修复的问题。这有效地阻止了低质量或错误的配置进入主分支。

总结

本节课中我们一起学习了如何为GitHub Actions工作流集成Dockerfile的自动化检查。我们通过添加Hadolint步骤来对Dockerfile进行代码质量分析,并配置了一个构建验证步骤来确保Dockerfile能成功生成镜像。这套流程能自动捕获配置错误和最佳实践违规,显著提升了项目在容器化方面的代码健壮性和可维护性,是CI/CD流程中至关重要的一环。

159:容器应用的打包与发布 🚢

在本节课中,我们将学习如何将我们的Rust项目容器化,并通过自动化工作流将其构建并发布到容器注册表。我们将使用GitHub Actions来实现这一过程,并最终将镜像推送到GitHub容器注册表。


上一节我们介绍了如何编写Dockerfile来容器化应用,本节中我们来看看如何自动化构建和发布这个容器镜像。

创建GitHub Actions工作流文件

为了实现自动化,我们需要在项目中创建一个GitHub Actions工作流文件。这个文件将定义构建和推送Docker镜像的步骤。

以下是创建该文件的步骤:

  1. 在项目根目录下,创建一个新文件,路径为 .github/workflows/docker-registry.yml
  2. 我们将在这个YAML文件中定义我们的工作流。

工作流配置详解

接下来,我们详细解析这个工作流配置文件的内容和含义。

工作流由以下几个核心部分组成:

  • 触发条件:我们配置工作流在手动触发时运行。
  • 任务:定义一个名为 release 的任务来执行所有步骤。
  • 步骤:包括检出代码、设置构建环境、登录注册表、构建并推送镜像。

以下是工作流配置的具体内容:

name: Docker Image CI

on:
  workflow_dispatch:

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Convert repository name to lowercase
        run: echo "REPO_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          push: true
          tags: |
            ghcr.io/${{ env.REPO_NAME }}:${{ github.sha }}
          file: ./Dockerfile

关键概念解释

  • workflow_dispatch: 允许在GitHub仓库的Actions标签页手动触发工作流。
  • ${{ github.actor }}${{ secrets.GITHUB_TOKEN }}: 这是GitHub提供的上下文变量和密钥,用于安全地认证到容器注册表,无需硬编码你的个人凭证。
  • ghcr.io: 这是GitHub容器注册表(GHCR)的域名。
  • ${{ github.sha }}: 使用本次提交的SHA哈希值作为镜像标签。在实际发布中,你也可以使用版本号(如 v1.0.0)。

运行工作流与权限配置

创建并提交工作流文件后,我们可以在GitHub上手动运行它。但在首次运行前,可能需要配置仓库的权限。

以下是运行工作流和配置权限的步骤:

  1. 提交并推送 docker-registry.yml 文件到你的GitHub仓库。
  2. 导航到仓库的 “Actions” 标签页。
  3. 在左侧边栏找到名为 “Docker Image CI” 的工作流,点击它。
  4. 点击 “Run workflow” 按钮,选择主分支,然后再次点击绿色按钮来手动触发工作流。

工作流开始运行后,GitHub会拉取代码、设置环境、构建Docker镜像,并将其推送到GHCR。整个过程可能需要几分钟。

重要权限配置
如果工作流在推送镜像时失败并提示权限错误(403),你需要检查仓库设置。

请按以下步骤配置工作流权限:

  1. 进入你的GitHub仓库。
  2. 点击 “Settings” 标签页。
  3. 在左侧边栏找到 “Actions” -> “General”
  4. 页面滚动到底部的 “Workflow permissions” 部分。
  5. 选择 “Read and write permissions”
  6. 点击 “Save”

此设置允许工作流有足够的权限将构建好的包(容器镜像)写入到你的仓库关联的包注册表中。

验证发布结果

工作流成功运行后,我们可以验证镜像是否已被推送到GitHub容器注册表。

验证步骤如下:

  1. 回到你的GitHub仓库主页。
  2. 点击顶部的 “Packages” 标签页。
  3. 你应该能看到一个以你仓库名命名的包(例如 cicd-rust),这就是我们刚刚推送的Docker容器镜像。
  4. 现在,任何人都可以使用 docker pull ghcr.io/<你的用户名>/<仓库名>:<标签> 命令来拉取并使用这个镜像。

本节课中我们一起学习了如何为Rust项目配置完整的CI/CD流水线,以实现Docker镜像的自动化构建与发布。我们创建了GitHub Actions工作流文件,定义了从代码检出到镜像推送的全过程,并解决了可能遇到的权限配置问题。最终,我们成功地将容器化应用发布到了GitHub容器注册表,为后续的部署环节做好了准备。

posted @ 2026-03-29 09:35  布客飞龙I  阅读(8)  评论(0)    收藏  举报