您的位置 首页 golang

现代服务器端堆栈简介-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。 以下是一个示例:

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

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

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

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

 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
}  

简单的语言

与许多其他现代语言不同,Golang没有很多功能。 实际上,可以说出一种令人信服的理由,即该语言对其功能集的限制过于严格,这是故意的。 它不是围绕像 Java 这样的编程范例来设计的,也不是为了支持像Python这样的多种编程范例而设计的。 只是简单的结构编程。 语言中仅包含了基本功能,仅此而已。

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

这里的主要思想是,开发人员应该花费最少的时间将他/她的思想或算法表达为代码,而不用考虑”用x语言做到这一点的最佳方法是什么?” 对其他人来说应该很容易理解。 它仍然不是完美的,确实时有限制,并且” Go 2″正在考虑某些基本功能,例如”泛型”和”异常”。

性能

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

(Image Source: Medium)

开发人员工具

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

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

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

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

import “github.com/xlab/pocketsphinx-go/sphinx”

在您的源文件本身中,当您执行”生成”时,它将自动为您”获取”它。 您可以在此处查看完整的源文件:

 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)
}  

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

如您现在所见,它简单,最小,但足够优雅。火焰图表也为单元测试和基准提供了第一手支持。就像功能集一样,它也有缺点。例如,“ 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倍。 它是二进制格式,序列化的数据不是人类可读的,这不足为奇。

(Image Source: Beating JSON performance with Protobuf)

Protobuf采取了更具计划性的方法。 您定义了” .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;
}  

趣闻: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服务定义如下所示:

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

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}  

您只需为服务编写一个” .proto”文件,描述接口名称,期望的名称以及它作为Protobuf消息返回的内容。 然后Protobuf编译器将同时生成客户端和服务器端代码。 客户可以直接调用此方法,服务器端可以实现这些API来填充业务逻辑。

结论

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

(本文翻译自Velotio Technologies的文章《Introduction to the Modern Server-side Stack — Golang, Protobuf, and gRPC》,参考:

文章来源:智云一二三科技

文章标题:现代服务器端堆栈简介-Golang,Protobuf和gRPC

文章地址:https://www.zhihuclub.com/86832.shtml

关于作者: 智云科技

热门文章

网站地图