安全性能五星+的領克01,動態表現能拿幾分?

勇登峰環節展示了領克01較強的脫困能力,領克01搭配了BorgWarner第五代智能四驅系統,同時接近角度20°、離去角度25。4°、通過角20。2°,再加上發動機的大扭矩輸出,在這種程度的測試中表現得游刃有餘,確實讓駕駛員在這種環境下也充滿了信心。

4月21日,中國汽車技術研究中心發布了2018年度C-NCAp第一批車型的評價結果,共有13款車型參与測試,其中10款車型獲得五星。值得一提的是,此次測試中領克01獲得五星+評級,這也是現行中國汽車安全碰撞標準體系2015版規則中第一款,也是目前唯一一款獲得五星+成績的車型。

其實對於這個結果,雖然感到有些意外,但其實這也是情理之中的事情。畢竟領克01是吉利與沃爾沃合作開發的CMA基礎模塊架構下的產品,而且是第一款產品,對於吉利和沃爾沃來說這是出來打響招牌的車型,所以必定是會投入最多的精力來打磨。

所以得到在豪華品牌中都以安全著稱的沃爾沃技術支持的領克01,在安全測試中得不到好評才不正常,但是直接拿到了2015版規則以來的第一個五星+評級這樣的好成績確實不容易。

就在評價結果公布的這一天,正好參加了“領克01都市競技場”廣州站的活動,在試駕場地中對領克01進行了實際的試駕,那麼領克01的動態表現是否也像安全測試成績一樣能拿到五星+的評價呢?

此次試駕活動是在廣州五號停機坪購物廣場中進行,由於場地大小及時間上的限制,這一次領克01隻安排了三個項目的體驗:對戰競技環節、勇奪峰、智行環。

對戰競技環節,這一環節其實是領克01針對操控方面進行展示,通過一段較短距離的急加速、急減速及一段繞樁測試來展示領克搭載的2.0T發動機的強勁動力輸出、6速自動變速箱和7速DCT雙離合變速器的清晰的換擋邏輯和較快的換擋速度,還有百公里制動距離達到34米的出色制動效果,以及偏運動取向但整體調教良好的懸挂系統。

智行環體驗環節主要是展示領克01的自適應巡航功能以及在中低速下的自動排隊功能,整體表現良好,但是可能是現場道路指向不夠清晰,在彎度較大探測不到前車軌跡的時候,會出現自動排隊功能失效的情況。

勇登峰環節展示了領克01較強的脫困能力,領克01搭配了BorgWarner第五代智能四驅系統,同時接近角度20°、離去角度25.4°、通過角20.2°,再加上發動機的大扭矩輸出,在這種程度的測試中表現得游刃有餘,確實讓駕駛員在這種環境下也充滿了信心。

其實從車輛試駕的角度來看,這三個項目環節太少、時間太短,因此並不足以試出太多深淺,就好像是用一套小學題目來考驗初中學生,對這個價位的大部分車型來說,這些都只能算是基礎操作。因此對於領克01這種基礎功紮實的好學生來說,就更加沒什麼挑戰性了。通過這幾個項目的測試,只能說領克01確實基本功紮實,其他的表現還需要更深入的體驗才行。

活動最後,對領克銷售公司大區經理進行了群訪,據最新了解到的消息,同屬於CMA基礎模塊架構的領克02預計在年中上市發布,而領克03也將在年底正式亮相。對於這兩款車型也是抱有較高的期待的。從目前市場反饋來看,領克01在3月份已經賣了8000多輛,隨着銷售渠道的不斷擴張,其銷量很可能會有進一步的提高,對於領克這樣一個售價並不低廉的新品牌來說,首款車型能賣到這樣的銷量還是比較不錯的。

作為中國品牌向上突圍的排頭兵,領克給了國內消費者不小的驚喜,從安全測試的成績和實際試駕的表現來看,領克01確實有着中國品牌中數一數二的實力。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

中華鳥會遭除名 英國慈善委員會將調查

摘錄自2020年9月30日中央社報導

路透社報導,掌管慈善團體的英國慈善委員會(Charity Commission)29日發表聲明,國際鳥盟已提交事件報告,委員會將檢視所有提交資訊進行調查,以決定下一步動作。

中華鳥會自1996年成為全球最大的鳥類保育組織「國際鳥盟」(BirdLife International)會員。但國際鳥盟全球理事會2週前決議,由於中華鳥會的中文名稱「中華民國野鳥學會」對國際鳥盟的夥伴關係造成經營上的「風險」,因此將中華鳥會自正式夥伴關係中移除。

儘管國際鳥盟將中華鳥會除名,但許多組織仍表示將繼續與中華鳥會合作,包括國際鳥盟的重要夥伴英國皇家鳥類保護協會(Royal Society for the Protection of Birds)。

國際鳥盟禁止員工就此事發表評論,並未立即回應。

生物多樣性
國際新聞
英國
鳥會
政治因素

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

7獅「外出」捕食農場10隻羊 南非國家公園全數撲殺

摘錄自2020年10月02日自由時報、三立新聞網南非報導

根據南非《News24》報導,南非國家公園管理處(South Africa National Parks)公關部門主任塔克胡里(Rey Thakhuli)表示,卡魯國家公園的獅群於28日時一天內兩次溜出園區,清晨先是跑到附近的農場殺死10隻綿羊,晚上又跑出去搜尋羊隻屍體,為了為防止農莊居民與牲畜再度受到攻擊,南非國家公園管理處經過評估後,出動直升機撲殺撲殺2頭未成年公獅、3頭未成年母獅以及2頭成年母獅。

塔克胡里指出,這7頭獅子是「會造成破壞的動物群體」,而且長年聚集在公園北方邊界的高山地區,造成附近農莊居民與牲畜造成嚴重安全威脅,所以園方依照管理法規撲殺獅子。

南非四爪動物協會(FOUR PAWS)主任麥爾斯(Fiona Miles)心痛表示「這是非常悲慘的一天」,認為奪取獅群的生命絕對是最後的手段,應該要先試試看其他方法。麥爾斯進一步表示,南非的野獅剩不到3000隻,國家公園的射殺舉動,無疑對獅群是重大的打擊。國家公園事後回覆,會舉辦研討會討論這次的事件,後續有相關消息,會再公布給大家。

生物多樣性
國際新聞
南非
國家公園
人與動物衝突事件簿

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

【其他文章推薦】

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

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

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

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

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

超詳細實戰教程丨多場景解析如何遷移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網頁設計為架站首選

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

※回頭車貨運收費標準

「譯」懂點那啥編譯

原文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主題演講提到的)參与它們的開發。

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

【其他文章推薦】

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

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

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

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

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

歐盟氣候研究組織:9月創歷來高溫紀錄

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

歐洲聯盟地球觀測計畫(Earth Observation Programme)今(7日)表示,今年9月地球表面的氣溫較歷來的9月溫暖,且1月以來的氣溫和史上最熱的2016年同一期間相差無幾。

根攄歐盟氣候監測機構「哥白尼氣候變化服務」(Copernicus Climate Change Service),今年有三個月創下高溫紀錄,分別是1月、5月和9月。

從去年9月至今年9月,地球溫度比工業化前高出將近攝氏1.3度。這項發現令人非常憂心,因為非常接近聯合國政府間氣候變遷問題小組(IPCC)2018年報告提出的攝氏1.5度門檻。報告中闡述地球升溫攝氏1.5度後將產生的重大衝擊。

氣候變遷
國際新聞
歐盟
記錄高溫

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

中國若要達2060年碳中和目標 得編150兆預算投入再生能源

摘錄自2020年10月8日自由時報報導

