微服务(1)- 微服务架构

最后更新:2020-03-01

1. 单体应用的挑战

扩容困难

每次上线项目需要加机器总会遇到资源不足的情况,还要走非常复杂工单审批流程,还要与运维人员不断PK,才能申请下来资源,整个流程冗长,机器受限于资源申请困难。

部署困难

即使只修改了一行代码,也需要重新部署整个应用,这种部署影响很大、风险更高

部署的频率变低意味着两次发布之间我们对软件做了很多功能增强,但直到最后一刻才把这些大量的变更一次性发布到生产环境中

两次发布之间的差异越大、出错的可能性就更大

适配新技术困难

只能长期使用在开发初期选定的技术栈,即使使用的应用平台框架已经过时,也很难将应用迁移到其它更新并且更完善的框架当中

效率和质量降低

没有清晰的模块边界,模块化会逐渐消失,相似的功能代码开始在代码库中随处可见

新成员难以理解和修改代码

代码质量逐步下滑,陷入恶性循环

扩展困难

只能通过增加多个应用实例来实现扩展

每个应用实例需要访问所有数据,加大内存占用和I/O流量

无法单独伸缩每个组件,即使应用只有一小部分有性能问题,不同的组件有不同的资源需求——有的是CPU密集型的,有的是IO密集型的

快速开发困难

项目中采用单体应用,里面集成了太多功能模块,无法快速进行功能开发并且很容易牵一发动全身。

规模化开发困难

应用一旦达到特定规模,需要将现有组织拆分成多个团队,每个团队负责不同的功能模块。然而单体应用使团队无法独立展开工作,团队需要在工作进度和重新部署上进行协调

测试困难

测试人员没有自动化测试框架,或者Mock系统,导致只能采用简单的人工测试流程,而且还经常发生功能覆盖不全面等问题。

服用组件困难

各个组件之间的耦合度原来越高,无法再新项目中复用已实现的组件

2. 什么是微服务

微服务就是一些协同工作的小而自治的服务

小:很小,专注于做好一件事:随着新功能的增加,代码库会越变越大,时间久了代码库会非常庞大。以至于想要知道该在什么地方做修改都很困难。尽管我们想在巨大的代码库中做到清晰地模块化,但事实上这些模块之间的界限很难维护。相似的功能代码开始在代码库中随处可见,使得修复BUG或实现更加困难。

单一职责原则:把因相同原因而变化的东西聚合到一起,而把因不同原因而变化的东西分离开来。

根据业务的边界来确定服务的边界。

足够小即可,不要过小。当你不再感觉你的代码库过大,可能它就足够小了。

该服务是否能够很好地与团队结构相匹配。如果代码库过大,一个小团队无法正常维护,那么狠显然应该将其拆成更小的。

使用的服务越小,独立性带来的好处就越多。但是管理大量服务也会越复杂。

自治:

一个微服务就是一个独立的实体。服务之间均通过网络调用进行通信,从而加强了服务之间的隔离性,避免紧耦合。

这些服务应该可以彼此独立进行修改,并且某个服务的部署不应该引起该服务消费方的变动。

对于一个服务来说,我们需要考虑的是什么应该暴露,什么应该隐藏。如果暴露的过多,那么服务消费方会与改服务的内部实现产生耦合。这会使得服务和消费方之间产生额外的协调工作,从而降低服务的自治性。

服务会暴露出API,然后服务之间通过这些API进行通信。API的实现技术应该避免与消费方耦合,这就意味着应该选择与具体技术不相关的API实现方式,以保证技术的选择不被限制。

黄金法则:你是否能够修改一个服务并对其进行部署,而不影响其他任何服务?

1.1. 微服务的好处

技术异构性:可以在不同的服务中使用最合适该服务的技术。可以使用不同的技术栈,不同的数据存储技术。

弹性:如果系统的一个组件不可用了,但并没有导致级联故障,那么系统的其他部分还可以正常运行。服务边界就是一个很显然的舱壁。在单块系统中,如果服务不可用,那么所有的功能都会不可用。

扩展:庞大的单块服务只能作为一个整体进行扩展。即使系统中只有一小部分存在性能问题,也需要对整个服务进行扩展。如果使用较小的多个服务,则可以只对需要扩展的服务进行扩展,这样就可以把那些不需要扩展的服务运行在更小的、性能更差的硬件上。

简化部署:庞大的单块应用程序中,即使只修改了一行代码,也需要重新部署整个应用程序才能够发布该变更。这种部署影响很大、风险更高。于是在实际操作中,部署的频率就好变得很低。这意味着两次发布之间我们对软件做了很多功能增强,但直到最后一刻才把这些大量的变更一次性发布到生产环境中。这会导致另一个问题:两次发布之间的差异越大、出错的可能性就更大。在微服务架构中,各个服务的部署是独立的,这样就可以更快地对特定部分的代码进行部署。如果真的出了问题,也只会影响一个服务,并且容易快速回滚。

与组织结构相匹配:微服务架构可以很好地将架构与组织结构相匹配,避免出现过大的代码库,从而获得理想的团队大小及生产力。

可组合性:在微服务架构中,根据不同的目的,人们可以通过不同的方式使用同一个功能。

对可代替性的优化:当使用多个小规模服务时,重新实现某一个服务或者是直接删除该服务都是相对可操作的。

1.2. 微服务的挑战

  • 一旦设计不合理,交叉调用,相互依赖频繁,就会出现牵一发动全身的局面。想象单个应用内Service层依赖复杂的场面就明白了。
  • 项目多了,轮子需求也会变多,需要有人专注公共代码的开发。
  • 开发过程的质量需要通过持续集成(CI)严格把控,提高自动化测试的比例,因为往往一个接口改动会涉及多个项目,光靠人工测试很难覆盖所有情况。
  • 发布过程会变得复杂,因为微服务要发挥全部能力需要容器化的加持,容器编排就是最大的挑战。
  • 线上运维,当系统出现问题需要快速定位到某个机器节点或具体服务,监控和链路日志分析都必不可少。

1.3. 微服务的陷阱

很多团队在实践时不加思考地采用微服务——既不考虑团队的规模,也不考虑业务的发展,也没有考虑基础技术的支撑,只是觉得微服务很牛就赶紧来实施,以为实施了微服务后就什么问题都解决了,而一旦真正实施后才发现掉到微服务的坑里面去了

  • 服务划分过细,服务间关系复杂

服务划分过细,单个服务的复杂度确实下降了,但整个系统的复杂度却上升了,因为微服务将系统内的复杂度转移为系统间的复杂度了。从理论的角度来计算,n 个服务的复杂度是 n×(n-1)/2,整体系统的复杂度是随着微服务数量的增加呈指数级增加的。下图形象了说明了整体复杂度:

  • 服务数量太多,团队效率急剧下降

微服务的“微”字,本身就是一个陷阱,很多团队看到“微”字后,就想到必须将服务拆分得很细,有的团队人员规模是 5 ~ 6 个人,然而却拆分出 30 多个微服务,平均每个人要维护 5 个以上的微服务。这样做给工作效率带来了明显的影响,

一个简单的需求开发就需要涉及多个微服务,光是微服务之间的接口就有 6 ~ 7 个,无论是设计、开发、测试、部署,都需要工程师不停地在不同的服务间切换。

开发工程师要设计多个接口,打开多个工程,调试时要部署多个程序,提测时打多个包。

测试工程师要部署多个环境,准备多个微服务的数据,测试多个接口。

运维工程师每次上线都要操作多个微服务,并且微服务之间可能还有依赖关系。

  • 调用链太长,性能下降

由于微服务之间都是通过 HTTP 或者 RPC 调用的,每次调用必须经过网络。一般线上的业务接口之间的调用,平均响应时间大约为 50 毫秒,如果用户的一起请求需要经过 6 次微服务调用,则性能消耗就是 300 毫秒,这在很多高性能业务场景下是难以满足需求的。为了支撑业务请求,可能需要大幅增加硬件,这就导致了硬件成本的大幅上升。

  • 调用链太长,问题定位困难

系统拆分为微服务后,一次用户请求需要多个微服务协同处理,任意微服务的故障都将导致整个业务失败。然而由于微服务数量较多,且故障存在扩散现象,快速定位到底是哪个微服务故障是一件复杂的事情。下面是一个典型样例。

Service C 的数据库出现慢查询,导致 Service C 给 Service B 的响应错误,Service B 给 Service A 的响应错误,Service A 给用户的响应错误。我们在实际定位时是不会有样例图中这么清晰的,最开始是用户报错,这时我们首先会去查 Service A。导致 Service A 故障的原因有很多,我们可能要花半个小时甚至 1 个小时才能发现是 Service B 返回错误导致的。于是我们又去查 Service B,这相当于重复 Service A 故障定位的步骤……如此循环下去,最后可能花费了几个小时才能定位到是 Service C 的数据库慢查询导致了错误。

如果多个微服务同时发生不同类型的故障,则定位故障更加复杂,如下图所示。

Service C 的数据库发生慢查询故障,同时 Service C 到 Service D 的网络出现故障,此时到底是哪个原因导致了 Service C 返回 Error 给 Service B,需要大量的信息和人力去排查。

  • 没有自动化支撑,无法快速交付

如果没有相应的自动化系统进行支撑,都是靠人工去操作,那么微服务不但达不到快速交付的目的,甚至还不如一个大而全的系统效率高。例如:没有自动化测试支撑,每次测试时需要测试大量接口。没有自动化部署支撑,每次部署 6 ~ 7 个服务,几十台机器,运维人员敲 shell 命令逐台部署,手都要敲麻。没有自动化监控,每次故障定位都需要人工查几十台机器几百个微服务的各种状态和各种日志文件。

  • 没有服务治理,微服务数量多了后管理混乱

