OpenStack计量服务-Ceilometer

Ceilometer(计量服务)简介

Ceilometer属于OpenStack项目中的一个组件,是在Telemetry服务基础上发展而来,主要用作收集、监控和处理OpenStack资源的指标数据。它可以收集虚拟机、卷、网络等资源的性能指标,如CPU利用率、内存使用量、网络流量等。这些指标数据可以用于监控和分析OpenStack环境的性能、资源利用情况以及故障排除。

Ceilometer通过与OpenStack其他组件集成,如Nova(计算服务)、Cinder(块存储服务)、Neutron(网络服务)等,能够获取各个组件的指标数据,并将其存储在数据库中供后续查询和分析。

Ceilometer还提供了报警和事件处理机制,可以根据设定的阈值或规则触发报警并采取相应的操作。这样,管理员可以根据指标数据的变化情况及时采取措施,维护和优化OpenStack环境。

Ceilometer架构

1702252910007.png 1702200509691.png

数据采集

1702253165613.png

Ceilometer有两个核心服务,也是它采集数据的两种方式:

  1. polling agent:

    1. Compute agent (ceilometer-agent-compute)运行在每个 compute 节点上,以轮询的方式通过调用 Image 的 driver 来获取资源使用统计数据。

    2. Central agent (ceilometer-agent-central)运行在 management server 上,以轮询的方式通过调用 OpenStack 各个组件(包括 Nova、Cinder、Glance、Neutron、Swift 等)的 API 收集资源使用统计数据。

    1702204523484.png
  2. notification agent: 运行在一个或者多个management server上的数据收集程序,监听消息队列上的通知,将他们转换成时间和样本、应用管道操作。核心是通知守护进程(agent-notification),它监视消息队列中其他 OpenStack 组件(例如 Nova、Glance、Cinder、Neutron、Swift、Keystone 和 Heat)发送的数据以及 Ceilometer 内部通信。

    1702204497853.png

对于采集的数据类型也有两种:

  1. Sample(样本):Sample是指标数据的基本单位,用于表示一次指标的测量值。每个Sample包含了指标的名称、值、时间戳、资源标识符等信息。Ceilometer通过收集来自不同OpenStack组件的指标数据,生成一系列的Sample。这些Sample可以表示CPU利用率、内存使用量、网络流量等不同类型的指标数据。Sample通常与时间序列数据库(如Gnocchi)一起使用,用于存储和查询指标数据。

  2. Event(事件):Event用于表示OpenStack环境中的发生的事件。事件可以是资源的创建、修改、删除,或者其他与资源状态变化相关的操作。每个Event包含了事件的类型、资源标识符、时间戳、事件数据等信息。Ceilometer的Panko组件负责收集和存储这些事件数据,并与指标数据进行关联。通过事件数据,管理员可以了解资源的创建、状态变化等操作,以便进行监控、审计和故障排除等工作。

数据处理

Pipelines是Ceilometer中数据处理的机制,描述了一组sources(数据来源)和sinks(数据分发)之间的关系,Metes 数据依次经过(零个或者多个) Transformer 和 (一个或者多个)Publisher 处理,最后达到(一个或者多个)Receiver。其中Recivers 包括 Ceilometer Collector 和 外部系统。

1702252966065.png

对于采集到的数据Ceilometer可以通过多个pipelines以多个形式进行发布,该功能由notification agents完成。

以下是常见的可以用来发布数据的方式:

  1. gnocchi:发布samples/events到Gnocchi Api中。

  2. notifier:基于通知的发布器,将sample推送到可由外部系统使用的消息队列。

  3. http/https:通过REST API发布数据。

  4. file:发布数据到指定地址和名字的文件中。

Gnocchi是一个开源的时间序列数据库项目,专门用于存储和分析大规模的时间序列数据。它最初作为OpenStack项目的一部分,用于存储和查询Ceilometer(Telemetry)收集的指标数据,但现在已经成为一个独立的项目,可以与多个数据源和应用程序集成。

Ceilometer命令

1702252415032.png

Ceilometer实验

作业与实验环境

超星网址
虚拟机openstack-allinone,账户root,密码000000

  1. VmWare需要修改网络设置,在编辑->虚拟网络编辑器中将Vmnet1网卡的子网由原来的192.168.10.0改为192.168.100.0

    1701610472777.png
  2. 后续实验需要启动实例,Horizon登录地址192.168.100.10/dashboard,使用域xiandian、用户名admin、密码000000登录之后上传本地D盘的cirros镜像到OpenStack平台。

    1701650465473.png
  3. 执行脚本完成身份认证。

    # 加载管理员环境变量,后续 openstack/ceilometer/curl 命令无需手动传凭据
    [root@controller ~]# source /etc/keystone/admin-openrc.sh
  4. 使用上节课学习的heat编排模板创建实例。

    • 将以下内容保存到模板文件instance.yaml中:

      heat_template_version: 2015-04-30
      description: 
      resources:
        # 1. 创建私有网络
        my_network:
          type: OS::Neutron::Net
          properties:
            name: private-iso-net
      
        # 2. 创建私有子网
        my_subnet:
          type: OS::Neutron::Subnet
          properties:
            name: private-iso-net-subnet
            network_id: { get_resource: my_network }
            # 网段
            cidr: 192.168.200.0/24
            gateway_ip: 192.168.200.1
            # DNS服务器地址
            dns_nameservers: ["114.114.114.114", "8.8.8.8"]
            ip_version: 4
            enable_dhcp: true
        # 创建云主机
        my_instance:
          type: OS::Nova::Server
          properties:
            name: my-test-instance
            # 镜像和规格名称
            image: cirros
            flavor: m1.small
            networks:
                - network: { get_resource: my_network }
    • 创建堆栈

      # 创建堆栈:-t 指定模板文件,test 为堆栈名
      [root@controller ~]# openstack stack create -t instance.yaml test
      +---------------------+--------------------------------------+
      | Field               | Value                                |
      +---------------------+--------------------------------------+
      | id                  | 77aa77ea-1571-46eb-9a34-52c0d41f4172 |
      | stack_name          | test                                 |
      | description         | No description                       |
      | creation_time       | 2025-12-11T12:30:24                  |
      | updated_time        | None                                 |
      | stack_status        | CREATE_IN_PROGRESS                   |
      | stack_status_reason |                                      |
      +---------------------+--------------------------------------+
      # 查看堆栈资源状态,确认网络/子网/实例创建进度
      [root@controller ~]# openstack stack resource list test                 
      +---------------+-------------------------------------+---------------------+--------------------+---------------------+
      | resource_name | physical_resource_id                | resource_type       | resource_status    | updated_time        |
      +---------------+-------------------------------------+---------------------+--------------------+---------------------+
      | my_instance   | ad42c104-0d1c-457d-a8ef-            | OS::Nova::Server    | CREATE_IN_PROGRESS | 2025-12-11T12:30:24 |
      |               | c171e8a7a70f                        |                     |                    |                     |
      | my_subnet     | cf3d0052-587d-                      | OS::Neutron::Subnet | CREATE_IN_PROGRESS | 2025-12-11T12:30:24 |
      |               | 46ab-b784-054f011995ca              |                     |                    |                     |
      | my_network    | ab75c798-d8c8-4832-94af-            | OS::Neutron::Net    | CREATE_COMPLETE    | 2025-12-11T12:30:24 |
      |               | e36cc0e54d47                        |                     |                    |                     |
      +---------------+-------------------------------------+---------------------+--------------------+---------------------+

作业1:查看堆栈资源清单后截图上传。

meters.yaml查看

关于Ceilmoeter需要采集的数据(指标)都定义在/etc/ceilometer/meters.yaml中,管理员可以对其进行修改。

示例(不需要修改):

- name: "image.size"                    # 指标名称:镜像大小
   event_type:                          # 触发该指标的事件类型列表
     - "image.upload"                   # 事件类型1:镜像上传
     - "image.delete"                   # 事件类型2:镜像删除
     - "image.update"                   # 事件类型3:镜像更新
   type: "gauge"                        # 指标类型:gauge(可变度量值)
   unit: B                              # 指标单位:字节(Bytes)
   volume: $.payload.size               # 指标值提取:从事件payload的size字段获取
   resource_id: $.payload.id            # 资源标识:从事件payload的id字段获取
   project_id: $.payload.owner          # 所属项目:从事件payload的owner字段获取

pipeline.yaml修改

  1. 可使用配置文件/etc/ceilometer/pipeline.yaml配置对meters的处理,示例:

    修改cpu meter的采样间隔为1分钟,并且添加- file:///var/log/ceilometer/ceilometer-file-publisher到publishers字段,增加一个发布方式。

    # sources:定义采集来源(轮询间隔、meter 列表、输出到哪个 sink)
    - name: cpu_source
        interval: 60            # 采样间隔 60 秒
        meters:
            - "cpu"              # 采集 cpu meter
        sinks:
            - cpu_sink            # 发送到名为 cpu_sink 的下游处理链
    
    # sinks:定义处理链(transformer + publisher)
    - name: cpu_sink
        transformers:
            - name: "rate_of_change"   # 将累计值转为速率
                parameters:
                    target:
                        name: "cpu_util"     # 输出指标名
                        unit: "%"            # 单位
                        type: "gauge"
                        scale: "100.0 / (10**9 * (resource_metadata.cpu_number or 1))"
                        # 将纳秒级 cpu 时间换算为百分比,除以 vCPU 数
        publishers:
            - notifier://                # 默认通过消息队列发布
            - file:///var/log/ceilometer/ceilometer-file-publisher   # 追加:写入文件
            - udp://192.168.100.10:9000   # 追加:UDP 推送到外部监听端口
  2. 重启Ceilometer相关服务后查看发布的内容。

    [root@controller]# systemctl restart openstack-ceilometer*
    • 查看文件内接收的内容:

      [root@controller ~]# tail -f /var/log/ceilometer/ceilometer-file-publisher
      {'user_id': u'0befa70f767848e39df8224107b71858', 'name': 'cpu_util', 'resource_id': u'dcfb5539-454c-4d1a-9dc2-f39a3a64cb34', 'timestamp': u'2023-12-10T23:43:22.566393', 'resource_metadata': {u'status': u'active', u'cpu_number': 1, u'state': u'active', u'ramdisk_id': None, u'display_name': u'te', u'name': u'instance-00000009', u'disk_gb': 1, u'instance_host': u'controller', u'kernel_id': None, u'instance_id': u'dcfb5539-454c-4d1a-9dc2-f39a3a64cb34', u'image': {u'id': u'c65fc953-344c-46dc-8e3a-4a452fac4ffd', u'links': [{u'href': u'http://controller:8774/c88f5a1b7619420dadb4309743e53f1a/images/c65fc953-344c-46dc-8e3a-4a452fac4ffd', u'rel': u'bookmark'}], u'name': u'cirros'}, u'ephemeral_gb': 0, u'vcpus': 1, u'memory_mb': 512, u'instance_type': u'm1.tiny', u'host': u'e3d6b1115b110b721fd922d3e5c497f2c86621b692ddccdbee24320b', u'root_gb': 1, u'image_ref': u'c65fc953-344c-46dc-8e3a-4a452fac4ffd', u'flavor': {u'name': u'm1.tiny', u'links': [{u'href': u'http://controller:8774/c88f5a1b7619420dadb4309743e53f1a/flavors/1', u'rel': u'bookmark'}], u'ram': 512, u'ephemeral': 0, u'vcpus': 1, u'disk': 1, u'id': u'1'}, u'OS-EXT-AZ:availability_zone': u'nova', u'image_ref_url': u'http://controller:8774/c88f5a1b7619420dadb4309743e53f1a/images/c65fc953-344c-46dc-8e3a-4a452fac4ffd'}, 'volume': 0.0, 'source': 'openstack', 'project_id': u'f9ff39ba9daa4e5a8fee1fc50e2d2b34', 'type': 'gauge', 'id': 'e79bd3d4-97b5-11ee-b203-000c29d6d36c', 'unit': '%'}
    • 新建文件udp_receiver.py,将以下代码保存到该文件,然后使用python udp_receiver.py运行:

      # -*- coding: utf-8 -*-
      import BaseHTTPServer
      import socket
      import threading
      import json
      import datetime
      import sys
      import errno # 引入错误码模块
      
      # 尝试导入 msgpack
      try:
          import msgpack
          HAS_MSGPACK = True
      except ImportError:
          HAS_MSGPACK = False
      
      # --- 配置 ---
      UDP_PORT = 9000       
      WEB_PORT = 9001       # 端口保持和你刚才运行的一致
      DATA_STORE = []       
      
      # --- 线程1:UDP 接收 ---
      def udp_server_thread():
          sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          sock.bind(('0.0.0.0', UDP_PORT))
          print "[UDP] 监听服务已启动 (Port: %s)" % UDP_PORT
      
          while True:
              try:
                  data, addr = sock.recvfrom(65535)
                  payload = None
                  
                  if HAS_MSGPACK:
                      try: payload = msgpack.unpackb(data, encoding='utf-8')
                      except: 
                          try: payload = msgpack.unpackb(data)
                          except: pass
                  
                  if payload is None:
                      try: payload = json.loads(data)
                      except: pass
      
                  now = datetime.datetime.now().strftime("%H:%M:%S")
                  if payload:
                      if isinstance(payload, list):
                          for item in payload:
                              item['recv_time'] = now
                              DATA_STORE.insert(0, item)
                      elif isinstance(payload, dict):
                          payload['recv_time'] = now
                          DATA_STORE.insert(0, payload)
                  
                  if len(DATA_STORE) > 50:
                      del DATA_STORE[50:]
                      
                  # 简化日志,不打印具体内容,只打印计数
                  sys.stdout.write("[UDP] 收到数据! 当前缓存: %d\r" % len(DATA_STORE))
                  sys.stdout.flush()
      
              except Exception as e:
                  print "\nUDP Error:", e
      
      # --- 线程2:Web 展示 (修复 Broken Pipe) ---
      class MonitorHandler(BaseHTTPServer.BaseHTTPRequestHandler):
          def do_GET(self):
              # 【修复1】忽略 favicon.ico 请求,防止浏览器乱断开
              if self.path == '/favicon.ico':
                  self.send_response(204) # No Content
                  return
      
              try:
                  self.send_response(200)
                  self.send_header('Content-type', 'text/html; charset=utf-8')
                  self.end_headers()
      
                  html = u"""
                  <html>
                  <head>
                      <title>Ceilometer 监控</title>
                      <!-- 每 5 秒刷新一次,不要太快 -->
                      <meta http-equiv="refresh" content="5">
                      <style>
                          body { font-family: sans-serif; margin: 20px; }
                          table { border-collapse: collapse; width: 100%%; }
                          th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
                          th { background-color: #007bff; color: white; }
                          tr:nth-child(even) { background-color: #f2f2f2; }
                          .high { color: red; font-weight: bold; }
                      </style>
                  </head>
                  <body>
                      <h2>Ceilometer UDP 实时数据</h2>
                      <p>UDP端口: %d | Web端口: %d | 记录数: %d</p>
                      <table>
                          <tr>
                              <th>接收时间</th>
                              <th>Name</th>
                              <th>Volume</th>
                              <th>Resource ID</th>
                          </tr>
                  """ % (UDP_PORT, WEB_PORT, len(DATA_STORE))
                  
                  for item in DATA_STORE:
                      name = item.get('counter_name') or item.get('name', 'N/A')
                      vol = item.get('counter_volume') or item.get('volume', 0)
                      rid = item.get('resource_id', 'N/A')
                      time = item.get('recv_time', '')
      
                      style = u""
                      try:
                          if float(vol) > 80: style = u'class="high"'
                      except: pass
      
                      html += u"""
                          <tr>
                              <td>%s</td>
                              <td>%s</td>
                              <td %s>%s</td>
                              <td>%s</td>
                          </tr>
                      """ % (time, name, style, vol, rid)
      
                  html += u"</table></body></html>"
                  
                  # 【修复2】捕获写入时的 Broken Pipe 错误
                  self.wfile.write(html.encode('utf-8'))
      
              except socket.error as e:
                  # 如果是 Broken pipe (Errno 32),直接忽略,不打印报错
                  if e.errno == errno.EPIPE:
                      pass
                  else:
                      print "Socket Error:", e
              except Exception as e:
                  # 其他错误才打印
                  print "Web Error:", e
      
          # 禁止打印烦人的访问日志
          def log_message(self, format, *args): return
      
      if __name__ == '__main__':
          t = threading.Thread(target=udp_server_thread)
          t.setDaemon(True)
          t.start()
      
          # 允许地址重用,防止重启脚本时报 Address already in use
          BaseHTTPServer.HTTPServer.allow_reuse_address = True
          server = BaseHTTPServer.HTTPServer(('', WEB_PORT), MonitorHandler)
          
          print "\n==========================================="
          print "Web 面板地址: http://<Controller_IP>:%d" % WEB_PORT
          print "UDP 监听端口: %d" % UDP_PORT
          print "===========================================\n"
          
          try:
              server.serve_forever()
          except KeyboardInterrupt:
              print "\nExit."
    • 使用浏览器打开http://192.168.100.10:9001,查看网页端接收到的数据

    作业2:使用网页端接收到数据后截图上传。

数据读取

  1. 通过命令读取计量和样本。

    [root@controller ~]# ceilometer meter-list |head -10   # 列出计量项并截取前10行便于快速查看字段
    +---------------------------------+------------+-----------+-----------------------------------------------------------------------+----------------------------------+----------------------------------+
    | Name                            | Type       | Unit      | Resource ID                                                           | User ID                          | Project ID                       |
    +---------------------------------+------------+-----------+-----------------------------------------------------------------------+----------------------------------+----------------------------------+
    | compute.instance.booting.time   | gauge      | sec       | 2324c631-cf3e-49eb-a8e2-be384341e4ac                                  | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |
    | compute.instance.booting.time   | gauge      | sec       | 26b3b242-3704-4d37-88bc-d9cf8a5b0080                                  | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |
    | compute.instance.booting.time   | gauge      | sec       | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c                                  | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |
    | cpu                             | cumulative | ns        | 2324c631-cf3e-49eb-a8e2-be384341e4ac                                  | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |
    | cpu                             | cumulative | ns        | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c                                  | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |
    | cpu.delta                       | delta      | ns        | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c                                  | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |
    | cpu_util                        | gauge      | %         | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c                                  | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |
    [root@controller ~]# ceilometer sample-list -m cpu_util | head -10   # 指定 meter(cpu_util) 查看最近样本
    +--------------------------------------+----------+-------+----------------+------+----------------------------+
    | Resource ID                          | Name     | Type  | Volume         | Unit | Timestamp                  |
    +--------------------------------------+----------+-------+----------------+------+----------------------------+
    | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.34969394963  | %    | 2023-12-11T10:49:36.856000 |
    | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.41829637437  | %    | 2023-12-11T10:48:36.851000 |
    | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.50940950027  | %    | 2023-12-11T10:47:37.172000 |
    | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.53780765348  | %    | 2023-12-11T10:46:37.048000 |
    | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.50009759439  | %    | 2023-12-11T10:45:36.841000 |
    | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.46708312113  | %    | 2023-12-11T10:44:36.843000 |
    | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.31595080815  | %    | 2023-12-11T10:43:36.850000 |
  2. 通过api读取计量和样本。

    # 获取令牌Token(供 curl 直接使用)
    [root@controller ~]# openstack token issue
    # 复制令牌、导出令牌到环境变量 OS_TOKEN
    [root@controller ~]# export OS_TOKEN=gAAAAABlduo9h-PcRjEUSOgCRqzg06x6JzWSEMWWm2hFZqbbvh6zdR12vTzYP9HNRhk99fKbK0hDsYz6nVCZmDuqXJHGMaPWUutYxZ38t0jKwbvYDiVtzhAUK12ws6mVzXbjAIVThIv8QZZ_kuvaWhRf0EIp9JNZE0XTAHF_htTb1-v9LqX6aCg
    # 读取计量列表(8777 为 ceilometer-api 默认端口),json.tool 美化输出,less 分页
    [root@controller ~]# curl -X GET -H "X-Auth-Token: $OS_TOKEN" "http://localhost:8777/v2/meters" |python -m json.tool |less
    # 发起请求读取样本列表(同样使用 OS_TOKEN)
    [root@controller ~]# curl -X GET -H "X-Auth-Token: $OS_TOKEN" "http://localhost:8777/v2/samples" |python -m json.tool |less

    作业3:通过api读取到样本后截图上传。

Alarms告警规则

使用 CLI 创建一个名为 “cpu_high” 的 Threshold Alarm “当连续 2 个 1 分钟内 某 instance 的 cpu_util 值超过 70 的时候产生告警,并其内容被写入日志文件”:

# 创建阈值告警规则:当 CPU 利用率连续 3 个采样周期(3×60秒)超过 70% 时触发告警
# --name: 告警名称
# --meter-name: 监控指标名称(cpu_util)
# --threshold: 告警阈值(70%)
# --comparison-operator: 比较操作符(gt 表示大于)
# --statistic: 统计方式(avg 表示平均值)
# --period: 采样周期(60 秒)
# --evaluation-periods: 连续评估周期数(3 个周期都超过阈值才触发)
# --alarm-action: 告警触发时的动作(HTTP POST 到接收端点)
# --query: 查询条件(指定特定实例的 resource_id)
[root@controller ~]# ceilometer alarm-threshold-create \
    --name cpu_high \
    --description 'Alert when CPU exceeds 70% for 3 consecutive periods' \
    --meter-name cpu_util \
    --threshold 70.0 \
    --comparison-operator gt \
    --statistic avg \
    --period 60 \
    --evaluation-periods 3 \
    --alarm-action 'http://192.168.100.10:9001' \
    --query resource_id=INSTANCE_ID

提示:上述命令中的INSTANCE_ID需要替换成实际的实例ID

# 查看告警规则清单
[root@controller ~]# ceilometer alarm-list
+--------------------------------------+--------------+-------+----------+---------+------------+-------------------------------------+------------------+
| Alarm ID                             | Name         | State | Severity | Enabled | Continuous | Alarm condition                     | Time constraints |
+--------------------------------------+--------------+-------+----------+---------+------------+-------------------------------------+------------------+
| 3047034c-462a-461a-86ea-2c7446b3b024 | cpu_high_web | ok    | low      | True    | False      | avg(cpu_util) > 70.0 during 3 x 10s | None             |
+--------------------------------------+--------------+-------+----------+---------+------------+-------------------------------------+------------------+

CTRL-C关掉上一个python脚本,新建文件alarm_receiver.py,将以下代码保存到该文件,然后使用python alarm_receiver.py运行:

# -*- coding: utf-8 -*-
import BaseHTTPServer
import socket
import threading
import json
import datetime
import sys
import errno

# 尝试导入 msgpack
try:
    import msgpack
    HAS_MSGPACK = True
except ImportError:
    HAS_MSGPACK = False

# --- 配置 ---
UDP_PORT = 9000       
WEB_PORT = 9001       
DATA_STORE = []       
ALARM_STORE = []      

# --- 自定义静默服务器类 (用于屏蔽 Broken pipe 报错) ---
class QuietHTTPServer(BaseHTTPServer.HTTPServer):
    def handle_error(self, request, client_address):
        """重写错误处理方法,过滤掉 Broken pipe"""
        exc_type, exc_value, _ = sys.exc_info()
        # 如果是 socket 错误且错误码是 32 (Broken pipe),直接忽略
        if issubclass(exc_type, socket.error) and exc_value.errno == errno.EPIPE:
            return
        # 其他错误照常打印
        BaseHTTPServer.HTTPServer.handle_error(self, request, client_address)

# --- 线程1:UDP 接收 ---
def udp_server_thread():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('0.0.0.0', UDP_PORT))
    print "[UDP] 监听服务已启动 (Port: %s)" % UDP_PORT

    while True:
        try:
            data, addr = sock.recvfrom(65535)
            payload = None
            if HAS_MSGPACK:
                try: payload = msgpack.unpackb(data, encoding='utf-8')
                except: 
                    try: payload = msgpack.unpackb(data)
                    except: pass
            if payload is None:
                try: payload = json.loads(data)
                except: pass

            now = datetime.datetime.now().strftime("%H:%M:%S")
            if payload:
                if isinstance(payload, list):
                    for item in payload:
                        item['recv_time'] = now
                        DATA_STORE.insert(0, item)
                elif isinstance(payload, dict):
                    payload['recv_time'] = now
                    DATA_STORE.insert(0, payload)
            
            if len(DATA_STORE) > 50: del DATA_STORE[50:]
        except Exception as e:
            print "\nUDP Error:", e

