[Twisted] Part 16: Twisted Daemonologie

本文由Dave的Part 16: Twisted Daemonologie翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。

Introduction

到目前為止我們已經寫過的伺服器只能在終端視窗執行,並透過print statement輸出到螢幕。這是用於開發階段,但作為一個產品來布署服務還遠遠不足。一個作為良好產品的伺服器應該:

  1. 作為daemon程序執行,與任何終端或使用者session無關。你不會希望服務只是因為管理者登出就被關閉。
  2. 將除錯與錯誤訊息傳送到一組輪替(rotate)的log檔,或者syslog服務。
  3. 放棄過高的權限,例如在執行前切換到低權限的使用者。
  4. 在檔案中記錄它的pid,以便管理者向daemon發送信號

我們可以使用Twisted提供的twistd script來取得這些功能。但首先我們需要稍微修改我們的程式碼。

The Concepts

要理解twistd script需要學習一些Twisted的新概念,其中最重要的是Service。跟往常一樣,一些新的概念也伴隨著一些新的Interface。

ISERVICE

IService 介面定義一個由我們命名的服務,並且可以啟動或停止它。這個服務要做什麼?任何你喜歡的事情-而不是定義服務的特定函式,這個介面只需要它提供的一些通用的屬性與方法。

有兩個必要的屬性:name和running。Name屬性只是一個字串,像「fastpoetry」,如果你不想為你的服務命名就用None。Running屬性是一個布林值,如果服務已經被成功啟動則為true。

我們只簡單的談談一些IService的方法。我們將跳過一些一看就懂的方法,以及在一簡單Twisted程式中不常使用的進階方法。IService中兩個基本方法為startService與stopService:
    def startService():
        """
        Start the service.
        """

    def stopService():
        """
        Stop the service.

        @rtype: L{Deferred}
        @return: a L{Deferred} which is triggered when the service has
            finished shutting down. If shutting down is immediate, a
            value can be returned (usually, C{None}).
        """

同樣的,這些方法實際上做的將取決於涉及的服務,例如,startService方法可能會:

  • 載入一些設定資料。
  • 或初始化資料庫。
  • 或開始監聽一個埠口。
  • 或什麼也不做。

而stopService方法可能會:

  • 保存某些狀態。
  • 或關閉開啟的資料庫連接。
  • 或停止監聽一個埠口。
  • 或什麼也不做。

當我們撰寫自己的自訂服務時,我們需要合適的實現這些方法。對於一些常見的行為,像是監聽一個埠口,Twisted提供了現成的服務來替代。

注意stopService可以選擇性的回傳deferred實例,這些deferred實例被要求在服務完全關閉時觸發。這允許我們的服務在整個應用程式終止前完成自我清理(譯註:關閉一些連接、紀錄一些狀態等)。如果你的服務立即關閉,你只能回傳None而不是deferred實例(譯註:服務被不正常關閉而未完成自我清理)。

服務可以被組織成一起啟動和停止的集合。我們來看最後一個IService的方法,setServiceParent可將服務加入一個集合:
    def setServiceParent(parent):
        """
        Set the parent of the service.

        @type parent: L{IServiceCollection}
        @raise RuntimeError: Raised if the service already has a parent
            or if the service has a name and the parent already has a child
            by that name.
        """

任何服務都可以擁有父服務,這表示服務可以被組織為階層結構。這將帶領我們到下一個今天要看的介面。

ISERVICECOLLECTION

IServiceCollection介面定義了一個可以包含幾個IService物件的物件。服務集合只是具有以下方法的普通的容器類別(container class):

  • 由名字找尋服務(getServiceNamed)
  • 迭代集合中的服務(__iter__)
  • 增加一個服務到集合中(addService)
  • 從集合中移除一個服務(removeService)

注意到IserviceCollection的實現不會自動成為IService的實現,但是沒有理由一個類別不能實現這兩個接口(我們很快就會看到一個例子)。

APPLICATION

Twisted的Application不是由一個特定的介面定義的。相反的,Application物件需要實現IService與IServiceCollection介面,以及一些我們不打算介紹的介面。

Application是代表整個Twisted應用程式最高層級的服務。所有你daemon中的其他服務都會是Application物件的子代(或孫代等)。

實際實現你自己的Application是很罕見的。Twisted提供了我們今天將使用的實現。

TWISTED LOGGING

Twisted在twistd.python.log模組中有它自己的logging基礎架構。用於寫入log的基本API很簡單,所以我們只有一個簡短的範例在basic-twisted/log.py,如果你感興趣可以瀏覽Twisted模組的細節。

我們不打算展示用於安裝logging handlers的API,因為twistd script會幫我們安裝。

FastPoetry 2.0

好吧,讓我們看看一些程式碼。我們用twistd來執行已經更新過的快速詩歌伺服器。程式碼在twisted-server-3/fastpoetry.py。首先我們有詩歌協定
class PoetryProtocol(Protocol):

    def connectionMade(self):
        poem = self.factory.service.poem
        log.msg('sending %d bytes of poetry to %s'
                % (len(poem), self.transport.getPeer()))
        self.transport.write(poem)
        self.transport.loseConnection()

請注意,我們使用twisted.python.log.msg函式來記錄每個新連接,代替使用print statement。以下是工廠類別
class PoetryFactory(ServerFactory):

    protocol = PoetryProtocol

    def __init__(self, service):
        self.service = service

如你所見,詩歌不在儲存在工廠中,而是儲存在一個工廠參考的服務物件上。注意協定如何透過工廠從服務得到詩歌。最後,這裡是服務類別本身
class PoetryService(service.Service):

    def __init__(self, poetry_file):
        self.poetry_file = poetry_file

    def startService(self):
        service.Service.startService(self)
        self.poem = open(self.poetry_file).read()
        log.msg('loaded a poem from: %s' % (self.poetry_file,))

就像許多其他Interface類別一樣,Twisted提供了一個父類別讓我們可以建立我們自己的實現,並提供有幫助的預設行為。這裡我們使用twisted.application.service.Service類別來實現我們的PoetryService。

父類別提供了所有需要的方法的預設實現,所以我們只需要實現有自訂行為的方法。在這種情況下,我們只是override startService來載入詩歌檔案。注意我們仍然呼叫父類別的方法(用來為我們設定running屬性)。

來有一點值得一提。PoetryService物件不知道任何關於PoetryProtocol的細節。這個服務唯一的任務是載入詩歌並對任何需要它的物件提供對詩歌的存取。換句話說,PoetryService只關心提供詩歌的高層細節,而不是在TCP連接上發送詩歌的底層細節。所以同樣的服務可以被其他協定使用,像UDP或XML-RPC。雖然我們的簡單服務的優點相當小,但是你可以想像在更多實際服務實現中的優點。

如果這是個典型的Twisted程式,到目前為止我們看過的所有程式碼實際上都不會出現在這個檔案裡。相反的,它會出現在一些其他的模組中(也許是fastpoetry.protocol和fastpoetry.service)。但是按照我們習慣會讓這些範例可以獨立運作,我們會在單個script中包含所有我們需要的東西。

TWISTED TAC FILES

Script其餘的部分通常包含了完整的Twisted的tac檔。Tac檔是Twisted Application Configuration,它告訴Twisted如何建構應用程式。作為設定檔,它負責選擇設定(如埠號、詩歌檔位置等)來用特定的方式執行應用程式。換句話說,tac檔代表我們服務的特定布署(在這個埠口提供那首詩歌),而不是啟動任何詩歌伺服器的一般script。

如果我們在同一個主機上執行多的詩歌伺服器,每個伺服器都會有一個tac檔(所以你可以明白為什麼tac檔通常不會包含任何通用的程式碼)。在我們的範例中,tac文件設定於loopback介面的10000埠口執行提供poetry/ecstasy.txt的服務:
# configuration parameters
port = 10000
iface = 'localhost'
poetry_file = 'poetry/ecstasy.txt'

注意,twistd對這些特定變數一無所知,我們在這裡定義它們只是為了在一個地方保存所有我們的設定值。事實上,twistd在整個檔案中只關心一個變數,我們很快就會看到。接下來我們開始建構我們的應用程式:
# this will hold the services that combine to form the poetry server
top_service = service.MultiService()

我們的詩歌伺服器將由兩個服務組成,一個是上面我們定義的PoetryService,與一個Twisted內建的服務,它會建立用來提供詩歌的監聽scoket。由於這兩個服務明顯相互關聯,我們將使用MulitService將它們組合再一起,MultiService是一個實現IService和IServiceCollection的Twisted類別。

作為服務集合,MultiService將我們的兩個詩歌服務組合在一起。作為一個服務,當MultiService本身啟動時,會同時啟動兩個子服務(譯註:上述的PoetryService與Twisted內建的服務),而停止時會同時停止兩個子服務。讓我們在集合中加入一個詩歌服務:
# the poetry service holds the poem. it will load the poem when it is
# started
poetry_service = PoetryService(poetry_file)
poetry_service.setServiceParent(top_service)

這很簡單。我們只是建立了PoetryService然後用setServiceParent把它加入集合中,setServiceParent是我們從Twisted父類別繼承的方法。接著我們加入TCP監聽器:
# the tcp service connects the factory to a listening socket. it will
# create the listening socket when it is started
factory = PoetryFactory(poetry_service)
tcp_service = internet.TCPServer(port, factory, interface=iface)
tcp_service.setServiceParent(top_service)

Twisted提供TCPServer服務,用來建立連接到任意工廠(在這個案例中是我們的PoetryFactory)的TCP監聽socket。我們不直接呼叫reactor.listenTCP,因為tac檔的工作是讓我們的應用程式準備好啟動,而不是真的啟動它。TCPServer會在它被twistd啟動後建立socket。

你可能已經注意到我們沒有給出任何服務名稱。給服務命名不是必需的,只是一個可選擇的功能,如果你想在執行時可以「查詢」服務。由於我們不需要在我們的小程式中這樣做,我們不在這多費心了。

好,現在我們已經將我們兩個服務合併到一個集合中。現在我們只要建立我們的Application並把我們的集合加到Application中:
# this variable has to be named 'application'
application = service.Application("fastpoetry")

# this hooks the collection we made to the application
top_service.setServiceParent(application)

在這個script中twistd真正在乎的變數是application變數。這就是twistd將如何找到它應該要啟動的應用程式(也因此這個變數必須被命名為application)。當應用程式被啟動時,我們加入的所有服務也會被啟動。

圖34顯示了我們剛剛建構的應用程式的結構:
34.%2Bthe%2Bstructure%2Bof%2Bour%2Bfastpoetry%2Bapplication.png-Part 16: Twisted Daemonologie

圖34.我們fastpoetry應用程式的結構


RUNNING THE SERVER

讓我們執行看看我門的新伺服器。作為一個tac檔,我們需要用twistd啟動它。當然,它也只是一個普通的Python檔。所以讓我們先用Python執行它並看看會發生什麼事情:
python twisted-server-3/fastpoetry.py

如果你這樣做,你會發現什麼事情都沒發生!如我們之前所說,tac檔的工作是讓應用程式準備好啟動,而不是真的啟動它。為了提醒大家tac檔的這種特殊目的,有些人會用.tac副檔名命名它們來取代.py。但是twistd script事實上不關心副檔名。

讓我們用twistd真正的來執行我們的伺服器:
twistd --nodaemon --python twisted-server-3/fastpoetry.py

執行命令後,你應該看到一些像這樣的輸出:
2010-06-23 20:57:14-0700 [-] Log opened.
2010-06-23 20:57:14-0700 [-] twistd 10.0.0 (/usr/bin/python 2.6.5) starting up.
2010-06-23 20:57:14-0700 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2010-06-23 20:57:14-0700 [-] __builtin__.PoetryFactory starting on 10000
2010-06-23 20:57:14-0700 [-] Starting factory <__builtin__ .poetryfactory="" 0x14ae8c0="" at="" instance="">
2010-06-23 20:57:14-0700 [-] loaded a poem from: poetry/ecstasy.txt

