掌握SpringBoot-2.3的容器探針:實戰篇

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

  • 內容:原創文章分類匯總,及配套源碼,涉及Java、Docker、K8S、DevOPS等
    經過多篇知識積累終於來到實戰章節,親愛的讀者們,請將裝備就位,一起動手體驗SpringBoot官方帶給我們的最新技術;

關於《SpringBoot-2.3容器化技術》系列

  • 《SpringBoot-2.3容器化技術》系列,旨在和大家一起學習實踐2.3版本帶來的最新容器化技術,讓咱們的Java應用更加適應容器化環境,在雲計算時代依舊緊跟主流,保持競爭力;
  • 全系列文章分為主題和輔助兩部分,主題部分如下:
  1. 《體驗SpringBoot(2.3)應用製作Docker鏡像(官方方案)》;
  2. 《詳解SpringBoot(2.3)應用製作Docker鏡像(官方方案)》;
  3. 《掌握SpringBoot-2.3的容器探針:基礎篇》;
  4. 《掌握SpringBoot-2.3的容器探針:深入篇》;
  5. 《掌握SpringBoot-2.3的容器探針:實戰篇》;
  • 輔助部分是一些參考資料和備忘總結,如下:
  1. 《SpringBoot-2.3鏡像方案為什麼要做多個layer》;
  2. 《設置非root賬號不用sudo直接執行docker命令》;
  3. 《開發階段,將SpringBoot應用快速部署到K8S》;

SpringBoot-2.3容器探針知識點小結

經過前面的知識積累,我們知道了SpringBoot-2.3新增的探針規範以及適用場景,這裏做個簡短的回顧:

  1. kubernetes要求業務容器提供一個名為livenessProbe的地址,kubernetes會定時訪問該地址,如果該地址的返回碼不在200到400之間,kubernetes認為該容器不健康,會殺死該容器重建新的容器,這個地址就是存活探針
  2. kubernetes要求業務容器提供一個名為readinessProbe的地址,kubernetes會定時訪問該地址,如果該地址的返回碼不在200到400之間,kubernetes認為該容器無法對外提供服務,不會把請求調度到該容器,這個地址就是就緒探針
  3. SpringBoot的2.3.0.RELEASE發布了兩個新的actuator地址,/actuator/health/liveness/actuator/health/readiness,前者用作存活探針,後者用作就緒探針,這兩個地址的返回值來自兩個新增的actuator:Liveness StateReadiness State
  4. SpringBoot應用根據特殊環境變量是否存在來判定自己是否運行在容器環境,如果是,/actuator/health/liveness/actuator/health/readiness這兩個地址就有返回碼,具體的值是和應用的狀態有對應關係的,例如應用啟動過程中,/actuator/health/readiness返回503,啟動成功后返回200
  5. 業務應用可以通過Spring系統事件機制來讀取Liveness StateReadiness State,也可以訂閱這兩個actuator的變更事件;
  6. 業務應用可以通過Spring系統事件機制來修改Liveness StateReadiness State,此時/actuator/health/liveness和/actuator/health/readiness的返回值都會發生變更,從而影響kubernetes對此容器的行為(參照第一點和第二點),例如livenessProbe返回碼變成503,導致kubernetes認為容器不健康,從而殺死容器;

小結完畢,接下來開始實打實的編碼和操作實戰,驗證上述理論;

實戰環境信息

本次實戰有兩個環境:開發和運行環境,其中開發環境信息如下:

  1. 操作系統:Ubuntu 20.04 LTS 桌面版
  2. CPU :2.30GHz × 4,內存:32G,硬盤:1T NVMe
  3. JDK:1.8.0_231
  4. MAVEN:3.6.3
  5. SpringBoot:2.3.0.RELEASE
  6. Docker:19.03.10
  7. 開發工具:IDEA 2020.1.1 (Ultimate Edition)

運行環境信息如下:

  1. 操作系統:CentOS Linux release 7.8.2003
  2. Kubernetes:1.15

事實證明,用Ubuntu桌面版作為開發環境是可行的,體驗十分順暢,IDEA、SubLime、SSH、Chrome、微信都能正常使用,下圖是我的Ubuntu開發環境:

實戰內容簡介

本次實戰包括以下內容:

  1. 開發SpringBoot應用,部署在kubernetes;
  2. 檢查應用狀態和kubernetes的pod狀態的關聯變化;
  3. 修改Readiness State,看kubernetes是否還會把請求調度到pod;
  4. 修改Liveness State,看kubernetes會不是殺死pod;

源碼下載

  1. 本次實戰用到了一個普通的SpringBoot工程,源碼可在GitHub下載到,地址和鏈接信息如下錶所示(https://github.com/zq2599/blog_demos):
名稱 鏈接 備註
項目主頁 https://github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址,https協議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該項目源碼的倉庫地址,ssh協議
  1. 這個git項目中有多個文件夾,本章的應用在probedemo文件夾下,如下圖紅框所示:

開發SpringBoot應用

  1. 請在IDEA上安裝lombok插件:
  1. 在IDEA上新建名為probedemo的SpringBoot工程,版本選擇2.3.0
  1. 該工程的pom.xml內容如下,注意要有spring-boot-starter-actuatorlombok依賴,另外插件spring-boot-maven-plugin也要增加layers節點:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bolingcavalry</groupId>
    <artifactId>probedemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>probedemo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.0.RELEASE</version>
                <!--該配置會在jar中增加layer描述文件,以及提取layer的工具-->
                <configuration>
                    <layers>
                        <enabled>true</enabled>
                    </layers>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 應用啟動類ProbedemoApplication是個最普通的啟動類:
package com.bolingcavalry.probedemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProbedemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProbedemoApplication.class, args);
    }
}
  1. 增加一個監聽類,可以監聽存活和就緒狀態的變化:
package com.bolingcavalry.probedemo.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.AvailabilityState;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * description: 監聽系統事件的類 <br>
 * date: 2020/6/4 下午12:57 <br>
 * author: willzhao <br>
 * email: zq2599@gmail.com <br>
 * version: 1.0 <br>
 */
@Component
@Slf4j
public class AvailabilityListener {

    /**
     * 監聽系統消息,
     * AvailabilityChangeEvent類型的消息都從會觸發此方法被回調
     * @param event
     */
    @EventListener
    public void onStateChange(AvailabilityChangeEvent<? extends AvailabilityState> event) {
        log.info(event.getState().getClass().getSimpleName() + " : " + event.getState());
    }
}
  1. 增加名為StateReader的Controller的Controller,用於獲取存活和就緒狀態:
package com.bolingcavalry.probedemo.controller;

import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;

@RestController
@RequestMapping("/statereader")
public class StateReader {

    @Resource
    ApplicationAvailability applicationAvailability;

    @RequestMapping(value="/get")
    public String state() {
        return "livenessState : " + applicationAvailability.getLivenessState()
               + "<br>readinessState : " + applicationAvailability.getReadinessState()
               + "<br>" + new Date();
    }
}
  1. 增加名為StateWritter的Controller,用於設置存活和就緒狀態:
package com.bolingcavalry.probedemo.controller;

import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Date;

/**
 * description: 修改狀態的controller <br>
 * date: 2020/6/4 下午1:21 <br>
 * author: willzhao <br>
 * email: zq2599@gmail.com <br>
 * version: 1.0 <br>
 */
@RestController
@RequestMapping("/staterwriter")
public class StateWritter {

    @Resource
    ApplicationEventPublisher applicationEventPublisher;

    /**
     * 將存活狀態改為BROKEN(會導致kubernetes殺死pod)
     * @return
     */
    @RequestMapping(value="/broken")
    public String broken(){
        AvailabilityChangeEvent.publish(applicationEventPublisher, StateWritter.this, LivenessState.BROKEN);
        return "success broken, " + new Date();
    }

    /**
     * 將存活狀態改為CORRECT
     * @return
     */
    @RequestMapping(value="/correct")
    public String correct(){
        AvailabilityChangeEvent.publish(applicationEventPublisher, StateWritter.this, LivenessState.CORRECT);
        return "success correct, " + new Date();
    }

    /**
     * 將就緒狀態改為REFUSING_TRAFFIC(導致kubernetes不再把外部請求轉發到此pod)
     * @return
     */
    @RequestMapping(value="/refuse")
    public String refuse(){
        AvailabilityChangeEvent.publish(applicationEventPublisher, StateWritter.this, ReadinessState.REFUSING_TRAFFIC);
        return "success refuse, " + new Date();
    }

