做网站怎么与客户谈判,简述网站设计基本流程,水果香精东莞网站建设技术支持,商城网上购物分布式
传统开发方式的痛点#xff1a;
我们的服务分为很多种#xff1a;用户服务、商品服务、订单服务等#xff0c;若我们一个成熟的体系内#xff0c;新添加一个服务#xff0c;会变得十分的繁琐与困难
当我们的负载较大时#xff0c;如果选择添加机器的方式来减轻…分布式
传统开发方式的痛点
我们的服务分为很多种用户服务、商品服务、订单服务等若我们一个成熟的体系内新添加一个服务会变得十分的繁琐与困难
当我们的负载较大时如果选择添加机器的方式来减轻负载那么增加的机器需要修改很多配置文件甚至代码这样的重新部署会导致一系列的问题。
此时选用注册中心即可对这些问题有系统性的解决
用户服务-web、用户服务-srv、商品服务-web、商品服务1-srv、商品服务2-srv… 都需要注册到注册中心中也就是说具有一个注册中心其管控着所有的服务
也就是说当我们的一个服务要调用另一个服务时其会首先到注册中心拉取对应的服务信息再通过从注册中心获取的信息来访问对应的服务
注册中心技术选型
优点缺点接口一致性算法zookeeper1. 功能强大2. watcher 机制实时获取服务提供者的状态3. dubbo 等框架支持1. 没有健康检查2. 需要在服务中集成sdk复杂度高3. 不支持多数据中心sdkPaxosconsul1. 简单易用不需要集成sdk2. 自带健康检查3. 支持多数据中心4. 提供web管理界面不能实时获取服务变化http/dnsRaftetcd1. 简单易用不需要集成sdk2. 可配置性强1. 没有健康检查2. 配合第三方工具一起完成服务发现3. 不支持多数据中心httpRaft
由于这里使用golang 进行开发最贴合的还是 consul 作为注册中心最为强大也可选用 Nacos
consul
consul 的资料可以在 github 上找到
docker 拉取 consul
docker run -d \
-p 8500:8500 \
-p 8300:8300 \
-p 8301:8301 \
-p 8302:8302 \
-p 8600:8600/udp \
consul consul agent -dev -client0.0.0.0使用这种命令的话默认访问8500就是consul 的http端口、8600就是consul 的dns端口
// TODO : 1-2 5min 左右 consul 支持其作为 DNS 服务器 其可以将传过来的域名解析为对应的IP地址再进行进一步访问
consul
consul 是一个强大的服务注册中心其可以同时作为服务注册中心和 DNS 地址解析服务器在这种情况下consul 还提供服务健康检查的机制但与 Nacos 不同的是consul 的服务注册不是直接进行发现的而是要通过发送请求进行配置我们要发送的请求如下
PUT
http://192.168.202.140:8500/v1/agent/service/register
Body - json
{Name: mxshop-web,ID: mxshop-web,Tags: [web, mxshop, xxx, 笑死我啦哈哈哈],Address: 127.0.0.1,Port: 50051
}之后就可以在 consul 的控制台中看到我们注册的内容了但要注意通过这种方式的注册是没有健康监测功能的如果我们需要健康监测功能需要在请求中添加额外的参数
服务注销接口
PUT
http://192.168.202.140:8500/v1/agent/service/deregister/mxshop-web这里的请求最后一个位置要填写我们注册的服务的 id
对于每一个服务来讲都需要将其注册到注册中心而只有被调用的环节只需要进行服务注册但还调用其他模块的服务还需要配置服务发现功能
consul 的 go 语言集成
go 语言集成 consul 测试在任意一个地方创建一个 go 文件
package mainimport github.com/hashicorp/consul/apifunc Register(address string, port int, name string, tags []string, id string) error {cfg : api.DefaultConfig()cfg.Address 192.168.202.140:8500 // consul 的地址client, err : api.NewClient(cfg)if err ! nil {panic(err)}// 生成 consul 的注册对象// 配置基础信息registration : new(api.AgentServiceRegistration)registration.Name nameregistration.ID idregistration.Tags tagsregistration.Port portregistration.Address address// 配置检查对象也就是健康检查机制check : api.AgentServiceCheck{HTTP: http://192.168.10.48:8021/health, // 发送 GET 请求来进行健康检查服务的地址Timeout: 5s, // 每次健康检查中多久没有回复视为健康检查失败Interval: 5s, // 进行健康检查的频率DeregisterCriticalServiceAfter: 10s, // 不健康服务允许存活的时间当一个服务被检查为不健康时若 10s 内其没有转为健康则将其从服务中删除}// 将检查对象配置进 consul 的注册对象 registration 中registration.Check check// 将配置的 consul 注册进去err client.Agent().ServiceRegister(registration)if err ! nil {panic(err)}return nil}func main() {_ Register(192.168.10.48, 8021, user-web, []string{testtt}, user-web)
}
按照如上机制就可以将我们的服务注册进去
consul 获取服务节点信息
下面是一个获取 consul 中所有服务节点内容的示例
package mainimport (fmtgithub.com/hashicorp/consul/api
)func AllServices() {cfg : api.DefaultConfig()cfg.Address 192.168.202.140:8500client, err : api.NewClient(cfg)if err ! nil {panic(err)}// 获取所有的服务内容data, err : client.Agent().Services()if err ! nil {panic(err)}for key, _ : range data {fmt.Println(key)}
}func main() {AllServices()
}
这样子获取到的是所有服务节点的名称
gulimail-user
gulimail-web
mxshop-user
mxshop-web进一步的
如果我们希望获取某些特定服务节点就需要用到 consul 提供的过滤器来进行操作
package mainimport (fmtgithub.com/hashicorp/consul/api
)func AllServices() {cfg : api.DefaultConfig()cfg.Address 192.168.202.140:8500client, err : api.NewClient(cfg)if err ! nil {panic(err)}// 获取全部 Services 名称严格等于 gulimail-web 的服务// 如果我们要获取 ID ... 就可以写 ID gulimail-userdata, err : client.Agent().ServicesWithFilter(Service gulimail-web)for key, _ : range data {fmt.Println(key)}
}func main() {AllServices()
}
输出为
gulimail-webconsul - GRPC 健康检查
由于 GRPC 不是以简单的 HTTP 协议进行传输数据的其 默认使用Proto进行数据传输这就导致其心跳机制不能简单的开启一个配置就完成而是应该配置其自己的 Proto 的规范
在 main.go 中添加如下依赖
// 引入如下包
google.golang.org/grpc/health/grpc_health_v1
google.golang.org/grpc/health...// 在 server 创建之后添加如下监听功能
// 绑定服务健康检查
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
在 config/config.go 中添加如下配置对象
主要是 ConsulConfig MysqlConfig 是现补的
package configtype MysqlConfig struct {Host string mapstructure:host json:hostPort int mapstructure:port json:portName string mapstructure:db json:dbUser string mapstructure:user json:userPassword string mapstructure:passord json:password
}type ConsulConfig struct {Host string mapstruce:host json:hostPort int mapstruct:port json:port
}type ServerConfig struct {MysqlInfo MysqlConfig mapstructure:mysql json:mysqlConsulInfo ConsulConfig mapstructure:consul json:consul
}之后添加 GRPC 服务 向 CONSUL 的添加和 状态检测机制下面是完整的 main.go 代码
package mainimport (flagfmtmxshop_srvs/user_srv/globalmxshop_srvs/user_srv/initializenetgithub.com/hashicorp/consul/apigoogle.golang.org/grpcgoogle.golang.org/grpc/healthgoogle.golang.org/grpc/health/grpc_health_v1mxshop_srvs/user_srv/handlermxshop_srvs/user_srv/proto
)func main() {// 由于ip和端口号有可能需要用户输入所以这里摘出来// flag 包是一个命令行工具包允许从命令行中设置参数IP : flag.String(ip, 0.0.0.0, ip地址)Port : flag.Int(port, 50051, 端口号)initialize.InitLogger()initialize.InitConfig()flag.Parse()fmt.Println(ip: , *IP)fmt.Println(port: , *Port)// *************************************************************************************// 从这里开始是 GRPC 的心跳检测和服务注册功能// 创建新服务器server : grpc.NewServer()// 注册自己的已实现的方法进来proto.RegisterUserServer(server, handler.UserServer{})//lis, err : net.Listen(tcp, fmt.Sprintf(192.168.202.140:8021))lis, err : net.Listen(tcp, fmt.Sprintf(%s:%d, *IP, *Port))if err ! nil {panic(failed to listen err.Error())}// 绑定服务健康检查grpc_health_v1.RegisterHealthServer(server, health.NewServer())// 服务注册cfg : api.DefaultConfig()cfg.Address fmt.Sprintf(%s:%d, global.ServerConfig.ConsulInfo.Host, global.ServerConfig.ConsulInfo.Port)client, err : api.NewClient(cfg)if err ! nil {panic(err)}check : api.AgentServiceCheck{GRPC: fmt.Sprintf(192.168.0.111:50051),Interval: 5s,DeregisterCriticalServiceAfter: 15s,}registration : new(api.AgentServiceRegistration)registration.Address 192.168.0.111 // 这里是自己服务的地址这里我写的是本机registration.ID global.ServerConfig.Nameregistration.Port *Portregistration.Tags []string{imooc, bobby, user, srv, 666}registration.Name global.ServerConfig.Nameregistration.Check checkerr client.Agent().ServiceRegister(registration)if err ! nil {panic(err)}// ******************************************************************************************// 将自己的服务绑定端口err server.Serve(lis)if err ! nil {panic(fail to start grpc err.Error())}
}
consul-服务发现
对于我们的 gin 服务来说更为重要的是服务发现的功能因为具备服务发现功能才能从consul中发现尚在服务中的服务
先进行配置
config-debug.yaml
consul:host: 192.168.202.140port: 8500
config.go
type ServerConfig struct {Name string mapstructure:namePort int32 mapstructure:portUserSrvInfo UserSrvConfig mapstructure:user_srvJWTInfo JWTConfig mapstructure:jwtAliSmsInfo AliSmsConfig mapstructure:smsRedisInfo RedisConfig mapstructure:redisConsulInfo ConsulConfig mapstructure:consul
}type ConsulConfig struct {Host string mapstructure:hostPort string mapstructure:port
}之后我们就可以改写我们的服务让我们在拉取 grpc 服务时通过consul 进行拉取以实现服务发现
我们测试在 user-api 中进行服务发现的添加
import github.com/hashicorp/consul/api之后进行服务的发现
user.go
func GetUserList(ctx *gin.Context) {// 从注册中心获取用户信息cfg : api.DefaultConfig()consulInfo : global.ServerConfig.ConsulInfocfg.Address fmt.Sprintf(%s:%d, consulInfo.Host, consulInfo.Port)userSrvHost : userSrvPort : 0client, err : api.NewClient(cfg)if err ! nil {panic(err)}data, err : client.Agent().ServicesWithFilter(fmt.Sprintf(Service \%s\, global.ServerConfig.UserSrvInfo.Name))//data, err : client.Agent().ServicesWithFilter(fmt.Sprintf(Service %s, global.ServerConfig.UserSrvInfo.Name))if err ! nil {panic(err)}for _, value : range data {userSrvHost value.AddressuserSrvPort value.Port}if userSrvHost {ctx.JSON(http.StatusBadRequest, gin.H{msg: 用户服务不可达,})}//ip : 127.0.0.1//port : 50051// 拨号连接用户 GRPC 服务//userConn, err : grpc.Dial(fmt.Sprintf(%s:%d, global.ServerConfig.UserSrvInfo.Host, global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())// 引入consul后这个位置就不再是普通的了而是使用 Consul 中通过服务发现取出来的了userConn, err : grpc.Dial(fmt.Sprintf(%s:%d, userSrvHost, userSrvPort), grpc.WithInsecure())if err ! nil {zap.L().Error([GetUserList] 连接 【用户服务失败】,zap.String(msg, err.Error()))}// 生成 grpc 的 client 并调用接口userSrvClient : proto.NewUserClient(userConn)// 测试 ID 是否可以取到claims, _ : ctx.Get(claims)currentUser : claims.(*models.CustomClaims)zap.S().Infof(访问用户: %d, currentUser.ID)// 通过上下文 gin.Context 获取请求参数// 若能找到对应的请求参数则返回传入的请求参数若不存在则返回默认值pn : ctx.DefaultQuery(pn, 0)pnInt, _ : strconv.Atoi(pn)pSize : ctx.DefaultQuery(psize, 10)pSizeInt, _ : strconv.Atoi(pSize)rsp, err : userSrvClient.GetUserList(context.Background(), proto.PageInfo{Pn: uint32(pnInt),PSize: uint32(pSizeInt),})if err ! nil {zap.L().Error([GetUserList] 查询 用户列表失败)HandleGrpcErrorToHttp(err, ctx)return}// 构建请求结果result : make([]interface{}, 0)for _, value : range rsp.Data {//data : make(map[string]interface{}) // 创建一个 map//data[id] value.Id//data[name] value.NickName//data[birth] value.BirthDay//data[gender] value.Gender//data[mobile] value.Mobilevar user response.UserResponse{Id: value.Id,NickName: value.NickName,Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)),//Birthday: time.Time(time.Unix(int64(value.BirthDay), 0)).Format(2006-01-02),//Birthday: time.Time(time.Unix(Int64(value.BirthDay), 0)),Gender: value.Gender,Mobile: value.Mobile,}result append(result, user)}// 利用上下文的 JSON 转换返回结果在这里将结果返回给请求ctx.JSON(http.StatusOK, result)
}这是全部的 getUserList 接口的代码这里前面是通过consul来获取用户服务就可以直接进行开启了但注意这里需要配置 config-debug.yml
这里需要配置host 和 name
user_srv:host: 192.168.102.177port: 50051name: user-srv将consul 配置由拦截器全局变量实现
设置一个全局变量将这个全局变量配置进来以实现 consul 的功能
本质上我们是通过 consul 来实现找到 我们的 GRPC 服务并生成 userSrvClient 对象来进行后续对 GRPC 服务的调用的所以我们此时可以将 userSrvClient 定义为全局变量来实现一次定义、多处使用的效果。
在 global 中进行定义
// 全局变量
var (// 用于读取配置ServerConfig *config.ServerConfig config.ServerConfig{}// 用于进行错误处理Trans ut.Translator// 进行UserClient grpc 服务的生成UserSrvClient proto.UserClient
)在initialize 中创建 srv_conn.go 用于对GRPC 服务的连接初始化
func InitUserSrvConn() {// 从注册中心获取用户信息cfg : api.DefaultConfig()consulInfo : global.ServerConfig.ConsulInfocfg.Address fmt.Sprintf(%s:%d, consulInfo.Host, consulInfo.Port)userSrvHost : userSrvPort : 0client, err : api.NewClient(cfg)if err ! nil {panic(err)}data, err : client.Agent().ServicesWithFilter(fmt.Sprintf(Service \%s\, global.ServerConfig.UserSrvInfo.Name))//data, err : client.Agent().ServicesWithFilter(fmt.Sprintf(Service %s, global.ServerConfig.UserSrvInfo.Name))if err ! nil {panic(err)}for _, value : range data {userSrvHost value.AddressuserSrvPort value.Port}if userSrvHost {zap.S().Fatal([InitUserSrvConn] 用户服务无法获取 )}//ip : 127.0.0.1//port : 50051// 拨号连接用户 GRPC 服务//userConn, err : grpc.Dial(fmt.Sprintf(%s:%d, global.ServerConfig.UserSrvInfo.Host, global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())// 引入consul后这个位置就不再是普通的了而是使用 Consul 中通过服务发现取出来的了userConn, err : grpc.Dial(fmt.Sprintf(%s:%d, userSrvHost, userSrvPort), grpc.WithInsecure())if err ! nil {zap.L().Error([GetUserList] 连接 【用户服务失败】,zap.String(msg, err.Error()))}// 生成 grpc 的 client 并调用接口userSrvClient : proto.NewUserClient(userConn)global.UserSrvClient userSrvClient
}
并将其在 main.go 中进行调用这里的详细调用就不再做详细介绍只要在 initialize config 之后进行调用就可以
之后改造 对应的 GetUserList 接口进行尝试
func GetUserList(ctx *gin.Context) {userSrvClient : global.UserSrvClient// 测试 ID 是否可以取到claims, _ : ctx.Get(claims)currentUser : claims.(*models.CustomClaims)zap.S().Infof(访问用户: %d, currentUser.ID)// 通过上下文 gin.Context 获取请求参数// 若能找到对应的请求参数则返回传入的请求参数若不存在则返回默认值pn : ctx.DefaultQuery(pn, 0)pnInt, _ : strconv.Atoi(pn)pSize : ctx.DefaultQuery(psize, 10)pSizeInt, _ : strconv.Atoi(pSize)rsp, err : userSrvClient.GetUserList(context.Background(), proto.PageInfo{Pn: uint32(pnInt),PSize: uint32(pSizeInt),})if err ! nil {zap.L().Error([GetUserList] 查询 用户列表失败)HandleGrpcErrorToHttp(err, ctx)return}// 构建请求结果result : make([]interface{}, 0)for _, value : range rsp.Data {var user response.UserResponse{Id: value.Id,NickName: value.NickName,Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)),Gender: value.Gender,Mobile: value.Mobile,}result append(result, user)}// 利用上下文的 JSON 转换返回结果在这里将结果返回给请求ctx.JSON(http.StatusOK, result)
}但要注意的是
此时我们的服务正在运行但如果我们获取到的 GPC 服务如果下线了其也不会自动重新获取 GRPC服务或者改端口或IP了其都无法实现自动检错和修改我们的服务在一次启动时就进行了 TCP 的三次握手没有在每次功能调用时进行三次握手所以这样做的性能很高我们这样做仅仅实现了一条连接 但这个链接由多个 groutine 来实现我们可以考虑使用 GRPC 连接池来进行优化grpc-pool, 不过负载均衡同样可以解决这个问题。
负载均衡
端口分配
在服务的启动过程中每个服务都需要一个端口一个服务若需要启动多个实例也需要多个端口我们认为的去维护这个端口是一个比较复杂且不必要的情况故而我们可以考虑动态分配端口、端口也动态获取的情况可以让我们不再考虑端口带来的复杂情况。
我们创建一个 utils 目录在目录中创建一个 addr.go 工具用来获取端口
端口分配的核心逻辑
package utilsimport (net
)func GetFreePort() (int, error) {// 当指定端口号为 0 时操作系统会自动分配一个未被使用的端口给这个 TCP 地址addr, err : net.ResolveTCPAddr(tcp, localhost:0)if err ! nil {return 0, err}l, err : net.ListenTCP(tcp, addr)if err ! nil {return 0, err}defer l.Close()return l.Addr().(*net.TCPAddr).Port, nil
}
接着在 main 的位置进行设置设置为 生产环境自动获取开发环境固定 viper.AutomaticEnv()debug : viper.GetBool(MXSHOP-DEBUG)fmt.Println(debug)if debug {port, err : utils.GetFreePort()if err nil {global.ServerConfig.Port int32(port)}}在合适的位置添加如上代码来保证端口号的自动获取
在我们的 GPRC服务上也进行如上配置
GRPC 的 main.go
package mainimport (flagfmtmxshop_srvs/user_srv/globalmxshop_srvs/user_srv/initializemxshop_srvs/user_srv/utilsnetgithub.com/hashicorp/consul/apigoogle.golang.org/grpcgoogle.golang.org/grpc/healthgoogle.golang.org/grpc/health/grpc_health_v1mxshop_srvs/user_srv/handlermxshop_srvs/user_srv/proto
)func main() {// 由于ip和端口号有可能需要用户输入所以这里摘出来// flag 包是一个命令行工具包允许从命令行中设置参数IP : flag.String(ip, 0.0.0.0, ip地址)Port : flag.Int(port, 0, 端口号)initialize.InitLogger()initialize.InitConfig()flag.Parse()fmt.Println(ip: , *IP)// 设置端口号自动获取if *Port 0 {*Port, _ utils.GetFreePort()}fmt.Println(port: , *Port)// 创建新服务器server : grpc.NewServer()// 注册自己的已实现的方法进来proto.RegisterUserServer(server, handler.UserServer{})//lis, err : net.Listen(tcp, fmt.Sprintf(192.168.202.140:8021))lis, err : net.Listen(tcp, fmt.Sprintf(%s:%d, *IP, *Port))if err ! nil {panic(failed to listen err.Error())}// 绑定服务健康检查grpc_health_v1.RegisterHealthServer(server, health.NewServer())// 服务注册cfg : api.DefaultConfig()cfg.Address fmt.Sprintf(%s:%d, global.ServerConfig.ConsulInfo.Host, global.ServerConfig.ConsulInfo.Port)client, err : api.NewClient(cfg)if err ! nil {panic(err)}check : api.AgentServiceCheck{GRPC: fmt.Sprintf(192.168.102.177:%d, *Port),Interval: 5s,//Timeout: 10s,DeregisterCriticalServiceAfter: 30s,}registration : new(api.AgentServiceRegistration)registration.Address 192.168.102.177//registration.Address 127.0.0.1registration.ID global.ServerConfig.Nameregistration.Port *Portregistration.Tags []string{imooc, bobby, user, srv, 666}registration.Name global.ServerConfig.Nameregistration.Check checkerr client.Agent().ServiceRegister(registration)if err ! nil {panic(err)}//err server.Serve(lis)// 将自己的服务绑定端口err server.Serve(lis)if err ! nil {panic(fail to start grpc err.Error())}
}
负载均衡
负载均衡就是指我们在调用服务时如何选取合适服务的策略一个服务可能有很多的机器来分担压力这些压力的分担策略就是负载均衡策略。
对于 HTTP 服务其实 NGINX 就可以直接完成负载均衡的工作但是对于 GRPC 服务我们还需要进一步探究
另外的对于用户请求从网关发送到 GIN 服务的位置也需要进行负载均衡考虑
负载均衡的三种策略 集中式负载均衡 在用户调用和服务之间插入一个第三方软件或硬件所有的用户负载均衡都通过这种方式进入这种负载均衡策略有明显的劣势所有的流量都要经过这个第三方负载均衡器非常容易导致系统出现问题 进程内负载均衡 再 web 服务内启动一个 goroutine 在 gin 启动之前获取连接表并生成 TCP 连接在后续的调用中进行直接使用每一个web服务单独维护自己的负载均衡机制避免一个全服务的集中负载均衡器优点是避免了集中式负载均衡的集中问题缺点是需要每个web服务自己去实现一个自己独立的负载均衡机制提高了人员工作量。 独立负载均衡 在web服务的同一台机器上部署一个独立的负载均衡 LoadBalance 器这个独立的负载均衡器既规避了需要独立开发服务的问题也避免了需要将所有的流量都进行集中的问题但由于其在每个web服务的机器上都部署一个独立的负载均衡器其维护成本会偏高并且也需要独立编写 watchDog 机制来对负载均衡器的在线状态进行检测也是一种弊端较大的机制。
一般来说我们使用第二种进程内负载均衡的机制会比较多。
负载均衡的算法 轮询法 针对于每一个请求让他们依次按顺序访问服务 随机法 见名知意 源地址哈希法 对于同一个用户的服务将其进行 hash 运算生成一个固定的数对这个固定的数和服务数进行取模来选取机器这样的优点是每个用户访问的服务是固定的可以独立创建数据库大幅度降低服务压力但是如果我们要新增服务我们原来的数据就会全面失效但针对于这个问题还有一致性哈希可以解决这个问题。 加权轮询法 根据服务器的配置和负载生成一个权重根据权重对服务进行随机选择。 最小连接数法 最小连接数法会根据当前所有服务的现存积压连接数的多少来进行负载均衡负载均衡会根据当前积压连接数最少的服务进行分配。
基于 GRPC 的负载均衡和 Consul 的集成策略
GRPC 的负载均衡策略是基于第三方策略和内部策略两种形式的这两种形式都是可以被允许的只要在配置中进行配置就可。
另外对于GRPC 对于 CONSOLE 的负载均衡连接我们一般使用 grpc-console-resolver 组件对 console 内容进行拉取这个组件的作用是将console注册中心的信息拉取到服务中
注意这个包的使用可能不会真正引入到这个包的某一个变量但这个包还是必须要引入的因为引入这个包就是引入这个包里对于 Console连接的对应内容的 init 方法这个方法会帮助我们直接生成Console的相关信息。
注意在此处我们测试也将 proto 的整个文件夹复制过来进行测试
main.go
必须引入的包
_ github.com/mbobakov/grpc-consul-resolver全部的逻辑以及对于连接的尝试
package mainimport (GoTes/grpclb_test/protocontextfmt_ github.com/mbobakov/grpc-consul-resolvergoogle.golang.org/grpc
)func main() {// 注意这里是尝试连接的操作具体行为是第一个是 consul 的地址第一个标识是服务名后面的是连接等待时间tag是服务携带的标签的过滤波器注意这里的过滤是且的逻辑// grpc.WithDefaultServiceConfig({loadBalancingPolicy: round_robin}), 这段代码标注了负载均衡方式是 轮询conn, err : grpc.Dial(consul://192.168.202.140:8500/user-srv?wait14stagsrv,grpc.WithInsecure(),grpc.WithDefaultServiceConfig({loadBalancingPolicy: round_robin}),)if err ! nil {panic(err)}defer conn.Close()userSrvClient : proto.NewUserClient(conn)rsp, err : userSrvClient.GetUserList(context.Background(), proto.PageInfo{Pn: 1,PSize: 2,})if err ! nil {panic(err)}for index, data : range rsp.Data {fmt.Println(index, data)}}
注意此处的两个问题
我们的 user-srv 在启动的时候只启动了一个我们获取服务的时候也只会获取这一个无法体现出负载均衡的效果我们在终端中进行重复启动服务的时候会进行覆盖我们在进行consul 服务注册的时候相同的服务ID会覆盖前一个所以我们在进行测试的时候还需要进一步的考虑
我们可以使用终端 go run main.go 来进行服务启动这个时候我们启动两个终端来进行服务启动就可以启动两个服务了但是我们的后一个服务会将前一个服务覆盖。注意这里的启动需要在外层服务启动也就是 go.mod 的同一层也就是 main.go 的上一层
所以此处的解决方案是将 consul 的注册时的ID修改为唯一的此处选用的唯一方案是UUID
下面是更新后的服务注册的效果
引入包
github.com/satori/go.uuid//registration.ID global.ServerConfig.Name // 此处修改为使用 UUID 生成registration.ID fmt.Sprintf(%s, uuid.NewV4()) // 此处修改为使用 UUID 生成再次尝试我们就会发现 我们的 user-srv 实例中含有两个实例了
此处有一个需要理解的小案例这个案例很适用于理解go语言的基础知识
我们监听 go grpc 服务的优雅退出即一旦出现 使用 ctrl c 进行服务退出的场景我们就实现立刻从 consul 中将服务取消并在控制台提示服务注销成功的提示否则就只能等待一分钟 consul 检测不到服务活动才会自动将服务取消
// 注意此处是阻塞式的所以需要一个 goroutine 来进行异步操作// 将自己的服务绑定端口go func() {err server.Serve(lis)if err ! nil {panic(fail to start grpc err.Error())}}()// 创建一个通道quit : make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)// 阻塞住若接到请求则放通直接将服务注销-quitif err client.Agent().ServiceDeregister(serviceID); err ! nil {zap.S().Info(注销失败...)}zap.S().Info(注销成功)这是一个很好的理解 goroutine 和 信号消息队列机制的例子
配置负载均衡到 gin 服务中
这里要修改的是 initialize/srv_conn.go 这个策略
src_conn.go
import (fmtgithub.com/hashicorp/consul/api_ github.com/mbobakov/grpc-consul-resolvergo.uber.org/zapgoogle.golang.org/grpcmxshop-api/user-web/globalmxshop-api/user-web/proto
)func InitUserSrvConn() {consulInfo : global.ServerConfig.ConsulInfouserConn, err : grpc.Dial(fmt.Sprintf(consul://%s:%d/%s?wait14s, consulInfo.Host, consulInfo.Port, global.ServerConfig.UserSrvInfo.Name),grpc.WithInsecure(),grpc.WithDefaultServiceConfig({loadBalancingPolicy: round_robin}),)if err ! nil {zap.S().Fatal(InitUserSrvConn, 建立用户服务连接失败)}userSrvClient : proto.NewUserClient(userConn)global.UserSrvClient userSrvClient}将原来的 InitUserSrvConn() 修改为 不可用用这里的新的服务
配置中心
我们需要远程的统一配置来避免本地配置会出现的一系列问题
大型项目中每一个服务的实例过多一个服务可能有20多个实例这种情况下若要一个一个修改配置文件是很不现实的且极容易出错。大型项目中会有很多的服务有时可能会达到100多个服务这些服务有时候会有公共配置文件对于这些公共配置文件来讲如果要一个一个改的话也会极为复杂难以动态监听配置文件变化需要服务重启golang 中有 viper、但是其他语言的对应框架有不同的用法很割裂极容易出错
而我们的配置中心通常来讲需要具有以下功能配置实时推送、权限管理、集群支持度、配置回滚、环境隔离等多项能力用以解决上述问题。
目前的主流配置中心有apollo、nacos当然我们也可以直接使用consul 做我们的配置中心
apollo 是协程开源的配置中心其专注于配置中心功能大且完善但其没有丰富的多语言支持其多语言支持都是借助于第三方开发人员实现的nacos 是 alibaba 开源的配置中心同时也集成了服务注册和发现功能但其服务注册发现功能没有 consul 的生态更完善但其官方支持多语言开发较为稳定
使用 Docker 安装 Nacos
一键安装拉取
docker run --name nacos-standalone -e MODEstandlone -e JVM_XMS512m -e JVM_XMX512m -e JVM_XMN256m -p 8848:8848 -d nacos/nacos-server:latest# 下面是可以的上面最新版本可能不支持快速启动了docker run --name nacos-standalone -e MODEstandalone -e JVM_XMS512m -e JVM_XMX512m -e JVM_XMN256m -p 8848:8848 -p 9848:9848 -d nacos/nacos-server:v2.2.0
Nacos 的基本使用
打开nacos控制台我们一般使用 命名空间 配置文件的方式对配置进行管理每一个服务对应一个命名空间每一个命名空间下对应多个配置文件
例如 user-web 和 user-srv 就是同一个命名空间下的服务
我们使用组来区分生产、开发和测试环境
通过 api 调用配置中心
我们尝试新建一个 user 命名空间中的文件user-srv.json 组为 dev 格式为 json 进行简单配置
{name: user-srv
}测试
在一个标准 main.go 中创建如下测试代码
package mainimport (fmtgithub.com/nacos-group/nacos-sdk-go/clientsgithub.com/nacos-group/nacos-sdk-go/common/constantgithub.com/nacos-group/nacos-sdk-go/vo
)func main() {// 连接 nacos 服务器sc : []constant.ServerConfig{{IpAddr: 192.168.202.140,Port: 8848,},}// 创建客户端配置对象配置命名空间存活时间等信息cc : constant.ClientConfig{NamespaceId: 2a856b71-60d2-44ff-8ff4-4ae698544724,TimeoutMs: 5000,NotLoadCacheAtStart: true,LogDir: tmp/nacos/log,CacheDir: tmp/nacos/cache,LogLevel: debug,}// 之后将创建的这两个对象传入到具体的配置对象中configClient, err : clients.CreateConfigClient(map[string]interface{}{serverConfigs: sc,clientConfig: cc,})if err ! nil {panic(err)}// 获取配置content, err : configClient.GetConfig(vo.ConfigParam{DataId: user-srv.json,Group: dev,})if err ! nil {panic(err)}fmt.Println(content)
}
标准输出
{name: user-srv
}若要动态监听配置变化在GetConfig 后添加 ListenConfig 的参数中添加 OnChange 参数来获取监听配置文件变化的能力 // 添加配置监听的变化信息err configClient.ListenConfig(vo.ConfigParam{DataId: user-srv.json,Group: dev,OnChange: func(namespace, group, dataId, data string) {fmt.Println(配置文件发生变化)fmt.Println(namespace: namespace)fmt.Println(group: group)fmt.Println(dataId: dataId)fmt.Println(data: data)},})if err ! nil {panic(err)}Nacos 集成在 GIN 中
此时我们的 Nacos 就基本配置完成了但我们原先的配置文件还需要配置一个 Nacos 地址就可以了剩下的我们全部在远程注册中心Nacos 中完成
我们先建立本地的 Nacos 配置文件这个配置文件和 java 中的 bootstrap.yml 有异曲同工之妙
host: 192.168.202.140
port: 8848
namespace: 2a856b71-60d2-44ff-8ff4-4ae698544724
user: nacos
password: nacos
dataid: user-web.json
group: dev之后我们在 Config.go 中将Nacos 的读取信息录入
type NacosConfig struct {Host string mapstructure:hostPort int mapstructure:portNamespace string mapstructure:namespaceUser string mapstructure:userPassword string mapstructure:passwordDataid string mapstructure:dataidGroup string mapstructure:group
}
并且将 NacosConfig 作为全局变量提前生成
在 global.go 中添加
var (// 用于读取配置ServerConfig *config.ServerConfig config.ServerConfig{}// 用于进行错误处理Trans ut.Translator// nacos 配置NacosConfig *config.NacosConfig config.NacosConfig{}// 进行UserClient grpc 服务的生成UserSrvClient proto.UserClient
)
之后再 InitConfig 中将对应的信息进行初始化:
initialzie/config.go
func InitConfig() {configFileName : user-web/config-pro.yamldebug : GetenvInfo(MXSHOP-DEBUG)if debug {configFileName user-web/config-debug.yaml}v : viper.New()v.SetConfigFile(configFileName)if err : v.ReadInConfig(); err ! nil {panic(err)}// 注意这里应该是全局变量全局变量的部署应该是在 global 目录中//serverConfig : config.ServerConfig{}if err : v.Unmarshal(global.NacosConfig); err ! nil {panic(err)}zap.L().Info(fmt.Sprintf(配置信读取%v, global.NacosConfig))fmt.Println(global.NacosConfig)sc : []constant.ServerConfig{{IpAddr: global.NacosConfig.Host,Port: global.NacosConfig.Port,},}cc : constant.ClientConfig{NamespaceId: global.NacosConfig.Namespace,TimeoutMs: 5000,NotLoadCacheAtStart: true,LogDir: tmp/nacos/log,CacheDir: tmp/nacos/cache,LogLevel: debug,}configClient, err : clients.CreateConfigClient(map[string]interface{}{serverConfigs: sc,clientConfig: cc,})if err ! nil {zap.S().Fatalf(initialize config fail: %s, err.Error())}content, err : configClient.GetConfig(vo.ConfigParam{DataId: global.NacosConfig.Dataid,Group: global.NacosConfig.Group,})if err ! nil {zap.S().Fatal(initialize config fail: %s, err.Error())}zap.S().Infof(config info read success: %s, content)// 监听远程配置信息变化err configClient.ListenConfig(vo.ConfigParam{DataId: global.NacosConfig.Dataid,Group: global.NacosConfig.Group,OnChange: func(namespace, group, dataId, data string) {fmt.Println(配置文件发生变化)fmt.Println(namespace: namespace)fmt.Println(group: group)fmt.Println(dataId: dataId)fmt.Println(data: data)},})fmt.Println(content)err json.Unmarshal([]byte(content), global.ServerConfig)if err ! nil {zap.S().Fatalf(NACOS read fail: %s, err.Error())}fmt.Println(global.ServerConfig)//v.WatchConfig()//v.OnConfigChange(func(e fsnotify.Event) {// zap.S().Infof(配置文件产生变化%s, e.Name)// v.ReadInConfig()// v.Unmarshal(global.ServerConfig)// zap.L().Info(fmt.Sprintf(修改了配置信息%v\n, global.ServerConfig))//})
}Nacos 集成在 grpc 中
操作和 集成在 GIN 中的思路完全一致
package initializeimport (encoding/jsonfmtgithub.com/nacos-group/nacos-sdk-go/clientsgithub.com/nacos-group/nacos-sdk-go/vogithub.com/nacos-group/nacos-sdk-go/common/constantgithub.com/spf13/vipergo.uber.org/zapmxshop_srvs/user_srv/global
)func GetEnvInfo(env string) bool {viper.AutomaticEnv()var rs boolrs viper.GetBool(env)return rsreturn true
}func InitConfig() {debug : GetEnvInfo(MXSHOP-DEBUG)zap.S().Info(fmt.Sprintf(------------, debug))configFileNamePrefix : configconfigFileName : fmt.Sprintf(user_srv/%s-pro.yaml, configFileNamePrefix)if debug {configFileName fmt.Sprintf(user_srv/%s-debug.yaml, configFileNamePrefix)}v : viper.New()v.SetConfigFile(configFileName)if err : v.ReadInConfig(); err ! nil {panic(err)}// 将配置文件进行解析if err : v.Unmarshal(global.NacosConfig); err ! nil {panic(err)}sc : []constant.ServerConfig{{IpAddr: global.NacosConfig.Host,Port: global.NacosConfig.Port,},}cc : constant.ClientConfig{TimeoutMs: 5000,NamespaceId: 2a856b71-60d2-44ff-8ff4-4ae698544724,CacheDir: tmp/nacos/cache,NotLoadCacheAtStart: true,LogDir: tmp/nacos/log,LogLevel: debug,}configClient, err : clients.CreateConfigClient(map[string]interface{}{serverConfigs: sc,clientConfig: cc,})if err ! nil {zap.S().Fatalf(%s, err.Error())}content, err : configClient.GetConfig(vo.ConfigParam{DataId: global.NacosConfig.Dataid,Group: global.NacosConfig.Group,})if err ! nil {zap.S().Fatalf(%s, err.Error())}err configClient.ListenConfig(vo.ConfigParam{DataId: global.NacosConfig.Dataid,Group: global.NacosConfig.Group,OnChange: func(namespace, group, dataId, data string) {fmt.Println(配置文件发生变化)fmt.Println(namespace: namespace)fmt.Println(group: group)fmt.Println(dataId: dataId)fmt.Println(data: data)},})if err ! nil {zap.S().Fatalf(%s, err.Error())}err json.Unmarshal([]byte(content), global.ServerConfig)if err ! nil {zap.S().Fatalf(%s, err.Error())}zap.S().Info(global.ServerConfig)
}