Linux

collectd + Graphite + Grafana 搭建网络质量监控系统

前段时间入手一台 Gen8 服务器,主要用来做网络存储。光做网络存储显然太浪费了,感谢 ESXi,一机多用很方便。本文介绍如何在家庭服务器上搭建简单好用的网络质量监控系统。

一、选材

说到网络质量监控,大部分人会想到著名的 SmokePing。SmokePing 的确是经典工具,但未免老旧,配置也略复杂。本文使用 collectd 作为收集工具,Graphite 作为存储工具,Grafana 作为展示工具。这些工具符合「专做一件事情并把这件事情做好」的 Unix 哲学,配置灵活、功能强大。

整体结构是这样的:

asciiflow-collectd-graphite-grafana-2

二、收集:collectd

正如其名字所暗示的那样,collectd 是一个收集系统各项指标的进程。它自带很多插件,也可以通过自定义插件和数据类型的方式增加更多的收集项。网络质量监控主要用到其中的 ping 插件,该插件依赖 Liboping 这个库。这两个项目在主流 GNU/Linux 发行版中都有打包。

使用你最喜爱的包管理器安装 collectd 和 liboping 之后,使用你最喜爱的编辑器打开 /etc/collectd.conf 文件。这是一个带有详尽注释的超长配置文件,要让 ping 插件工作,以下是一份示例配置:

Hostname "your-hostname"
FQDNLookup false

LoadPlugin ping
LoadPlugin write_graphite

<Plugin ping>
  Host "8.8.8.8"
  Host "8.8.4.4"
  Interval 1.0
  Timeout 0.9
</Plugin>

<Plugin write_graphite>
  <Node "localhost">
    Host "localhost"
    Port "2003"
    Protocol "tcp"
    LogSendErrors true
  </Node>
</Plugin>

意义很明确,声明自己的主机名(用于上报数据),加载 ping 和 write_graphite 插件,然后配置这两个插件。

ping 插件的配置项中,Host 用于添加需要被监控的目标,一行一个,IntervalTimeout 则分别指定多久 ping 一次,以及多久没收到回包认为是超时。如有特殊需求,还可以指定 SourceAddress, Device 等参数指定从哪个网络设备发出 ICMP 包。注意,这里 Interval 不是指多久上报一次,而是指多久 ping 一次,上报的话还是按全局的来(默认 10 秒),上报时的数据是这段时间 RTT 的算术平均数。

write_graphite 插件的配置项中,填写 carbon-cache.py 监听的地址和端口。本例中跑 collectd 的机器同时也跑了 carbon-cache.py,因此填 localhost 即可。如果它们不在同一台机器上(比如 collectd 跑在路由器上,carbon-cache.py 跑在配置更高的设备上),则需要填写相应的地址。

collectd 发出去的数据是很简单的 TCP 消息,如:

foo.bar.baz 123 1458372405

以空格分隔,第一段是指标名字,第二段是数值,第三段是时间戳。

改好配置之后保存。因为现在 carbon-cache.py 还没有运行,因此还不能启动 collectd。

三、接收、存储和查询:carbon-cache.py 和 graphite-api

Graphite 是一个较大项目,它的主要组件有:

  • Carbon。包括 carbon-cache.py, carbon-relay.py 等,用于接收数据点;
  • Whisper。数据点存储格式,Carbon 用它把数据点写入磁盘;
  • Graphite Web。基于 Django 的网页应用,既提供查询数据点的 API,也提供一个展示用的网页。

由于 Graphite Web 较为臃肿而功能比较弱,因此这里不使用它,而是使用 Graphite-API 这个第三方项目提供查询 API,用漂亮的 Grafana 提供展示页面。

Carbon 可以直接从 PyPI 安装,注意它只支持 Legacy Python。它是纯 Python 的,没有其他依赖,使用 pip2 install carbon 即可轻松安装。

Carbon 使用 Whisper 存储数据点,这一格式的文件大小是预分配的,并且是固定的。旧的数据可以设置自动降低精度,非常旧的数据可以设置丢弃。在 storage-schemas.conf 中可以这么设置:

[ping]
pattern = \.ping\.
retentions = 10s:1d,1m:30d,5m:180d,30m:1y

[default]
pattern = .*
retentions = 1m:1d,5m:30d,30m:180d,1h:1y

其中 pattern 匹配指标的名字,retentions 指定这个指标应该保留多久。以 [default] 为例,这个指标的数据,1 分钟精度的会保留 1 天,之后自动降为 5 分钟精度,保留 30 天,之后自动降为 30 分钟精度,保留 180 天,之后自动降为 1 小时精度,保留 1 年。而对于 [ping] 一节的数据,我希望能更精确一些,因此 1 天内的数据是 10 秒精度的。这样的设置可以使不同的指标按需自动降低精度以节省存储空间,既能查到近期的高精度数据,也能反观远期的大致趋势。

注意这些政策仅对新创建的 .wsp 文件有效,已有的文件的存储策略需要通过 whisper-resize.py 进行更改。
Graphite-API 因为带有非 Python 的图形库依赖,编译安装时较为麻烦。Ubuntu / Debian 用户可以用官方提供的 .deb 安装。Arch Linux 用户可以使用我打包的 aur/graphite-api 安装。

安装之后打开 graphite-api.yaml 文件。根据安装方式的不同,Carbon 和 Graphite-API 的存储路径、配置文件路径会有一些差别,请按照自己的情况将 whisper – directories 一节的路径填写正确。

另外推荐配置 carbonlink,查询那些在 carbon-cache.py 内存中还未写入磁盘的数据。最终我使用的 graphite-api.yaml 内容如下:

finders:
  - graphite_api.finders.whisper.WhisperFinder
whisper:
  directories:
    - /var/lib/carbon/whisper
carbon:
  hosts:
    - 127.0.0.1:7002
  timeout: 1
  retry_delay: 15
  carbon_prefix: carbon
  replication_factor: 1

配置完成后,可以启动 carbon-cache.py 和 graphite-api。上节配置的 collectd 也可以启动了。

四、展示:grafana-server

Grafana 是一个功能强大的图表绘制服务器,支持 Graphite, InfluxDB, OpenTSDB 等多种后端,支持多种图表绘制,几乎无外部依赖。

Grafana 由 Go 和 Node.js 写成,编译结果是一个单文件 Go 程序和一堆 HTML + CSS + JavaScript。官方提供了 .deb.rpm 可供安装。我在 Arch Linux 上安装的时候,本来直接用的 aur/grafana,后来发现 go get 和 npm install 的过程简直蛋疼,于是将官方提供的 .tar.gz 二进制包做了一份 aur/grafana-bin,直接装这个就省力多了。

安装之后启动服务,在浏览器中打开 :3000 端口,即可用 admin / admin 登录。在 grafana.ini 中也可以开启匿名登录。

登录后需要先添加数据源(data source),将 graphite-api 的地址和端口(默认是 8888)填写进去即可。

然后便可以开始画图了,通过简单的网页操作即可添加各种不同类型的图表,也可以方便地选用 Graphite 内置的各种函数。Grafana 的具体操作可以从官方文档了解到,这里不再赘述。

以下是我用 Grafana 绘制的网络质量监控的页面,其中用到 TemplatingSinglestat Panel 等功能:

grafana-dashboard-network-fullscreen-20160319-2-blurred

请原谅那一段 100% 丢包率的部分,是我的一台 VPS 所在的机房出了问题……

五、监控更多的指标

collectd 还可以做很多事情,只用它的 ping 插件太大材小用了。玩熟了 ping 插件之后,我又用它监控了局域网内各机器(自己的笔记本、Gen8 上运行的其他虚拟机等)的 CPU、内存、磁盘、网络等其他指标。collectd 的客户端也是移植性很强,我甚至在 Raspberry Pi 上也部署了一下。Windows 机器的话,则可以安装 SSC Serv,这是一个协议兼容的 collectd agent,免费版本有五分钟上报一次的限制,基本够用了。