    /**
     * 將就緒狀態改為ACCEPTING_TRAFFIC(導致kubernetes會把外部請求轉發到此pod)
     * @return
     */
    @RequestMapping(value="/accept")
    public String accept(){
        AvailabilityChangeEvent.publish(applicationEventPublisher, StateWritter.this, ReadinessState.ACCEPTING_TRAFFIC);
        return "success accept, " + new Date();
    }

}
  1. 增加名為Hello的controller,此接口能返回當前pod的IP地址,在後面測試時會用到:
package com.bolingcavalry.probedemo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;

/**
 * description: hello demo <br>
 * date: 2020/6/4 下午4:38 <br>
 * author: willzhao <br>
 * email: zq2599@gmail.com <br>
 * version: 1.0 <br>
 */
@RestController
public class Hello {

    /**
     * 返回的是當前服務器IP地址,在k8s環境就是pod地址
     * @return
     * @throws SocketException
     */
    @RequestMapping(value="/hello")
    public String hello() throws SocketException {
        List<Inet4Address> addresses = getLocalIp4AddressFromNetworkInterface();
        if(null==addresses || addresses.isEmpty()) {
            return  "empty ip address, " + new Date();
        }

        return addresses.get(0).toString() + ", " + new Date();
    }

    public static List<Inet4Address> getLocalIp4AddressFromNetworkInterface() throws SocketException {
        List<Inet4Address> addresses = new ArrayList<>(1);
        Enumeration e = NetworkInterface.getNetworkInterfaces();
        if (e == null) {
            return addresses;
        }
        while (e.hasMoreElements()) {
            NetworkInterface n = (NetworkInterface) e.nextElement();
            if (!isValidInterface(n)) {
                continue;
            }
            Enumeration ee = n.getInetAddresses();
            while (ee.hasMoreElements()) {
                InetAddress i = (InetAddress) ee.nextElement();
                if (isValidAddress(i)) {
                    addresses.add((Inet4Address) i);
                }
            }
        }
        return addresses;
    }

    /**
     * 過濾迴環網卡、點對點網卡、非活動網卡、虛擬網卡並要求網卡名字是eth或ens開頭
     * @param ni 網卡
     * @return 如果滿足要求則true,否則false
     */
    private static boolean isValidInterface(NetworkInterface ni) throws SocketException {
        return !ni.isLoopback() && !ni.isPointToPoint() && ni.isUp() && !ni.isVirtual()
                && (ni.getName().startsWith("eth") || ni.getName().startsWith("ens"));
    }

    /**
     * 判斷是否是IPv4,並且內網地址並過濾迴環地址.
     */
    private static boolean isValidAddress(InetAddress address) {
        return address instanceof Inet4Address && address.isSiteLocalAddress() && !address.isLoopbackAddress();
    }
}

以上就是該SpringBoot工程的所有代碼了,請確保可以編譯運行;

製作Docker鏡像

  1. 在pom.xml所在目錄創建文件Dockerfile,內容如下:
