
简介面向TSN网络研究与开发人员的实践资源围绕TSNkit与OMNeT仿真框架解决TSN网络建模、流量调度与性能验证问题。压缩包大小83.24MB内含OMNeT工程源码、TSNkit扩展模块及YANG配置示例目录结构清晰便于按模块检索学习。已有118人学习适合具备OMNeT基础、希望深入TSN调度机制的工程师与研究者。内容涵盖802.1AS时间同步、802.1Qbv流量调度、GACT调度器及PFC等关键协议实现结合示例项目可直接上手配置不同数据流与优先级分析丢包率、延迟、抖动等指标为实际TSN网络部署与策略优化提供参考。1. 用 TSNkit 和 OMNeT 做 TSN 网络调度与仿真入门先抓住什么用 TSNkit 和 OMNeT 做 TSN 网络调度与仿真是很多接触时间敏感网络的人拿到工程包后的第一直觉。TSN 的核心不是把两点连上而是把多条流按时间塞进交换机既不丢帧又不超时延。这个组合把配置生成和仿真验证串起来先从拓扑与流量需求算出门控列表再放到 OMNeT 里跑一遍看端到端时延和抖动是否达标。适合做车间网络、车载以太网、工业控制网实时验证的工程师也适合把流量整形从概念落成可观察仿真结果的学生。反直觉的是真正难的不是建仿真而是生成的门控列表没和仿真框架的队列模型对齐跑得再久也是一堆漂亮但没意义的数据。2. TSN 调度与仿真里的角色分配谁负责算谁负责跑2.1 先搞清楚 TSN 要调的是什么TSNTime-Sensitive Networking时间敏感网络不是单一标准而是一族 IEEE 802.1 规范的统称。和“调度”关系最直接的是 802.1Qbv 时间感知整形器Time-Aware ShaperTAS除此之外还有 802.1Qav 基于信用的整形、802.1Qbu 帧抢占、802.1Qci 流过滤与监管等。日常说“做 TSN 网络调度”十有八九指的是 802.1Qbv在交换机每个出端口上定义若干队列每个队列配一张门控表Gate Control ListGCL一张表按固定周期循环周期内每个时刻哪些队列打开、哪些关闭都是预先排好的。很多人会以为把帧的优先级改成高时延就下来了。普通以太网里这是玄学802.1p 优先级只决定交换机先服务谁低优先级突发流量一多高优先级照样在队列里排队。TSN 的门控相当于在端口出口放了一道时间闸门闸门按周期表开合只有对应时间窗打开时队列才放行。这样就做到了带宽的“时间隔离”而不是“优先级竞争”。所以调度问题的本质是给定网络拓扑、流集合和它们的时间要求排出一组端口门控表使所有硬实时流在自己的时间窗内完成转发同时不饿死普通流量。2.2 TSNkit把“算门控”这件苦差事自动化TSNkit 是一个面向 TSN 网络的离线调度与配置生成工具我一般把它理解为“门控计算器”。它不负责转发数据只负责从输入需求算出时间表。常见做法是把拓扑、链路、流需求整理成 JSON 或 XMLTSNkit 内部跑调度算法输出每个交换机端口在基准时间之后、每个周期内队列门的开闭动作。不同发布版的输入 schema 会有差异但核心输入逃不开三类东西节点列表交换机、端站、链路信息连接关系、速率、传播时延、流量列表源、目的、周期、帧长、优先级、端到端时延上限。之所以用它而不是自己写约束求解器是因为调度问题本质上是组合优化一条流路上每个出端口都要分配发送窗口多个流的窗口不能冲突还要满足端到端截止时间。这些约束手写非常容易漏尤其当拓扑里出现多条路径、周期流量和非周期流量混跑时。TSNkit 把这些约束封装好输出更接近交换机能懂的配置格式。但要清醒一点工具算出来的是理想化配置交换芯片的排队实现、时钟同步误差、仿真框架的模型细节都会影响最终效果。所以 TSNkit 只是前半段后半段必须交给 OMNeT 这类离散事件仿真去验证“这个表到底能不能跑”。2.3 OMNeT离散事件仿真为什么适合 TSNOMNeT 是开源离散事件仿真平台节点由模块组成模块之间通过门和消息连接。TSN 场景天然适合它门控是周期性事件帧到达是离散事件端到端时延、队列长度、门开关次数都可以逐事件统计。相比 NS-3OMNeT 的 TSN 生态更贴近协议层社区里常见两个扩展CoRE4INET 和 NeSTiNg。CoRE4INET 的模型更偏向交换芯片内部能模拟队列、门控、流过滤这些细节NeSTiNg 支持从 XML 读入流配置和门控配置和 TSNkit 这类工具对接起来更直接。选哪个取决于你手里的项目包已经预装了什么如果只装了基础 OMNeT那就得按扩展框架的版本去匹配工程文件。这里想强调一个容易忽略的点OMNeT 仿真时长和真实时间不是一回事。仿真里跑 5 秒可能只需要几十秒计算也可能要跑几个小时完全取决于事件密度。TSN 调度的场景里事件密度主要来自周期流和门控周期周期 1ms 的流、门控周期 1ms跑 5 秒就是 5000 轮门控事件如果拓扑里有几十条流事件量很可观。所以仿真参数一上来就要想清楚跑多久、统计什么、在什么粒度上采样。否则你调了一晚上参数出来的结果只是噪声。3. 用 TSNkit 生成可仿真的门控表输入文件、调度脚本与 GCL 字段3.1 安装依赖与准备输入文件拿到 TSNkit 之后第一步不是急着写算法而是先把它跑起来。项目仓库的 README 一般会写清楚依赖和安装方式我习惯用虚拟环境隔离免得污染系统 Python。常见流程是先拉源码再建 Python 虚拟环境然后按 requirements 装依赖。依赖里一般会包含 numpy、scipy 或求解器相关的包不同版本差得很多所以不建议裸装。git clone tsnkit仓库地址 tsnkit cd tsnkit python3 -m venv venv source venv/bin/activate pip install -r requirements.txt这段命令做了四件事从仓库拉下源码、进入目录、创建独立虚拟环境、安装调度所需的依赖包。用虚拟环境不是可有可无的步骤TSNkit 的依赖和 OMNeT 的 Python 分析脚本经常冲突虚拟环境能让你在同一个机器上维护两套互不干扰的 Python 环境。仓库地址以项目页为准我这里不贴具体链接因为不同时间入口会变。装完后跑一次自带的示例确认工具本身没毛病再开始配自己的网络。3.2 用 Python 跑通一次最小调度输入文件准备好之后就可以写调度脚本了。下面是一个最小示例三条节点组成一条链路一条控制流从端站 A 发到端站 B周期 1ms帧长 200 字节截止时间 500 微秒。import json from tsnkit import TSNSchedule # 网络节点两个端站一台交换机 nodes [ {id: endpoint_A, type: endpoint}, {id: switch_1, type: bridge}, {id: endpoint_B, type: endpoint}, ] # 链路A 到交换机交换机到 B速率 100Mbps links [ {src: endpoint_A, dst: switch_1, rate: 100e6, datarate: 100e6}, {src: switch_1, dst: endpoint_B, rate: 100e6, datarate: 100e6}, ] # 控制流周期 1ms帧长 200B端到端截止 500us flows [ { name: ctrl_1, src: endpoint_A, dst: endpoint_B, period_us: 1000, frame_size_bytes: 200, deadline_us: 500, } ] scheduler TSNSchedule(nodesnodes, linkslinks) result scheduler.compute(flows) result.export_gcl(gcl_output.json) result.export_paths(paths_output.json)这段脚本先定义节点再定义链路最后定义流然后把三者交个调度器。compute 返回的结果里包含两条关键信息一是 gcl即每个交换机端口的门控表二是 paths即每条流实际走的路径。200 字节在 100Mbps 链路上传输时间大约是 16 微秒1ms 周期内的带宽占用不到 2%调度非常宽裕能准点排进同一个窗口。如果你把周期缩到 100 微秒或者把帧长改成 1500 字节两条连续帧就会挤进同一窗口这时候 GCL 的输出会明显变复杂甚至直接无解。3.3 读懂 GCL 输出每个字段以后都要和仿真对齐TSNkit 导出的 GCL 一般是 JSON拿到手不要直接扔给仿真先看几个关键字段。{ network: demo, base_time_ns: 0, cycle_time_ns: 1000000, port_gcl: [ { node: switch_1, port: 2, queue: 7, entries: [ {offset_ns: 0, state: open}, {offset_ns: 200000, state: close} ] } ] }base_time_ns 是这个门控表的基准时间所有 offset 都从这一刻开始算cycle_time_ns 是门控循环周期比如 1ms门控表会在整个仿真期间按这个周期重复。port_gcl 数组里每个元素绑定一个交换机的具体端口和队列queue 是队列编号entries 是一组门控动作在 offset_ns 时把门打开或关闭。上面这个例子意思很直白交换机 switch_1 的 2 号端口、7 号队列在每个 1ms 周期开始时放行 200 微秒然后关闭。这里要特别记住一个映射关系TSNkit 里的 queue 编号必须和 OMNeT 扩展框架里队列模块的编号一一对应。很多翻车都出在这一步——工具里算的是 7 号队列仿真框架却把配置塞给了 0 号队列结果门控开着但数据没走这个队列数据走了这个队列但门控没开。后面第四章会讲怎么在 ini 里做这个映射现在只需要在心理上建立“GCL 不是一张表而是一组端口队列时间窗的绑定关系”这个概念。4. 把 TSNkit 输出接进 OMNeT 跑通闭环最小 NED 工程与 ini 参数4.1 最小仿真工程怎么组织OMNeT 一个工程通常由 NED 文件、ini 配置文件、仿真模块代码三部分组成。NED 描述网络结构ini 描述仿真参数和模块参数模块代码在 TSN 扩展框架里已经写好所以搭工程时大部分工作是在写 NED 和 ini。目录结构可以保持简单比如这样tsn_demo/ package.ned networks/TSNDemo.ned simulation/omnetpp.ini results/ # 放仿真输出最小 NED 文件可以描述成一个三节点链两个端站中间夹一台交换机。具体怎么写取决于你用的扩展框架INET 风格的写法如下package tsn_demo; import inet.node.ethernet.Eth10M; import tsn_demo.nodes.TSNEndpoint; import tsn_demo.nodes.TSNSwitch; network TSNDemo { submodules: hostA: TSNEndpoint; hostB: TSNEndpoint; sw1: TSNSwitch; connections: hostA.ethg[0] -- Eth10M -- sw1.ethg[0]; sw1.ethg[1] -- Eth10M -- hostB.ethg[0]; }network 模块是仿真的顶层容器submodules 里实例化模块connections 里连接模块之间的门。Eth10M 是一条 10M 以太网链路的占位模块实际速率可以在 ini 里覆盖。TSNEndpoint 和 TSNSwitch 是从扩展框架继承来的节点类型它们内部已经包含协议栈、队列和门控模块。如果你的工程包用的是 CoRE4INET 或 NeSTiNg节点类型名会不同但 NED 的工作方式类似import 对应模块实例化连起来。4.2 omnetpp.ini 里必调的四个参数NED 只是搭骨架真正决定调度行为的是 ini 里的参数。以下四项是跑通 TSN 调度闭环必须检查的。[General] network tsn_demo.TSNDemo sim-time-limit 5s # 门控表入口 **.sw1.gclFile results/gcl_output.json **.sw1.cycleTime 1ms # 端站发包模型 **.hostA.app[0].period 1ms **.hostA.app[0].len 200B **.hostA.app[0].dest hostB # 统计记录端到端时延和队列门状态 **.sw1.ethg[*].mac.queue[*].gcl.state.record true **.hostB.app[0].endToEndDelay.record true第一段是仿真框架的公共设置network 指定启动哪个网络sim-time-limit 指定仿真结束时间。第二段把 TSNkit 算出的门控表和周期填进交换机的门控模块。第三段是流量模型hostA 上跑一个周期为 1ms、帧长 200 字节的应用发给 hostB。第四段是统计项重点记录门控状态和端到端时延。这里最常踩的坑是周期和门控周期不一致。TSNkit 的 cycle_time_ns 是 1000000对应 1msini 里就必须写 1ms 或 1e-3s不能写成 1000——OMNeT 里裸数字 1000 表示 1000 秒那你的门控表在一个仿真世纪里只循环了一次完全测不出调度效果。另外 sim-time-limit 设 5 秒看起来不长但对 1ms 周期流量意味着 5000 轮门控已经足够统计出稳定规律。如果你只是测一个临时场景可以先用 100ms 跑通流程再加大时长。4.3 命令行运行为什么用 Cmdenv 而不是 QtenvOMNeT 有图形界面 Qtenv 和命令行模式 Cmdenv。调试单条链路时 Qtenv 很直观能看动画和事件流但要批量跑多组参数时 Qtenv 会拖慢速度而且结果不好导出。我一般到了可复现阶段就切到 Cmdenvomnetpp -u Cmdenv -f omnetpp.ini \ --output-scalar-fileresults/scalars.sca \ --output-vector-fileresults/vectors.vec这条命令指定了无界面模式运行输出标量统计和向量统计。标量文件适合保存平均时延、总丢包数这类单值结果向量文件适合保存端到端时延随时间变化的曲线。跑完之后用 OMNeT 自带的 IDE 或者 Python 读这些文件做分析。可能会遇到一个很现实的麻烦OMNeT 默认把运行序号和配置名拼进文件名里多次跑同一命令不会覆盖旧结果而是生成新后戳文件。这不算 bug是防止意外覆盖的机制但也意味着你脚本化批量跑参数时要自己对结果文件做整理。5. TSN 仿真里的常见坑与排查顺序从门控不生效到调度无解5.1 门控表加载了但时延一点没变化现象跑完统计平均时延和没开调度之前几乎一样高优先级流和普通流看不出差别。原因这是最隐蔽的坑。GCL 文件确实被模块读进来了但门控模块没有挂在发送路径上。很多以太网模块默认走普通 FIFO 出队流程根本不执行 802.1Qbv 的时间门逻辑。读入 GCL 只是给模块存了个配置模块内部负责流量整形的逻辑没启用时这个配置就是死数据。我在大型工程里见过不止一次配置文件路径、队列编号全对但忘了在 ini 里打开 TSN 特性开关的情况。解决先去 ini 查对应交换机的 qbv 或 tsn 开关是什么名字把值从 false 改成 true。然后在结果文件里看 gcl.state 这条记录确认队列门的开关状态确实在周期变化。如果等了很久状态永远是 open 或者永远是 close说明门控逻辑没参与。再加一条临时统计项在门控模块里打印每次 gate event 的时间戳快速定位挂载路径。5.2 单位换算不对造成仿真不收敛或发散现象丢包率忽高忽低时延曲线像锯齿把周期调大一号又正常调小就乱跳。看起来像是随机抖动其实是仿真不收敛。原因TSNkit 输出以纳秒、微秒为单位OMNeT 内部默认时间单位是秒。周期 1ms 如果被写成 1e-3 没问题可一旦写成 1000OMNeT 解释成 1000 秒门控表和流量模型就完全不在一个时间尺度上。流量模型按 1ms 发包门控表一个世纪才循环一次队列几乎永远关闭丢包率就变成一团糟。反过来如果把周期写成 0.001 当 1ms但门控表内部 offset 还是纳秒换算精度也容易出偏差。解决所有时间参数统一用带单位后缀的写法比如 1ms、200us、500ns。不建议在 ini 里写裸数字更不要把 TSNkit 的 JSON 原封不动塞进 ini。可以在测试阶段加一条断言仿真第 1ms 到第 2ms 之间门控状态至少发生一次切换否则直接中止仿真并报警。5.3 配置能读入但加载量为 0模块路径和节点名对不上现象日志没有报错OpenCount 统计恒为 0GCL 文件内容看起来也在模块参数里。原因ini 里的通配路径**.sw1.gclFile没有实际匹配到 NED 里的模块。常见原因有两个一是 TSNkit 输出的 node 名叫 switch_1NED 里模块叫 sw1两者始终没有被映射二是扩展框架的模块层级比想象中深gclFile 实际挂在sw1.ethg[0].macLayer.gcl这种深层路径上**.sw1.gclFile没命中。解决先用 omnetpp 的模块路径枚举功能把 sw1 下所有可配置参数列出来确认门控挂在哪个层级再改 ini 的通配符。不要怕路径长写全路径比通配符可靠得多。TSNkit 输出的 node 字段建议在导入时做一层别名映射把 switch_1 统一成 sw1格式对齐问题越早暴露越省事。5.4 多交换机串联后抖动变大先查时钟同步现象单交换机一切正常三台交换机级联后端到端抖动超出预算而且时延呈现出和链路级数明显相关的增量。原因门控表是每个交换机独立工作的如果各交换机的基准时间没有对齐上游交换机 11 点放行下游交换机 11 点刚好关闭帧到了也只能等下一轮。这个错位在单交换机里不存在一旦级联就放大。很多仿真工程只加载 GCL忽略时钟同步模块所有交换机 base_time 都从 0 开始忽略了链路传播时延相当于每个交换机都在自己心里倒计时谁也不等谁。解决先检查仿真里有没有启用 gPTP 或等同时钟同步机制。如果扩展框架没做同步就用最朴素的办法从 TSNkit 或手算链路时延给每个交换机设置不同的 base_time让相邻交换机窗口相对错开。调完再看向量文件里的门控状态应该能看到上游打开的时间和下游打开的时间形成错落接力而不是同时开关。5.5 TSNkit 直接报“无解”先做可调度性估算别硬排现象compute 返回空或者跑了很久后输出一个空 GCL没有任何报错。原因调度本质上受链路容量约束。当一条链路上经过的周期流总带宽占用超过 100%无论怎么排都排不出无冲突的时间表。很多人在工具上报无解时第一时间怀疑算法调参但真正的问题是流量模型本身已经不可调度。解决先做粗算。每条流的带宽是帧长除以周期把所有经过同一条链路的流加在一起利用率超过 70% 就要警惕超过 100% 则直接判定不可调度。利用率的公式是 sum(frame_size_bits / period_s) / link_rate。如果利用率高但没到 100%通常是帧长度离散导致时间碎片试着把周期错开或用帧抢占特性。一条血泪经验不要迷信工具的无解提示先把链路利用率表列出来哪条链路利用率快满了无解原因就浮出水面。6. 让仿真结果能拍板P99 时延、抖动与多场景对比技巧当门控表和 OMNeT 工程对齐之后下一步不是急着收工而是把仿真数据变成能支撑决策的指标。TSN 讲究的是最坏情况时延不是平均值。平均值好看但 P99 很差说明有一部分帧没赶上窗口在实时网络里就是事故。所以我会从向量文件里把所有帧的端到端时延提出来算 P99 和峰峰值再加一条基线对比。常见做法是先在 Python 里读 vectors.vec按帧号聚合出端到端时延序列然后算 P99。代码很简单import numpy as np import pandas as pd data pd.read_csv(results/vectors.vec, sep\t) e2e data[data[name] endToEndDelay:vector][value].values p50 np.percentile(e2e, 50) p99 np.percentile(e2e, 99) peak e2e.max() print(fP50: {p50*1e6:.2f} us) print(fP99: {p99*1e6:.2f} us) print(fPeak: {peak*1e6:.2f} us) print(fJitter(p99-p50): {(p99-p50)*1e6:.2f} us)这段代码把向量数据读进来筛选出端到端时延向量计算 P50、P99 和 P99 与 P50 的差值作为抖动指标。把时间单位转为微秒是为了和 TSNkit 里的截止时间直接对比。比如流截止时间 500usP99 到了 520us那就说明门控配置没有完全满足要求。如果 P50 很低但 P99 很高典型问题是少数几帧在交换机里错过了门控窗口要回头查流量突发而不是整体压缩周期。多场景对比会用到两层循环。外层遍历不同流量负载比如把发包周期从 1ms 改成 800us、600us内层跑固定次数取中位数避免单次随机种子带来的偶发结果。每次跑完 RENAMED 的结果文件都要重新命名避免被 OMNeT 的后戳机制搞乱。我一般会在脚本里自动生成带参数名的目录比如results/exp_600us/这样后面整理数据时不用猜。最后建议加一个“故障注入”场景。把某条链路在仿真中途断掉观察其余流的端到端时延恢复时间。这个步骤能验证两件事一是门控配置在非理想情况下会不会连带影响无关流量二是你的 TSN 设计有没有冗余路径。断链路的时间点最好选在两个门控周期边界避免碰巧避开了所有排队窗口测不出真实影响。做这套组合仿真做到后期我自己的一个习惯是任何调度参数改动都先保一份 TSNkit 输入的 JSON 快照再跑 OMNeT。因为 GCL 文件本身看不出原始流量意图只有回到输入文件才能解释为什么某个时隙画成了 200us。每次都重新生成配置再仿真比在结果图上猜原因要快得多。如果某个排程结果反直觉第一反应别去调仿真参数回 TSNkit 里看这条流在关键链路上的窗口分布八成是窗口重叠或带宽估算偏了。这套“输入可回溯、输出可统计”的流程能省掉大量半夜翻日志的时间希望帮到你。本文还有配套的精品资源点击获取