Synology DS923+にAQC113C搭載の互換10GbEカードを導入してみた

今回の記事はChatGPTに生成してもらって私が味付けして書いてます(挨拶)

技術系記事で公開の意義はあると思うけど、正直理解してない部分もかなりあるというやつです。文体もできる限り直してますが、ご容赦ください。

DS923+はSynologyのNASで、ハイアマチュアというかプロシューマーモデルになります。SOHO(死語?)向けだと思います。
私はSynologyの監視カメラと連携して自宅前の画像を24時間態勢で保存したり、写真データのバックアップ先として活用しています。

DS923+ですが、10Gbps用の拡張カード(E10G22-T1-Mini)がオプションで売られています。見ての通り結構高額で流通数も少なめ、かつ他の方の記事を参照してもらえばわかりますが、互換カードが出ていて、そちらのほうがチップのモデルが新しくて発熱量が小さいそうです。送料込みで13000円くらいでした。今回生まれて初めてアリエク使いましたが問題なく届きました。
ただ、届いた外箱はE10G22-T1-Miniのパッケージで、開封済み内袋の中に互換品が入っている、という状態で届いて、古き良き中国の気配がしました。
今回はPCとNASを10GbEで直結して使うことにします。家庭内LANは1GbE接続です。
PC側はこのカードを買っていれました。LANケーブルはDS923+付属のもの(CAT5Eらしい)を使いましたが、問題なさそうです。

ちなみにNAS側のカードの取り付けでまず引っかかりました。拡張カードを入れるスロットのネジはP2サイズのドライバーじゃないと外せません。あとはそのまま差し込むだけ。簡単。

↓こっからチャッピー

結果から書くと、私の環境ではプラグアンドプレイではありませんでした。

まぁそれは想定内で、検索すると出てくるスクリプトを回せば動きそうだ、という当てはあったのですが、スクリプトの中身が不安だったのでチャッピーに聞いてみるとセキュリティリスクがゼロではないとのご回答、というわけでチャッピーに聞いた通りの手順で回していくと認識しました。具体的には以下の通り。

DSMに標準搭載されているatlantic_v2ドライバへPCI IDを手動登録すると、10Gbps Full Duplexで通信できました。

ただし、atlantic_v2へバインドしたままDSMを再起動すると、次回起動後にAQC113CがPCIeデバイス一覧から消える。
ドライバが認識しないのではない。lspciにも出てこなくなる。

条件を変えて確認したところ、再起動前にatlantic_v2から明示的にunbindすれば、この現象を回避できた。

注意

以下はSynologyの正式サポート外の構成であり、root権限の操作を含む。DSM更新で動かなくなる可能性がある。バックアップを確保し、管理用の1GbE接続を残した状態で作業すること。

使用環境

NAS側

  • Synology DS923+
  • DSM 7系
  • Linuxカーネル:4.4.302+
  • DSMビルド:#90080
  • 互換10GbEカード:A77P-10Gbe
  • コントローラー:AQC113C
  • PCI ID:1d6a:14c0

PC側

  • Windows
  • Realtek RTL8127搭載10GbE NIC
  • PCとNASをRJ45で直結
  • 既存の1GbE接続は通常ネットワークおよび管理用として維持

作業環境

SSHクライアント(puttyとか、WindowsPowerShellでもなんでも良い)を用いて(1GbEネットワークで)DS923+にSSH接続してください。ID,passは普段DSMにログインするものでできます。
もともとのユーザーの権限次第ですが、sudo -iでroot権限取れちゃうはずです。DS923+側で操作するコマンドはすべてrootでやってます。
DSM側でSSH接続接続を許可しておくことを忘れないように。

直結用ネットワーク

DS923+ LAN 3:10.10.10.1/30
Windows 10GbE:10.10.10.2/30
デフォルトゲートウェイ:双方とも空欄
MTU:1500

カードは見えるが、ネットワークインターフェースが出ない

カードを装着してDS923+を起動すると、PCIeデバイスとしては認識された。