# 指定基礎鏡像,這是分階段構建的前期階段
FROM openjdk:8u212-jdk-stretch as builder
# 執行工作目錄
WORKDIR application
# 配置參數
ARG JAR_FILE=target/*.jar
# 將編譯構建得到的jar文件複製到鏡像空間中
COPY ${JAR_FILE} application.jar
# 通過工具spring-boot-jarmode-layertools從application.jar中提取拆分后的構建結果
RUN java -Djarmode=layertools -jar application.jar extract

# 正式構建鏡像
FROM openjdk:8u212-jdk-stretch
WORKDIR application
# 前一階段從jar中提取除了多個文件,這裏分別執行COPY命令複製到鏡像空間中,每次COPY都是一個layer
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
  1. 先編譯構建工程,執行以下命令:
mvn clean package -U -DskipTests 
  1. 編譯成功后,通過Dockerfile文件創建鏡像:
sudo docker build -t bolingcavalry/probedemo:0.0.1 .
  1. 鏡像創建成功:

SpringBoot的鏡像準備完畢,接下來要讓kubernetes環境用上這個鏡像;

將鏡像加載到kubernetes環境

此時的鏡像保存在開發環境的電腦上,可以有以下三種方式加載到kubernetes環境:

  1. push到私有倉庫,kubernetes上使用時也從私有倉庫獲取;
  2. push到hub.docker.com,kubernetes上使用時也從hub.docker.com獲取,目前我已經將此鏡像push到hub.docker.com,您在kubernetes直接使用即可,就像nginx、tomcat這些官方鏡像一樣下載;
  3. 在開發環境執行docker save bolingcavalry/probedemo:0.0.1 > probedemo.tar,可將此鏡像另存為本地文件,再scp到kubernetes服務器,再在kubernetes服務器執行docker load < /root/temp/202006/04/probedemo.tar就能加載到kubernetes服務器的本地docker緩存中;

以上三種方法的優缺點整理如下:

  1. 首推第一種,但是需要您搭建私有倉庫;
  2. 由於springboot-2.3官方對鏡像構建作了優化,第二種方法也就執行第一次的時候上傳和下載很耗時,之後修改java代碼重新構建時,不論上傳還是下載都很快(只上傳下載某個layer);
  3. 在開發階段,使用第三種方法最為便捷,但如果kubernetes環境有多台機器,就不合適了,因為鏡像是存在指定機器的本地緩存的;

我的kubernetes環境只有一台電腦,因此用的是方法三,參考命令如下(建議安裝sshpass,就不用每次輸入帳號密碼了):

# 將鏡像保存為tar文件
sudo docker save bolingcavalry/probedemo:0.0.1 > probedemo.tar

# scp到kubernetes服務器
sshpass -p 888888 scp ./probedemo.tar root@192.168.50.135:/root/temp/202006/04/ 
  
# 遠程執行ssh命令,加載docker鏡像
sshpass -p 888888 ssh root@192.168.50.135 "docker load < /root/temp/202006/04/probedemo.tar"

kubernetes部署deployment和service

  1. 在kubernetes創建名為probedemo.yaml的文件,內容如下,注意pod副本數是2,另外請關注livenessProbe和readinessProbe的參數配置:
apiVersion: v1
kind: Service
metadata:
  name: probedemo
spec:
  type: NodePort
  ports:
    - port: 8080
      nodePort: 30080
  selector:
    name: probedemo
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: probedemo
spec:
  replicas: 2
  template:
    metadata:
      labels:
        name: probedemo
    spec:
      containers:
        - name: probedemo
          image: bolingcavalry/probedemo:0.0.1
          tty: true
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 5
            failureThreshold: 10
            timeoutSeconds: 10
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 10
            periodSeconds: 5
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: "512Mi"
              cpu: "100m"
            limits:
              memory: "1Gi"
              cpu: "500m"
  1. 執行命令kubectl apply -f probedemo..yaml,即可創建deployment和service:
  1. 這裏要重點關注的是livenessProbeinitialDelaySecondsfailureThreshold參數,initialDelaySeconds等於5,表示pod創建5秒后檢查存活探針,如果10秒內應用沒有完成啟動,存活探針不返回200,就會重試10次(failureThreshold等於10),如果重試10次后存活探針依舊無法返回200,該pod就會被kubernetes殺死重建,要是每次啟動都耗時這麼長,pod就會不停的被殺死重建;
  2. 執行命令kubectl apply -f probedemo.yaml,創建deployment和service,如下圖,可見在第十秒的時候pod創建成功,但是此時還未就緒:
  1. 繼續查看狀態,創建一分鐘后兩個pod終於就緒:
  1. kubectl describe命令查看pod狀態,事件通知显示存活和就緒探針都有失敗情況,不過因為有重試,因此後來狀態會變為成功:

至此,從編碼到部署都完成了,接下來驗證SpringBoot-2.3.0.RELEASE的探針技術;

驗證SpringBoot-2.3.0.RELEASE的探針技術

  1. 監聽類AvailabilityListener的作用是監聽狀態變化,看看pod日誌,看AvailabilityListener的代碼是否有效,如下圖紅框,在應用啟動階段AvailabilityListener被成功回調,打印了存活和就緒狀態:
  1. kubernetes所在機器的IP地址是192.168.50.135,因此SpringBoot服務的訪問地址是http://192.168.50.135:30080/xxx

  2. 訪問地址http://192.168.50.135:30080/actuator/health/liveness,返回碼如下圖紅框,可見存活探針已開啟:

  1. 就緒探針也正常:
  1. 打開兩個瀏覽器,都訪問:http://192.168.50.135:30080/hello,多次Ctrl+F5強刷,如下圖,很快就能得到不同結果,證明響應來自不同的Pod:
  1. 訪問:http://192.168.50.135:30080/statereader/get,可以得到存活和就緒的狀態,可見StateReader的代碼已經生效,可以通過ApplicationAvailability接口取得狀態:
  1. 修改就緒狀態,訪問:http://192.168.50.135:30080/statewriter/refuse,如下圖紅框,可見收到請求的pod,其就緒狀態已經出現了異常,證明StateWritter.java中修改就緒狀態后,可以讓kubernetes感知到這個pod的異常
  1. 用瀏覽器反覆強刷hello接口,返回的Pod地址也只有一個,證明只有一個Pod在響應請求:
  1. 嘗試恢復服務,注意請求要在服務器後台發送,而且IP地址要用剛才被設置為refuse的pod地址
curl http://10.233.90.195:8080/statewriter/accept
  1. 如下圖,狀態已經恢復:
  1. 最後再來試試將存活狀態從CORRECT改成BROKEN,瀏覽器訪問:http://192.168.50.135:30080/statewriter/broken
  2. 如下圖紅框,重啟次數變成1,表示pod被殺死了一次,並且由於重啟導致當前還未就緒,證明在SpringBoot中修改了存活探針的狀態,是會觸發kubernetes殺死pod的
  1. 等待pod重啟、就緒探針正常后,一切恢復如初:
  1. 強刷瀏覽器,如下圖紅框,兩個Pod都能正常響應:

官方忠告

  • 至此,《掌握SpringBoot-2.3的容器探針》系列就全部完成了,從理論到實踐,咱們一起學習了SpringBoot官方帶給我們的容器化技術,最後以一段官方忠告來結尾,大家一起將此忠告牢記在心:
  • 我對以上內容的理解:選擇外部系統的服務作為探針的時候要謹慎(外部系統可能是數據庫,也可能是其他web服務),如果外部系統出現問題,會導致kubernetes殺死pod(存活探針問題),或者導致kubernetes不再調度請求到pod(就緒探針問題);(再請感謝大家容忍我的英語水平)

歡迎關注我的公眾號:程序員欣宸

https://github.com/zq2599/blog_demos

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

10萬以下產品力最強的兩款SUV!詳解風神AX5和遠景SUV!

未來肯定不能以價格取勝。那麼要考慮的就是要從質量與技術可靠為主了。而且有了AX7在前面擋着,未來AX5隻要在宣傳上做得出色,未來還是可以期待的。而遠景SUV換裝了全新的家族式造型設計,與吉利汽車其他車型有了很好的延續性。

目前SUV市場非常紅火,但對於國內的消費者來說;他們很多時候對SUV的購車預算大多數會考慮在10萬以下的車型,而這個區間最火熱的要屬最火熱的遠景SUV了。遠景SUV作為一款上市多年的車型,憑藉著良好的口碑與超高的性價比在三四線城市當中一直都受到追捧。其他品牌看它這麼紅火,肯定心有不甘;醞釀着推出競品與其競爭。在這個背景下,東風風神AX5應該是與它最接近的一款車型了。

總結:AX5作為市場的新丁,未來要遇到的挑戰很多;首先它目前的市場肯可度比較一般,而且相對性價比較高的遠景SUV來說,價格相對高一些;未來肯定不能以價格取勝。那麼要考慮的就是要從質量與技術可靠為主了;而且有了AX7在前面擋着,未來AX5隻要在宣傳上做得出色,未來還是可以期待的。

而遠景SUV換裝了全新的家族式造型設計,與吉利汽車其他車型有了很好的延續性。但它的車尾卻與前臉不太協調,但改款之後提價的策略也被不少消費者詬病;這是吉利急需要改變的不好影響。當然目前吉利汽車一系列熱門車型會把整個市場口碑營造上去,但遠景SUV卻不能以此為傲。雖不說前有前敵,但肯定後邊會有不少追兵。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

※教你寫出一流的銷售文案?

不到15萬,軸距超過3米還有誰!

而依維柯的中控 則採用了深灰色的內飾配色,整體看起來略微有些沉悶。並且空調的出風口以及手套箱的設計相對過於緊湊,讓人感覺整个中控台略微有些凌亂。四輻式的方向盤造型也難以提升駕駛員的駕駛興趣。馬兒跑得快還要不吃草作為全新一代輕客的代表之作,光有光鮮的外表、時尚的造型可不夠,選擇這類車型的消費者們更為看重的必然是它們的內在能力。

拼家底誰更厚?

全順:全順在輕客領域摸爬滾打也有將近五十年歷史了。它的原型車可以追溯到1953年福特生產的一款輕型商用車FK1000,它於1961被稱作Ford Taunus Transit。1965年,福特汽車英國公司推出第一代福特Transit全順,迅速取得了輕型貨車領域的主導地位,並在此後一直霸佔着歐洲輕型客貨車銷量冠軍,成為了輕型客貨車的代名詞。

依維柯:作為歐洲的輕型商用車之一,依維柯在歐洲市場佔據很大的市場份額。在1975年的時候依維柯公司正式成立,1978年第一代依維柯Daily誕生。並且一直發展到今日,依維柯一共經歷了六次換代。

誰才是真正的“顏值帝”

外觀設計各花入各眼,但就目前的眼光來看,新全順外觀設計相比起老款可謂是翻天覆地,時尚動感的外型設計,微微收緊的車頭、熏黑的大燈以及一條筆直斜向上一直延伸到車尾的線條設計,彷彿讓新全順一躍跳出了輕客這個領域。

而依維柯則是傳統的造型設計,菱形的前大燈以及方方正正的車頭設計讓它很難與時尚設計扯上關係,保持了輕客一貫的傳統印象,但沒能給人眼前一亮的感覺。

內飾誰更前衛

新全順的內飾設計採用了福特最新家族的設計語言,非對稱式的中控設計,外加黑色內飾加銀色鍍鉻配色,整體顯得更有檔次感,中控面板和門把手對於細節的處理也非常細緻,非常符合新全順的車型定位。

而依維柯的中控 則採用了深灰色的內飾配色,整體看起來略微有些沉悶。並且空調的出風口以及手套箱的設計相對過於緊湊,讓人感覺整个中控台略微有些凌亂。四輻式的方向盤造型也難以提升駕駛員的駕駛興趣。

馬兒跑得快還要不吃草

作為全新一代輕客的代表之作,光有光鮮的外表、時尚的造型可不夠,選擇這類車型的消費者們更為看重的必然是它們的內在能力。就這方面來看,全順的表現還是極為優秀的。從動力上看新全順所搭載的2.0T柴油渦輪增壓發動機雖在排量上比起依維柯的2.5T渦輪發動機稍顯劣勢,但由於全順採用了新的渦輪技術使得在排量吃虧的情況下,依然在能在功率上追上對手,並且在扭矩上還略微有優勢。

江鈴福特-全順

南京依維柯-power Daily

【聽宣判pK結果】:兩位老對手的定位雖然相近,但是結合兩輛車的表現,新全順更加出色,在各個項目的表現上都要更勝一籌。而依維柯則保留了純粹的商用車氣息,缺乏了一點新時代的便利性以及通用性。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

前瞻布局 穩健經營 東風日產第800萬輛整車下線

建立經銷商能力診斷體系並擴大經銷商經營範圍,進行二手車、汽車保險、汽車金融、汽車租貸等業務的擴充發展,在2016年1-10月達到12。21%的置換率,為東風日產歷史新高,進一步提升了經銷商收益力、服務能力和渠道效率,從而實現更加便捷高效的服務。

2016年12月26日,東風日產第800萬整車下線儀式在花都二工廠舉行。100餘名媒體記者以及車主代表共同參与和見證這一盛事。

東風日產副總經理周先鵬在下線儀式上表示:“東風日產的經營理念始終伴隨着中國經濟發展以及汽車產業的升級而轉型,堅持用前瞻性的眼光探索行業發展的態勢,對未來的發展方向提前布局。13年來,東風日產穩健經營、用心發展,從容迎來第八百萬輛整車下線。”

伴隨着第800萬輛整車下線,東風日產提前完成2016年度銷售目標,截至12月25日,全年銷量達到110萬輛,比去年同期增長13%,再次穩健跨越百萬。

客戶至上 體系能力全面升級

品牌順應時代,不斷成長,是企業穩健經營的前提。在購車者年齡越來越年輕的新汽車消費時代,東風日產2016年繼續深化YOUNG NISSAN 戰略,通過一系列“創新、走心、用心”的營銷活動,全面彰顯品牌年輕化心態,極大提升了品牌知名度與好感度。無論是攜手NBA、歐冠等頂級賽事,還是邀約頂級明星易建聯代言新生代TIIDA,都讓消費者近距離感受體育運動的激情與活力;產品營銷方面,新樓蘭、新奇駿、全新軒逸、藍鳥等產品圍繞文化、越野、音樂等不同主題,通過創新的活動形式,不僅讓消費者體驗到各具特色的產品魅力,更展現出不同產品和目標消費者的情懷與個性。據悉,2016年東風日產品牌好感度相較於2015年提升3.8%,首次超越豐田,躋身合資品牌前三。

客戶服務是企業穩健經營的基礎。2016年,東風日產圍繞“客戶年”的主題,開展“擁抱客戶,用心服務”主題實踐活動,強化全員客戶意識;通過成立地區支持辦公室,以更扁平化的運作架構貼近客戶;同時,在全國77家店開展了一系列的呼叫制培訓方式,使受訓店服務投訴率降幅達到38%。此外,易誠認證車首推兩年四萬公里保修升級政策,此舉為行業首創,深度保障消費者利益。

渠道健康是企業穩健經營的保障。2016年,東風日產落實p20大城市戰略,優化專營店的數量及效率,經銷商整體收益得到提升;建立經銷商能力診斷體系並擴大經銷商經營範圍,進行二手車、汽車保險、汽車金融、汽車租貸等業務的擴充發展,在2016年1-10月達到12.21%的置換率,為東風日產歷史新高,進一步提升了經銷商收益力、服務能力和渠道效率,從而實現更加便捷高效的服務。

不僅如此,東風日產更在提升企業體系力方面,未雨綢繆,坐言起行。2016年,秉承“穩健經營”的理念,東風日產腳踏實地、強調客戶服務、渠道和品牌健康成長。價值鏈前端建設也初見成果,先進工程技術中心、啟辰造型中心及東風日產大學,全面投入使用,從產品、研發設計、製造、人才培養等多個緯度鍛造企業內功,提升綜合實力,為東風日產未來新中期事業提供有力支撐。

智能驅動未來 I³計劃全面展開

隨着社會及技術層面信息化、智能化的發展,以及國家“智能製造”戰略藍圖的提出,汽車企業面臨着新的的機遇及挑戰。汽車行業已進入了智能時代,順應消費者需求智能化發展的趨勢,東風日產聚焦智能時代,進入以智能技術為驅動的YOUNG NISSAN 3.0時代,發布了“I³計劃”。以全價值鏈智能升級為核心,從智能出行(Intelligent Mobility Technology)、智造品質(Intelligent Manufacture Quality)、智享體驗(Intelligent Customer Experience)三大維度布局未來。

在智能出行方面,以“零碰撞、零排放、零距離”作為終極目標,開啟汽車技術的智能化升級,東風日產將成為率先導入中國的量產電動車的首個合資品牌;在智造品質方面,構建國內首創“整建制”先進工程技術中心,以数字化開發平台、智能化精工製造和信息化品質管理,實現製造技術的智能化升級;在智享體驗等方面,依託國內首個合資汽車公司自建電商平台車巴巴、率先將VR技術應用於新車體驗的沉浸式產品数字體驗平台、車載智能信息服務的應用,進行顧客全觸點的智能化升級。

2017年是東風日產再次跨越百萬之後的重要一年,800萬輛整車下線,對東風日產來說是一個歷史性的里程碑,更是一個新的起點。東風日產將以“I³計劃”為基礎,助推品牌年輕化戰略再升級,進入以智能技術為驅動的YOUNG NISSAN 3.0時代。同時,東風日產還將以“客戶年2.0”作為2017年發展的整體指導方向,從消費者需求出發,持續提升品牌力和客戶滿意度,保證主力車型的銷量及新車上市,同時整合網絡安全,強化經銷商基礎,為客戶帶來更加精彩的智能化汽車生活。

周先鵬表示,“前瞻性的戰略思考,以及穩健高效的執行力,為東風日產更快速響應市場,決勝未來奠定了堅實基礎。在800萬份信賴之上,東風日產砥礪前行,以智能化的未來驅動力,助力東風日產引領行業趨勢,穩健前行。”本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

自動擋!大空間!ESP!這款不到10萬的SUV值得考慮

所以在10萬元以內的購車預算,選擇一台國產車很划算,不僅配置高、而且車身尺寸更大,看起來更大氣。作為一款擁有超高性價比的家用SUV車型,森雅R7自動擋順應了那些注重家庭,銳意進取,年輕時尚且懂得享受高品質生活的人群需求,剛上市首月就有1萬多的訂單也足以說明它具備成為熱門車型的潛力,而這一次上市的自動擋車型,更能進一步豐富了車型的產品線,而對小型SUV市場來說,又是一次強有力的衝擊。

隨着我們生活質量的不停提高,人們對於購車的需求越來越強烈,而今已經有越來越多的消費者將購車的計劃擺在了首位。來總結一下我國消費者的購車需求。目前汽車還算得上是一件奢飾品,很多人將買車當成一件漲面子的事情,所以人們買車都喜歡選擇一些尺寸大、顏值高、配置高的車子。

SUV之所以流行,除了它本身擁有高底盤高通過性的優勢以外,假如SUV和轎車的尺寸相差不大,兩者中SUV看起來更高檔次!消費者在選車時,除了看臉,內在也很重要,一個好的內飾、一堆逆天的配置更能吸引到消費者的關注。說實話,為什麼這麼多人在10萬以內都傾向於選擇國產車?因為合資車配置車型都太落後了呀!所以在10萬元以內的購車預算,選擇一台國產車很划算,不僅配置高、而且車身尺寸更大,看起來更大氣!

作為一款擁有超高性價比的家用SUV車型,森雅R7自動擋順應了那些注重家庭,銳意進取,年輕時尚且懂得享受高品質生活的人群需求,剛上市首月就有1萬多的訂單也足以說明它具備成為熱門車型的潛力,而這一次上市的自動擋車型,更能進一步豐富了車型的產品線,而對小型SUV市場來說,又是一次強有力的衝擊。而森雅R7自動擋車型目前有舒適型、豪華型、智能型、尊貴型,售價7.89~9.29萬之間。為了滿足用戶的更高需求,還將推出“森雅R7 AT版 旗艦型”,售價 9.99萬元。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

購車新生代都在關注啥車型?別忘了奔騰的高顏值SUV

只有帥氣外觀還不夠,撩妹還要看內功。打開車門,新款奔騰X80的內飾設計作出了更多優化。8英寸的懸浮式多媒體觸摸屏、簡化實體按鍵的中控面板,清晰易讀帶彩色显示屏的儀錶盤,都是新款奔騰X80提升乘客滿意度的一大法寶。

都說現在是90后小鮮肉的時代,踏入社會後工作穩定時,就應買台顏值出眾的SUV來好好的撩妹。想着每天開着高顏值的SUV上班,在路旁的年輕MM的注視下,展示你最帥的一面……別YY了,趕緊找來一款高顏值配置實用的SUV,這車帥得分分鐘讓你按耐不住馬上買買買!

顏值出眾的SUV,粉絲們也許首先會想到某些百萬豪車。但90后購入的第一款車,太貴的車咱先不考慮。其實在我們身邊的自主品牌,也有不少讓人眼前一亮的佳作。先賣個關子,曾有一款SUV,憑着車身C柱上的獨特“X”字形優美線條,以及紮實底盤,獲得一致好評。

現在,TA帶着更加年輕運動化的外觀,以及更多實用配置升級,回!來!了!

一汽新款奔騰X80

實力提升的一汽新款奔騰X80,將為兵家必爭之地的10~15萬自主緊湊型SUV車市,再添上一把火。值得一提的是,新款奔騰X80的外觀年輕運動化改造,吸睛能力簡直是棒棒噠,顏值控們可不要錯過了。

為照顧外貌協會,先從新款奔騰X80的重頭戲——外觀改造說起。重新設計的大燈線條,配合更具稜角的中網,讓車頭顯得更加精神及具有運動感。車身的最大特色,車側和車尾交界處標誌性的“X”字形優美設計得以保留。

來到車尾,熏黑尾燈組及雙邊單出橢圓排氣管,呼應前臉的運動化設計,視覺體驗更加年輕帥氣。此時也化身迷妹,實在是太帥啦!

新款奔騰X80在尺寸方面,與同級對手對比不落下風,乘坐舒適有保證。擁有一款外觀運動高顏值SUV,新款奔騰X80讓你撩妹實力大增!

只有帥氣外觀還不夠,撩妹還要看內功。打開車門,新款奔騰X80的內飾設計作出了更多優化。8英寸的懸浮式多媒體觸摸屏、簡化實體按鍵的中控面板,清晰易讀帶彩色显示屏的儀錶盤,都是新款奔騰X80提升乘客滿意度的一大法寶。

最值得一提是,吸睛能力MAX的8英寸懸浮式多媒體觸摸屏,還支持小鮮肉們喜聞樂見的Apple Carplay及百度Carlife,另外全景影像,一鍵啟動,胎壓監測,ESp車身穩定系統,前後泊車雷達及六安全氣囊等實用配置統統沒有缺席。新款奔騰X80真的有料!

手機與汽車無縫連接真方便,願意給一百個贊。YY一下,開着新款奔騰X80,用手機連接Carplay,放着喜歡的音樂,與妹子愉快的自駕游去,畫面太美了!

高顏值,配置實用,那就更少不了大空間。新款奔騰X80的空間表現,即使185cm的長腿歐巴坐進車內,也不會覺得局促。前排座椅針對大腿和肩部的包裹進行了優化,後排的座墊足夠長,乘坐真舒適。不像部分車型,為了大空間把後排改成了“小板凳”。這樣的後排,估計妹子上車,兩分鐘就投訴了!而在奔騰新款X80的大空間幫助下,完美駕馭舒適座椅,這是適合長腿歐巴乘坐的後排啊。

別忘了新款奔騰X80的動力部分,保持1.8T/2.0L的動力,與發動機匹配的是愛信的6擋手自一體變速箱,2.0L部分車款還搭配了6擋手動變速箱。對於一款緊湊型SUV,2.0L的動力輸出能比競品的小排量渦輪發動機,表現更加從容;1.8T則能滿足性能控對強勁動力的需求。兩款動力組合,相信能滿足小鮮肉們對動力的任性需求。

總結:

一汽新款奔騰X80的硬實力提升明顯,在高顏值、大空間及配置升級的針對性的升級后,能更好的滿足90后小鮮肉消費者的購車需求。2016年也拼搏一年了,買輛新款奔騰X80獎勵自己,好好撩妹再適合不過。年底打算要買車的粉絲,要留意了,2016年12月31日前購車,更享5000元購置稅補貼。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

比老司機更會玩の日常

com/x/page/u0353zzxlol。html)據傳,這就是新TIIDA車主的日常。果然“城會玩”,待俺去充個“會員”,咱一起飛。溫馨提示:點擊閱讀原文,預約試駕,馬上成為“會員”。(閱讀原文鏈接如下:http://www。dongfeng-nissan。com。cn/Nissan/car/tiida)。

話說,一群老司機聚在一起能幹什麼?

吃吃?

喝喝?

騷年,敢不敢幹一票“大”的?

↓↓↓↓↓

(視頻鏈接如下:https://v.qq.com/x/page/u0353zzxlol.html)

據傳,

這就是新TIIDA車主的日常。

果然“城會玩”,

待俺去充個“會員”,

咱一起飛。

溫馨提示:點擊閱讀原文,預約試駕,馬上成為“會員”。

(閱讀原文鏈接如下:http://www.dongfeng-nissan.com.cn/Nissan/car/tiida)本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Spring Security 實戰乾貨:如何實現不同的接口不同的安全策略

1. 前言

歡迎閱讀 Spring Security 實戰乾貨 系列文章 。最近有開發小夥伴提了一個有趣的問題。他正在做一個項目,涉及兩種風格,一種是給小程序出接口,安全上使用無狀態的JWT Token;另一種是管理後台使用的是Freemarker,也就是前後端不分離的Session機制。用Spring Security該怎麼辦?

2. 解決方案

我們可以通過多次繼承WebSecurityConfigurerAdapter構建多個HttpSecurityHttpSecurity 對象會告訴我們如何驗證用戶的身份,如何進行訪問控制,採取的何種策略等等。

如果你看過之前的教程,我們是這麼配置的:

/**
 * 單策略配置
 *
 * @author felord.cn
 * @see org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration
 * @since 14 :58 2019/10/15
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
@EnableWebSecurity
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
    
    /**
     * The type Default configurer adapter.
     */
    @Configuration
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            super.configure(auth);
        }

        @Override
        public void configure(WebSecurity web) {
            super.configure(web);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 配置 httpSecurity

        }
    }
}

上面的配置了一個HttpSecurity,我們如法炮製再增加一個WebSecurityConfigurerAdapter的子類來配置另一個HttpSecurity。伴隨而來的還有不少的問題要解決。

2.1 如何路由不同的安全配置

我們配置了兩個HttpSecurity之後,程序如何讓小程序接口和後台接口走對應的HttpSecurity

HttpSecurity.antMatcher(String antPattern)可以提供過濾機制。比如我們配置:

     @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 配置 httpSecurity
            http.antMatcher("/admin/v1");

        }

那麼該HttpSecurity將只提供給以/admin/v1開頭的所有URL。這要求我們針對不同的客戶端指定統一的URL前綴。

舉一反三隻要HttpSecurity提供的功能都可以進行個性化定製。比如登錄方式,角色體系等。

2.2 如何指定默認的HttpSecurity

我們可以通過在WebSecurityConfigurerAdapter實現上使用@Order註解來指定優先級,數值越大優先級越低,沒有@Order註解將優先級最低。

2.3 如何配置不同的UserDetailsService

很多情況下我們希望普通用戶和管理用戶完全隔離,我們就需要多個UserDetailsService,你可以在下面的方法中對AuthenticationManagerBuilder進行具體的設置來配置UserDetailsService,同時也可以配置不同的密碼策略。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 自行實現
            return  null ;
        }
    });
    // 也可以設計特定的密碼策略
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
    auth.authenticationProvider(daoAuthenticationProvider);
}

