10月19日,云+社区开发者大会(北京站)圆满落幕。本次开发者大会的主题为“5G探索:核心技术与挑战”,邀请了腾讯内部及业内行业大咖就5G场景下应该如何面对新业务与挑战?大型网站的技术应该如何进化?如何真正理解万物互联?5G有哪些值得探索与实践的方向?5G对应用发展的影响有哪些?等问题进行了深度探讨。同时,在圆桌论坛环节,各位技术专家也与到场的开发者们展开了开放式对话,精彩不断。下边是李智慧老师关于反应式框架 Flower 的架构设计原理与思想及落地实践与效果的分享。
讲师介绍:李智慧,腾讯云TVP,同程艺龙交通首席架构师。长期从事大数据、大型网站架构的研发工作,曾担任阿里技术专家、Intel亚太研发中心架构师、宅米和WiFi万能钥匙CTO。Apache Spark源代码贡献者,著有畅销书《大型网站技术架构:核心原理与案例分析》,极客时间《从零开始学大数据》专栏作者。现担任同程艺龙交通首席架构师。
我分享的题目叫“反应式编程在同程艺龙的实践。”这个话题跟5G也有关系,当5G的速度越来越快,终端跟服务器之间的通讯变得越来越密集,计算量越来越大的时候,我们的编程是什么样子?我不知道今天各位同学来听5G,有多少是做后台技术开发的?5G时代对编程的影响是什么样子?会使用什么样的技术解决这些问题?我大概分享这些东西。
我个人的背景,现在在同程艺龙交通做首席架构师,以前在阿里巴巴任技术专家,在Intel亚太研发中心做大数据架构师,主要参与Apache Spark和Hive开发,后来做过创业,写过一本书。接下来我们进入正题,高并发是如何导致程序崩溃的?互联网应用包括将来的物联网,物联网其实比互联网大数据更大。典型的特征高并发大数据请求量特别大,数据量特别大,这种情况下开发一个单人应用的系统,比如说开发一个新浪微博可能几天就开发出来,如果给几亿人使用,这个系统跟几个人使用完全不一样,这个大家可以理解。
新浪微博每一次技术进步出来后他们做分享的时候,总是说现在新浪微博技术架构的改进,可以支撑多少个人半夜发布他的信息,这样去宣传他的技术。就是因为一旦量大了之后,整个技术体系完全不一样,造成的影响就是典型新浪微博这样子的。当明星们发布发一条爆炸性消息的时候,新浪微博的服务器就挂掉了,为什么会挂掉?因为几千万人在转发这条消息,这些数据会对服务器造成很大的压力,系统会崩溃。像刚才提到的数据量会呈现几倍、几十倍的增加,用户的应用场景也会更加的复杂,这个时候服务器如何应对?编程方式是不是也有革命性的变化。
未来究竟什么样子?连5G本身还在探索的过程中,这个编程怎么样?为时尚早。今天更多分享一下我的看法和实践。
高并发是如何导致程序崩溃的?程序怎么就崩溃了?高并发的时候到底发生了什么?我们看一下后端服务器的具体架构大概这样子,用户并发请求进入到服务器,前面要经过负载均衡,负载均衡之后进入到服务器,在服务器里面运行着一个Web容器,Java开发的话可能就是一个Tomcat。当一个用户请求进来以后,容器如何去处理这个请求?写的程序如何去处理请求?这个里面有一个关键点会被经常忽略掉,这个工作是由Web容器去做的,Web容器去监听80端口,监听80端口以后收到一个用户请求,容器就会为这个用户请求创建一个计算线程,这个线程负责用户请求的操作处理,一直到处理完之后返回响应,每一个请求进来以后都创建一个线程,里面的线程数是有限的,现在是200个线程,创建200个线程以后如果有更多的用户请求进来怎么办?就拒绝的,用户这个时候就看到自己的请求失败了。如果不拒绝,请求继续进来。进入等待队列会消耗资源,服务器的压力非常大。如果更多用户的请求都接受进来,服务器的压力会逐渐的增大,超过系统可用资源就会崩溃。为每一个用户独占一个线程造成资源的消耗,这个线程如果处理结束都好办,如果处理来不及,这个时候有可能会导致系统巨大的负载增加,最后导致系统崩溃,这是一个点。为什么会特别慢呢?如果快速处理完释放了线程,下一个用户的请求可以继续处理,如果不释放的话就会堆积在这里,为什么会堆积在这里?因为线程被阻塞了,线程为什么被阻塞了?线程不仅仅在当地做一些CPU处理的计算,还会跟外部服务资源进行通信,还要调用一些其他的微服务,还要访问数据库,还有其他的东西,这个时候线程请求微服务进行远程调用或者是访问数据库都有可能,当请求远程操作的时候,请求是被阻塞了的,如果远程这些资源访问的话快速响应过来,也都还好。如果这个时候数据库里面有个表,因为什么原因响应特别的慢,这个时候线程迟迟不能释放,它不能释放别的请求就没有线程可用,就回到刚才所说的场景系统就崩溃了。这还是现在,就经常会遇到这种崩溃的情况。如果到5G时代,请求会更加的频繁和密集,计算量会越来越大。这种情况下,我们的系统崩溃的会更加频繁,我认为就是要面对这种问题。
解决方案,反应式编程大家都知道,有一些比较主流的解决方案。我们这边自己搞了一个编程框架叫Flower,我们看一下Flower怎么解决这些问题的?请求还是并发进入容器,容器就要监听那个端口,这个时候我们看看Flower是怎么解决这个问题的?Flower又是如何实现的?分为两步:
第一步,请求进入容器以后,每一个请求不再占据一个线程,把它异步化。每一个请求进来以后,容器还会启动一个线程,启动线程之后把传输过来的二进制流转换成对象,转换成对象以后,容器的工作就结束了,这个容器线程工作就结束了,把这个请求交给后面,想象一下容器只要启动一个线程。做这件事情可以是亚毫秒级的,1毫秒以内就可以完成这个协议的处理,因为它不做计算,只是把协议处理完就可以,这个时候如果还是200个,300个、500个都没有关系,几百个并发请求一秒钟过来,它的处理是亚毫秒级的,一秒钟可以处理1000以上的请求,一个线程处理几百个请求都是绰绰有余的,因为它并没有做太多的事情,只是把请求分发出去,一个线程就可以做这么多的事情,处理所有的请求。请求进来之后,这个时候就不会被拒绝,以前200个线程300个线程就没有了,现在2000个请求进来之后,一个线程全部处理了,所有数据全都处理了。然后这个请求就交给后面的处理,我们有一个Flower自己的运行环境,理论上讲也可以是只用一个线程。200个请求进来以后,变成200个请求对象,或者更多个请求,2000个请求对象,2000个请求对象交给我们的Service去处理,2000个请求进来可以迅速的把2000个请求都算完,计算只有零点零几毫秒,几毫秒可能就有2000个请求,而且只有一个线程。大家可以想象一个线程可以同时处理2000个请求,它处理完之后再交给下一个Service处理,这个里面是做一些预处理、参数校验,后面真正的做逻辑计算,把它的处理交给下一个Service,下一个Service也可以复用刚才的线程,因为刚才的线程算了ServiceA,也可以继续算ServiceB,只需要一个线程就可以把所有的请求整个处理逻辑全部处理完。理论上可以这样做到,处理完以后访问数据库,访问数据库的时候给数据库发一个请求,同步数据库处理的时候线程会处于阻塞状态,等到数据库有了返回以后,线程就会被唤醒,唤醒以后它去执行。从异步的方式来讲,把这个请求发过去以后,查数据库的SQL发出去以后,线程就没事了,可以继续处理Service,不用等。等到数据库真正有响应结果以后,我们想象以下有个ServiceC,有了结果以后变成消息了,都是消息启动,返回来的消息就是数据库返回来的结果集,交给ServiceC,ServiceC又有消息等着处理,它也是独立的。ServiceC有消息处理的时候,这个线程还可以回到ServiceC,处理完之后返回到结果,就可以把ServiceC拿到的结果返回回去。通过这样的方式,利用一个线程就把整个业务全部处理了。这个里面有一个线程专门做业务逻辑的处理,有一个线程专门做请求的接入,阻塞式编程200个线程就是200个请求,任何一种打破平衡都会导致系统失效宕机了。但是Flower利用这种方式只用几个线程可以解决2000个请求。解决方案核心点是这样的,如果处理的速度足够快,资源比较平衡的话,可能处理更多的并发请求,系统算力会变得更加的强大。当5G来临用户请求更多的时候,可以支持更大的算力。
这是Flower,它的工作原理是这样子去提高性能。这是真实在同程艺龙做的一个重构,红色是重构后的,绿色是重构前的,做了一次Flower重构。左侧是吞吐量TPS,绿色是重构前的,红色是重构后的,重构前是200TPS,重构以后是450TPS,现在提升了1倍多。右边是响应时间,红色是重构后的,绿色是重构前的。经过重构以后发现响应时间特性表现的更好一点,特别是在高并发情况下。
Flower究竟怎么实现的?刚才提到一个消息驱动。Flower自己没有运行时环境,刚才这样一套都有自己的运行时环境。Flower底层利用Akka的Actor,我们看一下在Actor进行通讯的时候怎么做的?为什么理想情况下,用一个线程就完成所有的服务计算?在传统的编程里面服务调用、方法调用,当A方法调B方法的时候,A方法的代码行里面加了一行调用B方法的代码,B方法执行的时候,A方法下一行代码一定是不执行,传统的方法调用是这样子,都是阻塞式的编程,所谓阻塞似的编程当你调用别的计算时,当前执行一定是阻塞的,调用其他的方法执行,执行完了再返回,所有的执行都是在由一个线程串起来,我们的代码可以写A方法,团队合作的时候那个团队实现A方法,另外一个团队实现B方法,但是我们可以在A里面调B。但是运行期都是由一个线程执行的,A执行进入B方法以后由B方法去执行,执行完了返回继续执行A方法,这是阻塞式编程。
在Actor模型里面,所有的计算都是以Actor为单位进行,Actor之间的调用没有延时和阻塞,由Actor调用另外一个Actor,是直接给另外一个Actor发个消息,发完消息可以继续做别的,如果有消息的话可以继续处理,Actor是通过消息传输进行计算调用的,而不是直接的方法依赖调用的。
这个里面不同点有两点,他们之间没有依赖和耦合,前面我们说一个公司有两个团队在工作,一个团队写了A方法,一个团队写了B方法。A团队用A方法调B方法的时候,一定要依赖B方法的代码,一定要知道B方法的签名是什么样子,即使通过接口定义的话,一定要依赖这个接口,方法签名必须要知道,要知道它才能去调用,这是一种耦合。
第二,他们之间调用是阻塞的,我去调用B方法的时候,一旦调用B方法,B方法返回之前一定不能执行下一行代码,这是强耦合的阻塞。Actor将这两点都解决了,当两个Actor进行通信的时候,不需要去阻塞,也不需要依赖你,我就是把我的消息发送给你就可以,你能够去处理我的消息就可以,大家基于中间消息协议进行编程,不是基于方法依赖进行编程。发送者也是Actor,发送者要给另外一个Actor发个消息让他进行处理的时候,和A方法调B方法一回事,我调一个B方法,其实是让B方法帮忙我去处理,这跟我给他发一个消息处理也是一样子,逻辑上是一回事。当Actor给另外一个Actor发消息的时候,这个过程其实是持有另外一个Actor的Ref,这个Ref就是一个路径,只要找到Actor就可以。知道Actor以后,就通过ActorRef发消息,ActorRef把消息推到邮箱里面,然后Actor立刻返回,你什么时候处理我不管,发送完之后自己可以做别的。另外一个Actor检查邮箱里面是否有消息,如果有消息取出消息进行处理,处理完之后要调别的Actor消息发出去,自己也结束了。在这个里面一方面Actor之间没有依赖和耦合,我不需要知道你在哪里,这个Actor可能在一个机器里,也可能在天边,都没有关系,只要能把这个消息投递进去就可以,可以远程投递。另外没有阻塞,发给你以后你什么时候处理不管你,发给你以后我就结束了,可以继续做我的事情。
最终产生的效果,由一个线程去扫描所有的Actor邮箱里有没有消息,有消息就把消息拿出来让另外一个Actor处理,处理完以后看其他的Actor是否也有消息,一个线程可以扫描所有Actor。线程是很重的,但是Actor可以创建几百万个都没有关系,可以轻量的创建,如果没有消息大家都很安静,如果有了消息就取消息进行处理。理想情况下,一个线程可以扫描几百万,只用一个线程就可以做到,资源更省,调用更加的低耦合,操作更加的异步,无需阻塞。刚才提到的一些特性都是Actor提供给我们的。如果大家自己写Actor发送这些消息,去找这些消息或者是处理消息之类,这些是很痛苦的一件事情,我自己在英特尔时用Actor做过大数据的程序,有点小乱。如果日常用Actor进行开发还是有点痛苦,我们要做的Flower把Actor分装成Service,像传统的编程Service一样,只需要写Service代码后编排一下流程,这个Service结束以后下一个Service怎么样处理?在编程应用流程做一个包装。
使用这个框架做异步开发非常的简单,5分钟就开发完。第一步写个Service,进来以后总是一个个操作,比如说我们利用一个Service调用一个Service,写Service服务就好了,写服务用框架提供的Service接口,做的就是输入一个消息输出一个消息,这个Service要处理的消息是什么样?拿到消息以后在里面进行处理,这个时候Service就完成。要用多个Service去完成的话,我们其实提倡Service的粒度越小、功能越单一,越能够复用。整个的处理过程拆分成很小的Service,把这些Service串联起来就好,串联成一个流程在系统里自动的完成,串联成流程也非常的简单,可以编辑流程。Service写的时候并不知道下一个Service如何处理,但是写完以后变成流程,上手5分钟就可以。
这是Flower的可视化,可以通过编程的方式把ServiceABC编成一个流程,也可以用更复杂一点的,最好用可视化的脱拽方式进行编程,同时调用Service2和Service3,处理完以后交给Service5处理,Service交给Service4处理,Service4再返回Service3,最后汇总计算,用这样的方式完成编排,提供可视化。当然现在可以用文本编写进行编辑,要实现什么功能拖进来就好,当你要修改编码的时候,中间处理的时候请用户验证,加一个验证,也不需要去修改代码,需要加一个Service拖到流程里面多一个验证,代码完全不用修改。
如果用Spring编程的话,这个里是Spring调用的框架。
这是异步数据库的访问,系统会自动把结果拿回来交给下一个Service处理,中间不会等待数据库返回,不会停在那里。这个我们曾经遇到过一个案例,其中数据库里面一张表特别慢,也不是系统崩溃,系统崩溃还好,系统崩溃一旦连接失败后就失效,失效就返回。这个时候没有崩溃,发一个请求过去还能响应,响应5s、10s、20s这个时候就痛苦了,APP连接就超时了,这个时候一直给你一种慢查询,占据线程,线程一直不能释放。我们通过网关调用微服务,微服务访问数据库然后调用其他第三方服务。微服务访问这一张表里面就是特别慢,用了几十秒,微服务全部被阻塞。因为网关也是调的微服务,网关跟微服务之间也是同样,因为它没有响应,所以网关跟微服务之间的消息全都是延迟,导致网关的线程全部被微服务阻塞。网关被微服务阻塞以后,线程都被微服务占满,一张表导致所有的网关线程全部被锁死,所有请求都进不来,最后整个系统宕机了。这个微服务失效,对系统仅仅影响一些不太重要的业务场景,但是最后导致全部的服务停止。如果用Flower异步架构的话,不会出现这种问题,不占用任何线程,你失效以后失去响应了,顶多延迟的话用户通讯延迟,不会阻塞整个线程,导致整个系统崩溃。
远程怎么办?刚才提到远程通讯的异步微服务解决方案,服务是可编排的,这些Service可以跟网关应用程序放在一起,也可以分布式编排。分布式编排微服务有一个注册中心,在注册中心进行流程编排,第一个服务处理完之后下一个服务进行处理,然后进行可视化的编排。编排完以后,我要使用哪个流程进行请求数据?这是根据URL绑定的请求,请求进来以后就知道我通过哪个流程处理请求,到注册中心把请求拉下来,看看哪个服务需要,把请求发送给它,底层是Actor通讯,底层给到它之后,自己自动结束,处理下一个请求,我就发给它,然后到下一个Service,也会很好的异步化处理,处理完之后交给下一个Service,也可能是个远程的,可以通过Actor远程通信发送过去。一旦完成这个消息的投递本身就结束了,结束之后就可以继续处理。
这种情况下的远程编排变得更加的简单,因为调用之间没有任何的依赖。现在的编程依赖还是一个比较大的问题,一旦有了依赖,如果接口变化整个编程会非常的痛苦。而如果没有任何的依赖你,这个里面前一个请求是下一个流程,下一个请求又走到另外一个Actor的Service去了,非常轻松。场景就算比较复杂,有这样开发的功能,真正灵活用起来,我个人觉得还是比较憧憬。
为什么选择Flower?现在已经有一些反应式编程框架,总的来说Flower是反应式编程框架,Actor本身的编程有点不友好,对人们的编程习惯有些挑战,所以我们的技术用Flower这样的编程框架。它为什么不用Web Flux和RxJava,如果我不想要函数式编程,用反应式编程是被绑架的。其实你可以不用,反应式编程无阻塞的及时响应就可以了,我们可以很好的及时响应。还有个消息驱动,没有消息缓存,如果有大量的请求进来系统崩溃怎么办?MailBox是有溢出机制的,MailBox接收10个消息和20个消息,系统不会崩溃的。以前系统架构一个解决方案处理不了那么多的请求怎么办?那就限流,限流在请求的时候拒绝,到底什么时候拒绝我不知道,现在来说我们用MailBox任何一个Service处理不过来的时候,它自动就溢出了,溢出就把消息丢弃了,也可以不丢弃,放在一个什么地方,重新走一个通道继续处理也可以,而且非常的灵活,消息驱动。更好的性能和更低的成本,更高的可能性。成本非常低,如果用的话几分钟能搞定,用起来好用,不需要自己搞一套比较痛苦的东西。