談下python微服務中的序列化場景

上一篇文章中說到了驗參,如今接着說另外一個微服務中的工程性問題,序列化。 做爲編寫業務的程序員,常被戲稱爲CRUD程序員,會增刪改查,給個if else給個for就能混碗飯吃。此話倒不假。 在微服務體系下,工做中有時會接觸多個項目,各個service與各個gateway,因爲維護人員的不一樣,雖然都是作CRUD的工做,但項目結構與寫法卻不盡相同。前端

這讓我回想起剛參加工做時加入一家很小的創業公司,一個簡單的單體應用,只有一層,flask的http request進來直接去操做DB,操做sqlalchemy的session,沒有任何的抽象,能夠說沒有任何能夠複用的業務代碼,每一個函數進來都是全新的世界,要開始從新探索,極其醜陋。(甚至能夠開玩笑地說,這樣有了bug能夠將bug只控制在那一個函數之內,不會存在遷一發而動全身的風險。。。)python

因此我以爲,將Python微服務中的常見業務代碼作到規範化而且儘量的優雅實際上是並不容易的,Python靈活歸靈活,但太靈活卻容易寫出太飄的代碼,沒法與Java的工程性相提並論。nginx

代碼結構層面其實也不用多說,上篇文章中提到過,handler,service,model三層結構,handler進行驗參後調service,service能夠調service和model,model不能夠互相調,驗參也是一個比較重要的環節。 這篇來講一下序列化問題,總結一下一個最簡單的由前端發起的http請求到gateway再由thrift rpc到service再到db總共要經歷多少次序列化和反序列化,以及對Python序列化庫marshmallow的應用實踐,搞清楚一個簡單的CRUD的請求中間到底通過了多少層的轉換以及每一次的轉換是爲了什麼。git


其實就序列化這個描述自己而言,並無說清楚從哪一種類型到哪一種類型是序列化,從而在各類場景下都會用到這個詞,經常會讓人感到混淆。 好比,Python有個pickle庫,衆所周知是用於將任意Python對象以文件的形式存入磁盤,它有的序列化函數dump是將Python對象轉化爲寫到磁盤上的二進制文件。 json是Python的內置處理json的包,用於序列化json。但這又是什麼意思呢?此時的序列化是說將Python的dict類型轉化爲符合json規範的字符串類型。 而marshmallow的序列化,倒是將業務對象轉化爲Python的dict結構,這就很容易搞亂了,在json的語境來講dict是序列化的輸入,而在這裏卻成了輸出。 這就一度搞得我很亂,一度我只能經過dump與記憶來區分各個序列化場景。但不知爲何在某層的某個節點要這樣轉一下。程序員

在定義不清的時候,去查維基的標準定義每每會頗有用。github

序列化(serialization)在計算機科學的資料處理中,是指將數據結構或物件狀態轉換成可取用格式(例如存成檔案,存於緩衝,或經由網絡中傳送),以留待後續在相同或另外一臺計算機環境中,能恢復原先狀態的過程。web

因此,一個序列化過程,並不肯定它的輸入方與輸出方到底是什麼類型,而是要根據狀況而定。個人感覺是,越接近業務自己,越接近Python語言自己,離序列化的輸入方就越近;越與業務無關、與語言無關,更接近某協議自己表示的,離序列化的輸出方向就越近。sql

下面仍是上篇文章的簡單場景,前端以http調gateway,gateway以thrift rpc調service,來分別看一個在這個請求鏈路中,對gateway與service來講,分別經歷了幾回序列化。數據庫

gateway

在gateway中,好比一個flask應用,我總結序列化與反序列化一般有如下幾個過程。json

  1. 由http請求而來的參數轉化爲Python的dict,使用flask-restful+webargs(其使用了marshmallw),將gunicorn或是nginx過來的http協議內的數據反序列化爲Python的dict結構;
  2. gateway要向service發thrift rpc請求,須要將dict結構反序列化爲thrift生成的client代碼中對應idl中定義的request對象,這裏能夠抽出一個方法在抽象層面上作一樣結構的dict到相應request的序列化,調用方法可能爲request = wrap_struct(user_info_dict, NewUserRequest)
  3. 在請求發出後,拿到rpc的response,獲得的依然是由thrift生成的代碼中由idl定義的response對象,此時可能須要一個序列化的方法將thrift的response對象序列化爲dict結構,create_res = dump_struct(createResponse); 固然,上面的兩步也可使用marshmallow來作,但在gateway層再寫一堆schema用來作這個事情真的是有些冗餘了,一個更好的辦法是使用更加抽象的方式,在反序列化時給定一個dict與相應的已經由thrift生成的request類來生成相應object,同時,能夠直接由thrift response生成相同層級結構的dict。
  4. gateway拿到數據後要給前端http的response,這是一層序列化,flask提供了jsonify將dict轉化爲相應的結構返回,flask-restful更進了一步,直接在resource函數中返回json,它會自動作這層序列化; 在工做中還見到過一些在這一層使用marshmallow,用來作什麼呢?設想,當你調用一個service取回一些數據,好比一樣是用戶的姓名這個字段,在thrift接口中定義爲name,調用其餘團隊的服務,這個不依賴於你,而同時,以前跟前端定的接口中返回用戶名爲username,相信各位有必定實踐經驗的同窗都有這種字段名轉換的經歷,在代碼中手動處理這種轉換真的有些噁心,此時使用marshmallow的dump_to參數來作就會顯得比較優雅。 若不是這種複雜的狀況,直接使用dump_struct回來的數據直接返回給前端便可。

