老舊項目二次開髮指南 背景: 最近新入職公司,負責技術。由於各種原因現在項目全權交由我們團隊負責,之前的研發團隊不再參與(及以後可能完全聯繫不上)。作為技術負責人,又剛入職公司壓力巨大。經過兩個多月的改造,算是接手得還算行。該項目存在的問題比較典型,特此記錄。歡迎各位大佬批評指教。技術能力有限,文中 ...
老舊項目二次開髮指南
背景:
最近新入職公司,負責技術。由於各種原因現在項目全權交由我們團隊負責,之前的研發團隊不再參與(及以後可能完全聯繫不上)。作為技術負責人,又剛入職公司壓力巨大。經過兩個多月的改造,算是接手得還算行。該項目存在的問題比較典型,特此記錄。歡迎各位大佬批評指教。技術能力有限,文中所說解決方案(思路)只代表筆者的愚見。
1 老項目存在的問題
-
項目採用的主要技術:
後端: spring-boot+dubbo+redis+mybatis+oracle+jeager+apollo+flyway
前段: node+react
1.1 架構層面
1.1.1 微服務並不微
多個服務的代碼量遠超許多單體服務;
1.1.2 依賴代碼不受控制
技術框架二次封裝:由於spring框架提供的便利性,上個團隊應該是出於對技術規範的考慮,對整個項目所使用的技術框架進行了二次封裝,技術框架做了封裝;
依賴第三方團隊代碼:項目中存在對三方非開源代碼的依賴;
1.1.3 開源項目版本老舊
項目框架老舊,項目框架被二次封裝,導致依賴的項目主體技術版本老舊,不能及時更新。
1.1.4 沒有事務(包含分散式事務)
單體服務事務:使用不規範,部分CUD操作甚至沒有開啟事務;
分散式事務:雖然項目框架中有引入seata事務解決分散式事務,但可能是代碼先行框架後用,代碼中存在大量複雜SQL,導致seata不能使用。
1.1.5 缺少主要服務
沒有字典服務,字典服務、用戶信息服務、許可權服務為同一個服務(暫且叫做“主數據服務”)沒有分離;
需要說明下
字典服務:指維護字典數據的服務,及簡單的key-value數據,例如:性別(1:男,2:女),民族(1:漢,2:滿,3,蒙古...)。
用戶信息服務:指維護用戶信息的服務;
許可權服務:指用戶許可權信息的維護,設計各個子服務操作許可權,菜單許可權的單獨維護。
服務是否需要如此拆分,各個業務場景不同,需慎重考慮。只是筆者評估目前接手的項目適合如此拆分。
1.1.6 網關服務定位混亂
後端沒有統一的網關服務,每個服務單獨校驗用戶登錄信息,前段node服務只做了介面轉發並沒有承擔應有的大前端工作。
1.2 資料庫層面
1.2.1 未分庫分表
未分庫:整個工程四百多張表全部創建在同一個用戶下;
未分表:用戶基本信息表,欄位上百,存在大量非常用欄位;
1.2.2 依賴資料庫
使用觸發器
使用自定義函數
使用存儲過程
1.3 緩存層面(主要說明redis緩存)
1.3.1 使用不同的序列化方式
項目中沒有使用統一的序列化方式
1.3.2 沒有充分使用數據類型(只使用String類型)
例如用戶基本信息,直接序列化成jSON字元串存儲,而不是使用hash,導致當需要使用用戶信息中的某個欄位時得全部取出用戶信息,無端增加IO。
1.4. 性能層面
1.4.1 存在迴圈使用調用資料庫的情況而沒有使用批量操作(List,batch)
數據CUD等操作時,沒有使用批量操作執行器,而是for迴圈獲取SQL拼接(並不是說SQL拼接等操作性能就一定查,這裡只說明部分數據量大的情況下更推薦使用批量執行器來操作);
同一個數據迴圈跨服務調用,舉例說明:
例如,“A應用服務”的某一個數據A_data保存著“用戶ID”,現在需要在頁面展示A應用的A_data數據,此時肯定需要把用戶名字也展示出來。對於該業務就存在返回數據給前端時,需要調用主數據獲取“用戶ID”對應的“用戶信息"。
就這樣的一個應用場景。如果是返回一個A_data列表數據給前端,則應該是先獲取所有的用戶ID列表,再通過用戶ID列表去獲取所有需要的用戶數據。可實際代碼中大量地方是使用的迴圈調用,這無疑是一個巨大的性能損耗。
1.4.2 返回大集合對象
就介面請求的數據返回不夠精細,往往存在一個返回數據再多個地方使用,而消費端卻僅僅只需要使用少量欄位。
1.4.3 存在重覆解釋欄位情況
對字典數據的解釋沒有統一放到前段處理,而是由後端解釋翻譯;
後端介面沒有隔離,存在同樣的欄位A服務解釋了之後B服務接著重覆解釋,具體距離:
A服務依賴B服務,現在前端請求A服務獲取數據,A服務調用B服務,B服務返回數據(對欄位進行瞭解釋),A服務再返回給前端之前對欄位又進行瞭解釋。
1.5 代碼規範
1.5.1 沒有使用枚舉,全部使用常量;
1.5.2 命名不規範
1.5.3 沒有註釋
1.6 項目文檔
1.6.1 沒有任何文檔
2. 對應的解決方案
優先順序確定
可以看到整個項目存在問題較多,加上研發任務也再同時進行,基於自己多年研發的直覺,對上述問題進行簡單的優先順序排序,並逐一闡述理由。
經過前面對項目中存在問題的整體梳理,對項目現狀有了比較全面的瞭解。由於研發任務同時進行,無法投入太多人力對以上問題進行全面休整,因此基於自己多年研發累積經驗對以上問題進行了簡單的優先順序排序處理。
2.1 風險規避——必須儘快解決的問題
-
原因
由於項目是整體交接,之前的團隊退出,新來接手的團隊對之前的項目完全不瞭解,因此為了避免項目變得更亂,資料庫分離迫在眉睫。 因為所有子服務的表全部放到同一個用戶下麵,如果不從技術層面實現數據的分離,由於join操作的便利性,必然會產生更多的跨服務關聯查詢。
技術實現
1.通過python腳本(正則匹配)找出各個子服務中在使用的表,按照CRUD操作進行歸類; 2.對於已經存在的多個服務同時存在CUD同一個表的情況,仔細確定表的歸宿問題;對於無法確認歸宿問題的表先保存記錄,待後續業務熟悉之後再次確認(非常感謝之前的開發者,整個項目梳理下來這樣的表也只有十幾個); 3.將所有表都按照服務歸類之後,通過使用資料庫提供的公共同義詞,對跨服務使用的表分配對應的CRUDE許可權(除非像上文2中提到無法確認歸宿的表之後,禁止分配CUD許可權)。
-
原因
相信這一點不用多說,不受控制的東西千萬不要使用。所用一段無法控制的代碼,不知道啥時候會爆雷;當沒有利益做橋梁,很難期望別人會給予恩惠。要不被人鎖住咽喉,唯有將不穩定的因素解決。 當然對於這樣全面的技術棧替換,測試的支持必不可少。這也是我將優先順序排到此處的原因。由於項目剛接受,剛好產品、測試都需要對項目進行全面瞭解。這樣避免到以後真的被技術鎖喉時,還需要來進行一次全方位的測試(這工作不是一般的小)。
技術實現
就框架的二次封裝而言,相對比較容易解決;把項目中使用到的技術整理出來,逐一使用開源技術替換即可;還在是對開源技術的二次封裝,大家的使用技術比較統一,項目中並沒有太多依賴二次封裝框架的代碼存在。少量的存在也通過反編譯的方式生成了源碼。 其中最麻煩的是上個研發團隊自己封裝的工具包,整個項目涉及十幾個。由於一些緣由無法獲得該部分源碼,沒有辦法對於這樣,也只有使用最原始的方式——反編譯(這無疑是整個項目最頭疼的一部分)。
-
原因
一、項目安全是任何一種技術被採用必須被考慮的問題。一個三年前更新的技術版本,讓我無法放心使用;以後技術版本越老舊,以後升級成本就越高。這也是需要先行處理開源技術被二次封裝的原因。 二、過時的版本,也意味著使用不到新的技術特性。
技術實現
由於已經處理好了項目框架被二次封裝的問題,對於開源技術的升級相對來說比較愉悅,各個技術官網、論壇都能找到幫助文檔。
2.2 架構優化——保證業務進度的前提下,儘快處理
-
原因
主數據服務過重,性能將成為短期可預見的瓶頸。尤其是包含了字典服務的功能,而且該項目原有的架構還存在大量重覆解釋同一欄位的問題,必然加重服務負擔。
技術實現
筆者結合項目的實際情況,對服務的拆分目前沒有徹底進行,而是循序漸進(在迭代中重構)。目前只是將相對獨立的字典服務先抽取出來。以後需要使用字典數據的時候之後調用字典服務。要請所有研發人員在迭代開發的過程中逐步替換掉原有走主數據的方式,改為直接調用欄位服務。許可權服務和用戶信息服務,由於涉及到的業務相對複雜,考慮到項目穩定性的問題暫時未做處理(待後期業務熟悉)。
-
原因
沒有事務帶來的可怕性,不言而喻。
技術實現
由於目前對整個項目業務並不熟悉,因此筆者目前還未深入處理這塊;只是先行升級開源框架,使用spring-boot 提供的優雅停服務功能來減少由於直接關閉服務帶來的數據錯亂問題; 筆者目前對這一塊的想法本來是想通過橫向切麵統一給所有給GET介面都加上事務,但由於考慮到整個項目目前可以正常使用,且重新發版時間還不是特別緊因此未做處理,在此提出僅供大家參考。 另外對於分散式事務,由於項目中存在大量複雜SQL,原有seata框架沒有辦法繼續使用(這也是為啥之前引入了此框架沒有使用的原因)。筆者烤爐到跨服務的事務畢竟是少數再結合目前項目進度緊張的實際情況,暫時未做處理。目前的想法是等服務先行全面測試之後,再通過調用鏈路服務梳理出所有的跨服務寫數據業務,逐一排查處理。
-
原因
網關服務混亂,每個自服務都有對用戶登錄信息的校驗,這無疑是沒有意義的,反倒使代碼不夠整潔。 許可權認證如有任何的調整都將牽涉到所有的服務。 而且如果現階段不准確的定位各個服務職能,由於每個人的喜好(代碼習慣)不同,必然後導致這個服務越來越混亂。 由於“不信任原則”,沒有服務都會過重的對客戶端進行校驗。
技術實現
前端雖有node服務,但由於並沒有實際運用起來,在結合目前的人員問題。因此決定後端引入網關服務,將各個服務的許可權認證業務分離,統一由網關服務承擔。 說明: 筆者將該服務重新拆分,放棄大前端模式的主要原因是: 1.原有本應由原有node服務所聚合分離不同客戶端介面的功能,並沒有在node中,而是在後端介面中看到各種webController,AppController代碼; 2.受限於前端目前人手及人員技術。 3.筆者自身對前端架構不夠熟悉,前端負責人也贊同此設計。
-
原因
不同的序列化方式使得對緩存問題的排查,數據不夠直觀;
相同的服務由於序列化方式的不同導致同時存在多分;技術實現
1.技術選型確定合理的序列化方式;
2.新開module,完成對redis客戶端的統一包裝RedisService;
3.應用spring-boot提供的自動裝配技術,統一裝配RedisService bean;
4.使用RedisService替換原有服務中所有使用redis客戶端的地方。
2.3 性能優化——保證業務進度的前提下,儘快處理
-
存在迴圈使用調用資料庫的情況而沒有使用批量操作(List,batch)
原因
迴圈中調用介面是編碼的大忌,無需多言,尤其還設計跨服務調用,IO,時間都是幾何級增長。
技術實現
使用調用鏈路記錄,統計出所有的介面調用。再分析出所有迴圈調用介面的鏈路交由開發人員逐一處理。
-
原因
涉及具體業務邏輯,改造時間長,需等業務熟悉之後方可修改技術實現
通過python腳本,分析統計出所有的大數據Key,在代碼中逐一排查審核。
2.4 風格理念——暫不處理
以下幾點由於只是風格理念的問題,因此暫不處理。作為對新服務的規範。新增服務嚴格按照新的規範實施。老服務在之後的重構計劃中再行調整。
-
原因
單一職責,資料庫只存儲數據,業務邏輯交由代碼。
技術實現
禁止使用資料庫存儲之外的其他功能。
-
原因
使用微服務架構,就使用微服務理念
技術實現
需求評審,合理定位,嚴格按照微服務的理念設計服務(掌握粒度,避免過度拆分)。
-
原因
返回大對象會暴露一些不必要的欄位,泄漏隱私;增加IO消耗。技術實現
VO獨立,介面返回欄位按需返回,避免大的VO設計。 -
存在重覆解釋欄位情況
原因
無端的性能消耗技術實現
介面分離,同時提供解釋欄位和不解釋欄位的介面(處於對現有介面的相容,因此提供解釋欄位的介面),這樣消費者可以按需獲取。 說明:按照的架構風格,所有的介面不對字典數據做解釋處理。全部交由前段展示時翻譯。這樣的好處有: 1.對於字典數據的處理,只需要在用戶登錄系統時從後臺請求一次返回保存到本地即可,一次請求多次使用。 2.將解釋欄位的工作交由各個客戶端處理,減少伺服器壓力。
-
1.老服務的命名問題不做處理,幹掉強迫症;
2.在迭代開發中增加代碼註釋;
3.新開發研發按照編碼規範實施,使用sonar檢測代碼質量,不合格代碼一律不予以合併。
-
給產品和測試提需求,要求逐步完善項目需求相關文檔。
技術相關文檔在迭代開發中同步完善。
筆者的話
以上調整,只是筆者對當前項目存在問題處理方式;由於技術能力有限,此中難免有不合理之處,誠邀各位大佬批評指教。
以下連接為筆者平時對技術的整理歸檔也歡迎各位大佬評論交流!
zero mgy ProcessOn