Python 解決logging功能使用過程中遇到的一個問題

現象:

生產中心進行拷機任務下瞭300個任務,過瞭一陣時間後發現任務不再被調度起來,查看後臺日志發現日志輸出停在某個時間點。

分析:

1、首先確認進程存在並沒有dead。

2、然後用strace –p看瞭一下進程,發現進程卡在futex調用上面,應該是在鎖操作上面出問題瞭。

3、用gdb attach進程ID,用py-bt查看一下堆棧,發現堆棧的信息大致為:sig_handler(某個信號處理函數)->auroralogger(自定義的日志函數)->logging(python的logging模塊)->threading.acquire(獲取鎖)。從gdb的bt信息基本驗證瞭上面的猜想,應該是出現瞭死鎖。

4、Python的logging模塊本身肯定不會有死鎖的這種bug有可能出問題的就是我們的使用方式,看python中logging模塊的doc,發現有一個有一個Thread Safety的章節,內容很簡單但是也一下就解釋瞭我遇到的這個問題,內容如下:

The logging module is intended to be thread-safe without any special work needing to be done by its clients. It achieves this though using threading locks; there is one lock to serialize access to the module’s shared data, and each handler also creates a lock to serialize access to its underlying I/O.

If you are implementing asynchronous signal handlers using the signal module, you may not be able to use logging from within such handlers. This is because lock implementations in the threading module are not always re-entrant, and so cannot be invoked from such signal handlers.

第一部分是說logging是線程安全的,通過threading的lock對公用的數據進行瞭加鎖。

第二部分特意提到瞭在異步的信號處理函數中不能使用logging模塊,因為threading的lock機制是不支持重入的。

這樣就解釋瞭上面我遇到的死鎖問題,因為我在信號處理函數中調用瞭不可以重入的logging模塊。

線程安全和可重入:

從上面的logging模塊來看線程安全和可重入不是等價的,那麼這兩個概念之間有什麼聯系、區別呢?

1、可重入函數:從字面意思來理解就是這個函數可以重復調用,函數被多個線程亂序執行甚至交錯執行都能保證函數的輸出和函數單獨被執行一次的輸出一致。也就是說函數的輸出隻決定於輸入。

線程安全函數:函數可以被多個線程調用,並且保證不會引用到錯誤的或者臟的數據。線程安全的函數輸出不僅僅依賴於輸入還可能依賴於被調用時的順序。

2、可重入函數和線程安全函數之間有一個最大的差異是:是否是異步信號安全。可重入函數在異步信號處理函數中可以被安全調用,而線程安全函數不保證可以在異步信號處理函數中被安全調用。

上面我們遇到的loggin模塊就是非異步信號安全的,在主線程中我們正在使用log函數而log函數調用瞭threading.lock來獲取到瞭鎖,此時一個異步信號產生程序跳轉到信號處理函數中,信號處理函數又正好調用瞭log函數,因為前一個被調用的log函數還未釋放鎖,最後就形成瞭一個死鎖。

1、可重入函數必然是線程安全函數和異步信號安全函數,線程安全函數不一定是可重入函數。

總結:

異步信號處理函數中一定要盡可能的功能簡單並且不能調用不可重入的函數。

Python loggin模塊是線程安全但是是不可重入的。

補充:Python—logging模塊使用教程

簡單用法

日志等級

級別 何時使用
DEBUG 細節信息,僅當診斷問題時適用。
INFO 確認程序按預期運行
WARNING 表明有已經或即將發生的意外(例如:磁盤空間不足)。程序仍按預期進行
ERROR 由於嚴重的問題,程序的某些功能已經不能正常執行
CRITICAL 嚴重的錯誤,表明程序已不能繼續執行

控制臺輸出日志

import logging
logging.warning('Watch out!')  
logging.info('I told you so') 

將日志保存到文件並且設置時間和輸出格式

import logging
# 保存文件為example.log,記錄等級為DEBUG,即隻記錄DENBUG及以上的日志
# 輸出格式為 2019-19-06 18:47:06 - WARNING - And this, too
logging.basicConfig(
  filename='example.log',
  filemode='w',
  level=logging.DEBUG,
  format='%(asctime)s - %(levelname)s - %(message)s',
  datefmt='%Y-%d-%d %H:%M:%S'
)
logging.debug('This message should go to the log file')
logging.info('So shoul this')
logging.warning('And this, too')

參數解釋

filename日志文件路徑

filemode記錄日志文件的模式,w為每次啟動程序都創建一個全新的文件記錄, a表示追加到文件末尾, 默認為a

level記錄日志的等級

format日志輸出的格式

datefmt日志輸出時間的格式

使用配置文件配置日志

[loggers]
# 配置日志對象名, 默認為root
keys=root, poj
[handlers]
# 日志配置名對象名
keys=writeFileHandlers
[formatters]
# 日志輸出格式對象名
keys=writeFileFormatters
[logger_root]
level=DEBUG
handlers=writeFileHandlers
[logger_poj]
level=DEBUG
handlers=writeFileHandlers
qualname=writeFileFormatters
propagate=0
[logger_leetcode]
level=DEBUG
handlers=writeFileHandlers
qualname=writeFileFormatters
propagate=0
[handler_writeFileHandlers]
# 設置writeFileHandlers對象的配置
class=FileHandler
level=DEBUG
formatter=writeFileFormatters
# 記錄在文件中,以追加的形式
args=("demo.log", "a")
[formatter_writeFileFormatters]
設置writeFileHandlers對象的輸出配置
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=%Y-%d-%m %H:%M:%S

使用配置文件

import logging.config
# 加載配置文件
logging.config.fileConfig('logging.conf')
# 獲取日志對象名為poj的
logger = logging.getLogger("poj")
logger.debug('This message should go to the log file')
logger.info('So shoul this')
logger.warning('And this, too')

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。

推薦閱讀:

    None Found