两件工作中的“事故”

有一次,公司让我重构一个广告的系统,所谓的广告,就是预先设置好一个广告位,然后在运营后台通过配置上传图片和链接一个广告的展示就完成了,当然广告不仅仅只是展示一张图片就结束了,还有两个主要的功能其一就是要根据用户的自身信息和业务属性做出定向投放(就是在app 的不同位置展示,但是每个人展示的都不一样),这就需要将广告的配置与用户的若干业务属性进行一个对比和判断,但是这个系统存在一个重要的问题就是

 公司使用的是微服务架构,这几个业务属性都需要调动不同的服务,这里面的几个服务响应都不够快,有几个的响应时间都是接近几百毫秒的,如果串行的调用这几个服务,那我的接口的响应时间就至少要等于这几个服务的响应时间之和也就是几秒。导致这个服务响应时间过久的问题

当时的我首先想到的解决方案就是,要用异步请求来请求多个服务,然后将多个请求的结果进行结合计算,这样最终的响应时间就不是所有服务的响应时间之和,而是最慢的那个服务,然后就将这个设计沾沾自喜的提交了,自测了一下性能比之前还要快很多。

但是很快就会有测试反馈广告展示的慢,当时我首先的想法是不可能,一定是哪里搞错了,我都自测过了,比原来快到不知道哪里去了,怎么会比以前慢了呢?不过我还是调查了一番,原来有另外一个机智的同事,在做app首页,这个首页要展示十几个广告位,他和我想的一样,要想快速响应,那就也异步的调用我,从我这里来看也就是一瞬间来了十个请求,而我的每个请求呢又要以一定的并发调用好几个服务,这样就相当于我要一瞬间,发几十个请求,而我发异步请求的线程池大小也就十几 二十几的样子,几乎是一瞬间线程池就耗尽了,后面的几十个请求就相当于放到了阻塞队列里面,这样原本的异步就退化成了串行 甚至还不如原来的速度,不过我没有气馁,既然是线程池耗尽了那就把线程池调大调到100就好了,我果然还是很机制,自测了一下好像比原来快了一些了。我果然很机制,提交了之后将这个代码提交上去。。。

但是过了一会测试还是反馈慢,而且这次不仅仅是慢,而且这次内存cpu使用率都比以前要高好多,原本一个广告服务只用500m内存,现在要接近一个G,而且首页展示的瞬间cpu使用率还会飙升,我迷茫了,小小的广告竟然如此棘手,真的后悔嫌前任的代码丑就擅自重构,但是既然都走到这一步就只能硬上了,这次采取一些麻烦一点的操作,首先我让每次查询一下其它的服务的时候都将其的结果加一层redis缓存,其次我将业务属性也加了一层缓存,并且写个定时任务,将业务属性的结果定时往缓存上刷,然后测试终于不再反馈慢的事情了,只不过原来的异步操作仿佛没有用上,我当时的小机制并没有起到任何作用。

这件事就这么过去了,但是这件事的阴影没有过去,相当长的时间我都希望可以找到更优雅的方式来解决这个问题,因为我总感觉这个东西用异步肯定是没错的,只是现在的异步的代价是要消耗"有限的线程池"的时间,而线程池是不能无限调大的,其一是线程池会占用非常多的内存的,其二就是线程池一旦设置过多又会有频繁的上下文切换,导致cpu使用飙升,这仿佛是个无解的问题,但是我还是找到了三个最接近差不多能解决这个问题的方案。

 1 就是Scala 的 Akka :1个G最多可以创建2000个线程,但是却可以创建500万的Actor ,每个Actor可以当一个线程来用

 2 协程 不是订酒店的携程,是轻量级的线程,其在并发的事情和Akka差不多也是可以用极少的内存 有很高的并发 但是java没有原生的支持 所以也没太细看

 3 响应式编程,最主流的框架就是WebFlux ,写起来感觉和函数式编程差不多。

这三个都试过,但是当时自测都是用Thread.sleep来模拟慢的io,让我与真正解决这个阴影擦肩而过,不了了之

后来我进了一下大数据公司,参与开发了一款ETL工具,这款工具简单说就是将各种数据库中的数据迁移到大数据平台,这里迁移的就是表中的数据,我当时负责的就是这个调度引擎的开发,就是构件一个迁移的任务,每个任务有子任务,子任务有状态,我要监听每个任务的状态,我也是重构旧的系统自己写了一个新的,很快问题就出现了,有一天一个测试构建了一个2000张表的任务,这个时候2000个并发就过来了,又是熟悉的cpu飙升,系统卡死,等一系列问题,但是经历过上次的阴影已经有所准备了,这次通过以减法代替加法的思路,我通过减少执行器线程数,让上述问题解决了,但是因此吞吐量也下降了,好处是不会拖慢整个集群了,不过我的那个心理阴影又加深了一层,面对这个问题我需要一个完美的方案,期间协程我还找到了java的实现方式,但是试了一下 和 Akka Reactice还是老样子 面对Thread.sleep都是会吞吐量下降,这仿佛是个诅咒,一个永远都绕不过去的诅咒。

然后我又进到这家公司,我又要接一个与这个问题相关的一个需求,概括一下就是我要对接两个模块的数据,这里就涉及到要推拉数据的操作,推数据就是将数据推到Kafka然后我们来消费比较简单,问题是拉数据,因为数据被拉的对象有很多但是对方的相应时间很慢,性能还很有限,我们不仅仅不可以并发请求,还要加限流,这样要请求很多,但是还不能请求很快,我们任务就有可能会大量积压,面对这种需求 我们当然可以加机器,但是请求量和机器根本不是一个量级的,也许是困扰我太久才能产生的灵感,脑中忽然浮现出nio这个词。我发现我以前包括这个问题都存在一个共同特性。

就是因为阻塞式io + 线程的问题

阻塞式io都会有一个问题,就是在它返回结果之前,它会绑架当前的线程,当前的线程除了“等”什么都不能干,这就是阻塞式io,正是因为这个特性,在有限的线程池下 面对大量的请求,线程池就会耗尽,带来整个任务被阻塞系统崩溃的问题,这种问题通过加线程可以一定程度缓解,但是线程过多又会带来内存占用过多,上下文切换的问题,因此使用nio

然而nio,就意味着拿数据和,主线程不在一个线程内,使得开发难度陡然上升,而go语言的协程就是为了简化nio的使用难度的,akka 也是 响应式编程也是,三者的本质都是为了简化nio的使用的,之前我使用Thread.sleep模拟的都是bio,因为它需要和操作系统进行一次中断。仅此而已

我的阴影终于得到彻底的解决,三者的目的都是殊途同归的,不过前提是要结合非阻塞io才有意义,至于三者选择什么,我认为主要看语言,java内部我推荐用WebFlux 因为有java11 和spring的官方支持,尤其还有r2dbc的数据库驱动支持,得以让整个响应式形成一个逻辑闭环,go语言就不用多说了,akka 我感觉除了简化nio之外应该还存在别的意义但是仅仅简化nio这一点,akka我是不建议使用的

你可能感兴趣的:(两件工作中的“事故”)