listmonk:Go 邮件营销平台,单二进制如何支撑百万级订阅者


listmonk:Go 邮件营销平台,单二进制如何支撑百万级订阅者

点击上方蓝色“Go语言中文网”关注,每天一起学 Go

邮件营销是一个古老的互联网业务——1998 年的 Mailchimp 到今天还在做。但当你想自托管一个邮件列表管理系统时,选择并不多:Mailchimp 按订阅者收费(10 万订阅者每月 $500+),SendGrid 按邮件量收费,Brevo(原 Sendinblue)类似。更关键的是,你的订阅者数据、打开率、点击率全部存在别人的服务器上。

listmonk 解决的是自托管 newsletter + 邮件列表管理的问题。Go 单二进制 + PostgreSQL,支持百万级订阅者、并发发送、模板引擎、退信处理、多用户权限、双确认订阅、实时分析。19,600+ Star,AGPL v3 协议,Zerodha(印度最大券商)内部孵化。

安装

# Docker Compose 一键部署
curl -LO https://github.com/knadh/listmonk/raw/master/docker-compose.yml
docker compose up -d
# 访问 http://localhost:9000

# 或直接下载二进制
./listmonk --new-config   # 生成 config.toml
# 编辑 PostgreSQL 连接信息
./listmonk --install      # 初始化数据库
./listmonk                # 启动

零依赖——只要一个 PostgreSQL 数据库。

五层架构

listmonk 的架构是清晰的分层设计:

┌──────────────────────────────────┐
│  Frontend (Vue + Buefy)          │  管理界面
├──────────────────────────────────┤
│  HTTP API (Echo v4)              │  REST 端点
├──────────────────────────────────┤
│  Business Logic (core.Core)      │  CRUD + 查询
├──────────────────────────────────┤
│  Campaign Engine (manager)       │  异步发送引擎
├──────────────────────────────────┤
│  Data (PostgreSQL + SQL)         │  持久化
└──────────────────────────────────┘

核心组件:

组件
职责
core.Core
订阅者/列表/活动/模板/用户的 CRUD
manager.Manager
活动发送引擎(异步、批量、并发)
bounce.Manager
退信处理(Webhook + POP3)
subimporter.Importer
CSV/ZIP 批量导入
auth.Auth
认证(密码 + OIDC + API Token)
media.Store
媒体存储(本地 + S3)
models.Queries
预编译 SQL 语句

邮件发送引擎(Campaign Pipeline)

这是 listmonk 的技术核心——如何高效地把一封邮件发给百万级订阅者。

执行流水线

Manager 每 5 秒轮询 DB
    │
    ├── 发现 status=running 的 campaign
    │
    ├── 创建 pipe(处理管线)
    │   ├── 按 batch_size(默认 1000)分批取订阅者
    │   ├── 用 last_subscriber_id 做游标(不跳不重)
    │   ├── 模板渲染(Sprig 模板函数 + 个性化替换)
    │   └── 推入发送队列
    │
    └── Worker goroutine 池(默认 10 个)
        ├── 从队列取消息
        ├── 通过 SMTP 连接池发送
        ├── 滑动窗口限流(默认 10000/hour)
        └── 错误计数,超过阈值自动暂停

关键参数

参数
默认值
说明
app.batch_size
1000
每批取的订阅者数
app.concurrency
10
并发 worker goroutine 数
app.message_rate
10
每秒最大发送数
app.max_send_errors
1000
超过此数自动暂停活动
app.message_sliding_window
false
启用滑动窗口限流
app.message_sliding_window_rate
10000
滑动窗口内最大消息数
app.message_sliding_window_duration
1h
滑动窗口时间范围

SMTP 连接池

// knadh/smtppool/v2 — 作者自己的 SMTP 连接池库
type Pool struct {
    servers []*Server
// 多 SMTP 服务器轮询
// 连接复用、自动重连
// TLS/SSL 支持
}

knadh/smtppool/v2 是作者专门写的 SMTP 连接池——支持多个 SMTP 服务器轮询、连接复用、自动重连。配置多个 SMTP 服务器时,listmonk 会自动在它们之间做负载均衡。

游标分页

