Files
obsidian/infra/data/nas-storage.md

339 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: NAS StorageClass (NFS + iSCSI)
updated: 2026-04-14 문서 정확성 보정 (hp2 연결 상태, nodeAffinity, iSCSI on-demand 동작)
tags: [infra, k3s, storage, nfs, iscsi, synology]
---
## 개요
Synology NAS(DS916+)를 K3s NFS/iSCSI StorageClass로 사용. Longhorn(로컬 블록)과 병행하여 파일 저장소, 웹소스 등 대용량/RWX 워크로드에 사용.
## NAS 정보
| 항목 | 값 |
|------|-----|
| 모델 | Synology DS916+ (Braswell, x86_64, Linux 3.10.108) |
| bond0 (eth0+eth1, 1G×2) | **192.168.9.100** — 관리/SMB/DSM 웹 |
| eth2 (USB 2.5G, RTL8157, r8152) | **192.168.205.100** — NFS/iSCSI 데이터 플레인, MTU 9000 |
| 디스크 | 11TB (사용 2%) |
| NFS export | `/volume1/k3s-nfs` |
| NFS 옵션 | `rw,async,no_wdelay,crossmnt,no_root_squash,insecure_locks,sec=sys,anonuid=1025,anongid=100` |
K3s 노드는 모두 192.168.205.0/24 서브넷에 참여하고 있음 (2026-04-14 검증):
| 노드 | 출구 NIC | src IP |
|------|---------|--------|
| incus-kr1 | enp7s0 (PCIe 2.5G) | 192.168.205.214 |
| incus-kr2 | enx803f5dd34c9f (USB 2.5G) | 192.168.205.135 |
| incus-hp2 | ens2 | 192.168.205.134 |
## K3s 설정
| 항목 | 값 |
|------|-----|
| StorageClass | `nfs` (default 아님, 명시 지정 필요) |
| Provisioner | nfs-subdir-external-provisioner (Helm) |
| Namespace | `nfs-provisioner` |
| NFS 경로 | `/volume1/k3s-nfs` |
| 마운트 옵션 | `soft,timeo=50,retrans=3` |
| archiveOnDelete | true (PVC 삭제 시 데이터 archived- 접두사로 보존) |
| nodeAffinity | **없음 (제약 없이 배포)** — 2026-04-14 기준 Deployment에 nodeSelector/affinity 미설정. 3 노드(kr1/kr2/hp2) 전부 192.168.205.0/24 도달 가능해 스케줄 자유 |
### Helm 설치 명령
```bash
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--namespace nfs-provisioner --create-namespace \
--set nfs.server=192.168.205.100 \
--set nfs.path=/volume1/k3s-nfs \
--set storageClass.name=nfs \
--set storageClass.defaultClass=false \
--set storageClass.reclaimPolicy=Delete \
--set storageClass.archiveOnDelete=true \
--set nfs.mountOptions[0]=soft \
--set nfs.mountOptions[1]=timeo=50 \
--set nfs.mountOptions[2]=retrans=3
```
### PVC 사용 예시
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data
spec:
storageClassName: nfs
accessModes: [ReadWriteMany]
resources:
requests:
storage: 10Gi
```
## 성능 (fio 벤치마크, 2026-04-05)
| 테스트 | IOPS | 대역폭 | 레이턴시 |
|--------|------|--------|---------|
| Random Read 4K | 20.6K | 84 MB/s | 1.5 ms |
| Random Write 4K | 18.1K | 74 MB/s | 1.8 ms |
| Seq Read 1M | 263 | 276 MB/s | 30 ms |
| Seq Write 1M | 275 | 288 MB/s | 29 ms |
## 용도별 StorageClass 선택
| 용도 | StorageClass | 이유 |
|------|-------------|------|
| DB (PostgreSQL, etcd, Redis) | longhorn | 저레이턴시 블록 필요 |
| 파일 업로드, 사용자 데이터 | nfs | 대용량, RWX 지원 |
| 웹소스, 정적 파일 | nfs | 대용량, 여러 Pod 공유 |
| 로그, 임시 데이터 | longhorn/local-path | 빠른 쓰기 |
## 파일 소유권
- `no_root_squash` 설정으로 root 컨테이너는 소유권 문제 없음
- 비-root 컨테이너는 Pod `securityContext.fsGroup`으로 제어
## NFS hard vs soft
- **hard**: NAS 끊기면 무한 대기 → 서버 먹통
- **soft**: 타임아웃 후 에러 반환 → 서버 생존
모든 NFS 마운트는 `soft,timeo=50,retrans=3` 필수. 인시던트 이력: [[2026-04-04-usb-25g-hang|history]]
## iSCSI StorageClass (democratic-csi)
Synology NAS의 iSCSI를 K3s 블록 스토리지로 사용. democratic-csi가 PVC 생성/삭제 시 자동으로 iSCSI Target + LUN을 관리.
| 항목 | 값 |
|------|-----|
| StorageClass | `synology-iscsi` |
| CSI Driver | democratic-csi (Helm) |
| Namespace | `democratic-csi` |
| iSCSI Portal | 192.168.205.100:3260 |
| Base IQN | `iqn.2000-01.com.synology:NAS.k3s.` |
| LUN 타입 | BLUN (Btrfs thin provisioning) |
| Volume | /volume1 |
| 인증 | kaffa 계정 (HTTPS API) |
| 자동 관리 | PVC 생성 → Target+LUN 생성, PVC 삭제 → Target+LUN 삭제 |
### Helm 설치
values 파일: `/tmp/democratic-csi-values.yaml` (kr1)
```bash
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
helm repo add democratic-csi https://democratic-csi.github.io/charts/
helm install synology-iscsi democratic-csi/democratic-csi \
--namespace democratic-csi --create-namespace \
-f /tmp/democratic-csi-values.yaml
```
### ScsiTarget 서비스의 on-demand 동작 (중요)
DSM의 `ScsiTarget` 패키지는 LIO(kernel configfs) 기반이며, **Target이 0개이면 3260 포트를 LISTEN하지 않는다.** 이건 고장이 아니라 LIO의 정상 동작.
- PVC 없는 상태 → `netstat -lnt | grep 3260` 비어있음
- PVC 생성 → democratic-csi가 Synology API로 Target+LUN 자동 생성 → configfs에 Target 등록 → 3260 자동 LISTEN
- PVC 삭제 → Target/LUN 자동 제거 → Target 0개 되면 3260 닫힘
따라서 "3260 포트가 안 열려있다"만으로 장애 판단 금지. **Target 존재 여부 먼저 확인:**
```bash
# NAS에서
find /sys/kernel/config/target/iscsi -maxdepth 2 -type d
sudo synopkg status ScsiTarget # "running"이어야 함
```
### 2026-04-14 smoke test 결과
| 단계 | 결과 |
|------|------|
| PVC 생성 (1Gi, synology-iscsi) → Bound | 30초 |
| Target + LUN 자동 생성 | OK (democratic-csi → Synology HTTPS API) |
| 3260 LISTEN | OK (Target 등록과 동시에) |
| discovery (kr1/kr2/hp2 전부) | OK (양 포털 192.168.205.100 + 192.168.9.100) |
| PVC 삭제 → PV Released → DeleteVolume → PV gone | OK |
2026-04-14 이전 며칠간 ScsiTarget이 stop 상태로 방치되어 있었음. 원인 미확인(`/var/log/synopkg.log` 로그 기준 3/24 부팅 이후 stop 기록 없지만 실제 상태는 stop). 수동 `sudo synopkg start ScsiTarget`로 복구. 실제 소비자 PVC는 계속 0건이라 서비스 영향 없었음.
### 전체 StorageClass 요약
| StorageClass | 방식 | 용도 | HA |
|---|---|---|---|
| longhorn | 로컬 NVMe 블록 | DB, 고성능 블록 | 노드 간 레플리카 |
| synology-iscsi | NAS iSCSI 블록 | 블록 스토리지 (NAS) | RAID5 |
| nfs | NAS NFS 파일 | 파일, 웹소스, RWX | RAID5 |
| local-path | 로컬 디스크 | 캐시, 임시 | 없음 |
## Jumbo Frame (MTU 9000) 설정 (2026-04-05)
모든 NAS 전용 경로에 MTU 9000 적용. 스위치 포함 전체 경로가 JF 지원.
| 장비 | 인터페이스 | MTU | 설정 위치 |
|------|-----------|-----|----------|
| kr1 | enp7s0 (PCIe 2.5GbE) | 9000 | `/etc/systemd/network/20-enp7s0.network` |
| kr2 | enx803f5dd34c9f (USB 2.5GbE) | 9000 | `/etc/systemd/network/30-usb-2g5.network` |
| hp2 | ens2 | 9000 | (설정 위치 확인 필요) |
| NAS | eth2 (USB 2.5GbE) | 9000 | Synology DSM 네트워크 설정 |
### 2026-04-14 end-to-end JF 검증
`ping -M do -s 8972`(DF bit, 9000B 프레임) 전 구간 통과:
| 경로 | 결과 | avg RTT |
|------|------|---------|
| kr1 → NAS | 2/2 | 0.46ms |
| kr2 → NAS | 2/2 | 0.44ms |
| hp2 → NAS | 2/2 | 0.47ms |
| kr1 → kr2 | 2/2 | 1.09ms |
| kr1 → hp2 | 2/2 | 0.54ms |
| hp2 → kr2 | 2/2 | 0.46ms |
스위치 포함 전 경로 9000B fragmentation 없이 통과. 노드 간(kr1↔kr2, kr1↔hp2 등)도 JF 동작 — storage 전용망 외 노드 간 통신에도 활용 가능.
## NAS eth2 USB NIC watchdog (2026-04-14)
NAS의 eth2(USB 2.5G, RTL8157)가 USB 3.0 LPM exit latency 미보고 이슈로 간헐적 disconnect 발생 (21일 uptime 기준 2회). 드라이버는 자동 reconnect하지만 인터페이스가 DOWN 상태로 남아 네트워크 서비스 중단.
근본 원인: DSM 3.10.108 커널의 xHCI 호스트 컨트롤러가 LPM `U1/U2 exit latency` descriptor를 OS에 노출하지 않음 → 링크 절전 복귀 타이밍 불일치 → `-71 EPROTO` → USB disconnect. DS916+ 하드웨어/커널 조합의 근본적 한계로 드라이버 업데이트·커널 교체로 해결 불가.
### 대응: cron watchdog
| 항목 | 값 |
|------|-----|
| 스크립트 | `/usr/local/bin/eth2-watchdog.sh` (root:755) |
| 주기 | 1분 (root cron, `/etc/crontab`) |
| 로그 | `/var/log/eth2-watchdog.log` |
| 영속성 | `/dev/md0` ext4 root, 재부팅 유지 (DSM 메이저 업그레이드 시 재확인 필요) |
동작:
1. `/sys/class/net/eth2/operstate` 확인
2. `up`이 아니면 `ip link set eth2 up` (IP는 DSM ifcfg-eth2가 자동 할당)
3. UP 후 IP 없으면 `ip addr add 192.168.205.100/24 dev eth2`
4. MTU ≠ 9000 이면 보정
### 검증 기록
- 2026-04-14 강제 down 테스트: 1분 내 watchdog 실행 → `ip link up` → DSM ifcfg 자동 IP 할당 → 정상 복귀 (MTU 9000 유지)
- Outline 상세 기록: `2026-04-14 / NAS eth2 watchdog 구축 / kappa` (id `93baf66b-f003-47a9-9d14-7a66e3dbfde0`)
- RCA 문서: `2026-04-14 / NAS eth2 USB NIC 링크 드롭 원인 조사 / kappa` (id `db923170-8c16-459d-82ce-46fdc1f0f0d0`)
### kr2 USB NIC 드라이버 (r8152 DKMS)
kr2의 USB 2.5GbE 어댑터(Realtek RTL8157, 0bda:8157)는 커널 기본 `cdc_ncm` 드라이버로 잡히면 Half Duplex + MTU 1500 제한. DKMS r8152 드라이버(v2.21.4)를 설치하여 Full Duplex + Jumbo Frame 지원.
- DKMS 패키지: `linux-headers-$(uname -r)` 설치 시 자동 빌드
- udev 규칙: `/etc/udev/rules.d/50-usb-realtek-net.rules` — USB config를 1로 설정하여 r8152가 바인딩
- 커널 업데이트 시 DKMS가 자동으로 재빌드
### 주의사항
- Jumbo Frame은 경로 전체(NIC → 스위치 → NIC)가 지원해야 함. 미지원 스위치가 중간에 있으면 프레임 드롭
- `ping -M do -s 8972` 로 end-to-end JF 동작 확인 가능
## DSM Reverse Proxy — `nas.inouter.com`
LAN 전용으로 Synology DSM 웹 UI 접근. `nas.inouter.com``*.inouter.com` 와일드카드 폴백으로 `k3s.inouter.com (192.168.9.53, Traefik MetalLB VIP)` 로 해석되고, Traefik 에서 이 호스트를 Synology NAS 로 reverse proxy 한다.
| 항목 | 값 |
|---|---|
| Namespace | `lan-proxies` (신규 — LAN-only 리버스 프록시 전용) |
| Service | `nas` (selector 없는 ClusterIP) + EndpointSlice → `192.168.9.100:5000` |
| Backend | Synology DSM HTTP (포트 5000, 평문) |
| Traefik IngressRoute | `nas` (web entrypoint, redirect → https), `nas-tls` (websecure entrypoint) |
| Middleware | `redirect-https` (`redirectScheme.scheme=https permanent=true`) |
| TLS | 기존 wildcard `wildcard-inouter-tls` (cert-manager + emberstack reflector 자동 복제) |
| 외부 노출 | LAN-only — 와일드카드 `*.inouter.com` 가 사설 IP `192.168.9.53` 로 폴백되므로 인터넷 라우팅 없음 |
### ExternalName 대신 Service+EndpointSlice 사용 이유
요구사항은 "ExternalName Service" 였으나 Kubernetes ExternalName 은 RFC1123 DNS 호스트네임만 허용하고 IP(`192.168.9.100`)는 거부. 동등한 K8s-native 패턴인 **selector-less Service + 수동 EndpointSlice** 로 구현. CoreDNS 가 ClusterIP 를 정상 발행하고 kube-proxy 가 EndpointSlice 의 외부 IP 로 분산.
### 매니페스트
원본: `/tmp/nas-reverse-proxy.yaml` (heimdall). 핵심 발췌 — 전체 6 객체.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: lan-proxies
labels:
purpose: lan-only-reverse-proxy
---
apiVersion: v1
kind: Service
metadata: { name: nas, namespace: lan-proxies }
spec:
type: ClusterIP
ports:
- { name: dsm-http, port: 5000, targetPort: 5000, protocol: TCP }
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: nas
namespace: lan-proxies
labels: { kubernetes.io/service-name: nas }
addressType: IPv4
ports:
- { name: dsm-http, port: 5000, protocol: TCP }
endpoints:
- addresses: [192.168.9.100]
conditions: { ready: true }
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata: { name: redirect-https, namespace: lan-proxies }
spec:
redirectScheme: { scheme: https, permanent: true }
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata: { name: nas, namespace: lan-proxies }
spec:
entryPoints: [web]
routes:
- kind: Rule
match: Host(`nas.inouter.com`)
middlewares: [{ name: redirect-https }]
services: [{ name: nas, port: 5000 }]
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata: { name: nas-tls, namespace: lan-proxies }
spec:
entryPoints: [websecure]
routes:
- kind: Rule
match: Host(`nas.inouter.com`)
services: [{ name: nas, port: 5000 }]
tls:
secretName: wildcard-inouter-tls
```
### 동작 검증
```bash
curl -sk --resolve nas.inouter.com:80:192.168.9.53 http://nas.inouter.com/ # → 301 https://nas.inouter.com/
curl -sk --resolve nas.inouter.com:443:192.168.9.53 https://nas.inouter.com/ # → 200, 12094B
# <title>NAS - Synology DiskStation</title>
```
### 주의사항
- **LAN-only**. `*.inouter.com` 가 사설 192.168.9.53 으로 가는 한 외부 인터넷에서는 도달 불가. 외부 노출 시 보안/인증 재검토 필요 (Synology DSM 직노출은 CVE 노출면 큼).
- DSM HTTPS(5001) 도 동일 호스트에서 200 응답하지만 자기서명 인증서 + 이중 TLS 종료라 백엔드는 평문 5000 사용.
- Traefik 리버스 프록시 패턴은 `lan-proxies` ns 에 추가 LAN 서비스(예: 스위치/라우터/NAS 부속 UI) 적층 가능.
## 관련 문서
- [[backup]] — 백업 파이프라인 (NAS 활용)
- [[storage-plan]] — NVMe NAS + iSCSI 기획, 벤치마크 결과
- [[postgresql-ha]] — PostgreSQL HA (Patroni + etcd)
- [[infra-hosts]] — 서버 목록
- [[cert-manager]] — `wildcard-inouter-tls` 발급/복제
- [[cloudflare]] — `nas.inouter.com` DNS 이력