分類
發燒車訊

新來的”大神”用策略模式把if else給”優化”了,技術總監說:能不能想好了再改?

本文來自作者投稿,原作者:上帝愛吃蘋果

目前在魔都,貝殼找房是我的僱主,平時關注一些 java 領域相關的技術,希望你們能在這篇文章中找到些有用的東西。個人水平有限,如果文章有錯誤還請指出,在留言區一起交流。

我想大家肯定都或多或少的看過各種“策略模式”的講解、佈道等等,這篇文章就是來好好“澄清”一下策略模式,並嘗試回答以下的問題:

1. 策略模式是如何優化業務邏輯代碼結構的?

2. 殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?

3. 有沒有什麼更好的代碼結構來實現策略模式的嗎?

策略模式是如何優化業務邏輯代碼結構的?

要回答這個問題,我們還得先扒一扒策略模式的定義,從定義着手來理解它

策略模式的教科書定義

它的定義很精簡:一個類的行為或其算法可以在運行時更改。我們把它降維到代碼層面,用人話翻譯一下就是,運行時我給你這個類的方法傳不同的“key”,你這個方法會執行不同的業務邏輯。細品一下,這不就是 if else 乾的事嗎?

策略模式優化了什麼?

其實策略模式的核心思想和 if else如出一轍,根據不同的key動態的找到不同的業務邏輯,那它就只是如此嗎?

實際上,我們口中的策略模式其實就是在代碼結構上調整,用接口+實現類+分派邏輯來使代碼結構可維護性好點。

一般教科書上講解到接口與實現類就結束了,其他博客上會帶上提及分派邏輯。這裏就不啰嗦了。

小結一下,即使用了策略模式,你該寫的業務邏輯照常寫,到邏輯分派的時候,還是變相的if else。而它的優化點是抽象了出了接口,將業務邏輯封裝成一個一個的實現類,任意地替換。在複雜場景(業務邏輯較多)時比直接 if else 來的好維護些。

殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!

我想小夥伴們經常有這樣的不滿,我的業務邏輯就3 4 行,你給我整一大堆類定義?有必要這麼麻煩嗎?我看具體的業務邏輯還需要去不同的類中,簡單點行不行。

其實我們所不滿的就是策略模式帶來的缺點:

1、策略類會增多

2、業務邏輯分散到各個實現類中,而且沒有一個地方可以俯視整個業務邏輯

針對傳統策略模式的缺點,在這分享一個實現思路,這個思路已經幫我們團隊解決了多個複雜if else的業務場景,理解上比較容易,代碼上需要用到Java8的特性——利用Map與函數式接口來實現。

直接show代碼結構:為了簡單演示一個思路,代碼用String 類型來模擬一個業務BO

其中:

  1. getCheckResult() 為傳統的做法

  2. getCheckResultSuper()則事先在Map中定義好了“判斷條件”與“業務邏輯”的映射關係,具體講解請看代碼註釋

/**
 * 某個業務服務類
 */
@Service
public class BizService {

    /**
     * 傳統的 if else 解決方法
     * 當每個業務邏輯有 3 4 行時,用傳統的策略模式不值得,直接的if else又顯得不易讀
     */
    public String getCheckResult(String order) {
        if ("校驗1".equals(order)) {
            return "執行業務邏輯1";
        } else if ("校驗2".equals(order)) {
            return "執行業務邏輯2";
        }else if ("校驗3".equals(order)) {
            return "執行業務邏輯3";
        }else if ("校驗4".equals(order)) {
            return "執行業務邏輯4";
        }else if ("校驗5".equals(order)) {
            return "執行業務邏輯5";
        }else if ("校驗6".equals(order)) {
            return "執行業務邏輯6";
        }else if ("校驗7".equals(order)) {
            return "執行業務邏輯7";
        }else if ("校驗8".equals(order)) {
            return "執行業務邏輯8";
        }else if ("校驗9".equals(order)) {
            return "執行業務邏輯9";
        }
        return "不在處理的邏輯中返回業務錯誤";
    }

    /**
     * 業務邏輯分派Map
     * Function為函數式接口,下面代碼中 Function<String, String> 的含義是接收一個Stirng類型的變量,返回一個String類型的結果
     */
    private Map<String, Function<String, String>> checkResultDispatcher = new HashMap<>();

    /**
     * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
     */
    @PostConstruct
    public void checkResultDispatcherInit() {
        checkResultDispatcher.put("校驗1", order -> String.format("對%s執行業務邏輯1", order));
        checkResultDispatcher.put("校驗2", order -> String.format("對%s執行業務邏輯2", order));
        checkResultDispatcher.put("校驗3", order -> String.format("對%s執行業務邏輯3", order));
        checkResultDispatcher.put("校驗4", order -> String.format("對%s執行業務邏輯4", order));
        checkResultDispatcher.put("校驗5", order -> String.format("對%s執行業務邏輯5", order));
        checkResultDispatcher.put("校驗6", order -> String.format("對%s執行業務邏輯6", order));
        checkResultDispatcher.put("校驗7", order -> String.format("對%s執行業務邏輯7", order));
        checkResultDispatcher.put("校驗8", order -> String.format("對%s執行業務邏輯8", order));
        checkResultDispatcher.put("校驗9", order -> String.format("對%s執行業務邏輯9", order));
    }

    public String getCheckResultSuper(String order) {
        //從邏輯分派Dispatcher中獲得業務邏輯代碼,result變量是一段lambda表達式
        Function<String, String> result = checkResultDispatcher.get(order);
        if (result != null) {
            //執行這段表達式獲得String類型的結果
            return result.apply(order);
        }
        return "不在處理的邏輯中返回業務錯誤";
    }
}

通過http調用一下看看效果:

/**
 * 模擬一次http調用
 */
@RestController
public class BizController {

    @Autowired
    private BizService bizService;

    @PostMapping("/v1/biz/testSuper")
    public String test2(String order) {
        return bizService.getCheckResultSuper(order);
    }
}

這是個簡單的demo演示,之後會舉一些複雜的場景案例。

魯迅曾說過,“每解決一個問題,就會因出更多的問題”。我們一起來看看這樣的實現有什麼好處,會帶來什麼問題。

好處很直觀:

  1. 在一段代碼里直觀的看到”判斷條件”與業務邏輯的映射關係
  2. 不需要單獨定義接口與實現類,直接使用現有的函數式接口(什麼?不知道函數式接口?快去了解),而實現類直接就是業務代碼本身。

不好的點:

  1. 需要團隊成員對lambda表達式有所了解(什麼?Java14都出來了還有沒用上Java8新特性的小夥伴?)

接下來我舉幾個在業務中經常遇到的if else場景,並用Map+函數式接口的方式來解決它

在真實業務場景問題的解決思路

有的小夥伴會說,我的判斷條件有多個啊,而且很複雜,你之前舉個例子只有單個判斷邏輯,而我有多個判斷邏輯該怎麼辦呢?

很好解決:寫一個判斷邏輯的方法,Map的key由方法計算出

/**
 * 某個業務服務類
 */
@Service
public class BizService {

    private Map<String, Function<String, String>> checkResultDispatcherMuti = new HashMap<>();

    /**
     * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
     */
    @PostConstruct
    public void checkResultDispatcherMuitInit() {
        checkResultDispatcherMuti.put("key_訂單1", order -> String.format("對%s執行業務邏輯1", order));
        checkResultDispatcherMuti.put("key_訂單1_訂單2", order -> String.format("對%s執行業務邏輯2", order));
        checkResultDispatcherMuti.put("key_訂單1_訂單2_訂單3", order -> String.format("對%s執行業務邏輯3", order));
    }

    public String getCheckResultMuti(String order, int level) {
        //寫一段生成key的邏輯:
        String ley = getDispatcherKey(order, level);

        Function<String, String> result = checkResultDispatcherMuti.get(ley);
        if (result != null) {
            //執行這段表達式獲得String類型的結果
            return result.apply(order);
        }
        return "不在處理的邏輯中返回業務錯誤";
    }

    /**
     * 判斷條件方法
     */
    private String getDispatcherKey(String order, int level) {
        StringBuilder key = new StringBuilder("key");
        for (int i = 1; i <= level; i++) {
            key.append("_" + order + i);
        }
        return key.toString();
    }
}

用http模擬一下:

/**
 * 模擬一次http調用
 */
@RestController
public class BizController {

    @Autowired
    private BizService bizService;

    @PostMapping("/v1/biz/testMuti")
    public String test1(String order, Integer level) {
        return bizService.getCheckResultMuti(order, level);
    }
}

只要設計好你的key的生成規則就好。

還有小夥伴會問:我的業務邏輯有很多很多行,在checkResultDispatcherMuitInit()方法的Map中直接寫不會很長嗎?

直接寫當然長了,我們可以抽象出一個service服務專門放業務邏輯,然後在定義中調用它就好了:

提供一個業務邏輯單元:

/**
 * 提供業務邏輯單元
 */
@Service
public class BizUnitService {

    public String bizOne(String order) {
        return order + "各種花式操作1";
    }
    public String bizTwo(String order) {
        return order + "各種花式操作2";
    }
    public String bizThree(String order) {
        return order + "各種花式操作3";
    }
}
/**
 * 某個業務服務類
 */
@Service
public class BizService {
    @Autowired
    private BizUnitService bizUnitService;

    private Map<String, Function<String, String>> checkResultDispatcherComX = new HashMap<>();

    /**
     * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
     */
    @PostConstruct
    public void checkResultDispatcherComXInit() {
        checkResultDispatcherComX.put("key_訂單1", order -> bizUnitService.bizOne(order));
        checkResultDispatcherComX.put("key_訂單1_訂單2", order -> bizUnitService.bizTwo(order));
        checkResultDispatcherComX.put("key_訂單1_訂單2_訂單3", order -> bizUnitService.bizThree(order));
    }

    public String getCheckResultComX(String order, int level) {
        //寫一段生成key的邏輯:
        String ley = getDispatcherComXKey(order, level);

        Function<String, String> result = checkResultDispatcherComX.get(ley);
        if (result != null) {
            //執行這段表達式獲得String類型的結果
            return result.apply(order);
        }
        return "不在處理的邏輯中返回業務錯誤";
    }

    /**
     * 判斷條件方法
     */
    private String getDispatcherComXKey(String order, int level) {
        StringBuilder key = new StringBuilder("key");
        for (int i = 1; i <= level; i++) {
            key.append("_" + order + i);
        }
        return key.toString();
    }
}

調用結果:

總結

最後,我們一起嘗試回答以下幾個問題:

1. 策略模式是如何優化業務邏輯代碼結構的?

抽象了出了接口,將業務邏輯封裝成一個一個的實現類,任意地替換。在複雜場景(業務邏輯較多)時比直接 if else 來的好維護些。

2. 殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!

我們所不滿的其實就是傳統接口實現的缺點: 1、策略類會很多。 2、業務邏輯分散到各個實現類中,而且沒有一個地方可以俯覽整個業務邏輯

3. 有沒有什麼更好的代碼結構來實現策略模式的嗎?

針對傳統策略模式的缺點,分享了利用Map與函數式接口來實現的思路。

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

分類
發燒車訊

網絡虛擬化之linux虛擬網絡基礎

linux虛擬網絡基礎

1 Device

在linux裏面devic(設備)與傳統網絡概念里的物理設備(如交換機、路由器)不同,Linux所說的設備,其背後指的是一個類似於數據結構、內核模塊或設備驅動這樣的含義。就是說device可能只是軟件系統里的一個驅動,一個函數接口。

2 Tap