2.4 最終的配置模板

上面的幾個問題解決之後,我們基本上掌握了在一個應用中執行多種安全策略。配置模板如下:

/**
 * 多個策略配置
 *
 * @author felord.cn
 * @see org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration
 * @since 14 :58 2019/10/15
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
@EnableWebSecurity
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {

    /**
     * 後台接口安全策略. 默認配置
     */
    @Configuration
    @Order(1)
    static class AdminConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
            //用戶詳情服務個性化
            daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
                @Override
                public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                    // 自行實現
                    return null;
                }
            });
            // 也可以設計特定的密碼策略
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
            auth.authenticationProvider(daoAuthenticationProvider);
        }

        @Override
        public void configure(WebSecurity web) {
            super.configure(web);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 根據需求自行定製
            http.antMatcher("/admin/v1")
                    .sessionManagement(Customizer.withDefaults())
                    .formLogin(Customizer.withDefaults());


        }
    }

    /**
     * app接口安全策略. 沒有{@link Order}註解優先級比上面低
     */
    @Configuration
    static class AppConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
            //用戶詳情服務個性化
            daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
                @Override
                public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                    // 自行實現
                    return null;
                }
            });
            // 也可以設計特定的密碼策略
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
            auth.authenticationProvider(daoAuthenticationProvider);
        }

        @Override
        public void configure(WebSecurity web) {
            super.configure(web);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 根據需求自行定製
            http.antMatcher("/app/v1")
                    .sessionManagement(Customizer.withDefaults())
                    .formLogin(Customizer.withDefaults());


        }
    }
}

3. 總結

今天我們解決了如何針對不同類型接口採取不同的安全策略的方法,希望對你有用,如果你有什麼問題可以留言。多多關注:碼農小胖哥,更多乾貨奉上。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

※教你寫出一流的銷售文案?

【Java Spring Cloud 實戰之路】添加一個SpringBootAdmin監控

0. 前言

在之前的幾章中,我們先搭建了一個項目骨架,又搭建了一個使用nacos的gateway網關項目,網關項目中並沒有配置太多的東西。現在我們就接着搭建在Spring Cloud 微服務中另一個重要的項目 – Spring boot admin.

1. Spring Boot Admin 介紹

Spring Boot Admin 用來監控基於Spring Boot的應用,在Spring Boot Actuator的基礎上提供了簡潔的可視化Web UI。Spring Boot Admin 提供了以下功能:

  • 显示應用的健康狀態
  • 显示應用的細節內容: JVM和內存信息,micrometer信息, 數據源信息,緩存信息等
  • 显示 編譯版本
  • 查看和下載日誌
  • 查看jvm參數和環境變量值
  • 查看Spring Boot項目配置
  • 显示 thread dump
  • 显示 http-traces

……

等一系列內容。

2. 創建一個 Spring Boot Admin項目

那麼,我們就來創建一個Spring Boot Admin 項目吧。

2.1 創建 Spring Boot Admin 服務端

在manager 目錄下,創建一個 monitor目錄,並在monitor目錄下創建一個pom.xml 文件,添加以下內容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>monitor</artifactId>
    <version>${revision}</version>
    <packaging>jar</packaging>
    <parent>
        <artifactId>manager</artifactId>
        <groupId>club.attachie</groupId>
        <version>${revision}</version>
    </parent>

</project>

在 manager/pom.xml 註冊我們新建的項目模塊:

<modules>
    <module>gateway</module>
    <module>monitor</module>
</modules>

在 monitor 創建如下目錄:

.
├── pom.xml
└── src
    └── main
        ├── java
        └── resources

在根目錄的pom.xml 添加 Spring Boot Admin 依賴:

先添加spring-boot-admin版本號變量:

<spring-boot-admin.version>2.2.3</spring-boot-admin.version>

並在dependencyManagement > dependencies 下添加:

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <version>${spring-boot-admin.version}</version>
</dependency>

在monitor/pom.xml文件中添加:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-server</artifactId>
    </dependency>
</dependencies>

運行

mvn clean install

檢查並刷mvn引用緩存。

創建MonitorApplication類:

package club.attachie.nature.monitor;

import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableAdminServer
public class MonitorApplication {
    public static void main(String[] args) {
        SpringApplication.run(MonitorApplication.class, args);
    }
}

啟動后能看到如下界面:

3 與網關服務進行互通

在上一篇中,我們添加了Spring Cloud Gateway項目,到目前為止兩個項目之間完全割裂沒有關聯。在這一節,我們在兩者之間建立關聯。也就是說,將gateway 項目引入Spring Admin Boot監聽。

在 manager/gateway 的pom.xml 文件中加入如下引用:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然後修改 gateway項目的啟動端口,在resources/bootstrap.yml 添加:

server:
  port: 8070

在 monitor中加入nacos引用:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>      
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

修改MonitorApplication 為:

package club.attachie.nature.monitor;

import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.context.config.annotation.RefreshScope;

@SpringBootApplication
@EnableAdminServer
@RefreshScope
public class MonitorApplication {
    public static void main(String[] args) {
        SpringApplication.run(MonitorApplication.class, args);
    }
}

創建monitor項目的bootsrap.yml:

spring:
  application:
    name: monitor
  
  cloud:
  	nacos:
      discovery:
        server-addr: 127.0.0.1:8848

關於這裏的配置 在上一篇 中有個錯誤,應該是 discovery > server-addr,不是 config > server-addr。兩者有區別,discovery表示設置nacos為服務發現中心,config表示nacos為配置中心。

啟動 gateway 項目和 monitor項目查看效果, 訪問 8080端口:

可以看到兩個應用可以被發現,如果沒有設置monitor項目把nacos當做服務發現中心,將無法獲取到具體在線的應用。點擊 gateway 進去后可以查看到:

4. 總結

我們搭建了一個Spring Boot Admin 項目作為一個監控系統,後續會在這裏添加更多的內容。

更多內容煩請關注我的博客《高先生小屋》

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

小菜成長之路,警惕淪為 API 調用俠

小菜(化名)在某互聯網公司擔任運維工程師,負責公司後台業務的運維保障工作。由於自己編程經驗不多,平時有不少工作需要開發協助。

聽說 Python 很火,能快速開發一些運維腳本,小菜也加入 Python 大軍學起來。 Python 語言確實簡單,小菜很快就上手了,覺得自己應對運維開發工作已經綽綽有餘,便不再深入研究。

背景

這天老闆給小菜派了一個數據採集任務,要實時統計服務器 TCP 連接數。需求背景是這樣的:開發同事需要知道服務的連接數以及不同狀態連接的比例,以便判斷服務狀態。

因此,小菜需要開發一個腳本,定期採集並報告 TCP 連接數,提交數據格式定為 json :

{
  "LISTEN": 4,
  "ESTABLISHED": 100,
  "TIME_WAIT": 10
}

作為運維工程師,小菜當然知道怎麼查看系統 TCP 連接。
Linux 系統中有兩個命令可以辦到, netstat 和 ss :

$ netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:8388          0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp        0      0 192.168.56.3:22         192.168.56.1:54983      ESTABLISHED
tcp6       0      0 :::22                   :::*                    LISTEN
$ ss -nat
State                    Recv-Q                    Send-Q                                         Local Address:Port                                         Peer Address:Port
LISTEN                   0                         128                                                127.0.0.1:8388                                              0.0.0.0:*
LISTEN                   0                         128                                            127.0.0.53%lo:53                                                0.0.0.0:*
LISTEN                   0                         128                                                  0.0.0.0:22                                                0.0.0.0:*
ESTAB                    0                         0                                               192.168.56.3:22                                           192.168.56.1:54983
LISTEN                   0                         128                                                     [::]:22                                                   [::]:*

小菜還知道 ss 命令比 netstat 命令要快,但至於為什麼,小菜就不知道了。

小菜很快找到老闆,提出了自己的解決方案:寫一個 Python 程序,調用 ss 命令採集 TCP 連接信息,然後再逐條統計。

老闆告訴小菜,線上服務器很多都是最小化安裝,並不能保證每台機器上都有 ss 或者 netstat 命令。

老闆還告訴小菜,程序開發要學會 站在巨人的肩膀上 。動手寫代碼前,先調研一番,看是否有現成的解決方案。 切忌重複造輪子 ,浪費時間不說,可能代碼質量還差,效果也不好。

最後老闆給小菜指了條明路,讓他回去再看看 psutil 。 psutil 是一個 Python 第三方包,用於採集系統性能數據,包括: CPU 、內存、磁盤、網卡以及進程等等。臨走前,老闆還叮囑小菜,完成工作后花點時間研究下這個庫。

psutil 方案

小菜搜索 psutil 發現,原來有這麼順手的第三方庫,喜出望外!他立馬裝好 psutil ,準備開干:

$ pip install psutil

導入 psutil 后,一個函數調用就可以拿到系統所有連接,連接信息非常豐富:

>>> import psutil
>>> for conn in psutil.net_connections('tcp'):
...     print(conn)
...
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='192.168.56.3', port=22), raddr=addr(ip='192.168.56.1', port=54983), status='ESTABLISHED', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='LISTEN', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='0.0.0.0', port=22), raddr=(), status='LISTEN', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='LISTEN', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET6: 10>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='::', port=22), raddr=(), status='LISTEN', pid=None)

小菜很滿意,感覺不用花多少時間就可搞定數據採集需求了,準時下班有望!噼里啪啦,很快小菜就寫下這段代碼:

import psutil
from collections import defaultdict

# 遍歷每個連接,按連接狀態累加
stats = defaultdict(int)
for conn in psutil.net_connections('tcp'):
    stats[conn.status] += 1

# 遍歷每種狀態,輸出連接數
for status, count in stats.items():
    print(status, count)

小菜接着在服務器上測試這段代碼,功能完全正常:

ESTABLISHED 1
LISTEN 4

小菜將數據採集腳本提交,並按既定節奏逐步發布到生產服務器上。開發同事很快就看到小菜採集的數據,都誇小菜能力不錯,需求完成得很及時。小菜也很高興,感覺 Python 沒白學。如果用其他語言開發,說不定現在還在加班加點呢!Life is short, use Python! 果然沒錯!

