前提

最近线上的项目使用了 spring-actuator 做度量统计收集,使用 Prometheus 进行数据收集, Grafana 进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为"埋点"。 SpringBoot 中的依赖 spring-actuator 中集成的度量统计API使用的框架是 Micrometer ,官网是 micrometer.io 。在实践中发现了业务开发者滥用了 Micrometer 的度量类型 Counter ,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于 Micrometer 分析其他的度量类型API的作用和适用场景。全文接近3W字,内容比较干,希望能够耐心阅读,有所收获。

Micrometer提供的度量类库

Meter 是指一组用于收集应用中的度量数据的接口,Meter单词可以翻译为"米"或者"千分尺",但是显然听起来都不是很合理,因此下文直接叫 Meter ,直接当成一个专有名词,理解它为度量接口即可。 Meter 是由 MeterRegistry 创建和保存的,可以理解 MeterRegistry 是 Meter 的工厂和缓存中心,一般而言每个JVM应用在使用Micrometer的时候必须创建一个 MeterRegistry 的具体实现。Micrometer中, Meter 的具体类型包括: Timer , Counter , Gauge , DistributionSummary , LongTaskTimer , FunctionCounter , FunctionTimer 和 TimeGauge 。下面分节详细介绍这些类型的使用方法和实战使用场景。而一个 Meter 具体类型需要通过名字和 Tag (这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的 Tag 去区分多种维度进行数据统计。

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的命名

Micrometer 中, Meter 的命名约定使用英文逗号(dot,也就是".")分隔单词。但是对于不同的监控系统,对命名的规约可能并不相同,如果命名规约不一致,在做监控系统迁移或者切换的时候,可能会对新的系统造成破坏。 Micrometer 中使用英文逗号分隔单词的命名规则,再通过底层的命名转换接口 NamingConvention 进行转换,最终可以适配不同的监控系统,同时可以消除监控系统不允许的特殊字符的名称和标记等。开发者也可以覆盖 NamingConvention 实现自定义的命名转换规则: registry.config().namingConvention(myCustomNamingConvention); 。在 Micrometer 中,对一些主流的监控系统或者存储系统的命名规则提供了默认的转换方式,例如当我们使用下面的命名时候:

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。

另外, Tag (标签)是 Micrometer 的一个重要的功能,严格来说,一个度量框架只有实现了标签的功能,才能真正地多维度进行度量数据收集。Tag的命名一般需要是有意义的,所谓有意义就是可以根据 Tag 的命名可以推断出它指向的数据到底代表什么维度或者什么类型的度量指标。假设我们需要监控数据库的调用和Http请求调用统计,一般推荐的做法是:

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");

通过命名"calls"得到的计数器,由于标签混乱,数据是基本无法分组统计分析,这个时候可以认为得到的时间序列的统计数据是没有意义的。可以定义全局的Tag,也就是全局的Tag定义之后,会附加到所有的使用到的Meter上(只要是使用同一个MeterRegistry),全局的Tag可以这样定义:

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());}
}

当然,有些时候,我们需要过滤一些必要的标签或者名称进行统计,或者为Meter的名称添加白名单,这个时候可以使用 MeterFilter 。 MeterFilter 本身提供一些列的静态方法,多个 MeterFilter 可以叠加或者组成链实现用户最终的过滤策略。例如:

MeterRegistry registry = ...
registry.config().meterFilter(MeterFilter.ignoreTags("http")).meterFilter(MeterFilter.denyNameStartsWith("jvm"));

表示忽略"http"标签,拒绝名称以"jvm"字符串开头的 Meter 。更多用法可以参详一下 MeterFilter 这个类。

Meter 的命名和 Meter 的 Tag 相互结合,以命名为轴心,以 Tag 为多维度要素,可以使度量数据的维度更加丰富,便于统计和分析。

Meters

前面提到Meter主要包括: Timer , Counter , Gauge , DistributionSummary , LongTaskTimer , FunctionCounter , FunctionTimer 和 TimeGauge 。下面逐一分析它们的作用和个人理解的实际使用场景(应该说是生产环境)。

Counter

Counter 是一种比较简单的 Meter ,它是一种单值的度量类型,或者说是一个单值计数器。 Counter 接口允许使用者使用一个固定值(必须为正数)进行计数。准确来说: 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}]