-- 不用 OFFSET(百万级 OFFSET 性能灾难)
-- 用 last_subscriber_id 做游标
SELECT * FROM subscribers
WHEREid > :last_subscriber_id
ORDERBYidASC
LIMIT :batch_size

OFFSET 1000000 在 PostgreSQL 里需要扫描前 100 万行再丢弃。游标分页通过 WHERE id > last_id 直接定位,无论数据量多大都是 O(1)。

模板引擎

listmonk 使用 Go 标准库的 text/template + Masterminds/sprig/v3(150+ 模板函数):

// 邮件模板示例
Subject: {{ .Data.Subject }}

Hi {{ .Subscriber.Name }},

{{ .Content }}

Unsubscribe: {{ .UnsubscribeURL }}

可用变量

变量
说明
.Subscriber.Email
订阅者邮箱
.Subscriber.Name
订阅者姓名
.Subscriber.Attribs
自定义属性(JSON)
.Campaign.Name
活动名称
.UnsubscribeURL
退订链接
.ViewURL
网页版链接

Sprig 函数

// 条件逻辑
{{ if gt .Subscriber.Attribs.score 80 }}
  VIP 用户专属内容
{{ end }}

// 字符串操作
{{ .Subscriber.Name | upper | trunc 20 }}

// 日期格式化
{{ now | date "2006-01-02" }}

// 数学运算
{{ add .Data.Price .Data.Tax }}

150+ 函数覆盖字符串、日期、数学、加密、正则、JSON 处理——大部分邮件个性化需求不需要写代码。

数据库设计

11 个 ENUM 类型

CREATETYPE subscriber_status AS ENUM ('enabled''disabled''blocklisted');
CREATETYPE subscription_status AS ENUM ('unconfirmed''confirmed''unsubscribed');
CREATETYPE campaign_status AS ENUM ('draft''running''scheduled''paused''cancelled''finished');
CREATETYPE campaign_type AS ENUM ('regular''optin');
CREATETYPE content_type AS ENUM ('richtext''html''plain''markdown''visual');
CREATETYPE list_type AS ENUM ('public''private''temporary');
CREATETYPE list_optin AS ENUM ('single''double');

PostgreSQL ENUM 保证类型安全——活动状态只能是 draft/running/scheduled/paused/cancelled/finished 之一,不可能写入非法状态。

订阅者-列表多对多

-- 多对多关系表
CREATETABLE subscriber_lists (
    subscriber_id INTREFERENCES subscribers(id),
    list_id INTREFERENCES lists(id),
status subscription_status NOTNULLDEFAULT'unconfirmed',
    PRIMARY KEY (subscriber_id, list_id)
);

双层状态设计:

  • 订阅者全局状态enabled / disabled / blocklisted
  • 列表级别状态unconfirmed / confirmed / unsubscribed

一个订阅者可以退订列表 A,但仍在列表 B 中。这种细粒度控制在邮件营销中很重要——你不能因为用户退订了产品更新,就把他从小技巧周刊里也删掉。

物化视图

-- 仪表盘缓存
CREATEMATERIALIZEDVIEW mat_dashboard_counts AS
SELECTCOUNT(*) FROM subscribers WHEREstatus = 'enabled';

CREATEMATERIALIZEDVIEW mat_dashboard_charts AS
-- 30 天点击和打开趋势
SELECTdateSUM(clicks), SUM(views) FROM campaign_views
GROUPBYdate;

CREATEMATERIALIZEDVIEW mat_list_subscriber_stats AS
-- 每个列表的订阅者统计
SELECT list_id, statusCOUNT(*) FROM subscriber_lists GROUPBY list_id, status;

三个物化视图缓存昂贵的聚合查询。百万级订阅者的仪表盘不需要每次都 COUNT——刷新物化视图就行。

退信处理(Bounce Management)

邮件发送中最难处理的部分不是”发出去”,而是”被退回来”。

双通道退信

通道 1:Webhook(实时)
├── AWS SES SNS → POST /webhooks/bounce
├── SendGrid → POST /webhooks/bounce
├── Postmark → POST /webhooks/bounce
└── Mailgun → POST /webhooks/bounce

通道 2:POP3(延迟)
├── 定期检查退信邮箱(POP3 协议)
├── knadh/go-pop3 库连接邮箱
├── emersion/go-message 解析 MIME 邮件
└── 正则匹配退信内容

