Skywalking 中 Agent 自动同步配置源码解析

news2024/11/24 13:42:29

文章目录

    • 前言
    • 正文
      • 实现架构
      • 实现模型
      • OAP 同步 Apollo
        • ConfigWatcherRegister
        • ConfigChangeWatcher
      • Agent 侧

前言

本文代码 OAP 基于 v9.7,Java Agent 基于 v9.1,配置中心使用 apollo。

看本文需要配合代码“食用”。

正文

Skywalking 中就使用这种模型实现了 Agent 同步Apollo 配置,本文介绍下提供的功能以及代码实现,一起学习下。

Skywalking 支持 agent 动态更新配置,使 agent 可以依据业务需求进行自定义配置;更重要的是建立起这一个通信机制,那么 agent 的可管理性、扩展性都大大提升。

目前 Skywalking 提供了以下配置项

在这里插入图片描述

按照文档描述,主要为以下内容:

  • 控制采样速率

  • 忽略指定后缀的请求,注意必须是 first span 的 opretationName 匹配到

    针对 web 服务,有些静态资源是放在服务端,那么可以过滤掉这些请求

  • 忽略某些 path 的 trace

  • 限定每个 segment 中的 span 最大数量

  • 是否收集执行 sql 的参数

样例配置

configurations:
  serviceA:
    trace.sample_n_per_3_secs: 1000
    trace.ignore_path: /a/b/c,/a1/b1/c1
  serviceB:
    trace.sample_n_per_3_secs: 1000
    trace.ignore_path: /a/b/c,/a1/b1/c1

注意:这个是按照服务来进行逐项配置,如果不需要变动,不要添加对应 key,会使用默认值。

实现架构

  • OAP 同步 Apollo 配置

  • Agent 同步 OAP 配置。

每阶段的操作无关联,都是作为 Client 的一端发起的请求来同步数据。

实现模型

配置动态变更实际上是一个订阅发布模型,简单描述就是有发布者和订阅者两种角色,之间交互一般是:有一个注册接口,方便订阅者注册自身,以及发布者可以获取到订阅者列表;一个通知接口,方便发布者发送消息给订阅者。

例如需要订水,只要给订水公司留下自己的电话、地址及数量(发布者知道如何找到你),之后就有人送水上门(有水时进行派送)。

这种模型理解起来很简单,实现上难度也不大,且使用场景很广泛。

OAP 同步 Apollo

首先看下 OAP 是如何同步 apollo 数据。

ConfigWatcherRegister

这是一个抽象类,代表的是配置中心的角色,实现上有 apollo、nacos、zk 等方式。

在这里插入图片描述

先看下 notifySingleValue 方法:

protected void notifySingleValue(final ConfigChangeWatcher watcher, ConfigTable.ConfigItem configItem) {
    String newItemValue = configItem.getValue();
    if (newItemValue == null) {
        if (watcher.value() != null) {
            // Notify watcher, the new value is null with delete event type.
            // 调用 watcher 的 notify 进行处理 
            watcher.notify(
                new ConfigChangeWatcher.ConfigChangeEvent(null, ConfigChangeWatcher.EventType.DELETE));
        } else {
            // Don't need to notify, stay in null.
        }
    } else {
        if (!newItemValue.equals(watcher.value())) {
            watcher.notify(new ConfigChangeWatcher.ConfigChangeEvent(
                newItemValue,
                ConfigChangeWatcher.EventType.MODIFY
            ));
        } else {
            // Don't need to notify, stay in the same config value.
        }
    }
}

该方法的逻辑是:读取 configItem 中的值,并且与 watcher 中的值进行比较,不相等之后判定是 DELETE、还是 UPDATE 操作,并封装成一个 ConfigChangeEvent 发送给 ConfigChangeWatcher,那么可以看出 ConfigChangeWatcher 是个订阅者的角色。

继续看下调用 notifySingleValue 方法的地方:

FetchingConfigWatcherRegister#singleConfigsSync

private final Register singleConfigChangeWatcherRegister = new Register();

public abstract Optional<ConfigTable> readConfig(Set<String> keys);

private void singleConfigsSync() {
  	// 1. 读取配置数据
    Optional<ConfigTable> configTable = readConfig(singleConfigChangeWatcherRegister.keys());

    // Config table would be null if no change detected from the implementation.
    configTable.ifPresent(config -> {
        config.getItems().forEach(item -> {
          	// 2. 遍历获取配置中的 itemName
            String itemName = item.getName();
          	// 3. 依据 itemName 找到 WatcherHolder
            WatcherHolder holder = singleConfigChangeWatcherRegister.get(itemName);
            if (holder == null) {
                return;
            }
            ConfigChangeWatcher watcher = holder.getWatcher();
          	// 从 WatcherHolder 得到 ConfigChangeWatcher,发送通知
            notifySingleValue(watcher, item);
        });
    });
}

该方法执行的逻辑就是:

  1. 依据 singleConfigChangeWatcherRegister.keys() 作为参数读取配置信息
  2. 遍历配置信息,依据配置中的 name(即 itemName)找到 WatcherHolder,进而获取 ConfigChangeWatcher
  3. 调用 notifySingleValue。

readConfig 是个抽象方法,由具体的配置中心插件实现,本例中使用的 apollo,具体实现就是 ApolloConfigWatcherRegister。

读取到的内容类型 ConfigTable,并且可以知道是存储的 k-v 集合,那么 ConfigItem 就是每个配置项,itemName 就是 apollo 中配置的 key。

再看看调用 singleConfigsSync 的逻辑:

// FetchingConfigWatcherRegister.java

public void start() {
    isStarted = true;

    Executors.newSingleThreadScheduledExecutor()
             .scheduleAtFixedRate(
                 new RunnableWithExceptionProtection(
                     this::configSync, // 启动定时任务来执行
                     t -> log.error("Sync config center error.", t)
                 ), 0, syncPeriod, TimeUnit.SECONDS);
}

void configSync() {
    singleConfigsSync();
    groupConfigsSync();
}

再回到 singleConfigsSync 中,读取配置时需要先获取到配置项的 key 的集合:singleConfigChangeWatcherRegister.keys()

先看下 singleConfigChangeWatcherRegister 的具体实现:FetchingConfigWatcherRegister$Register 内部就是一个 Map<String, WatcherHolder> 来存储。

static class Register {
    private Map<String, WatcherHolder> register = new HashMap<>();

    private boolean containsKey(String key) {
        return register.containsKey(key);
    }

    private void put(String key, WatcherHolder holder) {
        register.put(key, holder);
    }

    public WatcherHolder get(String name) {
        return register.get(name);
    }

    public Set<String> keys() {
        return register.keySet();
    }
}

有读取就有存储,看看调用 put 逻辑:

// FetchingConfigWatcherRegister
synchronized public void registerConfigChangeWatcher(ConfigChangeWatcher watcher) {
    WatcherHolder holder = new WatcherHolder(watcher);
    if (singleConfigChangeWatcherRegister.containsKey(
        holder.getKey()) || groupConfigChangeWatcherRegister.containsKey(holder.getKey())) {
    }

    switch (holder.getWatcher().getWatchType()) {
        case SINGLE:
        		// put 调用
            singleConfigChangeWatcherRegister.put(holder.getKey(), holder);
            break;
        case GROUP:
            groupConfigChangeWatcherRegister.put(holder.getKey(), holder);
            break;
      default:
    }
}

registerConfigChangeWatcher 方法,用于注册 ConfigChangeWatcher ,内部处理逻辑:先将 watcher 放入 watchHolder 中,再以 holder key 分开存储 holder(放入 FetchingConfigWatcherRegister$Register 中)。

WatcherHolder 是 ConfigWatcherRegister 一个内部类,代码如下,重点是 key 生成规则:String.join(".", watcher.getModule(), watcher.getProvider().name(), watcher.getItemName());,每个 itemName 对应一个 watcher。

@Getter
protected static class WatcherHolder {
    private ConfigChangeWatcher watcher;
    private final String key;

    public WatcherHolder(ConfigChangeWatcher watcher) {
        this.watcher = watcher;
        this.key = String.join(
            ".", watcher.getModule(), watcher.getProvider().name(),
            watcher.getItemName()
        );
    }
}

总结:OAP 启动定时任务,同步 apollo 的配置数据,遍历每个配置项(configItem),找到对应的 ConfigChangerWater,将 watcher 中的值与 configItem 中的值进行比较,不相等之后继续判定是 DELETE、还是 UPDATE 操作,封装成一个 ConfigChangeEvent 发送给对应的 ConfigChangeWatcher。

ConfigChangeWatcher

抽象类,依据命名,表示的是关注配置变化的 watcher,是 OAP 中定义的用于对不同配置的具体实现;对于 Apollo 上的每个 Key 都有对应的 ConfigChangeWatcher。

在这里插入图片描述

具体的 ConfigChangeWatcher 获取到 ConfigChangeEvent,处理逻辑各有不同,本次具体看下 AgentConfigurationsWatcher。

private volatile String settingsString;

private volatile AgentConfigurationsTable agentConfigurationsTable;

public void notify(ConfigChangeEvent value) {
    if (value.getEventType().equals(EventType.DELETE)) {
        settingsString = null;
        this.agentConfigurationsTable = new AgentConfigurationsTable();
    } else {
        settingsString = value.getNewValue();
        AgentConfigurationsReader agentConfigurationsReader =
            new AgentConfigurationsReader(new StringReader(value.getNewValue()));
        this.agentConfigurationsTable = agentConfigurationsReader.readAgentConfigurationsTable();
    }
}

方法逻辑为:config value 存储到了 agentConfigurationsTable。

apollo value 是什么样子呢?

configurations:
  serviceA:
    trace.sample_n_per_3_secs: 1000
    trace.ignore_path: /a/b/c,/a1/b1/c1
  serviceB:
    trace.sample_n_per_3_secs: 1000
    trace.ignore_path: /a/b/c,/a1/b1/c1

AgentConfigurationsTable 如下具体实现

public class AgentConfigurationsTable {
    private Map<String, AgentConfigurations> agentConfigurationsCache;

    public AgentConfigurationsTable() {
        this.agentConfigurationsCache = new HashMap<>();
    }
}

public class AgentConfigurations {
    private String service;
    private Map<String, String> configuration;
    /**
     * The uuid is calculated by the dynamic configuration of the service.
     */
    private volatile String uuid;

    public AgentConfigurations(final String service, final Map<String, String> configuration, final String uuid) {
        this.service = service;
        this.configuration = configuration;
        this.uuid = uuid;
    }
}

将 agentConfigurationsTable 转换成 json 展示更容里理解数据存储的结构:

{
    "serviceB": {
        "service": "serviceB",
        "configuration": {
            "trace.sample_n_per_3_secs": "1000",
            "trace.ignore_path": "/a/b/c,/a1/b1/c1"
        },
        "uuid": "92670f1ccbdee60e14ffc0"
    },
    "serviceA": {
        "service": "serviceA",
        "configuration": {
            "trace.sample_n_per_3_secs": "1000",
            "trace.ignore_path": "/a/b/c,/a1/b1/c1"
        },
        "uuid": "92670f1ccbdee60e14ffc0"
    }
}

查看读取 agentConfigurationsTable 值的逻辑:

// AgentConfigurationsWatcher#getAgentConfigurations
public AgentConfigurations getAgentConfigurations(String service) {
  	// 依据 service 获取数据
    AgentConfigurations agentConfigurations = this.agentConfigurationsTable.getAgentConfigurationsCache().get(service);
    if (null == agentConfigurations) {
        return emptyAgentConfigurations;
    } else {
        return agentConfigurations;
    }
}

继续查看调用 getAgentConfigurations 的代码,并且将 value 包装成 ConfigurationDiscoveryCommand 返回。

// ConfigurationDiscoveryServiceHandler#fetchConfigurations
public void fetchConfigurations(final ConfigurationSyncRequest request,
                                final StreamObserver<Commands> responseObserver) {
    Commands.Builder commandsBuilder = Commands.newBuilder();

    AgentConfigurations agentConfigurations = agentConfigurationsWatcher.getAgentConfigurations(
        request.getService());
    if (null != agentConfigurations) {
        // 请求时会带有 uuid,会跟现有配置的 uuid 进行比对,如果不同,则获取最新值 
        if (disableMessageDigest || !Objects.equals(agentConfigurations.getUuid(), request.getUuid())) {
            ConfigurationDiscoveryCommand configurationDiscoveryCommand =
                newAgentDynamicConfigCommand(agentConfigurations);
            commandsBuilder.addCommands(configurationDiscoveryCommand.serialize().build());
        }
    }
    responseObserver.onNext(commandsBuilder.build());
    responseObserver.onCompleted();
}

ConfigurationDiscoveryServiceHandler 属于 GRPCHandler,类似 SpringBoot 中 Controller,暴露接口,外部就可以获取数据。

ConfigurationDiscoveryCommand 这个方法中有个属性来标识 command 的具体类型,这个在 agent 端接收到 command 需要依据 command 类型找到真正的处理器。

