ndnSIM仿真平台使用之仿真数据获取与处理

 

本文首先介绍如何通过平台内建输出程序与打印运行日志两种方式获取仿真数据,然后介绍如何使用Python处理这些数据。

版本信息如下:

操作系统:Ubuntu 16.04
ndnSIM:ndnSIM-2.1
ns-3-dev:ns-3.23-dev-ndnSIM-2.1

数据获取

ndnSIM仿真实验中,常用的数据为每个节点的缓存命中率和吞吐量(包括packets/s与kilobytes/s)等,如果不对ndnSIM的转发与缓存代码做修改,这两种数据均可使用内建的输出程序获得,但是当为了实现自定义的转发与缓存策略需要大量修改相关源码时,内建的输出程序输出的数据会不准确。因此,最为稳妥的方式是打印运行日志以获取所需数据。

平台内建输出程序

此部分主要参考官网文档,NFD’s Content StoreObtaining metrics

1. 缓存命中率

ndnSIM2.1中支持两种版本的Content Store实现,新版本的CS中只有FIFO缓存替换策略,旧版本的CS中缓存替换策略更加丰富(此处官网有更详细的介绍https://ndnsim.net/2.1/cs.html),且只有使用旧版本的实现才可使用平台内建的命中数与丢失数统计程序。如果研究目的不涉及到新的缓存策略,推荐使用旧版本的CS。以ndn-simple.cpp(在原示例中增加一个节点)文件为例,使用旧版CS且缓存替换策略为LRU,即:

// Install NDN stack on all nodes
ndn::StackHelper ndnHelper;
ndnHelper.SetDefaultRoutes(true);
ndnHelper.SetOldContentStore("ns3::ndn::cs::Lru", "MaxSize", "100");
ndnHelper.InstallAll();

然后在Simulator::Run();前添加缓存命中信息输出代码:

ndn::CsTracer::InstallAll("cs-trace.txt", Seconds(1.0));
Simulator::Run();
Simulator::Destroy();

其中Seconds(1.0)表示1秒统计1次。然后运行ndn-simple.cpp,在ns-3的主目录下,可以发现程序输出cs-trace.txt文件,内容为:

image

可以看出程序每隔1秒输出一次每个节点的命中包数与丢失包数,稍加计算即可得到单个缓存节点的命中率与全网平均命中率。

2. 节点吞吐量

使用ndn::L3RateTracer可以输出每个节点的流量信息,其中常用的为进/出兴趣包/数据包数量,在Simulator::Run();前添加如下代码:

ndn::L3RateTracer::InstallAll("rate-trace.txt", Seconds(1.0));
Simulator::Run();

然后运行ndn-simple.cpp,在ns-3的主目录下,可以发现程序输出文件rate-trace.txt,内容为:

节点0 节点1
image image
节点2 节点3
image image

由上可以清楚看到每个节点的InInterests、OutInterests、InData和OutData等信息,统计指标包括数量与字节。这里要额外说明第四列FaceDescr的选择,注意到每个节点包括四种FaceDescr:internal、netDeviceFace、ndnFace和appFace。我们需要用到的是netDeviceFace和appFace。

为了介绍这两种FaceDescr的区别,我们再次说明实验中用到的ndn-simple.cpp的拓扑结构:节点0、1、2、3串联,节点0上安装用户(Consumer),节点3上安装服务器(Provider)。

在上图中,节点1和节点2的netDeviceFace有两个端口号,而节点0和节点3的netDeviceFace只有一个端口号,因此netDeviceFace表示该节点与其他路由节点相连的端口。节点1和节点2没有appFace,而节点0和节点3分别有一个appFace,因此appFace表示该节点与用户或服务器相连的端口。

因此在这个示例中,统计服务器的负载时使用的是节点3的appFace端口的OutInterests和InData的信息。

打印运行日志

1. 终端打印

ndnSIM最常用的运行示例的方式是在终端里直接输入:

./waf configure --enable-examples
./waf --run ndn-simple

此时终端里仅输出开始与结束的信息(或者以cout方式打印出来的信息)。

image

此外,ndnSIM还支持以日志模式运行示例,可以打印出代码中NS_LOG*内的信息。在终端里输入:

NS_LOG=ndn.Consumer:ndn.Producer:nfd.Forwarder ./waf --run ndn-simple

此时终端里输出所选择NS_LOG的信息,以上命令中为ndn.Producerndn.Consumer(多选时用“:”隔开),即生产者类的方法里NS_LOG_INFO(*)中的内容、用户类的方法里NS_LOG_INFO(*)的内容和转发类的方法里NFD_LOG_DEBUG(*)中的内容,例如:

ndn.Consumer,在ndn-comsumer.cpp中
image
对应终端显示
image
ndn.Producer,在ndn-producer.cpp中
image
对应终端显示
image
ndn.Forwarder,在forwarder.cpp中
image
对应终端显示
image

NS_LOG=*能够选择的参数有很多,可以在终端里输入一个错误的参数以查看所有有效参数列表,如:

image

这里参数的名称实际上可以在对应文件的开头找到,比如在ndn-producer.cpp中,有如下定义:

image

所以如果自定义一个producer,比如ndn.ProducerNew,需要在对应ndn-producer-new.cpp文件中指定名称,即NS_LOG_COMPONENT_DEFINE("ndn.ProducerNew")

2. 日志文件打印

考虑到将日志信息打印到终端里不方便处理(当然也可以复制粘贴),因此更加有效的获取数据的方式是将日志信息打印到文件中,在终端中运行以下命令即可:

export NS_LOG=ndn.Consumer:ndn.Producer:nfd.Forwarder \\ 选择要打印的参数,选取方式同上
./waf --run ndn-simple>out.log 2>&1 \\ 运行示例ndn-simple

在ns-3的主目录下,可以发现程序输出的out.log文件,其中内容与在终端内以日志模式运行输出的信息完全一致。

数据处理

通过ndnSIM平台内建输出程序获取的数据只需稍加处理即可得到缓存命中率和节点吞吐量等,在此不做赘述。而通过打印运行日志获取的数据同样处理起来较为简单,仅需统计特定NS_LOG出现的次数即可,我们用以上获得的out.log文件为例,展示如何使用Python计算缓存命中率和节点吞吐量。

image

计算缓存命中率

分别统计forwarder.cpp中onContentStoreHitonContentStoreMiss方法调用次数,即统计out.log中包含字符串"onContentStoreHit""onContentStoreMiss"的行数:

hits = 0.0
misses = 0.0
f = open(r"out.log")
  line = f.readline()
  while line:
    if "onContentStoreHit" in line:
      hits += 1.0
    if "onContentStoreMiss" in line:
      misses += 1.0
    line = f.readline()
f.close()
hit_ratio = hits / (hits + misses)

需要注意的是以上统计的是全网平均命中率,若需统计单个节点的命中率,则只需依据节点号(每一行中第二个元素)分别计算即可。若只统计特定时间段内(如缓存系统稳定时)的命中率,则只需依据日志记录时间(每一行中第一个元素)分段计算即可。

计算节点吞吐量

实验中最常使用的吞吐量数据是服务器负载,即每秒钟发出的数据包个数。所以统计out.log文件中包含字符串"responding with Data"的行数再除以实验运行时间即可获得服务器负载(同样可以分段统计)。

packets = 0.0
f = open(r"out.log")
  line = f.readline()
  while line:
    if "responding with Data" in line:
      packets += 1.0
    line = f.readline()
f.close()
server_load = packets / 20.0

若统计单个节点的吞吐量,则依据节点号分别计算forwarder.cpp中onIncomingInterestonOutgoingInterestonIncomingDataonOutgoingData方法调用次数即可,此处若按统计包含同名字符串的行数的方式计算可能会有重复(同一方法内出现两次或多次同名字符串,具体要看源码),因此稳妥起见,最好在这些方法中自定义NS_LOG信息并统计包含自定义字符串的行数。比如在onOutgoingData方法添加NFD_LOG_DEBUG("Custom string");,然后统计out.log文件中包含字符串"Custom string"的行数。

void Forwarder::onOutgoingData(const Data& data, Face& outFace){
  ···
  NFD_LOG_DEBUG("Custom string");
  ···
}
packets = 0.0
f = open(r"out.log")
  line = f.readline()
  while line:
    if "Custom string" in line:
      packets += 1.0
    line = f.readline()
f.close()

计算其他指标

使用打印运行日志的方式计算其他的指标时,若源码中提供相应的NS_LOGNFD_LOG_DEBUGNS_LOG_INFO或其他)信息,则统计out.log文件中包含该信息中字符串的行数即可。若不提供或者新增自定义函数,则在需要统计的地方加入NS_LOG(*),其中*需要使用一个独特的字符串记号可与其他文本区别,然后统计out.log文件中包含该字符串记号的行数即可,示例同前述的NFD_LOG_DEBUG("Custom string");