VMOD-Varnish模块

对于您在VCL中可以做的所有事情,有一些事情是您不能做的。例如,在数据库文件中查找IP号。VCL提供内联C代码,您可以在那里做任何事情,但它不是一种解决此类问题的方便甚至可读的方法。

这就是VMOD的用武之地:VMOD是一个带有一些C函数的共享库,可以从VCL代码中调用这些函数。

例如:

import std;

sub vcl_deliver {
        set resp.http.foo = std.toupper(req.url);
}

“std”vmod是您使用Varnish获得的一个vmod,它将一直存在,我们将在其中添加“精品”函数,如上面显示的“Toupper”函数。Vmod_std(3)中记录了“std”模块的完整内容。

手册的这一部分是关于如何编写自己的VMOD,C和VCC之间的语言接口如何工作,在哪里可以找到贡献的VMOD等。这个解释将以“std”VMOD为例,手边有一个Varnish源码树可能是一个好主意。

VMOD目录

VMOD目录是为Varnish缓存编写的维护扩展的最新编译:

Vmod.vcc文件

您的VMOD和VCL编译器(“VCC”)和VCL运行时(“VRT”)之间的接口在vmod.vcc文件中定义,一个名为“vmodtool.py”的Python脚本将该文件转换为执行所有繁重工作的复杂的C数据结构。

Std vmods vmod.vcc文件如下所示:

$ABI strict
$Module std 3 "Varnish Standard Module"
$Event event_function
$Function STRING toupper(STRANDS s)
$Function STRING tolower(STRANDS s)
$Function VOID set_ip_tos(INT)

这个 $ABI 行是可选的。可能的值包括 strict (默认)和 vrt 。它允许指定vmod正在与受祝福的人集成 vrt 接口由提供 varnishd 或者在堆栈中走得更远。

根据经验,如果VMOD使用的不只是VRT(Varnish运行时),在这种情况下,它需要为准确的Varnish版本构建,请使用 strict 。如果它符合VRT,并且仅在向VRT API引入破坏性更改时才需要重新构建,请使用 vrt

这个 $Module 行给出了模块的名称、文档所在的手册部分以及描述。

这个 $Event LINE指定一个可选的“Event”函数,只要加载了导入此VMOD的VCL程序或转换到任何热、活动、冷或丢弃状态,就会调用该函数。下面是关于这一点的更多信息。

这个 $Function 行定义了VMOD中的三个函数,以及参数的类型,这可能是编写VMOD最难的地方,所以我们稍后将详细讨论这一点。

请注意,第三个函数返回空,这使得它成为VCL行话中的“过程”,这意味着它不能用于表达式、赋值的右侧等。相反,它可以用作主要操作,某些返回值的函数不能::

sub vcl_recv {
        std.set_ip_tos(32);
}

在vmod.vcc文件上运行vmodtool.py,将生成“vcc_if.c”和“vcc_if.h”文件,您必须使用它们来构建共享库文件。

忘掉vcc_if.c吧,除了你的Makefile,你永远不需要关心它的内容,你当然也不应该修改它,这会立即使你的保修失效。

但是vcc_if.h对您来说很重要,它包含您想要导出到VCL的函数的原型。

对于标准VMOD,编译后的vcc_if.h文件如下::

VCL_STRING vmod_toupper(VRT_CTX, VCL_STRANDS);
VCL_STRING vmod_tolower(VRT_CTX, VCL_STRANDS);
VCL_VOID vmod_set_ip_tos(VRT_CTX, VCL_INT);

vmod_event_f event_function;

这些是你们的C原型。请注意 vmod_ 函数名称上的前缀。

命名参数和缺省值

上面介绍的基本vmod.vcc函数声明语法使来自vCL的调用的所有参数都是强制的-这意味着它们需要按顺序给出。

将参数命名为::

$Function BOOL match_acl(ACL acl, IP ip)

允许使用命名参数以任何顺序从VCL调用,例如::

