通過與C++程序對比,徹底搞清楚JAVA的對象拷貝

目錄

  • 一、背景
  • 二、JAVA對象拷貝的實現
    • 2.1 淺拷貝
    • 2.2 深拷貝的實現方法一
    • 2.3 深拷貝的實現方法二
      • 2.3.1 C++拷貝構造函數
      • 2.3.2 C++源碼
      • 2.3.3 JAVA通過拷貝構造方法實現深拷貝
  • 四、總結

一、背景

JAVA編程中的對象一般都是通過new進行創建的,新創建的對象通常是初始化的狀態,但當這個對象某些屬性產生變更,且要求用一個對象副本來保存當前對象的“狀態”,這時候就需要用到對象拷貝的功能,以便封裝對象之間的快速克隆。

二、JAVA對象拷貝的實現

2.1 淺拷貝
  • 被複制的類需要實現Clonenable接口;
  • 覆蓋clone()方法,調用super.clone()方法得到需要的複製對象;
  • 淺拷貝對基本類型(boolean,char,byte,short,float,double.long)能完成自身的複製,但對於引用類型只對引用地址進行拷貝。
    — 下面我們用一個實例進行驗證:
/**
 * 單隻牌
 *
 * @author zhuhuix
 * @date 2020-06-10
 */
public class Card implements Comparable, Serializable,Cloneable {

    // 花色
    private String color = "";
    //数字
    private String number = "";

    public Card() {
    }

    public Card(String color, String number) {
        this.color = color;
        this.number = number;
    }

    public String getColor() {
        return this.color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getNumber() {
        return this.number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return this.color + this.number;
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof Card) {
            int thisColorIndex = Constant.COLORS.indexOf(this.getColor());
            int anotherColorIndex = Constant.COLORS.indexOf(((Card) o).getColor());
            int thisNumberIndex = Constant.NUMBERS.indexOf(this.getNumber());
            int anotherNumberIndex = Constant.NUMBERS.indexOf(((Card) o).getNumber());

            // 大小王之間相互比較: 大王大於小王
            if ("JOKER".equals(this.color) && "JOKER".equals(((Card) o).getColor())) {
                    return thisColorIndex > anotherColorIndex ? 1 : -1;
            }

            // 大小王與数字牌之間相互比較:大小王大於数字牌
            if ("JOKER".equals(this.color) && !"JOKER".equals(((Card) o).getColor())) {
                return 1;
            }
            if (!"JOKER".equals(this.color) && "JOKER".equals(((Card) o).getColor())) {
                return -1;
            }

            // 数字牌之間相互比較: 数字不相等,数字大則牌面大;数字相等 ,花色大則牌面大
            if (thisNumberIndex == anotherNumberIndex) {
                return thisColorIndex > anotherColorIndex ? 1 : -1;
            } else {
                return thisNumberIndex > anotherNumberIndex ? 1 : -1;
            }

        } else {
            return -1;
        }
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
/**
 * 撲克牌常量定義
 *
 * @author zhuhuix
 * @date 2020-06-10
 */
public class Constant {

    // 紙牌花色:黑桃,紅心,梅花,方塊
    final static List<String> COLORS = new ArrayList<>(
            Arrays.asList(new String[]{"", "", "", ""}));
    // 紙牌数字
    final static List<String> NUMBERS = new ArrayList<>(
            Arrays.asList(new String[]{"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"}));
    // 大王小王
    final static List<String> JOKER = new ArrayList<>(
            Arrays.asList(new String[]{"小王","大王"}));
}

/**
 * 整副副撲克牌
 *
 * @author zhuhuix
 * @date 2020-06-10
 */
public class Poker implements Cloneable, Serializable {

    private List<Card> cards;

    public Poker() {
        List<Card> cardList = new ArrayList<>();
        // 按花色與数字組合生成52張撲克牌
        for (int i = 0; i < Constant.COLORS.size(); i++) {
            for (int j = 0; j < Constant.NUMBERS.size(); j++) {
                cardList.add(new Card(Constant.COLORS.get(i), Constant.NUMBERS.get(j)));
            }
        }
        // 生成大小王
        for (int i = 0; i < Constant.JOKER.size(); i++) {
            cardList.add(new Card("JOKER", Constant.JOKER.get(i)));
        }

        this.cards = cardList;
    }

   
    // 從整副撲克牌中抽走大小王
    public void removeJoker() {
        Iterator<Card> iterator = this.cards.iterator();
        while (iterator.hasNext()) {
            Card cardJoker = iterator.next();
            if (cardJoker.getColor() == "JOKER") {
                iterator.remove();
            }
        }
    }

    public List<Card> getCards() {
        return cards;
    }

    public void setCards(List<Card> cards) {
        this.cards = cards;
    }

    public Integer getCardCount() {
        return this.cards.size();
    }

    @Override
    public String toString() {
        StringBuilder poker = new StringBuilder("[");
        Iterator<Card> iterator = this.cards.iterator();
        while (iterator.hasNext()) {
            poker.append(iterator.next().toString() + ",");
        }
        poker.setCharAt(poker.length() - 1, ']');
        return poker.toString();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

/**
 * 測試程序
 *
 * @author zhuhuix
 * @date 2020-6-10
 */
public class PlayDemo {

    public static void main(String[] args) throws CloneNotSupportedException {

        // 生成一副撲克牌並洗好牌
        Poker poker1 = new Poker();
        System.out.println("新建:第一副牌共 "+poker1.getCardCount()+" 張:"+poker1.toString());

        Poker poker2= (Poker) poker1.clone();
        System.out.println("第一副牌拷頁生成第二副牌,共 "+poker2.getCardCount()+" 張:"+poker2.toString());

        poker1.removeJoker();

        System.out.println("====第一副牌抽走大小王后====");
        System.out.println("第一副牌還有 "+poker1.getCardCount()+" 張:"+poker1.toString());
        System.out.println("第二副牌還有 "+poker2.getCardCount()+" 張:"+poker2.toString());

    }

}
  • 運行結果:
    在第一副的對象中抽走了“大小王”,克隆的第二副的對象的“大小王”竟然也被“抽走了”
2.2 深拷貝的實現方法一
  • 被複制的類需要實現Clonenable接口;
  • 覆蓋clone()方法,自主實現引用類型成員的拷貝複製。
    — 我們只要改寫一下Poker類中的clone方法,讓引用類型成員實現複製:
/**
 * 整副副撲克牌--自主實現引用變量的複製
 *
 * @author zhuhuix
 * @date 2020-06-10
 */
public class Poker implements Cloneable, Serializable {

    private List<Card> cards;

    public Poker() {
        List<Card> cardList = new ArrayList<>();
        // 按花色與数字組合生成52張撲克牌
        for (int i = 0; i < Constant.COLORS.size(); i++) {
            for (int j = 0; j < Constant.NUMBERS.size(); j++) {
                cardList.add(new Card(Constant.COLORS.get(i), Constant.NUMBERS.get(j)));
            }
        }
        // 生成大小王
        for (int i = 0; i < Constant.JOKER.size(); i++) {
            cardList.add(new Card("JOKER", Constant.JOKER.get(i)));
        }

        this.cards = cardList;
    }

    // 從整副撲克牌中抽走大小王
    public void removeJoker() {
        Iterator<Card> iterator = this.cards.iterator();
        while (iterator.hasNext()) {
            Card cardJoker = iterator.next();
            if (cardJoker.getColor() == "JOKER") {
                iterator.remove();
            }
        }
    }

    public List<Card> getCards() {
        return cards;
    }

    public void setCards(List<Card> cards) {
        this.cards = cards;
    }

    public Integer getCardCount() {
        return this.cards.size();
    }

    @Override
    public String toString() {
        StringBuilder poker = new StringBuilder("[");
        Iterator<Card> iterator = this.cards.iterator();
        while (iterator.hasNext()) {
            poker.append(iterator.next().toString() + ",");
        }
        poker.setCharAt(poker.length() - 1, ']');
        return poker.toString();
    }

	// 遍歷原始對象的集合,對生成的對象進行集合複製
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Poker newPoker = (Poker)super.clone();
        newPoker.cards = new ArrayList<>();
        newPoker.cards.addAll(this.cards);
        return newPoker;
    }
}

  • 輸出結果:
    — 通過自主實現引用類型的複製,原對象與對象的拷貝的引用類型成員地址不再關聯
2.3 深拷貝的實現方法二
  • 在用第二種方式實現JAVA深拷貝之前,我們首先對C++程序的對象拷貝做個了解:
2.3.1 C++拷貝構造函數

C++拷貝構造函數,它只有一個參數,參數類型是本類的引用,且一般用const修飾

2.3.2 C++源碼
// 單隻牌的類定義
// Created by Administrator on 2020-06-10.
//

#ifndef _CARD_H
#define _CARD_H

#include <string>

using namespace std;

class Card {
private :
    string color;
    string number;
public:
    Card();

    Card(const string &color, const string &number);

    const string &getColor() const;

    void setColor(const string &color);

    const string &getNumber() const;

    void setNumber(const string &number);

    string toString();

};


#endif //_CARD_H

// 單隻牌類的實現
// Created by Administrator on 2020-06-10.
//

#include "card.h"

Card::Card(){}

Card::Card(const string &color, const string &number) : color(color), number(number) {}

const string &Card::getColor() const {
    return color;
}

void Card::setColor(const string &color) {
    Card::color = color;
}

const string &Card::getNumber() const {
    return number;
}

void Card::setNumber(const string &number) {
    Card::number = number;
}


string Card::toString() {
    return getColor()+getNumber();
}




// 撲克牌類的定義
// Created by Administrator on 2020-06-10.
//

#ifndef _POKER_H
#define _POKER_H

#include <vector>
#include "card.h"

using namespace std;

const int COLOR_COUNT=4;
const int NUMBER_COUNT=13;
const int JOKER_COUNT=2;

const string COLORS[COLOR_COUNT] = {"", "", "", ""};
const string NUMBERS[NUMBER_COUNT]={"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
const string JOKER[JOKER_COUNT] ={"小王","大王"};

class Poker {
private:
    vector<Card> cards;
public:
    Poker();

    Poker(const Poker &poker);

    const vector<Card> &getCards() const;

    void setCards(const vector<Card> &cards);

    int getCardCount();

    void toString();

    void clear();
};


#endif //_POKER_H

// 撲克牌類的實現
// Created by zhuhuix on 2020-06-10.
//

#include "Poker.h"
#include <iostream>

const vector<Card> &Poker::getCards() const {
    return this->cards;
}

void Poker::setCards(const vector<Card> &cards) {
    Poker::cards = cards;
}

// 構造函數
Poker::Poker() {
    for (int i = 0; i < NUMBER_COUNT; i++) {
        for (int j = 0; j < COLOR_COUNT; j++) {
            this->cards.emplace_back(COLORS[j], NUMBERS[i]);
        }
    }
    for (int i = 0; i < JOKER_COUNT; i++) {
        this->cards.emplace_back("JOKER", JOKER[i]);
    }
}

// 拷貝構造函數
Poker::Poker(const Poker &poker) {
    for (int i = 0; i < poker.getCards().size(); i++) {
        this->cards.emplace_back(poker.cards[i].getColor(), poker.cards[i].getNumber());
    }
}

int Poker::getCardCount() {
    return this->cards.size();
}

void Poker::toString() {
    cout << "共" << getCardCount() << "張牌:";
    cout << "[";
    for (int i = 0; i < this->cards.size(); i++) {
        cout << this->cards[i].toString();
        if (i != getCardCount() - 1) {
            cout << ",";
        }
    }
    cout << "]" << endl;

}

void Poker::clear() {
    this->cards.clear();
}

// 主測試程序
// Created by Administrator on 2020-06-10.
//

#include "Poker.h"
#include <iostream>

using namespace std;

int main() {
    Poker poker1;
    cout << "第一副牌:";
    poker1.toString();
    // 通過拷貝構造函數生成第二副牌
    Poker poker2(poker1);
    cout << "第二副牌:";
    poker2.toString();
    // 清除撲克牌1
    poker1.clear();
    cout << "清空后,第一副牌:";
    poker1.toString();
    cout << "第二副牌:";
    poker2.toString();
    return 0;
}
  • 輸出:
2.3.3 JAVA通過拷貝構造方法實現深拷貝
  • JAVA拷貝構造方法與C++的拷貝構造函數相同,被複制對象的類需要實現拷貝構造方法:
    首先需要聲明帶有和本類相同類型的參數構造方法
    其次拷貝構造方法可以通過序列化實現快速複製
  • 拷貝對象通過調用拷貝構造方法進行創建。
    — 我們再改寫一下Poker類,實現拷貝構造方法:
/**
 * 整副副撲克牌--實現拷貝構造方法
 *
 * @author zhuhuix
 * @date 2020-06-10
 */
public class Poker implements Serializable {

    private List<Card> cards;

    public Poker() {
        List<Card> cardList = new ArrayList<>();
        // 按花色與数字組合生成52張撲克牌
        for (int i = 0; i < Constant.COLORS.size(); i++) {
            for (int j = 0; j < Constant.NUMBERS.size(); j++) {
                cardList.add(new Card(Constant.COLORS.get(i), Constant.NUMBERS.get(j)));
            }
        }
        // 生成大小王
        for (int i = 0; i < Constant.JOKER.size(); i++) {
            cardList.add(new Card("JOKER", Constant.JOKER.get(i)));
        }

        this.cards = cardList;
    }

    // 拷貝構造方法:利用序列化實現深拷貝
    public Poker(Poker poker) {

        try {

            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(poker);

            ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(is);
            this.cards = ((Poker) ois.readObject()).getCards();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

    // 從整副撲克牌中抽走大小王
    public void removeJoker() {
        Iterator<Card> iterator = this.cards.iterator();
        while (iterator.hasNext()) {
            Card cardJoker = iterator.next();
            if (cardJoker.getColor() == "JOKER") {
                iterator.remove();
            }
        }
    }

    public List<Card> getCards() {
        return cards;
    }

    public void setCards(List<Card> cards) {
        this.cards = cards;
    }

    public Integer getCardCount() {
        return this.cards.size();
    }

    @Override
    public String toString() {
        StringBuilder poker = new StringBuilder("[");
        Iterator<Card> iterator = this.cards.iterator();
        while (iterator.hasNext()) {
            poker.append(iterator.next().toString() + ",");
        }
        poker.setCharAt(poker.length() - 1, ']');
        return poker.toString();
    }
}

  • 對測試主程序進行修改:
/**
 * 測試程序
 *
 * @author zhuhuix
 * @date 2020-6-10
 */
public class PlayDemo {

    public static void main(String[] args) throws CloneNotSupportedException {

        // 生成一副撲克牌並洗好牌
        Poker poker1 = new Poker();
        System.out.println("新建:第一副牌共 "+poker1.getCardCount()+" 張:"+poker1.toString());

        Poker poker2 = new Poker(poker1);
        System.out.println("第一副牌拷頁生成第二副牌,共 "+poker2.getCardCount()+" 張:"+poker2.toString());

        poker1.removeJoker();

        System.out.println("====第一副牌抽走大小王后====");
        System.out.println("第一副牌還有 "+poker1.getCardCount()+" 張:"+poker1.toString());
        System.out.println("第二副牌還有 "+poker2.getCardCount()+" 張:"+poker2.toString());


        Poker poker3 = new Poker(poker1);
        System.out.println("第三副牌還有 "+poker3.getCardCount()+" 張:"+poker3.toString());
    }

}
  • 輸出結果:
    –通過序列化的有手段,同樣也能實現對象的深拷貝

四、總結

  • java程序進行對象拷貝時,如果對象的類中存在引用類型時,需進行深拷貝
  • 對象拷貝可以通過實現Cloneable接口完成
  • java編程也可仿照 C++程序的拷貝構造函數,實現拷貝構造方法進行對象的複製
  • 通過序列化與反序化手段可實現對象的深拷貝

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

小菜成長之路,警惕淪為 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,轉至 原文 可獲得最佳閱讀體驗。

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

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

[C#.NET 拾遺補漏]05:操作符的幾個騷操作

閱讀本文大概需要 1.5 分鐘。

大家好,這是極客精神【C#.NET 拾遺補漏】專輯的第 5 篇文章,今天要講的內容是操作符。

操作符的英文是 Operator,在數值計算中習慣性的被叫作運算符,所以在中文的概念中,運算符是操作符的一個子集。

操作符是很基礎的知識了,基礎歸基礎,我們來回顧一下 C# 操作符那些比較騷的操作,能想到的不多,請大家補充。

操作符的重載

操作符重載大部分語言都沒有,而 C# 有。C# 允許用戶定義類型對操作符進行重載,方式是使用 operate 關鍵字把操作符寫成公開靜態函數。下面來演示一下重載 + 這個操作符。

我們創建一個 Complex 結構類型來代表一個複數,我們知道複數有實數和虛數組成,於是可以這樣定義:

public struct Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }
}

現在我們想實現複數的相加操作,即:

Complex a = new Complex() { Real = 1, Imaginary = 2 };
Complex b = new Complex() { Real = 4, Imaginary = 8 };
Complex c = a + b;

默認情況,自定義類是不能進行算術運算的,以上 a + b 會編譯報錯,我們需要對 + 進行操作符重載:

public static Complex operator +(Complex c1, Complex c2)
{
    return new Complex
    {
        Real = c1.Real + c2.Real,
        Imaginary = c1.Imaginary + c2.Imaginary
    };
}

C# 中像加減乘除等這類操作符都可以重載,也有些操作符是不能重載的,具體請查看文末參考鏈接。

隱式和顯式轉換操作符

我們知道子類可以隱式轉換為父類,在某種情況下(如父類由子類賦值而來)父類可以顯式轉換為子類。

在 C# 中,對於沒有子父類關係的用戶定義類型,也是可以實現顯式和隱式轉換的。C# 允許用戶定義類型通過使用 implicitexplicit 關鍵字來控制對象的賦值和對象的類型轉換。它的定義形式如下:

public static <implicit/explicit> operator <結果類型>(<源類型> myType)

這裏以結果類型為方法名,源類型對象作為參數,只能是這一個參數,不能定義第二個參數,但可以通過該參數對象訪問其類的私有成員。下面是一個既有顯式又有隱式轉換操作符的例子:

public class BinaryImage
{
    private readonly bool[] _pixels;

    // 隱式轉換操作符示例
    public static implicit operator ColorImage(BinaryImage bm)
    {
        return new ColorImage(bm);
    }

    // 顯式轉換操作符示例
    public static explicit operator bool[](BinaryImage bm)
    {
        return bm._pixels;
    }
}

public class ColorImage
{
    public ColorImage(BinaryImage bm) { }
}

這樣,我們就可以把 BinaryImage 對象隱式轉換為 ColorImage 對象,把 BinaryImage 對象顯式轉換為 bool 數組對象:

var binaryImage = new BinaryImage();
ColorImage colorImage = binaryImage; // 隱式轉換
bool[] pixels = (bool[])binaryImage; // 顯式轉換

而且轉換操作符可以定義為雙向显示和隱式轉換。既可從你的類型而來,亦可到你的類型而去:

public class BinaryImage
{
    public BinaryImage(ColorImage cm) { }

    public static implicit operator ColorImage(BinaryImage bm)
    {
        return new ColorImage(bm);
    }

    public static explicit operator BinaryImage(ColorImage cm)
    {
        return new BinaryImage(cm);
    }
}

我們知道 as 操作符也是一種顯式轉換操作符,那它適用於上面的這種情況嗎,即:

ColorImage cm = myBinaryImage as ColorImage;

你覺得這樣寫有問題嗎?請在評論區告訴我答案。

空條件和空聯合操作符

空條件(Null Conditional)操作符 ?. 和空聯合(Null Coalescing)操作符 ??,都是 C# 6.0 的語法,大多數人都很熟悉了,使用也很簡單。

?. 操作符會在對象為 null 時立即返回 null,不為 null 時才會調用後面的代碼。其中的符號 ? 代表對象本身,符號 . 代表調用,後面不僅可以是對象的屬性也可以是索引器或方法。以該操作符為分隔的每一截類型相同時可以接龍。示例:

var bar = foo?.Value; // 相當於 foo == null ? null : foo.Value
var bar = foo?.StringValue?.ToString(); // 每一截類型相同支持接龍
var bar = foo?.IntValue?.ToString(); // 每一截類型不同,不能接龍,因為結果類型無法確定

如果是調用索引器,則不需要符號 .,比如:

var foo = new[] { 1, 2, 3 };
var bar = foo?[1]; // 相當於 foo == null ? null : foo[1]

空聯合操作符 ??,當左邊為空時則返回右邊的值,否則返回左邊的值。同樣,每一截的類型相同時支持接龍。

var fizz = foo.GetBar() ?? bar;
var buzz = foo ?? bar ?? fizz;

=> Lambda 操作符

Lambda 操作符,即 =>,它用來定義 Lambda 表達式,也被廣泛用於 LINQ 查詢。它的一般定義形式如下:

(input parameters) => expression

示例:

string[] words = { "cherry", "apple", "blueberry" };
int minLength = words.Min((string w) => w.Length);

實際應用中我們一般省略參數的類型聲明:

int minLength = words.Min(w => w.Length);

Lambda 操作符的後面可以是表達式,可以是語句,也可以是語句塊,比如:

// 表達式
(int x, int y) => x + y

// 語句
(string x) => Console.WriteLine(x)

// 語句塊
(string x) => {
    x += " says Hello!";
    Console.WriteLine(x);
}

這個操作符也可以很方便的用來定義委託方法(其實 Lambda 操作符就是由委託演變而來)。

單獨定義委託方法:

void MyMethod(string s)
{
    Console.WriteLine(s + " World");
}
delegate void TestDelegate(string s);
TestDelegate myDelegate = MyMethod;
myDelegate("Hello");

使用 Lambda 操作符:

delegate void TestDelegate(string s);
TestDelegate myDelegate = s => Console.WriteLine(s + " World");
myDelegate("Hello");

在一個類中,當實現體只有一句代碼時,也可以用 Lambda 操作符對方法和 Setter / Getter 進行簡寫:

public class Test
{
    public int MyProp { get => 123; }
    public void MyMethod() => Console.WriteLine("Hello!");
}

以上是幾種比較有代表性的操作符的“騷”操作,當然還有,但大多都過於基礎,大家都知道,就不總結了。

C# 雖然目前不是最受歡迎的語言,但確實是一門優美的語言,其中少不了這些操作符語法糖帶來的功勞。

參考:https://bit.ly/3h5yKNr

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

【其他文章推薦】

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

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

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

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

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

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

Uber基於Apache Hudi構建PB級數據湖實踐

1. 引言

從確保準確預計到達時間到預測最佳交通路線,在Uber平台上提供安全、無縫的運輸和交付體驗需要可靠、高性能的大規模數據存儲和分析。2016年,Uber開發了增量處理框架Apache Hudi,以低延遲和高效率為關鍵業務數據管道賦能。一年後,我們開源了該解決方案,以使得其他有需要的組織也可以利用Hudi的優勢。接着在2019年,我們履行承諾,進一步將其捐贈給了Apache Software Foundation,差不多一年半之後,Apache Hudi畢業成為Apache Software Foundation頂級項目。為紀念這一里程碑,我們想分享Apache Hudi的構建、發布、優化和畢業之旅,以使更大的大數據社區受益。

2. 什麼是Apache Hudi

Apache Hudi是一個存儲抽象框架,可幫助組織構建和管理PB級數據湖,通過使用upsert增量拉取等原語,Hudi將流式處理帶到了類似批處理的大數據中。這些功能通過統一的服務層(幾分鐘左右即可實現數據延遲),幫助我們更快,更新鮮地獲取服務數據,從而避免了維護多個系統的額外開銷。更靈活地,Apache Hudi還可以在Hadoop分佈式文件系統(HDFS)或雲存儲上運行。

Hudi在數據湖上啟用原子性、一致性、隔離性和持久性(ACID)語義。 Hudi的兩個最廣泛使用的功能是upserts增量拉取,它使用戶能夠捕獲變更數據並將其應用於數據湖,為了實現這一點,Hudi提供了可插拔索引機制,以及自定義索引實現。Hudi具有控制和管理數據湖中文件布局的能力,這不僅能克服HDFS NameNode節點和其他雲存儲限制,而且對於通過提高可靠性和查詢性能來維護健康的數據生態系統也非常重要。另外Hudi支持多種查詢引擎,例如Presto,Apache Hive,Apache Spark和Apache Impala。

圖1. Apache Hudi通過在表上提供不同的視圖來攝取變更日誌、事件和增量流,以服務於不同的應用場景

從總體上講,Hudi在概念上分為3個主要組成部分:需要存儲的原始數據;用於提供upsert功能的索引數據以及用於管理數據集的元數據。內核方面,Hudi維護在不同時間點在表上執行的所有動作的時間軸,在Hudi中稱為即時,這提供了表格的即時視圖,同時還有效地支持了按序到達的數據檢索,Hudi保證時間軸上的操作是原子性的,並且基於即時時間,與數據庫中進行更改的時間是一致的。利用這些信息,Hudi提供了同一Hudi表的不同視圖,包括用於快速列式文件性能的讀優化視圖,用於快速數據攝取的實時視圖以及用於將Hudi表作為變更日誌流讀取的增量視圖,如上圖1所示。

Hudi將數據表組織到分佈式文件系統上基本路徑(basepath)下的目錄結構中。 表分為多個分區,在每個分區內,文件被組織成文件組,由文件ID唯一標識。 每個文件組包含幾個文件切片,其中每個切片包含在某個特定提交/壓縮(commit/compaction)瞬間生成的基本數據文件(*.parquet),以及包含對基本數據文件進行插入/更新的一組日誌文件(*.log)。Hudi採用了Multiversion Concurrency Control(MVCC),其中壓縮操作將日誌和基本文件合併以生成新的文件片,而清理操作則將未使用的/較舊的文件片去除,以回收文件系統上的空間。

Hudi支持兩種表類型:寫時複製和讀時合併。 寫時複製表類型僅使用列文件格式(例如,Apache Parquet)存儲數據。通過寫時複製,可以通過在寫過程中執行同步合併來簡單地更新版本並重寫文件。

讀時合併表類型使用列式(例如Apache Parquet)和基於行(例如Apache Avro)文件格式的組合來存儲數據。 更新記錄到增量文件中,然後以同步或異步壓縮方式生成列文件的新版本。

Hudi還支持兩種查詢類型:快照查詢和增量查詢。 快照查詢是從給定的提交或壓縮操作開始對錶進行”快照”的請求。利用快照查詢時,寫時複製表類型僅暴露最新文件片中的基本/列文件,並且與非Hudi表相比,可保證相同的列查詢性能。寫入時複製提供了現有Parquet表的替代品,同時提供了upsert/delete和其他功能。對於讀時合併表,快照查詢通過動態合併最新文件切片的基本文件和增量文件來提供近乎實時的數據(分鐘級)。對於寫時複製表,自給定提交或壓縮以來,增量查詢將提供寫入表的新數據,並提供更改流以啟用增量數據管道。

3. Apache Hudi在Uber的使用

在Uber,我們在各種場景中都使用到了Hudi,從在Uber平台上提供有關行程的快速、準確的數據,從檢測欺詐到在我們的UberEats平台上提供餐廳和美食推薦。為了演示Hudi的工作原理,讓我們逐步了解如何確保Uber Marketplace中的行程數據在數據湖上是最新的,從而改善Uber平台上的騎手和駕駛員的用戶體驗。行程的典型生命周期始於騎手提出的行程,然後隨着行程的進行而繼續,直到行程結束且騎手到達最終目的地時才結束。 Uber的核心行程數據以表格形式存儲在Uber的可擴展數據存儲Schemaless中。行程表中的單個行程條目在行程的生命周期中可能會經歷許多更新。在Uber使用Hudi之前,大型Apache Spark作業會定期將整個數據集重新寫入HDFS,以獲取上游在線表的插入、更新和刪除,從而反映出行程狀態的變化。就背景而言,在2016年初(在構建Hudi之前),一些最大的任務是使用1000個executors並處理超過20TB的數據,此過程不僅效率低下,而且難以擴展。公司的各個團隊都依靠快速、準確的數據分析來提供高質量的用戶體驗,為滿足這些要求,我們當前的解決方案無法擴展進行數據湖上的增量處理。使用快照和重新加載解決方案將數據移至HDFS時,這些低效率的處理正在寫到到所有數據管道,包括使用此原始數據的下游ETL,我們可以看到這些問題只會隨着規模的擴大而加劇。

在沒有其他可行的開源解決方案可供使用的情況下,我們於2016年末為Uber構建並啟動了Hudi,以構建可促進大規模快速,可靠數據更新的事務性數據湖。Uber的第一代Hudi利用了寫時複製表類型,該表類型每30分鐘將作業處理速度提高到20GB,I/O和寫入放大減少了100倍。到2017年底,Uber的所有原始數據表都採用了Hudi格式,運行着地球上最大的事務數據湖之一。

圖2. Hudi的寫時複製功能使我們能夠執行文件級更新,從而大大提高數據的新鮮度

4. 改進Apache Hudi

隨着Uber數據處理和存儲需求的增長,我們開始遇到Hudi的寫時複製功能的局限性,主要是需要繼續提高數據的處理速度和新鮮度,即使使用Hudi”寫時複製”功能,我們的某些表收到的更新也分散在90%的文件中,從而導致需要重寫數據湖中任何給定的大型表的數據,重寫數據量大約為100TB。由於寫時複製甚至為單個修改的記錄重寫整個文件,因此寫複製功能導致較高的寫放大和損害的新鮮度,從而導致HDFS群集上不必要的I/O以及更快地消耗磁盤空間,此外,更多的數據表更新意味着更多的文件版本,以及HDFS文件數量激增,反過來,這些需求導致HDFS Namenode節點不穩定和較高的計算成本。

為了解決這些日益增長的擔憂,我們實現了第二種表類型,即”讀時合併”。由於讀時合併通過動態合併數據來使用近實時的數據,為避免查詢端的計算成本,我們需要合理使用此模式。”讀時合併”部署模型包括三個獨立的作業,其中包括一個攝取作業,包括由插入、更新和刪除組成的新數據,一個次要的壓縮作業,以異步方式主動地壓縮少量最新分區的更新/刪除內容,以及一個主要的壓縮作業,該作業會緩慢穩定地壓縮大量舊分區中的更新/刪除。這些作業中的每一個作業都以不同的頻率運行,次要作業和提取作業的運行頻率比主要作業要高,以確保其最新分區中的數據以列格式快速可用。通過這樣的部署模型,我們能夠以列式為數千個查詢提供新鮮數據,並將我們的查詢側合併成本限制在最近的分區上。使用讀時合併,我們能夠解決上面提到的所有三個問題,並且Hudi表幾乎不受任何對數據湖的更新或刪除的影響。現在,在Uber,我們會根據不同場景同時使用Apache Hudi的寫時複製和讀時合併功能。

圖3. Uber的Apache Hudi團隊開發了一種數據壓縮策略,用於讀時合併表,以便頻繁將最近的分區轉化為列式存儲,從而減少了查詢端的計算成本

有了Hudi,Uber每天向超過150PB數據湖中插入超過5,000億條記錄,每天使用30,000多個core,超過10,000多個表和數千個數據管道,Hudi每周在我們的各種服務中提供超過100萬個查詢。

5. Apache Hudi經驗總結

Uber在2017年開源了Hudi,為其他人帶來了該解決方案的好處,該解決方案可大規模提取和管理數據存儲,從而將流處理引入大數據。當Hudi畢業於Apache軟件基金會下的頂級項目時,Uber的大數據團隊總結了促使我們構建Hudi的各種考慮因素,包括:

  • 如何提高數據存儲和處理效率?
  • 如何確保數據湖包含高質量的表?
  • 隨着業務的增長,如何繼續大規模有效地提供低延遲的數據?
  • 在分鐘級別的場景中,我們如何統一服務層?

如果沒有良好的標準化和原語,數據湖將很快成為無法使用的”數據沼澤”。這樣的沼澤不僅需要花費大量時間和資源來協調、清理和修復表,而且還迫使各個服務所有者構建複雜的算法來進行調整、改組和交易,從而給技術棧帶來不必要的複雜性。

如上所述,Hudi通過無縫地攝取和管理分佈式文件系統上的大型分析數據集來幫助用戶控制其數據湖,從而彌補了這些差距。建立數據湖是一個多方面的問題,需要在數據標準化、存儲技術、文件管理實踐,數據攝取與數據查詢之間折衷性能等方面進行取捨。在我們建立Hudi時與大數據社區的其他成員交談時,我們了解到這些問題在許多工程組織中普遍存在。我們希望在過去的幾年中,開源和與Apache社區的合作,在Hudi基礎上發展可以使其他人在不同行業對大數據運營有更深入的了解。 在Uber之外,Apache Hudi已在多家公司用於生產,其中包括阿里雲,騰訊雲,AWS、Udemy等。

6. 未來計劃

圖4. Apache Hudi場景包括數據分析和基礎架構運行狀況監視

Hudi通過對數據集強制schema,幫助用戶構建更強大、更新鮮的數據湖,從而提供高質量的見解。

在Uber,擁有全球最大的事務數據湖之一為我們提供了各種Apache Hudi用例場景的機會,由於以這種規模解決問題並提高效率可能會產生重大影響,因此有直接的動機促使我們更加深入。在Uber,我們已經使用了先進的Hudi原語,如增量拉取來幫助建立鏈式增量流水線,從而減少了作業的計算空間,而這些作業本來會執行大型掃描和寫入。我們根據特定的用例場景和要求調整讀時合併表的壓縮策略。 自從我們將Hudi捐贈給Apache基金會以來,最近幾個月,Uber貢獻了一些功能,例如嵌入式時間軸服務以實現高效的文件系統訪問,刪除重命名以支持雲友好的部署並提高增量拉取性能。

在接下來的幾個月中,Uber計劃為Apache Hudi社區貢獻更多新功能。其中一些功能可通過優化計算使用量以及改善數據應用程序的性能來幫助降低成本,我們還將更深入地研究如何根據訪問模式和數據應用程序需求來改善存儲管理和查詢性能。

有關我們如何計劃實現這些目標的更多信息,您可以閱讀一些RFC,包括支持列索引和O(1)查詢計劃的智能元數據,將Parquet表高效引導到Hudi,記錄級別索引支持更快速插入,這些RFC由Uber的Hudi團隊向Apache社區提出。

隨着Apache Hudi畢業成為Apache頂級項目,我們很高興為該項目雄心勃勃的路線圖做出貢獻。Hudi使Uber和其他公司可以使用開放源文件格式,在未來證明其數據湖的速度,可靠性和交易能力,從而消除了許多大數據挑戰,並構建了豐富而可移植的數據應用程序。

Apache Hudi是一個成長中的社區,具有令人興奮且不斷髮展的發展路線圖。 如果您有興趣為這個項目做貢獻,可點擊這裏。

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

【其他文章推薦】

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

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

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

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

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

性能調優必備利器之 JMH

if 快還是 switch 快?HashMap 的初始化 size 要不要指定,指定之後性能可以提高多少?各種序列化方法哪個耗時更短?

無論出自何種原因需要進行性能評估,量化指標總是必要的。

在大部分場合,簡單地回答誰快誰慢是遠遠不夠的,如何將程序性能量化呢?

這就需要我們的主角 JMH 登場了!

JMH 簡介

JMH(Java Microbenchmark Harness)是用於代碼微基準測試的工具套件,主要是基於方法層面的基準測試,精度可以達到納秒級。該工具是由 Oracle 內部實現 JIT 的大牛們編寫的,他們應該比任何人都了解 JIT 以及 JVM 對於基準測試的影響。

當你定位到熱點方法,希望進一步優化方法性能的時候,就可以使用 JMH 對優化的結果進行量化的分析。

JMH 比較典型的應用場景如下:

  1. 想準確地知道某個方法需要執行多長時間,以及執行時間和輸入之間的相關性
  2. 對比接口不同實現在給定條件下的吞吐量
  3. 查看多少百分比的請求在多長時間內完成

下面我們以字符串拼接的兩種方法為例子使用 JMH 做基準測試。

加入依賴

因為 JMH 是 JDK9 自帶的,如果是 JDK9 之前的版本需要加入如下依賴(目前 JMH 的最新版本為 1.23):

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.23</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.23</version>
</dependency>

編寫基準測試

接下來,創建一個 JMH 測試類,用來判斷 +StringBuilder.append() 兩種字符串拼接哪個耗時更短,具體代碼如下所示:

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConnectTest {

    @Param(value = {"10", "50", "100"})
    private int length;

    @Benchmark
    public void testStringAdd(Blackhole blackhole) {
        String a = "";
        for (int i = 0; i < length; i++) {
            a += i;
        }
        blackhole.consume(a);
    }

    @Benchmark
    public void testStringBuilderAdd(Blackhole blackhole) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(i);
        }
        blackhole.consume(sb.toString());
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(StringConnectTest.class.getSimpleName())
                .result("result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }
}

其中需要測試的方法用 @Benchmark 註解標識,這些註解的具體含義將在下面介紹。

在 main() 函數中,首先對測試用例進行配置,使用 Builder 模式配置測試,將配置參數存入 Options 對象,並使用 Options 對象構造 Runner 啟動測試。

另外大家可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

執行基準測試

準備工作做好了,接下來,運行代碼,等待片刻,測試結果就出來了,下面對結果做下簡單說明:

# JMH version: 1.23
# VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09
# VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe
# VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd
# Parameters: (length = 100)

該部分為測試的基本信息,比如使用的 Java 路徑,預熱代碼的迭代次數,測量代碼的迭代次數,使用的線程數量,測試的統計單位等。

# Warmup Iteration   1: 1083.569 ±(99.9%) 393.884 ns/op
# Warmup Iteration   2: 864.685 ±(99.9%) 174.120 ns/op
# Warmup Iteration   3: 798.310 ±(99.9%) 121.161 ns/op

該部分為每一次熱身中的性能指標,預熱測試不會作為最終的統計結果。預熱的目的是讓 JVM 對被測代碼進行足夠多的優化,比如,在預熱后,被測代碼應該得到了充分的 JIT 編譯和優化。

Iteration   1: 810.667 ±(99.9%) 51.505 ns/op
Iteration   2: 807.861 ±(99.9%) 13.163 ns/op
Iteration   3: 851.421 ±(99.9%) 33.564 ns/op
Iteration   4: 805.675 ±(99.9%) 33.038 ns/op
Iteration   5: 821.020 ±(99.9%) 66.943 ns/op

Result "com.wupx.jmh.StringConnectTest.testStringBuilderAdd":
  819.329 ±(99.9%) 72.698 ns/op [Average]
  (min, avg, max) = (805.675, 819.329, 851.421), stdev = 18.879
  CI (99.9%): [746.631, 892.027] (assumes normal distribution)

Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

該部分显示測量迭代的情況,每一次迭代都显示了當前的執行速率,即一個操作所花費的時間。在進行 5 次迭代后,進行統計,在本例中,length 為 100 的情況下 testStringBuilderAdd 方法的平均執行花費時間為 819.329 ns,誤差為 72.698 ns

最後的測試結果如下所示:

Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringAdd               10  avgt    5   161.496 ±  17.097  ns/op
StringConnectTest.testStringAdd               50  avgt    5  1854.657 ± 227.902  ns/op
StringConnectTest.testStringAdd              100  avgt    5  6490.062 ± 327.626  ns/op
StringConnectTest.testStringBuilderAdd        10  avgt    5    68.769 ±   4.460  ns/op
StringConnectTest.testStringBuilderAdd        50  avgt    5   413.021 ±  30.950  ns/op
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

結果表明,在拼接字符次數越多的情況下,StringBuilder.append() 的性能就更好。

生成 jar 包執行

對於一些小測試,直接用上面的方式寫一個 main 函數手動執行就好了。

對於大型的測試,需要測試的時間比較久、線程數比較多,加上測試的服務器需要,一般要放在 Linux 服務器里去執行。

JMH 官方提供了生成 jar 包的方式來執行,我們需要在 maven 里增加一個 plugin,具體配置如下:

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.4.1</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    <finalName>jmh-demo</finalName>
                    <transformers>
                        <transformer
                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <mainClass>org.openjdk.jmh.Main</mainClass>
                        </transformer>
                    </transformers>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

接着執行 maven 的命令生成可執行 jar 包並執行:

mvn clean install
java -jar target/jmh-demo.jar StringConnectTest

JMH 基礎

為了能夠更好地使用 JMH 的各項功能,下面對 JMH 的基本概念進行講解:

@BenchmarkMode

用來配置 Mode 選項,可用於類或者方法上,這個註解的 value 是一個數組,可以把幾種 Mode 集合在一起執行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),還可以設置為 Mode.All,即全部執行一遍。

  1. Throughput:整體吞吐量,每秒執行了多少次調用,單位為 ops/time
  2. AverageTime:用的平均時間,每次操作的平均時間,單位為 time/op
  3. SampleTime:隨機取樣,最後輸出取樣結果的分佈
  4. SingleShotTime:只運行一次,往往同時把 Warmup 次數設為 0,用於測試冷啟動時的性能
  5. All:上面的所有模式都執行一次

@State

通過 State 可以指定一個對象的作用範圍,JMH 根據 scope 來進行實例化和共享操作。@State 可以被繼承使用,如果父類定義了該註解,子類則無需定義。由於 JMH 允許多線程同時執行測試,不同的選項含義如下:

  1. Scope.Benchmark:所有測試線程共享一個實例,測試有狀態實例在多線程共享下的性能
  2. Scope.Group:同一個線程在同一個 group 里共享實例
  3. Scope.Thread:默認的 State,每個測試線程分配一個實例

@OutputTimeUnit

為統計結果的時間單位,可用於類或者方法註解

@Warmup

預熱所需要配置的一些基本測試參數,可用於類或者方法上。一般前幾次進行程序測試的時候都會比較慢,所以要讓程序進行幾輪預熱,保證測試的準確性。參數如下所示:

  1. iterations:預熱的次數
  2. time:每次預熱的時間
  3. timeUnit:時間的單位,默認秒
  4. batchSize:批處理大小,每次操作調用幾次方法

為什麼需要預熱?

因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯為機器碼,從而提高執行速度,所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。

@Measurement

實際調用方法所需要配置的一些基本測試參數,可用於類或者方法上,參數和 @Warmup 相同。

@Threads

每個進程中的測試線程,可用於類或者方法上。

@Fork

進行 fork 的次數,可用於類或者方法上。如果 fork 數是 2 的話,則 JMH 會 fork 出兩個進程來進行測試。

@Param

指定某項參數的多種情況,特別適合用來測試一個函數在不同的參數輸入的情況下的性能,只能作用在字段上,使用該註解必須定義 @State 註解。

在介紹完常用的註解后,讓我們來看下 JMH 有哪些陷阱。

JMH 陷阱

在使用 JMH 的過程中,一定要避免一些陷阱。

比如 JIT 優化中的死碼消除,比如以下代碼:

@Benchmark
public void testStringAdd(Blackhole blackhole) {
    String a = "";
    for (int i = 0; i < length; i++) {
        a += i;
    }
}

JVM 可能會認為變量 a 從來沒有使用過,從而進行優化把整個方法內部代碼移除掉,這就會影響測試結果。

JMH 提供了兩種方式避免這種問題,一種是將這個變量作為方法返回值 return a,一種是通過 Blackhole 的 consume 來避免 JIT 的優化消除。

其他陷阱還有常量摺疊與常量傳播、永遠不要在測試中寫循環、使用 Fork 隔離多個測試方法、方法內聯、偽共享與緩存行、分支預測、多線程測試等,感興趣的可以閱讀 https://github.com/lexburner/JMH-samples 了解全部的陷阱。

JMH 插件

大家還可以通過 IDEA 安裝 JMH 插件使 JMH 更容易實現基準測試,在 IDEA 中點擊 File->Settings...->Plugins,然後搜索 jmh,選擇安裝 JMH plugin:

這個插件可以讓我們能夠以 JUnit 相同的方式使用 JMH,主要功能如下:

  1. 自動生成帶有 @Benchmark 的方法
  2. 像 JUnit 一樣,運行單獨的 Benchmark 方法
  3. 運行類中所有的 Benchmark 方法

比如可以通過右鍵點擊 Generate...,選擇操作 Generate JMH benchmark 就可以生成一個帶有 @Benchmark 的方法。

還有將光標移動到方法聲明並調用 Run 操作就運行一個單獨的 Benchmark 方法。

將光標移到類名所在行,右鍵點擊 Run 運行,該類下的所有被 @Benchmark 註解的方法都會被執行。

JMH 可視化

除此以外,如果你想將測試結果以圖表的形式可視化,可以試下這些網站:

  • JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
  • JMH Visualizer:https://jmh.morethan.io/

比如將上面測試例子結果的 json 文件導入,就可以實現可視化:

總結

本文主要介紹了性能基準測試工具 JMH,它可以通過一些功能來規避由 JVM 中的 JIT 或者其他優化對性能測試造成的影響。

只需要將待測的業務邏輯用 @Benchmark 註解標識,就可以讓 JMH 的註解處理器自動生成真正的性能測試代碼,以及相應的性能測試配置文件。

最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。

參考

http://openjdk.java.net/projects/code-tools/jmh/

深入拆解Java虛擬機

《實戰Java高併發程序設計》

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

【其他文章推薦】

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

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

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

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

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

自己動手寫SQL執行引擎

自己動手寫SQL執行引擎

前言

在閱讀了大量關於數據庫的資料后,筆者情不自禁產生了一個造數據庫輪子的想法。來驗證一下自己對於數據庫底層原理的掌握是否牢靠。在筆者的github中給這個database起名為Freedom。

整體結構

既然造輪子,那當然得從前端的網絡協議交互到後端的文件存儲全部給擼一遍。下面是Freedom實現的整體結構,裡面包含了實現的大致模塊:

最終存儲結構當然是使用經典的B+樹結構。當然在B+樹和文件系統block塊之間的轉換則通過Buffer(Page) Manager來進行。當然了,為了完成事務,還必須要用WAL協議,其通過Log Manager來操作。
Freedom採用的是索引組織表,通過DruidSQL Parse來將sql翻譯為對應的索引操作符進而進行對應的語義操作。

MySQL Protocol結構

client/server之間的交互採用的是MySQL協議,這樣很容易就可以和mysql client以及jdbc進行交互了。

query packet

mysql通過3byte的定長包頭去進行分包,進而解決tcp流的讀取問題。再通過一個sequenceId來再應用層判斷packet是否連續。

result set packet

mysql協議部分最複雜的內容是其對於result set的讀取,在NIO的方式下加重了複雜性。
Freedom通過設置一系列的讀取狀態可以比較好的在Netty框架下解決這一問題。

row packet

還有一個較簡單的是對row格式進行讀取,如上圖所示,只需要按部就班的解析即可。

由於協議解析部分較為簡單,在這裏就不再贅述。
關注筆者公眾號,獲取更多乾貨文章

SQL Parse

Freedom採用成熟好用的Druid SQL Parse作為解析器。事實上,解析sql就是將用文本表示
的sql語義表示為一系列操作符(這裏限於篇幅原因,僅僅給出select中where過濾的原理)。

對where的處理

例如where後面的謂詞就可以表示為一系列的以樹狀結構組織的SQL表達式,如下圖所示:

當access層通過游標提供一系列row后,就可以通過這個樹狀表達式來過濾出符合where要求的數據。Druid採用了Parse中常用的visitor很方便的處理上面的表達式計算操作。

對join的處理

對join最簡單處理方案就是對兩張表進行笛卡爾積,然後通過上面的where condition進行過濾,如下圖所示:

Freedom對於縮小笛卡爾積的處理

由於Freedom採用的是B+樹作為底層存儲結構,所以可以通過where謂詞來界定B+樹scan(搜索)的範圍(也即最大搜索key和最小搜索key在B+樹種中的位置)。考慮sql

select a.*,b.* from t_archer as a join t_rider as b where a.id>=3 and a.id<=11 b.id and b.id>=19 b.id<=31

那麼就可以界定出在id這個索引上,a的scan範圍為[3,11],如下圖所示:

b的scan範圍為[19,31],如下圖所示(假設兩張表數據一樣,便於繪圖):

scan少了從原來的15*15(一共15個元素)次循環減少到4*4次循環,即循環次數減少到7.1%

當然如果存在join condition的話,那麼Freedom在底層cursor遞歸處理的過程中會預先過濾掉一部分數據,進一步減少上層的過濾。

B+Tree的磁盤結構

leaf磁盤結構

Freedom的B+Tree是存儲到磁盤裡的。考慮到存儲的限制以及不定長的key值,所以會變得非常複雜。Freedom以page為單位來和磁盤進行交互。恭弘=叶 恭弘子節點和非恭弘=叶 恭弘子節點都由page承載並刷入磁盤。結構如下所示:

一個元組(tuple/item)在一個page中分為定長的ItemPointer和不定長的Item兩部分。
其中ItemPointer裏面存儲了對應item的起始偏移和長度。同時ItemPointer和Item如圖所示是向著中心方向進行伸張,這種結構很有效的組織了非定長Item。

leaf和node節點在Page中的不同

雖然leaf和node在page中組織結構一致,但其item包含的項確有區別。由於Freedom採用的是索引組織表,所以對於leaf在聚簇索引(clusterIndex)和二級索引(secondaryIndex)中對item的表示也有區別,如下圖所示:

其中在二級索引搜索時通過secondaryIndex通過index-key找到對應的clusterId,再通過
clusterId在clusterIndex中找到對應的row記錄。
由於要落盤,所以Freedom在node節點中的item裏面寫入了index-key對應的pageno,
這樣就可以容易的從磁盤恢復所有的索引結構了。

B+Tree在文件中的組織

有了Page結構,我們就可以將數據承載在一個個page大小的內存裏面,同時還可以將page刷新到對應的文件里。有了node.item中的pageno,我們就可以較容易的進行文件和內存結構之間的互相映射了。
B+樹在磁盤文件中的組織如下圖所示:

B+樹在內存中相對應的映射結構如下圖所示:

文件page和內存page中的內容基本是一致的,除了一些內存page中特有的字段,例如dirty等。

每個索引一個B+樹

在Freedom中,每個索引都是一顆B+樹,對記錄的插入和修改都要對所有的B+樹進行操作。

B+Tree的測試

筆者通過一系列測試case,例如隨機變長記錄對B+樹進行插入並落盤,修復了其中若干個非常詭異的corner case。

B+Tree的todo

筆者這裏只是完成了最簡單的B+樹結構,沒有給其添加併發修改的鎖機制,也沒有在B+樹做操作的時候記錄log來保證B+樹在宕機等災難性情況下的一致性,所以就算完成了這麼多的工作量,距離一個高併發高可用的bptree還有非常大的距離。

Meta Data

table的元信息由create table所創建。創建之後會將元信息落盤,以便Freedom在重啟的時候加載表信息。每張表的元信息只佔用一頁的空間,依舊復用page結構,主要保存的是聚簇索引和二級索引的信息。元信息對應的Item如下圖所示:

如果想讓mybatis可以自動生成關於Freedom的代碼,還需實現一些特定的sql來展現Freedom的元信息。這個在筆者另一個項目rider中有這樣的實現。原理如下圖所示:

實現了上述4類SQL之後,mybatis-generator就可以通過jdbc從Freedom獲取元信息進而自動生成代碼了。

事務支持

由於當前Freedom並沒有保證併發,所以對於事務的支持只做了最簡單的WAL協議。通過記錄redo/undolog從而實現原子性。

redo/undo log協議格式

Freedom在每做一個修改操作時,都會生成一條日誌,其中記錄了修改前(undo)和修改后(redo)的行信息,undo用來回滾,redo用來宕機recover。結構如下圖所示:

WAL協議

WAL協議很好理解,就是在事務commit前將當前事務中所產生的的所有log記錄刷入磁盤。
Freedom自然也做了這個操作,使得可以在宕機后通過log恢復出所有的數據。

回滾的實現

由於日誌中記錄了undo,所以對於一個事務的回滾直接通過日誌進行undo即可。如下圖所示:

宕機恢復

Freedom如果在page全部刷盤之後關機,則可以由通過加載page的方式獲取原來的數據。
但如果突然宕機,例如kill -9之後,則可以通過WAL協議中記錄的redo/undo log來重新
恢復所有的數據。由於時間和精力所限,筆者並沒有實現基於LSN的檢查點機制。

Freedom運行

git clone https://github.com/alchemystar/Freedom.git
// 並沒有做打包部署的工作,所以最簡單的方法是在java編輯器裏面
run alchemystar.freedom.engine.server.main

以下是筆者實際運行Freedom的例子:

join查詢

delete回滾

Freedom todo

Freedom還有很多工作沒有完成,例如有層次的鎖機制和MVCC等,由於工作忙起來就耽擱了。
於是筆者就看了看MySQL源碼的實現理解了一下鎖和MVCC實現原理,並寫了兩篇博客。比起
自己動手擼實在是輕鬆太多了_

MVCC

https://my.oschina.net/alchemystar/blog/1927425

二階段鎖

https://my.oschina.net/alchemystar/blog/1438839

尾聲

在造輪子的過程中一開始是非常有激情非常快樂的。但隨着系統越來越龐大,複雜性越來越高,進度就會越來越慢,還時不時要推翻自己原來的設想並重新設計,然後再協同修改關聯的所有代碼,就如同泥沼,越陷越深。至此,筆者才領悟了軟件工程最重要的其實是控制複雜度!始終保持簡潔的接口和優雅的設計是實現一個大型系統的必要條件。

收穫與遺憾

這次造輪子的過程基本滿足了筆者的初衷,通過寫一個數據庫來學習數據庫。不僅僅是加深了理解,最重要的是筆者在寫的過程中終於明白了數據庫為什麼要這麼設計,為什麼不那樣設計,僅僅對書本的閱讀可能並不會有這些思考與領悟。
當然,還是有很多遺憾的,Freedom並沒有實現鎖機制和MVCC。由於只能在工作閑暇時間寫,所以斷斷續續寫了一兩個月,工作一忙就將這個項目閑置了。現在將Freedom的設計寫出來,希望大家能有所收穫。
更多乾貨,盡在解Bug之路:

github鏈接

https://github.com/alchemystar/Freedom

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

阻塞隊列一——java中的阻塞隊列

目錄

  • 阻塞隊列簡介:介紹阻塞隊列的特性與應用場景
  • java中的阻塞隊列:介紹java中實現的供開發者使用的阻塞隊列
  • BlockQueue中方法:介紹阻塞隊列的API接口
  • 阻塞隊列的實現原理:具體的例子說明阻塞隊列的實現原理
  • 總結

阻塞隊列簡介

阻塞隊列(BlockingQueue)首先是一個支持先進先出的隊列,與普通的隊列完全相同;
其次是一個支持阻塞操作的隊列,即:

  • 當隊列滿時,會阻塞執行插入操作的線程,直到隊列不滿。
  • 當隊列為空時,會阻塞執行獲取操作的線程,直到隊列不為空。

阻塞隊列用在多線程的場景下,因此阻塞隊列使用了鎖機制來保證同步,這裏使用的可重入鎖;
而對於阻塞與喚醒機制則有與鎖綁定的Condition實現

應用場景:生產者消費者模式

java中的阻塞隊列

java中的阻塞隊列根據容量可以分為有界隊列和無界隊列:

  • 有界隊列:隊列中只能存儲有限個元素,超出后存放元素線程會被阻塞或者失敗。
  • 無界隊列:隊列中可以存儲無限個元素。

java8中提供了7種阻塞隊列阻塞隊列供開發者使用,如下錶:

類名 描述
ArrayBlockingQueue 一個由數組結構組成的有界阻塞隊列
LinkedBlockingQueue 由鏈表結構組成的有界阻塞隊列(默認大小Integer.MAX_VALUE)
PriorityBlockingQueue 支持優先級排序的無界阻塞隊列
DelayQueue 使用優先級隊列實現的延遲無界阻塞隊列
SynchronousQueue 不存儲元素的阻塞隊列,即單個元素的隊列
LinkedTransferQueue 由鏈表結構組成的無界阻塞隊列
LinkedBlockingDeque 由鏈表結構組成的雙向阻塞隊列

另外還有一個在ScheduledThreadPoolExecutor中實現的DelayedWorkQueue阻塞隊列,
但這個阻塞隊列開發者不能使用。它們之間的UML類圖如下圖:

BlockingQueue接口是阻塞隊列對外的訪問接口,所有的阻塞隊列都實現了BlockQueue中的方法

BlockQueue中方法

作為一個隊列的核心方法就是入隊和出隊。由於存在阻塞策略,BlockQueue將出隊入隊的情況分為了四組,每組提供不同的方法:

  • 拋出異常:當隊列滿時,如果再往隊列中插入元素,則拋出IllegalStateException異常;
    當隊列為空時,從隊列中獲取元素則拋出NoSuchElementException異常。

  • 返回特定值(布爾值):當隊列滿時,如果再往隊列中插入元素,則返回false;當隊列為空時,從隊列中獲取元素則返回null。

  • 一直阻塞:當隊列滿時,如果再往隊列中插入元素,阻塞當前線程直到隊列中至少一個被移除或者響應中斷退出;
    當隊列為空時,則阻塞當前線程直到至少一個元素元素入隊或者響應中斷退出。

  • 超時退出:當隊列滿時,如果再往隊列中插入元素,阻塞當前線程直到隊列中至少一個被移除或者達到指定的等待時間退出或者響應中斷退出;
    當隊列為空時,則阻塞當前線程直到至少一個元素元素入隊或者達到指定的等待時間退出或者響應中斷退出。

對於每種情況BlockingQueue提供的方法如下錶:

方法\處理方式 拋出異常 返回特定值(布爾值) 一直阻塞 超時退出
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time.unit)
檢查 element() peek() 不可用 不可用

上述方法一般用於生產者-消費者模型中,是其中的生產和消費操作隊列的核心方法。
除了這些方法,BlockingQueue還提供了一些其他的方法如下錶:

方法名稱 描述
remove(Object o) 從隊列中移除一個指定值
size() 獲取隊列中元素的個數
contains(Object o) 判斷隊列是否包含指定的元素,但是這個元素在這次判斷完可能就會被消費
drainTo(Collection<? super E> c) 將隊列中元素放在給定的集合中,並返回添加的元素個數
drainTo(Collection<? super E> c, int maxElements) 將隊列中元素取maxElements(不超過隊列中元素個數)個放在給定的集合中,並返回添加的元素個數
remainingCapacity() 計算隊列中還可以存放的元素個數
toArray() 以objetc數組的形式獲取隊列中所有的元素
toArray(T[] a) 以給定類型數組的方式獲取隊列中所有的元素
clear() 清空隊列,危險的操作

阻塞隊列的實現原理

阻塞隊列的實現依靠通知模式實現:當生產者向滿了的隊列中添加元素時,會阻塞住生產者,
直到消費者消費了一個隊列中的元素後會通知消費者隊列可用,此時再由生產者向隊列中添加元素。反之亦然。

阻塞隊列的阻塞喚醒依靠Condition——條件隊列來實現。

ArrayBlockingQueue為例說明:

ArrayBlockingQueue的定義:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
   
    /** The queued items */
    //以數組的結構存儲隊列的元素,採用的是循環數組
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    //隊列的隊頭索引
    int takeIndex;

    /** items index for next put, offer, or add */
    //隊列的隊尾索引
    int putIndex;

    /** Number of elements in the queue */
    //隊列中元素的個數
    int count;

    /** Main lock guarding all access */
    //對於ArrayBlockingQueue所有的操作都需要加鎖,
    final ReentrantLock lock;

    /** Condition for waiting takes */
    //條件隊列,當隊列為空時阻塞消費者並在生產者生產後喚醒消費者
    private final Condition notEmpty;

    /** Condition for waiting puts */
    //條件隊列,當隊列滿時阻塞生產者,並在消費者消費隊列后喚醒生產者
    private final Condition notFull;
}

根據類的定義字段可以看到,有兩個Condition條件隊列,猜測以下過程

  • 當隊列為空,消費者試圖消費時應該調用notEmpty.await()方法阻塞,並在生產者生產後調用notEmpty.single()方法
  • 當隊列已滿,生產者試圖放入元素應調用notFull.await()方法阻塞,並在消費者消費隊列后調用notFull.single()方法

向隊列中添加元素put()方法的添加過程。

    /**
    * 向隊列中添加元素
    * 當隊列已滿時需要阻塞當前線程
    * 放入元素后喚醒因隊列為空阻塞的消費者
    */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //當隊列已滿時需要notFull.await()阻塞當前線程
            //offer(e,time,unit)方法就是阻塞的時候加了超時設定
            while (count == items.length)
                notFull.await();
            //放入元素的過程
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
    /**enqueue實際添加元素的方法*/
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //如果條件隊列中存在等待的線程
        //喚醒
        notEmpty.signal();
    }

從隊列中獲取元素take()方法的獲取過程。

    /**
    * 從隊列中獲取元素
    * 當隊列已空時阻塞當前線程
    * 從隊列中消費元素后喚醒等待的生產線程
    */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //隊列為空需要阻塞當前線程
            while (count == 0)
                notEmpty.await();
            //獲取元素的過程
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
    /**dequeue實際消費元素的方法*/
    private E dequeue() {
       // assert lock.getHoldCount() == 1;
       // assert items[takeIndex] != null;
       final Object[] items = this.items;
       @SuppressWarnings("unchecked")
       E x = (E) items[takeIndex];
       items[takeIndex] = null;
       if (++takeIndex == items.length)
           takeIndex = 0;
       count--;
       if (itrs != null)
           itrs.elementDequeued();
       //消費元素后從喚醒阻塞的生產者線程
       notFull.signal();
       return x;
    }

總結

阻塞隊列提供了不同於普通隊列的增加、刪除元素的方法,核心在與隊列滿時阻塞生產者和隊列空時阻塞消費者。
這一阻塞過程依靠與鎖綁定的Condition對象實現。Condition接口的實現在AQS中實現,具體的實現類是
ConditionObject

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

Python面試進階問題,__init__和__new__的區別是什麼?

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天這篇是Python專題的第17篇文章,我們來聊聊Python當中一個新的默認函數__new__。

上一篇當中我們講了如何使用type函數來動態創建Python當中的類,除了type可以完成這一點之外,還有另外一種用法叫做metaclass。原本這一篇應該是繼續元類的內容,講解metaclass的使用。但是metaclass當中用到了一個新的默認函數__new__,關於這個函數大家可能會比較陌生,所以在我們研究metaclass之前,我們先來看看__new__這個函數的用法。

真假構造函數

如果你去面試Python工程師的崗位,面試官問你,請問Python當中的類的構造函數是什麼?

你不假思索,當然是__init__啦!如果你這麼回答,很有可能你就和offer無緣了。因為在Python當中__init__並不是構造函數,__new__才是。是不是有點蒙,多西得(日語:為什麼)?我們不是一直將__init__方法當做構造函數來用的嗎?怎麼又冒出來一個__new__,如果__new__才是構造函數,那麼為什麼我們創建類的時候從來不用它呢?

別著急,我們慢慢來看。首先我們回顧一下__init__的用法,我們隨便寫一段代碼:

class Student:
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

我們一直都是這麼用的,對不對,毫無問題。但是我們換一個問題,我們在Python當中怎麼實現單例(Singleton)的設計模式呢?怎麼樣實現工廠呢?

從這個問題出發,你會發現只使用__init__函數是不可能完成的,因為__init__並不是構造函數,它只是初始化方法。也就是說在調用__init__之前,我們的實例就已經被創建好了,__init__只是為這個實例賦上了一些值。如果我們把創建實例的過程比喻成做一個蛋糕,__init__方法並不是烘焙蛋糕的,只是點綴蛋糕的。那麼顯然,在點綴之前必須先烘焙出一個蛋糕來才行,那麼這個烘焙蛋糕的函數就是__new__。

__new__函數

我們來看下__new__這個函數的定義,我們在使用Python面向對象的時候,一般都不會重構這個函數,而是使用Python提供的默認構造函數,Python默認構造函數的邏輯大概是這樣的:

def __new__(cls, *args, **kwargs):
    return super().__new__(cls, *args, **kwargs)

從代碼可以看得出來,函數當中基本上什麼也沒做,就原封不動地調用了父類的構造函數。這裏隱藏着Python當中類的創建邏輯,是根據繼承關係一級一級創建的。根據邏輯關係,我們可以知道,當我們創建一個實例的時候,實際上是先調用的__new__函數創建實例,然後再調用__init__對實例進行的初始化。我們可以簡單做個實驗:

class Test:
    def __new__(cls):
        print('__new__')
        return object().__new__(cls)
    def __init__(self):
        print('__init__')

當我們創建Test這個類的時候,通過輸出的順序就可以知道Python內部的調用順序。

從結果上來看,和我們的推測完全一樣。

單例模式

那麼我們重載__new__函數可以做什麼呢?一般都是用來完成__init__無法完成的事情,比如前面說的單例模式,通過__new__函數就可以實現。我們來簡單實現一下:

class SingletonObject:
    def __new__(cls, *args, **kwargs):
        if not hasattr(SingletonObject, "_instance"):
            SingletonObject._instance = object.__new__(cls)
        return SingletonObject._instance
    
    def __init__(self):
        pass

當然,如果是在併發場景當中使用,還需要加上線程鎖防止併發問題,但邏輯是一樣的。

除了可以實現一些功能之外,還可以控制實例的創建。因為Python當中是先調用的__new__再調用的__init__,所以如果當調用__new__的時候返回了None,那麼最後得到的結果也是None。通過這個特性,我們可以控制類的創建。比如設置條件,只有在滿足條件的時候才能正確創建實例,否則會返回一個None。

比如我們想要創建一個類,它是一個int,但是不能為0值,我們就可以利用__new__的這個特性來實現:

class NonZero(int):
    def __new__(cls, value):
        return super().__new__(cls, value) if value != 0 else None

那麼當我們用0值來創建它的時候就會得到一個None,而不是一個實例。

工廠模式

理解了__new__函數的特性之後,我們就可以靈活運用了。我們可以用它來實現許多其他的設計模式,比如大名鼎鼎經常使用的工廠模式

所謂的工廠模式是指通過一個接口,根據參數的取值來創建不同的實例。創建過程的邏輯對外封閉,用戶不必關係實現的邏輯。就好比一個工廠可以生產多種零件,用戶並不關心生產的過程,只需要告知需要零件的種類。也因此稱為工廠模式。

比如說我們來創建一系列遊戲的類:

class Last_of_us:
    def play(self):
        print('the Last Of Us is really funny')
        
        
class Uncharted:
    def play(self):
        print('the Uncharted is really funny')
        

class PSGame:
    def play(self):
        print('PS has many games')

然後這個時候我們希望可以通過一個接口根據參數的不同返回不同的遊戲,如果不通過__new__,這段邏輯就只能寫成函數而不能通過面向對象來實現。通過重載__new__我們就可以很方便地用參數來獲取不同類的實例:

class GameFactory:
    games = {'last_of_us': Last_Of_us, 'uncharted': Uncharted}
    def __new__(cls, name):
        if name in cls.games:
            return cls.games[name]()
        else:
            return PSGame()
        

uncharted = GameFactory('uncharted')
last_of_us = GameFactory('last_of_us')

總結

相信看到這裏,關於__new__這個函數的用法應該都能理解了。一般情況下我們是用不到這個函數的,只會在一些特殊的場景下使用。雖然如此,我們學會它並不只是用來實現設計模式,更重要的是可以加深我們對於Python面向對象的理解。

除此之外,另一個經常使用__new__場景是元類。所以今天的這篇文章其實也是為了後面介紹元類的其他用法打基礎。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

超強教程!在樹莓派上構建多節點K8S集群!

在很長一段時間里,我對於在樹莓派上搭建Kubernetes集群極為感興趣。在網絡上找到一些教程並且跟着實操,我已經能夠將Kubernetes安裝在樹莓派上,並在三個Pi集群中工作。然而,在master節點上對於RAM和CPU的要求已經超過了我的樹莓派所能提供的,因此在執行Kubernetes任務時性能並不優異。這也使得就地升級Kubernetes成為不可能。

所以,我看到業界應用最為廣泛的Kubernetes管理平台創建者Rancher Labs推出輕量級Kubernetes發行版K3s時,十分興奮,它專為資源受限場景而設計,還針對ARM處理器進行了優化,這使得在樹莓派上運行Kubernetes更加可行。在本文中,我將使用K3s和樹莓派創建一個Kubernetes集群。

前期準備

要創建本文中的Kubernetes集群,我們需要準備:

  • 至少一個樹莓派(帶有SD卡和電源適配器)
  • 以太網電纜
  • 將我們所有的樹莓派連接在一起的交換機或路由器

我將從網絡上安裝K3s,所以需要通過路由器訪問互聯網。

集群架構

對於這一集群,我們將使用3個樹莓派。第一個樹莓派我把它命名為kmaster,並分配一個靜態IP 192.168.0.50(因為我的本地網絡是192.168.0.0/24)。第一個worker節點(也就是第二個Pi),我們稱它為knode1並分配IP 192.168.0.51。最後一個worker節點,我們稱它為knode2並分配IP 192.168.0.52。

當然如果你的網絡和我不一樣,可以使用你能夠獲得網絡IP。只要在本文使用IP的任何地方替換你自己的值即可。

為了不必再通過IP引用每個節點,我們將其主機名添加到PC上的/ etc / hosts文件中。

echo -e "192.168.0.50\tkmaster" | sudo tee -a /etc/hosts
echo -e "192.168.0.51\tknode1" | sudo tee -a /etc/hosts
echo -e "192.168.0.52\tknode2" | sudo tee -a /etc/hosts

安裝master節點

現在我們已經準備好,可以開始安裝master節點。第一步,安裝最新的Raspbian鏡像。我之前寫過一篇詳細的文章介紹為什麼需要最新的鏡像,感興趣的朋友可以在訪問鏈接查看:

https://carpie.net/articles/headless-pi-with-static-ip-wired-edition

接下來,開始安裝Raspbian,啟用SSH server,為kmaster設置主機名稱並分配靜態IP 192.168.0.50。

既然Raspbian已經在master節點上安裝完畢,讓我們啟用我們的master Pi並通過ssh進入它:

ssh pi@kmaster

現在我們要準備安裝K3s。在master Pi上運行:

curl -sfL https://get.k3s.io | sh -

命令執行完畢之後,我們就有了一個已經設置好的單節點集群並且正在運行中!讓我們檢查一下。依舊是在這個Pi上,運行:

sudo kubectl get nodes

你應該看到類似以下內容:

NAME     STATUS   ROLES    AGE    VERSION
kmaster  Ready    master   2m13s  v1.14.3-k3s.1

提取join token

我們想要添加一對worker節點。在這些節點上安裝K3s,我們需要一個join token。Join token存在於master節點的文件系統上。讓我們複製並將它保存在某個地方,稍後我們可以獲取它:

sudo cat /var/lib/rancher/k3s/server/node-token

安裝worker節點

為兩個worker節點獲取一些SD卡,並在每個節點上安裝Raspbian。對於其中一個,將主機名設置為knode1並分配IP 192.168.0.51。對於另一個,將主機名設置為knode2並分配IP 192.168.0.52。現在,讓我們安裝K3s。

啟動你的第一個worker節點,並通過ssh進入它:

ssh pi@knode1

在這個Pi上,我們將像之前一樣安裝K3s,但我們將給安裝程序額外的參數,讓它了解我們正在安裝一個worker節點並且要加入一個現有集群:

curl -sfL http://get.k3s.io | K3S_URL=https://192.168.0.50:6443 \
K3S_TOKEN=join_token_we_copied_earlier sh -

使用從上個部分提取出來的join token替換join_token_we_copied_earlier。為knode2重複這些步驟。

從我們的PC訪問集群

每當我們要檢查或修改集群時,都必須通過SSH到master節點來運行kubectl,這很煩人。因此,我們像將kubectl放在我們的PC上,但是首先讓我們從master節點獲取所需的配置信息。通過SSH進入kmaster,並運行:

sudo cat /etc/rancher/k3s/k3s.yaml

複製配置信息並返回到你的PC。為配置創建一個目錄:

mkdir ~/.kube

保存複製的配置為~/.kube/config。現在編輯文件並更改:

server: https://localhost:6443

改為:

server: https://kmaster:6443

為了安全起見,請將文件的讀/寫權限限製為你自己:

chmod 600 ~/.kube/config

現在讓我們在我們的PC上安裝kubectl(如果你還沒有)。Kubernetes網站上有針對各種平台執行此操作的說明。由於我正在運行Linux Mint(一個Ubuntu衍生版本),因此我將在此處显示Ubuntu的說明:

sudo apt update && sudo apt install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | \
sudo tee -a /etc/apt/sources.list.d/kubernetes.list
sudo apt update && sudo apt install kubectl

如果你還不熟悉,上述命令為Kubernetes添加了一個Debian倉庫,獲取其GPG密鑰以確保安全,然後更新軟件包列表並安裝kubectl。現在,我將通過標準軟件更新機制獲得有關kubectl更新的通知。

現在我們可以從我們的PC檢查我們的集群,運行:

kubectl get nodes

你應該看到類似以下內容:

NAME     STATUS  ROLES   AGE   VERSION
kmaster  Ready   master  12m   v1.14.3-k3s.1
knode1   Ready   worker  103s  v1.14.3-k3s.1
knode1   Ready   worker  103s  v1.14.3-k3s.1

Congratulations!你現在已經有一個正在工作的3個節點的Kubernetes集群!

使用K3s的附加 bonus

如果你運行kubectl get pods –all-namespaces,你將看到一些Traefik的額外pod。Treafik是一個反向代理和負載均衡器,我們可以使用它從單個入口點將流量引導到我們的集群中。Kubernetes當然也可以安裝Traefik,但不是默認提供的。所以K3s中默認提供Traefik是一個非常棒的設計!

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

程序員過關斬將–作為一個架構師,我是不是應該有很多職責?

每一個程序員都有一個架構夢。

上面其實本質上是一句富有事實哲理的廢話,要不然也不會有這麼多人關注你的公眾號。這些年隨着“企業数字化”轉型的口號,一大批企業奔跑在轉型的路上,希望領先一步對手將企業IT部門從單純的成本中心轉變為業務驅動者,而這個過程中,企業的架構師起着舉足輕重的作用。架構師的工作在很多擼碼的開發者眼中是很一項很神聖的工作,而且富有挑戰性。

但是事物都有兩面性,很多管理者和技術人員都認為架構師的薪酬不符合實際,有很多架構師確實只會用PPT和大幅海報來應付了事,而且會依仗着在公司地位把自己的一些想法強加給公司其他同事,有的架構師甚至會追求一些無關緊要的概念,在高層和底層灌輸一些錯誤的思想,從而導致做出一些不可逆轉的糟糕決策,使公司陷入危險逆境。

很多時候,公司給予架構師這個角色太多的責任,管理者希望他們能在突發性能問題時能快速解決問題,還能推動企業快速轉型,甚至能幫助企業文化的快速建立,作為一個架構師是不是要抗下這些職責呢?

我不是項目經理

架構師的日常工作經常會面臨并行處理多個不同維度的問題,這些問題可能是不同的主題,甚至在做決策的時候也需要考慮人員的分配,項目時間表的排期,需要用的核心技術以及組件等。有很多高層領導喜歡直接在架構師這裏獲取項目的詳細信息以及技術方案,雖然架構師角色涉及這些信息並且很了解這些信息,但是這並不是架構師的職責所在,甚至很多情況下令架構師處於項目經理的尷尬角色。

我不是開發人員

我想很多人看過那篇文章:作為架構師該不該寫代碼?很多架構師是出身於開發人員,這也難怪會出現這樣的疑問。但是,架構師其實和資深開發是兩條不同的職業路線,我認為兩者沒有高低之分。出色的開發人員需要很深的開發功力,需要最終交付出可運行的軟件。而架構師則需要更廣闊的知識面,更好的組織戰略思想,更好的溝通能力。在一個產品的開發流水線上,架構師可能會負責一部分核心代碼的編寫,但是最主要的工作還是保證這條流水線的正常運轉。

我不是救火員

由於架構師這個角色在公司的地位,很多管理者認為架構師要隨時隨地的能分析並解決任何突發的問題,不瞞各位,這種現象在很多大廠依然存在,包括我司(雖然只是一個四線小廠)。如果一個架構師每天都忙着“救火”這種工作,根本沒有時間去做真正的架構工作,真正的架構設計需要思考,是不可能在短短時間內完成的。但是架構師必須接受出現的產品問題,因為這些問題的產生有可能和架構有着直接關係,在很大程度上能反應架構的缺陷或者問題。

寫在最後

架構師作為企業中很重要的一環,在很多重大技術問題中都作為決策者而存在。很難用代碼的多少或者質量來衡量一個架構師的好壞,如果一個系統在正常運行5年後依然能良好運行並且可以承受一定的變更能力,說明這個系統的架構師的工作是很出色的。如果非要給架構師定義一個KPI標準的話,以下這些工作也許能成為一個參考

  1. 定義IT戰略。小到一個系統的組件列表可行性的確定,大到公司技術的發展方向,乃至未來10年公司技術的預測與大膽嘗試。這些技術戰略都需要架構師根據自身經驗來制定。

  2. 落實對IT藍圖的管控,以實現協調一致,降低複雜度,保證公司所有系統有條不紊的正常工作,架構師的工作之一就是要把複雜度降低,化繁為簡,這需要架構師很強的抽象能力。

  3. 關注項目的實際落地情況,並根據項目實施中反饋的問題進行戰略的適當調整。一個合格的架構師從來不會忽略來自實際項目中的問題反饋。

架構師一定要避免和消除那些系統設計中不可逆轉的錯誤決策

來源參考:架構師應該知道的37件事

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

【其他文章推薦】

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

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

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

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

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