文章目录
代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/11-distributed
以一个日志微服务为例,将日志服务注册到注册中心展开!目录结构如下:
一、registry服务(注册中心服务,包含服务端和客户端)
实际应该单独部署为一个服务,即注册中心,它自身内部便分为了客户端和服务端,如下图所示
registry/registration.go
定义了注册中心一个实例的结构,以及注册中心内部服务端与客户端交互的参数结构
package registry
/*服务使用到的一些参数以及对象*/
// 服务名集合
const (
LogService = ServiceName("LogService")
GradingService = ServiceName("GradingService")
PortalService = ServiceName("Portal")
)
type ServiceName string
// 后端单个需要注册的服务实例
type Registration struct {
ServiceName ServiceName // 服务名
ServiceURL string // 服务实例地址(一个服务可以有很多实例,但每个实例的IP地址肯定是不同的)
RequiredServices []ServiceName // 当前服务依赖的所有其他服务,在当前服务往注册中心注册时传进来
ServiceUpdateURl string // 服务更新的地址 用于接收注册中心信息变更的推送地址
HeartbeatURL string // 心跳地址 用于注册中心检测当前服务是否存活的地址
}
// 单个服务对象参数
type patchEntry struct {
Name ServiceName
URL string
}
// 用于注册中心有服务变更时(注册或注销了服务实例),通知依赖该变更服务的实例更新它们依赖的服务实例列表
type patch struct {
Added []patchEntry
Removed []patchEntry
}
registry/service.go
注册中心的服务端,负责服务的增删改查管理以及心跳检测,及时将注册的服务实例的最新信息通知给注册中心的客户端
package registry
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
)
/*注册中心的服务端,负责服务的增删改查管理以及心跳检测,及时将注册的服务实例的最新信息通知给注册中心的客户端*/
var once sync.Once
func SetupRegistryService() { // 开启协程每3秒进行一次心跳检测
once.Do(func() {
go reg.heartbeat(3 * time.Second)
})
}
// 注册中心管理器,存有所有注册过来的各个服务的所有实例,注:一个服务可以有多个实例
var reg = registry{
registrations: make([]Registration, 0),
mutex: new(sync.RWMutex),
}
// 服务对象集合,也即注册中心管理器
type registry struct {
registrations []Registration
mutex *sync.RWMutex
}
// 心跳检测
func (r *registry) heartbeat(freq time.Duration) {
for {
var wg sync.WaitGroup
for _, reg := range r.registrations {
wg.Add(1)
go func(reg Registration) {
defer wg.Done()
if err := recover(); err != any(nil) {
return // 捕获panic,避免程序退出
}
success := true // 默认要被检测心跳的服务是存活的
for i := 0; i < 3; i++ { // 尝试检测心跳3次,3可以按需作为参数控制
res, err := http.Get(reg.HeartbeatURL)
if err != any(nil) {
log.Println(err)
time.Sleep(1 * time.Second) // 心跳检测失败时的重试间隔隔,可用参数按需设置
continue
}
if res.StatusCode == http.StatusOK {
log.Printf("Heartbeat check passed for %v", reg.ServiceName)
// 检测心跳成功,但是当前服务实例之前可能失联了,说明此处是检测到心跳恢复了,可以重新注册回来
if !success {
r.add(reg)
}
break // 当前轮次心跳检测有一次检测成功,就可以退出当前轮次的检测了
}
// 执行到此,说明心跳检测没有报错,但是状态码不是OK(200),说明是心跳检测失败,移除服务,等待下次心跳检测成功时再注册回来
log.Printf("Heartbeat check failed for %v", reg.ServiceName)
if success {
success = false
r.remove(reg.ServiceURL)
}
time.Sleep(1 * time.Second) // 心跳检测失败时的重试间隔隔,可用参数按需设置
}
}(reg)
}
wg.Wait()
time.Sleep(freq) // 注册中心整体心跳检测间隔,可用参数按需设置
}
}
// 往注册中心中注册服务器实例
func (r *registry) add(reg Registration) error {
r.mutex.Lock()
r.registrations = append(r.registrations, reg)
r.mutex.Unlock()
// 获取当前注册进来的实例依赖的各个服务在注册中心的所有实例
err := r.sendRequiredServices(reg)
// 通知其他服务实例更新对象信息,即其他服务可能依赖了当前实例对应的服务,所以该服务新增的实例,其他依赖它的服务需要更新客户端保留的依赖服务列表信息
r.notify(patch{
Added: []patchEntry{
{Name: reg.ServiceName,
URL: reg.ServiceURL},
},
})
return err
}
// 获取到新注册的服务依赖的各个服务在注册中心的服务实例,然后让新注册实例的客户端保存
func (r *registry) sendRequiredServices(reg Registration) error {
r.mutex.RLock()
defer r.mutex.Unlock()
var p patch
for _, serviceReg := range r.registrations { // 遍历注册中心中所有服务器实例
for _, reqService := range reg.RequiredServices { // 遍历当前新注册进来的服务指定要依赖的服务
if reqService == serviceReg.ServiceName { // 获取到新注册的服务依赖的各个服务在注册中心的服务实例
p.Added = append(p.Added, patchEntry{
Name: serviceReg.ServiceName,
URL: serviceReg.ServiceURL,
})
}
}
}
err := r.sendPatch(p, reg.ServiceUpdateURl) // 告诉新注册的这个实例客户端,它依赖的各个服务的最新实例列表
if err != nil {
return err
}
return nil
}
// 注册中心删除某个服务实例 url:被删除服务实例的url
func (r *registry) remove(url string) error {
for i := range reg.registrations {
if reg.registrations[i].ServiceURL == url {
// 通知实例客户端更新对象信息
r.notify(patch{
Removed: []patchEntry{ // 表示当前服务ServiceName的其中一个实例ServiceURL下线了
{
Name: r.registrations[i].ServiceName,
URL: r.registrations[i].ServiceURL,
},
},
})
r.mutex.Lock()
reg.registrations = append(reg.registrations[:i], reg.registrations[i+1:]...) // 当前实例从注册中心移除
r.mutex.Unlock()
return nil
}
}
return fmt.Errorf("service at URL %s not found", url)
}
// 类似发布订阅模式,有服务的信息变更了,通知其他服务客户端需要刷新他们依赖服务的实例列表
// 比如其他服务依赖了当前变更的服务,则当前服务变更后(新增或剔除了实例),依赖该服务的所有服务都应该更新一下自己的依赖服务列表
func (r *registry) notify(fullPatch patch) {
r.mutex.RLock()
defer r.mutex.Unlock()
// 遍历注册中心的所有服务,得到依赖当前变更服务的所有服务,对需要更新的进行更新
for _, reg := range reg.registrations {
go func(reg Registration) { // 开启协程刷新每一个需要更新的服务客户端
for _, reqService := range reg.RequiredServices { // 遍历当前服务依赖的所有服务
p := patch{Added: []patchEntry{}, Removed: []patchEntry{}}
sendUpdate := false
for _, added := range fullPatch.Added {
if reqService == added.Name {
p.Added = append(p.Added, added)
sendUpdate = true
}
}
for _, removed := range fullPatch.Removed {
if reqService == removed.Name {
p.Removed = append(p.Removed, removed)
sendUpdate = true
}
}
if sendUpdate {
err := r.sendPatch(p, reg.ServiceUpdateURl) // 请求需要更新的服务指定的更新接收地址,进行更新
if err != nil {
log.Println(err)
return
}
}
}
}(reg)
}
}
// 通知客户端它依赖的服务最新的服务列表
func (r registry) sendPatch(p patch, url string) error {
d, err := json.Marshal(p)
if err != nil {
return err
}
_, err = http.Post(url, "application/json", bytes.NewBuffer(d))
if err != nil {
return err
}
return nil
}
registry/registry_service.go
上面写完了注册中心的服务端,下面则是定义了注册中心服务的Handler,注:RegistryService 实现了Handler接口,可以作为http.Handle的一个路由对应的处理函数
package registry
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
)
// 注册中心地址
const ServicePort = ":3000"
const ServicesURL = "http://localhost" + ServicePort + "/services"
// 注册中心服务
type RegistryService struct {
}
func (s RegistryService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Println("Request received")
switch r.Method {
case http.MethodPost: // 往注册中心注册服务
// Reader无法直接反序列化到对象中,故将Reader类型的JSON解码为Decoder对象,方便解析到对象中
// 或者使用ioutil.ReadAll先读取出数据为字节数组,然后反序列化到对象中也是可以的
dec := json.NewDecoder(r.Body)
var r Registration
err := dec.Decode(&r)
if err != nil {
log.Printf("[ServeHTTP] request is invalid,request:%v\n", r)
w.WriteHeader(http.StatusBadRequest)
return
}
log.Printf("[ServeHTTP] Adding service:%v with URL:%s\n", r.ServiceName, r.ServiceURL)
err = reg.add(r)
if err != nil {
log.Printf("[ServeHTTP] Adding service failed,err:%v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
case http.MethodDelete: // 从注册中心注销服务
payload, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("[ServeHTTP] read r.body err,err:%v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
url := string(payload)
log.Printf("Removing service at URL:%s", url)
err = reg.remove(url)
if err != nil {
log.Printf("[ServeHTTP] Removing service failed,err:%v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
// 启动注册中心 // 实际应该写到注册中心服务的main方法中去
func startService() {
// 注册路由,监听实例注册和注销的请求
http.Handle(ServicesURL, &RegistryService{})
// 开启心跳检测
SetupRegistryService()
//启动服务
log.Println(srv.ListenAndServe(ServicesPort))
}
registry/client.go
注册中心内部的客户端,为外部服务提供实例管理、注册以及注销的接口
实例在往注册中心注册的时候会连着心跳以及服务更新的方法一起注册!从而使得注册中心可以对当前实例进行探活检测,以及通知其更新他依赖的服务的实例列表信息。
package registry
import (
"bytes"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"net/url"
"sync"
)
// 注:一个实例注册到注册中心了,该实例就是注册中心的一个客户端,而下面providers中保存的就是当前服务依赖的所有服务,各个服务的实例列表
// 服务提供对象
var prov = providers{
services: make(map[ServiceName][]string), // 服务列表 服务名->对应服务名的集群地址集合(对应服务的每个服务器实例url地址)
mutex: new(sync.RWMutex), // 锁 防止服务注册更新时的并发情况
}
// 当前服务依赖的服务,各个服务在注册中心已经注册的所有实例
type providers struct {
services map[ServiceName][]string
mutex *sync.RWMutex
}
// 对外暴露生产者的方法
func GetProvider(name ServiceName) (string, error) {
return prov.get(name)
}
// 使用随机负载均衡算法获取指定服务名的一台服务器地址
func (p providers) get(name ServiceName) (string, error) {
providers, ok := p.services[name]
if !ok {
return "", fmt.Errorf("no providers available for service %v", name)
}
idx := int(rand.Float32() * float32(len(providers)))
return providers[idx], nil
}
// 更新服务列表
func (p *providers) Update(pat patch) {
p.mutex.Lock()
defer p.mutex.Unlock()
// 依赖的某个服务新增了服务器实例,因此可以加到对应服务所处的集群(服务器实例列表)中
for _, patchEntry := range pat.Added {
if _, ok := p.services[patchEntry.Name]; !ok {
p.services[patchEntry.Name] = make([]string, 0) // 当前依赖的这个服务是对应服务的第一个往注册中心注册的实例
}
p.services[patchEntry.Name] = append(p.services[patchEntry.Name], patchEntry.URL)
}
// 依赖的某个服务有服务器实例下线了,需要从列表中删除
for _, patchEntry := range pat.Removed {
if providersUrls, ok := p.services[patchEntry.Name]; ok {
for i := range providersUrls {
if providersUrls[i] == patchEntry.URL {
p.services[patchEntry.Name] = append(providersUrls[:i], providersUrls[i+1:]...)
}
}
}
}
}
type serviceUpdateHandler struct {
}
// 实现Handler接口,从而可以作为http.Handle方式注册路由,对应路由的处理器
func (s serviceUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
dec := json.NewDecoder(r.Body)
var p patch
err := dec.Decode(&p)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("updated received %v\n", p)
prov.Update(p) // 更新服务提供对象
}
// 注册服务
func RegisterService(r Registration) error {
// 获得心跳地址并注册路由接收注册中心发来的心跳检测
heartbeatURL, err := url.Parse(r.HeartbeatURL)
if err != nil {
return err
}
// 1. 接收注册中心发来的心跳检测请求,并响应200,证明自己还存活
http.HandleFunc(heartbeatURL.Path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// 获得服务接收更新的地址,并且自定义http服务的handler,因为每次更新服务的时候,可以在ServeHttp方法里面去维护
serviceUpdateURL, err := url.Parse(r.ServiceUpdateURl)
if err != nil {
return err
}
// 2. 注册更新路由,用于接收注册中心相关信息变更后的通知
http.Handle(serviceUpdateURL.Path, &serviceUpdateHandler{})
// 3. 将当前服务器实例注册到注册中心去
//将服务对象发送给注册中心的services地址完成注册,post方式为注册
buf := new(bytes.Buffer) // 属于IO.Reader类型,是http.Post接收的请求体类型
enc := json.NewEncoder(buf)
err = enc.Encode(r) // 将当前需要注册的服务器信息编码为IO.Reader对象类型
if err != nil {
return err
}
res, err := http.Post(ServicesURL, "application/json", buf) // 注册服务
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("failed to register service. Registry service "+"responded with code %v", res.StatusCode)
}
return nil
}
// 删除对应注册中心的服务地址
func ShutdownService(url string) error {
req, err := http.NewRequest(http.MethodDelete, ServicesURL, bytes.NewBuffer([]byte(url)))
if err != nil {
return err
}
req.Header.Add("Content-Type", "text/plain")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("failed to deregister service. Registry "+"service responded with code %v", res.StatusCode)
}
return nil
}
二 、日志服务(用于向注册中心注册)
service/service.go
封装了日志服务的启动和往注册中心注册的接口
package service
import (
"context"
"fmt"
"golang-trick/11-distributed/registry"
"log"
"net/http"
)
/*
host: 地址
port: 端口号
reg: 注册的服务对象
registerHandlersFunc: 注册方法
*/
func Start(ctx context.Context, host, port string, reg registry.Registration, registerHandlersFunc func()) (context.Context, error) {
// 启动注册方法
registerHandlersFunc()
// 启动服务
ctx = startService(ctx, reg.ServiceName, host, port)
// 注册服务
err := registry.RegisterService(reg)
if err != nil {
return ctx, err
}
return ctx, nil
}
func startService(ctx context.Context, serviceName registry.ServiceName, host string, port string) context.Context {
ctx, cancel := context.WithCancel(ctx)
var srv http.Server
srv.Addr = host + ":" + port
// 该协程为监听http服务,并且停止服务的时候cancel,实现优雅关闭服务
go func() {
log.Println(srv.ListenAndServe())
err := registry.ShutdownService(fmt.Sprintf("http:%s:%s", host, port))
if err != nil {
log.Println(err)
}
cancel()
}()
// 该协程为监听手动停止服务的信号
go func() {
fmt.Printf("%v started. Press any key to stop. \n", serviceName)
var s string
fmt.Scanln(&s)
err := registry.ShutdownService(fmt.Sprintf("http:%s:%s", host, port))
if err != nil {
log.Println(err)
}
srv.Shutdown(ctx)
cancel()
}()
return ctx
}
log/service.go
提供了日志服务能够提供的服务,即有哪些路由以及路由对应的handler
package log
import (
"io/ioutil"
stlog "log"
"net/http"
"os"
)
var log *stlog.Logger
type fileLog string
// 编写日志的方法
func (fl fileLog) Write(data []byte) (int, error) {
f, err := os.OpenFile(string(fl), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return 0, err
}
defer f.Close()
return f.Write(data)
}
// 启动一个日志对象 参数为日志文件名
func Run(destination string) {
log = stlog.New(fileLog(destination), "[go] - ", stlog.LstdFlags)
}
// 自身注册的一个服务方法
func RegisterHandlers() {
http.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
msg, err := ioutil.ReadAll(r.Body)
if err != nil || len(msg) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
write(string(msg))
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
})
}
func write(message string) {
log.Printf("%v\n", message)
}
logservice/main.go
用于启动日志服务,以及将实例注册到注册中心
package main
import (
"context"
"fmt"
"golang-trick/11-distributed/log"
"golang-trick/11-distributed/registry"
"golang-trick/11-distributed/service"
stlog "log"
)
func main() {
// 初始化启动一个日志文件对象
log.Run("./distributed.log")
// 日志服务注册的端口和地址
host, port := "localhost", "4000"
serviceAddress := fmt.Sprintf("http:%s:%s", host, port)
// 初始化注册对象
r := registry.Registration{
ServiceName: registry.LogService, // 自身服务名
ServiceURL: serviceAddress, // 自身服务地址
RequiredServices: make([]registry.ServiceName, 0), // 当前服务依赖哪些服务,在这里传入,此处是LogService不依赖其他服务,所以传的空列表
ServiceUpdateURl: serviceAddress + "/services", // 接收注册中心通知的地址
HeartbeatURL: serviceAddress + "/heartbeat", // 接收注册中心心跳检测的地址
}
// 启动当前日志服务实例,包含服务注册、发现
ctx, err := service.Start(
context.Background(),
host,
port,
r,
log.RegisterHandlers,
)
if err != nil {
stlog.Fatalln(err) // stlog 为go自带的log包起的别名
}
//超时停止退出服务
<-ctx.Done()
fmt.Println("shutting down log service.")
}
log/client.go
日志服务的客户端(假设是商品服务),用于从注册中心获取到日志服务的某个实例,然后调用日志服务的路由,如下代码中的res, err := http.Post(cl.url+"/log", "text/plain", b)
package log
import (
"bytes"
"fmt"
"golang-trick/11-distributed/registry"
stlog "log"
"net/http"
)
func SetClientLogger(serviceURL string, clientService registry.ServiceName) {
stlog.SetPrefix(fmt.Sprintf("[%v] - ", clientService))
stlog.SetFlags(0)
stlog.SetOutput(&clientLogger{url: serviceURL})
}
type clientLogger struct {
url string
}
func (cl clientLogger) Write(data []byte) (int, error) {
b := bytes.NewBuffer([]byte(data))
// 请求日志服务的路由
res, err := http.Post(cl.url+"/log", "text/plain", b)
if err != nil {
return 0, err
}
if res.StatusCode != http.StatusOK {
return 0, fmt.Errorf("Failed to send log message. Service responded with %d - %s", res.StatusCode, res.Status)
}
return len(data), nil
}
至此,一个注册中心的简单实现就完成了,当然,这个只是一个练手的小demo
,用于熟悉注册与发现的基本思路而已,实际使用时还是有很多需要改进的地方的。