Eureka 心跳和服务续约源码探秘——图解、源码级解析

news2024/11/19 18:44:52

🍊 Java学习:社区快速通道

🍊 深入浅出RocketMQ设计思想:深入浅出RocketMQ设计思想

🍊 绝对不一样的职场干货:大厂最佳实践经验指南


📆 最近更新:2023年5月25日


🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!


文章目录

  • 分布式系统的心跳机制
    • 心跳机制的实现方式
    • SpringCloud中的心跳
    • 关于服务续约的一些细节
  • Eureka 心跳和服务续约源码
    • 客户端源码
    • 心跳包的发送逻辑
    • 服务端流程

分布式系统的心跳机制

分布式系统是由多个计算机节点构成的系统,这些节点之间通过网络进行通信和协作。由于节点之间的网络连接不可靠,因此在分布式系统中,一个节点可能会因为网络故障或其他原因而失去与其他节点的联系。为了解决这个问题,分布式系统引入了心跳机制。

在这里插入图片描述

心跳机制是指每个节点定期向其他节点发送“心跳”消息,以表明自己的存在和正常运行。如果一个节点在一段时间内没有收到来自其他节点的心跳消息,那么它就会认为这些节点已经失去了联系,并采取相应的措施,例如重新选举领导节点或者启动备份节点。


心跳机制的实现方式

心跳机制的实现方式可以分为基于UDP协议、基于TCP协议和基于HTTP协议三种。

  • 基于UDP协议的心跳机制具有实现简单、网络开销小等优点。但由于UDP协议本身不可靠,因此可能会出现一些误判情况。

  • 基于TCP协议的心跳机制具有可靠性高、误判率低等优点。但由于TCP协议本身的特性,可能会出现一些网络延迟等问题。

  • 基于HTTP协议的心跳机制具有易于实现、可扩展性好等优点。但由于HTTP协议本身的特性,可能会出现一些网络延迟等问题。


在实际应用中,需要根据具体情况选择合适的心跳机制实现方式。

对于实时性要求较高的应用场景,可以选择基于UDP协议的心跳机制;对于对可靠性要求较高的应用场景,可以选择基于TCP协议的心跳机制。


SpringCloud中的心跳

SpringCloud也借助“心跳”来知晓服务的可用性,心跳检测有以下四种特点:

  1. 客户端发起: 心跳服务是由每个服务节点根据配置的时间主动发起的。
  2. 同步状态: 要告诉注册中心自己的状态,快不行了(OUT_OF_SERVICE)或是一切正常(UP)。
  3. 服务剔除: 对一段时间无响应的服务,要主动将其从注册列表中剔除,以防服务调用方请求失败。
  4. 服务续约: 服务续约底层也是靠着心跳来实现的,其中包含了一套处理流程。

关于服务续约的一些细节

服务续约分为两步:

  1. 将服务节点的状态同步到注册中心,这一步需要借助客户端的心跳功能来主动发送。
  2. 当心跳包到达注册中心的时候,注册中心有一套判别机制,来判定当前的续约心跳是否合理。并根据判断结果修改当前instance在注册中心记录的同步时间。

服务节点向注册中心发送续约请求:

  1. 服务续约请求: 客户端有一个DiscoverClient类,它是所有操作的入口。所以续约服务就从这个类的renew方法开始
  2. 发送心跳: 服务续约借助心跳来实现,因此发给注册中心的两个重要参数分别是服务的状态(UP)和lastDirtyTimeStamp
  • 如果续约成功,注册中心则会返回200的HTTP code
  • 如果续约不成功,注册中心返回404,这里的404并不是说没有找到注册中心的地址,而是注册中心认为当前服务节点并不存在。这个时候再怎么续约也不行了,客户端需要触发一次重新注册操作。
  1. 在重新注册之前,客户端会做下面两个操作,然后再主动调用服务注册流程:
  • 设置lastDirtyTimeStamp :由于重新注册意味着服务节点和注册中心的信息不同步,因此需要将当前系统时间更新到lastDirtyTimeStamp
  • 标记为脏节点
  1. 当注册成功的时候,清除脏节点标记,但是lastDirtyTimeStamp不会清除,因为这个属性将会在后面的服务续约中作为参数发给注册中心,以便服务中心判断节点的同步状态。

Eureka 心跳和服务续约源码

通过本章节,可以了解到:

  • 客户端心跳发送内容是什么?
  • 客户端续约流程
  • 服务端租约更新流程

在真正阅读之前,不妨先尝试回答一下上面的3个问题,带着疑问看文章,收获会更大 ~ 下面开始进入正题


客户端源码

打开DiscoveryClient,入口便是构造函数:

请添加图片描述
这里只关注服务的心跳是怎么发送的
请添加图片描述
通过方法名就可以看出这是一个在后台定时触发的任务

private void initScheduledTasks() {
    int renewalIntervalInSecs;
    int expBackOffBound;
    if (this.clientConfig.shouldFetchRegistry()) {
        renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
        expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
    }
	
	// 从这里开始看
    if (this.clientConfig.shouldRegisterWithEureka()) {
        renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
        this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
        this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
        this.statusChangeListener = new StatusChangeListener() {
            public String getId() {
                return "statusChangeListener";
            }

            public void notify(StatusChangeEvent statusChangeEvent) {
                if (InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) {
                    DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
                } else {
                    DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
                }

                DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
            }
        };
        if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
            this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
        }

        this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

可以直接从上面的第10行开始关注

this.scheduler.schedule(
    new TimedSupervisorTask(
        "heartbeat", 
        this.scheduler, 
        this.heartbeatExecutor, 
        renewalIntervalInSecs, 
        TimeUnit.SECONDS, 
        expBackOffBound, 
        new DiscoveryClient.HeartbeatThread()
    ), 
    (long)renewalIntervalInSecs, TimeUnit.SECONDS);

是定时启动后台任务的方法

  • renewalIntervalInSecs表示每多少秒启动一次定时任务
  • expBackOffBound是用来计算最大delay时间的

this.maxDelay = this.timeoutMillis * (long)expBackOffBound;

new DiscoveryClient.HeartbeatThread()是发送心跳的具体逻辑

private class HeartbeatThread implements Runnable {
    private HeartbeatThread() {
    }

    public void run() {
        if (DiscoveryClient.this.renew()) {
            DiscoveryClient.this.lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}

其中renew相当于续约的逻辑,心跳和续约是一套相互作用的机制,renew在客户端是发送了一个心跳,服务端接收了心跳之后会进行服务的续约

boolean renew() {
    try {
        EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);
        logger.debug("DiscoveryClient_{} - Heartbeat status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
        if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
            this.REREGISTER_COUNTER.increment();
            logger.info("DiscoveryClient_{} - Re-registering apps/{}", this.appPathIdentifier, this.instanceInfo.getAppName());
            long timestamp = this.instanceInfo.setIsDirtyWithTime();
            boolean success = this.register();
            if (success) {
                this.instanceInfo.unsetIsDirty(timestamp);
            }

            return success;
        } else {
            return httpResponse.getStatusCode() == Status.OK.getStatusCode();
        }
    } catch (Throwable var5) {
        logger.error("DiscoveryClient_{} - was unable to send heartbeat!", this.appPathIdentifier, var5);
        return false;
    }
}

心跳包的发送逻辑

EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);

和前面的服务注册一样一层层嵌套,第一层嵌套先是SessionEurekaClient
请添加图片描述
下一层是retry,再下一层是redirective,再下一层是matrix…和服务注册一模一样


直接进到最后一层AbstractJerseyEurekaHttpClient

public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
    String urlPath = "apps/" + appName + '/' + id;
    ClientResponse response = null;

    EurekaHttpResponse var10;
    try {
        WebResource webResource = this.jerseyClient.resource(this.serviceUrl).path(urlPath).queryParam("status", info.getStatus().toString()).queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
        if (overriddenStatus != null) {
            webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
        }

        Builder requestBuilder = webResource.getRequestBuilder();
        this.addExtraHeaders(requestBuilder);
        response = (ClientResponse)requestBuilder.put(ClientResponse.class);
        EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = EurekaHttpResponse.anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
        if (response.hasEntity()) {
            eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
        }

        var10 = eurekaResponseBuilder.build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", new Object[]{this.serviceUrl, urlPath, response == null ? "N/A" : response.getStatus()});
        }

        if (response != null) {
            response.close();
        }

    }

    return var10;
}

构造服务请求路径

请添加图片描述
之后构造WebResource对象

WebResource webResource = this.jerseyClient.resource(this.serviceUrl)
        .path(urlPath)
        .queryParam("status", info.getStatus().toString())
        .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp()
        .toString());

这里的serviceUrl是注册中心的url,前面的是当前机器的url,“lastDirtyTimestamp”是一个核心的属性

之后就是组装参数的流程,最后将请求发送出去,至此客户端发送心跳的逻辑就结束了。


服务端流程

服务端使用InstanceResource中的renewLease方法来接收心跳包:

public Response renewLease(@HeaderParam("x-netflix-discovery-replication") String isReplication, @QueryParam("overriddenstatus") String overriddenStatus, @QueryParam("status") String status, @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    boolean isFromReplicaNode = "true".equals(isReplication);
    boolean isSuccess = this.registry.renew(this.app.getName(), this.id, isFromReplicaNode);
    if (!isSuccess) {
        logger.warn("Not Found (Renew): {} - {}", this.app.getName(), this.id);
        return Response.status(Status.NOT_FOUND).build();
    } else {
        Response response;
        if (lastDirtyTimestamp != null && this.serverConfig.shouldSyncWhenTimestampDiffers()) {
            response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
            if (response.getStatus() == Status.NOT_FOUND.getStatusCode() && overriddenStatus != null && !InstanceStatus.UNKNOWN.name().equals(overriddenStatus) && isFromReplicaNode) {
                this.registry.storeOverriddenStatusIfRequired(this.app.getAppName(), this.id, InstanceStatus.valueOf(overriddenStatus));
            }
        } else {
            response = Response.ok().build();
        }

        logger.debug("Found (Renew): {} - {}; reply status={}", new Object[]{this.app.getName(), this.id, response.getStatus()});
        return response;
    }
}

当前心跳包是来自服务的提供者,并不是冗余备份,所以isFromReplicaNode是false。下面代码

boolean isSuccess = this.registry.renew(this.app.getName(), this.id, isFromReplicaNode);

是续约的方法

public boolean renew(final String appName, final String serverId, boolean isReplication) {
    this.log("renew " + appName + " serverId " + serverId + ", isReplication {}" + isReplication);
    List<Application> applications = this.getSortedApplications();
    Iterator var5 = applications.iterator();

    while(var5.hasNext()) {
        Application input = (Application)var5.next();
        if (input.getName().equals(appName)) {
            InstanceInfo instance = null;
            Iterator var8 = input.getInstances().iterator();

            while(var8.hasNext()) {
                InstanceInfo info = (InstanceInfo)var8.next();
                if (info.getId().equals(serverId)) {
                    instance = info;
                    break;
                }
            }

            this.publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId, instance, isReplication));
            break;
        }
    }
    return super.renew(appName, serverId, isReplication);
}

方法的入参serverId一定是唯一的


其中

List<Application> applications = this.getSortedApplications();

获取所有的application,判断哪一个服务需要续约的时候是通过遍历的方式,当list里的ApplicationName和传入的name相同时再把appication下的所有instance全部拿到,找出instanceidserverId相同的就知道该为哪一个instance进行续约了


this.publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId, instance, isReplication));

发布一个续约成功的event


最后进入到return后面调用的renew函数里

public boolean renew(String appName, String id, boolean isReplication) {
    if (super.renew(appName, id, isReplication)) {
        this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Heartbeat, appName, id, (InstanceInfo)null, (InstanceStatus)null, isReplication);
        return true;
    } else {
        return false;
    }
}

replicateToPeers表示高可用注册中心有多个中心节点,需要向peer同步,继续进到父类的renew方法:

public boolean renew(String appName, String id, boolean isReplication) {
    EurekaMonitors.RENEW.increment(isReplication);
    Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);
    Lease<InstanceInfo> leaseToRenew = null;
    if (gMap != null) {
        leaseToRenew = (Lease)gMap.get(id);
    }

    if (leaseToRenew == null) {
        EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
        logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
        return false;
    } else {
        InstanceInfo instanceInfo = (InstanceInfo)leaseToRenew.getHolder();
        if (instanceInfo != null) {
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(instanceInfo, leaseToRenew, isReplication);
            if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}; re-register required", instanceInfo.getId());
                EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
                return false;
            }

            if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                logger.info("The instance status {} is different from overridden instance status {} for instance {}. Hence setting the status to overridden status", new Object[]{instanceInfo.getStatus().name(), instanceInfo.getOverriddenStatus().name(), instanceInfo.getId()});
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
            }
        }

        this.renewsLastMin.increment();
        leaseToRenew.renew();
        return true;
    }
}

Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);

通过appName得到所有的租约,因为现在只有一个节点,所以租约只是1,如果租约不为空则通过serverId拿到租约

leaseToRenew = (Lease)gMap.get(id);

租约不为空,先获得到instance的信息:

InstanceInfo instanceInfo = (InstanceInfo)leaseToRenew.getHolder();
  • 如果instance的状态是UNKNOWN,则EurekaMonitors.RENEW_NOT_FOUND增加isReplication
  • 如果instance和当前的instance不相同(之前是down,现在发来心跳包是up),需要执行
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
public synchronized void setStatusWithoutDirty(InstanceInfo.InstanceStatus status) {
    if (this.status != status) {
        this.status = status;
    }
}

这里是将status设置到instanceInfo里。


下面的方法是记录过去一分钟有多少租约被更新了:
this.renewsLastMin.increment();

更新租约:
leaseToRenew.renew();
public void renew() {
    this.lastUpdateTimestamp = System.currentTimeMillis() + this.duration;
}

这里仅仅是将lastUpdateTimestamp进行更新


回到InstanceResourcerenewLease方法里:
此时如果renew的逻辑不成功,那么返回给客户端NOT_FOUNDrenew成功则继续流程

Response response;
if (lastDirtyTimestamp != null && this.serverConfig.shouldSyncWhenTimestampDiffers()) {
    response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
    if (response.getStatus() == Status.NOT_FOUND.getStatusCode() && overriddenStatus != null && !InstanceStatus.UNKNOWN.name().equals(overriddenStatus) && isFromReplicaNode) {
        this.registry.storeOverriddenStatusIfRequired(this.app.getAppName(), this.id, InstanceStatus.valueOf(overriddenStatus));
    }
} else {
    response = Response.ok().build();
}

logger.debug("Found (Renew): {} - {}; reply status={}", new Object[]{this.app.getName(), this.id, response.getStatus()});
return response;

lastDirtyTimestamp表示最近一次和服务端出现脏数据的时间戳,是从客户端发来的


如果lastDirtyTimestamp不为空且设置了需要做数据同步,则进入if逻辑,先验证一下

response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
private Response validateDirtyTimestamp(Long lastDirtyTimestamp, boolean isReplication) {
    InstanceInfo appInfo = this.registry.getInstanceByAppAndId(this.app.getName(), this.id, false);
    if (appInfo != null && lastDirtyTimestamp != null && !lastDirtyTimestamp.equals(appInfo.getLastDirtyTimestamp())) {
        Object[] args = new Object[]{this.id, appInfo.getLastDirtyTimestamp(), lastDirtyTimestamp, isReplication};
        if (lastDirtyTimestamp > appInfo.getLastDirtyTimestamp()) {
            logger.debug("Time to sync, since the last dirty timestamp differs - ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}", args);
            return Response.status(Status.NOT_FOUND).build();
        }

        if (appInfo.getLastDirtyTimestamp() > lastDirtyTimestamp) {
            if (isReplication) {
                logger.debug("Time to sync, since the last dirty timestamp differs - ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}", args);
                return Response.status(Status.CONFLICT).entity(appInfo).build();
            }

            return Response.ok().build();
        }
    }

    return Response.ok().build();
}

先根据appNameserverId获取InstanceInfo,如果产生了一段时间不同步的情况

  • 客户端发来的脏数据时间晚于服务端保存的脏数据时间,则客户端发生了事情没告诉服务端,返回NOT_FOUND
  • 服务端保存的脏数据时间比客户端发来的脏数据时间更新,则说明服务端保存的是新数据,如果是其他注册中心同步过来的则会返回CONFLICT;如果是客户端发过来的则直接返回OK

回到InstanceResourcerenewLease方法里:

if (response.getStatus() == Status.NOT_FOUND.getStatusCode() && overriddenStatus != null && !InstanceStatus.UNKNOWN.name().equals(overriddenStatus) && isFromReplicaNode) {
    this.registry.storeOverriddenStatusIfRequired(this.app.getAppName(), this.id, InstanceStatus.valueOf(overriddenStatus));
}

这个if进不去,此后服务续约的流程就完成了

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【SA8295P 源码分析】03 - SA8295P QNX Host 上电开机流程分析

【SA8295P 源码分析】03 - SA8295P QNX Host上电开机流程分析 一、阶段1 固件开机自检 (SM BIST):APPS PBL加载XBL后触发 INT_RESET进行Warm Reset二、阶段2 固件开机自检 (SM BIST):加载TZ,初始Hypervisor,启动QNX Kernel,加载并启动各子系统系列文章汇总见:《【SA8295P…

如何用前端技术打造自己的2048游戏

部分数据来源&#xff1a;ChatGPT 2048游戏规则 2048是一款数字益智类游戏&#xff0c;玩家需要通过合并数字方块来获得更高的分数。游戏的规则非常简单&#xff0c;只需要使用上下左右方向键移动数字方块&#xff0c;当两个相同数字方块碰撞时&#xff0c;会合并成一个数字方…

【KVM虚拟化】· KVM中的网络

目录 &#x1f34e;虚拟机的网络模式 &#x1f352;网络配置文件 &#x1f352;virsh查看命令 &#x1f34e;基于NAT的虚拟网络 &#x1f34e;基于网桥的虚拟网络 &#x1f34e;基于隔离的虚拟网络 &#x1f990;博客主页&#xff1a;大虾好吃吗的博客 &#x1f990;专栏地址&a…

全网最全的多模态实体识别论文列表-【原文+代码】

文章目录 写在前面的话【2017年】【2018年】【2019年】【2020年】【2021年】【2022年】【2023年】【写在最后的话】 写在前面的话 近期在梳理多模态NER相关的论文&#xff0c;因此&#xff0c;本篇文章主要是为大家整理了比较全面的聚焦于多模态实体识别任务的论文列表&#x…

Linux权限相关介绍

目录 前言 1.Linux操作系统下的两种用户 Linux权限管理 Linux文件访问对象分类 文件类型和访问权限 文件类型 基本权限 文件访问权限的相关设置方法 chmod chown chgrp umask掩码 目录权限 粘滞位 前言 权限指的就是我们对于某件事物所能够相关操作&#xff0c;而对于…

x86汇编语法基础(gnu格式)

一、寄存器 1.1 通用寄存器 一个x86-64的中央处理单元&#xff08;CPU&#xff09;包含一组16个存储64位值的通用寄存器。这些寄存器用来存储整数数据和指针。下图显示了这16个寄存器。它们的名字都以%r开头&#xff0c;不过后面还跟着不同命名规则的名字&#xff0c;这是由于…

macbook2023系统清理软件cleanmymac中文版

cleanmymac x 中文版基本都是大家首选Mac清理软件了。它集各种功能于一身&#xff0c;几乎满足用户所有的清理需求。它可以清理&#xff0c;优化&#xff0c;保养和监测您的电脑&#xff0c;确保您的Mac运行畅通无阻&#xff01;支持一键快速清理Mac&#xff0c;快速检查并安全…

opencv_c++学习(二十二)

一、凸包检测 图中左侧为边缘检测的效果&#xff0c;中间为图像经过二值化的效果&#xff0c;右图为凸包检测效果。 convexHull(lnputArraypoints, OutputArray hull&#xff0c;bool clockwise false, bool returnPoints true)points:输入的2D点集。 hull:输出凸包的顶点。…

【大学物理实验】基本测量

50分度的游标卡尺&#xff0c;最小分度为&#xff1a; A. 0.1mm B. 0.2mm C. 0.5mm D. 0.02mm 正确答案&#xff1a; D 保存游标卡尺和螺旋测微器是&#xff0c;下面说法正确的是&#xff1a; A. 游标卡尺测量位置应闭合&#xff0c;螺旋测微器小砧和螺杆间隙也应闭合 B. 游标…

PyG的Planetoid无法直接下载Cora等数据集的解决方法

