vlambda博客
学习文章列表

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

Chapter 6: Scaling a Gin Application

在本章中,您将学习如何提高使用 Gin 框架编写的分布式 Web 应用程序的性能和可扩展性。本章将介绍如何使用缓存机制来缓解性能瓶颈。在此过程中,您将学习如何使用消息代理解决方案(例如 RabbitMQ)扩展 Web 应用程序。最后,您将学习如何容器化应用程序并使用Docker Compose进行扩展。

在本章中,我们将介绍以下主题:

  • 使用消息代理扩展工作负载
  • 使用 Docker 副本水平扩展
  • 使用 Nginx 反向代理
  • 使用 HTTP 缓存标头缓存资产

在本章结束时,您将能够使用 Gin 框架、Docker 和 RabbitMQ 构建一个高可用性和分布式 Web 应用程序。

Technical requirements

要遵循本章中的内容,您将需要以下内容:

  • 完整理解上一章。本章是上一章的后续,因为它将使用相同的源代码。因此,为了避免重复,将不解释某些片段。
  • 了解 Docker 及其架构。理想情况下,之前对消息队列服务(如 RabbitMQ、ActiveMQ、Kafka 等)的一些经验将是有益的。

本章的代码包托管在 GitHub 上,地址为 https: //github.com/PacktPublishing/Building-Distributed-Applications-in-Gin/tree/main/chapter06

Scaling workloads with a message broker

在开发 Web 应用程序时,用户体验中经常被忽略的一个重要方面是响应时间。没有什么能比一个缓慢而迟钝的应用程序更快地拒绝用户了。在之前的章节中,您学习了如何使用 Redis 减少数据库查询以加快数据访问速度。在本章中,您将进一步了解如何扩展使用 Gin 框架编写的 Web 应用程序。

在我们了解为什么需要扩展应用程序工作负载之前,让我们在架构中添加另一个块。新服务将解析 Reddit RSS 提要并将提要条目插入 MongoDB recipes 集合。下图说明了新服务如何与架构集成:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.1 – 解析 Reddit RSS 提要

该服务将使用 subreddit RSS URL 作为参数。我们可以通过将 .rss 添加到现有 subreddit URL 的末尾来创建 RSS 提要:

https://www.reddit.com/r/THREAD_NAME/.rss

例如,让我们看看下面截图中显示的食谱subreddit:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.2 – 食谱 subreddit

此 subreddit 将具有以下 RSS 提要 URL:

https://www.reddit.com/r/recipes/.rss

如果您访问上述 URL,您应该会收到一个 XML 响应。以下是食谱 subreddit 的 RSS URL 返回的 XML 结构示例:

<?xml version="1.0" encoding="UTF-8"?>
<feed
   xmlns="http://www.w3.org/2005/Atom"
   xmlns:media="http://search.yahoo.com/mrss/">
   <title>recipes</title>
   <entry>
       <author>
           <name>/u/nolynskitchen</name>
           <uri>https://www.reddit.com/user/nolynskitchen
           </uri>
       </author>
       <category term="recipes" label="r/recipes"/>
       <id>t3_m4uvlm</id>
       <media:thumbnail url="https://b.thumbs.
          redditmedia.com
          /vDz3xCmo10TFkokqy9y1chopeIXdOqtGA33joNBtTDA.jpg" 
        />
       <link href="https://www.reddit.com/r/recipes
                  /comments/m4uvlm/best_butter_cookies/" />
       <updated>2021-03-14T12:57:05+00:00</updated>
       <title>Best Butter Cookies!</title>
   </entry>
</feed>

要启动 ,请按以下步骤操作:

  1. 创建一个rss-parser项目,加载到VSCode编辑器中,编写一个main .go 文件。在文件中,声明第一个 struct Entry 之后,使用 Entry:
    type Feed struct 的 struct Feed {    Entries []Entry `xml:"entry"` } 类型条目结构{    链接结构{        Href字符串`xml:"href,attr"`    }`xml:"链接"`    缩略图结构{        URL字符串`xml:"url,attr"`    }`xml:"缩略图"`    标题字符串`xml:"title"` } 
  2. Next, write a GetFeedEntries method, which takes the RSS URL as a parameter, and return a list of entries:
    func GetFeedEntries(url string) ([]Entry, error) {    client := &http.Client{}    req, err := http.NewRequest("GET", url, nil)    if err != nil {        return nil, err    }    req.Header.Add("User-Agent", "Mozilla/5.0 (       Windows NT 10.0; Win64; x64) AppleWebKit/537.36       (KHTML, like Gecko) Chrome/70.0.3538.110       Safari/537.36")    resp, err := client.Do(req)    if err != nil {        return nil, err    }    defer resp.Body.Close()    byteValue, _ := ioutil.ReadAll(resp.Body)    var feed Feed    xml.Unmarshal(byteValue, &feed)    return feed.Entries, nil }

    此方法使用HTTP客户端GetFeedEntries中给出的URL发出GET请求 方法参数。然后,它将响应正文编码为 Feed 结构。最后,它返回 Entries 属性。

    请注意使用 User-Agent 请求标头来模拟从浏览器发送的请求并避免被 Reddit 服务器阻止:

    req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36") 

    笔记

    可以在以下 URL 中找到有效用户代理的列表(它会定期更新):https:// developer.whatismybrowser.com/useragents/explore/

  3. Next, create a web server with the Gin router and expose a POST request on the /parse endpoint. Then, define a route handler called ParserHandler:
    func main() {    router := gin.Default()    router.POST("/parse", ParserHandler)    router.Run(":5000") }

    ParserHandler 是不言自明的:它将请求负载编组到 Request 结构中。然后,它使用 URL 属性 调用 GetFeedEntries 方法代码类="literal">请求结构。最后,基于 方法的响应,它返回 500 错误代码或 200 状态代码,以及提要条目列表:

    func ParserHandler(c *gin.Context) {    var request Request    if err := c.ShouldBindJSON(&request); err != nil {        c.JSON(http.StatusBadRequest, gin.H{           "error": err.Error()})        return    }    entries, err := GetFeedEntries(request.URL)    if err != nil {        c.JSON(http.StatusInternalServerError,           gin.H{"error": "error while parsing                  the RSS feed"})        return    }    c.JSON(http.StatusOK, entries) } 

    Request 结构有一个 URL 属性:

    type Request struct {   URL string `json:"url"` }
  4. 要测试 ,请在不同的 端口上运行服务器(例如,5000< /code>) 以避免与配方 API 的端口冲突(已经在端口 8080 上运行):
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图6.3 - RSS解析器日志

  5. 在 Postman 客户端上,在 /parse 端点上发出一个 POST 请求,请求正文中包含 subreddit 的 URL。服务器将解析 RSS 提要并返回提要条目列表,如以下屏幕截图所示:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.4 – RSS 提要片段

  6. 现在,通过连接到前面章节中部署的 MongoDB 服务器,将 结果插入 MongoDB 。在init()方法上定义连接指令,如下:
    var client *mongo.Client var ctx context.Context 函数初始化(){    ctx = context.Background()    客户端,_ = mongo.Connect(ctx,       options.Client().ApplyURI(os.Getenv("MONGO_URI"))) }
  7. 然后,更新 HTTP 处理程序以将 条目插入 recipes 集合InsertOne 操作:
    func ParserHandler(c *gin.Context) {    ...    集合 := client.Database(os.Getenv(       "MONGO_DATABASE")).Collection("recipes")    对于 _, entry := range entries[2:] {        collection.InsertOne(ctx, bson.M{            "title":     entry.title,            “缩略图”:entry.Thumbnail.URL,            "url":       entry.Link.Href,        })    }    ... }
  8. 重新运行应用程序,但这一次,提供 MONGO_URIMONGO_DATABASE 环境变量,如下所示:
    MONGO_URI="mongodb ://admin:password@localhost:27017/test?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false" MONGO_DATABASE=demo go run main.go 
  9. 使用 Postman 或 curl 命令重新发出 POST 请求。返回 MongoDB Compass 并刷新 recipes 集合。 RSS 条目应该已成功插入,如下所示:
读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.5 – 食谱集合

笔记

如果您使用的数据库和集合与前面章节中显示的相同,则可能需要在插入新配方之前删除现有文档。

recipes 集合现在已经用一个食谱列表初始化

您可以重复相同的步骤来解析其他 subreddit RSS 提要。但是,如果您想解析数千或数百万个 subreddit 怎么办?处理如此大量的工作负载将占用大量资源(CPU/RAM)并且非常耗时。这就是为什么我们将服务逻辑分成多个松散耦合的服务,然后根据传入的工作负载对其进行扩展。

这些服务需要相互通信,最有效的通信方法是使用消息代理。这就是 RabbitMQ 发挥作用的地方。

Deploying RabbitMQ with Docker

RabbitMQ (https://www.rabbitmq.com/#getstarted ) 是一个可靠的、高度可用的消息代理。 以下模式描述了 RabbitMQ 将如何在应用程序架构中使用:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.6 – 使用 RabbitMQ 进行扩展

要使用 Docker 部署 RabbitMQ,请使用官方 RabbitMQ Docker 映像 ( https://hub.docker.com/_/rabbitmq)并执行以下步骤:

  1. Issue the following command to run a container from the RabbitMQ image:
    docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password -p 8080:15672 -p 5672:5672 rabbitmq:3-management

    用户名和密码是通过环境变量设置的。上述命令公开了端口 8080 上的 RabbitMQ 仪表板和端口 5672 上的服务器。

  2. Once the container has been deployed, run the following command to display the server logs:
    docker logs -f CONTAINER_ID 

    以下是启动日志的显示方式:

    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.7 – RabbitMQ 启动日志

  3. 服务器初始化完成后,通过浏览器导航到 localhost:8080。将显示一个 RabbitMQ 登录页面。使用您的用户/密码凭据登录。您将登陆仪表板:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图6.8 – RabbitMQ仪表板

  4. 现在,创建 一个消息队列,RSS URL 将被推送到 到由服务。点击导航栏中的 Queues 并通过点击 Add a new queue 创建一个新队列:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.9 – 创建一个新的 RabbitMQ 机会

  5. 确保Durability字段设置为数据会在磁盘上持久保存。

随着 RabbitMQ 的启动和运行,我们可以继续并实现一个生产者服务来将传入的 RSS URL 推送到 RabbitMQ,以及一个消费者服务来使用队列中的 URL。

Exploring the Producer/Consumer pattern

我们深入更深的实现之前,我们需要探索Producer /消费者模式。以下架构说明了这两个概念:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.10 – 生产者/消费者模式

现在主要概念已经清楚,让我们开始吧:

  1. 创建 一个名为 producer 的新 Go 项目并安装 RabbitMQ SDK for Golang使用以下命令:
    去获取 github.com/streadway/amqp
  2. 编写 一个 main.go 文件并使用以下代码段设置到 RabbitMQ 服务器的 TCP 连接:
    var频道Amqp *amqp.Channel 函数初始化(){    amqpConnection, err := amqp.Dial(os.Getenv(       "RABBITMQ_URI"))    如果错误 != nil {        log.Fatal(err)    }    channelAmqp, _ = amqpConnection.Channel() }

    通过RABBITMQ_URI提供AMQP连接环境字符串以及密码。

  3. 接下来,在 /parse 端点上定义一个 HTTP 处理程序。处理程序将使用 将请求 body 中给出的 URL 推送到 RabbitMQ 队列中a>Publish 方法:
    func ParserHandler(c *gin.Context) {    var request 请求    if err := c.ShouldBindJSON(&request);错误!=无{        c.JSON(http.StatusBadRequest, gin.H{           “错误”:err.Error()})        返回    }    数据,_ := json.Marshal(请求)    err := channelAmqp.Publish(        "",        os.Getenv("RABBITMQ_QUEUE"),        假,        假,        amqp.Publishing{            ContentType: "application/json",            正文:        []字节(数据),        })    如果错误 != nil {        fmt.Println(err)        c.JSON(http.StatusInternalServerError,           gin.H{"error": "发布时出错           到 RabbitMQ"})        返回    }    c.JSON(http.StatusOK, map[string]string{       “消息”:“成功”}) } 功能主要(){  路由器 := gin.Default()   router.POST("/parse", ParserHandler)   router.Run(":5000") }
  4. 最后,使用 RABBITMQ_URIRABBITMQ_QUEUE 变量,如 如下:
    RABBITMQ_URI="amqp://user:password@localhost:5672/" RABBITMQ_QUEUE=rss_urls go run main .go
  5. 然后,在 /parse 端点上执行 POST 请求。您应该会收到 200 success 消息,如下所示:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.11 – 在 RabbitMQ 中发布数据

  6. 前往 返回 RabbitMQ 仪表板,转到 Queues 部分,然后点击 rss_urls 队列上的 。您应该被重定向到 队列指标 页面。在这里,您会注意到队列中有一条消息:
读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.12 – 队列指标页面

随着生产者服务启动并运行,您需要构建工作器/消费者来使用 RabbitMQ 队列中可用的消息/URL:

  1. 创建一个名为consumer新Go项目并创建一个新文件称为 main.go。在文件中写入以下代码:
    func main() {    amqpConnection, err := amqp.Dial(os.Getenv(        "RABBITMQ_URI"))    如果错误 != nil {        log.Fatal(err)    }    延迟 amqpConnection.Close()    channelAmqp, _ := amqpConnection.Channel()    延迟通道Amqp.Close()    永远 := make(chan bool)    消息,错误 := channelAmqp.Consume(        os.Getenv("RABBITMQ_QUEUE"),        "",        真的,        假,        假,        假,        无,    )    去 func() {        for d := range msgs {            log.Printf("收到消息:%s", d.Body)        }    }()    log.Printf(" [*] 等待消息。               退出按CTRL+C")    <-永远 }

    代码 很简单:它 建立到 RabbitMQ a> 代码类的服务器连接并订阅 中的新消息="literal">rss_urls 获得一个新消息。

  2. Run the consumer project by passing the RabbitMQ URI and queue name as environment variables:
    RABBITMQ_URI="amqp://user:password@localhost:5672/" RABBITMQ_QUEUE=rss_urls go run main.go

    一旦启动,消费者将获取之前由生产者推送的消息,并将其内容显示在控制台上。然后,它将从队列中删除消息:

    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.13 – 从 RabbitMQ 订阅和获取消息

  3. 通过刷新 队列指标 页面来验证消息是否已被删除。 Queued messages 图表应确认此删除:
读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.14 – 从队列中删除消息

有了这个,你的工人/消费者已经建立起来了!

到目前为止,您已经看到消费者显示了消息的内容。现在,让我们进一步了解 并将消息正文编码到 Request 结构中,并通过调用 GetFeedEntries 方法,我们前面提到过。然后这些条目将保存在 MongoDB 的 recipes 集合中:

go func() {
       for d := range msgs {
           log.Printf("Received a message: %s", d.Body)
           var request Request
           json.Unmarshal(d.Body, &request)
           log.Println("RSS URL:", request.URL)
           entries, _ := GetFeedEntries(request.URL)
           collection := mongoClient.Database(os.Getenv(
              "MONGO_DATABASE")).Collection("recipes")
           for _, entry := range entries[2:] {
               collection.InsertOne(ctx, bson.M{
                   "title":     entry.Title,
                   "thumbnail": entry.Thumbnail.URL,
                   "url":       entry.Link.Href,
               })
           }
       }
}()

重新运行应用程序,但是这个时间,提供MongoDB连接参数,在添加到 RabbitMQ 参数:

RABBITMQ_URI="amqp://user:password@localhost:5672/" RABBITMQ_QUEUE=rss_urls MONGO_URI="mongodb://admin:password@localhost:27017/test?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false" MONGO_DATABASE=demo go run main.go 

要对此进行测试,请在请求正文中使用 RSS 提要 URL 向生产者服务器发出 POST 请求。生产者将在 RabbitMQ 队列中发布 URL。从那里,消费者将获取消息并获取 RSS URL 的 XML 响应,将响应编码为条目数组,并将结果保存在 MongoDB 中:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.15 – 消费者服务器日志

您可以向生产者服务器发出多个 subreddit URL。这一次,消费者会一一获取URL,如下:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.16 – 解析多个 RSS URL

要查看已保存在 MongoDB 中的条目,请构建一个简单的仪表板以列出 recipes 集合中的所有 recipes。您可以从头开始创建一个新项目或在 我们在上一章中构建的 Web 应用程序 上公开一个额外的路由来服务于食谱的 HTML 表示:

router.GET("/dashboard", IndexHandler)

然后,确保更新 Recipe 结构字段以镜像 MongoDB 文档字段的结构:

type Recipe struct {
   Title     string `json:"title" bson:"title"`
   Thumbnail string `json:"thumbnail" bson:"thumbnail"`
   URL       string `json:"url" bson:"url"`
} 

路由处理程序将简单地调用 recipes 集合上的 Find 操作以返回所有食谱。然后,它将结果编码到 recipes 切片中。最后,它将 recipes 变量传递到 HTML 模板中以显示结果:

func IndexHandler(c *gin.Context) {
   cur, err := collection.Find(ctx, bson.M{})
   if err != nil {
       c.JSON(http.StatusInternalServerError, 
           gin.H{"error": err.Error()})
       return
   }
   defer cur.Close(ctx)
   recipes := make([]Recipe, 0)
   for cur.Next(ctx) {
       var recipe Recipe
       cur.Decode(&recipe)
       recipes = append(recipes, recipe)
   }
   c.HTML(http.StatusOK, "index.tmpl", gin.H{
       "recipes": recipes,
   })
} 

以下 是 HTML 模板的内容。它使用 Bootstrap 框架 来构建吸引人的用户界面。它还使用 range 关键字循环遍历 recipes 切片中的每个配方并显示其详细信息(标题、缩略图和Reddit 网址):

<section class="container">
       <div class="row">
           <ul class="list-group">
               {{range .recipes}}
               <li class="list-group-item">
                   <div style="width: 100%;">
                       <img src="{{ .Thumbnail }}" 
                          class="card-img-top thumbnail">
                       <span class="title">{{ .Title 
                       }}</span>
                       <a href="{{ .URL }}" target="_blank"
                          class="btn btn-warning 
                          btn-sm see_recipe">See recipe</a>
                   </div>
               </li>
               {{end}}
           </ul>
       </div>
</section> 

配置 Gin 服务器以在端口 3000 上运行 并执行服务器go run main.go 命令 带有 MONGO_URIMONGO_DATABASE 变量。在您的浏览器上,前往 Localhost:3000/dashboard 除了,其中应返回食谱列表,如以下屏幕截图所示:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.17 – 趋势 Reddit 食谱

笔记

应用程序布局和样式表可以在本书的 GitHub 存储库中找到:https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin/blob/main/chapter06/dashboard/templates/index.tmpl

惊人的!你现在已经熟悉如何使用消息代理如RabbitMQ到扩展您的 Gin 分布式应用程序。在下一节中,我们将演示另一种通过 Docker 扩展 Gin 分布式应用程序的技术。

Scaling horizontally with Docker replicas

到目前为止,您已经学习了如何使用 Gin 框架 和 RabbitMQ 构建生产者/消费者架构。在本节中,我们将介绍如何扩展消费者组件,以便我们可以将传入的工作负载分配给多个消费者。

您可以通过构建消费者项目的 Docker 镜像并基于该镜像构建多个容器来实现此目的。 Docker 镜像是不可变的,这保证了每次容器基于运行的镜像时都是相同的环境。

以下模式说明了如何使用多个消费者/工作者:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.18 – 使用 Docker 扩展多个工人

要创建 Docker 映像,我们需要定义一个 Dockerfile - 一个包含运行消费者项目的所有指令的蓝图。在你的 worker/consumer 目录中创建一个 Dockerfile,内容如下:

FROM golang:1.16
WORKDIR /go/src/github.com/worker
COPY main.go go.mod go.sum ./
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest 
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/worker/app .
CMD ["./app"]

Dockerfile 使用 多阶段构建功能来构建轻量级 Docker 映像。我们将在下一节中看到它是如何工作的。

Using Docker multi-stage builds

多阶段构建是在 Docker Engine 1.17.05 中引入的。它允许您在 Dockerfile 中使用多个 FROM 语句 并复制工件从一个阶段到另一个阶段,在最终图像中留下您不需要的一切。

之前的示例中,您使用 golang:1.16 作为基础映像来构建单个二进制文件。然后,第二个 FROM 指令开始了一个以 Alpine 映像为基础的新构建阶段。从这里,您可以使用 COPY –from=0 指令从上一阶段复制二进制文件。结果,您最终会得到一个小的 Docker 映像。

要构建映像,请运行以下命令。最后的点很重要,因为它指向当前目录:

docker build -t worker . 

构建过程应该需要几秒钟才能完成。然后,您将找到以下 Docker 构建日志:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.19 – Docker 构建日志

如果您查看前面的输出,您会看到 Docker 根据我们的 Dockerfileworker 映像的每条指令>。构建映像后,运行以下命令以列出计算机中的可用映像:

docker image ls

工人/消费者形象应列在列表顶部:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.20 – Worker Docker 镜像

使用 docker run 命令运行基于图像的容器。您需要提供 MongoDB 和 RabbitMQ URI 作为带有 –e 标志的环境变量。 –link 标志可用于与容器内的 MongoDB 和 RabbitMQ 交互:

docker run -d -e MONGO_URI="mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false" -e MONGO_DATABASE=demo2 -e RABBITMQ_URI="amqp://user:password@rabbitmq:5672/" -e RABBITMQ_QUEUE=rss_urls --link rabbitmq --link mongodb --name worker worker 

容器日志如下:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.21 – Worker 的容器日志

有了这个,你已经对工人的服务进行了码头化。接下来,我们将使用 Docker Compose 对其进行扩展。

Scaling services with Docker Compose

Docker Compose 是一个容器编排工具,建立在 Docker Engine 之上。它可以帮助您使用单个命令行管理应用程序堆栈或多个容器。

使用 Docker Compose 就像创建 Docker 映像一样简单:

  1. 在项目的根目录中定义一个 docker-compose.yml 文件并输入以下 YAML 代码:
    version: "3.9" 服务: 工人:    图片:工人    环境:      - MONGO_URI="mongodb://admin:password            @mongodb:27017/test?authSource=admin            &readPreference=primary&ssl=false"      - MONGO_DATABASE=demo2      - RABBITMQ_URI=amqp://user:password@rabbitmq:5672      - RABBITMQ_QUEUE=rss_urls    网络:      -app_network    external_links:      -mongodb      -rabbitmq 网络: 应用网络:    external: true

    指定工作人员需要的环境变量和网络配置。

  2. 定义 一个外部网络,worker、MongoDB 和 RabbitMQ 服务将在其中生存。执行以下命令:
    docker network create app_network
  3. 重新部署 RabbitMQ 和 MongoDB 容器,但这一次,通过传递 –network 标志将它们部署在 app_network 自定义网络中:
    docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password -p 8080:15672 -p 5672:5672 --network app_network rabbitmq:3-management
  4. With the containers being configured properly, issue the following command to deploy the worker:
    docker-compose up -d

    -d 标志指示 Docker Compose 在后台运行容器(分离模式)。

  5. 发出以下命令以列出正在运行的服务:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.22 – Docker Compose 服务

  6. To scale the worker, rerun the previous command with the –scale flag:
    docker-compose up -d --scale worker=5

    最终的 输出将如下所示:

    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.23 – 扩展五个工人

  7. 要测试所有内容,请创建一个名为 threads 的文件,其中包含 Reddit 最佳烹饪和食谱子版块的列表。为简洁起见,以下列表已被裁剪:
    https://www.reddit.com/r/recipes/.rss https://www.reddit.com/r/food/.rss https://www.reddit.com/r/Cooking/.rss https://www.reddit.com/r/IndianFood/.rss https://www.reddit.com/r/Baking/.rss https://www.reddit.com/r/vegan/.rss https://www.reddit.com/r/fastfood/.rss https://www.reddit.com/r/vegetarian/.rss https://www.reddit.com/r/cookingforbeginners/.rss https://www.reddit.com/r/MealPrepSunday/.rss https://www.reddit.com/r/EatCheapAndHealthy/.rss https://www.reddit.com/r/Cheap_Meals/.rss https://www.reddit.com/r/slowcooking/.rss https://www.reddit.com/r/AskCulinary/.rss https://www.reddit.com/r/fromscratch/.rss
  8. 然后,编写bulk.sh shell脚本逐行读取threads文件,向生产者服务发出POST请求:
    #!/bin/bash 而 IFS= 读取 -r 线程 做    printf "\n$线程\n"    curl -X POST http://localhost:5000/parse -d      '{"url":"$thread"}' http://localhost:5000/parse 完成 < “线程”
  9. To run the script, add the execution permission and execute the file with the following command:
    chmod +x bulk.sh ./bulk.sh 

    笔记

    确保生产者服务正在运行,否则使用 curl 命令发出的 HTTP 请求将超时。

    脚本会逐行读取threads文件并发出POST请求,如下:

    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.24 – Shell 脚本的输出

  10. 运行 docker-compose logs -f 命令。这一次,您应该注意到正在使用多个 worker。此外,Docker Compose 为实例分配了颜色,并且每条消息都由不同的工作人员获取:
读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.25 – 将工作负载分配给多个工作人员

这样,您已成功将工作负载分配给多个工作人员。这种方法称为水平缩放。

笔记

第 8 章中,在 AWS 上部署应用程序,我们将介绍如何在 AWS 上部署 Web 应用程序 以及如何使用 Simple Queue Service (SQS) 而不是 RabbitMQ 来扩展工作人员。

Using the NGINX reverse proxy

在上一节中,您学习了如何扩展负责解析 subreddit URL 的工作人员。在本节中,您将探索如何通过在反向代理后面提供服务来扩展我们在之前章节中构建的Recipes API。

最常用的反向代理之一是 Nginx。它面向客户端并接收传入的 HTTP(S) 请求。然后,它以循环方式将它们重定向到其中一个 API 实例。要部署 Recipes API 的多个实例,您将使用 Docker Compose 来编排容器。以下架构说明了 单实例架构和 负载平衡多实例架构与 Nginx 之间的区别:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.26 – Nginx 的负载均衡多实例架构

Recipes API 扩展的另一个解决方案是垂直扩展,它包括增加服务所在系统的 CPU/RAM跑步。但是,从长远来看,这种方法往往有一些局限性(不具有成本效益)。这就是为什么在这里,您将采用水平扩展方法并在多个 API 实例之间分配负载。

笔记

Nginx 的替代品是 Traefik (https://doc.traefik.io/traefik/)。这是一个考虑到可扩展性而构建的开源项目。

以下 是对 Recipes API 输出的快速提醒:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.27 – GET /recipes 端点响应

要部署 Recipes API 的多个实例,请执行以下步骤:

  1. 构建 Docker 映像并在包含以下内容的 Recipes API 实现的文件夹中写入 Dockerfile:
    FROM golang:1.16 工作目录 /go/src/github.com/api 复制 。 . RUN go mod 下载 运行 CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app 。 来自高山:最新 运行 apk --no-cache 添加 ca 证书 工作目录 /root/ 复制 --from=0 /go/src/github.com/api/app 。 CMD ["./app"]
  2. This Dockerfile uses the multi-stage feature to ship a lightweight image. The docker build -t recipes-api. Command's output is as follows:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.28 – Docker 构建日志

    构建镜像后,创建一个 docker-compose.yml 文件并定义三个服务:

    API:Recipes API 容器

    Redis:内存中的缓存数据库

    MongoDB:存储配方的 NoSQL 数据库

  3. 然后,在文件中添加 以下行:
    version: "3.9" 服务: 接口:    图片:recipes-api    环境:      - MONGO_URI=mongodb://admin:password           @mongodb:27017/test?authSource=admin           &readPreference=primary&ssl=false      - MONGO_DATABASE=demo      - REDIS_URI=redis:6379    网络:      - api_network    external_links:      -mongodb      -redis 雷迪斯:    图片:redis    网络:      - api_network 蒙哥达:    图片:mongo:4.4.3    网络:      - api_network    环境:      - MONGO_INITDB_ROOT_USERNAME=admin      - MONGO_INITDB_ROOT_PASSWORD=密码 网络:    api_network:
  4. 接下来,使用以下代码块定义 Nginx 服务:
    nginx:    图片:nginx    端口:      - 80:80    卷:      - $PWD/nginx.conf:/etc/nginx/nginx.conf    取决于:      -api    网络:      - api_network 

    此代码将nginx.conf映射到容器内的

  5. location /api中,设置一个反向代理,将请求转发到端口8080(内部)。
  6. 使用 docker-compose up –d 命令部署整个堆栈。然后,发出以下命令以显示正在运行的服务:
    docker-compose ps

    该命令的输出如下:

    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图6.29 – 应用程序

  7. The Nginx service is exposed on port 80. Head to localhost/api/recipes; the server will call the Recipes API and forward the recipes list response, as follows:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.30 – 使用 Nginx 转发 HTTP 响应

    笔记

    对于生产用途,使用 HTTPS 保护 API 端点非常重要。幸运的是,您可以使用 Nginx 的“Let's Encrypt”插件自动生成 TLS 证书。

  8. 要确保从 Recipes API 转发响应,请使用以下命令检查 Nginx 服务日志:
    docker-compose logs –f nginx 
  9. 应该会看到类似这样的内容:
    /docker-entrypoint.sh: /docker-entrypoint.d/ 不为空,将尝试执行配置 /docker-entrypoint.sh:在 /docker-entrypoint.d/ 中寻找 shell 脚本 /docker-entrypoint.sh:启动 /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh 10-listen-on-ipv6-by-default.sh: info: 获取 /etc/nginx/conf.d/default.conf 的校验和 10-listen-on-ipv6-by-default.sh:信息:启用在 /etc/nginx/conf.d/default.conf 中侦听 IPv6 /docker-entrypoint.sh:启动 /docker-entrypoint.d/20-envsubst-on-templates.sh /docker-entrypoint.sh:启动 /docker-entrypoint.d/30-tune-worker-processes.sh /docker-entrypoint.sh:配置完成;准备启动 172.21.0.1 - - [21/Mar/2021:18:11:02 +0000] "GET /api/recipes HTTP/1.1" 200 2 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/ 537.36(KHTML,如 Gecko)Chrome/88.0.4324.192 Safari/537.36"
  10. 到目前为止,Recipes API 容器的一个实例正在运行。要扩展它,请使用 docker-compose up 命令的 –scale 标志或在 docker-compose up 命令中定义副本数code class="literal">docker-compose.yml 文件,如下:
    api:    图片:recipes-api    环境:      - MONGO_URI=mongodb://admin:password           @mongodb:27017/test?authSource=admin           &readPreference=primary&ssl=false      - MONGO_DATABASE=demo      - REDIS_URI=redis:6379    网络:      - api_network    external_links:      -mongodb      -redis    规模:5
  11. Re-execute the docker-compose up command. Four additional services will be created based on the Recipes API Docker image. Following are the service logs:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图 6.31 – 扩展 Recipes API

    现在,当客户端发送请求时,它将命中 Nginx,然后以循环方式转发到其中一个 API 服务。这有助于我们平均分配负载。

    笔记

    第 10 章,捕获 Gin 应用程序指标中,我们将学习如何设置监控平台以在需求增加时触发横向扩展事件以增加服务数量。

    使用反向代理的优点是您可以为整个分布式 Web 应用程序设置一个入口点。后端和 Web 应用程序 将位于相同的 URL。这样,您就不需要在 API 服务器上处理 CORS。

  12. Similar to the Recipes API, create a Docker image for the react-ui service. The following is the content of our Dockerfile:
    FROM node:14.15.1 COPY package-lock.json . COPY package.json . RUN npm install CMD npm start

    如您所见,这非常简单。在这里,您使用的是预构建的 Node.js 基础映像,因为 react-ui 服务是用 JavaScript 编写的。

  13. 使用 `docker build -t dashboard' 构建 Docker 镜像。然后,更新 docker-compose.yml 以便它使用以下代码块从映像运行 Docker 服务:
    dashboard:    图片:仪表板    网络:      - api_network
  14. 接下来,更新 nginx.conf 以便它将位于 URL 根级别的传入请求转发到仪表板服务:
    事件{    worker_connections 1024; } http { server_tokens 关闭; 服务器 {    听着 80;    root  /var/www;    位置/{      proxy_set_header X-Forwarded-For $remote_addr;      proxy_set_header 主机            $http_host;      proxy_pass http://dashboard:3000/;    }    位置 /api/ {      proxy_set_header X-Forwarded-For $remote_addr;      proxy_set_header 主机            $http_host;      proxy_pass http://api:8080/;    } } }
  15. 重新运行 docker-compose up -d 命令以使更改生效:
    读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

    图6.32 – 从Nginx提供Web仪表板

  16. 前往 localhost/dashboard;您将被重定向到我们在上一章中编写的 的 Web 仪表板:
读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.33 – 从同一个 URL 为两个后端提供服务

现在,RESTful API 和仪表板都由同一个域名提供服务。

要取下容器,您可以使用以下命令:

docker-compose down

笔记

如果您使用基于会话的身份验证,则需要在 Nginx 上配置 cookie 粘性,以将用户的会话保持在启动它的服务器上。

您还可以通过将仪表板位置替换为另一个位置部分,将仪表板位置替换为">nginx.conf 文件:

location /reddit/ {
     proxy_set_header X-Forwarded-For $remote_addr;
     proxy_set_header Host            $http_host;
     proxy_pass http://reddit-trending:3000/;
}

react-ui 类似,为 subreddits 应用程序创建一个 Docker 镜像。以下是我们的Dockerfile的内容:

FROM node:14.15.1
COPY . .
COPY package-lock.json .
COPY package.json .
RUN npm install
CMD npm start

在这里,您使用的是预构建的 Node.js 基础映像,因为仪表板服务是用 JavaScript 编写的。使用 "docker build -t dashboard" 构建 Docker 镜像。

另外,不要忘记将应用程序添加到 docker-compose.yml 文件中:

  reddit-trending:
    image: web
    networks:
      - api_network 

一旦您使用 docker-compose 重新部署了堆栈,前往 localhost/reddit ;您将被重定向到以下 UI:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.34 – Trending recipes 应用程序

应用程序布局被破坏,因为 app.css 文件是从错误的后端提供的。您可以通过在 Chrome 上打开调试控制台来确认这一点:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.35 – 样式表位置

您可以通过将以下代码块添加到 nginx.conf:

location /assets/css/app.css {
     proxy_set_header X-Forwarded-For $remote_addr;
     proxy_set_header Host            $http_host;
     proxy_pass http://reddit-trending:3000/assets
       /css/app.css;
}

现在,刷新网页;应用程序布局将被修复:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.36 – 应用程序布局

如您所见,仪表板显示食谱缩略图。然后,每次您刷新页面时,这些图像都会从 后端提供。为了减轻后端的压力,您可以配置 Nginx 来缓存静态文件。在 Nginx 配置文件的 server 部分之前插入以下代码片段:

map $sent_http_content_type $expires {
   default                    off;
   text/html                  epoch;
   text/css                   max;
   application/javascript     max;
   ~image/                    max;
}

~image 关键字将处理各种图像(PNG、JPEG、GIF、SVG 等)。现在,使用 server 部分中的 expires 指令配置过期:

http {
 server {
   listen 80;
   expires $expires;
   ...
 }
}

然后,使用以下命令重新部署 堆栈:

docker-compose up –d

现在应该缓存图像,这减少了到达后端的请求数量。在下一节中,我们将介绍如何在后端使用 Gin 获得相同的结果。

Caching assets with HTTP cache headers

您还可以使用 Gin 框架管理缓存。为了说明这一点,编写一个简单的 Web 应用程序来提供图像。代码如下:

func IllustrationHandler(c *gin.Context) {
   c.File("illustration.png")
}
func main() {
   router := gin.Default()
   router.GET("/illustration", IllustrationHandler)
   router.Run(":3000")
}

当用户点击 /illustration 资源 URL 时,应用程序应该提供图像:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.37 – 使用 Gin 提供图像

因为 总是传送相同的图像,所以我们需要确保我们正在缓存图像。这样,我们可以避免不必要的流量并获得更好的网络性能。让我们看看这是怎么做到的。

Setting HTTP caching headers

要缓存此 HTTP 请求,您可以附加 实体标签(ETag ) 到 HTTP 响应标头。当用户发送 HTTP 请求时,服务器会读取 HTTP 头并检查 If-None-Match 字段是否有 Etag 键值。如果 If-None-Match 字段与生成的键匹配,则将返回 304 状态代码:

func IllustrationHandler(c *gin.Context) {
   c.Header("Etag", "illustration")
   c.Header("Cache-Control", "max-age=2592000")
   if match := c.GetHeader("If-None-Match"); match != "" {
       if strings.Contains(match, "illustration") {
           c.Writer.WriteHeader(http.StatusNotModified)
           return
       }
   }
   c.File("illustration.png")
}

使用前面的代码更新 HTTP 处理程序后,对其进行测试。当您第一次 请求 /illustration 资源时,您应该获得 200 的状态好的。但是,对于第二个请求,您应该得到 304 StatusNotModified 响应:

读书笔记《building-distributed-applications-in-gin》第5章在Gin中提供静态HTML

图 6.38 – 使用 Gin 的响应缓存

您可能还注意到第二个请求的延迟比第一个请求的延迟更短。通过将查询数量保持在最低限度,您可以减轻 API 对应用程序性能的影响。

Summary

在本章中,您学习了如何使用基于微服务架构的 Gin 框架构建分布式 Web 应用程序。

您还探索了如何将 RabbitMQ 设置为微服务之间的消息代理,以及如何使用 Docker 扩展这些服务。在此过程中,您学习了如何使用 Docker 的多阶段构建功能来维护服务映像的大小,以及如何使用 Nginx 和 HTTP 缓存标头提高 API 的性能。

在下一章中,您将学习如何为 Gin Web 应用程序编写单元测试和集成测试。

Further reading

  • RabbitMQ Essentials – Second Edition, 作者:Lovisa Johansson,Packt Publishing
  • Docker for Developers,作者:Richard Bullington-McGuire、Andrew K. Dennis、Michael Schwartz,Packt Publishing。