HP ProLiant MicroServer Gen8 上手玩

曾经,一台共享空间的 PHP 「虚拟主机」在大部分人眼里就是很高级的「服务器」了,后来随着虚拟化技术的发展,OpenVZ, Xen, KVM 虚拟机越来越普及,Linode 和 DigitalOcean 更是将 $10/mo 的廉价 VPS 推广开来,VPS 可以做很多 PHP 共享主机做不到的事情。近两年,随着越来越多中小企业的发展,「微型服务器」的概念也慢慢出现了。HP ProLiant MicroServer Gen8 就是其中的一员。最近我也入手了一台,当家庭服务器使用。

c03760124

一、购买

Gen8 是 2013 年的产品了,现在的售价已经比刚出来时便宜不少了。爱折腾的可以去海淘一个便宜的版本,我为了省事就直接在□东上购买了。□东上提供了三个型号,分别是:

  • Intel G1610 CPU + 2 GB RAM
  • Intel G2020 CPU + 2 GB RAM
  • Intel E3-1220Lv2 CPU + 4 GB RAM

主要的区别就是在 CPU 了,三款 CPU 分别是赛扬(Celeron)、奔腾(Pentium)和至强(Xeon)系列的产品,价格也由低到高。然而这内存实在是令人郁闷,两条插槽明明可以可以最高 16 GB(甚至单条 16 GB 的话可以最高 32 GB,兼容性未知),但最高配置也就 4 GB 是什么鬼……考虑了一下自己主要是用来做 NAS 使用,但是同时又有虚拟化的需求,因此决定购买赛扬型号的,然后自己加内存。

最终我的购买清单是:

  • HP ProLiant Gen8, Intel G1610 CPU, 2 GB RAM
  • Kingston DDR3 1600 ECC RAM, 8 GB
  • Samsung 850 EVO SSD, 120 GB
  • WD Red NAS HDD, 2 TB
  • 六类网线,3 米 * 3

Gen8 一共四个 SATA 盘位外加一个光驱位,因此有心折腾的话最多可以扩展成五块硬盘。我按照我目前的需求就一块 SSD 一块 HDD 搞定了。网线我买了三根,是因为 Gen8 有三个 RJ-45 接口,两个是主网卡上的,另一个是 iLO 的管理接口。在 iLO 中也可以设置让 iLO 和主网卡共享网络接口。

二、开箱及组装

以上内容合计约 3700 人民币,下单后当天下午就送到了。以下多图杀猫,点击可看大图。

在装硬盘的时候我踩坑了。由于之前对 Gen8 的 SATA 背板不了解,想当然地买了块普通的 2.5 英寸转 3.5 英寸硬盘支架,想要把 SSD 装到 1 号槽、HDD 装到 2 号槽的时候发现 2.5 英寸的 SSD 套个普通的 3.5 英寸支架并不能插进硬盘槽里……于是最后的解决方案是把 1 号槽的背板拆下来,拉到顶上,把 SSD 塞在那里,于是最后把 SSD 放顶上了。好在那儿本来就是光驱的位置,所以并不影响盖上盖子。

另一种方案是把 SSD 接到闲置的光驱线上,这样下面四个盘位都可以插满大容量 HDD。这种方案需要自己购买 SATA 转接线。

在装好硬盘之后,需要在 BIOS 里配置 B120i RAID 控制器。说是 RAID 控制器,其实是个伪 RAID,加之我并没有 RAID 需求,我直接禁用掉了,这样操作系统看到的就是个普通的 SATA 控制器,能直接访问到下面挂载的硬盘。

三、搭建 ESXi 虚拟化环境

Gen8 作为一个微型服务器,定位于中小企业和家庭,配置远不如它那些安身于 IDC 里的同侪们,但是用作网络存储却是非常合适。四盘位使用 3 TB 硬盘可提供 12 TB 的存储空间;如果使用那些 6 TB 的硬盘的话,更可扩展至 24 TB 存储空间。我入手 Gen8 也是想作为网络存储的,但是同时又想一机多用,跑几个虚拟机玩玩。因此需要一套虚拟化方案。

虚拟化方案选择

纳入我考虑范围的有 VMware ESXiXenServerlibvirt + QEMU 这三种方案。综合我自己的需求、自身的兴趣(后两者都已经玩过了,第一个还没玩过),以及 Twitter 的投票,我选择了 VMware ESXi 作为虚拟化方案。

ESXi 的前身是 ESX,是 VMware 出品的优秀的 bare metal 虚拟化方案,在 VMware 网站上可以免费注册得到一份授权,可无限期使用。与 VirtualBox、QEMU、Hyper-V 等方案不同,ESXi 是 Type-1 Hypervisor,它直接运行于裸机上,不需要先安装一个操作系统,因此性能开销小,本身占地也很小,很适合安装到 TF 卡或 U 盘等小型存储器中。正巧,Gen8 的机箱内主板上有一个 USB 和一个 TF 卡接口,可以把 ESXi 安装进去。由于 ESXi 启动完成之后就在内存中运行了,不用担心 U 盘和 TF 卡的读写性能问题。

安装并连接到 ESXi

在 VMware 网站上可以下载到 ESXi 6 的安装镜像,但是 HP 为 Gen8 专门制作了一个定制版的 ESXi 镜像,自带了 Gen8 的驱动程序,建议使用 HP 版本包括我在内已经有两位用户发现 HP 定制版 ESXi 与 RDM 有兼容性问题,会导致 ESXi 虚拟机卡死。使用原版 ESXi 镜像无兼容性问题。安装镜像约 380 MiB,局域网内网络挂载安装也不会太慢,直接使用 iLO 的虚拟光驱挂载功能即可。注意,默认的 iLO 版本并不支持虚拟光驱功能,需要去 HP 那里申请一个 60 天 iLO Advanced 试用版授权。或者你可以本地起个一个 HTTP 服务器,然后使用 iLO 免费版里的从 URL 加载镜像的功能安装。

我不知道原版 ESXi 安装过程是什么样的,但是 HP 为 Gen8 定制的 ESXi 安装镜像在整个安装过程中除了问了我安装到哪里,其他什么也没问。我自然是选择安装到 U 盘了(记得提前插好)。安装完成后可以看到上灰下黄的屏幕上显示着 ESXi 通过 DHCP 自动获取的 IP 地址,用浏览器访问即可看到欢迎页面,然后……你几乎什么也不能做,不过你可以顺着欢迎页面上的链接去下载个 vSphere Client。

这是令我比较郁闷的部分。通过这几天的使用,我发现 ESXi 是个好东西,但是它提供的连接方式却不怎么样。目前你有四种方式去管理它上面的虚拟主机:

  • 使用 vSphere Client —— 这是一个 Windows-only 的客户端;
  • 使用 vSphere Web Client —— 你需要部署 vCenter,对于只跑一个 ESXi 实例的用户来说太不值得了;
  • 使用 vSphere CLI —— 也依赖 vCenter;
  • 安装 ESXi Web UI —— 似乎是个 2016 年新出的试验产品,功能还不完善,bug 也多。

作为一个 Arch Linux 用户,我自然想免客户端直接从浏览器里管理虚拟机了,但是安装并使用了 ESXi Web UI 之后我发现它实在是初级了一些,于是最后还是屈服于 VirtualBox + Windows + vSphere Client 了。

