1. 程式人生 > >軟體設計中的可除錯性

軟體設計中的可除錯性

軟體除錯是我們學習軟體開發的第一課,開發往往大部分的時間不是在寫程式碼,而是在查 Bug,相信大家也深有體會。我們有很多手段可以除錯問題,除錯最常用的手段包括打日誌、GDB、分析堆疊、跟蹤系統呼叫等等。但要怎麼樣才能從設計開始就考慮降低除錯門檻,當我們的程式碼出現問題時能快速定位到問題呢?

本場 Chat 您將學到如下內容:

  • 瞭解如何通過設計的手段降低除錯門檻;
  • 什麼樣的程式碼比較易於除錯問題;
  • 出現問題怎麼儲存現場;
  • 怎麼分析和除錯問題。

軟體除錯是我們學習軟體開發的第一課,開發往往大部分的時間不是在寫程式碼,而是在查 Bug,相信大家也深有體會。

我們有很多手段可以除錯問題,除錯最常用的手段包括打日誌、GDB、分析堆疊、跟蹤系統呼叫等等。

但要怎麼樣才能從設計開始就考慮降低除錯門檻,當我們的程式碼出現問題時能快速定位到問題呢?

本場 Chat 您將學到如下內容:

  • 瞭解如何通過設計的手段降低除錯門檻;
  • 什麼樣的程式碼比較易於除錯問題;
  • 出現問題怎麼儲存現場,怎麼分析和除錯問題;

由於軟體除錯本身是一門很複雜的技術,而且每個領域各不相同,不同方向上具體方法差別很大,所以本文不打算描述具體的除錯方法,比如怎麼用 gdb、怎麼分析 core、怎麼查記憶體洩漏等。

本文會重點放在描述如何通過在設計階段就考慮軟體除錯性,即如何通過設計上的提前考慮儘可能地降低後期的除錯門檻,提升軟體的整體質量。

期望本文能帶給大家一些思考,但由於個人知識面有限,寫作時間也有限,考慮到的問題可能不夠完善,歡迎大家指正和探討。

本文目錄

什麼是可除錯性

關於軟體開發中的可除錯性,每個人都有不同的看法,通常大家會覺得就是方便查 bug,當然這個想法是沒有問題的, 但過於籠統。

為了便於描述,我在這裡先下個不太準確的定義。

這裡講的軟體可除錯性,主要包含兩部分:

  1. 程式碼編碼完成後,能快速驗證是否達到預期結果
  2. 當結果與預期不一致時,能快速定位到問題原因

關於第一點,這裡說的能快速驗證是否達到預期結果,大家都覺得比較簡單,但實際上並不容易,特別是大型軟體開發的中,驗證成本是很高的,比如改一行程式碼,有可能需要對整個專案重新編譯、需要準備測試環境、需要執行各種測試案例等等。面且還不一定靠譜,因為這裡的驗證指的是對各種輸入的驗證,包括各類正常的和異常的輸入,大部分情況下,如果我們只是對功能做驗證,是比較難保證完全可靠的。這時就要考慮我們程式碼是否有設計良好的邊界,比如模組與模組之間是否強耦合,單個模組是否可以很方便地做測試等。

關於第二點,就涉及到具體的技術問題了,當出現問題時,我們會分析一下結果和程式碼,有經驗的程式設計師都能快速定位到問題。現代軟體開發中,很多時候由於框架層面已經為我們考慮了很多事情,我們很容易就能拿到模組相關的日誌、狀態等資訊,從而快速定位到問題點。少數情況下我們需要開啟 IDE 稍加除錯,或斷個點。 但如果我們嘗試考慮軟體整體上的可除錯性時,問題就會變得相對複雜很多。比如什麼情況下該打日誌,怎麼打?怎麼將系統的狀態透出來?如果 bug 不可重現,我們該怎麼除錯?

為什麼需要考慮可除錯性

很多人對於除錯的第一反應是,出現 bug 就除錯一下,除錯那是 bug 出現之後的事情。

甚至很多人覺得,除錯只能在開發過程中通過 IDE 來做,如果沒有 IDE 或者開發環境除錯就很難進行。

當然這是不對的,除錯可以發生在軟體生命週期的各個階段,而能不能從容應對,就考驗設計者對可除錯性的考慮。

軟體設計是一門很複雜的手藝,使用者可見的需求只是冰山上的一角,軟體設計者在整個設計過程中, 需要考慮到所有利益方的訴求。比如對於編碼者,如何快速編碼。對於測試人員,如何方便測試。對於運維方,如何開心地運維。

而可除錯性,又涉及到多個利益相關方,並直接影響軟體的整體設計,是非常重要的一環。

從系統層面上分析,我們會發現,可除錯性做的很差的程式碼,往往質量也會很差,並且開發效率也不會太高。

試想如果每寫一段程式碼都需要經過很繁瑣的驗證,每發現一個問題,都需要發很長時間去定位,那開發者想必也是很崩潰的。

由於對預期結果難於驗證、問題難於定位,往往就容易偷懶,自測做不到位,結果就會容易導致質量下降,質量下降就會導致後期更多的問題,從而陷入開發困局。

可除錯性這塊,設計初期就應該想考慮清楚怎麼去做。一般來說可除錯性很好的軟體必然是一個強內聚、弱耦合、介面明確、意圖明晰的軟體,而可除錯性差的的軟體往往具有過強的耦合和混亂的邏輯。

所以可除錯性的好壞,從某些方面來講又直接代表了一個軟體的好壞,那當然是值得我們去提前考慮的事情。

怎麼設計提高可除錯性

對於軟體的設計者而言,關於可除錯性的考慮,我認為至少需要包含幾個方面:

  1. 良好的邊界
  2. 易於測試
  3. 易於理解
  4. 可觀察性

良好的邊界

這裡說的邊界,指的是軟體中模組與模組間、類與類間、程式碼與程式碼間的邊界。

一段好的程式碼,需要保證邊界清晰,職責分明,否則就會變得很難維護,一旦出現問題,就會因為範圍太大而除錯起來耗時耗力。

模組與模組之間耦合性太大, 不僅會影響擴充套件性、複用性、維護性等,還會影響單元測試、整合測試;我們在專案中會聽到” 我的模組依賴太多單測沒法做”,這種問題歸根結底還是設計問題,耦合性太強導致。

函式與函式之間強繫結, 單測時為了測一個函式就不得不對另外一堆模組打樁;模組與模組之間強繫結, 聯調測試一個模組時就不得不對另外一堆模組打樁;良好的邊界也是軟體易於測試和易於理解的基礎,下面以 Linux 為例講述為什麼良好的邊界如此重要。

Linux 系統本身的設計是很值得學習的,如下圖展示的 Linux 層次結構圖:

enter image description here

Linux 在分層和解耦上,都做的非常出色,各子系統間介面非常清晰,這既讓整個系統易於理解、又讓整個系統易於除錯和測試。

實際上 Linux 系統內部是非常複雜的,下圖展示的 Linux 詳細的內部實現:

enter image description here

大部分讀者看到這張圖都有點眼花了,但這麼複雜的實現,Linux 在設計上仍然能做到易於理解和維護,這很大一部分原因在於,Linux 為每個部件設定了非常清晰的邊界,並給出了明確的介面。所以我們在設計系統時也應該儘可能做到邊界清晰。

可觀察性

留下有效日誌

日誌是程式開發中最常見的除錯方法,也可以說是最有用的手段。Linux 之父 Linus Torvalds 就說過,他從不用任何 Debugger 工具。

本文也不打算介紹任何 Debugger 工具,但是打算重點聊一下日誌。

雖然日誌是大家都會用的一個除錯手段,但並沒有多少人能打的很好。打日誌是很有講究的一個事情,日誌的內容需要交代清楚: 時間、地點 (程式位置)、物件、關鍵因素,如果列印所在的函式有多個呼叫點,最好交代清楚呼叫棧。

例如列印函式呼叫失敗:

  • 初級打法: call function fail

  • 有效打法: 201809110800 [test.c:100] new conn src ip:port :%u.u.u.u:%d dst ip:port %u.u.u.u:u alloc memory fail size=%d

有效日誌應該包含以下特徵:

  1. 日誌分級, 不能所有級別的日誌混成一團, 出了問題時應該能快速定位到所需日誌。
  2. 日誌不能打太頻繁,需要考慮限速。
  3. 在關鍵位置需要留下日誌記錄,比如可能出錯的地方。
  4. 有執行時開關,可動態調整日誌輸出。

