谈下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已经实现了它。

相关文章
相关标签/搜索