天天看點

從軟體的曆史看架構的未來

作者:CTO修煉之路

1972 年,Edsger Dijkstra 在為圖靈獎頒授典禮所寫的感言文章 中說到:“在沒有計算機的時候,也就沒有程式設計問題;當我們有了簡單的計算機,程式設計隻是個小問題;而現在我們有了算力規模龐大的計算機,那程式設計就成為了一個同樣巨大的問題了”。半個世紀前,Dijkstra 已經敏銳洞見了機器算力的提升是程式設計方法發展的直接牽引,每當人類掌握了更強的算力,便按捺不住想去解決一些以前甚至都不敢去設想的新問題,由此引發軟體設計模式的重大變革。

曆史上的軟體危機和契機

計算機剛誕生的年代,硬體規模還很小,甚至程式員僅憑大腦就足夠記住資料在幾 KB 記憶體中的布局情況,了解每條指令在電路中的運作邏輯。此時的計算機盡管運算速度比人類快,但内部卻并沒有什麼人所不知道的事情;此時的軟體開發并沒有獨立的“架構”可言,軟體架構與硬體架構是直接實體對齊的。

随着計算機的快速發展,直接面向硬體進行的軟體開發很快觸碰到了瓶頸,人腦的生物局限顯然無法跟上機器算力前進的步伐,當機器強大到世界上最聰明的人都無法為它編寫出合适的程式了,那計算機科學還能繼續發展嗎?這便是曆史上第一次軟體危機的根源。第一次軟體危機,同時也是結構化程式設計 發展的契機,結構化程式設計扭轉了當時直接面向全局資料、滿屏 Goto 語句書寫面條式代碼 (Spaghetti Code)的程式設計風氣,強調可獨立編寫、可重複利用的子過程/局部塊的重要性,讓軟體的每個局部都能夠設計專門的算法和資料結構,允許每一位程式員隻關注自己所負責的部分,進而在整體上控制住了軟體開發的複雜度。此時,軟體的架構才開始獨立于硬體實體架構而存在,軟體業開始出現把控全局設計的架構師與聚焦局部實作的程式員的角色分工。

将軟體從整體劃分成若幹個局部,人類能夠以群體配合來共同開發軟體,使得人與計算機又和諧共處了十餘年。不過,機器的算力膨脹仍然在持續,人類群體的溝通協作能力卻終有極限。人畢竟不是可複制的程式,每個人都有自己的了解與認知,如何讓各個子產品能準确地協同工作成了一場災難,這就是第二次軟體危機的根源。《人月神話 》中有一個幾乎每位程式員都聽過的案例:IBM 公司為開發 OS/360 系統投入的成本達到了美國的“曼哈頓”原子彈計劃的 25%,共有 4000 多個子產品,約 100 萬條指令,超過 5000 人年,耗資數億美元,即使如此,結果還是延期傳遞,在傳遞使用後的系統中仍發現大量的缺陷。

渡過第二次軟體危機的過程中,面向對象程式設計逐漸取代了面向過程的結構化程式設計,成為主流的程式設計思想,這次思想轉變宣告“追求最符合人類思維的視角來抽象問題”取代了“追求最符合機器運作特征的算法與資料結構”成為軟體架構的最高優先級,并一直持續沿用至今。這次危機還直接導緻軟體工程的誕生, 如何以系統性的、規範化的、可定量的方法去高品質地開發和維護軟體成為一門獨立的科學。

雲與分布式漸成為主流

如果說曆史上的第一、二次軟體危機分别是機器算力規模超過了人類個體的生理極限,超過了人類群體的溝通極限的話。那麼在今天,在雲計算的時代,資料中心所能提供的算力其實已經逼近到了人類協作的工程極限,與此算力相符的程式規模,幾乎也到了無論采用何種工程措施去優化過程、無論采用什麼管理手段去提升品質,都仍然不可避免會出現意外與異常的程度。大家都相信隻要軟體系統由大量人員共同研發,并使其分布在雲中大量節點協同運作,随着項目規模的增大、時間變長,就肯定會有人疏忽犯錯,會有代碼攜帶缺陷,會有電腦當機崩潰,會有網絡堵塞中斷,總之,必然會受到墨菲定律的無情打擊。

軟體架構要與硬體算力規模對齊,目前用來适配雲計算與分布式的主流架構形式是微服務,微服務興起之時,曾湧現出各類文章去總結、贊美微服務帶來的種種好處,諸如簡化部署、邏輯拆分更清晰、便于技術異構、易于伸縮拓展應對更高的性能,等等,這些當然都是重要優點。可是,如果不拘泥于特定場景特定問題,以宏觀的角度來看,前面所列這種種好處都隻算是“錦上添花”、是屬于讓系統“更好”的動因,肯定比不上系統如何確定“生存”來得關鍵。在筆者看來,從單體到微服務的最根本的推動力,是為了友善某個服務能夠順利地“消亡”與“重生”,局部個體的生死更疊,是關系到整個系統能否可靠續存的關鍵因素。

舉個例子,在很長的時間裡面,企業應用中采用單體架構的 Java 系統,其更新、更新都必須要有固定的停機計劃,必須在特定的時間視窗内才能按時開始,必須按時結束。如果出現了非計劃的當機,那便是生産事故。但是軟體的缺陷不可能遵循停機計劃來“安排時間出錯”,為了應對缺陷與變化,做到不停機的檢修,Java 曾經搞出了 OSGi 和 JVMTI Instrumentation 等複雜的 HotSwap 方案,以實作給奔跑中的汽車更換輪胎這種匪夷所思卻又無可奈何的需求;而在微服務架構的視角下,所謂系統檢修,充其量隻是一次服務滾動更新而已,灰階部署新的程式版本,然後導流、測試、釋出即可。

流水不腐,有老朽,有消亡,有重生,有更疊才是生态運作的合理規律,如何采用不可靠的部件來構造出一個可靠的系統,是軟體架構适配雲與分布式算力發展的關鍵所在。如果系統中局部能擁有獨立的生命周期,在整體架構上有實體隔離的設計,那即便采用了不可靠部件,在系統外觀察,整體上仍然有可能表現出穩定健壯的服務能力。

從雲計算到雲不可知

我們繼續順着”軟體架構的演進由人與機器的沖突所驅動,逐漸與算力規模對齊“這條線索,思考軟體開發的下一個核心沖突将會是什麼?窺探下一個時代的軟體架構會具備何種特征?

筆者認為,軟體發展的下一個關鍵沖突将會是算力規模超過人應掌握合理知識的極限。經過良好設計的分布式系統,擁有局部的可再生性,确實能在整體上展現出可靠的服務能力。然而,“良好地設計”一個分布式系統很不容易,今天無限火熱的雲原生、微服務、不可變基礎設施、彈性計算、服務網格、無伺服器架構、高低零代碼,等等,背後都能展開成一整套成體系的開發或者設計方法。這些新的技術在為人們解決了更複雜軟體問題的同時,也正在把程式設計這件事情本身的複雜度推向更高層次。一名剛剛走出校園的大學生,要掌握計算機與程式執行的基本原理,要消化完所用程式設計語言的核心細節,要掌握領域中常用的類庫、架構和工具,要了解分布式系統的服務彈性、容錯、限流等設計技巧,要接觸容器、雲原生、函數計算等運作架構層面的知識,耗費上十年時間都絲毫不奇怪。

在哲學裡,有人曾經嚴肅研讨過“知識膨脹 ”的問題,說的是人類科學的前沿在不斷拓展,觸及到前沿所需的基礎知識也不斷增加,是否會陷入後來者終其一生都無法攢下足夠基礎,導緻人類知識陷入止步不前的危機之中。在計算機科學裡就更加現實了,知識膨脹直接表現為從畢業到“35 歲退休 ”(梗)之前,很多程式員恐怕都很難具備設計分布式架構所需的全面知識。

雲與分布式時代,軟體知識看來又到了該“打個結”的時刻,要設法把那些重要但普适的知識标準化并下沉。好比今天除非那些專門的領域,大多數程式員已經不再需要關注寄存器、信号、中斷等與機器底層的細節,也不會太關注作業系統記憶體頁/段、執行排程器、輸入輸出原理等作業系統底層的細節,等雲資料中心徹底成熟,成為主流的程式部署運作環境後,雲與分布式的複雜細節也同樣會被隐藏起來。

未來軟體如何使用雲服務,現階段還很難有定論,但有迹象表明,軟體中的非功能屬性 會率先被外置出去,而不會繼續像現在這樣,在開發階段镌刻定型于程式代碼之中。軟體是以單體還是以分布式運作,需要提供怎樣的 SLA,具體與哪些技術元件進行協作,通訊中是否要容錯限流,等等,都不必在開發期就鎖定起來,也不必由業務開發人員去關注,他們隻處理那些承載系統業務價值的功能屬性。這種外挂式的軟體架構風格,如同你要上戰場便穿上軍裝,要遊泳便穿上泳衣,去舞會便穿上禮服,不同的裝備讓人能适應不同的場景。而那些“可穿戴”的裝備,都是由專業廠商設計,有品質保證,不需要每位編寫代碼的程式員都知道它們應該如何工作。

正在逐漸成熟的 Service Mesh 就展露出一些這方面的特征,Sidecar 以流量劫持的方式,能夠為程式間的網絡通訊額外附加上連接配接穩定性(如重試、熔斷)、安全性(如鑒權、雙向通訊加密)、可管理性和可觀測性,既不依賴人專門去編碼,也不依賴某款語言或者架構的預置能力。不過,Service Mesh 僅僅能滿足與服務通訊能力相關的治理,而軟體設計所需的能力并不止通訊這一項,開發者要依賴多種提供不同能力的運作時來搭建軟體,譬如進階語言虛拟機提供執行能力、消息隊列提供 Pub/Sub 通知能力、容器編排系統提供生命周期管理能力,等等。開發者使用這些能力時,也面臨與通訊一樣的治理需求。

ShardingSphere 的作者張亮曾經在 InfoQ 撰文 ,提出過 Database Mesh 的設想,把資料庫發現、通路路由、資料分片、讀寫分離、負載均衡等特性從程式代碼中拿出去,也交給 Sidecar 來實作。既然 Service 和 Database 可以 Mesh 化,那 Cache Mesh、MessageQueue Mesh、Storage Mesh……等自然都有了登場的理由。更進一步,分布式中那些複雜卻有共性的處理技巧,如并行、并發、狀态、共識,等等,是不是也可以從程式代碼中獨立出去,由 Sidecar 引導至合适的、不特定的部件中妥善處理?最後,一旦雲計算服務提供商的技術貨架中大多數部件和能力被 Mesh 抹掉了差異化特性,剩下都是一緻的标準操作,那 Serverless 一直倡導的"後端即服務"(BaaS)便立刻有了無比廣泛的基礎。此時,雲資料中心就仿佛是一部擁有無限算力的機器與一套有标準接口的作業系統,開發者無需關心程式在哪裡執行(FaaS),也不再關心程式有哪些依賴(BaaS)。

上面僅談概念恐怕有些抽象,筆者以“如何存儲一個 K/V 值對”為例,來看一下目前的程式設計與未來設想的程式設計方式會有什麼差别。下面這段代碼是現在随處可見的大路貨,它具有稍後列舉的幾點問題:

Set<HostAndPort> nodes = new LinkedHashSet<HostAndPort>();
nodes.add(new HostAndPort("192.168.1.1", 6379));
nodes.add(new HostAndPort("192.168.1.2", 6379));
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(1);
config.setMaxIdle(1);
try (Jedis jedis = new JedisCluster(nodes, config)) {
	String result = jedis.set("icyfenix", "{\"name\":\"zzm\", \"email\":\"[email protected]\"}");
	return ok(result);
} catch (Exception e) {
	log.error("Redis error:{}", ExceptionTools.getExceptionStackTrace(e));
	return false;
}
           
  • 首先,這是一段操作 Redis 的代碼,意味着你需要了解 Redis 的知識,不說實作原理,起碼要知道它的 API 該如何使用,程式代碼也必須引入 Redis 的用戶端 SDK 作為依賴項。
  • 其次,這是一段可運作的 Java 代碼,意味着你需要知道 Redis 的服務位置(如 Host 位址、端口等)、部署方式(如單點、叢集、分片情況等)、連結資訊(如鑒權方式、密碼等),這些其實應該是 SRE 而不是 SDE 的職責。
  • 最後,這是一段在生産環境容易受到挑戰的代碼,生産可能還需要考慮額外的非功能屬性:要不要啟用連接配接池?并發政策是 first-write-wins 還是 last-write-wins ?是否需要支援事務?資料能保證什麼級别的一緻性?要批量操作該怎麼辦?假若這些非功能屬性都反應到代碼上,結果肯定要比現在看到的複雜上不少,其中有一些需求甚至僅憑應用代碼是無法解決的。譬如要支援事務,用 Redis 可以,用 Memcached/Cassandra 就不行;要支援強一緻性,用 Etcd/ZooKeeper 可以,用 Redis 就不行。

以上問題,在今天看來其實都算不上真正的問題,去寫程式就該懂得寫程式的知識,但是作為一名業務開發人員,意圖僅僅是想儲存或者讀取一個 K/V 值對而已,要用 Redis、Etcd、Memcached 或關系庫作為存儲、要用哪個雲服務商提供的存儲服務、要滿足哪些非功能特性,本不該屬于操作意圖的一部分,都應該被隐藏起來。譬如下面這樣來完成 K/V 值對的存儲和通路:

curl -X POST http://localhost:3500/v1.0/state/users \
  -H "Content-Type: application/json" \
  -d '[
        {
          "key": "icyfenix",
          "value": {"name":"zzm", "email":"[email protected]"}
        }
      ]'

curl http://localhost:3500/v1.0/state/users/icyfenix \
  -H "Content-Type: application/json"

{"name":"zzm", "email":"[email protected]"}
           

至于為什麼會存在“http://localhost:3500”這樣的服務位址,後面連接配接的具體是什麼存儲服務,這些是 Sidecar 而不是業務開發人員需要關心的事情。不同産品與不同雲計算服務商之間的差異,被隐藏在相同的操作原語(Primitives)和代表服務标準含義的接口(如 HTTP URL)之下。這樣雲計算就自然而然地打破了目前各廠商之間和産品之間的隔閡,順利步入到雲不可知 (Cloud Agnostic)的階段。這便是對雲計算與分布式架構“打個結”的具體動作。

雖然迄今為止,上述設想距離現實還很遙遠,理論不夠成熟,能在生産環境中使用的多運作時架構仍處于十分早期階段,譬如上面用于示範的代碼是基于微軟的 DAPR 架構,它在上周才剛剛進入 CNCF 孵化。對這個示範 DAPR 目前也僅僅能處理 K/V 存儲,其它存儲類型(如更為常用的關系庫)目前都還完全沒有考慮,但筆者願意相信這是未來架構演進的一個主要方向,必須把複雜的問題盡量關進籠子,由專業人員去看護,才能讓普通程式員更好參與軟體開發,甚至通過低/零代碼工具的支援,讓那些沒有太多程式設計知識,卻有豐富領域知識的業務專家,也能夠獨立制造出優秀的軟體産品。

軟體,架構與人

第一次軟體危機在 1950 年代末期初現端倪,結構化程式設計思想在 1970 年才被正式提出;第二次軟體危機(連同"軟體危機"這個概念)是在 1970 年北約 NATO 會議 上被定義的,要一直到 1990 年代面向對象的設計方法成為主流,以及 Scrum、XP 等軟體工程方法被提出後,這次危機才算是畫上句号。從 2010 年左右開始興起的雲計算是程式的運作環境繼“大型計算機”轉變到“用戶端-伺服器”之後的又一場巨變 ,與前兩次軟體危機帶來的變革契機一樣,現有的許多軟體架構和開發方法,一定也會在以十年計數機關的時間段内逐漸被颠覆,今天你我所談的雲原生、微服務等話題,僅僅是這次變革浪潮的開端。

與技術變革相伴的,是它對行業以及對程式員這個群體的影響。第一次軟體危機期間,世界上最聰明的科學家/工程學家在開發軟體;第二次軟體危機期間,社會中的高智商高學曆的精英群體在開發軟體;雲與分布式的時代,軟體開發者恐怕也不可避免會受到下一輪沖擊。未來的軟體架構對普通程式員應該會是更友善更簡單的,但是對普通程式員友善與簡單的背後,預示着未來的資訊技術行業很可能會出現“階級分層”的現象,由于更先進的軟體架構已經允許更平庸的開發者也同樣能寫出可運作、可用于生産的軟體産品,同時又對精英開發者提出更多、更複雜的技術要求,長此以往,在開發者群體中會出現比現在還要更顯著的馬太效應 ,迫使開發者逐漸分層,從如今所有開發者都普遍被認為是“高智商群體”的狀态,轉變為大部分工業化軟體生産勞工加上小部分軟體設計專家的金字塔結構,就如同現在的建築勞工與建築設計師的關系一般,今天我們經常自嘲的 CRUD Boy,随着軟體産業日趨成熟,恐怕還真的會成為現實。

本篇文章裡,筆者刻意在避免使用“第三次軟體危機”這樣有嘩衆取寵嫌疑的表述,危機總是與契機同時出現,未來的軟體一定是越來越貼近于普通平民百姓的軟體,但軟體的未來也一定有大量的挑戰與機會在等待着優秀的程式員與架構師去承擔。

軟體架構與硬體算力規模對齊

As long as there were no machines, programming was no problem at all; when we had a few weak computers, programming became a mild problem, and now we have gigantic computers, programming has become an equally gigantic problem.

在沒有計算機的時候,也就沒有程式設計問題;當我們有了簡單的計算機,程式設計隻是個小問題;而現在我們有了算力規模龐大的計算機,那程式設計就成為了一個同樣巨大的問題了。