Tap位於二層數據鏈路層,tun位於三層網絡層,兩者在linux里的函數結構幾乎一致,除了一個flag值區分tap/tun。在linux中二層特指以太網(Ethernet)(傳統網絡里二層分Ethernet,P2P,HDLC,FR,ATM),因此有時tap也叫“虛擬以太設備”。有意思的是linux創建tap需要用到tun模塊。Linux創建tap/tun都使用tun模塊。

 

3 Namespace

Namespace類似傳統網絡里的VRF,與VRF不同的是:VRF做的是網絡層三層隔離。而namespace隔離的更徹底,它做的是整個協議棧的隔離,隔離的資源包括:UTS(UnixTimesharing  System的簡稱,包含內存名稱、版本、 底層體繫結構等信息)、IPS(所有與進程間通信(IPC)有關的信息)、mnt(當前裝載的文件系統)、PID(有關進程ID的信息)、user(資源配額的信息)、net(網絡信息)。

從網絡角度看一個namespace提供了一份獨立的網絡協議棧(網絡設備接口、IPv4/v6、IP路由、防火牆規則、sockets等),而一個設備(Linux Device)只能位於一個namespace中,不同namespace中的設備可以利用vethpair進行橋接。

 

4 veth pair

veth pair不是一個設備,而是一對設備,以連接兩個虛擬以太端口。操作vethpair,需要跟namespace一起配合,不然就沒有意義。如圖

 

5 Bridge

在Linux的語境里,Bridge(網橋)與Switch(交換機)是一個概念。因為一對veth pair只能連接兩台device,因此如果需要多台設備互聯則需要bridge。

如圖:4個namespace,每個namespace都有一個tap,每個tap與網橋vb1的tap組成一對veth pair,這樣,這4個namespace就可以二層互通了。

 

6 Router

Linux創建Router並沒有像創建虛擬Bridge那樣,有一個直接的命令brctl,而且它間接的命令也沒有,不能創建虛擬路由器……因為它就是路由器(Router) !

如圖:我們需要在router(也就是我們的操作系統linux上增加去往各NS的路由)。

 

7 tun

tun是一個網絡層(IP)的點對點設備,它啟用了IP層隧道功能。Linux原生支持的三層隧道。支持隧道情況:ipip(ipv4 in ipv4)、gre(ipv4/ipv6 over ipv4)、sit(ipv6 over ipv4)、isatap(ipv6/ipv4隧道)、vti(ipsec接口)。

學過傳統網絡GRE隧道的人更容易理解,如圖:

NS1的tun1的ip 10.10.10.1與NS2的tun2的ip 10.10.20.2建立tun

NS1的tun的ip是10.10.10.1,隧道的外層源ip是192.168.1.1,目的ip是192.168.2.1,是不是跟GRE很像。

 

8 iptable

我們通常把iptable說成是linux的防火牆,實際上這種說法並不準確。實際上iptable只是一個運行在用戶空間的命令行工具,真正實現防火牆功能的是內核空間的netfilter模塊。

這裏我們先知道防火牆執行模塊netfilter位於內核空間,命令行iptable位於用戶空間。我們在通過iptable配置的防火牆策略(包括NAT)會在netfilter執行。

iptables有5個鏈:PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING

l  PREROUTING:報文進入網絡接口尚未進入路由之前的時刻;

l  INPUT:路由判斷是本機接收的報文,準備從內核空間進入到用戶空間的時刻;

l  FORWARD:路由判斷不是本機接收的報文,需要路由轉發,路由轉發的那個時刻;

l  OUTPUT:本機報文需要發出去 經過路由判斷選擇好端口以後,準備發送的那一刻;

l  POSTROUTING:FORWARD/OUTPUT已經完成,報文即將出網絡接口的那一刻。

 DNAT用的是PREROUTING,修改的是目的地址,SNAT用的是POSTROUTING,修改的是源地址。

Iptable有5個表:filter,nat,mangle,raw, security,raw表和security表不常用。主流文檔都是說5鏈4表,沒有包括security表。

l  Raw表——決定數據包是否被狀態跟蹤機制處理

l  Mangle表——修改數據包的服務類型、TTL、並且可以配置路由實現QOS

l  Nat表——用於網絡地址轉換(IP、端口)

l  filter表——過濾數據包

l  security 表(用於強制訪問控制網絡規則,例如:SELinux)

4個表的優先級由高到低的順序為:raw–>mangle–>nat–>filter。RAW表,在某個鏈上,RAW表處理完后,將跳過NAT表和 ip_conntrack處理,即不再做地址轉換和數據包的鏈接跟蹤處理了。RAW表可以應用在那些不需要做nat的情況下,以提高性能。如大量訪問的web服務器,可以讓80端口不再讓iptables做數據包的鏈接跟蹤處理,以提高用戶的訪問速度。

下面講下數據包流向與處理:

  1. 如果是外部訪問的目的是本機,比如用戶空間部署了WEB服務,外部來訪問。數據包從外部進入網卡—–>PREROUTING處理—–>INPUT處理—–>到達用戶空間程序接口,程序處理完成后發出—–>OUTPUT處理—–>POSTROUTING處理。每個處理點都有對應的表,表的處理順序按照raw–>mangle–>nat–>filter處理。
  2. 如果用戶訪問的目的不是本機,linux只是一个中轉(轉發)設備,此時需要開啟ip forward功能,數據流就是進入網卡—–> PREROUTING處理—–> FORWARD處理—–> POSTROUTING處理。

 

8.2 NAT

Netfilter中的NAT有三個點做處理,

(1)   NAT-PREROUTING (DNAT)

數據報文進入PREROUTING,NAT模塊就會處理,比如用戶空間的WEB服務私網地址192.168.0.1,對外提供公網ip是220.1.1.1。

當外部ip訪問220.1.1.1時,PREROUTING接受數據包,NAT模塊處理將目的ip 220.1.1.1轉換為私網ip192.168.0.1,這就是DNAT。

(2)   NAT-POSTROUTING (SNAT)

用戶空間應用程序訪問外部網絡,比如用戶空間應用程序訪問114.114.114.144,私網ip 192.168.0.1,此時數據包流經POSTROUTING,NAT模塊會處理,將192.168.0.1轉換為220.2.2.2,對於目的ip114.114.114.114來說,就是220.2.2.2訪問它,這就是SNAT。

(3)   NAT-OUTPUT (DNAT)

我們把內核空間想象成一台防火牆,防火牆自身對外發送報文訪問外部時,就在OUTPUT做DNAT,此時不需要再POSTROUTING點再做NAT。因為此時從OUTPUT出來的源IP已經是公網地址了

8.3  Firewall

防火牆根據規則執行accept/reject動作,防火牆規則的元素如下:

入接口、出接口、協議、源地址/子網、目的地址/子網、源端口、目的端口。

Netfilter中的Firewall會在這三個點進行處理:INPUT/FORWARD/OUTPUT

8.4 Mangle

mangle表主要用於修改數據包的ToS(  Type of Service,服務類型)、 TTL(Time to Live,生存周期)以及為數據包設置Mark標記,以實現QoS(Qualityof Service,服務質量)調整以及策略路由等應用。Netfilter每個點都可以做mangle。

9 總結

tap、tun、vethpair在Linux中都被稱為設備,但是在與日常概念的類比中,常常被稱作接口。而bridge和router這些日常稱為設備的再linux中反而不稱為設備。linux利用namespace做隔離,Bridge提供二層轉發功能,Router提供三層轉發功能。Router還常常藉助iptable提供SNAT/DNAT功能。Bridge也常常藉助iptable提供Firewall功能。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

分類
發燒車訊

跟着whatwg看一遍事件循環

前言

對於單線程來說,事件循環可以說是重中之重了,它為任務分配不同的優先級,井然有序的調度。讓js解析,用戶交互,頁面渲染等互不衝突,各司其職。

我們書寫的代碼無時無刻都在和事件循環打交道,要想寫出更流暢,我們就必須深入了解事件循環,下面我們將從規範中翻譯和解讀整個流程。

以下內容來自whatwg文檔,均為個人理解,若有不對,煩請指出,我會第一時間修改,避免誤導他人!

正文

為了協調用戶操作,js執行,頁面渲染,網絡請求等事件,每個宿主中,存在事件循環這樣的角色,並且該角色在當前宿主中是唯一的。

簡單解釋一下宿主:宿主是一個ECMAScript執行上下文,一般包含執行上下文棧,運行時執行環境,宿主記錄和一個執行線程,除了這個執行線程外,其他的專屬於當前宿主。例如,某些瀏覽器在不同的tabs使用同一個執行線程。

不僅如此,事件循環又存於在各個不同場景,有瀏覽器環境下的,worker環境下的和Worklet環境下的。

Worklet是一個輕量級的web worker,可以讓開發者訪問更底層的渲染工作線,也就是說你可以通過Worklet去干預瀏覽器的渲染環境。

提到了worklet,那就順便看一個例子(需開啟服務,不要以file協議運行),通過這個例子,可以看到事件循環不同階段觸發了什麼鈎子函數:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <style>
            .fancy {
                background-image: paint(headerHighlight);
                display: layout(sample-layout);
                background-color: green;
            }
        </style>
    </head>
    <body>
        <h1 class="fancy">My Cool Header</h1>
        <script>
            console.log('開始');
            CSS.paintWorklet.addModule('./paint.js');
            CSS.layoutWorklet.addModule('./layout.js');

            requestAnimationFrame(() => {
                console.log('requestAnimationFrame');
            });
            Promise.resolve().then(() => {
                console.log('微任務');
            });
            setTimeout(function () {
                document.querySelector('.fancy').style.height = '150px';
                ('translateZ(0)');

                Promise.resolve().then(() => {
                    console.log('新一輪的微任務');
                });
                requestAnimationFrame(() => {
                    console.log('新一輪的requestAnimationFrame');
                });
            }, 2000);
            console.log(2);
        </script>
    </body>
</html>

// paint.js
registerPaint(
    'headerHighlight',
    class {
        static get contextOptions() {
            console.log('contextOptions');
            return {alpha: true};
        }

        paint(ctx) {
            console.log('paint函數');
        }
    }
);

// ==========================分割線

// layout.js
registerLayout(
    'sample-layout',
    class {
        async intrinsicSizes(children, edges, styleMap) {}

        async layout(children, edges, constraints, styleMap, breakToken) {
            console.log('layout階段');
        }
    }
);

事件循環有一個或多個Task隊列,每個Task隊列都是Task的一個集合。其中Task不是指我們的某個函數,而是一個上下文環境,結構如下:

  • step:一系列任務將要執行的步驟
  • source:任務來源,常用來對相關任務進行分組和系列化
  • document:與當前任務相關的document對象,如果是非window環境則為null
  • 環境配置對象:在任務期間追蹤記錄任務狀態

這裏的Task隊列不是Task,是一個集合,因為取出一個Task隊列中的Task是選擇一個可執行的Task,而不是出隊操作。

微任務隊列是一個入對出對的隊列。

這裏說明一下,Task隊列為什麼有多個,因為不同的Task隊列有不同的優先級,進而進行次序排列和調用,有沒有感覺react的fiber和這個有點類似?

舉個例子,Task隊列可以是專門負責鼠標和鍵盤事件的,並且賦予鼠標鍵盤隊列較高的優先級,以便及時響應用戶操作。另一個Task隊列負責其他任務源。不過也不要餓死任何一個task,這個後續處理模型中會介紹。

Task封裝了負責以下任務的算法:

  • Events: 由專門的Task在特定的EventTarget(一個具有監聽訂閱模式列表的對象)上分發事件對象
  • Parsing: html解析器標記一個或多個字節,並處理所有生成的結果token
  • Callbacks: 由專門的Task觸發回調函數
  • Using a resource: 當該算法獲取資源的時候,如果該階段是以非阻塞方式發生,那麼一旦部分或者全部資源可用,則由Task進行後續處理
  • Reacting to DOM manipulation: 通過dom操作觸發的任務,例如插入一個節點到document

