深入分析基于Micrometer和Prometheus实现度量和监控的方案
深入分析基于Micrometer和Prometheus实现度量和监控的方案
前提#
Micrometer提供的度量类库#
MeterRegistry#
MeterRegistry
在Micrometer
是一个抽象类,主要实现包括:
- 1、
SimpleMeterRegistry
:每个Meter
的最新数据可以收集到SimpleMeterRegistry
实例中,但是这些数据不会发布到其他系统,也就是数据是位于应用的内存中的。 - 2、
CompositeMeterRegistry
:多个MeterRegistry
聚合,内部维护了一个MeterRegistry
的列表。 - 3、全局的
MeterRegistry
:工厂类io.micrometer.core.instrument.Metrics
中持有一个静态final
的CompositeMeterRegistry
实例globalRegistry
。
当然,使用者也可以自行继承MeterRegistry
去实现自定义的MeterRegistry
。SimpleMeterRegistry
适合做调试的时候使用,它的简单使用方式如下:
MeterRegistry registry = new SimpleMeterRegistry();
Counter counter = registry.counter("counter");
counter.increment();
CompositeMeterRegistry
实例初始化的时候,内部持有的MeterRegistry
列表是空的,如果此时用它新增一个Meter
实例,Meter
实例的操作是无效的:
CompositeMeterRegistry composite = new CompositeMeterRegistry();Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); // <- 实际上这一步操作是无效的,但是不会报错SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple); // <- 向CompositeMeterRegistry实例中添加SimpleMeterRegistry实例compositeCounter.increment(); // <-计数成功
全局的MeterRegistry
的使用方式更加简单便捷,因为一切只需要操作工厂类Metrics
的静态方法:
Metrics.addRegistry(new SimpleMeterRegistry());
Counter counter = Metrics.counter("counter", "tag-1", "tag-2");
counter.increment();
Tag与Meter的命名#
MeterRegistry registry = ...
registry.timer("http.server.requests");
- 1、Prometheus - http_server_requests_duration_seconds。
- 2、Atlas - httpServerRequests。
- 3、Graphite - http.server.requests。
- 4、InfluxDB - http_server_requests。
其实NamingConvention
已经提供了5种默认的转换规则:dot、snakeCase、camelCase、upperCamelCase和slashes。
MeterRegistry registry = ...
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")
这样,当我们选择命名为"database.calls"的计数器,我们可以进一步选择分组"db"或者"users"分别统计不同分组对总调用数的贡献或者组成。一个反例如下:
MeterRegistry registry = ...
registry.counter("calls", "class", "database", "db", "users");registry.counter("calls", "class", "http", "uri", "/api/users");
MeterRegistry registry = ...
registry.config().commonTags("stack", "prod", "region", "us-east-1");
// 和上面的意义是一样的
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));
像上面这样子使用,就能通过主机,实例,区域,堆栈等操作环境进行多维度深入分析。
- 1、
Tag
的值必须不为NULL。 - 2、
Micrometer
中,Tag
必须成对出现,也就是Tag
必须设置为偶数个,实际上它们以Key=Value的形式存在,具体可以看io.micrometer.core.instrument.Tag
接口:
public interface Tag extends Comparable<Tag> {String getKey();String getValue();static Tag of(String key, String value) {return new ImmutableTag(key, value);}default int compareTo(Tag o) {return this.getKey().compareTo(o.getKey());}
}
MeterRegistry registry = ...
registry.config().meterFilter(MeterFilter.ignoreTags("http")).meterFilter(MeterFilter.denyNameStartsWith("jvm"));
表示忽略"http"标签,拒绝名称以"jvm"字符串开头的Meter
。更多用法可以参详一下MeterFilter
这个类。
Meter
的命名和Meter
的Tag
相互结合,以命名为轴心,以Tag
为多维度要素,可以使度量数据的维度更加丰富,便于统计和分析。
Meters#
Counter#
MeterRegistry meterRegistry = new SimpleMeterRegistry();
Counter counter = meterRegistry.counter("http.request", "createOrder", "/order/create");
counter.increment();
System.out.println(counter.measure()); // [Measurement{statistic='COUNT', value=1.0}]
//实体
@Data
public class Order {private String orderId;private Integer amount;private String channel;private LocalDateTime createTime;
}public class CounterMain {private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");static {Metrics.addRegistry(new SimpleMeterRegistry());}public static void main(String[] args) throws Exception {Order order1 = new Order();order1.setOrderId("ORDER_ID_1");order1.setAmount(100);order1.setChannel("CHANNEL_A");order1.setCreateTime(LocalDateTime.now());createOrder(order1);Order order2 = new Order();order2.setOrderId("ORDER_ID_2");order2.setAmount(200);order2.setChannel("CHANNEL_B");order2.setCreateTime(LocalDateTime.now());createOrder(order2);Search.in(Metrics.globalRegistry).meters().forEach(each -> {StringBuilder builder = new StringBuilder();builder.append("name:").append(each.getId().getName()).append(",tags:").append(each.getId().getTags()).append(",type:").append(each.getId().getType()).append(",value:").append(each.measure());System.out.println(builder.toString());});}private static void createOrder(Order order) {//忽略订单入库等操作Metrics.counter("order.create","channel", order.getChannel(),"createTime", FORMATTER.format(order.getCreateTime())).increment();}
}
name:order.create,tags:[tag(channel=CHANNEL_A), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
name:order.create,tags:[tag(channel=CHANNEL_B), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
public class CounterBuilderMain {public static void main(String[] args) throws Exception{Counter counter = Counter.builder("name") //名称.baseUnit("unit") //基础单位.description("desc") //描述.tag("tagKey", "tagValue") //标签.register(new SimpleMeterRegistry());//绑定的MeterRegistrycounter.increment();}
}
FunctionCounter#
public class FunctionCounterMain {public static void main(String[] args) throws Exception {MeterRegistry registry = new SimpleMeterRegistry();AtomicInteger n = new AtomicInteger(0);//这里ToDoubleFunction匿名实现其实可以使用Lambda表达式简化为AtomicInteger::getFunctionCounter.builder("functionCounter", n, new ToDoubleFunction<AtomicInteger>() {@Overridepublic double applyAsDouble(AtomicInteger value) {return value.get();}}).baseUnit("function").description("functionCounter").tag("createOrder", "CHANNEL-A").register(registry);//下面模拟三次计数 n.incrementAndGet();n.incrementAndGet();n.incrementAndGet();}
}
Timer#
public interface Timer extends Meter {...void record(long var1, TimeUnit var3);default void record(Duration duration) {this.record(duration.toNanos(), TimeUnit.NANOSECONDS);}<T> T record(Supplier<T> var1);<T> T recordCallable(Callable<T> var1) throws Exception;void record(Runnable var1);default Runnable wrap(Runnable f) {return () -> {this.record(f);};}default <T> Callable<T> wrap(Callable<T> f) {return () -> {return this.recordCallable(f);};}long count();double totalTime(TimeUnit var1);default double mean(TimeUnit unit) {return this.count() == 0L ? 0.0D : this.totalTime(unit) / (double)this.count();}double max(TimeUnit var1);...
}
Timer timer = ...
timer.record(() -> dontCareAboutReturnValue());
timer.recordCallable(() -> returnValue());Runnable r = timer.wrap(() -> dontCareAboutReturnValue());
Callable c = timer.wrap(() -> returnValue());
这里举个实际的例子,要对系统做一个功能,记录指定方法的执行时间,还是用下单方法做例子:
public class TimerMain {private static final Random R = new Random();static {Metrics.addRegistry(new SimpleMeterRegistry());}public static void main(String[] args) throws Exception {Order order1 = new Order();order1.setOrderId("ORDER_ID_1");order1.setAmount(100);order1.setChannel("CHANNEL_A");order1.setCreateTime(LocalDateTime.now());Timer timer = Metrics.timer("timer", "createOrder", "cost");timer.record(() -> createOrder(order1));}private static void createOrder(Order order) {try {TimeUnit.SECONDS.sleep(R.nextInt(5)); //模拟方法耗时} catch (InterruptedException e) {//no-op}}
}
MeterRegistry registry = ...
Timer timer = Timer.builder("my.timer").description("a description of what this timer does") // 可选.tags("region", "test") // 可选.register(registry);
另外,Timer
的使用还可以基于它的内部类Timer.Sample
,通过start和stop两个方法记录两者之间的逻辑的执行耗时。例如:
Timer.Sample sample = Timer.start(registry);// 这里做业务逻辑
Response response = ...sample.stop(registry.timer("my.timer", "response", response.status()));
FunctionTimer#
public interface FunctionTimer extends Meter {static <T> Builder<T> builder(String name, T obj, ToLongFunction<T> countFunction,ToDoubleFunction<T> totalTimeFunction,TimeUnit totalTimeFunctionUnit) {return new Builder<>(name, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit);}...
}
IMap<?, ?> cache = ...; // 假设使用了Hazelcast缓存
registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache,c -> c.getLocalMapStats().getGetOperationCount(), //实际上就是cache的一个方法,记录缓存生命周期初始化的增量(个数)c -> c.getLocalMapStats().getTotalGetLatency(), // Get操作的延迟时间总量,可以理解为耗时TimeUnit.NANOSECONDS
);
public class FunctionTimerMain {public static void main(String[] args) throws Exception {//这个是为了满足参数,暂时不需要理会Object holder = new Object();AtomicLong totalTimeNanos = new AtomicLong(0);AtomicLong totalCount = new AtomicLong(0);FunctionTimer.builder("functionTimer", holder, p -> totalCount.get(), p -> totalTimeNanos.get(), TimeUnit.NANOSECONDS).register(new SimpleMeterRegistry());totalTimeNanos.addAndGet(10000000);totalCount.incrementAndGet();}
}
LongTaskTimer#
@Timed(value = "aws.scrape", longTask = true)
@Scheduled(fixedDelay = 360000)
void scrapeResources() {//这里做相对耗时的业务逻辑
}
当然,在非Spring
体系中也能方便地使用LongTaskTimer
:
public class LongTaskTimerMain {public static void main(String[] args) throws Exception{MeterRegistry meterRegistry = new SimpleMeterRegistry();LongTaskTimer longTaskTimer = meterRegistry.more().longTaskTimer("longTaskTimer");longTaskTimer.record(() -> {//这里编写Task的逻辑});//或者这样Metrics.more().longTaskTimer("longTaskTimer").record(()-> {//这里编写Task的逻辑});}
}
Gauge#
List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size);
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>());
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());
AtomicInteger n = registry.gauge("numberGauge", new AtomicInteger(0));
n.set(1);
n.set(2);
除了使用MeterRegistry
创建Gauge
之外,还可以使用建造器流式创建:
//一般我们不需要操作Gauge实例
Gauge gauge = Gauge.builder("gauge", myObj, myObj::gaugeValue).description("a description of what this gauge does") // 可选.tags("region", "test") // 可选.register(registry);
举个相对实际的例子,假设我们需要对登录后的用户发送一条短信或者推送,做法是消息先投放到一个阻塞队列,再由一个线程消费消息进行其他操作:
public class GaugeMain {private static final MeterRegistry MR = new SimpleMeterRegistry();private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);private static BlockingQueue<Message> REAL_QUEUE;static {REAL_QUEUE = MR.gauge("messageGauge", QUEUE, Collection::size);}public static void main(String[] args) throws Exception {consume();Message message = new Message();message.setUserId(1L);message.setContent("content");REAL_QUEUE.put(message);}private static void consume() throws Exception {new Thread(() -> {while (true) {try {Message message = REAL_QUEUE.take();//handle messageSystem.out.println(message);} catch (InterruptedException e) {//no-op}}}).start();}
}
上面的例子代码写得比较糟糕,只为了演示相关使用方式,切勿用于生产环境。
TimeGauge#
TimeGauge
是Gauge
的特化类型,相比Gauge
,它的构建器中多了一个TimeUnit
类型的参数,用于指定ToDoubleFunction
入参的基础时间单位。这里简单举个使用例子:
public class TimeGaugeMain {private static final SimpleMeterRegistry R = new SimpleMeterRegistry();public static void main(String[] args) throws Exception {AtomicInteger count = new AtomicInteger();TimeGauge.Builder<AtomicInteger> timeGauge = TimeGauge.builder("timeGauge", count,TimeUnit.SECONDS, AtomicInteger::get);timeGauge.register(R);count.addAndGet(10086);print();count.set(1);print();}private static void print() throws Exception {Search.in(R).meters().forEach(each -> {StringBuilder builder = new StringBuilder();builder.append("name:").append(each.getId().getName()).append(",tags:").append(each.getId().getTags()).append(",type:").append(each.getId().getType()).append(",value:").append(each.measure());System.out.println(builder.toString());});}
}//输出
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=10086.0}]
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=1.0}]
DistributionSummary#
DistributionSummary summary = registry.summary("response.size");
DistributionSummary summary = DistributionSummary.builder("response.size").description("a description of what this summary does") // 可选.baseUnit("bytes") // 可选.tags("region", "test") // 可选.scale(100) // 可选.register(registry);
public class DistributionSummaryMain {private static final DistributionSummary DS = DistributionSummary.builder("cacheHitPercent").register(new SimpleMeterRegistry());private static final LoadingCache<String, String> CACHE = CacheBuilder.newBuilder().maximumSize(1000).recordStats().expireAfterWrite(60, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {@Overridepublic String load(String s) throws Exception {return selectFromDatabase();}});public static void main(String[] args) throws Exception {String key = "doge";String value = CACHE.get(key);record();}private static void record() throws Exception {CacheStats stats = CACHE.stats();BigDecimal hitCount = new BigDecimal(stats.hitCount());BigDecimal requestCount = new BigDecimal(stats.requestCount());DS.record(hitCount.divide(requestCount, 2, BigDecimal.ROUND_HALF_DOWN).doubleValue());}
}
基于SpirngBoot、Prometheus、Grafana集成#
SpirngBoot中使用Micrometer#
我们先引入spring-boot-starter-actuator
和spring-boot-starter-web
,实现一个Counter
和Timer
作为示例。依赖:
<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.1.0.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.22</version></dependency><dependency><groupId>io.micrometer</groupId><artifactId>micrometer-registry-prometheus</artifactId><version>1.1.0</version></dependency></dependencies>
接着编写一个下单接口和一个消息发送模块,模拟用户下单之后向用户发送消息:
//实体
@Data
public class Message {private String orderId;private Long userId;private String content;
}@Data
public class Order {private String orderId;private Long userId;private Integer amount;private LocalDateTime createTime;
}//控制器和服务类
@RestController
public class OrderController {@Autowiredprivate OrderService orderService;@PostMapping(value = "/order")public ResponseEntity<Boolean> createOrder(@RequestBody Order order) {return ResponseEntity.ok(orderService.createOrder(order));}
}@Slf4j
@Service
public class OrderService {private static final Random R = new Random();@Autowiredprivate MessageService messageService;public Boolean createOrder(Order order) {//模拟下单try {int ms = R.nextInt(50) + 50;TimeUnit.MILLISECONDS.sleep(ms);log.info("保存订单模拟耗时{}毫秒...", ms);} catch (Exception e) {//no-op}//记录下单总数Metrics.counter("order.count", "order.channel", order.getChannel()).increment();//发送消息Message message = new Message();message.setContent("模拟短信...");message.setOrderId(order.getOrderId());message.setUserId(order.getUserId());messageService.sendMessage(message);return true;}
}@Slf4j
@Service
public class MessageService implements InitializingBean {private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);private static BlockingQueue<Message> REAL_QUEUE;private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();private static final Random R = new Random();static {REAL_QUEUE = Metrics.gauge("message.gauge", Tags.of("message.gauge", "message.queue.size"), QUEUE, Collection::size);}public void sendMessage(Message message) {try {REAL_QUEUE.put(message);} catch (InterruptedException e) {//no-op}}@Overridepublic void afterPropertiesSet() throws Exception {EXECUTOR.execute(() -> {while (true) {try {Message message = REAL_QUEUE.take();log.info("模拟发送短信,orderId:{},userId:{},内容:{},耗时:{}毫秒", message.getOrderId(), message.getUserId(),message.getContent(), R.nextInt(50));} catch (Exception e) {throw new IllegalStateException(e);}}});}
}//切面类
@Component
@Aspect
public class TimerAspect {@Around(value = "execution(* club.throwable.smp.service.*Service.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method method = methodSignature.getMethod();Timer timer = Metrics.timer("method.cost.time", "method.name", method.getName());ThrowableHolder holder = new ThrowableHolder();Object result = timer.recordCallable(() -> {try {return joinPoint.proceed();} catch (Throwable e) {holder.throwable = e;}return null;});if (null != holder.throwable) {throw holder.throwable;}return result;}private class ThrowableHolder {Throwable throwable;}
}
server:port: 9091
management:server:port: 10091endpoints:web:exposure:include: '*'base-path: /management
management.endpoint.${端点ID}.enabled=true/false
management.endpoints.web.exposure.include=info,health
management.endpoints.web.exposure.exclude=prometheus
Prometheus的安装和配置#
Prometheus目前的最新版本是2.5,鉴于笔者当前没深入玩过Docker
,这里还是直接下载它的压缩包解压安装。
wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-amd64.tar.gz
tar xvfz prometheus-*.tar.gz
cd prometheus-*
先编辑解压出来的目录下的Prometheus
配置文件prometheus.yml
,主要修改scrape_configs
节点的属性:
scrape_configs:# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.- job_name: 'prometheus'# metrics_path defaults to '/metrics'# scheme defaults to 'http'.# 这里配置需要拉取度量信息的URL路径,这里选择应用程序的prometheus端点metrics_path: /management/prometheusstatic_configs:# 这里配置host和port- targets: ['localhost:10091']
配置拉取度量数据的路径为localhost:10091/management/metrics
,此前记得把前一节提到的应用在虚拟机中启动。接着启动Prometheus
应用:
# 可选参数 --storage.tsdb.path=存储数据的路径,默认路径为./data
./prometheus --config.file=prometheus.yml
Prometheus
引用的默认启动端口是9090,启动成功后,日志如下:
此时,访问http://${虚拟机host}:9090/targets
就能看到当前Prometheus
中执行的Job
:
可以看到,Meter
的信息已经被收集和展示,但是显然不够详细和炫酷,这个时候就需要使用Grafana的UI做一下点缀。
Grafana的安装和使用#
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.4-1.x86_64.rpm
sudo yum localinstall grafana-5.3.4-1.x86_64.rpm
其实就是指向Prometheus服务端的端口就可以了。接下来可以天马行空地添加需要的面板,就下单数量统计的指标,可以添加一个Graph
的面板:
配置面板的时候,需要在基础(General)中指定Title:
接着比较重要的是Metrics的配置,需要指定数据源和Prometheus的查询语句:
接着添加一下项目中使用的Timer的Meter,便于监控方法的执行时间,完成之后大致如下:
上面的面板虽然设计相当粗糙,但是基本功能已经实现。设计面板并不是一件容易的事,如果有需要可以从Github
中搜索一下grafana dashboard
关键字找现成的开源配置使用或者二次加工后使用。
小结#
深入分析基于Micrometer和Prometheus实现度量和监控的方案相关推荐
- 基于DTU的强电井远程监控系统方案
一.项目背景 随着国家经济强大,综合国力的增强,现代化的高层建筑不断出现.高层建筑多采用强电井提供电力.照明用电电线.电缆.因此,强电井里面设备的正常运行关乎到某一层楼甚至整栋大楼的电力提供,强电井里 ...
- 基于云平台的智能变电站远程监控系统
智能变电站远程监控系统是通过智能网关将变电站相关数据实时地传输到通信网络中,通过网络传输,将现场的数据实时地传送到远程监控中心.通过远程监控中心可以实现现场的实时数据采集.通信和显示,并通过移动终端将 ...
- 基于 eBPF 的 prometheus 监控方案
基于 eBPF 的 prometheus 监控方案 1. 前言 2. ebpf_exporter 环境搭建 3. Prometheus 与 Grafana 配置 4. ebpf_exporter 代码 ...
- 网易云基于Prometheus的微服务监控实践
当监控遇上微服务 在过去数年里,微服务的落地一直都是业界重点关注的问题,其始终面临着部署.监控.配置和治理等方面的挑战.轻舟微服务平台是网易云为企业提供的一套微服务解决方案,其中微服务监控是其关注的重 ...
- android jmf,基于JMF与Android的无线视频监控系统的设计与实现
摘要: 视频监控系统是一种防范能力很强的综合系统,是安防领域的重要组成部分.随着无线网络与流媒体传输技术的飞速发展,视频监控系统也朝着无线化,高清化,智能化的方向发展.针对这一需求,通过研究JAVA多 ...
- Prometheus(一)——概述、监控体系、生态组件、部署
目录 前言:zabbix与prometheus区别 一.Prometheus概述 1.1 Prometheus具有以下特性 1.2 Prometheus核心组件 二.运维监控平台设计思路 三.pr ...
- 模糊综合评价法用什么软件实现_基于建管养一体化模式的钢桥面铺装方案综合评价分析...
文章来源:微信公众号"沥表路面" 钢桥面铺装技术复杂,原材料技术指标要求高,施工控制要求严,交通荷载大.环境温度高.钢桥面支撑体受力复杂.同时,由于受桥梁恒载限制,铺装层厚度有限. ...
- 基于 HTML5 的 WebGL 自定义 3D 摄像头监控模型
2019独角兽企业重金招聘Python工程师标准>>> 前言 随着视频监控联网系统的不断普及和发展, 网络摄像机更多的应用于监控系统中,尤其是高清时代的来临,更加快了网络摄像机的发展 ...
- led可见光通信直方图均衡matlab,基于RGB型LED的光学相机通信系统的实现方案
2019 年第 5 期 基于 RGB 型 LED 的光学相机通信系统的实现方案 Realization scheme of optical camera communication system ba ...
最新文章
- 关于设计模式的一次聊天
- Spring Boot(五):spring data jpa的使用
- 幼儿园计算机教案 妈妈的爱,幼儿园大班教案《妈妈的爱》(通用)
- 让你明白response.sendRedirect()与request.getRequestDispatcher().forward()区别
- python的numpy入门简介
- CUDA编程之CMAKE
- 20145322 Exp5 MS08_067漏洞测试
- 南京php吧,利用php爬虫分析南京房价
- 再探c++ priority
- 【论文笔记】A Survey on Federated Learning: The Journey From Centralized to Distributed On-Site...(综述)
- 群晖linux怎么进入u盘,黑群晖菜鸟安装教程(一)制作U盘引导及软洗白!
- Hadoop国内镜像下载地址:极速
- 有什么软件可以复制并粘贴文件?
- spring-cloud-stream通道多线程并发消费
- 2021-04-05 c++程序设计原理与实践持续学习笔记:第三章对象、类型和值。
- 哪些情况下会被银行拒贷?买房真的凑齐首付就够了吗?
- OSI七层参考模型是什么
- 小学数学开灯问题_小学一年级数学题库:开灯问题(高等难度)
- 完全背包问题(二维数组)
- 【micropython】两块microbit实现无线温度检测
热门文章
- 用户、角色、权限表的关系(mysql)
- 手机卫士项目——手机防盗时候——选择手机联系人
- 2022.3.21-3.27 AI行业周刊(第90期):中年裁员
- 计算机网络原理超详解说,你看懂了吗?
- Android q索尼手机相机算法,索尼相机很强为何索尼手机拍照DxO垫底?没有针对性优化...
- python怎样控制继电器_USB中继由python控制,继电器,USBrelay,用
- Android Retrofit网络获取数据+Recyclerview展示数据
- 招商银行2019Fintech训练营面试
- 常用的JS页面跳转代码调用大全
- 亲测最新授权系统后台功能很强大+PHP开源版