02月22, 2017

neutron host routes调整记录和代码分析

前两天的线上问题排查的时候,整体检查了一次线上的一些基本进程的状态,和一些基本的dnsmasq配置等,发现以前的subnet host route配置有些冗余,所以对host route进行了调整,并且分析了相关的代码。

如图:

alt

比如,该vm中插入了两条路由,其实只有第一条路由是生效的,第二条是没用的。这样的静态路由是在我们建subnet的时候指定插入的主机路由。

alt

这样的设计,是有问题的:

  1. 有冗余路由,但是该路由不生效,没意义。
  2. 我们的环境中dhcp 是HA的,有两台dhcp服务器,这里给指定这样的路由的话,如果当前其中一个dhcp服务down了,该vm仍然可以通过另一台获得ip地址和dns等信息,但是169.254.169.254的路由指向了第一台dhcp,所以该vm是拿不到metadata的, 所以这是有隐患的。

代码分析

  1. 每个dnsmasq对应一个network
  2. 每生成一个network,会spawn一个dnsmasq
  3. network下如果有多个subnet,每个subnet的子网范围对应的是不同的dhcp-range
  4. dhcp-range对应的不同的tag,tag0和tag1等等

不过在我们的环境中

  1. 每个网络节点接入一个交换机
  2. 每个交换机下只有一个network
  3. 每个network下也只有一个subnet

所以每个网络点上只有一个Dnsmasq进程,该进程的dhcp-range下也只有一个tag0,该tag0的ip范围是就是我们的subnet ip范围。

我们这里的Dnsmasq ip分配方式是static,根据mac地址静态分配ip的,host文件中维护了mac-ip的对应表。 在我们的网络节点上的dnsmasq加载如下

alt

所以我们从最开始一步一步深入分析,在建立一个network之后,是Dnsmasq是怎么加载对应的conf,然后生成相应的各种files,例如:hostsfile,addn-hosts,optsfile,dhcp-leasefile等。

首先我们建立一个network,然后再建subnet,然后subnet开启dhcp,这样才会开启一个Dnsmasq进程来给该network提供dhcp服务,而不同的subnet对应着Dnsmasq中的不同tag。

查看以下代码文件

 /usr/lib/python2.7/site-packages/neutron/agent/linux/dhcp.py
class DhcpLocalProcess(DhcpBase):
    def enable(self):
        """Enables DHCP for this network by spawning a local process."""
        if self.active:
            self.restart()
        elif self._enable_dhcp():
            utils.ensure_dir(self.network_conf_dir)
            interface_name = self.device_manager.setup(self.network)
            self.interface_name = interface_name
            self.spawn_process()

代码中是由dhcp模块下的Dnsmasq类来spawn Dnsmasq进程的,Dnsmasq进程继承自DhcpLocalProcess,当我们在subnet中开启dhcp的时候,其实是调用了DhcpLocalProcess类的enable方法.

class Dnsmasq(DhcpLocalProcess):
    def spawn_process(self):
        self._output_init_lease_file()                             ##这里lease file只是在Dnsmasq进程第一次start的时候生成一次。
        self._spawn_or_reload_process(reload_with_HUP=False)       ##然后开始第一次spawn Dnsmasq进程或者reload该进程,第一次肯定是spawn,后续对该subnet的一些参数修改也会rpc到dhcp agent,直接reload新配置就行
                                                                     这里默认进程不接受hup信号,就是不接受给Dnsmasq发送hup信号,让Dnsmasq自动reload,Dnsmasq只接受代码控制reload

然后在subnet中开启了dhcp之后,核心的方法就是开始spawn Dnsmasq进程了。

最后开始执行_spawn_or_reload_process方法,该方法首先会生成进程所需的各种config文件,例如 hostsfile,addn-hosts,optsfile,dhcp-leasefile。

这里介绍一下这些files:

  • hostfile非常重要是维护mac-ip表的,新的mac-ip关系都会写到该host文件中,然后vm boot的时候会根据mac获取到相应的static ip。
  • addn-hosts 这里主要是维护的ip和hostname的对应关系
  • optsfile 也是非常重要的,该文件中根据不同的subnet,生成了相应的tag,其中包含了,没个subnet所需的cidr,静态路由,网关,dns等
tag:tag0,option:dns-server,114.114.114.114,218.30.118.6                                  ###dns服务器
tag:tag0,option:classless-static-route,169.254.169.254/32,10.138.166.194,0.0.0.0/0,10.138.166.193  ##静态路由,这里包含了metadata的路由,和默认路由
tag:tag0,249,169.254.169.254/32,10.138.166.194,0.0.0.0/0,10.138.166.193
tag:tag0,option:router,10.138.166.193                                                   ##网关地址

dhcp-leasefie 包含了每个mac ip地址对应的hostname,和相应的租期,我们环境中的租期为了防止续租的时候Dnsmasq挂掉,并且24小时仍然没有恢复,造成的ip丢失问题,统一都设置成了-1,无限租期,因为我们的ip和vm是永久绑定的。

在上述几个文件中,我们重点关注的是optsfile,因为我们的问题主要是关注代码如何自动生成metadata静态路由。

接下来我们需要主要看看进程的孵化:

def _spawn_or_reload_process(self, reload_with_HUP):
    self._output_config_files()                               ##这里会生成Dnsmasq所需的所有files文件,比如我们的hostsfile,addn-hosts,optsfile,dhcp-leasefile等文件
    pm = self._get_process_manager(                           ##这里通过加载生成的config文件,回调_build_cmdline_callback方法,开始通过cmd方式生成Dnsmasq进程
        cmd_callback=self._build_cmdline_callback)
    pm.enable(reload_cfg=reload_with_HUP)                   
    self.process_monitor.register(uuid=self.network.id,                 ##给Dnsmasq进程加监控
                                  service_name=DNSMASQ_SERVICE_NAME,
                                  monitored_process=pm)

optsfile是由_output_config_files方法生成的。所以我们需要细看一下该方法

def _output_config_files(self):
    self._output_hosts_file()                                 ##生成hosts文件
    self._output_addn_hosts_file()                            ##生成addn_hosts_file文件
    self._output_opts_file()                                  ##生成opts_file文件

这里我们主要细看一下 _output_opts_file 方法,该方法主要是先针对每个subnet生成相应的opts参数,然后再针对每个port生成相应的opts参数,最后进行相应的格式替换,生成标准的opts_file

def _output_opts_file(self):
    """Write a dnsmasq compatible options file."""
    options, subnet_index_map = self._generate_opts_per_subnet()           ##生成每个subnet对应的opts参数
    options += self._generate_opts_per_port(subnet_index_map)
    name = self.get_conf_file_name('opts')
    utils.replace_file(name, '\n'.join(options))
    return name

这里我们主要的步骤方法是_generate_opts_per_subnet,下面我们细看一下该方法的具体实现过程:

def _generate_opts_per_subnet(self):
    options = []
    subnet_index_map = {}
    if self.conf.enable_isolated_metadata:                                                    ##如果dhcp_agent.ini中打开了enable_isolated_metadata = true,则先返回(subnet.id,第一个可用ip)
        subnet_to_interface_ip = self._make_subnet_interface_ip_map()                     
    isolated_subnets = self.get_isolated_subnets(self.network)                                ##返回network下的isolated_subnets,意思就是没有任何port绑定的subnet,一般新建的subnet都是属于isolated_subnets
    for i, subnet in enumerate(self.network.subnets):                                         ##开始enumerate每个subnets
        if (not subnet.enable_dhcp or                                                         ##这里我们不考虑,我们环境中没有使用ipv6
            (subnet.ip_version == 6 and
             getattr(subnet, 'ipv6_address_mode', None)
             in [None, constants.IPV6_SLAAC])):
            continue
        if subnet.dns_nameservers:                                                            ##如果我们新建的subnet中有指定dnsnameservers,则需要将dns-server加入optsfile
            options.append(                                                                     我们的环境中每个subnet都指定了固定的dns。例如:218.30.118.6等
                self._format_option(
                    subnet.ip_version, i, 'dns-server',
                    ','.join(
                        Dnsmasq._convert_to_literal_addrs(
                            subnet.ip_version, subnet.dns_nameservers))))
        else:                                                                                 ##如果不在subnet中指定dns,则默认使用Dnsmasq的ip,让Dnsmasq提供dns查询
            # use the dnsmasq ip as nameservers only if there is no
            # dns-server submitted by the server
            subnet_index_map[subnet.id] = i
        if self.conf.dhcp_domain and subnet.ip_version == 6:                                  ##这里同样不考虑。
            options.append('tag:tag%s,option6:domain-search,%s' %
                           (i, ''.join(self.conf.dhcp_domain)))

        gateway = subnet.gateway_ip                                                           ##返回gw ip地址
        host_routes = []                                                                      ##这里是我们要关注的,要开始生成host_routes
        for hr in subnet.host_routes:                                                         ##如果我们新建的subnet中添加了指定了固定的host_routes,开始遍历subnet中的host_routes,
            if hr.destination == constants.IPv4_ANY:                                            然后将subnet中指定的host_routes按固定格式,放到host_routes list中,用于最后的生成optsfile用
                if not gateway:
                    gateway = hr.nexthop
            else:
                host_routes.append("%s,%s" % (hr.destination, hr.nexthop))
        # Add host routes for isolated network segments
        if (isolated_subnets[subnet.id] and                                                   ##这里如果是isolated_subnets(新建的subnet都属于isolated_subnets),并且dhcp_agent.ini配置文件中开启了
                self.conf.enable_isolated_metadata and                                          metadata服务,并且是ipv4,这里是我们的关注的重点,因为我们的环境中开启了dhcp_agnet对metadata服务的支持,
                subnet.ip_version == 4):                                                       
            subnet_dhcp_ip = subnet_to_interface_ip[subnet.id]                                  然后返回该subnet中的dhcp ip,一般都是该subnet中除gw ip外的第一个可用ip,也是Dnsmasq监听的tap口的ip地址
            host_routes.append(                                                                 然后生成静态主机路由,(169.254.169.254/32,subnet_dhcp_ip)
                '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip)                             
            )
        if subnet.ip_version == 4:                                                           
            host_routes.extend(["%s,0.0.0.0" % (s.cidr) for s in                                ##生成默认路由
                                self.network.subnets
                                if (s.ip_version == 4 and
                                    s.cidr != subnet.cidr)])

            if host_routes:                                                                   
                if gateway:                                                                     ##如果有gw,则生成 0.0.0.010.138.166.193(gw ip)的默认路由
                    host_routes.append("%s,%s" % (constants.IPv4_ANY,
                                                  gateway))
                options.append(                                                                   然后将前面添加到host_routes列表中的静态路由,添加到opts_files的classless-static-route部分下
                    self._format_option(subnet.ip_version, i,                                     例如:classless-static-route,169.254.169.254/32,10.138.166.194,0.0.0.0/0,10.138.166.193
                                        'classless-static-route',
                                        ','.join(host_routes)))
                options.append(
                    self._format_option(subnet.ip_version, i,
                                        WIN2k3_STATIC_DNS,
                                        ','.join(host_routes)))
            if gateway:                                                                           ##如果subnet中指定了gw,则生成如右边的路由 tag:tag0,option:router,10.138.166.193
                options.append(self._format_option(subnet.ip_version,
                                                   i, 'router',
                                                   gateway))
            else:                                                                                 ##如果subnet中没有指定gw,则生成如右边的路由 tag:tag0,option:router
                options.append(self._format_option(subnet.ip_version,
                                                   i, 'router'))
    return options, subnet_index_map

所以在我们的代码中已经做了这样的工作,在生成一个新的subnet的时候,如果开启了dhcp,并且我们的环境中开启了enable_isolated_metadata = True,让dhcp_agent来代理metadata服务.

这样我们的host_route中会默认添加一条关于获取metadata服务的静态路由,下一跳地址为我们的Dnsmasq监听的tap口的ip地址。

所以如果vm启动的时候,发送dhcp request 广播,在收到ip地址,dns,gw等参数的同时,也会生成metadata的静态路由,路由next hop就是我们的dhcp 服务器的ip地址。

这样看来,指定host_route意义不大,需要把线上的的所有subnet中指定的metadata路由都去掉,使用默认的就行。

解决办法

使用系统默认的建网方式,不指定加主机路由。

alt

不指定主机路由的时候,如果我们设置了在 /etc/neutron/dhcp_agent.ini 开启了代理metadata服务,这样在qdhcp 中的tap口 ip 就是就是metadata proxy监听的ip地址

alt

并且代码中在启动dnsmasq的时候,判断参数如果enable_isolated_metadata = True开启的话,就会自动加一条静态路由,该路由为 alt

该路由的下一跳就是dnsmasq监听的tap口ip地址,这样设计的好处是,当vm启动的时候,会发送dhcp request广播,dhcp服务器reply该request,然后vm在获得ip地址,gw和dns等必备信息之后,同时会被插入一条 静态路由 169.254.169.254到dhcp server,这样vm在 curl 169.254.169.254的时候,请求只会发送到该dhcp server。

这样vm获得的主机路由下一跳地址,都是能正常响应该dhcp request广播的dhcp 服务器的ip地址。

本文链接:https://www.opsdev.cn/post/neutron-host-routes-research.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。