问题描述&#xff1a; 在使用PyG的时候&#xff0c;通常会涉及到一些公共数据集的下载&#xff0c;由于网络问题&#xff0c;导致无法下载出现以下问题&#xff1a; 尝试了很多的方法都没有成功&#xff08;主要是个人比较菜&#xff01;&#xff09;。但是皇天不负有心人&am…

基于Springboot的高校固定资产管理系统的设计与实现(源码完整)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据你想解决的问题&#xff0c;今天给…

1.标注自己的关键点检测数据集

1.标注自己的关键点检测数据集 1.1 labelme标注数据 labelme GitHub项目地址&#xff1a;https://github.com/wkentaro/labelme 1.1.1exe文件下载 https://github.com/wkentaro/labelme/releases 可直接下载打包好的exe文件 1.1.2python安装labelme cmd命令行中输入以下命…

极大似然估计法及其损失函数的优化方法

二分类-逻辑回归模型 1.模型函数 1&#xff09;多元线性回归函数&#xff1a; Z ^ 计算 X W T \hat{Z}_{计算} XW^T Z^计算​XWT 2&#xff09;softmax函数&#xff1a; Y ^ 模型 S i g m o i d ( Z ^ 计算 ) 1 1 e − Z ^ 计算 \hat{Y}_{模型} Sigmoid(\hat{Z}_{计算})…

继瑞吉外卖后的又一个项目——SpringBoot+Vued前后端的博客系统

文章目录 博客系统项目介绍前言项目演示前台演示后台演示 组织结构后端组织结构前端组织结构 技术选型前端技术后端技术架构图系统架构图业务架构图 模块介绍前端模块后端模块 环境搭建开发工具开发环境项目运行 未完待续结语 博客系统项目介绍 前言 本项目已开源在Gitee 后端…

【P32】JMeter While 控制器(While Controller)

文章目录 一、While 控制器&#xff08;While Controller&#xff09;参数说明二、测试计划设计2.1、变量2.2、函数2.2.1、groovy脚本2.2.2、jex13脚本2.2.3、js脚本 一、While 控制器&#xff08;While Controller&#xff09;参数说明 可以对部分逻辑按变量条件进行循环迭代…

【算法】—— 简单多状态 dp 问题

在上期&#xff0c;我给大家讲解了关于单个状态下的dp问题&#xff0c;本期我给大家讲述几道关于多状态下的dp问题。希望大家有所帮助&#xff01;&#xff01;&#xff01; 目录 &#xff08;一&#xff09;粉刷房⼦ &#xff08;二&#xff09;买卖股票的最佳时机含冷冻期 …

破解提升 LLMs 性能的黑匣子—— LlamaIndex

“可以将 LlamaIndex 视为外部数据和 LLM 连接在一起的黑匣子。”在 Zilliz 组织的网络研讨会中&#xff0c;LlamaIndex 的联合创始人兼首席执行官 Jerry Liu 曾这样说道。 对于 Jerry Liu 的这个比喻&#xff0c;熟悉 LLMs 的开发者会觉得颇为贴切&#xff0c;尤其是对于那些想…

Chinese-LLaMA-Alpaca代码实战

文章目录 微调chinese-alpaca部署llama.cpp将FP16模型量化为4-bit 项目地址&#xff1a; https://github.com/ymcui/Chinese-LLaMA-Alpaca 微调chinese-alpaca 本项目基于中文数据 开源了使用中文文本数据预训练的中文LLaMA大模型&#xff08;7B、13B&#xff09;开源了进一…

JavaFX【TableView使用详解】

目录 概述 组件 Student ObservableList TableView setCellValueFactory() TableColumn 1. Callback 2. PropertyValueFactory 增加到末行 1、tableView.getItems().add(Student s) 2、list.add(Student s) 删除指定行 1、tableView.getItems().remove(int i) 2、…

软件测试完后,运行后还有BUG,测试人员就应该背锅吗?

测试完成后还有bug&#xff0c;测试人员肯定是有责任的&#xff0c;第一时间要赶紧处理而不是着急甩锅。但是这口锅全部扣测试身上&#xff0c;明显也是不能接受的&#xff0c;关键在于测试人员需要找出足够的证据来保护自己。 或许很多人会说测试不可能发现所有的bug&#xf…