美文网首页微服务架构和实践
《Designing Data-Intensive Applic

《Designing Data-Intensive Applic

作者: 赤子心_d709 | 来源:发表于2018-01-04 17:05 被阅读104次

    1.引言

    第一章的时候讲了 进化性, 就是应对不断变化的需求
    如果说数据模型变了,我们的表以及业务代码也要相应变化,比如说多了一个字段等等
    对于关系型数据库,是有schema的,通过修改schema来应对变化的结构(如alter语句)
    对于文档性,schema less的(也是schema on read)的,需要自行兼容新老数据结构

    当数据变化的时候,要考虑两个问题

    向前兼容性:
      新的代码能够解析旧的数据
    向后兼容性:
      老的代码能够读出新的数据(忽略掉新的属性)
    

    向后兼容更难一点

    这一张讲解encoding data的几种格式,包含json,xml,proto buff,thrift和avro
    讲解他们如何处理schema的变化,保证新旧数据的共存,并且讲解在REST和RPC中的作用

    2.数据的format以及序列化

    数据往往有两种表现形式:
    1.内存中,用object,structs,list等记录
    2.bytes串,当需要把数据进行持久化或网络发送的时候

    当两种表现形式进行转换的时候,就需要encoding技术(java中称为序列化)
    有很多第三方的包提供这个功能,但是往往有下面几个问题

    1.针对特定的语言:encoding时用A语言,那么decoding的时候往往也要A语言
    2.安全性:如果encode和decode方式泄露了很不安全
    3.考虑兼容性:向前向后兼容
    4.效率(encode和decode时间,序列化出来的bytes长度等)
    

    2.1 json,xml以及二进制变种

    json,xml比较常见
    xml因为过于详细,和不必要的负责而被批评。
    json由于支持浏览器(js的一部分)而流行。
    但是依旧有一些问题

    • 编码数字时有歧义,xml无法区分String还是Number
    • json和xml支持Unicode字符集但是不支持binary string,使得data size变大
    • json和xml有一些可选的schema,但是太强大太复杂了

    2.1.1 二进制变种

    json没有xml那么详细,但是相对二进制format,仍然用了很多空间,
    有一些JSON变种如 MessagePack,BSON,BJSON等
    还有些XML变种如WBXML等
    比如json串如下

    {
    "userName": "Martin",
    "favoriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
    }
    

    用MessagePack表示如下


    image.png

    从json的81字节到MessagePack的66字节

    是否值得为了这么小的空间压缩而失去可读性,尚不明确


    2.2 Thrift和protocal buffers

    Thrift是Facebook开发的,pbf是谷歌开发的,都需要interface definition language(IDL),
    PBF的例子如下

    message Person {
      required string user_name = 1;
      optional int64 favorite_number = 2;
      repeated string interests = 3;
    }
    

    都有一个code生成器,根据schema的定义,用不同的语言生成classes来实现这个schema
    比如Thrift的CompactProtocol如下,

    image.png

    和上面MessagePack的区别就是不包含field names,如“userName”,“favoriteNumber”这些内容
    只用了field tags(1,2,3)

    2.2.1 field tags与schema的变动

    之前说schema不可避免的会有改动,称为schema evolution,Thrift和PBF如何处理前后的兼容性呢?

    改字段的名字:IDL里面用field tags替代了field names,因此只要filed tags不变,decode方就可以正常处理
    
    添加field:旧的代码忽略掉它不认识的field tags即可(向后兼容),但是要求这个field在IDL定义中不能使required
    否则解析旧数据出错。新的代码依然能够处理旧的field,因为旧的tags能够识别(向前兼容)
    删除field:同上,旧代码读新数据时忽略掉不认识的tag,新代码读旧数据时处理它知道的tags即可
    但是要求删掉的fields的tags以后也不能再用
    

    2.2.2 数据类型与schema的变动

    如果改变field的数据类型怎么办,详情看文档,但是有损失精度以及截断的风险
    比如32位改成了64位,旧的代码不一定能处理新的数据


    2.3 Avro

    是Hadoop的一个子项目,IDL如下

    record Person {
      string userName;
      union { null, long } favoriteNumber = null;
      array<string> interests;
    }
    

    序列化后,内容如下


    Avro只有32个字节

    首先注意IDL里面没有tag number
    序列化后内容没有标识是哪一个字段,数据类型是什么

    这样的话,要求数据decode时用的schema和写时的schema完全一样才行。
    那么如何处理schema改动的问题呢?


    2.3.1 写schema和读schema

    encode的时候会有一个写schema
    decode的时候会有一个读schema
    Avro不要求两个一样,只要两个兼容即可
    Avro的包会解决两个schema的差异,通过比较两个schema
    解决方法如下


    Avro解决writer和reader的差异

    如果两个schema顺序不同,会通过匹配field name来解决
    如果read schema遇到了只在write schema出现的field,那么会忽略掉
    如果read schema遇到了自己希望有但是write schema未出现的field,那么填充默认值


    2.3.2 schema改动规则

    前后的兼容性要求添加或者删除field时,field需要有一个默认值。
    其他一些改动只有向前兼容不具有向后兼容:
    如改动field name则只满足向前兼容(类似于field name有一个alias)
    等等


    2.3.3 read schema怎么知道write schema是怎样的

    分几个场景

    有很多记录的大文件:如hadoop,所有的记录的schema都是一样的,因此直接把write schema记录在file中即可
    有不同write schema写记录的数据库:每一个记录新加一个write schema version number,加一个write schema的表,这样reader获取record时通过version找到对应的write schema
    网络发送记录:双方协商好schema,之后进行RPC调用
    

    2.3.4 动态生成schema

    Avro对于动态变化的schema更友好,直接生成一个新的schema就好了
    而Thrift和PBF面对动态变化的schema的时候,则需要手动控制好field tags(个人感觉类似于自增主键的管理)


    2.3.5 代码生成以及动态类型语言

    Thrift和PBF依赖代码生成,即根据schema生成对应的class的代码,在静态类性语言中有效,如Java,C++等。默认不支持动态类型语言
    Avro则既支持静态语言,也支持动态类型语言(python,js等)


    2.4 schema的优点

    相比JSON,XML来说,上面的Thrift,PBF,Avro有很多优点:

    • 数据压缩的更小
    • schema能够表示文档,记录的一种组织形式
    • 记录下schema能够更好的保证前后兼容性
    • 对于静态类型语言,schema能够generate code的特性非常有用,能够进行编辑检查

    3. 数据流的模式

    上面讲了数据的encode和decode,那么是谁进行encode和decode呢,数据的发送方和接收方,是怎样进行数据的传输的呢,最主要有下面几种

    数据库
    服务调用(REST或者RPC)
    异步消息处理
    

    3.1 数据库的数据流

    写入db的进程对数据进行encode,从db读数据的进程对数据进行decode
    前后兼容性依然需要保证:新老代码都要能够处理新老schema的数据
    另外在业务层的代码也要注意保证兼容性
    比如


    旧的代码执行toJson之后写入db,导致新的字段photoURL被冲掉了

    不同时期写下的记录

    比如有5s前写下的记录,有5年前写的记录,但是两者的encoding是不一样的(因为不同的schema)
    重启应用程序很快,但是重启db也不会改变它们的encoding,重复rewrite。

    Rewrite和migrate都是可行的,但是是很重的操作。
    一般是加一些有默认值的列进来,而不修改原有的数据。当旧的数据被读的时候,就塞入默认值。


    3.2 REST和RPC的数据流

    当网络通信时,最常见的是client和server交互。
    server把API暴露在网络上,称为服务,并且client能够连接上server去请求API。
    当然,server自己也可以作为client去调用其他服务,比如数据库
    某种意义上,服务就类似数据库,允许client提交和查询数据,不过数据库只允许特定的sql语言,而应用的server方则暴露API。
    所以同样的,client和server之间的数据encode和decode也要用兼容性。


    3.2.1 web服务

    把http作为底层协议调用的服务称为web服务
    主要有两种形式,SOAP和REST
    REST不是一个协议,是一个基于HTTP协议上的设计思路
    强调用简单的数据形式,用URL来标示资源,以及HTTP的特性来完成控制认证等。相对SOAP更加流行

    而SOAP就是用基于XML的如WSDL语言描述,在静态类型语言中有用,但是在动态类型语言中没那么有用。
    并且不是人可读的,而且人为构建太复杂。
    两者在这里不详细介绍了


    3.2.2 RPC遇到的问题

    RPC定义参照refer
    目前网络请求相对本地调用,有这些问题

    1.本地调用要么成功要么失败,而通过网络请求,要考虑丢包,延迟,超时等等。必须考虑这些情况,比如失败重试
    2.本地调用要么返回结果,要么抛异常,要么死循环。而网络请求则可能由于超时而无法返回。此时根本不知道发生了什么,不知道请求是否发送过去了
    3.网络传输,重试的时候,要求要有幂等性,即第一次请求和第二次同样的请求,得到的结果应该是一样的。而本地操作不用考虑这个问题。
    4.网络调用要考虑网路情况,可能快可能慢。本地调用则更快
    5.调用方法传递参数时,如果是基础类型还好,如果是自定义的很大的object,就会变得复杂(应该是考虑序列化之类的问题)
    6.client和server如果用不同语言实现,那么数据类型从一个语言到另外一个语言可能会有问题,比如js的数字可以比2^53大
    

    3.2.3 目前RPC的方向

    尽管上述诸多问题,我们还是会用RPC。比如Thrift和Avro支持RPC,谷歌研发了gRPC等等。
    有些框架提供了 服务发现功能,即允许client找到在哪个ip,port上有哪个服务。

    而REST方便调试,被主流语言,平台支持,有一些的工具
    因此公共API用RESTful
    而同组织的内部服务调用,则用RPC好


    3.2.4 数据encoding以及RPC的进化

    对于进化性来说,RPC需要允许client和server进行改变并且独立部署。
    合理假设RPC服务都是server先升级,然后client升级。
    因此,只要考虑 request的向前兼容性(server处理)以及response的向后兼容性(client处理)即可

    1.Thrift,gRPC(PBF),Avro的RPC都能根据encoding规则达到兼容性
    2.SOAP用xml schema,能够达到兼容性但是有一些陷阱
    3.RESTful API常用json串进行回复,用json或者URI-encoded/form-encoded进行请求。如果请求加入了可选册数,或者回复加入了新字段都被认为是变动
    

    RPC常用于组织内部,使得服务兼容性更困难。
    因为无法强制client升级,要一直保持兼容性。
    如果有些破坏兼容性的改动,那么往往会终止对某些旧版本client的支持。


    3.3 消息传递数据流

    这里简单看看异步消息传递系统(就是消息队列)

    它是介于RPC和db之间
    像RPC是client发送请求(消息)到另一个处理进程
    像DB是消息会间接的暂时存储在broker中(中间件)
    

    broker相对RPC来说有以下优点

    1.如果接收方挂了,broker还可以作为buffer
    2.自动重发机制处理进程挂掉,避免消息遗失
    3.不需要直接知道ip端口(部署在云上方便)
    4.允许消息发送给多个接收方
    5.逻辑上把sender和receiver分开,两者可以互相不知道
    

    另外一个区别在于消息队列是单向的,发送方不会接收到接收方的回复,所以消息是异步处理的


    3.3.1 message broker

    最近很多开源实现如RabbitMQ,ActiveMQ以及Kafka等很流行。
    通常,broker工作机制如下

    一个进程发送消息到特定的queue或者topic。
    然后broker保证消息发送到订阅了这个queue或者topic的receiver中。
    

    当然也要考虑到兼容性问题,这里不展开对MQ的讨论


    3.3.2 分布式actor框架

    这个直接参照refer就好,书中简单介绍了下


    4.总结

    考虑到数据在网络传输,数据有序列化的需求。然后为了应对数据格式的变化(进化性),数据的encode和decode方需要有前后的兼容性。
    讲解了几种序列化(Avro,Proto buff, Thrift)的方法,以及应对兼容性的处理.
    再讨论了数据传输的几个场景(数据流):从DB,RPC以及REST API以及消息队列三个场景出发,介绍了背景以及编码以及兼容性的

    5.思考

    RPC和REST的思考

    书中说REST更适合public API而RPC适合公司内部调用(3.2.3)
    另外对RPC的兼容性的总结讲的太好了,公司内部server先升级再client升级,而如果作为public API,则往往要考虑client不能升级,终止某些client版本的服务等等(3.2.4)

    数据流上

    思考数据是如何encode和decode,怎样保证兼容性的,满足变化的需求

    6.问题

    讲Avro最后对动静态语言的支持(2.3.5),这一小部分不是非常理解,没有深入研究
    对于REST我还不是很熟悉,后面再抽空看
    

    7.refer

    SOAP和RESTful
    https://zh.wikipedia.org/wiki/SOAP
    https://zh.wikipedia.org/wiki/%E5%85%B7%E8%B1%A1%E7%8A%B6%E6%80%81%E4%BC%A0%E8%BE%93

    RPC
    https://zh.wikipedia.org/wiki/%E9%81%A0%E7%A8%8B%E9%81%8E%E7%A8%8B%E8%AA%BF%E7%94%A8

    actor framework 参与者模式
    https://zh.wikipedia.org/wiki/%E5%8F%83%E8%88%87%E8%80%85%E6%A8%A1%E5%BC%8F

    相关文章

      网友评论

        本文标题:《Designing Data-Intensive Applic

        本文链接:https://www.haomeiwen.com/subject/hiofnxtx.html