如今,大多数大型云部署都依赖于外部服务:数据库,缓存和第三方API。 在虚拟机是临时组件的环境中,获取这些关键资源可能是一个挑战。 在本教程中,您将学习一个嵌入式解决方案,该解决方案将基础结构知识公开给应用程序运行时。 此外,您还将看到如何设计一个通用框架来监视复杂的服务器拓扑,以更好地协调微服务。
“我的服务发现非侵入式解决方案为DevOps提供了必不可少的工具。 它是立即可操作的,不会将我们锁定在框架之后。 ”
背景
现代应用程序部署通常涉及大规模的微服务。 例如,您可以将一个应用程序拆分为多个相互协作的单一用途单元,而不是创建单个整体应用程序。 这样,您可以免费进行关注点分离和水平缩放的模块化开发。
真的免费吗? 不完全的。 我们仍然需要协调所有基础结构的移动部分。 容器引擎Docker的广泛采用加剧了这一挑战。 尽管Docker解锁了用于开发,发布和运行程序的工作流,但许多开发人员在考虑多主机部署或日志管理等老派问题时遇到了麻烦。
挑战
当今,典型的Web应用程序涉及复杂程度各异的前端,后端,数据库以及很多第三方服务。 所有这些技术都通过网络进行通信,我们可以利用这一事实:将后端部署在可用资源的地方,并且出于性能考虑,数据库碎片会旋转节点。 同时,整个设置会在整个群集中动态发展以处理负载。
但是后端如何在这种变化的云拓扑中找到数据库URL? 您需要设计一个过程,使应用程序对基础结构具有最新知识。
领事介绍
Consul在GitHub上被描述为“一种用于服务发现,监视和配置的工具”。 Consul是由Vagrant的创建者HashiCorp开发的开源项目之一。 它提供了一个分布式,高度可用的系统来注册服务,存储共享配置并保持多个数据中心的准确视图。 此外,它以简单的Go二进制文件形式分发,因此部署起来很简单。
为了使步骤易于遵循(并与我们的主题保持一致),我们将使用Docker。
一旦安装了Docker,并且借助progrium(也称为Jeff Lindsay ),清单1中的单一代码足以引导Consul服务器。
清单1.引导Consul服务器
docker run --detach --name consul --hostname consul-server-1 progrium/consul
-server -bootstrap -ui-dir /ui
# Get container ip for further interactions
CONSUL_IP=$(docker inspect -f '{{ .NetworkSettings.IPAddress }}' consul)
# The container also runs a web UI at $CONSUL_IP:8500/ui
注意:虽然官方文档建议您至少旋转三台服务器来处理故障案例,但是这些注意事项不在本教程的讨论范围之内。
现在,您可以查询基础结构并发现一项服务:Consul本身(请参见清单2)。
清单2.发现Consul服务
curl $CONSUL_IP:8500/v1/catalog/services
{"consul": []}
# we can fetch more details about a specific service
curl $CONSUL_IP:8500/v1/catalog/service/consul
[{"Node":"consul-server-1","Address":"172.17.0.1","ServiceID":"consul",
"ServiceName":"consul","ServiceTags":[],"ServiceAddress":"",
"ServicePort":8300}]
如您所见,Consul存储有关服务的重要事实。 它涵盖了信息和标签,即以编程方式访问远程服务的基本数据。
声明式服务
现在让我们看一下注册,外部服务和Docker在我们的解决方案中的作用。 为了说明这一点,让我们想象一下一个现代应用程序,它将数据存储在MongoDB中并通过Mailgun发送电子邮件。 后者是一项外部服务,而我们将自己运行前者。 继续阅读以了解我们如何处理这两种情况。
注册
为了公开这些有价值的属性,您首先需要注册该服务。 您将在集群的每个节点上运行Consul代理,该代理负责加入Consul服务器,公开节点的服务并执行运行状况检查(请参见清单3)。
清单3.注册服务
# download and install the latest version
wget https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip -O
/tmp/consul.zip
cd /usr/local/bin && unzip /tmp/consul.zip
# create state and configuration directories
mkdir -p {/srv/consul,/etc/consul.d}
# check that everything worked
consul --help
MongoDB的下载量超过1000万,是文档数据库的流行选择。 让我们使用它,并将以下文件保存在/etc/consul.d/mongo.json中(参见清单4)。
清单4.使用MongoDB作为数据库
{
"service": {
"name": "mongo",
"tags": [
"database",
"nosql"
],
"port": 27017,
"check": {
"name": "status",
"script": "mongo --eval 'printjson(rs.status())'",
"interval": "30s"
}
}
}
该语法提供了一种定义服务属性和运行状况检查的简洁,易读和声明性的方式。 您可以在版本控制系统中拾取这些文件,并立即识别应用程序的组件。 上面的文件在端口27017上声明了一个名为“ mongo”的服务。检查部分为Consul代理提供了一个脚本,用于测试节点是否正常。 实际上,在请求服务器提供服务要求时,您需要确保它返回可靠的端点。
剩下的一切只是启动实际的Mongo服务器和本地Consul代理(请参见清单5)。
清单5.启动Mongo服务器和本地Consul代理
# launch mongodb server on default port 27017
mongod
# launch local agent
consul agent \
-join $CONSUL_HOST \ # explicitly provide how to reach the server
-data-dir /data/consul \ # internal state storage
-config-dir /etc/consul.d # configuration directory where services and checks
are expected to be defined
奏效了吗? 让我们查询Consul HTTP API(请参见清单6)。
清单6.查询Consul HTTP API
# fetch infrastructure overview
curl $CONSUL_IP:8500/v1/catalog/nodes
[{"Node":"consul-server-1","Address":"172.17.0.1"},{"Node":"mongo-1","Address"
:"172.17.0.2"}]
# consul correctly registered mongo service
curl $CONSUL_IP:8500/v1/catalog/service/mongo
[{
"Node": "mongo-1",
"Address": "172.17.0.2",
"ServiceID": "mongo",
"ServiceName": "mongo",
"ServiceTags": ["database", "no-sql"],
"ServiceAddress": "",
"ServicePort": 27017
}]
# it also exposes health state
curl $CONSUL_IP:8500/v1/health/service/mongo
[{
"Node": {
"Node":"mongo-1",
},
"Service": {
"ID": "mongo",
"Service": "mongo",
"Tags": ["database","no-sql"],
"Address": "",
},
"Checks":[{
"Node": "mongo-1",
"CheckID": "service:mongo",
"Name": "Service 'mongo' check",
"Status": "passing",
"Notes": "",
"Output": "MongoDB shell version: 3.0.3\nconnecting to: test\n{ \"ok\" : 0,
\"errmsg\" : \"not running with --replSet\", \"code\" : 76 }\n",
"ServiceID": "mongo",
"ServiceName": "mongo"
},{
"Node": "mongo-1",
"CheckID": "serfHealth",
"Status": "passing",
"Notes": "",
"Output": "Agent alive and reachable",
"ServiceID": "",
"ServiceName": ""
}]
}]
给定Consul代理或服务器地址,集群中能够进行HTTP请求的任何代码段现在都可以使用该信息。 简短地讲,我将解释如何处理这一切,但是在此之前,让我们看看如何注册不受控制的服务,以及作为奖励,如何使用Docker自动执行上述步骤。
外部服务
为了避免浪费时间,最好将第三方服务集成到您的应用程序中。 但是在这种情况下,您将无法在适当的节点上启动Consul代理。 领事再次为您介绍了(请参见清单7)。
清单7.查询Consul HTTP API
# manually register mailgun service through the HTTP API
curl -X PUT -d \
'{"Datacenter": "dc1", "Node": "mailgun", "Address": "http://www.mailgun.com",
"Service": {"Service": "email", "Port": 80}, "Check": {"Name": "mailgun api",
"http": "www.status.mailgun.com", "interval": "360s", "timeout": "1s"}}' \
http://$CONSUL_IP:8500/v1/catalog/register
# looks like we're all good !
curl $CONSUL_IP:8500/v1/catalog/services
{"consul":[],"email":[],"mongo":["database","nosql"]}
由于Mailgun是Web服务,因此您可以使用HTTP字段来检查API可用性。 要深入了解领事超级大国,请参阅其综合文档。
Docker整合
到目前为止,Go二进制文件,单个JSON文件和一些HTTP请求已启用服务发现工作流程。 您不受特定技术的束缚,但是如前所述,这种敏捷设置特别适合微服务。
在这种情况下,Docker可让您将服务打包到可重现的自注册容器中。 给定您现有的mongo.json,只需清单8中的Dockerfile和Procfile。
清单8.将服务打包到可复制的自注册容器中
# Dockerfile
# start from official mongo image
FROM mongo:3.0
RUN apt-get update && apt-get install -y unzip
# install consul agent
ADD https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip /tmp/consul.zip
RUN cd /bin && \
unzip /tmp/consul.zip&& \
chmod +x /bin/consul && \
mkdir -p {/data/consul,/etc/consul.d} && \
rm /tmp/consul.zip
# copy service and check definition, as we wrote them earlier
ADD mongo.json /etc/consul.d/mongo.json
# Install goreman - foreman clone written in Go language
ADD https://github.com/mattn/goreman/releases/download/v0.0.6
/goreman_linux_amd64.tar.gz /tmp/goreman.tar.gz
RUN tar -xvzf /tmp/goreman.tar.gz -C /usr/local/bin --strip-components 1 && \
rm -r /tmp/goreman*
# copy startup script
ADD Procfile /root/Procfile
# launch both mongo server and consul agent
ENTRYPOINT ["goreman"]
CMD ["-f", "/root/Procfile", "start"]
Dockerfiles让我们定义了一个在启动容器时运行的命令。 但是,我们现在需要同时运行MongoDB和Consul。 Goreman
让我们实现了这一目标。 它读取一个名为Procfile
的配置文件,该文件定义了多个要管理的进程(生命周期,环境,日志等)。 容器世界中的这种方法本身就是一场辩论,还存在其他解决方案,但目前它以简单的方式完成了工作。
清单9. Procfile
# Procfile
database: mongod
consul: consul agent -join $CONSUL_HOST -data-dir /data/consul -config-dir
/etc/consul.d
清单10.构建容器的Shell命令
ls
Dockerfile mongo.json Procfile
docker build -t article/mongo .
# ...
docker run --detach --name docker-mongo \
--hostname docker-mongo-2 \ # if not explicitly configured, consul agent
set its name to the node hostname
--env CONSUL_HOST=$CONSUL_IP article/mongo
curl $CONSUL_IP:8500/v1/catalog/nodes
[
{
"Node": "consul-server-1",
"Address": "172.17.0.1"
}, {
"Node": "docker-mongo-2",
"Address": "172.17.0.3"
}, {
"Node": "mailgun",
"Address": "http://www.mailgun.com"
}, {
"Node": "mongo-1",
"Address": "172.17.0.2"
}
]
太棒了! 将Docker和服务发现结合在一起可以使您看起来不错!
我们可以通过查询$CONSUL_IP:8500/v1/catalog/service/mongo
来获取更多详细信息,如清单6所示,并找到服务端口。 领事将容器IP公开为服务地址。 只要容器公开端口,该方法就可以使用,即使Docker将其映射到主机上的随机值。 但是,在多主机拓扑上,您需要将容器的端口显式映射到主机上的端口。 为了避免此限制,请考虑Weave 。
总结起来,这是如何在多个数据中心中公开服务信息:
- 启动至少一台Consul服务器并存储其地址。
- 在每个节点上:
- 下载Consul二进制文件。
- 在Consul配置目录中编写服务并检查定义。
- 启动应用程序。
- 使用另一个代理或服务器的地址启动Consul代理。
创建基础架构感知的应用程序
现在,您已经构建了一个方便且非侵入式的工作流来部署和注册新服务。 下一步的逻辑步骤是将此知识导出到相关的应用程序。
十二要素应用程序,一种用于构建软件即服务应用程序的方法,对于在环境中存储配置非常重要:
- 保持配置与更改代码的严格隔离。
- 避免将敏感信息检入存储库。
- 保持语言和操作系统不可知。
现在是时候编写包装程序了,该包装程序可以查询Consul端点中的可用服务,将其连接属性导出到环境中,然后执行给定的命令。 选择Go语言会为您提供潜在的跨平台二进制文件(到目前为止,就像其他工具一样),并且可以访问官方客户端API(请参见清单11)。
清单11.将服务打包到可复制的自注册容器中
package main
import (
"strconv"
"strings"
"flag"
"log"
"os"
"os/exec"
"fmt"
"github.com/hashicorp/consul/api"
)
// critical quits on errors with a debug message
func critical(err error) {
if err != nil {
log.Printf("error: %v", err)
os.Exit(1)
}
}
// inject exports properties into runtime environment
func inject(properties map[string]string) []string {
// read current process environment
processEnv := os.Environ()
// allocate and copy it
env := make([]string, len(processEnv), len(properties) + len(processEnv))
copy(env, processEnv)
for k, v := range properties {
// format key/value mapping as exec.Command and system style (i.e. KEY=VALUE)
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
return env
}
// discoverServices queries Consul for services data
func discoverServices(addr string, healthyOnly bool) map[string]string {
servicesEnv := make(map[string]string)
// initialize consul api client
consulConf := api.DefaultConfig()
consulConf.Address = addr
client, err := api.NewClient(consulConf)
critical(err)
// retrieve full list of services throughout our infrastructure
services, _, err := client.Catalog().Services(&api.QueryOptions{})
critical(err)
for name, _ := range services {
// query healthy services information
servicesData, _, err := client.Health().Service(name, "", healthyOnly,
&api.QueryOptions{})
critical(err)
// loop over this category of service
for _, entry := range servicesData {
// store connection information like environment variables : {"MONGO_HOST":
"172.17.0.5"}
id := strings.ToUpper(entry.Service.ID)
servicesEnv[id + "_HOST"] = entry.Node.Address
servicesEnv[id + "_PORT"] = strconv.Itoa(entry.Service.Port)
}
}
return servicesEnv
}
func main() {
flag.Parse()
// keep it consistent and read consul service address from environment
consulAddress = os.Getenv("CONSUL")
command = flag.Args()
log.Printf("inspecting infrastructure")
services := discoverServices(consulAddress, true)
env := inject(services)
log.Printf("running `%s`", strings.Join(command, " "))
cmd := exec.Command(command[0], command[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
critical(cmd.Start())
critical(cmd.Wait())
}
清单12中的下一个命令编译该原型并验证其行为。
清单12.编译和验证原型
# install the single dependency
go get github.com/hashicorp/consul
# compile to `wrapper` (depends on your directory name)
go build ./...
export CONSUL=$CONSUL_IP:8500
./wrapper env
最后一条命令应该输出MONGO_PORT=27017
变量。 现在,任何命令都应该能够从其环境读取服务数据。
动态重新配置基础架构
您仍然可能要面对的情况正在挑战您当前的实现。 一个Web应用程序可以像上面的应用程序一样启动,并成功连接到MongoDB,并且在数据库故障或迁移时仍然可能发生不良情况。 您想要的是在基础结构发生正常或意外更改时动态更新应用程序的知识。
设计一个健壮的解决方案可能需要单独的教程,而Consul模板采用了一种有趣的方法。
Consul模板查询Consul实例并更新文件系统上任意数量的指定模板。 另外,领事模板可以在模板更新完成后执行任意命令。 因此,您可以使用Consul Template监视服务(地址和运行状况),并在检测到更改时自动重新启动应用程序。 因为您的包装器将获取服务数据,所以运行时环境将镜像基础结构的正确状态(请参见清单13)。
清单13.使用Consul模板监视服务并重新启动应用程序
consul-template \
-consul $CONSUL \
-wait 1s \ # Avoid re-running multiple times on changes
-template "app.ctmpl:/tmp/app.conf:./wrapper env"
现在,您可以享受模板化配置文件的所有好处。 清单14是改编自GitHub上的hackathon-starter的示例。
清单14.模板化配置文件的示例
// app.ctmpl
// store third-party service information in the environment
db: 'mongodb://' + process.env.MONGO_HOST + ':' + process.env.MONGO_PORT + '/test',
// or you can leverage consul-template built-in service discovery
{{ range service "mongo" }}
db2: 'mongodb://{{ .Address }}:{{ .Port }}/test',
{{ end}}
// Use consul-template to fetch information from consul kv store
// curl -X PUT "http://$CONSUL/v1/kv/hackathon/mailgun_user" -d "xavier"
mailgun: {
user: '{{ key "hackathon/mailgun_user" }}',
password: '{{ key "hackathon/mailgun_password" }}'
}
这种经验需要更多的思考。 例如,重新启动应用程序以更新其服务知识可能很棘手。 相反,我们可以向它发送特定信号,使其有机会优雅地处理更改。 但是,这需要我们进入应用程序的代码库,并且到目前为止,它不需要了解任何内容。 此外,易失的云提供商上微服务的兴起应鼓励我们运行无状态,具有故障恢复能力的应用程序。
但是,将功能强大的工具与明确的合同相结合,可以使您将分布式应用程序集成到复杂的基础架构中,而不必局限于特定的提供程序或应用程序堆栈。
结论
服务发现以及更广泛的服务编排是现代发展中最令人兴奋的挑战之一。 大型企业以及开发人员社区正在介入并进一步推动技术和思想的发展。
例如,IBM Cloud通过工作负载调度程序,智能数据库,监视,成本管理,数据同步,REST API等解决了这一挑战。 只有少数工具可使开发人员仅专注于其应用程序的松耦合模块。
感谢Consul and Go,您可以朝这个方向迈出一步,并建立一套具有以下特点的服务:
- 自我注册
- 自我更新
- 堆栈不可知论
- 嵌入式部署
- 容器友好
本教程介绍了生产部署的基础知识,并展示了一种即插即用的服务发现方法如何使您自由地思考现代部署管道的其他部分,而没有所有通常的约束。 进一步的步骤可能包括通过加密扩展我们的包装器,并提供一致的集成以安全地公开凭据,例如服务令牌。 我希望本教程为您提供了应对超敏捷云部署挑战的想法。