小菜愈發自信,早就把老闆的話拋到腦後了。 psutil 這個庫這麼好上手,有啥好深入研究的?

內存悲劇

突然有一天,其他同事緊急告訴小菜,他開發的採集腳本佔用很多內存, CPU 也跑到了 100% ,已經開始影響線上服務了。小菜還沉浸在成功的喜悅中,收到這個反饋如同晴天霹靂,有點举手無措。

業務同事告訴小菜,受影響的機器系統連接數非常大,質疑小菜是不是腳本存在性能問題。小菜覺得很背,腳本只是調用 psutil 並統計數據,怎麼就攤上性能故障?腳本影響線上服務,小菜壓力很大,但不知道如何是好,只能跑去找老闆尋求幫助。

老闆要小菜第一時間停止數據採集,降低影響。復盤故障時,老闆很敏銳地問小菜,是不是用容器保存所有連接了?小菜自己並沒有,但是 psutil 這麼做了:

>>> psutil.net_connections()
[sconn(fd=-1, family=<AddressFamily.AF_INET6: 10>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='::', port=22), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='0.0.0.0', port=22), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='10.0.2.15', port=68), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='192.168.56.3', port=22), raddr=addr(ip='192.168.56.1', port=54983), status='ESTABLISHED', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='LISTEN', pid=None)]

psutil 將採集到的所有 TCP 連接放在一個列表裡返回。如果服務器上有十萬個 TCP 連接,那麼列表裡將有十萬個連接對象。難怪採集腳本吃了那麼多內存!

老闆告訴小菜,可以用生成器加以解決。與列表不同,生成器逐個返回數據,因此不會佔用太多內存。Python2 中 range 和 xrange 函數的區別也是一樣的道理。

小菜從 pstuil  fork 了一個分支,並將 net_connections 函數改造成 生成器 :

def net_connections():
    while True:
        if done:
            break

        # 解析一個TCP連接
        conn = xxx

        yield conn

代碼上線后,採集腳本內存佔用量果然下降了! 生成器 將統計算法的空間複雜度由原來的 O(n) 優化為 O(1) 。經過這次教訓,小菜不敢再盲目自信了,他決定抽時間好好看看 psutil 的源碼。

源碼體會

深入學習源碼后,小菜發現原來 psutil 採集 TCP 連接數的秘笈是:從 /proc/net/tcp 以及 /proc/net/tcp6 讀取連接信息。

由此,他還進一步了解到 procfs ,這是一個偽文件系統,將內核空間信息以文件方式暴露到用戶空間。 /proc/net/tcp 文件則是提供內核 TCP 連接信息:

$ cat /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 0100007F:20C4 00000000:0000 0A 00000000:00000000 00:00000000 00000000 65534        0 18183 1 0000000000000000 100 0 0 10 0
   1: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   101        0 16624 1 0000000000000000 100 0 0 10 0
   2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 18967 1 0000000000000000 100 0 0 10 0
   3: 0338A8C0:0016 0138A8C0:D6C7 01 00000000:00000000 02:00023B11 00000000     0        0 22284 4 0000000000000000 20 13 23 10 20

小菜還注意到,連接信息看起來像個自定義類對象,但其實是一個 nametuple :

# psutil.net_connections()
sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr',
                             'status', 'pid'])

小菜一開始並不知道作者為啥要這麼做。後來,小菜開始研究 Python 源碼,學習了 Python 類機制后他恍然大悟。

Python 自定義類的每個實例對象均需要一個 dict 來保存對象屬性,這也就是對象的 屬性空間 。

如果用自定義類來實現,每個連接都需要創建一個字典,而字典又是 散列表 實現的。如果系統存在成千上萬的連接,開銷可想而知。

小菜將學到的知識總結起來:對於 數量大 而 屬性固定 的實體,沒有必要用自定義類來實現,用 nametuple 更合適,開銷更小。由此,小菜不經由衷佩服 psutil 的作者。

CPU悲劇

後來小菜又收到業務反饋,採集腳本在高併發的服務器上, CPU 使用率很高,需要再優化一下。

小菜回憶 psutil 源碼,很快就找到了性能瓶頸處: psutil 將連接信息所有字段都解析了,而採集腳本只需要其中的 狀態 字段而已。

跟老闆商量后,小菜決定自行讀取 procfs 來實現採集腳本,只解析狀態字段,避免不必要的計算開銷。

procfs 方案

直接讀取 /proc/net/tcp ,可以得到完整的 TCP 連接信息:

>>> with open('/proc/net/tcp') as f:
...     for line in f:
...         print(line.rstrip())
...
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 0100007F:20C4 00000000:0000 0A 00000000:00000000 00:00000000 00000000 65534        0 18183 1 0000000000000000 100 0 0 10 0
   1: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   101        0 16624 1 0000000000000000 100 0 0 10 0
   2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 18967 1 0000000000000000 100 0 0 10 0
   3: 0338A8C0:0016 0138A8C0:D6C7 01 00000000:00000000 02:0007169E 00000000     0        0 22284 3 0000000000000000 20 20 33 10 20

其中, IP 、端口、狀態等字段都是以十六進制編碼的。例如, st 列表示狀態,狀態碼 0A 表示 LISTEN 。很快小菜就寫下這段代碼:

from collections import defaultdict

stat_names = {
    '0A': 'LISTEN',
    '01': 'ESTABLISHED',
    # ...
}

# 遍歷每個連接,按連接狀態累加
stats = defaultdict(int)

with open('/proc/net/tcp') as f:
    # 跳過表頭行
    f.readline()

    for line in f:
        st = line.strip().split()[3]
        stats[st] += 1

for st, count in stats.items():
    print(stat_names[st], count)

現在,小菜寫代碼比之前講究多了。在統計連接數時,他並不急於將狀態碼解析成名字,而是按原樣統計。等統計完成,他再一次性轉換,這樣狀態碼轉換開銷便降到最低: O(1)  而不是 O(n) 。

這次改進符合業務同事預期,但小菜決定好好做一遍性能測試,不打無準備之仗。他找業務同事要了一個連接數最大的 /proc/net/tcp 樣本,拉到本地測試。測試結果還算符合預期,採集腳本能夠扛住十萬連接採集壓力。

性能測試中,小菜發現了一個比較奇怪的問題。同樣的連接規模,把 /proc/net/tcp 拉到本地跑比直接在服務器上跑要快,而本地電腦性能肯定比不上服務器。

他百思不得其解,又去找老闆幫忙。老闆很快指出到其中的區別,將 /proc/net/tcp 拉到本地就成為普通 磁盤文件 ,而 procfs 是內核映射出來的 偽文件 ,並不是磁盤文件。

他讓小菜研究一下 Python 文件 IO 以及內核 IO 子系統在處理這兩種文件時有什麼區別,還讓小菜特別留意 IO 緩衝區大小。

IO緩衝

小菜打開一個普通的磁盤文件,發現 Python 選的默認緩衝區大小是 4K (讀緩存對象頭 152 字節):

>>> f = open('test.py')
>>> f.buffer.__sizeof__()
4248

但是如果打開的是 procfs 文件, Python 選的緩衝區卻只有 1K ,相差了 4 倍呢!

>>> f = open('/proc/net/tcp')
>>> f.buffer.__sizeof__()
1176

因此,理論上 Python 默認讀取 procfs 發生的上下文切換次數是普通磁盤文件的 4 倍,怪不得會慢。

雖然小菜還不知道這種現象背後的原因,但是他已經知道怎麼進行優化了。隨即他決定將緩衝區設置為 1M 以上,盡量避免 IO 上下文切換,以空間換時間:

with open('/proc/net/tcp', buffering=1*1024*1024) as f:
    # ...

經過這次優化,採集腳本在大部分服務器上運行良好,基本可以高枕無憂了。而小菜也意識到 編程語言 以及 操作系統 等底層基礎知識的重要性,他開始制定學習計劃補全計算機基礎知識。

netlink 方案

後來負載均衡團隊找到小菜,他們也想統計服務器上的連接信息。由於負載均衡服務器作為入口轉發流量,連接數規模特別大,達到幾十萬,將近百萬的規模。小菜決定好好進行性能測試,再視情況上線。

