etcd campaign

1. 引言

本文主要讲解使用etcd进行选举的流程,以及对应的缺陷和使用场景

2. etcd选举流程

流程如以代码所示,流程为:

  • clientv3.New

    创建client与etcd server建立连接

  • concurrency.NewSession

    创建选举的session,一般会配置session的TTL(内部会创建一个lease并进行保活)

  • concurrency.NewElection

    创建选举,并指定prefix key

    func NewElection(s *Session, pfx string) *Election {
    	return &Election{session: s, keyPrefix: pfx + "/"}
    }
    
  • e.Campaign

    开始选举,并配置选举key的val,一般配置节点名

代码:

	cli, err := clientv3.New(clientv3.Config{
		Endpoints:            []string{"172.20.20.55:2379"},
		DialTimeout:          5 * time.Second,
		DialKeepAliveTime:    3 * time.Second,
		DialKeepAliveTimeout: 3 * time.Second,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	for {
		s, err := concurrency.NewSession(cli, concurrency.WithTTL(2))
		if err != nil {
			log.Fatal(err)
		}
		defer s.Close()
		e := concurrency.NewElection(s, "/test/election")

		log.Println("Start campaign", e.Key())
		if err := e.Campaign(cli.Ctx(), etcdServerIpAndPort); err != nil {
			log.Fatal(err)
		}
        // TODO: send a message indicating that the current node has become the leader
		log.Println("Campaign success, become leader")

        // determine whether the campaign session is done

        select {
        case <-s.Done():
          	log.Println("Campaign session done")
        }
	}

2.1. 创建Session流程

concurrency.NewSession里的具体实现,参考以下源码,流程:

  • 根据参数使用传入的lease,或根据TTL创建lease

    	ops := &sessionOptions{ttl: defaultSessionTTL, ctx: client.Ctx()}
    	for _, opt := range opts {
    		opt(ops)
    	}
    
    	id := ops.leaseID
    	if id == v3.NoLease {
    		resp, err := client.Grant(ops.ctx, int64(ops.ttl))
    		if err != nil {
    			return nil, err
    		}
    		id = v3.LeaseID(resp.ID)
    	}
    
  • client.KeepAlive

    对创建的lease进行保活(lease过期,也意味着session失效)

    	ctx, cancel := context.WithCancel(ops.ctx)
    	keepAlive, err := client.KeepAlive(ctx, id)
    	if err != nil || keepAlive == nil {
    		cancel()
    		return nil, err
    	}
    

    client.KeepAlive会返回一个keepAlive channel,如果保活失败,lease过期,此channel会关闭,从而通知调用方Session已失效(如果当前节点为lease,意味着leader失效),参考代码:

    	donec := make(chan struct{})
    	s := &Session{client: client, opts: ops, id: id, cancel: cancel, donec: donec}
    
    	// keep the lease alive until client error or cancelled context
    	go func() {
    		defer close(donec)
    		for range keepAlive {
    			// eat messages until keep alive channel closes
    		}
    	}()
    

完整代码:

func NewSession(client *v3.Client, opts ...SessionOption) (*Session, error) {
	ops := &sessionOptions{ttl: defaultSessionTTL, ctx: client.Ctx()}
	for _, opt := range opts {
		opt(ops)
	}

	id := ops.leaseID
	if id == v3.NoLease {
		resp, err := client.Grant(ops.ctx, int64(ops.ttl))
		if err != nil {
			return nil, err
		}
		id = v3.LeaseID(resp.ID)
	}

	ctx, cancel := context.WithCancel(ops.ctx)
	keepAlive, err := client.KeepAlive(ctx, id)
	if err != nil || keepAlive == nil {
		cancel()
		return nil, err
	}

	donec := make(chan struct{})
	s := &Session{client: client, opts: ops, id: id, cancel: cancel, donec: donec}

	// keep the lease alive until client error or cancelled context
	go func() {
		defer close(donec)
		for range keepAlive {
			// eat messages until keep alive channel closes
		}
	}()

	return s, nil
}
2.1.1. 保活流程

client.KeepAlive内部流程:

  • 判断id是否在保活的队列中,参考上一部分,创建session是可以传入一个已存在的lease

    • 不存在则创建并加入到l.keepAlives保活队列中
    • 存在则将当前创建的channel和ctx加入到keepAlive结构体中
    	ka, ok := l.keepAlives[id]
    	if !ok {
    		// create fresh keep alive
    		ka = &keepAlive{
    			chs:           []chan<- *LeaseKeepAliveResponse{ch},
    			ctxs:          []context.Context{ctx},
    			deadline:      time.Now().Add(l.firstKeepAliveTimeout),
    			nextKeepAlive: time.Now(),
    			donec:         make(chan struct{}),
    		}
    		l.keepAlives[id] = ka
    	} else {
    		// add channel and context to existing keep alive
    		ka.ctxs = append(ka.ctxs, ctx)
    		ka.chs = append(ka.chs, ch)
    	}
    

    keepAlive结构体参数描述:

    • chs:当前lease关联的ch列表,若保活失败,则都会关闭,以此通知调用KeepAlive处,进行相应的逻辑处理,如需要处理Session失效。

    • ctxs:保存调用KeepAlive时传入的ctx,若ctx失效,意味着调用方不再需要进行lease保活

    • deadline:当前lease的失效时间,默认值为l.firstKeepAliveTimeout,此值默认为client.cfg.DialTimeout+time.Second,初始化代码如下:

      func NewLease(c *Client) Lease {
      	return NewLeaseFromLeaseClient(RetryLeaseClient(c), c, c.cfg.DialTimeout+time.Second)
      }
      
      func NewLeaseFromLeaseClient(remote pb.LeaseClient, c *Client, keepAliveTimeout time.Duration) Lease {
      	l := &lessor{
      		donec:                 make(chan struct{}),
      		keepAlives:            make(map[LeaseID]*keepAlive),
      		remote:                remote,
      		firstKeepAliveTimeout: keepAliveTimeout,
      	}
      	if l.firstKeepAliveTimeout == time.Second {
      		l.firstKeepAliveTimeout = defaultTTL
      	}
      	if c != nil {
      		l.callOpts = c.callOpts
      	}
      	reqLeaderCtx := WithRequireLeader(context.Background())
      	l.stopCtx, l.stopCancel = context.WithCancel(reqLeaderCtx)
      	return l
      }
      
    • donec:lease失效后,用于通知清理l.keepAlives中对应的数据

  • 开启协程清理ctx

    仅清理ctx对应keepAlive中的ch和ctx

    go l.keepAliveCtxCloser(id, ctx, ka.donec)
    
    func (l *lessor) keepAliveCtxCloser(id LeaseID, ctx context.Context, donec <-chan struct{}) {
    	select {
    	case <-donec:
    		return
    	case <-l.donec:
    		return
    	case <-ctx.Done():
    	}
    
    	l.mu.Lock()
    	defer l.mu.Unlock()
    
    	ka, ok := l.keepAlives[id]
    	if !ok {
    		return
    	}
    
    	// close channel and remove context if still associated with keep alive
    	for i, c := range ka.ctxs {
    		if c == ctx {
    			close(ka.chs[i])
    			ka.ctxs = append(ka.ctxs[:i], ka.ctxs[i+1:]...)
    			ka.chs = append(ka.chs[:i], ka.chs[i+1:]...)
    			break
    		}
    	}
    	// remove if no one more listeners
    	if len(ka.chs) == 0 {
    		delete(l.keepAlives, id)
    	}
    }
    
  • 开启协程发送保活信息,以及确认lease是否过期

    firstKeepAliveOnce为sync.Once类型,多次调用仅会执行一次

    	l.firstKeepAliveOnce.Do(func() {
    		go l.recvKeepAliveLoop()
    		go l.deadlineLoop()
    	})
    
    • 发送以及接收保活信息

      func (l *lessor) recvKeepAliveLoop() (gerr error) {
      	defer func() {
      		l.mu.Lock()
      		close(l.donec)
      		l.loopErr = gerr
      		for _, ka := range l.keepAlives {
      			ka.close()
      		}
      		l.keepAlives = make(map[LeaseID]*keepAlive)
      		l.mu.Unlock()
      	}()
      
      	for {
      		stream, err := l.resetRecv()
      		if err != nil {
      			if canceledByCaller(l.stopCtx, err) {
      				return err
      			}
      		} else {
      			for {
      				resp, err := stream.Recv()
      				if err != nil {
      					if canceledByCaller(l.stopCtx, err) {
      						return err
      					}
      
      					if toErr(l.stopCtx, err) == rpctypes.ErrNoLeader {
      						l.closeRequireLeader()
      					}
      					break
      				}
      
      				l.recvKeepAlive(resp)
      			}
      		}
      		log.Println("resetRecv")
      		select {
      		case <-time.After(retryConnWait):
      			continue
      		case <-l.stopCtx.Done():
      			return l.stopCtx.Err()
      		}
      	}
      }
      
      
      • 发送

        resetRecv函数中获取一个grpc的stream,并通过此发送保活信息

        // resetRecv opens a new lease stream and starts sending keep alive requests.
        func (l *lessor) resetRecv() (pb.Lease_LeaseKeepAliveClient, error) {
        	sctx, cancel := context.WithCancel(l.stopCtx)
        	stream, err := l.remote.LeaseKeepAlive(sctx, l.callOpts...)
        	if err != nil {
        		cancel()
        		return nil, err
        	}
        
        	l.mu.Lock()
        	defer l.mu.Unlock()
        	if l.stream != nil && l.streamCancel != nil {
        		l.streamCancel()
        	}
        
        	l.streamCancel = cancel
        	l.stream = stream
        
        	go l.sendKeepAliveLoop(stream)
        	return stream, nil
        }
        

        通过sendKeepAliveLoop函数进行保活信息的发送,关键逻辑:

        1. 遍历l.keepAlives,通过每个keepAlive结构体中的nextKeepAlive来判断是否要发送保活信息(nextKeepAlive数据参考之前讲的初始化和接收保活回复处)
        2. 每隔0.5秒运行一次,出现错误时直接退出执行
        // sendKeepAliveLoop sends keep alive requests for the lifetime of the given stream.
        func (l *lessor) sendKeepAliveLoop(stream pb.Lease_LeaseKeepAliveClient) {
        	for {
        		var tosend []LeaseID
        
        		now := time.Now()
        		l.mu.Lock()
        		for id, ka := range l.keepAlives {
        			if ka.nextKeepAlive.Before(now) {
        				tosend = append(tosend, id)
        			}
        		}
        		l.mu.Unlock()
        
        		for _, id := range tosend {
        			r := &pb.LeaseKeepAliveRequest{ID: int64(id)}
        			if err := stream.Send(r); err != nil {
        				// TODO do something with this error?
        				return
        			}
        		}
        
        		select {
        		case <-time.After(500 * time.Millisecond):
        		case <-stream.Context().Done():
        			log.Println("stream context done")
        			return
        		case <-l.donec:
        			return
        		case <-l.stopCtx.Done():
        			return
        		}
        	}
        }
        
      • 接收信息

        接收lease保活信息,并进行处理,主要更新nextKeepAlive(下一次发送时间)和deadline

        关键逻辑:

        1. nextKeepAlive为time.Now().Add((time.Duration(karesp.TTL) * time.Second) / 3.0),其中TTL为NewSession时传入的TTL。
        2. 如果回复中TTL为0,表明lease过期

        处理函数如下:

        // recvKeepAlive updates a lease based on its LeaseKeepAliveResponse
        func (l *lessor) recvKeepAlive(resp *pb.LeaseKeepAliveResponse) {
        	karesp := &LeaseKeepAliveResponse{
        		ResponseHeader: resp.GetHeader(),
        		ID:             LeaseID(resp.ID),
        		TTL:            resp.TTL,
        	}
        
        	l.mu.Lock()
        	defer l.mu.Unlock()
        
        	ka, ok := l.keepAlives[karesp.ID]
        	if !ok {
        		return
        	}
        
        	if karesp.TTL <= 0 {
        		// lease expired; close all keep alive channels
        		delete(l.keepAlives, karesp.ID)
        		ka.close()
        		return
        	}
        
        	// send update to all channels
        	nextKeepAlive := time.Now().Add((time.Duration(karesp.TTL) * time.Second) / 3.0)
        	ka.deadline = time.Now().Add(time.Duration(karesp.TTL) * time.Second)
        	for _, ch := range ka.chs {
        		select {
        		case ch <- karesp:
        		default:
        		}
        		// still advance in order to rate-limit keep-alive sends
        		ka.nextKeepAlive = nextKeepAlive
        	}
        }
        
    • 判断lease是否过期

      主要通过deadline进行判断,是否会实时更新。

      func (l *lessor) deadlineLoop() {
      	for {
      		select {
      		case <-time.After(time.Second):
      		case <-l.donec:
      			return
      		}
      		now := time.Now()
      		l.mu.Lock()
      		for id, ka := range l.keepAlives {
      			if ka.deadline.Before(now) {
      				// waited too long for response; lease may be expired
      				ka.close()
      				delete(l.keepAlives, id)
      			}
      		}
      		l.mu.Unlock()
      	}
      }
      

KeepAlive完整代码:

func (l *lessor) KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error) {
	ch := make(chan *LeaseKeepAliveResponse, LeaseResponseChSize)

	l.mu.Lock()
	// ensure that recvKeepAliveLoop is still running
	select {
	case <-l.donec:
		err := l.loopErr
		l.mu.Unlock()
		close(ch)
		return ch, ErrKeepAliveHalted{Reason: err}
	default:
	}
	ka, ok := l.keepAlives[id]
	if !ok {
		// create fresh keep alive
		ka = &keepAlive{
			chs:           []chan<- *LeaseKeepAliveResponse{ch},
			ctxs:          []context.Context{ctx},
			deadline:      time.Now().Add(l.firstKeepAliveTimeout),
			nextKeepAlive: time.Now(),
			donec:         make(chan struct{}),
		}
		l.keepAlives[id] = ka
	} else {
		// add channel and context to existing keep alive
		ka.ctxs = append(ka.ctxs, ctx)
		ka.chs = append(ka.chs, ch)
	}
	l.mu.Unlock()

	go l.keepAliveCtxCloser(id, ctx, ka.donec)
	l.firstKeepAliveOnce.Do(func() {
		go l.recvKeepAliveLoop()
		go l.deadlineLoop()
	})

	return ch, nil
}

keepAlive.close()函数:

关闭所有调用KeepAlive函数返回的channel,此处为通知对应的Session

func (ka *keepAlive) close() {
	close(ka.donec)
	for _, ch := range ka.chs {
		close(ch)
	}
}
2.1.2. 保活流程总结
  1. 保活消息发送的间隔为创建Session时传入的TTL或者lease的TTL除以3,如TTL为3,则每隔1s发送一次;但是如果TTL为2,并不是每0.667s发送一次,因为执行保活的函数是固定每0.5s执行一次。所以间隔只能是0.5的整数倍,即如果TTL为2,则为1s发送一次保活信息。
    在这里插入图片描述

  2. lease过期也就意味着Session失效

2.2 选举流程

在这里插入图片描述

流程:

  1. 创建一个选举对象

    func NewElection(s *Session, pfx string) *Election {
    	return &Election{session: s, keyPrefix: pfx + "/"}
    }
    
  2. 进行选举

主要介绍选举的步骤和逻辑:

  • 根据keyPrefix(NewElection时传入的)和lease id,组成代表当前节点的key

    k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
    
  • 通过事务判断key是否存在

    • 存在则获取值
      • 如果val与获取值不同,更新val,参考e.Proclaim
    • 不存在则插入key和val数据,并绑定对应的Session lease,如果lease过期后,对应的key和val也会被删除
    	txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
    	txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
    	txn = txn.Else(v3.OpGet(k))
    	resp, err := txn.Commit()
    	if err != nil {
    		return err
    	}
    	e.leaderKey, e.leaderRev, e.leaderSession = k, resp.Header.Revision, s
    	if !resp.Succeeded {
    		kv := resp.Responses[0].GetResponseRange().Kvs[0]
    		e.leaderRev = kv.CreateRevision
    		if string(kv.Value) != val {
    			if err = e.Proclaim(ctx, val); err != nil {
    				e.Resign(ctx)
    				return err
    			}
    		}
    	}
    

    e.Proclaim代码:

    func (e *Election) Proclaim(ctx context.Context, val string) error {
    	if e.leaderSession == nil {
    		return ErrElectionNotLeader
    	}
    	client := e.session.Client()
    	cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
    	txn := client.Txn(ctx).If(cmp)
    	txn = txn.Then(v3.OpPut(e.leaderKey, val, v3.WithLease(e.leaderSession.Lease())))
    	tresp, terr := txn.Commit()
    	if terr != nil {
    		return terr
    	}
    	if !tresp.Succeeded {
    		e.leaderKey = ""
    		return ErrElectionNotLeader
    	}
    
    	e.hdr = tresp.Header
    	return nil
    }
    

    如果e.Proclaim更新值失败则删除key,然后Campaign返回错误,下次调用Campaign时继续执行

    e.Resign功能为删除相应的选举key,代码:

    func (e *Election) Resign(ctx context.Context) (err error) {
    	if e.leaderSession == nil {
    		return nil
    	}
    	client := e.session.Client()
    	cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
    	resp, err := client.Txn(ctx).If(cmp).Then(v3.OpDelete(e.leaderKey)).Commit()
    	if err == nil {
    		e.hdr = resp.Header
    	}
    	e.leaderKey = ""
    	e.leaderSession = nil
    	return err
    }
    
  • 根据e.keyPrefix和e.leaderRev(上一步骤中key存入etcd server时的Revision),等待在此Revision之前创建的,具有相同prefix的key被删除

    	_, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
    	if err != nil {
    		// clean up in case of context cancel
    		select {
    		case <-ctx.Done():
    			e.Resign(client.Ctx())
    		default:
    			e.leaderSession = nil
    		}
    		return err
    	}
    

    waitDeletes逻辑:

    • 通过client.Get()获取指定前缀、指定最大创建Revision的最后一条key。即与当前选举key含有相同的prefix的,上一条数据,也可以理解为获取比当前节点先插入选举key、val的其它节点的key和val
      • 获取到数据,表明其它节点先创建了key,需要等待其过期,通过waitDelete watch keyPrefix的每个删除操作;watch到相应的删除事件,则重新调用client.Get(),判断是否需要继续等待
      • 没有获取到,表明没有其它节点先创建了key,自身可以成为leader,直接返回

    waitDeletes代码:

    func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) {
    	getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
    	for {
    		resp, err := client.Get(ctx, pfx, getOpts...)
    		if err != nil {
    			return nil, err
    		}
    		if len(resp.Kvs) == 0 {
    			return resp.Header, nil
    		}
    		lastKey := string(resp.Kvs[0].Key)
    		if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
    			return nil, err
    		}
    	}
    }
    

    waitDelete代码:

    func waitDelete(ctx context.Context, client *v3.Client, key string, rev int64) error {
    	cctx, cancel := context.WithCancel(ctx)
    	defer cancel()
    
    	var wr v3.WatchResponse
    	wch := client.Watch(cctx, key, v3.WithRev(rev))
    	for wr = range wch {
    		for _, ev := range wr.Events {
    			if ev.Type == mvccpb.DELETE {
    				return nil
    			}
    		}
    	}
    	if err := wr.Err(); err != nil {
    		return err
    	}
    	if err := ctx.Err(); err != nil {
    		return err
    	}
    	return fmt.Errorf("lost watcher waiting for delete")
    }
    

