字节青训营第十三课之深入浅出RPC框架的笔记与总结.md

2023-10-26

基本概念

本地函数调用

函数调用完整过程如图,藏实际上编译器经常优化,参数和返回值少时直接将其存在寄存器,不需操作栈,直接online不需call:
在这里插入图片描述

  1. 将a和b的值入栈
  2. 经函数指针找到calculate,进入函数取栈中值2和3赋给x和y
  3. 计算x*y并将结果存在z
  4. 将z的值压栈,再从calculate返回
  5. 从栈中取出z返回值付给result

远程函数调用

RPC需解决的问题:

  1. 函数映射

远程调用无法根据函数指针确定函数,因为两进程地址空间完全不同,因此函数都有自己的ID,做RPC时都要附上该ID,且要有ID和函数的对照关系表,以此经ID找到函数并执行

  1. 客户端如何传参给远程函数

本地只需参数入栈,函数自己去栈中读,但远程调用时,客户和服务端进程不同不能经内存传参,需客户端把参数先转成字节流,传给服务端后,在把字节流转成自己能读取的格式

  1. 网络传输

远程调用往往在网络上,需保证网络上高效稳定传输数据

RPC概念模型

RPC最早由Nelson发表的论文<<Implementing Remote Procedure Calls>>中提出,提出RPC过程由5个模型组成:User、User-Stub、RPC-Runtime、Server-Stub、Server
在这里插入图片描述

一次RPC完整过程

一次RPC的完整过程包括IDL文件、生成代码、编解码、通信协议、网络传输。如图:
在这里插入图片描述

  1. IDL文件:通过中立的方式描述接口(方法和参数),使不同平台运行对象和不同语言的程序可相互通信
  2. 生成代码:编译器工具把IDL文件转成语言对应的静态库
  3. 编解码:从内存中表示到字节序列的转换称为编码,反之为解码,也叫序列化和反序列化
  4. 通信协议:规范数据在网络中的传输内容和格式,除必须的请求/响应数据外,通常还包含额外的元数据
  5. 网络传输:通常基于成熟网络库走TCP/UDP传输

RPC的好处

  1. 单一职责,有利于分工协作和运维开发,开发部署及运维都独立
  2. 可扩展性强,资源使率更优,如压力大时扩展资源,底层基础服务可复用
  3. 故障隔离,某个模块发生故障不影响整体可靠性,服务的整体可靠性更高

RPC带来的问题

当然RPC也带来很多问题:

  1. 假如服务宕机,对方如何处理
  2. 调用过程中发生网络异常,如何保证消息可达性
  3. 请求量突增导致服务无法及时处理,应对措施有哪些

分层设计

以Apache Thrift为例,分层如图:
在这里插入图片描述

编解码层

编解码层中客户端和服务端都依赖IDL文件,生成不同语言的CodeGen,如C++和Golang等

  1. 数据格式包括三种:

    • 语言特定格式:很多编程语言内置的将内存对象编码为字节序列的支持,但通常与特定编程语言深度绑定,其他语言很难读取,同时可能有安全和兼容性问题
    • 文本格式:有较好人类可读性,如json、xml、csv等文本格式,但编码会有歧义,如xml和csv不能区分数字和字符串,json虽然可区分,但不区分整数和浮点数且不能指定精度,json需反射机制故性能较差
    • 二进制编码:跨语言和高效能的优点,实现可有TLV和Varint编码,常用Protobuf和Thrift的BinaryProtocol
  2. 二进制编码

    • TLV编码:Tag标签,理解为类型;Length长度;Value值,也可是个TLV结构。如图:首个byte是类型如string、int、list,其次是field tag而不是字符串如userName等,取代key,后续分别是长和值。尽管TLV简单扩展性好,但增加Type和Length两冗余信息,有内存开销因此也有Varint编码,不详叙
      在这里插入图片描述
  3. 选型:从三个层面考虑选型:兼容性、通用性、性能

    • 兼容性:支持自动增加新字段而不影响老服务,这件提高系统灵活度以适应需求的变化
    • 通用性:技术层面是否支持跨平台、跨语言,流程程度,使用少的协议意味着高成本学习,流行度低往往缺乏成熟跨语言、跨平台公共包
    • 性能:空间开销,序列化需在原有数据上加描述字段,以反序列化解析,若开销过高可能导致网络磁盘压力,时间开销,复杂协议解析需较长时间