退信分类

// 启发式分类:分析退信邮件内容中的关键词
// Hard bounce → 永久性错误(邮箱不存在、域名无效)
// Soft bounce → 临时性错误(邮箱满了、服务器拒收)

硬退信(Hard Bounce):自动将订阅者标记为 blocklisted,后续活动不再发送。软退信(Soft Bounce):记录但不断供,等待下次重试。

权限系统

角色 + 权限

// permissions.json
{
"roles": [
    {
"id"1,
"name""Super Admin",
"permissions": ["*"]
    },
    {
"id"2,
"name""Manager",
"permissions": [
"subscribers:manage",
"campaigns:manage",
"lists:manage"
      ]
    },
    {
"id"3,
"name""Viewer",
"permissions": [
"subscribers:view",
"campaigns:view",
"lists:view"
      ]
    }
  ]
}

双层权限:

  • 全局角色user_role_id):控制用户能访问哪些功能模块
  • 列表角色list_role_id):控制用户能访问哪些具体的邮件列表

支持角色继承(parent_id)——”编辑”角色继承”查看者”的所有权限,再加上编辑权限。

认证方式

方式
说明
密码 + bcrypt
默认认证方式
OIDC
coreos/go-oidc/v3

,支持 Google/GitLab/任意 OIDC Provider
TOTP 2FA
pquerna/otp

,Google Authenticator 兼容
API Token
无状态 token,用于 API 调用
Session
zerodha/simplesessions/v3

 + PostgreSQL 存储

Go 技术亮点

jmoiron/sqlx + lib/pq

// 不是 ORM,是 sqlx(在 database/sql 上的薄封装)
// SQL 写在 queries/ 目录的 .sql 文件里
// goyesql/v2 将 SQL 文件解析为 named queries

-- queries/subscribers.sql
-- name: get-subscriber
SELECT * FROM subscribers WHERE id = $1;

-- name: query-subscribers
SELECT * FROM subscribers
WHERE id > :offset
ORDER BY id ASC
LIMIT :limit;

SQL 和 Go 代码分离——.sql 文件专注 SQL,Go 代码专注业务逻辑。goyesql/v2 解析 SQL 文件,sqlx 执行参数化查询。没有 ORM 的抽象泄漏问题。

koanf/v2 配置

// 作者自己的配置库(knadh/koanf)
// 支持 TOML + 环境变量 + 命令行参数 + JSON
// 统一合并,优先级清晰

// config.toml
[app]
batch_size = 1000
concurrency = 10

# 环境变量覆盖
LISTMONK_APP__BATCH_SIZE=2000

koanf 支持 8+ 种配置源,按优先级合并。环境变量用双下划线表示嵌套(APP__BATCH_SIZE → app.batch_size)。

smtppool/v2 — SMTP 连接池

// 核心设计
type Pool struct {
    servers []*Server      // 多服务器
    index   uint64// 原子计数器轮询
}

func(p *Pool)Send(m *Message)error {
    server := p.next()     // Round-robin
    conn := server.get()   // 复用连接
return conn.Send(m)
}

多 SMTP 服务器轮询、连接复用、自动重连。配置 AWS SES + Mailgun + 自建 SMTP 时,listmonk 自动在三者的连接池之间做负载均衡。

easyjson 高性能 JSON

// zerodha/easyjson — 编译时 JSON 序列化代码生成
// 比标准库 encoding/json 快 3-5 倍
// 用于 API 响应的高频序列化场景

Zerodha(listmonk 的孵化公司)自己的 easyjson 库——通过代码生成避免运行时反射,JSON 序列化性能接近手动编写。

Echo v4 HTTP 框架

// labstack/echo/v4 — 高性能 HTTP 框架
// 路由、中间件、参数绑定、错误处理
e := echo.New()
e.GET("/api/subscribers", handlers.QuerySubscribers)
e.POST("/api/campaigns", handlers.CreateCampaign)

goldmark Markdown

// yuin/goldmark — Go 标准 Markdown 解析器
// 邮件内容支持 Markdown 格式
// 自动转换为 HTML

