【雜談】對代碼的一些建議:從單模塊到多模塊

對於產品,一般來講,從單模塊,到負載均衡的多模塊,最後到有服務治理的規模化集羣(例如微服務),逐步發展和演進。本文並不打算涉及框架或者架構,也不講什麼大道理,僅從代碼編寫的角度,看看開發人員需要注意什麼。

單模塊開發的一些注意事項

單模塊並不是指單體方式,根據功能進行模塊劃分,每個模塊在生產環境中是單模塊運行(主備方式)。單模塊階段開發人員仍是有要求,在我的實踐中,從code review看,經驗不足的開發人員會存在以下的疏忽:

一、開發版本、測試版本和生產版本不是同一個版本。What?怎麼回事,就是開發人員將配置數據、業務數據(包括log所在路徑)都統統打包在一起。

我們以 tomcat 的 war 包爲例,一般來講,開發人員不會將諸如數據庫的ip地址、密碼、服務提供者的url寫入到程序代碼中,但是會寫入諸如web.xml的配置中,一起打包。這就需要專門爲測試環境、生產環境修改配置,重新打包,就有了測試版本,生產版本。但實際上,生產環境的ip地址,賬號,密碼,開發人員並不需要(也沒必要或者不應該)知道,而且一旦生產環境發生變化,都需要返工打包,運維會崩潰的。

有些開發人員會認爲,安裝了 war 包之後,tomcat 將之解壓爲文件夾,可以手動修改裏面的配置文件。這想法需要扼殺在搖籃當中:

  1. 解開的文件夾其實算是個臨時文件夾,只要 war 包修改,就會重新解壓,也就是,我們需要爲每一個版本手動進行配置的重設置,而很多情況下,版本的升級,並不需要修改配置。
  2. 很容易出現疏漏,例如進行版本回退的時候,運維人員可能會忘記重新進行配置修改。
  3. 只要涉及到手動修改,就無法進行自動化操作,包括自動測試,自動發佈,自動部署。當業務發展,我們終要走向devops,手動是無法容忍的
  4. 如果採用容器封裝,是無法修改的。這限制了系統的容器化和雲化。

我們看看linux是如何組織軟件包的,程序在/var/lib或者/usr/lib,配置在/etc,log在/var/log下面。這是將業務邏輯(程序)、配置、業務完全獨立開來。簡單講就是:代碼(邏輯)歸代碼(邏輯),數據歸數據。如果我們混在一起:開發人員開發環境或測試環境的數據,log會污染到生產環境,而生產環境中的業務數據,log很容易因版本升級中出現數據丟失,哪怕是丟失了log,在生產環境中都是運維事故。要養成將邏輯、配置、數據分離的開發好習慣。這種分離,在集羣編排部署中尤爲重要。

二、缺乏對同一資源併發處理的關注。不要以爲單模塊就沒有併發處理,現在哪個程序不是多線程的。對於剛入行的程序員,比較容易出現下面的問題:

  1. 使用靜態全局數據傳遞數據,在併發處理時,這個數據可能會被改寫。特別出現在那些開發人員的開發思路仍處於過程開發,還未進化爲對象開發時,代碼組織相當混亂。
  2. 對同一資源,例如數據庫,沒有考慮如何寫保護,採用哪種鎖。初入行程序員很多沒有意識到這個問題,他們調測是單步調測,跟蹤完一個業務請求,OK了就認爲都OK了。測試的時候,要加上併發操作同一資源的測試,看看結果是否符合預期。

三、優雅關閉的問題。優雅關閉是在程序結束時,釋放程序佔用的所有資源,保證正在的處理業務,能夠處理完,而不能是處理了一半這種不確定的狀態,可以選擇拒絕業務,更多的情況是完成業務。

我看過升級時,簡單粗暴的直接將tomcat給 kill 掉的,這種方式,如果數據庫的事務沒有收到commit,這個事務會掛起;又例如 tcp的keepalive在阿土中缺省是2小時,而數據庫的連接數是有上限的。還有如果業務執行到一半,可能導致該用戶後續業務狀態異常而掛起,由於log也被突然中斷,出現了問題很難排除。

四、缺乏對異常流程的關注。用戶的輸入,內部服務提供方或者第三方api的調用,都可能存在意外,開發人員對本模塊外的是不信任的,開發世界是性本惡。最簡單的,萬一web api不是響應200,而是4xx,5xx,你怎麼處理。我們在原型中可以不考慮,但是在生產環境中,程序必須以警惕的眼光看待外面的世界。

五、業務邏輯抽象很重要。這是區別你會寫幾行代碼,還是會開發。良好的業務抽象,當業務需求不斷增加時,我們可以遊刃有餘,說不定就只是原來基礎抽象的組合,代碼具有良好的業務擴展能力。如果不注意抽象,說白了就是我們程序內部的邏輯設計,來一個新的需求,吭呲吭呲地寫一堆代碼,將其堆疊在原來的代碼之上,幾個需求後,就是一堆草,代碼沒法看了。

六、接口名字要清晰表達其準確功能。例如選擇接口的名字叫deleteUser,resetUserPolicy,或者resetUserPolicyAndClearUserStock。要給接口函數加上註釋。我們不是交一次作業,代碼要維護要發展,生命期長着呢,有時開發人員自己都不太記得了。應有寫文檔寫註釋的習慣,明確調用的影響。

在某個案例中,開發人員只是想重置用戶的某個屬性,結果連帶將用戶的持有物品清單也一併清空。只要是人都會犯錯,但是有很多可以通過規範化的開發流程來避免,在這個案例中:

  1. 接口調用時,開發人員不清楚這個接口引發的影響,雖然這個接口是他自己以前寫的。這可以通過明晰的接口命名以及註釋來避免。
  2. 調測中,只關心本次新增功能是否實現,沒有認真核查對外的影響,包括該用戶在數據庫完整數據,是否觸發了第三方接口的調用,可以通過認真核查log發現。開發人員不能只看log的最後結果,要全部覈查是否都符合預期。

橫向擴展的多模塊

從單模塊到多模塊,最基礎的,就是 AKF 的 X 軸擴展。不要認爲 x 軸擴展是自然天成,多部署幾個 copy 就可以了。X軸擴展也是有條件的。我們舉個例子,某個web網站,用戶登錄進去後可以進行數據的增刪改查,具體業務是什麼不重要,以 J2EE spring 爲例,是否可以簡單部署 n 個模塊,在前面加上負載均衡器(LB)就OK了?

沒那麼簡單,當用戶登錄成功,會分配一個 session 給該用戶(瀏覽器),在後續的訪問中,瀏覽器的 http 請求中會通過 cookie 攜帶 session id,服務器由此判斷用戶的合法身份。如果使用最常見的輪詢負載均衡,模塊 A 收到用戶的登錄請求,分配了 session,並保存在內存中,用戶下一個請求,被路由到模塊 B,模塊 B 的內存中並沒有這個session的ID,由此判斷用戶無效,重定向到登錄頁面中。如此,來來回回,這個可憐的用戶就在不斷地登錄成功和重新要求登錄中掙扎。

因此,如果不做任何的處理,直接將模塊 copy n 份,可能會導致整個業務出現問題。例子的問題就是模塊 B 無法獲得模塊 A 內存中的session信息,要處理,可以沿兩個思路:

  • 消滅問題
  • 解決問題

先看看消滅問題,就是不讓問題出現。例子只要確保相同用戶,也即相同 session id 的 http 請求能夠路由到同一模塊即可。這種方式,對代碼沒有要求,但是對負載均衡器有要求,要求將相同的 session id 的 http 請求路由到同一個上游模塊。方式有很多,例如根據 session id 的哈希值取模,也可以作爲 proxy,在 session-id 的結尾加上一個上游服務器的標記,等下次 http 到來時,可以根據 session id 得到它以往是哪個上游模塊處理的。這種方式雖然對代碼沒有要求,但是對運維和部署有要求,開發人員應明確指出,以此要求負載均衡器。例如,如果使用 nginx,是否該用openresty,通過 lua 來自定義路由。

如果採用解決問題的方式,就是讓 B 模塊也能獲取 A 模塊分配的 session id,也就是 A 模塊不能將 session 數據只放在自己的內存中,由於模塊可能部署在不同的機器中,因此也不能通過本機共享內存的方式。如果你想到存放在數據庫,那麼思路方向對了,我們確實要找一個各模塊都能訪問的地方來存放 session 數據,但是這個不能採用持久化的存儲,如數據庫。很簡單,這就不是一個持久化的數據,這是臨時的,會變化的數據,不需要也不應該存放在數據庫。如果放在數據庫,會加重數據庫的讀寫壓力。這時高速緩存,例如 redis,memcached 的就是比較合適的選擇。由於引入了高速緩存,系統的架構就出現了變化,對代碼就有要求,本地內存讀寫變爲高速緩存的讀寫。

由此可見,橫向的 x 軸的擴展絕不是簡單將模塊copy幾份,前面加個負載均衡就可以。有了多模塊,跟着會有很多新的問題,例如服務發現、全鏈條跟蹤等,這些就是微服務的事情了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章