現代服務器端堆棧簡介-Golang,Protobuf和gRPC


現代服務器端堆棧簡介-Golang,Protobuf和gRPC

鎮上有一些新的服務器編程人員,而這次全都與Google有關。 自從Google開始將Golang用於自己的生產系統以來,Golang迅速受到歡迎。 自從微服務架構誕生以來,人們一直專注於gRPC和Protobuf等現代數據通信解決方案。 在這篇文章中,我將向您簡要介紹所有這些內容。

Golang

Golang或Go是Google的一種開源通用編程語言。 由於種種原因,它最近越來越受歡迎。 據谷歌稱,對於大多數人來說,這可能是一個令人驚訝的發現,這種語言已經使用了將近10年,並且已經可以投入生產將近7年。

Golang被設計為簡單,現代,易於理解和快速掌握。 語言的創建者以一種普通程序員可以在週末掌握該語言的方式來設計它。 我可以證明他們確實成功了。 說到創建者,這些都是參與C語言原始草案的專家,因此我們可以放心,這些人知道他們在做什麼。

很好,但是為什麼我們需要另一種語言?

對於大多數用例,我們實際上沒有。 實際上,Go並不能解決以前其他語言/工具尚未解決的任何新問題。 但是它確實試圖以有效,優雅和直觀的方式解決人們通常面臨的一系列特定的相關問題。 Go的主要重點是:

· 一流的併發支持

· 優雅,現代的語言,對其核心非常簡單

· 性能很好

· 對現代軟件開發所需工具的第一手支持

我將簡要說明Go如何提供上述所有功能。 您可以從Go的官方網站上詳細瞭解該語言及其功能。

併發

併發是大多數服務器應用程序中的主要問題之一,考慮到現代微處理器,它應該是該語言的主要問題。 Go引入了稱為" goroutine"的概念。 " goroutine"類似於"輕量級用戶空間線程"。 它比實際複雜得多,因為多個goroutine在單個線程上多路複用,但是上面的表達式應該為您提供一個總體思路。 這些足夠輕,您實際上可以同時啟動一百萬個goroutine,因為它們從很小的堆棧開始。 實際上,這是推薦的。 Go中的任何函數/方法都可用於生成Goroutine。 您只需執行" go myAsyncTask()",即可從" myAsyncTask"函數中生成goroutine。 以下是一個示例:

<code>// 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()
}
}/<code>


是的,就這麼簡單,它的意思就是那樣,因為Go是一種簡單的語言,並且您會為每一個獨立的異步任務生成一個goroutine,而無需太在意。 如果有多個內核,Go的運行時將自動並行運行goroutine。 但是這些goroutine如何通信? 答案是通道。

"通道"也是一種語言原語,旨在用於goroutine之間的通信。 您可以將任何內容從一個通道傳遞到另一個goroutine(原始Go類型或Go結構或其他通道)。 通道本質上是一個阻塞的雙端隊列(也可以是單端)。 如果您希望goroutine在繼續滿足某個條件之前等待某個條件,則可以在通道的幫助下實現goroutine的協作阻塞。

這兩個原語在編寫異步或並行代碼時提供了很大的靈活性和簡便性。 其他幫助程序庫(例如goroutine池)可以從上述原語輕鬆創建。 一個基本的例子是:

<code>package executor

