Zhuang's Diary

言之有物,持之以恒

本文内容比原文略微变更:

https://mp.weixin.qq.com/s/kTunEa9AdO3mTlBp0Np5-g

3月15日,DRC智能合约账户遭受黑客攻击,导致账户内的DRC Token被盗。DRC开发人员及时对该黑客钱包地址中的DRC Token进行了锁定,这部分代币将无法被转出。所有DRC Token的持有者不会因此次事件而遭受经济损失。

在全面分析本次黑客攻击事件后,决定采取封禁原有代币及合约,重新部署新代币及智能合约。此前接收过DRC Token的用户(3,543 addresses),DRC将在部署新的代币合约后,按原区块链浏览器上的地址和代币数重新打币。原接收的DRC Token销毁后将作废。

在分析黑客行为的过程中,发现该黑客在过去一年时间内对多个区块链项目进行持续的偷盗,累计金额达2000万美金。尽管有多位受害曾经通过不同媒体发布相关报道,但黑客行动仍在继续。因此在此提醒区块链项目的开发团队能够引以为鉴,将代码合约安全放在首位。

漏洞解析

此次遭遇的安全漏洞,实际上在早在2015年Ethereum 团队就给出过安全提醒,一年前My Ether Wallet的Founder也在etherscan的这个黑客地址下留言解释过,并给出了建议。

https://blog.ethereum.org/2015/08/29/security-alert-insecurely-configured-geth-can-make-funds-remotely-accessible/

下面这个链接是以太坊官方reddit论坛上关于这个漏洞产生的实际问题的讨论:

https://www.reddit.com/r/ethereum/comments/4z0mvi/ethereum_nodes_with_insecure_rpc_settings_are/

根据以太坊官方论坛讨论的内容以及我们对后台log的分析,这个安全漏洞的主要原理如下:

  1. 使用geth或者parity客户端在本机创建一个全节点来同步以太坊区块,同时提供HTTP RPC API 方便其他地方的机器通过geth或者parity客户端访问,这样其他地方的geth或者parity不用重复的在本地同步区块。通常HTTP RPC API的访问是通过首先连接 xxx.xxx.xxx.xxx:8545 这个URL,然后就能访问诸如“eth_accounts”,”eth_sendTransaction”等API了。 8545是geth开启RPC服务的默认端口,xxx.xxx.xxx.xxx是这个全节点的可被外网访问的IP地址。

2.这样做有安全漏洞,原因是黑客也可以连接这个服务,调用”eth_xxxxx”等API。举例来说,他会做下面三步操作:

1)首先call eth_getBlockByNumber(“0x00”, false),因为本机同步了所有区块,所以这个调用可以得到本地同步的区块高度。

2)接下来调用eth_accounts API,可以通过这个API得到当前本机节点导入的所有以太坊账户的地址列表。

3) 然后通过所有账户地址对外发送eth_sendTransaction 调用,这个API调用可以发送该账户地址下的所有数字资产到一个指定的以太坊地址。

以上这三步可以不停的重复,几乎可以达到每秒调用一次,这样就有可能正好碰上本机用户对自己的钱包地址执行unlockAccount方法,这个方法将使用自己的密码从本地的keystore里获取private key对要发起的交易进行签名,从而成功交易。而unlock状态会保持一个短暂的时间,也就是在这个短暂的时间段内,用户不用再次输入密码为交易签名,在这个时间段内所有发起的交易都可以自动签名,因此,在这个时间段内如果恰好黑客用这个地址所发起的eth_sendTransaction调用被执行到,那么黑客就成功的获取用户的签名权限并将该钱包地址上的数字资产转走。而且一旦交易成功,黑客应该能通过某种方式获得整个签名加密的信息,从而完全控制这个账户,只要这个账户仍然在geth上连接,黑客就能一直用这个账户地址发起交易并签名成功,直到这个账户退出geth客户端。

这就是通过HTTP RPC API发动黑客攻击的具体过程。如果要想避免这种行为带来的危害,首先,不要用默认端口(8545)开启HTTP RPC服务,也不要对Internet全网开放。但即使这样做了,黑客仍然有可能通过别的方法获取到端口或者IP地址成功的连上RPC服务,那么这就要求全节点使用者在对外提供HTTP RPC API的时候,一定不能使用unlockAccount方法来解锁本地的账户地址,而要用其他方式进行签名交易,比如每次通过调用web3接口的函数发起交易的时候,使用密码文件作为参数在函数调用语句中签名,或调用web3提供的Sign函数接口去签名交易。unlockAccount方法只能在测试环境或者是不提供HTTP RPC API服务的主机上使用。

这个漏洞对geth提供的WS API应该也是有效的,因为WS服务也可以对外公开。

另外,根据这次教训,有一条原则就是拥有大量数字代币的账户一定不要用来作为在主网上调用各种接口和智能合约的主体,因为一旦遭受攻击就会损失大量代币。应该使用其他独立的账户地址做这些事情,通过代币合约提供的approve()方法从拥有代币的账户地址得到授权,然后用这个独立的账户地址(可以是另一个智能合约的地址)去执行transferFrom()方法来分发代币,因为这样一来该地址得到的授权额度是有限的,二来黑客使用的sendTransaction API调用是不能调用到transferFrom这个方法的,因此会更安全。

无论是对于普通用户还是团体账户,谨记不要让某一个账户的余额特别高,如果有存储高额余额的需要,最好用冷钱包。如上所述,可以用代币合约的授权方法,授权给一些账户可以交易的额度,让这些授权账户去做实际的交易,这样就能规避这种黑客攻击的风险。因为这些授权账户地址里是没有实际的代币的,只是代替有余额的冷钱包账户对外发币。

黑客地址

该黑客的钱包地址为:

0x957cD4Ff9b3894FC78b5134A8DC72b032fFbC464

谷歌和百度黑客的地址,发现有若干条关于该地址的记录,国内外都有受害者。

使用etherscan.io对黑客地址进行查看后发现:

1、地址最早的Token Transfer记录可回溯到2016年2月14日;

2、地址中存在大量尚未上市流通的代币,但也有超过50个已上市流通的代币;

3、在已上市流通的代币中,有3个代币的交易笔数较多,分别是:Minereum(420笔)、PlusCoin(97笔)、Soarcoin(49笔);

4、除了两笔OUT外,其余均为IN。许多代币的转入具有固定模式:在同一时间段,以固定金额、固定笔数转入。例如,Minereum,每批次固定转入6笔,每批转入金额固定。

这种固定模式不仅发生在已上市流通的代币交易中,也广泛发生在未流通的代币交易中。因此,推测以这种模式转入的代币,亦为黑客盗取所得。

1、地址仅有的两笔OUT,是卖出soarcoin,一共卖掉了15278193.22 SOAR。其余关于soarcoin的记录均为转入,且累计转入=累计卖出。按照最后一笔卖掉的SOAR的价格,这个黑客变现了约42万美金;

2、地址中存在的可流通的代币里,价值最高的是UGT(约113万个,市值约65万美金),还有TNB(500万个,市值约13万美金)、NULS(约4万个,市值约10万美金)、KBR(约1万个,市值约6万美金)、OMG(约3千个,市值约3万美金)等。

我们诚恳邀请关注以太坊安全问题的读者,和我们一起深挖该黑客的行为路径。

黑客钱包地址:0x957cD4Ff9b3894FC78b5134A8DC72b032fFbC464

DRC社区采取应对措施

区块链技术仍然处于一个非常早期阶段、技术壁垒高的行业,几乎所有的团队都在学习和探索中。DRC团队经历此次事件后,深刻体会到安全风险无处不在,代币智能合约的开发只有更细致、更严密和更谨慎。

我们全面、深刻地分析整个事件始末,并积极采取措施改善与提升技术和管理,应对措施要点如下:

1、我们将联合国内、外顶级的代码审计团队,对代码进行更严格的审查;

2、邀请专家共同探讨关于智能合约、网络安全、代币管理、帐户管理等一系列相关主题,以寻求建立更严谨、更规范的管理体系,更加重视并优化内部操作管理流程;

3、加强开发规范与管理,将更积极的学习有关智能合约和代币相关的安全注意事项,加入以太坊与安全相关社区和论坛,更快的知晓有哪些安全漏洞需要注意,提前规避风险;

4、开发人员将避免在提供RPC服务的全节点上执行类似unlockAccount方法的操作,而使用Web3的Sign接口函数在发起交易时实时的签名,以规避账户风险;

5、提议并倡导建立智能合约安全联盟,为改善区块链行业的安全现状,DRC呼吁并号召成立行业安全联盟,集合项目方、安全团队、极客团队等加入,联合发起安全联盟,推动行业健康发展。联合相关专家与团队共同发起《区块链行业安全联盟公约发起函》。

6、DRC生态建设基金愿出资2000万DRC作为安全联盟专家费用,促进区块链行业有关智能合约、网络安全、法律法规等专题讨论,也欢迎更多有此志向的区块链项目团队和专家共同出资共建。

我们将此次事件进行披露公开,虚心接受大家的批评和建议,请大家多多指正。也希望其他区块链项目的开发团队引以为鉴,防患于未然。将代码合约安全放在首位,发布前进行严密的系统测试,必要时要请外部专业团队进行测试。请加群秘微信 chiajung ,邀请进入社群作进一步沟通。

探讨的话题不限于以下内容:

1、智能合约中需要采取的必要安全措施

2、区块链项目遭受黑客攻击后的应对方案

3、智能合约中已知的安全漏洞

4、区块链项目对代币及资金安全的管理制度

5、相关的法律法规问题

附件:相关以太坊主要漏洞描述

如果智能合约开发者疏忽或者测试不充分,而造成智能合约的代码有漏洞的话,就非常容易被黑客利用并攻击。并且越是功能强大的智能合约,就越是逻辑复杂,也越容易出现逻辑上的漏洞。同时,智能合约语言Solidity自身与合约设计都可能存在漏洞。据有关调查统计,以太坊主要漏洞情况描述如下表:



上述漏洞目前已经广泛存在以太坊网络中,2018年2月24日,新加坡和英国几位研究员指出,3.4万多份以太坊智能合约可能存在容易被攻击的漏洞,导致数百万美元以太币暴露在风险中,其中2,365份属于著名项目。

什么是zk-SNARKs?

Zcash是zk-SNARKs的第一个广泛应用,它是一种零知识密码学的新形式。Zcash的隐私保证源于可以屏蔽交易,但仍可通过zk-SNARK证明在网络共识规则下验证为有效。

缩写zk-SNARK代表“Zero-Knowledge Succinct Non-Interactive Argument of Knowledge 零知识简明非交互式知识”,并且指向的证明构造可以证明拥有某些信息,例如,一个密钥,不泄露该信息的情况下,也不在证明者和验证者之间进行任何交互。

“零知识”证明允许一方(证明方 prover)向另一方(验证方 verifier)证明声明的内容是真实的,不泄露超出声明本身有效性的任何信息。例如,考虑使用随机数的散列,证明者可以说服验证者确实存在具有该散列值的数字,而不透露数字的内容是什么。

在零知识“知识证明”中,证明者可以说服验证者不仅知道该数字存在,而且他们实际上知道这样一个数字,且不透露关于数字的任何信息。

可以在几毫秒内验证“简洁”的零知识证明,即使对于非常大的程序的声明内容,验证长度也只有几百字节。在最初零知识协议中,证明者和验证者必须多次来回通信,但在“非交互式”结构中,证明者发送“证明”给验证者,“证明”由单个消息组成。目前,唯一已知的生成非交互式的,足够短的,以发布到区块链的零知识证明方法具有“初始设置阶段 initial setup phase”,此阶段生成在证明者和验证者之间共享的公共参考字符串。我们将这个公共参考字符串称为系统的公共参数。

如果有人能够获得用于生成这些参数的随机性秘密,他们将能够创建对验证者看起来有效的错误证明。对于Zcash来说,这意味着恶意方可能会制造假币。为了防止此情况,Zcash设计了由多方程序产生公众参数。要详细了解参数生成程序并查看为防止随机性问题而采取的预防措施(例如,计算机正在生成公共参数),请访问Paramgen页面。要了解有关参数生成协议背后的数学的更多信息,请阅读博客文章有关该主题的白皮书

ZK-SNARKS如何在ZCASH中构建

为了在Zcash中拥有零知识隐私,根据网络共识规则确定交易有效的函数必须返回交易是否有效,而不泄露其执行计算的任何信息。这是通过对zk-SNARKs中的一些网络共识规则进行编码完成的。在很高的层面上,zk-SNARKs首先将你想要证明的东西转化为关于知道某些代数方程解的等价形式。

在下面的章节中,将简要介绍如何将用于确定有效交易的规则转换为方程,然后可以在候选解决方案上对方程进行评估,而不会向验证方程的当事方透露任何敏感信息。

计算 → 算术电路 → R1CS → QAP → zk-SNARK

将交易有效性函数转化为数学表示,第一步是将逻辑步骤分解为最小可能的操作,从而创建一个“算术电路”。 类似于一个布尔逻辑电路,其中一个程序被编译成离散的单个步骤,如AND,OR,NOT,当一个程序转换为一个算术电路时,它被分解为单个步骤,包括加法,减法, 乘法和除法(在特殊情况下,我们将避免使用除法)。

下面是计算表达式(a + b)*(b * c)的算术电路的例子:

看着这样一个电路,我们可以将输入值a,b,c看作在输出线上从左到右的“行进”。我们的下一步是建立的一级约束系统 (Rank 1 Constraint System),即R1CS,以检查这些值是否“行进正确”。在上图中,R1CS将确认,b和c进入的乘法门的值是 b*c。

在这个R1CS表示中,验证者必须检查许多约束条件 - 几乎每条线路都有一个约束条件。 (由于技术原因,事实证明,我们只能对乘法门引出的线路有所限制。)在2012年关于该主题的论文中,Gennaro,Gentry,Parno和Raykova提出了一种很好的方法来“将所有这些约束集中成一个” 。该方法使用称为二次算术程序(QAP)的电路表示。需要检查的单个约束在多项式之间而不是数字之间。多项式可能相当大,因为当多项式之间的身份不成立时,它将无法保持多数点。因此,您只需检查两个多项式是否在一个随机选择的点上匹配,以便以高概率正确验证证明。

如果证明者事先知道验证者选择检查哪一点,则他们可能能够制造无效的多项式,但仍然满足当时的身份。使用zk-SNARKs,复杂的数学技术(如同态加密和椭圆曲线配对)可用于“盲人式”评估多项式 - 即不知道哪个点正在评估。上面描述的公共参数用于确定将检查哪个点,且以加密形式,以便证明者和验证者都不知道公共参数是什么。

到目前为止的描述主要涉及如何获得“SNARKs”中的S和N - 如何获得短暂的,非交互式的单一消息证明 - 但尚未解决允许 “zk”(零知识)部分证明者维护其秘密投入的机密性。事实证明,在这个阶段,通过使证明者使用仍然满足所需身份的原始多项式的“随机偏移”,可以容易地添加“zk”部分。

有关Zcash中关于zk-SNARKs背后关键概念的深入解释,请参阅后面的博客文章。

Zcash使用 libsnark 分支,这是一个用于zk-SNARKs的C++库。 您可以检查代码并了解更多关于在 github 上的实现。要深入了解 Zcash zk-SNARKs 的协议,请参阅 Pinocchio协议 的本文。

ZK-SNARKS如何应用于创建屏蔽交易

在比特币中,通过链接发件人地址,收件人地址以及公共区块链上的输入和输出值来验证交易。 Zcash使用zk-SNARKs来证明有效交易的条件得到了满足,但没有透露任何有关地址或值的重要信息。 屏蔽交易的发件人构建了一个证据,以高概率显示:

输入总和 等于 每个屏蔽转换的输出总和。

发件人证明他们拥有输入的私钥,赋予他们花费的权力。

输入的私钥与整个交易的签名链接起来,链接是加密的,则,交易不会被不知道这些私钥的人修改。

另外,屏蔽交易必须满足下面描述的其他条件:

比特币跟踪(未花费的交易输出)UTXO,以确定哪些交易是可用的。 在Zcash中,UTXO的屏蔽等同物称为“承诺”,花费承诺包含暴露出“花费人”。 Zcash节点保留已经创建的所有承诺的清单以及所有已经暴露的花费人。承诺和无效值存储为散列值,以避免公开关于承诺的任何信息,或者哪些无效值与哪些承诺相关。

对于由屏蔽付款创建的每个新钞票,将公布一个承诺,其中包含以下内容的散列:

  • 钞票发送的地址
  • 发送的金额
  • 本钞票所特有的数字“rho”(稍后用于派生花费人)和一个随机数。
1
Commitment = HASH(recipient address, amount, rho, r)

当屏蔽交易被花费时,发件人使用他们的支出密钥来公布一个花费者,该花费者是来自尚未用过的现有承诺的唯一编号(“rho”)的hash,并提供零知识证明,证明他们有权花费它。 该散列必须不在花费清单中,花费清单在区块链的每个节点中保存。

1
Nullifier = HASH(spending key, rho)

屏蔽交易的零知识证明证实,除了上面列出的条件外,以下认定也是正确的:

对于每个输入,都存在明确的承诺。

花费者和钞票承诺计算正确。

输出钞票的花费者与任何其他钞票的花费者发生绝不会发生冲突。

除了用于控制地址的支出密钥之外,Zcash还使用一组证明和验证密钥来创建和检查证明。这些密钥在上面讨论的公共参数程序中生成,并在Zcash网络中的所有参与者之间共享。对于每个屏蔽交易,发件人使用他们的证明密钥来生成他们的输入有效的证据。矿工通过使用验证密钥检查证明者的计算来检查被保护的交易遵循共识规则。Zcash证明生成的设计方式要求证明者事先做更多的工作,但它简化了验证,因此主要的计算工作被转移到交易的创建者(这就是为什么创建屏蔽Zcash事务可能会占用到40秒,同时验证事务有效仅需要几毫秒)。

Zcash的屏蔽交易的隐私依赖于标准的,经过验证的密码学(散列函数和流密码),但它增加了zk-SNARKs,与承诺和花费机制一起使用,允许屏蔽交易的发送者和接收者证明加密交易是有效的。其他为加密货币提供隐私的方法依赖于模糊交易之间的链接,但事实上Zcash交易可以存储在完全加密的区块链中,为加密货币应用程序开辟了新的可能性。加密交易使得各方可以享受公共区块链的好处,同时保护他们的隐私。计划的未来升级将允许用户根据自己的判断有选择地披露有关被屏蔽交易的信息。查看Zcash博客文章的近期未来,了解Zcash的未来计划

有关如何在Zcash中构建屏蔽事务的更深入解释,请参阅我们的博客文章,了解屏蔽地址之间的事务如何工作。有关当前Zcash协议的完整详细信息,请参阅我们的协议规范

ZK-SNARKS的未来应用

在Zcash中创建屏蔽交易只是zk-SNARKs许多可能应用中的一个例子。从理论上讲,您可以使用zk-SNARK来验证任何关系,而不会泄露输入或泄露信息。对复杂函数生成证明的计算量仍然过大,许多程序还不适用,但Zcash团队正在推动优化zk-SNARKs的界限,并且已经以更高效的实施方式开创了新局面。

就目前而言,Zcash的zk-SNARKs实现可以作为企业用例的零知识安全层添加到任何现有的分布式分类帐解决方案中。

zk-SNARKs 原文链接

参考

Zcash协议规范

解释SNARKs系列文章

1. 同态隐藏

2. 多项式盲评估

3. 系数测试知识和假设

4. 如何验证多项式盲评估

5. 从计算到多项式

6. Pinocchio Protocol — 皮诺曹协议

7. 配对椭圆曲线

8. 零知识证明在 Quorum 中是如何使用的 — Zero Knowledge Proofs and how they can be implemented in Quorum

9. 零知识证明在以太坊智能合约端如何实现区间证明 — zero knowledge range-proof precompiled contract for the go-ethereum client

论文

1. 皮诺曹协议

2. Paramgen白皮书

3. Zerocash论文

技术博客文章

1. Matt Green: 零知识证明

2. Zcash交易解剖

3. 隐私交易如何完成的

4. Zcash的参数是如何生成的

5. 在Rust进行密码学配对

相关项目

1. HAWK:私有的智能合约

2. BOLT:私有的支付通道

后面的博客中会针对上述内容进行分析。

背景

channel,即“管道”,是用来传递数据(叫消息更为合适)的一个数据结构,即可以从channel里面塞数据,也可以从中获取数据。channel本身并没有什么神奇的地方,但是channel加上了goroutine,就形成了一种既简单又强大的请求处理模型,即N个工作goroutine将处理的中间结果或者最终结果放入一个channel,另外有M个工作goroutine从这个channel拿数据,再进行进一步加工,通过组合这种过程,从而胜任各种复杂的业务模型。

goroutine不同于thread,threads是操作系统中的对于一个独立运行实例的描述,不同操作系统,对于thread的实现也不尽相同;但是,操作系统并不知道goroutine的存在,goroutine的调度是有Golang运行时进行管理的。启动thread虽然比process所需的资源要少,但是多个thread之间的上下文切换仍然是需要大量的工作的(寄存器/Program Count/Stack Pointer/…),Golang有自己的调度器,许多goroutine的数据都是共享的,因此goroutine之间的切换会快很多,启动goroutine所耗费的资源也很少,一个Golang程序同时存在几百个goroutine是很正常的。

Go Channel的基本操作语法如下:

1
2
3
4
5
c := make(chan bool)    //创建一个无缓冲的bool型Channel
c <- x //向一个Channel发送一个值
<- c //从一个Channel中接收一个值
x = <- c //从Channel c接收一个值并将其存储到x中
x, ok = <- c //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

不带缓冲的Channel兼具通信和同步两种特性,颇受青睐。

模型01-go关键字

直接加上go关键字,就可以让一个函数脱离原先的主函数独立运行,即主函数直接继续进行剩下的操作,而不需要等待某个十分耗时的操作完成。

1
2
3
4
5
6
7
8
9
func (m *SomeController) PorcessSomeTask() {
var task models.Task
if err := task.Parse(m.Ctx.Request); err != nil {
m.Data["json"] = err
m.ServeJson()
return
}
go task.Process()
m.ServeJson()

如果 func(peer Peer) 函数需要耗费大量时间的话,这个请求就会被 block 住。有时候,前端只需要发出一个请求给后端,并且不需要后端立即所处响应。遇到这样的需求,直接在耗时的函数前面加上go关键字就可以将请求之间返回给前端了,保证了体验。

不过,这种做法也是有许多限制的。比如:

  • 只能在前端不需要立即得到后端处理的结果的情况下使用
  • 这种请求的频率不应该很大,因为目前的做法没有控制并发量

模型02-并发控制

上一个方案有一个缺点就是无法控制并发,如果这一类请求同一个时间段有很多的话,每一个请求都启动一个goroutine,如果每个goroutine中还需要使用其他系统资源,消耗将是不可控的。

遇到这种情况,一个解决方案是:将请求都转发给一个channel,然后初始化多个goroutine读取这个channel中的内容,并进行处理。假设我们可以新建一个全局的channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var TASK_CHANNEL = make(chan models.Task)

// 然后,启动多个goroutine:
for i := 0; i < WORKER_NUM; i ++ {
go func() {
for {
select {
case task := <- TASK_CHANNEL:
task.Process()
}
}
} ()
}

// 服务端接收到请求之后,将任务传入channel中即可:
func (m *SomeController) PorcessSomeTask() {
var task models.Task
if err := task.Parse(m.Ctx.Request); err != nil {
m.Data["json"] = err
m.ServeJson()
return
}
// go task.Process()
TASK_CHANNEL <- task
m.ServeJson()
}

这样一来,这个操作的并发度就可以通过WORKER_NUM来控制了。

模型03-处理channel满的情况

不过,上面方案有一个bug:那就是channel初始化时是没有设置长度的,因此当所有WORKER_NUM个goroutine都正在处理请求时,再有请求过来的话,仍然会出现被block的情况,而且会比没有经过优化的方案还要慢(因为需要等某一个goroutine结束时才能处理它)。因此,需要在channel初始化时增加一个长度:

1
var TASK_CHANNEL = make(chan models.Task, TASK_CHANNEL_LEN)

这样一来,我们将 TASK_CHANNEL_LEN 设置得足够大,请求就可以同时接收 TASK_CHANNEL_LEN 个请求而不用担心被block。不过,这其实还是有问题的:那如果真的同时有大于 TASK_CHANNEL_LEN 个请求过来呢?一方面,这就应该算是架构方面的问题了,可以通过对模块进行扩容等操作进行解决。另一方面,模块本身也要考虑如何进行“优雅降级了”。遇到这种情况,我们应该希望模块能够及时告知调用方,“我已经达到处理极限了,无法给你处理请求了”。其实,这种需求,可以很简单的在Golang中实现:如果channel发送以及接收操作在select语句中执行并且发生阻塞,default语句就会立即执行。

1
2
3
4
5
6
7
8
select {
case TASK_CHANNEL <- task:
//do nothing
default:
//warnning!
return fmt.Errorf("TASK_CHANNEL is full!")
}
//...

模型04-接收发送给channel之后返回的结果

如果处理程序比较复杂的时候,通常都会出现在一个goroutine中,还会发送一些中间处理的结果发送给其他goroutine去做,经过多道“工序”才能最终将结果产出。

那么,我们既需要把某一个中间结果发送给某个channel,也要能获取到处理这次请求的结果。解决的方法是:将一个channel实例包含在请求中,goroutine处理完成后将结果写回这个channel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type TaskResponse struct {
//...
}

type Task struct {
TaskParameter SomeStruct
ResChan *chan TaskResponse
}

//...

task := Task {
TaskParameter : xxx,
ResChan : make(chan TaskResponse),
}

TASK_CHANNEL <- task
res := <- task.ResChan
//...

(这边可能会有疑问:为什么不把一个复杂的任务都放在一个goroutine中依次的执行呢?是因为这里需要考虑到不同子任务,所消耗的系统资源不尽相同,有些是CPU集中的,有些是IO集中的,所以需要对这些子任务设置不同的并发数,因此需要经由不同的channel + goroutine去完成。)

模型05-等待一组goroutine的返回

将任务经过分组,交由不同的goroutine进行处理,最终再将每个goroutine处理的结果进行合并,这个是比较常见的处理流程。这里需要用到WaitGroup来对一组goroutine进行同步。一般的处理流程如下:

1
2
3
4
5
6
7
8
9
10
11
var wg sync.WaitGroup
for i := 0; i < someLen; i ++ {
wg.Add(1)
go func(t Task) {
defer wg.Done()
//对某一段子任务进行处理
} (tasks[i])
}

wg.Wait()
//处理剩下的工作

模型06-超时机制

即使是复杂、耗时的任务,也必须设置超时时间。一方面可能是业务对此有时限要求(用户必须在XX分钟内看到结果),另一方面模块本身也不能都消耗在一直无法结束的任务上,使得其他请求无法得到正常处理。因此,也需要对处理流程增加超时机制。

我一般设置超时的方案是:和之前提到的“接收发送给channel之后返回的结果”结合起来,在等待返回channel的外层添加select,并在其中通过time.After()来判断超时。

1
2
3
4
5
6
7
8
9
10
11
task := Task {
TaskParameter : xxx,
ResChan : make(chan TaskResponse),
}

select {
case res := <- task.ResChan:
//...
case <- time.After(PROCESS_MAX_TIME):
//处理超时
}

模型07-广播机制

既然有了超时机制,那也需要一种机制来告知其他 goroutine 结束手上正在做的事情并退出。很明显,还是需要利用channel来进行交流,第一个想到的肯定就是向某一个chan发送一个struct即可。比如执行任务的goroutine在参数中,增加一个 chan struct{} 类型的参数,当接收到该channel的消息时,就退出任务。但是,还需要解决两个问题:

  1. 怎样能在执行任务的同时去接收这个消息呢?
  2. 如何通知所有的goroutine?

对于第一个问题,比较优雅的作法是:使用另外一个channel作为函数的输出,再加上select,就可以一边输出结果,一边接收退出信号了。

另一方面,对于同时有未知数目个执行goroutine的情况,一次次调用 done <-struct{}{},显然无法实现。这时候,就会用到golang对于channel的tricky用法:当关闭一个channel时,所有因为接收该channel而阻塞的语句会立即返回。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 执行方
func doTask(done <-chan struct{}, tasks <-chan Task) (chan Result) {
out := make(chan Result)
go func() {
// close 是为了让调用方的range能够正常退出
defer close(out)
for t := range tasks {
select {
case result <-f(t):
// do task
case <-done:
return
}
}
}()

return out
}

// 调用方
func Process(tasks <-chan Task, num int) {
done := make(chan struct{})
out := doTask(done, tasks)

go func() {
<- time.After(MAX_TIME)
//done <-struct{}{}

//通知所有的执行goroutine退出
close(done)
}()

// 因为goroutine执行完毕,或者超时,导致out被close,range退出
for res := range out {
fmt.Println(res)
//...
}
}

参考:

http://blog.golang.org/pipelines

https://gobyexample.com/non-blocking-channel-operations