基於consul構建golang系統分布式服務發現機制

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

在分布式架構中,服務治理是一個重要的問題。在沒有服務治理的分布式叢集中,各個服務之間通過手工或者配置的方式進行服務關係管理,遇到服務關係變化或者增加服務的時候,人肉配置極其麻煩且容易出錯。

之前在一個C/C++項目中,採用ZooKeeper進行服務治理,可以很好的維護服務之間的關係,但是使用起來較為麻煩。現在越來越多新的項目採用consul進行服務治理,各方面的評價都優於ZooKeeper,經過幾天的研究,這裡做一個總結。

zookeeper和consul比較

  • 開發語言方面,zookeeper採用java開發,安裝的時候需要部署java環境;consul採用golang開發,所有依賴都編譯到了可執行程式中,隨插即用。

  • 部署方面,zookeeper一般部署奇數個節點方便做簡單多數的選舉機制。consul部署的時候分server節點和client節點(通過不同的啟動參數區分),server節點做leader選舉和資料一致性維護,client節點部署在服務機器上,作為服務程式訪問consul的介面。

  • 一致性協議方面,zookeeper使用自訂的zab協議,consul的一致性協議採用更流行的Raft。

  • zookeeper不支援多資料中心,consul可以跨機房支援多資料中心部署,有效避免了單資料中心故障不能訪問的情況。

  • 連結方式上,zookeeper client api和伺服器保持長串連,需要服務程式自行管理和維護連結有效性,服務程式註冊回呼函數處理zookeeper事件,並自己維護在zookeeper上建立的目錄結構有效性(如臨時節點維護);consul 採用DNS或者http擷取服務資訊,沒有主動通知,需要自己輪訓擷取。

  • 工具方面,zookeeper內建一個cli_mt工具,可以通過命令列登入zookeeper伺服器,手動管理目錄結構。consul內建一個Web UI管理系統, 可以通過參數啟動並在瀏覽器中直接查看資訊。

consul相關資源

  • 可執行程式下載地址: https://www.consul.io/downloa...

  • 官方說明文檔: https://www.consul.io/docs/in...

  • api說明文檔: https://godoc.org/github.com/...

  • golang api代碼位置:github.com/hashicorp/consul/api

linux系統中,下載consul可執行程式後直接拷貝到/usr/local/bin就可以使用了,無需其他額外配置。

服務節點啟動方式:

 consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=service-center -bind=192.168.0.2 -client 0.0.0.0 -ui -config-dir /etc/consul.d/

參數說明:

  • -server 表示以server節點模式啟動consul

  • -bootstrap-expect 1 表示期待的server節點一共有幾個,如3個server叢集模式

  • -data-dir consul儲存資料的目錄

  • -node 節點的名字

  • -bind 綁定的服務ip

  • -client 0.0.0.0 -ui 啟動Web UI管理工具

  • -config-dir 指定服務組態檔的目錄(這個目錄下的所有.json檔案,作為服務組態檔讀取)

consul服務發現機制測試

為了測試consul服務治理方式,設定如下情境:

一個manager類型的服務,需要根據負載來管理若干worker類型的服務並進行業務通訊;而worker服務也需要知道manager提供的內部服務介面地址做業務互動。即manger和worker都需要互相知道對方的通訊地址。

做如下規則設定準備:

  • manager和worker都需要向consul註冊自己的服務,讓對方發現自己的服務地址(ip和連接埠)

  • 採用consul的key-value儲存機制,worker周期性更新自己的負載資訊到相應的key;manger從worker的key中擷取負載資訊,並同步更新到本地。

  • 服務類型規則: manager的服務類型用字串"manager"表示,各個worker的服務類型採用字串"worker"表示。

  • 服務註冊ID規則: 服務類型-服務IP,如 manager-192.168.0.2

  • key的構建規則: 服務類型/IP:Port, 如 worker/192.168.0.2:5400

  • 儲存的資料採用json格式:{"load":100,"ts":1482828232}

golang測試程式

package mainimport (    "encoding/json"    "flag"    "fmt"    "github.com/hashicorp/consul/api"    "log"    "math/rand"    "net/http"    "os"    "os/signal"    "strconv"    "strings"    "sync"    "time")type ServiceInfo struct {    ServiceID string    IP        string    Port      int    Load      int    Timestamp int //load updated ts}type ServiceList []ServiceInfotype KVData struct {    Load      int `json:"load"`    Timestamp int `json:"ts"`}var (    servics_map     = make(map[string]ServiceList)    service_locker  = new(sync.Mutex)    consul_client   *api.Client    my_service_id   string    my_service_name string    my_kv_key       string)func CheckErr(err error) {    if err != nil {        log.Printf("error: %v", err)        os.Exit(1)    }}func StatusHandler(w http.ResponseWriter, r *http.Request) {    fmt.Println("check status.")    fmt.Fprint(w, "status ok!")}func StartService(addr string) {    http.HandleFunc("/status", StatusHandler)    fmt.Println("start listen...")    err := http.ListenAndServe(addr, nil)    CheckErr(err)}func main() {    var status_monitor_addr, service_name, service_ip, consul_addr, found_service string    var service_port int    flag.StringVar(&consul_addr, "consul_addr", "localhost:8500", "host:port of the service stuats monitor interface")    flag.StringVar(&status_monitor_addr, "monitor_addr", "127.0.0.1:54321", "host:port of the service stuats monitor interface")    flag.StringVar(&service_name, "service_name", "worker", "name of the service")    flag.StringVar(&service_ip, "ip", "127.0.0.1", "service serve ip")    flag.StringVar(&found_service, "found_service", "worker", "found the target service")    flag.IntVar(&service_port, "port", 4300, "service serve port")    flag.Parse()    my_service_name = service_name    DoRegistService(consul_addr, status_monitor_addr, service_name, service_ip, service_port)    go DoDiscover(consul_addr, found_service)    go StartService(status_monitor_addr)    go WaitToUnRegistService()    go DoUpdateKeyValue(consul_addr, service_name, service_ip, service_port)    select {}}func DoRegistService(consul_addr string, monitor_addr string, service_name string, ip string, port int) {    my_service_id = service_name + "-" + ip    var tags []string    service := &api.AgentServiceRegistration{        ID:      my_service_id,        Name:    service_name,        Port:    port,        Address: ip,        Tags:    tags,        Check: &api.AgentServiceCheck{            HTTP:     "http://" + monitor_addr + "/status",            Interval: "5s",            Timeout:  "1s",        },    }    client, err := api.NewClient(api.DefaultConfig())    if err != nil {        log.Fatal(err)    }    consul_client = client    if err := consul_client.Agent().ServiceRegister(service); err != nil {        log.Fatal(err)    }    log.Printf("Registered service %q in consul with tags %q", service_name, strings.Join(tags, ","))}func WaitToUnRegistService() {    quit := make(chan os.Signal, 1)    signal.Notify(quit, os.Interrupt, os.Kill)    <-quit    if consul_client == nil {        return    }    if err := consul_client.Agent().ServiceDeregister(my_service_id); err != nil {        log.Fatal(err)    }}func DoDiscover(consul_addr string, found_service string) {    t := time.NewTicker(time.Second * 5)    for {        select {        case <-t.C:            DiscoverServices(consul_addr, true, found_service)        }    }}func DiscoverServices(addr string, healthyOnly bool, service_name string) {    consulConf := api.DefaultConfig()    consulConf.Address = addr    client, err := api.NewClient(consulConf)    CheckErr(err)    services, _, err := client.Catalog().Services(&api.QueryOptions{})    CheckErr(err)    fmt.Println("--do discover ---:", addr)    var sers ServiceList    for name := range services {        servicesData, _, err := client.Health().Service(name, "", healthyOnly,            &api.QueryOptions{})        CheckErr(err)        for _, entry := range servicesData {            if service_name != entry.Service.Service {                continue            }            for _, health := range entry.Checks {                if health.ServiceName != service_name {                    continue                }                fmt.Println("  health nodeid:", health.Node, " service_name:", health.ServiceName, " service_id:", health.ServiceID, " status:", health.Status, " ip:", entry.Service.Address, " port:", entry.Service.Port)                var node ServiceInfo                node.IP = entry.Service.Address                node.Port = entry.Service.Port                node.ServiceID = health.ServiceID                //get data from kv store                s := GetKeyValue(service_name, node.IP, node.Port)                if len(s) > 0 {                    var data KVData                    err = json.Unmarshal([]byte(s), &data)                    if err == nil {                        node.Load = data.Load                        node.Timestamp = data.Timestamp                    }                }                fmt.Println("service node updated ip:", node.IP, " port:", node.Port, " serviceid:", node.ServiceID, " load:", node.Load, " ts:", node.Timestamp)                sers = append(sers, node)            }        }    }    service_locker.Lock()    servics_map[service_name] = sers    service_locker.Unlock()}func DoUpdateKeyValue(consul_addr string, service_name string, ip string, port int) {    t := time.NewTicker(time.Second * 10)    for {        select {        case <-t.C:            StoreKeyValue(consul_addr, service_name, ip, port)        }    }}func StoreKeyValue(consul_addr string, service_name string, ip string, port int) {    my_kv_key = my_service_name + "/" + ip + ":" + strconv.Itoa(port)    var data KVData    data.Load = rand.Intn(100)    data.Timestamp = int(time.Now().Unix())    bys, _ := json.Marshal(&data)    kv := &api.KVPair{        Key:   my_kv_key,        Flags: 0,        Value: bys,    }    _, err := consul_client.KV().Put(kv, nil)    CheckErr(err)    fmt.Println(" store data key:", kv.Key, " value:", string(bys))}func GetKeyValue(service_name string, ip string, port int) string {    key := service_name + "/" + ip + ":" + strconv.Itoa(port)    kv, _, err := consul_client.KV().Get(key, nil)    if kv == nil {        return ""    }    CheckErr(err)    return string(kv.Value)}

程式通過參數控制自己啟動的服務角色類型和需要發現的服務類型。傳入的consul_addr是本機consul client agent的地址,一般是loacalhost:8500 。 由於consul整合了服務健全狀態檢查,所以服務需要啟動一個檢查介面,這裡啟動一個http服務來做響應。

consul叢集啟動

啟動3個consul server :

consul agent -server -bootstrap-expect 3 -data-dir /tmp/consul -node=server001 -bind=10.2.1.54consul agent -server  -data-dir /tmp/consul -node=server002 -bind=10.2.1.83 -join 10.2.1.54consul agent -server  -data-dir /tmp/consul -node=server003 -bind=10.2.1.80 -join 10.2.1.54

server001-003構成了一個3個server node的consul叢集。先啟動server001,並指定需要3個server node構成叢集,server002和server003啟動的時候指定加入(-join)server001.

啟動一個manger:

consul agent -data-dir /tmp/consul -node=mangaer -bind=10.2.1.92  -join 10.2.1.54./service -consul_addr=127.0.0.1:8500 -monitor_addr=127.0.0.1:54321 -service_name=manager -ip=10.2.1.92 -port=4300 -found_service=worker

啟動2個worker:

consul agent -data-dir /tmp/consul -node=worker001 -bind=10.2.1.93  -join 10.2.1.54./service -consul_addr=127.0.0.1:8500 -monitor_addr=127.0.0.1:54321 -service_name=worker -ip=10.2.1.93 -port=4300 -found_service=managerconsul agent -data-dir /tmp/consul -node=worker002 -bind=10.2.1.94  -join 10.2.1.54./service -consul_addr=127.0.0.1:8500 -monitor_addr=127.0.0.1:54321 -service_name=worker -ip=10.2.1.94 -port=4300 -found_service=manager

service程式是前面部分代碼編譯後的測試程式。

這樣就構建了3個server node的consul叢集,以及1各manager和2個worker的分布式服務程式,他們可以互相發現對方,並且manager可以擷取到worker的負載情況,實現了互連。

結束

通過使用consul的服務註冊發現機制和key-value儲存機制,實現了服務發現以及manager擷取worker服務負載資料的機制。由於consul的發現機制不能進行更多的資料互動,所以只能使用key-value機制配合進行資料共用(zookeeper中資料可以儲存在節點上)。如果業務有進一步需求,可以方便的擴充儲存的資料結構來實現。

以上的測試程式既有服務註冊,儲存資料更新,也有服務發現和資料擷取,但是代碼量比zookeeper機制少很多,因為zookeeper需要自己建立和維護分類樹,註冊和處理zookeeper event事件,監控zookeeper的連結並處理重連和資訊重建等健康管理工作。

總的來說,consul比zookeeper使用簡單易用很多。可以在新項目中嘗試使用,特別是golang項目,技術棧也比較統一。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.