譯注:
并發與并行:并發是虛擬的并行,比如通過時間切片技術在單核CPU上運行多個任務,讓每個使用者“以為”自己在獨占這一CPU資源;并行是實際的同一時間多任務同時運行,多數是指在多核CPU的場景下。
隊列與雙端隊列:隊列遵循先入先出的原則,從一端存數,從另一端取數,雙端隊列支持從隊列的兩端存數和取數。
阻塞和非阻塞:阻塞和非阻塞描述了程序等待返回結果時的狀態,阻塞代表不返回結果就掛起,不進行任何操作;非阻塞是在沒返回結果時可以執行其他任務。
合作和搶占:高優先級任務可以打斷其他正在運行的低優先級任務,則調度器是搶占式的;反之,則是合作式的。
服務端編程的陣營中有很多新面孔,一水兒的谷歌血統。在谷歌開始將Golang應用于其產品系統后,Golang快速的吸引了大量的關注。隨著微服務架構的興起,人們開始關注一些現代的數據通信解決方案,如gRPC和Protobuf。在本文中,我們會對以上這些概念作一些簡要的介紹。
一、Golang
Golang又稱Go語言,是一個開源的、多用途的編程語言,由Google研發,并由于種種原因,正在日益流行。Golang已經有10年的歷史,并且據Google稱已經在生產環境中使用了接近7年的時間,這一點可能讓大多數人大跌眼鏡。
Golang的設計理念是,簡單、現代、易于理解和快速上手。Golang的創造者將Golang設計為一個普通的程序員可以在一個周末的時間就可以掌握,并達到使用Golang進行工作的程度。這一點我已經親身證實。Golang的創造者,都是C語言原始草案的專家組成員,可以說,Golang根紅苗正,值得信賴。
理都懂,但話說回來,為什么我們需要另一門編程語言呢
多數場景下,確實并不需要。事實上,Go語言并不能解決其他語言或工具無法解決的新問題。但在一些強調效率、優雅與直觀的場景下,人們通常會面臨一系列的相關問題,這正是Go所致力于解決的領域。Go的主要特點是:
一流的并發支持
內核十分簡單,語言優雅、現代
高性能
提供現代軟件開發所需要的原生工具支持
我將簡要介紹Go是如何提供上述的支持的。在Go語言的官網可以了解更多的特性和細節。
一流的并發支持
并發是多數服務端應用所需要考慮的主要問題之一,考慮到現代微處理器的特性,并發也成為編程語言的主要關切之一。Go語言引入了“goroutine”的理念。可以把“goroutine”理解為一個“輕量級的用戶空間線程”(現實中,當然遠比這要復雜得多,同一線程可能會附著多路的goroutine,但這樣的提法可以讓你有一個大致的概念)。所謂“輕量級”,可以這樣理解,由于采用了十分袖珍的堆棧,你可以同時啟動數以百萬計的goroutine,事實上這也是Go語言官方所推薦的方式。在Go語言中,任何函數或方法都可以生成一個goroutine。比如,只需要運行“go myAsyncTask()”就可以從“myAsyncTask”函數生成一個goroutine。示例代碼如下:
// This function performs the given task concurrently by spawing a goroutine
// for each of those tasks.
func performAsyncTasks(task []Task) {
for _, task := range tasks {
// This will spawn a separate goroutine to carry out this task.
// This call is non-blocking
go task.Execute()
}
}
goroutineExample.go hosted with ? by GitHub
(左右滑動查看全部代碼)
怎么樣,是不是很簡單?Go是一門簡單的語言,因此注定是以這樣的方式來解決問題。你可以為每個獨立的異步任務生成一個goroutine而不需要顧慮太多事情。如果處理器支持多核運行,Go語言運行時會自動的以并行的方式運行所有的goroutine。那么,goroutine之間是如何通信的呢,答案是channel。
“channel”也是Go語言的一個概念,用于進行goroutine之間的通信。通過channel,你可以向另一個goroutine傳遞各種信息(比如Go語言概念里的type或者struct甚至是channel)。一個channel大體上是一個“雙端阻塞隊列”(也可以單端的)。如果需要goroutine基于特定條件觸發下一步的行動,也可以利用channel來實現goroutine的協作阻塞任務模式。
在編寫異步或者并發的代碼時,goroutine和channel這兩個概念賦予了編程者大量的靈活性和簡便性。可以籍此很容易的建立其他很有用的庫,比如goroutine pool,舉個簡單的例子:
package executor
import (
"log"
"sync/atomic"
)
// The Executor struct is the main executor for tasks.
// 'maxWorkers' represents the maximum number of simultaneous goroutines.
// 'ActiveWorkers' tells the number of active goroutines spawned by the Executor at given time.
// 'Tasks' is the channel on which the Executor receives the tasks.
// 'Reports' is channel on which the Executor publishes the every tasks reports.
// 'signals' is channel that can be used to control the executor. Right now, only the termination
// signal is supported which is essentially is sending '1' on this channel by the client.
type Executor struct {
maxWorkers int64
ActiveWorkers int64
Tasks chan Task
Reports chan Report
signals chan int
}
// NewExecutor creates a new Executor.
// 'maxWorkers' tells the maximum number of simultaneous goroutines.
// 'signals' channel can be used to control the Executor.
func NewExecutor(maxWorkers int, signals chan int) *Executor {
chanSize := 1000
if maxWorkers > chanSize {
chanSize = maxWorkers
}
executor := Executor{
maxWorkers: int64(maxWorkers),
Tasks: make(chan Task, chanSize),
Reports: make(chan Report, chanSize),
signals: signals,
}
go executor.launch()
return &executor
}
// launch starts the main loop for polling on the all the relevant channels and handling differents
// messages.
func (executor *Executor) launch() int {
reports := make(chan Report, executor.maxWorkers)
for {
select {
case signal := <-executor.signals:
if executor.handleSignals(signal) == 0 {
return 0
}
case r := <-reports:
executor.addReport(r)
default:
if executor.ActiveWorkers < executor.maxWorkers && len(executor.Tasks) > 0 {
task := <-executor.Tasks
atomic.AddInt64(&executor.ActiveWorkers, 1)
go executor.launchWorker(task, reports)
}
}
}
}
// handleSignals is called whenever anything is received on the 'signals' channel.
// It performs the relevant task according to the received signal(request) and then responds either
// with 0 or 1 indicating whether the request was respected(0) or rejected(1).
func (executor *Executor) handleSignals(signal int) int {
if signal == 1 {
log.Println("Received termination request...")
if executor.Inactive() {
log.Println("No active workers, exiting...")
executor.signals <- 0
return 0
}
executor.signals <- 1
log.Println("Some tasks are still active...")
}
return 1
}
// launchWorker is called whenever a new Task is received and Executor can spawn more workers to spawn
// a new Worker.
// Each worker is launched on a new goroutine. It performs the given task and publishes the report on
// the Executor's internal reports channel.
func (executor *Executor) launchWorker(task Task, reports chan<- Report) {
report := task.Execute()
if len(reports) < cap(reports) {
reports <- report
} else {
log.Println("Executor's report channel is full...")
}
atomic.AddInt64(&executor.ActiveWorkers, -1)
}
// AddTask is used to submit a new task to the Executor is a non-blocking way. The Client can submit
// a new task using the Executor's tasks channel directly but that will block if the tasks channel is
// full.
// It should be considered that this method doesn't add the given task if the tasks channel is full
// and it is up to client to try again later.
func (executor *Executor) AddTask(task Task) bool {
if len(executor.Tasks) == cap(executor.Tasks) {
return false
}
executor.Tasks <- task
return true
}
// addReport is used by the Executor to publish the reports in a non-blocking way. It client is not
// reading the reports channel or is slower that the Executor publishing the reports, the Executor's
// reports channel is going to get full. In that case this method will not block and that report will
// not be added.
func (executor *Executor) addReport(report Report) bool {
if len(executor.Reports) == cap(executor.Reports) {
return false
}
executor.Reports <- report
return true
}
// Inactive checks if the Executor is idle. This happens when there are no pending tasks, active
// workers and reports to publish.
func (executor *Executor) Inactive() bool {
return executor.ActiveWorkers == 0 && len(executor.Tasks) == 0 && len(executor.Reports) == 0
}
executor.go hosted with ? by GitHub
(左右滑動查看全部代碼)
內核十分簡單,語言優雅、現代
與其他多數的現代語言不同,Golang本身并沒有提供太多的特性。事實上,嚴格限制特性集的范圍正是Go語言的顯著特征,且Go語言著意于此。Go語言的設計與Java的編程范式不同,也不支持如Python一樣的多語言的編程范式。Go只是一個編程的骨架結構。除了必要的特性,其他一無所有。
看過Go語言之后,第一感覺是其不遵循任何特定的哲學或者設計指引,所有的特性都是以引用的方式解決某一個特定的問題,不會畫蛇添足做多余的工作。比如,Go語言提供方法和接口但沒有類;Go語言的編譯器生成動態鏈接庫,但同時保留垃圾回收器;Go語言有嚴格的類型但不支持泛型;Go語言有一個輕量級的運行時但不支持異常。
Go的這一設計理念的主要用意在于,在表達想法、算法或者編碼的環節,開發者可以盡量少想或者不去想“在某種編程語言中處理此事的最佳方案”,讓不同的開發者可以更容易理解對方的代碼。不支持泛型和異常使得Go語言并不那么完美,也因此在很多場景下束手束腳 ,因此在“Go 2”版本中,官方加入了對這些必要特性的考慮。
高性能
單線程的執行效率并不足以評估一門語言的優劣,當語言本身聚焦于解決并發和并行問題的時候尤其如此。即便如此,Golang還是跑出了亮眼的成績,僅次于一些硬核的系統編程語言,如C/C++/Rust等等,并且Golang還在不斷的改進。考慮到Go是有垃圾回收機制的語言,這一成績實際上相當的令人印象深刻,這使得Go語言的性能可以應付幾乎所有的使用場景。
(Image Source: Medium)
提供現代軟件開發所需要的原生工具支持
是否采用一種新的語言或工具,直接取決于開發者體驗的好壞。就Go語言來說,其工具集是用戶采納的主要考量。同最小化的內核一樣,Go的工具集也采用了同樣的設計理念,最小化,但足夠應付需要。執行所有Go語言工具,都采用 go 命令及其子命令,并且全部是以命令行的方式。
Go語言中并沒有類似pip或者npm這類包管理器。但只需要下面的命令,就可以得到任何的社區包:
go get github.com/farkaskid/WebCrawler/blob/master/executor/executor.go
(左右滑動查看全部代碼)
是的,這樣就行。可以直接從Github或其他地方拉取所需要的包。所有的包都是源代碼文件的形態。
對于package.json這類的包,我沒有看到與 goget 等價的命令。事實上也沒有。在Go語言中,無須在一個單一文件中指定所有的依賴,可以在源文件中直接使用下面的命令:
import "github.com/xlab/pocketsphinx-go/sphinx"
(左右滑動查看全部代碼)
那么,當執行go build命令的時候,運行時會自動的運行 goget 來獲取所需要的依賴。完整的源碼如下:
package main
import (
"encoding/binary"
"bytes"
"log"
"os/exec"
"github.com/xlab/pocketsphinx-go/sphinx"
pulse "github.com/mesilliac/pulse-simple" // pulse-simple
)
var buffSize int
func readInt16(buf []byte) (val int16) {
binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &val)
return
}
func createStream() *pulse.Stream {
ss := pulse.SampleSpec{pulse.SAMPLE_S16LE, 16000, 1}
buffSize = int(ss.UsecToBytes(1 * 1000000))
stream, err := pulse.Capture("pulse-simple test", "capture test", &ss)
if err != nil {
log.Panicln(err)
}
return stream
}
func listen(decoder *sphinx.Decoder) {
stream := createStream()
defer stream.Free()
defer decoder.Destroy()
buf := make([]byte, buffSize)
var bits []int16
log.Println("Listening...")
for {
_, err := stream.Read(buf)
if err != nil {
log.Panicln(err)
}
for i := 0; i < buffSize; i += 2 {
bits = append(bits, readInt16(buf[i:i+2]))
}
process(decoder, bits)
bits = nil
}
}
func process(dec *sphinx.Decoder, bits []int16) {
if !dec.StartUtt() {
panic("Decoder failed to start Utt")
}
dec.ProcessRaw(bits, false, false)
dec.EndUtt()
hyp, score := dec.Hypothesis()
if score > -2500 {
log.Println("Predicted:", hyp, score)
handleAction(hyp)
}
}
func executeCommand(commands ...string) {
cmd := exec.Command(commands[0], commands[1:]...)
cmd.Run()
}
func handleAction(hyp string) {
switch hyp {
case "SLEEP":
executeCommand("loginctl", "lock-session")
case "WAKE UP":
executeCommand("loginctl", "unlock-session")
case "POWEROFF":
executeCommand("poweroff")
}
}
func main() {
cfg := sphinx.NewConfig(
sphinx.HMMDirOption("/usr/local/share/pocketsphinx/model/en-us/en-us"),
sphinx.DictFileOption("6129.dic"),
sphinx.LMFileOption("6129.lm"),
sphinx.LogFileOption("commander.log"),
)
dec, err := sphinx.NewDecoder(cfg)
if err != nil {
panic(err)
}
listen(dec)
}
client.go hosted with ? by GitHub
(左右滑動查看全部代碼)
上述的代碼將把所有的依賴聲明與源文件綁定在一起。
如你所見,Go語言是如此的簡單、最小化但仍足夠滿足需要并且十分優雅。Go語言提供了諸多的直接的工具支持,既可用于單元測試,也可以用于benchmark的火焰圖。誠然,正如前面所講到的特性集方面的限制,Go語言也有其缺陷。比如, goget 并不支持版本化,一旦源文件中引用了某個URL,就將鎖定于此。但是,Go也還在逐漸的演進,一些依賴管理的工具也正在涌現。
Golang最初是設計用來解決Google的一些產品問題,比如厚重的代碼庫,以及滿足編寫高效并發類應用的急迫需求。在需要利用現代處理器的多核特性的場景,Go語言使得在應用和庫文件的編程方面變得更加容易。并且,這些都不需要開發者來考慮。Go語言是一門現代的編程語言,簡單是其主旨,Go語言永遠不會考慮超過這一主旨的范疇。
二、Protobuf(Protocol Buffers)
Protobuf 或者說 Protocol Buffers是由Google研發的一種二進制通信格式,用以對結構化數據進行序列化。格式是什么意思?類似于JSON這樣?是的。Protobuf已經有10年的歷史,在Google內部也已經使用了一段時間。
既然已經有了JSON這種通信格式,并且得到了廣泛的應用,為什么需要Protobuf?
與Golang一樣,Protobuf實際上并有解決任何新的問題,只是在解決現有的問題方面更加高效,更加現代化。與Golang不同的是,Protobuf并不一定比現存的解決方案更加優雅。下面是Protobuf的主要特性:
Protobuf是一種二進制格式,不同于JSON和XML,后者是基于文本的也因此相對比較節省空間。
Protobuf提供了對于schema的精巧而直接的支持
Protobuf為生成解析代碼和消費者代碼提供直接的多語言支持。
Protobuf的二進制格式帶來的是傳輸速度方面的優化
那么Protobuf是不是真的很快?簡單回答,是的。根據Google Developer的數據,相對于XML來說,Protobuf在體積上只有前者的1/3到1/10,在速度上卻要快20到100倍。毋庸置疑的是,由于采用了二進制格式,序列化的數據對于人類來說是不可讀的。
(Image Source: Beating JSON performance with Protobuf)
相對其他傳輸協議格式來說,Protobuf采用了更有規劃性的方式。首先需要定義 .proto 文件,這種文件與schema類似,但更強大。在 .proto 文件中定義消息結構,哪些字段是必選的哪些是可選的,以及字段的數據類型等。接下來,Protobuf編譯器會生成用于數據訪問的類,開發者可以在業務邏輯中使用這些類來更方便的進行數據傳輸。
觀察某個服務的 .proto 文件,可以清晰的獲知通信的細節以及暴露的特性。一個典型的 .proto 文件類似如下:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
protobufExample.proto hosted with ? by GitHub
(左右滑動查看全部代碼)
曝個料:Stack Overflow的大牛Jon Skeet也是Protobuf項目的主要貢獻者之一。
三、gRPC
gRPC,物如其名,是一種功能齊備的現代的RPC框架,提供了諸多內置支持的機制,如負載均衡、跟蹤、健康檢查和認證等。gRPC由Google在2015年開源,并由此日益火爆。
既然已經有了REST,還搞個RPC做什么?
在SOA架構的時代,有相當長的時間,基于WSDL的SOAP協議是系統間通信的解決方案。彼時,通信協議的定義是十分嚴格的,龐大的單體架構系統暴露大量的接口用于擴展。
隨著B/S理念的興起,服務器和客戶端開始解耦,在這樣的架構下,即使客戶端和服務端分別進行獨立的編碼,也不影響對服務的調用。客戶端想查詢一本書的信息,服務端會根據請求提供相關的列表供客戶端瀏覽。REST范式主要解決的就是這種場景下的問題,REST允許服務端和客戶端可以自由的通信,而不需要定義嚴格的契約以及獨有的語義。
從某種意義上講,此時的服務已經開始變得像是單體式架構系統一樣,對于某個特定的請求,會返回一坨毫無必要的數據,用以滿足客戶端的“瀏覽”需求。但這并不是所有場景下都會發生的情況,不是么?
跨入微服務的時代
采用微服務架構理由多多。最常提及的事實是,單體架構太難擴展了。以微服務架構設計大型系統,所有的業務和技術需求都傾向于實現成互相合作的組件,這些組件就是“微”服務。
微服務不需要以包羅萬象的信息響應用戶請求,而僅需要根據請求完成特定的任務并給出所需要的回應。理想情況下,微服務應該像一堆可以無縫組裝的函數。
使用REST做為此類服務的通信范式變得不那么有效。一方面,采用REST API確實可以讓服務的表達能力更強,但同時,如果這種表達的能力既非必要也并不出自設計者的本意,我們就需要根據不同的因素考慮其他范式了。
gRPC嘗試在如下的技術方面改進傳統的HTTP請求:
默認支持HTTP/2協議,并可以享受該協議帶來的所有好處
采用Protobuf格式用于機器間通信
得益于HTTP/2協議,提供了對流式調用的專有支持
對所有常用的功能提供了插件化的支持,如認證、跟蹤、負載均衡和健康檢查等。
當然,既然是RPC框架,仍舊會有服務定義和接口描述語言(DSL)的相關概念,REST世代的開發者可能會感覺這些概念有些格格不入,但是由于gRPC采用Protobuf做為通信格式,就不會顯得像以前那么笨拙。
Protobuf的設計理念使得其既是一種通信格式,又可以是一種協議規范工具,在此過程中無需做任何額外的工作。一個典型的gRPC服務定義類似如下:
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
serviceDefinition.proto hosted with ? by GitHub
(左右滑動查看全部代碼)
只需要為服務定義一個 .proto 文件,并在其中描述接口名稱,服務的需求,以及以Protobuf格式返回的消息即可。Protobuf編譯器會生成客戶端和服務端代碼。客戶端可以直接調用這些代碼,服務端可以用這些代碼實現API來填充業務邏輯。
四、結語
Golang,Protobuf和gRPC是現代服務端編程的后起之秀。Golang簡化了并發/并行應用的編程,gRPC和Protobuf的結合提供了更高效的通信并同時提供了更愉悅的開發者體驗。
-
代碼
+關注
關注
30文章
4825瀏覽量
69043 -
服務端
+關注
關注
0文章
66瀏覽量
7056
發布評論請先 登錄
相關推薦
評論