首页> 搜索结果页
" 性能测试" 检索
共 49452 条结果
简要聊聊UNIX,MINIX,LINUX,BSD的区别与特质
聊聊linux,minux,bsd,unix的历史与区别Linux、MINIX、BSD和UNIX都是类UNIX操作系统,它们都是基于UNIX操作系统的,但也有很多不同点。UNIX操作系统最早由贝尔实验室的Ken Thompson和Dennis Ritchie于1970年代初开发。Unix是商业操作系统,主要应用于大型机和服务器领域,具有高性能和可靠性。但Unix的源代码是封闭的,用户只能购买许可证来使用。MINIX操作系统是由荷兰计算机科学教授Andrew S. Tanenbaum于1987年开发的,它的设计目标是作为操作系统课程的教学工具。MINIX是一个微内核操作系统,它的内核很小,大部分操作系统的功能都在用户空间中实现,使得操作系统的可靠性更高。MINIX的源代码也是开放的,但并没有成为广泛使用的操作系统。Linux操作系统最初由芬兰学生Linus Torvalds于1991年开发。Linux操作系统是一种免费、开源、高性能、可靠、灵活的操作系统,它的源代码是开放的,任何人都可以免费使用和修改。Linux操作系统的成功在于它的开放性和庞大的社区,有数千名开发者在为Linux操作系统做出贡献,不断地推动Linux的发展和完善。BSD操作系统是由伯克利大学的计算机科学系开发的,它最早于1977年发布,主要用于研究和教育。BSD操作系统的开发者也是志愿者,他们致力于开发一个免费、开源、安全、可靠的操作系统。BSD操作系统注重安全性和可靠性,在网络安全和服务器管理方面非常出色,也被广泛用于科学研究和教育领域。这四个操作系统在内核、文件系统、命令行工具、应用程序等方面都有所不同。UNIX和BSD操作系统是商业和开源操作系统,MINIX操作系统是微内核操作系统,而Linux操作系统则是单内核操作系统。此外,它们还有不同的开发模式和社区文化。Linux操作系统具有庞大的社区和生态系统,而BSD操作系统则有着严格的代码审核和开发流程。总的来说,这四个操作系统都是非常出色和强大的操作系统,它们都具有自己独特的特点和优势。Linux操作系统在开源、高性能、可靠、灵活等方面表现突出,MINIX操作系统在教学和可靠性方面表现出色,BSD操作系统在安全性和可靠性方面非常强大,UNIX操作系统则在商业领域的应用和高性能方面具有优势。选择哪种操作系统取决于具体的需求和应用场景。Linux操作系统在各个领域都有广泛的应用,从个人电脑到服务器和嵌入式系统。它的开放性和社区生态系统使得它可以被广泛使用和开发,同时也有很多发行版供用户选择。MINIX操作系统虽然并不像Linux那样流行,但它在教学方面有很大的价值,可以帮助学生深入了解操作系统的内部结构和设计。BSD操作系统则主要用于服务器和安全领域,如防火墙、VPN等,它的源代码也是开放的,可以被广泛使用和开发。UNIX操作系统则在商业领域被广泛使用,如金融、电信、航空等领域,同时也是高性能计算和科学研究的首选操作系统。总的来说,这些操作系统虽然在历史和特点上有所不同,但它们都是在UNIX操作系统的基础上发展而来,吸收了UNIX操作系统的设计思想和优秀的特性,同时也加入了自己的特点和优势。无论是Linux、MINIX、BSD还是UNIX,它们都为计算机技术的发展做出了重要贡献,推动了计算机操作系统的不断发展和进步。linux与minix区别Linux和MINIX是两个非常受欢迎的操作系统,都是基于UNIX的设计理念,并且都使用了微内核的架构,但它们在内核方面有很多区别。本文将介绍Linux和MINIX的内核区别,以帮助读者更好地理解这两个操作系统。首先,Linux和MINIX的内核架构是不同的。Linux内核使用了一种单内核设计,其中所有的核心功能和驱动程序都作为内核模块编译在一起。而MINIX的内核则是基于微内核的设计,只包含了操作系统最基本的功能。微内核只提供最基本的功能,例如进程管理、内存管理和通信机制等,所有的其他功能都通过进程来实现。因此,MINIX的内核更加简单,安全性更高,但Linux的内核更加灵活,可以根据需要添加或删除模块。其次,Linux内核和MINIX内核在系统调用方面也有所不同。Linux内核的系统调用比较多,包括诸如进程管理、内存管理、文件管理、网络管理等功能。这些系统调用通常使用GNU C库进行封装,使得开发人员可以使用高级语言编写应用程序。MINIX内核的系统调用较少,仅提供了一些基本的系统调用,例如fork、exit、wait等。这意味着在MINIX上编写应用程序需要使用低级语言,例如汇编语言或C语言的底层库。另一个区别是Linux和MINIX的内存管理机制。Linux内核使用了一种叫做“虚拟内存”的机制,可以让应用程序在物理内存和硬盘之间交换数据,从而提高系统的性能。而MINIX则使用了一种基本的内存管理机制,不支持虚拟内存。这意味着在MINIX上运行的程序需要占用更多的物理内存,而且如果内存不足时,系统可能会崩溃。此外,Linux和MINIX的设备驱动程序也有所不同。Linux内核的设备驱动程序通常包含在内核模块中,当需要时自动加载。而MINIX的设备驱动程序通常由用户态程序实现,并且需要手动加载。这使得Linux更容易添加新的硬件支持,而MINIX需要更多的手动工作。Linux和MINIX的性能也有所不同。由于Linux内核的灵活性,它可以更好地适应不同的硬件环境和应用场景,并且在处理大型应用程序时更加高效。而MINIX由于其简单的内核设计,更加适合嵌入式系统和安全性要求较另外,Minix采用了微内核架构,即将操作系统分成核心模块和外围模块,只在核心模块中实现最基本的功能,如进程调度、内存管理、进程间通信等,而将其他功能实现在外围模块中,如文件系统、设备驱动程序等。这种架构的优势在于,微内核可以保证系统更加稳定和安全,因为核心模块更小、更简单,更容易测试和维护,而且可以灵活地添加、删除和更新外围模块,使得系统更加可扩展。但是,由于外围模块需要频繁地与核心模块进行通信,会导致性能下降,因此,在Minix中,为了弥补这种性能差异,使用了高效的消息传递机制和缓存机制。相比之下,Linux采用了单内核架构,即将操作系统的所有功能都实现在内核中,包括进程调度、内存管理、文件系统、设备驱动程序等。这种架构的优势在于,内核和外围模块之间的通信和数据传输更加高效,因为它们可以直接共享同一内存空间,而不需要使用消息传递机制,这可以提高系统的性能。此外,由于所有的功能都在内核中实现,使得Linux具有更好的灵活性和可配置性,可以根据用户的需要添加、删除和更新各种功能模块。然而,单内核架构的缺点也很明显。由于内核的代码量很大,包含了所有功能的实现,因此,内核本身的复杂性很高,难以维护和测试,而且在添加、删除和更新功能时,需要重新编译整个内核,非常繁琐和耗时。此外,由于所有功能都在内核中实现,使得内核的大小和资源占用量很大,这会导致系统的启动时间变长,占用更多的内存和磁盘空间。因此,在Linux中,为了克服这些问题,引入了模块化设计的概念。模块化设计指的是将内核分成多个模块,每个模块只实现一部分功能,然后将这些模块编译成动态链接库,通过模块加载器动态地加载和卸载。这种设计可以在保证内核的整体性和完整性的同时,提高内核的可维护性和可扩展性,同时减小内核的大小和资源占用量。总的来说,Linux和Minix在内核架构上的区别在于,Minix采另一个区别是关于文件系统。Linux使用了一个单一的虚拟文件系统(VFS),可以支持多种不同的文件系统。这使得用户可以使用不同的文件系统类型,如ext4、NTFS、FAT32等,从而适应各种不同的需求。同时,Linux的文件系统支持日志记录,这可以提高系统的可靠性和恢复能力。Linux还支持许多其他的文件系统特性,例如存储在文件系统上的扩展属性和ACL(访问控制列表)等。Minix的文件系统则较为简单,它只支持一个基本的文件系统,称为Minix文件系统。它也没有日志记录功能,这意味着在文件系统出现问题时,文件系统的恢复过程将更加困难和不可预测。最后,还有一个重要的区别是Linux和Minix之间的许可证。Linux采用了GNU通用公共许可证(GPL),这意味着任何人都可以免费地访问、使用、修改和分发Linux内核源代码。而Minix则使用了较为严格的BSD许可证,这意味着任何人都可以免费地使用和修改Minix,但不能将其用于商业目的,而且必须在发行的软件中包含原始代码的副本。综上所述,尽管Linux和Minix之间有许多相似之处,但它们之间的区别也很明显。Linux更加先进,具有更广泛的硬件支持和更多的软件选择,同时也更加灵活和可定制。与此相比,Minix则更加简单和稳定,但功能较为有限。对于不同的用户和应用场景,选择适合自己的操作系统是非常重要的。unix与linux的区别Unix和Linux是两个相似的操作系统,都属于类Unix操作系统的范畴,但它们之间也有一些重要的区别。首先,Unix最初是由AT&T公司的开发,而Linux则是由芬兰的一位学生Linus Torvalds在1991年开始开发的。Unix是商业操作系统,而Linux是免费开源软件。由于Unix是商业软件,因此对于普通用户来说,它的价格昂贵,而对于企业用户和大型机构而言,则可能更加适合。其次,Unix和Linux在内核和文件系统上也有一些不同。Unix采用了标准的System V和BSD Unix内核,而Linux则采用了一个自由和开源的内核。这个内核是基于Unix内核的设计,但是具有更先进的功能和更好的可定制性。Linux内核的源代码也是公开的,这使得其易于修改和调整,以满足各种不同的需求。此外,Unix和Linux在软件包管理和应用程序支持上也有所不同。Unix操作系统的应用程序通常需要经过复杂的安装过程,而Linux则具有许多易于使用的软件包管理工具,例如APT、YUM等,这使得安装、更新和删除软件变得非常容易。同时,Linux有许多专门为其开发的应用程序,例如Apache Web服务器、MySQL数据库等。另一个区别是关于硬件支持。Unix操作系统最初是为大型计算机设计的,因此在小型计算机上的使用可能会遇到一些问题。而Linux则已经被移植到了许多不同的架构和平台上,包括个人电脑、服务器、移动设备和嵌入式系统等。最后,Unix和Linux在许可证上也有所不同。Unix是商业软件,通常需要购买许可证,而Linux则是开源软件,可以免费使用、修改和分发。综上所述,Unix和Linux虽然有一些相似之处,但它们之间也有很多显著的区别。选择使用哪种操作系统,应该根据具体的需求和使用情况来进行评估。unix特征Unix是一种操作系统,它具有许多独特的特征和意义。下面是Unix的一些主要特征和意义:多用户和多任务:Unix可以同时运行多个用户的程序,并且可以在多个程序之间进行切换。这使得Unix成为一种适合服务器操作的操作系统,可以同时处理多个请求。遵循标准:Unix有许多标准和规范,这些标准有助于确保软件在不同的Unix系统上具有良好的兼容性。Unix还支持POSIX标准,这是一种为各种操作系统提供统一接口的标准。强大的命令行界面:Unix的命令行界面是其最强大的特征之一。通过使用命令行界面,用户可以执行复杂的任务,并对系统进行广泛的配置和管理。灵活的文件系统:Unix的文件系统具有灵活性,可以支持多个文件系统,并提供了许多不同的文件权限选项。这使得Unix可以为多种用途进行配置,并为用户提供更多的灵活性。稳定和安全:Unix是一种非常稳定和安全的操作系统。这得益于Unix的内部设计,以及它具有强大的权限控制和安全功能。开放源代码:许多Unix操作系统都是开放源代码的,这意味着用户可以查看和修改操作系统的代码。这使得Unix成为一种适合开发和创新的操作系统。可移植性:Unix具有很高的可移植性,可以在不同的硬件平台上运行。这是因为Unix的内部设计是模块化的,可以根据不同的硬件平台进行配置和编译。可扩展性:Unix具有很高的可扩展性,可以为不同的应用程序和任务进行配置。这是因为Unix的内核和文件系统都是模块化的,可以通过添加和删除模块来进行扩展和修改。开放标准:Unix的开放标准和协议使其成为互联网和计算机网络的首选操作系统。这是因为Unix支持许多网络协议,包括TCP/IP和UDP等,这使得Unix成为一个非常强大的网络操作系统。工具和应用程序:Unix具有非常丰富的工具和应用程序,可以为用户提供各种不同的功能。这些工具和应用程序可以通过命令行界面或图形用户界面来访问,使其成为一个非常灵活和功能强大的操作系统。总之,Unix具有许多特征和意义,使其成为计算机领域中最重要和最广泛使用的操作系统之一。其多用户和多任务功能、标准化和可移植性、灵活的文件系统和强大的命令行界面都使其成为适用于各种应用程序和任务的首选操作系统。此外,Unix的开放标准和协议、可扩展性、稳定性和安全性,以及丰富的工具和应用程序也使其成为计算机网络和互联网的首选操作系统。bsd与linuxBSD(Berkeley Software Distribution)和Linux是两个常用的开源操作系统,它们之间有一些区别。历史和开发方式:BSD是从Unix衍生出来的,最初是由加州大学伯克利分校开发的,而Linux是由林纳斯·托瓦兹在1991年开发的。内核:BSD使用的内核是宏内核,而Linux使用的内核是微内核。宏内核在内核中集成了所有操作系统的功能,而微内核将操作系统的功能分成模块化的部分。这使得Linux更灵活,因为它允许用户自己添加或删除模块,而BSD则需要更多的代码来完成这些任务。许可证:BSD和Linux都是开源软件,但是它们使用的许可证略有不同。BSD使用的是BSD许可证,允许用户自由使用、复制和分发软件,但是必须包含版权声明和许可证。Linux使用的是GPL(GNU通用公共许可证),它要求任何使用或修改Linux代码的用户都必须使用同样的许可证来发布其代码,这意味着用户不能将自己的修改作为专有软件发布。可移植性:BSD具有很高的可移植性,可以在不同的硬件平台上运行,这是因为BSD的内核是宏内核,可以在编译时根据不同的硬件平台进行配置。而Linux也具有很高的可移植性,但是需要使用特定的硬件架构版本才能在不同的硬件平台上运行。社区和支持:Linux拥有一个庞大的开发和用户社区,可以为用户提供丰富的支持和资源。BSD的社区比较小,因此可能不如Linux的社区提供的支持和资源丰富。稳定性和安全性:BSD通常被认为比Linux更稳定和安全。这是因为BSD的代码通常更加稳定和可靠,它们也更加关注安全性,BSD内置了一些安全功能,例如jails等。总之,BSD和Linux都是流行的开源操作系统,它们之间有很多相似之处,但也有一些区别。BSD的内核是宏内核,许可证是BSD许可证,稳定性和安全性方面比较优秀,而Linux的内核是微内核,使用的是GPL许可证,具有更灵活的可定制性和庞大的社区支持。
文章
缓存  ·  安全  ·  网络协议  ·  Unix  ·  Linux  ·  网络安全  ·  调度  ·  数据安全/隐私保护  ·  C语言  ·  开发者
2023-03-28
JAVA中如何高效的实现SQL的like语法?
提及最近在优化项目的like语法,那既然谈到了SQL,我们不妨来看看一些主流的解析器是怎么实现like的语法逻辑。这里需要提一下主流的两种SQL解析器,它们分别是ANTLR和Calcite。ANTLR是一款功能强大的语法分析器生成器,可以用来读取、处理、执行和转换结构化文本或者二进制文件。在大数据的一些SQL框架里面有广泛的应用,比如Hive的词法文件是ANTLR3写的,Presto词法文件也是ANTLR4实现的。但是ANTLR并不会直接实现具体的语法,因此没办法找到实现语句。Calcite简化了ANTLR生成代码的过程,它提供了标准的 SQL 语言、多种查询优化和连接各种数据源的能力,同时Calcite 有着良好的可插拔的架构设计,可以让用户很方便的给自己的系统套上一个SQL的外壳,并且提供足够高效的查询性能优化,因此也获得了不少开发者的青睐。这里附上找到的Calcite对于like逻辑匹配的实现。/** SQL {@code LIKE} function. */ public static boolean like(String s,String pattern){ final String regex = Like.sqlToRegexLike(pattern, null); return Pattern.matches(regex, s); }/** Translates a SQL LIKE pattern to Java regex pattern.*/ static String sqlToRegexLike(String sqlPattern,char escapeChar) { int i; final int len = sqlPattern.length(); final StringBuilder javaPattern = new StringBuilder(len + len); for (i = 0; i < len; i++) { char c = sqlPattern.charAt(i); if (JAVA_REGEX_SPECIALS.indexOf(c) >= 0) { javaPattern.append('\\'); } if (c == escapeChar) { if (i == (sqlPattern.length() - 1)) { throw invalidEscapeSequence(sqlPattern, i); } char nextChar = sqlPattern.charAt(i + 1); if ((nextChar == '_') || (nextChar == '%') || (nextChar == escapeChar)) { javaPattern.append(nextChar); i++; } else { throw invalidEscapeSequence(sqlPattern, i); } } else if (c == '_') { javaPattern.append('.'); } else if (c == '%') { javaPattern.append("(?s:.*)"); } else { javaPattern.append(c); } } return javaPattern.toString(); }还有一些别的编译器或者中间件,比如TDDL,附上我找到的实现方式,我们简单看下其实现方式,整体差不多,就是buildPattern的逻辑不太相同。... try { Pattern pattern = patterns.get(buildKey(right, escTmp), new Callable<Pattern>() { @Override public Pattern call() throws Exception { return Pattern.compile(buildPattern(right, escTmp), Pattern.CASE_INSENSITIVE); } }); Matcher m = pattern.matcher(left); return m.matches() ? 1l : 0l; } catch (ExecutionException e) { throw new FunctionException(e.getCause()); } ...到此,综上来看,不少项目是基于正则表达式来完成的,接下来我整理了下我最近实现的几种方式:正则表达式实现Java的正则表达式与SQL的"like"具有不同的语法。最重要的就是必须转义Java视为特殊字符的任何字符,简单处理了下regexParse函数里面就是对于特殊符号的遍历替换操作([](){}.*+?$^|#\)等。public static boolean like(final String dest, final String pattern) { String regex = regexParse(pattern); regex = regex.replace("_",".").replace("%",".*?"); Pattern p = Pattern.compile(regex,Pattern.CASE_INSENSITIVE | Pattern.DOTALL); return p.matcher(dest).matches(); }这种方式在代码层面简单明了,但是性能非常差,多次replace的使用就已经进行了多次遍历,这里有个可以优化的点,对于单个字符做替换可以选择用replaceChars(str, searchChar, replaceChar)这个方案。Java 语言使用的正则表达式执行引擎是 NFA (Non-deterministic finite automaton) 非确定型有穷自动机,这种引擎的特点是:功能很强大,但存在回溯机制导致执行效率慢(回溯严重时可以导致机器 CPU 使用率 100%,直接卡死机器),正则里对于Pattern的处理相关的优化也是可以做的,将编译后的Pattern对象缓存下来,避免反复编译Pattern(对于每一个pattern-exprstr需要缓存一个Pattern),尽量选择懒惰模式和独占模式,避免使用贪婪模式(默认)。这里说的三种模式分别是:贪婪模式、懒惰模式、独占模式贪婪模式数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,直到无法匹配为止。如果发现表达式匹配的结果与预期的不符合,很有可能是因为咱们以为表达式只会匹配前面几个字符,实际确会不停匹配。懒惰模式与贪婪模式相反,懒惰模式会尽量匹配更少的内容,如上面对于百分号的处理。独占模式独占模式应该算是贪婪模式的一种变种,它同样会尽量匹配更多的内容,区别在于在匹配失败的情况下不会触发回溯机制,而是继续向后判断,所以该模式效率最佳。简单算法实现有什么比裸写一个定制版的like更好呢,简单易用,对于内存和性能几乎到了最优的地步,最复杂的情况是O(n*m),有点类似于做一道算法题一样,纯一个match过程,不需要前置的缓存处理,下面用的双指针的方法实现,也可以考虑动态规划去做。(一部分的代码隐藏了)。public static boolean like(final String dest, final String pattern) { int destPointer = 0, patternPointer = 0; int destRecall = -1, patternRecall = -1; final int patternLen = pattern.length(); final int destLen = dest.length(); while( destPointer < destLen) { ...... ...... } while(patternPointer < patternLen && pattern.chatAt(patternPointer) == '%') { patternPointer++; } return patternPointer == patternLen; }    有个场景我们不得不去考虑,那就是回溯的情况,举个例子对于pattern = "a%bcd" 同时匹配 abcde和 abcdbcdbcd这两种情况是无法一样的,用这种方式就需要记录回溯标记(后面我会讲一种方法不需要用到回溯)如果说这是最省内存的方法的话,确实只用到了内部的几个变量就完成了全部的工作,硬要说缺点的话,就是可维护性太差了,逻辑处理之间藕断丝连,将来如果针对语法做一些扩展,那将会是一个比较棘手的改动。有限状态机实现状态机有 3 个组成部分:状态、事件、动作。状态:所有可能存在的状态。包括当前状态和条件满足后要迁移的状态。事件:也称为转移条件,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。一般使用涉及到穷举法、查表法、状态模式穷举法状态机最简单的实现方式,利用if-else或者switch-case,参照状态转移图,将每一种状态转移直接翻译成代码。对于简单的状态机,分支逻辑法的实现方式可以接受。对于复杂的状态机,缺点是容易漏写、错写某些状态转移。除此之外,代码中充斥着大量的if-else,可读性、可维护性都很差。查表法查表法适用于实现状态、事件类型很多、状态转移比较复杂的状态机,利用二维数组表示状态转移表,能极大的提高代码的可读性和可维护性。在二维数组的表示形式中,用数组transitionTable和actionTable分别表示状态转移和动作执行。在这两个数组中,横坐标都表示当前状态,纵坐标都表示发生的事件,值则分别表示转移后的新状态和需要执行的动作。查表法这里引用一个入门举例比如有个pattern为"SCSAS"的字符串,那我们尝试梳理一下这个字符串对应的状态转移表,"!"表示与"S","C","A"无关的字符,下面为有限状态机状态转换表下图为pattern为 "SCSAS" 的有限状态机状态转换图接下来就是match的过程,拿到dest字符串,按照表中的状态数据依次匹配字符就好了,直到找到State = 5的值。状态模式状态模式通常是表达实现状态不多、状态转移简单,但是事件触发动作所包含的业务逻辑比较复杂的状态机。将不同状态下事件所触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。和查表法相比,当状态模式会引入较多的状态类,对于状态比较多的推荐使用查表法;而对于状态比较少,但是动作复杂的,状态模式更加适合。那么like的实现基于状态模式的方式会更方便。动手前我们需要做个简单的分析。假如测试的语句是 'CBEED' like '%B%E%D%',显而易见这个结果肯定是为true的,那么我们怎么实现这么个状态机呢?具体的拆解可以分为两个部分,状态机的构建和运行匹配。public void compile(final String pattern) { ... LikeStateMachine machine = LikeStateMachine.build(pattern); ... }构建的过程就是我们把pattern解析加载的过程,我采用的方式是构建链表的方式。实现就是遍历构建的过程,compile时间复杂度O(n)接下来就是match字符串'CBEED'过程了,代码的实现就是遍历匹配过程,match时间复杂度O(n*m)那么最终的匹配结果可以是图1,也可以是图2,这取决于匹配的逻辑是倒序优先的还是正序优先的图1图2这里的难度的在于匹配的可能性不是唯一的,同时并不是每个百分号符号对应的字符是同一个。比如下面这个case因此我在做matcher状态设计的时候就定义了5种状态类型,最终帮助我实现了整体的逻辑。上面这种方式在实现的时候也需要考虑到回溯的情况,JUMP就是专门为回溯设计的因此整体下来由状态机相关的类一共需要定义5个,足以完成我们的功能以及足够的扩展性。(LikeStateMachine)---状态机 (LikeStateMatcher)---状态机匹配器 (LikeState)---状态机节点(LikeStateStatus)---状态机节点状态 (LikeStateWord)---状态机匹配词 回溯场景优化我在想怎么能够优化掉这种可逆的场景,因为回溯的存在让整个逻辑变得复杂,因此我的目标就是让复杂的逻辑表达更简单一点,我尝试在编译的阶段处理更多的事情,以便于匹配的时候能获取更好的性能。相比之前的case,我对pattern数据进行了类似split的操作,将非百分号的部分用字符串的形式进行了表达,效果出乎意料的好。举个例子,'%BF%EA%D',下面是它的优化过程。我对几种场景进行了场景类型定义: %a = TYPE:LEFT a% = TYPE:RIGHT %a% = TYPE:SURROUND任何带有'%'的pattern都可以被解析成这三种类型,同时我们的状态机节点个数被优化掉了不少。基于这样的实现方式后,回溯逻辑就变得不复存在了,通过当前节点和下个节点的联合校验就能识别,最终能做到O(n) ,同时对于一些场景,可以通过类型+长度判断就能识别,无需继续遍历。让我们在看下之前回溯的case,pattern = "a%bcd" 同时匹配 abcde和 abcdbcdbcd,对于这种RIGHT+LEFT的场景做下特殊处理就行。常用场景优化到目前为止那么是不是还能做进一步的优化呢?其实还可以基于常用的场景再次进一步的优化,我尝试把这些场景罗列出来,'ab' like 'abc','ab' like 'abc%','ab' like '%abc','ab' like '%abc%'可能超过80%的使用场景莫过于上面这些常规运用了,这个时候我想又可以在编译的时候识别到这样的场景了并且做进一步的优化了。'ab' like 'abc' ----> equals("abc")'ab' like 'abc%' (或者类似'abc%%','abc%%%' 以n个'%'结尾)----> startsWith("abc")'ab' like '%abc' (或者类似'%%abc','%%%abc' 以n个'%'开始)----> endsWith("abc")'ab' like '%abc%'(或者类似'%%abc%%', 被n个'%'围绕前后)----> contains("abc")'ab' like '%'(或者类似'%%', n个'%')----> true尽量使用jdk底层的能力去做这样的事,在长度检验等判断上就可能不需要再遍历了。public LikeParserResult compile(final String pattern) { return parseLikeExpress(pattern); } .... public boolean match(final String dest, LikeParserResult likeParserResult) { switch (likeParserResult.getMatchResult()) { case LikeMatchResult.TYPE.ALLMATCH: return true; case LikeMatchResult.TYPE.EQUALS: return doEquals(dest, likeParserResult.getFinalPattern()); case LikeMatchResult.TYPE.STARTSWITH: return doStartsWith(dest, likeParserResult.getFinalPattern()); case LikeMatchResult.TYPE.ENDSWITH: return doEndsWith(dest, likeParserResult.getFinalPattern()); case LikeMatchResult.TYPE.CONTAINS: return doContain(dest, likeParserResult.getFinalPattern()); default: //或者别的实现 return likeParserResult.getLikeStateMachine().match(dest); } }上面给出的代码是为了清楚的看到里面运行,最终根据自己擅长的代码风格(函数式Function,接口等),还可以进一步优化和精简。... public LikeParserResult compile(final String pattern) { return parseLikeExpress(pattern); } public boolean match(final String dest, LikeParserResult likeParserResult) { return likeParserResult.getMatcher().match(dest); } ... public class StartsWithMatcher implements Matcher { ... @Override public Boolean match(String dest) { return dest.startsWith(this.pattern); } } ...压测数据对比算法/运行1亿次avgms/业务场景短-精确匹配pattern = "halo",dest = "halo";短-只存在百分号pattern = "%",dest = "hello";短-右匹配pattern = "he%",dest = "hello!";短-左匹配pattern = "%go",dest ="let's go";短-双向匹配pattern = "%wo%",dest ="world";短-右左匹配pattern = "he%lo",dest ="hello";短-双左匹配pattern = "%he%lo",dest ="hello";状态机(提前编译)11718132149211981477双指针算法74078012771710113317682152长-精确匹配pattern ="this is hello world! let's go!",dest = "this is hello world! let's go!";长-只存在百分号pattern = "%%%%%",dest = "this is hello world! let's go!";长-右匹配pattern = "hello%",dest = "hello world! let's to!";长-左匹配pattern = "%go",dest ="this is hello world! let's go";长-双向匹配pattern = "%wo%",dest ="hello world! let's go!";长-右左匹配pattern = "he%go",dest ="hello world, let's go";长-双左匹配pattern = "%lo%go",dest ="hello world, let's go";状态机(提前编译)10522835652411821966双指针算法2063198424333775307332944569短-右双匹配pattern = "he%lo%",dest ="hello!";长-右双匹配pattern = "he%ld%",dest ="hello world, let's go";短-多双向匹配pattern = "%wo%ld%",dest ="world";长-多双向匹配pattern = "%wo%et%",dest ="this is hello world! let's go!";短-回溯pattern = "%he%rld",dest ="helloworld world";长-回溯pattern = "%he%rld",dest ="this is hello world world world";状态机(提前编译)136613821345343317492063双指针算法218744022072475549868650上面是根据典型的场景设计的测试用例,大体上的数据结论来看是字符串越长于通用算法来说就会是越来越慢的,状态机在绝大多数情况下的表现会优于通用算法,相当少数情况性能差不多。最后按照多个优缺点综合评估的话,个人感觉状态机实现 > 直接实现 > 正则表达式实现,状态机和正则的编译过程可以放到代码编译阶段或者初始化的时候去做,能够避免频繁的性能损耗,对于扩展性和可维护性上来看,更倾向于基于状态机来实现like的语法。
文章
SQL  ·  缓存  ·  自然语言处理  ·  算法  ·  Java  ·  测试技术  ·  编译器  ·  数据库连接  ·  Go  ·  HIVE
2023-03-29
深聊性能测试,从入门到放弃之: Windows系统性能监控(一) 性能监视器介绍及使用。
1、引言小屌丝:鱼哥,你有没有监控Windows系统的工具小鱼:???小屌丝:我的Windows系统要做负载机, 我想监控负载机的性能,但是,不知道下载什么监控工具。小鱼:??小屌丝:我不想在负载机搭建一个监控系统,太费劲了。小鱼:??小屌丝:我觉得负载机,就应该用轻量级的监控工具,但是在网上找了好多,都没有合适的。小鱼:…小屌丝:你有没有好的工具,推荐一下。小鱼:Windows自带的性能监视器,小屌丝:鱼哥,别闹。小鱼:没闹。小屌丝:鱼哥, 你就推荐一个,都说你的电脑是百宝箱,啥都有,别不舍得给我哦。小鱼:给你了, 还不用安装, 就是Windows自带的性能监控器,小屌丝:这… 能用…吗? 能符合我的要求吗?小鱼:是不是白给的就觉得不香??小屌丝:额… 这… 那这性能监控器都包含哪些呢?小鱼:主要有三个: 性能监视器,资源监控器,任务管理器。小屌丝:鱼哥,这三个,我最常用的就是任务管理器…小鱼:呦呵,那你还挺厉害的, 那你能说说,都做啥呢?小屌丝:就是… 关进程…小鱼:我… 确实,是一个方法…。小屌丝:难道,还有其他的功能?小鱼:我… 确实, 有很多功能…小屌丝:那你就给俺说说呗…小鱼:勉为其难…关于Windows系统性能监控,我会分篇来详细介绍,今天,我们先聊一聊性能监视器。2、性能监视器2.1 打开方式打开方式很多种, 这里主要说两种:1、快捷键:windows键+R键, 弹窗输入 perfmon 即可打开 性能监视器 界面2、搜索方式:搜索栏直接输入"性能监视器",打开即可; 2.2 基本介绍Perfmon性能监视器是Windows自带的一个性能工具。主要收集3种类型数据:性能计数器;时间跟踪数据;系统配置信息性能计数器是系统状态或活动情况的度量单位;包含在操作系统中或作为个别应用程序的一部分;以指定的时间间隔请求性能计数器的当前值;事件跟踪数据是从跟踪提供程序收集到的;这些跟踪提供程序是操作系统或者用于报告操作或事件的个别应用程序的组件;可将多个跟踪提供程序的输出合并到一个跟踪会话中;配置信息是从 Windows 注册表项值收集到的;可以在指定时间或间隔记录注册表项值作为日志文件的一部分。性能监视器 界面2.3 计数器介绍2.3.1 处理器性能计数器主要关注系统中的CPU,Processor:%Processor Time如果CPU使用率的值持续超过95%,则表示CPU是瓶颈。计算方式:%Processor Time值 = 100% - Idle process时间比例(即 空闲线程Idle Thread),Processor:% User Time是系统非核心操作消耗的CPU时间;如果表示数据库,则% User Time值大的原因可能是数据库的排序或者函数操作导致消耗过多的CPU时间;2.3.2 内存性能计数器Memory:Pages/sec表示由于硬件页面错误而从磁盘取出的页面数,或由于页面错误而写入磁盘以释放工作及空间的页面数。如果Pages/sec 持续高于几百,可能就需要增加内存,以减少换页的需求。但是,Pages/sec的值很大,并不一定就是内存的问题导致的,还可能是运行使用内存映射文件的程序导致;计数器的比率搞标识分页过多;Memory:Available Mbytes可以使用的内存大小;如果改指标的数据比较小,可能是内存的问题;Memory:Page Faults/sec 和 Memory:Page Reads/sec 计数器测量内存性能。当进程所引用的细腻内存页不在内存中,就会发生页错误,Memory:Page Faults/sec表示页错误的个数;Memory:Page Reads/sec是读取磁盘,提取解决页错误所需页的次数;2.3.3 网络性能计数器Network Interface:Bytes received/sec每秒接收的数据为多少Bytes,结合Bytes total/sec 进行分析;Network Interface:Bytes sent/sec每秒发送数据为多少Bytes;结合Bytes total/sec 进行分析;Network Interface:Bytes total/sec机器接受和发送的总共为多少Bytes推荐不要超过带宽的50%;Network Interface:Packets/sec每秒的数据包个数,根据实际数据量大小,无建议阈值;结合Bytes total/sec 进行分析;2.4 创建及使用2.4.1 用户自定义创建1、在性能→数据收集器集→用户定义: 右键 新建→数据收集器集2、新建页面, 输入名称 并选择 手动创建(高级),点击下一步3、床架数据日志:选择 性能计数器,并点击下一步;4、我们就选择数据库性能计数器,具体操作如下:5、在数据保存位置,可以默认,也可以自己选择,具体如下:6、这一页,默认即可, 然后点击完成;7、在用户定义列表, 启动创建的"数据收集器Demo",如下:2.4.2 直接添加计数器1、性能→性能监视器,点击 +,进入到添加计数器 页面,如下: 2、选择 计数器,这里依然选择 数据库,并点击添加 →确定 按钮,如下:3、此时在性能监视器页面, 可以看到添加的数据库计数器,并展示数据,如下;3、总结看到这里, 关于Windows系统自带的性能监视器的内容,就介绍的差不多了。其实,Windows系统自带的性能监视器,还是有很强大的功能。并且, 不需要你安装任何第三个监控工具,就可以把你的系统信息展示的明明白白的 。当然, 《Windows系统性能监控》是一个系列的文章,《深聊性能测试,从入门到放弃之: Windows系统性能监控(一) 性能监视器介绍及使用。》《深聊性能测试,从入门到放弃之: Windows系统性能监控(二) 资源监控器介绍及使用。》《深聊性能测试,从入门到放弃之: Windows系统性能监控(三)任务管理器介绍及使用。》当然,小鱼的性能专栏,还有很多系列文字, 如 性能专栏, 包含:从性能理论到实战的系列博文;性能工具的使用,如: Jmeter、Locust等;性能整个流程的梳理,性能需求的概述;如何进行性能分析;如何进行性能调优;手把手从0到1搭建Locust 性能测试平台;MySQL性能监控的使用及分析;APP性能测试及关注点;… 等等因为 小鱼的性能专栏内容太多了,我就不一一列举了,如果你现在想进阶性能测试高级工程师,如果你想第一时间学习小鱼发布的性能文字;如果你想学习最专业的的性能测试知识;可以持续关注小鱼。我是小鱼:CSDN 博客专家;阿里云-专家博主;金牌面试官;51讲师;关注我,带你学习更多更专业的性能知识。
文章
监控  ·  关系型数据库  ·  MySQL  ·  测试技术  ·  数据库  ·  Windows
2022-12-02
深聊性能测试,从入门到放弃之:Locust性能自动化(一)初识Locust
1. Locust基本介绍1.1 引言现在不管是互联网行业或者是传统行业,对性能的要求,都日渐增多,为了能更快更准确的定位问题,发现问题,解决问题,市面上出现了越来越多的性能测试工具,例如Jmeter,Loadrunner,Locus等,而今天,我们主要介绍的,就是Locust!很多人并不知道什么是Locust,包括使用python的人,因为不涉及到,所以不会去可以了解,那么,什么是Loucst,以及Locust的功能,有点是啥呢,跟着小鱼,往下看~1.2 简介Locust是开源的使用Python开发,基于事件,支持分布式并且提供Web UI进行测试执行和结果展示的性能测试工具。1.Locust 与Jmeter占用资源比较Locust之所以在资源占用方面完胜开源的Jmeter,主要是因为:>>两者的模式用户方式不同:①Jmeter是通过线程来作为虚拟用户②Locust借助gevent库对协程的支持,以greenlet来实现对用户的模拟你。所以,在相同配置下,Locust能支持的并发用户数相比Jmeter,就不止提升了一个Level。2.Locust使用语言Locust使用的是Python代码定义测试场景,目前支持Python2.7,3.3~3.7。它自带一个Web UI,用于定义用户模型,发起测试,实时测试数据,错误统计等。3.官方文档更多更详细的内容,可以参考:1.文档内容,点击:官方文档2.源代码,点击:Github2. Locust安装方式同样,我们直接 pip安装即可2.1 安装 locustpip install locust2.2 安装pyzmq如果打算运行Locust 分布在多个进程/进程,需要安装pyzmq同样使用pip安装pip install pyzmq注:如果安装 locust-1.2.3 版本,可能就不需要再次安装pyzmq了,好像直接附带安装 pyzmq2.3 安装成功确认打开cmd窗口,直接输入locust --help如果出现下图,则说明安装成功3. Locust 参数说明针对上图的安装成功后,我们来进行参数解析3.1 参数信息我们先把locust --help 里面的所有信息,copy出来Common options: -h, --help show this help message and exit -f LOCUSTFILE, --locustfile LOCUSTFILE Python module file to import, e.g. '../other.py'. Default: locustfile --config CONFIG Config file path -H HOST, --host HOST Host to load test in the following format: http://10.21.32.33 -u NUM_USERS, --users NUM_USERS Number of concurrent Locust users. Primarily used together with --headless -r SPAWN_RATE, --spawn-rate SPAWN_RATE The rate per second in which users are spawned. Primarily used together with --headless -t RUN_TIME, --run-time RUN_TIME Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with --headless -l, --list Show list of possible User classes and exit Web UI options: --web-host WEB_HOST Host to bind the web interface to. Defaults to '*' (all interfaces) --web-port WEB_PORT, -P WEB_PORT Port on which to run web host --headless Disable the web interface, and instead start the load test immediately. Requires -u and -t to be specified. --web-auth WEB_AUTH Turn on Basic Auth for the web interface. Should be supplied in the following format: username:password --tls-cert TLS_CERT Optional path to TLS certificate to use to serve over HTTPS --tls-key TLS_KEY Optional path to TLS private key to use to serve over HTTPS Master options: Options for running a Locust Master node when running Locust distributed. A Master node need Worker nodes that connect to it before it can run load tests. --master Set locust to run in distributed mode with this process as master --master-bind-host MASTER_BIND_HOST Interfaces (hostname, ip) that locust master should bind to. Only used when running with --master. Defaults to * (all available interfaces). --master-bind-port MASTER_BIND_PORT Port that locust master should bind to. Only used when running with --master. Defaults to 5557. --expect-workers EXPECT_WORKERS How many workers master should expect to connect before starting the test (only when --headless used). Worker options: Options for running a Locust Worker node when running Locust distributed. Only the LOCUSTFILE (-f option) need to be specified when starting a Worker, since other options such as -u, -r, -t are specified on the Master node. --worker Set locust to run in distributed mode with this process as worker --master-host MASTER_NODE_HOST Host or IP address of locust master for distributed load testing. Only used when running with --worker. Defaults to 127.0.0.1. --master-port MASTER_NODE_PORT The port to connect to that is used by the locust master for distributed load testing. Only used when running with --worker. Defaults to 5557. Tag options: Locust tasks can be tagged using the @tag decorator. These options let specify which tasks to include or exclude during a test. -T [TAG [TAG ...]], --tags [TAG [TAG ...]] List of tags to include in the test, so only tasks with any matching tags will be executed -E [TAG [TAG ...]], --exclude-tags [TAG [TAG ...]] List of tags to exclude from the test, so only tasks with no matching tags will be executed Request statistics options: --csv CSV_PREFIX Store current request stats to files in CSV format. Setting this option will generate three files: [CSV_PREFIX]_stats.csv, [CSV_PREFIX]_stats_history.csv and [CSV_PREFIX]_failures.csv --csv-full-history Store each stats entry in CSV format to _stats_history.csv file. You must also specify the '-- csv' argument to enable this. --print-stats Print stats in the console --only-summary Only print the summary stats --reset-stats Reset statistics once spawning has been completed. Should be set on both master and workers when running in distributed mode Logging options: --skip-log-setup Disable Locust's logging setup. Instead, the configuration is provided by the Locust test or Python defaults. --loglevel LOGLEVEL, -L LOGLEVEL Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. Default is INFO. --logfile LOGFILE Path to log file. If not set, log will go to stdout/stderr Step load options: --step-load Enable Step Load mode to monitor how performance metrics varies when user load increases. Requires --step-users and --step-time to be specified. --step-users STEP_USERS User count to increase by step in Step Load mode. Only used together with --step-load --step-time STEP_TIME Step duration in Step Load mode, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with --step-load Other options: --show-task-ratio Print table of the User classes' task execution ratio --show-task-ratio-json Print json data of the User classes' task execution ratio --version, -V Show program's version number and exit --exit-code-on-error EXIT_CODE_ON_ERROR Sets the process exit code to use when a test result contain any failure or error -s STOP_TIMEOUT, --stop-timeout STOP_TIMEOUT Number of seconds to wait for a simulated user to complete any executing task before exiting. Default is to terminate immediately. This parameter only needs to be specified for the master process when running Locust distributed. User classes: UserClass Optionally specify which User classes that should be used (available User classes can be listed with -l or --list)3.2 参数信息解析这里,只写了大部分,至于缺少的部分,小鱼觉得不太常用,就不浪费浪费电了~毕竟1度电 1块多钱 ! !还有,就是小鱼要吃午饭~-h, --help 查看帮助 -H HOST, --host=HOST 指定被测试的主机,采用以格式:http://10.21.32.33 --web-host=WEB_HOST 指定运行 Locust Web 页面的主机,默认为空 ''。 -P PORT, --port=PORT, --web-port=PORT 指定 --web-host 的端口,默认是8089 -f LOCUSTFILE, --locustfile=LOCUSTFILE 指定运行 Locust 性能测试文件,默认为: locustfile.py --csv=CSVFILEBASE, --csv-base-name=CSVFILEBASE 以CSV格式存储当前请求测试数据。 --master Locust 分布式模式使用,当前节点为 master 节点。 --slave Locust 分布式模式使用,当前节点为 slave 节点。 --master-host=MASTER_HOST 分布式模式运行,设置 master 节点的主机或 IP 地址,只在与 --slave 节点一起运行时使用,默认为:127.0.0.1. --master-port=MASTER_PORT 分布式模式运行, 设置 master 节点的端口号,只在与 --slave 节点一起运行时使用,默认为:5557。注意,slave 节点也将连接到这个端口+1 上的 master 节点。 --master-bind-host=MASTER_BIND_HOST Interfaces (hostname, ip) that locust master should bind to. Only used when running with --master. Defaults to * (all available interfaces). --master-bind-port=MASTER_BIND_PORT Port that locust master should bind to. Only used when running with --master. Defaults to 5557. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558. --expect-slaves=EXPECT_SLAVES How many slaves master should expect to connect before starting the test (only when --no-web used). --no-web no-web 模式运行测试,需要 -c 和 -r 配合使用. -c NUM_CLIENTS, --clients=NUM_CLIENTS 指定并发用户数,作用于 --no-web 模式。 -r HATCH_RATE, --hatch-rate=HATCH_RATE 指定每秒启动的用户数,作用于 --no-web 模式。 -t RUN_TIME, --run-time=RUN_TIME 设置运行时间, 例如: (300s, 20m, 3h, 1h30m). 作用于 --no-web 模式。 -L LOGLEVEL, --loglevel=LOGLEVEL 选择 log 级别(DEBUG/INFO/WARNING/ERROR/CRITICAL). 默认是 INFO. --logfile=LOGFILE 日志文件路径。如果没有设置,日志将去 stdout/stderr --print-stats 在控制台中打印数据 --only-summary 只打印摘要统计 --no-reset-stats Do not reset statistics once hatching has been completed。 -l, --list 显示测试类, 配置 -f 参数使用 --show-task-ratio 打印 locust 测试类的任务执行比例,配合 -f 参数使用. --show-task-ratio-json 以 json 格式打印 locust 测试类的任务执行比例,配合 -f 参数使用. -V, --version 查看当前 Locust 工具的版本.3.3 Locust主要库①geventgevent是一种基于协程的Python网络库,它用到Greenlet提供的,封装了libevent事件循环的高层同步API。②flaskPython编写的轻量级Web应用框架。如果想了解flask及代码实战,看小鱼的这篇文章:《Python3,网站搭建之构建Flask项目》③requestsPython的HTTP库可以参考小鱼的这篇文章《requests库常用到的7个主要方法及控制访问参数》④msgpack-pythonMessagePack是一种快速、紧凑的二进制序列化格式,适用于类似JSON的数据格式。msgpack-python主要提供MessagePack数据序列化及反序列化的方法。⑤sixPython2和3兼容库,用来封装Python2和Python3之间的差异性⑥ pyzmqpyzmq是zeromq(一种通信队列)的Python绑定,主要用来实现Locust的分布式模式运行。4. Locust类说明4.1 client属性①在Locust类中,静态字段client即客户端的请求方法,这里的client字段没有绑定客户端请求方法,因此在使用Locust时,需要先继承Locust类class HttpLocust(Locust),然后在self.client =HttpSession(base_url=self.host)绑定客户端请求方法;②对于常见的HTTP(s)协议,Locust已经实现了HttpLocust类,其self.client=HttpSession(base_url=self.host),而HttpSession继承自requests.Session。③在测试HTTP(s)的Locust脚本中,可以通过client属性来使用Python requests库的所 有方法,调用方式与 reqeusts完全一致。④由于requests.Session的使用,client的方法调用之间就自动具有了状态记忆功能。⑤常见的场景就是,在登录系统后可以维持登录状态的Session,从而后续HTTP请求操作都能带上登录状态。4.2 其他属性4.2.1 task_set指向一个TaskSet类,TaskSet类定义了用户的任务信息,该静态字段为必填。4.2.2 max_wait/min_wait每个用户执行两个任务间隔的上下限(毫秒),具体数值在上下限中随机取值,若不指定则默认间隔时间为1秒。4.2.3 host被测试系统的host,当在终端中启动locust时没有指定–host参数时才会用到。4.2.4 weight同时运行多个Locust类时,用于控制不同类型的任务执行权重。5. Loucst执行流程具体流程如下:①先执行WebsiteTasks中的on_start(只执行一次),作为初始化;②从WebsiteTasks中随机挑选(如果定义了任务间的权重关系,那么就按照权重关系随机挑选)一个任务执行;③根据Locust类中min_wait和max_wait定义的间隔时间范围(如果TaskSet类中也定义了min_wait或者max_wait,以TaskSet中的优先),在时间范围中随机取一个值,休眠等待;④重复2~3步骤,直到测试任务终止。6. 性能测试工具比较关于如何选择性能测试工具,小鱼在《深聊性能测试,从入门到放弃之:初识性能测试》写过,每个工具,都有自己存在的价值,即,存在即有意义接下来,小鱼给大家分析一下,Jmeter、Loadrunner、Locust这三个工具。通过对比,可以看到,Locust并不占优,但是,小鱼喜欢用这个的原因,是因为:1、首先是模拟用户操作①Locust采用Pure Python脚本描述,并且HTTP请求完全基于Requests库。②Requests这个库非常简洁易用,但功能十分强大,很多其它编程语言的HTTP库都借鉴了它的思想和模式,如果将其评选为最好用的HTTP库之一(不限语言),应该也不会有太大的争议。③除了HTTP(S)协议,Locust也可以测试其它任意协议的系统,只需要采用Python调用对应的库进行请求描述即可。2、并发机制①Locust的并发机制采用协程(gevent)的机制。②采用多线程来模拟多用户时,线程数会随着并发数的增加而增加,而线程之间的切换是需要占用资源的,IO的阻塞和线程的sleep会不可避免的导致并发效率下降;正因如此,LoadRunner和Jmeter这类采用进程和线程的测试工具,都很难在单机上模拟出较高的并发压力。③而协程和线程的区别在于:协程避免了系统级资源调度,由此大幅提高了性能。④正常情况下,单台普通配置的测试机可以生产数千并发压力,这是LoadRunner和Jmeter都无法实现的。7. Locust代码实战代码实战部分,我放到了第二章节来展示。可以直接点击传送《深聊性能测试,从入门到放弃之:Locust性能自动化(二)代码实战》
文章
消息中间件  ·  JSON  ·  资源调度  ·  测试技术  ·  API  ·  数据格式  ·  Python
2022-11-02
深聊性能测试,从入门到放弃之:Locust性能自动化(二)代码实战
1. 引言在本章节,你可以学习到:1、Locust代码实例展示及解读:①官网代码示例②demo模板代码3、Loucst的高级用法:①关联②参数化③检查点4、Locust的运行模式:①单进程运行模式②多进程分布式运行5、Locust界面展示及结果分析2. Locust实例展示2.1 官网代码示例我们来看看官网的第一个例子,很简单:# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import HttpUser, between, task class WebsiteUser(HttpUser): #设置等待时间间隔 wait_time = between(5, 15) def on_start(self): self.client.post("/login", { "username": "test_user", "password": "" }) @task def index(self): self.client.get("/") self.client.get("/static/assets.js") @task def about(self): self.client.get("/about/")这里有几点说一下:1、between: 设置等待时间, 5s~15s;2、client.get/ client.post: 用法跟request是一样的。其他的就没有什么可以重点强调的!2.2 Locust 代码模板及执行顺序这段代码,小鱼展示两点:1、locust demo模板;2、locust 代码执行顺序。# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import HttpUser,TaskSet,task ''' 执行顺序: Locust setup → TaskSet setup → TaskSet on_start → TaskSet tasks → TaskSet on_stop → TaskSet teardown → Locust teardown ''' class UserBehavor(TaskSet): #启动locust是运行setup方法 def setup(self): print('task setup') def teardown(self): print('task teardown') #虚拟用户启动task时运行 def on_start(self): print('start') #虚拟用户结束task时运行 def on_stop(self): print('end') @task(2) def index(self): self.client.get('/') @task(1) def profile(self): self.client.get('/profile') class WebsitUser(HttpUser): def setup(self): print('locust setup') def teardown(self): print('locust teardown') host = 'http://xxx.com' task_set = task(UserBehavor) min_wait = 100 max_wait = 5000 if __name__ == '__main__': pass虽然小鱼展示了模板,可以直接使用,但是,里面的内容,需要各位大佬自己填充~~毕竟 业务不同,填充的内容也不一样!!小屌丝:鱼哥,感觉你在开车,但是没证据!!小鱼:都快一点了,你还不睡觉,白富美不香吗!!!小屌丝:…3. Locust 类代码分析3.1 实例代码展示关于locust类的详细讲解,放在了第一章节,因为小鱼觉得:先了解基础,再去看代码,这样就不至于看代码想看天书,至少遇到一个类,能有一个印象。这里,为了方便大家,点击下方带颜色的文字即可进入第一章节:《深聊性能测试,从入门到放弃之:Locust性能自动化(一)初识Locust》回归正题,老规矩,先上代码,再逐层分析# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import HttpUser,task,TaskSet ''' 在版本10.1,已经不再使用HttpLocust 和Locust, 取而代之的是HttpUser 和User ''' # 定义ScriptTasks类,继承TaskSet类 class ScriptTasks(TaskSet): #初始化,每个locust用户开始做的第一件事 def on_start(self): #放置 用户名和密码 self.client.post('/login', { "username":"carl_dj", "password":'111111' }) #@task()装饰的方法为一个事务,方法的参数用于指定该行为的执行权重,参数越大每次被虚拟用户执行的概率越高,默认为1 @task(2) #创建index方法, def index(self): self.client.get('/') @task(1) def about(self): #self.client 属性使用python的request库的方法,调用和使用方法和request一样 self.client.get('/about') @task(2) def demo(self): payload = {} headers = {} self.client.post('/demo', data = payload,headers = headers) #TaskSet类,该类定义用户任务信息(模拟用户信息), class WebsitUser(HttpUser): #指向一个定义的用户行为 task_set = task(ScriptTasks) #被测系统的host, host = 'http://www.xxxxxx.com' #每个用户执行两个任务间隔时间最小值,单位是(毫秒,默认是1000ms) min_wait = 100 # 每个用户执行两个任务间隔时间最大值,单位是(毫秒) max_wait = 5000这里小鱼在强调一次:1、关于 HttpUser 和User的使用, 在版本10.1之后,就需要换成HttpUser 和 User,否则报错;>>>因为小鱼发现,很多网站的大佬都在使用HttpLocust 和Locust,如果你的Locust 版本是 9.x或者8.x,可以使用,不做强要求。3.2 classTaskSet 用法及展示3.2.1 定义1、TaskSet类实现了虚拟用户所执行任务的调度算法,包括:①规划任务执行顺序:schedule_task;②挑选下一个任务:execute_next_task;③执行任务:execute_task;④休眠等待:wait;⑤中断控制:interrupt;2、在1的基础上,就可以在TaskSet子类中进行以下操作:①描述虚拟用户的业务测试场景;②对虚拟用户的所有行为进行组织和描述;③对不同任务的权重进行配置;3、 @task①通过@task()装饰的方法为一个事务。>>>参数越大每次被虚拟用户执行的概率越高,默认是1。4、TaskSet子类中采用2种方式定义任务信息:① @task② tasks属性3.2.2 代码展示1、采用@task装饰器定义任务信息:# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import task,TaskSet class UserBehav(TaskSet): @task(2) def test_case1(self): self.client.get("/testcase1") @task(4) def test_case2(self): self.client.get("/testcase2")2、采用tasks属性定义任务信息:# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import TaskSet def test_case1(self): self.client.get("/testcase1") def test_case2(self): self.client.get("/testcase2") class UserBehav(TaskSet): tasks = {test_case1:2,test_case2:4} #另一种写法 # tasks = [(test_job1,1), (test_job1,3)]上面的代码,没有什么难度,这里就不做解释。4. Locust高级用法4.1 关联我们先上代码# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import HttpUser,task,TaskSet from lxml import etree class WebsitTasks(TaskSet): #获取session def get_session(self,html): tags = etree.HTML(html) return tags.xpath("输入标签需要定位的到元素") #启动 def on_start(self): html = self.client.get('/index') session = self.get_session(html.text) #设置payload参数 payload = { 'username': 'carl_dj', 'password':'111111', 'session':session } #设置header参数 header = {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36"} self.client.post('/login',data = payload,headers = header) @task(5) def index(self): self.client.get('/') @task(1) def about(self): self.client.about('/about/') class WebsiteUser(HttpUser): # 被测系统的host,在终端中启动locust时没有指定--host参数时才会用到 host = "http://www.xxx.com/user/login" # TaskSet类,该类定义用户任务信息,必填。这里就是:WebsiteTasks类名,因为该类继承TaskSet; task_set = task(WebsiteTasks) # 每个用户执行两个任务间隔时间的上下限(毫秒),具体数值在上下限中随机取值,若不指定默认间隔时间固定为1秒 min_wait = 5000 max_wait = 15000嗯,详细的内容都在代码中标注,这里就不再重新唠叨。4.2 参数化4.2.1 聊一聊参数化老话说的好:代码写死一时爽,框架重构火葬场虽然大部分大佬还没有涉及到 设计框架的阶段,但是,只要稍微努努力…火葬场 迟早都是要去滴~ ~所以,就有了另一句老话:动态代码一时爽,一直动态一时爽可见,参数化的作用,真的,很Nice!话说回来,参数化的作用是啥呢:循环取数据,数据可重复使用。4.2.2 三个场景认识参数化场景1:>> 模拟3个用户并发请求网页,共有100个URL地址,每个虚拟用户都会依次循环加载100个URL地址代码展示:# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import TaskSet, task, HttpUser class UserBehav(TaskSet): def on_start(self): self.index = 0 @task def test_visit(self): url = self.locust.share_data[self.index] print('visit url: %s' % url) self.index = (self.index + 1) % len(self.locust.share_data) self.client.get(url) class WebsiteUser( HttpUser): host = 'http://www.xxx.com' task_set = task(UserBehav) share_data = ['url1', 'url2', 'url3', 'url4', 'url5'] min_wait = 1000 max_wait = 15000场景2>>>模拟3用户并发注册账号,共有9个账号,要求注册账号不重复,注册完毕后结束测试概括:保证并发测试数据唯一性,不循环取数据>>>所有并发虚拟用户共享同一份测试数据,并且保证虚拟用户使用的数据不重复;代码采用队列# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import TaskSet, task, HttpUser import queue class UserBehav(TaskSet): @task def test_register(self): try: data = self.locust.user_data_queue.get() except queue.Empty: print('account data run out, test ended.') exit(0) print('register with user: {}, pwd: {}'\ .format(data['username'], data['password'])) payload = { 'username': data['username'], 'password': data['password'] } self.client.post('/register', data=payload) class WebsiteUser(HttpUser): host = 'http://www.xxx.com' task_set = task(UserBehav) user_data_queue = queue.Queue() for index in range(100): data = { "username": "test%04d" % index, "password": "pwd%04d" % index, "email": "test%04d@debugtalk.test" % index, "phone": "186%08d" % index, } user_data_queue.put_nowait(data) min_wait = 1000 max_wait = 15000场景3>>>模拟3个用户并发登录账号,总共有9个账号,要求并发登录账号不相同,但数据可循环使用;概括:保证并发测试数据唯一性,循环取数据;>>>所有并发虚拟用户共享同一份测试数据,保证并发虚拟用户使用的数据不重复,并且数据可循环重复使用。代码展示# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import TaskSet, task, HttpUser import queue class UserBehav(TaskSet): @task def test_register(self): try: data = self.locust.user_data_queue.get() except queue.Empty: print('account data run out, test ended') exit(0) print('register with user: {0}, pwd: {1}' .format(data['username'], data['password'])) payload = { 'username': data['username'], 'password': data['password'] } self.client.post('/register', data=payload) self.locust.user_data_queue.put_nowait(data) class WebsiteUser(HttpUser): host = 'http://www.xxx.com' task_set = task(UserBehav) user_data_queue = queue.Queue() for index in range(100): data = { "username": "test%04d" % index, "password": "pwd%04d" % index, "email": "test%04d@debugtalk.test" % index, "phone": "186%08d" % index, } user_data_queue.put_nowait(data) min_wait = 1000 max_wait = 150004.3 检查点我们直接使用assert来进行断言操作。上代码:# -*- coding: utf-8 -*- """ @ auth : carl_DJ @ time : 2020-9-23 """ from locust import task @task def test_interface(self): #直接使用csdn的某一个api with self.client.get("https://editor.csdn.net/md?articleId=108596407",name = 'fileconfig',catch_response=True) as response: #python断言对接口返回值中的max字段进行断言 assert response.json()['rating']['max']==100 #对http响应码是否200进行判断 if response.status_code ==200: response.success() else: response.failure("Failed!")这里说明一下:1、断言形式:with self.client.get(“url地址”,catch_response=True) as response;2、response.status_code获取http响应码进行判断,失败后会加到统计错误表中;>>>如果直接使用python自带assert,则不会进入到locust报表,3、默认不写参数catch_response=False断言无效,将catch_response=True才生效。5. Locust运行模式运行Locust时,通常会使用到两种运行模式:单进程运行和多进程分布式运行。5.1 单进程运行模式5.1.1 定义及解析1、Locust所有的虚拟并发用户均运行在单个Python进程中,由于单进程的原因,并不能完全发挥压力机所有处理器的能力,因此主要用于调试脚本和小并发压测的情况。2、当并发压力要求较高时,就需要用到Locust的多进程分布式运行模式。>>> 一旦单台计算机不足以模拟所需的用户数量,Locust就会支持运行分布在多台计算机上的负载测试。3、多进程分布运行情况:①多台压力机同时运行,每台压力机分担负载一部分的压力生成;②同一台压力机上开启多个slave的情况。>>>如果一台压力机有N个处理器内核,那么就在这台压力机上启动一个master,N个slave。>>>也可以启动N的倍数个slave。5.1.2 有Web UI模式Locust默认采用8089端口启动web;如果要使用其它端口,就可以使用如下参数进行指定。参数说明:① -P, --port:指定web端口,默认为8089.终端中—>进入到代码目录: locust -f locustfile.py --host = xxxxx.com② -f: 指定性能测试脚本文件③ -host: 被测试应用的URL地址【如果不填写,读取继承(HttpLocust)类中定义的host】注意1、如果Locust运行在本机,在浏览器中访问http://localhost:8089即可进入Locust的Web管理页面;2、如果Locust运行在其它机器上,那么在浏览器中访问http://locust_machine_ip:8089即可。5.1.3 无Web UI模式如果采用no_web形式,则需使用–no-web参数,并会用到如下几个参数。参数说明:① -c, --clients:指定并发用户数;② -n, --num-request:指定总执行测试次数;③ -r, --hatch-rate:指定并发加压速率,默认值位1。示例展示:$ locust -f locustfile.py --host = xxxxx --no-web -c 1 -n 25.1.4 启动locust在Pycharm的 的Terminal 中启动 locust,输入内容:locust --host =http://localhost -f test_load.py也可以在 VScode、WindowsPowserShell中启动,这里我就是用 Pycharm演示一下5.2 多进程分布式运行不管是单机多进程,还是多机负载模式,运行方式都是一样的,都是先运行一个master,再启动多个slave。5.2.1 master启动1、启动master时,需要使用–master参数2、如果要使用8089以外的端口,还需要使用-P, --port参数示例展示:locust -f prof_load.py --master --port=80895.2.2 slave 启动1、启动slave时需要使用–slave参数2、在slave中,就不需要再指定端口3、master启动后,还需要启动slave才能执行测试任务示例展示locust -f monitorAgent.py --slaveocust -f monitorAgent.py --slave --master-host=<locust_machine_ip>master和slave都启动完成,就可以进入到Locust 的web界面。剩下的操作,就是界面操作了~6. Locust 结果分析Number of users to simulate: 设置虚拟用户数,对应中no_web模式的-c, --clients参数;Hatch rate(users spawned/second): 每秒产生(启动)的虚拟用户数 , 对应着no_web模式的-r, --hatch-rate参数,默认为1。上图:启动了一个 master 和两个 slave,由两个 slave 来向被测试系统发送请求性能测试参数Type: 请求的类型,例如GET/POST。Name:请求的路径。这里为百度首页,即:https://www.baidu.com/request:当前请求的数量。fails:当前请求失败的数量。Median:中间值,单位毫秒,一半的服务器响应时间低于该值,而另一半高于该值。Average:平均值,单位毫秒,所有请求的平均响应时间。Min:请求的最小服务器响应时间,单位毫秒。Max:请求的最大服务器响应时间,单位毫秒。Content Size:单个请求的大小,单位字节。reqs/sec:是每秒钟请求的个数。相比于LoadRunner,Locust的结果展示十分简单,主要就四个指标:并发数、RPS、响应时间、异常率。但对于大多数场景来说,这几个指标已经足够了。上图是 曲线分析图。关于locust的代码实战及结果分析,就先到这里。在这里,小鱼再多说一句:万行代码从头写,先看基础挺要紧所以,要弄懂代码,还是先看基础。点击传送《深聊性能测试,从入门到放弃之:Locust性能自动化(一)初识Locust》
文章
算法  ·  测试技术  ·  BI  ·  调度  ·  Python
2022-11-02
高德Go生态的服务稳定性建设|性能优化的实战总结
本文共同作者:阳迪、联想、君清前言go语言凭借着优秀的性能,简洁的编码风格,极易使用的协程等优点,逐渐在各大互联网公司中流行起来。而高德业务使用go语言已经有3年时间了,随着高德业务的发展,go语言生态也日趋完善,今后会有越来越多新的go服务出现。在任何时候,保障服务的稳定性都是首要的,go服务也不例外,而性能优化作为保障服务稳定性,降本增效的重要手段之一,在高德go服务日益普及的当下显得愈发重要。此时此刻,我们将过去go服务开发中的性能调优经验进行总结和沉淀,为您呈上这篇精心准备的go性能调优指南。通过本文您将收获以下内容: 从理论的角度,和你一起捋清性能优化的思路,制定最合适的优化方案。推荐几款go语言性能分析利器,与你一起在性能优化的路上披荆斩棘。总结归纳了众多go语言中常用的性能优化小技巧,总有一个你能用上。基于高德go服务百万级QPS实践,分享几个性能优化实战案例,让性能优化不再是纸上谈兵。1. 性能调优-理论篇1.1 衡量指标优化的第一步是先衡量一个应用性能的好坏,性能良好的应用自然不必费心优化,性能较差的应用,则需要从多个方面来考察,找到木桶里的短板,才能对症下药。那么如何衡量一个应用的性能好坏呢?最主要的还是通过观察应用对核心资源的占用情况以及应用的稳定性指标来衡量。所谓核心资源,就是相对稀缺的,并且可能会导致应用无法正常运行的资源,常见的核心资源如下:cpu:对于偏计算型的应用,cpu往往是影响性能好坏的关键,如果代码中存在无限循环,或是频繁的线程上下文切换,亦或是糟糕的垃圾回收策略,都将导致cpu被大量占用,使得应用程序无法获取到足够的cpu资源,从而响应缓慢,性能变差。内存:内存的读写速度非常快,往往不是性能的瓶颈,但是内存相对来说容量有限切价格昂贵,如果应用大量分配内存而不及时回收,就会造成内存溢出或泄漏,应用无法分配新的内存,便无法正常运行,这将导致很严重的事故。带宽:对于偏网络I/O型的应用,例如网关服务,带宽的大小也决定了应用的性能好坏,如果带宽太小,当系统遇到大量并发请求时,带宽不够用,网络延迟就会变高,这个虽然对服务端可能无感知,但是对客户端则是影响甚大。磁盘:相对内存来说,磁盘价格低廉,容量很大,但是读写速度较慢,如果应用频繁的进行磁盘I/O,那性能可想而知也不会太好。以上这些都是系统资源层面用于衡量性能的指标,除此之外还有应用本身的稳定性指标:异常率:也叫错误率,一般分两种,执行超时和应用panic。panic会导致应用不可用,虽然服务通常都会配置相应的重启机制,确保偶然的应用挂掉后能重启再次提供服务,但是经常性的panic,会导致应用频繁的重启,减少了应用正常提供服务的时间,整体性能也就变差了。异常率是非常重要的指标,服务的稳定和可用是一切的前提,如果服务都不可用了,还谈何性能优化。响应时间(RT):包括平均响应时间,百分位(top percentile)响应时间。响应时间是指应用从收到请求到返回结果后的耗时,反应的是应用处理请求的快慢。通常平均响应时间无法反应服务的整体响应情况,响应慢的请求会被响应快的请求平均掉,而响应慢的请求往往会给用户带来糟糕的体验,即所谓的长尾请求,所以我们需要百分位响应时间,例如tp99响应时间,即99%的请求都会在这个时间内返回。吞吐量:主要指应用在一定时间内处理请求/事务的数量,反应的是应用的负载能力。我们当然希望在应用稳定的情况下,能承接的流量越大越好,主要指标包括QPS(每秒处理请求数)和QPM(每分钟处理请求数)。1.2 制定优化方案明确了性能指标以后,我们就可以评估一个应用的性能好坏,同时也能发现其中的短板并对其进行优化。但是做性能优化,有几个点需要提前注意:第一,不要反向优化。比如我们的应用整体占用内存资源较少,但是rt偏高,那我们就针对rt做优化,优化完后,rt下降了30%,但是cpu使用率上升了50%,导致一台机器负载能力下降30%,这便是反向优化。性能优化要从整体考虑,尽量在优化一个方面时,不影响其他方面,或是其他方面略微下降。第二,不要过度优化。如果应用性能已经很好了,优化的空间很小,比如rt的tp99在2ms内,继续尝试优化可能投入产出比就很低了,不如将这些精力放在其他需要优化的地方上。由此可见,在优化之前,明确想要优化的指标,并制定合理的优化方案是很重要的。常见的优化方案有以下几种:优化代码有经验的程序员在编写代码时,会时刻注意减少代码中不必要的性能消耗,比如使用strconv而不是fmt.Sprint进行数字到字符串的转化,在初始化map或slice时指定合理的容量以减少内存分配等。良好的编程习惯不仅能使应用性能良好,同时也能减少故障发生的几率。总结下来,常用的代码优化方向有以下几种:提高复用性,将通用的代码抽象出来,减少重复开发。池化,对象可以池化,减少内存分配;协程可以池化,避免无限制创建协程打满内存。并行化,在合理创建协程数量的前提下,把互不依赖的部分并行处理,减少整体的耗时。异步化,把不需要关心实时结果的请求,用异步的方式处理,不用一直等待结果返回。算法优化,使用时间复杂度更低的算法。使用设计模式设计模式是对代码组织形式的抽象和总结,代码的结构对应用的性能有着重要的影响,结构清晰,层次分明的代码不仅可读性好,扩展性高,还能避免许多潜在的性能问题,帮助开发人员快速找到性能瓶颈,进行专项优化,为服务的稳定性提供保障。常见的对性能有所提升的设计模式例如单例模式,我们可以在应用启动时将需要的外部依赖服务用单例模式先初始化,避免创建太多重复的连接。空间换时间或时间换空间在优化的前期,可能一个小的优化就能达到很好的效果。但是优化的尽头,往往要面临抉择,鱼和熊掌不可兼得。性能优秀的应用往往是多项资源的综合利用最优。为了达到综合平衡,在某些场景下,就需要做出一些调整和牺牲,常用的方法就是空间换时间或时间换空间。比如在响应时间优先的场景下,把需要耗费大量计算时间或是网络i/o时间的中间结果缓存起来,以提升后续相似请求的响应速度,便是空间换时间的一种体现。使用更好的三方库在我们的应用中往往会用到很多开源的第三方库,目前在github上的go开源项目就有173万+。有很多go官方库的性能表现并不佳,比如go官方的日志库性能就一般,下面是zap发布的基准测试信息(记录一条消息和10个字段的性能表现)。PackageTimeTime % to zapObjects Allocated⚡️ zap862 ns/op+0%5 allocs/op⚡️ zap (sugared)1250 ns/op+45%11 allocs/opzerolog4021 ns/op+366%76 allocs/opgo-kit4542 ns/op+427%105 allocs/opapex/log26785 ns/op+3007%115 allocs/oplogrus29501 ns/op+3322%125 allocs/oplog1529906 ns/op+3369%122 allocs/op从上面可以看出zap的性能比同类结构化日志包更好,也比标准库更快,那我们就可以选择更好的三方库。2. 性能调优-工具篇当我们找到应用的性能短板,并针对短板制定相应优化方案,最后按照方案对代码进行优化之后,我们怎么知道优化是有效的呢?直接将代码上线,观察性能指标的变化,风险太大了。此时我们需要有好用的性能分析工具,帮助我们检验优化的效果,下面将为大家介绍几款go语言中性能分析的利器。2.1 benchmarkGo语言标准库内置的 testing 测试框架提供了基准测试(benchmark)的能力,benchmark可以帮助我们评估代码的性能表现,主要方式是通过在一定时间(默认1秒)内重复运行测试代码,然后输出执行次数和内存分配结果。下面我们用一个简单的例子来验证一下,strconv是否真的比fmt.Sprint快。首先我们来编写一段基准测试的代码,如下:package main import ( "fmt" "strconv" "testing" ) func BenchmarkStrconv(b *testing.B) { for n := 0; n < b.N; n++ { strconv.Itoa(n) } } func BenchmarkFmtSprint(b *testing.B) { for n := 0; n < b.N; n++ { fmt.Sprint(n) } }我们可以用命令行go test -bench . 来运行基准测试,输出结果如下:goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 41988014 27.41 ns/op BenchmarkFmtSprint-12 13738172 81.19 ns/op ok main 7.039s可以看到strconv每次执行只用了27.41纳秒,而fmt.Sprint则是81.19纳秒,strconv的性能是fmt.Sprint的三倍,那为什么strconv要更快呢?会不会是这次运行时间太短呢?为了公平起见,我们决定让他们再比赛一轮,这次我们延长比赛时间,看看结果如何。通过go test -bench . -benchtime=5s 命令,我们可以把测试时间延长到5秒,结果如下:goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 211533207 31.60 ns/op BenchmarkFmtSprint-12 69481287 89.58 ns/op PASS ok main 18.891s结果有些变化,strconv每次执行的时间上涨了4ns,但变化不大,差距仍有2.9倍。但是我们仍然不死心,我们决定让他们一次跑三轮,每轮5秒,三局两胜。通过go test -bench . -benchtime=5s -count=3 命令,我们可以把测试进行3轮,结果如下:goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 217894554 31.76 ns/op BenchmarkStrconv-12 217140132 31.45 ns/op BenchmarkStrconv-12 219136828 31.79 ns/op BenchmarkFmtSprint-12 70683580 89.53 ns/op BenchmarkFmtSprint-12 63881758 82.51 ns/op BenchmarkFmtSprint-12 64984329 82.04 ns/op PASS ok main 54.296s结果变化也不大,看来strconv是真的比fmt.Sprint快很多。那快是快,会不会内存分配上情况就相反呢?通过go test -bench . -benchmem 这个命令我们可以看到两个方法的内存分配情况,结果如下:goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 43700922 27.46 ns/op 7 B/op 0 allocs/op BenchmarkFmtSprint-12 143412 80.88 ns/op 16 B/op 2 allocs/op PASS ok main 7.031s可以看到strconv在内存分配上是0次,每次运行使用的内存是7字节,只是fmt.Sprint的43.8%,简直是全方面的优于fmt.Sprint啊。那究竟是为什么strconv比fmt.Sprint好这么多呢?通过查看strconv的代码,我们发现,对于小于100的数字,strconv是直接通过digits和smallsString这两个常量进行转换的,而大于等于100的数字,则是通过不断除以100取余,然后再找到余数对应的字符串,把这些余数的结果拼起来进行转换的。const digits = "0123456789abcdefghijklmnopqrstuvwxyz" const smallsString = "00010203040506070809" + "10111213141516171819" + "20212223242526272829" + "30313233343536373839" + "40414243444546474849" + "50515253545556575859" + "60616263646566676869" + "70717273747576777879" + "80818283848586878889" + "90919293949596979899" // small returns the string for an i with 0 <= i < nSmalls. func small(i int) string { if i < 10 { return digits[i : i+1] } return smallsString[i*2 : i*2+2] } func formatBits(dst []byte, u uint64, base int, neg, append_ bool) (d []byte, s string) { ... for j := 4; j > 0; j-- { is := us % 100 * 2 us /= 100 i -= 2 a[i+1] = smallsString[is+1] a[i+0] = smallsString[is+0] } ... }而fmt.Sprint则是通过反射来实现这一目的的,fmt.Sprint得先判断入参的类型,在知道参数是int型后,再调用fmt.fmtInteger方法把int转换成string,这多出来的步骤肯定没有直接把int转成string来的高效。// fmtInteger formats signed and unsigned integers. func (f *fmt) fmtInteger(u uint64, base int, isSigned bool, verb rune, digits string) { ... switch base { case 10: for u >= 10 { i-- next := u / 10 buf[i] = byte('0' + u - next*10) u = next } ... }benchmark还有很多实用的函数,比如ResetTimer可以重置启动时耗费的准备时间,StopTimer和StartTimer则可以暂停和启动计时,让测试结果更集中在核心逻辑上。2.2 pprof2.2.1 使用介绍pprof是go语言官方提供的profile工具,支持可视化查看性能报告,功能十分强大。pprof基于定时器(10ms/次)对运行的go程序进行采样,搜集程序运行时的堆栈信息,包括CPU时间、内存分配等,最终生成性能报告。pprof有两个标准库,使用的场景不同:runtime/pprof 通过在代码中显式的增加触发和结束埋点来收集指定代码块运行时数据生成性能报告。net/http/pprof 是对runtime/pprof的二次封装,基于web服务运行,通过访问链接触发,采集服务运行时的数据生成性能报告。runtime/pprof的使用方法如下:package main import ( "os" "runtime/pprof" "time" ) func main() { w, _ := os.OpenFile("test_cpu", os.O_RDWR | os.O_CREATE | os.O_APPEND, 0644) pprof.StartCPUProfile(w) time.Sleep(time.Second) pprof.StopCPUProfile() }我们也可以使用另外一种方法,net/http/pprof:package main import ( "net/http" _ "net/http/pprof" ) func main() { err := http.ListenAndServe(":6060", nil) if err != nil { panic(err) } }将程序run起来后,我们通过访问http://127.0.0.1:6060/debug/pprof/就可以看到如下页面:点击profile就可以下载cpu profile文件。那我们如何查看我们的性能报告呢? pprof支持两种查看模式,终端和web界面,注意: 想要查看可视化界面需要提前安装graphviz。这里我们以web界面为例,在终端内我们输入如下命令:go tool pprof -http :6060 test_cpu就会在浏览器里打开一个页面,内容如下:从界面左上方VIEW栏下,我们可以看到,pprof支持Flame Graph,dot Graph和Top等多种视图,下面我们将一一介绍如何阅读这些视图。2.2.1 火焰图 Flame Graph如何阅读首先,推荐直接阅读火焰图,在查函数耗时场景,这个比较直观;最简单的:横条越长,资源消耗、占用越多; 注意:每一个function 的横条虽然很长,但可能是他的下层“子调用”耗时产生的,所以一定要关注“下一层子调用”各自的耗时分布;每个横条支持点击下钻能力,可以更详细的分析子层的耗时占比。2.2.2 dot Graph 图如何阅读英文原文在这里:https://github.com/google/pprof/blob/master/doc/README.md节点颜色:红色表示耗时多的节点;绿色表示耗时少的节点;灰色表示耗时几乎可以忽略不计(接近零);节点字体大小 :字体越大,表示占“上层函数调用”比例越大;(其实上层函数自身也有耗时,没包含在此)字体越小,表示占“上层函数调用”比例越小;线条(边)粗细:线条越粗,表示消耗了更多的资源;反之,则越少;线条(边)颜色:颜色越红,表示性能消耗占比越高;颜色越绿,表示性能消耗占比越低;灰色,表示性能消耗几乎可以忽略不计;虚线:表示中间有一些节点被“移除”或者忽略了;(一般是因为耗时较少所以忽略了) 实线:表示节点之间直接调用 内联边标记:被调用函数已经被内联到调用函数中(对于一些代码行比较少的函数,编译器倾向于将它们在编译期展开从而消除函数调用,这种行为就是内联。)2.2.3 TOP 表如何阅读flat:当前函数,运行耗时(不包含内部调用其他函数的耗时)flat%:当前函数,占用的 CPU 运行耗时总比例(不包含外部调用函数)sum%:当前行的flat%与上面所有行的flat%总和。cum:当前函数加上它内部的调用的运行总耗时(包含内部调用其他函数的耗时)cum%:同上的 CPU 运行耗时总比例2.3 tracepprof已经有了对内存和CPU的分析能力,那trace工具有什么不同呢?虽然pprof的CPU分析器,可以告诉你什么函数占用了最多的CPU时间,但它并不能帮助你定位到是什么阻止了goroutine运行,或者在可用的OS线程上如何调度goroutines。这正是trace真正起作用的地方。我们需要更多关于Go应用中各个goroutine的执行情况的更为详细的信息,可以从P(goroutine调度器概念中的processor)和G(goroutine调度器概念中的goroutine)的视角完整的看到每个P和每个G在Tracer开启期间的全部“所作所为”,对Tracer输出数据中的每个P和G的行为分析并结合详细的event数据来辅助问题诊断的。Tracer可以帮助我们记录的详细事件包含有:与goroutine调度有关的事件信息:goroutine的创建、启动和结束;goroutine在同步原语(包括mutex、channel收发操作)上的阻塞与解锁。与网络有关的事件:goroutine在网络I/O上的阻塞和解锁;与系统调用有关的事件:goroutine进入系统调用与从系统调用返回;与垃圾回收器有关的事件:GC的开始/停止,并发标记、清扫的开始/停止。Tracer主要也是用于辅助诊断这三个场景下的具体问题的:并行执行程度不足的问题:比如没有充分利用多核资源等;因GC导致的延迟较大的问题;Goroutine执行情况分析,尝试发现goroutine因各种阻塞(锁竞争、系统调用、调度、辅助GC)而导致的有效运行时间较短或延迟的问题。2.3.1 trace性能报告打开trace性能报告,首页信息包含了多维度数据,如下图:View trace:以图形页面的形式渲染和展示tracer的数据,这也是我们最为关注/最常用的功能Goroutine analysis:以表的形式记录执行同一个函数的多个goroutine的各项trace数据Network blocking profile:用pprof profile形式的调用关系图展示网络I/O阻塞的情况Synchronization blocking profile:用pprof profile形式的调用关系图展示同步阻塞耗时情况Syscall blocking profile:用pprof profile形式的调用关系图展示系统调用阻塞耗时情况Scheduler latency profile:用pprof profile形式的调用关系图展示调度器延迟情况User-defined tasks和User-defined regions:用户自定义trace的task和regionMinimum mutator utilization:分析GC对应用延迟和吞吐影响情况的曲线图通常我们最为关注的是View trace和Goroutine analysis,下面将详细说说这两项的用法。2.3.2 view trace如果Tracer跟踪时间较长,trace会将View trace按时间段进行划分,避免触碰到trace-viewer的限制:View trace使用快捷键来缩放时间线标尺:w键用于放大(从秒向纳秒缩放),s键用于缩小标尺(从纳秒向秒缩放)。我们同样可以通过快捷键在时间线上左右移动:s键用于左移,d键用于右移。(游戏快捷键WASD)采样状态这个区内展示了三个指标:Goroutines、Heap和Threads,某个时间点上的这三个指标的数据是这个时间点上的状态快照采样:Goroutines:某一时间点上应用中启动的goroutine的数量,当我们点击某个时间点上的goroutines采样状态区域时(我们可以用快捷键m来准确标记出那个时间点),事件详情区会显示当前的goroutines指标采样状态:Heap指标则显示了某个时间点上Go应用heap分配情况(包括已经分配的Allocated和下一次GC的目标值NextGC):Threads指标显示了某个时间点上Go应用启动的线程数量情况,事件详情区将显示处于InSyscall(整阻塞在系统调用上)和Running两个状态的线程数量情况:P视角区这里将View trace视图中最大的一块区域称为“P视角区”。这是因为在这个区域,我们能看到Go应用中每个P(Goroutine调度概念中的P)上发生的所有事件,包括:EventProcStart、EventProcStop、EventGoStart、EventGoStop、EventGoPreempt、Goroutine辅助GC的各种事件以及Goroutine的GC阻塞(STW)、系统调用阻塞、网络阻塞以及同步原语阻塞(mutex)等事件。除了每个P上发生的事件,我们还可以看到以单独行显示的GC过程中的所有事件。事件详情区点选某个事件后,关于该事件的详细信息便会在这个区域显示出来,事件详情区可以看到关于该事件的详细信息:Title:事件的可读名称;Start:事件的开始时间,相对于时间线上的起始时间;Wall Duration:这个事件的持续时间,这里表示的是G1在P4上此次持续执行的时间;Start Stack Trace:当P4开始执行G1时G1的调用栈;End Stack Trace:当P4结束执行G1时G1的调用栈;从上面End Stack Trace栈顶的函数为runtime.asyncPreempt来看,该Goroutine G1是被强行抢占了,这样P4才结束了其运行;Incoming flow:触发P4执行G1的事件;Outgoing flow:触发G1结束在P4上执行的事件;Preceding events:与G1这个goroutine相关的之前的所有的事件;Follwing events:与G1这个goroutine相关的之后的所有的事件All connected:与G1这个goroutine相关的所有事件。2.3.3 Goroutine analysisGoroutine analysis提供了从G视角看Go应用执行的图景。与View trace不同,这次页面中最广阔的区域提供的G视角视图,而不再是P视角视图。在这个视图中,每个G都会对应一个单独的条带(和P视角视图一样,每个条带都有两行),通过这一条带可以按时间线看到这个G的全部执行情况。通常仅需在goroutine analysis的表格页面找出执行最快和最慢的两个goroutine,在Go视角视图中沿着时间线对它们进行对比,以试图找出执行慢的goroutine究竟出了什么问题。2.4 后记虽然pprof和trace有着非常强大的profile能力,但在使用过程中,仍存在以下痛点:获取性能报告麻烦:一般大家做压测,为了更接近真实环境性能态,都使用生产环境/pre环境进行。而出于安全考虑,生产环境内网一般和PC办公内网是隔离不通的,需要单独配置通路才可以获得生产环境内网的profile 文件下载到PC办公电脑中,这也有一些额外的成本;查看profile分析报告麻烦:之前大家在本地查看profile 分析报告,一般 go tool pprof -http=":8083" profile 命令在本地PC开启一个web service 查看,并且需要至少安装graphviz 等库。查看trace分析同样麻烦:查看go trace 的profile 信息来分析routine 锁和生命周期时,也需要类似的方式在本地PC执行命令 go tool trace mytrace.profile 分享麻烦:如果我想把自己压测的性能结果内容,分享个另一位同学,那只能把1中获取的性能报告“profile文件”通过钉钉发给被分享人。然而有时候本地profile文件比较多,一不小心就发错了,还不如截图,但是截图又没有了交互放大、缩小、下钻等能力。处处不给力!留存复盘麻烦:系统的性能分析就像一份病历,每每看到阶段性的压测报告,总结或者对照时,不禁要询问,做过了哪些优化和改造,病因病灶是什么,有没有共性,值不值得总结归纳,现在是不是又面临相似的性能问题?那么能不能开发一个平台工具,解决以上的这些痛点呢?目前在阿里集团内部,高德的研发同学已经通过对go官方库的定制开发,实现了go语言性能平台,解决了以上这些痛点,并在内部进行了开源。该平台已面向阿里集团,累计实现性能场景快照数万条的获取和分析,解决了很多的线上服务性能调试和优化问题,这里暂时不展开,后续有机会可以单独分享。3. 性能调优-技巧篇除了前面提到的尽量用strconv而不是fmt.Sprint进行数字到字符串的转化以外,我们还将介绍一些在实际开发中经常会用到的技巧,供各位参考。3.1 字符串拼接拼接字符串为了书写方便快捷,最常用的两个方法是运算符 + 和 fmt.Sprintf()运算符 + 只能简单地完成字符串之间的拼接,fmt.Sprintf() 其底层实现使用了反射,性能上会有所损耗。从性能出发,兼顾易用可读,如果待拼接的变量不涉及类型转换且数量较少(<=5),拼接字符串推荐使用运算符 +,反之使用 fmt.Sprintf()。// 推荐:用+进行字符串拼接 func BenchmarkPlus(b *testing.B) { for i := 0; i < b.N; i++ { s := "a" + "b" _ = s } } // 不推荐:用fmt.Sprintf进行字符串拼接 func BenchmarkFmt(b *testing.B) { for i := 0; i < b.N; i++ { s := fmt.Sprintf("%s%s", "a", "b") _ = s } } goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkPlus-12 1000000000 0.2658 ns/op 0 B/op 0 allocs/op BenchmarkFmt-12 16559949 70.83 ns/op 2 B/op 1 allocs/op PASS ok main 5.908s3.2 提前指定容器容量在初始化slice时,尽量指定容量,这是因为当添加元素时,如果容量的不足,slice会重新申请一个更大容量的容器,然后把原来的元素复制到新的容器中。// 推荐:初始化时指定容量 func BenchmarkGenerateWithCap(b *testing.B) { nums := make([]int, 0, 10000) for n := 0; n < b.N; n++ { for i:=0; i < 10000; i++ { nums = append(nums, i) } } } // 不推荐:初始化时不指定容量 func BenchmarkGenerate(b *testing.B) { nums := make([]int, 0) for n := 0; n < b.N; n++ { for i:=0; i < 10000; i++ { nums = append(nums, i) } } } goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkGenerateWithCap-12 23508 336485 ns/op 476667 B/op 0 allocs/op BenchmarkGenerate-12 22620 68747 ns/op 426141 B/op 0 allocs/op PASS ok main 16.628s3.3 遍历 []struct{} 使用下标而不是 range常用的遍历方式有两种,一种是for循环下标遍历,一种是for循环range遍历,这两种遍历在性能上是否有差异呢?让我们来一探究竟。针对[]int,我们来看看两种遍历有和差别吧func getIntSlice() []int { nums := make([]int, 1024, 1024) for i := 0; i < 1024; i++ { nums[i] = i } return nums } // 用下标遍历[]int func BenchmarkIndexIntSlice(b *testing.B) { nums := getIntSlice() b.ResetTimer() for i := 0; i < b.N; i++ { var tmp int for k := 0; k < len(nums); k++ { tmp = nums[k] } _ = tmp } } // 用range遍历[]int元素 func BenchmarkRangeIntSlice(b *testing.B) { nums := getIntSlice() b.ResetTimer() for i := 0; i < b.N; i++ { var tmp int for _, num := range nums { tmp = num } _ = tmp } } goos: darwin goarch: amd64 pkg: demo/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkIndexIntSlice-12 3923230 270.2 ns/op 0 B/op 0 allocs/op BenchmarkRangeIntSlice-12 4518495 287.8 ns/op 0 B/op 0 allocs/op PASS ok demo/test 3.303s可以看到,在遍历[]int时,两种方式并无差别。我们再看看遍历[]struct{}的情况type Item struct { id int val [1024]byte } // 推荐:用下标遍历[]struct{} func BenchmarkIndexStructSlice(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { var tmp int for j := 0; j < len(items); j++ { tmp = items[j].id } _ = tmp } } // 推荐:用range的下标遍历[]struct{} func BenchmarkRangeIndexStructSlice(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { var tmp int for k := range items { tmp = items[k].id } _ = tmp } } // 不推荐:用range遍历[]struct{}的元素 func BenchmarkRangeStructSlice(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { var tmp int for _, item := range items { tmp = item.id } _ = tmp } } goos: darwin goarch: amd64 pkg: demo/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkIndexStructSlice-12 4413182 266.7 ns/op 0 B/op 0 allocs/op BenchmarkRangeIndexStructSlice-12 4545476 269.4 ns/op 0 B/op 0 allocs/op BenchmarkRangeStructSlice-12 33300 35444 ns/op 0 B/op 0 allocs/op PASS ok demo/test 5.282s可以看到,用for循环下标的方式性能都差不多,但是用range遍历数组里的元素时,性能则相差很多,前面两种方法是第三种方法的130多倍。主要原因是通过for k, v := range获取到的元素v实际上是原始值的一个拷贝。所以在面对复杂的struct进行遍历的时候,推荐使用下标。但是当遍历对象是复杂结构体的指针([]*struct{})时,用下标还是用range迭代元素的性能就差不多了。3.4 利用unsafe包避开内存copyunsafe包提供了任何类型的指针和 unsafe.Pointer 的相互转换及uintptr 类型和 unsafe.Pointer 可以相互转换,如下图unsafe包指针转换关系依据上述转换关系,其实除了string和[]byte的转换,也可以用于slice、map等的求长度及一些结构体的偏移量获取等,但是这种黑科技在一些情况下会带来一些匪夷所思的诡异问题,官方也不建议用,所以还是慎用,除非你确实很理解各种机制了,这里给出项目中实际用到的常规string和[]byte之间的转换,如下:func Str2bytes(s string) []byte { x := (*[2]uintptr)(unsafe.Pointer(&s)) h := [3]uintptr{x[0], x[1], x[1]} return *(*[]byte)(unsafe.Pointer(&h)) } func Bytes2str(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } 我们通过benchmark来验证一下是否性能更优:// 推荐:用unsafe.Pointer实现string到bytes func BenchmarkStr2bytes(b *testing.B) { s := "testString" var bs []byte for n := 0; n < b.N; n++ { bs = Str2bytes(s) } _ = bs } // 不推荐:用类型转换实现string到bytes func BenchmarkStr2bytes2(b *testing.B) { s := "testString" var bs []byte for n := 0; n < b.N; n++ { bs = []byte(s) } _ = bs } // 推荐:用unsafe.Pointer实现bytes到string func BenchmarkBytes2str(b *testing.B) { bs := Str2bytes("testString") var s string b.ResetTimer() for n := 0; n < b.N; n++ { s = Bytes2str(bs) } _ = s } // 不推荐:用类型转换实现bytes到string func BenchmarkBytes2str2(b *testing.B) { bs := Str2bytes("testString") var s string b.ResetTimer() for n := 0; n < b.N; n++ { s = string(bs) } _ = s } goos: darwin goarch: amd64 pkg: demo/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStr2bytes-12 1000000000 0.2938 ns/op 0 B/op 0 allocs/op BenchmarkStr2bytes2-12 38193139 28.39 ns/op 16 B/op 1 allocs/op BenchmarkBytes2str-12 1000000000 0.2552 ns/op 0 B/op 0 allocs/op BenchmarkBytes2str2-12 60836140 19.60 ns/op 16 B/op 1 allocs/op PASS ok demo/test 3.301s可以看到使用unsafe.Pointer比强制类型转换性能是要高不少的,从内存分配上也可以看到完全没有新的内存被分配。3.5 协程池go语言最大的特色就是很容易的创建协程,同时go语言的协程调度策略也让go程序可以最大化的利用cpu资源,减少线程切换。但是无限度的创建goroutine,仍然会带来问题。我们知道,一个go协程占用内存大小在2KB左右,无限度的创建协程除了会占用大量的内存空间,同时协程的切换也有不少开销,一次协程切换大概需要100ns,虽然相较于线程毫秒级的切换要优秀很多,但依然存在开销,而且这些协程最后还是需要GC来回收,过多的创建协程,对GC也是很大的压力。所以我们在使用协程时,可以通过协程池来限制goroutine数量,避免无限制的增长。限制协程的方式有很多,比如可以用channel来限制:var wg sync.WaitGroup ch := make(chan struct{}, 3) for i := 0; i < 10; i++ { ch <- struct{}{} wg.Add(1) go func(i int) { defer wg.Done() log.Println(i) time.Sleep(time.Second) <-ch }(i) } wg.Wait()这里通过限制channel长度为3,可以实现最多只有3个协程被创建的效果。当然也可以使用@烟渺实现的errgoup。使用方法如下:func Test_ErrGroupRun(t *testing.T) { errgroup := WithTimeout(nil, 10*time.Second) errgroup.SetMaxProcs(4) for index := 0; index < 10; index++ { errgroup.Run(nil, index, "test", func(context *gin.Context, i interface{}) (interface{}, error) { t.Logf("[%s]input:%+v, time:%s", "test", i, time.Now().Format("2006-01-02 15:04:05")) time.Sleep(2*time.Second) return i, nil }) } errgroup.Wait() }输出结果如下:=== RUN Test_ErrGroupRun errgroup_test.go:23: [test]input:0, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:3, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:1, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:2, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:4, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:5, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:6, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:7, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:8, time:2022-12-04 17:31:33 errgroup_test.go:23: [test]input:9, time:2022-12-04 17:31:33 --- PASS: Test_ErrGroupRun (6.00s) PASSerrgroup可以通过SetMaxProcs设定协程池的大小,从上面的结果可以看到,最多就4个协程在运行。3.6 sync.Pool 对象复用我们在代码中经常会用到json进行序列化和反序列化,举一个投放活动的例子,一个投放活动会有许多字段会转换为字节数组。type ActTask struct { Id int64 `ddb:"id"` // 主键id Status common.Status `ddb:"status"` // 状态 0=初始 1=生效 2=失效 3=过期 BizProd common.BizProd `ddb:"biz_prod"` // 业务类型 Name string `ddb:"name"` // 活动名 Adcode string `ddb:"adcode"` // 城市 RealTimeRuleByte []byte `ddb:"realtime_rule"` // 实时规则json ... } type RealTimeRuleStruct struct { Filter []*struct { PropertyId int64 `json:"property_id"` PropertyCode string `json:"property_code"` Operator string `json:"operator"` Value []string `json:"value"` } `json:"filter"` ExtData [1024]byte `json:"ext_data"` } func (at *ActTask) RealTimeRule() *form.RealTimeRule { if err := json.Unmarshal(at.RealTimeRuleByte, &at.RealTimeRuleStruct); err != nil { return nil } return at.RealTimeRuleStruct }以这里的实时投放规则为例,我们会将过滤规则反序列化为字节数组。每次json.Unmarshal都会申请一个临时的结构体对象,而这些对象都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。对于需要频繁创建并回收的对象,我们可以使用对象池来提升性能。sync.Pool可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。sync.Pool的使用方法很简单,只需要实现 New 函数即可。对象池中没有对象时,将会调用 New 函数创建。var realTimeRulePool = sync.Pool{ New: func() interface{} { return new(RealTimeRuleStruct) }, }然后调用 Pool 的 Get() 和 Put() 方法来获取和放回池子中。rule := realTimeRulePool.Get().(*RealTimeRuleStruct) json.Unmarshal(buf, rule) realTimeRulePool.Put(rule)Get() 用于从对象池中获取对象,因为返回值是 interface{},因此需要类型转换。Put() 则是在对象使用完毕后,放回到对象池。接下来我们进行性能测试,看看性能如何var realTimeRule = []byte("{\\\"filter\\\":[{\\\"property_id\\\":2,\\\"property_code\\\":\\\"search_poiid_industry\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"yimei\\\"]},{\\\"property_id\\\":4,\\\"property_code\\\":\\\"request_page_id\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"all\\\"]}],\\\"white_list\\\":[{\\\"property_id\\\":1,\\\"property_code\\\":\\\"white_list_for_adiu\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"j838ef77bf227chcl89888f3fb0946\\\",\\\"lb89bea9af558589i55559764bc83e\\\"]}],\\\"ipc_user_tag\\\":[{\\\"property_id\\\":1,\\\"property_code\\\":\\\"ipc_crowd_tag\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"test_20227041152_mix_ipc_tag\\\"]}],\\\"relation_id\\\":0,\\\"is_copy\\\":true}") // 推荐:复用一个对象,不用每次都生成新的 func BenchmarkUnmarshalWithPool(b *testing.B) { for n := 0; n < b.N; n++ { task := realTimeRulePool.Get().(*RealTimeRuleStruct) json.Unmarshal(realTimeRule, task) realTimeRulePool.Put(task) } } // 不推荐:每次都会生成一个新的临时对象 func BenchmarkUnmarshal(b *testing.B) { for n := 0; n < b.N; n++ { task := &RealTimeRuleStruct{} json.Unmarshal(realTimeRule, task) } } goos: darwin goarch: amd64 pkg: demo/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkUnmarshalWithPool-12 3627546 319.4 ns/op 312 B/op 7 allocs/op BenchmarkUnmarshal-12 2342208 490.8 ns/op 1464 B/op 8 allocs/op PASS ok demo/test 3.525s可以看到,两种方法在时间消耗上差不太多,但是在内存分配上差距明显,使用sync.Pool后内存占用仅为不使用的1/5。3.7 避免系统调用系统调用是一个很耗时的操作,在各种语言中都是,go也不例外,在go的GPM模型中,异步系统调用G会和MP分离,同步系统调用GM会和P分离,不管何种形式除了状态切换及内核态中执行操作耗时外,调度器本身的调度也耗时。所以在可以避免系统调用的地方尽量去避免// 推荐:不使用系统调用 func BenchmarkNoSytemcall(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { if configs.PUBLIC_KEY != nil { } } }) } // 不推荐:使用系统调用 func BenchmarkSytemcall(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { if os.Getenv("PUBLIC_KEY") != "" { } } }) } goos: darwin goarch: amd64 pkg: demo/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkNoSytemcall-12 1000000000 0.1495 ns/op 0 B/op 0 allocs/op BenchmarkSytemcall-12 37224988 31.10 ns/op 0 B/op 0 allocs/op PASS ok demo/test 1.877s4. 性能调优-实战篇案例1: go协程创建数据库连接不释放导致内存暴涨应用背景感谢@路现提供的案例。遇到的问题及表象特征线上机器偶尔出现内存使用率超过百分之九十报警。分析思路及排查方向在报警触发时,通过直接拉取线上应用的profile文件,查看内存分配情况,我们看到内存分配主要产生在本地缓存的组件上。但是分析代码并没有发现存在内存泄露的情况,看着像是资源一直没有被释放,进一步分析goroutine的profile文件发现存在大量的goroutine未释放,表现在本地缓存击穿后回源数据库,对数据库的查询访问一直不释放。调优手段与效果最终通过排查,发现使用的数据库组件存在bug,在极端情况下会出现死锁的情况,导致数据库访问请求无法返回也无法释放。最终bug修复后升级数据库组件版本解决了问题。案例2: 优惠索引内存分配大,gc 耗时高应用背景感谢@梅东提供的案例。遇到的问题及表象特征接口tp99高,偶尔会有一些特别耗时的请求,导致用户的优惠信息展示不出来分析思路及排查方向通过直接在平台上抓包观察,我们发现使用的分配索引这个方法占用的堆内存特别高,通过 top 可以看到是排在第一位的我们分析代码,可以看到,获取城市索引的地方,每次都是重新申请了内存的,通过改动为返回指针,就不需要每次都单独申请内存了,核心代码改动:调优手段与效果修改后,上线观察,可以看到使用中的内存以及gc耗时都有了明显降低案例3:流量上涨导致cpu异常飙升应用背景感谢@君度提供的案例。遇到的问题及表象特征能量站v2接口和task-home-page接口流量较大时,会造成ab实验策略匹配时cpu飙升分析思路及排查方向调优手段与效果主要优化点如下:1、优化toEntity方法,简化为单独的ID()方法2、优化数组、map初始化结构3、优化adCode转换为string过程4、关闭过多的match log打印优化后profile:优化上线前后CPU的对比案例4: 内存对象未释放导致内存泄漏应用背景感谢@淳深提供的案例,提供案例的服务,日常流量峰值在百万qps左右,是高德内部十分重要的服务。此前该服务是由java实现的,后来用go语言进行重构,在重构完成切全量后,有许多性能优化的优秀案例,这里选取内存泄漏相关的一个案例分享给大家,希望对大家在自己服务进行内存泄漏问题排查时能提供参考和帮助。遇到的问题及表象特征go语言版本全量切流后,每天会对服务各项指标进行详细review,发现每日内存上涨约0.4%,如下图在go版本服务切全量前,从第一张图可以看到整个内存使用是平稳的,无上涨趋势,但是切go版本后,从第二张图可以看到,整个内存曲线呈上升趋势,遂认定内存泄漏,开始排查内存泄漏的“罪魁祸首”。分析思路及排查方向我们先到线上机器抓取当前时间的heap文件,间隔一天后再次抓取heap文件,通过pprof diff对比,我们发现time.NewTicker的内存占用增长了几十MB(由于未保留当时的heap文件,此处没有截图),通过调用栈信息,我们找到了问题的源头,来自中间件vipserver client的SrvHost方法,通过深扒vipserver client代码,我们发现,每个vipserver域名都会有一个对应的协程,这个协程每隔三秒钟就会新建一个ticker对象,且用过的ticker对象没有stop,也就不会释放相应的内存资源。而这个time.NewTicker会创建一个timer对象,这个对象会占用72字节内存。在服务运行一天的情况下,进过计算,该对象累计会在内存中占用约35.6MB,和上述内存每日增长0.4%正好能对上,我们就能断定这个内存泄漏来自这里。调优手段与效果知道是timer对象重复创建的问题后,只需要修改这部分的代码就好了,最新的vipserver client修改了此处的逻辑,如下修改完后,运行一段时间,内存运行状态平稳,已无内存泄漏问题。结语目前go语言不仅在阿里集团内部,在整个互联网行业内也越来越流行,希望本文能为正在使用go语言的同学在性能优化方面带来一些参考价值。在阿里集团内部,高德也是最早规模化使用go语言的团队之一,目前高德线上运行的go服务已经达到近百个,整体qps已突破百万量级。在使用go语言的同时,高德也为集团内go语言生态建设做出了许多贡献,包括开发支持阿里集团常见的中间件(比如配置中心-Diamond、分布式RPC服务框架-HSF、服务发现-Vipserver、消息队列-MetaQ、流量控制-Sentinel、日志追踪-Eagleeye等)go语言版本,并被阿里中间件团队官方收录。但是go语言生态建设仍然有很长的道路要走,希望能有更多对go感兴趣的同学能够加入我们,一起参与阿里的go生态建设,乃至为互联网业界的go生态发展添砖加瓦。
文章
设计模式  ·  缓存  ·  Java  ·  中间件  ·  测试技术  ·  Go  ·  调度  ·  数据库  ·  索引  ·  容器
2023-03-03
机器学习算法(一): 基于逻辑回归的分类预测
机器学习算法(一): 基于逻辑回归的分类预测项目链接参考fork一下直接运行:https://www.heywhale.com/home/column/64141d6b1c8c8b518ba97dcc1 逻辑回归的介绍和应用1.1 逻辑回归的介绍逻辑回归(Logistic regression,简称LR)虽然其中带有"回归"两个字,但逻辑回归其实是一个分类模型,并且广泛应用于各个领域之中。虽然现在深度学习相对于这些传统方法更为火热,但实则这些传统方法由于其独特的优势依然广泛应用于各个领域中。而对于逻辑回归而且,最为突出的两点就是其模型简单和模型的可解释性强。逻辑回归模型的优劣势:优点:实现简单,易于理解和实现;计算代价不高,速度很快,存储资源低;缺点:容易欠拟合,分类精度可能不高1.1 逻辑回归的应用逻辑回归模型广泛用于各个领域,包括机器学习,大多数医学领域和社会科学。例如,最初由Boyd 等人开发的创伤和损伤严重度评分(TRISS)被广泛用于预测受伤患者的死亡率,使用逻辑回归 基于观察到的患者特征(年龄,性别,体重指数,各种血液检查的结果等)分析预测发生特定疾病(例如糖尿病,冠心病)的风险。逻辑回归模型也用于预测在给定的过程中,系统或产品的故障的可能性。还用于市场营销应用程序,例如预测客户购买产品或中止订购的倾向等。在经济学中它可以用来预测一个人选择进入劳动力市场的可能性,而商业应用则可以用来预测房主拖欠抵押贷款的可能性。条件随机字段是逻辑回归到顺序数据的扩展,用于自然语言处理。逻辑回归模型现在同样是很多分类算法的基础组件,比如 分类任务中基于GBDT算法+LR逻辑回归实现的信用卡交易反欺诈,CTR(点击通过率)预估等,其好处在于输出值自然地落在0到1之间,并且有概率意义。模型清晰,有对应的概率学理论基础。它拟合出来的参数就代表了每一个特征(feature)对结果的影响。也是一个理解数据的好工具。但同时由于其本质上是一个线性的分类器,所以不能应对较为复杂的数据情况。很多时候我们也会拿逻辑回归模型去做一些任务尝试的基线(基础水平)。说了这些逻辑回归的概念和应用,大家应该已经对其有所期待了吧,那么我们现在开始吧!!!2 学习目标了解 逻辑回归 的理论掌握 逻辑回归 的 sklearn 函数调用使用并将其运用到鸢尾花数据集预测3 代码流程Part1 Demo实践Step1:库函数导入Step2:模型训练Step3:模型参数查看Step4:数据和模型可视化Step5:模型预测Part2 基于鸢尾花(iris)数据集的逻辑回归分类实践Step1:库函数导入Step2:数据读取/载入Step3:数据信息简单查看Step4:可视化描述Step5:利用 逻辑回归模型 在二分类上 进行训练和预测Step5:利用 逻辑回归模型 在三分类(多分类)上 进行训练和预测4 算法实战### 4.1 Demo实践Step1:库函数导入## 基础函数库 import numpy as np ## 导入画图库 import matplotlib.pyplot as plt import seaborn as sns ## 导入逻辑回归模型函数 from sklearn.linear_model import LogisticRegressionStep2:模型训练##Demo演示LogisticRegression分类 ## 构造数据集 x_fearures = np.array([[-1, -2], [-2, -1], [-3, -2], [1, 3], [2, 1], [3, 2]]) y_label = np.array([0, 0, 0, 1, 1, 1]) ## 调用逻辑回归模型 lr_clf = LogisticRegression() ## 用逻辑回归模型拟合构造的数据集 lr_clf = lr_clf.fit(x_fearures, y_label) #其拟合方程为 y=w0+w1*x1+w2*x2Step3:模型参数查看## 查看其对应模型的w print('the weight of Logistic Regression:',lr_clf.coef_) ## 查看其对应模型的w0 print('the intercept(w0) of Logistic Regression:',lr_clf.intercept_) the weight of Logistic Regression: [[0.73455784 0.69539712]] the intercept(w0) of Logistic Regression: [-0.13139986]Step4:数据和模型可视化## 可视化构造的数据样本点 plt.figure() plt.scatter(x_fearures[:,0],x_fearures[:,1], c=y_label, s=50, cmap='viridis') plt.title('Dataset') plt.show()# 可视化决策边界 plt.figure() plt.scatter(x_fearures[:,0],x_fearures[:,1], c=y_label, s=50, cmap='viridis') plt.title('Dataset') nx, ny = 200, 100 x_min, x_max = plt.xlim() y_min, y_max = plt.ylim() x_grid, y_grid = np.meshgrid(np.linspace(x_min, x_max, nx),np.linspace(y_min, y_max, ny)) z_proba = lr_clf.predict_proba(np.c_[x_grid.ravel(), y_grid.ravel()]) z_proba = z_proba[:, 1].reshape(x_grid.shape) plt.contour(x_grid, y_grid, z_proba, [0.5], linewidths=2., colors='blue') plt.show()### 可视化预测新样本 plt.figure() ## new point 1 x_fearures_new1 = np.array([[0, -1]]) plt.scatter(x_fearures_new1[:,0],x_fearures_new1[:,1], s=50, cmap='viridis') plt.annotate(s='New point 1',xy=(0,-1),xytext=(-2,0),color='blue',arrowprops=dict(arrowstyle='-|>',connectionstyle='arc3',color='red')) ## new point 2 x_fearures_new2 = np.array([[1, 2]]) plt.scatter(x_fearures_new2[:,0],x_fearures_new2[:,1], s=50, cmap='viridis') plt.annotate(s='New point 2',xy=(1,2),xytext=(-1.5,2.5),color='red',arrowprops=dict(arrowstyle='-|>',connectionstyle='arc3',color='red')) ## 训练样本 plt.scatter(x_fearures[:,0],x_fearures[:,1], c=y_label, s=50, cmap='viridis') plt.title('Dataset') # 可视化决策边界 plt.contour(x_grid, y_grid, z_proba, [0.5], linewidths=2., colors='blue') plt.show()Step5:模型预测## 在训练集和测试集上分别利用训练好的模型进行预测 y_label_new1_predict = lr_clf.predict(x_fearures_new1) y_label_new2_predict = lr_clf.predict(x_fearures_new2) print('The New point 1 predict class:\n',y_label_new1_predict) print('The New point 2 predict class:\n',y_label_new2_predict) ## 由于逻辑回归模型是概率预测模型(前文介绍的 p = p(y=1|x,\theta)),所以我们可以利用 predict_proba 函数预测其概率 y_label_new1_predict_proba = lr_clf.predict_proba(x_fearures_new1) y_label_new2_predict_proba = lr_clf.predict_proba(x_fearures_new2) print('The New point 1 predict Probability of each class:\n',y_label_new1_predict_proba) print('The New point 2 predict Probability of each class:\n',y_label_new2_predict_proba) The New point 1 predict class: [0] The New point 2 predict class: [1] The New point 1 predict Probability of each class: [[0.69567724 0.30432276]] The New point 2 predict Probability of each class: [[0.11983936 0.88016064]]可以发现训练好的回归模型将X_new1预测为了类别0(判别面左下侧),X_new2预测为了类别1(判别面右上侧)。其训练得到的逻辑回归模型的概率为0.5的判别面为上图中蓝色的线。4.2 基于鸢尾花(iris)数据集的逻辑回归分类实践在实践的最开始,我们首先需要导入一些基础的函数库包括:numpy (Python进行科学计算的基础软件包),pandas(pandas是一种快速,强大,灵活且易于使用的开源数据分析和处理工具),matplotlib和seaborn绘图。Step1:库函数导入## 基础函数库 import numpy as np import pandas as pd ## 绘图函数库 import matplotlib.pyplot as plt import seaborn as sns本次我们选择鸢花数据(iris)进行方法的尝试训练,该数据集一共包含5个变量,其中4个特征变量,1个目标分类变量。共有150个样本,目标变量为 花的类别 其都属于鸢尾属下的三个亚属,分别是山鸢尾 (Iris-setosa),变色鸢尾(Iris-versicolor)和维吉尼亚鸢尾(Iris-virginica)。包含的三种鸢尾花的四个特征,分别是花萼长度(cm)、花萼宽度(cm)、花瓣长度(cm)、花瓣宽度(cm),这些形态特征在过去被用来识别物种。变量描述sepal length花萼长度(cm)sepal width花萼宽度(cm)petal length花瓣长度(cm)petal width花瓣宽度(cm)target鸢尾的三个亚属类别,'setosa'(0), 'versicolor'(1), 'virginica'(2)Step2:数据读取/载入## 我们利用 sklearn 中自带的 iris 数据作为数据载入,并利用Pandas转化为DataFrame格式 from sklearn.datasets import load_iris data = load_iris() #得到数据特征 iris_target = data.target #得到数据对应的标签 iris_features = pd.DataFrame(data=data.data, columns=data.feature_names) #利用Pandas转化为DataFrame格式Step3:数据信息简单查看## 利用.info()查看数据的整体信息 iris_features.info() # Column Non-Null Count Dtype --- ------ -------------- ----- 0 sepal length (cm) 150 non-null float64 1 sepal width (cm) 150 non-null float64 2 petal length (cm) 150 non-null float64 3 petal width (cm) 150 non-null float64 dtypes: float64(4) memory usage: 4.8 KB ## 对于特征进行一些统计描述 iris_features.describe() sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) count 150.000000 150.000000 150.000000 150.000000 mean 5.843333 3.057333 3.758000 1.199333 std 0.828066 0.435866 1.765298 0.762238 min 4.300000 2.000000 1.000000 0.100000 25% 5.100000 2.800000 1.600000 0.300000 50% 5.800000 3.000000 4.350000 1.300000 75% 6.400000 3.300000 5.100000 1.800000 max 7.900000 4.400000 6.900000 2.500000Step4:可视化描述## 合并标签和特征信息 iris_all = iris_features.copy() ##进行浅拷贝,防止对于原始数据的修改 iris_all['target'] = iris_target ## 特征与标签组合的散点可视化 sns.pairplot(data=iris_all,diag_kind='hist', hue= 'target') plt.show() 从上图可以发现,在2D情况下不同的特征组合对于不同类别的花的散点分布,以及大概的区分能力。for col in iris_features.columns: sns.boxplot(x='target', y=col, saturation=0.5,palette='pastel', data=iris_all) plt.title(col) plt.show()利用箱型图我们也可以得到不同类别在不同特征上的分布差异情况。# 选取其前三个特征绘制三维散点图 from mpl_toolkits.mplot3d import Axes3D fig = plt.figure(figsize=(10,8)) ax = fig.add_subplot(111, projection='3d') iris_all_class0 = iris_all[iris_all['target']==0].values iris_all_class1 = iris_all[iris_all['target']==1].values iris_all_class2 = iris_all[iris_all['target']==2].values # 'setosa'(0), 'versicolor'(1), 'virginica'(2) ax.scatter(iris_all_class0[:,0], iris_all_class0[:,1], iris_all_class0[:,2],label='setosa') ax.scatter(iris_all_class1[:,0], iris_all_class1[:,1], iris_all_class1[:,2],label='versicolor') ax.scatter(iris_all_class2[:,0], iris_all_class2[:,1], iris_all_class2[:,2],label='virginica') plt.legend() plt.show()Step5:利用 逻辑回归模型 在二分类上 进行训练和预测## 为了正确评估模型性能,将数据划分为训练集和测试集,并在训练集上训练模型,在测试集上验证模型性能。 from sklearn.model_selection import train_test_split ## 选择其类别为0和1的样本 (不包括类别为2的样本) iris_features_part = iris_features.iloc[:100] iris_target_part = iris_target[:100] ## 测试集大小为20%, 80%/20%分 x_train, x_test, y_train, y_test = train_test_split(iris_features_part, iris_target_part, test_size = 0.2, random_state = 2020) ## 从sklearn中导入逻辑回归模型 from sklearn.linear_model import LogisticRegression ## 定义 逻辑回归模型 clf = LogisticRegression(random_state=0, solver='lbfgs') # 在训练集上训练逻辑回归模型 clf.fit(x_train, y_train) ## 查看其对应的w print('the weight of Logistic Regression:',clf.coef_) ## 查看其对应的w0 print('the intercept(w0) of Logistic Regression:',clf.intercept_) ## 在训练集和测试集上分布利用训练好的模型进行预测 train_predict = clf.predict(x_train) test_predict = clf.predict(x_test) from sklearn import metrics ## 利用accuracy(准确度)【预测正确的样本数目占总预测样本数目的比例】评估模型效果 print('The accuracy of the Logistic Regression is:',metrics.accuracy_score(y_train,train_predict)) print('The accuracy of the Logistic Regression is:',metrics.accuracy_score(y_test,test_predict)) ## 查看混淆矩阵 (预测值和真实值的各类情况统计矩阵) confusion_matrix_result = metrics.confusion_matrix(test_predict,y_test) print('The confusion matrix result:\n',confusion_matrix_result) # 利用热力图对于结果进行可视化 plt.figure(figsize=(8, 6)) sns.heatmap(confusion_matrix_result, annot=True, cmap='Blues') plt.xlabel('Predicted labels') plt.ylabel('True labels') plt.show()The accuracy of the Logistic Regression is: 1.0The accuracy of the Logistic Regression is: 1.0The confusion matrix result: [[ 9 0] [ 0 11]]我们可以发现其准确度为1,代表所有的样本都预测正确了。Step6:利用 逻辑回归模型 在三分类(多分类)上 进行训练和预测## 测试集大小为20%, 80%/20%分 x_train, x_test, y_train, y_test = train_test_split(iris_features, iris_target, test_size = 0.2, random_state = 2020) ## 定义 逻辑回归模型 clf = LogisticRegression(random_state=0, solver='lbfgs') # 在训练集上训练逻辑回归模型 clf.fit(x_train, y_train) ## 查看其对应的w print('the weight of Logistic Regression:\n',clf.coef_) ## 查看其对应的w0 print('the intercept(w0) of Logistic Regression:\n',clf.intercept_) ## 由于这个是3分类,所有我们这里得到了三个逻辑回归模型的参数,其三个逻辑回归组合起来即可实现三分类。 ## 在训练集和测试集上分布利用训练好的模型进行预测 train_predict = clf.predict(x_train) test_predict = clf.predict(x_test) ## 由于逻辑回归模型是概率预测模型(前文介绍的 p = p(y=1|x,\theta)),所有我们可以利用 predict_proba 函数预测其概率 train_predict_proba = clf.predict_proba(x_train) test_predict_proba = clf.predict_proba(x_test) print('The test predict Probability of each class:\n',test_predict_proba) ## 其中第一列代表预测为0类的概率,第二列代表预测为1类的概率,第三列代表预测为2类的概率。 ## 利用accuracy(准确度)【预测正确的样本数目占总预测样本数目的比例】评估模型效果 print('The accuracy of the Logistic Regression is:',metrics.accuracy_score(y_train,train_predict)) print('The accuracy of the Logistic Regression is:',metrics.accuracy_score(y_test,test_predict)) [9.35695863e-01 6.43039513e-02 1.85301359e-07] [9.80621190e-01 1.93787400e-02 7.00125246e-08] [1.68478815e-04 3.30167226e-01 6.69664295e-01] [3.54046163e-03 4.02267805e-01 5.94191734e-01] [9.70617284e-01 2.93824740e-02 2.42443967e-07] ... [9.64848137e-01 3.51516748e-02 1.87917880e-07] [9.70436779e-01 2.95624025e-02 8.18591606e-07]] The accuracy of the Logistic Regression is: 0.9833333333333333 The accuracy of the Logistic Regression is: 0.8666666666666667## 查看混淆矩阵 confusion_matrix_result = metrics.confusion_matrix(test_predict,y_test) print('The confusion matrix result:\n',confusion_matrix_result) # 利用热力图对于结果进行可视化 plt.figure(figsize=(8, 6)) sns.heatmap(confusion_matrix_result, annot=True, cmap='Blues') plt.xlabel('Predicted labels') plt.ylabel('True labels') plt.show()通过结果我们可以发现,其在三分类的结果的预测准确度上有所下降,其在测试集上的准确度为:$86.67\%$,这是由于'versicolor'(1)和 'virginica'(2)这两个类别的特征,我们从可视化的时候也可以发现,其特征的边界具有一定的模糊性(边界类别混杂,没有明显区分边界),所有在这两类的预测上出现了一定的错误。5 重要知识点逻辑回归 原理简介:Logistic回归虽然名字里带“回归”,但是它实际上是一种分类方法,主要用于两分类问题(即输出只有两种,分别代表两个类别),所以利用了Logistic函数(或称为Sigmoid函数),函数形式为:$$ logi(z)=\frac{1}{1+e^{-z}} $$其对应的函数图像可以表示如下:import numpy as np import matplotlib.pyplot as plt x = np.arange(-5,5,0.01) y = 1/(1+np.exp(-x)) plt.plot(x,y) plt.xlabel('z') plt.ylabel('y') plt.grid() plt.show()通过上图我们可以发现 Logistic 函数是单调递增函数,并且在z=0的时候取值为0.5,并且$logi(\cdot)$函数的取值范围为$(0,1)$。而回归的基本方程为$z=w_0+\sum_i^N w_ix_i$,将回归方程写入其中为:$$ p = p(y=1|x,\theta) = h_\theta(x,\theta)=\frac{1}{1+e^{-(w_0+\sum_i^N w_ix_i)}} $$所以, $p(y=1|x,\theta) = h_\theta(x,\theta)$,$p(y=0|x,\theta) = 1-h_\theta(x,\theta)$逻辑回归从其原理上来说,逻辑回归其实是实现了一个决策边界:对于函数 $y=\frac{1}{1+e^{-z}}$,当 $z=>0$时,$y=>0.5$,分类为1,当 $z<0$时,$y<0.5$,分类为0,其对应的$y$值我们可以视为类别1的概率预测值.对于模型的训练而言:实质上来说就是利用数据求解出对应的模型的特定的$w$。从而得到一个针对于当前数据的特征逻辑回归模型。而对于多分类而言,将多个二分类的逻辑回归组合,即可实现多分类。
文章
机器学习/深度学习  ·  存储  ·  自然语言处理  ·  算法  ·  数据可视化  ·  搜索推荐  ·  数据挖掘  ·  Python
2023-03-22
IoT小程序在展示中央空调采集数据和实时运行状态上的应用
  利用前端语言实现跨平台应用开发似乎是大势所趋,跨平台并不是一个新的概念,“一次编译、到处运行”是老牌服务端跨平台语言Java的一个基本特性。随着时代的发展,无论是后端开发语言还是前端开发语言,一切都在朝着减少工作量,降低工作成本的方向发展。  与后端开发语言不同,利用前端语言实现跨平台有先天的优势,比如后端语言Java跨平台需要将源代码编译为class字节码文件后,再放进 Java 虚拟机运行;而前端语言JavaScript是直接将源代码放进JavaScript解释器运行。这就使得以JavaScript为跨平台语言开发的应用,可移植性非常强大。  目前跨平台技术按照解决方案分类,主要分为 Web 跨平台、容器跨平台、小程序跨平台。这里,我们主要以小程序跨端为例,测试对比IoT小程序和其他小程序在开发和应用上的优缺点。说到小程序,大家肯定想到微信小程序,实际在各大互联网公司:支付宝、百度、头条等等都有自己的小程序,小程序跨平台和Web跨平台十分类似,都是基于前端语言实现,小程序跨平台的优势在于可以调用系统底层能力,例如:蓝牙、相机等,性能方面也优于Web跨平台。  IoT小程序和大多数小程序一样,它是一套跨平台应用显示框架,它利用JS语言低门槛和API标准化大幅度降低了IoT应用的研发难度,其官方框架介绍如下:  IoT小程序在前端框架能力、应用框架能力、图形框架能力都进行了适配和优化。那么接下来,我们按照其官方步骤搭建开发环境,然后结合中央空调数据采集和状态显示的实际应用场景开发物联网小程序应用。一、IoT小程序开发环境搭建  IoT小程序开发环境搭建一共分为四步,对于前端开发来说,安装NodeJS、配置cnpm、安装VSCode都是轻车熟路,不需要细讲,唯一不同的是按照官方说明安装IoT小程序的模拟器和VSCode开发插件HaaS UI,前期开发环境准备完毕,运行Demo查看一下效果,然后就可以进行IoT小程序应用开发了。搭建开发环境,安装HaaS UI插件和运行新建项目,出现一下界面说明开发环境搭建成功,就可以进行IoT小程序开发了:二、开发展示中央空调采集数据和运行状态的IoT小程序应用应用场景  中央空调的维保单位会对中央空调进行定期维护保养,定期的维护保养可排出故障隐患,减少事故发生,降低运行费用,延长设备的使用寿命,同时保障正常的工作时序。除了定期的维护保养外,还需要实时监测中央空调的运行参数(温度、累计排污量、不锈钢_腐蚀率等)和运行状态,及时发现中央空调运行过程中某些参数低于或高于报警值的问题,以便及时定位诊断中央空调存在的问题,然后进行相应的维护保养操作。架构实现  中央空调的数据采集和展示是典型的物联网应用架构,在中央空调端部署采集终端,通过Modbus通信协议采集中央空调设备参数,然后再由采集终端通过MQTT消息发送的我们的云端服务器,云端服务器接收到MQTT消息后转发到消息队列Kafka中,由云服务器上的自定义服务应用订阅Kafka主题,再存储到我们时序数据库中。下图展示了物联网应用的整体架构和IoT小程序在物联网架构中的位置:  IoT小程序框架作为跨平台应用显示框架,顾名思义,其在物联网应用中主要作为显示框架开发。在传统应用中,我们使用微信小程序实现采集数据和运行状态的展示。而IoT小程序支持部署在AliOS Things、Ubuntu、Linux、MacOS、Window等系统中,这就使得我们可以灵活的将IoT小程序部署到多种设备终端中运行。  下面将以阿里云ASP-80智显面板为例,把展示中央空调采集数据和运行状态的IoT小程序部署在阿里云ASP-80智显面板中。IoT小程序开发  我们将从IoT小程序提供的前端框架能力、应用框架能力、图形框架能力来规划相应的功能开发。  IoT小程序采用Vue.js(v2.6.12)开源框架,实现了W3C标准的标签和样式子集;定义了四个应用生命周期,分别是:onLaunch,onShow,onHide,onDestroy;定义了十四个前端基础组件,除了基础的CSS样式支持外,还提供了对Less的支持;Net网络请求通过框架内置的JSAPI实现。  为了快速熟悉IoT小程序框架的开发方式,我们将在VSCode中导入官方公版案例,并以公版案例为基础框架开发我们想要的功能。简单实现通过网络请求获取中央空调采集数据并展示:1、在VSCode编辑器中导入从IoT小程序官网下载的公版案例,下载地址。2、因为IoT小程序前端框架使用的是Vue.js框架,所以在新增页面时也是按照Vue.js框架的模式,将页面添加到pages目录。我们是空调项目的IoT小程序,所以这里在pages目录下新增air-conditioning目录用于存放空调IoT小程序相关前端代码。3、在app.json中配置新增的页面,修改pages项,增加"air-conditioning": "pages/air-conditioning/index.vue"。{ "pages": { ...... "air-conditioning": "pages/air-conditioning/index.vue", ...... }, "options": { "style": { "theme": "theme-dark" } } }4、在air-conditioning目录下新增index.vue前端页面代码,用于展示空调的采集数据是否正常及历史曲线图。设计需要开发的界面如下,页面的元素有栅格布局、Tabs 标签页、Radio单选框、日期选择框、曲线图表等元素。5、首先是实现Tabs标签页,IoT小程序没有Tabs组件,只能自己设置多个Text组件自定义样式并添加click事件来实现。 <div class="tab-list"> <fl-icon name="back" class="nav-back" @click="onBack" /> <text v-for="(item, index) in scenes" :key="index" :class="'tab-item' + (index === selectedIndex ? ' tab-item-selected' : '')" @click="tabSelected(index)" >{{ item }}</text > </div> ...... data() { return { scenes: ["设备概览", "实时数据", "数据统计", "状态统计"], selectedIndex: 0 }; }, ......6、添加采集数据显示列表,在其他小程序框架中,尤其是以Vue.js为基础框架的小程序框架,这里有成熟的组件,而IoT小程序也是需要自己来实现。<template> <div class="scene-wrapper" v-if="current"> <div class="label-temperature-wrapper top-title"> <div class="label-temperature-wrapper left-text"> <text class="label-temperature">设备编码:</text> <text class="label-temperature-unit">{{deviceNo}}</text> </div> <div class="label-temperature-wrapper right-text"> <text class="label-temperature">数据日期:</text> <text class="label-temperature-unit">{{collectTime}}</text> </div> </div> <div class="main-wrapper"> <div class="section"> <div class="demo-block icon-block"> <div class="icons-item" v-for="(value, key, index) in IconTypes" :key="index"> <div class="label-title-wrapper"> <text class="label-title left-text">{{paramName}}</text> <text class="label-title-unit right-text" style="padding-right: 5px;">{{paramWarn}}</text> </div> <div class="label-zhibiao-wrapper"> <text class="label-zhibiao">当前值:</text> <text class="label-zhibiao-unit">{{value}}</text> </div> <div class="label-zhibiao-wrapper" style="margin-bottom: 10px;"> <text class="label-zhibiao">目标值:</text> <text class="label-zhibiao-unit">{{targetValue}}</text> </div> </div> </div> </div> </div> </div> </template>  在开发过程中发现,IoT小程序对样式的支持不是很全面,本来想将组件放置在同一行,一般情况下,只需要使用标准CSS样式display: inline就可以实现,但这里没有效果只能通过Flexbox进行布局在同一行。在设置字体方面,本来想把采集数据显示的描述字段加粗,用于突出显示,但是使用CSS样式font-weight无效,无论是设置数值还是blod,没有一点加粗效果。7、界面实现之后,需要发送数据请求,来查询采集数据并显示在界面上。IoT小程序通过框架内置JSAPI的Net网络提供网络请求工具。目前从官方文档和代码中来看,官方框架只提供了http请求,没有提供物联网中常用的WebSocket和MQTT工具,估计需要自定义扩展系统JSAPI实现其他网络请求。 created() { const http = $falcon.jsapi.http http.request({ url: 'http://服务域名/device/iot/query/data/point', data: { 'deviceNo': '97306000000000005', 'rangeType': 'mo', 'lastPoint': '1', 'beginDateTime': '2023-02-10+16:09:42', 'endDateTime': '2023-03-12+16:09:42' }, header: { 'Accept': 'application/json;charset=UTF-8', 'Accept-Encoding': 'gzip, deflate, br', 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': '有效token' } }, (response) => { console.log(response) var obj = JSON.parse(response.result) console.log(obj.success) console.log(JSON.parse(obj.data)) }); },  按照官方要求编写http请求,发现默认未开启https请求:Protocol "https" not supported or disabled in libcurl。切换为http请求,返回数据为乱码,设置Accept-Encoding和Accept为application/json;charset=UTF-8仍然无效,且返回数据为JSON字符串,需要自己手动使用JSON.parse()进行转换,对于习惯于应用成熟框架的人来说,十分不友好。想了解更多关于 $falcon.jsapi.http的相关配置和实现,但是官方文档只有寥寥几句,没有详细的说明如何使用和配置,以及http请求中遇到一些常见问题的解决方式。8、IoT小程序框架提供画布组件,原则上来讲可以实现常用的曲线图表功能,但是如果使用其基础能力从零开始开发一套图表系统,耗时又耗力,所以这里尝试引入常用的图表组件库ECharts,使用ECharts在IoT小程序上显示曲线图表。执行cnpm install echarts --save安装echarts组件cnpm install echarts --save新建echarts配置文件,按需引入// 加载echarts,注意引入文件的路径 import echarts from 'echarts/lib/echarts' // 再引入你需要使用的图表类型,标题,提示信息等 import 'echarts/lib/chart/bar' import 'echarts/lib/chart/pie' import 'echarts/lib/component/legend' import 'echarts/lib/component/title' import 'echarts/lib/component/tooltip' export default echarts新增echarts组件ChartDemo.vue<template> <div ref="chartDemo" style="height:200px;" ></div> </template> <script> import echarts from '@/utils/echarts-config.js' const ChartDemo = { name: 'ChartDemo', data() { return { chart: null } }, watch: { option: { handler(newValue, oldValue) { this.chart.setOption(newValue) }, deep: true } }, mounted() { this.chart = echarts.init(this.$refs.chartDemo) }, methods: { setOption(option) { this.chart && this.chart.setOption(option) }, throttle(func, wait, options) { let time, context, args let previous = 0 if (!options) options = {} const later = function() { previous = options.leading === false ? 0 : new Date().getTime() time = null func.apply(context, args) if (!time) context = args = null } const throttled = function() { const now = new Date().getTime() if (!previous && options.leading === false) previous = now const remaining = wait - (now - previous) context = this args = arguments if (remaining <= 0 || remaining > wait) { if (time) { clearTimeout(time) time = null } previous = now func.apply(context, args) if (!time) context = args = null } else if (!time && options.trailing !== false) { time = setTimeout(later, remaining) } } return throttled } } } export default ChartDemo </script> 在base-page.js中注册全局组件...... import ChartDemo from './components/ChartDemo.vue'; export class BasePage extends $falcon.Page { constructor() { super() } beforeVueInstantiate(Vue) { ...... Vue.component('ChartDemo', ChartDemo); } }新建空调采集数据展示页history-charts.vue,用于展示Echarts图表<template> <div class="scene-wrapper" v-if="current"> <div class="brightness-wrap"> <ChartBlock ref="chart2"></ChartBlock> </div> </div> </template> <script> let option2 = { title: { text: '中央空调状态图', subtext: '运行状态占比', left: 'center' }, tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' }, legend: { orient: 'vertical', left: 'left', data: ['开机', '关机', '报警', '故障', '空闲'] }, series: [ { name: '运行状态', type: 'pie', radius: '55%', center: ['50%', '60%'], data: [ { value: 335, name: '开机' }, { value: 310, name: '关机' }, { value: 234, name: '报警' }, { value: 135, name: '故障' }, { value: 1548, name: '空闲' } ], emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] } export default { props:{ current:{ type:Boolean, default:false } }, data() { return { }; }, methods: { }, mounted: function() { this.$refs.chart2.setOption(option2) } }; </script>执行HaaS UI: Build-Debug ,显示打包成功执行HaaS UI: Simulator ,显示“当前HaaS UI: Simulator任务正在执行,请稍后再试”  本来想在模拟器上看一下Echarts显示效果,但是执行HaaS UI: Simulator时一直显示任务正在执行。然后以为是系统进程占用,但是重启、关闭进程等操作一系列操作下来,仍然显示此提示,最后将Echarts代码删除,恢复到没有Echarts的状态,又可以执行了。这里不清楚是否是IoT小程序不支持引入第三方图表组件,从官方文档中没有找到答案。后来又使用echarts的封装组件v-charts进行了尝试,结果依然不能展示。  如果不能使用第三方组件,那么只能使用IoT官方小程序提供的画布组件来自己实现图表功能,官方提供的画布曲线图示例。9、通过IoT小程序提供的组件分别实现显示中央空调采集数据的实时数据、数据统计、状态统计图表。-实现实时数据折线图<template> <div class="scene-wrapper" v-show="current"> <div class="main-wrapper"> <div class="label-temperature-wrapper top-title"> <div class="label-temperature-wrapper left-text"> <text class="label-temperature">设备编码:</text> <text class="label-temperature-unit">{{deviceNo}}</text> </div> <div class="label-temperature-wrapper right-text"> <text class="label-temperature">数据日期:</text> <text class="label-temperature-unit">{{collectTime}}</text> </div> </div> <canvas ref="c2" class="canvas" width="650" height="300"></canvas> </div> </div> </template> <script> export default { name: "canvas", props: {}, data() { return { deviceNo: '97306000000000005', collectTime: '2023-03-11 23:59:59' }; }, mounted() { this.c2(); }, methods: { c2() { let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c2) : this.$refs.c1.getContext("2d"); // Demo测试数据 let arr = [{key:'01:00',value:61.68},{key:'02:00',value:83.68},{key:'03:00',value:56.68},{key:'04:00',value:86.68},{key:'05:00',value:53.68}, {key:'06:00',value:41.68},{key:'07:00',value:33.68}]; this.drawStat(ctx, arr); }, //该函数用来绘制折线图 drawStat(ctx, arr) { //画布的款高 var cw = 700; var ch = 300; //内间距padding var padding = 35; //原点,bottomRight:X轴终点,topLeft:Y轴终点 var origin = {x:padding,y:ch-padding}; var bottomRight = {x:cw-padding,y:ch-padding}; var topLeft = {x:padding,y:padding}; ctx.strokeStyle='#FF9500'; ctx.fillStyle='#FF9500'; //绘制X轴 ctx.beginPath(); ctx.moveTo(origin.x,origin.y); ctx.lineTo(bottomRight.x,bottomRight.y); //绘制X轴箭头 ctx.lineTo(bottomRight.x-10,bottomRight.y-5); ctx.moveTo(bottomRight.x,bottomRight.y); ctx.lineTo(bottomRight.x-10,bottomRight.y+5); //绘制Y轴 ctx.moveTo(origin.x,origin.y); ctx.lineTo(topLeft.x,topLeft.y); //绘制Y轴箭头 ctx.lineTo(topLeft.x-5,topLeft.y+10); ctx.moveTo(topLeft.x,topLeft.y); ctx.lineTo(topLeft.x+5,topLeft.y+10); //设置字号 var color = '#FF9500'; ctx.fillStyle=color; ctx.font = "13px scans-serif";//设置字体 //绘制X方向刻度 //计算刻度可使用的总宽度 var avgWidth = (cw - 2*padding - 50)/(arr.length-1); for(var i=0;i<arr.length;i++){ //循环绘制所有刻度线 if(i > 0){ //移动刻度起点 ctx.moveTo(origin.x+i*avgWidth,origin.y); //绘制到刻度终点 ctx.lineTo(origin.x+i*avgWidth,origin.y-10); } //X轴说明文字:1月,2月... var txtWidth = 35; ctx.fillText( arr[i].key, origin.x+i*avgWidth-txtWidth/2 + 10, origin.y+20); } //绘制Y方向刻度 //最大刻度max var max = 0; for(var i=0;i<arr.length;i++){ if(arr[i].value>max){ max=arr[i].value; } } console.log(max); /*var max = Math.max.apply(this,arr); console.log(max);*/ var avgValue=Math.floor(max/5); var avgHeight = (ch-padding*2-50)/5; for(var i=1;i<arr.length;i++){ //绘制Y轴刻度 ctx.moveTo(origin.x,origin.y-i*avgHeight); ctx.lineTo(origin.x+10,origin.y-i*avgHeight); //绘制Y轴文字 var txtWidth = 40; ctx.fillText(avgValue*i, origin.x-txtWidth-5, origin.y-i*avgHeight+6); } //绘制折线 for(var i=0;i<arr.length;i++){ var posY = origin.y - Math.floor(arr[i].value/max*(ch-2*padding-50)); if(i==0){ ctx.moveTo(origin.x+i*avgWidth,posY); }else{ ctx.lineTo(origin.x+i*avgWidth,posY); } //具体金额文字 ctx.fillText(arr[i].value, origin.x+i*avgWidth, posY ) } ctx.stroke(); //绘制折线上的小圆点 ctx.beginPath(); for(var i=0;i<arr.length;i++){ var posY = origin.y - Math.floor(arr[i].value/max*(ch-2*padding-50)); ctx.arc(origin.x+i*avgWidth,posY,4,0,Math.PI*2);//圆心,半径,画圆 ctx.closePath(); } ctx.fill(); } } }; </script>-数据统计图表<template> <div class="scene-wrapper" v-show="current"> <div class="main-wrapper"> <div class="label-temperature-wrapper top-title"> <div class="label-temperature-wrapper left-text"> <text class="label-temperature">设备编码:</text> <text class="label-temperature-unit">{{deviceNo}}</text> </div> <div class="label-temperature-wrapper right-text"> <text class="label-temperature">数据日期:</text> <text class="label-temperature-unit">{{collectTime}}</text> </div> </div> <canvas ref="c1" class="canvas" width="650" height="300"></canvas> </div> </div> </template> <script> export default { name: "canvas", props: {}, data() { return { deviceNo: '97306000000000005', collectTime: '2023-03-13 20:23:36' }; }, mounted() { this.c1(); }, methods: { c1() { let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c1) : this.$refs.c1.getContext("2d"); this.draw(ctx); }, draw(ctx){ var x0=30,//x轴0处坐标 y0=280,//y轴0处坐标 x1=700,//x轴顶处坐标 y1=30,//y轴顶处坐标 dis=30; //先绘制X和Y轴 ctx.beginPath(); ctx.lineWidth=1; ctx.strokeStyle='#FF9500'; ctx.fillStyle='#FF9500'; ctx.moveTo(x0,y1);//笔移动到Y轴的顶部 ctx.lineTo(x0,y0);//绘制Y轴 ctx.lineTo(x1,y0);//绘制X轴 ctx.stroke(); //绘制虚线和Y轴值 var yDis = y0-y1; var n=1; ctx.fillText(0,x0-20,y0);//x,y轴原点显示0 while(yDis>dis){ ctx.beginPath(); //每隔30划一个虚线 ctx.setLineDash([2,2]);//实线和空白的比例 ctx.moveTo(x1,y0-dis); ctx.lineTo(x0,y0-dis); ctx.fillText(dis,x0-20,y0-dis); //每隔30划一个虚线 dis+=30; ctx.stroke(); } var xDis=30,//设定柱子之前的间距 width=40;//设定每个柱子的宽度 //绘制柱状和在顶部显示值 for(var i=0;i<12;i++){//假设有8个月 ctx.beginPath(); var color = '#' + Math.random().toString(16).substr(2, 6).toUpperCase();//随机颜色 ctx.fillStyle=color; ctx.font = "13px scans-serif";//设置字体 var height = Math.round(Math.random()*220+20);//在一定范围内随机高度 var rectX=x0+(width+xDis)*i,//柱子的x位置 rectY=height;//柱子的y位置 ctx.color='#FF9500'; ctx.fillText((i+1)+'月份',rectX,y0+15);//绘制最下面的月份稳住 ctx.fillRect(rectX,y0, width, -height);//绘制一个柱状 ctx.fillText(rectY,rectX+10,280-rectY-5);//显示柱子的值 } }, } }; </script>-状态统计图表<template> <div class="scene-wrapper" v-show="current"> <div class="main-wrapper"> <div class="label-temperature-wrapper top-title"> <div class="label-temperature-wrapper left-text"> <text class="label-temperature">设备编码:</text> <text class="label-temperature-unit">{{deviceNo}}</text> </div> <div class="label-temperature-wrapper right-text"> <text class="label-temperature">数据日期:</text> <text class="label-temperature-unit">{{collectTime}}</text> </div> </div> <canvas ref="c3" class="canvas" width="600" height="300"></canvas> </div> </div> </template> <script> export default { name: "canvas", props: {}, data() { return { deviceNo: '97306000000000005', collectTime: '2023-03-13 20:29:36' }; }, mounted() { this.c3(); }, methods: { c3() { let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c3) : this.$refs.c3.getContext("2d"); this.drawPie(ctx); }, drawPie(pen){ // Demo测试数据 var deg = Math.PI / 180 var arr = [ { name: "开机", time: 8000, color: '#7CFF00' }, { name: "关机", time: 1580, color: '#737F9C' }, { name: "空闲", time: 5790, color: '#0ECC9B' }, { name: "故障", time: 4090, color: '#893FCD' }, { name: "报警", time: 2439, color: '#EF4141' }, ]; //总价 pen.translate(30,-120); arr.tatol = 0; for (let i = 0; i < arr.length; i++) { arr.tatol = arr.tatol + arr[i].time } var stardeg = 0 arr.forEach(el => { pen.beginPath() var r1 = 115 pen.fillStyle = el.color pen.strokeStyle='#209AAD'; pen.font = "15px scans-serif"; //求出每个time的占比 var angle = (el.time / arr.tatol) * 360 //利用占比来画圆弧 pen.arc(300, 300, r1, stardeg * deg, (stardeg + angle) * deg) //将圆弧与圆心相连接,形成扇形 pen.lineTo(300, 300) var r2 = r1+10; if(el.name === '关机' || el.name === '空闲') { r2 = r1+30 } //给每个扇形添加数组的name var y1 = 300 + Math.sin((stardeg + angle) * deg-angle*deg/2 ) *( r2) var x1 = 300 + Math.cos((stardeg + angle) * deg-angle*deg/2 ) * (r2) pen.fillText(`${el.name}`, x1, y1) stardeg = stardeg + angle pen.fill() pen.stroke() }); }, } }; </script>三、将IoT小程序更新到ASP-80智显面板查看运行效果  将IoT小程序更新到ASP-80智显面板,在硬件设备上查看IoT应用运行效果。如果是使用PC端初次连接,那么需要安装相关驱动和配置,否则无法使用VSCode直接更新IoT小程序到ASP-80智显面板。1、如果使用Win10将IoT小程序包更新到ASP-80智显面板上,必须用到CH340串口驱动,第一次通过TypeC数据线连接设备,PC端设备管理器的端口处不显示端口,这时需要下载Windows版本的CH340串口驱动下载链接 。2、将下载的驱动文件CH341SER.ZIP解压并安装之后,再次查看PC端设备管理器端口就有了USB Serial CH340端口。3、使用SourceCRT连接ASP-80智显面板,按照官方文档说明,修改配置文件,连接好WiFi无线网,下一步通过VSCode直接更新IoT小程序到ASP-80智显面板上查看测试。4、所有准备工作就绪后,点击VSCode的上传按钮HaaS UI: Device,将应用打包并上传至ASP-80智显面板。在选择ip地址框的时候,输入我们上一步获取到的ip地址192.168.1.112,其他参数保持默认即可,上传成功后,VSCode控制台提示安装app成功。5、IoT小程序安装成功之后就可以在ASP-80智显面板上查看运行效果了。  综上所述,IoT小程序框架在跨系统平台(AliOS Things、Ubuntu、Linux、MacOS、Window等)方面提供了非常优秀的基础能力,应用的更新升级提供了多种方式,在实际业务开发过程中可以灵活选择。IoT小程序框架通过JSAPI提供了调用系统底层应用的能力,同时提供了自定义JSAPI扩展封装的方法,这样就足够业务开发通过自定义的方式满足特殊的业务需求。  虽然多家互联网公司都提供了小程序框架,但在128M 128M这样的低资源设备里输出,IoT小程序是比较领先的,它不需要另外下载APP作为小程序的容器,降低了资源的消耗,这一点是其他小程序框架所不能比拟的。  但是在前端框架方面,实用组件太少。其他小程序已发展多年,基于基础组件封装并开源的前端组件应用场景非常丰富,对于中小企业来说,习惯于使用成熟的开源组件,如果使用IoT小程序开发物联网应用可能需要耗费一定的人力物力。既然是基于Vue.js的框架,却没有提供引入其他优秀组件的文档说明和示例,不利于物联网应用的快速开发,希望官方能够完善文档,详细说明IoT小程序开发框架配置项,将来能够提供更多的实用组件。
文章
数据采集  ·  小程序  ·  前端开发  ·  JavaScript  ·  Ubuntu  ·  物联网  ·  Java  ·  Linux  ·  iOS开发  ·  MacOS
2023-03-22
鲲鹏展翅凌云志:iLogtail社区2022年度开源报告
2022 注定是不平凡的一年,在这一年中我们亲历了北京冬奥会、冬残奥会成功举办,冰雪健儿驰骋赛场,取得了骄人成绩;见证了神舟十三号、十四号、十五号接力腾飞,中国空间站全面建成,“太空之家”遨游苍穹;目睹了潘帕斯雄鹰时隔36年再度登顶世界之巅,球王梅西君临波斯湾。同时在这一年,iLogtail 也迈出了发展历程中关键的一步,于6月正式完整开源。iLogtail 作为一款阿里云日志服务(SLS)团队自研的可观测数据采集器,拥有的轻量级、高性能、自动化配置等诸多生产级别特性,可以部署于物理机、虚拟机、Kubernetes 等多种环境中,用于采集文件、容器输出、指标等各类可观测数据。iLogtail 的核心定位是帮助开发者构建统一的数据采集层,助力可观测平台打造各种上层的应用场景;此外,对于一些寻求轻量计算的场景,也可以使用 iLogtail 承担一些数据聚合、数据过滤、数据路由等功能。开源之路 -- 路漫漫其修远兮,吾将上下而求索当今云原生时代是个快速发展变迁的时代,闭源自建的软件永远无法紧跟时代潮流,我们坚信开源才是 iLogtail 最优的发展策略,也是释放其最大价值的方法。通过开源,iLogtail 插上了腾飞的翅膀,仿佛获得了新的动力之源。目前已有来自字节跳动、同程旅行、石墨文档、小红书、哔哩哔哩、阿里巴巴等知名企业的多位同学参与社区共建,相信通过大家共同的努力,iLogtail 将会被打造成业界最顶级的可观测数据采集器。iLogtail 的前身源自阿里云的神农项目,自从2013年正式孵化以来,10年的发展历程中,iLogtail 始终在不断演进,到目前为止总体经历了四个阶段:飞天 5K 阶段、阿里集团阶段、云原生阶段、开源共建阶段。2021年11月 iLogtail 迈出了开源的第一步,在 Github 上低调开源了 Golang 插件部分的代码。2022年6月,iLogtail 正式向外界开源全部C++核心代码,发布了 1.1.0 版本,内核能力上首次对齐企业版,同时发布了《iLogtail用户手册》,社区版正式进入大规模生产可用阶段,社区的发展速度也犹如装上了加速引擎。开源社区自此变得活跃起来,吸引了众多开发者的关注和参与。2022年11月7日,在完整开源不到半年的时间,iLogtail 在 GitHub 上的关注人数就 突破 1000 大关。越来越多的人知道并开始使用 iLogtail,社区活跃度也持续增强,截止到 2023.2.7 数据,Github 活跃度数据:社区运营 -- 有朋自远方来,不亦乐乎取得的成绩在过去一年中,通过与各方朋友共同的努力,iLogtail 逐渐得到了越来越多的认可。相对年初,iLogtail 在OpenLeaderboard 开源项目中国排名中活跃度上升147位,影响力上升117位,并最终荣获了“阿里巴巴2022新锐开源项目”的称号。活跃的社区应社区发展的需要,建立了 iLogtail 社区钉钉群、微信群(详见文末二维码),群内志同道合的小伙伴共计近500人。在 GitHub 上,制定了 iLogtail 贡献指南、代码开发指南、测试指南(UT 和 E2E),并发布了一系列的 good first issue,以便期望参与社区建设的小伙伴快速入手;建立了 iLogtail 社区论坛,收集意见并沉淀解决的常见问题,所有iLogtail社区的活动的预告也都可以在群公告或论坛置顶帖看到;为了构建更广泛的社区合作机制,发布社区 RoadMap。在过去的一年中,社区同学贡献了很多优秀提案,并落地实现。iLogtail 社区已经逐步形成了浓厚的沟通交流氛围。为了鼓励更多用户和开发者参与社区的建设和宣传,专门设立了贡献者奖励机制,发布了社区徽章(开发者徽章、布道师徽章、答题王徽章),定义了社区角色及发展路径。截止发稿时,已有24人获得 Junior Developer 成就,9人获得 Senior Developer 成就,4人获得 Junior Junior Ambassador 成就,3人获得 Senior Ambassador 成就,11人获得 Junior Moderator 成就,1 人获得 Senior Moderator 成就。为了更好地与社区和开发者沟通,iLogtail 社区例行在线上召开开发者例会(已组织了6次),用于跟开发者同步开发进展信息,商讨项目下一步计划;Meetup 活动(已组织3次),用于向社区介绍整个项目在这一周期的项目进展并进行功能演示,也会邀请一些合作伙伴来分享 iLogtail 应用案例。积极的贡献者iLogtail 社区的快速发展离不开贡献者们的努力,目前 iLogtail 项目的贡献者已增加至30人。在 iLogtail 社区高速发展的过程中,涌现了一批活跃的高质量的贡献者,在帮助 iLogtail 生态完善的同时也建立起了自己的行业信誉。核心贡献者(排名不分先后)如下:刘浩杨(liuhaoyang,字节跳动工程师,iLogtail 社区的 Committer):贡献了数据模型、OpenTelemetry Log Flusher等。郭刚平(snakorse,字节跳动工程师):贡献了私有仓库插件编译构建方案、HTTP Flusher。朱舜佳(shunjiazhu ,字节跳动工程师):贡献了 OTLP Input。俞倩雯(urnotsally,字节跳动工程师):贡献了 ByteArray 数据结构和 HTTP 数据透传功能。孙宇(shalousun,来自同程旅行):贡献了 Kafka v2 Flusher、Pulsar Flusher。魏文晗(XLPE,国内某大型通信与信息服务类公司):贡献了第三方性能测试。舒震宇(szy441687879,小红书工程师):多次分享使用经验,提出多项大数量场景下 GC 优化建议。杜旻翔(kl7sn,石墨文档工程师):贡献了 ClickHouse Flusher。彭友顺(askuy,石墨文档工程师):贡献了 ClickHouse Flusher。杨泽华(Takuka0311,在校学生):贡献 Grok Processor、Desensitize Processor、ConfigServer 实现。周弘懿(pj1987111,阿里云 EMR 团队):贡献了 Fields_with_conditions Processor 及多篇案例分享。余韬(yyuuttaaoo,阿里云工程师)徐可甲(messixukejia,阿里云工程师)张浩翔(henryzhx8,阿里云工程师)刘嘉鹏(EvanLjp,阿里云工程师)林润骑(linrunqi08,阿里云工程师)功能演进 -- 积跬步以至千里核心能力建设更完整的系统支持对于不同的企业来说,生产环境是复杂多变的,因此,作为一款通用的采集器需要首先需要有完备的操作系统和架构的支持。目前社区版的系统支持能力已经基本对齐企业版,对于暂不支持的也提供了基础的依赖,期望有需求的开发者参与共建。系统与架构企业版社区版Linux X86-64支持支持Linux ARM64支持支持,相关 PRWindows支持Windows 编译 Issue,归档了编译依赖及构建方法,期待社区共建。K8s DockerLinux版支持支持K8s ContainerdLinux版支持支持K8s DockerWindows 版支持支持,但缺少部署模版。目前,已有同学也将 iLogtail 成功部署到树莓派上,具体可参见《树莓派上玩转iLogtail:构建/采集/分析NAS日志》。更通用的数据模型iLogtail 最初定位为 SLS 的采集器,因此内部数据流是以 SLS-PB 为结构的,对于开源社区流行的 OTLP 等协议兼容性不是太好;此外,也是缺乏原生的 Metrics 、Traces 数据模型,这两种类型的数据处理都需要以 SLS-PB 结构做数据中转,而这往往会造成性能额外开销及兼容性问题。字节跳动同学发起了全新数据模型的提案,经 iLogtail 社区讨论后接收。新的数据模型中 Pipeline 中流动的每一条数据定义为 PipelineEvent,为 iLogtail 向通用 OneAgent 发展打下了坚实的基础。// 基础 Event 模型type PipelineEvent interface { GetName() string SetName(string) GetTags() Tags GetType() EventType GetTimestamp() uint64 GetObservedTimestamp() uint64 SetObservedTimestamp(uint64)}// 用于对数据进行分组聚合的 Event Grouptype GroupInfo struct { Metadata Metadata Tags     Tags}type PipelineGroupEvents struct { Group  *GroupInfo Events []PipelineEvent}Metric、Trace、Log、Profiling 等都是派生自 PipelineEvent 的具体事件。目前各数据模型的支持情况:Metric 模型:支持 MetricSingleValue 和 MetricMultiValue 两种实现,兼容开源主流的 Metric 单值(eg. Prometheus)和多值(eg. Influxdb)模型;同时,扩展了非数值类型的实现 MetricTypedValues。更多详见《Metrics 数据模型改进设计》、PR。Trace 模型:包含追踪上下文的数据,详见 PR。Log 模型:定义中。ByteArray 模型:字节流模型,详见 PR。Profiling 模型:讨论中,待入库。新的数据模型打通了 iLogtail 的任督二脉,大大提升了 iLogtail 通用处理能力。新的数据模型将被定义为 iLogtail 的核心处理模型,后续新的插件都将支持新的数据模型,存量的插件也会逐渐适配支持。更强大的容器采集Kubernetes 元数据关联K8s 元数据(例如,Namespace、Pod、Container、Labels等)对于应用日志分析往往起着至关重要的作用。K8s Pod 的元信息存在于 CRI 定义下的 Sandbox Container 内,因此,iLogtail 基于标准 CRI API 与 Pod 的底层定义进行交互,实现获取 K8s 下各类元数据信息,从而无侵入的实现日志采集时的 K8s 元信息关联及过滤筛选能力。更多详见《阿里云SLS 容器采集全面兼容Kubernetes》短生命周期容器采集增强从 Sysdig 2023 容器安全报告可以看出,72% 的容器存活时间不到五分钟,23% 的容器(例如,K8s 的 Job 任务)生命周期甚至只有十秒左右!这些短生命周期的容器,对业务团队进行应用系统分析、基础架构团队进行故障排查、安全团队进行安全取证,都造成了巨大的困难。iLogtail 针对 Job 类容器增删频率高、生命周期短、突发并发大等特点,从容器发现速度、句柄锁定机制、秒退容器数据完整性等方面进行了针对性的优化与方案探究,给出了可靠性稳定的采集方案。更多详见《分布式短任务的数据采集原理与实践》更便捷的管控能力对于可观测采集器,采集配置变更是常有的事。从 1.1.1 版本起,iLogtail 社区版增加了配置热加载能力,即使不重启iLogtail的情况下,也能够动态识别到配置的变更,大大降低了管控复杂度。同时,也提供了针对主机、K8s的配置热加载案例,供社区同学交流。此外,为了满足 K8s 用户的部署需求,发布了 k8s_templates,覆盖从采集源到典型处理,再到输出的整个过程,便于开发者开箱即用、快速上手。K8s 作为开源容器编排平台,在应用部署领域提供了便利的手段,iLogtail 采集配置管理可以通过 Cofigmap 实现全局的管理。但是主机场景下,需要逐个实例进行配置管理,或者需要借助第三方完成。此外,不管是主机还是 K8s 场景,都缺少对 Agent 版本信息、运行状态的统一管理。鉴于行业上缺乏一套统一有效的管控标准的现状,iLogtail 社区联合哔哩哔哩共同制定了采集 Agent 管控协议,并基于该协议实现了 ConfigServer 服务,可以管控任意符合该协议的 Agent。ConfigServer 作为一款可观测 Agent 的管控服务,支持以下功能:以 Agent 组的形式对采集 Agent 进行统一管理。远程批量配置采集 Agent 的采集配置。监控采集 Agent 的运行状态,汇总告警信息。同时,对于存储适配层进行了抽象,便于开发者对接符合自己环境需求的持久化存储。采集生态建设iLogtail 作为一款通用的可观测数据采集器,核心目的就是从各种可观测数据源进行数据采集,并将采集到的数据经过一些列处理后,发送到对应的存储系统。目前 iLogtail 数据源类型覆盖 Logs、Metrics、Traces,数据源除了主机或 K8s 文件的采集外,还包括了主流标准协议的采集,例如 Opentelemetry、HTTP、Mysql Binlog、Prometheus、Skywalking、Syslog 等;iLogtail 也通过eBPF支持,实现了无侵入的网路数据采集能力。数据输出生态也从 SLS 逐步扩展到 Kafka、gPRC、Opentelemetry、Pulsar、ClickHouse 等,未来通过开源社区共建也会支持ElasticSearch等更多类型的协议。整体概览插件建设概览提供了目前支持插件的全景视图,可以看出社区贡献的比例在逐步提高,社区贡献者已成为 iLogtail 生态建设不可或缺的核心力量。据不完全统计,比较受开发者青睐的插件有:Input:service_docker_stdout、file_log、service_http_server、service_docker_event、service_skywalking_agent_v3Processor:processor_add_fields、processor_rename、processor_json、processor_regex、processor_strptimeFlusher:flusher_kafka、flusher_kafka_v2、flusher_sls、flusher_stdout、flusher_http开发友好度提升开发者永远是 iLogtail 社区最宝贵的财产,开发者的数量和质量决定了社区的发展。为了让开发者更低门槛的入手 iLogtail,做出了如下努力:快速的使用案例是开发者接触 iLogtail 的第一印象,好的印象有助于拉近开发者的距离。为此,重新设计了 结构清晰的 Pipeline 配置文件,输出了极简的快速开始和 getting-started 案例。编译构建是开发者参与 iLogtail 开发的第一步。为了解决众多编译依赖(特别是 C++ 语言部分)的问题,发布了多架构镜像,用户可以基于统一的镜像,实现 Linux X86-64、ARM64 系统的快速开发;提供了基于 VSCode 的远程开发案例,便于开发者快速入手;并且应社区的强烈建议,优化了 Go Mod 管理机制,大大降低新引入依赖库的复杂度;Go版本升级到1.18 方便开发者利用更多高版本的新特性。iLogtail 数据流是基于 SLS-PB 或者 V2 扩展后的内部通用结构实现的,因此在开发第三方 Flusher 时往往面临着格式转换的问题。为了加快开发插件流程,iLogtail 提供了通用协议转换模块,开发者只需要指定目标协议的名称和编码方式即可获得编码后的字节流。相关PR add support for protocol conversion,完整开发指南。支持的协议类型协议名称支持的编码方式标准协议sls协议json、protobuf自定义协议单条协议json标准协议Influxdb协议custom字节流协议raw协议custom为了保证代码质量,iLogtail 基于 docker-compose 提供了一个完整的 E2E 测试引擎,便于开发者快速开展各类插件的集成测试。在大部分情况下,开发者只需要编写一个 Yaml 配置文件来定义测试行为,即可轻松完成测试。如果测试流程涉及外部环境依赖,只需额外提供该环境的镜像(或构建镜像所需文件),从而省却了人工配置测试环境(包括网络等)的麻烦。更多详见:《iLogtail社区版使用入门》、《iLogtail社区版开发者指南》。eBPF 支持随着微服务、云原生、DevOps 等技术的推进,推动着服务部署环境动态性的不断提升,带来了弹性、资源利用率、解耦等诸多优势,但也带来了如上文所述的异构语言、多种协议等诸多问题,这对传统观测性带来了极大的挑战。而 eBPF 基于内核态观测手段,可以灵活、无侵入、高性能的解决上述复杂环境带来的多语言与多协议观测等挑战。iLogtail 与 Coolbpf 合作,研发了无侵入 eBPF 采集方案,相关 PR。目前已支持 L4/L7 网络流量采集,HTTP、Redis、MySQL、PgSQL、DNS等协议解析,未来也将支持 Dubbo、gRPC、Profling等更多场景。iLogtail 无侵入监控采集分为两部分:Kernel Space:通过内核态 Hook 模块,进行数据的抓取与预处理。User Space:根据网络协议进行细粒度的协议分析、数据聚合等操作,以及一些资源控制管理的工作。更多详见《阿里SLS无侵入监控采集方案发布》OpenTelemetry 全面兼容OpenTelemetry 旨在提供可观测领域标准化方案,OTLP 作为数据传输模型被众多系统支持。iLogtail 也积极拥抱 OTLP,目前已提供:service_otlp:支持 OTLP Log、Metric、Trace 的 HTTP/gPRC 请求。service_http_server:扩展了 OTLP Log、Metric、Trace HTTP 接收能力。flusher_otlp_log:将采集到的日志发送到支持 OTLP 的后端系统。更多插件建设更丰富的接入能力:metric_input_netping、service_mssql(采集Sql Server查询数据)、service_pgsql(采集PostgreSQL 查询数据)更灵活的处理能力:Grok Processor、CSV Processor、Desensitize Processor(数据脱敏)、条件字段处理插件(根据日志字段取值动态进行字段扩展或删除,使用案例)。更完整的第三方 Flusher 支持:HTTP Flusher(以 HTTP 发送可观测数据,支持多种 custom_single、influxdb、raw 等多种协议)、新版 Kafka Flusher、Pulsar Flusher、Clickhouse Flusher(PR 中)。更强大的数据聚合能力:aggregator_content_value_group(按照指定的Key对采集到的数据进行分组聚合)、aggregator_metadata_group(按照指定的 Metadata Key 进行重新聚合)。第三方性能/可靠性测试日志采集是整个日志基础设施中最基础最关键的组件之一,影响着企业内部数据的完整性以及实时性。采集器作为数据链路的前置环节,其可靠性、扩展性、灵活性以及资源(CPU 和内存)消耗等,往往是最被关注的核心技术点。来自国内某大型通信与信息服务类公司的运维同学,从中立的第三方视角,针对五个主流的开源日志采集器做的详细的测评:ilogtail(社区版 v1.1.1)、filebeat(v8.4.2)、vector(v0.24.1)、fluent-bit(v1.9.9)、rsyslog(v8)。首先,从进程正常退出、进程异常终止、日志轮转、配置在线升级等四个常见可靠性场景出发,进行了采集数据一致性测试。结果显示,iLogtail 除了在异常退出场景下少量重复外,其他场景数据完全一致。其次,在多种场景下(多行/单行日志、采集速率变化、正则的使用)进行了完备的性能测试对比。从性能测试结果看,ilogtail 在采集速率上优势明显。在某些极端速率场景下,iLogtail是唯一一个采集速度可以个日志生成速度接近的采集器,且资源占用率和采集速率基本维持一个线性关系。更多详见:《性能与可靠的超强碰撞:第三方测评开源日志采集器》交流分享 -- 肯与邻翁相对饮,隔篱呼取尽余杯通过 iLogtail 社区搭建了一个技术交流的平台,秉承着开放交流的态度,通过线上 iLogtail 例会、线下技术大会多路出击,与开发者近距离交流可观测采集相关技术、解决方案及实践案例。成功案例 -- 三人行必有我师根据社区用户群和公开资料统计,目前使用 iLogtail 社区版的公司包括阿里巴巴、字节跳动、小红书、同程旅行、同城必应、Bilibili等知名企业。这些企业的应用实践,为广大企业构建可观测性数据实时采集和大数据平台提供了宝贵的经验。在字节内部,我们建设了可以处理数十PB级别海量数据的可观测性基础设施,随着越来越多的数据接入和内外一体背景下 ToB 场景的需求,我们想要使用 OneAgent 来覆盖 Metrics、Trace、Log 、Event 等可观测性数据。通过对比 iLogtail、Vector、Opentelemetry Collector、Fluentbit 等开源的数据采集器,基于下面的几点考虑 我们最终选择了和阿里的同学一起建设 iLogtail。iLogtail 经过在阿里生态和阿里云上的大规模使用,性能和稳定性有足够的保障我们的经验更多在 Metrics & Trace 数据使用上,iLogtail 对 Log 完善的支持可以和我们的场景进行互补 iLogtail 使用 Apache 2.0 开源协议对商业使用比较友好,同时我们也可以和阿里的同学密切合作来共同建设 iLogtail 的开源生态目前我们已经在 APM 产品上使用了 iLogtail 的日志采集功能,并且在私有云中有实际的落地应用,接下来我们已经在尝试增强 iLogtail 的 Metrics 采集能力用于百万级的云基础设施和公有云产品的监控数据接入场景。——字节跳动目前小红书正在使用 iLogtail 替换 ELK 体系日打造新一代日志系统,iLogtail 在新系统中同时承担了采集(Filebeat)和管道(Logstash)的功能,在业务日志场景下使用将其级联的方式去除了 Kafka 的中间环节,提高系统效率;在 APM、风控等日志场景下使用其作为统一的数据采集处理层,消费 Kafka 做后续处理。——小红书在同程旅行数据中心大数据云原生的开发建设场景中,我们必须把 HDFS、Flink、Spark、Kudu 等大数据组件和机器学习平台的服务日志进行实时收集,为大数据基础设施建立起完整的可观测闭环。随着大数据集群业务规模的不断扩大,过去采用 Filebeat 作为可观测性日志采集的方案出现了 CPU 占用高、日志采集延迟等一系列问题。通过调研和测试对比一些其它开源日志采集组件后,我们决定采用高性能的 iLogtail 替代 Filebeat。由于iLogtail 优异的性能和社区丰富的日志处理插件,过去日志采集中 CPU 占用高和采集延迟的问题得到有效的解决,也减轻了 Flink 日志清洗层任务的工作和压力、降低了日志处理对资源的消耗。——同程旅行同城必应的业务分别部署在阿里云容器集群和 AWS 的 EC2,由于用户涉及国内和海外,故未使用厂商提供的日志聚合功能,而是自建 ES 的方式来做日志聚合。 Kubernetes 社区推荐的日志采集方案为 Deamonset 方式部署采集器,但只针对于标准输出的采集,由于我们的业务日志均为写日志方式,故之前采用的日志采集方案为 Sidecar 模式部署 Filebeat。但该方案存在以下问题:1. 资源占用高,单集群中会存在多个日志采集容器;2. 业务侵入性高,业务容器可能会因为日志采集器容器 crash 而无法就绪,无法正常接收业务流量;3. 配置复杂,每次新增 Pod 都需要再部署一次日志采集器。而 Deamonset 方式部署的 iLogtail 有:与业务容器解耦分离单节点仅一个采集器容器自动发现等特性其很好的解决了我们之前 Sidecar 部署方式存在的问题,可在满足我们业务需求的前提下,降低集群资源成本。——同城必应业界主流的开源采集 Agent,例如 FileBeat、Fluentd 都是提供了本地部署模式,如果需要全局管控只能借助第三方的工具例如 supervisor 进行管理。目前 Bilibili 内部使用多种采集端,而行业上缺乏一套统一有效的管控手段,基于这个现状,Bilibili 与 iLogtail 开源社区一起联合制定了采集 Agent 管控协议。iLogtail 社区也基于该协议提供了 ConfigServer 服务,可以管控任意符合该协议的 Agent。ConfigServer 作为一款可观测 Agent 的管控服务,支持以下功能:以 Agent 组的形式对采集 Agent 进行统一管理远程批量配置采集 Agent 的采集配置监控采集 Agent 的运行状态,汇总告警信息——Bilibili目前石墨文档正基于 ClickHouse 和自研的 ClickVisual 构建全新的日志系统,使用 Fluent Bit 配合 Kafka 进行日志采集传输,未来计划使用 iLogtail 替换 Fluent Bit。目前已在部分业务场景下使用 iLogtail 结合 ClickHouse 缓冲表的方式完成日志采集存储,该流程缩减了中间环节,iLogtail 在过程中出色的承担了日志采集和传输管道的角色,依托于活跃的社区环境与丰富的插件组件,在整个开发实施过程中高效的解决了很多问题,并最终有效的提升了系统效率。——石墨文档2023展望 -- 海阔凭鱼跃,天高任鸟飞2022年,iLogtail 迈出了开源的关键一步,不管是数据模型演进,还是生态集成都有了很大的飞跃。在这里,首先需要感谢下众多开发者的积极参与,特别需要感谢字节跳动同学的做出的卓越贡献,提出了很多极具建设性的提案并实施落地。iLogtail 2022 开源的故事分享的差不多了,但是 iLogtail 过去一年还不仅于此,我们不久将继续分享关于 iLogtail 企业版,尤其是是借助 SLS 可观测平台能力的一些工作,敬请期待本文姊妹篇《iLogtail 企业版 2022年度技术总结》。新的一年,iLogtail 社区将围绕以下领域同开发者一起共建,将 iLogtail 打造成更加高效、稳定、可扩展性强的通用采集器。更多技术规划,社区将通过 Roadmap 定期发布。极致的采集性能、超强可靠性、跨系统支持一直是 iLogtail 追求的目标,社区将持续加大核心竞争力投入。通过改进 C++/Golang 交互,让开源用户享受到更多 C++的加速能力,通过资源控制等手段持续增强极端场景下的可靠性与稳定性。可观测采集 OneAgent 越来越成为趋势,iLogtail 社区将继续与字节跳动等同学一起通过数据模型的升级来完善 Log、Metric、Trace、Profiling、Security的核心处理能力,通过扩展 Pipeline 语义增强各个场景的处理能力,通过对 C++ 代码优化提升模块化扩展能力。持续降低开发门槛,助力开发者快速参与项目开发。借助 eBPF、第三方协议对接等技术手段,增强时序、安全、Profiling等垂直场景等采集能力,打造业界领先的数据生态。其他方面:加强 iLogtail 自身可观测建设,扩展第三方系统的对接,持续降低监控及问题排查门槛;通过推进标准管控协议的建设,打造统一管控方案;与更多开源软件(例如,KubeVela、OpenKruise等)的横向合作,并输出实用案例。技术共享一直是 iLogtail 秉承的理念,如果以上内容让您感觉到兴奋,非常欢迎加入 iLogtail 社区参与共建。“集百家之所长,融百家之所思”,希望跟开发者一起能够将 iLogtail 的核心能力打造的更完善,上下游生态构建的更丰富。相信通过大家共同的努力,iLogtail 将会被打造成业界最顶级的可观测数据采集器。iLogtail已开源,欢迎使用及共建!iLogtail作为阿里云SLS提供的可观测数据采集器,可以运行在服务器、容器、K8s、嵌入式等多种环境,支持采集数百种可观测数据(日志、监控、Trace、事件等),已经有千万级的安装量。目前,iLogtail已正式开源,欢迎使用及参与共建。GitHub:https://github.com/alibaba/ilogtail社区版文档:https://ilogtail.gitbook.io/ilogtail-docs/about/readme企业版官网:https://help.aliyun.com/document_detail/65018.html
文章
消息中间件  ·  数据采集  ·  Kubernetes  ·  监控  ·  Cloud Native  ·  大数据  ·  Kafka  ·  开发者  ·  C++  ·  容器
2023-02-16
fastjson2为什么这么快
fastjson 是很多企业应用中处理 json 数据的基础工具,其凭借在易用性、处理速度上的优越性,支撑了很多数据处理场景。fastjson 的作者「高铁」已更新推出 2.0 版本的 fastjson,即 fastjosn2。据 “相关数据” 显示,fastjson2 各方面性能均有提升,常规数据序列化相比 1.0 系列提升达到 30%,那么,fastjson2 使用了哪些核心技术来提升速度的呢?笔者总结包含但不限于以下几个方面:用「Lambda 生成函数映射」代替「高频的反射操作」对 String 做零拷贝优化常见类型解析优化1、用「 Lambda 生成函数映射」代替「高频的反射操作」我们来看一段最简单的反射执行代码:public class Bean { int id; public int getId() { return id; } } Method methodGetId = Bean.class.getMethod("getId"); Bean bean = createInstance(); int value = (Integer) methodGetId.invoke(bean);上面的反射执行代码可以被改写成这样:// 将getId()映射为function函数 java.util.function.ToIntFunction<Bean> function = Bean::getId; int i = function.applyAsInt(bean);fastjson2 中的具体实现的要复杂一点,但本质上跟上面一样,其本质也是生成了一个 function//function java.util.function.ToIntFunction<Bean> function = LambdaMetafactory.metafactory( lookup, "applyAsInt", methodHanlder, methodType(ToIntFunction.class), lookup.findVirtual(int.class, "getId", methodType(int.class)), methodType(int.class) ); int i = function.applyAsInt(bean);我们使用反射获取到的 Method 和 Lambda 函数分别执行 10000 次来看下处理速度差异:Method invoke elapsed: 25ms Bean::getId elapsed: 1ms处理速度相差居然达到 25 倍,使用 Java8 Lambda 为什么能提升这多呢?答案就是:Lambda 利用 LambdaMetafactory 生成了函数映射代替反射。下面我们详细分析下 Java反射 与 Lambda 函数映射 的底层区别。A、反射执行的底层原理注:以下只是想表达出反射调用本身的繁杂性,大可不必深究这些代码细节从代码角度,我们从 Java 方法反射 Method.invoke 的源码入口来深入:public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException,InvocationTargetException{ if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor;// read volatile if (ma == null) ma = acquireMethodAccessor(); return ma.invoke(obj, args); }可见,经过简单的检查后,调用的是MethodAccessor.invoke(),这部分的实际实现:public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException { if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) { MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers()); this.parent.setDelegate(var3); } return invoke0(this.method, var1, var2); } private static native Object invoke0(Method var0, Object var1, Object[] var2);可见,最终调用的是 native 本地方法(本地方法栈)的 invoke0(),这部分的实现:JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0 (JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args) { return JVM_InvokeMethod(env, m, obj, args); }可见,调用的是 jvm.h 模块的 JVM_InvokeMethod 方法,这部分的实现:JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0)) JVMWrapper("JVM_InvokeMethod"); Handle method_handle; if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) { method_handle = Handle(THREAD, JNIHandles::resolve(method)); Handle receiver(THREAD, JNIHandles::resolve(obj)); objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0))); oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL); jobject res = JNIHandles::make_local(env, result); if (JvmtiExport::should_post_vm_object_alloc()) { oop ret_type = java_lang_reflect_Method::return_type(method_handle()); assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!"); if (java_lang_Class::is_primitive(ret_type)) { // Only for primitive type vm allocates memory for java object. // See box() method. JvmtiExport::post_vm_object_alloc(JavaThread::current(), result); } } return res; } else { THROW_0(vmSymbols::java_lang_StackOverflowError()); } JVM_END更详细的细节:https://www.zhihu.com/question/464985077/answer/1940021614B、Lambda生成函数映射的底层原理具体来讲,Bean::getId 这种 Lambda 写法进过编译后,会通过 java.lang.invoke.LambdaMetafactory 调用到java.lang.invoke.InnerClassLambdaMetafactory#spinInnerClass,最终实现是调用 JDK 自带的字节码库 jdk.internal.org.objectweb.asm 动态生成一个内部类,上层 call 内部类的方法执行调用。所以 Lambda 生成函数映射的方式,核心消耗就在于生成函数映射,那生成函数映射的效率究竟如何呢?我们和反射获取 Method 做个对比,Benchmark 结论:Benchmark ModeCntScoreErrorUnitsgenMethod(反射获取方法)avgt(平均耗时)50.1250.015us/opgenLambda(生成方法的函数映射)avgt551.88040.040us/op从数据来看,生成函数映射的耗时远高于反射获取 Method。那为我们不禁要问,既然生成函数映射的性能远低于反射获取方法,那为什么最终用生成函数的方式的执行速度比反射要快?答案就在于——函数复用,将一个固定签名的函数缓存起来,下次调用就可以省去函数创建的过程。比如 fastjson2 直接将常用函数的初始化缓存放在 static 代码块,这就将函数创建的消耗就被前置到类加载阶段,在数据处理阶段的耗时进一步降低。C、对比分析 & 结论从原理上来说,反射方式,在获取 Method 阶段消耗较少,但 invoke 阶段则是每次都用都调用本地方法执行,先是在 jvm 层面多出一些检查,而后转到 JNI 本地库,除了有额外的 jvm 堆栈与本地方法栈的 context 交换 ,还多出一系列 C 体系额外操作,在性能上自然是不如 Lambda 函数映射;Lambda 生成函数映射的方式,在生成代理类的过程中有部分开销,这部分开销可以通过缓存机制大量减少,而后的调用则全部属于 Java 范畴内的堆栈调用(即拿到代理类后,调用效率和原生方法调用几乎一致)。2、对 String 做零拷贝优化A、何为零拷贝零拷贝是老生常谈的问题,Kafka 还是 Netty 等都用到了零拷贝的知识,这里简单介绍一下其概念以便生疏的读者理解上流畅。零拷贝:是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。JDK8 中的 String 是如何拷贝的?为了实现字符串是不可变的特性,JDK 在构造 String 构造字符串的时候,会有拷贝的过程,比如上图是 JDK8 的 String 的一个构造函数的实现,其在堆内存中重新开辟了一块内存区域。如果要提升构造字符串的开销,就要避免这样的拷贝,即零拷贝。B、fastjson2 中如何实现 0 拷贝在 JDK8 中,String 有一个构造函数是不做拷贝的:但这个方法不是 public,不能直接访问到,可以反射执行,也可以使用 LambdaMetafactory 创建函数映射来调用,前面有介绍这个技巧。生成的函数映射可以缓存起来复用,而这个构造方法的签名是固定不变的,这意味着,只需要生成一次,后续所有需要初始化 String 的时候都可以复用。C、fastjson2 中的应用将 LocalDate 格式化为 “yyyy-MM-dd” 的 String 源码(注:针对 JDK8 的实现,此处对源码精简整理以方便阅读):static BiFunction<char[], Boolean, String> STRING_CREATOR_JDK8; static { //为上述String的0拷贝构造方法创建一个映射函数 CallSite callSite = LambdaMetafactory.metafactory(caller, "apply", methodType(BiFunction.class), methodType(Object.class, Object.class, Object.class), handle, methodType(String.class, char[].class, boolean.class)); STRING_CREATOR_JDK8 = (BiFunction<char[], Boolean, String>) callSite.getTarget().invokeExact(); } static String formatYYYYMMDD(LocalDate date) { int year = date.getYear(); int month = date.getMonthValue(); int dayOfMonth = date.getDayOfMonth(); int y0 = year / 1000 + '0'; int y1 = (year / 100) % 10 + '0'; int y2 = (year / 10) % 10 + '0'; int y3 = year % 10 + '0'; int m0 = month / 10 + '0'; int m1 = month % 10 + '0'; int d0 = dayOfMonth / 10 + '0'; int d1 = dayOfMonth % 10 + '0'; //char array char[] chars = new char[10]; chars[0] = (char) y1; chars[1] = (char) y2; chars[2] = (char) y3; chars[3] = (char) y4; chars[4] = '-'; chars[5] = (char) m0; chars[6] = (char) m1; chars[7] = '-'; chars[8] = (char) d0; chars[9] = (char) d1; //执行「lambda函数映射」构造String String str = STRING_CREATOR_JDK8.apply(chars, Boolean.TRUE); return str; }在 JDK8 的实现中,先拼接好格式中每一个 char 字符,然后通过零拷贝的方式构造字符串对象,这样就实现了快速格式化 LocalDate 到 String,这样的实现远比使用 SimpleDateFormat 之类要快。这种实例化 String 的方式在fatsjson2 中的 JSONReader、JSONWritter 随处可见。3、常见类型解析优化fastjson2 里针对各种类型的优化处理很多,不能一一列举,这里仅以 Date 类型举例,我们前面举例了将 Date 格式化为 String,这次我们反过来,将 String 转换为 Date —— 如何快速将字符串解析成日期?以下给出几种实现方式,随后我们来做个对比。A、使用SimpleDateFormatSimpleDateFormat 是我们使用最广泛、最容易想到的方式,需要注意的是 SimpleDateFormat 不是线程安全的,并发场景下要 sync 同步处理。static final ThreadLocal<SimpleDateFormat> formatThreadLocal = ThreadLocal.withInitial( () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") ); // get format from ThreadLocal SimpleDateFormat format = formatThreadLocal.get(); format.parse(str);B、使用java.time.DateTimeFormatterJDK8 提供了 java.time API,吸收了 joda-time的部分精华,功能更强大,性能也更好。同时,DateTimeFormatter 是线程安全的。static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // use formatter parse Date LocalDateTime ldt = LocalDateTime.parse(str, formatter); ZoneOffset offset = DEFAULT_ZONE_ID.getRules().getOffset(ldt); long millis = ldt.toInstant(offset).toEpochMilli(); Date date = new Date(millis);这种方法比使用 SimpleDateFormat 组合 ThreadLocal 代码更简洁,速度也大约要快 50%。 图片源自githubC、针对固定格式和固定时区优化我们在日常处理 Date 数据时,在国内最常见的格式就是 "yyyy-MM-dd HH:mm:ss",默认的时区为东 8 区,在 java.time 中的 ZonedId 是 "Asia/Shanghai"(而不是 Asia/Beijing),而东 8 区在1992年之后,不在使用夏令时,固定的 zoneOffset 是 +8,根据这个情况,我们可以针对性做优化,如下(为方便理解,以下为源码的简化版,去掉了影响阅读的边界处理等逻辑):public static Date parseYYYYMMDDHHMMSS19(String str) { char y0 = str.charAt(0); char y1 = str.charAt(1); char y2 = str.charAt(2); char y3 = str.charAt(3); char m0 = str.charAt(4); char m1 = str.charAt(5); ... char s1 = str.charAt(18); int year = (y0 - '0') * 1000 + (y1 - '0') * 100 + (y2 - '0') * 10 + (y3 - '0'); int month = (m0 - '0') * 10 + (m1 - '0'); int dom = (d0 - '0') * 10 + (d1 - '0'); int hour = (h0 - '0') * 10 + (h1 - '0'); int minute = (i0 - '0') * 10 + (i1 - '0'); int second = (s0 - '0') * 10 + (s1 - '0'); //换算成毫秒 long millis; if (year >= 1992 && (DEFAULT_ZONE_ID == SHANGHAI_ZONE_ID || DEFAULT_ZONE_ID.getRules() == IOUtils.SHANGHAI_ZONE_RULES)) { final int DAYS_PER_CYCLE = 146097; final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L); long y = year; long m = month; long epochDay; { long total = 0; total += 365 * y; total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400; total += ((367 * m - 362) / 12); total += dom - 1; if (m > 2) { total--; boolean leapYear = (year & 3) == 0 && ((year % 100) != 0 || (year % 400) == 0); if (!leapYear) { total--; } } epochDay = total - DAYS_0000_TO_1970; } long seconds = epochDay * 86400 + hour * 3600 + minute * 60 + second - SHANGHAI_ZONE_OFFSET_TOTAL_SECONDS; millis = seconds * 1000L; } else { LocalDate localDate = LocalDate.of(year, month, dom); LocalTime localTime = LocalTime.of(hour, minute, second, 0); LocalDateTime ldt = LocalDateTime.of(localDate, localTime); ZoneOffset offset = DEFAULT_ZONE_ID.getRules().getOffset(ldt); millis = ldt.toEpochSecond(offset) * 1000; } return new Date(millis); }核心逻辑就是根据位数,直接开始计算给定的时间字符串,相对于参照的原点时间(1970-1-1 0点)过去了多少毫秒,这个优化,避免了parse Number的开销,精简了大量 Partten 的处理,处理流程非常高效。D、性能测试 & 结论benchmark:Benchmark ModeCntScoreErrorUnitsDateParse.simpleDateFormatParseavgt(平均耗时)511.5404.170us/msDateParse.dateTimeFormatterParseavgt57.5940.200us/msDateParse.parseYYYYMMDDHHMMSS19avgt50.4250.098us/msJMH测试显示:方法 3 的耗时远低于其他方式,方法 3 这种针对性的类型解析优化可以使用在重度使用日期解析的优化场景,比如数据批量导入解析日期,大数据场景的 UDF 日期解析等。One more thingfastjson 系列相比同类 json 处理工具,虽然在安全性、鲁棒性等方面还可以提升,但其最大优势——处理速度,却使其他同类竞品望尘莫及。我们也可以在日常业务处理中,学习其精华部分,运用其中的技术亮点,优化业务处理速度,提升用户体验。
文章
存储  ·  缓存  ·  JSON  ·  安全  ·  Java  ·  fastjson  ·  测试技术  ·  数据处理  ·  数据格式  ·  UED
2023-02-20
...
跳转至:
阿里云存储服务
193959 人关注 | 1246 讨论 | 1248 内容
+ 订阅
  • 关系代数和SQL语法
  • Pandas+ SLS SQL:融合灵活性和高性能的数据透视
  • 今天的应用架构,正处在一个不可测的阶段
查看更多 >
开发与运维
5786 人关注 | 133444 讨论 | 319524 内容
+ 订阅
  • 算法题学习链路简要分析与面向 ChatGPT 编程
  • Linux的IPtables可以阻挡ddos攻击吗?底层原理是什么?
  • Codeup的实用性评价
查看更多 >
大数据
188713 人关注 | 30991 讨论 | 83955 内容
+ 订阅
  • Yii2如何进行代码审查?具体怎么做?底层原理是什么?
  • 什么是 WebSocket 协议?底层原理是什么?
  • office全版本软件安装包(win+mac版本)——2016office软件下载
查看更多 >
人工智能
2875 人关注 | 12395 讨论 | 102680 内容
+ 订阅
  • 算法题学习链路简要分析与面向 ChatGPT 编程
  • Codeup的实用性评价
  • 基于PSO三维极点搜索matlab仿真
查看更多 >
数据库
252947 人关注 | 52318 讨论 | 99273 内容
+ 订阅
  • MySQL的数值型数据类型是干什么的?使用场景是什么?底层原理是什么?
  • MySQL的日期/时间型数据类型是干什么的?使用场景是什么?底层原理是什么?
  • MySQL的字符型数据类型是干什么的?使用场景是什么?底层原理是什么?
查看更多 >