iT邦幫忙

5

Python多執行緒日誌記錄系統問題

  • 分享至 

  • xImage

高效日誌系統設計挑戰

大家好,我最近在研究 Python 的多執行緒與多處理程序設計,尤其是涉及 高效處理共享資源 的問題時,發現了一些值得深挖的挑戰,想和大家討論一下。

特別是我在設計一個 高效的日誌系統 時,面臨了多執行緒寫入檔案的問題,想請教大家的經驗與見解。這裡不是單純探討如何用鎖(threading.Lock)來解決競態條件,而是希望能討論一些更高階的模式,比如如何設計一個 無鎖(lock-free) 的系統,或者如何避免共享資源導致的性能瓶頸。


場景:多執行緒日誌記錄系統

假設我們需要設計一個日誌系統,允許多個執行緒同時記錄日誌,並將結果寫入同一個檔案中。問題在於:

  • 使用鎖(如 threading.Lock)來同步日誌寫入,雖然可以確保資料一致性,但會大幅降低寫入效能,特別是在高併發的場景下。
  • 如果不用鎖,則可能出現資料覆蓋、日誌內容混亂的情況。
  • 日誌寫入本身可能是系統的性能瓶頸,是否應該考慮將日誌先存入記憶體佇列(in-memory queue),然後用一個單獨的執行緒統一寫入檔案?
  • 如果採用這種方式,又該如何設計來避免記憶體佇列溢出?
  • 在這種架構下,是否會出現新的性能瓶頸?

測試程式碼

下面是我嘗試寫的一段模擬程式碼,用來測試多執行緒同時寫入檔案的情況。程式中啟動了 3 個執行緒,分別模擬不同執行緒寫入相同檔案。

import threading
import time

# 模擬多執行緒寫入日誌
def log_to_file(filename, thread_id):
    with open(filename, "a") as file:
        for i in range(5):
            file.write(f"Thread {thread_id} - Log entry {i}\n")
            time.sleep(0.1)  # 模擬寫入延遲

threads = []
filename = "logfile.txt"

# 啟動多個執行緒
for i in range(3):
    t = threading.Thread(target=log_to_file, args=(filename, i))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("日誌記錄完成。請檢查檔案內容。")
    

測試結果

執行上面的程式碼後,我發現以下問題:

日誌順序混亂

不同執行緒的日誌輸出順序並不穩定,有時候會出現一個執行緒的日誌被其他執行緒插入,導致內容混亂。

效能瓶頸

如果為了解決混亂的問題而加入鎖,日誌的寫入速度會明顯下降,特別是當執行緒數量增加時,這種情況更加明顯。

問題

無鎖的架構

  • 是否可以引入一個無鎖的架構來提高效能?比如:
    • 每個執行緒寫入到自己的暫存檔案中,然後由單獨的執行緒進行合併。
    • 使用內存佇列(例如 queue.Queue),將日誌先寫入佇列,再統一寫入檔案。

無鎖日誌系統的可能性

如果要避免使用鎖,是否可以通過內存佇列緩存日誌內容,讓一個專門的寫入執行緒負責定期將佇列內容寫入檔案?但這樣的設計會引入新的挑戰:

  • 如何控制佇列的大小,避免內存溢出?
  • 如果系統崩潰,是否會導致佇列中的日誌丟失?

日誌分片與合併

是否應該考慮讓每個執行緒將日誌寫入不同的檔案,然後在後處理階段進行合併?但這會帶來額外的合併開銷,並且如何高效合併成為新的問題。

第三方工具的選擇

有沒有現成的工具或設計模式可以幫助實現這種高效的日誌系統?例如使用 logging 模組的多處理程序功能是否可以解決這些問題?

目前研究

目前我們嘗試了一些方案,但都面臨挑戰:

  • 引入鎖來解決競態條件:測試後發現性能下降明顯,尤其是執行緒數量增多時。

import threading
import time

# Shared lock for file writing
lock = threading.Lock()