事件循環有一個當前運行中的Task,可以為null,如果是null的話,代表着可以接受一個新的Task(新一輪的步驟)。

事件循環有微任務隊列,默認為空,其中的任務由微任務排隊算法創建。

事件循環有一個執行微任務檢查點,默認為false,用來防止微任務死循環。

微任務排隊算法:

  1. 如果未提供event loop,設置一個隱式event loop。
  2. 如果未提供document,設置一個隱式document.
  3. 創建一個Task作為新的微任務
  4. 設置setp、source、document到新的Task上
  5. 設置Task的環境配置對象為空集
  6. 添加到event loop的微任務隊列中

微任務檢查算法:

  1. 如果微任務檢查標誌為true,直接return
  2. 設置微任務檢查標誌為true
  3. 如果微任務隊里不為空(也就是說微任務添加的微任務也會在這個循環中出現,直到微任務隊列為空):
    1. 從微任務隊列中找出最老的任務(防餓死)
    2. 設置當前執行任務為這個最老的任務
    3. 執行
    4. 重置當前執行任務為null
  4. 通知環境配置對象的promise進行reject操作
  5. 清理indexdb事務(不太明白這一步,如果有讀者了解,煩請點撥一下)
  6. 設置微任務檢查標誌為false

處理模型

event loop會按照下面這些步驟進行調度:

  1. 找到一個可執行的Task隊列,如果沒有則跳轉到下面的微任務步驟
  2. 讓最老的Task作為Task隊列中第一個可執行的Task,並將其移除
  3. 將最老的Task作為event loop的可執行Task
  4. 記錄任務開始時間點
  5. 執行Task中的setp對應的步驟(上文中Task結構中的step)
  6. 設置event loop的可執行任務為null
  7. 執行微任務檢查算法
  8. 設置hasARenderingOpportunity(是否可以渲染的flag)為false
  9. 記住當前時間點
  10. 通過下面步驟記錄任務持續時間
    1. 設置頂層瀏覽器環境為空
    2. 對於每個最老Task的腳本執行環境配置對象,設置當前的頂級瀏覽器上下文到其上
    3. 報告消耗過長的任務,並附帶開始時間,結束時間,頂級瀏覽器上下文和當前Task
  11. 如果在window環境下,會根據硬件條件決定是否渲染,比如刷新率,頁面性能,頁面是否在後台,不過渲染會定期出現,避免頁面卡頓。值得注意的是,正常的刷新率為60hz,大概是每秒60幀,大約16.7ms每幀,如果當前瀏覽器環境不支持這個刷新率的話,會自動降為30hz,而不是丟幀。而李蘭其在後台的時候,聰明的瀏覽器會將這個渲染時機降為每秒4幀甚至更低,事件循環也會減少(這就是為什麼我們可以用setInterval來判斷時候能打開其他app的判斷依據的原因)。如果能渲染的話會設置hasARenderingOpportunity為true。

除此之外,還會在觸發resize、scroll、建立媒體查詢、運行css動畫等,也就是說瀏覽器幾乎大部分用戶操作都發生在事件循環中,更具體點是事件循環中的ui render部分。之後會進行requestAnimationFrame和IntersectionObserver的觸發,再之後是ui渲染

  1. 如果下麵條件都成立,那麼執行空閑階段算法,對於開發者來說就是調用window.requestIdleCallback方法
    1. 在window環境下
    2. event loop中沒有活躍的Task
    3. 微任務隊列為空
    4. hasARenderingOpportunity為false

借鑒網上的一張圖來粗略表示下整個流程

小結

上面就是整個事件循環的流程,瀏覽器就是按照這個規則一遍遍的執行,而我們要做的就是了解並適應這個規則,讓瀏覽器渲染出性能更高的頁面。

比如:

  1. 非首屏相關性能打點可以放到idle callback中執行,減少對頁面性能的損耗
  2. 微任務中遞歸添加微任務會導致頁面卡死,而不是隨着事件循環一輪輪的執行
  3. 更新元素布局的最好時機是在requestAnimateFrame中
  4. 盡量避免頻繁獲取元素布局信息,因為這會觸發強制layout(哪些屬性會導致強制layout?),影響頁面性能
  5. 事件循環有多個任務隊列,他們互不衝突,但是用戶交互相關的優先級更高
  6. resize、scroll等會伴隨事件循環中ui渲染觸發,而不是根據我們的滾動觸發,換句話說,這些操作自帶節流
  7. 等等,歡迎補充

最後感謝大家閱讀,歡迎一起探討!

提前祝大家端午節nb

參考

composite

深入探究 eventloop 與瀏覽器渲染的時序問題

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

Spark學習筆記(三)-Spark Streaming

Spark Streaming支持實時數據流的可擴展(scalable)、高吞吐(high-throughput)、容錯(fault-tolerant)的流處理(stream processing)。

 

                                                    架構圖

 

特性如下:

 

  • 可線性伸縮至超過數百個節點;

  • 實現亞秒級延遲處理;

  • 可與Spark批處理和交互式處理無縫集成;

  • 提供簡單的API實現複雜算法;

  • 更多的流方式支持,包括Kafka、Flume、Kinesis、Twitter、ZeroMQ等。

 

原理

 

Spark在接收到實時輸入數據流后,將數據劃分成批次(divides the data into batches),然後轉給Spark Engine處理,按批次生成最後的結果流(generate the final stream of results in batches)。 

 

 

API

 

DStream

 

DStream(Discretized Stream,離散流)是Spark Stream提供的高級抽象連續數據流。

 

  • 組成:一個DStream可看作一個RDDs序列。

  • 核心思想:將計算作為一系列較小時間間隔的、狀態無關的、確定批次的任務,每個時間間隔內接收的輸入數據被可靠存儲在集群中,作為一個輸入數據集。

 

 

  • 特性:一個高層次的函數式編程API、強一致性以及高校的故障恢復。

  • 應用程序模板:

  • 模板1

  • 模板2

 

WordCount示例

 

 

Input DStream

 

Input DStream是一種從流式數據源獲取原始數據流的DStream,分為基本輸入源(文件系統、Socket、Akka Actor、自定義數據源)和高級輸入源(Kafka、Flume等)。

 

  • Receiver:
  • 每個Input DStream(文件流除外)都會對應一個單一的Receiver對象,負責從數據源接收數據並存入Spark內存進行處理。應用程序中可創建多個Input DStream并行接收多個數據流。

  • 每個Receiver是一個長期運行在Worker或者Executor上的Task,所以會佔用該應用程序的一個核(core)。如果分配給Spark Streaming應用程序的核數小於或等於Input DStream個數(即Receiver個數),則只能接收數據,卻沒有能力全部處理(文件流除外,因為無需Receiver)。

  • Spark Streaming已封裝各種數據源,需要時參考官方文檔。

 

Transformation Operation

 

  • 常用Transformation

 

* map(func) :對源DStream的每個元素,採用func函數進行轉換,得到一個新的DStream;

* flatMap(func):與map相似,但是每個輸入項可用被映射為0個或者多個輸出項;

* filter(func):返回一個新的DStream,僅包含源DStream中滿足函數func的項;

* repartition(numPartitions):通過創建更多或者更少的分區改變DStream的并行程度;

* union(otherStream):返回一個新的DStream,包含源DStream和其他DStream的元素;

* count():統計源DStream中每個RDD的元素數量;

* reduce(func):利用函數func聚集源DStream中每個RDD的元素,返回一個包含單元素RDDs的新DStream;

* countByValue():應用於元素類型為K的DStream上,返回一個(K,V)鍵值對類型的新DStream,每個鍵的值是在原DStream的每個RDD中的出現次數;

* reduceByKey(func, [numTasks]):當在一個由(K,V)鍵值對組成的DStream上執行該操作時,返回一個新的由(K,V)鍵值對組成的DStream,每一個key的值均由給定的recuce函數(func)聚集起來;

* join(otherStream, [numTasks]):當應用於兩個DStream(一個包含(K,V)鍵值對,一個包含(K,W)鍵值對),返回一個包含(K, (V, W))鍵值對的新DStream;

* cogroup(otherStream, [numTasks]):當應用於兩個DStream(一個包含(K,V)鍵值對,一個包含(K,W)鍵值對),返回一個包含(K, Seq[V], Seq[W])的元組;

* transform(func):通過對源DStream的每個RDD應用RDD-to-RDD函數,創建一個新的DStream。支持在新的DStream中做任何RDD操作。

 

  • updateStateByKey(func)

  • updateStateByKey可對DStream中的數據按key做reduce,然後對各批次數據累加

  • WordCount的updateStateByKey版本

 

  • transform(func)

  • 通過對原DStream的每個RDD應用轉換函數,創建一個新的DStream。

  • 官方文檔代碼舉例

 

  • Window operations

  • 窗口操作:基於window對數據transformation(個人認為與Storm的tick相似,但功能更強大)。

  • 參數:窗口長度(window length)和滑動時間間隔(slide interval)必須是源DStream批次間隔的倍數。

  • 舉例說明:窗口長度為3,滑動時間間隔為2;上一行是原始DStream,下一行是窗口化的DStream。

  • 常見window operation

有狀態轉換包括基於滑動窗口的轉換和追蹤狀態變化(updateStateByKey)的轉換。

基於滑動窗口的轉換

* window(windowLength, slideInterval) 基於源DStream產生的窗口化的批數據,計算得到一個新的DStream;

* countByWindow(windowLength, slideInterval) 返迴流中元素的一個滑動窗口數;

* reduceByWindow(func, windowLength, slideInterval) 返回一個單元素流。利用函數func聚集滑動時間間隔的流的元素創建這個單元素流。函數func必須滿足結合律,從而可以支持并行計算;

* reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) 應用到一個(K,V)鍵值對組成的DStream上時,會返回一個由(K,V)鍵值對組成的新的DStream。每一個key的值均由給定的reduce函數(func函數)進行聚合計算。注意:在默認情況下,這個算子利用了Spark默認的併發任務數去分組。可以通過numTasks參數的設置來指定不同的任務數;

* reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) 更加高效的reduceByKeyAndWindow,每個窗口的reduce值,是基於先前窗口的reduce值進行增量計算得到的;它會對進入滑動窗口的新數據進行reduce操作,並對離開窗口的老數據進行“逆向reduce”操作。但是,只能用於“可逆reduce函數”,即那些reduce函數都有一個對應的“逆向reduce函數”(以InvFunc參數傳入);

* countByValueAndWindow(windowLength, slideInterval, [numTasks]) 當應用到一個(K,V)鍵值對組成的DStream上,返回一個由(K,V)鍵值對組成的新的DStream。每個key的值都是它們在滑動窗口中出現的頻率。

  • 官方文檔代碼舉例 

 

  • join(otherStream, [numTasks])

  • 連接數據流

  • 官方文檔代碼舉例1

  • 官方文檔代碼舉例2

 

Output Operation

 

 

緩存與持久化

 

  • 通過persist()將DStream中每個RDD存儲在內存。

  • Window operations會自動持久化在內存,無需显示調用persist()。

  • 通過網絡接收的數據流(如Kafka、Flume、Socket、ZeroMQ、RocketMQ等)執行persist()時,默認在兩個節點上持久化序列化后的數據,實現容錯。

 

Checkpoint

 

  • 用途:Spark基於容錯存儲系統(如HDFS、S3)進行故障恢復。

  • 分類:

  • 元數據檢查點:保存流式計算信息用於Driver運行節點的故障恢復,包括創建應用程序的配置、應用程序定義的DStream operations、已入隊但未完成的批次。

  • 數據檢查點:保存生成的RDD。由於stateful transformation需要合併多個批次的數據,即生成的RDD依賴於前幾個批次RDD的數據(dependency chain),為縮短dependency chain從而減少故障恢復時間,需將中間RDD定期保存至可靠存儲(如HDFS)。

  • 使用時機:

  • Stateful transformation:updateStateByKey()以及window operations。

  • 需要Driver故障恢復的應用程序。

  • 使用方法

  • Stateful transformation

streamingContext.checkpoint(checkpointDirectory)

 

  • 需要Driver故障恢復的應用程序(以WordCount舉例):如果checkpoint目錄存在,則根據checkpoint數據創建新StreamingContext;否則(如首次運行)新建StreamingContext。

 

  • checkpoint時間間隔

  • 方法:

dstream.checkpoint(checkpointInterval)

 

  • 原則:一般設置為滑動時間間隔的5-10倍。

  • 分析:checkpoint會增加存儲開銷、增加批次處理時間。當批次間隔較小(如1秒)時,checkpoint可能會減小operation吞吐量;反之,checkpoint時間間隔較大會導致lineage和task數量增長。

 

性能調優

 

降低批次處理時間

 

  • 數據接收并行度

  • 增加DStream:接收網絡數據(如Kafka、Flume、Socket等)時會對數據反序列化再存儲在Spark,由於一個DStream只有Receiver對象,如果成為瓶頸可考慮增加DStream。

  • 設置“spark.streaming.blockInterval”參數:接收的數據被存儲在Spark內存前,會被合併成block,而block數量決定了Task數量;舉例,當批次時間間隔為2秒且block時間間隔為200毫秒時,Task數量約為10;如果Task數量過低,則浪費了CPU資源;推薦的最小block時間間隔為50毫秒。

  • 顯式對Input DStream重新分區:在進行更深層次處理前,先對輸入數據重新分區。

inputStream.repartition(<number of partitions>)

 

  • 數據處理并行度:reduceByKey、reduceByKeyAndWindow等operation可通過設置“spark.default.parallelism”參數或顯式設置并行度方法參數控制。

  • 數據序列化:可配置更高效的Kryo序列化。

 

設置合理批次時間間隔

 

  • 原則:處理數據的速度應大於或等於數據輸入的速度,即批次處理時間大於或等於批次時間間隔。

  • 方法:

  • 先設置批次時間間隔為5-10秒以降低數據輸入速度;

  • 再通過查看log4j日誌中的“Total delay”,逐步調整批次時間間隔,保證“Total delay”小於批次時間間隔。

 

內存調優

 

  • 持久化級別:開啟壓縮,設置參數“spark.rdd.compress”。

  • GC策略:在Driver和Executor上開啟CMS。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

分類
發燒車訊

前端業務代碼中的配置化

業務代碼中的配置化

工作中有許多邏輯冗雜、迭代頻繁的業務代碼,隨着迭代將越來越難以維護,一些場景適合通過配置化的方式來處理便於維護。

一、什麼是業務代碼配置化?

根據業務場景使用配置化的 Object|Array|Map 處理條件判斷邏輯,通常需要配置文件 CONFIG.js,若邏輯複雜需添加 getConfig 的處理函數 – tool.js

  • 本質上 if/else 邏輯是一種狀態匹配

  • 表驅動法,使用表數據,存儲對應的狀態處理

  • 可讀性好,減少了繁雜嵌套的 if-else,讀取配置,邏輯更清晰

  • 可維護性高,邏輯分支的增刪只是 CONFIG 的增刪

二、如何在業務場景中進行代碼配置化?

1. 簡單的狀態映射

  • 按需使用 Object|Map 配置
單一條件
  • Object 形式:
// CONFIG.JS
  export const STATUS = {
    STUDENT: 0,
    TEACHER: 1,
    MA_NONG: 2,
  };
  export const WORK_MAP = {
    STATUS.STUDENT: '學生',
    STATUS.TEACHER: '老師',
    STATUS.MA_NONG: '碼農',
  };

// index.js
  this.setData({
    work: WORK_MAP[status],
  });

  axios.post(url, { status: STATUS.MA_NONG });
  • Map 形式:
// CONFIG.JS
export const WORK_MAP = new Map([
  [0, "學生"],
  [1, "老師"],
  [2, "碼農"],
]);
// index.js
this.setData({
  work: WORK_MAP.get(status),
});
多重條件
const config = new Map([
  [
    (condition0, condition1, condition2) =>
      condition0 && condition1 && condition2,
    () => {
      console.log("map0");
    },
  ],
  [
    (condition0, condition1, condition2) =>
      condition0 || condition1 || condition2,
    () => {
      console.log("map1");
    },
  ],
]);
config.forEach((action, _if) => _if(0, 1, 0) && action());

2. 每個狀態有多種屬性

  • 多個屬性
  • 使用 Array 配置
// CONFIG.JS
  export const CONFIG = [
    {
      status: STATUS.STUDENT,
      name: '學生',
      action: '談戀愛',
    },
    {
      status: STATUS.TEACHER,
      name: '老師',
      action: '教書',
    },
    {
      status: STATUS.MA_NONG,
      name: '碼農',
      action: '寫bug',
    },
  ];

// index.js
  <!-- 根據狀態不同的行為 -->
  function action(status) {
    const { name, work } = CONFIG.find(i => i.status === status);
    console.log(`${name}在${action}`);
  }

3. 每個狀態有多種屬性且參數定製化

  • 參數高度定製化,不同狀態需要適配接口不同的字段
  • 使用 Array 配置
  • 通過配置函數並傳參注入接口數據可滿足定製化需求
// CONFIG.JS
  export const CONFIG = [
    {
      status: STATUS.STUDENT,
      name: '學生',
      action: () => {
        console.log('學生的工作是談戀愛');
      },
    },
    {
      status: STATUS.TEACHER,
      name: '老師',
      action: (info) => {
        alert(`老師${info.age}歲,每天${info.action}`);
      },
    },
    {
      status: STATUS.MA_NONG,
      name: '碼農',
      action: (info) => {
        toast(`碼農工作${info.workTime}年了,頭髮僅剩${info.hair}根了`);
      },
    },
  ];

// index.js
  <!-- 根據接口狀態action -->
  function action(res) {
    const { action, info } = CONFIG.find(i => i.status === res.status);
    action && action(info); // 傳參定製化
  }

三、實例

大首頁瀑布流 item 樣式

  • 根據 list 接口下發的 item 的類型(type)&樣式(layout)字段取 item 中的封面、標題、標籤、頭像…,字段各不相同
  • 十幾種 item 類型,有的還有不同的 layout,item 數據下發方式不同
  • 公共組件,需要適配其他模塊的接口數據作展示

index.xml

  • 數據驅動,減少模板中的判斷邏輯
<view class="panel" bind:tap="goDetail">
  <!-- 封面 -->
  <image  wx:if="{{panel.cover}}" class="panel__cover" src="{{panel.cover.image}}">
      <view class="panel__tag {{panel.tagClass}}" wx:if="{{panel.tagClass}}">{{panel.tag}}</view>
  </image>
  <!-- 標題 -->
  <view class="panel__titl" wx:if="{{panel.title}}">{{panel.title}}</view>
  <!-- footer -->
  <view class="panel__footer" wx:if="{{panel.showFooter}}">
    <image class="panel__footer-icon" wx:if="{{panel.user.icon}}" src="{{panel.user.icon}}"></image>
    <text class="panel__footer-name" wx:if="{{panel.user.nickname}}">{{panel.user.nickname}}</text>
    <text class="panel__footer-comment" wx:if="{{panel.commentCount}}">{{panel.commentCount}}評論</text>
  </view>
</view>

CONFIG.js

import { Layout, NewsType, Redirect } from 'src/constant';
import { formatImage, formatUser } from './tool';

/**
 * 配置項
 * @param {String} title 標題
 * @param {String} cover 封面
 * @param {String} tag 標籤
 * @param {Object} user 用戶信息
 * @param {Boolean} showFooter 是否显示footer
 * @param {Boolean} isAd 是否廣告
 * @param {Function} itemWrap 兼容接口數據函數,數據可能以ref_xxx下發,比如帖子:ref_post
 * ......
 */

<!-- 默認配置項 -->
export const DEFAULT = {
  title: ({ title = '' }) => title,
  cover: ({ image_list = [], article_images = [] }) =>
    formatImage(image_list[0]) || formatImage(article_images[0]),
  showFooter: true,
  user: ({ user, user_account, article_source_tx = '' }) =>
    user
      ? formatUser(user)
      : user_account
      ? formatUser(user_account)
      : {
          icon: '',
          nickname: article_source_tx,
        },
};

export const CONFIG = [
  {
    type: NewsType.NEWS,
    ...DEFAULT,
    tag: '資訊',
    tagClass: 'news',
  },
  {
    type: NewsType.VIDEO,
    ...DEFAULT,
    tag: '',
    tagClass: 'video',
    cover: ({ image_gif_list = [], image_list = [] }) => formatImage(image_gif_list[0] || image_list[0]),
    video: ({ video_url = '', tencent_vid = '', image_gif_list = [] }) => ({
      hasVideo: true,
      src: tencent_vid || video_url,
      video_url,
      tencent_vid,
      gifCover: formatImage(image_gif_list[0]),
    }),
  },
  {
    type: Redirect.EVAL_DETAIL,
    layouts: [
      {
        layout: Layout.EVALUATION,
        ...DEFAULT,
        tag: '口碑',
        tagClass: 'praise',
      },
      {
        layout: Layout.PRAISE_COMPONENT,
        ...DEFAULT,
        tag: '口碑',
        tagClass: 'praise',
        itemWrap: ({ ref_car_score = {} }) => ref_car_score,
        title: ({ chosen_topic = '' }) => chosen_topic,
        commentCount: ({ comment_count = null }) => comment_count,
        cover: ({ images = [], recommend_images = [] }) =>
          formatImage(images[0]) ||
          formatImage(getCoverFromRecommendImages(recommend_images)),
      },
      {
        layout: Layout.NORMAL,
        ...DEFAULT,
      },
    ],
  },
  ......
];

tool.js

import { CONFIG, DEFAULT, AD_CONFIG } from "./CONFIG";
// 獲取瀑布流item數據
export const getPanelData = (item) => {
  const getConfigByTypeAndLayout = () => {
    let config = CONFIG.find((i) => i.type == item.type);
    if (item.isAd) {
      config = AD_CONFIG;
    }
    if (config && config.layouts) {
      config = config.layouts.find(
        (i) => i.layout === item.layout_type || i.layout === item.display_style
      );
    }
    if (!config) {
      config = DEFAULT;
      console.log("no-config", item.type, item.layout_type, item);
    }
    return config;
  };
  const getPanelDataByConfig = (c) => {
    const panel = {};
    let _item = item;
    if (c.itemWrap) {
      _item = c.itemWrap(item);
    }
    Object.keys(c).forEach((key) => {
      if (typeof c[key] === "function") {
        panel[key] = c[key](_item);
      } else {
        panel[key] = c[key];
      }
    });
    return panel;
  };
  // 根據item的類型、樣式獲取配置
  const config = getConfigByTypeAndLayout(item);
  // 根據配置獲取瀑布流item信息
  return getPanelDataByConfig(config);
};

四、結語

所以,業務代碼配置化很簡單,大家也都一直在用,只是如果在一些業務場景中都形成配置化的習慣或者共識,可能更好維護吧。

鄙人拙見,大佬賜教~

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

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

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

分類
發燒車訊

在採用K8S之前您必須了解的5件事情

作者簡介

Christopher Tozzi,自2008年來以自由職業者的身份對Linux、虛擬化、容器、數據存儲及其相關主題進行報道。

時至今日,Kubernetes已然成為風靡一時的容器編排調度工具,許多IT分析師均提出了企業應當在何時採用Kubernetes的深刻建議。然而,和所有其他的軟件平台一樣,Kubernetes並非是一個適用於所有人的靈丹妙藥。我更傾向於認為人們在有些時候過分誇大了Kubernetes的作用,以至於他們產生了一種錯覺:他們無法離開Kubernetes,而實際上,Kubernetes比他們真正的需求要複雜得多。

為了分析人們真正的需求與Kubernetes的匹配程度,我分析了企業在採用Kubernetes編排之前必須考慮的5個事情。

Kubernetes是什麼?

如果您關注容器,您可能會知道Kubernetes是一個用於容器編排的開源工具,它可以自動執行諸如啟動容器、停止容器以及在同一個容器的不同實例之間的負載均衡等重要任務。

簡而言之,Kubernetes的主要目的是最大限度地減少工程師必須手動執行的管理工作量,並通過簡化容器操作,幫助企業大規模運行複雜的容器化應用程序。

決定是否採用Kubernetes的關鍵要素

基於Kubernetes的設立初衷,如果您喜歡自動化,討厭手動執行重複性的任務,那麼Kubernetes無疑是您的極佳選擇。

這是您決定是否採用Kubernetes的重要前提,但是,您不能僅根據這一“前提”就決定是否採用Kubernetes。在採用Kubernetes之前,您還需要考慮並權衡其他重要的因素。

1、Kubernetes的基礎設施規模

您的基礎設施規模是其中一個決定Kubernetes是否能夠很好地為您所用的關鍵要素。

Kubernetes的設計初衷是協調分佈在真正龐大的環境中的容器,這往往意味着企業應當擁有數十台主機服務器。根據過往的實施經驗,如果基礎架構中的服務器少於50個,那麼您可能沒有足夠的資源來利用Kubernetes的全部優勢。

這並不是指Kubernetes無法在較小規模的基礎設施上運行。實際上,如果您願意,您可以在單個主機上運行Kubernetes。然而,由於Kubernetes其中的一個研發目的是:通過在數量龐大的集群中分佈容器化應用程序提供高可用性,因此,如果您只有少量服務器,則無法享受到Kubernetes的某些價值。

除此之外,考慮到設置和維護Kubernetes的複雜性,如果您的基礎設施規模較小,無法完全實現Kubernetes的高可用性承諾,那麼或許您不應投入過多時間和精力在Kubernetes上。

對於較小的基礎架構,您可以使用較為簡單的容器編排工具,或者使用如AWS ECS等具有內置編排的基於雲的容器服務。

2、Kubernetes操作系統環境

Kubernetes主要是一種Linux技術。儘管Kubernetes可以用於管理託管Windows服務器上的容器化應用程序,這些應用程序作為Kubernetes服務器集群內的所謂工作節點運行。但託管Kubernetes核心服務的主要服務器或者說主節點必須是Linux。

因此,如果您的商店以Windows為中心,那麼Kubernetes並非您的最佳選擇。但是您可以選擇Rancher輕鬆將Kubernetes的優勢引入Windows,並且極大程度降低使用的複雜性。

3、安裝和設置Kubernetes

在決定採用Kubernetes之前,您還需要評估您可以在此項目上投入的工作時間。

普通的開放源代碼版本的Kubernetes缺少內置的應用程序,也並未提供一種可以適用於所有默認配置的安裝方式。在集群正常運行之前,您需要投入大量的時間從頭開始編寫及調整配置文件。因此,安裝和配置Kubernetes的過程或許是一個令人生畏的過程,您需要投入大量的時間和精力。

部分Kubernetes發行版提供了交互式安裝程序腳本,可以幫助您自動執行大部分設置過程。如果您選擇Rancher等Kubernetes發行版,則有望在一两天內輕鬆完成配置及安裝。

第三種選擇是使用諸如Google Kubernetes Engine等雲供應商解決方案,將Kubernetes作為託管服務在雲上運行。在這種情況下,您可以自行選擇安裝及設置。但值得注意的一點是,在確定如何配置Kubernetes環境時,您的選擇可能會受到限制。

您必須意識到最為關鍵的一點:不要低估配置Kubernetes的難度。在您真的要全身心投入Kubernetes之前,請確保您所付出的努力是值得的。另一方面,如果您無法確定為企業在生產集群上安裝和部署Kubernetes的難度,您可以嘗試使用K3s等輕量級Kubernetes發行版來進行測試,預估後續需要付出多少努力來進行Kubernetes的配置和設置。

4、Kubernetes和聲明式配置管理

Kubernetes採用了所謂的聲明式配置管理方法,這就意味着,您需要自行編寫配置文件來設置Kubernetes應用程序應當如何運行,而Kubernetes將自動指出如何使應用程序符合規範。

聲明式配置管理與命令式配置管理相反,在命令式配置管理中,您可以自行配置應用程序的每個組件,並讓其按照您所想要的方式運行。

聲明式配置是Kubernetes在許多用戶實例中如此強大和可伸縮的其中一個原因。您可以設置一次配置,並且根據需要多次應用它。

但是,如果您的配置需求不斷變化,或者在工作負載或環境中的不同部分之間變化,那麼您應當如何處理呢?在這種情況下,聲明式配置管理將成為一個障礙,您將發現自己需要不斷地調整先前認為是“一勞永逸”的配置文件。

因此,在您選擇採用Kubernetes之前,您需要考慮應用程序的配置需求。只有當您所需要的配置相對通用且靜態時,Kubernetes才是一個不錯的選項。

5、Kubernetes和多雲

Rancher等部分Kubernetes發行版的主要功能之一,是單個Kubernetes部署可以編排多個集群,無論集群位於在不同的公有雲還是私有雲上。這一功能使Kubernetes成為協助控制多雲架構複雜性的優秀工具。

在跨多雲部署容器化應用程序,並且Kubernetes的設置和配置工作很合理時,多雲上的Kubernetes是十分有意義的。

在這一因素中,您需要留意的是,在考慮是否以及何時採用Kubernetes時,應考慮您當前的多雲戰略以及多雲擴展計劃。

結 語

Kubernetes是一個非常棒的工具,在正確設置的情況下,它可以產生巨大的價值。但是,它並沒有達到殺手級應用程序的狀態,因為它無法在所有用戶實例中交付價值。在您被巨大的宣傳攻勢攻陷,並確定您無法離開Kubernetes之前,請清醒地對自己的需求進行評估,明確Kubernetes是否能在真正意義上幫助您更加有效、更加可靠地運行應用程序。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

分類
發燒車訊

基於層級表達的高效網絡搜索方法 | ICLR 2018

論文基於層級表達提出高效的進化算法來進行神經網絡結構搜索,通過層層堆疊來構建強大的卷積結構。論文的搜索方法簡單,從實驗結果看來,達到很不錯的準確率,值得學習

來源:【曉飛的算法工程筆記】 公眾號

論文: Hierarchical Representations for Efficient Architecture Search

  • 論文地址:https://arxiv.org/abs/1711.00436

Introduction

  由於網絡的驗證需要耗費很長的時間,神經網絡結構搜索計算量非常巨大,很多研究通過降低搜索空間的複雜度來提高搜索的效率。論文通過加入分層網絡結構來約束搜索空間,在最初幾層僅使用卷積和池化等簡單操作,逐步到高層將底層的block進行組合搭建,最後將最高層的block堆疊成最終的網絡。由於搜索空間設計夠好,網絡的搜索方法僅用進化算法或隨機搜索足以。
  論文總結如下:

  • 提出對神經網絡結構的層級表達
  • 通過實驗證明搜索空間的設計十分重要,可以降低搜索方法的投入,甚至隨機搜索也可以
  • 提出可擴展的進化搜索方法,對比其它進化搜索方法有更好的結果

Architecture Representations

Flat Architecture Representation

  將神經網絡結構定義為單輸入、單輸出的計算圖,圖中每個節點代表特徵圖,每條有向邊為基本操作(卷積、池化等),所以網絡的表達$(G,o)$包含兩部分:

  1. 一個有效的操作集合$o={o_1,o_2,…}$
  2. 一個鄰接矩陣$G$,用以指定操作的神經網絡圖,$G_{ij}=k$為節點$i$和節點$j$間的操作為$o_k$

  將操作集$o$和鄰接矩陣$G$組合起來就得到網絡的結構

  每個節點$i$的特徵圖$x_i$由其前面的節點$j$通過公式2計算而得,$|G|$是圖中節點數量,$merge$將多個特徵圖合併成一個的操作,這裏直接使用depthwise concatentation,由於element-wise addition要求維度一致,比較不靈活,而且如果融合特徵後接的是$1\times 1$卷積,這就其實類似於做concatienation

Hierarchical Architecture Representation

  層級結構表達的關鍵是找到不同的層級的模版,在構建高層模版時使用低層的模版作為積木(operation)進行構建

  對於$L$層的層級關係,$\ell$層包含$M_{\ell}$個模版,最高層$\ell=L$僅包含一個模版,對應完整的網絡,最低層$\ell=1$是元操作集,定義$o_m{(\ell)}$為$\ell$層的第$m$個模版,為低層模版$o{(\ell)}={o_1^{(\ell -1)},o_2^{(\ell -1)},…,o_1^{(\ell – 1)}}$根據公式3的組合。最終的層級結構表達為$({{G_m{(\ell)}}_{m=1}M}_{\ell=2}L,o{(1)})$,由每層的模版的網絡結構關係和最底層操作定義,如圖1

Primitive Operations

  低層的原操作共六種($\ell=1$,$M_t=6$):

  • 1 × 1 convolution of C channels
  • 3 × 3 depthwise convolution
  • 3 × 3 separable convolution of C channels
  • 3 × 3 max-pooling
  • 3 × 3 average-pooling
  • identity

  使用時,所有元操作為stride=1,以及進行padded來保留分辨率,卷積后都接BN+ReLU,維度固定為$C$。另外每層都有$none$操作,代表節點$i$和節點$j$之間沒有連接

Evolutionary Architecture Search

Mutation

  分層基因的變異包含以下步驟:

  • 採樣一個非原始層$\ell\ge2$作為目標層
  • 在目標層採樣一個模版$m$作為目標模版
  • 在目標模版中採樣一個後繼節點$i$
  • 在目標模版中採樣一個前置節點$j$
  • 隨機替換當前操作$o_k^{(\ell -1)}$為其它操作$o_{k{‘}}{(\ell -1)}$

  對於當前層級只有兩層的,第一步直接將$\ell$設為2,變異可總結為公式4,$\ell$,$m$,$i$,$j$,$k^{‘}$從各自區域的均勻分佈中隨機抽樣得到,上面的突變足夠對模版產生3種修改:

  • 添加邊:$o_k^{(\ell -1)}=none$,$o_{k{‘}}{(\ell -1)}\ne none$
  • 修改存在的邊:$o_k^{(\ell -1)}\ne none$,$o_{k{‘}}{(\ell -1)}\ne none$,$o_k^{(\ell -1)}\ne o_{k{‘}}{(\ell -1)}$
  • 刪除存在的邊:$o_k^{(\ell -1)}\ne none$,$o_{k{‘}}{(\ell -1)}= none$

