嘿,这篇文章继续分享适用于家里的安静万兆网络使用经验,希望能给有相似需求的朋友一些参考。

写在前面

在前面两篇文章《家用网络升级实践:低成本实现局部万兆(一)》、《家用万兆网络实践:紧凑型家用服务器静音化改造(二)》中,我们已经讨论了:

  • 升级网络接入能力: 选用经济实惠的同时具备低功耗光口和 2.5G 网口的交换机作为中继,提升网络接入容量
  • 设备增加万兆网卡: 为家里有 PCIe 插槽的 Gen10 和 Gen10 Plus 等老设备加装万兆光口网卡
  • 设备静音化改造: 通过定制风扇管理程序和更换静音消费级风扇,解决服务器噪音问题

这一篇,我们继续来折腾一下剩余设备的改造,目标是在保证高性能网络传输的同时,让整套系统运行更加安静。

Gen10 静音化改造

上一篇文章里,我们介绍了如何自制数据线、用开发板来定制服务器风扇控制程序,把服务器的原装风扇换成更安静的民用风扇。

静音化改造的主角:Gen10

因为我的机柜就放在客厅角落里,离我的办公桌还不到 1 米远,所以为了获得更好的使用体验,我也给第一篇文章提到的 Gen10 做了静音改造。效果相当不错,今天就把这部分经验也分享出来。

之前没有在第一篇文章里分享这个改造主要有两个原因:

  1. 当时改造需要的配件还在路上,没法实操
  2. 比起 Gen10,Gen10 Plus 插上 PCIe 扩展网卡后风扇转得更“欢快”些

那你可能会问,为什么 Gen10 会比 Gen10 Plus 更安静呢?

这是因为 Gen10 这个小方块和 Gen10 Plus 的设计理念不太一样。Gen10 主要依靠被动散热,用的是功耗较低的 AMD Opteron X3216、X3418 处理器。而 Plus 版本用的是功耗相对更高的 Intel Xeon E 系列,需要更强的散热能力。另外,Gen10 那个方方正正的机箱比 Plus 版大,内部散热空间也更充裕。

如果你对这两台设备感兴趣,可以阅读《省心和颜值兼顾的 HomeLab 设备:HPE MicroServer Gen10 Plus v2(一)》和《低成本和颜值兼顾的 HomeLab 设备:HPE MicroServer Gen10(二)》。

更换风扇后的主机运行状态(BIOS)

改造后的设备运行基础状况如图所示,可以看到整体效果还是蛮不错的。

改造材料准备

和前一篇改造教程类似,Gen10 的静音化改造主要需要三样核心材料:静音风扇、 Arduino 开发板和数据线。

Gen10 原装配备的是台达 AFB1212SH 风扇。基于之前的改造经验,这次我选择了“自带改装线材”的猫扇 NF-A12x15。当然,你也可以选择NF-A12x25,不过价格会贵一些,个人觉得没有这个必要。

Gen10 计划换上的新风扇:NF-A12x15

和原装风扇相比,新风扇的体型要纤薄不少。

两个风扇的参数规格对比

关于两款风扇的具体参数对比,我们先按下不表,这些数据会在后面编写风扇控制程序时用到。

制作风扇数据线

在制作数据线方面,由于已经有了上次的经验,这次的操作更加得心应手。将猫扇带的 1 转 2 线做掐头处理,定制的兼容 HP 主板的数据线的 GND Loop 线直接甩出来就好,不必都挤在一根热缩管里,这样处理起来会更加方便。

焊接主板并安装到机箱合理位置

焊接时我建议采用逐根焊接的方式,这比将所有线材捆扎在一起再焊接要稳妥得多,能有效降低线材断裂的风险。焊接完成后,我选择将开发板安装在靠近视频输出接口的位置。因为设备是放在机柜中使用,没有进行持续视频输出,这个位置也远离主板上的其他热源。另外,金属接口和机箱背板还能为包裹了绝缘胶带的开发板提供一定的散热作用。

为主机安装副风扇

和之前的改造一样,我额外安装了一个小型涡轮风扇。这次我把它装在了CPU散热器上,进风口对准散热器。这样设计的好处是可以更快地抽走 CPU 散热器产生的热量,并将热量导向新安装的主风扇方向。这或许能在一定程度上弥补新风扇在追求静音时带来的性能损失。

安装前,别忘了用绝缘胶带仔细包裹风扇,多缠绕几圈,以防止出现意外情况。

定制 Gen10 风扇控制程序

如果你将上篇文章的控制程序直接迁移到 Gen10 使用,会发现程序并不能够直接使用,设备会无法正常启动。

我目前在使用的风扇策略程序如下(代码开源在 soulteary/hpe-gen10-fan-proxy/gen10.ino):

// HPE Gen10 fan proxy for Noctua NF-A12 PWM fan
#include <FastLED.h>
#define pinOfPin(P)\
  (((P)>=0&&(P)<8)?&PIND:(((P)>7&&(P)<14)?&PINB:&PINC))
#define pinIndex(P)((uint8_t)(P>13?P-14:P&7))
#define pinMask(P)((uint8_t)(1<<pinIndex(P)))
#define isHigh(P)((*(pinOfPin(P))& pinMask(P))>0)
#define isLow(P)((*(pinOfPin(P))& pinMask(P))==0)

const int pwmInPin = A1;
const int pwmOutPin = 9;
const int pwm2OutPin = 10;
const int rpmInPin = 4;
const int hpeTachPin = A2;
const int normalTachPin = 3;
const uint16_t pwmTop = 320;
const int sample = 25000;
const float pwmMap[4] = {0.35, 0.45, 0.20, 0.65};
const int rpmUpper = 3400;
const int rpmLower = 60;
const float timer2Freq = 16000000 / 1024 / 2;
const int timer2RpmLower = 900;
const float pwm2OutDutty = 0.45;

volatile uint32_t pulseCount = 0;

void counter()
{
	pulseCount++;
}

ISR (PCINT2_vect)
{
  if (digitalRead(rpmInPin) == HIGH)
	  pulseCount++;
}

void setup()
{
  // Configure Timer 1 for PWM @ 25 kHz.
  TCCR1A = 0;           // undo the configuration done by...
  TCCR1B = 0;           // ...the Arduino core library
  TCNT1  = 0;           // reset timer
  TCCR1A = _BV(COM1A1)  // non-inverted PWM on ch. A
         | _BV(COM1B1)  // same on ch; B
         | _BV(WGM11);  // mode 10: ph. correct PWM, TOP = ICR1
  TCCR1B = _BV(WGM13)   // ditto
         | _BV(CS10);   // prescaler = 1
  ICR1   = pwmTop;      // TOP = 320

  // Configure Timer 2 for PWM @ 7.8125 kHz. for tach simulate
  TCCR2A = 0;
  TCCR2B = 0;
  TCNT2  = 0;
  TCCR2A = _BV(COM2B1)  // non-inverted PWM on ch. B
      | _BV(WGM20);  // mode 5: ph. correct PWM, TOP = OCR2A
  TCCR2B = _BV(WGM22)   // ditto
      | _BV(CS20)| _BV(CS21)| _BV(CS22);   // prescaler = 1024
  OCR2A  = 255;

  pinMode(pwmInPin, INPUT);
  // Tachometer output signal need pullup to 5v, ref https://noctua.at/pub/media/wysiwyg/Noctua_PWM_specifications_white_paper.pdf
  pinMode(rpmInPin, INPUT_PULLUP);
  pinMode(pwmOutPin, OUTPUT);
  pinMode(pwm2OutPin, OUTPUT);
  pinMode(hpeTachPin, OUTPUT);
  pinMode(normalTachPin, OUTPUT);
  // Tach low
  digitalWrite(hpeTachPin, LOW);
  // pwm2 
  analogWrite25k(pwm2OutPin, pwm2OutDutty * pwmTop);
  // RPM In
  //attachInterrupt(digitalPinToInterrupt(rpmInPin), counter, RISING);
  // Enable PCIE2 Bit3 = 1 (Port D)
  PCICR |= B00000100;
  // Select PCINT20 Bit4 = 1 (Pin D4)
  PCMSK2 |= _BV(rpmInPin);

  // Disable ugly rgb of nano mini board
  CRGB leds[3] = {
    {0, 0, 0},
    {0, 0, 0},
    {0, 0, 0},
  };
  FastLED.addLeds<WS2812, 2, GRB>(leds, 3);
  FastLED.show();

  Serial.begin(115200);
  Serial.println("HP fan proxy");
}

float readHpePWM(int pin, int sample) {
  uint32_t total = 0;
  uint32_t low = 0;
  for (uint16_t i = 0; i < sample; i++) {
    low += isLow(pin) ;
    total ++;
  }
  // Inversed PWM
  return (float) low / total;
}

float readIntelPWM(int pin, int sample) {
  uint32_t total = 0;
  uint32_t low = 0;
  for (uint16_t i = 0; i < sample; i++) {
    low += isHigh(pin) ;
    total ++;
  }
  // Normal PWM
  return (float) low / total;
}

void analogWrite25k(int pin, int value)
{
  switch (pin) {
    case 9:
      OCR1A = value;
      break;
    case 10:
      OCR1B = value;
      break;
    default:
      // no other pin will work
      break;
  }
}
uint32_t pulseFanPrev = 0; 
uint32_t clockPrev = 0;

float map_to_float(float x, float in_min, float in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;;
}

void loop()
{
  // handle first loop
  if (pulseFanPrev == 0 && clockPrev == 0) {
    pulseFanPrev = pulseCount;
    clockPrev = millis();
    delay(1000);
    return;
  }
  // compute rpm
  uint32_t clockLoopBegin = millis();
  uint32_t pulse = pulseCount - pulseFanPrev;
  uint32_t clock = clockLoopBegin - clockPrev;
  pulseFanPrev = pulseCount;
  clockPrev = millis();
  uint32_t rpm = pulse * 1000 * 60 / 2 / clock; /* 计算转速 */

  digitalWrite(hpeTachPin, rpm > 0 ? LOW: HIGH);

  // read inversed pwm(about 22ms with 25000 samples) and output mapped, 
  float pwmOut;
  float pwmIn = readHpePWM(pwmInPin, sample);
  if (pwmIn >= pwmMap[1])
    pwmOut = pwmMap[3];
  else if (pwmIn <= pwmMap[0])
    pwmOut = pwmMap[2];
  else
    pwmOut = map_to_float(pwmIn, pwmMap[0], pwmMap[1], pwmMap[2], pwmMap[3]);
  uint16_t out = uint16_t(pwmOut * pwmTop);
  analogWrite25k(pwmOutPin, out);

  // debug out
  Serial.print("iLO: "); Serial.print(pwmIn * 100); 
  Serial.print("% Out: "); Serial.print(pwmOut * 100); 
  Serial.print("% RPM: "); Serial.print(rpm); Serial.print(" : "); Serial.print(pulse); Serial.print(" / "); Serial.println(clock);


  if (rpm > 0) {
    /* 每秒输出2*RPM个脉冲 */
    float pulseInterval = 1000000.0 / (rpm * 2); /* 计算脉冲间隔(微秒) */
    digitalWrite(hpeTachPin, LOW);
    delayMicroseconds(pulseInterval / 2);
    digitalWrite(hpeTachPin, HIGH);
    delayMicroseconds(pulseInterval / 2);
  } else {
    digitalWrite(hpeTachPin, HIGH); /* 无转速时保持 HIGH */
  }
}

在对风扇控制系统进行优化时,我们除了要重新设计了性能参数和转速映射关系,还要对信号输出机制进行改进。

新的设计增加了与主板的通信频率,同时增加了一个保护机制:当系统无法获取实际转速数据时,会自动向主板发送 100% 运转的信号。这样的设计可以有效避免因主板固件逻辑缺陷而导致的误判断和非预期关机问题。放心,这样并不会让你的风扇转的更快。

IDE 中的调试日志

在开发阶段,我们可以借助 IDE 提供的串口监视器 (Serial Monitor) 功能,实时观察温度的采样数据以及风扇的输出状态。通过这些实时数据的反馈,我们就能及时调整和优化风扇控制的算法策略,让温控效果更加理想。

将调试完善的风扇控制程序烧录到开发板后,我们把机箱盖安装好,并将整个设备放回机柜。

至此,硬件准备工作就完成了,接下来就可以开始搭建万兆网络环境了。

万兆网络组建

解决了噪音问题后,让我们把目光转向网络配置这个环节。

设备网络接入情况

插网线,上电启动

经过一段时间的使用,我对之前文章提到的 10G 网卡印象相当不错。最让人惊喜的是它出色的散热表现,甚至同时安装两张网卡也不会有任何问题。正好当初测试时我买了两张同款,现在已经将它们都装在了 HP Gen10 服务器的 PCIe 插槽上。

为了充分发挥双网卡的优势,我又购入了两根 DAC(Direct Attach Cable)网线,开销 40 元。其中一根 DAC 线已经连接到了之前买的那个百元级交换机上,而另一根我打算用来和未来新增的设备做直连,这个等我改造完新设备再说。

原始设备配备了双千兆网口,我们可以同时启用这两个接口,不仅提升了网络连接的冗余性,还能确保设备运行更加稳定可靠。

验证网络基础性能

我把 ESXi 8 系统部署在了 HPE Gen10 Plus 服务器上,而 Gen10 服务器则暂时装了个黑群晖(等测试完后,打算换成 ESXi 或者 PVE 虚拟化平台)。

网络测试,两台机器通过交换机跑满万兆

测试结果显示,两台设备都能够通过交换机实现万兆网络的满速传输,同时 CPU 占用率保持在较低水平。值得一提的是,X3216 处理器虽然性能相对较弱,但这次测试中表现出色,可能是由于机器温度降低带来的性能提升。

Gen10 Plus ESXi 设置

如果你想在安装了 ESXi 的设备中进行网络测试,可以先创建一台虚拟机。在配置过程中,建议将网卡硬件设置为“直通”模式,然后在创建虚拟机时,通过“添加 PCI 设备”的方式将网卡分配给这台虚拟机。需要注意的是,由于 ESXi 的特殊要求,我们必须开启“预分配内存”选项。完成这些设置后,你就可以安装任意用于性能测试的操作系统了。

Ubuntu 虚拟机中配置万兆网卡

我用的测试虚拟机是 Ubuntu 24.04。默认情况下,Ubuntu 虚拟机里的万兆网卡不会自动启用。不过别担心,几个简单的步骤就能搞定。

首先用 ip addr 命令检查下网卡是否被识别。输入命令后,你会看到系统已经认出了这张网卡(3号和4号网卡):

# ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute 
       valid_lft forever preferred_lft forever
2: ens34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
...
...
3: ens64f0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 8c:dc:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    altname enp3s0f0
4: ens64f1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 8c:dc:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    altname enp3s0f1

因为是在虚拟机里用 PCIe 方式连接的网卡,我们还可以用 lspci 这个命令看看网卡是否正常识别:

# lspci | grep Ethernet
02:02.0 Ethernet controller: VMware VMXNET3 Ethernet Controller (rev 01)
03:00.0 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)
03:00.1 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)

Ubuntu 新版本缺了些常用工具,我们先装上:

sudo apt-get update && sudo apt-get install -y ethtool net-tools

通过 ethtool ,我们能看到网卡的详细状况:

# ethtool -i ens64f0
driver: ixgbe
version: 6.8.0-51-generic
firmware-version: 0x80000811, 1.475.0
expansion-rom-version: 
bus-info: 0000:03:00.0
supports-statistics: yes
supports-test: yes
supports-eeprom-access: yes
supports-register-dump: yes
supports-priv-flags: yes

要让网卡正常工作,我们需要创建一个合适的网络配置:

sudo vim /etc/netplan/01-netcfg.yaml

配置内容如下:

network:
  version: 2
  renderer: networkd
  ethernets:
    ens64f0:
      dhcp4: no
      addresses:
        - 10.11.12.233/24
      mtu: 9000  # 如果需要配置巨型帧
    ens64f1:
      dhcp4: no
      addresses:
        - 10.11.12.234/24
      mtu: 9000  # 如果需要配置巨型帧

为了避免安全警告,还需要调整配置文件的可访问权限:

sudo chmod 600 /etc/netplan/01-netcfg.yaml

保存配置后,执行 apply,让配置生效。

sudo netplan apply

如果命令没有出现报错信息。现在,我们可以使用 ip addr show 命令,来看网卡是否正确获取网络使用了。

# ip addr show ens64f0
# ip addr show ens64f1

3: ens64f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP group default qlen 1000
    link/ether 8c:dc:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    altname enp3s0f0
    inet 10.11.12.233/24 brd 10.11.12.255 scope global ens64f0
       valid_lft forever preferred_lft forever
...
4: ens64f1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP group default qlen 1000
    link/ether 8c:dc:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    altname enp3s0f1
    inet 10.11.12.234/24 brd 10.11.12.255 scope global ens64f1
       valid_lft forever preferred_lft forever
...

到这里,虚拟机中的网卡就可以正常使用啦。

激活群晖网络测试工具

在之前分享的群晖使用技巧中,我们主要通过 Docker 来安装和使用标准的 Linux 工具,比如网络性能测试工具 iperf3

群晖其实自带了一个“诊断工具”套件。通过几个简单的命令,我们就能激活这些官方预编译好的工具,来完成硬件性能测试。

使用方法很简单。可以先切换到管理员身份,然后用命令激活工具:

# 切换 root 管理员
sudo -i

# 使用命令安装/加载工具
synogear install

完成后,我们就可以直接使用包括 iperf3 在内的各种工具了。

举个例子:

# iperf3 -c 10.11.12.233

Connecting to host 10.11.12.233, port 5201
[  5] local 10.11.12.213 port 54818 connected to 10.11.12.233 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  1.09 GBytes  9.38 Gbits/sec  300    844 KBytes       
[  5]   1.00-2.00   sec  1.09 GBytes  9.37 Gbits/sec    0    844 KBytes       
[  5]   2.00-3.00   sec  1.07 GBytes  9.23 Gbits/sec    0    844 KBytes       
[  5]   3.00-4.00   sec  1.08 GBytes  9.29 Gbits/sec    0    856 KBytes       
[  5]   4.00-5.00   sec  1.06 GBytes  9.07 Gbits/sec    0    884 KBytes       
[  5]   5.00-6.00   sec  1.05 GBytes  9.05 Gbits/sec   91    636 KBytes       
[  5]   6.00-7.00   sec  1.07 GBytes  9.19 Gbits/sec    0    703 KBytes       
[  5]   7.00-8.00   sec  1.07 GBytes  9.20 Gbits/sec    0    796 KBytes       
[  5]   8.00-9.00   sec  1.09 GBytes  9.32 Gbits/sec    0    799 KBytes       
[  5]   9.00-10.00  sec  1.09 GBytes  9.35 Gbits/sec    0    799 KBytes       
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  10.8 GBytes  9.25 Gbits/sec  391             sender
[  5]   0.00-10.00  sec  10.8 GBytes  9.24 Gbits/sec                  receiver

iperf Done.

值得一提的是,群晖的诊断工具包里还包含了很多实用工具。通过 synogear list 命令,我们可以查看完整的工具列表(注意:不同系统版本和调试工具版本包含的工具可能会有所不同):

# 查看所有内置工具
# synogear list
All tools:
autojump autojump_match.py cifsiostat domain_test.sh file fix_idmap.sh iftop iostat iotop iperf iperf3 log-analyzer.sh lsof mpstat ncat nethogs nmap nping perf-check.py pidstat sa1 sa2 sadc sadf sar sid2ugid.sh sockstat speedtest-cli.py sysstat tcpdump tcpdump_wrapper tmux update_check.py zblacklist zmap ztee

其他

Gen10 Plus iLO

在折腾的过程中,能通过 Gen10 Plus 的 iLO 实时查看设备运行状态(比如 ESXi 运行到什么阶段啦)确实很方便。

不过不是所有设备都有这功能,比如前面提到的 Gen10,或者其他价格更亲民的消费级产品、笔记本之类的。后面的文章里,我会分享一些便宜又靠谱的解决方案,毕竟支持 iLO 的设备,还有 iLO 配件、授权都挺贵的。

最后

今天的内容就到这里,有了前面文章的铺垫,这篇写起来特别顺手。

我们,下篇文章再聊。

–EOF