def log_to_file_with_lock(filename, thread_id):
    for i in range(5):
        with lock:  # Ensure exclusive access to the file
            with open(filename, "a") as file:
                file.write(f"Thread {thread_id} - Log entry {i}\n")
        time.sleep(0.1)  # Simulate processing delay

threads = []
filename = "log_with_lock.txt"

# Start multiple threads
for i in range(10):  # Increased number of threads to demonstrate scalability issues
    t = threading.Thread(target=log_to_file_with_lock, args=(filename, i))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("日誌記錄完成 (使用鎖)。請檢查檔案內容。")
  • 使用內存佇列進行緩衝:性能有一定提升,但需要額外處理佇列溢出和資料丟失的問題。
import queue
import threading
import time

log_queue = queue.Queue(maxsize=50)  # Bounded queue to prevent memory overflow

def producer_log_entries(thread_id):
    for i in range(5):
        try:
            log_queue.put_nowait(f"Thread {thread_id} - Log entry {i}\n")
        except queue.Full:
            print(f"Thread {thread_id}: Queue is full, dropping log entry.")
        time.sleep(0.1)

def consumer_write_logs(filename):
    while True:
        try:
            log_entry = log_queue.get(timeout=1)
            with open(filename, "a") as file:
                file.write(log_entry)
            log_queue.task_done()
        except queue.Empty:
            break  # Exit when no more logs to process

threads = []
filename = "log_with_queue.txt"

# Start the producer threads
for i in range(10):  # 10 producer threads
    t = threading.Thread(target=producer_log_entries, args=(i,))
    threads.append(t)
    t.start()

# Start the consumer thread
consumer_thread = threading.Thread(target=consumer_write_logs, args=(filename,))
consumer_thread.start()

# Wait for all producer threads to finish
for t in threads:
    t.join()

# Wait for the consumer thread to finish
log_queue.join()  # Ensure all items in the queue are processed
consumer_thread.join()

print("日誌記錄完成 (使用內存佇列)。請檢查檔案內容。")
  • 日誌分片策略:測試過讓每個執行緒寫入不同檔案,但合併過程中容易出現順序錯亂。

期待大家的回覆與討論!🙏

看更多先前的討論...收起先前的討論...
froce iT邦大師 1 級 ‧ 2024-12-25 08:48:00 檢舉
https://stackoverflow.com/questions/16929639/ensuring-python-logging-in-multiple-threads-is-thread-safe
logging應該就行了。本身就是thread safe的,只是速度我不知道。
真要速度和資料完整的話,可能要考慮別的backend例如一台redis/rabbitmq server。
https://blog.51cto.com/u_16213424/12325483
https://h3xagn.com/streaming-logs-using-rabbitmq/
Alan iT邦新手 5 級 ‧ 2024-12-25 15:57:33 檢舉
如果你需要 High Performance 你應該要考慮使用 C or C++ logging,而不是 Python。
一般來說,高並發的記錄。我會偏向用獨立檔來處理。
例如假設我要用一個記錄檔為 test.log
每條連線都會生成自已的一個獨立的檔案。 test.log.ip123
一段時間後才用檔案合拼的方式。

但這是我早期的做法。現在如果是像JAVA、PY這一類的。(PY我是還沒玩過)
本身是在伺服器運行的應用。我就會利用一下作業系統的記錄機制來處理。
而不靠程式開檔寫檔關檔。

另外一種方式,就是看語言中是否本身就有支援LOG的應用方法。
PY我不熟所以不太清楚。但像是php、.net這些。都有其log的應用寫法。
並不會再透過檔案應用來處理。
畢竟,透過檔案應用來處理的話。就得要有開檔、讀檔、寫檔、關檔等動作。
自然就會比較慢,也容易有鎖檔的問題存在。
froce iT邦大師 1 級 ‧ 2024-12-25 19:01:12 檢舉
他提到的logging就是python裡最常用的了。
LOG最後還是會持久化保存,一樣會有檔案存取問題。
現在來說用MQ,把前面他自己提到那些問題外部化,應該是不錯的方法了。

