当前位置: 首页 > news >正文

网站建设页面图住房和城乡建设部网站政策发布

网站建设页面图,住房和城乡建设部网站政策发布,最好的网页设计公司,wordpress配置伪静态本篇内容是根据2020年2月份#117 Foundations of Go performance音频录制内容的整理与翻译 在这个多部分系列的第一部分中#xff0c;Ian 和 Johnny 以及 Miriah Peterson 和 Bryan Boreham 一起揭开了 Go 程序性能的第一层重要内容。 过程中为符合中文惯用表达有适当删改, 版…本篇内容是根据2020年2月份#117 Foundations of Go performance音频录制内容的整理与翻译 在这个多部分系列的第一部分中Ian 和 Johnny 以及 Miriah Peterson 和 Bryan Boreham 一起揭开了 Go 程序性能的第一层重要内容。 过程中为符合中文惯用表达有适当删改, 版权归原作者所有. Johnny Boursiquot: 好的大家好欢迎来到这一期的 Go Time 播客节目。今天我们有非常特别的一期节目。在我开始介绍今天的主题之前---这可是好东西哦---我想先介绍一下我的联合主持人 Ian Lopshire。Ian和大家打个招呼吧。 Ian Lopshire: 大家好。 Johnny Boursiquot: [笑] 够简洁了。 Ian Lopshire: 我只是按指示行事。 Johnny Boursiquot: 确实是按指示行事。哈哈这节目真是越来越有意思了。好了我今天请来了几位嘉宾和我一起讨论性能问题。哦对了我是 Johnny。 [笑] 我总是忘记介绍自己。不过你们应该已经听出我的声音了你们知道我是谁了。无论如何今天我请来了几位嘉宾一起讨论性能问题尤其是 Go 性能的基础知识。在介绍他们之前或者让他们自我介绍之前我想说一下我们这期节目的构思。希望这是一个系列节目的开始围绕 Go 和性能问题展开帮助你从零开始最终成为高手。我们会给你提供一些指导特别是针对初学者、中级开发者甚至是一些高级 Go 开发者帮助他们了解有哪些工具可用Go 编程的惯用方法有哪些以及在编写高效 Go 程序时应该注意什么。 为了帮助我讨论这些问题我首先请来了 Miriah Peterson。Miriah给大家介绍一下自己吧 Miriah Peterson: 大家好我是 Miriah Peterson。如果要用两个词来形容我自己那就是“不要相信我的数据运维工程师这个头衔因为数据工程师不怎么用 Go但我用了。” 所以…… Johnny Boursiquot: 我们稍后会深入讨论这个问题的哈哈。另外还有一位嘉宾是 Bryan Boreham。希望我发音没错Bryan来给大家介绍一下自己吧。 Bryan Boreham: 大家好我是 Bryan Boreham。我做了很多 Go 性能优化的工作已经使用 Go 近十年了。目前我在 Grafana Labs 工作同时也是 Prometheus 的维护者。 Johnny Boursiquot: 很棒我就说今天的节目会很有趣吧。我请来了真正懂行的嘉宾。好了那我们开始深入讨论吧。在开始之前我想先设定一下讨论的背景。假设你是团队中的一名开发人员负责维护多个组件或者服务不管它们是以 CLI 形式运行还是作为开发工具或者运行在某个集群上。无论如何你是某些服务的负责人。然后你的团队负责人找到你说“嘿这个组件当我们给它输入更多数据时表现得比其他服务更慢更不可预测。我们怀疑可能有性能问题可能是 CPU 或内存的瓶颈……但我们不确定。所以我让你来找出问题所在并解决它。” 所以我会扮演这个角色我会问一些问题。我假设自己对 Go 和 Go 性能优化并不了解。我会问一些可能不是愚蠢的问题但一定是天真的问题。我会扮演那种不懂但想学的人。大家觉得怎么样 Ian Lopshire: 好的…… Johnny Boursiquot: 听起来不错。我看到大家都点头了。好那从一开始告诉我关于 Go 的设计原则我知道它追求简单和高效……我知道它是一种垃圾回收语言。首先我可能需要了解垃圾回收到底是什么能不能帮我设定一下基础让我明白在性能方面该如何理解 Go 的设计原则你能为我提供一些关于 Go 性能哲学的起点吗Bryan为什么不由你开始呢 Bryan Boreham: 我想说在深入了解 Go 细节之前如果我们首先知道某个组件运行缓慢那我们接下来要了解的是它在做什么。它是因为占用了大量 CPU 而慢还是因为在等待其他东西而慢通常来说这个其他东西要么是网络要么是磁盘之类的东西。所以这是第一步在深入 Go 代码或代码细节之前先弄清楚它到底在做什么。 我几乎每天都对着屏幕喊这个问题。不过因为这是 Go Time 不是网络时间或磁盘时间我们可以假设我们已经完成了这一步并且确定问题出在 Go 代码中它占用了很多 CPU。那接下来该做什么呢这个时候进行性能分析是个好步骤。 Johnny Boursiquot: 所以一旦我知道了假设我排除了网络或磁盘问题---假设我的是某种服务它监听一个端口并接收一些流量……那我该如何理解 Go 的设计和哲学呢遇到性能问题时我该如何处理我应该从哪里开始思考 Miriah Peterson: 当然可以。不过我想先强调一下 Bryan 刚才说的东西这也是我经常遇到的情况。我从事软件开发只有六年刚开始工作时我只接触过云服务。所以有很多背景知识比如理解性能分析这些经验来自于“哦我在 Linux 上做过一些事情”或者“哦我有在不同内核、不同受限环境下工作的经验”。这些其实是非常基础的技能帮助我们理解很多问题的根源。因为我们总是会说“云资源很便宜所以我们可以随意使用。” 所以在深入 Go 之前有很多背景知识需要掌握。然后当我们确认问题出在代码时接下来要问的是“有哪些工具可以使用” 幸运的是我们选择了 Go很多工具都随标准库一起提供。所以这就是我们开始的地方。 Johnny Boursiquot: 我非常喜欢这个初步的思路。在我转向 Go 之前我自己也做了很多 Ruby 编程Ian我不确定你是否也有类似的经历。但在我转换到 Go 的过程中我发现即使是我那些天真的 Go 程序性能也比我优化过的 Ruby 程序快得多。这并不是说 Ruby 不好只是像 Go 这样的静态编译语言和 Ruby 或 Python 这样的动态语言有着不同的性能表现……在大多数情况下是如此。我并不想一概而论。但就我的情况而言我在解决某些问题时明显感到用 Go 事半功倍。所以作为一个切换到 Go 的程序员你可能会想“好吧我刚写了这个程序结果它比我之前做的任何东西都要快得多。” 你可以长时间不必担心性能优化除非你遇到某些情况---尤其是当你处理的项目规模较大时---你可能会发现需要进行优化。 比如在我们假设的情景下你的老板找你说“嘿我们通过集群层面的分析发现这个特定的服务是一个瓶颈。” 在这样的背景下Ian我很想知道在 Miriah 和 Bryan 提出的基础上你接下来会怎么做 Ian Lopshire: 是的我想我会开始问一些问题比如“为什么这是瓶颈它是在丢请求吗它的响应速度变慢了吗还是它偶尔会停滞完全停止运行” 我觉得很多人认为性能问题就是速度问题但其实关键是“我是否在我需要的约束条件内运行” 所以我的第一步是弄清楚这些约束条件是什么然后我们才能开始进行优化以满足这些条件。 Bryan Boreham: [无法听清 00:10:05.17] 你可能还需要进一步分析是每个请求都慢吗还是某一类请求或者是来自特定用户的请求你或许可以归类这些问题也可能无法归类……但如果能归类的话会非常有帮助特别是如果问题是可以复现的。最糟糕的问题是那些偶尔发生、你不知道原因、也无法自己触发的问题。所以找出问题的原因并能够复现它这非常重要。 Johnny Boursiquot: 我确实想谈谈---我会稍微概括一下你刚才说的Ian关于预期的问题。这个特定服务的性能预期是什么因为我认为这些预期通常在生产环境中会反映在资源分配上比如 CPU 或内存的分配。刚开始接触编排工具时我遇到过一个非常棘手的问题比如 Docker 或其他类似工具。我发现“哦我的程序……” 我运行程序时它们工作得很好但当达到某个阈值时突然就像被卡住了一样无法继续执行某些操作。它会突然停止运行。我当时就想“为什么这个程序运行得好好的突然就停了” 没有堆栈跟踪也没有错误信息……它就这么被终止了。我后来才意识到“哦原来是编排工具---”不管你用的是 ECS 还是 Kubernetes 或其他工具---为这个服务分配了固定的 CPU 和内存资源每当我超出这个分配的资源时进程就会被杀掉。我当时并不知道我的服务是被那个环境终止的。当我意识到这一点时我就想“哦天哪。” 正如 Ian 所说你得提前知道这些信息不管是和运维团队沟通还是你本身就是运维团队做一些 SRE站点可靠性工程工作了解“我需要提前做些什么” 这会帮助你了解你的应用或服务在处理数据时应该做什么……这也引出了另一个问题如果你需要处理大量数据并且你觉得在处理这些数据时遇到了问题接下来该怎么做你如何找出问题所在 Bryan Boreham: 嗯我已经提过分析了我还会再提一次。 Johnny Boursiquot: 好…… Miriah Peterson: 除了分析还有其他答案吗这是我一直想知道的。我觉得 Johnny 提到了一个有趣的点---我从来没做过但我知道有些人会这样做就是坐下来计算“我预计传输多少字节占用多少空间。” 我一直是那种蛮力派尽可能多地进行健全性测试看看什么时候会崩溃。但这会导致另一个问题---概念上的知识有时并不总是存在。我觉得你不想遇到 CPU 和内存问题。通常你不用担心这些问题直到你被锁定在机器外面不得不去解决它们。就像是这样的问题突然出现了你会想“好吧现在怎么办” 于是你才想到“我应该设置一个分析器。” 但到那时你已经撞上了墙已经达到了那个阈值程序已经崩溃了。这个时候再去考虑“哦天哪我之前从来不用担心的东西现在成了我唯一关心的。” 所以Bryan我同意你的看法…… Bryan Boreham: 是的…… 提前去预估资源使用是非常困难的。但我觉得这是随着经验的积累而来的。另外有一个概念叫“机械同情心”Mechanical Sympathy你听说过吗 译者注: —注释开始— “机械同情心”Mechanical Sympathy是一个术语最初由计算机科学家杰夫·阿特伍德Jeff Atwood提出用来描述程序员对计算机系统和软件的深刻理解和同情。它强调了程序员在编写代码时应理解并考虑计算机的内部工作原理。 主要概念 理解底层机制 程序员应了解计算机硬件、操作系统和编程语言的运行机制这样可以编写出更高效、更可靠的代码。 性能优化 通过理解计算机的工作原理程序员能够识别和消除性能瓶颈从而提高软件的整体性能。 错误调试 对系统内部运作的理解可以帮助开发者更快地找到和修复错误减少调试时间。 系统设计 在设计系统时考虑到硬件和软件的交互可以帮助创建更具可扩展性和可维护性的架构。 实际应用 在编写高性能程序时程序员会考虑内存管理、CPU缓存、并发处理等因素。在选择数据结构和算法时程序员会根据它们的时间复杂度和空间复杂度做出明智的选择。 机械同情心强调了对计算机系统深刻理解的重要性从而帮助程序员在开发过程中做出更好的决策。 —注释结束— Ian Lopshire: 这是我打算在这个播客里提到的内容之一。 Johnny Boursiquot: 那我们来聊聊吧。 Bryan Boreham: 我觉得这个概念来自一位一级方程式赛车手他谈到“如果你理解车子的内部运作方式你就能更好地驾驶它。” 计算机也有点类似。比如CPU 每秒可以处理十亿个操作而这些操作可能就是加两个数这样的简单操作。即便不需要知道太多细节---即使这个十亿的数字也有点偏差---我只是做个大致的简化。如果你坐在那里等待计算机返回结果意味着它可能花了半秒钟左右的时间。那么在这段时间里CPU 可以执行约五亿个操作。那么你到底在代码里写了些什么让 CPU 执行了五亿个操作这是我通常会思考的起点它到底在做什么到底是什么让它花了这么长时间来执行我要求的操作 Johnny Boursiquot: 如果你真的需要处理十亿个操作---如果是这样我为你感到遗憾……这确实是个难题…… [笑] Miriah Peterson: 欢迎来到我的世界。 Johnny Boursiquot: [笑] 如果你不得不处理---其实Miriah作为一个数据处理人员我知道你处理的数据量和方式可能会与那些编写网络应用程序的人有所不同。当然你也会涉及一些网络方面的工作但我觉得如果你处理的是---我应该这样说---不可预测的工作负载那么情况会有所不同。如果你处理的是不可预测的工作负载这将与处理一组你明确知道的数据有所不同。比如你知道自己要处理 5GB 的文本处理任务可能在编写代码时的方法就会和处理一个需要流数据的网络服务不同。这两者是不同的就像你在处理一个更大数据池中的某个子集一样。 Miriah Peterson: 我觉得这个话题很有趣也正好可以引入 Go 的讨论。最近我在为一个课程做研究这个课程的主题是 Go 和数据工程。我一直在使用 Go 的性能分析工具 pprof我们稍后可以回到这个话题。我一直在用 pprof试图理解 goroutines 的运行机制……主要是在编排层面上究竟是使用一个程序中的 goroutine 更好还是将程序扩展出去更好应该在 Kubernetes 中水平自动扩展还是应该在内部使用worker线程这些都是需要考虑的问题。部分问题涉及到基本的 API 调用可以这样说。不管你在构建什么程序Go 使用 io.Reader 和 io.Writer 作为其大部分操作的接口---无论你是连接数据库连接流服务还是连接 API所有东西都回归到这个层面。所以不论你是在处理成千上万的 API 调用还是成千上万的数据库写入或者是处理成千上万个数据点Go 的延迟通常不会---除非你的程序设计有问题---来自于实际的数据处理或数据点的操作而是来自于文件系统连接或 API 连接的延迟。 因此当你在 Go 中设计服务并尝试优化时那些连接点往往是内存问题的来源。内存问题通常来自于这些连接点或 API 点即从一个函数向另一个函数或从一个系统向另一个系统传递数据的连接点而不是来自于服务的水平扩展。这些问题往往出现在 IO 读写层面比如“哦糟糕我忘了关闭我的 writer”或者“哦糟糕我开了 15 个连接但实际上只需要一个。” 这些都是问题所在……我觉得不管你在构建什么程序这些问题都是一样的因为最终我们都在处理字节……而我发现很多人会转向 Kubernetes 日志或其他东西而并没有使用 Go 内置的一些工具来帮助我们跟踪这些问题。 Johnny Boursiquot: 我认为在代码中也存在一些简单的错误同样涉及到读取和 IO。比如当我教 Go 时我首先告诉大家的是如果你需要处理磁盘上的文件即使文件大小是可预测的要知道如果你使用 io.ReadAll你会把文件的每一个字节都加载到程序的内存中。这是一个容易犯的错误很多人会想“哦我只需将所有内容读入内存然后逐行进行某种转换或者统计每一行。” 但实际上你是在将整个文件加载到内存中。我解释说“你应该采用流处理的方式而不是将所有内容都读入内存。” 然后他们会反应过来“哦原来我可以这样做我可以一次读取一行并逐行处理” 这就是“哦原来可以这样做”的那种心态转变。如果我不知道这些库或者不知道如何避免这些容易出错的方式这些问题就会非常频繁地发生。 所以当你遇到这些情况时这就是我们开始引入更多工具的时候了。pprof 这个工具已经提到过几次了……我们来聊聊 pprof。它是什么为什么它重要 Bryan Boreham: pprof 这样的工具的基本原理是你运行你的程序让它执行它的任务分析器每秒会中断 100 次左右。每次中断时分析器会记录下当前正在执行的代码。经过几秒钟的运行或者你让它运行的任何时间长度它会统计这些数据这就是为什么我们称之为“分析”。你可以统计出程序运行了 10 秒钟分析器每秒中断 100 次所以总共有 1000 次记录。在这 1000 次记录中一半的时间花在了某个函数上10% 的时间花在了另一个函数上10% 的时间花在了另一个函数上。这就是分析结果。这个过程就是每秒中断 100 次记录执行情况然后加总这些计数最后在屏幕上绘制出来。 我喜欢一种叫做火焰图的可视化方式这种图表非常直观……不过在播客里描述并不太好我现在手舞足蹈地比划着但这并没有帮助。说真的如果你从没见过去找些展示这些图表的视频看看吧。基本上火焰图上会有一些矩形条条越长表示在那个函数上花费的时间越多。所以你只需要看这些大条形图就可以了。 Johnny Boursiquot: 这就是你首先要看的地方……当然这些是显而易见的标志但这并不一定意味着你会在这些地方获得最大的优化效果。也许你正在处理的某个函数已经高度优化了问题可能并不出在函数本身可能是你传递给它的数据量过多需要在其他地方优化。所以 pprof 给了你一个显而易见的起点让你开始深入分析对吧 Bryan Boreham: 是的你接下来可能会做的是---基本的过程是想出一些方法看看如何让程序运行更快。你会怎么做呢你可能会发现你在执行一些不必要的操作那就跳过这些操作。或者你会找到一个更聪明的方式来执行这些操作。或者你会发现你重复做了某些事情那就缓存结果后面再用。你会使用这些技术中的某一种。所以你需要规划一下你的优化路径……这就是你在找的东西。而你刚才提到的如果某个函数已经高度优化了那么……如果有人已经在那里应用了所有的优化技术你可能仍然可以通过并行化进一步优化。Go 是一个非常适合并行化的语言。如果你有足够的 CPU 资源你可以将任务分解分别在不同的 goroutines 上并发运行……并行和并发的区别我总是记不住…… Johnny Boursiquot: 我一般玩得比较安全我会说“并发”。这样比较保险哈哈…… Miriah Peterson: 这是另一个话题了我觉得…… [笑] Johnny Boursiquot: 好的好的。所以pprof 工具提供了很多调整的选项而且确实有很多。不过我通常觉得有趣的是 CPU 分析它和内存分析是不同的……还有一个追踪功能可以更清楚地显示 goroutine 和相关的执行情况。我想Bryan按照你的思路你有了一个起点比如某个函数接下来你要弄清楚“有哪些选项我可以做些什么” 比如确定那些“尴尬并行”的问题看看是否有并发机会你没有利用到。也许问题就是这么简单吧如果一个函数的多次运行之间没有什么依赖关系也就是说数据之间没有依赖那这可能就是一个很好的并发机会对吧你可以启动一些 goroutines……如果你事先知道需要多少个也许可以用一个等待组如果不知道也许可以用一些通道进行通信……然后你就开始逐步剥开问题的层层表面弄清楚“接下来该怎么做” 但说到这我想回到那个函数可能已经优化过的观点……我们怎么知道它已经优化了呢还有什么工具可以帮助我们确定这个函数在给定的数据下能够稳定地表现 Miriah Peterson: 你提得很明确我觉得你是在指向基准测试工具……[笑] 不过在我们讨论基准测试工具之前我想说……我以前在本地组织了一个 Meetup现在不再组织了我曾经为这个 Meetup 准备过关于 pprof 的演讲还做了一些视频……但老实说我个人从来记不住 pprof 工具到底是怎么用的。我记不住这些工具的所有含义也记不住所有图表的意思……Go 的 pprof 里有火焰图传统的火焰图还有一个叫做新火焰图的东西。这个工具可以告诉你“Go 的编译器是否为你做了优化它是否为你内联了一些函数” 你可以从火焰图中直接看出来编译器是否为你进行了优化。所以第一步把 pprof 加入到你的程序中。第二步查看火焰图看看“是否有函数已经为我内联了编译器是否已经为我做了一些优化” 接着你可能会发现某个函数……你看着那个网页图---这是他们的另一个工具。顺便说一句如果你想知道它的具体含义我在 YouTube 上有一个视频详细讲解了它。这是可视化的所以更容易理解。 你可以看到“哦这是一个昂贵的函数它占用了很大的 CPU 和内存……很好我想看看能不能优化它。让我写一组基准测试打开内存标志看看能不能调整这个高成本函数。” 所以我不觉得这是非此即彼的选择。我认为这些工具需要联合使用。 我个人觉得基准测试总是最后一步最后的“很好我的程序已经工作了……我现在试图优化它……也许它不是最优化的状态但我尽量让它达到 80% 的优化。我已经识别出一些占用大量 CPU 的函数或者占用大量内存的函数”你可以通过 pprof 很容易地识别这些问题。“现在让我选择这些函数打开正确的标志开始编写基准测试看看能否优化它们。” 基准测试的工作方式是不是只运行一次而是默认情况下在给定的时间窗口内尽可能多地运行它从而给出一个平均性能。所以你可以看到“很好平均来说我的这个函数分配了 3000 字节。” 这将是一个非常大的问题你应该解决它。然后你可以问自己“我能不能让它更低一点能不能让它不那么占用内存” 然后你发现“哦它每次运行需要 300 毫秒但旁边的函数每次只需要 20 纳秒” 也许我可以做一些权衡这样这个函数就不会成为整个系统的瓶颈。 所以当我讲解基准测试时我总是说的两件事是看看你的函数运行需要多长时间然后看看你在系统中分配了多少字节。这是我认为可以开始调整的第一个开关。我自己在工作中不是特别常用。我很少需要通过基准测试来证明优化。我从没在那种公司工作过速度慢到会影响公司盈利……我总是在那种需要把基础设施从一个地方迁移到另一个地方的公司所以我总能有足够的预算去构建新东西。不管怎样这仍然是我为大家指明的方向。 Johnny Boursiquot: 你真幸运。 Miriah Peterson: 我知道。当他们告诉我开始修复东西时我就会说--- Johnny Boursiquot: “我走了。” [笑] Miriah Peterson: “换新工作吧。去找一个新的‘绿地项目’。” 但这也是为什么我对这个话题如此感兴趣---也许我没有在工作中用到但现在我需要学的足够好才能教别人因为我确实认为它非常重要。所以这是给你的基准测试推荐Johnny。 Johnny Boursiquot: 很好很好很好。Ian对基准测试的讨论有什么要补充的吗 Ian Lopshire: 抱歉我有点走神了……没什么特别的…… Johnny Boursiquot: 你需要基准测试一下你的思路。 Ian Lopshire: 确实……我喜欢它们结合使用的想法。你发现了问题用 pprof 找到了占用大量资源的地方比如分配了大量内存或者使用了很多 CPU 周期……接下来的步骤就是编写基准测试这样你就能知道你是否真正做出了改变。我喜欢它们结合使用的想法必须要结合使用对吧 Bryan Boreham: 是的这让它变得可重复。我们最开始的假设例子是在生产环境中测量某些东西测量真实发生的事情……但你可能没法那么轻易地重新创建它而且你也不想在生产环境中做太多实验……所以将特定的任务提取出来做成一个小的独立程序---那就是基准测试。然后像 Miriah 说的那样你可以反复运行它这样我们就能得到一个平均的时长…… Go的测试框架会为你做这个。 我也做过一些这方面的教学我觉得结果大概是对半开的。有些人看过基准测试喜欢它们经常用它们……而另一半的人基本上从来没接触过基准测试。也许他们在文件里偶尔刷到过几次但从没真正看过它。 所以我当然会鼓励大家---这是一个非常简单的模式。你只需要写一个循环反复运行你感兴趣的东西……而更复杂的部分是设置测试条件。但这与任何单元测试是一样的。它只是一个可以反复运行同一事务的单元测试。现在你真的可以开始迭代了尝试一些想法运行基准测试……它变快了吗还是没有再尝试其他东西。 一次只改变一件事。这是另一个重要的建议。当你兴奋的时候你会有很多想法。“我要把它们都写进去。它一定会更快。” 但要一次只改一件事。改一个测一次。再改一个再测一次。这样才能真正弄清楚问题出在哪里。 Johnny Boursiquot: 有时候改变那一件事可能意味着要将它部署到生产环境看看结果是否真的不同对吧 Bryan Boreham: 是的确实有可能。我是说这取决于你的基准测试有多好。有些情况下确实很难模拟真实的生产环境。还有一些需要注意的事情。你知道我之前提到过处理器每秒可以执行数十亿个操作吗但前提是你不能使用超过几十KB的内存。一旦你超过了 L1 缓存整个系统的速度就会下降 10 倍。如果你再超过 L2 缓存速度又会再下降 10 倍。所以当你在基准测试中试图重现问题时你需要小心不要让测试规模太小。因为如果它太小就会不自然地适应处理器的紧凑缓存环境…… 这属于“机械同情”mechanical sympathy的一部分知识。要了解处理器架构和不同级别的缓存等方面的知识是非常庞大的任务。我不认为每个人都需要学会这些但至少应该了解一些基本的东西---比如你不想让基准测试的规模太小。你不希望它大到需要一天时间来运行但也不希望它小到不切实际地快。 Johnny Boursiquot: 说到缓存和内存关于内存使用优化的整个话题有自己的一套术语。当我第一次学习堆heap、栈stack和内存分配这些概念时我心想“我在编写程序时需要关心这些吗我需要担心变量的声明和保留吗Go 是垃圾回收garbage collected的语言难道它不会自动处理这些事情吗” 我们能不能给内存优化的话题做一些定义 Miriah Peterson: 不。[笑] 在 Go 的文档中它明确指出“你不需要知道什么是写入栈还是堆的区别。” 这是直接从 Go 的官网 go.dev 上摘下来的。我也在幻灯片中引用了这句话我会告诉大家“从技术上讲这并不重要但从概念上讲我认为它确实有帮助。” 因为它可以帮助你做出选择比如“哦这里我是不是应该使用指针或者这里我不应该使用指针” 了解 Go 在幕后如何使用指针是有帮助的。比如字符串在幕后总是使用指针……所以在 Go 中共享字符串要比共享字节切片slice of bytes或其他奇怪的东西更容易。但大多数情况下这并不重要因为垃圾回收器会处理它。但我认为它确实重要的时候是当你做了一些蠢事来阻止垃圾回收器工作……人们经常会这样做。另一个它重要的时刻是当你看到人们开始从其他语言中引入设计模式时。 我们总是开玩笑说那些从 Java 转来写 Go 的开发者他们会带来一些可能在 JVM 上运行更好的代码设计但这些设计并不适用于 Go 的编译器或类型系统。Bill Kennedy 的书《The Ultimate Go Notebook》 是我最喜欢的 Go 书之一因为它包含了所有那些不常见但很实用的小技巧。其中有一条用加粗字体写着“不要使用 getter 和 setter。” 每次我说这句话时所有曾经写过 Java 的人都会问“为什么我们需要这些” 我会回答“有时候你确实需要它们。” 比如当你有一个私有的类型时你可能需要通过方法来访问它。是的这是 getter 和 setter 的一个好用例。但如果是公有类型Go 编译器可以内联你所有的调用并为你进行优化而不是通过一个函数调用来执行这些操作这样会增加堆栈上的字节数……每个函数都会占用更多的内存并且需要通过接口进行额外的调用来完成所有这些事情……编译器的目标是快速它只能在达到某个速度阈值之前进行有限量的内联操作。 所以我们应该按照 Go 的惯用方法来构建代码这将帮助编译器更快地优化代码。这就是为什么我认为虽然这些细节本身可能不重要但如果我们了解这些背景知识就可以更好地理解为什么 Go 的惯用方法是这样为什么这段代码是好的而那段代码是不好的或者说是“Java 风格的代码”。不是说 Java 不好只是说在 Go 中写类似 Java 的代码可能会或者确实会导致系统效率降低因为这是一个不同的编译器、不同的系统、不同的类型签名。因此虽然这些细节本身并不重要但它们确实能帮助我们写出更好的代码。 Ian Lopshire: 我觉得我们应该把这叫做“编译器同情”compiler sympathy或者类似的东西…… Bryan Boreham: 是的……我认为值得尝试理解栈和堆这两者它们的基本区别在于生命周期。如果你进入一段代码或函数并且有一些数据这些数据的生命周期只持续到函数结束。然后 Go 的整个系统---编译器和运行时会协同工作来进行内存管理。所以如果你的数据的生命周期仅限于一个函数编译器可以非常快速地清理它。这就是栈的概念。每次我们调用一个函数数据会像堆叠一样叠加在我们之前使用的数据之上。而当我们离开函数时我们可以清理掉所有这些数据这基本上就是减去一个数字。 而堆则是存放我们不确定生命周期的地方。所以发生的事情是你还在使用的东西和你不再需要的东西都会放在堆上。而你不再需要的东西也就是你不再引用的东西---那就是垃圾。但系统的工作方式是它会让所有东西堆积起来直到某个时刻开始进行垃圾回收。而这才是真正影响性能的地方。 那么什么是垃圾回收呢当 Go 运行时开始垃圾回收时它会从程序中的那些可以访问数据的地方开始。所以包括所有的全局变量、所有局部变量中的指针等等。它会创建一个列表接着会说“好这个指针指向了什么哦这个东西需要用到我还能访问它。它有指针吗好我要访问它们的每一个。这些数据是需要的。当我到达那里时它们有指针吗” 这在大程序中或者说在任何规模的程序中都是非常繁琐的工作……它需要跟踪所有的指针这就是会拖慢程序的原因。这就是为什么在 Go 中内存管理对性能非常重要。 影响垃圾回收成本的有两个因素。首先是你有多少指针。这基本上取决于你实际需要的内存有多大。如果你的整个程序只使用 16K 的内存那就没多少指针。而我的程序通常会使用几 GB 的内存。所以会有成千上万甚至几百万个指针它们需要相当多的时间来追踪。 所以指针的数量基本上取决于堆的大小是一个因素。然后是你留下垃圾的速度有多快你生成新垃圾的速度有多快这两个因素相乘就会得出垃圾回收的成本。并且这两个因素都由你使用的内存量决定。第一个是你实际需要的内存量第二个是你创建并丢弃的垃圾量。 Johnny Boursiquot: 而每次清理运行时程序实际上都会暂停。 Miriah Peterson: 垃圾回收是一个“暂停世界”stop the world操作没错。不过我从来没有感受到它的明显影响。它不会停止运行时对吧 Bryan Boreham: 自从 Go 1.5 以来就没有了。垃圾回收有两个阶段。标记操作---这一过程叫做“标记-清除”mark and sweep。标记阶段是我刚才提到的那个过程我们会跟踪所有的指针。这个阶段可以并发进行让我们再用这个词。与……我不知道哪个是并行哪个是并发。希望会有人在推特上纠正我们。 Miriah Peterson: 可能是并发因为如果它碰到锁的话就会生气。所以我同意并发。[笑] Bryan Boreham: 标记阶段可以和你程序的其他部分同时进行。当标记完成时当我们知道哪些内存是需要的哪些是不需要的我们就进入清除阶段。在这个阶段我们基本上把所有的垃圾变成可用的内存。这是一个“暂停世界”stop the world的操作不过它真的非常短通常只需要几微秒。而对于一个 GB 级别的堆标记操作可能会持续几秒钟。 Miriah Peterson: 我现在要偷用这个解释了。我以前总是把垃圾回收解释为清除阶段……我总是说垃圾回收的标记是在并行进行的接着就是垃圾回收。我现在要改用这些术语了不管它是更混乱还是更清楚我不知道但它确实更准确。而准确才是最重要的。所以谢谢你Bryan今天教了我这些东西。 Bryan Boreham: 不用谢乐意之至。在 Go 1.5 之前整个过程都是 [无法听清 00:41:14.15]这让很多人感到相当不满……但现在它是并行运行的。其实你可以在你的性能分析profile中看到这一点。在 CPU 性能分析中你会看到垃圾回收器在运行。垃圾回收器的名字有点奇怪它不会直接以大大的“垃圾回收器”字样出现但你可以看到一些像 mallocgc 这样的函数。通常它们的名字中会有 gc 的字样你可以在 CPU 性能分析中找到它们。但如果不想让事情变得太复杂需要注意的不仅仅是标记和清除的时间……因为整个遍历内存的过程会把很多数据踢出 CPU 缓存。 我之前提到过CPU 中间的那一点是唯一能以最快速度运行的部分……标记过程中我们扫描所有数据确定哪些是需要的哪些是不需要的---这个过程会踢出缓存的数据因为它会去访问所有的东西。因此垃圾回收对程序整体的影响不仅仅是性能分析中显示的垃圾回收时间。 换句话说……如果你查看性能分析发现垃圾回收占用了你整个 CPU 的 20%然后你把垃圾回收减少一半我会预期你的程序速度会提升 40%。因为它的影响是你所能看到的测量值的倍数通常是两倍左右。 (译者注:个人经验是如果超过30%就要尝试干预,进行优化) Johnny Boursiquot: 那么作为程序员---我可以采用 Miriah 的方法基本上说“你知道吗没必要担心我是不是在这里用了指针或者在那里用了某个值……”嗯我不确定是否正确理解了---如果我理解错了请纠正我Miriah。如果我说错了你可以指出来。但你是在说“不要太担心你是否在用指针或者是值传递……让垃圾回收器来处理这些问题。或者先写出能运行的程序然后再考虑是否有一些内存溢出或者函数调用时是否有内存逃逸。” 那么我们究竟应该多关注内存分配的生命周期呢Bryan 提到的那些内存分配生命周期的影响到底有多大我们应该如何处理这一问题呢我还是经常遇到有人问“什么时候应该用指针什么时候应该传值我该怎么做从性能角度来看这重要吗还是说这只是语义问题我应该返回 nil 还是返回一个零值我该怎么处理这些问题” Miriah Peterson: 我通常会说所谓的最佳实践---我的建议是默认不使用指针。然后在一些特殊情况下这个规则有例外。比如“哦你正在使用一个接口所以你必须有一个实现该接口的类型。” 这些就是例外情况……我不知道我开始对编写优秀软件和编写优秀的 Go 代码变得挑剔起来。如果你到了某个阶段所使用的类型已经对垃圾回收速度产生了影响也许 Go 这个语言并不适合你。你可以去用 Rust它没有垃圾回收机制并且让你不得不考虑这些问题。 我不想遇到那样的问题。我使用 Go我会说“垃圾回收器帮帮我吧。” 我不会使用指针除非在某些情况下指针确实是最佳选择。我会使用切片直到需要使用数组的时候。我觉得 Go 的设计是有意地把很多底层的东西抽象掉了当我们需要用到这些低层知识时我并不确定 Go 是否是最佳选择。也许我有点唱反调但我只是用 Go 来做它擅长的事情。而 Go 擅长的是作为一个非常简单的语言帮你完成很多工作。你应该还是要了解它的工作机制还是要使用 pprof做性能基准测试了解底层原理并写出优秀的 Go 代码高效的 Go 代码。但当你超过了某个点也许你应该考虑一下 Rust。我不知道。再说一次我还没有遇到那个点我还在使用 Go。所以这只是我的想法。 Johnny Boursiquot: 但我们经历过那样的一个时代---我相信你们都记得在 Go 社区里我们经历过一个时期那时大家都有点……我们都反对内存分配。我无法告诉你我见过多少关于 HTTP 路由器的性能测试都是关于“哦这个是零分配的”或者“这个没有……” 我们经历过那个阶段…… Miriah Peterson: 那我们为什么还需要垃圾回收器呢我不知道……只要写出好 Go 代码而好 Go 代码是使用 Go 提供的工具写出来的。 Bryan Boreham: 我有一个例子…… Miriah Peterson: 你讲吧Bryan。你比我经验丰富。 Bryan Boreham: …这个例子应该是比较中立的这是很多人会用到的一个模式…… Miriah Peterson: 我并不是想划清界限我只是说这是我的经验。你继续Bryan。 Bryan Boreham想象一下在你的程序中你决定要创建一个切片并向其中加入一些元素。在 Go 语言中使用 append 是非常便利的。基本上有两种方式来实现这个操作一种是从一个完全空的切片开始然后不断地使用 append 操作。假设你要在切片中加入 100 个元素因此你会不断地 appendappendappendappend…… 在底层刚开始时并没有分配任何内存切片是空的没有分配任何空间。当你放入第一个元素时它会分配一个位置。然后你再放入另一个元素它会说“好吧空间不够了这次我要分配更多空间。” 我不想深入探讨细节但假设下次它会为三个元素分配空间。然后你填满这些空间后它可能会为八个元素分配空间。不要纠结具体的数值重点在于那些我们不再需要的小切片会变成垃圾。如果我们一开始就知道要放入 100 个元素我们可以在一开始就调用一个函数创建一个有 100 个空间的切片。这样整个 append 过程就不会产生垃圾。 我希望这个例子在音频中能够比较容易理解…… 有一些非常简单的模式比如预先分配你的数据结构到一个合适的大小。如果你知道要放入 100 个元素那就直接创建一个大小为 100 的切片。如果你大概知道是 100 个可以分配成 120 个类似这样。因为即使是一次不必要的内存分配其代价也可能比多分配 10% 或 20% 的空间更大。 如果你不确定是 100 个还是 100 万个元素那当然你可能会有一些浪费…… 但尽量接近正确的大小并倾向于稍微多分配一点这会帮你提升性能。这类技巧有很多。 Miriah Peterson我同意。这就是编写优秀 Go 代码的一个例子。我认为这正是你应该处理的方式……如果你能够预测到比如你知道这个数据会如何表现那么你应该明确地分配所需的空间。我也同意切片在底层的实现很有趣。 Johnny Boursiquot它们的代价一般是很小的但确实存在一些成本……是的这是一个很好的建议。随着讨论接近尾声作为 Go 程序员还有没有其他类似的显而易见的建议不一定是为了进行过早优化而是像你们所说的那样在日常开发中如果你知道一个数组或切片的大小在一开始就进行预分配这似乎是一个显而易见的好做法---不一定是在优化的精神下而是为了编写良好的 Go 代码。你们还有其他类似的最佳实践建议吗 Bryan Boreham嗯映射maps也是一样的。切片和映射都可以在创建时指定大小。映射要复杂得多但基本原理相同如果你知道会有 1000 个元素告诉运行时在创建时为 1000 个元素留出空间这样一切都会运转得更好。 很多节省内存分配的技巧都比较晦涩。不幸的是除了这些基本操作之外比如在循环开始时调用 make设计接口时当你调用某个函数并且它返回一个切片时如果你可以传入目标切片会更好因为你可能已经有一个大小合适的切片可以传入…… Go 标准库中有一些这样的 API。不过这确实会让事情变得复杂…… 我想说的是尽量让代码优雅然后再考虑性能优化。除非真的非常有必要否则不要打破这个规则。通常优雅的代码本身就能运行得很快。我真的不希望大家把程序搞得乱七八糟只是因为他们认为这样可以节省几个字节或者几纳秒。遵循 80/20 法则。大部分时间消耗通常集中在一个地方或几个地方。在这些地方你可能需要做一些小技巧。 Miriah Peterson我认为我的大部分建议都很显而易见…… 遵循惯用法小心使用指针…… 我建议不要在没有必要时使用它们。你需要指针的地方是显而易见的---我觉得 Bryan 提到了一个很好的点…… 如果你要填充某个数据结构并且需要修改其中的数据传递指针并在对象内部修改数据要比传递一个副本并返回新对象好得多。这类操作我认为是编写良好 Go 代码的典范。但我还想说尤其是对于新手找一个优秀的代码检查工具linter非常重要。所有优秀的代码检查工具都会检查诸如“你是否关闭了 SQL 连接你是否关闭了 HTTP 连接你是否关闭了文件连接” 这些小问题往往是我容易忘记的导致内存问题…… 它们都很简单在代码审查中也不一定能被发现。 因此代码检查工具尤其是当你对它们设置得非常严格时能够帮助你编写出优雅的代码也就是良好的 Go 代码这可以避免很多愚蠢的内存问题或 CPU 问题…… 当问题真的出现时它会是一个真正的问题而不仅仅是因为有人忘记关闭文件。这些是我认为大家应该从一开始就注意的然后再逐步深入。无论如何它确实救了我几次。 Johnny Boursiquot不错。在我们结束之前Ian 还有什么要补充的吗 Ian Lopshire我对于早期优化的看法是尽量减少工作量。因此如果你要使用正则表达式那就编译一次并重复使用。如果你要使用模板那就编译一次并重复使用。我经常看到有人在处理器中定义了正则表达式并且每次都会重新编译。所以即使在你进行性能分析或基准测试之前如果你发现有一些不必要的工作量那就是最低垂的果实---做更少的工作。 Jingle(插播音乐) Johnny Boursiquot好吧来吧我听着呢谁有不受欢迎的意见 Miriah Peterson我先来。 Johnny Boursiquot请讲。 Miriah Peterson我有两个不受欢迎的意见。第一个是“巧克力很难吃因为我不喜欢巧克力。” Johnny Boursiquot[笑] 好的。 Miriah Peterson第二个---我可能之前提到过。我认为 Python 是一个不适合数据工程的语言。我觉得它非常适合数据分析或数据科学……但对于数据工程本身来说Python 是一个缓慢、臃肿的语言它只是封装了其他语言。那么为什么我们要使用 Python而不是它底层的那些语言呢已经有很多人反驳我了所以我知道这并不受欢迎。 Johnny Boursiquot不可能吧……[笑] 哇好激烈的言论。 Ian Lopshire我希望它是对的…… Miriah Peterson 希望这是事实。这是在“不受欢迎的事实”部分抱歉。 Johnny Boursiquot哇…… Ian Lopshire我每天都在 Go 语言中工作……然后为了处理数据我还得切换到 Python重新记得怎么用它……所以我很希望不必这么做。但基础设施及其他东西使用别的语言做起来实在太麻烦了。 Miriah Peterson你还会经常看到的另一个 Python 场景是Python 的 SDK 有时并不实际执行代码只是配置另一个服务。而我想我们不是已经解决了这个配置问题了吗……这就是为什么我们在 DevOps 世界中使用 YAML因为它在配置方面比 Python 好得多所以总之我认为在数据工程中使用 Python 没有道理。 Johnny Boursiquot哇。我们看看当我们进行调查时你会因为这个观点招致多少批评…… Miriah Peterson我们看看有多少数据工程师在听这个播客吧。这就是我们要看的。 Johnny Boursiquot是的这可能会给我们带来全新的听众群体……[笑] Bryan你有不受欢迎的意见要分享吗 Bryan Boreham嗯……我的意见比巧克力要小众得多。我的不受欢迎意见是关于 Prometheus 查询语言 PromQL 的---我不知道你们是否熟悉它…… Miriah Peterson我很熟悉。 Bryan Boreham它有两个类似的函数---rate 和 irate。我的观点是永远不要使用 irate。 Johnny Boursiquot告诉我们为什么。 Bryan Boreham它们的区别在于---你给出一个窗口对吧你想查看的时间范围。基本上当你缩放时你会查看更大的窗口。比如当你放大时你可能会查看一分钟内的 rate而当你缩小时可能是五分钟进一步缩小可能是一个小时……而 irate 只考虑窗口中的最后两个点。因此你不应该使用它因为当你缩小窗口时它会丢弃越来越多的数据。这大大增加了你会得到错误数据的可能性。假设你查看一个五分钟的窗口而你只看窗口中最后的两个点。如果每五分钟有一个大的峰值---它看起来就像这个事情是持续发生的。 Johnny Boursiquot对。 Bryan Boreham它确实有用但使用它的场景非常少而且你必须对具体情况非常了解所以我通常会说“永远不要使用 irate”。 Johnny Boursiquot这对我来说其实很合理所以我不确定这会有多不受欢迎……但我相信会有一些人认为这不受欢迎。 Bryan Boreham是的irate 被很多人使用。我在各种人的仪表板中都见过它。 Ian Lopshirei 代表什么 Bryan Boreham我认为是“瞬时instantaneous”。 Johnny Boursiquot听起来它是一个针对非常特定用例的工具。 Bryan Boreham是的我想人们喜欢它是因为它让你的图表---因为 rate 会随着缩放平滑数据……而 irate 会保留数据中的峰值。当你使用 irate 时图表看起来更有活力显示的内容更多。 Johnny Boursiquot所以我认为这可能取决于你如何消费和可视化这些数据…… Bryan Boreham是的。 Johnny Boursiquot有意思。很酷。我有一个要带回家的观点……最近苹果发布了一款有趣的开源产品---这不是经常能看到的苹果开源。这不太常见。不过他们最近发布了一款让我觉得非常有趣的开源软件。他们推出了一种叫做 pickle 或 pkl 的配置编程语言……我敢说pkl 比 JSON 和 YAML 加起来还要好。 Bryan Boreham哇…… Ian Lopshire它和 CUE 怎么比 Miriah Peterson我本来也想问这个问题。 Johnny Boursiquot这是我脑海中第一个比较的对象。“嗯……CUE 语言。” 所以我要比较一下这两者然后再汇报。事实上我正在计划做一期关于 CUE 核心贡献者的节目。也许我会问他们“嘿现在你们似乎有了竞争对手。” 我觉得他们可能在解决同类问题---也许 CUE 有更多的细微差别但它们可能在要解决的问题上有重叠。所以是的我需要更深入地研究……但我看了一个视频读了一些文档觉得“你知道吗这确实有道理。” 就像我当时看 CUE 时想的那样“这确实有道理。” 所以我们拭目以待吧。但这就是我的不受欢迎意见。看看结果如何。 太棒了……那么Ian你有要补充的吗 Ian Lopshire没有…… Johnny Boursiquot没有……[笑] 他今天只是沉默寡言。好吧好吧好吧。那我来结束吧。
http://www.hkea.cn/news/14459256/