信奉微服务理念的设计人员总是强调微服务的 lightweight 特性。但具体实践后就会发现,随着微服务种类和数量越来越多,如果没有服务治理系统进行支撑,微服务提倡的 lightweight 就会变成问题。主要问题有:

服务路由:假设某个微服务有 60 个节点,部署在 20 台机器上,那么其他依赖的微服务如何知道这个部署情况呢?

服务故障隔离:假设上述例子中的 60 个节点有 5 个节点发生故障了,依赖的微服务如何处理这种情况呢?

服务注册和发现:同样是上述的例子,现在我们决定从 60 个节点扩容到 80 个节点,或者将 60 个节点缩减为 40 个节点,新增或者减少的节点如何让依赖的服务知道呢?

如果以上场景都依赖人工去管理,整个系统将陷入一片混乱,最终的解决方案必须依赖自动化的服务管理系统,

耗费更多服务器资源

例如我们将原来的单体式架构拆分为了 6 个微服务。考虑到高可用,每个服务至少需要部署在 2 个节点上,再加上网关层需要 2 台服务器,最终,我们一共部署了 14 台服务器。在这个拆分过程中,业务没有变,流量没有变,代码逻辑改动也不大,却无缘无故多出了 10 台服务器。

3. 如何大规模的使用微服务

我们真正使用微服务的时候,有很多需要注意和关注的点:

故障无所不在

网络是不可靠,只能尽力限制引起故障的因数,达到一定规模后,故障不可避免。

跨功能需求

服务吞吐量、可用性和数据持久性等这些需求需要持续测量,并保证服务满足可接受的目标。

功能降级

构建弹性系统,因微服务功能分散,在有可能down机的微服务上,能够安全的降级以保证弹性

超时

设置超时时间对于调用下游服务十分重要,超时时间设置太长有可能把下游系统拖慢,设置太短可能下游服务未处理完成。最好设置一个默认的超时时间,当超时发生时后,记录到日志里看看发生了什么,并且做响应的调整。

断路器

使用断路器,当请求下游服务发生一定数量的失败后,短路器打开,接下来的请求快速失败。一断时间后,查看下游服务是否已服务,重置断路器。

舱壁

为每个下游服务建立单独的连接池。超时和断路器资源受限时释放资源,舱壁第一时间确保它不成为限制。还有一个拒绝请求的舱壁,用以避免资源饱和,称之为减载。

隔离

当下游服务离线,上游服务不受影响。设置成为服务间隔离。

幂等

幂等操作,多次执行所产生的影响,均与一次执行影响相同。可以把某些特定业务操作设计成幂等的,比如客户下单送积分

4. 编排和协同

编排:依赖于某个中心大脑来指导并驱动整个流程,等待各个服务的返回结果。编排服务作为中心控制点承担了太多的职责,它会称为网状结构的中心枢纽及很多逻辑的起点,而与其打交道的那些服务通常都会沦为贫血的基于CRUD的服务。优点知道业务流程中每一步跨服务调用结果,缺点容易承担太多的调用,太耗时,导致调用方的不稳定性。

协同:仅仅会告知系统中各个部分各自的职责,而把具体怎么做的细节留给它们自己。这种方法可以显著地消除耦合。但这意味着需要做一些额外的工作来监控流程,以保证其正确地进行。不过可通过消费方处理完成后,回调服务方告知处理结果。

5. 微服务基础设施

大部分人主要关注的是微服务的“small”和“lightweight”特性,但实际上真正决定微服务成败的,恰恰是那个被大部分人都忽略的“automated”。为何这样说呢?因为服务粒度即使划分不合理,实际落地后如果团队遇到麻烦,自然会想到拆服务或者合服务;如果“automated”相关的基础设施不健全,那微服务就是焦油坑,让研发、测试、运维陷入各种微服务陷阱中。

微服务基础设施如下图所示

确实如此,微服务并不是很多人认为的那样又简单又轻量级。要做好微服务,这些基础设施都是必不可少的,否则微服务就会变成一个焦油坑,让业务和团队在里面不断挣扎且无法自拔。因此也可以说,微服务并没有减少复杂度,而只是将复杂度转移到了基础设施。

通常情况下,可以按照下面优先级来搭建基础设施:

  1. 服务发现、服务路由、服务容错:这是最基本的微服务基础设施。

  2. 接口框架、API 网关:主要是为了提升开发效率,接口框架是提升内部服务的开发效率,API 网关是为了提升与外部服务对接的效率。

  3. 自动化部署、自动化测试、配置中心:主要是为了提升测试和运维效率。

  4. 服务监控、服务跟踪、服务安全:主要是为了进一步提升运维效率。

以上 3 和 4 两类基础设施,其重要性会随着微服务节点数量增加而越来越重要,但在微服务节点数量较少的时候,可以通过人工的方式支撑,虽然效率不高,但也基本能够顶住。

参考资料

http://mp.weixin.qq.com/s/yRu4aD9e3CXNPSSwxna9zw

《从0开始学架构》

Edgar

Edgar
一个略懂Java的小菜比