最近开始尝试一些集群的玩意儿了，主要问题有二：如何通过主机名解析到IP；对于默认提供SSL的服务，证书怎么办？本文将阐述我的解决方法。最近开始搭建Ceph集群了，过程中遇到了两点问题。其中一点就是如果通过主机名来访问其他结点，那么旧必须要修改hosts文件，如果一次搭建还好，可摸索的过程中谁还不翻个车，我重装了6次系统才搞好Ceph集群。其中有不少弯路是因为装完系统之后没有改hosts而排查了半天。我：这怎么就连不上呢主机：有人没配hosts，但我不说是谁另一个问题就是现在大多数服务在启动后都会产生一个自签证书，对于简单的测试还好，可如果要长期运行，自签证书不光用起来麻烦，而且还是很大一个安全隐患——毕竟不能每次都看那个自签CA的公钥有没有变动，但是不检查的话被中间人攻击了也无从得知。如果决定使用自签证书的话，那么每一个服务的CA都不一样，就得在所有访问的设备上安装这个自签CA，并且如果这个自签CA的私钥泄露了，那么对于所有设备都是一个更大的安全隐患。经过一番谷歌搜索和一系列尝试，我自认为找到了一个相对不错的解决办法。以下详述：主机名解析到IP关于这个问题，最简单的解决办法就是利用域名直接解析成内网ip，这样方便又简单，有效避免了频繁改hosts的问题。但是，怎么设置域名呢？经过一番搜索，StackOverflow上无外乎两种声音：利用已有域名自己编造一个域名下面我分别说说这两种方法的利弊。利用已有域名利用已有域名最大的问题就是如何保证内网IP不泄露到公网——假如一个内网主机是192.168.1.1，你把它设置到了某个域名上，那么别人访问这个域名就会访问到对方内网上的主机，如果碰巧的话，可能就是他家的路由器。很明显，这会造成不必要的混乱。最好的办法就是设立一个单独的二级域名，例如internal.skyblond.info，然后在内网上搭建一个DNS服务器用来解析，这样的话公网上不会知道你的node1.internal.skyblond.info，而本地网络中又可以正常解析。而且这种方法的好处就是可以申请通配符SSL证书。至于坏处嘛，就是你得在本地网络上弄个DNS服务器。这个设施对于长期运行且颇具规模的网络来说必不可少，但是对于测试环境，有点杀鸡焉用牛刀的意思了。自己编造一个域名如果你没有域名，或者你的域名太长了，那么最好的办法就是随便编一个域名。一般来说不建议随便编造域名，但是如果一定要编造的话，那么最合理的应该是home.arpa。这个域名最开始被IETF提出，一开始是.home，但是后来改成了home.arpa，因为卖域名的不想把整个.home顶级域名拱手相让。虽然合理，但毕竟是二级域名，用起来的话就变成了三级域名了，何况都已经编造了，干嘛不编一个顶级域名呢？一般来说并不建议随意编造域名，即便是目前没有使用的顶级域名，在未来一旦分配到某个用途上，那对应的本地网络上的运维可能就要骂街了——鬼知道会不会有人用了你现在用的域名，万一发生冲突，谁知道会引发什么奇怪的bug。不过话是这么说，当你一定需要编造域名的话，最好符合行业标准。我们来看看AWS和GCP是怎么做的：Amazon EC2 instance hostname types：IP address-based namingWhen you launch an EC2 instance with IP address-based naming (IPBN), the guest OS hostname is configured to use the private IPv4 addressFormat for an instance in us-east-1: private-ipv4-address.ec2.internalExample: ip-10-24-34-0.ec2.internalFormat for an instance in any other AWS Region: private-ipv4-address.region.compute.internalExample: ip-10-24-34-0.us-west-2.compute.internalResource-based namingResource-based naming (RBN) is used automatically when you launch EC2 instances in IPv6-only subnets. RBN is not selected by default when you launch an instance in dual-stack (IPv4+IPv6) subnets, but it is an option that you can select depending on the subnet settings. After you launch an instance, you can manage the guest OS hostname configuration. For more information, see Modify RBN configurations.When you launch an EC2 instance with a resource-based hostname type, the guest OS hostname is configured to use the EC2 instance ID.Format for an instance in us-east-1: ec2-instance-id.ec2.internalExample: i-0123456789abcdef.ec2.internalFormat for an instance in any other AWS Region: ec2-instance-id.region.compute.internalExample: i-0123456789abcdef.us-west-2.compute.internal内部 DNS：Google Cloud 上的虚拟私有云网络具有一项内部 DNS 服务，允许同一网络内的实例使用内部 DNS 名称彼此访问。虚拟机实例的内部 A 记录是在 .internal 的 DNS 区域中创建的。虚拟机实例的 PTR 记录是在相应的反向区域中创建的。在您管理实例时，Google Cloud 会自动创建、更新和移除这些 DNS 记录。看起来巨头们都没有遵守RFC的建议，而是使用了.internal顶级域名在内部网络上使用。所以，当我们一定要编造域名的话，不妨也编造一个.internal域名。不过这样好像并没有解决问题——还是得在内网环境搭建一个DNS服务器。而且这种情况下由于无法通过CA的流程，所以编造的域名没法使用正常签发的SSL证书，就得用自签CA。我的解决办法反正我都有域名了，当然直接用已有的域名了。对于域名分配，像是master-node.skyblond.info或者172-16-71-100.skyblond.info这种域名固然简单，但我还有www.skyblond.info这样需要对公网提供服务的域名，两个混作一团实属不妥。于是我决定使用AWS的Route53托管internal.skyblond.info及其下的三级域名，过程也十分简单：创建一个托管区域，域名填写待托管的域名，例如internal.skyblond.info类型选择“公有托管区”，因为这个托管域名是面向公网解析的确认创建完成后需要在当前域名的DNS服务商处设置几个NS记录。NS记录的域是internal，指向托管区详细信息中的名称服务器，这一步相当于告诉现有的DNS服务器：internal及其下的域名现在由AWS的DNS服务器来解析。这样一来原有的网站不受影响，而针对内网使用的则单独由AWS托管。成本核算如下：每个托管区最高可以有一万条记录，超过一万条额外收费。一万条记录以内的托管区，每个每月0.5美元，标准查询每月每一百万个0.4美元。这里的查询是指实际打到AWS服务器的查询，如果查询过程中的DNS服务器有缓存，那么缓存的结果是不收费的。如果一个月要用满一百万次查询，那么就得每秒钟0.38次查询，相当于每3秒查询一次，7x24全年无休的查询。即便如此，每个托管区域每月的费用才0.9美元，不到7块钱。好处是之后所有的内网机器可以不需要配置DNS，直接用FQDN作为主机名即可解析。钱能解决的问题都不是问题。内网SSL证书还是上文的结论，编造的域名没法从商业CA处取得合法的SSL证书，只能使用自签证书。但是对于合法域名来说，这是可能的。但是现在大多数的通配符域名都只针对二级域名。例如我的网站SSL证书颁发给了*.skyblond.info，也就是说node1.skyblond.info、internal.skyblond.info和www.skyblond.info是可以被证书保护的，而node1.internal.skyblond.info却不能。如果为此单独买一个针对二级域名的证书，考虑到价格，好像有些犯不上。好在像Let's Encrypt这样的免费SSL提供商允许通过DNS-01的方式自动签发免费的通配符SSL证书，虽然有效期只有90天，但可以使用ACME Bot自动续期。使用过LNMP一键脚本的人可能有印象——如果没有SSL证书的话，选择Let's Encrypt就可以得到一个为期90天的免费证书，并且能够自动续期。DNS-01是ACME协议的一种验证方式，这种验证方式通过对域名添加一条指定内容的TXT记录来验证申请者对于域名的所有权，而ACME协议则做到了自动化。我这里使用的是acme.sh，这是一个基于shell的ACME机器人，只要填好信息，他就可以自动申请并续期证书，稍加配置还可以实现自动安装证书。如果采用DNS-01方式验证，它还支持一系列DNS服务商的API用以自动完成验证，而AWS的Route53刚好就是它支持的一个。首先需要在AWS IAM上创建一个API KEY，其中权限/策略如下（How to use Amazon Route53 API）：{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "route53:GetHostedZone", "route53:ListHostedZones", "route53:ListHostedZonesByName", "route53:GetHostedZoneCount", "route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets" ], "Resource": "*" } ] }这个IAM角色的配置可以用于所有Route53上的托管区域。随后安装acme.sh，过程不再赘述。安装完成后需要临时配一下环境变量：export AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY export AWS_SECRET_ACCESS_KEY=YOUT_SECRET_ACCESS_KEY然后就可以申请证书了：acme.sh --issue --dns dns_aws --keylength 4096 -d internal.skyblond.info -d '*.internal.skyblond.info'这行命令将申请一个密钥长度为4096位的证书，颁发给internal.skyblond.info和*.internal.skyblond.info，验证的方式为DNS，并且DNS服务商是AWS，通过预先配置的ACCESS KEY，他便能自动获取挑战，自动设计Route53记录，然后自动申请证书了。生成的证书在~/.acme.sh/internal.skyblond.info目录下。至此这个证书就可以配合先前的DNS解析一起使用了。
【歪门邪道】使用AWS记录加密货币的价格
本文将记述如何使用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/BTC，A/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.彳亍口巴。-全文完-