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成员都应仅用作真值。在参数结构成员中以相同的名称和相同的数据类型传递命名参数。
未命名(位置)参数作为
argn 使用 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.cookie或beresp.http.last-modified。通过传递对头的引用,VMOD代码既可以读写有问题的头。如果标头是作为字符串传递的,则VMOD代码只会看到值,而不会看到它的来源。
- HTTP
C型:
struct http *对Header对象的引用为
req.http或bereq.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可能已返回一个操作(Anyreturn(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_CALLVmod_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 将为非空值调用 .priv 的 struct 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_TASK 和 PRIV_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_TASK 和 PRIV_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_LOAD 和 VCL_EVENT_DISCARD 事件,分别。此外,当VCL温度更改为冷或暖时,将调用此函数,对应于 VCL_EVENT_COLD 和 VCL_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_LOAD 或 VCL_EVENT_WARM 事件。如果发生这样的故障,则 VCL_EVENT_DISCARD 或 VCL_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.c 和 VSC_foo.h 文件,就像 vmodtool.py 转身 foo.vcc vt.进入,进入 vcc_foo_if.c 和 vcc_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
计数器可以具有以下参数:
- 类型
这是一种度量类型。可以是以下之一
counter,gauge,或bitmap。- CTYPE
此计数器在C代码中将具有的类型。这只能是
uint64_t并且不需要指定。- 级别
此计数器的详细级别。 varnishstat 将仅显示比当前配置的详细级别更高的计数器。可以是以下之一
info,diag,或debug。- 眼线笔
对计数器的简短、一行描述。
- 群组
我不知道这是干什么用的。
- 格式
可以是以下之一
integer,bytes,bitmap,或duration。
在这些参数之后,计数器可以有更长的描述,尽管该描述必须在.vsc文件中的一行上。
你应该打电话给 VSC_*_New() 加载您的VMOD并 VSC_*_Destroy() 当它被卸载时。请参阅生成的 VSC_*.h 文件,以获取有关包含计数器的结构的完整详细信息。