ServiceComb 开发实战(2)—— Service-Center 服务注册发现对接

文章目录

      • 一. 服务注册发现
        • 为什么使用服务注册发现
        • 服务注册发现流程
      • 二. Service-Center对接
        • 1.创建项目
        • 2. v3 接口 client 实现
        • 3. 服务端实现
        • 4. 消费端实现
        • 5. 配置文件加载解析
        • 6. 构建编译
      • 三. 功能验证
        • 1. 启动 Service-Center
        • 2. 启动 Service-Center 前端 web 页面
        • 3. 启动 provider
        • 4. 启动 consumer
        • 5. 页面查看微服务状态
        • 6. 消费端 watch 验证

上一章 ServiceComb 开发实战(1) —— Service-Center 安装部署 中,主要了解了一下 Service-Center(SC)的安装和启动(不太了解的童鞋可以点击链接查看)。在文章末尾我们使用了 curl 命令模拟 provider 与 consumer 服务的注册与发现流程,本章将通过官方的 api 文档,动手构建对接示例。

一. 服务注册发现

为什么使用服务注册发现

在微服务架构中,一个应用由一组职责单一化的服务组成。在分布式系统中,各个服务会被动态的部署到不同的节点,为了高可用性,一些服务亦会出现多实例的情况。面对这样一组服务,加上服务的动态伸缩容,如果仅通过人工配置的形式来管理服务之间的依赖,这将是一件无比艰巨的维护任务。于是诞生了“注册中心”这样的解决方案,它提供了注册机制,让服务将自己的信息登记到中心;提供了发现机制,供服务从中心查找其他的服务信息。

服务注册发现流程

下面是来自 Service-Center 官方的设计原理
为了方便理解,这里将整个流程与现实生活中的“找对象”流程类比:

启动流程:
ServiceComb 开发实战(2)—— Service-Center 服务注册发现对接_第1张图片
1 待婚者(Provider)向 婚恋机构(Service-Center)提交 个人信息
2 婚恋机构(Service-Center)将 待婚者(Provider)个人信息存储起来(ETCD)
3 求偶者(Consumer)向 婚恋机构(Service-Center)获取符合条件的 待婚者(Provider)信息
4 求偶者(Consumer)将 待婚者(Provider)信息存储到通讯录(cache)
5 求偶者(Consumer)向 婚恋机构(Service-Center)订阅 待婚者(Provider)动态

通讯流程:
ServiceComb 开发实战(2)—— Service-Center 服务注册发现对接_第2张图片
1 婚恋机构(Service-Center)制定了契约,要求 待婚者(Provider)每30s向自己报告健康状况(心跳保活),如果未收到报告,信息将会过期并被删除;
2 求偶者(Consumer)向 婚恋机构(Service-Center)订阅了 待婚者(Provider)动态,若有变化,将更新通讯录(cache);
3 求偶者(Consumer)从通讯录(cache)中获取联系方式(endpoints),并进行通讯。

二. Service-Center对接

1.创建项目

创建名为 “helloworld”的项目,以下为参考目录结构:

.
└── rest
    ├── common
    │   ├── config
    │   │   └── config.go
    │   │       // 配置文件解析
    │   │   
    │   ├── restful
    │   │   └── restutil.go
    │   │       // http 请求简单封装
    │   │   
    │   └── servicecenter
    │       └── v3
    │           └── registery.go
    │               // Service-Center v3 接口 client 实现
    ├── consumer
    │   ├── conf
    │   │   └── microservice.yaml
    │   │       // 微服务配置
    │   │
    │   └── helloclient.go
    │       // 消费端入口
    │
    └── provider
        ├── conf
        │   └── microservice.yaml
        │       // 微服务配置
        │
        └── helloserver.go
            // 服务端入口

2. v3 接口 client 实现

Service-Center 的服务注册与发现是基于 RESTful 标准接口实现的,与编程语言无关,以下内容基于 golang 进行实现,仅因本人对 golang 比较熟悉,其他语言可以参考官网 API 文档 进行实现。

文件位置:rest/common/servicecenter/v3/registery.go 文件

var (
    // 接口 API 定义
    microServices = "/registry/v3/microservices"
    svcInstances  = "/registry/v3/microservices/%s/instances"
    discovery     = "/registry/v3/instances"
    existence     = "/registry/v3/existence"
    heartbeats    = "/registry/v3/heartbeats"
    watcher       = "/registry/v3/microservices/%s/watcher"

    microServiceType sourceType = "microservice"
    schemaType       sourceType = "schema"
)

type sourceType string

type Client struct {
    rawURL string
    domain string
}

func NewClient(addr string, domain string) *Client {
    return &Client{rawURL: addr, domain: domain}
}

// 查询微服务是否存在
func (c *Client) existence(params url.Values) (*proto.GetExistenceResponse, error) {
    reqURL := c.rawURL + existence + "?" + params.Encode()
    
    // 对http.NewRequest接口的简单封装,详情请见 rest/common/restful/restutil.go
    req, err := restful.NewRequest(http.MethodGet, reqURL, c.DefaultHeaders(), nil)
    if err == nil {
        respData := &proto.GetExistenceResponse{}
        
        // 对http.Do接口的简单封装,详情请见 rest/common/restful/restutil.go
        err = restful.DoRequest(req, respData)
        if err == nil {
            return respData, nil
        }
    }
    return nil, err
}

// 获取微服务服务ID
func (c *Client) GetServiceID(svc *config.ServiceConf) (string, error) {
    val := url.Values{}
    val.Set("type", string(microServiceType))
    val.Set("appId", svc.AppID)
    val.Set("serviceName", svc.Name)
    val.Set("version", svc.Version)
    respData, err := c.existence(val)
    if err == nil {
        return respData.ServiceId, nil
    }
    return "", fmt.Errorf("[GetServiceID]: %s", err)
}

// 注册微服务
func (c *Client) RegisterService(svc *config.ServiceConf) (string, error) {
    ms := &proto.CreateServiceRequest{
        Service: &proto.MicroService{
            AppId:       svc.AppID,
            ServiceName: svc.Name,
            Version:     svc.Version,
        },
    }

    reqURL := c.rawURL + microServices
    req, err := restful.NewRequest(http.MethodPost, reqURL, c.DefaultHeaders(), ms)
    if err == nil {
        respData := &proto.CreateServiceResponse{}
        err = restful.DoRequest(req, respData)
        if err == nil {
            return respData.ServiceId, nil
        }
    }
    return "", fmt.Errorf("[RegisterService]: %s", err)
}

// 注册微服务实例
func (c *Client) RegisterInstance(svcID string, ins *config.InstanceConf) (string, error) {
    endpoint := ins.Protocol + "://" + ins.ListenAddress
    ms := &proto.RegisterInstanceRequest{
        Instance: &proto.MicroServiceInstance{
            HostName:  ins.Hostname,
            Endpoints: []string{endpoint},
        },
    }

    reqURL := c.rawURL + fmt.Sprintf(svcInstances, svcID)
    req, err := restful.NewRequest(http.MethodPost, reqURL, c.DefaultHeaders(), ms)
    if err == nil {
        respData := &proto.RegisterInstanceResponse{}
        err = restful.DoRequest(req, respData)
        if err == nil {
            return respData.InstanceId, nil
        }
    }
    return "", fmt.Errorf("[RegisterInstance]: %s", err)
}

// 心跳保活
func (c *Client) Heartbeat(svcID, insID string) error {
    hb := &proto.HeartbeatSetRequest{
        Instances: []*proto.HeartbeatSetElement{
            {ServiceId: svcID, InstanceId: insID},
        },
    }

    reqURL := c.rawURL + heartbeats
    req, err := restful.NewRequest(http.MethodPut, reqURL, c.DefaultHeaders(), hb)
    if err == nil {
        err = restful.DoRequest(req, nil)
    }
    if err != nil {
        return fmt.Errorf("[Heartbeat]: %s", err)
    }
    return nil
}

// 服务发现
func (c *Client) Discovery(conID string, svc *config.ServiceConf) ([]*proto.MicroServiceInstance, error) {
    val := url.Values{}
    val.Set("appId", svc.AppID)
    val.Set("serviceName", svc.Name)
    val.Set("version", svc.Version)

    reqURL := c.rawURL + discovery + "?" + val.Encode()
    req, err := restful.NewRequest(http.MethodGet, reqURL, c.DefaultHeaders(), nil)
    if err == nil {
        req.Header.Set("x-consumerid", conID)
        respData := &proto.GetInstancesResponse{}
        err = restful.DoRequest(req, respData)
        if err == nil {
            return respData.Instances, nil
        }
    }
    return nil, fmt.Errorf("[Discovery]: %s", err)
}

// 服务订阅
func (c *Client) WatchService(svcID string, callback func(*proto.WatchInstanceResponse)) error {
    addr, err:= url.Parse(c.rawURL + fmt.Sprintf(watcher, svcID))
    if err != nil {
        return fmt.Errorf("[WatchService]: parse repositry url faild: %s", err)
    }

    // 注: watch接口使用了 websocket 长连接
    addr.Scheme = "ws"
    conn, _, err := (&websocket.Dialer{}).Dial(addr.String(),c.DefaultHeaders())
    if err != nil {
        return fmt.Errorf("[WatchService]: start websocket faild: %s", err)
    }

    for {
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Println(err)
            break
        }
        if messageType == websocket.TextMessage {
            data := &proto.WatchInstanceResponse{}
            err := json.Unmarshal(message, data)
            if err != nil {
                log.Println(err)
                break
            }
            callback(data)
        }
    }
    return fmt.Errorf("[WatchService]: receive message faild: %s", err)
}

// 设置默认头部
func (c *Client) DefaultHeaders() http.Header {
    headers := http.Header{
        "Content-Type":  []string{"application/json"},
        "X-Domain-Name": []string{"default"},
    }
    if c.domain != "" {
        headers.Set("X-Domain-Name", c.domain)
    }
    return headers
}

3. 服务端实现

配置文件: rest/provider/conf/microservice.ymal

service: # 微服务配置
  name: RestProviderDemo
  version: 1.0.0 
  appId: default
instance: # 实例信息
  hostname: rest-provider-demo
  protocol: rest
  listenAddress: 127.0.0.1:8080 #实例监听地址
registry: 
  address: http://127.0.0.1:30100 # service-center 地址

入口文件: rest/provider/helloserver.go

// 心跳间隔
var HeartbeatInterval = 30 * time.Second

func main() {
    // 加载配置文件
    err := config.LoadConfig("./conf/microservice.yaml")
    if err != nil {
        log.Fatalf("load config file faild: %s", err)
    }

    ctx, cancel := context.WithCancel(context.Background())
    // 注册微服务与实例,启动心跳
    go registerAndHeartbeat(ctx)

    // 启动 http 监听
    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("hello world"))
    })
    err = http.ListenAndServe(config.Instance.ListenAddress, nil)
    log.Println(err)
    cancel()
}

func registerAndHeartbeat(ctx context.Context) {
    // 微服务未注册则注册其信息
    cli := v3.NewClient(config.Registry.Address, config.Tenant.Domain)
    svcID, _ := cli.GetServiceID(config.Service)
    if svcID == "" {
        var err error
        svcID, err = cli.RegisterService(config.Service)
        if err != nil {
            log.Fatalln(err)
        }
    }

    // 注册微服务实例
    instanceID, err := cli.RegisterInstance(svcID, config.Instance)
    if err != nil {
        log.Fatalln(err)
    }

    // 启动定时器:间隔30s
    tk := time.NewTicker(HeartbeatInterval)
    for {
        select {
        case <-tk.C:
            // 定时发送心跳
            err := cli.Heartbeat(svcID, instanceID)
            if err != nil {
                log.Println(err)
                tk.Stop()
                return
            }
            log.Println("send heartbeat success")
        // 监听程序退出
        case <-ctx.Done():
            tk.Stop()
            log.Println("service is done")
            return
        }
    }
}

4. 消费端实现

配置文件: rest/consumer/conf/microservice.ymal

service: # 微服务配置
  name: RestConsumerDemo
  version: 1.0.0
  appId: default
registry:
  address: http://127.0.0.1:30100 # service-center 地址
provider: # 服务端信息
  name: RestProviderDemo
  appId: default
  version: 1.0.0

入口文件: rest/consumer/helloclient.go

// 缓存,使用并发安全的 map  
var caches = &sync.Map{}

func main() {
    // 配置文件加载
    err := config.LoadConfig("./conf/microservice.yaml")
    if err != nil {
        log.Fatalf("load config file faild: %s", err)
    }

    // 注册自身微服务
    svcID := registerService()

    // 服务发现 provider 实例信息
    discoveryProviderAndCache(svcID)

    // 与 provider 通讯
    log.Println(sayHello())

    // 提供对外服务,将请求转发到 helloServer 处理,验证 watch 功能
    sayHelloServer(svcID)
}

// 提供对外服务,将请求转发到 helloServer 处理,验证 watch 功能
func sayHelloServer(svcID string)  {
    // 启动 provider 订阅
    go watch(svcID)

    // 启动 http 监听
    http.HandleFunc("/sayhello", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(sayHello()))
    })
    err := http.ListenAndServe(":8090", nil)
    log.Println(err)
}

// 注册微服务信息
func registerService() string {
    // 微服务未注册则注册其信息
    cli := v3.NewClient(config.Registry.Address, config.Tenant.Domain)
    svcID, _ := cli.GetServiceID(config.Service)
    if svcID == "" {
        var err error
        svcID, err = cli.RegisterService(config.Service)
        if err != nil {
            log.Fatalln(err)
        }
    }
    return svcID
}

// 服务发现 provider  实例信息, 并缓存
func discoveryProviderAndCache(svcID string) {
    // 服务发现 provider 实例信息
    cli := v3.NewClient(config.Registry.Address, config.Tenant.Domain)
    pris, err := cli.Discovery(svcID, config.Provider)
    if err != nil {
        log.Fatalln(err)
    }

    if len(pris) == 0 {
        log.Fatalf("provider not found, serviceName: %s appID: %s, version: %s",
            config.Provider.Name, config.Provider.AppID, config.Provider.Version)
    }

    if len(pris[0].Endpoints) == 0 {
        log.Fatalln("provider endpoints is empty")
    }
    
    // 缓存 provider 实例信息
    caches.Store(config.Provider, pris)
}

// 订阅 provider 变更
func watch(svcID string) {
    cli := v3.NewClient(config.Registry.Address, config.Tenant.Domain)
    err := cli.WatchService(svcID, watchCallback)
    if err != nil {
        log.Println(err)
    }
}

// 订阅回调,更新 provider 缓存
func watchCallback(data *proto.WatchInstanceResponse) { 
    log.Println("reply from watch service")
    prisCache, ok := caches.Load(config.Provider)
    if !ok {
        log.Printf("provider \"%s\" not found", config.Provider.Name)
        return
    }
    pris := prisCache.([]*proto.MicroServiceInstance)
    renew := false
    for i := 0; i < len(pris); i ++{
        if pris[i].InstanceId == data.Instance.InstanceId{
            pris[i] = data.Instance
            renew = true
            break
        }
    }
    if !renew {
        pris = append(pris, data.Instance)
    }
    caches.Store(config.Provider, pris)
}

// 获取在线的 provider endpoint
func getProviderEndpoint() (string, error) {
    prisCache, ok := caches.Load(config.Provider)
    if !ok {
        return "", fmt.Errorf("provider \"%s\" not found", config.Provider.Name)

    }

    endpoint := ""
    pris := prisCache.([]*proto.MicroServiceInstance)

    for i := 0; i < len(pris); i ++{
        if pris[i].Status == "UP"{
            endpoint = pris[i].Endpoints[0]
            break
        }
    }

    if endpoint != ""{
        addr, err := url.Parse(endpoint)
        if err != nil {
            return "", fmt.Errorf("parse provider endpoint faild: %s", err)
        }
        if addr.Scheme == "rest" {
            addr.Scheme = "http"
        }
        return addr.String(), nil
    }
    return "", fmt.Errorf("provider \"%s\" endpoint not found", config.Provider.Name)
}

// 与 provider 通讯
func sayHello() string {
    addr, err := getProviderEndpoint()
    if err != nil {
        return err.Error()
    }
    req, err := http.NewRequest(http.MethodGet, addr+"/hello", nil)
    if err != nil {
        return fmt.Sprintf("create request faild: %s", err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return fmt.Sprintf("do request faild: %s", err)
    }

    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return fmt.Sprintf("read response body faild: %s, body: %s", err, string(data))
    }

    log.Printf("reply form provider: %s", string(data))
    return string(data)
}

5. 配置文件加载解析

文件位置:common/config/config.go

var (
    Service  *ServiceConf
    Instance *InstanceConf
    Registry *RegistryConf
    Provider *ServiceConf
    Tenant   *TenantConf
)

// microservice.yaml 配置
type MicroService struct {
    Service  *ServiceConf  `yaml:"service"`
    Instance *InstanceConf `yaml:"instance"`
    Registry *RegistryConf `yaml:"registry"`
    Provider *ServiceConf  `yaml:"provider"`
    Tenant   *TenantConf   `yaml:"tenant"`
}

