读亿级流量网站架构技术

简介:从高并发和高可用的角度分析网站系统的架构。涉及到缓存,降级,限流方方面面的知识。其中包含缓存的讲解,Nginx 的一些配置,Hystrix 隔离机制,数据库中间件,Disruptor+Redis 队列,以及池化技术。

一些设计原则

高并发原则 (High Concurrency)

通常指:保证系统能够同时并行处理很多请求

  • 响应时间 Response Time(系统对请求做出响应的时间)
  • 吞吐量 Throughput (单位时间内处理的请求数量)
  • QPS 每秒响应请求数
  • 并发用户数
  1. 无状态

    应用无状态(便于水平扩展)

  2. 拆分

    系统拆分

  • 按系统业务
  • 按读写
  • AOP
  • 模块维度 (web,Service,DAO)
  1. 服务化

    随着调用的增多,应该考虑服务化,与服务的自动发现与注册
    SOA(Service-Oriented Architecture 面向服务编程 dubbo thrift)

  2. 消息队列

    用来解耦一些不需要同步调用的服务或者订阅一些自己系统关系的变化

  3. 数据闭环

    先通数据异构收集数据,再进行数据聚合,目的在于实现数据的自我控制,当其他系统出现问题时,不影响自己的系统

  4. 缓存银弹

    数据离客户越近越好

客户端>游览器>CDN缓存>NGINX缓存>应用缓存>统一缓存

  1. 并发化

    对同时需要对多个其他系统进行依赖,可并行请求,减少等待时间

高可用原则 (High Availability)

通常指:通过设计减少系统不能提供提供服务的时间

  1. 降级

    当服务器压力剧增时,根据实际业务及流量,对一些服务有策略的不处理或简单处理,从而保证主要服务的可用

  • 配置开关集中化管理,通过推送机制把开关推送到各个应用
  • 可降低的多级服务
  • 业务降级,同步变异步等
  1. 限流

    防止恶意流量,恶意攻击,或者 正常流程超过系统峰值

  • 恶意请求流量只访问到Cache
  • 对于穿透到后端应用的流量可以考虑使用Nginx的limit 模块
  • 恶意IP可使用nginx deny 进行屏蔽
  1. 切流程

    在某机房,或者某台服务器挂了之后,需要将打在这台机器上的流量切换到其他正常的服务器上

  • DNS 切域名
  • HTTP DNS ,主要在APP场景下,在客户端分配好流量入口,绕过运营商LocalDNS ,并实现精准的流量调度
  • LVS/HaProxy
  • Nginx
  1. 可回滚

    包括代码,数据存储的回滚

业务设计原理

  1. 防重设计
  2. 幂等设计
  3. 流程可定义
  4. 状态与状态机
  5. 后台系统操作可反馈
  6. 后台系统审核话
  7. 文档与备注
  8. 备份

缓存:提高系统访问速度增大系统处理能力,抗高并发流量银弹;

降级:当服务出现问题,或者影响到核心服务时,降级处理,如直接屏蔽,高峰过后再打开;

限流:不能通过降级来处理,比如核心业务,只能通过限流,挡掉一部分请求;

高可用

  • 可扩展学习 LVS+HaProxy

Nginx 配置

upsteam 配置

1
2
3
4
5
6
7
8
upstream backend {
server 192.168.1.21:9080 weight=1;
server 192.168.1.21:9081 weight=2;
}

location / {
proxy_pass http://backend;
}

负载均衡算法

  • round-robin : 轮询
  • ip_hash :根据客户IP进行负载均衡
  • hash key [consistent]
  • least_conn : 将请求负载到最不活跃的服务器
1
2
3
4
5
upstream backend {
ip_hash;
server 192.168.1.21:9080 weight=1;
server 192.168.1.21:9081 weight=2;
}

失败重试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
upstream backend {
server 192.168.1.21:9080 max_fails=2 fail_timeout=10s weight=1;
server 192.168.1.21:9081 max_fails=2 fail_timeout=10s weight=2;
}

// 在10s内失败2次,则认为该服务不可用,摘除,然后10s后会进行重试

location / {
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;

proxy_next_upstream error timeout;
proxy_next_upstream_timeout 10s;
proxy_next_upstream_tries 2;

proxy_pass http://backend;

add_header upstream_addr $upstream_addr;
}

健康检查

nginx 默认采用惰性检查,可集成 nginx_upstream_check_modul 来支持TCP 心跳和HTTP 心跳来实现健康检查
不安装不起作用,另 抓包工具 wireshark

  1. TCP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    upstream backend {
    server 192.168.1.21:9080 weight=1;
    server 192.168.1.21:9081 weight=2;
    check interval=3000 rise=1 fall=3 timeout=20000 type=tcp;
    }
    //interval 检查间隔时间,此处为3s 检查1次
    //fail 检查失败多少次,标记上游不存活
    //rise 检查成功多少次,标记上游存活,可处理请求
    //timeout 检测请求超时时间配置
  2. HTTP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    upstream backend {
    server 192.168.1.21:9080 weight=1;
    server 192.168.1.21:9081 weight=2;
    check interval=3000 rise=1 fall=3 timeout=20000 type=tcp;
    check_http_send "HEAD /status HTTP/1.0 \r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
    }
    //check_http_send 检查时发的HTTP请求内容
    //check_http_expect_alive 当上游服务器返回匹配的响应状态码,则标记存活

其他配置

1
2
3
4
5
upstream backend {
server 192.168.1.21:9080 weight=1 ;
server 192.168.1.21:9081 weight=2 backup; //当所有服务器不存活,流量才会打到该节点
server 192.168.1.21:9081 weight=3 down; //永久不可用,可用来摘除服务器
}

长连接

特指 Nginx 与上游服务器的长连接,上游服务器不要忘记开启长连接

缓存配置

  1. 全局配置
1
2
3
4
5
6
7
8
9
10
proxy_buffering             on;
proxy_buffer_size 4k;
proxy_buffers 512 4k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 256k;
proxy_cache_lock on;
proxy_cache_lock_timeout 200ms;
proxy_temp_path /tmpfs/proxy_temp;
proxy_cache_path /tmpfs/proxy_cache levels=1:2 keys_zone=cache:512m inactive=5m max_size=8g;
proxy_connect_timeout 3s;
  1. location 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
location  / {

proxy_next_upstream error timeout;
proxy_next_upstream_timeout 10s;
proxy_next_upstream_tries 2;

//请求上游服务器使用GET方法
proxy_method GET;
//不给上游服务器传递请求体
proxy_pass_request_body off;
//不给上游服务器传递请求头
proxy_pass_request_headers off;
//设置上游服务器那些响应头不发送给客户端
proxy_hide_header Vary;


proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;

//可以开启gzip


proxy_pass http://backend;

add_header upstream_addr $upstream_addr;
}

HTTP 动态负载均衡

动态的实现 上游服务器的增减

  1. Consul + Consul-template
  2. Consul + OpenResty

隔离术

  1. 爬虫隔离

    在使用OpenResty 不仅对爬虫 user-agent 过滤,还会过滤一些恶意IP(通过统计IP访问量来配阀值),将他们分流到固定的分组,这种情况存在一定误杀,因为公司的公网IP 一般是同一个,所以 可以考虑 IP+Cookie 的方式进行过滤

  2. 热点隔离

秒杀,抢购 属于热点,可做成独立系统,保证秒杀,抢购出线问题不会影响主流程

使用 Hystrix 实现隔离

Hystrix 是 Netflix 开源的一款针对分布式系统的延迟和容错库,目的是用来隔离分布式服务故障

  • 提供信号量和线程隔离,以减少不同服务之间资源的竞争带来的相互影响
  • 提供优雅降级机制
  • 提供熔断机制,使服务可以快速失败,而不是一直阻塞等待服务,并能快速恢复
  • 提供请求缓存,请求合并等灵活功能

基于Servlet 3 实现请求隔离

必须使用实现了Servlet 3规范的容器进行处理,如Tomcat 7.x

  • 基于NIO能处理更高的并发连接数,使用JDK 7 配合 Tomcat 7
  • 请求解析和业务处理线程池分离
  • 根据业务重要性对业务分级,并分级线程池
  • 对业务线程池进行监控、运维、降级等处理

异步化之后吞吐量上升,响应时间不会减少

限流

常用的限流有

  • 限制总并发数(如 数据库连接池,线程池)
  • 限制瞬时并发数(如 Nginx 的limit_conn 模块)
  • 限制时间窗口内的平均速率(如 Guava 的RateLimiter, Nginx 的limit_req 模块)
  • 限制远程接口调用速率,限制MQ消费速率等
  • 还可以根据网络连接数,网络流量,CPU或内存负载

限流算法

  • 令牌桶 :放一个固定容量令牌的桶,按固定速率往桶里添加令牌
  • 漏桶算法 :一个固定容量的漏桶,按固定的速率流出水滴

应用级限流

  • 对于一个应用系统来说,一定会有极限并发/请求数,如使用Tomcat ,Connector 有一些配置
  • acceptCount:如果Tomcat线程忙,新来的连接会进入队列排队
  • maxConnections:瞬时最大连接数,超出会排队
  • maxThreads:处理请求的最大线程数,
  • 对于接口,现在总并发/平均速率

    Java中的AtomicLong ;Hystrix 信号量模式下 使用Semaphore ;Guava的Cache存储计数量;Guava 的RateLimiter;

分布式限流

分布式限流最关键的是要将限流服务做成原子化

  1. Redis+Lua 实现

  2. Nginx+Lua 实现

接入层限流

接入层:通常指流量的入口,该层如要目的有:负载均衡,非法请求过滤,请求聚合,缓存,降级,限流,A/B测试,服务质量监控等

对于使用Nginx 接入层限流,可使用Nginx 自带的两个模块

  • ngx_http_limit_conn_module :连接数限流模块
  • ngx_http_limit_req_module :漏桶算法实现请求限流模块
    还可使用OpenResty 提供的Lua 限流模块 lua-resty-limit-traffic

降级

  • 自动降级

    根据系统负载,资源使用情况,SLA 等指标进行自动降级,

  • 人工降级

    一般大促期间,线上灰度测试 等 可手工降级

  • 配置中心
    • 如使用properties本地文件 ,可借助JDK 7 WatchService 实现文件变更监听
    • 如使用分布式配置中心(ZK,Consul)

扩展:使用Consul 实现配置中心
扩展:使用Hystrix 实现降级,实现熔断

超时与重试

超时会导致请求等待时间延长,重试会导致请求增多,所以需要对他们进行一些配置

代理层

  1. Nginx
    主要有4类设置
  • 客户端超时设置

    • client_header_timeout xxx :设置读取客户端请求头超时,默认60s,响应408状态给客户端
    • client_body_timeout xxx : 设置读取客户端内容超时,默认60s,是指两次成功读取操作时间间隔,不是整个请求体的超时时间,返回408
    • send_timeout xxx :设置发送响应到客户端的超时时间,默认60s,同样指两次间隔,如果客户端没有收到响应,Nginx关闭此了解
    • keepalive_timeout xxx [yyy] : 设置Http 长连接超时时间,xxx 表示Nginx长连接超时,默认75s,yyy 设置响应头 “Keep-Alive: timeout=yyy” 告知客户端长连接超时时间,可不一样 ,如果 xxx = 0 表示禁用长连接

      此参数需要配合 keepalive_disable(禁用那些游览器长连接,默认 msie6)与keepalive_requests(长连接请求数,默认100)

  • 代理超时设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
upstream backend {
server 192.168.1.21:9080 max_fails=2 fail_timeout=10s weight=1;
server 192.168.1.21:9081 max_fails=2 fail_timeout=10s weight=1;
}

// 在10s内失败2次,则认为该服务不可用,摘除,然后10s后会进行重试

location / {

//设置与上游服务器建立连接超时时间,默认60s,此事件不超过75s
proxy_connect_timeout 5s;
//设置与上游服务器读取响应超时时间,默认60s,指两次成功读操作时间间隔。
proxy_read_timeout 5s;
//设置与上游服务器发送请求超时时间,默认60s,两次间隔
proxy_send_timeout 5s;


//设置什么情况下,请求下一台服务器进行重试
proxy_next_upstream error timeout;
//设置重试最大超时时间,默认0,表示不限制
proxy_next_upstream_timeout 10s;
//设置重试次数,默认0, 不限制
proxy_next_upstream_tries 2;

//以上表示:当error/timeout 时重试 upsteam 中的下一台上游服务器,如果重试总时间超过10s 或者重试了1次(之前请求过1次,2-1),表示重试失败,Nginx 结束重试并返回客户端响应。


proxy_pass http://backend;

add_header upstream_addr $upstream_addr;
}

以上配置有3组,网络连接/读/写超时设置;失败重试机制设置;upstream存活超时设置

  • Twemproxy

    Twitter 开源的Redis 和Memcache 代理中间件,减少后端缓存服务器连接数
    扩展学习 Twemproxy

Web容器

以 Tomcat 8.5 为例

  • connectionTimeout:配置与客户端建立连接超时时间,默认 60*1000 (60s)
  • socket.soTimeout: 从客户端读取请求的超时时间,默认同上,扩展阅读 NIO 与NIO2
  • asyncTimeout:Servlet 3 异步请求超时时间 默认 30000(30s)
  • disableUploadTimeout 和 connectionUploadTimeout:文件上传超时与否,超时时间。
  • keepAliveTimeout 和 maxKeepAliveRequests ,keepAliveTimeout 默认为connectionTimeout,配置-1 表示永不超时。maxKeepAliveRequests 默认100

中间件

以 Dubbo 为例,配置服务提供端与注册中心进行注册、发现的超时时间

数据库

以 Druid 为例,设置连接池的超时

NoSQl

以 MongoDB ,使用 spring-data-mongodb 客户端,配置超时时间
以 Redis,使用 Jedis 客户端,设置超时时间

Ajax 超时

1
2
3
4
5
$.ajax({
url:"http://xxxxx",
timeout:2000,
xxxx:xxxxx
})

JSONP 超时

回滚机制

事务回滚

在分布式系统中,一般采用最终一致性进行事务回滚,可采用: 事务表,消息队列,补偿机制,TCC模式(预占/确认/取消), Sagas模式(拆分事务+补偿机制)

部署版本回滚

版本的备份,灰度发布,通过Nginx A/B方式 慢慢讲流量引入到新版本集群

静态资源回滚

静态资源一般会放在CDN上,如果需要回滚需要清除CDN

高并发

缓存

  • 缓存命中率 = 从缓存中读取次数/总读取次数(从缓存中读取次数+从慢速设备上读取的次数)

  • 缓存回收策略

  • 基于空间:如设置10MB ,当达到存储空间上限时,按照一定的策略移除数据
  • 基于容量:设置最大大小,当条目超过最大时,按照一定的策略移除数据
  • 基于事件:TTL(Time to live) 存活期,缓存过了存活期,就将删除;TTI (Time to Idel) 空闲期,既缓存多久没有被访问后,将别移除
  • 基于Java对象引用 :软引用,弱引用
  • 缓存回收算法

    使用基于空间和基于容量的缓存会使用一定的策略移除旧数据,常见有

    • FIFO(First In First Out) :先进先出算法
    • LRU(Least Recently Used) :最近最少使用算法
    • LFU(Least Frequently Used):最不常用算法

Java 缓存类型

Guava Cache 只提供堆缓存,小巧灵活,性能最好

Ehchache 3.x 提供 堆缓存,堆外缓存,磁盘缓存,分布式缓存,但API不完善,2.x API较稳定(不支持堆外缓存)

MapDB 是一款嵌入式Java 数据库引擎和集合框架,提供Maps,Sets,Lists,Queues,Bitmaps 的支持,还支持ACID事务,增量备份。支持堆缓存,堆外缓存,磁盘缓存。

  • 堆缓存

    推荐使用Gauva Cache

  • 堆外缓存

  • 磁盘缓存

  • 分布式缓存

    推荐使用Redis (有主从与集群模式)

多级缓存封装

如果项目中使用了多级缓存,(不仅会使用本地缓存,还会使用分布式缓存),需要适当的API封装,以简化操作

  • 定义NULL 对象
    1
    private static final String NULL_STRING = new String();

当DB没有数据时,写入NULL对象到缓存,防止当KEY对应的数据再DB中不存在时,频繁的查询DB

  • 强制获取最新数据
  • 失败统计
  • 延迟报警

缓存使用模式实践

是否需要在程序中同时操作缓存与SOR 来区别

  • Cache-Aside(同时操作)
  • Cache-As-SoR(只操作缓存,由缓存作为中间件操作SOR)

HTTP缓存

HTTP缓存

  • Last-Modified: 表示文档最后修改时间,当去服务器验证的时候,会用到这个时间。
  • Expires: http/1.0 规范定义,表示文档在游览器中的过期时间,当缓存内容时间超过这个时间,则需要重新去服务器获取最新内容
  • Cache-Control: http/1.1规范定义,表示游览器缓存控制,max-age=20 表示文档可以在游览器中缓存20秒

根据规范定义 Cache-Control 优先级高于 Expres,实际使用时可以两个都用,或者使用Cache-Control 就可以。

按F5刷新时候,请求头会包含 Cache-Control,If-Modified-Since(值为上次返回的 Last-Modified)

  • Age /Vary :一般用于缓存代理层(如 CDN)
  • ETag:可理解为文档的摘要

HttpClient客户端缓存

HttpClient 4.3版本开始提供 Http 1.1 兼容的客户端缓存,可以把该层当做游览器缓存,通过责任链模式来支持可插拔的组件结构,来实现客户端缓存。

另扩展阅读 http 1.0 与http 1.1

Nginx 代理层缓存

Nginx 提供 expires,etag,if-modified-since 指令来实现游览器缓存控制

1
2
3
4
5
6
location /img{
alias /export/img/;
expires 1d;
}
//当访问静态资源时,会响应头会返回 ,Cache-Control:max-age=86400 Etag:"zxc223" Last-Modified:xxxx
// 可通过设置 etag off 关闭自动生成 ETag

Nginx proxy_pass

  1. 游览器发出请求,Nginx 根据URL在本地查找是否有缓存
  2. 没找到,回源,Nginx 本地缓存刷新
  3. 找到缓存,验证文档是否过期,过期回源,不过期返回

多级缓存

  • 如何缓存数据
  • 分布式缓存与应用负载均衡
  • 热点数据与更新缓存

如商品这种,可才分如基础属性,图片列表,上下架,规格等,实现增量更新,减少全量更新
使用Redis时,要警惕大Vaue,可拆解小的或者压缩
热点数据如果经常访问分布式缓存,可能造成分布式缓存流量集中,可在本地设置缓存

  • 缓存崩溃与快速修复
    • 主从机制,做好冗余,既其中一部分不可用,将对等的部分补上去
    • 如果因为缓存导致性能下降,可考虑部分用户降级,通过Worker预热缓存数据

池化技术

  • 数据库连接池
  • HttpClient 连接池
  • 线程池

异步并发实战

  • 异步Future
  • 异步Callback
  • 异步编排 CompletableFuture
  • 异步Web服务实现
  • 请求缓存与请求合并

扩容与拆分

  • 单系统水平扩容,垂直扩容
  • 应用拆分
  • 数据库拆分

    使用sharding-jdbc 分库分表

  • 数据异构

队列术

队列,在数据结构中是一种线性表,从一端插入数据,从另一端删除数据,在实际中经常使用队列来进行异步处理,系统解耦,数据同步,流量削峰,扩展性,缓冲。 在设计时,需要注意 是否需要保证消息处理的有序性,重复消息,幂等性等问题。

  • 缓冲队列

    典型的如Log4j 日志缓冲区,当我们使用log4j时,可配置字节缓冲区,当字节缓冲区满时,会立即同步到磁盘,他使用BufferedWriter 实现

  • 任务队列

    可以将一些不需要与主线程同步执行的任务,扔到任务队列进行异步处理。如 线程池队列(LinkedBloackingQueue,Disruptor,RingBuffer)

  • 消息队列

    如 各种JMS实现,扩展阅读ActiveMQ,Kafka

  • 数据总线队列

    扩展阅读 Canal(阿里),Databus(LinkedIn),otter,kettle

Disruptor+Redis 队列

  • 简介
  • XML 配置
  • EventWorker
  • EventPublishThread
  • EventHandler
  • EventQueue