视频:
Go语言编写简单分布式系统(完结)_哔哩哔哩_bilibili,作者:杨旭,非常感谢,大佬真牛批
参考笔记及代码:
Go语言实现简单分布式系统 - N3ptune - 博客园 (cnblogs.com)
整体框图:
log服务、grading服务、以及portal服务需要注册到服务注册中心Registry。grading服务依赖于log服务,portal服务依赖于log服务和grading服务,因此注册中心Registry需要将服务所依赖服务通知给该服务。
数据结构:
注册中心需要有一个列表(切片)保存已经注册的服务信息,实际应用中可使用数据库持久化保存数据。这些服务信息当前为一个结构体,包括服务名称、服务地址ServiceURL、所依赖服务列表(可以理解为订阅的服务),当依赖服务更新时所通知地址ServiceUpdateURL,以及健康检查地址HeartbeatURL。
服务注册:
Registry设置一个handler,当接收到post请求时,将待注册服务的信息保存起来,当收到delete请求时,将服务信息删除。
在其它服务启动时,首先设置三个handler,对应ServiceURL、ServiceUpdateURL、HeartbeatURL三个地址,并启动监听,之后便可以进行服务注册,即向Registry发送一个post请求;而当服务结束运行前,向Registry发送delete请求,取消注册。
服务发现:
Registry在接收到一个注册请求时(假设待注册服务为A),做两件事:一、找到当前已经注册服务中,哪些服务是A所需要的,将这些服务的信息通过A的ServiceUpdateURL告诉A;二、找到已经注册的服务中,哪些服务是依赖于A的,将A的信息通过这些服务的ServiceUpdateURL进行通知。而在收到delete请求时,只做一件事,就是对依赖这个服务的服务进行通知。
其它服务收到通知后,即在ServiceUpdateURL对应的handler里面,保存或者删除所依赖服务的信息,视频里称其为provider,为一个map数据结构,key为依赖的服务名,value为服务的地址列表(一般依赖的服务不止一个地址)。使用依赖的服务时,从map里取出地址即可。
健康检查:
Registry里面每隔一段时间,对已经注册服务的健康度进行检查,即向服务的HeartbeatURL发送一个get请求。
而在其它服务的HeartbeatURL对应的handler里面,只需要简单返回一个OK即可。
各个服务/组件及其代码实现:
注册中心的服务代码:
Registry是注册中心,包含一个已注册服务的数组,由于会有并发访问问题,将其与mutex绑定为一个结构体。根据method执行不同的函数,包括add函数和delete函数。
当接收到post请求时,执行add函数,将服务名和地址添加进数组里(服务注册),找到已经注册的服务中被当前服务所依赖的服务,将这些服务的信息告诉当前服务,同时找到依赖当前服务的服务,依次进行通知(服务发现)
当接收到delete请求时,执行remove函数,根据传入的url在数组里找到该服务并删除(取消注册),通知找到依赖当前取消注册的服务的服务,依次通知该服务已经取消注册。
注册中心的客户使用代码:
为了方便其它服务使用注册中心,设置了一个client.go文件,里面提供了一个注册服务函数、一个取消注册函数、以及一个getProviders函数供其它服务使用。也就是说,其它服务使用Registry是通过调用这三个函数实现的。
注册服务函数RegisterService:由于每个服务的ServiceUpdateURL和HeartbeatURL对应的handler要做的事情是一样的,因此将这些handler函数及其注册(http.Handle)都放到注册服务函数里了,然后向Registry发送post请求完成服务注册。
取消注册函数UnregisterService:即向Registry发送删除的delete请求,取消注册服务
getProviders函数:在ServiceUpdateURL对应的handler里会对Provider进行更新(这是一个map结构,保存了服务发现的结果),当客户需要使用所依赖服务时,传入服务名即可获得服务地址。注意这是客户代码,每个客户的Provider都是不一样的。这里的get并不是使用时去发送网络请求问注册中心,而是注册中心知道当前服务需要依赖什么服务并发现可用时主动告诉当前服务的(类似发布订阅模式),也就是说这里的Provider是缓存在当前服务本地的。
services通用包:
对于其它服务,每个服务的生命流程是类似的,即设置handler,启动监听、服务注册,当服务挂掉时取消注册。因此抽象出了这么一个包,里面仅包含一个start函数,它调用了服务传进来的RegisterHandlers函数设置服务的handlers并启动监听,然后调用注册中心的客户函数RegisterService对服务进行注册,然后设置一个go协程用于服务结束时调用注册中心的客户函数UnregisterService。
对于Registry,启动流程也是类似的,但不用自己注册自己,因此不能使用services通用包。
log服务:
服务端首先重定向了log的输出位置为一个文件;然后设置一个handler,收到post请求时直接调用log.pritnf,这时候由于重定向了log的输出,实际上就写到了文件里面了。其实个人感觉不做重定向也可以,就操作文件就好了。该服务的启动只需设置好5个服务信息,然后调用services通用包的start函数即可。
另外,为了方便其它服务使用log服务,同样也提供了client.go文件,实现了SetClientLogger函数,里面将log进行重定向。log的重定向输出是通过重写Write函数实现的,这里实现的Write函数就是向log服务发送post请求。在重定向后,使用者每次log.printf()就会自动向log服务发送post请求,提高了使用的便利性。
需要注意这里的两个重定向,一个发生在服务端(重定向至文件),一个发生在服务调用者处(自动向log服务发送http请求)。
grading服务:
业务内容略去。该服务也是通过services通用包启动的。由于其依赖log服务,在启动后(services通用包的start函数返回后),调用getproviders函数获得log服务的地址,然后调用SetClientLogger函数设置使用log服务。
文章结束。