ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Moby ipvs:用纯 Go 与 IPVS 内核模块通信的 netlink 编程实战

Moby ipvs:用纯 Go 与 IPVS 内核模块通信的 netlink 编程实战 Moby ipvs用纯 Go 与 IPVS 内核模块通信的 netlink 编程实战【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby导读本文围绕 Moby 仓库中内嵌的 moby/ipvs 组件 README 展开讲解这套以原生 Go 实现的 IPVS 用户态编程库它通过 netlink socket 与 Linux 内核的 IPVSIP Virtual Server模块直接通信让 Go 程序无需依赖ipvsadm就能增删查改虚拟服务Service与真实服务器Destination。读完本文你将掌握该库的数据模型、HandleAPI 用法、调度算法与转发方式常量并了解它在 Moby 中承载 swarm 服务负载均衡的具体落地位置。一、IPVS 与这套 Go 库的定位IPVSIP Virtual Server是 Linux 内核内置的第四层负载均衡框架运行在内核空间常见于 LVSLinux Virtual Server方案中。传统上管理 IPVS 规则靠ipvsadm用户态工具而在容器场景里编排系统需要以编程方式按网络命名空间network namespace动态下发规则这正是引入 Go 库的动机。仓库中的说明文档写道ipvs 提供了一套原生 Go 实现通过 netlink socket 与 IPVS 内核模块通信ipvs provides a native Go implementation for communicating with IPVS kernel module using a netlink socket。这句话概括了该库的两大技术要点全程 Go无需cgo或调用外部二进制通信链路是 netlink socket具体为 generic netlinkfamily 名为IPVS。该库目前以 vendor 副本形式存在于 vendor/github.com/moby/ipvs/从目录结构可以确认源码只有四个文件入口文档 doc.go、导出 API 与数据结构 ipvs_linux.go、netlink 编解码实现 netlink_linux.go、内核协议常量 constants_linux.go外加许可证 LICENSE。文件名中的_linux后缀表明该库面向 Linux 内核实现。二、快速上手文档给出的最小用例原文档给出了一个完整的入门代码块使用前只需 import 一个包import ( log github.com/moby/ipvs ) func main() { handle, err : ipvs.New() if err ! nil { log.Fatalf(ipvs.New: %s, err) } svcs, err : handle.GetServices() if err ! nil { log.Fatalf(handle.GetServices: %s, err) } }这段代码的执行路径值得逐层拆解它正好涵盖了库的两阶段工作方式ipvs.New()建立句柄。查看 ipvs_linux.go#L85-L113 可知空字符串表示在当前网络命名空间创建句柄传入路径如/proc/pid/ns/net则可让句柄指向指定命名空间。句柄内部会做一次setup()先尝试modprobe -va ip_vs装载内核模块失败仅记录警告再通过 generic netlink 查询IPVSfamily 的 ID供后续所有请求复用。handle.GetServices()发起查询。它内部走doGetServicesCmd(nil)在请求上打上NLM_F_DUMP标志请求内核 dump 全部服务记录再把响应逐条解析成*Service。对照 ipvs_linux.go#L171-L193除了GetServices()返回当前命名空间全部服务外还提供了GetService(s)——它精确匹配一个服务并强约束结果必须恰好为 1 条否则返回Expected only one service obtained%d错误。三、核心数据模型Service、Destination 与 Config3.1 Service——虚拟服务VIPipvs_linux.go#L18-L34 定义了Service结构体一个字段对应内核中一条虚拟服务规则字段类型含义Addressnet.IP虚拟服务地址VIPProtocoluint16传输层协议如syscall.IPPROTO_TCP/UDPPortuint16服务端口FWMarkuint32防火墙标记。非 0 时按 fwmark 标识服务此时Address/Port不参与匹配SchedNamestring调度算法名如rr、wrr、lc、wlcFlagsuint32服务标志Timeoutuint32会话超时Netmaskuint32与Address配合表示地址掩码多用于持久连接AddressFamilyuint16AF_INET或AF_INET6PENamestring持久引擎名可为空StatsSvcStats内核累计的服务统计序列化逻辑见 netlink_linux.go#L74-L101 的fillService它会按嵌套属性nested attribute逐个写入地址族、协议、地址、端口等。值得注意的实现细节是端口统一用网络字节序大端编码写入ipvsSvcAttrPort而FWMark ! 0时协议/地址/端口属性一律不写只发 fwmark——这与内核 IPVS 中 fwmark 服务与地址服务互斥的语义一致。3.2 Destination——真实服务器RSDestination见 ipvs_linux.go#L50-L63描述虚拟服务背后的一台真实服务器Address/Port真实服务器地址与端口Weight权重参与 wrr/wlc 等加权调度ConnectionFlags转发方式标志见下文第四节实际发出时只取其低 3 位ConnectionFlags ConnectionFlagFwdMaskUpperThreshold/LowerThreshold配合最少连接类调度时的心跳式上下阈值ActiveConnections/InactiveConnections活动/非活动连接数查询回读字段AddressFamily地址族。关于地址族还有一个兼容性细节老内核低于 3.18不会回传 destination 的地址族属性因此 netlink_linux.go#L454-L465 在AddressFamily 0时依据地址字节推断——IPv4 地址在内核回包中表现为前 4 字节有效、后 12 字节全零的 16 字节数组据此区分 IPv4/IPv6。3.3 统计与超时配置SvcStatsipvs_linux.go#L36-L48记录Connections、PacketsIn/Out、BytesIn/Out以及每秒速率CPS、PPSIn/PPSOut、BPSIn/BPSOut内核每类一个计数字段名与ipvsStatsConns等属性一一对应见 constants_linux.go#L91-L103。DstStats在源码中就是type DstStats SvcStats的复用。Configipvs_linux.go#L68-L73封装三条超时TimeoutTCP、TimeoutTCPFin、TimeoutUDP类型是time.Duration。内核返回的原始值是秒库在parseConfig中换算成秒级 Durationnetlink_linux.go#L576SetConfig语义是0 表示不修改。四、调度算法与转发方式常量调度算法名与内核字符串完全一致直接赋值给Service.SchedName。完整常量见 constants_linux.go#L127-L156常量内核名含义源码注释RoundRobinrr在所有真实服务器间平均分配请求WeightedRoundRobinwrr按权重比例分配权重高者优先且获得更多请求LeastConnectionlc分配给活动连接数更少的服务器WeightedLeastConnectionwlc结合连接数与权重分配DestinationHashingdh按目的 IP 查静态哈希表分配SourceHashingsh按源 IP 查静态哈希表分配转发方式定义了两组同名常量一组面向 service 的ConnectionFlag*一组面向连接的ConnFwd*掩码0x0007取低 3 位见 constants_linux.go#L105-L176常量值说明ConnectionFlagMasq/ConnFwdMasq0x0000NAT/Masquerade 方式改写目的地址后转发最常用ConnectionFlagLocalNode/ConnFwdLocalNode0x0001转发给本机ConnectionFlagTunnel/ConnFwdTunnel0x0002IPIP 隧道封装转发ConnectionFlagDirectRoute/ConnFwdDirectRoute0x0003DR 直接路由仅改写二层目的 MACConnFwdBypass0x0004旁路bypass绕过连接缓存仅连接级定义以用户态对照ipvsadm -m/-g/-i三种模式的本质就是写这三类值MasqNAT、DirectRouteDR、TunnelTUNNEL。五、Handle API 全景Handle是命名空间级别的编程句柄在 ipvs_linux.go#L75-L80 定义内部持有一个 generic netlink socket 与自增序号。调用方向内核发送请求时库会对每个请求做Seq递增、匹配响应中的Seq与发送端Pid并在收到NLMSG_ERROR时把内核错误码还原成syscall.Errnonetlink_linux.go#L204-L256。其导出方法可归纳为三组组别方法作用生命周期New(path)/Close()在指定命名空间创建/关闭句柄虚拟服务NewService、UpdateService、DelService、IsServicePresent、GetService、GetServices、Flush增/改/删/查虚拟服务Flush清空当前命名空间全部服务真实服务器NewDestination、UpdateDestination、DelDestination、GetDestinations在指定服务下管理真实服务器超时配置GetConfig/SetConfig读写 IPVS 的 TCP/TCPFIN/UDP 超时从方法名到内核命令的映射在 ipvs_linux.go#L123-L178底层调用doCmd并最终落到 constants_linux.go#L22-L42 中的ipvsCmdNewService、ipvsCmdSetDest、ipvsCmdFlush等 generic netlink 命令号。另外句柄创建时会设置两级超时以避免死锁见 ipvs_linux.go#L13-L16发送超时 30 秒、接收超时 3 秒。接收侧收到EAGAIN时继续循环等待下一批消息。完整的增删改查示例文档用例之外结合上述 API 可以写出完整的管理流程——先建一个 TCP 8080 的虚拟服务挂两个 NAT 转发方式的真实服务器再查询回读handle, err : ipvs.New() if err ! nil { log.Fatalf(ipvs.New: %s, err) } defer handle.Close() svc : ipvs.Service{ Address: net.ParseIP(10.0.0.100), Protocol: syscall.IPPROTO_TCP, Port: 8080, SchedName: ipvs.WeightedRoundRobin, // wrr AddressFamily: syscall.AF_INET, } if err : handle.NewService(svc); err ! nil { log.Fatalf(NewService: %s, err) // 已存在时内核会返回 EEXIST } for _, rs : range []string{192.168.1.11:8080, 192.168.1.12:8080} { addr, portStr, _ : net.SplitHostPort(rs) port, _ : strconv.Atoi(portStr) dst : ipvs.Destination{ Address: net.ParseIP(addr), Port: uint16(port), Weight: 1, ConnectionFlags: ipvs.ConnectionFlagMasq, // NAT 转发 AddressFamily: syscall.AF_INET, } if err : handle.NewDestination(svc, dst); err ! nil { log.Fatalf(NewDestination: %s, err) } } svcs, err : handle.GetServices() if err ! nil { log.Fatalf(GetServices: %s, err) } for _, s : range svcs { dests, _ : handle.GetDestinations(s) log.Printf(svc %s:%d stats%v, %d backends, s.Address, s.Port, s.Stats, len(dests)) }需要提示的工程点Service.FWMark与Address/Port是互斥的两类服务标识创建 fwmark 服务时保留FWMark即可服务已存在时再次NewService会返回内核EEXIST编排逻辑里通常像 Moby 那样容忍该错误见下文回读的Service.Stats/Destination.Stats由内核实时累计可用来做流量与健康度观测。六、Moby 中的实际落地swarm 服务负载均衡该库在 Moby 仓库内并非孤立存在而是 swarm 内置 LB 的关键零件之一。在 daemon/libnetwork/service_linux.go 中可以找到直接的调用证据第 96 行与第 192 行调用ipvs.New(sb.Key())其中sb.Key()是沙箱sandbox的命名空间路径也就是说每个容器的网络命名空间各有一个 IPVS 句柄规则彼此隔离这正是库支持按路径创建命名空间句柄的设计目的第 144 行创建虚拟服务i.NewService(s)且用errors.Is(err, syscall.EEXIST)容忍服务已存在幂等地保证规则就绪第 158 行随后NewDestination(s, ipvs.Destination{...})为虚拟服务挂上后端容器。由此可以还原 Moby 使用该库的典型范式为某个负载均衡虚拟 IP/端口先NewService忽略EEXIST再逐一NewDestination写入后端地址当后端变化时更新或删除 destination当命名空间销毁时句柄随之释放。对内核模块的依赖则集中在句柄创建时的modprobe ip_vs——如果内核未开启 IPVS库会打出 Native loadbalancing will not work until this is fixed 的错误日志netlink_linux.go#L69因此运行时要求宿主内核携带ip_vs相关模块ip_vs、ip_vs_rr、ip_vs_wrr等。七、netlink 报文格式进阶理解如果希望进一步理解库的序列化设计netlink_linux.go 文件末尾的注释块给出了完整的报文图解。要点如下每次收发都以NETLINK MSG为单位内部由genlMsgHdr命令号cmd、版本version固定 version1 若干syscall.NetlinkRouteAttr属性组成属性是 TLV 结构ATTR LEN | ATTR TYPE | VALUEVALUE 按 4 字节对齐填充一个Service或Destination对象在报文里是一个嵌套属性其 VALUE 内部再包含一组平铺字段属性——库正是通过递归解析这层嵌套关系把内核返回的字节流还原成结构体parseService/parseDestination/parseConfigdump 类请求如GetServices、GetDestinations依赖NLM_F_DUMP/NLM_F_MULTI标志接收多条消息直到NLMSG_DONE。理解这层格式有助于排查属性缺失/多余一类问题也能解释上一节提到的兼容分支老内核少回一个ipvsDestAttrAddressFamily属性就需要靠地址字节内容做启发式推断。八、适用前提与小结仅限 Linux所有实现文件均带_linux.go构建标签没有非 Linux 的 fallback 实现依赖内核能力需要内核启用 IPVS 与 generic netlink 支持首次使用时库会尝试modprobe ip_vs无外部依赖命令相比调用ipvsadm本库通过 netlink socket 直连内核天然适合嵌入编排系统按命名空间编程管理规则许可证原文档标注版权为 Docker, inc.代码以 Apache 2.0 发布见 vendor/github.com/moby/ipvs/LICENSE。对开发者而言掌握这套 API 就等于掌握了以 Go 语言驱动 Linux IPVS 的标准姿势一条NewService 若干NewDestination即可在自己的容器网络或负载均衡组件里复刻 Moby swarm 内置 LB 的实现思路。【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表