防微杜漸!向扁鵲學(xué)習(xí)治理代碼
疾在腠理,湯熨所及
扁鵲見蔡桓公,立有間。扁鵲曰:“君有疾在腠理,不治將恐深?!被负钤唬骸肮讶藷o疾?!北怡o出,桓侯曰:“醫(yī)之好治不病以為功?!?/p>
居十日,扁鵲復(fù)見,曰:“君之病在肌膚,不治將益深?!被负畈粦?yīng),扁鵲出,桓侯又不悅。
居十日,扁鵲復(fù)見,曰:“君之病在腸胃,不治將益深?!被负钣植粦?yīng),扁鵲出,桓侯又不悅。
居十日,扁鵲望桓侯而還走,桓侯故使人問之。扁鵲曰:“疾在腠理,湯熨之所及也;在肌膚,針石之所及也;在腸胃,火齊之所及也;在骨髓,司命之所屬,無奈何也。今在骨髓,臣是以無請(qǐng)也。”
居五日,桓公體痛,使人索扁鵲,已逃秦矣?;负钏焖馈?/p>
“扁鵲見蔡桓公“曾入選中學(xué)課本,當(dāng)年的教材節(jié)選刪去了原文后面的一句議論:
故良醫(yī)之治病也,攻之于腠理。此皆爭(zhēng)之于小者也。夫事之禍福亦有腠理之地,故曰:“圣人蚤從事焉?!?/p>
故而語文老師們?cè)谥v授這篇文章時(shí),將其中心思想落腳在“人要正視缺點(diǎn),切莫諱疾忌醫(yī)”上。但實(shí)際上有些斷章取義,作者的中心思想其實(shí)是借扁鵲闡述的醫(yī)理來講解做事的方法,即要爭(zhēng)之于小、蚤(早)從事。
在互聯(lián)網(wǎng)公司的開發(fā)工作中,80%甚至90%的程序員都是在做業(yè)務(wù)開發(fā)。盡管所應(yīng)用的技術(shù)深度和難度未必比的上基礎(chǔ)架構(gòu)的研發(fā),但隨著業(yè)務(wù)的成熟,在紛繁復(fù)雜的框架結(jié)構(gòu)與業(yè)務(wù)邏輯之上持續(xù)進(jìn)行快速且又穩(wěn)定的迭代也絕非易事。尤其是在已經(jīng)度過了快速迭代,野蠻生長(zhǎng)的階段,進(jìn)入互聯(lián)網(wǎng)行業(yè)的下半場(chǎng)之后,如何實(shí)現(xiàn)快遞迭代但又能保持持續(xù)穩(wěn)定這一目標(biāo),是一個(gè)重要的課題。
在前司期間,我陸續(xù)負(fù)責(zé)了多個(gè)模塊的工程開發(fā),每天都有大量算法同學(xué)在我管轄的模塊上進(jìn)行策略迭代。隨著日積月累的迭代,代碼腐壞在所難免,并且其中有兩個(gè)模塊本身有就很重的歷史包袱,作為工程OWNER,我個(gè)人除了一些業(yè)務(wù)需求開發(fā)外,也需要對(duì)模塊的穩(wěn)定性、性能問題以及算法同學(xué)的研發(fā)效率進(jìn)行負(fù)責(zé)。經(jīng)常面對(duì)各種core dump、內(nèi)存泄露、耗時(shí)暴漲等問題實(shí)在是疲于奔命。對(duì)于代碼的治理必須馬上提上議程,彼時(shí)心中千頭萬緒卻不知該從何做起。這時(shí)“疾在腠理,湯熨之所及也,今在骨髓,臣是以無請(qǐng)也?!暗脑~句映入腦海。
從扁鵲這里延伸,不難理解經(jīng)典的中醫(yī)學(xué)說:防病于未萌,治病于初起。并且治不如防。這和“未雨綢繆”一詞是類似的道理,更通俗一點(diǎn)的說法就是:
亡羊補(bǔ)牢不如防患于未然 。
“防病于未萌”是最理想的狀態(tài),即在沒有風(fēng)險(xiǎn)沒有暴露的時(shí)候就考慮到進(jìn)行預(yù)防。不過就如同棋手落子之前的計(jì)算,通常也會(huì)百密一疏,在實(shí)際工作中則有可能百密十疏。所以“治病于初起”其實(shí)也未嘗不可,即在風(fēng)險(xiǎn)初露的時(shí)候盡早拔除。留心觀察,見招拆招,以不變應(yīng)萬變。思來想去個(gè)人感覺“防微杜漸”一詞或許更為簡(jiǎn)明扼要,言簡(jiǎn)意賅。下面我都會(huì)盡量使用“防微杜漸”一詞來給我心中方法論下定義。
總而言之就是風(fēng)險(xiǎn)越早發(fā)現(xiàn)越好。做到早發(fā)現(xiàn),早治療。因?yàn)樵皆绨l(fā)現(xiàn),它造成的影響也越小,通常越早發(fā)現(xiàn)也意味著越好排查和解決。作為后臺(tái)開發(fā)程序員,我個(gè)人總結(jié)了幾句箴言:
能在編譯時(shí)發(fā)現(xiàn),不在開發(fā)時(shí)發(fā)現(xiàn)
能在開發(fā)時(shí)發(fā)現(xiàn),不在測(cè)試時(shí)發(fā)現(xiàn)
能在測(cè)試時(shí)發(fā)現(xiàn),不在上線時(shí)發(fā)現(xiàn)
能在服務(wù)啟動(dòng)時(shí)發(fā)現(xiàn),不在請(qǐng)求處理時(shí)發(fā)現(xiàn)
中國(guó)人講道與術(shù),本文中我會(huì)介紹一些術(shù),但是我需要大家明白,本文目的其實(shí)是讓大家對(duì)本文所述之道有更深刻的認(rèn)知。因?yàn)樾g(shù)是有局限性的,比如我是寫C++的后臺(tái)程序員,對(duì)于編程語言的部分,就不會(huì)闡述到Jave/Go的防治之術(shù),但是我認(rèn)為理解本文所述之道,對(duì)所有程序員都會(huì)有所幫助。另外術(shù)是沒辦法通過一篇文章窮盡的,記住只要心中有道,術(shù)會(huì)自生。
在編譯時(shí)發(fā)現(xiàn)
為什么要在編譯時(shí)發(fā)現(xiàn),因?yàn)檫@是最靠前的階段,如果能在編譯期間發(fā)現(xiàn)問題,能大大的節(jié)省我們開發(fā)自測(cè)的時(shí)間。往小了說,一些疏忽大意引發(fā)的問題,很可能在測(cè)試的時(shí)候排查很久才能找到問題,浪費(fèi)自己的時(shí)間,白白加班!往大了說,這種問題可能在測(cè)試階段都測(cè)不出來,直接帶到線上。引發(fā)風(fēng)險(xiǎn)。
先說一個(gè)親身經(jīng)歷。
C++中比較相等時(shí),右值應(yīng)放前面
這其實(shí)是大學(xué)時(shí),老師就講過的一個(gè)好的代碼習(xí)慣,即:
if (a == 1) {
}
應(yīng)該寫成:
if (1 == a) {
}
這樣主要是避免手誤,把 if (a == 1) 寫成 if (a = 1),因?yàn)樵贑++中 if (a = 1)是合法的,能通過編譯的,這個(gè)表達(dá)式的值為true,但 if (1 = a) 是不合法的語法,如果錯(cuò)把 == 寫成 = 編譯會(huì)失敗。當(dāng)然在Java中也是不合法的,可不遵守這一習(xí)慣。
盡管我早就知道右值放左邊是一個(gè)好的習(xí)慣,但是卻很少遵守它,感覺這樣代碼會(huì)不夠順手,a == 1 更符合左手到右的思維。就好像 if (vec.size() > 0) 比 if (!vec.empty()) 更順手一樣,因?yàn)楹笳呓?jīng)常會(huì)是先寫vec.empty(),再把光標(biāo)移動(dòng)到前面補(bǔ)!。另外感覺自己不會(huì)犯錯(cuò),寫出 if (a = 1) 這樣的代碼。
但多年以前我在百度工作期間,我就曾寫下了這樣的代碼。當(dāng)時(shí)有某一個(gè)新策略只在部分請(qǐng)求中生效,而如何判斷是否滿足條件,是去檢查一個(gè)int類型的變量是否為1。當(dāng)時(shí)應(yīng)該是需求比較匆忙,最后這行代碼并沒測(cè)試就上線了。沒錯(cuò)。我寫成了 = ,所以變成了全流量生效,因?yàn)槭悄硞€(gè)垂類業(yè)務(wù),對(duì)大盤影響不大,自身對(duì)監(jiān)控關(guān)注又不夠,所以終于釀成事故。好在由于廣告業(yè)務(wù)的特殊性,前期損失的收益在當(dāng)日后面的時(shí)間可以補(bǔ)救回來,所以當(dāng)日統(tǒng)計(jì)最終影響面并不大。但此次問題,我深感懊悔,因?yàn)檫@是一個(gè)十分低級(jí)的問題。
彼時(shí)回想起之前參加過的其他人的事故case study會(huì)議,經(jīng)常聽到的反思和改進(jìn)是:仔細(xì)、仔細(xì)、再仔細(xì)。但此次經(jīng)歷讓我深刻認(rèn)識(shí)到靠人的仔細(xì)是靠不住的,所以我認(rèn)為:
不能依靠仔細(xì),要依靠工具!
我所說的工具并不只是說依靠軟件或者Linux命令,也包括這種編程習(xí)慣。也許你會(huì)說,只要加強(qiáng)測(cè)試就可以了,是你測(cè)試不夠,但是一方面測(cè)試也可能百密一疏,另外如果編譯階段能發(fā)現(xiàn),那么解決起來肯定比測(cè)試時(shí)發(fā)現(xiàn)邏輯不符合預(yù)期后再去排查問題原因要更節(jié)省時(shí)間!
從此以后我再也不去寫右值在左的代碼,盡管有時(shí)候比較對(duì)象是常量并非變量。雖然a是常量的時(shí)候 if (a = 1)編譯也會(huì)失敗。但是好的編程習(xí)慣就是這樣,就是只要你無腦遵守就好了,減少一些思維勞動(dòng)去檢查比較對(duì)象是不是常量,讓自己變的機(jī)械有時(shí)候未必是壞事。
當(dāng)然如果是 != 或者 > < 都不需要把右值放到左邊。
值得一提是,基本數(shù)據(jù)類型遵守右值在前這個(gè)規(guī)則其實(shí)被遵守的概率很高,但其他類型的比較可能更容易被忽視。比如迭代器,比如:
auto it = exp_map.find(key);
if (it == exp.end()) {
...
}
應(yīng)寫作:
auto it = exp_map.find(key);
if (exp_map.end() == it) {
...
}
在第一種寫法中,如果 == 如果寫錯(cuò)成 =,也是能編譯通過的。
還有哪些工具或者工具思維可以幫助我們避免風(fēng)險(xiǎn)呢?下面會(huì)介紹一些C++相關(guān)的“術(shù)”的部分。如果是其他編程語言的使用者也可以跳過。如前文所言,“術(shù)”有局限性,并且無法窮盡。
利用g++參數(shù)
對(duì)于C++項(xiàng)目而言,首先我們要學(xué)會(huì)利用g++的編譯參數(shù)來幫我們規(guī)范風(fēng)險(xiǎn),如果你使用其他編譯器,應(yīng)該也有類似設(shè)置。
-Werror的探討
在條件允許的時(shí)候開啟-Werror是最理想的,它不放過任何語法錯(cuò)誤。但有時(shí)候不太現(xiàn)實(shí),因?yàn)槲覀円矔?huì)依賴到外部代碼,開啟-Werror導(dǎo)致外部庫(kù)的代碼編譯不過,我們可能不能直接修改它們的代碼。你可能會(huì)說你是以庫(kù)(.a,.so)的形式依賴的,不會(huì)受-Werror的編譯參數(shù)影響,但是你別忘記頭文件,頭文件是直接include到自己項(xiàng)目中并且參與編譯的,頭文件中有時(shí)候也是會(huì)有語法問題的,盡管可能不如源文件多。并且這對(duì)于header only的開源庫(kù)來說可能是災(zāi)難,因?yàn)樗麄兌贾挥蓄^文件,沒有源文件,大量邏輯寫在頭文件中。比如rapidjson、taskflow
關(guān)閉-fpermissive
g++編譯問題千萬條,-fpermissive 第一條!
作為C++程序員,第一職業(yè)素養(yǎng)就是不要開啟-fpermissive編譯參數(shù),如果老項(xiàng)目已經(jīng)有開啟的時(shí)候,那么就要關(guān)閉!因?yàn)?fpermissive會(huì)放過很多不規(guī)范甚至不合法的C++語法。比如:
- const的變量被修改
- 函數(shù)傳參和函數(shù)聲明不一致
- 頭文件中聲明了函數(shù)默認(rèn)值,源文件又也聲明了默認(rèn)(有默認(rèn)值不一致的風(fēng)險(xiǎn))
- 給中間的參數(shù)設(shè)置了默認(rèn)值但是最后一個(gè)的函數(shù)參數(shù)沒有默認(rèn)值
- 類的static成員在源文件定義的時(shí)候也寫了static(可能危害不大,但不規(guī)范)
- 其他
諸如此類的代碼問題竟然都能編譯通過,有時(shí)候你可能覺得對(duì)這些不規(guī)范語法零容忍僅僅是我個(gè)人的“代碼潔癖”。其實(shí)不然,這些很多都潛藏隱患。另外在針對(duì)老項(xiàng)目做后續(xù)進(jìn)一步的穩(wěn)定性優(yōu)化、性能優(yōu)化以及研效提升的時(shí)候,語法問題都是要首先修復(fù)的,后續(xù)的工作才好開展。比如某一個(gè)變量,你作為常量引用傳給了函數(shù)F,后續(xù)某次需要需要并行調(diào)用函數(shù)F兩次,但其他入?yún)⒉煌?。如果這個(gè)常量引入傳入的變量在函數(shù)F中被修改了也能編譯通過在線上運(yùn)行了很久也沒出問題,那么你當(dāng)你誤以為函數(shù)F不會(huì)修改這個(gè)變量,然后改成并行調(diào)用兩次的時(shí)候,很可能會(huì)有coredump風(fēng)險(xiǎn)!
遵守代碼的規(guī)范是維護(hù)了一個(gè)每個(gè)代碼開發(fā)者的認(rèn)知協(xié)議,不規(guī)范的代碼破壞了這一協(xié)議,使得代碼變成屎山,如果不能對(duì)老代碼面面俱到則極易踩坑!就好比大家都認(rèn)為靠右行駛,但是突然迎面出現(xiàn)一個(gè)逆行,釀成車禍。
所以當(dāng)我在前司接手了一個(gè)老模塊之后做的第一件事就是刪掉了這個(gè)編譯參數(shù),然后不出意外收到了一堆編譯錯(cuò)誤……,在這些都改完之后,才開啟的后續(xù)深度的優(yōu)化之旅。
有些公司是mono repo的代碼倉(cāng)庫(kù)管理模式,即很多服務(wù)/模塊的代碼不是在單獨(dú)的git上管理,而是在同一個(gè)git上,通過不同的二三級(jí)目錄來存儲(chǔ)不同模塊的代碼。這時(shí)候整個(gè)項(xiàng)目可能會(huì)有統(tǒng)一的編譯配置,比如整個(gè)項(xiàng)目都有一個(gè)bazel配置,會(huì)全局生效,當(dāng)然每個(gè)子目錄中的BUILD中可以添加單獨(dú)的編譯參數(shù)。此時(shí)如果全局配置是開啟了-fpermissive的時(shí)候,我們可能是沒有權(quán)限修改的,但這也有辦法解決,那就是在我們自己的模塊目錄中的BUILD中加上-fno-permissive編譯參數(shù)!
-Werror=return-type
這個(gè)可以防止函數(shù)在聲明了返回值的時(shí)候,但函數(shù)內(nèi)卻漏了return的bug。如下代碼:
int foo(Bar* bar) {
...
if (x) {
return 0;
}
}
上面代碼明顯有問題,如果if沒命中,那么缺失其他的return。但默認(rèn)情況下這個(gè)能編譯通過。但是加了-Werror=return-type 之后就能讓編譯失敗。從而減少bug。
-Werror=maybe-uninitialized
這個(gè)用來減少變量未初始化的bug。好的編碼規(guī)范,都告訴我們基礎(chǔ)數(shù)據(jù)類型的局部變量要做初始化,否則是默認(rèn)值。但是同樣,人的仔細(xì)是靠不住的,再有經(jīng)驗(yàn)的程序員也有可能犯錯(cuò)。比如:
int foo(int a) {
int x;
if (a >= 10) {
x = 2;
} else if (a >= 0) {
x = 1;
}
...
這里的邏輯依賴x
}
將x的初始化放到了下面的條件分支中,但是如果條件分支遺漏了條件。比如上例中 a 未負(fù)數(shù),那么x的值將是不確定的。而 -Werror=maybe-uninitialized 可以發(fā)現(xiàn)這類錯(cuò)誤。
-Werror=type-limits
比較的數(shù)字超出int范圍,if永遠(yuǎn)false
int doc_type = doc->doc_type();
...
if (doc_type == 3808059108 || doc_type == 3856151248) {
is_fresh_doc = true;
}
doc_type聲明成了int類型,但是下面if中和doc_type比較是否相等的數(shù)字都超過了int范圍,所以永遠(yuǎn)不可能為true。
其實(shí)在DocInstance的proto定義中,doc_type是uint32的,int doc_type = doc→doc_type(); 這段代碼相當(dāng)于強(qiáng)轉(zhuǎn)了uint32的值,變成了int,導(dǎo)致這個(gè) if 永遠(yuǎn)不會(huì)為true,邏輯不對(duì)。
現(xiàn)在可以在編譯階段編譯失敗,然后發(fā)現(xiàn)該錯(cuò)誤。
-Werror=shadow
這個(gè)是防止變量的shadow,引發(fā)bug。比如:
class Config {
public:
void init(const std::string& filename);
string param;
};
void Config::init(const std::string& filename) {
string param;
... 解析配置文件,給 param 賦值。
}
在init函數(shù)內(nèi)定義了一個(gè)和成員 param 同名的變量(也是手誤),這個(gè)導(dǎo)致最終給局部變量param 賦值,并沒有給 成員 param 賦值。但這并不會(huì)造成編譯失敗,從而引發(fā)bug。
加上 -Werror=shadow ,就能在編譯期間報(bào)錯(cuò)。
利用C++語法
有一些看似平平無奇的語法,其實(shí)對(duì)于防范風(fēng)險(xiǎn)來說也是有奇效的。
override
先回顧一下C++的多態(tài),父類函數(shù)用virtual關(guān)鍵字修飾(稱之為虛函數(shù)),子類可以覆寫(覆蓋/重寫)父類的虛函數(shù),當(dāng)使用父類指針調(diào)用該函數(shù)的時(shí)候,如果對(duì)象是子類對(duì)象,那么會(huì)自動(dòng)調(diào)用子類的該函數(shù),而不是父類的。但是有時(shí)候因?yàn)槭终`,可能導(dǎo)致并沒有覆寫父類虛函數(shù)。從而出現(xiàn)邏輯錯(cuò)誤。
從C++11開始引入的override就能幫你在編譯期間做這個(gè)校驗(yàn),從而發(fā)現(xiàn)問題。
當(dāng)然不加override也是能覆寫父類函數(shù)的,這個(gè)override只是幫你做一下檢查而已,然而據(jù)我的經(jīng)驗(yàn),override至少可以減少如下三種不仔細(xì)導(dǎo)致的問題。
第一種:參數(shù)列表不一致(比如類型,或參數(shù)個(gè)數(shù)寫錯(cuò))
// a.h
class A {
public:
virtual void foo(long x, int y);
};
// b.h
class B: public A {
void foo(int x, int y) override; // 編譯失敗
};
// c.h
class C: public A {
void foo(long x) override; // 編譯失敗
};
第二種:函數(shù)名不一致
// strategy_base.h
class StrategyBase {
public:
virtual void run_strategy(int x);
};
// video_strategy.h
class VideoStrategy: public StrategyBase {
void run_stratgy(int x) override; // 編譯失敗
};
在函數(shù)名很長(zhǎng)的時(shí)候,子類的函數(shù)名容易寫錯(cuò),你以為覆寫了父類虛函數(shù),其實(shí)是自己新建了一個(gè)函數(shù)!
第三種:父類忘記加virtual
很多時(shí)候父類不是來自于標(biāo)準(zhǔn)庫(kù)或第三方庫(kù),而是也是我們代碼的一部分,那么漏寫virtual也很場(chǎng)景(比較Java里就沒有virtual關(guān)鍵字)
// a.h
class A {
public:
void foo(int x);
};
// b.h
class B: public A {
void foo(int x) override; // 編譯失敗
};
成員函數(shù)加上 const 聲明
比較基礎(chǔ)的C++語法,在不需要修改成員變量的成員函數(shù)上加上const聲明。為了這樣呢?很多人誤以為是可讀性,其實(shí)不盡然。假設(shè)有一個(gè)全局單利的管理類A:
class A {
public:
static A& inst() {
static A inst;
return inst;
}
// ...
bool hit(const std::string& key);
private:
A() {}
map<std::string, int> dict_;
};
在每個(gè)請(qǐng)求中需要判斷是否命中,比如:
if (A::inst().hit("new_exp")) {
...
}
如果實(shí)現(xiàn)成這樣:
bool A::hit(const std::string& key) {
return dict_[key] != 0;
}
會(huì)有問題,因?yàn)閙ap的operator [] 在key不存在的時(shí)候,會(huì)自動(dòng)在dict_中插入。但是在處理多個(gè)線程的過程中,并發(fā)的插入map會(huì)導(dǎo)致core dump。如果加上const就可以避免非預(yù)期的修改。
比如:
class A {
public:
static A& inst() {
static A inst;
return inst;
}
// ...
bool hit(const std::string& key) const;
private:
A() {}
map<std::string, int> dict_;
};
bool A::hit(const std::string& key) const {
return dict_[key] != 0;
}
會(huì)直接編譯失敗,這時(shí)候就會(huì)發(fā)現(xiàn)有并發(fā)修改map的風(fēng)險(xiǎn),應(yīng)改成:
bool A::hit(const std::string& key) const {
auto it = dict_.find(key);
if (dict_.end() == it) {
return false;
}
return it->second > 0;
}
考慮C++實(shí)現(xiàn)反射時(shí)的同名風(fēng)險(xiǎn)
C++原生是不支持反射的,但是通過宏和map可以實(shí)現(xiàn)通過字符串來創(chuàng)建對(duì)象,從而拿到模擬對(duì)象的功能。比如實(shí)現(xiàn)一個(gè)Node類的反射功能。
class Node;
...
class NodeFactory {
public:
static NodeFactory& instance() {
static NodeFactory inst;
return inst;
}
using create_node_fun_t = std::function<Node*()>;
void put(const std::string& name, create_node_fun_t&& fun) {
_node_create_fun_map.emplace(name, fun);
}
Node* create(const std::string& name) {
auto it = _node_create_fun_map.find(name);
if (it == _node_create_fun_map.end()) {
return nullptr;
}
auto&& create_node_fun = it->second;
return create_node_fun();
}
private:
std::unordered_map<std::string, create_node_fun_t> _node_create_fun_map;
};
#define REGISTE_NODE(class_name) \
struct RegisterNode##class_name { \
RegisterNode##class_name() { \
NodeFactory::instance().put(#class_name, [](){ \
auto node = new class_name; \
return node; \
}); \
} \
}; \
在定義了Node子類后,通過函數(shù)宏REGISTE_NODE進(jìn)行注冊(cè)。
// foo.h
class Foo: public Node {
public:
...
};
// foo.cpp
REGISTE_NODE(Foo)
...
然后通過字符串"Foo" 就創(chuàng)建出Foo的對(duì)象。 但是如果在項(xiàng)目龐大后,各種Node變多之后,可能在兩個(gè)文件中存在同名的Node! 比如在兩個(gè)文件中命名空間不同,但類名都叫Foo:
namespace A {
class Foo: public Node {
public:
...
};
} // namespace A
namespace B {
class Foo: public Node {
public:
...
};
} // namespace B
在各自的源文件中都使用REGISTE_NODE(Foo)進(jìn)行注冊(cè)。
但最終在NodeFactory的map中存放的“foo”指向是是哪個(gè)Foo類型其實(shí)是不確定的,這就會(huì)出現(xiàn)潛在的風(fēng)險(xiǎn)! 如何解決呢?這時(shí)候可以利用extern "C"來消除C++命名崩壞的功能來達(dá)到。重新實(shí)現(xiàn)REGISTE_NODE()這一函數(shù)宏。
#define REGISTE_NODE(class_name) \
struct RegisterNode##class_name { \
RegisterNode##class_name() { \
dipper::flow::NodeFactory::instance().put(#class_name, [](){ \
auto node = new class_name; \
return node; \
}); \
} \
}; \
extern "C" { \
int reg_node_##class_name; \
} \
在extern "C"中定義了一個(gè)全局變量,變量名包含REGISTE_NODE的參數(shù)class_name。這時(shí)候如果有在不同命名空間中出現(xiàn)了同名的類,進(jìn)行了REGISTE_NODE注冊(cè),那么在編譯的時(shí)候會(huì)因?yàn)槌霈F(xiàn)了同名的全局變量而導(dǎo)致編譯失?。∵@時(shí)候也就能在編譯期間發(fā)現(xiàn)問題了!
其他C++語法
其他C++關(guān)鍵字,比如 static_assert 、 final 也能在編譯期間實(shí)現(xiàn)一些檢查,讓不符合預(yù)期的使用方式,直接編譯失敗。
前面提到的方法,都是編譯時(shí)發(fā)現(xiàn)。當(dāng)然也并不是所有問題都能在編譯期間發(fā)現(xiàn)。
在服務(wù)器啟動(dòng)時(shí)發(fā)現(xiàn)
當(dāng)我們使用第公共組件的時(shí)候,一般都需要初始化。這期間如果遇到初始化失敗一定要拋異?;蛘哒{(diào)用exit()讓程序無法啟動(dòng),從而在服務(wù)部署階段就發(fā)現(xiàn)問題。而不是在請(qǐng)求開始處理的時(shí)候,還在使用初始化失敗公共組件,這時(shí)候可能導(dǎo)致線上服務(wù)或者業(yè)務(wù)邏輯的種種非預(yù)期問題。
當(dāng)我們對(duì)外提供組件庫(kù)的時(shí)候,也一定要提供初始化的函數(shù),并且明確返回是否初始化成功的狀態(tài)。
另外有一些外部組件雖然有初始化函數(shù),但是僅僅是做了一個(gè)簡(jiǎn)單的變量存儲(chǔ),并沒有進(jìn)行過多的初始化檢查,這時(shí)候就需要我們自己來在自己服務(wù)的初始化階段補(bǔ)齊相關(guān)的初始化能力。
假設(shè)有一個(gè)RPC client的管理類,維護(hù)了多個(gè)RPC的client。初始化的時(shí)候是傳入了一個(gè)配置文件,配置文件中維護(hù)了一個(gè)name到服務(wù)地址的映射。它的初始化函數(shù)僅僅是做了讀取文件解析配置,然后存儲(chǔ)了映射關(guān)系。但是并沒有檢查里面的服務(wù)地址(命名服務(wù)或者ip:port)是不是正確,這時(shí)候就需要我們自己進(jìn)行額外的檢查,比如讓每個(gè)client發(fā)起一次探測(cè)的rpc。因?yàn)榇_實(shí)可能存在服務(wù)的地址筆誤填錯(cuò)的可能。
服務(wù)運(yùn)行時(shí)
運(yùn)維友好
無監(jiān)控不系統(tǒng)
在開發(fā)階段考慮到關(guān)鍵日志的輸出、新增監(jiān)控與報(bào)警。打印關(guān)鍵信息。
std::unique_ptr防止內(nèi)存泄露
在C++代碼中應(yīng)該避免實(shí)現(xiàn)顯式地使用new和delete。但是有時(shí)候代碼中仍然可能無法避免。比如brpc的bthread_start_background()函數(shù)函數(shù)需要接收一個(gè)void*類型的指針用來傳參,這時(shí)候一般是在外部使用new來創(chuàng)建一個(gè)參數(shù)對(duì)象,然后在回調(diào)函數(shù)中進(jìn)行delete。
void* callback(void* arg) {
Args* args = (Args*)arg;
...
delete arg;
return nullptr;
}
void foo() {
Args * args = new Args;
// args中的成員賦值
bthread_start_background(callback, (void*)args);
}
但是如果callback中有多處return,很難保證能夠在每次return之前都能進(jìn)行delete。這時(shí)候就可以利用std::unique_ptr。
void* callback(void* arg) {
Args* args = (Args*)arg;
std::unique_ptr<Args> up(args);
...
if (...) {
return nullptr; // 漏了delete
}
return nullptr;
}
出問題方便排查
比如代碼中使用noexcept關(guān)鍵字,當(dāng)遇到C++異常導(dǎo)致的coredump的時(shí)候,通過更加精準(zhǔn)的棧信息,來快速定位。詳情可以閱讀 《一劍破萬法:noexcept與C++異常導(dǎo)致的coredump》
清單,對(duì)抗新舊需求之間風(fēng)險(xiǎn)
前面提到的經(jīng)驗(yàn)和方法都是編程語言、后臺(tái)服務(wù)設(shè)計(jì)方面比較純粹技術(shù)。但實(shí)際工作中還有很多導(dǎo)致線上事故或者導(dǎo)致二次開發(fā),工作返工的事情是在實(shí)現(xiàn)產(chǎn)品或策略需求的過程中,對(duì)需求分析不到位,或者遺漏了本次需求與歷史需求的沖突點(diǎn),導(dǎo)致邊界情況無法自測(cè)到而導(dǎo)致的。
隨著時(shí)間的推移,需求開發(fā)的越來越多。每次新的產(chǎn)品需求、策略需求都需要考慮到是否要同時(shí)修改一些已有的流程或邏輯。雖然也會(huì)做測(cè)試,但難免會(huì)遺漏一些冷門的情況。稍有遺漏就會(huì)造成新的需求在某些偶條件下不符合預(yù)期,有時(shí)候會(huì)引發(fā)線上大面積的用戶體驗(yàn)case以及模塊穩(wěn)定性故障。
這時(shí)候該怎么辦呢?其實(shí)沒有高明的方法。我個(gè)人的經(jīng)驗(yàn)是使用清單,也就是Checklists。
有一本書《清單革命》講述了使用清單如何重塑了醫(yī)療、航空、建筑以及投資領(lǐng)域。
這本書中有一句話說的不錯(cuò):
“無知之錯(cuò)”可被原諒,“無能之錯(cuò)”不被原諒。
這里的“無知之錯(cuò)”指的是因?yàn)樽陨砑夹g(shù)、知識(shí)水平不夠?qū)е碌腻e(cuò)誤,這種通常是能被原諒的?!盁o能之錯(cuò)”(感覺翻譯的不夠好)并不是因?yàn)榧夹g(shù)、知識(shí)水平不夠,而是因?yàn)轳R虎大意導(dǎo)致本不該犯錯(cuò)的地方犯了錯(cuò),這種錯(cuò)誤不能被原諒!
其實(shí)前面介紹的編譯期、啟動(dòng)時(shí)、運(yùn)行時(shí)的經(jīng)驗(yàn)方法,也是在利用工具來避免“無能之錯(cuò)”。而對(duì)于需求,對(duì)于業(yè)務(wù)邏輯僅僅這些還有些欠缺。
受此啟發(fā),我認(rèn)為也需要維護(hù)一個(gè)需求分析的清單。比如:
檢查項(xiàng) | |
XXX是否受影響 | |
YYY是否受影響 | |
是不是要AAA | |
是不是要BBB |
在接到一個(gè)產(chǎn)品需求或者策略需求之后,對(duì)照清單來逐一核對(duì)。當(dāng)然清單不能又臭又長(zhǎng),需要簡(jiǎn)明扼要,不要妄圖做到大而全,這樣反而可能使得檢查變得流于形式,變成無腦操作,更重要的是通過清單中的檢查項(xiàng)建立一個(gè)需求分析的框架,在接到需求之后進(jìn)行一系列思考分析,最終實(shí)際的檢查和思考其實(shí)可能會(huì)超越清單本身覆蓋的內(nèi)容。
另外清單中的檢查項(xiàng)也是要經(jīng)常更新的,要加入新的或者刪去舊的。并且也可以有多種不同的清單,應(yīng)對(duì)不同類型的需求。
名不出于家!職場(chǎng)怪現(xiàn)象
魏文王問扁鵲曰:「子昆弟三人其孰最善為醫(yī)?」扁鵲曰:「長(zhǎng)兄最善,中兄次之,扁鵲最為下?!刮何暮钤唬骸缚傻寐勑埃俊贡怡o曰:「長(zhǎng)兄於病視神,未有形而除之,故名不出於家。中兄治病,其在毫毛,故名不出於閭。若扁鵲者,鑱(chán)血脈,投毒藥,副肌膚,閑而名出聞於諸侯。
魏文王曾詢問扁鵲,他們兄弟三人誰的醫(yī)術(shù)最高明。扁鵲回答說大哥醫(yī)術(shù)最高,二哥其次,自己居末。但若論名氣卻正好相反,因?yàn)榇蟾缭诓∪瞬∏榘l(fā)作之前就加以防范,他的醫(yī)術(shù)只被家人知道,鮮有人知。二哥在病情剛剛顯露的時(shí)候進(jìn)行治療,在家鄉(xiāng)內(nèi)聞名。而自己治療的疾病已經(jīng)在病情末期,需要用手術(shù)開刀并且使用猛烈的藥物來治療,但也因此在聞名于諸侯。
這個(gè)故事和中醫(yī)名著《黃帝內(nèi)經(jīng)》有共同之處,《黃帝內(nèi)經(jīng)》有云:
上醫(yī)治未病 中醫(yī)治欲病 下醫(yī)治已病
即醫(yī)術(shù)最高明的醫(yī)生能夠預(yù)防疾病的人。
且不討論中醫(yī)學(xué)說的科學(xué)性,也不糾結(jié)“醫(yī)術(shù)高超”該如何定義。這則小故事在職場(chǎng)中卻也常常被言中。比如A程序員善于防微杜漸,所負(fù)責(zé)的業(yè)務(wù)有很多風(fēng)險(xiǎn)都早早根除,減少了很多出現(xiàn)線上事故的風(fēng)險(xiǎn)。而B程序員并不精于此道,所轄業(yè)務(wù)事故不斷。但久而久之在領(lǐng)導(dǎo)眼中卻是另一番觀感:A程序員負(fù)責(zé)的業(yè)務(wù)簡(jiǎn)單,缺乏挑戰(zhàn)。若程序員A偶有事故會(huì)被抓住不放,時(shí)常拿來說事。領(lǐng)導(dǎo)只會(huì)看到半年出了一次事故,卻不知道在日益龐大的系統(tǒng)規(guī)模下預(yù)防了多少個(gè)事故;而在領(lǐng)導(dǎo)眼中B程序員負(fù)責(zé)業(yè)務(wù)難度大,有挑戰(zhàn),事故發(fā)生時(shí)能做到熬夜通宵排查解決,如此負(fù)責(zé)任,有擔(dān)當(dāng),不辭辛苦,兢兢業(yè)業(yè),值得嘉獎(jiǎng)。另外會(huì)對(duì)該業(yè)務(wù)重點(diǎn)關(guān)注,接著立專項(xiàng)、搞封閉,日會(huì)周會(huì)不斷,略有改觀便是莫大進(jìn)步。
停下來想象一下,相信很多人也會(huì)有類似感受:在事故發(fā)生時(shí)出現(xiàn)的救火隊(duì)長(zhǎng)是“鑱血脈,投毒藥,副肌膚“的英雄。卻記不得有哪些是“湯熨療疾”、“未有形而除之”的平凡人。
《孫子兵法》亦有云:
善戰(zhàn)者無赫赫之功。