开源监控系统Prometheus的前世今生

Prometheus是SoundCloud公司开源的监控系统,同时也是继Kubernetes之后,第二个加入CNCF的项目。Prometheus是一个优秀的监控系统,沃趣围绕着Prometheus先后开发了多个组件,包括基础告警组件,服务发现组件、各种采集的Exporters等,这些组件结合Prometheus支撑了沃趣大部分的监控业务。本文主要介绍Prometheus,从他的来源,架构以及一个具体的例子等方面来说明,以及沃趣围绕Prometheus做了哪些工作。 

起源

SoundCloud公司的之前的应用架构是巨石架构,也就是所有的功能放在一个大的模块里,各个功能之间没有明显的界线。巨石架构的应用主要存在两方面的问题,一方面在于很难对其进行水平扩展,只能垂直扩展,但是单台机器的能力毕竟是有限的;另外一方面在于各个功能耦合在一块,新增一个功能需要在已有的技术栈上进行开发,并且要确保不会对已有的功能造成影响。于是他们转向了微服务架构,将原有的功能拆分成了几百个独立的服务,整个系统运行上千个实例。迁移到微服务架构给监控带来一定的挑战,现在不仅需要知道某个组件的运行的情况,还要知道服务的整体运行情况。他们当时的监控方案是:StatsD + Graphite + Nagios,StatsD结合Graphite构建监控图表,各个服务将样本数据推送给StatsD,StatsD将推送来的样本数据聚合在一起,定时地推送给Graphite,Graphite将样本数据保存在时序数据库中,用户根据Graphite提供的API,结合自身监控的需求,构建监控图表,通过图表分析服务的指标(例如,延迟,每秒的请求数,每秒的错误数等)。 

1_SoundCloud巨石应用架构.png
2_SoundCloud微服务架构.png


那么这样一种方案能满足微服务架构对监控的要求么?什么要求呢:既能知道服务整体的运行情况,也能够保持足够的粒度,知道某个组件的运行情况。答案是很难,为什么呢?例如,我们要统计api-server服务响应POST /tracks请求错误的数量,指标的名称为api-server.tracks.post.500,这个指标可以通过http状态码来测量,服务响应的状态码为500就是错误的。Graphite指标名称的结构是一种层次结构,api-server指定服务的名称,tracks指定服务的handler,post指定请求的方法,500指定请求响应的状态码,api-server服务实例将该指标推送给StatsD,StatsD聚合各个实例推送来的指标,然后定时推送给Graphite。查询api-server.tracks.post.500指标,我们能获得服务错误的响应数,但是,如果我们的api-server服务跑了多个实例,想知道某个实例错误的响应数,该怎么查询呢?问题出在使用这样一种架构,往往会将各个服务实例发送来的指标聚合到一块,聚合到一起之后,实例维度的信息就丢失掉了,也就无法统计某个具体实例的指标信息。 

3_StatsD_Graphite监控方案.png


StatsD与Graphite的组合用来构建监控图表,告警是另外一个系统-Nagios-来做的,这个系统运行检测脚本,判断主机或服务运行的是否正常,如果不正常,发送告警。Nagios最大的问题在于告警是面向主机的,每个告警的检查项都是围绕着主机的,在分布式系统的环境底下,主机down掉是正常的场景,服务本身的设计也是可以容忍节点down掉的,但是,这种场景下Nagios依然会触发告警。 

4_面向主机的告警系统Nagios.png


如果大家之前看过这篇 https://landing.google.com/sre … arker 介绍Google Borgmon的文章,对比Prometheus,你会发现这两个系统非常相似。实际上,Prometheus深受Borgmon系统的影响,并且当时参与构建Google监控系统的员工加入了SoundCloud公司。总之,种种因素的结合,促使了Prometheus系统的诞生。 

Prometheus的解决方案

那么,Prometheus是如何解决上面这些问题的?之前的方案中,告警与图表的构建依赖于两个不同的系统,Prometheus采取了一种新的模型,将采集时序数据作为整个系统的核心,无论是告警还是构建监控图表,都是通过操纵时序数据来实现的。Prometheus通过指标的名称以及label(key/value)的组合来识别时序数据,每个label代表一个维度,可以增加或者减少label来控制所选择的时序数据,前面提到,微服务架构底下对监控的要求:既能知道服务整体的运行情况,也能够保持足够的粒度,知道某个组件的运行情况。借助于这种多维度的数据模型可以很轻松的实现这个目标,还是拿之前那个统计http错误响应的例子来说明,我们这里假设api_server服务有三个运行的实例,Prometheus采集到如下格式的样本数据(其中intance label是Prometheus自动添加上去的): 

api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample1"} -> 34
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample2"} -> 28
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample3"} -> 31


如果我们只关心特定实例的错误数,只需添加instance label即可,例如我们想要查看实例名称为sample1的错误的请求数,那么我就可以用api_server_http_requests_total{method=”POST”,handler=”/tracks”,status=”500″,instance=”sample1″}这个表达式来选择时序数据,选择的数据如下: 

api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample1"} -> 34


如果我们关心整个服务的错误数,只需忽略instance label去除,然后将结果聚合到一块,即可,例如 
sum without(instance) (api_server_http_requests_total{method=”POST”,handler=”/tracks”,status=”500″})计算得到的时序数据为: 

api_server_http_requests_total{method="POST",handler="/tracks",status="500"} -> 93


告警是通过操纵时序数据而不是运行一个自定义的脚本来实现的,因此,只要能够采集到服务或主机暴露出的指标数据,那么就可以告警。 

架构

我们再来简单的分析一下Prometheus的架构,看一下各个组件的功能,以及这些组件之间是如何交互的。 

Prometheus Server是整个系统的核心,它定时地从监控目标(Exporters)暴露的API中拉取指标,然后将这些数据保存到时序数据库中,如果是监控目标是动态的,可以借助服务发现的机制动态地添加这些监控目标,另外它还会暴露执行PromQL(用来操纵时序数据的语言)的API,其他组件,例如Prometheus Web,Grafana可以通过这个API查询对应的时序数据。Prometheus Server会定时地执行告警规则,告警规则是PromQL表达式,表达式的值是true或false,如果是true,就将产生的告警数据推送给alertmanger。告警通知的聚合、分组、发送、禁用、恢复等功能,并不是Prometheus Server来做的,而是Alertmanager来做的,Prometheus Server只是将触发的告警数据推送给Alertmanager,然后Alertmanger根据配置将告警聚合到一块,发送给对应的接收人。 

如果我们想要监控定时任务,想要instrument任务的执行时间,任务执行成功还是失败,那么如何将这些指标暴露给Prometheus Server?例如每隔一天做一次数据库备份,我们想要知道每次备份执行了多长时间,备份是否成功,我们备份任务只会执行一段时间,如果备份任务结束了,Prometheus Server该如何拉取备份指标的数据呢?解决这种问题,可以通过Prometheus的pushgateway组件来做,每个备份任务将指标推送pushgateway组件,pushgateway将推送来的指标缓存起来,Prometheus Server从Pushgateway中拉取指标。 

5_Prometheus架构.png

例子

前面都是从比较大的层面——背景、架构——来介绍Prometheus,现在,让我们从一个具体的例子出发,来看一下如何借助Prometheus来构建监控图表、分析系统性能以及告警。 

我们有个服务,暴露出四个API,每个API只返回一些简单的文本数据,现在,我们要对这个服务进行监控,希望借助监控能够查看、分析服务的请求速率,请求的平均延迟以及请求的延迟分布,并且当应用的延迟过高或者不可访问时能够触发告警,代码示例如下: 

package main
import (
"math/rand"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
Latency = promauto.NewHistogramVec(prometheus.HistogramOpts{
    Help: "latency of sample app",
    Name: "sample_app_latency_milliseconds",
    Buckets: prometheus.ExponentialBuckets(10, 2, 9),
}, []string{"handler", "method"})
)
func instrumentationFilter(f http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
    now := time.Now()
    f(writer, request)
    duration := time.Now().Sub(now)
    Latency.With(prometheus.Labels{"handler": request.URL.Path, "method": request.Method}).
        Observe(float64(duration.Nanoseconds()) / 1e6)
}
}
// jitterLatencyFilter make request latency between d and d*maxFactor
func jitterLatencyFilter(d time.Duration, maxFactor float64, f http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
    time.Sleep(d + time.Duration(rand.Float64()*maxFactor*float64(d)))
    f(writer, request)
}
}
func main() {
rand.Seed(time.Now().UnixNano())
http.Handle("/metrics", promhttp.Handler())
http.Handle("/a", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 256, func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("success"))
})))
http.Handle("/b", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 128, func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("success"))
})))
http.Handle("/c", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 64, func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("success"))
})))
http.Handle("/d", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 32, func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("success"))
})))
http.ListenAndServe(":5001", nil)
}


我们按照instrumentation、exposition、collection、query这样的流程构建监控系统,instrumentation关注的是如何测量应用的指标,有哪些指标需要测量;exposition关注的是如何通过http协议将指标暴露出来;collection关注的是如何采集指标;query关注的是如何构建查询时序数据的PromQL表达式。我们首先从instrumentation这里,有四个指标是我们关心的: 

  • 请求速率
  • 请求的平均延迟
  • 请求的延迟分布
  • 访问状态
var (
Latency = promauto.NewHistogramVec(prometheus.HistogramOpts{
    Help: "latency of sample app",
    Name: "sample_app_latency_milliseconds",
    Buckets: prometheus.ExponentialBuckets(10, 2, 9),
}, []string{"handler", "method"})
)


首先将指标注册进来,然后追踪、记录指标的值。用Prometheus提供的golang客户端库可以方便的追踪、记录指标的值,我们将instrumentation code放到应用的代码里,每次请求,对应的指标状态的值就会被记录下来。 

client golang提供了四种指标类型,分别为Counter, Gauge, Histogram, Summary,Counter类型的指标用来测量只会增加的值,例如服务的请求数;Gauge类型的指标用来测量状态值,即可以变大,也可以变小的值,例如请求的延迟时间;Histogram与Summary指标类似,这两个指标取样观察的值,记录值的分布,统计观察值的数量,累计观察到的值,可以用它来统计样本数据的分布。为了采集请求速率、平均延迟以及延迟分布指标,方便起见用Histogram类型的指标追踪、记录每次请求的情况,Histogram类型的指标与普通类型(Counter、Gauge)不同的地方在于会生成多条样本数据,一个是观察样本的总数,一个是观察样本值的累加值,另外是一系列的记录样本百分位数的样本数据。访问状态可以使用up指标来表示,每次采集时,Prometheus会将采集的健康状态记录到up指标中。 

http.Handle("/metrics", promhttp.Handler())


instrumentation完成之后,下一步要做的就是exposition,只需将Prometheus http handler添加进来,指标就可以暴露出来。访问这个Handler返回的样本数据如下(省略了一些无关的样本数据): 

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="10"} 0
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="20"} 0
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="40"} 0
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="80"} 0
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="160"} 0
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="320"} 0
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="640"} 1
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="1280"} 1
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="2560"} 1
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="+Inf"} 1
sample_app_latency_milliseconds_sum{handler="/d",method="GET"} 326.308075
sample_app_latency_milliseconds_count{handler="/d",method="GET"} 1


仅仅将指标暴露出来,并不能让prometheus server来采集指标,我们需要进行第三步collection,配置prometheus server发现我们的服务,从而采集服务暴露出的样本数据。我们简单地看下prometheus server的配置,其中,global指定采集时全局配置, scrape_interval 指定采集的间隔, evaluation_interval 指定 alerting rule (alerting rule是PromQL表达式,值为布尔类型,如果为true就将相关的告警通知推送给Alertmanager)也就是告警规则的求值时间间隔,scrape_timeout指定采集时的超时时间;alerting指定Alertmanager服务的地址;scrape_configs指定如何发现监控对象,其中job_name指定发现的服务属于哪一类,static_configs指定服务静态的地址,前面我们也提到,Prometheus支持动态服务发现,例如文件、kubernetes服务发现机制,这里我们使用最简单的静态服务发现机制。 

# my global config
global:
  scrape_interval:     2s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).
rule_files:
- rule.yaml
# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - localhost:9093
scrape_configs:
- job_name: sample-app
  scrape_interval: 3s
  static_configs:
  - targets:
    - sample:5001


采集完指标,就可以利用Prometheus提供的PromQL语言来操纵采集的时序数据,例如,我们想统计请求的平均速率,可以用这个表达式 
irate(sample_app_latency_milliseconds_sum[1m]) / irate(sample_app_latency_milliseconds_count[1m])来计算。 

有了时序数据之后,就可以借助Grafana来构建监控图表,具体怎么配置Grafana图表在这里就不展开了,核心点是利用PromQL表达式选择、计算时序数据。 

6_Grafana图表.png


Prometheus的告警是通过对Alerting Rule求值来实现的,alerting rule是一系列的PromQL表达式,alerting rule保存在配置文件中。我们想要对应用的延迟以及可用状态进行告警,当应用过高或者不可访问时就触发告警,规则可以如下这样定义: 

- name: sample-up
  rules:
  - alert: UP
    expr: up{instance="sample:5001"} == 0
    for: 1m
    labels:
      severity: page
    annotations:
      summary: Service health
  - alert: 95th-latency
    expr: histogram_quantile(0.95, rate(sample_app_latency_milliseconds_bucket[1m])) > 1000
    for: 1m
    labels:
      severity: page
    annotations:
      summary: 95th service latency


其中UP指定服务的可用状态,95th-latency指定95%的请求大于1000毫秒就触发告警。Prometheus定时的对这些规则进行求值,如果条件满足,就将告警通知发送给Alertmanger,Alertmanger会根据自身路由配置,对告警进行聚合,分发到指定的接收人,我们想通过邮箱接收到告警,可以如下进行配置: 

global:
  smtp_smarthost: <your_smtp_server>
  smtp_auth_username: <your_username>
  smtp_from: <from>
  smtp_auth_password: <secret>
  smtp_require_tls: false
  resolve_timeout: 5m
route:
  receiver: me
receivers:
- name: me
  email_configs:
  - to: example@domain.com
templates:
- '*.tmpl'


这样,我们就可以通过邮箱收到告警邮件了。 

相关的工作

无论是监控图表相关的业务,还是告警相关的业务,都离不开相关指标的采集工作,沃趣是一家做数据库产品的公司,我们花费了很多的精力去采集数据库相关的指标,从Oracle到MySQL,再到SQL Server,主流的关系型数据库的指标都有采集。对于一些通用的指标,例如操作系统相关的指标,我们主要是借助开源的Exporters来采集的。沃趣的产品是软、硬一体交付的,其中有大量硬件相关的指标需要采集,因此,我们也有专门采集硬件指标的Expoters。 

沃趣大部分场景中,要监控的服务都是动态的。比如,用户从平台上申请了一个数据库,需要增加相关的监控服务,用户删除数据库资源,需要移除相关的监控服务,要监控的数据库服务处于动态的变化之中。沃趣每个产品线的基础架构都不相同,数据库服务有跑在Oracle RAC上的,有跑在ZStack的,有跑在Kubernetes上的。对于跑在Kubernetes上的应用来说,并需要担心Prometheus怎么发现要监控的服务,只需要配置相关的服务发现的机制就可以了。对于其他类型的,我们主要借助Prometheus的file_sd服务发现机制来实现,基于文件的服务发现机制是一种最通用的机制,我们将要监控的对象写到一个文件中,Prometheus监听这个文件的变动,动态的维护要监控的对象,我们在file_sd基础上构建了专门的组件去负责服务的动态更新,其他应用调用这个组件暴露的API来维护自身想要监控的对象。 

