基于SDN网络的负载均衡研究与实现

为什么需要软件定义网络

1.网络缺乏可扩展性,创新正在停滞不前。

  我们最新的研究发现,几乎每两个组织中就有一个认为需要将网络功能扩展为采用SDN的主要业务触发因素,而不是其他催化剂。这一统计数据一点都不令人惊讶,我们的客户需要一个足够灵活的网络来支持业务,因为每个功能都试图对不断变化的市场条件做出更快速的响应。
  这一挑战与行业无关:在几乎所有可以想象到的行业中,企业都在尝试支持越来越多的应用程序和设备,因为它们在其产品和服务中添加了新的性能。网络容量和复杂性往往会阻碍这种发展,至少会延迟企业的创新能力。
  SDN为此问题提供了潜在的解决方案。它提供了一种从中心管理网络功能的方法,实现从单个设备(而不是逐个设备)跨多个设备对应用程序的更改。随着组织需求的发展,这大大减少了扩展所需的时间。

2.由于缺乏速度,市场机会正在丧失

  任何IT战略都必须以其目标业务成果为基础,即应该支持竞争优势。在机会窗口越来越短暂的市场中,如果企业无法快速创新,这种优势将会丧失。
  这是采用SDN的关键驱动因素。SDN是一种集中的,基于策略的IT资产管理方式,这意味着企业可以更快地进行创新。每个新的应用程序都可以从中心推出,设备可以通过与控制器的链接和已经设置的新策略自动配置自自己。
  在一个顾客的需求必须得到满足,但变化迅速且不可预测的世界里,SDN可以弥补我们日益看到的“快速失败”心理差距。新产品和服务到达目标市场的速度要更快,并且可以随时更新或更换。

3.公司希望快速创新

  公司还告诉我们,拥有敏捷性和灵活性来改善跨业务的服务是至关重要的。因此,SDN部署的速度被视为另一个SDN被采用的驱动力。使每个业务部门更快地独立至关重要。在我们合作的许多业务中,听到不同的部门都在尝试对相互独立的创新,但却发现它们的it基础设施不允许它们以期望的速度前进,这种情况并不少见。对于需要在工作之外访问按需服务的业务用户来说,这是令人沮丧的。
  在此背景下,SDN的出现进一步鼓励了组织的创新能力。这种创新能力体现在它能够在多大程度上试验和推出新的计划,无论是内部还是面向客户。SDN为网络复杂性提供了实用的解决方案,否则将威胁到实验和转型。

4.安全问题阻碍了创造力

  在一个组织从未如此意识到网络安全和威胁程度不断提高的世界中,对重大漏洞或失败的恐惧会抑制创新。企业担心移动太快或与新合作伙伴合作会使他们面临更多漏洞。可以理解的是,他们的反应是关注弹性,但这往往会损害改善跨业务的服务敏捷性。
  SDN可以在技术和实践方面增强企业安全性。一方面,承载加密流量的全封闭网络本质上比企业的传统网络解决方案更安全。另一方面,SDN为组织提供了在用户的虚拟环境中构建现有应用程序安全性的机会。
这意味着企业能够更好地管理其It弹性,同时满足它们对创新的迫切追求。

5.效率对于长期创新至关重要

  如果在这个转型的新世界中,快速失败是许多组织的一个重要原则,那么失败也是廉价的。当他们尝试新的应用程序和试用新产品和服务时,面对昂贵且繁琐的IT基础设施,企业将很快不堪重负。
  SDN在中心进行管理,无需为应用程序的每次新迭代重新配置单个设备,这可能具有巨大的价值。但更长远的机遇可能是将采用SDN作为向网络转型迈进的一部分,因为企业级虚拟化将为未来五年及以后的挑战提供一个精益高效的组织。

负载均衡在新兴网络环境下的改变

  在复杂多变的网络环境下保证网络服务的稳定性和效率,是负载均衡机制解决的一个重要问题,由于传统网络架构自身存在的缺点,负载均衡很难有大的突破,随着新型网络体系SDN的提出,可以从另一种思路出发,为负载均衡机制的改进提出新的突破,本文通过在以OpenFlow为代表的SDN架构下实施负载均衡策略,以期提高网络性能。