這裡有一些事情要注意:

  1. 你可以看到Twisted logging系統的輸出,包括PoetryFactory呼叫log.msg。但是我們沒有在tac檔中安裝logger,所以Twisted必須幫我們安裝。
  2. 你還可以看到我們的兩個主要服務PoetryService 和 TCPServer啟動了。
  3. shell的輸入提示符號不會再出現,這表示我們的伺服器沒有作為daemon執行。預設的情況下,twistd會以daemon 程序執行伺服器(這是twistd存在的主要原因),但是如果你用了--nodaemon選項那麼twistd會把你的伺服器作為一般的shell 程序來執行,並直接把log輸出到標準輸出。這對於debug你的tac檔很有用。

現在透過我們其中一個用戶端或者只用netcat取回一首詩歌來測試伺服器:
netcat localhost 10000

這應該會從伺服器取回詩歌,並且你應該看到一行像這樣的新log:
2010-06-27 22:17:39-0700 [__builtin__.PoetryFactory] sending 3003 bytes of poetry to IPv4Address(TCP, '127.0.0.1', 58208)

這是來自PoetryProtocol.connectionMade呼叫log.msg。當你像伺服器送出更多請求時,你會看到每個請求的log。

現在按Ctrl+C停止伺服器。你應該會看到一些像這樣的輸出:
^C2010-06-29 21:32:59-0700 [-] Received SIGINT, shutting down.
2010-06-29 21:32:59-0700 [-] (Port 10000 Closed)
2010-06-29 21:32:59-0700 [-] Stopping factory <__builtin__ .poetryfactory="" 0x28d38c0="" at="" instance="">
2010-06-29 21:32:59-0700 [-] Main loop terminated.
2010-06-29 21:32:59-0700 [-] Server Shut Down.

正如你所看到的,Twisted並不是簡單的崩潰,而是乾淨的關閉自己,並用log訊息告訴你。注意我們兩個主要的服務也關閉自己。

好,現在再次啟動伺服器:
twistd --nodaemon --python twisted-server-3/fastpoetry.py

