现代服务器端堆栈简介-Golang,Protobuf和gRPC

现代服务器端堆栈简介-Golang,Protobuf和gRPC

镇上有一些新的服务器编程人员,而这次全都与Google有关。 自从Google开始将Golang用于自己的生产系统以来,Golang迅速受到欢迎。 自从微服务架构诞生以来,^ ) h p ; !人们一直专) . y m Q注于gRPC和Protobuf等现代数据通信解决方案。 在这篇X ; k L文章中,我将向您简要介绍所有这些内容。

Golang

Golang或Go是Google的一种开源通用编程语言。 由于z 0 i . Z o种种原因,它最近越来越受欢迎。 据谷歌称,对于大多数人来说,这可能是一个令人惊讶的发现,这种语言已经使用了将近10年,并且已经可以投入生产将近7年。

Golang被设计为m . j l l 1简单,现代,易于理解和快速掌握。 语言的创b f m ? n F | `建者以一种普通程4 * * X W序员可以在周末掌握该9 Z E y x语言的方式来设计它。 我可以证明他们确实成功了。 说到创建者,这些都是参与C语言原始草案的专8 Y / r e z i 0 #家,因此我们可以放心,这些人知道他们在做什么。

很好,但是为什么我们需要另一种语言?

对于大多数用例,我们实际上没C * i . G 6 M C有。 实际上,Go并不能解决以前其他语言/工具尚未解决的任何新问题。 但是它确实试图以有效,优雅和直观的方式解决人们通常面临的一系列特定的相关问题。 Go的主要重I 1 . X P点是:

一流的并发支持

优雅,现代的语言,对其核心非常简单

性能很好

对现代软件开发所需工具的第一手支持

我将简要说明Go如何提供上述所有功能。 您可以从Go的官方网站上详细了解该语言及其功能。

并发

并发是大多数服务器应用程序中的主要问题之一4 = 6 S ! j l 3 G,考虑到现代微处理器,它应该是该语{ j u n 2言的主要问题。2 L = L 2 . Go引入了称为\" goroutine\"的概念。 \" goroutine\"类似于\"轻量级用户空间线( _ w程\"。S L ? I h ] n 6 & 它比实际复杂得多,因为多个goroutine在单个线程上多路复用,但是上面的表达式应该为您提供一个总体思路。 这些足够轻,您实际上可以同时启动一百万个goroutine,因为它们从很小的堆栈开始。 实际上,这是推荐的。 Go中的任何函数/方w Y l a 9 e 7 e 1法都可用于生成Goroutine。 您只需执行\" go myAsyncTask()\",即可从\" myAsyncTask\"函数中生成gorouv 9 -tine。 以下是一个示例:

// This function pG d G 6erforms the given task concurr} d %ently by spawing a goroutine
// for each of those tasks.

func performAsyncTasks(task []Task) {
for _, task := range tasks {d ` | * , E Z 6
// This will sp4 E 6 g f M $awn a separate goroutine to carry out this taskI ^ y ? i K M.
// This call is non-blocking
go task.Execute()
}
}

是的,就这么简单,它的意思就是那样,因为Go是一种简单的语言,并且您会为每一个独立的异步任务生成一个goroutine0 P $ =,而无需太在意。 如果有多个内核,Go的运行: W : A P时将自动并行运行goroutine。 但是这些goroutine如何通信? 答案是通道。

\"通道\"也是一种语言原语,旨在用于goroutine之间的通信。 您可以将任何内容从一个通道传递到另一个goroutine(原始Go类型或Go结构或其他通道)。 通道本质上是一个阻塞的双端队列(也可以是单端)。 如果您希望g1 D Xoroutine在继续满足某个条件之前等待某个条件,则可以在通道的帮助下实现goroutine的协作阻e s ~ T 9 A f塞。

这两个原语在编写异步或并行代码时提供了很大的灵活性和简便性。 其他帮助程序库(例如goroutine池)可以从上述原语轻松创建。 一个基] z 3本的例子是:

package exech C { I wutor

import (
\"log\"
\"sync/atomic\"
)

// The Executor stt B R e , U m Fruct is the main executor for tasks.
// \'maxWorkers\' represents the maximum number of simultaneous goroutines.
// \'ActiveWorkers\' tells the nuh 0mber of active goroutines spawned by the Executor at given time.
// \'Tasks\' is the channel on which the Executor receives the taB { | 7 { i 8sks.
// \'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, o6 o 2 b c P knly the termination
// signal is supported which is essentially is sending \'1\' on this channel by the client.
type Executor struct {
maxWorkers int64
ActiveWorkers int6% h [ H 7 & [ b4

Tasks chan Task
Reports chan Report
signals chan int
}

// NewExecutor creates a new Execu= Y b u [ q Xtor.
// \'maxWorkers\' tells the& l Q maximum number of simultaneous goroutiL N * S %nes.
// \'signals\I } k | Z 3 ] f ' channel caX ; Y K I ) 4 Zn be used to control the Executor.
fun. | Q uc NewExecu3 V C y N p Mtor(maxWorkers in, ^ /t, signals chan int) *Executor {
chanSize := 1000

if maxWorkers > chanSize {
chanSize = maxWorkers
}

executor := Executor{
maxWorkers: int64(maxWorkers),
Tasks: make(chan Task, chanSize),
Reports: make(chan ReT s 3 * ] . Gport, chaC R u : } mnSize),
sW 3 U J A V 1igna- n Vls: signals,
}

go executor.launch()

r= y ? N @ 8eturn &executor
}

// lB ( H I o 5 i |aunch starts the main loop for polling oe 2 = & i @ Z In the all the relevant channels and handling differents
// mesG k v & Y ssages.
func (executor *Executor) launch() int {
reports := make(chan Report, executor.maxWorkers)

for {
select {
c~ . u 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 := &` M y nlt;-executor.Tasks
atomic.AddInt64(&executor.ActiveWorkers, 1)
go executor.launchWorker(e 6task, reports)
}
}
}
}w R 4 ] X

// handleSignals is called whenever anything is received on the \'S ; - 6signr - 9 q d v %als\' channel.
// It performs the relevant task according to the received signal(request) and thene 3 4 J responds either
// with 0 or 1 indicating wheF D ! M X X & FthR 5 y n h oer the request was respectef e . * 9d(0) or rejected(1).
f, ~ $ g ; S bunc (executor *Executor) handleSignals(/ v * Q V ] M zsigna~ R o Nl int) int {
if signal == 1 {
log.Println(B a q S t\"Received termination request...\")

if executor.Inactive() {
log.Println(\"No active workers, exiting...\2 g 3 7 w r V i #")
executor.signals <F ? n- 0
return 0
}

exG j p m z / Decuto: D r ` E wr.signals <- 1
log.Println(\"Some tasks are still active...\")
}

return 1
}

// launchWorker is called wheneveri 6 r E x D R a new Task i) : . ) _ 9 D {s received and ExZ ] A E P U - ecutor can spawn more workers to spawn
// a new Worker.b } O /
// Each worker is launched on a ns b F ! Q P , lew goroutine. It performs the given task and publishes the rep0 ~ z ] lort oL - V [ n
// tf q ;he Executor\'s internal reports chat d t : P : Unnel.
func (et B K ] O l s @ +xecutor *Executor) lauZ 9 % ) E H bny n D 7 P e `chWorker(task Task, reports chan<- Repor* 5 ( 0 c % |t) {
report := task.Execute()

if len(reports) < cap(reports) {
reports <- report
} else {
loa W ^g.Println(\"Executor\'s reA # * ] v g R g ]port chano b r 3 M unel is full...\")
}

atomic.AddInt64(&executor.ActiveWorkers, -1)
}

// Add3 ? i ! 3Task is used to submit a new task to the Executor is a non-blocking way. The Client can submit
// a new task using thl h | X ee Executor\'s tasks channel directly b U R y V F t Rut that will block if the tasks channel is
//R F I V full.
// It should be considered that this method dT * M T ^ . i voesn\'t add the c N I d given task if the tasks channel isc + k full
// and it is up to client to try again later.
func (execut6 k { * } b ror *Executi y xor) A( 2 r M d 7 $ G #ddTask(task Task) bool {
if len(executor.Tasks) == cap(executor.Tasks) {
return false
}5 S 3 O

executor.Tasks <- task
return true
}

// addReporY P c # s o .t is used by the Executor to publish6 K - l the r` k 2 F o P e Neports in a non-blocking way. It cl* F { d j T S 2ient is not
// rX 2 u /eading the reports channel or is6 [ w o slower that the Execu2 ~ m b ( q %tor publishz f r N ming 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
}

executQ / N D U h = oor.Reports <- report
return true
}

// Inactive checks if the Exec - V $ut) 3 Z + p 3 yor is idle. Thisu 0 n T f b happens when there are no pending tasks, active
// workers and reports to publish.
func (executor *Exec= / ] 8 W q , ,utor) Inactive() bool {
return executor.ActiveWorkers == 0 && lel u , Q o Q * . ?n(executor.Tasks) == 0 && len(executor.Reports) == 0
}

