作者 | 梁唐
出品 | 公众号:Coder梁(ID:Coder_LT)
大家好,日拱一卒,我是梁唐。
今天我们继续麻省理工missing smester课程——消失的课程,那些不会在课堂上讲授的重要技能。
这节课的内容有两块,一块是debug相关的技巧,另外一个部分是对系统的性能分析。由于整节课内容非常多,篇幅很长,所以分成了两期,用两篇文章写完。今天我们先来看看debug技巧的部分,对于每个程序员来说,debug一定是不可避免的,所以花点时间了解一下debug技巧非常非常有必要,能够大大提升之后开发过程的效率,降低寻找bug的痛苦。
B站视频链接:https://www.bilibili.com/video/BV1x7411H7wa?p=7
和之前一样,这节课的note质量同样非常高。大家可以点击「阅读原文」跳转英文原文。
本文是基于本节课note以及老师上课演示的内容,还有我个人的一些理解做的翻译整理版本。日拱一卒,欢迎大家打卡一起学习。
前言
编程当中有一个铁律:代码不是像你设想的那样运行的,而是像你告诉它的那样。有时候填平设想和实际的鸿沟是非常艰难的。在这节课当中,我们将会涵盖一些有用的技术来处理bug以及资源管理。
Debugging
打印调试日志
最高效的debug工具就是缜密的思考配合恰当的输出语句——Brain Kernighan,Unix for Beginners.
第一个debug程序的方式就是在你觉得可能出问题的地方加入一些print语句,持续迭代直到你搜集了足够多的信息了解到底是什么导致了问题。
第二个方案是在你的程序当中使用日志,而不是临时添加print语句。相比于简单的print语句,日志拥有以下优势:
- 可以将日志写入文件、socket 或者甚至是发送到远端服务器而不仅仅是标准输出;
- 日志可以支持严重等级(例如 INFO, DEBUG, WARN, ERROR等),这使您可以根据需要过滤日志;
- 对于新发现的问题,很可能您的日志中已经包含了可以帮助您定位问题的足够的信息。
下面这段python代码是一个使用log的例子:
代码语言:javascript复制import logging
import sys
class CustomFormatter(logging.Formatter):
"""Logging Formatter to add colors and count warning / errors"""
grey = "x1b[38;21m"
yellow = "x1b[33;21m"
red = "x1b[31;21m"
bold_red = "x1b[31;1m"
reset = "x1b[0m"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
FORMATS = {
logging.DEBUG: grey format reset,
logging.INFO: grey format reset,
logging.WARNING: yellow format reset,
logging.ERROR: red format reset,
logging.CRITICAL: bold_red format reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
# create logger with 'spam_application'
logger = logging.getLogger("Sample")
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
if len(sys.argv)> 1:
if sys.argv[1] == 'log':
ch.setFormatter(logging.Formatter('%(asctime)s : %(levelname)s : %(name)s : %(message)s'))
elif sys.argv[1] == 'color':
ch.setFormatter(CustomFormatter())
if len(sys.argv) > 2:
logger.setLevel(logging.__getattribute__(sys.argv[2]))
else:
logger.setLevel(logging.DEBUG)
logger.addHandler(ch)
# logger.debug("debug message")
# logger.info("info message")
# logger.warning("warning message")
# logger.error("error message")
# logger.critical("critical message")
import random
import time
for _ in range(100):
i = random.randint(0, 10)
if i <= 4:
logger.info("Value is {} - Everything is fine".format(i))
elif i <= 6:
logger.warning("Value is {} - System is getting hot".format(i))
elif i <= 8:
logger.error("Value is {} - Dangerous region".format(i))
else:
logger.critical("Maximum value reached")
time.sleep(0.3)
一个我最喜欢的记录日志的技巧是对日志进行上色。你可能已经发现了你的终端使用一些高亮颜色让内容变得更加易读。但它是怎么实现的呢?像是grep这样的程序使用了ANSI escape codes:https://en.wikipedia.org/wiki/ANSI_escape_code,它是一系列特殊的字符,可以让你的shell改变输出结果的颜色。
比如运行echo -e "e[38;2;255;0;0mThis is rede[0m"
会在终端打印出红色的字符串This is red
。
img
前提是你的终端支持true color,如果不支持的话(比如macOS的terminal就不支持),你可以使用更加通用的16色,比如echo -e "e[31;1mThis is rede[0m"
接下来的脚本演示了如何在终端当中输出多种RGB颜色(同样这需要支持true color)
代码语言:javascript复制#!/usr/bin/env bash
for R in $(seq 0 20 255); do
for G in $(seq 0 20 255); do
for B in $(seq 0 20 255); do
printf "e[38;2;${R};${G};${B}m█e[0m";
done
done
done
第三方log
当你开始创建大规模的软件系统的时候,你可能会使用一些依赖,这些依赖可能会独立运行。网页服务器、数据库以及消息代理都是常见的第三方依赖。当和这些系统交互的时候,不可避免地会需要阅读它们的log,因为仅仅靠客户端的错误信息不足以定位问题。
庆幸的是,大多数程序都会把它们的日志记录在你的系统的某处。在UNIX系统当中,程序通常会把它们的日志写在/var/log
当中。比如NGINX服务器将它的日志写在/var/log/nginx
。
目前系统开始使用系统日志,你所有的日志消息都会存在这里。大多数(不是全部)Linux系统使用systemd
,这是一个系统守护进程,控制你系统当中的许多东西,比如一些服务的启动和运行。systemd
将特殊格式的日志存放在/var/log/journal
中,你可以使用journalctl
命令来展示这些消息。类似的,在macOS上仍然有/var/log/system.log
,但越来越多的工具开始使用系统日志,这些日志可以通过log show
展示。对于大多数UNIX系统来说,你可以使用dmesg
命令来访问内核日志。
你可以使用logger
shell程序来记录系统日志,下面是一个使用logger
记录日志系统日志,以及进行查询的例子。并且,大多数编程语言都支持向系统日志当中记录日志。
logger "Hello Logs"
# On macOS
log show --last 1m | grep Hello
# On Linux
journalctl --since "1m ago" | grep Hello
就像是我们在数据处理那节课上看到的一样,日志通常非常冗长,并且需要进行一定程度的处理和过滤才能提取出有用的信息。如果你发现通过jounrna
和log show
进行过滤非常麻烦,你可以试试使用它们的flag,可以先对结果进行一波过滤。同样也有类似于lnav
这样的工具,为日志提供了更好的浏览和导航。
Debuggers
当我们打印debug信息已经不足以找到问题的时候,你可以考虑使用debugger(调试器)。调试器是一些允许你可以交互式执行程序的工具,它允许你进行以下的操作:
- 当到达某一行时将程序暂停;
- 一次一条指令地逐步执行程序;
- 程序崩溃后查看变量的值;
- 满足特定条件时暂停程序;
- 其他高级功能。
许多编程语言支持调试器,在Python当中,调试器是pdb
下面是对pdb
支持的命令的一些简单介绍:
- l(ist) - 显示当前行附近的11行或继续执行之前的显示;
- s(tep) - 执行当前行,并在第一个可能的地方停止;
- n(ext) - 继续执行直到当前函数的下一条语句或者 return 语句;
- b(reak) - 设置断点(基于传入的参数);
- p(rint) - 在当前上下文对表达式求值并打印结果。还有一个命令是pp ,它使用 pprint 打印;
- r(eturn) - 继续执行直到当前函数返回;
- q(uit) - 退出调试器。
让我们使用pdb
来修复下面的Python代码(参考讲课视频)
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(n):
if arr[j] > arr[j 1]:
arr[j] = arr[j 1]
arr[j 1] = arr[j]
return arr
print(bubble_sort([4, 2, 1, 8, 7, 6]))
注意,由于Python是解释型语言,我们可以使用pdb
shell来执行命令。ipdb
是pdb
的一个改进版本,使用IPython
并作为REPL开启了tab不全、语法高亮、更好的回溯以及更好的检查,同时还保持了和pdb
模块相同的接口。
对于一些更底层的语言,你可以使用gdb
(它的改进版pwndbg
)和lldb
。它们都对C-like语言调试做了优化,允许你探索任意进程以及获得当前机器的状态:寄存器、栈、程序计数器等等。
特定工具
甚至当你在一个黑盒二进制文件中debug的时候,都有特定的工具可以帮到你。程序执行某些特定操作的时候必须要通过操作系统内核,这需要用到system call。有一些命令可以让你追踪你程序执行的system call。在Linux当中叫做strace
在macOS和BSD当中有dtrace
。
dtrace
用起来可能比较别扭,因为它使用它自有的D语言,但也有封装好的叫做dtruss
的工具,提供和strace
相似的接口。
下面是使用strace
或者dtruss
来展示执行ls
时,对stat system call的调用结果。如果想要深度了解strace
,可以阅读这两篇文章:https://blogs.oracle.com/linux/post/strace-the-sysadmins-microscope,https://jvns.ca/strace-zine-unfolded.pdf
# On Linux
sudo strace -e lstat ls -l > /dev/null
4
# On macOS
sudo dtruss -t lstat64_extended ls -l > /dev/null
对于 web 开发, Chrome/Firefox 的开发者工具非常方便,功能也很强大:
- 源码 -查看任意站点的 HTML/CSS/JS 源码;
- 实时地修改 HTML, CSS, JS 代码 - 修改网站的内容、样式和行为用于测试(从这一点您也能看出来,网页截图是不可靠的);
- Javascript shell - 在 JS REPL中执行命令;
- 网络 - 分析请求的时间线;
- 存储 - 查看 Cookies 和本地应用存储。
静态分析
对于一些问题,你不需要执行代码就可以发现。比如说,仔细观察一段代码,你就能发现你的循环变量覆盖了一个已有的变量名或函数名。或者是有一个变量在读取之前没有被定义。这种情况下静态分析工具就可以派上用场了。静态分析工具将源代码作为输入,基于编程规则对它进行分析,找出其中的问题。
在下面这个Python代码片段当中存在一些错误。首先,我们的循环变量foo覆盖了先前定义的函数foo。我们同样把最后一行的变量bar写成了baz,所以程序完成sleep之后会崩溃。
代码语言:javascript复制import time
def foo():
return 42
for foo in range(5):
print(foo)
bar = 1
bar *= 0.2
time.sleep(60)
print(baz)
静态分析工具可以定位这类问题。当我们运行pyflakes
之后,我们可以得到一些关于bug的错误提醒。mypy
是另外一个工具,可以帮助我们检查类型问题。这里mypy
将会警告我们bar
这个变量一开始是int类型,但后来被强制转换成了float。再次强调,这些问题都可以在不执行代码的情况下被发现。
$ pyflakes foobar.py
foobar.py:6: redefinition of unused 'foo' from line 3
foobar.py:11: undefined name 'baz'
$ mypy foobar.py
foobar.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")
foobar.py:9: error: Incompatible types in assignment (expression has type "float", variable has type "int")
foobar.py:11: error: Name 'baz' is not defined
Found 3 errors in 1 file (checked 1 source file)
在shell工具那节课当中,我们介绍了shellcheck,这是一个类似的工具,不过是用来检查shell脚本的。
大多数编辑器和IDE支持将这些工具的输出结果展示在界面里,对有警告和错误的地方进行高亮。这个过程通常被称为code linting。同样,其他类型的问题也可以同样被展示,比如代码风格检查和安全检查。
在vim当中,ale
和syntastic
插件可以让你做到这些。对于Python来说,pylint
和pep8
是两种用于进行代码风格检查的工具。bandit
则可以用来进行安全检查。
对于其他语言来说,人们编译、整合了一系列拥有的静态检查的工具,比如awesome static analysis
:https://github.com/analysis-tools-dev/static-analysis(你可以查看一下Writing章节),对于lint工具,也有awesome linters
:https://github.com/caramelomartins/awesome-linters
一个完善的工具用来做风格检查通常被叫做code formatter,比如Python中的black
,Go中的gofmt
,Rust中的rustfmt
或者是JavaScript、HTML、CSS中的prettier
。这些工具会自动格式化你的代码,让你的代码和常用的风格保持统一。虽然你可能并不想让这些工具控制你的代码,但标准的代码格式可以帮助其他人更好的理解和阅读你的代码,同样也会更方便你去阅读其他人的代码。
Exercises
- 使用Linux中的
journalctl
或者是macOS中的log show
来获取最近一天超级用户(root)登录以及所执行的命令。如果找不到任何记录,你可以手动执行一些无伤大雅的命令,比如ls
- 学习这份
pdb
指南:https://github.com/spiside/pdb-tutorial,并熟悉相关命令,你可以参考这份教程:https://realpython.com/python-debugging-pdb/ - 安装
shellcheck
并且尝试对下面的脚本进行检查,它当中有什么问题?修复它。在你的编辑器当中安装一个linter插件,这样的话它可以自动警告你
#!/bin/sh
## Example: a typical script with several problems
for f in $(ls *.m3u)
do
grep -qi hq.*mp3 $f
&& echo -e 'Playlist $f contains a HQ file in mp3 format'
done
4.(进阶)请阅读这份可逆调试文档:https://undo.io/resources/reverse-debugging-whitepaper/,并使用rr
或者RevPDB
运行一个简单的例子
喜欢本文的话不要忘记三连~