Initialization

  基因指代完整的網絡,基因的種群初始化包含兩個步驟:

  1. 建立一個不重要的基因,每個模版都使用identity進行連接
  2. 對基因進行大批量的隨機變異來多樣化

  對比以前的研究使用常見的網絡進行基因初始化,這樣的初始化不僅能很好地覆蓋不常見的網絡的搜索空間,還能去除人工初始化帶來的傳統偏向

Search Algorithms

  論文的進化算法基於錦標賽選擇(tournament selection),首先對初始化的種群網絡進行訓練和測試得到分數,然後從種群中隨機獲取5%的基因,表現最好的基因進行突變得到新網絡,在訓練和測試後放入種群中,重複進行上述選取與放回,種群數量不斷增大,最終取種群表現最好的基因
  論文也使用隨機搜索進行實驗,基因種群隨機生成,然後進行訓練和驗證,選取最好的模型,這種方法的主要好處在於能整個種群并行化計算,減少搜索時間

Implementation

  論文使用異步分佈式進行實現,包含一個controller和多個worker,分別負責基因的進化和測試,兩者共享一個內存表格$\mathcal{M}$,記錄基因及其準確率(fitness),還有一個數據隊列$\mathcal{Q}$,包含待測試的基因

  當有worker空餘時,controller使用錦標賽選擇從$\mathcal{M}$中選擇一個基因進行突變,然後放到隊列$\mathcal{Q}$中等待測試

  worker從$\mathcal{Q}$中拿到待測試的基因,測試後放到$\mathcal{M}$中,訓練是從頭開始訓練的,沒有使用權值共享加速

Experiments and Results

Experimental Setup

  在實驗中,沒有對整體網絡進行搜索,而是使用提出的方法進行卷積單元(cell)的搜索,這樣能夠在小網絡上快速進行網絡測試然後遷移到較大的網絡。具體的各結構如圖2,每個cell後面接$2c$維度和$stride=2$的$3\times 3$分離卷積,用於升維和降低分辨率,最後一個cell後面接$c$維度和$stride=1$的$3\times 3$分離卷積

Architecture Search on CIFAR-10

  200卡,初始種群為200,層級$L=3$,每層模版的操作分別為$M_1=6$,$M_2=6$和$M_3=1$,每層($\ell \ge2$)的節點圖分別為$|G{(2)}|=4$和$|G{(3)}|=5$,層2的模版跟一個跟模版輸入維度一樣$1\times 1$的卷積來降維。對於用於對比的不分層的搜索方法,則使用11個節點的計算圖。從圖3來看,論文提出的方法在收斂速度、準確率和參數量上都不錯

  為了進一步展示論文方法的效果,對圖3中間的結果的每輪增量進行了可視化。在P100 GPU上,每個網絡的測試需要花費1小時,進化共7000輪,200張卡共需要1.5天

Architecture Evaluation on CIFAR-10 and ImageNet

CONCLUSION

  論文基於層級表達提出高效的進化算法來進行神經網絡結構搜索,通過層層堆疊來構建強大的卷積結構。論文的搜索方法簡單,從實驗結果看來,200張卡共需要1.5天,達到很不錯的準確率,值得學習

APPENDIX A



如果本文對你有幫助,麻煩點個贊或在看唄~
更多內容請關注 微信公眾號【曉飛的算法工程筆記】

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

分類
發燒車訊

深入理解React:事件機制原理

目錄

  • 序言
  • DOM事件流
    • 事件捕獲階段、處於目標階段、事件冒泡階段
    • addEventListener 方法
  • React 事件概述
  • 事件註冊
    • document 上註冊
    • 回調函數存儲
  • 事件分發
  • 小結
  • 參考

1.序言

React 有一套自己的事件系統,其事件叫做合成事件。為什麼 React 要自定義一套事件系統?React 事件是如何註冊和觸發的?React 事件與原生 DOM 事件有什麼區別?帶着這些問題,讓我們一起來探究 React 事件機制的原理。為了便於理解,此篇分析將盡可能用圖解代替貼 React 源代碼進行解析。

2.DOM事件流

首先,在正式講解 React 事件之前,有必要了解一下 DOM 事件流,其包含三個流程:事件捕獲階段、處於目標階段和事件冒泡階段。

W3C協會早在1988年就開始了DOM標準的制定,W3C DOM標準可以分為 DOM1、DOM2、DOM3 三個版本。

從 DOM2 開始,DOM 的事件傳播分三個階段進行:事件捕獲階段、處於目標階段和事件冒泡階段。

(1)事件捕獲階段、處於目標階段和事件冒泡階段

示例代碼:

<html>
    <body>
        <div id="outer">
	    <p id="inner">Click me!</p>
	</div>
    </body>
</html>

上述代碼,如果點擊 <p>元素,那麼 DOM 事件流如下圖:

(1)事件捕獲階段:事件對象通過目標節點的祖先 Window 傳播到目標的父節點。

(2)處於目標階段:事件對象到達事件目標節點。如果阻止事件冒泡,那麼該事件對象將在此階段完成后停止傳播。

(3)事件冒泡階段:事件對象以相反的順序從目標節點的父項開始傳播,從目標節點的父項開始到 Window 結束。

(2)addEventListener 方法

DOM 的事件流中同時包含了事件捕獲階段和事件冒泡階段,而作為開發者,我們可以選擇事件處理函數在哪一個階段被調用。

addEventListener() 方法用於為特定元素綁定一個事件處理函數。addEventListener 有三個參數:

element.addEventListener(event, function, useCapture)

另外,如果一個元素(element)針對同一個事件類型(event),多次綁定同一個事件處理函數(function),那麼重複的實例會被拋棄。當然如果第三個參數capture值不一致,此時就算重複定義,也不會被拋棄掉。

3.React 事件概述

React 根據W3C 規範來定義自己的事件系統,其事件被稱之為合成事件 (SyntheticEvent)。而其自定義事件系統的動機主要包含以下幾個方面:

(1)抹平不同瀏覽器之間的兼容性差異。最主要的動機。

(2)事件”合成”,即事件自定義。事件合成既可以處理兼容性問題,也可以用來自定義事件(例如 React 的 onChange 事件)。

(3)提供一個抽象跨平台事件機制。類似 VirtualDOM 抽象了跨平台的渲染方式,合成事件(SyntheticEvent)提供一個抽象的跨平台事件機制。

(4)可以做更多優化。例如利用事件委託機制,幾乎所有事件的觸發都代理到了 document,而不是 DOM 節點本身,簡化了 DOM 事件處理邏輯,減少了內存開銷。(React 自身模擬了一套事件冒泡的機制)

(5)可以干預事件的分發。V16引入 Fiber 架構,React 可以通過干預事件的分發以優化用戶的交互體驗。

注:「幾乎」所有事件都代理到了 document,說明有例外,比如audiovideo標籤的一些媒體事件(如 onplay、onpause 等),是 document 所不具有,這些事件只能夠在這些標籤上進行事件進行代理,但依舊用統一的入口分發函數(dispatchEvent)進行綁定。

4.事件註冊

React 的事件註冊過程主要做了兩件事:document 上註冊、存儲事件回調。

(1)document 上註冊

在 React 組件掛載階段,根據組件內的聲明的事件類型(onclick、onchange 等),在 document 上註冊事件(使用addEventListener),並指定統一的回調函數 dispatchEvent。換句話說,document 上不管註冊的是什麼事件,都具有統一的回調函數 dispatchEvent。也正是因為這一事件委託機制,具有同樣的回調函數 dispatchEvent,所以對於同一種事件類型,不論在 document 上註冊了幾次,最終也只會保留一個有效實例,這能減少內存開銷。

示例代碼:

function TestComponent() {
  handleFatherClick=()=>{
		// ...
  }

  handleChildClick=()=>{
		// ...
  }

  return <div className="father" onClick={this.handleFatherClick}>
	<div className="child" onClick={this.handleChildClick}>child </div>
  </div>
}

上述代碼中,事件類型都是onclick,由於 React 的事件委託機制,會指定統一的回調函數 dispatchEvent,所以最終只會在 document 上保留一個 click 事件,類似document.addEventListener('click', dispatchEvent),從這裏也可以看出 React 的事件是在 DOM 事件流的冒泡階段被觸發執行。

(2)存儲事件回調

React 為了在觸發事件時可以查找到對應的回調去執行,會把組件內的所有事件統一地存放到一個對象中(listenerBank)。而存儲方式如上圖,首先會根據事件類型分類存儲,例如 click 事件相關的統一存儲在一個對象中,回調函數的存儲採用鍵值對(key/value)的方式存儲在對象中,key 是組件的唯一標識 id,value 對應的就是事件的回調函數。

React 的事件註冊的關鍵步驟如下圖:

5.事件分發

事件分發也就是事件觸發。React 的事件觸發只會發生在 DOM 事件流的冒泡階段,因為在 document 上註冊時就默認是在冒泡階段被觸發執行。

其大致流程如下:

  1. 觸發事件,開始 DOM 事件流,先後經過三個階段:事件捕獲階段、處於目標階段和事件冒泡階段
  2. 當事件冒泡到 document 時,觸發統一的事件分發函數 ReactEventListener.dispatchEvent
  3. 根據原生事件對象(nativeEvent)找到當前節點(即事件觸發節點)對應的 ReactDOMComponent 對象
  4. 事件的合成
    1. 根據當前事件類型生成對應的合成對象
    2. 封裝原生事件對象和冒泡機制
    3. 查找當前元素以及它所有父級
    4. 在 listenerBank 中查找事件回調函數併合成到 events 中
  5. 批量執行合成事件(events)內的回調函數
  6. 如果沒有阻止冒泡,會將繼續進行 DOM 事件流的冒泡(從 document 到 window),否則結束事件觸發

注:上圖中阻止冒泡是指調用stopImmediatePropagation 方法阻止冒泡,如果是調用stopPropagation阻止冒泡,document 上如果還註冊了同類型其他的事件,也將會被觸發執行,但會正常阻斷 window 上事件觸發。了解兩者之間的詳細區別

示例代碼:

class TestComponent extends React.Component {

  componentDidMount() {
    this.parent.addEventListener('click', (e) => {
      console.log('dom parent');
    })
    this.child.addEventListener('click', (e) => {
      console.log('dom child');
    })
    document.addEventListener('click', (e) => {
      console.log('document');
    })
    document.body.addEventListener('click', (e) => {
      console.log('body');
    })
    window.addEventListener('click', (e) => {
      console.log('window');
    })
  }

  childClick = (e) => {
    console.log('react child');
  }

  parentClick = (e) => {
    console.log('react parent');
  }

  render() {
    return (
      <div class='parent' onClick={this.parentClick} ref={ref => this.parent = ref}>
        <div class='child' onClick={this.childClick} ref={ref => this.child = ref}>
          Click me!
        </div>
      </div>)
  }
}

點擊 child div 時,其輸出如下:

在 DOM 事件流的冒泡階段先後經歷的元素:child <div> -> parent <div> -> <body> -> <html> -> document -> window,因此上面的輸出符合預期。

6.小結

React 合成事件和原生 DOM 事件的主要區別:

(1)React 組件上聲明的事件沒有綁定在 React 組件對應的原生 DOM 節點上。

(2)React 利用事件委託機制,將幾乎所有事件的觸發代理(delegate)在 document 節點上,事件對象(event)是合成對象(SyntheticEvent),不是原生事件對象,但通過 nativeEvent 屬性訪問原生事件對象。

(3)由於 React 的事件委託機制,React 組件對應的原生 DOM 節點上的事件觸發時機總是在 React 組件上的事件之前。

7.參考

javascript中DOM0,DOM2,DOM3級事件模型解析

Event dispatch and DOM event flow

EventTarget.addEventListener() – Web API 接口參考| MDN

合成事件

談談React事件機制和未來(react-events)

React源碼解讀系列 – 事件機制

一文吃透 react 事件機制原理

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

分類
發燒車訊

Python異常處理

Python中的異常處理

異常分類

  程序中難免出現錯誤,總共可分為兩種。

 

  1.邏輯錯誤

  2.語法錯誤

 

  對於剛接觸編程的人來說,這兩個錯誤都會經常去犯,但是隨着經驗慢慢的積累,語法錯誤的情況會越來越少反而邏輯錯誤的情況會越來越多(因為工程量巨大)。不論多麼老道的程序員都不可避免出現這兩種錯誤。

 

異常的三大信息

  異常其實就是程序運行時發生錯誤的信號,我們寫代碼的過程中不可避免也最害怕的就是出現異常,然而當程序拋出異常時實際上會分為三部分,即三大信息。

 

 

常用的異常類

  在Python中一切皆對象,異常本身也是由一個類生成的,NameError其實本身就是一個異常類,其他諸如此類的異常類還有很多。

Python中常見的異常類 
AttributeError 試圖訪問一個對象沒有的屬性,比如foo.x,但是foo並沒有屬性x
IOError 輸入/輸出異常;基本上是無法打開文件
ImportError 無法引入模塊或包;基本上是路徑問題或名稱錯誤
IndentationError 語法錯誤(的子類) ;代碼沒有正確對齊
IndexError 下標索引超出序列邊界,比如當x只有三個元素,卻試圖訪問x[5]
KeyError 試圖訪問字典里不存在的鍵
KeyboardInterrupt Ctrl+C被按下
NameError 試圖使用一個還未被賦予對象的變量
SyntaxError Python代碼非法,代碼不能編譯(其實就是語法錯誤,寫錯了)
TypeError 傳入對象類型與要求的不符合
UnboundLocalError 試圖訪問一個還未被設置的局部變量,基本上是由於另有一個同名的 全局變量,導致你以為正在訪問它
ValueError 傳入一個調用者不期望的值,即使值的類型是正確的

 

異常處理

  我們可以來用某些方法進行異常捕捉,當出現異常時我們希望代碼以另一種邏輯運行,使得我們的程序更加健壯,這個就叫做異常處理。異常處理是非常重要的,本身也並不複雜,千萬不可馬虎大意。

  但是切記不可濫用異常處理,這會使得你的代碼可讀性變差。

 

if else處理異常

  ifelse本身就具有處理異常的功能,他們更多的是在我們能預測到可能出現的範圍內進行規避異常,對於我們不能預測的異常來說就顯得不是那麼的好用。如下:

# ==== if else 處理異常 ====

while 1:
    select = input("請輸入数字0進行關機:").strip()
    if select.isdigit():  # 我們可以防止用戶輸入非数字的字符
        if select == "0":
            print("正在關機...")
            break
    print("輸入有誤,請重新輸入")

# ==== 執行結果 ====

"""
請輸入数字0進行關機:關機
輸入有誤,請重新輸入
請輸入数字0進行關機:關機啊
輸入有誤,請重新輸入
請輸入数字0進行關機:0
正在關機...
"""

  這種異常處理機制雖然非常簡單,但是並不靈活,我們可以使用更加簡單的方式來處理他們。

 

try except處理異常

 

  try:代表要檢測可能出現異常的代碼塊

  except:當異常出現后的處理情況

 

  執行流程:

    try中檢測的代碼塊 —> 如果有異常 —> 執行except代碼塊 —> 執行正常邏輯代碼 —> 程序結束

    try中檢測的代碼塊 —> 如果沒有異常 —> 執行完畢try中的代碼塊 —> 執行正常邏輯代碼 —> 程序結束

 

# ====  try except 執行流程 有異常的情況 ====

li = [1,2,3]

try:
    print("開始執行我try了...")
    print(li[10])  # 出錯點...
    print("繼續執行我try...")
except IndexError as e:
    print("有異常執行我except...")

print("正常邏輯代碼...")

# ==== 執行結果 ====

"""
開始執行我try了...
有異常執行我except...
正常邏輯代碼...
""" 
# ====  try except 執行流程 無異常的情況 ====

li = [1,2,3]

try:
    print("開始執行我try了...")
    print(li[2])
    print("繼續執行我try...")
except IndexError as e:
    print("有異常執行我except...")

print("正常邏輯代碼...")

# ==== 執行結果 ====

"""
開始執行我try了...
3
繼續執行我try...
正常邏輯代碼...
""" 
# ==== try except 處理異常 ====

while 1:
    try:  # try檢測可能出錯的語句,一旦出錯立馬跳轉到except語句塊執行代碼。
        select = int(input("請輸入数字0進行關機:").strip())
        if select == 0:
            print("正在關機...")
            break
        print("輸入有誤,請重新輸入...")
    except ValueError as e:  # 當執行完except的代碼塊后,程序運行結束,其中e代表的是異常信息。
        print("錯誤信息是:",e)
        print("輸入有誤,請重新輸入")

# ==== 執行結果 ====

"""
請輸入数字0進行關機:1
輸入有誤,請重新輸入...
請輸入数字0進行關機:tt
錯誤信息是: invalid literal for int() with base 10: 'tt'
輸入有誤,請重新輸入
請輸入数字0進行關機:0
正在關機...
"""

 

多段except捕捉多異常

  我們可以使用try和多段except的語法來檢測某一代碼塊,可以更加方便的應對更多類型的錯誤,Ps不常用:

# ==== 多段 except 捕捉多異常 ====

while 1:

    li = [1,2,3,4]
    dic = {"name":"Yunya","age":18}

    li_index = input("請輸入索引:")
    dic_key = input("請輸入鍵的名稱:")

    if li_index.isdigit():
        li_index = int(li_index)

    try:
        print(li[li_index])
        print(dic[dic_key])
    except IndexError as e1:  # 注意,先拋出的錯誤會直接跳到其處理的except代碼塊,而try下面的語句將不會被執行。
        print("索引出錯啦!")
    except KeyError as e2:
        print("鍵出錯啦!")

# ==== 執行結果 ====

"""
請輸入索引:10
請輸入鍵的名稱:gender
索引出錯啦!
請輸入索引:2
請輸入鍵的名稱:gender
3
鍵出錯啦!
"""

 

元組捕捉多異常

  使用多段except捕捉多異常會顯得特別麻煩,這個時候我們可以使用(異常類1,異常類2)來捕捉多異常,但是需要注意的是,對比多段except捕捉多異常來說,這種方式的處理邏輯會顯得較為複雜(因為只有一段處理邏輯),如下:

# ====  元組捕捉多異常 ====

while 1:

    li = [1,2,3,4]
    dic = {"name":"Yunya","age":18}

    li_index = input("請輸入索引:")
    dic_key = input("請輸入鍵的名稱:")

    if li_index.isdigit():
        li_index = int(li_index)

    try:
        print(li[li_index])
        print(dic[dic_key])
    except (IndexError,KeyError) as e:  # 使用()的方式可以同時捕捉很多異常。
        print("出錯啦!")

# ==== 執行結果 ====

"""
請輸入索引:10
請輸入鍵的名稱:gender
出錯啦!
請輸入索引:2
請輸入鍵的名稱:gender
3
出錯啦!
"""

  可以看到,不管是那種錯誤都只有一種應對策略,如果我們想要多種應對策略就只能寫if判斷來判斷異常類型再做處理。所以就會顯得很麻煩,如下:

# ====  元組捕捉多異常 ====

while 1:

    li = [1,2,3,4]
    dic = {"name":"Yunya","age":18}

    li_index = input("請輸入索引:")
    dic_key = input("請輸入鍵的名稱:")

    if li_index.isdigit():
        li_index = int(li_index)

    try:
        print(li[li_index])
        print(dic[dic_key])
    except (IndexError,KeyError) as e:
        # 判斷異常類型再做出相應的對應策略
        if isinstance(e,IndexError):
            print("索引出錯啦!")
        elif isinstance(e,KeyError):
            print("鍵出錯啦!")

# ==== 執行結果 ====

"""
請輸入索引:10
請輸入鍵的名稱:gender
索引出錯啦!
請輸入索引:2
請輸入鍵的名稱:gender
3
鍵出錯啦!
"""

 

萬能異常Exception

  我們可以捕捉Exception類引發的異常,它是所有異常類的基類。(Exception類的父類則是BaseException類,而BaseException的父類則是object類)

# ====  萬能異常Exception ====

while 1:

    li = [1,2,3,4]
    dic = {"name":"Yunya","age":18}

    li_index = input("請輸入索引:")
    dic_key = input("請輸入鍵的名稱:")

    if li_index.isdigit():
        li_index = int(li_index)

    try:
        print(li[li_index])
        print(dic[dic_key])
    except Exception as e:  #使用 Exception來捕捉所有異常。
        # 判斷異常類型再做出相應的對應策略
        if isinstance(e,IndexError):
            print("索引出錯啦!")
        elif isinstance(e,KeyError):
            print("鍵出錯啦!")

# ==== 執行結果 ====

"""
請輸入索引:10
請輸入鍵的名稱:gender
索引出錯啦!
請輸入索引:2
請輸入鍵的名稱:gender
3
鍵出錯啦!
"""

 

 

try except else聯用

  這種玩法比較少,else代表沒有異常發生的情況下執行的代碼,執行順序如下:

 

  try中檢測的代碼塊 —> 如果有異常 —> 終止try中的代碼塊繼續執行 —> 執行except代碼塊 —> 執行正常邏輯代碼 —> 程序結束

  try中檢測的代碼塊 —> 如果沒有異常 —> 執行完畢try中的代碼塊 —> 執行else代碼塊 —> 執行正常邏輯代碼 —> 程序結束

 

# ====  try except else聯用 有異常的情況====

li = [1,2,3]

try:
    print("開始執行我try了...")
    print(li[10])  # 出錯點...
    print("繼續執行我try...")
except IndexError as e:
    print("有異常執行我except...")
else:
    print("沒有異常執行我else...")
print("正常邏輯代碼...")

# ==== 執行結果 ====

"""
開始執行我try了...
有異常執行我except...
正常邏輯代碼...
"""
# ====  try except else聯用 無異常的情況====

li = [1,2,3]

try:
    print("開始執行我try了...")
    print(li[2])
    print("繼續執行我try...")
except IndexError as e:
    print("有異常執行我except...")
else:
    print("沒有異常執行我else...")
print("正常邏輯代碼...")

# ==== 執行結果 ====

"""
開始執行我try了...
3
繼續執行我try...
沒有異常執行我else...
正常邏輯代碼...
"""

 

try except finally聯用

  finally代表不論拋異常與否都會執行,因此常被用作關閉系統資源的操作,關於try,except,else,finally他們的優先級如下:

 

  有異常的情況下:

    try代碼塊

    終止try代碼塊繼續執行

    except代碼塊

    finally代碼塊

    正常邏輯代碼

 

  無異常的情況下:

    try代碼塊

    else代碼塊

    finally代碼塊

    正常邏輯代碼

 

# ====  try except else finally 執行流程 有異常的情況 ====

li = [1,2,3]

try:
    print("開始執行我try了...")
    print(li[10])  # 出錯點...
    print("繼續執行我try...")
except IndexError as e:
    print("有異常執行我except...")
else:
    print("沒有異常執行我else...")
finally:
    print("不管有沒有異常都執行我finally...")

print("正常邏輯代碼...")

# ==== 執行結果 ====

"""
開始執行我try了...
有異常執行我except...
不管有沒有異常都執行我finally...
正常邏輯代碼...
"""
# ====  try except else finally 執行流程 無異常的情況 ====

li = [1,2,3]

try:
    print("開始執行我try了...")
    print(li[2])
    print("繼續執行我try...")
except IndexError as e:
    print("有異常執行我except...")
else:
    print("沒有異常執行我else...")
finally:
    print("不管有沒有異常都執行我finally...")

print("正常邏輯代碼...")

# ==== 執行結果 ====

"""
開始執行我try了...
3
繼續執行我try...
沒有異常執行我else...
不管有沒有異常都執行我finally...
正常邏輯代碼...
"""

 

自定義異常

raise主動拋出異常

  在某些時候我們可能需要主動的去阻止程序的運行,主動的拋出一個異常。可以使用raise來進行操作。這個是一種非常常用的手段。

# ====  raise使用方法  ====

print("----1----")
print("----2----")
print("----3----")
raise Exception("我也不知道是什麼類型的異常...")
print("----4----")
print("----5----")
print("----6----")

# ==== 執行結果 ====

"""
----1----
----2----
----3----
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/learn/元類編程.py", line 6, in <module>
    raise Exception("我也不知道是什麼類型的異常...")
Exception: 我也不知道是什麼類型的異常...

Process finished with exit code 1
"""

 

自定義異常類

  前面已經說過一切皆對象,異常也來自一個對象。因此我們也可以自己來定製一個對象。注意,自定義異常類必須繼承BaseException類。

# ====  自定義異常類  ====

class MyError(BaseException):
    pass

raise MyError("我的異常")



# ==== 執行結果 ====

"""
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/learn/元類編程.py", line 6, in <module>
    raise MyError("我的異常")
__main__.MyError: 我的異常
"""

 

擴展:斷言assert

  斷言是一個十分裝逼的使用,假設多個函數進行計算,我們已經有了預期的結果只是在做一個算法的設計。如果函數的最後的結果不是我們本來預期的結果那麼寧願讓他停止運行也不要讓錯誤繼續擴大,在這種情況下就可以使用斷言操作,使用斷言會拋出一個AssertionError類的異常。

# ====  斷言assert  ====

def calculate():
    """假設在做非常複雜的運算"""
    return 3+2*5

res = calculate()

assert res == 25  # AssertionError

print("算法測試通過!你真的太厲害了")

# ==== 執行結果 ====

"""
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/learn/元類編程.py", line 8, in <module>
    assert res == 25
AssertionError
"""

 

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

分類
發燒車訊

在樹莓派上讀取土壤濕度傳感器讀數-Python代碼實現及常見問題(全面簡單易懂)

本篇文章簡單介紹了如何在樹莓派上配置土壤濕度傳感器以讀取土壤濕度及代碼實現。

主要包含有以下4個模塊:

一、土壤濕度傳感器常見類型及介紹

二、實驗所需設備

三、設備連線方式與Python代碼實現

四、常見問題及注意事項

需要哪個模塊的內容直接跳轉去看即可~

 

一、土壤濕度傳感器常見類型及介紹

 土壤濕度傳感器,又名土壤水分傳感器、土壤墒情傳感器、土壤含水量傳感器等。顧名思義,主要用來測量土壤相對含水量,做土壤墒情監測。在智能農業,農業灌溉和林業防護等領域極廣。該傳感器價格低廉,如果想在家製作一個簡易的智能自動化作物灌溉系統,有了它,再加上溫濕度傳感器、樹莓派/Arduino就可以輕鬆完成。

常見的土壤傳感器分為兩類,電阻型和電容型土壤濕度傳感器。但原理大同小異,都是測量土壤中水分的體積含量,並以電壓表示水分值。

(一)電阻型土壤濕度傳感器

常見的有傳感器型號有YL-69(下圖左)和FC-28(下圖右)。這是一種低技術含量的傳感器這類傳感器由兩部分組成,帶探針的傳感器,A to D(模擬信號轉数字信號)电子板(校準靈敏度主板),兩者用母對母杜邦線連接。                

            它根據土壤的介電常數(土壤的導電能力)來估計土壤體積水含量,工作時,使用兩個探針讓電流通過土壤,然後讀取電阻來獲得濕度水平。水分越多,土壤導電越容易(電阻越小),而土壤乾燥,導電越差(電阻越大)。土壤中的濕度是一個連續變化的一系列值,為模擬信號,使用A to D接線板之後可以將從環境中得來的模擬信號轉成数字信號,在該板上有兩個指示燈,PWR-LED和DO-LED,前者檢測是否插好電源,如果電源的正負極連接正確,則會亮起,如下圖(左)所示。在這裏我使用的是YL-69型號,燈為綠色,也有一些廠商生產的傳感器指示燈為紅色。該傳感器在輸出数字信號時,可以使用改錐調節板上的電位計(藍色中間有十字架的部位)來提前設定閾值大小,一旦土壤濕度達到或大於閾值,則DO-LED亮起,如下圖(中)所示。

 电子板從左到右的標記為AO、DO、GND、VCC,如下圖(右)所示。AO和DO為信號引腳,如果需要模擬信號,則連接AO,輸出的模擬值是介於所提供的電壓值到0V間的變化的電壓值,如果輸出0V,則代表土壤導電性不好,即水分含量低,可以用這個電壓值來估計土壤濕度。如果需要数字信號輸出則連接DO,簡單的輸出0和1,可直接通過信號燈判斷土壤中水分是否低於閾值,高於則“開”,低則“關”。GND表示接地,VCC連接電源,但在這個項目中,我們將單獨利用模擬輸出。

               

 該傳感器的優點是價格低廉,且有指示燈,觀察方便,但由於土壤環境是酸性的(acidic),隨着時間的推移,土壤里的化學物質會使得探針氧化(oxidize)導致測量不準確,所以需要時不時的進行更換以保證測量的準確性。

(二)電容型傳感器

相比較前一類型的傳感器,這類傳感器就顯得“光禿禿”了,只有一個組件,沒有指示燈,且只能輸出模擬信號。它區別於電阻傳感器,利用電容感應原理來檢測土壤濕度,避免了電阻式傳感器極易被腐蝕的問題,生命周期較長,缺點是不能用指示燈判斷傳感器是否正常工作,它同時只提供模擬信號。如圖設計DF Robot的一款傳感器,內置穩壓芯片,支持3.3V-5.5V寬電壓工作。DFRobot-Gravity接口具有兼容性,可直接和Gravity IO擴展板相連接。輸出電壓為0-3VDC。

在自動化澆灌系統中,濕度傳感器用於測量土壤中水分,可以提前預設一個閾值,一旦土壤中的水分低於此閾值,則啟動連接着蓄水池(家用拿礦泉水瓶裝滿水替代即可)的水泵噴水,等到水分值超過預設值,則水泵暫停工作。

 

二、實驗所需設備

樹莓派 3 b+/樹莓 4 b/樹莓派Zero W

MCP3008

麵包板

跳線(公對母,母對母,最好多準備一些)

土壤傳感器(電容式、電阻式均可)

一杯水(可以將傳感器放入水中來觀察濕度讀數的變化,若沒有條件也可直接用手握住傳感器的探針)

MCP3008

由於樹莓派沒有模擬信號引腳,所以沒有辦法直接輸出模擬信號數值,此時我們需要使用MCP3008集成電路。

MCP3008 IC(Integrated Circuit)是一個8通道,10位的具有SPI串行接口的A / D轉換器(模擬-数字轉換器),共有16個引腳,可以用來解決模擬引腳問題(MCP3004也是ADC,不過為4路,體型更小)MCP3008使用SPI總線協議從樹莓派接收模擬輸入值。它有8個模擬輸入(ch0-ch7),另外一列的8個引腳中有4個電源和地引腳和4個連接樹莓派的引腳,它產生範圍為0-1023的輸出值(注意:0表示0V, 1023表示3.3V)。

三、設備連線方式與代碼實現

在此實驗中,主要介紹和使用的FC-28型號土壤濕度傳感器,但其它型號的傳感器使用,連線均與此相同。

(一)設備連線方式

1.電路圖及說明

MCP3008共有16個引腳,其中8個用於記錄模擬輸入值,位於CH0-CH7(引腳1-8),4個通信引腳通過SPI協議方法與樹莓派通信,還有2個電源引腳,2個接地引腳。

(二)代碼實現

1. 啟用樹莓派的SPI接口

樹莓派的SPI接口與SSH、VNC服務相同,是默認關閉的,需要我們在配置中打開此服務才可以使用。

按照以下步驟啟動終端並輸入以下命令:

(1)打開樹莓派配置選項

sudo raspi-config

(2)導航到Interface選項,啟用SPI接口。

(3)重啟樹莓派

reboot

2. 安裝spidev庫

光啟用SPI接口,但是樹莓派還是無法讀取傳感器傳過來的值,spidev庫將幫助通過SPI接口讀取傳感器值。

 使用以下命令安裝spidev庫:

sudo apt-get install git python-dev
git clone git://github.com/doceme/py-spidev
cd py-spidev/
sudo python setup.py install

3. 安裝numpy庫

我們從傳感器獲得的值還是電壓值而非土壤濕度值,土壤濕度需要使用百分比的形式體現,為了將輸出值轉換為百分比,還需要使用numpy庫。我們從MCP3008 IC接收到的輸出值是在前面提到的0-0123範圍內的數值,仍需要將把這些值映射到0-100,以得到一個百分比。

使用以下命令安裝numpy模塊:

sudo apt-get install python-numpy

4. Python代碼

# Importing modules
import spidev # To communicate with SPI devices
from numpy import interp  # To scale values
from time import sleep  # To add delay


# Start SPI connection
spi = spidev.SpiDev() # Created an object
spi.open(0,0) 


# Read MCP3008 data
def analogInput(channel):
  spi.max_speed_hz = 1350000
  adc = spi.xfer2([1,(8+channel)<<4,0])
  data = ((adc[1]&3) << 8) + adc[2]
  return data


while True:
  output = analogInput(0) # Reading from CH0
  output = interp(output, [0, 1023], [100, 0])
  output = int(output)
  print("Moisture:", output)
  sleep(0.1)

當從土壤濕度傳感器讀取模擬輸出值時,它以百分比測量濕度,使用來自numpy庫的特定interp模塊進行映射得到從0-100的值。

四、常見問題及注意事項

(一)常見問題

1. 持續輸出0或100,無論探針是否放入水中均沒有變化

2. 沒有操作探針,但讀數呈有規律地變化

針對以上出現有以下幾種解決方案

1.檢查樹莓派的SPI服務有無正確打開

2.先檢查線有沒有接穩,查看是不是線的連接順序(傳感器的正負極有沒有接反,與樹莓派的連線有沒有串行)有誤

3. 線是否有生鏽或損壞(之前第一次做實驗時,各種調試都出現不了正確結果,後來才發現是有幾根線生鏽了所以不通)

(二)注意事項 

1. 盡量使用長線,便於看清連線位置

2. 盡量不要使用拼接線(一根公對公,一根母對母拼接成公對母的),這樣需要照顧的線更多,也更容易出紕漏

3. 一定一定要有耐心,出現問題后按照順序逐一排查。因為涉及到的連線較多,對硬件小白來說,很容易眼花繚亂想放棄,但是太簡單的東西誰都能做,能攻破學習或者生活中一個個難關的人才能有所成長啊~

如果你在配置土壤濕度傳感器時或使用樹莓派時遇見了什麼問題,歡迎在評論區寫下,看到了會及時答覆。期待與大家一起學習。

文字及圖片部分來源:https://maker.pro/raspberry-pi/tutorial/interfacing-soil-moisture-sensor-with-raspberry-pi

轉發請標明來源。祝大家學派happy!

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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