疫情肆虐油價跌 石油巨擘被迫減少探勘轉向綠能

摘錄自2020年10月4日中央社報導

疫情重創石油需求及油價,迫使能源巨擘縮減探勘規模,儘管尋找新油田對他們的生計依舊至關重要,他們仍將必須加強綠能投資。

根據研究團體Westwood統計,與疫情前計畫相比,能源業在英國北海水域探勘計畫削減7成,在挪威外海削減3成。

美國石油巨擘埃克森美孚公司(ExxonMobil)共砍了三成的探勘計畫,投資額減少100億美元。歐洲對手埃尼(Eni)、英國石油公司(BP)及艾基挪(Equinor)都有類似行動。根據美國軒尼詩布恩律師事務所(Haynes & Boone)統計,美國今年有30多家石油探勘和生產公司聲請破產。

根據能源諮詢機構「雷斯塔德能源」(Rystad Energy)估計,如果油價持續在當前每桶40美元左右徘徊,到了2022年可能還會有150家類似的公司消失。

能源轉型
國際新聞
石油
綠能

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

日本新潟「新世代熊」不怕人9天7人遇襲 半年內出沒逾700次

摘錄自2020年10月4日中央社報導

日本新潟縣近半年「熊出沒」次數創新高,次數多達逾700次,甚至發生近9天內有7人遭熊襲受傷的意外。專家推測,山上食物缺乏及不怕人的「新世代熊」逐年增加,恐是熊出沒次數創新高的原因。

專家提醒,在北海道、東北及北陸等很多熊棲息的地區,「由於熊分布區域愈來愈廣,大家應該要認知這是一種近在身旁的動物」。新潟縣政府表示,「熊出沒」次數主要是統計熊被目擊到或是被發現出沒痕跡的次數;縣府本月1日已發布「熊出沒警戒警報」。

日本新潟放送(BSN)報導,發生在新潟縣關川村的兩人遭熊攻擊事件,由於案發地附近約700公尺有一間小學,目前小學生都在警察等人護送下上下學。縣府表示,作為熊的食物之一的山毛櫸本季結果數量,是近10年來最少的一季,這些熊應該是下山到人居住的地方覓食,全縣各地都有必要警戒熊出沒。

生物多樣性
國際新聞
日本
人與動物衝突事件簿
人熊衝突

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

聯合國:恢復自然環境的資金缺口 高達7000億美元

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

CocosCreator實現微信排行榜

1. 概述

不管是在現實生活還是當今遊戲中,各式各樣的排名層出不窮。如果我們做好一款遊戲,卻沒有實現排行榜,一定是不完美的。排行榜不僅是玩家了解自己實力的途徑,也是遊戲運營刺激用戶留存的一種途徑。在微信小遊戲中普遍存以下兩種排名

  • 好友關係排名
  • 世界排名

其中好友的排名,需要通過微信子域實現。在子域上下文中,只能調用微信提供相關的api,且數據傳輸只能進不能出。即使在子域中調用雲函數也不行。這個對數據很嚴格,開發略為複雜。但好處也很明顯

  • 無需用戶確認授權就可實現排名
  • 排名信息均為自己好友,刺激效果更明顯

儘管這樣,我們還是先實現世界排行。世界排行需要用戶授權。早期只需要調用wx.authorize就可以實現,現在很不穩定(好像廢棄了)。所以不得不通過生成一個授權按鈕來實現

2. 微信雲開發

微信小遊戲為開發者提供了一部分免費的雲環境。可以實現文件存儲,數據存儲以及通過雲函數實現服務端接口。開通方式也很簡單,這裏不做說明。既然要實現排名,優先選用雲函數來實現對應的api。要實現雲函數,需要在project.config.json文件中通過屬性cloudfunctionRoot指定雲函數目錄。由於,是通過cocoscretor開發,每次構建發布都會清空輸出內容。為了解決人肉複製粘貼,我們需要通過定製小遊戲構建模板實現微信小遊戲所有代碼的管理。小遊戲地心俠士構建模板如下

從圖中,可以看到獲取openid、獲取世界排名、保存用戶授權信息等雲函數都放在cocoscreator代碼環境中。這樣在開發完成后,通過cocoscreator構建發布,對應的雲函數也會一起打包過去

3. 實現世界排行

3.1 獲取玩家openid

首先在構建模板的cloud-functions文件件中,使用npm初始一個名為getOpenId的node項目。初始好以後,運行npm install wx-server-sdk@latest --save。這樣就建立好了一個雲函數的基本框架。

我們在index.js文件,輸入以下代碼

// author:herbert 464884492
// project:地心俠士  獲取用戶openid
const cloud = require('wx-server-sdk')
cloud.init()
exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  return {
    event,
    openid: wxContext.OPENID,
    appid: wxContext.APPID,
    unionid: wxContext.UNIONID,
  }
}

 

調用雲函數時,上下文中便可以得到玩家openid和uninid。玩家進入遊戲就先調用此函數,得到玩家的openid用於後邊更新玩家數據和獲取世界排行的條件。

小遊戲端調用雲函數前,需要初始雲環境。因為採用定製構建模板,所以我們直接在模板的game.js文件末尾初始我的雲環境

// author:herbert 464884492
// 地心俠士 初始雲環境
....
wxDownloader.init();
window.boot();

//初始化雲調用
this.wx.cloud.init({
    traceUser: true,
    env: 'dxxs-dxxs'
});
...

  

後續調用雲函數中,第一步都是要獲取openid,這裏定義一個全局變量將其保存起來,調用方法如下

// author:herbert 464884492
// 地心俠士 玩家openid
private static openId: string = null;
private static initenv() {
  return new Promise((resolve, reject) => {
      if (!this.wx) reject();
      //直接使用本地緩存
      if (this.openId != null) resolve();
      // 調用雲函數獲取
      this.wx.cloud.callFunction({
          name: 'getOpenId',
          complete: res => {
              this.openId = res.result.openid;
              resolve();
          }
      });
  });
}

 

3.2 動態生成授權按鈕

先看下地心俠士布局界面

上圖中可以看到,地心俠士虛擬了一個遊戲操作區域。玩家聚焦到世界排行時,需要渲染一個授權按鈕在確定的位置。需求很簡單,可考慮到移動端多分辨率,這個操作就變得複雜了。需要做屏幕適配。地心俠士採用自適應寬度的適配策略,配置如下圖

遊戲運行時獲取實際分辨率的寬度與設計的寬度相除,變可知道當前寬度變化比列,鍵盤容器九宮格使用了主鍵widget底部111px,高度161px。確定按鈕寬度105px

微信小遊戲以左上角為原點,通過topleft確定位置。然而,cocoscreator以左下角為原點,所以在計算top值時需要用屏幕寬度 – box上邊緣y坐標。適配代碼如下

// author:herbert 464884492
// 地心俠士 動態生成透明授權按鈕
 initUserInfoButton() {
  // 獲取設計尺寸
  let desingSize: cc.Size = cc.view.getDesignResolutionSize();
  //  獲取實際屏幕尺寸
  let screenSize: cc.Size = cc.view.getFrameSize();
  // 獲取寬度倍率
  let widthRate = screenSize.width / desingSize.width;
  // 獲取當前倍率下九宮格鍵盤實際高度
  let halfKcHeight = 161 * widthRate / 2;
  // 獲取當前倍率下確定按鈕實際寬度
  let btnwidth = this.btnKeySuer.width * widthRate;
  WxCloudFun.createUserinfoButton("", 
  // 確定按鈕中心點對應小遊戲left值 (屏幕寬度-確定按鈕實際寬度)/2
  // 定義實際授權按鈕size為105*40,所以還必須加上對應的偏差值
  // 以下代碼中left體現整體適配過程,不考慮中間過程可以直接使用
  // (屏幕寬度-授權按鈕)/2 即可得到left值
  screenSize.width / 2 - 52.5 * widthRate + (btnwidth - 105) / 2, 
  // Canvas 適配策略是 Fit Width,所以Canvas下邊沿不一定就是屏幕邊緣
  // 通過111*widthRate得到具體下沿值,在加上虛擬鍵盤一半高度,可得到中心位置
  // 由於微信原點在左上角,需要保持按鈕處於中心位置,坐標還需要上移一半按鈕高度
  screenSize.height - (111 * widthRate + halfKcHeight + 20),
 () => {
      this.keyCode = cc.macro.KEY.r;
      this.scheduleOnce(async () => {
          this.dlgRank.active = true;
          // 獲取排名數據
          await this.getRankInfo();
      }, 0);
  });
}

 

3.3 獲取用戶頭像昵稱信息

經過上一步驟的適配操作,只要玩家聚焦到【世界排行】,地心俠士虛擬鍵盤的確定按鈕正上方會覆蓋一個透明的userInfoButton,玩家點擊確定就會喚起授權對話框,然後在對應的回調函數就可以完成用戶數據保存操作

// author:herbert 464884492
// 地心俠士 獲取玩家基本信息
 public static createUserinfoButton(text: string, left: number, top: number, cb: Function) {
   this.userInfoButton = this.wx.createUserInfoButton({
      type: 'text',
      text: text,
      style: {
          left: left,
          top: top,
          height: 40,
          width: 105,
          lineHeight: 40,
          textAlign: 'center',
          fontSize: 16,
          backgroundColor: '#ff000000',// 透明
          color: '#ffffff',
      }
   });
   this.userInfoButton.hide();
   this.userInfoButton.onTap((res) => {
     // 將獲取到的用戶數據提交到雲端
      this.wx.cloud.callFunction({
          name: 'putUserinfo',
          data: { ...res.userInfo, openid: this.openId }
      });
      this.hideUserInfoButton();
      cb.call();
   });
   }

 

在代碼中,除了傳入玩家微信信息外。我還額外傳遞進入遊戲時就獲取的openid。正常情況下不需要的,因為,雲函數中天然會告訴你openid。不過,我們在後端使用了got獲取玩家頭像保存到雲端文件存儲中。引入此包后,後端就獲取不到openid了,相當奇怪。對應雲平台雲函數代碼如下

// author:herbert 464884492
// 地心俠士 雲函數保存玩家基本信息
const cloud = require('wx-server-sdk')
const got = require('got')
cloud.init()
// 雲函數入口函數
exports.main = async(event, context) => {
  const {
    nickName,
    avatarUrl,
    gender,
    openid
  } = event;
  let wxContext = cloud.getWXContext();
  // 如果後端拿不到openid就採用前端傳入的openid
  wxContext.OPENID = wxContext.OPENID || openid;
  const log = cloud.logger()
  log.info({
    tip: `正在請求頭像地址[${avatarUrl}]`
  })
  // 獲取頭像數據流
  const stream = await got.stream(avatarUrl);
  let chunks = [];
  let size = 0;
  const body = await (async() => {
    return new Promise((res, reg) => {
      stream.on('data', chunk => {
        chunks.push(chunk)
        size += chunk.length
        log.info({
          tip: `正在讀取圖片流信息:[${chunk.length}]`
        })
      })
      stream.on('end', async() => {
        const body = Buffer.concat(chunks, size)
        log.info({
          tip: `正在保存頭像文件:[${size}]`
        })
        res(body)
      })
    })
  })()
  //保存頭像到雲存儲
  const {
    fileID
  } = await cloud.uploadFile({
    cloudPath: `avatars/${wxContext.OPENID}.jpg`,
    fileContent: body
  })
  // 添加或更新玩家信息到數據庫
  const db = cloud.database()
  const {
    data
  } = await db.collection("dxxs").where({
    _openid: wxContext.OPENID
  }).get()
  const updateData = {
    fileId: fileID,
    nickName: nickName,
    sex: gender == 1 ? '男' : '女',
    avatarUrl: avatarUrl
  }
  if (data.length > 0) {
    log.info({
      tip: `正在修改數據庫信息:[${size}]`
    })
    await db.collection("dxxs").doc(data[0]._id).update({
      data: updateData
    })
  } else {
    log.info({
      tip: `正在添加數據庫信息:[${size}]`
    })
    await db.collection("dxxs").add({
      data: { ...updateData,
        _openid: openid
      }
    })
  }

  return {
    openid: wxContext.OPENID,
    appid: wxContext.APPID,
    unionid: wxContext.UNIONID
  }
}

 

3.4 獲取排行數據

保存完用戶數據后,通過一個回調函數,實現了玩家排名數據獲取。細心的朋友可以在前邊授權按鈕適配的章節看到await this.getRankInfo();這句代碼。後端雲函數就是一個簡單數據查詢。效果圖如下

從上圖可以看到,我實現了三個維度排名,需要在前端需要傳入排名字段。對應代碼如下

// author:herbert 464884492
// 地心俠士 獲取排名信息
 public static async getWorldRanking(field: string = "level") {
     const { result } = await this.wx.cloud.callFunction({
         name: 'getWordRanking',
         data: { order: field }
     });
     return result.ranks;
 }

 

雲函數代碼如下

// author:herbert 464884492
// 地心俠士 雲函數返回排名信息
const cloud = require('wx-server-sdk')
cloud.init()
exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  const db = cloud.database();
  const {
    order = "level"
  } = event;

  const openData = await db.collection("dxxs")
    .orderBy(order, "asc")
    .get()
  const ranks = openData.data.map(item => {
    return {
      openid: item._openid,
      [order]: item[order],
      nickName: item.nickName,
      fileId: item.fileId,
      avatarUrl: item.avatarUrl
    }
  });
  return {
    ranks: ranks,
    openid: wxContext.OPENID,
    appid: wxContext.APPID,
    unionid: wxContext.UNIONID
  }
}

 

4. 總結

  • 微信子域數據很嚴格,數據只進不出。調用雲函數也不行
  • 雲函數中使用http請求,可能會得不到openid
  • 屏幕適配知道定位原則,也可以很簡單
  • avatarUrl通過Sprite現實頭像,需要設置安全域名
  • 目前部分華為手機分享截屏出現黑屏使用canvas.toTempFilePath就可以解決

這裡有一個CoscosCreator遊戲開發群,歡迎喜歡聊技術的朋友加入

 

 

歡迎感興趣的朋友關注我的訂閱號“小院不小”,或點擊下方二維碼關注。我將多年開發中遇到的難點,以及一些有意思的功能,體會都會一一發布到我的訂閱號中

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

【其他文章推薦】

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

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

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

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

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

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

NASH:基於豐富網絡態射和爬山算法的神經網絡架構搜索 | ICLR 2018

論文提出NASH方法來進行神經網絡結構搜索,核心思想與之前的EAS方法類似,使用網絡態射來生成一系列效果一致且繼承權重的複雜子網,本文的網絡態射更豐富,而且僅需要簡單的爬山算法輔助就可以完成搜索,耗時0.5GPU day

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

論文: Simple And Efficient Architecture Search for Convolutional Neural Networks

  • 論文地址:https://arxiv.org/pdf/1711.04528.pdf

Introduction

  論文目標在於大量減少網絡搜索的計算量並保持結果的高性能,核心思想與EAS算法類似,主要貢獻如下:

  • 提供baseline方法,隨機構造網絡並配合SGDR進行訓練,在CIFAR-10上能達到6%-7%的錯誤率,高於大部分NAS方法。
  • 拓展了EAS在網絡態射(network morphisms)上的研究,能夠提供流行的網絡構造block,比如skip connection和BN。
  • 提出基於爬山算法的神經網絡結構搜索NASH,該方法迭代地進行網絡搜索,在每次迭代中,對當前網絡使用一系列網絡態射得到多個新網絡,然後使用餘弦退火進行快速優化,最終得到性能更好的新網絡。在CIFAR-10上,NASH僅需要單卡12小時就可以達到baseline的準確率。

Network Morphism

  $\mathcal{N}(\mathcal{X})$為$\mathcal{X}\in \mathbb{R}^n$上的一系列網絡,網絡態射(network morphism)為映射$M: \mathcal{N}(\mathcal{X}) \times \mathbb{R}^k \to \mathcal{N}(\mathcal{X}) \times \mathbb{R}^j$,從參數為$w\in \mathbb{R}k$的網絡$fw \in \mathcal{N}(\mathcal{X})$轉換為參數為$\tilde{w} \in \mathbb{R}j$的網絡$g\tilde{w} \in \mathcal{N}(\mathcal{X})$,並且滿足公式1,即對於相同的輸入,網絡的輸出不變。

  下面給出幾種標準網絡結構的網絡態射例子:

Network morphism Type I

  將$f^w$進行公式2的替換,$\tilde{w}=(w_i, C, d)$,為了滿足公式1,設定$A=1$和$b=0$,可用於添加全連接層。

  另外一種複雜點的策略如公式3,$\tilde{w}=(w_i, C, d)$,設定$C=A^{-1}$和$d=-Cb$,可用於表達BN層,其中$A$和$b$表示統計結構,$C$和$d$為可學習的$\gamma$和$\beta$。

Network morphism Type II

  假設$f_i{w_i}$可由任何函數$h$表示,即$f_i{w_i}=Ah^{w_h}(x)+b$

  則可以將$f^w$,$w_i = (w_h, A, b)$配合任意函數$\tilde{h}{w_{\tilde{h}}}(x)$根據公式4替換為$\tilde{f}{\tilde{w}i}$,$\tilde{w}=(w_i, w{\tilde{h}}, \tilde{A})$,設定$\tilde{A}=0$。這個態射可以表示為兩種結構:

  • 增加層寬度,將$h(x)$想象為待拓寬的層,設定$\tilde{h}=h$則可以增加兩倍的層寬度。
  • concatenation型的skip connection,假設$h(x)$本身就是一系列層操作$h(x)=h_n(x) \circ \cdots \circ h_0(x)$,設定$\tilde{h}(x)=x$來實現短路連接。

Network morphism Type III

  任何冪等的函數$f_i^{w_i}$都可以通過公式5進行替換,初始化$\tilde{w}_i=w_i$,公式5在無權重的冪等函數上也成立,比如ReLU。

Network morphism Type IV

  任何層$f_i^{w_i}$都可以配合任意函數$h$進行公式6的替換,初始化$\lambda=1$,可用於結合任意函數,特別是非線性函數,也可以用於加入additive型的skip connection。
  此外,不同的網絡態射組合也可以產生新的態射,比如可以通過公式2、3和5在ReLU層後面插入”Conv-BatchNorm-Relu”的網絡結構。

Architecture Search by Network Morphisms

  NASH方法基於爬山算法,先從小網絡開始,對其進行網絡態射生成更大的子網絡,由於公式1的約束,子網的性能與原網絡是一樣的,後續子網進行簡單的訓練看是否有更好的性能,最後選擇性能優異的子網進行重複的操作。

  圖1可視化了NASH方法的一個step,算法1的ApplyNetMorph(model, n)包含n個網絡態射操作,每個為以下方法的隨機一種:

  • 加深網絡,例如添加Conv-BatchNorm-Relu模塊,插入位置和卷積核大小都是隨機的,channel數量跟最近的卷積操作一致。
  • 加寬網絡,例如使用network morphism type II來加寬輸出的channel,加寬比例隨機。
  • 添加從層$i$到層$j$的skup connection,使用network morphism type II或IV,插入位置均隨機選擇。

  由於使用了網絡態射,子網繼承了原網絡的權重且性能一致,NASH方法優勢在於能夠很快的評估子網的性能,論文使用了簡單的爬山算法,當然也可以選擇其它的優化策略。

Experiments

Baslines

Retraining from Scratch

CIFAR-10

CIFAR-100

CONCLUSION

  論文提出NASH方法來進行神經網絡結構搜索,核心思想與之前的EAS方法類似,使用網絡態射來生成一系列效果一致且繼承權重的複雜子網,本文的網絡態射更豐富,而且僅需要簡單的爬山算法輔助就可以完成搜索,耗時0.5GPU day



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

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

【其他文章推薦】

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

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

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

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

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

超詳細實戰教程丨多場景解析如何遷移Rancher Server

本文轉自Rancher Labs

作者介紹

王海龍,Rancher中國社區技術經理,負責Rancher中國技術社區的維護和運營。擁有6年的雲計算領域經驗,經歷了OpenStack到Kubernetes的技術變革,無論底層操作系統Linux,還是虛擬化KVM或是Docker容器技術都有豐富的運維和實踐經驗。

Rancher提供兩種安裝方法,單節點和高可用安裝。單節點安裝允許用戶快速部署適用於短期開發測試為目的的安裝工作。高可用部署明顯更適合Rancher的長期使用。

在實際使用中可能會遇到需要將Rancher Server遷移到其他的節點或local集群去管理的情況。 雖然可以使用最簡單的import集群方式納管,但帶來的問題是後續無法做集群的管理和升級維護,而且一些namespace和project的關聯關係將會消失。所以本文主要介紹如何將Rancher Server遷移到其他節點或local集群。

本文主要針對3個場景去講解如何遷移Rancher Server:

  1. Rancher單節點安裝遷移至其他主機

  2. Rancher單節點安裝遷移至高可用安裝

  3. Rancher高可用安裝遷移至其他Local集群

重要說明

  1. Rancher 官方文檔文檔中並沒有說明支持以下場景的遷移,本文檔只是利用一些Rancher和RKE現有的功能實現遷移。

  2. 如果您在此過程中遇到問題,則應該熟悉Rancher架構/故障排除

  3. 遷移非常危險,遷移前一定剛要做好備份,以免發生意外無法恢復

  4. 您應該熟悉單節點安裝和高可用安裝之間的體繫結構差異

  5. 本文檔基於Rancher 2.4.x測試,其他版本操作可能會略有不同

  6. 本文檔主要講解Rancher Server的遷移,遷移過程中不會影響業務集群的使用

準備集群直連 kubeconfig 配置文件

默認情況下, Rancher UI 上複製的 kubeconfig 通過cluster agent代理連接到 kubernetes 集群。變更 Rancher Server會導致cluster agent無法連接 Rancher Server,從而導致kubectl無法使用 Rancher UI 上複製的 kubeconfig 去操作 kubernetes 集群。但可以使用kubectl –context <CLUSTER_NAME>-fqdn 直接連接kubernetes集群進行操作。所以在執行遷移之前,請準備好所有集群的直連 kubeconfig 配置文件。

Rancher v2.2.2以及之後的版本,可以直接從UI上下載kubeconfig文件。

Rancher v2.2.2之前的版本,請參考:恢復 kubectl 配置文件

場景1:Rancher單節點安裝遷移至其他主機

Rancher單節點安裝遷移至其他主機,只需要將舊集群Rancher Server容器的/var/lib/rancher目錄打包,然後替換到新Rancher Server對應的目錄,最後啟動新Rancher Server容器之後再更新agent相關配置即可。

1. Rancher 單節點安裝

提示:以下步驟創建用於演示遷移的 Rancher 單節點環境,如果您需要遷移正式環境可以跳過此步驟。

執行以下 docker 命令運行單節點 Rancher Server 服務

docker run -itd -p 80:80 -p 443:443 --restart=unless-stopped rancher/rancher:v2.4.3

等容器初始化完成后,通過節點 IP 訪問 Rancher Server UI,設置密碼並登錄。

2. 創建自定義集群

提示: 以下步驟創建用於演示的業務集群,用來驗證 Rancher 遷移后數據是否丟失,如果您需要遷移正式環境可以跳過此步驟。

登錄 Rancher UI 后,添加一個自定義集群

授權集群訪問地址設置為啟用,FQDN 和證書可以不用填寫。

注意:

這一步很關鍵。因為Rancher 遷移后,地址或者 token 或者證書的變更,將會導致 agent 無法連接 Rancher Server。遷移后,需要通過 kubectl 去編輯配置文件更新一些 agent 相關的參數。默認 UI 上的 kubeconfig文件是通過 agent 代理連接到 Kubernetes,如果 agent 無法連接 Rancher Server,則通過這個 kubeconfig 文件也無法訪問 Kubernetes 集群。開啟授權集群訪問地址功能會生成多個 Contexts Cluster,這些 Contexts Cluster 是直連 Kubernetes,不通過 agent 代理。如果業務集群未開啟這個功能,可以通過編輯集群來開啟這個功能。

點擊下一步,根據預先分配的節點角色選擇需要的角色,然後複製命令到主機終端執行。

集群部署完成后,進入集群首頁,點擊kubeconfig文件按鈕。在彈窗頁面中複製 kubeconfg 配置文件備用。

3.部署測試應用

部署一個nginx workload。再從應用商店部署一個測試應用。

4. 備份單節點Racher Server數據

docker create --volumes-from <RANCHER_CONTAINER_NAME> --name rancher-data-<DATE> rancher/rancher:<RANCHER_CONTAINER_TAG>

docker run  --volumes-from rancher-data-<DATE> -v $PWD:/backup:z busybox tar pzcvf /backup/rancher-data-backup-<RANCHER_VERSION>-<DATE>.tar.gz -C /var/lib rancher

詳細請參考Rancher中文官網單節點備份指南。

5. 將生成的rancher-data-backup-<RANCHER_VERSION>-<DATE>.tar.gz複製到新的Rancher Server節點

scp rancher-data-backup-<RANCHER_VERSION>-<DATE>.tar.gz root@<new_rancher_ip>:/opt/

6. 使用備份數據啟動新節點Rancher Server

如果原Rancher Server通過使用已有的自簽名證書或使用已有的可信證書安裝,遷移時,需要將證書一起複制到新Rancher Server,使用相同啟動命令掛載證書和備份數據啟動Rancher Server。

cd /opt && tar -xvz -f rancher-data-backup-<RANCHER_VERSION>-<DATE>.tar.gz

docker run -itd -p 80:80 -p 443:443 -v /opt/rancher:/var/lib/rancher --restart=unless-stopped rancher/rancher:v2.4.3

7. 更新Rancher Server IP或域名

注意:

如果您的環境使用自簽名證書或Let’s Encrypt 證書,並且配置域名訪問Rancher Server。遷移之後集群狀態為Active,請直接跳到第9步去驗證集群。

此時訪問新的Rancher Server就可以看見已經被管理的Kubernetes集群了,但此時集群狀態是unavailable,因為agent還連的是舊Rancher Server所以需要更新agent信息。

  • 依次訪問全局 > 系統設置,頁面往下翻找到server-url文件

  • 單擊右側的省略號菜單,選擇升級

  • 修改server-url地址為新Rancher Server的地址

  • 保存

8. 更新agent配置

通過新域名或IP登錄 Rancher Server;

通過瀏覽器地址欄查詢集群ID, c/後面以c開頭的字段即為集群 ID,本例的集群ID為c-4wzvf

  • 訪問https://<新的server_url>/v3/clusters/<集群ID>/clusterregistrationtokens頁面;

  • 打開clusterRegistrationTokens頁面后,定位到data字段;找到insecureCommand字段,複製 YAML 連接備用;

可能會有多組"baseType": "clusterRegistrationToken",如上圖。這種情況以createdTS最大、時間最新的一組為準,一般是最後一組。

使用kubectl工具,通過前文中準備的直連kubeconfig配置文件和上面步驟中獲取的 YAML 文件,執行以下命令更新agent相關配置。

curl --insecure -sfL <替換為上面步驟獲取的YAML文件鏈接> | kubectl --context=xxx  apply -f -

關於--context=xxx說明請參考直接使用下游集群進行身份驗證。

9. 驗證

過一會,集群變為Active狀態,然後驗證我們之前部署的應用是否可用。

場景2:Rancher單節點安裝遷移至高可用安裝

從單個節點遷移到Rancher的高可用性安裝的過程可以概括為以下幾個步驟:

在Rancher單節點實例上:

  1. 備份Rancher單節點容器

  2. 備份etcd快照

  3. 停止舊的Rancher單節點容器

在RKE Local集群上:

  1. 使用RKE啟動Rancher Local集群

  2. 利用rke etcd snapshot-restore,將單節點備份的etcd快照恢復到RKE HA

  3. 在RKE Local集群中安裝Rancher

  4. 更新Local集群和業務集群的相關配置,使agent可以連接到正確的Rancher Server

在單節點Rancher Server上操作

1. Rancher 單節點安裝

提示: 以下步驟創建用於演示遷移的 Rancher 環境,如果您需要遷移正式環境可以跳過此步驟。

執行以下 docker 命令運行單節點 Rancher Server 服務

docker run -itd -p 80:80 -p 443:443 --restart=unless-stopped rancher/rancher:v2.4.3

等容器初始化完成后,通過節點 IP 訪問 Rancher Server UI,設置密碼並登錄。

2. 創建自定義集群

提示: 以下步驟創建用於演示的業務集群,用來驗證 Rancher 遷移后數據是否丟失,如果您需要遷移正式環境可以跳過此步驟。

登錄 Rancher UI 后,添加一個自定義集群

授權集群訪問地址設置為啟用,FQDN 和證書可以不用填寫。

注意: 這一步很關鍵。因為Rancher 遷移后,地址或者 token 或者證書的變更,將會導致 agent 無法連接 Rancher Server。遷移后,需要通過 kubectl 去編輯配置文件更新一些 agent 相關的參數。默認 UI 上的 kubeconfig文件是通過 agent 代理連接到 Kubernetes,如果 agent 無法連接 Rancher Server,則通過這個 kubeconfig 文件也無法訪問 Kubernetes 集群。開啟授權集群訪問地址功能會生成多個 Contexts Cluster,這些 Contexts Cluster 是直連 Kubernetes,不通過 agent 代理。如果業務集群未開啟這個功能,可以通過編輯集群來開啟這個功能。

點擊 下一步 ,根據預先分配的節點角色選擇需要的角色,然後複製命令到主機終端執行。

集群部署完成后,進入集群首頁,點擊kubeconfig文件按鈕。在彈窗頁面中複製 kubeconfg 配置文件備用。

3. 部署測試應用

部署一個nginx workload。再從應用商店部署一個測試應用。

4. 創建將單節點etcd快照

docker exec -it <RANCHER_CONTAINER_NAME> bash

root@78efdcbe08a6:/# cd /

root@78efdcbe08a6:/# ETCDCTL_API=3 etcdctl snapshot save single-node-etcd-snapshot

root@78efdcbe08a6:/# exit

docker cp <RANCHER_CONTAINER_NAME>:/single-node-etcd-snapshot .

5. 關閉單節點Rancher Server

docker stop <RANCHER_CONTAINER_NAME>

在RKE Local集群上

1. RKE部署Local Kubernetes 集群

根據RKE示例配置 創建 RKE 配置文件 cluster.yml:

nodes:
- address: 99.79.49.94
    internal_address: 172.31.13.209
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 35.183.174.120
    internal_address: 172.31.8.28
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 15.223.49.238
    internal_address: 172.31.0.199
    user: ubuntu
    role: [controlplane, worker, etcd]

執行 rke 命令創建 Local Kubernetes 集群

rke up --config cluster.yml

檢查 Kubernetes 集群運行狀態

使用kubectl檢查節點狀態,確認節點狀態為Ready

kubectl get nodes

NAME             STATUS   ROLES                      AGE   VERSION
15.223.49.238    Ready    controlplane,etcd,worker   93s   v1.17.6
35.183.174.120   Ready    controlplane,etcd,worker   92s   v1.17.6
99.79.49.94      Ready    controlplane,etcd,worker   93s   v1.17.6

檢查所有必需的 Pod 和容器是否狀況良好,然後可以繼續進行

kubectl get pods --all-namespaces

NAMESPACE       NAME                                      READY   STATUS      RESTARTS   AGE
ingress-nginx   default-http-backend-67cf578fc4-9vjq4     1/1     Running     0          67s
ingress-nginx   nginx-ingress-controller-8g7kq            1/1     Running     0          67s
ingress-nginx   nginx-ingress-controller-8jvsd            1/1     Running     0          67s
ingress-nginx   nginx-ingress-controller-lrt57            1/1     Running     0          67s
kube-system     canal-68j4r                               2/2     Running     0          100s
kube-system     canal-ff4qg                               2/2     Running     0          100s
kube-system     canal-wl9hd                               2/2     Running     0          100s
kube-system     coredns-7c5566588d-bhbmm                  1/1     Running     0          64s
kube-system     coredns-7c5566588d-rhjpv                  1/1     Running     0          87s
kube-system     coredns-autoscaler-65bfc8d47d-tq4gj       1/1     Running     0          86s
kube-system     metrics-server-6b55c64f86-vg7qs           1/1     Running     0          79s
kube-system     rke-coredns-addon-deploy-job-fr2bx        0/1     Completed   0          92s
kube-system     rke-ingress-controller-deploy-job-vksrk   0/1     Completed   0          72s
kube-system     rke-metrics-addon-deploy-job-d9hlv        0/1     Completed   0          82s
kube-system     rke-network-plugin-deploy-job-kf8bn       0/1     Completed   0          103s

2. 將生成的單節點etcd快照從Rancher單節點實例傳到RKE Local集群節點上

在RKE HA Local節點上創建一個/opt/rke/etcd-snapshots目錄,並將single-node-etcd-snapshot文件複製到該目錄:

mkdir -p /opt/rke/etcd-snapshots
scp root@<old_rancher_ip>:/root/single-node-etcd-snapshot /opt/rke/etcd-snapshots

3. 使用RKE將單節點etcd快照還原到新的HA節點

rke etcd snapshot-restore --name single-node-etcd-snapshot --config cluster.yml

4. Rancher HA 安裝

參考安裝文檔安裝 Rancher HA。

5. 為Rancher HA配置NGINX 負載均衡

參考NGINX 配置示例為Rancher HA配置負載均衡。

Nginx 配置:

worker_processes 4;
worker_rlimit_nofile 40000;

events {
    worker_connections 8192;
}

stream {
    upstream rancher_servers_http {
        least_conn;
        server 172.31.11.95:80 max_fails=3 fail_timeout=5s;
        server 172.31.0.201:80 max_fails=3 fail_timeout=5s;
        server 172.31.15.236:80 max_fails=3 fail_timeout=5s;
    }
    server {
        listen 80;
        proxy_pass rancher_servers_http;
    }

    upstream rancher_servers_https {
        least_conn;
        server 172.31.11.95:443 max_fails=3 fail_timeout=5s;
        server 172.31.0.201:443 max_fails=3 fail_timeout=5s;
        server 172.31.15.236:443 max_fails=3 fail_timeout=5s;
    }
    server {
        listen     443;
        proxy_pass rancher_servers_https;
    }
}

Nginx啟動后,我們就可以通過配置的域名/IP去訪問Rancher UI。可以看到業務集群demoUnavailable狀態,local集群雖然為Active,但cluster-agentnode-agent均啟動失敗。

這兩種情況都是因為agent依然連接的舊的Rancher Server。

6. 更新Rancher Server IP或域名

  • 依次訪問全局 > 系統設置,頁面往下翻找到server-url文件

  • 單擊右側的省略號菜單,選擇升級

  • 修改server-url地址為新Rancher server的地址

  • 保存

7. 更新local集群和業務集群的agent配置

通過新域名或IP登錄 Rancher Server;

通過瀏覽器地址欄查詢集群ID, c/後面以c開頭的字段即為集群 ID,本例的集群ID為c-hftcn

訪問https://<新的server_url>/v3/clusters/<集群ID>/clusterregistrationtokens頁面;

打開clusterRegistrationTokens頁面后,定位到data字段;找到insecureCommand字段,複製 YAML 連接備用;

可能會有多組"baseType": "clusterRegistrationToken",如上圖。這種情況以createdTS最大、時間最新的一組為準,一般是最後一組。

使用kubectl工具,通過前文中準備的直連kubeconfig配置文件和上面步驟中獲取的 YAML 文件,執行以下命令更新agent相關配置。

注意:

更新local集群和業務集群使用的kubeconfig是不同的,請針對不通集群選擇需要的kubeconfig。

關於--context=xxx說明請參考直接使用下游集群進行身份驗證。

curl --insecure -sfL <替換為上面步驟獲取的YAML文件鏈接> | kubectl --context=xxx  apply -f -

業務集群agent更新成功后,使用相同的方法更新local集群agent配置。

9. 驗證

過一會,localdemo集群都變為Active狀態:

Local集群的cluster-agentnode-agent啟動成功

Demo集群的cluster-agentnode-agent啟動成功

然後驗證我們之前部署的應用是否可用。

場景3:Rancehr高可用安裝遷移至其他Local集群

Rancehr高可用安裝遷移至其他Local集群,可以藉助rke的更新功能完成。通過rke將原來的3節點local集群擴展成6個節點,此時etcd數據將自動同步到local集群內的6個節點上,然後再使用rke將原有的3台節點移除,再次更新。這樣就將Rancher Server可以平滑的遷移到新的Rancher local集群。

1. RKE部署Local Kubernetes 集群

根據RKE示例配置創建 RKE 配置文件 cluster.yml:

nodes:
- address: 3.96.52.186
    internal_address: 172.31.11.95
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 35.183.186.213
    internal_address: 172.31.0.201
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 35.183.130.12
    internal_address: 172.31.15.236
    user: ubuntu
    role: [controlplane, worker, etcd]

執行 rke 命令創建 Local Kubernetes 集群

rke up --config cluster.yml

檢查 Kubernetes 集群運行狀態

使用kubectl檢查節點狀態,確認節點狀態為Ready

kubectl get nodes
NAME             STATUS   ROLES                      AGE   VERSION
3.96.52.186      Ready    controlplane,etcd,worker   71s   v1.17.6
35.183.130.12    Ready    controlplane,etcd,worker   72s   v1.17.6
35.183.186.213   Ready    controlplane,etcd,worker   72s   v1.17.6

檢查所有必需的 Pod 和容器是否狀況良好,然後可以繼續進行

kubectl get pods --all-namespaces

NAMESPACE       NAME                                      READY   STATUS      RESTARTS   AGE
ingress-nginx   default-http-backend-67cf578fc4-gnt5c     1/1     Running     0          72s
ingress-nginx   nginx-ingress-controller-47p4b            1/1     Running     0          72s
ingress-nginx   nginx-ingress-controller-85284            1/1     Running     0          72s
ingress-nginx   nginx-ingress-controller-9qbdz            1/1     Running     0          72s
kube-system     canal-9bx8k                               2/2     Running     0          97s
kube-system     canal-l2fjb                               2/2     Running     0          97s
kube-system     canal-v7fzs                               2/2     Running     0          97s
kube-system     coredns-7c5566588d-7kv7b                  1/1     Running     0          67s
kube-system     coredns-7c5566588d-t4jfm                  1/1     Running     0          90s
kube-system     coredns-autoscaler-65bfc8d47d-vnrzc       1/1     Running     0          90s
kube-system     metrics-server-6b55c64f86-r4p8w           1/1     Running     0          79s
kube-system     rke-coredns-addon-deploy-job-lx667        0/1     Completed   0          94s
kube-system     rke-ingress-controller-deploy-job-r2nw5   0/1     Completed   0          74s
kube-system     rke-metrics-addon-deploy-job-4bq76        0/1     Completed   0          84s
kube-system     rke-network-plugin-deploy-job-gjpm8       0/1     Completed   0          99s

2. Rancher HA 安裝

參考安裝文檔安裝 Rancher HA。

3. 為Rancher HA配置NGINX 負載均衡

參考NGINX 配置示例為Rancher HA配置負載均衡。

Nginx 配置:

worker_processes 4;
worker_rlimit_nofile 40000;

events {
    worker_connections 8192;
}

stream {
    upstream rancher_servers_http {
        least_conn;
        server 172.31.11.95:80 max_fails=3 fail_timeout=5s;
        server 172.31.0.201:80 max_fails=3 fail_timeout=5s;
        server 172.31.15.236:80 max_fails=3 fail_timeout=5s;
    }
    server {
        listen 80;
        proxy_pass rancher_servers_http;
    }

    upstream rancher_servers_https {
        least_conn;
        server 172.31.11.95:443 max_fails=3 fail_timeout=5s;
        server 172.31.0.201:443 max_fails=3 fail_timeout=5s;
        server 172.31.15.236:443 max_fails=3 fail_timeout=5s;
    }
    server {
        listen     443;
        proxy_pass rancher_servers_https;
    }
}

Nginx啟動后,我們就可以通過配置的域名/IP去訪問Rancher UI。可以導航到local->Nodes 查看到local集群三個節點的狀態:

4. 部署測試集群及應用

添加測試集群,Node Role同時選中etcdControl PlaneWorker

等待測試集群添加成功后,部署一個nginx workload。再從應用商店部署一個測試應用。

5. 將新集群的節點添加到Local集群

修改剛才創建local集群所使用的rke配置文件,增加新集群的配置。

cluster.yml:

nodes:
- address: 3.96.52.186
    internal_address: 172.31.11.95
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 35.183.186.213
    internal_address: 172.31.0.201
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 35.183.130.12
    internal_address: 172.31.15.236
    user: ubuntu
    role: [controlplane, worker, etcd]

# 以下內容為新增節點的配置
- address: 52.60.116.56
    internal_address: 172.31.14.146
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 99.79.9.244
    internal_address: 172.31.15.215
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 15.223.77.84
    internal_address: 172.31.8.64
    user: ubuntu
    role: [controlplane, worker, etcd]

更新集群,將local集群節點擴展到6個

rke up --cluster.yml

檢查 Kubernetes 集群運行狀態

使用kubectl測試您的連通性,並確認原節點(3.96.52.186、35.183.186.213、35.183.130.12)和新增節點(52.60.116.56、99.79.9.244、15.223.77.84)都處於Ready狀態

kubectl get nodes
NAME             STATUS   ROLES                      AGE    VERSION
15.223.77.84     Ready    controlplane,etcd,worker   33s    v1.17.6
3.96.52.186      Ready    controlplane,etcd,worker   88m    v1.17.6
35.183.130.12    Ready    controlplane,etcd,worker   89m    v1.17.6
35.183.186.213   Ready    controlplane,etcd,worker   89m    v1.17.6
52.60.116.56     Ready    controlplane,etcd,worker   101s   v1.17.6
99.79.9.244      Ready    controlplane,etcd,worker   67s    v1.17.6

檢查所有必需的 Pod 和容器是否狀況良好,然後可以繼續進行

kubectl get pods --all-namespaces

NAMESPACE       NAME                                      READY   STATUS      RESTARTS   AGE
cattle-system   cattle-cluster-agent-68898b5c4d-lkz5m     1/1     Running     0          46m
cattle-system   cattle-node-agent-9xrbs                   1/1     Running     0          109s
cattle-system   cattle-node-agent-lvdlf                   1/1     Running     0          46m
cattle-system   cattle-node-agent-mnk76                   1/1     Running     0          46m
cattle-system   cattle-node-agent-qfwcm                   1/1     Running     0          75s
cattle-system   cattle-node-agent-tk66h                   1/1     Running     0          2m23s
cattle-system   cattle-node-agent-v2vpf                   1/1     Running     0          46m
cattle-system   rancher-749fd64664-8cg4w                  1/1     Running     1          58m
cattle-system   rancher-749fd64664-fms8x                  1/1     Running     1          58m
cattle-system   rancher-749fd64664-rb5pt                  1/1     Running     1          58m
ingress-nginx   default-http-backend-67cf578fc4-gnt5c     1/1     Running     0          89m
ingress-nginx   nginx-ingress-controller-44c5z            1/1     Running     0          61s
ingress-nginx   nginx-ingress-controller-47p4b            1/1     Running     0          89m
ingress-nginx   nginx-ingress-controller-85284            1/1     Running     0          89m
ingress-nginx   nginx-ingress-controller-9qbdz            1/1     Running     0          89m
ingress-nginx   nginx-ingress-controller-kp7p6            1/1     Running     0          61s
ingress-nginx   nginx-ingress-controller-tfjrw            1/1     Running     0          61s
kube-system     canal-9bx8k                               2/2     Running     0          89m
kube-system     canal-fqrqv                               2/2     Running     0          109s
kube-system     canal-kkj7q                               2/2     Running     0          75s
kube-system     canal-l2fjb                               2/2     Running     0          89m
kube-system     canal-v7fzs                               2/2     Running     0          89m
kube-system     canal-w7t58                               2/2     Running     0          2m23s
kube-system     coredns-7c5566588d-7kv7b                  1/1     Running     0          89m
kube-system     coredns-7c5566588d-t4jfm                  1/1     Running     0          89m
kube-system     coredns-autoscaler-65bfc8d47d-vnrzc       1/1     Running     0          89m
kube-system     metrics-server-6b55c64f86-r4p8w           1/1     Running     0          89m
kube-system     rke-coredns-addon-deploy-job-lx667        0/1     Completed   0          89m
kube-system     rke-ingress-controller-deploy-job-r2nw5   0/1     Completed   0          89m
kube-system     rke-metrics-addon-deploy-job-4bq76        0/1     Completed   0          89m
kube-system     rke-network-plugin-deploy-job-gjpm8       0/1     Completed   0          89m

從上面的信息可以確認現在local集群已經擴展到6個,並且所有workload均正常運行。

6. 再次更新集群,剔除掉原Local集群節點

再次修改local集群所使用的rke配置文件,將原local集群節點配置註釋掉。

cluster.yml:

nodes:
#  - address: 3.96.52.186
#    internal_address: 172.31.11.95
#    user: ubuntu
#    role: [controlplane, worker, etcd]
#  - address: 35.183.186.213
#    internal_address: 172.31.0.201
#    user: ubuntu
#    role: [controlplane, worker, etcd]
#  - address: 35.183.130.12
#    internal_address: 172.31.15.236
#    user: ubuntu
#    role: [controlplane, worker, etcd]
# 以下內容為新增節點
- address: 52.60.116.56
    internal_address: 172.31.14.146
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 99.79.9.244
    internal_address: 172.31.15.215
    user: ubuntu
    role: [controlplane, worker, etcd]
- address: 15.223.77.84
    internal_address: 172.31.8.64
    user: ubuntu
    role: [controlplane, worker, etcd]

更新集群,完成遷移。

rke up --cluster.yml

檢查 Kubernetes 集群運行狀態

使用kubectl檢查節點狀態為Ready,可以看到local集群的節點已經替換成了以下3個:

kubectl get nodes
NAME           STATUS   ROLES                      AGE   VERSION
15.223.77.84   Ready    controlplane,etcd,worker   11m   v1.17.6
52.60.116.56   Ready    controlplane,etcd,worker   13m   v1.17.6
99.79.9.244    Ready    controlplane,etcd,worker   12m   v1.17.6

檢查所有必需的 Pod 和容器是否狀況良好,然後可以繼續進行

kubectl get pods --all-namespaces

NAMESPACE       NAME                                    READY   STATUS    RESTARTS   AGE
cattle-system   cattle-cluster-agent-68898b5c4d-tm6db   1/1     Running   3          3m14s
cattle-system   cattle-node-agent-9xrbs                 1/1     Running   0          14m
cattle-system   cattle-node-agent-qfwcm                 1/1     Running   0          14m
cattle-system   cattle-node-agent-tk66h                 1/1     Running   0          15m
cattle-system   rancher-749fd64664-47jw2                1/1     Running   0          3m14s
cattle-system   rancher-749fd64664-jpqdd                1/1     Running   0          3m14s
cattle-system   rancher-749fd64664-xn6js                1/1     Running   0          3m14s
ingress-nginx   default-http-backend-67cf578fc4-4668g   1/1     Running   0          3m14s
ingress-nginx   nginx-ingress-controller-44c5z          1/1     Running   0          13m
ingress-nginx   nginx-ingress-controller-kp7p6          1/1     Running   0          13m
ingress-nginx   nginx-ingress-controller-tfjrw          1/1     Running   0          13m
kube-system     canal-fqrqv                             2/2     Running   0          14m
kube-system     canal-kkj7q                             2/2     Running   0          14m
kube-system     canal-w7t58                             2/2     Running   0          15m
kube-system     coredns-7c5566588d-nmtrn                1/1     Running   0          3m13s
kube-system     coredns-7c5566588d-q6hlb                1/1     Running   0          3m13s
kube-system     coredns-autoscaler-65bfc8d47d-rx7fm     1/1     Running   0          3m14s
kube-system     metrics-server-6b55c64f86-mcx9z         1/1     Running   0          3m14s

從上面的信息可以確認現在local集群已經遷移成功,並且所有workload均正常運行。

修改nginx負載均衡配置,將新節點的信息更新到nginx配置文件中

worker_processes 4;
worker_rlimit_nofile 40000;

events {
    worker_connections 8192;
}

stream {
    upstream rancher_servers_http {
        least_conn;
        server 172.31.14.146:80 max_fails=3 fail_timeout=5s;
        server 172.31.8.64:80 max_fails=3 fail_timeout=5s;
        server 172.31.15.215:80 max_fails=3 fail_timeout=5s;
    }
    server {
        listen 80;
        proxy_pass rancher_servers_http;
    }

    upstream rancher_servers_https {
        least_conn;
        server 172.31.14.146:443 max_fails=3 fail_timeout=5s;
        server 172.31.8.64:443 max_fails=3 fail_timeout=5s;
        server 172.31.15.215:443 max_fails=3 fail_timeout=5s;
    }
    server {
        listen     443;
        proxy_pass rancher_servers_https;
    }

}

7. 驗證

確認local集群和業務集群狀態為Active

確認Local集群節點已被替換

原集群節點IP分別為:3.96.52.186、35.183.186.213、35.183.130.12

然後驗證我們之前部署的應用是否可用。

總 結

開源一直是Rancher的產品理念,我們也一向重視與開源社區用戶的交流,為此創建了20個微信交流群。本篇文章的誕生源於和社區用戶的多次交流,發現許多Rancher用戶都有類似的問題。於是,我總結了三個場景並經過反覆測試,最終完成這篇教程。我們也十分歡迎各位Rancher用戶以各種形式分享自己的使用經驗,一起共建愉快的開源社區。

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

Scrum Master教你四招,瓦解團隊內部刺頭

摘要:《Scrum精髓》一書中將Scrum Master的職責總結為六類:敏捷教練,服務型領導,“保護傘”,“清道夫”,過程權威,“變革代言人”。作為“保護傘“,Scrum Master應該保護團隊免受任何干擾,當然也包括團隊內衝突,成員關係等。

背景

拜訪企業的過程中,不少企業領導提到過一個相似的問題:“我們團隊有個人平時總是和我(或者其他成員)對着干,把團隊氛圍搞得很差,Scrum Master應該怎麼引導他們,讓他們好好工作?”

本文就針對這樣的問題來聊聊,團隊中遇到“刺頭”應該怎麼辦?

問題分析

學生時代班級里總有幾個刺頭,他們惹是生非,擾亂課堂紀律——課堂上講話,接老師話茬,讓老師很是頭疼。企業中,很多團隊也有一兩個成員,他們難以合作,常常捅婁子,給團隊交付帶來不良影響,令管理層也很頭疼。

在交流過程中,筆者從不同人口中了解到不同類型的刺頭,分別有:

  • 技術骨幹

技術骨幹通常掌握着項目的核心技術,是開發交付不可或缺的關鍵角色,項目少了他很可能會產生較大的影響。有的技術骨幹對其他人,甚至是領導都愛答不理,和團隊很難溝通協作。

  • 原地踏步的“老人”

同一時期入職的員工,有的人做上了項目經理,有的人還在原地踏步寫一些基礎代碼

做同一個項目的團隊成員,有的人績效一直是A,獎金豐厚,有的人一直作為“吊車尾”混跡於團隊尾端

兩種情況的後者會存在一部分人表現出對工作消極懈怠,在團隊內傳播負面能量。比如經常抱怨、不滿、碎碎念,對其他團隊成員尤其是新人加以冷嘲熱諷。

  • 有離職打算的員工

大多數人做事講究善始善終,離職時都會做好交接工作。但有小部分人在離職時喜歡破罐子破摔——反正都要走了,還遵守什麼規矩,於是不認真交接,消極怠工,就等着離職辦手續。

  • 工作經驗少的“新人”

正所謂”初生牛犢不怕虎“,有些應屆畢業生在學校做過幾個項目,便感覺自己懂得很多,比其他同學高一等,步入職場后,對其他同事不尊重;還有一些工作經驗少的年輕員工做過幾個技術框架簡單的項目,便感覺自己的技術能力很強,心裏有了“開發就那麼點東西”的錯誤想法,進而輕視日常工作,不服從領導安排。

除個別極端情況比如天生性格不好,大多數刺頭不配合都是有原因的。

以上幾種情況總結起來有如下幾點原因:

  • 性格外冷內熱

筆者接觸過的技術骨幹性格很多都是外冷內熱——他們本身是樂於助人的,但由於他們日常工作很忙或者工作太投入,沒有太多精力去顧及自己對別人的態度,所以給人造成愛答不理的錯覺。

  • 自負

有少數技術牛人因為自己能力突出而變得自負,看不起公司其他人。在這部分人看來團隊成員都應該很輕鬆的完成任務,團隊成員問的問題都沒有技術含量,懶得回答;而且這部分人技術至上,很多公司管理層因為不會開發技術而被他們輕視。

和他們類似,有些年輕員工因為閱歷有限,沒有接觸過複雜的場景,做兩個項目就以為自己能力很突出,表現同上。

  • 對工作存在怨念

做同樣工作,評價有的高有的低,如果不懂得正視自己的缺點,那獲得低評價的人難免心中不服。經常如此,就會感覺自己懷才不遇,對周遭事物存在怨念。

如果員工在一家公司工作的很開心,通常離職時也會站好最後一班崗;離職前搗蛋的員工則多數是因為工作憋了一肚子氣或者和團隊鬧不愉快,離職前這幾天正好把憋得氣全撒出去。

常見的怨念來源還有工作中與其他成員的衝突未得到解決,個人未被團隊關注等。

針對這些問題,Scrum Master應該如何引導呢?

解決措施

很多人對於Scrum Master的理解是Scrum Master的工作就是幫助團隊召開各種會議,其實這是對Scrum Master工作的一種誤解。Scrum Master除了組織團隊召開會議,還有幫助團隊掃除障礙,促進團隊溝通等工作要做。《Scrum精髓》一書中將Scrum Master的職責總結為六類:敏捷教練,服務型領導,“保護傘”,“清道夫”,過程權威,“變革代言人”。作為“保護傘“,Scrum Master應該保護團隊免受任何干擾,當然也包括團隊內衝突,成員關係等。

敏捷開發中,Scrum Master應該幫助團隊成員建立共同的願景與集體價值觀,幫助每個成員成長並實現其自身價值,同時鼓勵成員們相互尊重、信賴。當遇到上文的問題時,Scrum Master可通過引導的方式加強團隊協作改變團隊現狀。

對於之前提到的問題,Scrum Master可以參考如下措施。

組織團建,拉近團隊成員之間的距離

外冷內熱型的團隊成員通常沒有機會與其他成員交流,定期抽出時間比如每次完成發布計劃或者兩個月為間隔,搞一次團隊建設(以下簡稱團建),團建可以讓整個團隊放鬆身心,團建本身也是一個很好的拉近團隊成員之間距離的方法。

團建通常會策劃一系列團隊運動:參与團建的成員以組為單位進行PK,每組成員在團建中為了團隊榮譽,盡情的釋放自己的能量。

團建考驗團隊協作,工作中看起來很難合作的人,可能在遊戲中就很容易合作。如果遊戲中合作愉快,這次合作的經歷很容易就會被帶到工作中,從而推動團隊向一個更积極的方向發展,讓團隊更有凝聚力。

讓刺頭知道人外有人

自信的人通常對自己的能力有一個正確的判斷,清楚自己的能力範圍;而自負的人則會高估自己的能力,輕視他人。自負常常源於自己認知有限,自負的刺頭通常認為自己技術能力已經處於行業巔峰,如果讓刺頭意識到自己並沒有比他人強太多,就可以讓其認清事實,調整心態,具體有如下幾種做法:

讓刺頭與能力更強的人一起共事,一方面可以認識到自己的不足,另一方面可以提高自己的境界。

如果刺頭是一個剛工作的年輕人,與老員工一起工作通常會發現老員工思考問題方式,代碼編寫習慣等都和自己不同,Bug也更少。兩人對比如同《賣油翁》里的陳康肅和賣油翁,“無他,唯手熟爾”,習慣源自多年工作經驗的積累——走過的坑多了就知道如何避開了。差距會讓刺頭認識到自己能力不足,還有很多方面需要提升,同時也是年輕人學習的一個好機會。

技術骨幹和能力相當的人共事,可以了解到自身並沒有比其他人強太多,在團隊內也並非無可替代,進而消除其自負心理。

如果公司內沒有比刺頭更強的人,可以讓其處理更難的工作

更強的人應該接受更強的挑戰。Scrum Master可以建議刺頭認領更難的工作,而非其擅長的工作,比如讓其使用行業新技術優化現有產品或者開發一系列工具提高團隊工作效率,建議的同時可以說“你覺得這工作有困難么,大概多久能完成”,或者“我聽說XXX用了一周就作完了,你技術這麼強應該也沒問題吧” 之類能夠刺激到他的話。

如果刺頭能夠按時完成工作,項目一定程度上會從中獲益;如果不能,刺頭自身也會有一種挫敗感——原來自己並非無所不能,自己和別人比還是有差距的,自負的情況也會有所改善。

傾聽刺頭心聲,耐心疏導

對工作存有怨念的刺頭,我們應該找到怨念的根因,想辦法疏導。

面對既將離職的刺頭,Scrum Master可以通過稱讚其以往對團隊做出的貢獻,緩和其不滿情緒。雖然雙方合作關係即將結束,但刺頭多少對團隊有些感情,Scrum Master動之以情,曉之以理,在維持其情緒穩定的同時,鼓勵其做到善始善終,站好最後一班崗。

刺頭沒有離職打算:

  • 如果Scrum Master清楚其怨念的根因

Scrum Master可以通話一對一談話的形式,為其打開心結。比如可以從最近狀態切入,引出根因(比如績效不如別人好,晉陞沒有別人快),然後解析其他人狀態,將刺頭與其他人形成對比,並列舉差異;Scrum Master可以在最後進行鼓勵比如“如果你能完成XX目標,我覺得你就可以獲得更好的績效,而且你絕對有這個能力完成”,讓刺頭了解與別人差距的同時也得到認可,並且獲得前進的動力。

講一個親身經歷的故事,故事發生在筆者步入職場后的第一個團隊。團隊的項目經理A和技術人員B同年入職,兩人對項目貢獻都很多。論技術A不如B,但是A的職級比B高一級(職級直接影響待遇),B大為不滿,工作中處處與A對着干,消極怠工,嚴重影響項目交付。公司總部領導了解情況后,給B制定了目標,只要B完成目標就提升其職級,最後B努力完成了目標,領導兌現諾言,給B提升了職級。對於B來說,職級上調,其目的已經達到,工作狀態也慢慢變得积極。

  • 如果Scrum Master不清楚其怨念根因

Scrum Master也可以找刺頭單獨談話。如果感覺直奔主題不好的話,可以在閑暇之餘對其進行重點關注,比如午休一起打打遊戲等,建立一定社交基礎后再嘗試詢問,然後逐步引導。

如果刺頭負面情緒來源於衝突沒有得到很好解決,Scrum Master可以擔任衝突調解員。Scrum Master站在客觀角度,和衝突雙方一起重新審視衝突,並客觀的給出建議,解決衝突。

在日常工作中,Scrum Master應該多認可,多鼓勵團隊成員,多與成員溝通,營造出輕鬆,融洽的工作氛圍,避免團隊成員因工作產生負面情緒。

崗位調整

如果刺頭始終無法改變,雙方互相耗着必將是一個雙輸的局面。

調整其崗位,讓其在能力範圍內選擇自己想做的工作也是企業中常見的做法。

參考附錄

Kenneth S.Rubin:Scrum精髓. 北京:清華大學出版社

 

點擊關注,第一時間了解華為雲新鮮技術~

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

C# 9.0 新特性之參數非空檢查簡化

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

參數非空檢查是縮寫類庫很常見的操作,在一個方法中要求參數不能為空,否則拋出相應的異常。比如:

public static string HashPassword(string password)
{
    if(password is null)
    {
        throw new ArgumentNullException(nameof(password));
    }
    ...
}

當異常發生時,調用者很容易知道是什麼問題。如果不加這個檢查,可能就會由系統拋出未將對象引用為實例之類的錯誤,這不利於調用者診斷錯誤。

由於這個場景太常見了,於是我經常在我的項目中通過一個輔助類來做此類檢查。這個類用來檢查方法參數,所以命名為 Guard,主要代碼如下:

public static class Guard
{
    public static void NotNull(object param, string paramName)
    {
        if (param is null)
        {
            throw new ArgumentNullException(paramName);
        }
    }

    public static void NotNullOrEmpty(string param, string paramName)
    {
        NotNull(param, paramName);
        if (param == string.Empty)
        {
            throw new ArgumentException($"The string can not be empty.", paramName);
        }
    }

    public static void NotNullOrEmpty<T>(IEnumerable<T> param, string paramName)
    {
        NotNull(param, paramName);
        if (param.Count() == 0)
        {
            throw new ArgumentException("The collection can not be empty.", paramName);
        }
    }
    ...
}

這個類包含了三個常見的非空檢查,包括 null、空字符串、空集合的檢查。使用示例:

public static string HashPassword(string password)
{
    Guard.NotNull(password, nameof(password));
    ...
}

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector)
{
    Guard.NotNullOrEmpty(source, nameof(source));
    ...
}

介於這種非空檢查極其常見,C# 9.0 對此做了簡化,增加了操作符‘!’,放在參數名後面,表示此參數不接受 null 值。使用方式如下:

public static string HashPassword(string password!)
{
    ...
}

簡化了很多有木有。這個提案已經納入 C# 9.0 的特性中,但目前(2020-06-13)還沒有完成開發。

這個特性只支持非 null 檢查,其它參數檢查場景還是不夠用的,我還是會通過輔助類來進行像空字符串、空集合的檢查。

這個特性在寫公共類庫的時候很有用,但我想大多數人在寫業務邏輯代碼的時候可能用不到這個特性,一般會封自己的參數檢查機制。比如,我在項目中,對於上層 API 開發,我通過封裝一個輔助類(ApiGuard)來對對參數進行檢查,如果參數不通過,則拋出相應的業務異常,而不是 ArgumentNullException。比如下面的一段截取自我的 GeekGist 小項目的代碼:

public static class ApiGuard
{
    public static void EnsureNotNull(object param, string paramName)
    {
        if (param == null) throw new BadRequestException($"{paramName} can not be null.");
    }

    public static void EnsureNotEmpty<T>(IEnumerable<T> collection, string paramName)
    {
        if (collection == null || collection.Count() == 0)
            throw new BadRequestException($"{paramName} can not be null or empty.");
    }

    public static void EnsureExist(object value, string message = "Not found")
    {
        if (value == null) throw new BadRequestException(message);
    }

    public static void EnsureNotExist(object value, string message = "Already existed")
    {
        if (value != null) throw new BadRequestException(message);
    }
    ...
}

使用示例:

public async Task UpdateAsync(long id, BookUpdateDto dto)
{
    ApiGuard.EnsureNotNull(dto, nameof(dto));
    ApiGuard.EnsureNotEmpty(dto.TagValues, nameof(dto.TagValues));

    var book = await DbSet
        .Include(x => x.BookTags)
        .FirstOrDefaultAsync(x => x.Id == id);
    ApiGuard.EnsureExist(book);

    Mapper.Map(dto, book);

    ...
}

ApiGuard 的好處是,當 API 接口接到不合要求的參數時,可以自定義響應返回內容。比如,增加一個 Filter 或中間件用來全局捕獲業務代碼異常,根據不同的異常返回給前端不同的狀態碼和消息提示:

private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    ApiResult result;
    if (exception is BadRequestException)
    {
        result = ApiResult.Error(exception.Message, 400);
    }
    else if (exception is NotFoundException)
    {
        message = string.IsNullOrEmpty(message) ? "Not Found" : message;
        result = ApiResult.Error(message, 404);
    }
    else if (exception is UnauthorizedAccessException)
    {
        message = string.IsNullOrEmpty(message) ? "Unauthorized" : message;
        result = ApiResult.Error(message, 401);
    }
    ...
}

只是一個參數非空檢查,在實際開發中卻有不少的學問,所以學好了理論還要多實踐才能更透徹的理解它。

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

【其他文章推薦】

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

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

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

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

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

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

「譯」懂點那啥編譯

原文compilers-for-free,主要用Ruby來描述,意譯為主,文章太長了,typo/翻譯錯誤等請留言,謝謝。
最後,對編譯器解釋器感興趣的可以看看

目錄

  • 介紹
  • 執行
  • 解釋
  • 編譯
  • 部分求值
  • 應用
  • 二村映射
  • 總結

介紹

我喜歡編程,尤其喜歡元編程。當Ruby開發者討論元編程時他們說的通常是“讓程序來寫程序”,因為Ruby有很多這方面的特性,如 BasicObject#instance_eval, Module#define_method還有BasicObject#method_missing,這讓我們寫的程序能在運行時新增功能。

但是我發現元編程還有另一種更有趣的方面:操縱程序表示的程序(programs that manipulate representations of programs)。這些程序將一些程序的源碼作為輸入,然後在其上做一些事情,如分析,求值,翻譯或者轉換。

這是一個由解釋器、編譯器和靜態分析器組成的世界,我覺得它很迷人,我們能編寫的最純粹、最自指(self-referential)的軟件。

我會給你展示這個世界,並讓你對程序有不同的看法。我不會說一些很複雜的技術,我只會說很酷的技術,告訴你操縱其他程序的程序是有趣的,並希望能激勵你繼續探索這方面的知識。

執行

我們從一個最熟悉的東西開始:程序執行。

假設我們有一個要運行的程序,以及一台運行它的機器。運行程序的第一步是把程序放到機器裏面。然後我們輸入一些數據作為程序的——命令行參數,或者配置文件,或者標準輸入,又或者其它的——總之我們把這些輸入放到了機器裏面。

原則上,程序在機器上執行,讀取輸入,併產生一些輸出:

但實際上,上圖只可能發生在那些硬件能直接執行該程序的情況。對於一個裸機,這意味着除非你的程序是用機器代碼寫的,否則就不可能像上圖那樣。(如果程序使用一些高級語言寫的,那麼機器要想理解這門語言,必須套一個語言虛擬機,比如Java字節碼之於JVM)。我們需要一個解釋器或者編譯器。

解釋

解釋器的大致工作機制如下:

  • 它讀取程序源碼,接着
  • 它解析程序源碼,構造出抽象語法樹(Abstract syntax tree,AST),最終
  • 解釋器遍歷AST並求值

我們試着構造一門玩具語言SIMPLE的解釋器,然後逐步討論。SIMPLE非常的直觀,類似於Ruby,但也不完全一樣。下面是一個SIMPLE的示例代碼:

a = 19 + 23

x = 2; y = x * 3

if (a < 10) { a = 0; b = 0 } else { b = 10 }

x = 1; while (x < 5) { x = x * 3 }

和Ruby不同的是,SIMPLE明確區分表達式語句。表達式求值得到一個值。比如表達式19 + 23求值得到42,表達式x * 3將當前x的值乘以3。語句求值不產生任何值,但是可能有副作用(side effect)導致當前的值被修改。對賦值語句a = 19 + 23求值會將a的值修改為42。

除了賦值外,SIMPLE還有一些語句:序列語句(挨個求值,逗號隔開),條件語句(if (…) { … } else { … }),循環語句(while (…) { … })。這些語句不會直接影響變量的值,但是它們的代碼塊中可能包含賦值語句,這些賦值語句就會影響變量的值。

有一個名為Treetop的Ruby庫,可以很容易的構造解析器,所以我們會使用Treetop來構造SIMPLE的解析器。(這裏不討論Treetop的細節,如果想了解更多請參見Treetop documentation)

在使用Treetop之前,我們還得寫一個語法文件,就像下面:

grammar Simple
  rule statement
    sequence
  end

  rule sequence
    first:sequenced_statement '; ' second:sequence
    /
    sequenced_statement
  end

  rule sequenced_statement
    while / assign / if
  end

  rule while
    'while (' condition:expression ') { ' body:statement ' }'
  end

  rule assign
    name:[a-z]+ ' = ' expression
  end

  rule if
    'if (' condition:expression ') { ' consequence:statement
      ' } else { ' alternative:statement ' }'
  end

  rule expression
    less_than
  end

  rule less_than
    left:add ' < ' right:less_than
    /
    add
  end

  rule add
    left:multiply ' + ' right:add
    /
    multiply
  end

  rule multiply
    left:term ' * ' right:multiply
    /
    term
  end

  rule term
    number / boolean / variable
  end

  rule number
    [0-9]+
  end

  rule boolean
    ('true' / 'false') ![a-z]
  end

  rule variable
    [a-z]+
  end
end

這個語法文件包含一些rules,它們很好的显示了表達式和語句有哪些。sequence rule是兩個用逗號隔開的語句,while rule表示一個由關鍵字while開頭,後面跟一個圓括號包裹的條件表達式,再後面跟一個花括號包裹的代碼體。assignment rule表示變量名跟一個等號,再跟一個表達式。然後number,boolean和variable這些rules分別表示数字字面值,布爾字面值和變量名字。

當我們寫完語法文件,就可以使用Treetop讀取它,Treetop會生成一個解析器。解析器的代碼用Ruby的class組織,所以我們可以實例化解析器,然後給它一個表示字符串讓它解析。
假設語法文件名字是simple.treetop,然後我們用IRB演示一下:

>> Treetop.load('simple.treetop')
=> SimpleParser

>> SimpleParser.new.parse('x = 2; y = x * 3')
=> SyntaxNode+Sequence1+Sequence0 offset=0, "x = 2; y = x * 3" (first,second):
     SyntaxNode+Assign1+Assign0 offset=0, "x = 2" (name,expression):
       SyntaxNode offset=0, "x":
         SyntaxNode offset=0, "x"
       SyntaxNode offset=1, " = "
       SyntaxNode+Number0 offset=4, "2":
         SyntaxNode offset=4, "2"
     SyntaxNode offset=5, "; "
     SyntaxNode+Assign1+Assign0 offset=7, "y = x * 3" (name,expression):
       SyntaxNode offset=7, "y":
         SyntaxNode offset=7, "y"
       SyntaxNode offset=8, " = "
       SyntaxNode+Multiply1+Multiply0 offset=11, "x * 3" (left,right):
         SyntaxNode+Variable0 offset=11, "x":
           SyntaxNode offset=11, "x"
         SyntaxNode offset=12, " * "
         SyntaxNode+Number0 offset=15, "3":
           SyntaxNode offset=15, "3"

