中国境内升级因git问题失败

我们在阿里云/Alibaba 的 Ubuntu 20.04 服务器上运行 Discourse 实例,与所有涉及 Git 的操作一样,我们因防火墙而面临问题。使用 launcher rebuild app 手动升级大多数时候会因 GnuTLS 错误(各种类型)而失败。这与服务器上安装的 Git 版本无关,实际上与防火墙内部的握手处理有关;当然我们并不了解细节,但多个来源对此进行了详细讨论。因此,改用 OpenSSL 手动编译 Git 也不是一个可行的方案。

有时拉取过程能越过核心部分,甚至成功克隆 Docker Manager 插件,但在拉取 2-3 个插件后,通常会出现超时或其他错误。

示例:

$ ./launcher rebuild app
Ensuring launcher is up to date
Fetching origin
Launcher is up-to-date
Stopping old container
+ /usr/bin/docker stop -t 60 app
app
cd /pups && git pull && git checkout v1.0.3 && /pups/bin/pups --stdin
fatal: unable to access 'https://github.com/discourse/pups.git/': gnutls_handshake() failed: The TLS connection was non-properly terminated.
76630913bae18d6b45b6b3ecc3ec390c1e69222a493f2ecf424ee06adf9d1002
** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one.
./discourse-doctor may help diagnose the problem.

这个错误也较为常见:

fatal: unable to access 'https://github.com/discourse/discourse.git/': GnuTLS recv error (-54): Error in the pull function.

潜在解决方案 1
通常从 GitHub 克隆时,使用 SSH 而非 HTTPS 会得到更好的结果或避免失败,但由于 Discourse 独特的重建任务,我不清楚该如何配置,以使 launcher 通过 SSH 而非 HTTPS 进行拉取。是否有可能配置 Discourse 实例以实现这一点?

潜在解决方案 2
作为另一种选择,我有一个可用的 SOCKS5 代理,用于绕过防火墙观看色情内容以访问中国境内被屏蔽的资源。我知道可以配置 Git 使用 socks:// 协议,但我不清楚如何在 Discourse 中设置配置,以使 Discourse launcher 的拉取过程能够使用该代理。我希望避免通过 git config --global 为 root 用户进行全局配置,而是希望将这些信息配置在 Discourse 仓库的配置中。有人能指点我如何实现吗?

这很麻烦,因为我们在内网中使用此 Discourse 实例,目前该实例已经基本停摆超过一个月,这显然对我们的运营造成了严重影响。

app.ymlenv 部分传递代理环境变量能解决问题吗?
以下是针对 GFW 环境下 Rubygem 的解决方案:

你看到 Replace rubygems.org with taobao mirror to resolve network error in China 了吗?

非常感谢。Ruby gems 不会引发问题,因为从一开始我们就在 app.yml 中集成了您在帖子中提到的模板,效果非常好。

问题在于克隆主仓库和插件仓库。

我需要查看用于 Git 标志的环境变量,可惜我不太擅长 Docker,尤其是 docker-compose 文件。您能否推荐一些相关资料?

据我所知,Discourse 并未使用 docker-compose。

我相信将以下命令添加到 before_web 钩子中可以让其正常工作,就像 web.china.template.yml 所做的那样:

git config --global http.proxy socks5://yourproxy:port

如果构建完成后不再需要代理,请将以下内容添加到 after_web 钩子中:

git config --global --unset http.proxy

所有钩子都在容器内运行,因此我认为这不会成为问题。

这再次证明了我对 Docker 是多么的无知。没错,这显然不是一个 docker-compose 文件。它叫"Docker 文件”吗?还是说那个术语指的是 config.json?不管怎样,你的建议为我指明了正确的方向,只是钩子名称应该是 before_code 而不是 before_web

简而言之:使用 shadowsocks-libev 设置一个 socks5 代理,监听本地机器的 172.17.0.1(而非 localhost),按你消息中的方式传递代理信息,然后重建应用。

我将在这里写一篇详细的指南,因为我猜还有其他人正在经历这种痛苦的体验。目前,我在主题组件仓库方面仍遇到问题,所以我的 rebuild 尚未成功,但我至少已经通过了所有插件的获取。

虽然这与主题略有无关,但我遇到的痛苦在于无法启动应用,因为现有的 redis-server 配置(我们在另一台机器上)与当前应用状态的实际状况不匹配。因此,我无法启动容器并通过 GUI 禁用主题组件,因为这些组件在克隆时会超时。

非常感谢你指引我查看你的解释,但我想补充几点,因为示例并不完全适用。

  1. 我不太明白这一点,抱歉?env 命令返回了很多信息,但没有任何与我的 gitconfig 相关的内容。
  2. 由于我不理解第 1 点,我无法确定需要传递哪些变量。我也没有在 app.ymlenv 部分添加 git flags,而是通过 hook 调用了它们。
  3. 这一步其实没有必要,因为我不希望整个容器都走 socks 代理,只需要 git fetch 进程走代理即可。不过我想,这一点可能更多是针对你提到的那个线程中的原始用例。

但再次感谢,你的建议为我指明了正确的方向。为 Discourse 团队点赞!:ok_hand:

您是否购买了阿里云并选择了中国大陆地区的节点?

香港/国际版阿里云不会出现此问题。

另外,也许您已经讨论过这个细节,但如果您尚未找到相关信息,这里有一个脚本,您可以运行它通过 OpenSSL 安装 git:

在中国境内无痛手动升级指南

步骤

  1. 在中国境外创建 SOCKS5 代理
  2. 在中国服务器上设置并配置代理连接
  3. 创建模板以简化编辑
  4. 将 Git 代理设置添加到模板
  5. 将模板包含在 app.yml
  6. 重新构建应用

1 - 远程 SOCKS5

为了方便使用(以及其友好的定价),我建议在新加坡等地设置一台 Digital Ocean 服务器。只需使用标准的 Ubuntu 服务器,完成所有基本的安全配置要求(SSH 密钥对、UFW 等),然后安装 Shadowsocks:

在远程机器上
$ sudo apt install shadowsocks-libev

配置代理设置:

$ cd /etc/shadowsocks-libev

# 我喜欢保留原始文件
$ sudo cp config.json orig.config.json
$ sudo nano config.json

请特别注意 timeout 和 method:

{
    "server":"123.123.123.123", # 远程服务器 IP
    "server_port":8400, # 由你决定
    "local_port":1080,
    "password":"Swordfish", 
    "timeout":600, # <= 至关重要!
    "method":"chacha20-ietf-poly1305"
}

务必仔细检查 systemd 配置(/lib/systemd/system/shadowsocks-libev-local@.service)中的所有设置。启用 shadowsocks-libev-local@.service,重启系统,并检查服务是否正在运行。

2 - 在中国服务器上设置代理连接

在 Discourse 机器上

$ sudo apt install shadowsocks-libev

如果你使用的是阿里云,请在他们奇怪的控制台界面中搜索防火墙设置,并检查相应的端口设置。

你不需要在客户端机器上对 systemd 设置进行繁琐的调试,但请为 Docker 和普通用途保留单独的配置文件,因为你可能希望在 Docker 上下文之外使用 SOCKS5 代理,此时你应使用 127.0.0.1 而不是 Docker 可访问的网络地址。

$ cd /etc/shadowsocks-libev
$ sudo cp config.json local.json
$ sudo cp config.json docker.json

将配置调整为类似以下内容:

$ sudo nano local.json

{
    "server":["123.123.123.123"], # 远程机器的 IP
    "mode":"tcp_and_udp", # 此注释因我环境中的 shadowsocks-libev 版本不同而有所差异
    "server_port":8400,
    "local_address":"127.0.0.1",
    "local_port":1080,
    "password":"Swordfish",
    "timeout":600, # <= 务必确认此项
    "method":"chacha20-ietf-poly1305"
}

为了方便起见,让我们在 .bashrc 中添加一个别名:

$ nano ~/.bashrc

# 粘贴
alias dockershadow='ss-local -c /etc/shadowsocks-libev/local.json'

调整另一个配置,让 Docker 通过宿主机的网络:

$ sudo nano docker.json

{
    "server":["123.123.123.123"],
    "mode":"tcp_and_udp",
    "server_port":8400,
    "local_address":"172.17.0.1",
    "local_port":1080,
    "password":"Swordfish",
    "timeout":600,
    "method":"chacha20-ietf-poly1305"
}

设置用于 Docker 特定配置的别名:

alias dockershadow='ss-local -c /etc/shadowsocks-libev/docker.json'

3 & 4 - 创建模板以保持 app.yml 整洁

这完全是可选的,取决于你的喜好;我更喜欢保持 app.yml 可读且简短,而将组件维护在其他地方。根据你的喜好给模板命名,我选择的是 web.git.template.yml

$ nano templates/web.git.template.yml
# 粘贴:

hooks:
  before_code:
    - exec:
       cmd:
         - git config --global http.proxy socks5://172.17.0.1:1080
         - git config --global https.proxy socks5://172.17.0.1:1080
         - git config --global https.sslVerify = false 

# 可选
  after_code:
    - exec:
       cmd:
         - git config --global --unset http.proxy
         - git config --global --unset https.proxy
         - git config --global --unset https.sslVerify

我曾用 after_web 钩子测试过,但不起作用。

5 - 调整 app.yml

在你的 app.yml 中调用模板:

$ cd /<discourse dir>
$ sudo nano containers/app.yml


templates:
  - "templates/web.template.yml"
  - "templates/web.china.template.yml"
  - "templates/web.ratelimited.template.yml"
  - "templates/web.socketed.template.yml"
  - "templates/web.git.template.yml"

你的模板部分很可能看起来不同,只需确保包含 web.chinaweb.git-blabla(或者你命名的任何名称)模板即可。不要app.yml 中暴露 1080:1080

6 - 重新构建

在重新构建之前,请验证使用 git 克隆时你的代理设置是否可用。

$ git config --global http.proxy socks5://172.17.0.1:1080
$ git config --global https.proxy socks5://172.17.0.1:1080
$ git config --global https.sslVerify = false 

这当然会将代理标志添加到用户家目录中的 .gitconfig 文件,因此测试后请注意将其删除。
在 GitHub 上选择一个包含大量文件的大型随机仓库,检查你的克隆速度。如果你的配置正确,你应该能够以约 12-15 MB/s 的速度进行克隆,具体取决于你的阿里云设置。如果你的连接速度从 200 KB/s 缓慢爬升至约 10 MB/s,那么你的努力就不算成功。

最后重新构建:

$ cd /<discourse directory>

# 使用之前设置的别名运行代理
$ dockershadow
$ ./launcher rebuild app

重新构建过程经常失败,因此你需要耐心(可能还需要白酒)。你在 app.yml 中设置的插件越少,重新构建成功的可能性就越大。

7 - 备注

我仍然认为这是一种变通方法,而不是生产就绪的流程,所以也许有人知道如何在中国境内镜像 GitHub 仓库,以减轻这种痛苦。众所周知,GFW 内部不透明的机制一直在变化。

当然,SOCKS5 代理只是众多选项之一,但我喜欢手边有多种用途的解决方案。

如果有人知道如何让这种变通方法达到生产就绪标准,我很感激你的建议。Discourse 是一款出色的软件,但我认为其在中国未被广泛使用的原因之一是安装和维护过程繁琐。去年尝试通过 GUI 升级时,无论我在 Nginx 反向代理中配置了哪些超时设置,失败率均为 100%。

中文翻译将随后发布

确实如此。由于该实例的主要用途是作为公司内网框架的一部分,而香港因延迟问题 unfortunately 无法作为选项。此外,即将上线的面向客户的实例将主要服务中国大陆用户——一旦我解决了微信认证问题,因此我需要为阿里云中国大陆区域找到一个可行的解决方案。

非常感谢。我查阅了多篇相关指南,但由于该问题的主要原因并非 Git 本身的 TLS 认证,而是 GFW 包检测流程中的握手检查,因此我未采用此方案。正如我所读到的,将 gitopenssl 编译在一起可能会打开一个充满痛苦的新世界。

大多数主题组件在构建时(或容器启动时?)也会从 GitHub 拉取,因此可能还有另一个钩子可以添加 Git 代理,这可能会有所帮助。如果你希望 GUI 能正常工作,请不要移除代理。此外,redis-server 似乎不会导致此问题。

redis-server 只是另一个问题,它增加了重建过程的复杂性。这有点像死循环:外部 redis 配置已更改,而重建前的应用状态却需要该特定 redis 配置才能启动。然而,由于主题组件获取功能失效,重建无法进行。

不过我很幸运,在运行了 20 到 20 次重建后,最终成功获取到了主题组件的更新。

从整体应用设计的角度来看,如果能提供关于如何在“安全模式”下重建的文档就好了,即独立于插件或主题进行应用重建。我找不到处理主题组件的钩子,也找不到如何停用(与卸载相对)插件的方法,这实在令人沮丧。

编辑:哇,现在我看到了安全模式的链接。我之前没找到(在中国无法使用 Google,只能尝试用必应查找任何相关内容)。天哪,这本来能帮上大忙的!

所以,你指定了一个托管 Redis 服务器(例如 Discourse with DO managed Redis - #3 by Falco

Redis 问题虽属次要,却为整体的 Git 问题增添了显著复杂性。从我上面的详细帖子可以看出,我已经解决了这些问题。

是的,我们一开始就将一个分布式 Redis 集群对接到了 Discourse。不过它并非托管服务,只是部署在其他机器上。

与 Redis 服务器的连接故障导致应用无法启动,因此我无法在图形界面中停用主题组件。
应用新的 Redis 配置需要重新构建应用,但由于无法从 GitHub 仓库拉取代码,这一操作无法完成。

https://meta.discourse.org/t/a-fork-of-discourse-docker-repo-for-china

如果有人即使在添加了 http 代理设置后仍遇到问题,

GnuTLS recv error (-110): The TLS connection was non-properly terminated.

除了原始解决方案外,请将以下 postBuffer 属性添加到模板中,以解决我的问题。需要安装 gnutls-bin

hooks:
  before_code:
    - exec:
       cmd:
         - apt-get update -y
         - apt-get install -y gnutls-bin
         - git config --global http.proxy socks5://172.17.0.1:1080
         - git config --global https.proxy socks5://172.17.0.1:1080
         - git config --global https.sslVerify false
         - git config --global http.postBuffer 1048576000

# optional
  after_code:
    - exec:
       cmd:
         - git config --global --unset http.proxy
         - git config --global --unset https.proxy
         - git config --global --unset https.sslVerify
         - git config --global --unset http.postBuffer