写点什么

API 标准化对 Dapr 的重要性

  • 2021 年 12 月 09 日
  • 本文字数:5304 字

    阅读完需:约 17 分钟

API标准化对Dapr的重要性

在 Dapr 中,Dapr 倡导 “面向能力编程",即:


  • Dapr API 提供了对分布式能力的抽象,并提取为标准 API

  • Dapr 的 Runtime 隔离 应用和底层组件的具体实现

而这些组件都是可替换的,可以在运行时才进行绑定。


Dapr 通过这样的方式,实现了能力和实现的解耦,并给出了一个美好的愿景:在有一个业界普遍认可并遵循的标准化 API 的基础上,用户可以自由选择编程语言开发云原生,这些云原生可以在不同的平台上运行,不被厂商和平台限制——终极目标是使得云原生应用真正具备跨云跨平台的可移植性。


理想很美好,但现实依然残酷:和 ServiceMesh 相比,Dapr 在落地时存在一个无可回避的问题——应用改造是有成本的。



从落地的角度来看, ServiceMesh 的低侵入性使得应用在迁移到 ServiceMesh 时无需太大的改动,只需要像往常一样向 sidecar 发出原生协议的请求即可,甚至在流量劫持的帮助下可以做到应用完全无感知。从工作模式上说,基于原生协议转发的 ServiceMesh 天然对旧应用友好。而 Dapr 出于对可移植性目标的追求,需要为应用提供一个标准的分布式能力抽象层来屏蔽底层分布式能力的具体实现方式,应用需要基于这个抽象层进行开发,才能获得跨云跨平台无厂商绑定等可移植性方面的收益。因此,在 Dapr 落地过程中,新应用需要基于 Dapr API 全新开发,老应用则不可避免的需要进行改造以对接 Dapr API。


API 标准化是 Dapr 成败的关键,为 Dapr 的发展建立起良性循环:



1.API 标准化

定义 Dapr API,对某一个分布式能力进行良好的抽象,覆盖日常使用的大部分场景,满足应用的常见需求


2.提供组件支持

基于标准 Dapr API,为开源产品和公有云商业产品提供支持组件,覆盖主流产品和厂商


3.具备可移植性

基于标准 Dapr API 开发的应用,可以在主流开源产品和公有云商业产品之间自行选择适合的组件,不受平台和厂商的限制


4.API 得到更多认可

可移植性为 Dapr 构建核心价值,Dapr API 得到更多的认可,逐渐成为业界的事实标准


5.更广泛的组件支持

Dapr API 越接近业界标准,就会有越多的产品和厂商愿意提供支持 Dapr API 的组件


6.可移植性更强

越来越多的组件支持,可以覆盖更多的开源产品和厂商,从而更接近 Anywhere 的愿景


理想情况下,“标准化” / “组件支持” / “可移植性” 之间的相互促进和支撑将成为 Dapr 发展源源不断的动力。反之,如果 API 标准化出现问题,则组件的支持必然受影响,大大削弱可移植性,Dapr 存在的核心价值将受到强烈挑战。


左右为难:取舍之间何去何从


既然 API 标准化如此重要,那 Dapr 该如何去定义 API 并推动其标准化呢?我们以 Dapr State API 为例,介绍在 API 定义和标准化过程中常见的问题。


State API 的基本定义


State 形式上是 key-value 存储,即状态信息被序列化为 byte[]然后以 value 的形式存储并关联到 key,当然实践中非 kv 存储也可以实现 State 的功能,比如 mysql 等关系型数据库。Dapr 的 State API 的定义非常简单明了,除了基于 key 的 CRUD 基本操作外,还有 CRUD 的批量操作,以及一个原子执行多个操作的事务操作:



上述 API 定义貌似非常简单,毕竟 kv 基本操作的语义非常容易理解。但是,一旦各种高级特性陆续加入之后,API 就会逐渐复杂:数据一致性 / 并发保护 / 过期时间 / 批量操作等。


