關於解決python線上問題的幾種有效技術


     工作后好久沒上博客園了,雖然不是很忙,但也沒學生時代閑了。今天上博客園,發現好多的文章都是年終總結,想想是不是自己也應該總結下,不過現在還沒想好,等想好了再寫吧。今天寫寫自己在工作后用到的技術干貨,爭取以后多上博客園寫寫總結吧,真是懷念學生時代啊!!!

背景

     項目組開發的游戲客戶端使用的腳本是python,服務器也是python。之所以選擇python,主要還是基於開發效率的考慮,畢竟這是腳本語言天生的優勢;其次就是有很多庫,不用自己再造輪子了。可能使用過python的同學都會認為python比較耗,運行效率不高,一個簡單的賦值語句就包含了多個對象的生成和釋放。但其實現在服務器的性能非常好,通常性能都是過剩的,所以python在服務器上高效地跑是完全沒問題的;至於客戶端,性能的瓶頸主要還是在引擎層,在一幀中最多也就20%的時間在執行腳本,超過太多說明邏輯寫的有問題或者可以分攤到多幀去執行。本文主要介紹下在使用python腳本的情況下解決線上問題的幾種有效技術,其它語言應該也有類似的技術,特別是腳本語言,這里只是做個拋磚引玉~~

熱更新(hotfix)

     這種技術主要是針對情況比較緊急,並且bug是腳本邏輯錯誤導致的。如客戶端邏輯寫的有問題導致出現exception,使得玩家某個玩法不能玩,或者是服務端某個代碼邏輯寫的有問題。這種技術實現的主要思路是(以熱更新客戶端為例):服務器將修正的代碼發送到客戶端,客戶端動態執行這段代碼來修復bug。用python來實現這個其實非常簡單,只需要在客戶端內嵌的python虛擬機中動態編譯服務端發過來的代碼,並執行這段代碼就行了。例如:現在客戶端有下面一段的代碼,這段代碼是有錯誤的。

1 #模塊test
2 
3 def not_has_a(x):
4     return hasattr(x, 'a')

      本來上面代碼是希望x對象沒有a屬性后返回True,但現在情況正好反過來了。現在我們需要寫一段代碼來修正這個問題,也就是寫一段代碼給python虛擬機執行,動態修改test模塊中not_has_a函數的定義。這個在python中很好實現的,因為python中函數也是一個對象,模塊中只是根據函數名來索引對應的函數對象的,所以我們只需要重新定義一個新的not_has_a函數對象,將模塊中根據not_has_a函數名索引的對象指向新定義的函數對象就行。具體代碼如下:

1 import test
2 
3 def not_has_a(x)
4     return not hasattr(x, 'a')
5 
6 setattr(test, 'not_has_a', not_has_a)

      最后就是讓python虛擬機執行上面的代碼。首先服務端會把上面代碼的字符串發送給客戶端,客戶端接收到代碼后編譯這段字符串,然后執行就可以了,具體代碼如下:

1 def hotfix(self, hotfix_content):
2     compiled_code = compile(hotfix_content, 'hotfix', 'exec')
3     import __main__
4     exec compiled_code in __main__.__dict__

日志系統(logging)

      如果產品上線出現問題,最快定位、發現和解決問題的有效方法就是查看日志,所以日志系統應該也必須是線上系統的組成部分之一。python在代碼中輸出日志很簡單,使用logging模塊就行,不需要自己再超輪子了,獲取模塊日志器代碼如下:

 1 def get_logger (moduleName):
 2     logger = logging.getLogger(moduleName)
 3     logger.setLevel(logging.DEBUG)
 4     ch = logging.StreamHandler()
 5     ch.setLevel(logging.DEBUG)
 6     formatter = logging.Formatter(
 7                         "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
 8     ch.setFormatter(formatter)
 9     logger.addHandler(ch)
10     return logger

有了模塊日志器,我們就可以通過日志器在代碼中輸出日志信息了。例如打印一些trace信息:

 

 1 logger = get_logger('test')
 2 try:
 3     1 / 0
 4 except:
 5     import  traceback
 6     logger.error(traceback.format_exc())
 7 logger.info('info')
 8 logger.debug('debug')
 9 logger.warning('warning')
10 logger.error('error')
11 logger.critical('critical')

遠程連接(telnet)

      雖然有了上面的日志系統后,遇到線上問題我們可以很快的定位問題,但可能有時候只有這些信息還不夠,我們還想查看出問題的地方涉及的類或者模塊的一些變量的信息。雖然也可以通過日志的方式進行查看,但每次輸出log都要把相關的變量值都輸出一來會導致log信息增多,影響系統性能;二來大部分時間這些變量的信息是沒用的,只有出現了問題才需要。python提供了code.InteractiveConsole類,它的功能類似於python的命令行交互解釋器,可以將一段python代碼字符串push到code.InteractiveConsole類實例中,code.InteractiveConsole類實例會讓python虛擬機去執行這段代碼,並返回執行結果。為了做到類似於python命令行交互解釋器那樣直接以命令行方式運行,很方便,不需要運行特殊的客戶端,我們使用telnet來連接python虛擬機,通過telnet將輸入的python代碼發送給code.InteractiveConsole類實例。這種方法需要在系統初始化的時候啟動一個類似telnet服務,用來監聽telnet客戶端的連接,並將客戶端發過來的python代碼push到code.InteractiveConsole類實例中去執行。有了這個功能后,通過telnet就可以連接上python虛擬機了,通過導入模塊可以很容易的獲得模塊全局變量的內容。如果需要獲取類實例中變量的內容,可以通過將類實例存放在模塊的全局變量中的方式來獲取。除了可以查看變量內容,還可以修改變量的內容,調用某些函數等,這在debug一些功能的時候非常的方便。

objgraph

      相較於傳統C、C++語言,Python語言不存在真正的內存泄漏問題,依靠引用計數機制及標記-清除算法,Python中的gc模塊可以很好地為代碼編寫者管理內存。但每次gc需要遍歷所有對象進行標記-清除操作,找到存在循環引用的應該被釋放的對象。這個過程是非常耗時的,所以如果頻繁的gc,將會導致客戶端發過來的請求長時間無法得到響應,這是不能容忍的。python的垃圾回收機制是標記-清除算法加分代策略,在這個主體機制下,我們能夠控制的東西不多,主要是對分代策略中的幾個參數進行控制。《python源碼剖析》對於分代策略的描述是:將系統中的所有內存塊根據其存活時間划分為不同的集合,每一個集合就稱為一個“代”,垃圾收集的頻率隨着“代”的級別的增大而減小。新對象被加入最年輕的一代(0代),當對象在一次垃圾收集過程中存活下來時,將被移往更老的一代,更老一代的收集頻率相對較低。本代是否應該進行垃圾回收由一個閾值控制,這個通過python提供的gc.set_threshold(threshold0,[, threshold1[, threshold2]])來進行設置,threshold0代表新建對象與銷毀對象的差值上限,threshold1和threshold2均代表上一代運行多少次垃圾收集算法之后,自己這一代則進行垃圾回收。Python對於threshold的默認配置是(700, 10, 10),即第0代最多700個對象,第1代最多7000個,第2代在第一次進行回收時對象最多有70000個。可以通過這個接口將閾值設置大點減少gc次數,但也不能設置太大,這樣會消耗比較多的內存,並且一次gc所消耗的時間也會更長。即使把閾值設置的比較大,如果代碼中存在不停的產生循環引用對象的話,依然會頻繁觸發gc。為了降低gc次數,我們就需要找到產生循環引用的代碼,手動解掉這些循環引用。查循環引用一個很好的工具就是objgraph,里頭有很多工具函數,比如show_most_common_types,可以看到實例最多的那些類,大部分情況下只需要看一眼就知道哪些類實例次數不正常了。還可以show_growth,看類型的增長速度。例如下面進行了10000次循環,每次循環都會創建A和B的實例,並且它們互相引用,最后通過show_most_common_types可以看到A和B的實例個數為10000。

 1 class A(object):
 2    def __init__(self):
 3       self.other = None
 4 
 5    def set_other(self, other):
 6       self.other = other
 7 
 8 class B(object):
 9    def __init__(self, other):
10       self.other = other
11 
12 if __name__ == '__main__':
13    gc.disable()
14    for i in xrange(10000):
15       a = A()
16       b = B(a)
17       a.set_other(b)
18    print objgraph.most_common_types(50)

 

 

strace和gdb神器

      對於在linux做開發的人來說,對strace和gdb肯定不陌生,因為我們經常需要用到它們,不管程序處於線上還是開發階段。當程序的行為與我們的邏輯不符合的時候(寫代碼肯定會遇到~~),特別是一些靜態語言,如c/c++,出了問題很麻煩。打log?,需要重新編譯運行,如果是線上程序基本行不通。即使是腳本語言,如果腳本導致虛擬機層出現問題,基本很難排除定位問題。這時候可以使用strace來跟蹤程序的系統調用,大致估計程序的行為。例如當你的程序阻塞在某個IO上時,但不知道具體阻塞在哪個IO的時候,可以通過strace很明確的看到程序發送的系統調用信息,獲取IO對應的fd,然后通過lsof查看這個程序的所有fd信息,就可以定位到具體阻塞在哪個IO上了。gdb神器更不用說了,debug的利器,即使是線上的程序,也可以通過attach的方式進行debug,設置斷點,查看變量,堆棧等信息。

總結

      上面的這些技術僅僅是個思想,正如開頭說的,只是個拋磚引玉,不僅限於python語言,其實還有很多其它的實用的線上技術,歡迎知道的補充哈~~~。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM