docs(.wiki): ingest NAS recovery session 2026-05-18/19
15-hour cross-hypervisor recovery from WD40EFAX SMR RAID5 cascade failure. 5 entities + 7 concepts + 1 source documenting: - Root cause: WD40EFAX SMR cascade in 3-disk RAID 5 - Hyper Backup .hbk structure + SFTP-jail / ACL workarounds - OVA from Synology VMM (KVM) → VirtualBox: SCSI→SATA, Hyper-V driver disable, paravirt=kvm, GA install, NAT switch - MSSQL restore via named volume + chown, sqlcmd -x, mssql-conf - Web.config UTF-16 vs UTF-8 BOM IIS 500.19 trap - Traefik on Windows DD: configFile, named volume for acme.json, file-provider as docker.sock workaround - Snapshot of current recovery architecture + SPOF list - Placeholder for future resilient-architecture work Plus .tasks/nas-recovery.md and STATUS.md updates closing the task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
125
cms-config-rewrite-pattern.md
Normal file
125
cms-config-rewrite-pattern.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: Web.config rewrite — кодировка и connection-string patterns
|
||||
type: concept
|
||||
tags: [iis, webconfig, encoding, powershell, traceback]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Web.config rewrite на Windows: encoding pitfall
|
||||
|
||||
## Сценарий
|
||||
|
||||
После cross-host миграции CMS в VM, нужно переписать connection strings (`192.168.1.10` → `10.0.2.2` или другой host). Делается через SSH/PowerShell `Get-Content`+`-replace`+`Set-Content`.
|
||||
|
||||
## Pitfall: Set-Content без -Encoding
|
||||
|
||||
```powershell
|
||||
# WRONG — на PowerShell 5.1 пишет UTF-16 LE
|
||||
(Get-Content $f -Raw) -replace 'old', 'new' | Set-Content $f -NoNewline
|
||||
```
|
||||
|
||||
**Симптом** после этого:
|
||||
- IIS возвращает `HTTP 500.19 - Internal Server Error`, "Файл конфигурации создан в неправильном формате XML", error code `0x8007000d`.
|
||||
- В детале: первые строки файла показывают артефакты `????????` (UTF-16 в окне UTF-8 ожидаемой проверки).
|
||||
|
||||
**Корень:** PowerShell 5.1 (Windows встроенный) `Set-Content` без `-Encoding` использует **default = UTF-16 LE** (Unicode). IIS ожидает UTF-8 (BOM или без BOM).
|
||||
|
||||
## Правильный способ
|
||||
|
||||
```powershell
|
||||
$utf8WithBom = New-Object System.Text.UTF8Encoding($true)
|
||||
$content = Get-Content $file -Raw
|
||||
$new = $content -replace 'old', 'new'
|
||||
[System.IO.File]::WriteAllText($file, $new, $utf8WithBom)
|
||||
```
|
||||
|
||||
Или, если хочется без BOM:
|
||||
```powershell
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($file, $new, $utf8NoBom)
|
||||
```
|
||||
|
||||
ASP.NET / IIS работают и с BOM, и без — но для дефолтного XML конфига Microsoft предпочитает с BOM.
|
||||
|
||||
### PowerShell 7+ workaround
|
||||
|
||||
В Core: `Set-Content -Encoding utf8` пишет UTF-8 БЕЗ BOM (отличается от PS 5!). `-Encoding utf8BOM` — с BOM.
|
||||
|
||||
## Connection string patterns в CMS
|
||||
|
||||
Нашли в `MoreThenCms.WebUI/Web.config` source (development):
|
||||
|
||||
```xml
|
||||
<add name="MoreThenCmsEntities"
|
||||
connectionString="Data Source=DESKTOP-XYZ\SQLEXPRESS;Initial Catalog=MoreThenCms;
|
||||
Integrated Security=True;MultipleActiveResultSets=True"
|
||||
providerName="System.Data.SqlClient" />
|
||||
```
|
||||
|
||||
В production (внутри OVA, после восстановления):
|
||||
|
||||
```xml
|
||||
<add name="MoreThenCmsEntities"
|
||||
connectionString="Data Source=192.168.1.10;Initial Catalog=MoreThenCms;
|
||||
Integrated Security=False;User Id=snolla;
|
||||
Password=fXkH4@8O%3pc;MultipleActiveResultSets=True"
|
||||
providerName="System.Data.SqlClient" />
|
||||
```
|
||||
|
||||
То есть в prod CMS использует **SQL login `snolla`**, не Integrated Security. Сам логин остался в `master.mdf` после восстановления — никаких дополнительных шагов не понадобилось.
|
||||
|
||||
## Mass-edit нескольких сайтов
|
||||
|
||||
Под Windows VM было 4 Web.config с одинаковым паттерном:
|
||||
|
||||
- `C:\inetpub\wwwroot\MoreThenCms.Web\Web.config`
|
||||
- `C:\inetpub\wwwroot\Snolla.IdentityManager\Web.config`
|
||||
- `C:\stayer\stostayer.old\web.config`
|
||||
- (плюс `C:\stayer\MoreThenCms.Web\` — main stostayer без 192.168.1.10 reference)
|
||||
|
||||
Скрипт через SSH в VM:
|
||||
|
||||
```powershell
|
||||
$files = @(
|
||||
'C:\inetpub\wwwroot\MoreThenCms.Web\Web.config',
|
||||
'C:\inetpub\wwwroot\Snolla.IdentityManager\Web.config',
|
||||
'C:\stayer\stostayer.old\web.config'
|
||||
)
|
||||
$utf8WithBom = New-Object System.Text.UTF8Encoding($true)
|
||||
foreach ($f in $files) {
|
||||
$content = Get-Content $f -Raw
|
||||
$new = $content -replace [regex]::Escape('192.168.1.10'), '10.0.2.2'
|
||||
[System.IO.File]::WriteAllText($f, $new, $utf8WithBom)
|
||||
}
|
||||
# затем
|
||||
iisreset
|
||||
```
|
||||
|
||||
## Storage providers (Azure / MinIO)
|
||||
|
||||
В `appSettings` нашли:
|
||||
|
||||
```xml
|
||||
<add name="azureGalleries" storageType="MoreThenCms.FileStorage.Azure.AzureCloudStorage, ...">
|
||||
<settings>
|
||||
<add name="connectionString"
|
||||
value="DefaultEndpointsProtocol=http;AccountName=snolla;
|
||||
AccountKey=<base64>" />
|
||||
<add name="container" value="galleries" />
|
||||
</settings>
|
||||
</add>
|
||||
```
|
||||
|
||||
Используется **Azure Storage SDK с custom endpoint**. В production endpoint указывает на **MinIO** (S3-compat но **не** Azure-compat — пользователь упомянул "не так всё" и обещал пояснить). Точная схема подключения — TBD, надо изучать `MoreThenCms.FileStorage.Azure.AzureCloudStorage` класс.
|
||||
|
||||
## `[regex]::Escape` для безопасной замены IP
|
||||
|
||||
`192.168.1.10` без escape — точки в regex matchят любой char. Лучше escape:
|
||||
|
||||
```powershell
|
||||
$pattern = [regex]::Escape('192.168.1.10')
|
||||
$new = $content -replace $pattern, '10.0.2.2'
|
||||
```
|
||||
|
||||
Связано: [[snolla-recovery-vm]], [[traefik-on-windows-docker-desktop]].
|
||||
100
future-resilient-architecture-goals.md
Normal file
100
future-resilient-architecture-goals.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Future Resilient Architecture — цели на потом
|
||||
type: concept
|
||||
tags: [planning, architecture, resilience, roadmap, todo]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Future Resilient Architecture
|
||||
|
||||
Placeholder для глобальной задачи "выстроить отказоустойчивую архитектуру" — обсуждается **после** того как recovery полностью устаканится.
|
||||
|
||||
Эта страница — **черновик целей**, не план. По ходу обсуждения распилится на специфичные концепты + ADR (architectural decision records).
|
||||
|
||||
## Что сейчас не так
|
||||
|
||||
См. [[recovery-architecture-snapshot]] → раздел "Single Points of Failure". Кратко:
|
||||
|
||||
- ⚠️ Windows-PC = SPOF
|
||||
- ⚠️ Один public IP
|
||||
- ⚠️ Один MSSQL primary без replica
|
||||
- ⚠️ Один MinIO single-node
|
||||
- ⚠️ Backup только Hyper Backup на одну синку (Kreknin), сейчас она cold
|
||||
- ⚠️ Backup нового рабочего состояния (CMS data на recovery-host) **отсутствует**
|
||||
- ⚠️ Нет off-site backup (cloud)
|
||||
|
||||
## Цели верхнего уровня
|
||||
|
||||
1. **RTO** (Recovery Time Objective) — сколько максимум **downtime** клиенты должны видеть при отказе.
|
||||
- Текущий по факту: ~15 часов (что и было).
|
||||
- Целевой: **< 1 час** для одиночного отказа, **< 4 часов** для каскадного.
|
||||
2. **RPO** (Recovery Point Objective) — сколько максимум **данных потерять** при отказе.
|
||||
- Текущий: до 24 часов (если успеет бэкап после изменения) — больше если бэкап не успел.
|
||||
- Целевой: **< 1 час**, в идеале continuous (replication).
|
||||
3. **Стоимость** — bounded by разумным % от выручки клиентов.
|
||||
4. **Operational simplicity** — никаких heroic ops по 15 часов в случае проблемы.
|
||||
|
||||
## Технические направления (наброски)
|
||||
|
||||
### Backup-стратегия 3-2-1
|
||||
|
||||
Стандарт: **3** копии данных, **2** разных media, **1** off-site.
|
||||
|
||||
- **Копия 1:** primary storage (live) на NAS / Windows-PC
|
||||
- **Копия 2:** local backup target ([[kreknin-synology]] остаётся, плюс отдельный)
|
||||
- **Копия 3:** **cloud off-site** — Backblaze B2 / Yandex Object Storage / AWS S3 Deep Archive / Hetzner Storage Box
|
||||
- Все backups encrypted client-side
|
||||
- Retention: 7 daily / 4 weekly / 12 monthly
|
||||
|
||||
### Compute resilience
|
||||
|
||||
Варианты:
|
||||
|
||||
A. **Multi-host on-prem:** 2-3 физических машины с виртуализацией (Proxmox VE), HA-кластер, VMотом migration. Дорого, но автономно.
|
||||
|
||||
B. **Cloud-first:** перенос CMS-стека в managed Kubernetes (Yandex Cloud / VK Cloud / hetzner) — managed MSSQL / managed S3 / managed Postgres. Простота операций, но vendor lock-in.
|
||||
|
||||
C. **Гибрид:** primary on-prem (Synology с CMR), warm standby в cloud (готовый к failover за 5 мин). Compromise.
|
||||
|
||||
### Database resilience
|
||||
|
||||
MSSQL options:
|
||||
- **Always On Availability Groups** (требует Enterprise license — недёшево)
|
||||
- **Log Shipping** (бесплатно, но manual failover)
|
||||
- **MSSQL → PostgreSQL миграция?** (если есть полная свобода — выгоды Open Source + распространённые managed предложения).
|
||||
|
||||
Для recovery-сценария: **continuous backup** через VDI + native MSSQL TDE backup → S3.
|
||||
|
||||
### Network resilience
|
||||
|
||||
- **Multi-WAN на OpenWRT:** primary провайдер + 4G/LTE USB-modem как failover. OpenWRT mwan3 package.
|
||||
- **Cloudflare Proxy / Tunnel:** перенаправление публичного трафика через Cloudflare edge. Помогает с DDoS и переключением IP без DNS-changes.
|
||||
- **Static IP пересмотр:** разные провайдеры → multi-homed setup.
|
||||
|
||||
### Monitoring + auto-recovery
|
||||
|
||||
- **Healthchecks** на каждый layer (TCP, HTTP, deep DB query) — Prometheus + Alertmanager или Uptime Kuma.
|
||||
- **Watchdog скрипты** — auto `ipconfig /release/renew` в VM, auto `docker compose restart` контейнера если healthcheck падает > N min.
|
||||
- **Telegram-уведомления** на инциденты (для пользователя в реал-тайм когда что-то фейлится).
|
||||
|
||||
### Документация и runbook
|
||||
|
||||
- Runbook на каждый процедурный сценарий (failover, restore, network swap).
|
||||
- Регулярные **failover drills** раз в квартал — буквально нажать "восстановиться" и засечь время.
|
||||
- Wiki эта — стартовая точка.
|
||||
|
||||
## Что НЕ цели
|
||||
|
||||
- Перестройка с нуля «потому что сейчас не идеально». Текущая архитектура работает, замена должна быть инкрементальной.
|
||||
- 99.99% uptime — нереалистично для one-person infrastructure, и не нужно для CMS-сайтов клиентов.
|
||||
|
||||
## Следующие шаги (когда дойдут руки)
|
||||
|
||||
1. Заменить WD40EFAX на CMR-диски, пересобрать пул [[dead-synology-diskstation]].
|
||||
2. Включить ежедневный Hyper Backup из нового пула на [[kreknin-synology]].
|
||||
3. Добавить cloud-backup target (Yandex Object Storage наиболее очевидный для RU-юрисдикции).
|
||||
4. Настроить monitoring/alerts.
|
||||
5. Документировать runbooks.
|
||||
|
||||
Связано со всеми остальными страницами этого wiki.
|
||||
101
hyper-backup-structure-and-recovery.md
Normal file
101
hyper-backup-structure-and-recovery.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Hyper Backup — структура репо и стратегия восстановления
|
||||
type: concept
|
||||
tags: [backup, synology, hyperbackup, recovery, lessons]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Hyper Backup — структура и recovery
|
||||
|
||||
## Структура `.hbk` репозитория
|
||||
|
||||
Каталог `<task-name>.hbk` на target-NAS содержит:
|
||||
|
||||
```
|
||||
<task>.hbk/
|
||||
├── Config/ — метаданные задачи (план бэкапа)
|
||||
│ ├── @Share/<sharename>/ — список файлов в каждой бэкапленной шаре
|
||||
│ ├── target_info.db.<N> — SQLite, инфа о target
|
||||
│ ├── version_info.db.<N> — версии бэкапов
|
||||
│ ├── file_chunk*.index — индексы для дедупликации
|
||||
│ ├── virtual_file.index — карта файлов
|
||||
│ └── _Syno_TaskConfig — **plain text JSON конфиг задачи** (имя, source-host, encryption flag, schedule, backup_folders[], backup_apps[], backup_volumes[])
|
||||
├── Control/ — управление (lock, @writer)
|
||||
├── Guard/ — служебное
|
||||
├── Pool/ — дедуплицированные chunks данных
|
||||
├── synobkpinfo.db — SQLite с backup_info_tb, task_id_tb
|
||||
├── _Syno_TaskConfig — копия конфига в корне
|
||||
└── SynologyHyperBackup.bkpi — маркер
|
||||
```
|
||||
|
||||
**`_Syno_TaskConfig`** — самый ценный файл для оценки бэкапа без полной распаковки. JSON в нём содержит:
|
||||
- `name` — имя задачи (в нашем кейсе `rsync Server 1`)
|
||||
- `host_name` — имя source-NAS
|
||||
- `enable_data_encrypt` (true/false) — шифрование клиентским паролем
|
||||
- `backup_folders[]` — список путей, которые бэкапились
|
||||
- `backup_apps[]` — DSM-пакеты (если бэкапили их данные)
|
||||
- `backup_volumes[]` — VM-снимки (Synology VMM)
|
||||
|
||||
## Hyper Backup Vault vs Hyper Backup
|
||||
|
||||
- **Hyper Backup** (`/var/packages/HyperBackup/`) — клиент. Бэкапит ИЗ этого DSM.
|
||||
- **Hyper Backup Vault** (`/var/packages/HyperBackupVault/`) — сервер. Принимает бэкап от других DSM.
|
||||
|
||||
На target-NAS (где лежит репо) обычно установлен **Vault**. Сам он не имеет UI для restore — только receive. Restore инициируется из **Hyper Backup** (тот же пакет, но в другом режиме).
|
||||
|
||||
## Стратегии восстановления
|
||||
|
||||
Когда мёртвый NAS — source, репо на живом target:
|
||||
|
||||
### A. **Restore через DSM UI на target-NAS** (применили мы)
|
||||
|
||||
1. Открыть DSM web UI на target.
|
||||
2. Hyper Backup → **Restore** → **Data Restore Wizard**.
|
||||
3. Список задач **пустой** (этот NAS — приёмник, не source). Снизу-слева: **"Восстановить из существующих репозиториев"**.
|
||||
4. Server type: **"В локальную папку и на USB"**.
|
||||
5. Указать путь к `.hbk` (`/volume1/NetBackup/diskstation_1.hbk`).
|
||||
6. Если шифрования нет (`enable_data_encrypt=false`) — сразу к выбору данных.
|
||||
7. **Системную конфигурацию НЕ восстанавливать** (иначе наложатся пользователи/сеть/шары source-NAS на target).
|
||||
8. Selective restore — пик нужные подпапки.
|
||||
9. **Restore destination:** "Restore to another location" → новая папка (`/volume1/NetBackup/restore-tmp/` или просто root тома → создаст шары с именами как на source).
|
||||
10. Версия: топовая (свежая дата).
|
||||
|
||||
**Гочча:** при restore "to original location" DSM создаст **новые SMB-шары** на target c именами как на source (`backup/`, `docker/`, `work/`). Это **меняет state target-NAS**. Если важно — выбирать "another location".
|
||||
|
||||
### B. **Hyper Backup Explorer (HBE)** — офлайн на Windows/Mac/Linux
|
||||
|
||||
GUI-приложение от Synology, читает `.hbk` напрямую (требует password если шифрован). Подходит когда target-NAS недоступен и есть только файлы репо.
|
||||
|
||||
**Минусы:** GUI, тысячи кликов для многих файлов, нет batch-restore.
|
||||
|
||||
### C. **Restore + tar | ssh pull** (бекап-копия на чужую машину)
|
||||
|
||||
Если уже restored to a target-NAS:
|
||||
|
||||
```
|
||||
ssh user@target "tar cf - -C /volume1/backup ." | tar xf - -C /local/path
|
||||
```
|
||||
|
||||
При chrooted SFTP (типичный DSM) — нельзя через scp/sftp пройти за пределы home. Решение: **`scp -O`** (legacy SCP protocol через чистый SSH-канал). Или `ssh ... 'cat file' > local-file` для одиночных файлов.
|
||||
|
||||
## ACL/permissions особенности (DSM 7)
|
||||
|
||||
- DSM 7 использует **Synology ACL** поверх POSIX. Видно `+` после permissions: `d---------+`.
|
||||
- POSIX-биты могут показывать `0` для всех, но реально ACL может open file для специфичных users/groups.
|
||||
- Файлы в Hyper Backup-репо могут иметь permission `-rw-------` (owner-only) — тогда другому юзеру `scp -O` не достанет, требуется `chmod` от root.
|
||||
|
||||
## SFTP-jailed subsystem
|
||||
|
||||
DSM SFTP-subsystem **chroot'ит** пользователя в home (`/var/services/homes/<user>/`). Пути за пределами не видны через SFTP-клиент.
|
||||
|
||||
**Обход:** `scp -O` (флаг "use legacy SCP protocol") — обходит SFTP-subsystem, использует raw SSH-channel + shell-команды target-side. Тогда видно всё, что shell-пользователь видит.
|
||||
|
||||
## Стратегии для будущего
|
||||
|
||||
- **Hyper Backup ежедневно**, не "по триггеру" (как было). Окно потерь = 1 день.
|
||||
- **Retention** 30+ дней — даёт возможность откатиться при позднем обнаружении проблем.
|
||||
- **Test restore раз в квартал** — простейшая дисциплина: пик одну папку, восстанови в temp, проверь что файлы корректны. Иначе бэкап может тихо умереть без вашего ведома.
|
||||
- **Многослойный backup:** не только Synology→Synology. Дополнительно cloud (Backblaze B2, AWS S3 Glacier, Yandex Object Storage) — на случай если оба NAS физически рядом и сгорят вместе.
|
||||
|
||||
Связано: [[dead-synology-diskstation]], [[kreknin-synology]].
|
||||
151
mssql-container-data-restore.md
Normal file
151
mssql-container-data-restore.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: MSSQL контейнер с восстановленными production data — паттерн
|
||||
type: concept
|
||||
tags: [mssql, docker, recovery, named-volume, chown]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# MSSQL container с production data
|
||||
|
||||
## Проблема
|
||||
|
||||
Восстановить MSSQL базу в Docker-контейнере на Windows-хосте из:
|
||||
- Готовых `.mdf/.ldf` файлов production (взяты из tar `/var/opt/mssql/` source-контейнера)
|
||||
- + 5 user-databases + системные (master/model/msdb/tempdb)
|
||||
|
||||
## Что НЕ работает: bind-mount production data
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./production-data/mssql:/var/opt/mssql
|
||||
```
|
||||
|
||||
**Не работает** на Windows Docker Desktop. MSSQL-контейнер требует:
|
||||
- `master.mdf` owned by `mssql` user (UID 10001) или root
|
||||
- `chmod` на файлах внутри `/var/opt/mssql/log/` (логи, .xel, .trc)
|
||||
|
||||
На Windows DD bind-mount через WSL2-слой не позволяет:
|
||||
- Файлы видятся как owned by root or other UID — MSSQL отказывается: `Your master database file is owned by root.`
|
||||
- `chmod` внутри контейнера фейлится: `Operation not permitted`
|
||||
|
||||
Симптом: MSSQL стартует, в логах ругается на chmod, потом стартует ещё раз и зависает.
|
||||
|
||||
## Что работает: named volume + chown через temp container
|
||||
|
||||
Идея: создать named volume, скопировать данные внутрь с правильным chown, потом примонтировать к MSSQL.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml — финальная версия
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
container_name: mssql
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_SA_PASSWORD: ${SA_PASSWORD}
|
||||
MSSQL_PID: Developer
|
||||
ports:
|
||||
- "1433:1433"
|
||||
volumes:
|
||||
- mssql_data:/var/opt/mssql
|
||||
networks:
|
||||
- proxy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mssql_data:
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
```
|
||||
|
||||
Заполнение volume (один раз перед `docker compose up -d`):
|
||||
|
||||
```powershell
|
||||
docker volume create mssql_mssql_data
|
||||
docker run --rm \
|
||||
-v mssql_mssql_data:/dest \
|
||||
-v <local-path-to-production-data>/mssql:/src:ro \
|
||||
alpine cp -a /src/. /dest/
|
||||
|
||||
docker run --rm -v mssql_mssql_data:/dest \
|
||||
alpine chown -R 10001:0 /dest
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
После этого MSSQL стартует с production data, делает crash recovery в master.mdf, поднимается за 30-60 секунд (если CU контейнера == CU source) или 5-30 минут (если CU source старше — script upgrade mode).
|
||||
|
||||
## sqlcmd "$( var-substitution" pitfall
|
||||
|
||||
Альтернативный путь — restore из `.sql` script (export через "Generate Scripts" в SSMS). Файл 547 MB с CREATE+INSERT'ами всей БД.
|
||||
|
||||
**Не запустить через `sqlcmd -i file.sql`** без флага `-x`. Потому что:
|
||||
|
||||
- В данных встречаются jQuery JS-сниппеты типа `INSERT ... VALUES (N'$(function(){ ... })')`
|
||||
- sqlcmd по умолчанию интерпретирует `$(varname)` как переменную для подстановки.
|
||||
- На `$(function(){` парсер ломается → "Syntax error near command '('" в середине файла.
|
||||
|
||||
Фикс: `sqlcmd -x` (disable variable substitution).
|
||||
|
||||
```
|
||||
docker exec mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P '<pw>' -C -x \
|
||||
-i /var/opt/mssql/backup/MoreThenCms.sql
|
||||
```
|
||||
|
||||
## SA-аккаунт disabled в production
|
||||
|
||||
Production SQL Server конфигурации часто **отключают `sa`** (security best practice). Пароль может быть прав, но account disabled → `Login failed for user 'sa'. Reason: The account is disabled.`
|
||||
|
||||
Фикс: **`mssql-conf set-sa-password`** в offline-mode (server stopped) под root:
|
||||
|
||||
```
|
||||
# Stop running container
|
||||
docker compose stop
|
||||
|
||||
# Run offline mssql-conf in temp container с тем же volume
|
||||
docker run --rm --user 0:0 \
|
||||
-e ACCEPT_EULA=Y \
|
||||
-e MSSQL_SA_PASSWORD='<new-pw>' \
|
||||
-v mssql_mssql_data:/var/opt/mssql \
|
||||
mcr.microsoft.com/mssql/server:2019-latest \
|
||||
/opt/mssql/bin/mssql-conf set-sa-password
|
||||
|
||||
# Запуск нормального container
|
||||
docker compose start
|
||||
```
|
||||
|
||||
`mssql-conf set-sa-password` сбрасывает пароль И **enables sa**. Если env-pw совпадает с тем что ожидает приложение — приложение продолжает работать.
|
||||
|
||||
## Production passwords reuse
|
||||
|
||||
Замеченный паттерн: у пользователя один пароль `fXkH4@8O%3pc` используется как:
|
||||
- SA password (MSSQL)
|
||||
- Connection string password для user `snolla` (CMS DB user)
|
||||
- Возможно где-то ещё
|
||||
|
||||
Лучше: rotate after recovery, разделить.
|
||||
|
||||
## CU mismatch warning
|
||||
|
||||
Если master.mdf был из старой CU (например 2019 CU14), а контейнер `2019-latest` (например CU32-GDR), MSSQL после старта войдёт в **script upgrade mode** на 10-30 минут:
|
||||
|
||||
```
|
||||
Server is in script upgrade mode. Only administrator can connect at this time.
|
||||
```
|
||||
|
||||
Видно в логах сотни строк `spid9s ... Deleting AlwaysOnAgReplicas...` — это нормальный msdb-upgrade. Подождать. Один раз. После завершения — никаких задержек на следующих стартах.
|
||||
|
||||
## Healthcheck path для 2019-latest image
|
||||
|
||||
В современных image SQL Server инструменты лежат в `/opt/mssql-tools18/bin/sqlcmd` (с TLS-флагом `-C`), а не `/opt/mssql-tools/bin/sqlcmd`:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -C -Q 'SELECT 1' -b -o /dev/null || exit 1"]
|
||||
```
|
||||
|
||||
Связано: [[snolla-recovery-vm]] (Web.config указывает на `10.0.2.2:1433` = host из VM-NAT), [[windows-recovery-host]].
|
||||
127
recovery-architecture-snapshot.md
Normal file
127
recovery-architecture-snapshot.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Recovery architecture — текущая инфраструктура (2026-05-19)
|
||||
type: concept
|
||||
tags: [architecture, current-state, snapshot, recovery]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Recovery Architecture Snapshot
|
||||
|
||||
Снимок production-инфраструктуры на 2026-05-19, после завершения recovery. Это **рабочее, но временное** состояние — single-point-of-failure на бытовом железе. Замечания по улучшению — [[future-resilient-architecture-goals]].
|
||||
|
||||
## Цепочка запроса от клиента до CMS
|
||||
|
||||
```
|
||||
Клиент (browser)
|
||||
→ DNS resolve (REGRU): *.kzntsv.site, snolla.com, rimiz.ru, labtools.pro/ru, и т.д.
|
||||
→ 94.19.247.14 (public IP, статический у провайдера)
|
||||
→ router OpenWRT (192.168.1.1) [[openwrt-router]]
|
||||
→ NAT 443 → 192.168.1.143:4443
|
||||
→ NAT 80 → 192.168.1.143:8000
|
||||
→ Windows-PC (192.168.1.143) [[windows-recovery-host]]
|
||||
→ traefik 2.6.6 на 4443/8000
|
||||
→ TLS termination, certResolver=letsEncrypt из acme.json
|
||||
→ match по Host header (file-provider rules в data/custom/*.yml)
|
||||
→ backend = http://host.docker.internal:18080/
|
||||
→ host:18080 = VBox NAT port forward → VM:80
|
||||
→ snolla-recovery VM [[snolla-recovery-vm]]
|
||||
→ IIS на 80
|
||||
→ IIS site matched by Host header (catch-all binding)
|
||||
→ MoreThenCms.Web → C:\inetpub\wwwroot\MoreThenCms.Web\
|
||||
→ ASP.NET CMS code (.NET Framework 4.8)
|
||||
→ Connection strings:
|
||||
→ MSSQL: Data Source=10.0.2.2:1433 (VBox NAT gateway = host)
|
||||
→ MinIO/storage: TBD точная схема
|
||||
→ Elasticsearch: не используется CMS (там books-стек)
|
||||
→ MSSQL container на host:1433
|
||||
→ 5 production DB (MoreThenCms, Stayer*, stostayer, TireService)
|
||||
← HTTP response back through chain
|
||||
```
|
||||
|
||||
## Запущенные docker контейнеры на хосте
|
||||
|
||||
| Container | Image | Port (host) | Volume |
|
||||
|---|---|---|---|
|
||||
| **traefik** | `traefik:v2.6.6` | 4443, 8000, 8080 | named: `traefik_traefik_letsencrypt`; bind: `data/traefik.yml`, `data/custom/` |
|
||||
| **mssql** | `mcr.microsoft.com/mssql/server:2019-latest` | 1433 | named: `mssql_mssql_data` (filled from production tar) |
|
||||
| **minio** | `minio/minio:RELEASE.2020-07-13T18-09-56Z` | 9000 | bind: `./data` |
|
||||
| **imgproxy** | `darthsim/imgproxy:latest` | 8787 | (нет state) |
|
||||
| **imgproxy-nginx** | `nginx:alpine` | 8788 | bind: `./nginx/cache`, `./nginx/nginx.conf` |
|
||||
| **elasticsearch** | `elasticsearch:7.10.1` | 9200 | bind: `./data` |
|
||||
|
||||
Все на docker network `proxy` (external) — это позволяет traefik резолвить `minio`, `elasticsearch`, `imgproxy-nginx` напрямую по docker DNS.
|
||||
|
||||
## Traefik routes
|
||||
|
||||
13 client домен-маршрутов в `data/custom/`:
|
||||
|
||||
| File | Hosts | Backend |
|
||||
|---|---|---|
|
||||
| snolla.yml | snolla.com + 10 subdomains | host.docker.internal:18080 |
|
||||
| rimiz.yml | rimiz.ru, www.rimiz.ru | host.docker.internal:18080 |
|
||||
| labtools.yml | labtools.ru, www.labtools.ru | same |
|
||||
| labtoolspro.yml | labtools.pro, www.labtools.pro | same |
|
||||
| pilorama98.yml | pilorama98.ru, www.pilorama98.ru | same |
|
||||
| tandemmebel.yml | tandemmebel.ru, www.tandemmebel.ru | same |
|
||||
| emspb.yml | emspb.ru, www.emspb.ru | same |
|
||||
| kupimknigi.yml | kupimknigi.spb.ru | same |
|
||||
| maljarka.yml | maljarka.tandemmebel.ru | same |
|
||||
| sestech.yml | sestech.ru, www.sestech.ru | same |
|
||||
| isc-artmaterials.yml | ics-artmaterials.com, www.ics-artmaterials.com | same |
|
||||
| oldstostayer.yml | (старый stostayer) | host.docker.internal:18181 (VM port 8081) |
|
||||
| stostayer.yml | stostayer.ru или похожий | host.docker.internal:18180 (VM port 8080) |
|
||||
|
||||
Плюс file-provider маршруты для инфраструктурных хостов:
|
||||
|
||||
| File | Host | Backend |
|
||||
|---|---|---|
|
||||
| elasticsearch.yml | elasticold.kzntsv.site | http://elasticsearch:9200 + basicAuth `books:...` |
|
||||
| minio.yml | minio.kzntsv.site | http://minio:9000 |
|
||||
| imgproxy.yml | imgproxy.kzntsv.site | http://imgproxy-nginx:80 |
|
||||
|
||||
И мёртвые (не отключены, но смотрят в никуда):
|
||||
- `disk.yml` → 192.168.1.10:5005 (Synology disk на мёртвой синке)
|
||||
- `dsm.yml` → 192.168.1.10:5000 (DSM мёртвой синки)
|
||||
|
||||
## DNS
|
||||
|
||||
Все домены остались указывать на **94.19.247.14** (public IP, статический). REGRU как registrar/DNS. Никаких DNS-изменений во время recovery не требовалось — only router NAT перенастроен.
|
||||
|
||||
## Backup инфраструктура
|
||||
|
||||
Текущая (на момент 2026-05-19):
|
||||
|
||||
- [[kreknin-synology]] держит Hyper Backup репо мёртвой синки (~430 GB). Новых бэкапов с recovered Windows-PC **НЕТ** — рабочее состояние на Windows-PC не бэкапится никуда.
|
||||
- Локально на Windows-PC: `C:\nas-recovery\vm-sites\` (~11 GB, дамп IIS sites из VM) — резерв если VM полностью умрёт.
|
||||
|
||||
**Дыра:** если Windows-PC сгорит — ВСЁ ляжет. Никакой репликации, никакого off-site backup для нового рабочего состояния.
|
||||
|
||||
## SSH ключи и доступ
|
||||
|
||||
- [[windows-recovery-host]] → [[kreknin-synology]]: `id_ed25519_kreknin` (vitya@195.19.90.188)
|
||||
- [[windows-recovery-host]] → [[openwrt-router]]: `id_ed25519_openwrt` (root@192.168.1.1)
|
||||
- [[windows-recovery-host]] → [[snolla-recovery-vm]]: `id_ed25519_snolla_vm` (vitya@127.0.0.1:8022)
|
||||
|
||||
После recovery — отозвать публичные ключи Claude из этих 3 машин (`~/.ssh/authorized_keys` или эквивалент). См. соответствующие entity-страницы.
|
||||
|
||||
## Известные открытые баги
|
||||
|
||||
1. **X-Forwarded headers** не передаются от traefik в IIS → CMS делает редирект с `:4443` в URL. См. [[traefik-on-windows-docker-desktop]] Pitfall 5.
|
||||
2. **MinIO/Azure storage** в CMS — точная схема подключения TBD. Пользователь упомянул "не так всё".
|
||||
3. **acme.json HTTP-01 renewal** будет фейлить для доменов с DNS не на нашем IP — нужен переезд на DNS-01 через REGRU до истечения сертификатов (~3 месяца).
|
||||
4. **VM один раз "повисла" в сети** через 1-2 часа uptime — лечилось `ipconfig /release/renew` через VBoxManage guestcontrol. Нужен auto-watchdog.
|
||||
5. **C:\inetpub\logs\** в VM растёт (6+ GB к recovery) — нужна ротация.
|
||||
6. **docker.sock provider в traefik** не работает — но не блокирует (всё через file-provider). См. [[traefik-on-windows-docker-desktop]] Pitfall 3.
|
||||
|
||||
## Single Points of Failure
|
||||
|
||||
- Один Windows-PC (если сгорит — всё ляжет)
|
||||
- Один публичный IP / провайдер
|
||||
- Один WiFi-канал
|
||||
- Один OpenWRT-роутер
|
||||
- Один VBox VM (single instance, не replicate)
|
||||
- Один MSSQL контейнер (single primary, нет replica)
|
||||
- Один MinIO (single drive, не distributed)
|
||||
|
||||
Каждый SPOF — кандидат на улучшение в [[future-resilient-architecture-goals]].
|
||||
185
traefik-on-windows-docker-desktop.md
Normal file
185
traefik-on-windows-docker-desktop.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
title: Traefik на Windows Docker Desktop — нюансы
|
||||
type: concept
|
||||
tags: [traefik, docker, windows, letsencrypt, reverse-proxy]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Traefik на Windows Docker Desktop
|
||||
|
||||
Запуск traefik 2.6.6 на Windows Docker Desktop (WSL2 backend) с импортированным production-конфигом и сертификатами — пара ловушек.
|
||||
|
||||
## Pitfall 1: traefik.yml не загружается автоматом
|
||||
|
||||
Symptom: traefik стартует, но routers из `data/custom/*.yml` ругаются на "non-existent resolver: letsEncrypt", сертификаты не отдаются.
|
||||
|
||||
Reason: traefik 2.x при отсутствии `--configFile=` ищет в дефолтных путях (`/etc/traefik/`, `./traefik.yml`), но Linux-контейнер с CWD=`/` и bind-mount `./data/traefik.yml:/traefik.yml` — почему-то не подхватывает.
|
||||
|
||||
**Fix:** явно указать в compose `command:`:
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- "--configFile=/traefik.yml"
|
||||
- "--log.level=DEBUG"
|
||||
```
|
||||
|
||||
Подтверждение в логах: `Configuration loaded from file: /traefik.yml`.
|
||||
|
||||
## Pitfall 2: acme.json permissions через bind-mount
|
||||
|
||||
Symptom: traefik ругается `permissions 777 for /letsencrypt/acme.json are too open, please use 600` и **отключает** letsEncrypt resolver (даже если у него есть валидные certs).
|
||||
|
||||
Reason: bind-mount Windows-файла в Linux-контейнере **всегда** показывает permissions `0777`. Изменить через `chmod` нельзя — bind-mount не транслирует POSIX-perms на Windows-side.
|
||||
|
||||
**Fix:** named volume вместо bind для `/letsencrypt`:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "traefik_letsencrypt:/letsencrypt" # вместо ./letsencrypt:/letsencrypt
|
||||
- "./data/traefik.yml:/traefik.yml:ro"
|
||||
- "./data/custom/:/custom/:ro"
|
||||
|
||||
# и снизу:
|
||||
volumes:
|
||||
traefik_letsencrypt:
|
||||
```
|
||||
|
||||
Population volume единоразово:
|
||||
|
||||
```powershell
|
||||
docker volume create traefik_traefik_letsencrypt
|
||||
docker run --rm \
|
||||
-v traefik_traefik_letsencrypt:/dest \
|
||||
-v <local-path>/traefik/letsencrypt:/src:ro \
|
||||
alpine sh -c "cp /src/acme.json /dest/acme.json && cp /src/acme.old.json /dest/acme.old.json && chmod 600 /dest/*"
|
||||
```
|
||||
|
||||
## Pitfall 3: docker.sock provider не работает
|
||||
|
||||
Symptom:
|
||||
```
|
||||
Failed to retrieve information of the docker client and server host: Error response from daemon:
|
||||
providerName=docker
|
||||
```
|
||||
|
||||
С `-v /var/run/docker.sock:/var/run/docker.sock:ro` сокет монтируется (видно `srw-rw---- 1 root root` внутри контейнера), но соединение с daemon обрывается.
|
||||
|
||||
Reason: Docker Desktop на Windows транслирует docker.sock через WSL2 layer. Иногда permission-моэль клиента (libdocker) не принимает то, что предоставляет Desktop's proxy.
|
||||
|
||||
**Workaround:** **полностью отключить docker-provider, использовать только file-provider** для всех routes.
|
||||
|
||||
Маршруты, которые в производстве были как traefik labels на контейнерах (minio, elasticsearch, imgproxy с `traefik.http.routers...labels`), переписываются в `data/custom/<name>.yml` руками:
|
||||
|
||||
```yaml
|
||||
# data/custom/minio.yml
|
||||
http:
|
||||
routers:
|
||||
minio:
|
||||
entryPoints: [https]
|
||||
rule: Host(`minio.kzntsv.site`)
|
||||
tls:
|
||||
certResolver: letsEncrypt
|
||||
service: minio
|
||||
services:
|
||||
minio:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://minio:9000 # docker DNS name (работает потому что все на одной network=proxy)
|
||||
```
|
||||
|
||||
Подобно для elasticsearch (с basicAuth middleware), imgproxy (→ imgproxy-nginx:80), etc.
|
||||
|
||||
## Pitfall 4: file-provider не подхватывает изменения через bind-mount
|
||||
|
||||
`traefik.yml` имеет `providers.file.watch: true`, но на Windows bind-mount inotify не работает через WSL2-слой. Изменения в `data/custom/*.yml` не подхватываются автоматически.
|
||||
|
||||
**Workaround:** `docker compose restart traefik` после изменений в custom/. Несколько секунд downtime, ничего страшного.
|
||||
|
||||
## Production порты vs нашa конфигурация
|
||||
|
||||
Production traefik compose биндил на host:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "8000:80" # router forwards public 80 → host 8000 → container 80
|
||||
- "4443:443"
|
||||
- "8080:8080"
|
||||
```
|
||||
|
||||
То есть **роутер делает port-translation** 80→8000, 443→4443. Не стандартные порты, но работает.
|
||||
|
||||
На Windows-хосте оставили те же порты (8000/4443) — не конфликтуют с локальным IIS на 80 (который не используется, но не выключен). И не требуют admin для bind.
|
||||
|
||||
## host.docker.internal
|
||||
|
||||
Из traefik-контейнера достучаться до VM (которая в VBox NAT, не в docker network) — через **`host.docker.internal`**:
|
||||
|
||||
```yaml
|
||||
servers:
|
||||
- url: http://host.docker.internal:18080/ # 18080 = VBox NAT-forwarded port → VM:80
|
||||
```
|
||||
|
||||
Docker Desktop резолвит `host.docker.internal` в IP хоста (обычно 172.x.x.1 из docker bridge perspective). Дальше Windows-host обрабатывает 18080 → VBox NAT → VM:80 → IIS → CMS.
|
||||
|
||||
## Pitfall 5: X-Forwarded headers НЕ настроены
|
||||
|
||||
Traefik отдаёт backend (IIS) запрос с заголовком `Host: snolla.com`, но **не** добавляет `X-Forwarded-Proto: https` / `X-Forwarded-Host`. Backend (IIS) считает, что получил HTTP-запрос, делает 301 redirect на канонический URL `http://www.<domain>:4443/`.
|
||||
|
||||
Симптом для клиента: видит URL с портом 4443 в адресной строке после редиректа.
|
||||
|
||||
**Fix (TODO, не делали в emergency):**
|
||||
|
||||
```yaml
|
||||
# data/custom/forwarded-headers-middleware.yml
|
||||
http:
|
||||
middlewares:
|
||||
secure-headers:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: https
|
||||
customResponseHeaders:
|
||||
X-Forwarded-Proto: ""
|
||||
```
|
||||
|
||||
И применить middleware к каждому router (или к глобальному entrypoint-level через `--entrypoints.https.forwardedheaders.insecure=true` в command).
|
||||
|
||||
Plus на стороне IIS: установить ARR module + URL Rewrite, доверять `X-Forwarded-*` от 127.0.0.1 (host).
|
||||
|
||||
## DNS-01 challenge через REGRU
|
||||
|
||||
`traefik.yml` имеет HTTP-01 challenge:
|
||||
|
||||
```yaml
|
||||
certificatesResolvers:
|
||||
letsEncrypt:
|
||||
acme:
|
||||
email: vitya.kuznetsov@gmail.com
|
||||
storage: /letsencrypt/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: http
|
||||
```
|
||||
|
||||
В compose env уже:
|
||||
```
|
||||
REGRU_USERNAME=OpeItcLoc03
|
||||
REGRU_PASSWORD=ytyYqC%u%QAJ
|
||||
```
|
||||
|
||||
Эти креды для **DNS-01** через REGRU API. Просто закомментировать `httpChallenge` и активировать `dnsChallenge` в traefik.yml:
|
||||
|
||||
```yaml
|
||||
certificatesResolvers:
|
||||
letsEncrypt:
|
||||
acme:
|
||||
email: ...
|
||||
storage: /letsencrypt/acme.json
|
||||
dnsChallenge:
|
||||
provider: regru
|
||||
```
|
||||
|
||||
DNS-01 более надёжный (не требует public:80 reachable для validation) и работает даже если HTTP-01 challenge не пройдёт (например, домен временно на другой хостинге).
|
||||
|
||||
**Текущие 40 LE-сертификатов из acme.json валидны ~3 месяца** (LE default). Когда подойдут к истечению — пора переключать на DNS-01.
|
||||
|
||||
Связано: [[snolla-recovery-vm]], [[windows-recovery-host]], [[recovery-architecture-snapshot]].
|
||||
140
vbox-windows-stability-tuning.md
Normal file
140
vbox-windows-stability-tuning.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: VirtualBox + Windows-гость — нюансы стабильности при cross-hypervisor миграции
|
||||
type: concept
|
||||
tags: [virtualbox, windows, kvm, migration, troubleshooting]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# VirtualBox + Windows-гость: cross-hypervisor migration
|
||||
|
||||
Импорт OVA с Synology VMM (KVM-based) в VirtualBox прошёл через 3 круга проблем. Записано чтобы в следующий раз не танцевать. Касается [[snolla-recovery-vm]].
|
||||
|
||||
## Симптом исходный
|
||||
|
||||
OVA импортируется через `VBoxManage import`, VM стартует, Windows валится в WinRE ("Восстановление при загрузке не удалось восстановить компьютер"). Startup Repair не помогает.
|
||||
|
||||
## Цепочка фиксов (применять по порядку)
|
||||
|
||||
### 1. Storage controller: SCSI LsiLogic → SATA AHCI
|
||||
|
||||
OVA с KVM-источника обычно имеет SCSI LsiLogic. Windows-гость не имеет встроенного boot-driver для VBox's LsiLogic emulation. SATA AHCI — generic, поддерживается любым Windows из коробки.
|
||||
|
||||
```
|
||||
VBoxManage controlvm "<vm>" poweroff
|
||||
VBoxManage storageattach "<vm>" --storagectl "SCSI" --port 0 --device 0 --medium none
|
||||
VBoxManage storagectl "<vm>" --name "SCSI" --remove
|
||||
VBoxManage storagectl "<vm>" --name "SATA" --add sata --controller IntelAhci --portcount 4
|
||||
VBoxManage storageattach "<vm>" --storagectl "SATA" --port 0 --device 0 --type hdd --medium "<vmdk-path>"
|
||||
```
|
||||
|
||||
После этого Windows бутится дальше WinRE → доходит до login screen.
|
||||
|
||||
### 2. Hyper-V Virtualization Infrastructure Driver — disable в Safe Mode
|
||||
|
||||
После login Windows зависает в **чёрный экран после Welcome**. Виновник — Microsoft Hyper-V virtualization infrastructure driver, который остался от Synology VMM/KVM. Под VBox он не находит свой target hypervisor → виснет.
|
||||
|
||||
Доступ: жмёшь power → Shift+Restart → Recovery → Troubleshoot → Advanced Options → Startup Settings → Restart → **4 (Safe Mode)** или **5 (Safe Mode with Networking)**.
|
||||
|
||||
В Safe Mode:
|
||||
- Device Manager → Системные устройства → **Драйвер инфраструктуры виртуализации Microsoft Hyper-V** → правый клик → **Отключить устройство** (Disable, не удалять — потом можно вернуть).
|
||||
- Параллельно почистить "Другие устройства" с жёлтыми треугольниками (audio-controller, base system device) — uninstall, без удаления драйверов.
|
||||
|
||||
Reboot нормально → Windows загружается до desktop.
|
||||
|
||||
### 3. Paravirt provider: default → kvm
|
||||
|
||||
После пары часов uptime VM начинает виснуть рандомно. Корень — несоответствие paravirt-интерфейса между source-гипервизором (Synology VMM = KVM) и VBox default (`default`/`auto`, который пытается подружиться с гостем но не угадывает).
|
||||
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --paravirtprovider kvm
|
||||
```
|
||||
|
||||
Windows-гость, который изначально загружал KVM paravirt drivers (virtio?), теперь под VBox видит знакомый интерфейс → стабильнее.
|
||||
|
||||
### 4. OS type: Other_64 → Windows10_64
|
||||
|
||||
VBox по умолчанию ставит OS type = `Other/Unknown (64-bit)` при импорте OVA с неузнанной маркировкой. Это означает дефолтные acceleration settings, которые могут не подходить Windows.
|
||||
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --ostype Windows10_64
|
||||
```
|
||||
|
||||
Включает VBox-внутренние оптимизации для Windows (HPET off, large pages on, и др.).
|
||||
|
||||
### 5. HPET off, vCPU 2 (а не 4), RAM 4 GB
|
||||
|
||||
- HPET (High Precision Event Timer) — для Windows-гостя на VBox чаще создаёт jitter чем помогает. Off:
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --hpet off
|
||||
```
|
||||
- vCPU: 4 на 2-ядерном/4-ядерном хосте может создавать contention. Снизить до 2:
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --cpus 2
|
||||
```
|
||||
- RAM: 4 GB достаточно для IIS + CMS + Windows на легкой нагрузке.
|
||||
|
||||
### 6. Установить **VirtualBox Guest Additions**
|
||||
|
||||
Без GA Windows использует generic Microsoft драйверы для VBox-эмулированного железа. С GA — нативные оптимизированные VBox-драйверы для сети/видео/storage/устройств.
|
||||
|
||||
**Установить через VRDE-консоль** (mstsc к `localhost:13389`), а не через сетевой RDP — потому что сеть может умереть до установки GA.
|
||||
|
||||
Шаги:
|
||||
1. На хосте: `VBoxManage storagectl "<vm>" --name "IDE" --add ide --controller PIIX4` (новый контроллер для DVD)
|
||||
2. `VBoxManage storageattach "<vm>" --storagectl "IDE" --port 0 --device 0 --type dvddrive --medium "C:\Program Files\Oracle\VirtualBox\VBoxGuestAdditions.iso"`
|
||||
3. В VM: Win+E → D: drive → запустить `VBoxWindowsAdditions.exe` → Next/Install/доверять Oracle publisher → Restart.
|
||||
|
||||
После: `GuestAdditionsRunLevel=3` (полностью активны). VM существенно стабильнее.
|
||||
|
||||
## Network nuances
|
||||
|
||||
### Bridged WiFi нестабильно
|
||||
|
||||
Если хост-машина подключена по WiFi и VBox NIC = bridged через WiFi adapter — частые проблемы с promiscuous mode. Симптомы:
|
||||
- VM получает IP по DHCP
|
||||
- Работает 10-60 минут
|
||||
- Network "повисает": TCP-handshake проходит, но трафик не идёт
|
||||
- ARP-table показывает MAC, но State=`Stale`
|
||||
|
||||
Решение: **NAT с port forwarding** вместо bridged.
|
||||
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --nic1 nat
|
||||
VBoxManage controlvm "<vm>" natpf1 "rdp,tcp,127.0.0.1,23389,,3389"
|
||||
VBoxManage controlvm "<vm>" natpf1 "ssh,tcp,127.0.0.1,8022,,22"
|
||||
VBoxManage controlvm "<vm>" natpf1 "http,tcp,127.0.0.1,18080,,80"
|
||||
# ...и так далее на нужные порты
|
||||
```
|
||||
|
||||
VM получает 10.0.2.15 (default NAT subnet). Host достижим из VM по 10.0.2.2 (NAT gateway).
|
||||
|
||||
### Network recovery inside Windows VM
|
||||
|
||||
Если внутри VM network "повис" (бывает даже с GA + NAT):
|
||||
|
||||
```
|
||||
VBoxManage guestcontrol "<vm>" run --exe "C:\Windows\System32\cmd.exe" \
|
||||
--username vitya --password '<pw>' --wait-stdout --wait-stderr \
|
||||
-- cmd.exe /c "ipconfig /release && ipconfig /renew"
|
||||
```
|
||||
|
||||
Через GA это работает не требуя SSH/RDP связи.
|
||||
|
||||
## VRDE backup-доступ
|
||||
|
||||
Всегда включён в нашей конфигурации как fallback:
|
||||
|
||||
```
|
||||
VBoxManage controlvm "<vm>" vrde on
|
||||
VBoxManage controlvm "<vm>" vrdeport 13389
|
||||
```
|
||||
|
||||
`mstsc → localhost:13389` показывает VM-консоль независимо от состояния сети в VM. Полезно для recovery когда RDP в VM умер.
|
||||
|
||||
## Что не сработало
|
||||
|
||||
- Hyper-V на хосте — рассматривался как cleaner альтернатива для Windows-гостя, но требует доустановки Hyper-V Manager и перезагрузки хоста. Отложено как Plan B, не понадобилось.
|
||||
- VMware Workstation Pro 17 — бесплатен с 2024, но та же migration-головная боль на Windows-госте с другого гипервизора.
|
||||
|
||||
Связано: [[snolla-recovery-vm]], [[windows-recovery-host]].
|
||||
76
wd40efax-smr-cascade.md
Normal file
76
wd40efax-smr-cascade.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
|
||||
type: concept
|
||||
tags: [hardware, raid, smr, failure-mode, wd, lessons]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# WD40EFAX SMR Cascade
|
||||
|
||||
Сценарий смерти RAID 5 на WD Red WD40EFAX (SMR-диски). Случился 2026-05-18 на [[dead-synology-diskstation]]. Известная community-проблема, не уникальная.
|
||||
|
||||
## Что такое SMR
|
||||
|
||||
**SMR** (Shingled Magnetic Recording) — способ записи на пластины, где дорожки наезжают друг на друга как черепица. Плюс: больше плотность, дешевле гигабайт. Минус: запись на одну дорожку **физически портит соседние** → нужна re-write whole "zone". Запись стала зональной (вместо random-access).
|
||||
|
||||
**CMR** (Conventional Magnetic Recording) — стандарт, дорожки независимые.
|
||||
|
||||
### Где SMR ломается
|
||||
|
||||
| Сценарий | CMR | SMR |
|
||||
|---|---|---|
|
||||
| Sequential write | ✅ | ✅ |
|
||||
| Чтение | ✅ | ✅ |
|
||||
| Random writes (БД, активная FS) | ✅ | ❌ просаживается |
|
||||
| **RAID rebuild** | ✅ | ❌❌ **смертельно** |
|
||||
|
||||
Во время RAID-rebuild диск получает **долгую sustained-нагрузку**: parity-чтение + запись на новый диск. SMR-firmware пытается жонглировать зонами; внутренний CMR-кэш (есть в начале диска) забивается. Performance падает в 5-10 раз → **command timeouts** → RAID-контроллер выкидывает диск из массива.
|
||||
|
||||
## Скандал WD
|
||||
|
||||
WD в 2018-2020 **тихо** перевёл часть линейки "WD Red" (заточена под NAS) на SMR, не указав в маркировке. Community catch'нуло (Reddit, Servethehome, forum.synology). Class-action в США 2020, WD settled. После — WD переименовал CMR-варианты в "WD Red **Plus**" / "WD Red **Pro**"; "WD Red" без Plus остался SMR.
|
||||
|
||||
**WD40EFAX-68JH4N1, WD40EFAX-68JN4N0** — главные жертвы. Если в RAID — лотерея.
|
||||
|
||||
## Конкретный путь к смерти (наш кейс)
|
||||
|
||||
1. **3-диск RAID 5** на WD40EFAX. Storage pool в DSM на `cachedev_0`, 7 TB usable.
|
||||
2. **Начало мая 2026** — один из 3 дисков вылетел (вероятно тайм-аут под нагрузкой, не физический отказ).
|
||||
3. **Пул degraded.** RAID 5 на 3 дисках теперь толерирует 0 дополнительных отказов.
|
||||
4. **2 недели пользователь не заменил failed диск.** Пул работал в degraded; оставшиеся 2 SMR-диска делают parity-чтение для любого запроса.
|
||||
5. **Под этой нагрузкой второй WD40EFAX накопил тайм-ауты** (известный паттерн для SMR в degraded-RAID).
|
||||
6. **2026-05-18 ~17:00 MSK** — второй вылет. Пул past redundancy, "Сбой сборки" в DSM Storage Manager.
|
||||
7. **Виден только Disk 3 + Disk 8** (третий, который вылетел первым, физически не определяется системой даже).
|
||||
|
||||
## Ключевая ошибка
|
||||
|
||||
**2 недели в degraded не лечатся.** В нормальном RAID 5 на CMR — можно прожить недели без последствий (всё работает). На SMR — каждый день в degraded **повышает шанс второго отказа** из-за SMR-induced таймаутов.
|
||||
|
||||
Правило: при SMR в RAID 5 — **24-48 часов** на замену failed диска. Дольше — лотерея. Поэтому SMR в RAID **запрещён de facto** для серьёзных продакшнов.
|
||||
|
||||
## Что НЕ делать после второго отказа
|
||||
|
||||
- ❌ Repair / Online Assembly в DSM — пул past redundancy, mdadm не соберёт.
|
||||
- ❌ Менять disks в degraded-пуле — может ускорить deterioration оставшихся.
|
||||
- ❌ Запускать `mdadm --assemble` руками с force — без знания внутренней структуры → разрушение partial-data.
|
||||
|
||||
## Что МОЖНО (но дорого)
|
||||
|
||||
- **Pro data recovery** (Storelab/R.LAB/Ace Lab клиенты): $500-3000 typical. Контора берёт диски (все 3, включая failed), делает offline-reconstruct parity, восстанавливает file-tree. Подходит для возврата 9-дневного окна между последним бэкапом (2026-05-09) и инцидентом (2026-05-18).
|
||||
- **Условие:** диски физически живы (головки не упали). У нас 2 видимых "Исправно" + 1 не определяемый — стандартный кейс для recovery service.
|
||||
|
||||
## Lessons для будущего
|
||||
|
||||
Для следующего NAS-пула:
|
||||
|
||||
1. **CMR-only.** Никаких WD40EFAX/EFRX/EFAZ/EFGX (если они SMR). Кандидаты:
|
||||
- WD Red **Plus** (CMR, маркировка "Plus" — важно)
|
||||
- WD Red **Pro** (CMR, enterprise-grade)
|
||||
- Seagate IronWolf 4TB+ (CMR — модели <4TB могут быть SMR, проверять по datasheet)
|
||||
- HGST/WD Ultrastar (enterprise CMR)
|
||||
2. **RAID 6 / SHR-2** при 4+ дисках — толерирует 2 отказа. Один отказ + один SMR-cascade не убивает массив.
|
||||
3. **Дисциплина replace failed disk в 24-48 часов** — в degraded долго не сидеть.
|
||||
4. **Hot spare** если есть place в шасси.
|
||||
5. **Hyper Backup ежедневно** (а не "по триггеру") + retention 30+ дней.
|
||||
6. **Тест восстановления раз в квартал** — мы впервые узнали что наш backup рабочий **только когда случился инцидент**. Это нехорошо.
|
||||
Reference in New Issue
Block a user