go语言实现raft协议的简单介绍

国内重要的 Go 语言项目:TiDB 3.0 GA,稳定性和性能大幅提升

TiDB 是 PingCAP 自主研发的开源分布式关系型数据库,具备商业级数据库的数据可靠性,可用性,安全性等特性,支持在线弹性水平扩展,兼容 MySQL 协议及生态,创新性实现 OLTP 及 OLAP 融合。

创新互联基于分布式IDC数据中心构建的平台为众多户提供成都服务器托管 四川大带宽租用 成都机柜租用 成都服务器租用。

TiDB 3.0 版本显著提升了大规模集群的稳定性,集群支持 150+ 存储节点,300+TB 存储容量长期稳定运行。易用性方面引入大量降低用户运维成本的优化,包括引入 Information_Schema 中的多个实用系统视图、EXPLAIN ANALYZE、SQL Trace 等。在性能方面,特别是 OLTP 性能方面,3.0 比 2.1 也有大幅提升,其中 TPC-C 性能提升约 4.5 倍,Sysbench 性能提升约 1.5 倍,OLAP 方面,TPC-H 50G Q15 因实现 View 可以执行,至此 TPC-H 22 个 Query 均可正常运行。新功能方面增加了窗口函数、视图(实验特性)、分区表、插件系统、悲观锁(实验特性)。

截止本文发稿时 TiDB 已在 500+ 用户的生产环境中长期稳定运行,涵盖金融、保险、制造,互联网, 游戏 等领域,涉及交易、数据中台、 历史 库等多个业务场景。不同业务场景对关系型数据库的诉求可用 “百花齐放”来形容,但对关系数据库最根本的诉求未发生任何变化,如数据可靠性,系统稳定性,可扩展性,安全性,易用性等。请跟随我们的脚步梳理 TiDB 3.0 有什么样的惊喜。

3.0 与 2.1 版本相比,显著提升了大规模集群的稳定性,支持单集群 150+ 存储节点,300+TB 存储容量长期稳定运行,主要的优化点如下:

1. 优化 Raft 副本之间的心跳机制,按照 Region 的活跃程度调整心跳频率,减小冷数据对集群的负担。

2. 热点调度策略支持更多参数配置,采用更高优先级,并提升热点调度的准确性。

3. 优化 PD 调度流程,提供调度限流机制,提升系统稳定性。

4. 新增分布式 GC 功能,提升 GC 的性能,降低大集群 GC 时间,提升系统稳定性。

众所周知,数据库查询计划的稳定性对业务至关重要,TiDB 3.0 版本采用多种优化手段提升查询计划的稳定性,如下:

1. 新增 Fast Analyze 功能,提升收集统计信息的速度,降低集群资源的消耗及对业务的影响。

2. 新增 Incremental Analyze 功能,提升收集单调递增的索引统计信息的速度,降低集群资源的消耗及对业务的影响。

3. 在 CM-Sketch 中新增 TopN 的统计信息,缓解 CM-Sketch 哈希冲突导致估算偏大,提升代价估算的准确性,提升查询计划的稳定性。

4. 引入 Skyline Pruning 框架,利用规则防止查询计划过度依赖统计信息,缓解因统计信息滞后导致选择的查询计划不是最优的情况,提升查询计划的稳定性。

5. 新增 SQL Plan Management 功能,支持在查询计划不准确时手动绑定查询计划,提升查询计划的稳定性。

1. OLTP

3.0 与 2.1 版本相比 Sysbench 的 Point Select,Update Index,Update Non-Index 均提升约 1.5 倍,TPC-C 性能提升约 4.5 倍。主要的优化点如下:

1. TiDB 持续优化 SQL 执行器,包括:优化 NOT EXISTS 子查询转化为 Anti Semi Join,优化多表 Join 时 Join 顺序选择等。

2. 优化 Index Join 逻辑,扩大 Index Join 算子的适用场景并提升代价估算的准确性。

3. TiKV 批量接收和发送消息功能,提升写入密集的场景的 TPS 约 7%,读密集的场景提升约 30%。

4. TiKV 优化内存管理,减少 Iterator Key Bound Option 的内存分配和拷贝,多个 Column Families 共享 block cache 提升 cache 命中率等手段大幅提升性能。

5. 引入 Titan 存储引擎插件,提升 Value 值超过 1KB 时性能,缓解 RocksDB 写放大问题,减少磁盘 IO 的占用。

6. TiKV 新增多线程 Raftstore 和 Apply 功能,提升单节点内可扩展性,进而提升单节点内并发处理能力和资源利用率,降低延时,大幅提升集群写入能力。

TiDB Lightning 性能与 2019 年年初相比提升 3 倍,从 100GB/h 提升到 300GB/h,即 28MB/s 提升到 85MB/s,优化点,如下:

1. 提升 SQL 转化成 KV Pairs 的性能,减少不必要的开销。

2. 提升单表导入性能,单表支持批量导入。

3. 提升 TiKV-Importer 导入数据性能,支持将数据和索引分别导入。

4. TiKV-Importer 支持上传 SST 文件限速功能。

RBAC(Role-Based Access Control,基于角色的权限访问控制) 是商业系统中最常见的权限管理技术之一,通过 RBAC 思想可以构建最简单“用户-角色-权限”的访问权限控制模型。RBAC 中用户与角色关联,权限与角色关联,角色与权限之间一般是多对多的关系,用户通过成为什么样的角色获取该角色所拥有的权限,达到简化权限管理的目的,通过此版本的迭代 RBAC 功能开发完成。

IP 白名单功能(企业版特性) :TiDB 提供基于 IP 白名单实现网络安全访问控制,用户可根据实际情况配置相关的访问策略。

Audit log 功能(企业版特性) :Audit log 记录用户对数据库所执行的操作,通过记录 Audit log 用户可以对数据库进行故障分析,行为分析,安全审计等,帮助用户获取数据执行情况。

加密存储(企业版特性) :TiDB 利用 RocksDB 自身加密功能,实现加密存储的功能,保证所有写入到磁盘的数据都经过加密,降低数据泄露的风险。

完善权限语句的权限检查 ,新增 ANALYZE,USE,SET GLOBAL,SHOW PROCESSLIST 语句权限检查。

1. 新增 SQL 方式查询慢查询,丰富 TiDB 慢查询日志内容,如:Coprocessor 任务数,平均/最长/90% 执行/等待时间,执行/等待时间最长的 TiKV 地址,简化慢查询定位工作,提高排查慢查询问题效率,提升产品易用性。

2. 新增系统配置项合法性检查,优化系统监控项等,提升产品易用性。

3. 新增对 TableReader、IndexReader 和 IndexLookupReader 算子内存使用情况统计信息,提高 Query 内存使用统计的准确性,提升处理内存消耗较大语句的效率。

4. 制定日志规范,重构日志系统,统一日志格式,方便用户理解日志内容,有助于通过工具对日志进行定量分析。

5. 新增 EXPLAIN ANALYZE 功能,提升SQL 调优的易用性。

6. 新增 SQL 语句 Trace 功能,方便排查问题。

7. 新增通过 unix_socket 方式连接数据库。

8. 新增快速恢复被删除表功能,当误删除数据时可通过此功能快速恢复数据。

TiDB 3.0 新增 TiFlash 组件,解决复杂分析及 HTAP 场景。TiFlash 是列式存储系统,与行存储系统实时同步,具备低延时,高性能,事务一致性读等特性。 通过 Raft 协议从 TiKV 中实时同步行存数据并转化成列存储格式持久化到一组独立的节点,解决行列混合存储以及资源隔离性问题。TiFlash 可用作行存储系统(TiKV)实时镜像,实时镜像可独立于行存储系统,将行存储及列存储从物理隔离开,提供完善的资源隔离方案,HTAP 场景最优推荐方案;亦可用作行存储表的索引,配合行存储对外提供智能的 OLAP 服务,提升约 10 倍复杂的混合查询的性能。

TiFlash 目前处于 Beta 阶段,计划 2019 年 12 月 31 日之前 GA,欢迎大家申请试用。

未来我们会继续投入到系统稳定性,易用性,性能,弹性扩展方面,向用户提供极致的弹性伸缩能力,极致的性能体验,极致的用户体验。

稳定性方面 V4.0 版本将继续完善 V3.0 未 GA 的重大特性,例如:悲观事务模型,View,Table Partition,Titan 行存储引擎,TiFlash 列存储引擎;引入近似物理备份恢复解决分布数据库备份恢复难题;优化 PD 调度功能等。

性能方面 V4.0 版本将继续优化事务处理流程,减少事务资源消耗,提升性能,例如:1PC,省去获取 commit ts 操作等。

弹性扩展方面,PD 将提供弹性扩展所需的元信息供外部系统调用,外部系统可根据元信息及负载情况动态伸缩集群规模,达成节省成本的目标。

我们相信战胜“未知”最好的武器就是社区的力量,基础软件需要坚定地走开源路线。截止发稿我们已经完成 41 篇源码阅读文章。TiDB 开源社区总计 265 位 Contributor,6 位 Committer,在这里我们对社区贡献者表示由衷的感谢,希望更多志同道合的人能加入进来,也希望大家在 TiDB 这个开源社区能够有所收获。

TiDB 3.0 GA Release Notes:

Raft协议简析

一致性算法之所以可以保证在有节点挂掉时也能够继续服务, 就是因为有Replicated state machines的存在。 在分布式系统中, 有两种方式来实现这个复制状态机

一致性算法一般有一下特点:

Raft算法首先会选举一个leader, 然后又leader来管理replicated log。 leader会从client处接收请求, 然后转发给别的节点, 并且告诉这些节点什么时候可以把这些请求应用在状态机上(落地)。 当一个leader挂了的时候, 会马上选举出一个新的leader。 由上可知, Raft将一致性问题拆分成了3个独立的子问题:

Logs 组织的形式如图所示。 当leader收到一个Log entry时, 每个log entry都会存储一个带有term number的state machine命令。 Term number是用来探测log之间的不一致性并且保证Figure3的一些特性。 每个log也还带有他们在log里的索引数字。

当leader认为这个entry是可以成功应用到状态机上, 那么这个entry就被成为commited。Raft保证所有commited entry最终都会被应用到所有的节点上。

当一个entry被成功复制到半数以上的节点后, 这个log就可以认为成功写入了。并且会将leader之前的写入也视为commit。

设计Raft日志机制不仅简化了系统的行为, 也保证了正确性。 保证了Figure3的以下特性

1. 当不同log中的两个entry有相同的term和index, 那么这两个entry就是相同的

2. 当不同log中的两个entry有相同的term和index, 那么这两个entry之前的所有entry也都是相同的

当leader发现follower的log跟自己的不同时, 他会针对每个follower维护一个nextIndex。 这个nextIndex就是Leader下次会发第几个entry给这个follower。 而follower也会拒绝这个append entry的rpc。

Safety

如Figure 4所示, 如果term T commit的数据在之后的term U丢失了, 那么

1. 这个log一定不在Leader-U的log中(因为leader不会覆盖旧数据)

2. Leader-T把这个日志复制给了大多数节点并且Leader-U收到了大多数节点的投票, 所以至少有一个节点是既收到了这个entry并且又给Leader-U投票了

3. 那么这个投票的节点也一定收到了所有Leader-T的commited entries

4. 因为这个投票节点把票投给了U, 那么说明Leader-U也至少有所有这个节点的entries

由此可知, 如果投票节点和Leader-U 共享了之前的log, 那么Leader-U肯定会有所有投票节点的entry。 另外, Leader-U的last-log-term必须比T大, 而投票节点的term至少是T。之前term的leader在复制entry给Leader-U时也必须包含了之前的这个entry。 投票节点和之前term的leader都会有这个entry, 所以Leader-U也不会没有这个entry。可下结论所有大于T的term的leader绝对包含了所有term T commited的entries。

Raft协议

raft协议是一个共识算法,主要包括leader election,log replication,safety三个关键部分,另外还包括membership changes和snapshot。

复制状态机是分布式系统中解决fault tolerance问题的常用手段。raft通过log replication来保证集群的多个server,会有同样的数据输入到各自的状态机。如图1所示。

关键术语:

Apply:将entry输入到状态机

committed:entry可以被安全的Apply到状态机,一般情况下entry被同步到集群的大多数节点上时,就可以认为是committed(有特殊情况)。

每个server都有一个log,log中包含一系列的entry(entry中有相应的命令,即客户端请求),状态机按照log中的顺序执行这些命令。

如果每个server输入状态机的数据相同,状态机产生的结果也是相同的。因此共识算法的目的就是保证多个server的log一致。

leader上的consensus module接收到客户端的命令,将这些命令作为entry添加到log中,并且和其他follower上的consensus module通信,将log entry同步到其他follower,以确保多个server之间日志文件的最终一致。

当达到一定条件,即该条entry committed时,leader会将命令输入状态机,并将输出返回给客户端,同时通过心跳通知其他follower可以Apply该entry。

共识算法有以下特点:

1.safety,在所有非拜占庭条件下(包括网络延迟,分区,丢包,duplication,reordering等),不会返回错误的结果

2.大部分节点正常话,系统就可以正常工作

3.不依靠物理时钟来确保日志的一致,错误的物理时钟和消息延迟最多会造成可用性问题

4.集群中的大多数节点在一轮rpc调用中正常响应的话,一个客户端的请求就会被正常返回,不会受部分慢节点的影响

任何时刻,一个server处于以下三个状态之一:leader,follower,candidate。

一般情况下,有1个leader,其他节点都是follower,follower是被动的,不会发送请求,只会响应leader和candidate的请求。

leader处理所有客户端的请求(如果客户端请求了follower,follower将请求重定向到leader)。

candidate状态用于选举一个新的leader,状态转换如下图

raft将物理时间分隔为一个个的任意长度的term,term是连续的。

每个term从election开始,一个或者多个candidate尝试竞选为leader,如果一个candidate赢得了选举,就会成为term的余下时间内的leader。

一些情况下,会产生split vote,term会以没有leader的状态结束,开始新一轮的term以及选举

raft确保一个term中最多只会有一个leader。

term是逻辑时钟,每个server存储一个current term number,current term number随着时间单调递增,当节点之间通信时,会交换current term number,

如果一个server发现自己的current term number小于其他节点的,该server会将自己的term更新为更大的term,

如果一个candidate或者leader发现有节点的term大于自己的term,就会转变为follower(有特殊情况),

如果一个节点接收到一个有着过期term number的请求,则会拒绝这个请求。

raft的server之间使用RPC通信,主要为两种类型的RPC,

RequestVote RPC:用于candidate选举

AppendEntries RPC:用于leader发送log entry给follower,或者心跳

另外还有一种InstallSnapshot RPC,用于传输snapshot

raft协议首先需要选举一个唯一的leader,leader接受客户端的命令,将这些命令复制到其他follower,通知follower什么时候可以将这些日志输入到状态机。data flow是单向的,从leader到follower。

raft使用心跳机制触发leader election,当一个server start up,起始状态是follower,只要收到leader和candidate的正确RPC请求,server就会保持follower的状态。

如果follower在一定时间内(election timeout)没有收到心跳,follower会认为当前没有leader,并开始竞选。

开始竞选时,follower增加自己的current term并将状态转换为candidate,然后会选举自己并发送RequestVote RPC请求给集群中的其他server。

一个candidate会保持自己的状态直到下面三种情况之一发生:

1.赢得选举

一个candidate在接收到集群中大多数节点对当前term的投票之后,赢得选举。每个server在一个term中,最多只会给一个candidate投票,first-come-first-served,

2.其他节点成为leader

如果candidate收到其他节点的RPC请求,而且请求中的term大于等于candidate的current term,candidate会认为已经选出leader,并返回到follower状态。

如果RPC请求中的term小于candidate的current term,candidate会拒绝该RPC请求。

3.一定时间内(election timeout)没有选举出leader

每个candidate都会time out,并且增加自己的term,开始新一轮的选举

election timeout是在一个固定范围内(例如150ms-300ms内)随机的

上述机制保证在一个term中,只有一个candidate会成为leader,当一个candidate成为leader,它会发送心跳信息给所有其他的节点。

当一个leader被选举出来之后,client发送请求给leader,leader将将请求作为一个新的entry添加到log中,然后并行的发送AppendEntries RPC请求(携带该entry)给follower。

leader判断当前是否可以安全地将entry apply到状态机中,此时该entry被叫做committed。然后leader将请求Apply到状态机,并返回执行结果。

log entry中会保存接受到entry时的term,以及一个用于标记log entry位置的index。

raft保证committed entries是持久化的,并最终会被所有的状态机执行。

当一个entry被leader replicate到集群中的大多数节点上时,该entry就是committed。

如果某条entry是committed的,该entry之前的entry也都是committed的,包括之前的leader创建的entry。

leader会记录committed的日志的最高index,并将该index包含在之后的AppendEntries RPC中(包括 heartbeats),

follower知道某个entry是committed,就会将该entry apply到状态机中。

AppendEntries Consistency Check:

当发送一个AppendEntries RPC,leader将新entries之前最近的log entry的index和term包含在RPC请求中。如果follower发现自己的log中没有该index和term的entry,就会拒绝新的entries。

类似于一个归纳的过程,最初的空的log满足Log Matching Property,当有新的log entry时,consistency check同样保证了新的log entry满足Log Matching Property。

这样,当AppendEntries请求返回成功的响应时,leader就知道follower的log在new entries之前的部分和自己的log一样。

一个新的leader被选举出来之后,follower的log可能和新的leader不一样,follower可能有leader没有的entry,也可能有老的leader没有commit的entry。

为了让follower的log和leader的完全一致,leader需要找到follower的log和自己的log分叉的地方,删除follower在分叉点之后的log entry,然后leader向follower发送自己在分叉点之后的log entry。

上述操作通过AppendEntries RPC来实现,leader会记录每个follower的nextIndex,即leader应该发送给这个follower的下一个log entry的index。

如果follower的log和leader的不一样,AppendEntries RPC会失败,leader减小nextIndex并重试。

如果需要,这个协议也可以优化,如果AppendEntries RPC失败,follower可以返回冲突的term,以及该term的第一个index。这样原来一个不同的entry就需要一个AppendEntries请求,现在一个term需要一个AppendEntries请求。

这样多个节点之间的日志就会收敛一致。同时,leader从不会覆盖或者删除自己的log entry,符合Leader Append-Only Property。

上述部分并不能完全保证每个状态机以相同的顺序执行相同的命令。

例如,一个follower可能在当前leader commit一些log entry的时候不可用,然后该follower被选举为新的leader后,就可能覆盖之前committed的日志,从而造成不同的状态机执行了不同的命令。

下面讨论leader election的限制,这些限制能保证任何term的leader都会包含之前term中committed的log entry。

RequestVote RPC请求包含candidate的log,如果voter的log比candidate的log更加up-to-date,voter会拒绝这次投票。

up-to-date:两个log,如果term不同,term更大的更新,如果term相同,日志更长的更新

一个leader不能立即判断出一个之前term的entry是否应该committed,即使该entry被存储到了大多数节点上。

(a)S1是leader,写入一条命令,index是2

(b)S1 crash,S5选举为leader,写入一条命令,index是3

(c)S5 crash,S1选举为leader,写入一条命令,index是4,并将index为2的log entry同步到S3,commit和apply index为2的log entry

(d)S1 crash,S5选举为leader,会覆盖掉index2,造成多个server的状态机apply不一样的log entry

因此,raft不会因为之前term的log entry被存储到了大多数节点上,就将该entry commit(raft never commits log entries from pervious terms by counting replicas),只有当前term的log entry被存储到大多数节点上时,才会判断该entry为commit

(only log entries from the leader's current term are committed by counting replicas)。这样,由于Log Matching Property,所有之前的entries都会间接地被commit掉。

raft使用two-phase的方案来处理configuration change,集群首先会切换到一个名叫joint consensus的中间状态,一旦joint consensus被committed了,集群就会使用新的configuration。

joint consensus将老的和新的configuration结合在一起:

集群configuration也是以log entry的方式存储和同步到其他server上。

当leader接收到configuration从C-old变为C-new的请求之后,将C-old,new的entry存储到log中,并同步到其他server上。

follower接收到entry后,无论该entry是否已经committed,都会使用entry包含的configuration替换当前的configuration。

如果leader crash,新的leader的configuration只可能是C-old或者C-old,new。

C-old,new被committed之后,leader创建一条C-new的entry,并同步到其他server上。

follower接收到该entry之后,无论该entry是否已经committed,都会使用C-new替换之前的configuration。

当C-new被committed之后,C-old中的节点就可以被shut down。

上述方案需要解决三个问题:

1.新加入的server需要很长时间才能追上leader,在这段时间内无法committed,为此raft引入了non-voting 成员

2.老的leader可能不在新的configuration中。为此,leader在C-new committed之后,leader需要变成follower

3.removed servers可能会影响集群。这些节点不会接收到心跳,然后time out,然后开始新一轮的选举。这会造成当前的leader变成follower,然后重新选举leader。上述过程会不断重复。

为此,server需要忽略RequestVote RPC,如果当前的leader没有time out。

snapshotting是log compacting的最简单的办法,状态机将当前系统状态被写进snapshot,之前的log entry会被删除。

每个server会独立的take snapshot,snapshot会包含log中已经committed的log entry。

snapshot中会包含少量的元数据,

last included index:状态机apply的最后一个log entry,也就是snapshot替换掉的最后一个log entry 的index。

last included term:上述entry的term

元数据用于snapshot之后的第一个log entry的AppendEntries consistency check,由于该entry需要之前的log的index和term。

元数据也包含最近的configuration。

对于一个刚加进集群的server,leader使用InstallSnapshot RPC发送snapshot给follower。

raft需要把所有的请求发送给leader,当一个client start,client连接集群中的任意一个节点,如果该节点不是leader,则会拒绝client的请求,并返回leader的信息(AppendEntries请求包含了leader的网络地址)。

如果leader crash,client请求会timeout,然后随机选择一个节点继续重试。

raft协议需要实现线性语义(linearizable semantics),每个操作会且只会执行一次(exactly once),但是仅靠之前提到的几点,raft协议的可能会让一个命令执行多次。

例如,leader在commit一个log entry,但是还没有来得及返回给client之后,就crash掉,client会在新的leader上重复发送相同的请求,造成该请求执行两次。

解决方法是client给每个命令一个序列号,状态机记录每个client最近执行的序列号。如果状态机收到一个命令,该命令的序列号是之前执行过的,就立即返回而不再执行该命令。

只读操作可能会读到过期的数据。因为client访问一个leader时,集群中选举出了其他leader,该leader马上就会变成follower。linear semantics不能返回过期数据。

raft的解决方案分两步,

首先,一个leader必须确认哪些entry是committed,Leader Completeness Property保证一个leader拥有所有committed的entry,但是在term的开始阶段,leader并不知道哪些是已经committed的。因此,leader需要在term的开始,先commit一个no-op entry。

然后,leader必须检查当前是否有其他leader被选举出来,将要取代自己的leader位置。raft在返回read-only请求的响应之前,需要和集群中的大多数节点发送心跳。

NET中有没有类似ZooKeeper这样的分布式服务框架

本文是JasonWilder对于常见的服务发现项目Zookeeper,Doozer,Etcd所写的一篇博客,其原文地址如下:Open-SourceServiceDiscovery。服务发现是大多数分布式系统以及面向服务架构(SOA)的一个核心组成部分。这个难题,简单来说,可以认为是:当一项服务存在于多个主机节点上时,client端如何决策获取相应正确的IP和port。在传统情况下,当出现服务存在于多个主机节点上时,都会使用静态配置的方法来实现服务信息的注册。但是当大型系统中,需要部署服务的时候,事情就显得复杂得多。在一个实时的系统中,由于自动或者人工的服务扩展,或者服务的新添加部署,还有主机的宕机或者被替换,服务的location信息可能会很频繁的变化。在这样的场景下,为了避免不必要的服务中断,动态的服务注册和发现就显得尤为重要。关于服务发现的话题,已经很多次被人所提及,而且也的确不断的在发展。现在,笔者介绍一下该领域内一些open-source或者被经常被世人广泛讨论的解决方案,尝试理解它们到底是如何工作的。特别的是,我们会较为专注于每一个解决方案的一致性算法,到底是强一致性,还是弱一致性;运行时依赖;client的集成选择;以后最后这些特性的折中情况。本文首先从几个强一致性的项目于开始,比如Zookeeper,Doozer,Etcd,这些项目主要用于服务间的协调,同时又可用于服务的注册。随后,本文将讨论一些在服务注册以及发现方面比较有意思的项目,比如:Airbnb的SmartStack,Netflix的Eureka,Bitly的NSQ,Serf,SpotifyandDNS,最后是SkyDNS。问题陈述在定位服务的时候,其实会有两个方面的问题:服务注册(ServiceRegistration)和服务发现(ServiceDiscovery)。服务注册——一个服务将其位置信息在中心注册节点注册的过程。该服务一般会将它的主机IP地址以及端口号进行注册,有时也会有服务访问的认证信息,使用协议,版本号,以及关于环境的一些细节信息。服务发现——client端的应用实例查询中心注册节点以获知服务位置的过程。每一个服务的服务注册以及服务发现,都需要考虑一些关于开发以及运营方面的问题:监控——当一个已注册完毕的服务失效的时候,如何处理。一些情况下,在一个设定的超时定时(timeout)后,该服务立即被一个其他的进程在中心注册节点处注销。这种情况下,服务通常需要执行一个心跳机制,来确保自身的存活状态;而客户端必然需要能够可靠处理失效的服务。负载均衡——如果多个相同地位的服务都注册完毕,如何在这些服务之间均衡所有client的请求负载?如果有一个master节点的话,是否可以正确处理client访问的服务的位置。集成方式——信息注册节点是否需要提供一些语言绑定的支持,比如说,只支持Java?集成的过程是否需要将注册过程以及发现过程的代码嵌入到你的应用程序中,或者使用一个类似于集成助手的进程?运行时依赖——是否需要JVM,ruby或者其他在你的环境中并不兼容的运行时?可用性考虑——如果系统失去一个节点的话,是否还能正常工作?系统是否可以实时更新或升级,而不造成任何系统的瘫痪?既然集群的信息注册节点是架构中的中心部分,那该模块是否会存在单点故障问题?强一致性的Registries首先介绍的三个服务注册系统都采用了强一致性协议,实际上为达到通用的效果,使用了一致性的数据存储。尽管我们把它们看作服务的注册系统,其实它们还可以用于协调服务来协助leader选举,以及在一个分布式clients的集合中做centralizedlocking。ZookeeperZookeeper是一个集中式的服务,该服务可以维护服务配置信息,命名空间,提供分布式的同步,以及提供组化服务。Zookeeper是由Java语言实现,实现了强一致性(CP),并且是使用Zab协议在ensemble集群之间协调服务信息的变化。Zookeeper在ensemble集群中运行3个,5个或者7个成员。众多client端为了可以访问ensemble,需要使用绑定特定的语言。这种访问形式被显性的嵌入到了client的应用实例以及服务中。服务注册的实现主要是通过命令空间(namespace)下的ephemeralnodes。ephemeralnodes只有在client建立连接后才存在。当client所在节点启动之后,该client端会使用一个后台进程获取client的位置信息,并完成自身的注册。如果该client失效或者失去连接的时候,该ephemeralnode就从树中消息。服务发现是通过列举以及查看具体服务的命名空间来完成的。Client端收到目前所有注册服务的信息,无论一个服务是否不可用或者系统新添加了一个同类的服务。Client端同时也需要自行处理所有的负载均衡工作,以及服务的失效工作。Zookeeper的API用起来可能并没有那么方便,因为语言的绑定之间可能会造成一些细小的差异。如果使用的是基于JVM的语言的话,CuratorServiceDiscoveryExtension可能会对你有帮助。由于Zookeeper是一个CP强一致性的系统,因此当网络分区(Partition)出故障的时候,你的部分系统可能将出出现不能注册的情况,也可能出现不能找到已存在的注册信息,即使它们可能在Partition出现期间仍然正常工作。特殊的是,在任何一个non-quorum端,任何读写都会返回一个错误信息。DoozerDoozer是一个一致的分布式数据存储系统,Go语言实现,通过Paxos算法来实现共识的强一致性系统。这个项目开展了数年之后,停滞了一段时间,而且现在也关闭了一些fork数,使得fork数降至160。.不幸的是,现在很难知道该项目的实际发展状态,以及它是否适合使用于生产环境。Doozer在集群中运行3,5或者7个节点。和Zookeeper类似,Client端为了访问集群,需要在自身的应用或者服务中使用特殊的语言绑定。Doozer的服务注册就没有Zookeeper这么直接,因为Doozer没有那些ephemeralnode的概念。一个服务可以在一条路径下注册自己,如果该服务不可用的话,它也不会自动地被移除。现有很多种方式来解决这样的问题。一个选择是给注册进程添加一个时间戳和心跳机制,随后在服务发现进程中处理那些超时的路径,也就是注册的服务信息,当然也可以通过另外一个清理进程来实现。服务发现和Zookeeper很类似,Doozer可以罗列出指定路径下的所有入口,随后可以等待该路径下的任意改动。如果你在注册期间使用一个时间戳和心跳,你就可以在服务发现期间忽略或者删除任何过期的入口,也就是服务信息。和Zookeeper一样,Doozer是一个CP强一致性系统,当发生网络分区故障时,会导致同样的后果。EtcdEtcd是一个高可用的K-V存储系统,主要应用于共享配置、服务发现等场景。Etcd可以说是被Zookeeper和Doozer催生而出。整个系统使用Go语言实现,使用Raft算法来实现选举一致,同时又具有一个基于HTTP+JSON的API。Etcd,和Doozer和Zookeeper相似,通常在集群中运行3,5或者7个节点。client端可以使用一种特定的语言进行绑定,同时也可以通过使用HTTP客户端自行实现一种。服务注册环节主要依赖于使用一个keyTTL来确保key的可用性,该keyTTL会和服务端的心跳捆绑在一起。如果一个服务在更新key的TTL时失败了,那么Etcd会对它进行超时处理。如果一个服务变为不可用状态,client会需要处理这样的连接失效,然后尝试另连接一个服务实例。服务发现环节设计到罗列在一个目录下的所有key值,随后等待在该目录上的所有变动信息。由于API接口是基于HTTP的,所以client应用会的Etcd集群保持一个long-polling的连接。由于Etcd使用Raft一致性协议,故它应该是一个强一致性系统。Raft需要一个leader被选举,然后所有的client请求会被该leader所处理。然而,Etcd似乎也支持从non-leaders中进行读取信息,使用的方式是在读情况下提高可用性的未公开的一致性参数。在网络分区故障期间,写操作还是会被leader处理,而且同样会出现失效的情况。

如何实现支持数亿用户的长连消息系统

此文是根据周洋在【高可用架构群】中的分享内容整理而成,转发请注明出处。

周洋,360手机助手技术经理及架构师,负责360长连接消息系统,360手机助手架构的开发与维护。

不知道咱们群名什么时候改为“Python高可用架构群”了,所以不得不说,很荣幸能在接下来的一个小时里在Python群里讨论golang....

360消息系统介绍

360消息系统更确切的说是长连接push系统,目前服务于360内部多个产品,开发平台数千款app,也支持部分聊天业务场景,单通道多app复用,支持上行数据,提供接入方不同粒度的上行数据和用户状态回调服务。

目前整个系统按不同业务分成9个功能完整的集群,部署在多个idc上(每个集群覆盖不同的idc),实时在线数亿量级。通常情况下,pc,手机,甚至是智能硬件上的360产品的push消息,基本上是从我们系统发出的。

关于push系统对比与性能指标的讨论

很多同行比较关心go语言在实现push系统上的性能问题,单机性能究竟如何,能否和其他语言实现的类似系统做对比么?甚至问如果是创业,第三方云推送平台,推荐哪个?

其实各大厂都有类似的push系统,市场上也有类似功能的云服务。包括我们公司早期也有erlang,nodejs实现的类似系统,也一度被公司要求做类似的对比测试。我感觉在讨论对比数据的时候,很难保证大家环境和需求的统一,我只能说下我这里的体会,数据是有的,但这个数据前面估计会有很多定语~

第一个重要指标:单机的连接数指标

做过长连接的同行,应该有体会,如果在稳定连接情况下,连接数这个指标,在没有网络吞吐情况下对比,其实意义往往不大,维持连接消耗cpu资源很小,每条连接tcp协议栈会占约4k的内存开销,系统参数调整后,我们单机测试数据,最高也是可以达到单实例300w长连接。但做更高的测试,我个人感觉意义不大。

因为实际网络环境下,单实例300w长连接,从理论上算压力就很大:实际弱网络环境下,移动客户端的断线率很高,假设每秒有1000分之一的用户断线重连。300w长连接,每秒新建连接达到3w,这同时连入的3w用户,要进行注册,加载离线存储等对内rpc调用,另外300w长连接的用户心跳需要维持,假设心跳300s一次,心跳包每秒需要1w tps。单播和多播数据的转发,广播数据的转发,本身也要响应内部的rpc调用,300w长连接情况下,gc带来的压力,内部接口的响应延迟能否稳定保障。这些集中在一个实例中,可用性是一个挑战。所以线上单实例不会hold很高的长连接,实际情况也要根据接入客户端网络状况来决定。

第二个重要指标:消息系统的内存使用量指标

这一点上,使用go语言情况下,由于协程的原因,会有一部分额外开销。但是要做两个推送系统的对比,也有些需要确定问题。比如系统从设计上是否需要全双工(即读写是否需要同时进行)如果半双工,理论上对一个用户的连接只需要使用一个协程即可(这种情况下,对用户的断线检测可能会有延时),如果是全双工,那读/写各一个协程。两种场景内存开销是有区别的。

另外测试数据的大小往往决定我们对连接上设置的读写buffer是多大,是全局复用的,还是每个连接上独享的,还是动态申请的。另外是否全双工也决定buffer怎么开。不同的策略,可能在不同情况的测试中表现不一样。

第三个重要指标:每秒消息下发量

这一点上,也要看我们对消息到达的QoS级别(回复ack策略区别),另外看架构策略,每种策略有其更适用的场景,是纯粹推?还是推拉结合?甚至是否开启了消息日志?日志库的实现机制、以及缓冲开多大?flush策略……这些都影响整个系统的吞吐量。

另外为了HA,增加了内部通信成本,为了避免一些小概率事件,提供闪断补偿策略,这些都要考虑进去。如果所有的都去掉,那就是比较基础库的性能了。

所以我只能给出大概数据,24核,64G的服务器上,在QoS为message at least,纯粹推,消息体256B~1kB情况下,单个实例100w实际用户(200w+)协程,峰值可以达到2~5w的QPS...内存可以稳定在25G左右,gc时间在200~800ms左右(还有优化空间)。

我们正常线上单实例用户控制在80w以内,单机最多两个实例。事实上,整个系统在推送的需求上,对高峰的输出不是提速,往往是进行限速,以防push系统瞬时的高吞吐量,转化成对接入方业务服务器的ddos攻击所以对于性能上,我感觉大家可以放心使用,至少在我们这个量级上,经受过考验,go1.5到来后,确实有之前投资又增值了的感觉。

消息系统架构介绍

下面是对消息系统的大概介绍,之前一些同学可能在gopher china上可以看到分享,这里简单讲解下架构和各个组件功能,额外补充一些当时遗漏的信息:

架构图如下,所有的service都 written by golang.

几个大概重要组件介绍如下:

dispatcher service根据客户端请求信息,将应网络和区域的长连接服务器的,一组IP传送给客户端。客户端根据返回的IP,建立长连接,连接Room service.

room Service,长连接网关,hold用户连接,并将用户注册进register service,本身也做一些接入安全策略、白名单、IP限制等。

register service是我们全局session存储组件,存储和索引用户的相关信息,以供获取和查询。

coordinator service用来转发用户的上行数据,包括接入方订阅的用户状态信息的回调,另外做需要协调各个组件的异步操作,比如kick用户操作,需要从register拿出其他用户做异步操作.

saver service是存储访问层,承担了对redis和mysql的操作,另外也提供部分业务逻辑相关的内存缓存,比如广播信息的加载可以在saver中进行缓存。另外一些策略,比如客户端sdk由于被恶意或者意外修改,每次加载了消息,不回复ack,那服务端就不会删除消息,消息就会被反复加载,形成死循环,可以通过在saver中做策略和判断。(客户端总是不可信的)。

center service提供给接入方的内部api服务器,比如单播或者广播接口,状态查询接口等一系列api,包括运维和管理的api。

举两个常见例子,了解工作机制:比如发一条单播给一个用户,center先请求Register获取这个用户之前注册的连接通道标识、room实例地址,通过room service下发给长连接 Center Service比较重的工作如全网广播,需要把所有的任务分解成一系列的子任务,分发给所有center,然后在所有的子任务里,分别获取在线和离线的所有用户,再批量推到Room Service。通常整个集群在那一瞬间压力很大。

deployd/agent service用于部署管理各个进程,收集各组件的状态和信息,zookeeper和keeper用于整个系统的配置文件管理和简单调度

关于推送的服务端架构

常见的推送模型有长轮训拉取,服务端直接推送(360消息系统目前主要是这种),推拉结合(推送只发通知,推送后根据通知去拉取消息).

拉取的方式不说了,现在并不常用了,早期很多是nginx+lua+redis,长轮训,主要问题是开销比较大,时效性也不好,能做的优化策略不多。

直接推送的系统,目前就是360消息系统这种,消息类型是消耗型的,并且对于同一个用户并不允许重复消耗,如果需要多终端重复消耗,需要抽象成不同用户。

推的好处是实时性好,开销小,直接将消息下发给客户端,不需要客户端走从接入层到存储层主动拉取.

但纯推送模型,有个很大问题,由于系统是异步的,他的时序性无法精确保证。这对于push需求来说是够用的,但如果复用推送系统做im类型通信,可能并不合适。

对于严格要求时序性,消息可以重复消耗的系统,目前也都是走推拉结合的模型,就是只使用我们的推送系统发通知,并附带id等给客户端做拉取的判断策略,客户端根据推送的key,主动从业务服务器拉取消息。并且当主从同步延迟的时候,跟进推送的key做延迟拉取策略。同时也可以通过消息本身的QoS,做纯粹的推送策略,比如一些“正在打字的”低优先级消息,不需要主动拉取了,通过推送直接消耗掉。

哪些因素决定推送系统的效果?

首先是sdk的完善程度,sdk策略和细节完善度,往往决定了弱网络环境下最终推送质量.

SDK选路策略,最基本的一些策略如下:有些开源服务可能会针对用户hash一个该接入区域的固定ip,实际上在国内环境下不可行,最好分配器(dispatcher)是返回散列的一组,而且端口也要参开,必要时候,客户端告知是retry多组都连不上,返回不同idc的服务器。因为我们会经常检测到一些case,同一地区的不同用户,可能对同一idc内的不同ip连通性都不一样,也出现过同一ip不同端口连通性不同,所以用户的选路策略一定要灵活,策略要足够完善.另外在选路过程中,客户端要对不同网络情况下的长连接ip做缓存,当网络环境切换时候(wifi、2G、3G),重新请求分配器,缓存不同网络环境的长连接ip。

客户端对于数据心跳和读写超时设置,完善断线检测重连机制

针对不同网络环境,或者客户端本身消息的活跃程度,心跳要自适应的进行调整并与服务端协商,来保证链路的连通性。并且在弱网络环境下,除了网络切换(wifi切3G)或者读写出错情况,什么时候重新建立链路也是一个问题。客户端发出的ping包,不同网络下,多久没有得到响应,认为网络出现问题,重新建立链路需要有个权衡。另外对于不同网络环境下,读取不同的消息长度,也要有不同的容忍时间,不能一刀切。好的心跳和读写超时设置,可以让客户端最快的检测到网络问题,重新建立链路,同时在网络抖动情况下也能完成大数据传输。

结合服务端做策略

另外系统可能结合服务端做一些特殊的策略,比如我们在选路时候,我们会将同一个用户尽量映射到同一个room service实例上。断线时,客户端尽量对上次连接成功的地址进行重试。主要是方便服务端做闪断情况下策略,会暂存用户闪断时实例上的信息,重新连入的 时候,做单实例内的迁移,减少延时与加载开销.

客户端保活策略

很多创业公司愿意重新搭建一套push系统,确实不难实现,其实在协议完备情况下(最简单就是客户端不回ack不清数据),服务端会保证消息是不丢的。但问题是为什么在消息有效期内,到达率上不去?往往因为自己app的push service存活能力不高。选用云平台或者大厂的,往往sdk会做一些保活策略,比如和其他app共生,互相唤醒,这也是云平台的push service更有保障原因。我相信很多云平台旗下的sdk,多个使用同样sdk的app,为了实现服务存活,是可以互相唤醒和保证活跃的。另外现在push sdk本身是单连接,多app复用的,这为sdk实现,增加了新的挑战。

综上,对我来说,选择推送平台,优先会考虑客户端sdk的完善程度。对于服务端,选择条件稍微简单,要求部署接入点(IDC)越要多,配合精细的选路策略,效果越有保证,至于想知道哪些云服务有多少点,这个群里来自各地的小伙伴们,可以合伙测测。

go语言开发问题与解决方案

下面讲下,go开发过程中遇到挑战和优化策略,给大家看下当年的一张图,在第一版优化方案上线前一天截图~

可以看到,内存最高占用69G,GC时间单实例最高时候高达3~6s.这种情况下,试想一次悲剧的请求,经过了几个正在执行gc的组件,后果必然是超时... gc照成的接入方重试,又加重了系统的负担。遇到这种情况当时整个系统最差情况每隔2,3天就需要重启一次~

当时出现问题,现在总结起来,大概以下几点

1.散落在协程里的I/O,Buffer和对象不复用。

当时(12年)由于对go的gc效率理解有限,比较奔放,程序里大量short live的协程,对内通信的很多io操作,由于不想阻塞主循环逻辑或者需要及时响应的逻辑,通过单独go协程来实现异步。这回会gc带来很多负担。

针对这个问题,应尽量控制协程创建,对于长连接这种应用,本身已经有几百万并发协程情况下,很多情况没必要在各个并发协程内部做异步io,因为程序的并行度是有限,理论上做协程内做阻塞操作是没问题。

如果有些需要异步执行,比如如果不异步执行,影响对用户心跳或者等待response无法响应,最好通过一个任务池,和一组常驻协程,来消耗,处理结果,通过channel再传回调用方。使用任务池还有额外的好处,可以对请求进行打包处理,提高吞吐量,并且可以加入控量策略.

2.网络环境不好引起激增

go协程相比较以往高并发程序,如果做不好流控,会引起协程数量激增。早期的时候也会发现,时不时有部分主机内存会远远大于其他服务器,但发现时候,所有主要profiling参数都正常了。

后来发现,通信较多系统中,网络抖动阻塞是不可免的(即使是内网),对外不停accept接受新请求,但执行过程中,由于对内通信阻塞,大量协程被 创建,业务协程等待通信结果没有释放,往往瞬时会迎来协程暴涨。但这些内存在系统稳定后,virt和res都并没能彻底释放,下降后,维持高位。

处理这种情况,需要增加一些流控策略,流控策略可以选择在rpc库来做,或者上面说的任务池来做,其实我感觉放在任务池里做更合理些,毕竟rpc通信库可以做读写数据的限流,但它并不清楚具体的限流策略,到底是重试还是日志还是缓存到指定队列。任务池本身就是业务逻辑相关的,它清楚针对不同的接口需要的流控限制策略。

3.低效和开销大的rpc框架

早期rpc通信框架比较简单,对内通信时候使用的也是短连接。这本来短连接开销和性能瓶颈超出我们预期,短连接io效率是低一些,但端口资源够,本身吞吐可以满足需要,用是没问题的,很多分层的系统,也有http短连接对内进行请求的

但早期go版本,这样写程序,在一定量级情况,是支撑不住的。短连接大量临时对象和临时buffer创建,在本已经百万协程的程序中,是无法承受的。所以后续我们对我们的rpc框架作了两次调整。

第二版的rpc框架,使用了连接池,通过长连接对内进行通信(复用的资源包括client和server的:编解码Buffer、Request/response),大大改善了性能。

但这种在一次request和response还是占用连接的,如果网络状况ok情况下,这不是问题,足够满足需要了,但试想一个room实例要与后面的数百个的register,coordinator,saver,center,keeper实例进行通信,需要建立大量的常驻连接,每个目标机几十个连接,也有数千个连接被占用。

非持续抖动时候(持续逗开多少无解),或者有延迟较高的请求时候,如果针对目标ip连接开少了,会有瞬时大量请求阻塞,连接无法得到充分利用。第三版增加了Pipeline操作,Pipeline会带来一些额外的开销,利用tcp的全双特性,以尽量少的连接完成对各个服务集群的rpc调用。

4.Gc时间过长

Go的Gc仍旧在持续改善中,大量对象和buffer创建,仍旧会给gc带来很大负担,尤其一个占用了25G左右的程序。之前go team的大咖邮件也告知我们,未来会让使用协程的成本更低,理论上不需要在应用层做更多的策略来缓解gc.

改善方式,一种是多实例的拆分,如果公司没有端口限制,可以很快部署大量实例,减少gc时长,最直接方法。不过对于360来说,外网通常只能使用80和433。因此常规上只能开启两个实例。当然很多人给我建议能否使用SO_REUSEPORT,不过我们内核版本确实比较低,并没有实践过。

另外能否模仿nginx,fork多个进程监控同样端口,至少我们目前没有这样做,主要对于我们目前进程管理上,还是独立的运行的,对外监听不同端口程序,还有配套的内部通信和管理端口,实例管理和升级上要做调整。

解决gc的另两个手段,是内存池和对象池,不过最好做仔细评估和测试,内存池、对象池使用,也需要对于代码可读性与整体效率进行权衡。

这种程序一定情况下会降低并行度,因为用池内资源一定要加互斥锁或者原子操作做CAS,通常原子操作实测要更快一些。CAS可以理解为可操作的更细行为粒度的锁(可以做更多CAS策略,放弃运行,防止忙等)。这种方式带来的问题是,程序的可读性会越来越像C语言,每次要malloc,各地方用完后要free,对于对象池free之前要reset,我曾经在应用层尝试做了一个分层次结构的“无锁队列”

上图左边的数组实际上是一个列表,这个列表按大小将内存分块,然后使用atomic操作进行CAS。但实际要看测试数据了,池技术可以明显减少临时对象和内存的申请和释放,gc时间会减少,但加锁带来的并行度的降低,是否能给一段时间内的整体吞吐量带来提升,要做测试和权衡…

在我们消息系统,实际上后续去除了部分这种黑科技,试想在百万个协程里面做自旋操作申请复用的buffer和对象,开销会很大,尤其在协程对线程多对多模型情况下,更依赖于golang本身调度策略,除非我对池增加更多的策略处理,减少忙等,感觉是在把runtime做的事情,在应用层非常不优雅的实现。普遍使用开销理论就大于收益。

但对于rpc库或者codec库,任务池内部,这些开定量协程,集中处理数据的区域,可以尝试改造~

对于有些固定对象复用,比如固定的心跳包什么的,可以考虑使用全局一些对象,进行复用,针对应用层数据,具体设计对象池,在部分环节去复用,可能比这种无差别的设计一个通用池更能进行效果评估.

消息系统的运维及测试

下面介绍消息系统的架构迭代和一些迭代经验,由于之前在其他地方有过分享,后面的会给出相关链接,下面实际做个简单介绍,感兴趣可以去链接里面看

架构迭代~根据业务和集群的拆分,能解决部分灰度部署上线测试,减少点对点通信和广播通信不同产品的相互影响,针对特定的功能做独立的优化.

消息系统架构和集群拆分,最基本的是拆分多实例,其次是按照业务类型对资源占用情况分类,按用户接入网络和对idc布点要求分类(目前没有条件,所有的产品都部署到全部idc)

系统的测试go语言在并发测试上有独特优势。

对于压力测试,目前主要针对指定的服务器,选定线上空闲的服务器做长连接压测。然后结合可视化,分析压测过程中的系统状态。但压测早期用的比较多,但实现的统计报表功能和我理想有一定差距。我觉得最近出的golang开源产品都符合这种场景,go写网络并发程序给大家带来的便利,让大家把以往为了降低复杂度,拆解或者分层协作的组件,又组合在了一起。

QA

Q1:协议栈大小,超时时间定制原则?

移动网络下超时时间按产品需求通常2g,3G情况下是5分钟,wifi情况下5~8分钟。但对于个别场景,要求响应非常迅速的场景,如果连接idle超过1分钟,都会有ping,pong,来校验是否断线检测,尽快做到重新连接。

Q2:消息是否持久化?

消息持久化,通常是先存后发,存储用的redis,但落地用的mysql。mysql只做故障恢复使用。

Q3:消息风暴怎么解决的?

如果是发送情况下,普通产品是不需要限速的,对于较大产品是有发送队列做控速度,按人数,按秒进行控速度发放,发送成功再发送下一条。

Q4:golang的工具链支持怎么样?我自己写过一些小程序千把行之内,确实很不错,但不知道代码量上去之后,配套的debug工具和profiling工具如何,我看上边有分享说golang自带的profiling工具还不错,那debug呢怎么样呢,官方一直没有出debug工具,gdb支持也不完善,不知你们用的什么?

是这样的,我们正常就是println,我感觉基本上可以定位我所有问题,但也不排除由于并行性通过println无法复现的问题,目前来看只能靠经验了。只要常见并发尝试,经过分析是可以找到的。go很快会推出调试工具的~

Q5:协议栈是基于tcp吗?

是否有协议拓展功能?协议栈是tcp,整个系统tcp长连接,没有考虑扩展其功能~如果有好的经验,可以分享~

Q6:问个问题,这个系统是接收上行数据的吧,系统接收上行数据后是转发给相应系统做处理么,是怎么转发呢,如果需要给客户端返回调用结果又是怎么处理呢?

系统上行数据是根据协议头进行转发,协议头里面标记了产品和转发类型,在coordinator里面跟进产品和转发类型,回调用户,如果用户需要阻塞等待回复才能后续操作,那通过再发送消息,路由回用户。因为整个系统是全异步的。

Q7:问个pushsdk的问题。pushsdk的单连接,多app复用方式,这样的情况下以下几个问题是如何解决的:1)系统流量统计会把所有流量都算到启动连接的应用吧?而启动应用的连接是不固定的吧?2)同一个pushsdk在不同的应用中的版本号可能不一样,这样暴露出来的接口可能有版本问题,如果用单连接模式怎么解决?

流量只能算在启动的app上了,但一般这种安装率很高的app承担可能性大,常用app本身被检测和杀死可能性较少,另外消息下发量是有严格控制 的。整体上用户还是省电和省流量的。我们pushsdk尽量向上兼容,出于这个目的,push sdk本身做的工作非常有限,抽象出来一些常见的功能,纯推的系统,客户端策略目前做的很少,也有这个原因。

Q8:生产系统的profiling是一直打开的么?

不是一直打开,每个集群都有采样,但需要开启哪个可以后台控制。这个profling是通过接口调用。

Q9:面前系统中的消息消费者可不可以分组?类似于Kafka。

客户端可以订阅不同产品的消息,接受不同的分组。接入的时候进行bind或者unbind操作

Q10:为什么放弃erlang,而选择go,有什么特别原因吗?我们现在用的erlang?

erlang没有问题,原因是我们上线后,其他团队才做出来,经过qa一个部门对比测试,在没有显著性能提升下,选择继续使用go版本的push,作为公司基础服务。

Q11:流控问题有排查过网卡配置导致的idle问题吗?

流控是业务级别的流控,我们上线前对于内网的极限通信量做了测试,后续将请求在rpc库内,控制在小于内部通信开销的上限以下.在到达上限前作流控。

Q12:服务的协调调度为什么选择zk有考虑过raft实现吗?golang的raft实现很多啊,比如Consul和ectd之类的。

3年前,还没有后两者或者后两者没听过应该。zk当时公司内部成熟方案,不过目前来看,我们不准备用zk作结合系统的定制开发,准备用自己写的keeper代替zk,完成配置文件自动转数据结构,数据结构自动同步指定进程,同时里面可以完成很多自定义的发现和控制策略,客户端包含keeper的sdk就可以实现以上的所有监控数据,profling数据收集,配置文件更新,启动关闭等回调。完全抽象成语keeper通信sdk,keeper之间考虑用raft。

Q13:负载策略是否同时在服务侧与CLIENT侧同时做的 (DISPATCHER 会返回一组IP)?另外,ROOM SERVER/REGISTER SERVER连接状态的一致性|可用性如何保证? 服务侧保活有无特别关注的地方? 安全性方面是基于TLS再加上应用层加密?

会在server端做,比如重启操作前,会下发指令类型消息,让客户端进行主动行为。部分消息使用了加密策略,自定义的rsa+des,另外满足我们安全公司的需要,也定制开发很多安全加密策略。一致性是通过冷备解决的,早期考虑双写,但实时状态双写同步代价太高而且容易有脏数据,比如register挂了,调用所有room,通过重新刷入指定register来解决。

Q14:这个keeper有开源打算吗?

还在写,如果没耦合我们系统太多功能,一定会开源的,主要这意味着,我们所有的bind在sdk的库也需要开源~

Q15:比较好奇lisence是哪个如果开源?

请教个etcd中的raft算法问题

etcd是一个高可用的键值存储系统,主要用于共享配置和服务发现。 etcd是由CoreOS开发并维护的,灵感来自于 ZooKeeper 和 Doozer,它使用Go语言编写,并通过Raft一致性算法处理日志复制以保证强一致性。 Raft是一个来自Stanford的新的一致性算法,。


网站名称:go语言实现raft协议的简单介绍
新闻来源:http://myzitong.com/article/hieheo.html