菜鸟与高并发与性能优化

  二狗也从头到尾做过几个项目了,前后端也都打过酱油,数据库也能鼓捣一把,却还没真正的整过高并发相关的东西。正好趁着项目组变动,有机会参与到某澳门项目的开发,也算是亲身体验了一把“高并发”。

项目背景

  这里我尽量描述一下,但涉及到一些客户相关信息的都会隐去或替换,各位看官见谅哈。举例来说就是大概客户有几百个柜台提供给客户的客户,这几百个柜台会实时的产生一些“交易”信息啊,验证信息啊等等。但之前这些操作都必须全程联网,如果断网了就只能停止使用了。另一方面,如果服务器需要升级维护,也是会影响柜台的操作,因此那边的维护小哥也只能半夜至凌晨来进行维护了。二狗所在的这个组的目标就是提供一个Offline的模式出来,这样即使因为天气或是别的什么原因网络不通,也不会影响客户来为客户的客户进行服务。而一旦网络通畅以后呢,再将离线期间产生的数据同步到服务器中,做处理或者验证。

  如果只是一两台机器网络不通,那么它回复到网络Online状态时其实并不会产生多少数据,Server和DB也可以很轻松的处理完。但如果是由于别的原因导致大量机器不通(如网络电缆被挖断),那么当他们同时恢复到Online状态时就很可怕了。几百台机器的数据需要处理。

  这些消息自然是丢到EMS中去处理,这一部分我们先暂且不细说。但这些消息在同步完成之前是会阻塞用户进行进一步操作的,因此我们需要在Local那边不断的去向Server发送请求(下称check1),验证是否已经完成了同步。这样对每一个Event来说就有下面几种状态:

 

UploadStatusEnumValueMeaning
NotUpload 0 生成了Event,但未上传至EMS
Uploaded 1 已经上传至EMS,超过时间间隔需要重发
Acknowledged 2 已经从Server确认,Server已经收到了这条Event,但Server还没有处理完成,不需要重发了
Processed 3 Server处理完成

 

  由于一些原因送往EMS的消息有可能会丢失,因此我们去向Server不断发送检查数据有没有完成处理的请求也会去重发丢失的请求。

  想了想毕竟有许多细节不方便描述,还是采用Q&A的方式记录一下吧,顺便记录一下自己的一些想法.

 

Q&A

  1.假设从离线模式切回上线模式时上传到EMS中1000条交易数据,为了降低Server端的压力,check1并不会一次性将所有符合条件的LocalUploadEvent都发往Server进行验证。而是按照不同的Type分开配置。但这种方式在极端情况下会出现一种问题,即本地有100条记录需要被check,我们配置每次取10条发往Server进行check,但这10条由于一些特殊的原因始终不会被Server接受、处理。因此Local始终不会更新这10条记录的状态,此时CheckUploadStatusSchedulerService被“Block”在这10条记录,后面的90条记录永远没有机会发往Server进行检查。

  因此在柜台pc那边发送Check时会同时更新被Check数据的最后check时间,并按升序排序,用来保证所有Event都有机会被检查到。

为了降低Server端的压力,CheckUploadStatusSchedulerService并不会一次性将所有符合条件的LocalUploadEvent都发往Server。目前是按照EventType分开配置。但这种方式在极端情况下会出现一种问题,即本地有100条记录需要被check,我们配置每次取10条发往Server进行check,但这10条由于一些特殊的原因始终不会被Server接受、处理。因此Local始终不会更新这10条记录的状态,此时CheckUploadStatusSchedulerService被“Block”在这10条记录,后面的90条记录永远没有机会发往Server进行检查。

  因此Local在CheckUploadStatus时会同时更新被Check数据的字段LatestCheckStatusTime,保证所有Event都有机会被检查到。

  2.由于客户交易Local PC的限制,只能使用较低版本的.net环境版本进行开发,因此有一些写法不得不改写。

  具体PC使用的目标框架版本为.NET Framework 4.6, EntityFramework 6.0.0.0 SQLite.CodeFirst 1.5.2.28. 在此配置下会有一些代码书写方面不得不妥协的地方,如在批量Update的时候,不可以使用linq去单独更新每一项,只能在循环中为每个要被更新的Entity赋值,再单独更新。作为码农,肯定是希望用上更“优雅”的方式,但有时不得不做一些妥协。好在单个PC的数据量不会特别大,这么写的影响也不会特别大。

      3.需要同步的数据可能有不同的类型,混在一起使得部分需要优先处理的数据较长时间的等待。

  分出两个不同的队列来接收消息,server端接收时也会从这两队列中去分别处理,我们可以通过提高要优先处理的队列的server端接收者的数量来进行控制。

     4. 开始模拟真实环境测试时遇到了一些性能方面的问题,比如服务器端内存不断的提高始终不下降。

  C#的垃圾回收其实已经很不错了,时机到了会自动的释放掉空间,但我们观察到的结果不是这样,一开始刚启动只占用了大概几十M,后来慢慢提升到几百M,第二天早上再去检查时已经几乎占用了全部的内存。这里我们是这样的处理,一是利用Visual Studio的性能探查器去查看内存的变化,另一边是分析代码查看哪里有可能的内存泄露或者可以优化的地方。再者由于服务器上同时还部署了别的服务,我们不能再让这样的内存几乎用尽的情况发生,就设置了IIS的Receyle。具体为

  • Start Mode设置为OnDemand
  • Private Memory Limit (KB) 1048576
  • Regular Time Interval(minutes) 30

  即启用Recycling,当时间超过30分钟或内存占用超过1048576KB时就会自动进行Recycling。

  另一方面,发现关于Linq的一些小秘密。当我们同时使用EntityFramework和linq去join时会有这样一些情况:

  1. 内存与内存join
  2. 内存与Entityframework(这里方便起见就直接称为DB)join
  3. DB与DB的join

  问题就出在第二种情况里,虽然我们为join添加了匹配条件,但是EntityFramework仍会将整个Table中的数据全部加载到内存中,然后进行join,这可能是我们的server内存不断升高的一个重要原因。具体两种写法如下:

1 // 有问题的写法
2 // ...
3     join dbTable in this.store.Find<dbTable>()
4     on memooryList.Guid equals dbTable .Guid into result
5 // 上述写法会将dbTable中的全部数据load到内存中再进行join
// 更新后的写法
// ...
    join dbTable in this.store.Find<dbTable >().Where(x => MemoryGuidList.Contains(x.Guid))
    on memooryList.Guid equals dbTable .Guid into result
// 上述写法会将dbTable中的数据filter之后再取出

  另外,由于一些原因,db中某张表上同时存在主键(非聚集索引)与另一字段A(聚集索引),我们希望使用聚集索引来进行更新数据,但Entityframework貌似自动选择了主键,且无法选择别的索引,只能上raw sql了。

  5.前面有介绍过,当我们的系统从Offline状态切回Online状态时,所有的柜台都会同时发送离线时的数据,然后一遍又一遍的去向服务器端发送请求,我的数据处理好了没有呀?有没有感觉很类似,对,就是春运抢票的感觉。这里我们发现了一个问题,比如我们配置每分钟的第一秒去发送请求,然后每分钟发送一次。几百个请求几乎同时达到,然后再一分钟又循环一次。虽然local每天机器的时间可能不完全一样,但是这样的峰值仍然是会出现的。

  为此,我们根据每台柜台的编号取哈希值来尽量使得请求分散开来,使得每段时间内到达的数据量尽量统一起来。

  6.服务端的一些改进

  项目初期我们发现跑了一晚上的数据,第二天消息队列中还有几十万条数据。这个情况其实很可怕的,原因在于,消息队列中还存有这么多数据说明我们的服务器端还没有接收下来,因此local去不断检查时会发现哦原来服务器没收到,那我应该重发了,于是消息队列中的消息越来越多。即产生消息的速度高于了服务器端接收的速度。除了使用load balance来增加机器以为,我们分析代码意识到,其实接收方(消费者)是类似于这么一个逻辑。接收消息,然后去进行处理(由于流程很复杂,这里的时间很长,且几乎不能提升),同步到DB中,然后再接收下一条。虽然有多个线程同时去跑,仍然是速度很慢的。

  你也一定意识到了上面的问题,接收消息这个动作其实可以很快,这里确实是由于一开始没有思考清楚,造成了耦合。于是我们改写成了直接接收消息,然后再起不同的job去完成这些消息的处理。果然没有再发生这样的消息堆积的情况。

  7.由此引出的问题

  由于使用的是Quartz来进行定时job的功能实现,当不同机器同时run时貌似只有一台机器会执行,这里有一些改进来使得其正常执行(这里是老大搞定的,还没来及瞅瞅代码,晚些再补上吧)。

  今天不早了,回来再补齐吧,还有一些测试的例子以及最终的结果也可以整理一下。虽然这几周很忙,但感觉还不错。

  

posted @ 2020-01-15 19:23  DogTwo  阅读(381)  评论(0编辑  收藏  举报