LLM推理优化技术(二)

4. 系统优化 4.1 Iteration-level Scheduling 传统上的模型推理批量计算都是将一批请求合并到一起,然后一起发送到GPU批量推理计算,完成后返回结果。但这会引起两个问题: 大语言模型请求到达时间不固定,这样先到的请求得等待后到的请求集齐一个批量,并且等待上一批量计算完成之后,才能一起发送到GPU进行批量推理计算,这对于先到的请求而言,浪费了等待时间 大语言模型每次输入和输出序列的长度不定,一般会采用padding的方法插入一些空白token用于对齐。这种批量计算对于较短的序列并不公平,它虽然已经完成,但仍然等待长序列完成之后才能返回,而且空白token用于padding计算也是浪费,GPU整体利用率也很低。 为此,2022年Orca论文1提出了iteration-level scheduling方法,将每次批量decoding作为一个迭代(iteration),每次批量decoding输出一批token之后,检查有没有完成输出的序列,如果有则从批量token序列中删除,而从排队的请求中挑选一个请求序列插入到空出的序列位置。 通过这种方法,可以有效节省等待时间,只要一个迭代中有完成的请求,就可以加入新请求继续批量decoding,而且还无需特别处理长度不一致导致的padding。这种方法将会极大提升GPU利用率,并在各个主流LLM引擎中得到了广泛应用,例如vLLM和Huggingface TGI的continuous batching,TensorRT-LLM的In-flight Batching,都是这种技术的实际应用。 4.2 Chunked Prefills Iteration-level scheduling可以极大提升GPU利用率,但还未能将GPU利用率压榨到极致。主要有两点问题: 如前所述,推理中的Prefill阶段是计算密集型,Decoding阶段是内存密集型。GPU执行prefill阶段任务时,由于大部分计算都是矩阵和矩阵乘法,因此GPU利用率可以达到最大。但当GPU在执行decoding计算时,由于大部分计算都是向量和矩阵乘法,计算受到内存带宽瓶颈限制,整体利用率很难达到最大化。 由于prefill和decoding阶段分别批量计算,存在两个阶段调度互相抢占问题。如果优先decoding,则会影响首token延迟(TTFT)。如果优先prefill,则会影响生成token间耗时(ITL)。这些调度方法都对整体的延迟长尾不友好。 Chunked prefill技术2通过将prefill拆分为多段,和decoding的序列拼在一起批量计算,来提升GPU吞吐和利用率,而且对整体服务的延迟长尾能有较好的提升。拆分主要有如下两种: 长prompt拆分为多段,调度到多次迭代批量计算中,只有最后一个段才真正使用生成的token 短prompt和decode token一起拼凑成一次批量计算,但是prompt和decode token在attention计算时分开计算。这种可以充分利用GPU算力。 Chunked Prefill的好处是将访存密集型的decode预算通过和prefill融合,从而转换为算力密集型。将decode效率得到较大提升。一些测试报告显示,chunked prefill调度相对vLLM的continuous batching调度,吞吐提升2.3倍,延迟降低50%。 该项技术已经成熟,已经被引入到vLLM,DeepSpeed等项目中,成为可以配置开启的调度类型。 4.3 PD分离 除了chunked prefill调度优化,另外一种提升GPU利用率的方法就是将prefill和decoding分别运行在不同的GPU集群3 4。这种优化可以针对算力瓶颈不同的两阶段,分配不同的GPU算力,甚至不同型号的GPU,来最大化整个集群的利用率。 典型的架构可以参考MoonCake4架构图,如下所示: 这种调度方法原理上比较直观。在prefill集群只运行prefill计算,将计算完成后得到的KV Cache传输到decoding集群,decoding集群自回归式计算输出token,完成文本生成。整个过程可以发生在一台机器中的多卡之间,通过NVLink完成卡间通讯。也可以发生在多机之间,通过IB或者RDMA网卡来通讯传输KV Cache数据。 为什么这种调度会提升GPU利用率,节省整体的GPU资源呢。可以这样模拟计算一下: 假设prefill的最大吞吐为5.6 RPS(request per second),decoding最大吞吐为10 RPS,那么如果放在一起,平均每卡吞吐最大为min(5.6, 10)/2=2.8 RPS。而将prefill阶段放在两张GPU运行,decoding阶段放在一张GPU,这样平均每卡吞吐最大为min(5.6 * 2, 10)/3=3.3 RPS,相对运行在一张卡,吞吐提升了18%。 更进一步,prefill和decoding可以采用不同的GPU型号,例如prefill集群采用算力较强的GPU,而decoding阶段采用内存带宽更大的型号,整体利用率还可以进一步提升。 PD分离带来的优势有: 可以分别优化首token延迟(TTFT)和token间延迟(ITL),由于PD分离,可以针对P和D分别采用不同的并行策略(TP或者PP),两种延迟之间不互相影响 有效控制ITL的长尾延迟。由于单卡调度Prefill和decoding,会导致prefill和decoding互相抢占GPU资源,导致最坏情况下,输出token发生严重的长尾延迟。 当然,PD分离带来的性能提升,也是建立在保证SLO目标基础上。如果不考虑SLO,PD分离带来的收益并不显著,相反,还会导致运维复杂性提高,P和D集群资源规划更加复杂。这些都是需要考虑的因素。目前PD分离也作为实验功能,进入vLLM等开源项目中。 4.4 多GPU并行推理 对于参数规模在数百B量级以上的大语言模型而言,单GPU的显存大小已经无法承载如此数量的参数和KV Cache了,这必然依赖多GPU共同加载模型,实现并行推理服务。Google在这个领域很早就开始布局5,PaLM的540B大模型就是成功部署在64块TPU v4芯片上。 多GPU并行推理,核心的难题是如何拆分模型,使得推理延迟,MFU(model FLOPS utilization),以及显存使用量同时达到帕累托最优。这个拆分还依赖模型结构和GPU的体系结构特点。例如MHA和MQA下需要考虑不同的分片方法。 模型网络每个阶段,可以采用不同的分片方法,例如在前馈神经网络层(feedforward layer)可以精巧的设计tensor的拆分,通过all-gather操作实现并行推理,在attention层则可以通过batch维度拆分,来降低显存消耗,同时降低卡间通信量,提升MFU。通过这类优化,PaLM 540B大模型的Prefill MFU可以达到76%,Decoding MFU可以达到33%。...

