从接入层入手,设计高并发的微服务架构?

更多干货

  • 分布式实战(干货)
  • spring cloud 实战(干货)
  • mybatis 实战(干货)
  • spring boot 实战(干货)
  • React 入门实战(干货)
  • 构建中小型互联网企业架构(干货)
  • python 学习持续更新
  • ElasticSearch 笔记
  • kafka storm 实战 (干货)

对于静态资源来讲,其实在真实的访问机房内的对象存储之前,在最最接近用户的地方,可以先通过 CDN 进行缓存,这也是高并发应用的一个总体的思路,能接近客户,尽量接近客户。

本篇介绍微服务的高并发设计,先从最外层的接入层入手,看都有什么样的策略保证高并发。

接入层的架构,如下图:

接下来我们依次解析各个部分以及可以做的优化。

数据中心之外:DNS、HttpDNS、GSLB

当我们要访问一个网站的服务的时候,首先访问的肯定是一个域名,然后由 DNS,将域名解析为 IP 地址。

我们通过 DNS 访问数据中心中的对象存储上的静态资源为例子,看一看整个过程。

我们建议将例如文件、图片、视频、音频等静态资源放在对象存储中,直接通过 CDN 下发,而非放在服务器上,和动态资源绑定在一起。

假设全国有多个数据中心,托管在多个运营商,每个数据中心三个可用区 Available Zone,对象存储通过跨可用区部署,实现高可用性。

在每个数据中心中,都至少部署两个内部负载均衡器,内部负载均衡器后面对接多个对象存储的前置服务 proxy-server。

1、当一个客户端要访问 object.yourcompany.com 的时候,需要将域名转换为 IP 地址进行访问,所以他要请求本地的 resolver 帮忙。

2、本地的 resolver 看本地的缓存是否有这个记录呢?如果有则直接使用。

3、如果本地无缓存,则需要请求本地的 Name Server。

4、本地的 Name Server 一般部署在客户的数据中心或者客户所在运营商的网络中,本地 Name Server 看本地是否有缓存,如果有则返回。

5、如果本地没有,本地 Name Server 需要从 Root Name Server 开始查起,Root Name Server 会将 .com Name Server 的地址返回给本地 Name Server。

6、本地的 Name Server 接着访问 .com 的 Name Server,他会将你们公司的 yourcompany.com 的 Name Server 给本地 Name Server。

7、本地的 Name Server 接着访问 yourcompany.com 的 Name Server,到这一步就应该返回真实要访问的 IP 地址。

对于不需要做全局负载均衡的简单应用来讲,yourcompany.com 的 Name Server 可以直接将 object.yourcompany.com 这个域名解析为一个或者多个 IP 地址,然后客户端可以通过多个 IP 地址,进行简单的轮询,实现简单的负载均衡即可。

但是对于复杂的应用,尤其是跨地域跨运营商的大型应用,则需要更加复杂的全局负载均衡机制,因而需要专门的设备或者服务器来做这件事情,这就是 GSLB,全局负载均衡器。

从 yourcompany.com 的 Name Server 中,一般是通过配置 CNAME 的方式,给 object.yourcompany.com 起一个别名。

例如 object.vip.yourcomany.com,然后告诉本地 Name Server,让他去请求 GSLB 去解析这个域名,则 GSLB 就可以在解析这个域名的过程中,通过自己的策略实现负载均衡。

图中画了两层的 GSLB,是因为分运营商和分地域,我们希望将属于不同运营商的客户,访问相同运营商机房中的资源,这样不用跨运营商访问,有利于提高吞吐量,减少时延。

8、第一层 GSLB 通过查看请求他的本地 Name Server 所在的运营商,就知道了用户所在的运营商。

假设是移动,然后通过 CNAME 的方式,通过另一个别名 object.yd.yourcompany.com,告诉本地 Name Server 去请求第二层的 GSLB。

9、第二层的 GSLB 通过查看请求他的本地 Name Server 所在的地址,就知道了用户所在的地理位置,然后将距离用户位置比较近的 Region 里面的内部负载均衡 SLB 的地址共六个返回给本地 Name Server。

10、本地 Name Server 将结果返回给 resolver。

11、resolver 将结果缓存后,返回给客户端。

12、客户端开始访问属于相同运营商的距离较近的 Region1 中的对象存储,当然客户端得到了六个 IP 地址。

它可以通过负载均衡的方式,随机或者轮询选择一个可用区进行访问,对象存储一般会有三份备份,从而可以实现对存储读写的负载均衡。

从上面的过程可以看出,基于 DNS 域名的 GSLB 实现全局的负载均衡,可是现在跨运营商和跨地域的流量调度,由于不同运营商的 DNS 缓存策略不同,会造成 GSLB 的工作失效。

有的用户的 DNS 会将域名解析的请求转发给其他运营商的 DNS 进行解析,导致到 GSLB 的时候,错误的判断了用户所在的运营商。

有的运营商的 DNS 出口会做 NAT,导致 GSLB 判断错误用户所在的运营商。

所以不同于传统的 DNS,有另一种机制称为 httpDNS,可以在用户的手机 App 里面嵌入 SDK,通过 http 的方式访问一个 httpDNS 服务器。

由于手机 App 可以精确的获得自己的 IP 地址,可以将 IP 地址传给 httpDNS 服务器,httpDNS 服务器完全由应用的服务商提供,可以实现完全自主的全网流量调度。

数据中心之外:CDN

对于静态资源来讲,其实在真实的访问机房内的对象存储之前,在最最接近用户的地方,可以先通过 CDN 进行缓存,这也是高并发应用的一个总体的思路,能接近客户,尽量接近客户。

CDN 厂商的覆盖范围往往更广,在每个运营商,每个地区都有自己的 POP 点,所以总有更加靠近用户的相同运营商和相近地点的 CDN 节点就近获取静态数据,避免了跨运营商和跨地域的访问。

在使用了 CDN 之后,用户访问资源的时候,和上面的过程类似,但是不同的是,DNS 解析的时候,会将域名的解析权交给 CDN 厂商的 DNS 服务器。

而 CDN 厂商的 DNS 服务器可以通过 CDN 厂商的 GSLB,找到最最接近客户的 POP 点,将数据返回给用户。

当 CDN 中没有找到缓存数据的时候,则需要到真正的服务器中去拿,这个称为回源,仅仅非常少数的流量需要回源,大大减少了服务器的压力。

数据中心边界与核心:边界路由、核心交换、等价路由

如果真的需要回源,或者访问的压根就不是静态资源,而是动态资源,则需要进入数据中心了。

刚才第一节中说到,最终 GSLB 返回了 6 个 IP 地址,都是内部负载均衡 SLB 的 IP 地址,说明这 6 个 IP 地址都是公网可以访问的,那么公网如何知道这些 IP 地址的呢?

这就要看机房的结构了,如下图:

一个机房一般会有边界路由器、核心交换机,每个 AZ 有汇聚交换机,6 个 SLB 是在 AZ 里面的,所以他们的 IP 地址是通过 iBGP 协议告知边界路由器的。

当用户从 6 个 IP 里面选择了 1 个 IP 地址进行访问的时候,可以通过公网上面的路由,找到机房的边界路由器。

边界路由器知道当时这个路由是从哪个 AZ 里面给它的,于是就通过核心交换一层,将请求转发给某一个 AZ,这个 AZ 的汇聚交换机会将请求转发给这个 SLB。

如果一个 AZ 出现了问题,是否可以让对某个公网 IP 的访问给另一个 AZ 呢?当然是可以的,在核心路由和核心交换之间,可以做 ECMP 等价路由。

当然也可以在边界路由上将外部地址 NAT 成为内部的一个 VIP 地址,通过等价路由实现跨 AZ 的流量分担。

数据中心可用区中:负载均衡 SLB、LVS、HAProxy

进入一个可用区 AZ 之后,首先到达的是负载均衡 SLB,可以购买商用的 SLB,也可以自己搭建,例如通过 LVS 实现基本的负载均衡功能。

LVS 的性能比较好,很多工作通过内核模块 ipvs 完成,如下图:

LVS 可使用 keepalived 实现双机热备,也可以通过 OSPF 使用等价路由的方式,在多个 LVS 之间进行流量分担,往往作为统一的负载均衡入口,承载大的流量。

有时候需要更加复杂的 4 层和 7 层负载均衡,则会在 LVS 后面加上 HAProxy 集群,也即将 LVS 导入的流量,分发到一大批 HAProxy 上。

这些 HAProxy 可以根据不同的应用或者租户进行隔离,每个租户独享单独的 HAProxy,但是所有的租户共享 LVS 集群。

如果有云环境,则 HAProxy 可以部署在虚拟机里面,可以根据流量的情况和租户的请求进行动态的创建和删除。

数据中心可用区中:接入层 Nginx、接入层缓存

在负载均衡之后,是接入网关,或者 API 网关,往往需要实现很多灵活的转发策略,这里会选择使用 Nginx+Lua 或者 OpenResty 做这一层。

由于 Nginx 本身也有负载均衡机制,有的时候会将 HAProxy 这一层和 Nginx 这一层合并,LVS 后面直接跟 Nginx 集群。

API 的聚合

使用微服务之后,后端的服务会拆分的非常的细,因而前端应用如果要获取整个页面的显示,往往需要从多个服务获取数据,将数据做一定的聚合后,方能够显示出来。

如果是网页其实还好,如果你在 Chrome 的 debug 模式下,打开一个复杂的电商主页的时候,你会发现这个页面同时会发出很多的 HTTP 请求,最终聚合成为一个页面。

如果是 App 的话,其实也没有问题,但是会有大量的工作要在客户端做,这样会非常的耗电,用户体验非常不好,因而最好有一个地方可以将请求聚合,这就是 API 网关的职责之一。

这样对于前端 App 来讲,后端似乎是一个统一的入口,即后端的服务的拆分和聚合,灰度发布,缓存策略等全部被屏蔽了。

服务发现与动态负载均衡

既然统一的入口变为了接入层,则接入层就有责任自动的发现后端拆分、聚合、扩容、缩容的服务集群,当后端服务有所变化的时候,能够实现健康检查和动态的负载均衡。

对于微服务来讲,服务之间也是需要做服务发现的,常见的框架是 Dubbo 和 Spring Cloud,服务的注册中心可以是 ZooKeeper、Consul、etcd、Eureka 等。

我们以 Consul 为例子,既然服务之间的调用已经注册到 Consul 上,则 Nginx 自然也可以通过 Consul 来获取后端服务的状态,实现动态的负载均衡。

Nginx 可以集成 consul-template,可监听 Consul 的事件, 当已注册 service 列表或 key/value 发生变化时,consul-template 会修改配置文件同时可执行一段 Shell,如 nginx reload。

 
  
  1. consul-template \    -template "/tmp/nginx.hcl:/var/nginx/nginx.conf:service nginx reload" \ 

consul-template 模式配置相对复杂,需要 reload nginx。

