如果你不是在石頭下住著,那麼你也應該聽過最近興起一種新的對“函數作為服務”的理解。在開源社區,Alex Ellis 的 [OpenFaas](https://github.com/openfaas/faas) 項目受到了很高的關注,並且 [亞馬遜Lambda宣布對Go語言的支援](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/)。這些系統允許你按需擴容,並且通過 API 呼叫的方式來調用你的 CLI 程式。## Lambda/Faas 背後的動機讓我們這麼來描述 - 整個“無伺服器”運動是雲平台,比如 AWS,的市場營銷行為,它允許你將任一個伺服器管理移交給它們,比如,理想情況下,你收入系統的一小部分。在具體的條款上,這意味著 AWS 或其他類似的解決方案 託管的你的應用,運行你的應用,並且在他們的資料中心上根據需要自動維護它的硬體規模。但是,你可能早就知道這些了。但是,你是否知道,這種能力早就在 CGI 中存在?參考維基百科,1993,並且在 1997 正式以 RFC 規範定下來的定義。所有舊的東西又捲土重來了。 CGI(Common Gateway Interface) 的目的是:> 在計算上,通用閘道介面 (CGI) 為網站伺服器提供一個標準的協議,用以在動態網頁伺服器上像執行控制台應用(也稱為命令列介面程式)一樣執行應用程式。> > 來源: [維基百科](https://en.wikipedia.org/wiki/Common_Gateway_Interface)> 對 Go 語言來說,最簡單的 Fass/CGI 服務程式只需要 10 行左右的代碼就能實現。Go 語言的標準庫中已經包含了 [net/http/cgi](https://golang.org/pkg/net/http/cgi/) 來完成所有的困難工作。實現一個 PHP CGI 只需要如下寥寥數行:```gofunc phpHandler() func(w http.ResponseWriter, r *http.Request) {return func(w http.ResponseWriter, r *http.Request) {handler := new(cgi.Handler)handler.Dir = "/var/www/"handler.Path = handler.Dir + "api"args := []string{r.RequestURI}handler.Args = append(handler.Args, args...)fmt.Printf("%v", handler.Args)handler.ServeHTTP(w, r)}}```調用這個也是非常簡單的:```gohttp.HandleFunc("/api/", phpHandler())```當然,我不清楚為什麼你會使用它。因為在那時黎明前的時刻, CGI 碰到了效能問題,其中最大的問題是它在系統上產生的壓力。對於每個請求,就會發起一個 `os.Exec` 調用,而這絕對不是一個系統友好的調用。實際上,如果你在做任何類似即時交通這樣的服務,你很可能希望這樣的調用數是 0 。這就是為什麼 CGI 進化為了 FastCGI。> FastCGI 是通用閘道介面 (CGI) 早期的一種變化; FastCGI 的主要目的是減少網頁伺服器和 CGI 程式之間的過度的聯絡,讓伺服器能一次性處理更多的網頁請求> > 來源:[維基百科](https://en.wikipedia.org/wiki/FastCGI) 我現在不想實現一個 FastCGI 程式(在標準庫中也有一個 [net/http/fcgi](https://golang.org/pkg/net/http/fcgi/)) ,但我想要示範這種實現會帶來的效能陷阱。當然,你在 AWS 上運行你的程式時,你可能不怎麼關心這個,因為它們有能力按照你的訪問量來擴容硬體。## CGI 的解決辦法 如果這幾年我有學到點東西的話,那一定是大多數的服務是資料驅動的。這意味著,一定有某種資料庫儲存著至關重要的資料。根據一個 [Quora上的回答](https://www.quora.com/Which-database-system-s-does-Twitter-use) , Twitter 使用了至少8種不同類型的資料庫,從 MySQL,Cassandra 和 Redis,到其他更複雜的資料庫。實際上,我的大部分工作大概是這樣的,從資料庫中讀取員工資料,然後把它變成 json 格式並且通過 REST 調用提供出去,這些查詢通常不能僅僅用一條 SQL 陳述式來實現,當然在很多情況下也是可以的。那麼,不如我們寫一些不會有 `os.Exec` 調用成本的 SQL 指令碼來實現一些功能,而不是用 CGI 程式來實現它們? 挑戰接受了。 ## 以 SQL 作為 API 我並不想把這個變成龐然大物,雖然 以 SQL 作為 API 是有能力成為的,但我確實想要實現一個遠程可用的版本。我希望通過在磁碟上建立一個 .sql 檔案來實現 API 呼叫,並且我還希望這個 API 可以調用 http 請求中的任意參數。這意味著我們可以通過傳遞給 API 的參數來過濾結果集。我選擇了 MySQL 和 sqlx 來實現這個任務。最近我為 Twitch,Slack,Yotube,Discord 寫了一些聊天機器人,看起來很快我要寫一個 Teleganm 的版本了。其實他們的目的是對各種通道進行相似的串連,記錄訊息,加總一些統計資訊並且對一些命令或者問題進行反饋。對於用 Vue.js 寫成的前端網站,我們需要通過 API 來向他們傳遞一些資料。雖然不是所有的 API 都能用 SQL 來實現,但還是有很大一部分是可以的。比如:1. 列出所有的通道2. 通過通道ID列出通道這兩個調用相對來說很相似並且容易實現,我特別建立了兩個檔案,來提供這些資訊:`api/channels.sql` (對應於 `/api/channels` )```select * from channels````api/channelByID.sql` (對應於 `/api/channelByID?id=...` )```select * from channels where id=:id```就像你看到的這樣,用 SQL 查詢來實現一個新的 API 並不需要太多的工作。我嘗試設計一個系統,這樣一旦你建立了 `api/name.sql` 馬上就可以通過 `api/name` 訪問到。所有 Http 請求的參數被封裝在 `map[string]interface{}` 中,並且作為綁定變數傳遞給 SQL 查詢語句。SQL 驅動負責來處理這些參數。我也設計了錯誤資訊的格式化。如果你沒法串連到你的資料庫,或者一個 API 對應的 .sql 檔案不存在,會有一個如下的錯誤資訊返回:```json{"error": {"message": "open api/messages.sql: no such file or directory"}}```在 SQL 查詢中使用 URL 參數在 go 語言中獲得請求的參數,只需要在請求對象中的 `*url.URL` 結構體上調用 Query() 函數即可。這個函數返回 `url.Values` 對象,這是一個 `map[string][]string` 類型的別名。我們需要轉換這個對象並傳遞到 sqlx 的語句中。我們需要建立一個 `map[string]interface{}` 。因為我們需要調用的 sqlx 函數在查詢時接受這種格式的參數([sqlx.NamedStmt.Queryx](https://godoc.org/github.com/jmoiron/sqlx#NamedStmt.Queryx))。讓我們轉換它們並且發起查詢:```goparams := make(map[string]interface{})urlQuery := r.URL.Query()for name, param := range urlQuery {params[name] = param[0]}stmt, err := db.PrepareNamed(string(query))if err != nil {return err}rows, err := stmt.Queryx(params)if err != nil {return err}```我們還沒有處理的是 `rows` 變數,我們可以遍曆它以獲得每一個行的資訊。我們需要將它們加入到一個切片中,並且在 API 的最後步驟中把它們封裝到 JSON 裡面。```gofor rows.Next() {row := make(map[string]interface{})err = rows.MapScan(row)if err != nil {return err}```這兒是事情變得有趣的地方。每一行中包含的值都需要轉換成 JSON 編碼器能理解的東西。因為底層的類型是 `[]uint8` ,我們首先要把它們轉換成字串。如果我們不這麼做,這種結構的 JSON 會自動使用 base64 編碼。既然查詢的反饋可以用 `map[string]string` 來表示,並且 `uint8` 是 `byte` 類型的別名,我們選擇使用這種轉換:```gorowStrings := make(map[string]string)for name, val := range row {switch tval := val.(type) {case []uint8:ba := make([]byte, len(tval))for i, v := range tval {ba[i] = byte(v)}rowStrings[name] = string(ba)default:return fmt.Errorf("Unknown column type %s %#v", name, spew.Sdump(val))}}```這裡我們有一個 `rowStrings` 對象表示每個返回的 SQL 行,它可以輕鬆的編碼到 JSON 中。我們需要做的就是把它們添加到一個返回結果中,對它編碼並且返回編碼後的值。完整(相對短小)的代碼可以在 [titpetric/sql-as-a-service](https://github.com/titpetric/sql-as-an-api) 這裡擷取。### 使用須知雖然這種方法以資料庫作為 API 層的實現有獨特的好處,但是為了讓它適合被更廣泛的調用,還有許多使用者情境需要考慮。比如:### 對結果的排序這種方法其實不能進行排序。因為我們沒法把一個查詢參數綁定到 `order by` 參數中,因為 SQL 不允許這麼做。資料的清洗也是完全不可能的,你甚至都不能用函數來作為一個替代方法。這樣的代碼是完全的不可行的:`ORDER BY :column IF(:order = 'asc', 'asc', 'desc')`。### 參數為了創造某種分頁規則, MySQL 提供了 `LIMIT $offset, $length` 從句。雖然你可以將這些作為查詢參數,但是在這裡我們沒法綁定它們,或者找到一種傳遞它們值的辦法,我們嘗試做的結果是得到類似這樣的錯誤返回資訊:“未定義的變數...”### 多條 SQL 查詢通常來說,我們會執行多條 SQL 陳述式來返回單一的一個結果集。然而這需要在在資料庫上被配置為 enabled 狀態,而這個特性通常是被禁止的,其中一個主要的原因是為了防止 SQL 插入式攻擊。在一個理想的世界裡,類似下面的語句應該是可以執行的:```set @start=:start;set @length=:length;select * from channels order by id desc limit @start, @length;```可惜,現實中它行不通。上面的語句甚至都沒法遠程執行。如果你在一個 MySQL 用戶端中嘗試執行這些語句,它將報錯。這些變數被定義了,但是它們既不能在 order by 從句也不能在 limit 從句中使用。那麼,就沒法分頁了嗎?有一個我見過的最奇怪的曲線救國的辦法, Twitch 和 Tumblr 的 API 有一個特別的特性,它們允許你傳遞一個 `since` 或者 `previousID` 的值。它們可以類似這樣的 SQL 中起作用:```select * from messages where id < :previousID order by id desc limit 0, 20```這讓你可以按照預先定好的分頁大小來遍曆一個表。這種方法要求表裡面有個可排序的主鍵(比如 sonyflake / snowflake 演算法生產的 ID)或者有另外一個可用於排序的順序列。### 函數SQL 資料庫並不是蠢笨的野獸,實際上它們非常強大,強大的源頭之一就是可以建立函數,或者,過程。用這個你可以實現一個複雜的商務邏輯。對傳統的 DBA ,或資料庫程式員來說,讀取或者自己建立一個 SQL 函數都是很容易的。這是一個很好的解決在用戶端無法一次執行多條 SQL 陳述式限制的辦法,但是它要求你把所有的商務邏輯在資料庫中實現。這對我所知的大部分程式員來說,都是超出了他們舒適區的事兒,基本上要與一大堆的 IF 語句打交道。## 結論如果你真的有一些簡單的查詢,通過它們就能獲得你需要的結果的話,那麼你可以從這種以 SQL 作為 API 的方法中大大受益,除了節約系統開銷外,你還可以提供給那些不熟悉 Go 語言但是熟悉 SQL 的程式員們一個增加系統功能的方法。隨著要求一個 API 返回特定結果的需求的逐漸增加,我們需要一種指令碼語言,它能達到甚至超越 [PL/SQL](https://en.wikipedia.org/wiki/PL/SQL) 的種種限制,並且在不同關係型資料庫中有不同的實現。或者,你在你的應用中一直掛接一個 JavaScript 的虛機就如同 [dop251/goja ](https://github.com/dop251/goja) 這樣,然後讓你們團隊的前景程式員來一次他/她可能永遠不會忘記的嘗試。現在也有用純粹 go 語言實現的 LUA 虛機,如果你想要某種比整個 ES5.1 小的“運行時”。### 如果我能在這裡遇到你...如果你能買一本我的書,那就太棒了:- [API Foundations in Go](https://leanpub.com/api-foundations)- [12 Factor Apps with Docker and Go](https://leanpub.com/12fa-docker-golang)- [The SaaS Handbook (創作中)](https://leanpub.com/saas-handbook)我保證你可以從中學到更多。買一本書能支援我寫更多類似的文章。對你購買我的書說聲謝謝。如果你想預約我的 顧問/作家 服務歡迎[給我發送郵件](black@scene-si.org),我擅長 APIs,Go,Docker,VueJs 和系統擴容,[以及很多其他的事情](https://scene-si.org/about)
via: https://scene-si.org/2018/02/07/sql-as-an-api/
作者:Tit Petric 譯者:MoodWu 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
288 次點擊