在 MySQL 中嵌入 Perl

作者:Brian Aker

尽管 MySQL 自带了丰富的功能集,但在某些时候,您会发现自己希望拥有一些额外的功能,或者需要一个高级的正则表达式引擎。为了解决这个问题,MySQL 支持用户自定义函数 (UDF)。通过 UDF 接口,您可以动态地将新函数加载到您的数据库中。虽然这是一个强大的功能,但这确实意味着需要花费时间来调试 C 或 C++ 代码。尽管我非常喜欢 C,但有时我没有时间编写和调试用它编写的应用程序,有时我想要一个更快的开发周期。Perl,这门语言界的瑞士军刀,拥有成千上万的模块,这要归功于 Comprehensive Perl Archive Network (CPAN)。将 Perl 嵌入到 MySQL 中,让我在快速扩展数据库方面有了很大的灵活性。出于这些原因,编写了嵌入式 MySQL Perl 解释器 MyPerl。

设置 Perl

将 Perl 放入数据库的第一步是为 Perl 获得正确的设置。默认情况下,Perl 不是线程安全的,而另一方面,MySQL 为每个用户连接使用一个线程。因此,为了让 Perl 驻留在数据库内部,您必须编译一个线程安全版本的 Perl

./Configure -Dusethreads -Duseithreads

一旦完成并构建,您将拥有一个线程安全的 Perl。这并不意味着您的代码或您使用的任何 Perl 模块将是线程安全的,它仅仅意味着 Perl 本身将是线程安全的。构建线程安全的 Perl 是一个必要的步骤,因为据我所知,目前没有供应商发布线程安全的 Perl。不要被 MyPerl 可以使用非线程 Perl 构建的事实所迷惑;它可以,但在某些时候它会使您的数据库崩溃。我怀疑随着 Apache 2.0 的出现和 mod_perl 2 的最终发布,一些供应商会考虑发布启用线程的 Perl 二进制文件。在完成本文的过程中,我最终升级到了 Red Hat 9,并看到他们已经开始发布启用线程的 Perl。

编写嵌入式 Perl 解释器

UDF 有三个阶段:init、request 和 deinit。init 阶段在查询开始时调用一次;request 阶段为返回的每一行调用一次,deinit 阶段在数据发送到客户端后调用。init 和 deinit 阶段都可以跳过,尽管对于除了最简单的 UDF 之外的所有 UDF,您都需要创建和清理您将用来向客户端返回数据的内存。

MyPerl 以以下 init 函数开始

my_bool
myperl_init(UDF_INIT *initid, UDF_ARGS *args,
            char *message)
{
    myperl_passable *pass = NULL;
    int exitstatus = 0;
    char *embedding[] = { "perl",
                          "-MMyPerl::Request",
                          "-e", "1" };
    PerlInterpreter *my_perl;
    uint i =0;
    initid->max_length = 256;
    initid->maybe_null=1;

三个参数被传递到 init 方法中,它返回成功或失败。UDF_INIT 结构保存控制 UDF 响应行为的信息。它也是在所有三个阶段之间传递的唯一结构。首先,MySQL 被告知 UDF 将发送回大于 VARCHAR 大小的数据。通过告诉服务器它需要比 VARCHAR 更多的空间,服务器假定它将返回 blobs。

虽然 MyPerl 不知道它实际上会这样做,但在这一点上,它无法知道它将返回多少数据,因此告诉服务器它正在返回 blobs 是更安全的选择。接下来,它将 maybe_null 设置为 1,因为始终有可能返回 NULL 值。MyPerl 为空结果和 eval() 代码中发生的编译错误都返回 NULL。

接下来要做的是检查传入的行

if (args->arg_count == 0 || i
    args->arg_type[0] != STRING_RESULT) {
    strncpy(message,USAGE, MYSQL_ERRMSG_SIZE);
    return 1;
}
for (i=0 ; i < args->arg_count; i++)
        args->arg_type[i]=STRING_RESULT;

MyPerl 期望传入的第一行是它将执行的代码。因此,如果没有传入行,或者第一行不是字符串,则应该发生错误。错误消息的最大大小必须为 MYSQL_ERRMSG_SIZE,并且必须将其复制到消息字符串中。为了节省一些时间,MyPerl 遍历 MySQL 将要传入的参数,并告诉它将它们转换为字符串。

在设置 Perl 解释器之前,必须创建一个结构来存储解释器并跟踪一个内存块,该内存块将用于为每个请求返回数据

pass = (myperl_passable *)
    malloc(sizeof(myperl_passable));;
if (!pass) {
    strncpy(message, "Could not allocate memory",
            MYSQL_ERRMSG_SIZE);
    return 1;
}

结构如下

typedef struct {
    char *returnable;
    size_t  size;
    PerlInterpreter *myperl;
    size_t  messagesize;
} myperl_passable;

char 指针 returnable 用于存储内存块,size 和 messagesize 用于跟踪 returnable 数据的总大小和当前大小。由于创建和销毁内存的成本非常高,因此将此降至最低非常重要。Perl 解释器也将存储在这个结构中。

在这一点上,必须完成设置将用于查询的 Perl 解释器的工作。目前,MyPerl 为每个请求创建一个新的 Perl 解释器,以防止内存泄漏并确保请求之间的数据安全。将来这种情况很可能变成 Perl 解释器池

if((my_perl = perl_alloc()) == NULL) {
    strncpy(message, "Could not allocate perl",
            MYSQL_ERRMSG_SIZE);
    return 1;
}
perl_construct(my_perl);
exitstatus = perl_parse(my_perl, xs_init, 4,
                        embedding, environ);
PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
if (exitstatus) {
    strncpy(message, "Error in creating perl parser",
            MYSQL_ERRMSG_SIZE);
    goto error;
}
exitstatus = perl_run(my_perl);
if (exitstatus) {
    strncpy(message, "Error in parsing your perl",
            MYSQL_ERRMSG_SIZE);
    goto error;
}

第一个函数 perl_alloc() 分配一个新的 Perl 解释器,然后使用 perl_construct() 构建它。现在只需启动 Perl 即可。embedding 变量用作 Perl 解释器的参数。这些参数与您在命令行上使用的参数完全相同。在处理 Perl 解释器的每个点都必须检查错误。在当前设计中,如果发生错误,MyPerl 会跳转到一系列函数调用,这些调用将清理已分配的内存。

现在存在一个良好的 Perl 解释器,需要在 pass 结构中设置一些默认值;必须存储解释器,并且结构的地址必须存储在 initid->ptr 指针中,以便在整个查询中使用它

pass->returnable = NULL;
pass->size = 0;
pass->messagesize = 0;
pass->myperl = my_perl;
initid->ptr = (char*)pass;
return 0;

完成所有这些设置后,MyPerl 准备好开始接受请求

char *
myperl(UDF_INIT *initid, UDF_ARGS *args,
       char *result, unsigned long *length,
       char *is_null, char *error)
{
    myperl_passable *pass =
        (myperl_passable *)initid->ptr ;
    char *returnable = NULL;
    unsigned long x = 0;
    size_t size = 0;
    char *newspot = NULL;
    char *string = NULL;
    myperl_passable *pass =
        (myperl_passable *)initid->ptr ;
    STRLEN n_a; //Return strings length
    PerlInterpreter *my_perl = pass->myperl;

重要的是,解释器的地址被复制到一个名为 my_perl 的变量中。许多 Perl 内部结构都基于宏,这些宏期望您使用具有特定名称的变量。STRLEN 是 Perl 使用的一种变量类型,用于存储字符串的大小。

此时调用解释器

dSP;
ENTER;
SAVETMPS;
PUSHMARK(SP);
// Now we push the additional values into ARGV
for(x = 0; x < args->arg_count  ; x++) {
    XPUSHs(sv_2mortal(newSVpvn(args->args[x],
           args->lengths[x])));
}
PUTBACK;
call_pv("MyPerl::Request::handler", G_SCALAR);
SPAGAIN;
string = POPpx;
size = (size_t)n_a;

XPUSHs 用于将所有行推送到字符串数组中,该数组将传递给库 MyPerl::Request() 中的 Perl 函数 handler()。这个 Perl 模块类似于 Apache::Request 模块,不同之处在于 Apache 模块使用文件名来跟踪它 eval() 的代码,而 MyPerl 使用代码本身来确定这一点。

由于名为 size 的变量现在保存了将要返回的数据的大小,因此需要为其分配空间

if (size) {
    if(pass->size < size) {
        newspot = (char *)realloc(pass->returnable,
                                  size);
        if(!newspot) {
            error[0] = '1';
            returnable =  NULL;
            goto error;
        }
        pass->size = size;
        pass->returnable = newspot;
    }
    // Always know the current size,
    // it may be less than the full size
    pass->messagesize = size;
    memcpy(pass->returnable, string, size);
} else {
    is_null[0] = '1';
}

error:
PUTBACK;
FREETMPS;
LEAVE;
*length = pass->messagesize;

return pass->returnable;
}

这是存储需要发送到服务器的信息的地方。如果需要分配更多内存,则使用内存调用 realloc()。如果未从 Perl 解释器接收到数据,则 is_null 设置为 1,以便 MySQL 知道应该向客户端返回空结果。MyPerl 还确保清理可能已用于 call_pv() 函数的内存。

现在为每一行调用 myperl() 函数。在 MySQL 将其数据返回给客户端后,它会调用 deinit 函数来释放解释器并释放任何已分配的内存

void myperl_deinit(UDF_INIT *initid)
{
    myperl_passable *pass =
        (myperl_passable *)initid->ptr ;
    perl_destruct(pass->myperl);
    perl_free(pass->myperl);
    free(pass->returnable);
    free(initid->ptr);
}

使用 MyPerl 的示例

现在存在一个 Perl UDF,可以执行像这样的简单技巧

mysql> select myperl('return $ARGV[0]',User)
       from mysql.user;

正如您所看到的,每一行都对应于 @ARGV 中的一个值。您还可以将 MyPerl 与 CPAN 模块一起使用来直接输入数据。此示例获取 URL 列表的内容并将内容插入到数据库中

mysql> insert into html select
       myperl("use LWP::Simple;
       my $content = get($ARGV[0]);
       return $content", url) from urls;

使用像 XML::Simple 和 XML::XPath 这样的模块,您甚至可以查询您可能存储在数据库中的任何 XML。我使用 MyPerl 快速调试我存储在数据库中的序列化 Perl 对象。

但是 GROUP BY 呢?

虽然以上演示了如何使用此代码处理行请求,但它不适用于使用 GROUP BY 将数据视为集合的查询。因此,还有一种称为聚合的 UDF 类型。聚合与其更普通的表亲的不同之处在于它有两个额外的阶段,reset 和 add。使用聚合 UDF,add 函数处理每一行,request 阶段整理结果并将数据发送到客户端。reset 阶段在每个数据集的开头调用,因此保证至少调用一次。MyPerl 目前有一个聚合 UDF,但其设计仍不稳定。

结论

通过将 Perl 嵌入到 MySQL 中,您可以在数据库中执行的操作范围得到了扩展。虽然通常最好保持数据库尽可能简单,但您可能会发现在某些情况下这是不切实际的。想象一下,必须从数据库中提取千兆字节的文本并将其发送给客户端以供使用。花费在发送数据上的时间将是相当可观的;能够直接在数据库中使用 Perl 处理数据可能会节省大量时间。能够利用 Perl 高级的正则表达式可能会让您用其他没有良好正则表达式支持的语言编写简单的客户端。我相信您会在自己的环境中找到许多用途。MyPerl 可以在 software.tangent.org 找到,以及您可以使用的其他 UDF 作为编写您自己的 UDF 的示例。

Brian Aker (brian@tangent.org) 将他的时间用于开发 MySQL 和 Apache 模块,其中包括 mod_layout 和 Apache 流媒体服务模块 mod_mp3。他最近与人合著了 O'Reilly 的 Running Weblogs with Slash 一书。多年来,他曾在 Slashdot 工作,现在在 MySQL AB 担任高级软件架构师。他还在华盛顿大学教授 Perl 认证课程。他和他的狗 Rosalynd 一起住在华盛顿州西雅图市。

加载 Disqus 评论