使用 go 語言開發 hive 導出工具
1 前言
新版 hive 提供了 beeline 工具,可以執行 SQL 並導出數據,不過操作還是有點複雜的,團隊裏有些同學不會 Linux 的基本操作,所以我花了億點點時間寫了個交互式的命令行工具方便使用。
2 效果
命令行工具,就是這麼樸實無華。
3 探索過程
一開始是打算用 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 開發。
4 環境配置
我的電腦裏已經有 go 的環境,不過爲了文章的完整性,還是記錄(水)一下😃
在 Windows 上推薦使用 scoop 來管理軟件
只要一行命令就可以安裝 go
scoop install go
因爲衆所周知的原因,國內網絡的特殊性,需要配置一下 go proxy
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
搞定,接下來安裝個 goland 或者 vscode 就可以開始愉快的寫代碼了。
5 使用 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
參數指定,查詢結果會出現在標準輸出,這時候重定向到文件裏,就完成了查詢結果導出。
6 分析一下
說起來很簡單,這個命令行工具就是提示用戶輸入 SQL 語句,然後保存到一個臨時文件裏,再執行上述的 beeline 命令,將標準輸出保存到一個文件裏,就完成了一個導出的流程。
7ASCII 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字符畫"
就用得好好的,不用轉來轉去。
8 讀取輸入
本來是很簡單的功能,但不知道爲啥實際用起來還是有一些坑的
一開始我用得是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 裏包含換行符。
9 讀取環境變量
使用 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
10 創建臨時目錄和文件
網上很多文章都是用 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())
11 生成 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 沒有用上。
一開始我是打算用來作爲輸出的文件名,不過後面覺得還是用日期時間更合適。
12 日期時間格式化
老生常談的問題,我直接用 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)
13 執行命令 + 輸出重定向
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 輸出就行。
搞定~
14 小結
好久沒寫 Go 了,設計得確實很簡單,屬於是優缺點都很明顯的語言。
最直接的就是良好的發佈部署體驗,.Net8 的 AOT 吹到天上去了但是實測發佈 AOT 的體驗還是比較一般,而且還挑 Linux 的版本,這也是我選擇使用 go 來開發這個工具的直接原因。
缺點裏面我感知最強的就是 go 的錯誤處理機制,err != nil
這個寫得好煩啊!雖然可以用 data, _ := xxx()
直接強行隱藏報錯,但還是不如 try
來得舒服。
就這樣吧,一個很簡單的小工具,因爲我對 go 不是很熟悉所以花了些時間探索。
15 參考資料
-
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter01/01.4.html
-
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter06/06.1.html
-
https://juejin.cn/post/7000925379145760782
-
https://www.cnblogs.com/mayanan/p/15342214.html
-
https://www.cnblogs.com/wongbingming/p/13984538.html
-
https://colobu.com/2020/12/27/go-with-os-exec
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Zx5F5vnYW1xDRt6O34p83g