在内部构建中央日志记录服务
已发表: 2022-03-10我们都知道调试对于提高应用程序性能和功能的重要性。 BrowserStack 每天在高度分布式的应用程序堆栈上运行一百万个会话! 每个都涉及多个移动部分,因为客户端的单个会话可以跨越多个地理区域的多个组件。
如果没有正确的框架和工具,调试过程可能是一场噩梦。 在我们的案例中,我们需要一种方法来收集在每个流程的不同阶段发生的事件,以便深入了解会话期间发生的所有事情。 使用我们的基础架构,解决这个问题变得很复杂,因为每个组件在处理请求的生命周期中可能有多个事件。
这就是为什么我们开发了自己的内部中央日志记录服务工具 (CLS) 来记录会话期间记录的所有重要事件。 这些事件可帮助我们的开发人员识别会话中出现问题的条件,并帮助跟踪某些关键产品指标。
调试数据的范围从 API 响应延迟等简单的事情到监控用户的网络健康状况。 在本文中,我们分享了构建 CLS 工具的故事,该工具每天从 100 多个组件可靠、大规模地收集 70G 的相关时序数据,并使用两个 M3.large EC2 实例。
内部建设的决定
首先,让我们考虑一下为什么我们在内部构建了 CLS 工具,而不是使用现有的解决方案。 我们的每个会话平均发送 15 个事件,从多个组件到服务 - 转化为每天大约 1500 万个事件总数。
我们的服务需要能够存储所有这些数据。 我们寻求一个完整的解决方案来支持跨事件的事件存储、发送和查询。 当我们考虑第三方解决方案(例如 Amplitude 和 Keen)时,我们的评估指标包括成本、处理高并行请求的性能和易于采用。 不幸的是,我们无法在预算内找到满足我们所有要求的合适方案 - 尽管好处包括节省时间和最大限度地减少警报。 虽然这需要额外的努力,但我们决定自己开发一个内部解决方案。
技术细节
在我们组件的架构方面,我们概述了以下基本要求:
- 客户表现
不影响发送事件的客户端/组件的性能。 - 规模
能够并行处理大量请求。 - 服务表现
快速处理发送给它的所有事件。 - 洞察数据
每个记录的事件都需要有一些元信息,以便能够唯一地标识组件或用户、帐户或消息,并提供更多信息以帮助开发人员更快地调试。 - 可查询接口
开发人员可以查询特定会话的所有事件,帮助调试特定会话、构建组件健康报告或生成有意义的系统性能统计数据。 - 更快、更容易采用
与现有或新组件轻松集成,不会给团队带来负担并占用他们的资源。 - 低维护
我们是一个小型工程团队,因此我们寻求一种解决方案来最大限度地减少警报!
构建我们的 CLS 解决方案
决策 1:选择要公开的接口
在开发 CLS 时,我们显然不想丢失任何数据,但我们也不希望组件性能受到影响。 更不用说防止现有组件变得更加复杂的额外因素,因为它会延迟整体采用和发布。 在确定我们的界面时,我们考虑了以下选择:
- 将事件存储在每个组件的本地 Redis 中,作为后台处理器将其推送到 CLS。 但是,这需要对所有组件进行更改,并为尚未包含 Redis 的组件引入 Redis。
- 发布者 - 订阅者模型,其中 Redis 更接近 CLS。 当每个人都发布事件时,我们再次考虑到组件在全球范围内运行的因素。 在高流量期间,这会延迟组件。 此外,此写入可能会间歇性地跳到五秒钟(仅由于互联网)。
- 通过 UDP 发送事件,这对应用程序性能的影响较小。 在这种情况下,数据将被发送和遗忘,但是,这里的缺点是数据丢失。
有趣的是,我们在 UDP 上的数据丢失率不到 0.1%,这对于我们考虑构建这样的服务来说是可以接受的。 我们能够让所有团队相信,这样的损失是值得的,并继续利用 UDP 接口来监听所有发送的事件。
虽然一个结果是对应用程序性能的影响较小,但我们确实遇到了一个问题,因为 UDP 流量不允许来自所有网络,主要来自我们的用户 - 导致我们在某些情况下根本没有收到任何数据。 作为一种解决方法,我们支持使用 HTTP 请求记录事件。 来自用户端的所有事件都将通过 HTTP 发送,而从我们的组件记录的所有事件都将通过 UDP 发送。
决策 2:技术堆栈(语言、框架和存储)
我们是一家红宝石商店。 但是,我们不确定 Ruby 是否会成为我们特定问题的更好选择。 我们的服务必须处理大量传入请求,以及处理大量写入。 使用全局解释器锁,在 Ruby 中实现多线程或并发将是困难的(请不要冒犯 - 我们喜欢 Ruby!)。 所以我们需要一个解决方案来帮助我们实现这种并发。
我们还热衷于在我们的技术堆栈中评估一种新语言,这个项目似乎非常适合尝试新事物。 那时我们决定试一试 Golang,因为它提供了对并发和轻量级线程和 go-routines 的内置支持。 每个记录的数据点都类似于一个键值对,其中“键”是事件,“值”作为其关联值。
但是只有一个简单的键和值不足以检索与会话相关的数据——它还有更多的元数据。 为了解决这个问题,我们决定任何需要记录的事件都会有一个会话 ID 以及它的键和值。 我们还添加了时间戳、用户 ID 和记录数据的组件等额外字段,以便更容易获取和分析数据。
现在我们决定了我们的有效负载结构,我们必须选择我们的数据存储。 我们考虑过 Elastic Search,但我们也希望支持密钥的更新请求。 这会触发整个文档被重新索引,这可能会影响我们写入的性能。 MongoDB 作为数据存储更有意义,因为它更容易根据将添加的任何数据字段查询所有事件。 这很容易!
决策 3:数据库大小很大,查询和归档很糟糕!
为了减少维护,我们的服务必须处理尽可能多的事件。 鉴于 BrowserStack 发布功能和产品的速度,我们确信我们的活动数量会随着时间的推移以更高的速度增加,这意味着我们的服务必须继续保持良好的性能。 随着空间的增加,读取和写入需要更多时间——这可能会对服务的性能造成巨大影响。
我们探索的第一个解决方案是将某个时期的日志从数据库中移出(在我们的例子中,我们决定为 15 天)。 为此,我们每天创建一个不同的数据库,让我们无需扫描所有书面文档即可查找早于特定时期的日志。 现在我们不断地从 Mongo 中删除超过 15 天的数据库,同时保留备份以防万一。
唯一剩下的部分是用于查询会话相关数据的开发人员界面。 老实说,这是最容易解决的问题。 我们提供了一个 HTTP 接口,人们可以在其中查询 MongoDB 中相应数据库中与会话相关的事件,以获取具有特定会话 ID 的任何数据。
建筑学
让我们谈谈服务的内部组件,考虑以下几点:
- 如前所述,我们需要两个接口——一个通过 UDP 侦听,另一个通过 HTTP 侦听。 因此,我们构建了两台服务器,每个接口也各一台,用于监听事件。 一旦事件到达,我们就会对其进行解析以检查它是否具有必需的字段——这些是会话 ID、键和值。 如果没有,则丢弃数据。 否则,数据将通过 Go 通道传递到另一个 goroutine,其唯一职责是写入 MongoDB。
- 这里一个可能的问题是写入 MongoDB。 如果写入 MongoDB 的速度比接收数据的速度慢,则会产生瓶颈。 反过来,这会使其他传入事件饿死,并意味着数据丢失。 因此,服务器应该快速处理传入的日志并准备好处理即将到来的日志。 为了解决这个问题,我们将服务器分成两部分:第一部分接收所有事件并将它们排队等待第二部分,第二部分处理并将它们写入 MongoDB。
- 对于排队,我们选择了 Redis。 通过将整个组件分成这两部分,我们减少了服务器的工作量,给它处理更多日志的空间。
- 我们使用 Sinatra 服务器编写了一个小型服务来处理使用给定参数查询 MongoDB 的所有工作。 当开发人员需要特定会话的信息时,它会向他们返回 HTML/JSON 响应。
所有这些进程都在单个m3.large实例上愉快地运行。
功能请求
随着我们的 CLS 工具随着时间的推移越来越多的使用,它需要更多的功能。 下面,我们将讨论这些以及它们是如何添加的。
缺少元数据
随着 BrowserStack 中组件数量的逐渐增加,我们对 CLS 的要求也越来越高。 例如,我们需要能够记录来自缺少会话 ID 的组件的事件。 否则,获得一个会以影响应用程序性能和在我们的主服务器上产生流量的形式给我们的基础设施带来负担。
我们通过使用其他键(例如终端和用户 ID)启用事件记录来解决此问题。 现在,无论何时创建或更新会话,CLS 都会收到会话 ID 以及相应的用户和终端 ID 的通知。 它存储了一个可以通过写入 MongoDB 的过程来检索的映射。 每当检索到包含用户或终端 ID 的事件时,都会添加会话 ID。
处理垃圾邮件(其他组件中的代码问题)
CLS 也面临处理垃圾邮件事件的常见困难。 我们经常发现组件中的部署会生成大量发送到 CLS 的请求。 其他日志将在此过程中受到影响,因为服务器变得太忙而无法处理这些日志并且重要的日志被丢弃了。
在大多数情况下,记录的大部分数据都是通过 HTTP 请求进行的。 为了控制它们,我们在 nginx 上启用了速率限制(使用 limit_req_zone 模块),它会阻止来自我们发现的任何 IP 的请求,在很短的时间内命中超过一定数量的请求。 当然,我们确实会利用所有被阻止 IP 的健康报告并通知负责的团队。
规模 v2
随着我们每天的会话增加,记录到 CLS 的数据也在增加。 这影响了我们的开发人员每天运行的查询,很快我们遇到的瓶颈就是机器本身。 我们的设置包括运行上述所有组件的两台核心机器,以及一组用于查询 Mongo 并跟踪每个产品的关键指标的脚本。 随着时间的推移,机器上的数据大量增加,脚本开始占用大量 CPU 时间。 即使在尝试优化 Mongo 查询之后,我们总是会遇到同样的问题。
为了解决这个问题,我们添加了另一台机器来运行健康报告脚本和查询这些会话的接口。 该过程涉及启动一台新机器并设置在主机上运行的 Mongo 的从属设备。 这有助于减少我们每天看到的由这些脚本引起的 CPU 峰值。
结论
随着数据量的增加,为像数据记录这样简单的任务构建服务可能会变得复杂。 本文讨论了我们探索的解决方案,以及解决此问题时面临的挑战。 我们对 Golang 进行了试验,看看它与我们的生态系统的契合程度,到目前为止,我们已经很满意了。 我们选择创建内部服务而不是支付外部服务费用非常划算。 直到很久以后,我们也不必将我们的设置扩展到另一台机器 - 当我们的会话量增加时。 当然,我们开发 CLS 的选择完全基于我们的需求和优先级。
如今,CLS 每天处理多达 1500 万个事件,构成多达 70 GB 的数据。 这些数据用于帮助我们解决客户在任何会话期间面临的任何问题。 我们还将这些数据用于其他目的。 鉴于每个会话的数据提供的关于不同产品和内部组件的见解,我们已经开始利用这些数据来跟踪每个产品。 这是通过提取所有重要组件的关键指标来实现的。
总而言之,我们在构建自己的 CLS 工具方面取得了巨大成功。 如果这对您有意义,我建议您考虑这样做!