全部產品
Search
文件中心

Elastic Compute Service:ECS主機狀態變化事件的自動化營運

更新時間:Feb 28, 2024

本文通過實踐案例為您介紹CloudMonitor如何通過Message ServiceMNS的隊列實現自動化處理ECS主機狀態變化事件。

前提條件

  • 請您確保已在Message ServiceMNS控制台,建立隊列,例如:ecs-cms-event。

    關於如何建立隊列,請參見建立隊列

  • 請您確保已在CloudMonitor控制台,建立系統事件警示規則。

    關於如何建立隊列,請參見管理系統事件警示規則(舊版)

  • 請您確保已安裝Python依賴。

    本文所有代碼均以Python 3.6為例,您也可以使用其他程式設計語言,例如:Java和PHP。

    關於如何安裝Python SDK,請參見Python SDK安裝

背景資訊

ECS在已有的系統事件基礎上,通過CloudMonitor新發布了狀態變化類事件和搶佔型執行個體的中斷通知事件。每當ECS主機的狀態發生變化時,都會觸發一條ECS狀態變化事件。這種變化包括您通過控制台、OpenAPI或SDK操作導致的變化,也包括Auto Scaling或欠費等原因而自動觸發的變化,還包括因為系統異常而觸發的變化。

CloudMonitor提供四種事件警示處理方式,包括:Message Service隊列、Function Compute、URL回調和Log Service。本文以Message Service隊列為例,為您介紹CloudMonitor自動化處理ECS主機狀態變更事件的三種最佳實務。

操作步驟

CloudMonitor將ECS主機所有的狀態變化事件投遞到Message ServiceMNS,Message ServiceMNS擷取訊息並進行訊息處理。

  • 實踐一:對所有ECS主機的建立和釋放事件進行記錄。

    目前ECS控制台無法查詢已經釋放的執行個體。如果您有查詢需求,可以通過ECS主機狀態變化事件將所有ECS主機的生命週期記錄在資料庫或Log Service中。每當您建立ECS主機時,會發送一個Pending事件,每當您釋放ECS主機時,會發送一個Deleted事件。

    1. 編輯一個Conf檔案。

      Conf檔案中需要包含Message ServiceMNS的endpoint、阿里雲的access_keyaccess_key_secretregion_id(例如:cn-beijing)和queue_name

      說明

      endpoint可以在Message ServiceMNS控制台的隊列頁面,單擊擷取Endpoint

      import os
      
      # 請確保代碼運行環境設定了環境變數 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
      # 工程代碼泄露可能會導致 AccessKey 泄露,並威脅帳號下所有資源的安全性。以下程式碼範例使用環境變數擷取 AccessKey 的方式進行調用,僅供參考,建議使用更安全的 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>'
                                          
    2. 使用Message ServiceMNS的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訊息擷取的資料,調用Listener消費訊息之後刪除訊息。

    3. 註冊一個指定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函數寫法如下:

      mns_client = MNSClient()
      
      mns_client.regist_listener(ListenerLog())
      
      mns_client.run()

      實際生產環境下,可能需要將事件儲存在資料庫或Log ServiceSLS中,方便後期的搜尋和審計。

  • 實踐二:ECS主機關機自動重啟。

    在某些情境下,ECS主機會非預期地關機,您可能需要自動重啟已經關機的ECS主機。

    為了實現ECS主機關機後自動重啟,您可以複用實踐一中的MNS Client,添加一個新的Listener。當您收到Stopped事件時,對該ECS主機執行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主機
        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)
                        

    在實際生產環境下,執行完Start命令後,可能還需要繼續接收後續的Starting、Running或Stopped等事件,再配合計時器和計數器,進行成功或失敗之後的處理。

  • 實踐三:搶佔型執行個體釋放前,自動從Server Load Balancer移除。

    搶佔型執行個體在釋放之前五分鐘左右,會發出釋放警示事件,您可以在這短暫的時間運行業務不中斷邏輯,例如:主動從Server Load Balancer的後端伺服器中去掉這台即將被釋放的搶佔型執行個體,而非被動等待執行個體釋放後Server Load Balancer的自動處理。

    您複用實踐一的MNS Client,添加一個新的Listener,當收到搶佔型執行個體的釋放警示時,調用Server Load Balancer的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與前面不同,應該是mns_client.regist_listener(ListenerSLB(Conf.vsever_group_id), 'Instance:PreemptibleInstanceInterruption')

    在實際生產環境下,您需要再申請一台新的搶佔型執行個體,掛載到Server Load Balancer,來保證服務能力。