在微服务架构中,一个应用由一组职责单一化的服务组成。在分布式系统中,各个服务会被动态的部署到不同的节点,为了高可用性,一些服务亦会出现多实例的情况。面对这样一组服务,加上服务的动态伸缩容,如果仅通过人工配置的形式来管理服务之间的依赖,这将是一件无比艰巨的维护任务。于是诞生了“注册中心”这样的解决方案,它提供了注册机制,让服务将自己的信息登记到中心;提供了发现机制,供服务从中心查找其他的服务信息。
下面是来自 Service-Center 官方的设计原理
为了方便理解,这里将整个流程与现实生活中的“找对象”流程类比:
启动流程:
1 待婚者(Provider)向 婚恋机构(Service-Center)提交 个人信息
2 婚恋机构(Service-Center)将 待婚者(Provider)个人信息存储起来(ETCD)
3 求偶者(Consumer)向 婚恋机构(Service-Center)获取符合条件的 待婚者(Provider)信息
4 求偶者(Consumer)将 待婚者(Provider)信息存储到通讯录(cache)
5 求偶者(Consumer)向 婚恋机构(Service-Center)订阅 待婚者(Provider)动态
通讯流程:
1 婚恋机构(Service-Center)制定了契约,要求 待婚者(Provider)每30s向自己报告健康状况(心跳保活),如果未收到报告,信息将会过期并被删除;
2 求偶者(Consumer)向 婚恋机构(Service-Center)订阅了 待婚者(Provider)动态,若有变化,将更新通讯录(cache);
3 求偶者(Consumer)从通讯录(cache)中获取联系方式(endpoints),并进行通讯。
创建名为 “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
// 服务端入口
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
}
配置文件: 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
}
}
}
配置文件: 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)
}
文件位置: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
}
以下基于 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
详见上一章: ServiceComb 开发实战(1) —— Service-Center 安装部署
$ 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 进行访问
进入 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 后成功发送一次心跳
进入 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 的通讯
浏览器打开 http://127.0.0.1:30103 (请使用 service-center 所在主机IP ),可以查看到 service-center 中注册服务的实时状态。
a. 服务端微服务和实例(RestProviderDemo)处于在线状态
b. 客户端微服务(RestConsumerDemo)处于离线状态,无实例信息(RestConsumerDemo 未进行服务实例注册、心跳心跳保活)
c. 服务端(RestProviderDemo)经过 30s 后仍处于在在线状态,说明心跳已生效
a. 修改服务端的启动端口为:8081, 启动第2个实例
b. 查看客户端打印,若出现如下信息,则 watch 生效
2019/01/24 17:13:14 reply from watch service
完整代码 github 地址:https://github.com/ChinX/helloworld