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鎖的問題。