前些天摸排wireguard-go的并发瓶颈,愧于对goroutine理解不深入,被误导,有必要记录以备忘。
wireguard-go为每一个peer开一个单独的goroutine,看起来像极了Apache的典型mpm,而boringtun则采用固定线程异步模型,颇像Nginx架构,从数据上看,boringtun吞吐碾压wireguard-go,身边又有人煽动加码,便得出结论,为每一个peer创建一个goroutine这种架构是不正确的。
很显然,这个结论是错误的。
理论上,创建万级别的goroutine毫无压力,而我只用不到10个甚至单peer进行测试,听闻几句话就把罪过归于goroutine,这是我的罪过,应该归于自己的无知。
回到问题和质疑的原点,为每个请求创建一个task是不正确的,原因在于每个CPU同时只能运行一个task,CPU数量固定,随着task的增加,切换开销线性增加。异步IO之所以好,因为task切换开销固定。我们知道,切换开销是业务处理的额外开销,越大越不好。
这就是我说的不要让请求去调度资源,而要让资源去调度请求。
现在看goroutine。goroutine并不是 *** 作系统级的task,切换开销极小。理解了goroutine的G(goroutine)-P(processor)-M(machine)模型,就会发现它事实上和异步事件模型一致:
固定数量的M上跑不固定数量的G,G之间的切换开销与查找无异。异步事件模型中,固定数量的线程处理所有被激活的请求,这些请求或被轮询处理,或排以优先级,这与goroutine的方式无异,goroutine的方式只是将“或轮询或优先级处理”这件事交给了Runtime库,程序员只需要关注自己的业务逻辑即可,即写一个前面加上go关键字的func。
goroutine的调度,本质上就是异步事件驱动的多请求处理的轮询。
golang好在,它甚至让你无法使用错误的方式处理并发,不信试试看,如何写一个程序,让 *** 作系统级别的切换开销随着请求的数量线性增加。
无论怎么样,系统级task的数量都是一定的(more or less),就算100万并发,也不会有100万系统级task,系统级task永远都是CPU数量级的,如果用CPU数量级的task处理100万的并发,就是goroutine的调度问题了,而调度的本质就是查找,这个查找的开销,便和系统task切换无关了。
这便和下面的文章统一了:
https://zhuanlan.zhihu.com/p/492863461
因此,goroutie不足以构成并发瓶颈,它甚至是优势,虽然性能可能达不到标准异步模型,但它大大简化了编程难度,门槛极低。性价比还是优于rust的。
当关注纠结某件事时,偏见便侵入了,比方说当已有事实证据指向某个方向时,旁人只需轻微一点,你便很容易将所指之处作为根本去试图揭露,这便是偏见之害。若对goroutine没有深入理解,便很容易将per peer a goroutine等同于per client a thread/process,如果此时你还不敢下定论,只需有人也这么一说,便判定无疑了,但事实上,据此下的结论其效果正好相反。所以,不要针对自己不了解的任何东西下任何结论。
浙江温州皮鞋湿,下雨进水不会胖。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)