免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
Python 代碼調(diào)試二三事
作者王維棟,網(wǎng)易游戲運(yùn)維與基礎(chǔ)架構(gòu)部,產(chǎn)品軟件設(shè)計(jì)師,專注于智能監(jiān)控、性能優(yōu)化等領(lǐng)域。

本文來自《網(wǎng)易游戲運(yùn)維平臺(tái) (neteasegameops)》微信公眾號(hào)的投稿,隸屬于網(wǎng)易游戲運(yùn)維與基礎(chǔ)架構(gòu)部,運(yùn)基部專注于虛擬化、智能運(yùn)維、SAAS服務(wù)、游戲安全、大數(shù)據(jù)、網(wǎng)絡(luò)等領(lǐng)域, 服務(wù)的產(chǎn)品包括《明日之后》、《夢(mèng)幻西游》、《大話西游》、《第五人格》等數(shù)百款游戲。


前言

借著機(jī)器學(xué)習(xí)的東風(fēng),Python近年來大熱,一舉沖入最受歡迎編程語言榜前三。同時(shí),Python已經(jīng)在Web、運(yùn)維、科學(xué)計(jì)算、游戲等領(lǐng)域廣泛應(yīng)用,很多人使用它來構(gòu)建原型、實(shí)現(xiàn)周邊模塊、做實(shí)驗(yàn)、寫性能要求不高的業(yè)務(wù)邏輯、作為膠水語言完成系統(tǒng)串聯(lián),亦有不少公司使用Python來開發(fā)大型應(yīng)用。

如今,拿Python寫的代碼越來越多,有代碼的地方就有bug。雖然bug free是我們的追求,但有時(shí)面對(duì)復(fù)雜需求、大型系統(tǒng),我們不得不接受一個(gè)事實(shí):我們可能要花一半時(shí)間寫bug,再花另一半時(shí)間把它們調(diào)試好。代碼調(diào)試占了如此高的工作比重,那么提升它的效率就是一個(gè)很有意義的話題了。

Python是一種強(qiáng)類型動(dòng)態(tài)語言,具有非常活躍的社區(qū)和海量的庫。所以,我們?cè)谑褂脗鹘y(tǒng)的代碼調(diào)試方法的同時(shí),還多了一些額外的選擇。接下來的章節(jié)中我會(huì)介紹一些常見的調(diào)試方法和Python特有的技巧,并分享一些調(diào)試的經(jīng)驗(yàn)。

Print & Logging

Print和logging可以在代碼運(yùn)行過程中暴露出一些變量的值、打印出關(guān)鍵信息,幫助我們觀察和跟蹤代碼的執(zhí)行過程。

Print Print Print

作為script programmer,我們?cè)谡{(diào)試時(shí)的第一反應(yīng)是print here, print there, print everywhere. 在寫小腳本的時(shí)候這樣做完全沒問題,但當(dāng)我們開始構(gòu)建比較復(fù)雜的業(yè)務(wù)模塊的時(shí)候,print的問題就顯現(xiàn)出來了,格式隨意、缺乏時(shí)間戳等必要信息、通常只print到stdout,寫其他的流或者文件還需要額外的代碼。最關(guān)鍵的一點(diǎn)是:看起來一點(diǎn)都不專業(yè) 233。下面是一個(gè)簡單的例子:

  1. try:


  2.    1 / 0


  3. except ZeroDivisionError as e:


  4.    print('some text', e)


  5.    with open('some.log', 'w') as f:


  6.        print('some text', e, file=f)

Logging的實(shí)踐與價(jià)值

Logging是比print更規(guī)范和“專業(yè)“的做法,下面羅列一些logging的好處和個(gè)人覺得比較有用的實(shí)踐:

規(guī)范格式

我們可以定義好日志的規(guī)范格式,包含必要信息,并且有利于后續(xù)的日志處理和分析。如下是使用logging.Formatter格式化日志的例子:

  1. LOG_FORMAT = '%(asctime)s %(levelname) 8s: [%(filename)s:%(lineno)d] [%(processName)s:%(process)d %(threadName)s] - %(message)s'


  2. DATE_FORMAT = '[%Y-%m-%d %H:%M:%S]'






  3. def get_logger(name):


  4.    logger = logging.getLogger(name)


  5.    formatter = logging.Formatter(LOG_FORMAT, DATE_FORMAT)


  6.    handler = logging.StreamHandler(sys.stdout)


  7.    handler.setFormatter(formatter)


  8.    logger.addHandler(handler)


  9.    return logger






  10. logger = get_logger(__name__)


  11. logger.warning('test msg')

得到這樣的日志輸出:

  1. [2019-01-09 13:36:26]  WARNING: [test1.py:33] [MainProcess:16830 MainThread] - test msg

如果想讓日志更易處理,更有表現(xiàn)力,可以嘗試結(jié)構(gòu)化的日志,一個(gè)使用logging.Filter結(jié)構(gòu)化日志的例子:

  1. import json






  2. class JsonLogFilter(logging.Filter):




  3.    def filter(self, log):


  4.        if isinstance(log.msg, str):


  5.            return True


  6.        if isinstance(log.msg, (dict, list, tuple)):


  7.            log.msg = json.dumps(log.msg)


  8.            return True


  9.        return False






  10. logger.addFilter(JsonLogFilter())


  11. logger.warning({


  12.    'action': 'server_start',


  13.    'port': 8080,


  14.    'engine': {'name': 'tornado', 'version': '0.1.1'}


  15. })

得到這樣的日志輸出:

  1. [2019-01-09 13:46:01]  WARNING: [test1.py:57] [MainProcess:17549 MainThread] - {'action': 'server_start', 'port': 8080, 'engine': {'name': 'tornado', 'version': '0.1.1'}}

這樣的日志可以很清晰地?cái)y帶大量信息,后續(xù)的日志解析和處理也更方便。

