资讯详情

资讯详情

TIPC透明进程间通信:用服务名替代IP打造集群高可用

做高可用集群或者折腾分布式中间件的时候跨节点的进程通信问题永远绕不开TCP可靠、通用但脑子里得时刻装着IP、端口、服务列表共享内存性能好可它天生出不了单机。我第一次认真研究TIPCTransparent Inter-Process Communication透明进程间通信是在看集群容灾方案的时候当时最让我好奇的是这个协议居然让应用层用“服务的名字”去连接一个远端进程而不是用IP加端口。后来把资料翻完我发现TIPC在服务器高可用场景里其实是一条很独特的路线进程之间互相发现、节点故障感知、连接恢复这些平时靠注册中心和健康检查才能做到的事情内核自己就给办了。这篇文章是整个系列的起点我先把它来龙去脉和核心设计讲清楚后面会一步步深入到配置、寻址和实战案例。适合正在选型集群通信方案、或者对内核网络协议有好奇心的朋友。1. TIPC是什么一条和TCP、共享内存都不同的通信路线1.1 “透明”“进程间”“通信”这三个词各自的分量TIPC全称是Transparent Inter-Process Communication中文一般叫透明进程间通信。拆开看Inter-Process Communication是它的目标也就是让不同进程之间能互相交换数据Transparent是它的卖点意思是这种通信对应用来说“看不见、摸不着”就像在同一个进程里调用函数一样轻松。它最早出自电信设备领域当时的设计目标很简单一套电信设备里的多个子系统要跑在不同单板上还要能像在同一块单板上一样协调工作于是就有了这个面向集群环境的通信协议。后来这个协议被放进内核网络协议栈成为AF_TIPC协议族的一部分。这带来一个非常关键的改变任何用C、Go、Rust等语言写程序的开发者都能用最普通的socket接口来使用TIPC。它不再是一个封闭的私有方案而是一个程序员可以实实在在调用的传输层协议。1.2 它不是TCP的补丁而是换了一套底层逻辑TCP不是不好它的问题出现在“服务发现”和“故障转移”这两个环节。用TCP写分布式服务客户端必须知道服务端机器在哪、端口多少于是需要注册中心、负载均衡、健康检查这一整套外部组件。服务端扩容、缩容、宕机客户端缓存里那个IP就失效了得靠额外机制去刷新。这一整套复杂度本质上是在给TCP查漏补缺。TIPC换了一个思路应用层只声明“我要用某个服务”不关心这个服务此刻在哪台机器上。内核在集群范围内维护一张动态地址表服务进程启动后把名字发布进去客户端用名字发起连接内核自动完成到目标节点的路由。节点挂了名字自动从表中移除新的连接转向存活节点。对应用来说这一切都发生在socket层之下代码里没有任何IP和端口的痕迹。这种差异不是优化层面的是模型层面的。TCP给的是一个“点到点的管道”TIPC给的是一个“名字到多个进程的映射”。理解了这一层后面看TIPC的API就会非常顺。顺带说一句共享内存单机性能确实无敌内核都不用参与但它跨不了物理机TIPC可以看作把“共享内存式的透明体验”推广到集群只不过底层走的是网络。2. 破局的起点不再用IP寻址而是用“服务名”寻址2.1 TCP世界里服务发现的繁琐路径回忆一下用TCP实现一个高可用服务的完整链路服务端启动时绑定某个端口然后把地址注册到注册中心或配置中心客户端启动时去查询得到地址列表后挑选一个连接服务端扩容要注册新节点服务端宕机注册中心要摘除旧节点客户端还要自己处理“连不上就换一个”的逻辑。如果想让流量分布均匀还得再挂一个负载均衡器。这套东西本身没有错但它把大量复杂度从通信层搬到了业务层。TIPC希望做的是把这些复杂度重新放回通信层。在TIPC里服务端只需要bind一个名字客户端只需要connect同一个名字中间的一切由内核完成。你不用写健康检查因为链路故障本身就是内核自动检测的你也不用维护地址列表因为内核的地址表就是实时列表。2.2 域、集群、节点三层编址是怎么分工的TIPC用三个层级来组织集群里的机器域zone、集群cluster、节点node。域是最大的范围一般不用大多数部署都是单域集群是“可以自由互通的一组机器”这个概念集群内的节点可以互相发现和通信节点是单台机器的编号。习惯上表示成一个三元组例如1.1.5表示1号域、1号集群、5号节点。层级取值范围作用域zone0~255划分相互隔离的大区域集群cluster0~4094一组互通节点名字注册表的有效范围节点node0~4094单台机器的编号对应一个TIPC实例这个三层结构和IP的“网络号主机号”有点像但它不是用来寻址数据包的而是用来管理“哪些节点在同一个信任域内”。举个例子一个集群里的节点可以随便互通跨集群通信则需要显式配置这在做多机房的隔离时很实用。2.3 服务类型和实例编号应用只认这一对数字应用层用的TIPC服务地址只有两个数字服务类型type和实例编号instance。type相当于服务分类你可以在自己的项目里约定“10001代表订单服务、10002代表库存服务”instance代表这个分类下的第几个实例。如果发布了一个type10001、instance0的名字别的进程连接这个名字就会被送到当前发布了该名字的那个socket上。这里有个很实用的设计同一个名字可以被多个节点发布也就是多副本。TIPC内核会记住每个名字对应哪些节点连接请求到达时可以选择其中一个节点送达。这个能力相当于TCP世界里“客户端负载均衡故障转移”的组合而且不需要业务代码配合。2.4 故障感知不用等外部心跳模块来“捞人”平时做集群节点故障靠的是外部心跳和健康检查发现异常后再由管理端摘除节点。TIPC的做法更底层它跟踪的是节点间的链路状态。当链路断了内核立刻知道哪个节点不可达并把这个节点发布过的所有名字都标记为不可用。如果该名字还有别的节点在提供服务新连接就会自动去存活节点。这种故障感知由内核完成反应速度比用户态心跳快得多而且不会出现“应用还连接着但服务其实已经挂了”的尴尬窗口。代价是它只对链路故障敏感进程内部逻辑卡死它感知不到——这一点和第4章要讲的适用边界有关。3. TIPC的几种通信口子Socket类型与可靠性取舍3.1 SOCK_STREAM和TCP最像的长连接SOCK_STREAM提供可靠、面向连接、按序的字节流。连接建立后数据不区分消息边界和TCP一样是纯字节流。它适合长时间保持连接、传输大量数据的场景比如集群内的日志汇聚、数据同步。对写惯TCP的人来说API几乎可以无缝迁移区别只是把sockaddr从IP端口换成TIPC名称。和TCP的流控机制相比TIPC的流控相对简单。它面向的是可信内网不需要像公网TCP那样应付各种网络异常。因此如果网络环境非常复杂、亟需精细拥塞控制TIPC的SOCK_STREAM反而不如TCP稳妥。3.2 SOCK_RDM可靠数据报RPC的最佳搭档SOCK_RDM是TIPC里最有特色的模式可靠数据报保留消息边界同时消息不丢不重。用过TCP做RPC的人一定经历过粘包、半包因为TCP是字节流应用层必须自己定义帧格式。RDM直接把这个活干了一次send对应一次recv消息边界天然存在。需要注意RDM不保证像TCP那样的流控。如果发送方拼命灌数据接收方来不及消费内核缓冲区会有压力。所以使用RDM时业务层要做好背压设计比如限制并发请求数。它的典型场景是RPC、命令请求、状态上报这类一问一答式的通信。3.3 SOCK_DGRAM丢得起的数据报SOCK_DGRAM是不可靠数据报语义和UDP接近不需要建立连接直接sendto一个服务名就行消息可能丢失。它适合心跳、监控采样、日志上报这类丢了也无所谓的场景。TIPC提供这个口子不是为了替代UDP而是让同一套寻址体系下能覆盖更多类型的流量这样应用不用维护两套通信栈。3.4 怎么选一张表格说清楚Socket类型可靠性是否保留消息边界是否需要建立连接典型用途SOCK_STREAM可靠、有序否字节流需要长连接、批量传输SOCK_RDM可靠是不需要RPC、请求响应SOCK_DGRAM尽力而为是不需要心跳、监控、日志选择的原则很简单丢不得又需要消息边界选RDM大流量传输选STREAM能容忍丢失选DGRAM。实际项目里我见过不少混合使用主业务用STREAM控制命令用RDM心跳用DGRAM三个socket并存于同一进程寻址方式完全一致。4. TIPC的优势与代价不是银弹看清边界再用4.1 真正发挥价值的场景小规模可信集群内部TIPC最合适的舞台是那些机器数量可控、网络环境可信的集群。典型例子高可用双机、主备热切换的电信网元一组负载均衡器共享状态监控采集器去各节点拉数据。这些场景的共同点是节点之间要频繁互相发现、机器可能发生故障切换、但又不想为此引入一套复杂的注册中心体系。在这些场景里TIPC的价值很直接。服务发现免费获得故障切换免费获得节点地址变化不影响业务代码。不需要额外注册组件一个内核协议就把位置透明性解决了。如果团队本来就只有两三台机器为了搞定服务发现而部署一套完整注册中心多少有点大炮打蚊子TIPC这时候反而是最省事的方案。4.2 不适合的场景公网、海量节点、复杂拥塞TIPC的假设是“底层网络比较可信”所以它没有TCP那么多针对公网的防御机制。跨公网、跨NAT部署基本不用考虑TIPC老老实实用TCP或专门的消息队列。节点规模方面TIPC的地址空间和广播机制是为中小规模集群设计的几百台以内的集群很舒服几千台的场景它不是首选。另一个限制是部署条件。应用要使用TIPC运行环境的内核必须开启CONFIG_TIPC且运行进程有加载模块、配置bearer的权限。在容器环境里如果内核没带这个模块或者容器限制了模块操作TIPC就没法用。这个门槛不高但确实比纯用户态方案多一层依赖。4.3 和其它“高性能通信”方案的边界很多人会把TIPC和RDMA、DPDK、共享内存放在一起比较。严格来说它们不是同类东西。共享内存只能在单机内通信跨机器还得靠网络。RDMA和DPDK追求极致的低延迟但需要特定网卡、驱动和用户态库部署成本高。TIPC的目标不是刷新延迟记录而是在普通以太网上提供“位置透明、带故障感知”的集群通信体验。延迟比TCP略好但它真正的卖点不是快而是省心。做一个粗略的对比方案跨机特殊硬件位置透明典型定位TCP支持不需要否通用网络通信TIPC支持不需要是集群内部高可用通信共享内存否不需要是单机高性能IPCRDMA/DPDK支持需要部分极致低延迟4.4 生态和长期维护成本TIPC的生态确实比TCP小得多文档也不算丰富遇到问题时可以查的资料有限。但它有一个很大的优势协议实现稳定且已经存在很多年API形态简单一旦跑起来很少需要维护。对团队来说真正要权衡的是团队里有没有人能理解它以及是否愿意为“少写注册中心”付出一点学习成本。5. 动手前先检查基础条件模块、命令和最小配置5.1 内核模块先看编译配置再加载TIPC在内核里通常以模块形式存在。在绝大多数主流发行版上CONFIG_TIPC默认是开启的。检查方式分两步。第一步看配置用grep TIPC /boot/config-$(uname -r)看到CONFIG_TIPCm说明编成了模块看到y说明编进了内核镜像。第二步看加载状态执行lsmod | grep tipc如果没有输出就执行modprobe tipc把模块加载进来。加载模块需要root权限。在容器里面如果没有特权操作内核模块通常是受限的这也是前面说的部署依赖之一。5.2 用tipc命令确认节点状态iproute2工具包里带了一个tipc命令可以用它管理TIPC节点和bearer。常用检查命令有tipc node list查看当前集群里的节点tipc bearer list查看底层媒体。刚加载模块后节点地址通常还没设置处于未配置状态这时连名字都会失败。配置的最小操作可以分两步。第一步设置节点地址写法类似tipc node set address 1.1.5表示这台机器是1号域、1号集群、5号节点。第二步启用bearer写法类似tipc bearer enable media eth dev eth0让TIPC流量可以走某个网卡出去。完整配置涉及更多细节我会在系列后续文章里展开这里先知道有这两步就行。5.3 常见错误和排查顺序我实际调试时遇到过几次这个错误socket(AF_TIPC, SOCK_STREAM, 0)返回的却是“Address family not supported”。原因几乎都是模块没有加载或者内核编译时根本没启用TIPC。排查顺序建议是先看模块是否加载再看节点地址是否配置最后看bearer有没有起来。三步依次确认大部分问题都能定位。另外一个容易忽略的点TIPC需要root权限来加载模块和配置bearer但普通用户建立AF_TIPC连接是不需要特权的只要系统已经配置好。所以生产环境里比较合理的做法是由启动脚本在系统初始化阶段把TIPC配置好应用进程用普通用户身份运行。6. 一个最小的可运行示例通过名字建立连接6.1 先看服务端bind一个名字等于发布一个服务服务端的代码很直白和TCP服务端的差别集中在地址结构上。先创建一个AF_TIPC的SOCK_STREAM socket然后填充sockaddr_tipcfamily设为AF_TIPCaddrtype设为TIPC_ADDR_NAMEaddr.name里填type和instance。bind之后这个进程就在TIPC的名称服务表里发布了一个名字。listen和accept的用法和TCP完全一样。#include sys/socket.h #include linux/tipc.h #include stdio.h #include string.h #include unistd.h int main(void) { int sd socket(AF_TIPC, SOCK_STREAM, 0); if (sd 0) { perror(socket); return 1; } struct sockaddr_tipc addr; memset(addr, 0, sizeof(addr)); addr.family AF_TIPC; addr.addrtype TIPC_ADDR_NAME; addr.addr.name.type 11111; addr.addr.name.instance 0; if (bind(sd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); return 1; } if (listen(sd, 5) 0) { perror(listen); return 1; } printf(server ready: type11111 instance0\n); int conn accept(sd, NULL, NULL); if (conn 0) { perror(accept); return 1; } char buf[256]; ssize_t n recv(conn, buf, sizeof(buf) - 1, 0); if (n 0) { buf[n] \0; printf(recv: %s\n, buf); } close(conn); close(sd); return 0; }6.2 客户端connect一个名字而不是一个IP客户端的写法同样简单。socket创建后填充一个同样的sockaddr_tipc地址直接connect。这里的connect并不要求你知道服务端在哪台机器它只负责告诉TIPC“请帮我找到type11111、instance0这个名称所对应的服务并建立连接。”内核会在名称服务表里查然后自动完成路由。#include sys/socket.h #include linux/tipc.h #include stdio.h #include string.h #include unistd.h int main(void) { int sd socket(AF_TIPC, SOCK_STREAM, 0); if (sd 0) { perror(socket); return 1; } struct sockaddr_tipc addr; memset(addr, 0, sizeof(addr)); addr.family AF_TIPC; addr.addrtype TIPC_ADDR_NAME; addr.addr.name.type 11111; addr.addr.name.instance 0; if (connect(sd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(connect); return 1; } const char *msg hello from client; send(sd, msg, strlen(msg), 0); close(sd); return 0; }6.3 编译、运行与前置条件编译命令是普通的gccgcc -o tipc_server server.c gcc -o tipc_client client.c运行前要把第5章那几步做了模块已加载、节点地址已配置、bearer已启用。如果只有一台机器做实验只要节点配置完成后两个进程跑在同一台机器上也能通过名称服务完成连接如果是在多台机器上实验给每台机器设置不同的节点编号再启用对应网卡的bearer即可。运行顺序是先服务端后客户端。终端里执行./tipc_server它会停在accept等待连接另一个终端执行./tipc_client服务端就能打印出收到的字符串。这个最小闭环验证了“服务名寻址”的完整流程。6.4 这个示例背后的机制bind名字那一步等价于向内核发布了一条“type11111 instance0 在本地可用”的记录connect名字那一步等价于向内核查询“谁发布了这个名字”然后建立会话。整个交互里应用代码没有任何IP地址和端口号出现这就是TIPC和TCP最直观的区别。这个系列接下来我会继续写节点地址的具体配置、bearer的选型和多节点集群的搭建也会专门讲一讲TIPC在实际项目里的RPC用法和状态同步问题。最后说点私货。我自己第一次跑通这段代码的时候最大的感受不是“这协议多好用”而是“原来服务发现还能长在协议栈里”。在TCP的世界里位置透明这件事被做成了业务层的数十种中间件在TIPC这里它只是一个内核选项。如果这个切入点打动了你后面的配置部分值得继续往下读。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →