统一和清理故障远程处理

https://blueprints.launchpad.net/oslo.messaging/+spec/failure-remoting

我们目前有几种将远程系统上发生的故障(异常 + 回溯信息)返回到其源头的方式。 这些不同的方式各有差异,使得每种解决方案都有效且适用于其问题领域。 为了鼓励统一,本规范将致力于提出一个提案,该提案可以结合两种实现方式的最佳方面,并抛弃它们的弱点,从而创建一个最佳实践的实现。

问题描述

一直以来都有希望能够序列化一个异常、异常类型以及关于异常原因(即其回溯)尽可能多的信息,当远程系统上的创建者失败到另一个系统时(通常通过某种 RPC 或 REST 或其他非本地接口传输)。 为简洁起见,让我们将 (exception_type, value, traceback) 的元组(通常由对 sys.exc_info 的某些调用创建)称为 故障 对象。 当在本地机器上并且故障在其自身进程内创建时,异常、其类及其回溯信息都得到原生支持,并且可以检查、输出、记录(通常使用 traceback 模块)、处理(通过 try/catch 块)和分析; 但是,当该异常在远程创建并发送到接收者时,故障的重现变得更加复杂,原因如下

  • 将回溯对象(通常包含对本地堆栈帧的引用)序列化为某种可序列化格式通常意味着重建的回溯信息将不会像在本地进程中创建时那样丰富,因为这些本地堆栈帧将不会存在于接收者进程中。 这意味着回溯序列化/反序列化是一个有损过程,因此远程异常的 traceback 模块不能使用,或者其生成的信息可能不准确。

  • 现在必须执行输入验证,以确保发送者创建的序列化格式实际上是有效的(这排除了使用 pickle 进行序列化/反序列化,因为它众所周知存在安全漏洞)。

  • 如果接收者希望从序列化版本重现异常对象,则必须可以访问用于创建原始异常的相同异常类型/类; 这可能并非总是可行的(取决于接收者 sys.path 中可访问的模块和类)。

  • 任何包含的异常值(通常是 string,但并不局限于此)都需要进行重建(这可能并非总是可行的,例如,如果原始异常值引用了某些本地文件句柄或其他不可序列化的对象,例如本地线程锁)。

现有方案

有几种已知的故障捕获、序列化和反序列化/重构实现。 让我们深入了解每种实现的工作方式,并分析每种方法的优点和缺点。

Oslo.privsep

来源

评论

  • 通过套接字通道发送类 + 模块名称 + 异常参数。

  • 丢弃回溯(在特权侧记录)。

  • 在非特权侧重新创建类对象,并使用发送的参数(并重新引发)(理想情况下,不会发生任何泄漏?)。

Oslo.messaging

来源

类似(相同?)的副本似乎在 nova 中(用于 cells?)

文档:未知

评论

序列化:是(到 json);异常的关键字参数从可选的异常属性 kwargs 中提取,异常的类名和模块名被捕获,最终数据格式为

data = {
    'class': cls_name,
    'module': mod_name,
    'message': six.text_type(exception),
    'tb': tb,
    'args': exception.args,
    'kwargs': kwargs
}

反序列化:是;先前的 json 数据加载为字典。

验证:否;当前未执行 jsonschema 验证。

重构:是(有局限性);从 data 字典中的 message 中加载异常消息,并与 tb 字典元素中的回溯信息连接,然后验证接收到的模块是否在提供的列表中,如果接收到的模块不允许,则会引发一个通用异常,该异常尝试封装接收到的故障。 此通用异常(保留回溯信息)通过以下方式创建:

oslo_messaging.RemoteError(data.get('class'), data.get('message'),
                           data.get('tb'))

否则,如果模块是允许的类型之一,则通过使用以下方式重新创建异常类对象:

klass = <load module and class and verify class is an exception type>
exception = klass(*data.get('args', []), **data.get('kwargs', {}))

然后,如果这可行,为了确保 __str____unicode__ 方法正确返回先前提到的 data 字典中的 message 键,会创建一个具有动态创建的函数的动态异常类型,该函数返回提供的 message; 然后,上述创建的 exception__class__ 属性被替换为这种新的动态异常类型(哇!)。

