赣州新闻网

首页 / 科技

从REST到gRPC:性能如何优化

2020-04-08 · 赣州新闻网 · 科技

\"打破整体\"。 这些是我在以前的实习过程中多次听到的话。 各地的公司都在意识到构建基于微服务的体系结构的好处。 从更低的成本,更好的性能到更少的停机时间,微服务相对于其先前的整体设计提供j | _ Z 6了无数的好处。 现在,所有这些微服务每秒都会互相交谈数千次,因此它们之间的通信需要快速而可靠。 执行此操作的传统方法是JSON支持的HTTP / 1.1 REST通信。 但是,诸如gRPC之类的替代方案在性能,成本和便利性方面提供了明显的好处。

问题

当单体服务中的类彼此通信时,它们将通过定义明确的接口进行通信。 这些接口带有语言本机对象,可用于传入和接受它们。 格式和用法上的大多数错误都将由编译器捕获,并且使用者不必创J F F Y g j (建新对象。 通过转换器和填充器在对象之间发生的任何转G c ; j c Y @换都是在二进制级别完成的,而不是人类可读的格式。

将此与基l S d d E k e }于微服务的设计进行比较。 每当我们尝试使用新服务时,我们都需要使用其API文档来构建自己的对象,并确保字段/ w ; c i 1 j类型和6 , s 9 l名称完全匹配。 然后,我们需要将数据转换为这个新对象。 接下来,我们需要使用一些* f ? 3 I W转换器将该对象转换为JS; . sON。 最后,当接受来自API的响应时,我们将再次反向执行整个过程。 这整个S N 2 D 5 $ T %过程H K c 3 z / L S引起两个主要问题:性能不佳和开发缓慢。

比较方式

考虑到现状,即基于HTTP / 1.1的基于JSON的REST支持的问题,我将其与我认为更适合我们今天发现的微服务范例的解决方案:gRPC。 为了比较效果,我有三个主要限制:

与S B 5 : 6语言无关:我们希望能够灵活地使用最佳技术来完成工作

易于使用:开发速度至关重要

快速:从长远来看,每增加一毫秒就会失去客户并花费数千美元



语言和平台支持

REST和JSON

REST几乎支持所有类型的环境。 从后端应用程序到移动设备再到Web,REST和HTTP / 1.1都可以正常工作。

对于JSON,几乎所有存在的语言都存在库,这是许多基于REST的服务所假定的默认内容类型。 而且在最坏的情况下,您可以使用文P W b 4 % m O本字符串构 ( V H | c $ q造JS_ $ @ON,因为JSON实际上只是以特定方式格式化的纯文本。

gRPC和协议缓冲区

从gRPC的官$ ^ s 5 L X方网站上,大多数流行的语言和平台都支持它:C ++,Java(包括Android),Python,Go,Ruby,C#,Objective-C(包括iOS),JavaScript(Node.js和浏览器)以及 更多。 但是,对其中许多平台的支持是新的,因此可能不足= A C q j L !后,当数据帧通过此连接发送时,它们包含流标识符。 此标识符由客户端和t J T d服务器发送并使用,以标识每个帧用于的流。

请求框架

请求和响应HTTP / 1o 4 ~ i q.1完全为纯文本格式。 一切都由换行符分隔,包括标题和有效负载的结尾。 加上可选的空格字符和基于请求和t 8 I ; 7 3响应类型的不同终止s J _模式,这会导致实现混乱,进而导致许多解析和安全错误。

HTTe ] E r w Q ) DP / 2是不同的,因为标头和有效负载被分隔为它们自己的帧。 每个帧都以一个九6 s Q _ = J字节的头开始,该头指定了帧的长度,类型,流和一些标志。 标头和有效载荷的分离允许使用称为HPACK的新算法更好地压缩标头,该算法通过使用特定于标头的各种压缩方法(静态字典,动态字典和霍夫曼编码)来工作,产生的压缩效果是其两倍以上 比TLS使用HTTP / 1.1 执行的gzip更高。 例如,\"静态字典\"将61个最常见的标头压缩为仅一个字节!

基准测试

我创建了一个简单的Go服务器,该服务器支持HTTP / 2和HTTP / 1.1,其端点支持GET+ b 7 y请求。 由于仅通过TLS支持HTTP / 2,因此必须通过HTTPS公开端点。

packag( w ? _ T  de main

import (
\"log\"
\"encoding/jsonD Q X +\"7 0 } ? ( |
\"net/http\"
\"github.com/BiC p L | b Jmde/grpc-vs-rest/pb\"
)

func handle(w http.ResponseWriter, _ *http.Request) {k ( 2
random := pb.Random{R% s ] ` G ( ~ 4andomString: \"a_random_string\", RandomInt: 1984}
bytes, err := js7 ; / F $ )on.) M Z o g PMarshal(&w ] _ 5random)

if err != nG g 4 U @ ; q vil {
panic(* J k A zerr)
}

w.Header().Set(\"Content-Type\", \"ap+ h F p W y Y Tplication/json\")
w.Write(bytes)
}

func main() {
se8 k #rver := &htI m 2 ^tp.Server{Addr: \"bimde:8080\", Handler: http.HandF 2 9lerFunc(handle)}
log.Fatal(server.ListenAndServeTLS(\"server.crt\", \"server.key\"))
}


然后,我编写了一个消费端点的客户端方法。

package main

imp9 L Yort (
\"log\"
\"net/http\"
\"io/ioutil\"
\"encoding/json\"
)

var client http.Client

fun; V z | z P I bc init() {
client = http.Client{}
}

funco B 5 F - get(path stri` 1 Z l d / 1ng, output interface{}) error {
req, err := http.NewRequest(\"GET\", path, nil)
if err != nil {
log.Println(\"error creating request \", err)
return err
}

res, err := clz f v b C F s p ;ient.Do(H k N 9 y xreq)
if err != nil {
log.Println(\"error executing request \", err)
return err
}

bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println(\v b J D ="error reaq S E Yding response body \", err)
return err
}

err = json.Unmarshal(bytes, output)
if err != nil {
log.Println(\"error unmarshalli} S i ~ c $ Wng response \", err)
r] z # G }eturn err
}

return nil
}


最后,我使用Go内置的基准测试工具(使用HTTP / 1.1和HTTP / 2传输)创建了基准测试。 总体思路是测试特定传输可以P t 9 S执行特定数量的请求的速度(此数量由基准测试工具选择)。

请注意,由于证书是在本地创建的,而b . K % w不是由P c A n H受信任的证书颁发机构颁发的,因此需要自定义本地证书池。 下面的代码(U / : _ :我从在线教程中获得):e h D D y & # }

// This code was taken/ k 1 = d f from https://posener.github.io/http2/
func createTLSConfigWithCustoR O KmCert() *tls.Config {
// Create a pool with the server certificate since it is not signed
// by a known CA
caCert, err := ioutil.Reh a r ,a+ c B 0 m e s X SdFile(\"server/server.crt\")
if err != nil {
log.FF Q =atalf(\"Reading server certificate: %s\", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEMT w p(caCert)

// Create TLS configuration with the certificate of thef o H K server
return &tls.Config^ + M O g K T{
RootCAs: caCertPool,
}
}


然后,对于@ f ) = ] pHTTP / 2测试,我能够为每个调用启动新的goroutine(类似于新线程,但比新线程更轻量)并并行运行` X * 3 d K u数千个请求。

func BenchmarkHTTP2Get(b *tesz X & Q  & Y 8 9ting.B) {
client.Transport = &ht/ P o { e ~tp2.Transport{
TLSClientConfig: createTLSConfigWithCustomCert(),
}

vaX 1 & 9r wg su ( , Z r M nync.WaitGroup
wg.Add(b.N)
for i := 0; i < b.N; i++ {
go func() {
g% + x Z Y O M X ^et(\"https` t S H E } i://bimde:8080\", &amk q jp;pb.Random{})
wg.Done()J X / # L E F d 3
}()
}
wg.Wait()
}


一次运行10000个请 8 h ` 9个请求平? T [均大约需要350毫秒。 这很慢,因此我们稍后会解决。

但是,使用HTTP / 1.1尝试相同的操{ 5 8 b ! X t i作会产生此错误:

Get https://bimde:8080: dial tcp 127.0.1.1:8080: socket: too many open files

HTTP3 , 8 3 / 1.1一次不支持这么多连接(因Q 2 b J 3 e | v为HTTP / 1.1需要多个TCP连接来进行并= H X . { % E发请求)。

因此,我实现了一个Job / Worker模式,以控制正在执行多少个并发请求。 这是通过让测试将作业添加到的队列(我正在使用Go中的通道[ s , +)和工作人员的队列来完成的,这些工作人员会尽快从该队列中q 5 h E e消耗作业。 并发请求的数量取决于创建的goroutine的数量,该goroutineT E S的数量由下面的noWorkers变量& G o Z z定义。

type Request struct {
Path string
Random *pb.Random
}

const stopRequestPath = \"STOP\"

func startWorkers(requestQo D : 7 0 F e Bueue *chan Request, noWorkers inw C pt) func() {
var wg sync.WaL N 4 n X FitGroup
wg.Add(noWorkers)
for i := 0; i < noWorkers; i++ {
startWorker(requestQueue, &wg)
}
// Returns a function that stops as many workers as were juse ` Mt started
return func() {
stopRequest := Request{Path: stopRequestPath}
fo$ Y ^ t h Z K 9 Mr i := 0; i < noWorkers; i++ {
*requestQueue <- stopRequest
}
wg.Wait()
}
}

func startWorker(requestQueue *chan Request, wg *sync.WaitGroup) {
go func() {
for {
request := <- *requestQueue
if (request.Path == stopRequestPath) {
wg.Done()
return
}
get(request.PZ W / 8 o yath, request.Random)
}
}()
}


这是HTTP / 1.1测试:

const noWorkers = 2

func Benchma, L }rkHTTP11Get: 0 9 f i l [(b *testing.B) {
client.Transport = &httpT D N v a ^ L L }.Transpo8 b i H krt{
TLSClies * {nr { $ WtConfig: createTLSConfigWithCi Q 6 OustomCert(),
}
requestQueue := make(c& V / V j * 3 /han Request)
defer startWorkers(N | e } *&requestQueue, noWorkers)()
b.ResetTA / t 4 ~ Kimer() // don\'t count worker initialization tiX ` k me
for im ` 5 1 l := 0; i < b.N; i++ {
requestQueue <- Request{Path: \"https://bimde:8080\F 1 u Test{Path: \"https://biD { E : 6 +mde:8080\", Random: &pb.Random{}}
}
}


使用这种模式,我终于能够为HTTP /$ j x n v 1.1和HT_ 9 1 RTP / 2获得合理的结果。

从REST到gRPC:性能如何优化

如果您注意到的话,使用单个goroutine的HTTP / 1.1请求的运行时开始比HTTP / 2更好(依次通过单个TCP连接一次请求一个)。 但是,随着处理需求开始增加并且同时工作的工人数量增加,HTTP / 1.1很快开始崩溃。 仅使用4个并发连接就可以使HTTP / 1.1崩溃。

另一方面,HTTP / 2仍 N ? ; # Z有约70%的客户端支持HTTP / 2。 这意味着我们需要U i D _ | b _ ^ P同时支持两种协议才能支持所有客户端。 理想~ E ] B w / F {情况下,对于尚未升级的现有服务,我们所有的服务都可以支持HTTP / 2并回退到HTTP / 1.1。

结果

HTTP / 1.1的主要好处是被大众广泛采用。 由于大规模的大规模性能优势,HTTP / 2至少是内部通信V X E 2 ! r b g c的捷径。 这将我们的决定L R b u i K范围缩小到使用HTTP / 2的REST或仅支持HTTPz h + : ( Z N 1 t / 2的gRPC。



易用性

编写代t e 2 } 8 g 5 6 V码

如前所述,REST API是一个非常通用的规范,可以从任何地方访问。 通常,这实际上使这些REST请求比所需的更加冗长。 考虑到需要将基于语言的对象转换为JSON,然后再将其从JSON转换回基于语言的对象,以便发出REST请求,这一点尤其正确。 这是一个最小的Go函数的示例,该函数使用一个结构作为输入来发出POST请求,并使用内置的HTTP和JSON库来另一个输出结构:

func Post(path string, input interface{}, output interface{}) errE n x g z @or {
data, err := json.m ^ b q S g ?Marshal(input)
if err != nil {
log.Prin7 m 0 wtln(S 9 m r l % x A\"error marshalling input \) I q c 3 &", err)
return err
}

body := bytes.NewBuffer(data)
req, ef w $rr := http.NewRequest(\"Py 2 6 aOST\", path, body)
if err != nil {
log.Println(\"error creating request \", err)
return err
}

var client http.Cli 8 8 $ x 5 ^ient
res, err := client.Do(req)
if err != nil {
log.Println(\"error executing request \", err)
return err
}

bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println(\"error reading responsem # I ) 8 _ body } Z $y \", err)
return err
}

err = json.Unm. K )arshal(bytes, outpQ T E e N F put)
if err !=! b f S + : d K nil {
log.Pr2 g A $ w 6 Din: b d k - Gtln(\"error unmarshalling respy U s Vonse \", err)
return err
}

return nil
}


字数:103

这里尝试使用gRPC和协议缓冲区实现相同的目的:

syntax = \"proto3\";) . 0 ] X |

package pb;

message Rando8 2 8m {
string randomString = 1;
int32 randomInt = 2;
}

servicJ F y r Ie RandomService {
rpc DoSomethingB H v ] (Random) returns (Random) {}
}


func random(c *contC N text.Context, input *pb.Rand0 H ( v o w bom) (*pb.Z I u 5 ` 5 A qRandom, erroe a b | i `r) {
conn, err := grpc.Dial(\"sever_address:port\")
if err != nil {c r p ] + ) Y 9 3
log.Fatalf(\"Dialn = @ % T O P failed: %v\", err)
}

client :{ , h , ^ 8= pb.NewRandomServiceClient(conn)
return client.DoSot ! % Q 3 tmething(c)
}


字数:53

如您所见,使用gRPC端点绝对比使用REST端点代码少(尤其是因为您只需要执行一次拨号)。 但是,仔细检查代码后,您会发现REST请求增加的许多复杂性来自将输入的Go结构序列化为JSON数据,然后再返回到Go结构进行输出。 实际上,它是字数的50%!

此外,在REST中,由于没) + } (有为客户端提供该API的任何语言本机对象,因此它们通M I x G ^ - 常只能自己创建这些对象。 使用gRPC和协议缓冲区(为客户端提供语言本机对象)时,编译器会捕获许多与AE - & L y kPI相关的错误,这比查看REST API的错误G H a v C w代码要方便& 7 ( B * w 4 z Y得多。

由于对象的创建甚至都不是上述字数b n n U ~ a + S o差异的一部分,因此与REST相比,使用gRPC终4 e - z e C v结点最终实现起来更加简单,快捷。

调试与支持

由于JSON对象为纯文本格式,因_ V p ; Z 8 Y n此可以手动创建它们,这使它们作为开发人员非常容易使用。 如果遇到问题,您可以目视检查代码中的$ - gJSON对象并找出问题所在。 您甚至F - $ * } l m可以自己编辑JSON对象以添加或删除属性。 当使用以前从未使用 P X } O t m ^ t过的新API时,此功能特别有用。

gRPC请求上的协议缓冲区使直接查看通过网络传输的数据变得更加困难,因为它只是被编码为二进制数据c ~ A d ! J | Y"golang.o4 R e U u e N =rg/x/net/context\"
\"google.golang.org/g* e F L ` v Zrpc\"

\"github.com/Bimd# ! 9 fe/grpc-vs-rest/pb\"
)

type server struct{}

func main() {
lis, err := net.Listen(\"tcp\", \":9090[ r 6 d 4 t\")
if err != nil {
log.Fatalf(\"Failed to listen: %v\", err)n { i _ T *
}
s := grpc.NewServer()
pb.RegisterRandomServiceServer(s, &server{})
log.Println(\K ? ~ U N / d )"Starting gRPC s? k F s ? X }erver\")
if err := s.Serve/ t ! _ t , : : |(lis); err != nil {
log.Fatalf(\"Failed toi p n 9 C serve: %v\", err)
}
}

func (s *server) DoSomething(_ context.Context, random *pb.Random) (*pb.Random, error) {
random.RandomString = \" \" + random.RandomString;
return raZ | ;ndom, nil
}


package main

import (
\"log\"
\"encoding/json\"
\"} f 7 W l A ^ Pnet/httN Fp\"
\"github.com/Bimde/grpc-vs-rest/pb\"
)

func handle(w http.ResponseWriterS d % Y r ^ L G B, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var random pbp ) ] : j ..Random
if err := decoder.Decode(&rand5 0 J + & ( d hom); err != nil {
panic(err)
}
random.RandomSU L 9 8 u I ztring = \" \" + random.RandomString

bytes, err := json.Z Y J X , t Marshal(&random)
if err != nI X 9 #il {
panic(err)
}

w.Header().Set(\"Content-Type\", \"U 8 ( ~ & 5 , d 0application/json\w P $ ` 7 P G i ]")
w.Write(bytU r 4 J 8 R j n xes)
}

func maQ ) Y T & 3in() {
server := &http{ * K t o &.Server{Addr: \"biY } pmde:b : + 7 E X _8080\", Handler: http.HandlerFunc(handle)}
log.Fatal(server.ListenAndServeTLS(\"server.crt\", ? ^ t A Z V\"server.key\"))
}


这两个服务器程序都非常; d J r m 8 rpc.WithInsecure())
if erj X Br != nil {
log.Fatalf(\"Dial failed: %v\", err)
}
client := pb.NewRandomServiceClient(conn)
requestQueue := make(chan Request)
defer startWorkers(&aa U 1 I c W =mp;requestQueue, noWorkers, getStartGRPCWorkerFunc I O a E / K m mtion(client))()
b.ResetTimer() // don\'t count worker initialization time

foq % !r i := 0; i < b.N; i++ {
rH S F cequestD : N ! ! R . * QQueue <- Request{
Path: \"http://localhost:9090\",
Random:5 n h f M _ &pb.RanD t V = M 7dom{
RandomInt: 2019,
RandomString: \"a_string\",
},
}
}
}

func getStartGRPCj $ 8 g ] Y mWorkerFunction(clis ? J 5 ? Aent pb.RandomServiceClo N [ 2 {ient) func (*chan Request, *sync.WaitGroup) {
return func(requestQueue *chan Request, wg *s7 - = Y w w hync.WaitGroup) {
go func() {
for {@ * g
request := &l[ d y : 6t;- *requestQueue
if (request.Path == stopRequestPath) {
wg.Done()
return
}
client.DoSy N , 1 . d r Yomething(context.TODO(), rH M iequest.Random)
}
}()
}
}


请注意,getStartGRPCWorkerFM ( 6 / p ^unction函数返回其中带有RandomServiceClient的闭包。 这~ 5 y # p A样一来,我们只需拨打一次g+ R t %RPC服务器,即可完成整个测试,而仅执行一次TCP握手。

结果

从REST到gRPC:性能如何优化

尽管基H g = | T于HTTP / 2的REST的扩展能力与gRPC差不多,但就纯性能而言,gRPC在整个工作负载范围内可将处理时间减少50%至75%。



结果

让我们回到原始标准:

语言中立

快速

易于使用

在语言支持方面,JSON支持的REST无疑是赢家。 在过去的几年中,gRPC的语言支持已大大改善,可以说对于大多数用例来说已经足够; n / [ H |了。

我们的性能比较从所有用例中N D l 8 u v j消除了HTTP / 1.1,但通过前端API服务支持旧版客户端。 在gRPC和HTTP / 2上的REST之间,性能差异: { v l仍然很大。 每当请求性能成为关键问题时,gRPC似乎都z C u j } B ` w x是正确的选择。

在易用性方面,与REST相比,开发人员只需编写更少的代码即可在gRPC中完成相同的@ / M N任务。 调试是不同的& Q * 0 h H o J !,但不一定会( k N ? G更困难。 开发人员习惯于新范式的问题更多。

从我# [ Z P n I们的发现中,我们可以看到gRPC是内部服务与服务通信之间更好的解决方案。 它具有更好3 k } ! , S (的性能,提高了开发速度,并且在语言方面足够中立。 我们可以得出结论,除非需要REST支持外部客户x - 2 U }端或尚未支持gRPC的语言/平台,否则我们应该默认构建gRPC服务。

影响

减少客户的等待时间; 更好的用户体验

缩短请求处理时间; 降低成本

提高开发人员效率; 降低公司成本,开发更多新功能


源代码

私信译者获得本文代码的github地址


(本文翻译自+ ) ( L s 2 G | %Bimesh De Silva的文章《gRPC vs. REST+ p N ~ = % s: Performance Simplified》,参考:https://med. ( t h c Kium.com/@bimeshde/grpc-vs-rest-performan} / :ce-simplified-fd35d01bbd4)

美元测试确保项目工作需求性能定南优化