0%

[資料中心網路技術] Project3 在 Mininet 中建立拓樸並使用 SDN 控制器監視系統

資料中心網路技術 project3 在 Mininet 中建立拓樸並使用 SDN 控制器監視系統


交大資料中心網路技術 project 3,在這個 project 將會學到如何使用 Mininet 建立拓樸並使用 SDN controller 監視系統。

Demo日期: 4/21 16:00

Mininet 介紹


基本指令

  • $ 在 shell 中執行 Linux 指令

  • mininet> 在 Mininet’s CLI 中執行 Mininet 指令

  • # 在 root shell 中執行 Linux 指令

  • 顯示 Mininet 參數選項

    1
    $ sudo mn -h
  • 使用最小拓樸進入 Mininet CLI

    1
    $ sudo mn

    mininet 預設拓樸 mininal,包含一個 switch、一個 controller 和 兩個主機

  • 顯示 Mininet CLI commands

    1
    mininet> help
  • 顯示 nodes

    1
    mininet> nodes
  • 顯示鏈結

    1
    mininet> net
  • 印出所有 nodes 的資訊

    1
    mininet> dump
  • 對 host1 下 ifonfig -a 指令

    1
    mininet> h1 ifconfig -a
  • 測試兩個hosts的連線

    1
    mininet> h1 ping h2
  • 測試所有連線

    1
    pingall
  • 執行一簡單的 web server and client

    1
    2
    3
    4
    mininet> h1 python -m SimpleHTTPServer 80 &
    mininet> h2 wget -O - h1
    ...
    mininet> h1 kill %python
  • 離開 CLI

    1
    mininet> exit
  • CleanUp,若有 carsh 則執行

    1
    $ sudo mn -c

進階選項

  • --custom: 使用 custom topology
  • --topo: 使用在腳本中的字典 “topos” 的 “mytopo”
  • --link=tc: 使用 traffic control link
  • --controller: 使用外部 controller 控制
    1
    sudo mn --topo mytopo --custom ~/mininet/custom/xxxscript.py --controller remote --switch default,protocols=OpenFlow13 --link=tc

執行測試

  • 在 shell 執行 pingall
    1
    $ sudo mn --test pingair
  • 測試 h1/h2 間流量效能
    1
    $ sudo mn --test iperf

改變拓樸大小和型態

  • 使用 --topo 這個參數
    • single 是單個 switch 連接多台 host
    • linear 是一台 host 接一台 switch,switch 再串成線狀
      1
      2
      $ sudo mn --test pingall --topo single,3
      $ sudo mn --test pingall --topo linear,4
  • Mininet 2.0 後允許對 link 下參數
  • 假設將頻寬設為 10 Mps,每條 link 有 delay 10 ms,在測試中可見 RTT 約為 40 ms
    1
    2
    3
    $ sudo mn --link tc,bw=10,delay=10ms
    mininet> iperf
    mininet> h1 ping -c10 h2

步驟


大綱:

  • 在 mininet 中建立一個特定的拓樸網路系統
  • 修改 SDN controller simple_switch_13.py 程式碼
    • 使控制器可以監控交換器的流量
    • 並顯示交換器的 L2 address table
      Source Address Table

在 mininet 中建立拓樸

參考 mininet 中一簡單 script: ~/mininet/custom/topo-2sw-2host.py

建立拓樸指令

1
sudo mn --topo mytopo --custom ~/mininet/custom/yourscript.py --controller remote --switch default,protocols=OpenFlow13 -- link=tc
  • --custom: 使用 custom topology
  • --topo: 使用在腳本中的字典 “topos” 的 “mytopo”
  • --link=tc: 使用 traffic control link
  • --controller: 使用外部 controller 控制

~/mininet/custom/topo-2sw-2host.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from mininet.topo import Topo