下在舉幾個日誌的例子:

1、下面是一段儲存軟體的日誌

這段日誌詳細地記錄了所有會修改到磁碟的操作,並單獨清晰地記錄下來,這樣一旦發現儲存資料出現問題,只需要通過分析日誌就一定能查到問題點,找出是什麼時間哪個操作導致的資料問題。

[2017-02-22 14:23:42.053233]  : mkdir:31824, path: /images/cluster                                [2017-02-22 14:23:42.141262]  : mkdir:31824, path: /images/cluster/vst_tmp1.vm                 [2017-02-22 14:23:42.255441]  : create:31833, path: /images/cluster/vst_tmp1.vm/vm-disk-1.qcow2    [2017-02-22 14:23:44.156812]  : create:32213, path: /images/cluster/vst_tmp1.vm/3436121041554.conf[2017-02-22 14:23:47.581945]  : mkdir:32355, path: /images/cluster/vst_tmp2.vm                     [2017-02-22 14:23:53.802746]  : mkdir:582, path: /images/cluster/vst_tmp3.vm                    [2017-02-22 14:23:53.972376]  : create:600, path: /images/cluster/vst_tmp3.vm/vm-disk-1.qcow2      [2017-02-22 14:23:56.344781]  : create:994, path: /images/cluster/vst_tmp3.vm/3875145219722.conf[2017-02-22 14:23:58.745449]  : mkdir:1104, path: /images/cluster/vst_tmp4.vm                      [2017-02-22 14:24:08.479255]  : mkdir:4879, path: /images/cluster/vst_tmp5.vm                  [2017-02-22 14:24:08.672113]  : create:5005, path: /images/cluster/vst_tmp5.vm/vm

2、某容器產品日誌

該日誌記錄配置的更改過程,將變更前後的對比一併輸出,這樣後續如果發現配置出現問題,就可以通過分析日誌打到哪個時間點導致配置出現了問題。

如下日誌可以清楚地看到變化的資料,版本號從 4 到 5,update 時間修改,members 修改,這種日誌一條足夠了然。

2017-05-31 14:28:27.005945 info [sfvt_docker-status] 28994 cluster:393 change cluster from {id:'cluster-1aa5324f231e' version:4 update:'05-31 14:27:39' master:0/8527115551848 members:3 [1013873187874,3510809305044,8527115551848] to {id:'cluster-1aa5324f231e' version:5 update:'05-31 14:28:27' master:0/8527115551848 members:4 [1013873187874,3510809305044,7616676694300,8527115551848] 
系統狀態可視

這裡講的系統狀態可視,指的是系統設計中,考慮能夠將部分核心狀態透出來。從而能在執行過程中,直接檢視系統的執行狀態,方便動態跟蹤軟體情況。

1、比如下面的 Golang 內建的 pprof 機制

enter image description here

從上圖可以看到所有 Goroutine 的執行狀態, Goroutine 是 Golang 比較重要的概念,並且容易出錯和不方便除錯。但有了 pprof,事情就變得簡單了很多。

2、某容器管理平臺提供工具快速檢視整個叢集核心元件執行狀態

整個容器平臺,是分散式部署的,本身涉及到的元件和狀態非常多,如果出了問題逐一排查,是比較麻煩的事情,通過下面的工具, 可以將整個分散式系統的大部分元件狀態,快速透出來,從而簡單系統除錯。

[email protected]:/# sxfdcloudctl status[hosts]192.168.139.233 node-005056b80c59192.168.139.232 node-005056b8493c192.168.139.231 node-005056b86c82192.168.139.234 node-005056b86ee6[haproxy]......[majorconfig]......[etcd]member 27fbacb64f7266ce is healthy: got healthy result from https://node-005056b80c59:12379member 71fd66d8f282f9cd is healthy: got healthy result from https://node-005056b86ee6:12379member 864d4384881ea091 is healthy: got healthy result from https://node-005056b86c82:12379cluster is healthy[majors]{  "etcd": {    "v3": {      "name": "node-005056b86ee6",      "initialCluster": "node-005056b86ee6=https://node-005056b86ee6:2380",      "initialClusterState": "existing",      "initialClusterToken": "cluster-1e92bbce"    }  },  "majorPodName": "kube-major-v1-x4h7c"}.....[node]NAME                STATUS    AGE       LABELSnode-005056b80c59   Ready     21d       kontroller.sxfdcloud.io/ElasticsearchNode=true,kontroller.sxfdcloud.io/MajorCandidate=true,kontroller.sxfdcloud.io/Name=node-005056b80c59node-005056b8493c   Ready     21d       kontroller.sxfdcloud.io/ElasticsearchNode=truenode-005056b86c82   Ready     21d       kontroller.sxfdcloud.io/MajorCandidate=true,kontroller.sxfdcloud.io/Name=node-005056b86c82node-005056b86ee6   Ready     21d       kontroller.sxfdcloud.io/ElasticsearchNode=true,kontroller.sxfdcloud.io/MajorCandidate=true,kontroller.sxfdcloud.io/Name=node-005056b86ee6[kube-base]NAME                                        READY     STATUS             RESTARTS   AGEetcd-node-005056b80c59                      1/1       Running            0          8hetcd-node-005056b86c82                      1/1       Running            0          1hetcd-node-005056b86ee6                      1/1       Running            0          8hkube-apiserver-node-005056b80c59            1/1       Running            0          8hkube-apiserver-node-005056b86c82            1/1       Running            0          1hkube-apiserver-node-005056b86ee6            1/1       Running            1          8hkube-scheduler-node-005056b80c59            1/1       Running            0          8hkube-scheduler-node-005056b86c82            1/1       Running            0          1hkube-scheduler-node-005056b86ee6            1/1       Running            0          8h.....[kube-system]NAME                                 READY     STATUS              RESTARTS   AGEheapster-v1-lhxdk                    1/1       Running             6          1dinfluxdb-v1-dq0jt                    1/1       Running             6          1dkube-dns-v1-lrvqr                    1/1       Running             1          10hkube-kingress-v1-13kjd               1/1       Running             20         6dkube-kingress-v1-3m3wr               1/1       Running             15         6dkube-kingress-v1-97hqx               1/1       Running             18         6dkube-kingress-v1-c74vc               1/1       Running             17         6dkube-major-v1-5ln7p                  1/1       Running             0          1hkube-major-v1-rtvsp                  1/1       Running             0          8hkube-major-v1-x4h7c                  1/1       Running             0          8h......

3、 通過 SIGUSR1 輸出內部狀態

和執行時開關類似,如果不想中斷程式,檢視內部狀態,就需要有執行時檢視的介面,比如通過 SIGUSR1 可以輸出內部狀態。

下面的機制,通過工具直接檢視程式的內部狀態,輸出三個節點的狀態資訊:

Sangfor:aCloud/node-332 /usr/lib/python2.7/site-packages/mmon # python mon.py -c Remote=Status --param1=all --remoteNode=39.39.0.639.39.0.5:       start: 01-21 10:21:39         end: 01-21 10:26:18      nodeIP: 39.39.0.5         pid: 53311       count: 8      status: OK         pom: 011     vipNode: 39.39.0.6  nodesCount: 3    masterIP: 39.39.0.6     onlines: 39.39.0.5,39.39.0.6,39.39.0.8        desc: SLAVE OK, Behind master(1s) SQL delay(0s)39.39.0.6:       start: 01-21 10:23:38         end: 01-21 10:26:09      nodeIP: 39.39.0.6         pid: 8611       count: 3      status: OK         pom: 111     vipNode: 39.39.0.6  nodesCount: 3    masterIP: 39.39.0.6     onlines: 39.39.0.5,39.39.0.6,39.39.0.8        desc: master is ok and in sync mode39.39.0.8:get status failed: connect to (39.39.0.8:54213) failed [Errno 111] Connection refused

可線上除錯

上節講的是在執行過程中,將系統狀態直接展示出來,這樣除錯時就可以有更多的系統資訊,從而在不通過 Debugger 的情況下,也能輕鬆除錯。

但有時候,光看狀態還不夠,需要能線上修改程式狀態, 提供一些直接修改系統內部狀態的手段,比如通過訊號、套接字等。

機制上和上節的內容相似,可以參考 Linux 的 proc 等機制,這裡不做詳細展開。

總結

本文主要跟大家講述了什麼是可除錯性以及如何提高系統的可除錯性,歡迎大家積極討論。

一場場看太麻煩?成為 GitChat 會員,暢享 1000+ 場 Chat !點選檢視

相關推薦

no