exc_type = type(exception)
str_override = lambda self: message
new_ex_type = type(ex_type.__name__ + _REMOTE_POSTFIX, (ex_type,),
                   {'__str__': str_override, '__unicode__': str_override})
new_ex_type.__module__ = '%s%s' % (module, _REMOTE_POSTFIX)
exception.__class__ = new_ex_type

如果不起作用,则 exception 将保持不变,而是将 exception.args 列表替换为新的 args 列表,该列表将 data 字典中的 message 作为其第一个条目(替换先前的 args 的第一个条目)。

注意事项

  • 在上述重构过程中似乎会丢失远程回溯信息(除非返回 RemoteError,后者不会丢失回溯信息,但会丢失原始类型 + 相关信息)。

  • 未捕获 chained 异常信息。

  • 复制(或其某些版本)到 nova cells(目前未知 nova 团队复制了哪个版本/sha)。

TaskFlow

来源

文档

评论

序列化:True;使用 to_dict 方法将异常(或 sys.exc_info 调用)转换为字典。 示例

>>> from taskflow.types import failure
>>> try:
...    raise IOError("I have broke")
... except Exception:
...    f = failure.Failure()
...
>>> print(json.dumps(f.to_dict(), indent=4, sort_keys=True))
{
    "causes": [],
    "exc_type_names": [
        "IOError",
        "EnvironmentError",
        "StandardError",
        "Exception"
    ],
    "exception_str": "I have broke",
    "traceback_str": "  File \"<stdin>\", line 2, in <module>\n",
    "version": 1
}

反序列化:True;从 json 加载到字典。

验证:True;使用 jsonschema 和模式

SCHEMA = {
    "$ref": "#/definitions/cause",
    "definitions": {
        "cause": {
            "type": "object",
            'properties': {
                'version': {
                    "type": "integer",
                    "minimum": 0,
                },
                'exception_str': {
                    "type": "string",
                },
                'traceback_str': {
                    "type": "string",
                },
                'exc_type_names': {
                    "type": "array",
                    "items": {
                        "type": "string",
                    },
                    "minItems": 1,
                },
                'causes': {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/cause",
                    },
                }
            },
            "required": [
                "exception_str",
                'traceback_str',
                'exc_type_names',
            ],
            "additionalProperties": True,
        },
    },
}

重构:True,当故障对象在本地引发时(当不使用序列化时)。 False,当使用 to_dict 序列化时; 不像 oslo.messaging 上面定义的过程,此对象而是用一个新的异常 WrappedFailure 包装原始异常,并公开其类型(字符串版本)信息及其回溯信息,并提供访问器和有用的方法(在故障类上定义)以用于内省目的。

注意事项

  • 捕获(并序列化和反序列化)chained 异常(作为嵌套的故障对象)。 如上述模式中的 causes 键所示(该键自我引用模式对象)。

Twisted

来源

文档

评论

示例

>>> from twisted.python import failure
>>> import pickle
>>> import traceback
>>> def blow_up():
...    raise ValueError("broken")
>>> try:
...    blow_up()
... except ValueError:
...    f = failure.Failure()
>>> print(f)
[Failure instance: Traceback: <type 'exceptions.ValueError'>: broken
--- <exception caught here> ---
<stdin>:2:<module>
<stdin>:2:blow_up
]
>>> f.raiseException()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 2, in blow_up
ValueError: broken
>>> f_p = pickle.dumps(f)
>>> f_2 = pickle.loads(f_p)
>>> f_2.raiseException()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 2, in raiseException
ValueError: broken
>>> print(f_2.tb)
None
>>> traceback.print_tb(f_2.getTracebackObject())
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 2, in blow_up

序列化:pickle 支持通过 __getstate__ 方法。 由于他们创建了一个大部分可用的框架信息替代方案,该框架信息存储了回溯信息,因此可以更好地与 traceback 模块集成(该模块访问该框架信息以尝试创建有用的回溯详细信息)。

