首页 > 其他分享 >服务端的高可用方案

服务端的高可用方案

时间:2022-11-22 23:33:18浏览次数:70  
标签:方案 cli err 可用 grpc nginx etcd 节点 服务端


前言

高可用主要解决以下问题:

  • 不是单个节点,任何一个服务节点挂了,能够自动使用其他业务节点。
  • 允许新的服务节点进入服务群体,并且是客户端无感知。

不同的服务协议,解决方案也不同。

grpc

  • 推荐指数: 3星
  • 推荐理由: 当前主流的etcd+grpc架构,有成熟的套件,有大量实际的案例,以及足够的可问人群。

grpc协议,主流使用基于 etcd 的服务发现做高可用。他的业务场景是服务群的内网相互调用。每一个服务节点,在发布时,都会将服务的host+port注册进etcd,key值是某个服务群名称的前缀,比如 /user/node/1, /user/node/2。这一步注册必须保持每5秒一次的租约。

缺点是,需要对开放的业务逻辑,重写方法,适配到grpc服务里。客户端代码需要重写。proto-gen-go版本在服务端与服务端之间的调用里,必须 统一。可以说是比较恶心的缺点了。

当某一个服务需要调用时,会以前缀匹配的形式,从etcd拿到可用列表,进而均衡得选择可用的服务。

etcd官方提供grpc协议的高可用最佳实践,这里贴一下关键代码:

调用方

import (
"go.etcd.io/etcd/clientv3"
etcdnaming "go.etcd.io/etcd/clientv3/naming"

"google.golang.org/grpc"
)

...

cli, cerr := clientv3.NewFromURL("http://localhost:2379")
r := &etcdnaming.GRPCResolver{Client: cli}
b := grpc.RoundRobin(r)
conn, gerr := grpc.Dial("my-service", grpc.WithBalancer(b), grpc.WithBlock(), ...)

续约的服务方

go etcd.Register("x.x.x.x:port", "app_key", "y.y.y.y:port", 5)
package etcd

import (
"context"
"encoding/json"
"go.uber.org/zap"
"log"
"strings"
"time"

"fmt"
"go.etcd.io/etcd/client/v3"
)

var cli *clientv3.Client

// Register register service with name as prefix to etcd, multi etcd addr should use ; to split
func Register(etcdAddr, name string, addr string, ttl int64) error {
var err error

if cli == nil {
cli, err = clientv3.New(clientv3.Config{
Endpoints: strings.Split(etcdAddr, ";"),
DialTimeout: 15 * time.Second,
LogConfig: &zap.Config{
Level: zap.NewAtomicLevelAt(zap.ErrorLevel),
Development: false,
Sampling: &zap.SamplingConfig{
Initial: 100,
Thereafter: 100,
},
Encoding: "json",
EncoderConfig: zap.NewProductionEncoderConfig(),
// Use "/dev/null" to discard all
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
},
})
if err != nil {
return err
}
}

service := Service{
Addr: addr,
}
bts, err := json.Marshal(service)
if err != nil {
return err
}

serviceValue := string(bts)
serviceKey := fmt.Sprintf("%s/%s", name, serviceValue)

ticker := time.NewTicker(time.Second * time.Duration(ttl))

go func() {
for {
getResp, err := cli.Get(context.Background(), serviceKey)
if err != nil {
log.Println(err)
} else if getResp.Count == 0 {
err = withAlive(serviceKey, serviceValue, ttl)
if err != nil {
log.Println(err)
}
} else {
// do nothing
}

<-ticker.C
}
}()

return nil
}

type Service struct {
Addr string `json:"Addr"`
}

func withAlive(serviceKey string, serviceValue string, ttl int64) error {
leaseResp, err := cli.Grant(context.Background(), ttl)
if err != nil {
return err
}

fmt.Printf("key:%v\n", serviceKey)
_, err = cli.Put(context.Background(), serviceKey, serviceValue, clientv3.WithLease(leaseResp.ID))
if err != nil {
return err
}

ch, err := cli.KeepAlive(context.Background(), leaseResp.ID)
if err != nil {
log.Println(err)
return err
}

// ch管道的值需要持续取出释放,否则会占用通道导致切片饱和
go func() {
for {
_ , ok:= <-ch
if !ok {
return
}
}
}()

return nil
}

// UnRegister remove service from etcd
func UnRegister(serviceKey string) {
if cli != nil {
cli.Delete(context.Background(), serviceKey)
}
}

http

http的高可用解决方案比较多,大致有以下三种主流:

  • nginx前置路由,通过upstream配置可用的服务节点
  • 腾讯云后台支持域名<负载均衡>到多个ip,并且通过<健康检查>做到和nginx同样的效果。
  • 手动实现基于http的robin均衡器,接入etcd

第一种

  • 推荐指数: 3星
  • 理由: 需要人工上服务器维护节点增减,不是很简约。 高可用依赖upstream,无损迁流和重启,需要人工参与,比较笨。
  • 在服务前置,有一个nginx集群, 每个nginx里有如下配置示例:
upstream srv_name_http {
server y.y.y.y:8112 weight=7;
server x.x.x.x:8112 weight=3;
server z.z.z.z:8112 weight=10;
}
server {
listen 80;
server_name your.addr.com;
error_log /data/log/nginx/your.addr.com.log;

# request header
proxy_read_timeout 3200;
proxy_send_timeout 3200;
proxy_set_header Host $http_host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

location / {
proxy_pass http://srv_name_http;
}

error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}

值得注意的是,nginx的维护,最好也要服务器解耦,就是不需要人工登录服务器配置nginx,而是将整个配置文件(一般是代码自动生成出来的,防错),嵌入项目目录下,在部署时,上传到对应位置,执行​​nginx -t​​​ ​​nginx -s reload​​。降低维护成本。

第二种

  • 推荐指数: 五星
  • 理由: 比较主流的解决方案,使用群体很多,可以做到无损,且不需要手动维护nginx
  • 服务器商一般都提供域名的负载均衡和​​健康检查​​。由于http域名到服务器固定了80端口,所以每个服务节点,都需要使用nginx来做proxy_pass。不过和第一种的区别在于, 1.nginx和服务节点绑定在同一个服务器,不需要前置nginx集群。2.nginx只有服务节点server,而不需要有upstream。
  • 基于域名做高可用。分别有公网域名对外,私有域名对内,在使用成本上,比grpc的方式强很多。

第三种,

  • 基于etcd实现一个http的服务发现。
  • 推荐指数: 4星
  • 理由: 实现看似简单,但是要了解etcd的原理以及无损迁移的原理,才能写出好的服务发现组件。如果已经做到了,那么它的效果等价于etcd+grpc那种。有用过的童鞋说好用,不过实现是不打算开源,哈哈。

大体实现原理为:

  • 每次以前缀获取时,请拉取到所有value并存入服务的内存里。
  • 监听前缀key变化,一旦某个key续约没了,则将内存里的该value移除。
  • 调用方,仅从内存中的队列中,寻找可用的url,而不是直接向etcd拿。

但是真正要做到无损,有两个方向可以实施:
第一, 实现的组件,必须做到roundrobin轮训机制,失败一个请求时,继续对下一个url请求,直到成功或者阈值。 这样就不怕心跳租期的窗口期。

第二,节点上可以增加一个下线路由,手动将这个节点下线后,再等待消费积压,关闭节点。这个实现的难点就是这个路由,在命令中要书写 curl ​​localhost:xxxx/offline-from-etcd/​​。 这个xxxx端口的寻找便是最难的地方。

这里不藏了,找到xxxx的方式,就是通过(主机名:节点名) 存一份到环境变量里,然后curl的xxxx会被这个环境变量替换。

哈哈,是不是很简单。获取主机名的方法是​​os.Hostname()​

tcp、websocket

nginx作client-hash的均衡策略,是可以实现websokect。tcp没测过。

增加一个服务节点,关注以下功能:

  • 获取客户端能访达的最快的tcp可用ip
  • 获取某个服务模块的ip和端口并返回告知客户端

客户端直连tcp。架构图如下:

服务端的高可用方案_nginx

tcp的高可用只体现在建立连接时,已经建立好的连接,在tcp服务挂了时,一定会蹦。这里推荐客户端作自动重连。所以设计上,tcp没有http那么复杂。


标签:方案,cli,err,可用,grpc,nginx,etcd,节点,服务端
From: https://blog.51cto.com/u_11553781/5878745

相关文章