读书笔记《building-distributed-applications-in-gin》第3章使用MongoDB管理数据持久化
Chapter 3: Managing Data Persistence with MongoDB
在上一章中,我们学习了如何使用 Gin Web 框架构建 RESTful API。在这一期中,我们将 MongoDB 集成到后端进行数据存储,我们还将介绍如何使用 Redis 作为缓存层来优化数据库查询。
在本章中,我们将介绍以下主题:
- Setting up MongoDB with Docker
- Configuring Go MongoDB driver
- Implementing MongoDB queries & and CRUD operations
- Standard Go project layout
- Deploying Redis with Docker
- Optimizing API response time with caching
- Performance benchmark with Apache Benchmark
在本章结束时,您将能够使用 Go 在 MongoDB 数据库上执行 CRUD 操作。
Technical requirements
要遵循本章中的内容,您将需要以下内容:
- You must have a complete understanding of the previous chapter since this chapter is a follow-up of the previous one; it will use the same source code. Hence, some snippets won't be explained to avoid repetition.
- Some knowledge of NoSQL concepts and MongoDB basic queries.
本章的代码包托管在 GitHub 上的 https: //github.com/PacktPublishing/Building-Distributed-Applications-in-Gin/tree/main/chapter03。
Running a MongoDB Server
到目前为止,我们构建的 API 没有连接到数据库。对于现实世界的应用程序,我们需要使用一种数据存储形式;否则,如果 API 崩溃或托管 API 的服务器出现故障,数据将丢失。 MongoDB 是最流行的 NoSQL 数据库之一。
以下模式显示了 MongoDB 将如何集成到 API 架构中:
在开始之前,我们需要部署一个 MongoDB 服务器。有很多部署选项:
- You can download the MongoDB Community Edition binary from the following URL: https://www.mongodb.com/try/download/community. Select a package based on your OS:
- You can use the MongoDB as a Service solution, known as MongoDB Atlas (https://www.mongodb.com/cloud/atlas), to run a free 500 MB database on the cloud. You can deploy a fully managed MongoDB server on AWS, Google Cloud Platform, or Microsoft Azure.
- You can run MongoDB locally with a containerization solution such as Docker. Multiple Docker images are available on DockerHub with a MongoDB server configured and ready to use out of the box.
我选择使用 Docker,因为它在运行临时环境中很受欢迎且简单。
Installing Docker CE
Docker (https://www.docker.com/get-started) 是一个允许您运行、构建和管理容器的开源项目。容器就像一个单独的操作系统,但没有虚拟化;它仅包含该应用程序所需的依赖项,这使得容器可移植和部署在本地或云上。
下图显示了容器和虚拟机在架构方法上的主要区别:
虚拟化发生在虚拟机的硬件级别,而对于容器,它发生在应用程序层。因此,容器可以共享操作系统内核和库,这使得它们非常轻量级和资源高效(CPU、RAM、磁盘等)。
首先,您需要在您的机器上安装 Docker Engine。导航到 https://docs.docker.com/get-docker/ 并安装 Docker您的平台:
笔记
Mac 用户还可以使用 Homebrew 实用程序通过 brew install docker
命令安装 Docker。
在撰写本书时,我正在使用 Docker Community Edition (CE< /strong>) 版本 20.10.2,如以下屏幕截图所示:
安装 Docker 后,您可以部署您的第一个容器。在终端会话中发出以下命令:
上述命令将基于 hello-world
镜像部署容器。当容器运行时,它会打印一个 Hello from Docker!消息并退出:
Running a MongoDB container
MongoDB的官方镜像可以在DockerHub上找到(https://hub.docker.com/_/mongo)。有 许多可用的图像,每个图像代表不同版本的 MongoDB。您可以使用 latest
标签找到它们;但是,建议指定目标版本。在编写本书时,MongoDB 4.4.3 是最新的稳定版本。执行以下命令以部署基于该版本的容器:
此命令将以分离模式运行 MongoDB 容器(-d
标志)。我们还将容器端口映射到主机端口,以便我们可以从主机级别访问数据库。最后,我们必须创建一个新用户并通过 MONGO_INITDB_ROOT_USERNAME
和 MONGO_INITDB_ROOT_PASSWORD
环境变量设置该用户的密码。
目前,MongoDB 凭证是纯文本格式。通过环境变量传递敏感信息的另一种方法是使用 Docker Secrets。如果您在 Swarm 模式下运行,则可以执行以下命令:
笔记
Docker Swarm 模式原生集成在 Docker 引擎中。它是一个容器编排平台,用于跨节点集群构建、部署和扩展容器。
此命令将为 MongoDB 用户生成一个随机密码并将其设置为 Docker 机密。
接下来,更新 docker run
命令,使其使用 Docker Secret 而不是纯文本密码:
docker run
命令的输出如下。它从 DockerHub 下载图像并从中创建一个实例(容器):
值得一提的是如果您已经在运行MongoDB容器,请确保在执行上一条命令之前将其删除;否则,您将收到“容器已存在”错误。要删除现有容器,请发出以下命令:
创建容器后,通过键入以下内容检查日志:
日志应该显示 MongoDB 服务器的健康检查:
笔记
建议您使用 Docker 卷将容器内的 /data/db
目录与底层主机系统映射。这样,如果 MongoDB 服务器出现故障或您的笔记本电脑重新启动,数据将不会丢失(数据持久性)。在主机系统上创建一个数据目录,并使用以下命令将该目录挂载到 /data/db
目录:
mkdir /home/data
docker run -d --name mongodb –v /home/data:/data/db -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -p 27017:27017 mongo:4.4.3
要与 MongoDB 服务器交互,您可以使用 MongoDB shell 在命令行上发出查询和查看数据。但是,还有一个更好的选择:MongoDB Compass。
安装 MongoDB 指南针
MongoDB Compass 是一个 GUI 工具,允许您轻松构建查询、了解数据库架构和分析索引,而无需了解 MongoDB 的查询语法。
从 https://www.mongodb.com/try/download/compass? 下载指南针? tck=docs_compass 基于您的操作系统:
一旦您下载了与您的操作系统相关的软件包,运行安装程序并按照它之后的步骤进行操作。安装后,打开 Compass,单击 New Connection,然后在输入字段中输入以下 URI(用您自己的凭据替换给定的凭据): mongodb://admin:password@localhost:27017/test
。
MongoDB 在本地运行,因此主机名是 localhost,端口是 27017:
点击连接按钮。现在,您已连接到 MongoDB 服务器。您将看到可用的数据库列表:
至此,我们拥有了一个功能性的 MongoDB 部署。在下一节中,我们将使用我们在上一章中构建的 Recipes API 与数据库进行交互。
笔记
要停止 MongoDB 服务器,请运行 docker ps
命令查看正在运行的容器列表,并运行 docker stop CONTAINER_ID
停止容器。
Configuring Go's MongoDB driver
我们在上一章实现的Recipes API 是用Golang 编写的。因此,我们需要安装官方的MongoDB Go驱动(https://github.com/mongodb/mongo -go-driver) 与 MongoDB 服务器交互。该驱动程序与 MongoDB API 完全集成,并支持 API 的所有主要查询和聚合功能。
发出以下命令以从 GitHub 安装包:
这会将包添加为 require
部分中的依赖项,在 go.mod
文件下:
在 init()
方法中,使用 Connect
mongo.Client > 功能。此函数将上下文作为参数和连接字符串,该字符串由名为 MONGO_URI
的环境变量提供。另外,创建以下全局变量;它们将用于所有 CRUD 操作功能:
笔记
我省略了一些代码以使示例可读且易于理解。完整的源代码在本书的 GitHub 存储库中,位于 chapter03
文件夹下。
一旦 Connect
方法 返回客户端对象,我们就可以使用 Ping
方法来检查连接是否成功。
将 MONGO_URI
环境变量传递给 go run
命令并检查应用程序是否可以成功连接到您的 MongoDB 服务器:
如果成功,将显示 Connected to MongoDB 消息:
Exploring MongoDB queries
在本节中,我们将使用 CRUD 操作与 MongoDB 服务器交互 ,但首先,让我们创建一个用于存储 API 数据的数据库。
笔记
您可以在 GoDoc 网站上查看 MongoDB Go 驱动程序的完整文档 (https://godoc .org/go.mongodb.org/mongo-driver)。
The InsertMany operation
让我们用 在上一章。首先,检索一个 Database
,然后从 Client
检索一个 Collection
实例。 Collection
实例将用于插入文档:
上述代码读取一个 JSON 文件 (https ://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin/blob/main/chapter03/recipes.json),其中包含食谱列表,并将其编码为 < code class="literal">Recipe 结构。然后,它与 MongoDB 服务器建立连接并将食谱插入 recipes
集合。
要一次插入多个 文档,我们可以使用 InsertMany()
方法。此方法接受接口切片作为参数。因此,我们必须将Recipes
struct slice 映射到interface slice。
重新运行应用程序,但这一次,设置 MONGO_URI 和 MONGO_DATABASE
变量如下:
确保将 USER
替换为您的数据库用户,并将 PASSWORD
替换为我们在部署 MongoDB 容器时创建的用户密码。
应用程序将启动; init()
方法将首先被执行,配方项将被插入到 MongoDB 集合中:
要验证数据是否已加载到食谱集合中,请刷新 MongoDB Compass。您应该看到您创建的条目:
现在recipes
集合已经准备好了,我们需要更新每一个API 端点的代码,以便他们使用集合而不是硬编码的配方列表。但首先,我们需要更新 init()
方法以移除 recipes.json
文件的加载和编码:
值得一提的是,您可以使用 mongoimport
实用程序将 recipe.json
文件直接加载到 recipes
集合,无需在 Golang 中编写一行代码。执行此操作的命令如下:
此命令 会将 JSON 文件 中的内容导入 recipes
集合:
在下一节中,我们将更新现有的函数处理程序以读取/写入 recipes
集合。
The Find operation
首先,我们需要实现函数,该函数负责返回一个食谱列表。更新 ListRecipesHandler
以便它使用 Find()
方法从 获取所有项目食谱
集合:
Find()
方法返回一个游标,它是一个文档流。我们必须遍历文档流并一次将一个解码到 Recipe
结构中。然后,我们必须将文档附加到食谱列表中。
运行应用程序,然后在 /recipes
端点上发出 GET 请求; find()
操作将在 recipes
集合上执行。结果,将返回食谱列表:
The InsertOne operation
要实现的第二个函数将是负责保存新配方。更新 NewRecipeHandler
函数,使其调用 recipes
InsertOne() 方法代码>集合:
在这里,我们使用 primitive.NewObjectID()
方法 设置了一个 唯一标识符>在将项目保存在集合中之前。因此,我们需要更改 Recipe
结构的 ID 类型。另外,请注意使用 bson
标签将 struct
字段映射到 document< MongoDB 集合中的 /code> 属性:
笔记
默认情况下,Go 在编码结构值时将结构字段名称小写。如果需要不同的名称,您可以使用 bson
标签覆盖默认机制。
通过 使用 Postman 客户端调用以下 POST 请求插入一个新的 recipe:
验证配方是否已 插入到 MongoDB 集合中,如以下屏幕截图所示:
为了得到最后一个插入的recipe,我们使用sort()
操作.
The UpdateOne operation
最后,为了更新集合中的一个项目,更新 UpdateRecipeHandler
函数以便它调用 UpdateOne()
方法。这个方法需要一个过滤器文档来匹配数据库中的文档和一个更新器文档来描述更新操作。您可以使用 bson.D{}
构建一个 过滤器 - 二进制编码 JSON< /strong> (BSON) 文件:
此方法按对象 ID 过滤 文档。我们通过将 ObjectIDFromHex
应用于路由参数 ID 来获得 Object ID。这将使用来自请求正文的新值更新匹配配方的字段。
通过对现有配方调用 PUT 请求来验证端点是否正常工作:
该请求将匹配 ID
为 600dcc85a65917cbd1f201b0
的配方,并将更新其 名称
从“自制披萨
”到“自制意大利辣香肠披萨
”,以及说明
字段以及制作“
Pepperoni Pizza
”的附加步骤。
至此,配方已成功更新。您可以使用 MongoDB Compass 确认这些更改:
您现在应该熟悉基本的 MongoDB 查询。继续执行剩余的 CRUD 操作。
最后,确保使用以下命令将更改推送到远程存储库:
然后,创建一个拉取请求以将 feature
分支合并到 develop
:
笔记
端点的完整实现可以在本书的 GitHub 存储库中找到(https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin/blob/main/chapter03/main.go)。
您刚刚看到 如何将 MongoDB 集成到 应用程序架构中。在下一节中,我们将介绍如何重构我们的应用程序的源代码,以便从长远来看它是可维护的、可扩展的和可扩展的。
Designing the project's layout
到目前为止,我们编写的所有 代码都在 main.go
文件中。虽然这很好用,但确保代码结构良好很重要。否则,随着项目的发展,您最终会得到很多隐藏的依赖项和混乱的代码(意大利面条式代码)。
我们将从数据模型开始。让我们创建一个 models
文件夹,以便我们可以存储所有模型结构。目前,我们有一个模型,即 Recipe
结构。在models
文件夹下创建一个recipe.go
文件,粘贴以下内容:
然后,使用 handler.go
文件创建一个 handlers
文件夹 。顾名思义,此文件夹通过公开为每个 HTTP 请求调用的正确函数来处理任何传入的 HTTP 请求:
此代码创建一个 RecipesHandler
结构,其中包含 MongoDB 集合和上下文实例 封装。在我们早期的简单实现中,我们倾向于将这些变量全局保存在主包中。在这里,我们将这些变量保存在结构中。接下来,我们必须定义一个 NewRecipesHandler
以便我们可以从 RecipesHandler
结构创建一个实例。
现在,我们可以定义 RecipesHandler
类型的端点处理程序。处理程序可以访问结构的所有变量,例如数据库连接,因为它是 RecipesHandler
类型的 方法:
从我们的 main.go
文件中,我们将提供所有数据库凭据并连接到 MongoDB 服务器:
然后,我们必须创建一个 全局变量来访问端点处理程序。更新 init()
方法,如下:
最后,使用 recipesHandler
变量 访问每个 HTTP 端点的处理程序:
运行应用程序。这次,运行当前目录中的所有 .go
文件:
该应用程序将按预期工作。服务器日志如下:
现在,您的项目结构应如下所示:
这是 Go 应用程序项目的基本布局。我们将在接下来的章节中介绍 Go 目录。
将更改推送到功能分支上的 GitHub,并将分支合并到 develop
:
在运行与数据库交互的服务时,其操作可能会成为瓶颈,从而降低用户体验并影响您的业务。这就是为什么响应时间是开发 RESTful API 时评估的最重要指标之一。
幸运的是,我们可以添加一个缓存层来将频繁访问的数据存储在内存中以加快速度,从而减少对数据库的操作/查询数量。
Caching an API with Redis
在本节中,我们将介绍如何向我们的 API 添加缓存机制。假设我们的 MongoDB 数据库中有大量的食谱。每次 尝试查询食谱列表时,我们都会遇到性能问题。我们可以做的是使用内存数据库(例如 Redis)来重用以前检索到的配方,并避免在每次请求时访问 MongoDB 数据库。
Redis 在检索数据方面始终更快,因为它始终位于 RAM 中——这就是为什么它是缓存的绝佳选择。另一方面,MongoDB 可能必须从磁盘中检索数据以进行查询。
根据官方文档(https://redis.io/ ),Redis 是一个开源的、分布式的、内存中的键值数据库、缓存和消息代理。下图说明了 Redis 如何适合我们的 API 架构:
假设我们想要获取食谱列表。首先,API 将在 Redis 中查看。如果存在配方列表,则会返回(这称为缓存命中)。如果缓存 为空(这称为 缓存未命中),则 MongoDB find ({})
查询将被发出,结果将被返回并保存在缓存中以供将来的请求使用。
Running Redis in Docker
设置 Redis 最简单的方法是通过 Docker。为此,我们将使用 DockerHub 提供的 Redis 官方镜像 ( https://hub.docker.com/_/redis)。在编写本书时,最新的稳定版本是 6.0。运行基于该图像的容器:
该命令主要做以下两件事:
- The
–d
flag runs the Redis container as a daemon. - The
–p
flag maps port 6379 of the container to port 6379 of the host. Port 6379 is the port where the Redis server is exposed.
该命令的输出如下:
始终检查 Docker 日志以查看事件链:
日志提供了大量有用的信息,例如默认配置和暴露的服务器端口:
Redis 容器 使用基本缓存策略。对于生产使用,建议配置驱逐策略。您可以使用 redis.conf
文件配置策略:
此配置为 Redis 分配 512 MB 内存并将驱逐策略设置为 最近最少使用 (LRU) 算法,删除最近最少使用的缓存项。因此,我们只保留最有可能再次阅读的项目。
然后,您可以使用以下命令在容器运行时传递配置:
这里,$PWD/conf
是包含 redis.conf
文件的文件夹。
现在 Redis 正在运行,我们可以使用它来缓存 API 数据。但首先,让我们安装官方的 Redis Go 驱动程序(https://github.com/go-redis/redis< /a>) 通过执行以下命令:
在 main.go
文件中导入以下包:
现在,在 init()
方法中,使用 redis.NewClient()
初始化 Redis 客户端。该方法以服务器地址、密码和数据库作为参数。接下来,我们将在 Redis 客户端调用 Ping()
方法来检查与 Redis 服务器的连接状态:
如果连接成功,将显示 ping: PONG
消息,如上图所示。
Optimizing MongoDB queries
通过与 Redis 服务器建立 连接,我们可以更新 RecipesHandler
结构以存储 Redis 客户端的实例,以便处理程序可以与 Redis 交互:
确保将 Redis 客户端实例传递给 init() 中的
方法:RecipesHandler
实例
接下来,我们必须更新 ListRecipesHandler
以检查食谱是否已缓存在 Redis 中。如果是,我们返回一个列表。如果没有,我们将从 MongoDB 中检索数据并将其缓存在 Redis 中。我们必须对代码进行的新更改如下:
值得一提的是,Redis 的值必须是字符串,所以我们必须使用 json.Marshal( )
方法。
要测试新的 更改,请运行应用程序。然后,使用 Postman 客户端或使用 cURL
命令在 /recipes
端点上发出 GET 请求。返回您的终端并查看杜松子酒日志。您应该在控制台中看到与从 MongoDB 获取数据相对应的第一个请求的消息:
笔记
有关如何使用 Postman 客户端或 cURL
命令的分步指南,请查看 第 1 章,Gin 入门。
如果你点击了第二个 HTTP 请求,这一次,数据将从 Redis 返回,因为它被缓存在第一个请求中:
正如我们所见,与从磁盘 (MongoDB) 中检索数据相比,从内存 (Redis) 中检索 数据的速度非常快。
我们可以通过从容器运行 Redis CLI 来验证数据是否缓存在 Redis 中。运行以下命令:
这些命令将使用交互式终端连接到 Redis 容器并启动 bash shell。您会注意到您现在正在使用终端,就好像您在容器中一样,如以下屏幕截图所示:
现在我们已附加到 Redis 容器,我们可以使用 Redis 命令行:
从那里,我们可以使用 EXISTS
命令检查 recipes
键是否存在:
此命令将返回 1
(如果键存在)或 0
(如果键不存在)。在我们的例子中,食谱列表已经缓存在 Redis 中:
您可以使用 shell 客户端完成很多工作,但您已经掌握了大体思路。输入 exit
离开 MongoDB shell,然后再次输入 exit
离开交互式 shell。
对于 GUI 爱好者,您可以使用 Redis Insights (https://redislabs.com/fr/ redis-企业/redis-insight/)。它提供了一个直观的界面来探索 Redis 并与其数据进行交互。与 Redis 服务器类似,您可以使用 Docker 部署 Redis Insights:
该命令将运行一个基于 Redis Insight 官方镜像的容器,并将接口暴露在 8001 端口。
使用浏览器导航到 http://localhost:8081
。 Redis Insights 主页应该会出现。点击我已经有一个数据库,然后点击连接到Redis数据库按钮:
将 Host 设置为 redis
,port 设置为 6379
,并将数据库命名为 。设置如下:
接下来,单击添加 REDIS DATABASE。 本地数据库将被保存;点击它:
您将被重定向到 Summary 页面,其中包含有关 Redis 服务器的真实指标和统计信息:
如果您单击BROWSE,您将看到已存储在 Redis 中的所有键的列表。如以下屏幕截图所示,recipes 键已被缓存:
现在,您可以使用 界面在 Redis 中探索、操作和可视化数据。
到目前为止,我们构建的 API 是一种魅力,对吧?并不真地;假设您向数据库添加了一个新配方:
现在,如果您 发出 GET /recipes
请求,将找不到新配方。这是因为数据是从缓存中返回的:
缓存引入的问题之一是在数据更改时使缓存保持最新:
这个案例中有两个组规则来修复不一致。首先,我们可以为Redis 中的 recipes 键。其次,我们可以在每次插入或更新新配方时清除 Redis 中的 recipes 键。
笔记
保留缓存 TTL 的时间取决于您的应用程序逻辑。您可能需要将其保存一小时或几天,具体取决于数据更新的频率。
我们可以通过更新 NewRecipeHandler
函数来实现第二种解决方案,以便在插入新配方时删除 recipes
键。在这种情况下,实现将如下所示:
重新部署 应用程序。现在,如果您点击 GET /recipes
请求,数据将按预期从 MongoDB 返回;然后,它将被缓存在 Redis 中。第二个 GET 请求将从 Redis 返回数据。但是,现在,如果我们发出 POST /recipes
请求来插入新的配方,Redis 中的 recipes
键将被清除,正如 Remove data from Redis
消息所确认的。这意味着下一个 GET /recipes
请求将从 MongoDB 获取数据:
现在,新配方将在配方列表中返回:
笔记
当 /recipes/{id}
端点上发生 PUT 请求时,更新 UpdateRecipeHandler
以清除缓存。
虽然缓存为具有大量读取的应用程序提供了很大的好处,但对于执行大量数据库更新并可能减慢写入速度的应用程序而言,它可能没有那么有益。
Performance benchmark
我们可以进一步了解这个 ,看看 API 在大量请求下的表现如何。我们可以使用 Apache Benchmark (https://httpd.apache.org/ docs/2.4/programs/ab.html)。
首先,让我们在没有缓存层的情况下测试 API。您可以使用以下命令在 /recipes
端点上总共运行 2,000 个 GET 请求,其中包含 100 个并发请求:
从这个输出中得到的重要信息如下:
- Time taken for tests: This means the total time to complete the 2,000 requests.
- Time per request: This means how many milliseconds it takes to complete one request.
接下来,我们将发出相同的请求,但这次是在带有缓存的 API(Redis)上:
完成 2,000 个请求需要几秒钟的时间:
为了比较这两个结果,我们可以使用 gnuplot
实用程序根据 绘制图表without-cache.data
和 with-cache.data
文件。但首先,创建一个 apache-benchmark.p
文件以将数据呈现为图形:
这些命令将根据 .data
文件在同一图表上绘制两个图,并将输出保存为 PNG 图像。接下来,运行 gnuplot
命令来创建图像:
将创建一个 benchmark.png
图像,如下所示:
与没有缓存的 API 响应时间相比,启用缓存机制的 API 响应时间非常快。
确保使用功能分支将更改推送到 GitHub。然后,创建一个合并到 develop
的拉取请求:
在本章结束时,您的 GitHub 存储库应如下所示:
伟大的!现在,您应该能够将 MongoDB 数据库集成到您的 API 架构中以管理数据持久性。
Summary
在本章中,我们学习了如何构建一个 RESTful API,它利用 Gin 框架和 Go 驱动程序在 MongoDB 等 NoSQL 数据库中创建和查询。
我们还探讨了如何通过缓存使用 Redis 访问的数据来加速 API。如果您的数据大部分是静态的并且不会不断变化,那么它绝对是您应用程序的一个很好的补充。最后,我们介绍了如何使用 Apache Benchmark 运行性能基准测试。
到目前为止,我们构建的 RESTful API 就像一个魅力,并且对公众开放(如果部署在远程服务器上)。如果您让 API 未经身份验证,那么任何人都可以访问任何端点,这可能是非常不可取的,因为您的数据可能会被用户损坏。更糟糕的是,您可能会将数据库中的敏感信息暴露给整个互联网。这就是为什么在下一章中,我们将介绍如何使用身份验证来保护 API,例如 JWT。
Further reading
- MongoDB Fundamentals, by Amit Phaltankar, Juned Ahsan, Michael Harrison, and Liviu Nedov, Packt Publishing
- Learn MongoDB 4.x, by Doug Bierer, Packt Publishing
- Hands-On RESTful Web Services with Go – Second Edition, by Naren Yellavula, Packt Publishing