反序列化:是,通过 pickle

验证:否(pickle 众所周知容易受到加载任意代码的攻击)。

重构:部分地,创建了一个类似框架的副本结构,该结构大部分像原始结构一样工作(但无法重新引发,但可以传递给 traceback 模块,使其函数似乎有效)。

提议的变更

创建一个新的库,https://pypi.ac.cn/project/failure(或其他更好的命名库),该库涵盖了上述 3-4 个模型的组合。

它主要提供一个 Failure 对象(如 taskflow 和 twisted 中提供的),作为其主要公开的 API。 该故障类将具有一个 __get_state__ 方法,以便可以对其进行腌制(在需要时),以及一个 to_dictfrom_dict,可用于 json 序列化和反序列化。 它还将具有内省 API(类似于 twisted 和 taskflow 提供的 API),以便可以以一种好的方式访问底层的异常信息。

以下是一些基本示例 API(已被证明有用):

@classmethod
def validate(cls, data):
    """Validate input data matches expected failure format."""

def check(self, *exc_classes):
    """Check if any of ``exc_classes`` caused the failure.

    ...

    """

def reraise(self):
    """Re-raise captured exception."""

@property
def causes(self):
    """Tuple of all *inner* failure *causes* of this failure.

    ...

    """

def pformat(self, traceback=False):
    """Pretty formats the failure object into a string."""

@classmethod
def from_dict(cls, data):
    """Converts this from a dictionary to a object."""

def to_dict(self):
    """Converts this object to a dictionary."""

def copy(self):
    """Copies this object."""

为了利用 oslo.messaging 中的重新引发功能,此类还应具有一个 reraise 方法,该方法可以尝试重新引发给定的故障(仅当它与给定的异常类型列表匹配时)。 它不会尝试动态创建 __str____repr__ 方法(oslo.messaging 中发生的代码操作),以避免这段代码的怪癖。 如果包含的故障与已知故障列表不匹配,则 reraise 将返回 false,并且不会重新引发任何内容(让调用者决定在这种情况下该怎么做,也许此时应该引发一个类似于 WrappedFailure 的通用异常?)。

使用 jsonschema 的验证逻辑将从 taskflow 中获取,并在反序列化时使用,以便可以在数据加载时(而不是数据访问时)更早地发现数据错误。

为了提供类似于 twisted 与 traceback 模块的集成(通过将 traceback 的内部格式转换为纯 python 对象表示),已经讨论过是否可以使用 traceback2 模块提供等效的功能,如果可以,则应使用它来实现类似的集成(如果集成还可以允许将这种纯 python 回溯和框架表示重新引发为实际的回溯,那会更好,但可能不是一个合理的期望)。

备选方案

保持多种变体,每种变体都有其自身的弱点和优点,而不是将它们统一到一个库中。

Impact on Existing APIs

理想情况下,没有,因为用户应该仍然获得相同的功能,但如果正确完成,他们将获得更有意义的回溯信息、对故障对象的更有意义的内省以及整体更好、更一致的故障。

安全影响

性能影响

N/A

Configuration Impact

N/A

开发人员影响

这将改善开发人员的生活。

Testing Impact

将故障代码放在自己的库中,可以轻松地对其进行模拟和测试(与将其深深嵌入 oslo.messaging 中,不容易进行测试/审查相比); 因此,总体而言,这将提高测试覆盖率(和整体代码质量)。

实现

负责人

主要负责人:harlowja

里程碑

完成目标里程碑:Mikita

工作项

  1. 创建骨架库。

  2. 将骨架上传到 gerrit 并集成到 oslo 管道中。

  3. 开始从 oslo.messaging 和 taskflow 中移动代码并重构,以开始形成这个新库; 使用来自 twisted 和 bolt-ons(和其他)的概念和经验来帮助使这个库成为最好的。

  4. 审查、编码和重复。

  5. 发布和集成。

  6. 删除较旧的死代码。

  7. 盈利!

孵化

N/A

文档影响

依赖项

参考资料

不适用(全部内联)

注意

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