About neohope

一直在努力,还没想过要放弃...

分布式一致性算法10:区块链共识算法

一、概述
将区块链的共识算法放到这里,其实只是做个简单的补充。
想了解更多内容,可以参考:
区块链资料汇总
区块链白皮书

二、常见区块链共识机制说明
1、PoW: Proof of Work,工作量证明
工作量证明中,所有参与的节点一起计算区块的Hash值,通过调整区块的一个随机数nonce,得到不同的Hash值。
最终第一个计算出前N位为0的Hash值,获得记账资格,并获取区块的奖励。
其余节点可以快速验证Hash结果,从而快速达成一致(算出这个Hash工作量很大,验证工作量很小)。
此类区块链,采用长链高于短链的原则,也就是如果产生了分区,短链必须遵从于长链的结果,达到最终一致性。

如果要攻破PoW,需要在该网络中,破坏者算力超过全网络算力一半,才有可能破坏最终一致性。

但PoW计算量太大,能耗太高,而且达成一致性的速度太慢,吞吐量就很难提高。

2、PoS: Proof of Stack,权益证明
权益证明网络中,所有参与节点都知道彼此节点的权益(比如,每个Token*该Token持有时间,然后求和)
在达成一致性的时候,权益越大(Token越多,持有时间越久)的节点,越容易被选择为记账节点

如果要攻破PoS,就需要较长时间持有大量的Token,增加了攻击者的攻击成本。

但PoS会导致,强者恒强,造成垄断。与区块链区中心化的目标,背道而驰。

3、DPoS: Delegated Proof of Stake,委托式权益证明
在委托式权益证明网络中,每个节点都知道彼此节点的权益(比如,持有的Token数)
想参与记账的节点,被叫做受托节点,受托节点会进行竞选,要求普通节点投票给他
普通节点不参与记账,而是根据自己的利益,投票给自己选中的受托节点,节点权益越高,投票权重越高
最终,获得最多投票的受托节点进行记账

三、部分区块链项目的共识机制

项目名称 链类型 匿名性 共识机制 合约语言
Bitcoin 公链 匿名 PoW 只是使用合约实现了业务逻辑
Ethereum 公链/联盟链 匿名或私有 22年后为PoS Solidity/Serpent/LLL
EOS 公链 匿名 BFT-DPOS CPP/Web Assembly
Fabric 联盟链 共有或认证 PBFT(classic, batch, sieve) Chaincode

分布式一致性算法09:2PC3PC

一、两阶段提交2PC(Two-Phase Commit)
1、概述
顾名思义,两阶段提交,就是分布式数据库系统,把事务的提交过程,分为两个阶段进行操作,从而保持数据的一致性。

2、流程
a、准备阶段(Prepare Phase)
协调者(Coordinator)收到事务请求。
协调者向所有参与者(Participants)发送准备事务提交的请求。
参与者收到准备请求后,会检查本地事务的状态,判断能否完成此事务请求,并将检查结果反馈给协调者。
如果可以完成请求,参与者需要确保所有操作都已完成,事务处于可以提交、可以回滚的状态。

b、提交阶段(Commit Phase):
协调者根据所有参与者的反馈结果,决定是否提交事务。
如果任何一个参与者不同意提交,则协调者向所有参与者发送回滚请求,全部参与者进行事务回滚,事务失败。
如果所有参与者都同意提交,则协调者向所有参与者发送提交请求,全部参与者进行事务提交,事务成功。

3、示例
假设我们有一个分布式数据库系统,包含两个数据库实例A和B,以及一个协调者C。

a、准备阶段:
客户端向协调者C发送事务请求。
协调者C向数据库A和B发送准备提交的请求。
数据库A和B检查本地事务状态,判断事务可以提交,确认所有操作已完成,并将结果反馈给协调者C。

b、提交阶段:
数据库A和B都反馈可以提交,协调者C向数据库A和B发送提交请求。
数据库A和B执行提交操作,更新数据并持久化。

二、三阶段提交3PC(Three-Phase Commit)
1、概述
顾名思义,三阶段提交就是分布式数据库系统,把事务的提交过程,分为三个阶段进行操作,可以解决两阶段提交协议2PC的阻塞问题。

2、流程
a、CanCommit阶段
协调者(Coordinator)收到事务请求。
协调者向所有参与者发送一个“CanCommit”请求。
参与者收到请求后,会根据本地状态,判断是否可以完成此事务,并将判断结果反馈给协调者。
如果参与者判断可以支持事务,则返回“就绪”(Ready);否则,则返回“中止”(Abort)。

b、PreCommit阶段
协调者收到所有参与者的确认消息后。
如果所有参与者都返回“就绪”(Ready),则协调者会向所有参与者发送“PreCommit”请求。
参与者收到请求后,会锁定其资源以防止其他事务干扰,需要确保所有操作都已完成,事务处于可以提交、可以回滚的状态,并返回确认消息给协调者。

如果任何一个参与者不同意提交,则协调者向所有参与者发送取消请求,事务失败。

c、DoCommit阶段
协调者收到所有参与者的确认消息后,如果所有参与者都表示准备好提交事务,则协调者会向所有参与者发送“DoCommit”请求。
参与者收到请求后,会真正提交事务,并返回确认消息给协调者。
协调者收到所有参与者的确认消息后,事务成功。

如果任何一个参与者不同意提交,则协调者向所有参与者发送回滚请求,全部参与者进行事务回滚,事务失败。

3、示例
假设我们有一个分布式数据库系统,包含两个数据库实例A和B,以及一个协调者C。

a、CanCommit阶段
协调者C收到事务请求。
协调者C向所有参与者发送一个“CanCommit”请求。
A判断可以支持事务,返回“就绪”(Ready)。
B判断可以支持事务,返回“就绪”(Ready)。

b、PreCommit阶段
参与者A和B都向协调者C反馈“就绪”,协调者C判断可以进行下一阶段。
C向A和B发送“PreCommit”请求。
A和B收到消息后,锁定资源,让事务处于可以提交、可以回滚的状态,并返回确认消息给协调者。

c、DoCommit阶段
协调者C收到所有参与者的确认消息。
C向参与者A和B发送“DoCommit”请求。
参与者A,提交事务,向C返回提交成功。
参与者B,提交事务,向C返回提交成功。
C收到所有参与者的提交成功消息后,事务完成。

三、事务补偿TCC(Try Confirm Cancel)
1、概述
在高并发场景下,2PC和3PC的效率根本无法满足要求,于是大家就考虑如何在应用层进行优化,而不要把压力都给到数据库呢,于是TCC应运而生。
TCC可以跨数据库类型、跨系统类型操作,而且灵活性强,效率高。
但TCC需要大量业务代码的改造,各服务需要实现Try、Confirm和Cancel接口,而且要求接口必须支持幂等操作,是一种入侵性比较强的一致性实现方式。

2、流程
TCC通常会将一个大的事务,拆分为多个子事务,并将事务的提交拆分为三个阶段:Try、Confirm 和 Cancel。
这里要注意,对于非严格一致的场景,可以通过MQ等方法异步解决问题,不要加入TCC。TCC应该只涉及到强一致性的各个服务。

a、Try阶段
在Try阶段,系统会进行业务检查,并预留资源。
比如:检查用户余额、检查库存,并暂时冻结资源。

b、Confirm阶段
确认所有业务服务的操作。
比如:扣余额,扣库存。

c、Cancel阶段
如果在Try或Confirm阶段有任何问题导致事务需要回滚,系统会执行Cancel阶段,释放之前预留的所有资源。
比如:释放余额、释放库存等。

3、示例
有一个电商平台,用户想要购买一个商品,这笔交易涉及到以下几个子业务:
用户账户扣款:需要确保账户余额足够,并能正确扣款。
库存服务扣减库存:确保商品库存足够,并能正确扣库存。
订单服务创建订单:记录交易的详细信息,并能设置为正确的状态。

a、Try阶段
客户下单,事务协调器(Transaction Coordinator)指示每个服务进行Try操作:
账户服务尝试扣款,但只是冻结资金,不会实际扣除。
库存服务标记商品库存为预留状态,不会实际减少库存。
订单服务准备创建订单,但不会实际创建。

b、Confirm阶段
如果事务协调器(Transaction Coordinator)收到所有Try操作都成功的消息,确认它们的操作:
账户服务将冻结的资金实际扣减。
库存服务将预留的库存实际扣减。
订单服务实际创建订单,并标记为已完成支付。
事务协调器(Transaction Coordinator)收到全部操作成功消息,此时可判断事务成功。

c、Cancel阶段
如果Try或Confirm的任意操作失败,事务协调器将指示每个服务撤销它们的操作:
账户服务将冻结的资金解冻。
库存服务将预留的库存重新释放。
订单服务放弃创建订单。
事务协调器(Transaction Coordinator)同时会判断,事务失败,并监督完成全部业务补偿操作。

分布式一致性算法08:PBFT

一、概述
上一节说了OM算法,但OM算法效率太低了,在实际工程上几乎无法使用。
1999年,Miguel Castro和Barbara Liskov提出了PBFT(Practical Byzantine Fault Tolerance)算法,大幅提高拜占庭容错算法的效率和实用性。
PBFT算法更好的平衡了效率和容错能力,在n个节点的网络中,同样可以允许的故障的节点数可以达到f =(n-1)/3个。

二、PBFT算法的工作原理
PBFT算法主要包括三个阶段:预准备(Pre-Prepare)、准备(Prepare)和提交(Commit)。
PBFT算法的复杂度为O(n^2),虽然消息数量还是很多,所以不适合大规模的网络,但比OM算法已经有大幅提升。

1、发起请求
客户端向主节点发起请求。

2、预准备阶段(Pre-Prepare)
主节点(Primary Node)收到消息后,为其分配一个视图消息编号vid,并广播预准备消息给所有副本节点。
广播消息格式为(Pre-Prepare,视图编号v,视图消息编号vid,请求内容msg,请求内容摘要msg-hash,时间有效区间T,主节点签名sign)

3、准备阶段(Prepare)
副本节点接收到Pre-Prepare预准备消息后,进行验证:
a、消息签名不正确,不通过
b、通过v和vid判断是否有相同编号消息,但不同内容的消息,不通过
c、超出时间有效期间T,不通过
d、msg与msg-hash不一致,不通过
e、通过
消息验证通过后,副本节点进入准备阶段,并进行消息广播
广播消息格式为(Prepare,视图编号v,视图消息编号vid,请求内容msg,请求内容摘要msg-hash,时间有效区间T,副本消息签名sign)

4、提交阶段(Commit)
每个节点在收到Prepare准备消息后,进行验证:
a、消息签名不正确,不通过
b、通过v和vid判断是否有相同编号消息,但不同内容的消息,不通过
c、超出时间有效期间T,不通过
d、msg与msg-hash与之前Pre-Prepare不一致,不通过
e、通过
每个节点在接收到足够数量的提交消息(通常是2f+1个)后,进入Commit阶段,并进行消息广播
广播消息格式为(Commit,视图编号v,视图消息编号vid,请求内容msg,请求内容摘要msg-hash,时间有效区间T,节点消息签名sign)

5、回复阶段
每个节点在收到Commit提交消息后,进行验证:
a、消息签名不正确,不通过
b、通过v和vid判断是否有相同编号消息,但不同内容的消息,不通过
c、超出时间有效期间T,不通过
d、msg与msg-hash与之前Prepare不一致,不通过
e、通过
节点收到(2f+1)条相同结果的Commit消息后,确认请求已成功执行,返回给客户端。

客户端收到(f+1)条相同结果的消息后,得到最终结果。
这里设置为f+1,因为节点最多收到f条一样的错误消息,不可能收到f+1条错误的消息。

三、主节点选举
如果主节点出现了异常,或则主节点故意作恶,PBFT是无法达成一致的。
此时,就要通过视图变更(View Change,类似于主节点任期的概念),选择新的主节点。

1、故障检测
当遇到以下情况时,备份节点会触发视图变更:
主节点,在约定时间内不响应客户端及备份节点请求
备份节点发送了准备消息后,在约定的时间内未接收到来自其他节点的2f个相同的准备消息。
备份节点发送了提交消息后,在约定的时间内未接收到来自其他节点的2f个相同的提交消息。
备份节点接收到异常消息,比如视图值、序号和已接受的消息相同,但内容摘要不同。

2、发起变更消息
触发视图变更的节点会向其他节点广播视图变更消息(View-Change消息)
这个消息包含了当前视图号v+1、序列号n(最后一个稳定的检查点的序列号)、以及已经准备好的消息集合P。

3、收集变更消息
其他节点在收到视图变更消息后,会检查消息的有效性。
如果消息有效,节点会记录该消息到本地日志中,并启动一个定时器,等待接收2f+1个视图变更消息来确认视图变更的成功。

4、选举主节点
当一个节点接收到2f+1个视图变更消息后,它会确认视图变更成功,并通过预定规则开始选举新的主节点:
(v + 1) mod |R|,其中v为当前视图的值,|R|为节点数选出下一个视图的主节点
节点启用新的视图号v+1,并将选举结果,广播通知其他节点。

5、广播NEW-VIEW消息
新的主节点接收到2f个其他节点的视图变更消息后,会广播一个NEW-VIEW消息,这个消息包含了上一个视图中所有未确认的请求信息。

6、处理未确认的请求
在新的视图中,副本节点需要处理在旧视图中未能达成一致的那些请求。
一旦新主节点广播了新视图消息,副本节点将执行这些请求,并进入提交阶段。

7、变更完毕,恢复正常

分布式一致性算法07:OM

一、概述
前面讲的几种一致性算法,比较适合封闭式网络,各节点都是可信的,也就是没有节点故意作恶。
但对于开放式网络,可以允许任意节点加入的时候,我们无法判定这些节点是否有恶意,也就是无法判定这些节点是否可信。
此时,我们就需要一种新的思路了:
当存在少数节点作恶( 消息可能被伪造) 场景下,其余节点如何达成一致性呢?
对于此种场景,最出名的就是拜占庭算法,而为了更快的理解拜占庭算法,我们要先解释一下拜占庭问题。

二、拜占庭将军问题(The Byzantine Generals Problem)
拜占庭问题又叫拜占庭将军问题,是Leslie Lamport在1982年提出用来解释一致性问题的一个虚构模型。
拜占庭是古代东罗马帝国的首都,由于地域宽广,守卫边境的多个将军(系统节点) 需要通过信使来传递消息,达成某些一致的决定。
但由于将军中可能存在叛徒(恶意节点),这些叛徒将努力向不同的将军发送不同的消息,试图会干扰一致性的达成。
拜占庭问题即为在此情况下,如何让忠诚的将军们能达成行动的一致。
(其实,还有一个隐含的限定,就是节点无法伪装为其他节点,实际中可以通过签名算法来达到这个目的)。

这样说可能有些抽象,我们举个例子:
比如一共有3个将军G1~G3,3个将军约定,少数服从多数。
G1、G2为忠诚的将军,G3是个叛徒
此时,G3有多种破坏方案,让G1和G2无法取得共识:

1、破坏方案A、否认提案内容,消息重放
比如:G1发起提案,G1要求明天进攻A城市,要把消息发给G2~G3

但G3先收到了消息,然后通过小道(比如更快的路由)向G2发送了“撤退”的提案
G2先收到了撤退的消息,后收到进攻的消息
G3向G1反馈进攻

结果:(G1被欺骗,G2被劫持)
由于只有G1发起了进攻,兵力不足,吃了败仗。
G3一口咬定,收到了G1要撤退的消息。

2、破坏方案B、故意发起错误提案
比如:G3发起提案,要把消息发给G1~G2。
但G3给G1的提案是进攻,给G2的提案时撤退

结果:(G1和G2不一致,但G1和G2认为彼此一致)
由于只有G1发起了进攻,兵力不足,吃了败仗。
G3一口咬定,发送了撤退的消息。

3、破坏方案C、故意传播相反的消息,操纵投票结果
比如:G1发起提案,G1要求明天进攻A城市,要把消息发给G2~G3。

此时:
G1希望进攻
G2希望撤退(比如节点有问题,无法接收事务提交)
G3是个叛徒,他给G1答复“进攻”,给G2答复“撤退”

结果:(G1和G2不一致,但G1和G2认为彼此一致)
G1收到了2票进攻,1票撤退,于是进攻
G2收到了1票进攻,2票撤退,于是撤退
G3是个叛徒,撤退
由于只有G1发起了进攻,兵力不足,吃了败仗。

三、Byzantine Fault Tolerant (BFT) 算法
对于上述问题,作者提出了一种BFT算法(OM算法),并证明了:
当叛变者为m,将军总人数不小于3m+1时,存在有效的算法,不论叛变者如何折腾,忠诚的将军们总能达成一致的结果。
或者,当将军总人数为n,当叛变者不大于(n-1)/3时(向下取整),存在有效的算法,不论叛变者如何折腾,忠诚的将军们总能达成一致的结果。

当恶意节点为m,节点总数必须不小于3m+1,可以这样证明:
在极端情况下:
a、m个正常节点下线。
b、m个恶意节点给出同一的恶意结论F。
c、此时必须至少有m+1个正常节点,给出正确结论T,才能保证系统得到结论T
因此节点总数,必须不小于3m+1

可见,能确保达成一致的拜占庭系统节点数至少为 4,允许出现1个坏的节点。如果叛变者过多,则无法保证能达到一致性。

四、OM算法过程
OM算法(Oral Message Algorithm)的核心思想是通过递归的方式,将命令从指挥官传递给下属,确保所有忠诚的下属最终都能接收到相同的命令。
OM算法复杂度为O(n^(m+1)),就是当有m个叛变者时,进行m+1轮递归通讯。
算法复杂度太大,在实际工程中无法应用。

1、初始化:
指挥官(通常是忠诚的将军)发送其命令给所有其他将军。
如果某个将军没有收到命令,则默认执行撤退命令。

2、递归传递:
每个将军在接收到命令后,会将该命令传递给其他未收到命令的将军。
如果某个将军接收到多个不同的命令,它会选择一个多数命令作为最终命令。

3、递归终止:
当所有将军都接收到相同的命令时,递归终止。
最终,所有忠诚的将军都会执行相同的命令。

具体步骤如下:
1、OM(0):(递归终止条件)
指挥官发送其命令给所有其他将军。
每个将军使用接收到的命令来做出决策。如果没有接收到命令,则使用默认命令(撤退)。

2、OM(x):(递归步骤:x=m、m-1、…、2、1):
指挥官发送其命令给所有其他n位将军。
对于每个将军i,如果它接收到命令,则将军i作为新的指挥官,执行OM(x-1),并将其收到的多数命令,传递给其他n-2位将军。

3、重复上述过程,所有忠诚的将军都会执行相同的命令。

Apple Intelligence三层模型结构

苹果在AI上很久没有实质性进展了:
Siri多年没有进步,停止了造车项目,解散了部分AI团队。
虽然陆续低调的进行了一些AI公司收购,但没有什么可称道的成果,实在算不上有什么进展。

今年WWDC上,终于发布了AI相关的内容,一如既往的“重新定义”了AI的概念:发明了一个新词Apple Intelligence,缩写还是AI。

