跳转至

博客

Sunway 处理器架构分析

参考文档

SW26010

Sunway TaihuLight 的层次:

  1. 1 Sunway TaihuLight = 40 Cabinet
  2. 1 Cabinet = 4 Super nodes
  3. 1 Super node = 256 nodes
  4. 1 node = 4 core groups
  5. 1 core group = 1 MPE(management processing element) + 8*8 CPE(computer processing element)

MPE 双精度性能:16 FLOP/cycle * 1.45 GHz = 23.2 GFlops CPE 双精度性能:8 FLOP/cycle * 1.45 GHz = 11.6 GFlops CPE 单精度性能:8 FLOP/cycle * 1.45 GHz = 11.6 GFlops 单节点双精度性能:4 * 8 * 8 * 11.6 + 4 * 23.2 = 3.0624 TFlops Sunway TaihuLight 双精度性能:40 * 4 * 256 * 3.0624 = 125.435904 PFlops

MPE: 32KB L1I, 32 KB L1D, 256 KB L2(中文文献里写的是 512 KB)。乱序执行,4 译码,7 发射(5 整数 2 浮点)。指令预取,分支预测,寄存器重命名,预测执行。5 条整数流水线,2 条 256 位 SIMD 浮点流水线。

CPE:16KB L1I,无 DCache,有 64KB 可重构局部数据存储器(SPM scratch pad memory/LDM local data memory)。2 译码 2 发射,乱序执行,1 条 256 位 SIMD 流水线,1 条整数流水线。不同精度的 SIMD 宽度不同,单精度浮点运算 128 位(4 个单精度),双精度浮点运算 256 位(4 个双精度)。从 SPM 每个周期可以读取 32 字节的数据(正好一个 SIMD 寄存器)。

每个 core group 中还有一个 MC(Memory Controller),连接 8GB DDR3 memory,每个 MC 内存带宽 128 bit * 2133 MT/s = 34.128 GB/s,单节点内存带宽 4 * 34.128 = 136.512 GB/s。在 Stream Triad 测试,每个 core group 用 DMA 从内存到 SPM 传输数据带宽为 22.6 GB/s,而全局读写 gload/gstore 带宽只有 1.5 GB/s。访问全局内存需要 120+ 个周期。

8x8 矩阵中的从核可以在同行和同列方向上进行低延迟和高带宽的数据传递:2 个从核点对点通信延迟不超过 11 个周期,单个 core group 寄存器通信集合带宽达到 637 GB/s。

28nm 工艺流片,芯片 die 面积超过 500 mm^2,峰值功耗 292.7W,峰值能效比达 10.559 GFLOPS∕W(HPL 6.05 GFLOPS/W)。

SW26010-Pro

SW26010-Pro 是升级版 SW26010,升级的内容在于:

  1. 每个 node 从 4 个 core group 升级到 6 个,一共有 6 * (8 * 8 + 1) = 390 个核心。频率也提高了,MPE 频率 2.1GHz,CPE 频率 2.25 GHz。SIMD 宽度扩展到 512 位。
  2. 每个 MC 连接了 16 GB DDR4 内存,带宽是 128 bit * 3200 MT/s = 51.2 GB/s;单节点总内存 96 GB,总内存带宽 51.2 * 6 = 307.2 GB/s
  3. 每个 CPE 的局部存储(LDM)从 64KB 升级到 256KB。
  4. CPE 之间的通信可以通过 RMA 进行,而之前的 SW26010 只能在同一行/列之间进行寄存器通信。

SW52020

在新闻稿和 Sunway supercomputer architecture towards exascale computing: analysis and practice 文章中出现,没有在今年发出来的论文里实际采用,名称可能是新闻稿自己编的,我猜可能没有实际采用,而是做了 SW26010P。和 SW26010 区别:

  1. Core Group 从 4 个提升到了 8 个,所以每个 node 有 8 * (8 * 8 + 1) = 520 个核心。
  2. MPE 和 CPE 向量宽度从 256 位扩展到了 512 位。添加了 16 位半精度浮点支持。
  3. 每个 node 提供超过 12 TFlops 的双精度浮点性能。应该是靠两倍的 Core Group,乘上两倍的向量计算宽度,达到四倍的性能。

移植系统到 Rocket Chip on VCU128

背景

最近需要在 VCU128 上搭建一个 SOC,然后想到可以把 OpenSBI、U-Boot 和 Linux 移植到这个平台上方便测试,于是又开始折腾这些东西。代码仓库都已经开源:

Rocket Chip on VCU128

第一部分是基于之前 rocket2thinpad 在 Thinpad 上移植 Rocket Chip 的经验,做了一些更新,主要是因为 VCU128 的外设不大一样,同时我也要运行更复杂的程序,主要做了这些事情:

  1. 添加了 VCU128 的内存和外设:HBM、SPI、I2C、UART、ETH
  2. 打开了更多核心选项:S-mode 和 U-mode

主要踩过的坑:

  1. BSCAN 不工作,估计是因为一些参数不对,@jsteward 之前在 zcu 平台上做了一些测试,估计要用类似的办法进行修改;我最后直接去掉了这部分逻辑
  2. 这个板子的 PHY RESET 信号要通过 I2C 接口访问 TI 的 Port Expander,所以没法直接连,要通过 gpio 输出来手动 reset
  3. SPI Startup Flash 的时序配置,见我之前的博客
  4. Xilinx PCS/PMA IP 也会自己挂一个设备到 MDIO bus 上,应该有自己的 PHY 地址,而不要和物理的 PHY 冲突

U-Boot

在 U-Boot 上花了比较多的时间,用它的目的主要是:

  1. BootROM 中的代码只支持从串口加载程序,如果后续要加载 Linux 内核等软件,性能太差。
  2. U-Boot 驱动比较完善,而且 dts 也可以很容易地迁移到 Linux 中
  3. 有一些可以参考的资料

移植的时候,首先新建一个自定义的 board,然后自己写 defconfig 和 dts,其中 dts 可以参考 rocket chip 生成的 dts 文件。然后,按照各个外设的 device tree binding 去写,然后打开/关闭各个 CONFIG 开关。

对代码主要的改动是,实现了 DCache 的 flush 功能,因为以太网部分用了 DMA,所以要让外设看到内存的更改,这里采用的是 SiFive 的扩展指令 cflush.d.l1。由于编译器还不支持这个指令,就按照网上的方式去构造了汇编指令。实现完成以后,就可以用网络了。

一开始的时候,为了简单,直接在 M-mode 中运行 U-Boot,这样不需要 OpenSBI,同时 DTB 也是内置的。但后续为了运行 Linux,还是需要一个 SBI 实现:OpenSBI,然后在 S-mode 中运行 U-Boot,再引导到 Linux。

此外还花了很多努力来缩小 binary 大小,首先可以用 nm --size -r u-boot | head -20 来找到比较大的一些符号,不考虑其中 BSS 的部分(type=b),主要看哪些代码/数据比较占空间。

UPDATE: U-Boot 在 v2022.01 版本修复了一个 BUG,之前的版本在 riscv 架构下没有 reserve lmb region,使得加载 initrd 的时候,会覆盖掉自己的栈空间,这解释了之前的诸多玄学内存问题,升级到 v2022.01 后就好了。

OpenSBI

OpenSBI 移植比较简单,直接参考 template 修改即可,主要就是串口的配置,其他基本不用改。然后,我把 U-Boot 作为 OpenSBI 的 Payload 放到 OpenSBI 的后面,此时要把 U-Boot 配置为 S-mode 模式。接着,遇到了新的问题:cflush.d.l1 指令只能在 M-mode 用,因此我在 OpenSBI 代码中处理了 trap,转而在 M-mode 里面运行这条指令。这样,就可以在 S-mode 里刷新 Cache 了。

Linux

Linux 目前可以 boot 到寻找 init,还没有碰文件系统,之后计划用 buildroot 打一个 initramfs 出来。为了在 U-Boot 中启动 Linux,用 U-Boot 的 mkimage 工具生成了 FIT 格式的 uImage,里面打包了 kernel image 和 dtb,就可以用 bootm 命令启动了,注意地址不要和加载地址重复。

此外还遇到一个坑:RV64 里面 Linux dts 的 address cell 得是 2(对应 64 位),否则会有错误。但 U-Boot 对这个没有做要求。

缓存一致性

一开始的时候,AXI DMA 直接接到内存上,所以与 CPU 缓存是不一致的,网卡驱动需要经常地刷缓存。在 Rocket Chip 上,可以用 sifive 自己的 cflush 指令来刷缓存,但是它只能在 M 态执行,同时又支持虚拟地址,这种奇怪的设计就使得要在 OpenSBI,U-Boot 和 Linux 三处都添加逻辑:OpenSBI 处理 illegal instruction,如果发现是 cflush 指令,就再次 cflush;U-Boot 和 Linux 修改驱动,在合适的地方添加 cflush 指令。U-Boot 驱动比较简单,工作得比较好,但是 Linux 的网卡驱动怎么都改不好。

最后决定,打开 Rocket Chip 的 Frontend Bus,添加一个 AXI Slave 接口,然后让 AXI DMA 通过 AXI Slave 接入到 Rocket Chip 中,然后通过 TLBroadcast 实现缓存一致性。这样软件实现会比较简单,但是硬件就更复杂了。

ESXi 常用信息

常用链接

vsish -e cat /hardware/cpu/cpuList/0 | grep -i -E 'family|model|stepping|microcode|revision'
$ esxcli software vib install -d $PWD/Net-Community-Driver_1.2.0.0-1vmw.700.1.0.15843807_18028830.zip

离线升级方法

  1. 下载 Offline Bundle 文件
  2. 上传到 ESXi datastore 中
  3. /vmfs/volumes/ 里找到更新文件
  4. 查询 profile 列表 esxcli software sources profile list -d <zip>
  5. 更新到 profile esxcli software profile update -p <profile> -d <zip>

ref: Upgrade or Update a Host with Image Profiles

如果 CPU 比较旧,可能会有警告:Updated Plan for CPU Support Discontinuation In Future Major vSphere Releases,按照信息添加参数忽略即可,ESXi 7.0 系列都是支持的,如果之后出了新的版本可能不支持。

在线升级方法

$ esxcli network firewall ruleset set -e true -r httpClient
# find profile name
$ esxcli software sources profile list -d https://hostupdate.vmware.com/software/VUM/PRODUCTION/main/vmw-depot-index.xml
# upgrade to 7.0u3 for example
$ esxcli software profile update -p ESXi-7.0U3-18644231-standard -d https://hostupdate.vmware.com/software/VUM/PRODUCTION/main/vmw-depot-index.xml

ref: Update Standalone ESXi Host

目前 OEM 版本还没找到在线升级方法,需要下载 zip 然后按照离线升级方法安装。

防火墙

列出所有防火墙规则:

$ esxcli network firewall ruleset list

允许出站 SSH:

$ esxcli network firewall ruleset set --enabled=true --ruleset-id=sshClient

关闭出站 SSH:

$ esxcli network firewall ruleset set --enabled=false --ruleset-id=sshClient

NUC11i5 ESXi 7.0 安装过程

  1. 下载 ESXi ISO 文件,用 UNetbootin 制作安装盘
  2. 插入 U 盘,在 NUC 上安装 ESXi,在 81% 的时候卡住了,不管直接重启
  3. 用 root 无密码登录进去,然后重置网络设置
  4. 配置 usb 网卡,然后通过网页访问 ESXi,打开 SSH
  5. 下载 Fling 上面的社区网卡支持,用 esxcli 安装
  6. 重启以后,就可以看到 vmnic0 网卡了

参考:Solution: ESXi Installation with USB NIC only fails at 81%

推荐博客

发现以下博客有很多关于 ESXi 的内容:

  • https://williamlam.com/
  • https://www.virten.net/

重要版本更新

参考 ESXi/vCSA Release Notes:

  • Performance improvements for AMD Zen CPUs: With ESXi 7.0 Update 2, out-of-the-box optimizations can increase AMD Zen CPU performance by up to 30% in various benchmarks. The updated ESXi scheduler takes full advantage of the AMD NUMA architecture to make the most appropriate placement decisions for virtual machines and containers. AMD Zen CPU optimizations allow a higher number of VMs or container deployments with better performance.
  • Reduced compute and I/O latency, and jitter for latency sensitive workloads: Latency sensitive workloads, such as in financial and telecom applications, can see significant performance benefit from I/O latency and jitter optimizations in ESXi 7.0 Update 2. The optimizations reduce interference and jitter sources to provide a consistent runtime environment. With ESXi 7.0 Update 2, you can also see higher speed in interrupt delivery for passthrough devices.
  • vSphere Lifecycle Manager fast upgrades: Starting with vSphere 7.0 Update 2, you can configure vSphere Lifecycle Manager to suspend virtual machines to memory instead of migrating them, powering them off, or suspending them to disk. For more information, see Configuring vSphere Lifecycle Manager for Fast Upgrades.
  • Zero downtime, zero data loss for mission critical VMs in case of Machine Check Exception (MCE) hardware failure: With vSphere 7.0 Update 3, mission critical VMs protected by VMware vSphere Fault Tolerance can achieve zero downtime, zero data loss in case of Machine Check Exception (MCE) hardware failure, because VMs fallback to the secondary VM, instead of failing. For more information, see How Fault Tolerance Works.

vCSA 相关常见错误

  • https://kb.vmware.com/s/article/85468 vCSA 日志分区 /storage/log 满,原因是访问 vmware 网站失败打印的日志太大:/storage/log/vmware/analytics/analytics-runtime.log*;解决方法:vmon-cli -r analytics 重启服务,然后删掉旧的日志。
  • https://kb.vmware.com/s/article/83070 vCSA 日志分区 /storage/log 满,原因是 tomcat 日志太大。
  • XXX Service Health Alarm:尝试重启对应服务,比如 vmon-cli -r perfcharts 对应 Performance Chartsvmon-cli -r vapi-endpoint 对应 VMWare vAPI Endpoint

查看更新状态:cat /storage/core/software-update/stage_operation;更新文件下载路径:/storage/updatemgr/software-update*/stage。有一个包特别大:wcpovf 需要两个多 G。

CLI 更新方法:https://earlruby.org/2021/01/upgrading-vcenter-7-via-the-command-line/

迁移虚拟机到不同 VM

首先,unregister 原来的 VM,然后把文件移动到新的路径下。对于 Thin Provisioned Disk,需要特殊处理,否则直接复制的话,会变成 Thick Provisioned Disk,正确方法是采用 vmkfstool

vmkfstool -i "old.vmdk" -d thin "new.vmdk"

需要注意的是,这里的路径用的是不带 -flat 的 vmdk,因为这个文件记录了 metadata,而 -flat.vmdk 保存了实际的数据。可以用 du 命令看实际的硬盘占用,从而确认它确实是 Thin Provisioned。

如果已经在 Web UI 上复制了,你会发现无法停止复制,解决办法是:

/etc/init.d/hostd restart

这样就会重启 Web UI,不过等它恢复需要很长的时间,还要删掉 cookie。

「教学」AXI Quad SPI 时序分析

本文的内容已经整合到知识库中。

背景

之前一直没搞懂 Vivado 中 xdc 需要怎么编写,遇到一些必须要写 xdc 的时候就很头疼,不知道怎么写才可以得到正确的结果。今天分析了一下 AXI Quad SPI 的时序 xdc,终于理解了其中的含义。

AXI Quad SPI

AXI Quad SPI 是一个 SPI 的控制器,它支持 XIP(eXecute In Place)模式,即可以暴露一个只读 AXI Slave 接口,当接收到读请求的时候,就按照标准的 SPI Flash 命令去对应的地址进行读取,然后返回结果。由于不同厂家的 SPI Flash 支持有所不同,所以 IP 上的设置可以看到厂家的选择。

特别地,一个常见的需求是希望访问 Cfg(Configuration)Flash,亦即用来保存 Bitstream 的 Flash。当 FPGA 上电的时候,如果启动模式设置为 SPI Flash,FPGA 就会向 Cfg Flash 读取 Bitstream,Cfg Flash 需要连接到 FPGA 的指定引脚上,当 FPGA 初始化的时候由内部逻辑驱动,初始化完成后又要转交给用户逻辑。转交的方式就是通过 STARTUP 系列的 primitive。

通常,如果要连接外部的 SPI Flash,需要连接几条信号线到顶层,然后通过 xdc 把信号绑定到引脚上,然后引脚连接了一个外部的 SPI Flash。但由于 Cfg Flash 比较特殊,所以信号从 AXI Quad SPI 直接连到 STARTUP 系列的 primitive 上。如果是采用 STARTUPE2 原语的 7 系列的 FPGA,那么只有时钟会通过 STARTUPE2 pritimive 连接到 SPI Flash 上,其他数据信号还是正常通过顶层绑定;如果是采用 STARTUPE3 原语的 UltraScale 系列的 FPGA,那么时钟和数据都通过 STARTUPE3 primitive 连接到 SPI Flash。

Virtex UltraScale+ 时序

把信号连好了只是第一步,因为外设对时序要求比较复杂,如果用一个比较高直接跑,很大可能就读取到错误的数据了。很贴心的是,AXI Quad SPI 已经在生成的文件里提供了一个样例的 xdc,在文档里也有体现。在这里,我使用的设备是 Virtex Ultrascale+ 的 FPGA,其他系列的 FPGA 会有所不一样。它内容如下:

#### All the delay numbers have to be provided by the user

#### Following are the SPI device parameters
#### Max Tco
set tco_max 7
#### Min Tco
set tco_min 1
#### Setup time requirement
set tsu 2
#### Hold time requirement
set th 3
#####################################################################################################
# STARTUPE3 primitive included inside IP for US+                                                             #
#####################################################################################################
set tdata_trace_delay_max 0.25
set tdata_trace_delay_min 0.25
set tclk_trace_delay_max 0.2
set tclk_trace_delay_min 0.2

create_generated_clock -name clk_sck -source [get_pins -hierarchical *axi_quad_spi_0/ext_spi_clk] [get_pins -hierarchical */CCLK] -edges {3 5 7}
set_input_delay -clock clk_sck -max [expr $tco_max + $tdata_trace_delay_max + $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;
set_input_delay -clock clk_sck -min [expr $tco_min + $tdata_trace_delay_min + $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;
set_multicycle_path 2 -setup -from clk_sck -to [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]]
set_multicycle_path 1 -hold -end -from clk_sck -to [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]]
set_output_delay -clock clk_sck -max [expr $tsu + $tdata_trace_delay_max - $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];
set_output_delay -clock clk_sck -min [expr $tdata_trace_delay_min - $th - $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];
set_multicycle_path 2 -setup -start -from [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]] -to clk_sck
set_multicycle_path 1 -hold -from [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]] -to clk_sck

我们分段来看这个 xdc 都做了什么:

create_generated_clock -name clk_sck -source [get_pins -hierarchical *axi_quad_spi_0/ext_spi_clk] [get_pins -hierarchical */CCLK] -edges {3 5 7}

首先,它创建了一个时钟 clk_sck。CCLK 是 STARTUP 输出的实际时钟,会连接到 Cfg Flash 的时钟信号上。而 AXI Quad SPI 的 ext_spi_clk 会输出到 CCLK 上,因此这里是一个生成的时钟,并且指定上下边沿的位置。edges 参数有三个,分别表示上升、下降和上升沿分别的位置。1 表示源时钟的第一个上升沿,2 表示源时钟的第一个下降沿,以此类推,所以 {3, 5, 7} 的意思就是频率减半,相位差半个周期。

接着,最主要的就是,怎么设置延迟。可以看到,代码中首先定义了一些参数:

#### Max Tco
set tco_max 7
#### Min Tco
set tco_min 1
#### Setup time requirement
set tsu 2
#### Hold time requirement
set th 3

#### Trace delay
set tdata_trace_delay_max 0.25
set tdata_trace_delay_min 0.25
set tclk_trace_delay_max 0.2
set tclk_trace_delay_min 0.2

首先是 \(t_{co}\),应该表示的是 SPI Flash 的时钟到输出的延迟。本文用的 SPI Flash 型号是 Micron MT25QU02GCBB8E12-0SIT,可以从它的 Datasheet 看到,时钟到输出的延迟应该是 Max 7ns:

Clock LOW to output valid under 30pF Max 7ns
Clock LOW to output valid under 10pF Max 6ns

因此 tco_max 设为 7,tco_min 默认即可,因为 Datasheet 中没有做要求。

然后 \(t_{su}\)\(t_h\) 则是输入的 setup 和 hold time。类似的,可以查到 SPI Flash 的参数:

Data in setup time Min 2.5ns
Data in hold time Min 2ns

所以 tsu 设为 2.5,th 设为 2。

接下来则是 tdata 和 tclk 的 trace delay。这指的是从 FPGA 引脚到 SPI Flash 引脚的信号传输延迟。从严谨的角度来说,可以从板子的布线上测量长度来计算出来,不过这里就先用默认值了。一个简单的估算方法:光速 \(3*10^8 \text{m/s}\),考虑电信号传播速度是光速的一半,可以得到延迟和长度的比值: \(0.06 \text{ns/cm} = 0.15 \text{ns/inch}\)

那么,这些变量怎么参与到 input/output delay 的计算呢?

首先考虑 input delay。它指的是,从 SPI Flash 到 FPGA 的数据,相对于时钟的延迟。这个延迟由三部分组成:

  1. 从 FPGA 输出的时钟 CCLK 到 SPI Flash 的时钟有延迟 \(t_{clk}\),下图 a -> b
  2. 从 SPI Flash 的时钟到数据输出有延迟 \(t_{co}\),下图 b -> c
  3. 从 SPI Flash 的数据到 FPGA 的数据输入有延迟 \(t_{data}\),下图 c -> d
clk_fpgaclk_flashdata_flashdata_fpgaabcd

因此总延迟就是 \(t_{clk}+t_{co}+t_{data}\),就可以得到对应的设置:

set_input_delay -clock clk_sck -max [expr $tco_max + $tdata_trace_delay_max + $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;
set_input_delay -clock clk_sck -min [expr $tco_min + $tdata_trace_delay_min + $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;

接下来要考虑 output delay。虽然 output delay 也有 min 和 max,但其含义有所区别,需要分别考虑。

首先是 max,它对应的是 setup time。如果定义时间 0 为时钟的上升沿,沿更早的时间为正的时间轴,沿更晚的时间为负的时间轴。那么,我们希望的是,数据到达寄存器输入的时间大于 setup time,此时可以满足 setup 条件。那么,具体怎么算呢?注意,我们要考虑的是从 FPGA 数据输出到 SPI Flash 上时钟的延迟。

假设 FPGA CCLK 时钟上升沿在 \(0\) 时刻(下图的 a),那么 SPI Flash 时钟上升沿在 \(-t_{clk}\) 时刻(下图的 b)。假设 FPGA 数据输出时刻为 \(t_0\)(通常为正,下图的 c),那么 FPGA 数据输出到达 SPI Flash 在 \(t_0-t_{data}\) 时刻(下图的 d),我们期望 \(t_0-t_{data}\)\(-t_{clk}\) 时刻之前(下图的 d -> b)至少 \(t_{su}\) 时间到达,可以得到表达式:

clk_fpgaclk_flashdata_fpgadata_flashabcd
\[ t_0 - t_{data} > -t_{clk} + t_{su} \]

化简一下,就可以得到 \(t_0 > t_{data} + t_{su} - t_{clk}\),如果考虑极端情况,右侧 \(t_{data}\) 取最大值,\(t_{clk}\) 取最小值,我们就可以得到约束:

set_output_delay -clock clk_sck -max [expr $tsu + $tdata_trace_delay_max - $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];

接下来考虑 output delay 的 min,这对应的是 hold time。我们希望数据到达 SPI Flash 寄存器的时候,距离上升沿时间超过了 \(t_h\)。还是一样的假设,如果 FPGA CCLK 时钟上升沿在 0 时刻(下图的 a),那么 SPI Flash 时钟上升沿在 \(-t_{clk}\) 时刻(下图的 b)。假设 FPGA 数据输出时刻为 \(t_0\)(下图的 c),那么 FPGA 数据输出到达 SPI Flash 在 \(t_0-t_{data}\) 时刻(下图的 d),要求满足 hold 条件,可以得到:

clk_fpgadata_fpgaclk_flashdata_flashacbd
\[ t_0 - t_{data} < -t_{clk} - t_h \]

化简以后,可以得到 \(t_0 < t_{data} - t_{clk} - t_h\),按照极限来取,\(t_{data}\) 取最小值,\(t_{clk}\) 取最大值,可以得到最终的时序约束:

set_output_delay -clock clk_sck -min [expr $tdata_trace_delay_min - $th - $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];

这样就可以实现 FPGA 和 SPI Flash 之间的正常通讯了。我觉得,这里比较绕的就是时间轴的定义,和我们平常思考的是反过来的。而且,这里的 min 和 max 并不是指 \([\min, \max]\),而是 \((-\inf, \min] \cup [\max, \inf)\)。代入上面的数据,可以得到 \(\max=2.05, \min=-2.95, t_0 \in (\inf, -2.95] \cup [2.05, \inf)\)。如果变化的时刻距离时钟上升沿太接近,就会导致在 SPI Flash 侧出现不满足 setup 或者 hold 约束的情况。

也可以换个角度来理解 min 和 max:对于同一个周期的时钟和数据来说,数据相对时钟有一个延迟,这个延迟不能太小,至少要满足 hold,所以这是一个最小的延迟;同时这个延迟不能太大,最多需要满足下一个时钟上升沿的 setup,所以这是一个最大的延迟。如果从这个角度来看,那就是延迟在一个 \([\min, \max]\) 的范围内。但是,这样在计算的时候就需要把时钟周期纳入到 \(\max\) 的计算中,比如 \(\max=t_c-t_{su}\)。如果我们把坐标轴修改一下,原点变成原来的下一个时钟周期的上升沿,x 的正方向变成反向,就可以得到上面的形式了。

Artix 7 时序

那么,更常见的 FPGA 是 7 系列的,比如 Artix 7,它采用的是 STARTUPE2 原语,只有时钟是通过 STARTUPE2 原语的 USRCCLKO 信号传递到 CCLK 引脚上的,其他数据信号都是需要在顶层信号绑定对应的引脚。在 AXI Quad SPI 文档中,描述了 STARTUPE2 所需要的时序约束,我们分段来分析一下。

# You must provide all the delay numbers
# CCLK delay is 0.5, 6.7 ns min/max for K7-2; refer Data sheet
# Consider the max delay for worst case analysis
set cclk_delay 6.7
# Following are the SPI device parameters
# Max Tco
set tco_max 7
# Min Tco
set tco_min 1
# Setup time requirement
set tsu 2
# Hold time requirement
set th 3
# Following are the board/trace delay numbers
# Assumption is that all Data lines are matched
set tdata_trace_delay_max 0.25
set tdata_trace_delay_min 0.25
set tclk_trace_delay_max 0.2
set tclk_trace_delay_min 0.2
### End of user provided delay numbers

可以看到,这一部分和上面 UltraScale+ 部分差不多,只是多一个 cclk_delay 变量,这是因为 Artix 7 中,时钟只能创建到 USRCCLKO 引脚上,但是实际 SPI Flash 接收到的时钟等于 USRCCLKO 到 CCLK 引脚,然后再通过 PCB 上的线传播到 SPI Flash,所以需要手动添加一个偏移,这个偏移就是 USRCCLKO 到 CCLK 的延迟,可以在 Artix 7 Data Sheet 里面看到:对于 1.0V,-2 速度的 FPGA,这个延迟最小值为 0.50ns,最大值为 6.70ns,这里采用了最大值。

所以,下面的约束,除了时钟部分以外,和上面分析的 UltraScale+ 时序约束计算方法是相同的。不同点在于,首先约束了从 AXI Quad SPI 到 STARTUPE2 的路由时延,从 0.1ns 到 1.5ns,然后又从 USRCCLKO 创建了一个分频 + 延迟 cclk_delay 纳秒的时钟,作为 SPI Flash 上 SCK 引脚的时钟。

# this is to ensure min routing delay from SCK generation to STARTUP input
# User should change this value based on the results
# having more delay on this net reduces the Fmax
set_max_delay 1.5 -from [get_pins -hier *SCK_O_reg_reg/C] -to [get_pins -hier
*USRCCLKO] -datapath_only
set_min_delay 0.1 -from [get_pins -hier *SCK_O_reg_reg/C] -to [get_pins -hier
*USRCCLKO]
# Following command creates a divide by 2 clock
# It also takes into account the delay added by STARTUP block to route the CCLK
create_generated_clock -name clk_sck -source [get_pins -hierarchical
*axi_quad_spi_1/ext_spi_clk] [get_pins -hierarchical *USRCCLKO] -edges {3 5 7}
-edge_shift [list $cclk_delay $cclk_delay $cclk_delay]
# Data is captured into FPGA on the second rising edge of ext_spi_clk after the SCK
falling edge

# Data is driven by the FPGA on every alternate rising_edge of ext_spi_clk
set_input_delay -clock clk_sck -max [expr $tco_max + $tdata_trace_delay_max +
$tclk_trace_delay_max] [get_ports IO*_IO] -clock_fall;
set_input_delay -clock clk_sck -min [expr $tco_min + $tdata_trace_delay_min +
$tclk_trace_delay_min] [get_ports IO*_IO] -clock_fall;
set_multicycle_path 2 -setup -from clk_sck -to [get_clocks -of_objects [get_pins
-hierarchical */ext_spi_clk]]
set_multicycle_path 1 -hold -end -from clk_sck -to [get_clocks -of_objects [get_pins
-hierarchical */ext_spi_clk]]
# Data is captured into SPI on the following rising edge of SCK
# Data is driven by the IP on alternate rising_edge of the ext_spi_clk
set_output_delay -clock clk_sck -max [expr $tsu + $tdata_trace_delay_max -
$tclk_trace_delay_min] [get_ports IO*_IO];
set_output_delay -clock clk_sck -min [expr $tdata_trace_delay_min - $th -
$tclk_trace_delay_max] [get_ports IO*_IO];
set_multicycle_path 2 -setup -start -from [get_clocks -of_objects [get_pins
-hierarchical */ext_spi_clk]] -to clk_sck
set_multicycle_path 1 -hold -from [get_clocks -of_objects [get_pins -hierarchical */
ext_spi_clk]] -to clk_sck

一个 Artix 7 上配置 STARTUP SPI Flash 的例子 io_timings.xdc 可供参考。

研究 k8s 网络工作原理

背景

用 k8s 也有一段时间了,之前遇到过 iptables 等出现问题,导致 k8s 节点间网络出现问题,于是想研究一下 k8s 的网络工作原理。

Docker 网络

首先研究一下 Docker 网络连接是如何实现的。Docker 首先会创建一个 bridge,名为 bridge0:

$ ip a show docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:c4:87:73:bf brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:c4ff:fe87:73bf/64 scope link
       valid_lft forever preferred_lft forever

默认情况下,每个容器都会有单独的一个 netns,然后创建一对 veth pair,一端留在 global netns,另一端放到容器中。在 global netns 中的 veth 端口会加入到 docker0 中:

$ ip a show dev veth3db9316
21: veth3db9316@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether e2:49:a6:2d:5a:bd brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::e049:a6ff:fe2d:5abd/64 scope link
       valid_lft forever preferred_lft forever
$ brctl show docker0
bridge name     bridge id               STP enabled     interfaces
docker0         8000.0242c48773bf       no              veth3db9316

容器中的网络,在 veth 上 docker 会分配并配置一个地址(比如 172.17.0.2),然后设置默认路由 via 172.17.0.1。一方面,可以通过默认路由到 172.17.0.1 再通过 iptables NAT 访问外面的网络:

$ iptables-save -t nat
# Generated by xtables-save v1.8.2 on Sat Sep 18 10:44:49 2021
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Sat Sep 18 10:44:49 2021

另一方面,因为连接不同容器的 veth 在同一个 bridge 下面,所以不同容器的可以认为在同一个二层网络中,自然可以互相访问。

K8s 网络

在 k8s 中,所有的 pod 都希望可以通过 IP 地址互联。一个思路是把各个节点上的 pod 通过类似 docker 的方法实现,即每个 netns 通过 veth 连接到一个 bridge 上,然后再想办法去路由在其它节点上的 pod。

因为我用 k3s 搭建 k8s 集群,它用的 cni 是 flannel。flannel 采用的是 vxlan 的方式来实现节点间的网络通信。

首先还是看看节点内的 pod 如何组网。

5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
    link/ether 6a:4f:ff:8b:b1:b3 brd ff:ff:ff:ff:ff:ff
    inet 10.42.0.1/24 brd 10.42.0.255 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::7cf6:57ff:fed7:c49b/64 scope link
       valid_lft forever preferred_lft forever
6: vethc47d6140@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
    link/ether da:19:f8:48:f6:49 brd ff:ff:ff:ff:ff:ff link-netns cni-9d2a5120-16a3-453e-bf64-c4006c06c93b
    inet6 fe80::d819:f8ff:fe48:f649/64 scope link
       valid_lft forever preferred_lft forever

首先,flannel 给每个节点分配了一个 /24 的网段,比如第一个节点是 10.42.0.0/24,第二个是 10.42.1.0/24,依次类推。然后,节点内的 pod 就从这个网段里分配地址,比如 10.42.0.50/24,它的默认网关是 10.42.0.1。这些 veth 都会加入到 cni0 的 bridge 中。这一部分原理和 docker 是一样的,只不过名字不同了。也有相应的 iptables 规则:

$ iptables-save | grep MASQUERADE
-A POSTROUTING -s 10.42.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE --random-fully
-A POSTROUTING ! -s 10.42.0.0/16 -d 10.42.0.0/16 -j MASQUERADE --random-fully

那么,节点间网络如何实现呢?假如,我们要从第一个节点 pod 10.42.0.50/24 访问第二个节点的 pod 10.42.1.51/24,首先,pod 根据默认路由会发给 10.42.0.1/24,到达第一个节点的 cni0,然后查路由表:

$ ip r
10.42.0.0/24 dev cni0 proto kernel scope link src 10.42.0.1
10.42.1.0/24 via 10.42.1.0 dev flannel.1 onlink

可以看到,它会匹配 10.42.1.0/24 via 10.42.1.0 dev flannel.1 的路由。flannel.1 是一个 vxlan 的 interface:

$ ip a show flannel.1
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether b6:2f:39:4a:02:c0 brd ff:ff:ff:ff:ff:ff
    inet 10.42.0.0/32 scope global flannel.1
       valid_lft forever preferred_lft forever
    inet6 fe80::b42f:39ff:fe4a:2c0/64 scope link
       valid_lft forever preferred_lft forever

当这个 interface 接收到一个 packet 的时候,会查询 fdb:

$ bridge fdb show brport flannel.1
...

这个 fdb 中包括了 (MAC 地址,IP 地址) 的 tuple。当 flannel.1 收到一个 Ethernet Frame 的时候,如果目的地址匹配这里的 MAC 地址,就会直接把 Eth Frame 封装到 UDP 里面发给目的 IP 地址;否则,就会在这个表里面 broadcast。这样,第二个节点就会收到 packet 并且转给实际的 pod。

总结

总结一下 k8s 的网络互联的实现方法:节点内通过 bridge 实现,把链接各个 netns 的 veth 桥接起来;节点间划分为多个子网,子网间通过 flannel 的网关进行路由,flannel 网关间通过 vxlan 进行互联。

参考文档

技术干货 | 深入理解 flannel

一文看懂 k8s 的 Flannel 网络

浅谈乱序执行 CPU(一:乱序)

本文的内容已经整合到知识库中。

背景

最早学习乱序执行 CPU 的时候,是在 Wikipedia 上自学的,后来在计算机系统结构课上又学了一遍,但发现学的和现在实际采用的乱序执行 CPU 又有很大区别,后来又仔细研究了一下,觉得理解更多了,就想总结一下。

本文主要讨论乱序执行的部分。

本系列的所有文章:

Locale 影响排序的特殊副作用

背景

最近在答疑的时候,发现同一条命令在不同系统上行为不同,一开始以为是 bash 版本问题,排查后最后发现是 locale 的问题。一个例子如下:

$ cat poc.txt | tr '\\n' ' '
1 + - * / \ a b A B     α
$ LANG="" sort poc.txt | tr '\\n' ' '
* + - / 1 A B \ a b α    $ LANG="zh_CN.UTF-8" sort poc.txt | tr '\\n' ' '
* + - / \ 1     a A b B α
$ LANG="en_US.UTF-8" sort poc.txt | tr '\\n' ' '
* + - / \ 1 a A b B α    

注意 1 a A 的顺序,在不同的 locale 下结果不同。

网上也有关于这个问题的讨论:

  1. https://unix.stackexchange.com/questions/75341/specify-the-sort-order-with-lc-collate-so-lowercase-is-before-uppercase
  2. https://stackoverflow.com/questions/43448655/weird-behavior-of-bash-glob-regex-ranges

一次从 RHEL 6 到 CentOS 7 的更新

背景

有一台 RHEL 6 的服务器,各种软件版本太老了,用起来很难受,因此想升级。一开始想升级到 RHEL 7,但是发现必须要从 RedHat 下载 ISO,比较慢,所以我就先切换到 CentOS 6,再升级到 CentOS 7

过程

RHEL 6 Pre upgrade

一开始还是打算升级到 RHEL 7,所以跟随 RedHat 的文档去做 pre upgrade check,发现有一步要跑好久,网上搜了一下,发现这个步骤会扫描已有的各种程序,检查升级以后会不会出现不能运行的问题。但是如果有很多小文件,这一个过程就会进行很久,好在可以设置 exclusion 目录。最后检查出来的结果就是 GNOME 没法升级,建议卸载。

倒腾了一下升级工具,发现需要离线安装,比较麻烦,我就干脆换 CentOS 了。

RHEL 6 -> CentOS 6

首先,把软件源都切换到 CentOS,这一步很简单,因为包都是一样的。只不过,因为 CentOS 6 在 centos-vault 里面,所以用起来比较麻烦。

CentOS 6 -> CentOS 7

由于 CentOS 6 到 CentOS 7 升级涉及的改动比较多,官方提供了一个升级工具。一开始,我想直接升级到 CentOS 7 最新版本,但是报错,看到网上说可以升级到 CentOS 7 的早期版本,试了一下,确实没问题。

一通升级以后,重启,进入更新过程,发现很多包都安装失败了。重启以后,因为找不到 rootfs,挂在了 dracut 的 initramfs 里面。

漫长的修复过程

简单试了一下,发现 dracut 的 initramfs 里程序太少了,调试起来很痛苦。所以,我在 BMC 里通过 Virtual Media 挂了一个 Arch Linux 的 Live CD。因为通过 Web 访问延迟太高,我设了一个 root 密码,然后直接 ssh 到 live cd 系统中。

接着,我发现,可以正常找到盘和里面的各个分区,所以怀疑是之前 initramfs 里缺了什么东西,导致找不到硬盘。我 arch-chroot 到 root 分区里,然后手动更新各个包,特别麻烦:我首先升级了 yum repos 到最新的 CentOS 7,然后手动删掉/升级 el6 的各个软件包。最后好不容易把 kernel 终于升级好了,又重新生成 grub2 的配置,因为 CentOS 6 是 grub1。这时候,重启进入系统,发现可以找到 rootfs 了,但是经过 selinux relabel 以后,仍然会遇到 systemd-logind 起不来的问题,伴随着一系列的 audit 报警。

最后,使出了暴力的解决办法:在 cmdline 中设置 selinux=0 audit=0,然后终于进入系统了。再继续删掉一些 el6 的包,然后升级各种包,最后终于是恢复了正常。

轶事一则

7.31 号周六的时候,发现 Intel Intrinsics Guide(https://software.intel.com/sites/landingpage/IntrinsicsGuide/) 出现错误,加载数据失败,于是在 Intel 的网站上提交了一个 bug。

8.2 号的时候,Intel 发邮件过来,说已经复现了问题,已经汇报给了后端团队。邮件原文:

Thank you for bringing this to our attention. We have verified and
encountered the same issue. Please know that we have escalated this
issue to our backend technical team.

We will get back to you as soon as we have an update. Have a nice day
ahead!

8.4 号的时候,Intel 再次发邮件过来,说后端团队正在处理这个问题,会尽快完成修复,请我耐心等待。这个时候我去网站上看,还是有问题。邮件原文:

Our backend team is still working on this issue. We are trying our level
best to get back to you with an update soon.

Have a nice day ahead!

8.6 号 19:27 的时候,Intel 又发了一次邮件,说后端团队依然在处理这个问题,并且正在进行一个永久性的修复(言下之意是现在提供了一个临时性的修复)。这个时候去网站上看,终于是修好了。邮件原文:

We have received an update from our backend team is that they are
working on this issue and, a more permanent fix is in the works.
Hopefully, it will resolve soon.

We appreciate your patience and understanding on this matter. Have a
nice day!

我回复了一下邮件,告诉 Intel 我这边看到已经是修复好的版本,紧接着又收到了一封邮件,告诉我可以从网站上下载离线版的 Intrinsics Guide:

Thank you for your prompt response. We are glad that your issue has been
resolved and we would like to thank you for your co operation.  Please
be informed that the offline version of the Intrinsic Guide is now
available for download from the site. The offline version of the guide
has the same content as the site, but is viewable offline by the user. A
link to the download is now added in the left column of the site:
https://software.intel.com/sites/landingpage/IntrinsicsGuide/

That said, we are closing this ticket and if you have further issues
please open another ticket and we will be happy to help you.

After case closure, you will receive a survey email. We appreciate it if
you can complete this survey regarding the support you received.  Your
feedback will help us improve our support.

For any concerns related to Intel® Developer Zone account, login or
website, please feel free to open a new ticket:
https://software.intel.com/en-us/support

这次 Intel Support 的反应挺快的,给个好评。就是希望 Intel 能够不挤牙膏,能拿出和 AMD 相当水平的 CPU。

背景

最近发现空调遥控器电池有点不足,有时候会自动关机,于是拿出以前买的 Broadlink RM mini 3 充当远程的空调遥控器使用。为了方便手机上配置,分别采用了官方的 App 智慧星和 homebridge 进行配置。

步骤

首先用官方的智慧星配置好 Broadlink RM mini 3 的网络,然后配置 homebridge-broadlink-rm-pro。最早的插件作者不怎么更新了,这个版本是目前用的比较多的一个 fork。

安装好以后,在 Home 里面可以看到 Scan Code 的开关。打开以后,用遥控器在 Broadlink RM mini 3 附近按按键,就可以在 Homebridge 日志里看到 hex code 了。然后,就按照插件教程里的方法写配置,例子如下:

{
        "platform": "BroadlinkRM",
        "name": "Broadlink RM",
        "accessories": [
        {
                "name": "Air Conditioner",
                "type": "air-conditioner",
                "noHumidity": true,
                "minTemperature": 26,
                "maxTemperature": 28,
                "defaultCoolTemperature": 27,
                "data": {
                        "off": "2600...",
                        "cool28": {
                                "data": "2600..."
                        },
                        "cool27": {
                                "data": "2600..."
                        },
                        "cool26": {
                                "data": "2600..."
                        }
                }
        }]
}

这样就可以在手机上方便地控制空调温度了。测试了一下,可以用 Siri 说“设置空调为 XX 度”,也是完全可以工作的。

P.S. 小米的空气净化器现在可以用插件 https://github.com/torifat/xiaomi-mi-air-purifier#readme,之前博客里写的那一个已经不更新了。

轶事一则

7.17 号周六的时候,一位朋友 @elenacliu 发现深研院网站的一个文档链接,文档中标题写的是 2021,但是网页的标题显示的是 2006 年。于是我发邮件给深研院的招生办,抄送本部的研招办,提交了这个 issue。

7.19 号周一上午的时候,本部的研招办回复了一封邮件,没有理解我想表达的意思,可能以为我是要报考的学生,让我关注明年发布的文档。

7.19 号周一下午的时候,深研院招生办回复了邮件,说“谢谢你的反馈”,不过没有提到是否进行了修复。我晚上再查看页面的时候,发现新的文档链接已经修复了问题。

Nginx 处理 POST 请求出现 Internal Server Error 排查一则

前言

最近一个服务忽然出现问题,用户反馈,HTTP POST 一个小的 body 不会出错,POST 一个大的 body 就会 500 Internal Server Error。

排查

观察后端日志,发现没有出错的那一个请求。观察 Nginx 日志,发现最后一次日志是几个小时前。最后几条 Nginx 日志写的是 a client request body is buffered to a temporary file

结论

继续研究后,发现是硬盘满了。Nginx 在处理 POST body 的时候,如果 body 超过阈值,会写入到临时文件中:

Syntax: client_body_buffer_size size;
Default: client_body_buffer_size 8k|16k;
Context: http, server, location
Sets buffer size for reading client request body. In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file. By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64. It is usually 16K on other 64-bit platforms.

详见 https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size

这就可以解释为什么 Nginx 返回 500 而且没有转发到后端,也可以解释为什么 Nginx 没有输出新的错误日志。

将 k8s rook ceph 集群迁移到 cephadm

背景

前段时间用 rook 搭建了一个 k8s 内部的 ceph 集群,但是使用过程中遇到了一些稳定性问题,所以想要用 cephadm 重建一个 ceph 集群。

重建过程

重建的时候,我首先用 cephadm 搭建了一个 ceph 集群,再把原来的 MON 数据导入,再恢复各个 OSD。理论上,可能有更优雅的办法,但我还是慢慢通过比较复杂的办法解决了。

cephadm 搭建 ceph 集群

首先,配置 TUNA 源,在各个节点上安装 docker-cecephadm。接着,在主节点上 bootstrap:

cephadm bootstrap --mon-ip HOST1_IP

此时,在主节点上会运行最基础的 ceph 集群,不过此时还没有任何数据。寻找 ceph 分区,会发现因为 FSID 不匹配而无法导入。所以,首先要恢复 MON 数据。

参考文档:cephadm install

恢复 MON 数据

首先,关掉 rook ceph 集群,找到留存下来的 MON 数据目录,默认路径是 /var/lib/rook 下的 mon-[a-z] 目录,找到最新的一个即可。我把目录下的路径覆盖到 cephadm 生成的 MON 目录下,然后跑起来,发现有几个问题:

  1. cephadm 生成的 /etc/ceph/ceph.client.admin.keyring 与 MON 中保存的 auth 信息不匹配,导致无法访问
  2. FSID 不一致,而 cephadm 会将各个设置目录放到 /var/lib/ceph/$FSID

第一个问题的解决办法就是临时用 MON 目录下的 keyring 进行认证,再创建一个新的 client.admin 认证。第二个问题的解决办法就是将遇到的各种 cephadm 生成的 FSID 替换为 MON 中的 FSID,包括目录名、各个目录下 unit.run 中的路径和 systemd unit 的名称。

进行一系列替换以后,原来的 MON 已经起来了,可以看到原来保留的各个 pool 和 cephfs 信息。

扩展到多节点

接下来,由于 MON 中保存的数据更新了,所以要重新生成 cephadm 的 SSH 密钥。将 SSH 密钥复制到各节点后,再用 cephadm 的 orch 功能部署到其他节点上。此时 FSID 都已经是 MON 中的 FSID,不需要替换。此时可以在 ceph orch ps 命令中看到在各个节点上运行的程序。接下来,还需要恢复各个 OSD。

导入 OSD

为了从 ceph 分区从导出 OSD 的配置文件,需要用 ceph-volume 工具。这个工具会生成一个 /var/lib/ceph/osd-ID 目录,在 cephadm 的概念里属于 legacy,因此我们首先要把路径 mount 到 shell 里面:

$ cephadm shell --mount /var/lib/ceph:/var/lib/ceph

接着,生成 osd 目录配置:

$ ceph-volume lvm activate --all --no-systemd

然后,可以看到创建了对应的 osd 路径,再用 cephadm 进行转换:

$ cephadm adopt --style legacy --name osd.ID

这样就可以用 cephadm 管理了。

配置 k8s

配置好外部 ceph 集群后,还需要配置 k8s rook。

参考 https://rook.github.io/docs/rook/v1.8/ceph-cluster-crd.html#external-cluster,大概有这么几步:

  1. 在 ceph 集群上运行 create-external-cluster-resources.sh,创建用户,并且导出 key
  2. 在 k8s 集群上应用第一步生成的环境变量,然后运行 import-external-cluster.sh
  3. 复制一份 cluster-external.yaml 然后应用
  4. 复制 storageclass.yaml,把里面的 namespace 改成 rook-ceph-external

C++ 11 的 ABI 问题

背景

有同学遇到这样的一个问题,代码中链接了一个第三方的动态库,在链接的时候出现了不一致的问题,比如有一个函数签名如下:

void foobar(std::string s) {}

使用 GCC 11.1.0 编译上面的代码,可以发现它需要的符号是 _Z6foobarNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE,但是第三方库里面却是 _Z6foobarSs,因此找不到对应的符号,链接失败。

问题

经过一番研究,发现 SsItanium ABI 中表示的是缩写:

In addition, the following catalog of abbreviations of the form "Sx" are used:


   <substitution> ::= St # ::std::
   <substitution> ::= Sa # ::std::allocator
   <substitution> ::= Sb # ::std::basic_string
   <substitution> ::= Ss # ::std::basic_string < char,
                         ::std::char_traits<char>,
                         ::std::allocator<char> >
   <substitution> ::= Si # ::std::basic_istream<char,  std::char_traits<char> >
   <substitution> ::= So # ::std::basic_ostream<char,  std::char_traits<char> >
   <substitution> ::= Sd # ::std::basic_iostream<char, std::char_traits<char> >

这看起来很正常,_Z6foobarSs 表示的是 foobar(std::basic_string<char, std::char_traits<char>, std::allocator<char> >),但是 GCC 11.1.0 编译出来的上面的代码却没有用这个符号,而是 foobar(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)。差别就在于 __cxx11 中。

经过一番搜索,找到了 GCC 关于这个问题的文档网上的文章,找到了原因:从 GCC5 开始,为了兼容 C++11 标准的改变,做了这个变动。如果要恢复原来的行为,需要添加一个定义:

$ g++ -D_GLIBCXX_USE_CXX11_ABI=0 -c test.cpp -o test.o && nm test.o | grep foobar
0000000000000000 T _Z6foobarSs
$ g++ -c test.cpp -o test.o && nm test.o | grep foobar
0000000000000000 T _Z6foobarNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
# install g++-4.9 in ubuntu 16.04
$ g++-4.9 -c test.cpp -o test.o && nm test.o | grep foobar
0000000000000000 T _Z6foobarSs

这样就可以正常链接到第三方的动态库了。

硬盘相关的概念

ATA

ATA 定义了发送给硬盘的命令,标准定义了命令:

  • ech IDENTIFY DEVICE: 获取设备信息
  • 25h READ DMA EXT: 读取扇区
  • 35h WRITE DMA EXT: 写入扇区

ATA 同时也是接口,图片如下。ATA 前身是 IDE,现在 ATA 叫做 PATA。

PATA Pin

AHCI

AHCI 可以简单理解为 PCIe <-> SATA 的转换器。AHCI 暴露为一个 PCIe 设备:

$ lspci -vv
00:1f.2 SATA controller: Intel Corporation C600/X79 series chipset 6-Port SATA AHCI Controller (rev 05)
        Kernel modules: ahci

处理器通过 IO port/MMIO 访问 AHCI,然后 AHCI HBA 连接到 SATA 设备。

SATA

SATA 一般说的是接口。它一般分为两个部分,数据和电源。数据部分只有 7 个 pin,三个 GND 和两对差分线(A+A- B+B-),图片如下:

SATA Data

电源部分有 15 个 pin,有 GND 3.3V 5V 和 12V,图片如下:

SATA Power

常见的 SATA 盘有 2.5 英寸(small form factor, SFF)和 3.5 英寸(large form factor,LFF)两种规格。

M.2

M.2 又称 NGFF,有不同的 key 类型。常见的是 B 和 M:

  • B key: 12-19 notched, PCIe x2, SATA
  • M key: 59-66 notched, PCIe x4, SATA

都有部分引脚的位置是空的:

M.2

这里可以看到两种 key 的 pinout。

  • B key: SATA pin B(41,43) A(47,49), PCIe x2 pin R1(29,31) T1(35,37) R0(41,43) T0(47,49), USB 3.0 pin TX(29, 31) RX(35,37)
  • M key: SATA 同上,PCIe x4 pin R3(5,7) T3(11,13) R2(17,19) T2(23,25) Lane 0,1 同上

可以看到,SATA pin 和 PCIe 的两个 lane 在 B 和 M key 中是一样的,物理上也是可以兼容的。

因为支持 SATA 和 PCIe,就有下面三种可能的使用方式:

  • PCIe -- AHCI HBA(Board) -- SATA(M.2) -- Disk: 传统方式,只不过物理接口从 SATA 变成了 M.2
  • PCIe -- PCIe Device(M.2) -- Disk(AHCI):硬盘实现了 AHCI 的接口,通过 PCIe 连接到 CPU
  • PCIe -- PCIe Device(M.2) -- Disk(NVMe):硬盘实现了 NVMe 的接口,通过 PCIe 连接到 CPU

SATA express

SATA express 在 SATA 3.2 引入,它用的很少,被 U.2 取代。提供了 PCIe x2 或者 SATA x2。

U.2

U.2 也叫 SFF-8639。它和 SATA express 接口一样,但提供了 PCIe x4 或者 SATA x2。详见 pinout

U.2

速度比较

不同的协议的速度如下:

  • SATA 3.0: 6Gb/s(8b/10b, 4Gb/s uncoded)
  • SAS-1: 3Gb/s
  • SAS-2: 6Gb/s
  • SAS-3: 12Gb/s
  • SAS-4: 22.5Gb/s
  • PCIe 3.0 x4: 32Gb/s(8GT/s, 128b/130b, 31.5 Gb/s uncoded)

更完整的可以看List of interface bit rates

Intel SSD DC P4618 Series 读写速度可以达到 40~50 Gb/s,它采用的是 PCIe 3.0 x8(64Gb/s) NVMe。

Intel SSD 545s Series 读写速度约 4Gb/s,采用的是 SATA 3.0 6Gb/s。

SAMSUNG 970 EVO 读写速度 20~30 Gb/s,它采用的是 PCIe 3.0 x4(32Gb/s) NVMe。

SAS

SAS 涉及的物理接口比较多,下面举一个具体的例子:DELL SCv2000

文档:https://dl.dell.com/topicspdf/storage-sc2000_owners-manual_en-us.pdf

它的背面:

它有四个前端接口 Mini-SAS High Density (HD),即 SFF-8644;两个后端接口 Mini-SAS,即 SFF-8088。

RAID 卡例子:MegaRAID SAS 9361-8i

文档:https://docs.broadcom.com/doc/12351995

它的接口有:

  1. 两个 mini-SAS SFF-8643(Mini Multilane 4/8X 12 Gb/s Unshielded Connector (HD12un)) 内部连接器,连接到硬盘
  2. PCIe 3.0 8x 连接主板

SAS 标准:

  • INCITS 417 Serial Attached SCSI 1.1 (SAS-1.1)
  • INCITS 457 Serial Attached SCSI 2 (SAS-2)
  • INCITS 478 Serial Attached SCSI 2.1 (SAS-2.1)
  • INCITS 519 Serial Attached SCSI - 3 (SAS-3)
  • INCITS 534 Serial Attached SCSI - 4 (SAS-4)

可以从 https://www.t10.org/drafts.htm#SCSI3_SAS 免费下载尚未成为标准的 SAS-4.1 Working Draft。

SAS 相关的物理接口

查找 SFF 标准:https://www.snia.org/technology-communities/sff/specifications

中文介绍:https://www.163.com/dy/article/H8TGPEUA0532B75P.html

SFF-8087

Mini Multilane 4X Unshielded Connector Shell and Plug

介绍:https://cs-electronics.com/sff-8087/

Mini SAS 4i 连接器就是 36 pin 的 SFF-8087,支持四路 SAS。i 表示用于 internal 连接。对应的 external 接口是 SFF-8088。

标准下载地址:https://members.snia.org/document/dl/25823

它的引脚定义可以在 SFF-9402 看到,它的引脚分为 A 面和 B 面,每面有 18 个 PIN,用途如下:

  • A2(Rx0+), A3(Rx0-), B2(Tx0+), B3(Tx0-):第一组差分对
  • A4(Rx1+), A5(Rx1-), B4(Tx1+), B5(Tx1-):第二组差分对
  • A13(Rx2+), A14(Rx2-), B13(Tx2+), B14(Tx2-):第三组差分对
  • A16(Rx3+), A17(Rx3-), B16(Tx3+), B17(Tx3-):第四组差分对
  • B8(Sclock), B9(Sload), A10(SDataOut), A11(SDataIn):SGPIO 协议
  • B8(2W-CLK), B9(2W-DATA):用于 SES 的 I2C 协议

这四组差分对对应四路 SAS 或者 SATA。SGPIO 协议的标准是 SFF-8485,主要用途是控制硬盘状态灯,以及判断盘是否插入。

相关标准:

  • SFF-8086: Mini Multilane 10 Gb/s 4X Common Elements Connector

SFF-8088

Mini Multilane 4X Shielded Connector Shell and Plug

标准下载地址:https://members.snia.org/document/dl/25824

Mini SAS 4x 连接器就是 26 pin 的 SFF-8088,支持四路 SAS。用于 external 连接。对应的 internal 接口是 SFF-8087。

SFF-8482/SFF-8678/SFF-8680/SFF-8681

SFF-8482: Serial Attachment 2X Unshielded Connector (EIA-966)

介绍:https://cs-electronics.com/sff-8482/

支持两路 SAS,29 个引脚。和 SATA 的接口大小一样,目的是为了可以兼容 SATA 和 SAS 盘,比较常见。

标准下载地址:https://members.snia.org/document/dl/25920

不同速率的版本:

  • SFF-8678: Serial Attachment 2X 6Gb/s Unshielded Connector
  • SFF-8680: Serial Attachment 2X 12Gb/s Unshielded Connector, 支持 SAS-2.x 和 SAS-3
  • SFF-8681: Serial Attachment 2X 24Gb/s Unshielded Connector, 支持 SAS-4

SFF-8614/8644

SFF-8614: Mini Multilane 4/8X Shielded Cage/Connector (HDsh)

标准下载地址:https://members.snia.org/document/dl/25939

对应的 internal 版本是 SFF-8643: Mini Multilane 4/8X 12 Gb/s Unshielded Connector

名称:External Mini-SAS HD(High Density)

升级版本:

SFF-8644: Mini Multilane 4/8X 12 Gb/s Shielded Cage/Connector (HD12sh)

标准下载地址:https://members.snia.org/document/dl/25952

支持 SAS-3 和 PCIe 3.0

SFF-8639

Multifunction 6X Unshielded Connector

又称 U.2

标准下载地址:https://members.snia.org/document/dl/26489

用途:

  • Single port SATA (as defined by Serial ATA revision 3.1)
  • Two port SATA Express (as defined in Serial ATA Technical Proposal #TPR_C109, currently under development)
  • Dual port SAS (as defined by SFF-8482)
  • MultiLink SAS (as defined by SFF-8630)
  • Up to 4 lanes of PCIe (as defined in this specification)

SFF-8611

MiniLink 4/8X I/O Cable Assemblies

又称 OCuLink 1.0

标准下载地址:https://members.snia.org/document/dl/27937

用途:

  • PCIe
  • SAS

硬盘性能指标

硬盘性能一般会看如下几个指标:

  1. 顺序读写性能,在指定的 block size 下,顺序读写的传输速度,单位通常是 MB/s
  2. 随机读写性能,在指定的 block size 下(一般是 4KB),随机地址读写的每秒读写操作次数,单位通常是 IOPS
  3. 延迟

CrystalDiskInfo 术语:

  • SEQ1MQ8T1:顺序读写(SEQ),一次读写 1 MB(1M),队列深度是 8(Q8),单线程(T1)
  • SEQ128KQ32T1:顺序读写(SEQ),一次读写 128 KB(128K),队列深度是 32(Q32),单线程(T1)
  • RND4KQ32T16:随机读写(RND),一次读写 4KB(4K),队列深度是 32(Q32),16 线程(T16)
  • RND4KQ1T1:随机读写(RND),一次读写 4KB(4K),队列深度是 1(Q1),单线程(T1)

根据 SSD vs HDD - Should You Buy a Solid State Drive or Hard Disk Drive?,SSD 和 HDD 在顺序读写性能上的常见范围:

  • SSD: 300 ~ 500 MB/s
  • HDD: 100 ~ 160 MB/s

SSD 和 HDD 在随机读写性能上的常见范围:

  • SSD: 20,000 ~ 100,000 IOPS
  • HDD: 75 ~ 100 IOPS

可以看到,SSD 和 HDD 相比,顺序读写性能高,随机读写性能显著高。但实际上,这个数据有些过时了。

下面看一些具体的例子:

在 ESXi 中用 PERCCli 换 RAID 中的盘

背景

最近有一台机器的盘出现了报警,需要换掉,然后重建 RAID5 阵列。iDRAC 出现报错:

  1. Disk 2 in Backplane 1 of Integrated RAID Controller 1 is not functioning correctly.
  2. Virtual Disk 1 on Integrated RAID Controller 1 has become degraded.
  3. Error occurred on Disk2 in Backplane 1 of Integrated RAID Controller 1 : (Error 2)

安装 PERCCli

首先,因为系统是 VMware ESXi 6.7,所以在DELL 官网下载对应的文件。按照里面的 README 安装 vib:

esxcli software vib install -v /vmware-perccli-007.1420.vib

如果要升级系统,需要先卸载 vib:esxcli software vib remove -n vmware-perccli,因为升级的时候会发现缺少新版系统的 perccli,建议先卸载,升级后再安装新的。

需要注意的是,如果复制上去 Linux 版本的 PERCCli,虽然也可以运行,但是找不到控制器。安装好以后,就可以运行 /opt/lsi/perccli/perccli 。接着,运行 perccli show all,可以看到类似下面的信息:

$ perccli show all
--------------------------------------------------------------------------------
EID:Slt DID State  DG     Size Intf Med SED PI SeSz Model               Sp Type
--------------------------------------------------------------------------------
32:2      2 Failed  1 3.637 TB SATA HDD N   N  512B ST4000NM0033-9ZM170 U  -
32:4      4 UGood   F 3.637 TB SATA HDD N   N  512B ST4000NM0033-9ZM170 U  -
--------------------------------------------------------------------------------

其中 E32S2 是 Failed 的盘,属于 Disk Group 1;E32S4 是新插入的盘,准备替换掉 E32S2,目前不属于任何的 Disk Group。查看一下 Disk Group:perccli /c0/dall show

$ perccli /c0/dall show
-----------------------------------------------------------------------------
DG Arr Row EID:Slot DID Type  State BT       Size PDC  PI SED DS3  FSpace TR
-----------------------------------------------------------------------------
 1 -   -   -        -   RAID5 Dgrd   N    7.276 TB dflt N  N   dflt N      N
 1 0   -   -        -   RAID5 Dgrd   N    7.276 TB dflt N  N   dflt N      N
 1 0   0   32:1     1   DRIVE Onln   N    3.637 TB dflt N  N   dflt -      N
 1 0   1   32:2     2   DRIVE Failed N    3.637 TB dflt N  N   dflt -      N
 1 0   2   32:3     3   DRIVE Onln   N    3.637 TB dflt N  N   dflt -      N

可以看到 DG1 处于 Degraded 状态,然后 E32S4 处于 Failed 状态。参考了一下 PERCCli 文档,它告诉我们要这么做:

perccli /cx[/ex]/sx set offline
perccli /cx[/ex]/sx set missing
perccli /cx /dall show
perccli /cx[/ex]/sx insert dg=a array=b row=c
perccli /cx[/ex]/sx start rebuild

具体到我们这个情景,就是把 E32S2 设为 offline,然后用 E32S4 来替换它:

perccli /c0/e32/s2 set offline
perccli /c0/e32/s2 set missing
perccli /cx /dall show
perccli /cx/e32/s4 insert dg=1 array=0 row=2
perccli /cx/e32/s4 start rebuild

完成以后的状态:

TOPOLOGY :
========

---------------------------------------------------------------------------
DG Arr Row EID:Slot DID Type  State BT     Size PDC  PI SED DS3  FSpace TR
---------------------------------------------------------------------------
 1 -   -   -        -   RAID5 Dgrd  N  7.276 TB dflt N  N   dflt N      N
 1 0   -   -        -   RAID5 Dgrd  N  7.276 TB dflt N  N   dflt N      N
 1 0   0   32:1     1   DRIVE Onln  N  3.637 TB dflt N  N   dflt -      N
 1 0   1   32:4     4   DRIVE Rbld  Y  3.637 TB dflt N  N   dflt -      N
 1 0   2   32:3     3   DRIVE Onln  N  3.637 TB dflt N  N   dflt -      N
---------------------------------------------------------------------------

可以看到 E32S4 替换了原来 E32S2 的位置,并且开始重建。查看重建进度:

$ perccli /c0/32/s4 show rebuild
-----------------------------------------------------
Drive-ID   Progress% Status      Estimated Time Left
-----------------------------------------------------
/c0/e32/s4         3 In progress -
-----------------------------------------------------
$ perccli show all
Need Attention :
==============

Controller 0 :
============

-------------------------------------------------------------------------------
EID:Slt DID State DG     Size Intf Med SED PI SeSz Model               Sp Type
-------------------------------------------------------------------------------
32:4      4 Rbld   1 3.637 TB SATA HDD N   N  512B ST4000NM0033-9ZM170 U  -
-------------------------------------------------------------------------------

然后,查看一下出错的盘:

$ perccli /c0/e32/s2 show all
Drive /c0/e32/s2 State :
======================
Shield Counter = 0
Media Error Count = 0
Other Error Count = 6
Drive Temperature =  36C (96.80 F)
Predictive Failure Count = 0
S.M.A.R.T alert flagged by drive = No

果然有错误,但是也看不到更多信息了。

坏块统计:

$ perccli /c0 show badblocks
Detailed Status :
===============

-------------------------------------------------------------
Ctrl Status Ctrl_Prop       Value ErrMsg               ErrCd
-------------------------------------------------------------
   0 Failed Bad Block Count -     BadBlockCount failed     2
-------------------------------------------------------------

经过检查以后,发现 E32S2 盘的 SMART 并没有报告什么问题,所以也没有把盘取走,而是作为 hot spare 当备用:

$ perccli /c0/e32/s2 add hotsparedrive DG=1
$ perccli /c0/d1 show
TOPOLOGY :
========

---------------------------------------------------------------------------
DG Arr Row EID:Slot DID Type  State BT     Size PDC  PI SED DS3  FSpace TR
---------------------------------------------------------------------------
 1 -   -   -        -   RAID5 Dgrd  N  7.276 TB dflt N  N   dflt N      N
 1 0   -   -        -   RAID5 Dgrd  N  7.276 TB dflt N  N   dflt N      N
 1 0   0   32:1     1   DRIVE Onln  N  3.637 TB dflt N  N   dflt -      N
 1 0   1   32:4     4   DRIVE Rbld  Y  3.637 TB dflt N  N   dflt -      N
 1 0   2   32:3     3   DRIVE Onln  N  3.637 TB dflt N  N   dflt -      N
 1 -   -   32:2     2   DRIVE DHS   -  3.637 TB -    -  -   -    -      N
---------------------------------------------------------------------------

DG=Disk Group Index|Arr=Array Index|Row=Row Index|EID=Enclosure Device ID
DID=Device ID|Type=Drive Type|Onln=Online|Rbld=Rebuild|Optl=Optimal|Dgrd=Degraded
Pdgd=Partially degraded|Offln=Offline|BT=Background Task Active
PDC=PD Cache|PI=Protection Info|SED=Self Encrypting Drive|Frgn=Foreign
DS3=Dimmer Switch 3|dflt=Default|Msng=Missing|FSpace=Free Space Present
TR=Transport Ready

这样就可以做后备盘,当别的盘坏的时候,作为备用。

相关软件下载

可以在这里这里寻找 StorCLI 版本。

StorCLI:

MegaCLI:

PercCLI:

用 fluentd 收集 k8s 中容器的日志

背景

在维护一个 k8s 集群的时候,一个很常见的需求就是把日志持久化存下来,一方面是方便日后回溯,一方面也是聚合 replicate 出来的同一个服务的日志。

在我目前的需求下,只需要把日志持久下来,还不需要做额外的分析。所以我并没有部署类似 ElasticSearch 的服务来对日志进行索引。

实现

实现主要参考官方的仓库:https://github.com/fluent/fluentd-kubernetes-daemonset。它把一些常用的插件打包到 docker 镜像中,然后提供了一些默认的设置,比如获取 k8s 日志和 pod 日志等等。为了达到我的需求,我希望:

  1. 每个结点上有一个 fluentd 收集日志,forward 到单独的 log server 上的 fluentd
  2. log server 上的 fluentd 把收到的日志保存到文件

由于 log server 不由 k8s 管理,所以按照官网的方式手动安装:

curl -fsSL https://toolbelt.treasuredata.com/sh/install-debian-bookworm-fluent-package5.sh | sh

然后,编辑配置 /etc/td-agent/td-agent.conf

<source>
  @type forward
  @id input_forward
  bind x.x.x.x
</source>

<match **>
  @type file
  path /var/log/fluentd/k8s
  compress gzip
  <buffer>
    timekey 1d
    timekey_use_utc true
    timekey_wait 10m
  </buffer>
</match>

分别设置输入:监听 fluentd forward 协议;输出:设置输出文件,和 buffer 配置。如有需要,可以加鉴权。

接着,按照 https://github.com/fluent/fluentd-kubernetes-daemonset/blob/master/fluentd-daemonset-forward.yaml,我做了一些修改,得到了下面的配置:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluentd
  namespace: kube-system
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - namespaces
  verbs:
  - get
  - list
  - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd
roleRef:
  kind: ClusterRole
  name: fluentd
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
  name: fluentd
  namespace: kube-system

---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
    version: v1
spec:
  selector:
    matchLabels:
      k8s-app: fluentd-logging
      version: v1
  template:
    metadata:
      labels:
        k8s-app: fluentd-logging
        version: v1
    spec:
      serviceAccount: fluentd
      serviceAccountName: fluentd
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:v1-debian-forward
        env:
          - name: FLUENT_FOWARD_HOST
            value: "x.x.x.x"
          - name: FLUENT_FOWARD_PORT
            value: "24224"
          - name: FLUENTD_SYSTEMD_CONF
            value: "disable"
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: config-volume
          mountPath: /fluentd/etc/tail_container_parse.conf
          subPath: tail_container_parse.conf
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: config-volume
        configMap:
          name: fluentd-config
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  namespace: kube-system
data:
  tail_container_parse.conf: |-
    <parse>
      @type cri
    </parse>

和原版有几点细节上的不同:

  1. k8s 启用了 rbac,所以需要对应的配置;照着仓库里其他带 rbac 配置的文件抄一下即可。
  2. 禁用了 SYSTEMD 日志的抓取,因为我用的是 k3s,而不是 kubeadm,自然找不到 kubelet 的 systemd service。
  3. 覆盖了 container 日志的读取,因为使用的 container runtime 日志格式和默认的不同,这部分设置在仓库的 README 中也有提到。

部署到 k8s 中即可。为了保证日志的准确性,建议各个结点都保持 NTP 的同步。

通过 ipmitool 配置 iLO 4 管理端口

ipmitool 自带了对 iDRAC 的支持,可以通过 ipmitool delloem 设置 iDRAC 的管理端口。但是对 iLO 的支持并没有实现。研究了一番,找到了通过 raw command 配置 iLO 4 管理端口的方法。

这篇文章 讲述了 ipmitool lan 命令实际会发送的命令:

读取配置:

$ ipmitool raw 0x0c 0x02 CHANNEL KEY SET BLOCK

一般来说 SET 和 BLOCK 都是 0。KEY 的常见取值:

  • 3: IP 地址
  • 4: IP 地址来源
  • 5: MAC 地址
  • 6: 子网掩码
  • 12: 默认网关

返回的数据中,第一个字节忽略,剩下的就是数据了。

写入配置:

$ ipmitool raw 0x0c 0x01 CHANNEL KEY DATA...

知道如何读取配置后,接下来就是找到 iLO 4 配置 NIC 的地方了。一番搜索,找到了 HPE iLO IPMI User Guide。在第 101 页,可以找到一个用于配置 iLO NIC 选择的设置:

Index: 224
iLO Dedicated/Shared NIC Selection.
data 3:
• Selected iLO NIC.
◦ 0h = iLO Dedicated NIC is selected.
◦ 1h = iLO Shared NIC is selected.
◦ All others = reserved
• To switch to another iLO NIC:
1. Write this (and possibly parameter 197) to the desired NIC selection
2. Configure all other relevant network parameters for the desin
3. Reset iLO. The desired NIC will be in use after iLO reset.
• When writing changes to data 3, NIC selection:
◦ data 1 must be AAh
◦ data 2 must be 55h
◦ data 4 must be FFh

有这样的信息以后,可以通过下面的命令来设置 Shared NIC:

$ ipmitool raw 0x0c 0x01 0x01 224 0xAA 0x55 0x01 0xFF

再读出来 224,发现它的 data 4 表示 iLO reset needed for some settings changes that have been made。于是,执行 ipmitool mc reset warm 之后,就可以看到 NIC 选择已经更新:

$ ipmitool raw 0x0c 0x02 0x01 197 0x00 0x00
11 02 01 02

数据分别表示:

  • 0x02: Shared NIC Selection = ALOM
  • 0x01: Shared NIC Port Number = Port 1
  • 0x02: Platform supports ALOM shared NIC

如果想要的端口和默认选择不一样,可以写入 197 来更新。详见上面的文档链接。

超微的机器也有类似的办法:https://www.supermicro.org.cn/support/faqs/faq.cfm?faq=15868,可以用 ipmiutil smcoem lanport 命令来读取/修改。

Update:我给 IPMITOOL 提交了 PR,来简化这个过程

静态编译 ipmitool

为了在 ESXi 上运行 ipmitool,需要静态编译 ipmitool。网上已经有一些解决方案:

https://github.com/ryanbarrie/ESXI-ipmitool https://github.com/hobbsh/static-ipmitool https://github.com/ewenmcneill/docker-build-static-ipmitool

我稍微修改了一下,用来编译最新 ipmitool:

#!/bin/bash
set -x
export VERSION=1.8.18
rm -rf ipmitool_$VERSION
curl -L -o ipmitool_$VERSION.tar.bz2 http://deb.debian.org/debian/pool/main/i/ipmitool/ipmitool_$VERSION.orig.tar.bz2
tar xvf ipmitool_$VERSION.tar.bz2
cd ipmitool-$VERSION
CC=gcc CFLAGS=-m64 LDFLAGS=-static ./configure
make -j24
cd src
../libtool --silent --tag=CC --mode=link gcc -m64 -fno-strict-aliasing -Wreturn-type -all-static -o ipmitool.static ipmitool.o ipmishell.o ../lib/libipmitool.la plugins/libintf.la
file $PWD/ipmitool.static

复制下来,编译完成后 scp 到 esxi 中即可使用。

ESXi 网络配置

用过 ESXi 的大家都知道,它网页版对网络的配置功能有限,特别是 IPv6 的部分,有的事情无法实现。更好的办法是 SSH 到 ESXi 上直接用命令行进行配置。

可能会用到的一些命令:

  1. esxcfg-vmknic: 用来给 vmkernel 配置地址
  2. esxcfg-route: 设置系统路由表
  3. esxcli: 大杂烩,很多功能都在里面
  4. tcpdump-uw:魔改版 tcpdump

一些例子:

设置 IPv6 默认路由:

[root@esxi:~]esxcfg-route -f V6 -a default $IPV6

删除 vmkernel 的 IPv6 地址:

[root@esxi:~]esxcli network ip interface ipv6 address remove -i $VMKERNEL -I $IPV6/$PREFIX

参考:https://kb.vmware.com/s/article/1002662

Linksys E8450 OpenWRT 配置 w/ 802.11ax

背景

之前用的 newifi 路由器(Lenovo y1s)无线网总是出问题,于是换了一个新的支持 802.11ax 的路由器 Linksys E8450,目前在 openwrt snapshot 支持。Openwrt 的支持页面:Linksys E8450

过程

按照支持页面,下载固件:

$ wget https://downloads.openwrt.org/snapshots/targets/mediatek/mt7622/openwrt-mediatek-mt7622-linksys_e8450-squashfs-sysupgrade.bin

更新(2023-02-27):固件已经从 snapshot 进入正式版,下载链接为 https://downloads.openwrt.org/releases/22.03.3/targets/mediatek/mt7622/openwrt-22.03.3-mediatek-mt7622-linksys_e8450-squashfs-sysupgrade.bin。如果已经替换为 UBI,则使用 https://downloads.openwrt.org/releases/22.03.3/targets/mediatek/mt7622/openwrt-22.03.3-mediatek-mt7622-linksys_e8450-ubi-squashfs-sysupgrade.itb 固件。

然后访问固件升级页面:http://192.168.1.1/config-admin-firmware.html#firmware,选择下载的 bin 文件。点击“开始升级”,然后等待。一段时间后,ssh 到路由器:

$ ssh root@192.168.1.1
The authenticity of host '192.168.1.1 (192.168.1.1)' can't be established.
ED25519 key fingerprint is SHA256:REDACTED.
No matching host key fingerprint found in DNS.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.1' (ED25519) to the list of known hosts.


BusyBox v1.33.0 () built-in shell (ash)

  _______                     ________        __
 |       |.-----.-----.-----.|  |  |  |.----.|  |_
 |   -   ||  _  |  -__|     ||  |  |  ||   _||   _|
 |_______||   __|_____|__|__||________||__|  |____|
          |__| W I R E L E S S   F R E E D O M
 -----------------------------------------------------
 OpenWrt SNAPSHOT, r16242-41af8735d4
 -----------------------------------------------------
=== WARNING! =====================================
There is no root password defined on this device!
Use the "passwd" command to set up a new password
in order to prevent unauthorized SSH logins.
--------------------------------------------------
root@OpenWrt:~# uname -a
Linux OpenWrt 5.10.23 #0 SMP Wed Mar 17 19:55:38 2021 aarch64 GNU/Linux

配置 luci:

$ opkg update
$ opkg install luci

然后就可以网页访问看到 luci 了:Powered by LuCI Master (git-21.060.51374-cd06e70) / OpenWrt SNAPSHOT r16242-41af8735d4。

由于目前 luci 不支持 802.11ax 的配置,可以直接修改 uci 配置来达到效果:

root@OpenWrt:/# uci show wireless
root@OpenWrt:/# uci set wireless.radio1.htmode='HE80'
root@OpenWrt:/# /etc/init.d/network restart
'radio0' is disabled

注:实际上设置为 HE 开头的字符串即可,见 mac80211.sh

再连接上 Wi-Fi 的时候就可以看到是 802.11ax 模式了。也在 OpenWRT 论坛 上分享了一下这个方案。

更新(2021-07-31):目前最新的 luci 版本已经可以在网页上配置 802.11ax 模式了。

用 gitlab ci 构建并部署应用到 k8s

背景

在 k8s 集群中部署了 gitlab-runner,并且希望在 gitlab ci 构建完成后,把新的 docker image push 到 private repo,然后更新应用。

参考文档:Gitlab CI 与 Kubernetes 的结合Using Docker to build Docker images

在 gitlab ci 中构建 docker 镜像

这一步需要 DinD 来实现在容器中构建容器。为了达到这个目的,首先要在 gitlab-runner 的配置中添加一个 volume 来共享 DinD 的证书路径:

gitlabUrl: REDACTED
rbac:
  create: true
runnerRegistrationToken: REDACTED
runners:
  config: |
    [[runners]]
      [runners.kubernetes]
        image = "ubuntu:20.04"
        privileged = true
      [[runners.kubernetes.volumes.empty_dir]]
        name = "docker-certs"
        mount_path = "/certs/client"
        medium = "Memory"
  privileged: true

注意两点:1. privileged 2. 多出来的 volume

用 helm 部署 gitlab runner 之后,按照下面的方式配置 gitlab-ci:

image: docker:19.03.12

variables:
  DOCKER_HOST: tcp://docker:2376
  #
  # The 'docker' hostname is the alias of the service container as described at
  # https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services.
  # If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
  # the variable must be set to tcp://localhost:2376 because of how the
  # Kubernetes executor connects services to the job container
  # DOCKER_HOST: tcp://localhost:2376
  #
  # Specify to Docker where to create the certificates, Docker will
  # create them automatically on boot, and will create
  # `/certs/client` that will be shared between the service and job
  # container, thanks to volume mount from config.toml
  DOCKER_TLS_CERTDIR: "/certs"
  # These are usually specified by the entrypoint, however the
  # Kubernetes executor doesn't run entrypoints
  # https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4125
  DOCKER_TLS_VERIFY: 1
  DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
  DOCKER_DAEMON_OPTIONS: "--insecure-registry=${REGISTRY}"

services:
  - name: docker:19.03.12-dind
    entrypoint: ["sh", "-c", "dockerd-entrypoint.sh $DOCKER_DAEMON_OPTIONS"]

before_script:
  # Wait until client certs are generated
  # https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27384
  - until docker info; do sleep 1; done
  - echo "$REGISTRY_PASS" | docker login $REGISTRY --username $REGISTRY_USER --password-stdin

build:
  stage: build
  script: ./build.sh

这里有很多细节,包括 DinD 的访问方式,等待 client cert,设置 docker 的 insecure registry 和 login 等等。经过 @CircuitCoder 的不断摸索,终于写出了可以用的配置。

如此配置以后,就可以在 gitlab ci 的构建脚本里用 docker 来 build 并且 push 到自己的 registry 了。为了防止泄露密钥,建议把这些变量放到 gitlab ci 设置的 secrets 中。

自动部署到 k8s

为了让 k8s 重启一个 deployment,一般的做法是:

kubectl -n NAMESPACE rollout restart deployment/NAME

我们希望 gitlab ci 在 build 之后,去执行这一个命令,但又不希望提供太多的权限给 gitlab。所以,我们创建 Service Account 并设置最小权限:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitlab
  namespace: default

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: gitlab-test
  namespace: test
rules:
- verbs:
    - get
    - patch
  apiGroups:
    - 'apps'
  resources:
    - 'deployments'
  resourceNames:
    - 'test-deployment'

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: gitlab
  namespace: test
subjects:
  - kind: ServiceAccount
    name: gitlab
    namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: gitlab-test

要特别注意这几个配置的 namespace 的对应关系:Role 和 RoleBinding 需要放在同一个 ns 下。

接着,到 GitLab 的 Operations->Kubernetes 创建 cluster,把 service account 的 token 和 ca.crt 从 secret 里找到并贴到网页上。GitLab 会按照 Environment scope 匹配到 environment,如果某个 stage 的 environment 匹配上了,就会把 kube credentials 配置好。修改 gitlab-ci.yml:

deploy:
  stage: deploy
  image: bitnami/kubectl:1.20
  environment:
    name: production
  only:
    - master
  script:
    - kubectl -n test rollout restart deployment/test

这样就完成配置了。

Gnome 的 Fractional Scaling

背景

最近发现部分软件(包括 Google Chrome,Firefox 和 Visual Studio Code)在 125% 的 Fractional Scaling 模式下会很卡。找到了一些临时解决方法,但是很不优雅,也很麻烦。所以深入研究了一下 Fractional Scaling 的工作方式。

临时解决方法

根据关键字,找到了 Chrome menus too slow after enabling fractional scaling in Ubuntu 20.04。按它的方法,关闭 Google Chrome 的硬件加速,发现卡顿问题确实解决了。

类似地,也可以关闭 VSCode 的硬件加速,在 Firefox 里也可以找到相应的设置。这样操作确实可以解决问题。但是,对于每一个出问题的应用都这样搞一遍,还是挺麻烦的。

另一个思路是,不使用 Fractional Scaling,而只是把字体变大。但毕竟和我们想要的效果不大一样。

一些发现

在物理机进行了一些实验以后,发现一个现象:125% 的时候卡顿,而其他比例(100%,150%,175%,200%)都不卡顿。

网上一顿搜到,找到了 xrandr 工具。下面是观察到的一些现象(GNOME 设置分辨率一直是 1920x1080):

放缩比例 xrandr 显示的分辨率 xrandr 显示的 transform
100% 1920x1080 diag(1.0, 1.0, 1.0)
125% 3072x1728 diag(1.6, 1.6, 1.0)
150% 2560x1440 diag(1.33, 1.33, 1.0)
175% 2208x1242 diag(1.15, 1.15, 1.0)
200% 1920x1080 diag(1.0, 1.0, 1.0)

xrandr 文档 中,写了:transform 是一个 3x3 矩阵,矩阵乘以输出的点的坐标得到图形缓存里面的坐标。

由此可以猜想:fractional scaling 的工作方式是,把绘制的 buffer 调大,然后再用 transform 把最终输出分辨率调成 1920x1080。可以看到,xrandr 显示的分辨率除以 transform 对应的值,就是 1920x1080。但这并不能解释 100% 和 200% 的区别,所以肯定还漏了什么信息。

翻了翻 mutter 实现 fractional scaling 的 pr,找到了实现 scale 的一部分:

if (clutter_actor_get_resource_scale (priv->actor, &resource_scale) &&
    resource_scale != 1.0f)
  {
    float paint_scale = 1.0f / resource_scale;
    cogl_matrix_scale (&modelview, paint_scale, paint_scale, 1);
  }

然后找到了一段对 scale 做 ceiling 的代码

if (_clutter_actor_get_real_resource_scale (priv->actor, &resource_scale))
  {
    ceiled_resource_scale = ceilf (resource_scale);
    stage_width *= ceiled_resource_scale;
    stage_height *= ceiled_resource_scale;
  }

这样,100% 和其他比例就区分开了。

另外,也在代码 中发现:

#define SCALE_FACTORS_PER_INTEGER 4
#define SCALE_FACTORS_STEPS (1.0 / (float) SCALE_FACTORS_PER_INTEGER)
#define MINIMUM_SCALE_FACTOR 1.0f
#define MAXIMUM_SCALE_FACTOR 4.0f

这段代码规定了比例只能是 25% 的倍数。

我也试了一下用 xrandr --scale 1.5x1.5:效果就是窗口看起来都更小了,分辨率变成了 2880x1620,transform 是 diag(1.5, 1.5, 1.0)。

虚拟机测试

接着,用虚拟机做了一些测试。为了在 GNOME over Wayland 上使用 fractional scaling,需要运行:

$ gsettings set org.gnome.mutter experimental-features "['scale-monitor-framebuffer']"

接着又做了类似上面的测试(GNOME 设置分辨率一直是 2560x1600):

放缩比例 xrandr 显示的分辨率
100% 2560x1600
125% 2048x1280
150% 1704x1065
175% 1464x915
200% 1280x800

在这个测试中,xrandr 显示的 transform 一直都是单位矩阵;还用了来自 xyproto/wallutilswayinfo 命令查看输出的分辨率,一直是 2560x1600,DPI 一直是 96。用 wallutils 的 xinfo 看到的结果和 xrandr 一致(通过 XWayland)。但是和物理机有一点不同:物理机有一个选项问要不要打开 fractional scaling,下面还会提示性能下降的问题;但是虚拟机上并没有这个提示,而是直接给了一些 Scale 比例的选项。

尝试了一下,在 GNOME over X11 上是找不到 fractional scaling 的(没有出现设置 scale 的选项)。找到一个实现这个功能的 fork:https://github.com/puxplaying/mutter-x11-scaling,不过没有尝试过。

我也尝试在虚拟机中用 xrandr --scale,结果就是输出黑屏,需要重启 gdm 来恢复到登录界面。

更新:由于物理机使用的是 Ubuntu,想到是不是 Ubuntu 采用了上面那个 fork 的 patch,然后就在 changelog 中看到:

mutter (3.38.1-1ubuntu1) groovy; urgency=medium

  * Merge with debian, including new upstream version, remaining changes:
    - debian/gbp.conf: update upstream branch to point to ubuntu/master
    - debian/patches/x11-Add-support-for-fractional-scaling-using-Randr.patch:
      + X11: Add support for fractional scaling using Randr
  * d/p/clutter-backend-x11-Don-t-set-the-font-dpi-computed-on-X1.patch:
    - Dropped, applied upstream

也找到了对应的 patch 文件。这也就解释了,为什么网上会说 GNOME over X11 支持 fractional scaling,并且需要用 gsettings 打开,而我在 Debian 和 Arch Linux 上设置这个选项也没有用了。原来是 Ubuntu 加的私货啊。

在 patch 中,找到了这么一段配置的解释:

+    <key name="fractional-scale-mode" enum="org.gnome.mutter.X11.scale-mode">
+      <default>"scale-ui-down"</default>
+      <description>
+        Choose the scaling mode to be used under X11 via Randr extension.
+
+        Supported methods are:
+
+        • “scale-up”     — Scale everything up to the requested scale, shrinking
+                           the UI. The applications will look blurry when scaling
+                           at higher values and the resolution will be lowered.
+        • “scale-ui-down — Scale up the UI toolkits to the closest integer
+                           scaling value upwards, while scale down the display
+                           to match the requested scaling level.
+                           It increases the resolution of the logical display.
+      </description>
+    </key>

这样就可以解释前面看到的现象了:默认是 scale-ui-down,也就是先放大到两倍(closest integer scaling value upwards),再缩小(scale down the display to match the requested scaling level)。

通过 rook 在 k8s 上部署 ceph 集群

背景

为了方便集群的使用,想在 k8s 集群里部署一个 ceph 集群。

Ceph 介绍

Ceph 有这些组成部分:

  1. mon:monitor
  2. mgr:manager
  3. osd:storage
  4. mds(optional):用于 CephFS
  5. radosgw(optional:用于 Ceph Object Storage

配置

我们采用的是 rook 来部署 ceph 集群。

参考文档:https://rook.github.io/docs/rook/v1.5/ceph-examples.html

首先,克隆 rook 的仓库。建议选择一个 release 版本。

接着,运行下面的命令:

sudo apt install -y lvm2
# required
kubectl apply -f rook/cluster/examples/kubernetes/ceph/crds.yaml
kubectl apply -f rook/cluster/examples/kubernetes/ceph/common.yaml
kubectl apply -f rook/cluster/examples/kubernetes/ceph/operator.yaml
# debugging only
kubectl apply -f rook/cluster/examples/kubernetes/ceph/toolbox.yaml
kubectl apply -f rook/cluster/examples/kubernetes/ceph/direct-mount.yaml
# CephFS
kubectl apply -f rook/cluster/examples/kubernetes/ceph/filesystem.yaml
kubectl apply -f rook/cluster/examples/kubernetes/ceph/csi/cephfs/storageclass.yaml

前面三个 yaml 是必须的,toolbox 是用来查看 ceph 状态的,direct mount 是用来 mount cephfs 的,后两个是为了用 cephfs 的。

接着,按照自己的需求编辑 rook/cluster/exmaples/kuberenetes/ceph/cluster.yaml 然后应用。此时你的集群应该就已经起来了。

然后,可以进 toolbox 查看 ceph 状态

$ kubectl -n rook-ceph exec -it deploy/rook-ceph-tools -- bash

也可以进 direct-mount 容器查看 pv 路径

# get volume path of pvc
kubectl get pv -o custom-columns=NAME:.metadata.name,NAMSEPACE:.spec.claimRef.namespace,CLAIM:.spec.claimRef.name,PATH:.spec.csi.volumeAttributes.subvolumeName

kubectl -n rook-ceph exec -it deploy/rook-direct-mount -- bash
# in the pod
mkdir /tmp/registry
mon_endpoints=$(grep mon_host /etc/ceph/ceph.conf | awk '{print $3}')
my_secret=$(grep key /etc/ceph/keyring | awk '{print $3}')
mount -t ceph -o mds_namespace=myfs,name=admin,secret=$my_secret $mon_endpoints:/ /tmp/registry
df -h

cd /tmp/registry/volumes/csi/PATH

用 k3s 部署 k8s

背景

最近需要部署一个 k8s 集群,觉得之前配置 kubeadm 太繁琐了,想要找一个简单的。比较了一下 k0s 和 k3s,最后选择了 k3s。

配置

k3s 的好处就是配置十分简单:https://rancher.com/docs/k3s/latest/en/quick-start/。不需要装 docker,也不需要装 kubeadm。

  1. 在第一个 node 上跑:curl -sfL https://get.k3s.io | sh -
  2. 在第一个 node 上获取 token:cat /var/lib/rancher/k3s/server/node-token
  3. 在其他 node 上跑:curl -sfL https://get.k3s.io | K3S_URL=https://myserver:6443 K3S_TOKEN=mynodetoken sh -

然后就搞定了。从第一个 node 的 /etc/rancher/k3s/k3s.yaml获取 kubectl 配置。