lspci -nn | grep -i '1d6a:14c0'

出力:

01:00.0 Class [0200]: Device [1d6a:14c0] (rev 03)

一方、ドライバはバインドされていなかった。

lspci -k -s 01:00.0

出力には、次の行がない。

Kernel driver in use:

sysfsにもdriverリンクが存在しなかった。

ls -l /sys/bus/pci/devices/0000:01:00.0/driver

DSM側にはatlanticatlantic_v2の両モジュールがロードされていた。

lsmod | grep atlantic

そこで、DSM標準のatlantic_v2へAQC113CのPCI IDを追加した。

echo "1d6a 14c0" > /sys/bus/pci/drivers/atlantic_v2/new_id

new_idは、ロード済みのPCIドライバへVendor IDとDevice IDを追加し、該当する未バインドデバイスをprobeさせるLinuxの標準的なsysfsインターフェースである。

直後、カードはatlantic_v2へバインドされた。

lspci -k -s 01:00.0
Kernel driver in use: atlantic_v2

さらにeth2が生成された。

ip link show eth2

リンク速度も正常だった。

ethtool eth2

主要部分:

Speed: 10000Mb/s
Duplex: Full
Link detected: yes

LAN 3へ固定IPを設定する

ドライバをバインドすると、DSMのネットワーク画面にもLAN 3として表示された。

途中、手動で設定した10.10.10.1が消え、169.254.x.xのリンクローカルアドレスへ戻ることがあった。これは物理リンクの異常ではなく、DSMがLAN 3をDHCPまたはリンクローカル設定として扱っていたためだった。

DSMの以下の画面から固定IPへ変更した。

コントロールパネル
→ ネットワーク
→ ネットワークインターフェース
→ LAN 3
→ 編集

NAS側:

IPアドレス:10.10.10.1
サブネットマスク:255.255.255.252
デフォルトゲートウェイ:空欄
DNS:空欄
MTU:1500

Windows側:

IPアドレス:10.10.10.2
サブネットマスク:255.255.255.252
デフォルトゲートウェイ:空欄
DNS:空欄

Windowsからのpingは正常に通った。

ping 10.10.10.1
Reply from 10.10.10.1: bytes=32 time<1ms TTL=64

SMBが本当に10GbE側を通っているか確認する

SMB共有にはNAS名や1GbE側IPではなく、10GbE側IPを指定して接続した。

\\10.10.10.1\共有フォルダ名

Windows上で使用中のSMB経路を確認する。

Get-NetTCPConnection -State Established -RemotePort 445 |
    Select-Object LocalAddress, RemoteAddress

今回の出力:

LocalAddress RemoteAddress
------------ -------------
10.10.10.2   10.10.10.1

SMBは間違いなく10GbE側を通っていた。

転送速度はHDDで頭打ちになった

10Gbpsでリンクしたからといって、HDD上の共有フォルダが1GB/sで転送できるわけではない。

今回の実測値は概ね次のとおりだった。

  • PCからNAS、50GB単一ファイル:約100~125MB/s
  • NASからPC、50GB単一ファイル:約150MB/s
  • RAW画像を多数転送:約60MB/s

DSMのリソースモニターでは、転送先ストレージプールのHDD使用率が80~100%に張り付いていた。一方、10GbE側の通信量には余裕があり、メモリ(16GBの互換カード追加で20GBに増設済み)も不足していなかった。

監視カメラの録画先は別ストレージプールに分離している。録画を停止しても傾向は変わらず、今回の主な律速は対象ストレージプールのHDDと判断した。

単一ファイルよりRAW画像群の方が遅いのは、ファイルごとに作成、属性更新、ディレクトリ更新などが発生するためと考えられる。

転送開始直後だけ一時的に速度が高くなったのは、Windows側とNAS側のファイルキャッシュの影響と考えられる。DSM上では20GBのメモリのうち約16GBがキャッシュ済みとして利用されていた。キャッシュが吸収できる間はHDDの持続書き込み速度を超えて見えるが、長時間転送では最終的にHDD速度へ収束する。

最大の罠――バインドしたまま再起動するとカードが消える

起動時にnew_idを書けば運用できると思ったが、ウォームリブートで問題が発生した。

atlantic_v2へバインドした状態でDSMを再起動すると、次回起動後のlspciからAQC113C自体が消えた。

lspci -nn | grep -i '1d6a:14c0'

何も返らない。

これは単なるドライバ未認識ではない。OSから見てPCIeデバイスそのものが存在しない状態なので、起動後にnew_idを書いても復旧しない。ACアダプターを抜いた完全なコールドブートが必要だった(※一応DSMから電源OFF→スイッチONでも認識はしてます。)。

条件を変えて切り分けた

再起動前の状態 再起動後
AQC113Cをドライバへバインドしない lspciに残る
atlantic_v2へバインドしたまま再起動 lspciから消える
再起動前にatlantic_v2からunbind lspciに残る

したがって、カード自体が無条件にウォームリブートへ失敗するわけではない。

少なくとも今回の環境では、atlantic_v2へバインドした状態でシステムを終了することが発生条件だった。

内部原因までは特定できていない。

  • DSM付属atlantic_v2のshutdown処理
  • AQC113Cのファームウェア
  • PCIeの電源状態またはリセット処理
  • DS923+側のドライバ停止順序

これらのいずれか、または組み合わせと考えている。

再起動前にunbindすれば回避できた

手動では、次の手順で問題を回避できた。

まず、AQC113CのPCIアドレスをVendor IDとDevice IDから検索する。

BDF="$(
    for dev in /sys/bus/pci/devices/*; do
        [ "$(cat "$dev/vendor" 2>/dev/null)" = "0x1d6a" ] || continue
        [ "$(cat "$dev/device" 2>/dev/null)" = "0x14c0" ] || continue
        basename "$dev"
        break
    done
)"

echo "$BDF"

出力例:

0000:01:00.0

ネットワークインターフェースを停止し、ドライバから切り離す。

ip link set eth2 down
echo "$BDF" > /sys/bus/pci/drivers/atlantic_v2/unbind

この状態ではカード自体はlspciに残るが、Kernel driver in use: atlantic_v2は消える。

その後DSMを再起動すると、再起動後もAQC113CはPCIeデバイスとして列挙された。

起動時bindスクリプト

PCIアドレスは固定せず、Vendor IDとDevice IDから毎回検索する。

保存先例:

/volume1/test/_admin_scripts/aqc113-bind.sh
#!/bin/sh

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH

DRIVER="/sys/bus/pci/drivers/atlantic_v2"
VENDOR_ID="0x1d6a"
DEVICE_ID="0x14c0"
NEW_ID="1d6a 14c0"
STATIC_IP="10.10.10.1/30"

find_device()
{
    for dev in /sys/bus/pci/devices/*; do
        [ -r "$dev/vendor" ] || continue
        [ -r "$dev/device" ] || continue
        [ "$(cat "$dev/vendor" 2>/dev/null)" = "$VENDOR_ID" ] || continue
        [ "$(cat "$dev/device" 2>/dev/null)" = "$DEVICE_ID" ] || continue
        echo "$dev"
        return 0
    done

    return 1
}

echo "===== $(date '+%Y-%m-%d %H:%M:%S') ====="
echo "Starting AQC113 binding"

i=0
DEVICE_PATH=""

while [ "$i" -lt 60 ]; do
    DEVICE_PATH="$(find_device)"

    if [ -n "$DEVICE_PATH" ] && [ -e "$DRIVER/new_id" ] && [ -e "$DRIVER/bind" ]; then
        break
    fi

    sleep 1
    i=$((i + 1))
done

if [ -z "$DEVICE_PATH" ]; then
    echo "ERROR: AQC113 1d6a:14c0 was not found"
    exit 1
fi

if [ ! -e "$DRIVER/new_id" ]; then
    echo "ERROR: atlantic_v2 new_id was not found"
    exit 1
fi

BDF="${DEVICE_PATH##*/}"
echo "PCI address: $BDF"

if [ -L "$DEVICE_PATH/driver" ]; then
    BOUND_DRIVER="$(basename "$(readlink "$DEVICE_PATH/driver")")"

    if [ "$BOUND_DRIVER" != "atlantic_v2" ]; then
        echo "ERROR: Unexpected driver: $BOUND_DRIVER"
        exit 1
    fi
else
    echo "Registering PCI ID $NEW_ID"

    printf '%s\n' "$NEW_ID" > "$DRIVER/new_id" 2>/dev/null || true

    i=0
    while [ ! -L "$DEVICE_PATH/driver" ] && [ "$i" -lt 15 ]; do
        sleep 1
        i=$((i + 1))
    done

    if [ ! -L "$DEVICE_PATH/driver" ]; then
        echo "Trying explicit bind"
        printf '%s\n' "$BDF" > "$DRIVER/bind"
    fi
fi

if [ ! -L "$DEVICE_PATH/driver" ]; then
    echo "ERROR: Driver binding failed"
    exit 1
fi

BOUND_DRIVER="$(basename "$(readlink "$DEVICE_PATH/driver")")"
echo "Bound driver: $BOUND_DRIVER"

if [ "$BOUND_DRIVER" != "atlantic_v2" ]; then
    echo "ERROR: Unexpected bound driver: $BOUND_DRIVER"
    exit 1
fi

i=0
while [ ! -d "$DEVICE_PATH/net" ] && [ "$i" -lt 60 ]; do
    sleep 1
    i=$((i + 1))
done

NETDEV=""

for netpath in "$DEVICE_PATH"/net/*; do
    [ -e "$netpath" ] || continue
    NETDEV="${netpath##*/}"
    break
done

if [ -z "$NETDEV" ]; then
    echo "ERROR: Network interface was not created"
    exit 1
fi

echo "Network interface: $NETDEV"

ip link set dev "$NETDEV" up

sleep 15

if ! ip -4 -o addr show dev "$NETDEV" |
    grep -q '10\.10\.10\.1/30'; then

    echo "Applying fallback address $STATIC_IP"
    ip -4 addr flush dev "$NETDEV" scope global
    ip addr add "$STATIC_IP" dev "$NETDEV"
fi

ip addr show dev "$NETDEV"

ethtool "$NETDEV" 2>/dev/null |
    grep -E 'Speed:|Duplex:|Link detected:' || true

echo "AQC113 initialization completed"
exit 0

実行権限を付与する。

chmod 700 /volume2/test/_admin_scripts/aqc113-bind.sh

DSMの起動タスク

コントロールパネル
→ タスク スケジューラ
→ 作成
→ トリガーされたタスク
→ ユーザー定義のスクリプト
  • タスク名:AQC113 Bind
  • ユーザー:root
  • イベント:ブートアップ
  • 有効:オン

ユーザー定義スクリプト:

/bin/sleep 120
/bin/sh /volume1/test/_admin_scripts/aqc113-bind.sh >> /volume2/test/_admin_scripts/aqc113-bind.log 2>&1

起動直後はDSMのネットワーク処理と競合したため、120秒の待ち時間を入れている。(※バグ取りの過程で入れてるsleepなので、なくても良いと思う(未検証))

終了時unbindスクリプト

保存先例:

/volume2/test/_admin_scripts/aqc113-unbind.sh
#!/bin/sh

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH

DRIVER="/sys/bus/pci/drivers/atlantic_v2"
VENDOR_ID="0x1d6a"
DEVICE_ID="0x14c0"

