者:琴水玉 cnblogs.com/lovesqcc/p/4319594.html
在程序中打錯誤日志的主要目標是為更好地排查問題和解決問題提供重要線索和指導。但是在實際中打的錯誤日志內容和格式變化多樣,錯誤提示上可能殘缺不全、沒有相關背景、不明其義,使得排查解決問題成為非常不方便或者耗時的操作。
而實際上,如果編程的時候稍加用心,就會減少排查問題的很多無用功。在闡述如何編寫有效的錯誤日志之前,了解錯誤是怎么產生的, 非常重要。
對于當前系統來說, 錯誤的產生由三個地方引入:
a.下層系統處理成功了,但是通信出錯了, 這樣會導致子系統之間的數據不一致;
對于這種情況,可以采用超時補償機制,預先將任務記錄下來,通過定時任務在后續將數據訂正過來。
有什么更好的設計方案,也可以留言。
b.通信成功了,但是下層處理出錯了。
對于這種情況, 需要與下層開發人員溝通, 協調子系統之間的交互;
需要根據下層返回的錯誤碼和錯誤描述做適當的處理或給予合理的提示信息。
無論哪一種情況, 都要假設下層系統可靠性一般, 做好出錯的設計考慮。
本層系統產生錯誤的原因:
原因一:疏忽導致。疏忽是指程序員能力完全可避免此類錯誤但實際上沒做到。比如將 && 敲成了 & ,==敲成了=;邊界錯誤, 復合邏輯判斷錯誤等。疏忽要么是程序員注意力不夠集中, 比如處于疲倦狀態、加班通宵、邊開會邊寫程序;要么是急著實現功能,沒有顧及程序的健壯性等。
改進措施:使用代碼靜態分析工具,通過單元測試行覆蓋可有效避免此類問題。
原因二:錯誤與異常處理不夠周全導致的。比如輸入問題。計算兩個數相加, 不僅要考慮計算溢出問題, 還要考慮輸入非法的情形。對于前者,可能通過了解、犯錯或經驗就可以避免,而對于后者,則必須加以限定,以使之處于我們的智商能夠控制的范圍內,比如使用正則表達式過濾掉不合法的輸入。對于正則表達式必須進行測試。對于不合法輸入, 要給出盡可能詳細、易懂、友好的提示信息、原因及建議方案。
改進措施:盡可能周全地考慮各種錯誤情形和異常處理。在實現主流程之后,增加一個步驟:仔細推敲可能的各種錯誤和異常,返回合理錯誤碼和錯誤描述。每個接口或模塊都有效處理好自己的錯誤和異常,可有效避免因場景交互復雜導致的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實戰篇
打錯誤日志的基本原則:
排查問題的基本步驟:
登錄到應用服務器 -> 打開日志文件 -> 定位到錯誤日志位置 -> 根據錯誤日志的線索的指導去排查、確認問題和解決問題。
其中:
通常, 程序錯誤日志的問題就是日志內容是針對當前代碼情境才能理解,看上去簡潔, 但總是寫的不全, 半英文格式;一旦離開代碼情境, 就很難知道究竟說的是什么, 非要讓人思考一下或者去看看代碼才能明白日志說的是什么含義。這不是自己給自己罪受?拓展:細說 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 ?
錯誤日志是排查問題的重要手段之一。當我們編程實現一項功能時, 通常會考慮可能發生的各種錯誤及相應原因:
要排查出相應的原因, 就需要一些關鍵描述來定位原因。
這就會形成三元組:
錯誤現象 -> 錯誤關鍵描述 -> 最終的錯誤原因。
需要針對每一種錯誤盡可能提供相應的錯誤關鍵描述,從而定位到相應的錯誤原因。
也就是說,編程的時候,要仔細思考, 哪些描述是非常有利于定位錯誤原因的, 盡可能將這些描述添加到錯誤日志中。
1、分類
● 無序列表
元素:ul
全稱:unordered list
>> type屬性,不要使用,已經廢棄,樣式統一使用css控制
● 有序列表
元素:ol
全稱:ordered list
>> 基本使用參考栗子:
>> type屬性,不建議使用,樣式使用css控制
● 定義列表
HTML <dl> 元素 (或 HTML 描述列表元素)是一個包含術語定義以及描述的列表,通常用于展示詞匯表或者元數據 (鍵-值對列表)。
英文全稱:
dl: definition list
dt: definition title
dd: definition description
① 單條術語與描述
② 多條術語,單條描述
③ 單條術語,多條描述
④ 多條術語,多條描述
2、無序列表與有序列表的區別
結合上篇的講解,如何選擇元素,應該取決于它的語義,而不是它的顯示。
舉兩個栗子:
① 場景一
② 場景二
場景一,我們使用有序列表元素,因為這是存在邏輯的,如果我們打亂順序,可能讀者不明所以;相反,場景二我們使用無需列表元素。
容器元素:該元素代表一個塊區域,內部用于放置其他元素
1、 div元素
沒有語義
2、 語義化容器元素
● header元素
用于展示介紹性內容,通常包含一組介紹性的或是輔助導航的實用元素。它可能包含一些標題元素,但也可能包含其他元素,比如 Logo、搜索框、作者名稱,等等。
● footer元素
表示最近一個章節內容或者根節點(sectioning root )元素的頁腳。一個頁腳通常包含該章節作者、版權數據或者與文檔相關的鏈接等信息。
● article
表示文檔、頁面、應用或網站中的獨立結構,其意在成為可獨立分配的或可復用的結構,如在發布中,它可能是論壇帖子、雜志或新聞文章、博客、用戶提交的評論、交互式組件,或者其他獨立的內容項目。??
● section
表示一個包含在HTML文檔中的獨立部分,它沒有更具體的語義元素來表示,一般來說會有包含一個標題。
● aside
表示一個和其余頁面內容幾乎無關的部分,被認為是獨立于該內容的一部分并且可以被單獨的拆分出來而不會使整體受影響。其通常表現為側邊欄或者標注框(call-out boxes)。
>>>《Python Web全棧之旅01--Web前端●走入HTML的世界(中)》
Python Web前端的學習走完html的旅程,html相當于房屋的骨架,如何提高自己對整體骨架的搭建?可以多參考大型公司官網主頁,學習別人的搭建的思路,逐步提高自身的能力。
者:張鑫旭(@張鑫旭)
網址:http://www.zhangxinxu.com/wordpress/2016/03/know-about-css3-all/
一、兼容性
一些CSS文章,或者CSS文檔,兼容性什么的都是放在最后。
又不是什么見不得人的事情,來,直接開篇就亮刺刀。
兼容性還行, 除了IE不支持,其他瀏覽器基本上都綠油油,目前自娛自樂,內網項目什么的都是可以用用的。
二、all是干嘛用的
all屬性實際上是所有CSS屬性的縮寫,表示,所有的CSS屬性都怎樣怎樣,但是,不包括unicode-bidi和direction這兩個CSS屬性。
為什么會有這個CSS屬性呢?
我們可能知道,有些CSS屬性值基本上所有CSS屬性都有,比方說繼承inherit!
我們CSS reset輸入框的時候,是不是有類似這樣的代碼(實際可能是具體值,作用類似):
input, textarea {
color: inherit;
font-size: inherit;
font-family: inherit;
}
因為這類輸入控件自身有內置的大小和字體,需要重置。
此時,大家就會發現,這些屬性值都是inherit, 要是可以合并就好了!
喲喲喲,說到點子上了,CSS all屬性就是用來合并的。
input, textarea {
all: inherit;
}
喔噢,三合一,頓時一陣春風拂面之感。但是,這里僅僅是展示作用,實際上是不會這么用的,因為,all:inherit會讓背景色什么的,都繼承父級,相信這不是你希望看到的。
三、語法和區別
語法如下:
all: initial;
all: inherit;
all: unset;
/* CSS4特性,無視之 */
all: revert;
支持三個CSS通用屬性值,initial, inherit, unset. 這三個屬性值分別表示什么意思呢?
您可以狠狠地點擊這里:CSS3 all屬性演示demo(http://www.zhangxinxu.com/study/201603/css3-all.html)
默認的HTML和CSS是這樣的,一個傳統的標簽內容,有標題有列表:
<article>
<h6>標題</h6>
<p>p變文字</p>
<ol>
<li>有序列表1</li>
<li>有序列表2</li>
<li>有序列表3</li>
</ol>
<textarea>文本域</textarea>
</article>
article {
background-color: #f0f3f9;
color: green;
}
article > textarea {
border: 1px solid #34538b;
background-color: #ffffe0;
color: red;
}
這盛世,如你所見:
顏色啊,間距啊,以及文本域狀態都是我們認為的樣子顯示。
現在,點擊demo的下拉,選擇對應的all屬性值,實現下面CSS效果:
.initial > * {
all: initial;
}
.inherit > * {
all: inherit;
}
.unset > * {
all: unset;
}
結果:
initial
initial是初始值的意思,也就是,article元素下面所有的第一級子元素都除了unicode-bidi和direction以外的CSS都使用初始值。
例如,<h6>, <p>元素瀏覽器內置的display:block直接拜拜了,都變成了inline元素,因此在一行顯示:
font-size也使用了瀏覽器軟件本身設置的大小16px,color顏色也變成了瀏覽器軟件本身的黑色。例如,在瀏覽器設置中改變字號,從中變成大:
會看到顯示的文字內容也變大了:
由于我們只是對相鄰層級子元素進行了initial設置,因此,<li>元素不受影響,但是,由于在父元素不明的情況下,<li>元素的默認是打點,因此,這里從數字變成了點,list-style-type和list-style-position都變化了。
inherit
inherit是繼承的意思,也就是,article元素下面所有的相鄰子元素都除了unicode-bidi和direction以外的CSS都繼承了<article>元素的CSS。
因此,<h6>, <p>元素還是塊狀的,background-color都是<article>元素的背景色,color顏色也跟<article>一樣,是綠色(文本域的紅色被干掉了)。
不僅上面這些CSS,padding/margin也都繼承了,只是默認是0, 看不出來,我們稍微修改下,例如給<article>元素來個margin值:
結果,那些子元素都開花了:
unset
unset是取消設置的意思,也就是,article元素下面所有的相鄰子元素除了unicode-bidi和direction以外的CSS都干掉都不要,不要了那用什么呢?unset值的特性如下,當前元素瀏覽器或用戶設置的CSS忽略,然后如果是具有繼承特性的CSS,如color, 則使用繼承值;如果是沒有繼承特性的CSS屬性,如background-color, 則使用初始值。
因此,<h6>, <p>元素的display屬性值使用了initial初始值,因此變成了inline元素,兩個同一行顯示了;而這些元素的color顏色使用了inherit繼承值,因此都是綠色,<textarea>的背景色background-color不具有繼承特性,因此使用的是initial初始值,也就是transparent透明,因此,就是截圖所示效果。
三、結語
動手寫文章之前,還以為這個CSS屬性會有意想不到的妙用,最后結果是,表現確實意想不到,至于妙用,我只能科科一下。
all所牽扯的CSS實在是太多了,我目前還是想不出有什么場景,就是要用這個CSS屬性。或者說什么時候,希望特殊的元素一下子變成普通元素,例如,input輸入框外面一個div,來個input{all:inherit;},貌似還真有這樣的場景,輸入框不需要border不需要background不需要padding不需要margin, 恩,不錯,不枉寫此文了!
更多精彩內容在微信公眾平臺:網頁設計自學平臺
干貨!免費領取Adobe高級講師前端教程
點我領取
*請認真填寫需求信息,我們會在24小時內與您取得聯系。