咱们仔细看一下这个Apple Intelligence,还是动了一些脑筋的,整体架构分了三层:
1、首先是在移动设备端,运行了一个30亿参数的小模型,处理一些简单的任务(苹果自研芯片,让小模型可以在功耗可控的情况下,及时响应这些请求)
2、如果本地模型无法处理,就将请求发送到是云端,通过苹果自己的大模型,响应用户请求
3、如果任务太复杂,苹果自家模型处理不好,则将请求发送到合作伙伴提供的大模型,比如GPT-4o等,合作伙伴会不断增加
当然,对于用户的授权,和数据隐私保护,还是做了不少工作的

这样乍一看,好像没有什么吗,就是集成了多个模型。但咱们加上一个事实后,这个事情就不这么简单了:
苹果对自己的操作系统完全可控,就让本地模型可以获取比竞争对手高的多的权限。
苹果自家模型,可以读邮件、可以看日程、可以访问通讯记录、可以查看网页浏览记录,可以搜集全部图像。。。
也就是说,苹果的自家模型,可以高效收集客户设备上所有信息。
同样的,苹果自家模型,可以调用用户设备全部的功能,包括第三方APP的功能。
通过整合这些信息,就可以让苹果自家模型,吊打全部竞争对手。

细思极恐,在移动小模型上,在IOS设备上,几乎已经没有了任何生存空间。
如果Google也在安卓上,部署自己的小模型,那安卓设备上的机会,也就不存在了。
无论Google如何选择,国内厂商必然快速跟进,那手机小模型这个赛道很快就不存在了。
而第三方的移动小模型和应用,无论如何努力,由于无法控制操作系统底层,几乎不可能形成任何竞争优势,几乎必然出局。

可以看下,现在国内大模型赛道整体太卷了,小厂商几乎没有机会:
1、大模型的研发、训练,需要大量的资金、人员、算力、数据的投入,小厂玩不起,大厂不赚钱
2、开源大模型的性能,比闭源大模型并不差太多,而且也在疯狂迭代,没有商业模式,更没有资本愿意长期投入,小厂更玩不起
3、小厂在垂直赛道可能会有些机会,但如果市场足够大,被大厂嗅到,没有赚钱途径的大厂一定会下场卷死你
4、移动端小模型,上面也说了,没有操作系统权限,小厂几乎没有机会了
5、在APP创新上,国内互联网流量过于集中,应用开发出来只能依附于几个大流量平台。这些平台不会允许某几个应用过热,而且在有了热度后,大厂还会无良的抄小厂的作业,让某类APP瞬间消失

所以很可惜,虽然大家都知道大模型是个好东西。但国内环境太卷了:
没有给小厂的生态位,没有好的生态
就不会有大量的创新,后面难以出现百花齐放的场景
到头来,还是要等别人创新后,大厂去抄?
大家都懂,但停不下来。
卷来卷去,难有赢家。

好像扯远了。。。
其实,对于苹果,其实还有两个事情做的挺到位的
1、将prompt屏蔽了,让普通人可以更便捷的使用AI
2、再次发挥,强大的整合能力,提前抢占了移动AI的入口

当然,对于个人来说,用好大模型,提高自己获取知识的速度,提升自己的认知圈,扩展自己的能力边界,还是很重要的。

GoLang实现跨平台的一些技巧03

以新建文件为例,对比一下几个常见平台的区别。

继续看下MacOS平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_unix.go

// openFileNolog的unix实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	setSticky := false
	if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
		if _, err := Stat(name); IsNotExist(err) {
			setSticky = true
		}
	}

	var r int
	var s poll.SysFile
	for {
		var e error
		//跳转到open
		r, s, e = open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
		if e == nil {
			break
		}

		// We have to check EINTR here, per issues 11180 and 39237.
		if e == syscall.EINTR {
			continue
		}

		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// open(2) itself won't handle the sticky bit on *BSD and Solaris
	if setSticky {
		setStickyBit(name)
	}

	// There's a race here with fork/exec, which we are
	// content to live with. See ../syscall/exec_unix.go.
	if !supportsCloseOnExec {
		syscall.CloseOnExec(r)
	}

	kind := kindOpenFile
	if unix.HasNonblockFlag(flag) {
		kind = kindNonBlock
	}

	// 封装为File结构
	f := newFile(r, name, kind)
	f.pfd.SysFile = s
	return f, nil
}
// os/file_open_unix.go

func open(path string, flag int, perm uint32) (int, poll.SysFile, error) {
	// 跳转到syscall.Open
	fd, err := syscall.Open(path, flag, perm)
	return fd, poll.SysFile{}, err
}
// syscall/zsyscall_darwin_amd64.go

func Open(path string, mode int, perm uint32) (fd int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil {
		return
	}
	// 调用syscall
	r0, _, e1 := syscall(abi.FuncPCABI0(libc_open_trampoline), uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
// syscall/syscall_darwin.go
func syscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

// internal/abi/funcpc.go
func FuncPCABI0(f interface{}) uintptr

// syscall/zsyscall_darwin_amd64.go
func libc_open_trampoline()
//go:cgo_import_dynamic libc_open open "/usr/lib/libSystem.B.dylib"

// 先通过abi.FuncPCABI0(libc_open_trampoline)先获取到open函数的地址
// 然后通过syscall调用open函数
// open函数是libc标准库中的函数,C语言定义为
int open(const char *pathname, int flags, mode_t mode);
//syscall在这里实现

//runtime/sys_darwin.go
//go:linkname syscall_syscall syscall.syscall
//go:nosplit
func syscall_syscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) {
	args := struct{ fn, a1, a2, a3, r1, r2, err uintptr }{fn, a1, a2, a3, r1, r2, err}
	entersyscall()
	//跳转到libcCall
	libcCall(unsafe.Pointer(abi.FuncPCABI0(syscall)), unsafe.Pointer(&args))
	exitsyscall()
	return args.r1, args.r2, args.err
}
func syscall()

// runtime/sys_libc.go
func libcCall(fn, arg unsafe.Pointer) int32 {
	// Leave caller's PC/SP/G around for traceback.
	gp := getg()
	var mp *m
	if gp != nil {
		mp = gp.m
	}
	if mp != nil && mp.libcallsp == 0 {
		mp.libcallg.set(gp)
		mp.libcallpc = getcallerpc()
		// sp must be the last, because once async cpu profiler finds
		// all three values to be non-zero, it will use them
		mp.libcallsp = getcallersp()
	} else {
		// Make sure we don't reset libcallsp. This makes
		// libcCall reentrant; We remember the g/pc/sp for the
		// first call on an M, until that libcCall instance
		// returns.  Reentrance only matters for signals, as
		// libc never calls back into Go.  The tricky case is
		// where we call libcX from an M and record g/pc/sp.
		// Before that call returns, a signal arrives on the
		// same M and the signal handling code calls another
		// libc function.  We don't want that second libcCall
		// from within the handler to be recorded, and we
		// don't want that call's completion to zero
		// libcallsp.
		// We don't need to set libcall* while we're in a sighandler
		// (even if we're not currently in libc) because we block all
		// signals while we're handling a signal. That includes the
		// profile signal, which is the one that uses the libcall* info.
		mp = nil
	}
	// 跳转到asmcgocall
	res := asmcgocall(fn, arg)
	if mp != nil {
		mp.libcallsp = 0
	}
	return res
}

// 硬件平台相关代码
// runtime/asm_arm64.s
// func asmcgocall(fn, arg unsafe.Pointer) int32
// Call fn(arg) on the scheduler stack,
// aligned appropriately for the gcc ABI.
// See cgocall.go for more details.
TEXT ·asmcgocall(SB),NOSPLIT,$0-20
	MOVD	fn+0(FP), R1
	MOVD	arg+8(FP), R0

	MOVD	RSP, R2		// save original stack pointer
	CBZ	g, nosave
	MOVD	g, R4

	// Figure out if we need to switch to m->g0 stack.
	// We get called to create new OS threads too, and those
	// come in on the m->g0 stack already. Or we might already
	// be on the m->gsignal stack.
	MOVD	g_m(g), R8
	MOVD	m_gsignal(R8), R3
	CMP	R3, g
	BEQ	nosave
	MOVD	m_g0(R8), R3
	CMP	R3, g
	BEQ	nosave

	// Switch to system stack.
	MOVD	R0, R9	// gosave_systemstack_switch<> and save_g might clobber R0
	BL	gosave_systemstack_switch<>(SB)
	MOVD	R3, g
	BL	runtime·save_g(SB)
	MOVD	(g_sched+gobuf_sp)(g), R0
	MOVD	R0, RSP
	MOVD	(g_sched+gobuf_bp)(g), R29
	MOVD	R9, R0

	// Now on a scheduling stack (a pthread-created stack).
	// Save room for two of our pointers /*, plus 32 bytes of callee
	// save area that lives on the caller stack. */
	MOVD	RSP, R13
	SUB	$16, R13
	MOVD	R13, RSP
	MOVD	R4, 0(RSP)	// save old g on stack
	MOVD	(g_stack+stack_hi)(R4), R4
	SUB	R2, R4
	MOVD	R4, 8(RSP)	// save depth in old g stack (can't just save SP, as stack might be copied during a callback)
	BL	(R1)
	MOVD	R0, R9

	// Restore g, stack pointer. R0 is errno, so don't touch it
	MOVD	0(RSP), g
	BL	runtime·save_g(SB)
	MOVD	(g_stack+stack_hi)(g), R5
	MOVD	8(RSP), R6
	SUB	R6, R5
	MOVD	R9, R0
	MOVD	R5, RSP

	MOVW	R0, ret+16(FP)
	RET

nosave:
	// Running on a system stack, perhaps even without a g.
	// Having no g can happen during thread creation or thread teardown
	// (see needm/dropm on Solaris, for example).
	// This code is like the above sequence but without saving/restoring g
	// and without worrying about the stack moving out from under us
	// (because we're on a system stack, not a goroutine stack).
	// The above code could be used directly if already on a system stack,
	// but then the only path through this code would be a rare case on Solaris.
	// Using this code for all "already on system stack" calls exercises it more,
	// which should help keep it correct.
	MOVD	RSP, R13
	SUB	$16, R13
	MOVD	R13, RSP
	MOVD	$0, R4
	MOVD	R4, 0(RSP)	// Where above code stores g, in case someone looks during debugging.
	MOVD	R2, 8(RSP)	// Save original stack pointer.
	BL	(R1)
	// Restore stack pointer.
	MOVD	8(RSP), R2
	MOVD	R2, RSP
	MOVD	R0, ret+16(FP)
	RET

// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(fd int, name string, kind newFileKind) *File {
	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         fd,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name:        name,
		stdoutOrErr: fd == 1 || fd == 2,
	}}

	pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock

	// If the caller passed a non-blocking filedes (kindNonBlock),
	// we assume they know what they are doing so we allow it to be
	// used with kqueue.
	if kind == kindOpenFile {
		switch runtime.GOOS {
		case "darwin", "ios", "dragonfly", "freebsd", "netbsd", "openbsd":
			var st syscall.Stat_t
			err := ignoringEINTR(func() error {
				return syscall.Fstat(fd, &st)
			})
			typ := st.Mode & syscall.S_IFMT
			// Don't try to use kqueue with regular files on *BSDs.
			// On FreeBSD a regular file is always
			// reported as ready for writing.
			// On Dragonfly, NetBSD and OpenBSD the fd is signaled
			// only once as ready (both read and write).
			// Issue 19093.
			// Also don't add directories to the netpoller.
			if err == nil && (typ == syscall.S_IFREG || typ == syscall.S_IFDIR) {
				pollable = false
			}

			// In addition to the behavior described above for regular files,
			// on Darwin, kqueue does not work properly with fifos:
			// closing the last writer does not cause a kqueue event
			// for any readers. See issue #24164.
			if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && typ == syscall.S_IFIFO {
				pollable = false
			}
		}
	}

	clearNonBlock := false
	if pollable {
		if kind == kindNonBlock {
			// The descriptor is already in non-blocking mode.
			// We only set f.nonblock if we put the file into
			// non-blocking mode.
		} else if err := syscall.SetNonblock(fd, true); err == nil {
			f.nonblock = true
			clearNonBlock = true
		} else {
			pollable = false
		}
	}

	// An error here indicates a failure to register
	// with the netpoll system. That can happen for
	// a file descriptor that is not supported by
	// epoll/kqueue; for example, disk files on
	// Linux systems. We assume that any real error
	// will show up in later I/O.
	// We do restore the blocking behavior if it was set by us.
	if pollErr := f.pfd.Init("file", pollable); pollErr != nil && clearNonBlock {
		if err := syscall.SetNonblock(fd, false); err == nil {
			f.nonblock = false
		}
	}

	runtime.SetFinalizer(f.file, (*file).close)
	return f
}

GoLang实现跨平台的一些技巧02

以新建文件为例,对比一下几个常见平台的区别。

继续看下Linux平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_unix.go

// openFileNolog的unix实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	setSticky := false
	if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
		if _, err := Stat(name); IsNotExist(err) {
			setSticky = true
		}
	}

	var r int
	var s poll.SysFile
	for {
		var e error
		//跳转到open
		r, s, e = open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
		if e == nil {
			break
		}

		// We have to check EINTR here, per issues 11180 and 39237.
		if e == syscall.EINTR {
			continue
		}

		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// open(2) itself won't handle the sticky bit on *BSD and Solaris
	if setSticky {
		setStickyBit(name)
	}

	// There's a race here with fork/exec, which we are
	// content to live with. See ../syscall/exec_unix.go.
	if !supportsCloseOnExec {
		syscall.CloseOnExec(r)
	}

	kind := kindOpenFile
	if unix.HasNonblockFlag(flag) {
		kind = kindNonBlock
	}

	// 封装为File结构
	f := newFile(r, name, kind)
	f.pfd.SysFile = s
	return f, nil
}
// os/file_open_unix.go

func open(path string, flag int, perm uint32) (int, poll.SysFile, error) {
	// 跳转到syscall.Open
	fd, err := syscall.Open(path, flag, perm)
	return fd, poll.SysFile{}, err
}
// syscall/syscall_linux.go

func Open(path string, mode int, perm uint32) (fd int, err error) {
	// 跳转到openat
	return openat(AT_FDCWD, path, mode|O_LARGEFILE, perm)
}

//sys	openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)

// syscall/zsyscall_linux_amd64.go

func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil {
		return
	}
	// 跳转到Syscall6
	r0, _, e1 := Syscall6(SYS_OPENAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags), uintptr(mode), 0, 0)
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
// syscall/syscall_linux.go

func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) {
	runtime_entersyscall()
	// 跳转到RawSyscall6
	r1, r2, err = RawSyscall6(trap, a1, a2, a3, a4, a5, a6)
	runtime_exitsyscall()
	return
}

// N.B. RawSyscall6 is provided via linkname by runtime/internal/syscall.
//
// Errno is uintptr and thus compatible with the runtime/internal/syscall
// definition.
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

// syscall/zsysnum_linux_amd64.go
	SYS_OPENAT                 = 257

// RawSyscall6是通过汇编实现的,传入SYS_OPENAT,最终调用openat函数
// openat函数是libc标准库中的函数,C语言定义为
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
// runtime/internal/syscall/asm_linux_amd64.s

// Syscall6 的实现在这里
// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
//
// We need to convert to the syscall ABI.
//
// arg | ABIInternal | Syscall
// ---------------------------
// num | AX          | AX
// a1  | BX          | DI
// a2  | CX          | SI
// a3  | DI          | DX
// a4  | SI          | R10
// a5  | R8          | R8
// a6  | R9          | R9
//
// r1  | AX          | AX
// r2  | BX          | DX
// err | CX          | part of AX
//
// Note that this differs from "standard" ABI convention, which would pass 4th
// arg in CX, not R10.
TEXT ·Syscall6<ABIInternal>(SB),NOSPLIT,$0
	// a6 already in R9.
	// a5 already in R8.
	MOVQ	SI, R10 // a4
	MOVQ	DI, DX  // a3
	MOVQ	CX, SI  // a2
	MOVQ	BX, DI  // a1
	// num already in AX.
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok
	NEGQ	AX
	MOVQ	AX, CX  // errno
	MOVQ	$-1, AX // r1
	MOVQ	$0, BX  // r2
	RET
ok:
	// r1 already in AX.
	MOVQ	DX, BX // r2
	MOVQ	$0, CX // errno
	RET

// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(fd int, name string, kind newFileKind) *File {
	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         fd,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name:        name,
		stdoutOrErr: fd == 1 || fd == 2,
	}}

	pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock

	// If the caller passed a non-blocking filedes (kindNonBlock),
	// we assume they know what they are doing so we allow it to be
	// used with kqueue.
	if kind == kindOpenFile {
		switch runtime.GOOS {
		case "darwin", "ios", "dragonfly", "freebsd", "netbsd", "openbsd":
			var st syscall.Stat_t
			err := ignoringEINTR(func() error {
				return syscall.Fstat(fd, &st)
			})
			typ := st.Mode & syscall.S_IFMT
			// Don't try to use kqueue with regular files on *BSDs.
			// On FreeBSD a regular file is always
			// reported as ready for writing.
			// On Dragonfly, NetBSD and OpenBSD the fd is signaled
			// only once as ready (both read and write).
			// Issue 19093.
			// Also don't add directories to the netpoller.
			if err == nil && (typ == syscall.S_IFREG || typ == syscall.S_IFDIR) {
				pollable = false
			}

			// In addition to the behavior described above for regular files,
			// on Darwin, kqueue does not work properly with fifos:
			// closing the last writer does not cause a kqueue event
			// for any readers. See issue #24164.
			if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && typ == syscall.S_IFIFO {
				pollable = false
			}
		}
	}

	clearNonBlock := false
	if pollable {
		if kind == kindNonBlock {
			// The descriptor is already in non-blocking mode.
			// We only set f.nonblock if we put the file into
			// non-blocking mode.
		} else if err := syscall.SetNonblock(fd, true); err == nil {
			f.nonblock = true
			clearNonBlock = true
		} else {
			pollable = false
		}
	}

	// An error here indicates a failure to register
	// with the netpoll system. That can happen for
	// a file descriptor that is not supported by
	// epoll/kqueue; for example, disk files on
	// Linux systems. We assume that any real error
	// will show up in later I/O.
	// We do restore the blocking behavior if it was set by us.
	if pollErr := f.pfd.Init("file", pollable); pollErr != nil && clearNonBlock {
		if err := syscall.SetNonblock(fd, false); err == nil {
			f.nonblock = false
		}
	}

	runtime.SetFinalizer(f.file, (*file).close)
	return f
}

GoLang实现跨平台的一些技巧01

最近在读GoLang的源码,源码中有一些跨平台的操作,Go处理的很有意思,在这整理一下。

以新建文件为例,对比一下几个常见平台的区别。

首先看下Windows平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_windows.go

// openFileNolog的windows实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	if name == "" {
		return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT}
	}
	path := fixLongPath(name)
	// 跳转到了syscall.Open
	r, e := syscall.Open(path, flag|syscall.O_CLOEXEC, syscallMode(perm))
	if e != nil {
		// We should return EISDIR when we are trying to open a directory with write access.
		if e == syscall.ERROR_ACCESS_DENIED && (flag&O_WRONLY != 0 || flag&O_RDWR != 0) {
			pathp, e1 := syscall.UTF16PtrFromString(path)
			if e1 == nil {
				var fa syscall.Win32FileAttributeData
				e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
				if e1 == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
					e = syscall.EISDIR
				}
			}
		}
		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// 封装为File结构
	f, e := newFile(r, name, "file"), nil
	if e != nil {
		return nil, &PathError{Op: "open", Path: name, Err: e}
	}
	return f, nil
}
// syscall/syscall_windows.go

func Open(path string, mode int, perm uint32) (fd Handle, err error) {
	if len(path) == 0 {
		return InvalidHandle, ERROR_FILE_NOT_FOUND
	}
	pathp, err := UTF16PtrFromString(path)
	if err != nil {
		return InvalidHandle, err
	}
	var access uint32
	switch mode & (O_RDONLY | O_WRONLY | O_RDWR) {
	case O_RDONLY:
		access = GENERIC_READ
	case O_WRONLY:
		access = GENERIC_WRITE
	case O_RDWR:
		access = GENERIC_READ | GENERIC_WRITE
	}
	if mode&O_CREAT != 0 {
		access |= GENERIC_WRITE
	}
	if mode&O_APPEND != 0 {
		access &^= GENERIC_WRITE
		access |= FILE_APPEND_DATA
	}
	sharemode := uint32(FILE_SHARE_READ | FILE_SHARE_WRITE)
	var sa *SecurityAttributes
	if mode&O_CLOEXEC == 0 {
		sa = makeInheritSa()
	}
	var createmode uint32
	switch {
	case mode&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL):
		createmode = CREATE_NEW
	case mode&(O_CREAT|O_TRUNC) == (O_CREAT | O_TRUNC):
		createmode = CREATE_ALWAYS
	case mode&O_CREAT == O_CREAT:
		createmode = OPEN_ALWAYS
	case mode&O_TRUNC == O_TRUNC:
		createmode = TRUNCATE_EXISTING
	default:
		createmode = OPEN_EXISTING
	}
	var attrs uint32 = FILE_ATTRIBUTE_NORMAL
	if perm&S_IWRITE == 0 {
		attrs = FILE_ATTRIBUTE_READONLY
		if createmode == CREATE_ALWAYS {
			// We have been asked to create a read-only file.
			// If the file already exists, the semantics of
			// the Unix open system call is to preserve the
			// existing permissions. If we pass CREATE_ALWAYS
			// and FILE_ATTRIBUTE_READONLY to CreateFile,
			// and the file already exists, CreateFile will
			// change the file permissions.
			// Avoid that to preserve the Unix semantics.
			h, e := CreateFile(pathp, access, sharemode, sa, TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
			switch e {
			case ERROR_FILE_NOT_FOUND, _ERROR_BAD_NETPATH, ERROR_PATH_NOT_FOUND:
				// File does not exist. These are the same
				// errors as Errno.Is checks for ErrNotExist.
				// Carry on to create the file.
			default:
				// Success or some different error.
				return h, e
			}
		}
	}
	if createmode == OPEN_EXISTING && access == GENERIC_READ {
		// Necessary for opening directory handles.
		attrs |= FILE_FLAG_BACKUP_SEMANTICS
	}
	if mode&O_SYNC != 0 {
		const _FILE_FLAG_WRITE_THROUGH = 0x80000000
		attrs |= _FILE_FLAG_WRITE_THROUGH
	}

	// 跳转CreateFile
	return CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
}


func CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile int32) (handle Handle, err error) {
	// 跳转Syscall9
	r0, _, e1 := Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0)
	handle = Handle(r0)
	if handle == InvalidHandle {
		err = errnoErr(e1)
	}
	return
}
// syscall/dll_windows.go
// 封装了Syscall9
func Syscall9(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)

// syscall/zsyscall_windows.go
// Syscall9中传入的API名为procCreateFileW 
procCreateFileW                        = modkernel32.NewProc("CreateFileW")

// 实际上最终调用了windows API CreateFileW,下面是CPP版本的API定义
// 到这里,也可以看到,通过Syscall的定义,比较巧妙的做了一定程度上的解耦
HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);
// runtime/syscall_windows.go

// Syscall9是在这里实现的
//go:linkname syscall_Syscall9 syscall.Syscall9
//go:nosplit
func syscall_Syscall9(fn, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, err uintptr) {
	return syscall_SyscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9)
}

//go:linkname syscall_SyscallN syscall.SyscallN
//go:nosplit
func syscall_SyscallN(trap uintptr, args ...uintptr) (r1, r2, err uintptr) {
	nargs := len(args)

	// asmstdcall expects it can access the first 4 arguments
	// to load them into registers.
	var tmp [4]uintptr
	switch {
	case nargs < 4:
		copy(tmp[:], args)
		args = tmp[:]
	case nargs > maxArgs:
		panic("runtime: SyscallN has too many arguments")
	}

	lockOSThread()
	defer unlockOSThread()
	c := &getg().m.syscall
	c.fn = trap
	c.n = uintptr(nargs)
	c.args = uintptr(noescape(unsafe.Pointer(&args[0])))
	cgocall(asmstdcallAddr, unsafe.Pointer(c))
	return c.r1, c.r2, c.err
}

// 最后,通过cgocall,将go的调用,转换为c的调用
// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(h syscall.Handle, name string, kind string) *File {
	if kind == "file" {
		var m uint32
		if syscall.GetConsoleMode(h, &m) == nil {
			kind = "console"
		}
		if t, err := syscall.GetFileType(h); err == nil && t == syscall.FILE_TYPE_PIPE {
			kind = "pipe"
		}
	}

	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         h,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name: name,
	}}
	runtime.SetFinalizer(f.file, (*file).close)

	// Ignore initialization errors.
	// Assume any problems will show up in later I/O.
	f.pfd.Init(kind, false)

	return f
}

分布式一致性算法06:Gossip

Gossip是一个最终一致性协议,适用于大规模的、弱一致性的、去中心化的场景。

为了达到最终一致性,Gossip实际上提供了三种同步方式:Direct Mail(直接邮寄)、Rumor Mongering(谣言传播)及 Anti-Entropy(反熵)。看起来都是新技术名词,但分开来看,却都十分简单。

1、Direct Mail(直接邮寄,增量)
通俗解释就是,当一个节点收到客户端的新信息后,就把这个新信息传递给系统内的每个节点。
Direct Mail功能实现简单、效率也很高。

但在一个开放性的大规模非中心化网络中,经常会出现节点的变化(增加、掉线、宕机),这种场景下,仅靠Direct Mail,是不可能实现最终一致性的。
比如,节点X宕机了1一小时,然后启动。这一小时中的数据就丢失了。虽然在技术上,我们可以将信息做一些缓存,但在一个开放网络里,管理每个节点是否接收并处理好自己发送的全部消息,这本身就是个技术难题,而且效率将会及其低下。

2、Anti-Entropy(反熵,全量)
通俗解释就是,一个节点,定期会选择一些节点,对比数据的差异,并相互修复缺失的数据。
同步方式,可以是推送、拉取、连推带拉。
Anti-Entropy会比较整个数据库的异同,是达成最终一致性的最后手段。

Anti-Entropy消息以固定的概率传播全量的数据。
所有节点只有两种状态:Suspective(病原)、Infective(感染),也被称作simple epidemics(SI model)。
S节点会把所有的数据都跟I节点共享,以便消除节点之间数据的任何不一致,它可以保证最终、完全的一致。

但是,在一个开放性的大规模非中心化网络中,定期同步全量数据,将会带来巨大的资源消耗。
所以这个操作的频率,必须足够低,否则整个网络就不用做其他事情了。
注:在实际工程落地中,为了加快数据同步效率,并不一定会“随机”选择同步节点,而是会想办法,用一定的顺序,尽快让全部节点完成同步。

聪明的你一定会发现,通过Direct Mail和Anti-Entropy,已经可以实现最终一致性的效果了。
但Direct Mail无法保证成功,Anti-Entropy无法保证频率,我们需要寻找额外的同步方案,在消耗尽量少资源的前提下,让整个网络的的可用性大幅提升。

3、Rumor Mongering(谣言传播,增量)
通俗解释就是,当一个节点收到新消息后,随机挑选N个节点,把新消息推送给这些节点。这N的节点在收到消息后,又会分别随机选择N个节点,推送新消息。
同步方式,同样可以是推送、拉取、连推带拉。

Rumor Mongering消息以固定的概率传播增量数据。
所有节点有三种状态:Suspective(病原)、Infective(感染)、Removed(愈除)。也被称作complex epidemics(SIR model)。
S节点只会把追加消息发送给随机选择的I节点。而这个消息在某个时间点之后会被标记为Removed,并且不再被传播。
根据六度分隔理论,经过几轮随机推送,可以基本确保每个节点都收到了新消息。但部分特殊节点仍有可能并未收到所有的追加消息。
所以,通过Direct Mail和Rumor Mongering并无法保证达到最终一致性。

聪明的你一定会发现,这一个信息,会被多次重复推送,一个节点也会重复接收。这其实是一个实现复杂度和性能之间的一个均衡。
和协议的名字相似,风言风语,口口相传,很快全村就都知道了。

4、新节点加入怎么处理
当一个节点加入网络后,会先使用Anti-Entropy的拉取方式,获取一个相对比较新的数据库。
然后就可以通过Direct Mail、Rumor Mongering获取新数据啦。
最后,还有Anti-Entropy,定期全量对比更新数据,这样新节点加入后,网络很快就能达到一致性了。

可见,Gossip协议,原理很简单,实现也并不复杂。虽然有一定程度的通讯浪费,但对于开放性的大规模非中心化网络中,Gossip协议很好的平衡了可用性、性能、工程复杂度之间的关系,实际中也获得了不少项目的青睐。

分布式一致性算法05:NWR

分布式一致性算法05 NWR协议

一、基本概念
NWR模型是一种强一致性算法,它巧妙的利用了N(备份数)、W(写入成功数)、R(读取成功数)之间的关系(W+R>N),从而达到一致性的要求。

其中:
N(备份数):系统中备份的总数。
W(写入成功数):执行写操作时,需要写入成功的最小备份数量。
R(读取成功数):执行读操作时,需要查询成功的最小备份数量。

为了保证一致性,必须满足以下条件:
W+R>N
这个不等式确保了在执行读操作时,至少有一个最新的备份被读取到,从而避免了读到过时的数据。

二、相关角色
NWR协议中,所有的节点是一样的。

三、算法流程
1、客户端请求写入
各节点收到消息,写入成功后,返回写入成功
当客户端收到W以上个写入成功后,认为写入成功
2、客户端读取请求
个节点收到读取消息,返回结果
当客户端收到R个以上的结果后,直接使用最新版本的数据即可。

由于W+R>N,所以客户端至少可以读取到一份最新的数据,不会读取到历史版本。

四、举例说明
假设我们有一个分布式系统,其中有5个节点,即N=5。
要求确保写操作在至少3个节点上成功(W=3),并且读操作至少查询3个节点(R=3)。

1、写操作
客户端向系统发送一个写请求,比如更新某个键值对。

2、写操作的传播
写请求被发送到所有5个备份节点,等待至少3个节点确认写操作成功。

3、写操作的确认
假设有3个节点成功更新了数据,满足了W=3的要求,写操作被认为是成功的。

4、读操作
客户端发送一个读请求,希望获取最新的键值对数据。

5、读操作的数据收集
系统从5个备份节点中的任意3个节点获取数据,由于W+R>N(3+3>5),至少有一个节点上的数据是最新的。

6、结果的确认
客户端收到来自3个节点的响应,可能会发现不同节点的数据版本不同。
客户端选择版本号最高的数据使用即可。

五、NWR的优点
1、实现简单
2、在NWR体系下,无需等待所有节点都写入成功,即可判定数据更新成功,而且保证可以读取到最新数据,提升了系统的吞吐量,同时系统的可用性也有较大提升。
3、即使部分节点宕机,只要能保证W+R>N,系统还是处于可运行状态,比如
N=5,W=3,R=3
即使宕掉2个节点,仍然可以保证运行,只不过系统退化为了强C系统。

六、NWR突破了CAP的限制吗?
并没有哦,其实我们挑战一下NWR的设置就可以看懂了(先不考虑P,我们讨论一下CA)
当W=N、R=1的时候,其实就是写入时,牺牲了A,保证了C(节点都一致)
当W=1、R=N,其实就是写入时,保证了A,牺牲了C(节点都不一致)