# --- 线程2:Web 展示 + 告警接收 ---
class MonitorHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    
    def do_GET(self):
        if self.path == '/favicon.ico':
            self.send_response(204)
            return

        try:
            self.send_response(200)
            self.send_header('Content-type', 'text/html; charset=utf-8')
            self.end_headers()

            html = u"""
            <html>
            <head>
                <title>OpenStack 全栈监控</title>
                <meta http-equiv="refresh" content="3">
                <style>
                    body { font-family: "Microsoft YaHei", sans-serif; margin: 20px; }
                    table { border-collapse: collapse; width: 100%%; margin-bottom: 20px; }
                    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
                    .realtime-th { background-color: #007bff; color: white; }
                    .alarm-th { background-color: #dc3545; color: white; }
                    tr:nth-child(even) { background-color: #f2f2f2; }
                    .high { color: red; font-weight: bold; }
                    h2 { border-bottom: 2px solid #ccc; padding-bottom: 5px; }
                </style>
            </head>
            <body>
                <h2 style="color: #dc3545;"> 告警历史 (Alarm History)</h2>
                <table>
                    <tr>
                        <th class="alarm-th">触发时间</th>
                        <th class="alarm-th">告警名称</th>
                        <th class="alarm-th">原因 (Reason)</th>
                        <th class="alarm-th">状态</th>
                    </tr>
            """
            
            if not ALARM_STORE:
                html += u"<tr><td colspan='4' style='text-align:center'>暂无告警</td></tr>"
            else:
                for alarm in ALARM_STORE:
                    reason = alarm.get('reason', u'N/A')
                    if not isinstance(reason, unicode):
                        reason = str(reason).decode('utf-8', 'ignore')
                    html += u"""
                    <tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>
                    """ % (alarm.get('time'), alarm.get('name'), reason, alarm.get('state'))

            html += u"""
                </table>
                <h2 style="color: #007bff;"> 实时采样 (Real-time Samples)</h2>
                <table>
                    <tr>
                        <th class="realtime-th">接收时间</th>
                        <th class="realtime-th">监控项</th>
                        <th class="realtime-th">数值</th>
                        <th class="realtime-th">Resource ID</th>
                    </tr>
            """
            
            for item in DATA_STORE:
                name = item.get('counter_name') or item.get('name', 'N/A')
                vol = item.get('counter_volume') or item.get('volume', 0)
                rid = item.get('resource_id', 'N/A')
                time = item.get('recv_time', '')
                style = u'class="high"' if float(vol) > 80 else u""
                html += u"""
                    <tr><td>%s</td><td>%s</td><td %s>%s</td><td>%s</td></tr>
                """ % (time, name, style, vol, rid)

            html += u"</table></body></html>"
            
            self.wfile.write(html.encode('utf-8'))
        
        except socket.error:
            # 这里虽然捕获了,但 finish() 阶段可能还会抛出,会被 QuietHTTPServer 拦截
            pass

    def do_POST(self):
        try:
            content_length = int(self.headers['Content-Length'])
            post_data = self.rfile.read(content_length)
            
            post_data_str = post_data.decode('utf-8', 'ignore')
            payload = json.loads(post_data_str)

            now = datetime.datetime.now().strftime("%H:%M:%S")
            reason = payload.get('reason', u'N/A')
            if not isinstance(reason, unicode):
                reason = str(reason).decode('utf-8', 'ignore')

            alarm_info = {
                'time': now,
                'name': payload.get('alarm_name', u'Unknown'),
                'state': payload.get('current', u'alarm'),
                'reason': reason
            }

            ALARM_STORE.insert(0, alarm_info)
            if len(ALARM_STORE) > 10: del ALARM_STORE[10:]
            
            # 打印到终端,证明收到了
            print_msg = u"[ALARM] 收到告警: %s" % reason
            print print_msg.encode('utf-8')

            self.send_response(200)
            self.end_headers()
        except Exception as e:
            print "POST Error:", str(e)
            self.send_response(500)
            self.end_headers()

    def log_message(self, format, *args): return

if __name__ == '__main__':
    t = threading.Thread(target=udp_server_thread)
    t.setDaemon(True)
    t.start()
    
    # 使用 QuietHTTPServer 而不是 BaseHTTPServer.HTTPServer
    QuietHTTPServer.allow_reuse_address = True
    server = QuietHTTPServer(('', WEB_PORT), MonitorHandler)
    
    print "\n==========================================="
    print "全栈监控面板: http://<Controller_IP>:%d" % WEB_PORT
    print "UDP 监听端口: %d" % UDP_PORT
    print "HTTP 监听端口: %d" % WEB_PORT
    print "===========================================\n"
    
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print "Exit."

网站运行起来之后使用浏览器访问192.168.100.10:9001查看监控页面。

出发告警规则

告警规则要求是云主机的占用率持续超过70%,这里我们需要在云主机内执行一个while循环,在云主机的控制台内运行以下命令:

while true; do :;done

运行循环后云主机的CPU占用会持续超过70%,等待3分钟后浏览器上会产生一个告警记录。
作业4:等待数据产生,观察浏览器上是否出现告警记录,截图上传。