國際能源顧問公司伍德麥肯茲(Wood Mackenzie)今(8日)對中國2060年碳中和目標提出分析,表示此一目標中國恐將花超過新台幣150兆元經費,其中又會牽涉到煤礦省份未來所面臨的轉型問題,對此,該顧問公司認為,碳捕存(CCS)技術將成中國達成目標的關鍵,若少了碳捕存,則碳中和目標幾乎不可能。

據路透社報導,伍德麥肯茲公司今天表示,中國若要達到2060年碳中和目標,預計需要的投資金額將超過5兆美元(約為新台幣150兆元),其中包含發展再生能源發電設施等,中國國家主席習近平9月在聯合國成立75週年視訊大會上表示,中國要在2060年達到「碳中和」目標,即為碳的排放量會等於減少量。目前,中國是世界上最大的碳排放國,約佔全球排放量的28%。

氣候變遷
國際新聞
中國
碳中和
碳捕存技術

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

【其他文章推薦】

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

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

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

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

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

開着容易上癮,這3款10來萬的合資車,比朗逸好玩得多

6L的動力版本,會讓你在日常代步之餘,也能享受到小鋼炮的魅力。至於1。5T的,朋友,秋名山見。福克斯的短板在於儲物空間較為一般,後排在兩廂車這個級別裏面算是表現一般,中下的水平。致悅,沒錯,不是致炫,是來自菲亞特选手的致悅,至於為啥會推薦它,不是它表現的多突出,主要是價格便宜,優惠還大,這是許多人買車考慮的問題之一。

緊湊型車三廂的感覺操控不過來?

有便宜的兩廂車推薦嗎?

想買兩廂車怎麼辦?

有哪些兩廂車選擇?

首先是來自馬自達选手帶來的昂科賽拉,對於這款車大家都不陌生,操控就不說了,兩廂車操控還不好也不好意思推薦了,昂科賽拉的底盤懸挂是想要表揚的,一個字,穩,即使是高速過彎,你也不會覺得虛。

在眾多車企紛紛投入渦輪增壓發動機的大軍中去是,馬自達堅持做自吸發動機,這簡直是個異類,但是馬自達憑藉創馳藍天技術使得昂科賽拉在動力方面不遜色於搭載渦輪發動機的車型,而且還省油,這也是馬自達敢拿自吸發動機打着運動旗號的原因了。

後排較小,是昂科賽拉的短板,這也是許多馬自達車型的短板了,買馬自達的朋友們要注意哦~

另外是昂科賽拉在胎躁控制方面的還是不夠,這是許多車主反應的問題了。

福特选手帶來的則是福克斯,福克斯是大家心目中的理想車型之一了,美系車的隔音好,配置豐富的特點都具備,還有眾多的動力選擇。

1.0T的動力總成,對於日常上下班代步通勤的朋友們,會說,夠用!1.6L的動力版本,會讓你在日常代步之餘,也能享受到小鋼炮的魅力!至於1.5T的,朋友,秋名山見!

福克斯的短板在於儲物空間較為一般,後排在兩廂車這個級別裏面算是表現一般,中下的水平。

致悅,沒錯,不是致炫,是來自菲亞特选手的致悅,至於為啥會推薦它,不是它表現的多突出,主要是價格便宜,優惠還大,這是許多人買車考慮的問題之一。

除了價格之外,致悅的空間表現以及動力表現也是挺不錯的,全系搭載1.4T渦輪增壓發動機,動力能說不夠嗎?用車主的話說,就是一給油就跑!

從車身尺寸以及乘坐空間測試來看,致悅的表現在同級別不能說最好,但是中上水平還是可以有的。而讓人不滿意的是所搭配的DCT雙離合變速器在低擋換擋的時候頓挫感比較明顯。

以上三款車都是所推薦的兩廂車,綜合實力來說都是比較強的,各位看官可以根據自己的需求來挑選喲!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

【Spring註解驅動開發】面試官:如何將Service注入到Servlet中?朋友又栽了!!

寫在前面

最近,一位讀者出去面試前準備了很久,信心滿滿的去面試。沒想到面試官的一個問題把他難住了。面試官的問題是這樣的:如何使用Spring將Service注入到Servlet中呢?這位讀者平時也是很努力的,看什麼源碼啊、多線程啊、高併發啊、設計模式啊等等。沒想到卻在一個很簡單的問題上栽了跟頭,這就說明學習知識要系統化,要有條理,切忌東學一點,西記一點,否則,到頭來,啥也學不到。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

如何實現將Service注入到Servlet中??

這裏,我們列舉兩種解決方法(推薦使用第二種)

方法一:

直接重寫Servlet的Init()方法,代碼如下:

public void init(ServletConfig servletConfig) throws ServletException {
	ServletContext servletContext = servletConfig.getServletContext();
	WebApplicationContext webApplicationContext = WebApplicationContextUtils
			.getWebApplicationContext(servletContext);
	AutowireCapableBeanFactory autowireCapableBeanFactory = webApplicationContext
			.getAutowireCapableBeanFactory();
	autowireCapableBeanFactory.configureBean(this, BEAN_NAME);
}

這裏的BEAN_NAME即為我們需要注入到Spring容器中的服務,但這並不是一個好的方法,因為我們需要在每一個Servlet中都進行這樣的操作。

方法二:

我們可以寫一個類似於“org.springframework.web.struts.DelegatingRequestProcessor”的委託的Bean,然後通過配置的方法把我們的服務注入到servlet中,具體方法如下,

Step 1:編寫委託類DelegatingServletProxy

package com.telek.pba.base.util;

import java.io.IOException;
import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * 以下是類似org.springframework.web.struts.DelegatingRequestProcessor的一個委託
 * 用於通過配置的方法,在Servlet中注入Service
 * @author binghe
 * */
public class DelegatingServletProxy extends GenericServlet{
    private static final long serialVersionUID = 1L;
    private String targetBean;
    private Servlet proxy;

   @Override
   public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
   		proxy.service(req, res);
   }
     /**
      * 初始化
      */
      public void init() throws ServletException {
          this.targetBean = getServletName();
          getServletBean();
          proxy.init(getServletConfig());
      }

     /**
      * 獲取Bean
      */
      private void getServletBean() {
          WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
          this.proxy = (Servlet) wac.getBean(targetBean);
      }
}

Step 2:修改Web.xml配置

在純Servlet模式下,我們的配置方式如下(以下由於代碼高亮插件的問題,請將代碼中的#替換成尖括號)

<servlet>
  <description>活動發起模塊活動查詢分頁Servlet</description>
  <display-name>launchActivityQueryServlet</display>
  <servlet-name>LaunchActivityQueryServlet</servlet-name>
  <servlet-class>com.telek.pba.launch.servlet.LaunchActivityQueryServlet</servlet-class>
<servlet>

<servlet-mapping>
  <servlet-name>LaunchActivityQueryServlet</servlet-name>
  <url-pattern>/servlet/launch/LaunchActivityQueryServlet</url-pattern>
</servlet-mapping>
</servlet>

如果採用我們這種代理的方法,則配置應該修改為:

<servlet>
  <description>活動發起模塊活動查詢分頁Servlet</description>
  <display-name>launchActivityQueryServlet</display>
  <servlet-name>launchActivityQueryServlet</servlet-name>
  <servlet-class>com.telek.pba.base.util.DelegatingServletProxy</servlet-class>
<servlet>

<servlet-mapping>
  <servlet-name>launchActivityQuery</servlet-name>
  <url-pattern>/servlet/launch/LaunchActivityQueryServlet</url-pattern>
</servlet-mapping>
</servlet> 

注意:默認情況下,Servlet的配置中,LaunchActivityQuery的首字母一般為大寫,而我們的標題中已註明,我們採用Spring的註解模式,如果是自動掃描註解的話,默認情況下,註解的value值為首字母小寫,即:launchActivityQuery,因此,在我們新的配置中,要注意將首字母改為小寫,否則會報無法找到Bean的錯誤。

Step 3:至此,我們就可以像SSH的注入方式一樣,注入Servlet了,以下是個小示例:

package com.telek.pba.launch.servlet;

import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import com.telek.pba.base.model.PbaUserInfo;
import com.telek.pba.launch.dao.IPbaActivityInfoCurrentDAO;

@Component
public class LaunchActivityQueryServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	 
	//注入IPbaActivityInfoCurrentDAO
	@Resource
	private IPbaActivityInfoCurrentDAO pbaActivityInfoCurrentDAO;

	public LaunchActivityQueryServlet() {
		super();
	}
	 
	public void destroy() {
		super.destroy(); // Just puts "destroy" string in log
		// Put your code here
	}
	 
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		//sth to do
	}
	 
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		//sth to do
	}
	 
	public void init() throws ServletException {
		// Put your code here
	}
}

最後,請留心在Spring配置文件中,配置上自動掃描包的路徑:

<context:component-scan base-package="com.telek.pba.*.dao.impl,
 					com.telek.pba.*.service.impl,
 					com.telek.pba.*.servlet"/>

大功告成!

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

python工業互聯網應用實戰2—從需求開始

前言:隨着國家工業2025戰略的推進,工業互聯網發展將會提速,將迎來一個新的發展時期,越來越多的企業開始逐步的把產線自動化,去年年底投產的小米亦庄的智能工廠就是一個熱議的新聞。小米/華為智能工廠只能說是中國製造2025的一個代表,產業轉型和製造升級,筆者從事的企業領域就越到越來越多的(製造)企業開始悄悄的自動化/智能化。這裏肯定有國家政策推動的大背景,同時,也有着企業自身不斷提高生產率的“剛需”。

  本序列我們也從一個需求問題開始;然後,拆解需求(問題);其次,解決拆解需求(問題)的點;再次,通過的不斷技術摸索和迭代過程找到一個個合理的解決需求的方法和手段,最終,完成了這個需求(問題)的項目實戰。我們會在文中描述演化過程、方法論、過程保證機制和實用的工具等,最終帶着大家完成這樣一個實際項目需求的項目過程項目涉及的Django的多基礎知識請大家閱讀筆者早期的《Python開發入門與實戰系列》。

  本文的過程採用python3.6和django2.1版本

項目需求:這是一個簡版物流自動化倉庫實例,倉庫控制系統(以下簡稱WCS)需要調度AGV小車運送一個實托盤(搬運單元)從1樓入庫站台經由提升機搬運到2樓指定的倉庫貨位。 

1.需求分析

  倉庫控制系統(以下簡稱WCS)需要調度AGV小車運送一個實托盤(搬運單元)從1樓入庫站台經由提升機搬運到2樓指定的倉庫貨位。這句簡要描述常常是我們在項目URS看到的需求描述,接下來我們需要對拿到的需求進行分析,形成一個個可度量的開發功能點。

1.1.用例說明

  需求用例說明文檔能夠很好的對需求進行詳細的分析,主要的包含內容包括:前置條件、事件流程和後置條件(執行結果)用例描述如下錶,通過用例分析我們能夠很好的把握需求的具體事件執行流程,並通過文檔清晰的描述出來,便於後期設計編碼時作為開發設計人員的指導。

  經過團隊頭腦風暴討論,大家基本上達成了如下錶的需求用例說明,我們把這個調度設備的搬運過程設計成一個序列順序執行的步驟(子任務),這些子任務對應着設備的執行分解動作。

用例編碼

1.1

執行角色

WCS

用例名稱

托盤入庫用例

前提條件

  1. WMS已完成任務貨位的分配,任務核心信息:托盤條碼、源地址、目標地址;
  2. AGV不能跨樓層作業,1樓與2樓分別是不同的AGV小車執行任務;

需求描述

1 WCS分解搬運任務成3個設備作業

2 調度AGV入庫站台搬運托盤到提升機1樓門口工位;

3 調度提升機到1樓並打開廊門

4 調度AGV提升機1樓門口工位到提升機並卸貨,返回提升機門口工位;

5 調度提升機1樓提升到2樓並開門;

6 調度AGV進入提升機並載貨搬運到2樓門口工位;

7 調度提升機關門;

8 調度AGV從2樓門口工位搬運到庫存貨位。

備選過程

 

擴展過程

 

 

原始需求描述

參見 《XXX URS》

特殊需求

後置條件

WCS調度相關設備完成實托盤從入庫站台搬運到庫存貨位,反饋結果給WMS 

2. 需求功能點

  從上面的用例說明的需求描述事件流程來看,我們需要先把這一趟搬運任務分解成設備子任務,並串行的方式順序下達到設備執行,任務清單如下錶:

序號

作業描述

執行設備

1

調度AGV入庫站台搬運托盤到提升機1樓門口工位

1AGV

2

調度提升機到1樓並打開門

提升機

3

調度AGV提升機1樓門口工位到提升機並卸貨,返回提升機門口工位

1AGV

4

調度提升機1樓提升到2樓並開門

提升機

5

調度AGV進入提升機並載貨搬運到2樓門口工位

2AGV

6

調度提升機關門

提升機

7

調度AGV從2樓門口工位搬運到庫存貨位

2AGV

  也就是說這一趟搬運任務,WCS需要分解成7個設備作業子任務,並順序下達給相應的執行設備執行,最終完成任務的執行(當前的任務劃分粒度實際對接AGV和提升機廠家來說會有調整,最終以上步驟會依賴與實際對接的情況,但是主流程不會有太大變化)。 

   經過分析從1樓入庫站台運送托盤到二樓某個指定貨位這樣一個任務,系統需要分解成7個子任務,下達給設備順序執行。系統活動圖如下:

3. 活動圖

  經過分析從1樓入庫站台運送托盤到2樓某個指定貨位這樣一個任務,系統需要分解成7個子任務,下達給設備順序執行。我們還可以通過UML活動圖來進一步詳細的描述作業的執行順序如下圖: 

    從圖中我們可以看出來一次入庫任務,系統分解為7個設備子任務(作業)來執行完整的托盤入庫流程,只有所有子任務(作業)執行完成,托盤的入庫才算完成。

 4. 功能模塊

  對於這樣一個看似簡單的需求來說,包含兩大主要功能模塊

    1. 任務分解:依據物理設備處理任務的條件,對任務進行分解,任務分解的粒度是設備能夠識別並執行(動作)
    2. 任務調度:任務調度就是按照順序執行的邏輯,把任務順序和逐一下達給設備 

  這裏也有幾個基本邏輯就是,設備在某一個時間點上只能執行一個子任務,只有這個任務執行完畢後方能下達新的子任務。多重任務邏輯只會導致設備無法完成任務(不知道到底該執行那個動作)。

 4.1. 實體關係

  我們從上面需求分析整理當前至少包括2個實體,包含的屬性(字段)如下: 

1任務

任務ID,任務號,源地址(從哪兒),目標地址(到哪兒),開始時間,結束時間,狀態 

2子任務:

子任務ID,任務ID,源地址(從哪兒 上一個子任務的目標地址), 目標地址(到哪兒 下一個子任務的源地址), 執行機構,開始時間,結束時間,狀態

5. 小節

  本章我們從一個需求問題開始,然後經過需求分析,把需求問題分解為功能點和數據實體,實體是下一步我們設計表或ORM model的基礎原型,上面的實體只是一個初步的需求分析主要字段要求,實體屬性(字段)會設計時會增加特定的其它屬性(字段)。 下一章節我們將描繪如何把這個需求逐步演化到模型設計。

 

 

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

【其他文章推薦】

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

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

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

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

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