协议层

协议是双方确定的通信的语义。包括两种:

  • 特殊结束符协议:特殊字符作为协议单元结束的标示,如http协议头以回车和换行符号作为结束标示
  • 变长协议:定长和变长部分组成,定长部分需描述不定长内容长度

下图是变长协议,除长度和内容外,还包括其他内容如版本信息、数据包序列化等:
在这里插入图片描述
协议解析包括以下几部分:

在这里插入图片描述

网络通信层

通信流程

底层通过socket api进行通信:
在这里插入图片描述

socket API

socket创建套接字,bind将套接字绑定到地址,listen监听进来的连接,参数backlog指定挂起的连接队列的长度,当客户端连接时,服务器可能正处理其他逻辑而未调accept接受连接,会导致该连接被挂起,内核维护挂起的连接队列,accept从队列中取出连接请求并接收它,该连接从挂起队列移除。若队列未满,调用connect马上成功,若满可能会阻塞等待队列未满(实际测试并不是这样后面专门研究)。backlog默认128,通常指定128即可。connect客户端向服务器发起连接,accept接收一个连接请求。得到客户端的fd后,可调read,write和客户端通讯,read/write从fd读/写数据,socket默认阻塞,如果对方没有写/读数据,read/write会一直阻塞

对端socket关闭后,这端尝试去读会得到一个EOF并返回0。去写会触发SIGPIPE信号并返回-1和errno=EPIPE,SIGPIPE默认终止程序,故通常应忽略该信号,以免程序终止。但如果这端不读写,可能无法知道对端socket关闭

网络库

  • 提供易用API,封装底层socket API,连接管理和事件分发
  • 协议支持tcp、udp等,支持优雅退出异常处理等功能
  • 应用层buffer减少copy,支持高性能定时器、对象池等以满足性能要求

关键指标

稳定性

保障策略

稳定性是保障策略,包括熔断、限流、超时三方面,某种程度上说,超时、限流、熔断也是服务降级手段:

  • 熔断:保护调用方,防止被调用服务出现问题影响整个链路。如A调服务B时,B又调C,若C响应超时,C超时导致B业务一直等待,A继续调B,服务8就可能堆积大量的请求而导致宕机,由此导致服务雪崩
  • 限流:保护被调用方,防止大流量把服务压垮,当调用端发请求过来时,服务端在执行业务逻辑前先检查限流逻辑,若访问量过大且超出限流条件,就让服务端降级或返回给调用方限流异常
  • 超时:当下游服务因某种原因响应过慢,下游服务主动停掉不太重要的业务,释放服务器资源避免浪废

请求成功率

为提高请求成功率,有负载均衡和重试方法,前者根据服务集群情况,通过算法将请求分发给空闲的服务器,使所有服务器负载处于平衡状态。后者重新请求看是否成功,但重试会加大下游负载,可能放大故障风险,防止重试风暴,应限制单点重试和限制链路重试
在这里插入图片描述

长尾请求

一般指延迟明显高于均值的那部分占比较小的请求。P99标准把单个请求响应耗时从小到大排序,99%后的1%可认为是长尾请求。在复杂系统中,长尾延时总会存在。造成的原因有网络抖动、GC、系统调度
在这里插入图片描述
假定阈值t3(比超时时间小,通常建议是RPC求延时的pct99),Req1发出去后超过t3时间没返回,就直接发起重试Req2,相当于同时两个请求运行。等待请求返回,只要Resp1或Resp2任意返回成功,就可立即结束,整体的耗时是t4,表示首个请求发出到成功返回之间的时间,相比等待超时后再请求,该机制大大减少整体延时

注册中间件

上述提到的策略及请求成功率、长尾请求都可通过中间件实现,Kitex Client和Server的创建接口均采用Option模式,提供极大灵活性,很方便就能实现
在这里插入图片描述

易用性

  • 开箱即用:合理的默认参数选项、丰富的文档
  • 周边工具:生成代码工具、脚手架工具

Kitex使用Suite来打包自定义功能,提供一键配置基础依赖的体验,简单易用,支持protobuf和thrift,内置功能丰富选项,支持自定义生成代码插件等

