Open stack的计量计费功能(一)

2023-05-16

简要

针对于一些云计算的产品来讲,实现资源的计量/计费功能是一个比较大的工程,很多公司都是基于OpenStack开发自己的计费服务作为其产品化的一部分。本文主要是针对OpenStack的计量计费根据自己的学习认知做一个总结

文中涉及到的一些链接:
cloudkitty运用
Ceilometer的认知

一 如何实现

1.1 总述

完成资源的计费需要经过以下几个步骤:
在这里插入图片描述

  1. 步骤一:资源数据的收集工作,包含资源的使用对象(what),使用者(who),使用时间(when),使用量(how much)等基本信息,Ceilometer是专门为OpenStack环境提供一个获取和保存各种测量值的统一框架
  2. 步骤二:对【步骤一】的资源数据根据产品的计费策咯进行计费
  3. 步骤三:在【步骤一】【步骤二】已经完成资源的计费和存储的前提下,我们可以根据业务需求去合理的使用已经完成计费的数据,开发针对于业务层面的API
1.2 Ceilometer介绍

Ceilometer是专门为OpenStack环境提供一个获取和保存各种测量值的统一框架,明确目标,同时将其告警功能拆分到aodh项目,采集数据存储到gnocchi时间序列数据库,事件相关的服务拆分到panko项目,相关数据存储在MongoDB中

1.2.1 框架

在这里插入图片描述

  • Ceilometer:数据采集服务,采集资源使用量相关数据,并推送到Gnocchi,采集操作事件数据,并推送到Panko。
  • Gnocchi:时间序列数据库,保存计量数据。
  • Panko:事件数据库,保存事件数据。
  • Aodh:告警服务,基于计量和事件数据提供告警通知功能。
1.2.2 数据收集方式

数据收集方式从大方向上来看分为两种:主动/被动,下图为Ceilometer收集数据的方式,主要分为三大类:

  1. 通知【被动】:所有的OpenStack服务都会在执行了某种操作或者状态变化时发送通知消息到oslo-messaging(OpenStack整体的消息队列框架),一些消息中包含了计量所需要的数据,这部分消息会被Ceilometer的ceilometer-agent-notification服务组件处理,并转化为samples。通知数据采集方式是被动地采集计量数据。
  2. 轮询【主动】:Ceilometer中的服务组件根据配置定期主动地通过OpenStack服务的API或者其他辅助工具(如Hypervisors)去远端或本地的不同服务实体中获取所需要的计量数据;Ceilometer的轮询机制通过3种类型的代理实现,即ceilometer-agent-central、ceilometer-agent-compute和ceilometer-agent-ipmi服务组件。每种代理使用不同的轮询插件(pollster)从不同的命名空间来收集数据。
  3. REST ful API【被动】:用户可以通过调用RESTful API直接把利用其他方式获取的任意测量数据送达给Ceilometer。
    在这里插入图片描述
1.2.3 数据处理方式

Ceilometer获取到测量值数据后,会把它转化为符合某种标准格式的数据采样(Sample)通过内部总线发送给Notification agent。然后Notification Agent根据用户定义的Pipeline来对数据采样进行转换(Transform)和发布(Publish)。如果根据Pipeline的定义,这个数据采样(Sample)最后被发布给Collector的话,Collector会把这个数据采样保存在数据库中。Ceilometer的计量数据经过数据采集(agent)、数据处理(流水线数据转换及发布)、数据存储(Storage)。Gnocchi是一个多租户时间序列,计量和资源数据库。提供了REST API接口来创建和操作数据,用于超大规模计量数据的存储,同时向操作者和用户提供对度量和资源信息的访问。

1.3 CloudKitty 计费
1.3.1 框架

在这里插入图片描述
整体上Cloudkitty计费引擎以定时(CONF.collect.period)task的形式执行计费任务;每一轮计费任务之初要知道需要为哪些租户进行计费get_tenants(),再依次对每个租户的每一项服务进行计费;首先会通过collector模块从计量数据源中获得计费数据data;其次将数据交给计费模型根据定价规则完成费用计算;最后使用storage模块将费用数据持久化存储保存。

