Python網(wǎng)絡(luò)編程筆記(六):服務(wù)器架構(gòu)

網(wǎng)絡(luò)服務(wù)面臨兩個基本問題:

  1. 編碼:編寫出正確處理請求和響應(yīng)的代碼。
  2. 部署:使用守護(hù)進(jìn)程溯乒,活動日志持久化存儲,防止各種失敗或者在失敗后立即重啟豹爹。

部署

  • 服務(wù)地址:硬編碼IP裆悄、DNS解析、在服務(wù)前端配置負(fù)載均衡器帅戒。
  • 兩種部署思路:
    • 每個服務(wù)器程序編寫服務(wù)的所有功能:成為unix守護(hù)進(jìn)程或者windows服務(wù),安排系統(tǒng)級的日志崖技,支持配置文件逻住,提供啟動、關(guān)閉迎献、重啟功能瞎访。這些功能不一定自己編寫,也使用第三方庫吁恍。
    • 服務(wù)器程序只實(shí)現(xiàn)最小功能扒秸,只是一個普通的前臺程序,而不是守護(hù)進(jìn)程冀瓦。一般重環(huán)境變量中獲得配置伴奥。不過可以添加框架使思路二變成思路一的守護(hù)進(jìn)程。Paas服務(wù)即是如此翼闽,重復(fù)的功能平臺方搞定拾徙。
    • 推薦supervisord

參考:十二要素應(yīng)用宣言

單線程服務(wù)器

以一個簡單的TCP協(xié)議為例:
即使 listen()的參數(shù)大于0,第一個對話未完成時感局,第二個對話仍舊會在隊(duì)列中尼啡,只是減少了切換時建立連接的時間暂衡。

缺點(diǎn):

  • 拒絕服務(wù)攻擊
  • 嚴(yán)重浪費(fèi)CPU資源,因?yàn)樵诘却蛻舳藭r崖瞭,服務(wù)端什么都不能做狂巢。

tips:性能測試使用 trace 模塊:python -m trace -tg --ignore-dir=/usr main.py

多線程與多進(jìn)程服務(wù)器

利用操作系統(tǒng)的多路復(fù)用∈榫郏可以創(chuàng)建多個共享相同內(nèi)存的線程唧领,或者完全獨(dú)立的進(jìn)程。

優(yōu)點(diǎn):簡單寺惫,復(fù)用單線程代碼疹吃。

缺點(diǎn):

  • 服務(wù)器同時通信的客戶端數(shù)量受OS限制。
  • 即使某個客戶端空閑或者運(yùn)行緩慢西雀,仍會獨(dú)占線程或進(jìn)程萨驶。
  • 大量客戶端時,上下文切換成本很大艇肴。

模塊:threadingmultiprocessing

異步服務(wù)器

利用服務(wù)端向客戶端發(fā)送響應(yīng)后等待下一次響應(yīng)的時間腔呜。

異步(asyhchronous),表示從不停下來再悼,區(qū)別于同步(synchronized)

操作系統(tǒng)級異步

傳統(tǒng)的 select(), 后續(xù) Linux 的 poll() 和 BSD 的 epoll()

看下面這段簡單的異步代碼:精髓在于自己設(shè)計數(shù)據(jù)結(jié)構(gòu)保存客戶端狀態(tài)核畴,而不依賴操作系統(tǒng)的上下文切換。

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_async.py
# Asynchronous I/O driven directly by the poll() system call.

# zen_utils 是自己編寫的處理各種業(yè)務(wù)邏輯的包
import select, zen_utils

# 兩層循環(huán)冲九,while 不斷調(diào)用 poll(), 針對poll返回的不通事件再循環(huán)
# 為了代碼簡潔谤草,用生成器寫
def all_events_forever(poll_object):
    while True:
        for fd, event in poll_object.poll():
            yield fd, event

def serve(listener):
    # 維護(hù) sockets 字典和 address 字典
    sockets = {listener.fileno(): listener} 
    addresses = {}
    # 要接受和要發(fā)送的緩存字典。這四個字典是核心莺奸。
    bytes_received = {}
    bytes_to_send = {}

    poll_object = select.poll()
    # 監(jiān)聽套接字始終在 sockets 字典里丑孩,且狀態(tài)始終未 POLLIN
    poll_object.register(listener, select.POLLIN)

    for fd, event in all_events_forever(poll_object):
        sock = sockets[fd]

        # Socket closed: remove it from our data structures.
        # 出錯、關(guān)閉灭贷、異常等

        if event & (select.POLLHUP | select.POLLERR | select.POLLNVAL):
            address = addresses.pop(sock)
            rb = bytes_received.pop(sock, b'')
            sb = bytes_to_send.pop(sock, b'')
            if rb:
                print('Client {} sent {} but then closed'.format(address, rb))
            elif sb:
                print('Client {} closed before we sent {}'.format(address, sb))
            else:
                print('Client {} closed socket normally'.format(address))
            poll_object.unregister(fd)
            del sockets[fd]

        # New socket: add it to our data structures.
        # 監(jiān)聽套接字温学,accept

        elif sock is listener:
            sock, address = sock.accept()
            print('Accepted connection from {}'.format(address))
            sock.setblocking(False)     # force socket.timeout if we blunder
            sockets[sock.fileno()] = sock
            addresses[sock] = address
            poll_object.register(sock, select.POLLIN)

        # Incoming data: keep receiving until we see the suffix.
        # POLLIN狀態(tài),recv()

        elif event & select.POLLIN:
            more_data = sock.recv(4096)
            if not more_data:  # end-of-file
                sock.close()  # next poll() will POLLNVAL, and thus clean up
                continue
            data = bytes_received.pop(sock, b'') + more_data
            if data.endswith(b'?'):
                bytes_to_send[sock] = zen_utils.get_answer(data)
                poll_object.modify(sock, select.POLLOUT)
            else:
                bytes_received[sock] = data

        # Socket ready to send: keep sending until all bytes are delivered.
        # POLLOUT狀態(tài)甚疟,send

        elif event & select.POLLOUT:
            data = bytes_to_send.pop(sock)
            n = sock.send(data)
            if n < len(data):
                bytes_to_send[sock] = data[n:]
            else:
                poll_object.modify(sock, select.POLLIN)

if __name__ == '__main__':
    address = zen_utils.parse_command_line('low-level async server')
    listener = zen_utils.create_srv_socket(address)
    serve(listener)

回調(diào)風(fēng)格的 asynio

把 select 調(diào)用的細(xì)節(jié)隱藏起來仗岖。

通過對象實(shí)例來維護(hù)每個打開的客戶端鏈接,使用對象的方法調(diào)用览妖。

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_asyncio1.py
# Asynchronous I/O inside "asyncio" callback methods.

import asyncio, zen_utils

# 一個對象實(shí)例維護(hù)一個客戶端鏈接
class ZenServer(asyncio.Protocol):

    def connection_made(self, transport):
        self.transport = transport
        self.address = transport.get_extra_info('peername')
        self.data = b''
        print('Accepted connection from {}'.format(self.address))

    def data_received(self, data):
        self.data += data
        if self.data.endswith(b'?'):
            answer = zen_utils.get_answer(self.data)
            # 響應(yīng)通過 self.transport.write() 即可
            self.transport.write(answer)
            self.data = b''

    def connection_lost(self, exc):
        if exc:
            print('Client {} error: {}'.format(self.address, exc))
        elif self.data:
            print('Client {} sent {} but then closed'
                  .format(self.address, self.data))
        else:
            print('Client {} closed socket'.format(self.address))

if __name__ == '__main__':
    address = zen_utils.parse_command_line('asyncio server using callbacks')
    loop = asyncio.get_event_loop()
    coro = loop.create_server(ZenServer, *address)
    server = loop.run_until_complete(coro)
    print('Listening at {}'.format(address))
    try:
        loop.run_forever()
    finally:
        server.close()
        loop.close()

協(xié)程風(fēng)格的 asyncio

協(xié)程(coroutine)是一個函數(shù)轧拄,在進(jìn)行IO操作是不會阻塞,而是暫停讽膏,將控制權(quán)轉(zhuǎn)移回調(diào)用方紧帕。python支持協(xié)程的標(biāo)準(zhǔn)形式就是生成器 yield。

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_asyncio2.py
# Asynchronous I/O inside an "asyncio" coroutine.

import asyncio, zen_utils

@asyncio.coroutine
def handle_conversation(reader, writer):
    address = writer.get_extra_info('peername')
    print('Accepted connection from {}'.format(address))
    while True:
        data = b''
        while not data.endswith(b'?'):
            # 注意 yield from 
            more_data = yield from reader.read(4096)
            if not more_data:
                if data:
                    print('Client {} sent {!r} but then closed'
                          .format(address, data))
                else:
                    print('Client {} closed socket normally'.format(address))
                return
            data += more_data
        answer = zen_utils.get_answer(data)
        writer.write(answer)

if __name__ == '__main__':
    address = zen_utils.parse_command_line('asyncio server using coroutine')
    loop = asyncio.get_event_loop()
    coro = asyncio.start_server(handle_conversation, *address)
    server = loop.run_until_complete(coro)
    print('Listening at {}'.format(address))
    try:
        loop.run_forever()
    finally:
        server.close()
        loop.close()

完美方案

異步的缺點(diǎn)是:所有操作都在單個線程中完成。即使多核機(jī)器是嗜,也只會使用一個核愈案。

方案:檢查核數(shù),有幾個核鹅搪,就啟動幾個事件循環(huán)進(jìn)程站绪。在每個CPU上,使用異步(回調(diào)或者協(xié)程)方案丽柿。操作系統(tǒng)負(fù)責(zé)新建立的連接分配給某個服務(wù)器進(jìn)程恢准。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市甫题,隨后出現(xiàn)的幾起案子馁筐,更是在濱河造成了極大的恐慌,老刑警劉巖坠非,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敏沉,死亡現(xiàn)場離奇詭異,居然都是意外死亡炎码,警方通過查閱死者的電腦和手機(jī)盟迟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來潦闲,“玉大人攒菠,你說我怎么就攤上這事∏溉颍” “怎么了辖众?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長和敬。 經(jīng)常有香客問我凹炸,道長,這世上最難降的妖魔是什么概龄? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任还惠,我火速辦了婚禮饲握,結(jié)果婚禮上私杜,老公的妹妹穿的比我還像新娘。我一直安慰自己救欧,他們只是感情好衰粹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著笆怠,像睡著了一般铝耻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天瓢捉,我揣著相機(jī)與錄音频丘,去河邊找鬼。 笑死泡态,一個胖子當(dāng)著我的面吹牛搂漠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播某弦,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼桐汤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了靶壮?” 一聲冷哼從身側(cè)響起怔毛,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腾降,沒想到半個月后拣度,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜂莉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年蜡娶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片映穗。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡窖张,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蚁滋,到底是詐尸還是另有隱情宿接,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布辕录,位于F島的核電站睦霎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏走诞。R本人自食惡果不足惜副女,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蚣旱。 院中可真熱鬧碑幅,春花似錦、人聲如沸塞绿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽异吻。三九已至裹赴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棋返。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工延都, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人睛竣。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓窄潭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親酵颁。 傳聞我的和親對象是個殘疾皇子嫉你,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容