Global Request IDs

https://blueprints.launchpad.net/oslo?searchtext=global-req-id

构建一个复杂的资源,比如启动一个实例,不仅需要触及许多 Nova 进程,还需要触及其他服务,例如 Neutron、Glance,甚至可能还有 Cinder。当我们在这些服务之间跳转时,我们目前会生成一个新的请求 ID,这使得跟踪这些流程变得非常手动。

问题描述

当用户创建一个资源,例如服务器时,会收到一个请求 ID。这个 ID 在大多数服务的 paste pipeline 中很早就被生成。它最终被嵌入到 context 中,然后隐式地用于记录与该请求相关的所有活动。这对于跟踪请求在单个服务内部通过其 worker 传递时非常有效,但在操作跨越多个服务时就会失效。一个常见的例子是服务器构建,它需要 Nova 多次调用 Neutron 和 Glance(以及可能其他服务)才能在网络上创建服务器。

云环境通常拥有一个 ELK(Elastic Search、Logstash、Kibana)基础设施来消费它们的日志。查询这些流程的唯一方法是在所有相关消息中有一个共同的标识符。全局请求 ID 立即使现有的已部署工具更好地管理 OpenStack。

提议的变更

高级解决方案如下(具体细节稍后说明)

  • 接受请求中的入站 X-OpenStack-Request-ID header。要求它看起来像一个 uuid 以防止注入问题。将其设置为 global_request_id 的值

  • 保留自动生成的现有 request_id

  • 更新 oslo.log 以默认情况下也记录 global_request_id,当它在 context logging 模式中时。

Paste pipelines

传入请求的处理通过一组 paste pipelines 逐步进行。这些 pipelines 在各个项目中大多是通用的,但存在足够的本地变化,因此需要突出显示这对于基础 IaaS 服务来说是什么样子,这些服务将是此规范的初始目标。

Neutron [1]

[composite:neutronapi_v2_0]
use = call:neutron.auth:pipeline_factory
noauth = cors http_proxy_to_wsgi request_id catch_errors extensions neutronapiapp_v2_0
keystone = cors http_proxy_to_wsgi request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
#                                  ^                                 ^
# request_id generated here -------+                                 |
# context built here ------------------------------------------------+

Glance [2]

# Use this pipeline for keystone auth
[pipeline:glance-api-keystone]
pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken context  rootapp
#                                                                                      ^
# request_id & context built here -----------------------------------------------------+

Cinder [3]

[composite:openstack_volume_api_v3]
use = call:cinder.api.middleware.auth:pipeline_factory
noauth = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler noauth apiv3
keystone = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv3
#                                  ^                                                   ^
# request_id generated here -------+                                                   |
# context built here ------------------------------------------------------------------+

Nova [4]

[composite:openstack_compute_api_v21]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21
#                                  ^                                                       ^
# request_id generated here -------+                                                       |
# context built here ----------------------------------------------------------------------+

oslo.middleware.request_id

在几乎所有服务中,request_id 的生成都发生在很早的时候,远早于任何本地逻辑之前。中间件设置一个 X-OpenStack-Request-ID 响应 header,以及稍后被 oslo.context 消费的环境变量。

我们将接受一个入站的 X-OpenStack-Request-ID,并验证它看起来像 req-$UUID,然后才将其作为 global_request_id 接受。

返回的 X-OpenStack-Request-ID 将是现有的 request_id。这就像父进程在 fork() 调用中获取子进程 ID 一样。

oslo.context from_environ

幸运的是,大多数项目现在都使用 oslo.context from_environ 构造函数。这意味着我们可以向 context 添加内容,或调整 context,而无需更改每个项目。例如,在 Glance 中,context 构造函数看起来像 [5]

kwargs = {
   'owner_is_tenant': CONF.owner_is_tenant,
   'service_catalog': service_catalog,
   'policy_enforcer': self.policy_enforcer,
   'request_id': request_id,
}

ctxt = glance.context.RequestContext.from_environ(req.environ,
                                                  **kwargs)

由于所有日志记录都发生在 context 构建之后。所有 context 的必需部分都将在日志记录开始之前存在。

oslo.log

oslo.log 的默认设置应包括在 context logging 期间 global_request_id。这可以在后期完成,因为用户始终可以覆盖他们的 context logging 字符串格式。

projects and clients

在上述基础设施实现之后,在 python 客户端中保存和发出 global_request_id 将会是一个小的改变。例如,Nova 调用 Neutron 时,在 get_client 调用中 context.request_id 将被存储在客户端中。 [6]