Prometheus本身的机制的并不能满足我们业务上对告警的要求,一方面我们需要对告警通知进行统计,但是Alertmanager本身并没有对告警通知做持久化,服务重启之后告警通知就丢失掉了;另外一方面用户通过Web页面来配置相关的告警,告警规则以及告警通知的路由需要根据用户的配置动态的生成。为了解决这两方面的问题,我们将相关的业务功能做成基础的告警组件,供各个产品线去使用。针对Alertmanager不能持久化告警通知的问题,基础告警组件利用Alertmanager webhook的机制来接收告警通知,然后将通知保存到数据库中;另外用户的告警配置需要动态的生成,我们定义了一种新的模型来描述我们业务上的告警模型。 

总结

Promtheus将采集时序数据作为整个系统的核心,无论是构建监控图表还是告警,都是通过操纵时序数据来完成的。Prometheus借助多维度的数据模型,以及强大的查询语言满足了微服务架构底下对监控的要求:既能知道服务整体的运行情况,也能够保持足够的粒度,知道某个组件的运行情况。沃趣站在巨人的肩旁上,围绕Prometheus构建了自己的监控系统,从满足不同采集要求的Exporters到服务发现,最后到基础告警组件,这些组件结合Prometheus,构成了沃趣监控系统的核心。 

作者:郭振,沃趣科技开发工程师,多年的Python、Golang等语言的开发经验,熟悉Kubernetes、Prometheus等云原生应用,负责QFusion RDS平台以及基础告警平台的研发工作。

Image placeholder
qiuyucn
未设置
  47人点赞

没有讨论,发表一下自己的看法吧

推荐文章
打造高逼格、可视化的Docker容器监控系统平台

关于Docker技术的文章之前也断断续续写了几篇:Docker容器系列文章|Docker技术入门(一)Docker容器系列文章|Docker技术入门(二)Docker容器系列文章|这20个Docker

聊聊chronos的pullFromDefaultCFAndPush

序本文主要研究一下chronos的pullFromDefaultCFAndPushpullFromDefaultCFAndPushDDMQ/carrera-chronos/src/main/java/

日志监控实践 – 监控Agent集成Lua引擎实现多维度日志采集

作者简介:董涵   百度资深研发工程师负责百度智能运维(Noah)服务管理和分布式监控架构研发工作,在分布式系统和大规模数据处理、可用性工程方向有广泛的实践经验。干货概览对于互联网行业来说,最有价值的

聊聊chronos的DeleteBgWorker

序本文主要研究一下chronos的DeleteBgWorkerDeleteBgWorkerDDMQ/carrera-chronos/src/main/java/com/xiaojukeji/chron

为什么说谷歌Anthos是kubernetes的翻版?

在本周纽约的一次会议活动中,谷歌谈到了Anthos。那么,Anthos到底是什么?有哪些新功能?本文将逐一解答!什么是Anthos?从官方资料来看,Anthos是谷歌的混合云平台,主要作用是保护客户的

最流行的十大开源云监控工具

Linux系统在企业中的应用程度已经非常广泛,人们听到过太多关于Docker和Kubernetes的消息,以至于忘记了监控和日志记录也是同样重要的任务。Docker持续发展,随之而来的是围绕它构建的服

基于时序数据库做监控,这里有超流行的开源方案

在微服务架构下,我们对服务进行了拆分,所以用户的每次请求不再是由某一个服务独立完成了,而是变成了多个服务一起配合完成。这种情况下,一旦请求出现异常,我们必须得知道是在哪个服务环节出了故障,就需要对每一

为什么说Kubernetes的崛起预示着云原生时代到来?

现在,云原生、Kubernetes已经成为企业IT领域的时髦概念,几乎所有的企业都在关注;如果不提这些概念,好像企业就会在云市场竞争中失去绝对话语权。那么,云原生和Kubernetes是怎样一种关系?

vuetifyjs的优点是什么?

官方网站:https://vuetifyjs.com/zh-Hans/Vuetify优点:几乎不需要任何CSS代码,而element-ui许多布局样式需要我们来编写Vuetify从底层构建起来的语义化

一小时快速搭建基于阿里云容器服务-Kubernetes的Web应用

本文面向的读者如果您是一个Kubernetes的初学者,本文可以帮助你快速在云上搭建一个可实际使用的集群环境,并发布自己的第一个应用。你无须提前准备任何的硬件资源或者下载任何的软件包。 如果您已经有一

聊聊chronos的BackupDB

序本文主要研究一下chronos的BackupDBBackupDBDDMQ/carrera-chronos/src/main/java/com/xiaojukeji/chronos/db/Backup

配置 Horizon 的 queue, balance, processes 参数以及 Redis 中的优先级

我假设你正在使用redis和 LaravelHorizon,要开始配置horizon,仅需用我下面的命令清空所有进程队列任务中horizon的仪表盘。我也曾在本系列的a教程中描述过 链接:phpar

Kubernetes监控在小米的落地

本文介绍了高可用、持久存储、可动态调整的Kubernetes监控方案的实现过程。小米的弹性调度平台(Ocean)以及容器平台主要基于开源容器自动化管理平台kubernetes(简称k8s)来提供服务,

Stylus系列——webpack-spritesmith配合stylus使用示例

一、前言基于Webpack的CSSSprites实现方案,若是直接在html中调用雪碧图图标已经很方便,但是实际开发过程可能遇到需要在伪元素中使用雪碧图,或者需要hover切换另一个图标,这种情况下就

redis的常用命令

1,redis安装2,redis的基本指令:3,redis的存储结构之list4,redis的存储结构之Set5,redis的存储结构之zset6,redis的存储结构之hash7,redis的远程访

云数据管理会成为DataOps的未来吗?

如今,现代化的数据管理平台正在成为企业的首先,而传统的数据运维应用平台则逐渐被边缘化。尤其在AI和机器学习技术的推动下,企业数据正在走向以云为核心的数字化征程。根据思科的一份数据显示:到2021年,企

SD-WAN时代 MPLS的命运

SD-WAN网络市场正在蓬勃发展,预计到2025年将增长到170亿美元。这并不奇怪,软件定义的广域网络消除了对昂贵路由器的需求,并实现了云网络连接。它的优势之一是支持安全的云连接,这是多协议标签交换(

基于JS的高性能Flutter动态化框架MXFlutter

导语:18年10月份,手机QQ看点团队尝试使用Flutter,做为iOS开发,一接触到Flutter就马上感受到,Flutter虽然强大,但不能像RN一样动态化是阻碍我们使用她的唯一障碍了。看Goog

“12306”是如何支撑百万QPS的?

12306抢票,极限并发带来的思考每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。尤其是春节

面向DevOps的企业自动化运维体系如何构建?

随着软件交付速度的加快,过去那种研发、测试、部署和运维各自为政的模式,已经无法满足用户需求。越来越多的企业希望通过更高效、更敏捷的方式,快速交付和部署相关应用。所以,DevOps顺势而生!那么,什么是

ie加载不了css的原因是什么?

可能原因如下:1.HTML页面编码与CSS编码不同(如HTML为gbk,CSS为utf-8)。2.CSS文件中未指定@charset头声明,导致IE默认使用页面编码来解码CSS文件(DEMO中IE浏览

dreamweaver和css的区别是什么?

DreamweaverAdobeDreamweaver,简称“DW”,中文名称"梦想编织者",是美国MACROMEDIA公司开发的集网页制作和管理网站于一身的所见即所得网页编辑器。DW是第一套针对专业

jsp和css的区别是什么?

jsp是什么?JSP全名为JavaServerPages,中文名叫java服务器页面,是一种动态网页开发技术。它使用JSP标签在HTML网页中插入Java代码;标签通常以结束。JSP技术有点类似ASP

jsp和css的区别?

一、JSPJSP全名为JavaServerPages,他实现了Html语法中的java扩张(以形式)。JSP与Servlet一样,是在服务器端执行的。通常返回给客户端的就是一个HTML文本,因此客户端

webpack中css的url报错?

webpack中css的url报错?css-loader://打包样式中背景图 { test:/\.(png|jpg)$/, loader:"url-loader?limit=8192&name=im