Giới thiệu
Chào các bạn 👋👋👋, sau một thời gian dài chưa có thêm bài viết nào thì mình đã quay trở lại rồi đây :dead-inside: =)).
Dạo gần đây mình có tìm hiểu về Microservices và mình thấy có một vấn đề khá thú vị của nó đó là việc giao tiếp hay trao đổi dữ liệu giữa các services trong những ứng dụng triển khai kiến trúc microservices. Để có thể giao tiếp dữ liệu giữa các services ta có một số cách như: service mesh, RESTful, GraphQL, gRPC, ... Mình thì đặc biệt ấn tượng với gRPC do từ hồi chuyển sang làm Golang song song với PHP mình đã có nghe đến RPC và gRPC rồi. Vì vậy trong bài viết này mình sẽ chia sẻ cho các bạn cái nhìn tổng quan về RPC, gRPC và tại sao lại sử dụng nó cho việc giao tiếp giữa các service trong hệ thống microservices nhé. Trong bài viết mình có sử dụng một số ví dụ đơn giản được viết bằng Go nên các bạn nên có một chút kiến thức cơ bản về Go nhé. Nếu các bạn đã sẵn sàng rồi thì cùng bắt đầu thôi!!!.
RPC là gì?
Trước tiên để hiểu gRPC là gì thì mình sẽ giới thiệu cho các bạn 1 khái niệm khác trước đó là RPC. RPC (Remote Procedure Call) là một mô hình kỹ thuật mạng, là giao thức giao tiếp giữa các phần mềm (hoặc process - tiến trình) mà một chương trình có thể yêu cầu một dịch vụ từ một chương trình khác nằm trong một máy tính/máy chủ khác hay hiểu đơn giản là phương pháp gọi hàm từ một máy tính ở xa để lấy về kết quả.
RPC sử dụng mô hình client-sever. Trong đó, chương trình gửi yêu cầu đóng vai trò là client còn chương trình cung cấp dịch vụ cho yêu cầu là máy chủ. Để hiểu rõ hơn về mô hình này các bạn có thể xem hình bên dưới.
Đơn giản thì RPC nó có vậy thôi còn việc nó được ứng dụng như thế nào mình sẽ không đi sâu vào. Để hiểu rõ hơn về giao thức này thì mình có một ví dụ nhỏ bên dưới được viết bằng Go.
// File Sever: sever/main.go
package main import ( "log" "net" "net/rpc"
) type GreetingService struct{} func (p *GreetingService) Hello(request string, reply *string) error { *reply = "Hello " + request return nil
} func main() { // Đăng ký service với tên là Greeting có kiểu là GreetingService rpc.RegisterName("Greeting", new(GreetingService)) // Tạo sever chạy trên port 1234 listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("Can not create sever because:", err) } log.Print("Sever is listening on port 1234") for { // Chấp nhận connection conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } // Phục vụ lời gọi Client trên một goroutine khác để tiếp tục nhận các lời gọi RPC khác go rpc.ServeConn(conn) }
}
Ở dưới máy mọi người có thể tạo một thư mục root chứa hai thư mục đó là sever
và client
. Trong hai thư mục này sẽ chứa file main.go
chạy sever và client để phục vụ cho lời gọi RPC. Giải thích một chút thì trong file sever/main.go
mình sẽ tạo một service export một vài method và đăng ký service đó để phục vụ cho lời gọi RPC. Mình có mở thêm một sever tại port 1234
để client có thể yêu cầu tới.
// File Client: client/main.go
package main import ( "fmt" "log" "net/rpc"
) func main() { // Tạo Client kết nối đến địa chỉ localhost:1234 client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal(err) } var reply string // Thực hiện lời gọi RPC với service là Greeting và tên method là Hello err = client.Call("Greeting.Hello", "Simon", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply)
}
Trong file client/main.go
, mình chỉ đơn giản tạo một client để kết nối đến địa chỉ đã mở ở file sever/main.go
sau đó thực hiện lời gọi RPC. Sau khi đã thêm hai file này xong các bạn tiến hành mở 2 terminal để tiến hành chạy sever và client.
// Chạy server
go run server/main.go // Chạy client
go run client/main.go
Sau khi chạy xong các bạn sẽ được kết quả như bên hình dưới.
Hy vọng khi đọc đến đay các bạn đã có cái nhìn cơ bản và trả lời được câu hỏi RPC là gì rồi. Nếu bạn nào có bất kỳ thắc mắc nào về RPC hoặc chưa hiểu bất kỳ chỗ nào thì có thể comment xuống dưới để mọi người cùng thảo luận nhé. Nếu đã nắm được RPC là gì rồi thì chúng ta tiếp tục đến với phần tiếp theo đó là gRPC.
gRPC là gì?
gRPC là một framework RPC mã nguồn mở đã ngôn ngữ, hiện đại và hiệu năng cao có thể chạy trên bất cứ môi trường nào. Nó được Google phát triển dựa trên Protobuf, giao thức HTTP/2 và được công bố vào năm 2016.
Trong gRPC, bất cứ một ứng dụng client nào có thể gọi đến một phương thức trên một ứng dụng server nằm ở một máy khác (máy chủ khác) giúp cho việc tạo những ứng dụng hoặc dịch vụ phân tán trở nên dễ dàng hơn. gRPC dựa trên ý tưởng giống như RPC đó là khai báo dịch vụ, chỉ định những phương thức có thể được gọi từ xa (remote call) với các tham số và kiểu trả về mà phương thức đó định nghĩa tương tự như ví dụ trên.
Việc triển khai gRPC cũng rất đơn giản. Ở phía server sẽ cung cấp những lời gọi hàm và chạy một máy chủ gRPC để xử lý những lời gọi hàm được cung cấp từ client. Ở phía client có một gRPC Stub (được gọi là client của gRPC tương ứng với ngôn ngữ của service đó) cung cấp những hàm giống như ở phía server.
Tại là lại là gRPC?
Protobuf
Trong gRPC service thì ta sẽ sử dụng .proto
file của Protobuf để định nghĩa nhưng phương thức với những tham số và kiểu trả về để phục vụ cho những lời gọi RPC. Với .proto
file thì việc triển khai gRPC service trở nên rất dễ dàng. Mọi người sẽ không phải tự xây dựng gRPC Server và gRPC Stub ở phía client mà ta sẽ sử dụng tool hỗ trợ sẵn để sinh ra hai thành phần này từ proto
file tùy theo service mà mọi người xây dựng. Ta sẽ sử dụng những class đã được sinh ra từ file .proto
để thực hiện lời gọi RPC ở phía client và xử lý lời gọi ở phía server. Dưới đây là một ví dụ .proto
file và trong bài viết này mình sẽ không đi sâu vào phần này mà chỉ mang tính chất giới thiệu về nó. Cụ thể hơn các bạn có thể tham khảo tại đây hoặc trong tương lai mình sẽ viết thêm một bài viết về nó, hy vọng lúc đó nhận được sự đón nhận của các bạn.
// Greeting.proto
service Greeting { rpc SayHello (GreetingRequest) returns (GreetingReply) {}
} message GreetingRequest { required string name = 1;
} message GreetingReply { required string message = 1;
}
Protocol Buffers cung cấp tool hỗ trợ hầu hết các ngôn ngữ:
- gRPC Core - C, C++, Ruby, Node.js, ...
- gRPC Go
- gRPC Kotlin
- gRPC Swift
- gRPC Dart
- gRPC Web
- ...
Vì hỗ trợ rất nhiều ngôn ngữ nên gRPC mạng lại lợi ích rất khi cần triển khai hệ thống microervices mà mỗi service sử dụng một ngôn ngữ. Vì sự tiện dụng của nó mà khiến cho gRPC trở nên rất phổ biến như ngày nay.
High Performance
Ngoài lý do hỗ trợ hầu hết loại ngôn ngữ kể trên thì còn một lý do khác nữa khiến gRPC trở nên phổ biến đó chính là hiệu năng cao. gRPC sử dụng binary encoding format nên nó nhanh hơn nhiều so với JSON format.
Giả sử hệ thống services của bạn có rất nhiều services và đang phải xử lý rất nhiều yêu cầu. Khi một yêu cầu phải tổng hợp dữ liệu từ nhiều services. Ở mỗi service này khi nhận các yêu cầu phải encode và decode (JSON) liên tục. Việc này chiếm rất nhiều tài nguyên của CPU mà lẽ ra CPU sẽ cần phải xử lý những việc khác quan trọng hơn là chỉ encoding và decoding dữ liệu trung gian. gRPC đã khắc phục được tình trạng này.
Ngoài ra gRPC còn tận dụng được sức mạng của HTTP/2 giúp nâng cao hiệu năng lên rất nhiều. Điều này cho phép gRPC xử lý được rất nhiều lời gọi RPC với số lượng kết nối TCP rất nhỏ.
Luồng hoạt động của gRPC
Mình sẽ có một ví dụ về luồng hoạt động cơ bản của gRPC . Trong ví dụ này thì Order Service đóng vai là client thực hiện lời gọi hàm đến Payment Service là sever.
Khi Order Service thực hiện lời gọi hàm đến Payment Service, gRPC sẽ encoding dữ liệu sang Protocol Buffer (binary) đưa vào client stub. Sau đó sẽ chuyển đến tầng transport để gửi dữ liệu stream đến Payment Service bằng HTTP/2. Bởi vì sử dụng binary encoding và tận dụng được sực mạnh của HTTP/2 nên gRPC sẽ nhanh hơn gấp 5 lần so với JSON. Sau khi Payment Service nhận được package sẽ decode nó và thực hiện lời gọi hàm bên phía sever (local call). Kết quả trả về sau khi hàm được gọi thực thi xong sẽ lại được encoding sang Protocol Buffer (binary) và lại được gửi đến tầng transport layer. Order Service sau khi nhận được package cũng tươn tự như bên Payment Service sẽ decode dữ liệu và trả về kết quả cho client. Để dễ hình dung hơn các bạn có thể tham khảo hình phía bên dưới.
Lời kết
Qua bài viết này mình và các bạn đã cùng tìm hiểu về khái niệm, đặc điểm, cách hoạt động của RPC và gRPC. Những kiến thức trên cũng chỉ là do mình tự tìm hiểu và học từ nhiều nguồn khác nhau nếu có sai sót gì mong mọi người có thể góp ý để bài viết hoàn thiện hơn. Hy vọng qua bài viết này các bạn có thể hiểu hơn về RPC, gRPC, cách chúng hoạt động và cách triển khai gRPC với hệ thống Microservices. Cảm ơn các bạn đã theo dõi đến hết bài viết ❤️.
Tham khảo
- Bộ tài liệu Advanced Go Programming
- ByteByteGo
- gRPC