什么是微服务?

微服务指的是一种应用架构,一系列独立的服务通过轻量级API来进行通信。

单体应用中随着功能的增加,版本的迭代,代码库会越变越大。尽管工程师努力地想要模块化每个功能模块,以达成优雅的工程化,事实上模块之间的界限变得模糊不清,代码的维护和Bug的修复会变得相当复杂。

微服务将一个巨大的单体应用拆分成若干个较小的服务,它们之间通过某种通信协议交互,最终组成整个系统服务。

比较理想的情况下,微服务通过分布式部署,这意味着开发人员可以并行开发多个微服务,进而压缩开发所需时间。对比传统的单体应用,微服务的应用要更加模块化且小巧,随着用户的激增和新功能的迭代,某一部分服务可以不断扩展,通过 Kubernetes 和云计算可以非常自由做扩缩容。

但微服务不是“银弹”,按照设计者的想法是想利用微服务灵活性和可扩展来构建出高可用高容错的系统服务。单一编程语言的单体应用很难推倒后以另一种编程语言来重写,这对产品的稳定性几乎是毁灭性打击,但微服务不同,它足够小,开发者可以短时间使用另一种编程语言重写它。

一个微服务构成的服务可能包含多种编程语言编写的服务,Go JAVA C# Python Node 等,当一个服务不在局限于某一种语言,设计者理所应当用不同编程语言的天然特性来面对复杂的现实场景,比如利用Go语言的轻便型,在网络编程方面它足够轻量和易用;使用 JAVA 可以应付大部分业务需求场景,它的泛用性足够广;在人工智能领域 Python 社区提供大量易用和前沿的代码库;

微服务可以让工程师更快引入一些新技术,并享受这些新技术带来的好处,相比单体应用,微服务总会在边缘场景中尝试一些新技术来验证它是否适合,在这种情况下即使出现风险也可控。

微服务会面对所有分布式系统需要面对的复杂性,为了得到微服务的好处,工程师往往需要在部署,测试和监控等基础设施上做大量工作,同时还需要考虑如何扩展系统,保证每个服务等弹性,多个相同服务部署后可能出现的不一致性。

什么是一个好的微服务?

构建一个微服务的前提是:要知道什么样的服务是好的,首先得明白什么样的服务是不好的,不好的服务会给整个系统来带什么样的影响。

松耦合和高内聚在微服务中是两个重要的概念,松耦合代表着微服务的独立修改,这点非常重要,如果修改一个服务需要修改另外一个服务则代表丧失松耦合;如果工程师需要更改某个业务逻辑或行为时需要在多个不同地方做修改,则代表着丧失高内聚。

目前最常见的集成形式是数据库集成,很多大型应用将服务拆分成若干个微服务,但这些服务都会访问同一个数据库,如果一个服务想从另外一个服务获取信息,可以直接访问数据库获取信息,如果想要修改某张表的数据,也会直接在数据库中修改。这种方式看来非常简单,但往往也是被错误理解和滥用导致的典型例子。

数据库是一个很大的共享API,只要能完全访问该数据库,那么微服务中的所有服务都是平等的,代表的是非常不稳定性。当业务发生变化,工程师想要改变相对应的逻辑,为了更好表示数据和可维护性修改表结构时,波及面不可避免的扩大。为了不影响其他服务,工程师必须小心避免修改与其他服务相关的表结构,从个人的经验上来看,某些情况是避无可避的,这种情况下压力就转移到了测试工程师的身上,需要做大量回归测试来保证功能的正确性。

使用微服务是为了让服务拥有一定的容错性,当修复一个Bug时需要修改多个不同的地方,并且要对这些修改分别编译并重新部署,这样内聚性和松耦合彻底丧失,微服务也将不复存在。

同步/异步

服务之间的通信是同步还是异步?这决定最终实现的细节。

如果使用同步通信,当发起一个远程服务调用后,调用方会阻塞自己并等待远程服务的回应,直到拿到返回结果或超时。如果使用异步通信,调用方不需要等待操作完成就可以直接返回,甚至不太关心这个操作的结果是否完成。

一般来说同步通信比较常见,当用户点击页面某个按钮,发起某个表单的提交都希望能快速确定成功与否。异步通信对于运行较长的任务来说非常有用,如果不是游戏领域中需要进行大量交互反馈,一般来说在微服务中开启客户端与服务端的长连接比较少见。

微服务设计中有一种设计风格叫做:基于事件驱动架构。

对于使用基于事件的协作方式来说,情况会颠倒过来。客户端不再发起请求,而是发布一个事件,然后期待其他的协作者收到该消息,并且知道该怎么做。

基于事件的系统天生就是异步的。

业务逻辑并非集中存在某个核心大脑,而是平均地分布在不同的协作者中,基于事件的协作方式耦合很低。客户端发布一个事件,但并不需要知道谁或者什么会对此作出响应,这也意味着,可以在不影响客户端的情况下对该事件添加新的订阅者。

从作者本人参与过的微服务设计来看,基于事件的异步协作方式能通过 Kafka RabbitMQ 这样的消息代理服务能处理微服务发布和消费者接收事件。无论哪个服务都可以使用API向中间件消息队列发布事件,消费者也可以通过它来订阅服务。从表面上来看这种系统拥有较好的伸缩性和弹性,可以根据业务需求动态扩展多个消费者和生产者。初期会享受到它带来的好处,比如实现松耦合,事件驱动架构的有效手段。

但是这种方式也不是银弹,它会增加开发流程的复杂度,你还需要一个全流程的测试环境才能加速开发和测试。这种设计会导致过分依赖中间件,你需要额外的机器和专业来保证这些基础设施的正常运行,试想,当所有的服务依赖中间件消息的推送才能保持系统的正常运行,那中间件变成某种胶水,它做为粘合剂将所有服务牢牢绑在一起,如果某天这个胶水失效了怎么办?

在《微服务设计》一书中有一个例子:假设代码中存在一个Bug,某个服务的请求会导致另外一个服务崩溃,当使用事务处理队列后:当工作者服务崩溃后,这个请求上的锁会超时,然后该请求被放回到队列中。另一个工作者会尝试从新处理这个请求,当然它也会崩溃……

这个一个灾难性故障转移的例子,除了代码中的Bug以外,设置最大重试的次数也非常重要。事件驱动架构和异步编程肯定会来带复杂性,这点是无法避开的,无论是软件开发中还是基础设施建设上,架构师在设计时需要确保各个流程有很好的监控机制,以此方便跨进程的请求跟踪。

要保证中间件的高可用可以从基础设施的角度来保证它的稳定性,当你决定用基于事件的架构设计时,需要投入更多的成本在基础设施方向和代码审核,以此来保证整个系统的稳定性。

微服务架构的挑战

对服务器开发有经验的工程师会熟知,微服务中包含三大挑战:复制客户端;嵌套响应的持久化;预测执行;

Aegean: Replication beyond the client-server model1这篇论文中对于上述挑战提出了一些建议,比如使用服务器垫片来解决复制客户端的问题,本文会简单介绍这些问题和解决方案。

复制客户端

在微服务中,一组复制的中间服务可能像其他服务发送请求,这些服务副本发送的不同请求应该在逻辑上被看作单个请求。

简单点描述就是:在微服务中当中间件使用主动复制协议后,后端服务会请求大量的重复请求,为了保证所有的请求都得到相同的响应,同时又不想修改后端服务,所以衍生出一种叫做[服务器垫片]的方案。

服务器垫片会和服务器一起执行,当中间服务发出的所有请求会先经过服务器垫片再到后端服务,垫片会认证其他服务器发送的请求并确认发送方的身份,同时还会将相同请求变成一个请求,再转发到后端。当后端服务器响应后,垫片将同一个响应体信息广播给所有请求。

这种方式有点像服务网格中的“边车”模式,垫片主要起到一个API网关的压缩和扩散。

嵌套响应的持久化

当中间服务接收到其他服务返回的响应时,它应该先保证该响应被足够的副本确认,这样才能在宕机时保证其他副本能够正确返回响应;

在客户端服务器模型中,副本服务的输入仅仅来源于客户端的输入,而在微服务的架构中,嵌套请求的响应也是中间服务的输入;因为传统的复制协议需要所有的输入都必须持久化地存储到日志中,所以在多服务的设置中,我们需要保证来自客户端的输入嵌套响应都存储在日志中,只有在持久化日志之后,我们才可以向客户端或者后端服务提交变更。

预测执行

在预测状态下,中间服务不能发出任何的嵌套请求,这可能会导致后端服务提交未经确认的状态;

在微服务中会遇到和分布式架构中的一致性问题,如果多个服务中在没有达成一致前,就进行预测执行调用下游服务提交变更请求,如果这时要求回滚就会陷入不一致性。为了解决预测来带的不确定性,在请求执行的不同阶段引入屏障来对齐多个副本之间的状态。

可以编写一个第三方校验服务,只有在状态达成一致时才让副本执行具体的任务。

总结

微服务是个比较复杂的话题,在如何设计一个比较好的服务,既不贫血也要高内聚,在架构方面还要做到低耦合绝不是个容事。在建模方面可以参考领域驱动设计(DDD)一书,如何划分上下文,分析各个服务的边界终归要根据实际业务场景来分析。在构建大型反脆弱系统时,领域驱动设计,CI/CD,容器化,编排调度,这些词汇越来越流行,但具体到如何划分微服务则需要更多的实践经验。

Jon Eaves 认为一个微服务应该可以在两周内完全重写,这个经验法则来自他的经验判断,作者本人认同这句话在他当前项目中肯定是有效的,但觉得太过模糊。

更老套的回答是:足够小即可,不要过小。《微服务设计》这本书的作者认为,如果工程师认为自己的系统过大,想把它拆分成更小的服务,那么这意味着大家都明白什么叫“过大”,当工程师拆分成足够合适大小的服务时,那么就已经完成了单体应用程序向微服务的转变。

本文也是以这样的思想来书写的,全文都是谈论如何避开一个不好的微服务设计,那么摒弃掉不好的,剩下的就是合适的。