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