// 微服务配置
type ServiceConf struct {
    Name    string `yaml:"name"`
    Version string `yaml:"version"`
    AppID   string `yaml:"appId"`
}

// 实例配置
type InstanceConf struct {
    Hostname      string `yaml:"hostname"`
    Protocol      string `yaml:"protocol"`
    ListenAddress string `yaml:"listenAddress"`
}

// Service-Center 配置
type RegistryConf struct {
    Address string `yaml:"address"`
}

// 租户信息
type TenantConf struct {
    Domain string `yaml:"domain"`
}

// 加载配置
func LoadConfig(filePath string) error {
    data, err := ioutil.ReadFile(filePath)
    if err != nil {
        return err
    }

    conf := MicroService{}

    err = yaml.Unmarshal(data, &conf)
    // ... 省略参数校验

    Service = conf.Service
    Instance = conf.Instance
    Registry = conf.Registry
    Provider = conf.Provider
    Tenant = conf.Tenant
    return nil
}

6. 构建编译

以下基于 go 1.11+ 进行构建,请检测自身 go 环境.
进入 hellowrld 目录:

# 初始化项目
$ GO111MODULE=on go mod init github.com/chinx/helloworld
go: creating new go.mod: module github.com/chinx/helloworld

# 检测并下载依赖包
$ go mod tidy

# 编译 provider
$ cd rest/provider/
$ GO111MODULE=on go build

# 编译 consumer
$ cd ../consumer/
$ GO111MODULE=on go build

三. 功能验证

1. 启动 Service-Center

详见上一章: ServiceComb 开发实战(1) —— Service-Center 安装部署

2. 启动 Service-Center 前端 web 页面

$ cd $GOPATH/src/github.com/apache/servicecomb-service-center/frontend
$ GO111MODULE=on go build
$ mv frontend ../frontend-server
$ cd ../
$ ./frontend-server

可通过 http://127.0.0.1:30103 进行访问

3. 启动 provider

进入 helloworld 项目目录

cd rest/provider
$ ./provider
2019/01/24 16:37:12 {"serviceId":"0ed181551fb311e99ebbfa163eca30e0"}
2019/01/24 16:37:12 {"instanceId":"0ed208e81fb311e99ebbfa163eca30e0"}
2019/01/24 16:37:42 send heartbeat success

服务端启动监听,成功注册自身服务,并且在 30s 后成功发送一次心跳

4. 启动 consumer

进入 helloworld 项目目录

cd rest/provider
$ ./consumer
2019/01/24 16:41:42 {"serviceId":"7a10d0af1fb311e99ebbfa163eca30e0"}
2019/01/24 16:41:42 {"instances":[{"instanceId":"0ed208e81fb311e99ebbfa163eca30e0","serviceId":"0ed181551fb311e99ebbfa163eca30e0","endpoints":["rest://127.0.0.1:8080"],"hostName":"rest-provider-demo","status":"UP","healthCheck":{"mode":"push","interval":30,"times":3},"timestamp":"1548318951","modTimestamp":"1548318951","version":"1.0.0"}]}

2019/01/24 16:41:42 reply form provider: hello world
2019/01/24 16:41:42 hello world

消费端执行服务发现成功,并完成了一次与 provider 的通讯

5. 页面查看微服务状态

浏览器打开 http://127.0.0.1:30103 (请使用 service-center 所在主机IP ),可以查看到 service-center 中注册服务的实时状态。

ServiceComb 开发实战(2)—— Service-Center 服务注册发现对接_第3张图片

ServiceComb 开发实战(2)—— Service-Center 服务注册发现对接_第4张图片

a. 服务端微服务和实例(RestProviderDemo)处于在线状态
b. 客户端微服务(RestConsumerDemo)处于离线状态,无实例信息(RestConsumerDemo 未进行服务实例注册、心跳心跳保活)
c. 服务端(RestProviderDemo)经过 30s 后仍处于在在线状态,说明心跳已生效

6. 消费端 watch 验证

a. 修改服务端的启动端口为:8081, 启动第2个实例
b. 查看客户端打印,若出现如下信息,则 watch 生效

2019/01/24 17:13:14 reply from watch service

完整代码 github 地址:https://github.com/ChinX/helloworld

你可能感兴趣的:(ServiceCenter)