邮件内容支持四种格式:RichText(WYSIWYG 编辑器)、HTML、Markdown、纯文本。

和其他方案对比

维度
listmonk
Mailchimp
Brevo
Mailtrain
部署方式
自托管(单二进制)
SaaS
SaaS
自托管(Node.js)
数据库
PostgreSQL
自有
自有
MySQL
订阅者费用
免费(无限)
按人数收费
按人数收费
免费
发送方式
自有 SMTP
自有
自有
自有 SMTP
数据主权
100% 本地
服务商
服务商
100% 本地
多用户
RBAC 权限
团队协作
团队协作
单用户
退信处理
Webhook + POP3
内置
内置
有限
API
RESTful
RESTful
RESTful
有限
语言
Go
PHP/Python
Node.js
Node.js
Star
19.6K
6K
协议
AGPL v3
专有
专有
MIT

Mailchimp 最省心但最贵(10 万订阅者月费 $500+)。Brevo 功能全但锁定服务商。Mailtrain 也是开源但 Node.js 技术栈、单用户、功能较少。listmonk 在”自托管 + 功能全面 + 高性能”这个组合上优势明显。

实战场景

场景 1:技术博客 Newsletter

# config.toml
[app]
batch_size=1000
concurrency=5
message_rate=5# 不想打爆 SMTP

[smtp]
host="email-smtp.us-east-1.amazonaws.com"
port=587
username="SES_USER"
password="SES_PASS"

1 万订阅者的周报——每封个性化,5 秒发完。

场景 2:电商用户分层

列表 A:VIP 用户(过去 12 个月消费 > 500)
列表 B:活跃用户(30 天内有登录)
列表 C:沉睡用户(90 天无登录)

活动 1 → 列表 A:新品预览 + 专属折扣
活动 2 → 列表 B:常规促销
活动 3 → 列表 C:召回优惠

订阅者可以同时在多个列表中,退订一个不影响其他。

场景 3:多团队共享

市场部 → 管理"产品更新""促销"列表
技术部 → 管理"API 变更通知"列表
客服部 → 只能查看订阅者数据,不能发送

通过全局角色 + 列表角色控制各团队的访问权限。

注意事项

  • AGPL v3:如果你修改 listmonk 代码并通过网络提供服务,需要开源你的修改。内部使用或自托管不在此限,但 SaaS 产品需要留意传染性
  • PostgreSQL 必需:不支持 MySQL/SQLite。PostgreSQL 的 ENUM、物化视图、窗口函数是核心依赖
  • SMTP 依赖:listmonk 不内置邮件发送——你需要配置外部 SMTP(AWS SES、Mailgun、自建 Postfix 等)
  • 单实例:不支持多实例水平扩展。百万级订阅者单实例可以应对,更大规模需要数据库分片
  • 前端编译:Vue 前端编译后通过 embed 嵌入二进制,修改前端需要重新编译
  • 退信延迟:POP3 方式的退信有延迟(取决于轮询间隔),Webhook 方式实时性更好

总结

listmonk 的价值在于它回答了一个问题:”邮件营销系统到底有多复杂?”答案是:如果选对技术栈,一个 Go 二进制 + PostgreSQL 就够了。Zerodha 用它管理印度最大的券商用户群——这是真实的百万级生产验证。

从 Go 项目的角度看,listmonk 是”SQL-first 架构”的工程典范:

  • sqlx + goyesql:SQL 和 Go 代码分离,没有 ORM 的抽象泄漏
  • PostgreSQL ENUM:11 个自定义类型,数据库层保证数据一致性
  • 游标分页last_subscriber_id 避免 OFFSET 性能灾难
  • 物化视图:缓存昂贵聚合,百万级仪表盘秒开
  • smtppool/v2:自研 SMTP 连接池,多服务器负载均衡
  • Campaign Pipeline:批量取订阅者 + 并发 worker + 滑动窗口限流 + 自动错误暂停
  • koanf/v2:作者自己的配置库,8 种配置源统一合并
  • easyjson:编译时 JSON 代码生成,避免运行时反射

如果你需要一个自托管的邮件营销系统,listmonk 是目前 Go 生态中最成熟的选择。

GitHub 仓库:github.com/knadh/listmonk


推荐阅读

福利
我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。