服务端的高可用方案

前言

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

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

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

grpc

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

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

当某一个服务需要调用时,会以前缀匹配的形式,从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 {
    
    
			_ = <-ch
		}
	}()

	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。
    在这里插入图片描述

第三种,

  • 基于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.GetHostname()

tcp、websocket

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

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

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

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

在这里插入图片描述

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

猜你喜欢

转载自blog.csdn.net/fwhezfwhez/article/details/110918799