1.3.2 CloudKitty - orchestrator(数据处理)
    def run(self):
        LOG.debug('Started worker {}.'.format(self._worker_id))
        while True:
            self.tenants = self.fetcher.get_tenants()
            random.shuffle(self.tenants)
            LOG.info('[Worker: {w}] {n} tena nts loaded for fetcher {f}'.format(
                w=self._worker_id, n=len(self.tenants), f=self.fetcher.name))

            for tenant_id in self.tenants:

                lock_name, lock = get_lock(self.coord, tenant_id)
                LOG.debug(
                    '[Worker: {w}] Trying to acquire lock "{lck}" ...'.format(
                        w=self._worker_id, lck=lock_name)
                )
                if lock.acquire(blocking=False):
                    LOG.debug(
                        '[Worker: {w}] Acquired lock "{lck}" ...'.format(
                            w=self._worker_id, lck=lock_name)
                    )
                    state = self._check_state(tenant_id)
                    if state:
                        worker = Worker(
                            self.collector,
                            self.storage,
                            tenant_id,
                            self._worker_id,
                        )
                        worker.run()

                    lock.release()

            # FIXME(sheeprine): We may cause a drift here
            time.sleep(CONF.collect.period)

…\cloudkitty\orchestrator.py
以上代码是CloudKitty的run函数,从函数当中整个计费分为以下几个部分:

  1. 获取到租户信息
  2. 循环租户列表,检查计费情况
  3. 在Worker中进行数据收集,数据计量,数据存储操作
    def run(self):
        while True:
            # 判读是否到计费周期了,timestamp为起始周期,一般获取到的都是当前第一个计费周期,即该月的起止时间
            timestamp = self._check_state()
            if not timestamp:
                break

            #服务包括:compute,image,volume,network.bw.in,network.bw.out,network.floating
            metrics = list(self._conf['metrics'].keys())

            # Collection
            usage_data = self._do_collection(metrics, timestamp)

            frame = dataframe.DataFrame(
                start=timestamp,
                end=tzutils.add_delta(timestamp,
                                      timedelta(seconds=self._period)),
                usage=usage_data,
            )
            # Rating
            # 根据stevestore的插件功能,查询’cloudkitty.rating.processors’提供的计费策略,并进行计费,当前主要讲hashmap的计费代码
            for processor in self._processors:
                frame = processor.obj.process(frame)

            # Writing
            self._storage.push([frame], self._tenant_id)
            self._state.set_state(self._tenant_id, timestamp)

1.3.3 获取到租户的信息-Fetcher

在这里插入图片描述
目前支持五种获取租户的方式,除了PrometheusFetcher外每种方式里面都提供了获取租户的方法get_tenants
Openstack文档连接: https://docs.openstack.org/cloudkitty/latest/admin/configuration/fetcher.html

  1. GnocchiFetcher
    Gnocchi中存储了不同种类资源的数据
  2. KeystoneFetcher
    Keystone是默认的获取计费租户的方式,同时支持V2和V3版本。具体逻辑是检查Cloudkitty用户是否在某个tenant内并拥有rating角色。所以在使用Cloudkitty时,一般对于想要计费的租户需要执行命令’keystone user-role-add --user cloudkitty --role rating --tenant demo’,将Cloudkitty用户加入租户并赋予rating角色。
  3. MonascaFetcher
    Monasca:一个具有高性能,可扩展,高可用的监控即服务的(MONaas)解决方案。Monasca 是一个多租户监控即服务工具,可以帮助IT团队分析日志数据并设置告警和通知
  4. PrometheusFetcher
    Prometheus针对资源的监控收集到的数据,获取的是监控数据的对象作为租户
  5. SourceFetcher
    从配置文件中获取
1.3.4 获取数据-Controller

Open stack文档链接:https://docs.openstack.org/cloudkitty/latest/developer/collector.html
在这里插入图片描述
Controller的数据来源主要分为三个部分:

  1. GnocchiCollector
  2. MonascaCollector
  3. PrometheusCollector
    上面的三种收集方式中gnocchi为默认的数据收集方式,每种收集方式都会有一个fetch_all函数,该方法会收集到数据然后用在retrieve函数当中,最终返回统一的数据格式
    fetch_all函数的数据收集是依赖于/etc/cloudkitty/metrics.yml文件的,该文件中对要统计的资源进行了配置,数据的收集以及处理都依赖该配置文件