class MyTopo( Topo ):
"Simple topology example."
def build( self ):
"Create custom topo."
# Add hosts and switches
leftHost = self.addHost( 'h1' )
rightHost = self.addHost( 'h2' )
leftSwitch = self.addSwitch( 's3' )
rightSwitch = self.addSwitch( 's4' )

# Add links
self.addLink( leftHost, leftSwitch )
self.addLink( leftSwitch, rightSwitch )
self.addLink( rightSwitch, rightHost )
topos = { 'mytopo': ( lambda: MyTopo() ) }

建立一拓樸

topology

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyTopo( Topo ):
"Simple topology example."
def build( self ):
"Create custom topo."
# Add 6 hosts
myHost = []
for h in range(6):
myHost.append(self.addHost('h%s' % (h+1)))
# Add 4 switches
mySwitch = []
for h in range(4):
mySwitch.append(self.addSwitch('s%s' % (h+1)))
# Add links
# bw: bandwidth of 10 Mbps
# loss: 10% packet loss rate
self.addLink(myHost[0], mySwitch[0], bw=100)
self.addLink(myHost[1], mySwitch[0], bw=100)
self.addLink(myHost[2], mySwitch[1], bw=100)
self.addLink(myHost[3], mySwitch[2], bw=100)
self.addLink(myHost[4], mySwitch[3], bw=100)
self.addLink(myHost[5], mySwitch[3], bw=100)
self.addLink(mySwitch[0], mySwitch[1], bw=1000, loss=5)
self.addLink(mySwitch[1], mySwitch[2], bw=1000, loss=5)
self.addLink(mySwitch[2], mySwitch[3], bw=1000, loss=5)
topos = { 'mytopo': ( lambda: MyTopo() ) }

修改 SDN controller code

  • 建立一 thread 來每五秒監視交換器流量
    參考 Chap.3 of Ryubook
  • 使用 OFPPortStatsRequest()OFPPortStatsReply() 來取得交換器埠的資訊
    參考 Chap.3 of Ryubook or the link below
  • 目標
    • 使用 OFPPortStatsRequest()、OFPPortStatsReply() 得到交換器 port 資訊
    • 監控資訊
      • Switch ID
      • TX 和 RX 封包資訊
      • 交換器 MAC address table
    • 每五秒將結果印出來
      monitor
    • 使用上面所規定的拓樸(4sw_6hosts)

實作一流量監視器

網路是許多服務和業務的基礎設施,因此必須保持正常穩定運行。當網路發生錯誤時,必須查明原因並迅速恢復運行。為了檢測錯誤並找出原因,有必要定期了解網路狀態。

例如,假設某個網路設備的某個 port 的流量指示一個非常高的值,如果沒有連續測量該 port 的流量,則無法確定是異常狀態還是通常是這種狀態以及何時變為這種狀態,出於這個原因,持續監控網路的健康狀況對於使用該網次的服務或企業的持續和安全運行至關重要。當然,簡單地監控流量資訊並不能提供完美的保證,但本節將介紹如何使用 OpenFlow 獲取交換器的統計信息。

Fixed-Cycle Processing

在交換集線器處理的同時,創建一個 thread 定期向 OpenFlow 交換器發出請求以得到統計資訊。

1
2
3
4
5
6
class SimpleMonitor13(simple_switch_13.SimpleSwitch13):
def __init__(self, *args, **kwargs):
super(SimpleMonitor13, self).__init__(*args, **kwargs)
self.datapaths = {}
self.monitor_thread = hub.spawn(self._monitor)
# ...

ryu.lib.hub 中有一些 eventlet wrappers 和基本類別實現。在此使用 hub.spawn(),用於建立 thread。實際上建立的 thread 是一個 eventlet green thread。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ...
@set_ev_cls(ofp_event.EventOFPStateChange, [MAIN_DISPATCHER, DEAD_DISPATCHER])
def _state_change_handler(self, ev):
datapath = ev.datapath
if ev.state == MAIN_DISPATCHER:
if datapath.id not in self.datapaths:
self.logger.debug('register datapath: %016x', datapath.id)
self.datapaths[datapath.id] = datapath
elif ev.state == DEAD_DISPATCHER:
if datapath.id in self.datapaths:
self.logger.debug('unregister datapath: %016x', datapath.id)
del self.datapaths[datapath.id]
# 每 10 秒對交換器發出統計訊息的請求
def _monitor(self):
while True:
for dp in self.datapaths.values():
self._request_stats(dp)
hub.sleep(10)
# ...

為了確保連接的交換器被監控,EventOFPStateChange 事件用於檢測連接和斷開連接。此事件由 Ryu 框架發出,在 Datapath 狀態更改時發出。
因此當 Datapath 狀態變為 MAIN_DISPATCHER 時,該交換器被註冊為監視目標,當它變為 DEAD_DISPATCHER 時,該註冊被刪除。

1
2
3
4
5
6
7
8
9
10
def _request_stats(self, datapath):
self.logger.debug('send stats request: %016x', datapath.id)
ofproto = datapath.ofproto
parser = datapath.ofproto_parser

req = parser.OFPFlowStatsRequest(datapath)
datapath.send_msg(req)

req = parser.OFPPortStatsRequest(datapath, 0, ofproto.OFPP_ANY)
datapath.send_msg(req)

透過定期呼叫 _request_stats(),向交換器發出 OFPFlowStatsRequestOFPPortStatsRequest

  • OFPFlowStatsRequest 請求交換器提供與 flow entry 相關的統計資訊。請求的目標 flow entry 可以通過 table ID、output port、cookie 值和 match 等條件來縮小範圍,但這裡所有 entry 都受請求的約束。
  • OFPPortStatsRequest 請求交換器提供 port 相關的統計資訊。可以指定所需的 port number 以從中獲取資訊。在這裡,OFPP_ANY 被指定為從所有 port 請求資訊。

FlowStats

為了從交換器接收回應,要建立一個接收 FlowStatsReply 消息的事件處理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@set_ev_cls(ofp_event.EventOFPFlowStatsReply, MAIN_DISPATCHER)
def _flow_stats_reply_handler(self, ev):
body = ev.msg.body
self.logger.info('datapath '
'in-port eth-dst '
'out-port packets bytes')
self.logger.info('---------------- '
'-------- ----------------- '
'-------- -------- --------')
for stat in sorted([flow for flow in body if flow.priority == 1],
key=lambda flow: (flow.match['in_port'],
flow.match['eth_dst'])):
self.logger.info('%016x %8x %17s %8x %8d %8d',
ev.msg.datapath.id,
stat.match['in_port'], stat.match['eth_dst'],
stat.instructions[0].actions[0].port,
stat.packet_count, stat.byte_count)

OPFFlowStatsReply 類別的屬性 bodyOFPFlowStats 的列表,存儲了每個 flow entry 的統計資訊,受 FlowStatsRequest 的約束。

除了優先權為 0 的 Table-miss flow 外,所有 flow entry 都被選中。按照接收到的 port 和目的 MAC 位址排序,輸出與各個 flow entry match 的封包數和位元組數。

此處,只將部分值輸出到日誌中,但為了持續收集和分析資訊,可能需要與外部程序連動。在這種情況下,可以將 OFPFlowStatsReply 的內容轉換為 JSON 格式。

如下:

1
2
3
4
import json
# ...
self.logger.info('%s', json.dumps(ev.msg.to_jsondict(),
ensure_ascii=True, indent=3, sort_keys=True))

PortStats

為了從交換器接收回應,要建立一個接收 PortStatsReply 消息的事件處理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER)
def _port_stats_reply_handler(self, ev):
body = ev.msg.body
self.logger.info('datapath port '
'rx-pkts rx-bytes rx-error '
'tx-pkts tx-bytes tx-error')
self.logger.info('---------------- -------- '
'-------- -------- -------- '
'-------- -------- --------')
for stat in sorted(body, key=attrgetter('port_no')):
self.logger.info('%016x %8x %8d %8d %8d %8d %8d %8d',
ev.msg.datapath.id, stat.port_no,
stat.rx_packets, stat.rx_bytes, stat.rx_errors,
stat.tx_packets, stat.tx_bytes, stat.tx_errors)

