每当用户与你的网站互动时,你几乎会自动发现某些信息。他们加载了哪些页面,他们请求了哪些资产,他们从哪个页面进入你的网站等等。但有时你需要更细化的信息,一些无法从你的服务器日志中读取的信息。这通常就是谷歌分析会给你的东西。虽然谷歌给你提供了一些汇总的统计数据,但你可能想深入挖掘一下,也许可以推出你自己的解决方案。
在我们进一步讨论之前,我想澄清,这不是关于跟踪,这里提供的解决方案并没有真正考虑到任何用户身份,也没有跨网站跟踪任何人。这是为了了解你的用户是否真正阅读了你的文章,他们是否打开了模版,他们需要多长时间才能发现某些功能,他们是否点击了不能点击的东西,以达到调试的目的等等。
我们曾经运行一个现成的商业产品,想想Heap或Mixpanel。我们最终决定迁移,原因如下。
- 仓库重复 我们为存储在另一个数据仓库中的数据和它自己的可视化而付费。但我们有自己的仓库和BI工具,所以运行这个仓库,做ETL,验证,在格式改变时修复管道等是没有意义的。
- 屏蔽 该解决方案使用了一个供应商的JavaScript,所以它被我们很多客户屏蔽了。我们甚至在公司内部也遇到了这个问题,人们抱怨说他们不能在系统中进行自己的操作。
- 非线性扩展 解决方案并不昂贵,但如果我们想把事件数量增加到三倍,我们突然得到了10倍的账单。所以我们不得不通过删除大量的事件来解决这个问题,这使得整个事情变得适得其反。
- 数据和模式的所有权 这不是一个非常敏感的数据集,但拥有它确实是有意义的,不仅仅是为了它的价值,也是为了模式和数据模型的稳定性。
推出你自己的解决方案
这其中只有几个部分。
- 前台数据收集
- 后台数据持久性
- 可视化和分析
我们不会涉及第三部分,那是一个完整的问题,公司通常有一些适合他们的解决方案(例如Tableau,Looker)。
首先,让我们建立一个简单的前端网站。这不会做任何花哨的事情,它只是报告网站上任何按钮的点击情况。你可以把这些事件附加到任何东西上,包括鼠标移动。
<!DOCTYPE html>
<html>
<head><title>My Site</title></head>
<body>
<button>Click me!</button>
<button>Click me as well!</button>
<script type='text/javascript'>
// common functionality across all event logs
async function sendEvent(eventName, properties) {
const event = {
event: eventName,
url: window.location.href,
properties: properties,
}
return fetch('/events', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(event),
})
}
// now let's register all event calls
const buttons = document.getElementsByTagName('button');
for (let button of buttons) {
button.addEventListener('click', (e) => {
const properties = {
'button_text': e.target.textContent,
}
sendEvent('clicked_event', properties);
})
}
</script>
</body>
</html>
这将做的是,它将发送一个POST请求到你的后端,所以让我们来处理这个问题。这里是最穷的人的Node.js服务器,处理这些传入的事件。
const http = require('http');
const fs = require('fs');
const listener = function (req, res) {
// serve the frontend
if (req.url === '/' && req.method === 'GET') {
fs.readFile('index.html', function (err, data) {
if (err) {
res.writeHead(404);
return;
}
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(data, 'utf-8')
});
return;
}
// fail on anything but the frontend and event handling
if (!(req.url === '/events' && req.method === 'POST')) {
res.writeHead(400);
res.end();
return;
}
// accumulate data and parse it once it all comes in
let data = []
req.on('data', chunk => {
data.push(chunk)
})
req.on('end', () => {
const event = JSON.parse(data);
event.server_time = Date.now();
console.log(event);
// send the event to your backend
})
res.writeHead(202);
res.end();
}
const server = http.createServer(listener);
server.listen(8080);
正如你所看到的,服务器只是接受了JSON并添加了一个时间戳。这是为了确保我们有一个可靠的事件时间来源。这是流处理中的一个术语,虽然我们没有严格遵守这个定义--事件发生在浏览器中,而不是在后端--但我们无法从前端获得可靠的时间戳,所以这将不得不做。谷歌 "事件时间与处理时间 "可以了解更多这方面的信息。
我们在这个服务器中唯一没有实现的是实际的事件处理。这真的取决于你已经有哪些现有的基础设施,也取决于你期望有什么样的流量。
处理事件
对于流量不大的简单网站,我只需将该事件插入到一个关系型数据库系统(Postgres、MySQL等)。现代RDBMS可以很好地处理JSON数据,然后你可以有一些简单的ETL过程,将事件中的一些比特提取为关系对象,但总之,管道将保持相当简单。
如果你预计会有更多的流量和/或你想把你的应用程序与你的数据库解耦,有几个替代方案。我最喜欢的,也是我们在生产中使用的,是你简单地将事件发送到一个消息队列--不是Kafka或Pulsar,而是像RabbitMQ或ActiveMQ这样的普通AMQP。这些都可以处理大多数工作负载,而且它们很简单,经过了测试,更重要的是,在基础设施中已经很普遍。
现在完全由你来处理这个流。我们有三个独立的过程。
- 收集数据,一旦我们达到
n事件或m秒,无论哪个先到,就把它保存到S3(作为一个.json.gz)。这样我们就不依赖于流成为架构的持久部分,数据被安全地存储在我们的blob存储中。 - 不时地去把那些S3的blob压缩成大文件。在S3上有一堆小文件并不是最好的主意,所以我们把它们合并成较大的(每小时)块状。
- 将一些S3的数据加载到数据库中,这样我们就可以随时查询。
这是一个很好的工作管道--我们利用了整个堆栈的现有技术,对资源的利用微乎其微(<1%),我们可以轻松地将其扩展100倍,而不用担心管道的任何一个部分。
采集端点可以很容易地进行负载平衡,消息队列有足够的容量,S3的扩展超出了我们的需要,数据库可以换成Presto、Drill、Athena、BigQuery或任何类似的东西--我们已经有了blob存储的数据,所以这些大数据处理工具可以随时使用。
结论
显然,这不是唯一的解决方案。常见的替代方案是,你在前端使用 "你的 "库(它被捆绑在你的JavaScript中,但它实际上是你的供应商的代码)收集数据,将其发送到你的后端,然后从那里将其记录到你的供应商的API,绕过了前端的阻断器。
这只解决了我概述的其中一个问题,这就是为什么我们走另一条路。但是,你的里程可能会有所不同!另外,我省略了一些东西--错误处理、uuid生成、验证。这只是为了说明我们解决方案的原则。
这个解决方案有很多我喜欢的地方。
- 在架构方面,它超级简单。
- 它是组成的,而不是继承的,所以部分可以很容易地被交换、添加和删除。
- 它的扩展性远远超过我们的需要。
我希望你能从中发现价值。