MENU

【歪门邪道】使用AWS记录加密货币的价格

December 8, 2021 • 瞎折腾

本文将记述如何使用 AWS 的相关服务完成定时抓取各种加密货币的实时价格,并将它们存入数据库中供以后查询。

最近很关注 NEO 这个区块链,一方面因为他是起源于国内,另一方面则是因为它比比特币环保,比以太坊便于开发,整个社区环境还算不错。前几天尝试了一下他们新主网上的 DEX,叫 Flamingo,感觉还不错。虽然我不是资深炒币人士,但是偶尔还是有使用加密货币的需求(主要是作为汽油费使用),所以对于历史价格有一个相对直观的把握,有助于我判断到底什么时候买币会更划算一些。而目前现有的各大交易所,大多数都将于 2021 年 12 月 31 日清退中国大陆用户,即便是不清退的,对于历史数据也比较有限,不能提供很好的查询服务。因此我决定自己写一个。

实现这个需求的大体思路就是利用 Java 请求一些公开的 API 来获取各种加密货币的价格,然后整理出我想要的数据之后存到 AWS 上,同时尽可能降低成本。所以写一个服务长期运行是不现实了,AWS 的 EC2 贵的要死,为这个专门买一个 Lightsail 也不划算,所以我决定使用 Fargate 来达到容器化部署,通过 EventBridge 实现定时调用,然后将数据存储到 DynamoDB 中。这样一来整个过程只有 Fargate 那块是按实际占用时间收费,其他的都是按量付费,例如 DynamoDB 只是按请求数和存储量收费,具体的每月成本可以看文末的分析。下面先说说程序怎么写。

程序

程序是比较核心的组件,当 Fargate 容器拉起来之后,AWS 的任务就完成了。我们需要在程序中计算关注的币种的价格。比较无聊的部分就不说了,无非就是调 API 拿数据,这里我用的是实时价格,因为它可以一次性返回某一时刻,币安上所有交易对的价格数据,这样的话一次 HTTP 调用,剩下的就用程序找就完了。

为了实现最大的灵活性,我并没有采取直接查交易对的方法,因为有些币种,例如 GAS,就只有和 BTC 的交易对,如果要查询 GAS 的 USDT 或 BUSD 价格,那就废了。所以得想办法搜索出一种可能的路径,于是就有了如下的算法:

  • private List<String> resolvePath(LinkedList<String> path, String toSymbol, Map<String, String> priceMap) {
  • String currentSymbol = path.getLast();
  • if (priceMap.containsKey(currentSymbol + toSymbol)) {
  • path.add(toSymbol);
  • return path;
  • }
  • int currentPathLength = path.size();
  • var option = priceMap.keySet().stream()
  • // related pair
  • .filter(it -> it.startsWith(currentSymbol))
  • // get the symbol
  • .map(it -> it.substring(currentSymbol.length()))
  • // not a loop
  • .filter(it -> !path.contains(it))
  • .map(it -> {
  • var tempPath = new LinkedList<>(path.subList(0, currentPathLength));
  • tempPath.add(it);
  • return resolvePath(tempPath, toSymbol, priceMap);
  • })
  • .filter(Objects::nonNull)
  • .min(Comparator.comparingInt(List::size));
  • if (option.isEmpty()) {
  • // not found
  • return null;
  • }
  • return option.get();
  • }

这个是一个简单的深度优先搜索,具有三个入参:

  • path:当前搜索出的路径
  • toSymbol:目标币种的符号
  • priceMap:全部交易对的价格数据

初始时 path 就是源币种的符号,我们以 A 币为例(虚拟的,万一以后真有这个币了,跟它没关系,下面的 END 也一样),初始就是一个 [A]。首先我们过一遍所有交易对,如果有 A/END,那万事大吉,搜索结束。如果没有,那就找出有哪些相关的交易对,例如 A/BTCA/ETH 之类的,然后提取出他们的名字,然后要检查这个币种是否已经搜索过了。搜索过就不要在尝试了,不然就成环了。对于剩下的不会成环的,挨个加到路径里看看行不行就是了。例如选定 BTC 进行尝试,那么递归调用的入参就变成了 [A, BTC],那么把 A 换成 BTC,再重复这个过程就可以了,最终的结果是:要么找到了交易对;要么搜索遍了所有币种,都没找到一个路径。前者直接返回路径,后者返回 null。对于顶层的递归调用,程序去掉没找到的(也就是返回值为 null)的,按照搜索路径的长度排序,拿个最短的作为结果返回。如果很不幸,一个也没找到,那就返回 null 就是了。

这里有几个局限性。首先是币安本身的局限性。前文我们以 BTC/ETH 这样的方式来表达交易对,但实际上币安在交易对中并没有使用任何分隔符,所以实际上程序得到的交易对是 BTCETH,这样就导致了程序没法自动推理出反向的价格,因为程序也不知道哪几个字母是一个币。当然,程序可以找币安列出所有的币的符号,然后挨个过一遍找最长匹配,但是那样会增加复杂度,而且我也没有需要反向推导的需求,因此就此作罢。

第二个则是内存的问题。我在深度优先搜索中使用链表的本意是希望底层能够复用结点,而不必说相同的数据存在好多个结点中。但遗憾的是,LinkedList 的 subList 方法是继承来的,并没有针对链表进行优化。所以很无奈,这个程序的内存占用可能在币种比较多的时候非常离谱。另外计算的时候使用了 BigDecimal 来避免小数溢出和精度损失,这样一来内存占用情况雪上加霜。

在获取到所有币种的价格后,存到 DynamoDB 的表里面,主键是当前的时间戳,后面的属性就按照币种和价格对应的来就可以了。完整的工程在 GitHub 上。

打包 Docker 镜像

程序准备好之后就是打包 Docker 镜像了。Docker 镜像越小越好,因为 Fargate 计费是从开始拉去镜像那一刻就计时了,镜像越大耗时越长。但是再怎么精简,Java 运行时还是精简不掉。程序本身加上各种依赖,只有不到 15MB,我选用了尽可能小的 azul/zulu-openjdk-alpine:11-jre-headless 作为运行时镜像,即便如此,打包之后还是将近 200MB。

AWS 准备

这里关于 DynamoDB 怎么建表就不说了。说说 ECR、Fargate 和 EventBridge 的配置吧。

首先使用 Fargate 需要自己弄一个 ECR,我这里建了一个私有的,因为并不需要公开。按照 AWS 官方的说明把刚刚构建的镜像推送上去即可。推送到 ECR 之后会得到一个镜像 URI,这个是之后在 Fargate 中指定任务用的。

首先创建一个 Fargate 集群,选默认的仅限联网即可,因为我们这个程序需要访问公网,但是不需要对外提供服务,因此可以直接使用随机分配的实例 IP 地址。在创建任务时需要注意任务角色,该角色必须具有 DynamoDB 对应表的写权限。对于任务的环境,我给了 1vCPU 和 2G 内存,应该够用了。

创建完成后可以手动触发一下该任务,看看能不能正常运行。如果可以,那么就可以配置 EventBridge 做自动触发了。手动触发时记得要允许分配公网 IP,并且选一个允许出站的安全组,不然 Fargate 没法访问公网,就获取不到币安的数据。我这边手动测试,从启动到结束一共不到 30 秒,其中程序运行了 4 秒,剩下的都是拉取镜像和启动 Java 虚拟机的耗时。

确认结果能正常写入之后,就可以开始配置 EventBridge 了。直接创建一个新的规则,在模式里面选定时(Schedule),然后设置好间隔就可以了。后面的 Target 要设定成我们的 ECS 任务定义,其中启动类型要选 FARGATE,然后联网那块需要手动填写子网和安全组,然后公网 IP 选 ENABLE 就行了。

刚开始建立任务建议使用较短的间隔,例如 2 分钟一次,看看能不能拉起来,如果没有任何反应的话,建议去 CloudTrail 中查看时间历史记录,搜索 RunTask 事件,找到相关的 ECS 调用,看看是哪里出了问题。

调取数据

调取数据不需要任何额外的程序,直接去 DynamoDB 的控制台,要么按 timestamp 筛选,要么直接按 timestamp 降序找最近的项目。得到查询结果之后就可以选下载到 CSV,之后就可以用 Excel 打开了。

如果在 Excel 中需要将时间戳转换为日期和时间,可以使用如下公式:

  • =(((A2/60)/60)/24)+DATE(1970,1,1)

这个转换出来之后是 UTC 时间。

成本估算

以每次调用耗时 30 秒计算,我选的每半小时抓取一次,则一天总耗时 24 分钟,一个月按 31 天计算,12.4 小时,就算 13 小时好了。我在 US East 1 区,这里的定价是每 vCPU 每小时 0.04048 USD,每 GB 内存每小时 0.004445 USD,这样的话,1vCPU 配合 2GB 内存,一个月的开销是 0.64181 美元,即便是考虑到 DynamoDB 的请求收费,每月也不到 1 美元,可以接受。

可靠性

本来昨天晚上写完代码部署好 AWS,准备隔天早上看看效果,没想到就遇到了 CloudWatchEvents operational issue

  • Event Delivery Delays
  • [02:54 PM PST] We have temporarily disabled event deliveries in the US-EAST-1 Region. Customers who have EventBridge rules that trigger from 1st party AWS events (including CloudTrail), scheduled events via CloudWatch, events from 3rd parties, and events they post themselves via the PutEvents API action will not trigger targets. These events will still be received by EventBridge and will deliver once we recover.
  • [03:00 PM PST] We have re-enabled event deliveries in the US-EAST-1 Region, but are experiencing event delivery latencies. Customers who have EventBridge rules that trigger from 1st party AWS events (including CloudTrail), scheduled events via CloudWatch, events from 3rd parties, and events they post themselves via the PutEvents API action will be delayed.
  • [04:31 PM PST] We continue to see event delivery latencies in the US-EAST-1 region. We have identified the root cause and are working toward recovery.

彳亍口巴。

- 全文完 -


知识共享许可协议
【歪门邪道】使用 AWS 记录加密货币的价格天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Archives QR Code
QR Code for this page
Tipping QR Code