连接到 ESXi 之后需要先创建数据存储(Datastore)。前面说过,我的 Gen8 主板上插了个 16 GB 的 U 盘(ESXi 就装在里面),硬盘位插了一块 SSD 和一块 HDD,我直接把整块 SSD 都添加到 ESXi 里作为 Datastore 了。ESXi 会将其分区并格式化成 VMFS 作为虚拟机存储。剩下的 HDD 则通过 RDM 分配给虚拟机使用。

创建磁盘映射(RDM)

vSphere Client 的 GUI 还是不错的,功能强大、操作简单,创建虚拟机的过程不再详述。下面讲讲 RDM。

前面说过,NAS 将是 Gen8 的重要用途,因此大容量硬盘是少不了的,但是我的 NAS 是跑在虚拟机里的,考虑到数据迁移和恢复的便利性,最好能让虚拟机直接访问到物理磁盘,而不是再隔一层文件系统。让虚拟机直接访问到物理机的磁盘有两种思路:Passthrough 和 RDM (Raw Disk Mapping)。前者需要 CPU 支持 VT-d,并且按照 @Orz_C 的实验,似乎只能把整个 SATA 控制器(连同下面的四个盘位)一起直通进去,不能单独传递下面挂载的磁盘。很遗憾,我的赛扬 CPU 虽然支持 VT-x 却不支持 VT-d,并且我的 SATA 控制器下面挂的并不都是想传递给虚拟机的硬盘,因此此路不通。

那就用 RDM 吧。RDM 其实是 VMDK(VMware 开发的开放虚拟磁盘格式)的功能,原理是创建一个(几乎不占空间)的特殊 .vmdk 文件映射到一块物理磁盘,当虚拟机向这块 VMDK 写入时,实际写入的是后面的物理磁盘。在 VirtualBox 里也可以用这种方法让虚拟机直接使用物理磁盘。ESXi 支持在 GUI 中创建到 LUN 的 RDM,却必须要借助命令行才能创建到本地 SATA 磁盘的 RDM。

在 ESXi 的设置中打开 SSH 访问,用 SSH 登录之后看到 /dev/disks/ 底下有 Gen8 上所有的磁盘:

[root@ezsetupsystem3ca82a9fd134:~] ls -lh /dev/disks/
total 4160150040
-rw------- 1 root root 14.4G Mar 13 16:29 mpx.vmhba32:C0:T0:L0
-rw------- 1 root root 4.0M Mar 13 16:29 mpx.vmhba32:C0:T0:L0:1
-rw------- 1 root root 250.0M Mar 13 16:29 mpx.vmhba32:C0:T0:L0:5
-rw------- 1 root root 250.0M Mar 13 16:29 mpx.vmhba32:C0:T0:L0:6
-rw------- 1 root root 110.0M Mar 13 16:29 mpx.vmhba32:C0:T0:L0:7
-rw------- 1 root root 286.0M Mar 13 16:29 mpx.vmhba32:C0:T0:L0:8
-rw------- 1 root root 2.5G Mar 13 16:29 mpx.vmhba32:C0:T0:L0:9
-rw------- 1 root root 111.8G Mar 13 16:29 t10.ATA_____Samsung_SSD_850_EVO_120GB_______________S21XXXX_____
-rw------- 1 root root 111.8G Mar 13 16:29 t10.ATA_____Samsung_SSD_850_EVO_120GB_______________S21XXXX_____:1
-rw------- 1 root root 1.8T Mar 13 16:29 t10.ATA_____WDC_WD20XXXX_________________________WD2DXXXX
-rw------- 1 root root 1.8T Mar 13 16:29 t10.ATA_____WDC_WD20XXXX_________________________WD2DXXXX:1
lrwxrwxrwx 1 root root 20 Mar 13 16:29 vml.0000000000766d68626133323a303a30 -> mpx.vmhba32:C0:T0:L0
lrwxrwxrwx 1 root root 22 Mar 13 16:29 vml.0000000000766d68626133323a303a30:1 -> mpx.vmhba32:C0:T0:L0:1
lrwxrwxrwx 1 root root 22 Mar 13 16:29 vml.0000000000766d68626133323a303a30:5 -> mpx.vmhba32:C0:T0:L0:5
lrwxrwxrwx 1 root root 22 Mar 13 16:29 vml.0000000000766d68626133323a303a30:6 -> mpx.vmhba32:C0:T0:L0:6
lrwxrwxrwx 1 root root 22 Mar 13 16:29 vml.0000000000766d68626133323a303a30:7 -> mpx.vmhba32:C0:T0:L0:7
lrwxrwxrwx 1 root root 22 Mar 13 16:29 vml.0000000000766d68626133323a303a30:8 -> mpx.vmhba32:C0:T0:L0:8
lrwxrwxrwx 1 root root 22 Mar 13 16:29 vml.0000000000766d68626133323a303a30:9 -> mpx.vmhba32:C0:T0:L0:9
lrwxrwxrwx 1 root root 74 Mar 13 16:29 vml.0100000000202020202057442d574343344d30465a36374831574443205744 -> t10.ATA_____WDC_WD20XXXX_________________________WD2DXXXX
lrwxrwxrwx 1 root root 76 Mar 13 16:29 vml.0100000000202020202057442d574343344d30465a36374831574443205744:1 -> t10.ATA_____WDC_WD20XXXX_________________________WD2DXXXX:1
lrwxrwxrwx 1 root root 72 Mar 13 16:29 vml.0100000000533231564e58414831373636383944202020202053616d73756e -> t10.ATA_____Samsung_SSD_850_EVO_120GB_______________S21XXXX_____
lrwxrwxrwx 1 root root 74 Mar 13 16:29 vml.0100000000533231564e58414831373636383944202020202053616d73756e:1 -> t10.ATA_____Samsung_SSD_850_EVO_120GB_______________S21XXXX_____:1

那些 mpx 开头的是那只 16 GB 的 U 盘及其分区,四个 t10 开头的代表 SSD 和 HDD,剩下的那些是指向前面那些的符号链接。

这里我选择 WDC 的那个块设备(即我的 WD 硬盘),创建 RDM:

vmkfstools -r /dev/disks/XXXX /vmfs/volumes/EVO/WDYYYY.vmdk

第一个参数是代表物理磁盘的块设备(注意不带冒号,即整块硬盘,而不是其中的分区),第二个参数是创建的 .vmdk 文件的路径,我这里保存到上一节创建的名为 EVO 的 Datastore(即我的 SSD)根目录下名为 WDYYYY.vmdk 的文件(我是拿硬盘序列号作为文件名的)。

另外,-r 参数创建的是 Virtual Compabilitiy Mode RDM,即 ESXi 会截获除 READ / WRITE 之外所有 SATA 指令;如果换成 -z 参数,则是创建 Physical Compability Mode RDM,即 ESXi 除了 LUN REPORT 指令,其他全部原样传递给物理磁盘。没啥特殊需求用 -r 即可。两种模式只有在 hdparm 等涉及到磁盘本身参数的命令才会有区别,数据层面没有区别。

将这样创建得到的特殊 .vmdk 文件分配给虚拟机,便可在虚拟机中访问到外层 ESXi 的硬盘。

四、搭建 NFS 和 Samba

接下来需要将硬盘中的文件分享给其他主机。如果你的网络里只有 Linux 主机的话,那么搭个 NFS 就足够了。初版 NFS 于 1984 年由 Sun 研发,几十年来久经考验,十分适合局域网内文件共享。

在 Linux 发行版中,NFS 服务通常由 nfs-utils 包提供,安装后修改 /etc/exports 文件,将硬盘挂载点共享出去即可。比如这是我的配置:

/media/disk1/public 192.168.2.0/24(ro,sync,no_subtree_check,no_root_squash)
/media/disk1/incoming 192.168.2.0/24(rw,sync,no_subtree_check)

共享了 public 和 incoming 两个目录,前者允许局域网内读写操作,后者则是只读。修改此文件用后可以用 exportfs -rav 命令重载。注意,no_root_squash 使得拥有 NFS 客户机 root 权限的人也可以 root 身份对 NFS 服务器的共享目录进行操作(与 MooseFS 的默认行为一致),如果不需要这一特性可以关闭。

在我的 Linux 笔记本上,可将 NFS 挂载项写个 /etc/fstab 实现开机自动挂载(voila 是文件共享服务器的主机名):

# NFS @ voila
voila:/media/disk1 /nfs/voila nfs4 rsize=8192,wsize=8192,timeo=14,_netdev 0 0

因为我还需要向 Windows 用户分享文件,因此我还装了一个 Samba。Samba 的配置比 NFS 复杂多了,以下是我用的最简配置:

# /etc/samba/smb.conf
[global]
workgroup = WORKGROUP
server string = Voila Samba Server
hosts allow = 192.168.2. 127.
log file = /var/log/samba/%m.log
max log size = 50
security = user
map to guest = Bad User

[public]
comment = Public Read-only
path = /media/disk1/public
public = yes
browseable = yes
read only = yes

[incoming]
comment = Public Read-write
path = /media/disk1/incoming
public = yes
only guest = yes
browseable = yes
read only = no
writable = yes

启动 Samba 服务后,列在 hosts allow 里的主机即可无密码访问 Samba 共享。Windows 用户可以将 Samba 目录映射为网络驱动器,方便日后访问。

五、家庭服务器的其他用途

本节列举一些我能想到家庭服务器的其他用途,可能是我已经实现的、正在实现的、想要实现的,以及暂时不想实现的。排名不分先后,仅供参考:

六、致谢

以下推友作为 Gen8 的先驱玩家,在本文作者折腾 Gen8 的过程中给予了诸多帮助和指导,特此感谢(排名由 sort 提供):

使用 nghttpx 搭建 HTTP/2 代理

HTTP/1.1,定义于 1999 年,至今仍在流行。纵使人们试图在它上面添加各种黑科技,但它依然有各种各样的不足。终于,在 2015 年 5 月,HTTP/2 发布了。HTTP/2 基于 SPDY 而建,性能和特性较 HTTP/1.1 有了极大的提升,此外,虽然 HTTP/2 标准本身并没有强制 TLS 加密(HTTPS),但主流实现(Google Chrome, Mozilla Firefox)均要求 HTTP/2 被包裹在 TLS 中,因此,HTTP/2 + TLS(HTTPS)已是事实上的标准

本文中,如无特殊说明,「HTTPS」指代「HTTP + TLS」,其中的 HTTP 可以是 HTTP/1.1, SPDY/3.1 或 HTTP/2;但由于几乎所有的 HTTP/2 实现全部要求 TLS,因此单说「HTTP/2」的话,一般指自带了 TLS 的 HTTP/2。

本文介绍使用 nghttpx 配合 Squid 搭建一个支持 HTTP/2 的 HTTPS 代理的方法。

nghttpx 本身并不是一个代理,它只是一个翻译器,因此如果我们需要一个支持 HTTP/2 的正向 HTTPS 代理,可以用一个 HTTP/1.1 的正向代理(如 Squid)和 nghttpx 接在一起实现。使用这样一个 HTTPS 代理,既可以享受 HTTP/2 对多连接的优化(提高客户端和代理服务器之间的连接流畅度),又可以享受外层 TLS 带来的加密和安全。且由于流量特征是 HTTPS,不仅额外开销小,而且在一些封锁严重的 ISP 里也能应用自如。(如封锁了 DTLS 流量的情况下,OpenConnect / AnyConnect 只能 TCP over TCP,效率很低)

一、需求

需求有两种,一种是客户端原生支持 HTTP/2 的,以下以 Chrome 为例:

+------------+    +------------+    +------------+    +------------+                  
|            |    |            |    |            |    |            |                  
|   Chrome   +----+  nghttpx   +----+   Squid    +----+  Internet  |                  
|            |    |            |    |            |    |            |                  
+------------+    +------------+    +------------+    +------------+                  

如图,nghttpx 与 Squid 部署于服务器上,客户端的 Chrome 与 nghttpx 用 HTTP/2 交流,nghttpx 将请求翻译成 HTTP/1.1 发给 Squid,最后 Squid 抓取了结果返回。

另一种是客户端不支持 TLS 的,以下以 Pidgin 为例:

+------------+    +------------+    +------------+    +------------+    +------------+
|            |    |            |    |            |    |            |    |            |
|   Pidgin   +----+  nghttpx   +----+  nghttpx   +----+   Squid    +----+  Internet  |
|            |    |            |    |            |    |            |    |            |
+------------+    +------------+    +------------+    +------------+    +------------+

如图,Pidgin 将 HTTP/1.1 请求发给本机的 nghttpx,本机的 nghttpx 翻译成 HTTP/2 之后发给服务器上的 nghttpx。之后的过程和上一种相同。

二、工具

nghttp2 是一个很优秀的 HTTP/2 的 C 类实现。它的前身是 SPDY 库 spdylay,作者都是 Tatsuhiro Tsujikawa,同时他也是著名下载工具 Aria2 的作者。nghttp2 含有多个组件,其中的 nghttpx 程序,可以进行 HTTP/2 和 HTTP/1.1 之间的翻译,如果编译时链接了 spdylay,它也可以支持 SPDY/3.1。

如果你是 Arch Linux 用户,可以直接使用我维护aur/nghttp2 包,直接 yaourt -S nghttp2 即可,吃豆人会帮你照料好剩下的一切。

如果你是 Debian / Ubuntu 用户,请按照官方 README 完成编译操作,编译完成后在 contrib 目录里可以找到 Upstart 配置文件。注意:nghttp2 库默认是不带 SPDY/3.1 支持的,如果需要 SPDY 支持,请先编译 spdylay 再编译 nghttp2,后者会自动检测到 spdylay 的存在并链接。

如果你是 CentOS 用户,祝您今天有个好心情

三、服务器配置

无论是哪种需求,服务器上都需要 nghttpx 和 Squid。

nghttpx

服务器上 nghttpx,前端接受的是来自客户端的 HTTP/2 请求,后端是 Squid,最小配置是这样:

frontend=0.0.0.0,443;tls
backend=127.0.0.1,3128;no-tls
private-key-file=/path/to/private/key
certificate-file=/path/to/certificate
http2-proxy=yes

其中私钥和证书必须是客户端认可的。你可以选择:

  • NameCheap 之类的网站上买一个商业证书,低至 $9 一年;
  • 自己用 OpenSSL / GnuTLS 等工具签一个,然后在你的客户端里强制设置为信任;
  • 如果你不愿意花钱也不愿意折腾 OpenSSL,那你可以尝试去找家免费的 CA 给你签一个。

需要说明的是,GFW 曾被报道会区分商业证书和野证书并对后者做定点清除。试图使用野证书的同学请将此因素考虑在内。

以上只是最小配置,我个人使用的配置还加上了以下内容,是我在 nghttpx 的文档中挑出来觉得比较有用的选项:

# 使用四个 worker,请根据自己服务器的 CPU 合理调整,太小性能差,太大机器挂
workers=4
# 开启客户端 TLS 认证
verify-client=yes
verify-client-cacert=/path/to/client/ca
# 不添加 X-Forwarded-For 头
add-x-forwarded-for=no
# 不添加 Via 头
no-via=yes
# 不查询 OCSP 服务器
no-ocsp=yes
# 指定 NPN / ALPN 的顺序
#npn-list=spdy/3.1,h2 →这一行已经不用加了,见下
# 只使用 TLS 1.2
tls-proto-list=TLSv1.2
# 只使用 ECDHE 交换(目前性能安全比最优)和 AES 加密,指定 128 位是因为它已经足够安全但性能比 256 位稍优一些
ciphers=ECDHE+AES128
# 开启日志功能
accesslog-file=/var/log/nghttpx/access.log
accesslog-format=$remote_addr [$time_iso8601] "$request" $status $body_bytes_sent $alpn "$http_user_agent"

有关 --npn-list 选项:前文已经说明了,nghttp2 如其名字所示,是一个 HTTP/2 的库,但是由于 Chromium / Google Chrome 的一个 bug(发稿时最新的 v45 仍未修复已在 v46 中修复,Cr 对 HTTP/2 代理的支持有点问题(Firefox nightly 没有问题),而 nghttpx 默认的 NPN / ALPN 顺序是 h2 优先的,所以需要在这里把 spdy/3.1 的优先级调成最高,以便让 Cr 能用 SPDY/3.1 协商……如果它在编译时链接了 spdylay 的话,也能向下支持(已过时的)SPDY。这一选项是调节优先使用哪种协议的。Chrome 46 之后对 HTTP/2 代理的支持已经正常了,这个选项可以不用调了。

有关 --verify-client 功能:请看下文「有关鉴权」一节。

Squid

Squid 是一个久经考验的正向代理。在我们的用例中,它是 nghttpx 的后端,只需监听 localhost 即可。我用的最小配置如下:

http_port 127.0.0.1:3128
http_access allow localhost

# 关闭缓存功能和日志功能
cache deny all
access_log none

# 优先访问 IPv4 站点,有完整 IPv6 支持的机器可以去掉
dns_v4_first on
# 不添加 Via 头
via off
# 删除 X-Forwarded-For 头
forwarded_for delete

我曾试图让 nghttpx 把源 IP 地址发给 Squid 然后让 Squid 记到日志里,但是未能成功,于是 Squid 始终只能记到一堆来自 127.0.0.1 的请求,干脆就把 Squid 的日志关闭,让 nghttpx 去记日志了。其实是可以在 nghttpx 配置里保留 X-Forwarded-For 然后在 Squid 配置里加上 follow_x_forwarded_for allow localhost让 Squid 能记录到原始的来源 IP 地址。感谢 @JmyXu 的指正。

一个可能会让强迫症不爽的地方是,Squid 默认的错误页面会引用 Squid 官网的图片(一只乌贼),而这个图片资源是 http:// 的,因此页面会带有「混合内容」,强迫症用户可以通过编辑 errorpage.css 把这个去掉:

background: url('http://www.squid-cache.org/Artwork/SN.png') no-repeat left;

或者像我一样把这个图片换成 data:image/png;base64 嵌在 CSS 里……

有关鉴权

只按照最小配置来做的话,配置出来的 HTTP/2 代理是没有任何鉴权的,任何人都可以把这个地址填进 Chrome 里当代理用,也就是说,这是一个开放代理。但实践证明:

  • 如果你在公网上搭一个不带 TLS 的 HTTP/1.1 开放代理的话,分分钟各种爬虫就会把你的地址撸走,教你做人;
  • 如果你在公网上搭一个带 TLS 的 HTTP/1.1 开放代理的话,来光顾你的爬虫就非常非常少了,几个月也遇不到几只;
  • 如果你在公网上搭一个只允许 TLS 1.2 的 HTTP/2 开放代理的话,根本不会有爬虫来光顾你……

所以,如果不想弄鉴权的话,问题也不大,因为目前根本没有 TLS 1.2 + HTTP/2 的爬虫,除非你主动把地址告诉别人,否则不会有人来用你的代理。不过,这样毕竟只是迷宫,而不是门锁,所以为了安全还是可以配置一下鉴权。

在这种 TLS 1.2 + HTTP/2 的结构下,鉴权可以在两个阶段做:TLS 和 HTTP(感觉是废话),也就是 nghttpx 和 Squid(好像还是废话)。

在 TLS 层面做鉴权的话,就是用上文所述的 --verify-client 了。你需要自己维护一个 CA,然后把 CA 的根证书放到服务器上,持有该 CA 根证书的私钥签出的证书对应的私钥的用户可以使用该代理,否则根本完成不了 TLS 握手,直接被拒绝。CA 的搭建和管理又是一个巨大的话题了,在此不多做叙述,只是推荐一下两个软件:适用于 GNU/Linux 用户的 XCA,和适用于 OS X 用户的 Keychain。这两个都是能够管理中小型 CA 的 GUI 程序。我个人使用的则是 EasyRSA。当然如果你足够硬核,也可以直接使用命令行的 OpenSSL 去管理 CA。再次强调,这个 CA 只是客户端认证所用的,和你买证书的那种商业 CA 没有也不应该有联系

在 HTTP 层面做鉴权的话,请照着 Squid 官方文档做。

推荐用 TLS 鉴权,你会爱上它的。而且 TLS 鉴权的话,Chrome 能用 AutoSelectCertificateForUrls 策略自动选证书,不用每次开 Chrome 的时候点一下。

四、客户端配置

客户端配置分为两种。Chrome 和 Firefox 等直接支持 HTTP/2 代理的,直接填进去就行。大部分不支持 TLS 的程序,需要在本地再起一个 nghttpx,翻译一下,在本地生成一个 HTTP/1.1 的代理,供程序使用。

无需翻译的程序

Chromium / Google Chrome 理论上支持 HTTPS 代理的,但是如上文所述,目前由于一个 bug 的存在,对 HTTP/2 代理的支持有问题,暂时只能用 SPDY/3.1;Firefox 曾经不能正常使用 HTTPS 代理(当年 Chrome 是唯一能使用 HTTPS 代理的浏览器),现在它的 nightly 版本反而是支持 HTTP/2 了而超越了 Cr……(bug 已经修复)

令人郁闷的是,无论是 Cr 还是 Fx,都不能方便地通过 GUI 配置 HTTPS 代理,只能通过命令行或插件的方式来使用 HTTPS 代理。比如这样一个 pac 文件便可以让 Cr 和 Fx 使用 HTTPS 代理了:

function FindProxyForURL(url, host) {
  return "HTTPS proxy.example.org:443";
}

当然,pac 文件可以写得非常复杂,也可以使用浏览器插件进行更灵活的代理配置。Cr 用户推荐使用 SwitchyOmega

需要翻译的程序

如前文所述,目前除了 Cr 和 Fx,大部分软件是不支持 HTTP/2 的,而 nghttpx 是个 HTTP/1.1 和 HTTP/2 的翻译器,因此我们可以在本机起一个 nghttpx 生成一个 HTTP/1.1 的代理供不支持 HTTP/2 的程序使用。这种情况下,nghttpx 的前端接收 HTTP/1.1 的请求,然后翻译成 HTTP/2 发给服务器上的另一个 nghttpx 实例。最小配置如下:

frontend=127.0.0.1,8080;no-tls
backend=proxy.example.org,443;proto=h2;tls
http2-proxy=yes

同样地,这只是最小配置,我个人使用的配置中还有以下选项:

# 认证用证书和私钥,如果你没用 TLS 认证则不需要
client-cert-file=/path/to/certificate
client-private-key-file=/path/to/private/key
# 四个 worker,请根据自己计算机/手机性能调整
workers=4
# 不添加 X-Forwarded-For 头
add-x-forwarded-for=no
# 不添加 Via 头
no-via=yes
# 不查询 OCSP
no-ocsp=yes
# NPN / ALPN 优先使用 h2
npn-list=h2

另外可能有用的选项是 -k。如果你用了野证书,这个选项让 nghttpx 放弃证书校验(不安全!),此外它在手机等慢速网络下也有缩短启次握手时间的效果。另外如果你想看实时请求情况的话,加上 -L INFO 能看到漂亮的彩色输出。这个 nghttpx 跑起来之后,别的程序设置 http://127.0.0.1:8080 为代理即可使用。

刚才提到了手机。是的,手机。Tatsuhiro Tsujikawa 大大的程序都是为 Android 交叉编译优化过的。(什么,您是 iOS 用户?您还是用您的 APN 代理,也就是 HTTP 明文代理吧……)

在 nghttp2 的文档中,提供了两种方便的交叉编译 Android 版 nghttpx 的方法,一种是自己装 Android NDK 和依赖,然后用 android-configandroid-make 脚本自动做。另一种是用 Dockerfile.android 文件,在 Docker 容器里装上乱七八糟的编译环境和依赖,最后产出珍贵的 nghttpx 文件并复制到容器外面来。编译完的二进制文件记得 strip 一下,能从 11 MiB 减到 2 MiB……

在 Android 上运行起 nghttpx 之后(可以用 JuiceSSH 之类的起一个,无需 root),推荐配合 Drony 使用(也无需 root),该应用使用 VpnService() 捕获所有应用流量,然后再按照你定的规则(来源地址、应用名、目标主机名、目标端口号、HTTP 方法等),将这些流量进行分流(直连、截断、传给代理、交给 pac 处理等)。

五、尾声

我竟然已经整整一年没有写博客了。一年里可以写的东西其实不少,但是由于各种原因的确没怎么写。今天因某人提醒我一年没更新了,又正好手头的事情告一段落,于是便这么写了一篇。也算是能造福一些人吧。

最后,Google Chrome 使用 HTTP/2 代理看 YouTube 4k 效果如下:

youtube-4k-nghttpx

本文原载于: https://wzyboy.im/post/1052.html 。如有转载请注明。

BIOS + GPT + GRUB + Linux + Windows 折腾笔记

其实从标题就能看出来我有多蛋疼了。我不期望还有别的人和我有同样的奇怪需求,但是希望本文的一部分或几部分能对部分折腾者有一定有作用。

一、为什么会有这样的需求

要 BIOS 不要 UEFI

虽说现在的主板都采用 UEFI 了;虽说 BIOS 是很古老的东西了……但是,我实在不喜欢 UEFI 的复杂设计。说是 Unified 但是我感觉它一点也不统一。最重要的是:UEFI 对 Linux 不够友好。

要 GPT 不要 MBR

虽说严格来说 GPT 也是 UEFI 的一部分,但是我对它的印象好多了——MBR 只支持 4 个主分区而 GPT 默认情况就能支持 128 个分区,再也不用小心翼翼地折腾扩展分区和逻辑分区——这也是我所讨厌的。

要 Steam.exe 不要 Steam.deb

虽然我已经用惯了 Arch Linux;虽然 Valve 也有出 Steam for Linux 甚至 SteamOS,但是至少到目前为止,毕竟 Windows 才是正经的玩游戏的操作系统。

二、BIOS + GPT

2009 年之后的主板基本是 BIOS + UEFI 双配置,为了不让 UEFI 来瞎捣乱,我在主板设置里会选择 BIOS Only 以堵死 UEFI 的路。至于 2009 年以前的那些不支持 UEFI 的主板,倒有些需要小心:虽然理论上,只要不是古董电脑,都能支持 GPT,但是有一小部分有问题的 BIOS 会无法从 GPT 启动。 GPT 的分区工具首选 gdisk,不要用太旧的版本,默认就能 4k 对齐。

三、GRUB + Linux

虽然受到软件无政府主义的困扰,但是 GRUB 依旧是一款功能强大且十分流行的引导器。本文所指的 GRUB 一律指 GRUB 版本 2,而不是曾经的 GRUB Legacy。 要想让 BIOS + GPT + GRUB 工作,你需要一个 EF02 分区。由于没有了 post-MBR gap,这个分区是给 GRUB 放置它的 core.img 的,不需要文件系统。事实上,把 core.img 放在一个单独的分区里比放在 post-MBR gap 里稳定、整洁多了。在 gdisk 里新建分区时将分区标识符改为 EF02 即可,大小的话,2 MiB 足够了。 创建完 EF02 分区之后,其他的分区正常创建即可,比如我这样:

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            6143   2.0 MiB     EF02  BIOS boot partition
   2            6144        20977663   10.0 GiB    8300  Linux filesystem
   3        20977664       230692863   100.0 GiB   8300  Linux filesystem
   4       230692864       500117503   128.5 GiB   0700  Microsoft basic data

然后使用 grub-install 即可自动将 core.img 嵌入那个迷你小分区。 至于 Linux 的安装,无需多言。

四、+ Windows

如无特别说明,以下提到的 Windows 指 NT 内核 6.0 以上版本。本文以 Windows 8.1 为例。

傲娇的 Windows

MSDN 明确指出,Windows 只能安装于 BIOS + MBR 或是 UEFI + GPT 的组合上,而 BIOS + GPT 和 UEFI + MBR 是不允许的。这实在是太傲娇了——因为 BIOS + GPT + GRUB + Linux 是完全没有问题的。事实上,我的笔记本电脑刚安装的时候并没有考虑到往硬盘里灌 Windows,因此之前一直是 BIOS + GPT + GRUB 的配置,在这样的情况想让 Windows 入驻,简直是逼我上梁山…… 为什么 MSDN 声称 Windows 不能在 BIOS + GPT 工作?经过我的试验,发现其实只是 bootmgr 读不了 GPT 而已。直到 bootmgr 被唤醒之前,一切都是没有问题的,而 bootmgr 应该去读取 \Boot\BCD 然后再根据 BCD 去加载 \Windows\System32\winload.exentoskrnl.exe。可是 bootmgr 读不了 GPT,直接导致它找不到 \Boot\BCD…… 那么怎么办呢?

  • 换一个能读 GPT 的引导器,读取 BCD 之后正常加载 Windows 内核。——不好意思,这样的引导器不存在。在得出这个结论之前,我吃了很多苦。
  • 将 BCD 放在 bootmgr 能读的地方。——比如一(小)块 MBR 存储设备,它不一定要是物理的,也能是虚拟的。在得出这个结论之前,我流了很多泪。

而 Windows 默认的安装程序要求又高、功能又弱,根本不会给你选择启动文件安装到哪里的,所以必须要手工安装。

要 imagex 不要 Setup.exe

Windows 默认安装框架是在 Windows PE 里使用 Setup.exe。该程序会运行一系列烦人的检查并增加一堆不合理的限定,比如要求 .NET Framework,比如强制添加 300 MiB 的系统分区等。而实际上它所做的事情也不过是:解压 install.wim 到指定分区、写入引导扇区、写入 BCD 这三样。可是这三件事,能自定义的部分还很少或很麻烦,那为啥不自己来呢?所以我更喜欢的方法是启动到 Windows PE 之后手工安装。

Windows PE 哪里来?TechNet 提供的方法是从 WAIK 里生成。但前面说了,Windows 的安装框架是 Windows PE,而 Windows 安装镜像文件也不过就是 Windows PE 和 install.wim 的组合而已,所以直接从安装镜像里就可以释放一个 Windows PE 出来。用 wimlib 提供的 mkwinpeimg 可以很方便地做到这一点,能直接从 Windows 安装镜像中获取 Windows PE 所需的 boot.wim 并制成可启动的镜像文件。如果你懒得自己提取 Windows PE,那么这里有我做好的两份。两份 Windows PE 除了添加了一个 imagex.exe 之外没有任何改动,十分纯洁干净:(其实不添加也行,用更加高大上的 dism 也能释放文件)

install.wim 哪里来?Windows 安装镜像里就有,使用 7z e Windows.iso sources/install.wim 即可把它解压出来。在移动存储设备里保存 install.wim 而不是完整的 iso,甚至能省下 500 MiB 以上的空间。

接下来就是安装了。通过可启动媒体启动进 Windows PE,用 diskpart 进行合理的分区。以下为带注释的操作过程:

# 确认当前硬盘情况
DISKPART> list disk
# 假设系统硬盘为 Disk 0 (Windows 将安装于此)
# Windows PE 所在的盘为 Disk 1 (请忽略)
# 过会儿要创建的 VHD 为 Disk 2 (Windows 启动文件将安装于此)
DISKPART> select disk 0
DISKPART> list partition
DISKPART> select partition 4
DISKPART> format label="Windows 8.1" quick
DISKPART> assign letter=c
# 以上命令格式化要安装 Windows 的分区并分配卷标 C:
# 接下来创建并挂载 VHD
DISKPART> create vdisk file=c:\bootmgr.vhd maximum=32 type=fixed
DISKPART> attach vdisk
# 然后在 VHD 里创建并激活分区。offset 不是必须的,但是我喜欢
DISKPART> select disk 2
DISKPART> create partition primary offset=1024
DISKPART> active
DISKPART> format label=bootmgr quick
# 分配一个卷标 B:
DISKPART> assign letter=b
# 退出 diskpart
DISKPART> exit

然后就是安装 Windows 和 Windows 启动文件了:

# 解压文件,注意根据实际情况选择 index,此处为 1
X:\> imagex /apply install.wim 1 c:
# dism 的等效命令是:(长多了)
X:\> dism /Apply-Image /ImageFile:install.wim /Index:1 /ApplyDir:C:\
# 写入启动代码和启动文件
X:\> bootsect /nt60 b: /mbr
X:\> bcdboot c:\Windows /s b:

至此 Windows 部分就算完成了,可以在 diskpart 里 detach vdisk 然后重启进 Linux 继续操作。

用 MEMDISK 引导硬盘镜像

在上一节中,我们得到了一个装好了 Windows 的分区,以及一块 32 MiB 的虚拟硬盘镜像,这块虚拟硬盘采用 MBR,有且只有一个主分区,主分区里装着大约 19 MiB 的 Windows 启动文件。其实这些启动文件真正核心的只有不到 1 MiB,其他的都是语言包和字体等,如果你闲得无聊可以挂载来删除它们,当然硬盘镜像文件大小不会自动缩小就是了。

那么,MEMDISK 能直接引导 vhd 么?我一开始也觉得不能,因此查到了用 VirtualBox 或 QEMU 来把 vhd 转成 raw image 的方法:VBoxManage clonehd --type raw bootmgr.vhd bootmgr.img。但是后来经 @tjmao 的提醒,我才发现原来当 type=fixed 的时候,vhd 其实就是 raw image 加上 512 字节的 footer。切掉这个尾巴之后,得到的东西和 dd 式的 raw image 是一模一样的。而就算不切掉尾巴,这一部分也会被认为是未分区空间从而被忽略掉。所以,其实 type=fixed 的 vhd 是不用转换,直接可以当 raw image 用喂给 MEMDISK 的。

那么怎么喂呢?根据调用 MEMDISK 的方法不同,具体的语法也有一定差别,完整的叙述可在这里找到。以下是 GRUB 的方法:

menuentry "bootmgr.vhd" {
  linux16 /boot/syslinux/memdisk harddisk
  initrd16 /boot/bootmgr.vhd
}

把这一段复制到 /boot/grub/grub.cfg 中即可使用。如果要使它可在 grub-mkconfig 后自动生成的话,复制到 /etc/grub.d/40_custom 中即可。

至此 Windows 的引导就算是做好了。重启计算机在 GRUB 菜单中选择对应的 menuentry 即可进入。第一次进入的时候会自动安装驱动等,安装完自动重启,然后就功德圆满了。

五、一些可以改动的部分

如导言中所说,有和我完全一样的需求的人应该不存在,但是本文的思路可用于一些别的折腾过程中。以下列举了部分可以改动的部分,适用于不同的具体情况。

Linux 不是必须的

如果你只是想安装 BIOS + GPT 的 Windows,自然不用装 Linux。但是你可能需要备一个 Notepad.exe 在 Windows PE 环境中,用来编辑引导器的配置文件。

GRUB 可以换成别的

如果不装 Linux 了,那单独装个 GRUB 也没啥意思,还不如换点别的引导器。诸如 Syslinux, Grub4Dos 等,都可以用来加载 MEMDISK。具体的语法依然戳这里不过如果换成别的启动器的话,那个启动器得要像 GRUB 一样对 GPT 有支持……

VHD 的大小问题

在想到 VHD 的方法之前,我曾试过的是把装的引导分区的 U 盘整个 dd_rescue 复制下来。这是一个较老的 U 盘,容量只有 4 GB,但是就这样产生的镜像文件也太大了,无法被 MEMDISK 正确加载。虽然最后我成功地把这个镜像挂载之后缩小文件系统和分区使它「瘦身」,但步骤较复杂。想到 VHD 的方法之后就简单了,直接创建一个小一点的 VHD 然后在里面操作即可。现在已知镜像文件不能太大,否则 MEMDISK 加载不了,那么它最小能多小呢?

核心的 Windows 启动文件只有 bootmgrBoot\BCD 这两个,加起来不到 1 MiB。我也试过,引导分区里只放这两个文件,依然能正常引导,但是实际上 VHD 不能只有 1 MiB。这是因为:

  • bcdboot 复制启动文件的时候,默认会复制那些语言包和字体,总共 19 MiB 左右
  • NTFS 分区最小需要 8 MiB

我实验成功的最小大小是 10 MiB 的 VHD,里面装着一个 8 MiB 的 NTFS。

当代的计算机大部分都是 4 GB 和 8 GB 的内存了,所以不用克扣这么一点点空间(毕竟它引导完就被释放掉了),所以创建 VHD 时选择 32 MiB 是个比较好的选择。记得 type=fixed 就行。

六、吐槽和尾巴

好久没写有关 Windows 的博文了。本来以为自己的对 Windows 的需求在 VirtualBox 里就能完全满足了,因此笔记本电脑安装系统的时候并没有为 Windows 考虑过,直接就是 BIOS + GPT + Linux 的组合了。可是自从 2014 年初在迷上了 Call of Duty 之后就入了 Steam 的大坑了,因此对独立的 Windows OS 的需求再次浮现。之前尝试 BIOS + GPT + Linux 既有配置下再塞个 Windows 怎么都没成功,因此选择了 Windows To Go 的解决方案——将整个 Windows 及其引导文件装到外置 USB 硬盘里。可是 USB 机械硬盘的速度又怎么能和 Samsung 840 Pro SSD 相比……于是发愤图强,努力研究,终于实现了 Windows 本体安装在 GPT SSD 里而把引导文件装到 MBR U 盘里的方法。慢慢地这样的方法又不能满足我的要求了,于是继续发愤图强,在各种来源都说这是不可能的情况下,我最终还是曲线救国成功了。

于是便有了本文。

转载请注明出处: https://wzyboy.im/post/1049.html

GNU/Linux 启用 Intel Rapid Start

Intel Rapid Start 是 Intel 公司研发的一种帮助笔记本电脑节省电源和快速恢复的技术,与传统的睡眠(Sleep)和休眠(Hibernate)有一定的区别,相较而言,与混合睡眠(Hybrid Sleep)倒是有一些相似的地方。Intel 说,这技术首先要主板支持,其次要有 SSD,最后要有 Windows。但其实 SSD 和 Windows 都不是必须的。昨天,我就在 HDD + Linux 的组合里成功地使用了 Intel Rapid Start。

一、Intel Rapid Start 原理

其实 Intel Rapid Start 的原理很简单,就是让计算机在传统的睡眠(Sleep)的基础上有机会进入更深层的睡眠(Deep Sleep),用户可以通过 BIOS 设置一个超时,比如一分钟、十分钟、半个小时等(也可以设置「立即」),当计算机进入睡眠状态之后,计时开始,如果用户在计时到达之前唤醒了计算机,那就啥也没发生,如果预定时间达到,则计算机会被浅唤醒(wake up briefly),将内存中的数据写入硬盘(官方文档指 SSD)中的一个特定分区,然后彻底断电。在这之后,用户只能通过按电源键的方式开机,主板固件会把特定分区中的内容读回内存,然后计算机就可以继续使用了。

总结一下,Intel Rapid Start 技术(IRST)的前半段与普通的睡眠是一样的,而后半段则与休眠(Hibernate)类似,但是休眠是由操作系统完成的,而 IRST 则是由主板固件(BIOS 或 UEFI)去完成这一操作,也就是说,在加电自检(POST)之后,直接就是从硬盘中读取内容的过程了,根本没有引导器(Boot loader)和操作系统的加载过程,因此比操作系统级别的休眠要快不少,再加上 SSD 的应用,就更加快了,这就是 Intel 所宣称的「六秒恢复」。按照我的理解,就是恢复速度和睡眠一样快,但是耗电和休眠一样低(零耗电)

Intel 的官方文档提到该技术需要 SSD + Windows,可是我没看出其中的必要性:SSD 是为了加速,如果用 HDD 代替的话除了慢点也没啥问题,而 Windows 呢?令人兴奋的是,2013年6月的时候,Linux 开发者 Matthew Garrett 就提交了一个内核补丁,实现了 Linux 内核对此的支持。该补丁已在 Linux 3.11 中得到了应用,而截至我写这篇文章时,最新的 Linux 内核版本已经是 3.12.6 了,显然可以使用。

二、调整分区

我的计算机是 ThinkPad X240s,该型号内部有一个 SATA 和两个 NGFF 接口,后者是 Intel 推的新接口,可以用来接蓝牙、3G 等模块,也可以用来接 NGFF 接口的 SSD。最理想的配置应该是 SATA 接口用来接 HDD 而 NGFF 用来接 SSD,这样的版本是已经配置好 Intel Rapid Start 的。我的这个版本 SATA 接口上接了一块 500GB 7200 转的机械硬盘,一个 NGFF 接了 WiFi + 蓝牙,另一个 NGFF 是空的。也就是说,没有 SSD,自然默认也没得用 Intel Rapid Start。但除了淘宝一块小而贵的 NGFF SSD 之外没有别的方法了吗?当然不是。仔细阅读 Intel Rapid Start 的用户手册就会发现,它提到主板固件是通过分区标识符来找寻 IRST 分区的,对于 GPT,是 D3BFE2DE-3DAF-11DF-BA-40-E3A556D89593,对于 MBR 则是 0x84

调整前我的分区结构是这样的:

NAME           MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINT
sda              8:0    0 465,8G  0 disk  
├─sda1           8:1    0     2M  0 part  
├─sda2           8:2    0   200M  0 part  /boot
├─sda3           8:3    0    50G  0 part  
│ └─crypt-sda3 254:0    0    50G  0 crypt /
└──sda4          8:4    0 415,6G  0 part  
  └─crypt-sda4 254:1    0 415,6G  0 crypt /home

整块硬盘 465 GiB 使用 GPT 分为四个区,开头 2 MiB 用来存放 GRUB,之后 200 MiB 是 /boot,然后是 50 GiB 和 /,最后剩下的 415.6 GiB 用作 /home。其中 sda3 和 sda4 都是 LUKS 容器。我的目的是要把 sda4 调整为 400 GiB,然后把剩下的 15.6 GiB 划为 sda5,作为 IRST 分区。

由于 sda4 是 GPT + LUKS + ext4 三层嵌套结构,所以需要一层一层地缩小它们,然后再一层一层地撑大它们到合适的位置。

# 把 /home 卸载
umount /home
# 调整文件系统前必须要先检查文件系统
e2fsck -f /dev/mapper/crypt-sda4
# 缩小文件系统至 398 GiB
resize2fs -p /dev/mapper/crypt-sda4 398G
# 缩小 LUKS 容器至 399 GiB
# 换算成 512 KiB 的区块是 399 * 1024 * 1024 * 2 = 836763648
cryptsetup resize crypt-sda4 836763648
# 将 sda4 分区调整为 400 GiB
# 并用剩余的空间建立 sda5
# 建立的时候分区标识符输入 8400
gdisk /dev/sda
# 重启计算机使内核使用新的分区表
reboot
# 再次卸载 /home
umount /home
# 放大 LUKS 容器撑满 sda4
cryptsetup resize crypt-sda4
# 检查文件系统
e2fsck -f /dev/mapper/crypt-sda4
# 放大文件系统至撑满 LUKS 容器
resize2fs -p /dev/mapper/crypt-sda4
# 挂载测试
mount /dev/mapper/crypt-sda4 /home

调整完成后,就是这样的效果了:

NAME           MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINT
sda              8:0    0 465,8G  0 disk  
├─sda1           8:1    0     2M  0 part  
├─sda2           8:2    0   200M  0 part  /boot
├─sda3           8:3    0    50G  0 part  
│ └─crypt-sda3 254:0    0    50G  0 crypt /
├─sda4           8:4    0   400G  0 part  
│ └─crypt-sda4 254:1    0   400G  0 crypt /home
└─sda5           8:5    0  15,6G  0 part

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            6143   2.0 MiB     EF02  BIOS boot partition
   2            6144          415743   200.0 MiB   8300  Linux filesystem
   3          415744       105273343   50.0 GiB    8300  LUKS partition
   4       105273344       944134143   400.0 GiB   8300  LUKS partition
   5       944134144       976773134   15.6 GiB    8400  Intel Rapid Start

三、设置主板固件(BIOS/UEFI)

这一步视不同的制造商自有不同,但主旨就是找到 BIOS/UEFI 中有关 Intel Rapid Start 的设置,将之开启。在完成以上步骤之前,这一选项是不可开启的状态,会提示没有找到合适的存储设备,但是在完成了上述步骤之后,固件就会根据 GUID 找到 IRST 分区,从而允许开启这一功能。至于多久后启用,就是个人爱好了,我选择的是「立即」,也就是相当于用 IRST 代替了睡眠(Sleep)功能。

四、测试

为了测试效果明显,可以先用 dd_rescue 把 sda5 清空一下:dd_rescue /dev/zero /dev/sda5。可以用诸如 head -c 1G /dev/sda5 之类的命令检验一下。

准备完成之后开始测试:echo -n mem > /sys/power/state 将系统挂起至内存(S3,睡眠)→系统顺利睡眠→系统被短暂唤醒并将内存中的内容写入 IRST 分区,此时如果仔细听可以听到机械硬盘写入的声音→写入完成,系统断电停机。

断电停机之后,用户只能通过按电源键把计算机唤醒。按下电源键后可观察到 BIOS/UEFI 上显示类似 Resuming from deep sleep ... 字样,说明这一恢复过程是由固件完成的,与操作系统无关。恢复完成后,直接回复到进入睡眠之前的样子。此时使用 head /dev/sda5 可观察到大量输出,说明 sda5 的确被写入了。

至此 IRST 部署成功。

五、扩展阅读

本文原载于 https://wzyboy.im/post/1022.html ,以 CC BY-NC-SA 3.0 释出,转载请注明出处。