OPFPortStatsReply 類別的屬性 body 是 OFPPortStats 的列表。

OFPPortStats 分別儲存 port number、發送/接收封包 count、byte count、drop count、error count、frame error count、overrun count、CRC error count 和 collision count 等統計資訊。

這裡,按照 port number 排序,輸出接收封包計數、接收字元組計數、接收錯誤計數、發送封包計數、發送字元組計數和發送錯誤計數。

實作監視器,修改 simple_monitor_13.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class SwitchMonitor13(simple_switch_13.SimpleSwitch13):

def __init__(self, *args, **kwargs):
super(SwitchMonitor13, self).__init__(*args, **kwargs)
self.datapaths = {}
self.monitor_thread = hub.spawn(self._monitor)

@set_ev_cls(ofp_event.EventOFPStateChange,
[MAIN_DISPATCHER, DEAD_DISPATCHER])
def _state_change_handler(self, ev):
datapath = ev.datapath
if ev.state == MAIN_DISPATCHER:
if datapath.id not in self.datapaths:
self.logger.debug('register datapath: %016x', datapath.id)
self.datapaths[datapath.id] = datapath
elif ev.state == DEAD_DISPATCHER:
if datapath.id in self.datapaths:
self.logger.debug('unregister datapath: %016x', datapath.id)
del self.datapaths[datapath.id]

def _monitor(self):
count = 0
while True:
for dp in self.datapaths.values():
self._request_stats(dp)
# Print the address table of all switches every 5 seconds
# Count print times

self.logger.info(f'Print the address table of all switches {count} times')
count = count + 1
hub.sleep(5)

def _request_stats(self, datapath):
self.logger.debug('send stats request: %016x', datapath.id)
ofproto = datapath.ofproto
parser = datapath.ofproto_parser

# get the switch port information
req_port = parser.OFPPortStatsRequest(datapath, 0, ofproto.OFPP_ANY)
datapath.send_msg(req_port)

# Switch MAC Address Table
req_flow = parser.OFPFlowStatsRequest(datapath)
datapath.send_msg(req_flow)

@set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER)
def _port_stats_reply_handler(self, ev):
body = ev.msg.body

# Print Switch ID
self.logger.info('Switch ID: %d', ev.msg.datapath.id)
# TX and RX packets information of each port in a switch
self.logger.info('Port No. Tx-Bytes Rx-Bytes')
self.logger.info('-------- -------- --------')
for stat in sorted(body, key=attrgetter('port_no')):
self.logger.info('%8x %8d %8d', stat.port_no, stat.tx_bytes, stat.rx_bytes)

@set_ev_cls(ofp_event.EventOFPFlowStatsReply, MAIN_DISPATCHER)
def _flow_stats_reply_handler(self, ev):
body = ev.msg.body

# Switch MAC Address Table
self.logger.info('MAC Address Table Port No.')
self.logger.info('----------------- --------')
for stat in sorted([flow for flow in body if flow.priority == 1],
key=lambda flow: (flow.match['in_port'],
flow.match['eth_dst'])):
self.logger.info('%17s %8x', stat.match['eth_dst'], stat.match['in_port'])
self.logger.info('***************************\n')

結果


為了方便執行,使用 makefile 更加快速,相關教學可以參考連結

1
2
3
all:
xterm -e "ryu-manager switchMonitor13.py" &
sudo mn --topo mytopo --custom ./topo-4sw-6host.py --link tc

利用 mininet 開啟一拓樸

1
$ sudo mn --topo mytopo --custom ./topo-4sw-6host.py --link tc

topo-4sw-6host

利用 ryu 進行控制

1
$ ryu-manager switchMonitor13.py

switchMonitor13