if (debug.match_acl(ip=client.ip, acl=local)) { # ...

命名参数也采用缺省值,因此对于此示例,来自调试vmod::

$Function STRING argtest(STRING one, REAL two=2, STRING three="3",
                         STRING comma=",", INT four=4)

唯一的论据 one 是必需的,因此以下所有内容都是来自vcl的有效调用:

debug.argtest("1", 2.1, "3a")
debug.argtest("1", two=2.2, three="3b")
debug.argtest("1", three="3c", two=2.3)
debug.argtest("1", 2.4, three="3d")
debug.argtest("1", 2.5)
debug.argtest("1", four=6);

C接口不随命名参数和缺省值而改变,参数保持位置不变,缺省值看起来与用户指定的值没有区别。

Note 该缺省值必须以本机C-type语法给出,见下文。作为特例, NULL 必须以下列方式给予 0

可选参数

Vmod.vcc声明还允许在方括号中使用可选参数,如::

$Function VOID opt(PRIV_TASK priv, INT four = 4, [STRING opt])

如果存在任何可选参数,则C函数原型看起来完全不同:

  • 只有 VRT_CTX 对象指针参数(仅用于方法)保持位置不变

  • 所有其他参数都作为C函数的最后一个参数在结构中传递。

参数结构很简单,vmod作者应该检查 vmodtool -生成 vcc_if.c 函数和结构声明的文件:

  • 对于每个可选参数,一个 valid_ argument 成员用于表示存在相应的可选参数。

    valid_ 无论其实际数据类型如何,argstruct成员都应仅用作真值。

  • 在参数结构成员中以相同的名称和相同的数据类型传递命名参数。

  • 未命名(位置)参数作为 arg n 使用 n 从1开始,并随着参数的位置递增。

对象和方法

Varnish还支持用于vmod的简单对象模型。对象和方法在VCC文件中声明为::

$Object class(...)
$Method .method(...)

对于vmod声明的对象类,然后可以在中创建对象实例 vcl_init { } 使用 new 声明::

sub vcl_init {
        new foo = vmod.class(...);
}

并在任何地方调用它们的方法(包括在 vcl_init {} 实例化之后)::

sub somewhere {
        foo.method(...);
}

没有什么可以阻止将方法命名为类似构造函数的方法,而这种方法的含义取决于vmod作者::

$Object foo(...)
$Method .bar(...)
$Method .foo(...)

对象实例表示为指向vmod实现的C结构的指针。Varnish只提供空间来存储对象实例的地址,并确保将正确的对象地址传递给实现方法的C函数。

  • 对象的作用域和生存期是

  • 对象只能在中创建 vcl_init {} 然后用Varnish调用它们的析构函数。 vcl_fini {} 已经完成了。

建议vmod作者理解 vmodtool -生成 vcc_if.c 文件:

  • $Object 声明、构造函数和析构函数必须实现

  • 构造函数使用后缀命名 __init ,永远都是 VOID 返回类型,并且在VCC声明的参数之前具有以下参数:

    • VRT_CTX 像往常一样

    • 返回所创建对象的地址的指针指针

    • 包含对象实例的VCL名称的字符串

  • 析构函数使用后缀命名 __fini ,永远都是 VOID 返回类型,并且只有一个参数,即指向对象地址的指针。析构函数应该清除存储在该指针指针中的对象的地址。

  • 方法将指向对象的指针作为参数获取

    这个 VRT_CTX

由于除了传递对象实例的地址之外,Varnish根本不参与管理对象实例,因此vmod需要实现管理实例的所有方面,特别是它们的内存管理。由于对象实例的生存期是VCL,因此它们通常将从堆中分配。

函数和方法范围限制

这个 $Restrict Stanza提供了一种方法来限制前面的vmod函数或方法的作用域,以便只能从受限的vcl调用点调用它们。它必须仅出现在 $Method$Function 并具有以下语法:

$Restrict scope1 [scope2 ...]

可能的作用域值包括: backend, client, housekeeping, vcl_recv, vcl_pipe, vcl_pass, vcl_hash, vcl_purge, vcl_miss, vcl_hit, vcl_deliver, vcl_synth, vcl_backend_fetch, vcl_backend_response, vcl_backend_error, vcl_init, vcl_fini

不推荐使用的别名

这个 $Alias Stanza提供了一种重命名函数或对象方法的机制,而无需删除先前的名称。这允许更改名称以保持兼容性,直到删除别名。

函数的语法为::

$Alias deprecated_function original_function

[description]

方法的语法为::

$Alias .deprecated_method object.original_method

[description]

这个 $Alias 节可以出现在任何地方,这允许将它们分组到手册中专门的“不推荐使用的”部分。可选描述可用于解释为什么重命名函数。

VCL和C数据类型

VCL数据类型是针对作业的,因此,例如,我们有“持续时间”和“标题”这样的数据类型,但它们都有某种C语言表示形式。以下是对它们的描述。

除PRIV类型外,所有类型都有typedef:vclint、vclreal等。

请注意,大多数非本机(C指针)类型是 const ,如果由vmod函数/方法返回,则假定它们是不可变的。换句话说,vmod must not 修改以前返回的任何数据。

当返回非本机值时,生成函数负责安排内存管理。或者通过稍后通过任何可用的方法释放结构,或者通过使用从客户端或后端工作区分配的存储。

ACL

C型: const struct vrt_acl *

在VCL中声明的命名ACL的类型。

BACKEND

C型: const struct director *

用于后端和定向器实施的类型。看见 写一部导演

BLOB

C型: const struct vmod_priv *

在VMOD函数之间传递随机内存位的不透明类型。

BODY

C型: const void *

仅在赋值的LHS上使用的类型,该赋值可以接受BLOB或可以转换为字符串的表达式。

BOOL

C型: unsigned

零表示错误,其他任何值都表示正确。

BYTES

C型: double

单位:字节。

一种存储空间,如1024字节。

DURATION

C型: double

单位:秒。

时间间隔时间间隔,如25秒

ENUM

VCC语法:ENUM{val1,val2,...}

VCC示例: ENUM { one, two, three } number="one"

C型: const char *

允许来自一组常量字符串的值。 Note C类型是字符串,而不是C枚举。

枚举将作为固定指针传递,因此不是字符串比较,而是 VENUM(name) 都是可能的。

HEADER

C型: const struct gethdr_s *

例如,这些是引用特定HTTP实体中的特定标头的VCL编译器生成的常量 req.http.cookieberesp.http.last-modified 。通过传递对头的引用,VMOD代码既可以读写有问题的头。

如果标头是作为字符串传递的,则VMOD代码只会看到值,而不会看到它的来源。

HTTP

C型: struct http *

对Header对象的引用为 req.httpbereq.http

INT

C型: long

一个我们所了解和喜爱的(长)整数。

IP

C型: const struct suckaddr *

这是一种不透明类型,请参见 include/vsa.h 我们在此类型上支持的基元的文件。

PRIV_CALL

看见 私有指针 下面。

PRIV_TASK

看见 私有指针 下面。

PRIV_TOP

看见 私有指针 下面。

PRIV_VCL

看见 私有指针 下面。

PROBE

C型: const struct vrt_backend_probe *

命名的独立后端探测定义。

REAL

C型: double

一个浮点值。

REGEX

C型: const struct vre *

这是具有VCL作用域的正则表达式的不透明类型。REGEX类型仅适用于由VCL编译器管理的正则表达式文字。有关动态正则表达式或复杂用法,请参阅 include/vre.h 文件。

STRING

C型: const char *

以NUL结尾的文本字符串。

可以为空,表示不存在的字符串,例如::

mymod.foo(req.http.foobar);

如果没有“foobar”HTTP头,将向vmod_foo()函数传递一个空指针作为参数。

STEVEDORE

C型: const struct stevedore *

存储后端。

STRANDS

C型: const struct strands *

Strands是在带有以下成员的结构中传递的字符串的列表:

  • int n :字符串数

  • const char **p :字符串数组,其中 n 元素

VMOD永远不应该停留在函数或方法执行之外的地方。看见 include/vrt.h 详情请看。

TIME

C型: double

单位:从UNIX纪元开始的秒数。

绝对时间绝对时间,如1284401161

VCL_SUB

C型: const struct vcl_sub *

VCL子例程上的不透明句柄。

对子例程的引用可以作为参数传递到VMOD中,并在以后通过 VRT_call() 。严格地说,范围是VCL:vmods必须确保 VCL_SUB 永远不能从不同的VCL调用引用。

VRT_call() 使递归调用的VCL失败,并且当 VCL_SUB 无法从当前上下文调用(例如,调用子例程访问 req 从后端)。

对于多次调用 VRT_call() 、VMODS must 检查是否 VRT_handled() 在两次调用之间返回非零值:被调用的Sub可能已返回一个操作(Any return(x) 不是普通的 return )或可能已使VCL失败,并且在这两种情况下,调用VMOD must 也可以返回,可能是在进行了一些清理之后。请注意,通过撤消处理 VRT_handling() 是个窃听器。

VRT_check_call() 可用于检查是否存在 VRT_call() 将会成功,以避免潜在的VCL故障。它又回来了 NULL 如果 VRT_call() 为什么不会发出调用或错误字符串。

VOID

C型: void

只能用于返回值,这使得该函数成为VCL过程。

私有指针

库函数维护本地状态通常很有用,这可以是从预编译的regexp到打开的文件描述符和大量数据结构的任何东西。

VCL编译器支持以下私有指针:

  • PRIV_CALL “per call”私有指针对于缓存/存储与特定调用或其参数相关的状态非常有用,例如特定于regSub()语句的已编译正则表达式,或者只是缓存一些开销较大的操作的最新输出。这些私有指针在加载的VCL的持续时间内有效。

  • PRIV_TASK 对于应用于对特定请求或后端请求的调用的状态,“每个任务”私有指针非常有用。例如,这可能是特定于客户端的解析Cookie的结果。请注意 PRIV_TASK 客户端和后端的上下文是分开的,因此在 vcl_backend_* 将产生与客户端使用的私有指针不同的私有指针。这些私有指针仅在其任务期间有效。

  • PRIV_TOP “每个顶级请求”私有指针在一个请求及其所有ESI-Include的持续时间内有效。它们仅为客户端定义。从后端VCL订阅使用时,可能会传递空指针并触发VCL故障。这些私有指针仅在其顶级请求的持续时间内有效

  • PRIV_VCL 对于应用于此VCL中的所有调用的全局状态,例如确定正则表达式在此vmod或类似中是否区分大小写的标志,“per vcl”私有指针非常有用。这个 PRIV_VCL 对象与传递给VMOD的事件函数的对象相同。该私有指针在加载的VCL的持续时间内有效。

    这个 PRIV_CALL Vmod_Priv在此之前完成 PRIV_VCL

它在vmod代码中的工作方式是 struct vmod_priv * 传递给函数,其中一个 PRIV_* 参数类型已指定。

该结构包含三个成员::

struct vmod_priv {
        void                            *priv;
        long                            len;
        const struct vmod_priv_methods  *methods;
};

这个 .priv.len 元素可以用于vmod代码想要使用它们的任何用途。

.methods 可以是指向回调结构的可选指针::

typedef void vmod_priv_fini_f(VRT_CTX, void *);

struct vmod_priv_methods {
        unsigned                        magic;
        const char                      *type;
        vmod_priv_fini_f                *fini;
};

.magic 必须初始化为 VMOD_PRIV_METHODS_MAGIC.type 应为描述性名称,以帮助调试。

.fini 将为非空值调用 .privstruct vmod_priv 当作用域以此结束时 .priv 指针作为其第二个参数,除了 VRT_CTX

使用Malloc(3)分配私有数据结构的常见情况如下:

static void
myfree(VRT_CTX, void *p)
{
        CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
        free (p);
}

static const struct vmod_priv_methods mymethods[1] = {{
        .magic = VMOD_PRIV_METHODS_MAGIC,
        .type = "mystate",
        .fini = myfree
}};

// ....

if (priv->priv == NULL) {
        priv->priv = calloc(1, sizeof(struct myfoo));
        AN(priv->priv);
        priv->methods = mymethods;
        mystate = priv->priv;
        mystate->foo = 21;
        ...
} else {
        mystate = priv->priv;
}
if (foo > 25) {
        ...
}

私有指针内存管理

上面介绍的通用的Malloc(3)/Free(3)方法适用于所有私有指针。它是最简单且不易出错的(只要通过fini回调正确释放了已分配的内存),但代价是调用堆内存分配器。

每个vmod常量数据结构可以分配给任何私有指针类型,但显然不能在它们上使用Free(3)。

动态数据存储在 PRIV_TASKPRIV_TOP 指针也可以来自工作区:

  • PRIV_TASK ,任何来自 ctx->ws 工作方式如下::

    if (priv->priv == NULL) {
            priv->priv = WS_Alloc(ctx->ws, sizeof(struct myfoo));
            if (priv->priv == NULL) {
                    VRT_fail(ctx, "WS_Alloc failed");
                    return (...);
            }
            priv->methods = mymethods;
            mystate = priv->priv;
            mystate->foo = 21;
            ...
    
  • PRIV_TOP 首先,请记住,它只能在客户端上下文中使用,因此vmod代码应该会出错 ctx->req == NULL

    对于动态数据, top request's 必须使用工作空间,这让事情变得有点复杂::

    if (priv->priv == NULL) {
            struct ws *ws;
    
            CHECK_OBJ_NOTNULL(ctx->req, REQ_MAGIC);
            CHECK_OBJ_NOTNULL(ctx->req->top, REQTOP_MAGIC);
            CHECK_OBJ_NOTNULL(ctx->req->top->topreq, REQ_MAGIC);
            ws = ctx->req->top->topreq->ws;
    
            priv->priv = WS_Alloc(ws, sizeof(struct myfoo));
            // ... same as above for PRIV_TASK
    

请注意,不需要释放工作区上的分配,它们的生存期是各自的任务。

私有指针和对象

PRIV_TASKPRIV_TOP 与普通的vmod函数一样,方法的参数不是每个对象实例的,而是每个vmod的。因此,需要对象实例的每个任务/每个顶级请求状态的VMOD需要实现其他方法来将存储与对象实例相关联。

这就是 VRT_priv_task() / VRT_priv_task_get()VRT_priv_top() / VRT_priv_top_get() 适用于:

非GET函数或者返回现有的 PRIV_TASK / PRIV_TOP 对于给定的 void * 争辩或创建一个。他们回来了 NULL 在分配失败的情况下。

这个 _get() 函数不会创建 PRIV_* ,但返回现有的或 NULL

按照约定,对象实例的私有指针是在对象的地址上创建的,如本例中的 PRIV_TASK **

VCL_VOID
myvmod_obj_method(VRT_CTX, struct myvmod_obj *o)
{
    struct vmod_priv *p;

    p = VRT_priv_task(ctx, o);

    // ... see above

这个 PRIV_TOP 案件看起来一模一样,除了呼唤 VRT_priv_top(ctx, o) 代替 VRT_priv_task(ctx, o) ,但请注意, VRT_priv_top*() 函数只能从客户端上下文调用(如果 ctx->req != NULL )。

事件函数

VMOD可以具有当加载或丢弃导入VMOD的VCL时调用的“事件”函数。这对应于 VCL_EVENT_LOADVCL_EVENT_DISCARD 事件,分别。此外,当VCL温度更改为冷或暖时,将调用此函数,对应于 VCL_EVENT_COLDVCL_EVENT_WARM 事件。

事件函数的第一个参数是VRT上下文。

第二个参数是特定于这个特定VCL的vmod_priv,如果需要,可以将特定于VCL的Vmod“fini”函数附加到它的“free”挂钩。

第三个参数是事件。

如果VMOD具有私有全局状态,包括任何打开的套接字或文件、分配给C代码中的全局变量或私有变量的任何内存等,则VMOD自己负责跟踪有多少VCL被加载或丢弃,并在计数达到零时释放该全局状态。

VMOD编写器是 strongly 鼓励在给定的VCL发出 VCL_EVENT_COLD 事件。您将有机会在VCL再次变为活动状态之前重新获取资源,并首先收到通知 VCL_EVENT_WARM 事件。除非用户决定给定的VCL应该始终是热的,否则非活动的VMOD最终会变冷,并且应该相应地管理资源。

成功时,事件函数必须返回零。初始化失败的可能性仅为 VCL_EVENT_LOADVCL_EVENT_WARM 事件。如果发生这样的故障,则 VCL_EVENT_DISCARDVCL_EVENT_COLD 事件将被发送到成功将其置于冷状态的VMOD。出现故障的VMOD将不会收到此事件,因此在发生故障时不能保持半初始化状态。

如果您的VMOD正在运行一个异步后台作业,您可以持有对VCL的引用,以防止它太快变冷,并获得与后端相同的保证,例如,对于正在进行的请求。为此,您必须通过调用 VRT_VCL_Prevent_Discard 当您收到一个 VCL_EVENT_WARM 后来又打电话给 VRT_VCL_Allow_Discard 一旦后台工作结束。收到一份 VCL_EVENT_COLD 是否提示终止任何绑定到VCL的后台作业。

您可以在vmod-debug::中找到VCL引用的示例

priv_vcl->vclref = VRT_VCL_Prevent_Discard(ctx, "vmod-debug");
...
VRT_VCL_Allow_Discard(&ctx, &priv_vcl->vclref);

在这个简化版本中,您可以看到至少需要一个VCL绑定的数据结构,如 PRIV_VCL 或VMOD对象来跟踪引用并在以后释放它。您还必须提供说明,如果用户尝试预热冷却VCL::

$ varnishadm vcl.list
available  auto/cooling       0 vcl1
active     auto/warm          0 vcl2

$ varnishadm vcl.state vcl1 warm
Command failed with error code 300
Failed <vcl.state vcl1 auto>
Message:
        VCL vcl1 is waiting for:
        - vmod-debug

在适当释放资源可能需要一些时间的情况下,您可以选择使用异步工作器,方法是生成一个线程并跟踪它,或者使用Varnish的工作器池。

何时锁定,何时不锁定

Varnish是高度多线程的,因此默认情况下,VMOD必须实现自己的锁定以保护共享资源。

当加载或卸载VCL时,事件和PRIV->FREE都在单个线程中顺序运行,并且保证不会有与该特定VCL相关的其他活动,也不会有任何其他VCL或VMOD中的init/fini活动。

这意味着VMOD init和任何对象init/fini函数已经按合理的顺序序列化,不需要任何锁定,除非它们访问特定于VMOD的全局状态,并与其他VCL共享。

也导入此VMOD的其他VCL中的流量将在内务处理进行时发生。

统计计数器

从Varnish 6.0开始,VMOD可以定义它们自己的计数器 varnishstat

如果您使用的是自动工具,请参阅 VARNISH_COUNTERS Varnish.m4中的宏,以获取有关设置构建的文档。

计数器在.vsc文件中定义。这个 VARNISH_COUNTERS 宏调用 vsctool.py 要把一个 foo.vsc 文件放入 VSC_foo.cVSC_foo.h 文件,就像 vmodtool.py 转身 foo.vcc vt.进入,进入 vcc_foo_if.cvcc_foo_if.h 档案。与VCC文件类似,生成的VSC文件提供了一个结构和函数,您可以在VMOD的代码中使用它们来创建和销毁您定义的计数器。这个 vsctool.py 工具还会生成一个 VSC_foo.rst 文件,您可以将其包括在文档中以描述您的VMOD拥有的计数器。

.vsc文件如下所示:

.. varnish_vsc_begin:: xkey
        :oneliner:      xkey Counters
        :order:         70

        Metrics from vmod_xkey

.. varnish_vsc:: g_keys
        :type:          gauge
        :oneliner:      Number of surrogate keys

        Number of surrogate keys in use. Increases after a request that includes a new key in the xkey header. Decreases when a key is purged or when all cache objects associated with a key expire.

.. varnish_vsc_end:: xkey

计数器可以具有以下参数:

类型

这是一种度量类型。可以是以下之一 countergauge ,或 bitmap

CTYPE

此计数器在C代码中将具有的类型。这只能是 uint64_t 并且不需要指定。

级别

此计数器的详细级别。 varnishstat 将仅显示比当前配置的详细级别更高的计数器。可以是以下之一 infodiag ,或 debug

眼线笔

对计数器的简短、一行描述。

群组

我不知道这是干什么用的。

格式

可以是以下之一 integerbytesbitmap ,或 duration

在这些参数之后,计数器可以有更长的描述,尽管该描述必须在.vsc文件中的一行上。

你应该打电话给 VSC_*_New() 加载您的VMOD并 VSC_*_Destroy() 当它被卸载时。请参阅生成的 VSC_*.h 文件,以获取有关包含计数器的结构的完整详细信息。