负载均衡常用算法

  软件负载均衡是指使用软件的方式来分发和均衡流量。软件负载均衡,分为7层协议和4层协议。网络协议有七层,基于第四层传输层来做流量分发的方案称为4层负载均衡,例如LVS,而基于第七层应用层来做流量分发的称为7层负载均衡,例如Nginx。
  这两种在性能和灵活性上是有些区别的。基于4层的负载均衡性能要高一些,一般能达到 几十万/秒的处理量,而基于7层的负载均衡处理量一般只在几万/秒。基于软件的负载均衡的特点也很明显,便宜。在正常的服务器上部署即可,无需额外采购,就是投入一点技术去优化优化即可,因此这种方式是互联网公司中用得最多的一种方式。SDN的负载均衡自然也属于软件负载均衡的范畴。

1.随机算法

  Random随机,按权重设置随机概率。在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

2.轮询及加权轮询

  轮询(RoundRobbin)当服务器群中各服务器的处理能力相同时,且每笔业务处理量差异不大时,最适合使用这种算法。轮循,按公约后的权重设置轮循比率。存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。加权轮询(Weighted Round Robbin)为轮询中的每台服务器附加一定权重的算法。比如服务器1权重1,服务器2权重2,服务器3权重3,则顺序为1-2-2-3-3-3-1-2-2-3-3-3- ......

3.最小连接及加权最小连接

  最少连接(LeastConnections)在多个服务器中,与处理连接数(会话数)最少的服务器进行通信的算法。即使在每台服务器处理能力各不相同,每笔业务处理量也不相同的情况下,也能够在一定程度上降低服务器的负载。
加权最少连接(WeightedLeastConnection)为最少连接算法中的每台服务器附加权重的算法,该算法事先为每台服务器分配处理连接的数量,并将客户端请求转至连接数最少的服务器上。

4.哈希算法

  一致性Hash,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

5.IP地址散列

  通过管理发送方IP和目的地IP地址的散列,将来自同一发送方的分组(或发送至同一目的地的分组)统一转发到相同服务器的算法。当客户端有一系列业务需要处理而必须和一个服务器反复通信时,该算法能够以流(会话)为单位,保证来自相同客户端的通信能够一直在同一服务器中进行处理。

6.URL散列

  通过管理客户端请求URL信息的散列,将发送至相同URL的请求转发至同一服务器的算法。

解决方案

  在多个用户并发访问台服务器的时候,服务器可能会出现性能下降甚至宕机的情况。为解决此种情况,我们组提出的方案是将用户的访问流量分担不同的服务器上,也就是负载均衡的实现。目前传统网络的负载均衡存在硬件设备高成本和架构难的特点。因此我们的方案是用软件定义网络(SDN)来实现网络流量的负载均衡。在独立的SDN控制器POX控制器,通过python脚本实现与部署该方案。

一、负载均衡架构

  SDN的负载均衡的实现架构部署为三层,分别为数据层、控制层、应用层。POX控制器用Restful API实现南向接口连接控制层与应用层,北向接口连接至Mininet软件的拓扑网络。主机以GET方法请求POX控制器的内容,POX控制器监测网络数据并下发流表到各个交换机,交换机按照流标进行数据传输,选择不同的服务器,完成流量的负载均衡,增大吞吐量,减少服务端的压力。如图所示为三层架构。

二、策略算法

ip_loadbalancer.py

  官方随机算法实现的一种负载均衡代码如下

展开查看

#Copyright 2013,2014 James McCauley
#
#Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.

"""
A very sloppy IP load balancer.

Run it with --ip= --servers=IP1,IP2,...

By default, it will do load balancing on the first switch that connects. If
you want, you can add --dpid= to specify a particular switch.

Please submit improvements. 😃
"""

from pox.core import core
import pox
log = core.getLogger("iplb")

from pox.lib.packet.ethernet import ethernet, ETHER_BROADCAST
from pox.lib.packet.ipv4 import ipv4
from pox.lib.packet.arp import arp
from pox.lib.addresses import IPAddr, EthAddr
from pox.lib.util import str_to_bool, dpid_to_str, str_to_dpid

