Openstack配置文件管理的变迁之路

在管理一个Openstack集群时,如何维护配置文件无疑是其中最艰难和繁琐的任务之一。因为你不仅要面对众多的核心服务(nova,keystone,glance,cinder,etc)的配置文件,还需要管理其相关服务的配置文件(mysql,rabbitmq,bind9,etc)。此外,Openstack基于组件式的设计架构,将某些功能或是后端驱动抽象为一个个单独的plugin或是pipeline中的一个filter,用户可以根据自己的需求来选择适合自己的架构和技术栈,只需要通过修改配置文件就可以完成架构的变化。

随手举一些例子:

  •  选择使用nova-network还是neutron来构建虚拟网络?
  •  glance的后端使用本地存储还是swift或是s3?
  •  cinder的后端使用ceph还是sheepdog?
  •  选择template还是sql作为Keystone catalog的driver?
  •  选择kvm还是xen作为nova-compute的虚拟化后端?
  •  如果要使用nova的live migration功能需要做哪些配置?
  •  如果要使用nova的cold migration功能需要做哪些配置?
  •  如何把原先neutron all-in-one角色拆分为Api和network角色?
  •  修改了nova.conf中的quota参数,应该通知哪些服务重启?
  •  Swift proxy-server要加载哪些filter到pipeline中?
  •  Desingate的后端是选择bind还是powerdns?
  •  如何使Keystone向MQ发送消息使得ceilometer可以监听?
  •  .......

 

从上面罗列的一些简单提问中,我们可以发现,架构和后端驱动的变更其实都是在和配置文件打交道。那么摆在眼前的一个迫切需求就是如何将Openstack各服务的配置文件有效灵活地管理起来:

  1. 仅nova服务就多达近千个配置选项,那么各个服务大量的配置选项如何进行管理?
  2. 如何方便地添加自定义选项?
  3. 当我添加一个新功能时,需要修改多个服务的配置文件,如何关联?
  4. 如何确保配置文件修改后,相应服务被恰当地重启?
  5. ...

Openstack社区中的puppet-openstack项目致力于Openstack的自动化部署,并被广泛地用于业界,如Redhat的Packstack, Mriantis的FuelWeb, UnitedStack的UOS, Cisco的内部云等诸多项目均用到了puppet-openstack的核心模块。从2012年项目伊始,我就参与到其中的开发,在经历了两年多的频繁迭代,最近我们在配置文件的管理上有了不错的进展,最新增加的特性使得配置文件的管理变得更为灵活。

OK,下面开始介绍我们在Openstack配置文件管理所经历的变更以及用到的技术。

 

模板(template)统治一切

     在最早期的时候,我们使用了简单有效的工具:模板(template)。例如,我们希望管理nova.conf中的libvirt_type选项,那么在nova模块的templates文件夹下新建一个nova.conf.erb文件,使用ERB语法在里面添加以下内容:

[default]
libvirt_type = <%= @libvirt_type %>

   然后在nova模块的init.pp文件中使用file resource来管理nova.conf。它看起来是这样的:

class nova(
  $libvirt_type = 'kvm',
){
  .....
  file { "nova.conf":
        path    => "/etc/nova/nova.conf",
        owner   => 'root',
        group   => 'root',
        mode    => '0644',
        content => template('nova/nova.conf.erb'),
      }

}

     

这种方法在需要配置选项较少的情况下,还是不错的。但对于处在快速迭代开发中的openstack各核心项目来说,几乎每天都会有选项的新增和删改的变动,那么我们总不能每天都紧盯着Openstack社区的与配置文件变更相关的patch,如果有变更然后就提交一个对应变更的模板patch吧。因此,这种方法带来最麻烦的问题是:不灵活。

 

拼接(concat)取而代之

    部署过openstack的同学会发现,openstack的配置文件是标准的INI格式,每个配置文件由多个section组成。

例如,在swift的proxy-server.conf中有[default],[pipeline:main],[app:proxy-server]等等。其中,[pipeline:main]中的pipeline选项,当这个filter出现在这个pipeline中,下面才会有对应这个filter的section。举个例子:

    pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync  proxy-logging proxy-server

     当需要在pipeline选项中添加bulk时,同时也要在配置文件中添加[filter:bulk]一节。当然,我们可以在proxy-server.conf.erb中,使用if逻辑把这些section都添加上,但这种做法不利于根据代码逻辑进行类的分离,例如,我从来不用bulk这个filter,那么它为什么要出现在proxy-server.conf.erb模板中呢?

[filter:bulk]
use = egg:swift#bulk
# max_containers_per_extraction = 10000
# max_failed_extractions = 1000
# max_deletes_per_request = 10000
# max_failed_deletes = 1000

# In order to keep a connection active during a potentially long bulk request,
# Swift may return whitespace prepended to the actual response body. This
# whitespace will be yielded no more than every yield_frequency seconds.
# yield_frequency = 10

# Note: The following parameter is used during a bulk delete of objects and
# their container. This would frequently fail because it is very likely
# that all replicated objects have not been deleted by the time the middleware got a
# successful response. It can be configured the number of retries. And the
# number of seconds to wait between each retry will be 1.5**retry

# delete_container_retry_count = 0

# Note: Put after auth in the pipeline.

  为何不把这些section拆成一个个的配置文件片段,我想使用哪些filter,只需要在pipeline中指定,自动地帮我把这些配置文件片段拼接出来?

  concat模块(https://github.com/puppetlabs/puppetlabs-concat.git)正是为此而来。继续以swift-proxy为例,我们希望在pipeline中使用healthcheck,cache,tempauth,proxy-server作为默认值,那么只需要在class swift::proxy中定义:

class swift::proxy(
  $proxy_local_net_ip,
  $port = '8080',
  $pipeline = ['healthcheck', 'cache', 'tempauth', 'proxy-server'],
  .....
  }

然后在templates下分别定义它们的erb模板:

healthcheck.conf.erb 对应 swift::proxy::healthcheck

[filter:healthcheck] use = egg:swift#healthcheck

cache.conf.erb 对应  swift::proxy::cache

[filter:cache]
use = egg:swift#memcache
memcache_servers = <%= [@memcache_servers].flatten.join(',') %>

tempauth.conf.erb 对应  swift::proxy::tempauth

[filter:tempauth]
use = egg:swift#tempauth
user_admin_admin = admin .admin .reseller_admin
user_test_tester = testing .admin
user_test2_tester2 = testing2 .admin
user_test_tester3 = testing3

最后是proxy-server.conf.erb 对应 swift::proxy

# This file is managed by puppet.  Do not edit
#
[DEFAULT]
bind_port = <%= @port %>
<% if @proxy_local_net_ip %>
bind_ip = <%= @proxy_local_net_ip %>
<% end %>
workers = <%= @workers %>
user = swift
log_name = swift
log_facility = <%= @log_facility %>
log_level = <%= @log_level %>
log_headers = <%= @log_headers %>
log_address = <%= @log_address %>
<% if @log_udp_host != '' -%>
# If set, log_udp_host will override log_address
log_udp_host = <%= @log_udp_host -%>
<% end %>
<% if @log_udp_host !='' and @log_udp_port != '' -%>
log_udp_port = <%= @log_udp_port -%>
<% end %>

[pipeline:main]
pipeline = <%= @pipeline.to_a.join(' ') %>

[app:proxy-server]
use = egg:swift#proxy
set log_name = proxy-server
set log_facility = <%= @log_facility %>
set log_level = <%= @log_level %>
set log_address = <%= @log_address %>
log_handoffs = <%= @log_handoffs %>
allow_account_management = <%= @allow_account_management %>
account_autocreate = <%= @account_autocreate %>

 

那么如何把这些模板拼接成一个完整的proxy-server.conf文件呢?

首先在swift::server中声明一个concat resource,指定要拼接的目的文件的读写权限和所有者属性:

 concat { '/etc/swift/proxy-server.conf':
    owner   => 'swift',
    group   => 'swift',
    mode    => '0660',
    require => Package['swift-proxy'],
  }

 接着使用concat::fragment define把这些template拼接起来,例如,proxy-server:

  concat::fragment { 'swift_proxy':
    target  => '/etc/swift/proxy-server.conf',
    content => template('swift/proxy-server.conf.erb'),
    order   => '00',
  }

     其实和使用file resource来渲染template类似,唯一的区别就是order参数,指明这片(fragment)配置在目标配置文件中的顺序,升序排列。

 

自定义资源类型(custom resource type)的出现

      前面所提的两种方法的核心都是使用模板来管理配置文件,唯一的区别是concat的方法将一个配置文件进行了拆分,使之变成可组合的。使用模板的一个主要缺点是每次添加一个新变量,需要在模板文件里添加上这个新变量:

<%= @new_variable %>

      然后在类文件中添加这个变量:

  $new_vairable = 'hello',

     对于一个成熟的项目来说,并不频繁的配置变更,使得使用模板成为理所当然的方法。例如,memcached,mysql的配置文件。但是对于快速迭代,频繁变更的Openstack项目来说,模板将成为一个梦魇。我们的PTL曾经做了一个统计,在G版前的过半提交都是和配置选项有关。那么有没有一种方法,使得终端用户在添加一个新配置选项时,无需在模板中预先定义就可以使用?

     我们在邮件列表中针对这个问题做了大量的讨论,最后选择使用自定义的资源类型来解决这个问题。以nova为例,我们使用nova_config来管理nova.conf中所有的配置选项。

     例如,我们想管理nova.conf中[default]下的libvirt_type选项。那么只要在类中添加:

 $libvirt_type = 'kvm',

nova_config { 'default/libvirt_type': value => $libvirt_type;}

     而我无需再去模板中定义这个变量。

     使用这种方式的另一大好处是,可以做到细粒度地控制与之相关服务的重载。

     例如:

      当nova.conf中的connection选项发生变更时,那么需要执行一次db_sync的操作;

      当vncproxy_host参数发生变更时,需要通知nova-novncproxy服务去重新加载配置文件;

      当quota相关参数发生变更时,需要通知nova-scheduler服务区重新加载配置文件;

      ......

      至此,在puppet openstack核心模块中,模板被完全弃用。

 

管理自定义参数的*::config类

     社区的puppet openstack modules针对的是终端用户的通用需求,因此并不能完全地满足用户的需求。所谓不能满足用户的需求,大致可以划分为两类:

     1. 某些plugin或者driver的配置选项缺失,例如cinder的solidfire,neutron的ibm plugin等等,使用的人少,社区的精力有限,暂时还没有完成这些功能的配置,那么开发者会把这些功能放入到作为某个模块中的一个类,然后再推到社区来,但这个过程可能需要数天或者几个礼拜的时间,期间需要等待core devs的aprove;

     2. 自定义参数  例如,我司针对neutron和nova做了大量的定制化修改,添加了一些自定义选项。因此,upstream modules就不能满足我的需求,那么我就不得不使用自定义的资源类型去修改源代码,这我来说轻车熟路,但是许多终端用户并不懂puppet,当然最终驱动我去开发这个功能的源头是:懒。

 

 我司的neutron core dev @gongysh 是个活力十足的家伙,每天都会在协作平台上给我分上几个非常boring的task:

       a. 在开发环境中,在xxx配置文件中添加xxx参数

       b. 把xxx参数的值修改为yyy

       c. 删除xxx参数

 

   在被折腾了两个多月后,我终于厌倦这样烦躁的重复劳动了,开始思考如何解决这样的问题。4月初,我留意到iweb的Mathieu(core member)向puppet-nova提交了一个patch,添加了一个新类:nova::config,旨在解决管理那些暂时还没有被upstream module收录的参数,例如nova.conf中rabbit_retry_interval参数还没被puppet-nova管理,但我想使用,又不想去修改源码,使用nova::config就可以处理。

代码很简洁,只有十几行,核心在于create_resouces函数所做的迭代,类似于编程语言中的for循环,Puppet 3.5之前还不支持这种语法的迭代(虽然有trick可以做到),因此一般都会使用create_resouces,create_resources一般接受两个参数,第一个是resource name,第二个是带有多组键值的字典。

class nova::config (
  $nova_config        = {},
  $nova_paste_api_ini = {},
) {

  validate_hash($nova_config)
  validate_hash($nova_paste_api_ini)

  create_resources('nova_config', $nova_config)
  create_resources('nova_paste_api_ini', $nova_paste_api_ini)
}

  我在做code review的时候,发现这家伙的use case是错的,我在修复之后,在hieradata里可以按照以下格式添加参数。

 nova_config:
     DEFAULT/foo:
        value: fooValue
     DEFAULT/bar:
        value: barValue

   

       某天下午,我在往puppet-neutron里添加自定义参数的时候,突然想到使用这种方法就可以不用预先在class中这个参数,而直接在hierdata中为这个参数赋值!

       于是我在社区的邮件列表发起了一个关于更好地管理自定义参数方法的讨论,得到了社区的肯定,并且Cisco的Michael Chapman(core member)将这个议题作为一个“good topic”收录进Atlanta Design Summit上的Puppet section,参见:https://etherpad.openstack.org/p/ATL-ops-unconference-RFC

 

目前puppet-neutron,puppet-cinder,puppet-glance,puppet-keystone均已支持使用这种方式来管理自定义参数,其他项目还在Code review中。

相关patch的链接参见:

https://review.openstack.org/#/c/79506

https://review.openstack.org/#/c/84987/

https://review.openstack.org/#/c/82699/

https://review.openstack.org/#/c/84976/

https://review.openstack.org/#/c/84981/

https://review.openstack.org/#/c/84999/

 

鱼与熊掌不能兼得,未来路在何方

      从前面的例子中,目前结合使用最新的openstack自定义资源类型和*::config类可以很好地驾驭Openstack这头巨兽,灵活地处理各类参数的配置。但这种方法与模板相比,也有它的缺点:无法做到强收敛性,这些自定义资源和类无法管理那些没有显式调用的选项。

    再来举个简单的例子,当系统安装完keystone包后,keystone-paste.ini会被放置到/etc/keystone目录下,我不会去管理keystone-paste.ini中[fileter:debug]下的paste.filter_facotry参数,但也希望默认参数不会被篡改,或者被删除:

[filter:debug]
paste.filter_factory = keystone.common.wsgi:Debug.factory

如果是使用模板,只需要把这段直接粘贴进去即可,但若是使用keystone_config的话,那么就必须在类中显式地指明,这显然是非常低效的,那么有没有更好的方法呢?

暂时没有,在经过了多次的讨论后,社区决定在没有更好的解决办法前,不主动去维护那些不被管理的参数,毕竟在一个严格的生产环境中,被人为篡改的可能不高。

亲,如果你有更好的方案,欢迎向我们提patch ! :)

 

 

 

 

posted @ 2014-04-27 16:43 牛皮糖NewPtone 阅读(...) 评论(...) 编辑 收藏