ceilometer监控源码分析之任务队列
场景描述:
ceilometer(这里仅指监控任务)在每个宿主机上运行,读取/etc/ceilometer/pipeline.yaml中配置定时执行监控任务。
pipeline.yaml内容如下:
图注:该图中有三个监控项,分别是heartbeat, cpu, memory,interval表示定时间隔。
代码分析:
( 一 )
故事从/ceilometer/agent/base.py 讲起
openstack项目通常的结构是manage+instance。base.py下有两个关键的类,一个是AgentManager,一个是PollingTask.
1. PollingTask
实现Task的类,主要函数是 poll_and_publish (),该函数实现了从获取监控数据至发送数据的全过程。
2. AgentManager
该类继承os_service, 作为进程入口,持有且管理Task,主要函数是 start()
def start(self):
self.pipeline_manager = publish_pipeline.setup_pipeline()
self.partition_coordinator.start()
self.join_partitioning_groups()
# allow time for coordination if necessary
delay_start = self.partition_coordinator.is_active()
# set shuffle time before polling task if necessary
delay_polling_time = random.randint(
0, cfg.CONF.shuffle_time_before_polling_task)
for interval, task in six.iteritems(self.setup_polling_tasks()):
delay_time = (interval + delay_polling_time if delay_start
else delay_polling_time)
self.tg.add_timer(interval,
self.interval_task,
initial_delay=delay_time,
task=task)
self.tg.add_timer(cfg.CONF.coordination.heartbeat,
self.partition_coordinator.heartbeat)
代码注释:
- pipeline_manager从pipeline.yaml文件解析出需要监控的项,封装为interval,task的二元组
- self.tg 实例化一个线程池,实现在/ceilometer/openstack/common/threadgroup.py
- self.interval_task 方法是调用每一个task类中的poll_and_publish()
def poll_and_publish(self):
cache = {}
discovery_cache = {}
for source_name in self.pollster_matches:
with self.publishers[source_name] as publisher:
for pollster in self.pollster_matches[source_name]:
try:
samples = list(pollster.obj.get_samples(
manager=self.manager,
cache=cache,
resources=polling_resources
))
publisher(samples)
except plugin_base.PollsterPermanentError as err:
LOG.error(_(
'Prevent pollster %(name)s for '
'polling source %(source)s anymore!')
% ({'name': pollster.name, 'source': source_name}))
self.resources[key].blacklist.append(err.fail_res)
代码注释:
- 只摘抄了核心代码
- samples = list() 是调用pollster.obj的get_samples获取每一个vm的监控数据,并返回一个list。
- publisher() 将获取的监控数据,一起发送。
小结:到目前为止,ceilometer启动服务后,会读取配置拿到需要监控的监控项,然后针对每一项起一个线程去执行定时任务。任务内容是获取宿主机上所有虚拟机监控信息,并发送。
( 二 )
/ceilometer/openstack/common/threadgroup.py
该文件下有两个类,一个是 ThreadGroup,一个是 Thread。
- Thread 主要是对greenthread简单的封装,并将threadgroup作为类变量。
- ThreadGroup 实例化一个eventlet的greenpool.封装控制pool的常规操作。
上一小结第一个代码片段中,我们通过 self.tg.add_timer() 将每一个pipeline任务加入线程池。
def add_timer(self, interval, callback, initial_delay=None,
*args, **kwargs):
pulse = loopingcall.FixedIntervalLoopingCall(callback, *args, **kwargs)
pulse.start(interval=interval,
initial_delay=initial_delay)
self.timers.append(pulse)
@param: interval 定时任务执行间隔
@param: callback 上一小节分析的poll_and_publish方法
这节代码关键是pulse是个什么?
loopingcall.FixedIntervalLoopingCall里面封装了一个greenthread,将callback作为参数传进去,并生成一个新的协程,其基本方法就是 start(), stop() 和 wait()
def _inner():
try:
while self._running:
start = _ts()
self.f(*self.args, **self.kw)
end = _ts()
if not self._running:
break
delay = end - start - interval
if delay > 0:
LOG.warn(_LW('task %(func_name)r run outlasted '
'interval by %(delay).2f sec'),
{'func_name': self.f, 'delay': delay})
greenthread.sleep(-delay if delay < 0 else 0)
代码注释:
- self.f 还是之前分析的 poll_and_publish(),service的主要 job
- delay是计算 poll_and_publish() 执行时间差,并减去任务执行间隔时间
- delay取反后,就是任务需要sleep的准确时间。
小结:至此,就是服务主循环实现的过程。对于多个pipeline,我们启动多个协程,各自计时,实现获取数据及推送功能。
( 三 )
回到 /ceilometer/agent/base.py
我们来看一下如何获取一台宿主机上,所有虚拟机监控数据。在这里我们以 cpu负载为例。
如果忘记下面这段代码可以回顾下第一小节。
samples = list(pollster.obj.get_samples(
manager=self.manager,
cache=cache,
resources=polling_resources))
我们通过这个得到监控数据的list。也就是 pollster.obj.get_samples() 会返回所有监控数据。
def get_samples(self, manager, cache, resources):
resources_no_repeat = []
for r in resources:
uniq_id = BaseParallelPollster.get_resource_identity(r)
# avoid re-add task.If not those most time-consuming task will
# occupy all thread, other waiting tasks cannot be attached
# to a thread.
if uniq_id not in BaseParallelPollster.uniq_ids:
BaseParallelPollster.uniq_ids.add(uniq_id)
resources_no_repeat.append(r)
self._collector.add_tasks(
[PoolTask(
self.inspector_resource_info,
args=[r, manager, cache],
callback=self.handle_result, ex_callback=self.handle_exception)
for r in resources_no_repeat])
success_taskes = self._collector.wait_for_result(self._default_timeout)
result = []
for _t in success_taskes:
result += self.convert_info_2_sample(_t.result, *_t.args)
return result.__iter__()
代码注释:
- resources是通过libvirt接口获取该宿主机上所有vm的instances。
- self._collector 显然是针对每一个vm实例,将其放入线程池中,获取其监控数据。
- success_taskes 是拿到所有运行结果。
- self.convert_info_2_sample() 将数据转换为我们需要的格式。
self._collector.add_tasks() 是在ResultCollector类中,主要方法是 add_tasks , start_exec_tasks 和 wait_for_result。
主要分析 wait_for_result 方法
def wait_for_result(self, time_out, check_interval=0.1):
assert check_interval > 0
assert check_interval < time_out
# start all task.
self.start_exec_tasks()
# wait for all task for result.
time_start = int(time.time())
time.sleep(0.1)
while time.time() - time_start < time_out and not self._is_finished():
time.sleep(check_interval)
return [_t for _t in self._tasks if _t.result]
代码注释:
- 如果等待执行时间超过 timeout, 则跳出while循环
- 如果所有结果都完成, 则跳出while循环
- 只将有数据的对象返回
- 每一个任务执行完会回调 _inc_decorate 给 self._finished + 1, 如果self._finished 数量大于等于task,则所有任务都完成。
总结
ceiometer监控任务队列框架如上所述,一个大的协程里面套了一个小的协程池。由于python协程存在自己的缺陷,为了满足并发,我们将大的协程拆开为进程,例如ceilometer-agent-compute-cpu, ceilometer-agent-compute-mem 等。
遗留问题:
如果一台宿主机上vm很多,那么再执行的时候,会大量并发调用libvirt接口,会造成libvirt部分锁的问题。从代码分析结果来看,只要ceilometer在interval内获取到数据即可,( sleep 时间是 interval - 函数执行时间)。
那么针对这个问题,我觉得可以限制ceilometer小协程池的数量,让获取监控数据在interval之内,来缓解libvirt锁的问题。