普通视图

发现新文章,点击刷新页面。
昨天以前Mythsman

Chromium + Mitmproxy 组合使用踩坑

作者 mythsman
2023年3月21日 15:57

背景

众所周知,Chromium 目前是事实上的地表最强浏览器内核,Mitmproxy 是事实上地表最强的中间人代理工具。二者组合使用可以非常方便的进行控制与数据分离的自动化数据提取。不过在实际生产中大规模使用时,还是会或多或少的遇到了一些难以察觉的坑。。。

Mitmproxy 低版本长期运行易 OOM

现象

在容器中部署 chromium + mitmproxy 后,发现在多次访问某些类型网站时,mitmproxy 经常周期性地出现内存缓慢增长,直到超过 docker 限制而被 OOMKilled。虽然有 docker 的自动重启进程功能,但是总会不可避免的导致业务上网络连接的周期性断开。

分析

初步怀疑是流量本身过多(chromium 对 mitmproxy 是“多对一”)以及给 mitmproxy 分配的内存过低导致内存不足。于是尝试将 mitmproxy 的内存配额从 200MB 增长到 1G。

但是实际结果却是这只是延长了 OOM 的时间,并没有解决问题。于是考虑是出现了内存泄漏问题,但是业务脚本无论如何也排查不出问题,因此只能暂时用 docker 自动重启进程的功能保持服务的大致可用。

同时发现似乎在 chromium 中增加 --disable-http2 的启动参数后,内存泄漏的情况会有所缓解。

解决

经过一段时间,偶然回头一看才检索到 mitmproxy 有一个 #4786 的相关 issue。原来在较低版本中(8.0.0及以下),拦截的 HttpFlow 长连接对象的确存在连接泄漏导致内存不断膨胀直至 OOM 的问题。(这样一想强制关闭 http2 长连接的确有概率会降级到短连接,从而缓解长连接的 OOM 问题。)

这个问题终于在 8.1.0 版本得到了修复(CHANGELOG):

我们要做的就是直接使用最新稳定版的 mitmproxy 即可。不过这件事情也没有想象中的容易。

如果你的系统是 ubuntu:focal (20.04 LTS) 的版本,默认安装的 python3 版本应当是 3.8.x ,这时你会发现无论如何也装不上 mitmproxy@8.1.0 版本:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.5 LTS
Release: 20.04
Codename: focal

$ python3 --version
Python 3.8.10

$ pip3 install mitmproxy==8.1.0
ERROR: Could not find a version that satisfies the requirement mitmproxy==8.1.0 (from versions: 0.8, 0.8.1, 0.9, 0.9.1, 0.9.2, 0.10, 0.10.1, 0.11, 0.11.1, 0.11.2, 0.11.3, 0.12.0, 0.12.1, 0.13, 0.14.0, 0.15, 0.18.1, 0.18.2, 0.18.3, 1.0.0, 1.0.1, 1.0.2, 2.0.0, 2.0.1, 2.0.2, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, 4.0.0, 4.0.1, 4.0.3, 4.0.4, 5.0.0, 5.0.1, 5.1.0, 5.1.1, 5.2, 5.3.0, 6.0.0, 6.0.1, 6.0.2, 7.0.0, 7.0.1, 7.0.2, 7.0.3, 7.0.4, 8.0.0)
ERROR: No matching distribution found for mitmproxy==8.1.0

这里的关键是要记得去 pypi 上去看下 mitmproxy 对不同 python 版本的支持:8.0.0 的最低支持 python 版本是 3.8;而刚巧修复了 bug 的 8.1.0 的最低支持 python 版本就跳到了 3.9。于是这里又要继续升级 python3 到 3.9 以上。

这里又有两条路:要么需要在 20.04 的 ubuntu 里增加新的 python3.9 的源,把老的 python3.8 的相关数据清理干净,再安装新的 python3.9 ;要么直接升级到 jammy (22.04 LTS)。

经过一番尝试,发现在老的镜像里升级 python3.9 还是非常麻烦的,处理不好经常会残留一些老版本的库。于是我这里选择了直接将基础镜像换成了 ubuntu:22.04 。

全部升级完成后,正常运行的 mitmproxy 的内存占用基本都会维持在 100MB 左右了,还是非常稳定的。

Chromium 忽略证书校验会导致缓存失效

现象

原先的系统架构是先启动一个 mitmdump 服务监听 8888 端口,再使用一个基于 chromium 内核的浏览器,通过 --proxy-server=localhost:8888 将流量指向代理服务,再通过 --ignore-certificate-erros 参数忽略对 mitmdump 的自签名证书的校验,保证流量器正常访问。

同时为了减少图片、视频等带来的带宽损失,结合具体任务,在 mitmdump 的脚本里将视频、图片等相关的请求 drop 掉,保持对流量的高效利用。

本来这就是一个非常的朴素、透明、易理解的普通架构,线上也稳定运行了多年,没啥大的变动也没人想着改。不过近期业务流量逐渐大了起来,发现出口带宽有点撑不住了。于是增加了个对响应体的 Content-Type 监控,发现流量的大头竟然是 application/javascript 这一类的东西。

这显然不太合理,因为这些 javascript 资源理论上都是走的 cdn,数据都会带 Cache-Control 相关 header 方便浏览器进行本地缓存。在重复执行类似网页的时候,大概率应当会复用之前已经缓存好的 javascript 文件。

分析

仔细审查了一下正常浏览器请求和线上环境下请求的资源请求情况,果然发现了不同点。

本地环境:

线上环境:

可见本地环境的各种 javascript 资源在多次请求时都是要么命中了 memory cache ,要么命中了 disk cache,从而正常节省了流量。而 线上环境的各种 javascript 资源却只会命中 memory cache 而从未命中过 disk cache。

仔细对比了二者的环境下 chromium 的启动参数差别,多次实验后(需要注意每次实验之间一定要清空用户目录)终于发现区别只在于 本地环境没有使用 mitmproxy 抓包,而线上环境配置了mitmproxy抓包 。在本地环境下配置了 mitmproxy 抓包后终于复现了线上场景。

经过一番搜索,竟然在 MicrosoftEdge 的项目 issue #2634 里找到了对 chromium 问题的解释,具体原因可以参见 chromium 这里的解释:

Status: WontFix : The rule is actually quite simple: any error with the certificate means the page will not be cached.

没错,chromium 做了这样一个规定:证书错误的页面不会被持久化缓存,即使你配置了忽略证书校验。

解决

问题原因发现了,解决起来也就容易了。至少有两种方案可以处理:

  1. 在 mitmproxy 层基于 Http 的 Cache-Control 相关协议,自己实现一层静态资源的持久化缓存。
  2. chromium 不配置 --ignore-certificate-errors ,而是直接想办法将 mitmproxy 的证书种到 chromium 信任的 CA 里,保证对 TLS 流量的正常解析。

实测下来,二者都能很好地优化大并发任务下的网络请求。javascript 相关请求量近似跌零,整体的流量会减少 70% 以上。不过总体看下来,方案二处理起来更加便捷和稳妥。

Chromium 默认不信任 Linux 下的系统证书

现象

话接上一个问题的解决方案二,想将证书种到 chromium 中其实并不简单。一个 Ubuntu 下的通用种 mitmproxy 证书的方法是:

  1. $HOME/.mitmproxy/mitmproxy-ca-cert.pem 中拿到 mitmproxy 的默认证书;或者自己用 openssl 生成一对证书+私钥,并放在 mitmproxy 的相应位置下。
  2. 将上述的 mitmproxy-ca-cert.pem 复制到 /usr/local/share/ca-certificates 下,并重命名为 mitm.crt (一定要以 crt 为后缀)。
  3. 执行 update-ca-certificates ,会自动将 mitm.crt 按证书信息重命名并软链接到 /etc/ssl/certs 中。

这样搞完,例如 curl wget 等绝大多数应用就都能认得我们自签名的证书了。

可惜 chromium 不是这绝大多数,实测下来依然不信任我们已经种在系统 CA 里的自签名证书。

分析

其实不信任系统默认 CA 证书的事情也很常见,比如很多 App 为了安全考虑会自己做 SSL Pinning,不信任用户机器上的证书;或者像 Java 这种工具为了跨平台的考虑也不会使用系统的证书,而是使用自己存储的 keystore。这里 Chromium 可能是也是出于类似考虑,反正也是默认只信任了自己安装时带过来的证书。对于用户新增的证书,也是希望直接通过软件本身的配置进行管理。

官方配置中添加自签名证书的方法是通过 chrome://settings/certificates 自行导入。

不过显然,这中配置方式对于打镜像并不合适,我们还是要寻找通过配置文件进行配置的方案。

解决

一番搜索后,从 superuser 中的这篇文章大概了解了 chromium 对自定义证书的管理方式。官方的说明是在 chromium 的 cert_management 文档中。

简而言之,Linux 下的 Chromium 使用的是公共 nssdb 来管理证书。数据存放在 $HOME/.pki/nssdb 下。

如果这个目录不存在,那么在第一次打开 Chromium 时会自动创建。不过对于预构建的环境来说,这里还是需要自己事先初始化下的。

具体步骤如下:

$ mkdir -p ~/.pki/nssdb                                       # 准备路径和文件夹

$ certutil -d ~/.pki/nssdb -N --empty-password                # 初始化DB环境

$ ls ~/.pki/nssdb/                                            # 查看DB文件
cert9.db  key4.db  pkcs11.txt

$ certutil -d ~/.pki/nssdb -L                                 # 查看证书信息(目前为空)

Certificate Nickname                                         Trust Attributes
                                                             SSL,S/MIME,JAR/XPI

$ certutil -d ~/.pki/nssdb -A -t "C,," -n mitm -i ~/mitm.crt  # 将准备好的证书导入进 CA

$ certutil -d ~/.pki/nssdb -L                                 # 查看导入后的证书信息

Certificate Nickname                                         Trust Attributes
                                                             SSL,S/MIME,JAR/XPI

mitm                                                         C,,

正常情况下,这样处理是没有问题的,不过具体使用时,还是踩了一些坑。

注意到 chromium 文档中给出的所有 nssdb 相关指令的 -d 参数和我上述用的有所不同,多带了一个 sql: 的前缀:

$ certutil -d sql:$HOME/.pki/nssdb -L

这是因为在本地测试时,由于 bash 用习惯了,直接用 ~ 代替了 $HOME 。结果命令敲出来结果就是这样:

$ certutil -d sql:~/.pki/nssdb -L
certutil: function failed: SEC_ERROR_BAD_DATABASE: security library: bad database.

报了一个奇怪的错,想了半天没想明白问题出在哪,随手试了试将 sql: 前缀干掉,发现一切又都能 work 了,也就是我上述记录的命令。

回头仔细看了下文档:

       -d [prefix]directory
           Specify the database directory containing the certificate and key database files.

           certutil supports two types of databases: the legacy security databases (cert8.db, key3.db, and secmod.db) and new
           SQLite databases (cert9.db, key4.db, and pkcs11.txt).

           NSS recognizes the following prefixes:

           o   sql: requests the newer database

           o   dbm: requests the legacy database

           If no prefix is specified the default type is retrieved from NSS_DEFAULT_DB_TYPE. If NSS_DEFAULT_DB_TYPE is not set
           then dbm: is the default.

原来 nssdb 是有两种模式的,可以通过为 -d 参数加不同前缀指定。但是坑爹的是如果指定了前缀,似乎就无法识别 bash 下的 ~ 。。。因此这里要么不用 ~ 、改用完整路径,要么就不指定 db ,使用默认配置即可。

最后,这个 pki 的文件权限也要注意,开启 chromium 的用户一定要对这个目录有读写权限。一个稳妥的方法就是 chown -R 一下,保证用户权限没问题。

这样一番配置后,终于可以在 chrome://settings/certificates 下看到新增的自签名证书了。

记一次家庭科学网络架构调整

作者 mythsman
2023年3月16日 13:17

背景

年初入了折腾软路由的坑,买了个 R66s 玩了玩。考虑到是头一次折腾,各个地方的配置都比较保守,没有默认将所有的网关都设置成 R66s ,而是由各个客户端手动配置到软路由网关。用了一段时间下来感觉在正常使用的情况下都还是很稳的,于是打算配置的更激进一点。

刚巧,之前移动的人总是打电话过来,说他们之前给我装的光猫有故障,把某个什么设备的端口打满了,导致影响了其他用户的使用(不过我用起来都很正常),希望能上门我换一个新的光猫。

考虑到一直接电话实在不胜其烦,于是就同意让他们上门换了一波。意外的收获是,这次不仅免费帮我多续费了四个月,而且除了给光猫之外,还多给了一个 WIFI 路由器🥹。

趁着新 WIFI 路由器的余威加持,就把家庭科学网络架构调整了一下,使其更好的符合家庭网络的两大宗旨(by jack stone):

  1. 所有设备统一在一个网段下,只有一层 NAT 转换,只有一个负责 NAT 的设备。
  2. 所有网络控制功能都应该尽力收归 OpenWrt 服务器。

设备说明

  • Raspi 是用来冒充 NAS,并另外部署一些 Docker 服务的树莓派4B
  • OpenWrt 是用来科学工作的 R66s 软路由
  • Modem 是移动安装的带 WIFI 功能的光猫路由器,使用默认的 NAT 模式。
  • Wifi Router 是移动后来多送的一个 WIFI 路由器。

旧架构(旁路由)

最初的需求只是想保守地尝试使用软路由进行科学工作,因此对原生的网络结构没有做任何调整。只是在初始的网络环境下新增了一个 Openwrt 作为网关服务器。

配置要点:

  1. Modem 配置不做任何变更(其实也没权限配置),默认自动拨号、默认使用 NAT 模式、默认开启 DHCP 服务。
  2. OpenWrt 的 LAN 口接入 Modem LAN 口,手动配置 IP 为 Modem 子网下的一个固定IP。
  3. 其他设备均正常接入。默认连上 WIFI 的设备不走 OpenWrt。需要时不时讲科学的机器就配置网关和 DNS 指向 OpenWrt 的 IP。

优点:

  1. 无需打电话找运营商改光猫配置。这一点对于经常搬家的社恐租房党还是很省事的。
  2. 设备要求少。无需额外 WIFI 路由器也能让手机等无线设备实现简单的科学功能。
  3. 网络层级简单。只有光猫的一层 NAT ,一层 DHCP。
  4. 容错能力强,OpenWrt 挂了也不影响正常设备的使用。

缺点:

  1. 科学配置比较麻烦。设备需要手动修改网关和 DNS 指向 OpenWrt。
  2. OpenWrt 无法获得完整的网络控制功能。由于 DHCP 使用的是 Modem 的,因此无法通过 OpenWrt 对家庭网络进行完整的控制。
  3. OpenWrt 无法获得完整流量。对于不走加密信道的情况,即使设备将网关配置指向了 OpenWrt,也只会让上行流量经过;下行流量则会直接由 Modem 返回设备。这就导致在 OpenWrt 部署的流量监控服务其实无法检测到完整流量。

新架构(主路由)

在有了白嫖的 WIFI 路由器之后,网络架构就可以灵活很多了。我们可以抛弃掉 Modem 自带的 WIFI,改用配置更方便的的 WIFI 路由器,并自动配置网关指向 OpenWrt。这样可以使得 OpenWrt 对下游的网络拥有完整的控制能力。

配置要点:

  1. Modem 仍然无需任何配置变更。不过出于效率,可以关闭 WIFI 功能。
  2. OpenWrt 用 WAN 口连接 Modem 的 LAN 口。WAN 口使用 DHCP 即可,LAN 口配置静态地址 IP 为一个与 Modem 不同的新网段,并开启 DHCP 服务
  3. Wifi Router 用 WAN 口连接 OpenWrt 的 LAN 口。调整配置为桥接模式,使得后续接入 WIFI 或者接入 LAN 口的其他设备都进入和 WAN 口的同一个子网。
  4. OpenWrt 在管控面板中为下游需要访问的设备配置静态IP。
  5. 后续如果需要添加有线设备,则直接接入 Wifi Router 空余的LAN口,或者在 OpenWrt 到 Wifi Router 之间新增小交换机即可。不要接入 Modem 上的空余 LAN 口(因为会不受 OpenWrt 管理)。

优点:

  1. 仍然无需打电话找运营商改光猫配置。
  2. OpenWrt 拥有对下游网络的全面控制权。
  3. 科学配置方便,无线设备等只要接入 WIFI 就科学了,不用做任何配置。
  4. OpenWrt 作为主路由能获得子网下的全部流量。

缺点:

  1. 配置稍显复杂,容易配错。
  2. 看起来有了两层 NAT ,网络略微复杂。但其实这里也可以把 Modem+OpenWrt 看成一个整体,其实真正用到的设备还是只在一层 NAT 下的。当然如果硬是想改成一层 NAT 也简单,只需要找运营商把光猫改下桥接,再让 OpenWrt 拨号即可。

Ghost 解决 jsdelivr 资源加载慢的问题

作者 mythsman
2023年1月27日 22:58

背景

用了很久的自建 Ghost 博客系统不知道从哪个版本开始,页面加载速度忽然变慢了很多。看了下加载的资源,发现多了很多走 jsdelivr cdn 的资源,加载速度竟然长达半分钟。。。

本来选择自建博客系统的重要目的之一就是为了页面加载速度可控,尽量避免加载不可靠、容易被墙的第三方资源。结果没想到 Ghost 官方又在核心模块里引用了第三方的 CDN。

不过还好 Ghost 项目本身的配置化做的还是不错的,大年初六上班摸个鱼的时间解决了一下。

解决

仔细看了下,新加入的走 CDN 的资源主要是 会员系统(portal)+评论系统(comments)+页面搜索 (sodo-search),因此在某次支持这些系统的更新前都是没问题的。不过考虑到目前的主题已经集成了这些系统,所以这些功能也不能禁用掉。

参考 Ghost Forum 的这篇讨论,可以通过在 config.[env].json 中修改配置,将 url 等替换成 self-hosted 的版本。不过这里的讨论中提到的配置来源并不清晰,在源码中搜索了一番发现了端倪(这里的 /var/lib/ghost 目录是我这 docker 里的 ghost 安装目录):

root@1f7b379a87f4:/var/lib/ghost/current# grep -r 'cdn.jsdelivr.net/ghost' *
core/shared/config/defaults.json:        "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
core/shared/config/defaults.json:        "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
core/shared/config/defaults.json:        "styles": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/main.css",
core/shared/config/defaults.json:        "url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js",
core/shared/config/defaults.json:        "styles": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/main.css",

可见这些配置都来源于 core/shared/config/defaults.json 这个文件,涉及到 CDN 的相关配置如下:

{

    ...

    "portal": {
        "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
        "version": "2.23"
    },
    "sodoSearch": {
        "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
        "styles": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/main.css",
        "version": "1.1"
    },
    "comments": {
        "url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js",
        "styles": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/main.css",
        "version": "0.12"
    },
    
    ...

 }

为了自托管这些文件,我们首先将这些文件下载到静态文件夹下,然后在想办法将 config.[env].json 配置好即可。

不过问题来了,静态文件夹有哪些?如果想当然的放在 /var/lib/ghost/content/public 下,肯定是要吃瘪的。正解应该是要看下 /var/lib/ghost/current/core/frontend/web/site.js 这里启动 express 的地方:

module.exports = function setupSiteApp(routerConfig) {
    debug('Site setup start', routerConfig);

    const siteApp = express('site');

    //...
    
    // Serve sitemap.xsl file
    siteApp.use(mw.servePublicFile('static', 'sitemap.xsl', 'text/xsl', config.get('caching:sitemapXSL:maxAge')));

    // Serve stylesheets for default templates
    siteApp.use(mw.servePublicFile('static', 'public/ghost.css', 'text/css', config.get('caching:publicAssets:maxAge')));
    siteApp.use(mw.servePublicFile('static', 'public/ghost.min.css', 'text/css', config.get('caching:publicAssets:maxAge')));

    // Card assets
    siteApp.use(mw.servePublicFile('built', 'public/cards.min.css', 'text/css', config.get('caching:publicAssets:maxAge')));
    siteApp.use(mw.servePublicFile('built', 'public/cards.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));

    // Comment counts
    siteApp.use(mw.servePublicFile('built', 'public/comment-counts.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));

    // Member attribution
    siteApp.use(mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));

    // Serve site images using the storage adapter
    siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
    // Serve site media using the storage adapter
    siteApp.use(STATIC_MEDIA_URL_PREFIX, storage.getStorage('media').serve());
    // Serve site files using the storage adapter
    siteApp.use(STATIC_FILES_URL_PREFIX, storage.getStorage('files').serve());
    
    //...
};

显然,这里 public 文件夹下的文件都是单独配置绑定的,不是整个文件夹的绑定。额外添加文件的话是不会映射到外部路径上的。这样一来,我们只能绑定在 images , media , files 这些路径下。再 check 下这些具体的变量,就会得知这些路径映射到的外部路径:

root@1f7b379a87f4:/var/lib/ghost/current# grep -r 'STATIC_.*_URL_PREFIX' *
...
node_modules/@tryghost/constants/index.js:    STATIC_IMAGES_URL_PREFIX: 'content/images',
node_modules/@tryghost/constants/index.js:    STATIC_MEDIA_URL_PREFIX: 'content/media',
node_modules/@tryghost/constants/index.js:    STATIC_FILES_URL_PREFIX: 'content/files'

我这里就选择放在 files 文件夹下,这里新建一个 self-host 文件夹:

root@1f7b379a87f4:/var/lib/ghost/content/files/self-host# ls
comments-ui-0.12.css  comments-ui-0.12.min.js  portal-2.23.min.js  sodo-search-1.1.css  sodo-search-1.1.min.js

由于我是通过 docker-compose 部署,相比于修改 config 文件,直接通过环境变量配置更为方便,变量名跟 config 中的 json 格式一一对应,这里不得不夸奖下 Ghost 的配置自动映射做的挺方便:

version: "3.8"
services:
  ghost:
  image: ghost:5.27
  container_name: "ghost"
  environment:
    ...
    portal__url: /content/files/self-host/portal-2.23.min.js
    sodoSearch__url: /content/files/self-host/sodo-search-1.1.min.js
    sodoSearch__styles: /content/files/self-host/sodo-search-1.1.css
    comments__url: /content/files/self-host/comments-ui-0.12.min.js
    comments__styles: /content/files/self-host/comments-ui-0.12.css
  ...

效果

首次加载速度直接从 30s 优化到了 300ms ,优化效果十分感人。。。

更新(2024.4)

考虑到 Ghost 经常会更新,每次都复制文件怪麻烦的,于是搭建了一个简单的 jsdelivr 代理 :

https://cdn.mythsman.com/jsdelivr/{path_to_jsdelivr}

这样只要在 docker-compose 做如下配置就好:

ghost:
    image: ghost:5.69.3
    container_name: "ghost"
    restart: always
    depends_on:
        - "mariadb"
    environment:
        # ...
        portal__url: https://cdn.mythsman.com/jsdelivr/ghost/portal@~2.37/umd/portal.min.js
        sodoSearch__url: https://cdn.mythsman.com/jsdelivr/ghost/sodo-search@~1.1/umd/sodo-search.min.js
        sodoSearch__styles: https://cdn.mythsman.com/jsdelivr/ghost/sodo-search@~1.1/umd/main.css
        comments__url: https://cdn.mythsman.com/jsdelivr/ghost/comments-ui@~0.16/umd/comments-ui.min.js
        # ...
    volumes:
        - ./ghost/:/var/lib/ghost/content
    expose:
        - "2368"

PDF字体乱码问题分析

作者 mythsman
2022年10月27日 18:03

背景

最近身体有恙,于是就想起来整理下这几年体检的电子报告汇总看看。结果发现在善诊平台下载的体检报告在电脑上看直接乱码了。不过这种字符集的问题怎么能难得了本码农呢,所以就尝试起各种办法进行恢复,结果发现这果然是个深坑。。。

问题

本来好好的一个 PDF 报告,结果打开后画风是这样的:

我本地的环境是 Macbook,尝试了用 Chrome 打开、自带的 Preview 工具打开,都是同样的结果。除了 ASCII 字符能展示,其他所有的中文信息都没有,但是 PDF 中的所有图片都能正常展示。这么看大概率还是中文字体的问题。

分析

EXIF信息

不管如何,首先肯定要看一下这个 PDF 本身带的 EXIF 信息,寻找一些分析线索。这里 mac 下可以直接在 Preview 里看,不过我更喜欢用命令行工具 exiftool 看。

$ brew install exiftool
$ exiftool -a -G1:2 report.pdf
[ExifTool]      ExifTool Version Number         : 12.42
[System:Other]  File Name                       : report.pdf
[System:Other]  Directory                       : .
[System:Other]  File Size                       : 522 kB
[System:Other]  File Permissions                : -rw-r--r--
[System:Time]   File Modification Date/Time     : 2022:10:27 11:06:00+08:00
[System:Time]   File Access Date/Time           : 2022:10:27 14:14:04+08:00
[System:Time]   File Inode Change Date/Time     : 2022:10:27 15:34:02+08:00
[File:Other]    File Type                       : PDF
[File:Other]    File Type Extension             : pdf
[File:Other]    MIME Type                       : application/pdf
[PDF:Document]  PDF Version                     : 1.7
[PDF:Document]  Linearized                      : No
[PDF:Document]  PDF Version                     : 1.7
[PDF:Document]  Tagged PDF                      : Yes
[PDF:Document]  Page Count                      : 11
[PDF:Document]  Page Mode                       : UseNone
[PDF:Document]  Title                           :
[PDF:Document]  Subject                         : FastReport PDF export
[PDF:Document]  Keywords                        : 宋体, Bold, -, SimSun;宋体, -, SimSun;微软雅黑, -, Microsoft, YaHei;微软雅黑, Bold, -, Microsoft, YaHei, Negreta;宋体, Bold, -, SimSun;宋体, -, SimSun;微软雅黑, -, Microsoft, YaHei;微软雅黑, Bold, -, Microsoft, YaHei, Negreta;
[PDF:Document]  Creator                         :
[PDF:Document]  Producer                        :
[PDF:Author]    Author                          : FastReport
[PDF:Time]      Create Date                     : 2021:02:24 15:35:03
[PDF:Time]      Modify Date                     : 2021:02:24 15:35:03

划一下重点:

  1. Keywords 字段似乎展示了他使用的字体信息,看起来都是微软字体。
  2. Author 和 Subject 字段似乎展示了生成这个 PDF 的工具:FastReport

众所周知 Mac 是不带微软字体的,那么这个问题似乎就是微软字体导致的。

微软字体替换

既然是 Mac 找不到微软字体,那我就把微软字体安装到本地应该就行了吧。于是我就找了个 Windows 电脑,跑去 C:\Windows\Fonts 下把所有的 ttf , ttc 文件拷贝到 MacBook 的 ~/Library/Fonts 下,作为当前用户的字体。

当然,这些字体看起来比较多,但是重要的还是 sim (中易系列)和 msyh (微软雅黑系列) 打头的这几个。

C:\Windows\Fonts> dir|findstr sim
2019/10/15  21:46        10,578,152 simfang.ttf
2019/10/15  21:46         9,753,388 simhei.ttf
2019/10/15  21:46        11,787,328 simkai.ttf
2019/12/07  17:08        18,214,472 simsun.ttc
2019/12/07  17:08        17,064,180 simsunb.ttf

C:\Windows\Fonts> dir|findstr msyh
2019/12/07  17:08        19,647,736 msyh.ttc
2019/12/07  17:09        16,829,116 msyhbd.ttc
2019/12/07  17:09        12,139,380 msyhl.ttc

可是,安装好后发现并不起作用,Chrome 和 Preview 还是不能用,感觉这就不太合理了。于是我把 PDF 拷回 Windows 下试试,发现果然连 Windows 自己都不能打开。。。

思考了一下,既然 PDF 格式主要是 Adobe 公司主导的,是不是他家的官方软件就能搞定呢?

Adobe Acrobat 的尝试

看网上有人说 Adobe Acrobat 有个 Chrome 插件,据说能解决很多乱码问题。于是就抱着试试看的心情装了一个。

果然没什么卵用。

考虑到 Adobe Acrobat 是收费的,为了这个小问题直接买有点不合适,又懒得找破解,于是就去 PDD 花 9¥ 买了个破解版。

软件是童叟无欺的,不过还是没什么卵用,依然预览起来都是乱码。而且还报了一个乱码的错,似乎是在说找不到一些字体(这些字体的名字是乱码的)。一番搜寻,发现有人在 Google Group 里提到:

通常对于字体的识别方式是先在文档内部寻找内嵌字体文件,如果没有字体文件,那么就根据文档所使用的字体名称在用户本地查找 相应的字体,最后使用替代机制——利用本地的默认字体来显示。
很不幸的是,一般的默认字体都是不识别非 acsii 字符的,所以就会出现各种乱码和字体很丑的 情况。

再结合报错时的一大堆乱码,那么我有理由进一步怀疑:如果文件的文本字符集有问题,而字体名又是非纯 ASCII 码的中文名,那么文本字符集的问题就会导致字体名的解析问题,进而就会导致字体解析的问题。但是究竟具体要怎么做才能修正字符集呢?不知道。

忽然想起来 PDF 传到电脑前明明在手机上预览过,于是我把目光转到了手机。。。

WPS 的逆袭

把 PDF 发回我的小米手机,发现果然能成功打开。

既然这样,那我是不是可以在手机上对 PDF 进行编辑,将有问题的字体切换成好使的通用字体,再重新保存呢?

小米默认采用的是 WPS 的组件,下载 WPS 后尝试编辑发现竟然又要冲会员。不能忍,于是下载了一个不要钱的 福昕PDF编辑器。结果福昕编辑器竟然又不能打开了。

难道说只能用 WPS 打开?于是我又在 Mac 上下载了 WPS,果然能打开了。。。

绕了一圈,能否正确打开这个 PDF 这件事跟平台没有任何关系,只跟软件有关。不得不说 FastReport 这个工具生成的 PDF 是真的流氓。

可是 WPS 的 PDF 编辑功能需要开通超级会员。没办法,只能又去 PDD 花了 2.9¥ 买了一个 ⭐️WPS 一天超级会员试用版⭐️

但是问题又来了,PDF 的编辑非常麻烦,每个文本框都是独立的,不能一次全选所有文本然后统一修改字体,只能一个框一个框的选。。。

忽然想到,为啥不先把 PDF 转成 Word,利用 Word 快速修改字体,然后再保存成 PDF 呢?

于是尝试了 WPS 的 PDF 转 Word 功能,按照刚才的思路,导出 Word 之后将所有文字的字体统一改成了 Arial ,再导出回 PDF。

终于成功生成了在各个平台都能正常使用的 PDF 。

iLovePDF

问题解决了,但是为了用一个小功能就要给流氓软件冲会员,实在不能忍。思考一番忽然明白了,我需要的其实只是一个“高兼容性的PDF转Word“工具——iLovePDF

有免费工具用了,谁还愿意交智商税呢?

将你的 PDF 转换为 DOC 和 DOCX 文件,以便于编辑。
将你的 PDF 转换为可编辑的 Office DOC 和 DOCX 文件。转换准确率近乎100%。
iLovePDF - 提供在线处理PDF文件的多种工具

记一次SSH下无法umount磁盘的问题

作者 mythsman
2022年10月25日 00:35

问题

最近在用 吃灰树莓派 + 外接硬盘盒 搭建 NAS ,由于硬盘默认的文件系统是 NTFS ,出于种种原因(参考这里),Linux 下的写入速度特别慢(我这里的酷鱼1T写入只有33MB/s)。于是我打算把硬盘格式化成 ext4

但是在测完速后准备 umount 当前磁盘(/media/pi/Seagate)时,却发生了报错:

$ dd bs=1M count=256 if=/dev/zero of=/media/pi/Seagate/test conv=fdatasync
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 8.04687 s, 33.4 MB/s

$ sudo umount /media/pi/Seagate
umount: /media/pi/Seagate: target is busy.

非常奇怪,我当前的 pwd 并不在挂载盘下,按理说应该没有进程会访问这个文件夹才对,可是他依然报了设备被占用。之前出现过这个问题时尝试过重新插入磁盘、重启机器等等操作,有时候也能莫名其妙的恢复了,但是这次正好有空,我希望能找到问题的原因。

分析

既然是进程占用了文件资源,那我就先来看下到底是谁:(注意,这里一定要加 sudo  ,否则可能会假装查不到)

$ sudo lsof /media/pi/Seagate/
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
sftp-serv 1242   pi    4r   DIR    8,0     4096    5 /media/pi/Seagate
sftp-serv 1242   pi    6r   DIR    8,0     4096    5 /media/pi/Seagate

看上去是一个叫 sftp-serv 啥啥的进程,拿到进程号 1242 ,看下到底是什么:

$ ps -ef|grep 1242 | grep -v grep
pi        1242  1241  0 00:10 ?        00:00:00 /usr/lib/openssh/sftp-server

原来是 sftp-server 进程,但是我并没有手动启这个进程,systemd 下也没有这个服务,那么这个进程是谁拉起的呢?

$ pstree
systemd─┬─agetty
        ...
        ├─ssh-agent
        ├─sshd─┬─sshd───sshd───bash───pstree
        │      └─sshd───sshd───sftp-server

原来是 sshd 拉起的,毕竟我是通过 ssh 登录到机器上的,访问机器上文件的功能应当就是通过 sftp 来实现的。

那么 sftp 为啥需要加载我的这个文件么?难道他会默认拉起所有的文件?我们回头查看下:

 $ sudo lsof |grep sftp
sftp-serv  1242                              pi  cwd       DIR              179,2         4096       1407 /home/pi
sftp-serv  1242                              pi  rtd       DIR              179,2         4096          2 /
sftp-serv  1242                              pi  txt       REG              179,2       105664      13203 /usr/lib/openssh/sftp-server
sftp-serv  1242                              pi  mem       REG              179,2        51640      25196 /usr/lib/aarch64-linux-gnu/libnss_files-2.28.so
sftp-serv  1242                              pi  mem       REG              179,2      1439544      11838 /usr/lib/aarch64-linux-gnu/libc-2.28.so
sftp-serv  1242                              pi  mem       REG              179,2       136992      11834 /usr/lib/aarch64-linux-gnu/ld-2.28.so
sftp-serv  1242                              pi    0r     FIFO               0,11          0t0     300723 pipe
sftp-serv  1242                              pi    1w     FIFO               0,11          0t0     300724 pipe
sftp-serv  1242                              pi    2w     FIFO               0,11          0t0     300725 pipe
sftp-serv  1242                              pi    4r      DIR                8,0         4096          5 /media/pi/Seagate
sftp-serv  1242                              pi    5r      DIR              179,2         4096     260625 /media/pi
sftp-serv  1242                              pi    6r      DIR                8,0         4096          5 /media/pi/Seagate
sftp-serv  1242                              pi    7r      DIR              179,2         4096       1407 /home/pi

看起来 sftp 也只占用了他自己的链接库以及我当前访问的、以及曾经访问过的页面。。。

解决

既然原因找到了,解决起来就很轻松了。看似最简单的方法就是干掉占用文件的进程 sftp-server 。事实上这个方案也的确能 work ,不过 kill 进程这种事情实在不优雅,万一出锅了呢?

最简单的方法其实就是:登出 ssh,再重新登录。毕竟 sftp 进程是依附于当前会话的 sshd 进程,只要退出这个 ssh 进程,sftp 进程也就能正常终止了。

Linux主机性能测试方法

作者 mythsman
2022年10月13日 17:11

背景

最近打算用躺家吃灰的树莓派4B搭一个NAS,用来快捷方便地访问和备份一些资源。由于备选的硬件(芯片、硬盘、网线、路由器等)和软件(内网穿透技术)的技术选型比较多,这时候就需要有一个能简单评估服务性能的方法。因此简单搜寻了一下常见方案,方便在技术选型时有个统一的对比标准,并且对一些常见指标能在数量级上有一些感性的理解。

硬盘

对于硬盘的读写速度测试,首先我们需要注意根据读写的实现细节不同,测试出的结果会有很大的差别。例如对于读来说,是否走缓存读、缓存的大小如何;对于写来说,是否只写缓存、是否同步等待刷盘、刷盘的时机如何,等等。

在实际测试的时候一定要明确自己使用的是哪种IO模式,否则就会得到一些似是而非的结论。

设备查询

在测试硬盘前,我们首先得知道我们有哪些硬盘、分别对应哪些分区。用 lsblk (list block)命令可以查看当前机器下挂载的块设备:

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda           8:0    1 29.3G  0 disk
└─sda1        8:1    1 29.3G  0 part /media/pi/5615-BDE2
mmcblk0     179:0    0 59.5G  0 disk
├─mmcblk0p1 179:1    0  256M  0 part /boot
└─mmcblk0p2 179:2    0 59.2G  0 part /

可以看到,这里的 sda (SATA device a) 表示我外部插入的一个U盘;mmcblk0 (Multimedia card block 0) 表示树莓派自带的一张 SD 卡。这两个类型是 disk,也就是实体磁盘。

每个 disk 会被分成多个 partition,也就是这里的 sda1 和 mmcblk0p1、mmcblk0p2。每个 partition 又会 mount 到不同的文件夹下,用于在文件系统中进行访问。因此对于文件系统本身来说,我们只会关心到 partition 层面。

通过 df 命令我们也能直接观察到所有分区的挂载情况:

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/root        59G   11G   46G  19% /
devtmpfs        3.5G     0  3.5G   0% /dev
tmpfs           3.7G     0  3.7G   0% /dev/shm
tmpfs           3.7G   65M  3.6G   2% /run
tmpfs           5.0M  4.0K  5.0M   1% /run/lock
tmpfs           3.7G     0  3.7G   0% /sys/fs/cgroup
/dev/mmcblk0p1  253M   32M  221M  13% /boot
tmpfs           738M  4.0K  738M   1% /run/user/1000
/dev/sda1        30G   23M   30G   1% /media/pi/5615-BDE2

同时,disk 和 partition 的详细信息也可以通过 fdisk 命令查看:

$ sudo fdisk -l
Disk /dev/mmcblk0: 59.5 GiB, 63864569856 bytes, 124735488 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x140cee6b

Device         Boot  Start       End   Sectors  Size Id Type
/dev/mmcblk0p1        8192    532479    524288  256M  c W95 FAT32 (LBA)
/dev/mmcblk0p2      532480 124735487 124203008 59.2G 83 Linux


Disk /dev/sda: 29.3 GiB, 31457280000 bytes, 61440000 sectors
Disk model: ProductCode
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xf3203eea

Device     Boot Start      End  Sectors  Size Id Type
/dev/sda1        2048 61439999 61437952 29.3G  7 HPFS/NTFS/exFAT

总之,在这里我们只需要搞清楚我们想测试的磁盘和分区分别是哪个即可。

hdparm

对于读性能测试,我们一般可以用 hdparm 工具(hard disk parameter? hardware device parameter?)。Debian下直接 apt 安装即可:

$ sudo apt install hdparm -y

hdparm目前只支持磁盘读性能测试,提供了三种方式进行测试:

  • 直接读内存: sudo hdparm -T [device]
  • 带buffer读磁盘: sudo hdparm -t [device]
  • 不带buffer读磁盘: sudo hdparm -t --direct [device]

以我的 mmcblk0 设备为例,跑出来结果分别如下(当然,每次测试建议跑多次取平均值,这里偷个懒):

$ sudo hdparm -T /dev/mmcblk0

/dev/mmcblk0:
 Timing cached reads:   1840 MB in  2.00 seconds = 921.43 MB/sec
 
$ sudo hdparm -t /dev/mmcblk0

/dev/mmcblk0:
 Timing buffered disk reads: 130 MB in  3.04 seconds =  42.74 MB/sec
 
$ sudo hdparm -t --direct /dev/mmcblk0

/dev/mmcblk0:
 Timing O_DIRECT disk reads: 124 MB in  3.01 seconds =  41.17 MB/sec
 

显然,走内存读是飞快,不过对于测试磁盘性能来说没有任何意义;不带buffer看起来比带buffer要慢一点点,差别不太大;考虑到现实场景中大多数都是带buffer的读,因此我们在比较时用带buffer读的结果来进行参考即可。

dd

dd (data definition? data duplicator?) 是进行磁盘操作、文件生成之类的常用工具。在 gnu 的 coreutils 下,类 Unix 发行版几乎都自带。

写性能

一般我们会用 dd 来进行磁盘写性能测试,一般来说也有三种方式:

  1. 直接写内存: dd bs=1M count=256 if=/dev/zero of=test
  2. 使用内存做缓存写完后一次性刷盘:dd bs=1M count=256 if=/dev/zero of=test conv=fdatasync
  3. 使用内存做缓存,每写完一部分就刷一次盘:dd bs=1M count=256 if=/dev/zero of=test oflag=dsync

还是以我的 mmcblk0 设备为例(当前目录即挂载的 mmcblk0 设备),跑出来结果分别如下(当然,每次测试建议跑多次取平均值,这里还是偷个懒):

$ dd bs=1M count=256 if=/dev/zero of=test
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 1.39574 s, 192 MB/s

$ dd bs=1M count=256 if=/dev/zero of=test conv=fdatasync
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 31.739 s, 8.5 MB/s

$ dd bs=1M count=256 if=/dev/zero of=test oflag=dsync
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 39.694 s, 6.8 MB/s

$ rm test

可见写内存不sync的确还是快,不过还是没啥参考意义。而由于 SD 卡本身写入速度就不高,因此这里 fdatasync 和 dsync 的差别不太大,如果是用更快的设备,这两个的差别会更大。考虑到实际情况下大部分程序都是采用 fdatasync 的模式来写,因此我们在比较时用这个数据即可。

虚假的测试

前几天发现有的业余测评UP主会在 1 的基础上加上 status=progress 参数来实时测写入速度,看起来还挺专业:

$ dd if=/dev/zero of=test status=progress bs=1M

为了方便文本展示,我把 '\r' 替换成 '\n' 以打印每一次刷新:

$ dd if=/dev/zero of=test status=progress bs=1M  2>&1 |tr -s '\r' '\n'

200278016 bytes (200 MB, 191 MiB) copied, 1 s, 199 MB/s
401604608 bytes (402 MB, 383 MiB) copied, 2 s, 200 MB/s
602931200 bytes (603 MB, 575 MiB) copied, 3 s, 201 MB/s
805306368 bytes (805 MB, 768 MiB) copied, 4 s, 201 MB/s
1007681536 bytes (1.0 GB, 961 MiB) copied, 5 s, 201 MB/s
1210056704 bytes (1.2 GB, 1.1 GiB) copied, 6 s, 202 MB/s
1309671424 bytes (1.3 GB, 1.2 GiB) copied, 17 s, 77.0 MB/s
1455423488 bytes (1.5 GB, 1.4 GiB) copied, 18 s, 80.9 MB/s
1563426816 bytes (1.6 GB, 1.5 GiB) copied, 20 s, 78.0 MB/s
1750073344 bytes (1.8 GB, 1.6 GiB) copied, 60 s, 29.2 MB/s
1860173824 bytes (1.9 GB, 1.7 GiB) copied, 72 s, 25.8 MB/s
1966080000 bytes (2.0 GB, 1.8 GiB) copied, 82 s, 24.0 MB/s
2051014656 bytes (2.1 GB, 1.9 GiB) copied, 98 s, 20.8 MB/s
2162163712 bytes (2.2 GB, 2.0 GiB) copied, 101 s, 21.4 MB/s
2250244096 bytes (2.3 GB, 2.1 GiB) copied, 113 s, 20.0 MB/s
2357198848 bytes (2.4 GB, 2.2 GiB) copied, 117 s, 20.1 MB/s
2459959296 bytes (2.5 GB, 2.3 GiB) copied, 129 s, 19.1 MB/s
2558525440 bytes (2.6 GB, 2.4 GiB) copied, 139 s, 18.4 MB/s
2656043008 bytes (2.7 GB, 2.5 GiB) copied, 152 s, 17.5 MB/s
2754609152 bytes (2.8 GB, 2.6 GiB) copied, 170 s, 16.2 MB/s
2850029568 bytes (2.9 GB, 2.7 GiB) copied, 182 s, 15.6 MB/s
2960130048 bytes (3.0 GB, 2.8 GiB) copied, 187 s, 15.8 MB/s
3064987648 bytes (3.1 GB, 2.9 GiB) copied, 201 s, 15.2 MB/s
3178233856 bytes (3.2 GB, 3.0 GiB) copied, 223 s, 14.3 MB/s
3300917248 bytes (3.3 GB, 3.1 GiB) copied, 235 s, 14.0 MB/s
3371171840 bytes (3.4 GB, 3.1 GiB) copied, 247 s, 13.6 MB/s
3460300800 bytes (3.5 GB, 3.2 GiB) copied, 259 s, 13.4 MB/s
3552575488 bytes (3.6 GB, 3.3 GiB) copied, 262 s, 13.6 MB/s
3668967424 bytes (3.7 GB, 3.4 GiB) copied, 276 s, 13.3 MB/s
3764387840 bytes (3.8 GB, 3.5 GiB) copied, 289 s, 13.0 MB/s
3872391168 bytes (3.9 GB, 3.6 GiB) copied, 300 s, 12.9 MB/s
3954180096 bytes (4.0 GB, 3.7 GiB) copied, 303 s, 13.0 MB/s
4066377728 bytes (4.1 GB, 3.8 GiB) copied, 329 s, 12.4 MB/s
4173332480 bytes (4.2 GB, 3.9 GiB) copied, 332 s, 12.6 MB/s
4251975680 bytes (4.3 GB, 4.0 GiB) copied, 353 s, 12.0 MB/s
4387241984 bytes (4.4 GB, 4.1 GiB) copied, 355 s, 12.4 MB/s
4513071104 bytes (4.5 GB, 4.2 GiB) copied, 371 s, 12.2 MB/s
4582277120 bytes (4.6 GB, 4.3 GiB) copied, 372 s, 12.3 MB/s
4652531712 bytes (4.7 GB, 4.3 GiB) copied, 377 s, 12.3 MB/s
4811915264 bytes (4.8 GB, 4.5 GiB) copied, 387 s, 12.4 MB/s
4869586944 bytes (4.9 GB, 4.5 GiB) copied, 388 s, 12.6 MB/s
4979687424 bytes (5.0 GB, 4.6 GiB) copied, 406 s, 12.3 MB/s
5100273664 bytes (5.1 GB, 4.8 GiB) copied, 411 s, 12.4 MB/s
5181014016 bytes (5.2 GB, 4.8 GiB) copied, 419 s, 12.4 MB/s
5307891712 bytes (5.3 GB, 4.9 GiB) copied, 420 s, 12.6 MB/s
5356126208 bytes (5.4 GB, 5.0 GiB) copied, 422 s, 12.7 MB/s
5462032384 bytes (5.5 GB, 5.1 GiB) copied, 436 s, 12.5 MB/s
5557452800 bytes (5.6 GB, 5.2 GiB) copied, 450 s, 12.3 MB/s
5667553280 bytes (5.7 GB, 5.3 GiB) copied, 463 s, 12.2 MB/s
5750390784 bytes (5.8 GB, 5.4 GiB) copied, 485 s, 11.9 MB/s
5892997120 bytes (5.9 GB, 5.5 GiB) copied, 495 s, 11.9 MB/s
5975834624 bytes (6.0 GB, 5.6 GiB) copied, 513 s, 11.6 MB/s
6086983680 bytes (6.1 GB, 5.7 GiB) copied, 515 s, 11.8 MB/s
6180306944 bytes (6.2 GB, 5.8 GiB) copied, 517 s, 12.0 MB/s
6287261696 bytes (6.3 GB, 5.9 GiB) copied, 530 s, 11.9 MB/s
6370099200 bytes (6.4 GB, 5.9 GiB) copied, 532 s, 12.0 MB/s
6490685440 bytes (6.5 GB, 6.0 GiB) copied, 542 s, 12.0 MB/s
6550454272 bytes (6.6 GB, 6.1 GiB) copied, 545 s, 12.0 MB/s
6658457600 bytes (6.7 GB, 6.2 GiB) copied, 563 s, 11.8 MB/s
6755975168 bytes (6.8 GB, 6.3 GiB) copied, 576 s, 11.7 MB/s
6856638464 bytes (6.9 GB, 6.4 GiB) copied, 592 s, 11.6 MB/s
7010779136 bytes (7.0 GB, 6.5 GiB) copied, 601 s, 11.7 MB/s
7055867904 bytes (7.1 GB, 6.6 GiB) copied, 603 s, 11.7 MB/s
7158628352 bytes (7.2 GB, 6.7 GiB) copied, 615 s, 11.6 MB/s
7265583104 bytes (7.3 GB, 6.8 GiB) copied, 631 s, 11.5 MB/s
^C

测试了整整十分钟,发现展示出来的速率硬生生从 200MB/s 降到了 11MB/s。事实上如果我愿意等,展示出来的速率还会再降。而且即使是最后展示的这个速率看起来已经低了不少,其实他也远比实际的速率要大很多,因为当我按 Ctrl+C 终止测试时,仍然有大量的数据没有真正落盘,整个程序仍然 hold 了很久才终止。最后真正落盘所消耗的时间显然是没有统计在前面计算的速率中的。

读性能

当然,有人也会利用 dd 进行读性能测试,比如:

$ sudo dd bs=1M count=256 if=/dev/mmcblk0 of=/dev/null
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 6.03649 s, 44.5 MB/s

看起来很美好,结果也和 hdparm 差不多。但是当你第二次再跑这个命令的时候,由于读缓存的存在,结果会快特别多:

$ sudo dd bs=1M count=256 if=/dev/mmcblk0 of=/dev/null
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 0.267236 s, 1.0 GB/s

显然这样的测试是没有意义的。考虑到这种测试方法甚至很难做到幂等,这里还是不建议用 dd 来测试读性能。

小结

对于磁盘读性能测试,建议使用 sudo hdparm -t [device]

对于磁盘写性能测试,建议使用 dd bs=1M count=256 if=/dev/zero of=test conv=fdatasync。当然, bs 和 count 也可以适当改大以进一步提高准确性。

网络

网速测试一般分两种,一种是测试当前设备对普通公网设备的读写速度;另一种是点对点测试两个服务器之间的速度。

speedtest

speedtest 工具在各地都有测速服务器,通过命令行(speedtest-cli)或者网页(https://speedtest.cn,https://speedtest.net)都可以进行网速上下行的测试。这里以命令行为例:

$ sudo apt install speedtest-cli -y

$ speedtest-cli
Retrieving speedtest.net configuration...
Testing from China Mobile (183.192.82.69)...
Retrieving speedtest.net server list...
Selecting best server based on ping...
Hosted by Chinamobile-5G (Shanghai) [8.49 km]: 6.054 ms
Testing download speed................................................................................
Download: 128.24 Mbit/s
Testing upload speed......................................................................................................
Upload: 22.88 Mbit/s

虽然多次测试可能会访问到不同的测试点、导致结果有区别,不过其实也大差不差了。

在使用时偶尔会遇到返403,不要慌,可以先多试几下,如果还是不行,可以加下  --secure 参数(参考Reddit) 。

iperf3

如果我们并不是想测试公网网速,而是测试两个服务器之间点对点的网速。这时用 iperf3 工具就好。

$ sudo apt install iperf3 -y # Debian 下
$ brew install iperf3        # MacOS 下

需要注意,除了 iperf3 之外,还有一个 iperf。这两个版本分别由不同组织开发,前后也不兼容。虽然似乎 iperf3 有坑,不过似乎功能多一点,尤其是支持了下行带宽测试(iperf 只支持上行带宽测试),所以这里还是用 iperf3。

iperf3 是 C/S 架构,服务端开启 server ,客户端开启 client,然后互相通信进行测速。以我在家的树莓派和一个在 HK 的 Azure 主机为例:

$ iperf3 -s -p 5555                   # azure 主机上开启 server

$ iperf3 -c 104.208.65.181 -p 5555    # pi 上开启 client 并连接 server

测试完成后,client 上会有报告:

$ iperf3 -c 104.208.65.181 -p 5555
Connecting to host 104.208.65.181, port 5555
[  5] local 192.168.1.2 port 35994 connected to 104.208.65.181 port 5555
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  8.68 MBytes  72.8 Mbits/sec    0   2.95 MBytes
[  5]   1.00-2.00   sec  1.25 MBytes  10.5 Mbits/sec  2018    252 KBytes
[  5]   2.00-3.00   sec  2.50 MBytes  21.0 Mbits/sec  1076    286 KBytes
[  5]   3.00-4.00   sec  3.75 MBytes  31.5 Mbits/sec  861    237 KBytes
[  5]   4.00-5.00   sec  2.50 MBytes  21.0 Mbits/sec   99    187 KBytes
[  5]   5.00-6.00   sec  1.25 MBytes  10.5 Mbits/sec    0    206 KBytes
[  5]   6.00-7.00   sec  2.50 MBytes  21.0 Mbits/sec    0    215 KBytes
[  5]   7.00-8.00   sec  2.50 MBytes  21.0 Mbits/sec    0    218 KBytes
[  5]   8.00-9.00   sec  2.50 MBytes  21.0 Mbits/sec    0    218 KBytes
[  5]   9.00-10.00  sec  2.50 MBytes  21.0 Mbits/sec    0    220 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  29.9 MBytes  25.1 Mbits/sec  4054             sender
[  5]   0.00-10.09  sec  26.5 MBytes  22.1 Mbits/sec                  receiver

iperf Done.

这里可以看出 client 对 server 的上行带宽大约是 22Mbit/sec。

类似的,server配置不变,client加上 -R 参数后可以测试出 server 对 client 的下行带宽:

$ iperf3 -c 104.208.65.181 -p 5555 -R
Connecting to host 104.208.65.181, port 5555
Reverse mode, remote host 104.208.65.181 is sending
[  5] local 192.168.1.2 port 36000 connected to 104.208.65.181 port 5555
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-1.00   sec  6.47 MBytes  54.3 Mbits/sec
[  5]   1.00-2.00   sec  25.7 MBytes   216 Mbits/sec
[  5]   2.00-3.00   sec  13.2 MBytes   111 Mbits/sec
[  5]   3.00-4.00   sec  16.3 MBytes   137 Mbits/sec
[  5]   4.00-5.00   sec  12.4 MBytes   104 Mbits/sec
[  5]   5.00-6.00   sec  14.4 MBytes   121 Mbits/sec
[  5]   6.00-7.00   sec  14.0 MBytes   117 Mbits/sec
[  5]   7.00-8.00   sec  13.9 MBytes   116 Mbits/sec
[  5]   8.00-9.00   sec  11.5 MBytes  96.7 Mbits/sec
[  5]   9.00-10.00  sec  11.3 MBytes  95.2 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.09  sec   142 MBytes   118 Mbits/sec  2560             sender
[  5]   0.00-10.00  sec   139 MBytes   117 Mbits/sec                  receiver

iperf Done.

这里可以看出 server 对 client 的下行带宽大约是 117Mbit/sec。

CPU&内存

sysbench

对于CPU和内存的性能测试,可以使用 sysbench 工具。

$ sudo apt install sysbench -y # Debian 下
$ brew install sysbench        # MacOS 下

对于我的树莓派测试如下:

CPU单线程测试,可以看出单核每秒操作数大约1483:

$ sysbench cpu --threads=1 run
sysbench 1.0.18 (using system LuaJIT 2.1.0-beta3)

Running the test with following options:
Number of threads: 1
Initializing random number generator from current time


Prime numbers limit: 10000

Initializing worker threads...

Threads started!

CPU speed:
    events per second:  1483.17

General statistics:
    total time:                          10.0002s
    total number of events:              14839

Latency (ms):
         min:                                    0.67
         avg:                                    0.67
         max:                                    1.88
         95th percentile:                        0.68
         sum:                                 9992.91

Threads fairness:
    events (avg/stddev):           14839.0000/0.00
    execution time (avg/stddev):   9.9929/0.00

内存读写测试,可以看出读写速度约是 1814MiB 每秒:

$ sysbench memory run
sysbench 1.0.18 (using system LuaJIT 2.1.0-beta3)

Running the test with following options:
Number of threads: 1
Initializing random number generator from current time


Running memory speed test with the following options:
  block size: 1KiB
  total size: 102400MiB
  operation: write
  scope: global

Initializing worker threads...

Threads started!

Total operations: 18590983 (1858174.07 per second)

18155.26 MiB transferred (1814.62 MiB/sec)


General statistics:
    total time:                          10.0001s
    total number of events:              18590983

Latency (ms):
         min:                                    0.00
         avg:                                    0.00
         max:                                    0.20
         95th percentile:                        0.00
         sum:                                 4463.57

Threads fairness:
    events (avg/stddev):           18590983.0000/0.00
    execution time (avg/stddev):   4.4636/0.00

需要注意的是,对 MacOS 的 CPU benchmark 似乎有坑,测试出来的结果异常的大,不具有参考价值。

md5sum

虽然 sysbench 的基准测试看起来比较靠谱,但是实际环境下,真正的执行效率还跟执行的指令啥的都有挺大关系。比如,我们以执行 md5 的速度来对比 CPU 的执行效率:

$ dd if=/dev/zero bs=1M count=1024 | md5sum
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 5.81996 s, 184 MB/s
cd573cfaace07e7949bc0c46028904ff  -

通过 dd 命令向 md5sum 持续发送 1GB 的数据进行计算,计算的速度也可以看成是 CPU 单核性能的一种指标。(考虑到管道操作是单线程,这个指令其实也只能用到一个核)

然后,如果我们多找几个 CPU 进行以下对比,我们就会发现一些神奇的现象:

Cortex-A72(ARM)

  • sysbench 单线程:1479.99 event/s
  • dd + md5sum:184 MB/s

Neoverse-N1(ARM)

  • sysbench 单线程:3497.70 event/s
  • dd + md5sum:424 MB/s

Intel(R) Xeon(R) Platinum 8171M CPU @ 2.60GHz(x86_64)

  • sysbench 单线程:820.19 event/s
  • dd + md5sum:409 MB/s

Intel(R) Xeon(R) CPU E5-26xx v4(x86_64)

  • sysbench 单线程:927.74 event/s
  • dd + md5sum:459 MB/s

Intel(R) Xeon(R) Platinum 8255C CPU @ 2.50GHz(x86_64)

  • sysbench 单线程:1054.30 event/s
  • dd + md5sum:515 MB/s

可以发现 ARM 架构的机器在 sysbench 上表现几乎都比 X86_64 的机器好很多,但是实际跑 md5sum 却相差不大甚至差不少。

可见不同架构间二者的指标并不完全正相关;不过相同架构间二者的指标还是基本正相关的。

参考资料

dd-benchmark

man-hdparm

iperf

利用 Redsocks 解决透明代理的远程抓包问题

作者 mythsman
2022年5月9日 22:10

背景

最近在做一些远程设备的抓包能力建设。具体来说是设备(基于 Docker 的 Redroid 云手机)在服务器上,抓包工具在本地( Mitmproxy , Charles, Fiddler 等类似工具),希望通过某种方法将远程设备上的流量打到本地的抓包工具上,并且流量通过本地的 IP 出到公网。

这样做的目的主要有两个:

  1. 可以做到抓包工具和待抓包设备的分离。既能利用上强大的第三方抓包工具,又无需本地部署待抓包的设备。毕竟如果在远程服务器上部署抓包工具,操作起来就不那么方便,甚至可能还需要自行开发管控界面;如果在本地部署待抓包设备,很可能会遇到例如芯片架构、操作系统、环境依赖、系统资源消耗等问题。
  2. 可以方便进行 IP 出口的调整。在调试一些不可言说的功能时,服务器上部署的设备很可能是走一些代理 IP 池,有时候这些 IP 池本身可能有点问题,导致远程设备被封。这时候如果我们能够将设备的流量导到本地的正常 IP 出公网,可能会更方便我们验证是 IP 问题还是其他的设备问题。

计划

为了打通远程设备本地抓包的这套链路,我们需要考虑如下技术点:

  1. 远程设备需要能够安装本地抓包工具的证书。
  2. 远程设备的流量需要通过某种内网穿透能力打到本地的代理工具上。
  3. 远程设备的流量需要保证不遗漏地进行转发。

由于我们的远程设备是有 root 权限的云手机,因此证书安装并不难。只要将本地的证书通过 openssl 命令转换成指定格式的证书文件,传到服务器上,在云手机启动时 bind 到 /system/etc/security/cacerts/  目录下即可。

同理,由于远程设备是云手机,通过暴露 adb 的 tcpip 端口,我们可以用本地的 adb 客户端进行连接,再通过 adb reverse 就可以构建一个云手机访问本机代理端口的信道。

而要保证云手机的流量(这里特指 HTTP/HTTPS 流量)不遗漏的进行转发,我们就不能采用配置全局正向代理的方法( adb shell settings put global http_proxy xxxx  ),因为个别 app 可以配置强制 NO_PROXY 不走系统代理。一个简单的方法是通过云手机自带的 iptables 工具进行转发,将云手机中所有目的端口为 80/443 的流量转发到 adb reverse 命令转发过来的、映射到本地抓包工具的代理端口即可。

理想的架构图如下:

问题

架构图谁都会画,但是真正实操起来才发现有一堆坑。这套流程对 HTTP 请求的确是有用的,透明代理的工具无论是使用 Charles 还是 Mitmproxy 等中间人代理工具都能正常抓到包。但是对与 HTTPS 的流量则都出现了问题:

  • Charles 会报 invalid first line in request 的错。
  • Mitmproxy 会报 Could not resolve original destination 的错。

当然,个别代理工具可能不支持解 HTTPS ,出现问题可以理解。但是各种代理工具都不能抓,那显然应当是流程上出了问题。经过实验我们发现,使用正向代理或非 HTTPS 的透明代理再加上端口转发都是能通的,唯一不能通的是 HTTPS 的透明代理模式。那么我们就需要先辨析一下这些模式的区别。

分析

正向代理

正向代理是由客户端主动发起,主动将流量打给一个代理服务器,由代理服务器代替请求的过程。下图主要展示正向代理过程中 IP 报文的变化:

  • Alice 代表发起请求的客户端
  • Flank 代表代理服务器(Forward Proxy)
  • Bob 代表客户端需要请求到的服务器

透明代理

透明代理是客户端本身无感知的,由路由转发工具强行进行流量转发(Linux 下可以用 iptables ,Windows 下可以用 netsh ,Mac 下可以用 pfctl)。下图主要展示透明代理过程中 IP 报文的变化:

  • Alice 代表发起请求的客户端
  • Ivan 代表转发流量的路由工具(Iptables之类的工具)
  • Tom 代表透明代理服务器(Transparent Proxy)
  • Bob 代表客户端需要请求到的服务器

在透明代理模式下,路由工具会非常暴力地将客户端发来的包的目的地址直接改为透明代理服务器,这会导致当数据包到了透明代理服务器中时,代理服务器是无法直接获取客户端真正想要到达的服务器地址。而正向代理服务器则不同,客户端会明确告知代理服务器他想访问谁。

那么透明代理服务器要如何在报文中获得真实的目的地址呢?这时候就需要分情况讨论了。

HTTP

我们知道 HTTP 报文是纯明文,就像一个没有封口的信封。只要打开来看就会发现,HTTP 请求报文会在 Header 中带上一个 Host 头表明当前的信期望到达的地方。透明代理服务器可以非常方便地解析到这个信息,从而知道报文需要被发送到的目的地址(Bob)。

HTTPS

HTTPS 这里就比较尴尬了。我们知道 HTTPS 在第四层有一个 TLS 加密层,如果想和 HTTP 一样从 Header 中获取 Host 头的话,则需要先进行 TLS 解密;但是,如果想进行 TLS 解密,则必须和实际的服务器进行 TLS 握手;可是你都不知道实际的服务器在哪,如何握手呢?这竟然变成了一个鸡生蛋还是蛋生鸡的问题。

那么问题最终会怎么解决呢?目光还得回到路由工具。

既然报文是你路由工具传给透明代理的,那显然路由工具这边是记录了报文原先实际需要访问的目的地址的,我直接请求你不就好了么?在 Linux 下,我们有一个用户工具 conntrack 可以展示当前网络连接的链路追踪信息:

$ sudo conntrack -L
tcp      6 60 TIME_WAIT src=192.168.32.1 dst=192.168.32.3 sport=40298 dport=8080 src=192.168.32.3 dst=192.168.32.1 sport=8080 dport=40298 [ASSURED] mark=0 use=1
tcp      6 56 TIME_WAIT src=192.168.32.1 dst=192.168.32.7 sport=54398 dport=2368 src=192.168.32.7 dst=192.168.32.1 sport=2368 dport=54398 [ASSURED] mark=0 use=1
tcp      6 60 TIME_WAIT src=192.168.32.5 dst=172.17.0.1 sport=52992 dport=5001 src=172.17.0.1 dst=192.168.32.5 sport=5001 dport=52992 [ASSURED] mark=0 use=1
tcp      6 79 TIME_WAIT src=10.0.0.4 dst=168.63.129.16 sport=39414 dport=80 src=168.63.129.16 dst=10.0.0.4 sport=80 dport=39414 [ASSURED] mark=0 use=1

而透明代理程序则可以通过 getsockopt 等方法直接向内核查询 socket 的链路信息:

static int getdestaddr(int fd, struct sockaddr_storage *destaddr)
{
    socklen_t socklen = sizeof(*destaddr);
    int error         = 0;

    error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen);
    if (error) { // Didn't find a proper way to detect IP version.
        error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
        if (error) {
            return -1;
        }
    }
    return 0;
}

这样一来,透明代理服务就能在 TLS 握手前就能拿到真实的目的 IP 了。

然而有了 IP 就够了么?并不是。路由工具中我们只能拿到目的 IP ,但是并没有域名!现代服务端的网关层基本都需要通过域名来进行转发。如何解决这个问题呢?这就引入了 SNI (Server Name Indication)头。在 TLS 握手阶段,客户端会在握手报文里额外增加 SNI 信息(这个已经是TLS标准了,但是有些客户端可能没有加),这样服务端或者透明代理服务器就能获取到实际的域名了。

最后我们再来看下 Mitmproxy 文档中提供的透明代理的流程图:

解决

了解了代理服务器的工作细节,我们再来尝试回答下开头的问题。为什么把远端的 HTTPS 流量通过路由工具+端口映射转发到本地的透明代理服务器中会报错呢?

答案已经很明显了,那就是本地的透明代理工具无法查询到远端服务器中的路由链路信息,导致无法获取真实的目的地址。(也就是为什么 mitmproxy 会报 Could not resolve original destination的原因。

解决这个问题的思路也很清晰,既然希望走透明代理的流量无法跨主机,那我们就将透明代理服务部署在本机,然后转换成正向代理出去即可

具体操作中,由于 mitmproxy 默认不支持在透明代理模式下再配置一个上游正向代理,因此我们可以选择简单魔改一下代码,在透明代理模式下的出口处请求另一个正向代理。

如果懒得改代码,我找到了一个 Redsocks 工具。这个工具可以直接作为透明代理服务器,并将流量转发给一个上游的正向代理。Ubuntu 下可以直接用 apt 安装,配置一下 /etc/redsocks.conf 就能直接使用。

如此,架构图修改成下图即可:

稳定性

方案上线后,原先预计 redsocks 可能会对系统稳定性造成影响,后来发现这一块还好,反倒是由于 mitmproxy 从透明代理模式切到正向代理/上行代理模式,导致 mitmproxy 经常 crash,参见 issue ,略坑。

参考资料

Mitmproxy Docs - Transparent HTTPS

如何使用透明代理抓 HTTPS

Istio的流量劫持和Linux下透明代理实现

利用 FFmpeg 将视频转换为 GIF 操作指北

作者 mythsman
2022年5月2日 12:05

背景

在写技术文档的时候,为了演示方便,经常需要插入一些短小的视频资源。比如一些操作的录频、一些经典视频片段、一些科普动画等。由于展示的地方通常是网页,而如果是插入视频之类的资源,通常需要占用额外的存储,而且也需要可用的视频播放器的支持,非常不便。对于命令行操作的回放,我们可能会采用 Asciinema,但是这个方案对非终端的操作无法模拟。一个常见的方案是把需要的资源转换成网页支持的 GIF、WEBP 格式等动图。考虑到各大浏览器对 GIF 格式的支持最稳定,因此大部分情况下我们都想将待展示的短小视频转换成 GIF 格式,方便直接插入文档中。

将视频转为 GIF 并对 GIF 做调整的过程中会有很多的坑,比如经常容易出现色差严重、分辨率不合适、GIF 占用的存储过大等问题。有时我们也希望在转换前做一些剪切等。处理图片的工具其实有很多,比如 ImageMagick 的 convert 工具,或者 gifsicle 工具,甚至是一些在线的 GIF 格式转换工具(极不推荐,很难用)。但是在处理类视频的信息时,这些工具其实并不很适合。毕竟原始数据本身是视频,相比先把视频转换成 GIF 再做操作,直接在视频上做完操作最后再一步转为 GIF 造成的信息丢失会更少,效果也会更好。本文主要基于 FFmpeg 总结一下转换过程中经常用到的命令,并介绍个人在处理这些问题的一些原则。

视频预处理

在典型的场景下,我们在将视频转为图片前一般会有如下步骤:

  • 视频采集
  • 视频剪裁
  • 视频分辨率调整
  • 视频帧率调整
  • 视频速率调整
  • 视频压缩

下面将基于此流程,结合样例做简要介绍。本地的 ffmpeg 版本为 4.4.1 。

视频采集

如果我们能获取到视频文件,则最好。不过很多情况下还是需要我们使用一些录屏工具进行采集,不过作为系统洁癖患者,个人不建议下载一些乱七八糟来源的录频工具或者很贵但是普通人根本用不到多少功能的视频处理软件。

  • 在 Windows 下,可以通过 Win+G 快捷键呼出 xbox 自带的录频工具。
  • 在 Mac 下,可以通过 Command+Shift+5 快捷键呼出。

以上工具足够用了。

视频裁剪

无论是自有视频,还是录屏来的视频,我们经常都希望剪裁掉头尾的一些无用片段。我们当然可以使用常见的编辑工具,例如 Mac 上的 imovie 等。不过显然我们不想杀鸡用牛刀,一两条命令就可以解决。

截取从 00:00:10 开始的 10 秒的时间段的视频:

$ ffmpeg -i sample.mov -ss 00:00:10 -t 00:00:10 output1.mov

(其中 -ss 表示开始时间,-t 表示截取的时长)

截取从 00:00:10 到 00:00:20 时间段的视频:

$ ffmpeg -i sample.mov -ss 00:00:10 -to 00:00:20 output2.mov

(其中 -ss 表示开始时间,-to 表示结束时间)

确认二者相同并确认视频时长:

$ diff output1.mov output2.mov
$ ffprobe -v error -show_entries format=duration sample.mov
[FORMAT]
duration=10.000000
[/FORMAT]

(其中 -v error 是为了屏蔽一开始打印的版本信息)

视频分辨率调整

由于我们需要展示的视频本身是嵌入文档或网页里的,因此本身对图片分辨率的要求并不大。这时候适当的缩小分辨率无论是对文件占用的大小、还是对展示的便捷都是有好处的。采用的工具是 ffmpeg 的 scale filter graph。

确认原视频的分辨率:

$ ffprobe -v error -show_entries stream=width,height sample.mov
[STREAM]
width=1560
height=1148
[/STREAM]

强制比例缩放,设置长度,宽度:

$ ffmpeg -i sample.mov -vf scale=720:530 output1.mov

(其中 720 为宽,530为高,且,宽高均不可为奇数)

固定比例缩放,设置宽度,高度自适应:

$ ffmpeg -i sample.mov -vf scale=720:-1 output2.mov

固定比例缩放,设置高度,宽度自适应:

$ ffmpeg -i sample.mov -vf scale=-1:530 output3.mov

(将需要自适应的部分设置为-1即可,如果自适应部分再按倍缩放,则可以设置为 -2 ,-3 等)

选择自定义scale算法,可选算法可见ScalerOptions

$ ffmpeg -i sample.mov -vf scale=-1:530:flags=lanczos  output4.mov

(缩放算法有很多种,如果效果不好可以换几个试试。默认是 bicubic 算法)

视频帧率调整

视频的帧率一般会比较高,而我们对 GIF 的要求一般没那么高。为了减少图片的体积,我们可以手动调节下帧率,以达到图片大小和用户体验的最佳平衡点。通常视频的帧率一般是 60 fps 。对普通图片来说,20 的fps早已够用,节约点的话,10 fps ,5 fps 也凑合能看,具体就自己体验喽。

确认原视频帧率:

$ ffprobe -v error -show_entries stream=r_frame_rate sample.mov
[STREAM]
r_frame_rate=60/1
[/STREAM]

(原视频的帧率就是 60 fps)

调整帧率为20:

$ ffmpeg -i sample.mov -r 20 output1.mov

视频速率调整

对于录屏而言,可能我们的动作比较慢,希望在展示的时候稍微加快点速度以提高展示效率并减少视频体积。或者视频本身很快,我们希望做一下慢放。这些时候我们最好提前就对视频速度做一些调整。

调整视频速度变快为5倍(时长*0.2):

$ ffmpeg -i sample.mov -filter:v "setpts=0.2*PTS" output1.mov

调整视频速度变慢2倍(时长x2):

$ ffmpeg -i sample.mov -filter:v "setpts=2*PTS" output2.mov

(原理通过调整视频帧中的 PTS 展示时间戳来调整速度)

需要注意的是,调整速率后,帧率仍然保持不变。因此将视频加速再减速成原视频的速度后,与原视频相比会丢失信息。

视频压缩

其实H264视频本身的压缩率已经很高了,如果想要进一步压缩,基本只能通过一些有损的形式。我们可以通过调整 x264, x265, 以及 libvpx 中的 Constant Rate Factor 参数来进行一些有损压缩处理。该参数取值在 0 到 51 之间,值越大则有损的比例越大,压缩率越好。通常我们会取 23这个值,稍微激进一点可以调整为30+。

调整 crf 取值到30 :

$ ffmpeg -i sample.mov -crf 30 output1.mov

比较二者大小:

$ ls -lah sample.mov output1.mov
-rw-r--r--  1 myths  staff   1.5M May  1 17:31 sample.mov
-rw-r--r--@ 1 myths  staff   1.0M May  1 17:51 output1.mov

可见视频的确压缩了一些,不过会发现 GIF 的质量会有一些下降。是否需要调整这个参数,需要根据实际情况进行取舍。

GIF 格式转换

ffmpeg默认支持根据输出文件的后缀名自动进行格式转换,非常方便。但是如果你以为能无脑用,那就大错特错了。

帧率问题

一个典型的错误转换方法是:

$ ffmpeg -i sample.mov output.gif

有什么问题呢?我们检查一下视频长度就知道了:

$ ffprobe -v error -show_entries format=duration sample.mov
[FORMAT]
duration=62.000000
[/FORMAT]

$ ffprobe -v error -show_entries format=duration output.gif
[FORMAT]
duration=173.680000
[/FORMAT]

我们发现转换后的 GIF 的视频长度竟然和原视频不一样。打开检测后发现的确 GIF 相比原视频要慢了许多。

再检查一下帧率就发现问题了:

$ ffprobe -v error -show_entries stream=r_frame_rate,avg_frame_rate sample.mov
[STREAM]
r_frame_rate=60/1
avg_frame_rate=60/1
[/STREAM]

$ ffprobe -v error -show_entries stream=r_frame_rate,avg_frame_rate output.gif
[STREAM]
r_frame_rate=50/1
avg_frame_rate=257/12
[/STREAM]

GIF 的实际帧率竟然变小了,难怪视频变长了。计算下比例也能对的上:

173.680000/62.000000≈(60/1)/(257/12)

既然如此,我们强制设置下帧率就好了:

$ ffmpeg -i sample.mov -r 20 output2.gif

$ ffprobe -v error -show_entries stream=r_frame_rate,avg_frame_rate,duration output2.gif
[STREAM]
r_frame_rate=20/1
avg_frame_rate=20/1
duration=62.050000
[/STREAM]

这样时长就和原视频对上了,帧率也是我们设置的帧率。具体原因未知,不过结论就是在对视频转 GIF 时,一定要重新指定一下帧率

调色板优化

你可能知道,和视频不同,PNG的调色盘只有256种颜色。默认情况下,这256种颜色会尽量平均分布在整个颜色空间中。这就导致对于一些色彩区分度比较小的图片,会出现颜色模糊的情况。

首先我们生成一张下未优化下的图片:

$ ffmpeg -i sample.mov -r 20 output-raw.gif

结果如下:

这时候需要用对图片进行一下全局调色板优化

$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" output-palette.gif

结果如下:

放大后仔细观察一下二者,你会很明显的发现原图的背景颜色变成了很多小颗粒,而使用调色版优化后,背景才真正显示了纯色

当然,如果对图片质量要求高,也可以对每一帧单独记录调色板(代价就是图片会变大很多):

$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen=stats_mode=single [p];[s1][p]paletteuse=new=1" output-palette-single.gif

如果图片的运动程度比较大,也可以修改一些防抖参数( dither = none / bayer / heckbert / floyd_steinberg / sierra2 / sierra2_4a),如果不指定,默认是 sierra2_4a 。例如:

$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=none" output-palette-none.gif

$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=bayer" output-palette-bayer.gif

$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=heckbert" output-palette-heckbert.gif

最后看一下添加了调色板优化后的图片大小:

$ ls -lah
-rw-r--r--   1 myths  staff   2.2M May  2 11:36 output-palette-bayer.gif
-rw-r--r--   1 myths  staff   2.5M May  2 11:36 output-palette-heckbert.gif
-rw-r--r--   1 myths  staff   1.9M May  2 11:40 output-palette-none.gif
-rw-r--r--   1 myths  staff    32M May  2 11:29 output-palette-single.gif
-rw-r--r--@  1 myths  staff   2.7M May  2 11:15 output-palette.gif
-rw-r--r--   1 myths  staff   1.0M May  2 11:30 output-raw.gif
-rw-r--r--@  1 myths  staff   198K May  1 22:15 sample.mov

显然,每帧记录调色板( output-palette-single.gif ) 最大;不进行调色板优化图片最小,但是质量最差( output-raw.gif );而不使用防抖策略( output-palette-none.gif ) 会使图片更小一点,也不太影响图片观感。

GIF 循环次数设置

通过 ffmpeg 还可以设置图片的循环次数。图片在播放完成后,默认会重头开始播放,如果想修改这个特性,可以通过 -loop 参数指定循环方式,也可以通过 -final_delay 参数配置间隔时间:

设置 GIF 播放完后不重头开始:

$ ffmpeg -i sample.mov -r 20 -loop -1 output.gif

gif muxer 支持的 -loop 和 -final_delay 参数的说明可以通过命令查看:

$ ffmpeg -v error -h muxer=gif
Muxer gif [CompuServe Graphics Interchange Format (GIF)]:
    Common extensions: gif.
    Mime type: image/gif.
    Default video codec: gif.
GIF muxer AVOptions:
  -loop              <int>        E.......... Number of times to loop the output: -1 - no loop, 0 - infinite loop (from -1 to 65535) (default 0)
  -final_delay       <int>        E.......... Force delay (in centiseconds) after the last frame (from -1 to 65535) (default -1)

有点令人费解的是,除了无限重复(0)和不重复(-1)的值,如果你想重复 N 次,那么这个 -loop 参数就要设置为 N+1 。。。

图片大小分析

最后记录一下我的一个测试资源在顺序进行以上各种处理后的大小变化情况:

  1. 录频后的原视频:9.3M (mov格式)
  2. 不加任何参数转码后视频:1.5M(mov格式)
  3. 按需裁减后:1.2M (mov格式)
  4. 分辨率由 1560x1148 调整为 720x539 后:541K(mov格式)
  5. 帧率从 60 调整为 20 后:339K (mov格式)
  6. 速率 x2 后:235K (mov格式)
  7. 视频压缩 CRF 取值 30 后:198K (mov格式)
  8. 转换为 GIF ,使用全局调色板并取消防抖设置后:1.9M (gif格式)

GIF 格式的压缩效果和普通视频格式相比还是差很多的,不过在尽量保证图片质量的前提下,把图片大小控制在 2M 以内一般也还能够接受。

参考资料

压缩 gif 图片的方法(命令行)

Speeding up/slowing down video

CRF Guide

How do I convert a video to GIF using ffmpeg, with reasonable quality?

High quality GIF with FFmpeg

ffmpegでとにかく綺麗なGIFを作りたい

纯Docker部署Https服务——以Nextcloud为例

作者 mythsman
2022年3月9日 15:00

背景

近期阿里云的廉价学生机小水管到期了,打算把一些服务打包迁移到微软员工不要钱的azure云上。

原先各种服务的部署方式都是直接部署的,换主机需要重新搭建各种环境非常麻烦,也容易遗漏。于是就想跟随潮流把这些服务都上docker,能够做到部署一把梭。

难点

一般的开源组件都会有官方docker镜像,部署起来其实都很方便。大部分对着官方文档改改配置再用docker-compose组合一下参数就能跑起来。比较麻烦的点就是网站要部署成 https 的话需要 SSL 证书。对于个人使用的小网站来说,云厂商的 SSL 证书又实在是比较贵(阿里云上通配符域名大概2000¥/年,单域名也要400¥/年)。

比较经济实惠的做法是使用 Let’s Encrypt 的免费证书,不过代价就是他需要定时check你对这个域名的所有权。显然,我们肯定不能手动更新,那样简直要疯。一般我们会用 certbot 来定时进行站点所有权的确认。经常对接站长后台工具的人应该都知道,认证的方式一般有两种:一种是将给定的验证字符串写进 DNS 的 TXT 记录,从而确认你对这个域名的所有权;第二种是将给定的验证字符串写在网站的给定目录下,从而确认你对这个网站的所有权。

理论上最方便的做法是通过一个定时任务,定时调用 DNS 服务商的接口来改 TXT 记录。这样可以做到将 SSL 证书的校验和网站本身的部署分离开,使得校验所有权的逻辑不干扰正常的网站配置。不过尴尬的是 certbot 提供的 DNS插件 基本都不包含国内的运营商。而考虑到域名在国内解析的性能,我还是不太想把域名切到国外的服务商去。

因此事实上我只能采用 webroot 的方式来认证。这个方式比较万金油,不过缺点就是不支持通配符域名,如果需要同时验证多个子域名,则需要手动添加。同时,传统的 certbot 使用方式中一定还需要在系统中配置一个 crontab 任务来做更新,而我现在则希望将这个 crontab 任务也集成在 docker 中,尽量不要对除 docker 外的逻辑做任何感知。

步骤

逻辑上讲,步骤大致应当如下:

  1. 去 DNS 服务商提供的配置后台,将给定域名配置好A记录指向目的主机。
  2. 安装 docker  docker-compose ,并设置好配置文件。
  3. 配置并启动 nginx 只开放 http 端口 ,准备 certbot 的认证环境。
  4. 启动 certbot 初始化,配合nginx,生成首个私钥和证书链。
  5. 利用 certbot 提供的私钥和证书,配置 nginx 的 https 端口。
  6. 配置 certbot 的自动 renew ,进行自动验证。

样例

下面以在 https://pan.mythsman.com 下配置 nextcloud 为例。 DNS 配置、docker 和 docker-compose 安装等步骤略过。

初始化docker配置

./docker-compose.yml

version: "3.8"
services:
    nginx:
        container_name: nginx
        image: nginx:latest
        restart: always
        volumes:
            - ./nginx/logs:/var/log/nginx
            - ./nginx/conf.d:/etc/nginx/conf.d
            - ./certbot/conf:/etc/nginx/ssl
            - ./certbot/data:/var/www/certbot
        ports:
            - "80:80"
            - "443:443"
        command: ["/bin/sh", "-c", "while :;do sleep 24h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\""]

    certbot:
        container_name: certbot
        image: certbot/certbot:latest
        command: certonly --webroot --webroot-path=/var/www/certbot --agree-tos --email mythsman@foxmail.com -d  pan.mythsman.com
        # entrypoint: ["/bin/sh", "-c", "trap exit TERM;while :; do certbot renew --webroot -w /var/www/certbot; sleep 24h & wait $${!}; done;"]
        volumes:
            - ./certbot/conf:/etc/letsencrypt
            - ./certbot/logs:/var/log/letsencrypt
            - ./certbot/data:/var/www/certbot

(需要注意,certbot 的 latest 镜像不支持 arm,如果当前机器是 arm 架构则需要去 dockerhub 中自行选购🥲)

说明:

  1. ./certbot/conf 目录存放的即是 certbot 生成的证书等一大堆文件,因此需要挂载给 nginx 。
  2. ./certbot/logs 目录下存放的是证书创建相关的日志。
  3. ./certbot/data 目录是 certbot 用来存放校验字符串的,需要通过nginx对外网暴露,因此也需要挂在给 nginx。
  4. ./nginx/logs 目录存放的即是 nginx 的相关日志。
  5. ./nginx/conf.d 目录存放 nginx 的配置,下面会介绍。
  6. nginx 的 command 指令写了一个小脚本,用来让 nginx 定时自动 reload,方便 certbot 刷新证书后 nginx 能及时更新。
  7. certbot 的 command 指令中需要配置接受邮箱,用来接受一些通知消息,以及需要配置的域名 pan.mythsman.com
  8. certbot 的 entrypoint 指令是用来后续 renew 的,暂时注释掉。

初始化nginx配置

./nginx/conf.d/default.conf

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

     server_name _;

     location ~ /.well-known/acme-challenge {
         allow all;
         root /var/www/certbot;
     }

     location / {
         if ( $host = "pan.mythsman.com" ){
             return 301 https://pan.mythsman.com$request_uri;
         }
         return 444;
     }

}

server {
       listen 443 default_server;
       listen [::]:443 default_server;
       server_name _;
       ssl_reject_handshake on;
       ssl_session_tickets off;
}

说明:

  1. 这里最关键的是 .well-known 行,用来关联 certbot 的校验文件。
  2. return 444 是用来拒绝掉一些未知域名的http访问。
  3. ssl_reject_handshake 是用来拒绝掉一些未知域名的https的访问。

生成证书

执行 docker-compose up -d ,证书生成成功后会在 ./certbot/logs/letsencrypt.log 打印相关日志:

2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/archive.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/live.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Writing README to /etc/letsencrypt/live/README.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/archive/pan.mythsman.com.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/live/pan.mythsman.com.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing certificate to /etc/letsencrypt/live/pan.mythsman.com/cert.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing private key to /etc/letsencrypt/live/pan.mythsman.com/privkey.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing chain to /etc/letsencrypt/live/pan.mythsman.com/chain.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing full chain to /etc/letsencrypt/live/pan.mythsman.com/fullchain.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing README to /etc/letsencrypt/live/pan.mythsman.com/README.
2022-03-09 03:41:39,909:DEBUG:certbot._internal.plugins.selection:Requested authenticator webroot and installer <certbot._internal.cli.cli_utils._Default object at 0x7f94027a1370>
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var authenticator=webroot (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var webroot_map={'webroot_path'} (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.storage:Writing new config /etc/letsencrypt/renewal/pan.mythsman.com.conf.
2022-03-09 03:41:39,911:DEBUG:certbot._internal.display.obj:Notifying user:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/pan.mythsman.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/pan.mythsman.com/privkey.pem
This certificate expires on 2022-06-07.
These files will be updated when the certificate renews.
2022-03-09 03:41:39,911:DEBUG:certbot._internal.display.obj:Notifying user: NEXT STEPS:
2022-03-09 03:41:39,912:DEBUG:certbot._internal.display.obj:Notifying user: - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
2022-03-09 03:41:39,913:DEBUG:certbot._internal.display.obj:Notifying user: If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le

也可以通过 nginx 的访问日志 /nginx/logs/access.log  查看他在校验的过程中访问了哪些页面:

3.120.130.29 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"
3.19.56.43 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"
34.221.255.206 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"
64.78.149.164 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"

配置站点容器和证书更新

./docker-compose.yml

version: "3.8"
services:
    nginx:
        container_name: nginx
        image: nginx:latest
        restart: always
        volumes:
            - ./nginx/logs:/var/log/nginx
            - ./nginx/conf.d:/etc/nginx/conf.d
            - ./certbot/conf:/etc/nginx/ssl
            - ./certbot/data:/var/www/certbot
        ports:
            - "80:80"
            - "443:443"
        command: ["/bin/sh", "-c", "while :;do sleep 24h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\""]

    certbot:
        container_name: certbot
        image: certbot/certbot:latest
        # command: certonly --webroot --webroot-path=/var/www/certbot --agree-tos --email mythsman@foxmail.com -d  pan.mythsman.com
        entrypoint: ["/bin/sh", "-c", "trap exit TERM;while :; do certbot renew --webroot -w /var/www/certbot; sleep 24h & wait $${!}; done;"]
        volumes:
            - ./certbot/conf:/etc/letsencrypt
            - ./certbot/logs:/var/log/letsencrypt
            - ./certbot/data:/var/www/certbot
    nextcloud:
        container_name: nextcloud
        image: nextcloud:latest
        volumes:
            - ./nextcloud:/var/www/html
        ports:
            - "8080:80"

说明:

  1. 这里将 cerbot 的 command 注释掉,更换了 entrypoint 的启动脚本,用来每天自动更新 cerbot 证书。
  2. 新增了 nextcloud 的镜像。

配置站点的Nginx

./nginx/conf.d/pan.mythsman.com-ssl.conf

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name pan.mythsman.com;

    ssl_certificate /etc/nginx/ssl/live/pan.mythsman.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/pan.mythsman.com/privkey.pem;

    ssl_stapling on;
    ssl_stapling_verify on;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://172.17.0.1:8080;
    }

    client_max_body_size 10G;

}

说明:

  1. proxy_pass 中的 172.17.0.1 为 host 主机在 docker 中访问的 ip,8080端口暴露的即为前文设置的 nextcloud 的服务。
  2. ssl_certificate 和 ssl_certificate_key 配置为前文 certbot 生成的文件。
  3. 如果申请证书的时候也填了一级域名,那么这里的证书文件中间的域名就是一级域名。
  4. client_max_body_size 10G; 配置是增大上传文件的大小限制。

重新部署

执行 docker-compose down 和  docker-compose up -d ,重新部署各个服务。部署完成后,可以查看 certbot 的日志:

2022-03-09 06:27:54,847:DEBUG:certbot._internal.main:certbot version: 1.24.0
2022-03-09 06:27:54,848:DEBUG:certbot._internal.main:Location of certbot entry point: /usr/local/bin/certbot
2022-03-09 06:27:54,848:DEBUG:certbot._internal.main:Arguments: ['--webroot', '-w', '/var/www/certbot']
2022-03-09 06:27:54,848:DEBUG:certbot._internal.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#manual,PluginEntryPoint#null,PluginEntryPoint#standalone,PluginEntryPoint#webroot)
2022-03-09 06:27:54,863:DEBUG:certbot._internal.log:Root logging level set at 30
2022-03-09 06:27:54,865:DEBUG:certbot._internal.display.obj:Notifying user: Processing /etc/letsencrypt/renewal/pan.mythsman.com.conf
2022-03-09 06:27:54,883:DEBUG:certbot._internal.plugins.selection:Requested authenticator webroot and installer <certbot._internal.cli.cli_utils._Default object at 0x7f64a6694f10>
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var authenticator=webroot (set by user).
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var webroot_map={'webroot_path'} (set by user).
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 06:27:54,907:DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): r3.o.lencr.org:80
2022-03-09 06:27:55,136:DEBUG:urllib3.connectionpool:http://r3.o.lencr.org:80 "POST / HTTP/1.1" 200 503
2022-03-09 06:27:55,137:DEBUG:certbot.ocsp:OCSP response for certificate /etc/letsencrypt/archive/pan.mythsman.com/cert1.pem is signed by the certificate's issuer.
2022-03-09 06:27:55,138:DEBUG:certbot.ocsp:OCSP certificate status for /etc/letsencrypt/archive/pan.mythsman.com/cert1.pem is: OCSPCertStatus.GOOD
2022-03-09 06:27:55,141:DEBUG:certbot._internal.display.obj:Notifying user: Certificate not yet due for renewal
2022-03-09 06:27:55,142:DEBUG:certbot._internal.plugins.selection:Requested authenticator webroot and installer None
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user: The following certificates are not due for renewal yet:
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user:   /etc/letsencrypt/live/pan.mythsman.com/fullchain.pem expires on 2022-06-07 (skipped)
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user: No renewals were attempted.
2022-03-09 06:27:55,143:DEBUG:certbot._internal.display.obj:Notifying user: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2022-03-09 06:27:55,143:DEBUG:certbot._internal.renewal:no renewal failures

没有报错说明流程就是通的,不过由于这个证书是刚申请的,离过期还很远,因此在renew的时候显示不需要更新。如果是想强制更新的话,可以在 certbot 的参数中加 --renew-by-default 选项。如果想确认证书的过期时间,可以通过 openssl 命令查看:

$ openssl x509 -in cert.pem -noout -dates
notBefore=Jan 30 03:00:32 2022 GMT
notAfter=Apr 30 03:00:31 2022 GMT

这时候访问 pan.mythsman.com 即可发现 https 证书已经OK了。

新增子域名

有时候,我们可能想在 nginx 里新增个子域名,这时候就需要把整个 certbot/ 文件夹删除,然后全部重新来一遍

这里有一个坑,就是如果直接删除 certbot/ 文件夹,重新跑初始化一定会有报错。这是因为 nginx/conf.d/ 下的 *-ssl.conf 文件引用了原先在 certbot/ 下生成的证书文件。

简单的做法是在重新跑 certbot 的初始化指令前先把 nginx/conf.d/*-ssl.conf 文件挪走,等初始化完后再挪回来即可。

Nextcloud的其他配置

配置好 https 后,nextcloud有时还不认得自己的scheme已经是https了,这里最好需要修改一下配置。

./nextcloud/config/config.php 新增一行:

'overwriteprotocol' => 'https',

参考资料

Nginx & Certbot (Letsencrypt) via Docker

Certbot User Guide

Can’t use webroot authenticator needed for wildcard domain

树莓派4B使用arm64系统踩坑

作者 mythsman
2021年12月27日 21:39

背景

由于工作原因,需要一台 arm64 的服务器测试一些功能。但是目前这个点没法快速采购到腾讯云或者百度云的arm服务器(这俩公司的arm服务器好像都只是在内测阶段,据说得2022年初才能 Release)。想了一圈发现树莓派似乎正好有 arm64 的cpu,于是去官网确认了下 Specification

Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz

看起来挺好的,于是在亚博智能店里买了个 4B ,但是买到后跑起来发现竟然不是 arm64 的,大呼上当。不过仔细一研究发现原来是原装的 Raspberry os 镜像使用的是 armhf 内核(即支持hard float 的32位 arm )。而树莓派官方其实在 2020 年已经发布了支持 arm64 内核的镜像了,参见这个 Reddit 讨论。同时也发现 arm64 的系统相比 armhf 其实也更能发挥64位cpu的能力。

既然如此,那就搞起来吧。

方案

网上冲浪了一把,发现基本有俩套解决方案:

  1. 修改 /boot/config.txt 的 arm_64bit 配置(参考官方配置文档)。
  2. 重新刷入 arm64 位的镜像。

方案1看起来更方便,方案2看起来更稳妥,于是我挨个试了下。为了验证系统架构的正确性,我也准备了几个测试方法:

  1. uname -a
  2. cat /proc/cpuinfo
  3. getconf LONG_BIT
  4. file /bin/bash
  5. cat /proc/version
  6. dpkg --print-architecture

验证

原始环境配置

首先看一下原版基于 raspios_armhf-2020-05-28 的镜像下的配置情况。

pi@raspberrypi:~ $ uname -a
Linux raspberrypi 5.10.63-v7l+ #1496 SMP Wed Dec 1 15:58:56 GMT 2021 armv7l GNU/Linux
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor : 0
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 1
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 2
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 3
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
Hardware : BCM2711
Revision : d03114
Serial  : 10000000b6d307b2
Model  : Raspberry Pi 4 Model B Rev 1.4
pi@raspberrypi:~ $ getconf LONG_BIT
32
pi@raspberrypi:~ $ file /bin/bash
/bin/bash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=3e5e2847bbc51da2ab313bc53d4bdcff0faf2462, stripped
pi@raspberrypi:~ $ cat /proc/version
Linux version 5.10.63-v7l+ (dom@buildbot) (arm-linux-gnueabihf-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1496 SMP Wed Dec 1 15:58:56 GMT 2021
pi@raspberrypi:~ $ dpkg --print-architecture
armhf

基本符合预期,唯一让我一开始比较意外的就是 /proc/cpuinfo 里的 Model Name 是 ARMv7 Processor rev 3 (v7l),和官方声称的 ARM v8 并不一致。后来才明白原来这个值也是跟系统内核有关的,有时并不能真正准确的表达真是硬件的属性。这个镜像下的系统的确是32位arm无疑了。

修改环境配置

参照官网配置文档,将 /boot/config.txt 中的 arm64_bit 置位1,重启后重新进行下检测。

pi@raspberrypi:~ $ uname -a
Linux raspberrypi 5.10.63-v8+ #1496 SMP PREEMPT Wed Dec 1 15:59:46 GMT 2021 aarch64 GNU/Linux
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor : 0
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 1
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 2
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 3
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
Hardware : BCM2835
Revision : d03114
Serial  : 10000000b6d307b2
Model  : Raspberry Pi 4 Model B Rev 1.4
pi@raspberrypi:~ $ getconf LONG_BIT
32
pi@raspberrypi:~ $ file /bin/bash
/bin/bash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=3e5e2847bbc51da2ab313bc53d4bdcff0faf2462, stripped
pi@raspberrypi:~ $ cat /proc/version
Linux version 5.10.63-v8+ (dom@buildbot) (aarch64-linux-gnu-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1496 SMP PREEMPT Wed Dec 1 15:59:46 GMT 2021
pi@raspberrypi:~ $ dpkg --print-architecture
armhf

结果有点出乎意料。

通过内核版本查看的cpu架构基本都能正确展示出 aarch64 的关键字,但是其实系统中除了内核之外的其他软件仍然是 32 位的版本(毕竟只是更新了内核)。而且 dpkg 默认使用的软件架构仍然是 32 位的。这意味着当前系统其实有点四不像,既有64位的东西,又有32位的东西,不用想就知道以后100%会踩坑。

同时注意到,刷了新内核之后,cpuinfo 展示硬件信息为 Hardware : BCM2835 ,而原来的硬件信息却是Hardware : BCM2711 。在 gadgetversus 里对比了一下两种芯片:

虽然 2835>2711,但其实是变成了个老版本,真的是闹不明白。。。后来在官方论坛上瞅了瞅,才发现树莓派的软件总监说这好像是因为实际上存在多个芯片类型,但是为了一些上下游兼容的逻辑,就展示了一个错的。。。

顺带吃一个瓜:

mmuaa同学在博客里分享了他的方法。pingwei同学照着弄死机了,也写了个博客,然后还去mmuaa同学的博客下面喷粪,笑死了。

重新刷入镜像

四不像的系统肯定是不要用的,于是就去官网下了Imager,又去清华镜像站里下了 最新的基于 buster (ubuntu 18.04 , debian 10) 的arm64位镜像,烧进TF卡里。装机成功后验证了下数据。

pi@raspberrypi:~ $ uname -a
Linux raspberrypi 5.10.17-v8+ #1414 SMP PREEMPT Fri Apr 30 13:23:25 BST 2021 aarch64 GNU/Linux
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor : 0
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 1
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 2
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 3
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
Hardware : BCM2835
Revision : d03114
Serial  : 10000000b6d307b2
Model  : Raspberry Pi 4 Model B Rev 1.4
pi@raspberrypi:~ $ getconf LONG_BIT
64
pi@raspberrypi:~ $  file /bin/bash
/bin/bash: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=b11533bde88bb45ef2891fbf3ad86c1869ed3a41, stripped
pi@raspberrypi:~ $ cat /proc/version
Linux version 5.10.17-v8+ (dom@buildbot) (aarch64-linux-gnu-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1414 SMP PREEMPT Fri Apr 30 13:23:25 BST 2021
pi@raspberrypi:~ $ dpkg --print-architecture
arm64

这下终于统一都是 arm64 的了,虽然 cpuinfo 还是显示的是老的芯片信息(BCM2835),不过毕竟没啥太大影响。

剩下需要注意的就是,在配置新的apt源的时候,/etc/apt/sources.list 直接配置 debian 10 的镜像即可,不要用 raspberrypi 的镜像了;/etc/apt/sources.list.d/raspi.list 里再配置下 raspberrypi 的特定源即可。

❌
❌