第2部分 高可用
构建高可用系统从哪几部分入手
负载均衡与反向代理
balabala主要用nginx做负载均衡,常见负载均衡算法:加权轮询,ip hash,一致性hash。
还有各种nginx模块的介绍,还有配合lua脚本保证原子性
隔离术
线程隔离,进程隔离,机房隔离,读写隔离,动静隔离,爬虫隔离…还有使用hystrix实现隔离
限流
令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
-
假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
-
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
-
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
-
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
漏桶算法
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:
-
一个固定容量的漏桶,按照常量固定速率流出水滴;
-
如果桶是空的,则不需流出水滴;
-
可以以任意速率流入水滴到漏桶;
-
如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
令牌桶和漏桶对比
-
令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
-
漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
-
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
-
漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
-
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
-
两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
节流
throttleFirst/Last
throttleFirst/ throttleLast是指在一个时间窗口内,如果有重复的多个相同事件要处理,则只处理第一个或最后一个。其相当于一个事件频率控制器,把一段时间内重复的多个相同事件变为一个,减少事件处理频率,从而减少无用处理,提升性能。
throttleWithTimeout
throttleWithTimeout也叫做debounce(去抖),限制两个连续事件的先后执行时间不得小于某个时间窗口。
降级
如淘宝双十一时候关闭评论,进行服务降级
超时与重试机制
回滚机制
压测和预案
第3部分 高并发
应用级缓存
-
SoR(system-of-record):记录系统,或者可以叫做数据源,即实际存储原始数据的系统。
-
Cache:缓存,是SoR的快照数据,Cache的访问速度比SoR要快,放入Cache的目的是提升访问速度,减少回源到SoR的次数。
-
回源:即回到数据源头获取数据,Cache没有命中时,需要从SoR读取数据,这叫做回源。
Cache-Aside
Cache-Aside即业务代码围绕着Cache写,是由业务代码直接维护缓存,示例代码如下所示。
读场景,先从缓存获取数据,如果没有命中,则回源到SoR并将源数据放入缓存供下次读取使用。
//1、先从缓存中获取数据
value = myCache.getIfPresent(key);
if(value == null) {
//2.1、如果缓存没有命中,则回源到SoR获取源数据
value = loadFromSoR(key);
//2.2、将数据放入缓存,下次即可从缓存中获取数据
myCache.put(key, value);
}
写场景,先将数据写入SoR,写入成功后立即将数据同步写入缓存。
//1、先将数据写入SoR
writeToSoR(key,value);
//2、执行成功后立即同步写入缓存
myCache.put(key, value);
或者先将数据写入SoR,写入成功后将缓存数据过期,下次读取时再加载缓存。
//1、先将数据写入SoR
writeToSoR(key,value);
//2、失效缓存,然后下次读时再加载缓存
myCache.invalidate(key);
Cache-As-SoR
Cache-As-SoR即把Cache看作为SoR,所有操作都是对Cache进行,然后Cache再委托给SoR进行真实的读/写。即业务代码中只看到Cache的操作,看不到关于SoR相关的代码。有三种实现:read-through、write-through、write-behind。
Read-through
Read-Through,业务代码首先调用Cache,如果Cache不命中由Cache回源到SoR,而不是业务代码(即由Cache读SoR)。使用Read-Through模式,需要配置一个CacheLoader组件用来回源到SoR加载源数据。Guava Cache和Ehcache 3.x都支持该模式。
Guava Cache实现
LoadingCache<Integer,Result
CacheBuilder.newBuilder()
.softValues()
.maximumSize(5000).expireAfterWrite(2, TimeUnit.MINUTES)
.build(new CacheLoader<Integer,Result<Category>>() {
@Override
public Result<Category> load(final Integer sortId) throwsException {
return categoryService.get(sortId);
}
});
在build Cache时,传入一个CacheLoader用来加载缓存,操作流程如下。
1.应用业务代码直接调用getCache.get(sortId)。
2.首先查询Cache,如果缓存中有,则直接返回缓存数据。
3.如果缓存没有命中,则委托给CacheLoader,CacheLoader会回源到SoR查询源数据(返回值必须不为null,可以包装为Null对象),然后写入缓存。
使用CacheLoader后有几个好处。
● 应用业务代码更简洁了,不需要像Cache-Aside模式那样缓存查询代码和SoR代码交织在一起。如果缓存使用逻辑散落在多处,则使用这种方式很简单的消除了重复代码。
● 解决Dog-pile effect,即当某个缓存失效时,又有大量相同的请求没命中缓存,从而同时请求到后端,导致后端压力太大,此时限制一个请求去拿即可。
if (firstCreateNewEntry) {//第一个请求加载缓存的线程去SoR加载源数据
try {
synchronized (e) {
returnloadSync(key, hash, loadingValueReference, loader);
}
} finally{
statsCounter.recordMisses(1);
}
} else {//其他并发线程等待“第一个线程”加载的数据
return waitForLoadingValue(e, key,valueReference);
}
Guava Cache还支持get(K key, Callable<? extends V> valueLoader)方法,传入一个Callable实例,当缓存没命中时,会调用Callable#call来查询SoR加载源数据。
Ehcache 3.x实现
CacheManager cacheManager = CacheManagerBuilder. newCacheManagerBuilder(). build(true);
org.ehcache.Cache<String, String> myCache =cacheManager. createCache (“myCache”,
CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB))
.withDispatcherConcurrency(4)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)))
.withLoaderWriter(newDefaultCacheLoaderWriter<String, String> () {
@Override
public String load(String key) throws Exception {
return readDB(key);
}
@Override
public Map<String, String> loadAll(Iterable<? extendsString> keys) throws BulkCacheLoadingException, Exception {
return null;
}
}));
Ehcache 3.x使用CacheLoaderWriter来实现,通过load(K key)和loadAll(Iterable<? extends K> keys)分别来加载单个KEY和批量KEY。Ehcache 3.1没有自己去解决Dog-pile effect。
Write-through
Write-Through,称之为穿透写模式/直写模式,业务代码首先调用Cache写(新增/修改)数据,然后由Cache负责写缓存和写SoR,而不是业务代码。使用Write-Through模式需要配置一个CacheWriter组件用来回写SoR。Guava Cache没有提供支持。Ehcache 3.x支持该模式。Ehcache需要配置一个CacheLoaderWriter,CacheLoaderWriter知道如何去写SoR。当Cache需要写(新增/修改)数据时,首先调用CacheLoaderWriter来同步(立即)到SoR,成功后会更新缓存。
Write-behind
Write-Behind,也叫Write-Back,称之为回写模式,不同于Write-Through是同步写SoR和Cache,Write-Behind是异步写。异步之后可以实现批量写、合并写、延时和限流。
http缓存
多级缓存
所谓多级缓存,即在整个系统架构的不同系统层级进行数据缓存,以提升访问效率,这也是应用最广的方案之一。我们应用的整体架构如下图所示:
整体流程如上图所示:
1、首先接入Nginx将请求负载均衡到应用Nginx,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升应用Nginx的缓存命中率;后续负载均衡和缓存算法部分我们再细聊;
2、接着应用Nginx读取本地缓存(本地缓存可以使用Lua Shared Dict、Nginx Proxy Cache(磁盘/内存)、Local Redis实现),如果本地缓存命中则直接返回,使用应用Nginx本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点问题非常有效;为什么要使用应用Nginx本地缓存我们将在热点数据与缓存失效部分细聊;
3、如果Nginx本地缓存没命中,则会读取相应的分布式缓存(如Redis缓存,另外可以考虑使用主从架构来提升性能和吞吐量),如果分布式缓存命中则直接返回相应数据(并回写到Nginx本地缓存);
4、如果分布式缓存也没有命中,则会回源到Tomcat集群,在回源到Tomcat集群时也可以使用轮询和一致性哈希作为负载均衡算法;
5、在Tomcat应用中,首先读取本地堆缓存,如果有则直接返回(并会写到主Redis集群),为什么要加一层本地堆缓存将在缓存崩溃与快速修复部分细聊;
6、作为可选部分,如果步骤4没有命中可以再尝试一次读主Redis集群操作,目的是防止当从有问题时的流量冲击;
7、如果所有缓存都没有命中只能查询DB或相关服务获取相关数据并返回;
8、步骤7返回的数据异步写到主Redis集群,此处可能多个Tomcat实例同时写主Redis集群,可能造成数据错乱,如何解决该问题将在更新缓存与原子性部分细聊。
整体分了三部分缓存:应用Nginx本地缓存、分布式缓存、Tomcat堆缓存,每一层缓存都用来解决相关的问题,如应用Nginx本地缓存用来解决热点缓存问题,分布式缓存用来减少访问回源率、Tomcat堆缓存用于防止相关缓存失效/崩溃之后的冲击。
连接池
数据库连接池,线程池,httpclient连接池
队列
其他
说实话不推荐这本书。。。f5物理机,数据库表分库发表,水平切分,垂直切分。数据异构,队列,cdn加速,异步future。
xss:XSS又叫CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意用户的特殊目的。
csrf:CSRF(Cross site request forgery),即跨站请求伪造。我们知道XSS是跨站脚本攻击,就是在用户的浏览器中执行攻击者的脚本,来获得其cookie等信息。而CSRF确实,借用用户的身份,向web server发送请求,因为该请求不是用户本意,所以称为“跨站请求伪造”。
cap原理:分布式计算系统不可能同时确保一致性(Consistency)、可用性(Availablity)和分区容忍性(Partition),设计中往往需要弱化对某个特性的保证。
- 一致性(Consistency):任何操作应该都是原子的,发生在后面的事件能看到前面事件发生导致的结果,注意这里指的是强一致性;
- 可用性(Availablity):在有限时间内,任何非失败节点都能应答请求;
- 分区容忍性(Partition):网络可能发生分区,即节点之间的通信不可保障。