解析器產生了一個名為具體語法樹(concrete syntax tree)的數據結構,也叫解析樹(parse tree),它包含了一大堆細節,但是實際上我們不需要這麼多細節,我們更希望一個抽象語法樹(abstract syntax tree),它包含更少的細節,更具表現力。

為了產出抽象語法樹,我們需要聲明一些類,它們是最後生成的抽象語法樹的節點。這些通過Struct很容易完成:

Number    = Struct.new :value
Boolean   = Struct.new :value
Variable  = Struct.new :name

Add       = Struct.new :left, :right
Multiply  = Struct.new :left, :right
LessThan  = Struct.new :left, :right

Assign    = Struct.new :name, :expression
If        = Struct.new :condition, :consequence, :alternative
Sequence  = Struct.new :first, :second
While     = Struct.new :condition, :body

現在,我們定義了一堆用來表示表達式和語句的node類:簡單的表達式如Number,Boolean只有包含一個value,二元表達式如Add,Multiply有left和right兩個表達式。Assign語句的name表示變量名,還有一個expression表示賦值語句的右側,等等等等。

Treetop還允許這些node類攜帶語義動作,這個語義動作是一個Ruby方法。我們可以用這個特性將具體語法樹的節點轉換為抽象語法樹的節點,讓我們把這個表示語義動作的方面命名為#to_ast

下面展示了如何定義number,boolean和變量的語義動作方法:

 rule number
  [0-9]+ {
    def to_ast
      Number.new(text_value.to_i)
    end
  }
end

rule boolean
  ('true' / 'false') ![a-z] {
    def to_ast
      Boolean.new(text_value == 'true')
    end
  }
end

rule variable
  [a-z]+ {
    def to_ast
      Variable.new(text_value.to_sym)
    end
  }
end

當Treetop應用number這條rule時,它繼續調用後面緊跟着的to_ast方法,這個方法把整数字面值轉換為一個整数字符串(使用String#to_i),然後用Number包裹創造處integer常量。#to_ast的實現在boolean和variable的rule中與之類似,它們最終都構造出合適的AST節點。

下面是一些二元表達式的#to_ast實現:

rule less_than
  left:add ' < ' right:less_than {
    def to_ast
      LessThan.new(left.to_ast, right.to_ast)
    end
  }
  /
  add
end

rule add
  left:multiply ' + ' right:add {
    def to_ast
      Add.new(left.to_ast, right.to_ast)
    end
  }
  /
  multiply
end

rule multiply
  left:term ' * ' right:multiply {
    def to_ast
      Multiply.new(left.to_ast, right.to_ast)
    end
  }
  /
  term
end

在上面三個例子中,#to_ast遞歸地調用具體語法樹的左右子表達式的#to_ast,將它們轉換為AST,然後把它們組合起來,放到一個類中。

語句也類似

rule sequence
  first:sequenced_statement '; ' second:sequence {
    def to_ast
      Sequence.new(first.to_ast, second.to_ast)
    end
  }
  /
  sequenced_statement
end

rule while
  'while (' condition:expression ') { ' body:statement ' }' {
    def to_ast
      While.new(condition.to_ast, body.to_ast)
    end
  }
end

rule assign
  name:[a-z]+ ' = ' expression {
    def to_ast
      Assign.new(name.text_value.to_sym, expression.to_ast)
    end
  }
end

rule if
  'if (' condition:expression ') { ' consequence:statement
    ' } else { ' alternative:statement ' }' {
    def to_ast
      If.new(condition.to_ast, consequence.to_ast, alternative.to_ast)
    end
  }
end

一樣的,#to_ast遞歸調用子表達式或者子語句的#to_ast將它們轉換為AST,然後組合成一個正確的AST類。

當所有的#to_ast都定義完成后,我們對具體語法樹的根節點調用#to_ast,遞歸地將整個樹轉換為AST:

>> SimpleParser.new.parse('x = 2; y = x * 3').to_ast
=> #<struct Sequence
     first=#<struct Assign
       name=:x,
       expression=#<struct Number value=2>
     >,
     second=#<struct Assign
       name=:y,
       expression=#<struct Multiply
         left=#<struct Variable name=:x>,
         right=#<struct Number value=3>
       >
     >
   >

儘管Treetop的設計目的是產出具體語法樹,但是用我們定義的node類和語義動作生成的AST是更簡單的結構。代碼x = 2; y = x * 3的AST如下圖所示:

它告訴我們x = 2; y = x * 3 是一個序列語句,包含兩個賦值語句:第一個將Number 2賦值給x,第二個將Variable x和Number 3相乘,即Multiply,結果賦值給y。

現在我們獲得類AST,可以遍歷它並求值。我們為每個AST node類定義一個#evaluate方法,它將對當前節點以及節點的子樹求值。當定義好所有的#evaluate方法后,可以調用AST樹的根節點的#evaluate求值。

下面是Number,Boolean,Variable的#evaluate定義

class Number
  def evaluate(environment)
    value
  end
end

class Boolean
  def evaluate(environment)
    value
  end
end

class Variable
  def evaluate(environment)
    environment[name]
  end
end

environment(環境)參數是一個哈希表,它將每個變量名和值關聯起來。比如說,一個 { x: 7, y: 11 }的environment表示有兩個變量,x和y,它們的值分別是7和11.我們假設每個SIMPLE程序的初始environment是一個空的哈希表。後面我們會看到語句求值如何改變environment。

當我們求值Number或者Boolean時,我們不關心它們是否在environment中,因為我們總是返回一個值,這個值是AST node構建的時候的那個值。如果我們對Variable求值,就得看看environment是否存在這個變量的值。

所以如果我們創建一個Number AST節點,然後用空environment開始求值,我們得到原始的Ruby整數:

>> Number.new(3).evaluate({})
=> 3

Boolean與之類似:

>> Boolean.new(false).evaluate({})
=> false

如果我們對一個名為y的Variable求值,並且environment是之前那個,那麼我們得到11,然而如果environment中的y的值是另一個,那麼對名為y的Variable求值也會得到另一個:

>> Variable.new(:y).evaluate({ x: 7, y: 11 })
=> 11

>> Variable.new(:y).evaluate({ x: 7, y: true })
=> true

下面是二元表達式的#evaluate定義

class Add
  def evaluate(environment)
    left.evaluate(environment) + right.evaluate(environment)
  end
end

class Multiply
  def evaluate(environment)
    left.evaluate(environment) * right.evaluate(environment)
  end
end

class LessThan
  def evaluate(environment)
    left.evaluate(environment) < right.evaluate(environment)
  end
end

這些實現遞歸地求值左右子表達式,然後對結果執行一些運算。如果是Add節點,那麼結果相加;如果是Multiply節點,結果相乘;如果是LessThan節點,結果用<運算符進行比較。

當我們對一個Multiply表達式x * y求值時,environment的x為2,y為3,那麼結果是6:

>> Multiply.new(Variable.new(:x), Variable.new(:y)).
     evaluate({ x: 2, y: 3 })
=> 6

如果在相同environment中對LessThan表達式x < y求值,得到結果true:

>> LessThan.new(Variable.new(:x), Variable.new(:y)).
     evaluate({ x: 2, y: 3 })
=> true

對於語句,#evaluate稍有不同。因為對語句求值會更新environment的值,而不是像表達式求值那樣簡單的返回一個值。表達式的#evaluate接受一個environment參數,返回一個新的environment,返回的environment與environment參數的差異就是語句的效果導致的。

直接影響變量的語句有Assign:

class Assign
  def evaluate(environment)
    environment.merge({ name => expression.evaluate(environment) })
  end
end

要求值Assign,我們得先求值右邊的表達式,得到一個值,然後使用Hash#merge創建一個修改后的environment。

如果我們在空environment中對x = 2求值,我們會得到一個新的environment,裡面包含了變量名x到值2的映射關係:

>> Assign.new(:x, Number.new(2)).evaluate({})
=> {:x=>2}

類似的,對y = x * 3求值,會在新的environment中關聯變量名y和值6:

>> Assign.new(:y, Multiply.new(Variable.new(:x), Number.new(3))).
     evaluate({ x: 2 })
=> {:x=>2, :y=>6}

對序列語句的求值是先求第一個語句,作為中間environment,然後求第二個語句,用中間environment作為第二個語句求值的environment,得到最終的environment:

class Sequence
  def evaluate(environment)
    second.evaluate(first.evaluate(environment))
  end
end

如果序列語句中有兩個賦值,那麼第二個賦值將是最終贏家:

>> Sequence.new(
     Assign.new(:x, Number.new(1)),
     Assign.new(:x, Number.new(2))
   ).evaluate({})
=> {:x=>2}

最後,我們還需要為If和While增加#evaluate

class If
  def evaluate(environment)
    if condition.evaluate(environment)
      consequence.evaluate(environment)
    else
      alternative.evaluate(environment)
    end
  end
end

class While
  def evaluate(environment)
    if condition.evaluate(environment)
      evaluate(body.evaluate(environment))
    else
      environment
    end
  end
end

If語句根據它的條件選擇求值兩個子語句中的一個。While在條件為true的情況下反覆對循環體求值。

現在,我們已經為所有表達式和語句實現了#evaluate,我們可以解析簡單的SIMPLE程序,然後得到AST。讓我們看看在空environment中對x = 2; y = x * 3求值會得到什麼:

>> SimpleParser.new.parse('x = 2; y = x * 3').
     to_ast.evaluate({})
=> {:x=>2, :y=>6}

正如期待的那樣,x和y的最終結果分別是2和6。

讓我們嘗試更複雜的程序:

>> SimpleParser.new.parse('x = 1; while (x < 5) { x = x * 3 }').
     to_ast.evaluate({})
=> {:x=>9}

這個程序將初始化x為1,然後反覆的將它翻三倍,直到它大於5。當求值結束,x的值為9,因為9是最小的大於等於5且三倍之於x的值。

以上,我們實現了一個解釋器:解析器讀取源代碼,生成具體語法樹,然後語法制導翻譯將具體語法樹轉換為AST,最後遍歷AST求值。

解釋器是單階段執行(single-stage execution)。我們可以將源程序和其它數據(在SIMPLE的例子中是初始的空environment)作為解釋器的輸入,然後解釋器基於輸入得到輸出:

所有的過程都發生在運行時——程序執行階段

編譯

現在我們了解了解釋器是如何工作的,那編譯器呢?

編譯器將源碼解析為AST,然後遍歷它,和解釋器一樣的套路。但是不同的是,編譯器不根據AST節點語義執行代碼,它是生成代碼。

讓我們寫一個SIMPLE到JavaScript的編譯器來解釋這個過程。現在不為AST node定義#evaluate方法,我們定義一個#to_javascript方法,這個方法將所有表達式和語句節點轉換為JavaScript代碼字符串。

下面是Number,Boolean,Variable的#to_javascript定義:

require 'json'

class Number
  def to_javascript
    "function (e) { return #{JSON.dump(value)}; }"
  end
end

class Boolean
  def to_javascript
    "function (e) { return #{JSON.dump(value)}; }"
  end
end

class Variable
  def to_javascript
    "function (e) { return e[#{JSON.dump(name)}]; }"
  end
end

上面的每個#to_javascript都會生成一個JavaScript的函數,這個函數接受一個environment參數e,然後返回一個值。對於Number表達式,返回的是Number所表達的整數:

>> Number.new(3).to_javascript
=> "function (e) { return 3; }"

我們可以獲取編譯后的JavaScript代碼,然後在瀏覽器console界面,或者Node.js REPL中調用JavaScript函數:

// 譯註:下面是JavaScript代碼
> program = function (e) { return 3; }
[Function]

> program({ x: 7, y: 11 })
3

類似的,Boolean AST節點編譯后也是一個JS函數,它返回true或者false:

>> Boolean.new(false).to_javascript
=> "function (e) { return false; }"

SIMPLE的Variable AST節點編譯後會在環境e中查找合適的變量,並返回這個值:

>> Variable.new(:y).to_javascript
=> "function (e) { return e[\"y\"]; }"

很顯然,上面的函數返回的內容取決於環境e的內容:

> program = function (e) { return e["y"]; }
[Function]

> program({ x: 7, y: 11 })
11

> program({ x: 7, y: true })
true

下面是二元表達式的#to_javascript實現

class Add
  def to_javascript
    "function (e) { return #{left.to_javascript}(e) + #{right.to_javascript}(e); }"
  end
end

class Multiply
  def to_javascript
    "function (e) { return #{left.to_javascript}(e) * #{right.to_javascript}(e); }"
  end
end

class LessThan
  def to_javascript
    "function (e) { return #{left.to_javascript}(e) < #{right.to_javascript}(e); }"
  end
end

我們將二元表達式的左右子表達式編譯成JavaScript的函數,然後外面再用JavaScript代碼包裹。當這段代碼真正執行的時候,左右子表達式的js函數都會被調用,得到值,然後兩個值做加法、乘法或者比較運算。讓我們以x * y為例,看看編譯過程:

>> Multiply.new(Variable.new(:x), Variable.new(:y)).to_javascript
=> "function (e) { return function (e) { return e[\"x\"]; }(e) *
    function (e) { return e[\"y\"]; }(e); }"

然後將編譯得到的JavaScript代碼(和上面一樣,只是看起來更美觀)放到支持的環境中運行:

> program = function (e) {
    return function (e) {
      return e["x"];
    }(e) * function (e) {
      return e["y"];
    }(e);
  }
[Function]

> program({ x: 2, y: 3 })
6

接下來是語句的#to_javascript,If用的是JavaScript的條件語句,While是JavaScript的循環,諸如此類。最外面和表達式一樣用一個JavaScript函數包裹,該函數接受一個環境e,語句可以更新環境。

class Assign
  def to_javascript
    "function (e) { e[#{JSON.dump(name)}] = #{expression.to_javascript}(e); return e; }"
  end
end

class If
  def to_javascript
    "function (e) { if (#{condition.to_javascript}(e))" +
      " { return #{consequence.to_javascript}(e); }" +
      " else { return #{alternative.to_javascript}(e); }" +
      ' }'
  end
end

class Sequence
  def to_javascript
    "function (e) { return #{second.to_javascript}(#{first.to_javascript}(e)); }"
  end
end

class While
  def to_javascript
    'function (e) {' +
      " while (#{condition.to_javascript}(e)) { e = #{body.to_javascript}(e); }" +
      ' return e;' +
      ' }'
  end
end

之前我們試過解釋器解釋x = 1; while (x < 5) { x = x * 3 },現在我們試試編譯器編譯:

>> SimpleParser.new.parse('x = 1; while (x < 5) { x = x * 3 }').
     to_ast.to_javascript
=> "function (e) { return function (e) { while (function (e)
   { return function (e) { return e[\"x\"]; }(e) < function (e)
   { return 5; }(e); }(e)) { e = function (e) { e[\"x\"] =
   function (e) { return function (e) { return e[\"x\"]; }(e) *
   function (e) { return 3; }(e); }(e); return e; }(e); } return
   e; }(function (e) { e[\"x\"] = function (e) { return 1; }(e);
   return e; }(e)); }"

儘管編譯后的代碼比源程序還長,但是至少我們已經將SIMPLE代碼轉換為了JavaScript代碼。如果我們在JavaScript的環境中運行編譯后的代碼,我們會獲得和解釋執行一樣的結果——x最終是9:

> program = function (e) {
    return function (e) {
      while (function (e) {
        return function (e) {
          return e["x"];
        }(e) < function (e) {
          return 5;
        }(e);
      }(e)) {
        e = function (e) {
          e["x"] = function (e) {
            return function (e) {
              return e["x"];
            }(e) * function (e) {
              return 3;
            }(e);
          }(e);
          return e;
        }(e);
      }
      return e;
    }(function (e) {
      e["x"] = function (e) { return 1; }(e);
      return e;
    }(e));
  }
[Function]

> program({})
{ x: 9 }

這個編譯器相當的傻——想讓它聰明些需要付出更多的努力——但是它向我們展示了如何在只能運行JavaScript的機器上執行SIMPLE程序。

編譯器是雙階段執行(two-stage execution)。首先我們提供源程序,編譯器接受它並生成目標程序作為輸出。接下來,我們拿出目標程序,給它一些輸入,然後允許它,得到最終結果:

編譯和解釋的最終結果是一致的——源代碼加輸入都最終變成了輸出——但是它將執行過程分成了兩個階段。第一個階段是編譯時,當目標程序生成后,第二個階段是運行時,這一階段讀取輸入和目標程序,最終得到輸出。

好消息是一個好的編譯器生成的目標程序會比解釋器執行的更快。將計算過程分成兩個階段免除了運行時的解釋開銷:解析源代碼,構造AST,遍歷AST樹這些過程都在編譯時完成,它們不影響程序運行。

(編譯還有其它性能受益,比如編譯器可以使用更簡潔的數據結構和優化讓目標程序跑的更快,尤其是在確定了目標程序運行的機器的情況下。)

壞消息是編譯會比解釋更難實現,原因有很多:

  • 編譯器必須要考慮兩個階段。當我們寫解釋器的時候,我們只需要關心解釋器的執行,但是編譯器還需要考慮到它生成的代碼在運行時的行為
  • 編譯器開發者需要懂兩門編程語言。解釋器用一門編程語言編寫,運行也是這一門語言,而我們的SIMPLE編譯器使用Ruby寫的,生成的目標程序代碼卻是JavaScript片段。
  • 將動態語言編譯為高效的目標程序生來就難。像Ruby和JavaScript這種語言允許程序在運行時改變程序自身或者新增代碼,因此程序源碼的靜態表示沒有必要告訴編譯器它想知道的關於自己在運行時的情況。

總結來說,寫解釋器比寫編譯器要簡單,但是解釋程序比執行編譯后的程序要慢。解釋器只有一個階段,只用一門語言,並且動態語言也是沒問題的——如果程序在運行時自身改變了,這沒問題,因為AST可以同步更新,解釋器也工作正常——但是這種簡潔和靈活性會招致運行時性能懲罰。

理想情況下我們想只寫一個解釋器,但是我們程序要想運行的快一些,通常還是需要再寫個編譯器。

部分求值

程序員總是使用解釋器和編譯器,但還有另一種操縱程序的程序不太為人所知:部分求值器。部分求值器是解釋器和編譯器的混合:

解釋器立即執行程序,編譯器生成程序稍後執行,而部分求值器立刻執行部分代碼,然後剩下的代碼稍後執行。

部分求值器讀取程序(又叫subject program)和程序的輸入,然後只執行部分代碼,這些代碼直接依賴輸入。當這些部分被求值后,剩下的程序部分(又叫residual program)作為輸出產出。

部分求值(partial evaluation)允許我們提取程序的一部分執行。現在我們不必一次提供程序的所有輸入,我們可以只提供一些,剩下的以後提供。

現在,我們不用一次執行整個subject program,我們可以將它和一些程序輸入放到部分求值器裏面。部分求值器將執行subject program的一部分,產生一個residual program;接下來,我們執行residual program,並給它剩下的程序輸入:

部分求值的總體效果是,通過將一些工作從未來(當我們打算運行程序時)移到現在,將單階段執行變成了兩個階段,程序運行時可以少執行一些代碼。

部分求值器將subject program轉換為AST,這一步和解釋器編譯器一樣,接着讀取程序的部分輸入。它分析整個AST找到哪些地方用到了程序的部分輸入,然後這些地方被求值,使用包含求值結果的代碼代替。當部分求值完成時,AST重新轉換為文本形式,即輸出為residual program。

真的構建一個部分求值器太龐大了,不太現實,但是我們可以通過一個示例來了解部分求值的過程。

假設有一個Ruby程序#power,它計算x的n次冪:

def power(n, x)
  if n.zero?
    1
  else
    x * power(n - 1, x)
  end
end

現在讓我們扮演一個Ruby部分求值器的角色,假設我們已經獲得了足夠的程序輸入,知道將用實參5作為第一個形參n調用power。我們可以很容易地生成一個新版本的方法,稱之為#power_5,其中形參n已被刪除,所有出現n的地方都被替換為5(這也叫常量傳播):

def power_5(x)
  if 5.zero?
    1
  else
    x * power(5 - 1, x)
  end
end

我們找到了兩個表達式——5.zero?5 - 1——這是潛在的部分求值對象。讓我們選一個,對5.zero?求值,用結果false代替這個表達式:

def power_5(x)
  if false
    1
  else
    x * power(5 - 1, x)
  end
end

這個部分求值行為使得我們還能再求值整個條件語句——if false ... else ... end總是走else分支(稀疏常量傳播):

def power_5(x)
  x * power(5 - 1, x)
end

現在對5 - 1求值,用常量4代替(常量摺疊):

def power_5(x)
  x * power(4, x)
end

現在停一下,因為我們知道#power的行為,這裡有一個優化機會,用#power方法的代碼代替這裏的power(4,x)調用(內聯展開),然後再用4代替所有形參n

def power_5(x)
  x *
  if 4.zero?
    1
  else
    x * power(4 - 1, x)
  end
end

情況和之前一樣,我們知道4.zero?,用false代替:

def power_5(x)
  x *
  if false
    1
  else
    x * power(4 - 1, x)
  end
end

繼續,知道條件語句走else分支:

def power_5(x)
  x *
  x * power(4 - 1, x)
end

知道4 - 1的值是3:

def power_5(x)
  x *
  x * power(3, x)
end

如此繼續。通過將#power調用內聯,然後對常量表達式求值,我們能用乘法代替它們,直到0:

def power_5(x)
  x *
  x *
  x *
  x *
  x * power(0, x)
end

讓我們再手動展開一次:

def power_5(x)
  x *
  x *
  x *
  x *
  x *
  if 0.zero?
    1
  else
    x * power(0 - 1, x)
  end
end

我們直到0.zero?是true:

def power_5(x)
  x *
  x *
  x *
  x *
  x *
  if true
    1
  else
    x * power(0 - 1, x)
  end
end

這是我們第一次在例子中遇到if true而不是if false的情況,現在選擇then分支,而不是else分支:

def power_5(x)
  x *
  x *
  x *
  x *
  x *
  1
end

最終效果是,我們把#power_5轉換為x重複乘以自身五次,然後乘以1。乘以1對計算沒有影響,所以可以消除它,最終得到的代碼如下:

def power_5(x)
  x * x * x * x * x
end

下面是圖形化的過程:

#power加上程序輸入5,最終經過部分求值,得到residual program#power_5。沒有新的代碼產生,#power_5的代碼是由#power組成的,只是重新安排並以新的方式結合在一起。

#power_5的優勢是當輸入為5的時候它比#power跑的更快。#power_5簡單的重複乘以x五次,而#power是執行了五次遞歸的方法調用,零值檢查,條件計算和減法。這些工作都由部分求值器完成,然後結果保留在residual program中,現在redisual program只依賴未知的程序輸入x。

所以,如果我們直到n為5,那麼#power_5就是#power優化改進版。

注意部分求值和部分應用(partial application)是不一樣的。基於部分應用的#power_5和部分求值得到的residual program是不一樣的:

def power_5(x)
  power(5, x)
end

通過部分應用,我們固定一個常量到代碼中,成功的將#power轉換為了#power_5,這個實現也是沒問題的:

>> power_5(2)
=> 32

但是這個版本的#power_5不見得比#power更高效——實際上它還會慢一些,因為它引入了額外的方法調用。

或者,我們也可以不定義新的方法,而是用Method#to_proc和Proc#curry將#power柯里化(currying)為一個過程,然後用實參5調用柯里化后的過程:

>> power_5 = method(:power).to_proc.curry.call(5)
=> #<Proc (lambda)>

接着,我們調用部分應用后的代碼,傳入的參數是#power的第二個形參,即x:

>> power_5.call(2)
=> 32

不難看出,部分應用和部分求值很像是,但是仔細審視后可以看出,部分應用是程序內的行為:當我們寫代碼時,部分應用允許我們固定一些參數,然後得到一個方法的更少參數的版本。而部分求值是程序外部的行為:它是一個源碼級別的轉換,通過特化一些程序輸入,可以得到更高效的方法的版本。

應用

部分求值有很多有用的應用場景。固定程序的一些輸入,即特化,可以提供兩方面的好處:我們可以編寫一個通用的、靈活的程序來支持許多不同的輸入,然後,當我們真正知道如何使用這個程序時,我們可以使用一個部分求值器來自動生成一個特化后的版本,該版本移除了代碼的通用性,但是提高了性能。

例如,像nginx或[Apache(http://httpd.apache.org/)這樣的web服務器在啟動時會讀取配置文件。該文件的內容會影響服務器後續每個HTTP請求的處理,因此web服務器必須花費一些執行時間來檢查其配置數據,以決定要做什麼。如果我們對web服務器使用部分求值器專門針對一個特定的配置文件進行部分求值,我們將得到一個新版本,它只執行該文件所說的操作;在啟動期間解析配置文件並在每個請求期間檢查其數據的開銷將不再是程序的一部分。

另一個經典的例子是光線跟蹤。要製作一部攝影機在三維場景中移動的電影,我們可能最終會使用光線跟蹤器渲染數千個單獨的幀。但是如果場景對於每一幀都是相同的,我們可以使用一個部分求值器來專門處理光線跟蹤器對於我們特定場景的描述,我們會得到一個新的光線跟蹤器,它只能渲染該場景。然後,每次我們使用專門的光線跟蹤器渲染幀時,都會避免讀取場景文件、設置表示其幾何體所需的數據結構以及對光線在場景中的行為做出與相機無關的決策的開銷。

一個更實際(有一些爭議)的例子是OpenGL管道。在OS X中,OpenGL管道的一部分是用LLVM中間語言編寫的,這其中包括一些GPU硬件就實現了的。不管GPU支持與否,都可以用軟件的方式實現,而有了部分求值,Apple公司可以對LLVM部分求值,刪除掉特定GPU上不需要的代碼,剩下的代碼是硬件沒有直接實現的。

二村映射

首先,我想說一個cool story。
1971年,Yoshihiko Futamura在Hitachi Central Research Laboratory工作時發表了一些有趣的論文。他在思考部分求值如何更早的處理subject program的一些輸入,然後產出後續可以執行的residual program。

具體來說,Futamura將解釋器作為部分求值的輸入並思考了一些問題。他意識到解釋器只是一個計算機程序而已,它讀取輸入,執行,然後得到輸出。解釋器的輸入之一是源碼,但是除了這個比較特別外解釋器和普通程序沒啥兩樣。

他在思考,如果我們先用部分求值器做一些解釋器的工作,會發生什麼?

如果我們使用部分求值器特化解釋器和它的某個程序的輸入,我們會得到residual program。然後,我們可以用程序的剩下的輸入加上residual program,得到最終結果:

最終效果和用解釋器直接運行程序一樣,只是現在單階段執行變成了兩階段。

Futamura注意到residual program讀取剩下的程序輸入,然後產生輸出,我們通常把這個residual program稱為目標程序——一個能被底層機器直接執行的源程序的新版本。這意味着部分求值器讀取源程序,產出目標程序,它實際上扮演了編譯器的角色。

看起來難以置信。它是怎麼工作的?讓我們看一個示例。下面是SIMPLE解釋器的top level環境:

source, environment = read_source, read_environment

Treetop.load('simple.treetop')
ast = SimpleParser.new.parse(source).to_ast
puts ast.evaluate(environment)

解釋器讀取源程序和初始化environment(從哪讀的我們不關心)。它加載Treetop語法文件,實例化解析器,然後解析器產出AST,然後對AST求值並輸出。#evaluate的定義和前面解釋器小結是一樣的。

讓我們想象一下將這個解釋器和SIMPLE源代碼 x = 2; y = x * 3作為輸入放入Ruby部分求值器。這意味這上面代碼中的#read_source將返回字符串 x = 2; y = x * 3,所以我們可以用字符串代替它:

source, environment = 'x = 2; y = x * 3', read_environment

Treetop.load('simple.treetop')
ast = SimpleParser.new.parse(source).to_ast
puts ast.evaluate(environment)

現在,讓我手動做一下複寫傳播,由於source變量只在代碼中用到了一次,我們可以完全消除它,用值代替:

environment = read_environment

Treetop.load('simple.treetop')
ast = SimpleParser.new.parse('x = 2; y = x * 3').to_ast
puts ast.evaluate(environment)

構造AST的數據都已經備起了,Treetop語法文件也準備好了,現在我們還知道待解析的字符串是什麼。讓我們手動求值表達式,創建一個手工AST代替解析過程:

environment = read_environment

ast = Sequence.new(
        Assign.new(
          :x,
          Number.new(2)
        ),
        Assign.new(
          :y,
          Multiply.new(
            Variable.new(:x),
            Number.new(3)
          )
        )
      )
puts ast.evaluate(environment)

我們將解釋器簡化為讀取環境、構建AST字面值、在AST根節點上調用#evaluate

在特定節點上調用#evaluate會發生什麼?我們早已直到每種節點的#evaluate定義,現在可以遍歷樹,然後找到AST節點的#evaluate調用,進行部分求值,和我們之前對#power做的差不多。AST包含了所有我們需要的數據,所以我們可以逐步將所有的#evaluate歸納為幾行Ruby代碼:

對於Number和Variable,我們直到它的值和變量名字,所以我們可以將這些信息傳播到其它節點。對於Multiply和Assign節點,我們可以內聯方法調用。對於序列語句,我們可以先內聯first.evaluate再內聯second.evaluate,使用臨時變量保持中間environment。

下面的代碼隱藏了大量的細節,但重要的是,我們擁有最終代碼和數據,它們可以幫助我們對AST根節點的#evaluate方法進行部分求值:

def evaluate(environment)
  environment = environment.merge({ :x => 2 })
  environment.merge({ :y => environment[:x] * 3 })
end

讓我們回到解釋器的主要代碼:

environment = read_environment

ast = Sequence.new(
        Assign.new(
          :x,
          Number.new(2)
        ),
        Assign.new(
          :y,
          Multiply.new(
            Variable.new(:x),
            Number.new(3)
          )
        )
      )
puts ast.evaluate(environment)

既然我們已經生成了AST根節點的#evaluate方法,現在我們可以對ast.evaluate(environment)部分求值,方法是展開這個調用:

environment = read_environment

environment = environment.merge({ :x => 2 })
puts environment.merge({ :y => environment[:x] * 3 })

這個Ruby代碼和原始的SIMPLE代碼產生了相同的行為:它將x設置為2,將y設置為x乘以3,x和y都存放到environment中。所以,從某種意義上說,我們通過對解釋器進行部分求值,將SIMPLE程序編譯為Ruby代碼——雖然還有一些其它environment的內容,但是總之,我們最終的Ruby代碼做的還是x = 2; y = x * 3這件事。

#power那個例子一樣,我們沒有寫Ruby代碼,residual program是重新安排解釋器的代碼並以新的方式結合在一起,這樣它就完成了與最初的SIMPLE程序相同的功能。

這種方式被稱為第一二村映射(first futamura projection):如果我們將源代碼和解釋器一起進行部分求值,我們將得到目標程序。

Futamura很高興他意識到了這一點。然後他繼續思考更多,他繼續意識到將源代碼和解釋器一起進行部分求值得到目標程序這件事本身,其實就是給一個程序多個參數。如果我們先使用部分求值來完成部分求值器的一些工作,然後通過運行剩餘程序來完成其餘的工作,會發生什麼情況?

如果我們用部分求值來特化部分求值器的一個輸入—— 解釋器——我們會得到residual program。然後後面我們可以運行這個residual program,將源程序作為輸入,得到目標程序,最後運行目標程序,傳入剩餘程序輸入,得到最終結果:

總體的效果與給出程序輸入,直接用解釋器運行源程序行為一致,但現在執行已分為三個階段。

Futamura注意到residual program讀取源程序產生目標程序,這個過程是我們常說的編譯器做的事情。這意味着部分求值器將解釋器作為輸入,輸出來編譯器。換句話說,部分求值器此時成了編譯器生成器。

這種方式被稱為第二二村映射(second futamura projection):如果我們將部分求值器和解釋器一起進行部分求值,我們將得到編譯器。

Futamura很高興他意識到了這一點。然後他繼續思考更多,他繼續意識到將部分求值器和解釋器一起進行部分求值得到編譯器這件事本身,其實就是給一個程序多個參數。如果我們先使用部分求值來完成部分求值器的一些工作,然後通過運行剩餘程序來完成其餘的工作,會發生什麼情況?

如果我們用部分求值來特化部分求值器的一個輸入—— 部分求值器——我們會得到residual program。然後後面我們可以運行這個residual program,將解釋器作為輸入,得到編譯器,最後運行編譯器,將源程序作為輸入得到目標程序,在運行目標程序,給它剩下的程序輸入,得到最終結果:

總體的效果與給出程序輸入,直接用解釋器運行源程序行為一致,但現在執行已分為四個階段。

Futamura注意到residual program讀取解釋器產生編譯器,這個過程是我們常說的編譯器做的事情。這意味着部分求值器產出來一個編譯器生成器。換句話說,部分求值器此時成了編譯器生成器的生成器。

這種方式被稱為第三二村映射(third futamura projection):如果我們將部分求值器和部分求值器一起進行部分求值,我們將得到編譯器生成器。

謝天謝地,我們不能再進一步了,因為如果我們重複這個過程,我們仍然會對部分求值器本身進行部分求值。只有三個二村映射。

結論

二村映射非常有趣,但不是說有了它編譯器工程師就是多餘的了。部分求值是一種適用於任何計算機程序的完全通用的技術;當應用於解釋器時,它可以消除解析源代碼和操作AST的開銷,但它不會自動地發明和創造具備工業級水平的編譯器的數據結構和優化。 我們仍然需要聰明的人寫編譯器,使我們的程序盡可能快地運行。

如果你想了解更多關於部分求值的知識,有一本叫做“Partial Evaluation and Automatic Program Generation”的免費書籍詳細的討論了它。LLVM的JIT,PyPy工具鏈的一部分——即RPython和VM以及它底層依賴的JIT)——也使用相關技術讓程序更高效執行。這些項目與Ruby直接相關,因為Rubinius依賴LLVM,還有一個用Python編寫的Ruby實現,名為Topaz,也依賴PyPy工具鏈。

撇開部分求值不談,Rubinius和JRuby也是高質量的編譯器,代碼很有趣,可以免費下載。如果你對操縱程序的程序感興趣,你可以深入Rubinius或JRuby,看看它們是如何工作的,並且(根據Matz的RubyConf 2013主題演講提到的)參与它們的開發。

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

【其他文章推薦】

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

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

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

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

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

關於問問題和時間管理的感悟

這應該算是第一次認真的談談自己,第一次聊自己的時候是Java建設者剛出生沒多久,現在看看屆時的文筆,簡直了,不像是寫了一篇讓讀者觀看的文字,更像是自己情感的集散地。

首談自己

其實,讀者是很容易埋單的,只要你為他考慮一些即可。你的文字能否做到通俗易懂,你的文章是否能夠做到風趣幽默,你的文章是否能讓讀者學到什麼東西,亦或者說是你想傳達/表達的意思是否表達到位?

比如我們拿一篇源碼分析的文章舉例子,你是否能做好這幾點

  • 你這篇源碼分析,想要教會讀者什麼?
  • 這篇源碼分析,自己看完有沒有疑惑?怎麼解決這些疑惑?或者說哪段代碼比較難以理解,是否再應該詳細解釋?
  • 論述文章中大量代碼對讀者的直觀感受是怎樣的,如果你文章中出現了大量源碼,幾十行以上,而且還帶着中文註釋的話,是否應該以另外的一種形式來展現出來?這些代碼能否放在 Github 上面,讓意猶未盡的讀者得到釋懷?大部分人看文章還是在手機上的,所以你要為他們考慮啊。
  • 對於源碼的話,有必要都貼出來嗎?有沒有可能簡化一些無關代碼,比如日誌打印?邏輯判斷?各種方法調用?這些能否通過流程圖給出呢?
  • 只講關鍵代碼,比如說某段代碼很關鍵,這段代碼做了什麼事兒,能否簡單列一下?
  • 注意講述的措辭,源碼分析的 title 註定就是枯燥無味的,你能否加一些適量措辭,讓源碼分析不再枯燥?

其實上面你滿足一點或幾點的話,都是會有讀者買賬的。某一個點 get 住,讀者就三連了

如果你不想考慮這些問題的話,那你終究還是為了自己寫作了。這個方向也沒錯,那拜託你就不要想着天天還要硬性指標漲粉了,佛系一些更快樂。

上面這段論述是想告訴大家,我寫文章過程中的一些疑惑和注意事項,下面來真正談一談我自己

我不是大神

請千萬擦亮眼睛來看我,我真的不是大神。一部分人給我留言非常尊重我,把我封為大佬、大神,其實我真不是。我只是一個堅持學習,堅持分享的程序員,想要通過文章獲得更多的關注,擴大自己的影響力罷了。

這裡有一點大家需要注意一下,在了解一個人之前,千萬不要給他人過早的樹立 IP,下面就是我犯的一個錯誤。

只通過三言兩句就覺得別人非常厲害?萬一他只是一個網絡搬運工呢?

所以一些讀者朋友可能看我文章寫的還能看過去,就覺得我是大神,其實真不是。但是我不可能和每個人都講我不是大神,我只能默默的承擔這個稱呼,欲戴皇冠,必承其重,我本是一個普通學校畢業的辣雞,非要給我扣上大神的帽子,讓我的行事、回答樣樣標準,不好意思這個真做不到。

私信問問題

這個我需要說一下自己的看法,關於私信問問題這件事情,我相信大家都有接觸過,而且很多大佬其實都談過了,這裏我也說一下自己的想法,暫時列出來幾類吧

  • 私信問代碼怎麼跑不通,能否幫忙看一下。這種問題我現在一般的回復就是 不好意思,我沒有時間來看這些問題,請發到群里謝謝,一方面是我菜,在一方面是我本理解讀者的意思,着急、摳了好久摳不出來,但是你有沒有想過,解決一個問題的成本有多少?你需要和他溝通,你需要把你表達的意思闡述到位,你需要讓他理解清楚你的困惑,你需要理解他的意思,你需要理解他的意思然後懂得對應的知識點,你需要理解他的意思懂得對應的知識點成功的把問題解決,流程圖如下。

任何一個環節遺漏都會提高溝通成本,大家都非常忙,真的沒有時間給你解決這種代碼問題……

換個角度想,代碼問題其實是提高你自己解決辦法的一個機會,如果你是學生,你要學會自己修改代碼。如果你是職場人士,那就不用多說了,大家都是吃這口飯的。

  • 私信問這錯誤怎麼回事?這種問題問出來我覺得就是對人的不尊重。代碼錯誤的原因有很多種,你要讓其他人都給你分析到位嗎?有的時候貼出來自己的代碼片段,但是你以為你以為的就是你以為的嗎?

我記得有很多人寫過關於如何詢問一個技術問題的回答,stackoverflow 上面好像也有這個回答,我找到了一個回答。

https://princetonuniversity.github.io/PUbootcamp/sessions/technical-questions/HowToAskQuestions2018Bootcamp.pdf

真實情況下我們都不會仔細研究這個 pdf,所以普世的我覺得能接受的問問題方式就是

  • 針對 xxx 情況,有沒有什麼解決辦法或者方向?
  • 針對 xxx ,有沒有可以參考的書籍或者是博客/論文?
  • 大家有沒有了解過 xxx ,針對 xxx 的 xxx 問題,大家有沒有好的建議?

理性提問,拒絕做伸手黨,沒有人有義務的幫你解決問題,提問題前請先想好自己能否把問題描述清楚,需要得到什麼樣的回答。很多人埋怨自己問問題沒人回,總覺得是他人的原因,其實是自己根本沒有描述好問題本身,就是自己不知道自己有問題,這是最可怕的

關於時間管理

很多人問我如何管理時間的,這個我要哭了

真的沒有什麼時間管理辦法……我一般都是硬肝,就算你列周全一個詳細的計劃表,也會被各種各樣的因素所打斷,打斷了之後就要做計劃變更,一兩件事情還好,一旦多了之後,可以想像你這個計劃表還有意義嗎?

或者說是這樣,可以夜深人靜沒有人打擾的時候,做一份學習計劃呢

但是有幾個時間管理的小技巧需要注意一下

問題:每個人都特么只有 24 小時,怎麼能讓自己的時間變得比別人多?

那特么只能睡的少啊!!!

如果能正確理解上面的意思,就可以看看下面這些小技巧

  • 關於出行,公交/地鐵,這是比較理想化的出行方式,因為路上可以學習東西,我一般在公交/地鐵上看指導性、結構化的東西,比如一本書他的章節是怎樣的,值不值得看。不會細摳面試題、源碼之類,別問為什麼,容易頭暈。
  • 上班摸魚時間,這個大家都懂。建議這個時間多搜羅一些好的文章,及時收藏保存,會很有用的。
  • 早上早起的時間是最適合看書的時間,看源碼/文章都可以,這個時間太寶貴了
  • 有朋友問女友太纏着我怎麼辦,這個無解,只能把她哄開心了你才能做自己的事兒,要不只能吵架,吵架更麻煩,還得哄着,需要時間更多。所以遇到一個不可拒絕的條件,只能受着,你讓她開開心心,你才能開開心心。太難了。
  • 關於飯後時間,我相信大部分人都拿着手機和朋友圈在刷,這個時間我一般都會讀一些收藏過的文章,效果也很不錯

關於學習

我之前提過,學習是最簡單的事兒,同時也是你應該始終堅持的一件事情。

關於學習的重要性比如三天不讀書,智商輸給豬這類的,我就不再多說了。

我說一下我學習的順序把

  • 一般早上和晚上,我都會刷計算機基礎之類的書,或者源碼之類
  • 工作時間,一般都會看框架方面的書
  • 累了,才會看小說這類

聯繫我

下面我匯總了一下 Java 高頻面試題 PDF,可以關注公眾號回復 面試題 領取

另外,我最近發起了一個 Github 項目,裏面有我認為比較全的技術棧,祝你學習一臂之力。

https://github.com/crisxuan/bestJavaer

點擊閱讀原文跳轉,歡迎各位 star

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

【其他文章推薦】

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

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

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

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

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