service

如今來講一下基礎服務層的各個序列化階段,與gateway仍是有着明顯的不一樣的。 1- 拿到thrift過來的請求,使用上面提到的dump_struct將thrift response反序列化爲dict,方便進行驗參等進一步操做; 2- 在數據庫插入記錄前,使用marshmallow的load()方法反序列化,檢查各個字段是否符合規則,還見到過一種處理是直接在load()以後插入記錄,方法是使用@post_load裝飾器,在驗參成功後,直接在model中插入記錄,並返回給load的調用者,使用起來很天然;

from marshmallow import Schema, fields, validate, validates

class UserSchema(Schema):
    name = fields.Str(required=True, validate=lambda n: n)
    age = fields.Decimal(required=True, validate=lambda n: n > 18)
    location = fields.Str(required=True)
    
 @post_load
    def make_object(self, data):
        from model.user import User
        return User(**data)
複製代碼

3- service調model執行查詢,對外要先吐出一層json格式的數據,見過一些不夠clean的方式是在model的class中定義to_json()方法(實際上是返回dict對象),將model的各個字段填入dict的key與value,大體長下面這個樣子:

def to_json(self):
    return {
        'name': self.name,
        'age': self.age,
        'location': self.location
    }
複製代碼

但這個轉換用marshmallow來處理明顯會更好一些,直接調用UserSchema().dump(record).data便可獲得dict對象,這個UserSchema與request進來時load的時候是能夠複用的,能夠減小編寫上面那樣不怎麼樣的代碼。同時,上面也提到過,可使用dump_to來進行model與dict字段的轉換,很好用;

4- service獲得的dict對象返回給handler,handler使用wrap_struct(result, UserResponse)來進行反序列化,生成thrift response對象,最終給到thrift去處理。

網絡通訊中的序列化

上面提到的都是每每都是須要開發本身去處理的,但在這個過程當中,還有一些顯然存在的序列化過程,這裏簡單提一下。

uwsgi協議 正常狀況下,python服務都不多是裸奔的,它前面每每還有一層uwsgi(或gunicorn)與nginx。 uwsgi有它本身的二進制協議,nginx配置後,將http請求序列化爲uwsgi協議的傳輸二進制給到uwsgi服務,uwsgi再將此二進制反序列化爲Python對象交給你的flask應用。

thrift中的序列化 上面有不少的地方都提到將request對象交給thrift,而後呢? 做爲知名的開源rpc框架,它提供了多種序列化機制。支持xml,json等文本協議,亦可以使用thrift或是google Protobuf協議,在可讀性與性能方面,用戶能夠自由選擇。 拿到request的Python對象後,根據不一樣的序列化協議生成相應格式,最終仍是要交給socket來進行數據傳輸處理。 再由socket拿出來後進行反序列化爲thrift response。


上面提到的thrift中很重要的一點(思想)是,thrift的這些處理都是經過idl爲基礎,使用代碼生成器來生成的,開發人員只須要編寫idl文件,就能夠獲得各類語言的直接可使用的代碼。

由上所述,各類轉化真的還蠻多的,不免會有各類重複的字段定義等會出如今項目的各處(在http文檔中,在idl中,在marshmallow schema中,在db model中),參考【程序員修煉之道】中的安利,再借鑑thrift的實踐構想,我認爲在總結清楚這些常見的調用、序列化、驗參等規劃與比較好的具體實踐、代碼編寫方式後,能夠開發一個代碼生成器,由一個相似idl的語言,來生成各階段的原本要由程序員去手動編寫的代碼,從而大幅提升總體編碼效率與代碼質量。 這確定是能夠實現的,由於thrift已經實現了它。

相關文章
相關標籤/搜索