Campaign完整代码:

func (e *Election) Campaign(ctx context.Context, val string) error {
	s := e.session
	client := e.session.Client()

	k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
	txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
	txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
	txn = txn.Else(v3.OpGet(k))
	resp, err := txn.Commit()
	if err != nil {
		return err
	}
	e.leaderKey, e.leaderRev, e.leaderSession = k, resp.Header.Revision, s
	if !resp.Succeeded {
		kv := resp.Responses[0].GetResponseRange().Kvs[0]
		e.leaderRev = kv.CreateRevision
		if string(kv.Value) != val {
			if err = e.Proclaim(ctx, val); err != nil {
				e.Resign(ctx)
				return err
			}
		}
	}

	_, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
	if err != nil {
		// clean up in case of context cancel
		select {
		case <-ctx.Done():
			e.Resign(client.Ctx())
		default:
			e.leaderSession = nil
		}
		return err
	}
	e.hdr = resp.Header

	return nil
}
2.3.1. 选举流程总结
  1. 选举本质上为先到先得,是一个FIFO的队列,后来的需要等待前边的释放,而前边的释放时间则取决于设置的Session TTL,在lease过期,由etcd server删除对应的key后,下一个才可成为leader

3. 缺陷和使用场景

由上一章节描述的,当前节点要成为leder,需要等etcd server删除比当前节点先写入的其它节点的key和val。

