后台开发

微服务相关的一些问题

Posted by Mcoder on March 22, 2020 文章阅读量

微服务架构

背景

传统的WEB应用核心分为业务逻辑、适配器以及API或通过UI访问的WEB界面。业务逻辑定义业务流程、业务规则以及领域实体。适配器包括数据库访问组件、消息组件以及访问接口等。

通常我们把所有的模块写在同一个程序中单体打包,部署在服务器,比如对于 java 应用来说,可以打包成 jar 或者 war ,部署在 Tomcat 容器中。 这种传统方式适合于小型业务,开发快、代码集中、易于 Debug 、部署方便、不同模块间没有网络损耗。

但对于现在大型互联网公司来说,这种传统方式已经无法适用了:

  • 开发效率低:大量的开发都集中在同一个代码仓库中,提交、同步代码时需要频繁的进行代码合并;
  • 代码维护难:代码功能耦合在一起,新人难以维护老代码;
  • 部署不灵活:一个小改动就需要将整个系统的代码重新编译部署;
  • 稳定性存疑:某一个模块出了 bug ,整个系统都可能会死掉(比如 C++ 发生段错误,整个进程会被杀掉;
  • 拓展性不够:如果业务量超过了单机负载能力,除了提升服务器性能没有很好的方法。

基本模式

微服务运用了以业务功能的设计概念,应用程序在设计时就能先以业务功能或流程设计先行分割,将各个业务功能都独立实现成一个能自主运行的个体服务,然后再利用相同的协议将所有应用程序需要的服务都组合起来,形成一个应用程序。若需要针对特定业务功能进行扩展时,只要对该业务功能的服务进行扩展就好,不需要整个应用程序都扩展,同时,由于微服务是以业务功能导向的实现,因此不会受到应用程序的干扰,微服务的管理员可以视运算资源的需要来配置微服务到不同的运算资源内,或是布建新的运算资源并将它配置进去。

几个问题点

微服务和传统设计存在一些不同,因此引入了新的问题点。

数据库

首先微服务面临的问题是数据库设计的问题。通常有如下三类实际方式:

  • 每个服务都各有一个数据库,同属性的服务可共享同个数据库。
  • 所有服务都共享同个数据库,但是不同表格,并且不会跨域访问。
  • 每个服务都有自己的数据库,就算是同属性的服务也是,数据库并不会共享。

但值得注意的是,为了实现微服务各模块独立运行互不影响,这里的数据库并不只存放该服务所需要的数据,而是指该个服务需要用到的所有数据,这样可以将各个服务之间的依赖关系降低。

如果一个数据库服务于所有的微服务会有什么弊端呢?

  • 单点故障:数据库宕机了,所有依赖于该数据库的服务都会死掉,则微服务的解耦合完全没有意义。
  • 事务管理:当更多的服务共用同一个数据库,则发生加锁等待的频率会更高,导致实际的使用性能降低。
  • 扩展性差:当一个服务需要进行拓展时,对所需要的的表进行优化、拓展,可能会影响到其他的微服务。

Session 共享

比如我们将一个博客系统分为如下模块:用户管理、博客管理、评论模块,则用户登录了其中一个模块,如何让其他模块也获得用户的登录状态?

方案一:Session 共享

将各个微服务中的session放入redis中,通过读取redis来实现session共享。就获取登陆信息这个场景而言,具体步骤如下:

  1. 用户请求网关,网关将请求转发到登陆服务,进行用户名密码校验,校验通过后将sessionId和用户id关联存到redis中,并返回登陆前请求的页面;
  2. 调用其他微服务时,网关服务从redis中获取sessionId关联的用户id,若存在则已登录,则允许调用,否则未登录,重定向到登陆页面。

由于各个微服务都将session存在了redis中,所以这个session是全局共享的。这样做的好处是实现简单,因为Spring session已经集成了redis,可以很容易的将session存到redis,且可以做到单点登陆/登出的效果,但是从微服务的角度来说,会增加系统的耦合度。在实际应用中可以使用一个单独的redis服务器或者集群来用作session共享。

方案二:ip hash 方式

这种方式针对微服务集群而言,同一个微服务模块可能有很多个同样的机器作为集群对外提供服务,那么可能每次通过负载均衡服务器分配到的机器不一样,则如何保持之前的 Session 呢?

直接粗暴的解决方案便是让负载均衡服务器使用 哈希 算法,每次同一个 ip 只会分配到同一个台机器上,这样也能实现同样的 session 。(这种方法不好。)

方案三:token 方式

这种方式便是服务器不存放 Session,由客户端每次访问时附带一个 token,服务器从 token 中还原客户端信息,目前比较主流的是使用 JWT Token

JWT 方式便是 服务器认证以后,生成一个 JSON 对象给用户,以后用户和服务器通信时,用户都要附带这个 JSON,服务器从中还原用户信息。JSON 作为明文对象,每次传输都携带,非常的不安全,因此服务器在生成 JWT 时添加了些许字段,并使用签名进行加密。

则服务器无需存放 Session,不同微服务组件都可以从 Token 中还原出数据,因而可以保证各个组件访问到的数据是一致的。

消息沟通 消息队列

消息队列应用场景如下:

  1. 异步处理,例如用户注册时,需要的发送短信和邮件验证处理;
  2. 应用解耦,例如订单系统与库存系统的解耦;
  3. 流量消峰,例如在用户流量太大时,排队。

消息队列意味着你能够在 A 服务上广播一个“创建新用户”的事件,这个事件可以顺便带有新用户的数据。而 B 服务可以监听这个事件并在接收到之后有所处理。这些过程都是异步处理的,这意味着 A 服务并不需要等到 B 服务处理完该事件后才能继续,而这也代表 A 服务无法获取 B 服务的处理结果。

远程 RPC 调用

实际的项目中,所有的微服务组件之间完全解耦合很难做到,有的时候服务A想要调用服务B的某项服务的结果,便比较麻烦。也有实现方法:通过让服务B提供一个 HTTP RESTful 的接口,由服务A每次去调用该接口。

RPC(Remote Produce Call)是一种技术的概念名词,RPC 框架让开发者不用手动书写 http 请求,具体的代码实现封装在框架中,常见的框架比如 Dubbo。RPC的核心并不在于使用什么协议。RPC的目的是让你在本地调用远程的方法,而对你来说这个调用是透明的,你并不知道这个调用的方法是部署哪里。

通过 RPC 可以使服务解耦合,你只需要调用这个服务,但又不关系服务是谁实现的,服务部署在哪儿。RPC 调用分以下两种:

  1. 同步调用 客户方等待调用执行完成并返回结果。
  2. 异步调用 客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。 若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。异步和同步的区分在于是否等待服务端执行完成并返回结果。

服务探索 Consul

RPC 可以让不同的微服务组件之间进行通信,但如何知道和哪个微服务组件进行通信,则需要服务探索功能,常见的比如 Consul 框架。

单个微服务在上线的时候,会向服务探索中心(如:Consul)注册自己的 IP 位置、服务内容,如此一来就不需要向每个微服务表明自己的 IP 位置,也就不用替每个微服务单独设置。当服务需要调用另一个服务的时候,会去询问服务探索中心该服务的 IP 位置为何,得到位置后即可直接向目标服务调用。

这么做的用意是可以统一集中所有服务的位置,就不会分散于每个微服务中,且服务探索中心可以每隔一段时间就向微服务进行健康检查(如透过:TCP 调用、HTTP 调用、Ping),倘若该服务在时间内没有回应,则将其从服务中心移除,避免其他微服务对一个无回应的服务进行调用。

参考链接

  1. SpringCloud微服务session一致性解决方案
  2. wikipaedia
  3. JWT
  4. 初识服务发现及Consul框架的简单使用