另一种集成 Consul 的方式是 nginx-upsync-module,可以同步 Consul 的服务列表或 key/value 存储,需要重新编译 Nginx,不需要 reload nginx。

 
  
  1. upstream test { 
  2.         server 127.0.0.1:11111; 
  3.         # 所有的后端服务列表会从consul拉取, 并删除上面的占位server 
  4.         upsync 127.0.0.1:8500/v1/catelog/service/test upsync_timeout=6m upsync_interval=500ms upsync_type=consul strong_dependency=off
  5.         # 备份的地址, 保证nginx不强依赖consul 
  6.         upsync_dump_path /usr/local/nginx/conf/servers/servers_test.conf; 

还有一种方式是 OpenResty+Lua,相对 nginx-upsync-module,可以加入更多自己的逻辑,init_*_by_lua 阶段通过 http api 获取服务列表载入 Nginx 内存,并设置 timer 轮训更新列表,balancer_by_lua 阶段读取内存的列表, 设置后端服务器。

Lua 实现同样可以不 reload nginx,相比 nginx-upsync-module 来说更加可扩展。

动静资源隔离、静态页面缓存、页面静态化

为什么静态资源需要隔离呢?静态资源往往变化较少,但是却往往比较大,如果每次都加载,则影响性能,浪费带宽。其实静态资源可以预加载,并且可以进行缓存,甚至可以推送到 CDN。

所以应该在接入层 Nginx 中配置动态资源和静态资源的分离,将静态资源的 url 导入到 Nginx 的本地缓存或者单独的缓存层如 Varnish 或者 Squid,将动态的资源访问后端的应用或者动态资源的缓存。

在 Nginx 中,可以通过配置 expires、cache-control、if-modified-since 来控制浏览器端的缓存控制。使得浏览器端在一段时间内,对于静态资源,不会重复请求服务端。这一层称为浏览器端的缓存。

当有的请求的确到达了接入层 Nginx 的时候,也不用总是去应用层获取页面,可以在接入层 Nginx 先拦截一部分热点的请求。

在这里可以有两层缓存。一是 Nginx 本身的缓存 proxy_cache,二是缓存层的 Varnish 或者 Squid。

在使用接入层缓存的时候,需要注意的是缓存 key 的选择,不应该包含于用户相关的信息,如用户名、地理信息、cookie、deviceid 等,这样相当于每个用户单独的一份缓存,使得缓存的命中率比较低。

在分离了静态和动态资源之后,就存在组合的问题,可以通过 Ajax 访问动态资源,在浏览器端进行组合,也可以在接入层进行组合。

如果在接入层聚合,或者 Varnish 进行聚合,则可以让接入层缓存定时轮询后端的应用,当有数据修改的时候,进行动态页面静态化。

这样用户访问的数据到接入层就会被拦截,缺点是更新的速度有些慢,对于大促场景下的并发访问高的页面,可以进行如此的处理。

动态资源缓存

在动静分离之后,静态页面可以很好的缓存,而动态的数据还是会向后端请求。

动态页面静态化延时相对比较高,而且页面数目多的时候,静态化的工作量也比较大,因而在接入层还可以通过 Redis 或者 Memcached,对动态资源进行缓存。

资源隔离

接入层的 Nginx 集群不是一个,而是不同的请求可以有独立的 Nginx 集群。

例如抢券或者秒杀系统,会成为热点中的热点,因而应该有独立的 Nginx 集群。

统一鉴权、认证、过滤

API Gateway 的另一个作用是统一的认证和鉴权。

一种是基于 Session 的,当客户端输入用户名密码之后,API Gateway 会向后端服务提交认证和鉴权,成功后生成 Session,Session 统一放在 Redis 里面,则接下来的访问全部都带着 Session 进行。

另一种方式是通过统一的认证鉴权中心,分配 Token 的方式进行。

这是一个三角形的结构,当 API Gateway 接收到登陆请求的时候,去认证中心请求认证和授权,如果成功则返回 Token。

Token 是一个加密过的字符串,里面包含很多的认证信息,接下来的访问中,API Gateway 可以验证这个 Token 是否有效来认证,而真正的服务可以根据 Token 来鉴权。

限流

在大促过程中,常常会遇到真实的流量远远大于系统测试下来的可承载流量,如果这些流量都进来,则整个系统一定垮掉,最后谁也别玩。所以常采用的方式是限流。

限流是从上到下贯穿整个应用的,当然接入层作为最外面的屏障,需要做好整个系统的限流。

对于 Nginx 来讲,限流有多种方式,可以进行连接数限制 limit_conn,可以进行访问频率限制 limit_req,可以启用过载保护 sysgurad 模块。

对请求的目标 URL 进行限流(例如:某个 URL 每分钟只允许调用多少次)。

对客户端的访问 IP 进行限流(例如:某个 IP 每分钟只允许请求多少次)。

对于被限流的用户,可以进行相对友好的返回,不同的页面的策略可以不同。

对于首页和活动页,是读取比较多的,可以返回缓存中的老的页面,或者 App 定时刷新。

对于加入购物车、下单、支付等写入请求被限流的,可以返回等待页面,或者返回一个圈圈转啊转,如果过了一段时间还转不出来,就可以返回挤爆了。

对于支付结果返回,如果被限流,需要马上返回错误页面。

灰度发布与 AB 测试

在接入层,由于可以配置访问路由,以及访问权重,可以实现灰度发布,或者 AB 测试,同时上线两套系统,通过切入部分流量的方式,测试新上系统的稳定性或者是否更受欢迎。



你可能感兴趣的:(【构建高可用架构】,【转载】)