分級(jí)

logging可以定義消息等級(jí),常見的等級(jí):

我們可以通過logger的同名成員函數(shù)來輸出不同等級(jí)的日志,設(shè)置logger或者logHandler的level,來決定什么級(jí)別的日志會(huì)被輸出:

  1. logger.setLevel(logging.ERROR)


  2. stdLogHandler.setLevel(logging.INFO)


這些工作會(huì)在后續(xù)的日志處理中產(chǎn)生價(jià)值,一些實(shí)踐例子:

  • 把error和critical的日志輸出到單獨(dú)的error log文件,查問題時(shí)優(yōu)先關(guān)注它們;

  • 把error和critical日志直接流入報(bào)警系統(tǒng)、故障自愈系統(tǒng)等,觸發(fā)對(duì)應(yīng)策略;

  • 業(yè)務(wù)發(fā)布時(shí)關(guān)掉debug日志,在排查問題時(shí)通過外部配置、RPC等方式觸發(fā)進(jìn)程修改日志等級(jí);

  • 排查問題時(shí),啟動(dòng)一個(gè)新的進(jìn)程,指定debug level的日志,輸出debug log來輔助排查;

多種輸出流

logging模塊提供了文件、標(biāo)準(zhǔn)輸出等多種logHandler,我們也可以自定義自己的handler,讓日志輸出到不同的流。

logger也可以添加多個(gè)logHandler,并且分別定義Formatter、Level,讓日志同時(shí)以不同的方式輸出到多個(gè)流。

按模塊管理

每個(gè)logger初始化的時(shí)候都可以指定名字,我們通常會(huì)以模塊名作為logger name,方便后續(xù)的管理和日志使用。

例如,我們同時(shí)使用了requests和kafka,但我們比較關(guān)注kafka的日志,requests只有在error的時(shí)候才值得關(guān)注,可以這樣配置:

  1. logging.getLogger('requests').setLevel(logging.ERROR)


  2. logging.getLogger('kafka').setLevel(logging.WARNING)

Logging準(zhǔn)則

通過以上的的介紹,我們已經(jīng)可以自如地定制日志的輸出了,那么什么時(shí)候應(yīng)該輸出日志呢?日志太多會(huì)影響性能,造成不必要的干擾,難以閱讀,日志太少會(huì)讓我們對(duì)進(jìn)程的執(zhí)行情況缺乏了解。在實(shí)踐中,我們應(yīng)該在生產(chǎn)環(huán)境盡可能取得二者平衡。

這里有一些建議:

  • 確保每一次日志輸出都是有價(jià)值、無歧義、易讀的;

  • Debug日志盡可能詳盡地輸出細(xì)節(jié)信息,例如關(guān)鍵函數(shù)的調(diào)用、輸入、輸出,某些關(guān)鍵分支步驟的選擇等,在生產(chǎn)環(huán)境中關(guān)閉,必要時(shí)再打開;

  • 一些重要變量的變更、重要函數(shù)的調(diào)用應(yīng)該盡量輸出日志;

  • 做一些周期性指標(biāo)統(tǒng)計(jì)并通過日志等方式輸出,以備監(jiān)控報(bào)警和觀察執(zhí)行情況,例如QPS、數(shù)據(jù)吞吐量、任務(wù)堆積數(shù)等;

  • CRUD中的Create、Update、Delete通常需要輸出Info log,用作資源操作記錄;

  • 對(duì)一些可以發(fā)生,但大量發(fā)生代表可能有嚴(yán)重問題的代碼分支,最好輸出Warning日志,并在后續(xù)日志報(bào)警中按數(shù)量報(bào)警,例如:緩存未命中,RPC Retry等;

  • 可自動(dòng)恢復(fù)的異常通常至少要輸出Warning日志;

  • 不可恢復(fù)的異常視邏輯嚴(yán)重程度輸出Error或者Critical日志,并且要帶上堆棧和必要的變量信息;

  • 微服務(wù)系統(tǒng)中,確保每條日志都帶上可追蹤的session id;

  • 不要在頻繁調(diào)用的函數(shù)中輸出Info及以上級(jí)別的日志,如果真的需要,可以考慮周期統(tǒng)計(jì)輸出,或者采樣輸出;

IDE Debugger

無意挑起IDE和編輯器之爭,但現(xiàn)代的IDE確實(shí)提供了更便捷更強(qiáng)大的Debug功能,這些功能通常需要我們這些vim、sublime、emacs愛好者花費(fèi)額外的時(shí)間才能得到。

對(duì)廣大IDE使用者來說,最便捷的離線調(diào)試方法顯然就是IDE Debugger了,我們以PyCharm為例簡單介紹下。

PyCharm Debugger提供了一系列常見的功能:

  • 斷點(diǎn);

  • step in / out, step over等操作;

  • Variables窗口,查看當(dāng)前scope內(nèi)的變量,也可以自己添加Watches;

  • Frames窗口,可以切換線程,查看線程的Frames鏈。

  • Evaluate窗口,可以直接計(jì)算表達(dá)式值;

  • Attach到已運(yùn)行的進(jìn)程,并開啟Debugger。

GDB系列調(diào)試器

仍然有大量程序員是GDB、PDB、IPDB系列Debugger的用戶,這些工具同樣能在代碼調(diào)試過程中產(chǎn)生幫助,以ipdb(Ipython pdb)為例。

IPDB調(diào)試代碼

我們可以直接加載ipdb,啟動(dòng)進(jìn)程:

  1. ?  test python3 -m ipdb run.py


  2. > /Users/Valens/PycharmProjects/test/run.py(1)<module>()


  3. ----> 1 import time


  4.      2 import logging


  5.      3 import random




  6. ipdb> r


  7. WARNING:root:handling a big value: 0.8740442611857767


  8. > /Users/Valens/PycharmProjects/test/run.py(18)handle()


  9.     17             set_trace()


  10. ---> 18             self._last_big_value = data['value']


  11.     19         self._counter += 1




  12. ipdb> p data


  13. {'value': 0.8740442611857767, 'done': True}


  14. ipdb> n


  15. > /Users/Valens/PycharmProjects/test/run.py(19)handle()


  16.     18             self._last_big_value = data['value']


  17. ---> 19         self._counter += 1


  18.     20         return data

也可以在代碼中設(shè)置斷點(diǎn),并在斷點(diǎn)處停下來,啟動(dòng)ipython debugger:

  1. import time


  2. import logging


  3. import random


  4. from ipdb import set_trace






  5. class Processor(object):




  6.    def __init__(self):


  7.        self._counter = 0


  8.        self._last_big_value = float('inf')




  9.    def handle(self, data):


  10.        data['done'] = True


  11.        if data['value'] > 0.8:


  12.            logging.warning('handling a big value: %s' % data['value'])


  13.            set_trace() # 斷點(diǎn)


  14.            self._last_big_value = data['value']


  15.        self._counter += 1


  16.        return data






  17. def feed(processor):


  18.    while True:


  19.        time.sleep(random.random())


  20.        processor.handle({


  21.            'value': random.random()


  22.        })






  23. def main():


  24.    feed(Processor())






  25. if __name__ == '__main__':


  26.    main()

然后執(zhí)行

  1. ?  test python3 run.py


  2. WARNING:root:handling a big value: 0.844438382990963


  3. > /Users/Valens/PycharmProjects/test/run.py(18)handle()


  4.     17             set_trace()


  5. ---> 18             self._last_big_value = data['value']


  6.     19         self._counter += 1




  7. ipdb> p data


  8. {'value': 0.844438382990963, 'done': True}

GDB調(diào)試COREDUMP

GDB是老牌且功能強(qiáng)大的Debugger,在后起之秀更加易用的今天,它出場的機(jī)會(huì)不算多。但當(dāng)線上進(jìn)程Coredump時(shí),我們的選擇并不多,這時(shí)候GDB的強(qiáng)大之處就顯現(xiàn)出來了。

使用GDB調(diào)試Python,首先需要準(zhǔn)備好必要的包:

  • libpython 讓GDB可以調(diào)試python代碼,包含pybt pyprint pyup pydown等一系列GDB命令的python版本;

  • python-dbg python debug擴(kuò)展,包括python符號(hào)文件和libpython等工具,在不同發(fā)行版的包名可能不一樣;

  • 如果你用到了一些c擴(kuò)展的python庫,需要解與之相關(guān)的棧,可能也需要對(duì)應(yīng)的dbg包,例如 python-mysqldb-dbg

通常,如果我們知道一個(gè)python進(jìn)程會(huì)core dump,那可以選擇用python-dbg運(yùn)行它(當(dāng)然會(huì)有少量性能的trade off),等待它c(diǎn)ore dump后,再用gdb打開會(huì)獲得最全面的信息。非dbg運(yùn)行的python core dump也可以如此嘗試,但可能因?yàn)榉?hào)表缺失、版本不一致等原因損失一些信息。

我們準(zhǔn)備一個(gè)測(cè)試,還是剛才熟悉的代碼,在random數(shù)字大于0.9時(shí)觸發(fā)一段“神秘代碼“,python2.7下的經(jīng)典stack overflow bug,讓進(jìn)程因?yàn)镾eg fault而被內(nèi)核core dump掉。

  1. import time


  2. import logging


  3. import random






  4. class Processor(object):




  5.    def __init__(self):


  6.        self._counter = 0


  7.        self._last_big_value = float('inf')




  8.    def handle(self, data):


  9.        data['done'] = True


  10.        if data['value'] > 0.8:


  11.            logging.warning('handling a big value: %s' % data['value'])


  12.            self._last_big_value = data['value']


  13.        if data['value'] > 0.9:


  14.            raise_seg_fault()


  15.        self._counter += 1


  16.        return data






  17. def raise_seg_fault():


  18.    logging.error('seg fault')


  19.    exec'()'*7**6






  20. def feed(processor):


  21.    while True:


  22.        time.sleep(random.random())


  23.        processor.handle({


  24.            'value': random.random()


  25.        })






  26. def main():


  27.    feed(Processor())






  28. if __name__ == '__main__':


  29.    main()

跑起~

  1. valens@some-host:~$ python-dbg run.py


  2. WARNING:root:handling a big value: 0.860424864923


  3. WARNING:root:handling a big value: 0.94984309816


  4. ERROR:root:seg fault


  5. Segmentation fault (core dumped)

然后用gdb打開core dump文件,指定exec為python-dbg

  1. valens@some-host:~$ gdb python-dbg core.python-dbg.some-host.28249


  2. GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1


  3. ...


  4. Reading symbols from python-dbg...done.


  5. [New LWP 28249]


  6. [Thread debugging using libthread_db enabled]


  7. Using host libthread_db library '/lib/x86_64-linux-gnu/libthread_db.so.1'.


  8. Core was generated by `python-dbg run.py`.


  9. Program terminated with signal SIGSEGV, Segmentation fault.


  10. #0  0x0000000000563415 in symtable_visit_expr (st=<error reading variable: Cannot access memory at address 0x7ffdc1083fe8>, e=<error reading variable: Cannot access memory at address 0x7ffdc1083fe0>) at ../Python/symtable.c:1185

跟蹤c棧

  1. (gdb) bt


  2. #0  0x0000000000563415 in symtable_visit_expr (st=<error reading variable: Cannot access memory at address 0x7ffdc1083fe8>, e=<error reading variable: Cannot access memory at address 0x7ffdc1083fe0>) at ../Python/symtable.c:1185


  3. #1  0x0000000000563bde in symtable_visit_expr (st=0x7f3fe9b00110, e=0x20f8150) at ../Python/symtable.c:1256

跟蹤py棧和變量

  1. # 列出py棧


  2. (gdb) py-bt


  3. #32710 Frame 0x7f3fe9b17248, for file run.py, line 25, in raise_seg_fault ()


  4.    exec'()'*7**6


  5. #32714 Frame 0x7f3fe9b33c60, for file run.py, line 18, in handle (self=<Processor(_counter=19, _last_big_value=<float at remote 0x1b7e620>) at remote 0x7f3fe9b0ba00>, data={'done': True, 'value': <float at remote 0x1b7e620>})


  6.    raise_seg_fault()


  7. #32717 Frame 0x7f3fe9aef840, for file run.py, line 32, in feed (processor=<Processor(_counter=19, _last_big_value=<float at remote 0x1b7e620>) at remote 0x7f3fe9b0ba00>)


  8.    'value': random.random()


  9. #32720 Frame 0x7f3fe9b887e0, for file run.py, line 37, in main ()


  10.    feed(Processor())


  11. #32723 Frame 0x7f3feb29f060, for file run.py, line 41, in <module> ()


  12.    main()




  13. # 向上回到frame: handle


  14. (gdb) py-up


  15. #32714 Frame 0x7f3fe9b33c60, for file run.py, line 18, in handle (self=<Processor(_counter=19, _last_big_value=<float at remote 0x1b7e620>) at remote 0x7f3fe9b0ba00>, data={'done': True, 'value': <float at remote 0x1b7e620>})




  16. # 查看這個(gè)frame的局部變量


  17. (gdb) py-locals


  18. self = <Processor(_counter=19, _last_big_value=<float at remote 0x1b7e620>) at remote 0x7f3fe9b0ba00>


  19. data = {'done': True, 'value': <float at remote 0x1b7e620>}




  20. # 讀一下value


  21. (gdb) p *(PyFloatObject*) 0x1b7e620


  22. $3 = {_ob_next = 0x7f3fe9b10d30, _ob_prev = 0x7f3fe9b33c60, ob_refcnt = 2, ob_type = 0x8b7580 <PyFloat_Type>, ob_fval = 0.94984309816000645}

通過查看py棧和c棧的調(diào)用關(guān)系和變量值,大部分的問題都應(yīng)該能定位了~

Tracers

線上的情況經(jīng)常是錯(cuò)綜復(fù)雜的,我們也沒辦法在寫代碼和測(cè)試階段就完全預(yù)測(cè)代碼的每一步執(zhí)行和每個(gè)分支。當(dāng)線上業(yè)務(wù)出問題通常都會(huì)有點(diǎn)難以下手。其實(shí)很多l(xiāng)inux工具都可以在這時(shí)候提供幫助。當(dāng)然,不僅限于看看cpu內(nèi)存消耗、查查網(wǎng)絡(luò)鏈接和fd那么簡單,這里以perf和strace做簡單舉例,如果大家覺得有用,可以更深入地學(xué)習(xí)具體用法。

Perf追蹤性能

perf是linux下一個(gè)非常好用的性能profile工具。在日常用來進(jìn)行性能優(yōu)化以外,我們也可以在用它來排查一些線上的問題,當(dāng)一個(gè)數(shù)據(jù)處理進(jìn)程消費(fèi)速度減慢,或者一個(gè)server cpu暴增時(shí),不妨perf一下,看看進(jìn)程現(xiàn)在在干嘛,為我們排查和推斷問題原因提供依據(jù)。

