服务器网络启动方式探索Part1:Legacy启动篇

最近花了很多时间、调研了服务器的网络启动方案,目的呢是想设计并实现一个更加统一和标准化的装机系统尽最大努力把物理机、裸金属、虚拟机、以及基于裸金属的虚拟机的操作系统镜像和装机方案融合起来,同时能适应现代的硬件。
本来以为是一件很轻松的事情,毕竟基于PXE的方案已经运行很多年了,似乎简单的修改一些问题、老方案上打打补丁就能很好的实现。但现实又被疯狂打脸,因为面对多个OEM厂商提供的服务器、每家厂商不同的BMC管理系统、每家厂商不同的接口格式和功能。在没有统一服务器供应商或者基于ODM方案之前,似乎那个“完美”的方案并不很容易实现。

不过呢,这些问题也都不重要,关键是在整个调研的过程中,也是补足了很多似懂非懂、一知半解的技术细节,也算是一个非常大的提升了,其实短期的妥协方案,也不影响最终的实现效果。所以准备写写这段时间学习到的知识,也算是补足了之前网络上找不到技术细节的坑吧,本篇算是第一部分吧,从最简单的开始,说说当前Legacy Boot相关的网络启动方案。

UEFI Legacy Boot + PXE

虽然现在所有的服务器厂商都将BIOS的实现换成了UEFI,但为了兼容性考虑,所有的厂商依然提供老的BIOS形式的基于MBRLegacy引导方式,在这个模式下,通过PXE可以实现最基础的功能。

在Legacy模式下,简单来说,PXE是通过网卡内置的一个小固件(PXE Client)实现的。大致的流程可以总结成这样:在系统启动的时候,会启动网卡里的PXE Client,固件启动后、会发起DHCP请求、当DHCP服务器收到PXE Client的DHCP请求后,会通过预定义的字段返回给客户端IP地址信息、TFTP地址信息、以及需要加载的bootloader的名字。PXE Client收到这些信息后,首先会配置IP地址,此时网络就可以通信了,再根据TFTP的地址和名字信息获取到bootloader并执行,剩下的,就由bootloader去拉起内核和OS。

接下来我们尝试搭建一个PXE Server试试:

首先PXE Server依赖两个组件:DHCP Server和TFTP,先配置DHCP Server:

这里使用isc-dhcp-server,附上配置文件:

option domain-name-servers 10.1.1.1;
option routers 10.1.1.1;
default-lease-time 14400;
ddns-update-style none;
subnet 10.1.1.0 netmask 255.255.255.0 {
    range dynamic-bootp 10.1.1.100 10.1.1.120;
    default-lease-time 14400;
    max-lease-time 172800;
    next-server 10.1.1.10;   #关键配置!用于指明tftp server的地址
    filename "pxelinux.0";   #关键配置!用于指明bootloader的名字
}

其他部分的配置并不重要,针对PXE启动来说,最重要的配置只有next-serverfilenamenext-server指明了tftp server服务器的地址,filename指明了bootloader的名字,有了这俩信息,PXE Client就可以去tftp上去获取bootloader并加载了。

接下来,配置tftp。tftp其实不需要配置,安装并启动就行了。主要是需要准备好启动所需要的bootloader和内核、initrd等文件。在配置文件里的pxelinux.0是从哪来的呢?为了简单、一般来说PXE里不使用grub作为bootloader、使用最多的还是PXELINUX,他是syslinux的一部分,所以很简单,只需要安装syslinux就行了,在我的CentOS系统里,这个文件在/usr/share/syslinux/pxelinux.0。直接拷贝到tftp root目录下就行。需要注意的是,在syslinux 5.0以上的版本,还需要把ldlinux.c32这个文件同步拷贝到tftp root下。

有了bootloader、还需要bootloader配置文件,针对pxelinux而言,默认会根据以下的顺序加载配置文件:

/pxelinux.cfg/b8945908-d6a6-41a9-611d-74a6ab80b83d
/pxelinux.cfg/01-88-99-aa-bb-cc-dd
/pxelinux.cfg/C0A8025B
/pxelinux.cfg/C0A8025
/pxelinux.cfg/C0A802
/pxelinux.cfg/C0A80
/pxelinux.cfg/C0A8
/pxelinux.cfg/C0A
/pxelinux.cfg/C0
/pxelinux.cfg/C
/pxelinux.cfg/default

其中根目录指的是和pxelinux.0相同的目录,具体每个文件所代表的含义,可以参考PXELINUX的文档这么做的目的呢,是为了方便同一个PXE Server为多个机器服务,每个机器可以通过单独的配置文件进行配置,而不用为每台客户端配置一个PXE Server了。实际上、有一个广泛使用的装机系统cobbler、也是基于这个特性,来解决不同机器的不同装机配置问题的。

在我们只有一台机器的情况下,默认写一个pxelinux.cfg/default文件就行了:

DEFAULT test-pxe-boot
LABEL test-pxe-boot
  MENU LABEL ^Test Boot
  KERNEL vmlinuz
  APPEND initrd=initrd.img

配置文件很简单,指明KERNEL和initrd的位置就行,默认情况下这些文件依然会从tftp里去取。至于vmlinuzinitrd.img这两个文件的获取,这里就不多说了,最简单的办法,就是从发行版的iso安装镜像里去找就可以了。

到目前为止,一个最简单的PXE Server就搭建完成了。已经可以测试是否能拉起一个RamOS了。当然,我们用的PXELINUX还有很多高级的用法,比如基于http去加载kernel和initrd,从而绕开tftp协议进行加速,或者通过dhcpd下发一些配置等等,这里就不多说了,可以继续参考PXELINUX的文档

UEFI Legacy Boot + iPXE

基于PXE的启动已经很成熟了,但是对于PXE的一个重要依赖,tftp来说,大家还是会觉得他太慢了,毕竟tftp还是比较适用于小文件的传输,在实际的应用中,如果你的initrd.img比较大的话,那么需要花的时间就比较可观了,根据我自己的测试,大概传输200M左右的数据至少也需要60s,即使你用的是25GbE的网卡,依然是这个速度。那么,有没有支持更多协议的方法呢?

答案自然是有的,那就是iPXE,相比于比较原始的PXE来说,iPXE极大的增强了功能,根据官网的描述,相比PXE来说,主要有以下的提升:

  • boot from a web server via HTTP
  • boot from an iSCSI SAN
  • boot from a Fibre Channel SAN via FCoE
  • boot from an AoE SAN
  • boot from a wireless network
  • boot from a wide-area network
  • boot from an Infiniband network
  • control the boot process with a script

可以看到,功能和可编程性提升了很多,尤其重要的,一个是可以支持HTTP协议了,另外还有脚本执行能力,易用性大幅度的提升了。

而对于iPXE,主要有两种使用方式,一种方式,就是把iPXE直接烧进网卡的ROM里,替换掉网卡老的PXE ROM,这样就直接启动到iPXE环境了,另外一种,就是使用链式加载的模式,iPXE支持通过PXE环境、ISO、UEFI、以及其他引导器运行,也就是说,可以先通过PXE启动到iPXE环境,再对iPXE环境进行配置,从而实现系统启动。

这里主要还是会以链式加载的方式进行试验,相比直接刷ROM的方式,兼容性和可操作性都比较好。

那首先第一步还是要实现PXE的配置,只是这次的bootloader要从PXELINUX换成了iPXE,对于DHCP Server来说,配置不会变化太大:

option domain-name-servers 10.1.1.1;
option routers 10.1.1.1;
default-lease-time 14400;
ddns-update-style none;
subnet 10.1.1.0 netmask 255.255.255.0 {
    range dynamic-bootp 10.1.1.100 10.1.1.120;
    default-lease-time 14400;
    max-lease-time 172800;
    next-server 10.1.1.10;  #关键配置!用于指明tftp server的地址
    filename "ipxe.pxe";    #关键配置!用于指明ipxe的名字
}

可以看到,只要把filename换成ipxe的名字就可以了,而这个ipxe.pxe,可以从http://boot.ipxe.org直接下载,顺便说一下,这个网站里还包括了其他环境使用的iPXE,比如UFEI使用的,或者iso、U盘启动使用的启动文件,触类旁通,可以根据需求下载不同的文件。

配置好DHCP Server、下载好文件之后,只要一启动,iPXE就会被拉起。但是问题还没结束,为什么呢,因为iPXE被拉起之后,还是会通过DHCP协议获取IP地址和其他启动的配置,但是思考一下,当iPXE获取DHCP配置的时候,按我们现在的配置,服务器依然会把filename: ipxe.pxe返回给iPXE客户端,然后iPXE拉起iPXE,如此反复,进入了一个死循环。

所以得需要有个办法来打破这个死循环,对于这个问题,官方也给出了对应的方案,一种是将脚本直接嵌入到ipxe文件里,另外一个,就是通过配置DHCP Server实现,由于将脚本嵌入到ipxe文件里需要自己编译,那肯定不是个简单的方案,所以这里还是优先通过配置DHCP Server完成目标。

先想想原理,其实也比较简单,如果DHCP Server能感知到客户端是PXE还是iPXE,如果是PXE,就把”ipxe.pxe”作为文件名传给客户端,如果是iPXE,就把iPXE Script的文件名传给客户端,这样一个简单的if判断,就可以打破这个死循环了,而iPXE确实给我们一个非常明确的区分方式:

option domain-name-servers 10.1.1.1;
option routers 10.1.1.1;
default-lease-time 14400;
ddns-update-style none;
subnet 10.1.1.0 netmask 255.255.255.0 {
    range dynamic-bootp 10.1.1.100 10.1.1.120;
    default-lease-time 14400;
    max-lease-time 172800;
    next-server 10.1.1.10;   #关键配置!用于指明tftp server的地址
    if exists user-class and option user-class = "iPXE" { #根据user-class字段来判断客户端类型
      filename "http://10.1.1.10/boot.script";
    } else {
      filename "ipxe.pxe";
    }
}

对于iPXE来说,在发送DHCP请求的时候,会加上一个user-class Option,值是”iPXE”,根据这个信息,我们就可以区分到底现在是PXE环境还是iPXE环境了。然后就是filename的配置,因为iPXE支持http协议,所以在filename字段,就可以直接填一个URL了,这样就可以把boot.script放到一个HTTP服务器上了。一个最简单的boot.script可以这样写:

#!ipxe

kernel http://10.1.1.10/vmlinuz initrd=initrd.img
initrd http://10.1.1.10/initrd.img
boot

因为支持HTTP协议,内核和initrd.img文件也都可以放在HTTP服务器上了,相比用tftp协议去获取,速度快了10倍不止。同样的,因为支持HTTP协议,对于多台机器同时装机的需求,可以实现的方式就更多了。一个最简单的思路就是使用php或者其他编程语言实现一个页面,在DHCP服务器把返回filename配置成http://10.1.1.10/boot.php,然后程序里根据不同机器的配置,生成不同的ipxe script。iPXE根据不同机器的不同script,来定制化不同的启动逻辑。

总结

到这里,基于UEFI Legacy Boot的网络启动方式基本就说完了,当初PXE协议的设计者们的思路还是十分清晰的。而每个bootloader对于在PXE环境下多配置文件的支持,也是很早就设计好的,在上面的这些例子里,特别是PXE的例子,除了PXELINUX之外,理论上grub或者其他bootloader,都可以很好的完成类似的任务,比如grub的配置文件加载顺序,也可以找到非常完整的文档。而对于后来者iPXE,确实功能上增强了不少,可编程性也有了很大的提升,同时自身也可以作为bootloader,相比之下,确实iPXE会是一个非常好的选择。

下一篇我们再说说UFEI模式下的网络启动,尽情期待!