尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Loki 中的 containerd ttrpc:面向低内存环境的轻量 RPC 协议与 Go 实现解析

Loki 中的 containerd ttrpc:面向低内存环境的轻量 RPC 协议与 Go 实现解析 Loki 中的 containerd ttrpc面向低内存环境的轻量 RPC 协议与 Go 实现解析【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/lokittrpcTiny/Thin RPC是 containerd 子项目推出的「为低内存环境设计的 gRPC 替代方案」它复用 gRPC 的 IDL 与生成代码心智模型却用一套 10 字节轻量帧协议替代了net/http、net/http2与grpc运行时从而显著降低二进制体积与常驻内存占用。本指南以 vendor/github.com/containerd/ttrpc/README.md 为主体结合同目录下的协议规范与 Go 源码完整讲解 ttrpc 的设计动机、帧协议、流状态机、RPC 消息模型以及客户端/服务端实现细节读完你将理解 ttrpc 与 gRPC 的本质差异并能读懂 Loki 仓库 vendor 中这份依赖的底层工作原理。ttrpc 是什么为什么需要「低内存的 gRPC」在讲解实现之前先明确 ttrpc 的定位。官方 README 开宗明义GRPC for low-memory environments.现有的grpc-go在导入包时和运行时都会产生较高的内存开销。对低密度部署场景这没有问题但当在一台机器上运行大量服务、或机器本身内存很小时这种开销就变得棘手。ttrpc 的思路是沿用同一套 gRPC 的 protobuf 定义只把传输层替换掉——去除net/http、net/http2和grpc包改用一套轻量分帧lightweight framing协议从而获得更小的二进制和更低的驻留内存同时保持与 gRPC 相近的使用体验。需要特别强调的是 README 中的明确警告虽然 ttrpc 支持生成协议的两端客户端与服务端但生成的 service 定义与常规 gRPC 服务互不兼容因为它们讲的是不同的协议详见下文「协议」一节。也就是说 ttrpc 是协议级的不兼容替代而非透明兼容层。与 gRPC 的差异README 将其归纳为三条核心差异差异点gRPCttrpc协议栈依赖 http、http2 与 TLS更轻的协议不要求 http、http2 和 tls客户端/服务端接口客户端接口与服务端接口不同客户端和服务端接口一致上下文实现自带 context 体系直接使用 Go 标准库context第三条差异意味着 ttrpc 原生拥抱context.Context的取消、超时与值传递语义超时信息会通过 RPC 请求中的timeout_nano字段传递到对端见下文 RPC 消息定义。从协议设计目标看ttrpc 面向的是同主机内低延迟、可靠连接的进程间通信如 containerd 与 shim 之间的调用并不打算在网络环境下替代 HTTP/2 或 HTTP/3。协议规范10 字节头 3 种消息类型完整协议定义见 vendor/github.com/containerd/ttrpc/PROTOCOL.md。该协议是一个支持单条连接上多路请求流multiple request streams的客户端/服务端协议客户端是发起底层连接的一方服务端是接受连接的一方当前协议是不对称的——客户端发送请求、服务端发送响应但双方都可以发送流数据stream data。流的标识也由角色决定客户端发起的流使用奇数 ID服务端发起的流使用偶数 ID服务端发起流在最新版本中尚不支持。协议明确不包含处理不可靠连接的特性如握手、重置、ping、流控其目的就是让低开销实现尽可能简单。消息帧Message Frame每个消息帧由10 字节的消息头 消息数据组成格式如下--------------------------------------------------------------- | Data Length (32) | --------------------------------------------------------------- | Stream ID (32) | -------------------------------------------------------------- | Msg Type (8) | --------------- | Flags (8) | -------------------------------------------------------------- | Data (*) | ---------------------------------------------------------------字段含义与约束均与 channel.go 中的实现对应Data Length32 位无符号整数大端序Data 字段的字节数总帧大小恒等于Data Length 10最大数据长度 4MB任何更大的帧都应被拒绝。由于最大数据量小于 16MB帧的第一个字节始终为 0该字节保留给未来使用。实现侧常量见messageLengthMax 4 204 MiB。Stream ID32 位无符号整数大端序客户端发起流必须为奇数服务端发起流为偶数当前不支持。Msg Type8 位无符号整数消息类型。Flags8 位无符号整数标志位具体含义由消息类型决定。消息类型Message TypesMessage Type名称描述0x01Request发起流0x02Response最终流数据并终止流0x03Data流数据Request0x01用于发起流并携带路由和处理该流所需的请求数据。请求可以表示 unary无入站/出站流数据只期待一个响应也可以指示流仍处于打开状态、在数据发送完之前不应期待响应。若远端指示流已关闭remote closed则该请求可视为非 unary 但不再有流数据发送——此时远端仍期待收到响应或流数据。为了兼容非流式客户端空标志的请求即表示 unary 请求。Request 标志Flag名称描述0x01remote closed非 unary但不再期待远端发送更多数据0x02remote open非 unary远端仍在发送数据Response0x02以数据、空响应或错误来结束一个流。unary 请求之后唯一期待的消息就是 Response非 unary 请求如果服务端直接回送流数据则不一定需要 Response 消息非 unary 流也可能返回一个Response但之后不允许再有其他流数据。当前未定义任何 Response 标志flags 应为空。Data0x03在已初始化的流上发送数据客户端或服务端均可发送。约束包括unary 流上不允许 Data 消息向对端表明remote closed之后不应再发送 Data流上最后一条 Data 消息必须设置remote closed标志。Data 标志Flag名称描述0x01remote closed不再期待远端发送更多数据0x04no data本消息不含数据no data标志用于指示 Data 消息不含任何数据通常与remote closed配合表示「流已关闭且未传输任何数据」。由于 ttrpc 每条消息通常只传输一个对象零长度 Data 消息可被解释为空对象——例如传输 protobuf 消息「数字 0」时数据长度就是 0但该消息仍被视为数据并应被处理。实现侧这些类型与标志在 channel.go 中定义为常量messageTypeRequest 0x1、messageTypeResponse 0x2、messageTypeData 0x3以及flagRemoteClosed 0x1、flagRemoteOpen 0x2、flagNoData 0x4。流模型local closed 与 remote closed 双状态所有 ttrpc 请求都通过流来传输数据unary 流每条流只发送两条消息——客户端一个 Request、服务端一个 Response。非 unary 流客户端与服务端都可能发送任意数量的消息流的生命周期管理因此更复杂。ttrpc 把状态管理做到最简用两个标志代替控制帧。每条存活流只有两个状态local closed与remote closed。每个对端从自己的视角看待 local/remote并按对方视角设置标志。例如客户端发送带remote closed标志的 Data 帧表示客户端自己现在local closed服务端将变为remote closed。unary 操作无需发送这些标志因为收到的每条消息都隐式表示remote closed。一旦某个对端同时处于local closed和remote closed流即视为完成可以被清理。由于协议的不对称性存在一个有序约束客户端应始终先于remote closed进入local closed服务端应始终先于local closed进入remote closed。这是因为客户端总是发起请求、总是期待服务端返回最终响应来确认请求已完成——即使服务端已经先于客户端发完数据也可能需要发送一条空的最终 Response 来结束流。Unary 状态图-------- -------- | Client | | Server | ------- ------- | --------- | local --------------- Request -------------------- remote closed | --------- | closed | | | ---------- | finished -------------- Response -------------------- finished | ---------- | | |非 Unary 状态图RC remote closed标志RO remote open标志客户端流式发送数据服务端单向接收最后以 Response 结束-------- -------- | Client | | Server | ------- ------- | -------------- | ------------- Request [RO] ----------------- | -------------- | | ------ | ----------------- Data --------------------- | ------ | | ----------- | local --------------- Data [RC] ------------------ remote closed | ----------- | closed | | | ---------- | finished -------------- Response -------------------- finished | ---------- |服务端单向流式发送数据客户端先发带remote closed的请求服务端发多条 Data最后一条带remote closed-------- -------- | Client | | Server | ------- ------- | -------------- | local ------------- Request [RC] ----------------- remote closed | -------------- | closed | ------ | ----------------- Data --------------------- | ------ | | ----------- | finished --------------- Data [RC] ------------------ finished | ----------- |双向流式双方交替发送多条 Data各自最后一条带remote closed-------- -------- | Client | | Server | ------- ------- | -------------- | ------------- Request [RO] ----------------- | -------------- | | ------ | ----------------- Data --------------------- | ------ | ----------------- Data --------------------- | ------ | ----------------- Data --------------------- | ------ | | ----------- | local --------------- Data [RC] ------------------ remote closed | ----------- | closed | ------ | ----------------- Data --------------------- | ----------- | finished --------------- Data [RC] ------------------ finished | ----------- |从客户端实现看上述状态机直接落在 client.go 的clientStream中CloseSend()发送flagRemoteClosed|flagNoData并置localClosedSendMsg()发送普通 Data 帧RecvMsg()依据收到的消息类型Response 或 Data解包、按flagRemoteClosed/flagNoData更新remoteClosed状态并决定是否返回io.EOF。RPC 消息定义Request 与 Response协议本身只定义传输层的 Request/Response/Data 消息并不规定上层 RPC 的请求/响应类型。实现提供了一套默认的 protobuf 定义可用于跨语言 RPC见 request.protosyntax proto3; package ttrpc; import google/rpc/status.proto; option go_package github.com/containerd/ttrpc; message Request { string service 1; string method 2; bytes payload 3; int64 timeout_nano 4; repeated KeyValue metadata 5; } message Response { google.rpc.Status status 1; bytes payload 2; } message StringList { repeated string list 1; } message KeyValue { string key 1; string value 2; }所有实现至少应定义一个支持按 procedure 名路由的请求类型和一个支持调用状态的响应类型。可以看到Request中servicemethod承担路由职责timeout_nano承载超时metadata以KeyValue列表传递附加信息Response复用google.rpc.Status表达调用结果OK 或错误码成功时payload携带响应数据。在 client.go 的Client.Call中这段逻辑得到印证请求会被编码为Request{Service, Method, Payload}如果context中带元数据则填充 metadata如果带 deadline 则写入TimeoutNano time.Until(dl).Nanoseconds()响应解码后若Status.Code ! codes.OK则通过status.ErrorProto转为 Go error 返回。Go 实现解析channel、Client 与 Serverttrpc 的 Go 实现位于 vendor/github.com/containerd/ttrpc 目录核心文件为channel.go、client.go、server.go。仓库 go.mod 中记录其版本为github.com/containerd/ttrpc v1.2.9 // indirect属于 Loki 的间接依赖通过 vendor 目录随仓库一并分发。channel帧的读写与缓冲复用channel.go 实现帧的编解码messageHeader结构体精确对应 10 字节头部Length、StreamID、Type、Flags读写均按大端序进行channel.recv()读取头部后若Length messageLengthMax4 MiB则丢弃数据并返回ResourceExhausted状态错误正常时从bufio.Reader读取负载channel.send()写入头部、追加负载并Flush负载超过上限时返回OversizedMessageError负载缓冲通过sync.Poolgetmbuf/putmbuf复用减少分配与 GC 压力——这与 README「低内存占用」的目标一致头部读写缓冲区hrbuf/hwbuf以数组形式内嵌在 channel 结构中避免读取头时产生堆分配。ClientUnary 与 Streaming 两套调用路径client.go 提供两种调用方式Client.Callunary直接封装请求编码、拦截器、流调度与响应解码Client.NewStreamstreaming依据StreamDesc的StreamingClient/StreamingServer组合决定请求标志——客户端要流式发送则 Request 带flagRemoteOpen否则带flagRemoteClosed返回的ClientStream提供CloseSend/SendMsg/RecvMsg。流 ID 的分配在createStream中nextStreamID从 1 开始、每次2保证客户端发起的流 ID 始终为奇数且单调递增代码注释明确指出这是 ttrpc 协议的要求。客户端还支持通过WithUnaryClientInterceptor/WithChainUnaryClientInterceptor注册拦截器链作用域与 gRPC 的 unary 拦截器一致。连接关闭时filterCloseErr会把EOF、EPIPE、ECONNRESET等错误统一规整为ErrClosed。Server服务注册、握手与优雅关闭server.go 提供服务端实现服务注册Server.Registermap 形式的方法注册2.0 将移除与Server.RegisterServiceServiceDesc形式支持流两个入口连接处理Serve循环Accept连接先经handshaker.Handshake握手校验再为每个连接启动serverConn.run连接按active有未完成请求/idle/closed三态管理协议校验接收循环强制StreamID必须为奇数偶数即报InvalidArgument、StreamID 必须递增且不可复用Data 消息必须落在已激活的流上超时与元数据getRequestContext根据请求中的 metadata 与TimeoutNano构造带元数据或带超时的处理上下文关闭语义Shutdown优雅退出——关闭监听、周期性关闭 idle 连接直至无连接残留Close则立即关闭所有连接。服务端侧同样复用了flagRemoteClosed/flagNoData发送流式响应并在closeStream时删除流、递减活跃计数。值得注意的是服务端注释明确当前协议不支持「服务端local closed但未remote closed」的情况——一旦服务端关闭整个流即可视为结束这正对应 PROTOCOL.md 中不对称状态机约束的实现。生成与构建工具链README 建议使用protobuild来生成 protobuf 代码但直接用protoc也可以。仓库中的生成配置文件齐全buf.yaml、buf.gen.yaml、buf.lockbuf 工具链配置Makefile生成与构建入口生成的产物即为 request.pb.go与request.proto对应。由于该模块以 vendor 形式存在实际构建由 Loki 仓库统一驱动无需单独安装 protobuf 工具链即可编译。版本历史与项目状态协议演进记录如下版本特性1.0仅支持 unary 请求1.2增加流式支持仓库中 vendor 的版本为v1.2.9见 go.mod即同时包含 unary 与流式能力的较新版本。README 的 Status 部分列出两项 TODO在并发负载下补充测试、验证连接错误处理。ttrpc 是 containerd 子项目采用 Apache 2.0 许可见 vendor/github.com/containerd/ttrpc/LICENSE。小结何时选择 ttrpc综合 README 与源码可以得出清晰的选型画像适用场景同主机内、低延迟、可靠连接的进程间通信在单机运行大量服务或内存受限时需要尽量小的二进制与常驻内存设计取舍以极简分帧协议10 字节头、3 种消息类型、2 个标志位换掉 HTTP/HTTP2/TLS 栈以双状态local/remote closed替代控制帧换取实现简单与开销最低明确边界不适用于不可靠网络环境不是 HTTP/2 或 HTTP/3 的网络替代品生成的 service 与常规 gRPC 服务协议不互通。对 Loki 的读者而言这份 vendor 依赖的价值在于当需要理解同主机内高密度、低开销 RPC 的协议设计时ttrpc 提供了一个完整且可读的参考实现——从 PROTOCOL.md 的帧格式到 channel.go 的编解码再到 client.go 与 server.go 的双端状态机全链路可在仓库内闭环研读。【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表