Varnish Cache 运维教程 / 第09章:VMOD 扩展开发
第09章:VMOD 扩展开发
9.1 VMOD 概述
VMOD(Varnish Module)是 Varnish 的扩展模块机制,允许通过 C 语言编写自定义功能并集成到 VCL 中。
9.1.1 VMOD 的作用
| 功能 | 说明 |
|---|---|
| 扩展 VCL 功能 | 实现 VCL 原生不支持的功能 |
| 性能优化 | C 实现比 VCL 逻辑更快 |
| 系统集成 | 与外部系统(数据库、缓存)交互 |
| 工具函数 | 提供字符串、日期、加密等工具函数 |
9.1.2 官方 VMOD 列表
| VMOD | 功能 | 说明 |
|---|---|---|
std | 标准库 | 基础工具函数 |
directors | 负载均衡 | Director 管理 |
cookie | Cookie 操作 | Cookie 解析和操作 |
header | 头部操作 | HTTP 头部处理 |
tcp | TCP 操作 | TCP 连接控制 |
vtc | 测试支持 | 测试辅助函数 |
blob | 二进制数据 | Blob 数据处理 |
digest | 摘要算法 | MD5/SHA 等哈希 |
geoip2 | 地理位置 | GeoIP2 数据库查询 |
bodyaccess | 请求体访问 | 读取请求体 |
saintmode | 圣人模式 | 故障后端隔离 |
xkey | 软清除 | 基于 key 的缓存清除 |
9.2 使用官方 VMOD
9.2.1 std VMOD
vcl 4.1;
import std;
sub vcl_recv {
# 字符串操作
set req.http.X-Lower = std.tolower(req.http.Host);
set req.http.X-Upper = std.toupper(req.url);
set req.http.X-Length = std.integer(req.http.Content-Length, 0);
# 时间操作
set req.http.X-Time = std.time("2026-01-01T00:00:00", now);
set req.http.X-Duration = std.duration("300s", 0s);
# 随机数
set req.http.X-Random = std.random(1, 100);
# 日志
std.log("Request received: " + req.url);
# IP 操作
set req.http.X-IP = std.ip(req.http.X-Forwarded-For, "0.0.0.0");
# 后端健康检查
if (!std.healthy(req.backend_hint)) {
set req.http.X-Backend-Status = "unhealthy";
}
# 文件操作
if (std.file_exists("/tmp/maintenance")) {
return (synth(503, "Maintenance mode"));
}
# 整数转字符串
set req.http.X-Port = std.port(server.ip);
}
sub vcl_backend_response {
# 时间解析
set beresp.http.X-Start-Time = std.time(beresp.http.X-Start-Time, now);
# 持续时间
set beresp.http.X-TTL-Seconds = std.duration(beresp.ttl, 0s);
}
9.2.2 cookie VMOD
vcl 4.1;
import cookie;
sub vcl_recv {
# 解析请求中的 Cookie
cookie.parse(req.http.Cookie);
# 获取特定 Cookie 值
set req.http.X-Session = cookie.get("session_id");
set req.http.X-User = cookie.get("user_id");
# 检查 Cookie 是否存在
if (cookie.isset("logged_in")) {
set req.http.X-Logged-In = "true";
}
# 清理 Cookie,只保留需要的
cookie.keep("session_id,user_id,csrf_token");
set req.http.Cookie = cookie.get_string();
# 删除特定 Cookie
cookie.delete("tracking_id");
set req.http.Cookie = cookie.get_string();
}
sub vcl_backend_response {
# 设置 Cookie
if (beresp.http.Set-Cookie ~ "session_id=") {
# 标记为会话 Cookie
cookie.parse(beresp.http.Set-Cookie);
}
}
9.2.3 digest VMOD
vcl 4.1;
import digest;
sub vcl_recv {
# MD5 哈希
set req.http.X-MD5 = digest.md5(req.url);
# SHA256 哈希
set req.http.X-SHA256 = digest.sha256(req.url);
# HMAC 签名
set req.http.X-HMAC = digest.hmac_sha256("secret-key", req.url);
# Base64 编码
set req.http.X-Base64 = digest.base64_encode(req.url);
# Base64 解码
set req.http.X-Decoded = digest.base64_decode(req.http.X-Base64);
}
sub vcl_deliver {
# 生成 ETag
set resp.http.ETag = "\"" + digest.md5(resp.http.Content-Length + obj.last_modified) + "\"";
}
9.2.4 header VMOD
vcl 4.1;
import header;
sub vcl_recv {
# 获取头部值
set req.http.X-Accept = header.get(req.http.Accept, "text/html");
# 追加头部值
header.append(req.http.X-Custom, "value1");
header.append(req.http.X-Custom, "value2");
# 头部存在检查
if (header.exists(req.http.Authorization)) {
set req.http.X-Auth = "present";
}
}
9.2.5 xkey VMOD(软清除)
vcl 4.1;
import xkey;
acl purge_allowed {
"localhost";
"192.168.0.0"/24;
}
sub vcl_recv {
# 使用 xkey 进行软清除
if (req.method == "PURGE") {
if (!client.ip ~ purge_allowed) {
return (synth(403, "Forbidden"));
}
if (req.http.xkey) {
# 按照 key 清除
set req.http.n-purged = xkey.purge(req.http.xkey);
return (synth(200, "Purged " + req.http.n-purged + " objects"));
}
}
}
sub vcl_backend_response {
# 设置缓存 key
if (beresp.http.X-Cache-Key) {
xkey.add(beresp.http.X-Cache-Key);
}
}
# 使用示例:
# curl -X PURGE -H "xkey: product-123" http://localhost:6081/
# 这会清除所有带有 product-123 key 的缓存对象
9.3 自定义 VMOD 开发
9.3.1 开发环境准备
# Ubuntu/Debian
sudo apt-get install -y \
varnish-dev \
python3 \
python3-docutils \
automake \
autoconf \
libtool \
pkg-config
# RHEL/CentOS
sudo dnf install -y \
varnish-devel \
python3 \
python3-docutils \
automake \
autoconf \
libtool \
pkgconfig
9.3.2 VMOD 项目结构
my-vmod/
├── src/
│ ├── vmod_my_module.c # C 源码
│ └── vmod_my_module.vcc # VCC 定义文件
├── autogen.des # 自动生成脚本
├── configure.ac # Autoconf 配置
├── Makefile.am # Automake 配置
└── README.md
9.3.3 VCC 定义文件
# src/vmod_my_module.vcc
$Module my_module 3 "My Custom VMOD"
# 函数声明
$Function STRING to_upper(STRING str)
$Function STRING to_lower(STRING str)
$Function STRING md5_hash(STRING input)
$Function INT random_range(INT min, INT max)
$Function BOOL is_valid_email(STRING email)
$Function STRING get_env(STRING name, STRING default)
# 子程序声明
$Function VOID init_session(PRIV_TASK session)
$Function STRING get_session_id(PRIV_TASK session)
9.3.4 C 源码实现
// src/vmod_my_module.c
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <openssl/md5.h>
#include "vdef.h"
#include "vrt.h"
#include "vre.h"
#include "vas.h"
#include "vcl.h"
// 字符串转大写
VCL_STRING
vmod_to_upper(VRT_CTX, VCL_STRING str)
{
char *result;
size_t len;
int i;
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
if (str == NULL)
return (NULL);
len = strlen(str);
result = WS_Alloc(ctx->ws, len + 1);
if (result == NULL)
return (NULL);
for (i = 0; i < len; i++)
result[i] = toupper(str[i]);
result[len] = '\0';
return (result);
}
// 字符串转小写
VCL_STRING
vmod_to_lower(VRT_CTX, VCL_STRING str)
{
char *result;
size_t len;
int i;
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
if (str == NULL)
return (NULL);
len = strlen(str);
result = WS_Alloc(ctx->ws, len + 1);
if (result == NULL)
return (NULL);
for (i = 0; i < len; i++)
result[i] = tolower(str[i]);
result[len] = '\0';
return (result);
}
// MD5 哈希
VCL_STRING
vmod_md5_hash(VRT_CTX, VCL_STRING input)
{
unsigned char digest[MD5_DIGEST_LENGTH];
char *result;
int i;
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
if (input == NULL)
return (NULL);
MD5((unsigned char *)input, strlen(input), digest);
result = WS_Alloc(ctx->ws, MD5_DIGEST_LENGTH * 2 + 1);
if (result == NULL)
return (NULL);
for (i = 0; i < MD5_DIGEST_LENGTH; i++)
sprintf(result + (i * 2), "%02x", digest[i]);
return (result);
}
// 随机数范围
VCL_INT
vmod_random_range(VRT_CTX, VCL_INT min, VCL_INT max)
{
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
return (min + rand() % (max - min + 1));
}
// 邮箱验证
VCL_BOOL
vmod_is_valid_email(VRT_CTX, VCL_STRING email)
{
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
if (email == NULL)
return (0);
// 简单验证:包含 @ 和 .
if (strchr(email, '@') == NULL || strchr(email, '.') == NULL)
return (0);
return (1);
}
// 获取环境变量
VCL_STRING
vmod_get_env(VRT_CTX, VCL_STRING name, VCL_STRING default_val)
{
const char *value;
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
value = getenv(name);
if (value == NULL)
return (default_val);
return (VRT_INT_string(ctx, value));
}
9.3.5 编译和安装 VMOD
# 生成配置文件
./autogen.des
# 配置
./configure
# 编译
make
# 安装
sudo make install
# 验证安装
varnishadm vmod.list
9.3.6 在 VCL 中使用自定义 VMOD
vcl 4.1;
import my_module;
sub vcl_recv {
# 使用自定义 VMOD 函数
set req.http.X-Upper = my_module.to_upper(req.url);
set req.http.X-Hash = my_module.md5_hash(req.url);
if (!my_module.is_valid_email(req.http.X-Email)) {
return (synth(400, "Invalid email"));
}
}
9.4 VMOD 开发最佳实践
9.4.1 内存管理
// 使用 Varnish 工作空间(推荐)
char *result = WS_Alloc(ctx->ws, size);
// 复制字符串到工作空间
char *result = WS_Printf(ctx->ws, "format: %s", value);
// 检查工作空间溢出
AN(WS_Allocated(ctx->ws, result));
9.4.2 错误处理
VCL_STRING
vmod_my_function(VRT_CTX, VCL_STRING input)
{
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
// 处理 NULL 输入
if (input == NULL) {
VRT_fail(ctx, "my_module: input is NULL");
return (NULL);
}
// 处理分配失败
char *result = WS_Alloc(ctx->ws, size);
if (result == NULL) {
VRT_fail(ctx, "my_module: workspace overflow");
return (NULL);
}
return (result);
}
9.4.3 线程安全
// VMOD 函数必须是线程安全的
// 避免使用全局变量
// 使用 PRIV_TASK 或 PRIV_TOP 存储请求级别的数据
struct priv_task {
unsigned magic;
#define PRIV_TASK_MAGIC 0x12345678
char session_id[64];
};
VCL_STRING
vmod_get_session_id(VRT_CTX, struct vmod_priv *priv)
{
struct priv_task *task;
CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
if (priv->priv == NULL) {
ALLOC_OBJ(task, PRIV_TASK_MAGIC);
AN(task);
// 生成 session ID
snprintf(task->session_id, sizeof(task->session_id),
"sess-%ld-%d", time(NULL), rand());
priv->priv = task;
priv->free = free;
} else {
CAST_OBJ(task, priv->priv, PRIV_TASK_MAGIC);
}
return (task->session_id);
}
9.5 调试 VMOD
9.5.1 日志输出
#include "vsl.h"
VCL_STRING
vmod_debug_function(VRT_CTX, VCL_STRING input)
{
// 使用 VSL 日志
VSLb(ctx->vsl, SLT_Debug, "my_module: input=%s", input);
// 使用 VRT_fail 记录错误
if (input == NULL) {
VRT_fail(ctx, "my_module: NULL input");
return (NULL);
}
return (input);
}
9.5.2 单元测试
# tests/my_module.vtc
varnishtest "Test to_upper function"
server s1 {
rxreq
txresp -body "Hello World"
} -start
varnish v1 -vcl+backend {
import my_module;
sub vcl_recv {
set req.http.X-Upper = my_module.to_upper("hello");
}
sub vcl_deliver {
set resp.http.X-Upper = req.http.X-Upper;
}
} -start
client c1 {
txreq
rxresp
expect resp.http.X-Upper == "HELLO"
} -run
9.5.3 运行测试
# 运行所有测试
make check
# 运行单个测试
varnishtest tests/my_module.vtc
# 调试模式
varnishtest -v tests/my_module.vtc
9.6 常用 VMOD 示例
9.6.1 URL 解析 VMOD
$Module urlparse 1 "URL Parsing VMOD"
$Function STRING get_scheme(STRING url)
$Function STRING get_host(STRING url)
$Function STRING get_path(STRING url)
$Function STRING get_query(STRING url)
$Function STRING get_fragment(STRING url)
$Function STRING get_param(STRING query, STRING name, STRING default)
// src/vmod_urlparse.c
#include <string.h>
#include <stdlib.h>
VCL_STRING
vmod_get_path(VRT_CTX, VCL_STRING url)
{
const char *p, *end;
char *result;
size_t len;
if (url == NULL)
return (NULL);
// 跳过 scheme
p = strstr(url, "://");
if (p != NULL)
p += 3;
else
p = url;
// 跳过 host
p = strchr(p, '/');
if (p == NULL)
return ("/");
// 找到路径结束位置
end = strchr(p, '?');
if (end == NULL)
end = strchr(p, '#');
if (end == NULL)
end = p + strlen(p);
len = end - p;
result = WS_Alloc(ctx->ws, len + 1);
if (result == NULL)
return (NULL);
memcpy(result, p, len);
result[len] = '\0';
return (result);
}
9.6.2 JSON 操作 VMOD
$Module json 1 "JSON VMOD"
$Function STRING get_value(STRING json, STRING key)
$Function STRING get_string(STRING json, STRING path)
$Function INT get_int(STRING json, STRING path, INT default)
$Function BOOL get_bool(STRING json, STRING path, BOOL default)
9.7 注意事项
重要
- VMOD 使用 Varnish 工作空间(Workspace),空间有限,避免大量分配
- VMOD 函数必须是线程安全的,不能使用全局状态
- 使用
CHECK_OBJ_NOTNULL验证对象有效性- 错误时使用
VRT_fail报告错误,而不是直接崩溃- VMOD 版本号需要与 Varnish 版本兼容
- 编译 VMOD 需要与运行时相同的 Varnish 开发库版本