February 18, 2025 · Skyan

DeepSeek V3/R1 训练笔记

1. 概述 最近学习了下DeepSeek-V31和R12的论文,整个训练流程虽然复杂,但描述还是比较清楚的。也从中对大模型最新的训练流程,管中窥豹,可见一斑。 2. DeepSeek-V3 训练流程 首先DeepSeek-V3的训练比较直观,就是Pretrain→SFT→RL这种经典的方式,如下图所示: flowchart TD A{{"14.8T High-Quality Dataset"}} --> B(["Pretrain"]) B --> C["DeepSeek-V3-Base Model"] C --> D(["Supervised Fine-tuning \(2 epochs\)"]) E["DeepSeek-R1 Model"] --> F{{"Reasoning Dataset"}} F --> D G["DeepSeek-V2.5 Model"] --> H{{"Non-Reasoning Dataset"}} H --> D D --> I[DeepSeek-V3-SFT] I --> J([Reinforcement Learning]) J --> K[DeepSeek-V3] I --SFT--> L[Reward Model] L1[Rule] --> J L --> J style B fill:#424242,color:#FFFFFF style D fill:#424242,color:#FFFFFF style J fill:#424242,color:#FFFFFF 需要注意的是SFT这个阶段所使用的语料,采用了自我进化的思想,即推理数据集由DeepSeek-R1生成,而非推理类数据集,由DeepSeek-V2.5生成。...

February 13, 2025 · Skyan

LLM推理优化技术(一)

1. 前言 随着2022年底以ChatGPT为代表的LLM(大语言模型)爆发式发展,LLM成为智能时代新的增长引擎,受到学术和工业界广泛的关注。业界对于LLM的深度研究极其火热,从训练、推理、可解释性、数据集、评估方法,甚至神经科学的交叉方向都有丰富的研究成果。 作为大模型应用领域最为关心的话题,还是如何能进一步降低大模型的成本,降低响应延迟,在不影响模型效果的前提下,提升模型推理性能。这方面的工作自从2023年以来,已有诸多新技术新思想的出现,而且已经迅速成熟,成为「开箱即用」的开源组件。本文主要介绍一些工业界成熟落地的推理优化技术,作为LLM推理技术的阶段性总结。 2. LLM推理技术原理 现代主流的大语言模型都是基于decoder-only的自回归transformer模型结构。这种模型预测方式是:已知一个token序列,预测下一个token的概率。整个模型的计算非常简洁,只需前向执行多层堆叠的transformer结构,经过抽样(sampling)计算,输出下一个token。而且现代服务一般采用GPU等专用AI芯片用于推理加速计算。 模型执行一次推理分为两个阶段: 第一阶段称为prefill,将输入的文本token序列执行一遍前向计算,填充内部每一个transformer层的key-value cache,实现初始化内部KV Cache的目的,准备下一阶段的计算。此时KV Cache缓存在GPU的HBM高速缓存中。 第二阶段称为decoding,这是一个循环计算,模型使用prefill阶段填充好的KV Cache,预测下一个token,将下一个token加入输入序列的尾部,重新执行一遍前向计算并更新KV Cache,再预测下一个token,更新KV Cache,再将新生成的token加入输入序列的尾部,如此循环反复,直到遇到结尾token<EOS>或者最大输出长度限制停止,此时将生成的文本序列返回。 整个流程如下图所示: graph LR subgraph Prefill Phase A1["'世界'"] --> A2([Iterator 1]) A2 --> A3["'人民'"] end subgraph Decoding Phase B1([Iteration 2]) --> B2["'大'"] B2 --> B3([Iteration 3]) B3 --> B4["'团结'"] B4 --> B5([Iteration 4]) B5 --> B6["&lt;EOS&gt;"] end subgraph KV Cache KV[KV-Cache] end A3 --> B1 %% KV-Cache as a shared block A2 --> |Update| KV KV -->|Shared| B1 B1 --> |Update| KV KV -->|Shared| B3 B3 --> |Update| KV KV -->|Shared| B5 B5 --> |Update| KV 从上面的推理流程可以看出,prefill阶段和decoding阶段是完全不同的两个阶段。prefill阶段主要是批量计算,由于整个序列可以一次输入计算,并行度更高,所需要的GPU算力也更高,所以属于计算密集型,GPU的算力(FLOPS)越高,运算越快,模型的首token耗时(TTFT)也就越低。...

January 13, 2025 · Skyan

llama2.c 源码阅读

1. 概述 前OpenAI著名工程师Andrej Kapathy开源了llama2.c项目,该项目是llama2模型推理代码的C语言实现,用大概970行C代码实现了LLama2模型的推理算法。整个项目代码简洁高效,值得深度阅读。对掌握大模型推理算法的细节有极大的帮助。 2. 源码阅读 2.1 基础算法 RMS归一化公式是: $$ o_i = w_i \times x_i \times \frac {1}{\sqrt{\frac{1}{n}\sum_{j=0}^{n-1} x_j^2 + \epsilon}} $$ 其中,\(\epsilon\) 为防止分母为0的数值。还有RMS因子是对x的归一化,w变量是gain变量,重新缩放标准化后的输入向量。 // ---------------------------------------------------------------------------- // neural net blocks; the dynamics of the Transformer void rmsnorm(float* o, float* x, float* weight, int size) { // calculate sum of squares float ss = 0.0f; for (int j = 0; j < size; j++) { ss += x[j] * x[j]; } ss /= size; ss += 1e-5f; ss = 1....

July 6, 2024 · Skyan

X下云

背景 2022 年 10 月 27 日马斯克以 440 亿美元完成 Twitter私有化交易并改名X.com,整整一年过去后,X的工程架构也发生巨大的变化,今年10月27日X工程团队发表了一个推文,总结一年的巨大变化,我觉得这个帖子很有代表性,给所有做engineering的同学新的启发。 原文 https://twitter.com/XEng/status/1717754398410240018 This has been a year full of engineering excellence that sometimes can go unnoticed. Besides all the visible changes you see on our app, here are some of the most important improvements we have made under the hood. Consolidated the tech stacks for For you, Following, Search, Profiles, Lists, Communities and Explore around a singular product framework. Completely rebuilt the For you serving and ranking systems from the ground up, resulting in a decrease 90% reduction in lines of code from 700K to 70K, a 50% decrease in our compute footprint, and an 80% increase in the throughput of posts scored per request....

November 22, 2023 · Skyan

大语言模型如何扩充上下文长度?

我们在应用大语言模型遇到的最典型的限制就是输入文本的上下文长度。开源的模型的上下文长度限制从2K到32K不等,商业模型最大上下文限制从2K到100K范围。上下文长度对应用大语言模型有着非常关键的影响,包括知识增强、记忆等Agents完成的工作,都是为了解决大语言模型上下文长度限制而设计的。大语言模型为什么会有上下文长度限制?是否有方法能扩充长度到几倍甚至十几倍?这几个问题困扰我很久。最近一段时间经过调研之后,我发现这些问题已经有了令人兴奋的进展,我也收获一些心得,记录于此。 先说结论: LLM的训练和计算都是没有上下文长度限制的,限制的只有计算资源和模型效果 头部公司和开源社区都有了阶段性的成果,最新的transformers,llama.cpp等开源项目已经内置了扩充上下文长度方法 如何在扩充上下文长度的同时,降低训练成本,保证模型效果,是一个还在不断探索的话题 LLM的上下文长度限制之谜 实际上,目前以Transformer为核心的LLM,理论上而言是没有上下文长度限制的。唯一限制上下文长度的,只有训练时的资源消耗,以及预测时的输出效果。如果不考虑这两点,LLM完全可以支持任意长度的上下文,这里本质原因是,Transformer的核心:Attention算法和上下文长度无关。 为说明这个本质原因,我们回顾下Attention的计算,参考「Attention is all your need」经典论文1 : 定义n为输入的token数量,vocab_size为token词典大小,d为文本embedding的维度大小,k为Query和Key向量的维度大小。那么整个Attention计算过程如下: Inputs是n个token序列,查找(vocab_size, d)大小的Embedding词典,转化为(n, d)的输入矩阵X 类似的,将输入的token位置i经过位置向量计算(查表或者实时计算),转化为(n, d)的词典,和上面的X词典相加,获得带上位置向量的X作为输入。注意位置向量的计算有两种方法,一种是通过查表的方式,即查找一个(pos_size, d)大小的Embedding词典,另外一种是实时计算,根据token的位置i,通过位置embedding算法,计算出对应的位置向量。这两种方法各有优缺点,这将是突破上下文长度限制的重点。 将X乘以\(W^Q,W^K和W^V\)三个Q,K,V权重矩阵,获得Q,K,V值矩阵。其中\(W^Q\)形状为(d, k), \(W^K\)形状为(d,k), \(W^V\)形状为(d, v),注意着三个权重矩阵都和输入长度无关,获得的Q和K矩阵大小是(n, k),V矩阵大小是(n, v) 如下计算attention: $$Attention(Q,K,V) =softmax(\frac {QK^T}{\sqrt{k}})V$$ 其中\(QK^T\)计算结果为(n, n)矩阵,再乘以V的,输出结果为(n,v)矩阵。注意这些计算都是实时计算,计算复杂度和输入长度有关。 5. 在Multi-Head Attention算法中,上述4个步骤所有矩阵变成了张量,增加了h个header,输入矩阵X变成(h, n, d)大小,\(W_q\)大小为(h, d, k), \(W_k\)大小为(h, d, k), \(W_v\)大小为(h, d, v)。Q, K, V矩阵分别大小为(h, n, k), (h, n, k), (h, n, v)。通过将多头concat,输出(n, hv)大小的结果\(Attention(Q,K,V)\),再经过一次线性变化,最终Attention结果为: $$MultiHead(Q, K, V) = Concat(Attention(Q, K, V))W^O$$ \(W^O\)大小为(hv, d),所以最终Attention结果\(MultiHead\)维度为(n, d)。 从上面的计算可见,整个模型的参数,只有位置向量需要查表的时候,是和上下文长度有关,而其他的所有权重矩阵,都和上下文长度无关。如果位置向量实时计算时,attention算法所依赖的所有参数都和上下文长度无关。 那么限制到底在哪里呢?上面已经提到,限制就在: 计算资源 模型效果 先说计算资源的限制,可以证明(过程略),上述第三步计算Q,K,V矩阵的计算复杂度是\(O(nd^2)\),第四步计算attention的计算复杂度是\(O(n^2d)\),所以计算attention总体的计算复杂度是\(O(nd^2+n^2d)\),如果d > n,则\(O(nd^2)\)占计算复杂度的大头,例如LLaMa1模型的n为2048,d为4096,所以可以估计训练复杂度和训练输入的上下文长度呈线性关系。以LLaMa1模型举例,训练一次的计算成本约为300万美元,如果将输入长度扩大到8倍(即16K),那么训练成本将增长到2400万美元。因此如果要在预训练阶段用更长的上下文长度训练,这个成本将变得难以接受。...

August 4, 2023 · Skyan

大模型时代,我们如何重构产品

近期厂内关于AI云原生思维的讨论非常热烈,尤其是AI大模型时代,产品如何重做重构的话题异常火热。关于这个话题,我倒是联想到一句经典的谚语:“It is like teenage sex: Everyone talks about it, nobody really knows how to do it, everyone thinks everyone else is doing it, so everyone claims”. 但说说容易,我们应该如何思考这个话题呢?在我看来,需要引入一些模型化思维来拆解这个问题,这样很多现象就能得到很好的解释,关于如何重构产品也就能逐步看到清晰的路线了。 首先,我们可以观察到,一切复杂系统,包括人体,组织,社会,甚至互联网,都是由三个子系统组成: 信息系统:数据,文章,网页,图片,视频等等一切我们能看,能读,能听,能感受到的东西,这个大千世界充满了信息,信息可以是数字化的,例如互联网上的数据,也可以是模拟的,例如线下的交流对话等 模型系统:通过各种推理,拆解,演绎,总结等手段,将信息转化为知识沉淀下来,例如人的大脑内部就存在一个抽象的世界模型,这个模型赋予了我们认知的能力,让我们可以产生意识,理解这个世界 行动系统:当模型处理完信息之后,它需要执行一定的操作和这个环境互动,这就是行动系统的作用。例如我开车过程中看到了一只小狗,我的大脑获取这个信息后,交给认知模型进行分析和处理,认知模型决策需要避开,我的行动系统的手和脚操作方向盘和刹车,完美避开相撞的风险。 当把互联网这样的复杂系统拆解为这三个子系统之后,最近三十年互联网发生的事情,就能用这三个系统的变化得到如下分类: 信息时代:从1995年开始,我们进入了信息时代。这个时代的互联网产品,本质上都是在做信息的搬运工。这其中还细分多个阶段,例如90年代开始由论坛,新闻门户网站组成的Web1.0阶段,初步形成信息的数字化。典型的代表有雅虎,新浪,搜狐和网易这些老牌门户网站。从2000开始,随着Web2.0的兴起,我们有了更高效的搜索引擎,社交网络,电子商务平台,实现了信息更高效的运转,真正将获取信息的边际成本降为了0。这个阶段出现了很多伟大的公司,典型的代表有Google,Amazon,百度,腾讯,阿里等。正是有了这些平台,我们才真正实现了获取信息效率的极大提升。到了2010年以后逐步兴起的移动互联网,又诞生了一批新的平台公司,实现了移动设备上的信息生产和消费,让用户可以随时随地地获取信息,使用信息 模型时代:其实从2010年前后,互联网平台产品就已经开始具备了模型能力,但这种模型还是“小”模型,并不具备转化为知识的能力。例如在搜索引擎中,我们有LTR模型来优化排序,在推荐引擎中也有稀疏大模型来优化推荐的列表。但这些模型都专属于某一个垂直领域,无法通用化使用。一个搜索引擎的模型是无法用在推荐系统中的,模型应用的边际成本依然很高。直到2022年开始,以OpenAI的ChatGPT为代表的大模型真正登台之后,我们才真正步入了模型极大发展的阶段。代表特色就是这种模型具备通用的认知能力,将使用和获取知识的边际成本真正降为0。举个例子来说,在模型时代之前,我们想了解一种疾病如何治疗,需要去阅读,去理解各种专业知识,甚至花费数年专业的学习才能真正解决临床上的病症。但如果有一个经过医学专业知识训练好的大模型,通过大模型就直接获得了治疗方法,这就是将获取知识的成本降为了0。当前我们正处于这个模型时代刚开始的时期 下一个时代——行动时代:行动时代其实已经初露雏形了。典型的代表就是自动驾驶。自动驾驶是将我们驾驶行动的边际成本降为0,我们不需要付出太多获取信息-认知处理-操作汽车这一系列的成本,自动驾驶就自动替我们解决了。这个时代同样也处于早期阶段,但并不遥远 因此,在当前的模型时代,我们需要思考的是,如何用大模型来改造以及创造各种产品,将各行各业需要人来认知的过程的边际成本降为0,将是这个时代的主旋律。 以New Bing为例,为什么搜索是第一个大模型为先的应用及平台,从上面的分析可以获取答案。在信息时代,搜索产品本质上是人通过搜索query来找信息,找知识,完成任务,如下结构所示: 人 → 信息 但人在用搜索引擎找信息的过程中,还需要去理解网页信息,阅读内容,并将这些网页内容通过人自身的认知模型转换为知识,转换为下一步的行动。而且每一次搜索,都要重复这一过程,付出类似的成本,这就是人找信息过程中,需要付出的边际成本。 人 → 理解认知 → 信息 但大模型的出现,将中间需要人的认知模型来理解的步骤交给大模型来完成,整个流程变成了: 人 → 大模型 → 信息 这就意味着,大模型可以部分代替人的认知能力,把繁琐的总结,汇总,提取信息的边际成本都降为0,直接可以获得人想获得的答案。这就是经过大模型改造后的搜索引擎,其效率将比传统的搜索引擎更为高效,成本更低。 更进一步,人在找到信息之后,还会有下一步的行动。传统上这些行动还需要人去做判断,思考,决策才能执行这些行动。这个阶段的边际成本也可以交给大模型来降低,如下所示: 人 → 大模型 → 信息 → 大模型 → 人 → 行动 其实上面本质上就是一种所谓的端到端闭环。举一个例子,我希望买一个空调,在搜索引擎搜索后,大模型帮我筛选了若干最新款空调的信息,并总结展示给我供我选择,但我决定选某一款空调后,大模型帮我进一步规划去哪里买,什么价格最便宜,售后服务更完善,最终高效地完成了一次人找信息,再从信息到行动的闭环。 以上,就是New Bing在这次大模型时代对传统搜索引擎做出的重构:通过在人找信息这个环境中加入大模型,将人的理解和汇总的边际成本降为0,甚至实现信息到行动的0边际成本的延伸,真正实现一个高效率的搜索引擎。 如果这样推演,我们可以扩展到更多互联网产品和平台上么?完全是可能的。我们就以另一个Web2.0时代产物,推荐系统为例,尝试推演推荐系统如何应用大模型。...

July 6, 2023 · Skyan

Google abseil开源项目介绍

Google abseil是Google开源的优秀C++基础库,持续维护并且持续迭代。该库的代码质量和工程化水平属于业界顶级,值得我们在实际生产中使用和学习。不仅要善于使用abseil库,还要多看abseil的文档和代码,从中学习Google业界领先的C++经验。 这里先介绍几个abseil库的经典组件: 容器 Recommendation Prefer absl::flat_hash_map or absl::flat_hash_set in most new code (see above). Use absl::node_hash_map or absl::node_hash_set when pointer stability of both keys and values is required (rare), or for code migrations from other containers with this property. Note: Do not use popularity as a guide. You will see the “node” containers used a lot, but only because it was safe to migrate code to them from other containers....

June 1, 2023 · Skyan

FlameGraph火焰图原理

FlameGraph是世界知名计算机性能优化专家Brendan Gregg发明的一种性能数据可视化方法。通过不同色块可交互的展示,可动态展示系统运行时的性能热点。FlameGraph如下图所示: 按照作者介绍,FlameGraph可以用于分析CPU耗时,内存分配,非CPU耗时(线程等待,调度性能),混合运行时(CPU和非CPU运行混合),以及性能对比这5种性能分析的可视化。我们常用FlameGraph生成CPU耗时分析图,用来找到服务运行性能的热点,并专项优化以节省资源成本。其实,FlameGraph也有一套标准数据格式,根据这个格式,可以在任何适合的场景中生成所需要的FlameGraph,并可视化可交互地展示和分析。 FlameGraph适合用于有调用栈的数据可视化,通过一种类似柱子的布局,加上暖色调的颜色,很像火焰一样的图案,所以被称为火焰图。以大家常用的CPU耗时火焰图为例,它的组成元素如下: 一个函数调用栈用一列方框组成,每个方框代表一个函数 y轴代表函数调用的深度,从底向上逐层递进调用,最顶端的函数代表性能采集时刻正在运行的函数,下面是它的父函数,依次类推 x轴代表不同采集栈的集合。需要注意的是x轴并不代表时间顺序,同一层的函数按照字母序从左往右排列。这样如果两个同名函数在同一层,将会被合并成一个区块。 方框的宽度代表该函数在调用栈抽样中出现的次数,如果宽度越宽,代表这个函数在栈抽样中出现的越频繁,从CPU耗时的角度来说,也表示这个函数更耗时。 如果方框宽度足够则展示函数的全名,如果不够则展示部分或者不展示,鼠标放上去可以展示全名 每个方框的背景色其实并没有什么特别,其实就是选取了一组随机的暖色调。主要是为了方便眼睛能区分不同层的方框。 一个火焰图可以用于可视化单线程,多线程,多进程甚至多主机的性能数据。也可以为每个线程生成一份独立的火焰图用于更详细的分析。 方框的宽度不仅限于表示抽样次数,也可以表示其他指标。例如宽度可以表示线程阻塞的时长,这样的一个火焰图可以很清晰的看出哪些函数在阻塞线程,以及整个线程阻塞时函数的调用栈情况 以上即是一个火焰图静态的组成部分,更为有趣的是,Gregg也为火焰图增加了互动功能,使得用户体验更佳。 火焰图本身是一个SVG文件,配合Javascript,支持鼠标悬浮,点击放大,以及搜索功能: 鼠标悬浮:当鼠标光标放置到一个方框上方时,可以展示该方框的详细数据(函数名,采样次数,以及占比) 点击放大:当鼠标点击一个方框时,火焰图按照垂直方向放大,该方框以上的函数将被放大展示,其他部分浅色处理,方便聚焦一个函数的分析 搜索:可以用Ctrl/Command+F快捷键或者右上角搜索功能,按照函数名搜索。搜索到的函数会高亮显示,还是在右下角展示所有搜索到的函数出现次数占比 火焰图生成流程是怎样的呢?按照Gregg开源的flamegraph.pl程序,生成一个火焰图只需要3步: 从perf,dtrace,gperftools等程序中获取运行时的调用栈数据 转换为折叠栈格式(Fold stacks) 调用flamegraph.pl生成火焰图 原理非常简单。所以搞懂火焰图只需要明白折叠栈是怎么回事即可。首先一个函数的调用栈可能长这样: func_c func_b func_a start_thread clone func_d func_a start_thread clone func_d func_a start_thread clone 上面展示了三次性能抽样,每次抽样从底向上显示了一个调用栈,折叠栈格式如下: clone;start_thread;func_a;func_b;func_c 1 clone;start_thread;func_a;func_d 2 其实就是将三个栈做了一个汇总,将同样的栈汇总到一起,每个函数用’;‘分隔,最后加空格和打印出现次数。 前面还可以加上程序的名称,例如cpp,如下也是一个合法的folded stacks: cpp;clone;start_thread;func_a;func_b;func_c 1 cpp;clone;start_thread;func_a;func_d 2 有了折叠栈数据,即可利用flamegraph.pl生成对应的火焰图svg文件。该文件可以嵌入到任何网页中展示,简单方便。 进一步,我们可以利用火焰图原理,生成各种场景所需要的“火焰图”,用于性能分析,数据可视化展示等功能。我们以应用层算子调度框架的性能分析为例,展示如何利用火焰图生成算子耗时可视化图。 应用层算子调度框架常见于RPC服务端的应用层,主要思想是将策略逻辑封装成若干个函数式的算子,再通过调度框架,串行或者并行调度这些算子运行,最终产出RPC的返回结果。算子调度的拓扑图有叫Tree,也有叫DAG的,也有叫图的。下图所示就是一个典型的算子调度框架的运行时拓扑图。 针对这样的算子调度,经常需要分析的问题有:哪个算子耗时最高,最长路径在哪里,哪个算子可以优化调度路径等,我们通常通过打印日志和统计最长路径等方法来分析运行时性能情况。但同样,通过火焰图也可以将运行拓扑的耗时可视化起来。 生成火焰图的关键即为生成栈的汇总数据,假设每个算子的耗时情况如下: 运行阶段 解析请求 访问A服务 访问B服务 汇总结果 产生返回数据 开始运行时间 0 10 10 24 32 结束运行时间 10 20 24 32 34 我们想象有一个抽样程序,可以按照上面的运行时图,从左往右分别垂直抽样,可以看出在不同时间段有这样几种“栈”:...

December 5, 2022 · Skyan

程序框架的最佳实践【翻译】

原文作者Chris Nokleberg and Brad Hawkes来自Google,Java框架开发者 原标题:程序框架最佳实践——虽然很强大,但并不适合所有人 共享代码库鼓励代码复用,实现不同团队技术的一致性并改进产品效率和质量。开发者需要选择合适的库,研究如何正确配置,最终把所有的库组装在一起。程序框架可以通过对库的默认安装和配置,来简化开发流程,提供更好的一致性,当然也损失了一些库的便利性。 框架不仅是一堆库的集合,还控制了整个程序的生命周期。确定的框架行为可以为开发开拓空间——例如,不用为每个程序应用都深入审查安全和隐私相关的代码。框架提供了跨团队和跨语言的功能一致性,同时也是更高级的自动化和智能系统的基础。 这篇文章先从框架的核心方面综述开始,然后深入到框架的优势、妥协以及我们推荐实现的最为重要的框架功能。最后,这篇文章展示了一个Google的实际框架应用案例:如何开发一个微服务平台,使得Google可以打破单一代码库的限制,以及框架如何使这一切成为可能。 什么是框架 框架和共享代码库在很多方面都类似。在Google,有两个技术原理用于区分框架和库:依赖反转和可扩展性。虽然看起来比较容易,但本文所讨论的框架很多优势,原则上都是从这两个原理衍生出来的。 IOC(依赖反转) 当从头开始开发一个程序的时候,工程师来决定程序的流程——这通常称为普通控制流。在一个基于框架的程序中,则是由框架来控制流程以及调用用户的代码——这叫做反转控制流。反转控制流有时也被引用为好莱坞原则:“别给我们打电话,我们会给你打电话”。框架控制流很好地定义了不同程序之间的标准。理想情况下,程序只需要实现它们特有的逻辑,而由框架来解决所有其他构建一个微服务所需要的各种细节。 可扩展性 扩展性是区分库和框架的第二个重要特点,它和依赖反转互相配合。因为框架的控制流是由框架负责的,唯一的改变框架运行方式的方法只能通过它暴露出来的扩展点。例如,一个服务端框架可能有一个扩展点是允许一个应用程序在每个请求来的时候运行一些代码。扩展点的模式也意味着框架的其他不可扩展性部分很固定,应用程序无法改变。 框架的好处 框架除了提供共享代码库所能提供的功能之外,还有很多好处,它们对不同的角色在不同方面都有益处。 对开发者 开发者这个角色最终决定是否使用一个可用的框架,也是最显然从框架中获益的角色。大部分的开发者的好处在于提升生产力,简便高效以及可应用最佳实践。开发者可以写很少的代码,利用内置的框架功能。由于框架处理了各种样板代码,因此他们所写的代码可以大幅简化。框架提供了合理的默认配置,消除了无意义而且浪费时间的决策判断,从而提供了充足的最佳实践。 对产品团队 除了为开发者提供生产力以外,框架也为产品团队解放了团队浪费在重复建设基础设施的资源。产品团队因此可以更关注于使他们产品与众不同的功能开发了。 框架可以让产品团队的开发与底层基础设施的变更隔离开,这也能让他们从中受益。虽然并不是普遍适用,但框架提供了额外的抽象,这意味着有些基础设施的迁移可以被当做实现细节,完全由框架维护者全权处理。 Google产品的发布需要很多团队的确认。例如,一个发布协调工程师负责审查产品安全性和有效性,同时有一个信息安全工程师检查程序应用的场景安全漏洞防范设计。当负责审查的团队熟悉框架并可以信任框架的功能保证时,框架就可以帮助简化审查流程。产品发布以后,标准化流程可以让系统更加便于管理。 对公司 从公司层面来说,常用的框架可以减少开发者上手启动一个新应用的时间,从而提升开发者的灵活性。如果一个公司有一个足够大的程序员社群,投入资源在高质量的文档和培训程序员将变得非常有价值。这也有助于吸引社群贡献文档和代码。一个被广泛应用的框架意味着,对框架很小的改进投入可以换来巨大的收益影响。 在过去一段时间,收敛框架架构可以使针对全局性的变更有广泛的应用范围。例如,如果你依赖一个统一的微服务/RPC框架,而且带宽比CPU更贵,那么框架可以基于成本权衡来优化默认的压缩参数。 框架的权衡 虽然框架有上述的多种好处,但也面临着各种权衡。 死板的框架能够掩盖创新 框架常常决定支持哪种技术方案。由于支持所有能想到的技术是不现实的,所以一个比较死板的框架也是有明显的好处的——那就是,它们更鼓励使用某种技术或者更偏好某种设计模式。 死板的框架可以极大简化一个开发者从无到有创建一个系统的工作量。当开发者有很多种方式来完成同一种任务时,他们很容易陷入是否会影响整个系统的细节决策中。对于这些开发者而言,接受一个框架所推荐的技术,可以让他们把重点放在构建他们系统的业务逻辑上。有一个普适并且一致的技术偏好对于整个公司而言更有利,虽然这个答案还不尽完美。 当然,你也必须处理程序和团队的长尾问题,有些产品需求或者团队喜好和现有的框架可能并不完全适配。框架的维护者被放在一个判断什么是最佳实践的位置上,也会需要确认一个不方便的使用案例是否“真实”,因为这个案例可能对每个用户来说都不方便。 另一个需要重点考虑的是,即使某些技术在今天看来的确是一个最佳实践,但技术也在不断快速地进化,对于框架而言也有一定的风险无法跟上技术创新的发展。采用不同的程序设计方案可能开发成本更高,因为开发者既需要学习框架实现细节,也需要框架维护者的协助。 普适性也会导致不必要的抽象 很多框架优势,例如通用的控制面板(后面解释),只有当大部分关键重要的应用都基于这个框架才有真正的意义。这样的一个框架必须足够普适来支持绝大多数用户的场景,这也意味着必须拥有丰富的请求生命周期,以及任何程序都需要的所有的扩展点。这些需求有必要在应用和底层库之间增加若干间接层,这些层也会增加学习成本以及CPU开销。对于应用开发者而言,软件栈中的更多层也会导致调试更复杂。 另一个框架潜在的缺陷是,它们需要工程师额外学习。当新来的Google员工学习如何让一个“hello world”样例运行的时候,他们经常会被他们所需要学习的技术的数量所抓狂。一个功能完备的框架也会让这个情况更糟而不会更好。 Google已经开始尝试让每个框架的核心尽量简单符合预期,让其他功能都成为可选模块,从而来解决这些问题。Google也会尝试提供配套框架的工具,可以帮助了解框架内部结构来简化调试。最终,虽然框架有一些你必须学习的成本,但你需要确保任何一个给定的框架提供了足够的好处来弥补这个成本。不同的编程语言的框架也许有不同的权衡组合,对于开发者而言这也是一个新的决策点和成本/收益权衡场景。 重要的框架特性 如上所述,反转控制和扩展性是框架最为基础的两个特点。除了这些基础能力以外,框架还需要负责若干其他功能。 标准化程序生命周期 再次重申,反转控制意味着框架拥有并且使一个应用程序的整个生命周期标准化,但这样的结构带来什么样的好处呢?我们以避免级联故障为例来说明这个问题。 级联故障是一个典型的引起系统超载的原因,Google内部也很有很多。它可能发生在分布式系统的部分服务失败,增加了其他部分失败的可能。关于级联故障更多的原因,以及如何避免他们,参考SRE(Site Reliability Engineering (O’Reilly Media, 2016))中的定位级联故障这一章。 Google的服务框架有很多内置的避免级联故障的保护功能,两个最重要的原则是: 持续运行。如果一个服务能成功响应请求,那么就应该保持。如果它能处理一部分请求但不能处理其他部分请求,那么就必须继续运行,而且响应它能服务的请求。 快速启动。一个服务必须尽量快地启动。更快地启动意味着可以快速从崩溃中恢复。服务必须避免类似启动时顺序等待RPC访问外部系统返回这样的操作。 Google生产环境给每个服务配置了一个成为健康状态(可以响应请求)的时长。如果超时了,系统则认为一个无法恢复的故障发生了,则终止这个服务进程。 有一个常见的反模式,常发生在缺乏框架的场景中:一个库创建他们管理的RPC连接,并等待连接可用。当服务端代码随着时间推移不断膨胀,你能获得一个实际有几十个这样的有序依赖的库。这样的结果是,处理初始化代码有效展开的话看起来如图1所示: 在通常的情况下,这样的代码工作很好。但因为没有任何潜在问题的提示,这样的代码会有个特殊问题。这个问题只有当一个相关的后端服务慢了或者同时挂了才会显现出来——这时关键服务启动已经延迟了。如果服务启动足够慢,那它将会在有机会处理请求之前被杀死,这会导致一场级联故障。 一种可能的改进是先创建RPC stubs,如图2所示,然后并行等待它们初始化完毕。在这种情况下,你只需等待stub初始化时间最长的那个而不是初始化时间之和。 虽然处理的并不十分完美,但这个有限的重构也说明了你需要某种在库所创建的RPC stubs之间的协调机制——它们必须可以有等待stub以及库以外的某些资源的能力。在Google的案例中,这样的事情是由服务端框架负责的,同时它也有如下功能: 通过定期拉取可用性来并行等待所有stubs可用(<1秒)。一旦配置的超时到了,即使并不是所有后端服务都准备好,服务依然可以继续初始化。 去掉用于人工或者机器可读的调试日志,而是用集成标准化监控和报警来代替。 通过一个通用的机制来支持插拔不同的资源,而不仅是RPC stubs。技术上来说,仅有返回布尔值的函数(为了“我是否准备好”)以及对应的名字,是有打印日志的必要。这些插拔点常被那些处理资源的通用库(例如:文件API)所使用;程序开发者通常只需要用这个库,就能自动地拥有这些能力 提供一个中心化的方式配置特定的关键后端服务,可以改变他们启动或者运行时的行为。 对任何一个单独的库来说,这些功能可能(也正确地)被认为是多了,但实现这些功能,能够让你在一个集中的地方实现,并在所有用这个库的后端服务都能生效。这也是有意义的一件事情。就像共享库是一种在不同应用程序中分享代码的方式,在这个定义下,框架也是一种在不同库之间共享功能的方式。 因为有类似这些的功能,SRE们更愿意支持基于框架的服务。他们也经过鼓励他们对口的程序员选择基于框架的开发方案。框架提供了一个生产规范的基础水平。这个生产规范将一堆没有关联的库关联到一起时是非常难以实现的——但也不是不可能。 标准化的请求生命周期 虽然细节取决于应用程序的类型,但很多框架支持在总的程序生命周期以外的生命周期管理。对于Google的服务端框架而言,最为重要的任务单位就是请求。除了遵循类似依赖反转的模型以外,请求生命周期管理的目标是将请求的不同方面的职责划分成独立的可扩展的代码片段。这允许程序开发者只关注于开发让他们的应用程序独一无二的实际业务逻辑。 这里有一个这样的实际使用的框架例子,它的组件片段见图3: Processors——拦截请求和返回的包。常用于打印日志,但也有一些短路请求的功能(例如,强制在整个程序中不强制执行不变量) Action——程序业务逻辑,接受请求,返回一个对象,可能有一些副作用。 Exception handler——将一个未捕捉的异常转换为一个响应对象。 Response handler——序列化一个响应对象给客户端。 虽然程序能从框架的扩展点中收益,程序代码的绝大部分都是以响应的形式执行,包括程序特有的业务逻辑。...

December 5, 2022 · Skyan