我花了近十年打造 SaaS 產品,如果說我有什麼心得體會,那就是可擴展性並非事後補救就能實現的。它不是在你新創公司突然登上 TechCrunch 封面、伺服器崩潰時才加入的功能。可擴展性是一種思維模式,一系列架構決策的集合,坦白說,更是凌晨三點生產環境崩潰時累積的大量經驗教訓。
我剛入行的時候,以為可擴充性只是指處理更多用戶。只要增加伺服器就行了,對吧?但實際情況遠比這複雜得多。可擴展性涵蓋了程式碼庫容納新功能的能力、團隊協作不衝突的交付能力、資料庫在增長後仍能穩定執行的能力,以及基礎設施應對流量高峰而不至於在晚餐時被 PagerDuty 的警報吵醒的能力。
本指南並非探討流行術語或電腦科學理論,而是著眼於真正實際的考慮因素,這些因素決定了哪些SaaS產品能夠優雅地擴展,哪些產品會因自身規模過大而崩潰。我將分享我學到的經驗教訓、犯過的錯誤,以及在服務數百萬用戶的生產環境中反覆驗證有效的模式。
在深入探討技術細節之前,我們需要先明確「可擴展性」的真正意義。這個詞經常被隨意使用,不同的人對它的理解也各不相同。有些人認為它只是指負載下的效能;而有些人則認為它是指組織的發展。事實上,可擴展性是多維度的,忽略任何一個維度最終都會帶來問題。
我們先從最根本的差異說起。垂直擴充是指提升現有機器的效能-增加 CPU 核心數、記憶體容量和儲存速度。這是最便捷的方式。您的應用程式無需更改。您只需將 EC2 執行個體從 t3.medium 升級到 c5.9xlarge 即可。其優點顯而易見:無需架構變更,無需面對分散式系統的複雜性,也無需擔心一致性問題。
但垂直擴展存在嚴格的限制。單台機器的處理能力終究有限,隨著效能的提升,成本會呈指數級成長。更重要的是,這會帶來單點故障的風險。一旦這台效能強勁的伺服器宕機,整個應用程式也會跟著崩潰。
水平擴展採用了不同的方法。它不是增強單一伺服器的效能,而是增加伺服器的數量。這才是真正有趣,但也更複雜的地方。你的應用程式需要設計成能夠在多個實例上執行。你的資料層需要處理分散式查詢。你的會話管理需要在使用者請求可能存取不同伺服器的情況下正常運作。
現實世界需要兩種方法。資料庫的垂直擴展會達到一定程度,然後開始考慮只讀副本和分片。應用伺服器的水平擴充則需要負載平衡器,但同時也要根據工作負載選擇適當大小的執行個體。關鍵在於理解哪些問題需要哪種解決方案。
這裡有一個容易讓許多開發者犯錯的差別:效能和可擴充性並非同一概念。執行速度快的應用程式未必具有可擴展性,而可擴展的應用程式對某些用戶而言可能感覺不到速度快。
效能指的是應用程式回應單一使用者請求的速度。它以毫秒為單位衡量——API 端點回傳回應的速度、頁面渲染的速度、介面流暢度等等。您可以透過最佳化來提升效能:例如,改進演算法、提高資料庫查詢效率、使用快取以及優化程式碼。
可擴展性是指在負載增加時仍能保持可接受的效能。一個應用程式在 50 毫秒內響應單一請求可能非常出色。但是,當並髮用戶從 100 個增加到 10,000 個時會發生什麼?如果回應時間飆升至 5 秒,那麼即使在輕負載下技術上“很快”,你的應用程式也缺乏可擴展性。
我看過一些團隊執著於縮短 API 回應時間幾毫秒,卻忽略了真正的問題:系統在中等負載下就會崩潰。反之,我也看過一些系統能夠處理大規模資料,但對單一使用者來說卻運作緩慢,原因在於沒有人費心優化關鍵路徑。兩者都不可或缺,但要實現它們需要不同的思考方式和工具。
在技術討論中,這個維度往往被忽視,但它至關重要。你的程式碼庫不僅需要具備擴展性以應對更多用戶,還需要具備擴展性以應對更多開發人員。
當你的團隊只有三名工程師時,你可以使用單體應用,因為每個人都清楚所有功能的運作方式。你可以在 Slack 上協調部署。你可以把整個架構記在腦子裡。但當團隊擴展到十五名工程師,然後是五十名時,一切都會改變。
技術可擴充性意味著您的架構需要支援獨立部署、清晰的服務邊界和定義完善的介面。這意味著您的測試套件需要執行得足夠快,以便開發人員能夠實際執行它。這意味著您的開發環境需要易於設置,並且您的部署管道需要足夠可靠,以至於交付程式碼不需要任何繁瑣的準備工作。
我曾在一家公司工作,那裡每周有一天是“部署日”,因為部署流程非常脆弱且耗時。每週三,都會有人手動部署本週的變更,我們所有人都提心吊膽。這根本不符合組織可擴展性的要求。當出現需要緊急修復的關鍵漏洞時,我們有時會等到下週三,而不是冒險部署。這就是忽視可擴展性這一維度所導致的種種弊端。
如果你負責損益,這個問題會讓你夜不能眠。你的基礎設施成本應該隨著使用者數量的成長呈現亞線性成長。如果每次使用者數量翻倍,你的 AWS 帳單也翻倍,那麼你的架構或使用模式肯定存在根本性問題。
成本可擴展性源自於效率。它包括選擇合適的資料庫、實施有效的快取策略、優化成本最高的查詢以及認真考慮資料儲存。它還包括使用更經濟的儲存層來儲存冷資料、實施合理的資料保留策略以及不儲存不需要的資料。
初期階段,你可能不會太在意成本優化。當你還在尋找產品與市場的契合點時,每月在基礎建設上多花幾百美元,與工程時間成本相比,簡直微不足道。但隨著規模擴大,效率低的問題會不斷累積。當每秒處理數千個請求時,一個查詢如果每次請求浪費 100 毫秒的資料庫 CPU 時間,就會變成一個嚴重的問題。
早期做出的架構決策將直接影響你的擴展能力。這時,「技術債」這個詞就顯得格外刺眼了。在急於發布最小可行產品(MVP)時做出的糟糕架構選擇,可能會讓你在未來幾年裡飽受困擾,需要進行大量的重構工作,而這些原本可以透過更周全的考慮來避免。
讓我們直面這個顯而易見卻又難以啟齒的問題。多年來,業界對微服務的態度搖擺不定。微服務一度被譽為解決所有可擴展性問題的靈丹妙藥,隨後又被嘲諷為過度設計,增加了複雜性卻沒有帶來相應的收益。真相,一如既往,介於兩者之間,難以捉摸。
先從單體架構開始。我知道鑑於微服務架構已經鋪天蓋地,這個建議可能聽起來有些反常,但請聽我解釋。一個結構良好的單體架構比分散式系統更容易建置、部署、偵錯和維護。當你還在摸索產品方向,領域模型也還在不斷變化時,單體架構的彈性就顯得格外寶貴。
關鍵在於「結構良好」。你的單體應用不應該像一團亂麻,所有東西都緊密耦合、互相依賴。它應該是模組化的,應用程式的不同領域之間界限分明。你可以把它想像成一個可以根據需要拆分成微服務的單體應用,但目前並沒有拆分,因為你已經享受到了更簡單部署模型帶來的所有好處。
我看過太多新創公司盲目採用微服務架構,只因為 Netflix 就是這麼做的,卻忽略了 Netflix 擁有數千名工程師,以及大多數公司永遠不會遇到的問題。這些新創公司最終耗費大量工程時間管理部署流程、服務發現、分散式追蹤以及微服務帶來的所有維運複雜性,而這些時間本來可以用來開發功能和服務客戶。
但單體架構也有其限制。隨著團隊規模的擴大,單一程式碼庫會成為協調的瓶頸。由於所有人的變更同時發布,部署頻率會下降。團隊成員會因為邊界不清晰而不敢碰觸某些程式碼部分。測試套件的運作時間也會變得異常漫長。這時,你就應該考慮拆分單體架構了。
從單體架構到微服務的轉型,組織需求和技術需求也同樣重要。當擁有一個能夠端到端負責該服務的團隊,當領域邊界清晰穩定,並且當獨立部署的優勢大於分散式系統複雜性的成本時,就可以考慮分割服務。
在拆分服務時,務必明確劃分邊界。微服務應該與業務能力一致,而不是技術層。不要建立“資料庫服務”或“驗證服務”。而應該建立“用戶管理服務”或“計費服務”,將特定業務領域的所有邏輯、資料和功能封裝起來。
資料庫幾乎總是瓶頸所在。這一點我怎麼強調都不為過。你可以輕鬆地橫向擴展應用伺服器——只需在負載平衡器後面啟動更多實例即可。但資料庫是有狀態的,而有狀態的事物很難擴展。
首先也是最重要的決定是選擇適合你用例的資料庫。我知道,我知道,每個人都想用在 Hacker News 上看到的那些酷炫的新資料庫。但要務實。 PostgreSQL 對於大多數 SaaS 應用程式來說都是絕佳的選擇。它成熟、易於理解、擁有優秀的工具集,只要配置得當,就能應付大量資料。
也就是說,你需要了解應用程式所需的存取模式。如果你建立的主要是鍵值查找,那麼像 MongoDB 這樣的文件資料庫可能比較合適。如果需要全文搜尋,則需要 Elasticsearch 或類似的專用工具。如果處理的是時間序列資料,那麼像 TimescaleDB 或 InfluxDB 這樣的資料庫可能更合適。
但關鍵在於:不要為了應對極端情況而過早選擇特殊的資料庫進行最佳化。首先選擇一個可靠的關係型資料庫,並將其用於所有用途,直到有確鑿證據表明它無法正常工作為止。之後,您可以隨時根據具體用例新增專用資料庫。
模式設計對可擴展性至關重要。索引的使用要謹慎——它們可以加快讀取速度,但會降低寫入速度。要了解何時該規範化,何時該反規範化。在一個完全規範化的模式中,你可能需要連接五個表格才能取得單一頁面的資料。在生產環境中,大規模應用時,這些連接操作會嚴重影響效能。有時,反規範化資料——儲存針對讀取模式最佳化的冗餘副本——才是正確的選擇。
從一開始就認真思考你的查詢語句。你可以使用 ORM,但請務必理解它產生的 SQL 程式碼。我除錯過太多效能問題,最終都歸結於 N+1 查詢——先存取資料庫獲取列表項,然後再存取每個項獲取相關資料。這種查詢處理 10 個結果時可能沒問題,但處理 1000 個結果時就會崩潰。
如果我能給出一條對應用程式可擴充性有最大直接影響的建議,那就是:在每一層都積極地進行快取。
首先,啟用 HTTP 快取。使用正確的快取頭。允許瀏覽器快取靜態資源。在應用程式前面部署 CDN。 CloudFront、Fastly、Cloudflare——它們都很好用。僅此一項就能大幅減少來源伺服器的流量。
應用層快取是動態內容優化中最有效的途徑。 Redis 或 Memcached 應該儘早整合到你的架構中。其模式很簡單:首先檢查緩存,如果緩存中沒有資料,則從資料庫中獲取並存儲到緩存中以備下次使用。
但快取引入了複雜性,尤其是在快取失效方面。有句名言:「電腦科學只有兩件難事:快取失效和命名。」 這句話說起來好笑,但卻無比真實。你需要一個策略來確保快取與資料庫保持一致。
基於時間的過期機制是最簡單的方法。為緩存項設定 TTL(生存時間)。經過一段時間後,快取過期,您需要取得最新資料。這種方法適用於不需要完全即時更新的資料。使用者設定檔、配置資料、參考資料-這些資料通常可以容忍輕微的過時。
對於需要維持資料時效性的資料,您需要主動失效機制。當您在資料庫中更新使用者個人資料時,您也同時失效(或更新)了該使用者的快取條目。這需要嚴格的規範和細緻的編碼。很容易忘記在某個地方失效緩存,導致用戶看到過時的資料。
我發現一種有效的模式是快取旁路模式,並採用事件驅動的快取失效機制。應用程式程式碼首先檢查緩存,如果快取發生變化則回退到資料庫。但當資料發生變化時,您需要發布一個事件(例如使用 Redis 的發布/訂閱機製或訊息佇列),然後由一個獨立的進程來處理所有應用程式實例的快取失效。
查詢結果快取是另一種強大的技術。許多 ORM 框架都原生支援此功能。您可以快取開銷較大的查詢結果,這樣後續對相同資料的請求就不會存取資料庫。不過,使用時請務必小心。快取鍵必須涵蓋所有查詢參數,否則最終會傳回錯誤資料。
別忘了在單一請求中使用快取。如果在單一 HTTP 請求中多次使用相同的參數呼叫同一個函數,請快取結果。這是一個很容易實現的最佳化方法,可以避免重複工作。
並非所有操作都必須在請求-回應週期內完成。事實上,大多數操作可能都不應該這樣做。這種思維方式的根本轉變能夠顯著提升效能和可擴展性。
使用者執行操作時,通常並不需要立即得到結果。他們只需要確認請求已被接收並將被處理。這就為後台處理提供了可能,而後台處理具有許多巨大優勢。
首先,它能讓你的應用程式運作速度更快。你無需讓使用者等待你發送電子郵件、產生報告、處理圖像或執行任何其他耗時的任務,而是立即回傳回應並非同步處理這些工作。
其次,它能增強系統的彈性。即使郵件服務發生故障或運作緩慢,也不會影響主應用程式的運作。系統會不斷重試,直到任務成功為止。
第三,它能更好地利用資源。您可以讓專門的工作流程處理不同類型的任務,並根據工作負載獨立擴充資源。您的 Web 伺服器無需足夠的容量來處理尖峰後台處理負載,因為這些負載已在其他地方處理。
這種實作方式通常涉及訊息佇列——例如 RabbitMQ、帶有 Sidekiq 的 Redis、AWS SQS 或類似服務。當某個操作需要在背景處理時,你需要將所有必要資訊放入佇列中建立一個任務。工作進程會輪詢隊列,領取任務並執行它們。
我最常用的模式是這樣的:Web 請求建立一個資料庫記錄來表示待完成的工作,並將一個引用該記錄 ID 的作業加入佇列。工作進程領取該作業,載入記錄,執行工作,並將結果更新到記錄中。這樣可以形成清晰的審計跟踪,並方便地向用戶展示其請求的狀態。
冪等性對於後台作業至關重要。作業可能會失敗並重試。由於分散式系統中存在各種故障模式,作業甚至可能被執行多次。因此,您的作業處理程序必須編寫得當,確保多次執行與執行一次產生相同的結果。
我曾吃過虧,一次計費作業執行了兩次,導致客戶被重複收費。解決方法是在資料庫中新增唯一約束,防止重複收費,並將收費邏輯封裝在一個事務中,該事務會先檢查是否存在已存在的收費。現在,我們所有的財務作業都經過精心設計,確保其冪等性。
API 是你的應用程式與外部世界之間的契約。從一開始就做好 API 可以避免日後大量的麻煩。
遵循正確的 REST 原則,但不要教條主義。 RESTful API 之所以有效,是因為它們是無狀態的且可快取的。每個請求都包含完成請求所需的所有資訊。無需管理伺服器端會話狀態,這使得橫向擴展變得輕而易舉。
從一開始就對 API 進行版本控制。這一點我怎麼強調都不為過。可以使用 URL 版本控制( /api/v1/users )或基於回應頭的版本控制,但請務必選擇方法並堅持下去。你最終肯定需要進行重大更改,而版本控制策略可以讓你在不影響現有客戶端的情況下完成這些更改。
儘早實施速率限制。您肯定不希望因為一個行為不端的客戶端或一次 DDoS 攻擊就導致整個系統崩潰。像是限制每個 API 金鑰每小時請求數不超過 1000 次這樣簡單的措施,就能輕鬆實現,並避免一大類問題。
任何返回清單的端點都應使用分頁。切勿傳回無界結果集。如果可以,請優先使用基於遊標的分頁——它比基於偏移量的分頁效率更高,並且能更好地處理資料變更。當使用者使用基於偏移量的分頁來取得第 5 頁結果時,如果清單開頭新增了新項,則可能會出現重複項或遺漏項。基於遊標的分頁則不會出現此問題。
仔細考慮你的回應負載。只包含客戶端真正需要的資料。我看過一些 API 傳回包含深度嵌套關係的整個物件圖,因為這樣做比思考哪些資料真正必要要容易得多。這不僅浪費頻寬、降低反應速度,還可能洩漏你原本不想公開的資訊。
考慮支援部分響應或欄位選擇。允許客戶端指定所需的欄位: /api/v1/users/123?fields=id,name,email 。這對於網路連線速度較慢的行動用戶端尤其有用。
批量操作可以顯著提升效能。如果客戶端需要取得多個使用者的資料,那麼使用一個可以一次接收多個 ID 的端點,比多次往返要高效得多。
身份驗證本質上是有狀態的,這給橫向擴展帶來了有趣的挑戰。簡單地將會話資料儲存在應用程式伺服器上的方法,一旦有了多台伺服器就會失效。使用者登入後,他們的會話儲存在伺服器 A 上,但他們的下一個請求會傳送到伺服器 B,而伺服器 B 並不知道他們的會話資訊。
傳統的解決方案是會話黏性,即透過負載平衡器確保使用者的請求始終發送到同一台伺服器。這種方法雖然有效,但也有缺點。如果該伺服器宕機,使用者的會話就會遺失。此外,由於需要優雅地斷開連接,部署難度也更高。而且,它還可能導致負載分佈不均。
更好的方法是集中式會話儲存。將會話儲存在 Redis 或類似的快速資料儲存系統中,以便所有應用程式伺服器都能存取。當使用者發出請求時,任何伺服器都可以透過檢查 Redis 來驗證其會話。這會增加少量延遲(Redis 查詢通常不到一毫秒),但能帶來極大的靈活性。
使用 JWT(JSON Web Tokens)進行無狀態身份驗證是更好選擇。令牌本身包含驗證用戶所需的所有訊息,並經過加密簽名,因此無法篡改。完全不需要會話儲存。應用伺服器無需協調或共享狀態。
不過,JWT 也有一些特殊注意事項。如果不重新新增狀態(維護一個撤銷清單),JWT 無法在過期前失效,因此過期時間應相對較短。對於長時間會話,請使用刷新令牌。 JWT 中僅儲存必要資訊以保持其體積小——它會隨每個請求一起發送。
如果需要整合第三方服務,請實施 OAuth2。不要自行開發身份驗證協定。使用經過實戰檢驗和安全漏洞審計的成熟庫。
隨著應用程式的成長,資料層會變得越來越複雜。在 100 個用戶時行之有效的方法,在 10,000 個用戶時可能就行不通了,在 100 萬用戶時更是肯定行不通。你需要能夠隨著規模擴大而不斷演進的策略。
大多數的應用程式都是以閱讀為主。使用者瀏覽產品的頻率遠高於購買產品的頻率。他們查看個人資料的頻率遠高於更新個人資料的頻率。這種不對稱性對你有利。
只讀副本可讓您橫向擴展讀取容量。您的主資料庫處理所有寫入操作,並將資料複製到一個或多個副本資料庫,由這些副本資料庫處理讀取操作。大多數現代資料庫都內建了此功能,實現起來相對簡單。
問題在於資料複製延遲。當您向主資料庫寫入資料時,變更需要一些時間(通常是幾毫秒,但有時會更長)才能傳播到副本。如果用戶更新了個人資料並立即查看,而讀取操作是在副本上進行的,那麼用戶可能會看到過時的資料。
有幾種方法可以解決這個問題。最簡單的方法是在寫入作業後的一段時間內從主資料庫讀取資料。例如,如果使用者剛剛更新了個人資料,則在接下來的 30 秒內從主資料庫讀取其個人資料資料。之後,從副本資料庫讀取資料即可。
另一種方法是利用會話親和性和時間戳記。寫入資料後,將時間戳記儲存在使用者的會話中。讀取資料時,如果時間戳較新,則從主伺服器讀取;否則,使用副本伺服器。
某些資料庫支援讀取後寫入一致性,即允許讀取操作看到某個時間點之前的所有寫入操作。 PostgreSQL 的同步複製可以實現這一點,但會帶來效能方面的妥協。
對於寫入擴充而言,情況就變得更加複雜。你不能簡單地增加主資料庫的數量就指望一切正常運作。寫入操作需要協調一致才能保持資料一致性。
連接池是首要的最佳化措施。開啟資料庫連線的成本很高。連線池維護一個已開啟的連線池,這些連線可以在不同的請求之間重複使用。這在橫向擴展應用伺服器時尤其重要——如果沒有連接池,很容易就會耗盡資料庫的連線數限制。
當單一資料庫的寫入容量達到極限時,就需要考慮分片了。這才是真正考驗能力的時候。
分片是指將資料分散到多個資料庫伺服器(稱為分片)上。每個分片包含一部分資料。這種方法功能強大,但也引入了顯著的複雜性。
第一個問題是使用什麼來進行分片。這就是你的分片鍵,這是一個至關重要的決策。對於大多數 SaaS 應用程式來說,使用者 ID 或租用戶 ID 都是合理的選擇。給定使用者的所有資料都儲存在同一個分片中。
假設你有四個分片。你可以使用user_id % 4來決定使用者資料所在的分片。使用者 1 的資料放在分片 1,使用者 2 的資料放在分片 2,使用者 3 的資料放在分片 3,使用者 4 的資料放在分片 0,使用者 5 的資料放回分片 1,依此類推。
這種方法雖然可行,但有一個重大問題:以後很難再增加更多分片。若加入第五個分片,取模運算的規則就會改變,原本位於分片 1 的使用者 5 將會被移到分片 0。這樣一來,大多數用戶的資料就需要遷移。
一致性哈希是一種更好的方法。它允許你以最小的資料移動量加入分片。或使用基於範圍的分片-例如,將使用者 1 到 1,000,000 放在分片 1,將使用者 1,000,001 到 2,000,000 放在分片 2,依此類推。這使得加入分片更加簡潔,但如果使用者成長不均勻,則可能導致分片分佈不均。
你的應用程式程式碼需要具備分片感知能力。在取得使用者資料時,你首先需要確定該使用者位於哪個分片,然後查詢該分片。這通常會被抽像到一個資料存取層中,因此你的大部分程式碼無需關心分片問題。
跨分片查詢變得非常棘手。你無法有效率地跨分片連接資料。如果需要查詢所有用戶,則需要查詢所有分片,然後在應用程式程式碼中合併結果。聚合、報表和分析也變得更加複雜。
這就是我之前說的,分片鍵的選擇至關重要。它應該與你最常用的存取模式保持一致。如果你按使用者分片,但經常需要按組織查詢,而組織又跨越多個分片,那你將會遇到很多麻煩。
有些操作無法有效地進行分片。例如,全域唯一性約束。如果您需要確保所有使用者的電子郵件地址唯一,而使用者分佈在不同的分片中,那麼檢查唯一性就需要檢查所有分片。您可以使用一個單獨的服務來管理唯一標識符,但這會增加複雜性。
分片也會使部署和維運流程變得更加複雜。你不能再只在「資料庫」上執行遷移,而需要在所有分片上執行,並仔細協調以避免停機。
鑑於上述種種複雜性,我的建議是盡可能避免分片。現代資料庫能夠在單一伺服器上處理大量資料和流量。 PostgreSQL 在適當的硬體和配置下,每秒可以處理數 TB 的資料和數萬筆事務。在考慮分片之前,請先嘗試其他優化策略。
如果你正在建立一款 B2B SaaS 產品,你需要考慮多租戶問題——如何隔離不同客戶的資料。主要有三種方法,每種方法都有其優缺點。
第一種方法是為每個租戶使用獨立的資料庫。每個客戶都擁有自己的資料庫。這提供了最強的隔離性,並簡化了一些操作(例如,每個租戶的備份和遷移)。但這種方法在維運上非常複雜。管理成千上萬個資料庫是一項挑戰。模式遷移需要在所有資料庫上運作。資源分配也很棘手——一個大型租戶無法利用小型租戶未使用的容量。
第二種方案是在單一資料庫中為每個租用戶建立獨立的模式。這種方案提供了一定程度的隔離性,仍然可以輕鬆地執行針對每個租戶的操作。雖然它比使用獨立資料庫的操作複雜度更低,但仍需要管理大量的模式。
第三種方案是共用模式,每個表格都包含一個 tenant_id 欄位。所有租戶的資料都儲存在同一張表中,僅透過 tenant_id 進行區分。從資源利用率的角度來看,這是最有效的方案,但需要精心實現以確保租戶隔離。每個查詢都必須按 tenant_id 進行過濾。即使漏掉一次過濾,也可能導致租戶間的資料外洩。
我三種方法都用過。對於新創公司,我通常建議先採用共享模式。這種方法最簡單,成本也最低。以後如果需要服務大型客戶或提高安全性,可以隨時遷移到獨立資料庫。
無論採用何種方法,都需要行級安全措施來防止資料外洩。盡可能使用資料庫級約束和策略。不要只依賴應用程式程式碼每次都能正確設定 tenant_id 篩選條件。我見過太多因為開發人員忘記加入 WHERE 子句而導致的 bug。
實作一個中介軟體層,自動將租用戶上下文注入到查詢中。當收到請求時,辨識租用戶(通常透過子網域、請求頭或 JWT 聲明),並在整個請求生命週期中保持該資訊可用。資料存取層應使用此上下文自動限定查詢範圍。
隨著應用程式的成長,資料量也會隨之成長。最終,您將擁有多年歷史資料,這些資料很少被存取,但仍然佔用空間並降低查詢速度。
從一開始就實施資料保留策略。確定不同類型的資料需要保留多長時間。出於監管原因,交易資料可能需要保留七年。日誌資料可能只需要保留 90 天。
使用分區可以簡化資料管理。大多數資料庫都支援基於日期範圍的表格分區。您可以按月或按年對交易表進行分區。這樣,歸檔或刪除舊資料就變得非常簡單—只需分離分割區即可。
將冷資料遷移到更便宜的儲存媒體。 AWS Glacier、Google Cloud Archive Storage 和 Azure Cool Blob Storage 都專為存取頻率較低的資料而設計,它們的成本比主資料庫儲存低幾個數量級。
您的歸檔流程應該自動化且經過充分測試。我看過一些案例,由於歸檔流程本身沒有經過大規模測試,導致系統中斷。請在流量低谷期執行歸檔流程,並確保採用增量式歸檔-不要試圖一次歸檔一年的資料。
維護一份已歸檔資料的索引,以便在需要時檢索。當使用者要求已歸檔的資料時,您可能需要從冷儲存(速度較慢)中獲取資料,並將其與當前資料一起呈現。
基礎架構的選擇和部署實踐對你的擴展能力有著巨大的影響。錯誤的選擇會造成維運負擔,而且這種負擔會隨著時間的推移而不斷加劇。
容器,特別是 Docker,已經成為應用程式打包的事實標準。它們透過將應用程式及其所有依賴項封裝到一個可移植的單元中,解決了「在我的機器上執行正常」的問題。
可擴展性方面的優勢非常顯著。您可以將同一個容器部署到開發、測試和生產環境,從而確保一致性。您可以透過執行更多容器執行個體來實現橫向擴展。您也可以透過逐步用新容器取代舊容器,實現零停機時間的新版本發布。
但光有容器還不夠。你還需要一個編排平台來管理它們。 Kubernetes 贏得了這場競爭,無論好壞。它功能強大且豐富,但也帶來了相當大的複雜性。
對於許多 SaaS 應用,尤其是在早期階段,Kubernetes 顯得過於複雜。 AWS ECS(彈性容器服務)或 Google Cloud Run 提供了更簡單的替代方案,可處理大多數常見用例,而無需執行 Kubernetes 叢集帶來的運維開銷。
如果你確實要使用 Kubernetes,請務必投入時間和精力去深入了解它。不要只是從網路複製貼上配置,而不去理解它們的作用。除非有特殊原因,否則請使用託管的 Kubernetes 服務(例如 EKS、GKE、AKS)——執行自己的叢集是一項全職工作。
正確配置資源請求和限制。 Kubernetes 使用這些設定來調度 Pod 和管理資源。如果不進行設置,要么會導致叢集資源利用率不足,要么會遇到意外的限流情況。
使用水平 Pod 自動擴縮容,可根據 CPU 或記憶體使用情況自動擴充應用程式。這是 Kubernetes 的核心功能之一——應用程式會在流量高峰期自動擴容,在流量低谷期自動縮容,從而優化成本。
手動更改基礎設施無法擴展。規模較小時,透過 Web 控制台設定伺服器和負載平衡器尚可接受。但隨著業務成長,這種方式將變得難以維護且容易出錯。
基礎設施即程式碼 (IaC) 指的是在設定檔中定義基礎設施,這些設定檔可以進行版本控制、審查並自動套用。 Terraform 是目前最受歡迎的 IaC 工具,支援所有主流雲端服務供應商。
您的所有基礎設施——網路、子網路、安全群組、負載平衡器、資料庫、快取叢集等等——都應該用程式碼定義。這樣做有很多好處。
首先,它具有可復現性。您可以建造一個完全相同的環境來測試或災難復原。只需一條命令,即可在新區域複製您的生產環境。
其次,它是可審計的。每次更改都要經過程式碼審查。您可以準確地看到更改的時間和更改者。
第三,它可以防止配置漂移。當基礎設施手動管理時,不同的環境會逐漸出現差異。一個人在生產環境中做了更改,另一個人在測試環境中做了不同的更改,很快,你的環境就會變得完全不同。而使用基礎設施即程式碼 (IaC),你的環境由同一套程式碼和不同的變數定義。
使用模組可以避免重複勞動。如果您需要在多個環境或區域部署類似的基礎設施,請將通用模式抽象化為可重複使用的模組。
確保狀態文件安全。 Terraform 儲存基礎架構的目前狀態,其中包含資料庫密碼和 API 金鑰等敏感資料。請使用具有加密和存取控制的遠端狀態儲存。
持續整合和持續部署不僅僅是流行語,它們是擴展開發流程的基本實踐。
你的持續整合 (CI) 管線應該在每次提交時執行。它應該檢出程式碼、執行所有測試、執行程式碼檢查器和安全掃描器,並建立工件(例如容器鏡像)。如果任何步驟失敗,則該提交將被標記為損壞,並立即通知負責的開發人員。
這讓您確信主分支始終處於可部署狀態。它能及早發現 bug,在最容易修復的時候進行修復。它還能防止劣質程式碼進入生產環境。
當程式碼合併到主分支後,持續交付 (CD) 管線就會接管。它會取得持續整合 (CI) 建置的製品,並將其部署到各個環境—開發、預發布和生產環境。每個環境都可以設定自動或手動審批流程。
使用藍綠部署或捲動更新可以實現零停機部署。藍綠部署是指將新版本與舊版本並行部署,並在新版本準備就緒後切換流量。滾動更新則是逐步以新實例取代舊實例。這兩種方法都能確保使用者在部署過程中不會受到任何影響。
實施自動回滾。如果部署導致錯誤激增或健康檢查失敗,則自動回滾到先前的版本。問題修復後,您可以隨時進行調查並再次部署。
功能開關是 CI/CD 的強大補充。它們允許您在不暴露給使用者的情況下將程式碼部署到生產環境。您可以逐步向一部分用戶推出功能,在生產環境中使用真實流量進行測試,並立即停用有問題的功能而無需重新部署。
我曾經參與過一些團隊,他們的部署流程非常可靠,每天部署數十次。我也曾參與過一些團隊,他們的部署流程極為糟糕,我們必須把幾週的變更集中起來,然後祈禱一切順利。這其中的差異完全在於你使用的工具和流程。
你無法管理你無法衡量的東西。隨著系統規模的擴大,了解系統內部發生的事情變得越來越重要,也越來越困難。
從一開始就實施日誌記錄,但要謹慎行事。不要記錄所有內容-日誌量很快就會變得難以管理。記錄錯誤、重要的業務事件以及足夠的上下文資訊以便除錯問題。使用結構化日誌(JSON 格式),以便於解析和查詢。
集中管理日誌。當您有幾十甚至幾百個實例時,透過 SSH 連接到伺服器讀取日誌檔案是不可行的。使用日誌聚合服務,例如 ELK 技術堆疊(Elasticsearch、Logstash、Kibana)、Splunk、Datadog 或眾多雲端原生解決方案之一。
指標可以幫助您量化了解系統的行為。追蹤請求速率、回應時間、錯誤率、資料庫查詢時間、快取命中率、佇列深度—任何能夠幫助您了解系統健康狀況的指標。
使用專為指標設計的時序資料庫。 Prometheus 是一款流行且功能強大的資料庫。 InfluxDB 也是不錯的選擇。雲端服務供應商也提供各自的解決方案,例如 AWS 的 CloudWatch 和 Google Cloud 的 Cloud Monitoring。
建立儀表板,讓您一目了然地了解系統運作狀況。凌晨兩點出現問題時,您需要快速查看異常情況。資料庫運作緩慢嗎?錯誤率飆升嗎?某個依賴項宕機了嗎?
一旦擺脫單體架構,分散式追蹤就至關重要。當一個請求跨越多個服務時,如果沒有追踪,就很難了解時間都消耗在了哪裡。像 Jaeger、Zipkin 這樣的工具,或像 AWS X-Ray 這樣的雲端原生解決方案,會將追蹤 ID 傳播到請求涉及的所有服務,讓你了解完整的流程圖。
設定異常警報。您無需手動監控儀錶板。當錯誤率超過閾值、回應時間變慢、磁碟空間不足或佇列積壓時,都應發出警報。但要注意不要對所有情況都發出警報——警報疲勞是真實存在的。如果人們因為警報總是誤報而忽略它們,他們就會錯過真正的問題。
延遲監控應使用百分位數,而非平均值。平均反應時間是一個具有誤導性的指標。如果 99% 的請求速度很快,但有 1% 的請求速度極慢,即使使用者體驗非常糟糕,平均值看起來也可能正常。應監控第 50 個百分位數(中位數)、第 95 個百分位數和第 99 個百分位數。第 99 百分位數可以告訴你速度最慢的 1% 的請求到底有多慢。
負載平衡器負責將流量分配到各個應用程式實例中。它們對於橫向擴展和高可用性都至關重要。
第七層(應用層)負載平衡器能夠理解 HTTP 協議,並可根據 URL 路徑、標頭或 Cookie 做出路由決策。它們可以將/api/*請求路由到 API 伺服器,將/admin/*請求路由到管理伺服器。它們可以執行 SSL 終止,處理 HTTPS 加密/解密,從而使應用程式伺服器無需進行此類操作。它們還可以實現複雜的路由規則和流量整形策略。
第四層(傳輸層)負載平衡器工作在 TCP 層。它們更簡單、速度更快,但靈活性較差。它們無法查看 HTTP 請求內部內容來進行路由決策。對大多數 SaaS 應用而言,第七層(傳輸層)負載平衡器才是更合適的選擇。
健康檢查至關重要。負載平衡器需要知道哪些執行個體運作狀況良好,能夠處理請求。在應用程式中實作一個健康檢查端點,例如/health ,如果應用程式已準備好處理流量,則傳回 200 OK;否則傳回 5xx 錯誤。
讓你的健康檢查更有意義。不要只是返回靜態的“OK”響應。檢查你的應用程式是否可以存取其資料庫、快取和關鍵依賴項。如果你的應用程式無法與資料庫通信,即使進程正在執行,也不應該接收任何流量。
在部署期間使用連線耗盡機制。當您將執行個體從服務中移除時,您不希望突然終止正在進行的請求。連線耗盡機制會指示負載平衡器停止向該實例發送新請求,同時允許現有請求完成。經過一段時間的超時(通常為 30-60 秒)後,該實例將被完全移除。
在負載平衡器層級實施速率限制。這為惡意客戶端提供了最後一道防線,防止它們存取您的應用程式伺服器。 AWS ALB 原生支援此功能。對於更複雜的速率限制(例如按使用者、按 API 金鑰、自訂規則),您可以使用 Kong 之類的工具,或在應用程式中使用 Redis 來實現。
隨著業務規模的擴大,地理負載平衡變得至關重要。如果公司在歐洲設有伺服器,那麼歐洲用戶就不應該存取美國的伺服器。 Route 53、Cloudflare 和其他 DNS 供應商都支援地理路由,它們會根據使用者的位置傳回不同的 IP 位址。
考慮使用內容分發網路 (CDN) 來託管靜態資源。 CloudFront、Fastly 和 Cloudflare 都是不錯的選擇。 CDN 會將您的內容快取到世界各地的邊緣節點,從而顯著降低遠離來源伺服器使用者的延遲。對於全球 SaaS 產品而言,這可能決定使用者體驗的流暢度。
如果 API 回應可快取(例如,每個使用者請求的 GET 請求都相同),您甚至可以在 CDN 層快取這些回應。這可以大幅減輕來源伺服器的流量壓力。使用合適的 Cache-Control 標頭來控制哪些內容需要快取以及快取時長。
指望一切順利不是策略。你需要的是應對突發狀況的計劃,而不是假設狀況會如何發展的計劃。
備份是您的安全保障。務必實施資料庫的自動化定期備份。不要只備份到同一資料中心;使用跨區域備份。 AWS 和 Google Cloud 讓這一切變得輕鬆簡單。定期測試您的備份。我見過一些團隊直到需要恢復備份時才發現備份已損壞。
制定一套完善的復原流程。災難發生時,你肯定不想手忙腳亂地摸索。進行災難復原演練。從備份中還原一個完整的環境。計時測試所需時間。找出流程中的漏洞並加以改進。
為資料庫實施時間點復原。這樣,您就可以還原到保留期內的任何時間點,而不僅僅是最新備份。當您需要從邏輯損壞中恢復時(例如糟糕的遷移、導致資料損壞的錯誤或意外刪除),這一點至關重要。
考慮您的復原時間目標 (RTO) 和復原點目標 (RPO)。 RTO 表示您可以承受的停機時間。 RPO 表示您可以承受的資料遺失量。這些目標會影響您的架構決策。如果您需要幾分鐘的 RTO,則需要隨時準備接管的熱備用系統。如果您可以容忍數小時的停機時間,則更簡單(也更經濟)的方案就足夠了。
多區域部署可提供最高的可用性,但也帶來了顯著的複雜性。您需要跨區域複製資料、智慧地路由流量並處理故障轉移場景。大多數 SaaS 產品最初並不需要如此高的冗餘級別,但隨著業務成長,尤其是當您服務於具有嚴格 SLA 要求的企業客戶時,冗餘就變得至關重要。
針對常見故障場景編寫運作手冊。如果資料庫宕機怎麼辦?如果某個區域不可用怎麼辦?如果關鍵的第三方服務故障怎麼辦?記錄下操作步驟、聯絡人以及升級流程。凌晨三點處理故障時,你需要的是一份清晰的清單,而不是一張白紙。
除了基礎設施之外,你的應用程式程式碼本身也需要考慮到可擴展性。糟糕的程式碼甚至會讓最好的基礎設施也崩潰。
在我除錯過的效能問題中,由糟糕的查詢語句引起的比其他所有問題加起來都多。你們的 ORM 讓編寫產生糟糕 SQL 的程式碼變得輕而易舉。
一律使用明確列名的SELECT ,切勿SELECT * 。取得不需要的列會浪費網路頻寬和記憶體。如果您只需要使用者的姓名和電子郵件地址,則無需取得其整個個人資料。
務必經常使用EXPLAIN ANALYZE 。它可以準確地顯示資料庫如何執行查詢、使用了哪些索引以及時間都消耗在了哪裡。如果看到對大型表進行順序掃描,則很可能需要建立索引。
盡可能使用批量操作。與其使用 100 個單獨的 INSERT 語句插入 100 行資料,不如使用 INSERT 語句插入 100 行資料。與其逐行更新資料,不如使用帶有 WHERE 子句的 UPDATE 語句來匹配多行資料。
使用LIMIT和OFFSET進行分頁時要格外小心。偏移量越大,效能下降越嚴重。例如,要取得第 1000 頁結果(偏移量為 50,000),資料庫必須讀取 50,000 行資料,然後丟棄其中一部分。基於遊標的分頁透過在索引列上使用 WHERE 子句來避免這種情況。
謹慎使用資料庫級鎖。行級鎖通常沒問題,但表級鎖可能會造成嚴重的效能瓶頸。理解樂觀鎖和悲觀鎖之間的區別,並根據你的使用情境選擇合適的策略。
對於不修改資料的查詢,請考慮使用唯讀交易。某些資料庫會針對此類查詢進行不同的最佳化。在 PostgreSQL 中,您可以使用SET TRANSACTION READ ONLY來表明您的意圖。
在規模化應用中,反規範化可能成為你的得力助手。誠然,它違反了規範化原則,也確實會造成資料冗餘。但有時,效能提升帶來的效益遠大於其弊端。如果你每次頁面載入都需要連接五個表,那麼不妨考慮儲存預先計算好的聚合結果或反規範化視圖。
對於頻繁執行的複雜查詢,請使用資料庫視圖。視圖可以封裝複雜的 SQL 邏輯,使應用程式程式碼更簡潔,並確保資料查詢方式的一致性。
如果處理不當,文件上傳很容易成為瓶頸。切勿將上傳的檔案儲存在資料庫中—這會使資料庫膨脹,並降低涉及這些行的所有操作的速度。切勿將檔案儲存在應用程式伺服器的本機檔案系統中—這會使橫向擴展變得困難,並導致資料持久性問題。
使用物件儲存服務,例如 S3、Google Cloud Storage 或 Azure Blob Storage。這些服務專為大規模儲存和提供文件而設計,具有持久性、高可用性和相對低廉的價格。
實作客戶端直接上傳到物件儲存。傳統的方式——客戶端上傳到您的伺服器,伺服器再上傳到 S3——會使上傳時間翻倍,並增加伺服器負載。而另一種方法是產生一個預簽名 URL,讓客戶端可以直接上傳到 S3。您的伺服器只需驗證上傳請求並產生 URL,這樣就非常輕量級。
異步處理文件。如果需要產生縮圖、轉碼影片、提取文字或對上傳的檔案進行任何處理,請在背景任務中執行。立即向使用者回傳回應,並非同步處理文件。
在物件儲存前端使用 CDN。 S3 是全球分散式存儲,但仍可從 CDN 快取中獲益,尤其是對於頻繁存取的檔案。在 S3 前端部署 CloudFront 是常見的模式。
實施適當的存取控制。對私有內容使用簽名 URL。不要將所有內容都公開可讀。仔細考慮您的資料模型—哪些使用者應該能夠存取哪些檔案?
考慮為大檔案啟用斷點續傳功能。如果使用者在上傳大檔案過程中網路連線中斷,則不應要求他們重新開始上傳。 Amazon S3 支援分段上傳,可實現此功能。
後台作業對於可擴展性至關重要,但需要謹慎實施。
正如我之前提到的,作業應該是冪等的。但這一點值得再次強調,因為它非常重要。如果一個作業處理付款,執行兩次不應該導致客戶被收取兩次費用。如果一個作業發送電子郵件,執行兩次不應該發送兩封電子郵件。
使用唯一辨識碼和資料庫約束來確保冪等性。在執行關鍵操作之前,請檢查該操作是否已執行。使用事務來保證檢查和操作的原子性。
實現適當的錯誤處理和重試機制。作業可能會失敗—例如網路問題、臨時服務中斷或程式錯誤。您的作業框架應使用指數退避演算法自動重試失敗的作業。經過一定次數的重試後,將作業移至死信佇列進行人工調查。
為任務設定超時時間。無限期執行的任務最終會耗盡你的工作資源。如果任務在合理的時間內沒有完成,請終止它並將其標記為失敗,以便可以重試或進行調查。
監控你的隊列。如果任務積壓速度超過處理速度,你需要調查一下。也許你需要更多工作進程。也許是某個錯誤導致任務運作緩慢。也許某種特定類型的任務突然比平常產生了更多任務。
使用優先順序確保關鍵任務優先運作。並非所有任務都同等重要。發送密碼重設郵件的任務比產生分析報告的任務更緊急。大多數隊列系統都支援優先權等級。
考慮對某些類型的任務實施速率限制。如果您向第三方服務發出有速率限制的 API 呼叫,則需要確保您的後台任務遵守這些限制。可以使用令牌桶或類似演算法來限制任務執行。
我們之前已經提到過緩存,但它值得更深入地探討,因為有效的緩存可以將系統的容量倍增。
快取預熱是一種在請求資料之前主動將其載入到快取中的技術。這對於已知會被頻繁存取的資料非常有用,例如熱門產品、流行內容等。您可以設定一個後台任務,定期刷新快取中的這些資料。
快取搶注是一個需要注意的問題。當一個熱門快取專案過期時,就會發生這種情況,數百個請求會同時湧入資料庫試圖重新產生該快取專案。解決方案是使用鎖定機制-當快取未命中時,第一個請求會取得鎖,重新產生緩存,然後釋放鎖定。其他請求則等待鎖的釋放,或使用略微過期的資料。
考慮使用兩層快取-每個應用程式實例中都包含一個本地記憶體快取和一個分散式快取(例如 Redis)。先檢查本地緩存,然後是分散式緩存,最後是資料庫。這樣既能減少頻繁存取資料的網路跳轉次數,又能確保實例間共享快取。
快取失效需要與你的領域事件關聯。當某些變化影響到快取資料時,你需要使相關的快取條目失效或更新。這通常意味著在設計功能時就應該考慮快取失效,而不是事後才想起。
使用能夠編碼所有影響快取資料的參數的快取鍵。例如,如果您快取的是按類別篩選並按價格排序的產品列表,則快取鍵應同時包含類別和排序順序。否則,您將提供錯誤的資料。
根據資料變更頻率和資料過期容忍度設定合適的 TTL(生存時間)。配置資料的 TTL 可能以小時為單位,使用者設定檔的 TTL 可能以分鐘為單位,而即時股票價格可能根本不會被快取。
考慮使用快取控制標頭進行 HTTP 等級快取。 ETag 和 Last-Modified 標頭允許客戶端快取回應,並有效率地檢查快取版本是否仍然有效。這不僅可以節省伺服器處理能力,還可以節省網路頻寬。
隨著您的 API 變得越來越受歡迎,您需要保護它免受濫用,並確保客戶之間的公平使用。
實施多級速率限制。用戶級限制可防止單一用戶對系統造成過載。 IP 位址級限制可防止未經身份驗證的攻擊。端點級限制可防止濫用高成本操作。
使用令牌桶演算法進行速率限制。每個使用者都會獲得一個包含一定數量令牌的令牌桶。每次 API 請求都會消耗一個令牌。令牌桶以恆定速率補充令牌。這既能允許突發性請求,又能確保長期平均速率的穩定。
在 API 回應中傳回對應的標頭: X-RateLimit-Limit 、 X-RateLimit-Remaining和X-RateLimit-Reset 。這樣客戶端就可以查看其目前狀態並相應地調整其行為。
當超過速率限制時,傳回 429 Too Many Requests 狀態碼,並附帶 Retry-After 標頭,指示客戶端何時可以再次嘗試。
考慮針對不同服務等級實施不同的速率限制。免費用戶每小時可以有 1000 次請求,而付費用戶每小時可以有 10000 次請求。企業客戶可以在合約中協商客製化的速率限制。
對不同操作賦予不同的權重。傳回快取資料的簡單 GET 請求比觸發複雜處理的 POST 請求成本更低。例如,POST 請求可能消耗 10 個令牌,而 GET 請求只消耗 1 個令牌。
實現優雅降級。當請求超出限制時,不要完全阻止請求,而是降低請求優先順序、從過期的快取中提供服務,或減少傳回的資料量。
對於全球 SaaS 產品而言,正確處理時區和國際化並非可有可無,而是不可或缺的。
資料庫中所有時間戳應以 UTC 格式儲存。切勿儲存不包含時區資訊的本地時間。向使用者顯示時間時,請在應用程式層將 UTC 時間轉換為使用者的本機時區時間。
在資料庫中使用正確的日期/時間類型。在 PostgreSQL 中,使用具有timestamp with time zone (或timestamptz ),而不是timestamp without time zone 。前者儲存的是實際時間點;後者儲存的是本地時間,在不同的時區可能代表不同的時間。
處理日期運算時要格外小心。由於夏令時的存在,時間戳加上 24 小時並不等於加上一天。請使用能夠正確處理日期/時間的函式庫,例如 JavaScript 中的 moment.js(雖然現在已被棄用)、date-fns 或 Luxon;Python 中的 datetime 模組;以及 Go 語言中的 time 套件。
為了實現國際化 (i18n),請將面向使用者的字串與程式碼分開。在程式碼中使用翻譯鍵,並為每種語言建立查找檔案。這樣,翻譯人員就可以獨立於工程師來開展工作。
不要將翻譯後的字串拼接在一起-不同語言的詞序不同。請使用參數化的翻譯字串:“歡迎,{name}!”,而不是“歡迎,” + name + "!”。
請注意,文字在不同語言中會放大。英文文字翻譯成西班牙文或法文時,通常會放大 20-30%。您的使用者介面需要妥善處理這種情況。
數字、貨幣和日期在不同的地區格式不同。請使用能夠處理這些差異的國際化庫。例如,美國的 1,234.56 美元在許多歐洲國家是 1.234.56 美元。
要打造真正全球化的產品,您需要考慮字元集和編碼。 UTF-8 是最佳選擇。請確保您的資料庫、應用程式和用戶端都使用 UTF-8 編碼。
隨著規模擴大,安全問題也變得更加複雜。更大的攻擊面、更多的用戶、更多的資料——所有這些都會增加風險。
我們之前已經談到了身份驗證,但授權(確定用戶可以做什麼)值得單獨關注。
實施基於角色的存取控制 (RBAC),或者,對於更複雜的需求,實施基於屬性的存取控制 (ABAC)。使用者擁有角色,角色擁有權限,應用程式會在允許操作之前檢查權限。
採用基於策略的授權決策方法。不要將授權檢查分散在整個程式碼庫中,而是將它們集中起來。像 Open Policy Agent (OPA) 這樣的工具讓你將政策定義為應用程式可以查詢的資料。
在適當情況下快取授權決策。權限檢查可能涉及資料庫查詢或對授權服務的呼叫。如果您對相同使用者重複檢查相同權限,請快取結果。
但要注意快取授權資料——權限變更時需要使其失效。如果您撤銷了使用者的管理員角色,則該變更必須立即生效,而不是在快取過期後才生效。
對敏感操作實施審計日誌記錄。當有人存取個人辨識資訊 (PII)、修改財務記錄或執行管理操作時,必須記錄足夠詳細的訊息,以便重現事件經過。這對於合規性和安全事件調查至關重要。
遵循最小權限原則。使用者和服務應僅擁有完成其工作所需的最低權限。如果應用程式資料庫使用者只需要執行 SELECT、INSERT 和 UPDATE 操作,則不要授予其 DROP TABLE 權限。
如果正確使用參數化查詢或 ORM,SQL 注入攻擊應該不可能發生,但由於後果極其嚴重,因此仍然值得一提。切勿將使用者輸入拼接到 SQL 查詢中。請使用參數化查詢,將使用者輸入作為參數傳遞,而不是作為 SQL 字串的一部分。
透過在 HTML 中渲染使用者輸入時正確轉義,可以防止跨站腳本攻擊 (XSS)。大多數現代框架預設都會這樣做,但使用明確渲染原始 HTML 的功能時需要格外小心。
跨站請求偽造 (CSRF) 攻擊可以透過使用 CSRF 令牌來防止-CSRF 令牌是與使用者會話綁定的唯一令牌,必須包含在任何狀態變更請求中。大多數框架都預設提供了此功能。
實施內容安全策略 (CSP) 標頭以防止各種注入攻擊。 CSP 可讓您指定哪些內容來源是可信任的,從而防止瀏覽器執行惡意腳本。
務必在所有地方使用 HTTPS,沒有任何藉口。 Let's Encrypt 提供免費憑證。沒有任何理由透過 HTTP 提供任何內容。使用 HTTP 嚴格傳輸安全性 (HSTS) 標頭,確保瀏覽器僅透過 HTTPS 連線。
實施正確的輸入驗證。切勿輕信客戶端輸入。驗證資料類型、範圍、格式和長度。電子郵件欄位應驗證是否為電子郵件地址。數量欄位應為合理範圍內的正整數。
使用參數化 SQL 查詢,但也要在應用程式層進行驗證。縱深防禦意味著擁有多層保護。
防範登入端點遭受暴力破解攻擊。多次嘗試失敗後,實施指數退避或臨時帳戶鎖定。多次失敗後,考慮使用驗證碼(CAPTCHA)。
隨著規模擴大,您需要管理的金鑰也會越來越多——資料庫密碼、API金鑰、加密金鑰、憑證私鑰等等。安全地管理這些金鑰至關重要。
切勿將金鑰提交到版本控制系統中。請使用環境變數或專用的金鑰管理服務。 AWS Secrets Manager、Google Cloud Secret Manager 和 HashiCorp Vault 等服務都是專為此目的而設計的。
定期輪換密鑰。盡可能實現自動輪換。某些服務支援自動輪調-例如,金鑰管理員可以自動輪替 RDS 資料庫密碼。
針對不同的環境使用不同的密鑰。你的測試環境不應該使用與生產環境相同的資料庫密碼。這樣可以限制密鑰洩漏後的影響範圍。
對靜態金鑰進行加密。如果您將金鑰儲存在資料庫或設定管理系統中,請務必對其進行加密。使用金鑰管理服務 (KMS) 來管理加密金鑰。
限制對密鑰的存取。使用身分和存取管理 (IAM) 策略來控制哪些使用者和服務可以存取哪些金鑰。遵循最小權限原則。
審核機密資訊存取。了解機密資訊何時被存取以及由誰存取。這有助於檢測洩漏的憑證。
對靜態資料和傳輸中的資料進行加密。大多數雲端服務供應商都為資料庫、物件儲存和區塊儲存提供靜態資料加密。啟用此功能。在現代硬體上,效能開銷微乎其微。
對於敏感資料,請考慮應用層加密。在將資料儲存到資料庫之前對其進行加密。這可以防止資料庫遭到入侵,並提供縱深防禦。缺點是您無法直接查詢加密欄位——您需要加密查詢值並進行精確匹配。
使用強大且現代的加密演算法。對稱加密可使用 AES-256,非對稱加密可使用 2048 位元金鑰的 RSA 或橢圓曲線加密。不要自行編寫加密演算法,請使用經過充分測試的程式庫。
為應用層加密實施適當的金鑰管理。採用金鑰層級結構-主金鑰用於加密資料加密金鑰,資料加密金鑰再用於加密實際資料。定期輪換資料加密金鑰。將主金鑰儲存在硬體安全模組 (HSM) 或雲端金鑰管理系統 (KMS) 中。
請注意合規性要求。信用卡資料需遵守 PCI DSS,健康資訊需遵守 HIPAA,歐盟居民個人資料需遵守 GDPR—這些都有特定的加密要求。
GDPR、CCPA 和其他隱私權法規會影響您處理使用者資料的方式。即使您目前不受這些法規的約束,從一開始就考慮隱私問題也是一種很好的做法。
實施資料最小化原則。只收集真正需要的資料。不要將敏感資料儲存超過必要的時間。
為使用者提供資料存取權限。用戶應該能夠匯出您擁有的關於他們的所有資料。實作一個用於產生這些匯出檔案的介面或管理工具。
實施資料刪除機制。用戶應該能夠刪除自己的帳戶並清除所有資料。這很複雜——出於法律或財務原因,您可能需要保留一些資料,但個人資料應該被徹底刪除或匿名化。
在非生產環境中對資料進行匿名化處理。您的測試環境和開發環境不應包含生產用戶資料。使用真實但虛假的資料,或透過移除或雜湊處理辨識資訊來匿名化生產資料。
實施完善的同意管理。使用者應該知道你收集哪些資料以及收集原因。他們應該能夠選擇加入或退出各種類型的資料收集。
考慮資料駐留問題。某些法規要求資料儲存在特定的地理區域。這會影響您的基礎架構設計—您可能需要將資料複製到特定區域,並確保資料不會離開這些區域。
一旦架構穩固,最佳化就可以從相同的基礎設施中榨取大量額外容量。
你無法優化你沒有衡量的東西。效能分析工具可以幫助你確定應用程式的時間都消耗在了哪些地方。
使用 New Relic、Datadog 等應用效能監控 (APM) 工具,或 Elastic APM 等開源替代方案。這些工具可以幫助您了解應用程式在生產環境中的效能,突出顯示緩慢的資料庫查詢、耗時的函數呼叫和外部服務延遲。
在實際負載下對應用程式進行效能分析。在開發環境中耗時 10 毫秒的函數,在生產環境資料量下可能會表現得截然不同。請在測試環境中使用與生產環境類似的資料。
重點關注熱路徑——即執行頻率最高的程式碼。優化一個每小時只呼叫一次的函數效果不大。而優化一個每次請求都會呼叫的函數,則可以顯著提升整體效能。
運用 80/20 法則。通常,應用程式 80% 的時間都消耗在 20% 的程式碼上。找到這 20% 的程式碼並進行最佳化。
留意 N+1 查詢。這些查詢會悄無聲息地降低效能,通常只有在高負載下才會出現。使用能夠偵測這些模式的 APM 工具。
後端可擴展性固然重要,但如果前端速度慢,使用者會覺得整個應用程式都很慢。
盡量減小 JavaScript 包的大小。過大的套件需要更長的下載和解析時間。使用程式碼分割技術,僅載入目前頁面所需的 JavaScript 程式碼。使用 tree shaking 演算法來移除未使用的程式碼。
圖片和其他媒體採用延遲載入。不加載不可見的圖片。使用交叉觀察者 API,在圖片捲動到視圖中時載入它們。
優化圖片。使用適當的格式-照片使用 WebP,圖示和標誌使用 SVG。使用srcset屬性提供響應式圖片,避免行動用戶下載桌面尺寸的圖片。
對靜態資源積極啟用瀏覽器快取。為不可變檔案(例如 JavaScript 套件、檔案名稱中包含內容雜湊值的圖片)設定較長的快取生命週期。啟用快取清除(檔案名稱中包含內容雜湊值),以便使用者在部署新版本時能夠取得到新版本。
實現資源提示。 <link rel="preconnect">事先建立與所需來源的連結。 <link rel="dns-prefetch">提前解析 DNS。 <link rel="preload">指示瀏覽器提前載入關鍵資源。
盡量減少阻塞渲染的資源。 CSS 會阻塞渲染,因此請將關鍵 CSS 內聯,其餘部分則非同步載入。 JavaScript 可能會阻塞解析,因此請使用async或defer屬性,或將腳本放在 body 標籤的末尾載入。
使用 CDN 來分發前端資源。這可以降低遠離伺服器用戶的延遲,並減輕來源伺服器的流量壓力。
為漸進式 Web 應用功能實作 Service Worker。 Service Worker 可以將資源快取到本機,從而實現離線功能並顯著加快後續載入速度。
除了查詢最佳化之外,資料庫層面的最佳化也能顯著提升效能。
正確的索引至關重要,但索引並非免費。它們可以加快讀取速度,但會降低寫入速度。每次插入或更新操作都必須更新所有相關的索引。找到適合您工作負載的最佳平衡點。
對於按多列篩選的查詢,請使用複合索引。如果您經常查詢WHERE user_id = ? AND created_at > ? ,請在(user_id, created_at)上建立索引。
了解索引類型。 B 樹索引(預設索引)適用於相等性和範圍查詢。哈希索引速度更快,適合精確匹配,但無法進行範圍查詢。 PostgreSQL 中的 GIN 和 GiST 索引專用於全文搜尋和幾何資料。
盡可能使用覆蓋索引。覆蓋索引包含查詢所需的所有列,因此資料庫根本不需要存取表本身。這大大提高了速度。
定期執行清理和分析操作。在 PostgreSQL 中,更新和刪除操作產生的死元組會不斷累積,導致表膨脹並降低效能。定期執行清理操作可以回收這些空間。分析查詢規劃器用於做出決策的更新統計資料。
根據工作負載調整資料庫配置。預設值較為保守,適用於小型資料庫。隨著規模的擴大,您需要增加shared_buffers 、調整work_mem 、最佳化檢查點設置,並根據硬體和工作負載配置其他參數。
考慮對大型表進行分區。我們之前討論過分區在資料管理方面的作用,但它也能提升查詢效能。如果大多數查詢只需要最近的資料,按日期分區意味著它們只會掃描相關的分區。
對於計算量較