从0开始学人工智能测试节选:Spark -- 结构化数据领域中测试人员的万金油技术(四)

上一章节我们了解了 shuffle 相关的概念和原理后其实可以发现一个问题,那就是 shuffle 比较容易造成数据倾斜的情况。 例如上一节我们看到的图,在这批数据中,hello 这个单词的行占据了绝大部分,当我们执行 groupByKey 的时候触发了 shuffle。这时候大部分的数据 (Hello) 都汇集到了一个 partition 上。这种极端的情况就会造成著名的长尾现象,就是说由于大部分数据都汇集到了一个 partition 而造成了这个 partition 的 task 运行的十分慢。而其他的 task 早已完成,整个任务都在等这个大尾巴 task 的结束。 这种现象破坏了分布式计算的设计初衷,因为最终大部分的计算任务都在一个单点上执行了。所以极端的数据分布就成为了机器学习和大数据处理这类产品的劲敌,我跟我司的研发人员聊的时候,他们也觉得数据倾斜的情况比较难处理,当然我们可以做 repartition(重新分片) 来重新整合 parition 的数量和分布等操作,以及避免或者减少 shuffle 的成本,各家不同的业务有不同的做法。在做这类产品的性能测试的时候,也跟我们以往的互联网模式不同,产品的压力不在于并发量上,而在于数据量和数据分布上。

造数工具

一般我们需要模拟以下这些情况的数据:

  1. 数据拥有大量的分片
  2. 数据倾斜
  3. 宽表
  4. 空表
  5. 空行
  6. 空文件
  7. 中文行和中文列
  8. 超长列名
  9. 包含特殊字符的数据

下面是造数工具的架构图:

解释一下原理:

  1. 通过spark-submit把分布式计算任务提交到集群中, 可以是hadoop集群可以是spark自建的集群也可以是k8s集群, 其目的就是利用分布式计算的原理, 把造数任务分布在多个机器上进行处理。 最后汇总数据落盘到分布式存储设备中。 所以我们需要把造数逻辑编写到spark脚本中
  2. 在落盘的时候,面对不同的业务可以选择只输出到一个存储设备中, 但如果面对的是比较复杂的大数据或者机器学习系统, 那么有可能会需要把一份数据双写或者多写到不同的存储设备中, 而在spark中也是有这样的连接器帮助我们对接各种存储设备。
  3. 如果数据没有构造的特别庞大,也可以像上图那样把数据压缩导出到磁盘中,下一次换个环境使用的时候可以原封不动的再导入进去, 但是这样做的前提是数据不能非常庞大。

下面给一个使用pyspark构造数据的DEMO:

from pyspark import SparkContext, SparkConf, SQLContextimport randomfrom pyspark.sql.types import StructType, StructField, StringType, IntegerTypedef choice_one_random(sequence):return random.choice(sequence)def choice_one_with_weights(sequence, weight):return random.choices(sequence, weights=weight)[0]def randomInt(start, end):return random.randint(start, end)conf = SparkConf().setMaster("local").setAppName("My App")sc = SparkContext(conf=conf)sqlContext = SQLContext(sc)title = ['程序员', '教师', '测试人员', '产品经理']gender = ['男性', '女性']gender_weights = weights = [0.6, 0.4]nums = sc.parallelize(range(10000))rdd = nums.map(lambda x: (randomInt(1, 100),choice_one_random(title),choice_one_with_weights(gender, gender_weights)))schema = StructType([StructField("age", IntegerType(), True),StructField("title", StringType(), True),StructField("gender", StringType(), True),])data = sqlContext.createDataFrame(rdd, schema=schema)data.show()

原理:

  1. 通过 range 在内存中生成足够数量元素的 list
  2. 通过 rdd.map 函数遍历每一个元素, 然后按照规则生成自己需要的数据
  3. 转成 dataframe 并保存到存储设备中

可以参考上面的DEMO来完成自己的造数任务, 可以看到python的库中有很多好用的功能, 比如上面我通过random库不仅可以生成随机的数据, 也可以给存储一个列表,让数据生成的时候从这个列表中选一个并且给不同的值不同的权重来控制数据的分布, 这就可以造出数据倾斜的场景。

下面给一个用java实现的例子(实际的项目中我是用java的,下面就是一个项目中的构造程序):

package yuanhang;import generator.field.random.RandomDateField;import generator.field.random.RandomIntField;import generator.field.random.RandomScopeField;import generator.field.random.RandomStringField;import generator.table.XRange;import generator.utils.DateUtil;import org.apache.spark.SparkConf;import org.apache.spark.SparkContext;import org.apache.spark.api.java.JavaRDD;import org.apache.spark.api.java.JavaSparkContext;import org.apache.spark.sql.*;import org.apache.spark.sql.types.DataTypes;import org.apache.spark.sql.types.StructField;import org.apache.spark.sql.types.StructType;import java.sql.Date;import java.sql.Timestamp;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.time.ZoneId;import java.util.ArrayList;import java.util.List;import java.util.Properties;import org.apache.spark.util.LongAccumulator;import java.time.LocalDate;import java.util.Random;/***/public class event {public static void main(String[] args) {// SparkConf conf = new SparkConf().setAppName("data produce")// .setMaster("local");SparkConf conf = new SparkConf().setAppName("data produce");JavaSparkContext sc = new JavaSparkContext(conf);SparkSession spark = SparkSession.builder().appName("Java Spark SQL basic example").getOrCreate();// SparkContext sparkSC = spark.sparkContext();List<StructField> fields = new ArrayList<>();fields.add(DataTypes.createStructField("uin", DataTypes.StringType, true));fields.add(DataTypes.createStructField("app_key", DataTypes.StringType, true));fields.add(DataTypes.createStructField("event_time", DataTypes.DateType, true));fields.add(DataTypes.createStructField("event_code", DataTypes.StringType, true));fields.add(DataTypes.createStructField("ds", DataTypes.IntegerType, true));fields.add(DataTypes.createStructField("i001", DataTypes.IntegerType, true));fields.add(DataTypes.createStructField("i002", DataTypes.IntegerType, true));fields.add(DataTypes.createStructField("s001", DataTypes.StringType, true));fields.add(DataTypes.createStructField("s002", DataTypes.StringType, true));fields.add(DataTypes.createStructField("s003", DataTypes.StringType, true));fields.add(DataTypes.createStructField("s004", DataTypes.StringType, true));fields.add(DataTypes.createStructField("s005", DataTypes.StringType, true));fields.add(DataTypes.createStructField("s006", DataTypes.StringType, true));fields.add(DataTypes.createStructField("s007", DataTypes.StringType, true));fields.add(DataTypes.createStructField("s008", DataTypes.StringType, true));fields.add(DataTypes.createStructField("d001", DataTypes.DoubleType, true));StructType schema = DataTypes.createStructType(fields);final LongAccumulator accumulator = sc.sc().longAccumulator();// LocalDate startDate = LocalDate.of(2023, 5, 1);LocalDate startDate = LocalDate.of(2022, 10, 30);// LocalDate beginDate = LocalDate.of(2022, 5, 1);LocalDate endDate = LocalDate.of(2023, 10, 30);//default time zoneZoneId defaultZoneId = ZoneId.systemDefault();RandomScopeField event_codes = new RandomScopeField();event_codes.getValues().add("app_jhapp_search_res");event_codes.getValues().add("app_jhapp_tab_switch");event_codes.getValues().add("app_jhapp_applnch");event_codes.getValues().add("app_jhapp_search_ck");event_codes.getValues().add("app_jhapp_search_page_imp");event_codes.getValues().add("app_jhapp_search_page_ck");event_codes.getValues().add("app_jhapp_explore_subtab_imp");event_codes.getValues().add("app_jhapp_carousels_imp");event_codes.getValues().add("app_jhapp_carousels_intrct");event_codes.getValues().add("app_jhapp_first_open");event_codes.getValues().add("app_jhapp_content_detail_imp");event_codes.getValues().add("app_jhapp_content_detail_interact");event_codes.getValues().add("app_jhapp_tab_imp");while (!startDate.isAfter(endDate)) {// System.out.println(startDate);// List data1 = new XRange(1000);List data1 = new XRange(274000);JavaRDD distData = sc.parallelize(data1, 20);DateFormat dateformat = new SimpleDateFormat("yyyyMMddhh");Date date = new Date(java.util.Date.from(startDate.atStartOfDay(defaultZoneId).toInstant()).getTime());// Date bDate = new Date(java.util.Date.from(beginDate.atStartOfDay(defaultZoneId).toInstant()).getTime());// Date eDate = new Date(java.util.Date.from(endDate.atStartOfDay(defaultZoneId).toInstant()).getTime());int ds = Integer.parseInt(dateformat.format(date));JavaRDD rowRDD = distData.map( record ->{RandomIntField userId = new RandomIntField();userId.setMax(1000000);userId.setMin(1);// Date date = DateUtil.randomDate("2023-05-01", "2023-06-19");RandomIntField r = new RandomIntField();r.setMin(1);r.setMax(100);int i001 = Integer.parseInt(r.gen().toString());int i002 = Integer.parseInt(r.gen().toString());String s001 = "C"+ r.gen();String s002 = "当前一级板" + r.gen();String s003 = "去向一级板块" + r.gen();String s004 = "去向一级板块" + r.gen();String s005 = "去向一级板块" + r.gen();String s006 = "去向一级板块" + r.gen();String s007 = "2022090701";String s008 = "2023090701";double d001 = Double.parseDouble(r.gen().toString());return RowFactory.create(userId.gen().toString(), "0WEB05LD02D5FL6K",date,event_codes.gen(),ds,i001,i002,s001,s002,s003,s004,s005,s006,s007,s008,d001);});Dataset dataset =spark.createDataFrame(rowRDD, schema);dataset.show();// DataFrameWriter writer = new DataFrameWriter(dataset);String jdbcUrl = "jdbc:clickhouse://clickhouse-hs:8123/beacon_olap";// String jdbcUrl = "jdbc:clickhouse://10.27.20.122:8123/beacon_olap";Properties ckProperties = new Properties();// ckProperties.setProperty("user", "beidou");ckProperties.setProperty("user", "default");ckProperties.setProperty("password", "QdFx@00700!*");// ckProperties.setProperty("password", "Beidou@qidian");ckProperties.setProperty("driver", "ru.yandex.clickhouse.ClickHouseDriver");// ckProperties.setProperty("dbtable", "event_record_240");// ckProperties.setProperty("batchsize", "50000");// ckProperties.setProperty("isolationLevel", "NONE");// ckProperties.setProperty("numPartitions", "12");// ckProperties.setProperty("url", "jdbc:clickhouse://clickhouse-hs:8123/beacon_olap");dataset.write().option("batchsize", "50000").option("isolationLevel", "NONE").option("numPartitions", "10").option("truncate", "true").option("compression", "snappy").mode(SaveMode.Append).jdbc(jdbcUrl, "event_record_273", ckProperties);// dataset.write().mode(SaveMode.Append).jdbc(jdbcUrl, "event_record_240", ckProperties);startDate = startDate.plusDays(1);}}}

生成海量小文件

上一篇中也提到了海量的小文件是所有分布式存储设备的天敌, 我们之前在做一个车企的项目的时候, 也是要为客户搭建一个人工智能系统, 但是客户那边有20亿张图片的数据量需要做模型的训练推理(主要是计算机视觉方向,所以都是图片数据)。 所以专门引入了一个适合存储小文件的分布式存储设备(比如可以用ceph), 这时候就需要测试在这样庞大的文件数量下,不仅仅是存储系统,还有我们的产品本身是否能符合客户的性能需要。

海量小文件的构建与之前所讲的构造方式完全不同, spark可以造大量的数据,但这些数据都是在少数文件中的, 它无法构建海量的小文件, 这是因为在spark中每个parition(这里可以理解为一个小文件, 因为如果一个比较大的数据被切分成了很多很小的文件, 那么即便这个文件只有1k,在它读取到内存的时候也会当成一个partition处理)都会生成一个独立的task来计算, 一个task可以理解为一个线程。 所以当文件数量过多时,spark就会启动非常多的线程争抢cpu资源。所以不仅仅是分布式存储系统, 在分布式计算本身,过多的文件数量都是噩梦,试想一下,当一个文件只有100w条数据,但是每条数据都单独保存在一个文件中。 这时候spark就要开启100w个线程来处理这个数据,这是多么可怕的事情。

所以以前的构造方式是无法满足我们的需要的。 就要引入另外一种机制 -- 异步IO,这是一种利用少量线程就可以支撑大并发量的技术。 因为我们常见的普通的同步IO是无法满足我们的需要的,它有如下的缺点:

  1. IO:不管是生成图片,还是把与数据库交互都会消耗网络和磁盘 io, 20 亿张图片对于 IO 的考验是比较大的
  2. CPU:如果按照传统的思路,为了提升造数性能, 会开很多个线程来并发生成图片,计算元数据和数据交互。 但线程开的太多 CPU 的上下文切换也很损耗性能。 尤其我们使用的是 ssd 磁盘,多线程的模式可能是无法最大化利用磁盘 IO 的。

后面经过讨论, 最后的方案是使用 golang 语言, 用协程 + 异步 IO 来进行造数:

  1. 首先 golang 语言的 IO 库都是使用 netpoll 进行优化过的,netpoll 底层用的就是 epoll。 这种异步 IO 技术能保证用更少的线程处理更多的文件。 关于 epoll 为什么性能好可以参考这篇文章:https://www.cnblogs.com/Hijack-you/p/13057792.html 也可以去查一下同步 IO 和异步 IO 的区别。 我大概总结一下就是, 传统的多线程 + 同步 IO 模型是开多个线程抗压力, 每个线程同一时间只处理一个 IO。 一旦 IO 触发开始读写文件线程就会处于阻塞状态, 这个线程就会从 CPU 队列中移除也就是切换线程。 等 IO 操作结束了再把线程放到 CPU 队列里让线程继续执行下面的操作,而异步 IO 是如果一个线程遇到了 IO 操作,它不会进入阻塞状态, 而是继续处理其他的事, 等到那个 IO 操作结束了再通知程序(通过系统中断),调用回调函数处理后面的事情。 这样就保证了异步 IO 的机制下可以用更少的线程处理更多的 IO 操作。 这是为什么异步 IO 性能更好的原因,也是为什么异步 IO 能最大化利用磁盘性能。 比如在这个造图片的场景里, 我在内存中造好图片后,开始写入文件系统, 如果是同步 IO 那这时候就要阻塞了,直到文件写入完毕线程才会继续处理, 但因为我用的异步 IO, 调用玩函数让内存中的数据写入到文件就不管了, 直接开始造下一张图片, 什么时候 IO 结束通知我我在回过头来处理。 所以是可以用更少的线程来完成更多的 IO 操作也就是异步 IO 能很容易的把磁盘性能打满。 我自己测试的时候再自己的笔记本上造 100k 的图片, 大概是 1s 就能造 1W 张图片
  2. 其次 golang 的 GMP 模型本身就很高效, 编写异步程序也非常简单。 我也就花了一上午就把脚本写完了。
  3. 最后利用 k8s 集群把造数任务调度到集群中, 充分利用分布式计算的优势, 在多台机器上启动多个造数任务共同完成。

代码实现:

package mainimport ("crypto/md5""encoding/hex""fmt""io/ioutil""math/rand""net/http""os""path""sync""time")var (fileQueue = make(chan FileInfo, 1000) // 缓冲队列,用来存储文件key, 方便后面的协程取出来插入数据库sourceFiles = []string{"asfdf.png"} // 复制的源文件sourceFileCache sync.Map // 缓存源文件内容, 避免每次copy都重新读取源文件)const (destDir = "file" // 需要复制的目录路径copyNumber = 10 // 每个协程需要copy的文件数量)// 文件 信息type FileInfo struct {key stringcreatedAt time.Time}// 生成随机字符串func GetRandomString(n int) string {str := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"bytes := []byte(str)var result []bytefor i := 0; i < n; i++ {result = append(result, bytes[rand.Intn(len(bytes))])}return string(result)}func copyFile(src, dst string) ([]byte, int64, error) {var input []byteif data, ok := sourceFileCache.Load(src); ok {input = data.([]byte)} else {data, err := ioutil.ReadFile(src)if err != nil {return []byte{}, 0, err}input = datasourceFileCache.Store(src, input)}err := ioutil.WriteFile(dst, input, 0644)if err != nil {return []byte{}, 0, err}fi, err := os.Stat(dst)if err != nil {return []byte{}, 0, err}return input, fi.Size(), nil}func copyFiles(wg *sync.WaitGroup) {defer wg.Done()for i := 0; i < copyNumber; i++ {// 随机种子rand.Seed(time.Now().UnixNano())// 从源文件中选择一个文件进行copysourceFileCount := len(sourceFiles)sourceFilePath := sourceFiles[rand.Intn(sourceFileCount)]// 生成随机的文件名称fileName := GetRandomString(30) + ".jpg"destFilePath := path.Join(destDir, fileName)data, size, err := copyFile(sourceFilePath, destFilePath)if err != nil {fmt.Printf("copyFile file from %s to %s err, the message is %s", sourceFilePath, destFilePath, err.Error())}key, err := NewUploadFileKey("superadmin", fileName, md5V(string(data)), size)if err != nil {fmt.Printf("gen file key error, the message is %s", err.Error())}fileQueue <- FileInfo{key: key,createdAt: time.Now(),}}}func parseBasenameFromURI(uri string) (string, error) {r, _ := http.NewRequest("GET", uri, nil)return path.Base(r.URL.Path), nil}func NewUploadFileKey(uin, filename, hash string, size int64) (string, error) {str := fmt.Sprintf("uin:%s-hash:%s-size:%d", uin, hash, size)basename, err := parseBasenameFromURI(filename)if err != nil {return "", err}partHash := md5V(str)result := md5V(fmt.Sprintf("hash:%s-name:%s", partHash, filename))content := md5V(fmt.Sprintf("%s-%s-%s", result[8:10], result[10:], basename))u := fmt.Sprintf("%s%s/%s/%s-%s", "upload/", result[8:10], content[8:10], result[10:], basename)return u, nil}func md5V(str string) string {h := md5.New()h.Write([]byte(str))return hex.EncodeToString(h.Sum(nil))}func main() {var wg1 sync.WaitGroupwg1.Add(10)for i := 0; i < 10; i++ {go copyFiles(&wg1)}// 等待所有复制文件的协程结束go func() {wg1.Wait()// 关闭chan, 通知插入数据的协程, 文件都已经复制完毕close(fileQueue)fmt.Println("关闭通道")}()// 使用10个线程来插入数据库var wg2 sync.WaitGroupwg2.Add(10)for i := 0; i < 10; i++ {go insert(&wg2)}wg2.Wait()fmt.Println("数据生成完毕")}// 从chan中取出文件key插入数据库func insert(wg *sync.WaitGroup) {defer wg.Done()var cache []FileInfofor fileInfo := range fileQueue {if len(cache) < 1000 {cache = append(cache, fileInfo)//fmt.Println("数据没到1000条, 继续缓存")} else {// todo 将1000条数据插入到数据库中fmt.Println("积累了1k个文件, 开始插入数据库")}}

// todo for循环结束说明队列已经被关闭, 所有文件都copy完毕这时候需要缓存中剩余的记录一块插入到数据库中

fmt.Println("通道已经关闭, 现在开始把剩余的插入到数据库中")

//fmt.Println(cache)

for _, fileInfo := range cache{

fmt.Println(fileInfo.key)

}

}

因为golang原生就支持异步IO,实现起来最简单便选择了golang语言,对于go语言不熟悉的同学也可以查找一下python语言的异步io库

 更多内容欢迎来到我的知识星球:
 

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

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

相关文章

刚刚❗️德勤2025校招暑期实习测评笔试SHL测评题库已发(答案)

&#x1f4e3;德勤 2024暑期实习测评已发&#xff0c;正在申请的小伙伴看过来哦&#x1f440; ㊙️本次暑期实习优先考虑2025年本科及以上学历的毕业生&#xff0c;此次只有“审计及鉴定”“税务与商务咨询”两个部门开放了岗位~ ⚠️测评注意事项&#xff1a; &#x1f44…

使用Nextjs学习(学习+项目完整版本)

创建项目 运行如下命令 npx create-next-app next-create创建项目中出现的各种提示直接走默认的就行,一直回车就行了 创建完成后进入到项目运行localhost:3000访问页面,如果和我下面页面一样就是创建项目成功了 整理项目 将app/globals.css里面的样式都删除,只留下最上面三…

【LeetCode算法】第112题:路径总和

目录 一、题目描述 二、初次解答 三、官方解法 四、总结 一、题目描述 二、初次解答 1. 思路&#xff1a;二叉树先序遍历。首先访问根节点&#xff0c;若根节点是叶子节点并且值等于目标值&#xff0c;则返回true&#xff0c;否则递归访问左子树和右子树&#xff0c;只要左…

PG 数据库常用参数调整

1.shard_buffers Postgresql使用自己的缓冲区,也使用操作系统缓冲区。这意味着数据存储在内存中两次,首先是 Postgresql缓冲区,然后是操作系统缓冲区。 与其他数据库不同, Postgresql不提供直接IO。这称为双缓冲&#xff08;就是磁盘中的时候读的时候先放在数据库的缓冲区&am…

【上下界分析 差分数组】798得分最高的最小轮调

本文涉及知识点 差分数组 本题同解 C算法前缀和的应用&#xff1a;798得分最高的最小轮调 LeetCode798得分最高的最小轮调 给你一个数组 nums&#xff0c;我们可以将它按一个非负整数 k 进行轮调&#xff0c;这样可以使数组变为 [nums[k], nums[k 1], … nums[nums.lengt…

Web学习_SQL注入_布尔盲注

盲注就是在SQL注入过程中&#xff0c;SQL语句执行后&#xff0c;查询到的数据不能 回显到前端页面。此时&#xff0c;我们需要利用一些方法进行判断或者尝 试&#xff0c;这个过程称之为盲注。而布尔盲注就是SQL语句执行后&#xff0c;页面 不返回具体数据&#xff0c;数据库只…

未卸载干净的proteus安装教程7.8

提醒&#xff1a; 针对第一次安装推荐博文&#xff1a;https://jingyan.baidu.com/article/656db918f8590de381249cbf.html 1、一定要以管理员身份运行软件。 2、以管理员身份运行软件后&#xff0c;默认的ISIS Professional路径是C:\Program Files \Labcenter Electronics\…

802.11漫游流程简单解析与笔记_Part1

最近在进行和802.11漫游有关的工作&#xff0c;需要对wpa_supplicant认证流程和漫游过程有更多的了解&#xff0c;所以通过阅读论文等方式&#xff0c;记录整理漫游相关知识。Part1将记录802.11漫游的基本流程、802.11R的基本流程、与认证和漫游都有关的三层秘钥基础。Part1将包…

Excel行列条件转换问题,怎么实现如图一到图二的效果?

图一 图二 如果数据比较&#xff0c;不建议一上来就用公式&#xff0c;风速值那一列的数据可以确定都是数值型数字&#xff0c;可以先试试用数据透视表做转换工具&#xff1a; 1.创建数据透视表 将采集时间放在行字段&#xff0c;测风放在列字段&#xff0c;风速放在值字段 2.…

安卓逆向经典案例——XX牛

安卓逆向经典案例——XX牛 按钮绑定方式 1.抓包 2.查看界面元素&#xff0c;找到控件id 通过抓包&#xff0c;发现点击登录后&#xff0c;才会出现Encrpt加密信息&#xff0c;所以我们通过控件找到对应id&#xff1a;btn_login 按钮绑定方法——第四种 public class LoginA…

python tushare股票量化数据处理:学习中

1、安装python和tushare及相关库 matplotlib pyplot pandas pandas_datareader >>> import matplotlib.pyplot as plt >>> import pandas as pd >>> import datetime as dt >>> import pandas_datareader.data as web 失败的尝试yf…

使用NetAssist网络调试助手在单台计算机上配置TCP服务器和客户端

要使用NetAssist网络调试助手在同一台计算机上配置一个实例作为服务器&#xff08;server&#xff09;和另一个实例作为客户端&#xff08;client&#xff09;&#xff0c;可以按照以下步骤进行操作&#xff1a; 前提条件 确保已经安装NetAssist网络调试助手&#xff0c;并了…

如何制定工程战略

本文介绍了领导者如何有效制定工程战略&#xff0c;包括理解战略核心、如何收集信息并制定可行的策略&#xff0c;以及如何利用行业最佳实践和技术债务管理来提升团队效能和产品质量。原文: How to Build Engineering Strategy 如果你了解过目标框架&#xff08;如 OKR&#xf…

数仓建模中的一些问题

​​​在数仓建设的过程中&#xff0c;由于未能完全按照规范操作&#xff0c; 从而导致数据仓库建设比较混乱&#xff0c;常见有以下问题&#xff1a; 数仓常见问题 ● 数仓分层不清晰&#xff1a;数仓的分层没有明确的逻辑&#xff0c;难以管理和维护。 ● 数据域划分不明确…

Duck Bro的第512天创作纪念日

Tips&#xff1a;发布的文章将会展示至 里程碑专区 &#xff0c;也可以在 专区 内查看其他创作者的纪念日文章 我的创作纪念日第512天 文章目录 我的创作纪念日第512天一、与CSDN平台的相遇1. 为什么在CSDN这个平台进行创作&#xff1f;2. 创作这些文章是为了赚钱吗&#xff1f…

算法金 | AI 基石,无处不在的朴素贝叶斯算法

大侠幸会&#xff0c;在下全网同名「算法金」 0 基础转 AI 上岸&#xff0c;多个算法赛 Top 「日更万日&#xff0c;让更多人享受智能乐趣」 历史上&#xff0c;许多杰出人才在他们有生之年默默无闻&#xff0c; 却在逝世后被人们广泛追忆和崇拜。 18世纪的数学家托马斯贝叶斯…

用函数指针求a和b中的大者

指针变量也可以指向一个函数。一个函数在编译时被分配给一个入口地址。这个函数入口地址就称为函数的指针。可以用一个指针变量指向函数&#xff0c;然后通过该指针变量调用此函数。 先按一般方法编写程序&#xff1a; 可以用一个指针变量指向max函数&#xff0c;然后通过该指…

【python/pytorch】已解决ModuleNotFoundError: No module named ‘torch‘

【PyTorch】成功解决ModuleNotFoundError: No module named torch 一、引言 在深度学习领域&#xff0c;PyTorch作为一款强大的开源机器学习库&#xff0c;受到了众多研究者和开发者的青睐。然而&#xff0c;在安装和使用PyTorch的过程中&#xff0c;有时会遇到一些问题和挑战…

排序-快排算法对数组进行排序

目录 一、问题描述 二、解题思路 1.初始化 2.将右侧小于基准元素移到左边 3.将左侧大于基准元素移到右边 4.重复执行上面的操作 5.对分好的左、右分区再次执行分区操作 6.最终排序结果 三、代码实现 四、刷题链接 一、问题描述 二、解题思路 快排算法实现数组排序&am…