Zeka 的记事本

2021 年 4 月
关于 Nginx reverse proxy 只支持到 http1.1 的讨论
2021427 日说: Nginx

目前 Nginx 开启 http2 已经是常识。但是我所不解的是,Nginx 默认的反向代理(proxy_pass)http 协议只能支持到 1.1。

官网文档 查找发现,对于 proxy_http_version 只能支持到 1.1,而且默认开启的是 1.0。官方备注说明 1.1 是留给长连接(websocket 等)的,因此可以理解为如果没有长连接需求保留默认即可。那么反向代理的协议不同是否会带来影响?

官方邮件 给出了答案(添加了自己的理解):

  • http2 的解决问题在于 tcp 的连接重用.对于端(edge)用户到服务器,光是三次握手都可能带来巨大开销,此时重用效果很明显。对于国外应用,往往 ping 100 以上是常态,此时更加明显。但是对于反向代理的服务,我们往往放在内网,延迟低于 1ms,重用不重用影响不大。(在这里 有所提到)
  • 如果上面只是对重用没有必要说明的话,那么后面这一条就直接说明不可能了。http2 只会建立一条 tcp 隧道,这不符合我们的代理模型(多个不同 session,多服务负载均衡)。

在这里保存疑问,为什么 Nginx 不能与 upstream 建立多个持久 tcp 连接(http2),http 协议本身无状态,只需要将数据传输到代理端即可,这样似乎不会产生问题。

做出这个想法的原因是 Nginx 很多时候 reverse proxy 的一方不是本地,比如说各种反代站点。这些站点与被反代站点延迟就不是 1ms 了。在这种情况下,也许会发生即使 Nginx 声明了 http2,由于与反代站点通讯是 1.1 甚至以下,导致出现性能瓶颈(个人没有性能需求,因此上述只是想 法,不会进行实验测试)。

我想到的一个理由为反代站长期与被代理站点建立 TCP 长连接,而不是像浏览器一样随着关闭断开,可能导致被代理站点长期资源消耗。

顺便思考一下,反向代理网站从这一点来说安全性未知。因为你直接通讯的证书是反代站点的,即使声明了 proxy_pass 为 tls 流量,Nginx 在 pass 时完全可以解析包内容,这使用 lua 等可以非常方便的处理。

不管怎样,上面提到的邮件是 14 年的。现在是 21 年,这么多年过去 Nginx 没有实现,那么将来也不会。也许基于 UDP 的 QUIC 会带来变化,但是谁知道呢?

对于常规部署在本地的反代站点,1.1 协议不会成为性能瓶颈。但是一个问题是 1.1 协议会导致每一个 TCP 连接占用一个端口,换句话说 Nginx 瞬时单机并发受到反向代理限制,低于 65535。

对大型站点要进行突破 65535 限制,最简单的方案是分配多 ip。对个人站点,使用 unix socket 可能是更好选择。unix socket 的协议栈更浅,性能更好。它的缺点是不方便做负载均衡,因为 socket 要求开启在本地。

记一次 nignx 错误的配置导致 Certbot webroot 认证失败
2021426 日说: 网络 Nginx

失败的配置文件是这样的:

server {
        listen 80;
        listen [::]:80;

        server_name host;

        # enable lets encrypt webroot auth
        location /.well-known/acme-challenge/ {
                root /path/to/webroot;
        }

        return 301 https://$server_name:443$request_uri;
}

简单来说,实现了一个 301 跳转,将 http 定向到 https。当时为了省事直接将 return 写在了 server 块里,这样重定向效果是正确的。

但是这样会导致 nginx 直接忽视内部定义的其他 location 块,立即返回进行重定向。所以查看报错日志发现 acme challege 流量也被跑到了 443 口,而 443 口没有对应 acme 路由,导致失败。

将 return 包裹在 location / 块中即可:

server {
        listen 80;
        listen [::]:80;

        server_name host;

        # enable lets encrypt webroot auth
                
        location /.well-known/acme-challenge/ {
                root /path/to/webroot;
        }

        location / {
                return 301 https://$server_name:443$request_uri;
        }
}

BTW,第一次如果是使用 standalone 获取的证书的话,那么 certbot 配置文件将无法找到正确的 werbroot。之后再需要通过 webroot 更新就需要通过 --webroot-path 指定更新路径。一次生效后将会写入配置,之后就不需要了

关于联想笔记本安装 Linux 无法识别磁盘问题的解释
2021421 日说: Linux

随着 Linux 驱动的更新,信息可能会过时。 使用型号为联想 Y9000X(2020),16G 4K 版本。 参考的解释,可以看 知乎上的回答

在最开始的时候,不论是 Ubuntu 还是 Arch,在进入启动盘后,都无法发现磁盘。当时作为萌新自然是不懂的,看着网上的教程将 raid 模式改为 ahci 模式就解决了。

现在重新回顾这个问题,发现原因很简单,那就是 Linux 缺少针对英特尔的 raid 驱动。Linux 自然是有 raid 驱动的,而且远比 Windows 系统广泛(服务器)。但是为什么到这里就拉跨了呢?原因是此 raid 为英特尔推出的 rst 技术。

rst 黑科技

英特尔的 rst 技术是从软件上实现的,被称为软 raid,这种 raid 理论性能会比硬件组合的 raid 要弱,但是好处是可以非常方便(几乎没有成本)的开启 raid 模式。对于垃圾佬来说,这可是大名鼎鼎的傲腾(Optane)内存技术,使用他就可以将机械盘获得媲美固态的速度。很可惜官方似乎直接 表示了 Linux 不支持

也许几百年后 Wintel 这个牢不可破的联盟解体的时候,Intel 才会突然记起有个系统叫做 Linux 吧。

修改不会出现问题

那么问题就很明白了,我们数据都在一个磁盘上,而不是真正 raid 模式分散在各个磁盘上。所以可以直接修改控制器模式,而理论上不会影响我们的 Windows。照着教程将磁盘格式修改为 ahci 就能在不影响 Windows 的情况下按照完成了。

对于我们的本子,这没有问题。但是对于部分高端笔记本使用了双固态组 raid(有点害怕),要修改格式恐怕只能格式化了。

为什么 raid 模式就不行

但是还有一个问题, 咱们的本子只有 nvme 的 m.2 接口,通信上司是 PCIe。修改的 ahci 只作用于 SATA 协议。为什么修改 SATA 这个与固态似乎没有关系的设置会有效呢?我们的 Linux 用到的不会是 ahci 驱动的固态吧?

回答是放心,通过 lsblk 指令我们看到的是货真价实的 nvme 协议磁盘,并没有经过 ahci。

我们的磁盘是 nvme 驱动的,所以根本不会经过 ahci 协议,在设备管理器上检查也能发现固态是直接挂载在 NVME 控制器上的。所以修改 ahci 是直接绕过去了。

但是开启 rst 就不一样了,rst 是同时软件与硬件合作(iu 集成了相关控件,所以反过来说,au 就不会有这个问题),linux 无法调用 iu 的 rst 相关模块,自然无法读取到磁盘。

ahci 模式下,由于没有 rst 干扰,所以 Linux 直接使用 nvme 相关驱动去识别磁盘,这个过程就不与 ahci 有关系了。

raid 与 ahci 模式性能区别不大

测试 表明两种模式性能差别不大。当然这篇文章发布在 17 年,现在几年过去,虽然不排除有做特殊优化,但是对 Linux 系统来说,是基本享受不到这种好事的。

当然对折腾傲腾内存的垃圾佬来说就不一定了。

厂商默认开启 raid 模式

厂商是默认开启 raid 模式的。

那么问题来了,既然只有一块磁盘,而绝大多数笔记本都不会组 raid(m.2 接口数量本来就不够用,笔记本看重的是轻便性而不是可靠性),两者性能都一样,为何要默认开启 raid 呢?在上面的知乎回答似乎提是厂商与某牙膏厂 py 的结果,是为了防止安装 Linux。这篇文章 甚至提到了因为 rst 导致硬盘本身的管理系统失效,甚至出错等问题。

所以也许只有厂商才能解释清楚了。

所以对于咱们这个本子来说,将格式从 raid 修改为 ahci 是没问题的(在正确操作下),这也解释了为什么 Linux 无法识别到我们的固态。

油猴脚本提前注入脚本
2021415 日说: 前端

之前看到这个 脚本 有点意思,但是美中不足是打开页面图片不会直接替换,而是需要闪烁一下。这已经很影响用户体验了。

为此我重写了一份脚本,发布在 这里

脚本作用很简单,但是使用了一点技巧才最终去掉了闪烁问题。

首先要弄懂为什么要闪烁。如果翻阅 API 文档,就会发现油猴脚本默认执行时间为 document-idle,也就是在文档树解析完成,解释引擎空闲的时候才注入执行。这个默认值是合理的,因为现代应用大多都使用大量的 js 操作 DOM(尤其是 Vue,React 等框架),等待解析完成之后执行不会出现 DOM 未找到的问题。

所以闪烁原因出在这里了。如果要在解析完成之后才注入脚本,那么我们的执行是延迟的。所以为了能尽早注入,我们至少需要添加:

// @run-at document-start

通过这个脚本能告知油猴脚本尽快注入,然后一系列问题出现了。在这个脚本中,出现的主要问题是找不到 DOM 节点。

这个问题有几种产生原因。

浏览器边下载文档边解析

其实是老生长谈了。在最开始学 JavaScript 的时候,就已经有人告诉你 style 要放在最前面,script 要放在最后面。

稍微了解 浏览器工作原理就知道当浏览器是边下载 document 边解析的,所以很有可能脚本在注入的时候 DOM 甚至不完全(此处存疑,没有具体考证),这种情况是无法拿到后面 DOM 的。当然这种情况往往只有解析大量 DOM 结点,而你希望操作很后面结点的时候才会发生。

遇到这种情况,我们期望能在 document 准备完毕之后再次尝试,最简单的可以通过事件 DOMContentLoaded。一个可行的 demo 大概是这样的:

// 直接获取结点,不一定能用,使用 try 捕获异常
const nodeNotYetReady = document.querySelector('#id');
try {
    nodeNotYetReady.doSomeMethod();
} catch (_) {
    window.addEventListener('DOMContentLoaded', () => { 
      // 结点应该 OK,可以使用了
      const nodeReadyWithDOM = document.querySelector('#id');
      nodeReadyWithDOM.doSomeMethod();
 })
}

动态渲染

对于动态渲染,在主程序没有执行完毕的时候可能会无法拿到节点。 以 Google 首页 为例,由于 Google 有 doodles 项目,所以首页 logo 会不断改变,这是通过 DOM 操作实现的。

我们需要知道什么时候 Google 的图片已经准备好了,有两种方式,即轮询与订阅。订阅用到的是 MutationObserver 接口,不论性能还是速率(因为这个事件为微任务,能插队执行)都比较好。文档可以看 这里

也可以做个简单轮询:

const doSomething = () => {
  try {
    // 执行代码
  } catch (e) {
    // 触发最短延迟重试,可以添加 flag 控制重试次数,防止爆栈
    setTimeout(doSomething);
  } 
};

还有一个问题是原脚本是每次即使发送请求的,所以网络 IO 导致了闪烁。这个的解决方式为将百度的 logo 转化为 base64,然后硬编码到脚本中(事实证明local torage 获取速度非常快,所以也可以通过它来存储)。通过上述执行,效果不错,至少都没有闪烁了。