“容器和微服务是计算的未来” ——Eric Brewer

单体应用

在很长一段时间内,服务端的程序都处于「单体应用(monolithic)」的形态。所谓单体应用,就是应用的所有模块都在一个项目(程序)内,这也是最自然的方式,开发简单、门槛低。

但随着互联网的发展,用户不断增多,随之而来的是数据量的指数级提升。计算压力越来越大,单机服务器显然难以应对这样情况,那么就需要一个办法来解决应用的性能问题,既然单台服务器性能不足,最简单粗暴的方法就是加服务器。这时候的服务端应用开始逐渐往「分布式系统(distributed system)」发展

分布式

在初期,大家还是单体应用+分布式/集群的方案。将应用部署在一起或异地的多台服务器上,然后通过负载均衡将请求转发到各个服务器上,平摊了压力,看起来似乎解决了性能问题,但分布式系统却带来了更多棘手问题和新的挑战。

其中最重要的就是 「CAP 理论」,CAP 理论在我的另一篇文章有说明,大概意思是分布式系统无法同时保持数据一致性、可用性、分区容错性。分区容错性是分布式系统必须的,因为分布式环境下分区问题不可能避免,那么就是说,分布式系统无法同时保证一致性和可用性,无法保证系统对用户绝对可用的同时,各个节点(服务器)的数据一致。

比如像防止超卖这种问题,在单机应用上大部分关系型数据库都有 ACID 性质,所以可以轻松通过加锁和事务解决。但在分布式系统中,服务器和数据库在物理上是不同的,想要实现分布式锁和分布式事务就相当困难。现在的主流解决方案就是各退一步:保证服务基本可用,允许数据在不影响可用性的情况下存在中间状态,并在最终期限内达成一致性。这即是 「BASE 理论」。

分布式系统虽然使系统更加复杂,带来了新的挑战,但也在一定程序上解决了性能问题。不过一切似乎都没有这么简单,现在虽然是分布式部署了,但应用仍然是单体结构的。随着产品的发展,业务需求接踵而至。在 deadline 和一次次迭代下,应用变得越来越臃肿,可维护性越来越差。老手不敢改,新人难下手。而且由于过于臃肿,就算是只修改一行代码,也需要忍受长时间的编译和启动,极大降低了开发效率,对于持续部署和自动化测试来说,更是致命。所有模块运行在同一进程,一个严重 bug 和内存泄漏就能拖垮整个应用,仿佛用牙签堆成的高楼大厦,随时会倾塌。用新技术和框架重写也是几乎不可能的,这种应用也被称为「单体地狱」。

微服务

「微服务(microservice)」在这个时候渐渐走进大家视野,带来了完全崭新的开发方式。微服务将应用按照业务拆分成多个模块(重点:按照业务拆分,而不是按照技术拆分),这些模块就被称为服务。每个服务就像一个单体应用,都是完全隔离且独立的,且有着自己的数据库,能够单独部署。这是和传统模块化开发最重要的区别;服务之间则通过 REST、RPC、消息队列等方式进行同步或异步的远程调用。

例如一个在线打车应用,可以分为乘客服务、司机服务、订单服务、支付服务、定位服务、通知服务等等。通过微服务,我们就解决了单体地狱的不少问题。微服务分解和解耦了应用,每个服务都是轻量的,可以很方便地进行维护和开发。不同的服务可以由不同团队进行开发,语言和技术栈完全不受限制,只要提供的接口一致即可;这对年代久远的应用无疑是个福音,可以通过迭代更新服务的方式来轻松地更换到全新的技术栈。而独立部署使得其对持续部署和自动化测试相当友好。不同服务处于不同的服务器,就算一个服务的不可用也不会影响整个应用,同时假设某些服务需要执行图像计算,也可以对其进行针对性硬件适配,提高了性能利用率。

当然没有技术是完美的,就像分布式+单体应用一样,微服务也带来了新的问题。

首先,服务之间如何通信?每个服务也可以是集群部署的,还可能动态扩容,直接通过 IP 互相访问肯定不现实,这时候需要一个能够感知和管理所有服务的组件,一般称为「注册中心」,这个行为叫「服务发现」和「服务治理」。每个服务启动和注销时都会通知服务中心,调用其他服务时也会通过注册中心(同步通信的情况下),注册中心也可以实现负载均衡,决定具体连接哪台服务器。也有通过消息队列实现的完全解耦的异步通信,各有优劣;一般都会同时使用,根据不同的场景进行配合

二、分布式+单体应用中每台服务器都是完整的应用,就算一台挂了也影响不大。但微服务中会进行互相调用,一个服务出现问题虽然不会导致整个应用的崩溃,但别的服务对其进行调用时就出现了长时间等待。特别是服务调用链路很长的时候,这个问题是灾难性的。这时候就需要「熔断器」了。当检测到一个服务不可用时,熔断器会直接将其下线一段时间进行隔离(称为「降级」)。别的服务对其调用时可以返回预设的内容(例如空数据)或直接使用本地缓存。再根据这个内容对用户进行友好的提示,这样就解决了可用性的问题

三、如何定位故障?随着服务越来越多,调用链路也越来越长、越来越复杂,出现问题也难以定位。「服务链路追踪」就可以帮助我们监控服务的依赖和调用关系。其最早来自于 2010 年的 Google Dapper 论文,原理是给每一次调用添加一个标识,通过对标识排序,就可以清晰地看出服务的先后调用关系,再加上调用者的标识和调用与返回的时间戳,就能得到调用的层级关系和调用时间。这样一来就能轻松监控每一次调用的链路,定位问题和优化性能

四、客户端如何访问各个服务?对于用户来说不管你是不是微服务,这就是单独的一个应用。让客户端直接通过各个服务的地址去访问肯定也不现实,既然服务与服务之间有注册中心统一服务的通信,那么针对客户端也有「API 网关」统一对外接口。客户端就只用像单体应用一样只访问这一个地址即可,API 网关会统一进行授权、校验、流控、路由和转发请求等,进行 API 的统一管理。但 API 网关也可能成为单点故障点或性能瓶颈

如同 CAP 理论 和 BASE 理论一样,这些问题其实并不能完美地解决,只能做到在一定程度内令人接受,这种妥协在软件系统中也是非常常见的

总结

可以发现,从单体应用到分布式到微服务,系统架构越来越复杂,门槛越来越高,要解决的问题也越来越多。但在如今的发展下,微服务仍然是大型系统服务端架构的主流选择,与 DevOps 和敏捷开发更是相辅相成。Java 系的解决方案也非常成熟,常见的有 SpringCloud 和 Dubbo + Zookeeper 等。但也要明白技术没有绝对的优劣,选型时要注意应用场景,不要盲目跟风。微服务解决了大型系统的维护、扩展和性能问题,在小型系统上盲目使用微服务,这些需要解决的问题既不存在,同时也引入了微服务和分布式系统产生的一系列问题,得不偿失

要时刻谨记软件工程中「没有银弹」:

“没有任何一种单纯的技术或管理上的进步,能够独立地承诺在十年内大幅度地提高生成率、可靠性和简洁性。”
—— Fred Brooks