metrics:
  cpu:
    unit: instance  # metrics的计量单位
    alt_name: instance  # 对该计量项起了一个别名
    groupby:  # 数据分类的依据
      - id
      - user_id
      - project_id
    metadata:  # 数据计量的依据
      - flavor_name
      - flavor_id
      - vcpus
    mutate: NUMBOOL # 转化的数据类型
    extra_args: # 计量时所依赖的一些参数
      aggregation_method: mean
      resource_type: instance
      force_granularity:60  #代表着多少秒
      re_aggregation_method:rate:
      re_aggregation_method:sum  # 以防检索到的聚合需要重新聚合
      use_all_resource_revisions:True # 该配置默认为True 用在过滤数据的方法中作为判断依据(filter_unecessary_measurements)

配置文件中的mutate类型

def mutate(value, mode='NONE'):
    """Mutate value according provided mode."""

    if mode == 'NUMBOOL':
        return float(value != 0.0)

    if mode == 'FLOOR':
        return math.floor(value)

    if mode == 'CEIL':
        return math.ceil(value)

    return value

下面分析下GnocchiCollector的数据收集方式

  1. 根据metric_name在配置文件中获取该metric的全部配置
  2. 获取时间范围内metric的数据 ,如果该metric的extra_args中存在force_granularity>0并且re_aggregation_method.startswith(‘rate:’),则下面的方法会返回时间范围为【start - timedelta(seconds=force_granularity)】-【end】的数据
  3. 对2 获取到的数据进行处理,过滤掉不需要的数据,默认是返回全部
  4. 根据配置文件中的metadata获取资源的数据
  5. 进行数据的格式化处理
    def fetch_all(self, metric_name, start, end,
                  project_id=None, q_filter=None):
                  
		# 1 根据metric_name在配置文件中获取该metric的全部配置
        met = self.conf[metric_name]
		# 2 获取时间范围内metric的数据 ,如果该metric的extra_args中存在force_granularity>0并且re_aggregation_method.startswith('rate:'),则下面的方法会返回时间范围为【start - timedelta(seconds=force_granularity)】-【end】的数据
        data = self._fetch_metric(
            metric_name,
            start,
            end,
            project_id=project_id,
            q_filter=q_filter,
        )
		# 3 对2 获取到的数据进行处理,过滤掉不需要的数据,默认是返回全部
        data = GnocchiCollector.filter_unecessary_measurements(
            data, met, metric_name)
		
		# 4 根据配置文件中的metadata获取资源的数据
        resources_info = None
        if met['metadata']:
            resources_info = self._fetch_resources(
                metric_name,
                start,
                end,
                project_id=project_id,
                q_filter=q_filter
            )
		# 5 进行数据的格式化处理  
        formated_resources = list()
        for d in data:
            # Only if aggregates have been found
            LOG.debug("Processing entry [%s] for [%s] in timestamp ["
                      "start=%s, end=%s] and project id [%s]", d,
                      metric_name, start, end, project_id)
            if d['measures']['measures']['aggregated']:
                try:
                 # 在该方法中会进行单位的处理
                    metadata, groupby, qty = self._format_data(
                        met, d, resources_info)
                except AssociatedResourceNotFound as e:
                    LOG.warning(
                        '[{}] An error occured during data collection '
                        'between {} and {}: {}'.format(
                            project_id, start, end, e),
                    )
                    continue
                formated_resources.append(dataframe.DataPoint(
                    met['unit'],
                    qty,
                    0,
                    groupby,
                    metadata,
                ))
        return formated_resources
 @abc.abstractmethod
    def fetch_all(self, metric_name, start, end,
                  project_id=None, q_filter=None):
        ""
        Fetches information about a specific metric for a given period.

        This method must respect the ``groupby`` and ``metadata`` arguments
        provided in the metric conf at initialization.
        (Available in ``self.conf['groupby']`` and ``self.conf['metadata']``).

        Returns a list of cloudkitty.dataframe.DataPoint objects.

        :param metric_name: Name of the metric to fetch
        :type metric_name: str
        :param start: start of the period
        :type start: datetime.datetime
        :param end: end of the period
        :type end: datetime.datetime
        :param project_id: ID of the scope for which data should be collected
        :type project_id: str
        :param q_filter: Optional filters
        :type q_filter: dict
        ""

    def retrieve(self, metric_name, start, end,
                 project_id=None, q_filter=None):

        data = self.fetch_all(
            metric_name,
            start,
            end,
            project_id,
            q_filter=q_filter,
        )

        name = self.conf[metric_name].get('alt_name', metric_name)
        if not data:
            raise NoDataCollected(self.collector_name, name)

        return name, data
