6

使用go语言开发hive导出工具 - 程序设计实验室

 1 year ago
source link: https://www.cnblogs.com/deali/p/17770172.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言#

新版 hive 提供了 beeline 工具,可以执行SQL并导出数据,不过操作还是有点复杂的,团队里有些同学不会Linux的基本操作,所以我花了亿点点时间写了个交互式的命令行工具方便使用。

效果#

命令行工具,就是这么朴实无华。

866942-20231017170748125-251687264.png

探索过程#

一开始是打算用 bash 脚本,结果发现根本不会写,beeline 导出数据只能使用输出重定向,shell 脚本里面的重定向老是有问题,一直报错。我直接放弃了。

接着又打算换成 Python 来写,这个倒是很顺利,但主机终端似乎是旧版本的Ubuntu,自带的Python居然是2.7,Python2的语法写得我实在是烦,于是我打算用能编译成二进制的语言在本地写,然后上传到终端上去运行。

这时候想到了 C# ,.Net8 对 AOT 提供了很好的支持,可以当成 C++ 使用,不过我用下来还是有一些小坑,比如 AOT 模式只能在 Linux 下才能编译 Linux 版本的可执行文件,不能像 Go 那样在 Windows 下交叉编译 Linux 版的可执行文件,然后当我费尽周折编译好上传上去执行的时候又报错说 glibc 库找不到。

/lib64/libm.so.6: version `GLIBC_2.29' not found

我查了一下似乎是跟系统版本有关,最终还是放弃了,转向使用 go 开发。

环境配置#

我的电脑里已经有 go 的环境,不过为了文章的完整性,还是记录(水)一下😃

在 Windows 上推荐使用 scoop 来管理软件

只要一行命令就可以安装 go

scoop install go

因为众所周知的原因,国内网络的特殊性,需要配置一下 go proxy

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

搞定,接下来安装个 goland 或者 vscode 就可以开始愉快的写代码了。

使用 beeline#

使用 beeline 导出 hive 的查询结果,命令如下

beeline -u jdbc:hive2://host:port -n username -p password --incremental=true --verbose=false --fastConnect=true --silent=true --outputformat=csv2 -f ./temp.sql > result.csv

需要先把要执行的SQL保存在一个文件里,然后通过 -f 参数指定,查询结果会出现在标准输出,这时候重定向到文件里,就完成了查询结果导出。

分析一下#

说起来很简单,这个命令行工具就是提示用户输入 SQL 语句,然后保存到一个临时文件里,再执行上述的 beeline 命令,将标准输出保存到一个文件里,就完成了一个导出的流程。

ASCII Logo#

在实现正经功能之前,先来个花里胡哨的。

很多命令行工具启动的时候都会显示一个炫酷的 ASCII Logo ,我也要整一个😀

很多在线工具都提供了生成 ASCII 艺术字的功能,我使用的是这个: https://tools.kalvinbg.cn/txt/ascii

输入文字之后生成 ASCII 字符画,复制下来。我这里用的是 StarBlog ,就当是给我的博客宣传了~

然后需要使用不转义的字符串来保存,大部分语言都有这个功能,go 当然也不例外。

go 使用的是两个反引号,像这样

var str = `hello`

但是很快我发现了问题,ASCII字符画里面也有反引号字符,直接复制进去结果就是报错,除非手动做转义,太麻烦了。

最后我只能先把 ASCII字符画用 base64 编码,然后在程序里解码后打印出来。

记得先 import 这个 encoding/base64

const asciiTitle string = "base64"
var titleDec, _ = base64.StdEncoding.DecodeString(asciiTitle)
fmt.Printf("%s\n", titleDec)

这样就搞定了~

这点还是得吐槽一下,我第一次使用是 C# 来写这个工具,C# 使用 @"ASCII字符画" 就用得好好的,不用转来转去。

读取输入#

本来是很简单的功能,但不知道为啥实际用起来还是有一些坑的

一开始我用得是fmt.Scanln() 来读取

但有个问题,这个方法是遇到回车(换行)就读取结束,而很多 SQL 语句里都有包含换行,这就麻烦了。要不就只能要求输入的 SQL 语句不能含有换行,要不就只能换一种方式读取输入。

最终当然是换一种方式了,使用 bufio 库来读取流

reader := bufio.NewReader(os.Stdin)
fmt.Println("请输入要执行的SQL语句,以分号结束,之后按回车确认。")
line, err := reader.ReadBytes(';')
line = bytes.TrimRight(line, "\r\n")
sql := string(line)

以分号结束就可以允许 SQL 里包含换行符。

读取环境变量#

使用 beeline 命令是需要指定用户名和密码的,这部分不能写在代码里,我打算使用读取环境变量来实现。

但每次设置环境变量再运行太麻烦了,所以掏出了 dotenv 大法,go 的生态不算丰富,但大部分功能都能找到对应的库,这个 dotenv 我用的是 github.com/joho/godotenv 这个库

err := godotenv.Load()
if err != nil {
  log.Fatal("Error loading .env file", err.Error())
}

之后在程序的同级目录下创建 .env 文件,将环境变量保存在这个文件里就完事了

CONN_STR=jdbc:hive2://host:port
USERNAME=user
PASSWORD=pwd
OUTPUT_FORMAT=csv2

创建临时目录和文件#

网上很多文章都是用 ioutil.TempFile() 这类已经弃用的方法,事实上现在应该用 os 库提供的方法

首先创建临时目录

dir, err := os.MkdirTemp("", "hive-out-")
if err != nil {
  fmt.Printf("创建临时目录错误!%v\n", err)
  log.Fatal(err)
}

创建临时文件并写入

tempFile, err := os.CreateTemp(dir, "hive.*.sql")
if err != nil {
  fmt.Printf("创建临时文件错误!错误:%v\n", err)
  log.Fatal(err)
}

defer tempFile.Close()
defer os.Remove(tempFile.Name())
defer os.RemoveAll(dir)

data := []byte(sql)
if _, err := tempFile.Write(data); err != nil {
  fmt.Println("写入文件失败!", err.Error())
  log.Fatal(err)
}

log.Printf("创建临时文件 %s\n\n", tempFile.Name())

生成 UUID#

go 的标准库竟然没有提供 UUID 功能,这也许就是 go 的哲学 Simplicity

这块我使用的是 github.com/google/uuid 这个库,Google 出品应该没毛病。

u1, err := uuid.NewUUID()
if err != nil {
  log.Printf("创建UUID失败: %v\n", err)
  log.Fatal(err)
}
log.Printf("Session ID: %v\n\n", u1)

不过最后这个 UUID 没有用上。

一开始我是打算用来作为输出的文件名,不过后面觉得还是用日期时间更合适。

日期时间格式化#

老生常谈的问题,我直接用 fmt.Sprintf 方法,简单粗暴

t := time.Now()
timeStr := fmt.Sprintf("%04d-%02d-%02d_%02d-%02d-%02d-%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond())
outputPath := fmt.Sprintf("%s.csv", timeStr)

执行命令+输出重定向#

Linux 的输出有两种

  • stdout
  • stderr

对于 hive 的导出结果,我们只需要把 stdout 保存到文件里就行。

go 里面设计得很简单,直接创建个文件,将命令行的 stdout 重定向到文件就行。

outputFile, err := os.Create(outputPath)
if err != nil {
  log.Fatalf("创建文件失败: %v\n", err)
}

执行命令并重定向输出

log.Printf("正在执行SQL,请稍等...\n\n")
cmd := exec.Command(
  "beeline",
  "-u", os.Getenv("CONN_STR"),
  "-n", os.Getenv("USERNAME"),
  "-p", os.Getenv("PASSWORD"),
  "--incremental=true",
  "--verbose=true",
  "--fastConnect=true",
  "--silent=true",
  "--outputformat="+os.Getenv("OUTPUT_FORMAT"),
  "-f", tempFile.Name(),
)
cmd.Stderr = os.Stderr
cmd.Stdout = outputFile
err = cmd.Run()
if err != nil {
  log.Fatalf("cmd.Run() failed with %s\n", err)
}

defer outputFile.Close()

log.Printf("导出数据到:%s\n\n", outputPath)
log.Println("搞定!")

stderr 不需要保存到文件里,所以重定向到 stderr 输出就行。

小结#

好久没写 Go 了,设计得确实很简单,属于是优缺点都很明显的语言。

最直接的就是良好的发布部署体验,.Net8 的 AOT 吹到天上去了但是实测发布 AOT 的体验还是比较一般,而且还挑 Linux 的版本,这也是我选择使用 go 来开发这个工具的直接原因。

缺点里面我感知最强的就是 go 的错误处理机制,err != nil 这个写得好烦啊!虽然可以用 data, _ := xxx() 直接强行隐藏报错,但还是不如 try 来得舒服。

就这样吧,一个很简单的小工具,因为我对 go 不是很熟悉所以花了些时间探索。

参考资料#


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK