Node.js 根本没有这样搞性能优化的?

1、使用最新版本的 Node.js

仅仅是简单的升级 Node.js 版本就可以轻松地获得性能提升,因为几乎任何新版本的 Node.js 都会比老版本性能更好,为什么?

Node.js 每个版本的性能提升主要来自于两个方面:

  • V8 的版本更新;
  • Node.js 内部代码的更新优化。

例如最新的 V8 7.1 中,就优化了某些情形下闭包的逃逸分析,让 Array 的一些方法得到了性能提升:

Node.js 的内部代码,随着版本的升级,也会有明显的优化,比如下面这个图就是 require 的性能随着 Node.js 版本升级的变化:

每个提交到 Node.js 的 PR 都会在 review 的时候考虑会不会对当前性能造成衰退。同时也有专门的 benchmarking 团队来监控性能变化,你可以在这里看到 Node.js 的每个版本的性能变化:

https://benchmarking.nodejs.org/

所以,你可以完全对新版本 Node.js 的性能放心,如果发现了任何在新版本下的性能衰退,欢迎提交一个 issue。

如何选择 Node.js 的版本?

这里就要科普一下 Node.js 的版本策略:

  • Node.js 的版本主要分为 Current 和 LTS;
  • Current 就是当前最新的、依然处于开发中的 Node.js 版本;
  • LTS 就是稳定的、会长期维护的版本;
  • Node.js 每六个月(每年的四月和十月)会发布一次大版本升级,大版本会带来一些不兼容的升级;
  • 每年四月发布的版本(版本号为偶数,如 v10)是 LTS 版本,即长期支持的版本,社区会从发布当年的十月开始,继续维护 18 + 12 个月(Active LTS + Maintaince LTS);
  • 每年十月发布的版本(版本号为奇数,例如现在的 v11)只有 8 个月的维护期。

举个例子,现在(2018年11月),Node.js Current 的版本是 v11,LTS 版本是 v10 和 v8。更老的 v6 处于 Maintenace LTS,从明年四月起就不再维护了。去年十月发布的 v9 版本在今年六月结束了维护。

对于生产环境而言,Node.js 官方推荐使用最新的 LTS 版本,现在是 v10.13.0。

2、使用 fast-json-stringify 加速 JSON 序列化

在 JavaScript 中,生成 JSON 字符串是非常方便的:

<pre>const json = JSON.stringify(obj)</pre>

但很少人会想到这里竟然也存在性能优化的空间,那就是使用 JSON Schema 来加速序列化。

在 JSON 序列化时,我们需要识别大量的字段类型,比如对于 string 类型,我们就需要在两边加上 " ,对于数组类型,我们需要遍历数组,把每个对象序列化后,用 , 隔开,然后在两边加上 [ 和 ] ,诸如此类等等。

但 如果已经提前通过 Schema 知道每个字段的类型,那么就不需要遍历、识别字段类型 ,而可以直接用序列化对应的字段,这就大大减少了计算开销,这就是 fast-json-stringfy 的原理。

根据项目中的跑分,在某些情况下甚至可以比 JSON.stringify 快接近 10 倍!

一个简单的示例:

<pre>const fastJson = require('fast-json-stringify')
const stringify = fastJson({title: 'Example Schema',type: 'object',properties: {name: { type: 'string' },age: { type: 'integer' },books: {type: 'array',items: {type: 'string',uniqueItems: true}}}
})console.log(stringify({name: 'Starkwang',age: 23,books: ['C++ Primier', '響け!ユーフォニアム~']
}))
//=> {"name":"Starkwang","age":23,"books":["C++ Primier","響け!ユーフォニアム~"]}</pre>

在 Node.js 的中间件业务中,通常会有很多数据使用 JSON 进行,并且这些 JSON 的结构是非常相似的(如果你使用了 TypeScript,更是这样),这种场景就非常适合使用 JSON Schema 来优化。

3、提升 Promise 的性能

Promise 是解决回调嵌套地狱的灵丹妙药,特别是当自从 async/await 全面普及之后,它们的组合无疑成为了 JavaScript 异步编程的终极解决方案,现在大量的项目都已经开始使用这种模式。

但是优雅的语法后面也隐藏着性能损耗,我们可以使用 github 上一个已有的跑分项目 进行测试,以下是测试结果:

<pre>file                               time(ms)  memory(MB)
callbacks-baseline.js                   380       70.83
promises-bluebird.js                    554       97.23
promises-bluebird-generator.js          585       97.05
async-bluebird.js                       593      105.43
promises-es2015-util.promisify.js      1203      219.04
promises-es2015-native.js              1257      227.03
async-es2017-native.js                 1312      231.08
async-es2017-util.promisify.js         1550      228.74Platform info:
Darwin 18.0.0 x64
Node.JS 11.1.0
V8 7.0.276.32-node.7
Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz × 4</pre>

我们可以从结果中看到,原生 async/await + Promise 的性能比 callback 要差很多,并且内存占用也高得多。对于大量异步逻辑的中间件项目而言,这里的性能开销还是不能忽视的。

通过对比可以发现,性能损耗主要来自于 Promise 对象自身的实现,V8 原生实现的 Promise 比 bluebird 这样第三方实现的 Promise 库要慢很多。而 async/await 语法并不会带来太多的性能损失。

所以对于大量异步逻辑、轻量计算的中间件项目而言,可以在代码中把全局的 Promise 换为 bluebird 的实现:

<pre>global.Promise = require('bluebird');</pre>

4、正确地编写异步代码

使用 async/await 之后,项目的异步代码会非常好看:

<pre>const foo = await doSomethingAsync();
const bar = await doSomethingElseAsync();</pre>

但因此,有时我们也会忘记使用 Promise 给我们带来的其它能力,比如 Promise.all() 的并行能力:

<pre>// bad
async function getUserInfo(id) {const profile = await getUserProfile(id);const repo = await getUserRepo(id)return { profile, repo }
}// good
async function getUserInfo(id) {const [profile, repo] = await Promise.all([getUserProfile(id),getUserRepo(id)])return { profile, repo }
}</pre>

还有比如 Promise.any() (此方法不在ES6 Promise标准中,也可以使用标准的 Promise.race() 代替),我们可以用它轻松实现更加可靠快速的调用:

<pre>async function getServiceIP(name) {// 从 DNS 和 ZooKeeper 获取服务 IP,哪个先成功返回用哪个// 与 Promise.race 不同的是,这里只有当两个调用都 reject 时,才会抛出错误return await Promise.any([getIPFromDNS(name),getIPFromZooKeeper(name)])
}</pre>

5、优化 V8 GC

关于 V8 的垃圾回收机制,已经有很多类似的文章了,这里就不再重复介绍。推荐两篇文章:

  • 解读 V8 GC Log(一): Node.js 应用背景与 GC 基础知识
  • 解读 V8 GC Log(二): 堆内外内存的划分与 GC 算法

我们在日常开发代码的时候,比较容易踩到下面几个坑:

坑一:使用大对象作为缓存,导致老生代(Old Space)的垃圾回收变慢

示例:

<pre>const cache = {}
async function getUserInfo(id) {if (!cache[id]) {cache[id] = await getUserInfoFromDatabase(id)}return cache[id]
}</pre>

这里我们使用了一个变量 cache 作为缓存,加速用户信息的查询,进行了很多次查询后, cache 对象会进入老生代,并且会变得无比庞大,而老生代是使用三色标记 + DFS 的方式进行 GC 的,一个大对象会直接导致 GC 花费的时间增长(而且也有内存泄漏的风险)。

解决方法就是:

  • 使用 Redis 这样的外部缓存,实际上像 Redis 这样的内存型数据库非常适合这种场景;
  • 限制本地缓存对象的大小,比如使用 FIFO、TTL 之类的机制来清理对象中的缓存。

坑二:新生代空间不足,导致频繁 GC

这个坑会比较隐蔽。

Node.js 默认给新生代分配的内存是 64MB(64位的机器,后同),但因为新生代 GC 使用的是 Scavenge 算法,所以实际能使用的内存只有一半,即 32MB。

当业务代码频繁地产生大量的小对象时,这个空间很容易就会被占满,从而触发 GC。虽然新生代的 GC 比老生代要快得多,但频繁的 GC 依然会很大地影响性能。极端的情况下,GC 甚至可以占用全部计算时间的 30% 左右。

解决方法就是,在启动 Node.js 时,修改新生代的内存上限,减少 GC 的次数:

<pre>node --max-semi-space-size=128 app.js</pre>

当然有人肯定会问,新生代的内存是不是越大越好呢?

随着内存的增大,GC 的次数减少,但每次 GC 所需要的时间也会增加,所以并不是越大越好,具体数值需要对业务进行压测 profile 才能确定分配多少新生代内存最好。

但一般根据经验而言, 分配 64MB 或者 128MB 是比较合理的 。

6、正确地使用 Stream

Stream 是 Node.js 最基本的概念之一,Node.js 内部的大部分与 IO 相关的模块,比如 http、net、fs、repl,都是建立在各种 Stream 之上的。

下面这个经典的例子应该大部分人都知道,对于大文件,我们不需要把它完全读入内存,而是使用 Stream 流式地把它发送出去:

<pre>const http = require('http');
const fs = require('fs');// bad
http.createServer(function (req, res) {fs.readFile(__dirname + '/data.txt', function (err, data) {res.end(data);});
});// good
http.createServer(function (req, res) {const stream = fs.createReadStream(__dirname + '/data.txt');stream.pipe(res);
});</pre>

在业务代码中合理地使用 Stream 能很大程度地提升性能,当然是但实际的业务中我们很可能会忽略这一点,比如采用 React 服务器端渲染的项目,我们就可以用 renderToNodeStream :

<pre>const ReactDOMServer require('react-dom/server')
const http = require('http')
const fs = require('fs')
const app = require('./app')// bad
const server = http.createServer((req, res) => {const body = ReactDOMServer.renderToString(app)res.end(body)
});// good
const server = http.createServer(function (req, res) {const stream = ReactDOMServer.renderToNodeStream(app)stream.pipe(res)
})server.listen(8000)</pre>

使用 pipeline 管理 stream

在过去的 Node.js 中,处理 stream 是非常麻烦的,举个例子:

<pre>source.pipe(a).pipe(b).pipe(c).pipe(dest)</pre>

一旦其中 source、a、b、c、dest 中,有任何一个 stream 出错或者关闭,会导致整个管道停止,此时我们需要手工销毁所有的 stream,在代码层面这是非常麻烦的。

所以社区出现了 pump 这样的库来自动控制 stream 的销毁。而 Node.js v10.0 加入了一个新的特性: stream.pipeline ,可以替代 pump 帮助我们更好的管理 stream。

一个官方的例子:

<pre>const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');pipeline(fs.createReadStream('archive.tar'),zlib.createGzip(),fs.createWriteStream('archive.tar.gz'),(err) => {if (err) {console.error('Pipeline failed', err);} else {console.log('Pipeline succeeded');}}
);</pre>

实现自己的高性能 Stream

在业务中你可能也会自己实现一个 Stream,可读、可写、或者双向流,可以参考文档:

*   implementing Readable streams
*   implementing Writable streams

Stream 虽然很神奇,但自己实现 Stream 也可能会存在隐藏的性能问题,比如:

<pre>class MyReadable extends Readable {_read(size) {while (null !== (chunk = getNextChunk())) {this.push(chunk);}}
}</pre>

当我们调用 new MyReadable().pipe(xxx) 时,会把 getNextChunk() 所得到的 chunk 都 push 出去,直到读取结束。但如果此时管道的下一步处理速度较慢,就会导致数据堆积在内存中,导致内存占用变大,GC 速度降低。

而正确的做法应该是,根据 this.push() 返回值选择正确的行为,当返回值为 false 时,说明此时堆积的 chunk 已经满了,应该停止读入。

<pre>class MyReadable extends Readable {_read(size) {while (null !== (chunk = getNextChunk())) {if (!this.push(chunk)) {return false  }}}
}</pre>

这个问题在 Node.js 官方的一篇文章中有详细的介绍: Backpressuring in Streams

7、C++ 扩展一定比 JavaScript 快吗?

Node.js 非常适合 IO 密集型的应用,而对于计算密集的业务,很多人都会想到用编写 C++ Addon 的方式来优化性能。但实际上 C++ 扩展并不是灵丹妙药,V8 的性能也没有想象的那么差。

比如,我在今年九月份的时候把 Node.js 的 net.isIPv6() 从 C++ 迁移到了 JS 的实现,让大多数的测试用例都获得了 10%- 250% 不等的性能提升( 具体PR可以看这里 )。

JavaScript 在 V8 上跑得比 C++ 扩展还快,这种情况多半发生在与字符串、正则表达式相关的场景,因为 V8 内部使用的正则表达式引擎是 irregexp ,这个正则表达式引擎比 boost 中自带的引擎( boost::regex )要快得多。

还有一处值得注意的就是,Node.js 的 C++ 扩展在进行类型转换的时候,可能会消耗非常多的性能,如果不注意 C++ 代码的细节,性能会很大地下降。

这里有一篇文章对比了相同算法下 C++ 和 JS 的性能(需翻墙): How to get a performance boost using Node.js native addons 。其中值得注意的结论就是,C++ 代码在对参数中的字符串进行转换后( String::Utf8Value 转为 std::string ),性能甚至不如 JS 实现的一半。只有在使用 NAN 提供的类型封装后,才获得了比 JS 更高的性能。

换句话说,C++ 是否比 JavaScript 更加高效需要具体问题具体分析,某些情况下,C++ 扩展不一定就会比原生 JavaScript 更高效。如果你对自己的 C++ 水平不是那么有信心,其实还是建议用 JavaScript 来实现,因为 V8 的性能比你想象的要好得多。

8、使用 node-clinic 快速定位性能问题

说了这么多,有没有什么可以开箱即用,五分钟见效的呢?当然有。

node-clinic 是 NearForm 开源的一款 Node.js 性能诊断工具,可以非常快速地定位性能问题。

<pre>npm i -g clinic
npm i -g autocannon</pre>

使用的时候,先开启服务进程:

<pre>clinic doctor -- node server.js</pre>

然后我们可以用任何压测工具跑一次压测,比如使用同一个作者的 autocannon (当然你也可以使用 ab、curl 这样的工具来进行压测。):

<pre>autocannon http://localhost:3000</pre>

压测完毕后,我们 ctrl + c 关闭 clinic 开启的进程,就会自动生成报告。比如下面就是我们一个中间件服务的性能报告:

我们可以从 CPU 的使用曲线看出,这个中间件服务的性能瓶颈不在自身内部的计算,而在于 I/O 速度太慢。clinic 也在上面告诉我们检测到了潜在的 I/O 问题。

下面我们使用 clinic bubbleprof 来检测 I/O 问题:

<pre>clinic bubbleprof -- node server.js</pre>

再次进行压测后,我们得到了新的报告:

这个报告中,我们可以看到, http.Server 在整个程序运行期间,96% 的时间都处于 pending 状态,点开后,我们会发现调用栈中存在大量的 empty frame,也就是说,由于网络 I/O 的限制,CPU 存在大量的空转,这在中间件业务中非常常见,也为我们指明了优化方向不在服务内部,而在服务器的网关和依赖的服务相应速度上。

想知道如何读懂 clinic bubbleprof 生成的报告,可以看这里: https://clinicjs.org/bubblepr…

同样,clinic 也可以检测到服务内部的计算性能问题,下面我们做一些“破坏”,让这个服务的性能瓶颈出现在 CPU 计算上。

我们在某个中间件中加入了空转一亿次这样非常消耗 CPU 的“破坏性”代码:

<pre>function sleep() {let n = 0while (n++ < 10e7) {empty()}
}
function empty() { }module.exports = (ctx, next) => {sleep()// ......return next()
}</pre>

然后使用 clinic doctor ,重复上面的步骤,生成性能报告:

这就是一个非常典型的 同步计算阻塞了异步队列 的“病例”,即主线程上进行了大量的计算,导致 JavaScript 的异步回调没法及时触发,Event Loop 的延迟极高。

对于这样的应用,我们可以继续使用 clinic flame 来确定到底是哪里出现了密集计算:

<pre>clinic flame -- node app.js</pre>

压测后,我们得到了火焰图(这里把空转次数减少到了100万次,让火焰图看起来不至于那么极端):

从这张图里,我们可以明显看到顶部的那个大白条,它代表了 sleep 函数空转所消耗的 CPU 时间。根据这样的火焰图,我们可以非常轻松地看出 CPU 资源的消耗情况,从而定位代码中哪里有密集的计算,找到性能瓶颈。

为了学习工作与休闲娱乐互不冲突,现新建圈【码农茶水铺】用于程序员生活,爱好,交友,求职招聘,吐槽等话题交流,希望各位大神工作之余到茶水铺来喝茶聊天。群号:582735936

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

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

相关文章

可交付成果、核实的可交付成果、验收的可交付成果?

①可交付成果。指的是在某一过程、阶段或项目完成时&#xff0c;产出的任何独特并可核实的产品、成果或服务。可交付成果可能是有形的&#xff0c;也可能是无形的。【研发完成】 ②核实的可交付成果。是指已经完成&#xff0c;并经过“控制质量”过程检查为正确的可交付成果。…

安装oracle到create inventory时卡住了怎么办_win10系统安装教程(官方工具)

Hi&#xff0c;大家好。对于小白用户&#xff0c;装系统是比较头疼的事&#xff0c;所以今天写一个简单易懂的装系统教程。使用微软官方提供的工具制作U盘启动盘&#xff0c;操作简单&#xff0c;系统纯净&#xff0c;强烈建议小白用户使用。缺点是该工具功能单一&#xff0c;并…

Linux的启动流程简析(以Debian为例)

Linux的启动流程简析(以Debian为例) 正文&#xff1a;前面的文章探讨BIOS和主引导记录的作用。那篇文章不涉及操作系统&#xff0c;只与主板的板载程序有关。今天&#xff0c;我想接着往下写&#xff0c;探讨操作系统接管硬件以后发生的事情&#xff0c;也就是操作系统的启动流…

一阶电路中的时间常数_精确移相电路的设计举例

移相电路就是对输入信号(一般是正弦波)进行相位控制&#xff0c;而不改变其幅度&#xff0c;本推文以移相电路为例&#xff0c;展示模拟电路的反馈设计技巧与方法&#xff1a;一、全通滤波器实现移相以上是两种移相电路 的原理&#xff0c;其输出幅度保持不变&#xff0c;移动的…

工作绩效数据、工作绩效信息、工作绩效报告

目录 概念 举例 概念 工作绩效数据&#xff1a;观察&#xff0c;测量&#xff0c;采集到的原始数据 工作绩效信息&#xff1a;对“工作绩效数据”进行加工分析 工作绩效报告&#xff1a;对“工作绩效信息”进行白话 举例 以去健身房为例 1&#xff09;工作绩效数据&…

tickcount()修改成小时分钟_银行核心系统24小时机制实现总结

本文共2268字 | 建议阅读时间&#xff1a;5分钟作者&#xff1a;张广在核心系统的设计实现中&#xff0c;24小时机制向来是一个重点难点。早期的银行只有柜面一个业务办理渠道&#xff0c;因此当时的综合业务系统&#xff0c;跟随网点的营业时间&#xff0c;分为日起&#xff0…

WBS结果输出表

目录​​​​​​​ 概述 模板 案例 扩展阅读 概述 花了很大力气分解的WBS&#xff0c;要趁热打铁&#xff0c;把每个活动落实到人。 模板 推荐一个模板 1&#xff1a;修改成项目名 2&#xff1a;填写项目基本信息 3&#xff1a;WBS的最底层工作包 4&#xff1a;工作…

svn管理工具_主流代码管理工具深度评测

引言 作为有十几年IT行业代码的从业人员&#xff0c;经历过代码管理工具的变迁&#xff0c;从早期的微软的Source Code Control&#xff0c;到TFS&#xff0c;再到SVN&#xff0c;再到现在的Git。我深知代码管理工具是代码开发过程中非常重要的工具。市场上的代码管理工具有很多…

我的2018

写在开始 2018年以飞快的速度临近尾声了&#xff0c;只感慨时间过得真快&#xff01; 这一年过得算是平平淡淡&#xff0c;没有比较特别的地方。 工作 从去年8月来到这公司&#xff0c;是个做旅游产品的互联网公司&#xff0c;平时里做的事可以说是很简单&#xff0c;我只能说&…

程序员笔试面试后上机_hcie面试有哪些要注意的事项?

大家都知道&#xff0c;华为认证hcie考试分为三个部分&#xff0c;分别是笔试、lab实验和面试。其中&#xff0c;考生讨论得最多的就是面试部分&#xff0c;因为面试不同于笔试和lab实验&#xff0c;自己埋头答题和操作就行&#xff0c;面试要面对考官&#xff0c;考核的东西非…

【Infragistics教程】在javascript构造函数中创建基本继承

2019独角兽企业重金招聘Python工程师标准>>> 【下载Infragistics Ultimate最新版本】 用javascript创建对象有四种方法。具体如下&#xff1a; 对象作为文本构造函数调用模式创建&#xff08;&#xff09;方法在ES6之后使用类继承的实现因对象创建方法而异。本文将解…

switch芯片和phy芯片的区别_感应式芯片卡CPU卡的FM1208-9和FM1208-10有什么区别,你知道吗?...

感应式CPU卡是目前芯片卡中安全系统较高的芯片&#xff0c;使用范围也较为广泛&#xff0c;但是这款CPU分为FM1208-9和FM1208-10&#xff0c;那你们知道分别代表什么意思呢&#xff1f;他们之间有什么不同呢&#xff1f;CPU白卡FM是什么&#xff1f;首先&#xff0c;我们来说下…

每次登陆都要滑动验证_湖人队冠军成员卡鲁索很吃香:每次谈判都有N支球队点名要他...

10月24日NBA直播台讯&#xff1a;洛杉矶湖人队助理教练迈克-彭伯西在接受媒体采访时透露&#xff0c;湖人队替补控球后卫卡鲁索目前在联盟中很吃香。湖人队每次进行交易谈判时&#xff0c;对方球队都点名想要卡鲁索。彭伯西表示&#xff1a;“每一次我们在休赛期或者交易截止日…

MySQL在DOS指令里面的使用以及增删改查的使用

本人的第一条博客&#xff0c;选中我的电脑单机右键&#xff0c;点开管理&#xff0c;选中服务找到MySQL57.启动该服务。回退至桌面&#xff0c;按住winR 输入cmd打开DOS指令的窗口。 在窗口输入: mysql -h localhost -u root -p 显示password输入提示&#xff1a;表示已经…

安卓应用用户数据_用户指标数据应用

一、如何理解数据用户数据&#xff1a;gender:性别、 birthday:出生日期行为数据&#xff1a;user_id:用户id、auction_id:购买行为编号、buy_mount:购买数量、day:购买时间商品数据&#xff1a;cat_id:商品种类ID、cat1:商品类别、property:商品属性二、用户数据指标1.用户数据…

webgis从基础到开发实践_开源WebGIS教程系列——11.1 GISLite 的开发背景与设计

地理信息门户可以帮助人们更容易地发现、访问和使用地理空间信息&#xff0c; 是地理信息发布、服务和共享的重要环节。许多国家都很重视地理信息门户的 建设&#xff0c;把它作为国家空间数据基础设施(spatial data infrastructure&#xff0c;SDI)的重要组成部分。GISLite 是…

Oracle数据库及在DOS命令下面的简单操作

在Oracle数据库注释用--表明为注释&#xff0c;但以下用//或--代表解释;数据库不怎么区分大小写&#xff1b; 先说说一些简单Oracle数据库操作的语句&#xff1a; 使用语句创建普通用户&#xff1a; Create user username identified by password; //创建普通用户 Grant reso…

CSS属性(display)

1.display属性 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><title>08display属性</title><style>.c1 {background-color: red;/*display: none; !* 让其在页面上不显示 *!*//*display: i…

产品发布系统_【产品发布】第3期|阀门遥控系统

更多精彩&#xff0c;请点击上方蓝字关注我们&#xff01;常熟瑞特电气股份有限公司的阀门遥控系统是一款经典的产品线&#xff0c;包括了全系列的液压执行器&#xff0c;电液执行器&#xff0c;微型动力单元&#xff0c;液压动力泵站&#xff0c;液压电磁阀箱等产品。阀门遥控…

大数据就业前景,分析的太到位了

大数据广泛应用于电网运行、经营管理及优质服务等各大领域&#xff0c;并正在改变着各行各业&#xff0c;也引领了大数据人才的变革。大数据就业前景怎么样&#xff1f;这对于在就业迷途中的我们是一个很重要的信息。 随着大数据时代的到来【这次国家教育部也改革动真格了】&am…