find_device()
{
    for dev in /sys/bus/pci/devices/*; do
        [ -r "$dev/vendor" ] || continue
        [ -r "$dev/device" ] || continue
        [ "$(cat "$dev/vendor" 2>/dev/null)" = "$VENDOR_ID" ] || continue
        [ "$(cat "$dev/device" 2>/dev/null)" = "$DEVICE_ID" ] || continue
        echo "$dev"
        return 0
    done

    return 1
}

echo "===== $(date '+%Y-%m-%d %H:%M:%S') ====="
echo "Starting AQC113 unbind"

DEVICE_PATH="$(find_device)"

if [ -z "$DEVICE_PATH" ]; then
    echo "NOTICE: AQC113 is not present"
    exit 0
fi

BDF="${DEVICE_PATH##*/}"
echo "PCI address: $BDF"

if [ ! -L "$DEVICE_PATH/driver" ]; then
    echo "AQC113 is already unbound"
    exit 0
fi

BOUND_DRIVER="$(basename "$(readlink "$DEVICE_PATH/driver")")"
echo "Current driver: $BOUND_DRIVER"

if [ "$BOUND_DRIVER" != "atlantic_v2" ]; then
    echo "ERROR: Refusing to unbind unexpected driver: $BOUND_DRIVER"
    exit 1
fi

for netpath in "$DEVICE_PATH"/net/*; do
    [ -e "$netpath" ] || continue

    NETDEV="${netpath##*/}"
    echo "Setting $NETDEV down"
    ip link set dev "$NETDEV" down 2>/dev/null || true
done

sync

if ! printf '%s\n' "$BDF" > "$DRIVER/unbind"; then
    echo "ERROR: Failed to unbind $BDF"
    exit 1
fi

sleep 1

if [ -L "$DEVICE_PATH/driver" ]; then
    echo "ERROR: Driver is still bound"
    exit 1
fi

echo "Successfully unbound AQC113 at $BDF"
exit 0

実行権限を付与する。

chmod 700 /volume2/test/_admin_scripts/aqc113-unbind.sh

DSMのシャットダウンタスク

  • タスク名:AQC113 Unbind
  • ユーザー:root
  • イベント:シャットダウン
  • 有効:オン

ユーザー定義スクリプト:

/bin/sh /volume2/test/_admin_scripts/aqc113-unbind.sh >> /volume2/test/_admin_scripts/aqc113-unbind.log 2>&1

シャットダウンおよび再起動前にこの処理が実行され、atlantic_v2からカードを切り離す。

自動化の確認方法

ウォームリブート後、まず終了ログを確認する。

tail -50 /volume2/test/_admin_scripts/aqc113-unbind.log

期待する出力:

Successfully unbound AQC113 at 0000:01:00.0

続いて起動ログを確認する。

tail -50 /volume2/test/_admin_scripts/aqc113-bind.log

期待する出力:

Bound driver: atlantic_v2
Network interface: eth2
AQC113 initialization completed

最終状態を確認する。

lspci -nnk | grep -A4 -i '1d6a:14c0'
ip addr show eth2
ethtool eth2 | grep -E 'Speed:|Duplex:|Link detected:'

iperfでネットワーク単体を測定する

ファイルコピーではHDDが律速になるため、NIC、ドライバ、ケーブル、TCP処理だけを確認するにはiperfを使う。

ここでもいくつか罠があった。

iperf 2とiperf3には互換性がない

NAS側には次のiperfが入っていた。

iperf version 2.1.8 (12 August 2022) pthreads

当初、Windows側のiperf3と接続したところ、次のエラーになった。

warning: JSON data length overflow - 33554688 bytes JSON size is not allowed
iperf3: error - unable to receive parameters from client

iperf 2とiperf3は通信プロトコルが異なり、相互接続できない。両側をiperf 2へ統一した。

5001番ポートが使用中だった

iperf 2の既定ポート5001でサーバーを起動しようとすると、次のエラーが出た。

listen failed: Address already in use

そこで、測定には5002番ポートを使用した。

PCからNAS方向

NAS側をサーバーにする。

iperf -s -B 10.10.10.1 -p 5002 -i 1

Windows側をクライアントにする。

iperf.exe -c 10.10.10.1 -B 10.10.10.2 -p 5002 -t 30 -i 1 -P 4

NASからPC方向

iperf 2.1.8の--reverseを試したところ、NAS側で次のエラーが発生した。

*** buffer overflow detected ***: terminated
Aborted (core dumped)

このため、逆方向測定では--reverseを使わず、サーバーとクライアントを手動で入れ替えた。

Windows側をサーバーにする。

iperf.exe -s -B 10.10.10.2 -p 5002 -i 1

NAS側をクライアントにする。

iperf -c 10.10.10.2 -B 10.10.10.1 -p 5002 -t 30 -i 1 -P 4

Windows側をサーバーにする場合は、Windows Defenderファイアウォールでiperfの受信を許可する必要がある場合がある。

iperf実測値: (NAS→PC)0.00-30.03 sec 32.9 GBytes 9.41 Gbits/sec、(PC→NAS)0.00-30.02 sec 33.2 GBytes 9.49 Gbits/sec

外部ドライバ導入は見送った

AQC113C向けの外部atlantic.koをダウンロードし、起動時にロードするスクリプトも見つかった。

しかし、そのスクリプトはroot権限で外部サイトからBusyBox、シェルスクリプト、カーネルモジュールを取得し、一部のHTTPS接続では証明書検証を無効化していた。

目的とコードの大筋には整合性があったものの、実際に実行されるファイルを十分に監査できなかったため採用しなかった。

今回の環境では、DSM標準のatlantic_v2だけで10GbE通信まで確認できた。外部カーネルモジュールを導入するより、bindとunbindのライフサイクルを補う方を選んだ。

自作SPK化も断念した

起動時bindと終了時unbindをパッケージのライフサイクルに載せる方が構造としては自然である。

そこでroot権限で動作する自作SPKも作成したが、DSM 7では署名されていないroot権限パッケージのインストールが拒否された。

DSMのセキュリティ制約を迂回してまで導入するのは適切ではないため、DSMが標準で提供しているrootのユーザー定義タスクを使用した。

この運用の限界

DSMから正常に再起動またはシャットダウンした場合は、終了タスクでunbindできる。

一方、次のケースでは終了タスクが実行されない。

  • 停電
  • 電源コードを抜いた場合
  • カーネルパニック
  • 強制電源断
  • DSMの終了処理自体が停止した場合

その場合、次回起動時にAQC113Cがlspciから消える可能性がある。復旧には、DS923+を停止し、ACアダプターを抜いて完全に電源を落とした後、再起動する必要がある。

したがって、1GbE管理経路は残しておくべきである。

現時点での評価

項目 結果
PCIeデバイスとして認識 する
DSM標準atlantic_v2で通信 できる
10Gbps Full Duplex 確認
ドライバ不要・挿すだけ 不可
バインドしたままウォームリブート カードが消える
事前にunbindしてウォームリブート カードが残る
停電・強制終了時 回避処理は動かない

この互換カードは、純正品と同じ安定性とは言えない。

それでも、DSM標準ドライバだけで10GbEリンクが成立し、再起動時の発生条件と回避方法まで切り分けられた。

ファイル転送ではHDDが先に限界へ達した。RAW画像群では約60MB/sまで低下し、10GbE化だけで写真整理が劇的に速くなるわけではなさそうです。CrystalDiskMarkみたいなので検証したほうが良いのかもしれないですが。

純正カードとの価格差は、部品代だけの差ではない。

挿して終わること、再起動を意識しなくてよいこと、DSM更新時の保守対象を増やさないことにも価格が付いている。

今回、互換カードは動いた。

しかし、挿して終わりではなかった。

以上チャッピー

1年以上やろうかなぁ、まぁええか……って思いながらスルーしてたことができたので、夏の工作としては上等だったと思います。結果は伴わず。出費は2万円。
高速化について有識者のお話があったら情報ください。基本的には撮影したRAWファイル爆速転送したいとかそのくらいなので。

というわけで、今日はこの辺で。

シェアする