相关文章:

  • 上海高端定制网站公司公司邮箱价格
  • 吉林省软环境建设网站瑞安市公用建设局网站
  • 免费网站建设加盟校园在线网站怎么做
  • 浙江省永康市建设局网站进不去域名注册以后怎样做网站
  • 咖啡店网站模板创建吃的网站怎么做
  • 百度seo整站优化微信登录wordpress
  • 网站建设外包注意事项福田网站建设信科网络
  • 我做网站了珠海做网站那家好
  • stanley工具网站开发点播视频网站怎么建设
  • 网站收录少的原因南和网站建设
  • 网站 目录结构开发一个网站大概多少钱
  • 视频网站应该怎么做西安seo技术
  • 湖南平台网站建设推荐免费响应式企业网站源码
  • wordpress网站回调域iis添加网站无法访问
  • 易班网站建设设计商城网站
  • 网页设计网站有哪些京东网站建设的经费预算
  • 网站空间域名能不能自己续费网站刷流量对网站有影响吗
  • dw免费网站模板珠海网站建设培训学校
  • 用ps做的网站怎么发布wordpress 引用页面
  • 网站免费搭建平台珠海网站建设兼职
  • 做网站得多钱北京公司黄页大全
  • 网站怎么企业备案信息网站优化哪里好
  • 盐山县做网站价格程序源代码下载网站
  • 企业网站建设公司哪家好河南旅游集团 网站建设
  • 医学分类手机网站模版工商局外网
  • 做彩票网站推广集团网站设计开发
  • 搭建网站需要什么服务器办公室设计说明
  • 百度网盘搜索引擎网站网络营销公司做什么
  • 赣州建设网站微信小程序哪里找
  • 有哪些做平面设计好素材网站有哪些婚嫁网站设计