这篇博文其实是受知乎上的一个问题“技术部门Leader是不是一定要技术大牛担任?”触动有感而发。在我个人理解来看如果大家都是从架构师-tech leader这条线发展的职业来讲都需要有一定的technical roadmap的规划能力。一个好的tech leader不但有团队成员技术成长管理能力(引导)也需要一些类似于博士的research的能力(业界未有但是产品规划未来需求)。
    这篇文章来源于“Cloudstate-下一代serverless 框架

    为什么是Cloudstate?
    TL;DR

    • 从开发到生产,无服务器的开发人员体验是革命性的,并将在未来的云计算中占据主导地位

    FaaS以其短暂、无状态和短暂的功能仅仅是无服务器开发体验的第一步和实现。
    FaaS非常适合处理密集的、可并行化的工作负载,可以将数据从A移动到B,从而提供丰富的内容和转换。但是 它在很好地处理用例方面受到了限制和限制,这使得传统的应用程序开发和分布式系统协议的实现变得困难和低效。

    • 我们需要的是下一代无服务器平台(Serverless 2.0)和通用应用程序开发(如微服务、流媒体管道、AI/ML等)的编程模型。

      它允许我们实现常见的用例,例如:购物车、用户会话、事务、ML模型培训、低延迟预测服务、作业调度等等。

    • 缺少的是对长时间运行虚拟有状态服务的支持、以可伸缩和可用的方式管理分布式状态的方法,以及为作业选择正确一致性模型的选项。

    • 我们通过在Knative/Kubernetes、gRPC和Akka(集群、持久性等)上构建下一代无服务器,使用丰富的客户端api(JavaScript、Go、Python、Java、Scala、PHP等)来解决这些问题

    当前无服务器实现的局限性
    **
    无服务器对不同的人意味着不同的东西。许多人认为它与功能即服务(FaaS)一样。我们看到的不仅仅是:PaaS本身的一个新类别,重点是开发人员的经验,以及支持应用程序的整个生命周期,而不仅仅是它最新版本的编程API。
    Adzic等人的论文“无服务器计算:经济和架构影响”中的定义。描绘了更广阔的前景:
    “‘无服务器’是指新一代平台即服务产品,其中基础设施提供商负责接收和响应客户端请求、容量规划、任务调度和操作监视。开发人员只需要担心处理客户机请求的逻辑。”

    今天的无服务器框架是一个很好的无状态服务平台,专注于从1-10000个请求扩展到零,并且以非常经济高效的方式(无事件=无成本)在可扩展性方面做得非常出色。它简化了规模的交付和操作的简单性。
    无服务器的当前体现,即所谓的功能即服务(FaaS),是一种典型的数据传送架构,我们将数据移动到代码中,而不是反过来。它非常适合处理密集的所谓令人尴尬的并行工作负载,将数据从A移动到B,从而提供丰富和转换。

    然而,我们相信无服务器比FaaS更重要(这只是旅程的第一步)。这与具体的实现无关。相反,这一切都是关于开发人员的经验——一种构建和运行应用程序的新方法,现在是我们扩展其范围和支持的用例的时候了。
    FaaS的一个限制是它的功能是短暂的、无状态的和短暂的[^1]。这使得构建以数据为中心的通用云本机应用程序成为一个问题,因为在性能、延迟和吞吐量方面,损失计算上下文(引用位置)和被迫一次又一次地从后端存储加载和存储状态代价太高。
    然而(FaaS)的另一个限制是,函数通常没有直接的可寻址性,这意味着它们不能使用点对点通信彼此直接通信,而总是需要使用发布-订阅(publish-subscribe),通过一些缓慢而昂贵的存储介质传递所有数据。一种模型,它可以很好地用于事件驱动的用例,但是对于解决一般用途的分布式计算问题来说,它的延迟太高了[^2]。

    对有状态无服务器计算的需求
    如果Serverless在概念上是关于如何从等式中移除人类,并通过推理解决开发人员在生产中遇到的最困难的问题,然后,它们需要具有丰富且易于理解的语义的声明性api和高级抽象(除了像函数这样的低级原语之外),用于处理永无止境的数据流、管理复杂的分布式数据工作流,以及以可靠、弹性、可伸缩和可执行的方式管理分布式状态。

    我们需要支持的是:

    • 有状态的长寿命虚拟可寻址组件。[^3]
    • 正如Hellerstein等人所讨论的:“如果平台为创建关联性(例如移动数据)支付了成本,那么它应该在多个请求之间收回该成本。这就激发了程序员建立软件代理的能力,称之为功能、参与者、服务等,这些软件代理随着时间的推移在云中以已知的身份持续存在。”
    • 用于协调和通信模式的更广泛的选项(不包括代理上基于事件的pub-sub),包括使用常见模式(如点对点、广播、聚合、合并、洗牌等)的细粒度状态共享。

      1. 正如[Jonas等人总结](https://arxiv.org/pdf/1902.03383.pdf)的那样:“这一限制还表明,无服务器计算的新变体可能值得探索,例如命名函数实例并允许直接寻址以访问其内部状态(例如,Actors As a Service)”。
    • 以持久或短暂的方式可靠地按比例管理分布式状态的工具,其一致性选项从强一致性到最终一致性和因果一致性[^4],以及在保持逻辑独立的同时物理地共同定位代码和数据的方法。

      1. 正如[Hellerstein等人所讨论](https://blog.acolyer.org/2019/01/14/serverless-computing-one-step-forward-two-steps-back/)的:“程序编程的顺序隐喻不会扩展到云。开发人员需要能够鼓励代码在小型、细粒度单元(包括数据和计算单元)中正确工作的语言,这些单元可以轻松地跨时间和空间移动。”
    • 有状态函数的智能自适应放置在物理上共同定位代码和数据,同时保持逻辑上的独立性[^9]。

    • 端到端的正确性和一致性能够对流管道和属性“^5”进行推理,并保证它作为一个整体。
    • 启动时间、通信、协调和数据持久存储/访问方面的可预测性能、延迟和吞吐量。

    端到端的正确性、一致性和安全性对于不同的服务意味着不同的东西。它完全依赖于用例,不能完全外包给基础设施。下一代无服务器实现需要提供编程模型和整体的开发人员体验,与维护这些属性的底层基础设施协同工作,而不会继续忽略最困难、也是最重要的问题:如何在云中可靠地按比例管理数据。

    了解Cloudstate
    Cloudstate是定义规范、协议和参考实现的标准工作,旨在将无服务器及其开发人员的经验扩展到通用应用程序开发。

    Cloudstate建立并扩展了传统的无状态FaaS模型,增加了对长寿命可寻址有状态服务的支持,以及通过gRPC访问映射的格式良好的数据的方法,同时根据数据的性质以及数据的处理、管理和存储方式,允许从强一致性到最终一致性的一系列不同的一致性模型。
    定义数据模型,选择其一致性模式和解析方法,并通过格式良好的gRPC命令和读取通道协议访问数据、以数据为中心的操作、流管道和事件。

    有状态函数与CRUD不兼容
    我们需要重新考虑在无服务器中使用CRUD。一般来说,CRUD意味着无限制的数据库访问,而且它太广泛和开放,无法在无服务器环境中有效地使用(或任何一般的云开发)。
    faas_with_crud.png

    无约束的数据库访问意味着用户函数本身需要管理有关数据访问和存储的细节,因此您将所有操作问题从无服务器框架转移到用户函数中。现在框架很难知道每个访问的意图。例如:

    • 这个操作是读还是写?
    • 可以缓存吗?
    • 一致性是可以放松的,还是需要很强的一致性?
    • 在部分故障期间可以继续操作吗?

    相反,如果我们了解这些属性,我们就可以自动做出更好的决定。例如:

    • 写操作很快,读操作很慢:向数据库添加更多内存
    • 读取不可变值:添加缓存
    • 写入必须可序列化:添加分片,每个实体一个写入程序。

    抽象状态
    我们都知道约束可以是自由的,这对无服务器和其他任何东西都一样适用。事实上,Serverless成功的原因之一是它具有如此有限的开发经验,这使得作为开发人员,您可以专注于本质:功能的业务逻辑。例如,Serverless有一个很好的模型来抽象通信,其中所有通信都转换为接收和发送事件。
    我们问自己的问题是:我们能用同样的方式抽象出状态吗?为函数提供状态输入和状态输出的干净统一的抽象。
    abstract_over_state.png

    这将允许框架代表功能管理持久状态,在整个系统中全面监控和管理它,并做出更智能的决策。
    无约束CRUD在这个模型中不起作用,因为我们不能将整个数据集传入和传出函数。我们需要的是具有受限输入/输出协议的数据存储模式。属于这一类的模式包括关键值、事件源和CRDTs。
    事件溯源模式中,state In是事件日志,state out是处理命令后的任何新持久化事件。

    data_model_event_sourcing.png

    CRDTs中,state In是增量和/或状态更新的流,state out是增量和/或状态更新的流。

    abstract_over_state.png

    在键值中,state out是键值中的键和状态。
    虽然大多数开发人员都使用过关键价值存储,但事件源和CRDTs可能有点陌生。有趣的是,它们非常适合事件驱动的模型,同时处于状态一致性谱的相反侧,前者提供强(ACID)一致性(通过事件日志),后者提供最终/因果一致性。综合起来,它们为我们提供了一个以一致方式管理分布式状态的真正广泛的选项,允许您为您的特定用例和数据集选择最佳模型[^7]。

    高层设计
    Cloudstate引用实现构建在Kubernetes、Knative、Graal VM、gRPC和Akka之上,并为不同的语言提供了越来越多的客户端API库。入站和出站通信始终通过gRPC通道上的sidecars,使用受约束且定义良好的协议,在该协议中,用户定义命令输入、事件输入、命令应答输出和事件输出。通过gRPC进行通信允许以不同的语言(JavaScript、Java、Go、Scala、Python等)实现用户代码。

    serving_stateful_functions.png

    每个有状态服务都有一个由持久的Akka参与者组成的Akka集群(支持多个数据模型、存储技术和数据库)。然而,通过一组将用户代码桥接到后端状态和集群管理的sidecars,用户可以避免这些复杂性。
    powered_by_akka_sidecars.png

    管理分布式状态不仅仅是以可靠的方式将数据从A推送到B。它是关于选择一个模型来反映真实世界中数据的使用情况,以及它在可用一致性上的收敛性,而不是人为强制的一致性。能够让数据跨越集群、数据中心、可用区和大陆,并保持有用的一致状态是Kubernetes和Akka的结合所擅长的。此外,可以通过命令通道嵌入在状态集群中更好地执行的重复工作,或者需要维护长时间运行的状态的重复工作。
    你可以在这里阅读更多关于设计的内容。

    扩展无服务器的用例
    使用FaaS很好地解决了问题的用例
    正如我们前面所讨论的,无服务器1.0(FaaS)非常适合以并行处理为中心的用例,在这种用例中,传入的数据通过无状态函数的管道向下推送,在向下游推送之前进行数据丰富和转换。

    这方面的用例示例有

    • 令人难堪的并行任务通常是按需调用和间歇调用的。例如,调整图像大小、执行对象识别和运行基于整数规划的优化。
    • 编排函数,用于协调对专有自动伸缩服务的调用,后端服务本身在其中完成真正的繁重工作。
    • 组成功能链的应用程序,例如,通过数据依赖关系连接的工作流。不过,这些用例通常显示出高端到端的延迟。

    如Adzic等人。在他们的论文“无服务器计算:经济和架构影响”中写道:
    “……现在的无服务器平台对于重要(但不是5个9任务关键)任务非常有用,在这些任务中,高吞吐量是关键,而不是非常低的延迟,并且单个请求可以在相对较短的时间内完成。在无服务器环境中托管此类任务的经济性使其成为显著降低托管成本和加快新功能交付上市时间的一种引人注目的方式。”

    Cloudstate中可以使用的新用例
    然而,使用无状态函数(FaaS)实现传统的应用程序开发、微服务、有状态数据管道和通用的分布式系统问题,在低延迟、高性能、可靠的方式下是非常困难的。

    Cloudstate旨在扩展模型并使其易于实现用例,例如:

    • 机器学习模型的训练与服务
    • 任何需要建立动态模型并提供低延迟服务的用例
    • 低延迟实时预测/推荐服务
    • 低延迟实时欺诈检测
    • 低延迟实时异常检测
    • 用户会话、购物车等
    • 在单个请求的生命周期中管理内存中(但可能是持久的)会话状态。非常常见的用例,例如零售、在线游戏、实时投注等。
    • 事务和工作流管理

    事务分布式工作流管理,如Saga模式。管理工作流中的每个步骤,包括失败时的回滚/补偿操作,同时提供一致性保证方面的选项。

    • 共享协作工作区

    例如:协作文档编辑和聊天室。

    • 分布式计票、投票等。
    • 领导者选举和其他分布式系统协调协议

    使用Akka集群/分布式数据实现起来很简单,但总是在分布式存储上进行协调(例如Lambda中的DynamoDB)代价太高、速度太慢,而且可能成为单点故障。
    Cloudstate的目标是提供一种以可伸缩和可用的方式实现这些用例的方法,与应用程序本身协同工作,同时提供端到端的正确性、一致性和安全性。

    设计与架构
    注意:随着项目的发展,设计很可能会发生变化。

    高层概述
    Cloudstate服务如下所示:
    high-level-design.svg

    • 入口-这可以是Istio、Knative,也可以是Kubernetes中的常规ClusterIP服务通信。无论使用何种服务方法,Cloudstate都希望在其pod上随机、均匀地进行负载平衡。
    • Akka侧车-这款侧车由Cloudstate运营商注入。所有的请求都通过了。单个Cloudstate服务的sidecars形成一个集群,使用Akka remoting彼此直接通信。这个集群以及侧车之间的通信链路允许状态的分片和复制,以及pod之间的寻址P2P消息传递。
    • 代码-这是由开发人员实现的函数。它可以用任何支持gRPC的语言编写。Akka sidecars使用预定义的gRPC协议与用户功能进行通信。此协议携带传入请求和传出响应,以及传输系统当前状态的消息。通常,Cloudstate将为每种语言提供支持库,使gRPC协议适应该语言的惯用API。
    • 分布式数据存储-当服务需要持久化状态时(例如实现事件源实体时),此状态将持久化到分布式数据存储。需要注意的是,用户代码并不直接与数据存储交互——它与Akka sidecars交互,Akka sidecars与数据存储交互。这样,所有数据库通信都可以由Akka sidecar直接管理和监视。由于这是通过提供高级模式来完成的,因此可以做出假设,允许sidecars在集群中安全地缓存、切分和复制数据。

    共享的中间表示层
    Akka侧车和用户代码之间的gRPC协议是Hellerstein等人定义的通用中间表示(IR)。在无服务器计算中:前进一步,后退两步。这是为了允许用户功能利用分布式系统技术(如Akka)所提供的特性,而无需使用与这些技术相同的语言编写。该协议还允许sidecar使用任何技术实现,而不仅仅是Akka。Cloudstate基于Akka的实现作为参考实现提供。
    IR分为两部分。

    • 第一个是发现:用户函数在这里声明它希望公开哪些服务,以及需要用哪些有状态特性来丰富这些服务。这是由sidecar使用IR协议调用用户函数来请求描述它的描述符来完成的。此描述符包含用户函数希望公开的服务的序列化protobuf定义。每个服务都声明有一个特定的实体类型,支持的类型包括事件源和CRDTs。
    • IR的第二部分是一个可插入的实体类型协议:每个实体类型都定义了自己的gRPC协议,用于sidecar和用户功能之间的通信。以下是事件源协议的片段:

    屏幕快照 2020-04-01 下午3.26.48.png

    当实体的命令到达时,将使用此协议发送以下消息:

    • 如果该实体没有现有的句柄流,则调用句柄流调用。只要有更多的命令到达该实体,这个流就会保持打开状态,在一段时间的不活动之后,这个流就会被关闭。
    • sidecar加载该实体的事件日志,并使用EventSourcedEvent消息将每个事件传递给用户函数。
    • 回放实体事件日志后,命令将发送给用户函数。
    • user函数处理命令,并使用EventSourcedReply进行响应。它包含两个响应之一,一个发送到原始源的响应,或一个转发到另一个实体的处理。它还包含零个或多个要持久化的事件。在发送回复或转发之前,这些事件将被持久化。
    • 当实体流仍处于活动状态时,可以接收后续命令,这些命令可以在不重放事件日志的情况下处理。

    用户函数应在流式函数调用的上下文中保持实体的当前状态。
    命令消息包含正在调用的gRPC rpc调用的名称,其中之一是此rpc调用是在发现阶段声明的。它还包含该gRPC调用的有效负载,以及一个提取的实体id,该id标识该调用所针对的实体。通过使用Protobuf字段扩展来声明实体id,下面是声明实体id的用户函数消息示例:
    屏幕快照 2020-04-01 下午3.28.31.png

    Kubernetes算子
    Cloudstate实体的部署设计为以独立方式工作,或与Knative集成。该设计允许将来实现与其他无服务器技术的集成。
    提供了一个运算符,将Cloudstate实体CRD或Knative修订版转换为kubernetes部署,该部署配置有注入的Akka sidecar容器,并创建必要的RBAC权限以允许集群发现、引导和形成以及自动缩放。
    与Knative的集成目前需要一个Knative分支,使部署人员可以插入。在此请求中可以找到所需的更改

    支持自动缩放
    用Knative自动定标器进行的实验发现,它不适合于Akka簇的定标。问题包括:

    • 扩展Akka集群并不是免费的。添加新节点时,碎片将重新平衡到该节点,状态将复制到该节点。一次启动太多节点,或者只启动节点而不立即停止节点,都会显著降低吞吐量和性能。Knative autoscaler没有考虑到这一点,并且非常频繁地启动和停止许多节点。
    • 特别是当CPU受到限制时,JVM预热/jitting可能需要很长时间,在做出进一步的扩展决策之前,需要考虑预热时间。此外,将状态复制和重新平衡碎片到新启动的节点需要时间。我们希望通过使用带有SubstrateVM的Graal AOT编译来解决JVM预热/jitting问题。
    • Akka sidecar替代的Knative proxy与Knative autoscaler高度耦合,两者之间的接口没有很好的定义,这使得从未来Knative的演变来看,Akka sidecar高度依赖它。

    基于这些原因,我们实现了自己的自动定标器。为了简单起见,这个autoscaler被实现为Akka sidecar集群中的一个集群singleton——Akka cluster remoting使得从所有节点到autoscaler singleton的度量传播变得微不足道。我们收集以下指标:

    • 每个pod的平均请求并发性-这是从服务外部同时处理的请求数。这包括当前由用户函数处理的请求、通过其他节点路由以进行分片的请求以及当前与数据库交互的请求。
    • 每个pod的平均用户函数并发性-这是用户函数正在处理的同时请求数。
    • 每个pod的平均数据库并发性-这是在任何时候对数据库执行的同时操作数。这通常从请求并发性中减去,这样数据库性能就不会影响基于请求并发性做出的决策。
    • 请求速率-这是传入请求到达的速率。

    一般来说,当用户功能并发性和请求并发性超过或低于可配置阈值时,就会做出缩放决定。使用两个度量的原因是,在分片情况下,请求并发性高度依赖于节点的数量。

    • 当只有一个节点时,没有请求被转发到其他节点,这意味着延迟保持很低,这意味着请求并发性保持很低。
    • 当有两个节点时,平均50%的请求被转发到其他节点,当有多个节点时,这个数量增加。
    • 由于这个原因,请求并发性不是一个很好的度量标准,无法根据节点数量少的情况来进行扩展决策,因此使用用户函数并发性。然而,请求并发性仍然是一个重要的度量标准,因为集群分片对正在处理的负载的影响是非零的,而且事实上,如果与用户函数相比,它的性能很差,那么用户函数并发性将保持在较低的水平,同时请求将在集群分片缓冲区中备份。
    • 因此,请求并发性被用作缩放度量,但设置为在只有一个节点时永远不会触发的高值,而在负载较高时更可能触发。

    在作出缩放决定后,自动缩放器进入可配置的稳定等待期。在此期间,不会做出基于并发的扩展决策,因为新节点启动和预热可能需要时间,因此并发稳定也需要时间。如果没有稳定的等待期,负载的突然增加将导致并发性线性增加,并且autoscaler将启动越来越多的节点来处理这种增加的并发性。新的节点最初会导致性能下降,因为它们会预热并使碎片与它们重新平衡,从而导致进一步的缩放,这会导致反馈循环,看到节点缩放到不切实际的数字。
    但是,在等待期间,负载可能会继续增加,我们希望能够对此做出响应。为了检测负载的增加,当自动缩放器在放大时首次进入稳定等待期时,记录传入的请求速率。如果此传入请求速率增加一个可配置的阈值,则会做出进一步的缩放决定。
    缩小时不使用基于请求速率的缩放,因为缩小时的请求速率可能非常低(例如,0),因此无法确定处理该请求速率的适当节点数。相反,缩小稳定周期要比增大稳定周期短得多。
    当检测到升级时,也会做出基于请求速率的扩展决策,因为升级会导致性能暂时下降,因为状态会被复制并重新平衡到新升级的节点。
    在编写本文时,autoscaler只在独立模式下工作,即每个用户使用一个部署功能。支持Knative的用户函数每次修订一次部署还没有实现,在Knative中也没有支持在使用自定义部署程序时禁用Knative autoscaler。

    支持HTTP和JSON
    Akka sidecar支持将gRPC用户功能服务作为gRPC提供,也支持使用HTTP/JSON和gRPC HTTP扩展。

    数据库支持
    Cloudstate代理引用实现支持许多数据库。下表为支撑范围,各栏说明如下:

    日志:此数据库是否支持事件源日志。RI事件源支持是使用Akka持久层来构建持久存储的,它支持范围广泛的NoSQL和SQL数据库。
    键值:是否对此数据库实现键值支持。Cloudstate代理尚未提供密钥值支持,但将在将来提供。
    本机映像:此数据库的Cloudstate代理是否可以使用GraalVM本机映像生成。要让GraalVM本机映像为任何库工作可能需要相当大的工作量,因此并非所有代理都支持它。
    屏幕快照 2020-04-01 下午3.33.56.png