当前位置: 首页 > article >正文

记一次 Golang pkg 性能提升 38147125738.8 倍之旅

我正在使用 linux-open-ports 项目来获取系统上当前打开的端口列表。不过我注意到实现速度比我预期的要慢一点,所以我开始调查、分析、修改并使整个过程,使其性能提升了 38147125738.8 倍。下面我对整个过程做一个详细叙述。

1

 

基线

Go 提供了通过基准测试建立基线的工具,因此让我们利用它并编写一个基准测试:

package linuxopenportsimport "testing"func BenchmarkGetOpenPorts(b *testing.B) {	ports, err := GetOpenPorts()	if err != nil {		b.Fatal(err)	}	if len(ports) == 0 {		b.Fatal("no ports detected")	}}

这在我的系统上产生的结果:

goos: linuxgoarch: amd64pkg: github.com/intevel/linux-open-portscpu: AMD Ryzen 7 PRO 7840U w/ Radeon 780M GraphicsBenchmarkGetOpenPortsBenchmarkGetOpenPorts-16               1        1570580328 ns/opPASSok      github.com/intevel/linux-open-ports     1.588s

2

 

pkg 的概念

在基于 Linux 的系统中获取开放端口列表的方法非常容易理解:

  1. 获取所有连接及其 inode 的列表,这些存储在:

/proc/net/tcp/proc/net/tcp6/proc/net/udp/proc/net/udp6

以上所有内容均遵循相同的格式:

~ :: head -n1 < /proc/net/tcpsl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode 0: 3600007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   991        0 15761 1 0000000000000000 100 0 0 10 5

这里没有进程 ID(PID),我们必须将所有找到的进程映射到系统上的 inode 相应进程 pids

  1. 读取每个/proc//fd 目录以访问其套接字 inode(格式的文件 socket:[])

  2. 返回端口及其 inode 和进程 ID 的列表

3

 

深入探索

linux-open-ports 导出一个函数 GetOpenPorts 和相应的类型 OpenPort:

type OpenPort struct {	Protocol string	Port     int	PID      int}func GetOpenPorts() ([]OpenPort, error) {    // ...}

GetOpenPorts 调用 findPIDByInode 每个文件中的每个 inode:

func GetOpenPorts() ([]OpenPort, error) {	var openPorts []OpenPort	uniquePorts := make(map[string]bool)	protocolFiles := map[string][]string{		"tcp": {"/proc/net/tcp", "/proc/net/tcp6"},		"udp": {"/proc/net/udp", "/proc/net/udp6"},	}	for protocol, files := range protocolFiles {		for _, filePath := range files {            // .. scanner setup and error handling			for scanner.Scan() {				fields := strings.Fields(scanner.Text())                // .. field checks and assignments				inode := fields[9]				pid := findPIDByInode(inode)                // ..			}		}	}	return openPorts, nil}

该 findPIDByInode 函数在每次调用时读取整个/proc 目录来查找 inode 所属的进程。

func findPIDByInode(inode string) int {	procDirs, _ := os.ReadDir("/proc")	for _, procDir := range procDirs {		if !procDir.IsDir() || !isNumeric(procDir.Name()) {			continue		}		pid := procDir.Name()		fdDir := filepath.Join("/proc", pid, "fd")		fdFiles, err := os.ReadDir(fdDir)		if err != nil {			continue		}		for _, fdFile := range fdFiles {			fdPath := filepath.Join(fdDir, fdFile.Name())			link, err := os.Readlink(fdPath)			if err == nil && strings.Contains(link, fmt.Sprintf("socket:[%s]", inode)) {				pidInt, _ := strconv.Atoi(pid)				return pidInt			}		}	}	return -1}func isNumeric(s string) bool {	_, err := strconv.Atoi(s)	return err == nil}

4

 

解析整数使用不当

第一个引起我注意的简单方法是 isNumeric 函数调用,它尝试解析目录名称并检查它是否为数字,以确保目录是进程的目录。使用 unicode.IsDigit 判断目录名称的第一个字节应该足够且更快:

func findPIDByInode(inode string) int {	procDirs, _ := os.ReadDir("/proc")	for _, procDir := range procDirs {		pid := procDir.Name()		if !procDir.IsDir() || !unicode.IsDigit(rune(pid[0])) {			continue		}		fdDir := filepath.Join("/proc", pid, "fd")		fdFiles, err := os.ReadDir(fdDir)		if err != nil {			continue		}		for _, fdFile := range fdFiles {			fdPath := filepath.Join(fdDir, fdFile.Name())			link, err := os.Readlink(fdPath)			if err == nil && strings.Contains(link, fmt.Sprintf("socket:[%s]", inode)) {				pidInt, _ := strconv.Atoi(pid)				return pidInt			}		}	}	return -1}
goos: linuxgoarch: amd64pkg: github.com/intevel/linux-open-portscpu: AMD Ryzen 7 PRO 7840U w/ Radeon 780M GraphicsBenchmarkGetOpenPortsBenchmarkGetOpenPorts-16               1        1108555474 ns/opPASSok      github.com/intevel/linux-open-ports     1.123s

基准测试记录从 1570580328 ns/op 到 1108555474 ns/op (1.57s 到 1.11s, 快了 0.46s 或者 1.41x ).

5

 

让我们缓存 Map 并获得 38147125738.8 倍的加速

了解情况的读者会注意到,/proc/每次遇到新的 inode 时迭代目录并不是那么明智。相反,我们应该只迭代一次,然后在 inode 到 pid 的 map 中查找找到的 inode:

func inodePIDMap() map[string]string {	m := map[string]string{}	procDirs, _ := os.ReadDir("/proc")	for _, procDir := range procDirs {		pid := procDir.Name()		if !procDir.IsDir() && !unicode.IsDigit(rune(pid[0])) {			continue		}		fdDir := filepath.Join("/proc", pid, "fd")		fdFiles, err := os.ReadDir(fdDir)		if err != nil {			continue		}		for _, fdFile := range fdFiles {			path := filepath.Join(fdDir, fdFile.Name())			linkName, err := os.Readlink(path)			if err != nil {				continue			}			if strings.Contains(linkName, "socket") {				// index 8:till end -1 because socket:[ is 8 bytes long and ]				// is at the end				inode := linkName[8 : len(linkName)-1]				m[inode] = pid			}		}	}	return m}

GetOpenPorts 必须更新该功能以匹配此更改:

func GetOpenPorts() ([]OpenPort, error) {	var openPorts []OpenPort	uniquePorts := make(map[string]bool)	protocolFiles := map[string][]string{		"tcp": {"/proc/net/tcp", "/proc/net/tcp6"},		"udp": {"/proc/net/udp", "/proc/net/udp6"},	}	cachedInodePIDMap := inodePIDMap()	for protocol, files := range protocolFiles {		for _, filePath := range files {            // .. scanner setup and error handling			for scanner.Scan() {				fields := strings.Fields(scanner.Text())                // .. field checks and assignments				inode := fields[9]				pid, ok := cachedInodePIDMap[inode]				if !ok {					continue				}                // ..			}		}	}	return openPorts, nil}

加速比是巨大的,从 1108555474 ns/op 到 0.02906 ns/op,对应于 38147125739.848587x 加速比。

  • 旧基准

goos: linuxgoarch: amd64pkg: github.com/intevel/linux-open-portscpu: AMD Ryzen 7 PRO 7840U w/ Radeon 780M GraphicsBenchmarkGetOpenPortsBenchmarkGetOpenPorts-16               1        1108555474 ns/opPASSok      github.com/intevel/linux-open-ports     1.123s
  • 新基准

goos: linuxgoarch: amd64pkg: github.com/intevel/linux-open-portscpu: AMD Ryzen 7 PRO 7840U w/ Radeon 780M GraphicsBenchmarkGetOpenPorts-16        1000000000               0.02906 ns/opPASSok      github.com/intevel/linux-open-ports     0.235s

6

 

总结

我会进一步研究这个问题,但上述更改已经带来了如此大的性能提升,我并没有打算就此打住。我的下一步将是删除 fmt.Sprintffor 循环主体中的。在此之前,我会启动一个 pprof 分析器并调查热点函数。

推荐


如何将 Docker 镜像的大小减少 99.82%


24 年最快的 REST API Web 服务器:Node.js、Go、Rust 和 C# (.NET) 基准测试


原创不易,随手关注或者”在看“,诚挚感谢!


http://www.kler.cn/a/416782.html

相关文章:

  • 浅谈C#库之DevExpress
  • webrtc视频会议学习(三)
  • Flink在Linux系统上的安装与入门
  • 去中心化物理基础设施网络(DePIN):重塑未来的基石
  • SQL基础入门——SQL基础语法
  • k8s 20版本以上,有了 CoreDNS作为域名解析服务器了,pod通过域名相互访问,需要额外配置dns条目吗
  • 网络安全(一):信息收集之玩转nmap(理论篇)
  • C++多态---面向对象的心动信号:多态之美
  • Vue 将推出「无虚拟DOM」版本,又是新的前端框架趋势?
  • uniapp中父组件数组更新后与页面渲染数组不一致实战记录
  • 力扣98:验证二叉搜索树
  • [CTF/网络安全] 攻防世界 upload1 解题详析
  • js 中 file 文件 应用
  • 应急响应靶机——Windows挖矿事件
  • Istio笔记01--快速体验Istio
  • 使用 Spring AI + Elasticsearch 让 RAG 变得简单
  • jmeter基础(超详细总结)
  • 第五讲:运算符与表达式:算术、关系、逻辑、赋值等运算符及其优先级
  • 超级灵感:前端页面功能统一管理方案
  • Flutter 权限申请
  • 数据结构——有序二叉树的构建遍历查找
  • 【iOS】多线程基础
  • 基于R语言森林生态系统结构、功能与稳定性分析与可视化
  • 搭建一个基于Web的文档管理系统,用于存储、共享和协作编辑文档
  • 面试小札:ThreadLocal底层实现原理和具体应用场景
  • 浅谈JAVA中的Random随机数