使用场景:

Counter 的作用是记录XXX的总量或者计数值,适用于一些增长类型的统计,例如下单、支付次数、 HTTP 请求总量记录等等,通过 Tag 可以区分不同的场景,对于下单,可以使用不同的 Tag 标记不同的业务来源或者是按日期划分,对于 HTTP 请求总量记录,可以使用 Tag 区分不同的 URL 。用下单业务举个例子:

//实体
@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}]

上面的例子是使用全局静态方法工厂类 Metrics 去构造 Counter 实例,实际上, io.micrometer.core.instrument.Counter 接口提供了一个内部建造器类 Counter.Builder 去实例化 Counter , Counter.Builder 的使用方式如下:

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

FunctionCounter 是 Counter 的特化类型,它把计数器数值增加的动作抽象成接口类型 ToDoubleFunction ,这个接口JDK1.8中对于 Function 的特化类型接口。 FunctionCounter 的使用场景和 Counter 是一致的,这里介绍一下它的用法:

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();}
}

FunctionCounter 使用的一个明显的好处是,我们不需要感知 FunctionCounter实例的存在,实际上我们只需要操作作为 FunctionCounter 实例构建元素之一的 AtomicInteger 实例即可,这种接口的设计方式在很多主流框架里面可以看到。

Timer

Timer (计时器)适用于记录耗时比较短的事件的执行时间,通过时间分布展示事件的序列和发生频率。所有的 Timer 的实现至少记录了发生的事件的数量和这些事件的总耗时,从而生成一个时间序列。 Timer 的基本单位基于服务端的指标而定,但是实际上我们不需要过于关注 Timer 的基本单位,因为 Micrometer 在存储生成的时间序列的时候会自动选择适当的基本单位。 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());

使用场景:

根据个人经验和实践,总结如下:

  • 1、记录指定方法的执行时间用于展示。
  • 2、记录一些任务的执行时间,从而确定某些数据来源的速率,例如消息队列消息的消费速率等。

这里举个实际的例子,要对系统做一个功能,记录指定方法的执行时间,还是用下单方法做例子:

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}}
}

在实际生产环境中,可以通过 spring-aop 把记录方法耗时的逻辑抽象到一个切面中,这样就能减少不必要的冗余的模板代码。上面的例子是通过Mertics构造Timer实例,实际上也可以使用Builder构造:

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

FunctionTimer 是 Timer 的特化类型,它主要提供两个单调递增的函数(其实并不是单调递增,只是在使用中一般需要随着时间最少保持不变或者说不减少):一个用于计数的函数和一个用于记录总调用耗时的函数,它的建造器的入参如下:

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
);

按照个人理解, ToDoubleFunction 用于统计事件个数, ToDoubleFunction 用于记录执行总时间,实际上两个函数都只是 Function 函数的变体,还有一个比较重要的是总时间的单位totalTimeFunctionUnit。简单的使用方式如下:

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

LongTaskTimer 是 Timer 的特化类型,主要用于记录长时间执行的任务的持续时间,在任务完成之前,被监测的事件或者任务仍然处于运行状态,任务完成的时候,任务执行的总耗时才会被记录下来。 LongTaskTimer 适合用于长时间持续运行的事件耗时的记录,例如相对耗时的定时任务。在 Spring(Boot) 应用中,可以简单地使用 @Scheduled 和 @Timed 注解,基于 spring-aop 完成定时调度任务的总耗时记录:

@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

Gauge (仪表)是获取当前度量记录值的句柄,也就是它表示一个可以任意上下浮动的单数值度量 Meter 。 Gauge 通常用于变动的测量值,测量值用 ToDoubleFunction参数的返回值设置,如当前的内存使用情况,同时也可以测量上下移动的"计数",比如队列中的消息数量。官网文档中提到 Gauge 的典型使用场景是用于测量集合或映射的大小或运行状态中的线程数。一般情况下, Gauge 适合用于监测有自然上界的事件或者任务,而 Counter 一般使用于无自然上界的事件或者任务的监测,所以像 HTTP 请求总量计数应该使用 Counter 而非 Gauge 。 MeterRegistry 中提供了一些便于构建用于观察数值、函数、集合和映射的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<>());

上面的三个方法通过 MeterRegistry 构建 Gauge 并且返回了集合或者映射实例,使用这些集合或者映射实例就能在其size变化过程中记录这个变更值。更重要的优点是,我们不需要感知 Gauge 接口的存在,只需要像平时一样使用集合或者映射实例就可以了。此外, Gauge 还支持 java.lang.Number 的子类, java.util.concurrent.atomic包中的 AtomicInteger 和 AtomicLong ,还有 Guava 提供的 AtomicDouble :

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);

使用场景:

根据个人经验和实践,总结如下:

  • 1、有自然(物理)上界的浮动值的监测,例如物理内存、集合、映射、数值等。
  • 2、有逻辑上界的浮动值的监测,例如积压的消息、(线程池中)积压的任务等,其实本质也是集合或者映射的监测。

举个相对实际的例子,假设我们需要对登录后的用户发送一条短信或者推送,做法是消息先投放到一个阻塞队列,再由一个线程消费消息进行其他操作:

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

Summary (摘要)主要用于跟踪事件的分布,在 Micrometer 中,对应的类是 DistributionSummary (分布式摘要)。它的使用方式和 Timer 十分相似,但是它的记录值并不依赖于时间单位。常见的使用场景:使用 DistributionSummary 测量命中服务器的请求的有效负载大小。使用 MeterRegistry 创建 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);

使用场景:

根据个人经验和实践,总结如下:

  • 1、不依赖于时间单位的记录值的测量,例如服务器有效负载值,缓存的命中率等。

举个相对具体的例子:

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集成

集成了 Micrometer 框架的 JVM 应用使用到 Micrometer 的 API 收集的度量数据位于内存之中,因此,需要额外的存储系统去存储这些度量数据,需要有监控系统负责统一收集和处理这些数据,还需要有一些UI工具去展示数据, 一般情况下大佬或者老板只喜欢看炫酷的仪表盘或者动画 。常见的存储系统就是时序数据库,主流的有 Influx 、 Datadog 等。比较主流的监控系统(主要是用于数据收集和处理)就是 Prometheus (一般叫普罗米修斯,下面就这样叫吧)。而展示的UI目前相对用得比较多的就是 Grafana。另外, Prometheus 已经内置了一个时序数据库的实现,因此,在做一套相对完善的度量数据监控的系统只需要依赖目标 JVM 应用, Prometheus 组件和 Grafana 组件即可。下面花一点时间从零开始搭建一个这样的系统,之前写的一篇文章基于 Windows 系统,操作可能跟生产环境不够接近,这次使用 CentOS7 。

SpirngBoot中使用Micrometer

SpringBoot 中的 spring-boot-starter-actuator 依赖已经集成了对 Micrometer 的支持,其中的 metrics 端点的很多功能就是通过 Micrometer 实现的, prometheus 端点默认也是开启支持的,实际上 actuator 依赖的 spring-boot-actuator-autoconfigure 中集成了对很多框架的开箱即用的 API ,其中 prometheus 包中集成了对 Prometheus 的支持,使得使用了 actuator 可以轻易地让项目暴露出 prometheus 端点,使得应用作为 Prometheus 收集数据的客户端, Prometheus(服务端软件)可以通过此端点收集应用中 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;}
}

yaml的配置如下:

server:port: 9091
management:server:port: 10091endpoints:web:exposure:include: '*'base-path: /management

注意多看 spring官方文档 关于 Actuator 的详细描述,在 SpringBoot2.x 之后,配置Web端点暴露的权限控制和 SpringBoot1.x 有很大的不同。总结一下就是:除了 shutdown 端点之外,其他端点默认都是开启支持的( 这里仅仅是开启支持,并不是暴露为Web端点,端点必须暴露为Web端点才能被访问 ),禁用或者开启端点支持的配置方式如下:

management.endpoint.${端点ID}.enabled=true/false

可以查看 actuator-api文档 查看所有支持的端点的特性,这个是2.1.0.RELEASE版本的官方文档,不知道日后链接会不会挂掉。端点只开启支持,但是不暴露为Web端点,是无法通过 http://{host}:{management.port}/{management.endpoints.web.base-path}/{endpointId} 访问的。暴露监控端点为Web端点的配置是:

management.endpoints.web.exposure.include=info,health
management.endpoints.web.exposure.exclude=prometheus

