.NET ABP.Zero 项目疑似内存排查历程

news2024/11/17 8:46:16

        当前项目是 .NET 5 EentityFrameworkCore,疑似内存泄漏,之所以说是疑似是因为到目前位置还没有能准确的定位到问题。当前这个框架从 .NET Core 2.1 就开始用,期间有升级到 3.1、5.0、6.0,在排查过程中还把 5.0 分支升级到了 7.0 。不幸的是这些分支都存在疑似泄漏的现象。

        项目部署在 Linux 中的 Docker,Linux 有 RedHat、CentOS、Deban,Docker 版本也是多个版本。这个项目很多分支版本,现在所在公司的主要业务存在很多相似的需求,当出现客户定制化需求的时候就会做差异化代码甚至单独切一个分支出来。根据目前的情况来看问题应该是在 .NET Core 2.1 的时候就已经存在。

        发现这个问题是在大约是在2023年的6月份,这篇过程记录写于10月份,问题的现象是部署的 Docker API 服务内存占用从刚刚启动时的 3xx MB然后一直涨,只要有人访问就涨。不同的项目存在些许差异。这里主要分析的是两个 .NET Core 3.1 和 .NET 5.0 版本的两个分支项目。

        这里虽然提到 .NET 版本并不是说版本存在问题,这里可以排除,首先作为大厂 Release 版本的分支理论上出现这种情况的概率小之又小,另外当前公司存在另外一个 .NET 6 项目使用不同的框架而并没有出现内存泄漏的现象。所以目前怀疑的主要是业务代码。版本只是区分不同项目。

        这两个项目的存在使用上的明显的区别:

        .NET Core 3.1:

        背景:这个项目使用人少,但是接入物联网设备数百个,一直处于持续运行被动接收数据的状态几乎没有喘息的机会。

        现象:内存上涨缓慢(同比 .NET 5 项目),一天 200+ MB 的涨幅。

       

        .NET 5:

        背景:这个项目使用的时段主要是工作日的白天,其中有几个接口的数据单次拉取大的时候有几十上百兆。晚上几乎没人用。

        现象:无人访问时内存稳定,在调用大数据接口后暴增至 1.xx ~ 2.xx GB,在月末月初使用高峰时期,内存占用持续增加至物理内存上限,然后 Docker 自动重启。(服务器有升级见正文)

1. 首先想到的是存在没有释放的内存。

排查方案:使用 using 释放内存

于是排查整个项目中的代码,到处尝试加 using 手动释放资源。这个过程连续搞了大约两周,此后断断续续都在反复看代码,应该是把整个项目的代码都看完了。这段时间看代码都会自带特效,几乎一眼就能看出哪些需要加 using。这个滤镜有一个后遗症,就看大部分代码都可以加。感觉不能加的都要手动去加一个试一下,万一呢 ~~~

结论:没有效果 ~~~ 可能是因为当前项目主要是原生代码,这些 GC 都有安排就算没用手动释放,只要没有一直处于被应用状态,都会被安排回收。期间写了下面这段代码,每隔一分钟输出当前 API 情况,就是通过这个观察到 Docker 是在内存耗尽时重启的。

        #region 系统资源监测

        [DllImport("kernel32.dll")]
        public static extern void GlobalMemoryStatus(ref MemoryInfo32 meminfo);

        [DllImport("kernel32.dll")]
        public static extern void GlobalMemoryStatus(ref MemoryInfo64 meminfo);

        public void GetSystemInfo()
        {
            var stopwatch = Stopwatch.StartNew();
            try
            {
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                {
                    var meminfos = System.IO.File.ReadAllText(@"/proc/meminfo").Split(' ').Where(o => o != string.Empty).ToList();
                    var remainPercent = decimal.Parse(meminfos[5]) / decimal.Parse(meminfos[1]) * 100;
                    // if (remainPercent > 79) Collect();
                    Logger.Info($"============================> 物理内存:{GetUnit(meminfos[1])}");
                    Logger.Info($"============================> 已用内存:{GetUnit(meminfos[3])}");
                    Logger.Info($"============================> 可用内存:{GetUnit(meminfos[5])}");
                    Logger.Info($"======================> 剩余内存百分比:{remainPercent:N2}%");

                    /*
                     Logger.Info(meminfos.ToJsonString());

                     ["MemTotal:","8008064","kB\nMemFree:","1924628","kB\nMemAvailable:","4107656","kB\nBuffers:","15132","kB\nCached:","2466712","kB\nSwapCached:","0",\
                    "kB\nActive:","3761208","kB\nInactive:","2060276","kB\nActive(anon):","3373112","kB\nInactive(anon):","88024","kB\nActive(file):","388096",
                    "kB\nInactive(file):","1972252","kB\nUnevictable:","0","kB\nMlocked:","0","kB\nSwapTotal:","0","kB\nSwapFree:","0","kB\nDirty:","280",
                    "kB\nWriteback:","0","kB\nAnonPages:","3337920","kB\nMapped:","212864","kB\nShmem:","121496","kB\nSlab:","118600","kB\nSReclaimable:","88208",
                    "kB\nSUnreclaim:","30392","kB\nKernelStack:","4656","kB\nPageTables:","15832","kB\nNFS_Unstable:","0","kB\nBounce:","0","kB\nWritebackTmp:","0",
                    "kB\nCommitLimit:","4004032","kB\nCommitted_AS:","4199784","kB\nVmallocTotal:","34359738367","kB\nVmallocUsed:","20180","kB\nVmallocChunk:","34359684828",
                    "kB\nPercpu:","880","kB\nHardwareCorrupted:","0","kB\nAnonHugePages:","1921024","kB\nCmaTotal:","0","kB\nCmaFree:","0",
                    "kB\nHugePages_Total:","0\nHugePages_Free:","0\nHugePages_Rsvd:","0\nHugePages_Surp:","0\nHugepagesize:","2048","kB\nDirectMap4k:","81408",
                    "kB\nDirectMap2M:","4112384","kB\nDirectMap1G:","6291456","kB\n"]
                     */
                }
                else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                {
                    if (RuntimeInformation.OSArchitecture == Architecture.X64)
                    {
                        var memInfo = new MemoryInfo64();
                        GlobalMemoryStatus(ref memInfo);

                        var remainPercent = memInfo.AvailablePhysical / (decimal)memInfo.TotalPhysical * 100;
                        // if (remainPercent > 79) Collect();
                        Logger.Info($"============================> 物理内存:{GetUnit((long)memInfo.TotalPhysical)}");
                        Logger.Info($"============================> 已用内存:{GetUnit((long)(memInfo.TotalPhysical - memInfo.AvailablePhysical))}");
                        Logger.Info($"============================> 可用内存:{GetUnit((long)memInfo.AvailablePhysical)}");
                        Logger.Info($"======================> 剩余内存百分比:{remainPercent:N2}%");
                    }
                    else if (RuntimeInformation.OSArchitecture == Architecture.X86)
                    {
                        var memInfo = new MemoryInfo32();
                        GlobalMemoryStatus(ref memInfo);

                        var remainPercent = (decimal)memInfo.AvailablePhysical / (decimal)memInfo.TotalPhysical * 100;
                        // if (remainPercent > 79) Collect();
                        Logger.Info($"============================> 物理内存:{GetUnit((long)memInfo.TotalPhysical)}");
                        Logger.Info($"============================> 已用内存:{GetUnit((long)(memInfo.TotalPhysical - memInfo.AvailablePhysical))}");
                        Logger.Info($"============================> 可用内存:{GetUnit((long)memInfo.AvailablePhysical)}");
                        Logger.Info($"======================> 剩余内存百分比:{remainPercent:N2}%");
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Error($"系统信息获取异常:{ex.Message}");
            }

            try
            {
                using var proc = Process.GetCurrentProcess();
                Logger.Info($"============================> 启动时间:{proc.StartTime}");
                Logger.Info($"============================> 运行时间:{Clock.Now - proc.StartTime}");
                // Logger.Info($"====================> 2小时登录/总人数:{login}/{total}");
                Logger.Info($"==============================> 线程数:{proc.Threads.Count}");
                Logger.Info($"============================> 虚拟内存:{GetUnit(proc.VirtualMemorySize64)}");
                Logger.Info($"========================> 应用峰值内存:{GetUnit(proc.PeakWorkingSet64)}");
                Logger.Info($"========================> 应用当前内存:{GetUnit(proc.WorkingSet64)}");
                Logger.Info($"======================> 专用工作集内存:{GetUnit(proc.PrivateMemorySize64)}");
            }
            catch (Exception ex)
            {
                Logger.Error($"进程信息获取异常:{ex.Message}");
            }
            Logger.Info($"========================> 监测日志耗时:{stopwatch.ElapsedMilliseconds}ms");
        }

        /// <summary>一次性回收三代内存</summary>
        private void Collect()
        {
            GC.Collect(0);
            GC.Collect(1);
            GC.Collect(2);
        }

        #endregion

2.怀疑是服务器性能不够导致排队处理不过来

排查方案:认为服务器不行,换服务器

当前服务器是 ARM 4核8GB,在迁移到这个服务器之前使用的是 x86 服务器也是 4核8GB,并且当时在迁移过来后有明显的“反应慢”的体验。所以怀疑服务器不行,其实更怀疑是代码问题,但是换服务器比来得快,同时当前对于代码其实是没有头绪的。所以选择升级服务器至 x86 8核16GB 这种更快的方式。

结论:升级后整个站点的访问性能有明显提升,但是内存占用高的问题并没有解决,并且占用彷佛就是物理内存的上限(当前没有再继续升级内存不能验证)。从 ARM 升级至 x86 后整个站点有明显“快” 的感觉。

3.怀疑静态 ConcurrentDictionary 一直在增长

排查方案:1.确认 ConcurrentDictionary 是否一直增长。2.验证 Remove 后内存是否释放。

        1.将所有代码中的 static ConcurrentDictionary 集中输出 .Count 来观察是不是一直在增加,这个动作搞了半天,把全是总数都输出了,发现只有部分静态变量出现双倍数据,没有一直增长。解决了个小问题,去掉了一半。期间还把部分 static 转移到 Redis 中去,给 API 服务腾空间(效果不怎么明显,因为内存还是在涨,可以说是没用效果)

        2.通过测试代码验证到底 Remove 后有没有释放内存。观看下面代码,左边主要是两个按钮的点击事件第一个是往 ConcurrentDictionary 中插入 10W 个 Guid。可以从右边的内存占用中看到三个峰值就是在这个动作下产生的。

        然后执行第二个方法,将 ConcurrentDictionary 中的数据 Remove 和 Clear。两种操作看起来并没有完全削峰,手动多次后会释放。期间还测试了联系多次继续 TryAdd 内存会一直涨,并且在 Remove 和 Clear 后并不会立刻释放。等 GC 大佬安排,等多久?什么时候?完全不清楚 ~~~

        在 67 ~ 70 行新增了手动回收 0、1、2 三代内存多次后会释放内存。

结论:static ConcurrentDictionary 变量没有一直增长。在 Remove 和 Clear 后不会释立刻放内存,除非手动回收。但是问题并没有解决,因为这两个场景的数据量都不是特别的多。

4. 升级至 .NET 7 版本,怀疑是 .NET 5 有问题(已经开始不要脸了)

排查过程:当前分支中有 .NET 6,.NET 7 说是对于性能和内存管理都有大幅的提升,所以乘此机会升级试试(万一呢),说实话,其实内心对于 .NET 出问题的概率还是比较低,不至于被我碰到,毕竟都是很普通的业务场景没有多少高科技的逻辑。并且 Visusl Studio 2022 的升级频率也相当高,但是现在对于解决问题的渴望已经达到了顶点,这种感觉就像是前面的 using ,明知道没用但是哪怕有一丝丝希望还是愿意尝试。

结论:从 .NET 5 升级到 .NET 7 后应用的启动性能有了明显提升,但是内存持续增长的问题并没用消失。最后继续不要脸(这两个版本都不是长期支持版本 ~~~ 等下一版 .NET 8 发布后继续升级)

5. 使用 Visual Studio 诊断工具分析内存占用

排查过程:在不断的搜索中,接触到 VS 诊断工具,不确定这个工具是从哪个版本开始的,但是指导2023年才知道可以分析内存占用。这个过程有几天,不是全情投入,因为“摸不到头脑”,又被难住了。通过工具得知 .NET 5 项目之所有在调用接口后内存暴增的原因是 EntityFrameworkCore 的缓存导致的,但是对于这个缓存机制的理解仅限于“缓存”这两个字,其他的不清楚 ~~~

        又尝试使用 builder.EnableServiceProviderCaching(false); 禁用 EFCore 的缓存,确实有效果整个站点立马变得比之前 ARM 平台还要慢几倍,吓得我立刻注释掉。毕竟现在是疑似内存泄漏,就这个动作解决了问题,但是这种体验是不能被接受的。

        在搜索 Visual Studio 诊断工具如何使用的过程中,又接触到 WinDbg。也搜索了一些 WinDbg 的使用方法,成功在服务器中导出了 dmp 文件(特别大 ~~~)但是这个的使用方法还没入门就进行不下去了,大约两天时间经历了一次“从入门到放弃”。

        这里顺带提一下,这个问题虽然几个月都没能解决但是在人肉运维的加持下还没有出大问题。例如 .NET 5 这个版本虽然内存增长明显,但是晚上没人用,所以我们就加了凌晨3点的重启自动任务,使得 API 每天充满活力,我们一边开发一边如痴如醉的排查问题,好几次都是把开发任务拖到 deadline 才从死胡同中抽身去完成。内心来说,因为这个问题不解决,自己面子上有点挂不住(几个月过去了,其实已经开始习惯了 ~~~)

结论:知道了 EntityFrameworkCore 默认启用缓存,看起来缓存的时间非常的长,感觉不重启彷佛一直都在(不知道缓存的内部实现规则),没有验证禁用缓存是否任然会有内存溢出的,因为不能接受没有缓存时的丝滑。

        虽然没有真正定位到为题,但是根据目前查询的信息来 Visual Studio 诊断工具和 WinDbg 是最有可能定位到问题的方式,但是精力不够没能继续探索下去。

6. 将实例引用改为接口引用(不知道有没有用,因为理解不够透彻)

排查方案:将项目中的实例应用改为接口引用。

这个项目已经迭代好多年了,从 2018 年开始一直由本人亲自开发维护,对于这些代码已经熟悉到忘记了。时不时的还会吐槽几年前的自己为什么这么不严谨,自己定的规范自己都不遵守。

结论:不清楚有没用用,但是感觉整个项目的代码正在蜕变并趋于“完美”

7. 排查 new 关键字

排查方案:鉴定 new 关键字的业务是否会存在风险

减少多余的 new,可以不赋初值的就不赋。而且在 new 数组的时候,尽可能的不要填长度,因为如果长度太大喀得就把内存给吃了,不熟悉的从表面上还看不出来,就比如现在这之前的我就没看出来。

        在搜索的过程中发现有很多人出现内存泄漏的原因是用到了 HttpClient,可惜目前项目中没有“直接”使用 HttpClient,而且使用开源项目 WebApiClientCore,老实说我怀疑过这个组件(我就没有脸,不要脸 ~~~ ),但是现在项目中应用比较多,还不能通过去掉的方式来验证。但是一边又想这个开源项目也是从 .NET Core 2.1 开始用的当时是 WebApiClient.JIT 目前 .NET Core 3.1 项目使用的最新版 1.1.4,WebApiClientCore 是在 .NET 5 项目中使用,并且也一直在升级 目前使用的版本是 2.0.2 最新版本是 2.0.4 准备有机会就把它点了。

        在排查期间整出个幺蛾子,一个线程里 while(true) 监测链接状态的业务,因为前面排查 static 变量时没注意把 static 去掉了,导致一直在 new Thread,在本地开发调试没发现问题,发布到测试环境因为用的人少,也没发现问题。直到上线的时候发现线程数一直在新增直到大约13,000 的时候 Docker 会自动重启。这个问题排查代码找了三天,全工时投入期间还不要脸的怀疑有第三方组件出问题又挨个升级了一遍。这个情况比内存泄漏的问题要急迫,因为只要用户数一起来一多个小时甚至不到一小时就会重启一次,这几天搞得心惊胆战心力憔悴生怕数据出问题。

结论:也许有效果,如前面的 “3.怀疑静态 ConcurrentDictionary 一直在增长” 相似,只要不是一直被引用的部分,最终 GC 都会给安排。这次到是减少了部分可以不许 new 赋初值的逻辑,感觉代码又变得更“完美”了,彷佛仅限于感觉。

8. 怀疑数据库资源没有释放

排查过程:当前使用的 PostgreSQL 数据,通过下面的语句发现连接数很少一共只有5个其中还要两个是 Navicat 也就是说 API 只有三个连接数,backend_start 与 query_start 的时间都是今天的。按理说这种情况就不存在资源没有释放了。

SELECT * FROM pg_stat_activity

结论:不确定是不是方法不对,虽然认为连接数没问题,但是始终认为 EntityFreamworkCore 存在没有释放的资源,肯定有什么设置可以解决(取消缓存除外)。


后续有进展再更新,目前问题仍然存在没有解决 2023年10月9日

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1073308.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NanoPC-T4 RK3399:移植U-Boot

一:启动流程 瑞芯微平台目前支持两种启动方式,本系列只针对完全开源方式做详细讲解: 1、完全开源方式:使用 U-Boot TPL/SPL,源码可来自主流U-boot开源代码或瑞芯微SDK(在主流源码上做针对性优化) 2、不开源方式:使用rockchip idbloader,由rockchip ddr init bin和min…

delphi调用edge的截图功能

一、设置edge中截图的快捷键。 二、代码&#xff1a; constVK_CONTROL $11;VK_4 $34;procedure SendCtrl4ToDesktopVirtualKeyboard; begin// 模拟Ctrl键按下keybd_event(VK_CONTROL, 0, 0, 0);// 模拟4键按下keybd_event(VK_4, 0, 0, 0);// 模拟4键释放keybd_event(VK_4, 0…

JVM监控及诊断工具-GUI篇

文章目录 JVM监控及诊断工具-GUI篇工具概述JConsoleVisual VM再谈内存泄漏Java中内存泄漏的8种情况Arthas&#xff08;阿尔萨斯&#xff09; JVM监控及诊断工具-GUI篇 工具概述 使用上一章命令行工具或组合能获取目标Java应用性能相关的基础信息&#xff0c;但它们存在下列局…

JavaScript入门——基础知识(4)

一、for语句 1.1 for语句的基本使用 1.1.1 for循环语法 作用&#xff1a;重复执行代码 好处&#xff1a;把声明起始值、循环条件、变化值写到一起&#xff0c;让人一目了然&#xff0c;它是最常使用的循环形式 for(变量起始值;终止条件;变量变化量){// 循环体 } <script&g…

gici-open示例数据运行(ground_truth坐标的转换)

1. 坐标系转换说明 涉及的两个坐标转换&#xff1a; nmea_pose_to_pose &#xff1a;激光IMU中心到数据集IMU中心&#xff0c;主要是杆臂误差&#xff0c;转换关系为&#xff1a; //坐标转换的主要步骤(若发现有错误的地方&#xff0c;请评论指出) //定义激光IMU和数据集IMU之…

springboot项目做成公共项目

一&#xff1a;引言 最近碰到个需求&#xff0c;就是把我项目做成一个公共的提供jar包给别人使用&#xff0c;我也是捣鼓了一段时间去研究这个问题&#xff0c;这个东西其实就是A 项目提供jar包给B项目&#xff0c;B项目只要引入A项目的jar包就可以使用A项目的功能。 问题一&…

Jumpserver安全一窥:Sep系列漏洞深度解析

Jumpserver是中国国内公司开发的一个开源项目&#xff0c;在开源堡垒机领域一家独大。在2023年9月官方集中修复了一系列安全问题&#xff0c;其中涉及到如下安全漏洞&#xff1a; JumpServer 重置密码验证码可被计算推演的漏洞&#xff0c;CVE编号为CVE-2023-42820JumpServer 重…

untitle

实用的科研图形美化处理教程分享 ​ 显微照片排版标记 除了统计图表之外&#xff0c;显微照片也是文章中必不可少的实验结果呈现方式。除了常规实验的各种组织切片照片&#xff0c;在空间转录组文章中显微照片更是常见。显微照片的呈现方式也是有讲究的&#xff0c;比如对照片…

硬核!一个基于SpringBoot+Vue前后端分离低代码项目

一、项目介绍 这是一款基于SpringBootVue的前后端分离的项目&#xff0c;麻雀虽小&#xff0c;五脏俱全&#xff0c;开箱即用&#xff01; JNPF开发平台的前端采用Vue.js&#xff0c;这是一种流行的前端JavaScript框架&#xff0c;用于构建用户界面。Vue.js具有轻量级、可扩展性…

网络电视机顶盒怎么样?数码粉私藏网络机顶盒排行榜

对于新手来说&#xff0c;选购网络机顶盒十分困难&#xff0c;不少低价网络机顶盒虚标配置&#xff0c;偷工减料&#xff0c;售后也没有保障&#xff0c;不知道网络机顶盒什么牌子好很容易踩雷&#xff0c;近来某知名数码媒体发布了最新的网络机顶盒排名&#xff0c;入围的是哪…

swift界面初体验

1.添加视图 lazy var contentView: UIView {let a UIView()self.view.addSubview(a)return a}()2.添加文本 lazy var tipsLabel: UILabel {let a UILabel()a.font UIFont.regular13a.textColor UIColor.withHex(hexString:"#58C65C")a.text R.string.locali…

延时中间继电器 JZS-7/125 DC220V 0.02-9.99S 带一延时一瞬动辅助接点

JZS-7/125可调延时中间继电器系列型号&#xff1a; JZS-7/125静态可调延时中间继电器&#xff1b; JZS-7/145静态可调延时中间继电器&#xff1b; JZS-7/127静态可调延时中间继电器&#xff1b; JZS-7147静态可调延时中间继电器&#xff1b; 1 用途 JZS-7系列静态可调延时中间…

QT5 WebCapture 页面定时截图工具

QT5 WebCapture 网页定时截图工具 1.设置启动时间&#xff0c;程序会到启动时间后开始对网页依次进行截图 2.根据所需截图的页面加载速度&#xff0c;设置页面等待时间&#xff0c;尽量达到等页面加载完成后&#xff0c;再执行截图 3.根据需求&#xff0c;设置截图周期 4.程序…

基于多线程的Reactor模式的 回声服务器 EchoServer

记录下 一个线程专门用来接受accept获取客户端的fd 获取fd之后 从剩余的执行线程中 找到一个连接客户端数量最少的线程 然后将客户端的fd加入到这个线程中并通过EPOLL监听这个fd 线程之间通过eventfd来通信 将客户端的fd传到 对应的线程中 参考了MediaServer 引入…

[java基础学习]之DOS命令

#java基础学习 1.常用的DOS命令&#xff1a; dir:列出当前目录下的文件以及文件夹 md: 创建目录 rd:删除目录cd:进入指定目录 cd.. :退回到上级目录 cd\ : 退回到根目录 del:删除文件 exit:退出dos命令行 1.dir:列出当前目录下的文件以及文件夹 2.md: 创建目录 …

解决Adobe Premiere Pro CC 2018打开无反应,并出现.crash的文件问题

一 问题描述 Adobe Premiere Pro CC 2018软件安装完成后&#xff0c;打开该软件没反应&#xff0c;且打开时桌面会出现Crash文件&#xff01; 二 解决方法 如果Adobe Premiere Pro CC 2018在打开时无反应&#xff0c;并出现.crash文件的问题&#xff0c;可以尝试以下解决方法…

KekeBlog项目实战(更新中)

一、前言 1. 项目简介 本项目是前后端分离项目&#xff0c;而我们所做的只有完整的后端开发工作&#xff0c;前端已经写好&#xff0c;故不做任何开发&#xff0c;仅开发后端。项目包含完整的后端中前台和后台的代码编写 前端项目下载链接&#xff1a; https://pan.baidu.c…

Git仓库迁移记录

背景&#xff1a;gitlab私服上面&#xff0c;使用 import project的方式&#xff0c;从旧项目迁移到新地址仓库&#xff0c;但是代码一直没拉过去。所以使用命令的方式&#xff0c;进行代码迁移。 第一步&#xff1a;使用git clone --mirror git地址&#xff0c;进行代码克隆 …

建立数据科学基础设施的绝佳指南 数据工程师都该人手一册

《Effective数据科学基础设施》由Netflix工程师Ville Tuulos撰写&#xff0c;以Metaflow为对象&#xff0c;介绍了数据科学所需要的基础设施&#xff0c;囊括数据准备、特征工程、模型训练、模型部署、服务和持续监控等环节。Metaflow专注于构建生产流程&#xff0c;更适合具有…

《理解深度学习》2023最新版本+习题答案册pdf

刚入门深度学习或者觉得学起来很困难的同学看过来了&#xff0c;今天分享的这本深度学习教科书绝对适合你。 就是这本已在外网获13.1万次下载的宝藏教科书《理解深度学习》。本书由巴斯大学计算机科学教授Simon J.D. Prince撰写&#xff0c;全书共541页&#xff0c;目前共有21…