高并发架构演进之路(下)——一体化架构到微服务

一体化机构问题:

1、在技术层面上,数据库连接数可能成为系统的瓶颈;资源极限,扩展困难

2、继续演进研发成本、共同成本高,业务耦合大,查问题复杂,团队管理困难

3、运维成本大,编译、测试上线复杂

解决方案:

按照业务做横向拆分的方式,解决了数据库层面的扩展性问题;将与业务无关的公用服务抽取出来,下沉成单独的服务。

服务拆分时要遵循哪些原则?

服务的边界如何确定?

服务的粒度是怎样呢?

在服务化之后,会遇到哪些问题呢?

原则一,做到单一服务内部功能的高内聚,和低耦合

原则二,你需要关注服务拆分的粒度,先粗略拆分,再逐渐细化。

原则三,拆分的过程,要尽量避免影响产品的日常功能迭代,也就是说,要一边做产品功能迭代,一边完成服务化拆分。先边缘独立,再依赖优先。

原则四,服务接口的定义要具备可扩展性。

微服务化带来的问题和解决思路

1. 服务接口的调用,不再是同一进程内的方法调用,而是跨进程的网络调用,这会增加接口响应时间的增加。——私有网络协议RPC

2. 多个服务之间有着错综复杂的依赖关系。——微服务注册

3. 服务拆分到多个进程后,一条请求的调用链路上,涉及多个服务,那么一旦这个请求的响应时间增长,或者是出现错误,我们就很难知道,是哪一个服务出现的问题——分布式追踪、监控

如何提升网络传输性能:

选择好的网络

网络参数调优

优化编解码速度

所谓 I/O 模型,就是我们处理 I/O 的方式。而一般单次 I/O 请求会分为两个阶段,每个阶段对于 I/O 的处理方式是不同的。

首先,I/O 会经历一个等待资源的阶段,比方说,等待网络传输数据可用,在这个过程中我们对 I/O 会有两种处理方式:

阻塞。指的是在数据不可用时,I/O 请求一直阻塞,直到数据返回;

非阻塞。指的是数据不可用时,I/O 请求立即返回,直到被通知资源可用为止。

然后是使用资源的阶段,比如说从网络上接收到数据,并且拷贝到应用程序的缓冲区里面。在这个阶段我们也会有两种处理方式:

同步处理。指的是 I/O 请求在读取或者写入数据时会阻塞,直到读取或者写入数据完成;

异步处理。指的是 I/O 请求在读取或者写入数据时立即返回,当操作系统处理完成 I/O 请求,并且将数据拷贝到用户提供的缓冲区后,再通知应用 I/O 请求执行完成。

将这两个阶段的四种处理方式,做一些排列组合,再做一些补充,就得到了我们常见的五种 I/O 模型:

同步阻塞 I/O

同步非阻塞 I/O

同步多路 I/O

复用信号驱动 I/O

异步 I/O

1. 选择高性能的 I/O 模型,这里我推荐使用同步多路 I/O 复用模型;

2. 调试网络参数,这里面有一些经验值的推荐。比如将 tcp_nodelay 设置为 true,也有一些参数需要在运行中来调试,比如接受缓冲区和发送缓冲区的大小,客户端连接请求缓冲队列的大小(back log)等等;

3. 序列化协议依据具体业务来选择。如果对性能要求不高,可以选择 JSON,否则可以从 Thrift 和 Protobuf 中选择其一。

注册中心:

其一是提供了服务地址的存储;

其二是当存储内容发生变化时,可以将变更的内容推送给客户端。

客户端会与注册中心建立连接,并且告诉注册中心,它对哪一组服务感兴趣;

服务端向注册中心注册服务后,注册中心会将最新的服务注册信息通知给客户端;

客户端拿到服务端的地址之后就可以向服务端发起调用请求了

注册中心可以让我们动态地,变更 RPC 服务的节点信息,对于动态扩缩容,故障快速恢复,以及服务的优雅关闭都有重要的意义;心跳机制是一种常见的探测服务状态的方式,你在实际的项目中也可以考虑使用;我们需要对注册中心中管理的节点提供一些保护策略,避免节点被过度摘除导致的服务不可用。

分布式追踪