State API 的高级特性


以 GetState() 为例,我们展开 GetStateRequest 和 GetStateResponse 这两个消息的定义,了解一下数据一致性 / 并发保护这两个高级特性:



可以看到 GetStateRequest 中的字段 key 和 GetStateResponse 中以 bytes[] 格式定义的字段 data,对应于 key-value 中的 key 和 value。GetState()的基本语义非常明显的呈现:请求中给出 key,在应答中返回对应的 value。除此之外,在 API 的设计中还有三个字段:


请求中的 consistency 字段用于数据一致性



当组件支持多副本时,consistency 字段将用于指定对数据一致性的要求,其取值有两种:eventual:(最终一致性)和 strong:(强一致性)。除了 getState()方法外,这个参数也适用于 saveState()和 deleteState() 方法。


应答中的 etag 字段用于并发,实现乐观锁



乐观锁的工作原理如上图所示,假定有三个请求同时查询同一个 key,三个应答中都会返回当前 key 的 etag(值为"10”)。当这三个线程同时进行并发修改时,在 saveState()的请求中需要设置之前获取到的 etag,第一个 save 请求将被接受然后对应 key 的 etag 将修改为"11”,而后续的两个 save 请求会因为 etag 不匹配而被拒绝。


etag 参数在 getState() 方法中返回,在 saveState() 方法中设置,每次对 key 进行写操作都要求必须修改 etag。


concurrency 参数在 saveState()方法中设置,有两个值可选:first_write(启用乐观锁) 和 last_write(无乐观锁,简单覆盖)。


请求和应答中都有的 metadata


类型定义为 map,可以方便的传递未在 API 中定义的参数,为 API 提供扩展性:即提供实现个性化功能(而不是通用功能)的扩展途径。


对批量操作的处理


State API 提供的批量操作,用于一次性操作多个 key,和应用多次调用单个操作的 API 相比,减少了多次往返的性能开销和延迟。考虑到组件原生对批量操作的支持程度,Dapr 中的批量操作的实现方式有两种:


  • 原生支持批量操作:Dapr 组件将多个 key 一起打包提交给组件的后端实现,此时批量操作的实现由后端完成,Dapr 只是简单转发了多个 key

  • 原生不支持批量操作:Dapr 组件将多次调用组件的后端实现,此时批量操作的实现由 Dapr 组件完成


展开细节看一下 Dapr 中 GetBulkState() 方法的代码实现,忽略细节代码和加密处理,只看主体逻辑:



Dapr 的 GetBulkState()方法先尝试调用组件实现的 BulkGet(),如果组件支持批量操作则直接返回结果。而当组件不支持批量操作时,GetBulkState()方法会做两个事情:


1.兜底:Dapr 通过多次调用单个 getState() 方法来模拟实现批量操作,对于应用来说是没有感知的


2.优化:如果只是简单的循环调用,当 key 比较多时延迟累加会比较大,因此 Dapr 做了一个并行查询的优化,容许启动多线程同时发起多个查询,然后将结果汇总起来后再一起返回。


Dapr 这样做的好处是:对于支持批量操作的组件可以充分发挥其功能,同时对于不支持批量操作的 组件由 Dapr 模拟出了批量操作的功能并提供了基本的性能优化。最终使得批量操作的 API 可以被所有组件都支持,从而让使用者在使用批量 API 时可以有统一的体验。


对事务操作的处理


相对于批量操作的简单处理方式,事务的支持在 Dapr 中就要麻烦的多,是目前 State API 在实现中最大的挑战,其根源在于:很多组件不支持事务!而且,事务性也无法像批量操作那边在 Dapr 侧进行简单补救。


以下是实现了 Dapr State API 的组件对事务支持的情况,其中支持事务的组件有:


  • Cosmosdb

  • Mongodb

  • Mysql

  • Postgresql

  • Redis

  • Rethinkdb

  • Sqlserver


    不支持事务的组件有:


  • Aerospike

  • Aws/dynamodb

  • Azure/blobstorage

  • Azure/tablestorage

  • Cassandra

  • Cloudstate

  • Couchbase

  • Gcp/firestore

  • Hashicorp/consul

  • hazelcase

  • memcached

  • zookeeper


因此,Dapr State API 的组件被是否支持事务分成了两大类。这些组件在开发时和运行时调用上需要就是否支持事务进行区分:


1.组件在初始化时需要指明是否支持事务



2.Dapr 在启动时进行过滤,支持事务的组件单独放在一个集合中



3.Dapr 在启动时进行过滤,支持事务的组件单独放在一个集合中


这直接导致了一个严重的后果:当用户使用 Dapr State API 时,就必须先明确自己是否会使用到事务操作,如果是,则只能选择支持事务的组件。


残酷的现实:高级特性的支持度


在前面我们讲述 API 标准化的价值时,是基于一个基本假设:在能力抽象和 API 标准化之后,各种组件都可以提供对 Dapr API 的良好实现,从而使得基于这些标准 API 开发的应用在功能得到满足的同时也可以获得可移植性。


这是一个非常美好的想法,但这个假设的成立是有前提条件的:


1.API 定义全部特性:即 API 提供的完整的能力,包括各种高级特性,从功能的角度满足用户对分布式能力的各种需求


2.所有组件都完美支持:每个组件可以完整的实现 API 抽象和标准化的这些能力,不存在功能缺失,从而保证在任意一个平台上都可以以相同的体验获取同样的功能



  • 而现实是残酷的:特性越是高级,就越难于让所有组件都支持。以 Dapr State API 为例,如上图所示从做向右的各种特性,组件的支持程度越来越差:

  • 基本操作:这些是基本的 KV 语义,CURD 操作,而且是每次操作单个 key。所有组件都支持,支持度=100%

  • 批量操作:在基本操作的基础上增加对多个 key 同时操作的支持,部分组件不能原生支持,但是 Dapr 可以在单个的基本操作上模拟出批量操作来进行弥补,因此也可以视为都支持,支持度~=100%

  • 过期时间:可选特性,设置过期时间可以让 key 在该时间之后自动被清理,有部分组件原生支持这个特性,但也有部分组件无法支持。这是一个可选特性,Dapr 的设计是通过在请求中提供名为 TtlInSeconds 的 metadata 来指定。

  • 并发支持:乐观锁机制,要求组件为每个 key 提供一个 etag 字段(或者称为 version),每次修改时都要比对 etag,修改后要更新 etag。这个特性也是只有部分组件支持,需要在组件支持特性中明确指出是否支持。

  • 数据一致性:容许在请求中提供参数指定操作对数据一致性的要求,可以是强一致性或最终一致性,组件如果支持就可以依照这个参数的指示进行操作。这个特性同样只有部分组件支持

  • 事务:提供对多个写操作的原子性支持,只有部分组件支持(按照前面列出来的组件支持情况,大概是 40%),需要在组件支持特性中明确指出是否支持。


但实际上,在 API 定义和标准化的过程中,我们不得不面对这样一个残酷的现实:API 定义全部特性 和 所有组件都完美支持 无法同时满足!


这导致在定义 Dapr API 时不得不面对这么一个痛苦的抉择:向左?还是向右?



  • 向左,只定义基本特性,最终得到的 API 倾向于功能最小集

优点:所有组件都支持,可移植性好     缺点:功能有限,很可能不满足需求


  • 向右,定义各种高级特性,最终得到的 API 倾向于功能最大集

优点:功能齐全,很好的满足需求     缺点:组件只提供部分支持,可移植性差


API 定义的核心挑战


Dapr API 定义的核心挑战在于:功能丰富性和组件支持度难于兼顾。


如下图所示,当 API 定义的功能越丰富时,组件的支持度越差,越来越多的组件出现无法支持某个定义的高级特性,导致可移植性下降:


在 Dapr 现有的设计中,为了在标准 API 定义之外提供扩展功能,引入请求级别的 metadata 来进行自定义扩展:Dapr 现有的各种 API,包括上面我们详细介绍的 State API,基本都经历过这样一个流程:


  • 每个 Dapr 构建块的 API 在初始创建时,通常会从基本功能开始,相对偏左侧

  • 随着时间的推移,为了满足更多场景下的用户需求,会向右移动,在 API 中增加新功能

  • 新增的功能可能会导致部分组件无法提供支持,损害可移植性


因此 Dapr API 在定义和后续演进时需要做权衡和取舍:


  • 不能过于保守:太靠近左侧,虽然可移植性得以体现,但功能的缺失会影响使用

  • 不能过于激进:太靠近右侧,虽然功能非常齐备,但是组件的支持度会变差,影响可移植性


Metadata 的引入和实践


在 Dapr 现有的设计中,为了在标准 API 定义之外提供扩展功能,引入请求级别的 metadata 来进行自定义扩展:



metadata 字段的类型定位为 map,可以方便的携带任意的 key-value,在不改变 API 定义的情况下,组件和使用者可以约定在请求级别的 metadata 中通过传递某些参数来使用更多的底层能力。


下图是在阿里云在内部落地 Dapr 时,对 Dapr State API 的各种 metadata 自定义扩展:

注意:State API 中,expire 的功能在通过名为 ttlInSeconds 的 metadata 来实现,而没有直接在 getStateRequest 中定义固定字段。


metadata 的引入解决了 API 功能不足的问题,但是也造成了另外一个严重问题:破坏可移植性。


  1. 可移植性是 Dapr 的核心价值:因此定义 Dapr API 时应尽量满足可移植性的诉求,API 设计时会偏功能最小集


  2. 为了提供最大限度的可移植性,设计时往往会倾向于从功能最小集出发,如下图所示:


3.出现功能缺失功能最小集合意味着 Dapr API 只定义基本功能,自然会导致缺乏各种高级特性,落地时会遇到无法满足应用需求的情况


4.进行自定义扩展为了满足需求,使用请求级别的 metadata 进行自定义扩展,提供 Dapr API 没有定义的功能


5.优点:满足功能需求 metadata 的使用扩展了功能,使得底层组件的能力得以释放


6.缺点:严重破坏可移植性自定义扩展越多,在迁移到其他组件时可能丢失的功能就越多,可移植性就越差



从图上看,当从可移植性为出发点进行 API 设计时,由于功能缺失迫使引入 metadata 进行自定义扩展,在解决功能问题的同时,造成了可移植性的严重破坏。从而偏离了我们的初衷,也造成整个 API 设计和落地打磨的流程无法形成闭环,无法建立良性循环。


路阻且长:但行好事莫问前程


虽然 Dapr 还很稚嫩,虽然多运行时(Mecha)的理论还在早期实践的过程中,但我坚信 Dapr 作为多运行时理论的第一个实践项目是符合云原生的大方向,Dapr 能为云原生应用带来巨大的价值。而从产品形态来说,目前 Dapr 是走在云原生社区的前面,作为 Dapr 的早期实践者,可以很骄傲的说:我们是云原生的开拓者,我们正在创造云原生新的历史。


而 Dapr API 是 Dapr 成败的关键之一,从云原生发展的角度未来也需要这么一个通用的分布式能力的 API 标准,诚然目前的 Dapr API 需要在不断实践中补充和完善,而且这个过程注定会很艰难,就像前面这个迟迟未能顺产的 Configuration API。


用户头像

还未添加个人签名 2019.03.12 加入

还未添加个人简介

评论

发布
暂无评论
API标准化对Dapr的重要性