❶ [redis 源码走读] sentinel 哨兵 - 节点发现流程
承接上一章 《 [redis 源码走读] sentinel 哨兵 - 原理 》,本章通过 strace 命令从底层抓取 sentinel 工作日志,熟悉节点通信流程,阅读相关源码。
节点之间通过 TCP 建立联系,下图展示了 sentinel A 节点与其它节点的关系。
通过 strace 命令查看 socket 的发送和接收数据日志内容,我们基本可以掌握 sentinel/master/slave 这三个角色是怎么联系起来的。
这样 sentinel 只需要配置 master 的信息,通过 INFO 命令和订阅频道 __sentinel__:hello 就能将集群中所有角色的节点紧密联系在一起。
根据 strace 日志参考上述对应连接关系图。
通过上述分析,我们基本了解了节点之间的通信流程时序,下面来分析一下源码。
sentinel 进程对 sentinel / master / slave 三个角色用数据结构 sentinelRedisInstance 进行管理。
sentinel 进程启动,加载配置,创建对应节点的管理实例 sentinelRedisInstance 。
定时器定期对其它节点进行监控管理。sentinel 利用 hiredis 作为 redis client,链接其它节点进行相互通信。
sentinel 异步重连其它节点。
sentinel 定期发送命令:PING / INFO / PUBLISH。每种命令发送的时间间隔不一样;不同场景下,同一个命令发送时间间隔可能会改变。
命令发送对象 :
sentinel 通过 master / slave 的 INFO 回复,主要下面几件事:
如果文章不错,给个点赞呗 ~ 谢谢。👍
❷ 阿里sentinel源码解析
sentinel是阿里巴巴开源的流量整形(限流、熔断)框架,目前在github拥有15k+的star,sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
我们以sentinel的主流程入手,分析sentinel是怎么搜集流量指标,完成流量整形的。
首先我们先看一个sentinel的简单使用demo,只需要调用SphU.entry获取到entry,然后在完成业务方法之后调用entry.exit即可。
SphU.entry会调用Env.sph.entry,将name和流量流向封装成StringResourceWrapper,然后继续调用entry处理。
进入CtSph的entry方法,最终来到entryWithPriority,调用InternalContextUtil.internalEnter初始化ThreadLocal的Context,然后调用lookProcessChain初始化责任链,最终调用chain.entry进入责任链进行处理。
InternalContextUtil.internalEnter会调用trueEnter方法,主要是生成DefaultNode到contextNameNodeMap,然后生成Context设置到contextHolder的过程。
lookProcessChain已经做过优化,支持spi加载自定义的责任链bulider,如果没有定义则使用默认的DefaultSlotChainBuilder进行加载。默认加载的slot和顺序可见镇楼图,不再细说。
最后来到重头戏chain.entry进入责任链进行处理,下面会按照顺序分别对每个处理器进行分析。
首先来到NodeSelectorSlot,主要是获取到name对应的DefaultNode并缓存起来,设置为context的当前节点,然后通知下一个节点。
下一个节点是ClusterBuilderSlot,继续对DefaultNode设置ClusterNode与OriginNode,然后通知下一节点。
下一个节点是LogSlot,只是单纯的打印日志,不再细说。
下一个节点是StatisticSlot,是一个后置节点,先通知下一个节点处理完后,
1.如果没有报错,则对node、clusterNode、originNode、ENTRY_NODE的线程数、通过请求数进行增加。
2.如果报错是PriorityWaitException,则只对线程数进行增加。
3.如果报错是BlockException,设置报错到node,然后对阻挡请求数进行增加。
4.如果是其他报错,设置报错到node即可。
下一个节点是FlowSlot,这个节点就是重要的限流处理节点,进入此节点是调用checker.checkFlow进行限流处理。
来到FlowRuleChecker的checkFlow方法,调用ruleProvider.apply获取到资源对应的FlowRule列表,然后遍历FlowRule调用canPassCheck校验限流规则。
canPassCheck会根据rule的限流模式,选择集群限流或者本地限流,这里分别作出分析。
passLocalCheck是本地限流的入口,首先会调用选出限流的node,然后调用canPass进行校验。
会根据以下规则选中node。
1.strategy是STRATEGY_DIRECT。
1.1.limitApp不是other和default,并且等于orgin时,选择originNode。
1.2.limitApp是other,选择originNode。
1.3.limitApp是default,选择clusterNode。
2.strategy是STRATEGY_RELATE,选择clusterNode。
3.strategy是STRATEGY_CHAIN,选择node。
选择好对应的node后就是调用canPass校验限流规则,目前sentinel有三种本地限流规则:普通限流、匀速限流、冷启动限流。
普通限流的实现是DefaultController,就是统计当前的线程数或者qps加上需要通过的数量有没有大于限定值,小于等于则直接通过,否则阻挡。
匀速限流的实现是RateLimiterController,使用了AtomicLong保证了latestPassedTime的原子增长,因此停顿的时间是根据latestPassedTime-currentTime计算出来,得到一个匀速的睡眠时间。
冷启动限流的实现是WarmUpController,是sentinel中最难懂的限流方式,其实不太需要关注这些复杂公式的计算,也可以得出冷启动的限流思路:
1.当qps已经达到温热状态时,按照正常的添加令牌消耗令牌即可。
2.当qps处于过冷状态时,会添加令牌使得算法继续降温。
3.当qps逐渐回升,大于过冷的边界qps值时,不再添加令牌,慢慢消耗令牌使得逐渐增大单位时间可通过的请求数,让算法继续回温。
总结出一点,可通过的请求数跟令牌桶剩余令牌数量成反比,以达到冷启动的作用。
接下来是集群限流,passClusterCheck是集群限流的入口,会根据flowId调用clusterSerivce获取指定数量的token,然后根据其结果判断是否通过、睡眠、降级到本地限流、阻挡。
接下来看一下ClusterService的处理,会根据ruleId获取到对应的FlowRule,然后调用ClusterFlowChecker.acquireClusterToken获取结果返回。ClusterFlowChecker.acquireClusterToken的处理方式跟普通限流是一样的,只是会将集群的请求都集中在一个service中处理,来达到集群限流的效果,不再细说。
FlowSlot的下一个节点是DegradeSlot,是熔断处理器,进入时会调用performChecking,进而获取到CircuitBreaker列表,然后调用其tryPass校验是否熔断。
来到AbstractCircuitBreaker的tryPass方法,主要是判断熔断器状态,如果是close直接放行,如果是open则会校验是否到达开启halfopen的时间,如果成功将状态cas成halfopen则继续放行,其他情况都是阻拦。
那怎么将熔断器的状态从close变成open呢?怎么将halfopen变成close或者open呢?sentinel由两种熔断器:错误数熔断器ExceptionCircuitBreaker、响应时间熔断器ResponseTimeCircuitBreaker,都分析一遍。
当业务方法报错时会调用Tracer.traceEntry将报错设置到entry上。
当调用entry.exit时,会随着责任链来到DegradeSlot的exit方法,会遍历熔断器列表调用其onRequestComplete方法。
ExceptionCircuitBreaker的onRequestComplete会记录错误数和总请求数,然后调用继续处理。
1.当前状态是open时,不应该由熔断器底层去转换状态,直接退出。
2.当前状态是halfopen时,如果没有报错,则将halfopen变成close,否则将halfopen变成open。
3.当前状态时close时,则根据是否总请求达到了最低请求数,如果达到了话再比较错误数/错误比例是否大于限定值,如果大于则直接转换成open。
ExceptionCircuitBreaker的onRequestComplete会记录慢响应数和总请求数,然后调用继续处理。
1.当前状态是open时,不应该由熔断器底层去转换状态,直接退出。
2.当前状态是halfopen时,如果当前响应时间小于限定值,则将halfopen变成close,否则将halfopen变成open。
3.当前状态时close时,则根据是否总请求达到了最低请求数,如果达到了话再比较慢请求数/慢请求比例是否大于限定值,如果大于则直接转换成open。
下一个节点是AuthoritySlot,权限控制器,这个控制器就是看当前origin是否被允许进入请求,不允许则报错,不再细说。
终于来到最后一个节点SystemSlot了,此节点是自适应处理器,主要是根据系统自身负载(qps、最大线程数、最高响应时间、cpu使用率、系统bbr)来判断请求是否能够通过,保证系统处于一个能稳定处理请求的安全状态。
尤其值得一提的是bbr算法,作者参考了tcp bbr的设计,通过最大的qps和最小的响应时间动态计算出可进入的线程数,而不是一个粗暴的固定可进入的线程数,为什么能通过这两个值就能计算出可进入的线程数?可以网上搜索一下tcp bbr算法的解析,十分巧妙,不再细说。
❸ Python中的10条冷门知识
下面时Python中一些不常见的冷门知识,感兴趣的小伙伴不妨来学习一下。
1、省略号也是对象
… 这是省略号,在Python中,一切皆对象。它也不例外。在 Python 中,它叫做 Ellipsis 。在 Python 3 中你可以直接写…来得到这玩意。
>>> ...
Ellipsis
>>> type(...)
<class 'ellipsis'>
而在 Python2 中没有…这个语法,只能直接写Ellipsis来获取。
>>> Ellipsis
Ellipsis
>>> type(Ellipsis)
<type 'ellipsis'>
它转为布尔值时为真
>>> bool(...)
True
最后,这东西是一个单例。
>>> id(...)
4362672336
>>> id(...)
4362672336
这东西有啥用呢?据说它是Numpy的语法糖,不玩 Numpy 的人,可以说是没啥用的。
在网上只看到这个 用 … 代替 pass ,稍微有点用,但又不是必须使用的。
try:
1/0
except ZeroDivisionError:
...
2、增量赋值的性能更好
诸如 += 和 *= 这些运算符,叫做 增量赋值运算符。这里使用用 += 举例,以下两种写法,在效果上是等价的。
# 第一种
a = 1 ; a += 1
# 第二种
a = 1; a = a + 1
+= 其背后使用的魔法方法是 iadd,如果没有实现这个方法则会退而求其次,使用 add 。
这两种写法有什么区别呢?
用列表举例 a += b,使用 add 的话就像是使用了a.extend(b),如果使用 add 的话,则是 a = a+b,前者是直接在原列表上进行扩展,而后者是先从原列表中取出值,在一个新的列表中进行扩展,然后再将新的列表对象返回给变量,显然后者的消耗要大些。
所以在能使用增量赋值的时候尽量使用它。
3、and 和or 的取值顺序
and 和 or 是我们再熟悉不过的两个逻辑运算符。而我们通常只用它来做判断,很少用它来取值。
如果一个or表达式中所有值都为真,Python会选择第一个值,而and表达式则会选择第二个。
>>>(2 or 3) * (5 and 7)
14 # 2*7
4、修改解释器提示符
>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>>
>>> sys.ps2 = '---------------- '
>>> sys.ps1 = 'Python编程时光>>>'
Python编程时光>>>for i in range(2):
---------------- print (i)
----------------
5、默认参数最好不为可变对象
函数的参数分三种
可变参数
默认参数
关键字参数
今天要说的是,传递默认参数时,新手很容易踩雷的一个坑。
先来看一个示例:
def func(item, item_list=[]):
item_list.append(item)
print(item_list)
func('iphone')
func('xiaomi', item_list=['oppo','vivo'])
func('huawei')
在这里,你可以暂停一下,思考一下会输出什么?
思考过后,你的答案是否和下面的一致呢
['iphone']
['oppo', 'vivo', 'xiaomi']
['iphone', 'huawei']
如果是,那你可以跳过这部分内容,如果不是,请接着往下看,这里来分析一下。
Python 中的 def 语句在每次执行的时候都初始化一个函数对象,这个函数对象就是我们要调用的函数,可以把它当成一个一般的对象,只不过这个对象拥有一个可执行的方法和部分属性。
对于参数中提供了初始值的参数,由于 Python 中的函数参数传递的是对象,也可以认为是传地址,在第一次初始化 def 的时候,会先生成这个可变对象的内存地址,然后将这个默认参数 item_list 会与这个内存地址绑定。在后面的函数调用中,如果调用方指定了新的默认值,就会将原来的默认值覆盖。如果调用方没有指定新的默认值,那就会使用原来的默认值。
在这里插入图片描述
6、访问类中的私有方法
大家都知道,类中可供直接调用的方法,只有公有方法(protected类型的方法也可以,但是不建议)。也就是说,类的私有方法是无法直接调用的。
这里先看一下例子
class Kls():
def public(self):
print('Hello public world!')
def __private(self):
print('Hello private world!')
def call_private(self):
self.__private()
ins = Kls()
# 调用公有方法,没问题
ins.public()
# 直接调用私有方法,不行
ins.__private()
# 但你可以通过内部公有方法,进行代理
ins.call_private()
既然都是方法,那我们真的没有方法可以直接调用吗?
当然有啦,只是建议你千万不要这样弄,这里只是普及,让你了解一下。
# 调用私有方法,以下两种等价
ins._Kls__private()
ins.call_private()
7、时有时无的切片异常
这是个简单例子
my_list = [1, 2, 3, 4, 5]
print(my_list[5])
Traceback (most recent call last):
File "F:/Python Script/test.py", line 2, in <mole>
print(my_list[5])
IndexError: list index out of range
来看看,如下这种写法就不会报索引异常,执行my_list[5:],会返回一个新list:[]。
my_list = [1, 2, 3]
print(my_list[5:])
8、for 死循环
for 循环可以说是 基础得不能再基础的知识点了。但是如果让你用 for 写一个死循环,你会写吗?(问题来自群友 陈**)
这是个开放性的问题,在往下看之前,建议你先尝试自己思考,你会如何解答。
好了,如果你还没有思路,那就来看一下 一个海外 MIT 群友的回答:
for i in iter(int, 1):pass
是不是懵逼了。iter 还有这种用法?这为啥是个死循环?
这真的是个冷知识,关于这个知识点,你如果看中文网站,可能找不到相关资料。
还好你可以通过 IDE 看py源码里的注释内容,介绍了很详细的使用方法。
原来iter有两种使用方法,通常我们的认知是第一种,将一个列表转化为一个迭代器。
而第二种方法,他接收一个 callable对象,和一个sentinel 参数。第一个对象会一直运行,直到它返回 sentinel 值才结束。
在这里插入图片描述
那int 呢,这又是一个知识点,int 是一个内建方法。通过看注释,可以看出它是有默认值0的。你可以在终端上输入 int() 看看是不是返回0。
在这里插入图片描述
由于int() 永远返回0,永远返回不了1,所以这个 for 循环会没有终点。一直运行下去。
9、奇怪的字符串
字符串类型作为 Python 中最常用的数据类型之一,Python解释器为了提高字符串使用的效率和使用性能,做了很多优化。
例如:Python 解释器中使用了 intern(字符串驻留)的技术来提高字符串效率。
什么是 intern 机制?就是同样的字符串对象仅仅会保存一份,放在一个字符串储蓄池中,是共用的,当然,肯定不能改变,这也决定了字符串必须是不可变对象。
示例一
# Python2.7
>>> a = "Hello_Python"
>>> id(a)
32045616
>>> id("Hello" + "_" + "Python")
32045616
# Python3.7
>>> a = "Hello_Python"
>>> id(a)
38764272
>>> id("Hello" + "_" + "Python")
32045616
示例二
>>> a = "MING"
>>> b = "MING"
>>> a is b
True
# Python2.7
>>> a, b = "MING!", "MING!"
>>> a is b
True
# Python3.7
>>> a, b = "MING!", "MING!"
>>> a is b
False
示例三
# Python2.7
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False
# Python3.7
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
True
示例四
>>> s1="hello"
>>> s2="hello"
>>> s1 is s2
True
# 如果有空格,默认不启用intern机制
>>> s1="hell o"
>>> s2="hell o"
>>> s1 is s2
False
# 如果一个字符串长度超过20个字符,不启动intern机制
>>> s1 = "a" * 20
>>> s2 = "a" * 20
>>> s1 is s2
True
>>> s1 = "a" * 21
>>> s2 = "a" * 21
>>> s1 is s2
False
>>> s1 = "ab" * 10
>>> s2 = "ab" * 10
>>> s1 is s2
True
>>> s1 = "ab" * 11
>>> s2 = "ab" * 11
>>> s1 is s2
False
10、两次return
我们都知道,try…finally… 语句的用法,不管 try 里面是正常执行还是报异常,最终都能保证finally能够执行。
同时,我们又知道,一个函数里只要遇到 return 函数就会立马结束。
基于以上这两点,我们来看看这个例子,到底运行过程是怎么样的?
>>> def func():
... try:
... return 'try'
... finally:
... return 'finally'
...
>>> func()
'finally'
惊奇的发现,在try里的return居然不起作用。
原因是,在try…finally…语句中,try中的return会被直接忽视,因为要保证finally能够执行。