就概念而言,构建 RESTAPI 很容易。但是扩展它们以接受巨大的流量是一个挑战。到目前为止,我们研究了创建 REST API 结构和示例 REST API 的细节。在本章中,我们将探讨 Go 工具包,这是一个用于构建微服务的奇妙、惯用的 Go 包。这是一个微服务时代,初创企业很快就会转变为企业。微服务架构允许公司快速并行迭代。我们将从定义微服务开始,然后通过创建 REST 样式的微服务进入 Go Kit。
在本章中,我们将介绍以下主题:
- 整体式服务与微服务的区别
- 对微服务的需求
- 介绍 Go Kit,Go 中的一个微服务工具包
- 使用 Go 工具包创建 restapi
- 向 API 添加日志记录
- 向 API 添加工具
您可以通过 GitHub 存储库链接获取本章的代码示例 https://github.com/narenaryan/gorestful/tree/master/chapter9 。在上一章中,我们讨论了 GoAPI 客户端。这里,我们回到带微服务架构的 RESTAPI。
什么是微服务?这是企业界向计算界提出的问题。由于团队规模更大,这些公司准备采用微服务来分解任务。微服务体系结构用粒度服务取代了传统的整体结构,粒度服务通过某种协议相互通信。
微服务为该板块带来以下好处:
- 如果团队很大,人们可以处理大量的应用程序
- 对于新开发人员来说,适应性很容易
- 采用最佳实践,如持续集成(CI)和持续交付(CD)
- 具有松散耦合体系结构的易于更换的软件
在整体式应用程序(传统应用程序)中,单个大型服务器通过复用计算能力为传入请求提供服务。这很好,因为我们在一个地方拥有所有东西,比如应用服务器、数据库和其他东西。它也有缺点。当一个软件坏了,一切都坏了。此外,开发人员需要设置整个应用程序来开发一小块。
单片应用程序的缺点列表可能是:
- 紧耦合结构
- 单点失效
- 添加新功能和组件的速度
- 工作的分散仅限于团队
- 连续部署非常困难,因为需要推送整个应用程序
查看 monolith 应用程序,整个堆栈被视为单个实体。如果数据库失败,应用程序也会失败。如果代码中的错误使软件应用程序崩溃,那么与客户端的整个连接就会中断。这实际上导致了微服务的出现。
让我们看一个场景。Bob 运营的一家公司使用传统的面向服务的架构(SOA),开发人员日以继夜地工作以添加新功能。如果有发布,人们需要对每个小组件的代码进行全面测试。完成所有更改后,项目将从开发转移到测试。下一条街上的另一家公司,由 Alice 经营,使用微服务架构。Alice 公司的所有软件开发人员都致力于单独的服务,这些服务通过一个持续的构建管道进行测试,该管道可以非常快速地通知事情。开发人员相互讨论 REST/RPC API 以添加新特性。与 Bob 的开发人员相比,他们可以轻松地将堆栈从一种技术转移到另一种技术。这个例子表明,Alice 公司的灵活性和速度比 Bob 公司大。
微服务还创建了一个平台,允许我们使用容器(docker 等)。在微服务中,编排和服务发现对于跟踪松散耦合的元素非常重要。Kubernetes 等工具用于管理 docker 容器。一般来说,为微服务提供 docker 容器是一个很好的实践。服务发现是动态自动检测 IP 地址和其他详细信息。这消除了硬编码微服务相互协商所需内容的潜在威胁。
行业专家建议将软件应用程序作为一个整体启动,然后从长远来看将其分解为微服务。这实际上帮助我们关注应用程序交付,而不是研究微服务模式。一旦产品稳定下来,开发人员就应该找到一种松散耦合功能的方法。请看下图:
此图描述了整体式和微服务架构的结构。一块巨石把所有的东西都包裹成洋葱的形状。它被称为紧密耦合系统。相反,微服务是个性化的,易于更换和修改。每个微服务都可以通过各种传输机制(如 HTTP 和 RPC)相互通信。格式可以是 JSON 或协议缓冲区。
在企业界,人们从 Java 社区了解 Netflix 的 Eureka 和 Spring Boot。在 Go 中,试图达到该实现级别的包显然是Go kit。它是一个用于构建微服务的工具包。
它有一种添加服务的 Go 风格,这让我们感觉很好。它附带了一个添加微服务的过程。在接下来的部分中,我们将看到如何使用 Go Kit 定义的步骤创建微服务。它主要由许多层组成,Go Kit 中的请求和响应流分为三层:
- 传输层:负责将数据从一个服务传输到另一个服务
- 端点层:负责为给定服务构建端点
- 服务层:这是 API 处理程序的实际业务逻辑
使用以下命令安装 Go 套件:
go get github.com/go-kit/kit
让我们为我们的第一个微服务制定计划。我们都知道信息的加密。可以使用一个密钥对消息字符串进行加密,该密钥输出一条可以通过电线传递的乱七八糟的消息。收件人解密消息并返回原始字符串。这个过程在密码学中称为加密。我们将尝试将其作为微服务演示的一部分来实现:
- 首先,开发加密逻辑
- 然后,将其与 Go Kit 集成
Go 附带用于加密消息的软件包。我们需要从这些包中导入加密算法并使用它们。作为第一步的一部分,我们将编写一个使用高级加密标准(AES的项目。
在您的GOPATH/src/user
目录中创建一个名为encryptString
的目录:
mkdir $GOPATH/src/github.com/narenaryan/encryptString
cd $GOPATH/src/github.com/narenaryan/encryptString
现在让我们在新目录中再添加一个名为 utils 的目录。在项目目录中添加两个文件main.go
,在名为utils
的新目录中添加utils.go
。目录结构如下所示:
**```go └── encryptString ├── main.go └── utils └── utils.go
现在让我们在`utils.go`文件中添加加密逻辑。我们创建了两个函数,一个用于加密,另一个用于解密消息,如以下代码所示:
```go
package utils
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)
AES 算法采用初始化向量。让我们首先定义一下:
// Implements AES encryption algorithm(Rijndael Algorithm)
/* Initialization vector for the AES algorithm
More details visit this link https://en.wikipedia.org/wiki/Advanced_Encryption_Standard */
var initVector = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 05}
现在,让我们实现加密和解密的逻辑:
// EncryptString encrypts the string with given key
func EncryptString(key, text string) string {
block, err := aes.NewCipher([]byte(key))
if err != nil {
panic(err)
}
plaintext := []byte(text)
cfb := cipher.NewCFBEncrypter(block, initVector)
ciphertext := make([]byte, len(plaintext))
cfb.XORKeyStream(ciphertext, plaintext)
return base64.StdEncoding.EncodeToString(ciphertext)
}
在EncryptString
函数中,我们使用密钥创建一个新的密码块。然后我们将该块传递给密码块加密函数。该加密程序接受块和初始化向量。然后,我们通过对密码块执行XORKeyStream
生成密文(加密消息)。它填充密文。然后我们需要进行 Base64 编码以生成受保护的字符串:
// DecryptString decrypts the encrypted string to original
func DecryptString(key, text string) string {
block, err := aes.NewCipher([]byte(key))
if err != nil {
panic(err)
}
ciphertext, _ := base64.StdEncoding.DecodeString(text)
cfb := cipher.NewCFBEncrypter(block, initVector)
plaintext := make([]byte, len(ciphertext))
cfb.XORKeyStream(plaintext, ciphertext)
return string(plaintext)
}
在DecryptString
函数中,解码 Base64 编码并使用密钥创建密码块。将此密码块与初始化向量一起传递给NewCFBEncrypter
。接下来,使用XORKeyStream
将内容从密文加载到纯文本。基本上,这是一个交换XORKeyStream
中加密和解密消息的过程。这就完成了utils.go
文件。
****现在让我们编辑main.go
文件以利用前面的utils
包:
package main
import (
"log"
"github.com/narenaryan/encryptString/utils"
)
// AES keys should be of length 16, 24, 32
func main() {
key := "111023043350789514532147"
message := "I am A Message"
log.Println("Original message: ", message)
encryptedString := utils.EncryptString(key, message)
log.Println("Encrypted message: ", encryptedString)
decryptedString := utils.DecryptString(key, encryptedString)
log.Println("Decrypted message: ", decryptedString)
}
这里,我们从utils
包中导入加密/解密函数,并使用它们来展示一个示例。
如果运行此程序,我们将看到以下输出:
go run main.go
Original message: I am A Message
Encrypted message: 8/+JCfTb+ibIjzQtmCo=
Decrypted message: I am A Message
它展示了如何使用 AES 算法对消息进行加密,并使用相同的密钥将其取回。该算法也称为Rijndael(发音为 rain dahl)算法
有了这些知识,我们已经准备好构建第一个提供加密/解密 API 的微服务。我们使用 Go 工具包和加密utils
来编写微服务。正如我们在上一节所讨论的,Go Kit 微服务应该以逐步的方式构建。要创建服务,我们需要预先设计一些东西。他们是:
- 服务实现
- 端点
- 请求/响应模型
- 运输
坐好。这个术语现在看来似乎很陌生。我们很快就会适应的。让我们创建一个具有以下目录结构的目录。每个 Go Kit 项目都可以在此项目结构中。让我们调用我们的项目encryptService.
在encryptService
目录中以相同的树结构创建这些文件:
├── helpers
│ ├── endpoints.go
│ ├── implementations.go
│ ├── jsonutils.go
│ └── models.go
└── main.go
我们将浏览每个文件,看看应该如何构建。首先,在 GoKit 中,创建一个界面,告诉我们的微服务执行的所有功能。在这种情况下,这些函数是Encrypt
和Decrypt
。Encrypt
获取密钥并将文本转换为密码消息。Decrypt
使用密钥将密码信息转换回文本。请看下面的代码:
import (
"context"
)
// EncryptService is a blueprint for our service
type EncryptService interface {
Encrypt(context.Context, string, string) (string, error)
Decrypt(context.Context, string, string) (string, error)
}
服务需要实现这些功能以满足接口的要求。接下来,为您的服务创建模型。模型指定服务可以接收和生成哪些数据。在项目的helpers
目录中创建一个models.go
文件:
encryptService/helpers/models.go
package helpers
// EncryptRequest strctures request coming from client
type EncryptRequest struct {
Text string `json:"text"`
Key string `json:"key"`
}
// EncryptResponse strctures response going to the client
type EncryptResponse struct {
Message string `json:"message"`
Err string `json:"error"`
}
// DecryptRequest strctures request coming from client
type DecryptRequest struct {
Message string `json:"message"`
Key string `json:"key"`
}
// DecryptResponse strctures response going to the client
type DecryptResponse struct {
Text string `json:"text"`
Err string `json:"error"`
}
因为我们有两个服务函数,所以有四个函数映射到请求和响应。下一步是创建一个实现前面定义的接口EncryptService
的结构。因此,在以下路径的实现文件中创建该逻辑:
encryptService/helpers/implementations.go
首先,让我们导入所有必要的包。另外,请给出程序包名称:
package helpers
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
)
// EncryptServiceInstance is the implementation of interface for micro service
type EncryptServiceInstance struct{}
// Implements AES encryption algorithm(Rijndael Algorithm)
/* Initialization vector for the AES algorithm
More details visit this link https://en.wikipedia.org/wiki/Advanced_Encryption_Standard */
var initVector = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 05}
// Encrypt encrypts the string with given key
func (EncryptServiceInstance) Encrypt(_ context.Context, key string, text string) (string, error) {
block, err := aes.NewCipher([]byte(key))
if err != nil {
panic(err)
}
plaintext := []byte(text)
cfb := cipher.NewCFBEncrypter(block, initVector)
ciphertext := make([]byte, len(plaintext))
cfb.XORKeyStream(ciphertext, plaintext)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt decrypts the encrypted string to original
func (EncryptServiceInstance) Decrypt(_ context.Context, key string, text string) (string, error) {
if key == "" || text == "" {
return "", errEmpty
}
block, err := aes.NewCipher([]byte(key))
if err != nil {
panic(err)
}
ciphertext, _ := base64.StdEncoding.DecodeString(text)
cfb := cipher.NewCFBEncrypter(block, initVector)
plaintext := make([]byte, len(ciphertext))
cfb.XORKeyStream(plaintext, ciphertext)
return string(plaintext), nil
}
var errEmpty = errors.New("Secret Key or Text should not be empty")
这与我们在上一个示例中看到的 AES 加密相同。在这个文件中,我们正在创建一个名为EncyptionServiceInstance
的结构,它有两种方法,Encrypt
和Decrypt
。满足前面的接口。现在,我们如何将这些实际的服务实现与服务请求和响应联系起来?我们需要为此定义端点。因此,添加以下端点将服务请求与服务业务逻辑链接起来。
****我们使用Capitalized
函数和变量名,因为在 Go 中,任何大写的函数或变量都是从该包名导出的。在main.go
中,要使用所有这些函数,首先需要导出它们。使用大写名称可以让主程序看到它们
在helpers
目录中创建endpoints.go
:
package helpers
import (
"context"
"github.com/go-kit/kit/endpoint"
)
// EncryptService is a blueprint for our service
type EncryptService interface {
Encrypt(context.Context, string, string) (string, error)
Decrypt(context.Context, string, string) (string, error)
}
// MakeEncryptEndpoint forms endpoint for request/response of encrypt function
func MakeEncryptEndpoint(svc EncryptService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(EncryptRequest)
message, err := svc.Encrypt(ctx, req.Key, req.Text)
if err != nil {
return EncryptResponse{message, err.Error()}, nil
}
return EncryptResponse{message, ""}, nil
}
}
// MakeDecryptEndpoint forms endpoint for request/response of decrypt function
func MakeDecryptEndpoint(svc EncryptService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(DecryptRequest)
text, err := svc.Decrypt(ctx, req.Key, req.Message)
if err != nil {
return DecryptResponse{text, err.Error()}, nil
}
return DecryptResponse{text, ""}, nil
}
}
这里,我们使用端点定义代码来替换前面的接口定义代码。端点将服务作为参数并返回函数。此函数依次接受请求并返回响应。这些内容与我们在models.go
文件中定义的内容相同。我们检查错误,然后返回结构以获得响应。
**现在,一切都很好。在前面的 RESTAPI 示例中,我们总是尝试将 JSON 字符串解组到 Go 结构中。对于响应,我们通过封送将结构转换回 JSON 字符串。这里,我们分别解组和封送请求和响应。为此,我们再编写一个用于编码/解码逻辑的文件。我们将该文件命名为jsonutils.go
并将其添加到helpers
目录中:
package helpers
import (
"context"
"encoding/json"
"net/http"
)
// DecodeEncryptRequest fills struct from JSON details of request
func DecodeEncryptRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request EncryptRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
// DecodeDecryptRequest fills struct from JSON details of request
func DecodeDecryptRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request DecryptRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
// EncodeResponse is common for both the reponses from encrypt and decrypt services
func EncodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
return json.NewEncoder(w).Encode(response)
}
EncodeResponse
通常用于封送EncyptService
和DecryptService
的响应,但在将 JSON 解码为结构时,我们需要两种不同的方法。我们将其定义为DecodeEncryptRequest
和DecodeDecryptRequest
。这些函数使用 Go 的内部 JSON 包封送和解封送数据。
******现在我们有了所有帮助文件,这些文件都具有创建微服务所需的结构。让我们设计main
功能,将现有的东西导入并将微服务连接到服务器:
package main
import (
"log"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/narenaryan/encryptService/helpers"
)
func main() {
svc := helpers.EncryptServiceInstance{}
encryptHandler := httptransport.NewServer(helpers.MakeEncryptEndpoint(svc),
helpers.DecodeEncryptRequest,\
helpers.EncodeResponse)
decryptHandler := httptransport.NewServer(helpers.MakeDecryptEndpoint(svc),
helpers.DecodeDecryptRequest,
helpers.EncodeResponse)
http.Handle("/encrypt", encryptHandler)
http.Handle("/decrypt", decryptHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
我们正在导入 Go Kit 的传输/http 作为httptransport
来创建处理程序。处理程序附加端点、JSON 解码器和 JSON 编码器。然后,使用 Go 的 net/http,我们处理给定 URL 端点的 http 请求。**httptransport.NewServer
接受几个参数:端点、JSON 解码器和 JSON 编码器。服务执行的逻辑在哪里?它位于终点。端点获取请求模型并输出响应模型。现在,让我们在encryptService
目录下运行这个项目:**
****```go go run main.go
我们可以发出 curl`POST`请求来检查输出:
```go
curl -XPOST -d'{"key":"111023043350789514532147", "text": "I am A Message"}' localhost:8080/encrypt
{"message":"8/+JCfTb+ibIjzQtmCo=","error":""}
我们向微服务提供了密钥和消息。它返回了密码信息。这意味着服务对文本进行了加密。通过将相同的密钥与密码消息一起传递,再次请求解密消息:
curl -XPOST -d'{"key":"111023043350789514532147", "message": "8/+JCfTb+ibIjzQtmCo="}' localhost:8080/decrypt
{"text":"I am A Message","error":""}
它返回我们最初传递的确切信息。万岁!我们编写了第一个用于加密/解密消息的微服务。除了处理正常的 HTTP 请求外,Go Kit 还提供了许多其他有用的构造,例如中间件:
- 运输日志
- 应用程序日志记录
- 应用仪器
- 服务发现
在接下来的部分中,我们将讨论前面列表中的几个重要结构。
在本节中,让我们了解如何将传输级日志记录和应用程序级日志记录添加到 Go Kit 微服务中。我们使用上面的例子,但是稍微修改一下。让我们称我们的新项目为encryptServiceWithLogging
。在本书的 GitHub 项目中,您将找到此目录。在本书中,我们多次访问了中间件的概念。对于修订版,中间件是在请求/响应到达相应的请求处理程序之前/之后篡改请求/响应的功能。Go 工具包允许我们创建日志中间件,并将其连接到我们的服务。该中间件将具有日志逻辑。在本例中,我们尝试登录到 Stderr(控制台)。将一个名为middleware.go
的新文件添加到helpers
目录中,如下代码所示:
package helpers
import (
"context"
"time"
log "github.com/go-kit/kit/log"
)
// LoggingMiddleware wraps the logs for incoming requests
type LoggingMiddleware struct {
Logger log.Logger
Next EncryptService
}
// Encrypt logs the encyption requests
func (mw LoggingMiddleware) Encrypt(ctx context.Context, key string, text string) (output string, err error) {
defer func(begin time.Time) {
_ = mw.Logger.Log(
"method", "encrypt",
"key", key,
"text", text,
"output", output,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
output, err = mw.Next.Encrypt(ctx, key, text)
return
}
// Decrypt logs the encyption requests
func (mw LoggingMiddleware) Decrypt(ctx context.Context, key string,
text string) (output string, err error) {
defer func(begin time.Time) {
_ = mw.Logger.Log(
"method", "decrypt",
"key", key,
"message", text,
"output", output,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
output, err = mw.Next.Decrypt(ctx, key, text)
return
}
我们需要创建一个包含记录器和服务实例的结构。然后,在名称与服务方法类似的方法上定义几个方法(本例中为encrypt
和decrypt
。记录器是 Go 套件的记录器,具有Log
功能。这个Log
函数接受几个参数。它需要两个参数。第一套和第二套是一套。第三组和第四组是另一组。请参阅以下代码段:
mw.Logger.Log(
"method", "decrypt",
"key", key,
"message", text,
"output", output,
"err", err,
"took", time.Since(begin),
)
我们需要保持日志打印的顺序。在记录请求详细信息之后,我们确保允许请求转到使用此函数的下一个中间件/处理程序。Next
属于EncryptService
类型,这是我们的实际实现:
**```go mw.Next.(Encrypt/Decrypt)
对于加密功能,中间件记录加密请求并将其传递给服务的实现。为了将这个创建的中间件连接到我们的服务中,将`main.go`修改为:
```go
package main
import (
"log"
"net/http"
"os"
kitlog "github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/narenaryan/encryptService/helpers"
)
func main() {
logger := kitlog.NewLogfmtLogger(os.Stderr)
var svc helpers.EncryptService
svc = helpers.EncryptServiceInstance{}
svc = helpers.LoggingMiddleware{Logger: logger, Next: svc}
encryptHandler := httptransport.NewServer(helpers.MakeEncryptEndpoint(svc),
helpers.DecodeEncryptRequest,
helpers.EncodeResponse)
decryptHandler := httptransport.NewServer(helpers.MakeDecryptEndpoint(svc),
helpers.DecodeDecryptRequest,
helpers.EncodeResponse)
http.Handle("/encrypt", encryptHandler)
http.Handle("/decrypt", decryptHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
我们从 Go Kit 中导入日志作为kitlog
。我们使用NewLogfmtLogger(os.Stderr)
创建了一个新的记录器。这会将日志记录附加到控制台。现在,将此记录器和服务传递给LoggingMiddleware
。它返回可以传递给 HTTP 服务器的服务。现在,让我们从encryptServiceWithLogging
运行程序,看看控制台上记录了哪些输出:
**```go go run main.go
它启动了我们的微服务。现在,从`CURL`命令发出客户端请求:
```go
curl -XPOST -d'{"key":"111023043350789514532147", "text": "I am A Message"}' localhost:8080/encrypt
curl -XPOST -d'{"key":"111023043350789514532147", "message": "8/+JCfTb+ibIjzQtmCo="}' localhost:8080/decrypt
{"text":"I am A Message","error":""}
在服务器控制台上记录以下消息:
method=encrypt key=111023043350789514532147 text="I am A Message" output="8/+JCfTb+ibIjzQtmCo=" err=null took=11.32µs
method=decrypt key=111023043350789514532147 message="8/+JCfTb+ibIjzQtmCo=" output="I am A Message" err=null took=6.773µs
这是为了记录每个应用程序/服务的消息。系统级日志记录也可用,可以从 Go 工具包的文档中找到。
对于任何微服务,以及日志记录,仪器都是至关重要的。Go Kit 的metrics
包记录有关服务运行时行为的统计信息:计算处理的作业数,记录请求完成后的持续时间,等等。这也是一个篡改 HTTP 请求和收集度量的中间件。要定义中间件,只需再添加一个 struct,类似于日志中间件。除非我们进行监控,否则度量是无用的。Prometheus是一个度量监控工具,可以收集给定服务的延迟、请求数等。普罗米修斯从 Go Kit 生成的指标中获取数据。
你可以从这个网站下载普罗米修斯的最新稳定版本。在使用 Prometheus 之前,请确保安装 Go 工具包所需的以下软件包:
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp
一旦安装了这些,尝试将上一次讨论的日志服务项目复制到名为encryptServiceWithInstrumentation
的目录中。该目录完全相同,只是我们在helpers
目录中添加了一个名为instrumentation.go
的文件,并修改了我们的main.go
以导入插装中间件。项目结构如下所示:
├── helpers
│ ├── endpoints.go
│ ├── implementations.go
│ ├── instrumentation.go
│ ├── jsonutils.go
│ ├── middleware.go
│ └── models.go
└── main.go
检测可以根据参数(例如Counter
和Histogram
)分别测量每个服务的请求数和延迟。我们尝试创建一个具有这两个度量(请求计数、延迟)的中间件,并实现给定服务的功能。在这些中间件函数中,我们尝试调用 Prometheus 客户端 API 来增加请求数量、记录延迟等。核心 Prometheus 客户端库尝试以以下方式增加请求计数:
// Prometheus
c := prometheus.NewCounter(stdprometheus.CounterOpts{
Name: "request_duration",
...
}, []string{"method", "status_code"})
c.With("method", "MyMethod", "status_code", strconv.Itoa(code)).Add(1)
NewCounter
创建一个需要计数器选项的新计数器结构。这些选项是操作的名称和其他详细信息。然后,我们需要使用方法、方法名称和错误代码在结构上调用With
函数。普罗米修斯需要这个特殊的签名来生成计数器度量。最后,我们使用Add(1)
函数调用递增计数器。
******新添加的文件instrumentation.go
实现如下:
package helpers
import (
"context"
"fmt"
"time"
"github.com/go-kit/kit/metrics"
)
// InstrumentingMiddleware is a struct representing middleware
type InstrumentingMiddleware struct {
RequestCount metrics.Counter
RequestLatency metrics.Histogram
Next EncryptService
}
func (mw InstrumentingMiddleware) Encrypt(ctx context.Context, key string, text string) (output string, err error) {
defer func(begin time.Time) {
lvs := []string{"method", "encrypt", "error", fmt.Sprint(err != nil)}
mw.RequestCount.With(lvs...).Add(1)
mw.RequestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
}(time.Now())
output, err = mw.Next.Encrypt(ctx, key, text)
return
}
func (mw InstrumentingMiddleware) Decrypt(ctx context.Context, key string, text string) (output string, err error) {
defer func(begin time.Time) {
lvs := []string{"method", "decrypt", "error", "false"}
mw.RequestCount.With(lvs...).Add(1)
mw.RequestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
}(time.Now())
output, err = mw.Next.Decrypt(ctx, key, text)
return
}
这与日志中间件代码完全相同。我们用几个字段创建了一个结构。我们附加了加密和解密服务的函数。在中间件功能中,我们寻找两个指标;一个是计数,第二个是延迟。通过此中间件传递请求时:
mw.RequestCount.With(lvs...).Add(1)
这行递增计数器。现在看另一行:
mw.RequestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
此行通过计算请求到达时间和最终时间之间的差异来观察延迟(因为使用了 defer 关键字,这将在请求和响应周期完成后执行)。简单地说,前面的中间件将请求计数和延迟记录到 Prometheus 客户端提供的度量中。现在让我们修改main.go
文件,如下所示:
package main
import (
"log"
"net/http"
"os"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
kitlog "github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/narenaryan/encryptService/helpers"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
)
func main() {
logger := kitlog.NewLogfmtLogger(os.Stderr)
fieldKeys := []string{"method", "error"}
requestCount := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "encryption",
Subsystem: "my_service",
Name: "request_count",
Help: "Number of requests received.",
}, fieldKeys)
requestLatency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "encryption",
Subsystem: "my_service",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, fieldKeys)
var svc helpers.EncryptService
svc = helpers.EncryptServiceInstance{}
svc = helpers.LoggingMiddleware{Logger: logger, Next: svc}
svc = helpers.InstrumentingMiddleware{RequestCount: requestCount, RequestLatency: requestLatency, Next: svc}
encryptHandler := httptransport.NewServer(helpers.MakeEncryptEndpoint(svc),
helpers.DecodeEncryptRequest,
helpers.EncodeResponse)
decryptHandler := httptransport.NewServer(helpers.MakeDecryptEndpoint(svc),
helpers.DecodeDecryptRequest,
helpers.EncodeResponse)
http.Handle("/encrypt", encryptHandler)
http.Handle("/decrypt", decryptHandler)
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))
}
我们正在导入用于初始化度量模板的工具包 Prometheus 包,以及用于提供选项结构的客户端 Prometheus 包。我们正在创建requestCount
和requestLatency
度量类型结构,并将它们传递给我们的InstrumentingMiddleware
,这是从helpers
导入的。如果您看到这一行:
**```go requestCount := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: "encryption", Subsystem: "my_service", Name: "request_count", Help: "Number of requests received.", }, fieldKeys)
这就是我们如何创建一个与`helpers.go`中`InstrumentingMiddleware`**结构中的`RequestCount`**匹配的模板。我们传递的选项将在生成度量时附加到单个字符串:****
****```go
encryption_my_service_request_count
这是一个唯一可识别的服务工具,它告诉我们,这是我的微服务的请求计数操作,称为加密。在main.go
中,我们在代码的服务器部分又添加了一行有趣的代码:
"github.com/prometheus/client_golang/prometheus/promhttp"
...
http.Handle("/metrics", promhttp.Handler())
这实际上创建了一个端点,该端点可以生成包含收集的度量的页面。普罗米修斯可以对该页面进行抓取(解析)以存储、绘制和显示度量。如果我们运行该程序并向加密服务发出 5 个 HTTP 请求,向解密服务发出 10 个 HTTP 请求,则度量页面会记录请求的计数及其延迟:
go run main.go # This starts the server
从另一个 bash shell(Linux 中)循环向加密服务发出 5 个 CURL 请求:
for i in 1 2 3 4 5; do curl -XPOST -d'{"key":"111023043350789514532147", "text": "I am A Message"}' localhost:8080/encrypt; done
{"message":"8/+JCfTb+ibIjzQtmCo=","error":""}
{"message":"8/+JCfTb+ibIjzQtmCo=","error":""}
{"message":"8/+JCfTb+ibIjzQtmCo=","error":""}
{"message":"8/+JCfTb+ibIjzQtmCo=","error":""}
{"message":"8/+JCfTb+ibIjzQtmCo=","error":""}
在解密服务的循环中发出 10 个 CURL 请求(为简洁起见,输出是隐藏的):
for i in 1 2 3 4 5 6 7 8 9 10; do curl -XPOST -d'{"key":"111023043350789514532147", "message": "8/+JCfTb+ibIjzQtmCo="}' localhost:8080/decrypt; done
现在,访问 URLhttp://localhost:8080/metrics
,您将看到普罗米修斯 Go 客户端为我们生成的页面。页面内容将包含以下信息:
# HELP encryption_my_service_request_count Number of requests received.
# TYPE encryption_my_service_request_count counter
encryption_my_service_request_count{error="false",method="decrypt"} 10
encryption_my_service_request_count{error="false",method="encrypt"} 5
# HELP encryption_my_service_request_latency_microseconds Total duration of requests in microseconds.
# TYPE encryption_my_service_request_latency_microseconds summary
encryption_my_service_request_latency_microseconds{error="false",method="decrypt",quantile="0.5"} 5.4538e-05
encryption_my_service_request_latency_microseconds{error="false",method="decrypt",quantile="0.9"} 7.6279e-05
encryption_my_service_request_latency_microseconds{error="false",method="decrypt",quantile="0.99"} 8.097e-05
encryption_my_service_request_latency_microseconds_sum{error="false",method="decrypt"} 0.000603101
encryption_my_service_request_latency_microseconds_count{error="false",method="decrypt"} 10
encryption_my_service_request_latency_microseconds{error="false",method="encrypt",quantile="0.5"} 5.02e-05
encryption_my_service_request_latency_microseconds{error="false",method="encrypt",quantile="0.9"} 8.8164e-05
encryption_my_service_request_latency_microseconds{error="false",method="encrypt",quantile="0.99"} 8.8164e-05
encryption_my_service_request_latency_microseconds_sum{error="false",method="encrypt"} 0.000284823
encryption_my_service_request_latency_microseconds_count{error="false",method="encrypt"} 5
如您所见,有两种类型的指标:
encryption_myservice_request_count
encryption_myservice_request_latency_microseconds
如果您看到对encrypt
方法和decrypt
方法的请求数量,它们与我们发出的 CURL 请求相匹配。
encryption_myservice
度量类型具有加密和解密微服务的计数和延迟度量。method 参数告诉我们度量是从哪个微服务中提取的。
**这些类型的指标为我们提供了关键的见解,例如哪个微服务被大量使用,以及延迟趋势如何随时间变化,等等。但是为了查看实际数据,您需要安装 Prometheus 服务器,并为 Prometheus 编写一个配置文件,以便从您的 Go Kit 服务中获取指标。有关在普罗米修斯中创建目标(主机生成度量页面)的更多信息,请访问https://prometheus.io/docs/operating/configuration/ 。
我们还可以将普罗米修斯的数据传递给 Grafana,Grafana 是一种用于绘制实时度量图表的图形和监控工具。Go Kit 提供了许多其他功能,例如服务发现。只有当系统松散耦合、监控和优化时,才能扩展微服务
在本章中,我们从微服务的定义开始。整体式应用程序和微服务之间的主要区别在于紧密耦合的体系结构被分解为松散耦合的体系结构。微服务使用基于 REST 的 JSON 或基于 RPC 的协议缓冲区相互通信。使用微服务,我们可以将业务逻辑分解为多个块。每项服务都能很好地完成一项工作。这种方法有一个缺点。监视和管理微服务是痛苦的。Go 提供了一个很棒的工具包,名为 Go Kit。它是一个微服务框架,我们可以使用它为微服务生成样板代码。
我们需要在 GoKit 中定义一些东西。我们需要为 GoKit 服务创建实现、端点和模型。端点接受请求并返回响应。实现具有服务的实际业务逻辑。模型是解码和编码请求和响应对象的好方法。Go Kit 提供了各种中间件,用于执行重要任务,如日志记录、检测(度量)和服务发现。
小型组织可以从一个整体开始,但在拥有庞大团队的大型组织中,微服务更适合。在下一章中,我们将看到如何使用 Nginx 部署 Go 服务。需要部署服务,才能将其暴露于外部世界。****************************************