後端老兵的工具箱:C# 非同步、Python 逆向工程、架構選型實戰

後端老兵的工具箱:C# 非同步、Python 逆向工程、架構選型實戰

寫了十幾年後端,從 SQL Server DBA 一路走到架構師,我發現後端開發實戰能力的核心從來不只是會寫 SQL。真正拉開差距的,是你工具箱裡有多少把不同的扳手。這篇文章整理了我在 dotblogs 上累積的幾個關鍵主題:C# 非同步模式、Python 逆向工程實戰、資料庫架構選型、壓力測試,以及從 DBA 到架構師的技術演進。每一個主題都不是教科書式的介紹,而是實際踩過坑之後的心得。後端開發實戰最重要的一課:不要只讀,要動手測。

TL;DR 重點摘要

  • C# 非同步控制:SemaphoreSlim 比 lock 更適合 async 場景,BlockingCollection 是 producer-consumer 的標準解法,別再自己造輪子。
  • Python 逆向工程:當 API 太貴,直接逆向 Web 介面的 JSON 回應是可行路線,但要做好延遲控制與路徑文件化。
  • 架構選型:資料庫不是選最潮的,而是看存取模式決定 — 高頻讀用 cache、搜尋用倒排索引、日誌用列式儲存。
  • 壓測不是選配:沒壓測過的系統就是紙老虎,JMeter 的 Thread Group + Listener 是最基本的品質門檻。

1. C# 非同步模式 — Semaphore 與 BlockingCollection

在 .NET 後端開發中,非同步處理是繞不開的主題。當你有 100 個 HTTP 請求要同時發出去,但目標伺服器只能承受 10 個並發時,你需要的不是 lock,而是 SemaphoreSlim

為什麼 Semaphore 比 lock 更適合 async?

很多人習慣用 lock 來控制並發,但 lock 有一個致命問題:它不支援 async/await。你不能在 lock 區塊裡面 await,否則會收到編譯錯誤。即使你繞過去了(用 Monitor),async 的 continuation 可能在不同執行緒上執行,導致 unlock 失敗。

SemaphoreSlim 則原生支援 WaitAsync(),專為 async 場景設計。它的心智模型是「停車場」:車位(permit)有限,滿了就在外面等,有車出來才放行。

// SemaphoreSlim throttling concurrent HTTP requests
public async Task<List<string>> FetchAllAsync(List<string> urls)
{
    // Only allow 10 concurrent requests
    var semaphore = new SemaphoreSlim(10);
    var httpClient = new HttpClient();
    var tasks = urls.Select(async url =>
    {
        await semaphore.WaitAsync();
        try
        {
            var response = await httpClient.GetStringAsync(url);
            return response;
        }
        finally
        {
            semaphore.Release();
        }
    });

    var results = await Task.WhenAll(tasks);
    return results.ToList();
}

注意 finally 裡的 Release() — 不管成功或失敗都要釋放,否則 permit 會洩漏,最終所有請求都會卡住。

BlockingCollection:Producer-Consumer 的標準解法

BlockingCollection<T> 是 .NET 內建的執行緒安全佇列,底層預設使用 ConcurrentQueue。它最強大的特性是 GetConsumingEnumerable() — consumer 端可以用 foreach 持續等待新資料,直到 producer 呼叫 CompleteAdding()

// Producer-Consumer pattern with BlockingCollection
var queue = new BlockingCollection<WorkItem>(boundedCapacity: 100);

// Producer thread
Task.Run(() =>
{
    foreach (var item in GetWorkItems())
    {
        // Blocks if queue is full (back-pressure!)
        queue.Add(item);
        Console.WriteLine($"Produced: {item.Id}");
    }
    queue.CompleteAdding(); // Signal no more items
});

// Consumer thread
Task.Run(() =>
{
    // Blocks automatically when queue is empty
    // Exits when CompleteAdding() is called and queue is drained
    foreach (var item in queue.GetConsumingEnumerable())
    {
        ProcessItem(item);
        Console.WriteLine($"Consumed: {item.Id}");
    }
});

這裡的 boundedCapacity: 100 是關鍵 — 它提供了背壓(back-pressure)機制。當 consumer 處理速度跟不上 producer 時,佇列滿了 producer 就會被阻塞,而不是無限制地吃記憶體。這跟傳統 ThreadPool 的固定 worker 模型不同:ThreadPool 維護一組固定的執行緒,而 Semaphore + BlockingCollection 用的是「等待」機制,更彈性也更省資源。


2. Python 逆向工程 — 當 API 太貴,我就自己拆

2021 年我碰到一個需求:要把 2600 多筆地址轉成經緯度座標。Google Maps Geocoding API 當時每 1000 次要收 5 美元,算一算要十幾美元。對一個一次性的專案來說,這太不划算了。

於是我打開 Chrome DevTools,觀察 Google Maps 搜尋框的網路請求。發現它回傳的不是標準 JSON API,而是一個巢狀極深的陣列結構。座標藏在類似 d[16][0][0][7][1][3] 這種路徑裡。

import requests
import json
import time
import random

def geocode_address(address):
    """Reverse-engineered Google Maps search to extract coordinates."""
    url = "https://www.google.com/maps/search/"
    params = {"q": address}
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 Chrome/91.0"
    }

    resp = requests.get(url, params=params, headers=headers)
    text = resp.text

    # The response contains a deeply nested JSON-like structure
    # Find the coordinate data block
    try:
        # Extract the nested array from response
        start = text.find("window.APP_INITIALIZATION_STATE")
        if start == -1:
            return None

        # Parse the nested structure
        data_start = text.find("[", start)
        data_end = text.find("];", data_start) + 1
        raw = text[data_start:data_end]

        # Navigate the deeply nested path for coordinates
        parsed = json.loads(raw)
        # Path varies by response type, document it!
        lat = parsed[16][0][0][7][1][3][0]
        lng = parsed[16][0][0][7][1][3][1]
        return (lat, lng)
    except (IndexError, KeyError, json.JSONDecodeError):
        return None


def batch_geocode(addresses):
    """Process addresses with random delays to avoid detection."""
    results = {}
    for i, addr in enumerate(addresses):
        coords = geocode_address(addr)
        results[addr] = coords
        print(f"[{i+1}/{len(addresses)}] {addr} -> {coords}")

        # Random delay: 3~15 seconds to mimic human behavior
        delay = random.uniform(3, 15)
        time.sleep(delay)

    return results

幾個實戰經驗:

  • 隨機延遲是必須的:固定間隔很容易被偵測,3 到 15 秒的隨機延遲更像人類行為。
  • 文件化提取路徑d[16][0][0][7][1][3] 這種路徑完全沒有語意,Google 隨時可能改結構。你必須在程式碼旁邊寫清楚這條路徑代表什麼,否則三個月後你自己也看不懂。
  • 錯誤處理要寬容:有些地址搜不到、有些回傳結構不同,用 try/except 包住並記錄失敗的地址,事後手動補。
  • 這不是長期方案:逆向工程的結果隨時會因為前端改版而失效,只適合一次性或低頻的資料收集。

最終我用這個方法在一個週末處理完 2600 多筆地址,成本是零。但我也很清楚:如果這是一個需要長期維護的服務,老老實實付 API 費用才是正道。


3. 架構選型 — 不同系統該用什麼資料庫?

2021 年我在部落格上寫了一系列「架構師慢慢學」的文章,其中最受歡迎的是資料庫選型。核心觀點只有一句話:資料庫的選擇應該由存取模式驅動,而不是技術潮流。

太多團隊因為「大家都在用 MongoDB」就把所有東西塞進去,結果需要 JOIN 時痛不欲生。也有人因為「Redis 很快」就把所有資料都放 cache,然後面對一致性問題束手無策。

系統類型 vs 資料庫選型對照表

系統類型 建議資料庫 原因
後台管理系統(Admin) RDBMS(PostgreSQL / SQL Server) 低併發、需要複雜查詢和 JOIN、資料完整性優先
高流量前台系統 RDBMS 後端 + Redis 前端 RDBMS 保證資料正確,Redis 用 Key/Value 加速讀取
日誌 / Log 系統 列式儲存(ClickHouse)+ 倒排索引(Elasticsearch) 寫入量大、需要聚合分析和全文搜尋
交易系統 RDBMS + Cache + 一致性協議 ACID 不可妥協,cache 用於讀加速但需要失效策略
即時監控系統 時序資料庫(InfluxDB / TimescaleDB) 時間序列寫入優化、自動聚合降精度
搜尋系統 RDBMS 後端 + Elasticsearch 前端 RDBMS 為資料源,ES 提供倒排索引加速模糊搜尋

幾個決策原則:

  1. 先問讀寫比例:讀多寫少 → 考慮加 cache;寫多讀少 → 考慮列式儲存或訊息佇列緩衝。
  2. 再問一致性需求:金融交易不能最終一致,社群按讚可以。
  3. 最後問查詢模式:需要 JOIN → RDBMS;需要全文搜尋 → 倒排索引;需要時間範圍聚合 → 時序 DB。

這不是什麼高深的理論,但我看過太多團隊在第一步就跳過去,直接被「這個技術很紅」帶著走。


4. JMeter 壓測 — 不壓測的系統都是紙老虎

你寫的 API 在開發機上跑得飛快,但上線後 100 人同時用就當機了。這種事我見過不止一次。壓力測試不是「有空再做」的事情,它是品質門檻。

JMeter 基本設定

Apache JMeter 是免費的壓測工具,核心概念只有三個:

  • Thread Group(執行緒群組):模擬多少使用者同時操作。設定 Number of Threads = 100 就是 100 個並發使用者。
  • Sampler(取樣器):每個使用者要做什麼動作。最常用的是 HTTP Request Sampler,填入 URL、Method、Body 就行。
  • Listener(監聽器):收集結果的報表。Summary Report 給你吞吐量和錯誤率,Aggregate Report 給你百分位數響應時間。

關鍵指標怎麼看

指標 意義 健康標準(參考)
Throughput 每秒處理的請求數 依業務而定,但應隨並發數線性增長直到瓶頸
P90 Response Time 90% 的請求在此時間內完成 一般 API < 500ms
P99 Response Time 99% 的請求在此時間內完成 應 < P90 的 3 倍,否則有長尾問題
Error Rate 失敗請求的百分比 < 0.1% 為優秀,> 1% 要警覺

常見錯誤

  1. 在同一台機器上跑 JMeter 和被測服務:JMeter 本身也吃 CPU 和記憶體,會互相干擾。壓測機和被測機必須分開。
  2. 沒有暖機(Warm-up):JVM 或 .NET 的 JIT 編譯在前幾次請求時會比較慢,應該先跑一輪不計入結果的請求。
  3. 只看平均值:平均響應時間 200ms 看起來很好,但如果 P99 是 5 秒,代表每 100 個使用者就有 1 個等 5 秒。看百分位數才有意義。
  4. 不模擬真實場景:所有人都打同一個 API endpoint 不代表真實負載。應該混合不同操作的比例。

我的習慣是:在專案中期就開始跑基準壓測,而不是上線前才慌張地補。早發現瓶頸,修復成本低十倍。


5. 從 DBA 到架構師 — 我的技術演進路線

我的技術路線不是一開始就規劃好的,而是一步步堆疊出來的:

  • 2020:SQL Server DBA — 每天看執行計劃、調索引、處理 deadlock。這個階段讓我理解了資料庫內部的儲存引擎、鎖機制、B-Tree 索引結構。
  • 2021:.NET 後端開發 — 開始寫 C# Web API,發現 DBA 背景讓我寫出的 SQL 比大多數工程師都好。但也發現自己在非同步、設計模式上的不足。
  • 2022-2023:全端開發 — 接觸前端、Python、爬蟲、自動化。工具箱從一把螺絲起子變成一整個工具箱。
  • 2024-2026:架構設計 — 開始做系統設計、技術選型、效能規劃。發現以前每個階段的經驗都在這裡匯聚。

DBA 背景帶給我的不公平優勢

理解資料庫內部運作,會從根本上改變你寫應用程式的方式:

  • 你知道 SELECT * 在有 covering index 時多浪費多少 I/O,所以你會主動只選需要的欄位。
  • 你知道 NVARCHARVARCHARDATALENGTH() 下的差異,所以你會根據實際資料選擇正確的型別。
  • 你看過太多 table scan 的慘案,所以你設計 API 時會強制分頁,而不是讓使用者一次撈全部。
  • 你理解 transaction isolation level 的差異,所以你知道什麼時候用 READ COMMITTED SNAPSHOT 可以大幅降低鎖爭用。

給後端工程師的建議

如果你想往架構方向發展,我的建議是:學任何東西都要動手測試

  • 想搞懂索引?建一個百萬筆的測試表,比較有索引和沒索引的執行計劃。
  • 想搞懂 SemaphoreSlim?寫一個 console app,開 1000 個 task,觀察不同 permit 數量的效果。
  • 想搞懂資料庫選型?不要只讀比較文章,自己用 Docker 裝一個 ClickHouse、一個 PostgreSQL,塞相同的資料,跑相同的查詢,比較速度。

2021 年我在部落格上寫「架構師慢慢學」系列時,最大的收穫不是寫出來的文章,而是為了寫文章去做的那些實驗。讀十篇文章不如自己跑一次 DATALENGTH() 比較 CHARVARCHAR 的儲存差異。那個數字會刻在你腦子裡,比任何文章都深。


結語

後端工程師的價值不在於精通某一個框架或語言,而在於工具箱的廣度和深度。C# 的非同步模式讓你處理高並發,Python 的靈活性讓你快速解決一次性問題,架構選型的思維讓你做出正確的技術決策,壓力測試讓你對系統有信心。

這些東西沒辦法在一天內學會,但每一個都值得你花時間去實驗。畢竟,不壓測的系統是紙老虎,不動手的學習是紙上談兵

如果你也是從 DBA 或其他專精領域起步的工程師,不要覺得自己起步晚。每一個階段的深度經驗,都會在你走向架構師的路上成為別人沒有的武器。

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *