Donne

Donne 查看完整档案

北京编辑北京工商大学  |  自动化 编辑  |  填写所在公司/组织 github.com/donng 编辑
编辑

知其然,知其所以然。

个人动态

Donne 收藏了文章 · 9月23日

微服务容错 - 隔离熔断限流

​在高并发访问下,系统所依赖的服务的稳定性对系统的影响非常大,依赖有很多不可控的因素,比如网络连接变慢,资源突然繁忙,暂时不可用,服务脱机等。我们要构建稳定、可靠的分布式系统,就必须要有这样一套容错机制。常用的的容错技术如:隔离,降级,熔断,限流等策略,本文将详细的介绍微服务中的容错机制。

隔离机制

为什么要隔离? 比如我们现在某个接口所在的服务A需要调用服务B,而服务B同时需要调用C服务,此时服务C突然宕机同时此时流量暴涨,调用全部打到服务B上,此时B服务调用C超时大量的线程资源被该接口所占全部hang住,慢慢服务B中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用渐渐蔓延到B服务集群中的其他节点,导致服务级联故障。

1570592685522.png

此时我们就需要对服务出现异常的情况进行隔离,防止级联故障效应,常用的隔离策略有线程池隔离和信号量隔离

线程池隔离

线程池隔离顾名思义就是通过Java的线程池进行隔离,B服务调用C服务给予固定的线程数量比如10个线程,如果此时C服务宕机了就算大量的请求过来,调用C服务的接口只会占用10个线程不会占用其他工作线程资源,因此B服务就不会出现级联故障

1570593867373.png

信号量隔离

另一种隔离信号量隔离是使用JUC下的Semaphore来实现的,当拿不到信号量的时候直接拒接因此不会出现超时占用其他工作线程的情况。

Semaphore semaphore = new Semaphore(10,true);
//获取信号量
semaphore.acquire();
//do something here
//释放信号量
semaphore.release();

比较

​线程池隔离针对不同的资源分别创建不同的线程池,不同服务调用都发生在不同的线程池中,在线程池排队、超时等阻塞情况时可以快速失败。线程池隔离的好处是隔离度比较高,可以针对某个资源的线程池去进行处理而不影响其它资源,但是代价就是线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。而信号量隔离非常轻量级,仅限制对某个资源调用的并发数,而不是显式地去创建线程池,所以 overhead 比较小,但是效果不错,也支持超时失败。

比较项线程池隔离信号量隔离
线程与调用线程不同,使用的是线程池创建的线程与调用线程相同
开销排队,切换,调度等开销无线程切换性能更高
是否支持异步支持不支持
是否支持超时支持超时支持超时(新版本支持)
并发支持支持通过线程池大小控制支持通过最大信号量控制

降级熔断机制

​ 什么是降级和熔断?降级和熔断有什么区别?虽然很多人把降级熔断当着一个词来说的,但是降级和熔断是完全不同的概念的,看看下面几种场景:

场景一:比如我们每天上班坐公交,1路和2路公交都能到公司,但是2路公交需要下车走点路,所以平时都是坐1路公交,
突然有一天等了1路公交好久都没来,于是就坐了2路公交作为替代方案总不能迟到吧!下次再等1路车。
场景二:第二天,第三天 ... 已经一个星期了都没看到1路公交,心里觉得可能是1路公交改路线了,
于是直接坐2路公交了,在接下来的日子里都是直接忽略1路车直接坐2路车
场景三:突然有一天在等2路车的时候看到了1路车,是不是1路车现在恢复了,于是天天开心的坐着1路车上班去了,领导再也不担心我迟到了

场景一在1路车没等到的情况下采取降级方案坐2路车,这就是降级策略,场景二如果多次都没有等到1路车就直接不等了下次直接坐2路车,这就是熔断策略,场景三如果过段时间1路车恢复了就使用2路车,这就是熔断恢复!

降级机制

常用的降级策略如:熔断器降级,限流降级,超时降级,异常降级,平均响应时间降级等

1570591437265.png

  • 熔断器降级:即熔断器开启的时间直接熔断走降级的策略
  • 限流降级:对流量进行限制达到降级的效果,如:Hystrix中的线程池,信号量都能达到限流的效果
  • 超时降级:课时设置对应的超时时间如果服务调用超时了就执行降级策略,如:Hystrix中默认为1s
  • 异常降级:异常降级很简单就是服务出现异常了执行降级策略
  • 平均响应时间降级:服务响应时间持续飙高的时候实现降级策略,如Sentinel中默认的RT 上限是 4900 ms

熔断机制

​ 熔断其实是一个框架级的处理,那么这套熔断机制的设计,基本上业内用的是Martin Fowler提出的断路器模式,断路器的基本原理非常简单。您将受保护的函数调用包装在断路器对象中,该对象将监视故障。一旦故障达到某个阈值,断路器将跳闸,并且所有进一步的断路器调用都会返回错误,而根本不会进行受保护的调用。常见的断路器模式有基本模式和扩展模式。

基本模式:

  • 如果断路器状态为close,则调用断路器将调用supplier服务模块;
  • 如果断路器状态为open则直接返回错误;
  • 如果超时,我们将增加失败计数器,成功的调用会将其重置为零;
  • 通过比较故障计数和阈值来确定断路器的状态;

20191024163112.png

扩展模式:

基础模式的断路器避免了在电路断开时发出受保护的呼叫,但是当情况恢复正常时,将需要外部干预才能将其重置。对于建筑物中的电路断路器,这是一种合理的方法,但是对于软件断路器,我们可以让断路器本身检测基础调用是否再次正常工作。我们可以通过在适当的时间间隔后再次尝试受保护的调用来实现这种自我重置行为,并在成功后重置断路器。于是就出现了扩展模式:

20191024163133.png

  • 最开始处于closed状态,一旦检测到错误到达一定阈值,便转为open状态;
  • 这时候会有个 reset timeout,到了这个时间了,会转移到half open状态;
  • 尝试放行一部分请求到后端,一旦检测成功便回归到closed状态,即恢复服务;

熔断策略

我们通常用以下几种方式来衡量资源是否处于稳定的状态:

  • 平均响应时间:如Sentinel中的熔断就使用了平均响应时间,当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口之内,对这个方法的调用都会自动地熔断。
  • 异常比例 :主流的容错框架Hystrixsentinel中都使用了异常比例熔断策略,比如当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值之后,资源进入熔断状态,即在接下的时间窗口之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数:如Sentinel中的熔断就使用了异常数熔断策略,当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

限流机制

​ 限流也是提高系统的容错性的一种方案,不同的场景对“流”的定义也是不同的,可以是网络流量,带宽,每秒处理的事务数 (TPS),每秒请求数 (hits per second),并发请求数,甚至还可能是业务上的某个指标,比如用户在某段时间内允许的最多请求短信验证码次数。我们常说的限流都是限制每秒请求数,从分布式角度来看,限流可分为 分布式限流 (比如基于Sentinel或者Redis的集群限流)和 单机限流 。从算法实现角度来看,限流算法可分为 漏桶算法令牌桶算法滑动时间窗口算法

单机限流

漏桶算法

1570701032430.png

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;
  • 如果桶是空的,则不需流出水滴;
  • 可以以任意速率流入水滴到漏桶;
  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

令牌桶算法

1570700342271.png

  • 假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

固定时间窗口算法
20191024163948.png

这种实现计数器限流方式由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的,但是将间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮。

滑动时间窗口算法

1571219763433.png

  • 0、初始化,设置时间窗口,设置时间窗口时间点间隔长度;
  • 1、判断请求时间点是否在时间窗口中,在进入步骤2,否则进入步骤3;
  • 2、判断是否超过时间窗口限流值,是->进行限流,否->对应时间窗口计数器+1;
  • 3、移动当时时间窗口,移动方式是:起始时间点变为时间列表中的第二时间点,结束时间增加一个时间点。重新步骤一的判断 。

分布式限流

当应用为单点应用时,只要应用进行了限流,那么应用所依赖的各种服务也都得到了保护。 但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。

1571903140149.png

如果实现了分布式限流,那么就可以方便地控制整个服务集群的请求限制,且由于整个集群的请求数量得到了限制,因此服务依赖的各种资源也得到了限流的保护。

1571904304647.png

分布式限流方案

分布式限流的思想我列举下面三个方案:

1,Redis令牌桶

这种方案是最简单的一种集群限流思想。在本地限流中,我们使用Long的原子类作令牌桶,当实例数量超过1,我们就考虑将Redis用作公共内存区域,进行读写。涉及到的并发控制,也可以使用Redis实现分布式锁。

缺点:每取一次令牌都会进行一次网络开销,而网络开销起码是毫秒级,所以这种方案支持的并发量是非常有限的。

2,QPS统一分配

这种方案的思想是将集群限流最大程度的本地化。

举个例子,我们有两台服务器实例,对应的是同一个应用程序(Application.name相同),程序中设置的QPS为100,将应用程序与同一个控制台程序进行连接,控制台端依据应用的实例数量将QPS进行均分,动态设置每个实例的QPS为50,若是遇到两个服务器的配置并不相同,在负载均衡层的就已经根据服务器的优劣对流量进行分配,例如一台分配70%流量,另一台分配30%的流量。面对这种情况,控制台也可以对其实行加权分配QPS的策略。

缺点:这也算一种集群限流的实现方案,但依旧存在不小的问题。该模式的分配比例是建立在大数据流量下的趋势进行分配,实际情况中可能并不是严格的五五分或三七分,误差不可控,极容易出现用户连续访问某一台服务器遇到请求驳回而另一台服务器此刻空闲流量充足的尴尬情况。

3,发票服务器

这种方案的思想是建立在Redis令牌桶方案的基础之上的。如何解决每次取令牌都伴随一次网络开销,该方案的解决方法是建立一层控制端,利用该控制端与Redis令牌桶进行交互,只有当客户端的剩余令牌数不足时,客户端才向该控制层取令牌并且每次取一批。

缺点:这种思想类似于Java集合框架的数组扩容,设置一个阈值,只有当超过该临界值时,才会触发异步调用。其余存取令牌的操作与本地限流无二。虽然该方案依旧存在误差,但误差最大也就一批次令牌数而已。

参考

1,https://www.cnblogs.com/rjzhe...
2,https://www.martinfowler.com/...
3,https://www.cnblogs.com/babyc...
4,https://github.com/alibaba/Se...
5,https://www.jishuwen.com/d/2TX1
6,https://juejin.im/post/5c74a2...
7,https://www.jianshu.com/p/259...
8,https://zhuanlan.zhihu.com/p/...

查看原文

Donne 关注了专栏 · 9月12日

我们一起进大厂

微信搜索:三太子敖丙

关注 5052

Donne 赞了文章 · 9月2日

MySQL的锁到底有多少内容 ?再和腾讯大佬的技术面谈,我还是小看锁了!

对酒当歌,人生几何! 朝朝暮暮,唯有己脱。

苦苦寻觅找工作之间,殊不知今日之时乃我心之痛,难到是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一紧。正处为难之间,手机忽然来了个短信预约后续面试。 我即刻三下五除二拎包踢门而出。飞奔而去。

此刻面试门外首先映入眼帘的是一个白色似皮球的东西,似圆非圆。好奇冬瓜落地一般。上半段还有一段湿湿的部分,显得尤为入目。这是什么情况?

紧接着现身一名中年男子。他身着纯白色T桖衫的,一灰色宽松的休闲西裤,腰围至少得三十好几。外加一双夏日必备皮制凉鞋。只见,他正低头看着手上的一张A4纸。透过一头黑色短发。满脸的赘肉横生。外加上那大腹便便快要把那T桖衫给撑爆的肚子。

看得我好生害怕,不由得咽了咽口水,生怕自己说错话。这宛如一颗肉粽呀。不在职场摸滚打拼8、9年,也不会有当前这景象。

什么是锁

面试官:: 你是来参加面试的吧?
吒吒辉: 不 不 不,我是来参加复试呢。

面试官:: 看到上次别人点评,MySQL优化还阔以。那你先谈谈对锁的理解?

吒吒辉: 嘿嘿,还好!

是计算机在进行多 进程、线程执行调度时强行限制资源访问的同步机制,用于在并发访问时保证数据的一致性、有效性;

锁是在执行多线程时,用于强行限制资源访问的同步机制,即用在并发控制中保证对互斥的要求。

一般的锁是建议锁(advisory lock),每个线程在访问对应资源前都需获取锁的信息,再根据信息决定是否可以访问。若访问对应信息,锁的状态会改变为锁定,因此其它线程此时不会来访问该资源,当资源结束后,会恢复锁的状态,允许其他线程的访问。

有些系统有强制锁(mandatory lock),若有未授权的线程想要访问锁定的数据,在访问时就会产生异常。

                          ---《维基百科》

锁的类型和应用原理

面试官:: 那一般数据库有哪些锁? 一般怎么使用?

此刻,用我那呆若木鸡的眼神看向面试官,内心实属尴尬+害怕,数据库不就是共享和互斥锁吗?
这样看来,是我太嫩。此处必有坑。殊不知此刻我内心已把你拿捏,定斩不饶。

吒吒辉: 数据库的锁根据不同划分方式有很多种说法,在业务访问上有以下两种:

  • 排他锁

在访问共享资源之前对其进行加锁,在访问完成后进行解锁操作。 加锁成功后,任何其它线程请求来获取锁都会被阻塞,直到当前线自行释放锁。

线程3状态:就绪、阻塞、执行

如解锁时,有一个以上的线程阻塞(资源已释放),那么所有尝试获取该锁的线程都被CPU认为就绪状态, 如果第一个就绪状态的线程又执行加锁操作,那么其他的线程又会进入就绪状态。 在这种方式下,只能有一个线程访问被互斥锁保护的资源

故此,MySQL的SQL语句加了互斥锁后,只有接受到请求并获取锁的线程才能够访问和修改数据。 因为互斥锁是针对线程访问控制而不是请求本身。

  • 共享锁

被加锁资源是可被共享的,但仅限于读请求。它的写请求只能被获取到锁的请求独占。 也就是加了共享锁的数据,只能够当前线程修改,其它线程只能读数据,并不能修改。

吒吒辉: 在 SQL 请求上可分为读、写锁。但本质还是对应对共享锁和排它锁。

面试官: 那 SQL 请求上不加锁怎么访问? 为啥说它们属于共享锁和排他锁? 这之间有何联系?

吒吒辉: 除加锁读外,还有一种不加锁读的情况。这种方式称为快照读,读请求加锁称为共享读。

针对请求加共享、排它锁的原因在于,读请求天生是幂等性的,不论你读多少次数据不会发生变化,所以给读请求加上锁就应该为共享锁。 不然怎么保证它的特点呢?
而写请求,本身就需对数据进行修改,所以就需要排它锁来保证数据修改的一致性。

吒吒辉: 如果按照锁的颗粒度划分看,就有表锁和行锁

  • 表锁:

是MySQL中最基本的锁策略,并且是开销最小的策略。并发处理较少。表锁由MySQL服务或存储引擎管理。多数情况由服务层管理,具体看SQL操作。

例如:服务器会为诸如 ALTER TABLE 之类的语句使用表锁
,而忽略存储引擎的锁。

加锁机制:

它会锁定整张表。一个用户在对表进行写操作(插人、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获取到读锁。

  • 行锁:

锁定当前访问行的数据,并发处理能力很强。但锁开销最大。具体视行数据多少决定。由innoDB存储引擎支持。

  • 页级锁:

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。由BDB 存储引擎管理页级锁。

面试官: 为啥是表锁开销小,而不是行锁呢? 毕竟表锁锁定是整张表

吒吒辉: 表锁锁定的是表没错,但它不是把表里面所有的数据行都上锁,相当于是封锁了表的入口,这样它只是需要判断每个请求是否可以获取到表的锁,没有就不锁定。
而行锁是针对表的每一行数据,数据量一多,锁定内容就多,故开销大。 但因它颗粒度小,锁定行不会影响到别的行。所以并发就高。而如果表锁在一个入口就卡死了,那整体请求处理肯定就会下降。

面试官: 我记得行锁里面有几种不同的实现方式,你知道吗?

您可真贴心啊,替我考虑这么多,大佬都是这么心比针细? 我要是说不知道,你老是不是又准备给出穿小鞋啦。强忍内心啃人的冲动

ps:读懂图,说明你有故事

吒吒辉: innodb虽支持行锁,但锁实现的算法却和SQL的查询形式有关系:

  • Record Lock(记录锁):单个行记录上的锁。也就是我们日常认为的行锁。由

`
where =
`
的形式触发

  • Gap Lock(间隙锁):间隙锁,锁定一个范围,但不包括记录本身(它锁住了某个范围内的多个行,包括根本不存在的数据)。

GAP锁的目的,是为了防止事务插入而导致幻读的情况。该锁只会在隔离级别是RR或者以上的级别内存在。间隙锁的目的是为了让其他事务无法在间隙中新增数据。 SQL里面用 where >、>=等范围条件触发,但会根据锁定的范围内,是否包含了表中真实存在的记录进行变化,如果存在真实记录就会进化为 临建锁。反之就为间隙所。

  • Next-Key Lock(临键锁):它是记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。next-key 锁是InnoDB默认的。是一个左开右闭的规则
  • IS锁:意向共享锁、Intention Shared Lock。当事务准备在某条记录上加S(读)锁时,需要先在表级别加一个IS锁。
  • IX锁:意向排它锁、Intention Exclusive Lock。当事务准备在某条记录上加X(写)锁时,需要先在表级别加一个IX锁。

面试官: 那这个东西是怎么实现的?

t(id PK, name KEY, sex, flag);

表中有四条记录:

1, zhazhahui, m, A

3, nezha, m, A

5, lisi, m, A

9, wangwu, f, B
  • 记录锁

select * from t where id=1 for update;
锁定 id =1的记录

  • 间隙锁

select * from t where id > 3 and id < 9 ;

锁定(3,5],(5,9)范围的值,因为当前访问3到9的范围记录,就需要锁定表里面已经存在的数据来解决幻读和不可重复读的问题

  • 临建锁

select * from t where id >=9 ;

会锁定 [9,+∞) 。查询会先选中 9 号记录,所以锁定范围就以9开始到正无穷数据。

面试官: 那意向排它、共享锁呢?是怎么个内容

吒吒辉: 意向排它锁和意向共享锁,是针对当前SQL请求访问数据行时,会提前进行申请访问,如果最终行锁未命中就会退化为该类型的表锁。

面试官: 那有这个意向排它锁有什么好处呢?

吒吒辉: 可提前做预判,每次尝试获取行锁之前会检查是否有表锁,如果存在就不会继续申请行锁,从而减少锁的开销。从而整个表就退化为表锁。

面试官: 那你动手给我演示下每个场景

嗯。。。(瞳孔放大2倍)我这不说的很明白吗?
难道故意和作对,这是干嘛啊。欺负人嘛不是
只见那面试官忽然翘起来二郎腿,还有节拍的抖动着腿,看向我。一看就是抖音整多了
哎,没办法 官大以及压死人。打碎了牙齿自己咽。你给我看细细看好了,最好眼睛都别眨

吒吒辉: 因为锁就是解决事务并发的问题,所以记录锁就不演示了,直接游荡在间隙和临建锁里面。

建立语句:

CREATE TABLE `t1` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `age` tinyint(3) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

表数据:

间隙锁:

  • 关闭 MySQL 默认的事务自动提交机制。

    • 关闭前:

  • 关闭后:

加锁:

直接插入 >8 的数据就阻塞,都会上锁。为的就解决插入新数据而导致幻读。

啊!幻读不知道呀。下篇文章给大家安排上】

面试官: 你这条件不是>=8吗? 那等于8呢? 被吃辣?

吒吒辉: 别着急嘛,这不还没说完吗。为什么不指定8呢?

因为 >=8 的条件会从间隙锁升级为临建锁,因为你条件里面包含了 8 这个真实存在的数据。所以会把它锁起来。如下:

所以,最终的行锁会和SQL语句的条件触发有关系,一旦范围查询包含了数据库里面真实存在数据,就会升级为临建锁。不要问我为什么? 看前面的定义

面试官独白:这小伙多少看来还有有点货,不错。此刻面试官露出一丝笑容。殊不知他内心又开酝酿起了新的想法。就等我入瓮

面试官: 那什么场景下行锁不会生效呢?锁 锁定的又是什么?

此刻,我呆了,这都什么跟什么啊。不带这么玩的吧。天杀的,净使坏

锁的触发机制

吒吒辉:
innodb的行锁是根据索引触发,如果没有相关的索引,那行锁将会退化成表锁(即锁定整个表里的行)。
锁定的是索引即索引树里面的数据库字段的值。

  • id为主键索引字段。

  • 给 age 字段上锁

  • age 字段没索引,退化成表锁。直接查询将失败。

有索引,用索引字段查询可得数据,其余字段查询将失败。因为获取不到行锁,只能等待。而锁定的是索引,故此其它用其它索引值查询能拿查询数据

  • 索引字段上锁

  • 索引当前字段锁定,用其余索引字段可查询

  • 不是索引字段都差不到。

面试官: 你前面说到的锁可以解决事务并发,然而MVCC也是用于解决并发,那干嘛还用锁来呢?你给说说

吒吒辉: 通过MVCC可以解决脏读、不可重复读、幻读这些读一致性问题,但实际上这只是解决了普通select语句的数据读取问题。
事务利用MVCC进行的读取操作称之为快照读,所有普通的SELECT语句在READ COMMITTED、REPEATABLE READ隔离级别下都算是快照读。

除了快照读之外,还有一种是锁定读,即在读取的时候给记录加锁,在锁定读的情况下依然要解决脏读、不可重复读、幻读的问题。

比如:如果 1 4 7 9 的数据。如果条件为 where > 4 的,那如果不锁定到 (4,7] (7,9],(9,+∞)。那势必就会早幻读,不可重复读的问题。

ps:不重复读?脏读是如何产生的?

死锁

面试官: 那你说下数据库的死锁是个什么情况?

吒吒辉: 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。

当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁。

一般可通过死锁检测和死锁超时机制来解决该问题。
死锁检查:
像InnoDB存储引擎,就能检测到死锁的循环依赖,并立即返回一个错误。否则死锁会导致出现非常慢的查询。通过参数 innodb_deadlock_detect 设置为on,来开启。

超时机制:
就是当查询的时间达到锁等待超时的设定后放弃锁请求。InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。

可通过配置参数 innodb_lock_wait_timeout 用来设置超时时间。如果有些用户使用哪种大事务,就设置锁超时时间大于事务执行时间
但这种情况下死锁超时检查的发现时间是无法接受的。

面试官: 那你说说InnoDB和MyisAM是如何发现死锁的?

吒吒辉:

  • innodb

数据库会把事务单元锁维持的锁和它所等待的锁都记录下来,Innodb提供了wait-for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要进入等待时,wait-for graph算法都会被触发。当数据库检测到两个事务不同方向地给同一个资源加锁(产生循序),它就认为发生了死锁,触发wait-for graph算法。

比如:事务1给A加锁,事务2给B加锁,同时事务1给B加锁(等待),事务2给A加锁就发生了死锁。那么死锁解决办法就是终止一边事务的执行即可,这种效率一般来说是最高的,也是主流数据库采用的办法。

Innodb目前处理死锁的方法就是将持有最少行级排他锁的事务进行回滚。这是相对比较简单的死锁回滚方式。死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。

对于事务型的系统,这是无法避免的,所以应用程序在设计必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。

  • MyisAM

MyisAM自身只支持表级锁,故加锁后一次性获取的。所以资源上不会出现多个事务之间互相需要对方释放锁之后再来进行处理。故不会有死锁

面试官: wait-for graph 算法怎么理解?

吒吒辉: 如下所示,四辆车就是死锁

它们相互等待对方的资源,而且形成环路!每辆车可看为一个节点,当节点1需要等待节点2的资源时,就生成一条有向边指向节点2,最后形成一个有向图。我们只要检测这个有向图是否出现环路即可,出现环路就是死锁!这就是wait-for graph算法。

Innodb将各个事务看为一个个节点,资源就是各个事务占用的锁,当事务1需要等待事务2的锁时,就生成一条有向边从1指向2,最后行成一个有向图。

面试官: 既然死锁无法避免,那如何减少发生呢?

吒吒辉:

  • 对应用程序进行调整/修改。某些情况下,你可以通过把大事务分解成多个小事务,使得锁能够更快被释放,从而极大程度地降低死锁发生的频率。在其他情况下,死锁的发生是因为两个事务采用不同的顺序操作了一个或多个表的相同的数据集。需要改成以相同顺序读写这些数据集,换言之,就是对这些数据集的访问采用串行化方式。这样在并发事务时,就让死锁变成了锁等待。
  • 修改表的schema,例如:删除外键约束来分离两张表,或者添加索引来减少扫描和锁定的行。
  • 如果发生了间隙锁,你可以把会话或者事务的事务隔离级别更改为RC(read committed)级别来避免,可以避免掉很多因为gap锁造成的死锁,但此时需要把binlog_format设置成row或者mixed格式。
  • 为表添加合理的索引,不走索引将会为表的每一行记录添加上锁(等同表锁),死锁的概率大大增大。
  • 为了在单个InnoDB 表上执行多个并发写入操作时避免死锁,可以在事务开始时通过为预期要修改的每个元祖(行)使用SELECT ... FOR UPDATE语句来获取必要的锁,即使这些行的更改语句是在之后才执行的。
  • 通过SELECT ... LOCK IN SHARE MODE获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。因进行获锁读取在修改

这时,只见对面所坐面试官,捋了捋那没有毛发的下巴,故作深思熟虑,像是在端详这什么。 难道 难道 是让我通过了吗?
此刻内心犹如小鹿乱撞,呐喊到我要干它二量。真的是不容易。 就在此时,他起身而立,那白色T桖衫包裹着那甩大肚子,犹如波浪上下翻滚。一看就是没少在酒桌上撸肉。

只见开口到,小伙子不错啊。

这是肯定我吗? 不容易啊,今天不开几把LOL,难消我心头之恨

面试官: 其实这数据库嘛 ,内容还是有很多的,你回去准备下,下一次的面试吧

。。。。什么个玩意儿,下次? 那就是这次不行啦, 这还没考够啊,下巴本来没毛,你捋个什么劲儿,整得个神神忽忽的。 此时内心犹如翻江倒海,猛龙过江。白鹤亮翅的冲动打他,奈何我这小身板子不行

吒吒辉: 那行吧,下次是多久啊,我这好多天都没整顿好的啦,你给我个准信呗。

我用那水汪汪可怜的小眼神望向他说到。他却很斯文的笑着,说道

面试官: 快了,小伙子别着急,我看好你的,加油

我加你那撸啊丝压榨花生油。 面个试,还嫌我脸上出油出的不多,都是被你挤出来的。只有强忍住内心的冲动。 哎 官大一级压死人啊
吒吒辉: 行吧,那我走啦
此刻,露出我那灰溜溜的背影,犹如鲁迅先生笔下的孔乙己
参考:
《高性能MySQL》
https://zhuanlan.zhihu.com/p/29150809
https://www.cnblogs.com/yulibostu/articles/9978618.html
如有帮助,欢迎点赞关注分享额,微信搜索【莲花童子哪吒】 获取体系化内容,加我纳入群聊,一起交流学习进步提升。
查看原文

赞 35 收藏 31 评论 3

Donne 赞了文章 · 6月10日

Golang 大杀器之性能剖析 PProf

原文地址:Golang 大杀器之性能剖析 PProf

前言

写了几吨代码,实现了几百个接口。功能测试也通过了,终于成功的部署上线了

结果,性能不佳,什么鬼?😭

想做性能分析

PProf

想要进行性能优化,首先瞩目在 Go 自身提供的工具链来作为分析依据,本文将带你学习、使用 Go 后花园,涉及如下:

  • runtime/pprof:采集程序(非 Server)的运行数据进行分析
  • net/http/pprof:采集 HTTP Server 的运行时数据进行分析

是什么

pprof 是用于可视化和分析性能分析数据的工具

pprof 以 profile.proto 读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)

profile.proto 是一个 Protocol Buffer v3 的描述文件,它描述了一组 callstack 和 symbolization 信息, 作用是表示统计分析的一组采样的调用栈,是很常见的 stacktrace 配置文件格式

支持什么使用模式

  • Report generation:报告生成
  • Interactive terminal use:交互式终端使用
  • Web interface:Web 界面

可以做什么

  • CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置
  • Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏
  • Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置
  • Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况

一个简单的例子

我们将编写一个简单且有点问题的例子,用于基本的程序初步分析

编写 demo 文件

(1)demo.go,文件内容:

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
    "github.com/EDDYCJY/go-pprof-example/data"
)

func main() {
    go func() {
        for {
            log.Println(data.Add("https://github.com/EDDYCJY"))
        }
    }()

    http.ListenAndServe("0.0.0.0:6060", nil)
}

(2)data/d.go,文件内容:

package data

var datas []string

func Add(str string) string {
    data := []byte(str)
    sData := string(data)
    datas = append(datas, sData)

    return sData
}

运行这个文件,你的 HTTP 服务会多出 /debug/pprof 的 endpoint 可用于观察应用程序的情况

分析

一、通过 Web 界面

查看当前总览:访问 http://127.0.0.1:6060/debug/pprof/

/debug/pprof/

profiles:
0    block
5    goroutine
3    heap
0    mutex
9    threadcreate

full goroutine stack dump

这个页面中有许多子页面,咱们继续深究下去,看看可以得到什么?

  • cpu(CPU Profiling): $HOST/debug/pprof/profile,默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件
  • block(Block Profiling):$HOST/debug/pprof/block,查看导致阻塞同步的堆栈跟踪
  • goroutine:$HOST/debug/pprof/goroutine,查看当前所有运行的 goroutines 堆栈跟踪
  • heap(Memory Profiling): $HOST/debug/pprof/heap,查看活动对象的内存分配情况
  • mutex(Mutex Profiling):$HOST/debug/pprof/mutex,查看导致互斥锁的竞争持有者的堆栈跟踪
  • threadcreate:$HOST/debug/pprof/threadcreate,查看创建新OS线程的堆栈跟踪

二、通过交互式终端使用

(1)go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60

$ go tool pprof http://localhost:6060/debug/pprof/profile\?seconds\=60

Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=60
Saved profile in /Users/eddycjy/pprof/pprof.samples.cpu.007.pb.gz
Type: cpu
Duration: 1mins, Total samples = 26.55s (44.15%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

执行该命令后,需等待 60 秒(可调整 seconds 的值),pprof 会进行 CPU Profiling。结束后将默认进入 pprof 的交互式命令模式,可以对分析的结果进行查看或导出。具体可执行 pprof help 查看命令说明

(pprof) top10
Showing nodes accounting for 25.92s, 97.63% of 26.55s total
Dropped 85 nodes (cum <= 0.13s)
Showing top 10 nodes out of 21
      flat  flat%   sum%        cum   cum%
    23.28s 87.68% 87.68%     23.29s 87.72%  syscall.Syscall
     0.77s  2.90% 90.58%      0.77s  2.90%  runtime.memmove
     0.58s  2.18% 92.77%      0.58s  2.18%  runtime.freedefer
     0.53s  2.00% 94.76%      1.42s  5.35%  runtime.scanobject
     0.36s  1.36% 96.12%      0.39s  1.47%  runtime.heapBitsForObject
     0.35s  1.32% 97.44%      0.45s  1.69%  runtime.greyobject
     0.02s 0.075% 97.51%     24.96s 94.01%  main.main.func1
     0.01s 0.038% 97.55%     23.91s 90.06%  os.(*File).Write
     0.01s 0.038% 97.59%      0.19s  0.72%  runtime.mallocgc
     0.01s 0.038% 97.63%     23.30s 87.76%  syscall.Write
  • flat:给定函数上运行耗时
  • flat%:同上的 CPU 运行耗时总比例
  • sum%:给定函数累积使用 CPU 总比例
  • cum:当前函数加上它之上的调用运行总耗时
  • cum%:同上的 CPU 运行耗时总比例

最后一列为函数名称,在大多数的情况下,我们可以通过这五列得出一个应用程序的运行情况,加以优化 🤔

(2)go tool pprof http://localhost:6060/debug/pprof/heap

$ go tool pprof http://localhost:6060/debug/pprof/heap
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /Users/eddycjy/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.008.pb.gz
Type: inuse_space
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 837.48MB, 100% of 837.48MB total
      flat  flat%   sum%        cum   cum%
  837.48MB   100%   100%   837.48MB   100%  main.main.func1
  • -inuse_space:分析应用程序的常驻内存占用情况
  • -alloc_objects:分析应用程序的内存临时分配情况

(3) go tool pprof http://localhost:6060/debug/pprof/block

(4) go tool pprof http://localhost:6060/debug/pprof/mutex

三、PProf 可视化界面

这是令人期待的一小节。在这之前,我们需要简单的编写好测试用例来跑一下

编写测试用例

(1)新建 data/d_test.go,文件内容:

package data

import "testing"

const url = "https://github.com/EDDYCJY"

func TestAdd(t *testing.T) {
    s := Add(url)
    if s == "" {
        t.Errorf("Test.Add error!")
    }
}

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(url)
    }
}

(2)执行测试用例

$ go test -bench=. -cpuprofile=cpu.prof
pkg: github.com/EDDYCJY/go-pprof-example/data
BenchmarkAdd-4       10000000           187 ns/op
PASS
ok      github.com/EDDYCJY/go-pprof-example/data    2.300s

-memprofile 也可以了解一下

启动 PProf 可视化界面
方法一:
$ go tool pprof -http=:8080 cpu.prof
方法二:
$ go tool pprof cpu.prof 
$ (pprof) web

如果出现 Could not execute dot; may need to install graphviz.,就是提示你要安装 graphviz 了 (请右拐谷歌)

查看 PProf 可视化界面

(1)Top

image

(2)Graph

image

框越大,线越粗代表它占用的时间越大哦

(3)Peek

image

(4)Source

image

通过 PProf 的可视化界面,我们能够更方便、更直观的看到 Go 应用程序的调用链、使用情况等,并且在 View 菜单栏中,还支持如上多种方式的切换

你想想,在烦恼不知道什么问题的时候,能用这些辅助工具来检测问题,是不是瞬间效率翻倍了呢 👌

四、PProf 火焰图

另一种可视化数据的方法是火焰图,需手动安装原生 PProf 工具:

(1) 安装 PProf

$ go get -u github.com/google/pprof

(2) 启动 PProf 可视化界面:

$ pprof -http=:8080 cpu.prof

(3) 查看 PProf 可视化界面

打开 PProf 的可视化界面时,你会明显发现比官方工具链的 PProf 精致一些,并且多了 Flame Graph(火焰图)

它就是本次的目标之一,它的最大优点是动态的。调用顺序由上到下(A -> B -> C -> D),每一块代表一个函数,越大代表占用 CPU 的时间更长。同时它也支持点击块深入进行分析!

image

总结

在本章节,粗略地介绍了 Go 的性能利器 PProf。在特定的场景中,PProf 给定位、剖析问题带了极大的帮助

希望本文对你有所帮助,另外建议能够自己实际操作一遍,最好是可以深入琢磨一下,内含大量的用法、知识点 🤓

思考题

你很优秀的看到了最后,那么有两道简单的思考题,希望拓展你的思路

(1)flat 一定大于 cum 吗,为什么?什么场景下 cum 会比 flat 大?

(2)本章节的 demo 代码,有什么性能问题?怎么解决它?

来,晒出你的想法!😆


原文地址:Golang 大杀器之性能剖析 PProf

查看原文

赞 100 收藏 63 评论 13

Donne 发布了文章 · 6月3日

xss无限弹窗

模拟一个无线弹窗的 xss 攻击请求。

以下为服务器端代码,接受 from 参数并通过 htmlspecialchars 进行过滤后输出到前端:

$from = htmlspecialchars($_GET['from']);
echo "<script>location.href='{$from}';</script>";

若此时前端访问的 url 为:

http://localhost:8080/?from=%27;alert(1)//

则最终输出到前端的内容为:

<script>location.href='';alert(1)//';</script>

分析此段输出,// 注释了后面的部分,alert 是同步 CPU 代码所以页面首先执行弹窗,然后执行 location.href='' 刷新当前页面。

最终的现象就是弹窗输出1,确定以后刷新当前页面,再次弹窗...,就造成了无限弹窗的 xss 攻击。

为什么过滤了还是无效?

因为 htmlspecialchars 是转换了双引号、尖括号和&符号,但是没有转义单引号,而且自己构造了一个脚本输出....

%27 是单引号的url编码,要了解url编码可以查看阮一峰的这篇文章http://www.ruanyifeng.com/blo...

查看原文

赞 0 收藏 0 评论 0

Donne 发布了文章 · 4月16日

gorm 返回 invalid connection

gorm 的 mysql 底层默认使用 go-sql-driver。

go-sql-driver 已经在 1.5.0 中解决了这个bug,但是 gorm 目前导入的版本还是 1.4.1,master 分支已经更新但是没有进入 tag 版本。所以只能我们手动的更新本地的 go-sql-driver 版本:

go get -u github.com/go-sql-driver/mysql
查看原文

赞 0 收藏 0 评论 2

Donne 发布了文章 · 4月10日

Mac 安装 Goland2020.1 无法启动

首先查看启动的错误日志,在 terminal 中执行以下命令:

`/Applications/GoLand.app/Contents/MacOS/goland`

显示信息如下:

➜  ~ /Applications/GoLand.app/Contents/MacOS/goland ; exit;
2020-04-10 10:29:57.895 goland[1523:31631] allVms required 1.8*,1.8+
2020-04-10 10:29:57.897 goland[1523:31634] Current Directory: /Users/Donne
2020-04-10 10:29:57.897 goland[1523:31634] Value of GOLAND_VM_OPTIONS is (null)
2020-04-10 10:29:57.897 goland[1523:31634] Processing VMOptions file at /Users/Donne/Library/Application Support/JetBrains/GoLand2020.1/goland.vmoptions
2020-04-10 10:29:57.897 goland[1523:31634] Done
Error: could not find libjava.dylib
Failed to GetJREPath()
OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.
Error opening zip file or JAR manifest missing : /Applications/GoLand.app/Contents/bin/jetbrains-agent.jar
Error occurred during initialization of VM
agent library failed to init: instrument

原因:因为之前用过盗版软件,所以 goland.vmoptions 最后一行添加过 jetbrains-agent.jar 路径。
解决方法:删除了 /Users/Donne/Library/Application Support/JetBrains/GoLand2020.1/goland.vmoptions 文件最后一行的jetbrains-agent.jar 路径。

查看原文

赞 4 收藏 1 评论 2

Donne 赞了文章 · 3月2日

ssh tunnel 转发 实现通过ECS跳板连接RDS

实现需求:

1、本机Mac电脑可以ssh登陆远程服务器A
2、远程服务器A可以连接服务器B的MYSQL
3、本机Mac不可以直接连接服务器B
4、实现目标:在本机Mac可以直接使用MYSQL工具连接和操作服务器B

命令:

ssh -CfNg -L <本地端口>:<目标机ip>:<目标机端口> <跳板机>

命令详解

-CfNg

    C:压缩数据

    f:后台用户验证,这个选项很有用,没shell的不可登陆账号也能使用

    N:不执行脚本或命令

    G:允许远程主机连接转发端口

-L:本地隧道转发
本地端口:本地要转发的端口
目标机ip:要转发到的主机
目标机端口:要转发到的端口
跳板机:作为跳板,且有访问目标机的机器SSH登录信息

命令示例1:

ssh -CfNg -L 13306:rm-uf6uc7g6.mysql.rds.aliyuncs.com:3306 root@49.116.8.196 -p59120

本地访问的端口为13306,转发到rm-uf6u73..cs.com:3306主机端口,跳板机ssh登录信息为root@49.116.8.196 跳板机ssh端口为59120

命令示例2:

ssh -CfNg -L 13306:rm-uf6uc7g6.mysql.rds.aliyuncs.com:3306 myserver

本地访问的端口为13306,转发到rm-uf6u73..cs.com:3306主机端口,跳板机ssh登录信息在.ssh/config下的myserver

使用方式

本地直接可以通过,127.0.0.1主机,13306端口连接MySQL

查看原文

赞 1 收藏 0 评论 0

Donne 回答了问题 · 2019-12-30

解决laravel 日志文件权限问题

Laravel 5.6.10 之后的版本支持对日志文件的权限配置,添加 config/logging.php 文件中的 permission 字段配置:

'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => 'debug',
        'days' => 7,
        'permission' => '0664',
    ],

dailysingle 的日志写入模式,都支持对 permission 字段的配置。关于这个问题更多的解决思路,可以查阅 stackoverflow的讨论

关注 4 回答 3

Donne 回答了问题 · 2019-12-27

解决关于php单例模式

如果你确定对象只会被使用一次,可以不用单例模式。

就像上面的回答说的,一次请求可能会进行多次数据库和日志记录操作,线上项目每次请求都会有很多次日志记录,如果不是单例模式结果可能是:

  1. 记录请求的参数,实例化一次
  2. 记录数据库查询结果,实例化一次
  3. 记录返回结果,实例化一次

这明显会造成不合理的浪费。

什么情况下不需要单例,model 层就是典型的例子,通常在一次请求中,实例化一次 model 对象就会一直使用,这时可以不用单例。

关注 4 回答 4

认证与成就

  • 获得 102 次点赞
  • 获得 28 枚徽章 获得 0 枚金徽章, 获得 7 枚银徽章, 获得 21 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-08-29
个人主页被 1.4k 人浏览