軟件可擴展性是一個有趣的話題。實現(xiàn)軟件可擴展性涉及很多因素,我們在本文將討論一些與開發(fā)和運維方面相關(guān)的因素。
我們將深入討論如何編寫軟件(軟件開發(fā))以及如何運行軟件(運維)來實現(xiàn)軟件可擴展性。對于初學(xué)者來說,成本和可擴展性通常是成比例的。
1. 什么是軟件可擴展性
Full-scale blog 將軟件可擴展性定義為:
軟件可擴展性是工具或系統(tǒng)的一種屬性,可以根據(jù)用戶需求增加其容量和功能。可擴展的軟件可以在適應(yīng)變化、升級、檢修和資源伸縮的同時保持穩(wěn)定。
因此,如果軟件可以彈性的處理負載,當(dāng)請求量增加時分配更多資源(通常是動態(tài)分配) ,那我們可以說這個軟件是可擴展的。現(xiàn)實中要實現(xiàn)這一點,我們還需要重視代碼部分。
2. 開發(fā)視角的軟件可擴展性
軟件工程師應(yīng)該知道如何編寫可擴展的軟件。你應(yīng)該專注于優(yōu)先編寫能使軟件易于擴展的代碼。編寫勉強可用的軟件很容易,但編寫易于測試、可維護、易擴展的代碼卻很難。以下是一些能讓軟件更易擴展的編程方法。
可擴展軟件的高性能代碼
軟件應(yīng)用程序編寫時可以只要求能用就行,也可以考慮到軟件的可擴展性、維護性和彈性。
選擇合適算法
基于時間和空間復(fù)雜度,選擇適合場景的合適算法可以產(chǎn)生很好的效果。
了解大 O 符號和流處理來對抗空間復(fù)雜度,對編寫可擴展的軟件非常有幫助。
例如,您可以采用二分搜索代替線性搜索來加快算法執(zhí)行。在空間復(fù)雜度要求高的場景下,您可以基于少量內(nèi)存的流式處理來實現(xiàn)小內(nèi)存復(fù)制大文件。看看這個用來可視化展示排序算法的 6 分鐘視頻。
https://www.youtube.com/watch?v=kPRA0W1kECg
更好的內(nèi)存管理
作為一位軟件工程師,您應(yīng)該關(guān)心像內(nèi)存管理、垃圾收集這樣的事情,不要讓它們成為可擴展性的障礙。對于可擴展軟件來說,預(yù)測資源爭用的情況并為其編寫代碼也是至關(guān)重要的。
選擇高性能函數(shù)庫
還有其他有助于軟件可擴展性的方法,包括對比和使用更多的高性能解決方案。例如,您可以使用 javascript 代替 lodash 來獲得更快更高性能。
另外,不要僅僅因為某個庫或軟件包很流行就使用它,還要檢查性能和軟件可擴展性的影響。
例如,您可以使用 Day.js 代替 Moment.js 來執(zhí)行簡單的日期操作。需要的話,還可以使用原生方法來使軟件更具可擴展性。
異步處理
想象一下,當(dāng)客戶已經(jīng)成功下單需要發(fā)送一封訂單確認郵件,您會怎么做?我總是會建議大家異步地執(zhí)行它,因為它是流程中非關(guān)鍵的部分。
使用隊列和消費者
您可以輕松地設(shè)置隊列和消費者,來完成下單后電子郵件的發(fā)送任務(wù)。即便郵件發(fā)送晚了 1 分鐘也沒問題。
如果您的訂單量很大,可以通過擴展消費者數(shù)量來降低延遲。任何非關(guān)鍵或非阻塞的任務(wù)都可以推到后臺異步完成,這有助于實現(xiàn)無故障的最優(yōu)化可用資源。
適當(dāng)使用異步代碼
異步處理的另一個例子是使用異步代碼。根據(jù)編程語言的具體情況,您應(yīng)該都能夠?qū)⒛承┤蝿?wù)推送到后臺執(zhí)行。當(dāng)任務(wù)被執(zhí)行時,可以發(fā)送一個響應(yīng)表明它已經(jīng)被調(diào)度過。您可以查看一個 Node.js 的異步響應(yīng)示例。當(dāng)然,這取決于您所選擇的語言,有些語言(比如 PHP)可能不提供開箱即用的異步代碼。
https://geshan.com.np/blog/2020/11/nodejs-for-php-developers/#node.js-code-execution-is-async-and-non-sequential
為可擴展軟件編寫無狀態(tài)的程序
無狀態(tài)是高可擴展性軟件的先決條件。正如 Redhat 在比較無狀態(tài)與有狀態(tài)時提到的“將無狀態(tài)事務(wù)想象成一臺自動售貨機:一個請求對應(yīng)一個響應(yīng)”,而將有狀態(tài)程序描述為“您可以將有狀態(tài)事務(wù)視為與同一個人進行中的多輪對話”。
無狀態(tài)軟件在請求之間不會共享任何東西,也不依賴于本地文件系統(tǒng)之類的東西。
不要使用本地文件系統(tǒng)
如果需要保存文件,應(yīng)該使用可靠的遠程系統(tǒng)如可進行訪問控制的存儲,例如 Amazon S3 bucket。
這使得它很容易保存文件并通過 CDN 來提供可擴展的服務(wù),它通過動靜分離來提高軟件擴展性。如圖像和其他類似 PDF 文件的靜態(tài)內(nèi)容,通過使用 CDN 來提供服務(wù)會比網(wǎng)站服務(wù)更高效。利用 Apache 或 Nginx 構(gòu)建網(wǎng)站服務(wù)來提供動態(tài)內(nèi)容,會比提供靜態(tài)內(nèi)容更好。
使用客戶端會話替換服務(wù)器端會話
另一個經(jīng)典例子是不在 web 應(yīng)用使用服務(wù)端會話,而是使用客戶端 cookie。
您可以輕松地用使用類似 Json Web Token (JWT) 的方案替換服務(wù)器端會話來進行身份驗證和授權(quán)。
JWTs 可以在每個請求中作為 header 或者 cookie 的一部分被輕松的從客戶端傳給服務(wù)端。因為服務(wù)器可以像牲口而非寵物一樣工作,擴展軟件變得非常容易。如果您必須使用會話,那么使用類似 Redis 的數(shù)據(jù)庫而不要保存在本地文件系統(tǒng),以保證服務(wù)器可以輕松的被替換。
http://cloudscaling.com/blog/cloud-computing/the-history-of-pets-vs-cattle/
這里的關(guān)鍵點是,不要留戀您的服務(wù)器,它們應(yīng)該是一次性并根據(jù)負載彈性配置的。這樣我們就可以通過編寫無狀態(tài)軟件來實現(xiàn)易擴展和高可用成為可能。
3. 運維視角的軟件可擴展性
關(guān)于運維和平臺這兩個表述,我指的是在哪里以什么方式部署和運行軟件,另外還涵蓋這些系統(tǒng)的架構(gòu)以及它們?nèi)绾谓换ァ?/p>
軟件部署的位置是至關(guān)重要的。
如果您的用戶在悉尼,而軟件部署在歐洲,它將有很大的網(wǎng)絡(luò)延遲。
類似的,如果組件布局不好或選擇不當(dāng)都將產(chǎn)生負面影響。讓我們看一下在運維層面對軟件可擴展性有至關(guān)重要影響的因素。
垂直擴展與水平擴展
這是一個關(guān)于把服務(wù)器類比成牲口還是寵物的延伸討論。想象一下,您正在管理一個相當(dāng)受歡迎的電子商務(wù)網(wǎng)站,該網(wǎng)站每天約有 500 個訂單和 5 萬個獨立訪問用戶。您有一個規(guī)格接近 Amazon EC2 m 5.4 xlarge 的大型 web 服務(wù)器,它有 16 核 CPU 以及 64 GB 的大內(nèi)存。我們假設(shè)在上面運行 Woo Commerce 商店,包括網(wǎng)站服務(wù)和 MySQL 數(shù)據(jù)庫都運行在這同一臺服務(wù)器上。
現(xiàn)在,距離黑色星期五只有 3 個月了,公司打算做一個大規(guī)模的電視廣告推廣,預(yù)計流量在節(jié)日期間有 5-7 倍的增加。管理層將在廣告方面投入大量資金,在這 4-5 天內(nèi)網(wǎng)站不能癱瘓。
預(yù)計該網(wǎng)站在這 3-4 天內(nèi),每天將有 30 萬以上的獨立用戶訪問和 3 千以上的訂單。
您現(xiàn)在有兩個選項來擴展應(yīng)用程序,要么垂直擴展(scale-up) ,要么水平擴展(scale-out)。
垂直擴展(Scale-Up)
如果選擇垂直擴展,那么需要增加更多的硬件資源來解決這個問題。
您可以改用一臺 EC2 m5.24 xlarge 的機器,它擁有 96 核 CPU 和 384 GB 內(nèi)存。
CPU 和 內(nèi)存 是老機器的 6 倍,所以理論上它應(yīng)該足以支撐。
但有 3 個重要問題,首先您將需要一點時間停機來升級硬件,其次也是最重要的原因是這臺機器會造成單點故障。考慮到網(wǎng)站負載,數(shù)據(jù)庫很可能由于某個問題而崩潰。稍后如果流量沒有預(yù)期的那么大,您還將為避免過度浪費資源進行收縮操作。
水平擴展(Scale-Out)
另一種選擇是水平擴展,您將嘗試獲得許多較小的 EC2 實例,比如 8-50 個 t3.mediums 實例。
每個實例將擁有 2 核 CPU 和 4 GB 的內(nèi)存。因此,一組包括 50 個 t3.mediums 實例的集群可以為您提供總共 100 核 CPU 和 200 GB 內(nèi)存。要在這些新的 EC2 實例集群之間均勻分配負載,可以使用 Amazon 應(yīng)用程序負載均衡器。
為了使應(yīng)用程序更具可擴展性,您可以使用具有 32 個核 CPU 和 128 GB 內(nèi)存的 Amazon RDS db.m5.8 xlarge 實例。根據(jù)需要,您還可以配置備份。這時您有 50 臺服務(wù)器可以使用,假如有 3 臺壞了可以馬上換上 3 臺新的。
如果負載偏低只有 3 個實例在運行,當(dāng)流量激增時分分鐘就可以增加 20 個。
在打折季結(jié)束后,您可以將 DB 縮放到 db.m5.large,這足以滿足每天 500 個訂單的情況。
考慮到這點很重要,讓我們在下面可視化地解釋一下。
這是 Docker 和 Kubernetes 的一部分亮點,您可以將工作任務(wù)打包進輕量級的容器,而 Kubernetes 可以管理水平擴展和滾動部署這些容器。這些年 Docker 已經(jīng)改變了我們工程師的工作方式。
https://geshan.com.np/blog/2018/11/4-ways-docker-changed-the-way-software-engineers-work-in-past-half-decade/
這里要提到的一點是擴展關(guān)系數(shù)據(jù)庫是非常困難的。即使有了分片之類的技術(shù)后,如果你不清楚自己在做什么,垂直擴展關(guān)系數(shù)據(jù)庫會比水平擴展更容易些。這里的 Amazon 就是一個例子,同樣的概念可以應(yīng)用于其他任何主要的云供應(yīng)商,比如谷歌云或 Azure。這就引出了我要講的下個要點,NoSQL 數(shù)據(jù)庫的使用。
使用 NoSQL 提高軟件可擴展性
在上面的例子中,如果您的在線商店網(wǎng)站上有 20 個人,可以使用關(guān)系數(shù)據(jù)庫提供服務(wù)。對于每個用戶的每個請求,應(yīng)用程序都會到達關(guān)系數(shù)據(jù)庫,雖然慢,但不會造成嚴重后果。現(xiàn)在想象 120 個用戶同時在線,性能很可能已經(jīng)很明顯的嚴重下降,我們可以看到基于預(yù)分配的數(shù)據(jù)庫開始出現(xiàn)一些數(shù)據(jù)庫連接的問題。
https://sysadminxpert.com/aws-rds-max-connections-limit/
用于可擴展軟件的 NoSQL 數(shù)據(jù)庫
NoSQL 數(shù)據(jù)庫非常靈活的地方在于我們可以像 Redis 一樣使用 NoSQL 的內(nèi)存鍵值存儲。
使用像 Redis 這樣的內(nèi)存數(shù)據(jù)庫來提供所有產(chǎn)品詳情將大大降低響應(yīng)時間。
另一個用途可以是使用 Solr 或 Elastic Search 來實現(xiàn)類似查詢“中號阿迪達斯牌紅色 T 恤”這樣的快速多條件搜索,而不是運行復(fù)雜的 SQL 查詢。Solr 是一個支持事務(wù)的 NoSQL 數(shù)據(jù)庫,它有助于提高軟件的可擴展性和彈性。
Redis 和 Solr/Elastic Search 都需要提前準(zhǔn)備一些數(shù)據(jù)才能正常工作,但這肯定比每次客戶端請求都要查詢關(guān)系數(shù)據(jù)庫要好得多。
對于每個寫請求,都需要寫入關(guān)系數(shù)據(jù)庫。
例如客戶的每次購買都必須存儲在關(guān)系數(shù)據(jù)庫中,而在所有瀏覽場景至少有 80-90% 流量我們可以使用 NoSQL 數(shù)據(jù)庫,這可以讓軟件更具可擴展性。
最終一致性和 CAP 理論
NoSQL 數(shù)據(jù)庫之所以速度快,是因為它們采取了最終一致性的折衷方案。為更好理解數(shù)據(jù)存儲,我非常建議您更新一下 CAP 理論的相關(guān)知識 —— 一致性、可用性和分區(qū)容錯性。
https://twitter.com/mykola/status/1101337299525267457
在這篇關(guān)于高可擴展性的文章中,您可以了解到更多關(guān)于從 100 萬到 1100 萬用戶如何擴展的信息。因為 NoSQL 數(shù)據(jù)庫還可以作為高效緩存工作,這就引出我的的下一個關(guān)于使用高效緩存來實現(xiàn)軟件可擴展性的要點。
http://highscalability.com/blog/2016/1/11/a-beginners-guide-to-scaling-to-11-million-users-on-amazons.html
緩存實現(xiàn)軟件的可擴展性
正如 Phil Carlton 所說
在計算機科學(xué)領(lǐng)域只有兩個難題: 緩存失效和命名。
緩存失效也是面臨的一個有趣問題。您將需要一個大規(guī)模的緩存,因為沒有緩存的普通方式不再可擴展。對于優(yōu)秀的軟件可擴展性來說,不同層級的緩存起著至關(guān)重要的作用。以下是您可以利用緩存來實現(xiàn)更可擴展的軟件的一些方法。
Memoization
第一級的緩存可以在代碼層面進行,其中一個基本方法是 Memoization。
Memoization 是其他緩存函數(shù)的高階函數(shù)。它可以優(yōu)化一些慢函數(shù)。它將函數(shù)第一次調(diào)用后的結(jié)果進行緩存,在后續(xù)調(diào)用時只要參數(shù)相同就可以直接在緩存中找到結(jié)果。
您可以看一個 Node.js Memoization 的例子,服務(wù)器可以緩存響應(yīng) 1 分鐘。因此在 1 分鐘以內(nèi),即使數(shù)據(jù)發(fā)生了變化,客戶端也會得到相同的舊數(shù)據(jù)。
https://geshan.com.np/blog/2020/11/nodejs-for-php-developers/#memoization-example
HTTP 緩存
另一個級別的緩存可以在 HTTP 層上完成。通過良好的使用 HTTP headers 可以按需要將響應(yīng)緩存一段時間。HTTP 緩存也可以使用像 Cloudflare 這樣的應(yīng)用程序來實現(xiàn),通過規(guī)則設(shè)置使響應(yīng)緩存數(shù)分鐘甚至數(shù)小時以減少服務(wù)器負載。這種類型的緩存機制有助于我們獲得高水平的軟件可擴展性。
如果您有足夠容量可以管理全量 HTTP 緩存和 HTTP 加速器,Varnish 是一個很好的選擇。Varnish 聲稱:
根據(jù)您的架構(gòu),它通常以 300-1000 倍的速度提高交付。
目前 Varnish Docker 映像的下載多達 100 多萬次,我認為人們?yōu)榱双@得其無與倫比的軟件可擴展性和巨大的 HTTP 緩存,很可能已經(jīng)在 Kubernetes 上大量使用了。
https://hub.docker.com/_/varnish
我不確定讀副本是不是一種純粹的數(shù)據(jù)庫緩存機制。但是我非常確信,從讀副本上進行查詢能大幅降低對主數(shù)據(jù)庫的壓力并有助于提高軟件可擴展性。當(dāng)然還有很多在多層應(yīng)用程序上實現(xiàn)緩存的其他方法。在了解軟件可擴展性后,您很可能根據(jù)自己的情況希望添加緩存來提高系統(tǒng)速度。
4. 總結(jié)
軟件擴展性是一個困難的問題,而運行環(huán)境使其變得更加復(fù)雜。
一家中型公司眼中的大規(guī)模概念在 FAANG 公司的眼中可能根本排不上號。究竟什么水平才算得上是大規(guī)模,取決于您的軟件系統(tǒng)日常處理的 RPM/RPS。
我沒有實際處理過每秒十萬或上百萬次請求的系統(tǒng),我只聽說過那樣的規(guī)模。我實際遇見并處理過的系統(tǒng)有每秒有 100 到 1000 次請求,即使是這樣規(guī)模滿足軟件可擴展性也是非常有趣和富有挑戰(zhàn)性的問題。
責(zé)編AJX
-
軟件
+關(guān)注
關(guān)注
69文章
4921瀏覽量
87396 -
源代碼
+關(guān)注
關(guān)注
96文章
2945瀏覽量
66730 -
運維
+關(guān)注
關(guān)注
1文章
256瀏覽量
7564
發(fā)布評論請先 登錄
相關(guān)推薦
評論