简单的语言

与许多其他现代语言不同,Golang[ C h @ i 2没有很多功能。 实际上,可以说出一种令人信服的理由,即该语言对其功能集{ | Z T的限制过于严格,这是故意的。 它不是围绕像Java这样的编程范例来设计的,也不是为了, : f 3 U w t支持像PythonP N A9 H f c F %样的多种编程范例而设计的。 只是简单的结构编程。 语言中仅包含了基本功能,仅此而已。

查看该语言后,您可能会觉得该语言没有遵_ 0 9 J }循任何特定的哲学或方向,并且感觉这里包含了解决特定问题的所有功能,仅此而已。 例如,它具有方法和接口,但没有m C R ` l类; 编译器生成静态链接的二进制文件,但仍具有垃圾回收器; 它具有严格的静态类型,但不支持泛型。 该语言的运行时很精简,但H ` : L ) q Z J M不支持例外。

这里的主d M 4要思想是,开发人员应该花费最少的时间将他[ { c z $ p T R 0/她的思想或算法表达为代码,而不用考虑\"用x语言做到这一点的最佳方M D ^ f法是什么/ B { Z K l?\" 对其他人来说应该很容易理解。 它仍然不是完美的,确实时有限制,并且\" Go 2\"正在考虑某些基本功能,例如\"泛型\"和\"异常\"。

性能

单线程执行性能不是判断语言的好指标,尤其是当语言关注并发和并行性时。 但是,Golang仍然拥有令人印象深刻的基准数字,只有C,C ++,Rust等硬核系统编程语言才能击败它,并且它还在a # m 1 y c v % f不断改善。 考虑到它是垃圾i y 0 , L收集语言,其性能实际上是非常令人印象深刻的,并且对于几乎每个用例都足够好。

现代服务器端堆栈简介-Golang,Protobuf和gRPC

(Imagel 3 w { s Source: Medium)

开发人员工

采用新工具/语言直接R L .取决于其开发人员的经验。 Go的采用确实代表了它的工具。 在这里,我们可以看到相同的想法和工具很少,但是足够了。 这& m ~ @ , {一切都可以通过\" go\"命令及其子命令来实现。 全部都是命令行。

没有像pip,npm这样的语言的软件包管理器。 但是,您只需执行以下操作即可获取任何社区软件包

goN d : D j 9 get github.com/farkaskid/WebCrawler/blob/master/executor/executor.go

是的,它有效。 您可以直接从Gi+ J [ h p X , .tHub或其他任何地方提取软件包。 它们只是源文件。

但是package.json ..呢? 我看不到\"去得到\"的任何等效内容。 因为没有。 您无需在单个文件中指定所有依赖项。 您可以直接使用:

import \"github.comy | p b x/xlab/pocketsphinx-go/sphinx\"

在您的源文件本身中,当您执行T x q C 9 l\"生成\"时,它将自G O m g ) Z动为您\"获取\"它。 您可以在此处查看完整的源i w s O c F文件:

package main

import (
# @ 1 * = t ] w\"~ 7 3 ^ } H p |encoding/binary\"
\"bytes\"
\"log\"
\"os/exec\"

\"github.com/xlab/pocketsphinx-go/sphinx\"
pulse \"github.c] b y e T Bom/mesilliac/pulse-simple\" //i H ] Q 9 z V E pulse-simple
)

var buffSize int

func readInt16(buf []byte) (val int16) {
binary.Read(bytes$ / F + ) z l 7.5 O 9NewBu [ ruffer(buf)3 & @ x A P `, binary.LittleEndian, &val)
retu] + _ j -rn
}

func createStream() *pulse.Stream {
ss := pulse.SampleSpec{p? p $ ! r J } _ulse.SAMP8 - CLE_S16LE, 16000, 1}
buffSize = int(ss.UsecToBytes(1 * 1000000))
stream, err := pulse.Cap~ d M + , Dture(\"pulse-simple test\", \"capture test\4 f M x ^ n U i", &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.7 j { 4 ) d C E ]Println(\"Listenine o z A h u _ ?g...\")

for {
_, err := stream.Read(buf)
if err != nila o l g ; {
l1 s kog.Pang g +icln(err)
}

for i := 0; i < buffSize; i += 2 {
bits = append(bits, readInt16(buf[i:3 @ . w ] l ki+2]))
}

process(decoder, bits)
bits = nil
}
}

func process(dec *sphinx.Dec| 7 ` C Coder, bits []int16) {
if !dec.StartUtt() {
panic(\"Decoder failed to start Utt\")
}

dec.ProcessRaw(bits, false, false)
dec.EndUtH # X X o m %t()
hyp, score := dec.Hypothesis()

if score > -2500 {
log7 4 $ w J l.Println(\"Predicted:\", hp L N # d / Wyp, score)
hay ) P H ` j NndleAction(hyp)
}
}

func executeComm * {and(commands ...stringb n N o I) {
cmd := exec.Command(commands[0], c! 7 s D p M : b Gommands[1:]...)
cmd.Run()
}

func handleAction(hyp string) {
switch hyp {
case \"SLEEP: Y q y q u H _\":
executeCommand(\"logiW T s *nctl\", \"/ - 6lock-sessiL # # _on\")

case \"WAKE UP\":
executeCommand(\"loginctl\", \"unlock-sessX * 5 T !ion\")

case \"POWEROFF\":
executeCom( p , n Jmand(\"poweroff\")
}
}

func main() {
ci % N @ h zfg := sphinx.NewCon, 8 ~ x p % i r mfig(
sphinx.HMMDirOption(\"/usr/local/share/pocketsphinx/model/en-us/eW e Wn-us\"),
sphinx.DictFileO . f s x u o ; xption(\"6129.dic\"),
sphinx.LMFileOption(\= K z $ ^"6129.lm\"),W 8 ( L e o a k
sphinx.LogFileO0 ] M i # ^ /ption(\"commander.log\"),
)

dec, err := sphinx.NewDecoder(cfg)
if err != nil {
panic(err)
}

listen(dec)
}

这会将依赖项声明与源自身绑定在一起。

如您现在所见,它简单,最小,但足够优雅。火焰图表也为单元测试和基准提供了第一手支持。就像功能集一样,它也有缺点。例如,“ go get”不支持版本,并且您已锁定到源文件中传递的导入URL。它正在发展,并且已经出现了用于依赖性管理的其他{ V 2 c工具。

Golang最初旨在解决Google庞大的代码库所存在的问题以及对高效 ) ] d并发应用进行编码的迫切需求。 它使利用现代微芯片的多核特性的编码应c ( % ~ q用程序/库非常K % Y A Q容易。 而且,它永远不会妨碍开发人员。 这是一种简单的现代语言,从没有尝试过成为其他语言。

Protobuf(协议缓冲区)

Protobuf或Protocol Buffers是Google的一种二进制通信格式。 它用于序列化结构化数据。 通讯格式? 有点像JSON? 是。 它已有10多年的历史了,Google已经使用了一段时间。

但是我们没有JSON,而且它无处不在...

就像Golang一样,Protobufs并不能真正解决任何新问题。 它只是以现代方, 5 [ 8 q M / ?式更有效地解决了^ U - _ O a c现有问题。 与Golang不同,它们不一定比现有解决k K o方案更优雅。 这是Protobuf的重点:@ o N i A - 7

它是一种二进制格式,与JSON和XML不同,后者是基于文本的,因此空间效率很高。

对架构的第一手资料和完善的支持。

对生成各种语言的B q Z % 6解析和使用者代码的第一手支持。

二进制格式和速度

那么Protobuf真的那么快吗? 简短的答案是,是的。 根据Google Developers的说法,它们比XML小3至10倍,并且快20至100倍。 它是二进制格式,序列化的数据不是人类可读的,这不足为奇。

现代服务器端堆栈简介-Golang,Protobuf和gRPC

(Image SourcH N }e: Beating JSON perfo^ = l ? v = & z prmance with Protobuf)

Protobuf采取了更具计划性的方$ U n K h法。 您定义了\" .proto\"文件,它们是模式文件的一种,但是功能j x q V x E k }更强大。 本质上,您定义了消息的结构,哪些字段是可选的或必填的,它们的数f ] [ R O I p 8 h据类) - ; w L e型等。然后,Protobuf编译器将为您生成= = u + n 6 V数据访问类。 您可以在业务逻辑中使用这些类来促进通信。
查看与服务相关的`.proto`文s # } `件还将使您对通信的细节和公开的功能有一个非常清晰的了解。典型的.proto X ) L y 9o文件如下所示

message Person {
required s8 p b Jtring 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 4 L K V ( g r _l PhoneType type = 2 [de, D e v T i Pfault = HOME];
}

repeated PhoneNumber phone = 4;
}

1 R s w d $ [闻:Stack Overflow之王Jon Skeet是该项目的主要贡献者之一。

gRPC

正如您所猜到的,gRPC是现9 S C ( a y /代RPC(远程过程F 8 z调用)框架。 它是一个自带动力的框架,具有对负载平衡,跟踪,运行状况检查和身份验证的内置支持。 它于2015年由Google开源,从那以后一直受到欢O V @ [ D迎。

RPC框架...? REST呢?

带有WSDLJ G ,的SOAP在面向服务的体系结构中的不同系统之间的通信已经使用了很长时间。 当时,契约曾经被严格定! Y U : = a p . w义,系统又庞n d T大又单一,暴露了很多这样的接口。

然后是\"浏览\"的概念,其中服务器和客户端不需要紧密耦合。 客户应该能够浏览服务产品,即使它们是独立编码的。 如果客户要求提供有关一本书的信息,则该服务以及所要求的内容可能还会提供相关图书的列表,以便客户可以浏览。 REST范式对此至关重要,因为它允许服务器和客户端使用某些原始动词自由通信,而不受严格限制。

正如您在上面看到的那样,该服务的行为就像一个整体系统,它与所需的一切同时还进行了许多其他事情,以便为客户提供预期的“浏览”体验。但这并不总是用例。M 2 . ? v C G是吗

进入微服务

采用微服务架构的原因很多。 一个突出的事实是,很难扩展单片系统; 4 Cg 6 x 8 ` . * s 在设计具有微服务架构的大型系统9 2 l ( n时,每项业务或技术要求都应作为几种原k o - E * c始\"微\"服务的合作组成来执行。

这些服务的响应不必太全面。 他们应履行预期职责的具体职责。 理想情况下,它们的行为应像纯函数一样,以b W h实现无缝组合。

现在,将REST用作此类服务的通信范例并不能给我们V S Q带来很多好处。 但是,为服务公开REST API确实可以为该服务启` q G @用很多表s ; U达能力,但是如果既不需要v q :也不打算表达这种表达能力,那么我们可以使用更关注其他因素的范式。

gRPC打算在传统HTTP请求上改进以下技术方面:

默认情况下,HTTP / 2及其所有优点。

机器通~ [ 9过Protobuf沟通。

借助HTZ Y ] | nTP / 2,对流式呼叫的专用支持。

可插拔的身份验证,跟踪,负载平衡M c k q } - ]和运行状况检查,因为您始终需要这些。

由于它是R` V E LPC框架,因此我们再次有了诸如服务定义和4 c h .接口描述& b & % I # 6语言之类的概念,这些概念可能与那些在REST之前没有的人感到陌生,但是这次由于gi / ` } LRPC将Protobuf用于这两者而显得s l ;不那么笨拙+ d B i V

Protobuf的设计方式使其可以用作通信格式以及协议规范工具( L P Z y L ;,而无需引入任何新内容。 典型的gRPC服务定义如下所示:

service HelloService {
rpc SayHello (HelloRequest) reo W w n y 8turns { ] 0 Y |s (HelloResponse);
}

message HelloRequest {
string greeting = 1;
}

message HelloResponse {
string reply = 1;
}

您只需为服务编写一个\" .proto\"文件,描述接口名称,期望的名称以及它作为Protobuf` X 5 ? / t K !消息返回的内容。 然后Protobuf编译器将同时生成客户端和服务器端代码。 客户可以直接调用此方法,服务器端可以实现这些A7 | $ G r uPI来填充业务逻辑。

结论

Golang和使用Protobuf的gRPC一起,是p J $ 9 #用于现代服务器编程的新兴堆栈。 Golang简化了并发/并行应用程序的制作,而Protobuf的gRPC可实现高效的通信并带来令人愉+ s C v悦的开发人员经验。

(本文翻译自Velotio Technologies的文章《Intv U : t Z w 6roductio0 L in to the Modern Serp L v F W | Rver-side Stack — Golang, ProtobufO j G K, aQ g O Jnd gRPC》,参考:https://medium.3 : O ` /com/velotio-perspectives/intro$ V @ 7 . Y x D (duction-to-the-modeT Q w D C 6 q zrn-server-side-stack-golang-protobuf-and-grpc-40407486568)

上一篇

刘亦菲穿裙子和穿裤子是两个样,当穿上裤子,缺点暴露无遗!

下一篇

中国股市:为什么80%的散户炒股都不赚钱?因为他们连“换手率10%”意味着什么都不知道

评论已经被关闭。

插入图片
返回顶部