当前位置:首页 » 操作系统 » 心跳包算法

心跳包算法

发布时间: 2022-11-25 11:41:32

‘壹’ 无线数据终端是什么

无线数据终端也叫无线数传终端,即实现无线数据传输所使用的终端模块。

无线数据终端通常与下位机相连,实现无线数据传输的目的,有“工业领域的手机”的称号,因为其传输原理和我们平常使用的手机的数据传输时基本一致的。

其中比较典型的设备包括无线数传,无线路由器,无线Modem等设备,下面介绍的就是应用最广泛的DTU的相关参数作为参考。

为用户提供高速,稳定可靠,数据终端永远在线,多种协议转换的虚拟专用网络。针对网络流量控制的用户,产品支持语音,短信,数据触发上线以及超时自动断线的功能。同时也支持双数据中心备份,以及多数据中心同步接收数据等功能。

(1)心跳包算法扩展阅读:

无线数据终端的特点:

1、DNS自动获取:自动获取DNS,不再需要人工配置输入DNS,规避了因为选择的DNS服务器异常,导致DTU设备当机的严重现象!

2、完善的协议栈: 新系统加载了完善的TCP/IP协议栈,原来系统采用的是轻量级TCP/IP(LPIP)协议栈,协议栈是有裁剪的,新系统采用了完善的TCP/IP协议栈,网络通信性能优异。

3、支持多中心:客户数据可以同时往客户指定的监控中心发送,最大支持发送中心达256个。

参考资料来源:网络-无线数传终端

‘贰’ CAP协议以及算法

两阶段提交

Two-phase Commit(2PC):保证一个事务跨越多个节点时保持 ACID 特性;

两类节点:协调者(Coordinator)和参与者(Participants),协调者只有一个,参与者可以有多个。

过程:

需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

存在的问题

Paxos(Lamport):

分布式系统中的节点通信存在两种模型: 共享内存 (Shared memory)和 消息传递 (Messages passing)。

基于消息传递通信模型的分布式系统,不可避免的会发生以下错误:进程可能会慢、被杀死或者重启,消息可能会延迟、丢失、重复,在基础Paxos场景中,先不考虑可能出现消息篡改即 拜占庭错误 的情况。

Paxos算法解决的问题是在一个可能发生上述异常的 分布式系统 中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。

主要有三类节点:

过程:

规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。

下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。

当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。

如下图,Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,并且设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。

如果 Acceptor 接受到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2,那么就丢弃该提议请求;否则,发送提议响应,该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收到的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。

如下图,Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的提议请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2,因此就抛弃该提议请求;Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的提议请求,因为之前接收到的提议为 [n=2, v=8],并且 2 <= 4,因此就发送 [n=2, v=8] 的提议响应,设置当前接收到的提议为 [n=4, v=5],并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。

当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。

Proposer A 接受到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。

Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。

Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。

Raft(14年): 简化,更容易理解,也更容易实现。

引入主节点,通过竞选。

节点类型:Follower、Candidate 和 Leader

Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。

流程:
① 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。

② 此时 A 发送投票请求给其它所有节点。

③ 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。

④ 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。

① 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。

② 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。

① 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。

② Leader 会把修改复制到所有 Follower。

③ Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。

④ 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。

‘叁’ redis最大多少个节点问题

转自 https://blog.csdn.net/chenxuegui1234/article/details/100171599

现在redis集群架构,redis cluster用的会比较多。

如下图所示

对于客户端请求的key,根据公式HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上,然后Redis会去相应的节点进行操作!

那大家思考过,为什么有16384个槽么?

ps:CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?

其实我当初第一次思考这个问题的时候,我心里是这么想的,作者应该是觉得16384就够了,然后我就开始查这方面资料。

很幸运的是,这个问题,作者是给出了回答的!

地址如下: https://github.com/antirez/redis/issues/2576

作者原版回答如下:

The reason is:

So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.

因此,能看懂上面那段话的读者。这篇文章不用看了,因为作者讲的很清楚了。本文只是对上面那段话做一些解释而已。

我们回忆一下Redis Cluster的工作原理!

这里要先将节点握手讲清楚。我们让两个redis节点之间进行通信的时候,需要在客户端执行下面一个命令

<pre style="box-sizing: border-box; outline: 0px; margin: 0px 0px 24px; padding: 8px; font-weight: 400; position: relative; white-space: pre-wrap; overflow-wrap: break-word; overflow-x: auto; font-family: Consolas, Inconsolata, Courier, monospace; font-size: 14px; line-height: 22px; color: rgb(0, 0, 0); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">127.0.0.1:7000>cluster meet 127.0.0.1:7001
</pre>

如下图所示

意思很简单,让7000节点和7001节点知道彼此存在!

在握手成功后,两个节点之间会 定期 发送ping/pong消息,交换 数据信息 ,如下图所示。

在这里,我们需要关注三个重点。

到底在交换什么数据信息?

交换的数据信息,由消息体和消息头组成。

消息体无外乎是一些节点标识啊,IP啊,端口号啊,发送时间啊。这与本文关系不是太大,我不细说。

我们来看消息头,结构如下

注意看红框的内容,type表示消息类型。

另外,消息头里面有个myslots的char数组,长度为16383/8,这其实是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的。

到底数据信息究竟多大?

在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]。这块的大小是:

16384÷8÷1024=2kb

那在消息体中,会携带一定数量的其他节点信息用于交换。

那这个其他节点的信息,到底是几个节点的信息呢?

约为集群总节点数量的1/10,至少携带3个节点的信息。

这里的重点是: 节点数量越多,消息体内容越大。

消息体大小是10个节点的状态信息约1kb。

那定期的频率是什么样的?

redis集群内节点,每秒都在发ping消息。规律如下

因此,每秒单节点发出ping消息数量为

1+10*num(node.pong_received>cluster_node_timeout/2)

那大致带宽损耗如下所示,图片来自《Redis开发与运维》

讲完基础知识以后,我们可以来看作者的回答了。

(1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。

如上所述,在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]。

当槽位为65536时,这块的大小是:

65536÷8÷1024=8kb

因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。

(2)redis的集群主节点数量基本不可能超过1000个。

如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。

那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。

(3)槽位越小,节点少的情况下,压缩率高

Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。

如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。

ps:文件压缩率指的是,文件压缩前后的大小比。

综上所述,作者决定取16384个槽,不多不少,刚刚好!

‘肆’ 网络(五):socket



服务端socket要做五件事,客户端socket要做三件事:

接下来双方就可以通过 read() 和 write() 函数通信了,双方也都可以通过 close() 函数主动断开连接。


上面的例子中,我们预期的效果是客户端点击一次发送,给服务端发送两条数据,服务端触发两次“收到客户端数据的回调”,然后分别打印:

但实际上两条数据被合并成一条数据发送给服务端了,服务端只触发了一次“收到客户端数据的回调”,也只打印了一次:

这就是 数据粘包——多条数据被合并成了一条数据传输。

我们知道TCP有个 发送缓存 ,有些情况下TCP并不是有一条数据就发一条数据,而是等发送缓存满了,再把发送缓存里的多条数据一起发送出去,这就会导致数据粘包。

此外TCP还采用了 Nagle优化算法 来打包数据,它会将多次间隔较小且数据量较小的数据自动合并成一个比较大的数据一块儿传输,这也会导致数据粘包。

处理数据粘包也很简单,核心思路就是: 发送方在发送数据的时候先给每条数据都添加一个包头,包头里存放的关键信息就是真实数据的长度,当然也可以存放更多的业务信息,此外包头的尾部还需要拼接一个包头结束标识——回车换行符,以便将来接收方读取数据时可以根据这个包头结束标识优先读取到包头数据。接收方调用指定的读取方法优先读取到包头数据,然后根据包头里的长度信息再去精准读取指定长度的真实数据,这样就可以读取到一条完整的数据了,然后再读取下一条数据就不会粘包了。

正常来说,socket连接一旦建立之后就会一直挂在那里,直到某一端主动断开连接。但实际上,运营商在检测到链路上有一段时间无数据传输时,就会自动断开这种处于非活跃状态的连接,这就是所谓的运营商NAT超时,超时时间为5分钟。 因此我们就需要做心跳保活——即客户端每隔一定的时间间隔就向服务端发送一个心跳数据包,用来保证当前socket连接处于活跃状态,避免运营商把我们的连接中断,这个时间间隔我们取的是3分钟,服务器在收到心跳包时不当做真实数据处理即可。

客户端主动断开连接时(如App退出登录或者App进入后台等场景),我们不需要做断线重连;其它情况下如果连接断开了(如服务器出了问题或者网断了等场景),我们就需要做断线重连,来尽量使连接处于正常连接的状态,这样才能保证业务的正常运行。 具体做法就是,当客户端检测到跟服务端断开连接时就启动第一次断线重连,2秒后启动第二次断线重连,再隔4秒后启动第三次断线重连,如果三次断线重连还没成功,就认为是服务器出了问题,不再重连。

‘伍’ 为什么redis集群的最大槽数是16384个

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用 char 进行bitmap压缩后是2k( 2 * 8 (8 bit) * 1024(1k) = 16K ),也就是说使用2k的空间创建了16k的槽数。

虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k( 8 * 8 (8 bit) * 1024(1k) =65K ),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。

‘陆’ Raft 算法(详细版)

在分布式系统中,一致性算法至关重要。在所有一致性算法中,Paxos 最负盛名,它由莱斯利·兰伯特(Leslie Lamport)于 1990 年提出,是一种基于消息传递的一致性算法,被认为是类似算法中最有效的。

Paxos 算法虽然很有效,但复杂的原理使它实现起来非常困难,截止目前,实现 Paxos 算法的开源软件很少,比较出名的有 Chubby、LibPaxos。此外,Zookeeper 采用的 ZAB(Zookeeper Atomic Broadcast)协议也是基于 Paxos 算法实现的,不过 ZAB 对 Paxos 进行了很多改进与优化,两者的设计目标也存在差异——ZAB 协议主要用于构建一个高可用的分布式数据主备系统,而 Paxos 算法则是用于构建一个分布式的一致性状态机系统。

由于 Paxos 算法过于复杂、实现困难,极大地制约了其应用,而分布式系统领域又亟需一种高效而易于实现的分布式一致性算法,在此背景下,Raft 算法应运而生。

Raft 算法在斯坦福 Diego Ongaro 和 John Ousterhout 于 2013 年发表的《In Search of an Understandable Consensus Algorithm》中提出。相较于 Paxos,Raft 通过逻辑分离使其更容易理解和实现,目前,已经有十多种语言的 Raft 算法实现框架,较为出名的有 etcd、Consul 。

根据官方文档解释,一个 Raft 集群包含若干节点,Raft 把这些节点分为三种状态:Leader、 Follower、Candidate,每种状态负责的任务也是不一样的。正常情况下,集群中的节点只存在 Leader 与 Follower 两种状态。

Leader(领导者) :负责日志的同步管理,处理来自客户端的请求,与Follower保持heartBeat的联系;

Follower(追随者) :响应 Leader 的日志同步请求,响应Candidate的邀票请求,以及把客户端请求到Follower的事务转发(重定向)给Leader;

Candidate(候选者) :负责选举投票,集群刚启动或者Leader宕机时,状态为Follower的节点将转为Candidate并发起选举,选举胜出(获得超过半数节点的投票)后,从Candidate转为Leader状态。

通常,Raft 集群中只有一个 Leader,其它节点都是 Follower。Follower 都是被动的,不会发送任何请求,只是简单地响应来自 Leader 或者 Candidate 的请求。Leader 负责处理所有的客户端请求(如果一个客户端和 Follower 联系,那么 Follower 会把请求重定向给 Leader)。

为简化逻辑和实现,Raft 将一致性问题分解成了三个相对独立的子问题。

选举(Leader Election) :当 Leader 宕机或者集群初创时,一个新的 Leader 需要被选举出来;

日志复制(Log Replication) :Leader 接收来自客户端的请求并将其以日志条目的形式复制到集群中的其它节点,并且强制要求其它节点的日志和自己保持一致;

安全性(Safety) :如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其它服务器节点不能在同一个日志索引位置应用一个不同的指令。

根据 Raft 协议,一个应用 Raft 协议的集群在刚启动时,所有节点的状态都是 Follower。由于没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),因此,Followers 会认为 Leader 已经下线,进而转为 Candidate 状态。然后,Candidate 将向集群中其它节点请求投票,同意自己升级为 Leader。如果 Candidate 收到超过半数节点的投票(N/2 + 1),它将获胜成为 Leader。

第一阶段:所有节点都是 Follower。

上面提到,一个应用 Raft 协议的集群在刚启动(或 Leader 宕机)时,所有节点的状态都是 Follower,初始 Term(任期)为 0。同时启动选举定时器,每个节点的选举定时器超时时间都在 100~500 毫秒之间且并不一致(避免同时发起选举)。

第二阶段:Follower 转为 Candidate 并发起投票。

没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),节点启动后在一个选举定时器周期内未收到心跳和投票请求,则状态转为候选者 Candidate 状态,且 Term 自增,并向集群中所有节点发送投票请求并且重置选举定时器。

注意,由于每个节点的选举定时器超时时间都在 100-500 毫秒之间,且彼此不一样,以避免所有 Follower 同时转为 Candidate 并同时发起投票请求。换言之,最先转为 Candidate 并发起投票请求的节点将具有成为 Leader 的“先发优势”。

第三阶段:投票策略。

节点收到投票请求后会根据以下情况决定是否接受投票请求(每个 follower 刚成为 Candidate 的时候会将票投给自己):

请求节点的 Term 大于自己的 Term,且自己尚未投票给其它节点,则接受请求,把票投给它;

请求节点的 Term 小于自己的 Term,且自己尚未投票,则拒绝请求,将票投给自己。

第四阶段:Candidate 转为 Leader。

一轮选举过后,正常情况下,会有一个 Candidate 收到超过半数节点(N/2 + 1)的投票,它将胜出并升级为 Leader。然后定时发送心跳给其它的节点,其它节点会转为 Follower 并与 Leader 保持同步,到此,本轮选举结束。

注意:有可能一轮选举中,没有 Candidate 收到超过半数节点投票,那么将进行下一轮选举。

在一个 Raft 集群中,只有 Leader 节点能够处理客户端的请求(如果客户端的请求发到了 Follower,Follower 将会把请求重定向到 Leader) ,客户端的每一个请求都包含一条被复制状态机执行的指令。Leader 把这条指令作为一条新的日志条目(Entry)附加到日志中去,然后并行得将附加条目发送给 Followers,让它们复制这条日志条目。

当这条日志条目被 Followers 安全复制,Leader 会将这条日志条目应用到它的状态机中,然后把执行的结果返回给客户端。如果 Follower 崩溃或者运行缓慢,再或者网络丢包,Leader 会不断得重复尝试附加日志条目(尽管已经回复了客户端)直到所有的 Follower 都最终存储了所有的日志条目,确保强一致性。

第一阶段:客户端请求提交到 Leader。

如下图所示,Leader 收到客户端的请求,比如存储数据 5。Leader 在收到请求后,会将它作为日志条目(Entry)写入本地日志中。需要注意的是,此时该 Entry 的状态是未提交(Uncommitted),Leader 并不会更新本地数据,因此它是不可读的。

第二阶段:Leader 将 Entry 发送到其它 Follower

Leader 与 Followers 之间保持着心跳联系,随心跳 Leader 将追加的 Entry(AppendEntries)并行地发送给其它的 Follower,并让它们复制这条日志条目,这一过程称为复制(Replicate)。

有几点需要注意:

1. 为什么 Leader 向 Follower 发送的 Entry 是 AppendEntries 呢?

因为 Leader 与 Follower 的心跳是周期性的,而一个周期间 Leader 可能接收到多条客户端的请求,因此,随心跳向 Followers 发送的大概率是多个 Entry,即 AppendEntries。当然,在本例中,我们假设只有一条请求,自然也就是一个Entry了。

2. Leader 向 Followers 发送的不仅仅是追加的 Entry(AppendEntries)。

在发送追加日志条目的时候,Leader 会把新的日志条目紧接着之前条目的索引位置(prevLogIndex), Leader 任期号(Term)也包含在其中。如果 Follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么它就会拒绝接收新的日志条目,因为出现这种情况说明 Follower 和 Leader 不一致。

3. 如何解决 Leader 与 Follower 不一致的问题?

在正常情况下,Leader 和 Follower 的日志保持一致,所以追加日志的一致性检查从来不会失败。然而,Leader 和 Follower 一系列崩溃的情况会使它们的日志处于不一致状态。Follower可能会丢失一些在新的 Leader 中有的日志条目,它也可能拥有一些 Leader 没有的日志条目,或者两者都发生。丢失或者多出日志条目可能会持续多个任期。

要使 Follower 的日志与 Leader 恢复一致,Leader 必须找到最后两者达成一致的地方(说白了就是回溯,找到两者最近的一致点),然后删除从那个点之后的所有日志条目,发送自己的日志给 Follower。所有的这些操作都在进行附加日志的一致性检查时完成。

Leader 为每一个 Follower 维护一个 nextIndex,它表示下一个需要发送给 Follower 的日志条目的索引地址。当一个 Leader 刚获得权力的时候,它初始化所有的 nextIndex 值,为自己的最后一条日志的 index 加 1。如果一个 Follower 的日志和 Leader 不一致,那么在下一次附加日志时一致性检查就会失败。在被 Follower 拒绝之后,Leader 就会减小该 Follower 对应的 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得 Leader 和 Follower 的日志达成一致。当这种情况发生,附加日志就会成功,这时就会把 Follower 冲突的日志条目全部删除并且加上 Leader 的日志。一旦附加日志成功,那么 Follower 的日志就会和 Leader 保持一致,并且在接下来的任期继续保持一致。

第三阶段:Leader 等待 Followers 回应。

Followers 接收到 Leader 发来的复制请求后,有两种可能的回应:

写入本地日志中,返回 Success;

一致性检查失败,拒绝写入,返回 False,原因和解决办法上面已做了详细说明。

需要注意的是,此时该 Entry 的状态也是未提交(Uncommitted)。完成上述步骤后,Followers 会向 Leader 发出 Success 的回应,当 Leader 收到大多数 Followers 的回应后,会将第一阶段写入的 Entry 标记为提交状态(Committed),并把这条日志条目应用到它的状态机中。

第四阶段:Leader 回应客户端。

完成前三个阶段后,Leader会向客户端回应 OK,表示写操作成功。

第五阶段,Leader 通知 Followers Entry 已提交

Leader 回应客户端后,将随着下一个心跳通知 Followers,Followers 收到通知后也会将 Entry 标记为提交状态。至此,Raft 集群超过半数节点已经达到一致状态,可以确保强一致性。

需要注意的是,由于网络、性能、故障等各种原因导致“反应慢”、“不一致”等问题的节点,最终也会与 Leader 达成一致。

前面描述了 Raft 算法是如何选举 Leader 和复制日志的。然而,到目前为止描述的机制并不能充分地保证每一个状态机会按照相同的顺序执行相同的指令。例如,一个 Follower 可能处于不可用状态,同时 Leader 已经提交了若干的日志条目;然后这个 Follower 恢复(尚未与 Leader 达成一致)而 Leader 故障;如果该 Follower 被选举为 Leader 并且覆盖这些日志条目,就会出现问题,即不同的状态机执行不同的指令序列。

鉴于此,在 Leader 选举的时候需增加一些限制来完善 Raft 算法。这些限制可保证任何的 Leader 对于给定的任期号(Term),都拥有之前任期的所有被提交的日志条目(所谓 Leader 的完整特性)。关于这一选举时的限制,下文将详细说明。

在所有基于 Leader 机制的一致性算法中,Leader 都必须存储所有已经提交的日志条目。为了保障这一点,Raft 使用了一种简单而有效的方法,以保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的 Leader 中。换言之,日志条目的传送是单向的,只从 Leader 传给 Follower,并且 Leader 从不会覆盖自身本地日志中已经存在的条目。

Raft 使用投票的方式来阻止一个 Candidate 赢得选举,除非这个 Candidate 包含了所有已经提交的日志条目。Candidate 为了赢得选举必须联系集群中的大部分节点。这意味着每一个已经提交的日志条目肯定存在于至少一个服务器节点上。如果 Candidate 的日志至少和大多数的服务器节点一样新(这个新的定义会在下面讨论),那么它一定持有了所有已经提交的日志条目(多数派的思想)。投票请求的限制中请求中包含了 Candidate 的日志信息,然后投票人会拒绝那些日志没有自己新的投票请求。

Raft 通过比较两份日志中最后一条日志条目的索引值和任期号,确定谁的日志比较新。如果两份日志最后条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。

如同 4.1 节介绍的那样,Leader 知道一条当前任期内的日志记录是可以被提交的,只要它被复制到了大多数的 Follower 上(多数派的思想)。如果一个 Leader 在提交日志条目之前崩溃了,继任的 Leader 会继续尝试复制这条日志记录。然而,一个 Leader 并不能断定被保存到大多数 Follower 上的一个之前任期里的日志条目 就一定已经提交了。这很明显,从日志复制的过程可以看出。

鉴于上述情况,Raft 算法不会通过计算副本数目的方式去提交一个之前任期内的日志条目。只有 Leader 当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交。在某些情况下,Leader 可以安全地知道一个老的日志条目是否已经被提交(只需判断该条目是否存储到所有节点上),但是 Raft 为了简化问题使用了一种更加保守的方法。

当 Leader 复制之前任期里的日志时,Raft 会为所有日志保留原始的任期号,这在提交规则上产生了额外的复杂性。但是,这种策略更加容易辨别出日志,即使随着时间和日志的变化,日志仍维护着同一个任期编号。此外,该策略使得新 Leader 只需要发送较少日志条目。

raft 的读写都在 leader 节点中进行,它保证了读的都是最新的值,它是符合强一致性的(线性一致性),raft 除了这个还在【客户端交互】那块也做了一些保证,详情可以参考论文。但是 zookeeper 不同,zookeeper 写在 leader,读可以在 follower 进行,可能会读到了旧值,它不符合强一致性(只考虑写一致性,不考虑读一致性),但是 zookeeper 去 follower 读可以有效提升读取的效率。

对比于 zab、raft,我们发现他们选举、setData 都是需要过半机制才行,所以他们针对网络分区的处理方法都是一样的。

一个集群的节点经过网络分区后,如一共有 A、B、C、D、E 5个节点,如果 A 是 leader,网络分区为 A、B、C 和 D、E,在A、B、C分区还是能正常提供服务的,而在 D、E 分区因为不能得到大多数成员确认(虽然分区了,但是因为配置的原因他们还是能知道所有的成员数量,比如 zk 集群启动前需要配置所有成员地址,raft 也一样),是不能进行选举的,所以保证只会有一个 leader。

如果分区为 A、B 和 C、D、E ,A、B 分区虽然 A 还是 leader,但是却不能提供事务服务(setData),C、D、E 分区能重新选出 leader,还是能正常向外提供服务。

1)我们所说的日志(log)与状态机(state machine)不是一回事,日志指还没有提交到状态机中的数据。
2)新 leader 永远不会通过计算副本数量提交旧日志,他只能复制旧日志都其他 follower 上,对于旧日志的提交,只能是新 leader 接收新的写请求写新日志,顺带着把旧日志提交了。

‘柒’ RocketMQ(三)——系统架构

RocketMQ架构上主要分为四部分构成:

消息生产者,负责生产消息。Procer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟

RocketMQ中的消息生产者都是以生产者组(Procer Group)的形式出现的。生产者组是同一类生产者的集合,这类Procer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。

消息消费者,负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务处理

RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。消费者组是统一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息。消费者组使得在消息消费方法,实现负载均衡(讲一个Topic中不同的Queue平均分配给同一个Consumer Group的不同Consumer,并不是负载均衡)和容错(一个Consumer挂了,该Consumer Group中的其他Consumer可以接着消费元Consumer消费的Queue)的目标变得非常容易

消费者组中Consumer的数量应小于等于Topic的Queue数量。如果超出Queue数量,则多出的Consumer将不能消费消息。

不过一个Topic类型的消息可以被多个消费者组同时消费。

NameServer是一个Broker与Topic路由的注册中心,支持Broker的动态注册与发现。
RocketMQ的思想来自于Kafuka,而Kafka是以来了Zookeeper的。所以,在RocketMQ的早期版本也依赖Zookeeper。从3.0开始去掉了Zookeeper的依赖,使用了自己的NameServer。

NameServer通常也是以集群的方式部署,不过,NameServer是无状态的,即NameServer集群中的各个节点之间是无差异的,各个节点相互不进行信息通讯。那各个节点中的数据是如何进行数据同步的呢?在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,发起注册请求。在NameServer内部维护者一个Broker列表,用来动态存储Broker信息

Broker节点为了证明自己是活着的,为了维护与NameServer间的长连接,会将最新的信息以心跳包的方式上报给NameServer,每30秒发送一次心跳。心跳包中包含BrokerId、Broker地址(IP+Port)、Broker名称、Broker所属集群名称等等。NameServer在接收到心跳包后,会更新心跳时间戳,记录这个Broker的最新存活时间。

由于Broker关机、宕机或网络抖动等原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker列表中剔除
NameServer中有一个定时任务,每隔10秒就会扫描一次Broker表,查看每一个Broker的最新心跳时间戳距离当前时间是否超过120秒,如果超过,则会判定Broekr失效,然后将其从Broker列表中剔除。

RocketMQ的路由发现采用的是Pull模型。当Topic路由信息出现变化时,NameServer不会主动推送给客户端,而是客户端定时拉取最新的路由。默认每30秒拉取一次最新的路由

客户端再配置时必须要写上NameServer集群的地址,那么客户端道理连接在哪个NameServer节点呢?客户端首先会生产一个随机数,然后再与NameServer节点数取模,此时得到的就是要连接的节点索引,然后就会进行连接。如果连接失败,则会采用round-robin策略,逐个尝试去连接其他节点。
首先采用的是 随机策略 进行选择,失败后采用的是轮询策略。

Broker充当着消息中转角色,负责存储消息、转发消息。Broker在RocketMQ系统中负责接收并存储从生产者发送来的消息,同时为消费者的拉取请求作准备。Broker同时也存储着消息相关的元数据,包括消费者组、消费进度偏移offset、主题、队列等

Remoting Mole :整个Broker的实体,负责处理来自clients端的请求。而这个Broker实体则由以下模块构成。
Client Manager :客户端管理器。负责接收、解析客户端(Procer/Consumer)请求,管理客户端。
Store Service :存储服务。提供方便简单的API接口,处理消息存储到物理硬盘和消息查询功能。
HA Service :高可用服务,提供Master Broker和Slave Broker之间的数据同步功能。
Index Service :索引服务。根据特定的Message Key,对投递到Broker的消息进行索引服务,同时也提供根据Message Key对消息进行快速查询的功能

为了增强Broker性能与吞吐量,Broker一般都是以集群形式出现的。各集群节点中可能存放着相同Topic的不同Queue。
如果某Broker节点宕机,如何保证数据不丢失呢?
其解决方案是,将每个Broekr集群节点进行横向扩展,即将Broker节点再建为一个HA集群,解决单点问题。
Broker节点集群是一个主从集群,即有Master和Slave两种角色。Master负责处理读写操作请求,Slave负责对Master中的数据进行备份。当Master挂掉了,Slaver会自动切换为Master去工作。所以这个Broker集群式主备集群。Master与Slave的对应关系是通过指定相同的BrokerName、不同的BrokerId来确定的。BrokerId为0表示Master,非0表示Slave。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。

①启动NameServer,NameServer启动后开始监听端口,等待Broker、Procer、Consumer连接
②启动Broker时,Broker会与所有的NameServer保持长连接,每30秒向NameServer定时发送心跳包
③发送消息前,可以先创建Topic ,创建Topic时需要指定该Topic要存储在哪些Broker上,当然,在创建Topic时也会将Topic与Broker的关系写入到NameServer中。也可以在发送消息时自动创建Topic。
④Procer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息,即当前发送Topic的Queue与Broker地址的映射关系。然后根据算法策略从队选择一个Queue,与队列所在的Broker建立长连接从而向Broker发送消息。
⑤Consumer与Procer类似,跟其中一台NameServer建立长连接,获取其所订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其要消费的Queue,然后与Broker建立长连接,消费其中的消息。Consumer会向Broker发送心跳,以确保Broker的存活状态

手动创建Topic时,有两种模式:

自动创建Topic时,默认采用的是Broker模式,会为每个Broker默认创建四个Queue

从物理上讲,读/写队列是同一个队列。所以,不存在读/写队列数据同步问题。读/写队列是逻辑上进行区分的概念 。一般来说,读/写队列数量是相同的。

读/写队列数量不同是有问题的。
但这样可以方便缩容
perm用于设置对当前创建Topic的操作权限:2表示只写,4表示只读,6表示读写

‘捌’ 如何正确计算自己的减脂心率

1、初级公式:针对健康状况较差的人群。目标心率=(200-年龄)*(60%~80%)60%~70%主要用于减脂;70%~80%主要用于提高心肺功能

补充一点自我感觉自我感觉是掌握运动量和运动强度的重要指标,包括轻度呼吸急促、感到有点心跳、周身微热、面色微红、津津小汗,这表明运动适量;

如果有明显的心慌、气短、心口发热、头晕、大汗、疲惫不堪,表明运动超限;

如果你的运动始终保持在“面不改色心不跳”的程度,心率距“靶心率”相差太远,那就说明你的锻炼不可能达到增强体质和耐力的目的,还需要再加点量。

‘玖’ 一文吃透 WebSocket 原理

踩着年末的尾巴,提前布局来年,为来年的工作做个好的铺垫,所以就开始了面试历程,因为项目中使用到了 WebSocket ,面试官在深挖项目经验的时候,也难免提到 WebSocket 相关的知识点,因为之前并没有考虑这么深,所以,回答的还是有所欠缺,因此,赶紧趁热再熟悉熟悉,也借此机会,整理出来供大家咀嚼,每个项目都有其值得挖掘的闪光点,要用有爱的眼睛去发现。

WebSocket 是一种在单个TCP连接上进行全双工通信的协议。 WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。(维基网络)

WebSocket 本质上一种计算机网络应用层的协议,用来弥补 http 协议在持久通信能力上的不足。

WebSocket 协议在2008年诞生,2011年成为国际标准。现在最新版本浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

WebSocket 的其他特点包括:


我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

因为 HTTP 协议有一个缺陷:通信只能由客户端发起,不具备服务器推送能力。

举例来说,我们想了解查询今天的实时数据,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

在 WebSocket 协议出现以前,创建一个和服务端进双通道通信的 web 应用,需要依赖HTTP协议,进行不停的轮询,这会导致一些问题:

http 协议本身是没有持久通信能力的,但是我们在实际的应用中,是很需要这种能力的,所以,为了解决这些问题, WebSocket 协议由此而生,于2011年被IETF定为标准RFC6455,并被RFC7936所补充规范。并且在 HTML5 标准中增加了有关 WebSocket 协议的相关 api ,所以只要实现了 HTML5 标准的客户端,就可以与支持 WebSocket 协议的服务器进行全双工的持久通信了。

WebSocket 与 HTTP 的关系图:

下面一张图说明了 HTTP 与 WebSocket 的主要区别:

不同点:

与http协议一样, WebSocket 协议也需要通过已建立的TCP连接来传输数据。具体实现上是通过http协议建立通道,然后在此基础上用真正 WebSocket 协议进行通信,所以WebSocket协议和http协议是有一定的交叉关系的。首先, WebSocket 是一个持久化的协议,相对于 HTTP 这种非持久的协议来说。简单的举个例子吧,用目前应用比较广泛的 PHP 生命周期来解释。

HTTP 的生命周期通过 Request 来界定,也就是一个 Request 一个 Response ,那么在 HTTP1.0 中,这次 HTTP 请求就结束了。

在 HTTP1.1 中进行了改进,使得有一个 keep-alive,也就是说,在一个 HTTP 连接中,可以发送多个 Request,接收多个 Response。但是请记住 Request = Response, 在 HTTP 中永远是这样,也就是说一个 Request 只能有一个 Response。而且这个 Response 也是被动的,不能主动发起。首先 WebSocket 是基于 HTTP 协议的,或者说借用了 HTTP 协议来完成一部分握手。

首先我们来看个典型的 WebSocket 握手

熟悉 HTTP 的童鞋可能发现了,这段类似 HTTP 协议的握手请求中,多了这么几个东西。

这个就是 WebSocket 的核心了,告诉 Apache 、 Nginx 等服务器:注意啦,我发起的请求要用 WebSocket 协议,快点帮我找到对应的助理处理~而不是那个老土的 HTTP 。

这里开始就是 HTTP 最后负责的区域了,告诉客户,我已经成功切换协议啦~

依然是固定的,告诉客户端即将升级的是 WebSocket 协议,而不是 mozillasocket ,lurnarsocket 或者 shitsocket 。

然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。服务器:好啦好啦,知道啦,给你看我的 ID CARD 来证明行了吧。后面的, Sec-WebSocket-Protocol 则是表示最终使用的协议。至此,HTTP 已经完成它所有工作了,接下来就是完全按照 WebSocket 协议进行了。总结, WebSocket 连接的过程是:

优点:

缺点:

心跳就是客户端定时的给服务端发送消息,证明客户端是在线的, 如果超过一定的时间没有发送则就是离线了。

当客户端第一次发送请求至服务端时会携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果不存在就存入db或者缓存中, 第二次客户端定时再次发送请求依旧携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果存在就把上次的时间戳拿取出来,使用当前时间戳减去上次的时间, 得出的毫秒秒数判断是否大于指定的时间,若小于的话就是在线,否则就是离线;

通过查阅资料了解到 nginx 代理的 websocket 转发,无消息连接会出现超时断开问题。网上资料提到解决方案两种,一种是修改nginx配置信息,第二种是 websocket 发送心跳包。下面就来总结一下本次项目实践中解决的 websocket 的断线 和 重连 这两个问题的解决方案。主动触发包括主动断开连接,客户端主动发送消息给后端

主动断开连接,根据需要使用,基本很少用到。

下面主要讲一下客户端也就是前端如何实现心跳包:

首先了解一下心跳包机制

跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

在 TCP 的机制里面,本身是存在有心跳包的机制的,也就是 TCP 的选项: SO_KEEPALIVE 。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。

心跳包一般来说都是在逻辑层发送空的 echo 包来实现的。下一个定时器,在一定时间间隔下发送一个空包给客户端,然后客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定说掉线了。

在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在这个时候,就需要我们的心跳包了,用于维持长连接,保活。

心跳检测步骤:

针对这种异常的中断解决方案就是处理重连,下面我们给出的重连方案是使用js库处理:引入 reconnecting-websocket.min.js ,ws建立链接方法使用js库api方法:

断网监测支持使用js库: offline.min.js

以上方案,只是抛砖引玉,如果大家有更好的解决方案欢迎评论区分享交流。

WebSocket 是为了在 web 应用上进行双通道通信而产生的协议,相比于轮询HTTP请求的方式,WebSocket 有节省服务器资源,效率高等优点。WebSocket 中的掩码是为了防止早期版本中存在中间缓存污染攻击等问题而设置的,客户端向服务端发送数据需要掩码,服务端向客户端发送数据不需要掩码。WebSocket 中 Sec-WebSocket-Key 的生成算法是拼接服务端和客户端生成的字符串,进行SHA1哈希算法,再用base64编码。WebSocket 协议握手是依靠 HTTP 协议的,依靠于 HTTP 响应101进行协议升级转换。

‘拾’ Uber实时推送平台是如何打造的

原文:Uber’s Real-Time Push Platform

译者:LZM

Uber 建立的出行平台每天在处理全球数以百万计的打车订单。

实时打车市场是一个十分活跃的市场。一次行程包括多个参与者(乘客、司机),他们需要能在 APP 上实时查看、修改当前旅程的状态。因此,Uber 需要保证每个参与者和他们的 APP 实时同步相关信息,无论是接车时间、达到时间还是行驶路线和附近的司机。

今天,手机端呈现的功能日益丰富,而这些功能对实时信息同步的需求也逐渐增多。本文将介绍 Uber 工程团队如何把 Uber 平台信息同步机制从轮询转为基于 gRPC 的双向消息流协议。

在 Uber 后台,一个行程连接了现实世界中的乘客和司机。在行程过程中,这两个实体需要实时更新后台系统的信息。

我们思考一个场景:乘客发出打车请求,而司机在系统上等待接单。Uber 配对系统在后台自动匹配二者,向司机发送订单。到此为止,每一方(乘客、司机、后台)应该彼此同步他们的内容。

如果一个新订单带来,司机 APP 会每隔几秒轮询一次信息以及时更新订单状态。与此同时,乘客 APP 也会每隔几秒轮询一个信息来查看司机时候接单。

轮询的频率由数据改变的速率决定。对于一个大型 APP(例如 Uber APP),这个变化速率从几秒到几个小时不等,变化范围十分宽泛。

80% 的后台 API 请求都是来自客户端的轮询请求。激进的轮询策略能让 APP 的消息保持最新,但也会导致服务器资源耗尽。任何轮询过程中的 bug 都可能频繁导致后台负载显着加剧,甚至崩溃。随着需要动态实时数据的功能的增加,这个方法变得不再可行。

轮询会导致更快的电池消耗、应用程序延迟和网络级拥塞。这在城市中使用 2G/3G 网络或网络不稳定的地方尤其明显。在这些地方,应用程序每次尝试拉取信息时,都会重试多次。

随着功能增加,开发者们尝试重载轮询 API 或重建一个新的 API。在高峰期,APP 同时向多个 API 发送轮询请求。每个 API 负载数个功能。这些轮询 API 本质上成为一组分片负载 API。但是,在 API 级别上保持一致性和逻辑分离仍然是一个越来越大的挑战。

冷启动问题是其中最具挑战性的问题之一。每当 APP 启动,所有功能都希望从后台获取最新状态,以渲染用户界面。这导致多个 API 并发竞争,APP 不能成功渲染出正常界面,直到关键组件的消息被返回。在没有优先级的情况下,因为所有的 API 都有一些关键信息,所以应用加载时间会持续增加。糟糕的网络条件会进一步恶化冷启动问题。

很明显,我们需要一个彻头彻尾的、对消息同步机制的改变。我们开启了建立一个全新的实时推送平台的旅程。在这个平台上,服务器可以根据需要向应用程序发送数据。当我们采用这种新架构时,我们发现效率有显着的改进,同时也解决了不同的问题和挑战。

接下来,来看看我们对推送平台的几代改进以及该平台是如何演变的。

虽然使用消息推送是取代轮询的自然选择,但在如何构建推送机制上有很多需要考虑的问题。四个主要设计原则如下:

1)从轮询到推送的简单迁移

目前存在大量端设备在进行轮询。新系统必须利用现有的、分配给轮询 API 的负载和逻辑,而不是完全推倒重来。

2)简易开发

与开发轮询 API 相比,开发人员在推送数据方面不应该做截然不同的事情。

3)可靠性

所有消息应该通过网络可靠地发送到客户的 APP 上,并在发送失败时重试。

4)高效率

随着 Uber 在发展中国家的迅速发展,数据使用成本对我们的用户来说是一个挑战,对于每天要在 Uber 平台上呆上几个小时的司机来说尤其如此。新协议必须最小化服务器和移动应用程序之间的数据传输量。

我们将这个消息推送系统命名为 RAMEN (Realtime Asynchronous MEssaging Network,实时异步消息网络)。

任何时候,实时信息都在变化。消息的生命周期开始于决定生成一条信息的那一刻。微服务 Fireball 用于决定何时推送消息。很大部分决策都由配置文件决定。Fireball 在系统间监听多种类型的事件,并决定是否推送给该消息涉及的客户。

例如,当一个司机加单,司机和行程的状态都会改变,并触发 Fireball。之后,根据配置文件的内容,Fireball 决定何类消息应该推送给客户。通常,一个触发器会向多个用户发送多个消息。

任何事件都可能被触发器捕获,例如一些客户行为(如发出打车请求、打开 APP)、定时器到期、消息总线上的后端业务事件或是地理上的驶出 / 驶入事件。所有这些触发器都被过滤并转换为对各种后台 API 的调用。这些 API 需要客户的上下文信息,如设备定位、设备的操作系统以及 APP 的版本号,来生成一个响应。Fireball 获取设备上下文 RAMEN 服务器,并在调用 API 时将它们添加到头部。

所有来自 Uber APP 的服务器调用都由我们的 API 网关提供。推送有效负载以同样的方式生成。一旦 Fireball 决定了推送消息的对象和时间,API 网关就负责决定推送什么。网关会调用各类域服务来生成正确的推送负载。

网关中的所有 API 在如何生成有效负载方面是相似的。这些 API 分为拉取式和推送式两种。。拉取式 API 由移动设备调用来执行任何 HTTP 操作。推送 API 由 Fireball 调用,它有一个额外的 “推送” 中间件,可以拦截拉取式 API 的响应,并将其转发给推送消息系统。

将 API 网关介乎于二者之间有以下好处:

l  拉式和推式 API 共享端设备上的大部分业务逻辑。一个给定的负载可以从拉式 API 无缝切换到推式 API。例如,无论你的 APP 是通过拉式 API 调用拉出一个客户对象,还是 Fireball 通过推式 API 调用发送一个客户对象,他们都使用相同的逻辑。

l  网关负责处理大量业务逻辑,如推送消息的速率、路由和消息验证。

在适当的时候,Fireball 和网关一起生成发送给客户的推送消息。负责将这些信息传递到移动设备的是 “推送消息传递系统”。

每条消息推送会根据不同的配置执行,这些配置项包括:

1)优先级

由于为不同的用例生成了数百个不同的消息负载,因此需要对发送到 APP 的内容进行优先排序。我们将在下一节中看到,我们采用的协议限制在单个连接上发送多个并发负载。此外,接收设备的带宽是有限的。为了给人一种相对优先级的感觉,我们将信息大致分为三个不同的优先级:

l  高优先级:核心功能数据

l  中优先级:其他有助于提升客户体验的功能数据

l  低优先级:需要发送的数据规模大且使用频率不高

优先级配置用于管理平台的多种行为。例如,连接建立后,消息按照优先级降序排列在套接字(socket)中。在 RPC 失败的情况下,通过服务器端重试,高优先级消息变得更加可靠,并且支持跨区域复制。

2)存活时间

推送消息是为了改善实时体验。因此,每个消息都有一个预先定义的生存时间,从几秒到半个小时不等。消息传递系统将消息持久化并在发生错误时重试传递消息,直到有效值过期为止。

3)去重复

当通过触发器机制或重传机制多次生成相同的消息时,此配置项确定是否应该删除重复的消息推送。对于我们的大多数用例,发送给定类型的最新推送消息足以满足用户体验,这允许我们降低总体数据传输速率。

消息推送系统的最后一个组件是实际的有效负载交付服务。该服务维持着与世界各地数百万 APP 程序的活跃连接,并在它们到达时将有效信息同步。世界各地的移动网络提供了不同级别的可靠性,因此传输系统需要足够鲁棒以适应故障。我们的系统保证 “至少一次” 交货。

为了保证可靠传输,我们必须基于 TCP 协议,建立从应用程序到数据中心的持久连接。对于 2015 年的一个应用协议,我们的选择是使用带有长轮询、网络套接字或最终服务器发送事件 (SSE) 的 HTTP/1.1。

基于各种考虑,如安全性、移动 SDK 的支持和数据大小的影响,我们决定使用 SSE。Uber 已经支持了 HTTP + JSON API 栈,它的简单性和可操作性使它成为我们当时的选择。

然而,SSE 是一种单向协议,即数据只能从服务器发送到应用程序。为了提供之前提到的 “至少一次” 的保障,需要确认和重传机制以构建到应用程序协议之上的交付协议中。在 SSE 的基础上,我们定义了一个非常优雅和简单的协议方案。

客户端开始接收第一个 HTTP 请求的消息 /ramen/receive?seq=0,在任何新会话开始时序列号为 0。服务器以 HTTP 200 和 “Content-Type: text/event-stream” 响应客户端以维护 SSE 连接。接下来,服务器将按照优先级降序发送所有挂起的消息并依次递增序列号。由于底层传输协议是 TCP 协议,如果没有交付带有 seq#3 的消息,那么该连接应该已断开、超时或失败。

客户端期望在下一个看到的带有最大序列号重新连接 (在本例中 seq=2)。这就告诉了服务器,即使编号 3 写到了套接字上,它也没有被正常传递。然后服务器将重新发送相同的消息或以 seq=3 开始的任何更高优先级的消息。该协议构建了流连接所需的可恢复性,服务器负责大部分的存储工作,在客户端实现起来非常简单。

为了获知链接是否存活,服务器每 4 秒会发送一个心跳包,这类数据包大小只有一个比特。如果超过 7 秒没有收到来自服务器的消息或心跳,客户端会认定服务终端并重新发起链接。

在上面的协议中,每当客户端重新以一个更高的序列号发起连接时,它就充当服务器刷新旧消息的确认机制。在一个环境良好的网络中,用户可能会保持连接数分钟,从而导致服务器不断积累旧消息。为了缓解这个问题,应用程序会每 30 秒一次调用 /ramen/ack?seq=N,不管连接质量如何。协议的简单性允许用许多不同的语言和平台非常快速地编写客户端。

在设备上下文存储上,RAMEN 服务器在每次建立连接时存储设备上下文,并将此上下文暴露给 Fireball。每个设备上下文的 id 是用户及其设备参数对应的唯一哈希值。这允许隔离推送消息,即使用户在不同的设置下同时使用多个设备或应用程序。

第一代 RAMEN 服务器使用 Node.js 编写,并使用 Uber 内部的一致性哈西 / 分片框架 Ringpop。Ringpop 是一个去中心化的分片系统。所有连接都使用用户的 UUID 进行分片,并使用 Redis 作为持久性数据存储。

在接下来的一年半时间里,消息推送平台在整个公司得到了广泛的应用。高峰期时,RAMEN 系统通过维持高达 60 万个并发数据流连接,每秒向三种不同类型的应用程序推送超过 70000 个 QPS 消息。该系统很快成为服务器 - 客户端 API 基础结构中最重要的部分。

随着通信量和持久连接的快速增加,我们的技术选择也需要扩展。基于 Ringpop 的分布式分片是一个非常简单的架构,不会随着 ring 中的节点数量的增加而动态扩展。Ringpop 库使用一种 gossip 协议来评估成员资格。gossip 协议的收敛时间也随着环的大小增加而增加。

此外,Node.js 是单线程的,并且会有更高级别的事件循环延迟,从而进一步延迟成员信息的收敛。这些问题可能引发拓扑信息不一致,进而导致消息丢失、超时和错误。

2017 年初,我们决定重新启动 RAMEN 协议的服务器实现,以继续扩大应用规模。在这次迭代中,我们使用了以下技术:Netty、Apache Zookeeper、Apache Helix、Redis 和 Apache Cassandra。

1)Netty: Netty 是一个用于构建网络服务器和客户端的高性能库。Netty 的 bytebuf 允许零拷贝缓冲区,这使得系统非常高效。

2)Apache ZooKeeper: Apache ZooKeeper 对网络连接进行一致性哈希,可以直接传输数据,不需要任何存储层。但是与分散的拓扑管理不同,我们选择了 ZooKeeper 的集中共享。ZooKeeper 是一个非常强大的分布式同步和配置管理系统,可以快速检测连接节点的故障。

3)Apache Helix: Helix 是一个健壮的集群管理框架,运行在 ZooKeeper 之上,允许定义自定义拓扑和重新平衡算法。它还很好地从核心业务逻辑中抽象出拓扑逻辑。它使用 ZooKeeper 来监控已连接的工作者,并传播分片状态信息的变化。它还允许我们编写一个自定义的 Leader-Follower 拓扑和自定义的渐进再平衡算法。

4)Redis 和 Apache Cassandra: 当我们为多区域云架构做准备时,有必要对消息进行正确的复制和存储。Cassandra 是一个持久的跨区域复制存储。Redis 被用作 Cassandra 之上的容量缓存,以避免分片系统在部署或故障转移事件中常见的群发问题。

5)Streamgate: 这个服务在 Netty 上实现了 RAMEN 协议,并拥有所有与处理连接、消息和存储相关的逻辑。该服务还实现了一个 Apache Helix 参与者来建立与 ZooKeeper 的连接并维护心跳。

6)StreamgateFE (Streamgate Front End): 该服务充当 Apache Helix 的旁观者,从 ZooKeeper 上侦听拓扑变化。它实现了反向代理。来自客户机 (火球、网关或移动应用程序) 的每个请求都使用拓扑信息进行分片,并路由到正确的 Streamgate 工作程序。

7)Helix Controllers: 顾名思义,这是一个 5 节点的独立服务,单独负责运行 Apache Helix Controller 进程,是拓扑管理的大脑。无论何时任何 Streamgate 节点启动或停止,它都会检测到更改并重新分配分片分区。

在过去的几年中,我们一直在使用这种架构,并且实现了 99.99% 的服务器端可靠性。我们推动基础设施的使用持续增长,支持 iOS、Android 和 Web 平台上的十多种不同类型的应用程序。我们已经使用超过 1.5M 的并发连接来操作这个系统,并且每秒推送超过 250,000 条消息。

服务器端基础设施一直保持稳定运行。随着我们为更多新城市提供各种各样的网络服务和应用程序,我们的重点将是继续提高向移动设备消息推送机制的长尾可靠性。我们一直在试验新协议、开发新方法,以弥合和现实需求的差距。在检查以往的不足时,我们发现以下方面是导致可靠性下降的原因。

1)缺乏认证

RAMEN 协议在减少数据传输进行了优化,仅在每 30 秒或客户端重新连接时才发送确认消息。这将导致延迟确认,在某些情况下无法确认消息达到,因此很难区分是真正的消息丢失还是确认失败。

2)连接不稳定

维持客户端和服务器的正常连接至关重要。跨不同平台的客户端实现方式在处理错误、超时、后退或应用生命周期事件 (打开或关闭)、网络状态更改、主机名和数据中心故障转移等方面有许多细微差别。这导致了不同版本间的性能差异。

3)传输限制

由于该协议在 SSE 协议基础上实现,因此数据传输是单向的。但是,许多新的应用程序要求我们启用双向消息传输机制。没有实时的往返行程时间测量,确定网络状况、传输速度、缓解线路阻塞都是不可能的。SSE 也是一个基于文本的协议,它限制了我们传输二进制有效负载的能力,不需要使用像 base64 这样的文本编码,从而获得更大的有效负载。

2019 年底,我们开始开发下一代 RAMEN 协议以解决上述缺点。经过大量考量,我们选择在 gRPC 的基础上进行构建。gRPC 是一个被广泛采用的 RPC 栈,具有跨多种语言的客户端和服务器的标准化实现,对许多不同的 RPC 方法提供了一流的支持,并具有与 QUIC 传输层协议的互操作性。

新的、基于 gRPC 的 RAMEN 协议扩展了以前基于 SSE 的协议,有几个关键的区别:

l  确认消息立即通过反向流发送,提高了确认的可靠性,而数据传输量几乎没有增加。

l  实时确认机制允许我们测量 RTT,了解实时的网络状况。我们可以区分真正的消息损失和网络损失。

l  在协议之上提供了抽象层,以支持流多路传输等功能。它还允许我们试验应用级网络优先级和流控制算法,从而在数据使用和通信延迟方面带来更高的效率。

l  协议对消息有效负载进行抽象,以支持不同类型的序列化。将来,我们会探索其他序列化方法,但要将 gRPC 保留在传输层。

l  不同语言的客户端实现也让我们能够快速支持不同类型的应用程序和设备。

目前,这项开发工作处于 beta 版阶段,很快就能上线。

消息推送平台是 Uber 出行体验的组成部分之一。今天有数百种功能建立在该平台的基础服务之上。我们总结了消息推送平台在 Uber 出行生态中取得巨大成功的几个关键原因。

1)职能分离

消息触发、创建和传递系统之间明确的职责分离允许我们在业务需求发生变化时将注意力转移到平台的不同部分。通过将交付组件分离到 Apache Helix 中,数据流的拓扑逻辑和核心业务逻辑被很好的区分开,这允许在完全相同的架构上使用不同的有线协议支持 gRPC。

2)行业标准技术

构建在行业标准技术之上使我们的实现更加鲁棒且低成本。上述系统的维护开销非常小。我们能够以一个非常高效的团队规模来传递平台的价值。根据我们的经验,Helix 和 Zookeeper 非常稳定。

我们可以在不同的网络条件下扩展到数百万用户的规模,支持数百个功能和几十个应用程序。该协议的简单性使其易于扩展和快速迭代。

原文:

https://eng.uber.com/real-time-push-platform/

热点内容
fbe加密 发布:2025-05-18 01:16:34 浏览:250
求中点编程 发布:2025-05-18 01:03:14 浏览:840
安卓pay是什么 发布:2025-05-18 01:02:27 浏览:747
免费手游挂机脚本 发布:2025-05-18 00:55:43 浏览:354
sd卡手机存储系统存储 发布:2025-05-18 00:55:28 浏览:637
pythonlistintstr 发布:2025-05-18 00:48:18 浏览:604
轻应用缓存 发布:2025-05-18 00:31:02 浏览:252
鸟存储空气 发布:2025-05-18 00:20:24 浏览:201
linux刻录iso 发布:2025-05-18 00:16:15 浏览:663
php动态参数 发布:2025-05-18 00:12:05 浏览:425