Go语言实现简单分布式系统
使用Go语言实现比较简单的分布式系统,这个系统中采用多个分布式模型,即混合模型,并且基于HTTP进行通信,传输JSON数据
github链接: https://github.com/T4t4KAU/distributed/tree/main/Simple-distributed-system
服务注册
服务进程是在注册中心注册自己的元数据信息,通常包括主机和端口号,有时还有身份验证信息,协议,版本号,以及运行环境的信息
本系统中包含了以下内容:
- 创建Web服务
- 创建注册服务
- 注册Web服务
- 取消注册Web服务
创建日志服务
在正式实现服务注册的功能之前,先实现日志服务,在项目文件夹下创建一个log文件夹,存放定义日志逻辑的代码
编写日志服务逻辑
日志服务是一个WEB服务,功能是接收web请求,将POST请求的内容写入到log,注意这里对标准的log包起了别名stdlog,因为后续要自定义一个Logger对象log:
// log/server.go
package log
import (
"io/ioutil"
stdlog "log"
"net/http"
"os"
)
var log *stdlog.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 = stdlog.New(fileLog(destination), "go", stdlog.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) // 读取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)
}
这段代码的作用是将日志写入文件系统,先为filelog实现io.Writer接口,定义Write方法
在这个方法中,首先调用了OpenFile方法,传入一个文件路径并返回一个file对象,指定了权限和模式,并随后判断是否产生错误,最后通过io.Writer接口写入文件:
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)
}
接着定义Run函数,作用是将log指向某个文件路径,使用log.New来创建一个Logger对象,要传入的参数: 写入的位置(实现io.Writer接口)、日志前缀和日志内容的flag(包含了日期和时间)
func Run(destination string) {
log = stdlog.New(fileLog(destination), "go", stdlog.LstdFlags)
}
然后注册一个handler:
func RegisterHandlers() {
http.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost: // POST请求
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
}
})
}
使用了一个http.HandleFunc来处理HTTP请求,使用switch-case结构判断请求方式来分支处理请求,如果是POST请求则调用ioutil.ReadAll读取Body数据,调用write函数(之后实现)写入文件,如果读取失败或者数据为空,则返回一个BadRequest(400)响应。如果接收到的请求不是POST请求,则返回一个MethodNotAllowed响应(405)
如下是write函数:
func write(message string) {
log.Printf("%v\n", message)
}
调用log.Printf就可以,这里的log是自定义的Logger对象(已经在Run函数中创建),路径在New方法中指定了,这时就会把日志信息写入所指向的文件中
运行日志服务
上述编写的server.go程序作为日志系统的后端,下面编写一段代码,将服务集中化管理,能够集中启动这些服务
创建一个service目录,并在其中编写service.go文件:
package service
import (
"context"
"fmt"
"log"
"net/http"
)
func Start(ctx context.Context, serviceName, host, port string,
registerHandlerFunc func()) (context.Context, error) {
registerHandlerFunc() // 注册请求处理函数
ctx = startService(ctx, serviceName, host, port) // 启动服务
return ctx, nil
}
func startService(ctx context.Context, serviceName, host, port string) context.Context {
ctx, cancel := context.WithCancel(ctx)
var srv http.Server
srv.Addr = ":" + port
go func() {
log.Println(srv.ListenAndServe()) // 监听HTTP请求 调用ServeHTTP方法
cancel()
}()
go func() {
fmt.Printf("%v started. Press any key to stop. \n", serviceName)
var s string
fmt.Scanln(&s)
srv.Shutdown(ctx)
cancel()
}()
return ctx
}
这段代码定义了一个Start函数,接收context接口,服务名称,地址,端口号,作用是启动指定的服务,先调用了registerHandleFunc注册请求处理函数(服务程序要有一个处理函数,来处理到来的HTTP请求),随后调用了startService函数,结束时返回context和nil
在startService函数中调用了WithCancel,返回一个context子节点和一个取消函数,用于触发取消信号,之后调一个goroutine启动HTTP服务器监听在指定的端口,处理到来的HTTP请求,当用户输入按下任意建就会触发Shutdown方法关闭服务器,并且调用取消函数撤销掉context:
func startService(ctx context.Context, serviceName, host, port string) context.Context {
ctx, cancel := context.WithCancel(ctx)
var srv http.Server
srv.Addr = ":" + port
go func() {
log.Println(srv.ListenAndServe())
cancel()
}()
go func() {
fmt.Printf("%v started. Press any key to stop. \n", serviceName)
var s string
fmt.Scanln(&s)
srv.Shutdown(ctx) // 关闭服务器
cancel() // 取消context
}()
return ctx
}
创建一个cmd文件夹,编写main.go作为整个程序的入口:
package main
import (
"context"
"distributed/log"
"distributed/service"
"fmt"
stdlog "log"
)
func main() {
log.Run("./distributed.log") // 指定日志文件路径
host, port := "localhost", "4000" // 指定地址和端口号
ctx, err := service.Start(context.Background(),
"Log Service",
host, port,
log.RegisterHandlers,
)
if err != nil {
stdlog.Fatalln(err)
}
<-ctx.Done()
fmt.Println("Shutting down log service.")
}
这段代码作为入口,启动这个服务
当取消函数(cancel)被调用后,ctx.Done就不会被阻塞,往下执行完整个程序
测试日志服务
使用Postman发出POST请求用于测试,如下所示:
发送请求后,查看日志文件:
$ cat distributed.log
go 2022/08/22 19:34:08 just for test
日志被记录了下来
服务注册逻辑
下面才正式实现注册中心服务注册的功能,应当实现服务注册的接口,这样客户端能够通过这个接口
创建registry文件夹,编写如下的registration.go文件:
package registry
type Registration struct {
ServiceName ServiceName
ServiceURL string
}
type ServiceName string
const (
LogService = ServiceName("LogService")
)
如上定义的Registration结构体代表被注册的服务,将目前存在服务定义为常量,目前只有一个日志服务LogService
在这个文件夹下再编写一个server.go,包含服务注册的主要逻辑:
package registry
import (
"encoding/json"
"log"
"net/http"
"sync"
)
const ServerPort = ":3000" // 端口
const ServicesURL = "http://localhost" + ServerPort + "/services" // 查询服务的URL
type registry struct {
registrations []Registration // 切片
mutex *sync.Mutex // 互斥锁
}
func (r *registry) add(reg Registration) error {
r.mutex.Lock()
r.registrations = append(r.registrations, reg)
r.mutex.Unlock()
return nil
}
var reg = registry{
registrations: make([]Registration, 0),
mutex: new(sync.Mutex),
}
type Service struct{}
func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Println("Request received")
switch r.Method {
case http.MethodPost:
dec := json.NewDecoder(r.Body)
var r Registration
err := dec.Decode(&r)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
log.Printf("Adding service: %v with URL: %s\n", r.ServiceName, r.ServiceURL)
err = reg.add(r)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
定义了一个结构体registry,包含了两个成员: 一个切片,可以看成是一系列服务的集合,和一个互斥锁,用于并发控制,之后将其创建
随后定义的add函数作用是注册,在上锁的情况下向集合添加元素,接着定义ServeHTTP函数,只接收POST请求,解析JSON数据,将其中服务名称通过add函数添加到上述创建的Registration结构中的集合中,这就意味着成功注册了一个服务
独立运行服务注册
将上述编写的服务注册的程序独立运行起来,将启动日志服务的main.go单独丢进一个cmd下的logservice文件夹,在cmd下创建一个registryservice文件夹,编写对应的main.go文件:
package main
import (
"context"
"distributed/registry"
"fmt"
"log"
"net/http"
)
func main() {
http.Handle("/services", ®istry.Service{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var srv http.Server
srv.Addr = registry.ServerPort
go func() {
log.Println(srv.ListenAndServe())
var s string
fmt.Scanln(&s)
srv.Shutdown(ctx)
cancel()
}()
<-ctx.Done()
fmt.Println("Shutting down registry service")
}
这段代码的作用就是将服务注册程序给运行起来,registry.Service已经实现了ServeHTTP接口方法,因此可以直接将这个结构体变量传给http.Handle函数
这段代码与上述service.go有点相似,在用户输入任意字符后中止掉程序
注册一个服务
封装一个函数向服务端发送POST请求来注册一个服务,在registry下编写一个client.go文件:
package registry
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func RegisterService(r Registration) error {
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
err := enc.Encode(r)
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
}
上述代码中定义了一个RegisterService函数,向URL发送POST请求来注册服务,请求中包括了服务名称和服务的URL
接下来要修改之前的代码,因为发送的数据是Registration结构体类型的数据,而上述编写的服务注册的服务端须要更改参数类型,修改service/service.go文件,这里在Start函数中加上Registration结构体类型的参数,serviceName这个参数可以删掉,并且启动这个服务时应该进行注册,所以在该函数中加上RegisterService函数:
func Start(ctx context.Context, host, port string, reg registry.Registration,
registerHandlerFunc func()) (context.Context, error) {
registerHandlerFunc()
ctx = startService(ctx, reg.ServiceName, host, port)
err := registry.RegisterService(reg) // 注册服务
if err != nil {
return ctx, err
}
return ctx, nil
}
同时一并修改startService函数的参数:
func startService(ctx context.Context, serviceName registry.ServiceName, host, port string) context.Context {
ctx, cancel := context.WithCancel(ctx)
var srv http.Server
srv.Addr = ":" + port
go func() {
log.Println(srv.ListenAndServe())
cancel()
}()
go func() {
fmt.Printf("%v started. Press any key to stop. \n", serviceName)
var s string
fmt.Scanln(&s)
srv.Shutdown(ctx)
cancel()
}()
return ctx
}
最后要修改logservice中的main.go,因为函数已经被修改:
....
serviceAddress := fmt.Sprintf("http://%s:%s", host, port)
r := registry.Registration{
ServiceName: "Log Service",
ServiceURL: serviceAddress,
}
ctx, err := service.Start(context.Background(),
"Log Service",
host, port, r, log.RegisterHandlers,
)
....
接下来运行这个服务,先启动registryservice/main.go,启动服务注册的程序,在运行logservice/main.go,这时会先发送一条POST请求到服务注册的程序,这时服务注册程序收到这条请求,其中包含了服务名称和URL,服务注册程序将其添加到集合,视为注册了这个服务:
$ go run .
2022/08/23 13:40:24 Request received
2022/08/23 13:40:24 Adding service: Log Service with URL: http://localhost:4000
这时服务注册程序输出已经添加了日志服务
取消注册服务
有增就有减,对应地,有服务注册的功能就应该有取消注册的功能
在registry/server.go中添加remove函数,与add函数作用相反,作用是从集合中去除掉指定的url所在的registration:
func (r *registry) remove(url string) error {
for i := range reg.registrations {
if reg.registrations[i].ServiceURL == url {
r.mutex.Lock()
reg.registrations = append(reg.registrations[:i], r.registrations[i+1:]...)
r.mutex.Unlock()
return nil
}
}
return fmt.Errorf("service at URL %s not found", url)
}
使用for-range遍历reg中的切片,当遇到指定的URL时则将其去除
服务注册中,POST请求用于注册服务,那么将通过DELETE请求来取消服务,所以在ServeHTTP函数中添加一个针对DELETE请求的分支:
func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Println("Request received")
switch r.Method {
...
case http.MethodDelete:
payload, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
url := string(payload)
log.Printf("Removing servcice at URL: %s", url)
err = reg.remove(url)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
在这个服务端接收到DELETE请求后,取出其中body的URL,再调用remove函数进行删除
在registry/client.go中添加客户端用来取消服务的函数:
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
}
使用http.NewRequest来发送DELETE请求
在StartService函数中,当服务停止时,调用该shutdown函数向服务端发送DELETE请求取消掉这个服务
func startService(ctx context.Context, serviceName registry.ServiceName, host, port string) context.Context {
...
go func() {
log.Println(srv.ListenAndServe())
err := registry.ShutdownService(fmt.Sprintf("http://%s:%s", host, port))
if err != nil {
log.Println(err)
}
cancel()
}()
...
}
测试:
先启动服务注册程序,然后再启动日志服务,再按任意键关闭
日志服务:
$ go run .
Log Service started. Press any key to stop.
2022/08/23 16:45:44 http: Server closed
Shutting down log service.
服务注册程序:
$ go run .
2022/08/23 16:45:42 Request received
2022/08/23 16:45:42 Adding service: Log Service with URL: http://localhost:4000
2022/08/23 16:45:44 Request received
2022/08/23 16:45:44 Removing servcice at URL: http://localhost:4000
2022/08/23 16:45:44 Request received
2022/08/23 16:45:44 Removing servcice at URL: http://localhost:4000
服务发现
在此之前,先实现一个业务服务,功能是学生成绩管理,用户可以查询和增加学生的成绩信息
创建业务服务
创建一个grades文件夹,编写相关代码,如下是grades.go文件,包含了成绩信息管理的主要逻辑:
package grades
import (
"fmt"
"sync"
)
type Student struct {
ID int
FirstName string
LastName string
Grades []Grade
}
func (s Student) Average() float32 {
var result float32
for _, grade := range s.Grades {
result += grade.Score
}
return result / float32(len(s.Grades))
}
type Students []Student
var (
students Students
studentsMutex sync.Mutex
)
func (ss Students) GetByID(id int) (*Student, error) {
for i := range ss {
if ss[i].ID == id {
return &ss[i], nil
}
}
return nil, fmt.Errorf("student with ID %d not found", id)
}
type GradeType string
const (
GradeQuiz = GradeType("Quiz")
GradeTest = GradeType("Test")
GradeExam = GradeType("Exam")
)
type Grade struct {
Title string
Type GradeType
Score float32
}
定义了students结构体,表示了学生信息:
type Student struct {
ID int // 学生ID
FirstName string // 名
LastName string // 姓
Grades []Grade // 成绩
}
定义了grade结构体:
type Grade struct {
Title string // 名称
Type GradeType // 类别
Score float32 // 得分
}
GetByID用于根据ID来查询学生信息,Average用于求平均成绩
创建server.go文件,作为该服务的后端,之后会被services中的Start函数调用:
package grades
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
func RegisterHandlers() {
handler := new(studentsHandler)
http.Handle("/students", handler)
http.Handle("/students/", handler)
}
type studentsHandler struct{}
func (sh studentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pathSegments := strings.Split(r.URL.Path, "/")
switch len(pathSegments) {
case 2:
sh.getAll(w, r)
case 3:
id, err := strconv.Atoi(pathSegments[2])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
sh.getOne(w, r, id)
case 4:
id, err := strconv.Atoi(pathSegments[2])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
sh.addGrade(w, r, id)
default:
w.WriteHeader(http.StatusNotFound)
}
}
func (sh studentsHandler) getAll(w http.ResponseWriter, r *http.Request) {
studentsMutex.Lock()
defer studentsMutex.Unlock()
data, err := sh.toJSON(students)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(data) // 写回数据
}
func (sh studentsHandler) toJSON(obj interface{}) ([]byte, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
err := enc.Encode(obj)
if err != nil {
return nil, fmt.Errorf("failed to serialize students: %q", err)
}
return b.Bytes(), nil
}
func (sh studentsHandler) getOne(w http.ResponseWriter, r *http.Request, id int) {
studentsMutex.Lock()
defer studentsMutex.Unlock()
student, err := students.GetByID(id)
if err != nil {
w.WriteHeader(http.StatusNotFound)
log.Println(err)
return
}
data, err := sh.toJSON(student)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Failed to serialize student: %q\n", err)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(data)
}
func (sh studentsHandler) addGrade(w http.ResponseWriter, r *http.Request, id int) {
studentsMutex.Lock()
defer studentsMutex.Unlock()
student, err := students.GetByID(id)
if err != nil {
w.WriteHeader(http.StatusNotFound)
log.Println(err)
return
}
var g Grade
dec := json.NewDecoder(r.Body)
err = dec.Decode(&g)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println(err)
return
}
student.Grades = append(student.Grades, g) // 添加数据
w.WriteHeader(http.StatusCreated)
data, err := sh.toJSON(g)
if err != nil {
log.Println(err)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(data)
}
使用RegisterHandlers注册http handler,路径指向URL下的students,会作为Start函数的参数
作为一个服务,同样要为结构体定义一个接口方法ServeHTTP,该方法要应对URL的几种情况:
/students
获得所有学生的成绩/students/{id}
获得指定ID的学生的信息/students/{id}/grades
获得增加学生的成绩
使用string.Split将URL路径切分为多段,使用switch-case结构根据分段的个数进行分支处理,段数为2、3和4分别对应了上述3种情况,分别调用getAll、getOne和addGrade函数,实现细节不再赘述
这里还要定义一个将结构体序列化为JSON数据的函数,接收一个空接口:
func (sh studentsHandler) toJSON(obj interface{}) ([]byte, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
err := enc.Encode(obj)
if err != nil {
return nil, fmt.Errorf("failed to serialize students: %q", err)
}
return b.Bytes(), nil
}
编写一个mockdata.go文件,制造一些数据:
package grades
func init() {
students = []Student{
{
ID: 1,
FirstName: "Nick",
LastName: "Carter",
Grades: []Grade{
{
"Quiz 1",
GradeQuiz,
85,
},
{
"Final Exam",
GradeExam,
94,
},
{
"Quiz 2",
GradeQuiz,
97,
},
},
},
{
ID: 2,
FirstName: "Jack",
LastName: "Bright",
Grades: []Grade{
{
"Final Exam",
GradeExam,
100,
},
{
"Quiz 2",
GradeQuiz,
80,
},
{
"Test 1",
GradeTest,
99,
},
},
},
}
}
这样一个业务服务就完成了,添加到registry/registrantion.go中:
const (
LogService = ServiceName("LogService")
GradingService = ServiceName("GradingService") // 业务服务
)
和日志服务相似,创建cmd/gradingservice/main.go文件,编写启动代码:
package main
import (
"context"
"distributed/log"
"distributed/registry"
"distributed/service"
"fmt"
stdlog "log"
)
func main() {
log.Run("./distributed.log")
host, port := "localhost", "6000"
serviceAddress := fmt.Sprintf("http://%s:%s", host, port)
r := registry.Registration{
ServiceName: registry.GradingService,
ServiceURL: serviceAddress,
}
ctx, err := service.Start(context.Background(),
host, port, r, log.RegisterHandlers,
)
if err != nil {
stdlog.Fatalln(err)
}
<-ctx.Done()
fmt.Println("Shutting down grading service.")
}
服务注册到这里就基本实现了,如下是项目结构:
.
├── cmd
│ ├── gradingservice
│ │ └── main.go
│ ├── logservice
│ │ └── main.go
│ └── registryservice
│ └── main.go
├── log
│ └── server.go
├── registry
│ ├── client.go
│ ├── registration.go
│ └── server.go
└── service
└── service.go
实现服务发现
完成了上述的业务服务gradingservice后,要使其能够请求logservice
接下来编辑registry/registration.go文件,扩展Registration结构体:
type Registration struct {
ServiceName ServiceName
ServiceURL string
RequiredServices []ServiceName
ServiceUpdateURL string
}
RequiredServices表示了该服务所依赖的服务名称,ServiceUpdateURL用于动态接收更新,比如注册中心就通过这个URL告诉这个服务,这里有一个logservice
创建两个结构体:
type patchEntry struct {
Name ServiceName
URL string
}
该结构体表示单条更新条目
type patch struct {
Added []patchEntry
Removed []patchEntry
}
该结构体记录增加和减少的条目
编写registry/server.go,扩展注册中心的后端,先扩展add函数,在添加服务时就添加为其依赖服务:
func (r *registry) add(reg Registration) error {
r.mutex.Lock()
r.registrations = append(r.registrations, reg)
r.mutex.Unlock()
err := r.sendRequiredServices(reg)
if err != nil {
return err
}
return nil
}
调用了一个sendRequiredServices函数,功能是添加依赖,发送一个请求,将所要依赖的服务给请求过来,方法实现如下:
func (r *registry) sendRequiredServices(reg Registration) error {
r.mutex.RLock()
defer r.mutex.RUnlock()
var p patch
for _, serviceReg := range r.registrations {
for _, reqService := range reg.RequiredServices {
if serviceReg.ServiceName == reqService {
p.Added = append(p.Added, patchEntry{
Name: serviceReg.ServiceName,
URL: serviceReg.ServiceURL,
})
}
}
}
err := r.sendPatch(p, reg.ServiceUpdateURL)
return err
}
上述函数中,循环遍历已经注册的服务,如果找到所依赖的服务,则添加到切片里,稍后调用sendPatch发送出去,实现如下:
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))
return err
}
该函数先将patch结构序列化为一个JSON数据,然后放在POST请求中发送
每个客户端的服务都有所依赖的服务,要向注册中心请求这些服务,得存储这些请求的服务,比如gradingservice就要依赖logservice
定义一个结构providers:
type providers struct {
services map[ServiceName][]string
mutex *sync.RWMutex
}
其中储存了服务的提供者,定义了一个map结构,和一个读写锁
初始化这个结构体变量:
var prov = providers{
services: make(map[ServiceName][]string),
mutex: new(sync.RWMutex),
}
定义对于的更新方法,用于更新这个结构中的数据:
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 providerURLs, ok := p.services[patchEntry.Name]; ok {
for i := range providerURLs {
if providerURLs[i] == patchEntry.URL {
p.services[patchEntry.Name] = append(providerURLs[:i], providerURLs[i+1:]...)
}
}
}
}
}
先遍历Added,也就是patch要增加的服务,如果added里的服务在providers中还不存在,则创建这个service,然后将这个服务和对应URL写入map,同理,接着遍历Removed,已有的服务就将其删除
接着定义了一个get函数,通过服务名称找到对应的URL:
func (p *providers) get(name ServiceName) (string, error) {
providers, ok := p.services[name]
if !ok {
return "", fmt.Errorf("no providers avaliable for service %v", name)
}
idx := int(rand.Float32() * float32(len(providers)))
return providers[idx], nil
}
func GetProvider(name ServiceName) (string, error) {
return prov.get(name)
}
目前providers只有logservice
接着再将ServiceUpdateURL绑定到一个handler上,从而能够对其进行处理:
func RegisterService(r Registration) error {
serviceUpdateURL, err := url.Parse(r.ServiceUpdateURL)
if err != nil {
return err
}
http.Handle(serviceUpdateURL.Path, &serviceUpdateHandler{})
....
}
定义接口方法:
type serviceUpdateHandler struct{}
func (suh 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.StatusBadGateway)
}
prov.Update(p)
}
ServeHTTP中接收POST请求,将Body到数据反序列化后传入Update方法再调用
因为经过了一些扩展,所以要修改Registration,增加所要依赖的服务和URL字段
type Registration struct {
ServiceName ServiceName
ServiceURL string
RequiredServices []ServiceName
ServiceUpdateURL string
}
下面优化一下logservice,目前日志服务只有一个服务端,要让客户端能够方便地使用提供的logservice服务,在log下再编写一个client.go文件:
package log
import (
"bytes"
"distributed/registry"
"fmt"
stdlog "log"
"net/http"
)
func SetClientLogger(serviceURL string, clientService registry.ServiceName) {
stdlog.SetPrefix(fmt.Sprintf("[%v] - ", clientService))
stdlog.SetFlags(0) // 不设置flag
stdlog.SetOutput(&clientLogger{serviceURL})
}
type clientLogger struct {
url string
}
func (cl clientLogger) Write(data []byte) (n int, err 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. ")
}
return len(data), nil
}
SetClientLogger负责设置客户端日志打印的相关的属性,因为要为clientLogger实现io.Writer接口,所以下面为其实现一个Write方法
扩展gradingservice/main.go中结构体的定义:
r := registry.Registration{
ServiceName: registry.GradingService,
ServiceURL: serviceAddress,
RequiredServices: []registry.ServiceName{registry.LogService},
ServiceUpdateURL: serviceAddress + "/services",
}
logservice同时接着还要获取Provider,即获取提供日志的服务的名称,这便是服务发现
if logProvider, err := registry.GetProvider(registry.LogService); err == nil {
fmt.Printf("Logging service found at: %s\n", logProvider)
log.SetClientLogger(logProvider, r.ServiceName)
}
运行测试:
先运行注册中心
$ go run .
2022/08/24 13:03:05 Request received
2022/08/24 13:03:05 Adding service: LogService with URL: http://localhost:4000
2022/08/24 13:03:21 Request received
2022/08/24 13:03:21 Adding service: GradingService with URL: http://localhost:6000
运行日志服务
$ go run .
LogService started. Press any key to stop.
运行gradding服务
$ go run .
GradingService started. Press any key to stop.
Logging service found at: http://localhost:4000
服务更新
对于一个服务,当所依赖的服务发生变化时,应当发出通知,例如logservice,当这个服务停止时,应该发出一条通知来告知gradingservice,启动时也应当发出一条通知,在上面的实现中,gradingservice只能在启动时发现logservice,并且logservice要先于gradingservice启动
在服务启动时就应该进行通知,所以在add函数中添加一个通知函数:
func (r *registry) add(reg Registration) error {
....
err := r.sendRequiredServices(reg)
r.notify(patch{
Added: []patchEntry{
{
Name: reg.ServiceName,
URL: reg.ServiceURL,
},
},
})
return err
}
通知函数的实现:
func (r *registry) notify(fullPatch patch) {
r.mutex.RLock()
defer r.mutex.RUnlock()
for _, reg := range r.registrations {
go func(reg Registration) {
for _, reqService := range reg.RequiredServices {
p := patch{
Added: []patchEntry{},
Removed: []patchEntry{},
}
sendUpdate := false
for _, added := range fullPatch.Added {
if added.Name == reqService {
p.Added = append(p.Added, added)
sendUpdate = true
}
}
for _, removed := range fullPatch.Removed {
if removed.Name == reqService {
p.Removed = append(p.Removed, removed)
sendUpdate = true
}
}
if sendUpdate {
err := r.sendPatch(p, reg.ServiceUpdateURL) // 发送patch
if err != nil {
log.Println(err)
return
}
}
}
}(reg)
}
}
这个notify函数接收一个patch变量作为参数,其中包含的added和removed,对应增加和移除的服务,函数体中要做的事就是先遍历所有的服务,然后遍历其所有的依赖服务,根据传入的fullPatch和added和removed,如果依赖服务是要增加或移除的,则将sendUpdate标志设置为true,产生patch并发送,进行更新,这些流程使用goroutine来并发地进行
在registry/client.go的ServeHTTP函数中增加一句提示信息,表示接收到了更新:
func (suh serviceUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
....
fmt.Printf("Updated received %v\n", p)
prov.Update(p)
}
对remove函数同样要进行扩展:
func (r *registry) remove(url string) error {
for i := range reg.registrations {
if reg.registrations[i].ServiceURL == url {
r.notify(patch{
Removed: []patchEntry{
{
Name: r.registrations[i].ServiceName,
URL: r.registrations[i].ServiceURL,
},
},
})
...
return fmt.Errorf("service at URL %s not found", url)
}
运行测试: 先运行注册中心,然后运行logservice,然后运行gradingservice,之后关闭logservice再启动
注册中心:
$ go run .
2022/08/24 18:03:45 Request received
2022/08/24 18:03:45 Adding service: LogService with URL: http://localhost:4000
2022/08/24 18:03:50 Request received
2022/08/24 18:03:50 Adding service: GradingService with URL: http://localhost:6000
2022/08/24 18:03:53 Request received
2022/08/24 18:03:53 Removing servcice at URL: http://localhost:4000
2022/08/24 18:03:53 Request received
2022/08/24 18:03:53 Removing servcice at URL: http://localhost:4000
2022/08/24 18:04:12 Request received
2022/08/24 18:04:12 Adding service: LogService with URL: http://localhost:4000
日志服务:
$ go run .
LogService started. Press any key to stop.
Updated received {[] []}
2022/08/24 18:03:53 http: Server closed
Shutting down log service.
$ go run .
LogService started. Press any key to stop.
Updated received {[] []}
grading服务:
$ go run .
GradingService started. Press any key to stop.
Updated received {[{LogService http://localhost:4000}] []}
Logging service found at: http://localhost:4000
Updated received {[] [{LogService http://localhost:4000}]}
Updated received {[{LogService http://localhost:4000}] []}
服务状态监控
检查所有服务的健康状况,方法是发送心跳请求,得知服务是否能正常响应
在Registration中加一条HeartbeatURL:
type Registration struct {
ServiceName ServiceName
ServiceURL string
RequiredServices []ServiceName
ServiceUpdateURL string
HeartbeatURL string
}
增加心跳检查函数:
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()
success := true
for attemps := 0; attemps < 3; attemps++ {
res, err := http.Get(reg.HeartbeatURL)
if err != nil {
log.Println(err)
} else if res.StatusCode == http.StatusOK {
log.Printf("Heartbeat check passed for %v", reg.ServiceName)
if !success {
r.add(reg)
}
break
}
log.Printf("Heartbeat check failed for %v", reg.ServiceName)
if success {
success = false
r.remove(reg.ServiceURL)
}
}
}(reg)
wg.Wait()
time.Sleep(freq)
}
}
}
上述代码中用到了waitgroup和goroutine,对每个服务的HeartbeatURL间隔freq的时间发起get请求,连续发送三次,如果有一次失败(状态码不等于200),那么success为false,那么会调用remove来移除服务,如果success为false又得到了正常的相应,那么又会调用add将服务加回来
上述这段代码将会在如下函数中调用,将其定义在registry/server.go中,每隔3秒检查一次服务健康状况:
var once sync.Once
func SetupRegistryService() {
once.Do(func() {
go reg.heartbeat(3 * time.Second)
})
}
与此同时,所有的客户端都应该知道如何相应这种心跳请求:
func RegisterService(r Registration) error {
heartbeatURL, err := url.Parse(r.HeartbeatURL)
if err != nil {
return err
}
http.HandleFunc(heartbeatURL.Path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
....
}
在服务注册时,就在心跳URL注册一个handler,处理心跳请求,最后为logservice和gradingservice都加上心跳URL:
r := registry.Registration{
ServiceName: registry.LogService,
ServiceURL: serviceAddress,
RequiredServices: make([]registry.ServiceName, 0),
ServiceUpdateURL: serviceAddress + "/services",
HeartbeatURL: serviceAddress + "/heartbeat",
}
测试
启动注册中心和两个服务,中途关掉logservice
注册中心的输出:
2022/08/24 19:12:24 Adding service: LogService with URL: http://localhost:4000
2022/08/24 19:12:24 Heartbeat check passed for LogService
2022/08/24 19:12:26 Request received
2022/08/24 19:12:26 Adding service: GradingService with URL: http://localhost:6000
2022/08/24 19:12:27 Heartbeat check passed for LogService
2022/08/24 19:12:30 Heartbeat check passed for GradingService
2022/08/24 19:12:33 Heartbeat check passed for LogService
2022/08/24 19:12:36 Heartbeat check passed for GradingService
2022/08/24 19:12:36 Request received
2022/08/24 19:12:36 Removing servcice at URL: http://localhost:4000
2022/08/24 19:12:36 Request received
2022/08/24 19:12:36 Removing servcice at URL: http://localhost:4000
2022/08/24 19:12:39 Heartbeat check passed for GradingService
2022/08/24 19:12:42 Heartbeat check passed for GradingService
2022/08/24 19:12:45 Heartbeat check passed for GradingService