import pox.openflow.libopenflow_01 as of

import time
import random

FLOW_IDLE_TIMEOUT = 10
FLOW_MEMORY_TIMEOUT = 60 * 5

class MemoryEntry (object):
"""
Record for flows we are balancing

Table entries in the switch "remember" flows for a period of time, but
rather than set their expirations to some long value (potentially leading
to lots of rules for dead connections), we let them expire from the
switch relatively quickly and remember them here in the controller for
longer.

Another tactic would be to increase the timeouts on the switch and use
the Nicira extension which can match packets with FIN set to remove them
when the connection closes.
"""
def init (self, server, first_packet, client_port):
self.server = server
self.first_packet = first_packet
self.client_port = client_port
self.refresh()

def refresh (self):
self.timeout = time.time() + FLOW_MEMORY_TIMEOUT

@property
def is_expired (self):
return time.time() > self.timeout

@property
def key1 (self):
ethp = self.first_packet
ipp = ethp.find('ipv4')
tcpp = ethp.find('tcp')

return ipp.srcip,ipp.dstip,tcpp.srcport,tcpp.dstport

@property
def key2 (self):
ethp = self.first_packet
ipp = ethp.find('ipv4')
tcpp = ethp.find('tcp')

return self.server,ipp.srcip,tcpp.dstport,tcpp.srcport

class iplb (object):
"""
A simple IP load balancer

Give it a service_ip and a list of server IP addresses. New TCP flows
to service_ip will be randomly redirected to one of the servers.

We probe the servers to see if they're alive by sending them ARPs.
"""
def init (self, connection, service_ip, servers = []):
self.service_ip = IPAddr(service_ip)
self.servers = [IPAddr(a) for a in servers]
self.con = connection
self.mac = self.con.eth_addr
self.live_servers = {} # IP -> MAC,port

try:
  self.log = log.getChild(dpid_to_str(self.con.dpid))
except:
  # Be nice to Python 2.6 (ugh)
  self.log = log

self.outstanding_probes = {} # IP -> expire_time

# How quickly do we probe?
self.probe_cycle_time = 5

# How long do we wait for an ARP reply before we consider a server dead?
self.arp_timeout = 3

# We remember where we directed flows so that if they start up again,
# we can send them to the same server if it's still up.  Alternate
# approach: hashing.
self.memory = {} # (srcip,dstip,srcport,dstport) -> MemoryEntry

self._do_probe() # Kick off the probing

# As part of a gross hack, we now do this from elsewhere
#self.con.addListeners(self)

def _do_expire (self):
"""
Expire probes and "memorized" flows

Each of these should only have a limited lifetime.
"""
t = time.time()

# Expire probes
for ip,expire_at in self.outstanding_probes.items():
  if t > expire_at:
    self.outstanding_probes.pop(ip, None)
    if ip in self.live_servers:
      self.log.warn("Server %s down", ip)
      del self.live_servers[ip]

# Expire old flows
c = len(self.memory)
self.memory = {k:v for k,v in self.memory.items()
               if not v.is_expired}
if len(self.memory) != c:
  self.log.debug("Expired %i flows", c-len(self.memory))

def _do_probe (self):
"""
Send an ARP to a server to see if it's still up
"""
self._do_expire()

server = self.servers.pop(0)
self.servers.append(server)

r = arp()
r.hwtype = r.HW_TYPE_ETHERNET
r.prototype = r.PROTO_TYPE_IP
r.opcode = r.REQUEST
r.hwdst = ETHER_BROADCAST
r.protodst = server
r.hwsrc = self.mac
r.protosrc = self.service_ip
e = ethernet(type=ethernet.ARP_TYPE, src=self.mac,
             dst=ETHER_BROADCAST)
e.set_payload(r)
#self.log.debug("ARPing for %s", server)
msg = of.ofp_packet_out()
msg.data = e.pack()
msg.actions.append(of.ofp_action_output(port = of.OFPP_FLOOD))
msg.in_port = of.OFPP_NONE
self.con.send(msg)

self.outstanding_probes[server] = time.time() + self.arp_timeout

core.callDelayed(self._probe_wait_time, self._do_probe)

@property
def _probe_wait_time (self):
"""
Time to wait between probes
"""
r = self.probe_cycle_time / float(len(self.servers))
r = max(.25, r) # Cap it at four per second
return r

def _pick_server (self, key, inport):
"""
Pick a server for a (hopefully) new connection
"""
return random.choice(self.live_servers.keys())

def _handle_PacketIn (self, event):
inport = event.port
packet = event.parsed

def drop ():
  if event.ofp.buffer_id is not None:
    # Kill the buffer
    msg = of.ofp_packet_out(data = event.ofp)
    self.con.send(msg)
  return None

tcpp = packet.find('tcp')
if not tcpp:
  arpp = packet.find('arp')
  if arpp:
    # Handle replies to our server-liveness probes
    if arpp.opcode == arpp.REPLY:
      if arpp.protosrc in self.outstanding_probes:
        # A server is (still?) up; cool.
        del self.outstanding_probes[arpp.protosrc]
        if (self.live_servers.get(arpp.protosrc, (None,None))
            == (arpp.hwsrc,inport)):
          # Ah, nothing new here.
          pass
        else:
          # Ooh, new server.
          self.live_servers[arpp.protosrc] = arpp.hwsrc,inport
          self.log.info("Server %s up", arpp.protosrc)
    return

  # Not TCP and not ARP.  Don't know what to do with this.  Drop it.
  return drop()

# It's TCP.

ipp = packet.find('ipv4')

if ipp.srcip in self.servers:
  # It's FROM one of our balanced servers.
  # Rewrite it BACK to the client

  key = ipp.srcip,ipp.dstip,tcpp.srcport,tcpp.dstport
  entry = self.memory.get(key)

  if entry is None:
    # We either didn't install it, or we forgot about it.
    self.log.debug("No client for %s", key)
    return drop()

  # Refresh time timeout and reinstall.
  entry.refresh()

  #self.log.debug("Install reverse flow for %s", key)

  # Install reverse table entry
  mac,port = self.live_servers[entry.server]

  actions = []
  actions.append(of.ofp_action_dl_addr.set_src(self.mac))
  actions.append(of.ofp_action_nw_addr.set_src(self.service_ip))
  actions.append(of.ofp_action_output(port = entry.client_port))
  match = of.ofp_match.from_packet(packet, inport)

  msg = of.ofp_flow_mod(command=of.OFPFC_ADD,
                        idle_timeout=FLOW_IDLE_TIMEOUT,
                        hard_timeout=of.OFP_FLOW_PERMANENT,
                        data=event.ofp,
                        actions=actions,
                        match=match)
  self.con.send(msg)

elif ipp.dstip == self.service_ip:
  # Ah, it's for our service IP and needs to be load balanced

  # Do we already know this flow?
  key = ipp.srcip,ipp.dstip,tcpp.srcport,tcpp.dstport
  entry = self.memory.get(key)
  if entry is None or entry.server not in self.live_servers:
    # Don't know it (hopefully it's new!)
    if len(self.live_servers) == 0:
      self.log.warn("No servers!")
      return drop()

    # Pick a server for this flow
    server = self._pick_server(key, inport)
    self.log.debug("Directing traffic to %s", server)
    entry = MemoryEntry(server, packet, inport)
    self.memory[entry.key1] = entry
    self.memory[entry.key2] = entry

  # Update timestamp
  entry.refresh()

  # Set up table entry towards selected server
  mac,port = self.live_servers[entry.server]

  actions = []
  actions.append(of.ofp_action_dl_addr.set_dst(mac))
  actions.append(of.ofp_action_nw_addr.set_dst(entry.server))
  actions.append(of.ofp_action_output(port = port))
  match = of.ofp_match.from_packet(packet, inport)

  msg = of.ofp_flow_mod(command=of.OFPFC_ADD,
                        idle_timeout=FLOW_IDLE_TIMEOUT,
                        hard_timeout=of.OFP_FLOW_PERMANENT,
                        data=event.ofp,
                        actions=actions,
                        match=match)
  self.con.send(msg)

