我的Golang热重载工具Air不好使了

我使用 VSCode Remote-Containters 作为 golang 开发环境,因为生产环境使用的镜像主要是 alpine,所以开发环境自然而然使用了 golang:alpine,对应 Dockerfile 的内容如下:

FROM golang:alpine
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.cloud.tencent.com/g' /etc/apk/repositories
RUN apk add alpine-sdk
RUN go env -w GOPROXY=https://goproxy.cn,direct
RUN go get github.com/cosmtrek/air

如上所示,出于众所周知的原因,我设置了 GOPROXY,并且安装了一个名为 air 的工具,熟悉 golang 的朋友都知道,它是用来实现热重载的,本来一切都正常,结果突然报错:「Setctty set but Ctty not valid in child」:

air

air

在 air 的 issue 里没找到对应的报告,不过在 golang 的 issue 里倒是发现了一些线索:

If tty is going to be open in the child process, then it must have a file descriptor in the child process. When using os/exec.Cmd as the creack/pty code does, tty must be in Stdin or Stdout or Stderr or ExtraFiles. If it isn’t in any of those, then it will be closed when the child process runs. If it is in one of those, that gives you the file descriptor number that you should use in Ctty.

Right now the creack code is setting Ctty to the parent’s file descriptor number. That is working (assuming that it is working) by accident. The correct fix is to ensure that tty is passed to the child somewhere, and set Ctty to the appropriate descriptor number. If it is possible for all of Stdin, Stdout, Stderr to be set, the simplest approach would be to always add tty to ExtraFiles, and set Ctty to 3. That will work for old versions of Go and also for the upcoming 1.15 release.

我只想让 air 正常工作,并不想深究工作原理,好在里面提到了 creack/pty,而 air 正好依赖它,于是顺藤摸瓜找到了对应的 issue,发现此问题是新版 golang 1.15 才出现的,并且已经修复了,可惜 air 没有升级 pty 版本,于是遇到新版 golang 后,问题就出来了。

恰好前几天 Golang 放出来 1.15 的正式版,因为我在 Dockerfile 里使用 golang:alpine 作为标签,并没有明确版本,相当于是 latest,也就是最新版 1.15,所以触发了问题。知道了问题的缘由后,解决方法就简单了,两种方法:

  • 改用 golang:1.14-alpine3.12 这种有版本的标签绕开问题版本。
  • 使用 go get 命令的时候,应该尽可能加上 -u 选项,以便能自动升级版本。

最好的方式莫过于继续使用新版 golang 1.15,同时给 go get 加上 -u 选项:

FROM golang:1.15-alpine3.12
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.cloud.tencent.com/g' /etc/apk/repositories
RUN apk add alpine-sdk
RUN go env -w GOPROXY=https://goproxy.cn,direct
RUN go get -u github.com/cosmtrek/air

不过当我们这么干的时候,发现我安装的 air 依然有问题,为了验证问题,我在一个干净的容器里手动安装,结果搞出一个匪夷所思的 v1.21.2 的版本来:

shell> go get -u github.com/cosmtrek/air
go: github.com/cosmtrek/air upgrade => v1.21.2

我在官网上根本查不到这个版本,直觉告诉我可能和 GOPROXY 有关,于是禁用后再执行,发现一切都正常了,好在 goproxy.cn 支持查询各个版本的下载量,于是我就查询了一下 v1.21.2 这个匪夷所思的版本,结果发现从 2020-08-07 开始一直有数据:

goproxy

goproxy

如此看来,问题的来龙去脉大概是这样的:2020-08-07 之前的某天,官方在升级打包的时候搞错了标签(v1.21.2),尽管很快删掉了,但是却被 goproxy.cn 给缓存了下来,之后发布的版本(v.1.12.X)虽然名义上是新版本,但是由于数字上都小于问题版本,结果导致是用 goproxy.cn 的用户在 go get 安装的时候加 -u 选项也得不到新版本。

让各个代理都删除错误版本显然并不现实,毕竟除了 goproxy.cn 还有 goproxy.io 等很多代理都可能有问题,其实只要重新发布一个保证大于 v1.21.2 的新版本(比如 v1.21.3)就可以了,在此之前,我们可以通过「go get -u github.com/cosmtrek/air@v1.12.4」这样的方式来固定主版本并升级依赖版本的权宜之计来缓解问题。

手把手教你用ETCD

一句话概括的话:ETCD 是一个基于 RAFT 的分布式 KV 存储系统。一个 ETCD 集群通常是由 3、5、7 之类奇数个节点组成的,为什么不选择偶数个节点?在集群系统中为了选出 LEADER 节点,至少要有半数以上的节点达成共识,举例说明:

  • 当集群有 3 个节点的时候,至少要有 2 个节点达成共识,最多容灾 1 个节点。
  • 当集群有 4 个节点的时候,至少要有 3 个节点达成共识,最多容灾 1 个节点。
  • 当集群有 5 个节点的时候,至少要有 3 个节点达成共识,最多容灾 2 个节点。

如上可见当节点数是偶数个的时候,系统的容灾能力并没有得到提升,所以节点数一般选择 3、5、7 之类的奇数,至于具体选择多少的话,一般推荐 3 或者 5 比较合适,太少的话系统没有容灾能力,太多的话 LEADER 节点的通信任务会变得繁重,影响性能。

继续阅读

白话布隆过滤器

日常开发中,一个常见需求是判断一个元素是否在一个集合中。比如当你在浏览器中输入一个网址的时候,浏览器会判断网址是否在黑名单里。通常的解决方案是直接查询数据库,看看是否存在相关的记录,不过这往往会比较慢,于是我们又会引入缓存来提升速度,可是当数据比较多的时候,缓存会消耗大量的内存。有没有既速度快又节省内存的解决方案呢?本文介绍一种算法:布隆过滤器(Bloom filter)。

继续阅读

如何在OpenResty里实现代码热更新

所谓「代码热更新」,是指代码发生变化后,不用 reload 或者 graceful restart 进程就能生效。比如有一个聊天服务,连接着一百万个用户的长连接,所谓代码热更新就是在长连接不断的前提下完成代码更新。实际上因为所有的 require 操作都是通过 package.loaded 来加载模块的,只要代码是以 module 的形式组织的,那么就可以通过 package.loaded 实现代码热更新,并且基本不影响性能。

继续阅读

手把手教你用OpenResty里的FFI

了解 OpenResty 的人应该知道,OpenResty 原本的 API 都是基于 C 实现的,不过在新版里都已经改成了基于 FFI 实现的,为什么这么做?因为 FFI 在效率上更有优势,除此以外,FFI 还有一个优点是可以很便利的和 C 交互,我们不妨设想一下,C 语言有那么多成熟的库,通过 FFI,我们可以轻而易举的引入到自己的应用中,何乐而不为呢?

继续阅读