使用 Python 扩展 GlusterFS

您是一位 Python 程序员,希望您的存储能够为您做更多事情吗?这里有一种简单的方法,可以使用您最喜欢的语言,为一个真正的分布式文件系统添加功能。
编程语言通常不是好的邻居。即使像 C 和 C++ 这样密切相关的语言混合使用,也常常会导致符号名称、初始化顺序和内存管理策略等方面产生混乱的约定冲突。随着语言之间距离的增加,集成它们的难度也随之增加。当尝试混合编译型语言和解释型语言时,情况尤其如此。大多数解释型语言都有方法调用编译库中的函数和访问符号,但这些工具通常远非方便,而且反过来调用——从编译代码到解释代码——则更不方便。解释型语言之间的集成甚至更不可行——一个值得注意的例外是共享 Java 虚拟机 (JVM) 的几种语言。使用不同虚拟机的解释型语言之间的互操作性通常仅限于不同进程之间的消息传递。
在这种背景下,Python 用于与其他语言编写的代码集成的工具就像一股新鲜空气。一种选择是 Jython,它非常舒适地存在于前面提到的 JVM 生态系统中。为了与编译代码集成,Python 提供了两种集成方法,而不是一种。第一种是“扩展 API”,它允许您用 C 编写 Python 模块。(“C”在这里用作任何编译代码的简写,这些代码都遵循最初为 C 定义的初始化和调用约定。)使用此接口,可以创建编译模块,这些模块提供本机 Python 模块的全部功能以及编译代码的全部性能。甚至还有像 Cython 这样的项目,可以为您生成大部分必要的“样板代码”。
Python ctypes 模块为与编译代码集成提供了更方便的选择,功能上只有很小的降低。使用 ctypes,Python 代码甚至可以调用 C 库中的函数和访问符号,即使这些库的作者从未考虑过 Python。Python 程序员还可以使用 ctypes 来解释 C 数据结构(在某种程度上与 struct 模块提供的功能重叠),甚至定义可以传递给 C 函数的 Python 回调。虽然使用 ctypes 不可能完成使用扩展接口可以完成的所有事情,但结合这两种方法可以产生非常强大的结果。
作为将 Python 代码与现有编译程序或语言相结合的案例研究,本文重点介绍为 GlusterFS 实现 Python “转换器”接口。GlusterFS 是一种现代分布式文件系统,它基于水平扩展的原则——通过添加更多基于商用硬件的服务器来为系统增加容量或性能,而不是不得不支付不断增加的溢价来使现有服务器更强大。开发由 Red Hat 赞助,但它是完全开源的,因此任何人都可以贡献。除了水平扩展之外,GlusterFS 的另一个核心原则是模块化。GlusterFS 中的大部分功能实际上是由转换器提供的——之所以称为转换器,是因为它们将来自用户的 I/O 调用(例如读取或写入)转换为相同或其他调用,然后传递到存储。这些调用从一个转换器传递到另一个转换器,以任意复杂的层次结构排列,直到最终在服务器的本地文件系统上执行最低级别的调用。为了简洁起见,我在这里将此接口称为 TXAPI,即使这不是官方术语。TXAPI 已被用于实现 GlusterFS 的内部功能,例如复制和缓存,以及外部功能,例如磁盘加密。
然而,本文主要不是关于 GlusterFS 的。尽管我使用 GlusterFS 来演示集成 Python 和 C 代码的技术,并展示结果以说明这种集成的潜在好处,但大多数技术同样适用于具有类似特征的其他程序。这些特征包括 C“顶层”调用 Python 而不是反过来,从根本上是多线程的执行模型,以及存在定义明确的插件接口 (TXAPI),该接口广泛使用双向回调。
GlusterFS 主要是一个 C 程序——毕竟文件系统是系统软件——这一事实意味着您不能将 ctypes 用于所有事情。为了引导您的集成,您需要使用 Python 的“嵌入 API”,它是前面提到的扩展 API 的近亲,允许 C 代码调用 Python 解释器。您至少需要调用此 API 一次以创建一个解释器并调用 Python 模块中的初始化函数。为此,您使用一个基于 C 的“元转换器”,它可以像以前一样加载转换器。此转换器称为 glupy,来自 GLUster 和 PYthon。(首选发音是“gloopy”,尽管考虑到这些来源,“glup-pie”可能更有意义。)glupy 的大部分工作是提供通用的嵌入 API 粘合代码来加载实际的 Python 转换器,该转换器被指定为一个选项。加载相当简单,只需调用 PyImport_Import
加载模块,然后调用 PyObject_CallObject
初始化它,如下所示(为清楚起见,省略了错误处理)
priv->py_module = PyImport_Import(py_mod_name);
Py_DECREF(py_mod_name);
py_init_func = PyObject_GetAttrString(priv->py_module, "xlator");
py_args = PyTuple_New(1);
/* "this" is the C pointer to this glupy instance */
PyTuple_SetItem(py_args,0,PyLong_FromLong((long)this));
priv->py_xlator = PyObject_CallObject(py_init_func, py_args);
Py_DECREF(py_args);
用户的 Python 初始化函数然后负责注册 TXAPI 回调以供稍后使用,以及其自身的特定于域的初始化。Glupy 还包括一个 Python/ctypes 模块,该模块封装了 GlusterFS 类型和一些 glupy 用户可以调用的函数(在示例中,这是使用“dl”句柄完成的)。
此时,您到达了一个岔路口。如果您已经在使用嵌入 API,为什么不继续使用它来完成几乎所有事情呢?在这种方法中,glupy 分派函数将使用 Py_BuildValue
构建参数列表,然后使用 PyObject_CallObject
从表中调用适当的 Python 函数/方法。手动编写这段代码非常繁琐,但大部分过程可以自动化。这种方法更大的问题是 TXAPI 涉及许多指向 GlusterFS 特定结构的指针,这些指针必须作为不透明的整数通过嵌入 API 传递。接收此类值的 Python 代码然后必须显式使用 from_address
将其转换为真正的 Python 对象。glupy 本身内部的混乱不是问题,但 glupy 用户代码内部的混乱使这种方法不太吸引人。
glupy 中实际使用的方法涉及较少的 C 代码和更多的 Python 代码,更强调 ctypes。在这种方法中,用户的 Python 代码不是作为 Python 函数而是作为 C 函数呈现的,使用 ctypes 定义函数类型,然后可以用作装饰器。不幸的是,ctypes 用于实现此类回调的特定于平台的外部函数接口的细节意味着,除了实际将其传递给 C 函数之外,无法获得 C 代码看到的实际函数指针。因此,您将 Python 回调对象传递给 glupy 注册函数,该函数可以看到此转换的结果。对于每种类型的操作,都有两个对应的注册函数:一个用于启动操作的分派函数,另一个用于处理完成的回调。然后,glupy 元转换器将指向注册函数的指针存储在一个表中,以便以后快速访问。这种方法的一个副作用是 glupy 函数是强类型的。这看起来可能相当不符合 Python 风格,但 TXAPI 本身是强类型的,并且混合类型可能会导致文件系统挂起,因此这似乎是一个合理的安全措施。尽管这一切看起来可能相当复杂,但最终结果是 Python 代码相对没有类型转换的混乱,并且只需要很少的初始化代码。例如,以下代码显示了我将要使用的示例的 init 函数,该示例为两种类型的操作注册分派函数和回调
def __init__ (self, xl):
dl.set_lookup_fop(xl,lookup_fop)
dl.set_lookup_cbk(xl,lookup_cbk)
dl.set_create_fop(xl,create_fop)
dl.set_create_cbk(xl,create_cbk)
下一个要解决的问题是多线程。Python 解释器本质上仍然是单线程的,因此调用 Python 的 C 代码必须确保获取全局解释器锁,并执行其他操作以保持解释器的正常运行。幸运的是,当前版本的 Python 使这比以前容易得多。您需要做的第一件事是在 Py_Initialize
之后调用 PyEval_InitThreads
来启用多线程。令人惊讶的是,似乎有很多人忽略了这一点,尽管它有相当完善的文档,PyEval_InitThreads
所做的工作之一是代表调用线程获取全局解释器锁。此锁必须在初始化结束时显式释放,否则任何其他尝试获取它的代码都将死锁。在这种情况下,此获取隐式地包含在对 PyGILState_Ensure
的调用中,这是从多线程 C 代码调用 Python 之前设置解释器状态的推荐方法。每个 glupy 分派函数和回调都会执行此操作,并在 Python 函数返回后匹配调用 PyGILState_Release
。
在从 glupy 内部的内容转移到 glupy 代码的外观之前,您需要了解此示例的基于 glupy 的转换器实际执行的操作。此示例尝试解决的问题是在使用 GlusterFS 存储 PHP Web 应用程序的代码时经常发生的问题。通常,此类应用程序在每次请求页面时都尝试加载数百个包含文件。每个包含文件可能存在于搜索路径中多个包含目录中的任何一个目录中。该示例缓存有关“肯定查找”(即那些成功的查找)的信息,但不缓存有关“否定查找”(即那些失败的查找)的信息。
尽管这种行为对于许多应用程序来说是有意义的,但对于许多 PHP 应用程序来说,性能影响可能是严重的。如果没有否定查找缓存,您很可能需要在找到包含每个包含文件的目录之前,徒劳地搜索其中一半的目录,每次请求包含页面时都是如此。(这种模式也发生在其他环境中,包括 Python Web 应用程序,但常见的 PHP 框架导致这些应用程序受到的影响最大。)正如影响是严重的,添加否定查找缓存的好处也可能是显着的。例如,C 版本的此类转换器将平均包含搜索时间减少了近七倍。Python 版本能做什么?
这是一个基于 glupy 的转换器的部分代码
@lookup_fop_t
def lookup_fop (frame, this, loc, xdata):
pargfid = uuid2str(loc.contents.pargfid)
print "lookup FOP: %s:%s" % (pargfid, loc.contents.name)
# Check the cache.
if cache.has_key(pargfid) and (loc.contents.name in cache[pargfid]):
dl.unwind_lookup(frame,0,this,-1,2,None,None,None,None)
return 0
key = dl.get_id(frame)
requests[key] = (pargfid, loc.contents.name[:])
dl.wind_lookup(frame,POINTER(xlator_t)(),loc,xdata)
return 0
这是被调用来查找文件的函数,这是此示例的核心功能。进入此函数表示从 C 到 Python 的转换,而其返回表示返回到 C 的转换。通过“dl”对象(支持 glupy 的 C 动态库的句柄)的调用也会在运行时暂停 Python 解释器。Python 装饰器语法允许您隐藏大部分函数类型细节,并且还值得注意的是缺少类型转换代码。大部分代码是特定于域的代码,而不是基础设施所需的样板代码。
在此函数的上半部分,您只需检查缓存,看看您是否已经知道请求的文件不会在那里。如果缓存检查成功,则查找立即失败,并且您“展开”转换器堆栈以报告该事实。与注册函数一样,每种操作类型都有其自己的特定 wind(向下调用)和 unwind(向上返回)函数。这表示从“Python 世界”到“C 世界”的临时返回,值得注意的是,在处理单个请求时,这些世界之间的转换可能会无缝地发生多次。特别是,常见的 GlusterFS 转换器习惯用法是,一个请求的完成回调启动下一个请求,如果该请求立即完成(如此处所示),那么您可以同时在堆栈上拥有多个请求和完成。
回到代码,如果您在缓存中找不到条目(并且您已经知道它一定不在标准肯定查找缓存中,否则您甚至不会被调用),则使用 wind_lookup
将请求传递给下一个转换器。当下一个转换器完成时,它将控制权返回(通过 glupy 元转换器)给 lookup_cbk
。在这里,您检索您的请求上下文,方便地由 lookup_fop
为您存储在字典中,并根据是否找到文件使用它来更新缓存。
关于此特定 glupy 转换器如何工作的还有一些其他不太相关的细节,但这确实是它的核心内容。使用不到一百行 Python 代码,包括注释和空行,您可以为一个真实的文件系统添加重要的功能。但是,它真的能很好地工作吗?事实证明,它工作得非常好;请参阅表 1。一个简单的测试表明,结果比相同事物的 C 版本慢,但仍然比基线快四倍以上。显然,您是否缓存这些结果比您使用什么语言来执行此操作更重要。
表 1. 缓存失败查找请求的结果毫秒/查找 | 最小值 | 平均值 | 最大值 | 第 99 百分位数 |
无缓存 | 0.368 | 6.898 | 16.286 | 9.702 |
C 版本 | 0.379 | 1.036 | 18.503 | 2.180 |
glupy 版本 | 0.381 | 1.527 | 21.163 | 2.916 |
尽管这些结果令人鼓舞,但它们更多的是一个开始而不是一个结束。Glupy 仍然是一个非常年轻的项目,还有很多工作要做。需要添加对几十种更多操作类型和几个数据结构的支持。GlusterFS 调用转换器和转换器本身调用的实用程序函数的方式仍然有很多。有很多方法可以使 glupy 接口更方便,并且无疑还有性能或并发问题需要解决。最重要的是,执行所有这些操作的基本基础设施已经存在,并且不仅仅适用于 GlusterFS 转换器。如果像这样一个高度多线程和异步的程序都可以利用 Python 提供的所有功能,那么几乎任何其他程序也可以。感谢 Python 的扩展/嵌入接口和 ctypes 模块,开发复杂软件的“两全其美”方法比大多数人想象的更可行。
资源Jython: https://jython.cn
Cython: http://www.cython.org
GlusterFS: http://www.gluster.org
Glupy 源代码仓库: https://github.com/jdarcy/glupy
C 语言的否定查找缓存转换器: https://github.com/jdarcy/negative-lookup
Zend (PHP) 框架关于包含文件: http://framework.zend.com/manual/1.12/en/performance.classloading.html