management.endpoints.web.exposure.include 用于指定暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。 management.endpoints.web.exposure.exclude 用于指定不暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。

management.endpoints.web.exposure.include 默认指定的只有 info 和 health 两个端点,我们可以直接指定暴露所有的端点: management.endpoints.web.exposure.include=* ,如果采用 YAML 配置, 记得要在星号两边加上英文单引号 。暴露所有Web监控端点是一件比较危险的事情,如果需要在生产环境这样做,请务必先确认 http://{host}:{management.port} 不能通过公网访问(也就是监控端点访问的端口只能通过内网访问,这样可以方便后面说到的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 :

访问 http://${虚拟机host}:9090/graph 可以查找到我们定义的度量 Meter 和 spring-boot-starter-actuator 中已经定义好的一些关于JVM或者 Tomcat 的度量 Meter 。我们先对应用的 /order 接口进行调用,然后查看一下监控前面在应用中定义的 order_count_total 和 method_cost_time_seconds_sum :

可以看到, Meter 的信息已经被收集和展示,但是显然不够详细和炫酷,这个时候就需要使用Grafana的UI做一下点缀。

Grafana的安装和使用

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

安装完成后,通过命令 service grafana-server start 启动即可,默认的启动端口为3000,通过 http://${host}:3000 访问即可。初始的账号密码都为admin,权限是管理员权限。接着需要在 Home 面板添加一个数据源,目的是对接 Prometheus 服务端从而可以拉取它里面的度量数据。数据源添加面板如下:

其实就是指向Prometheus服务端的端口就可以了。接下来可以天马行空地添加需要的面板,就下单数量统计的指标,可以添加一个 Graph 的面板:

配置面板的时候,需要在基础(General)中指定Title:

接着比较重要的是Metrics的配置,需要指定数据源和Prometheus的查询语句:

最好参考一下 Prometheus 的官方文档,稍微学习一下它的查询语言 PromQL 的使用方式,一个面板可以支持多个 PromQL 查询。前面提到的两项是基本配置,其他配置项一般是图表展示的辅助或者预警等辅助功能,这里先不展开,可以去 Grafana 的官网挖掘一下使用方式。然后我们再调用一下下单接口,过一段时间,图表的数据就会自动更新和展示:

接着添加一下项目中使用的Timer的Meter,便于监控方法的执行时间,完成之后大致如下:

上面的面板虽然设计相当粗糙,但是基本功能已经实现。设计面板并不是一件容易的事,如果有需要可以从 Github 中搜索一下 grafana dashboard 关键字找现成的开源配置使用或者二次加工后使用。

小结

常言道:工欲善其事,必先利其器。 Micrometer 是 JVM 应用的一款相当优异的度量框架,它提供基于 Tag 和丰富的度量类型和 API 便于多维度地进行不同角度度量数据的统计,可以方便地接入 Prometheus 进行数据收集,使用 Grafana 的面板进行炫酷的展示,提供了天然的 spring-boot 体系支持。但是,在实际的业务代码中,度量类型 Counter 经常被滥用,一旦工具被不加思考地滥用,就反而会成为混乱或者毒瘤。因此,这篇文章就是对 Micrometer 中的各种 Meter 的使用场景基于个人的理解做了调研和分析,后面还会有系列的文章分享一下这套方案在实战中的经验和踩坑经历。

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 绝对估值法和相对估值法

    绝对估值法金融市场在估算企业价值的时候,常常用现金流贴现法。也就是Discounted Cash Flow(DCF)。该方法把企业未来折现的现金流加起来,计算企业的内在价值。自由现金流:一个企业的经营活动,扣除所有开支后,可以自由支配的钱股东盈余:一个公司未来可以帮股东赚到的净利…...

    2024/4/19 20:13:32
  2. 阿里三面试题:为什么抽象类无法实例化,但是可以new出来实例?

    背景,二面阿里过关,三面的时候面试官问了我很多难题,挑选一个作详细解析。大致的经过是这样的:他问的:“为什么抽象类无法实例化,但是可以new出来一个实例,官方这么说是什么意思?而且去了各类论坛,回答的都是比较含糊其辞,切不到重点。说什么是面向对象设计的规范balab…...

    2024/4/16 7:54:38
  3. 深入理解Spring Boot数据源与连接池原理

    【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】一:开始 在使用Spring Boot数据源之前,我们一般会导入相关依赖。其中数据源核心依赖就是spring‐bo…...

    2024/4/19 18:13:53
  4. jQuery学习笔记——事件添加的特性以及方法

    jQuery绑定事件的一些特性同一元素相同的后续事件不会覆盖前面的事件事件绑定与解绑 同一元素相同的后续事件不会覆盖前面的事件//原生js中,相同元素注册相同的事件,后续的事件会把前面的事件覆盖掉document.getElementById(box).onclick = function(){console.log(js事件1)}do…...

    2024/4/16 7:54:23
  5. 【Yii2】高级模板跨应用调用组件的最佳解决方案

    项目需求 在Yii2的高级模板中,可以创建多个应用。 目前有两个应用 网站 管理后台 其中网站使用pageCache作为缓存组件。 由于使用缓存组件,导致后台的内容没有及时更新,所以,需要在后台创建模块,手动刷新网站的缓存。 思路 使用网站配置文件在后台注册组件 也就是说,在缓…...

    2024/4/19 13:47:47
  6. 淘宝开店怎么做运营?

    为了增加商品的流量和销量,一些商家不得不发放一些优惠券来吸引消费者的目光,一些优惠券是直接通过店铺发放,还有一些是隐藏发放的,那么消费者知道淘宝隐藏优惠券在哪里领吗?卖家怎么设置优惠券呢? 淘宝隐藏优惠券在哪里领?卖家怎么设置? 一、淘宝隐藏优惠券在哪里领 一…...

    2024/5/4 16:01:04
  7. 某段代码执行多久

    long t1 = System.currentTimeMillis();List<GoodsSelectDTO> goodsSelectDTOList = goodsService.getGoodsSelectUtilnbsp(companyId);System.out.println("queryTime=" + (System.currentTimeMillis() - t1));...

    2024/4/16 7:53:42
  8. 在Ubuntu导入Tesorflow出错问题

    文章目录 1.ImportError: libcusolver.so.10.0: cannot open shared object file: No such file… 分析:可能是cudnn未安装 执行命令查看cudnn是否安装 cat /usr/local/cuda/include/cudnn.h | grep CUDNN_MAJOR -A 2若有结果输出则表示cudnn已安装的,若没有结果,则需要进行…...

    2024/4/16 7:54:33
  9. Python 输出HTML实体字符(&#x***转html)

    #!/usr/bin/env python # encoding: utf-8 """ 出 关① 徐兰 凭山俯海古边州, 旆②影风翻见戍楼。 马后桃花马前雪,出关争得不回头? [注]①关,指居庸关。②旆(pi),旌旗。 """ import html string = [注]&#9312关,指居庸关。&…...

    2024/4/16 7:54:28
  10. C语言练习题1:英文字母大小写转换

    输入一行字符,判断其是否为英文字母,若是英文字母,则进行大小写转换,若不是英文字母,则不变。 #include <stdio.h>int main(){int i;char s[100];gets(s);for(i=0;s[i]!=0;i++)if(s[i]>=a&&s[i]<=z)s[i]=s[i]-32;else if(s[i]>=A&&s[i]<…...

    2024/4/16 7:53:42
  11. 轻量级实时语义分割经典BiSeNet

    基于轻量化网络模型的设计作为一个热门的研究方法,许多研究者都在运算量、参数量和精度之间寻找平衡,希望使用尽量少的运算量和参数量的同时获得较高的模型精度。目前,轻量级模型主要有SqueezeNet、MobileNet系列和ShuffleNet系列等,这些模型在图像分类领域取得了不错的效果…...

    2024/4/28 19:26:22
  12. 连接docker容器中出现ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using password: YES)

    问题复现 ERROR 1045 (28000): Access denied for user ‘root’@‘localhost’ (using password: YES) 很久没有登陆我的这个数据库容器,连不上。 开始百度方法,说改配置文件,但是很遗憾,我创建容器的时候没有挂在配置文件。 解决记录 大前提:我是docker容器中的mysql 错…...

    2024/4/16 7:54:33
  13. [Codeforces Round #649 (Div. 2)]1364

    文章目录A - XXXXX [思维]B - Sign Flipping [思维]C - Ehab and Prefix MEXs [构造][并查集]D - Ehabs Last Corollary [dfs环长度]比赛首页 标准题解 A - XXXXX [思维] 目录 题目传送门 题意 有 nnn 个数的数组 a[]a[]a[],要使数组的总和%k!=0\% k != 0%k!=0 可以选择删去开…...

    2024/4/17 22:03:49
  14. AQS基本原理

    什么是AQS? AQS即AbstractQueuedSynchronizer,是一个用于构建锁和同步器的框架。它能降低构建锁和同步器的工作量,还可以避免处理多个位置上发生的竞争问题。在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。 AQS支持独占锁(e…...

    2024/5/2 20:15:38
  15. 算术表达式运算栈实现-数据结构-golang

    算术表达式运算栈实现-数据结构-golang package mainimport ("fmt""log""strconv" )//运算符stack type opStack []string//操作数stack type dataStack []int//操作符的优先级标识 type priority map[string]map[string]intvar p = make(priori…...

    2024/4/16 7:54:43
  16. React-Native 二、登录注册模块开发

    前段时间公司项目比较赶, 没时间写文章, 这两天闲下来了, 总结了一下, 写了一篇比较综合的UI项目, 登录注册模块, 包含UI 网络请求等功能, 希望对大家理解React Native的界面布局和网络请求有一定的帮助.项目可到github下载: https://github.com/YTiOSer/YTReact-Native_LoginU…...

    2024/4/16 7:54:28
  17. 黑盒测试、白盒测试的定义

    白盒测试 是通过程序的源代码进行测试而不使用用户界面。这种类型的测试需要从代码句法发现内部代码在算法,溢出,路径,条件等等中的缺点或者错误,进而加以修正。 黑盒测试 是通过使用整个软件或某种软件功能来严格地测试, 而并没有通过检查程序的源代码或者很清楚地了解该软…...

    2024/4/19 1:28:58
  18. Python中如何使用类

    (因为入职之后除了学术研究之外,还要做一些工程的事情,所以小刘同学以后还要学习一些python面向对象的用法,今天就先学习一下类是怎么继承的,这也算是个学习记录帖子)Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对象…...

    2024/4/26 13:58:23
  19. checkio Surjection Strings

    题目: 两个字符串可不可以成某种规律映射 Example: isometric_strings(‘add’, ‘egg’) == True isometric_strings(‘foo’, ‘bar’) == False Precondition:both strings are the same size 链接: https://py.checkio.org/en/mission/isometric-strings/ def isometric_…...

    2024/4/1 1:50:09
  20. B/S结构和C/S结构

    B/S和C/S都是随着互联网的发展而出现的一种网络结构模式,而其用的非常广泛,在我们生活中都很常见。那它们到底是什么呢?接下来就详细的介绍一下B/S和C/S。一、B/S结构B是英文单词“Browser”的首字母,即浏览器的意思;S是英文单词“Server”的首字母,即服务器的意思。B/S就…...

    2024/4/25 23:09:19

最新文章

  1. 恶补《操作系统》5_1——王道学习笔记

    5设备管理 5.1_1 I-O设备的概念和分类 1、什么是I-O设备 输入/输出&#xff1a;I/O设备就是可以将数据输入到计算机&#xff0c;或者可以接收计算机输出数据的外部设备&#xff0c;属于计算机中的硬件部件。 2、按使用特性分类 人机交互的外部设备存储设备网络通信设备 3、…...

    2024/5/4 17:37:59
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. k8s_入门_kubelet安装

    安装 在大致了解了一些k8s的基本概念之后&#xff0c;我们实际部署一个k8s集群&#xff0c;做进一步的了解 1. 裸机安装 采用三台机器&#xff0c;一台机器为Master&#xff08;控制面板组件&#xff09;两台机器为Node&#xff08;工作节点&#xff09; 机器的准备有两种方式…...

    2024/5/4 14:33:56
  4. c++类的继承方式

    在 C 中&#xff0c;类的继承方式有三种&#xff1a;公有继承&#xff08;public inheritance&#xff09;、保护继承&#xff08;protected inheritance&#xff09;和私有继承&#xff08;private inheritance&#xff09;。这些继承方式决定了派生类对基类成员的访问权限。 …...

    2024/5/3 14:42:38
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/2 16:16:39
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/3 23:10:03
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/2 15:04:34
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/4 2:59:34
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/2 9:07:46
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57