☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
Server 進階

伺服器監控與維運

日誌集中管理(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 "還原測試完成,驗證資料正確性"

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