BK 云 / 系统 首页
ZH
English ENDeutsch DEEspañol ES中文 ZHFrançais FRPortuguês (Brasil) PT-BR日本語 JAहिन्दी HINederlands NL
联系我 ↗
← 返回技术笔记

静态网站也能看懂访问量:我的 AWS 访客统计方案

静态网站的好处,就在于省心:不用维护应用服务器,不必担心数据库连接池耗尽,也没有需要全天候运行的公开 API。浏览器请求文件,内容分发网络负责返回文件,流程就这么简单。

不过,在搭建这个个人网站时,我还是想知道:读者来自哪些国家?访问量随时间有什么变化?能不能在保留静态架构的同时,展示这些信息,又不把本应保密的数据公开出去?

最后,我用 CloudFront 访问日志、CloudWatch、Lambda 和 EventBridge 搭了一条小型数据处理流程,把结果写成 S3 中的一个 JSON 文件。访客看到的是世界地图、各国的页面浏览量、热门页面和请求速率图表,而不是原始日志、IP 地址、User-Agent 或个人浏览记录。

网站仍然是静态的。统计工作在后台完成,结果只是网站上的另一个静态文件。

浏览器只取文件,不参与后台查询

面向访客的请求链路尽量保持简单:

  1. 访客通过 Amazon CloudFront 访问网站。
  2. CloudFront 返回缓存中的文件,其私有源站是一个 S3 存储桶。
  3. 随后,浏览器像获取其他静态资源一样,读取 content/public-visitors.json。
  4. 前端 JavaScript 根据这份匿名统计数据绘制地图和图表。

浏览器不调用 Lambda,不查询 CloudWatch,也不持有 AWS 访问凭证。它只能读取预先生成的数据,不能向后台临时索取其他时间范围或更详细的记录。涉及敏感信息、消耗计算资源的工作,都放在这条公开请求链路之外。

后台的数据处理链路则是:

CloudFront → CloudWatch Logs → scheduled Lambda → aggregated JSON → S3 → CloudFront → browser

这两条链路分开,是整个设计的关键。后台运维环境可以处理详细的访问数据,对外发布的文件则只包含允许公开的那一小部分。

日志可以详细,公开数据要克制

CloudFront 标准日志 v2 会把选定的访问日志字段发送到 CloudWatch Logs 日志组。运维排查需要请求详情和状态码,但这并不意味着这些信息都应该出现在公开的统计页面上。

负责生成公开文件的函数只保留汇总结果:

  • 按国家或地区代码汇总、请求成功的 HTML 页面浏览量;
  • 访问次数最多的 HTML 页面路径;
  • 根据 HTTP 状态码汇总指标计算的每分钟请求数;
  • 数据更新时间,以及各统计时间范围的说明。

生成的文件不包含来源 IP、城市级位置、Cookie、User-Agent 字符串,也不包含能串联出某位访客浏览轨迹的记录。它回答的是“网站收到了多少访问流量”,而不是“谁来过”。

因此,我使用“页面浏览量”,而不是“人数”。同一个读者可以产生多次请求;搜索引擎爬虫、链接预览服务、监控探测等自动化程序也会访问网站。这里展示的是基础设施记录到的访问情况,不能据此识别具体的人。

为什么用 CloudWatch 做中间层

同一批访问数据,在 CloudWatch 中有两种用途:通过 CloudWatch Logs Insights 查询结构化日志,按国家和页面汇总;通过指标筛选器统计 2xx、3xx、4xx、5xx 四类 HTTP 状态码,再用指标数据生成请求速率曲线。后者不需要每次都重新解析全部日志事件。

对外提供的数据包含几种固定视图:

  • 过去 24 小时、7 天和 30 天按国家或地区汇总的页面浏览量;
  • 覆盖 24 小时、7 天、30 天和 12 个月的请求速率数据;
  • 时间跨度越大,采用的汇总时间间隔越长,以控制文件大小。

CloudFront 日志并不是实时事件流。按 AWS 文档的说明,标准日志通常会在一小时内送达,但部分记录可能更晚。因此,这些日志适合用来回看访问情况,不适合包装成实时在线人数。

定时生成快照,不开放查询 API

在本文所述的设计中,EventBridge 每六小时触发一次 Lambda。函数查询限定时间范围内的日志,读取指标序列,整理结果,最后只覆盖 S3 中的一个对象:content/public-visitors.json。

六小时一次,是有意做出的取舍。访客统计不需要逐秒更新,而每次刷新都要扫描日志。缩短间隔会增加查询次数和成本,对这个页面的实际帮助却有限。

这份 JSON 文件的缓存响应头允许客户端重新验证内容是否更新。这样,页面就能取得较新的统计快照,同时仍由 CloudFront 缓存保护 S3 源站。统计变化不需要重新部署整个网站,也不需要清除全站 CDN 缓存。

这种方式还有一个好处:后台失败,不会拖垮前台。即使查询或 Lambda 执行失败,原有的静态文件仍然可用。统计可能暂时停留在上一次更新,但网站照常访问,访客不必等待分析任务完成。

用最小权限限制出错后的影响

生成统计文件的 Lambda 使用独立的 IAM 角色,权限只覆盖必要操作:

  • 仅针对指定的访问日志组执行查询并读取结果;
  • 读取请求速率图表所需的 CloudWatch 指标;
  • 仅向网站存储桶中的 content/public-visitors.json 写入数据;
  • 写入函数自身的运行日志。

它不能部署其他网站文件,不能修改 CloudFront 分配,也不能把 S3 存储桶改成公开访问。EventBridge 的调用权限同样限定在指定的定时规则和这个函数之间。

网站的 S3 存储桶始终保持私有。CloudFront 通过源访问控制(Origin Access Control,OAC)向 S3 发送签名请求,避免访客绕过 CloudFront、直接把存储桶当作公开文件服务器使用。

地图不必精确到城市

公开统计并不是越详细越好。城市标记看起来很直观,但当访问量很少时,如果再结合精确时间和对某个人的已有了解,就可能推断出访问者身份。国家或地区级别的汇总,已经足够展示读者的大致分布,没有必要让基础设施数据看起来像是某位读者居住地的证明。

界面也应体现这条边界:地图展示各国的访问量分布和排名,旁边明确说明请求量不等于人数;在本文所述的界面中,不提供逐条访问记录的下钻查询。

再精美的图表,也不能消除数据本身的不确定性。根据 IP 推断的国家或地区,只是边缘平台记录到的信息。VPN、企业网络、移动运营商、爬虫和缓存的链接预览,都可能让它与读者的实际所在地出现偏差。

真正影响成本的是扫描量

地图和图表由浏览器中的前端代码绘制,不会因为图形复杂就增加服务端渲染费用。主要的运维成本来自日志采集与保留、指标摄取,以及定时查询时扫描的日志数据量。

所以,控制成本要从架构入手:

  • 日志只保留 30 天,避免无限积累;
  • 函数每天运行四次;
  • 查询限定时间范围,只读取需要的字段;
  • 较长的图表时间范围采用更大的指标汇总间隔;
  • 对外发布的 JSON 文件,远小于原始日志。

以这个网站的规模,Lambda 运行和保存一份小型 JSON 文件所需的 S3 存储费用很低。以后如果流量上升,或延长日志保留时间,更值得留意的是 Logs Insights 的扫描量。

这次实现让我想清楚的几件事

静态网站也可以有可观测性。 统计系统可以在旁边独立运行,不必加入访客请求的同步处理链路。

汇总处理也是一道安全边界。 从内部日志到公开文件,哪些信息可以出去、怎样转换,都应该明确设计并经过检查,不能顺手把原始数据带出去。

更新得够用,比假装实时更重要。 对于本就有延迟的标准访问日志,六小时一份快照符合这里的需求,也避免了多余的查询。

更新失败时,先保留上一次可用的结果。 统计暂时过期,总比分析服务一出问题,连网站首页也打不开要好。

图表旁边要说清楚它的局限。 自动化访问、重复请求、地理位置推断,以及流量和人数的区别,都不能留给读者自行猜测。

什么情况下,我会换一种方案

这套做法适合流量适中、展示维度比较固定的个人网站。如果以后需要让用户自由查询、查看登录后才能访问的统计页面,或者要求秒级更新、处理大型多维数据集,我会重新评估。那时,引入 API、专门的分析存储或托管可视化平台,才可能有必要。

但对目前这个个人网站来说,这些复杂度并没有对应的需求。一个定时函数,加上一份静态数据文件,就能保留我看重的几个特点:攻击面小、行为可预期,各个组件的职责也容易理解。

关键不在地图,而在数据边界

这个访客统计页面里,最重要的其实不是那张世界地图,而是它背后的数据边界。

详细的请求信息留在运维环境中;权限受限的后台任务定期把它们整理成匿名汇总;网站只接收一份小型静态快照,再由浏览器把数字画成地图和曲线。

这样,我可以了解网站的访问情况,又不必为了统计功能,把静态网站变成一个需要持续维护的动态应用。

参考资料

ZH
English ENDeutsch DEEspañol ES中文 ZHFrançais FRPortuguês (Brasil) PT-BR日本語 JAहिन्दी HINederlands NL