软件设计中的可调试性

本文探讨了软件设计中的可调试性,强调从设计阶段就开始考虑如何降低调试门槛。主要内容包括良好的边界设计、可观察性(如有效日志和系统状态可视化)以及可在线调试,以提升软件质量并简化问题定位。
摘要由CSDN通过智能技术生成

软件调试是我们学习软件开发的第一课,开发往往大部分的时间不是在写代码,而是在查 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、某容器管理平台提供工具快速查看整个集群核心组件运行状态

整个容器平台,是分布式部署的,本身涉及到的组件和状态非常多,如果出了问题逐一排查,是比较麻烦的事情,通过下面的工具, 可以将整个分布式系统的大部分组件状态,快速透出来,从而简单系统调试。

root@node-005056b8493c:/# 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,未经授权不得转载,转载需与GitChat联系。

阅读全文: http://gitbook.cn/gitchat/activity/5ac5a4a529fecb35827adaf3

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值