軟件系統的穩定性
軟件系統的穩定性,主要決定于整體的系統架構設計,然而也不可忽略編程的細節,正所謂“千里之堤,潰于蟻穴”,一旦考慮不周,看似無關緊要的代碼片段可能會帶來整體軟件系統的崩潰。這正是我閱讀Release It!的直接感受。究其原因,一方面是程序員對代碼質量的追求不夠,在項目進度的壓力下,只考慮了功能實現,而不用過多的追求質量屬性;第二則是對編程語言的正確編碼方式不夠了解,不知如何有效而正確的編碼;第三則是知識量的不足,在編程時沒有意識到實現會對哪些因素造成影響。
例如在Release It!一書中,給出了如下的Java代碼片段:
package com.example.cf.flightsearch; //... public class FlightSearch implements SessionBean { private MonitoredDataSource connectionPool; public List lookupByCity(. . .) throws SQLException, RemoteException { Connection conn = null; Statement stmt = null; try { conn = connectionPool.getConnection(); stmt = conn.createStatement(); // Do the lookup logic // return a list of results } finally { if (stmt != null) { stmt.close(); } if (conn != null) { conn.close(); } } } }
正是這一小段代碼,是造成Airline系統崩潰的罪魁禍首。程序員充分地考慮了資源的釋放,但在這段代碼中他卻沒有對多個資源的釋放給予足夠的重視,而是以釋放單資源的做法去處理多資源。在finally語句塊中,如果釋放Statement資源的操作失敗了,就可能拋出異常,因為在 finally中并沒有捕獲這種異常,就會導致后面的conn.close()語句沒有執行,從而導致Connection資源未能及時釋放。最終導致連接池中存放了大量未能及時釋放的Connection資源,卻不能得到使用,直到連接池滿。當后續請求lookupByCity()時,就會在調用 connectionPool.getConnection()方法時被阻塞。這些被阻塞的請求會越來越多,最后導致資源耗盡,整個系統崩潰。
Release It!的作者對Java中同步方法的使用也提出了警告。同步方法雖然可以較好地解決并發問題,在一定程度上可以避免出現資源搶占、竟態條件和死鎖的情況。但它的一個副作用同步鎖可能導致線程阻塞。這就要求同步方法的執行時間不能太長。此外,Java的接口方法是不能標記synchronized關鍵字。當我們在調用封裝好的第三方API時,基于“面向接口設計”的原理,可能調用者只知道公開的接口方法,卻不知道實現類事實上將其實現為同步方法,這種未知性就可能存在隱患。
假設有這樣的一個接口:
public interface GlobalObjectCache { public Object get(String id); }
如果接口方法get()的實現如下:
public synchronized Object get(String id){ Object obj = items.get(id); if(obj == null) { obj = create(id); items.put(id, obj); } return obj; } protected Object create(String id) { //... }
這段代碼很簡單,當調用者試圖根據id獲得目標對象時,首先會在Cache中尋找,如果有就直接返回;否則通過create()方法獲得目標對象,然后再將它存儲到Cache中。create()方法是該類定義的一個非final方法,它執行了DB的查詢功能。現在,假設使用該類的用戶對它進行了擴展,例如定義RemoteAvailabilityCache類派生該類,并重寫create()方法,將原來的本地調用改為遠程調用。問題出現了。由于采用create()方法是遠程調用,當服務端比較繁忙時,發出的遠程調用請求可能會被阻塞。由于get()方法是同步方法,在方法體內,每次只能有一個線程訪問它,直到方法執行完畢釋放鎖。現在create()方法被阻塞,就會導致其他試圖調用RemoteAvailabilityCache對象的 get()方法的線程隨之而被阻塞。進而可能導致系統崩潰。
當然,我們可以認為這種擴展本身是不合理的。但從設計的角度來看,它并沒有違背Liskove替換原則。從接口的角度看,它的行為也沒有發生任何改變,僅僅是實現發生了變化。如果不是同步方法,則一個調用線程的阻塞并不會影響到其他調用線程,問題就可以避免了。當然,這里的同步方法本身是合理的,因為只有采取同步的方式才能保證對Cache的讀取是支持并發的。書中給出這個例子,無非是要說明同步方法潛在的危險,提示我們在編寫代碼時,需要考慮周全。
本文原文出自:http://zhangyi.farbox.com/post/stable-software-system