本文介绍了微服务的高并发设计,从最外层的接入层入手,看看有哪些策略可以保证高并发。接入层的架构如下图所示:接下来我们依次分析各个部分以及可以做的优化。数据中心外:DNS、HttpDNS、GSLB当我们要访问一个网站的服务时,首先要访问一个域名,然后DNS将域名解析为IP地址。下面以使用DNS访问数据中心对象存储上的静态资源为例,看一下整个过程。我们建议将文件、图片、视频、音频等静态资源放在对象存储中,直接通过CDN进行分发,而不是放在服务器上,与动态资源绑定。假设全国有多个数据中心,由多个运营商托管,每个数据中心有3个可用区。对象存储跨可用性区域部署以实现高可用性。每个数据中心至少部署两个内部负载均衡器,内部负载均衡器连接多个对象存储的前端服务代理服务器。1、当客户端要访问object.yourcompany.com时,需要将域名转换为IP地址进行访问,因此需要请求本地解析器的帮助。2、本地解析器检查本地缓存是否有这条记录?如果可用,请直接使用它。3.如果没有本地缓存??,则需要请求本地NameServer。4、本地NameServer一般部署在客户的数据中心或客户所在运营商的网络中。本地名称服务器检查是否有本地缓存??,如果有则返回。5、如果没有本地NameServer,本地NameServer需要从RootNameServer开始查找,RootNameServer会将.comNameServer的地址返回给本地NameServer。6、然后本地的NameServer访问.com的NameServer,他会把你公司的yourcompany.com的NameServer发给本地的NameServer。7.本地名称服务器然后访问yourcompany.com的名称服务器。此时应该返回要访问的真实IP地址。对于不需要全局负载均衡的简单应用,yourcompany.com的NameServer可以直接将域名object.yourcompany.com解析为一个或多个IP地址,然后客户端进行简单的轮询即可实现简单的负载均衡.但对于复杂的应用,尤其是跨地域、跨运营商的大规模应用,需要更复杂的全局负载均衡机制,因此需要专门的设备或服务器来完成。这是GSLB,一个全局负载均衡器。从yourcompany.com的NameServer,一般通过配置CNAME,给object.yourcompany.com起一个别名。比如object.vip.yourcomany.com,然后告诉本地的NameServer去请求GSLB来解析这个域名,那么GSLB在解析这个域名的过程中可以通过自己的策略实现负载均衡。图中画了两层的GSLB,因为它分为运营商和区域。我们希望属于不同运营商的客户可以访问同一运营商机房的资源,这样就不需要跨运营商访问,有利于提高吞吐量。减少延迟。8.一级GSLB通过查询请求的本地NameServer的运营商来获知用户的运营商。假设是移动的,那么通过CNAME,通过另一个别名object.yd.yourcompany.com,告诉本地NameServer请求二级GSLB。9.第二层的GSLB通过查询请求它的本地NameServer的地址知道用户的地理位置,然后返回离用户比较近的Region内部负载均衡SLB的6个地址给本地名称服务器。10.本地名称服务器将结果返回给解析器。11、解析器缓存结果返回给客户端。12、客户端开始访问Region1的对象存储,属于同一个运营商,距离较近。当然,客户端得到六个IP地址。它可以通过负载均衡随机或轮询选择一个可用区进行访问。对象存储一般有三备,这样可以实现对存储读写的负载均衡。从上面的过程可以看出,基于DNS域名的GSLB实现了全局的负载均衡,但是现在跨运营商、跨地域的流量调度,由于不同运营商不同的DNS缓存策略,会导致GSLB失效。部分用户的DNS会将域名解析请求转发给其他运营商的DNS进行解析,导致GSLB误判用户的运营商。有些运营商的DNS出口会做NAT,导致GSLB误判用户的运营商。所以区别于传统的DNS,还有一种机制叫做httpDNS,可以把SDK嵌入到用户的手机APP中,通过http访问一个httpDNS服务器。由于手机APP可以准确获取到自己的IP地址,所以可以将IP地址传递给httpDNS服务器。httpDNS服务器完全由应用服务商提供,可以实现完全独立的全网流量调度。数据中心外:对于静态资源,可以在真正访问机房对象存储之前,通过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和SpringCloud,服务注册中心可以有ZooKeeper、Consul、etcd、Eureka等,这里以Consul为例。由于服务之间的调用已经注册到Consul上,Nginx也可以通过Consul获取后端服务的状态,实现动态负载均衡。Nginx可以集成consul-template,监听Consul事件。当注册的服务列表或key/value发生变化时,consul-template会修改配置文件并执行一个shell,比如nginxreload。consul-template\-template"/tmp/nginx.hcl:/var/nginx/nginx.conf:servicenginxreload"\consul-template模式配置比较复杂,需要重新加载nginx。另外一种集成Consul的方式是nginx-upsync-module,它可以同步Consul的服务列表或者key/value存储。它需要重新编译Nginx而不是重新加载nginx。upstreamtest{server127.0.0.1:11111;#所有后端服务列表都会从consul中拉取,并删除占位符serverupsync127.0.0.1:8500/v1/catelog/service/testupsync_timeout=6mupsync_interval=500msupsync_type=consulstrong_dependency=off;#备份地址,保证nginx不强依赖consulupsync_dump_path/usr/local/nginx/conf/servers/servers_test.conf;}另一种方式是OpenResty+Lua,相比nginx-upsync-module,可以增加更多自己的逻辑,init_*_by_lua阶段通过httpapi获取服务列表并加载到Nginx内存中,并设置定时器轮训更新列表,balancer_by_lua阶段读取内存列表并设置后端服务器。Lua实现也不需要重新加载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等,相当于为每一个单独的缓存user,使得缓存攻击率相对较低。静态资源和动态资源分离之后,就有一个组合的问题。可以通过Ajax访问动态资源,在浏览器端组合,或者在访问层组合。如果在接入层做聚合,或者varnish做聚合,接入层缓存可以周期性轮询后端应用,当有数据修改时,动态页面会静态化。这样,用户访问的数据就会在访问层被拦截。缺点是更新速度有点慢。对于大促场景下高并发访问的页面,可以进行这样的处理。动态资源缓存静态和动态分离后,静态页面可以很好的缓存,而动态数据仍然会请求到后端。动态页面的静态延迟比较高,当页面数量较多时,静态工作量比较大。因此,在接入层也可以通过Redis或者Memcached来缓存动态资源。资源隔离接入层没有一个Nginx集群,但是不同的请求可以有独立的Nginx集群。比如抢券或者秒杀系统会成为热点中的热点,所以应该有独立的Nginx集群。统一认证、鉴权、过滤APIGateway的另一个作用是统一认证、鉴权。一种是基于Session。客户端输入用户名和密码后,API网关会向后端服务提交认证授权。成功后会生成一个Session。Session会放在Redis中,之后所有的访问都会携带Session收益。另一种方式是通过统一的认证中心分发令牌。这是一个三角形结构。API网关收到登录请求后,会去认证中心请求认证授权,成功则返回一个Token。Token是一个加密的字符串,里面包含了很多认证信息。在下次访问时,APIGateway可以验证该Token是否有效进行认证,真正的服务可以基于该Token进行认证。流量限制在大促中,经常会遇到实际流量远大于系统可测试流量的情况。如果这些流量都进来,整个系统肯定会崩溃,根本没人玩。因此,常用的方法是限流。限流从上到下贯穿于整个应用。当然,接入层作为最外层的屏障,需要做好整个系统的限流工作。对于Nginx,限流的方式有很多种。可以限制连接数limit_conn,限制访问频率limit_req,开启过载保护sysgurad模块。限制请求的目标URL的速率(例如:每分钟允许调用一个URL的次数)。限制客户端访问IP的流量(例如:一个IP每分钟允许多少请求)。对于流量受限的用户,可以做相对友好的回报,不同页面的策略可以不同。对于首页和活动页面,读取较多,可以返回缓存中的旧页面,也可以定时刷新app。加入购物车、下单、支付等写作请求受限的,可返回等待页面,或返回圈子。如果一段时间后仍无法转移,可以返回到拥挤的人群中。对于支付结果返回,如果流量受限,需要立即返回错误页面。灰度发布和AB测试在接入层。由于可以配置访问路由和访问权重,可以实现灰度发布或者AB测试。两个系统同时上线,通过切掉部分流量来测试新系统的稳定性。性还是更受欢迎。
