PeerSim中文教程(1):解析 Cycle-based 模式仿真
本文介绍了PeerSim的基本概念,并解析了两个示例以更清晰地说明PeeSim的仿真流程。
Peersim支持两种仿真模式,即Cycle-based的模型和传统的event-based的模型,本文专注于前者,
Cycle-based模型是一个简化的模型,拥有更好的伸缩性及性能,在拥有4GB内存的情况下,event-driven模式目前最多支持十万节点级别,而cycle-based模式则支持千万个节点级别。 但是Cycle-based模型缺少对传输层的仿真和并行处理,节点之间是直接通信的,仿真核心以一定的顺序周期性地给以节点控制。在运行时,可以进行任意的操作,如调用其它对象的方法并执行一些计算。
Cycle-based模型损失了一些真实性,虽然一些简单的协议可以忽略这些差别,但是在选择使用这个模型时,需要注意这些区别。我们可以相对简单地将Cycle-based的仿真移植到Event-driven引擎上,但在本文中不讨论这个话题。
一.基本介绍
PeerSim鼓励基于接口的模块化编程,每一个组件都能被其它实现了相同接口的组件代替,一般的仿真过程如下:
1. 选择网络大小(即节点数量)。
2. 选择要实验的一个或多个协议并进行初始化。
3. 选择一个或多个Control对象来监视感兴趣的属性,并在仿真时修改一些参数(比如,网络大小,协议的内部状态,等等)。
4. 根据配置文件,调用Simulator类运行仿真。
在仿真时创建的对象都是实现了一个或多个接口的类的实例,主要的接口如下所示:
Node | P2P网络是由节点组成的,节点是协议的容器。Node接口提供了对节点所包含的协议的访问方法,并为节点提供了固定的ID。 |
CDProtocol | 这是一个特定的协议,被设计用来在Cycle-based模型中运行,它只定义了在每一个周期中要运行的操作。 |
Linkable | 一般都由协议来实现,这个接口为其它协议提供了访问邻居节点集合的服务,节点间相同的linkable协议类的实例定义了一个覆盖网络。 |
Control | 实现了这个接口的类可以在仿真期间的某个时间点调度执行,这些类一般用于观察或修改仿真过程。 |
Cycle-based仿真的生命周期是这样的:
1. 读取配置文件(通过命令行参数传递进来),然后仿真器初始化网络中的节点和节点中的协议,每个节点都拥有相同的协议栈。节点和协议的实例是通过克隆来创建的,只有一个原型是通过构造方法创建,其它的节点和协议都是从这个原型中克隆而来。基于这个原因,协议类中clone方法的实现是很重要的。
2. 初始化操作,设置每个协议的初始状态。初始化阶段是由Control对象控制运行的,仅在实验开始时运行一次。在配置文件中,初始化的组件可以由init前缀识别,在下面讨论的initializer对象也是controls,但为了标记其功能以区别于一般的Control对象,它被配置用来在初始阶段运行。
3. 在初始化完成后,Cycle-based引擎在每一个周期中调用所有组件(protocols和controls)一次,直到完成了指定的周期数,或者某个组件决定终止仿真为止。在PeerSim中每一个对象(controls和protocols)都被赋以一个Scheduler对象,它定义了什么时候本组件将会被执行。在默认情况下,所有对象都会在每个周期中运行。但我们也可以配置一个protocol或control只在某些特定的周期中运行,也可以在每一个周期中指定组件的执行顺序。
下图展示了对controls和protocols的调度,其中C代表Control而P代表一个协议。图下方的数字代表PeerSim的周期,在最后一个周期后,可以运行一个control来获取最后的快照(snapshot)。
peersim-schedule
在一个Control收集数据时,数据将会被格式化并发送到标准输出或重定向到一个文件以进行后续的处理。
配置文件只是一个普通的ASCII文本,本质上就是java.util.Properties,以#开头的行代表注释。
用以下的方式在命令行的模式下运行,比如:
1
| java -cp peersim.Simulator config-edexample.txt
|
具体来说,可能是:
1
| java -cp D:\library\peersim-1.0.5\jep-2.3.0.jar;D:\library\peersim-1.0.5\djep-1.0.0.jar;D:\library\peersim-1.0.5\peersim-1.0.5.jar;D:\library\peersim-1.0.5\peersim-doclet.jar peersim.Simulator D:\library\peersim-1.0.5\example\config-edexample.txt
|
当然你的jar包和配置文件的位置可能有所不同,也可以将这个jar包直接加到 classpath上,就不必使用-cp参数来显示指定classpath。
二.配置文件示例一
Gossip-based Aggregation协议,Aggregation是聚集的意思,这里是指对一个分布于网络中的数值集合运行一个特定的函数进行计算(如求平均数,最大值,最小值等等),每个节点周期性地选择一个邻居节点进行通讯(基于覆盖网),并且在每次通讯时,基于前一个取得的近似值,相互更新它们下次计算的近似值。
本例将创建一个由50000个节点组成的固定P2P随机拓扑,选定的协议是使用average函数的Aggregation协议,每个节点中用于求平均的值使用一个区间在(0,100)的线性分布来初始化,最后再定义一个Control监视平均值。
03
| random.seed 1234567890
|
04
| simulation.cycles 30
|
09
|
|
10
| protocol.lnk IdleProtocol
|
11
|
|
12
| protocol.avg example.aggregation.AverageFunction
|
13
| protocol.avg.linkable lnk
|
14
|
|
15
| init.rnd WireKOut
|
16
| init.rnd.protocol lnk
|
19
| init.peak example.aggregation.PeakDistributionInitializer
|
20
| init.peak.value 10000
|
21
| init.peak.protocol avg
|
22
|
|
23
| init.lin LinearDistribution
|
24
| init.lin.protocol avg
|
25
| init.lin.max 100
|
26
| init.lin.min 1
|
27
|
|
28
| # you can change this to select the peak initializer instead
|
29
| include.init rnd lin
|
30
|
|
31
| control.avgo example.aggregation.AverageObserver
|
32
| control.avgo.protocol avg
|
上面的配置中,一部份是全局属性,另一部分对应单个组件的实例。如simulation.cycles是全局属性,而protocol.lnk.xxx则定义了lnk协议的xxx参数。
第6行的control.shf Shuffle,Shuffle类是用来重新洗牌,在每次重新洗牌后,在一个Cycle-based类型的仿真周期中,节点迭代的次序将会变成随机的,这个类只对Cycle-based类型的仿真起作用。
每个组件都有一个名字,比如lnk。对于协议,这个名字将会被映射到一个在PeerSim引擎中称为protocol ID的数值型索引,虽然这个索引不出现在配置文件中,但在仿真时需要使用它来访问协议,这在后面将进一步解释。
一个组件,即protocol或control由下面的语法来声明:
1
| [protocol|init|control].string_id [full_path_]classname
|
注意到类的全路径是可选的,事实上PeerSim可以在类路径中搜索类名,只有在多个类拥有相同的名称时必须使用全路径。init前缀定义了一个Initializer对象,它实现了Control接口。
组件的参数(如果有的话)则以下面的语法定义:
1
| [protocol|init|control].string_id.parameter_name
|
第10行定义了第一个协议,键部份包含了它的类型,而值则是组件的类名,由于IdleProtocol类在peersim包中,所以不必使用全路径。
可以为每一个组件声明参数,如第13行;而从第3行到第8行一些全局的仿真属性被引入,如仿真的总周期数和覆盖网的大小。Shufflecontrol对每一个周期中节点的访问顺序进行重新洗牌。
从第10行到第13行,引入了两个协议:
· IdleProtocol是存储邻居节点链路的一个静态容器,在进行静态拓扑建模的时候尤其有用,这个协议的唯一功能是作为其它协议的邻居信息的源,它没有实现CDProtocol接口但实现了Linkable接口,Linkable接口提供了到邻居节点的链路。
· AverageFunction是聚集协议的求平均数版本。它的参数(linkable)是很重要的,aggregation协议需要与邻居节点交流但是本身没有邻居节点列表。在模块化的方式中,它能应用于任何覆盖网络;定义覆盖网的协议栈应当在这里指定,参数linkable的值是实现了Linkable接口的协议的类名(在这里是IdleProtocol)。
从15行到26行用于初始化之前声明的所有组件。前面声明了3个初始化组件,但只有其中的2个被使用了(见29行)。第一个初始化器,peersim.init.WireKOut,进行的是对静态覆盖网的布线,特别的,节点以度数k随机地与其它节点相连接。
第2个和第3个初始化器是初始化aggregation协议的可选方案,在这里是指需要求平均的初值。初始化器设置初始值遵循peak分布或线性分布。Peak的意思是只有一个节点拥有与0不同的值。而线性则代表节点被拥有一个线性增加的值。两个初始化都需要一个指定了协议来进行初始化(协议参数)的协议名。额外的参数是PeakDistributionInitializer的range(max,min参数)。
使用peak还是linear分布是由include.init属性来决定的(29行),它指定了选择哪个初始化器。这个属性也定义了组件运行的顺序,注意到默认的顺序(即如果没有include属性),是根据字母排序的,对于protocol和control的include属性也是如此。
最后,31行和32行声明了最后一个组件:aggregation.AverageObserver。它使用的唯一参数是protocol,它引用了aggregation.AverageFunction协议类型,所以这个参数的值是avg。
在命令行下,注释掉第3行的seed,运行这个仿真,得到的结果将是:
01
| control.avgo: 0 1.0 100.0 50000 50.49999999999998 816.7990066335468 1 1
|
02
| control.avgo: 1 1.2970059401188023 99.38519770395408 50000 50.50000000000005 249.40673287686545 1 1
|
03
| control.avgo: 2 9.573571471429428 84.38874902498048 50000 50.500000000000085 77.89385877895182 1 1
|
04
| control.avgo: 3 23.860361582231647 71.93627224106982 50000 50.49999999999967 24.131366707228402 1 1
|
05
| control.avgo: 4 34.920915967147465 68.92828482118958 50000 50.49999999999994 7.702082905414273 1 1
|
06
| control.avgo: 5 42.37228198409946 59.94511004870823 50000 50.49999999999987 2.431356211088775 1 1
|
07
| control.avgo: 6 45.19621912151794 54.855516163070746 50000 50.499999999999844 0.7741451706754877 1 1
|
08
| control.avgo: 7 47.68716274528092 53.11433934745646 50000 50.49999999999949 0.24515365729069857 1 1
|
09
| control.avgo: 8 48.97706271318158 52.38916238021276 50000 50.50000000000026 0.07746523384731269 1 1
|
10
| control.avgo: 9 49.59674440194668 51.46963472637451 50000 50.49999999999937 0.024689348817011823 1 1
|
11
| control.avgo: 10 49.946490417215266 51.13343750384934 50000 50.50000000000048 0.007807022577928414 2 1
|
12
| control.avgo: 11 50.18143472395333 50.858337267869565 50000 50.49999999999982 0.002493501256296898 2 1
|
13
| control.avgo: 12 50.30454978101492 50.67203454827276 50000 50.500000000000206 7.90551008686205E-4 1 1
|
14
| control.avgo: 13 50.3981394834783 50.60093898689035 50000 50.49999999999967 2.518940347803474E-4 1 1
|
15
| control.avgo: 14 50.449347314832124 50.54962989951735 50000 50.5000000000003 8.071623184942779E-5 1 1
|
16
| control.avgo: 15 50.47368195506415 50.52608817343459 50000 50.49999999999999 2.566284350168338E-5 1 1
|
17
| control.avgo: 16 50.48510475374435 50.518871021756894 50000 50.50000000000012 8.191527862075119E-6 1 1
|
18
| control.avgo: 17 50.49082426764112 50.51000681641142 50000 50.49999999999945 2.570199757692886E-6 1 1
|
19
| control.avgo: 18 50.494810505765045 50.50556221303088 50000 50.5000000000003 8.197012224814065E-7 1 1
|
20
| control.avgo: 19 50.496876367842034 50.50296444951085 50000 50.499999999999524 2.640584231868471E-7 1 1
|
21
| control.avgo: 20 50.498457906558905 50.50182062146254 50000 50.500000000000334 8.565428611988968E-8 1 1
|
22
| control.avgo: 21 50.49905541635283 50.50096466374638 50000 50.49999999999974 2.721171621666857E-8 1 1
|
23
| control.avgo: 22 50.49946061473347 50.500553628252945 50000 50.49999999999975 8.590349265230611E-9 1 1
|
24
| control.avgo: 23 50.49972602272376 50.500315571370415 50000 50.5000000000004 2.6248542064007986E-9 2 1
|
25
| control.avgo: 24 50.4998450606816 50.50018053311878 50000 50.50000000000005 8.845012874999227E-10 1 1
|
26
| control.avgo: 25 50.499894793874255 50.500096923965216 50000 50.50000000000079 1.864501428663076E-10 1 2
|
27
| control.avgo: 26 50.4999267984512 50.500056126785694 50000 50.5000000000003 8.594896829690765E-11 1 1
|
28
| control.avgo: 27 50.49996613170552 50.50003198608762 50000 50.50000000000017 1.9554527178661528E-11 1 1
|
29
| control.avgo: 28 50.49997903068333 50.500019172164286 50000 50.499999999999766 3.274246411310768E-11 1 1
|
30
| control.avgo: 29 50.49998958653935 50.5000099409645 50000 50.50000000000045 0.0 1 1
|
Observer组件产生了很多数字,从第3列和第4列的数据(网络中的最大值和最小值),可以很容易地看到方差衰减得非常快,从第12个周期开始,几乎所有的节点都近似于真实的平均值50。可以用不同的数字或改变初始的分布(例如,使用aggregation.PeakDistributionInitializer)。同时,也可以替换覆盖网,比如你可以用Newscast来代替IdleProtocol。
三. 配置文件二
第二个例子是前面例子的改进版本。现在aggregation协议将运行于Newscast并添加了一些扩展。例如,有一个Control对象用来改变网络的大小:在第5个周期至第10个周期间,每次调用时删除500个节点。
03
| random.seed 1234567890
|
04
|
|
05
| simulation.cycles 30
|
06
|
|
11
| protocol.lnk example.newscast.SimpleNewscast
|
12
| protocol.lnk.cache 20
|
13
|
|
14
| protocol.avg example.aggregation.AverageFunction
|
15
| protocol.avg.linkable lnk
|
16
|
|
17
| init.rnd WireKOut
|
18
| init.rnd.protocol lnk
|
21
| init.pk example.aggregation.PeakDistributionInitializer
|
22
| init.pk.value 10000
|
23
| init.pk.protocol avg
|
24
|
|
25
| init.ld LinearDistribution
|
26
| init.ld.protocol 1
|
27
| init.ld.max 100
|
28
| init.ld.min 1
|
29
|
|
30
| # you can change this to include the linear initializer instead
|
33
| control.ao example.aggregation.AverageObserver
|
34
| control.ao.protocol avg
|
35
|
|
36
| control.dnet DynamicNetwork
|
37
| control.dnet.add -500
|
38
| #control.dnet.minsize 4000
|
39
| control.dnet.from 5
|
40
| control.dnet.until 10
|
在这里,全局参数与前面的例子相同,现在只讨论添加的扩展。
11行到12行选择了Newscast协议,它唯一的参数是缓存的大小。Newscast是一个流行性的内容分布和拓扑管理协议,系统中的每个peer都有一个部份的节点信息(事实上是一个固定大小的的节点描述符(node descriptor)的集合),每个描述符是由peer地址和一个创建描述符的时间戳组成的元组。
每个节点通过选择一个随机的邻居并交换信息来更新自身的状态,在交换信息时,两个peer归并信息并且只留下最新的项。在这种方式中,陈旧的信息(描述符)从系统中删除。这个过程允许协议修复覆盖网拓扑,用最小的代价删除死链,这种特性在一个节点频繁加入退出的动态系统中是很有用的。
17到28行是初始化部分,与前面的例子相同,然而这里选择使用peak分布。为了将其转换为线性分布,在31行改变include init的属性。peak分布将用0初始化所有节点的值,除了取得value参数的那个节点除外。
在36到40行,DynamicNetwork是定义的最后一个组件,如前所述,一个Control对象可以用来修改仿真中的一些参数,这种改变可以在每个仿真周期中进行(默认的行为),或者使用一种更好的途径。示例中选择的对象每次在control执行时删除500个节点。
参数add指定了要添加的节点的数量,它可以是负值。而参数size则为网络大小设定了一个下限值,如果达到了下限,那不会再删除节点;参数from和until是一个可以为每个组件指定的一般化参数,它们指定了组件所要执行的周期,还有一个未使用的参数是step,如果是2,则表示每两个周期才执行一次。
至于其它的参数则可以参考PeerSim的文档。
四. 高级配置特性
高级配置特性由Java Expression Parser提供,用于处理一些表达式。例如:
11
| # 等价于 A=7, B=3, C=4, D=1, E=2 and F=2.
|
但是注意不允许递归定义。
对于组件的集合,可以指定执行的顺序,默认是根据组件名的字母顺序来排序的,但也可以显式地覆写为:
1
| control.conn ConnectivityObserver
|
2
| control.myClass Class1
|
3
| control.1 Class2
|
4
| order.observer myClass conn 1
|
如果不是所有的名字都出现在这个列表中,那些缺失的对象会按字母顺序执行,例如:
会导致下面的运行顺序:
observer.myClass, observer.1, observer.conn.
另外一个特性是告知仿真器哪些项是允许执行的:
1
| include.control conn myClass
|
这样可以让control.conn和control.myClass以这种顺序执行,但如果这个列表为空,则什么都不会执行。
PeerSim中文教程(2):编写一个新协议
本文的目的是在PeerSim中用Cycle-based模型实现一个简单的负载均衡算法。节点的状态有两种值:本地负载(local load)和配额(quota),其中配额是指节点在每个周期中允许传输的“负载”的大小。配额是必要的,是一个时间单元中能传输的负载上限。每个节点与和它距离最远的邻居节点交换配额值,这里“距离最远”是指与当前节点的负载差异最大。经过对比距离,协议将在负载均衡时选用push或pull的方式。
在每个周期之后,配额值将会被存储。协议并不关心拓扑管理,它依赖于其它组件来访问邻居节点(例如,Newscast,或者由IdleProtocol实现的静态拓扑)。
一. 必要的组件
一般来说只编写一个协议类是不足够的,还需要一些附加的组件。例如,为了在每个周期结束时为每个节点存储配额值,需要一个特定的Control对象。基本上来说,PeerSim是一个可替换的组件集合,所以在开发时需要注意模块化以让代码尽可能重用,出于这样的目的,我们这样设计下面的类:
· protocol 它基于peersim.vector.SimpleValueHolder,这是一个简单的基类,用于访问一个浮点变量。Aggreation协议也使用了同样的基类。
· ResetQuota 用于在每个周期结束时存储每个节点配额的control。
· QuotaObserver 一个control,用于监视quota参数,即覆盖网中交换的流量大小。
· initialization 这是在aggregation示例中的初始化器,这里也可以直接使用,因为它们实现了同样的接口SingleValueHolder。注意在example包中提供的初始化器是轻量级的,开发者应当更多地使用peersim.vector.*包中的初始化器。
· observers 可以使用aggreagation.AverageObserver,因为这些组件实现了相同接口。
下面将根据源代码来解释这一过程:
01
| package example.loadbalance;
|
02
|
| |
03
| import peersim.config.Configuration;
|
04
| import peersim.config.FastConfig;
| |
05
| import peersim.core.*;
|
06
| import peersim.vector.SingleValueHolder;
|
07
| import peersim.cdsim.CDProtocol;
|
08
|
| |
09
| public class BasicBalance extends SingleValueHolder implements CDProtocol {
|
10
| protected static final String PAR_QUOTA = "quota";
| |
11
|
|
12
| /** Quota amount. Obtained from config property {@link #PAR_QUOTA}. */
|
13
| private final double quota_value;
|
14
| protected double quota; // current cycle quota
|
17
| public BasicBalance(String prefix) {
|
18
| super(prefix);
| |
19
| // get quota value from the config file. Default 1.
|
20
| quota_value = (Configuration.getInt(prefix + "."
| |
21
| + PAR_QUOTA, 1));
|
22
| quota = quota_value;
| |
这个类需要实现 peersim.cdsim.CDProtocol,它提供了一个nextCycle()方法,这个方法包含了协议的算法。而且,这个协议继承了SingleValueHolder类(它是SingleValue接口的实现),为内部变量提供getter和setter方法,允许control以可重用的方式来操作这些数据,在这个例子中,变量保存了节点的实际负载。
在构造方法中的String参数是配置组件的全名,例如,对于LoadBanlance协议来说是protocol.lb。
1
| // Resets the quota.
|
2
| protected void resetQuota() {
|
3
| this.quota = quota_value;
|
4
| }
| |
resetQuota方法在每个周期结束时被一个control对象调用,显然地,一个恰当的control条目应该在配置文件中出现,这里是loadbalance.ResetQuota
01
| public void nextCycle(Node node, int protocolID) {
|
02
| int linkableID = FastConfig.getLinkable(protocolID);
|
03
| Linkable linkable = (Linkable) node.getProtocol(linkableID);
|
04
| if (this.quota == 0) {
| |
05
| return; // quota is exceeded
|
06
| }
| |
07
| // this takes the most distant neighbor based on local load
|
08
| BasicBalance neighbor = null;
| |
09
| double maxdiff = 0;
|
10
| for (int i = 0; i < linkable.degree(); ++i) {
|
11
| Node peer = linkable.getNeighbor(i);
|
12
| // The selected peer could be inactive
|
13
| if (!peer.isUp())
|
14
| continue;
| |
15
| BasicBalance n = (BasicBalance)peer.getProtocol(protocolID);
|
16
| if (n.quota == 0.0)
| |
17
| continue;
|
18
| double d = Math.abs(value - n.value);
|
19
| if (d > maxdiff) {
|
20
| neighbor = n;
| |
23
| }
|
24
| if (neighbor == null) {
|
27
| doTransfer(neighbor);
|
28
| }
| |
这个方法是由CDProtocol接口声明的,它定义了协议的行为。这里的参数代表了一个对节点自身的引用(即仿真器调用其nextCycle方法的那个节点)和正在运行的协议的protocol ID。首先我们要取得实现了Linkable接口的协议的protocol ID来访问节点的邻居节点,这可以由下面的代码来完成:
1
| int linkableID = FastConfig.getLinkable(protocolID);
|
2
| Linkable linkable = (Linkable)node.getProtocol(linkableID);
|
使用静态类peersim.config.FastConfig我们可以取得为正在执行的协议而配置的linkable协议的protocol ID。
如果本地的配额是0,代表着这个节点已经使用完网络流量,所以直接return。
为了取得与本地节点距离最远的节点,我们循环遍历所有邻居节点的负载值;邻居节点的数量等于节点的度(这可以通过linkable接口来访问),通过linkable接口来取得节点的代码如下:
1
| Node peer = linkable.getNeighbout(i);
|
而从这个节点就可以取得BasicBanlance协议:
1
| BasicBalance n = (BasicBalance)peer.getProtocol(protocolID);
|
当协议寻找到一个合适的邻居节点后,调用doTransfer方法来进行负载均衡。
01
| protected void doTransfer(BasicBalance neighbor) {
|
02
| double a1 = this.value;
| |
03
| double a2 = neighbor.value;
|
04
| double maxTrans = Math.abs((a1 - a2) / 2);
|
05
| double trans = Math.min(maxTrans, quota);
|
06
| trans = Math.min(trans, neighbor.quota);
| |
07
| if (a1 <= a2) {// PULL phase
|
08
| a1 += trans;
| |
09
| a2 -= trans;
|
10
| } else{ // PUSH phase
|
11
| a1 -= trans;
|
12
| a2 += trans;
|
15
| this.quota -= trans;
|
16
| neighbor.value = a2;
|
17
| neighbor.quota -= trans;
|
18
| }
| |
doTransfer方法将会在当前节点和由参数指定的邻居节点间进行实际的负载交换,它决定了在负载均衡时是用pull还是push方法:在Push的情况下,本地值增加而其它节点的值减少,在push情况下则反之。maxTrans变量是两个涉及的节点需要达到平衡而传输的负载的绝对值;由于配额(quota)是每个周期中传输的上限,这个算法将会选择quota和maxTrans中的最小值,最后两个节点都会减去相同数量的负载值。
二. 负载均衡的control类代码
01
| package example.loadbalance;
|
02
| import peersim.config.*;
| |
03
| import peersim.core.*;
|
04
|
| |
05
| public class ResetQuota implements Control {
|
06
| // 参数
| |
07
| private static final String PAR_PROT = "protocol";
|
08
|
| |
09
| /** Value obtained from config property {@link #PAR_PROT}. */
|
10
| private final int protocolID;
| |
13
| public ResetQuota(String prefix) {
|
14
| protocolID = Configuration.getPid(prefix + "." + PAR_PROT);
|
17
| public boolean execute() {
|
18
| for (int i = 0; i < Network.size(); ++i) {
|
19
| ((BasicBalance) Network.get(i).getProtocol(protocolID)).resetQuota();
|
20
| }
| |
这段代码很简洁,部份原因是Control接口本身是很简单的,它只有一个execute方法。构造方法利用配置文件来进行初始化。execute方法会在所有的协议上调用 resetQuota方法,它通过Network类来访问协议,Network是一个只拥有静态数据域的静态类,你可将它视为是一个节点的数组。
三. 扩展协议
这是对前面的协议的扩展。核心部份是相同的,但是算法在决定将要发送或接收多少负载时,使用了全局的负载平均而不是根据距离最远的邻居节点的负载值。为了计算全局的负载平均值,这里有一个小技巧,虽然本来可以通过聚集来求取平均数,但我们可以通过运行一个拥有全局信息的静态方法来仿真aggregation协议,这个方法为所有节点初始化了一个全局变量,这样我们就能提升性能的同时又不损失太多的真实性。
这个协议也是为了利用newscast协议:当一个节点到达了全局负载值(平均),它将会将它的fail-state转变为DOWN,然后这个节点会从覆盖网退出,因为newscast协议会删除它。这样的影响是有多少节点达到平均负载则会减少多少个节点。
01
| package example.loadbalance;
|
02
|
| |
03
| import peersim.core.*;
|
04
| import peersim.config.FastConfig;
|
05
|
|
06
| public class AvgBalance extends BasicBalance {
|
09
| * The overall system average load. It is computed once by
|
10
| * {@link #calculateAVG(int)} method.
| |
11
| */
|
12
| public static double average = 0.0;
|
15
| * This flag indicates if the average value computation has been performed
|
16
| * or not. Default is NO.
| |
17
| */
|
18
| public static boolean avg_done = false;
|
21
| public AvgBalance(String prefix) {
|
22
| super(prefix); // calls the BasicBalance constructor.
|
25
| /**
|
26
| * Calculates the system average load. It is run once by the first
|
27
| * node scheduled.
|
28
| */
| |
29
| private static void calculateAVG(int protocolID) {
|
30
| int len = Network.size();
| |
31
| double sum = 0.0;
|
32
| for (int i = 0; i < len; i++) {
|
33
| AvgBalance protocol = (AvgBalance) Network.get(i).getProtocol(protocolID);
|
34
| double value = protocol.getValue();
| |
37
|
|
38
| average = sum / len;
|
第一部份是很简单的,定义了两个全局变量,average和avg_done,其中avg_done是个用来确定不进行超过一次计算的标志。注意,虽然看起来在构造方法中定义一个计算平均值的方法是一个更优雅的方案,但这种方案是错误的!因为在构造方法运行时,并不能保证负载的分布已经定义了:那样的话全局的平均数是未定义的。
1
| protected static void suspend(Node node) {
|
2
| node.setFailState(Fallible.DOWN);
| |
这个功能函数用于让节点从覆盖网中退出,这里只是简单地在Fallible接口中设置节点的状态。
01
| public void nextCycle(Node node, int protocolID) {
|
02
| // 只运行一次:
| |
03
| if (avg_done == false) {
|
04
| calculateAVG(protocolID);
|
05
| System.out.println("AVG only once " + average);
|
06
| }
| |
07
|
|
08
| if (Math.abs(value - average) < 1) {
|
09
| AvgBalance.suspend(node); // switch off node
|
10
| return;
| |
13
| if (quota == 0)
|
14
| return; // skip this node if it has no quota
|
17
| if (value < average) {
|
18
| n = getOverloadedPeer(node, protocolID);
|
19
| if (n != null) {
|
20
| doTransfer((AvgBalance) n.getProtocol(protocolID));
|
23
| n = getUnderloadedPeer(node, protocolID);
|
24
| if (n != null) {
| |
25
| doTransfer((AvgBalance) n.getProtocol(protocolID));
|
26
| }
| |
29
| if (Math.abs(value - average) < 1)
|
30
| AvgBalance.suspend(node);
| |
33
| if (Math.abs(((AvgBalance) n.getProtocol(protocolID)).value- average) < 1)
|
34
| AvgBalance.suspend(n);
| |
nextCycle方法是核心的协议算法,它首先检查平均数,如果标志没有设置就会进行计算。
如果当前负载和平均负载的差别小于1(每个周期中固定的配额值),那么节点将会根据newcast协议从覆盖网退出;进一步地,如果配额已经使用完,将会直接return。然后,协议会检查本地的负载值是小于还是大小平均值,并分别查找负载最大和最小的邻居,最后进行交换。
01
| private Node getOverloadedPeer(Node node, int protocolID) {
|
02
| int linkableID = FastConfig.getLinkable(protocolID);
| |
03
| Linkable linkable = (Linkable) node.getProtocol(linkableID);
|
04
| Node neighborNode = null;
| |
05
| double maxdiff = 0.0;
|
06
| for (int i = 0; i < linkable.degree(); ++i) {
|
07
| Node peer = linkable.getNeighbor(i);
|
08
| if (!peer.isUp()) // only if the neighbor is active
|
09
| continue;
|
10
| AvgBalance n = (AvgBalance)
|
11
| peer.getProtocol(protocolID);
|
12
| if (n.quota == 0)
| |
13
| continue;
|
14
| if (value >= average && n.value >= average)
|
15
| continue;
|
16
| if (value <= average && n.value <= average)
|
17
| continue;
|
18
| double d = Math.abs(value - n.value);
|
19
| if (d > maxdiff) {
|
20
| neighborNode = peer;
|
23
| }
|
24
| return neighborNode;
|
25
| }
| |
01
| private Node getUnderloadedPeer(Node node, int protocolID) {
|
02
| int linkableID = FastConfig.getLinkable(protocolID);
| |
03
| Linkable linkable = (Linkable) node.getProtocol(linkableID);
|
04
| Node neighborNode = null;
| |
05
| double maxdiff = 0.0;
|
06
| for (int i = 0; i < linkable.degree(); ++i) {
|
07
| Node peer = linkable.getNeighbor(i);
|
08
| if (!peer.isUp()) // only if the neighbor is active
|
09
| continue;
|
10
| AvgBalance n = (AvgBalance) peer.getProtocol(protocolID);
|
11
| if (n.quota == 0)
|
12
| continue;
| |
13
| if (value >= average && n.value >= average)
|
14
| continue;
| |
15
| if (value <= average && n.value <= average)
|
16
| continue;
| |
17
| double d = Math.abs(value - n.value);
|
18
| if (d < maxdiff) {
| |
19
| neighborNode = peer;
|
20
| maxdiff = d;
| |
23
| return neighborNode;
|
24
| }
| |
查找最大和最小负载的邻居节点的代码是很相似的,在这里都展示是出于完整性的缘故。
四. 协议的评估
负载均衡协议是为了减少负载的变化 ,而变化可以使用aggregation.AverageObserver或者loadbalance.LBObserver(它们是非常相似的)来进行分析,出于这个标准,两个协议几乎拥有相同的性能,并独立于最初使用的分布。然而,AVGBalance协议相对BasicBalance来说提升了整体的负载传输,AVGBalance传输了一个可证明是最小的负载。
我们可以实现一个control来观察正被传输的负载:
01
| package example.loadbalance;
|
02
|
| |
03
| import peersim.config.*;
|
04
| import peersim.core.*;
| |
05
| import peersim.util.*;
|
06
|
| |
07
| public class QuotaObserver implements Control {
|
08
| /**
| |
09
| * The protocol to operate on.
|
10
| */
| |
11
| private static final String PAR_PROT = "protocol";
|
12
|
| |
13
| /**
|
14
| * The name of this observer in the configuration file.
|
15
| */
|
16
| private final String name;
|
17
|
|
18
| /** Protocol identifier,*/
|
19
| private final int pid;
|
20
|
| |
21
| // 构造方法
|
22
| public QuotaObserver(String name) {
|
23
| this.name = name;
|
24
| pid = Configuration.getPid(name + "." + PAR_PROT);
|
27
| public boolean execute() {
|
28
| IncrementalStats stats = new IncrementalStats();
|
29
| for (int i = 0; i < Network.size(); i++) {
|
30
| BasicBalance protocol = (BasicBalance) Network.get(i).getProtocol(pid);
|
31
| stats.add(protocol.quota);
|
32
| }
| |
33
| /* 打印统计量*/
|
34
| System.out.println(name + ": " + stats);
|
原理是很简单的,在每一个仿真周期中,它收集剩余的quota并在终端上打印统计数据,从这些统计数据和配额的初始值就可以计算出已经被传输的负载。
PeerSim中文教程(3):拓扑生成器
本教程描述了如何构建一个新的PeerSim拓扑生成器。
1. 什么是拓扑?为什么它很重要?
在一个大型的动态P2P系统中,节点没有关于整个网络的信息,而所有的节点都可能拥有一些邻居节点,即节点能“感知”的peers,这种“感知”的关系就定义了一个覆盖网络,这是P2P系统中的一个基本概念。
很多P2P协议都需要在多个不同的网络拓扑上进行实验。PeerSim中的peersim.dynamic.Wire*类已经包含了很多拓扑结构,可以直接用来对linkable协议进行初始化,本教程将展示如何构建一个自定义的拓扑生成器。
2. 一个用来模拟Internet的简单模型
下面我们将编写一个拓扑生成器来构建类似于Internet的树状拓扑,整个构建过程基于一个特定的,与位置相关的preferential attachment方法,编写规则很简单,并且会考虑几何和网络的限制以更好地模拟真实的网络。Preferential attachment由参数a来调整,这个参数能扩大或减少几何位置所带来的影响。
这个规则的策略如下:给定一个单位正方形,将x0置于中心,即x0 = (0.5,0.5),这个节点被称为root,令W()为与root相隔的跳数(hops),对于i=1 … n-1,随机在单位正方形中选择一个xi,然后选择使下面的表达式值最小的节点xj来连接它:
在这里dist()是欧几里德距离而a (alpha)是权重参数,显然,
通过这个方案我们得到了一个x0以为根的树。这个拓扑中每个节点(除了root外)的出度都为1,如果想更深入地理解这个模型,可以参考下面的文章:
· Heuristically OptimizedTrade-offs: A New Paradigm for Power Laws in the Internet
· Degree distributions of the FKPnetwork model
· On Power-Law Relationships ofthe Internet Topology
3. 如何编码
我们的目标是编写一个可以根据 a (alpha)参数生成所需拓扑的PeerSim组件,并且能对生成的拓扑进行分析。这个拓扑可以在仿真过程中逐步生成,也可以用一个步骤生成拓扑,在这里我们倾向后者。为了构建需要的拓扑结构,我们需要下面的组件(注意这只是其中一种方案)。
· 一个protocol 类,可以存储坐标,它不具备行为元素,只是一个普通的容器。
· 一个initializer 类,可以为每个节点设置坐标值。
· 一个control 类,可在一个任意的linkable协议中根据坐标连接拓扑(在节点间添加link)
· 一个observer 类,将拓扑结构打印到一个文件中(例如用GnuPlot对图进行可视化)。
· 一个observer 类,用来收集节点入度的分布的统计数据
· 一个observer 类,用来测试对随机节点失效的健壮性
在下节我们将看到,一些我们列出来的类是PeerSim中的基本组件,它们都实现了Linkable接口,Linkable以模块化的方式为用户提供了一个能处理任何拓扑结构的抽象。
4. 编写代码
a. Protocol类
01
| import peersim.core.Protocol;
|
02
|
|
03
| public class InetCoordinates implements Protocol {
|
04
|
|
05
| /** 2d coordinates components. */
|
06
| private double x, y;
|
07
|
|
08
| public InetCoordinates(String prefix) {
|
09
| /* Un-initialized coordinates defaults to -1. */
|
10
| x = y = -1;
|
13
| public Object clone() {
|
14
| InetCoordinates inp = null;
|
15
| try {
|
16
| inp = (InetCoordinates) super.clone();
|
17
| } catch (CloneNotSupportedException e) {
|
18
| } // never happens
|
23
| public double getX() {
|
24
| return x;
|
27
| public void setX(double x) {
|
28
| this.x = x;
|
31
| public double getY() {
|
32
| return y;
|
35
| public void setY(double y) {
|
36
| this.y = y;
|
这个类只存储坐标,而links则会存储在其它任意实现了Linkable接口的协议中。
clone方法必须新定义并且捕捉和压制所有的异常(它们永远不会被抛出),因为这里只有基本类型,不需要深拷贝操作。
坐标组件并不是public的,但可以通过getter/setter方法来存取,这是很重要的,因为我们可以使用peersim.vector包以一个弹性化的方式来初始化坐标值,但在本文中我们并没有使用这个包。
b. 初始化类
01
| package example.hot;
|
02
| import peersim.config.Configuration;
|
03
| import peersim.core.CommonState;
|
04
| import peersim.core.Control;
|
05
| import peersim.core.Network;
|
06
| import peersim.core.Node;
|
07
| public class InetInitializer implements Control {
|
08
|
|
09
| private static final String PAR_PROT = "protocol";
|
10
|
|
11
| /** Protocol identifier, obtained from config property*/
|
12
| private static int pid;
|
13
|
|
14
| public InetInitializer(String prefix) {
|
15
| pid = Configuration.getPid(prefix + "." + PAR_PROT);
|
16
| }
|
19
| * Initialize the node coordinates. The first node in the Network
|
20
| * is the root node by default and it is located in the middle
|
21
| * (the center of the square) of the surface area.
|
22
| */
|
23
| public boolean execute() {
|
24
| // Set the root: the index 0 node by default.
|
25
| Node n = Network.get(0);
|
26
| InetCoordinates prot = (InetCoordinates) n.getProtocol(pid);
|
27
| prot.setX(0.5);
|
28
| prot.setY(0.5);
|
29
| // Set coordinates x,y
|
30
| for (int i = 1; i < Network.size(); i++) {
|
31
| n = Network.get(i);
|
32
| prot = (InetCoordinates) n.getProtocol(pid);
|
33
| prot.setX(CommonState.r.nextDouble());
|
34
| prot.setY(CommonState.r.nextDouble());
|
初始化类应当实现Control接口中唯一的execute方法,构造方法从配置文件中读取唯一的参数(protocol),它声明了持有坐标的协议。
这个类是很简单的,它生成了一致的随机坐标(x和y),唯一的例外是root节点,默认情况下,它的下标是0,固定为(0.5,0.5)。
为了生成随机数,CommonState中的静态的数据域r必须总是使用,因为这样保证了实验的可重复性。
c. wiring类
这个类继承了peersim.dynamic.WireGraph,它实现了Control接口并提供了处理拓扑的通用功能,同时也提供了一个图的接口。wiring的逻辑应该放在由子类调用的wire方法中,并且在默认情况下将下标为0的节点视为root。
这个类需要从配置文件中读取 a(配置文件中的alpha)和坐标容器的protocol ID(配置文件中的coord_protocol),这是由类的的构造方法来完成的,其它的参数,比如 protocol 是由父类继承的,它是一个实现了Linkable接口的协议。
01
| package example.hot;
|
02
|
|
03
| import peersim.config.Configuration;
|
04
| import peersim.core.Linkable;
|
05
| import peersim.core.Network;
|
06
| import peersim.core.Node;
|
07
| import peersim.dynamics.WireGraph;
|
08
| import peersim.graph.Graph;
|
09
|
|
10
| public class WireInetTopology extends WireGraph {
|
11
|
|
12
| private static final String PAR_ALPHA = "alpha";
|
13
| private static final String PAR_COORDINATES_PROT =
|
14
| "coord_protocol";
|
15
|
|
16
| // A parameter that affects the distance importance.
|
17
| private final double alpha;
|
18
|
|
19
| // Coordinate protocol pid.
|
20
| private final int coordPid;
|
21
|
|
22
| public WireInetTopology(String prefix) {
|
23
| super(prefix);
|
24
| alpha = Configuration.getDouble(prefix + "."
|
25
| + PAR_ALPHA, 0.5);
|
26
| coordPid = Configuration.getPid(prefix + "."
|
27
| + PAR_COORDINATES_PROT);
|
28
| }
|
31
| * Performs the actual wiring.
|
32
| * @param g
|
33
| * a peersim.graph.Graph interface object to work on.
|
34
| */
|
35
| public void wire(Graph g) {
|
36
|
|
37
| // Contains the distance in hops from the root node
|
38
| // for each node.
|
39
|
|
40
| int[] hops = new int[Network.size()];
|
41
|
|
42
| // connect all the nodes other than roots
|
43
| for (int i = 1; i < Network.size(); ++i) {
|
44
| Node n = (Node) g.getNode(i);
|
45
| // Look for a suitable parent node between those
|
46
| // allready part of the overlay topology: alias
|
47
| // FIND THE MINIMUM!
|
48
| // Node candidate = null;
|
49
| int candidate_index = 0;
|
50
| double min = Double.POSITIVE_INFINITY;
|
51
|
|
52
| for (int j = 0; j < i; j++) {
|
53
| Node parent = (Node) g.getNode(j);
|
54
| double jHopDistance = hops[j];
|
55
| double value = jHopDistance +
|
56
| (alpha * distance(n, parent, coordPid));
|
57
| if (value < min) {
|
58
| // candidate = parent;
|
59
| // best parent node to connect to
|
60
| min = value;
|
61
| candidate_index = j;
|
62
| }
|
63
| }
|
64
| hops[i] = hops[candidate_index] + 1;
|
65
| g.setEdge(i, candidate_index);
|
66
| }
|
69
| private static double distance(Node new_node,
|
70
| Node old_node, int coordPid) {
|
71
| double x1 = ((InetCoordinates)
|
72
| new_node.getProtocol(coordPid)).getX();
|
73
| double x2 = ((InetCoordinates)
|
74
| old_node.getProtocol(coordPid)).getX();
|
75
| double y1 = ((InetCoordinates)
|
76
| new_node.getProtocol(coordPid)).getY();
|
77
| double y2 = ((InetCoordinates)
|
78
| old_node.getProtocol(coordPid)).getY();
|
79
|
|
80
| if (x1 == -1 || x2 == -1 || y1 == -1 || y2 == -1)
|
81
|
|
82
| // NOTE: in release 1.0 the line above incorrectly
|
83
| // contains |-s instead of ||. Use latest CVS version,
|
84
| // or fix it by hand.
|
85
| throw new RuntimeException(
|
86
| "Found un-initialized coordinate. Use e.g.,\
|
87
| InetInitializer class in the config file.");
|
88
|
|
89
| return Math.sqrt((x1 - x2) * (x1 - x2)
|
90
| + (y1 - y2) * (y1 - y2));
|
d. observers
前面提到的observer有些已经由PeerSim提供了相应的实现。例如,为了计算节点度的分布,用户可以使用peersim.reports.DegreeStats;为了检验网络的健壮性,可以使用peersim.reports.RandRemoval:它打印生成的clusters的数目及大小,并作为随机删除的节点数量的函数。
然而,为了将拓扑转换为可绘图的形式,需要自行编写observer:InetObserver实现了Control接口和对应的execute方法,我们继承了peersim.reports.GraphObserver,这个模板类能简化对图的观察。
构造方法根据配置文件进行初始化,其中参数protocol引用了ProtocolID,它拥有“who knows who”的关系(它必须是一个Linkable协议),这是由超类继承而来。
其它的参数,coord_protocol和file_base,分别是坐标容器的协议名和将要使用的文件名前缀。这样,最终由程序生成的文件名是:file_base + %08d + .dat,中间的8位数字是指周期数,因为作为一个control对象,observer可以在每个周期中运行,在这种情况下每次应该生成一个不同的文件。
01
| package example.hot;
|
02
|
|
03
| import java.io.FileOutputStream;
|
04
| import java.io.IOException;
|
05
| import java.io.PrintStream;
|
06
| import peersim.config.Configuration;
|
07
| import peersim.core.Node;
|
08
| import peersim.graph.Graph;
|
09
| import peersim.reports.GraphObserver;
|
10
| import peersim.util.FileNameGenerator;
|
11
|
|
12
| public class InetObserver extends GraphObserver {
|
13
|
|
14
| private static final String PAR_FILENAME_BASE = "file_base";
|
15
| private static final String PAR_COORDINATES_PROT
|
16
| = "coord_protocol";
|
17
|
|
18
| private final String graph_filename;
|
19
| private final FileNameGenerator fng;
|
20
| private final int coordPid;
|
21
|
|
22
| public InetObserver(String prefix) {
|
23
| super(prefix);
|
24
| coordPid = Configuration.getPid(prefix + "."
|
25
| + PAR_COORDINATES_PROT);
|
26
| graph_filename = Configuration.getString(prefix + "."
|
27
| + PAR_FILENAME_BASE, "graph_dump");
|
28
| fng = new FileNameGenerator(graph_filename, ".dat");
|
31
| // Control interface method.
|
32
| public boolean execute() {
|
33
| try {
|
34
| updateGraph();
|
35
| System.out.print(name + ": ");
|
36
| // initialize output streams
|
37
| String fname = fng.nextCounterName();
|
38
| FileOutputStream fos = new FileOutputStream(fname);
|
39
| System.out.println("Writing to file " + fname);
|
40
| PrintStream pstr = new PrintStream(fos);
|
41
| // dump topology:
|
42
| graphToFile(g, pstr, coordPid);
|
43
| fos.close();
|
44
| } catch (IOException e) {
|
45
| throw new RuntimeException(e);
|
46
| }
|
49
|
|
50
| private static void graphToFile(Graph g, PrintStream ps,
|
51
| int coordPid) {
|
52
| for (int i = 1; i < g.size(); i++) {
|
53
| Node current = (Node) g.getNode(i);
|
54
| double x_to = ((InetCoordinates)
|
55
| current.getProtocol(coordPid)).getX();
|
56
| double y_to = ((InetCoordinates)
|
57
| \ current.getProtocol(coordPid)).getY();
|
58
|
|
59
| for (int index : g.getNeighbours(i)) {
|
60
| Node n = (Node) g.getNode(index);
|
61
| double x_from = ((InetCoordinates)
|
62
| n.getProtocol(coordPid)).getX();
|
63
| double y_from = ((InetCoordinates)
|
64
| n.getProtocol(coordPid)).getY();
|
65
| ps.println(x_from + " " + y_from);
|
66
| ps.println(x_to + " " + y_to);
|
在execute方法中我们必须调用updateGraph方法(a GraphObserver protected method)以检验实际的图中是否发生了变化,这是为了在很多observer运行于同一个图中的时候节省构造图的时间。如果许多observers观察同一个图的无向版本,那节省时间的时间是很显著的。
注意在execute方法中使用的IO库函数可能抛出一些异常,这里任意的异常都被捕获并重新作为运行时异常抛出,它们会导致仿真的终止。
静态的功能方法graphToFile将实际的拓扑结构写到磁盘中,对于每个节点n,收集其x和y坐标,而对于节点n的每个邻居节点i,其坐标将会是下面的格式:
1
| n.neighbor(i).x n.neighbor(i).y \newline
|
2
| n.x n.y \newline
|
这种格式很适合于用GnuPlot来绘图,请注意循环是从下标1而不是0开始的,这是因为节点0是root,它没有向外的连接。
5. 运行实验
下面是本实验所对应的配置文件:
01
| # Complex Network file:
|
02
| #random.seed 1234567890
|
03
| simulation.cycles 1
|
04
| network.size 10000
|
05
| protocol.link IdleProtocol
|
06
| protocol.coord example.hot.InetCoordinates
|
07
| init.0 example.hot.InetInitializer
|
08
| init.0.protocol coord
|
09
| init.1 example.hot.WireInetTopology
|
10
| init.1.protocol link #the linkable to be wired
|
11
| init.1.coord_protocol coord
|
12
| init.1.alpha 4
|
13
| control.io example.hot.InetObserver
|
14
| control.io.protocol link
|
15
| control.io.coord_protocol coord
|
16
| control.io.file_base graph
|
17
| control.degree DegreeStats
|
18
| control.degree.protocol link
|
19
| control.degree.undir
|
20
| control.degree.method freq
|
21
| include.control io degree
|
topology snapshot
topology in-degreedistribution
topology snapshot 2
topology in-degreedistribution 2
它根据init.0部分的参数生成了具有10000个节点的覆盖网络。下面的图展示了在a不同的情况下生成的拓扑。事实上,它影响了系统的聚类行为并且它与网络的大小紧密相关:
如果,拓扑将变得越来越聚集,在a很小时,则拓扑会变成星形结构。如果,拓扑将趋向于随机分布而不是聚集在一起。
DegreeStats可以用来收集节点度的统计信息,然而,应当慎重地使用它。因为在PeerSim的默认情况下,“度”是指“出度”,然而我们感兴趣的是“入度”,那怎么样才能观察入度呢?首先我们将图视为无向图(通过undir参数),然后我们进行频率统计(freq参数)来绘图,observer会输出类似于下面的数据:
topology snapshot 3
topology in-degreedistribution
第一列对应于度数,而第二列是指拥有相应度数的节点数量,我们可以肯定除了root以外,其它每个节点都只有一个out-link,同时所有的link都是严格单向的,因而为了取得入度我们只需要从第一列简单地减去1即可。
PeerSim中文教程(4):Event-driven 模型
1. 介绍
本教程使用Event-driven模型来演示一个简单的例子,仍然使用的是gossip-based平均数协议,对消息的发送将进行更细节的建模;通过与cycle-based模型的对比,可以发现本协议存在的问题。
在Event-based模型中,除了时间管理和control传递给protocols的方式以外,其它与cycle-based模型相同。不可执行的Protocols(只用于存储数据,比如只存储邻居节点的linkable协议,或存储数值的vectors)可以以同样的方式应用和初始化,在peersim.cdsim包之外的controls也都可以使用。在默认情况下,在cycle-based模型中,controls会的每个周期中调用 ,但在event-based模型中,它们需要进行明确的调度,因为事件驱动模型并不存在周期的概念。
显然,我们可以编写专用于event-based模型的controls,即可以给协议发送事件(消息)。在很多情况下,这是必要的,因为系统经常完全或部分地由外部事件如用户的查询来驱动,这能很好地用由生成这些事件,并且驱动仿真执行的controls进行建模。
有些组件是不可用的。例如依赖于静态类peersim.cdsim.CDState(它提供了读取cycle相关的全局状态的接口)的所有组件。我们的经验是,很多依赖于这个状态的 cycle-based组件可以经过简单的修改并删除这个依赖。
然而,可能有些令人吃惊的是,实现了cycle-based接口的peersim.cdsim.CDProtocol也可以使用于event-based模型,但是必须指出,在大部份的情况下这样做没有什么意义。然而,这个特性的有用之处在于:它让周期性地调用protocols变得很简单,这是一个几乎对所有与housekeeping,失效检测和sending heartbeat message有关的P2P协议来说典型的特性。
2. Protocol
下面以event-based模型来实现平均数协议:
01
| package example.edaggregation;
|
02
|
|
03
| import peersim.vector.SingleValueHolder;
|
04
| import peersim.config.*;
|
05
| import peersim.core.*;
|
06
| import peersim.transport.Transport;
|
07
| import peersim.cdsim.CDProtocol;
|
08
| import peersim.edsim.EDProtocol;
|
11
| * Event driven version of epidemic averaging.
|
12
| */
|
13
| public class AverageED extends SingleValueHolder
|
14
| implements CDProtocol, EDProtocol {
|
首先,这里同时实现了EDProtocol和CDProtocol接口,前者能让这个类能处理输入的消息,后者则可能令人困惑,因为它属于cycle-based模型的接口。但注意 event-based 的协议不是必须实现CDProtocol接口的,然而想要实现一个可以周期性取得control的协议时,可以通过实现CDProtocol接口,并在配置文件中设置一个CDScheduler 来实现。这样,代码就显得更清晰:以一个单独的组件来管理周期性的执行,而且能单独地进行配置。最后,我们还能简单地将 cycle-based 的协议移植到event-based 模型上。
1
| /**
|
2
| * @param prefix string prefix for config properties
|
3
| */
|
4
| public AverageED(String prefix) {
|
这个简单的协议并不读取任何配置参数,现在来关注cycle-based接口的实现,这个方法定义了周期性进行的行为。
01
| /**
|
02
| * This is the standard method the define periodic activity.
|
03
| * The frequency of execution of this method is defined by a
|
04
| * {@link peersim.edsim.CDScheduler} component in the configuration.
|
05
| */
|
06
| public void nextCycle( Node node, int pid ) {
|
07
| Linkable linkable =
|
08
| (Linkable) node.getProtocol( FastConfig.getLinkable(pid) );
|
09
| if (linkable.degree() > 0) {
|
10
| Node peern = linkable.getNeighbor(
|
11
| CommonState.r.nextInt(linkable.degree()));
|
12
| // XXX quick and dirty handling of failures
|
13
| // (message would be lost anyway, we save time)
|
14
| if(!peern.isUp())
|
15
| return;
|
16
| AverageED peer = (AverageED) peern.getProtocol(pid);
|
17
| ((Transport)node.getProtocol(FastConfig.getTransport(pid))).
|
18
| send(node,peern,new AverageMessage(value,node),pid);
|
在这里要观察的是event-based模型中的方法,即如何处理传输层。首先,FastConfig类让我们能访问为这个协议配置的传输层,通过使用这个传输层,我们可以在其它节点上将消息发送给协议。一条消息可以是任意的对象,由于这个仿真器并不是分布式的,所以不用处理序列化等等问题;而对象则会通过引用来存储。
目标协议是由目标节点peern定义的,协议的标识符则是pid,在这个例子中,我们在一个不同的节点发送消息给同一个协议。显然,目标协议应当实现EDProtocol接口。
01
| /**
|
02
| * This is the standard method to define to
|
03
| * process incoming messages.
|
04
| */
|
05
| public void processEvent( Node node, int pid, Object event ) {
|
06
| AverageMessage aem = (AverageMessage)event;
|
07
|
|
08
| if( aem.sender!=null )
|
09
| ((Transport)node.getProtocol(FastConfig.getTransport(pid))).
|
10
| send(node,aem.sender,
|
11
| new AverageMessage(value,null),pid);
|
12
|
|
13
| value = (value + aem.value) / 2;
|
14
| }
|
上面实现的是EDSimulator中的方法,它用于处理进入的消息。本例中,消息只有一种类型,我们只需要检查sender是否为null,因为如果为null,则我们无需应答消息(当然,实际上这已经是一种“应答”),而如果需要应答时则是通过传输层来处理。
01
| /**
|
02
| * The type of a message. It contains a value of type double
|
03
| * and the sender node of type {@link peersim.core.Node}.
|
04
| */
|
05
| class AverageMessage {
|
06
| final double value;
|
09
| /* this has to be answered, otherwise this is the answer. */
|
10
| final Node sender;
|
11
|
|
12
| public AverageMessage( double value, Node sender ) {
|
13
| this.value = value;
|
14
| this.sender = sender;
|
这个私有类是协议所使用的消息类型,说其私有是因为没有任何其它组件需要处理这种类型的消息。
3. 配置文件
配置文件与Cycle-based的配置相似,只有很少地方不同,但这些区别很重要。
01
| # network size
|
02
| SIZE 1000
|
03
| # parameters of periodic execution
|
04
| CYCLES 100
|
05
| CYCLE SIZE*10000
|
06
| # parameters of message transfer
|
07
| # delay values here are relative to cycle length, in percentage,
|
08
| # eg 50 means half the cycle length, 200 twice the cycle length, etc.
|
09
| MINDELAY 0
|
10
| MAXDELAY 0
|
11
| # drop is a probability, 0<=DROP<=1
|
12
| DROP 0
|
这里定义了一些常量以让配置文件更清晰,同时也更易于在命令行中修改,例如,CYCLE定义了一个周期的长度。
1
| random.seed 1234567890
|
2
| network.size SIZE
|
3
| simulation.endtime CYCLE*CYCLES
|
4
| simulation.logtime CYCLE
|
在这里,simulation.endtime 是最关键的参数,它通知仿真器何时终止。PeerSim用一个64位的long 长整型来表示时间,在启动时它为0,并且由消息的延迟来推进。在事件队列为空,或者队列中所有的事件的调度时间都迟于终止时间时,仿真将会终止。
仿真器将在标准错误窗口打印时间的进度,simulation.logtime 指定了打印这些消息的频率。
01
| ################### protocols ===========================
|
02
| protocol.link peersim.core.IdleProtocol
|
03
| protocol.avg example.edaggregation.AverageED
|
04
| protocol.avg.linkable link
|
05
| protocol.avg.step CYCLE
|
06
| protocol.avg.transport tr
|
07
| protocol.urt UniformRandomTransport
|
08
| protocol.urt.mindelay (CYCLE*MINDELAY)/100
|
09
| protocol.urt.maxdelay (CYCLE*MAXDELAY)/100
|
10
| protocol.tr UnreliableTransport
|
11
| protocol.tr.transport urt
|
12
| protocol.tr.drop DROP
|
在这里我们配置了协议(avg),指定了覆盖网络(link)和传输层(tr),同时也需要指定step参数,这与cycle-based模型是相似的。这是由于我们实现了cycle-based接口,所以我们需要指定一个周期的长度以使用它。
覆盖网络只是一个links的容器,在仿真过程中会保持不变,它会按如下初始化:
传输层也被视为一个协议进行配置的,它对随机延迟和消息丢失进行了建模。首先我们定义了一个拥有随机延迟(urt)的传输层,然后将它封装在一个通用的传输层包装器中,并以给定的概率tr来丢弃消息。传输层被定义在peersim.transport包中,和其它组件一样,它也是模块化的,用户可以简单地开发和使用一个新的传输层协议。
01
| ################### initialization ======================
|
02
| init.rndlink WireKOut
|
03
| init.rndlink.k 20
|
04
| init.rndlink.protocol link
|
05
| init.vals LinearDistribution
|
06
| init.vals.protocol avg
|
07
| init.vals.max SIZE
|
08
| init.vals.min 1
|
09
| init.sch CDScheduler
|
10
| init.sch.protocol avg
|
在这里唯一属于event-based模型范畴的是sch,它对cycle-based接口(nextCycle)的周期性调用进行调度,在这个配置中,这个组件首先会在0到CYCLE的时间内给所有的节点赋一个随机的点,这是在协议avg上第一次调用nextCycle。而下一次的调用将严格地发生在恰好CYCLE的时间间隔之后。更多之前(advanced)的方法也会存在,除此之外,这个调度可以通过类的扩展进行自定义,但在这里不作详述。
1
| ################ control ==============================
|
2
| control.0 SingleValueObserver
|
3
| control.0.protocol avg
|
4
| control.0.step CYCLE
|
注意和协议avg一样,我们也需要指定step参数,它指定了这个control多久会调用一次,否则controls能像cycle-based模型那样进行调度,只是没有默认的step,因为这里没有周期。
4. 运行协议
如果我们用上面的配置文件运行仿真,我们将在标准错误窗口中得到输出:
01
| Simulator: loading configuration
|
02
| ConfigProperties: File config-edexample.txt loaded.
|
03
| Simulator: starting experiment 0 invoking peersim.edsim.EDSimulator
|
04
| Random seed: 1234567890
|
05
| EDSimulator: resetting
|
06
| Network: no node defined, using GeneralNode
|
07
| EDSimulator: running initializers
|
08
| - Running initializer init.rndlink: class peersim.dynamics.WireKOut
|
09
| - Running initializer init.sch: class peersim.edsim.CDScheduler
|
10
| - Running initializer init.vals: class peersim.vector.LinearDistribution
|
11
| EDSimulator: loaded controls [control.0]
|
12
| Current time: 0
|
13
| Current time: 10000000
|
14
| Current time: 20000000
|
15
| Current time: 30000000
|
16
| Current time: 40000000
|
17
| Current time: 50000000
|
18
| .
|
21
| Current time: 980000000
|
22
| Current time: 990000000
|
23
| EDSimulator: queue is empty, quitting at time 999980413
|
标准输出窗口的输出如下 :
01
| control.0: 1.0 1000.0 1000 500.5 83416.66666666667 1 1
|
02
| control.0: 37.5 919.0 1000 500.5 25724.159091250687 1 1
|
03
| control.0: 206.7109375 767.890625 1000 500.5 8096.807036889389 1 1
|
04
| control.0: 352.373046875 695.453125 1000 500.5 2578.022573176135 1 1
|
05
| control.0: 412.430419921875 625.474609375 1000 500.5 801.1082179446831 1 1
|
06
| control.0: 436.43787479400635 570.459858417511 1000 500.5 243.53994072762902 1 1
|
07
| control.0: 470.7608990445733 527.0359845032217 1000 500.49999999999994 74.13788674564383 1 2
|
08
| control.0: 483.6040476858616 518.0301055684686 1000 500.49999999999903 23.428974301677556 1 1
|
09
| control.0: 490.5196089811798 512.0301471857779 1000 500.4999999999993 7.285566419597019 1 1
|
10
| control.0: 494.97216907397836 506.0375954180854 1000 500.4999999999999 2.1798299307442246 1 1
|
11
| control.0: 497.18190345272336 503.5837144460532 1000 500.5000000000001 0.6073148838336206 1 1
|
12
| control.0: 498.54320551492475 502.3533156558903 1000 500.5 0.1786794435445898 1 2
|
13
| control.0: 499.4023441821402 501.4962048486104 1000 500.49999999999966 0.055257607540637785 1 1
|
14
| control.0: 500.0032071191514 501.09832936709677 1000 500.4999999999995 0.017914865984002482 1 1
|
17
| .
|
18
| control.0: 500.5 500.5 1000 500.5 0.0 1000 1000
|
这些值分别代表最小值,最大值,样本的数量,平均数,方差,最小值实例的数量,最大值实例的数量。这个输出代表已经找到正确的平均数500.5,方差为 0,即所有的节点都拥有相同的正确的平均值。
这看上去很不错,我们可以添加一些延迟并观察一下会发生什么(在默认的配置文件中delay是0),如果在命令行中添加MINDELAY=10和MAXDELAY=10(即表示所有的消息都会恰好延迟10%的周期时间),我们将会得到:
3
| .
|
4
| control.0: 499.126081326076 499.126081326076 1000 499.12608132608807 0.0 1000 1000
|
5
| control.0: 499.126081326076 499.126081326076 1000 499.12608132608807 0.0 1000 1000
|
6
| control.0: 499.126081326076 499.126081326076 1000 499.12608132608807 0.0 1000 1000
|
也就是说,简单的延迟方案已经破坏了这个协议的良好属性:尽管是收敛的,但结果不正确,还可以验证不同的随机种子将会导致不同的结果,而改变延迟的和丢失率也对性能有影响。
那么,将延迟和丢失率保持为0能保证得到正确的行为吗?不一定。下面用更短的周期长度来进行实验,例如,CYCLE=SIZE,这意味着在同一个时间点一般有更多的事件调度发生,在这样的情况下,PeerSim以随机的顺序来执行这些事件,我们将得到:
3
| .
|
4
| control.0: 500.4835099381911 500.4835099381911 1000 500.48350993818605 7.807634196601234E-9 1000 1000
|
5
| control.0: 500.4835099381911 500.4835099381911 1000 500.48350993818605 7.807634196601234E-9 1000 1000
|
6
| control.0: 500.4835099381911 500.4835099381911 1000 500.48350993818605 7.807634196601234E-9 1000 1000
|
结论是什么?必须指出,这个错误的结果是由于时间分辨率(time resolution)不足导致的。如果消息的确是零延迟的,那么执行时间的微小差别也会导致不重叠的成对交换,很明显,在连续的时间上没有事件会在相同的时间点执行。然而,在消息传送时微小的随机延迟让结果变得很有意义,因为顺序的不确定性是确实存在的。
大体而言,使用event-based模型能看到很多在cycle-based模型中看不到的问题,然而,这也会引入一些假象。
5. 说明
这个例子仅是为了入门,推荐进一步学习相关文档,如peersim.edsim, peersim.transport等。