如此意味着如果上一个节点故障后,需要等待上一个节点的Session TTL时间,下一个节点才会变为leader。而在此期间,如果etcd server发生故障,则这个时间还会延长。

3.1. etcd lease TTL测试

测试1:

测试流程:设置一个300s后超时的lease,关闭节点(etcd停止运行,etcd为单节点),300s后重启,发现该lease没有过期

结论:etcd停止服务后,lease的TTL会重置,且lease不会过期

在这里插入图片描述

测试2:

测试步骤:生成一个300s的lease,20s之后,kill掉etcd的leader,使etcd切主,然后查询该lease的剩余时间,结果为295s

结论:etcd切主后会重置lease的TTL

在这里插入图片描述

3.2 缺陷总结

通过上一部分中的测试,可以发现当etcd发生切主或重启(单节点)后,TTL会重置,也就是说当使用etcd进行选举的客户端发生故障后,在切主的过程中,etcd server也发生故障,则此时间会延长,因为故障节点的lease TTL被重置了,需要重新计算过期时间,这会导致切主时间延长。

使用场景:对切主的时间没有严苛的要求

3.3 使用的注意事项

根据前边的内容介绍,在选举的过程中,如果Session lease超时,Campaign处是感觉不到的,所以当Campaign返回后,需要额外判断Session是否Done了:

	for {
		s, err := concurrency.NewSession(cli, concurrency.WithTTL(2))
		if err != nil {
			log.Fatal(err)
		}
		defer s.Close()
		e := concurrency.NewElection(s, "/test/election")

		log.Println("Start campaign", e.Key())
		if err := e.Campaign(cli.Ctx(), etcdServerIpAndPort); err != nil {
			log.Fatal(err)
		}
        select {
		case <-s.Done():
			log.Println("Campaign session done")
			continue
		}
        // TODO: send a message indicating that the current node has become the leader
		log.Println("Campaign success, become leader")

        // determine whether the campaign session is done
        select {
        case <-s.Done():
          	log.Println("Campaign session done")
		}
	}

中,如果Session lease超时,Campaign处是感觉不到的,所以当Campaign返回后,需要额外判断Session是否Done了:

	for {
		s, err := concurrency.NewSession(cli, concurrency.WithTTL(2))
		if err != nil {
			log.Fatal(err)
		}
		defer s.Close()
		e := concurrency.NewElection(s, "/test/election")

		log.Println("Start campaign", e.Key())
		if err := e.Campaign(cli.Ctx(), etcdServerIpAndPort); err != nil {
			log.Fatal(err)
		}
        select {
		case <-s.Done():
			log.Println("Campaign session done")
			continue
		}
        // TODO: send a message indicating that the current node has become the leader
		log.Println("Campaign success, become leader")

        // determine whether the campaign session is done
        select {
        case <-s.Done():
          	log.Println("Campaign session done")
		}
	}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/567406.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序一到六章总结

第一章总结 认识微信小程序 小程序简介 微信(WeChat) 是腾讯公司于2011年1月21 日推出的一款为智能终端提供即时通信服务的应用程序。 小程序、订阅号、服务号、企业微信&#xff08;企业号&#xff09;属于微信公众平台的四大生态体系&#xff0c;它们面向不同的用户群体&…

Harmony OS应用开发性能优化全面指南

优化应用性能对于应用开发至关重要。通过高性能编程、减少丢帧卡顿、提升应用启动和响应速度&#xff0c;可以有效提升用户体验。本文将介绍一些优化应用性能的方法&#xff0c;以及常用的性能调优工具。 ArkTS高性能编程 为了提升代码执行速度&#xff0c;进而提升应用整体性…

若依如何去掉“正在加载系统资源,请耐心等待”

最近有网友反馈这个加载动画很丑&#xff0c;问我如何去掉&#xff1a; 首先找到前端页面的index.html文件&#xff0c;去掉或注释掉如下代码&#xff1a;

使用Gitee进行社交登录的流程

使用Gitee进行社交登录 创建Gitee第三方应用流程&#xff1a; 鼠标移动到个人头像上&#xff0c;点击账号设置 点击账号设置&#xff0c;选择左边目录下数据管理的第三方应用 然后选择创建应用 根据要求填写 填写好了上面的要求之后&#xff0c;点击创建应用&#xff0c;这样&…

【Java】如何获取客户端IP地址

在项目中往往涉及到“获取客户端IP地址”&#xff0c;常见到下面这样子的代码&#xff1a; package com.utils;import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.http.server.reactive.ServerHttpRequest; import java.net…

前端JS必用工具【js-tool-big-box】,获取浏览器参数、cookie、localStorage的存取

这一小节&#xff0c;我们针对js-tool-big-box工具做一些使用讲解&#xff0c;主要获取浏览器参数、cookie、localStorage的存取方面的。 这些方法差不多每次项目中要么用不到&#xff0c;要么就自己写一份&#xff0c;轮子造的很重复啊&#xff0c;而且localStorage有时候要求…