然後打開另一個shell並切換到twisted-intro目錄。目錄列表下應該有一個叫做twistd.pid的檔案(譯註:Windows系統不會產生這個檔案,基本上daemon是Linux系統才有的東西,當然Windows系統也可以透過其他方法建立pid檔,不過對於程式需要相當大幅度的修改,所以若你使用的是Windwos系統,記得文章中提到的pid檔都不會產生,而你的程式也不會變成背景執行)。這個檔案是由twistd建立,其包含了我們正在執行中的伺服器的process ID。試著執行這個替代命令來關閉這個伺服器(譯註:「`」是鍵盤1旁邊那個鍵):
kill `cat twistd.pid`

注意,當我們伺服器關閉時,twistd清理了process ID檔。

A REAL DAEMON

現在讓我們把伺服器作為真正的daemon程序來啟動,這更加簡單,因為這是Twisted預設的行為:
twistd --python twisted-server-3/fastpoetry.py

這次我們幾乎馬上看到shell輸入提示符出現。如果你列出目錄的內容,你會發現除了我們剛剛執行的伺服器的twistd.pid檔之外,還有一個twistd.log檔(譯註:Windows系統也不會產生這個檔案),內容是之前在shell下顯示的log內容。

啟動daemon程序時,twistd會安裝一個log handler,log的內容寫入到一個檔案中取代使用標準輸出。預設的log檔案是twistd.log,位在你執行twistd的同一個目錄,但如果你希望,你可以用--logfile選項改變儲存位置。當檔案大小超過1MB時,twistd安裝的handler也會輪替儲存log檔。

你應該能藉由列出你系統上所有程序來看到伺服器在執行。繼續透過取得另一首詩歌來測試你的伺服器。你應該會在log檔中看到每首詩歌請求新增的log內容。

由於這個伺服器不在連接到shell(或者任何程序,除了init),你不再能用Ctrl+C關閉它。作為一個真正的daemon程序,即使你登出它也會繼續執行。但我們仍然可以使用twistd.pid檔案來停止這個程序:
kill `cat twistd.pid`

而且當這種情況發生時關閉訊息會出現在log中,twistd.pid被移除,同時我們的伺服器停止執行。

了解一下其他的twistd啟動選項是個好主意。例如,你可以在啟動daemon前切換到不同的使用者或群組帳號(這是種常見的作法,丟棄你的伺服器不需要的權限,作為一種安全預防措施)。我們不打算深入額外的選項,你可以使用twistd的--help來找到它們。

The Twisted Plugin System

好,現在我們可以使用twistd啟動我們的伺服器作為真正的daemon程序。這都非常好,而且我們的「設置」檔實際上只是Python原始碼檔,這使得我們在設定上有很大的靈活性。但是我們並不總是需要那麼多的靈活性。對於我們的詩歌伺服器,我們一般只在乎一小部分選項:

  1. 要提供的詩歌。
  2. 提供詩歌的埠口。
  3. 監聽的介面。

為了幾個簡單的變數建立新的tac檔似乎相當小題大作。如果我們能指定這些值作為twistd命令列的選項,這樣會比較好。Twisted外掛(plugin)系統允許我們這樣做。

Twisted外掛程式提供了一種定義命名Application物件的方法,它有一組自訂的命令列選項,twistd可以動態的探索與執行。Twisted本身就有一組內建的外掛程式。你可以藉由執行沒有任何引數的twistd來看到它們。現在試試看執行它們,但是要在twisted-intro目錄外面。在幫助說明的部分之後,你應該可以看到一些像這樣的輸出:
    ...
    ftp                An FTP server.
    telnet             A simple, telnet-based remote debugging service.
    socks              A SOCKSv4 proxy service.
    ...

每一行顯示一個Twisted內建的外掛程式。你可以使用twistd運行任何一個。每個外掛程式還附帶一組自己的選項,你可以使用--help去探索這些選項。讓我們看看ftp外掛程式的選項是什麼:
twistd ftp --help

要注意你需要把--help放在ftp命令之後,因為你要的是ftp外掛程式的選項而不是twisted本身的。我們能像執行我們的詩歌伺服器一樣用twistd來執行ftp伺服器。但由於它是個外掛程式,我們只能透過它的名稱來執行:
twistd --nodaemon ftp --port 10001

這個命令在10001埠上以非daemon模式執行ftp外掛程式。注意twistd的--nodaemon選項位於外掛程式名稱之前,而外掛程式選項--port在外掛程式名稱之後。就像我們的詩歌伺服器一樣,你可以用Ctrl+C停止這個外掛程式。

好,讓我們來將詩歌伺服器轉變成為Twisted外掛程式。首先我們需要介紹幾個新概念。

IPLUGIN

任何Twisted外掛程式都必須實現twisted.plugin.IPlugin介面。如果你看一下這個介面的宣告,你會發現它實際上沒有指定任何方法。實現IPlugin介面對於一個外掛程式只要簡單的說「你好,我是一個外掛程式!」,這樣twistd就可以找到它。當然,為了可以泛用,它必須實現一些其他的介面,而我們很快就會來實現。

但是你怎麼知道物件是否真的實現了一個空介面? zope.interface套件包含了一個叫做implements的函式,你可以用來宣告一個特定的類別實現了一個特定的介面。我們會在我們外掛程式版本的詩歌伺服器看到這個範例。

ISERVICEMAKER

除了IPlugin之外,我們的外掛程式還會實現IServiceMaker介面。實現IServiceMaker的物件知道如何建立成為執行應用程式核心的IService。IServiceMaker指定了三個屬性與一個方法:

  1. tapname:我們外掛程式的名稱字串。「tap」代表Twisted Plugin Application。
    注意:較舊版本的Twisted還使用了叫做「tapfiles」的應用程式檔,但是這個功能已經棄用。
  2. description:外掛程式的描述,twistd將它作為幫助說明文件的一部分。
  3. options:一個描述外掛程式的命令列選項的物件。
  4. makeService:給予一組特定的命令列選項之下,建立新的IService物件的方法。

我們將在下一個版本的詩歌伺服器中看到上述這些是如何組合在一起的。

Fast Poetry 3.0

現在我們準備好看看位在twisted/plugins/fastpoetry_plugin.py中 Fast Poetry的外掛程式版本。

你可能注意到我們用與其它範例不同的方式命名這些目錄。這是因為twistd要求外掛程式檔案放在Python模組搜尋路徑中的twisted/plugins目錄。這個目錄不必是一個套件(即你不需要任何__init__.py檔),而且你可以在模組搜尋路徑上有多個twisted/plugins目錄,twistd會全部都找到。外掛程式的實際檔案名稱也無關緊要,但依據它所代表的應用程式命名它仍然是個好方法,像我們在這邊所做的。

我們外掛程式的第一個部分包含與我們tac檔相同的詩歌協定、工廠、與服務的實現。和以前一樣,這段程式碼通常都在一個單獨的模組中,但我們放在外掛程式中讓我們的範例可以獨立運作。

接下來是外掛程式命令列選項的宣告
class Options(usage.Options):

    optParameters = [
        ['port', 'p', 10000, 'The port number to listen on.'],
        ['poem', None, None, 'The file containing the poem.'],
        ['iface', None, 'localhost', 'The interface to listen on.'],
        ]

以上程式碼讓使用者可以在twistd命令列的外掛程式名稱後面放上外掛程式用的選項。我們不會在這裡深入細節,因為它會發生什麼事情應該很清楚了。現在我們來到外掛程式的主要部份,服務製造類別
class PoetryServiceMaker(object):

    implements(service.IServiceMaker, IPlugin)

    tapname = "fastpoetry"
    description = "A fast poetry service."
    options = Options

    def makeService(self, options):
        top_service = service.MultiService()

        poetry_service = PoetryService(options['poem'])
        poetry_service.setServiceParent(top_service)

        factory = PoetryFactory(poetry_service)
        tcp_service = internet.TCPServer(int(options['port']), factory,
                                         interface=options['iface'])
        tcp_service.setServiceParent(top_service)

        return top_service

這裡你可以看到如何使用zope.interface.implements函式宣告我們的類別實現IServiceMaker與 IPlugin。

你應該在我們之前的tac檔中看過makeService的程式碼。但這次我們不需要自己建立一個Application物件,我們只要建立並回傳最高層的服務,這樣我們的應用程式就可以執行了,而twistd會負責剩下的部分。注意我們如何使用options引數來取得傳給twistd的外掛程式用的命令列選項。

宣告了類別之後,剩下要做的事情只有:
service_maker = PoetryServiceMaker()

twistd script會探索我們外掛程式的實例,並使用它建構最高層的服務。與tac檔不同,我們選擇的變數名稱無關緊要。重要的是我們的物件實現了IPlugin和IServiceMaker。

現在我們已經建立我們的外掛程式了,讓我們來執行它。確保你在twisted-intro目錄中,或者twisted-intro目錄在你的python模組搜尋路徑中。然後試著執行teistd本身。你現在應該看到「fastpoetry」是外掛列表中其中一個外掛,後面有我們外掛程式檔案中的描述文字。

你還會注意到twisted/pludins目錄中出現了一個名為dropin.cache的新檔案。這個檔案由twistd所建立,用來加快後續對外掛程式的掃描速度。

現在讓我們獲得一些關於使用我們外掛程式的說明:
twistd fastpoetry --help

你應該在幫助說明中看到對於fastpoetry外掛程式選項的說明。最後,讓我們執行我們的外掛程式:
twistd fastpoetry --port 10000 --poem poetry/ecstasy.txt

這將啟動作為daemon執行的fastpoetry伺服器。和以前一樣,你應該在目前的目錄中看到twistd.pid和twistd.log檔。測試完伺服器後,你可以關閉它:
kill `cat twistd.pid`

這就是你如何製作Twisted外掛程式的方法。

Summary

在這個章節中,我們學習了如何將Twisted伺服器轉換為長時間執行的daemon。我們接觸了Twisted logging系統,以及如何使用tac設定檔或Twisted外掛程式,讓twistd將Twisted應用程式作為daemon啟動。在Part 17,我們將回到非同步程式設計的更基本的主題,並看看在Twisted中建構callbacks的另一種方法。

Suggested Exercises

  1. 修改tac檔來在另一個埠口上提供第二首詩歌。藉由使用MultiService物件保持每首詩歌的服務分開。
  2. 建立一個新tac檔來啟動詩歌代理伺服器。
  3. 修改外掛檔來接受第二個詩歌檔與提供它的第二個埠口的選項。
  4. 為詩歌代理伺服器建立一個新外掛程式。

留言