1.上層系統引入的非法參數。對於非法參數引入的錯誤,可以透過參數校驗和前置條件校驗來截獲錯誤;
#2.與下層系統互動產生的錯誤。與下層互動產生的錯誤, 有兩種:
a.下層系統處理成功了,但是通訊出錯了, 這樣會導致子系統之間的資料不一致;
對於這種情況,可以採用超時補償機制,預先將任務記錄下來,透過定時任務在後續將資料訂正過來。
有什麼更好的設計方案,也可以留言。
b.通訊成功了,但是下層處理出錯了。
對於這種情況,需要與下層開發人員溝通, 協調子系統之間的交互;
需要根據下層返回的錯誤碼和錯誤描述做適當的處理或給予合理的提示訊息。
無論哪一種情況, 都要假設下層系統可靠度一般, 做好出錯的設計考量。
3.本層系統處理出錯。
本層系統產生錯誤的原因:
原因一:疏忽導致。疏忽是指程式設計師能力完全可避免此類錯誤但實際上無法做到。例如將 && 敲成了 & , == 敲成了 = ;邊界錯誤, 複合邏輯判斷錯誤等。疏忽不是程式設計師注意力不夠集中, 例如處於疲倦狀態、加班通宵、邊開會邊寫程序;要嘛是急著實現功能,沒有顧及程序的健壯性等。
改進措施:使用程式碼靜態分析工具,透過單元測試行覆寫可有效避免此類問題。
原因二:錯誤與異常處理不夠周全所導致的。比如輸入問題。計算兩個數字相加, 不僅要考慮計算溢位問題, 還要考慮輸入非法的情形。對於前者,可能透過了解、犯錯或經驗就可以避免,而對於後者,則必須加以限定,以使之處於我們的智商能夠控制的範圍內,例如使用正規表示式過濾掉不合法的輸入。對於正規表示式必須進行測試。對於不合法輸入, 要給予盡可能詳細、易懂、友善的提示資訊、原因及建議方案。
改進措施:盡可能周全地考慮各種錯誤情形和異常處理。在實現主流程之後,增加一個步驟:仔細推敲可能的各種錯誤和異常,返回合理錯誤碼和錯誤描述。每個介面或模組都有效處理好自己的錯誤和異常,可有效避免因場景互動複雜而導致的bug。
譬如,一個業務用例由場景A.B.C互動完成。實際執行A.B成功了,C失敗了,這時B需要根據C返回合理的代碼和訊息進行回滾並返回給A合理的代碼和訊息,A根據B的返回進行回滾,並返回給客戶端合理的代碼和訊息。這是一種分段回滾的機制,要求每個場景都必須考慮異常情況下的回滾。
原因三:邏輯耦合緊密導致。由於業務邏輯耦合緊密, 隨著軟體產品一步步發展, 各種邏輯關係錯綜複雜, 難以看到全局狀況,導致局部修改影響波及到全局範圍,造成不可預測的問題。
改進措施:寫短函數和短方法,每個函數或方法最好不超過 50 行。寫無狀態函數和方法, 只讀全域狀態, 相同的前提條件總是會輸出相同的結果, 不會依賴外部狀態而變更自己的行為;定義合理的結構、 介面和邏輯段, 使介面之間的交互盡可能正交、低耦合;對於服務層, 盡可能提供簡單、正交的介面;持續重構, 保持應用模組化和鬆散耦合, 理清邏輯依賴關係。
對於有大量業務接口相互影響的情況, 必須整理各個業務接口的邏輯流程及相互依賴關係, 從整體上進行優化;對於有大量狀態的實體, 也需要梳理相關的業務接口,整理狀態之間的轉換關係。
原因四:演算法不正確導致。
改進措施:首先將演算法從應用中分離出來。若演算法有多種實現, 可以透過交叉校驗的單元測試找出來, 例如排序操作;如果演算法具有可逆性質, 可以透過可逆校驗的單元測試找出來, 例如加密解密操作。
原因五:相同類型的參數,傳入順序錯誤導致。例如,modifyFlow(int rx, int tx), 實際呼叫為 modifyFlow(tx,rx)
改進措施:盡可能將類型具體化。此用浮點數就用浮點數, 該用字串就用字串, 該用具體物件類型就用具體物件類型;相同類型的參數盡可能錯開;如果上述都無法滿足, 就必須透過介面測試來驗證, 介面參數值務必是不同的。
原因六:空指標異常。空指標異常通常是物件沒有正確初始化, 或使用物件之前沒有對物件是否非空做偵測。
改進措施:對於配置對象, 偵測其是否成功初始化;對於普通對象, 取得到實體物件使用之前, 偵測是否非空。
原因七:網路通訊錯誤。網路通訊錯誤通常是因為網路延遲、阻塞或不通導致的錯誤。網路通訊錯誤通常是小機率事件, 但小機率事件很可能會導致大面積的故障、 難以復現的BUG。
改進措施:在前一個子系統的結束點和後一個子系統的入口點分別打 INFO 日誌。透過兩者的時間差提供一點線索。
原因八:交易與並發錯誤。事務與並發結合在一起, 很容易產生非常難以定位的錯誤。
改進措施:對於程式中的並發操作, 涉及共享變數及重要狀態修改的, 要加 INFO 日誌。
如果有更有效的做法,歡迎留言指出。
原因九:設定錯誤。
改進措施:在啟動應用程式或啟動對應設定時, 偵測所有的設定項, 列印對應的INFO日誌, 確保所有設定都載入成功。
原因十:業務不熟悉導致的錯誤。在中大型系統, 部分業務邏輯和業務互動都比較複雜, 整個的業務邏輯可能存在於多個開發同學的大腦裡, 每個人的認識都不是完整的。這很容易導致業務編碼錯誤。
改進措施:透過多人討論和溝通, 設計正確的業務用例, 根據業務用例來編寫和實現業務邏輯;最終的業務邏輯和業務用例必須完整存檔;在業務介面中註明該業務的前置條件、處理邏輯、後置校驗和注意事項;當業務變化時, 需要同步更新業務註解;代碼REVIEW。業務註釋是業務介面的重要文檔, 對業務理解起著重要的快取作用。
原因十一:設計問題導致的錯誤。例如同步串列方式會有效能、反應慢的問題, 而並發非同步方式可以解決效能、反應慢的問題, 但會帶來安全、正確的隱患。非同步方式會導致程式設計模型的改變, 新增非同步訊息推播和接收等新的問題。使用快取能夠提高效能, 但是又會出現快取更新的問題。
改進措施:撰寫和仔細評審設計文件。設計文件必須闡述背景、需求、所滿足的業務目標、要達到的業務績效指標、可能的影響、設計整體思路、詳細方案、預見該方案的優缺點及可能的影響;透過測試和驗收, 確保改設計方案確實符合業務目標和業務績效指標。
原因十二:未知細節問題導致的錯誤。例如緩衝區溢位、 SQL 注入攻擊。從功能上看來是沒有問題的, 但是從惡意使用來看, 是存在漏洞的。再例如, 選擇 jackson 函式庫做 JSON 字串解析, 預設情況下, 當物件新增欄位時會導致解析出錯。必須在物件上加上 @JsonIgnoreProperties(ignoreUnknown = true) 註解才能正確應對變化。如果選用其他 JSON 函式庫就不一定有這個問題。
改進措施:一方面要透過經驗積累, 另一方面, 考慮安全問題和例外情況, 選擇成熟的經過嚴格測試的函式庫。
原因十三:隨時間變化而出現的bug。有些解決方案在過去看來是很不錯的,但在當前或未來的情景中可能變得笨拙甚至不中用,也是常見的事情。例如像加密解密演算法, 在過去可能認為是完善的, 在破解之後就要慎重使用了。
改進措施:專注於變更以及漏洞修復訊息,及時修正過時的程式碼、函式庫、行為。
原因十四:硬體相關的錯誤。例如記憶體洩露, 儲存空間不足, OutOfMemoryError 等。
改進措施:增加對應用系統的 CPU / 記憶體 / 網路等重要指標的效能監控。
系統出現的常見錯誤:
實體在資料庫中的記錄不存在,必須指明是哪個實體或實體識別;
實體配置不正確,必須指明是哪個配置有問題,正確的配置應該是什麼;
實體資源不符合條件,必須指明當前資源是什麼,資源要求是什麼;
實體操作前置條件不滿足,必須指明需要滿足什麼前置條件,目前的狀態是什麼;
實體操作後置校驗不滿足, 必須指明需要滿足什麼後置校驗, 當前的狀態是什麼;
性能問題導致逾時, 必須指明是什麼導致的性能問題,後續如何最佳化;
多個子系統互動通訊出錯導致之間的狀態或資料不一致?
一般難以定位的錯誤會出現在比較底層的地方。因為底層無法預知具體的業務場景, 給出的錯誤訊息都是比較通用的。
這就要求在業務上層提供盡可能豐富的線索。錯誤的產生一定是多個系統或層次互動的過程中在某一層堆疊上不滿足前置條件導致。在程式設計時, 在每一層堆疊中盡可能確保所有必須的前置條件滿足,盡可能避免錯誤的參數傳遞到底層, 盡可能地將錯誤截獲在業務層。
大多數錯誤都是由多種原因組合產生。但每一種錯誤必定有其原因。在解決錯誤之後, 要深入分析錯誤是如何發生的, 如何避免這些錯誤再次發生。努力就能成功, 但是:反思才能進步 !推薦:Java優雅的記錄日誌:log4j實戰篇
如何寫出更容易排查問題的錯誤日誌
打錯誤日誌的基本原則:
#盡可能完整。每個錯誤日誌都完整描述了:什麼場景下發生了什麼錯誤, 什麼原因(或哪些可能原因), 如何解決(或解決提示);
盡可能具體。例如 NC 資源不足, 究竟具體指什麼資源不足, 是否可以透過程式直接指明;通用錯誤,例如 VM NOT EXIST , 要指明在什麼場景下發生的,可能便於後續統計的工作。
#盡可能直接。最理想的錯誤日誌應該讓人在第一直覺下能夠知道是什麼原因導致,該怎麼去解決,而不是還要透過若干步驟去找真正的原因。
將已有經驗整合直接到系統中。所有已經解決過的問題及經驗都要盡可能以友善的方式整合到系統中,給新進人員更好的提示,而不是埋藏在其他地方。
排版要整齊有序, 格式統一化規範化。密密麻麻、隨筆式的日誌看著就揪心, 相當不友好, 也不便於排查問題。
採用多個關鍵字唯一識別請求,突出顯示關鍵字:時間、實體識別碼(例如vmname)、操作名稱。
排查問題的基本步驟:
登入應用程式伺服器-> 開啟日誌檔案-> 定位到錯誤日誌位置-> 根據錯誤日誌的線索的指導去排查、確認問題和解決問題。
其中:
從登陸到開啟日誌檔案。由於應用程式伺服器有多台, 要逐一登入上去查看實在不方便。需要編寫一個工具放在 AG 上直接在 AG 上查看所有伺服器日誌, 甚至直接篩選出所需的錯誤日誌。
定位錯誤日誌位置。目前日誌的排版密密麻麻,不易定位到錯誤日誌。一般可以先採用"時間"來定位到錯誤日誌的附近前面的地方, 然後使用 實體關鍵字 / 操作名稱 組合來鎖定錯誤日誌地方。根據 requestId 定位錯誤日誌雖然比較符合傳統,但是要先找到 requestId , 且不具描述性。最好能直接根據時間/內容關鍵字來定位錯誤日誌位置。
3. 分析錯誤日誌。錯誤日誌的內容最好能夠更加直接明了, 能夠明確指明與當前要排查的問題特徵是吻合的, 並且給出重要線索。
通常, 程式錯誤日誌的問題就是日誌內容是針對當前程式碼情境才能理解,看上去簡潔, 但總是寫的不全, 半英文格式;一旦離開程式碼情境, 就很難知道究竟說的是什麼, 非要讓讓人思考一下或去看看程式碼才能明白日誌說的是什麼意義。這不是自己給自己罪?拓展:細說Java 主流日誌工具庫
例如:
if ((storageType == StorageType.dfs1 || storageType == StorageType.dfs2) && (zone.hasStorageType(StorageType.io3) || zone.hasStorageType(StorageType.io4))) { // 进入dfs1 和dfs2 在io3 io4 存储。 } else { log.info("zone storage type not support, zone: " + zone.getZoneId() + ", storageType: " + storageType.name()); throw new BizException(DeviceErrorCode.ZONE_STORAGE_TYPE_NOT_SUPPORT); }
zone 要支援什麼storage type 才是正確的? Do Not Let Me Think !
#錯誤日誌應該要做到:即使離開程式碼情境,也能清楚地描述發生了什麼事。
此外,如果能夠直接在錯誤日誌中說明清楚原因, 在做巡檢日誌的時候也可以省些力氣。
從某種意義上來說, 錯誤日誌也可以是一種非常有益的文檔,記錄著各種不合法的運行用例。
目前程式錯誤日誌的內容可能有以下問題:
1.錯誤日誌沒有指明錯誤參數和內容:
catch(Exception ex){ log.error("control ip insert failed", ex); return new ResultSet<AddControlIpResponse>( ControlIpErrorCode.ERROR_CONTROL_IP_INSERT_FAILURE); }
沒有指明插入失敗的control ip. 如果加上control ip 關鍵字, 更容易搜尋和鎖定錯誤。
類似的還有:
log.error("Get some errors when insert subnet and its IPs into database. Add subnet or IP failure.", e);
沒有指明是哪個 subnet 的它下屬的哪些 IP. 值得注意的是, 要指明這些要額外做一些事情, 可能會稍微影響效能。這時候需要權衡效能和可調試性。
解决方案:使用 String.format("Some msg to ErrorObj: %s", errobj) 方法指明错误参数及内容。
这通常要求对 DO 对象编写可读的 toString 方法。
2.错误场景不明确:
log.error("nc has exist, nc ip" + request.getIp());
在 createNc 中检测到 NC 已经存在报错。但是日志上没有指明错误场景, 让人猜测,为什么会报NC已存在错误。
可以改为
log.error("nc has exist when want to create nc, please check nc parameters. Given nc ip: " + request.getIp()); log.error("[create nc] nc has exist, please check nc parameters. Given nc ip: " + request.getIp());
类似的还有:
log.error("not all vm destroyed, nc id " + request.getNcId());
改成
log.error("[delete nc] some vms [%s] in the nc are not destroyed. nc id: %s", vmNames, request.getNcId());
解决方案:错误消息加上 when 字句, 或者错误消息前加上 【接口名】, 指明错误场景,直接从错误日志就知道明白了。
一般能够知道 executor 的可以加上 【接口名】, service 加上 when 字句。
3.内容不明确, 或不明其义:
if(aliMonitorReporter == null) { log.error("aliMonitorReporter is null!"); } else { aliMonitorReporter.attach(new ThreadPoolMonitor(namePrefix, asynTaskThreadPool.getThreadPoolExecutor())); }
改为:
log.error("aliMonitorReporter is null, probably not initialized properly, please check configuration in file xxx.");
类似的还有:
if (diskWbps == null && diskRbps == null && diskWiops == null && diskRiops == null) { log.error("none of attribute is specified for modifying"); throw new BizException(DeviceErrorCode.NO_ATTRIBUTE_FOR_MODIFY); }
改为
log.error("[modify disk attribute] None of [diskWbps,diskRbps,diskWiops,diskRiops] is specified for disk id:" + diskId);
解决方案:更清晰贴切地描述错误内容。
4.排查问题的引导内容不明确:
log.error("get gw group ip segment failed. zkPath: " + LockResource.getGwGroupIpSegmnetLockPath(request.getGwGroupId()));
zkPath ? 如何去排查这个问题?我该去找谁?到哪里去查找更具体的线索?
解决方案:加上相应的背景知识和引导排查措施。
5.错误内容不够具体细致:
if (!ncResourceService.isNcResourceEnough(ncResourceDO, vmResourceCondition)) { log.error("disk space is not enough at vm's nc, nc id:" + vmDO.getNcId()); throw new BizException(ResourceErrorCode.ERROR_RESOURCE_NOT_ENOUGH); }
究竟是什么资源不够?目前剩余多少?现在需要多少?值得注意的是, 要指明这些要额外做一些事情, 可能会稍微影响性能。这时候需要权衡性能和可调试性。
解决方案:通过改进程序或程序技巧, 尽可能揭示出具体的差异所在, 减少人工比对的操作。
6.半英文句式读起来不够清晰明白,需要思考来拼凑起完整的意思:
log.warn("cache status conflict, device id "+deviceDO.getId()+" db status "+deviceDO.getStatus() +", nc status "+ status);
改为:
log.warn(String.format("[query cache status] device cache status conflicts between regiondb and nc, status of device '%s' in regiondb is %s , but is %s in nc.", deviceDO.getId(), deviceDO.getStatus(), status));
解决方案:改为自然可读的英文句式。
总结起来, 错误日志格式可以为:
log.error("[接口名或操作名] [Some Error Msg] happens. [params] [Probably Because]. [Probably need to do]."); log.error(String.format("[接口名或操作名] [Some Error Msg] happens. [%s]. [Probably Because]. [Probably need to do].", params));
或
log.error("[Some Error Msg] happens to 错误参数或内容 when [in some condition]. [Probably Because]. [Probably need to do]."); log.error(String.format("[Some Error Msg] happens to %s when [in some condition]. [Probably Because]. [Probably need to do].", parameters));
[Probably Reason]. [Probably need to do]. 在某些情况下可以省略;在一些重要接口和场景下最好能说明一下。
每一条错误日志都是独立的,尽可能完整、具体、直接说明何种场景下发生了什么错误,由什么原因导致,要采用什么措施或步骤。
问题:
1.String.format 的性能会影响打日志吗?
一般来说, 错误日志应该是比较少的, 使用 String.format 的频度并不会太高,不会对应用和日志造成影响。
2.开发时间非常紧张时, 有时间去斟酌字句吗?
建立一个标准化的内容格式,将内容往格式套,可以节省斟酌字句的时间。
3.什么时候使用 info, warn , error ?
info 用于打印程序应该出现的正常状态信息, 便于追踪定位;
warn 表明系统出现轻微的不合理但不影响运行和使用;
error 表明出现了系统错误和异常,无法正常完成目标操作。
错误日志是排查问题的重要手段之一。当我们编程实现一项功能时, 通常会考虑可能发生的各种错误及相应原因:
要排查出相应的原因, 就需要一些关键描述来定位原因。
这就会形成三元组:
错误现象 -> 错误关键描述 -> 最终的错误原因。
需要针对每一种错误尽可能提供相应的错误关键描述,从而定位到相应的错误原因。
以上是Java專案中錯誤日誌如何列印的詳細內容。更多資訊請關注PHP中文網其他相關文章!