牛客网:环形链表的约瑟夫问题

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;每日一练 &#x1f337;追光的人&#xff0c;终会万丈光芒 目录 &#x1f3dd;1.问题描述&#xff1a; &#x1f3dd;2.实现代码&#xff1a; &#x1f3dd;1.问题描述&#xff1a; 前言&am…

windows系统CUDA的详细安装教程

CUDA系列 文章目录 CUDA系列前言一、CUDA简介二、安装配置视频教程三、CUDA的下载及安装3.1 环境检查3.2 CUDA 安装包下载3.3 安装CUDA&#xff08;略&#xff09;3.4 验证CUDA是否安装成功 四、cuDNN的下载及安装4.1 cuDNN下载4.2 cuDNN配置 五、配置环境变量六、下载并配置zl…

springboot 集成 i18n实现国际化信息返回 实现中英文切换 实现网站支持多语言切换

还是直接上代码 目前实现了 中英文 返回 别的语言 都差不多 主要用spring boot 自带的 类实现的 不用引入任何 依赖 使用的就是下面的类 org.springframework.context.MessageSource 是 Spring Framework 中用于支持国际化&#xff08;Internationalization&#xff0c;简称 i…

把 WordPress 变成 BaaS 服务:API 调用指南

有了前面两篇内容的铺垫&#xff0c;我们来聊聊 WordPress 作为 CMS / BaaS 服务使用时绕不开的问题&#xff0c;API 调用。 这篇内容同样的&#xff0c;会尽量少贴代码&#xff0c;简单的讲清楚一件事&#xff0c;降低阅读负担。 写在前面 首先&#xff0c;我们需要进行清晰…

使用autocannon和0x对网站进行性能分析(node)

npm i autocannon -g autocannon -c 100 -d 5 -p 10 http://localhost:3000/ 0x -o app.js 火焰图是根据程序的栈的状态对出现函数的采样数据统计而得&#xff0c;宽度代表函数运行一次所需的时长、高度代表栈的层数、颜色深度代表函数在采样中出现的频率&#xff0c;因此宽度…

手摸手教你把Ingress Nginx集成进Skywalking

背景 在微服务大行其道的今天&#xff0c;如何观测众多微服务、快速理清服务间的依赖、如何对服务之间的调用性能进行衡量&#xff0c;成了摆在大家面前的难题。对此&#xff0c;Skywalking应运而生&#xff0c;它是托管在 Apache 基金会下的开源项目&#xff0c;旨在帮助开发…

vue element-ui 表格横向滚动条在合计项下方

目前效果 需求效果 1.隐藏bodyWrapper滚动条&#xff0c;显示footerWrapper滚动条 css代码如下&#xff1a; div ::v-deep .el-table--scrollable-x .el-table__body-wrapper{overflow-x: hidden!important;z-index: 2!important;} div ::v-deep .el-table__footer-wrapper …

git的安装与配置教程--超详细版

一、git的安装 1. 官网下载git git官网地址&#xff1a;https://git-scm.com/download/win/ 选择需要的版本进行下载 2、下载完成之后&#xff0c;双击下载好的exe文件进行安装。 3、默认是C盘&#xff0c;推荐修改一下路径&#xff0c;然后点击下一步 4、Git配置&#xff…