public static final String NAME = "ConfigurationDiscoveryCommand";

总结:当 AgentConfigurationsWatcher 收到订阅的 ConfigChangeEvent 时,会将值存储至 AgentConfigurationsTable,之后通过 ConfigurationDiscoveryServiceHandler 暴露接口,以方便 agent 可以获取到相应服务的配置。

至此,OAP 与 Apollo 间的配置更新逻辑以及值的处理逻辑大致理清了。

接下来看看 agent 与 oap 间的交互。

Agent 侧

找到调用 ConfigurationDiscoveryServiceGrpc#fetchConfigurations 的代码,看到 ConfigurationDiscoveryService,查看具体调用逻辑:

// ConfigurationDiscoveryService
private void getAgentDynamicConfig() {

    if (GRPCChannelStatus.CONNECTED.equals(status)) {
        try {
          	// 准备参数
            ConfigurationSyncRequest.Builder builder = ConfigurationSyncRequest.newBuilder();
            builder.setService(Config.Agent.SERVICE_NAME);

            if (configurationDiscoveryServiceBlockingStub != null) {
                final Commands commands = configurationDiscoveryServiceBlockingStub.withDeadlineAfter(
                    GRPC_UPSTREAM_TIMEOUT, TimeUnit.SECONDS
                ).fetchConfigurations(builder.build()); // 方法调用
              	// 结果处理
                ServiceManager.INSTANCE.findService(CommandService.class).receiveCommand(commands);
            }
        } catch (Throwable t) {
        }
    }
}

而 getAgentDynamicConfig 是在 ConfigurationDiscoveryService#boot 执行时 init 了一个定时任务调用。

public void boot() throws Throwable {
    getDynamicConfigurationFuture = Executors.newSingleThreadScheduledExecutor(
        new DefaultNamedThreadFactory("ConfigurationDiscoveryService")
    ).scheduleAtFixedRate(
        new RunnableWithExceptionProtection(
            this::getAgentDynamicConfig,
            t -> LOGGER.error("Sync config from OAP error.", t)
        ),
        Config.Collector.GET_AGENT_DYNAMIC_CONFIG_INTERVAL,
        Config.Collector.GET_AGENT_DYNAMIC_CONFIG_INTERVAL,
        TimeUnit.SECONDS
    );
}

获取结果后的处理逻辑:CommandService 接收 Commands,先是放入到队列中,

private LinkedBlockingQueue<BaseCommand> commands = new LinkedBlockingQueue<>(64);

public void receiveCommand(Commands commands) {
    for (Command command : commands.getCommandsList()) {
        try {
            BaseCommand baseCommand = CommandDeserializer.deserialize(command);
						// 将结果放入队列中
            boolean success = this.commands.offer(baseCommand);

            if (!success && LOGGER.isWarnEnable()) {
            }
        } catch (UnsupportedCommandException e) {
        }
    }
}

新开线程来消费队列,commandExecutorService 处理 Commands,通过代码调用链看到,最后依据 command 的类型找到真正指令执行器。

// CommandService#run
public void run() {
    final CommandExecutorService commandExecutorService = ServiceManager.INSTANCE.findService(CommandExecutorService.class);

    while (isRunning) {
        try {
            // 消费队列
            BaseCommand command = this.commands.take();
						// 判断是否已经执行过了
            if (isCommandExecuted(command)) {
                continue;
            }
						// 分发 command
            commandExecutorService.execute(command);
            serialNumberCache.add(command.getSerialNumber());
        } catch (CommandExecutionException e) {
        }
    }
}

// CommandExecutorService#execute
public void execute(final BaseCommand command) throws CommandExecutionException {
    this.executorForCommand(command).execute(command);
}
// CommandExecutorService#executorForCommand
private CommandExecutor executorForCommand(final BaseCommand command) {
    final CommandExecutor executor = this.commandExecutorMap.get(command.getCommand());
    if (executor != null) {
        return executor;
    }
    return NoopCommandExecutor.INSTANCE;
}

依据指令类型获取具体的指令执行器,这里为 ConfigurationDiscoveryService,发现又调用了 ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand 处理。

// ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand
public void handleConfigurationDiscoveryCommand(ConfigurationDiscoveryCommand configurationDiscoveryCommand) {
    final String responseUuid = configurationDiscoveryCommand.getUuid();

    List<KeyStringValuePair> config = readConfig(configurationDiscoveryCommand);
		// 遍历配置项
    config.forEach(property -> {
        String propertyKey = property.getKey();
        List<WatcherHolder> holderList = register.get(propertyKey);
        for (WatcherHolder holder : holderList) {
            if (holder != null) {
              	// 依据配置项找到对应的 AgentConfigChangeWatcher,封装成 ConfigChangeEvent 
                AgentConfigChangeWatcher watcher = holder.getWatcher();
                String newPropertyValue = property.getValue();
                if (StringUtil.isBlank(newPropertyValue)) {
                    if (watcher.value() != null) {
                        // Notify watcher, the new value is null with delete event type.
                        watcher.notify(
                                new AgentConfigChangeWatcher.ConfigChangeEvent(
                                        null, AgentConfigChangeWatcher.EventType.DELETE
                                ));
                    }
                } else {
                    if (!newPropertyValue.equals(watcher.value())) {
                        watcher.notify(new AgentConfigChangeWatcher.ConfigChangeEvent(
                                newPropertyValue, AgentConfigChangeWatcher.EventType.MODIFY
                        ));
                    }
                }
            }
        }
    });
    this.uuid = responseUuid;
}

ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand 进行处理,遍历配置项列表,依据 Key 找到对应的 AgentConfigChangeWatcher,进行 notify。

这个过程是不是很熟悉,跟 OAP 中处理逻辑不能说是完全一样,简直一模一样。

AgentConfigChangeWatcher 是个抽象类,查看其具体实现,关注其注册以及处理 value 的逻辑即可。
在这里插入图片描述

具体逻辑就不再展开细说了,需要自行了解下。

总之,agent 可以进行动态配置,能做的事情就多了,尤其是对 agent.config 中的配置大部分就可以实现动态管理了。

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

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

相关文章

基于SSM在线协同过滤汽车推荐销售系统

SSM毕设分享 基于SSM在线协同过滤汽车推荐销售系统 1 项目简介 Hi&#xff0c;各位同学好&#xff0c;这里是郑师兄&#xff01; 今天向大家分享一个毕业设计项目作品【】 师兄根据实现的难度和等级对项目进行评分(最低0分&#xff0c;满分5分) 难度系数&#xff1a;3分 工作…

Springsecurty【2】认证连接MySQL

1.前期准备 基于Spring Initializr创建SpringBoot项目&#xff08;基于SpringBoot 2.7.12版本&#xff09;&#xff0c;实现与MyBatisPlus的项目整合。分别导入&#xff1a;CodeGenerator和MyBatisPlusConfig。 CodeGenerator&#xff1a;用于MybatisPlus代码生成&#xff1b;…

JavaScript原型,原型链 ? 有什么特点?

一、原型 JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象 当试图访问一个对象的属性时&#xff0c;它不仅仅在该对象上搜寻&#xff0c;还会搜寻该对象的原型&#xff0c;以及该对象的原型的原型&#xff0c;依次层层向上搜索&#xff0c;直到找到一个…

<各国地图轮廓app>技术支持

如在app使用过程中遇到任何问题&#xff0c;请与开发者联系caohechunhotmail.com

贴片晶振无源石英谐振器直插晶振

贴片晶振 贴片晶振3.579M~25MHz无源石英谐振器直插晶振 文章目录 贴片晶振前言一、贴片晶振3.579M~25MHz无源石英谐振器直插晶振二、属性三、技术参数总结前言 贴片晶振(Surface Mount Crystal Oscillator)是一种采用表面贴装技术进行安装的晶振。它的主要特点是封装小巧、安…

【黑马甄选离线数仓day10_会员主题域开发_DWS和ADS层】

day10_会员主题域开发 会员主题_DWS和ADS层 DWS层开发 门店会员分类天表: 维度指标: 指标&#xff1a;新增注册会员数、累计注册会员数、新增消费会员数、累计消费会员数、新增复购会员数、累计复购会员数、活跃会员数、沉睡会员数、会员消费金额 维度: 时间维度&#xff08…

SpringBoot增删改查接口实例

前言 增删改查是后端最基本的技能。下面我就带领小伙伴们通过一个简单的示例来讲解SpringBoot的增删改查。Spring Boot框架层次从上至下可分为5层&#xff1a;分别为View层&#xff0c;Controller层&#xff0c;Service层&#xff0c;Mapper层&#xff0c;Model层 1. View层&a…

机械臂快速接触刚性环境阻抗对相互作用力的影响

当机械臂快速接触刚性环境时&#xff0c;阻抗对相互作用力的影响尤为显著。由于刚性环境对机械臂产生的阻力&#xff0c;机械臂在接触时会受到一个与运动方向相反的作用力&#xff0c;即接触力。阻抗参数的设置对接触力的大小具有重要影响。 一方面&#xff0c;阻尼参数决定了…

求三角形面积 C语言xdoj91

题目描述&#xff1a; 输入三角形的三边长&#xff0c;计算三角形的面积&#xff0c;结果保留两位小数。 边长分别为a,b,c&#xff0c;三角形的面积公式为ssqrt(p(p-a)(p-b)(p-c)),其中p(abc)/2。 输入格式&#xff1a;共一行&#xff0c;输入三个数&#xff0c;保留两位小数&a…

SQL注入【sqli靶场第23-28关】(七)

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 0、总体思路 先确认是否可以SQL注入&#xff0c;使用单…

Apache Commons IO: 简化文件和IO操作

第1章&#xff1a;引言 咱们在做Java编程的时候&#xff0c;经常会遇到各种文件操作和输入输出&#xff08;IO&#xff09;的问题。不论是读取一个配置文件&#xff0c;还是把数据写入日志&#xff0c;这些看似简单的任务有时候会让人头疼。传统的Java IO操作&#xff0c;虽然…

多维时序 | MATLAB实CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测

多维时序 | MATLAB实现CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测 目录 多维时序 | MATLAB实现CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测预测效果基本介…

IP地址定位解析

文章目录 1 IP地址定位解析1.1 获取用户 IP 地址1.2 淘宝库获取用户 IP 地址属地1.3 通过 Ip2region 定位1.3.1 Ip2region 介绍1.3.2 Ip2region 特性1.3.3 客户端实现1.3.3.1 引入 Maven 仓库1.3.3.2 基于文件查询1.3.3.3 缓存VectorIndex索引1.3.3.4 缓存整个 xdb 数据 1 IP地…

Portainer.io:让容器管理变得更加直观

在现代软件开发和部署中&#xff0c;容器化技术已经变得越来越流行。Docker 是其中一种领先的容器化平台&#xff0c;而 Portainer.io 则是一个优秀的管理工具&#xff0c;使得 Docker 的使用变得更加简单和可视化。本文将介绍 Portainer.io 的基本功能和如何在 Docker 上安装和…

C/C++常见面试题(四)

C/C面试题集合四 目录 1、什么是C中的类&#xff1f;如何定义和实例化一个类&#xff1f; 2、请解释C中的继承和多态性。 3、什么是虚函数&#xff1f;为什么在基类中使用虚函数&#xff1f; 4、解释封装、继承和多态的概念&#xff0c;并提供相应的代码示例 5、如何处理内…

融资项目——swagger2的注解

1. ApiModel与ApiModelProperty(在实体类中使用) 如上图&#xff0c;ApiModel加在实体类上方&#xff0c;用于整体描述实体类。ApiModelProperty(value"xxx",example"xxx")放于每个属性上方&#xff0c;用于对属性进行描述。swagger2网页上的效果如下图&am…

最大化控制资源成本 - 华为OD统一考试

OD统一考试 题解: Java / Python / C++ 题目描述 公司创新实验室正在研究如何最小化资源成本,最大化资源利用率,请你设计算法帮他们解决一个任务分布问题:有taskNum项任务,每人任务有开始时间(startTime) ,结更时间(endTme) 并行度(paralelism) 三个属性,并行度是指这个…

概率中的50个具有挑战性的问题[02/50]:连续获胜

一、说明 我最近对与概率有关的问题产生了兴趣。我偶然读到了弗雷德里克莫斯特勒&#xff08;Frederick Mosteller&#xff09;的《概率论中的五十个具有挑战性的问题与解决方案》&#xff08;Fifty Challenge Problems in Probability with Solutions&#xff09;一书。我认为…

[c]扫雷

题目描述 扫雷游戏是一款十分经典的单机小游戏。在n行m列的雷区中有一些格子含有地雷&#xff08;称之为地雷格&#xff09;&#xff0c;其他格子不含地雷&#xff08;称之为非地雷格&#xff09;。 玩家翻开一个非地雷格时&#xff0c;该格将会出现一个数字——提示周围格子中…

哈希三道题

两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意…