Skip to content

Latest commit

 

History

History
128 lines (72 loc) · 11.2 KB

设计高并发下的读服务.md

File metadata and controls

128 lines (72 loc) · 11.2 KB

设计高并发下的读服务

一、项目背景

​ 像库存、商品、价格等这些体量(访问量+数据量)非常大的服务将他们拆分为单一系统(一个系统只提供一种服务)是很有必要的。对于体量不够大的或者职责划分不清的服务,为了便于维护和使用,一般会将其融合在一个系统中(暂且称它为”非单一系统”)。这些服务一个共同的特点是读大于写,比如京东首页的全部分类、热搜索词等, 可以说是一个彻彻底底的读服务,这些信息数据量小而且很少改动,读取量远远高于写入(或更新)量,像单品页要用到的延保、pop套装等服务,虽然对于单个商品他们的读写不频繁,但他们会涉及很多(亿级别)sku,所以整体加起来他们的访问量、数据量、更新频率都不小。那么针对这些五花八门的服务,怎么才能在一个系统里,既要保证高可用,又保证高性能,还要保证数据一致性等问题。

二、系统特点

  1. 提供的服务多
  2. 依赖的数据源多样化,数据库、HTTP接口、JSF(公司内部RPC框架)接口等
  3. 系统以读为主
  4. 整体服务加起来体量大(访问量+数据量)
  5. 需要快速响应
  6. 服务之间相互影响性要小

三、实现该系统时遵循以下几个大的原则

  1. 使用HTTP协议对外通信
  2. 使用短连接
  3. 数据异构
  4. 巧用缓存
  5. 流量控制
  6. 异步、并行
  7. 数据托底
  8. 防刷
  9. 降级
  10. 多域名

下边针对这些原则进行一一阐述:

1、使用 HTTP 协议对外通信

​ 前面提到服务化后各个系统使用的语言可以不相同,对于使用同一种语言实现的不同系统,可以指定语言相关的协议进行通信,不同语言的系统之间就需要找一个通用的协议来通信。SOAP 简单对象访问协议是一种非语言相关的通信协议, 以 HTTP 协议为载体进行传输,虽然有各种辅助框架,但它还是太重了,相比较 HTTP 从便捷和使用范围上有绝对的优势,所以本系统以 HTTP 协议对外提供服务。

2、使用短连接

​ HTTP 协议本身是工作在 TCP 协议上的,这里说的长连接短连接本质上只的是 TCP 的长短连接。所谓的长连接顾名思义就是用完之后不立即断开连接,何时断开取决于上层业务设置和底层协议是否发生异常,短连接就比较干脆,干完活马上就将连接关闭。

​ 在 HTTP 中开启长连接需要在协议头中加上 Connection:keep-alive,当然最终是否使用长连接通信是需要双方进行协商的,客户端和服务端只要有一方不同意,则开启失败。长连接因为可以复用链路,所以如果请求频繁,可以减少连接的建立和关闭时间,从而节省资源。

​ HTTP 1.0 默认使用短连接,HTTP 1.1 中开启短连接需要在协议头上加上 Connection:close,如何单个客户请求频繁,TCP 链接的建立和关闭多少会浪费点资源。

​ 既然长连接这么好,短连接这么不好为什么还要使用短连接呢?我们知道这个连接实际上是 TCP 连接。TCP 连接是有一个四元组表示的,如 源 ip:源 port—目标 ip:目标 port。从这个四元组可以看到理论上可以有无数个连接, 但是操作系统能够承受的连接可是有限的,假设我们设置了长连接,那么不管这个时间有多短,在高并发下 server 端都会产生大量的 TCP 连接,操作系统维护每个连接不但要消耗内存也会消耗 CPU,在高并发下维护过多的活跃连接风险可想而知。

​ 而且在长连接的情况下如果有人搞恶意攻击,创建完连接后什么都不做,势必会对 Server 产生不小的压力。所以在互联网这种高并发系统中,使用短连接是一个明智的选择。对于服务端因短连接产生的大量的 TIME_WAIT 状态的连接,可以更改系统的一些内核参数来控制,比如net.ipv4.tcp_max_tw_buckets、net.ipv4.tcp_tw_recycle、net.ipv4.tcp_tw_reuse等参数(注:非专业人士调优内核参数要慎重)。

​ 具体 TIME_WAIT 等 TCP 的各种状态这里不再详述,给出一个简单状态转换图供参考:

img

3、数据异构

​ 一个大的原则,如果依赖的服务不可靠,那系统就可能随时出问题。对于依赖服务的数据,能异构的就要拿过来,有了数据就可以做任何你想做的事,有了数据,依赖服务再怎么变着花的挂对你的影响也是有限的。异构时可以将数据打散,将数据原子化,这样在向外提供服务时,可以任意组装拼合。

4、巧用缓存

​ 应对高并发系统,缓存是必不可少的利器,巧妙的使用缓存会使系统的性能有质的飞跃,下面就介绍一下本系统使用缓存的几种方式:

使用Redis缓存

很典型的使用缓存的一种方式,这里先重点介绍一下在缓存命中与不命中时都做了哪些事。

​ 当用户发起请求后,首先在Nginx这一层直接从Redis获取数据, 这个过程中Nginx使用lua-resty-redis操作Redis,该模块支持网络Socket和unix domain socket。如果命中缓存,则直接返回客户端。如果没有则回源请求数据,这里要记住另一个原则,不可『随意回源』(为了保护后端应用)。为了解决高并发下缓存失效后引发的雪崩效应,我们使用lua-resty-lock(异步非阻塞锁)来解决这个问题。

​ 很多人一谈到锁就心有忌惮,认为一旦用上锁必然会影响性能,这种想法的不妥的。我们这里使用的lua-resty-lock是一个基于Nginx共享内存(ngx.shared.DICT)的非阻塞锁(基于Nginx的时间事件实现),说它是非阻塞的是因为它不会阻塞Nginx的worker进程,当某个key(请求)获取到该锁后,后续试图对该key再一次获取锁时都会『阻塞』在这里,但不会阻塞其它的key。当第一个获取锁的key将获取到的数据更新到缓存后,后续的key就不会再回源后端应用了,从而可以起到保护后端应用的作用。

下面贴一段从官网弄过来的简化代码,详细使用请移步https://github.com/openresty/lua-resty-lock。

img

使用 Nginx 共享缓存

上面使用到的 Redis 缓存,即使 Redis 部署在本地仍然会有进程间通信、内核态和用户态的数据拷贝,使用 Nginx 的共享缓存可以将这些动作都省略掉。

Nginx 共享缓存是 worker 共享的,也就是说它是一个全局的缓存,使用 Nginx 的 lua_shared_dict 配置指令定义。语法如下:

#指定一个100m的共享缓存 lua_shared_dict cache 100m;

img

5、流量控制

​ 这一原则主要为了避免系统过载,可以采用多种方式达到此目的。流量控制可以在前端做(Nginx),也可以在后端做。

​ 目前 servlet 3 支持异步请求,是一种多线程异步模型, 它的每个请求仍然要使用一个线程,只不过可以进行异步操作了。这种模型的一个优点是可以按业务来分配请求资源了,比如你的系统要向外提供 10 种服务,你可以为每种服务分配一个固定的线程池,这样服务之间可以相互隔离。

​ 缺点是由于是异步,所以就需要各种回调,开发和维护成本高。同事有一个项目用到了servlet 3,测试结果显示这种方式不会获得更短的响应时间,反而会有稍微下降,但是吞吐量确实有提升。所以最终是否使用这种方式,取决于你的系统更倾向于完成哪种特性。

​ 除了在后端进行流量控制,还可以在 Nginx 层做控制。目前在 Nginx 层有多种模块可以支持流量控制,如ngx_http_limit_conn_module 、ngx_http_limit_req_module、lua-resty-limit-traffic(需安装 lua 模块)等,限于篇幅如何使用就不在详述,感兴趣可到官网查看。

6、数据托底

​ 生产环境中有些服务可能非常重要,需要保证绝对可用,这时如果业务允许,我们就可以为其做个数据托底。

​ 托底方式非常多,这里简单介绍几种,一种是在应用后端进行数据托底,这种方式比较灵活,可以将数据存储在内存、磁盘等各种设备上,当发生异常时可以返回托底数据,缺点是和后端应用高度耦合,一旦应用容器挂掉托底也就不起作用了。

​ 另一种方式将托底功能跟应用剥离出来,可以使用 Lua 的方式在 Nginx 做一层拦截,用每次请求回源返回的正确数据来更新托底数据(这个过程可以做各种校验),当服务或应用出问题时可以直接从 Nginx 层返回数据。

​ 还有一种是使用 Nginx 的 error_page 指令,简单配置如下:

img

7、降级

​ 降级的意义其实和流量控制的意义差不多,都是为了确保系统负载稳定。当线上流量超过我们预期时,为了降低系统负载就可以实施降级了。

​ 降级的方式可以是自动降级,比如我们对一个依赖服务可以设置一个超时,当超过这个时间时就可以自动的返回一个默认值(前提是业务允许)。

​ 手动降级,提前为某些服务设置降级开关,出现问题是可以将开关打开,比如前面我们说到了有些服务是有托底数据的,当系统过载后我们可以将其降级到直接走托底数据。

8、防刷

​ 对于一些有规律入参的请求,我们可以用严格检验入参的方式,来规避非法入参穿透缓存的行为(比如一些爬虫程序无限制的猜测商品价格),这种方式可以做在前端(Nginx),也可以做在后端(Tomcat),推荐在 Nginx 层做。 在 Nginx 层做入参校验的例子:

img

​ 使用计数器识别恶意用户,比如在一段时间内为每个用户或IP等记录访问次数,如果在规定的时间内超过规定的次数,则做一些对应策略。

​ 对恶意用户设置黑名单,每次访问都检查是否在黑名单中,存在就直接拒绝。

​ 使用 Cookie,如果用户访问是没有带指定的 Cookie,或者和规定的 Cookie 规则不符,则做一些对应策略。

​ 通过访问日志实时计算用户的行为,发现恶意行为后对其做相应的对策。

9、多域名

​ 除正常域名外,为系统提供其它访问域名。使用 CDN 域名缩短用户请求链路,使用不带 Cookie 的域名,降低用户请求流量。