ECS主機(jī)狀態(tài)變化事件的自動化運維
本文通過實踐案例為您介紹云監(jiān)控如何通過輕量消息隊列(原 MNS)的隊列實現(xiàn)自動化處理ECS主機(jī)狀態(tài)變化事件。
前提條件
請您確保已在輕量消息隊列(原 MNS)控制臺,創(chuàng)建隊列,例如:ecs-cms-event。
關(guān)于如何創(chuàng)建隊列,請參見創(chuàng)建隊列。
請您確保已在云監(jiān)控控制臺,創(chuàng)建系統(tǒng)事件報警規(guī)則,有關(guān)具體操作,請參見管理系統(tǒng)事件報警規(guī)則(舊版)。
請您確保已安裝Python依賴。
本文所有代碼均以Python 3.6為例,您也可以使用其他編程語言,例如:Java和PHP。
關(guān)于如何安裝Python SDK,請參見Python SDK安裝。
背景信息
ECS在已有的系統(tǒng)事件基礎(chǔ)上,通過云監(jiān)控新發(fā)布了狀態(tài)變化類事件和搶占型實例的中斷通知事件。每當(dāng)ECS主機(jī)的狀態(tài)發(fā)生變化時,都會觸發(fā)一條ECS狀態(tài)變化事件。這種變化包括您通過控制臺、OpenAPI或SDK操作導(dǎo)致的變化,也包括彈性伸縮或欠費等原因而自動觸發(fā)的變化,還包括因為系統(tǒng)異常而觸發(fā)的變化。
云監(jiān)控提供四種事件報警處理方式,包括:輕量消息隊列(原 MNS)、函數(shù)計算、URL回調(diào)和日志服務(wù)。本文以輕量消息隊列(原 MNS)為例,為您介紹云監(jiān)控自動化處理ECS主機(jī)狀態(tài)變更事件的三種最佳實踐。
操作步驟
云監(jiān)控將ECS主機(jī)所有的狀態(tài)變化事件投遞到輕量消息隊列(原 MNS),輕量消息隊列(原 MNS)獲取消息并進(jìn)行消息處理。
實踐一:對所有ECS主機(jī)的創(chuàng)建和釋放事件進(jìn)行記錄。
目前ECS控制臺無法查詢已經(jīng)釋放的實例。如果您有查詢需求,可以通過ECS主機(jī)狀態(tài)變化事件將所有ECS主機(jī)的生命周期記錄在數(shù)據(jù)庫或日志服務(wù)中。每當(dāng)您創(chuàng)建ECS主機(jī)時,會發(fā)送一個Pending事件,每當(dāng)您釋放ECS主機(jī)時,會發(fā)送一個Deleted事件。
編輯一個Conf文件。
Conf文件中需要包含輕量消息隊列(原 MNS)的
endpoint
、阿里云的access_key
和access_key_secret
、region_id
(例如:cn-beijing)和queue_name
。說明endpoint
可以在輕量消息隊列(原 MNS)控制臺的隊列頁面,單擊獲取Endpoint。import os # 請確保代碼運行環(huán)境設(shè)置了環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。 # 工程代碼泄露可能會導(dǎo)致 AccessKey 泄露,并威脅賬號下所有資源的安全性。以下代碼示例使用環(huán)境變量獲取 AccessKey 的方式進(jìn)行調(diào)用,僅供參考,建議使用更安全的 STS 方式 class Conf: endpoint = 'http://<id>.mns.<region>.aliyuncs.com/' access_key = os.environ['ALIBABA_CLOUD_ACCESS_KEY_ID'] access_key_secret = os.environ['ALIBABA_CLOUD_ACCESS_KEY_SECRET'] region_id = 'cn-beijing' queue_name = 'test' vsever_group_id = '<your_vserver_group_id>'
使用輕量消息隊列(原 MNS)的SDK編寫一個MNS Client用來獲取MNS消息。
# -*- coding: utf-8 -*- import json from mns.mns_exception import MNSExceptionBase import logging from mns.account import Account from . import Conf class MNSClient(object): def __init__(self): self.account = Account(Conf.endpoint, Conf.access_key, Conf.access_key_secret) self.queue_name = Conf.queue_name self.listeners = dict() def regist_listener(self, listener, eventname='Instance:StateChange'): if eventname in self.listeners.keys(): self.listeners.get(eventname).append(listener) else: self.listeners[eventname] = [listener] def run(self): queue = self.account.get_queue(self.queue_name) while True: try: message = queue.receive_message(wait_seconds=5) event = json.loads(message.message_body) if event['name'] in self.listeners: for listener in self.listeners.get(event['name']): listener.process(event) queue.delete_message(receipt_handle=message.receipt_handle) except MNSExceptionBase as e: if e.type == 'QueueNotExist': logging.error('Queue %s not exist, please create queue before receive message.', self.queue_name) else: logging.error('No Message, continue waiting') class BasicListener(object): def process(self, event): pass
上述代碼只對輕量消息隊列(原 MNS)獲取的數(shù)據(jù),調(diào)用Listener消費消息之后刪除消息。
注冊一個指定Listener消費事件。這個簡單的Listener判斷收到Pending和Deleted事件時,打印一行日志。
# -*- coding: utf-8 -*- import logging from .mns_client import BasicListener class ListenerLog(BasicListener): def process(self, event): state = event['content']['state'] resource_id = event['content']['resourceId'] if state == 'Panding': logging.info(f'The instance {resource_id} state is {state}') elif state == 'Deleted': logging.info(f'The instance {resource_id} state is {state}')
Main函數(shù)寫法如下:
mns_client = MNSClient() mns_client.regist_listener(ListenerLog()) mns_client.run()
實際生產(chǎn)環(huán)境下,可能需要將事件存儲在數(shù)據(jù)庫或日志服務(wù)SLS中,方便后期的搜索和審計。
實踐二:ECS主機(jī)關(guān)機(jī)自動重啟。
在某些場景下,ECS主機(jī)會非預(yù)期地關(guān)機(jī),您可能需要自動重啟已經(jīng)關(guān)機(jī)的ECS主機(jī)。
為了實現(xiàn)ECS主機(jī)關(guān)機(jī)后自動重啟,您可以復(fù)用實踐一中的MNS Client,添加一個新的Listener。當(dāng)您收到Stopped事件時,對該ECS主機(jī)執(zhí)行Start命令。
# -*- coding: utf-8 -*- import logging from aliyunsdkecs.request.v20140526 import StartInstanceRequest from aliyunsdkcore.client import AcsClient from .mns_client import BasicListener from .config import Conf class ECSClient(object): def __init__(self, acs_client): self.client = acs_client # 啟動ECS主機(jī) def start_instance(self, instance_id): logging.info(f'Start instance {instance_id} ...') request = StartInstanceRequest.StartInstanceRequest() request.set_accept_format('json') request.set_InstanceId(instance_id) self.client.do_action_with_exception(request) class ListenerStart(BasicListener): def __init__(self): acs_client = AcsClient(Conf.access_key, Conf.access_key_secret, Conf.region_id) self.ecs_client = ECSClient(acs_client) def process(self, event): detail = event['content'] instance_id = detail['resourceId'] if detail['state'] == 'Stopped': self.ecs_client.start_instance(instance_id)
在實際生產(chǎn)環(huán)境下,執(zhí)行完Start命令后,可能還需要繼續(xù)接收后續(xù)的Starting、Running或Stopped等事件,再配合計時器和計數(shù)器,進(jìn)行成功或失敗之后的處理。
實踐三:搶占型實例釋放前,自動從負(fù)載均衡SLB移除。
搶占型實例在釋放之前五分鐘左右,會發(fā)出釋放告警事件,您可以在這短暫的時間運行業(yè)務(wù)不中斷邏輯,例如:主動從負(fù)載均衡SLB的后端服務(wù)器中去掉這臺即將被釋放的搶占型實例,而非被動等待實例釋放后負(fù)載均衡SLB的自動處理。
您復(fù)用實踐一的MNS Client,添加一個新的Listener,當(dāng)收到搶占型實例的釋放告警時,調(diào)用負(fù)載均衡SLB的SDK。
# -*- coding: utf-8 -*- from aliyunsdkcore.client import AcsClient from aliyunsdkcore.request import CommonRequest from .mns_client import BasicListener from .config import Conf class SLBClient(object): def __init__(self): self.client = AcsClient(Conf.access_key, Conf.access_key_secret, Conf.region_id) self.request = CommonRequest() self.request.set_method('POST') self.request.set_accept_format('json') self.request.set_version('2014-05-15') self.request.set_domain('slb.aliyuncs.com') self.request.add_query_param('RegionId', Conf.region_id) def remove_vserver_group_backend_servers(self, vserver_group_id, instance_id): self.request.set_action_name('RemoveVServerGroupBackendServers') self.request.add_query_param('VServerGroupId', vserver_group_id) self.request.add_query_param('BackendServers', "[{'ServerId':'" + instance_id + "','Port':'80','Weight':'100'}]") response = self.client.do_action_with_exception(self.request) return str(response, encoding='utf-8') class ListenerSLB(BasicListener): def __init__(self, vsever_group_id): self.slb_caller = SLBClient() self.vsever_group_id = Conf.vsever_group_id def process(self, event): detail = event['content'] instance_id = detail['instanceId'] if detail['action'] == 'delete': self.slb_caller.remove_vserver_group_backend_servers(self.vsever_group_id, instance_id)
重要搶占型實例釋放告警的event name與前面不同,應(yīng)該是
mns_client.regist_listener(ListenerSLB(Conf.vsever_group_id), 'Instance:PreemptibleInstanceInterruption')
。在實際生產(chǎn)環(huán)境下,您需要再申請一臺新的搶占型實例,掛載到負(fù)載均衡SLB,來保證服務(wù)能力。