Remember which DPID we're operating on (first one to connect)

_dpid = None

def launch (ip, servers, dpid = None):
global _dpid
if dpid is not None:
_dpid = str_to_dpid(dpid)

servers = servers.replace(","," ").split()
servers = [IPAddr(x) for x in servers]
ip = IPAddr(ip)

We only want to enable ARP Responder only on the load balancer switch,

so we do some disgusting hackery and then boot it up.

from proto.arp_responder import ARPResponder
old_pi = ARPResponder._handle_PacketIn
def new_pi (self, event):
if event.dpid == _dpid:
#Yes, the packet-in is on the right switch
return old_pi(self, event)
ARPResponder._handle_PacketIn = new_pi

Hackery done. Now start it.

from proto.arp_responder import launch as arp_launch
arp_launch(eat_packets=False,**{str(ip):True})
import logging
logging.getLogger("proto.arp_responder").setLevel(logging.WARN)

def _handle_ConnectionUp (event):
global _dpid
if _dpid is None:
_dpid = event.dpid

if _dpid != event.dpid:
  log.warn("Ignoring switch %s", event.connection)
else:
  if not core.hasComponent('iplb'):
    # Need to initialize first...
    core.registerNew(iplb, event.connection, IPAddr(ip), servers)
    log.info("IP Load Balancer Ready.")
  log.info("Load Balancing on %s", event.connection)

  # Gross hack
  core.iplb.con = event.connection
  event.connection.addListeners(core.iplb)

core.openflow.addListenerByName("ConnectionUp", _handle_ConnectionUp)

解决方案

  本次方案是在SDNHub_tutorial_VM_64(当然你也可以在乌班图上进行)系统上实现的。控制器使用POX,虚拟拓扑的搭建使用Mininet,服务器使用python创建简易的HTTP服务器。进入系统后,通过”sudo mn –topo single,6 –controller=remote,port=6633”创建一个简单的拓扑图,其中6633指POX控制器的端口号。
step1.创建拓扑

  实验拓扑如图所示,由六台主机地址为10.0.0.x(1-6)和交换机组成,POX控制器连接交换机。

step2.打开服务器
  通过xterm[host]打开主机h1和h2,h1和h2作为服务器实验中对服务器的要求不是那么高,所以服务器选用python的server模块中的SimpleHTTPServer作为HTTP服务器来响应请求包,HTTP服务器端口设置为80。

step3.控制器与负载均衡策略
  sudo ./pox.py log.level –DEBUG misc.ip_loadbalanced行POX控制器并同时打开了ip_loadbalancer,ip_loadbalancer主要负载均衡的策略实现,POX控制器用于处理流量。当出现“IP LOAD BALANCER READY”和“Server up”表示运行成功,负载均衡和HTTP服务器已开启。

step4.发送请求
  打开其他的主机,作为发送请求的主机,通过curl指令,对服务器server1和server2发起METHOD为GET的请求,发送一个Request packet。请求成功,服务器会回送一个网页信息。

  同时使用多台主机,重复请求控制器多次,观察POX的流量路径走向。分析流量,可以观察到多台主机请求控制器,最终请求到的服务器不一样,流量的走向也不一样。
在服务器端,h1和h2上可以对收到的包进行拆包处理,在GET请求的这些过程中,h1和h2并行工作,并且每次同样的请求不会在同一台服务器模拟上进行处理。

step5.抓包测试
  为了确保实验结果的偶然性,我们重复进行多次测试,且使用wireshark进行抓包分析。.这次不进行大量重复发包实验,而是使用阶段性发包处理,第一次发送Request packet包后,间隔一段时间后再次发送一个包给控制器。如下为第一次和第二结果的抓包结果,可以看到第一次处理的服务器为h2,第二次处理的服务器为h1。
第一次:
第二次:

实验视频(英语好的朋友可以尝试跟着做一遍)>youtube

posted @ 2019-07-26 16:14  二进制的弗洛伊德  阅读(4944)  评论(1编辑  收藏  举报