关于architecture:聊聊Ports-and-Adapters-architecture

序本文次要钻研一下Ports and Adapters architecture Ports and Adapters architecturePorts and Adapters architecture,又叫Hexagonal architecture,其中ports层是六边形的边界,其中port又能够分为driver port及driven port,简略了解对应输出层及输入层;边界爱护的是外部的app,其中app包含use cases或者叫做application services层以及domain层;adapter能够了解为将内部依赖进行适配,实现port层定义的接口 示例构造github.com/albertllousas/implementing-hexagonal-architecture ├── app│ ├── domain│ │ ├── Account.kt│ │ ├── Ids.kt│ │ ├── Transaction.kt│ │ └── Transfer.kt│ ├── port│ │ ├── driven│ │ │ ├── AccountBalanceChecker.kt│ │ │ ├── AccountFinder.kt│ │ │ └── Transactor.kt│ │ └── driver│ │ └── TransferMoney.kt│ └── usecase│ └── TransferMoneyUseCase.kt└── infrastructure ├── adapter │ ├── driven │ │ ├── InMemoryAccounts.kt │ │ └── InMemoryTransactions.kt │ └── driver │ └── ktor │ └── TransferHttpRoutes.kt └── config ├── ApplicationModule.kt └── ApplicationRunner.ktport层这里定义了driven及driver两大类的接口,而后adapter层对应driven及driver这两大类应用内部的服务进行实现;domain层定义了domain model以及相干畛域办法;usecase或者是application service层则是编排小结Ports and Adapters architecture,又叫Hexagonal architecture,其中ports层是六边形的边界,其中port又能够分为driver port及driven port,简略了解对应输出层及输入层;边界爱护的是外部的app,其中app包含use cases或者叫做application services层以及domain层;adapter能够了解为将内部依赖进行适配,实现port层定义的接口。 ...

March 15, 2021 · 1 min · jiezi

关于architecture:DDD-LiteDDD-领域驱动设计微服务简化版

DDD LiteDDD 畛域驱动设计的小名大家应该都有所耳闻,然而理论我的项目残缺落地 DDD 的很少。因为 DDD 概念繁冗,畛域、子域、外围子域、通用子域、实体、值对象、畛域服务、应用服务、畛域事件、上下文等一大堆概念,间接把人绕晕,对应到理论业务模型时,横看成岭侧成峰,开发人员外部都难以达成统一。 因为 DDD 设计之初指标是作为简单软件解决之道,但咱们大部分利用并没有那么简单,一个简略的利用应用这么一套简单的概念,有点画蛇添足。在微服务时代,设计准则就是依据畛域划分上下文,单体利用复杂度大大降低,微服务须要一种精简的架构。 这里我提出轻量级 DDD 架构: DDD Lite,让其更好的符合微服务。 分层架构在 MVC 中就被宽泛采纳,DDD 中也有四层架构、五层架构、六边形架构等多种流派。 DDD Lite 采纳较为简单的四层架构,自上而下为: InterfaceServiceModelInfrastructure分层Interface接口层。 对外提供 HTTP,RPC 等接口,参数校验、编解码等逻辑在本层解决,业务状态码、对外数据结构在本层定义。 Service服务层。 次要业务逻辑层,调用 Model 层 Repository 接口实现畛域业务逻辑,事务组装。一个 Service 对应一个畛域,畛域是指类似业务逻辑的归类。一个微服务个别只有一到两个 Service,不宜过多。 Model数据模型层。 定义数据结构,以及数据模型对应的 Repository 接口,但不蕴含具体实现。多个相干 Model 组成畛域,此处对畛域进行简化,不再纠结于子域、实体、值对象等细粒度概念。围绕 Model 定义 Repository 接口,不掺杂业务逻辑,供 Service 层调用,不便在 Service 层组成事务。 Repository pattern这里独自讲一下 Repository 设计模式,这是设计好数据模型的重中之重。 Repository 模式即是将对数据结构的操作形象成接口,将业务逻辑(business logic)和数据处理(data access)拆散,退出了一个形象层。 有了 Repository 接口,依赖就从具体变成形象,和 DB 等根底层依赖解耦,不便施行 TDD(测试驱动开发)。 Repository 接口应该放弃细粒度,设计得足够通用,缩小业务属性,不便复用。 Infrastructure基础设施层。 实现 Model 层 Repository 接口,对接 DB、MQ 等数据长久化,或者 RPC 近程调用后端服务。 ...

February 6, 2021 · 1 min · jiezi

从-SOA-到微服务企业分布式应用架构在云原生时代如何重塑

作者 | 易立 阿里云资深技术专家 导读:从十余年前的各种分布式系统研发到现在的容器云,从支撑原有业务到孵化各个新业务,企业的发展离不开统一的、与时俱进的技术架构。本篇文章从企业分布式应用架构层面介绍了云原生计算架构带来的变化,希望能够帮助更多企业的 IT 转型,利用云计算技术推动其成为市场竞争中的敏捷力量。 进入 21 世纪以来,我们见证了企业分布式应用架构从 SOA(Service-oriented Architecture),到微服务架构,再到云原生应用架构的演化。 为了说明企业架构演化背后的思考,我们先谈一些玄学。 第一,企业 IT 系统的复杂性(熵)符合热力学第二定律。随着时间的推演,业务的变化,企业 IT 系统的复杂度会越来越高。第二,在计算机交互设计中有一个著名的复杂性守恒定律。应用交互的复杂性不会消失,只会换一种方式存在。这个原理也同样适用于软件架构。引入新的软件架构,不会降低IT系统的整体复杂性。听到这里,是否让生命不息、折腾不止的我们感到一丝凉凉?:-) 现代软件架构的核心任务之一就是定义基础设施与应用的边界,合理切分复杂性,减少应用开发者需要面对的复杂性。换句话说,就是让开发者专注在核心价值创新上,而把一些问题交给更合适的人和系统来解决。 我们就从下面这张图开始,探究企业分布式应用架构演进背后的逻辑。 本图来自 Bilgin Ibryam 的 twitter 蜕变之痛 - SOA2004 年,IBM 建立 SOA 全球设计中心,我作为研发 TL 和架构师参与了一系列全球客户的 pilot 项目,帮助 Pepboys, Office Depot 等国际企业利用 SOA 优化企业内部和企业间的业务流程,提升业务敏捷性。 当时的大背景是:随着经济全球化逐渐深入,企业面对的竞争加剧,商业变革也开始提速。在大型企业内部的 IT 系统已经经过了数十年的演化。整个的技术体系变得异常复杂,并存着诸如主机系统上的 CISC/COBOL 交易应用,小型机 AS400 中的 RPG 业务系统,和 X86/Power 等分布式系统的 C/JEE/.Net 应用。 大量应用系统由三方供应商提供,一些系统甚至已经无人维护。而且随着业务迭代,一些新的业务系统被持续构建出来,由于缺乏合理的方法论指导,系统之间缺乏有机的链接,形成了若干的孤岛,持续加剧了 IT 架构的复杂性,无法支撑业务的发展诉求。这就仿佛各派高手为了帮助受伤的令狐冲,把异种真气输入体中,虽然短时间可以缓解伤势。可是多道真气无法融合,互相激荡,长时间下来会伤上加伤。 因此,企业 IT 所面临的首要挑战就是整合企业中大量竖桶型(silo-ed)的 IT 系统,支撑日益复杂的业务流程,进行高效的业务决策和支撑业务快速变化。在这种背景下,IBM 等公司提出了 SOA(面向服务的架构)理念,将应用系统抽象成一个个粗粒度的服务,构建松耦合服务架构,可以通过业务流程对服务进行灵活组合,提升企业 IT 资产复用,提高了系统的适应性、灵活性和扩展性,解决“信息孤岛”问题。 ...

August 28, 2019 · 2 min · jiezi

生命周期组件 Lifecycle 源码解析(一)

在上篇文章:Android 生命周期组件 Lifecycle 使用详解 中,我们讲了 Lifecycle 的简单使用,本篇我们来研究下它的源码。基础环境搭建首先,按照上篇文章所讲,快速搭建环境。添加 Lifecycle 轻量级依赖库: implementation “android.arch.lifecycle:runtime:1.1.1"添加support library 28.0.0的支持库(希望大家能先保持一致,因为不同版本的源码是有区别的,后面会将到):implementation ‘com.android.support:appcompat-v7:28.0.0’再添加个注解处理器相关的依赖,至于用处,后面会讲:annotationProcessor “android.arch.lifecycle:compiler:1.1.1"接下来创建实现了 LifecycleObserver 接口的 MyObserver 类:让我们的 Activity 继承自 AppCompatActivity,并在 onCreate() 方法中通过 getLifecycle().addObserver(new MyObserver())绑定 MyObserver :核心代码就一句, getLifecycle().addObserver(new MyObserver()),就能让我们创建的 MyObserver 类,拥有生命周期感知能力。我们知道,这里主要的对象就两个。一个是 getLifecycle() 方法返回来的 LifecycleRegistry 对象(继承自抽象类 Lifecycle),一个是我们创建的需要监听生命周期的类 MyObserver。那我们不禁要问:LifecycleRegistry 是如何感知到生命周期的?它又是如何把生命周期事件分发给 LifecycleObserver 的?我们先来解决第一个问题,LifecycleRegistry 是如何感知到生命周期的。LifecycleRegistry 是如何感知到生命周期的首先,我们Command/Ctrl + 鼠标左键跟踪 getLifecycle() 代码,发现它的具体实现是在 AppCompatActivity 的祖先类 SupportActivity 中,该类实现了 LifecycleOwner 接口。在 onSaveInstanceState() 方法中将 mLifecycleRegistry 的状态置为了 Lifecycle.State.CREATED,这点我们在前篇也讲到过。但从这我们还是看不到跟生命周期有关的东西。此时,我们发现在 onCreate() 方法中有这一行代码:ReportFragment.injectIfNeededIn(this);ReportFragment 是做什么的?点进去看:可以看到, ReportFragment 的 injectIfNeededIn(Activity activity)方法向 Activity 中添加了一个未设置布局的 Fragment :然后又在重写的生命周期事件中调用dispatch(Lifecycle.Event event)方法,来分发生命周期事件,这就是“生命周期感知能力”的来源。这种通过一个空的 Activity 或者 Fragment 来实现特定功能的技巧还是挺常见的,比如权限请求库 RxPermission ,以及 airbnb 开源的用于URL跳转的 DeepLinkDispatch(前者是使用空的 Fragment,后者使用的是空的 Activity)ReportFragment#dispatch(Lifecycle.Event event) 这里面,又调用了 LifecycleRegistry 的handleLifecycleEvent(event)方法。至此,就引入了第二个问题,事件是如何分发到 LifecycleObserver 的。事件是如何分发到 LifecycleObserver 的进入 LifecycleRegistry#handleLifecycleEvent(Lifecycle.Event event)方法,发现它又调用了 moveToState(State next) 方法:而在 sync() 方法中,根据 state 的状态,最终会调用到backwardPass(…)或者forwardPass(…):以 forwardPass(…) 为例:上图可以看到,通过 mObserverMap 最终获取到一个 ObserverWithState 类型的 observer 对象,并调用它的dispatchEvent进行事件分发: observer.dispatchEvent(lifecycleOwner, upEvent(observer.mState));ObserverWithState 又是个什么鬼?我们继续追踪,发现 ObserverWithState 是 LifecycleRegistry 的一个静态内部类。从名称上就能看出,该类封装了 Observer 对象和 State 对象(具体就是 State 和 GenericLifecycleObserver,GenericLifecycleObserver 是个接口,继承自 LifecycleObserver),在其 dispatchEvent 方法中,最终会回调 mLifecycleObserver 的 onStateChanged(…) 方法。追踪到这里,我们知道了,Lifecycle在监听到生命周期变化之后,最终会回调 GenericLifecycleObserver 的 onStateChanged() 方法。我们不由得疑惑,我们定义的 MyObserver 哪去了?没看到有调用我们定义的回调方法啊。它和 GenericLifecycleObserver 又有什么关系?我们看到,ObserverWithState 的构造函数里传进来了一个 LifecycleObserver 类型的 observer 对象,这个参数是从哪传进来的?继续追踪,发现追到了LifecycleRegistry#addObserver(LifecycleObserver observer)方法。而这个方法,就是我们在MainActivity#onCreate(…)方法中调用的: getLifecycle().addObserver(new MyObserver());到这里,总算跟我们的 MyObserver 关联上了。查看LifecycleRegistry#addObserver(LifecycleObserver observer)方法源码:这里面的核心代码就两行,一行是: ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);这行代码,通过传进来的Observer对象,创建出 ObserverWithState 对象。还有一行是: ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);这行代码是将 LifecycleObserver 对象放入一个FastSafeIterableMap 中,以便进行迭代。接下来我们就进入 ObserverWithState 的构造方法中看看:在构造方法中,通过 Lifecycling.getCallback(observer)根据传进来的 observer ,构造了一个 GenericLifecycleObserver 类型的 mLifecycleObserver ,那秘密应该也就在这个方法里,继续跟进。这个方法的本质,其实就是根据传进来的一个LifecycleObserver 对象,构造出来一个 GenericLifecycleObserver 对象(目前有四个子类:FullLifecycleObserverAdapter、SingleGeneratedAdapterObserver、CompositeGeneratedAdaptersObserver、ReflectiveGenericLifecycleObserver),而最终构造出来的对象,就包含了我们创建的 LifecycleObserver 的所有信息,包括各种回调方法等。看到这里,就要提到文章开头要大家添加的一个注解处理器的依赖:annotationProcessor “android.arch.lifecycle:compiler:1.1.1"当我们通过注解的方式来自定义LifecycleObserver 的时候,按照传统方式,必定要通过反射来对注解进行解析,这样就会对性能造成影响。一方面,我们通过缓存,来避免每次都通过反射获取构造器。另一方面,又通过注解处理器,在编译时对那些被@OnLifecycleEvent注解标注的普通方法,进行预处理,生成以“类名_LifecycleAdapter”命名的类,将各种回调方法直接进行逻辑转换,避免反射,进而来提高性能。明白了这点,再看Lifecycling.getCallback(observer)方法就比较容易理解了。如果传进来的的参数 object 是 FullLifecycleObserver 类型,就把它构造成FullLifecycleObserverAdapter 对象,并返回如果传进来的的参数 object 是GenericLifecycleObserver类型,直接返回该对象如果1,2都不满足,就解析该类的的构造器的Type(该类是反射获取的,还是通过注解处理器生成的)。如果是通过注解处理器生成的类来调用回调函数,就返回一个SingleGeneratedAdapterObserver/CompositeGeneratedAdaptersObserver 对象如果以上条件都不满足,就通过反射来调用各回调函数。返回一个 ReflectiveGenericLifecycleObserver 对象现在我们在 app 目录下的 bulid.gradle 中添加上上面的注解处理器依赖,然后编译下项目,会发现在build目录下生成了对应的类:MyObserver_LifecycleAdapter.java点进去,看看生成的这个类的源码:可以看到,我们在 MyObserver 中通过@OnLifecycleEvent注解标注的那些方法,在这里都根据条件进行判断了,而非通过注解。这时候我们就能理清这个这个流程了,当添加了注解处理器之后,我们这里的Lifecycling.getCallback(observer)方法将会把我们的MyObserver对象构建成一个 SingleGeneratedAdapterObserver对象返回(因为这里只有一个构造器),之后的 mLifecycleObserver.onStateChanged(owner, event);其实调用的就是SingleGeneratedAdapterObserver的onStateChanged(owner, event)方法:这里面就可以看到,它调用了内部包裹的类的callMethods(…)方法,也就是我们上面提到的MyObserver_LifecycleAdapter的callMethonds(…)方法。到这里,就完成了 Lifecycle 源码的解析。通过反射获取注解信息这顺便提下通过注解的方式调用各回调方法的过程。主要相关类就是 ReflectiveGenericLifecycleObserver.java这里我们主要关注回调信息 CallbackInfo 的获取方式的代码: mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());因为反射的代价是比较大的,所以又通过 ClassesInfoCache.java这个单例类,为 ReflectiveGenericLifecycleObserver 类要调用的各种方法的相关信息进行了缓存。点进去看下它的 getInfo(…) 方法内部,是如何获取方法信息的。里面又调用了createInfo()方法:这里,就能看到对注解进行处理的代码了。到这,我们就算完成了继承自 AppCompactActivity 的情况下的源码解析,而继承自普通 Activity 这种情况下,原理是什么呢?鉴于篇幅,将放在下篇文章。欢迎关注我的公众号获取。 ...

March 5, 2019 · 2 min · jiezi

猫头鹰的深夜翻译:集成方式是如何影响微服务架构的

前言当万维网首次出现时,集成不同类型的操作系统是一项主要的挑战。HTTP的出现使得不同的操作系统之间可以通过超文本使用统一的协议进行通信。当微服务的架构出现后,系统的集成带来的挑战没有太大的分别:多种实现技术在物理网络上彼此分离,需要相互通信。从最终用户的角度来看,微服务的集成在系统的无缝体验方面起着至关重要的作用。正确集成的系统还有助于实现分布式系统的优势:它们可以在service级实现扩容提高效率,并且能够在满足业务需求的同时降低架构成本。另一方面,错误集成的系统可能会彻底破坏微服务架构的好处:它可能会导致数据丢失和继承问题。这些问题通常很难追踪,同时用户会受到不良的影响。无缝集成有许多因素需要考虑。我们需要针对不同的场景选择最合适的集成方式。接下来,我们会介绍不同集成方式的优缺点。数据库集成在这种模式下,两个或多个服务都读写自同一个中心数据库。所有的服务连接到同一个中心数据库上。我们可以用一个银行系统来解释这种架构。数据库集成的显著优点之一是简单。事务的管理和别的模式相比更加直观。这可能是使用最广泛的一种模式,同时也可能是最被滥用的。这个模式下服务耦合度较高,使得微服务较难以变更和扩展。定义数据的所属权以及更新schema会很麻烦,因为需要重新编译和部署所有相关的服务。它导致大爆炸形式的集成。这种类型的集成可能在维护微服务的自治性方面存在问题。在大规模应用的场景下,唯一的选择是在数据库中投入更多硬件,即使这样,也很难避免数据库和行级争用中的死锁。想情况下,我们不建议将此模式用于服务间通信。它可以用于分阶段微服务部署的早期阶段。换句话说:如果你使用它,很快就会抛弃它。同步API调用在这种集成模式下,服务之间通过API同步的通信。所有的数据访问都是通过API以请求响应的方式实现,调用方等待API的返回数据做进一步处理。在上面的例子中,如果事务服务需要读取用户信息,可以通过调用用户服务的API来实现。这种方式在直接数据库的调用之上进行了良好的抽象,并在调用技术选择方面提供了极好的灵活性。它隐藏了实现的细节:使得我们可以在不影响客户的前提下自由的变更。比如,用户模块可能使用的是Java和MySQL,而事务模块可能使用的是SQL Server和.NET,但是它们彼此之间还是可以通过API进行通信。但是,这个和直接的数据库集成模式没有本质的区别。在数据库调用之上添加另一个网络调用也会j降低可扩展性:增加了负载,降低了性能。这种集成模式也使事务管理变得困难并且影响了服务自治,因为服务依赖于彼此的正常运行。在微服务中,如果必须在系统边界之外同步读取数据,那就是SOA架构的感觉了。在某些场景下,这种集成模式是最佳的甚至是不可避免的。API集成模式中安全令牌是一个主要的应用场景,因为这些token生命周期短,而且在使用之前才生成。如果可能的话,应谨慎使用同步API调用。如果使用这种模式,它们应该版本化并且应该与熔断机制一起使用。ETL (Extract, Transform, and Load)ETL需要按预定义的时间表通过后台进程同步数据。这些数据可以是推模式或者拉模式。它是异步的,这意味着服务可以在不等待“回调”的情况下执行。这种集成模式也很好地隐藏了实现细节。它提供了合理的解耦,因为服务不依赖于彼此的正常运行。实时用户不会受到正常运行时间或处理时间的影响。ETL过程需要改变源和目标数据库。通过ETL集成,数据一致性取决于计划和持续时间。弄清楚变化的可能会使应用变得太复杂。在这种场景下,团队不得不导出全部的数据。这使得流程运行时间非常长,严重影响了它们的实用性。报告性的服务非常适用于这种集成。这些ETL的过程通常会有些耗时,只有在系统中可接受旧数据时才应使用它们。消息在这种模式下,服务彼此之间通过指令和事件交换信息它们通过消息中介如RabbitMQ,MSMQ等。在上面的例子中,事务服务建立了一个账号余额变更的时间并且放到消息中间件中。奖励服务,积分服务和消息服务分别订阅这个事件,并且进行处理。这是发布订阅模式,还有很多别的消息模式。只要正确的实现,消息提供了非常好的解耦。它提供了非常高的灵活性,只要能够正确的通信。将数据推送给订阅者使得数据发送的部分非常容易完成。而且发送方完全不会受订阅端处理细节的影响。不正确的服务或事务边界可能使消息传递实现复杂化。而且,这种模式提供的松耦合影响一致性为。它需要高度的规范化才能正确实现消息传递,而没有经验的团队可能会很挣扎。两种类型的消息典型的消息传递解决方案建立在传输属性的基础之上。在较高的层面上,这些传输可以分为两类:消息队列和消息流。排队解决方案涉及处理实时数据。一旦消息被成功消费,则这条消息会从队列中出队。只要处理能够跟上,队列就不会累积,也不会占用太多空间。但是,在扩展订户的情况下,无法保证消息的顺序性。在流式传输解决方案中,消息按顺序存储在流中。它发生在消息传输本身。订户在流上的位置保留在传输上。它可以根据需要在流上倒置。这对于故障和新的订阅方案非常有利。但是,这取决于流的长度。从存储角度来看,它需要更多配置,因此需要对流进行归档。这里的典型用例假设系统能够处理有些陈旧的数据。如果情况并非如此,我们需要更多地分析业务领域并理解原因。在我们的示例中,当事务更新发生时,信用分数更新不一定需要实时发生。消息在这个场景下非常适合。我们可以相应地设置用户的期望,并允许服务管理自己的负载。总结每一种微服务架构都不同,没有绝对完美的方案。当我们使用它们时,我们需要结合故障情景,驱动这些集成模式的组合。比如,Netflix使用消息传递来移动数据,如果消息不可用或数据仍在传输中,它们会回退同步API。在每种情况下,理想的是实现最灵活和可扩展的微服务架构,但是必须首先考虑实施细节和自己的能力。这里的关键是在需要扩展和自治时使用异步模式。要实现这一目标,您需要可靠的服务边界和明确的数据所有权。否则,您最终会遇到复杂且不可持续的集成方案。对业务流程建模很重要。这将让您知道哪些用例和进程本质上是异步的并且适合于消息传递。

December 2, 2018 · 1 min · jiezi

无服务器架构(Serverless Architectures):什么是 Serverless

译注:为了便于对照参考,“Serverless”、“BaaS” 等术语文中不做翻译。原文很长,这里分成上下两篇。翻译过程在 GitHub 上进行。原文:https://martinfowler.com/articles/serverless.html 作者:Mike Roberts无服务器架构(Serverless architectures)是指一个应用大量依赖第三方服务(后端即服务,Backend as a Service,简称“BaaS”),或者把代码交由托管的、短生命周期的容器中执行(函数即服务,Function as a Service,简称“FaaS”)。现在最知名的 FaaS 平台是 AWS Lambda。把这些技术和单页应用等相关概念相结合,这样的架构无需维护传统应用中永远保持在线的系统组件。Serverless 架构的长处是显著减少运维成本、复杂度、以及项目起步时间,劣势则在于更加依赖平台供应商和现阶段仍有待成熟的支持环境。引言无服务器计算(Severless computing,简称 Serverless)现在是软件架构圈中的热门话题,三大云计算供应商(Amazon、Google 和 Microsoft)都在大力投入这个领域,涌现了不计其数的相关书籍、开源框架、商业产品、技术大会。到底什么是 Serverless?它有什么长处/短处?我希望通过本文对这些问题提供一些启发。开篇我们先来看看 Serverless 是什么,之后我会尽我所能中立地谈谈它的优势和缺点。什么是 Serverless就像软件行业中的很多趋势一样,Serverless 的界限并不是特别清晰,尤其是它还涵盖了两个互相有重叠的概念:Serverless 最早用于描述那些大部分或者完全依赖于第三方(云端)应用或服务来管理服务器端逻辑和状态的应用,这些应用通常是富客户端应用(单页应用或者移动端 App),建立在云服务生态之上,包括数据库(Parse、Firebase)、账号系统(Auth0、AWS Cognito)等。这些服务最早被称为 “(Mobile) Backend as a Service”,下文将对此简称为 “BaaS”。Serverless 还可以指这种情况:应用的一部分服务端逻辑依然由开发者完成,但是和传统架构不同,它运行在一个无状态的计算容器中,由事件驱动、生命周期很短(甚至只有一次调用)、完全由第三方管理(感谢 ThoughtWorks 在他们最近的“技术观察”中对此所做的定义)。这种情况称为 Functions as a service / FaaS。AWS Lambda 是目前的热门 FaaS 实现之一,下文将对此简称为 “FaaS”。【边栏注释:“Serverless” 术语的起源】本文将主要聚焦于 FaaS,不仅仅因为它是 Serverless 中最新也最热门的领域,更重要的是它和我们传统技术架构思路的显著不同。BaaS 和 FaaS 在运维层面有类似之处(都无需管理硬件资源),并且也经常配合使用。主流的云服务商都会提供一套“Serverless 全家桶”,囊括了 BaaS 和 FaaS 产品——例如 Amazon 的 Serverless 产品介绍,Google 的 Firebase 服务也紧密集成了 Google Cloud Functions。对小公司而言这两个领域也有交叉,Auth0 最初是一个 BaaS 产品,提供用户管理的各种服务,他们后来创建了配套的 FaaS 服务 Webtask,并且将此概念进一步延伸推出了 Extend,支持其他 BaaS 和 SaaS 公司能够轻松地在现有产品中加入 FaaS 能力。一些示例界面驱动的应用(UI-driven applications)我们来设想一个传统的三层 C/S 架构,例如一个常见的电子商务应用(比如在线宠物商店),假设它服务端用 Java,客户端用 HTML/JavaScript:在这个架构下客户端通常没什么功能,系统中的大部分逻辑——身份验证、页面导航、搜索、交易——都在服务端实现。把它改造成 Serverless 架构的话会是这样:这是张大幅简化的架构图,但还是有相当多变化之处:我们移除了最初应用中的身份验证逻辑,换用一个第三方的 BaaS 服务。另一个 BaaS 示例:我们允许客户端直接访问一部分数据库内容,这部分数据完全由第三方托管(如 AWS Dynamo),这里我们会用一些安全配置来管理客户端访问相应数据的权限。前面两点已经隐含了非常重要的第三点:先前服务器端的部分逻辑已经转移到了客户端,如保持用户 Session、理解应用的 UX 结构(做页面导航)、获取数据并渲染出用户界面等等。客户端实际上已经在逐步演变为单页应用。还有一些任务需要保留在服务器上,比如繁重的计算任务或者需要访问大量数据的操作。这里以“搜索”为例,搜索功能可以从持续运行的服务端中拆分出来,以 FaaS 的方式实现,从 API 网关(后文做详细解释)接收请求返回响应。这个服务器端函数可以和客户端一样,从同一个数据库读取产品数据。我们原始的服务器端是用 Java 写的,而 AWS Lambda(假定我们用的这家 FaaS 平台)也支持 Java,那么原先的搜索代码略作修改就能实现这个搜索函数。最后我们还可以把“购买”功能改写为另一个 FaaS 函数,出于安全考虑它需要在服务器端,而非客户端实现。它同样经由 API 网关暴露给外部使用。消息驱动的应用(Message-driven applications)再举一个后端数据处理服务的例子。假设你在做一个需要快速响应 UI 的用户中心应用,同时你又想捕捉记录所有的用户行为。设想一个在线广告系统,当用户点击了广告你需要立刻跳转到广告目标,同时你还需要记录这次点击以便向广告客户收费(这个例子并非虚构,我的一位前同事最近就在做这项重构)。传统的架构会是这样:“广告服务器”同步响应用户的点击,同时发送一条消息给“点击处理应用”,异步地更新数据库(例如从客户的账户里扣款)。在 Serverless 架构下会是这样:这里两个架构的差异比我们上一个例子要小很多。我们把一个长期保持在内存中待命的任务替换为托管在第三方平台上以事件驱动的 FaaS 函数。注意这个第三方平台提供了消息代理和 FaaS 执行环境,这两个紧密相关的系统。解构 “Function as a Service”我们已经提到多次 FaaS 的概念,现在来挖掘下它究竟是什么含义。先来看看 Amazon 的 Lambda 产品简介:通过 AWS Lambda,无需配置或管理服务器(1)即可运行代码。您只需按消耗的计算时间付费 – 代码未运行时不产生费用。借助 Lambda,您几乎可以为任何类型的应用程序或后端服务(2)运行代码,而且全部无需管理。只需上传您的代码,Lambda 会处理运行(3)和扩展高可用性(4)代码所需的一切工作。您可以将您的代码设置为自动从其他 AWS 服务(5)触发,或者直接从任何 Web 或移动应用程序(6)调用。本质上 FaaS 就是无需配置或管理你自己的服务器系统或者服务器应用即可运行后端代码,其中第二项——服务器应用——是个关键因素,使其区别于现今其他一些流行的架构趋势如容器或者 PaaS(Platform as a Service)。回顾前面点击处理的例子,FaaS 替换掉了点击处理服务器(可能跑在一台物理服务器或者容器中,但绝对是一个独立的应用程序),它不需要服务器,也没有一个应用程序在持续运行。FaaS 不需要代码基于特定的库或框架,从语言或环境的层面来看 FaaS 就是一个普通的应用程序。例如 AWS Lambda 支持 JavaScript、Python 以及任意 JVM 语言(Java、Clojure、Scala 等),并且你的 FaaS 函数还可以调用任何一起部署的程序,也就是说实际上你可以用任何能编译为 Unix 程序的语言(稍后我们会讲到 Apex)。FaaS 也有一些不容忽视的局限,尤其是牵涉到状态和执行时长问题,这些我们稍后详谈。再次回顾一下点击处理的例子——代码迁移到 FaaS 唯一需要修改的是 main 方法(启动)的部分,删掉即可,也许还会有一些上层消息处理的代码(实现消息监听界面),不过这很可能只是方法签名上的小改动。所有其他代码(比如那些访问数据库的)都可以原样用在 FaaS 中。既然我们没有服务器应用要执行,部署过程也和传统的方式大相径庭——把代码上传到 FaaS 平台,平台搞定所有其他事情。具体而言我们要做的就是上传新版的代码(zip 文件或者 jar 包)然后调用一个 API 来激活更新。横向扩展是完全自动化、弹性十足、由 FaaS 平台供应商管理的。如果你需要并行处理 100 个请求,不用做任何处理系统可以自然而然地支持。FaaS 的“运算容器”会在运行时按需启动执行函数,飞快地完成并结束。回到我们的点击处理应用,假设某个好日子我们的客户点击广告的数量有平日的十倍之多,我们的点击处理应用能承载得住么?我们写的代码是否支持并行处理?支持的话,一个运行实例能够处理这么多点击量吗?如果环境允许多进程执行我们能自动支持或者手动配置支持吗?以 FaaS 实现你的代码需要一开始就以并行执行为默认前提,但除此之外就没有其他要求了,平台会完成所有的伸缩性需求。FaaS 中的函数通常都由平台指定的一些事件触发。在 AWS 上有 S3(文件)更新、时间(定时任务)、消息总线(Kinesis)消息等,你的函数需要指定监听某个事件源。在点击处理器的例子中我们有个假设是已经采用了支持 FaaS 订阅的消息代理,如果没有的话这部分也需要一些代码量。大部分的 FaaS 平台都支持 HTTP 请求触发函数执行,通常都是以某种 API 网关的形式实现(如 AWS API Gateway,Webtask)。我们在宠物商店的例子中就以此来实现搜索和购买功能。状态当牵涉到本地(机器或者运行实例)状态时 FaaS 有个不能忽视的限制。简单点说就是你需要接受这么一个预设:函数调用中创建的所有中间状态或环境状态都不会影响之后的任何一次调用。这里的状态包括了内存数据和本地磁盘存储数据。从部署的角度换句话说就是 FaaS 函数都是无状态的(Stateless)。这对于应用架构有重大的影响,无独有偶,“Twelve-Factor App” 的概念也有一模一样的要求。在此限制下的做法有多种,通常这个 FaaS 函数要么是天然无状态的——纯函数式地处理输入并且输出,要么使用数据库、跨应用缓存(如 Redis)或者网络文件系统(如 S3)来保存需要进一步处理的数据。执行时长FaaS 函数可以执行的时间通常都是受限的,目前 AWS Lambda 函数执行最长不能超过五分钟,否则会被强行终止。这意味着某些需要长时间执行的任务需要调整实现方法才能用于 FaaS 平台,例如你可能需要把一个原先长时间执行的任务拆分成多个协作的 FaaS 函数来执行。启动延迟目前你的 FaaS 函数响应请求的时间会受到大量因素的影响,可能从 10 毫秒到 2 分钟不等。这听起来很糟糕,不过我们来看看具体的情况,以 AWS Lambda 为例。如果你的函数是 JavaScript 或者 Python 的,并且代码量不是很大(千行以内),执行的消耗通常在 10 到 100 毫秒以内,大函数可能偶尔会稍高一些。如果你的函数实现在 JVM 上,会偶尔碰到 10 秒以上的 JVM 启动时间,不过这只会在两种情况下发生:你的函数调用触发比较稀少,两次调用间隔超过 10 分钟。流量突发峰值,比如通常每秒处理 10 个请求的任务在 10 秒内飙升到每秒 100 个。前一种情况可以用个 hack 来解决:每五分钟 ping 一次给函数保持热身。这些问题严重么?这要看你的应用类型和流量特征。我先前的团队有一个 Java 的异步消息处理 Lambda 应用每天处理数亿条消息,他们就完全不担心启动延迟的问题。如果你要写的是一个低延时的交易程序,目前而言肯定不会考虑 FaaS 架构,无论你是用什么语言。不论你是否认为你的应用会受此影响,都应该以生产环境级别的负载测试下实际性能情况。如果目前的情况还不能接受的话,可以几个月后再看看,因为这也是现在的 FaaS 平台供应商们主要集中精力在解决的问题。API 网关我们前面还碰到过一个 FaaS 的概念:“API 网关”。API 网关是一个配置了路由的 HTTP 服务器,每个路由对应一个 FaaS 函数,当 API 网关收到请求时它找到匹配请求的路由,调用相应的 FaaS 函数。通常 API 网关还会把请求参数转换成 FaaS 函数的调用参数。最后 API 网关把 FaaS 函数执行的结果返回给请求来源。AWS 有自己的一套 API 网关,其他平台也大同小异。除了纯粹的路由请求,API 网关还会负责身份认证、输入参数校验、响应代码映射等,你可能已经敏锐地意识到这是否合理,如果你有这个考虑的话,我们待会儿就谈。另一个应用 API 网关加 FaaS 的场景是创建无服务器的 http 前端微服务,同时又具备了 FaaS 函数的伸缩性、管理便利等优势。目前 API 网关的相关工具链还不成熟,尽管这是可行的但也要够大胆才能用。工具链前面关于工具链还不成熟的说法是指大体上 FaaS 无服务器架构平台的情况,也有例外,Auth0 Webtask 就很重视改善开发者体验,Tomasz Janczuk 在最近一届的 Serverless Conf 上做了精彩的展示。无服务器应用的监控和调试还是有点棘手,我们会在本文未来的更新中进一步探讨这方面。开源无服务器 FaaS 的一个主要好处就是只需要近乎透明的运行时启动调度,所以这个领域不像 Docker 或者容器领域那么依赖开源实现。未来肯定会有一些流行的 FaaS / API 网关平台实现可以跑在私有服务器或者开发者工作站上,IBM 的 OpenWhisk 就是一个这样的实现,不知道它是否能成为流行选择,接下来的时间里肯定会有更多竞争者出现。除了运行时的平台实现,还是有不少开源工具用以辅助开发和部署的,例如 Serverless Framework 在 API 网关 + Lambda 的易用性上就比它的原创者 AWS 要好很多,这是一个 JS 为主的项目,如果你在写一个 JS 网关应用一定要去了解下。再如 Apex——“轻松创建、部署及管理 AWS Lambda 函数”。Apex 有意思的一点是它允许你用 AWS 平台并不直接支持的语言来实现 Lambda 函数,比如 Go。什么不是 Serverless在前文中我定义了 “Serverless” 是两个概念的组合:“Backend as a Service” 和 “Function as a Service”,并且对后者的特性做了详细解释。在我们开始探讨它的好处和弊端之前,我想再花点儿时间在它的定义上,或者说:区分开那些容易和 Serverless 混淆的概念。我看到一些人(包括我自己最近)对此都有困惑,我想值得对此做个澄清。对比 PaaS既然 Serverless FaaS 这么像 12-Factor 应用,那不就是另一种形式的 Platform as a Service 么?就像 Heroku?对此借用 Adrian Cockcroft 一句非常简明的话:如果你的 PaaS 能在 20ms 内启动一个只运行半秒钟的实例,它就叫 Serverless。— Adrian Cockcroft换句话说,大部分 PaaS 应用不会为了每个请求都启动并结束整个应用,而 FaaS 就是这么做的。好吧,然而假设我是个娴熟的 12-Factor 应用开发者,写代码的方式还是没有区别对么?没错,但是你如何运维是有很大不同的。鉴于我们都是 DevOps 工程师我们会在开发阶段就充分考虑运维,对吧?FaaS 和 PaaS 在运维方面的关键区别是伸缩性(Scaling)。对于大多数 PaaS 平台而言你需要考虑如何伸缩,例如在 Heroku 上你要用到多少 Dyno 实例?对于 FaaS 应用这一步骤是完全透明的。即便你将 PaaS 配置为自动伸缩,也无法精细到单个请求级别,除非你有一个非常明确稳定的流量曲线可以针对性地配置。所以 FaaS 应用在成本方面要高效得多。既然如此,何必还用 PaaS?有很多原因,最主要的因素应该是工具链成熟度。另外像Cloud Foundry 能够给混合云和私有云的开发提供一致体验,在写就本文的时候 FaaS 还没有这么成熟的平台。对比容器使用 Serverless FaaS 的好处之一是避免在操作系统层面管理应用程序进程。有一些 PaaS 平台如 Heroku 也提供了这样的特性;另一种对进程的抽象是容器,这类技术的代表是 Docker。容器托管系统(Mesos、Kubernetes 等)把应用从系统级开发中抽象出来,这种做法日渐流行,甚至在此之上云服务商的容器平台(如 Amazon ECS、EKS、Google Cloud Engine)也像 Serverless FaaS 一样允许团队从管理主机中完全解放出来。在这股容器大潮中,FaaS 是否还有优势?概念上来说前面对 PaaS 的论断仍旧适用于容器。Serverless FaaS 的伸缩性是完全自动化、透明、良好组织的,并且自动进行资源监控和分配;而容器平台仍旧需要你对容量和集群进行管理。另外我还得说容器技术也处在不够成熟和稳定的阶段,尽管它越来越接近了。当然这不是说 Serverless 就成熟了,但你终究需要在两个都属前沿的技术方向中做出选择。还有个值得一提的是不少容器平台支持了自动伸缩的容器集群,Kubernetes 有内建的 Horizontal Pod Autoscaling 功能,AWS Fargate 则承诺会有“Serverless 容器”。总的来说 Serverless FaaS 和托管容器在管理和伸缩性方面的差别已经不大,在它们之间的取舍更多看风格取向和应用的类型。例如事件驱动的应用组件更适合用 FaaS 实现,而同步请求驱动的应用组件更适合用容器实现。我预计很快就会有不少团队和应用同时采用这两种架构模式,期待看它们会擦出怎样的火花。对比 NoOpsServerless 并非“零运维”——尽管它可能是“无系统管理员”,也要看你在这个 Serverless 的兔子洞里走多深。“运维”的意义远不止系统管理,它还包括并不限于监控、部署、安全、网络、支持、生产环境调试以及系统伸缩。这些事务同样存在于 Serverless 应用中,你仍旧需要相应的方法处理它们。某些情况下 Serverless 的运维会更难一些,毕竟它还是个崭新的技术。系统管理的工作仍然要做,你只是把它外包给了 Serverless 环境。这既不能说坏也不能说好——我们外包了大量的内容,是好是坏要看具体情况。不论怎样,某些时候这层抽象也会发生问题,就会需要一个来自某个地方的人类系统管理员来支持你的工作了。Charity Majors 在第一届 Serverless 大会上就这个主题做了个非常不错的演讲,也可以看看她相关的两篇文章:WTF is operations? 和 Operational Best Practices)。对比存储过程即服务还有一种说法把 Serverless FaaS 看做“存储过程即服务(Stored Procedures as a Service)”,我想原因是很多 FaaS 函数示范都用数据库访问作为例子。如果这就是它的主要用途,我想这个名字也不坏,但终究这只是 FaaS 的一种用例而已,这样去考虑 FaaS 局限了它的能力。我好奇 Serverless 会不会最终变成类似存储过程那样的东西,开始是个好主意,然后迅速演变成大规模技术债务。— Camille Fournier但我们仍然值得考虑 FaaS 是否会导致跟存储过程类似的问题,包括 Camille 提到的技术债。有很多存储过程给我们的教训可以放在 FaaS 场景下重新审视,存储过程的问题在于:通常依赖于服务商指定的语言,或者至少是指定的语言框架/扩展因为必须在数据库环境中执行所以很难测试难以进行版本控制,或者作为应用包进行管理尽管不是所有存储过程的实现都有这些问题,但它们都是常会碰到的。我们看看是否适用于 FaaS:第一条就目前看来显然不是 FaaS 的烦恼,直接排除。第二条,因为 FaaS 函数都是纯粹的代码,所以应该和其他任何代码一样容易测试。整合测试是另一个问题,我们稍后展开细说。第三条,既然 FaaS 函数都是纯粹的代码,版本控制自然不成问题;最近大家开始关心的应用打包,相关工具链也在日趋成熟,比如 Amazon 的 Serverless Application Model(SAM)和前面提到的其他 Serverless 框架都提供了类似的功能。2018 年初 Amazon 还开放了 Serverless Application Repository(SAR)服务,方便组织分发应用程序和组件,也是基于 AWS Serverless 服务构建的。关于 SAR 可以看看我的另一篇文章:Examining the AWS Serverless Application Repository。本文翻译的实时更新:https://amio.github.io/server… ...

November 13, 2018 · 3 min · jiezi