在设计系统时,应该多思考墨菲定律:
1.任何事情都没有表面看起来那么简单;
2.所有的事都会比你预计的时间长;
3.可能出错的事总会出错;
4.如果你担心某种情况发生,那么它就更有可能发生。
大流量缓冲:
电商大促时,系统流量高于正常流量的几倍甚至几十倍,解决手段很多,一般都是牺牲强一致性,而保证最终一致性。比如扣减库存:
数据闭环:
因为数据来源太多,影响服务稳定性的因素就非常多,可以把使用到的数据进行异构存储,形成数据闭环,基本步骤:
1.数据异构:通过如MQ机制接收数据变更,然后原子化存储到redis或持久化kv存储
2.数据聚合:异构的目的是把数据从多个数据源拿过来,聚合就是把这些数据做个聚合, 前端一次调用就能拿到所有数据,此步骤一般存储到kv存储中
3.前端展示:前端通过一次或少量几次调用拿到所有数据
这种方式的好处是数据的闭环,任何依赖系统出问题,还是能正常工作,只是更新会有积压,但是不影响前端展示。
eg:一次需要多个数据,考虑使用Hash Tag机制,如获取商品基本信息“p:productId:”和商品规格参数“d:productId:”,可以使用冒号中间的productId作为数据分片key,这样相同productId的商品相关数据就在一个实例。
缓存银弹:
1:浏览器端缓存:适用于对实时性不太敏感的数据,如商品详情页框架,商家评分,评价,广告词等;但对于价格,库存等实时要求比较高的数据,就不能做浏览器端缓存;
2:APP客户端缓存:大促之前把APP需要访问的一些素材,如:js/css/image提前下发到客户端进行缓存;APP地图也会做离线缓存,在网络异常情况下有托底数据给用户展示;
3:CDN缓存:让用户能在离他最近的节点找到想要的数据(使用CDN时,URL不能有随机数,否则每次都穿透CDN,回源到源服务器)
4:接入层缓存:使用nginx搭建一层接入层
(1)一致性Hash:保证相同数据落到一台服务器上;
(2)nginx配置proxy_cache缓存内容:
https://blog.csdn.net/dengjiexian123/article/details/53386586
(3)nginx配置proxy_cache_lock,使用lock机制,将多个回源合并为一个:
当多个客户端同时请求同一份内容时,如果开启proxy_cache_lock(默认off)则只有一个请求被发送至后端;其他请求将等待该内容返回;当第一个请求返回时,其他请求将从缓存中获取内容返回;当第一个请求超过了proxy_cache_lock_timeout超时时间(默认5s),则其他请求将同时请求到后端来获取响应,且响应不会被缓存;启用proxy_cache_lock可以应对雪崩效应;
5:应用层缓存:local redis cache,在应用所在服务器上部署一组redis,应用直接读本 机redis获取数据;
6:分布式缓存:
(1)接入层(nginx+lua)读取本地proxy cache/local cache
(2)不命中,读取分布式redis集群
(3)还不命中,回源到Tomcat,读取Tomcat应用堆内cache
(4)都没命中,调用依赖业务获取数据,异步化写到redis集群
负载均衡需要关心几个方面:
1:上游服务器配置:使用upstream server配置上游服务器
upstream backend {
server 192.168.61.1:9080 weight = 1;
server 192.168.61.1:9090 weight = 2;
}
location / {
proxy_pass http://backend;
}
2:负载均衡算法:
upstream backend {
round-robin; //轮询
ip_hash; //根据客户端ip负载均衡
hash $request_uri; //根据请求uri进行负载均衡
hash $consistent_key consistent; //一致性hash算法
least_conn; //负载到最少活跃连接的上游服务器
least_time; //最小平均响应时间进行负载均衡(商业版)
server 192.168.61.1:9080 weight = 1;
server 192.168.61.1:9090 weight = 2;
}
3:失败重试机制:
upstream backend {
server 192.168.61.1:9080 max_fails=2 fail_timeout=10s weight = 1;
server 192.168.61.1:9090 max_fails=2 fail_timeout=10s weight = 2;
//max_fails:允许请求失败的次数
//fail_timeout:max_fails次失败后,暂停的时间,之后再重试
}
location /test {
proxy_connect_timeout 5s;
//nginx与upstream server的连接超时时间,不管upstream server有没有足够的线程处理请求,只要建立连接就ok
proxy_read_timeout 5s;
//连接成功后,等候后端服务器响应时间
proxy_send_timeout 5s;
//后端服务器数据回传时间
proxy_next_upstream error timeout;
proxy_next_upstream_timeout 6s;
proxy_next_upstream_tries 2;
//当error timeout时,在6s内允许2次重试下一台上游服务器
client_header_timeout 5s;
//读取客户端请求头超时时间
client_body_timeout 10s;
//读取客户端内容体超时时间
send_timeout 10s;
//nginx向客户端传输数据的超时时间
proxy_pass http://backend;
}
4:服务器心跳检查:
upstream backend {
server 192.168.61.1:9080 weight = 1;
server 192.168.61.1:9090 weight = 2;
check interval=3000 rise=1 fall=3 timeout=2000 type=tcp;
//nginx使用tcp对上游服务器进行健康检查
//interval:每隔3秒检测一次
//检测失败3次后,上游服务器标识为不存活
//检测成功1次后,上游服务器标识为存活,可以处理请求
//检测请求超时时间配置
}
Http动态负载均衡:
Consul+Consul-template:
1:upstream服务启动,通过Consul管理后台注册服务;
2:Nginx机器上部署并启动Consul-template Agent,通过长轮询监听服务变更;
3:Consul-template监听到变更后,动态修改upstream列表;
4:调用重启Nginx脚本,重启nginx。
Consul+OpenResty:
1:upstream服务启动,通过Consul管理后台注册服务;
2:nginx启动时,调用init_by_lua,拉取配置,并更新到共享字典来存储upstream列表,然后通过init_worker_by_lua启动定时器,定期去Consul拉取配置并实时更新共享字典;
3:balancer_by_lua使用共享字典存储的upstream列表进行动态负载均衡。
隔离术:
1:线程隔离:将请求分类,交给不同的线程池处理;
2:进程隔离:将系统拆分为多个子系统,使得某一个子系统出现问题,不会影响其它子系统;
3:集群隔离:当秒杀服务被刷会影响其他服务稳定性时,应考虑为秒杀提供单独的服务集群,即服务分组,实现故障隔离;
4:机房隔离:一个机房发生问题,可通过DNS/负载均衡将请求全部切到另一个机房;
5:读写分离:通过主从模式,将读和写集群分离;
6:动静隔离:JS/CSS/图片等静态资源放在CDN上,防止访问量一大,带宽被打满,出现不可用;
7:爬虫隔离:通过限流或者将爬虫路由到单独集群,保证正常流量可用;
set $flag 0;
if ($http_user_agent ~* “spider”) {
set $flag = “1”;
}
if ($flag = “0”) {
//代理到正常集群
}
if ($flag = “1”) {
//代理到爬虫集群
}
限流详解:
1:数组方式频率控制:获取当前时间对数组求模得到数组下标,对该下标处的请求数进行原子+1操作,来判断是否达到每秒钟最大访问次数;
eg:
2:redis+抖动值方式频率控制:利用redis(设置合理失效时间)保存该ip地址上一次访问/controller/action的时间,根据设置的频率控制大小,计算访问一次的时间间隔,得到上次访问之后,下次访问时间应该在多少之后。设置抖动值为2,允许某些接口连续访问两次的情况;
eg:
3:TokenBucket算法:频率控制桶按照1/QPS的时间间隔(如果QPS是100,间隔是10ms),向桶中添加令牌。每次访问,若能从桶中拿走一个可用token,则没有超过访问频率;
eg:
4:使用redis:服务每秒/每分钟/每天的调用量;
5:漏桶算法:请求先进入漏桶,漏桶以一定速度出水,当请求速度过大导致漏桶溢出,就拒绝请求;
6:限流总并发/连接/请求数:Tomcat设置acceptCount(正在处理量+排队大小),maxConnects(瞬时最大连接数,超过会排队),maxThreads(请求最大线程数);Mysql:max_connections(最大用户连接数);Redis:maxclients,tcp-backlog(进程还没有accept的TCP连接的队列长度);
7:ngx_http_limit_conn_module:根据定义的键(域名,ip等)来限制每个键值的并发连接数;
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
//$binary_remote_addr:限流的key
//addr:限流的共享内存区域,以及大小
limit_conn_log_level error;
//被限流后的日志级别,默认error
limit_conn_status 503;
//被限流后返回的状态码
server {
location /limit {
limit_conn addr 1;
//nginx限制addr中的每个key,同一时刻最大处理1个连接
}
}
}
8:ngx_http_limit_req_module:根据定义的键(域名,ip等)来限制每个键值的请求速率(漏桶算法实现);
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
//$binary_remote_addr:限速的key
//one:限速的共享内存区域,以及大小
//rate:每个key的请求速率
limit_req_log_level error;
//超速后的日志级别,默认error
limit_req_status 503;
//超速后返回状态码
server {
location /limit {
limit_req zone=one burst=5 nodelay;
//one:限速区域;burst:桶容量;nodelay:超速后是否延迟处理
//burst>0 && nodelay:表示允许突发处理请求
}
}
}
9:throttleFirst/throttleLast:在一个时间窗口内,只处理第一个或最后一个事件(eg:网页中的resize,scroll,mousemove事件);
注:
使用redis进行控制,需要调用redis同步接口,可以理解为对多台机器整体进行频率控制。
多级缓存:支撑海量读服务:http://www.importnew.com/18983.html
1:接入Nginx将请求负载均衡到应用Nginx(多个机房)
轮询:使服务器请求更加均衡
一致性哈希:提高应用Nginx缓存命中率
2:应用Nginx读取本地缓存(降低后端压力,命中则直接返回)
Lua Shared Dict / Nginx Proxy Cache / Local Redis(本机的Redis集群)
3:读取相应分布式缓存(命中则直接返回,并回写Nginx本地缓存)
4:回源到Tomcat集群,读取本地堆缓存(轮询/一致性哈希)
5:可选:4未命中,尝试一次读主redis集群(防止从redis集群有问题的流量冲击)
6:查询DB或相关服务获取相关数据
7:步骤6返回数据异步写到主Redis集群
存在问题:多个Tomcat实例同时写主Redis集群,造成数据错乱
解决思路:(1)更新数据时使用时间戳/版本对比 + Redis单线程原子操作
(2)将更新请求按规则分散到多个队列,每个对列使用单线程拉最新数据
(3)分布式锁:
a)Mysql:数据库排他锁,基于Mysql的InnoDB引擎
注:在查询语句后加for update,使用行级锁需建唯一索引
b)Redis:http://ifeve.com/redis-lock/
思路1:在Redis单实例里创建一个带有过期时间的键值
缺点:master在向slave同步之前宕机,会出现两客户端持相同锁情况
思路2:使用Redis SET req_key req_id NX PX 3000指令(和思路1相比:增加req_id)
NX:key不存在才会设置
PX:key超时时间
req_id:用来判断这把锁是哪个请求加的(释放锁时使用防止错误删除锁)
思路3:RedLock算法(假设N个Redis master节点)
a)客户端获取当前时间
b)轮流用相同req_key req_id在N个节点上请求锁
c)当客户端在N/2+1个master节点获取锁成功,并且总耗时没有超过锁失效时间,认为该锁获取成功
d)获取成功锁不超过N/2+1或着消耗时间超过锁释放时间,客户端需要到每个master节点去释放锁
eg:
上图可以去掉主Redis集群,将Local Cache和从Redis集群,改成独立的Redis集群进行管理,分别作为一级缓存,二级缓存。
Nginx本地缓存:解决热点缓存问题
分布式缓存:减少回源率
Tomcat堆缓存:应对上一层缓存失效/崩溃后的冲击
如何缓存数据:
不过期缓存思路一:
存在问题:(1)事务提交失败,缓存不回滚,造成不一致
(2)并发写缓存出现脏数据
不过期缓存思路二(改进):
改进点:(1)写缓存改成写消息
(2)同步缓存订阅消息,根据消息更新缓存
(3)数据一致性:
a) 消息体只传id拉最近数据更新缓存(不担心回滚)
b) 时间戳和内容摘要机制(MD5)进行缓存更新(时间戳控制mq版本)
池化技术:通过复用,提升性能
对象池:复用对象从而减少创建对象,垃圾回收开销;
数据库连接池,Redis连接池,HTTP连接池:复用TCP连接减少创建和释放连接;
线程池:减少频繁创建和销毁线程的过程;
eg:golang sql连接池