import (
\t"log"
\t"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 {
\tmaxWorkers int64
\tActiveWorkers int64

\tTasks chan Task
\tReports chan Report
\tsignals 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 {
\tchanSize := 1000

\tif maxWorkers > chanSize {
\t\tchanSize = maxWorkers
\t}

\texecutor := Executor{
\t\tmaxWorkers: int64(maxWorkers),
\t\tTasks: make(chan Task, chanSize),
\t\tReports: make(chan Report, chanSize),
\t\tsignals: signals,
\t}

\tgo executor.launch()

\treturn &executor
}

// launch starts the main loop for polling on the all the relevant channels and handling differents
// messages.
func (executor *Executor) launch() int {
\treports := make(chan Report, executor.maxWorkers)

\tfor {
\t\tselect {
\t\tcase signal := \t\t\tif executor.handleSignals(signal) == 0 {
\t\t\t\treturn 0
\t\t\t}

\t\tcase r := \t\t\texecutor.addReport(r)

\t\tdefault:
\t\t\tif executor.ActiveWorkers < executor.maxWorkers && len(executor.Tasks) > 0 {
\t\t\t\ttask := \t\t\t\tatomic.AddInt64(&executor.ActiveWorkers, 1)
\t\t\t\tgo executor.launchWorker(task, reports)
\t\t\t}
\t\t}
\t}
}

// 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 {
\tif signal == 1 {
\t\tlog.Println("Received termination request...")

\t\tif executor.Inactive() {
\t\t\tlog.Println("No active workers, exiting...")
\t\t\texecutor.signals \t\t\treturn 0
\t\t}

\t\texecutor.signals \t\tlog.Println("Some tasks are still active...")
\t}

\treturn 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\treport := task.Execute()

\tif len(reports) < cap(reports) {
\t\treports \t} else {
\t\tlog.Println("Executor's report channel is full...")
\t}

\tatomic.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 {
\tif len(executor.Tasks) == cap(executor.Tasks) {
\t\treturn false
\t}


\texecutor.Tasks \treturn 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 {
\tif len(executor.Reports) == cap(executor.Reports) {
\t\treturn false
\t}

\texecutor.Reports \treturn 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 {
\treturn executor.ActiveWorkers == 0 && len(executor.Tasks) == 0 && len(executor.Reports) == 0
}/<code>


簡單的語言

與許多其他現代語言不同,Golang沒有很多功能。 實際上,可以說出一種令人信服的理由,即該語言對其功能集的限制過於嚴格,這是故意的。 它不是圍繞像Java這樣的編程範例來設計的,也不是為了支持像Python這樣的多種編程範例而設計的。 只是簡單的結構編程。 語言中僅包含了基本功能,僅此而已。

查看該語言後,您可能會覺得該語言沒有遵循任何特定的哲學或方向,並且感覺這裡包含了解決特定問題的所有功能,僅此而已。 例如,它具有方法和接口,但沒有類; 編譯器生成靜態鏈接的二進制文件,但仍具有垃圾回收器; 它具有嚴格的靜態類型,但不支持泛型。 該語言的運行時很精簡,但不支持例外。

這裡的主要思想是,開發人員應該花費最少的時間將他/她的思想或算法表達為代碼,而不用考慮"用x語言做到這一點的最佳方法是什麼?" 對其他人來說應該很容易理解。 它仍然不是完美的,確實時有限制,並且" Go 2"正在考慮某些基本功能,例如"泛型"和"異常"。

性能

單線程執行性能不是判斷語言的好指標,尤其是當語言關注併發和並行性時。 但是,Golang仍然擁有令人印象深刻的基準數字,只有C,C ++,Rust等硬核系統編程語言才能擊敗它,並且它還在不斷改善。 考慮到它是垃圾收集語言,其性能實際上是非常令人印象深刻的,並且對於幾乎每個用例都足夠好。

現代服務器端堆棧簡介-Golang,Protobuf和gRPC

(Image Source: Medium)


開發人員工具

採用新工具/語言直接取決於其開發人員的經驗。 Go的採用確實代表了它的工具。 在這裡,我們可以看到相同的想法和工具很少,但是足夠了。 這一切都可以通過" go"命令及其子命令來實現。 全部都是命令行。

沒有像pip,npm這樣的語言的軟件包管理器。 但是,您只需執行以下操作即可獲取任何社區軟件包

go get github.com/farkaskid/WebCrawler/blob/master/executor/executor.go


是的,它有效。 您可以直接從GitHub或其他任何地方提取軟件包。 它們只是源文件。

但是package.json ..呢? 我看不到"去得到"的任何等效內容。 因為沒有。 您無需在單個文件中指定所有依賴項。 您可以直接使用:

import "github.com/xlab/pocketsphinx-go/sphinx"


在您的源文件本身中,當您執行"生成"時,它將自動為您"獲取"它。 您可以在此處查看完整的源文件:

<code>package main

import (
\t"encoding/binary"
\t"bytes"
\t"log"
\t"os/exec"

\t"github.com/xlab/pocketsphinx-go/sphinx"
\tpulse "github.com/mesilliac/pulse-simple" // pulse-simple
)

var buffSize int

func readInt16(buf []byte) (val int16) {
\tbinary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &val)
\treturn
}

func createStream() *pulse.Stream {
\tss := pulse.SampleSpec{pulse.SAMPLE_S16LE, 16000, 1}
\tbuffSize = int(ss.UsecToBytes(1 * 1000000))
\tstream, err := pulse.Capture("pulse-simple test", "capture test", &ss)
\tif err != nil {
\t\tlog.Panicln(err)
\t}
\treturn stream
}

func listen(decoder *sphinx.Decoder) {
\tstream := createStream()
\tdefer stream.Free()
\tdefer decoder.Destroy()
\tbuf := make([]byte, buffSize)
\tvar bits []int16

\tlog.Println("Listening...")

\tfor {
\t\t_, err := stream.Read(buf)
\t\tif err != nil {
\t\t\tlog.Panicln(err)
\t\t}

\t\tfor i := 0; i < buffSize; i += 2 {
\t\t\tbits = append(bits, readInt16(buf[i:i+2]))
\t\t}

\t\tprocess(decoder, bits)

\t\tbits = nil
\t}
}

func process(dec *sphinx.Decoder, bits []int16) {
\tif !dec.StartUtt() {
\t\tpanic("Decoder failed to start Utt")
\t}
\t
\tdec.ProcessRaw(bits, false, false)
\tdec.EndUtt()
\thyp, score := dec.Hypothesis()
\t
\tif score > -2500 {
\t\tlog.Println("Predicted:", hyp, score)
\t\thandleAction(hyp)
\t}
}

func executeCommand(commands ...string) {
\tcmd := exec.Command(commands[0], commands[1:]...)
\tcmd.Run()
}

func handleAction(hyp string) {
\tswitch hyp {
\t\tcase "SLEEP":
\t\texecuteCommand("loginctl", "lock-session")
\t\t
\t\tcase "WAKE UP":
\t\texecuteCommand("loginctl", "unlock-session")

\t\tcase "POWEROFF":
\t\texecuteCommand("poweroff")
\t}
}

func main() {
\tcfg := sphinx.NewConfig(
\t\tsphinx.HMMDirOption("/usr/local/share/pocketsphinx/model/en-us/en-us"),
\t\tsphinx.DictFileOption("6129.dic"),
\t\tsphinx.LMFileOption("6129.lm"),
\t\tsphinx.LogFileOption("commander.log"),
\t)
\t
\tdec, err := sphinx.NewDecoder(cfg)
\tif err != nil {
\t\tpanic(err)
\t}

\tlisten(dec)
}/<code>


這會將依賴項聲明與源自身綁定在一起。

如您現在所見,它簡單,最小,但足夠優雅。火焰圖表也為單元測試和基準提供了第一手支持。就像功能集一樣,它也有缺點。例如,“ go get”不支持版本,並且您已鎖定到源文件中傳遞的導入URL。它正在發展,並且已經出現了用於依賴性管理的其他工具。

Golang最初旨在解決Google龐大的代碼庫所存在的問題以及對高效併發應用進行編碼的迫切需求。 它使利用現代微芯片的多核特性的編碼應用程序/庫非常容易。 而且,它永遠不會妨礙開發人員。 這是一種簡單的現代語言,從沒有嘗試過成為其他語言。

Protobuf(協議緩衝區)

Protobuf或Protocol Buffers是Google的一種二進制通信格式。 它用於序列化結構化數據。 通訊格式? 有點像JSON? 是。 它已有10多年的歷史了,Google已經使用了一段時間。

但是我們沒有JSON,而且它無處不在...

就像Golang一樣,Protobufs並不能真正解決任何新問題。 它只是以現代方式更有效地解決了現有問題。 與Golang不同,它們不一定比現有解決方案更優雅。 這是Protobuf的重點:

· 它是一種二進制格式,與JSON和XML不同,後者是基於文本的,因此空間效率很高。

· 對架構的第一手資料和完善的支持。

· 對生成各種語言的解析和使用者代碼的第一手支持。

二進制格式和速度

那麼Protobuf真的那麼快嗎? 簡短的答案是,是的。 根據Google Developers的說法,它們比XML小3至10倍,並且快20至100倍。 它是二進制格式,序列化的數據不是人類可讀的,這不足為奇。

現代服務器端堆棧簡介-Golang,Protobuf和gRPC

(Image Source: Beating JSON performance with Protobuf)


Protobuf採取了更具計劃性的方法。 您定義了" .proto"文件,它們是模式文件的一種,但是功能更強大。 本質上,您定義了消息的結構,哪些字段是可選的或必填的,它們的數據類型等。然後,Protobuf編譯器將為您生成數據訪問類。 您可以在業務邏輯中使用這些類來促進通信。
查看與服務相關的`.proto`文件還將使您對通信的細節和公開的功能有一個非常清晰的瞭解。典型的.proto文件如下所示

<code>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;
}/<code>


趣聞:Stack Overflow之王Jon Skeet是該項目的主要貢獻者之一。

gRPC

正如您所猜到的,gRPC是現代RPC(遠程過程調用)框架。 它是一個自帶動力的框架,具有對負載平衡,跟蹤,運行狀況檢查和身份驗證的內置支持。 它於2015年由Google開源,從那以後一直受到歡迎。

RPC框架...? REST呢?

帶有WSDL的SOAP在面向服務的體系結構中的不同系統之間的通信已經使用了很長時間。 當時,契約曾經被嚴格定義,系統又龐大又單一,暴露了很多這樣的接口。

然後是"瀏覽"的概念,其中服務器和客戶端不需要緊密耦合。 客戶應該能夠瀏覽服務產品,即使它們是獨立編碼的。 如果客戶要求提供有關一本書的信息,則該服務以及所要求的內容可能還會提供相關圖書的列表,以便客戶可以瀏覽。 REST範式對此至關重要,因為它允許服務器和客戶端使用某些原始動詞自由通信,而不受嚴格限制。

正如您在上面看到的那樣,該服務的行為就像一個整體系統,它與所需的一切同時還進行了許多其他事情,以便為客戶提供預期的“瀏覽”體驗。但這並不總是用例。是嗎

進入微服務

採用微服務架構的原因很多。 一個突出的事實是,很難擴展單片系統。 在設計具有微服務架構的大型系統時,每項業務或技術要求都應作為幾種原始"微"服務的合作組成來執行。

這些服務的響應不必太全面。 他們應履行預期職責的具體職責。 理想情況下,它們的行為應像純函數一樣,以實現無縫組合。

現在,將REST用作此類服務的通信範例並不能給我們帶來很多好處。 但是,為服務公開REST API確實可以為該服務啟用很多表達能力,但是如果既不需要也不打算表達這種表達能力,那麼我們可以使用更關注其他因素的範式。

gRPC打算在傳統HTTP請求上改進以下技術方面:

· 默認情況下,HTTP / 2及其所有優點。

· 機器通過Protobuf溝通。

· 藉助HTTP / 2,對流式呼叫的專用支持。

· 可插拔的身份驗證,跟蹤,負載平衡和運行狀況檢查,因為您始終需要這些。

由於它是RPC框架,因此我們再次有了諸如服務定義和接口描述語言之類的概念,這些概念可能與那些在REST之前沒有的人感到陌生,但是這次由於gRPC將Protobuf用於這兩者而顯得不那麼笨拙。

Protobuf的設計方式使其可以用作通信格式以及協議規範工具,而無需引入任何新內容。 典型的gRPC服務定義如下所示:

<code>service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
string greeting = 1;
}

message HelloResponse {
string reply = 1;
}/<code>


您只需為服務編寫一個" .proto"文件,描述接口名稱,期望的名稱以及它作為Protobuf消息返回的內容。 然後Protobuf編譯器將同時生成客戶端和服務器端代碼。 客戶可以直接調用此方法,服務器端可以實現這些API來填充業務邏輯。

結論

Golang和使用Protobuf的gRPC一起,是用於現代服務器編程的新興堆棧。 Golang簡化了併發/並行應用程序的製作,而Protobuf的gRPC可實現高效的通信並帶來令人愉悅的開發人員經驗。


(本文翻譯自Velotio Technologies的文章《Introduction to the Modern Server-side Stack — Golang, Protobuf, and gRPC》,參考:https://medium.com/velotio-perspectives/introduction-to-the-modern-server-side-stack-golang-protobuf-and-grpc-40407486568)


分享到:


相關文章: