Author: 方云麟 Date: 20170603

背景

zookeeper / etcd/ consul 是比较出名的三个内存数据库,多用于服务发现和键值存储。

consul 有一个 multi-datacenter 的概念。根据官方文档,这个概念用于做应用层抽象,而并非做数据冗余只用。

consul V.S. etcd

  consul etcd
数据库结构 tree tree
支持服务发现 原生 需要自己实现逻辑
配置下发插件 consul-template | confd confd
官方文档 不完整 完整
社区文档 嘴炮居多 一般
部署难度 简单 没 consul 那么简单
冗余方案 consul-replicate 还没了解
RESTful API 弱,很多 etcd 支持的功能并不支持。尤其是目录方面。不支持列出不包含文件的中间目录。 丰富

单数据中心部署

server

配置文件

[root@node1 ~]# cat /etc/consul.d/server.json
{
    "bind_addr": "0.0.0.0",
    "datacenter": "dc1",
    "data_dir": "/var/consul",
    "log_level": "INFO",
    "enable_syslog": false,
    "enable_debug": false,
    "node_name": "node1",
    "server": true,
    "bootstrap_expect": 3,
    "leave_on_terminate": false,
    "skip_leave_on_interrupt": true,
    "rejoin_after_leave": true,
    "retry_join": ["192.168.37.145", "192.168.37.147", "192.168.37.148"],
    "encrypt": "jI6h2Fp0KF2UHZ6Lxcalfw=="
}

正式环境 consul server 必须在 3 台或者 3 台以上。单数最佳。

encrypt 参数用于加密数据通讯的秘钥。可以由命令 consul keygen 生成。

启动命令

/usr/local/bin/consul agent -config-dir /etc/consul.d/server.json

client

配置文件

[root@node4 ~]# cat /etc/consul.d/client.json
{
    "bind_addr": "192.168.37.149",
    "datacenter": "dc1",
    "data_dir": "/var/consul_client",
    "encrypt": "jI6h2Fp0KF2UHZ6Lxcalfw==",
    "log_level": "INFO",
    "enable_syslog": false,
    "enable_debug": false,
    "node_name": "node4",
    "server": false,
    "leave_on_terminate": false,
    "rejoin_after_leave": true,
    "retry_join": ["192.168.37.145", "192.168.37.147", "192.168.37.148"]
}

启动命令

/usr/local/bin/consul agent -config-dir /etc/consul.d/

可在服务端或者客户端用命令

[root@node5 ~]# consul members
Node   Address              Status  Type    Build  Protocol  DC
node1  192.168.37.145:8301  alive   server  0.8.3  2         dc1
node2  192.168.37.147:8301  alive   server  0.8.3  2         dc1
node3  192.168.37.148:8301  alive   server  0.8.3  2         dc1
node4  192.168.37.149:8301  alive   client  0.8.3  2         dc1
node5  192.168.37.150:8301  alive   client  0.8.3  2         dc1
[root@node5 ~]#

查看节点存活情况。

consul 的使用姿势。

consul 常规有两种使用姿势:

  1. server - agent [ - SDK / API ]
  2. server - SDK / API

server - SDK / API

部署一个 consul server 集群,把集群 IP 和端口暴露到公共网络,供 Client 直接用 SDK / API 访问。

这种方式的注意点有:

  1. 访问端口暴露在公网,必须做好安全措施。
  2. 必须借用 DNS 或者程序自由逻辑来判断集群节点的存活状态。
  3. 部署少了部署 consul client 环节,相对简单。

server - agent [ - SDK / API ]

部署 consul server 集群后,在每台要使用 consul 的机器部署 consul client 服务。应用通过 SDK / API 访问本地 127.0.0.1 的 consul 做数据的处理,consul client 进程负责和 consul 实际通讯。

这种方式的注意点有:

  1. 减少了 consul server 暴露到公网的端口。相对更安全。
  2. consul client 承接了 server 存活和切换判断。

上述两种方式在应用层面使用并没有使用上的区别。

主要功能

  1. 键值存储
  2. 配置管理
  3. 服务发现

键值存储

K/V 操作请直接看文档:https://www.consul.io/docs/commands/kv.html

consul 不支持存储复杂数据,如果要存储数组或者结构,需要存成 json 文本格式。或者其他自定义格式。

配置管理

consul 配置管理常用的有三种方式

  1. 程序直接对接 consul,从 consul server 中获取并应用数据。
  2. 通过 consul-template 实现配置的获取、生成、重载
  3. 通过 confd 实现配置的获取、生成、重载。confd 和 etcd 配合的比较多,在此不表。

consul-template

GitHub 地址:https://github.com/hashicorp/consul-template

template 语法参考:

https://golang.org/pkg/text/template/

consul template 有一个问题:故意不支持查看中间路径。举个例子

/nginx/upstream/backend/node1

你无法通过 API 查看 /nginx/upstream/ 进而查看到 backend/ 这个目录。(代码写死,见:https://github.com/hashicorp/consul-template/blob/master/template/funcs.go)

在这个场景下你只能通过查看 /nginx/upstream/backend/ 目录查看到该目录下有 node 文件。

但是 consul client 也就是直接在 client 调用 consul 是支持查看中间目录的。

[root@node5 ~]# consul kv get -keys /nginx/upstream/
nginx/upstream/backend1/
nginx/upstream/backend2/
[root@node5 ~]#

consul-template 自带函数

API Functions
Scratch Functions
Helpers Functions
Math Functions

confd

官方地址:https://github.com/kelseyhightower/confd

confd 支持如下数据库后台

配置

# mkdir -p /etc/confd/{conf.d,templates}

e.g.

nginx.conf

[root@node5 ~]# cat /etc/confd/conf.d/nginx.toml
[template]
src = "upstream.tmpl"
dest = "/tmp/upstream.conf"
keys = [
	"/nginx/"
]

此外还有两个比较有用的参数:

e.g.

[template]
prefix = "/yourapp"
src = "nginx.tmpl"
dest = "/tmp/yourapp.conf"
owner = "nginx"
mode = "0644"
keys = [
  "/subdomain",
  "/upstream",
]
check_cmd = "/usr/sbin/nginx -t -c "
reload_cmd = "/usr/sbin/service nginx reload"

template 隐藏小知识

相关连接:https://github.com/hashicorp/consul-template/issues/590

默认的 consul 版本和常规的 template 语法下,template 代码和最终代码的排版友好程度是互斥的。

排版好看,最终代码不好看

[root@node5 ~]# cat /etc/confd/templates/upstream.tmpl



[root@node5 ~]# confd -onetime -backend consul -node 127.0.0.1:8500
2017-06-01T05:31:09+08:00 node5 confd[1667]: INFO Backend set to consul
2017-06-01T05:31:09+08:00 node5 confd[1667]: INFO Starting confd
2017-06-01T05:31:09+08:00 node5 confd[1667]: INFO Backend nodes set to 127.0.0.1:8500
2017-06-01T05:31:09+08:00 node5 confd[1667]: INFO /tmp/upstream.conf has md5sum 0da83e20244724113639934feb782054 should be 0a819b0ee0e1a1310ec1a4461d045bec
2017-06-01T05:31:09+08:00 node5 confd[1667]: INFO Target config /tmp/upstream.conf out of sync
2017-06-01T05:31:09+08:00 node5 confd[1667]: INFO Target config /tmp/upstream.conf has been updated
[root@node5 ~]# cat /tmp/upstream.conf

backend1

backend2

[root@node5 ~]#

因为 template 不能区分哪些换行是基于模板语言美观的换行,哪些是最终生成代码需要的换行。

排版不好看,最终代码好看

[root@node5 ~]# cat /etc/confd/templates/upstream.tmpl


[root@node5 ~]# confd -onetime -backend consul -node 127.0.0.1:8500
2017-06-01T05:33:18+08:00 node5 confd[1674]: INFO Backend set to consul
2017-06-01T05:33:18+08:00 node5 confd[1674]: INFO Starting confd
2017-06-01T05:33:18+08:00 node5 confd[1674]: INFO Backend nodes set to 127.0.0.1:8500
2017-06-01T05:33:18+08:00 node5 confd[1674]: INFO /tmp/upstream.conf has md5sum 0a819b0ee0e1a1310ec1a4461d045bec should be 518eaca1c861df11e3c1f6a0e10a5d51
2017-06-01T05:33:18+08:00 node5 confd[1674]: INFO Target config /tmp/upstream.conf out of sync
2017-06-01T05:33:18+08:00 node5 confd[1674]: INFO Target config /tmp/upstream.conf has been updated
[root@node5 ~]# cat /tmp/upstream.conf
backend1
backend2

[root@node5 ~]#

在 go 1.6.3 版本解决了这个问题。

[root@node5 ~]# cat /etc/confd/templates/upstream.tmpl



[root@node5 ~]# confd -onetime -backend consul -node 127.0.0.1:8500
2017-06-01T05:35:19+08:00 node5 confd[1693]: INFO Backend set to consul
2017-06-01T05:35:19+08:00 node5 confd[1693]: INFO Starting confd
2017-06-01T05:35:19+08:00 node5 confd[1693]: INFO Backend nodes set to 127.0.0.1:8500
2017-06-01T05:35:19+08:00 node5 confd[1693]: INFO /tmp/upstream.conf has md5sum 22e24197acc9b34f0a24eced6113047b should be 0da83e20244724113639934feb782054
2017-06-01T05:35:19+08:00 node5 confd[1693]: INFO Target config /tmp/upstream.conf out of sync
2017-06-01T05:35:19+08:00 node5 confd[1693]: INFO Target config /tmp/upstream.conf has been updated
[root@node5 ~]# cat /tmp/upstream.conf
backend1
backend2
[root@node5 ~]#

可以在花括号里加 - 号表示这里是语法美观换行,最终代码中这里就不会给解析换行了。 - 号加在左 表示忽略右边换行。

可惜 confd 目前是用 go 1.4 编译的,所以你需要拉取源码自己编译一次 confd。

自带命令

参考地址:https://github.com/kelseyhightower/confd/blob/master/docs/templates.md

服务发现

consul 服务发现,分为两大模块

  1. 服务注册
  2. 服务查询

在服务注册的时候可以注册心跳监测,以在查询的时候判断服务的健康状态。

服务注册文档:https://www.consul.io/docs/agent/services.html 服务监测文档:https://www.consul.io/docs/agent/checks.html

这里对服务发现有稍微详细的解释:https://www.consul.io/intro/getting-started/services.html

服务发现可以通过三种方式使用

  1. 命令
  2. RESTful API (HTTP API)
  3. 各种语言 SDK

服务监测支持 4 + 1 种类型

  1. 脚本监测
  2. HTTP 监测
  3. TCP 监测
  4. TTL 监测
  5. docker 监测

TTL 监测示例(python 版本)

注册服务

[root@node5 test]# cat register.py
#!/usr/bin/env python

from consul import Consul, Check

c = Consul()
c.agent.service.register('testservice', check=Check.ttl('10s'))
print c.agent.services()
print c.agent.checks()

发送心跳

[root@node5 test]# cat heartbeat.py
#!/usr/bin/env python

from consul import Consul

c = Consul()
c.agent.check.ttl_pass('service:testservice')

查询服务

[root@node5 test]# cat query.py
#!/usr/bin/env python

from consul import Consul

c = Consul()
print c.health.service('testservice', passing=True)

注销服务

[root@node5 test]# cat deregister.py
#!/usr/bin/env python

from consul import Consul

c = Consul()
c.agent.service.deregister('testservice')
print c.agent.services()
print c.agent.checks()

过程模拟

[root@node5 test]# ./query.py
('7645', [])
[root@node5 test]# ./register.py
{u'testservice': {u'Service': u'testservice', u'Tags': [], u'ModifyIndex': 0, u'EnableTagOverride': False, u'ID': u'testservice', u'Address': u'', u'CreateIndex': 0, u'Port': 0}}
{u'service:testservice': {u'Node': u'node5', u'CheckID': u'service:testservice', u'Name': u"Service 'testservice' check", u'ServiceName': u'testservice', u'Notes': u'', u'ModifyIndex': 0, u'Status': u'critical', u'ServiceID': u'testservice', u'ServiceTags': [], u'Output': u'', u'CreateIndex': 0}}
[root@node5 test]# ./query.py
('7655', [])
[root@node5 test]# ./heartbeat.py
[root@node5 test]# ./query.py
('7657', [{u'Node': {u'Node': u'node5', u'Datacenter': u'dc1', u'TaggedAddresses': {u'wan': u'192.168.37.150', u'lan': u'192.168.37.150'}, u'ModifyIndex': 6293, u'Meta': {}, u'Address': u'192.168.37.150', u'CreateIndex': 6292, u'ID': u'6c17544c-af06-2298-c01b-bf676315ac9a'}, u'Checks': [{u'Node': u'node5', u'CheckID': u'serfHealth', u'Name': u'Serf Health Status', u'ServiceName': u'', u'Notes': u'', u'ModifyIndex': 6292, u'Status': u'passing', u'ServiceID': u'', u'ServiceTags': [], u'Output': u'Agent alive and reachable', u'CreateIndex': 6292}, {u'Node': u'node5', u'CheckID': u'service:testservice', u'Name': u"Service 'testservice' check", u'ServiceName': u'testservice', u'Notes': u'', u'ModifyIndex': 7657, u'Status': u'passing', u'ServiceID': u'testservice', u'ServiceTags': [], u'Output': u'', u'CreateIndex': 7655}], u'Service': {u'Service': u'testservice', u'Tags': [], u'ModifyIndex': 7657, u'EnableTagOverride': False, u'ID': u'testservice', u'Address': u'', u'CreateIndex': 7655, u'Port': 0}}])
[root@node5 test]# ./query.py
('7659', [])
[root@node5 test]# ./deregister.py
{}
{}
[root@node5 test]#

TCP 版本(go 示例)

注册服务

fangyunlindeMacBookPro:service fangyunlin$ cat register/main.go
package main

import (
    "fmt"
    consul_api "github.com/hashicorp/consul/api"
)

func main() {
    // client, err := consul_api.NewClient(consul_api.DefaultConfig())
    client, err := consul_api.NewClient(&consul_api.Config{Address: "127.0.0.1:8500"})
    if err != nil {
        panic(err)
    }

    agent := client.Agent()
    reg := &consul_api.AgentServiceRegistration{
        Name: "testservice2",
        Check: &consul_api.AgentServiceCheck{
            TCP: "127.0.0.1:80",
            Timeout: "1s",
            Interval: "10s",
        },
    }
    err = agent.ServiceRegister(reg)
    if err != nil {
        panic(err)
    }
    fmt.Println(agent.Services())
    fmt.Println(agent.Checks())
}

查询服务

fangyunlindeMacBookPro:service fangyunlin$ cat query/main.go
package main

import (
    "fmt"
    consul_api "github.com/hashicorp/consul/api"
)

func main() {
    // client, err := consul_api.NewClient(consul_api.DefaultConfig())
    client, err := consul_api.NewClient(&consul_api.Config{Address: "127.0.0.1:8500"})
    if err != nil {
        panic(err)
    }

    health := client.Health()
    fmt.Println(health.Service("testservice2", "", true, nil))
}

注销服务

fangyunlindeMacBookPro:service fangyunlin$ cat deregister/main.go
package main

import (
    "fmt"
    consul_api "github.com/hashicorp/consul/api"
)

func main() {
    // client, err := consul_api.NewClient(consul_api.DefaultConfig())
    client, err := consul_api.NewClient(&consul_api.Config{Address: "127.0.0.1:8500"})
    if err != nil {
        panic(err)
    }

    agent := client.Agent()
    err = agent.ServiceDeregister("testservice2")
    if err != nil {
        panic(err)
    }
    fmt.Println(agent.Services())
    fmt.Println(agent.Checks())
}