静态网站的好处,就在于省心:不用维护应用服务器,不必担心数据库连接池耗尽,也没有需要全天候运行的公开 API。浏览器请求文件,内容分发网络负责返回文件,流程就这么简单。
不过,在搭建这个个人网站时,我还是想知道:读者来自哪些国家?访问量随时间有什么变化?能不能在保留静态架构的同时,展示这些信息,又不把本应保密的数据公开出去?
最后,我用 CloudFront 访问日志、CloudWatch、Lambda 和 EventBridge 搭了一条小型数据处理流程,把结果写成 S3 中的一个 JSON 文件。访客看到的是世界地图、各国的页面浏览量、热门页面和请求速率图表,而不是原始日志、IP 地址、User-Agent 或个人浏览记录。
网站仍然是静态的。统计工作在后台完成,结果只是网站上的另一个静态文件。
浏览器只取文件,不参与后台查询
面向访客的请求链路尽量保持简单:
- 访客通过 Amazon CloudFront 访问网站。
- CloudFront 返回缓存中的文件,其私有源站是一个 S3 存储桶。
- 随后,浏览器像获取其他静态资源一样,读取
content/public-visitors.json。 - 前端 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、专门的分析存储或托管可视化平台,才可能有必要。
但对目前这个个人网站来说,这些复杂度并没有对应的需求。一个定时函数,加上一份静态数据文件,就能保留我看重的几个特点:攻击面小、行为可预期,各个组件的职责也容易理解。
关键不在地图,而在数据边界
这个访客统计页面里,最重要的其实不是那张世界地图,而是它背后的数据边界。
详细的请求信息留在运维环境中;权限受限的后台任务定期把它们整理成匿名汇总;网站只接收一份小型静态快照,再由浏览器把数字画成地图和曲线。
这样,我可以了解网站的访问情况,又不必为了统计功能,把静态网站变成一个需要持续维护的动态应用。


云 / 系统