• 菜鸟与高并发与性能优化


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

    项目背景

      这里我尽量描述一下,但涉及到一些客户相关信息的都会隐去或替换,各位看官见谅哈。举例来说就是大概客户有几百个柜台提供给客户的客户,这几百个柜台会实时的产生一些“交易”信息啊,验证信息啊等等。但之前这些操作都必须全程联网,如果断网了就只能停止使用了。另一方面,如果服务器需要升级维护,也是会影响柜台的操作,因此那边的维护小哥也只能半夜至凌晨来进行维护了。二狗所在的这个组的目标就是提供一个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时貌似只有一台机器会执行,这里有一些改进来使得其正常执行(这里是老大搞定的,还没来及瞅瞅代码,晚些再补上吧)。

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

      

  • 相关阅读:
    解决阿里云服务器磁盘报警
    linux服务器启动报错UNEXPECTED INCONSISTENCY解决方法
    记一次gitlab添加用户收不到邮件的解决办法
    php7安装redis拓展
    centos6.5安装部署zabbix监控服务端和客户端
    centos-6.5安装部署LNMP环境
    centos6.5编译安装php7
    centos6.5新增加硬盘挂载并实现开机自动挂载
    简单快速部署samba服务器
    第177天:常用正则表达式(最全)
  • 原文地址:https://www.cnblogs.com/dogtwo0214/p/12151549.html
Copyright © 2020-2023  润新知