IS-IS协议的链路状态数据库依赖LSP(Link State PDU)来泛洪拓扑信息。每台路由器生成的LSP并不是一个可以无限膨胀的数据包,它受接口MTU和协议字段长度限制。以太网环境下常见的LSP最大长度在1492字节左右,即便使用巨帧,也要预留L2封装开销。当路由器需要携带大量前缀、邻居或者自定义TLV时,单一LSP放不下,这时就要用到LSP分片。分片不是简单按字节切割,它涉及IS-IS头部中的分片编号和更多分片标志位,接收方还要根据这些字段还原原始数据集。下面用Ruby写一个简化版本,把分片和重组的核心步骤讲清楚。

LSP头部为什么不能忽略分片编号
IS-IS的LSP头部包含多个固定字段,其中与分片关系最紧密的是LSP ID。LSP ID由三部分组成:System ID(6字节)、伪节点ID(1字节)和分片编号(1字节)。分片编号占8位,取值范围0到255,编号0对应主LSP,编号1到255是扩展分片。路由器在生成链路状态信息时,如果数据量超过单个LSP能承载的上限,就会把数据拆成多片,每一片对应一个递增的分片编号。
除了分片编号,LSP头部中还有一个关键标志位叫More Flag,它位于LSP ID最后一个字节的最高位。当该位为1时,表示后面还有编号更大的分片;为0时表示这是当前数据集最后一个分片。接收方必须依赖这个标志位判断是否收齐了所有分片。很多人误以为把数据按固定长度切成几段就行,但如果没有正确设置More Flag和分片编号,接收方要么提前停止组装,要么永远等待不存在的分片,导致链路状态数据库无法同步。
另一个容易被忽略的约束是分片大小必须小于接口的LSP MTU。LSP MTU通常比IP MTU小,因为要预留IS-IS协议头部、L2封装以及可能的认证TLV空间。假设接口MTU为1500字节,减去以太网头14字节、IS-IS LLC头3字节、LSP头部约27字节,实际可用的数据空间大概在1400字节左右。如果分片设置过大,底层IP层可能会再次分片,这会显著增加丢包概率,并且很多IS-IS实现直接丢弃IP分片后的LSP。因此分片器必须显式地把每个LSP数据部分控制在安全范围内。
用Ruby实现一个可用的LSP分片器
为了把逻辑讲清楚,下面实现一个简化版的分片函数。输入原始payload、System ID、伪节点ID和最大LSP尺寸,输出一组LSP分片。每个分片用LSP ID和More Flag区分,数据部分直接承载原始字节序列。这里不依赖任何第三方Ruby库,只用标准的pack和slice方法。
# 简化版IS-IS LSP分片器
# 输入:
# payload: 原始链路状态数据(字节串,可以是TLV序列)
# system_id: 6字节System ID(字节串)
# pseudonode_id: 1字节伪节点ID
# max_lsp_size: 单个LSP的最大长度(含LSP头部)
# 输出:
# 数组,元素为Hash,包含lsp_id、more_flag、data
def fragment_lsp(payload, system_id, pseudonode_id, max_lsp_size: 1400)
raise ArgumentError, "system_id必须为6字节" unless system_id.bytesize == 6
raise ArgumentError, "pseudonode_id必须为1字节" unless pseudonode_id.bytesize == 1
# LSP头部开销:这里简化处理,实际IS-IS头部约27字节,加上协议标识等
# 我们预留32字节作为头部和TLV包装,业务数据空间为max_lsp_size - 32
header_overhead = 32
data_space = max_lsp_size - header_overhead
raise ArgumentError, "max_lsp_size过小,无法容纳头部" if data_space <= 0
fragments = []
offset = 0
fragment_number = 0
total_length = payload.bytesize
# 先处理空payload的情况:仍然需要生成一个空分片
if total_length == 0
lsp_id = system_id + pseudonode_id + [0].pack("C")
fragments << { lsp_id: lsp_id, more_flag: 0, data: "".b }
return fragments
end
while offset < total_length
chunk = payload.byteslice(offset, data_space)
offset += chunk.bytesize
more_flag = offset < total_length ? 1 : 0
# LSP ID最后一个字节:高位置More Flag,低7位存分片编号
fragment_byte = (more_flag << 7) | (fragment_number & 0x7F)
lsp_id = system_id + pseudonode_id + [fragment_byte].pack("C")
fragments << {
lsp_id: lsp_id,
more_flag: more_flag,
fragment_number: fragment_number,
data: chunk
}
fragment_number += 1
# 分片编号7位有效,编号0-127足够,超出说明数据集异常庞大
raise "分片数量超过127,数据集过大" if fragment_number > 127
end
fragments
end
上面的代码把More Flag放到LSP ID最后一个字节的最高位,分片编号限制在0到127。真实IS-IS实现中分片编号使用8位,但最高位被More Flag占用,因此可用的分片编号范围为0到127,共128片。如果原始数据超过128片能承载的容量,IS-IS路由器通常会触发过载保护,或者直接把多余的数据丢弃并记录日志。对于Ruby实验环境,这个限制足够清晰。
分片过程中有一个细节:每次取数据时使用byteslice而不是slice,确保处理二进制字节流时不会因为Ruby字符串编码问题产生意外转换。同时,数据块的大小是固定的data_space,最后一个分片通常会更短。More Flag根据offset是否已经到达total_length来计算,避免出现最后一个分片还带着More Flag为1的情况。这样接收方在拿到More Flag为0的分片后就可以停止等待。
分片重组与缺失分片检测
接收方收到一组LSP分片后,需要把它们还原成原始数据。第一步是从每个分片的LSP ID中提取分片编号和More Flag,然后按分片编号升序排列。第二步检查所有分片的System ID和伪节点ID是否一致,避免把不同路由器的分片混在一起。第三步判断More Flag的连续性:从编号0开始,编号n的分片如果More Flag为1,那么必须存在编号n+1的分片;最后一个分片的More Flag必须为0。最后把各分片的数据按顺序拼接。
# 简化版LSP分片重组器
# 输入:
# fragments: 数组,元素为Hash,包含lsp_id和data
# 输出:
# 重组后的原始数据字节串
# 异常:
# 缺失分片、编号重复、来源不一致时抛出异常
def reassemble_lsp(fragments)
raise ArgumentError, "分片列表不能为空" if fragments.empty?
parsed = fragments.map do |frag|
lsp_id = frag[:lsp_id]
if lsp_id.bytesize != 8
raise "LSP ID长度错误,期望8字节,实际#{lsp_id.bytesize}字节"
end
system_id = lsp_id.byteslice(0, 6)
pseudonode_id = lsp_id.byteslice(6, 1)
fragment_byte = lsp_id.byteslice(7, 1).unpack1("C")
more_flag = (fragment_byte >> 7) & 0x01
fragment_number = fragment_byte & 0x7F
{
system_id: system_id,
pseudonode_id: pseudonode_id,
fragment_number: fragment_number,
more_flag: more_flag,
data: frag[:data]
}
end
# 检查所有分片来自同一个System ID和伪节点ID
first = parsed.first
parsed.each do |frag|
unless frag[:system_id] == first[:system_id] && frag[:pseudonode_id] == first[:pseudonode_id]
raise "分片来自不同的LSP ID,不能重组"
end
end
# 按分片编号排序
parsed.sort_by! { |frag| frag[:fragment_number] }
# 检查编号连续性和More Flag
parsed.each_with_index do |frag, index|
if frag[:fragment_number] != index
raise "分片编号缺失或重复,期望#{index},实际#{frag[:fragment_number]}"
end
if index == parsed.length - 1
if frag[:more_flag] != 0
raise "最后一个分片的More Flag不为0,可能还有后续分片未收到"
end
else
if frag[:more_flag] != 1
raise "非末尾分片的More Flag不为1,分片序列不完整"
end
end
end
# 拼接数据
parsed.map { |frag| frag[:data] }.join
end
这段代码展示了重组过程中最关键的三个校验:LSP ID长度、来源一致性、编号连续性。如果中间缺了一个分片,比如编号0、1、3,那么解析后排序会发现编号2缺失,直接抛出异常。这种严格校验在实验室测试里很有用,可以立刻暴露分片器或传输过程中的问题。生产环境里IS-IS实现通常不会立即丢弃不完整的数据集,而是等待一段时间让缺失分片通过泛洪到达,只有超过超时时间才报告错误。
还需要注意一点:分片数据本身可能包含二进制内容,Ruby的字符串默认编码通常是UTF-8,但网络字节流应该是ASCII-8BIT。在调用byteslice之前,建议对输入数据调用force_encoding("ASCII-8BIT"),或者在函数入口统一处理。上面的示例没有显式转换,实际使用时可以补上。另外,如果底层协议栈已经做了完整性校验,重组阶段还可以额外比较每个分片的序列号、校验和等字段,但这里为了聚焦分片逻辑,暂时省去这些部分。
分片大小如何选择与MTU的适配
分片大小不能拍脑袋决定,它必须和实际接口的LSP MTU匹配。一个常见的错误是直接拿1500字节去减以太网头,以为数据空间是1486字节。但实际上IS-IS LSP还要经过LLC封装和协议头,至少还要减去十几字节。更稳妥的做法是在路由器上查看接口的LSP MTU配置,或者在实验代码中定义一个保守的默认值。如果分片后单个LSP超过IP MTU,IP层会再次分片,接收方对IP分片的LSP处理通常不友好,甚至直接丢弃。
在Ruby实现里,可以把最大LSP尺寸做成可配置参数,并在分片前对payload大小进行预估。例如假设data_space是1400字节,原始数据是10000字节,那么会产生8个分片。每个分片的头部相同,数据部分大小不同,最后一个分片只有约200字节。这种不均衡是正常的,因为More Flag只表示是否有后续,不会为了数据对齐而填充。
实际部署中,还可以把分片大小设置得比理论最大值略小一些,留出余量给未来可能增加的认证TLV或者协议扩展。如果路由器可能经过隧道或VPN,外层封装会进一步压缩可用MTU,过大的LSP分片会引发路径MTU黑洞。因此,很多网络工程师在启用IS-IS分片时,会先做路径MTU测试,再根据测试结果调整LSP MTU和分片阈值。
另外,IS-IS分片机制与OSPF的LSA分片思路不同,IS-IS直接使用LSP ID中的分片编号,OSPF则会在LSA头中携带更多信息。理解这一点可以避免在排查数据库不一致问题时混淆两个协议的行为。Ruby代码输出了每个分片的lsp_id,可以直观看到分片编号的变化,也方便和抓包结果对照。
这篇简化实现覆盖了IS-IS LSP分片与重组的基本框架,省去了实际协议中的TLV解析和序列号字段,但保留了核心的编号、标志位和拼接约束。你可以在这个骨架上继续扩展,加入完整的LSP头部构造、校验和计算以及多源分片合并逻辑,用于协议测试或者教学演示。