01项目概述
警报地图显示当前空袭警报的位置:按州和地区划分,地图上有移动目标、前线、来自卫星的热点以及下面的监测频道信息。需要在几秒钟内得到答案的人会阅读它,并且是在手机上、夜间、在不良的移动互联网环境下阅读。
不过,这个页面只是产品的一部分。相同的数据通过开放的JSON API以密钥和速率限制的方式传出,并通过其他网站嵌入自己框架中的小部件。我们设计并构建了所有这些:收集器、存储、API、地图和小部件。
02背景和问题
回答“这里有警报吗”的数据并不集中在一个地方。官方警报按州发布。社区的地方警报来自不同的服务。目标的位置由监测频道用文字描述。前线由一个制图项目发布。火灾由卫星观察。五个来源,五种格式,五种对“现在”含义的不同理解。
第二个问题是沉默。任何一个来源都可能停止——一个频道变得安静,一个API开始超时——诚实的失败模式是承认这一点,而不是继续绘制昨天的图像。因为其来源消失而看起来平静的地图比没有地图更糟。
03项目目标
- 由五个独立来源组装而成的一幅图像,每个来源都有自己的刷新时钟;
- 在手机上、在弱连接、夜间几秒钟内得到的答案;
- 可以离开网站的数据:一个开放的API,带有密钥、限制和费用,以及一个可嵌入的小部件;
- 三种语言,因为受众不仅仅是本地的;
- 一个沉默的源必须被视为沉默,而不是平静;
- 服务器自己的地址绝不能到达它所读取的源。
04五个来源,一张图片
- 官方警报按地区 — 地图的骨干;
- 本地警报来自一个单独服务的社区和地区;
- 监控频道在Telegram中 — 从公共预览和对于封闭频道,通过消息协议本身读取;
- 前线,来自一个发布它的映射项目;
- 热点来自NASA卫星——从轨道上看到的火灾。
收集器作为两个长期运行的工作进程在系统监督下运行,每个工作进程独立重启,并写入本地存储。网站从不直接与源交谈:它读取收集器已经写入的内容,这就是为什么慢速源使地图变得更旧而不是不可用的原因。
05四个文件而不是一个数据库
数据存储在四个独立的SQLite文件中:警报日志、推送订阅及其每个区域的状态、与馈送相关的地图事件,以及带有速率计数器的API密钥。故意没有共享数据库——一个文件的故障不能使其他文件失效。如果推送订阅不可用,地图仍然可以绘制;如果密钥存储繁忙,页面仍然可以响应。
相同的文件由整个组的控制面板写入——发放密钥、重置警报状态——因此每个连接都以等待超时开始。SQLite默认情况下在忙碌的数据库上会立即放弃,而这里的“立即”意味着在工作进程仅仅写入一行时会出现错误页面。
06一页上三个时钟
不同的数据以不同的速度老化,因此以不同的速度刷新:每15秒警报,每10秒移动目标,每20秒馈送。一个共同的间隔在两个方向上都是错误的——对于目标来说太慢,对于馈送来说浪费,对于成千上万个打开的标签来说每几秒一次的无谓请求。
每个区块还携带其自己的“最后更新”。一个静默重绘的页面教会你在不应该信任它的时候恰好信任它;一个停止移动的时间戳是表示“这一部分过时”的最快方式。
07一个带有密钥和费用的开放 API
地图发布它所知道的内容:当前警报、事件、带有«oblast × time bucket»热图的分析端点、一天的节奏、目标类型和方向。匿名呼叫者有一个适度的每分钟限制;一个密钥可以提高这个限制,而那些在此基础上构建的用户的费用会逐步增加。
密钥从共享控制面板发放,绝不手动输入,使用量按密钥每天计算。一个端点故意对除了密钥拥有者之外的所有人关闭——这个端点告诉密钥它自己的费用、限制和消耗。其他一切都是开放的,因为一个隐藏其数据的公共服务地图仅仅是半个公共服务。
08一个可以在其他人网站上使用的小部件
小部件是产品中离开主页的部分。两种固定尺寸——状态条和方形面板——三种语言、两种主题,以及一个专门编写的框架政策,以便在这里允许嵌入,而在网站的其他地方不允许:地图和小部件可以被框架,账户和支持页面则不可以。
由于它生活在一个框架中,小部件没有自己的cookie,也不选择界面生成:对每个人来说看起来都是一样的。这是一个决定,而不是限制——一个根据访客之前选择而呈现不同的小部件在其他人的页面上是无法支持的。
09了解您所在地区的推送
订阅与一个地区相关,每个地区的状态与订阅并排保存。这就是为什么通知会在边缘到达——当你所在的oblast中的警报开始或结束时——而不是在每次刷新源时到达。区别就是整个产品:一个是信号,另一个是在一天内被人们关闭的噪音。
在相同数据之上有一个本地应用程序,它与应用程序 API 进行通信,而不是与网页进行通信。它有一个自己的健康端点来响应车队注册,因为存活检查的成本不能高于它所检查的工作。
10三种语言,包括小部件
地图支持乌克兰语、英语和俄语,API参考、账户页面和小部件也是如此。一个新的界面字符串同时添加到这三者中,并且一个检查会计算缺失的字符串:计数必须为零。半翻译的界面不会自我声明——它们悄悄地在另一种语言中显示一种语言,看起来对编写它们的人来说是没问题的。
地区名称是另一个故事:它们来自源,以自己的拼写方式出现,因此地图保留了自己的字典,而不是信任在数据流中到达的内容。
11阅读来源而不显示您的地址
收集器获取的所有内容都通过SOCKS5代理发送,包括用于封闭频道的消息协议——代理在代码中完成,没有额外的依赖。服务器的地址永远不会到达源,而决定阻止某人的源会阻止代理,而不是提供地图的机器。
速率限制器在API前面设置,原因正好相反:一个带有实时数据的公共端点吸引了抓取者,而每个密钥每分钟的限制是保持公共服务可用的关键,确保它能为编写它的人服务。
12技术栈
- PHP 8.3没有框架:一个小路由器、端点和渲染器;
- Python工作进程在系统监视器下——一个仅使用标准库,一个使用消息协议;
- SQLite,四个独立的文件,每个文件都有其存在的理由;
- Leaflet用于地图、哈希资产、具有固定大小的小部件;
- 按区域状态的网页推送,基于相同数据的本地应用;
- 每个出站请求的SOCKS5出口,API前的每个密钥速率限制。
13结果
一个在几秒钟内响应并在其五个来源之一停止时继续响应的地图。其他网站可以使用的数据:一个带有密钥和费用的开放API,以及一个支持三种语言和两种主题的小部件。事件边缘触发的通知,而不是每次轮询都触发。基于相同数据的本地应用,以及一个发放密钥和重置状态的控制面板。
14要点
我们会带到任何实时数据项目的三件事。将收集器与页面分开:一个在访客等待时获取源的网站是一个在源出现问题时就会崩溃的网站。给每个区块一个自己的时钟和时间戳:单个时间间隔在两个方向上都是错误的,静默重绘隐藏了过时。说出你不知道的事情:一个承认自己空白的区块比一个看起来平静但数据是昨天的地图更有价值。
还有一个产品教训:数据一旦可以嵌入到其他地方,部件就成为了产品。它需要固定的尺寸、自己的框架政策和翻译——与主页面一样的关注,而不是它的剩余部分。
如果你有实时数据并需要地图、API和其他网站可以使用的部件,这就是我们从头到尾所做的工作。