def _get_available_networks(self, context, project_id,
                            net_ids=None, neutron=None,
                            auto_allocate=False):
    """Return a network list available for the tenant.
    The list contains networks owned by the tenant and public networks.
    If net_ids specified, it searches networks with requested IDs only.
    """
    if not neutron:
        neutron = get_client(context)

    if net_ids:
        # If user has specified to attach instance only to specific
        # networks then only add these to **search_opts. This search will
        # also include 'shared' networks.
        search_opts = {'id': net_ids}
        nets = neutron.list_networks(**search_opts).get('networks', [])
    else:
        # (1) Retrieve non-public network list owned by the tenant.
        search_opts = {'tenant_id': project_id, 'shared': False}
        if auto_allocate:
            # The auto-allocated-topology extension may create complex
            # network topologies and it does so in a non-transactional
            # fashion. Therefore API users may be exposed to resources that
            # are transient or partially built. A client should use
            # resources that are meant to be ready and this can be done by
            # checking their admin_state_up flag.
            search_opts['admin_state_up'] = True
        nets = neutron.list_networks(**search_opts).get('networks', [])
        # (2) Retrieve public network list.
        search_opts = {'shared': True}
        nets += neutron.list_networks(**search_opts).get('networks', [])

    _ensure_requested_network_ordering(
        lambda x: x['id'],
        nets,
        net_ids)

    return nets

注意

有些使用模式是构建一个客户端并将其保留用于长时间运行的操作。在这种情况下,我们希望更改模型,假设客户端是短暂的,并且应该在其流程结束时被丢弃。

这也有助于跟踪非用户发起的任务,例如为了信息刷新而触及其他服务的定期作业。

备选方案

Log in the Caller

之前有一个 OpenStack 跨项目规范来完全在调用者中处理这个问题 - https://review.openstack.org/#/c/156508/。它在两年前被合并,但尚未获得进展。

它有一些缺点。事实证明客户端代码在这里不太标准化,因此修复每个客户端需要大量的工作。

它还需要一些标准约定来将这些内容写入调用者侧的日志,这些约定在所有服务之间都是一致的。

允许人们使用 Elastic Search 来跟踪他们的日志(所有大型站点都有正在运行的)。需要构建一个自定义的分析工具。

Verify trust in callers

很久以前,在一个遥远的星系里,在一个我没有参加的峰会上,我被告知存在对客户端泛洪此字段的担忧。如果我们在严格验证入站数据的情况下,似乎没有可行的攻击。

我们可以使用 Service roles 来验证信任,但如果没有令人信服的理由,我们应该采取更简单的方法。

作为参考,Glance 已经接受用户提供的 64 个字符或更少的 request-id。这已经存在很长时间了,到目前为止还没有关于滥用的报告。我们可以考虑删除最后一个约束,而不进行角色验证。

Swift multipart transaction id

Swift 有一种相关的方法,它们的事务 ID 是一种 multipart ID,包括服务器在入站请求上生成的部分、时间戳部分、固定的服务器部分(用于跟踪多个集群)以及用户提供部分。Swift 目前不使用上述任何 oslo 基础设施,并将 syslog 作为其主要的日志记录机制。

虽然这种方法中有些有趣的部分,但鉴于 oslo 组件,它是一个不太直接的工作过渡。此外,oslo.log 具有许多结构化日志后端(如 json stream、fluentd 和 systemd journal),我们真正希望全局和本地作为单独的字段,因此不需要启发式解析。

Impact on Existing APIs

oslo.middleware request_id 合约将更改为接受入站 header 并设置第二个环境变量。两者都向后兼容。

oslo.context 将接受一个新的 local_request_id。这需要在所有接受 request_id 的调用中插入 local_request_id。这看起来完全向后兼容。

oslo.log 需要调整以支持记录两个 request_id。它应该默认启用,尽管 log_context 字符串是用户配置的变量,因此他们可以设置对他们有效的任何站点本地格式。发生这种情况时,升级发布说明会是合适的。

安全影响

之前曾担心信任来自用户的 request id。它是一个入站用户数据,因此应小心处理。

  • 确保不允许它变得太大而产生 DOS 向量(大小验证)

  • 确保它不是可能的代码注入向量(严格验证)

这些项目可以通过严格验证内容看起来像有效的 uuid 来处理。

性能影响

Minimal. 这是现有通过路径中的几行额外指令。这个新代码中没有进行昂贵的活动。

Configuration Impact

唯一的配置影响将是 oslo.log context 字符串。

开发人员影响

开发人员现在将更容易在他们的 devstack 环境中跟踪构建请求!

Testing Impact

由各种 oslo 组件提供的单元测试。

实现

负责人

主要负责人

sdague

其他贡献者

注意

绝对需要帮助才能通过这个关卡,这里有很多小的补丁需要正确处理。

里程碑

完成目标里程碑:TBD

工作项

待定

文档影响

TBD - 但可能是一些关于跨服务跟踪的运营商指南更新。

依赖项

参考资料

注意

本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode