zoukankan      html  css  js  c++  java
  • NetCore踩坑记2、喜!悲?项目演进的分享和EntityFrameworkCore升级之后的奇怪现象

    随着微软对.NET Core2.2停止支持,我们也将项目从.NET Core2.2 升级到.NET Core3.1[LTS]以寻求最新的安全支持。
    这其中有一些不大不小的改动,但通过文档中心的指南,我们基本完成了版本的跨越。
    由于变更的积压,我们在经过简单的测试之后就将升级后的服务推送到了生产环境,不充分的测试也为后面的问题埋下了种子。

    提要

    我们的服务运行在阿里云提供的服务上,而不是自有服务器

    我司主营业务为内部交易设备的生产制造,销售,服务支持和售后;我们的主要客户是工厂/学校/医院食堂以及美食城。
    这意味这我们服务的高峰与三餐(宵夜的人其实也不少,严格的说是四餐)的时间重合,平时没有太多流量,但在就餐时间流量会激增。

    我们服务于全国3000+个客户,所以我们的项目是一个有3000+租户的Saas服务,我们对客户使用编号来标识他们的身份,也通过客户的编号对客户进行数据的隔离。

    过去我们采用直接分库的方式进行数据隔离,即 DB_{客户编号} 的数据库命名方式,这样是最优的隔离方式,也最有利于数据维护。
    但在前年,阿里云来我司进行游说,在阐述了一些优点之后,我们放弃了自建数据库,改为采用他们的RDS for SqlServer 2008R2,由于他们的限制(2008R2 数据库必须从阿里云后台新建,且每个实例只能新建50个数据库,不知道限制有没有改),我们不得不将隔离结构改为分表隔离,每个客户的数据表都是一样的,如果有客户需要定制功能,我们会引导他们进行数据的本地化,这使得我们的数据库结构相对稳定,我们进行分表隔离,对每个客户的每个数据表加上客户编号作为隔离标识,即采用 {客户编号}_数据表名 的命名方式,这样的方式使得数据库的可维护性变差了。

    团队规模不大,后端开发只有4人;我们选择使用ORM(EntityFrameworkCore)作为数据库访问的媒介,这大大的提升了我们团队的效率,但也带来了一些问题;比如,我们需要对EntityFrameworkCore的上下文进行复用,并实现对数据的隔离。

    1、我们如何应对访问的峰谷变化

    对于我们这种峰谷差异明显的服务来说,很明显的,我们需要弹性来对资源进行协调,以求获得可用性和成本的最大化收益,这其实也很容易找到解决方案,比如K8S。
    但这个方案并不是特别适合我们,我们现在处于一个业务迁移的时期,项目中还有很多依然运行在.NET Framework的部分,他们是和设备通信的Socket,采用Supersocket v1.6实现,他们是我们需要进行弹性的主要资源,是流量的主要入口,但它也是Windows服务,是无法一时半刻升级到.NET Core的部分。
    最终我们选择使用阿里云提供的 弹性伸缩(Auto Scaling)直接对ECS实例进行弹性伸缩,这样可以让我们不加任何改动就能轻松实现弹性。

    2、我们如何实现EntityFrameworkCore的上下文复用并且实现数据的隔离

    由于数据库结构的特殊原因,很多客户的数据其实是在一个数据库上的,分不同的数据库编号存储,这使得我们需要对每个客户进行Mapping,而不是简单的更改链接字符串。
    如下代码片段;我们全程使用FluentApi进行Mapping配置(不是必要的),并通过ToTable在运行时映射到实际访问的客户队友的数据表。

     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         modelBuilder.Entity<Table_SystemConfig>(entity =>
         {
          //...省略部分话题无关的Mapping配置
          entity.ToTable($"{CompanyCode}_T_SystemConfig");
         });
     }
    

    单凭这样的改动是不行的,EntityFrameworkCore会在第一次Mapping之后对Model进行缓存,进入缓存之后,Model和Table的Mapping关系即确定并缓存,这会导致下一个其它客户的请求进来时,由于Mapping关系的缓存,将错误的访问到第一个访问系统的客户的数据。有两个方式来解决这种问题
    *1、获取到Model的缓存后更改其Mapping关系,使其访问正确的数据。
    *2、缓存多份Model,使得每个客户获取到的Model包含正确的Mapping关系
    以上所述两种方式,暂且不谈可实现性;
    两种方式
    *1 将付出算力的代价。
    *2 将付出存储的代价(因为Mapping基本不能被序列化,因此只能缓存在内存中)。
    在CPU和内存的取舍中,最终选择舍弃内存来换取更加宝贵的算力资源,通过查询资料,我们最终找到了这样的实现方式,见以下代码片段。

        public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
        {
            public object Create(DbContext context)
            {
                return  $"{context.GetType()}-{(context as CompanyDbContext).CompanyCode}";
             
            }
        }
    
        public class CompanyDbContext: DbContext
        {
            public string CompanyCode{ get; set; };
    
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
            }
        }
    

    如上代码片段,通过重新实现IModelCacheKeyFactory接口并替换EntityFrameworkCore的默认实现,实现了预期的功能,经过测试,实际效果和预期一致。

    我们付出了内存的代价实现了功能,最终在生产环境表现为每个客户消耗了300kb的内存(每个客户大约有50个表),这样3000+的客户,我们实际上使用了1GB+的内存,并且这些内存不会在闲时被释放。

    2.2到3.1的升级EntityFrameworkCore遇到了什么问题

    前面除了付出了一些内存的代价外,一切都很稳定,这时候.NET Core3.1更新了,我们权衡利弊,决定升级到3.1版本,毕竟它是LTS版本,相对应的,EntityFrameworkCore也从2.2.x的版本升级到3.1.x的版本
    除了一些Api的更名,EFCore基本是没有什么使用上的改动。于是我们升级完之后就推送到了生产环境。
    即使是在业务的高峰,预期的1GB+的内存并没有被消耗,这让我很奇怪,学校都不恰饭了吗?
    升级后,内存的消耗维持在200MB+,太棒了!
    CPU使用高了许多,有问题!但回退已经来不及了。只好多加几个实例,短时间内解决问题,后面到年关了,不能再有大的改动,因此生产环境使用了堆配置的方法暂时解决了问题。
    【COVID-19来了】本计划年后就来解决的这个问题,一直被拖到了4月。

    我们如何定位

    重新审视这个问题,内存降低+CPU升高,难道是缓存没有命中?我首要怀疑这个原因,也整着手从这个角度去寻找,和解决问题。
    EntityFrameworkCore是一个开源项目,并且我能获取到它各个版本的源码,这给我解决问题带来了极大的方便。
    我比对了2.2.x版本和3.1.x版本的源码,发现了一些端倪。
    如下两个片段

        //EntityFrameworkCore version 2.2.8
        public class ModelSource : IModelSource
        {
            private readonly ConcurrentDictionary<object, Lazy<IModel>> _models = new ConcurrentDictionary<object, Lazy<IModel>>();
    
    
            public ModelSource([NotNull] ModelSourceDependencies dependencies)
            {
                Check.NotNull(dependencies, nameof(dependencies));
    
                Dependencies = dependencies;
            }
    
    
            protected virtual ModelSourceDependencies Dependencies { get; }
    
    
            public virtual IModel GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
                => _models.GetOrAdd(
                    Dependencies.ModelCacheKeyFactory.Create(context),
                    // Using a Lazy here so that OnModelCreating, etc. really only gets called once, since it may not be thread safe.
                    k => new Lazy<IModel>(
                        () => CreateModel(context, conventionSetBuilder, validator),
                        LazyThreadSafetyMode.ExecutionAndPublication)).Value;
          //省略了一些无关的代码
          }
    
        public class ModelSource : IModelSource
        {
            private readonly object _syncObject = new object();
    
    
            public ModelSource([NotNull] ModelSourceDependencies dependencies)
            {
                Check.NotNull(dependencies, nameof(dependencies));
    
                Dependencies = dependencies;
            }
    
    
            protected virtual ModelSourceDependencies Dependencies { get; }
    
    
            public virtual IModel GetModel(
                DbContext context,
                IConventionSetBuilder conventionSetBuilder)
            {
                var cache = Dependencies.MemoryCache;
                var cacheKey = Dependencies.ModelCacheKeyFactory.Create(context);
                if (!cache.TryGetValue(cacheKey, out IModel model))
                {
                    // Make sure OnModelCreating really only gets called once, since it may not be thread safe.
                    lock (_syncObject)
                    {
                        if (!cache.TryGetValue(cacheKey, out model))
                        {
                            model = CreateModel(context, conventionSetBuilder);
                            model = cache.Set(cacheKey, model, new MemoryCacheEntryOptions { Size = 100, Priority = CacheItemPriority.High });
                        }
                    }
                }
    
                return model;
            }
            //省略了一些无关的代码
        }
    
        public sealed class ModelSourceDependencies
        {
            [EntityFrameworkInternal]
            public ModelSourceDependencies(
                [NotNull] IModelCustomizer modelCustomizer,
                [NotNull] IModelCacheKeyFactory modelCacheKeyFactory,
                [NotNull] IMemoryCache memoryCache)
            {
                Check.NotNull(modelCustomizer, nameof(modelCustomizer));
                Check.NotNull(modelCacheKeyFactory, nameof(modelCacheKeyFactory));
                Check.NotNull(memoryCache, nameof(memoryCache));
    
                ModelCustomizer = modelCustomizer;
                ModelCacheKeyFactory = modelCacheKeyFactory;
                MemoryCache = memoryCache;
            }
        }
    

    可以看到,在缓存Model的时候,实际上是发生了一些变化的,缓存从ConcurrentDictionary 变成了IMemoryCache,查找EFCore内部注入的IMemoryCache 的来源发现,它来自于

      //其它代码太多就不贴了  源码相关文件 srcEFCoreInfrastructureEntityFrameworkServicesBuilder.cs
      TryAdd<IMemoryCache>(p => new MemoryCache(new MemoryCacheOptions { SizeLimit = 10240 }));
    

    看到这里就知道问题所在了;2.2.x缓存Model单纯的使用ConcurrentDictionary ,变成了使用MemoryCache,由Microsoft.Extensions.Caching.Memory包提供实现。
    在Model被创建后放入MemoryCache时,给Model设置的占有空间为100,而MemoryCache被初始化为空间大小10240,简单即可得知,当缓存最多102份(其它地方也使用了这个MemoryCache缓存数据)时,缓存空间即满;
    后续会移除之前的缓存以腾出空间给新的缓存;要缓存3000+份的Model,是远远不够的,这就造成了高峰时MemoryCache不断的写入新项,移除旧项,GetModel的过程(即为FluentApi构建Mapping关系的过程)一直在进行,真正命中缓存的很少;导致算力消耗增多,内存反而下降。

    我们如何解决

    找到问题之后,从缓存入手,就很好解决问题了,几个角度
    *1 增大MemoryCache实例的[空间?]最大值(使用新的MemoryCache实例替换掉原本的实现)
    *2 减小每个Model在MemoryCache中的[空间?]占用
    当然 设置缓存存活时间,配置缓存刷新时间,减少访问频率低的缓存留存,这样能减少不必要的内存消耗,并且算力的负担也不会增加太多,我认为这样更好。
    注: 这个空间是指定尺寸,并不是真实的占用内存大小,它的作用是控制缓存的上限

    记录此次的解决问题的过程,希望对你有所帮助,撰写仓促,如有错误,请指出,我将及时更正。

  • 相关阅读:
    [Go] golang 两个数组 list 的合并方式
    [Go] assignment count mismatch 1 = 2
    [Go] golang 时间格式化 12小时制 与 24小时制
    [Go] freecache 设置 SetGCPercent 的作用
    [FAQ] Vue 如何控制标签元素的某个属性的显示 ?
    [FE] 实时视频流库 hls.js 重载切换资源的方式
    [FE] 关于网页的一些反爬手段的解析思路,比如 58 等
    [Go] 让 go build 生成的可执行文件对 Mac、linux、Windows 平台一致
    [Go] go build 减小二进制文件大小的几种方式
    [Go] gin-jwt 业务逻辑中使用实例化的 middleware 的方式
  • 原文地址:https://www.cnblogs.com/for-example/p/12955788.html
Copyright © 2011-2022 走看看