Django Channels 2.0 發布
Andrew Godwin
作者簡介
Django核心開發
程序員、演說家
私人飛機飛行員
住在美國三藩的英國人
畢業於
牛津大學莫德林學院
這個版本等了很久,不過還好,我終於可以宣布 Channels 2.0 正式發布了,配套發布的還有 Daphne 2.0 和 channels_redis 2.0(就是原來的asgi_redis)。Channels 2 是一個重構版本,完全改變了內部的運行原理和代碼結構,希望能讓項目變得更好。
Channels 2 實際上還沒有完全發布 -- 它還缺少 Channels 1 中的一些關鍵特性(如 multiplexing),以及一個深度的教程和示例代碼 -- 不過總體來說項目可以說是完成了的,已經可以為這個項目寫些東西了。
當然,我很擅長做大的重構 -- 比如我把south重構為Django Migrations,這次也是差不多的程度,保留大多數核心概念,但是代碼幾乎全部重換掉了。我不鼓勵這樣規模的重構,即便做的時候我的心裡也感到不安 -- 畢竟在Django的世界中,後向兼容是一個一直被保持的良好傳統 -- 但是我認為這次重構是必要的,這裡我就來解釋一下這樣做的原因。
Channel Layers
Channel 1 設計的一個基石就是運行了網路終止碼 -- 這允許我們將 HTTP 請求和 Websocket 幀轉換為事件 -- 而這些事件由獨立進程去處理。
這個想法看起來有點奇怪,確實很奇怪 -- 這個做法相比於 WSGI 的簡潔模型顯得太複雜了,WSGI 就只是直接將應用導入進來然後在進程內運行就行了。當時那樣做是必要的,因為當時我給自己設定了一個限制:支持 Python2.7。
這意味著我無法使用Python內置的 async 特性,也就是說我必須把 Channels 和其他的同步組件區分開來。這樣我就需要為應用代碼單獨設置一套事件循環,並從一個隊列里拉取事件 -- 就像大家知道的 runworker -- 也因此,我需要在一個單獨的線程或進程中運行一個基於 twisted 的伺服器,這樣它才不會被同步代碼所阻塞,以便可以同時處理大量的請求。
當然,在大型的基於事件的系統中,你需要一種方法在進程之間傳遞消息 -- 在 Channels 中就是 channel layer -- 所以我重用了這種機制,使得伺服器和 Worker 之間可以通信,也可以用它來廣播事件(你也可以用它做其他的任務,也就是說 Channel 2 中這個機制仍舊存在)。
這個模型運行的意外地好,考慮到 -- 並發和延遲都沒有像其他人預期的那樣糟(不過確實也沒比第一版的原型好多少)-- 但是它使得部署過程增加了兩個額外的步驟(一個 Redis/RabbitMQ 做隊列,和一個單獨的工作進程),並且擴展方案更複雜了。我應該部署更多的 Worker ?還是增加更多的 Redis 陣列?還是運行更多的 Daphne 實例?
簡化
我18個月前就意識到我可能需要重構並且簡化這個程序,但是當時我太害怕,或者說太自負,以至於無法在沒有很強的理由的情況下去做出這種不兼容的改動。對虧了幾個 Django 和 Python 社區的大神級的朋友給我了我建議,這個建議雖然我之前也知道,但是他們讓我更能下定決心:不兼容 Python2.7 沒有關係(這個決定在我2014年編寫第一版時並不像現在這樣顯而易見),去做一個完全非同步的版本。
下定決心後,我退回來重新思考和設計 Channels ,以便於它更好地適應目前的狀況 -- 不是從內部代碼的角度考慮,而是從最終用戶的角度考慮。我為 API 設計以及部署文檔打了一個草稿,並預先確定了開發者用戶能夠使用的概念模型,然後再確定實現的最佳方式是什麼。
我心裡最確定的一件事就是請求還是應該在進程內完成。如果我想讓 ASGI 成為 WSGI 優秀的繼任者 -- 我正在為此而努力 -- 那麼 ASGI 就需要和 WSGI 一樣簡潔優雅,允許服務有一個類似的介面。因此,我稱 Channels 1 為 「ASGI」,並將它一分為二:其中一部分負責將 HTTP 和 WebSocket 轉化為一系列的事件(這會是新的 ASGI 的一部分),另外一部分構建 Channel Layers 和跨事件通信(成為 Channel Layer 標準)。
這樣,我就把 ASGI 構建為了真正的、簡潔的 WSGI 的繼任者 -- 一個你可以傳入內容的可調用對象,並且能夠在另一個進程中運行應用(進一步,允許你在應用中嵌套應用,也就是支持中間件的編寫)。下面是一個 WSGI 程序的基本介面:
下面是 ASGI 應用的基本介面:
WSGI 的 environ,告訴你一個鏈接的信息,在 ASGI 中對應的是 scope。這部分很好理解。不太好理解的部分是,ASGI 不僅支持簡單的 HTTP 請求,它把 WSGI 中「調用這個函數,函數內部會調用 start_response方法,並傳入必要的數據」變成了「運行它的協程,並傳入發送和接收的可等待對象」。
新的 ASGI 標準沒有任何針對於 Django 的內容,或者 Channel Layer 的內容,或者其他的什麼 -- 它只做將網路協議轉化為事件這一件事。它甚至自帶了一個 WSGI 到 ASGI 的轉換器,以便於你能夠在 ASGI 伺服器中運行一個 WSGI 的應用。這種後向兼容是一個標準應該有的,但是在 Channels 1 中卻很難做到。
Channel Layers 仍然保留在 Channels 中,不過現在它只用於伺服器進程間的事件通信 -- 類似「聊天室中有一個新消息」,或者「這個用戶有一個通知」,然後 ASGI 應用獲得這些消息並處理他們,而不負責把反饋的消息傳遞給 Websocket。關鍵是,它們不再需要處理所有的 HTTP 請求了,這樣事情就簡單多了。
我很期望 ASGI 在 Python 世界得到廣泛使用,過去幾年,我和幾個 Python 框架以及伺服器框架的維護者討論過這個想法。現在 Channels 2 基本完工了,我要更加專註地推進這個事情了。如果社區中的人真的開始採用 ASGI,那麼我就會推薦它成為PEP的一部分。
一致化組件
新的 ASGI 標準已經足夠整潔,以至於它已經支持實現「一致化組件」了 -- 這個概念是 Simon Willison 在2009年的 DjangoCon EU 上提出來的。
這個概念的含義,簡單點說,就是讓 Channels 2 的每個部分都成為一個 ASGI 應用。每一個消費者是,路由類是,靜態文件處理器是 -- 所有的部分都是。他們都是組件化地嵌套在一起。當你想在你的 ASGI 應用中做些事情的時候,你要做的就是把它掛在根路由下面。
這樣做不僅讓你的測試和調試更容易 -- 對於每一個部分你都能使用同樣的工具 -- 它還允許你更容易地添加刪除 Django 組件,這符合我一直追尋的一個原則:包含電池,但是必要時也易於移除。
甚至 Django 的 view 系統也被實現為了一個 ASGI 應用 -- 你可以傳遞 HTTP 請求給它,然後它會像往常一樣調用一系列的中間件,最後執行 view 函數本身。但是,如果你願意,你可以讓它走不一樣的路徑。甚至在不同的 URL 下面配置不同的路徑規則。
這樣也更便於重用和復用,這很重要,這就是我喜歡的簡潔。Channels 中的 URL 路由系統就是一個 ASGI 應用,它接收一系列其他的 URL 錄音和其他的 ASGI 應用作為參數,然後再進行路由調用。更贊的是,它們全部都是非同步的。也就是說,在 Channels 2 中,整個框架堆棧都是非同步的,直到你可能編寫了一個同步的 view,或者是同步的消費者。但是,我們還有......
非同步消費者
在 Channels 2 中,消費者仍然是最基礎的代碼單元,就像 Django 中的 view 一樣,但是它增加了一些新的技巧。在 Channels 1 中消費者必須是同步的(因為消費者端整體就是同步的)。而現在消費者都變成了 ASGI 應用,因為它們就是運行在非同步伺服器進程中的mini應用,這給全非同步消費者的出現提供了基礎。
這意味,如果你想打開一個 Websocket 鏈接10秒鐘,打開就好了,不必擔心它會阻塞整個線程:
當然,如果你覺得沒必要用非同步代碼,你也可以不用 -- 畢竟編寫非同步代碼更容易出錯 -- 你還可以使用同步消費者。有趣的是,你可以在同步非同步之間輕鬆切換,感謝 asgiref 提供的新的 AsyncToSync 和 SyncToAsync 幫助工具。這些工具能把任何非同步可調用對象轉換為同步的,反之亦然。這樣,你就能夠在一個非同步消費者中使用 Django ORM 了(這是一個同步介面)。
讓你能夠編寫原生非同步代碼,使得我們可以移除一些沒有必要的特性了,比如延遲伺服器,這個特性存在的原因就是因為之前沒辦法在不阻塞進程的情況下等待一段時間。如果你願意,你還可以使用 AsyncToSync 來在一個同步消費者中運行一點 asyncio 的代碼。
還有一些重大更新,一個支持 Django URL pattern 的路由系統(包含老式 URL 和新式路徑類型),更好的 auth 集成,等等。你可以在 Channels 的文檔中查看更詳細的說明。
道歉
這種大的改動是有代價的。從用戶角度來說,這個代價基本上是最大的一種:沒有後向兼容。雖然很多概念保持了一致,但是如果你想從 Channels 1 升級上來,那麼不可避免地,你需要調整你的代碼了。
我做出這個決定真的很艱難。我曾試圖找到一種辦法來實現後向兼容,但是由於底層原理的不同,使得這種後向兼容真的難以實現,即便實現,兼容帶來的維護成本也是我無法承受的。所以,最後,我們沒能實現。Django 社區有很好的後向兼容的優良傳統,但是很抱歉,我的 Channels 沒能做到。
如果你問我為什麼 Channels 一直沒有被 Django 接納為核心組件,這可能是原因之一。非同步 Web 框架在 Python 中還是相對比較新的領域,這些框架通常都是不考慮 Python2 的。也因此,我四年前的一些假設,現在已經不成立了。
更糟糕的是,我花了很多時間克服我天生的固執,以屈服於這種改變。在公眾場合說自己做錯了,有時候真的很難,尤其是,像我這種,大多數時候都是自己一個人做項目的人。
整個過程都是我一個人完成的,沒有任何 Python 社區或者 Django 社區的其他同伴們參與。當然,在我不知所措時,他們總是會給我很有用的建議。我很感謝 Django 核心團隊的其他同伴們,給予了我足夠的信任,相信我能夠搞定這一切,並以 Django 之名推進項目向前發展。
另外一件我要做出改變的事情就是,從 Channels 2 開始,這個項目開始接受新的貢獻者。我有一個不好的習慣,傾向於自己一個人做項目,苛求對項目完美設計的完整控制,這導致了沒有足夠的人手來進行 Channels 的開發。
Channels 2.0 還有一點點工作沒有完成。剩下的這部分工作沒有做,是因為完成比完美更重要,這部分沒完成並不影響大體功能的發布。如果你有興趣參與這個項目,請直接聯繫我,我會試著幫你在未完成的工作項中找到一些你可以幫忙的部分,並且必要的話,花點時間輔導你參與到項目中來。
接下來?
說了這麼多,這個版本對於 Channels 來說意味著什麼?
首先,重構後的結構便於增加一些很好的介面,如長輪詢、支持更多的協議、增加一些基礎電池,並且可以開始寫一些介紹教程和示例項目了。我稱這個版本為軟發布。如果說在參與開源項目維護的這10年我學到的最重要的一件事是什麼,那就是儘快發布版本,即便它還不完美。
其次,還有很多外圍工作和跨項目合作要做。我希望總體改善 Python Web 生態系統,Django 是其中的一部分,跟其他項目合作還有很多事情要做。如果我們想保證 Python 在未來10年能夠在後端開發中佔據舉足輕重的位置,我們需要做出很多努力。
最後,它還意味著 Channel 1 的支持就要變少了,可能只會做一些安全相關的修復,以及數據丟失類的 bug 的修復了。我還是不會終止支持,是因為我們的 Channel 2 沒有實現後向兼容,而我們的 Channels 人手不足,除了我外,沒有人手來做 Channel 1 最後這段時間的支持了。
Channels 會被合併入 Django 核心部分么?也許,但是這取決於 Django 內部和外部的很多因素。ASGI 會最終成為 WSGI 的繼任者么?如果沒人推進肯定不會,但是我會鼎力推進這個進展的。
我會休息一段時間,然後順利從為大家帶來困擾這件事情中振作起來么?當然會。我會記得 Channel 1 曾經幫助了很多人,很多網站運行在它之上,而且這一路以來我也學到了很多東西。感謝所有幫助過我,使用過 Channels 的人。我希望未來能夠幫助更多的網站和項目,接受更加令人興奮的挑戰!
英文原文:https://www.aeracode.org/2018/02/02/channels-20/
譯者:詩書塞外


※把自己的Python包部署到PyPi
※Python imports指南
TAG:Python部落 |