伺服器監控與維運
日誌集中管理(Serilog + Seq/ELK)
💡 比喻:醫院的病歷系統
- 沒有集中日誌:每個醫生手寫病歷,放在自己抽屜裡。要查病史?去每個診間翻!
- 有集中日誌:所有病歷電子化,存在中央系統。任何醫生都能查詢任何病人的完整病史。
伺服器的日誌就像病歷——你需要一個中央系統來統一收集、查詢、分析。
安裝 Serilog
# 安裝 Serilog 相關 NuGet 套件
dotnet add package Serilog.AspNetCore # Serilog ASP.NET Core 整合
dotnet add package Serilog.Sinks.Console # 輸出到終端機
dotnet add package Serilog.Sinks.File # 輸出到檔案
dotnet add package Serilog.Sinks.Seq # 輸出到 Seq 日誌平台
dotnet add package Serilog.Enrichers.Environment # 加入環境資訊
dotnet add package Serilog.Enrichers.Thread # 加入執行緒資訊
設定 Serilog
// Program.cs 設定 Serilog
using Serilog; // 引用 Serilog 命名空間
// 設定 Serilog Logger
Log.Logger = new LoggerConfiguration() // 建立 Logger 設定
.MinimumLevel.Information() // 最低記錄等級為 Information
.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) // Microsoft 的只記錄 Warning
.MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning) // System 的只記錄 Warning
.Enrich.FromLogContext() // 加入 Log 上下文資訊
.Enrich.WithEnvironmentName() // 加入環境名稱(Development/Production)
.Enrich.WithMachineName() // 加入機器名稱
.Enrich.WithThreadId() // 加入執行緒 ID
.WriteTo.Console( // 輸出到終端機
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" // 格式範本
)
.WriteTo.File( // 輸出到檔案
path: "logs/app-.log", // 檔案路徑(自動加日期)
rollingInterval: RollingInterval.Day, // 每天產生新檔案
retainedFileCountLimit: 30, // 保留最近 30 天的日誌
fileSizeLimitBytes: 10_000_000, // 每個檔案最大 10MB
rollOnFileSizeLimit: true // 超過大小就新建檔案
)
.WriteTo.Seq("http://localhost:5341") // 輸出到 Seq 日誌平台
.CreateLogger(); // 建立 Logger
try // 包在 try-catch 中保護啟動過程
{
Log.Information("應用程式啟動中..."); // 記錄啟動訊息
var builder = WebApplication.CreateBuilder(args); // 建構器
builder.Host.UseSerilog(); // 用 Serilog 取代內建日誌
var app = builder.Build(); // 建構應用程式
app.UseSerilogRequestLogging(options => // 記錄每個 HTTP 請求
{
options.MessageTemplate = // 自訂訊息範本
"{RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";
});
app.Run(); // 啟動應用程式
}
catch (Exception ex) // 捕捉啟動時的錯誤
{
Log.Fatal(ex, "應用程式啟動失敗"); // 記錄致命錯誤
}
finally // 無論成功失敗都執行
{
Log.CloseAndFlush(); // 確保所有日誌都寫入完畢
}
結構化日誌的威力
// 在 Controller 或 Service 中使用結構化日誌
public class OrderService // 訂單服務
{
private readonly ILogger<OrderService> _logger; // 注入 Logger
public OrderService(ILogger<OrderService> logger) // 建構函式
{
_logger = logger; // 儲存 Logger
}
public async Task<Order> CreateOrder(int userId, decimal amount) // 建立訂單
{
// 結構化日誌:用 {@} 記錄物件,用 {} 記錄純值
_logger.LogInformation( // 記錄訂單建立資訊
"建立訂單:使用者 {UserId},金額 {Amount:C},時間 {OrderTime}", // 訊息範本
userId, amount, DateTime.UtcNow // 參數值(會被結構化儲存)
);
try // 嘗試建立訂單
{
var order = new Order { UserId = userId, Amount = amount }; // 建立訂單物件
// 儲存訂單... // 資料庫操作
_logger.LogInformation("訂單 {OrderId} 建立成功", order.Id); // 記錄成功
return order; // 回傳訂單
}
catch (Exception ex) // 捕捉錯誤
{
_logger.LogError(ex, // 記錄錯誤(包含例外物件)
"訂單建立失敗:使用者 {UserId},金額 {Amount}", // 錯誤訊息
userId, amount // 參數值
);
throw; // 重新拋出例外
}
}
}
安裝 Seq 日誌平台
# 使用 Docker 安裝 Seq
docker run -d \ # 背景執行容器
--name seq \ # 容器名稱
-e ACCEPT_EULA=Y \ # 接受使用者授權合約
-p 5341:80 \ # 對應 Port(本機 5341 到容器 80)
datalust/seq:latest # 使用最新版 Seq 映像
# 開啟瀏覽器到 http://localhost:5341 就可以查看日誌儀表板
# Seq 提供強大的查詢語法:
# UserId = 123 # 查詢特定使用者
# @Level = 'Error' # 查詢所有錯誤
# Amount > 1000 # 查詢大金額訂單
# RequestPath like '/api/%' # 查詢 API 路徑
Health Check Endpoint (/health)
💡 比喻:定期健康檢查 Health Check 就像員工每年的健康檢查:
- 量血壓(檢查資料庫連線)
- 驗血(檢查記憶體使用量)
- 心電圖(檢查外部 API 回應時間) 如果任何一項不正常,就要發出警報。
完整 Health Check 設定
// 自訂 Health Check 類別
using Microsoft.Extensions.Diagnostics.HealthChecks; // 健康檢查命名空間
public class DiskSpaceHealthCheck : IHealthCheck // 實作健康檢查介面
{
public Task<HealthCheckResult> CheckHealthAsync( // 檢查方法
HealthCheckContext context, // 檢查上下文
CancellationToken cancellationToken = default) // 取消令牌
{
var drive = new DriveInfo("C"); // 取得 C 槽資訊
var freeSpacePercent = (double)drive.AvailableFreeSpace / drive.TotalSize * 100; // 計算剩餘空間百分比
if (freeSpacePercent < 5) // 剩餘空間小於 5%
{
return Task.FromResult(HealthCheckResult.Unhealthy( // 不健康
$"磁碟空間嚴重不足:剩餘 {freeSpacePercent:F1}%" // 錯誤訊息
));
}
if (freeSpacePercent < 15) // 剩餘空間小於 15%
{
return Task.FromResult(HealthCheckResult.Degraded( // 效能降低
$"磁碟空間偏低:剩餘 {freeSpacePercent:F1}%" // 警告訊息
));
}
return Task.FromResult(HealthCheckResult.Healthy( // 健康
$"磁碟空間正常:剩餘 {freeSpacePercent:F1}%" // 正常訊息
));
}
}
// Program.cs 註冊 Health Check
var builder = WebApplication.CreateBuilder(args); // 建構器
builder.Services.AddHealthChecks() // 註冊健康檢查
.AddSqlServer( // SQL Server 檢查
builder.Configuration.GetConnectionString("DefaultConnection"), // 連線字串
name: "sqlserver", // 檢查名稱
failureStatus: HealthStatus.Unhealthy, // 失敗時的狀態
tags: new[] { "db", "critical" }) // 標籤分類
.AddRedis( // Redis 檢查
"localhost:6379", // 連線位址
name: "redis", // 檢查名稱
tags: new[] { "cache" }) // 標籤
.AddCheck<DiskSpaceHealthCheck>( // 自訂磁碟檢查
"disk-space", // 檢查名稱
tags: new[] { "infrastructure" }); // 標籤
var app = builder.Build(); // 建構
// 基本健康端點
app.MapHealthChecks("/health"); // 對應到 /health
// 詳細健康端點(含每個檢查項目的結果)
app.MapHealthChecks("/health/detail", new HealthCheckOptions // 詳細端點
{
ResponseWriter = async (context, report) => // 自訂回應格式
{
context.Response.ContentType = "application/json"; // JSON 格式
var result = new // 建立回應物件
{
status = report.Status.ToString(), // 整體狀態
checks = report.Entries.Select(e => new // 各項目狀態
{
name = e.Key, // 項目名稱
status = e.Value.Status.ToString(), // 項目狀態
description = e.Value.Description, // 描述
duration = e.Value.Duration.TotalMilliseconds // 檢查花費時間
})
};
await context.Response.WriteAsJsonAsync(result); // 寫入 JSON 回應
}
});
app.Run(); // 啟動
APM(Application Performance Monitoring)
💡 比喻:汽車的儀表板 APM 就像汽車的儀表板:
- 時速表 → 回應時間
- 轉速表 → CPU 使用率
- 油量表 → 記憶體使用量
- 引擎警示燈 → 錯誤率
沒有 APM,就像開車沒有儀表板——出問題時完全不知道原因。
使用 OpenTelemetry
// 安裝 NuGet 套件
// dotnet add package OpenTelemetry.Extensions.Hosting // 主機整合
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore // ASP.NET Core 監控
// dotnet add package OpenTelemetry.Instrumentation.Http // HTTP 請求監控
// dotnet add package OpenTelemetry.Instrumentation.SqlClient // SQL 監控
// dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore // Prometheus 匯出
// Program.cs 設定 OpenTelemetry
using OpenTelemetry.Metrics; // 指標命名空間
using OpenTelemetry.Trace; // 追蹤命名空間
var builder = WebApplication.CreateBuilder(args); // 建構器
// 設定追蹤(Tracing)
builder.Services.AddOpenTelemetry() // 加入 OpenTelemetry
.WithTracing(tracing => // 設定追蹤
{
tracing
.AddAspNetCoreInstrumentation() // 監控 ASP.NET Core 請求
.AddHttpClientInstrumentation() // 監控 HttpClient 呼叫
.AddSqlClientInstrumentation(options => // 監控 SQL 查詢
{
options.SetDbStatementForText = true; // 記錄 SQL 語句文字
})
.AddConsoleExporter(); // 輸出到終端機(開發用)
})
.WithMetrics(metrics => // 設定指標
{
metrics
.AddAspNetCoreInstrumentation() // ASP.NET Core 指標
.AddHttpClientInstrumentation() // HTTP 客戶端指標
.AddRuntimeInstrumentation() // .NET Runtime 指標
.AddProcessInstrumentation() // 程序指標
.AddPrometheusExporter(); // 匯出到 Prometheus
});
var app = builder.Build(); // 建構
app.MapPrometheusScrapingEndpoint("/metrics"); // Prometheus 抓取端點
app.Run(); // 啟動
自訂指標
// 建立自訂指標來追蹤業務數據
using System.Diagnostics.Metrics; // 指標命名空間
public class OrderMetrics // 訂單指標類別
{
private readonly Counter<long> _ordersCreated; // 訂單建立計數器
private readonly Histogram<double> _orderAmount; // 訂單金額直方圖
private readonly UpDownCounter<int> _activeOrders; // 活躍訂單數量
public OrderMetrics(IMeterFactory meterFactory) // 透過 DI 注入
{
var meter = meterFactory.Create("MyApp.Orders"); // 建立指標計量器
_ordersCreated = meter.CreateCounter<long>( // 計數器:只增不減
"orders.created", // 指標名稱
unit: "orders", // 單位
description: "建立的訂單總數" // 描述
);
_orderAmount = meter.CreateHistogram<double>( // 直方圖:記錄數值分布
"orders.amount", // 指標名稱
unit: "TWD", // 單位(新台幣)
description: "訂單金額分布" // 描述
);
_activeOrders = meter.CreateUpDownCounter<int>( // 上下計數器:可增可減
"orders.active", // 指標名稱
description: "目前活躍的訂單數" // 描述
);
}
public void OrderCreated(decimal amount, string region) // 記錄訂單建立
{
_ordersCreated.Add(1, new("region", region)); // 計數加 1,附帶地區標籤
_orderAmount.Record((double)amount); // 記錄金額
_activeOrders.Add(1); // 活躍訂單加 1
}
public void OrderCompleted() // 記錄訂單完成
{
_activeOrders.Add(-1); // 活躍訂單減 1
}
}
記憶體洩漏排查
💡 比喻:水龍頭沒關好 記憶體洩漏就像水龍頭沒關好:
- 水一滴一滴地流(記憶體一點一點地增加)
- 短時間看不出問題(應用剛啟動很正常)
- 時間一長水桶就滿了(記憶體用完就崩潰 OOM) 排查就是找到哪個水龍頭沒關好。
使用 dotnet 診斷工具
# 安裝 .NET 診斷工具
dotnet tool install -g dotnet-counters # 即時效能計數器
dotnet tool install -g dotnet-dump # 記憶體傾印分析
dotnet tool install -g dotnet-trace # 效能追蹤
dotnet tool install -g dotnet-gcdump # GC 堆積傾印
# 使用 dotnet-counters 即時監控
dotnet-counters monitor -p <PID> # 監控指定程序的計數器
# 會顯示:
# CPU 使用率、記憶體使用量、GC 次數、執行緒數量
# Exception 數量、HTTP 請求速率等
# 監控特定計數器
dotnet-counters monitor -p <PID> \ # 指定程序 ID
--counters System.Runtime,Microsoft.AspNetCore.Hosting # 指定要監控的計數器類別
# 收集記憶體傾印
dotnet-dump collect -p <PID> # 收集記憶體快照
# 會產生一個 .dmp 檔案
# 分析記憶體傾印
dotnet-dump analyze <dump-file> # 開啟分析互動介面
# 常用分析命令:
# > dumpheap -stat # 查看堆積統計(哪個類型佔最多記憶體)
# > dumpheap -type System.String # 查看所有字串物件
# > gcroot <address> # 查看物件被誰參考(為什麼無法回收)
# 使用 dotnet-trace 追蹤效能
dotnet-trace collect -p <PID> \ # 收集效能追蹤資料
--duration 00:00:30 # 追蹤 30 秒
# 產生的 .nettrace 檔案可以用 Visual Studio 或 PerfView 開啟
常見記憶體洩漏原因
// ❌ 洩漏原因一:事件處理器沒有取消訂閱
public class LeakyService // 有洩漏的服務
{
public LeakyService(EventBus bus) // 建構函式
{
bus.OnOrderCreated += HandleOrder; // 訂閱事件,但從沒取消!
}
// 即使 LeakyService 不再使用,EventBus 仍然持有它的參考
// 垃圾回收器無法回收它
private void HandleOrder(Order order) { } // 事件處理方法
}
// ✅ 正確做法:實作 IDisposable 來取消訂閱
public class FixedService : IDisposable // 實作 IDisposable
{
private readonly EventBus _bus; // 儲存事件匯流排參考
public FixedService(EventBus bus) // 建構函式
{
_bus = bus; // 儲存參考
_bus.OnOrderCreated += HandleOrder; // 訂閱事件
}
public void Dispose() // 釋放資源
{
_bus.OnOrderCreated -= HandleOrder; // 取消訂閱事件!
}
private void HandleOrder(Order order) { } // 事件處理方法
}
// ❌ 洩漏原因二:靜態集合不斷增長
public static class Cache // 靜態快取
{
private static readonly Dictionary<string, object> // 永遠不會被清除的字典
_items = new(); // 只增不減
public static void Add(string key, object value) // 只有新增
{
_items[key] = value; // 加進去就永遠在那裡
}
// 沒有清除機制,記憶體會不斷增長
}
// ✅ 正確做法:使用 MemoryCache(有過期機制)
builder.Services.AddMemoryCache(); // 註冊記憶體快取服務
public class MyService // 服務類別
{
private readonly IMemoryCache _cache; // 注入快取
public MyService(IMemoryCache cache) => _cache = cache; // 建構函式
public void CacheData(string key, object value) // 快取資料
{
_cache.Set(key, value, TimeSpan.FromMinutes(30)); // 設定 30 分鐘過期
}
}
自動重啟與 Process Manager
💡 比喻:值班護士 Process Manager 就像醫院的值班護士:
- 定時巡房(監控程序狀態)
- 病人有異狀就按鈴(程序崩潰就重啟)
- 換班時交接(應用程式更新時平滑切換)
- 記錄巡房日誌(記錄重啟紀錄)
使用 systemd(Linux)
# 建立 systemd 服務設定檔
sudo nano /etc/systemd/system/myapp.service # 編輯服務設定檔
# /etc/systemd/system/myapp.service
[Unit]
Description=My ASP.NET Core App # 服務描述
After=network.target # 在網路啟動後才啟動
[Service]
WorkingDirectory=/var/www/myapp # 應用程式工作目錄
ExecStart=/usr/bin/dotnet /var/www/myapp/MyApp.dll # 啟動命令
Restart=always # 永遠自動重啟
RestartSec=10 # 重啟前等待 10 秒
SyslogIdentifier=myapp # 系統日誌識別名稱
User=www-data # 以 www-data 使用者身分執行
Group=www-data # 使用者群組
Environment=ASPNETCORE_ENVIRONMENT=Production # 環境設定
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false # 關閉遙測訊息
LimitNOFILE=65536 # 最大開啟檔案數
TimeoutStopSec=30 # 停止超時時間
[Install]
WantedBy=multi-user.target # 多使用者模式啟動
# 管理 systemd 服務
sudo systemctl daemon-reload # 重新載入 systemd 設定
sudo systemctl enable myapp # 設定開機自動啟動
sudo systemctl start myapp # 啟動服務
sudo systemctl status myapp # 查看服務狀態
sudo systemctl stop myapp # 停止服務
sudo systemctl restart myapp # 重啟服務
# 查看服務日誌
sudo journalctl -u myapp -f # 即時查看日誌(-f 持續追蹤)
sudo journalctl -u myapp --since "1 hour ago" # 查看最近一小時的日誌
sudo journalctl -u myapp --since today -p err # 查看今天的錯誤日誌
部署腳本
#!/bin/bash
# deploy.sh - 自動部署腳本
APP_NAME="myapp" # 應用程式名稱
DEPLOY_DIR="/var/www/$APP_NAME" # 部署目錄
PUBLISH_DIR="./publish" # 發佈輸出目錄
BACKUP_DIR="/var/www/backups/$APP_NAME" # 備份目錄
echo "開始部署 $APP_NAME..." # 顯示部署開始
# 步驟 1:建置發佈
echo "步驟 1:建置發佈版本" # 提示訊息
dotnet publish -c Release -o $PUBLISH_DIR # 以 Release 模式發佈
# 步驟 2:備份目前版本
echo "步驟 2:備份目前版本" # 提示訊息
TIMESTAMP=$(date +%Y%m%d_%H%M%S) # 產生時間戳記
mkdir -p $BACKUP_DIR # 建立備份目錄
cp -r $DEPLOY_DIR $BACKUP_DIR/$TIMESTAMP # 複製目前版本到備份
# 步驟 3:停止服務
echo "步驟 3:停止服務" # 提示訊息
sudo systemctl stop $APP_NAME # 停止服務
# 步驟 4:部署新版本
echo "步驟 4:部署新版本" # 提示訊息
rm -rf $DEPLOY_DIR/* # 清除舊檔案
cp -r $PUBLISH_DIR/* $DEPLOY_DIR/ # 複製新版本
# 步驟 5:重啟服務
echo "步驟 5:重啟服務" # 提示訊息
sudo systemctl start $APP_NAME # 啟動服務
# 步驟 6:檢查健康狀態
echo "步驟 6:檢查健康狀態" # 提示訊息
sleep 5 # 等待 5 秒讓應用程式啟動
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/health) # 呼叫健康端點
if [ "$HEALTH" == "200" ]; then # 如果回傳 200
echo "部署成功!健康檢查通過。" # 成功訊息
else # 否則
echo "部署可能有問題,健康檢查回傳: $HEALTH" # 警告訊息
echo "正在回滾到前一版本..." # 回滾提示
sudo systemctl stop $APP_NAME # 停止服務
rm -rf $DEPLOY_DIR/* # 清除失敗版本
cp -r $BACKUP_DIR/$TIMESTAMP/* $DEPLOY_DIR/ # 還原備份
sudo systemctl start $APP_NAME # 重啟
echo "回滾完成。" # 回滾完成
fi
備份策略
💡 比喻:保險箱策略 備份就像存放重要文件:
- 完整備份:把所有文件影印一份放進保險箱(慢但完整)
- 差異備份:只影印跟上次完整備份不同的文件(中等速度)
- 增量備份:只影印今天新增或修改的文件(快但還原麻煩)
3-2-1 法則:至少 3 份備份,存在 2 種不同媒體,1 份放在異地。
資料庫備份腳本
#!/bin/bash
# db-backup.sh - 資料庫備份腳本
DB_HOST="localhost" # 資料庫主機
DB_NAME="MyAppDb" # 資料庫名稱
BACKUP_DIR="/var/backups/database" # 備份目錄
RETENTION_DAYS=30 # 保留天數
DATE=$(date +%Y%m%d_%H%M%S) # 時間戳記
echo "開始備份資料庫 $DB_NAME..." # 顯示開始
# 建立備份目錄
mkdir -p $BACKUP_DIR # 確保目錄存在
# PostgreSQL 備份
pg_dump -h $DB_HOST \ # 指定主機
-U postgres \ # 使用者名稱
-d $DB_NAME \ # 資料庫名稱
-F c \ # 自訂格式(可壓縮)
-f $BACKUP_DIR/${DB_NAME}_${DATE}.backup # 輸出檔案路徑
# 壓縮備份檔
gzip $BACKUP_DIR/${DB_NAME}_${DATE}.backup # 壓縮節省空間
# 計算備份檔大小
SIZE=$(du -sh $BACKUP_DIR/${DB_NAME}_${DATE}.backup.gz | cut -f1) # 取得檔案大小
echo "備份完成!檔案大小: $SIZE" # 顯示大小
# 清除過期備份
find $BACKUP_DIR -name "*.backup.gz" -mtime +$RETENTION_DAYS -delete # 刪除超過保留天數的備份
echo "已清除 $RETENTION_DAYS 天前的備份。" # 顯示清除訊息
# 驗證備份完整性
pg_restore --list $BACKUP_DIR/${DB_NAME}_${DATE}.backup.gz > /dev/null 2>&1 # 測試備份是否可還原
if [ $? -eq 0 ]; then # 如果成功
echo "備份驗證通過。" # 驗證成功
else # 如果失敗
echo "警告:備份驗證失敗!" # 驗證失敗警告
fi
排程自動備份
# 使用 crontab 設定定時備份
crontab -e # 編輯排程任務
# 加入以下排程
# 每天凌晨 2 點執行資料庫備份
0 2 * * * /opt/scripts/db-backup.sh >> /var/log/backup.log 2>&1 # 每日備份,日誌輸出到檔案
# 每週日凌晨 3 點執行完整檔案備份
0 3 * * 0 /opt/scripts/full-backup.sh >> /var/log/backup.log 2>&1 # 每週完整備份
# 每 6 小時執行增量備份
0 */6 * * * /opt/scripts/incremental-backup.sh >> /var/log/backup.log 2>&1 # 每 6 小時增量備份
應用程式檔案備份
#!/bin/bash
# file-backup.sh - 應用程式檔案備份腳本
APP_DIR="/var/www/myapp" # 應用程式目錄
UPLOAD_DIR="/var/www/myapp/uploads" # 使用者上傳目錄
BACKUP_DIR="/var/backups/files" # 備份目錄
DATE=$(date +%Y%m%d) # 日期
mkdir -p $BACKUP_DIR # 建立備份目錄
# 備份應用程式設定檔
tar -czf $BACKUP_DIR/config_${DATE}.tar.gz \ # 壓縮打包
$APP_DIR/appsettings.Production.json \ # 生產環境設定
/etc/nginx/sites-available/ \ # Nginx 站台設定
/etc/systemd/system/myapp.service # systemd 服務設定
# 備份使用者上傳的檔案
tar -czf $BACKUP_DIR/uploads_${DATE}.tar.gz \ # 壓縮使用者上傳檔案
$UPLOAD_DIR # 上傳目錄
echo "檔案備份完成:$(date)" # 顯示完成時間
# 同步到遠端備份(異地備份)
rsync -avz $BACKUP_DIR/ \ # 增量同步到遠端
backup-user@remote-server:/backups/myapp/ # 遠端備份伺服器
echo "異地備份同步完成" # 同步完成訊息
🤔 我這樣寫為什麼會錯?
❌ 錯誤一:只用 Console.WriteLine 當日誌
// 錯誤:用 Console.WriteLine 記錄日誌
Console.WriteLine($"Order created: {orderId}"); // 沒有時間戳記
Console.WriteLine($"Error: {ex.Message}"); // 沒有日誌等級
// 問題:
// 1. 沒有時間戳記,不知道什麼時候發生的
// 2. 沒有日誌等級,無法過濾重要的錯誤
// 3. 應用程式重啟後日誌就消失了
// 4. 無法結構化查詢
// ✅ 正確做法:使用 ILogger
_logger.LogInformation("訂單 {OrderId} 建立成功", orderId); // 結構化日誌
_logger.LogError(ex, "訂單處理失敗 {OrderId}", orderId); // 包含例外和上下文
// 自動包含時間、等級、分類,可以輸出到多個目的地
❌ 錯誤二:Health Check 沒有設定超時
// 錯誤:Health Check 沒有設定超時時間
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, name: "database"); // 沒有設 timeout!
// 問題:如果資料庫回應很慢,Health Check 可能要等很久
// 負載平衡器可能以為這台伺服器掛了
// ✅ 正確做法:設定合理的超時時間
builder.Services.AddHealthChecks()
.AddSqlServer(
connectionString,
name: "database",
timeout: TimeSpan.FromSeconds(3) // 3 秒沒回應就算失敗
);
❌ 錯誤三:沒有備份驗證就安心
# 錯誤:備份了但從沒測試過還原
pg_dump -d MyDb -f backup.sql # 備份做了
echo "備份完成,可以安心了" # 就這樣?
# 問題:
# 1. 備份檔案可能損壞
# 2. 備份可能漏了重要的資料表
# 3. 還原程序可能有問題
# 4. 你從來沒練習過還原步驟
# ✅ 正確做法:定期測試還原
# 每月至少做一次還原演練
pg_restore -d TestDb backup.sql # 還原到測試資料庫
psql -d TestDb -c "SELECT COUNT(*) FROM orders" # 驗證資料筆數
# 比較還原的資料量是否與預期一致
echo "還原測試完成,驗證資料正確性"