1.3.5 数据计量-Rating

Openstack地址:https://docs.openstack.org/cloudkitty/latest/user/rating/index.html
Cloudkitty当前的计费模型有三个,分别是noop,hashmap和pyscripts。

  1. noop模型为空,仅作为测试用;
  2. hashmap是当前Cloudkitty实用价值最高,最容易使用,最接近实际案例的计费模型
  3. pyscripts计费模型提供了使用python代码定制计费的接口,用户可以直接将含有计费逻辑的python脚本上传给cloudkitty实现定制化的计费,所以pyscripts计费模型使用门槛较高。
    在这里插入图片描述
    Hashmap Mapping 方式
resource_typeFlatratebase_price
基础型2001.2200*1.2=240
计算型4001.2400*1.2=480
存储型200200200

Hashmap Threshold 方式

resource_typeFlatratelevelbase_price
高效云盘200.920G20*0.9=18
高效云盘200.850G20*0.8=16
ESSD云盘400.850G40*0.8=32
1.3.6 数据存储-Stroge

Openstack地址:https://docs.openstack.org/cloudkitty/latest/developer/storage.html
在这里插入图片描述
Stroge存在V和V2两个版本,open stack中只对V2版本进行了描述,并表示该功能是不稳定的

计费模型计算出来的费用数据将由storage模块持久化存储下来,所需记录的核心字段包括begin,end,unit,qty,res_type,desc和tenant_id等,包含了时间信息,资源相关信息,属主信息。每个租户的各项服务费用数据会先缓存下来,最后再将当前租户的当前周期内所产生的费用数据一次性提交到存储后端。

