微信图片_20210312154813.jpg

本文试图通过渐进的技术分析,和大家分享我们在互联网高并发技术方案选择上的一些思考和决策;从技术选型的高度为架构师在面对高并发业务设计时需要考虑的方方面面做一个参考。

Beautiful Concurrency & Pretty Erlang

前言

环信以“连接人与人,连接人与商业”为使命,旨在为广大企业开发者提供最优质的全球即时通讯PaaS服务。如何实现高并发场景下,弹性化的保障服务质量是我们一贯的业务要求和技术追求。

本文试图通过渐进的技术分析,和大家分享我们在互联网高并发技术方案选择上的一些思考和决策;从技术选型的高度为架构师在面对高并发业务设计时需要考虑的方方面面做一个参考。

接下来,enjoy:

Beautiful Concurrency – 并发的必要性

世界是并发的,软件也应该是并发的

我们生活在其中的世界,就是一个巨大的并发系统。每时每刻,在世界的每一个角落,人类的每一个个体都在和这个世界进行着频繁的能量交互,信息交互也是其中的重要组成部分;当我们转换身份,以造物主的视角俯瞰这个世界里所有的万物(动物,植物,海洋,土壤,机器等等)时,也可以看到它们同样在和这个世界进行着无休止的能量/信息交互。

为了与这个世界进行有效的交互,软件也应该是并发的。

世界是分布的,软件也应该是分布的

地球是圆的,世界是平的,不管怎样,从宇宙大爆炸的那个奇点之后,就是分布的宇宙/世界。作为与世界万物(包括人类自己)交互的软件,自然也必须满足分布式要求。而这种地理分布(Geo. Distribution)特性,也仅是并发在空间维度下的反映而已。

世界不可预测,软件也应该是容错的

没有完美的世界:冲突,灾难随时在发生,不管在什么样的维度上。作为软件,bug,crash也是不可规避的现实挑战。即使存在完美的没有bug的程序,运行程序的硬件也可能出现故障。为了增强软件的容错性,代码的独立性(指一个故障不会影响到故障任务以外的其它任务)和故障检测以及故障处理是关键:这一切都需要并发,因为串行程序的容错性远远不如并发程序。

并发方案概览

“七个模型”来源于Paul Butcher著的《Seven Concurrency Models in Seven Weeks》,中文译名《七周七并发模型》,概览的介绍了并发领域的常见方案,希望能给架构师提供一个轮廓化的分类描述。本人在其基础上添加了一些自己的拓展思考(见下文中斜体部分):

1.线程与锁:线程与锁模型有很多众所周知的不足,但仍是其他模型的技术基础,也是很多并发软件开发的首选。— 这个方案其实是一个anti-pattern,在高并发场景下如履薄冰(bug,dead lock如影随形),让开发者和运维人员胆战心惊。Ugly Locks to Ugly Concurrency (丑陋的锁,丑陋的并发):locks and condition variables is fundamentally flawed!


2.函数式编程:函数式编程日渐重要的原因之一,是其对并发编程和并行编程提供了良好的支持。函数式编程消除了可变状态,所以从根本上是线程安全的,而且易于并行执行。— 函数之美,逻辑之美!相信很多从命令式语言(Imperative Programming)转战到函数式编程语言(Functional Programminmg)的时候都会发出这样的感叹。其实这一进步恰恰体现了人类在不断的进化过程中,对这个世界认知不断提炼,思维模式逐步由具象走向抽象的演进轨迹。而回到高并发的话题上,函数式编程以不可变状态这一简易的策略,赢得了完美赞誉,俨然已是君临天下的明日霸主!


3.分离标识与状态:如果一个线程引用了持久数据结构,那么其他线程对数据结构的修改对该线程就是不可见的。因此持久数据结构对并发编程的意义非比寻 常,其分离了标识(identity)与状态(state)。 — 这又是一个很妙的策略,而大家熟知的version control system如git,包括比特币/区块链的机制都是这一思想指导下的具体实践,限于篇幅,本文不做进一步展开。


4.Actor Model:一种适用性很广的并发编程模型,适用于共享内存模型和分布式内存模型,也适合解决地理分布型问题,能提供强大的容错性。— 最开始接触到Actor模型就是通过Erlang语言,后来又接触到Akka(基于Scala)等基于各种语言实现的框架,也越来越体会到这一模型在高并发场景下的游刃有余。本文后续部分会展开介绍。另外,做个招聘小广告,环信通讯云研发团队正在广纳英才,欢迎懂Erlang,有相关高并发开发经验的小伙伴加盟,虚位以待!点这里直接联系我们哟


5.CSP(Communicating Sequential Processes, CSP):表面上看,CSP模型与Actor Model很相似,两者都基于消息传递。不过CSP模型侧重于传递信息的通道,而Actor Model侧重于通道两端的实体,使用CSP模型的代码会带有明显不同的风格。— 这里就是channel可以大施拳脚的天地了,go go go!限于篇幅,本文不做进一步展开。同样的,欢迎懂Golang,有相关高并发开发经验的小伙伴加盟,虚位以待!


6.数据级并行:每个笔记本电脑里都藏着一台超级计算机——GPU。GPU利用了数据级并行,不仅可以快速进行图像处理,也可以用于更 广阔的领域。如果要进行有限元分析、流体力学计算或其他的大量数字计算,GPU的性能将是不二选择。— 在过去的两年里,华人之光,黄仁勋(Jensen Huang, CEO of Nvidia),从自家的壁炉里一次又一次给大家带来了震撼世界的革新产品,让之前高不可攀的GPU飞入寻常百姓家,也带来了一次又一次的算力之争。相信在不久的将来,有了GPU算力加持和人工智能算法的普惠使用,会有无数的AI应用层出不穷的涌现,它们会从云端落地到边缘,人类可能会比自己想象的更早的进入前途未卜的人机争霸时代。


7.Lambda架构:Lambda架构综合了MapReduce和流式处理的特点,是一种可以处理多种大数据问题的架构。— Lambda架构也是采用了数据并行处理技术,但是它把并行算力的微观场景放大到了一个更大的尺度:将数据和计算分布到成千上万台机器组成的集群上进行,将并发,分布特性整合到一套方案中,通过两层(批处理层-Batch Layer,加速层-Speed Layer/Streaming Process)的组合,实现了高计算效率和低延迟的“鱼与熊掌兼得”。


Pretty Erlang

环信的全球即时通讯云的核心网络是基于Erlang/OTP开发的,截止目前共服务了几十万APP客户,单集群日消息几十亿量级并还在不断挑战新高。多年的经验积累,我们仍在不断的对系统进行优化,榨取计算,网络等资源的内生价值,挑战系统一个又一个不断提升的指标要求。感谢Erlang/OTP,有了它,我们像站在了巨人肩膀上的一名挥舞利刃的勇士,能够从容应对各种“黑云压城城欲摧”的业务压力和不断变化,五花八门的业务需求。以下,我们简单总结了Erlang/OTP几点让我们“迷醉的特质”,给想入坑的小伙伴以参考:

Let it Crash!

再一次,让我们理解了“思维高度决定人生高度”。当其它语言或者解决方案防御性的,想方设法捕获异常/错误并尽力挽回“败局”的时候,Erlang采纳了”Let it crash!”的策略,把重点放在了错误的检测和错误通知体系上,通过严谨设计的单向,双向link机制,OTP基于此设计的supervisor behaviour层级(Hierarchical)管理汇报机制,”任其崩溃“的同时又干净漂亮的将所有Actor(进程)在发生异常时的行为进行了简单但有效的管理,获得了意想不到的好处:

  • 代码简洁易懂,仅在需要关心崩溃处理的层级存在容错代码;

  • 由于Actor Model的设计,Actor之间相互独立,也不共享状态,因此任何一个Actor的崩溃并不会影响其它Actor,遑论它的管理者(Supervisor),因此管理者可以从容的处理被管理Actor的崩溃;

  • 管理者也可以不处理崩溃,仅记录相应崩溃,继而通过查看崩溃通知来进行后续处理。这个策略在后续Pattern Matching时候还会见到,我们可以在_Other(所有匹配均未触发)保护匹配中记录相应信息,而不必为事先预估不到的场景绞尽脑汁!


Actor Model

Actor Model是Pure OO的设计(而Java类与方法的设计并不是,意不意外?):每一个Actor封装了状态,外界并没有任何方法来操纵(manipulate)对象,它们唯有通过发送消息通知Actor,由Actor自己控制对消息的处理,这种简化同样为高并发处理提供了意料之外的强大支持。

Actor Model适用于共享内存模型和分布式内存模型,也适合解决地理分布型问题,能提供强大的容错性,用了都说好!

我个人在曾经的工作中做过一个网络安全过滤的产品,是靠自己团队设计编写的一套消息队列处理机制来解耦不同业务对相同数据的处理,后来回想起来才蓦然发现其实这就是个简陋的Actor Model设计,只是那个时候还不知道Erlang/OTP,要是早用上这把利器,能节省我生命中多少个日日夜夜啊!

Functional Programming

介绍函数式编程的书籍,资料已经很多了,本文不做过多的展开。简单列举下个人的几点体会:

  • 程序最终还是要交给机器处理,因此要尽量按照机器的思维模式去编写程序(虽然有时候会让程序员头大),最简单的 (a + b) 与 (+ a b):前者对人类友好,而后者对机器友好,因而也能带来更好的程序一致性,继而使得系统可以设计的更简单;

  • 命令式编程的代码由一系列改变全局状态的语句构成,而函数式编程则是将计算过程抽象成表达式求值。这些表达式由纯数学函数构成,而这些数学函数是第一类对象(我们可以像操作数值一样操作第一类对象)并且没有副作用。由于没有副作用,函数式编程可以更容易做到线程安全,因此特别适合于并发/并行编程。


  • 函数之美带来美之并发!原文摘抄:

For me, a beautiful program is one that is so simple and elegant that it obviously has no mistakes, rather than merely having no obvious mistakes. If we want to write parallel programs that work reliably, we must pay particular attention to beauty.

Pattern Matching

又一个让你被美折服的设计:函数在这种Pattern Matching的语法结构的描摹下,俨然变成了一道道证明题,你只需要描述(并不需要穷举)你所关心的场景(Conditon)下自己的想法,剩下的就交给”模式匹配“这个自动化机器帮你完成。你可能想不到的是,”Pattern Matching“发挥功效的地方并不只是case/if出现的地方,你的整个代码都是在”Pattern Matching“的魔力下散发它优雅的魅力。

Future Evolution

对高并发的不懈追求将一直是环信通讯云研发团队的目标,我们也期待在这条崎岖山路上不断攀登,厚积薄发,为环信的客户带来一贯的极致产品体验!