扩展性

请求发起会首先经过治理层面,治理相关逻辑被封装在middleware中,这些middleware会被构造成有序调用链逐个执行,如服务发现、路由、负载均衡、超时控制等,执行后进入remote模块,完成通信
在这里插入图片描述

观测性

除传统的Log、Metric、Tracing三件套外,对于框架来说可能还不够,还有些框架自身状态需暴露出来,如当前环境变量、配置、CIient/Server初始化参数、缓存信息等
在这里插入图片描述

高性能

高性能场景:单机多机、单连接多连接、单/多client 单/多server、不同大小请求包、不同请求类型

目标:高吞吐和低延迟,两者都重要甚至大部分场景下低延迟更重要

手段:连接池、多路复用、高性能编解码协议、高性能网络库

企业实践

以Kitex为例,它是字节内部多年最佳时间沉淀出来的高性能可扩展性的go rpc框架,难能可贵的是以大型互联网公司内部的实践经验为例,拓展技术视野

整体架构

在这里插入图片描述
core是核心组件,是主干逻辑,定义框架层次结构、接口及默认实现,最上面的client和server对用户暴露,下面的是框架治理层面的功能模块和交互元信息,remote是与对端交互的模块,包括编解码和网络通信。右边绿色的byted是字节内部的扩展,集成内部的二方库还有与字节相关的非通用的实现,byted部分是在生成代码中初始化client和server时通过suite集成进来的,方便与字节内部特性解耦,方便后续开源拆分,右边tool则是与生成代码相关的实现,包括id解析、校验、代码生成、插件支持、自更新等

自研网络库

背景

  • 原生库无法感知连接状态:使用连接池时,池中存在失效连接,影响连接池的复用
  • 原生库存在goroutine暴涨的风险
    一个连接一个goroutine的模式,由于连接利用率低下,存在大量goroutine占用调度开销,影响性能

自研网络库Netpoll

Go Net VS Netpool

  • GoNet使用Epoll ET,Netpoll使LT
  • Netpoll在大包场景下占更多的内存
  • GoNet只有一个EpoII事件循环(因为ET模式被唤醒的少,且事件循环内无需负责读写活少),而Netpoll允许有多个事件循环(循环内需要负责读写活多,读写越重,越需要开更多Loops)
  • GoNet一个连接一个Goroutine,Netpoll连接数和Goroutine数量无关,和请求数有关,但是有Gopool
  • GoNet不支持ZeroCopy,甚至若用户想实现BufferdConnection这类缓存读取,还会产生二次拷贝。Netpoll支持管理一个Buffer池直接交给户,且上层用户可不使用Read(p []byte)接口而使用特定零拷贝读取接口,对Buffer管理,实现零拷贝能力的传涕

Netpoll

  • 解决无法感知连接状态问题:Go Net无法感知连接状态,在用长连接池时,池中存在失效连接,严重影响连接池的使和效率。Netpoll引入epoll主动监听机制,感知连接状态
  • 解决goroutine暴涨的风险:Go Net缺乏协程数量的管理,Kite采取一个连接一个goroutine模式,连接利用率低,存在较多无用goroutine,占用调度开销影响性能,Netpoll建立goroutine池,复用goroutine
  • 提升性能:Netpoll基于epoll的Reactor模型,服务端主从Reactor模型,主reactor接受调用端的连接,将建立好的连接注册到某从Reactor上,从Reactor负责监听连接上的读写事件,将事件分发到协程池处理;引入NocopyBuffer,向上层提供NoCopy的调用接口,编解码层面零拷贝

扩展性设计

支持多协议,也支持灵活自定义协议扩展
在这里插入图片描述
框架内部不强依赖任何协议和网络模块,可基于接口扩展,在传输层上则可集成其他库进行扩展,目前集成有自研的Netpoll,基于netpoll实现http2库

性能优化

  • 调度优化:epoll_wait在调度上的控制;gopool重用goroutine降低同时运行协程数
  • LinkBuffer:读写并行无锁,支持nocopy地流式读写;高效扩缩容;NocopyBuffer池化,减少GC
  • Pool:引入内存池和对象池,减少GC开销
  • Codegen:预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝;lnline减少函数调用次数和避免不必要的反射操作等;自研Go语言实现的Thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,并支持插件机制-Thriftgo
  • JIT:使用JIT编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担;基于JIT编译技术的高性能动态Thrift编解码器-Frugal