服务的追踪的需求主要有两点,一点对代码要无侵入,你可以使用切面编程的方式来解决;另一点是性能上要低损耗,我建议你采用静态代理和日志采样的方式,来尽量减少追踪日志对于系统性能的影响;无论是单体系统还是服务化架构,无论是服务追踪还是业务问题排查,你都需要在日志中增加 requestId,这样可以将你的日志串起来,给你呈现一个完整的问题场景。如果 requestId 可以在客户端上生成,在请求业务接口的时候传递给服务端,那么就可以把客户端的日志体系也整合进来,对于问题的排查帮助更大。

采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系(这里 traceId 就是 requestId),也就是使用 traceId 串起单次请求,用 spanId 记录每一次 RPC 调用。

切面编程的实现分为两类:一类是静态代理,典型的代表是 AspectJ,它的特点是在编译期做切面代码注入;另一类是动态代理,典型的代表是 Spring AOP,它的特点是在运行期做切面代码注入。

考虑如何减少日志的数量。你可以考虑对请求做采样,采样的方式也简单,比如你想采样 10% 的日志,那么你可以只打印“requestId%10==0”的请求。

把日志不打印到本地文件中,而是发送到消息队列里,再由消息处理程序写入到集中存储中,比如 Elasticsearch。

1. 在记录打点日志时,我们使用 traceId 将日志串起来,这样方便比较一次请求中的多个步骤的耗时情况;

2. 我们使用静态代理的方式做切面编程,避免在业务代码中,加入大量打印耗时的日志的代码,减少了对于代码的侵入性,同时编译期的代码注入可以减少;

3. 我们增加了日志采样率,避免全量日志的打印;

4. 最后为了避免在排查问题时,需要到多台服务器上搜索日志,我们使用消息队列,将日志集中起来放在了 Elasticsearch 中。

服务治理:

1)负载均衡

客户端负载均衡服务,也就是把负载均衡的服务内嵌在 RPC 客户端中。

它一般和客户端应用,部署在一个进程中,提供多种选择节点的策略,最终为客户端应用提供一个最佳的,可用的服务端节点。这类服务一般会结合注册中心来使用,注册中心提供服务节点的完整列表,客户端拿到列表之后使用负载均衡服务的策略选取一个合适的节点,然后将请求发到这个节点上

负载均衡策略有哪些负载均衡策略从大体上来看可以分为两类:

一类是静态策略,也就是说负载均衡服务器在选择服务节点时,不会参考后端服务的实际运行的状态。

一类是动态策略,也就是说负载均衡服务器会依据后端服务的一些负载特性,来决定要选择哪一个服务节点。

网站负载均衡服务的部署,是以 LVS 承接入口流量,在应用服务器之前,部署 Nginx 做细化的流量分发,和故障节点检测。当然,如果你的网站的并发不高,也可以考虑不引入 LVS。

负载均衡的策略可以优先选择动态策略,保证请求发送到性能最优的节点上;如果没有合适的动态策略,那么可以选择轮询的策略,让请求平均分配到所有的服务节点上。

Nginx 可以引入 nginx_upstream_check_module,对后端服务做定期的存活检测,后端的服务节点在重启时,也要秉承着“先切流量后重启”的原则,尽量减少节点重启对于整体系统的影响。

2)API网关

API 网关(API Gateway)不是一个开源组件,而是一种架构模式,它是将一些服务共有的功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。你可以把它看作系统的边界,它可以对出入系统的流量做统一的管控。在我看来,API 网关可以分为两类:一类叫做入口网关,一类叫做出口网关

1.它提供客户端一个统一的接入地址,API 网关可以将用户的请求动态路由到不同的业务服务上,并且做一些必要的协议转换工作。在你的系统中,你部署的微服务对外暴露的协议可能不同:有些提供的是 HTTP 服务;有些已经完成 RPC 改造,对外暴露 RPC 服务;有些遗留系统可能还暴露的是 Web Service 服务。API 网关可以对客户端屏蔽这些服务的部署地址,以及协议的细节,给客户端的调用带来很大的便捷。

2.另一方面,在 API 网关中,我们可以植入一些服务治理的策略,比如服务的熔断、降级,流量控制和分流等等(关于服务降级和流量控制的细节,我会在后面的课程中具体讲解,在这里,你只要知道它们可以在 API 网关中实现就可以了)。

3.再有,客户端的认证和授权的实现,也可以放在 API 网关中。你要知道,不同类型的客户端使用的认证方式是不同的。在我之前项目中,手机 APP 使用 Oauth 协议认证,HTML5 端和 Web 端使用 Cookie 认证,内部服务使用自研的 Token 认证方式。这些认证方式在 API 网关上,可以得到统一处理,应用服务不需要了解认证的细节。

4.另外,API 网关还可以做一些与黑白名单相关的事情,比如针对设备 ID、用户 IP、用户 ID 等维度的黑白名单。

5. 最后,在 API 网关中也可以做一些日志记录的事情,比如记录 HTTP 请求的访问日志,分布式追踪系统时,提到的标记一次请求的 requestId,也可以在网关中来生成。

出口网关就没有这么丰富的功能和作用了。我们在系统开发中,会依赖很多外部的第三方系统,比如典型的例子:第三方账户登录、使用第三方工具支付等等。我们可以在应用服务器和第三方系统之间,部署出口网关,在出口网关中,对调用外部的 API 做统一的认证、授权,审计以及访问控制。

针对服务接口数据聚合的操作,一般有两种解决思路:

再独立出一组网关专门做服务聚合、超时控制方面的事情,我们一般把前一种网关叫做流量网关,

后一种可以叫做业务网关;抽取独立的服务层,专门做接口聚合的操作。这样服务层就大概分为原子服务层和聚合服务层。

API 网关分为入口网关和出口网关两类,入口网关作用很多,可以隔离客户端和微服务,从中提供协议转换、安全策略、认证、限流、熔断等功能。出口网关主要是为调用第三方服务提供统一的出口,在其中可以对调用外部的 API 做统一的认证、授权,审计以及访问控制;

API 网关的实现重点在于性能和扩展性,你可以使用多路 I/O 复用模型和线程池并发处理,来提升网关性能,使用责任链模式来提升网关的扩展性;

API 网关中的线程池,可以针对不同的接口或者服务做隔离和保护,这样可以提升网关的可用性;

API 网关可以替代原本系统中的 Web 层,将 Web 层中的协议转换、认证、限流等功能挪入到 API 网关中,将服务聚合的逻辑下沉到服务层。

3)多机房部署

1. 同城双活

制定多机房部署的方案不是一蹴而就的,而是不断迭代发展的。我在上面提到,同城机房之间的延时在 1ms~3ms 左右,对于跨机房调用的容忍度比较高,所以,这种同城双活的方案复杂度会比较低。

2. 异地多活

不同机房的数据传输延迟,是造成多机房部署困难的主要原因,你需要知道,同城多机房的延迟一般在 1ms~3ms,异地机房的延迟在 50ms 以下,而跨国机房的延迟在 200ms 以下。

同城多机房方案可以允许有跨机房数据写入的发生,但是数据的读取,和服务的调用应该尽量保证在同一个机房中。

异地多活方案则应该避免跨机房同步的数据写入和读取,而是采取异步的方式,将数据从一个机房同步到另一个机房。

4)server mesh

Service Mesh 主要处理服务之间的通信,它的主要实现形式就是在应用程序同主机上部署一个代理程序,一般来讲,我们将这个代理程序称为“Sidecar(边车)”,服务之间的通信也从之前的,客户端和服务端直连,变成了下面这种形式:

在这种形式下,RPC 客户端将数据包先发送给,与自身同主机部署的 Sidecar,在 Sidecar 中经过服务发现、负载均衡、服务路由、流量控制之后,再将数据发往指定服务节点的 Sidecar,在服务节点的 Sidecar 中,经过记录访问日志、记录分布式追踪日志、限流之后,再将数据发送给 RPC 服务端。

这种方式,可以把业务代码和服务治理的策略隔离开,将服务治理策略下沉,让它成为独立的基础模块。这样一来,不仅可以实现跨语言,服务治理策略的复用,还能对这些 Sidecar 做统一的管理。

目前,业界提及最多的 Service Mesh 方案当属istio, 它的玩法是这样的:

轻量客户端:

1.Service Mesh 分为数据平面和控制平面。数据平面主要负责数据的传输;控制平面用来控制服务治理策略的植入。出于性能的考虑,一般会把服务治理策略植入到数据平面中,控制平面负责服务治理策略数据的下发。

2.Sidecar 的植入方式目前主要有两种实现方式,一种是使用 iptables 实现流量的劫持;另一种是通过轻量级客户端来实现流量转发。

发布了27 篇原创文章 · 获赞 16 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/liangwqi/article/details/103314011