在這個項目中,我使用golang, Gin, MongoDB, Redis, Docker開發了一個高並發的廣告投放服務,能夠處理每10,000筆的請求。這個服務提供兩個API:一個POST用於生成廣告,另一個GET用於列出廣告,對於這兩個API,我各自實現三種策略以達到更高的性能。
- API
- Admin API: 提供一個POST RESTful API用於創建新的廣告。
- Placement API: 提供一個GET RESTful API用於列出活躍的廣告,定義為當前時間在"startAt"和"endAt"條件之間的廣告。
- 高併發策略
- 為了提升GET性能,我在MongoDB中使用了 複合索引(compound index),以達到更快的查詢速度. 複合索引的順序是"startAt"、"endAt"、"ageStart"、"ageEnd"、"country"、"gender"、"platform"。這種順序非常高效,因為查詢時我們首先按時間過濾以找到活躍的廣告。年齡也是一個高選擇性的索引,其次是國家、性別和平台。
- 對於POST請求,考慮到性能和數據持久性,我實現了三種策略:第一種是最基本沒有任何優化的 直接寫入,第二種使用 發布者/訂閱者進行批次寫入, 以及擁有最高即時回應性能的 異步寫入策略。
- 對於GET請求,我實現了三種策略: 第一種是最基本沒有任何優化的 直接從資料庫獲取 ,第二種是使用Redis作為快取,快取鍵值為查詢字串參數。第三種同樣使用Redis作為快取,快取內容是全部當前活躍廣告,為了減少序列化和反序列的操作時間,加上活躍的廣告筆數不多,因此我使用了L1/L2 Caching來進一步提升性能。
- 查詢驗證
- 對於寫入一個新的廣告,條件是可選的。如果提供了條件,我的驗證器將確保條件是有效的。從資料庫寫入和讀取出來的資料是符合條件的。
- MONGODB:MongoDB 是一種非關聯性資料庫,適合用於存儲結構變化較大或不完全結構化的數據。對於廣告數據來說,由於每個廣告可能有不同的條件,MongoDB 的彈性模式非常適合此類應用。
- Redis:Redis 通常用作快取系統,由於其極高的讀寫速度,適用於本系統中快取廣告查詢結果,減少對 MongoDB 的查詢壓力。
這個服務提供一個API來處理高並發(根據需求是每秒內3,000個請求,平均回應時間少於一秒)的POST請求。它使用了三種不同的策略:
- 直接寫入(Instant Direct Write):
- 將請求資料直接寫入資料庫,隨後立即向用戶發送響應。
- 優缺點:
- 優點:簡單直接,實現容易,資料一致性高。
- 缺點:對於大量請求,可能會導致數據庫壓力過大,影響性能。
- API端點: POST /ads
- 發布者 / 訂閱者的批次寫入(Publisher / Subscriber with Bulk write):
- 基本概念是有一個發布者和一個訂閱者。發布者:負責接收與回應用戶的POST請求,將收到的廣告送到一個通道。訂閱者負責定期從定期檢查(預設為0.1秒)此通道並將所有數據寫入數據庫,待收到資料庫的回應後。發布者會收到完成的通知(使用sync.WaitGroup)並傳送HTTP響應給用戶。
- 優缺點:
- 優點:可以實現異步處理與批次寫入到資料庫,減輕資料庫的即時壓力,提高系統吞吐量。
- 缺點:實現較為複雜,若系統突然中斷可能會面臨數據不一致的風險。此外,寫入資料庫的速度會受定時任務間隔的影響。
- API端點: POST /adsBulk
- 佇列異步寫入 (Queue-based Asynchronous Write):
- 首先將請求數據寫入Redis的
ads_list列表中,完成後即刻回應用戶,然後系統將定期從Redis中提取數據並寫入MongoDB。 - 實作細節:
- 從Redis
ads_list列表獲取所有廣告並從Redis中刪除。 - 嘗試將數據寫入MongoDB。如果寫入失敗,使用 指數退避(exponential backoff) 重試,最多重試3次。
- 若重試後依然失敗,將資料存至在Redis
fail_ads列表中,。
- 從Redis
- 優缺點:
- 優點:
- 所使用Redis中的異步操作是原子的,即使在高並發下也能保證數據一致性。
- 利用Redis的高速緩存特性,可以快速響應用戶請求,提高系統性能。
- 同樣使用了資料庫的批次寫入功能,提高系統吞吐量。
- 缺點:系統複雜度增加,需定期檢查Redis中的數據。若Redis出現問題,可能導致數據丟失。
- 優點:
- API端點: POST /adsAsync
- 首先將請求數據寫入Redis的
提供了三種策略來處理高並發(根據需求是每秒內10,000個請求,平均回應時間少於一秒)的GET請求:
- 直接獲取(Instant Direct Get):
- 將GET請求直接從資料庫獲取,隨後立即向用戶發送響應。
- 優缺點:
- 優點:簡單直接,實現容易,資料一致性高。
- 缺點:對於大量請求,可能會導致數據庫壓力過大,影響性能。
- API端點: GET /ads
- 快取鍵值為查詢字串 (Cache Key as Query String):
- 使用Redis作為快取,快取鍵值為查詢字串參數。當收到GET請求時,先檢查Redis中是否有對應的快取,如果有,則直接從Redis中獲取並回應,否則從資料庫獲取並存入Redis。
- 優缺點:
- 優點:可以減輕資料庫的壓力,提高系統性能。
- 缺點:需要管理快取的生命週期,並處理快取失效的情況。
- API端點: GET /adsRedisStringParams
- 快取內容是全部當前活躍廣告 (Cache All Active Ads):
-
我使用 Redis 作為快取層,儲存所有當前活躍的廣告。當系統收到 GET 請求時,會直接從 Redis 中取得所有活躍的廣告,然後在後端進行過濾,找出符合請求條件的廣告,並將其回傳給用戶。
-
為了進一步提升性能,我採用了 L1/L2 快取策略。L1 快取位於後端,同步儲存所有活躍的廣告,而 L2 快取則是 Redis,用於儲存所有活躍的廣告。這種策略可以減少序列化和反序列化的操作時間,並且由於活躍的廣告數量通常不會太多,因此不會對記憶體造成過大的壓力。
-
優缺點:
- 優點:可以減輕資料庫的壓力,提高系統性能,並且減少序列化和反序列的操作時間。
- 缺點:因為存放的所有活躍廣告,過濾的複雜度是O(n),喪失了在資料庫使用索引的優勢O(log(n)),因此較適合廣告筆數不高的情況。此外,如果活躍的廣告筆數非常多,可能會導致Redis壓力過大。
-
API端點:GET /adsRedisActiveDocs
-
- 實驗設置:
- 開啟另一個服務供測試用,該服務同時發起10,000個Goroutine向我的後端發送HTTP POST請求,保證10,000個請求是在一秒內發送完畢。
- 10,000筆中每筆POST的廣告資料都是不同的,每個策略的測試所收到這一萬筆都是相同的,以確保實驗公平。
- 以下表格展示了我用不同策略處理高並發POST請求的實驗結果。每種策略都經過每秒內收到10,000個請求的負載測試。
| 策略 | 平均響應時間 | 最大響應時間 | 最小響應時間 | 請求發送完畢時間 | 性能提升 |
|---|---|---|---|---|---|
| 直接寫入(基線) | 509.5ms | 928.1ms | 221.6ms | 626.3ms | 0% |
| 發布者 / 訂閱者的批次寫入 | 396.5ms | 878.8ms | 111.4ms | 349.2ms | 50.4% |
| 佇列異步寫入 | 279.5ms | 902.4ms | 5.5ms | 686.6ms | 45.1% |
-
平均響應時間:: 每個HTTP POST請求,從用戶發出到收到回應所需的平均時間。
-
最大響應時間:: 所有HTTP POST請求中,用戶發出到收到回應,所花費的最大時間
-
最小響應時間:: 所有HTTP POST請求中,用戶發出到收到回應,所花費的最小時間
-
請求發送完畢時間:: 這10,000個請求,從第一個用戶發出請求,到最後一個用戶發出請求的時間,保證是小於一秒。
-
性能提升:: 性能提升 = (基線平均響應時間 - 新策略平均響應時間) / 基線平均響應時間 * 100%
-
實驗結果:實驗結果都超越了需求的一秒處理3,000個POST的能力,甚至都超越一秒處理10,000個POST的能力。從上述數據可以看出,無論是使用發布者/訂閱者的批次寫入策略還是佇列異步寫入策略,都能顯著提高性能。特別是當使用佇列異步寫入的策略時,平均響應時間能降低到279.5毫秒,性能提升達到45.1%,遠超過直接寫入的策略。這證明在高並發的情況下,適當的寫入策略能有效提升系統的處理能力和響應速度。
- 實驗設置:
- 開啟另一個服務供測試用,該服務同時發起10,000個Goroutine向我的後端發送HTTP POST請求,保證10,000個請求是在一秒內發送完畢。
- 10,000筆中每筆POST的廣告資料都是不同的,每個策略的測試所收到這一萬筆都是相同的,以確保實驗公平。
- 以下表格展示了我用不同策略處理高並發POST請求的實驗結果。每種策略都經過每秒內收到10,000個請求的負載測試。
當查詢字串查詢字串種類數量為5000筆時:
| 策略 | 查詢字串種類數量 | 平均響應時間 | 最大響應時間 | 最小響應時間 | 請求發送完畢時間 | 性能提升 |
|---|---|---|---|---|---|---|
| 直接獲取(基線) | 5000 | 1.82s | 3.48s | 305.131451ms | 95.4ms | 0% |
| 快取查詢字串 | 5000 | 901.2ms | 1.87s | 3.48s | 7.9ms | 759.2ms |
| 快取當前活躍廣告 | 5000 | 789.1ms | 1.08s | 117.1ms | 120.9ms | 56.6% |
當查詢字串查詢字串種類數量為3000筆時:
| 策略 | 查詢字串種類數量 | 平均響應時間 | 最大響應時間 | 最小響應時間 | 請求發送完畢時間 | 性能提升 |
|---|---|---|---|---|---|---|
| 直接獲取(基線) | 3000 | 1.59s | 3.6s | 100.5ms | 622.2ms | 0% |
| 快取查詢字串 | 3000 | 862.4ms | 2.48s | 91.3ms | 755.4ms | 45.7% |
| 快取當前活躍廣告 | 3000 | 547.7ms | 989.9ms | 149.3ms | 536.3ms | 65.5% |
當查詢字串查詢字串種類數量為1000筆時:
| 策略 | 查詢字串種類數量 | 平均響應時間 | 最大響應時間 | 最小響應時間 | 請求發送完畢時間 | 性能提升 |
|---|---|---|---|---|---|---|
| 直接獲取(基線) | 1000 | 1.64s | 3.18s | 32.3ms | 686.6ms | 0% |
| 快取查詢字串 | 1000 | 695.7ms | 1.16s | 59.6ms | 80.3ms | 57.6% |
| 快取當前活躍廣告 | 1000 | 458.5ms | 997.5ms | 136.2µs | 539.1ms | 72.0% |
- 查詢字串種類數量: 10,000筆中的查詢字串種類數量,會影響快取命中率,當查詢字串數量種類越多,對於有快取的策略來說,平均響應時間應該就會越長。
- 實驗結果:從上述數據可以看出,無論查詢字串種類數量為5000、3000還是1000,使用快取查詢字串和快取當前活躍廣告的策略都能顯著提高性能。特別是當查詢字串種類數量為1000時,快取當前活躍廣告的策略能將平均響應時間降低到458.5毫秒,性能提升達到72.0%,遠超過其他策略。這證明在高並發的情況下,適當的快取策略能有效提升系統的處理能力和響應速度。
├─go-backend
│ │
│ ├─db:這個資料夾包含與資料庫相關的程式碼。
│ │ ├─mongodb.go: 管理MongoDB的連接,包括連接到數據庫、創建和刪除集合,以及為集合創建索引。
│ │ └─redis.go: 管理Redis的連接,包括連接到Redis,清除所有鍵,以及刪除特定的鍵。
│ │
│ ├─handlers:這個資料夾包含處理HTTP請求的程式碼。
│ │ └─handlers.go: 處理廣告的創建和寫入,包括同步寫入,異步寫入和批量寫入。以及獲取和過濾廣告,包含直接獲取,快取查詢字串,快取當前活躍廣告。
│ │
│ ├─models:這個資料夾包含定義資料模型的程式碼。
│ │ └─ad.go:定義廣告的數據結構,並提供驗證廣告條件的函數。
│ │
│ ├─Dockerfile:建立一個包含 Go 應用程式的 Docker 映像,該應用程式會在 Linux 環境中運行。
│ ├─Dockerfile.test:建立一個 Docker 映像,該映像用於執行 Go 應用程式的並發性測試。
│ ├─main.go:初始化資料庫連接,設定路由,並啟動服務器。
│ │
│ ├─get_test.go:生成廣告,創建 GET 查詢,發送 GET 請求,並測試資料庫和端點,檢查查詢的唯一性,對端點進行測試,並驗證廣告回應。
│ └─post_test.go:測試在並行環境中創建廣告的過程。
│
├─test_report:這個資料夾包含測試報告。
├─docker-compose.yml:定義和配置一個包含 MongoDB、Mongo Express、Go 後端服務、Go 後端測試服務和 Redis 的 Docker 應用程式。
└─wait-for-it.sh:檢測指定的 TCP 主機/端口是否可用,並在可用後執行特定的命令,供Docker使用。
本系統使用 Docker Compose 進行容器化 執行服務
docker-compose up --build go-backend-test啟動測試
docker-compose up --build go-backend-test; docker-compose down go-backend-test; docker rmi ad-placement-service-go-backend-test:latest