測試結果並不樂觀,採集腳本要跑幾十秒鐘才完成, CPU 跑到 100% 。小菜再次調高 IO 緩衝區,但效果不明顯。小菜又測試了 ss 命令,發現 ss 命令要快很多。由於之前嘗到了閱讀源碼的甜頭,小菜很想到 ss 源碼中尋找秘密。

由於項目時間較緊,老闆提醒小菜先用 strace 命令追蹤 ss 命令的系統調用,便可快速獲悉 ss 的實現方式。老闆演示了 strace 命令的用法,很快就找到了 ss 的秘密 —— Netlink :

$ strace ss -nat
...
socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_SOCK_DIAG) = 3
...

Netlink 套接字是 Linux 提供通訊機制,可用於內核與進程間、進程與進程間通訊。 Netlink 下的 sock_diag 子系統,提供了一種從內核獲取套接字信息的新方式。

procfs 不同,sock_diag 採用網絡通訊的方式,內核作為服務端接收客戶端進程查詢請求,並以二進制數據包響應查詢結果,效率更高。

這就是 ss 比 netstat 更快的原因, ss 採用 Netlink 機制,而 netstat 採用 procfs 機制。

很不幸 Python 並沒有提供 Netlink API ,一般人可能又要干著急了。好在小菜先前有意識地研究了部分 Python 源碼,對 Python 的運行機制有所了解。

他知道可以用 C 寫一個 Python 擴展模塊,在 C 語言中調用原生系統調用。

編寫 Python C 擴展模塊可不簡單,對編程功底要求很高,必須全面掌握 Python 運行機制,特別是對象內存管理。

一朝不慎可能導致程序異常退出、內存泄露等棘手問題。好在小菜已經不是當年的小菜了,他經受住了考驗。

小菜的擴展模塊上線后,效果非常好,頂住了百萬級連接的採集壓力。

一個看似簡單得不能再簡單的數據採集需求,背後涉及的知識可真不少,沒有一定的水平還真搞不定。好在小菜成長很快,他最終還是徹底地解決了性能問題,找回了久違的信心。

內核模塊方案

雖然性能問題已經徹底解決,小菜還是沒有將其淡忘。

他時常想:如果可以將統計邏輯放在內核空間做,就不用在內核和進程之間傳遞大量連接信息了,效率應該是最高的!受限於當時的知識水平,小菜還沒有能力實現這個設想。

後來小菜在研究 Linux 內核時,發現可以用內核模塊來擴展內核的功能,結合 procfs 的工作原理,他找到了技術方案!他順着 /proc/net/tcp 在內核中的實現源碼,依樣畫葫蘆寫了這個內核模塊:

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <net/tcp.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Xiaocai");
MODULE_DESCRIPTION("TCP state statistics");
MODULE_VERSION("1.0");

// 狀態名列表
static char *state_names[] = {
    NULL,
    "ESTABLISHED",
    "SYN_SENT",
    "SYN_RECV",
    "FIN_WAIT1",
    "FIN_WAIT2",
    "TIME_WAIT",
    "CLOSE",
    "CLOSE_WAIT",
    "LAST_ACK",
    "LISTEN",
    "CLOSING",
    NULL
};


static void stat_sock_list(struct hlist_nulls_head *head, spinlock_t *lock,
    unsigned int state_counters[])
{
    // 套接字節點指針(用於遍歷)
    struct sock *sk;
    struct hlist_nulls_node *node;

    // 鏈表為空直接返回
    if (hlist_nulls_empty(head)) {
        return;
    }

    // 自旋鎖鎖定
    spin_lock_bh(lock);

    // 遍歷套接字鏈表
    sk = sk_nulls_head(head);
    sk_nulls_for_each_from(sk, node) {
        if (sk->sk_state < TCP_MAX_STATES) {
            // 自增狀態計數器
            state_counters[sk->sk_state]++;
        }
    }

    // 自旋鎖解鎖
    spin_unlock_bh(lock);
}


static int tcpstat_seq_show(struct seq_file *seq, void *v)
{
    // 狀態計數器
    unsigned int state_counters[TCP_MAX_STATES] = { 0 };
    unsigned int state;

    // TCP套接字哈希槽序號
    unsigned int bucket;

    // 先遍歷Listen狀態
    for (bucket = 0; bucket < INET_LHTABLE_SIZE; bucket++) {
        struct inet_listen_hashbucket *ilb;

        // 哈希槽
        ilb = &tcp_hashinfo.listening_hash[bucket];

        // 遍歷鏈表並統計
        stat_sock_list(&ilb->head, &ilb->lock, state_counters);
    }

    // 遍歷其他狀態
    for (bucket = 0; bucket < tcp_hashinfo.ehash_mask; bucket++) {
        struct inet_ehash_bucket *ilb;
        spinlock_t *lock;

        // 哈希槽鏈表
        ilb = &tcp_hashinfo.ehash[bucket];
        // 保護鎖
        lock = inet_ehash_lockp(&tcp_hashinfo, bucket);

        // 遍歷鏈表並統計
        stat_sock_list(&ilb->chain, lock, state_counters);
    }

    // 遍歷狀態輸出統計值
    for (state = TCP_ESTABLISHED; state < TCP_MAX_STATES; state++) {
        seq_printf(seq, "%-12s: %d\n", state_names[state], state_counters[state]);
    }

    return 0;
}


static int tcpstat_seq_open(struct inode *inode, struct file *file)
{
    return single_open(file, tcpstat_seq_show, NULL);
}


static const struct file_operations tcpstat_file_ops = {
    .owner   = THIS_MODULE,
    .open    = tcpstat_seq_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = single_release
};


static __init int tcpstat_init(void)
{
    proc_create("tcpstat", 0, NULL, &tcpstat_file_ops);
    return 0;
}


static __exit void tcpstat_exit(void)
{
    remove_proc_entry("tcpstat", NULL);
}

module_init(tcpstat_init);
module_exit(tcpstat_exit);

內核模塊編譯好並加載到內核后, procfs 文件系統提供了一個新文件 /proc/tcpstat ,內容為統計結果:

$ cat /proc/tcpstat
ESTABLISHED : 5
SYN_SENT    : 0
SYN_RECV    : 0
FIN_WAIT1   : 0
FIN_WAIT2   : 0
TIME_WAIT   : 1
CLOSE       : 0
CLOSE_WAIT  : 0
LAST_ACK    : 0
LISTEN      : 14
CLOSING     : 0

當用戶程序讀取這個文件時,內核虛擬文件系統( VFS )調用小菜在內核模塊中寫的處理函數:遍歷內核 TCP 套接字完成統計並格式化統計結果。內核模塊、 VFS 以及套接字等知識超出專欄範圍,不再贅述。

小菜在服務器上試驗這個內核模塊,真的快得飛起!

經驗總結

小菜開始總結這次腳本開發工作中的經驗教訓,他列出了以下關鍵節點:

  1. 依靠 psutil 採集,沒有關注 psutil 實現導致性能問題;
  2. 用生成器代替列表返回連接信息,解決內存瓶頸;
  3. 直接讀取 procfs 文件系統,部分解決 CPU 性能瓶頸;
  4. 通過調節 IO 緩衝區大小,進一步降低 CPU 開銷;
  5. Netlink 代替 procfs ,徹底解決性能問題;
  6. 實驗內核模塊思路,終極解決方案快得飛起;

這些問題節點,一個比一個深入,沒有一定功底是搞不定的。小菜從剛開始跌跌撞撞,到後來獨當一面,快速成長的關鍵在於善於在問題中總結經驗教訓:

  • 程序開發完一定要做性能測試,看能夠扛住多大的壓力;
  • 使用任何工具,需要準確理解其背後的原理,避免誤用;
  • 對編程語言以及操作系統源碼要保持好奇心;
  • 計算機基礎知識很重要,需要及時補全才能達到新高度;
  • 學會問題發散,舉一反三;

更多章節

洞悉 Python 虛擬機運行機制,探索高效程序設計之道!

到底如何才能提升我的 Python 開發水平,向更高一級的崗位邁進? 如果你有這些問題或者疑惑,請訂閱我們的專欄,閱讀更多章節:

  • 內建對象
  • 虛擬機
  • 函數機制
  • 類機制
  • 生成器與協程
  • 內存管理機制

附錄

更多 Python 技術文章請訪問:小菜學Python,轉至 原文 可獲得最佳閱讀體驗。

訂閱更新,獲取更多學習資料,請關注 小菜學編程 :

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案