軟件系統的穩定性

jopen 9年前發布 | 33K 次閱讀 軟件系統

軟件系統的穩定性,主要決定于整體的系統架構設計,然而也不可忽略編程的細節,正所謂“千里之堤,潰于蟻穴”,一旦考慮不周,看似無關緊要的代碼片段可能會帶來整體軟件系統的崩潰。這正是我閱讀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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!