埋点这件事,别等系统跑起来才想
古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 AI 相关的开发。更完整的更新写在微信公众号「字与码」:工作经历、对新技术的想法,以及这些年走弯路的记录,会不定期发在那里。若觉得博客对你有用,欢迎顺手关注。
很多系统上线时都说“先加点日志,后面再分析”。过几个月再看,日志确实不少,问题还是回答不了。
用户从哪里来,不知道。注册页打开了多少次,不知道。免费用户为什么没有付费,不知道。某个接口调用量突然涨了,是新用户增长、爬虫、重试风暴,还是某个客户接入脚本写错了,也不知道。
日志不是埋点。日志通常是给工程师排障看的,讲的是“程序发生了什么”。埋点是给产品、运营、增长、风控、计费和工程一起看的,讲的是“用户和业务发生了什么”。
这篇文章不讲某个具体项目,而是把服务埋点这件事拆开:什么服务需要埋点,不同流量阶段怎么做,数据库怎么选,成本怎么算,性能怎么评估,最后用几个真实业务场景把“从用户源头到分析结果”的链路串起来。
![]()
埋点到底解决什么问题
埋点最常见的误区,是把它当成“访问量统计”。访问量当然重要,但只看 PV/UV,最多知道门口来了多少人。真正有价值的问题通常更尖锐:
- 哪个渠道来的用户更容易注册?
- 注册后多久会完成第一次关键行为?
- 免费用户在什么地方掉队?
- 付费前通常调用了哪些功能?
- 某个版本上线后,错误率上升是集中在一个入口,还是所有入口都变差?
- 高价值用户和普通用户的行为差异是什么?
- 一个接口调用量涨了,收入有没有跟着涨,成本有没有失控?
这些问题都有一个共同点:它们不只需要一条日志,而是需要把用户、会话、来源、页面、接口、订单、功能、错误、成本串起来。
所以服务埋点的核心不是“多记一点”,而是建立一条可追踪的事实链:
用户来源 -> 访问页面 -> 注册/登录 -> 激活行为 -> 功能使用 -> 成本消耗 -> 付费/留存
如果这条链断了,再多日志也只能做局部猜测。
哪些服务需要埋点
不是所有服务一上来都需要复杂埋点。埋点设计应该和服务类型、业务阶段、流量规模一起看。
| 服务类型 | 最重要的问题 | 优先埋点 |
|---|---|---|
| 内容站、官网、文档站 | 流量从哪来,哪些内容带来转化 | page_view、referrer、utm、入口按钮、注册点击 |
| SaaS 后台 | 用户是否真正激活,哪些功能被使用 | login、workspace_created、feature_used、invite_sent |
| 电商/订阅服务 | 漏斗哪里流失,支付失败原因是什么 | product_view、checkout_started、payment_succeeded、payment_failed |
| API 平台 | 调用量、成功率、耗时、成本、客户价值 | api_call、first_api_call、quota_exceeded、billing_usage |
| B2B 内部系统 | 关键流程是否完成,审批/任务卡在哪里 | workflow_started、step_completed、approval_rejected |
| 高流量网关 | 峰值、异常、区域分布、计费和风控 | request_sample、error_bucket、rate_limited、billable_event |
小服务最怕过度设计。刚上线的官网,不需要立刻建 ClickHouse 集群;但至少要有稳定的 anonymous_id、页面访问、关键按钮点击和注册成功事件。反过来,一个每天几百万 API 调用的平台,如果还只靠应用日志和后台 SQL 查账,很快会把业务库拖慢。
先定事件,不要先定数据库
数据库选型很重要,但埋点系统最容易失败的地方不是数据库,而是事件口径。
一个合格的事件至少要回答四个问题:
{
"event": "api_call",
"time": "2026-06-29T10:23:45.123Z",
"user_id": "u_123",
"anonymous_id": "anon_abc",
"session_id": "sess_789",
"source": "web",
"page": "/pricing",
"properties": {
"tool_id": "stock_quote",
"success": true,
"latency_ms": 183,
"credits": 2,
"plan": "pro"
}
}
这四个问题是:
- 谁:
user_id、anonymous_id、account_id、api_key_id - 什么时候:事件时间和服务端接收时间最好都保留
- 在哪里:页面、入口、客户端、地区、设备、服务名
- 做了什么:事件名和业务属性
这里有两个细节很关键。
第一,匿名用户不能丢。很多转化发生在注册前,如果没有 anonymous_id,注册前的广告来源、落地页、定价页访问就没法和注册后的用户连起来。
第二,事件名要稳定。checkout_start、start_checkout、click_pay_button 混着用,后面看板一定会乱。事件名应该有 allowlist,字段应该有 schema version。
可选的埋点方式
埋点入口大致分四类。
前端埋点适合采页面浏览、按钮点击、表单展示、页面停留、客户端错误。它离用户最近,能拿到 referrer、utm、屏幕、浏览器、入口位置。但它不可靠:用户可能关页面,浏览器可能拦截,网络可能失败,前端也容易被伪造。
后端埋点适合采注册成功、登录成功、API Key 创建、订单支付、接口调用、扣费、权限失败。这类事件必须以后端为准,特别是支付和计费,不能相信前端说“我付费成功了”。
网关埋点适合采 API 平台和微服务入口:请求路径、状态码、耗时、租户、key、限流、熔断、上游错误。它的优点是统一,缺点是业务语义有限。网关知道某个接口被调用了,但不一定知道这次调用对业务意味着什么。
异步埋点适合高流量场景。主流程只把事件放进内存队列、本地 buffer、Kafka、Pulsar 或云消息队列,后面的消费者批量写库。它的好处是不会让数据库写入反压用户请求;坏处是系统复杂度上来了,需要处理丢失、重复、乱序和重放。
一套常见的分工是:
前端:页面和交互
后端:业务事实
网关:请求事实
队列:削峰和批量写入
任务:聚合、修复、回填
![]()
采集时要避开的坑
服务埋点不是写一条 insert 就完事。下面几个坑很常见。
不要把敏感信息写进埋点。邮箱、手机号、token、API Key、支付卡号、原始 prompt、完整查询文本,都要非常谨慎。需要分析时可以写 hash、长度、分类、是否为空,不要把隐私和密钥带进分析库。
不要让埋点阻塞主流程。注册、支付、API 调用这些路径上,埋点失败不能导致业务失败。常见做法是 best effort、异步队列、超时丢弃、批量写入。
不要采太细的无意义事件。滚动、鼠标移动、hover 这类事件很容易把量打爆。除非明确做行为热力图,否则先别碰。
不要只存原始事件不做聚合。Dashboard 如果每次都扫 180 天明细表,迟早会变成数据库压力测试。常用指标应该做日聚合、小时聚合,明细只用于 drilldown。
不要让 properties 变成垃圾桶。JSON 字段可以灵活,但每个事件应该有字段字典。否则三个月后你会发现同一个含义有 plan、plan_type、tier、package 四种写法。
分析方法:从单点事件到用户旅程
埋点分析通常分几层。
最简单的是计数和趋势:
select date_trunc('day', time) as day, count(*) as pv
from event_logs
where event = 'page_view'
group by 1
order by 1;
第二层是分组对比:
select utm_source, count(distinct anonymous_id) as visitors
from event_logs
where event = 'page_view'
and time >= now() - interval '7 days'
group by utm_source
order by visitors desc;
第三层是漏斗:
landing_view -> signup_click -> signup_completed -> first_api_call -> payment_succeeded
漏斗不要只看总转化率。要按渠道、设备、国家、套餐、入口页面拆开看。总转化率下降 20%,可能只是某个渠道买量变差;也可能是移动端注册页坏了;也可能是支付失败率上升。
第四层是 cohort 留存。比如看“完成首次 API 调用的用户,7 天后是否还有调用”。这比 DAU 更接近产品是否真的有用。
第五层是单位经济账。API 平台尤其需要这层:某个客户调用量很高,但如果每次调用都带来第三方成本,收入没覆盖成本,就不是好事。
数据库怎么选
服务埋点常见的存储选项有 PostgreSQL、MongoDB、ClickHouse,也可以直接用产品分析工具,比如 PostHog、Amplitude、Mixpanel。自己做的好处是口径可控,能和业务数据打通;坏处是要承担数据质量、性能和运维。
PostgreSQL:中小规模最稳的起点
如果你的服务本来就用 PostgreSQL,日事件量在几十万以内,查询主要是近 7 天、近 30 天的 dashboard,PostgreSQL 是很好的起点。
它的优势是业务 join 方便。用户表、订单表、积分表、团队表都在同一个数据库或同类数据库里,分析时不用同步一堆维表。JSONB 也能承接不同事件的扩展属性。PostgreSQL 官方文档里,GIN 索引用于处理 JSONB、全文检索这类复合值查询,这让“结构化字段 + JSONB 扩展”成为一个现实方案。
但 PostgreSQL 不是无限事件仓库。写事件表时要注意:
- 按时间分区,比如月分区或日分区
- 常用查询字段单独列出来,不要全部塞进 JSONB
- JSONB 的 GIN 索引少建,确实需要按 properties 查询再建
- Dashboard 查聚合表,不要扫明细表
- 明细保留周期要明确,比如 90 天或 180 天
一个比较稳的表结构是:
create table event_logs (
id uuid primary key,
event text not null,
event_time timestamptz not null,
received_at timestamptz not null default now(),
user_id text,
anonymous_id text,
session_id text,
source text,
page text,
referrer text,
utm_source text,
utm_campaign text,
properties jsonb not null default '{}',
schema_version int not null default 1
) partition by range (event_time);
MongoDB:能存,但不适合作为主分析库
MongoDB 存事件很自然。事件本来就是 JSON,字段变化频繁,写入也简单。MongoDB 也有 time series collections,官方文档说明它会把相似时间和相同来源的数据组织到一起,提高时间序列数据的存储和查询效率。
所以 MongoDB 不是不能做埋点。它适合这些场景:
- 事件结构非常不稳定,早期还在探索
- 主要用于调试和审计,不做复杂漏斗
- 已经有成熟 MongoDB 运维体系
- 单用户或单对象维度查询多,跨全量聚合少
但如果你要长期做 DAU、渠道转化、留存、付费漏斗、用户分层、API 成本趋势,MongoDB 会越来越吃力。不是完全做不了,而是复杂 aggregation pipeline、索引数量、数据扫描成本、BI 接入成本都会上来。
一句话:MongoDB 可以当灵活事件仓库,不建议当核心埋点分析主库。
ClickHouse:高流量分析的正路
ClickHouse 是列式 OLAP 数据库,适合 append-only 的大规模事件、日志、时间序列和多维聚合。ClickHouse 文档里 MergeTree、分区和 TTL 是核心能力,TTL 文档也明确建议按同一个时间字段做分区,这样过期数据可以整块 drop,而不是逐行删除。
它的优势很适合埋点:
- 列式存储,只读查询需要的列
- 压缩率高,重复字段多的事件数据很省空间
- 按时间、事件、用户、渠道、区域聚合很快
- 批量写入吞吐高
- TTL、分区、冷热数据管理适合长期明细保留
Cloudflare 的公开案例很有代表性。Cloudflare 在一篇博客里介绍过用 ClickHouse 支撑 HTTP Analytics,处理每秒数百万请求级别的分析流水线;后来也多次分享过 ClickHouse 在日志分析、客户 dashboard、Bot 管理等场景中的使用。这类案例说明 ClickHouse 不是“更快的 PostgreSQL”,而是完全不同定位的分析型基础设施。
但 ClickHouse 也不是所有阶段都该上。它不适合频繁单行更新,不适合强事务,不适合替代业务主库。它最好作为分析库,由 PostgreSQL、日志、队列、业务服务异步同步过来。
![]()
一个简单的选型表
| 阶段 | 日事件量 | 推荐方案 | 说明 |
|---|---|---|---|
| 起步 | 1 千 - 10 万 | PostgreSQL 单表或月分区 | 先把事件口径做对 |
| 成长 | 10 万 - 50 万 | PostgreSQL 分区 + 聚合表 | 控制索引,聚合先行 |
| 高速增长 | 50 万 - 100 万 | PostgreSQL + ClickHouse 旁路同步 | 开始验证列式分析库 |
| 高流量 | 100 万 - 500 万 | ClickHouse 做明细分析,PostgreSQL 做业务事实 | Dashboard 不扫业务库 |
| 超高流量 | 500 万以上 | ClickHouse 集群 + 队列 + 分层存储 | 需要专门的数据平台治理 |
这里的日事件量只是经验线,不是绝对阈值。如果事件很窄、查询简单,PostgreSQL 能撑得更久;如果每个事件 properties 很大、看板又天天扫 180 天,几十万日事件也可能很痛苦。
成本怎么估算
埋点成本主要由五个变量决定:
日事件数 × 单条事件大小 × 保留周期 × 副本数 × 查询复杂度
PostgreSQL 里,一条带 JSONB 的事件,连同行开销和索引,粗略可以按 1KB 到 3KB 估。ClickHouse 压缩后可能只有 200B 到 800B,具体取决于字段设计和重复度。
按 50 万事件/日估算:
| 存储 | 单条估算 | 月明细 | 保留 12 个月 |
|---|---|---|---|
| PostgreSQL | 2KB | 约 30GB | 约 360GB |
| PostgreSQL 加索引膨胀 | 4KB | 约 60GB | 约 720GB |
| ClickHouse 压缩后 | 500B | 约 7.5GB | 约 90GB |
这只是明细,不含备份、副本、聚合表、WAL/binlog、对象存储归档。比较稳的预算方式,是再乘一个 1.5 到 2 的预留系数。
如果自建 ClickHouse,一个 4-8 vCPU、16-32GB 内存、几百 GB 到 1TB SSD 的单节点,通常足够做早期验证和中等规模分析。到了每天几百万事件,或者保留一年以上明细,就要认真规划副本、备份、冷热分层和查询隔离。
性能怎么评估
不要只压测写入。埋点系统真正容易出问题的是“写入 + 查询 + 聚合 + 清理”同时发生。
我会看这些指标:
- 写入延迟:主业务请求是否被埋点拖慢
- 写入吞吐:每秒事件数、批量大小、失败重试
- 查询延迟:常用 dashboard 的 P50/P95
- 聚合耗时:每日任务是否能在窗口内跑完
- 存储增长:每天新增多少数据,索引占比多少
- 数据延迟:事件产生后多久能在看板看到
- 数据质量:重复率、丢失率、无效 user_id、非法 event
一个简单的验收标准可以这样定:
| 指标 | 小中型服务目标 | 高流量服务目标 |
|---|---|---|
| 主流程埋点开销 | P95 小于 10ms,失败不影响业务 | 主流程只入队,P95 小于 3ms |
| 前端事件可见延迟 | 1-5 分钟 | 1 分钟内或准实时 |
| 日聚合任务 | 30 分钟内完成 | 分钟级增量聚合 |
| 常用看板查询 | P95 小于 2 秒 | P95 小于 1 秒 |
| 明细查询范围 | 默认限制 7-30 天 | 通过 ClickHouse 支撑长窗口 |
性能评估还有一个容易被忽略的点:事件增长不是线性的。业务正常增长是一种;某个 SDK 出 bug、客户端疯狂重试、debug 事件忘关、爬虫打爆接口,是另一种。埋点系统要能降级,比如采样 DEBUG 事件、丢弃低优先级事件、限制单用户事件频率。
端到端案例一:内容站如何分析注册转化
假设你有一个技术内容站,目标不是单纯看文章阅读量,而是希望读者注册产品。
事件设计:
| 阶段 | 事件 | 关键字段 |
|---|---|---|
| 来源 | page_view | anonymous_id、utm_source、referrer、slug |
| 阅读 | article_read | scroll_depth、read_seconds、slug |
| 点击 | signup_click | location、slug、button_type |
| 注册 | signup_completed | user_id、anonymous_id、method |
| 激活 | first_key_action | user_id、action、hours_since_signup |
分析链路:
广告/搜索/社交 -> 文章页 -> 注册按钮 -> 注册完成 -> 第一次关键行为
分析结果可能是:
- 搜索流量 PV 最大,但注册率一般
- 某篇深度教程 PV 不高,但注册转化率很高
- 移动端 signup_click 多,signup_completed 少,说明注册页移动端有问题
- 某个渠道带来的用户注册快,但激活率低,可能是低质量流量
这时优化动作就很明确:不是“多写文章”,而是把高转化文章前置、修移动端注册页、调整投放渠道。
端到端案例二:API 平台如何分析成本和收入
API 平台只看调用量很危险。调用量越高,成本可能越高;如果没有收入或留存,增长反而会变成亏损。
事件设计:
| 阶段 | 事件 | 关键字段 |
|---|---|---|
| 注册 | signup_completed | user_id、plan、utm_source |
| API Key | api_key_created | user_id、key_type |
| 首次调用 | first_api_call | user_id、tool_id、signup_to_first_call_hours |
| 调用 | api_call | user_id、api_key_id、tool_id、success、latency_ms、cost、credits |
| 限额 | quota_exceeded | user_id、plan、requested_action |
| 付费 | payment_succeeded | user_id、amount、plan、calls_before_pay |
核心分析:
select
tool_id,
count(*) as calls,
sum((properties->>'cost')::numeric) as cost,
sum((properties->>'credits')::numeric) as credits,
avg((properties->>'latency_ms')::numeric) as avg_latency
from event_logs
where event = 'api_call'
and event_time >= now() - interval '1 day'
group by tool_id
order by calls desc;
这类分析能回答:
- 哪些工具调用最多,但转化不付费?
- 哪些工具毛成本高,需要改定价?
- 免费用户到首次调用的时间是否太长?
- 首次调用成功率低,是参数难填、文档不清楚,还是第三方 provider 不稳定?
API 平台建议把“调用事实”和“业务行为”分开。调用事实可以来自网关或服务端,必须准确;业务行为可以来自产品埋点,负责解释用户为什么走到这里。
端到端案例三:订阅服务如何看付费漏斗
订阅服务最怕只看支付成功。支付成功当然重要,但付费前的路径更重要。
事件链路:
pricing_view -> plan_select -> checkout_started -> payment_succeeded/payment_failed -> subscription_renewed/subscription_cancelled
关键字段:
current_planselected_planpricepayment_methodfailure_reasoncountrydevice_typeentry_point
分析时不要只算一个总漏斗,要拆:
- 移动端和桌面端
- 不同国家/地区
- 不同入口,比如首页、文档、用量不足提醒
- 新用户和老用户
- 月付和年付
一个常见发现是:plan_select -> checkout_started 转化很好,但 checkout_started -> payment_succeeded 很差。这个问题一般不在定价页,而在支付链路:支付方式不适配、3DS 验证失败、账单地址要求过多、错误提示不清楚。
演进策略:别一步到位,也别没有退路
我比较喜欢的演进方式是四步。
第一步,只做核心事件。页面访问、注册成功、登录成功、关键功能使用、支付成功、API 调用。先保证口径准确,不要追求事件数量。
第二步,补身份链路。匿名用户、登录用户、账号、团队、API Key 要能串起来。没有身份链路,漏斗和留存都不稳。
第三步,做聚合表和看板。DAU、注册转化、首次关键行为、付费转化、功能使用、调用成功率、成本趋势。不要让业务方天天找工程师跑 SQL。
第四步,引入分析库。PostgreSQL 扛不住长窗口明细扫描时,再同步到 ClickHouse。这个时候事件口径已经稳定,表设计也更容易做对。
一个健康的埋点系统,应该允许你从简单开始,但未来能平滑长大:
PostgreSQL 原始事件
-> PostgreSQL 分区 + 聚合表
-> PostgreSQL + ClickHouse 旁路分析
-> ClickHouse 承担明细分析,PostgreSQL 保留业务事实
最后:埋点是产品和工程之间的协议
服务埋点不是产品经理写一张事件表,也不是工程师随手加几行日志。它更像一份协议:我们如何定义用户行为,如何定义成功,如何定义成本,如何定义一次真正有价值的使用。
一开始不用做得很重。小服务先把十几个核心事件采准,比一口气采一百个混乱事件有用得多。等流量上来,再考虑队列、分区、ClickHouse、冷热分层。
真正重要的是,系统上线后,团队能持续回答这些问题:
- 用户从哪里来?
- 他们做了什么?
- 哪里卡住了?
- 哪些行为带来收入?
- 哪些调用消耗成本?
- 改了一版之后,结果到底变好了还是变坏了?
能回答这些问题,埋点才算开始有价值。
参考资料
- Cloudflare:HTTP Analytics for 6M requests per second using ClickHouse
- Cloudflare:Log analytics using ClickHouse
- ClickHouse Docs:Manage data with TTL
- ClickHouse Docs:Table partitions
- PostgreSQL Docs:GIN Indexes
- MongoDB Docs:Time Series Collections
- PostHog Docs:Self-host PostHog
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。