文章

演进中的架构

这是目前最震撼我的一篇总结。过于高屋建瓴,我就不指指点点了:D

什么是“凤凰架构”

服务架构演进史

  1. 可靠的系统
  2. 架构的发展
    1. 原始分布式
    2. 单体
      1. 单体是源于本质的优秀
      2. 大型单体
    3. 微服务
    4. 后微服务(云原生)
      1. 服务网格(Service Mesh)
        1. sidecar
  3. 无服务(器)serverless/云计算
  4. 总结

可靠的系统

什么是可靠的系统?系统可靠不在于每一部分都可靠,而在于不可靠的地方在出错崩溃之后能自动被取代,以继续维持系统的整体稳定。这样即使用不可靠的部件,依旧能构造出可靠的系统

墨菲定律(Murphy’s Law):如果事情可能出错就总会出错(Anything that can go wrong will go wrong)。

所以该来的一定会来,跑不掉的,与其祈求不出错,不如做好重生措施。

所以哪怕是由极不靠谱的人员开发的极不靠谱的程序,哪怕存在严重的内存泄漏,哪怕服务三分钟就崩溃,只要能有多个实例并在失败后不断重建,从外部看来依然(有可能)是稳定健壮的。

架构的发展

架构不是被发明出来的,而是持续演进的结果。因为当前架构无法满足当前需求,所以才发生改变,而不是被凭空创造出来的。

原始分布式

虽然最简单的服务是单体,但一开始(1970+、1980+)人们其实是在尝试“使用多个独立的分布式系统共同构建一个更大型的系统”。虽然听起来匪夷所思,但这是当时的硬件条件决定的。当时的微型计算机(Intel 8086 processor)有限的算力让人不得不提出这样的构想:构建符合UNIX设计哲学的、如同本地调用一般简单透明的分布式系统

然而该构想终究要失败,因为分布式系统天然存在诸多难题,远比单体系统复杂,所以在当时的条件下几乎是不可能完成的:

  1. 服务发现:远程服务在哪里
  2. 负载均衡:有多少个实例
  3. 熔断、隔离、降级:网络出现分区、超时,服务出错,怎么办
  4. 序列化:方法的参数与返回值
  5. 传输协议:信息如何传输
  6. 认证授权:如何认证,如何鉴权
  7. 网络安全层:如何安全通信。当时还没有SSL/TLS
  8. 数据一致性:调用不同的机器如何返回一致的结果
  9. 等等

而这些问题在当时几乎都没有解决方案。经过这么多年的发展和研究,现在才有了种种解决方案,或者CAP这样的理论支撑。这就叫历史局限性,把现在的任何人放到当时的时代,都不可能复现现在的分布式架构。不经过几十年的探索,历史不进步,个人是无能为力的。

将一个系统拆分到不同的机器中运行,这样做带来的服务发现、跟踪、通信、容错、隔离、配置、传输、数据一致性和编码复杂度等方面的问题,所付出的代价远远超过了分布式所取得的收益。

看了这段历史,相信没有谁再敢不考虑实际情况上来就说分布式架构要比单体好。

Kyle Brown:某个功能能够进行分布式,并不意味着它就应该进行分布式,强行追求透明的分布式操作,只会自寻苦果(Just because something can be distributed doesn’t mean it should be distributed. Trying to make a distributed call act like a local call always ends in tears)。

单体

好在80年代是摩尔定律发挥作用的黄金年代,硬件性能飞速发展,用单体应用支撑起稳定的服务成为了可能。

单体是源于本质的优秀

单体系统比分布式简单很多很多,但简单并不代表落后。单体系统是一个人写程序时无需刻意学习就自然写出来的一种“架构”,是源自本能的优秀设计

单体是如此自然而然,以至于直到微服务流行开来之后,单体作为对照才被认为是一种“架构”。

单体系统的方法都是进程内调用,不会发生进程间通信(IPC,Inter-Process Communication),不仅避免了上述分布式系统的复杂问题,而且运行起来效率是最高的。

大型单体

虽然进程内调用简单高效,但是不自治、不隔离。如果一部分代码出现缺陷,过度消耗进程内的资源,会导致其他代码一块儿凉凉。而如果能自治,就只影响自己,影响范围可控。

单体的缺点,必须放在“大型”单体应用这个前提下才成立。当单体系统复杂起来,可维护性就变差了,因为不可能停掉半个进程做更新。为此出现了OSGi、JVMTI等复杂的HotSwap方案,但这就像是给奔跑中的汽车更换轮胎,用起来复杂而别扭。

太多这样的例子了,比如upnp使用http over udp,虽然放弃了tcp的可靠传输,但是在家庭局域网的场景下,几乎没什么影响,还能借助udp的multicast让设备即插即用,不需要任何配置。但是放在互联网里它就不合适了,http传输就得用http over tcp。

单体系统的不自治,使得它很难符合上述“可靠的系统”。规模越大,让单体系统不出错的概率越小。而由墨菲定律可知,哪怕有一丁点儿出错的可能,出错最终也是必然的,更何况出错的概率本身就越来越大。为了允许程序出错,为了获得隔离、自治的能力,为了可以技术异构等目标,是继为了性能与算力之后,让程序再次选择分布式的理由。

微服务

尽管远程服务有高昂的调用成本,但这是为组件带来自治与隔离能力的必要代价。

所谓系统检修,不过是一次在线服务更新而已。

后微服务(云原生)

其实微服务中碰到的上述分布式问题,未必一定要由软件解决,硬件也是可以的,比如:

  • 伸缩扩容:通过在新的服务器部署新的服务实例来解决;
  • 服务发现:通过部署DNS服务器来解决;
  • 安全传输:通过TLS和CA证书来解决;

人们之所以选择在软件层面去解决分不是问题,是因为硬件设施没有软件灵活:软件只需要敲敲键盘就可以copy并启动新的实例以实现伸缩扩容,硬件没法通过敲敲键盘变出新的服务器。

所以现在的微服务技术分成了两条路

  1. 虚拟化技术+容器化技术:比如Kubernetes,虚拟化基础设施(硬件),想要在基础设施层面解决微服务中的问题,让应用层面不再考虑这些问题;
  2. 传统软件层面的解决方案:比如spring cloud,从应用层面解决微服务的问题

比如:

  1. 硬件层面解决服务发现:用的是DNS服务器;
  2. 虚拟化硬件层面解决服务发现:KubeDNS/CoreDNS;
  3. 应用层面解决服务发现:spring cloud Eureka;

应用层面的解决方案使得应用不得不直面分布式的问题(只不过被spring cloud封装了),本质上这是对应用开发的一种“侵入”(只不过通过spring cloud的工作,侵入性变小了)。虚拟化的基础设施从虚拟化单个容器到虚拟化多个容器构成的集群、通信网络、存储设施,假设虚拟化的硬件能够跟上软件的灵活性,那么和业务无关的分布式问题就可以在虚拟化硬件层面解决,软件可以专注业务。

服务网格(Service Mesh)

就目前来看,kubernetes并不如spring cloud完美,因为有些问题很难在基础设施层做精细化处理。

比如某个微服务一直返回50x,实际上是内部出了问题,应该熔断,软件层面很好判断。但虚拟化的基础设施是针对整个容器来管理的,粒度过粗,只能到容器层面,很难深入到response的返回值。

为了解决这一问题,虚拟化硬件引入了服务网格service mesh,也叫边车代理模式Sidecar Proxy:由系统在服务容器(kubernetes的pod)里自动注入一个通信代理服务器(像不像边三轮的挎斗),以类似中间人攻击的方式劫持服务容器的流量(现在一个pod里有两个container)。由于sidecar是软件,可以提供精细的流量处理、接收来自控制器的指令,实现熔断等操作。

没错,正是曼岛TT的sidecar,边三轮!

从长远来看,kubernetes可能如现在的linux一样,成为服务端的标准运行环境,从而淘汰spring cloud等对业务产生侵入的纯软件层面的解决方案。

sidecar

kubernetes的一个服务有多个pod,每个pod一般只有一个container,但是也可以有多个container。比如带sidecar的pod就是拥有两个container的pod

添加sidecar本质上就是在服务所在的pod里再起一个容器。比如,如果想收集服务的日志(假设使用filebeat),一般的做法就是:

  1. 在服务所在的pod里再创建一个filebeat容器
  2. 让服务容器和filebeat容器挂载同一个volume,这样filebeat就能读到服务容器的日志。之后filebeat按照配置把关心的某些日志发往logstash或kafka等组件;

如果不需要保留日志,这个volume可以使用ephemeral volume,既做到数据共享,又能够不持久化存储:Ephemeral Volume通常用于存储短暂的数据,例如日志文件、临时文件等。另外,Ephemeral Volume还可以用于在容器之间共享数据。例如,多个容器可以共享一个Ephemeral Volume,以便在它们之间传递数据或共享状态。

在这里附上在rancher上使用filebeat的流程,一般的做法是:

  1. 在rancher上使用(kubernetes的)configmap创建配置文件:其实就是创建一对key value,key为filebeat.yml,value为文件内容;
  2. 把configmap作为一个volume,挂载在filebeat的容器上;
  3. 挂载点要看filebeat容器读取配置的位置。如果从/filebeat/filebeat.yml读取配置文件,那么configmap的挂载点就是/filebeatconfigmap里也可能有多个文件(多对key value,每一对是一个文件及其对应的内容),如果指向把configmap里的filebeat.yml挂载上去,可以在挂载时设置sub path in volume设置为filebeat.yml,挂载后的/filebeat目录下只会有filebeat.yml一个文件

如果只想将其中的一个子目录挂载到容器中,可以使用subPath参数来指定子目录的路径,从而实现只挂载volume的某个文件或文件夹到容器

无服务(器)serverless/云计算

一开始搞分布式是因为单台机器性能不足,后来搞分布式主要是为了容错能力,当然也能获得更好的性能。serverless则从另一个角度出发:如果你想要几乎无尽的性能,同时又不想采用复杂的分布式方案,那么花钱就行。比如亚马逊的lambda serverless计算平台、阿里云腾讯云的serverless。

serviceless才是无服务,serverless是无服务器。翻译的有问题。

serverless以简单为卖点,涉及两块内容:

  1. backend:指后端的设施如db、mq、log等都在云里,BaaS(backend as a service,后端即服务),不用操心自己部署;
  2. function:指业务逻辑代码。FaaS(function as a service,函数即服务)。开发者只需要关注纯业务就行了;

怪不得亚马逊取名lambda。function、lambda。

serverless走的是一条非分布式路线,只不过它是云端的单体。

不过serverless更大的意义在于云,和单体或分布式没有绝对的绑定关系。那么在未来分布式+云(用云函数实现微服务架构)也是很有可能的。

总结

架构不是被发明出来的,而是持续演进的结果。虽然很难想象以后架构会发展成什么样,但我们只需要选择当下合适的架构解决自己的问题就行了。

Alan Turing:尽管目光所及之处,只是不远的前方,即使如此,依然可以看到那里有许多值得去完成的工作在等待我们(We can only see a short distance ahead, but we can see plenty there that needs to be done)。

本文由作者按照 CC BY 4.0 进行授权