008.Kubernetes二進制部署Nginx實現高可用

一 Nginx代理實現kube-apiserver高可用

1.1 Nginx實現高可用


基於 nginx 代理的 kube-apiserver 高可用方案。

控制節點的 kube-controller-manager、kube-scheduler 是多實例部署,所以只要有一個實例正常,就可以保證高可用;

集群內的 Pod 使用 K8S 服務域名 kubernetes 訪問 kube-apiserver, kube-dns 會自動解析出多個 kube-apiserver 節點的 IP,所以也是高可用的;

在每個節點起一個 nginx 進程,後端對接多個 apiserver 實例,nginx 對它們做健康檢查和負載均衡;

kubelet、kube-proxy、controller-manager、scheduler 通過本地的 nginx(監聽 127.0.0.1)訪問 kube-apiserver,從而實現 kube-apiserver 的高可用;

從而基於 nginx 4 層透明代理功能實現 K8S 節點( master 節點和 worker 節點)高可用訪問 kube-apiserver 。

1.2 下載編譯Nginx

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# wget http://nginx.org/download/nginx-1.15.3.tar.gz
  3 [root@k8smaster01 work]# tar -xzvf nginx-1.15.3.tar.gz
  4 [root@k8smaster01 ~]# cd /opt/k8s/work/nginx-1.15.3/
  5 [root@k8smaster01 nginx-1.15.3]# mkdir nginx-prefix
  6 [root@k8smaster01 nginx-1.15.3]# ./configure --with-stream --without-http --prefix=$(pwd)/nginx-prefix --without-http_uwsgi_module --without-http_scgi_module --without-http_fastcgi_module
  7 [root@k8smaster01 ~]# cd /opt/k8s/work/nginx-1.15.3/
  8 [root@k8smaster01 nginx-1.15.3]# make && make install



解釋:

  • –with-stream:開啟 4 層透明轉發(TCP Proxy)功能;
  • –without-xxx:關閉所有其他功能,這樣生成的動態鏈接二進製程序依賴最小。
  • [root@k8smaster01 ~]# cd /opt/k8s/work/nginx-1.15.3/
  • [root@k8smaster01 nginx-1.15.3]# ./nginx-prefix/sbin/nginx -v

1.3 驗證編譯后的Nginx

  1 [root@k8smaster01 ~]# cd /opt/k8s/work/nginx-1.15.3
  2 [root@k8smaster01 nginx-1.15.3]# ./nginx-prefix/sbin/nginx -v
  3 nginx version: nginx/1.15.3
  4 [root@k8smaster01 nginx-1.15.3]# ldd ./nginx-prefix/sbin/nginx	#查看 nginx 動態鏈接的庫
  5         linux-vdso.so.1 =>  (0x00007ffdda980000)
  6         libdl.so.2 => /lib64/libdl.so.2 (0x00007feb37300000)
  7         libpthread.so.0 => /lib64/libpthread.so.0 (0x00007feb370e4000)
  8         libc.so.6 => /lib64/libc.so.6 (0x00007feb36d17000)
  9         /lib64/ld-linux-x86-64.so.2 (0x00007feb37504000)



提示:由於只開啟了 4 層透明轉發功能,所以除了依賴 libc 等操作系統核心 lib 庫外,沒有對其它 lib 的依賴(如 libz、libssl 等),以便達到精簡編譯的目的。

1.4 安裝和部署Nginx

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     mkdir -p /opt/k8s/kube-nginx/{conf,logs,sbin}
  7   done						#創建Nginx目錄
  8 [root@k8smaster01 ~]# cd /opt/k8s/work
  9 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
 10 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
 11   do
 12     echo ">>> ${master_ip}"
 13     scp /opt/k8s/work/nginx-1.15.3/nginx-prefix/sbin/nginx  root@${master_ip}:/opt/k8s/kube-nginx/sbin/kube-nginx
 14     ssh root@${master_ip} "chmod a+x /opt/k8s/kube-nginx/sbin/*"
 15     ssh root@${master_ip} "mkdir -p /opt/k8s/kube-nginx/{conf,logs,sbin}"
 16   done						#分發Nginx二進制


1.5 配置Nginx 四層透明轉發

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cat > kube-nginx.conf <<EOF
  3 worker_processes 1;
  4 
  5 events {
  6     worker_connections  1024;
  7 }
  8 
  9 stream {
 10     upstream backend {
 11         hash $remote_addr consistent;
 12         server 172.24.8.71:6443        max_fails=3 fail_timeout=30s;
 13         server 172.24.8.72:6443        max_fails=3 fail_timeout=30s;
 14         server 172.24.8.73:6443        max_fails=3 fail_timeout=30s;
 15     }
 16 
 17     server {
 18         listen 127.0.0.1:8443;
 19         proxy_connect_timeout 1s;
 20         proxy_pass backend;
 21     }
 22 }
 23 EOF
 24 [root@k8smaster01 ~]# cd /opt/k8s/work
 25 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
 26 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
 27   do
 28     echo ">>> ${master_ip}"
 29     scp kube-nginx.conf  root@${master_ip}:/opt/k8s/kube-nginx/conf/kube-nginx.conf
 30   done						#分發Nginx四層透明代理配置文件


1.6 配置Nginx system

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cat > kube-nginx.service <<EOF
  3 [Unit]
  4 Description=kube-apiserver nginx proxy
  5 After=network.target
  6 After=network-online.target
  7 Wants=network-online.target
  8 
  9 [Service]
 10 Type=forking
 11 ExecStartPre=/opt/k8s/kube-nginx/sbin/kube-nginx -c /opt/k8s/kube-nginx/conf/kube-nginx.conf -p /opt/k8s/kube-nginx -t
 12 ExecStart=/opt/k8s/kube-nginx/sbin/kube-nginx -c /opt/k8s/kube-nginx/conf/kube-nginx.conf -p /opt/k8s/kube-nginx
 13 ExecReload=/opt/k8s/kube-nginx/sbin/kube-nginx -c /opt/k8s/kube-nginx/conf/kube-nginx.conf -p /opt/k8s/kube-nginx -s reload
 14 PrivateTmp=true
 15 Restart=always
 16 RestartSec=5
 17 StartLimitInterval=0
 18 LimitNOFILE=65536
 19 
 20 [Install]
 21 WantedBy=multi-user.target
 22 EOF


1.7 分發Nginx systemd

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     scp kube-nginx.service  root@${master_ip}:/etc/systemd/system/
  7   done


二 啟動並驗證

2.1 啟動Nginx

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     ssh root@${master_ip} "systemctl daemon-reload && systemctl enable kube-nginx && systemctl restart kube-nginx"
  7   done


2.2 檢查Nginx服務

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     ssh root@${master_ip} "systemctl status kube-nginx |grep 'Active:'"
  7   done


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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

[ PyQt入門教程 ] PyQt5中數據表格控件QTableWidget使用方法

 如果你想讓你開發的PyQt5工具展示的數據顯得整齊、美觀、好看,顯得符合你的氣質,可以考慮使用QTableWidget控件。之前一直使用的是textBrowser文本框控件,數據展示還是不太美觀。其中QTableWidget是PyQt5程序中常用的显示數據表格的控件,显示的基本效果如下,有點素。。

  下面開始介紹QTableWidget常用的方法以及如何使用。既然是數據表格形式,經常使用的場景其實跟excel我覺得差不多。開始學習使用QTableWidget之前,我們帶着如下幾個問題再開始本文的閱讀。

  1、如何在GUI界面上創建QTableWidget表格?指定N行M列,列名稱等等

  2、如何在表格里添加每一個單元格的數據?

  3、如何排版數據表格的數據,比如你想單元格內容居中、左對齊、右對齊等。

  4、如何設置文字显示顏色、如何設置單元格背景色?

  5、如何合併指定單元格,讓排版更合理?

  基本上使用數據表格展示數據常見的問題就是這些,能夠熟練使用QTableWidget解決上述問題,QTableWidget基本使用方法應該就會了。下面開始學習本文內容。

QTableWidget常用方法

  setROwCount(int row)     設置QTableWidget表格控件的行數

  setColumnCount(int col)     設置QTableWidget表格控件的列數

  setHorizontalHeaderLabels()     設置QTableWidget表格控件的水平標籤

  setVerticalHeaderLabels()     設置QTableWidget表格控件的垂直標籤

  setItem(int ,int ,QTableWidgetItem)     在QTableWidget表格控件的每個選項的單元控件內添加控件

  horizontalHeader()     獲得QTableWidget表格控件的表格頭,以便執行隱藏

  rowCount()     獲得QTableWidget表格控件的行數

  columnCount()     獲得QTableWidget表格控件的列數

  setEditTriggers(EditTriggers triggers)     設置表格是否可以編輯,設置表格的枚舉值

  setSelectionBehavior     設置表格的選擇行為

  setTextAlignment()     設置單元格內文本的對齊方式

  setSpan(int row,int column,int rowSpanCount,int columnSpanCount)     合併單元格,要改變單元格的第row行,column列,要合併rowSpancount行數和columnSpanCount列數。其中row表示要改變的行數, column表示要改變的列數,rowSpanCount表示需要合併的行數,columnSpanCount表示需要合併的列數。

  setShowGrid()     在默認情況下錶格的显示是有網格的,可以設置True或False用於是否显示,默認True

  setColumnWidth(int column,int width)     設置單元格行的寬度

  setRowHeight(int row,int height)     設置單元格列的高度

  這裏的函數有些有點不好理解,比如setSpan()合併單元格函數,你可能一下不知道它的使用方法以及實現效果。沒關係,下面會通過實例講解以及效果演示展示這些函數的使用方法。

QTableWidget使用實例

 1、使用designer實現一個包含QTableWidget數據展示控件的窗體。界面設計一般都會採用designer工具,因為要考慮控件間的布局,純代碼實現會增加難度。界面實現如下

 雙擊在窗體界面上的QTableWidget控件,分別選擇Edit Table Widget中Columns、Rows、Items進行編輯。可以分別完成行、列標題以及單元格內容的添加。

 完成后,效果圖如下

 2、使用pyuic5工具將.ui文件轉換為.py文件。

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'cc.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_widget(object):
    def setupUi(self, widget):
        widget.setObjectName("widget")
        widget.resize(730, 574)
        self.tableWidget = QtWidgets.QTableWidget(widget)
        self.tableWidget.setGeometry(QtCore.QRect(10, 130, 701, 192))
        self.tableWidget.setObjectName("tableWidget")
        self.tableWidget.setColumnCount(4)
        self.tableWidget.setRowCount(3)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setVerticalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setVerticalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setVerticalHeaderItem(2, item)
        item = QtWidgets.QTableWidgetItem()
        item.setTextAlignment(QtCore.Qt.AlignCenter)
        self.tableWidget.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setHorizontalHeaderItem(2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setHorizontalHeaderItem(3, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(0, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(0, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(0, 2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(0, 3, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(1, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(1, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(1, 2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(1, 3, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(2, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(2, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(2, 2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidget.setItem(2, 3, item)
        self.tableWidget.horizontalHeader().setCascadingSectionResizes(True)

        self.retranslateUi(widget)
        QtCore.QMetaObject.connectSlotsByName(widget)

    def retranslateUi(self, widget):
        _translate = QtCore.QCoreApplication.translate
        widget.setWindowTitle(_translate("widget", "員工信息表"))
        item = self.tableWidget.verticalHeaderItem(0)
        item.setText(_translate("widget", "1"))
        item = self.tableWidget.verticalHeaderItem(1)
        item.setText(_translate("widget", "2"))
        item = self.tableWidget.verticalHeaderItem(2)
        item.setText(_translate("widget", "3"))
        item = self.tableWidget.horizontalHeaderItem(0)
        item.setText(_translate("widget", "姓名"))
        item = self.tableWidget.horizontalHeaderItem(1)
        item.setText(_translate("widget", "年齡"))
        item = self.tableWidget.horizontalHeaderItem(2)
        item.setText(_translate("widget", "性別"))
        item = self.tableWidget.horizontalHeaderItem(3)
        item.setText(_translate("widget", "基本信息"))
        __sortingEnabled = self.tableWidget.isSortingEnabled()
        self.tableWidget.setSortingEnabled(False)
        item = self.tableWidget.item(0, 0)
        item.setText(_translate("widget", "張三"))
        item = self.tableWidget.item(0, 1)
        item.setText(_translate("widget", "23"))
        item = self.tableWidget.item(0, 2)
        item.setText(_translate("widget", "Male"))
        item = self.tableWidget.item(0, 3)
        item.setText(_translate("widget", "家裡有礦"))
        item = self.tableWidget.item(1, 0)
        item.setText(_translate("widget", "王八"))
        item = self.tableWidget.item(1, 1)
        item.setText(_translate("widget", "25"))
        item = self.tableWidget.item(1, 2)
        item.setText(_translate("widget", "Female"))
        item = self.tableWidget.item(1, 3)
        item.setText(_translate("widget", "拆二代"))
        item = self.tableWidget.item(2, 0)
        item.setText(_translate("widget", "趙四"))
        item = self.tableWidget.item(2, 1)
        item.setText(_translate("widget", "28"))
        item = self.tableWidget.item(2, 2)
        item.setText(_translate("widget", "Male"))
        item = self.tableWidget.item(2, 3)
        item.setText(_translate("widget", "勤奮努力的程序員"))
        self.tableWidget.setSortingEnabled(__sortingEnabled)

附designer工具設計完成的.ui文件代碼。不作展示,方便有需要的讀者下載使用。

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>widget</class>
 <widget class="QWidget" name="widget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>730</width>
    <height>286</height>
   </rect>
  </property>
  <property name="font">
   <font>
    <pointsize>10</pointsize>
   </font>
  </property>
  <property name="windowTitle">
   <string>員工信息表</string>
  </property>
  <widget class="QTableWidget" name="tableWidget">
   <property name="geometry">
    <rect>
     <x>10</x>
     <y>51</y>
     <width>701</width>
     <height>211</height>
    </rect>
   </property>
   <attribute name="horizontalHeaderCascadingSectionResizes">
    <bool>true</bool>
   </attribute>
   <row>
    <property name="text">
     <string>1</string>
    </property>
   </row>
   <row>
    <property name="text">
     <string>2</string>
    </property>
   </row>
   <row>
    <property name="text">
     <string>3</string>
    </property>
   </row>
   <column>
    <property name="text">
     <string>姓名</string>
    </property>
    <property name="textAlignment">
     <set>AlignCenter</set>
    </property>
   </column>
   <column>
    <property name="text">
     <string>年齡</string>
    </property>
   </column>
   <column>
    <property name="text">
     <string>性別</string>
    </property>
   </column>
   <column>
    <property name="text">
     <string>基本信息</string>
    </property>
   </column>
   <item row="0" column="0">
    <property name="text">
     <string>張三</string>
    </property>
   </item>
   <item row="0" column="1">
    <property name="text">
     <string>23</string>
    </property>
   </item>
   <item row="0" column="2">
    <property name="text">
     <string>Male</string>
    </property>
   </item>
   <item row="0" column="3">
    <property name="text">
     <string>家裡有礦</string>
    </property>
   </item>
   <item row="1" column="0">
    <property name="text">
     <string>王八</string>
    </property>
   </item>
   <item row="1" column="1">
    <property name="text">
     <string>25</string>
    </property>
   </item>
   <item row="1" column="2">
    <property name="text">
     <string>Female</string>
    </property>
   </item>
   <item row="1" column="3">
    <property name="text">
     <string>拆二代</string>
    </property>
   </item>
   <item row="2" column="0">
    <property name="text">
     <string>趙四</string>
    </property>
   </item>
   <item row="2" column="1">
    <property name="text">
     <string>28</string>
    </property>
   </item>
   <item row="2" column="2">
    <property name="text">
     <string>Male</string>
    </property>
   </item>
   <item row="2" column="3">
    <property name="text">
     <string>勤奮努力的程序員</string>
    </property>
   </item>
  </widget>
  <widget class="QLabel" name="label">
   <property name="geometry">
    <rect>
     <x>250</x>
     <y>20</y>
     <width>111</width>
     <height>21</height>
    </rect>
   </property>
   <property name="font">
    <font>
     <family>YaHei Consolas Hybrid</family>
     <pointsize>10</pointsize>
    </font>
   </property>
   <property name="text">
    <string>員工信息展示</string>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

View Code

 3、編寫主程序,調用界面.py文件。使業界面和邏輯程序分離,這樣的好處就是後面界面任何改動程序邏輯幾乎不會有什麼大的影響。

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'connect_me.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
#導入程序運行必須模塊
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QColor, QBrush
from cc import Ui_widget


class MyMainForm(QMainWindow, Ui_widget):
    def __init__(self, parent=None):
        super(MyMainForm, self).__init__(parent)
        self.setupUi(self)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWin = MyMainForm()
    myWin.show()
    sys.exit(app.exec_())

 運行程序結果如下:

 4、程序運行可以正常显示界面,但是界面並不是很美觀(比如行、列沒有鋪滿整個显示窗口、列標題文字沒有加粗等)。下面通過QTableWidget相關函數的使用來實現優化。

QTableWidget函數使用方法

  上述基本例子實現了員工信息表格展示的初始化,單元格內容手工添加其實在實現場景不合適,只是方便展示,下面講解如何通過代碼實現方式添加單元格內容以及實現單元格內容顏色、字體變化的實現。說明,下述所有代碼添加都在主程序中完成。目的是使程序實現更靈活,方便維護。

 (1)初始化QTableWidget數據展示窗口對象。一般來說該步驟通常是通過designer實現。

self.tableWidget = QtWidgets.QTableWidget(widget) self.tableWidget.setColumnCount(4) self.tableWidget.setRowCount(3)

  設置表格的行、列標題,如下:

self.tableWidget.setHorizontalHeaderLabels(['姓名','年齡']) self.tableWidget.setVerticalHeaderLabels(['1','2'])

 當然也可以通過上面例子中的方式去添加,如下:

item = QtWidgets.QTableWidgetItem()
self.tableWidget.setVerticalHeaderItem(1, item)

item = QtWidgets.QTableWidgetItem()
self.tableWidget.setHorizontalHeaderItem(1, item)

(2)設置表格頭的伸縮模式,也就是讓表格鋪滿整個QTableWidget控件。

self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
效果如下:

 (2)將表格變為禁止編輯。默認情況下錶格中的字符串是可以更改的,比如雙擊一個單元格,就可以修改運來的內容。如果想禁止這種操作,讓表格對用戶是只讀的,可以添加如下代碼。

self.tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)

運行效果(雙擊單元格不可編輯,提前觀察一下鼠標單擊選中的只是單元格):

(3)設置表格整行選中。表格默認選中的是單個單元格。通過下面代碼可以設置成選中整行。添加代碼如下

self.tableWidget.setSelectionBehavior(QAbstractItemView.SelectRows)

運行效果如下:

  另外,單元格選中的類型還可以修改成如下方式:

  QAbstractItemView.SelectColumns 選中1列

  QAbstractItemView.SelectRows  選中1行

  QAbstractItemView.SelectItems 選中1個單元格

(4)行、列標題的显示與隱藏。

  對於列標題的显示或隱藏設置,可以通過添加如下代碼實現。默認是显示的。

self.tableWidget.horizontalHeader().setVisible(False)

運行效果如下(行標題不再显示):

對於行標題,可以使用如下代碼進行隱藏或显示設置。默認是显示
self.tableWidget.verticalHeader().setVisible(

 (5)在表格中添加內容。代碼如下:

        items = [['燕十三','21','Male','武林大俠'],['蕭十一郎','21','Male','武功好']]

        for i in range(len(items)):
            item = items[i]
            row = self.tableWidget.rowCount()
            self.tableWidget.insertRow(row)
            for j in range(len(item)):
                item = QTableWidgetItem(str(items[i][j]))
                self.tableWidget.setItem(row,j,item)

運行結果如下:

(6) 設置表格標題字體加粗。添加代碼如下:

font = self.tableWidget.horizontalHeader().font()
font.setBold(True)
self.tableWidget.horizontalHeader().setFont(font)

運行效果如下

(7) 設置表格指定列寬。添加代碼如下:

self.tableWidget.horizontalHeader().resizeSection(0,100)
self.tableWidget.horizontalHeader().resizeSection(1,100)
self.tableWidget.horizontalHeader().resizeSection(1,100)
self.tableWidget.horizontalHeader().resizeSection(3,400)
#self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 一定要註釋,否則上述設置不生效

說明,表頭的自適應模式要註釋,否則上述設置不生效。運行效果如下:

 (8)合併單元格,添加如下代碼

self.tableWidget.setSpan(0,3,2,1)

運行結果如下:

 這裏再補充描述一下setSpan()函數。setSpan(0,3,2,1)函數中(0,3)會指定到單元格第1行第4列(下標都是從0開始計算),(2,1)分別表示合併2行(把下一行合併了),1列(1列,列其實沒有合併)。我們把代碼修改為setSpan(1,2,3,2)。看效果如下(很直觀了吧):

(9)對齊單元格中的內容。添加代碼如下:

for i in range(len(items)):
    each_item = items[i]
    row = self.tableWidget.rowCount()
    self.tableWidget.insertRow(row)
    for j in range(len(each_item)):
        item = QTableWidgetItem(str(items[i][j]))
        if j != len(each_item) -1:
            item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.tableWidget.setItem(row,j,item)

  關鍵代碼就是item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)。前面3列居中显示,最後一列靠左显示。運行效果如下:

  另外,單元格內容對齊還有如下幾種方式。

  Qt.AlignLeft 將單元格內容沿單元格左邊緣對齊

  Qt.AlignRight 將單元格內容沿單元格右邊緣對齊

  Qt.AlignHCenter 將單元格內容居中显示在水平方向上。

  Qt.AlignJustify 將文本在可用的空間中對齊,默認是從左到右的

  Qt.AlignTop 與頂部對齊

  Qt.AlignBottom 與底部對齊

  Qt.AlignVCenter 在可用的空間中,居中显示在垂直方向上

  Qt.AlignBaseline 與基線對齊

(10)設置單元格字體顏色和背景顏色。添加代碼如下:

for i in range(len(items)):
    each_item = items[i]
    row = self.tableWidget.rowCount()
    self.tableWidget.insertRow(row)
    for j in range(len(each_item)):
        item = QTableWidgetItem(str(items[i][j]))
        if j != len(each_item) -1:
            item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
            item.setForeground(QBrush(QColor(255,0,0))) #設置除最後一列外的文字顏色為紅色 else:
            item.setBackground(QBrush(QColor(0,255,0))) #設置最後一列的背景色為綠色
        self.tableWidget.setItem(row,j,item)

運行結果如下所示:

 QTableWidget控件的基本使用方法就到這裏,後續有其他更好的使用技巧會繼續更新,為方便自己及大家閱讀本文,附本文使用到的代碼如下:

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'connect_me.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
#導入程序運行必須模塊
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QColor, QBrush
from PyQt5.QtCore import Qt
from cc import Ui_widget


class MyMainForm(QMainWindow, Ui_widget):
    def __init__(self, parent=None):
        super(MyMainForm, self).__init__(parent)
        self.setupUi(self)
        #self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.tableWidget.setSelectionBehavior(QAbstractItemView.SelectRows)
        #self.tableWidget.horizontalHeader().setVisible(False)
        self.tableWidget.verticalHeader().setVisible(False)
        font = self.tableWidget.horizontalHeader().font()
        font.setBold(True)
        self.tableWidget.horizontalHeader().setFont(font)
        self.tableWidget.horizontalHeader().resizeSection(0,100)
        self.tableWidget.horizontalHeader().resizeSection(1,100)
        self.tableWidget.horizontalHeader().resizeSection(1,100)
        self.tableWidget.horizontalHeader().resizeSection(3,400)
        #self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        items = [['燕十三','21','Male','武林大俠'],['蕭十一郎','21','Male','武功好']]
        for i in range(len(items)):
            each_item = items[i]
            row = self.tableWidget.rowCount()
            self.tableWidget.insertRow(row)
            for j in range(len(each_item)):
                item = QTableWidgetItem(str(items[i][j]))
                if j != len(each_item) -1:
                    item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
                    item.setForeground(QBrush(QColor(255,0,0)))
                else:
                    item.setBackground(QBrush(QColor(0,255,0)))
                self.tableWidget.setItem(row,j,item)

        self.tableWidget.setSpan(1,2,3,2)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWin = MyMainForm()
    myWin.show()
    sys.exit(app.exec_())

 總結

  本文描述PyQt5的QTableWidget控件的基本使用方法和效果,界面實現建議還是desiger設計工具實現,同時使用業務和邏輯相分離的方式編寫程序,都揉在一塊後面程序就不好維護,畢竟沒有誰的工具實現是可以一蹴而就的,還是要考慮可維護性。讀完本文內容相信讀者就可以上手使用QTableWidget控件了。。

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

海洋暖化再創新高 能量如36億枚原子彈爆炸

摘錄自2020年1月14日星島日報、公共電視報導

13日發表的一項最新研究報告指出,世界各大海洋的暖化程度十分嚴重,2019年的海水溫度又再創下有紀錄以來的新高水平。科學家形容目前的情況,好比每一秒投放五枚廣島原子彈在海中。

由14位科學家組成的國際團隊合作進行研究,分析上世紀50年代到近年的據數,計算海洋水面至2,000米深的溫度。他們在科學期刊《大氣科學進展》內發表報告,指全球各地海洋暖化的速度愈來愈快。報告指出,在1955至1986年間,海洋暖化的步伐保持穩定,但在過去數十年加速。在1987至2019年間,海洋暖化程度是對上數十年的四倍半。

2019年的海洋溫度,比1981至2010年間的平均溫度高出攝氏0.075度。過去25年海洋吸收的熱力,相等於36億次廣島原子彈爆炸。

研究也顯示,過去5年同時也是有紀錄以來海洋溫度最高的5年。高溫除了使得海洋生物如珊瑚,魚類和海鳥消失,更會導致海平面上升,造成沿岸洪災,以及產生更多颶風,帶來嚴重災情。

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

水都威尼斯運河乾涸 「貢多拉」無法航行

摘錄自2020年1月14日星島日報報導

意大利水都威尼斯兩個月前經歷嚴重洪患,當時大部分市區淹沒在水中。但威尼斯近日卻遭逢低潮,當地運河幾乎乾涸,令河道上的船隻無法航行。

遭逢極端低潮的威尼斯水位大幅下降,河道上的小船「貢多拉」擱淺在岸邊,城內運河看起來更像是充滿泥濘的溝渠,這也讓大多數市民面臨交通問題。澳洲廣播公司(ABC)報道,威尼斯水位一度降到海平面以下45厘米。當局稱,受低水位影響最大的是聖保羅(San Polo)和聖十字(Santa Croce)區的河道。氣象專家預測,未來幾天水位暫不會回升。威尼斯運河在2015、16和18年也出現過乾涸。

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

河北省:將新能源汽車應用到9大公共服務領域

河北省擴大公共服務領域用車範疇,將黨政機關公務車、機要通信車、執法執勤巡邏車、環衛車、郵政車、接送學生校車、城鄉公車(含農村客運車)、城市物流配送車、計程車等九個方面的新能源汽車納入公共服務領域範圍。   河北省要求,全省在新增或更換城市公車以及上述九類的汽車,必須換成新能源汽車,如果再換燃油、燃氣車,將不予上牌。九類新能源汽車購車補貼比例由1:0.5提高到1:1。  
推廣純電動計程車、新能源公車   明年起,河北省將啟動純電動計程車試點工作,張家口市、廊坊市作為首批純電動計程車試點市,各開展50-200輛的純電動計程車運營公司試點工作。逐步在全省範圍內推廣電動計程車。   河北省將統籌考慮用油、用氣、新能源公車一定期限內購置及運營成本,建立鼓勵新能源公車應用、限制燃油公車增長的新機制,調整現行燃油補貼政策,加大對新能源公車支持力度。2015年至2019年,我省新增及更換的公車中新能源公車比重將分別達到40%、50%、60%、70%、80%。  
加快充電設施建設   河北省將進一步加快充電設施建設,把充電站、充電樁建設納入城鄉建設規劃,按照統籌規劃、科學佈局、適度超前、有序建設的原則,加快推進住宅社區、單位內部、社會停車場等面向個人使用的充電設施建設,到2020年基本建成車樁相隨、智能高效的充電基礎設施體系。   河北省將在石家莊、張家口、邯鄲三市進行充電設施建設試點,整合企業、社會、政府等多方力量,在賓館飯店、商場超市、學校醫院、路邊車位、旅遊景點等公共場所,開展充電樁、充電樁群建設,形成規模效應。全省黨政機關、事業單位以及國有企業等也將積極建設充電樁,省直機關率先安裝,做出示範。   該消息是河北新聞網記者從11月27日召開的全省新能源汽車發展和推廣應用工作領導小組(擴大)會議上獲悉的。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

山西省:發佈《加快推進電動汽車產業發展和推廣應用的實施意見(公開徵求意見稿)》通知

為加快推進電動汽車產業發展和推廣應用,山西省擬於近期出臺《加快推進電動汽車產業發展和推廣應用的實施意見》(以下簡稱《實施意見》)。為保障公民、法人和其他社會組織的知情權、參與權和監督權,做到依法決策、科學決策、民主決策,現將《實施意見(公開徵求意見稿)》公佈,廣泛徵求社會各界意見,敬請提出寶貴意見和建議,於2015年11月18日24:00時前,通過電子郵件發送至郵箱:

政策原文:

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

webpack 打包優化的四種方法(多進程打包,多進程壓縮,資源 CDN,動態 polyfill)

如今,webpack 毫無疑問是前端構建領域里最耀眼的一顆星,無論你前端走哪條路線,都需要有很強的webpack 知識。webpack 的基本用法這裏就不展開講了。主要探討一下如何提高 webpack 的打包速度。

這篇文章以 vue cli3.0+webpack4.0+nodejs10.0+ 這幾個版本為例。

一、打包分析

1.1、速度分析

我們的目的是優化打包速度,那肯定需要一個速度分析插件,此時 speed-measure-webpack-plugin 就派上用場了。它的作用如下:

  • 分析整個打包總耗時
  • 每個 pluginloader 的耗時情況

首先,安裝插件

npm i -D speed-measure-webpack-plugin

然後修改 vue.config.js 配置文件 (vuecli3+ 版本的配置文件統一在這個文件里修改,如果沒有該文件,在根目錄新建一個)

// 導入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// 實例化插件
const smp = new SpeedMeasurePlugin();

module.exports = {
    configureWebpack: smp.wrap({
        plugins: [
            // 這裡是自己項目里需要使用到的其他插件
            new yourOtherPlugin()
        ]
    })
}

運行打包命令之後,可以看到,打包總耗時為 2min,51.99s

1.2、體積分析

分析完打包速度之後,接着我們來分析打包之後每個文件以及每個模塊對應的體積大小。使用到的插件為 webpack-bundle-analyzer,構建完成後會在 8888 端口展示大小。

首先,安裝插件

npm i -D webpack-bundle-analyzer

修改 vue.config.js 配置文件

// 導入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// 導入體積分析插件
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

// 實例化速度分析插件
const smp = new SpeedMeasurePlugin();

module.exports = {
    configureWebpack: smp.wrap({
        plugins: [
            // 實例化體積分析插件
            new BundleAnalyzerPlugin()
        ]
    })
}

構建之後可以看到,其中黃色塊 chunk-vendors 文件佔比最大,為 1.34MB

二、打包優化

2.1、多進程多實例構建,資源并行解析

多進程構建的方案比較知名的有以下三個:

  • thread-loader (推薦使用這個)
  • parallel-webpack
  • HappyPack

這裏以 thread-loader 為例配置多進程多實例構建
安裝 loader

npm i -D thread-loader

接下來在 vue.config.js 配置文件中使用該 loader

// 導入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// 實例化插件
const smp = new SpeedMeasurePlugin();
module.exports = {
    configureWebpack: smp.wrap({
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: ['thread-loader']
                }
            ]
        }
    })
}

然後看下構建花費的時間, 2min,49.21s,相較於之前提升了 5s

2.2、公用代碼提取,使用 CDN 加載

對於vue,vuex,vue-router,axios,echarts,swiper等我們可以利用webpack的externals參數來配置,這裏我們設定只需要在生產環境中才需要使用。
下面配置 vue.config.js

// 導入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// 導入體積分析插件
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

//判斷是否為生產環境
const isProduction = process.env.NODE_ENV === 'production';

//定義 CDN 路徑,這裏採用 bootstrap 的 cdn
const cdn = {
    css: [
        'https://cdn.bootcss.com/Swiper/4.5.1/css/swiper.min.css'
    ],
    js: [
        'https://cdn.bootcss.com/vue/2.6.10/vue.min.js',
        'https://cdn.bootcss.com/vue-router/3.1.3/vue-router.min.js',
        'https://cdn.bootcss.com/vuex/3.1.1/vuex.min.js',
        'https://cdn.bootcss.com/axios/0.19.0/axios.min.js',
        'https://cdn.bootcss.com/echarts/4.3.0/echarts.min.js',
        'https://cdn.bootcss.com/Swiper/4.5.1/js/swiper.min.js',
    ]
}

// 實例化插件
const smp = new SpeedMeasurePlugin();
module.exports = {
    chainWebpack: config => {
        // 生產環境配置
        if (isProduction) {
            // 生產環境注入 cdn
            config.plugin('html')
                .tap(args => {
                    args[0].cdn = cdn;
                    return args;
                });
        }
    },
    configureWebpack: smp.wrap({
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: ['thread-loader']
                }
            ]
        },
        plugins: [
            new BundleAnalyzerPlugin()
        ],
        //生產環境注入 cdn
        externals: isProduction && {
            'vue': 'Vue',
            'vuex': 'Vuex',
            'vue-router': 'VueRouter',
            'axios': 'axios',
            'echarts': 'echarts',
            'swiper': 'Swiper'
        } || {}
    })
}

緊接着,改造 html 頁面。用於讓我們配置的 cdn 路徑注入到 html 頁面

<!DOCTYPE html>
<html lang="zh">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <!-- 使用 CDN 的 CSS 文件 -->
  <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
  <% } %>
</head>

<body>
  <noscript>
    <strong>We're sorry but eye-admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
  </noscript>
  <div id="app"></div>
  <!-- built files will be auto injected -->

 <!-- 使用 CDN 的 JS 文件 -->
  <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
  <% } %>
</body>

</html>

最重要的一步,看下效果,可以看到現在耗時 1min,39.19s,整整提升了 1 分多鍾!

2.3、多進程多實例并行壓縮

并行壓縮主流有以下三種方案

  • 使用 parallel-uglify-plugin 插件
  • uglifyjs-webpack-plugin 開啟 parallel 參數
  • terser-webpack-plugin 開啟 parallel 參數 (推薦使用這個,支持 ES6 語法壓縮)

安裝插件依賴

npm i -D terser-webpack-plugin

接下來在 vue.config.js 配置文件中使用插件,最終的配置文件如下

// 導入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// 導入代碼壓縮插件
const TerserPlugin = require("terser-webpack-plugin");

// 導入體積分析插件
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

//判斷是否為生產環境
const isProduction = process.env.NODE_ENV === 'production';

//定義 CDN 路徑,這裏採用 bootstrap 的 cdn
const cdn = {
    css: [
        'https://cdn.bootcss.com/Swiper/4.5.1/css/swiper.min.css'
    ],
    js: [
        'https://cdn.bootcss.com/vue/2.6.10/vue.min.js',
        'https://cdn.bootcss.com/vue-router/3.1.3/vue-router.min.js',
        'https://cdn.bootcss.com/vuex/3.1.1/vuex.min.js',
        'https://cdn.bootcss.com/axios/0.19.0/axios.min.js',
        'https://cdn.bootcss.com/echarts/4.3.0/echarts.min.js',
        'https://cdn.bootcss.com/Swiper/4.5.1/js/swiper.min.js',
    ]
}

// 實例化插件
const smp = new SpeedMeasurePlugin();
module.exports = {
    chainWebpack: config => {
        // 生產環境配置
        if (isProduction) {
            // 生產環境注入 cdn
            config.plugin('html')
                .tap(args => {
                    args[0].cdn = cdn;
                    return args;
                });
        }
    },
    configureWebpack: smp.wrap({
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: ['thread-loader']
                }
            ]
        },
        plugins: [
            new BundleAnalyzerPlugin()
        ],
        //生產環境注入 cdn
        externals: isProduction && {
            'vue': 'Vue',
            'vuex': 'Vuex',
            'vue-router': 'VueRouter',
            'axios': 'axios',
            'echarts': 'echarts',
            'swiper': 'Swiper'
        } || {},
        optimization: {
            minimizer: [
                new TerserPlugin({
                    parallel: 4
                })
            ]
        }
    })
}

2.4、使用 polyfill 動態服務

動態 polyfill 指的是根據不同的瀏覽器,動態載入需要的 polyfillPolyfill.io 通過嘗試使用 polyfill 重新創建缺少的功能,可以更輕鬆地支持不同的瀏覽器,並且可以大幅度的減少構建體積。

Polyfill Service 原理

識別 User Agent,下發不同的 Polyfill

使用方法:在 index.html 中引入如下 script 標籤

<script crossorigin="anonymous" src=""></script>

三、完結

At last,看完之後有什麼不懂的,可以留言反饋。

轉載請註明出處:
作者:TSY
個人空間:

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

009.Kubernetes二進制部署kube-apiserver

一 部署master節點

1.1 master節點服務


kubernetes master 節點運行如下組件:

  • kube-apiserver
  • kube-scheduler
  • kube-controller-manager
  • kube-nginx


kube-apiserver、kube-scheduler 和 kube-controller-manager 均以多實例模式運行:

kube-scheduler 和 kube-controller-manager 會自動選舉產生一個 leader 實例,其它實例處於阻塞模式,當 leader 掛了后,重新選舉產生新的 leader,從而保證服務可用性。

kube-apiserver 是無狀態的,需要通過 kube-nginx 進行代理訪問,從而保證服務可用性。

1.2 安裝Kubernetes

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# wget https://dl.k8s.io/v1.14.2/kubernetes-server-linux-amd64.tar.gz
  3 [root@k8smaster01 work]# tar -xzvf kubernetes-server-linux-amd64.tar.gz
  4 [root@k8smaster01 work]# cd kubernetes
  5 [root@k8smaster01 kubernetes]# tar -xzvf  kubernetes-src.tar.gz


1.3 分發Kubernetes

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     scp kubernetes/server/bin/{apiextensions-apiserver,cloud-controller-manager,kube-apiserver,kube-controller-manager,kube-proxy,kube-scheduler,kubeadm,kubectl,kubelet,mounter} root@${master_ip}:/opt/k8s/bin/
  7     ssh root@${master_ip} "chmod +x /opt/k8s/bin/*"
  8   done


二 部署高可用kube-apiserver

2.1 高可用apiserver介紹


本實驗部署一個三實例 kube-apiserver 集群的步驟,它們通過 kube-nginx 進行代理訪問,從而保證服務可用性。

2.2 創建Kubernetes證書和私鑰

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cat > kubernetes-csr.json <<EOF
  3 {
  4   "CN": "kubernetes",
  5   "hosts": [
  6     "127.0.0.1",
  7     "172.24.8.71",
  8     "172.24.8.72",
  9     "172.24.8.73",
 10     "${CLUSTER_KUBERNETES_SVC_IP}",
 11     "kubernetes",
 12     "kubernetes.default",
 13     "kubernetes.default.svc",
 14     "kubernetes.default.svc.cluster",
 15     "kubernetes.default.svc.cluster.local."
 16   ],
 17   "key": {
 18     "algo": "rsa",
 19     "size": 2048
 20   },
 21   "names": [
 22     {
 23       "C": "CN",
 24       "ST": "Shanghai",
 25       "L": "Shanghai",
 26       "O": "k8s",
 27       "OU": "System"
 28     }
 29   ]
 30 }
 31 EOF
 32 #創建Kubernetes的CA證書請求文件



解釋:

hosts 字段指定授權使用該證書的 IP 和域名列表,這裏列出了 master 節點 IP、kubernetes 服務的 IP 和域名;

kubernetes 服務 IP 是 apiserver 自動創建的,一般是 –service-cluster-ip-range 參數指定的網段的第一個IP,後續可以通過下面命令獲取:

  1 # kubectl get svc kubernetes




  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cfssl gencert -ca=/opt/k8s/work/ca.pem \
  3 -ca-key=/opt/k8s/work/ca-key.pem -config=/opt/k8s/work/ca-config.json \
  4 -profile=kubernetes kubernetes-csr.json | cfssljson -bare kubernetes	#生成CA密鑰(ca-key.pem)和證書(ca.pem)


2.3 分發證書和私鑰

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     ssh root@${master_ip} "mkdir -p /etc/kubernetes/cert"
  7     scp kubernetes*.pem root@${master_ip}:/etc/kubernetes/cert/
  8   done


2.4 創建加密配置文件

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# cat > encryption-config.yaml <<EOF
  4 kind: EncryptionConfig
  5 apiVersion: v1
  6 resources:
  7   - resources:
  8       - secrets
  9     providers:
 10       - aescbc:
 11           keys:
 12             - name: key1
 13               secret: ${ENCRYPTION_KEY}
 14       - identity: {}
 15 EOF


2.5 分發加密配置文件

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     scp encryption-config.yaml root@${master_ip}:/etc/kubernetes/
  7   done


2.6 創建審計策略文件

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# cat > audit-policy.yaml <<EOF
  4 apiVersion: audit.k8s.io/v1beta1
  5 kind: Policy
  6 rules:
  7   # The following requests were manually identified as high-volume and low-risk, so drop them.
  8   - level: None
  9     resources:
 10       - group: ""
 11         resources:
 12           - endpoints
 13           - services
 14           - services/status
 15     users:
 16       - 'system:kube-proxy'
 17     verbs:
 18       - watch
 19 
 20   - level: None
 21     resources:
 22       - group: ""
 23         resources:
 24           - nodes
 25           - nodes/status
 26     userGroups:
 27       - 'system:nodes'
 28     verbs:
 29       - get
 30 
 31   - level: None
 32     namespaces:
 33       - kube-system
 34     resources:
 35       - group: ""
 36         resources:
 37           - endpoints
 38     users:
 39       - 'system:kube-controller-manager'
 40       - 'system:kube-scheduler'
 41       - 'system:serviceaccount:kube-system:endpoint-controller'
 42     verbs:
 43       - get
 44       - update
 45 
 46   - level: None
 47     resources:
 48       - group: ""
 49         resources:
 50           - namespaces
 51           - namespaces/status
 52           - namespaces/finalize
 53     users:
 54       - 'system:apiserver'
 55     verbs:
 56       - get
 57 
 58   # Don't log HPA fetching metrics.
 59   - level: None
 60     resources:
 61       - group: metrics.k8s.io
 62     users:
 63       - 'system:kube-controller-manager'
 64     verbs:
 65       - get
 66       - list
 67 
 68   # Don't log these read-only URLs.
 69   - level: None
 70     nonResourceURLs:
 71       - '/healthz*'
 72       - /version
 73       - '/swagger*'
 74 
 75   # Don't log events requests.
 76   - level: None
 77     resources:
 78       - group: ""
 79         resources:
 80           - events
 81 
 82   # node and pod status calls from nodes are high-volume and can be large, don't log responses for expected updates from nodes
 83   - level: Request
 84     omitStages:
 85       - RequestReceived
 86     resources:
 87       - group: ""
 88         resources:
 89           - nodes/status
 90           - pods/status
 91     users:
 92       - kubelet
 93       - 'system:node-problem-detector'
 94       - 'system:serviceaccount:kube-system:node-problem-detector'
 95     verbs:
 96       - update
 97       - patch
 98 
 99   - level: Request
100     omitStages:
101       - RequestReceived
102     resources:
103       - group: ""
104         resources:
105           - nodes/status
106           - pods/status
107     userGroups:
108       - 'system:nodes'
109     verbs:
110       - update
111       - patch
112 
113   # deletecollection calls can be large, don't log responses for expected namespace deletions
114   - level: Request
115     omitStages:
116       - RequestReceived
117     users:
118       - 'system:serviceaccount:kube-system:namespace-controller'
119     verbs:
120       - deletecollection
121 
122   # Secrets, ConfigMaps, and TokenReviews can contain sensitive & binary data,
123   # so only log at the Metadata level.
124   - level: Metadata
125     omitStages:
126       - RequestReceived
127     resources:
128       - group: ""
129         resources:
130           - secrets
131           - configmaps
132       - group: authentication.k8s.io
133         resources:
134           - tokenreviews
135   # Get repsonses can be large; skip them.
136   - level: Request
137     omitStages:
138       - RequestReceived
139     resources:
140       - group: ""
141       - group: admissionregistration.k8s.io
142       - group: apiextensions.k8s.io
143       - group: apiregistration.k8s.io
144       - group: apps
145       - group: authentication.k8s.io
146       - group: authorization.k8s.io
147       - group: autoscaling
148       - group: batch
149       - group: certificates.k8s.io
150       - group: extensions
151       - group: metrics.k8s.io
152       - group: networking.k8s.io
153       - group: policy
154       - group: rbac.authorization.k8s.io
155       - group: scheduling.k8s.io
156       - group: settings.k8s.io
157       - group: storage.k8s.io
158     verbs:
159       - get
160       - list
161       - watch
162 
163   # Default level for known APIs
164   - level: RequestResponse
165     omitStages:
166       - RequestReceived
167     resources:
168       - group: ""
169       - group: admissionregistration.k8s.io
170       - group: apiextensions.k8s.io
171       - group: apiregistration.k8s.io
172       - group: apps
173       - group: authentication.k8s.io
174       - group: authorization.k8s.io
175       - group: autoscaling
176       - group: batch
177       - group: certificates.k8s.io
178       - group: extensions
179       - group: metrics.k8s.io
180       - group: networking.k8s.io
181       - group: policy
182       - group: rbac.authorization.k8s.io
183       - group: scheduling.k8s.io
184       - group: settings.k8s.io
185       - group: storage.k8s.io
186 
187   # Default level for all other requests.
188   - level: Metadata
189     omitStages:
190       - RequestReceived
191 EOF


2.7 分發策略文件

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     scp audit-policy.yaml root@${master_ip}:/etc/kubernetes/audit-policy.yaml
  7   done


2.8 創建訪問 metrics-server的證書和密鑰

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cat > proxy-client-csr.json <<EOF
  3 {
  4   "CN": "aggregator",
  5   "hosts": [],
  6   "key": {
  7     "algo": "rsa",
  8     "size": 2048
  9   },
 10   "names": [
 11     {
 12       "C": "CN",
 13       "ST": "Shanghai",
 14       "L": "Shanghai",
 15       "O": "k8s",
 16       "OU": "System"
 17     }
 18   ]
 19 }
 20 EOF
 21 #創建metrics-server的CA證書請求文件



解釋:

CN 名稱需要位於 kube-apiserver 的 –requestheader-allowed-names 參數中,否則後續訪問 metrics 時會提示權限不足。

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# cfssl gencert -ca=/opt/k8s/work/ca.pem \
  3 -ca-key=/opt/k8s/work/ca-key.pem -config=/opt/k8s/work/ca-config.json \
  4 -profile=kubernetes proxy-client-csr.json | cfssljson -bare proxy-client	#生成CA密鑰(ca-key.pem)和證書(ca.pem)


2.9 分發證書和私鑰

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
  4   do
  5     echo ">>> ${master_ip}"
  6     scp proxy-client*.pem root@${master_ip}:/etc/kubernetes/cert/
  7   done


2.10 創建kube-apiserver的systemd

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# cat > kube-apiserver.service.template <<EOF
  4 [Unit]
  5 Description=Kubernetes API Server
  6 Documentation=https://github.com/GoogleCloudPlatform/kubernetes
  7 After=network.target
  8 
  9 [Service]
 10 WorkingDirectory=${K8S_DIR}/kube-apiserver
 11 ExecStart=/opt/k8s/bin/kube-apiserver \\
 12   --advertise-address=##MASTER_IP## \\
 13   --default-not-ready-toleration-seconds=360 \\
 14   --default-unreachable-toleration-seconds=360 \\
 15   --feature-gates=DynamicAuditing=true \\
 16   --max-mutating-requests-inflight=2000 \\
 17   --max-requests-inflight=4000 \\
 18   --default-watch-cache-size=200 \\
 19   --delete-collection-workers=2 \\
 20   --encryption-provider-config=/etc/kubernetes/encryption-config.yaml \\
 21   --etcd-cafile=/etc/kubernetes/cert/ca.pem \\
 22   --etcd-certfile=/etc/kubernetes/cert/kubernetes.pem \\
 23   --etcd-keyfile=/etc/kubernetes/cert/kubernetes-key.pem \\
 24   --etcd-servers=${ETCD_ENDPOINTS} \\
 25   --bind-address=##MASTER_IP## \\
 26   --secure-port=6443 \\
 27   --tls-cert-file=/etc/kubernetes/cert/kubernetes.pem \\
 28   --tls-private-key-file=/etc/kubernetes/cert/kubernetes-key.pem \\
 29   --insecure-port=0 \\
 30   --audit-dynamic-configuration \\
 31   --audit-log-maxage=15 \\
 32   --audit-log-maxbackup=3 \\
 33   --audit-log-maxsize=100 \\
 34   --audit-log-mode=batch \\
 35   --audit-log-truncate-enabled \\
 36   --audit-log-batch-buffer-size=20000 \\
 37   --audit-log-batch-max-size=2 \\
 38   --audit-log-path=${K8S_DIR}/kube-apiserver/audit.log \\
 39   --audit-policy-file=/etc/kubernetes/audit-policy.yaml \\
 40   --profiling \\
 41   --anonymous-auth=false \\
 42   --client-ca-file=/etc/kubernetes/cert/ca.pem \\
 43   --enable-bootstrap-token-auth \\
 44   --requestheader-allowed-names="aggregator" \\
 45   --requestheader-client-ca-file=/etc/kubernetes/cert/ca.pem \\
 46   --requestheader-extra-headers-prefix="X-Remote-Extra-" \\
 47   --requestheader-group-headers=X-Remote-Group \\
 48   --requestheader-username-headers=X-Remote-User \\
 49   --service-account-key-file=/etc/kubernetes/cert/ca.pem \\
 50   --authorization-mode=Node,RBAC \\
 51   --runtime-config=api/all=true \\
 52   --enable-admission-plugins=NodeRestriction \\
 53   --allow-privileged=true \\
 54   --apiserver-count=3 \\
 55   --event-ttl=168h \\
 56   --kubelet-certificate-authority=/etc/kubernetes/cert/ca.pem \\
 57   --kubelet-client-certificate=/etc/kubernetes/cert/kubernetes.pem \\
 58   --kubelet-client-key=/etc/kubernetes/cert/kubernetes-key.pem \\
 59   --kubelet-https=true \\
 60   --kubelet-timeout=10s \\
 61   --proxy-client-cert-file=/etc/kubernetes/cert/proxy-client.pem \\
 62   --proxy-client-key-file=/etc/kubernetes/cert/proxy-client-key.pem \\
 63   --service-cluster-ip-range=${SERVICE_CIDR} \\
 64   --service-node-port-range=${NODE_PORT_RANGE} \\
 65   --logtostderr=true \\
 66   --v=2
 67 Restart=on-failure
 68 RestartSec=10
 69 Type=notify
 70 LimitNOFILE=65536
 71 
 72 [Install]
 73 WantedBy=multi-user.target
 74 EOF



解釋:

  • –advertise-address:apiserver 對外通告的 IP(kubernetes 服務後端節點 IP);
  • –default-*-toleration-seconds:設置節點異常相關的閾值;
  • –max-*-requests-inflight:請求相關的最大閾值;
  • –etcd-*:訪問 etcd 的證書和 etcd 服務器地址;
  • –experimental-encryption-provider-config:指定用於加密 etcd 中 secret 的配置;
  • –bind-address: https 監聽的 IP,不能為 127.0.0.1,否則外界不能訪問它的安全端口 6443;
  • –secret-port:https 監聽端口;
  • –insecure-port=0:關閉監聽 http 非安全端口(8080);
  • –tls-*-file:指定 apiserver 使用的證書、私鑰和 CA 文件;
  • –audit-*:配置審計策略和審計日誌文件相關的參數;
  • –client-ca-file:驗證 client (kue-controller-manager、kube-scheduler、kubelet、kube-proxy 等)請求所帶的證書;
  • –enable-bootstrap-token-auth:啟用 kubelet bootstrap 的 token 認證;
  • –requestheader-*:kube-apiserver 的 aggregator layer 相關的配置參數,proxy-client & HPA 需要使用;
  • –requestheader-client-ca-file:用於簽名 –proxy-client-cert-file 和 –proxy-client-key-file 指定的證書;在啟用了 metric aggregator 時使用;
  • –requestheader-allowed-names:不能為空,值為逗號分割的 –proxy-client-cert-file 證書的 CN 名稱,這裏設置為 “aggregator”;
  • –service-account-key-file:簽名 ServiceAccount Token 的公鑰文件,kube-controller-manager 的 –service-account-private-key-file 指定私鑰文件,兩者配對使用;
  • –runtime-config=api/all=true: 啟用所有版本的 APIs,如 autoscaling/v2alpha1;
  • –authorization-mode=Node,RBAC、–anonymous-auth=false: 開啟 Node 和 RBAC 授權模式,拒絕未授權的請求;
  • –enable-admission-plugins:啟用一些默認關閉的 plugins;
  • –allow-privileged:運行執行 privileged 權限的容器;
  • –apiserver-count=3:指定 apiserver 實例的數量;
  • –event-ttl:指定 events 的保存時間;
  • –kubelet-*:如果指定,則使用 https 訪問 kubelet APIs;需要為證書對應的用戶(上面 kubernetes*.pem 證書的用戶為 kubernetes) 用戶定義 RBAC 規則,否則訪問 kubelet API 時提示未授權;
  • –proxy-client-*:apiserver 訪問 metrics-server 使用的證書;
  • –service-cluster-ip-range: 指定 Service Cluster IP 地址段;
  • –service-node-port-range: 指定 NodePort 的端口範圍。


提示:如果 kube-apiserver 機器沒有運行 kube-proxy,則還需要添加 –enable-aggregator-routing=true 參數。

注意:requestheader-client-ca-file 指定的 CA 證書,必須具有 client auth and server auth;

如果 –requestheader-allowed-names 為空,或者 –proxy-client-cert-file 證書的 CN 名稱不在 allowed-names 中,則後續查看 node 或 pods 的 metrics 失敗,提示:

  1 [root@zhangjun-k8s01 1.8+]# kubectl top nodes
  2 Error from server (Forbidden): nodes.metrics.k8s.io is forbidden: User "aggregator" cannot list resource "nodes" in API group "metrics.k8s.io" at the cluster scope
  3 


2.11 分發systemd

  1 [root@k8smaster01 ~]# cd /opt/k8s/work
  2 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
  3 [root@k8smaster01 work]# for (( i=0; i < 3; i++ ))
  4   do
  5     sed -e "s/##MASTER_NAME##/${MASTER_NAMES[i]}/" -e "s/##MASTER_IP##/${MASTER_IPS[i]}/" kube-apiserver.service.template > kube-apiserver-${MASTER_IPS[i]}.service
  6   done
  7 [root@k8smaster01 work]# ls kube-apiserver*.service			#替換相應的IP
  8 [root@k8smaster01 ~]# cd /opt/k8s/work
  9 [root@k8smaster01 work]# source /opt/k8s/bin/environment.sh
 10 [root@k8smaster01 work]# for master_ip in ${MASTER_IPS[@]}
 11   do
 12     echo ">>> ${master_ip}"
 13     scp kube-apiserver-${master_ip}.service root@${master_ip}:/etc/systemd/system/kube-apiserver.service
 14   done						                #分發systemd


三 啟動並驗證

3.1 啟動kube-apiserver服務

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# for master_ip in ${MASTER_IPS[@]}
  3   do
  4     echo ">>> ${master_ip}"
  5     ssh root@${master_ip} "mkdir -p ${K8S_DIR}/kube-apiserver"
  6     ssh root@${master_ip} "systemctl daemon-reload && systemctl enable kube-apiserver && systemctl restart kube-apiserver"
  7   done


3.2 檢查kube-apiserver服務

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# for master_ip in ${MASTER_IPS[@]}
  3   do
  4     echo ">>> ${master_ip}"
  5     ssh root@${master_ip} "systemctl status kube-apiserver |grep 'Active:'"
  6   done


3.3 查看kube-apiserver寫入etcd的數據

  1 [root@k8smaster01 ~]# source /opt/k8s/bin/environment.sh
  2 [root@k8smaster01 ~]# ETCDCTL_API=3 etcdctl \
  3     --endpoints=${ETCD_ENDPOINTS} \
  4     --cacert=/opt/k8s/work/ca.pem \
  5     --cert=/opt/k8s/work/etcd.pem \
  6     --key=/opt/k8s/work/etcd-key.pem \
  7     get /registry/ --prefix --keys-only


3.4 檢查集群信息

  1 [root@k8smaster01 ~]# kubectl cluster-info
  2 [root@k8smaster01 ~]# kubectl get all --all-namespaces
  3 [root@k8smaster01 ~]# kubectl get componentstatuses
  4 [root@k8smaster01 ~]# sudo netstat -lnpt|grep kube		#檢查 kube-apiserver 監聽的端口





提示:

如果執行 kubectl 命令式時輸出如下錯誤信息,則說明使用的 ~/.kube/config 文件不對,請切換到正確的賬戶后再執行該命令:

The connection to the server localhost:8080 was refused – did you specify the right host or port?

執行 kubectl get componentstatuses 命令時,apiserver 默認向 127.0.0.1 發送請求。當 controller-manager、scheduler 以集群模式運行時,有可能和 kube-apiserver 不在一台機器上,這時 controller-manager 或 scheduler 的狀態為 Unhealthy,但實際上它們工作正常;

6443: 接收 https 請求的安全端口,對所有請求做認證和授權;

由於關閉了非安全端口,故沒有監聽 8080。

3.5 授權


授予 kube-apiserver 訪問 kubelet API 的權限。

在執行 kubectl exec、run、logs 等命令時,apiserver 會將請求轉發到 kubelet 的 https 端口。本實驗定義 RBAC 規則,授權 apiserver 使用的證書(kubernetes.pem)用戶名(CN:kuberntes)訪問 kubelet API 的權限:

  1 [root@k8smaster01 ~]# kubectl create clusterrolebinding kube-apiserver:kubelet-apis --clusterrole=system:kubelet-api-admin --user kubernetes

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

雀巢砸逾620億 推動再生塑料使用

摘錄自2020年01月17日中央通訊社瑞士報導

瑞士食品大廠雀巢集團(Nestle)今天(17日)表示,將在5年內投資20億瑞士法郎(約新台幣621億元),以減少使用原始塑料,轉而採用食品級再生塑料。

法新社報導,雀巢也計劃投資新型永續包裝,以達到該公司的目標,也就是在2025年前全面使用可回收或可再利用的包裝材料。雀巢旗下品牌包含Nespresso咖啡、Vittel礦泉水和Smarties巧克力。

此外,雀巢發表聲明說,他們會在未來5年內,將原始塑料使用量降低1/3,且會撥出2億5000萬瑞士法郎作為創投基金,投資致力於廢棄物回收事業的新創公司。為打造再生塑料市場,雀巢計劃採購重達200萬公噸的食品級再生塑料,並撥出超過15億瑞士法郎,以支付從現在到2025年所需的材料附加費。雀巢執行長施奈德(Mark Schneider)在聲明中說:「最終不應有塑膠被丟入垃圾掩埋場,或成為廢棄物。」

綠色和平組織(Greenpeace)瑞士分部的維特里希(Matthias Wuthrich)表示,雀巢這項聲明「僅部分令人振奮」。他說:「這是朝正確方向所跨出的一步,但要結束當前危機,必須終止無用的塑膠生產作業,並須採用新的供應系統。」

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

升溫也列車禍原因! 研究:美國事故死亡人數隨暖化提高 且以年輕人、男性居多

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

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享