RPC
函数调用 IPC
本地函数调用
远程函数调用 RPC
两个进程的地址空间,都有自己的ip,远程对应关系通过ip执行
在本地调用的时候只需要把数据压栈就可以了,但是在远程调用中是不同的地址,这时候需要客户端先将数据转换成字节流方便传输
保证网络搞笑稳定的传输数据
RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
概念模型
左边的是调用的,右边是被调用的
User首先会发起一个本地调用,将参数打包给RPCRuntime,然后RPCRuntime会将数据传送给服务端
RPCRuntime接收完参数后将其交给server-stub进行解压,灾后在server中进行处理
返回流程相似
基本概念
优缺点
优点
- 单一职责,有利于分工协作和运维开发(不同的服务可以由不同的语言进行开发)
- 可扩展性强,资源使用率更优(压力大时可以独立的扩充资源,且底层服务可以共享,如客户信息等)
- 故障隔离,服务的整体可靠性更高
缺点
- 服务宕机处理方案
- 调用过程中发生网络异常,如何保证消息的可达性
- 请求量徒增导致服务无法及时处理
以上问题可以由RPC框架来处理
分层设计
编解码层
(生成层+编码器层)
生成代码
数据格式
二进制编码
结构比较清晰,但是增加了tag等与value无关的字段,且一些字段开销比较大
编码格式选择
- 兼容性:支持自动增加新的字段 而不影响老的服务以提高系统的灵活性
- 通用性:支持跨语言 跨平台
- 性能:编码后数据大小和编码耗时
协议层
(生成+编码+协议)
协议构造
协议解析
通过magicnumber知道具体是什么协议
payloadcodec读取编码方式,然后进行解码得到消息体
通信层
sockets API
介于应用层和传输层之间
进行socket的时候必须知道IP和端口,通过bind操作获得,然后进行监听,然后放到队列中
再然后客户端如果有请求的时候服务端通过accept进行接收,如果没有请求的时候处于阻塞状态
网络库
关键性能指标
稳定性
熔断:a调用b b调用c 这时c出现问题,那么会影响整个链路,a会频繁的向b发送请求,可能会导致b宕机
提高请求成功率的两种方式:
- 负载均衡:若a调用b 增b需要均匀的响应,以免某个节点压力过大
- 重试:
很明显高于平均响应时间并且占比比较小的请求
t3表示99%请求都可以在这个时间段返回,如果没有返回的话 重新发送了一个resp2请求,用时t4 表明1是个长尾请求
长尾请求产生原因:
一种是依赖的系统和环境出现了问题,例如硬盘慢了, 操作系统执行了后台操作造成cpu调度走了。
第二种是自己的调度原因, 例如采用了简单的锁竞争机制,导致有的倒霉的线程一直得不到运行权。
第三种是系统压力太大,造成了任务队列积压。
解决方法:
作为外部的客户端来说, 一个通用的解决长尾请求的方案是发送备用请求, 例如对一个后端的数据请求, 本来只需要发送1次, 而我们分别给三个实例发送三次, 最快的一个返回即终止其余的两个请求即可。
易用性
扩展性
中间件、参数
观测性
通过日志、监控、追踪(超时 可以查看每个阶段的耗时)的方式进行观测
内置观测性服务:暴露一些中间件等内部运行状况,一般是http服务
高性能
没有很好的衡量标准
企业实践
整体架构
自研网络库-字节
背景
goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。
不仅如此,Go语言内部已经实现了goroutine之间的内存共享,它比线程更加易用、高效和轻便。
在Go语言中,每一个并发的执行单元叫作一个goroutine。我们只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。
一旦我们使用了go关键字,函数的返回值就会被忽略,故不能使用函数返回值来与主线程进行数据交换,而只能使用channel。
解决
扩展性设计
目前 Kitex 支持的交互模式和协议列表
| 模式\支持 | 编码协议 | 传输层协议 |
|---|---|---|
| Pingpong | thrift/protobuf | thrift/gRPC |
| Oneway | thrift/protobuf | thrift |
| Streaming | protobuf | gRPC |
- Pingpong 发起一个请求等待一个响应
- Oneway 发起一个请求不等待一个响应
- Streaming 发起一个或多个请求, 等待一个或多个响应
性能优化
网络库优化
编解码优化
合并部署
HTTP协议
http协议
前后端分离框架
前后端分离的框架需要使用http协议进行数据的传输
协议
超文本:除了text外还可以传输视频音频
结构
PATCH方法是新引入的,是对PUT方法的补充,用来对已知资源进行局部更新
局部更新: 假设我们有一个UserInfo,里面有userId, userName, userGender等10个字段。可你的编辑功能因为需求,在某个特别的页面里只能修改userName,这时候的更新怎么做? 人们通常(为徒省事)把一个包含了修改后userName的完整userInfo对象传给后端,做完整更新。但是比较浪费带宽
patch诞生,只传一个userName到指定资源去,表示该请求是一个局部更新,后端仅更新接收到的字段。
而put虽然也是更新资源,但要求前端提供的一定是一个完整的资源对象,理论上说如果你用了put,但却没有提供完整的UserInfo,那么缺了的那些字段应该被清空
请求头和响应头:有逻辑相关和业务相关,比如head就是逻辑相关,业务相关自己定义 (与数据有关)
demo
给sis发送数据 需要返回OK的demo的代码
请求流程
route:根据对应的api选择执行的handler
不足
http1
后面的分片需要等前面的分片到来才能发送数据(队头阻塞,TCP问题)
http2
二进制协议传输更加高效,但是没有解决队头阻塞的问题
分层设计
网络分层
http框架分层
层与层之间使用接口进行解耦
Application:抽象、提供丰富的应用api 和用户直接打交道
中间件层:实现预处理和后处理的逻辑
路由层:注册寻址相关的操作
协议层:有了协议的抽象接口可以实现协议的扩展
网络层:不同网络使用场景不同
common: 放一些公共的文件
应用层
可见性:
- 安全性:不能随意更改接口
- 接口使用复杂
中间件设计
经典中间件模型
预处理:日志、metrics(通用逻辑)
业务逻辑处理(核心逻辑)
后处理:(通用逻辑)
中间件定义
调用链
路由设计
静态路由构建
参数路由构建
问题
如何设计
协议层设计
抽象出合适的接口
0. 不要吧context(上下文传递)存储在struct中,而是要通过函数的第一个参数传递进来
0. 返回值就行需要处理的数据
传输层
BIO和NIO
bio 阻塞io,必须read完成之后在业务逻辑处理,需要用户管理buffer
nio 注册一个监听器,坚挺到有足够的数据之后再进行业务逻辑处理,自动管理底层的buffer
性能优化
网络库
主流操作系统都有网络支持,并且提供 C 语言接口的网络 API。
但是通常来说,这些网络 API 很底层,并不是很好用。举例来说,send() 函数只能传递 char* 对象过去,如果我是 std::string,或者 std::array<unsigned char,4096>(话说我就经常用这个做buffer),那就要自己转换了。而网络库往往会帮你做好这种封装,让你调用起来可以写更少的代码,代码里的逻辑不会因为调用了这些API而显得混乱。此外还能跨平台(原生网络API是不跨平台的,每个系统都要单独写一份),能使开发、维护都更有效率。
字节跳动网络库 netpoll,Netpoll 是一款 Go 语言高性能、I/O 非阻塞 (NIO) 网络库,专注于 RPC 场景。
go net问题
优化
大部分包都是在4k以下的
netpoll 问题
采用链表的方式,实现buffer的无锁化,但是链表可能会导致跨节点的问题,如图header和body都不在同一个节点上,想要进行完整解析的话 需要先将两个header拼接并拷贝到一个完整的内存中才能解析
解决
比较
协议优化
header解析
可以使用SIMD进行加速优化, SIMD 的全称是 Single Instruction Multiple Data,中文名“单指令多数据”。顾名思义,一条指令处理多个数据。
热点资源池化
如果对每一个请求有一个一个requestcontext,那么压力比较大
所以维护了一个requestcontext池 有一个请求就从池中拿出来一个requestcontext
将requestcontext放回尺子中 会有一些复杂的reset操作,因为需要进行复用
如果reset超出声明周期的话 可能会带来数据不一致的问题