舉例:

  1. # perf 進(jìn)程 6252 10秒,采集call graph信息


  2. perf record -g -p 6252 sleep 10

  1. perf report --stdio


  2. ...


  3.    89.67%     3.54%  python-dbg  python2.7-dbg       [.] call_function


  4.            |


  5.            --- call_function


  6.               |


  7.               |--99.80%-- PyEval_EvalFrameEx


  8.               |          |


  9.               |          |--73.81%-- fast_function


  10.               |          |          call_function


  11.               |          |          PyEval_EvalFrameEx


  12.               |          |          |


  13.               |          |          |--72.77%-- fast_function


  14.               |          |          |          call_function


  15.               |          |          |          PyEval_EvalFrameEx


  16.               |          |          |          |


  17.               |          |          |          |--54.21%-- fast_function


  18.               |          |          |          |          call_function


  19.               |          |          |          |          PyEval_EvalFrameEx


  20.               |          |          |          |          |


  21.               |          |          |          |          |--62.17%-- fast_function


  22.               |          |          |          |          |          call_function


  23.               |          |          |          |          |          PyEval_EvalFrameEx


  24.               |          |          |          |          |          |


  25.               |          |          |          |          |          |--56.99%-- fast_function


  26.               |          |          |          |          |          |          call_function


  27.               |          |          |          |          |          |          PyEval_EvalFrameEx


  28. ...

  1. perf report --stdio -g none


  2. # Samples: 3K of event 'cpu-clock'


  3. # Event count (approx.): 839500000


  4. #


  5. # Children      Self     Command       Shared Object                                 Symbol


  6. # ........  ........  ..........  ..................  .....................................


  7. #


  8.    89.67%     0.00%  python-dbg  libc-2.19.so        [.] __libc_start_main


  9.    89.67%     0.00%  python-dbg  python2.7-dbg       [.] main


  10.    89.67%     0.00%  python-dbg  python2.7-dbg       [.] Py_Main


  11.    89.67%     0.00%  python-dbg  python2.7-dbg       [.] PyRun_AnyFileExFlags


  12.    89.67%     0.00%  python-dbg  python2.7-dbg       [.] PyRun_SimpleFileExFlags


  13.    89.67%     0.00%  python-dbg  python2.7-dbg       [.] PyRun_FileExFlags


  14.    89.67%     0.00%  python-dbg  python2.7-dbg       [.] run_mod


  15.    89.67%     0.00%  python-dbg  python2.7-dbg       [.] PyEval_EvalCode


  16.    89.67%     3.54%  python-dbg  python2.7-dbg       [.] call_function


  17.    89.67%     1.28%  python-dbg  python2.7-dbg       [.] fast_function


  18.    89.67%     0.95%  python-dbg  python2.7-dbg       [.] PyEval_EvalCodeEx


  19.    89.67%    14.80%  python-dbg  python2.7-dbg       [.] PyEval_EvalFrameEx


  20.    57.89%     0.09%  python-dbg  python2.7-dbg       [.] ext_do_call


  21.    57.74%     0.33%  python-dbg  python2.7-dbg       [.] PyObject_Call


  22.    57.65%     0.24%  python-dbg  python2.7-dbg       [.] function_call


  23.    19.15%     0.03%  python-dbg  python2.7-dbg       [.] do_call


  24.    18.82%     0.06%  python-dbg  python2.7-dbg       [.] type_call


  25.    18.55%     0.21%  python-dbg  python2.7-dbg       [.] slot_tp_init


  26.    18.20%     0.09%  python-dbg  python2.7-dbg       [.] instancemethod_call


  27.    15.78%     1.94%  python-dbg  python2.7-dbg       [.] PyObject_GetAttr


  28.    13.94%     0.12%  python-dbg  python2.7-dbg       [.] PyObject_GenericGetAttr


  29. ...

Strace追蹤系統(tǒng)調(diào)用

有時(shí)我們會(huì)遇到進(jìn)程阻塞,這時(shí)候可以通過strace來追蹤系統(tǒng)調(diào)用看看進(jìn)程是不是卡在某處io,或者在接收巨量的網(wǎng)絡(luò)包,以及其他跟系統(tǒng)調(diào)用、信號(hào)相關(guān)的場景。

舉例:

  1. # strace -p 1227


  2. Process 1227 attached


  3. select(0, NULL, NULL, NULL, {0, 524306}) = 0 (Timeout)


  4. gettimeofday({1547026076, 550492}, NULL) = 0


  5. write(2, 'WARNING:root:handling a big valu'..., 50) = 50


  6. select(0, NULL, NULL, NULL, {0, 195142}) = 0 (Timeout)

Inject Debugger

深究代碼調(diào)試的本質(zhì),是觀察代碼執(zhí)行,從中找出不合理的邏輯、未處理的邊界情況以及可優(yōu)化的地方。那么,沒有什么比直接進(jìn)入正在執(zhí)行的Python進(jìn)程并近距離觀察和把玩它的邏輯更方便的了。作為一個(gè)動(dòng)態(tài)語言,Python在Debug On-the-fly方面具有非常大的想象空間。

這里夾帶私貨地介紹一個(gè)Python Inject Debugger:pylane

 pylane,網(wǎng)易游戲開源的 Python 調(diào)試工具  

 https://github.com/NtesEyes/pylane

pylane 通過 gdb trace python 進(jìn)程,然后在該進(jìn)程的python vm中動(dòng)態(tài)地注入一段python代碼,從而對(duì)一個(gè)運(yùn)行中的python進(jìn)程執(zhí)行一段任意的新邏輯。

作為Debugger,pylane最常用的功能是pylane shell,它對(duì)目標(biāo)進(jìn)程注入了一個(gè)新線程,并在這個(gè)線程中啟動(dòng)一個(gè)python Interpreter,然后連接到終端的IPython Console。接下來,我們就可以在這個(gè)Console中訪問目標(biāo)進(jìn)程中的所有對(duì)象。

繼續(xù)使用上文中的Processor代碼,用pylane shell attach它。

  1. pylane shell <PID>

接下來舉幾個(gè)典型的用例。

友情提醒,在生產(chǎn)環(huán)境做任何非規(guī)范的變更都有危險(xiǎn),請(qǐng)三思。

查看變量

如果我們需要看到某個(gè)內(nèi)部變量的值,但這個(gè)變量沒有通過log等方式暴露出來,那就可以用pylane來直接查看。

  1. pylane shell 4996




  2. # 用內(nèi)置tools的get_insts方法獲取Processor的實(shí)例


  3. In [2]: p = tools.get_insts('Processor')[0]




  4. In [3]: p


  5. Out[3]:


  6. <__main__.Processor object at 0x7f3d45c6ad68>




  7. # 在代碼運(yùn)行的同時(shí)觀察p的屬性變化


  8. In [4]: p._counter


  9. Out[4]: 252




  10. In [5]: p._counter


  11. Out[5]: 262




  12. In [6]: p._last_big_value


  13. Out[6]: 0.8872528770214453

查看代碼

在源文件存在的情況下,可以直接查看某個(gè)object的代碼。

  1. In [2]: tools.print_source_code(tools.get_classes('Processor')[0])


  2. Out[2]:


  3. class Processor(object):




  4.    def __init__(self):


  5.        self._counter = 0


  6.        self._last_big_value = float('inf')




  7.    def handle(self, data):


  8.        data['done'] = True


  9.        if data['value'] > 0.8:


  10.            logging.warning('handling a big value: %s' % data['value'])


  11.            self._last_big_value = data['value']


  12.        self._counter += 1


  13.        return data

觸發(fā)函數(shù)調(diào)用

如果需要調(diào)用一次函數(shù),觸發(fā)某段邏輯,也可以直接調(diào)用。

  1. In [4]: p = tools.get_insts('Processor')[0]




  2. In [5]: p.handle({'value': -100})


  3. Out[5]: {'done': True, 'value': -100}

打印線程棧和變量

我們有時(shí)會(huì)想知道進(jìn)程目前在干什么,比如任務(wù)執(zhí)行的很慢,進(jìn)程完全阻塞了的情況。

執(zhí)行下面的方法打印所有線程的棧和變量:

  1. In [6]: tools.print_threads()


  2. Out[6]:




  3. Thread: MainThread


  4. Stack:


  5.   File '/usr/lib/python2.7/threading.py', line 1098, in _exitfunc


  6.    t.join()


  7.  File '/usr/lib/python2.7/threading.py', line 940, in join


  8.    self.__block.wait()


  9.  File '/usr/lib/python2.7/threading.py', line 340, in wait


  10.    waiter.acquire()




  11. Locals:


  12. {'saved_state': None, 'waiter': <thread.lock object at 0x7fc7c9d062d0>, 'self': <Condition(<thread.lock object at 0x7fc7c9d06290>, 1)>, 'timeout': None}

或者,如果我們知道線程名,可以這樣指定:

  1. tools.print_threads(['Thread1', 'Thread2'])

排查內(nèi)存泄露

objgraph 是一個(gè)非常好用的Python內(nèi)存排查工具,但它需要入侵代碼,如果線上有一個(gè)難以復(fù)現(xiàn)的內(nèi)存泄露,用pylane + objgraph可以更容易地排查:

先在跑服務(wù)的virtual env中準(zhǔn)備好objgraph包,然后打開pylane shell

  1. In [5]: import objgraph




  2. In [6]: objgraph.growth()


  3. Out[6]:


  4. [('function', 1484, 1484),


  5. ('wrapper_descriptor', 1105, 1105),


  6. ('builtin_function_or_method', 732, 732),


  7. ('method_descriptor', 590, 590),


  8. ('dict', 581, 581),


  9. ('tuple', 529, 529),


  10. ('weakref', 509, 509),


  11. ('list', 267, 267),


  12. ('getset_descriptor', 234, 234),


  13. ('member_descriptor', 229, 229)]




  14. In [7]: objgraph.growth()


  15. Out[7]: [('list', 268, 1)]

修改代碼邏輯

如果遇到某個(gè)調(diào)用比較深的函數(shù)執(zhí)行有問題,我們想知道它每次執(zhí)行了多久,輸入輸出是什么,但是之前沒有l(wèi)og出來,可以monkey patch它。

  1. In [2]: p = tools.get_insts('Processor')[0]


  2. In [3]: origin_func, p.handle = p.handle, tools.log_it(p.handle)

接下來可以看到新的log了

  1. WARNING:root:handling a big value: 0.821825815755


  2. WARNING:root:func: handle sec_cost: 0.0002 input: (({'done': True, 'value': 0.8218258157546575},), {}) output {'done': True, 'value': 0.8218258157546575}


  3. WARNING:root:func: handle sec_cost: 0.0003 input: (({'done': True, 'value': 0.5901812673170566},), {}) output {'done': True, 'value': 0.5901812673170566}

調(diào)試完,回滾即可

  1. In [4]: p.handle = origin_func

顯而易見,擁有一個(gè)attach console,我們能做的事情遠(yuǎn)不止此,我們可以用它來修改運(yùn)行時(shí)配置(?),開關(guān)某個(gè)功能(??),熱更新代碼(???),當(dāng)然,注意安全。

重申:在生產(chǎn)環(huán)境做任何非規(guī)范的變更都有危險(xiǎn),請(qǐng)三思。

結(jié)語

這篇文章是按個(gè)人的一些經(jīng)驗(yàn)和見解寫就,旨在分享一些個(gè)人常用的代碼調(diào)試方式和思路。能力和精力有限,如有謬誤,歡迎指正。文章中提到了的大部分工具,都是簡單介紹,淺嘗輒止,它們的更多功能請(qǐng)參閱各個(gè)工具的文檔。

在套路和工具以外,代碼調(diào)試更多的還是依靠開發(fā)者的知識(shí)儲(chǔ)備、編碼經(jīng)驗(yàn)和想象力,當(dāng)然,偶爾也需要一點(diǎn)運(yùn)氣。持續(xù)不斷地學(xué)習(xí),多看大神們的代碼,多寫,多總結(jié),才能在寫代碼和調(diào)代碼的時(shí)候更加駕輕就熟,離bug free的偉大目標(biāo)更近一點(diǎn)。



Python中文社區(qū)作為一個(gè)去中心化的全球技術(shù)社區(qū),以成為全球20萬Python中文開發(fā)者的精神部落為愿景,目前覆蓋各大主流媒體和協(xié)作平臺(tái),與阿里、騰訊、百度、微軟、亞馬遜、開源中國、CSDN等業(yè)界知名公司和技術(shù)社區(qū)建立了廣泛的聯(lián)系,擁有來自十多個(gè)國家和地區(qū)數(shù)萬名登記會(huì)員,會(huì)員來自以公安部、工信部、清華大學(xué)、北京大學(xué)、北京郵電大學(xué)、中國人民銀行、中科院、中金、華為、BAT、谷歌、微軟等為代表的政府機(jī)關(guān)、科研單位、金融機(jī)構(gòu)以及海內(nèi)外知名公司,全平臺(tái)近20萬開發(fā)者關(guān)注。

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
python 格式化日志輸出
跟我一起學(xué)Python——在python工程中添加日志功能
棄繁就簡!一行代碼搞定 Python 日志!
無壓力,輕松學(xué)會(huì)Python的裝飾器
bug解決不了?使用日志法
Python調(diào)試器,一個(gè)優(yōu)秀開發(fā)人員的必備技能包
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服