电子邮箱是什么?电子邮箱怎么申请注册?

虽然通过电子邮箱收发邮件办公已经成为常态&#xff0c;但是很多人不清楚电子邮箱是什么&#xff1f;电子邮箱是指通过网络传递的“邮局”&#xff0c;可以用来收发电子邮件。每个人的电子邮箱地址都是唯一的&#xff0c;确保他人的邮件能准确送到我们的电子邮箱之中。电子邮箱…

CRMEB pro版/多门店商城系统客服配置教程

客服功能配置介绍 功能提示&#xff1a; Pro v2.0系统采用swoole框架&#xff0c;客服不需要单独配置&#xff0c;按照正常安装流程配置好程序即可使用&#xff01; 如出现客服无法使用&#xff0c;请检查&#xff1a; 1.消息队列是否正常 2.重启swoole 一、功能介绍 CRMEB商城…

刷课必备!用Python实现网上自动做题

前言 开学少不了老师会布置一些 软件上面的作业&#xff0c;今天教大家用python制作自动答题脚本&#xff0c;100%准确率哦喜欢的同学记得关注、收藏哦 环境使用 Python3.8Pycharm 模块使用 import requests —> 数据请求模块 pip install requestsimport parsel —>…

GPU 之争:训练大模型的显卡规格大比拼

训练大模型有多烧钱&#xff1f;&#xff08;含常用GPU规格比较&#xff09; 训练大模型有多烧钱&#xff1f; 解锁大型语言模型的运行秘诀大型语言模型 (LLM) 对硬件要求很高&#xff0c;其中显卡内存至关重要。Meta 的 LLaMA 2 模型提供了规模不等的选项&#xff1a;* 70B 模…

C++/Qt 小知识记录5

工作中遇到的一些小问题&#xff0c;总结的小知识记录&#xff1a;C/Qt 小知识5 Windows下查看端口占用情况C调用Python三方库测试库有没有被加上的测试方法初始化使用Python的env环境&#xff0c;用Py_SetPythonHome设置GDAL相关的&#xff0c;需要把osgeo、rasterio的路径加入…

js some对比forEach

some&#xff1a;return true可以停止循环 forEach&#xff1a;return true无法停止循环 <!DOCTYPE html> <html ng-app"my_app"><head><script type"text/javascript">const array [10, 20, 30];const targetValue 10;// 检测…