张伦聪的技术博客 Research And Development

读《亿级流量网站架构核心技术》后感

2018-05-27

第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> getCache =

   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):网络可能发生分区,即节点之间的通信不可保障。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏博主

下一篇 netty源码解析

留言