至於要高效能用啥語言...我個人會推go啦,go的goroutine 真的好用。
gbaian10 iT邦新手 5 級 ‧ 2024-12-26 05:57:16 檢舉
第三方 log 套件推薦 loguru
感謝 froce 提供的建議!logging 確實是 Python 很常用的日誌系統,對於小型專案來說很方便。用 MQ 系統外部化問題聽起來不錯!想問一下,您有試過用 RabbitMQ 或 Redis 處理高並發日誌的實作經驗嗎?這樣的架構會怎麼處理日誌的順序問題呢?

Alan 的建議蠻有意思的!C 或 C++ 的確在效能上更有優勢。不過如果我們希望保留 Python 的易用性,有沒有其他方法可以在不轉換語言的情況下提升效能呢?像是透過優化多執行緒的處理流程之類的?

浩瀚的分享很有啟發!分片後合併的方式解決了並發的問題,對於日誌順序的處理特別有幫助。作業系統的記錄機制的確很吸引人,不曉得有用過哪些作業系統內建的日誌工具呢?

感謝 gbaian 推薦 loguru!這套件在日誌格式化和使用上聽說很方便。不知道有沒有試過用它處理多執行緒的日誌需求?像是效能或順序控制方面的體驗是怎麼樣的?
gbaian10 iT邦新手 5 級 ‧ 2024-12-29 20:19:23 檢舉
loguru 有一個 enqueue 參數選項可以設定,你可以查看看這個功能能不能符合你的要求
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
3
I code so I am
iT邦高手 1 級 ‧ 2024-12-26 08:57:19
  1. 普通的File IO不支援多執行緒,可使用Python內建模組logging。
  2. logging 提供5個level的訊息等級,可在正常運行時過濾某些等級的訊息,只記錄重要訊息,反之,在除錯時可記錄所有訊息。

感謝分享,logging 的確是 Python 中處理日誌的基本解法,訊息等級的過濾功能也很方便。不過在高並發的情境下,logging 雖然是 thread-safe,但效能上可能會因鎖的影響出現瓶頸。不知道有沒有試過用 QueueHandler 或其他方法來解決這類問題呢?對於順序和效能的優化有什麼建議嗎?

logging 支援 SocketHandler,可以實現分散式系統Logging的需求,我初步測試還蠻好的,可參閱:
https://ithelp.ithome.com.tw/articles/10356041

更複雜的解決方案,可考慮Message Queue框架,如RabbitMQ 或 Redis,我曾經使用Redis,效果非常好。

1
kwkevinchan
iT邦新手 5 級 ‧ 2024-12-26 20:09:33

因為 Log 本來就是無序的東西, 尤其是在分散式系統中更是如此
不然你分散在不同 Container 的同一個 app 怎麼互相感知並決定誰先誰後寫 Log?

一般作法是如果有要將同一 process 產生的 log 串在一起
可以在一開始就產出一個 trace id
並在 log 系統中透過 fliter 掃出來解讀

kwkevin 的 trace ID 概念很棒!分散式系統中這確實是常見的解決方案。不曉得在 Python 中,有沒有哪種日誌框架可以輕鬆整合這樣的設計?或是你會建議手動擴展來支援 trace ID?

1
麻糬Mouchi
iT邦新手 4 級 ‧ 2024-12-27 00:25:33

我個人會更傾向,把log先快取在記憶體(包含產生的時間戳記),然後把可能有需要的資訊統一傳送獨立的紀錄服務。但詳細的實作過程我也不太確定該怎麼弄比較好XD

麻糬的想法很實用!快取在記憶體再集中寫入確實能解決性能瓶頸。如果加上批次寫入和時間戳記,應該能避免queue溢出的問題。不過想問問看,對於獨立紀錄服務,你覺得會使用哪種框架或架構來實現呢?

我要發表回答

立即登入回答