Prometheus監控有所思:多標籤埋點及Mbean

  使用 grafana+prometheus+jmx 作為普通的監控手段,是比較有用的。我之前的文章介紹了相應的實現辦法。

  但是,按照之前的實現,我們更多的只能是監控 單值型的數據,如請求量,tps 等等,對於複雜組合型的指標卻不容易監控。

  這種情況一般帶有一定的業務屬性,比如想監控mq中的每個topic的消費情況,每類產品的實時訂單情況等等。當然,對於看過完整的 prometheus 的監控數據的同學來說,會覺得很正常,因為你會看到如下的數據:

# HELP java_lang_MemoryPool_PeakUsage_max java.lang.management.MemoryUsage (java.lang<type=MemoryPool, name=Metaspace><PeakUsage>max)
# TYPE java_lang_MemoryPool_PeakUsage_max untyped
java_lang_MemoryPool_PeakUsage_max{name="Metaspace",} -1.0
java_lang_MemoryPool_PeakUsage_max{name="PS Old Gen",} 1.415053312E9
java_lang_MemoryPool_PeakUsage_max{name="PS Eden Space",} 6.96778752E8
java_lang_MemoryPool_PeakUsage_max{name="Code Cache",} 2.5165824E8
java_lang_MemoryPool_PeakUsage_max{name="Compressed Class Space",} 1.073741824E9
java_lang_MemoryPool_PeakUsage_max{name="PS Survivor Space",} 5242880.0

  這裏面的 name 就是普通標籤嘛,同理於其他埋點咯。應該是可以實現的。

  是的,prometheus 是方便實現這玩意的,但是我們之前不是使用 jmx_exportor 作為導出工具嘛,使用的埋點組件是 io.dropwizard.metrics:metrics-core 。

  而它則是重在單值的監控,所以,用它我們是實現不了帶指標的數據的監控了。

  那怎麼辦呢?三個辦法!

1. 直接替換原有的 metrics-core 組件為 prometheus 的client 組件,因為官方是支持這種操作的;
2. 使用 prometheus-client 組件與 metrics-core 組件配合,各自使用各自的功能;
3. 自行實現帶標籤的埋點,這可能是基於 MBean 的;

 

  以上這幾種方案,各有優劣。方案1可能改動太大,而且可能功能不兼容不可行; 方案2可能存在整合不了或者功能衝突情況,當然如果能整合,絕對是最好的; 方案3實現複雜度就高了,比如監控值維護、線程安全、MBean數據吐出方式等等。

  好吧,不管怎麼樣,我們還是都看看吧。

 

一、 使用 prometheus-client 埋點實現帶標籤的監控

  1. 引入 pom 依賴

        <dependency>
            <groupId>io.prometheus</groupId>
            <artifactId>simpleclient</artifactId>
            <version>0.8.0</version>
        </dependency>
        <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient_hotspot</artifactId>
                <version>0.8.0</version>
        </dependency>
        <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient_servlet</artifactId>
                <version>0.8.0</version>
        </dependency>

  2. 框架註冊監控

        @Configuration
        public class PrometheusConfig {
            @Bean
            public ServletRegistrationBean servletRegistrationBean(){
                // 將埋點指標吐出到 /metrics 節點
                return new ServletRegistrationBean(new MetricsServlet(), "/metrics");
            }
        }

  3. 業務埋點數據

        // 註冊指標實例
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  4. 獲取埋點數據信息

        curl http://localhost:8080/metrics
        # 對外暴露http接口調用,結果如下
        # HELP jmx_test_abc_ffff counter usage.
        # TYPE jmx_test_abc_ffff counter
        jmx_test_abc_ffff{topic="bbb",} 1.0
        jmx_test_abc_ffff{topic="2",} 2.0
        jmx_test_abc_ffff{topic="test",} 1.0

  可以看出,效果咱們是實現了。但是,對於已經運行的東西,要改這玩意可能不是那麼友好。主要有以下幾點:

    1. 暴露數據方式變更,原來由javaagent進行統一處理的數據,現在可能由於應用端口的不一,導致收集的配置會變更,不一定符合運維場景;
    2. 需要將原來的埋點進行替換;

 

二、 prometheus-client 與 metrics-core 混合埋點

  不處理以前的監控,將新監控帶標籤數據吐入到 jmx_exportor 中。

  我們試着使用如上的埋點方式:

        // 註冊指標實例
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  好像數據是不會進入的到 jmx_exportor 的。這也不奇怪,畢竟咱們也不了解其原理,難道想靠運氣取勝??

  細去查看 metrics-core 組件的埋點實現方案,發現其是向 MBean 中吐入數據,從而被 jmx_exportor 抓取的。

        // com.codahale.metrics.jmx.JmxReporter.JmxListener#onCounterAdded
        @Override
        public void onCounterAdded(String name, Counter counter) {
            try {
                if (filter.matches(name, counter)) {
                    final ObjectName objectName = createName("counters", name);
                    registerMBean(new JmxCounter(counter, objectName), objectName);
                }
            } catch (InstanceAlreadyExistsException e) {
                LOGGER.debug("Unable to register counter", e);
            } catch (JMException e) {
                LOGGER.warn("Unable to register counter", e);
            }
        }
        // 向 mBeanServer 註冊監控實例
        // 默認情況下 mBeanServer = ManagementFactory.getPlatformMBeanServer();
        private void registerMBean(Object mBean, ObjectName objectName) throws InstanceAlreadyExistsException, JMException {
            ObjectInstance objectInstance = mBeanServer.registerMBean(mBean, objectName);
            if (objectInstance != null) {
                // the websphere mbeanserver rewrites the objectname to include
                // cell, node & server info
                // make sure we capture the new objectName for unregistration
                registered.put(objectName, objectInstance.getObjectName());
            } else {
                registered.put(objectName, objectName);
            }
        }

  而 prometheus-client 則是通過 CollectorRegistry.defaultRegistry 進行註冊實例的。

    // io.prometheus.client.SimpleCollector.Builder#register()
    /**
     * Create and register the Collector with the default registry.
     */
    public C register() {
      return register(CollectorRegistry.defaultRegistry);
    }
    /**
     * Create and register the Collector with the given registry.
     */
    public C register(CollectorRegistry registry) {
      C sc = create();
      registry.register(sc);
      return sc;
    }

  所以,好像原理上來講是不同的。至於到底為什麼不能監控到數據,那還不好說。至少,你可以學習 metrics-core 使用 MBean 的形式將數據導出。這是我們下一個方案要討論的事。

  這裏我可以給到一個最終簡單又不失巧合的方式,實現兩個監控組件的兼容,同時向 jmx_exportor 進行導出。如下:

  1. 引入 javaagent 依賴包

        <!-- javaagent 包,與 外部使用的 jmx_exportor 一致 -->
        <dependency>
            <groupId>io.prometheus.jmx</groupId>
            <artifactId>jmx_prometheus_javaagent</artifactId>
            <version>0.12.0</version>
        </dependency>

  2. 使用 agent 的工具類進行埋點

  因為 javaagent 裏面提供一套完整的 client 工具包,所以,我們可以使用。

        // 註冊指標實例
        // 將 io.prometheus.client.Counter 包替換為 io.prometheus.jmx.shaded.io.prometheus.client.Counter
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  3. 原樣使用 jmx_exportor 就可以導出監控數據了

  為什麼換一個包這樣就可以了?

  因為 jmx_exportor 也是通過註冊 CollectorRegistry.defaultRegistry 來進行收集數據的,我們只要保持與其實例一致,就可以做到在同一個jvm內共享數據了。

 

三、 基於 MBean自行實現帶標籤的埋點

// 測試類
public class PrometheusMbeanMetricsMain {
    private static ConcurrentHashMap<String, AtomicInteger> topicContainer = new ConcurrentHashMap<>();
    private static MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

    public static void main(String[] args) throws Exception {
        // 模擬某個topic
        String commingTopic = "test_topic";
        AtomicInteger myTopic1Counter = getMetricCounter(commingTopic);
        System.out.println("jmx started!");
        while(true){
            System.out.println("---");
            // 計數增加
            myTopic1Counter.incrementAndGet();
            Thread.sleep(10000);
        }
    }

    private static AtomicInteger getMetricCounter(String topic) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        AtomicInteger myTopic1Counter = topicContainer.get(topic);
        if(myTopic1Counter == null) {
            myTopic1Counter = new AtomicInteger(0);
            Hashtable<String, String> tab = new Hashtable<>();
            tab.put("topic", topic);
            // 佔位符,雖然不知道什麼意思,但是感覺很厲害的樣子
            tab.put("_", "_value");
            ObjectName objectName = new ObjectName("mydomain_test", tab);
            // 註冊監控實例 到 MBeanServer 中
            ObjectInstance objectInstance = mBeanServer.registerMBean(new JmxCounter(myTopic1Counter, objectName), objectName);
        }
        return myTopic1Counter;
    }
}
// JmxCounter, MBean 要求: 1. 接口必須定義成Public的;  2. 接口命名規範符合要求, 即接口名叫 XYZMBean ,那麼實現名就必須一定是XYZ;
// DynamicMBean
public interface JmxCounterMBean {
    public Object getCount() throws Exception;
}
public class JmxCounter implements JmxCounterMBean {
    private AtomicInteger metric;
    private ObjectName objectName;

    public JmxCounter(AtomicInteger metric, ObjectName objectName) {
        this.objectName = objectName;
        this.metric = metric;
    }

    @Override
    public Object getCount() throws Exception {
        // 返回監控結果
        return metric.get();
    }

}

  最後,見證奇迹的時刻。結果如下:

# HELP mydomain_test_value_Count Attribute exposed for management (mydomain_test<_=_value, topic=b_topic><>Count)
# TYPE mydomain_test_value_Count untyped
mydomain_test_value_Count{topic="b_topic",} 1.0
mydomain_test_value_Count{topic="a_topic",} 88.0

  很明顯,這是一個糟糕的實現,不要學他。僅為了演示效果。

  所以,總結下來,自然是使用方案2了。兩個組件兼容,實現簡單,性能也不錯。如果只是為了使用,到此就可以了。不過你得明白,以上方案有取巧的成分在。

 

四、 原理: jmx_exportor 是如何獲取數據的?

  jmx_exportor 也是可以通過 http_server 暴露數據。

    // io.prometheus.client.exporter.HTTPServer
    /**
     * Start a HTTP server serving Prometheus metrics from the given registry.
     */
    public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
        server = HttpServer.create();
        server.bind(addr, 3);
        // 使用 HTTPMetricHandler 處理請求
        HttpHandler mHandler = new HTTPMetricHandler(registry);
        // 綁定到 /metrics 地址上
        server.createContext("/", mHandler);
        server.createContext("/metrics", mHandler);
        executorService = Executors.newFixedThreadPool(5, DaemonThreadFactory.defaultThreadFactory(daemon));
        server.setExecutor(executorService);
        start(daemon);
    }    
    /**
     * Start a HTTP server by making sure that its background thread inherit proper daemon flag.
     */
    private void start(boolean daemon) {
        if (daemon == Thread.currentThread().isDaemon()) {
            server.start();
        } else {
            FutureTask<Void> startTask = new FutureTask<Void>(new Runnable() {
                @Override
                public void run() {
                    server.start();
                }
            }, null);
            DaemonThreadFactory.defaultThreadFactory(daemon).newThread(startTask).start();
            try {
                startTask.get();
            } catch (ExecutionException e) {
                throw new RuntimeException("Unexpected exception on starting HTTPSever", e);
            } catch (InterruptedException e) {
                // This is possible only if the current tread has been interrupted,
                // but in real use cases this should not happen.
                // In any case, there is nothing to do, except to propagate interrupted flag.
                Thread.currentThread().interrupt();
            }
        }
    }

  所以,可以主要邏輯是 HTTPMetricHandler 處理。來看看。

        // io.prometheus.client.exporter.HTTPServer.HTTPMetricHandler#handle
        public void handle(HttpExchange t) throws IOException {
            String query = t.getRequestURI().getRawQuery();

            ByteArrayOutputStream response = this.response.get();
            response.reset();
            OutputStreamWriter osw = new OutputStreamWriter(response);
            // 主要由該 TextFormat 進行格式化輸出
            // registry.filteredMetricFamilySamples() 進行數據收集
            TextFormat.write004(osw,
                    registry.filteredMetricFamilySamples(parseQuery(query)));
            osw.flush();
            osw.close();
            response.flush();
            response.close();

            t.getResponseHeaders().set("Content-Type",
                    TextFormat.CONTENT_TYPE_004);
            if (shouldUseCompression(t)) {
                t.getResponseHeaders().set("Content-Encoding", "gzip");
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
                final GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody());
                response.writeTo(os);
                os.close();
            } else {
                t.getResponseHeaders().set("Content-Length",
                        String.valueOf(response.size()));
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.size());
                // 寫向客戶端
                response.writeTo(t.getResponseBody());
            }
            t.close();
        }

    }

 

五、 原理: jmx_exportor 是如何獲取Mbean 的數據的?

  jmx_exportor 有一個 JmxScraper, 專門用於處理 MBean 的值。

    // io.prometheus.jmx.JmxScraper#doScrape
    /**
      * Get a list of mbeans on host_port and scrape their values.
      *
      * Values are passed to the receiver in a single thread.
      */
    public void doScrape() throws Exception {
        MBeanServerConnection beanConn;
        JMXConnector jmxc = null;
        // 默認直接獲取本地的 jmx 信息
        // 即是通過共享 ManagementFactory.getPlatformMBeanServer() 變量來實現通信的
        if (jmxUrl.isEmpty()) {
          beanConn = ManagementFactory.getPlatformMBeanServer();
        } else {
          Map<String, Object> environment = new HashMap<String, Object>();
          if (username != null && username.length() != 0 && password != null && password.length() != 0) {
            String[] credent = new String[] {username, password};
            environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credent);
          }
          if (ssl) {
              environment.put(Context.SECURITY_PROTOCOL, "ssl");
              SslRMIClientSocketFactory clientSocketFactory = new SslRMIClientSocketFactory();
              environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
              environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
          }
          // 如果是遠程獲取,則會通過 rmi 進行遠程通信獲取
          jmxc = JMXConnectorFactory.connect(new JMXServiceURL(jmxUrl), environment);
          beanConn = jmxc.getMBeanServerConnection();
        }
        try {
            // Query MBean names, see #89 for reasons queryMBeans() is used instead of queryNames()
            Set<ObjectName> mBeanNames = new HashSet<ObjectName>();
            for (ObjectName name : whitelistObjectNames) {
                for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
                    mBeanNames.add(instance.getObjectName());
                }
            }

            for (ObjectName name : blacklistObjectNames) {
                for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
                    mBeanNames.remove(instance.getObjectName());
                }
            }

            // Now that we have *only* the whitelisted mBeans, remove any old ones from the cache:
            jmxMBeanPropertyCache.onlyKeepMBeans(mBeanNames);

            for (ObjectName objectName : mBeanNames) {
                long start = System.nanoTime();
                scrapeBean(beanConn, objectName);
                logger.fine("TIME: " + (System.nanoTime() - start) + " ns for " + objectName.toString());
            }
        } finally {
          if (jmxc != null) {
            jmxc.close();
          }
        }
    }
    
    // io.prometheus.jmx.JmxScraper#scrapeBean
    private void scrapeBean(MBeanServerConnection beanConn, ObjectName mbeanName) {
        MBeanInfo info;
        try {
          info = beanConn.getMBeanInfo(mbeanName);
        } catch (IOException e) {
          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
          return;
        } catch (JMException e) {
          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
          return;
        }
        MBeanAttributeInfo[] attrInfos = info.getAttributes();

        Map<String, MBeanAttributeInfo> name2AttrInfo = new LinkedHashMap<String, MBeanAttributeInfo>();
        for (int idx = 0; idx < attrInfos.length; ++idx) {
            MBeanAttributeInfo attr = attrInfos[idx];
            if (!attr.isReadable()) {
                logScrape(mbeanName, attr, "not readable");
                continue;
            }
            name2AttrInfo.put(attr.getName(), attr);
        }
        final AttributeList attributes;
        try {
            // 通過 MBean 調用對象,獲取所有屬性值,略去不說
            attributes = beanConn.getAttributes(mbeanName, name2AttrInfo.keySet().toArray(new String[0]));
        } catch (Exception e) {
            logScrape(mbeanName, name2AttrInfo.keySet(), "Fail: " + e);
            return;
        }
        for (Attribute attribute : attributes.asList()) {
            MBeanAttributeInfo attr = name2AttrInfo.get(attribute.getName());
            logScrape(mbeanName, attr, "process");
            // 處理單個key的屬性值, 如 topic=aaa,ip=1 將會進行再次循環處理
            processBeanValue(
                    mbeanName.getDomain(),
                    // 獲取有效的屬性列表, 我們可以簡單看一下過濾規則, 如下文
                    jmxMBeanPropertyCache.getKeyPropertyList(mbeanName),
                    new LinkedList<String>(),
                    attr.getName(),
                    attr.getType(),
                    attr.getDescription(),
                    attribute.getValue()
            );
        }
    }
    // 處理每個 mBean 的屬性,寫入到 receiver 中
    // io.prometheus.jmx.JmxScraper#processBeanValue
    /**
     * Recursive function for exporting the values of an mBean.
     * JMX is a very open technology, without any prescribed way of declaring mBeans
     * so this function tries to do a best-effort pass of getting the values/names
     * out in a way it can be processed elsewhere easily.
     */
    private void processBeanValue(
            String domain,
            LinkedHashMap<String, String> beanProperties,
            LinkedList<String> attrKeys,
            String attrName,
            String attrType,
            String attrDescription,
            Object value) {
        if (value == null) {
            logScrape(domain + beanProperties + attrName, "null");
        } 
        // 單值情況,数字型,字符串型,可以處理
        else if (value instanceof Number || value instanceof String || value instanceof Boolean) {
            logScrape(domain + beanProperties + attrName, value.toString());
            // 解析出的數據存入 receiver 中,可以是 jmx, 或者 控制台
            this.receiver.recordBean(
                    domain,
                    beanProperties,
                    attrKeys,
                    attrName,
                    attrType,
                    attrDescription,
                    value);
        } 
        // 多值型情況
        else if (value instanceof CompositeData) {
            logScrape(domain + beanProperties + attrName, "compositedata");
            CompositeData composite = (CompositeData) value;
            CompositeType type = composite.getCompositeType();
            attrKeys = new LinkedList<String>(attrKeys);
            attrKeys.add(attrName);
            for(String key : type.keySet()) {
                String typ = type.getType(key).getTypeName();
                Object valu = composite.get(key);
                processBeanValue(
                        domain,
                        beanProperties,
                        attrKeys,
                        key,
                        typ,
                        type.getDescription(),
                        valu);
            }
        } 
        // 更複雜型對象
        else if (value instanceof TabularData) {
            // I don't pretend to have a good understanding of TabularData.
            // The real world usage doesn't appear to match how they were
            // meant to be used according to the docs. I've only seen them
            // used as 'key' 'value' pairs even when 'value' is itself a
            // CompositeData of multiple values.
            logScrape(domain + beanProperties + attrName, "tabulardata");
            TabularData tds = (TabularData) value;
            TabularType tt = tds.getTabularType();

            List<String> rowKeys = tt.getIndexNames();

            CompositeType type = tt.getRowType();
            Set<String> valueKeys = new TreeSet<String>(type.keySet());
            valueKeys.removeAll(rowKeys);

            LinkedList<String> extendedAttrKeys = new LinkedList<String>(attrKeys);
            extendedAttrKeys.add(attrName);
            for (Object valu : tds.values()) {
                if (valu instanceof CompositeData) {
                    CompositeData composite = (CompositeData) valu;
                    LinkedHashMap<String, String> l2s = new LinkedHashMap<String, String>(beanProperties);
                    for (String idx : rowKeys) {
                        Object obj = composite.get(idx);
                        if (obj != null) {
                            // Nested tabulardata will repeat the 'key' label, so
                            // append a suffix to distinguish each.
                            while (l2s.containsKey(idx)) {
                              idx = idx + "_";
                            }
                            l2s.put(idx, obj.toString());
                        }
                    }
                    for(String valueIdx : valueKeys) {
                        LinkedList<String> attrNames = extendedAttrKeys;
                        String typ = type.getType(valueIdx).getTypeName();
                        String name = valueIdx;
                        if (valueIdx.toLowerCase().equals("value")) {
                            // Skip appending 'value' to the name
                            attrNames = attrKeys;
                            name = attrName;
                        } 
                        processBeanValue(
                            domain,
                            l2s,
                            attrNames,
                            name,
                            typ,
                            type.getDescription(),
                            composite.get(valueIdx));
                    }
                } else {
                    logScrape(domain, "not a correct tabulardata format");
                }
            }
        } else if (value.getClass().isArray()) {
            logScrape(domain, "arrays are unsupported");
        } else {
            // 多半會返回不支持的對象然後得不到jmx監控值
            // mydomain_test{3=3, topic=aaa} java.util.Hashtable is not exported
            logScrape(domain + beanProperties, attrType + " is not exported");
        }
    }
    
    // 我們看下prometheus 對 mbeanName 的轉換操作,會將各種特殊字符轉換為 屬性列表
    // io.prometheus.jmx.JmxMBeanPropertyCache#getKeyPropertyList
    public LinkedHashMap<String, String> getKeyPropertyList(ObjectName mbeanName) {
        LinkedHashMap<String, String> keyProperties = keyPropertiesPerBean.get(mbeanName);
        if (keyProperties == null) {
            keyProperties = new LinkedHashMap<String, String>();
            // 轉化為 string 格式
            String properties = mbeanName.getKeyPropertyListString();
            // 此處為 prometheus 認識的格式,已經匹配上了
            Matcher match = PROPERTY_PATTERN.matcher(properties);
            while (match.lookingAt()) {
                keyProperties.put(match.group(1), match.group(2));
                properties = properties.substring(match.end());
                if (properties.startsWith(",")) {
                    properties = properties.substring(1);
                }
                match.reset(properties);
            }
            keyPropertiesPerBean.put(mbeanName, keyProperties);
        }
        return keyProperties;
    }
        // io.prometheus.jmx.JmxMBeanPropertyCache#PROPERTY_PATTERN
        private static final Pattern PROPERTY_PATTERN = Pattern.compile(
            "([^,=:\\*\\?]+)" + // Name - non-empty, anything but comma, equals, colon, star, or question mark
                    "=" +  // Equals
                    "(" + // Either
                    "\"" + // Quoted
                    "(?:" + // A possibly empty sequence of
                    "[^\\\\\"]*" + // Greedily match anything but backslash or quote
                    "(?:\\\\.)?" + // Greedily see if we can match an escaped sequence
                    ")*" +
                    "\"" +
                    "|" + // Or
                    "[^,=:\"]*" + // Unquoted - can be empty, anything but comma, equals, colon, or quote
                    ")");

 

六、 原理: jmx_exportor 為什麼輸出的格式是這樣的?

  prometheus 的數據格式如下,如何從埋點數據轉換?

# HELP mydomain_test_value_Count Attribute exposed for management (mydomain_test<_=_value, topic=b_topic><>Count)
# TYPE mydomain_test_value_Count untyped
mydomain_test_value_Count{topic="b_topic",} 1.0
mydomain_test_value_Count{topic="a_topic",} 132.0

  是一個輸出格式問題,也是一協議問題。

  // io.prometheus.client.exporter.common.TextFormat#write004
  public static void write004(Writer writer, Enumeration<Collector.MetricFamilySamples> mfs) throws IOException {
    /* See http://prometheus.io/docs/instrumenting/exposition_formats/
     * for the output format specification. */
    while(mfs.hasMoreElements()) {
      Collector.MetricFamilySamples metricFamilySamples = mfs.nextElement();
      writer.write("# HELP ");
      writer.write(metricFamilySamples.name);
      writer.write(' ');
      writeEscapedHelp(writer, metricFamilySamples.help);
      writer.write('\n');

      writer.write("# TYPE ");
      writer.write(metricFamilySamples.name);
      writer.write(' ');
      writer.write(typeString(metricFamilySamples.type));
      writer.write('\n');

      for (Collector.MetricFamilySamples.Sample sample: metricFamilySamples.samples) {
        writer.write(sample.name);
        // 帶 labelNames 的,依次輸出對應的標籤
        if (sample.labelNames.size() > 0) {
          writer.write('{');
          for (int i = 0; i < sample.labelNames.size(); ++i) {
            writer.write(sample.labelNames.get(i));
            writer.write("=\"");
            writeEscapedLabelValue(writer, sample.labelValues.get(i));
            writer.write("\",");
          }
          writer.write('}');
        }
        writer.write(' ');
        writer.write(Collector.doubleToGoString(sample.value));
        if (sample.timestampMs != null){
          writer.write(' ');
          writer.write(sample.timestampMs.toString());
        }
        writer.write('\n');
      }
    }
  }

 

  done.

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

2015年宇通共獲新能源汽車補貼逾68億

4月4日,宇通客車發佈2015年年報。報告期內,累計完成客車銷售 67,018 輛,實現營業收入312.1 億元,實現歸屬于母公司所有者淨利潤 35.35 億元。公司新能源客車合計生產20568輛,同比增長173.91%;銷售20446台,同比增長 176.1%。其中純電動客車銷量為13885輛,同比增長706.8%;插電式混合動力客車銷量6560輛,同比增長17.8%。

在收入方面,純電動客車營收約92.62億元,插電式混合動力客車營收47.13億元。公告指出,報告期內,公司抓住了新能源市場的爆發式增長的機會,營業收入同比增長 21.31%,歸屬于母公司所有者的淨利潤同比增長 35.31%,經營活動現金淨流量 60.1 億元。

2015年宇通客車新能源汽車產銷及獲得補貼統計(來源:宇通客車2015年年報)

據瞭解,2015年宇通客車共獲得新能源汽車推廣應用補貼68.565億元。其中純電動客車補貼52.345億元,插電式混合動力客車補貼16.22億元。從獲得的補貼比例來看,純電動客車佔據56.52%的份額,插電式混合動力客車占比34.41%。

根據公告,新能源客車的關鍵零部件中,整車控制系統為自主研發自主生產,集成式電機控制器、電機、超級電容和動力電池系統均與行業綜合實力較強的供應商聯合開發,通過整合行業資源,研製出技術領先有競爭力的零部件,支撐公司新能源客車的技術領先。

報告指出,新能源市場受國家補貼政策的推動快速發展,大量企業切入客車市場,競爭情況更加複雜,隨著補貼政策的進一步完善,產品經過時間的檢驗,技術實力強、產品優質、綜合性價比高的企業將在未來新能源客車市場中居於主導地位。

根據公告,2015年,宇通客車實現了全產品端新能源產品的覆蓋,在節能技術、NVH 技術、無人駕駛技術等方面開展了深入研究,整體研發實力進一步增強。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

SpEL + AOP實現註解的動態賦值.

一、自定義註解

先聊聊這個需求,我需要根據用戶的權限對數據進行一些處理,但是痛點在哪裡呢?用戶的權限是在請求的時候知道的,我怎麼把用戶的權限傳遞給處理規則呢?想了以下幾種方案:

  1. Mybatis 攔截器:如果你的權限參數可以滲透到 Dao 層,那麼這是最好的處理方式,直接在 Dao 層數據返回的時候,根據權限做數據處理。
  2. Dubbo 過濾器:如果 Dao 層沒辦法實現的話,只好考慮在 service 層做數據處理了。
  3. ResponseBodyAdvice :要是 service 層也沒辦法做到,只能在訪問層數據返回的時候,根據權限做數據處理。(以下介紹的正是這種方式)

那麼現在有個難點就是:我怎麼把 request 的權限參數傳遞到 response 中呢?當然可以在 Spring 攔截器中處理,但是我不想把這段代碼侵入到完整的鑒權邏輯中。突然想到,我能不能像 spring-data-redis 中 @Cacheable 一樣,利用註解和 SpEL 表達式動態的傳遞權限參數呢?然後在 ResponseBodyAdvice 讀取這個註解的權限參數,進而對數據進行處理。

首先,我們需要有個自定義註解,它有兩個參數:key 表示 SpEL 表達式;userType 表示權限參數。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseSensitiveOverride {

    /**
     * SPEL 表達式
     *
     * @return
     */
    String key() default "";

    /**
     * 1:主賬號、2:子賬號
     */
    int userType() default 1;
}

然後,把這個註解放在路由地址上,key 寫入獲取權限參數的 SpEL 表達式:

    @ResponseSensitiveOverride(key = "#driverPageParam.getUserType()")
    @RequestMapping(value = "/queryPage", method = RequestMethod.POST)
    public ResponseData<PageVo<AdminDriverVo>> queryPage(@RequestBody AdminDriverPageParam driverPageParam) {
        return driverService.queryPageAdmin(driverPageParam);
    }

二、SpEl + AOP 註解賦值

現在 SpEL 表達式是有了,怎麼把 SpEL 表達式的結果賦值給註解的 userType 參數呢?這就需要用 、 和 的知識。

@Aspect
@Component
public class SensitiveAspect {

    private SpelExpressionParser spelParser = new SpelExpressionParser();

    /**
     * 返回通知
     */    
    @AfterReturning("@annotation(com.yungu.swift.base.model.annotation.ResponseSensitiveOverride) && @annotation(sensitiveOverride)")
    public void doAfter(JoinPoint joinPoint, ResponseSensitiveOverride sensitiveOverride) throws Exception {
        //獲取方法的參數名和參數值
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        List<String> paramNameList = Arrays.asList(methodSignature.getParameterNames());
        List<Object> paramList = Arrays.asList(joinPoint.getArgs());

        //將方法的參數名和參數值一一對應的放入上下文中
        EvaluationContext ctx = new StandardEvaluationContext();
        for (int i = 0; i < paramNameList.size(); i++) {
            ctx.setVariable(paramNameList.get(i), paramList.get(i));
        }

        // 解析SpEL表達式獲取結果
        String value = spelParser.parseExpression(sensitiveOverride.key()).getValue(ctx).toString();
        //獲取 sensitiveOverride 這個代理實例所持有的 InvocationHandler
        InvocationHandler invocationHandler = Proxy.getInvocationHandler(sensitiveOverride);
        // 獲取 invocationHandler 的 memberValues 字段
        Field hField = invocationHandler.getClass().getDeclaredField("memberValues");
        // 因為這個字段是 private final 修飾,所以要打開權限
        hField.setAccessible(true);
        // 獲取 memberValues
        Map memberValues = (Map) hField.get(invocationHandler);
        // 修改 value 屬性值
        memberValues.put("userType", Integer.parseInt(value));

    }
}

通過這種方式,我們就實現了為註解動態賦值。

三、ResponseBodyAdvice 處理數據

現在要做的事情就是在 ResponseBody 數據返回前,對數據進行攔截,然後讀取註解上的權限參數,從而對數據進行處理,這裏使用的是 SpringMVC 的 ResponseBodyAdvice 來實現:

@Slf4j
@RestControllerAdvice
@Order(-1)
public class ResponseBodyAdvice implements org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice {

    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return SysUserDto.USER_TYPE_PRIMARY;
        }
    };

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        if (returnType.hasMethodAnnotation(ResponseSensitiveOverride.class)) {
            ResponseSensitiveOverride sensitiveOverride = returnType.getMethodAnnotation(ResponseSensitiveOverride.class);
            threadLocal.set(sensitiveOverride.userType());
            return true;
        }
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body != null && SysUserDto.USER_TYPE_SUB.equals(threadLocal.get())) {
            // 業務處理
        }
        return body;
    }
}

題外話,其實我最後還是擯棄了這個方案,選擇了 Dubbo 過濾器的處理方式,為什麼呢?因為在做數據導出的時候,這種方式沒辦法對二進制流進行處理呀!汗~ 但是該方案畢竟耗費了我一個下午的心血,還是在此記錄一下,可能有它更好的適用場景!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

用這個庫 3 分鐘實現讓你滿意的表格功能:Bootstrap-Table

本文作者:HelloGitHub-kalifun

這是 HelloGitHub 推出的系列,今天給大家推薦一個基於 Bootstrap 和 jQuery 的表格插件:Bootstrap-Table

一、介紹

從項目名稱就可以知道,這是一款 Bootstrap 的表格插件。表格的展示的形式所有的前端幾乎在工作中都有涉及過,Bootstrap Table 提供了快速的建表、查詢、分頁、排序等一系列功能。

項目地址:https://github.com/wenzhixin/bootstrap-table

可能 Bootstrap 和 jQuery 技術有些過時了,但如果因為歷史的技術選型或者舊的項目還在用這兩個庫的話,那這個項目一定會讓你的嘴角慢慢上揚,拿下錶格展示方面的需求易如反掌!

二、模式

Boostatrp Table 分為兩種模式:客戶端(client)模式、服務端(server)模式。

  • 客戶端:通過數據接口將服務器需要加載的數據一次性展現出來,然後裝換成 json 然後生成 table。我們可以自己定義显示行數,分頁等,此時就不再會向服務器發送請求了。

  • 服務器:根據設定的每頁記錄數和當前显示頁,發送數據到服務器進行查詢。

三、實戰操作

Tips: 解釋說明均在代碼中以註釋方式展示,請大家注意閱讀。

我們採用的是最簡單的 CDN 引入方式,代碼可直接運行。複製代碼並將配置好 json 文件的路徑即可看到效果。

3.1 快速上手

註釋中的星號表示該參數必寫,話不多說上代碼。示例代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello, Bootstrap Table!</title>
    // 引入 css
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.css">
</head>
<body>
    // 需要填充的表格
    <table id="tb_departments" data-filter-control="true" data-show-columns="true"></table>
// 引入js
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.js"></script>
<script>
        window.operateEvents = {
            // 當點擊 class=delete 時觸發
            'click .delete': function (e,value,row,index) {
                // 在 console 打印出整行數據
                console.log(row);
            }
        };

        $('#tb_departments').bootstrapTable({
            url: '/frontend/bootstrap-table/user.json',         //請求後台的 URL(*)
            method: 'get',                      //請求方式(*)
            // data: data,                      //當不使用上面的後台請求時,使用data來接收數據
            toolbar: '#toolbar',                //工具按鈕用哪個容器
            striped: true,                      //是否显示行間隔色
            cache: false,                       //是否使用緩存,默認為 true,所以一般情況下需要設置一下這個屬性(*)
            pagination: true,                   //是否显示分頁(*)
            sortable: false,                    //是否啟用排序
            sortOrder: "asc",                   //排序方式
            sidePagination: "client",           //分頁方式:client 客戶端分頁,server 服務端分頁(*)
            pageNumber:1,                       //初始化加載第一頁,默認第一頁
            pageSize: 6,                        //每頁的記錄行數(*)
            pageList: [10, 25, 50, 100],        //可供選擇的每頁的行數(*)
            search: true,                       //是否顯示錶格搜索,此搜索是客戶端搜索,不會進服務端,所以個人感覺意義不大
            strictSearch: true,                 //啟用嚴格搜索。禁用比較檢查。
            showColumns: true,                  //是否显示所有的列
            showRefresh: true,                  //是否显示刷新按鈕
            minimumCountColumns: 2,             //最少允許的列數
            clickToSelect: true,                //是否啟用點擊選中行
            height: 500,                        //行高,如果沒有設置 height 屬性,表格自動根據記錄條數覺得表格高度
            uniqueId: "ID",                     //每一行的唯一標識,一般為主鍵列
            showToggle:true,                    //是否显示詳細視圖和列表視圖的切換按鈕
            cardView: false,                    //是否显示詳細視圖
            detailView: false,                  //是否显示父子表
            showExport: true,                   //是否显示導出
            exportDataType: "basic",            //basic', 'all', 'selected'.
            columns: [{
                checkbox: true     //複選框標題,就是我們看到可以通過複選框選擇整行。
            }, {
                field: 'id', title: 'ID'       //我們取json中id的值,並將表頭title設置為ID
            }, {
                field: 'username', title: '用戶名'         //我們取 json 中 username 的值,並將表頭 title 設置為用戶名
            },{
                field: 'sex', title: '性別'                //我們取 json 中 sex 的值,並將表頭 title 設置為性別
            },{
                field: 'city', title: '城市'               //我們取 json 中 city 的值,並將表頭 title 設置為城市
            },{
                field: 'sign', title: '簽名'               //我們取 json 中 sign 的值,並將表頭 title 設置為簽名
            },{
                field: 'classify', title: '分類'           //我們取 json 中 classify 的值,並將表頭 title 設置為分類
            },{
                //ormatter:function(value,row,index) 對後台傳入數據 進行操作 對數據重新賦值 返回 return 到前台
                // events 觸發事件
                field: 'Button',title:"操作",align: 'center',events:operateEvents,formatter:function(value,row,index){
                    var del = '<button type="button" class="btn btn-danger delete">刪除</button>'
                    return del;
                }
            }
            ],
            responseHandler: function (res) {
                return res.data      //在加載遠程數據之前,處理響應數據格式.
                // 我們取的值在data字段中,所以需要先進行處理,這樣才能獲取我們想要的結果
            }
        });
</script>
</body>
</html>

上面的代碼展示通過基本 API 實現基礎的功能,示例代碼並沒有羅列所有的 API。該庫還有很多好玩的功能等着大家去發現,正所謂師父領進門修行靠個人~

3.2 拆解講解

下面對關鍵點進行闡述,為了更方便使用的小夥伴清楚插件的用法。

3.2.1 初始化部分

選擇需要初始化表格。
$('#tb_departments').bootstrapTable({})
這個就像table的入口一樣。
<table id="tb_departments" data-filter-control="true" data-show-columns="true"></table>

3.2.2 閱讀數據部分

columns:[{field: 'Key', title: '文件路徑',formatter: function(value,row,index){} }]
  • field json 中鍵值對中的 Key
  • title 是表格頭显示的內容
  • formatter 是一個函數類型,當我們對數據內容需要修改時會用它。例:編碼轉換

3.2.3 事件觸發器

events:operateEvents
 window.operateEvents = {
        'click .download': function (e,value,row,index) {
            console.log(row);
        }
   }

因為很多時候我們需要針對錶格進行處理,所以事件觸發器是一個不錯的選擇。比如:它可以記錄我們的行數據,可以利用觸發器進行定製函數的執行等。

四、擴展

介紹幾個擴展可以讓我們便捷的實現更多的表格功能,而不需要自己造輪子讓我們的工作更加高效(也可以進入官網查看擴展的具體使用方法,官方已經收集了大量的擴展)。老規矩直接上代碼:

4.1 表格導出

<script src="js/bootstrap-table-export.js"></script> 
showExport: true,                                           //是否显示導出
exportDataType: basic,                                      //導出數據類型,支持:'基本','全部','選中'
exportTypes:['json', 'xml', 'csv', 'txt', 'sql', 'excel']   //導出類型

4.2 自動刷新

<script src="extensions/auto-refresh/bootstrap-table-auto-refresh.js"></script>
autoRefresh: true,                              //設置 true 為啟用自動刷新插件。這並不意味着啟用自動刷新
autoRefreshStatus: true,                        //設置 true 為啟用自動刷新。這是表加載時狀態自動刷新
autoRefreshInterval: 60,                        //每次發生自動刷新的時間(以秒為單位)
autoRefreshSilent: true                         //設置為靜默自動刷新

4.3 複製行

<script src="extensions/copy-rows/bootstrap-table-copy-rows.js"></script>
showCopyRows: true,                                 //設置 true 為显示複製按鈕。此按鈕將所選行的內容複製到剪貼板
copyWithHidden: true,                               //設置 true 為使用隱藏列進行複製
copyDelimiter: ', ',                                //複製時,此分隔符將插入列值之間
copyNewline: '\n'                                   //複製時,此換行符將插入行值之間

五、總結

本篇文章只是簡單的闡述 Bootstrap-Table 如何使用,正在對錶格功能實現而憂愁的小夥伴,可以使用 HelloGitHub 推薦的這款插件。你會發現網頁製作表格還可以如此快捷,期待小夥伴挖掘出更加有意思的功能哦。

注:上面 js 部分並沒有採用函數形式,建議在使用熟悉之後還是採用函數形式,這樣也方便復用及讓代碼看起來更加規範。

六、參考資料

『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟着我們的文章,你會發現編程的樂趣、使用和發現參与開源項目如此簡單。歡迎留言聯繫我們、加入我們,讓更多人愛上開源、貢獻開源~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

動物皮草太殘忍 洛杉磯市議會全體贊成禁售

摘錄自2018年9月21日蘋果日報美國洛杉磯報導

洛杉磯市議會周二(18日)通過議案,將立法禁止銷售皮草衣飾。議會全體投下贊成票,立場堅定;洛杉磯將成為美國禁售皮草的最大城市,可望為其他時尚重鎮帶來示範作用。

洛杉磯市議會以12比0的票數,一致贊成禁止商業皮草。立法機構負責草擬法規,由市議會審核,正式法規將在通過審議的兩年後生效。預計這類法規會為宗教目的、合法漁獵執照持有者另闢途徑,允許合法使用或生產動物皮草。

加州舊金山、西好萊塢、柏克萊都已限制皮草,但像洛杉磯這麼大規模的城市還是首例。提出此議案的議員科瑞茲(Paul Koretz)表示,洛杉磯是世界時尚之都,期許此舉能成為世界典範,紐約、芝加哥和邁阿密等大城可以跟進。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

中國發佈EVOP電動汽車運營平臺 打造電動汽車網聯大腦

日前,中能工業智慧技術研究院發佈了EVOP電動汽車運營平臺,打造中國最強電動汽車網聯大腦。

從傳統意義上來說,電動汽車只是“行駛+充電”的物理組合,滿足人們最基本的代步需求;而EVOP在此基礎上,通過互聯網和智慧化平臺,將電動汽車和充電設備打造成為能源互聯網產業鏈的重要一環。

和其他智慧化汽車應用相比,EVOP平臺基於中國最強工業大腦DPEN而打造,將資料、資訊和互聯網相結合,讓電動汽車產業鏈變得更智慧。DPEN支持數千萬個採集節點。在DPEN的引領之下,源源不斷的資料進入EVOP平臺,分門別類進行存儲和分析,並通過互聯網傳遞到每一輛電動汽車或者充電設備上,指導設備智慧化、高效率運行,並實現充電網、互聯網、車聯網“三網融合”。

在充電端,EVOP可以輕鬆實現智慧充電功能,它可以即時檢測並調整充電狀態,加強電池的健康管理,引導智慧有序充電、計量計費,並讓車主通過手機隨時瞭解充電情況;而商業地產、物業管理公司、電動汽車廠商等運營商和服務商可以通過雲平臺實現充電樁和車載電池的智慧管理,提供良好的增值服務。

在行駛端,由EVOP平臺海量採集的資料經過精確梳理和分析,通過雲平臺提供給每一位車主,在EVOP營造的車聯網中智慧、高效、安全出行。人們不僅可以隨時瞭解車輛和電池的資訊,快速查詢身邊的充電設備、預約充電;也可以在EVOP的指導下獲得最佳行車路線和最佳能效使用方案;亦可以在EVOP的社交平臺中交流經驗、分享資訊、找到志同道合的朋友,享受 “大資料+互聯網”的時尚車生活。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

go中的關鍵字-select

1. select的使用

  定義:在golang裡頭select的功能與epoll(nginx)/poll/select的功能類似,都是堅挺IO操作,當IO操作發生的時候,觸發相應的動作。

1.1 一些使用規範

  在Go的語言規範中,select中的case的執行順序是隨機的,當有多個case都可以運行,select會隨機公平地選出一個執行,其他的便不會執行:

 1 package main
 2 
 3 import "fmt"
 4 
 5 func main() {
 6     ch := make (chan int, 1)
 7 
 8     ch<-1
 9     select {
10     case <-ch:
11         fmt.Println("隨機一")
12     case <-ch:
13         fmt.Println("隨機二n")
14     }
15 }

  輸出內容為隨機一二里面的任意一個。

  case後面必須是channel操作,否則報錯;default子句總是可運行的,所以沒有default的select才會阻塞等待事件 ;沒有運行的case,那麼將會阻塞事件發生報錯(死鎖)。

1.2 select的應用場景

timeout 機制(超時判斷)
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func main() {
 9     timeout := make (chan bool, 1)
10     go func() {
11         time.Sleep(1*time.Second) // 休眠1s,如果超過1s還沒I操作則認為超時,通知select已經超時啦~
12         timeout <- true
13     }()
14     ch := make (chan int)
15     select {
16     case <- ch:
17     case <- timeout:
18         fmt.Println("超時啦!")
19     }
20 }

  也可以這麼寫:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func main() {
 9     ch := make (chan int)
10     select {
11     case <-ch:
12     case <-time.After(time.Second * 1): // 利用time來實現,After代表多少時間后執行輸出東西
13         fmt.Println("超時啦!")
14     }
15 }

  判斷channel是否阻塞(或者說channel是否已經滿了)

 1 package main
 2 
 3 import (
 4     "fmt"
 5 )
 6 
 7 func main() {
 8     ch := make (chan int, 1)  // 注意這裏給的容量是1
 9     ch <- 1
10     select {
11     case ch <- 2:
12     default:
13         fmt.Println("通道channel已經滿啦,塞不下東西了!")
14     }
15 }

  退出機制

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func main() {
 9     i := 0
10     ch := make(chan string, 0)
11     defer func() {
12         close(ch)
13     }()
14 
15     go func() {
16         DONE: 
17         for {
18             time.Sleep(1*time.Second)
19             fmt.Println(time.Now().Unix())
20             i++
21 
22             select {
23             case m := <-ch:
24                 println(m)
25                 break DONE // 跳出 select 和 for 循環
26             default:
27             }
28         }
29     }()
30 
31     time.Sleep(time.Second * 4)
32     ch<-"stop"
33 }

2. select的實現

  select-case中的chan操作編譯成了if-else。如:

1  select {
2  case v = <-c:
3          ...foo
4  default:
5          ...bar
6  }

  會被編譯為:

1  if selectnbrecv(&v, c) {
2          ...foo
3  } else {
4          ...bar
5  }

  類似地

1  select {
2  case v, ok = <-c:
3      ... foo
4  default:
5      ... bar
6  }

  會被編譯為:

1  if c != nil && selectnbrecv2(&v, &ok, c) {
2      ... foo
3  } else {
4      ... bar
5  }

  selectnbrecv函數只是簡單地調用runtime.chanrecv函數,不過是設置了一個參數,告訴當runtime.chanrecv函數,當不能完成操作時不要阻塞,而是返回失敗。也就是說,所有的select操作其實都僅僅是被換成了if-else判斷,底層調用的不阻塞的通道操作函數。

  在Go的語言規範中,select中的case的執行順序是隨機的,那麼,如何實現隨機呢?

  select和case關鍵字使用了下面的結構體:

1 struct    Scase
2   {
3       SudoG    sg;            // must be first member (cast to Scase)
4       Hchan*    chan;        // chan
5       byte*    pc;            // return pc
6       uint16    kind;
7       uint16    so;            // vararg of selected bool
8       bool*    receivedp;    // pointer to received bool (recv2)
9   };
1  struct    Select
2      {
3      uint16    tcase;            // 總的scase[]數量
4      uint16    ncase;            // 當前填充了的scase[]數量
5      uint16*    pollorder;        // case的poll次序
6      Hchan**    lockorder;        // channel的鎖住的次序
7      Scase    scase[1];        // 每個case會在結構體里有一個Scase,順序是按出現的次序
8  };

  每個select都對應一個Select結構體。在Select數據結構中有個Scase數組,記錄下了每一個case,而Scase中包含了Hchan。然後pollorder數組將元素隨機排列,這樣就可以將Scase亂序了。

 3. select死鎖

  select不注意也會發生死鎖,分兩種情況:

  如果沒有數據需要發送,select中又存在接收通道數據的語句,那麼將發送死鎖

1 package main
2 func main() {  
3     ch := make(chan string)
4     select {
5     case <-ch:
6     }
7 }

  預防的話加default。

  空select,也會引起死鎖。

1 package main
2 
3 func main() {  
4     select {}
5 }

 4. select和switch的區別

select

select只能應用於channel的操作,既可以用於channel的數據接收,也可以用於channel的數據發送。如果select的多個分支都滿足條件,則會隨機的選取其中一個滿足條件的分支, 如規範中所述:

If multiple cases can proceed, a uniform pseudo-random choice is made to decide which single communication will execute.

`case`語句的表達式可以為一個變量或者兩個變量賦值。有default語句。

31 package main                                                                                                                                              32 import "time"
33 import "fmt"                                                                                                                                              
35 func main() {                                                                                                                                             36     c1 := make(chan string)
37     c2 := make(chan string)                                                                                                                               38     go func() {
39         time.Sleep(time.Second * 1)                                                                                                                       40         c1 <- "one"
41     }()                                                                                                                                                   42     go func() {
43         time.Sleep(time.Second * 2)                                                                                                                       44         c2 <- "two"
45     }()                                                                                                                                                   46     for i := 0; i < 2; i++ {
47         select {                                                                                                                                          48             case msg1 := <-c1:
49             fmt.Println("received", msg1)          
50 case msg2 := <-c2: 51 fmt.Println("received", msg2)
52 } 53 }

switch

  switch可以為各種類型進行分支操作, 設置可以為接口類型進行分支判斷(通過i.(type))。switch 分支是順序執行的,這和select不同。

 1 package main                  
 2 import "fmt"
 3 import "time"  
 4 
 5 func main() {                                                                                                                             
 6      i := 2
 7      fmt.Print("Write ", i, " as ")  
 8      switch i {
 9          case 1:
10          fmt.Println("one")
11          case 2:                                                                                                                                  
12          fmt.Println("two")
13          case 3:                                                                                                                      
14          fmt.Println("three")
15      }                                                                                                                                             
16      switch time.Now().Weekday() {
17          case time.Saturday, time.Sunday:
18          fmt.Println("It's the weekend")
19          default:                                                                                                                                      
20          fmt.Println("It's a weekday")
21      }                                                                                                                                                 
22      t := time.Now()
23      switch {                                                                                                                                         
24          case t.Hour() < 12:
25          fmt.Println("It's before noon")                                                                                                              
26          default:
27          fmt.Println("It's after noon")                                                                                                                  
28      }
29      whatAmI := func(i interface{}) {                                                                                                                   
30          switch t := i.(type) {
31              case bool:                                                                                                                              
32              fmt.Println("I'm a bool")
33              case int:                                                                                                                                 
34              fmt.Println("I'm an int")
35              default:                                                                                                                                 
36              fmt.Printf("Don't know type %T\n", t)
37          }
38      }
39      whatAmI(true)                                                                                                                                     
40      whatAmI(1)
41      whatAmI("hey")                                                                                                                                 
42  }

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

Tesla年產50萬電動車牛皮吹太大,供應商坦承沒把握

特斯拉(Tesla)計畫在2018年大規模量產平價電動車Model 3,時程較原先計畫提早兩年,雖然展現特斯拉執行長穆斯克(Elon Musk)強大企圖心,但也飽受專家質疑,甚至特斯拉旗下供應商對於是否能達成目標也沒把握。   Model 3已開放預定,特斯拉目前累計收到37.3萬輛的訂單、預計2017年底開始交車。但外界所不知道的是,供應商是在三個月之前才被告知增產計畫提前,相關準備作業是否先完成令人質疑。據路透社引述知情人事消息報導指出,特斯拉最新計畫把2017年Model 3產出規模提高兩倍至10萬輛,2018年更將達成40萬輛的產出水平。   穆斯克4月28日曾在財報法說會上對分析師誇下海口,2018年整體產出規模(含Model S、Model X、Model 3)將來到50萬輛,是去年的十倍,也較預設時程快了兩年。不過Model 3設計目前還要等到6月才定案,距初次量產僅剩13個月。   電動車零組件何其多,且缺一不可,特斯拉如何在短時間內搞定供應商、備齊所有零組件,雖然並非完全不可能,但挑戰難度絕對相當高,這也考驗穆斯克的管理協調能力。值得一提的是,北美目前僅有少數汽車產線有年產50萬輛的能力,且背後都有幾十年經驗的車廠在運籌帷幄。   (本文內容由授權使用;首圖來源: CC BY 2.0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

jwt 實踐應用以及特殊案例思考

JSON Web Token 是 出的一份標準,使用 JSON 來傳遞數據,用於判定用戶是否登錄狀態。

jwt 之前,使用 session 來做用戶認證。

以下代碼均使用 javascript 編寫。

  • 原文鏈接:

session

傳統判斷是否登錄的方式是使用 session + token

token 是指在客戶端使用 token 作為用戶狀態憑證,瀏覽器一般存儲在 localStorage 或者 cookie 中。

session 是指在服務器端使用 redis 或者 sql 類數據庫,存儲 user_id 以及 token 的鍵值對關係,基本工作原理如下。

在服務器端使用 sessions 存儲鍵值對

const sessions = {
  "ABCED1": 10086,
  "CDEFA0": 10010
}

每次客戶端請求帶權限數據時攜帶 token,在服務器端根據 token 與 sessions 獲取 user_id, 完成認證過程

function getUserIdByToken (token) {
  return sessions[token]
}

如果存儲在 cookie 中就是經常聽到的 session + cookie 的登錄方案。其實存儲在 cookielocalStorage 甚至 IndexedDB 或者 WebSQL 各有利弊,核心思想一致。

關於 cookie 以及 token 優缺點,在 中有討論。

如果不使用 cookie,可以採取 localStorage + Authorization 的方式進行認證,更加無狀態化

// http 的頭,每次請求權限接口時,需要攜帶 Authorization Header
const headers = {
  Authorization: `Bearer ${localStorage.get('token')}`
}

推薦一個前端的存儲庫 ,使用 IndexedDBWebSQL 以及 IndexedDB 做鍵值對存儲。

無狀態登錄

session 需要在數據庫中保持用戶及token對應信息,所以叫 有狀態

試想一下,如何在數據庫中不保持用戶狀態也可以登錄。

第一種方法: 前端直接傳 user_id 給服務端

缺點也特別特別明顯,容易被用戶篡改成任意 user_id,權限設置形同虛設。不過思路正確,接着往下走。

改進: 對 user_id 進行對稱加密

服務端對 user_id 進行對稱加密后,作為 token 返回客戶端,作為用戶狀態憑證。比上邊略微強點,但由於對稱加密,選擇合適的算法以及密鑰比較重要

改進: 對 user_id 不需要加密,只需要進行簽名,保證不被篡改

這便是 jwt 的思想:user_id,加密算法和簽名組成 token 一起存儲到客戶端,每當客戶端請求接口時攜帶 token,服務器根據 token 解析出加密算法與 user_id 來判斷簽名是否一致。

Json Web Token

jwt 根據 HeaderPayload 以及 Signature 三個部分由 . 拼接而成。

Header

Header 由非對稱加密算法和類型組成,如下

const header = {
  // 加密算法
  alg: 'HS256',
  type: 'jwt'
}

Payload

Payload 中由 以及需要通信的數據組成。這些數據字段也叫 Claim

Registered Claim 中比較重要的是 "exp" Claim 表示過期時間,在用戶登錄時會設置過期時間。

const payload = {
  // 表示 jwt 創建時間
  iat: 1532135735,

  // 表示 jwt 過期時間
  exp: 1532136735,

  // 用戶 id,用以通信
  user_id: 10086
}

Signature

SignatureHeaderPayload 以及 secretOrPrivateKey 計算而成。secretOrPrivateKey 作為敏感數據存儲在服務器端,可以考慮使用 vault secret 或者 k8s secret

對於 secretOrPrivateKey,如果加密算法採用 HMAC,則為字符串,如果採用 RSA 或者 ECDSA,則為 PrivateKey。

// 由 HMACSHA256 算法進行簽名,secret 不能外泄
const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)

// jwt 由三部分拼接而成
const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign

從生成 jwt 規則可知客戶端可以解析出 payload,因此不要在 payload 中攜帶敏感數據,比如用戶密碼

校驗過程

在生成規則中可知,jwt 前兩部分是對 header 以及 payload 的 base64 編碼。

當服務器收到客戶端的 token 后,解析前兩部分得到 header 以及 payload,並使用 header 中的算法與 secretOrPrivateKey 進行簽名,判斷與 jwt 中攜帶的簽名是否一致。

帶個問題,如何判斷 token 過期?

應用

由上可知,jwt 並不對數據進行加密,而是對數據進行簽名,保證不被篡改。除了在登錄中可以用到,在進行郵箱校驗,圖形驗證碼和短信驗證碼時也可以用到。

圖形驗證碼

在登錄時,輸入密碼錯誤次數過多會出現圖形驗證碼。

圖形驗證碼的原理是給客戶端一個圖形,並且在服務器端保存與這個圖片配對的字符串,以前也大都通過 session 來實現。

可以把驗證碼配對的字符串作為 secret,進行無狀態校驗。

const jwt = require('jsonwebtoken')

// 假設驗證碼為字符驗證碼,字符為 ACDE,10分鐘失效
const token = jwt.sign({}, secrect + 'ACDE', { expiresIn: 60 * 10 })

const codeImage = getImageFromString('ACDE')

// 給前端的響應
const res = {
  // 驗證碼圖片的 token,從中可以校驗前端發送的驗證碼
  token,
  // 驗證碼圖片
  codeImage,
}

短信驗證碼與圖形驗證碼同理

郵箱校驗

現在網站在註冊成功後會進行郵箱校驗,具體做法是給郵箱發一個鏈接,用戶點開鏈接校驗成功。

// 把郵箱以及用戶id綁定在一起
const code = jwt.sign({ email, userId }, secret, { expiresIn: 60 * 30 })

// 在此鏈接校驗驗證碼
const link = `https://example.com/code=${code}`

無狀態 VS 有狀態

關於無狀態和有狀態,在其它技術方向也有對比,比如 React 的 stateLess component 以及 stateful component,函數式編程中的副作用可以理解為狀態,http 也是一個無狀態協議,需要靠 header 以及 cookie 攜帶狀態。

在用戶認證這裏,有無狀態是指是否依賴外部數據存儲,如 mysql,redis 等。

案例

思考以下幾個關於登錄的問題如何使用 session 以及 jwt 實現,來更加清楚 jwt 的使用場景

當用戶註銷時,如何使該 token 失效

因為 jwt 無狀態,不保存用戶設備信息,沒法單純使用它完成以上問題,可以再利用數據庫保存一些狀態完成。

  • session: 只需要把 user_id 對應的 token 清掉即可
  • jwt: 使用 redis,維護一張黑名單,用戶註銷時把該 token 加入黑名單,過期時間與 jwt 的過期時間保持一致。

如何允許用戶只能在一個設備登錄,如微信

  • session: 使用 sql 類數據庫,對用戶數據庫表添加 token 字段並加索引,每次登陸重置 token 字段,每次請求需要權限接口時,根據 token 查找 user_id
  • jwt: 假使使用 sql 類數據庫,對用戶數據庫表添加 token 字段(不需要添加索引),每次登陸重置 token 字段,每次請求需要權限接口時,根據 jwt 獲取 user_id,根據 user_id 查用戶表獲取 token 判斷 token 是否一致。另外也可以使用計數器的方法,如下一個問題。

對於這個需求,session 稍微簡單些,畢竟 jwt 也需要依賴數據庫。

如何允許用戶只能在最近五個設備登錄,如諸多播放器

  • session: 使用 sql 類數據庫,創建 token 數據庫表,有 id, token, user_id 三個字段,user 與 token 表為 1:m 關係。每次登錄添加一行記錄。根據 token 獲取 user_id,再根據 user_id 獲取該用戶有多少設備登錄,超過 5 個,則刪除最小 id 一行。
  • jwt: 使用計數器,使用 sql 類數據庫,在用戶表中添加字段 count,默認值為 0,每次登錄 count 字段自增1,每次登錄創建的 jwt 的 Payload 中攜帶數據 current_count 為用戶的 count 值。每次請求權限接口時,根據 jwt 獲取 count 以及 current_count,根據 user_id 查用戶表獲取 count,判斷與 current_count 差值是否小於 5

對於這個需求,jwt 略簡單些,而使用 session 還需要多維護一張 token 表。

如何允許用戶只能在最近五個設備登錄,而且使某一用戶踢掉除現有設備外的其它所有設備,如諸多播放器

  • session: 在上一個問題的基礎上,刪掉該設備以外其它所有的token記錄。
  • jwt: 在上一個問題的基礎上,對 count + 5,並對該設備重新賦值為新的 count。

如何显示該用戶登錄設備列表 / 如何踢掉特定用戶

  • session: 在 token 表中新加列 device
  • jwt: 需要服務器端保持設備列表信息,做法與 session 一樣,使用 jwt 意義不大

總結

從以上問題得知,如果不需要控制登錄設備數量以及設備信息,無狀態的 jwt 是一個不錯的選擇。一旦涉及到了設備信息,就需要對 jwt 添加額外的狀態支持,增加了認證的複雜度,此時選用 session 是一個不錯的選擇。

jwt 不是萬能的,是否採用 jwt,需要根據業務需求來確定。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

中國財政部稱對新能源汽車騙補的現場核查已完成

財政部日前發佈聲明,表示對新能源汽車推廣騙補核查的第一部分工作,即現場核查,已經完成,目前仍處於對核查結果的會審階段,並稱對於核查和處理情況將按資訊公開有關規定及時公開。

業內人士原來普遍預期財政部能在4月或最遲5月公佈此項調查結果,但現在看來調查比預計的更耗時。

1月下旬,財政部、工信部等多個部委聯合發佈新能源汽車推廣應用核查工作的通知,在全國範圍對獲得中央財政補貼的新能源汽車及其生產企業和用戶展開核查。

在財政補貼的刺激下,2015年新能源汽車生產 34.05萬輛,銷售33.11萬輛,同比分別增長3.3倍和3.4倍。今年第一季度,增長有所放緩,但4月份又開始加速。

據中汽協統計,今年4月國內新能源汽車生產31266輛,銷售 31772輛,同比分別增長178.3%和190.6%。前4個月累計產銷量均超9萬輛,相比去年同期增長近130%。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?