Storage的具体实现过程包括:
(1)通过抽象方法get_time_frame从存储后端中的数据来确定下一个时间范围。
(2)通过append方法将费用数据记入提交缓存,期间可能会通过get_tenants函数和_dispatch函数做预处理或加工。
(3)通过commit函数将费用数据写入到后端存储持久化。这个过程会做_pre_commit,_commit,_post_commit一系列的工作确保数据被可靠地存储。
(4)对外提供get_total方法,返回费用情况。 sqlalchemy和gnocchi_hybrid的数据格式或者说表的结构都包括begin, end, unit, qty, res_type, rate, tenant_id和对资源描述相关的字段。实际上两种后端存储都是基于SQL实现的,所以随着时间的推移和云环境中需要计费资源的增加,费用数据的增加同样会出现之前类似ceilometer用SQL存储measurement数据带来性能瓶颈的问题。所以将费用数据存储在gnocchi中是必须的,详见https://review.openstack.org/#/c/319425/。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Open stack的计量计费功能(一) 的相关文章

  • 用 Python 解析化学公式

    我正在尝试解决这个问题 https leetcode com articles number of atoms approach 1 recursion accepted https leetcode com articles number
  • C++/C/Java:Anagrams - 从原始字符串到目标;

    我正在尝试解决这个问题 http uva onlinejudge org external 7 732 html http uva onlinejudge org external 7 732 html 对于给定的示例 他们给我们原始单词
  • 堆栈对象的“删除”行为是什么? [复制]

    这个问题在这里已经有答案了 int main Class Name t Class Name p t delete p return 0 这段代码在调用 2 个析构函数时执行得很好 删除如何处理堆栈对象 行为是否未定义 你遇到了未定义的行为
  • 运行时检查失败 #2 - 变量“x”周围的堆栈已损坏

    在以下代码中返回时 我收到此运行时检查失败 我相信类似的代码在程序的其他地方运行良好 有任何想法吗 String GetVariableName CString symbol CString filepath char acLine 512
  • Widcomm蓝牙:如何打开虚拟COM

    我正在尝试使用 Broadcomm 的 Widcomm 蓝牙堆栈 它应该可以工作 但有一件事我仍然无法理解 当我需要通信时 如何自动打开虚拟 COM 我正在尝试使用 SPP 串行端口配置文件 但 SDK 的文档并不是那么详尽 拜托 我就是不
  • 在Java中,为什么Stack是一个具体类,而Queue是一个接口?

    Queue 的哪一个子类是 普通 队列 1 java util Stack 是 Java 1 0 的遗留类 它早于 Collections 框架很多年 坦率地说 它是一个例子horrible多方面的设计 一切都不是事情应有的样子 主要问题是
  • 堆栈在缓存中吗?

    在现代计算机中 我知道当前代码区域位于高速缓存中 然而 在许多计算机语言实现中 本地 自动 变量将位于堆栈上 因此会对堆栈进行大量内存访问 在正常架构中 堆栈是否位于另一个缓存中 如果不是 则假设堆栈可以重新定位到 本地 即非常靠近当前代码
  • geom_text 仅位于堆积条形图的顶部

    我想仅在堆叠条形图的顶部添加标签 这是我的数据框 create data frame building lt c Burj nKhalifa Zifeng nTower Bank of nAmerica Tower Burj Al Arab
  • 使用 Stacks Java 将中缀转换为 Postfix

    我正在尝试编写一个程序将中缀表达式转换为后缀表达式 我正在使用的算法如下 1 Create a stack 2 For each character t in the expression If t is an operand append
  • 在C语言中,我可以通过堆栈指针访问另一个函数中主函数的局部变量吗?

    我需要访问在 main 函数中定义的变量 a 的值 而不将其作为参数传递 main int a 10 func printf d n a void func i need access of variable a here 我怎样才能做到这
  • C 函数堆栈布局

    我有一个看起来像这样的函数 int bof char str char buffer 12 strcpy buffer str return 1 我正在尝试覆盖其返回地址 我发现我可以通过使用来做到这一点 例如 memcpy buffer
  • NOP 雪橇如何工作?

    我找不到回答这个问题的好来源 我知道 nop sled 是一种用于规避缓冲区溢出攻击中堆栈随机化的技术 但我无法理解它是如何工作的 有什么简单的例子可以说明这种方法 128 字节 nop sled 等术语是什么意思 有些攻击包括使程序跳转到
  • 如何从 obj-c / ios 中的堆栈跟踪获取源代码行

    I use NSSetUncaughtExceptionHandler将堆栈跟踪打印到 iPhone 中的本地文件 该文件将在下次应用程序启动时发送到我们的服务器 然后我可以检查异常数据并修复错误 在某些崩溃中 我有模块名称和引发异常的函数
  • 如何避免将相同的片段添加到堆栈中

    我需要一些帮助 他们以这种方式将片段添加到活动中 问题是每次调用 openFragment 时都会创建片段并添加 这是显而易见的 问题 我做了什么修改 这样它只能添加一次片段 在下次使用相同片段标签的调用时 它将什么也不做 案例 第一次按下
  • 访问 Linux 线程(pthreads)的本地堆栈

    我目前正在实现一个使用多线程但对总内存消耗有要求的应用程序 我希望有一个主线程执行 I O 并有几个工作线程执行计算 目前 我在主堆栈上有几个可供工作人员访问的数据结构 我使用 OpenMP 进行工作分配 由于主 工作者模式不能很好地与 O
  • 为什么Python有最大递归深度?

    Python有最大递归深度 但没有最大迭代深度 为什么递归受到限制 把递归当成迭代来对待 而不限制递归调用的次数不是更自然吗 我只想说这个问题的根源来自于尝试实现流 参见这个问题 https stackoverflow com questi
  • 保存和恢复陷阱状态?管理多个陷阱处理程序的简单方法?

    有什么好的方法可以覆盖bash陷阱处理程序不会永久破坏可能已设置或尚未设置的现有处理程序 动态管理任意陷阱例程链怎么样 有没有办法保存陷阱处理程序的当前状态 以便以后可以恢复 在 Bash 中保存和恢复陷阱处理程序状态 我将提交以下堆栈实现
  • 将 DIV 堆叠在一起?

    是否可以堆叠多个 DIV 例如 div div div div div div div div div div 那么所有这些内部 DIV 都具有相同的 X 和 Y 位置吗 默认情况下 它们都在彼此下方 将 Y 位置增加了上一个 DIV 的高
  • 检测堆栈已满

    在编写 C 代码时 我了解到使用堆栈来存储内存是一个好主意 但最近我遇到了一个问题 我有一个实验 其代码如下所示 void fun const unsigned int N float data 1 N N float data 2 N N
  • Push 和 Pop 对堆栈意味着什么?

    长话短说 我的讲师很糟糕 他通过投影仪向我们展示前缀堆栈的中缀 他的大影子挡住了一切 所以我错过了重要的东西 他指的是push和pop push 0 pop x 他举了一个例子 但我根本看不出他是如何得到答案的 2 3 2 1 5 4 1

随机推荐