合并部署

微服务过微,传输和序列化开销越来越大;将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用
在这里插入图片描述

  • 中心化的部署调度和流量控制
  • 基于共享内存的通信协议
  • 定制化的服务发现和连接池实现
  • 定制化的服务启动和监听逻辑
    在这里插入图片描述
    某抖音服务,30%合并流量,服务端CPU减少19%,延迟PCT99减少29%

参考文献

  1. 官方文档 Kitex Netpoll

  2. 字节跳动 Go RPC 框架 KiteX 性能优化实践_架构_字节跳动技术团队_InfoQ精选文章

  3. [字节跳动微服务架构体系演进_架构_字节跳动技术团队_InfoQ精选文章](

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

字节青训营第十三课之深入浅出RPC框架的笔记与总结.md 的相关文章

  • 具有多个等待组的管道中通道范围内的死锁

    我正在练习通过同时将计算分为 100 组来计算阶乘的挑战 我解决了 WaitGroups 上的很多问题 但仍然处于calculateFactorial函数我在通道部分的范围上陷入了僵局 希望有人能指出这个问题 谢谢 package main
  • 如何使用json传递opentracing数据

    我的 API 网关启动一个跟踪器和一个用于验证电子邮件的范围 然后它传递给user service用于验证 我想通过这个span详情至user service作为 json 对象并启动另一个span as a tracer start sp
  • 如何使用golang中通过引用传递的索引访问切片中的元素

    我将切片的引用传递给函数 并且我正在函数内的切片中进行更改 我还尝试使用索引访问切片中的元素 它在 golang 中抛出异常 通过引用传递的索引访问切片中的元素的最佳方法是什么 您可以在此处找到示例代码 参考 http www reddit
  • 如何使用 mongo-go-driver 有效地将 bson 转换为 json?

    我想将 bson 转换为mongo go 驱动程序 https github com mongodb mongo go driver有效地转换为 json 我应该小心处理NaN 因为json Marshal失败如果NaN存在于数据中 例如
  • 如何顺序运行 golang 测试?

    当我跑步时go test 我的输出 FAIL TestGETSearchSuccess 0 00s Location drivers api test go 283 Error Not equal 200 expected 204 actu
  • 无需时间即可生成随机字符串?

    我知道如何使用 Runes 和播种 rand Init 在 go 中生成随机字符串time UnixNano 我的问题是 是否可以 使用 stdlib 在不使用当前时间戳 安全 的情况下播种 rand 此外 我问 因为仅仅依靠时间来为敏感操
  • 在 Go 中使用互斥锁

    我想了解互斥体是如何工作的 据我目前的理解 它是为了进行原子操作并同步对某些数据的访问 我在这里构建了一个队列数据结构的示例 https github com arnauddri algorithms blob master data st
  • 在 Go 中生成随机、固定长度的字节数组

    我有一个字节数组 固定长度为4 token make byte 4 我需要将每个字节设置为随机字节 我怎样才能以最有效的方式做到这一点 这math rand就我而言 方法不提供随机字节函数 也许有一种内置的方法 或者我应该生成一个随机字符串
  • 如何在 Visual Studio Code 中使用 Delve 调试器进行远程调试

    我已经问过了 得到了很好的答复answer https stackoverflow com questions 39058823 how to use delve debugger in visual studio code用于使用 del
  • Go中如何从json字符串中获取键值

    我想尝试从 Go 中的 JSON 获取键值 但我不确定如何操作 我已经能够使用 simplejson 读取 json 值 但是我无法找到如何获取键值 有人能指出我正确的方向和 或帮助我吗 谢谢你 您可以通过执行以下操作来获取 JSON 结构
  • 在 Go 中跟踪 HTTP 请求时指定超时

    我知道通过执行以下操作来指定 HTTP 请求超时的常用方法 httpClient http Client Timeout time Duration 5 time Second 但是 我似乎不知道在跟踪 HTTP 请求时如何执行相同的操作
  • 如何解析 Content-Disposition 标头以检索文件名属性?

    使用 go 如何解析从 http HEAD 请求检索到的 Content Disposition 标头以获取文件的文件名 此外 如何从 http HEAD 响应中检索标头本身 这样的事情正确吗 resp err http Head http
  • 如何在 Go 应用程序中处理打开/关闭数据库连接?

    我的 Web API 应用程序中有一组函数 他们对 Postgres 数据库中的数据执行一些操作 func CreateUser db err sql Open postgres user postgres password passwor
  • GAE Go — 如何对不存在的实体键使用 GetMulti?

    我发现自己需要做一个GetMulti使用键数组进行操作 其中某些实体存在 但有些实体不存在 我当前的代码 如下 返回错误 datastore no such entity err datastore GetMulti c keys info
  • 我怎么知道我的所有 goroutine 确实正在使用 golang 的同步包等待一个条件

    我有一个应用程序 我正在创建多个 goroutine 来同时执行某个任务 所有工作协程都会等待条件 事件发生 一旦事件被触发 它们就会开始执行 创建完所有goroutines后 主线程在发送广播信号之前应该知道所有goroutines确实处
  • 为什么我不能将左大括号放在下一行?

    当我尝试编译以下代码时遇到奇怪的错误 package main import fmt fmt func main var arr 3 int for i 0 i lt 3 i fmt Printf d arr i 错误如下 unexpect
  • 在 Go 中初始化嵌入结构

    我有以下内容struct其中包含一个net http Request type MyRequest struct http Request PathParams map string string 现在我想初始化匿名内部结构http Req
  • 如何使信号量超时

    Go 中的信号量是通过通道来实现的 一个例子是这样的 https sites google com site gopatterns concurrency semaphores https sites google com site gop
  • Golang中按长度分割字符串

    有谁知道如何在 Golang 中按长度分割字符串 例如 每 3 个字符分割 helloworld 那么理想情况下它应该返回一个 hel low orl d 数组 或者 一个可能的解决方案是在每 3 个字符后附加一个换行符 所有的想法都非常感
  • 如何关闭 gorm 1.20.0 中的数据库实例

    由于我没有在 Close 函数中找到 gorm 实例 任何帮助将不胜感激 dbURI fmt Sprintf user s password s dbname s port s sslmode s TimeZone s username p

随机推荐

  • 数学建模

    1 神经网络模型的基本概念是什么 神经网络模型模拟生物神经网络 通过大量互联节点来学习复杂的数据模式和关系 2 神经网络模型的主要优点是什么 可以自动学习时间序列数据的复杂非线性模式 不需要预先指定模型形式 具有很强的适应性和灵活性 3 神
  • WPF绘制图形

    利用WPF绘制简单图形 就像Winform那样 参考DrawToolsWPF using System using System Collections Generic using System Linq using System Text
  • 浏览器 hard refresh

    浏览器 hard referesh force refresh 不同于普通的按 F5 键刷新页面 它会先清当前页面的 cache 然后再刷新页面 这比手工找在浏览器设置中清当前站点的 cache 要方便的多 方法 Windows Ctrl
  • 【Java项目】多文件传输:文件分割与传输

    所谓的文件分割 并非真的将文件切割多个块存储发送 实质上是 用尺子和笔给整个文件块做上分割标记 那么发送时 文件的随机读写 可根据这些标记小块发送 接收端 也可以根据分割信息进行清点与组装 发送端与接收端只要约定好发送与接收 即传输协议 就
  • 算法可视化

    软件功能 以数组为例 一个数组是一个容器 对其进行可视化 主要针对数组的值查找 指定位置插入 指定位置删除进行可视化 软件针对每个数据结构可视化分两个方面 教学模式 主要针对数据结构每个方面进行讲解 可动画演示 实践模式 允许使用者自己通过
  • 汇编语言(王爽第三版)实验三

    实验三 题目预览 将下面的程序保存为t1 asm文件 将其生成可执行t1 exe 用Debug跟踪t1 exe的执行过程 写出每一步执行后 相关寄存器中的内容和栈顶的内容 PSP的头两个字节是CD20 用Debug加载t1 exe 查看PS
  • python3编码格式_python3编码方式

    ascii A 00000001 8位 1个字节 unicode A 00000000 00000000 00000000 00000001 32位 4个字节 utf 8 A 0000 0010 8位 1个字节 中文 00000000 00
  • 数据挖掘:认识数据

    越来越多的人认识到 数据对这个世界的影响越来越大 掌握数据就掌握了发言权 如何从数据中找到想要的知识 是得到数据之后最需要关心的 数据挖掘 也是知识发现的过程 1 理解数据 现实世界中 各行各业每时每刻都在产生数量庞大的数据集 让人眼花缭乱
  • 怎样才能跳过实名认证_和平精英qq怎么跳过实名认证!老司机告诉你仅需5步

    qq怎么跳过实名认证玩家是否知晓 虽然来说跳过实名认证对于手游来说并没有什么帮助 但是这个方式方法玩家还是需要知道的 这样能够帮助玩家轻松的做到某些事情 而这里就是样式玩家如何进行和平精英实名认证怎么跳过 其实在老司机手机仅需5步即可 和平
  • gtx1660是什么级别的_显卡天梯图秒懂GTX1660Ti性能 GTX1660Ti相当于什么显卡

    GTX1660Ti是NVIDIA二月份刚发布的一款显卡 从命名上看 它是历代英伟达显卡中 最 6 的显卡 名称中包含了3个6字 作为上一代甜品级GTX1060的继任者 而颇受关注 那么GTX1660Ti相当于什么显卡 其大致性能是什么水平呢
  • 色值的封装方法以及RGB和RGBA的区别

    取色值相关的方法 define RGB r g b UIColor colorWithRed r 255 f green g 255 f blue b 255 f alpha 1 f define RGBA r g b a UIColor
  • 项目搭建之代码规范化解决方案

    代码规范化解决方案Eslint Prettier Eslint 是一个插件化的 javascript 代码检测工具 用于代码格式检测 不符合Eslint规则的代码 会被检测到发出警告 报错 通过 eslintrc js 文件可以进行自定义的
  • 约瑟夫环:循环链表,数组

    约瑟夫环 1 循环链表 设计思路 2 顺序表 数组实现 设计思路 站成一圈 从1报数 报数为m则出列 求出列序列 1 循环链表 设计思路 用循环链表实现逻辑结构 mermaid svg Ejge7i4yvNzv6PZ2 label font
  • Python opencv学习-5创建带调色板的画板

    test5 带调色板的画板 可改变线的粗细 颜色 缺点 其实是不断画圆 鼠标动作快了能看出执行间隔 import cv2 import numpy as np drawing False mouse callback function de
  • MATLAB回归分析--------2019/8/2

    回归分析 多元线性回归 Matlab 统计工具箱用命令 regress 实现多元线性回归 用的方法是最小二乘法 用 法是 b r e g r e
  • linux中摄像头旋转90度,ok335xD 如何将屏幕显示转90度在linux下,竖屏变横屏。

    我在内核里发现这样几个文件 不知是否能用的到 在drivers video 下 Kconfig and 8 15 or 16 bpp color 90 degrees clockwise display rotation for xilin
  • linux 日志查看

    Linux 查看日志相关命令总结 Linux日志筛选命令 常用Linux日志查看命令 grep常用用法 zgrep 不解压过滤压缩包中文本 Linux命令大全 手册 循环实时查看最后50行记录 tailf n 50 tomcat stdou
  • 在论坛中出现的比较难的sql问题:21(递归问题 检索某个节点下所有叶子节点)...

    最近 在论坛中 遇到了不少比较难的sql问题 虽然自己都能解决 但发现过几天后 就记不起来了 也忘记解决的方法了 所以 觉得有必要记录下来 这样以后再次碰到这类问题 也能从中获取解答的思路 问题 求SQL 检索某个节点下所有叶子节点 部门表
  • centos7配置yum为国内源

    问题 yum install libXm so 4 报错 Cannot find a valid baseurl for repo base 7 x86 64 因为yum源不是国内的 切换一下 步骤 卸载 yum rpm aq grep y
  • 字节青训营第十三课之深入浅出RPC框架的笔记与总结.md

    基本概念 本地函数调用 函数调用完整过程如图 藏实际上编译器经常优化 参数和返回值少时直接将其存在寄存器 不需操作栈 直接online不需call 将a和b的值入栈 经函数指针找到calculate 进入函数取栈中值2和3赋给x和y 计算x