SDN多控制器资源池的设计与实现
1.功能介绍
SDN多控制器资源池的设计与实现,顾名思义,就是SDN控制器集群,这里使用Opendaylight控制器进行实现。主要功能如下:
- 将多个SDN控制器组成一个资源池,池中每个控制器处于负荷分担状态,通过选举出来的主节点向其他节点同步状态和动态数据。
- 设计故障检测和故障接管策略,其中任何一个控制器故障,资源池中其他控制器均可无损接管。
- 设计负载均衡策略,对控制器的负载进行动态平衡。
1.1开发注意
Opendaylight控制器本身带有集群功能,所以很大一部分工作只需要进行配置,本文仅用于对自己设计进行记载,如果有相同题目开发的人建议换用其他控制器,虽然自己设计代码变多,但是其他控制器本身带有Opendaylight所不具有的接口。更多的原因在于你需要阅读Opendaylight的源码。
PS:Opendaylight控制器版本为Carbon-0.6.4
2.开发流程
2.1使用Docker搭建Opendaylight集群
2.2.1 Dockerfile文件内容
使用ubuntu18作为镜像,dockerfile文件如下:
FROM ubuntu:18.04
RUN apt-get update &&\
apt-get install --assume-yes apt-utils &&\
apt-get install unzip -y &&\
apt-get install net-tools -y &&\
apt-get install iputils-ping -y &&\
apt-get install vim -y &&\
apt-get autoclean &&\
apt-get autoremove
COPY distribution-karaf-0.6.4-Carbon.tar.gz jdk-8u321-linux-x64.tar.gz /home/
WORKDIR /home
RUN tar -zxvf jdk-8u321-linux-x64.tar.gz &&\
rm jdk-8u321-linux-x64.tar.gz &&\
tar -zxvf distribution-karaf-0.6.4-Carbon.tar.gz &&\
rm distribution-karaf-0.6.4-Carbon.tar.gz
ENV JAVA_HOME=/home/jdk1.8.0_321
ENV JRE_HOME=$JAVA_HOME/jre
ENV CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
ENV PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
EXPOSE 8181 6653 6633
然后在dockerfile所在文件夹目录下之执行
sudo docker build -t opendaylight .
2.2.1 启动容器搭建集群
打开终端,输入
sudo docker run -it --name node1 -p 8181 -p 6653 -p 6633 opendaylight /bin/bash
这样就启动了一个Opendaylight控制器节点node1
本文一共启动了三个节点
sudo docker run -it --name node2 -p 8181 -p 6653 -p 6633 opendaylight /bin/bash
sudo docker run -it --name node3 -p 8181 -p 6653 -p 6633 opendaylight /bin/bash
这三个容器的目录切换到/home/distribution-karaf-0.6.4-Carbon/bin下,运行其中的configure_cluster.sh脚本。
node1
./configure_cluster.sh 1 172.17.0.2 172.17.0.3 172.17.0.4
node2
./configure_cluster.sh 2 172.17.0.2 172.17.0.3 172.17.0.4
node3
./configure_cluster.sh 3 172.17.0.2 172.17.0.3 172.17.0.4
这样就完成了集群的配置,然后就可以开启控制器了,在三个容器中通过./karaf开启控制器,开启之后在三个控制器上安装如下feature
feature:install odl-restconf odl-dluxapps-applications odl-openflowplugin-flow-services-ui odl-l2switch-switch-ui odl-mdsal-apidocs
feature:install odl-mdsal-clustering
以上集群搭建完成,具体可以参考以下链接使用Docker搭建ODL集群
2.2集群功能测试分析
经过上面的简单配置,一个多控制器集群就已经完成,这个集群就已经实现了节点的状态和数据一致,以及故障接管功能。详细测试就略过。
2.2.1 Raft一致性
Raft一致性问题主要是解决节点的状态和数据一致问题,Raft算法原理自行百度吧。简单来说,Raft一致性算法可以分为:选举,日志复制,安全性。
集群中的节点可以分为三种状态。Leader(领导者)、Follower(跟随者)、Candidate(候选人);当集群运行时,只有两种状态,领导者和跟随者。跟随者:当系统运行时,所有节点都处于跟随者模式,响应领导者注册同步请求、候选请求,并将跟随者请求的事件转发给领导者。当集群开始时,一个节点从跟随者模式切换到候选模式,开始选举,当超过一半的节点同意选举领导者时,从候选模式切换到领导者模式。
基本程序如下:一开始,每个追随者都可以成为候选人,并向其他追随者发送选举要求:如果候选人获得绝对半数以上的选票,他就成为新的领导者,并可以负责管理日志。领导者从上一层的客户端接收事件请求并将其复制到追随者,然后负责通知集群中的追随者提交日志。领导者节点负责同步日志文件。集群中的其他节点在领导者节点失败或崩溃时重新启动选举。具体实现的参见源码。源码下载在后面专门讲,关于Raft的代码在/controller-release-carbon-sr4(这个根据自己下载情况)/opendaylight/md-sal/sal-akka-raft文件夹下。
2.2.2 故障接管
故障接管需要借助OpenFlow1.3协议,OpenFlow1.3协议支持一个交换机连接多个控制器,每个控制器具有自己的角色(Role),master,slave,equal。equal一般是交换机刚连接到控制器分配的角色。master,对交换机具有写权限,slave,对交换机只有读权限。
但是,OpenFlow并不负责如何确定谁是master控制器,谁是slave控制器。所以还是需要控制器来提供一种机制来为交换机设备在多个控制器集群节点中选举出master角色。这个机制就是EntityOwnershipService(EOS)。下面对该机制进行简单介绍。首先介绍一下需要用到的概念,entity:实体,整个控制器集群中,可以被多个应用共享的东西。比如一台交换机。Owner:一种角色,即在整个控制器集群中选举出拥有Entity所有权的控制器节点。Candidate:候选者,指所有竞争某个Entity的Owner选举的控制器节点所拥有的初始角色状态。
(PS:这个Candidate和Raft一致性中Candidate并没有关系)
总体流程是用户在注册Candidate时,Entity以及Candidate信息会被写入数据分片,写入成功后,在分片的Leader节点上按照配置的EntityOwnerSelectionStrategy进行Owner的选举,选举出来的Owner也写入该分片,同时根据分片的数据变更通知,生成DOMEntityOwnershipChange的消息通知到注册的EntityOwnershipListener。
Opendaylight提供了两种选举实现策略。选举策略在/opendaylgiht/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/entityownership/selectionstrategy中可以找到。一种是FirstCandidateSelectionStrateg方案。该方案从候选列表的选择第一个可行的候选。代码如下:
/**
* The FirstCandidateSelectionStrategy always selects the first viable candidate from the list of candidates.
*/
public class FirstCandidateSelectionStrategy extends AbstractEntityOwnerSelectionStrategy {
public static final FirstCandidateSelectionStrategy INSTANCE =
new FirstCandidateSelectionStrategy(0L, Collections.emptyMap());
public FirstCandidateSelectionStrategy(long selectionDelayInMillis, Map<String, Long> initialStatistics) {
super(selectionDelayInMillis, initialStatistics);
}
@Override
public String newOwner(String currentOwner, Collection<String> viableCandidates) {
Preconditions.checkArgument(viableCandidates.size() > 0, "No viable candidates provided");
return viableCandidates.iterator().next();
}
}
另外一种方案是LeastLoadedCandidateSelectionStrategy。方案将实体的所有权分配给拥有最少实体的候选人。默认是第一候选人。也可以自定义实现方案。
/**
* The LeastLoadedCandidateSelectionStrategy assigns ownership for an entity to the candidate which owns the least
* number of entities.
*/
public class LeastLoadedCandidateSelectionStrategy extends AbstractEntityOwnerSelectionStrategy {
private final Map<String, Long> localStatistics = new HashMap<>();
protected LeastLoadedCandidateSelectionStrategy(long selectionDelayInMillis, Map<String, Long> initialStatistics) {
super(selectionDelayInMillis, initialStatistics);
localStatistics.putAll(initialStatistics);
}
@Override
public String newOwner(String currentOwner, Collection<String> viableCandidates) {
Preconditions.checkArgument(viableCandidates.size() > 0);
String leastLoadedCandidate = null;
long leastLoadedCount = Long.MAX_VALUE;
if (!Strings.isNullOrEmpty(currentOwner)) {
long localVal = MoreObjects.firstNonNull(localStatistics.get(currentOwner), 0L);
localStatistics.put(currentOwner, localVal - 1);
}
for (String candidateName : viableCandidates) {
long val = MoreObjects.firstNonNull(localStatistics.get(candidateName), 0L);
if (val < leastLoadedCount) {
leastLoadedCount = val;
leastLoadedCandidate = candidateName;
}
}
if (leastLoadedCandidate == null) {
leastLoadedCandidate = viableCandidates.iterator().next();
}
localStatistics.put(leastLoadedCandidate, leastLoadedCount + 1);
return leastLoadedCandidate;
}
@VisibleForTesting
Map<String, Long> getLocalStatistics() {
return localStatistics;
}
}
在etc/org.opendaylight.controller.cluster.datastore.entity.owner.selection.strategies.cfg
目录下修改配置策略。
entity.type.openflow=org.opendaylight.controller.cluster.datastore.entityownership.selectionstrategy.LeastLoadedCandidateSelectionStrategy
2.3负载均衡处理
由于多控制器组成集群,不可避免的就会造成负载不均衡的情况,某些控制器的负载过高,而某些控制器的负载却比较低。所以对整个控制器集群进行负载均衡是很有必要的。在本文中负载均衡主要采用交换机迁移技术来实现。同样,在本文中采用两种方式进行负载均衡。首先在外部应用层面,网络管理员可以根据节点的信息以及运维的经验来人为的进行交换机的迁移。其次,在控制器内部,交换机发送的Packet-in消息会需要控制器进行处理,Packet-in消息的速率在很大程度上可以代表着控制器的负载情况。
2.3.1 命令行执行交换机迁移
以交换机s1为例,将s1迁移到172.17.0.3控制器上,代码如下:
sudo ovs-vsctl set-controller s1 tcp:172.17.0.3:6633
管理系统使用java写的,在系统中调用命令行来执行,其他语言自行百度。
public String loadbalance(String switchname ,String controlerip) {
String cmd="ovs-vsctl set-controller "+switchname+" tcp:"+controlerip+":6653";
String sudoCmd="echo \"123456\" | sudo -S ";
String[] cmds={"/bin/bash","-c",sudoCmd+cmd};
// String cmd=switchname+" "+controlerip;
try {
Process process = Runtime.getRuntime().exec(cmds);
process.waitFor();
BufferedReader br=new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line=br.readLine())!=null){
System.out.println(line);
}
int exitValue = process.exitValue();
if (exitValue == 0) {
System.out.println("执行成功");
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "success";
}
2.3.2 控制器内部进行交换机迁移
本文中,控制器通过计算接收到Packet-in消息的速率来作为负载是否失衡的标准。
// 计数器加1
counter = counter + 1;
// 计算平均packet in速率
// 收到samplesLwm个数据包以后
if ((counter % samplesLwm) == 0) {
// 获取calendar
calendar = Calendar.getInstance();
// 获取当前时间
newTime = calendar.getTimeInMillis();
// 计算时间差
timeDiff = newTime - oldTime;
// 将oldTime时间更新
oldTime = newTime;
// 收到个数据包的平均速率,单位包/秒
avgPacketInRate = (samplesLwm / timeDiff) * 1000;
counter = 0;
LOG.info("Average PacketIn Rate is " + avgPacketInRate);
}
当速率超过规定的阈值时,则采用OFpRole角色变更方法,将该交换机的master控制器变更角色为slave,并发出通知。控制器会上文提到的的选举策略重新选举新的master控制器,以此来实现负载均衡的目的。
final OfpRole newRole;
newRole = OfpRole.BECOMESLAVE;
BigInteger switchkey=new BigInteger(ingressNode);
SwitchSessionKeyOF key=util.createSwitchSessionKey(switchkey);
SessionContext context =instance.getSessionContext(key);
RolePushTask task = new RolePushTask(newRole, context);
ListenableFuture<Boolean> rolePushResult = pool.submit(task);
3 Notice
Opendaylight控制器内部具有Packet-in消息限制机制。
final class PacketInRateLimiter extends SimpleRatelimiter {
private static final Logger LOG = LoggerFactory.getLogger(PacketInRateLimiter.class);
private final float rejectedDrainFactor;
private final ConnectionAdapter connectionAdapter;
private final MessageSpy messageSpy;
PacketInRateLimiter(final ConnectionAdapter connectionAdapter, final int lowWatermark, final int highWatermark, final MessageSpy messageSpy, float rejectedDrainFactor) {
super(lowWatermark, highWatermark);
Preconditions.checkArgument(rejectedDrainFactor > 0 && rejectedDrainFactor < 1);
this.rejectedDrainFactor = rejectedDrainFactor;
this.connectionAdapter = Preconditions.checkNotNull(connectionAdapter);
this.messageSpy = Preconditions.checkNotNull(messageSpy);
}
@Override
protected void disableFlow() {
messageSpy.spyMessage(DeviceContext.class, MessageSpy.STATISTIC_GROUP.OFJ_BACKPRESSURE_ON);
connectionAdapter.setPacketInFiltering(true);
LOG.debug("PacketIn filtering on: {}", connectionAdapter.getRemoteAddress());
}
@Override
protected void enableFlow() {
messageSpy.spyMessage(DeviceContext.class, MessageSpy.STATISTIC_GROUP.OFJ_BACKPRESSURE_OFF);
connectionAdapter.setPacketInFiltering(false);
LOG.debug("PacketIn filtering off: {}", connectionAdapter.getRemoteAddress());
}
public void LowWaterMdrainark() {
adaptLowWaterMarkAndDisableFlow((int) (getOccupiedPermits() * rejectedDrainFactor));
}
}
abstract class SimpleRatelimiter {
private final AtomicInteger counter = new AtomicInteger();
private int lowWatermark;
private int lowWatermarkEffective;
private int highWatermark;
@GuardedBy("counter")
private volatile boolean limited;
SimpleRatelimiter(final int lowWatermark, final int highWatermark) {
Preconditions.checkArgument(lowWatermark >= 0);
Preconditions.checkArgument(highWatermark >= 0);
Preconditions.checkArgument(lowWatermark <= highWatermark);
this.lowWatermark = lowWatermark;
this.highWatermark = highWatermark;
lowWatermarkEffective = lowWatermark;
}
protected final boolean isLimited() {
return limited;
}
protected abstract void disableFlow();
protected abstract void enableFlow();
boolean acquirePermit() {
final int cnt = counter.incrementAndGet();
if (cnt > highWatermark) {
synchronized (counter) {
final int recheck = counter.decrementAndGet();
if (recheck >= highWatermark && !limited) {
disableFlow();
limited = true;
}
}
return false;
}
return true;
}
void releasePermit() {
final int cnt = counter.decrementAndGet();
if (cnt <= lowWatermarkEffective) {
synchronized (counter) {
final int recheck = counter.get();
if (recheck <= lowWatermarkEffective && limited) {
enableFlow();
limited = false;
resetLowWaterMark();
}
}
}
}
void resetLowWaterMark() {
synchronized (counter) {
lowWatermarkEffective = lowWatermark;
}
}
void adaptLowWaterMarkAndDisableFlow(int temporaryLowWaterMark) {
if (temporaryLowWaterMark < highWatermark) {
synchronized (counter) {
lowWatermarkEffective = temporaryLowWaterMark;
if (!limited) {
disableFlow();
limited = true;
}
}
}
}
int getOccupiedPermits() {
return counter.get();
}
void changeWaterMarks(final int newLowWatermark, final int newHighWatermark) {
synchronized (counter) {
lowWatermark = newLowWatermark;
highWatermark = newHighWatermark;
resetLowWaterMark();
}
}
}
4 源码开发
4.1ODL碳版本模块开发
1.安装java1.8以上环境,安装maven。
2.配置maven settings.xml 。
首先在odl的git中访问odl-parent项目,进入项目可以看到settings.xml,把这个项目拷贝到自己maven的.m2文件夹下。(注意自己拷贝的版本和要开发的版本要保持一致)
mvn archetype:generate -DarchetypeGroupId=org.opendaylight.controller -DarchetypeArtifactId=opendaylight-startup-archetype -DarchetypeVersion=1.3.0-Carbon
然后mvn install就可以
mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true -Denforcer.skip=true
mvn idea:idea
详细参考ODL碳版本模块开发
4.2源码开发
可以使用git或者直接GitHub下载源码,然后mvn install即可。