Openshift 4.4 靜態 IP 離線安裝系列:準備離線資源

原文鏈接:https://fuckcloudnative.io/posts/openshift4.4-install-offline-static-1-requirement/

本系列文章描述了離線環境下以 UPI (User Provisioned Infrastructure) 模式安裝 Openshift Container Platform (OCP) 4.4.5 的步驟,我的環境是 VMware ESXI 虛擬化,也適用於其他方式提供的虛擬機或物理主機。離線資源包括安裝鏡像、所有樣例 Image StreamOperatorHub 中的所有 RedHat Operators。

本系列採用靜態 IP 的方式安裝 OCP 集群,如果你可以隨意分配網絡,建議採用 DHCP 的方式。

1. 離線環境

單獨準備一台節點用來執行安裝任務和離線資源準備,這台節點最好具備魔法上網的能力,以便可以同時訪問內外網,我們稱這台節點為基礎節點

除此之外還需要部署一個私有鏡像倉庫,以供 OCP 安裝和運行時使用,要求支持 version 2 schema 2 (manifest list),我這裏選擇的是 Quay 3.3。鏡像倉庫需要部署在另外一台節點,因為需要用到 443 端口,與後面的負載均衡端口衝突。

很多人誤以為必須聯繫 Red Hat 銷售,簽單之後才能使用 OCP4,其實不然,註冊一個開發者賬號后就可以獲得 quay.ioregistry.redhat.io 的拉取密鑰了。

2. 準備離線安裝介質

獲取版本信息

目前最新的 OCP 版本是 4.4.5,可以從這裏下載客戶端:

  • https://mirror.openshift.com/pub/openshift-v4/clients/ocp/latest-4.4/

解壓出來的二進制文件放到基礎節點的 $PATH 下,看下版本信息:

 → oc adm release info quay.io/openshift-release-dev/ocp-release:4.4.5-x86_64

Name:      4.4.5
Digest:    sha256:4a461dc23a9d323c8bd7a8631bed078a9e5eec690ce073f78b645c83fb4cdf74
Created:   2020-05-21T16:03:01Z
OS/Arch:   linux/amd64
Manifests: 412

Pull From: quay.io/openshift-release-dev/ocp-release@sha256:4a461dc23a9d323c8bd7a8631bed078a9e5eec690ce073f78b645c83fb4cdf74

Release Metadata:
  Version:  4.4.5
  Upgrades: 4.3.18, 4.3.19, 4.3.21, 4.3.22, 4.4.2, 4.4.3, 4.4.4
  Metadata:
    description:
  Metadata:
    url: https://access.redhat.com/errata/RHBA-2020:2180

Component Versions:
  kubernetes 1.17.1
  machine-os 44.81.202005180831-0 Red Hat Enterprise Linux CoreOS

Images:
  NAME                                           DIGEST
  aws-machine-controllers                        sha256:7817d9e707bb51bc1e5110ef66bb67947df42dcf3c9b782a8f12f60b8f229dca
  azure-machine-controllers                      sha256:5e2320f92b7308a4f1ec4aca151c752f69265e8c5b705d78e2f2ee70d717711a
  baremetal-installer                            sha256:4c8c6d2895e065711cfcbffe7e8679d9890480a4975cad683b643d8502375fe3
  baremetal-machine-controllers                  sha256:5f1b312ac47b7f9e91950463e9a4ce5af7094a3a8b0bc064c9b4dcfc9c725ad5
  baremetal-operator                             sha256:a77ff02f349d96567da8e06018ad0dfbfb5fef6600a9a216ade15fadc574f4b4
  baremetal-runtimecfg                           sha256:715bc48eda04afc06827189883451958d8940ed8ab6dd491f602611fe98a6fba
  cli                                            sha256:43159f5486cc113d64d5ba04d781c16a084d18745a911a5ae7200bb895778a72
  cli-artifacts                                  sha256:ce7130db82f5a3bb2c806d7080f356e4c68c0405bf3956d3e290bc2078a8bf32
  cloud-credential-operator                      sha256:244ab9d0fcf7315eb5c399bd3fa7c2e662cf23f87f625757b13f415d484621c3
  cluster-authentication-operator                sha256:3145e4fbd62dde385fd0e33d220c42ec3d00ac1dab72288e584cc502b4b8b6db
  cluster-autoscaler                             sha256:66e47de69f685f2dd063fbce9f4e5a00264a5572140d255f2db4c367cb00bad9
  cluster-autoscaler-operator                    sha256:6a32eafdbea3d12c0681a1a1660c7a424f7082a1c42e22d1b301ab0ab6da191b
  cluster-bootstrap                              sha256:fbde2b1a3df7172ce5dbc5e8818bfe631718399eda8058b301a1ef059f549e95
  cluster-config-operator                        sha256:5437794d2309ebe65ca08d1bdeb9fcd665732207b3287df8a7c56e5a2813eccb
  cluster-csi-snapshot-controller-operator       sha256:bc4d8ad97b473316518dbd8906dd900feba383425671eb7d4d73ed1d705c105e
  cluster-dns-operator                           sha256:1a7469258e351d2d56a98a5ef4a3dfa0326b4677fdc1dd11279b6a193ccdbad1
  cluster-etcd-operator                          sha256:9f7a02df3a5d91326d95e444e2e249f8205632ae986d6dccc7f007ec65c8af77
  cluster-image-registry-operator                sha256:0aaa817389487d266faf89cecbfd3197405d87172ee2dcda169dfa90e2e9ca18
  cluster-ingress-operator                       sha256:4887544363e052e656aa1fd44d2844226ee2e4617e08b88ba0211a93bb3101fa
  cluster-kube-apiserver-operator                sha256:718ca346d5499cccb4de98c1f858c9a9a13bbf429624226f466c3ee2c14ebf40
  cluster-kube-controller-manager-operator       sha256:0aa16b4ff32fbb9bc7b32aa1bf6441a19a1deb775fb203f21bb8792ff1a26c2e
  cluster-kube-scheduler-operator                sha256:887eda5ce495f1a33c5adbba8772064d3a8b78192162e4c75bd84763c5a1fb01
  cluster-kube-storage-version-migrator-operator sha256:0fd3e25304a6e23e9699172a84dc134b9b5b81dd89496322a9f46f4cd82ecf71
  cluster-machine-approver                       sha256:c35b382d426ff03cfe07719f19e871ec3bd4189fa27452b3e2eb2fb4ab085afc
  cluster-monitoring-operator                    sha256:d7d5f3b6094c88cb1aa9d5bf1b29c574f13db7142e0a9fba03c6681fe4b592a5
  cluster-network-operator                       sha256:563018341e5b37e5cf370ee0a112aa85dd5e17a658b303714252cc59ddfadea5
  cluster-node-tuned                             sha256:0d1a3f66cd7cfc889ddf17cbdb4cb2e4b9188c341b165de1c9c1df578fb53212
  cluster-node-tuning-operator                   sha256:8e00331fd6b725b1d44687bafa2186920e2864fd4d04869ad4e9f5ba56d663ca
  cluster-openshift-apiserver-operator           sha256:087dd3801b15ca614be0998615a0d827383e9c9ab39e64107324074bddccfff8
  cluster-openshift-controller-manager-operator  sha256:a25afbcb148f3535372784e82c66a6cc2843fe9e7119b9198a39422edb95c2ae
  cluster-policy-controller                      sha256:6294d4af2061d23f52a2a439d20272280aa6e5fcff7a5559b4797fb8e6536790
  cluster-samples-operator                       sha256:7040633af70ceb19147687d948a389d392945cb57236165409e66e5101c0d0c0
  cluster-storage-operator                       sha256:bcfeab624513563c9e26629be2914770436c49318c321bd99028a7d1ffab30cf
  cluster-svcat-apiserver-operator               sha256:21a562f26c967ad6d83e1f4219fad858154c3df9854f1462331b244906c6ca9c
  cluster-svcat-controller-manager-operator      sha256:b635529e5843996a51ace6a2aea4854e46256669ef1773c7371e4f0407dbf843
  cluster-update-keys                            sha256:828e11d8132caf5533e18b8e5d292d56ccf52b08e4fe4c53d7825404b05b2844
  cluster-version-operator                       sha256:7a2a210bc07fead80b3f4276cf14692c39a70640a124326ee919d415f0dc5b2c
  configmap-reloader                             sha256:07d46699cb9810e3f629b5142a571db83106aa1190d5177a9944272080cd053d
  console                                        sha256:69f14151fe8681e5fa48912f8f4df753a0dcc3d616ad7991c463402517d1eab4
  console-operator                               sha256:85c9a48c9b1896f36cf061bd4890e7f85e0dc383148f2a1dc498e668dee961df
  container-networking-plugins                   sha256:1a2ecb28b80800c327ad79fb4c8fb6cc9f0b434fc42a4de5b663b907852ee9fb
  coredns                                        sha256:b25b8b2219e8c247c088af93e833c9ac390bc63459955e131d89b77c485d144d
  csi-snapshot-controller                        sha256:33f89dbd081d119aac8d7c56abcb060906b23d31bc801091b789dea14190493f
  deployer                                       sha256:b24cd515360ae4eba89d4d92afe2689a84043106f7defe34df28acf252cd45b4
  docker-builder                                 sha256:d3cf4e3ad3c3ce4bef52d9543c87a1c555861b726ac9cae0cc57486be1095f8a
  docker-registry                                sha256:8b6ab4a0c14118020fa56b70cab440883045003a8d9304c96691a0401ad7117c
  etcd                                           sha256:aba3c59eb6d088d61b268f83b034230b3396ce67da4f6f6d49201e55efebc6b2
  gcp-machine-controllers                        sha256:1c67b5186bbbdc6f424d611eeff83f11e1985847f4a98f82642dcd0938757b0e
  grafana                                        sha256:aa5c9d3d828b04418d17a4bc3a37043413bdd7c036a75c41cd5f57d8db8aa25a
  haproxy-router                                 sha256:7064737dd9d0a43de7a87a094487ab4d7b9e666675c53cf4806d1c9279bd6c2e
  hyperkube                                      sha256:187b9d29fea1bde9f1785584b4a7bbf9a0b9f93e1323d92d138e61c861b6286c
  insights-operator                              sha256:51dc869dc1a105165543d12eeee8229916fc15387210edc6702dbc944f7cedd7
  installer                                      sha256:a0f23a3292a23257a16189bdae75f7b5413364799e67a480dfad086737e248e0
  installer-artifacts                            sha256:afe926af218d506a7f64ef3df0d949aa6653a311a320bc833398512d1f000645
  ironic                                         sha256:80087bd97c28c69fc08cd291f6115b0e12698abf2e87a3d2bbe0e64f600bae93
  ironic-hardware-inventory-recorder             sha256:2336af8eb4949ec283dc22865637e3fec80a4f6b1d3b78178d58ea05afbd49c2
  ironic-inspector                               sha256:1f48cc344aab15c107e2fb381f9825613f586e116c218cdaf18d1e67b13e2252
  ironic-ipa-downloader                          sha256:a417b910e06ad030b480988d6864367c604027d6476e02e0c3d5dcd6f6ab4ccb
  ironic-machine-os-downloader                   sha256:10b751d8e4ba2975dabc256c7ac4dcf94f4de99be35242505bf8db922e968403
  ironic-static-ip-manager                       sha256:0c122317e3a6407a56a16067d518c18ce08f883883745b2e11a5a39ff695d3d0
  jenkins                                        sha256:d4ab77a119479a95a33beac0d94980a7a0a87cf792f5850b30dff4f1f90a9c4d
  jenkins-agent-maven                            sha256:10559ec206191a9931b1044260007fe8dcedacb8b171be737dfb1ccca9bbf0f5
  jenkins-agent-nodejs                           sha256:ad9e83ea1ea3f338af4dbc9461f8b243bd817df722909293fde33b4f9cbab2bc
  k8s-prometheus-adapter                         sha256:be548d31a65e56234e4b98d6541a14936bc0135875ec61e068578f7014aac31e
  keepalived-ipfailover                          sha256:a882a11b55b2fc41b538b59bf5db8e4cfc47c537890e4906fe6bf22f9da75575
  kube-client-agent                              sha256:8eb481214103d8e0b5fe982ffd682f838b969c8ff7d4f3ed4f83d4a444fb841b
  kube-etcd-signer-server                        sha256:8468b1c575906ed41aa7c3ac3b0a440bf3bc254d2975ecc5e23f84aa54395c81
  kube-proxy                                     sha256:886ae5bd5777773c7ef2fc76f1100cc8f592653ce46f73b816de80a20a113769
  kube-rbac-proxy                                sha256:f6351c3aa750fea93050673f66c5ddaaf9e1db241c7ebe31f555e011b20d8c30
  kube-state-metrics                             sha256:ca47160369e67e1d502e93175f6360645ae02933cceddadedabe53cd874f0f89
  kube-storage-version-migrator                  sha256:319e88c22ea618e7b013166eace41c52eb70c8ad950868205f52385f09e96023
  kuryr-cni                                      sha256:3eecf00fdfca50e90ba2d659bd765eb04b5c446579e121656badcfd41da87663
  kuryr-controller                               sha256:7d70c92699a69a589a3c2e1045a16855ba02af39ce09d6a6df9b1dbabacff4f5
  libvirt-machine-controllers                    sha256:cc3c7778de8d9e8e4ed543655392f942d871317f4b3b7ed31208312b4cc2e61f
  local-storage-static-provisioner               sha256:a7ff3ec289d426c7aaee35a459ef8c862b744d709099dedcd98a4579136f7d47
  machine-api-operator                           sha256:4ca2f1b93ad00364c053592aea0992bbb3cb4b2ea2f7d1d1af286c26659c11d3
  machine-config-operator                        sha256:31dfdca3584982ed5a82d3017322b7d65a491ab25080c427f3f07d9ce93c52e2
  machine-os-content                             sha256:b397960b7cc14c2e2603111b7385c6e8e4b0f683f9873cd9252a789175e5c4e1
  mdns-publisher                                 sha256:dea1fcb456eae4aabdf5d2d5c537a968a2dafc3da52fe20e8d99a176fccaabce
  multus-admission-controller                    sha256:377ed5566c062bd2a677ddc0c962924c81796f8d45346b2eefedf5350d7de6b3
  multus-cni                                     sha256:bc58468a736e75083e0771d88095229bdd6c1e58db8aa33ef60b326e0bfaf271
  multus-route-override-cni                      sha256:e078599fde3b974832c06312973fae7ed93334ea30247b11b9f1861e2b0da7d6
  multus-whereabouts-ipam-cni                    sha256:89c386f5c3940d88d9bc2520f422a2983514f928585a51ae376c43f19e5a6cad
  must-gather                                    sha256:a295d2568410a45f1ab403173ee84d7012bb3ec010c24aa0a17925d08d726e20
  oauth-proxy                                    sha256:619bdb128e410b52451dbf79c9efb089e138127812da19a1f69907117480827f
  oauth-server                                   sha256:58545567c899686cae51d2de4e53a5d49323183a7a3065c0b96ad674686acbe8
  openshift-apiserver                            sha256:8fd79797e6e0e9337fc9689863c3817540a003685a6dfc2a55ecb77059967cef
  openshift-controller-manager                   sha256:4485d6eb7625becf581473690858a01ab83244ecb03bb0319bf849068e98a86a
  openshift-state-metrics                        sha256:6de02ce03089b715e9f767142de33f006809226f037fe21544e1f79755ade920
  openstack-machine-controllers                  sha256:d61e611416196650c81174967e5f11cbdc051d696e38ba341de169375d985709
  operator-lifecycle-manager                     sha256:6e1bca545c35fb7ae4d0f57006acce9a9fabce792c4026944da68d7ddfdec244
  operator-marketplace                           sha256:f0750960873a7cc96f7106e20ea260dd41c09b8a30ce714092d3dcd8a7ec396d
  operator-registry                              sha256:7914f42c9274d263c6ba8623db8e6af4940753dcb4160deb291a9cbc61487414
  ovirt-machine-controllers                      sha256:44f9e65ccd39858bf3d7aa2929f5feac634407e36f912ca88585b445d161506c
  ovn-kubernetes                                 sha256:d80899ed1a6a9f99eb8c64856cd4e576f6534b7390777f3180afb8a634743d62
  pod                                            sha256:d7862a735f492a18cb127742b5c2252281aa8f3bd92189176dd46ae9620ee68a
  prom-label-proxy                               sha256:1cf614e8acbe3bcca3978a07489cd47627f3a3bd132a5c2fe0072d9e3e797210
  prometheus                                     sha256:5eea86e59ffb32fca37cacff22ad00838ea6b947272138f8a56062f68ec40c28
  prometheus-alertmanager                        sha256:bb710e91873ad50ac10c2821b2a28c29e5b89b5da7740a920235ecc33fb063f5
  prometheus-config-reloader                     sha256:7cadb408d7c78440ddacf2770028ee0389b6840651c753f4b24032548f56b7aa
  prometheus-node-exporter                       sha256:7d4e76fea0786f4025e37b5ad0fb30498db5586183fc560554626e91066f60f3
  prometheus-operator                            sha256:6e599a9a8691cce0b40bf1ac5373ddb8009113a2115b5617b2d3a3996174c8f7
  sdn                                            sha256:08c256b7b07c57f195faa33ea4273694dd3504d4a85a10dbf7616b91eaa8e661
  service-ca-operator                            sha256:8c9a3071040f956cce15d1e6da70f6f47dc55b609e4f19fe469ce581cd42bfe5
  service-catalog                                sha256:d9a5fbf60e3bbf1c9811e1707ce9bd04e8263552ba3a6bea8f8c7b604808fdf9
  telemeter                                      sha256:19cfc3e37e12d9dd4e4dd9307781368bbeb07929b6ab788e99aa5543badee3c9
  tests                                          sha256:fc56c9805e2e4a8416c1c5433d7974148f0bad88be4a62feeedcd5d9db4b6ad6
  thanos                                         sha256:a4ea116aec2f972991f5a22f39aa1dbc567dddc3429ddca873601714d003a51c

創建內部鏡像倉庫

內部鏡像倉庫用於存放部署 OCP 集群所需的鏡像,倉庫本身使用 Quay 部署。Quay 包含了幾個核心組件:

  • 數據庫 : 主要存放鏡像倉庫的元數據(非鏡像存儲)
  • Redis : 存放構建日誌和Quay的嚮導
  • Quay : 作為鏡像倉庫
  • Clair : 提供鏡像掃描功能

首先修改鏡像倉庫節點的主機名:

$ hostnamectl set-hostname registry.openshift4.example.com

所有節點主機名都要採用三級域名格式,如 master1.aa.bb.com

接着安裝 podman

$ yum install -y podman

先創建一個 Pod,用來共享 Network Namespace:

 → podman pod create --name quay -p 443:8443

安裝 Mysql 數據庫:

$ mkdir -p /data/quay/lib/mysql
$ chmod 777 /data/quay/lib/mysql
$ export MYSQL_CONTAINER_NAME=quay-mysql
$ export MYSQL_DATABASE=enterpriseregistrydb
$ export MYSQL_PASSWORD=<PASSWD>
$ export MYSQL_USER=quayuser
$ export MYSQL_ROOT_PASSWORD=<PASSWD>
$ podman run \
    --detach \
    --restart=always \
    --env MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} \
    --env MYSQL_USER=${MYSQL_USER} \
    --env MYSQL_PASSWORD=${MYSQL_PASSWORD} \
    --env MYSQL_DATABASE=${MYSQL_DATABASE} \
    --name ${MYSQL_CONTAINER_NAME} \
    --privileged=true \
    --pod quay \
    -v /data/quay/lib/mysql:/var/lib/mysql/data:Z \
    registry.access.redhat.com/rhscl/mysql-57-rhel7

安裝 Redis:

$ mkdir -p /data/quay/lib/redis
$ chmod 777 /data/quay/lib/redis
$ podman run -d --restart=always \
    --pod quay \
    --privileged=true \
    --name quay-redis \
    -v  /data/quay/lib/redis:/var/lib/redis/data:Z \
    registry.access.redhat.com/rhscl/redis-32-rhel7

獲取 Red Hat Quay v3 鏡像的訪問權:

$ podman login -u="redhat+quay" -p="O81WSHRSJR14UAZBK54GQHJS0P1V4CLWAJV1X2C4SD7KO59CQ9N3RE12612XU1HR" quay.io

參考:https://access.redhat.com/solutions/3533201

配置 Quay:

$ podman run --privileged=true \
    --name quay-config \
    --pod quay \
    --add-host mysql:127.0.0.1 \
    --add-host redis:127.0.0.1 \
    --add-host clair:127.0.0.1 \
    -d quay.io/redhat/quay:v3.3.0 config fuckcloudnative.io

這一步會啟動一個配置 Quay 的進程,打開瀏覽器訪問:https://registry.openshift4.example.com,用戶名/密碼為:quayconfig/fuckcloudnative.io

選擇新建配置,然後設置數據庫:

設置超級管理員:

下一個界面要設置兩個地方,一個是 Server configuration 的 Server Hostname,另一個是 Redis Hostname,SSL 不用設置,後面直接通過命令行配置:

配置檢查通過後,就可以保存下載下來:

最後會導出一個 quay-config.tar.gz,將其上傳到 Quay 所在的服務器,解壓到配置文件目錄:

$ mkdir -p /data/quay/config
$ mkdir -p /data/quay/storage
$ cp quay-config.tar.gz /data/quay/config/
$ cd /data/quay/config/
$ tar zxvf quay-config.tar.gz

生成自簽名證書:

# 生成私鑰
$ openssl genrsa -out ssl.key 1024

根據私鑰生成證書申請文件 csr

$ openssl req -new -key ssl.key -out ssl.csr

這裏根據命令行嚮導來進行信息輸入:

Common Name 可以輸入:*.yourdomain.com,這種方式可以生成通配符域名證書。

使用私鑰對證書申請進行簽名從而生成證書:

$ openssl x509 -req -in ssl.csr -out ssl.cert -signkey ssl.key -days 3650

這樣就生成了有效期為 10 年的證書文件,對於自己內網服務使用足夠。

或者你也可以一步到位:

$ openssl req \
  -newkey rsa:2048 -nodes -keyout ssl.key \
  -x509 -days 3650 -out ssl.cert -subj \
  "/C=CN/ST=Shanghai/L=Shanghai/O=IBM/OU=IBM/CN=*.openshift4.example.com"

證書搞定了之後,還需要修改 config.yaml,將協議修改為 https

PREFERRED_URL_SCHEME: https

然後停止 quay-config:

$ podman stop quay-config

最後一步才是部署 Quay:

$ podman run --restart=always \
    --sysctl net.core.somaxconn=4096 \
    --privileged=true \
    --name quay-master \
    --pod quay \
    --add-host mysql:127.0.0.1 \
    --add-host redis:127.0.0.1 \
    --add-host clair:127.0.0.1 \
    -v /data/quay/config:/conf/stack:Z \
    -v /data/quay/storage:/datastorage:Z \
    -d quay.io/redhat/quay:v3.3.0

安裝成功后,將自簽名的證書複製到默認信任證書路徑:

$ cp ssl.cert /etc/pki/ca-trust/source/anchors/ssl.crt
$ update-ca-trust extract

現在可以通過 podman login 命令來測試倉庫的連通性,看到如下字樣即表示安裝成功(也可以通過瀏覽器訪問 Web UI):

 → podman login registry.openshift4.example.com
Username: admin
Password: ********

Login Succeeded

如果使用 Docker 登錄,需要將證書複製到 docker 的信任證書路徑:

$ mkdir -p /etc/docker/certs.d/registry.openshift4.example.com
$ cp ssl.cert /etc/docker/certs.d/registry.openshift4.example.com/ssl.crt
$ systemctl restart docker

下載鏡像文件

準備拉取鏡像權限認證文件。 從 Red Hat OpenShift Cluster Manager 站點的 Pull Secret 頁面下載 registry.redhat.iopull secret

# 把下載的 txt 文件轉出 json 格式,如果沒有 jq 命令,通過 epel 源安裝
$ cat ./pull-secret.txt | jq . > pull-secret.json

$ yum install epel-release
$ yum install jq

JSON 內容如下:

{
  "auths": {
    "cloud.openshift.com": {
      "auth": "b3BlbnNo...",
      "email": "you@example.com"
    },
    "quay.io": {
      "auth": "b3BlbnNo...",
      "email": "you@example.com"
    },
    "registry.connect.redhat.com": {
      "auth": "NTE3Njg5Nj...",
      "email": "you@example.com"
    },
    "registry.redhat.io": {
      "auth": "NTE3Njg5Nj...",
      "email": "you@example.com"
    }
  }
}

把本地倉庫的用戶密碼轉換成 base64 編碼:

$ echo -n 'admin:password' | base64 -w0 
cm9vdDpwYXNzd29yZA==

然後在 pull-secret.json 裏面加一段本地倉庫的權限。第一行倉庫域名和端口,第二行是上面的 base64,第三行隨便填個郵箱:

  "auths": {
...
    "registry.openshift4.example.com": {
      "auth": "cm9vdDpwYXNzd29yZA==",
      "email": "you@example.com"
   },
...

設置環境變量:

$ export OCP_RELEASE="4.4.5-x86_64"
$ export LOCAL_REGISTRY='registry.openshift4.example.com' 
$ export LOCAL_REPOSITORY='ocp4/openshift4'
$ export PRODUCT_REPO='openshift-release-dev'
$ export LOCAL_SECRET_JSON='/root/pull-secret.json'
$ export RELEASE_NAME="ocp-release"
  • OCP_RELEASE : OCP 版本,可以在這個頁面查看。如果版本不對,下面執行 oc adm 時會提示 image does not exist
  • LOCAL_REGISTRY : 本地倉庫的域名和端口。
  • LOCAL_REPOSITORY : 鏡像存儲庫名稱,使用 ocp4/openshift4
  • PRODUCT_REPORELEASE_NAME 都不需要改,這些都是一些版本特徵,保持不變即可。
  • LOCAL_SECRET_JSON : 密鑰路徑,就是上面 pull-secret.json 的存放路徑。

在 Quay 中創建一個組織(Organizationocp4 用來存放同步過來的鏡像。

最後一步就是同步鏡像,這一步的動作就是把 quay 官方倉庫中的鏡像同步到本地倉庫,如果失敗了可以重新執行命令,整體內容大概 5G

$ oc adm -a ${LOCAL_SECRET_JSON} release mirror \
     --from=quay.io/${PRODUCT_REPO}/${RELEASE_NAME}:${OCP_RELEASE} \
     --to=${LOCAL_REGISTRY}/${LOCAL_REPOSITORY} \
     --to-release-image=${LOCAL_REGISTRY}/${LOCAL_REPOSITORY}:${OCP_RELEASE}

oc adm release mirror 命令執行完成後會輸出下面類似的信息,保存下來,將來會用在 install-config.yaml 文件中:

imageContentSources:
- mirrors:
  - registry.openshift4.example.com/ocp4/openshift4
  source: quay.io/openshift-release-dev/ocp-release
- mirrors:
  - registry.openshift4.example.com/ocp4/openshift4
  source: quay.io/openshift-release-dev/ocp-v4.0-art-dev

本地鏡像倉庫緩存好鏡像之後,通過 tag/list 接口查看所有 tag,如果能列出來一堆就說明是正常的:

$ curl -s -X GET -H "Authorization: Bearer <token>" https://registry.openshift4.example.com/api/v1/repository/ocp4/openshift4/tag/|jq .

{
  "has_additional": true,
  "page": 1,
  "tags": [
    {
      "name": "4.4.5-cluster-kube-scheduler-operator",
      "reversion": false,
      "start_ts": 1590821178,
      "image_id": "a778898a93d4fc5413abea38aa604d14d7efbd99ee1ea75d2d1bea3c27a05859",
      "last_modified": "Sat, 30 May 2020 06:46:18 -0000",
      "manifest_digest": "sha256:887eda5ce495f1a33c5adbba8772064d3a8b78192162e4c75bd84763c5a1fb01",
      "docker_image_id": "a778898a93d4fc5413abea38aa604d14d7efbd99ee1ea75d2d1bea3c27a05859",
      "is_manifest_list": false,
      "size": 103582366
    },
    {
      "name": "4.4.5-kube-rbac-proxy",
      "reversion": false,
      "start_ts": 1590821178,
      "image_id": "f1714cda6028bd7998fbba1eb79348f33b9ed9ccb0a69388da2eb0aefc222f85",
      "last_modified": "Sat, 30 May 2020 06:46:18 -0000",
      "manifest_digest": "sha256:f6351c3aa750fea93050673f66c5ddaaf9e1db241c7ebe31f555e011b20d8c30",
      "docker_image_id": "f1714cda6028bd7998fbba1eb79348f33b9ed9ccb0a69388da2eb0aefc222f85",
      "is_manifest_list": false,
      "size": 102366055
    },
    {
      "name": "4.4.5-cluster-kube-controller-manager-operator",
      "reversion": false,
      "start_ts": 1590821178,
      "image_id": "bc7e19d35ec08c1a93058db1705998da2f8bbe5cdbb7f3f5974e6176e2f79eb6",
      "last_modified": "Sat, 30 May 2020 06:46:18 -0000",
      "manifest_digest": "sha256:0aa16b4ff32fbb9bc7b32aa1bf6441a19a1deb775fb203f21bb8792ff1a26c2e",
      "docker_image_id": "bc7e19d35ec08c1a93058db1705998da2f8bbe5cdbb7f3f5974e6176e2f79eb6",
      "is_manifest_list": false,
      "size": 104264263
    },
    {
      "name": "4.4.5-baremetal-operator",
      "reversion": false,
      "start_ts": 1590821178,
      "image_id": "6ec90c0fb53125801d41b37f8f28c4679e49ce19427f7848803a2bc397e4c23b",
      "last_modified": "Sat, 30 May 2020 06:46:18 -0000",
      "manifest_digest": "sha256:a77ff02f349d96567da8e06018ad0dfbfb5fef6600a9a216ade15fadc574f4b4",
      "docker_image_id": "6ec90c0fb53125801d41b37f8f28c4679e49ce19427f7848803a2bc397e4c23b",
      "is_manifest_list": false,
      "size": 110117444
    },
    {
      "name": "4.4.5-cluster-etcd-operator",
      "reversion": false,
      "start_ts": 1590821178,
      "image_id": "d0cf3539496e075954e53fce5ed56445ae87f9f32cfb41e9352a23af4aa04d69",
      "last_modified": "Sat, 30 May 2020 06:46:18 -0000",
      "manifest_digest": "sha256:9f7a02df3a5d91326d95e444e2e249f8205632ae986d6dccc7f007ec65c8af77",
      "docker_image_id": "d0cf3539496e075954e53fce5ed56445ae87f9f32cfb41e9352a23af4aa04d69",
      "is_manifest_list": false,
      "size": 103890103
    },
    {
      "name": "4.4.5-openshift-apiserver",
      "reversion": false,
      "start_ts": 1590821177,
      "image_id": "eba5a051dcbab534228728c7295d31edc0323c7930fa44b40059cf8d22948363",
      "last_modified": "Sat, 30 May 2020 06:46:17 -0000",
      "manifest_digest": "sha256:8fd79797e6e0e9337fc9689863c3817540a003685a6dfc2a55ecb77059967cef",
      "docker_image_id": "eba5a051dcbab534228728c7295d31edc0323c7930fa44b40059cf8d22948363",
      "is_manifest_list": false,
      "size": 109243025
    },
    {
      "name": "4.4.5-kube-client-agent",
      "reversion": false,
      "start_ts": 1590821177,
      "image_id": "fc1fdfb96e9cd250024094b15efa79344c955c7d0c93253df312ffdae02b5524",
      "last_modified": "Sat, 30 May 2020 06:46:17 -0000",
      "manifest_digest": "sha256:8eb481214103d8e0b5fe982ffd682f838b969c8ff7d4f3ed4f83d4a444fb841b",
      "docker_image_id": "fc1fdfb96e9cd250024094b15efa79344c955c7d0c93253df312ffdae02b5524",
      "is_manifest_list": false,
      "size": 99721802
    },
    {
      "name": "4.4.5-kube-proxy",
      "reversion": false,
      "start_ts": 1590821177,
      "image_id": "d2577f4816cb81444ef3b441bf9769904c602cd6626982c2fd8ebba162fd0c08",
      "last_modified": "Sat, 30 May 2020 06:46:17 -0000",
      "manifest_digest": "sha256:886ae5bd5777773c7ef2fc76f1100cc8f592653ce46f73b816de80a20a113769",
      "docker_image_id": "d2577f4816cb81444ef3b441bf9769904c602cd6626982c2fd8ebba162fd0c08",
      "is_manifest_list": false,
      "size": 103473573
    },
    ...
}

這裏需要創建一個 OAuth access token 來訪問 Quay 的 API,創建過程如下:

  1. 瀏覽器登錄 Red Hat Quay,選擇一個組織(Organization),例如 ocp4
  2. 在左側導航中選擇 Applications 圖標。
  3. 選擇 Create New Application,輸入 Application 的名字然後回車。
  4. 選擇你新創建的 Application,在左側導航欄中選擇 Generate Token
  5. 選擇相應的權限,然後點擊 Generate Access Token
  6. 再次確認你設置的權限,然後點擊 Authorize Application
  7. 保管好生成的 token。

Quay 的 API 文檔可以參考這裏:Appendix A: Red Hat Quay Application Programming Interface (API)。

Quay 中也能看到所有的鏡像:

提取 openshift-install 命令

為了保證安裝版本一致性,需要從鏡像庫中提取 openshift-install 二進制文件,不能直接從 https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.4.5 下載,不然後面會有 sha256 匹配不上的問題。

# 這一步需要用到上面的 export 變量
$ oc adm release extract \
  -a ${LOCAL_SECRET_JSON} \
  --command=openshift-install \
  "${LOCAL_REGISTRY}/${LOCAL_REPOSITORY}:${OCP_RELEASE}"

如果提示 error: image dose not exist,說明拉取的鏡像不全,或者版本不對。

把文件移動到 $PATH 並確認版本:

$ chmod +x openshift-install
$ mv openshift-install /usr/local/bin/

$ openshift-install version
openshift-install 4.4.5
built from commit 15eac3785998a5bc250c9f72101a4a9cb767e494
release image registry.openshift4.example.com/ocp4/openshift4@sha256:4a461dc23a9d323c8bd7a8631bed078a9e5eec690ce073f78b645c83fb4cdf74

3. 準備 Image Stream 樣例鏡像

準備一個鏡像列表,然後使用 oc image mirror 將鏡像同步到私有倉庫中:

cat sample-images.txt | while read line; do
  target=$(echo $line | sed 's/registry.redhat.io/registry.openshift4.example.com/')
  oc image mirror -a ${LOCAL_SECRET_JSON} $line $target
done

如果之前裝過 OCP 4.4.5,把 openshift-cluster-samples-operator 項目下 cluster-samples-operator Pod 的 /opt/openshift 目錄同步出來,簡單 grep 一下就都有了完整的鏡像列表。

完整列表參考這裏。

同步過程中如果遇到報錯,可根據報錯信息到 Quay 中創建相應的 Organization,不用中斷任務。這裏給出一個參考,需要創建以下的 Organization:

rhscl
jboss-datavirt-6
3scale-amp21
3scale-amp22
3scale-amp23
3scale-amp24
3scale-amp25
3scale-amp26
jboss-eap-6
devtools
openshift3
rhpam-7
rhdm-7
jboss-amq-6
jboss-datagrid-7
jboss-datagrid-6
jboss-webserver-3
amq-broker-7
jboss-webserver-5
redhat-sso-7
openjdk
redhat-openjdk-18
fuse7
dotnet

4. 準備 OperatorHub 離線資源

首先在 Quay 中創建一個 devinfra 項目,然後構建 RedHat Operators 的 catalog image, 保存為 registry.openshift4.example.com/devinfra/redhat-operators:v1

$ oc adm catalog build \
  -a ${LOCAL_SECRET_JSON} \
  --appregistry-endpoint https://quay.io/cnr \
  --from=registry.redhat.io/openshift4/ose-operator-registry:v4.4 \
  --appregistry-org redhat-operators \
  --to=registry.openshift4.example.com/devinfra/redhat-operators:v1

這個 catalog image 相當於 RedHat Operators 的一個目錄,通過 catalog image 可以找到 RedHat Operators 的所有鏡像。而且 catalog image 使用 sha256 digest 來引用鏡像,能夠確保應用有穩定可重複的部署。

然後使用 catalog image 同步 RedHat Operators 的所有鏡像到私有倉庫:

$ oc adm catalog mirror \
  -a ${LOCAL_SECRET_JSON} \
  registry.openshift4.example.com/devinfra/redhat-operators:v1 \
  registry.openshift4.example.com

如果執行過程中遇到 project not found 之類的錯誤,可根據報錯信息到 Quay 中創建相應的項目,不用中斷任務。

這裏還會遇到一個 bug,執行到最後會有如下的報錯信息:

...
I0409 08:04:48.342110   11331 mirror.go:231] wrote database to /tmp/db-225652515/bundles.db
W0409 08:04:48.347417   11331 mirror.go:258] errors during mirroring. the full contents of the catalog may not have been mirrored: couldn't parse image for mirroring (), skipping mirror: invalid reference format
I0409 08:04:48.385816   11331 mirror.go:329] wrote mirroring manifests to redhat-operators-manifests

先來看看有哪些 Operators:

$ sqlite3 /tmp/db-225652515/bundles.db 'select * from related_image'|grep '^|'

隨便挑一個 Operator,查看其 ClusterServiceVersionspec.relatedImages 字段內容:

$ cat /tmp/cache-943388495/manifests-698804708/3scale-operator/3scale-operator-9re7jpyl/0.5.0/3scale-operator.v0.5.0.clusterserviceversion.yaml

...
spec:
  replaces: 3scale-operator.v0.4.2
  relatedImages:
  - name: apicast-gateway-rhel8
    image: registry.redhat.io/3scale-amp2/apicast-gateway-rhel8@sha256:21be62a6557846337dc0cf764be63442718fab03b95c198a301363886a9e74f9
  - name: backend-rhel7
    image: registry.redhat.io/3scale-amp2/backend-rhel7@sha256:ea8a31345d3c2a56b02998b019db2e17f61eeaa26790a07962d5e3b66032d8e5
  - name: system-rhel7
    image: registry.redhat.io/3scale-amp2/system-rhel7@sha256:93819c324831353bb8f7cb6e9910694b88609c3a20d4c1b9a22d9c2bbfbad16f
  - name: zync-rhel7
    image: registry.redhat.io/3scale-amp2/zync-rhel7@sha256:f4d5c1fdebe306f4e891ddfc4d3045a622d2f01db21ecfc9397cab25c9baa91a
  - name: memcached-rhel7
    image: registry.redhat.io/3scale-amp2/memcached-rhel7@sha256:ff5f3d2d131631d5db8985a5855ff4607e91f0aa86d07dafdcec4f7da13c9e05
  - name: redis-32-rhel7
    value: registry.redhat.io/rhscl/redis-32-rhel7@sha256:a9bdf52384a222635efc0284db47d12fbde8c3d0fcb66517ba8eefad1d4e9dc9
  - name: mysql-57-rhel7
    value: registry.redhat.io/rhscl/mysql-57-rhel7@sha256:9a781abe7581cc141e14a7e404ec34125b3e89c008b14f4e7b41e094fd3049fe
  - name: postgresql-10-rhel7
    value: registry.redhat.io/rhscl/postgresql-10-rhel7@sha256:de3ab628b403dc5eed986a7f392c34687bddafee7bdfccfd65cecf137ade3dfd
...

可以看到 relatedImages 列表中有些條目的鍵是 value 而不是 image,這就是問題所在! 那些沒有 image 的條目在反序列化時會將 image 的值當成空字符串 ""

$ sqlite3 /tmp/db-225652515/bundles.db 'select * from related_image where operatorbundle_name="3scale-operator.v0.5.0"'

registry.redhat.io/3scale-amp2/zync-rhel7@sha256:f4d5c1fdebe306f4e891ddfc4d3045a622d2f01db21ecfc9397cab25c9baa91a|3scale-operator.v0.5.0
registry.redhat.io/3scale-amp2/memcached-rhel7@sha256:ff5f3d2d131631d5db8985a5855ff4607e91f0aa86d07dafdcec4f7da13c9e05|3scale-operator.v0.5.0
|3scale-operator.v0.5.0
registry.redhat.io/3scale-amp2/apicast-gateway-rhel8@sha256:21be62a6557846337dc0cf764be63442718fab03b95c198a301363886a9e74f9|3scale-operator.v0.5.0
registry.redhat.io/3scale-amp2/backend-rhel7@sha256:ea8a31345d3c2a56b02998b019db2e17f61eeaa26790a07962d5e3b66032d8e5|3scale-operator.v0.5.0
registry.redhat.io/3scale-amp2/3scale-rhel7-operator@sha256:2ba16314ee046b3c3814fe4e356b728da6853743bd72f8651e1a338e8bbf4f81|3scale-operator.v0.5.0
registry.redhat.io/3scale-amp2/system-rhel7@sha256:93819c324831353bb8f7cb6e9910694b88609c3a20d4c1b9a22d9c2bbfbad16f|3scale-operator.v0.5.0

從上面的輸出可以看到鍵為 value 的那幾個條目都反序列化失敗了,具體的討論參考:bundle validate should validate that there are no empty relatedImages。

這裏給出一個臨時解決方案,先打開另外一個窗口,然後回到原來的窗口執行命令:

$ oc adm catalog mirror \
  -a ${LOCAL_SECRET_JSON} \
  registry.openshift4.example.com/devinfra/redhat-operators:v1 \
  registry.openshift4.example.com

然後迅速切到下一個窗口,查找最新的 manifest 緩存目錄:

$ ls -l /tmp/cache-*/

根據日期判斷最新的緩存目錄,假設是 /tmp/cache-320634009,然後將所有的 value 替換為 image

$ sed -i "s/value: registry/image: registry/g" $(egrep -rl "value: registry" /tmp/cache-320634009/)

同步完成後會產生 redhat-operators-manifests 目錄,下面有兩個文件:

  • imageContentSourcePolicy.yaml : 定義了一個 ImageContentSourcePolicy 對象,該對象可以配置節點將其對官方 Operator manifests 中鏡像的引用改為對本地鏡像倉庫中鏡像的引用。
  • mapping.txt : 包含了所有的源鏡像在本地鏡像倉庫中的映射位置。oc image mirror 命令可以引用該文件進一步修改鏡像配置。

然而目前這麼做還是有問題 1800674: 同步出來的鏡像 manifest digest 不對,導致後面離線安裝 Operator 時會報鏡像無法獲取的錯誤。

暫時可以使用上面 bugzilla 鏈接里給出的臨時解決方案,先安裝 skopeo:

$ yum install -y golang gpgme-devel libassuan-devel btrfs-progs-devel device-mapper-devel
$ git clone https://github.com/containers/skopeo
$ cd skopeo
$ make binary-local
$ mv skopeo /usr/local/bin/

pull-secret.json 中解碼 quay.ioregistry.redhat.ioregistry.access.redhat.com 的用戶名密碼,然後通過下面的命令認證:

$ skopeo login -u <quay.io_user> -p <quay.io_psw> quay.io
$ skopeo login -u <registry.redhat.io_user> -p <registry.redhat.io_psw> registry.redhat.io
$ skopeo login -u <registry.access.redhat.com_user> -p <registry.access.redhat.com_psw> registry.access.redhat.com

最後同步鏡像的 manifest digest:

cat redhat-operators-manifests/mapping.txt | while read line; do
  origin=$(echo $line | cut -d= -f1)
  target=$(echo $line | cut -d= -f2)
  if [[ "$origin" =~ "sha256" ]]; then
    tag=$(echo $origin | cut -d: -f2 | cut -c -8)
    skopeo copy --all docker://$origin docker://$target:$tag
  else
    skopeo copy --all docker://$origin docker://$target
  fi
done

不得不說,OCP 的安裝真是個浩大的工程,這洋洋洒洒的一大篇也只是準備了離線資源,這隻是安裝的一小步,還有很長的步驟要寫,心理素質不過關的同學切勿隨意模仿。

5. 參考資料

  • 離線部署 Openshift Container Platform 4.3 – 1: 準備離線資源
  • Chapter 9. Using Operator Lifecycle Manager on restricted networks

Kubernetes 1.18.2 1.17.5 1.16.9 1.15.12離線安裝包發布地址http://store.lameleg.com ,歡迎體驗。 使用了最新的sealos v3.3.6版本。 作了主機名解析配置優化,lvscare 掛載/lib/module解決開機啟動ipvs加載問題, 修復lvscare社區netlink與3.10內核不兼容問題,sealos生成百年證書等特性。更多特性 https://github.com/fanux/sealos 。歡迎掃描下方的二維碼加入釘釘群 ,釘釘群已經集成sealos的機器人實時可以看到sealos的動態。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

深度學習在高德ETA應用的探索與實踐

1.導讀

駕車導航是数字地圖的核心用戶場景,用戶在進行導航規劃時,高德地圖會提供給用戶3條路線選擇,由用戶根據自身情況來決定按照哪條路線行駛。

同時各路線的ETA(estimated time of arrival,預估到達時間)會直接显示給用戶,這是用戶關心的核心點之一。用戶給定起點和終點后,我們的任務是預測起終點的ETA,ETA的準確率越高,給用戶帶來的出行體驗越好。

2.基於深度學習模型的探索和實踐

2.1模型選擇

傳統機器學習模型在ETA中,比較常用的有線性回歸、RF(隨機森林)、GBDT(梯度提升決策樹)等回歸預測類模型。線性模型表達能力較差,需要大量特徵工程預先分析出有效的特徵;RF通過樣本隨機和特徵隨機的方式引入更多的隨機性,解決了決策樹泛化能力弱的問題;GBDT是通過採用加法模型(即基函數的線性組合),以及不斷減小訓練過程產生的殘差來達到回歸的算法。

傳統機器學習模型相對簡單易懂,也能達到不錯的效果,但存在兩個問題:

  • 模型的表達能力跟選取的特徵有關,需要人工事先分析出有效的特徵。
  • 沒有考慮上游對下游路段的影響,產生了如丟失上下游關聯信息、下游受上游影響導致的不確定性等問題。

第一個問題很好理解,深度學習模型能很好地彌補這方面。針對第二個問題,以歷史速度信息選取存在的不確定性為例來說明一下,歷史速度信息是一個區分周一到周日七個工作日、10分鐘間隔的歷史平均時間,可以根據該路段的預計進入時間所在10分鐘區間來選定。如下圖(歷史平均速度)從0:00-24:00的變化曲線,可以看到一天中特別是早晚高峰,速度值存在較大波動。

而在選取歷史平均時間時,依賴的是預計進入時間,這個時間依賴於上游路段的預計通行時間,因此其選取存在不確定性,進而導致ETA計算不準確。

考慮到以上問題的存在,我們選擇利用RNN的時間序列思想將路線中上下游路段串聯起來進行路段ETA的預測。

另外考慮到RNN存在的長依賴問題,且結合實際業務情況,我們選擇使用LSTM模型來進行建模,LSTM的門結構具有的選擇性還能讓模型自行學習選擇保留哪些上游的特徵信息進行預測。

2.2網絡架構

上圖為整個模型的框架圖,主要分為兩部分,使用LSTM模塊對路線中的路段ETA的預測和最終使用N層全連接模塊對累計路段ETA及路線各特徵進行完整路線的ETA預測。

2.3路段ETA預測

上圖為各路段ETA預測使用的LSTM結構圖,Xt為路線中第t個路段的特徵信息,主要包含對應的實時路況信息、歷史路況信息、路段的靜態特徵等。

LSTM本是輸入時間序列數據的模型,我們利用該思想,將路線中各路段序列依次輸入模型。

2.4完整路線ETA預測

在LSTM模塊得到累計路線ETA預測值后,結合該路線的靜態屬性,使用全連接模塊將其整合成最終輸出的完整路線ETA預測值。

路線的屬性特徵主要指一些人工提取的特徵,如該路線的長度、導航規劃發起特徵日、是否早晚高峰時段等,用以加強模型在不同場景下的表達能力。

損失函數選用線性回歸常用的平方形式:MSE,公式如下:

其中,N是路線數量,ETA路線j為路線ETA,即預測值;用戶實走j為用戶在該路線的實走時間,即真值。

3.模型效果

衡量模型效果,即路線上ETA的預測值時,主要考慮的是準確率。一般情況下,用戶對ETA偏長和偏短的容忍度不同,對偏長容忍度更高。比如用戶要去機場,ETA給的時間偏短10分鐘比偏長10分鐘對用戶的損害更大。因此準確度的指標設計傾向於ETA偏長,定義為滿足用戶一定容忍範圍的請求比例,即準確率作為主要衡量指標。

在北京市上的實驗結果显示,ETA準確率得到提升,MSE loss下降比例28.2%,效果有了明顯的提升。

4.小結

本文介紹了引入深度學習模型,幫助建模導航規劃的預估到達時間預測,成功解決了線性模型的不足,也為後續引入更多特徵、進行更多探索打開了空間,如歷史速度信息的不確定度、時效性、周期性、突發事件、路網結構等。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

循序漸進VUE+Element 前端應用開發(9)— 界面語言國際化的處理,循序漸進VUE+Element 前端應用開發(5)— 表格列表頁面的查詢,列表展示和字段轉義處理,

我們開發的系統,一般可以不用考慮語言國際化的問題,大多數系統一般是給本國人使用的,而且直接使用中文開發界面會更加迅速 一些,不過框架最好能夠支持國際化的處理,以便在需要的時候,可以花點時間來實現多語言切換的處理,使系統具有更廣泛的受眾用戶。VUE+Element 前端應用實現國際化的處理還是非常方便的,一般在Main.js函數裏面引入語言文件,然後在界面上進行一定的處理,把對應的鍵值轉換為對應語言的語義即可。本篇隨筆介紹在VUE+Element 前端應用中如何實現在界面快速的支持多語言國際化的處理邏輯代碼。

1、main入口函數支持

Element 組件內部默認使用中文,若希望使用其他語言,則需要進行多語言設置。以英文為例,在 main.js 中:

// 完整引入 Element
import Vue from 'vue'
import ElementUI from 'element-ui'
import locale from 'element-ui/lib/locale/lang/en'

Vue.use(ElementUI, { locale })

由於我們現在是需要處理多語言的切換,那麼,我們在src下面的一個目錄裏面創建一個lang目錄,在其中裏面編輯zh.js和en.js分別代表中英文語言對照信息,index.js文件則為引入這兩個文件的處理關係。

  在index.js裏面,需要設置一個函數,用來獲取Cookie裏面存儲的語言,如果沒有找到,以瀏覽器國際化語言為準,如下代碼所示。

export function getLanguage() {
  const chooseLanguage = Cookies.get('language')
  if (chooseLanguage) return chooseLanguage

  // 如果沒有選擇語言,那麼使用瀏覽器語言
  const language = (navigator.language || navigator.browserLanguage).toLowerCase()
  const locales = Object.keys(messages)
  for (const locale of locales) {
    if (language.indexOf(locale) > -1) {
      return locale
    }
  }
  return 'en'
}

其中代碼行

const locales = Object.keys(messages)

是獲取message對象裏面的鍵,如下所示。

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import Cookies from 'js-cookie'
import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui lang
import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui lang
import enLocale from './en'
import zhLocale from './zh'

Vue.use(VueI18n)

// 定義對應語言鍵,展開對應的鍵值對應表
const messages = {
  en: {
    ...enLocale,
    ...elementEnLocale
  },
  zh: {
    ...zhLocale,
    ...elementZhLocale
  }
}

其中message就是一個兩個語言(en/zh)字典下的對照表,包含各自對應鍵值下的內容。

然後整個index.js文件就是公布對應的多語言處理接口和屬性。

const i18n = new VueI18n({
  locale: getLanguage(),
  messages
})

export default i18n

然後在main.js函數裏面處理國際化的處理即可

Vue.use(ElementUI, {
  size: Cookies.get('size') || 'medium', // set element-ui default size
  i18n: (key, value) => i18n.t(key, value) })

new Vue({
  el: '#app',
  router,
  store,
  i18n,
  render: h => h(App)
})

有了這些準備,那麼我們在界面上就可以調用對應的鍵來獲取對應語言的語義了,

2、界面處理實現

首先,我們編輯一下對應國際化的鍵值內容,例如中文參照如下所示。

  例如對應登錄界面上,界面效果如下所示。

  或者

 其中裏面的文本內容,我們都是以國際化處理內容。

如登陸表單裏面的代碼如下所示。

        <el-form ref="loginForm" :model="loginForm" :rules="rules" class="loginForm">
          <el-form-item prop="username" class="login-item">
            <el-input
              v-model="loginForm.username"
              class="area"
              type="text"
              :placeholder="$t('login.username')"
              prefix-icon="el-icon-user-solid"
              @keyup.enter.native="submitForm('loginForm')"
            />
          </el-form-item>
          <el-form-item prop="password" class="login-item">
            <el-input
              v-model="loginForm.password"
              class="area"
              type="password"
              :placeholder="$t('login.password')"
              prefix-icon="el-icon-lock"
              @keyup.enter.native="submitForm('loginForm')"
            />
          </el-form-item>

          <el-form-item>
            <el-button :loading="loading" type="primary" class="submit_btn" @click="submitForm('loginForm')">{{ $t('login.logIn') }}</el-button>
          </el-form-item>
          <div class="tiparea">
            <span style="margin-right:20px;">{{ $t('login.username') }} : admin</span>
            <span> {{ $t('login.password') }} : {{ $t('login.any') }}</span>
          </div>
        </el-form>

我們多處採用了類似 $t(‘login.username’) 的函數處理方式來動態獲取對應語言的內容即可,其中$t()函數裏面就是對應的語義解析的鍵參數,對應我們lang/zh.js裏面或者lang/en.js裏面的內容即可。

其中多語言切換的時候,單擊圖標就可以切換為其他語言內容了。

  切換英文後界面如下所示

 同樣,其他地方,如果需要切換多語言的國際化處理,也可以使用$t的轉義方式,在頂部導航欄裏面,我們可以設置得到多語言支持的界面。

 中文界面提示如下所示。

 這部分的實現代碼是在組件模塊裏面,一樣可以實現國際化的處理的。

  <template v-if="device!=='mobile'">
    <search id="header-search" class="right-menu-item" />
    <error-log class="errLog-container right-menu-item hover-effect" />
    <el-tooltip :content="$t('navbar.fullscreen')" effect="dark" placement="bottom">
      <screenfull id="screenfull" class="right-menu-item hover-effect" />
    </el-tooltip>
    <el-tooltip :content="$t('navbar.size')" effect="dark" placement="bottom">
      <size-select id="size-select" class="right-menu-item hover-effect" />
    </el-tooltip>
    <el-tooltip :content="$t('navbar.language')" effect="dark" placement="bottom">
      <lang-select class="right-menu-item hover-effect" />
    </el-tooltip>
  </template>

 

列出以下前面幾篇隨筆的連接,供參考:

循序漸進VUE+Element 前端應用開發(1)— 開發環境的準備工作

循序漸進VUE+Element 前端應用開發(2)— Vuex中的API、Store和View的使用

循序漸進VUE+Element 前端應用開發(3)— 動態菜單和路由的關聯處理

循序漸進VUE+Element 前端應用開發(4)— 獲取後端數據及產品信息頁面的處理

循序漸進VUE+Element 前端應用開發(5)— 表格列表頁面的查詢,列表展示和字段轉義處理

循序漸進VUE+Element 前端應用開發(6)— 常規Element 界面組件的使用

循序漸進VUE+Element 前端應用開發(7)— 介紹一些常規的JS處理函數

循序漸進VUE+Element 前端應用開發(8)— 樹列表組件的使用

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

ELK的踩坑之旅

前言

起源
許多年前,一個剛結婚的名叫 Shay Banon 的失業開發者,跟着他的妻子去了倫敦,他的妻子在那裡學習廚師。 在尋找一個賺錢的工作的時候,為了給他的妻子做一個食譜搜索引擎,他開始使用 Lucene 的一個早期版本。
直接使用 Lucene 是很難的,因此 Shay 開始做一個抽象層,Java 開發者使用它可以很簡單的給他們的程序添加搜索功能。 他發布了他的第一個開源項目 Compass。
後來 Shay 獲得了一份工作,主要是高性能,分佈式環境下的內存數據網格。這個對於高性能,實時,分佈式搜索引擎的需求尤為突出, 他決定重寫 Compass,把它變為一個獨立的服務並取名 Elasticsearch。
第一個公開版本在2010年2月發布,從此以後,Elasticsearch 已經成為了 Github 上最活躍的項目之一,他擁有超過300名 contributors(目前736名 contributors )。 一家公司已經開始圍繞 Elasticsearch 提供商業服務,並開發新的特性,但是,Elasticsearch 將永遠開源並對所有人可用。
據說,Shay 的妻子還在等着她的食譜搜索引擎…​

設計思路如下

有3台機器

2台做elasticsearch的主副節點

1台做kibana和elasticsearch_head 由於機器匱乏我還在這台機器上部署了logstash和nginx服務(雖然下面的架構中都提到了redis等緩存,但是暫時沒有加該服務,後期會研究添加上的)

先說目的:將nginx的日誌通過logstash收集后發送到ela,然後kibana進行展示

環境如下
elasticsearch master 10.5.2.175:9200
elasticsearch salve 10.5.2.176:9200
logstash 172.17.211.153 啟動命令: nohup /usr/local/logstash/bin/logstash -f /usr/local/logstash/config/agent.conf -w 10 -l /usr/local/logstash/logs/logstash-plain.log &
nginx 
es-head: 172.16.211.143:9100
kibana: 172.16.211.143:5601

架構如下:

加redis/kafa的原因:

在生產環境中,我們的日誌可能會有瞬時高峰,而這個時候如果直接存入es,可能會導致es承受不住,從而影響日誌的收集和查詢。 一般情況下,我們會將日誌存直接放到kafka或者redis這種讀寫性能非常高的應用中,作為一個緩存,然後通過下游組件(例如logstash)進行消費、過濾后存入ES,然後通過可視化界面查看。

ELK的工作流程

  • logstash客戶端收集到日誌后將日誌存入到redis之類的緩存中
  • Logstash_server將數據從redis中提取出來並根據/usr/local/logstash/patterns下的文件(文件名隨意取)這裏叫grok-patterns裏面根據不同的日誌,比如apache、nginx服務規定的不同格式來進行切割,切割完畢后將日誌存入到elastaicsearch中,格式裏面的key vlaue值就是els中的字段和值
  • elastaicsearch對logstash_server發送過來的值進行集群保存,提供被調用接口以及快速的搜索服務(這裏還可以安裝分詞插件,當做搜索引擎)
  • kibana對es根據條件進行搜索並對搜索到的數據進行展示,使我們看起來更加直觀。

一、elasticsearch

Elasticsearch 是一個開源的搜索引擎,建立在一個全文搜索引擎庫 Apache Lucene™ 基礎之上。 Lucene 可以說是當下最先進、高性能、全功能的搜索引擎庫—無論是開源還是私有。
中文文檔https://www.elastic.co/guide/cn/elasticsearch/guide/current/intro.html#intro

elasticsearch的安裝

具體安裝可以參考https://www.cnblogs.com/yanjieli/p/11187430.html這個教程,這裏直說下配置和思路,首先懂思路,安裝看教程就行。

需要有jdk環境
vim /etc/elasticsearch/jvm.options
2g
2g

1.vim /etc/elasticsearch/elasticsearch.yml
http.port: 9200
discovery.zen.ping.unicast.hosts: ["10.5.2.175","10.5.2.176"]
network.host: 10.5.2.175

2.vim /etc/systemd/system.conf
DefaultLimitNOFILE=65536
DefaultLimitNPROC=32000
DefaultLimitMEMLOCK=infinity

3.vim /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft nproc 32000
* hard nproc 32000
* hard memlock unlimited
* soft memlock unlimited

4.vim /etc/sysconfig/elasticsearch
JAVA_HOME=/usr/java/jdk1.8.0_151

5.vim /usr/lib/systemd/system/elasticsearch.service
[Service]
LimitMEMLOCK=infinity

elasticsearch的問題

啟動elasticsearch失敗,報找不到JAVA環境,可明明系統是有的 解決方法如下:

vim /etc/sysconfig/elasticsearch
JAVA_HOME=/usr/java/jdk1.8.0_151

elasticsearch的概念

index 索引 相當於數據庫里的“數據庫” 他是我們存儲和索引關聯數據的地方

type 類數據 將一類的數據放到一起 相當於數據庫中的“表”

id 相當於數據庫表中的一行

  • Elastic 本質上是一個分佈式數據庫,允許多台服務器協同工作,每台服務器可以運行多個 Elastic 實例。單個 Elastic 實例稱為一個節點(node)。一組節點構成一個集群(cluster)。

  • Elastic 會索引所有字段,經過處理后寫入一個反向索引(Inverted Index)。查找數據的時候,直接查找該索引。

    倒排索引(反向索引)

    原始文檔

    創建倒排索引列表

    倒排索引創建索引的流程:

    1) 首先把所有的原始數據進行編號,形成文檔列表

    2) 把文檔數據進行分詞,得到很多的詞條,以詞條為索引。保存包含這些詞條的文檔的編號信息。

    搜索的過程:

    當用戶輸入任意的詞條時,首先對用戶輸入的數據進行分詞,得到用戶要搜索的所有詞條,然後拿着這些詞條去倒排索引列表中進行匹配。找到這些詞條就能找到包含這些詞條的所有文檔的編號。

    然後根據這些編號去文檔列表中找到文檔

  • 所以,Elastic 數據管理的頂層單位就叫做 Index(索引)。它是單個數據庫的同義詞。每個 Index (即數據庫)的名字必須是小寫。

下面的命令可以查看當前節點的所有 Index。

$ curl -X GET 'http://localhost:9200/_cat/indices?v

index 索引 相當於數據庫里的“數據庫” 他是我們存儲和索引關聯數據的地方

type 類數據 將一類的數據放到一起 相當於數據庫中的“表”

id 相當於數據庫表中的一行

pertty 在網頁中格式化輸出響應內容

elasticsearch的操作方法

官方教程在這裏,我覺得這個更加實用官方教程 https://www.cnblogs.com/chuyuan/p/11380744.html

# 增加
http://10.5.103.176:9200/database1/table1 

{
  "name": "doudou",
  "age": 4.5,
  "weight": 20,
}

# 查詢

# 以上方法是正確的
但是再增加一個table2的是否發生如下報錯
http://10.5.103.176:9200/database1/table2
{
  "name": "dianche1",
   "weight": 1000
}

原因是elastic search在6.x版本調整了, 一個index只能存儲一種type。

GET /atguigu/_mapping
1. 檢索文檔
Mysql : select * from user where id = 1
ES : GET /atguigu/doc/1
響應結果
{
  "_index" :   "megacorp",
  "_type" :    "employee",
  "_id" :      "1",
  "_version" : 1,
  "found" :    true,
  "_source" :  {
      "first_name" :  "John",
      "last_name" :   "Smith",
      "age" :         25,
      "about" :       "I love to go rock climbing",
      "interests":  [ "sports", "music" ]
  }
}

2.簡單檢索
Mysql : select * from user
ES : GET /megacorp/employee/_search

3.全文檢索
ES : GET /megacorp/employee/_search?q=haha
查詢出所有文檔字段值為haha的文檔

4.搜索(模糊查詢)
ES : GET /megacorp/employee/_search?q=hello
查詢出所有文檔字段值分詞后包含hello的文檔

5.聚合
PUT atguigu/_mapping/doc
{ 
  "properties": { 
    "interests": { 
      "type": "text", 
      "fielddata": true 
      } 
  }
}

elasticsearch-head的安裝

ealsticsearch只是後端提供各種api,那麼怎麼直觀的使用它呢?elasticsearch-head將是一款專門針對於elasticsearch的客戶端工具,是es的集群管理工具、數據可視化、增刪改查工具。相關詳細的教程在這裏
1 https://www.sojson.com/blog/85.html
2 https://www.cnblogs.com/xuwenjin/p/8792919.html
3 https://blog.csdn.net/huwei2003/article/details/40581143

一、下載head插件
https://github.com/mobz/elasticsearch-head

二、解壓到任意目錄
注意:為避免找不到,一定要和elasticsearch的安裝目錄區分開

三、安裝Node.js
因為head是一個Node.js項目。所以,如果沒有安裝nodejs需要先安裝Node.js
32位安裝包下載地址: https://nodejs.org/dist/v4.4.3/node-v4.4.3-x86.tar.gz
64位安裝包下載地址: https://nodejs.org/dist/v4.4.3/node-v4.4.3-x64.tar.gz
檢測PATH環境變量是否配置了Node.js,打開命令行輸入命令"which npm",輸出如下結果:
/usr/bin/npm
wget https://nodejs.org/dist/latest-v8.x/node-v8.16.0.tar.gz
tar xf node-v8.16.0.tar.gz
cd node-v8.16.0
./configure --prefix=/usr/local/node-v8.16
make -j 8 && make install
添加環境變量
vim /etc/profile
################nodejs###############
export NODE_HOME=/usr/local/node-v8.16
export PATH=$PATH:$NODE_HOME/bin
source /etc/profile
node -v
v8.16.0
npm -v
6.4.1

四、安裝npm 
yum install npm -y

五、es-head安裝:
解壓源碼包:elasticsearch-head.tar.gz
啟動:cd /usr/local/src/elasticsearch-head
npm run start &
訪問地址是http://{你的ip地址}:9200/_plugin/head/
在瀏覽器中輸入:這台機器的ip+端口
http://10.5.2.220:9100/

問題解決:

在elasticsearch中沒有當天的索引

頭一天使用過的bj日誌第二天無法收集到,原因是昨天logstash已經收集過一遍,就被打過了標籤,今天再使用的話,如果這個日誌是不再增加的就不會被收集,因為日誌中沒有新的內容進來,解決方法如下:

在logstash的config文件下的agent.conf加入以下配置
start_position =>"beginning"#檢查時間戳

二、kibana

語言設置

vim config/kibana.yml
i18n.locale: "en" 或者zh-CN中文
systemctl restart kibana重啟即可

安裝配置

重新加載systemctl配置,這個是針對centos7以上使用systemctl kibana restart命令的

systemctl daemon-reload

這裏由於是二進制的安裝方法,所以要設置一個systemctl start kibana.service的啟動方法

1. vim /usr/lib/systemd/system/kibana.service
添加以下內容
[Unit]
Description=Kibana
After=network.target

[Service]
ExecStart=/usr/local/kibana/bin/kibana
Type=simple
PIDFile=/usr/local/kibana/kibana.pid
Restart=always
#User=es 這裏我直接使用root用戶進行啟動
#Group=es
[Install]
WantedBy=default.target

2. 重新加載一下配置文件
systemctl daemon-reload

3. 啟動
systemctl start kibana.service

4. 訪問測試
http://10.5.2.220:5601

查詢語法

參考地址1 參考地址2 參考地址3https://blog.csdn.net/u013958257/article/details/88567581

kibana查詢語法基於Lucene

Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。Lucene的目的是為軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此為基礎建立起完整的全文檢索引擎。Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟件基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋。在Java開發環境里Lucene是一個成熟的免費開源工具。就其本身而言,Lucene是當前以及最近幾年最受歡迎的免費Java信息檢索程序庫。人們經常提到信息檢索程序庫,雖然與搜索引擎有關,但不應該將信息檢索程序庫與搜索引擎相混淆。
Lucene最初是由Doug Cutting開發的,在SourceForge的網站上提供下載。在2001年9月作為高質量的開源Java產品加入到Apache軟件基金會的 Jakarta家族中

kibana在ELK陣營中用來查詢展示數據
elasticsearch構建在Lucene之上,過濾器語法和Lucene相同

1. 根據某個字段查詢
精確匹配: agent:"Mozilla/5.0"
如果不帶雙引號,只要包含指定值就可以搜索到 agent:Mozilla/5.0
如果是數值類型沒有以上區別

2. 數組範圍查詢
[7758794 TO 7758794] 表示等於,原意思是一個區間範圍
指定區間: response:[100 TO 200]
大於等於指定數值的: response:[201 TO *]
小於等於指定數值的: response:[* TO 200]

3. 從指定時間到現在/或者查詢指定時間前數據
2015-05-20T09:20:41.943Z之後的數據: @timestamp:{2015-05-20T09:20:41.943Z TO *}
2015-05-20T09:20:41.943Z之前的數據: @timestamp:{* TO 2015-05-20T09:20:41.943Z }
指定時間範圍: @timestamp:{2015-05-20T09:20:41.943Z TO 015-05-22T09:20:41.943Z}
備註:09:20:41事實上是17:20:41,存在8個小時差

4. 正則匹配
包含指定值: request:/uploads*/
不包含指定值: !request:/uploads*/

5. 邏輯查詢
AND(與關係數據庫一樣) request:/uploads*/ AND response:404
OR(與關係數據庫一樣) request:/uploads*/ OR response:200
組合查詢: (uid OR token) AND version

6. 存在/不存在
存在host字段但不存在url字段: _exists_:host AND _missing_:url
特殊轉義字符
+ – && || ! () {} [] ^” ~ * ? : \

kibana創建索引模式(手動)

https://blog.csdn.net/weixin_34727238/article/details/81540692

當在els中有了當天的索引,就可以到kibana中取創建索引模式了,只是這裏提供了一手動創建的方式,無法自動進行,需要本地定義腳本的方式進行自動索引的創建。

等所有索引都創建完畢后,在下面就能看到了

然後在下面這個裡面就能看到我們的索引裏面的數據情況了,前提是你的logstash成功將切割后的日誌發送到了els中

否則就是以下這種的

kibana創建索引模式(自動)

由於logstash客戶端運行的問題,只要有當天日誌產生,就會將該日誌發送給elasticsearch,然後會在elasticsearch裏面產生一個新的索引

方法如下:

在kibana節點上寫一個腳本,然後設置定時任務執行kibana中索引與elasticsearch的關聯
vim /usr/local/scripts/kibana_create_index.sh
#!/bin/bash
today=`date +%Y-%m-%d`
index_name="nginx-log-${today}.log"
curl -X POST -H "kbn-xsrf:reporting" -H "Content-Type: application/json"  -d  '{"attributes":{"title":"'$log_name'"}}'  'http://172.16.211.143:5601/api/saved_objects/index-pattern'

這裏遇到一個問題,json中調用變量的問題,一直調用不到,後來各種查詢原來是格式不對

json數據里變量要用''括起來
<font color=gray size=72>color=gray</font>

json數據里變量要用”括起來https://www.cnblogs.com/landhu/p/7048255.html

ELK 索引生命周期管理

問題解決

Kibana server is not ready yet出現的原因

第一點:KB、ES版本不一致(網上大部分都是這麼說的)

解決方法:把KB和ES版本調整為統一版本

第二點:kibana.yml中配置有問題(通過查看日誌,發現了Error: No Living connections的問題)

解決方法:將配置文件kibana.yml中的elasticsearch.url改為正確的鏈接,默認為: http://elasticsearch:9200

改為http://自己的IP地址:9200

第三點:瀏覽器沒有緩過來

解決方法:刷新幾次瀏覽器。

終極解決方法:在elasticsearch中刪除kibana的相關索引,只是再打開kibana看不到其他了之前創建的圖形什麼的了

kibana可以做哪些分析

分析的必要性:頂級

  • 用戶分佈
  • PV、UV
  • 狀態碼
  • 訪問時間

更多圖形看這裏https://www.cnblogs.com/hanyifeng/p/5860731.html

比較牛逼一點的教程看這裏https://www.bilibili.com/video/BV1TE411A77i?p=6

三、logstash

logstash和filebeat的對比

Filebeat是收集日誌的另外一種方式,二者區別在於以下

Filebeat用於日誌收集和傳輸,相比Logstash更加輕量級和易部署,對系統資源開銷更小,日誌流架構的話,Filebeat適合部署在收集的最前端,Logstash相比Filebeat功能更強,可以在Filebeat收集之後,由Logstash進一步做日誌的解析,至於kafka也可以考慮添加,然後收集的數據都存放在elasticsearch中。

  1. logstash和filebeat都是可以作為日誌採集的工具,目前日誌採集的工具有很多種,如fluentd, flume, logstash,betas等等。甚至最後我決定用filebeat作為日誌採集端工具的時候,還有人問我為什麼不用flume,logstash等採集工具。
  2. logstash出現時間要比filebeat早許多,隨着時間發展,logstash不僅僅是一個日誌採集工具,它也是可以作為一個日誌搜集工具,有豐富的input|filter|output插件可以使用。常用的ELK日誌採集方案中,大部分的做法就是將所有節點的日誌內容上送到kafka消息隊列,然後使用logstash集群讀取消息隊列內容,根據配置文件進行過濾。上送到elasticsearch。logstash詳細信息可前往https://www.elastic.co/
  3. logstash是使用Java編寫,插件是使用jruby編寫,對機器的資源要求會比較高,網上有一篇關於其性能測試的報告。之前自己也做過和filebeat的測試對比。在採集日誌方面,對CPU,內存上都要比前者高很多。LogStash::Inputs::Syslog 性能測試與優化
  4. filebeat也是elastic.公司開發的,其官方的說法是為了替代logstash-forward。採用go語言開發。代碼開源。elastic/beats filebeat是beats的一個文件採集工具,目前其官方基於libbeats平台開發的還有Packetbeat, Metricbeat, Winlogbeat。filebeat性能非常好,部署簡單。是一個非常理想的文件採集工具。我自己採集工具也是基於beats源碼進行的二次開發。

https://blog.csdn.net/m0_38120325/article/details/79072921

自動創建elasticsearch的索引

在logstash客戶端的配置文件中會有這麼一個配置文件

就是會將日誌發送到當天日期的索引匯總,沒有的話自動創建索引,我們只需要做的就是每日刪除舊的索引。

电子書教程推薦:http://doc.yonyoucloud.com/doc/logstash-best-practice-cn/get_start/introduction.html

啟動方法 基於最基礎的 nohup 方式
nohup /usr/local/logstash/bin/logstash -f /usr/local/logstash/conf/agent.conf &> /dev/null

也可以用daemontools來進行管理
安裝
yum -y install supervisord --enablerepo=epel
在 /etc/supervisord.conf 配置文件里添加內容,定義你要啟動的程序:
[program:elkpro_1]
environment=LS_HEAP_SIZE=5000m
directory=/opt/logstash
command=/opt/logstash/bin/logstash -f /etc/logstash/pro1.conf --pluginpath /opt/logstash/plugins/ -w 10 -l /var/log/logstash/pro1.log
[program:elkpro_2]
environment=LS_HEAP_SIZE=5000m
directory=/opt/logstash
command=/opt/logstash/bin/logstash -f /etc/logstash/pro2.conf --pluginpath /opt/logstash/plugins/ -w 10 -l /var/log/logstash/pro2.log

然後啟動 service supervisord start 即可。
logstash 會以 supervisord 子進程的身份運行,你還可以使用 supervisorctl 命令,單獨控制一系列 logstash 子進程中某一個進程的啟停操作:

supervisorctl stop elkpro_2

關於grok語法

官方給定的語法

https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns

https://github.com/elastic/logstash/tree/v1.4.2/patterns

如果你使用的正則語言可以將nginx日誌進行匹配,就可以成功對日誌進行切割,效果看下圖:

調試的過程中可以使用這個網站來進行正則語法的測試:http://grokdebug.herokuapp.com/

1. 線上配置的信息格式192.168.70.94 跟權威指南中的一樣
SYSLOGBASE %{SYSLOGTIMESTAMP:timestamp} (?:%{SYSLOGFACILITY} )?%{SYSLOGHOST:logsource} %{SYSLOGPROG}:

以下這個是Logstash默認自帶了Apache標準日誌的grok正則表達式:
COMMONAPACHELOG %{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "(?:%{WORD:verb} %{NOTSPACE:request}(?: HTTP/%{NUMBER:httpversion})?|%{DATA:rawrequest
})" %{NUMBER:response} (?:%{NUMBER:bytes}|-)

COMBINEDAPACHELOG %{COMMONAPACHELOG} %{QS:referrer} %{QS:agent}

2. 我的nginx日誌切割格式
NGINX_ACCESS %{IPORHOST:remote_addr} - %{USERNAME:remote_user} \[%{HTTPDATE:tiem_local}\] \"%{DATA:request}\" %{INT:status} %{NUMBER:bytes_sent} \"%{DATA:http_referer}\" \"%{DATA:http_user_agent}\"

MAINNGINXLOG %{COMBINEDAPACHELOG} %{QS:x_forwarded_for}
COMBINEDAPACHELOG 合併的apache日誌  logstash客戶端用的是這種方式
COMMONAPACHELOG 普通的apache日誌
當grok匹配失敗的時候,插件會為這個事件打個tag,默認是_grokparsefailure。LogStash允許你把這些處理失敗的事件路由到其他地方做後續的處理
input { # ... }
filter {
  grok {
    match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{IPV4:ip};%{WORD:environment}\] %{LOGLEVEL:log_level} %{GREEDYDATA:message}" }
  }
}
output {
  if "_grokparsefailure" in [tags] {
    # write events that didn't match to a file
    file { "path" => "/tmp/grok_failures.txt" }
  } else {
     elasticsearch { }
  }
}

看下面紅色的地方,表示grok匹配失敗,才會將tags的標籤定義成_grokparsefailure這個默認的

解決說是要設置錨點 目前不懂什麼意思 先放到這裏

https://www.jianshu.com/p/86133dd66ca4

另外一種說法,暫時不會用,先放着
1.
if "_grokparsefailure" in [tags] { drop { } }
2.match語句跟第一個一樣的  沒啥要點,看着官網例子搞就行了

3.盡量用grok吧 ,grep功能以後要去掉的。

當時想的另外一種解決方法就是改nginx的日誌格式成json形式的,但是我不想用這種方法。

log_format json '{"@timestamp":"$time_iso8601",'
                 '"host":"$server_addr",'
                 '"clientip":"$remote_addr",'
                 '"request":"$request",'
                 '"status":"$status",'
                 '"request_method": "$request_method",'
                 '"size":"$body_bytes_sent",'
                 '"request_time":"$request_time",'
                 '"upstreamtime":"$upstream_response_time",'
                 '"upstreamhost":"$upstream_addr",'
                 '"http_host":"$host",'
                 '"url":"$uri",'
                 '"http_forward":"$http_x_forwarded_for",'
                 '"referer":"$http_referer",'
                 '"agent":"$http_user_agent"}';

access_log  /var/log/nginx/access.log json ;

問題解決

Nginx日誌沒有被成功切割的終極原因

    以下是兩種日誌方式:
    log_format  main  '$remote_addr - $remote_user [$time_iso8601] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$host" "$request_time"';

    log_format format2  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$host" "$request_time"';
 
 在logstash中進行切割的時候調用的時間變量是不同的,靈感來自如下:
 grok {
        match => { "message" => "%{TIMESTAMP_ISO8601:time}" }
    }
date{
        match => ["time", "yyyy-MM-dd HH:mm:ss", "ISO8601"]
        target => "@timestamp"
    }
mutate{
        remove_field => ["time"]
    }
定義:
HTTPDATE %{MONTHDAY}/%{MONTH}/%{YEAR}:%{TIME} %{INT}
TIMESTAMP_ISO8601 %{YEAR}-%{MONTHNUM}-%{MONTHDAY}[T ]%{HOUR}:?%{MINUTE}(?::?%{SECOND})?%{ISO8601_TIMEZONE}?
#NGINX_ACCESS %{IPORHOST:remote_addr} - %{USERNAME:remote_user} \[%{HTTPDATE:time_iso8601}\] \"%{DATA:request}\" %{INT:status} %{NUMBER:bytes_sent} \"%{DATA:http_referer}\" \"%
{DATA:http_user_agent}\"

{DATA:http_user_agent}\"
NGINX_ACCESS %{IPORHOST:remote_addr} - %{USERNAME:remote_user} \[%{TIMESTAMP_ISO8601:time_iso8601}\] \"%{DATA:request}\" %{INT:status} %{NUMBER:bytes_sent} \"%{DATA:http_refere
r}\" \"%{DATA:http_user_agent}\"

SYSLOGBASE %{SYSLOGTIMESTAMP:timestamp} (?:%{SYSLOGFACILITY} )?%{SYSLOGHOST:logsource} %{SYSLOGPROG}:
COMMONAPACHELOG %{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "(?:%{WORD:verb} %{NOTSPACE:request}(?: HTTP/%{NUMBER:httpversion})?|%{DATA:rawrequest
})" %{NUMBER:response} (?:%{NUMBER:bytes}|-)
COMBINEDAPACHELOG %{COMMONAPACHELOG} %{QS:referrer} %{QS:agent}

Nginx中時間格式1:$time_local   對應logstash中\[%{HTTPDATE:timestamp}\]
Nginx中時間格式2:$time_iso8601 對應logstash中\[%{TIMESTAMP_ISO8601:timestamp}\]

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

厲害了!知道這樣重命名文件都是大佬級別!

大家好,我是良許。

在 Linux 下,重命名一個文件,我們通常是使用 mv 命令,一般是這樣操作的:

$ mv file1.txt file2.txt

這樣重命令的方式當然是可以,但有個弊端就是你需要輸入兩次文件名。文件名比較短還好,一旦比較長的話,輸兩次會很讓人崩潰的。

本文就介紹幾種更高效的文件重命令方法,一般人不知道哦,知道的都是大神級別。

高效重命名文件的方法

首先,我們需要使用 vim 編輯 ~/.bashrc 文件。

$ vim ~/.bashrc

將下面這段代碼添加到 ~/.bashrc 文件末尾:

# Bash Function To Rename Files Without Typing Full Name Twice
function mv() {
  if [ "$#" -ne 1 ] || [ ! -e "$1" ]; then
    command mv "$@"
    return
  fi

  read -ei "$1" newfilename
  command mv -v -- "$1" "$newfilename"
}

然後,再按 wq 保存並退出。再接下來,使用 source 命令讓這個修改生效。

$ source ~/.bashrc

接下來,就是見證奇迹的時刻!

現在,我們要重命名 file1.txt 文件,我們只需這樣操作即可:

$ mv file1.txt

然後,重命名的過程可以看下面這個動圖:

由上面這個動圖可以看出來,終端里會显示出來原來文件名稱,然後你可以對這個名稱直接進行更改,而無需再次輸入一遍原來的名稱,效率提高不止一點!

重命名成功后,終端也會這樣提示:

‘file1.txt’ -> ‘file2.txt’

其它高效重命名方法

當然,除了修改 .bashrc 文件之外,還有很多高效重命名方法,這裏再介紹兩個。

方法一:使用 mv 命令

又回到了 mv 命令?打臉啦?

然鵝,命令是 mv ,但用法不一樣!

很多情況下,我們只是修改文件名的某一個部分,比如說,將 file1.txt 修改為 file2.txt ,也就是把 1 改為 2 。在這種情況下,其實有個更高效的改法:

# mv file{1,2}.txt

運行的結果如下:

如果我們想保留原來的文件,再複製一個副本,那就只要將 mv 命令換成 cp 命令即可。

# cp file{1,2}.txt

這個結果應該是顯而易見的,就不截圖結果了。

方法二:使用快捷鍵(ctrl+w,ctrl+y)

既然大部分人是知道重命名文件需要輸入兩次文件名,那麼我就滿足你,依然輸入兩次文件名。但是,這裏介紹一種更高效的重複輸入文件名的方法。

在 Shell 里,我們可以使用 ctrl+w 來剪切最後的單詞,使用 ctrl+y 來粘貼被剪切出來的內容。所以利用這個快捷鍵我們可以更高效重複輸入文件名。

直接上動圖:

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

解惑HOT原理

2020-06-09 19:31:01

一、疑問

  前段時間;QQ群里有人對“這個表(0,4)這行數據我做了update操作,查看索引的page數據,看到索引一直指向(0,4),用ctid='(0,4)’查詢業務表是查不到數據的;然後我做了表的vacuum,reindex甚至drop/create index,還是這樣的”感到疑惑。

  在PostgreSQL8.3實現了(heap only tuple)HOT特性。它存在的目的就是消除表非索引列更新對索引影響。但是它如何工作的呢?

二、解析

  我們來模擬環境

postgres=# create table tbl_hot(id int primary key, info text);
CREATE TABLE
postgres=# insert into tbl_hot select generate_series(1, 4), 'lottu';
INSERT 0 4
postgres=# select ctid ,t.* from tbl_hot t;
 ctid  | id | info  
-------+----+-------
 (0,1) |  1 | lottu
 (0,2) |  2 | lottu
 (0,3) |  3 | lottu
 (0,4) |  4 | lottu
(4 rows)
postgres=# \d tbl_hot
              Table "public.tbl_hot"
 Column |  Type   | Collation | Nullable | Default 
--------+---------+-----------+----------+---------
 id     | integer |           | not null | 
 info   | text    |           |          | 
Indexes:
    "tbl_hot_pkey" PRIMARY KEY, btree (id)

我們創建表tbl_hot;並插入4條記錄。這是我們更新(0,4)這條記錄。如下

postgres=# update tbl_hot set info = 'rax' where id = 4;
UPDATE 1
postgres=# select ctid ,t.* from tbl_hot t;
 ctid  | id | info  
-------+----+-------
 (0,1) |  1 | lottu
 (0,2) |  2 | lottu
 (0,3) |  3 | lottu
 (0,5) |  4 | rax
(4 rows)

更新之後我們看下索引有變化沒?

postgres=# select * from bt_page_items('tbl_hot_pkey', 1);
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00
          2 | (0,2) |      16 | f     | f    | 02 00 00 00 00 00 00 00
          3 | (0,3) |      16 | f     | f    | 03 00 00 00 00 00 00 00
          4 | (0,4) |      16 | f     | f    | 04 00 00 00 00 00 00 00
(4 rows)
bt_page_items函數是用來:返回關於B-樹索引頁面上所有項的詳細信息,在B樹恭弘=叶 恭弘子頁面中,ctid指向一個堆元組。在內部頁面中,ctid的塊編號部分指向索引本身中的另一個頁面。

  我們可以看出索引沒變化。索引存放是表數據的ctid+索引值。使用索引可以快速找到對應記錄的ctid。現在 記錄id=4 索引的ctid(0,4)跟表對應ctid(0,5)不一致。那是不是索引失效了。我們來測試下

postgres=# explain select id from tbl_hot where id = 4;
                                   QUERY PLAN                                    
---------------------------------------------------------------------------------
 Index Only Scan using tbl_hot_pkey on tbl_hot  (cost=0.15..8.17 rows=1 width=4)
   Index Cond: (id = 4)
(2 rows)

  索引沒失效;那如何找到對應的記錄呢?我們先來看下錶存儲的page情況

get_raw_page: 根據參數表明、數據文件類型(main、fsm、vm)以及page位置,將當前表文件中的page內容返回。還有一個函數於此同名,只有兩個參數,是將第二個參數省略,直接使用'main'。
heap_page_items: 參數是函數get_raw_page的返回值,返回值是將page內的項指針(ItemIddata)以及HeapTupleHeaderData的詳細信息。
其中理解下下面字段含義
lp:這是插件自己定義的列,在源碼中其實沒有,這個是項指針的順序。
lp_off:tuple在page中的位置
lp_flags: 含義如下
#define LP_UNUSED       0       /* unused (should always have lp_len=0) */
#define LP_NORMAL       1       /* used (should always have lp_len>0) */
#define LP_REDIRECT     2       /* HOT redirect (should have lp_len=0) */
#define LP_DEAD         3       /* dead, may or may not have storage */
t_ctid: 這個是指物理ID
t_infomask2:表字段的個數以及一些flags;其中flag含義
#define HEAP_NATTS_MASK         0x07FF
             /* 11 bits for number of attributes *//* bits 0x1800 are available */
#define HEAP_KEYS_UPDATED       0x2000 
          /* tuple was updated and key cols* modified, or tuple deleted */
#define HEAP_HOT_UPDATED        0x4000  /* tuple was HOT-updated */
#define HEAP_ONLY_TUPLE         0x8000  /* this is heap-only tuple */
#define HEAP2_XACT_MASK         0xE000  /* visibility-related bits */
postgres=# select * from heap_page_items(get_raw_page('tbl_hot', 0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |         t_data         
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------------
  1 |   8152 |        1 |     34 |    554 |      0 |        0 | (0,1)  |           2 |       2306 |     24 |        |       | \x010000000d6c6f747475
  2 |   8112 |        1 |     34 |    554 |      0 |        0 | (0,2)  |           2 |       2306 |     24 |        |       | \x020000000d6c6f747475
  3 |   8072 |        1 |     34 |    554 |      0 |        0 | (0,3)  |           2 |       2306 |     24 |        |       | \x030000000d6c6f747475
  4 |   8032 |        1 |     34 |    554 |    555 |        0 | (0,5)  |       16386 |       1282 |     24 |        |       | \x040000000d6c6f747475
  5 |   8000 |        1 |     32 |    555 |      0 |        0 | (0,5)  |       32770 |      10498 |     24 |        |       | \x0400000009726178
(5 rows)

我們來理下:我們通過條件id=4;如何找到對應的記錄

  1. 找到指向目標數據tuple的索引tuple(0,4)
  2. 根據獲取索引tuple的位置(0,4);找到行指針lp為4的位置。即對應的ctid為(0,5)
  3. 根據ctid為(0,5);我們可以找到兩條tuple。根據PG的MVCC機制連判斷哪條tuple可見
  4. 可以找到對應tuple

更新多次原理也差不多。

這個時候你會有一個疑問“執行vacuum;清理表tuple(0,4);少了步驟2;那上面的流程就走不通了”。我們來解析下:

postgres=# vacuum tbl_hot;
VACUUM
postgres=# select lp, lp_off, lp_flags, t_ctid, t_infomask2 from heap_page_items(get_raw_page('tbl_hot', 0));
 lp | lp_off | lp_flags | t_ctid | t_infomask2 
----+--------+----------+--------+-------------
  1 |   8152 |        1 | (0,1)  |           2
  2 |   8112 |        1 | (0,2)  |           2
  3 |   8072 |        1 | (0,3)  |           2
  4 |      5 |        2 |        |            
  5 |   8040 |        1 | (0,5)  |       32770
(5 rows)

這時;為了解決這個問題,postgresql會在合適的時候進行行指針的重定向(redirect),這個過程稱為修剪。現在按照這種情況我們來理下:我們通過條件id=4;如何找到對應的記錄

  1. 找到指向目標數據tuple的索引tuple(0,4)
  2. 根據獲取索引tuple的位置(0,4);找到行指針lp為4的位置;這是lp_flags為2表示指針重定向lp為5;即行指針對應的位置是8040
  3. 通過指針可以找到對應tuple。

這是tuple(0,4);既然vacuum;表示可以再使用;但是這是標記是LP_REDIRECT;表明tuple非dead tuple;未進行回收;不可以重複使用。這時你可能會有一個疑問“那什麼時候可以回收?”;答案是這個tuple(0,4)不會標記dead tuple。但是執行vacuum;該page是可以回收空間;這個是PG的MVCC處理機制-vacuum的內容;可以分到下個篇幅再講。這裏我們可以簡單演示下:

postgres=# update tbl_hot set info = 'postgres' where id = 4;
UPDATE 1
postgres=# select lp, lp_off, lp_flags, t_ctid, t_infomask2 from heap_page_items(get_raw_page('tbl_hot', 0));
 lp | lp_off | lp_flags | t_ctid | t_infomask2 
----+--------+----------+--------+-------------
  1 |   8152 |        1 | (0,1)  |           2
  2 |   8112 |        1 | (0,2)  |           2
  3 |   8072 |        1 | (0,3)  |           2
  4 |      5 |        2 |        |            
  5 |   8040 |        1 | (0,6)  |       49154
  6 |   8000 |        1 | (0,6)  |       32770
(6 rows)
postgres=# vacuum tbl_hot;
VACUUM
postgres=# select lp, lp_off, lp_flags, t_ctid, t_infomask2 from heap_page_items(get_raw_page('tbl_hot', 0));
 lp | lp_off | lp_flags | t_ctid | t_infomask2 
----+--------+----------+--------+-------------
  1 |   8152 |        1 | (0,1)  |           2
  2 |   8112 |        1 | (0,2)  |           2
  3 |   8072 |        1 | (0,3)  |           2
  4 |      6 |        2 |        |            
  5 |      0 |        0 |        |            
  6 |   8032 |        1 | (0,6)  |       32770
(6 rows)
postgres=# select ctid,t.* from tbl_hot t;
 ctid  | id |   info   
-------+----+----------
 (0,1) |  1 | lottu
 (0,2) |  2 | lottu
 (0,3) |  3 | lottu
 (0,5) |  5 | lottu
 (0,6) |  4 | postgres
(5 rows)

  最後;當更新的元祖是在其他page;這是索引也會更新;這可以理解是行遷移。這在oracle也是存在這種情況。但是相比oracle更頻繁;當然可以設置降低fillfactor;減少這種情況出現。

三、參考

https://blog.csdn.net/xiaohai928ww/article/details/98603707

https://www.postgresql.org/docs/12/pageinspect.html

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

氦元素 – CUBA 應用程序新樣式主題

    CUBA 框架一直以來定位的目標是業務系統的開發。業務系統的界面通常是給後台員工使用的,看重的是功能實現。多年來,界面外觀和樣式並不是後台管理系統的主要關注點,界面中的控件也更緊湊,唯一的原因就是:在單一屏幕中擺放盡可能多的控件,以提供足夠多的功能。

    但是在最近十年裡,人們使用了越來越多的小尺寸觸控屏設備,遊戲規則因此而改變。要有易於觸控點擊的控件,它們之間需要有足夠的間距,防止被誤點擊。應用程序的配色也變得偏於使用雜色和對比色。所以,近些年的現代應用程序在設計上更注重樣式,其中非功能性需求都佔了很大的一部分。

    順應趨勢,我們很高興推出新可視化主題 – 氦元素(Helium)!與以前的樣式主題相比,氦元素主題更簡潔、明亮,視覺噪音也更少。另外很重要的一點:能夠以最少的工作量為最終用戶和 CUBA 開發人員提供樣式定製。

功能

    首先要提到的是,新主題是動態的,意味着您可以隨時在線更改應用程序樣式!以前,CUBA 使用 SCSS 變量定義顏色,這樣每次修改過後都需要重新編譯。而新主題依賴 CSS 定製屬性 ,即使沒有頁面刷新或重新登錄,這些屬性也可以在運行時立即起效。

預設配色

    開箱即用支持兩種預設的配色方案:淺色(light)和深色(dark)。

    終端用戶可選擇的主題需要通過下列屬性配置(主題安裝流程請參考後續章節):

cuba.theme.modes = light|dark
cuba.theme.defaultMode = light

    另外,也可以通過在線主題編輯器創建自定義的配色方案(點擊下方的在線示例和編輯器章節名稱了解更多細節)。

預設大小

    主題自帶三種內置的控件大小配置:小(small),中(medium)和大(large)。

    跟配色方案類似,也可以通過主題屬性修改:

cuba.theme.sizes = small|medium|large
cuba.theme.defaultSize = medium

設置界面

    設置界面可以通過主菜單的 Help -> Theme Settings 打開。這裏可以讓最終用戶自定義他們喜歡的配色和控件大小。該界面帶有幾乎所有的主要控件,所以用戶可以預覽他調整之後的界面大概什麼樣。

在線示例和編輯器

    關於新主題的另一個重要部分就是其在線交互式編輯器。這裏可以試試調整樣式變量,馬上就能看到調整結果 – 一旦樣式滿足您的需求,只需要點擊下載按鈕並按照提供的說明將其安裝到您的 CUBA 應用程序中即可。

    其實,在線編輯器最好的一點是:不只是開發人員可以使用。將這個鏈接發送給您的追求極致的客戶,這樣他們可以根據自己的偏好自定義配色,然後下載併發送結果給開發人員,只需要幾分鐘便可以應用新的樣式。同時,編輯器本身也允許導入顏色變量,這樣可以基於已有的配色做修改。

    如需使用自定義配色,需要基於氦元素創建主題擴展。

安裝

    該主題是通過 擴展插件 的形式發布。該擴展兼容 7.1.5 以上的 CUBA 應用程序(注意,我們推薦您跟蹤 CUBA 框架的 bugfix 版本並更新您的應用程序至最新版)。可以參考 Studio 的 相關章節 了解如何安裝擴展插件。

    擴展安裝完成后,可以通過下面的方式啟用新的主題:
    啟動應用程序,打開主菜單下的設置界面:Help -> Settings。在可視化主題選項中選擇 Helium 即可。

    如需將氦元素主題設為默認主題,可以在 web-app.properties 中添加:

cuba.web.theme = helium

    如果之前自定義設置過 cuba.themeConfig 屬性,別忘了也需要添加氦元素屬性:

cuba.themeConfig = +/com/haulmont/addon/helium/web/helium-theme.properties

    如需自定義主題屬性,可以在 web 模塊的主包路徑創建 helium-theme.properties,內容如下:

@include=com/haulmont/addon/helium/web/helium-theme.properties

cuba.theme.modes = light|dark
cuba.theme.defaultMode = light

cuba.theme.sizes = small|medium|large
cuba.theme.defaultSize = medium\

    然後在 cuba.themeConfig 屬性中註冊一下該文件:

cuba.themeConfig = +/com/example/project/helium-theme.properties

    如需深度定製該主題,包括應用自定義配色,則需要創建主題擴展。按照該說明操作。

路線圖

    我們計劃繼續開發氦元素主題並添加新功能,包括:

  • 深色模式改進
  • 在主題編輯器中提供更有用的模板
  • 在淺色和深色模式之間自動切換
  • 提供可自定義的 border radius 變量,同樣也基於 CSS 變量。
  • 提供 Figma 的 CUBA 套件。Figma 是 UI/UХ 設計師最流行的工具之一,他們可以用來創建界面模型。

結論

    我們很樂意看到新主題和在線編輯器能用在您的項目中,希望新主題能讓您的應用煥然一新。如果您有任何問題,可以在我們的論壇創建主題討論。項目源碼可以在 Github 找到: 主題 / 編輯器。期待您的反饋!

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

Istio的運維-診斷工具(istio 系列五)

Istio的運維-診斷工具

在參考官方文檔的時候發現環境偶爾會出現問題,因此插入一章與調試有關的內容,便於簡單問題的定位。涵蓋官方文檔的診斷工具章節

目錄

  • Istio的運維-診斷工具
    • 使用istioctl命令行工具
      • istioctl自動補全
      • 查看網格的狀態
      • 獲取代理配置
    • 調試Envoy和istiod
      • 獲取網格概況
      • 檢索Envoy和istiod的差異
      • 深入探究Envoy配置
      • 檢查bootstrap配置
      • 校驗到istiod的連通性
    • 通過istioctl的輸出理解網格
      • 校驗pod是否在網格中
      • 校驗destination rule配置
      • 校驗virtual service配置
      • 校驗流量路由
      • 檢查strict mutual TLS(官方文檔待更新)
      • 卸載
    • 使用istioctl analyse診斷配置
      • 分析現有的集群/本地文件或二者
    • 組件內省
    • 組件日誌
      • 日誌作用域
      • 控制輸出
      • 日誌滾動
      • 組件調試

使用istioctl命令行工具

首先可以通過日誌或Introspection檢查各個組件,如果不足以支持問題定位,可以參考如下操作:

istioctl是一個可以用於調試和診斷istio服務網格的工具。Istio項目為Bash和ZSH運行下的istioctl提供了自動補全功能。

建議安裝對應istio版本的istioctl

istioctl自動補全

  • 將如下內容添加到 ~/.bash_profile 文件中

    [[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"
    
  • 使用bash時,將在tools命令中的istioctl.bash文件拷貝到$HOME目錄下,然後執行如下操作即可

    $ source ~/istioctl.bash
    

查看網格的狀態

可以使用istioctl proxy-statusistioctl ps命令查看網格的狀態。如果輸出結果中缺少某個代理,說明該代理當前沒有連接到Pilot實例,因而無法接收到任何配置。如果狀態為stale,表示當前存在網絡故障,或Pilot需要擴容。

獲取代理配置

可以使用istioctl proxy-configistioctl pc檢索代理配置信息。

例如,使用如下方式可以檢索特定pod中的Envoy實例的集群配置信息。

$ istioctl proxy-config cluster <pod-name> [flags]

使用如下方式可以檢索特定pod中的Envoy實例的bootstrap配置信息。

$ istioctl proxy-config bootstrap <pod-name> [flags]

使用如下方式可以檢索特定pod中的Envoy實例的listener(監聽器)配置信息。

$ istioctl proxy-config listener <pod-name> [flags]

使用如下方式可以檢索特定pod中的Envoy實例的route(路由)配置信息。

$ istioctl proxy-config route <pod-name> [flags]

使用如下方式可以檢索特定pod中的Envoy實例的endpoint (後端)配置信息。

$ istioctl proxy-config endpoints <pod-name> [flags]

更多參見下一節調試Envoy和istiod

調試Envoy和istiod

istio提供了兩個非常有用的命令來診斷流量管理配置問題:proxy-status 和proxy-config。proxy-status可以獲取網格的概述並確定導致問題的代理。proxy-config可以檢查Envoy配置並診斷該問題。

如果要嘗試如下命令,可以:

  • 安裝Bookinfo
  • 使用kubernetes集群中部署類似應用

獲取網格概況

通過proxy-status命令可以查看網格的概況,了解是否有sidecar無法接收配置或無法保持同步。

如果某個代理沒有出現在輸出列表中,則說明該代理沒有連接到istiod實例,因此也無法接收任何配置信息。狀態信息如下:

  • SYNCED:表示Envoy確認了istiod發過來的配置
  • NOT SENT:表示istiod還沒有發送配置到Envoy。通常時因為istiod當前沒有需要發送的配置信息
  • STALE:表示istiod發送了一個更新到Envoy,但沒有接收到確認。通常表示Envoy和istiod之間的網絡出現了問題,或istio本身出現了bug。
$ istioctl ps                                                                               
NAME                                                  CDS      LDS       EDS       RDS        PILOT                     VERSION
details-v1-78d78fbddf-psnmk.default                   SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0
istio-ingressgateway-569669bb67-dsd5h.istio-system    SYNCED   SYNCED    SYNCED    NOT SENT   istiod-788cf6c878-4pq5g   1.6.0
productpage-v1-85b9bf9cd7-d8hm8.default               SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0
prometheus-79878ff5fd-tjdxx.istio-system              SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0
ratings-v1-6c9dbf6b45-xlf2q.default                   SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0
reviews-v1-564b97f875-q5l9r.default                   SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0
reviews-v2-568c7c9d8f-vcd94.default                   SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0
reviews-v3-67b4988599-psllq.default                   SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0
sleep-78484c89dd-fmxbc.default                        SYNCED   SYNCED    SYNCED    SYNCED     istiod-788cf6c878-4pq5g   1.6.0

檢索Envoy和istiod的差異

proxy-status加上proxy ID可以檢索Envoy加載的配置和istiod發送的配置之間的差異,通過這種方式可以確定哪部分內容沒有被同步,並確定可能存在的問題。

下面的例子可以看到ingressgateway的listeners和routers配置都與istiod發過來的配置匹配,但clusters不匹配。

$ istioctl proxy-status details-v1-6dcc6fbb9d-wsjz4.default
--- Istiod Clusters
+++ Envoy Clusters
@@ -374,36 +374,14 @@
             "edsClusterConfig": {
                "edsConfig": {
                   "ads": {

                   }
                },
                "serviceName": "outbound|443||public-cr0bdc785ce3f14722918080a97e1f26be-alb1.kube-system.svc.cluster.local"
-            },
-            "connectTimeout": "1.000s",
-            "circuitBreakers": {
-               "thresholds": [
-                  {
-
-                  }
-               ]
-            }
-         }
-      },
-      {
-         "cluster": {
-            "name": "outbound|53||kube-dns.kube-system.svc.cluster.local",
-            "type": "EDS",
-            "edsClusterConfig": {
-               "edsConfig": {
-                  "ads": {
-
-                  }
-               },
-               "serviceName": "outbound|53||kube-dns.kube-system.svc.cluster.local"
             },
             "connectTimeout": "1.000s",
             "circuitBreakers": {
                "thresholds": [
                   {

                   }

Listeners Match
Routes Match

深入探究Envoy配置

proxy-config命令可以查看一個Envoy實例的配置,用於定位無法通過查看istio配置和用戶資源發現的問題。例如,使用如下命令可以獲取特定pod的clusters,listeners或routes概要。注:首先通過istioctl ps查看出不匹配的代理,然後使用istioctl pc查看具體的不匹配的信息。

$ istioctl proxy-config cluster -n istio-system istio-ingressgateway-7d6874b48f-qxhn5
SERVICE FQDN                                           PORT    SUBSET  DIRECTION  TYPE
BlackHoleCluster                                       -       -       -          STATIC
agent                                                  -       -       -          STATIC
details.default.svc.cluster.local                      9080    -       outbound   EDS
istio-ingressgateway.istio-system.svc.cluster.local    80      -       outbound   EDS
istio-ingressgateway.istio-system.svc.cluster.local    443     -       outbound   EDS
istio-ingressgateway.istio-system.svc.cluster.local    15021   -       outbound   EDS
istio-ingressgateway.istio-system.svc.cluster.local    15443   -       outbound   EDS
istiod.istio-system.svc.cluster.local                  443     -       outbound   EDS
istiod.istio-system.svc.cluster.local                  853     -       outbound   EDS
istiod.istio-system.svc.cluster.local                  15010   -       outbound   EDS
istiod.istio-system.svc.cluster.local                  15012   -       outbound   EDS
istiod.istio-system.svc.cluster.local                  15014   -       outbound   EDS
kube-dns.kube-system.svc.cluster.local                 53      -       outbound   EDS
kube-dns.kube-system.svc.cluster.local                 9153    -       outbound   EDS
kubernetes.default.svc.cluster.local                   443     -       outbound   EDS
...                                                                               
productpage.default.svc.cluster.local                  9080    -       outbound   EDS
prometheus.istio-system.svc.cluster.local              9090    -       outbound   EDS
prometheus_stats                                       -       -       -          STATIC
ratings.default.svc.cluster.local                      9080    -       outbound   EDS
reviews.default.svc.cluster.local                      9080    -       outbound   EDS
sds-grpc                                               -       -       -          STATIC
xds-grpc                                               -       -       -          STRICT_DNS
zipkin                                                 -       -       -          STRICT_DNS

為了調試Envoy,首先需要理解Envoy的clusters/listeners/routes/endpoints,以及它們之間是如何交互的。下面將使用帶有-o json和篩選標誌的proxy config命令來跟蹤Envoy,因為它決定在哪裡將請求從productpage pod發送到reviews pod的 reviews:9080

  1. 如果請求了一個pod的listener概要,可以看到istio生成了如下listeners:

    • 一個0.0.0.0:15006的listener,用於接收到pod的入站流量;以及一個 0.0.0.0:15001的listener,用於接收所有到pod的出站流量,然後將請求交給一個 virtual listener。
    • 每個kubernetes service IP都對應一個virtual listener,非HTTP的listener用於出站的TCP/HTTPS流量
    • pod IP中的virtual listener暴露了接收入站流量的端口
    • 0.0.0.0的HTTP類型的virtual listener,用於出站的HTTP流量

    Istio使用的端口信息如下:

    Port Protocol Used by Description
    15000 TCP Envoy Envoy admin port (commands/diagnostics)
    15001 TCP Envoy √ Envoy Outbound
    15006 TCP Envoy √ Envoy Inbound
    15020 HTTP Envoy Istio agent Prometheus telemetry
    15021 HTTP Envoy Health checks
    15090 HTTP Envoy Envoy Prometheus telemetry
    15010 GRPC Istiod XDS and CA services (plaintext)
    15012 GRPC Istiod XDS and CA services (TLS)
    8080 HTTP Istiod Debug interface
    443 HTTPS Istiod Webhooks
    15014 HTTP Mixer, Istiod Control plane monitoring
    15443 TLS Ingress and Egress Gateways SNI
    9090 HTTP Prometheus Prometheus
    42422 TCP Mixer Telemetry – Prometheus
    15004 HTTP Mixer, Pilot Policy/Telemetry – mTLS
    9091 HTTP Mixer Policy/Telemetry

    可以看到TYPE字段是沒有HTTPS的,HTTPS作為TCP類型。下面是productpage的listeners,刪減了部分信息。10.84開頭的是各個kubernetes service的CLUSTER-IP,以172.20開頭的是kubernetes的node IP,以nodePort方式暴露服務。

    $ istioctl proxy-config listeners productpage-v1-85b9bf9cd7-d8hm8.default
    ADDRESS          PORT     TYPE    
    0.0.0.0          443      TCP     <--+
    10.84.71.37      443      TCP        |
    10.84.223.189    443      TCP        |
    10.84.100.226    15443    TCP        |
    10.84.121.154    443      TCP        |
    10.84.142.44     443      TCP        | #從0.0.0.0_15001相關IP:PORT上接收出站的non-HTTP流量
    10.84.155.219    443      TCP        |
    172.20.127.212   9100     TCP        |
    10.84.205.103    443      TCP        | 
    10.84.167.116    443      TCP        |
    172.20.127.211   9100     TCP     <--+
    10.84.113.197    9979     HTTP+TCP<--+
    0.0.0.0          9091     HTTP+TCP   |
    10.84.30.227     9092     HTTP+TCP   |
    10.84.108.37     8080     HTTP+TCP   |
    10.84.158.64     8443     HTTP+TCP   |
    10.84.202.185    8080     HTTP+TCP   |
    10.84.21.252     8443     HTTP+TCP   |
    10.84.215.56     8443     HTTP+TCP   |
    0.0.0.0          60000    HTTP+TCP   | # 從0.0.0.0_15001的相關端口上接收出站的HTTP+TCP流量
    10.84.126.74     8778     HTTP+TCP   |
    10.84.126.74     8080     HTTP+TCP   |
    10.84.123.207    8080     HTTP+TCP   |
    10.84.30.227     9091     HTTP+TCP   |
    10.84.229.5      8080     HTTP+TCP<--+
    0.0.0.0          9080     HTTP+TCP     # 從 0.0.0.0_15006 上接收所有到9080的入站流量
    0.0.0.0          15001    TCP          # 從IP tables接收pod的所有出站流量,並移交給虛擬偵聽器
    0.0.0.0          15006    HTTP+TCP     # Envoy 入站
    0.0.0.0          15090    HTTP         # Envoy Prometheus 遙測
    0.0.0.0          15021    HTTP         # 健康檢查
    

    下面是productpage Pod中實際監聽的端口信息,與上述對應。

    $ ss -ntpl                                 
    State          Recv-Q        Send-Q        Local Address:Port          Peer Address:Port
    LISTEN         0             128                 0.0.0.0:15090              0.0.0.0:*
    LISTEN         0             128               127.0.0.1:15000              0.0.0.0:*
    LISTEN         0             128                 0.0.0.0:9080               0.0.0.0:*
    LISTEN         0             128                 0.0.0.0:15001              0.0.0.0:*
    LISTEN         0             128                 0.0.0.0:15006              0.0.0.0:*
    LISTEN         0             128                 0.0.0.0:15021              0.0.0.0:*
    LISTEN         0             128                       *:15020                    *:* 
    
  2. 從上述輸出概要中可以看到每個sidecar都有一個綁定到 0.0.0.0:15006的listener,IP tables會將所有入站的Pod流量導入該listener;以及一個綁定到 0.0.0.0:15001的listener,IP tables會將所有出站流量導入該listener,該listener有一個字段useOriginalDst設置為true,表示會使用最佳匹配原始目的地的方式將請求分發到virtual listener,如果沒有找到任何virtual listener,將會直接發送到連接目的地的PassthroughCluster

    $ istioctl pc listener productpage-v1-85b9bf9cd7-d8hm8.default --port 15001 -o json
    [
        {
            "name": "virtualOutbound",
            "address": {
                "socketAddress": {
                    "address": "0.0.0.0",
                    "portValue": 15001
                }
            },
            "filterChains": [
                {
                    "filters": [
                        {
                            "name": "istio.stats",
                            "typedConfig": {
                                "@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
                                "typeUrl": "type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm",
                                "value": {
                                    "config": {
                                        "configuration": "{\n  \"debug\": \"false\",\n  \"stat_prefix\": \"istio\"\n}\n",
                                        "root_id": "stats_outbound",
                                        "vm_config": {
                                            "code": {
                                                "local": {
                                                    "inline_string": "envoy.wasm.stats"
                                                }
                                            },
                                            "runtime": "envoy.wasm.runtime.null",
                                            "vm_id": "tcp_stats_outbound"
                                        }
                                    }
                                }
                            }
                        },
                        {
                            "name": "envoy.tcp_proxy",
                            "typedConfig": {
                                "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
                                "statPrefix": "PassthroughCluster",
                                "cluster": "PassthroughCluster",
                                "accessLog": [
                                    {
                                        "name": "envoy.file_access_log",
                                        "typedConfig": {
                                            "@type": "type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog",
                                            "path": "/dev/stdout",
                                            "format": "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% \"%DYNAMIC_METADATA(istio.mixer:status)%\" \"%UPSTREAM_TRANSPORT_FAILURE_REASON%\" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%\n"
                                        }
                                    }
                                ]
                            }
                        }
                    ],
                    "name": "virtualOutbound-catchall-tcp"
                }
            ],
            "useOriginalDst": true,
            "trafficDirection": "OUTBOUND"
        }
    ]
    
  3. 應用的請求為一個出站HTTP請求,會到達9080端口,這意味着請求將傳遞給0.0.0.0:9080 virtual listener。該listener會查看其RDS中配置的路由。這種情況下會查找istiod配置的RDS的9080路由

    $ istioctl pc listener productpage-v1-85b9bf9cd7-d8hm8.default  -o json --address 0.0.0.0 --port 9080
    [
        {
            "name": "0.0.0.0_9080",
            "address": {
                "socketAddress": {
                    "address": "0.0.0.0",
                    "portValue": 9080
                }
            },
            "filterChains": [
                {
                    "filterChainMatch": {
                        "applicationProtocols": [
                            "http/1.0",
                            "http/1.1",
                            "h2c"
                        ]
                    },
                    "filters": [
                        {
                            "name": "envoy.http_connection_manager",
                            "typedConfig": {
                                "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
                                "statPrefix": "outbound_0.0.0.0_9080",
                                "rds": {
                                    "configSource": {
                                        "ads": {}
                                    },
                                    "routeConfigName": "9080"
                                },
    ...
    ]
    
  4. 9080的路由配置中每個服務只有一個virtual host。由於應用的請求會傳遞到reviews服務,因此Envoy會選擇請求與域相匹配的virtual host。一旦匹配到域,Envoy會查看匹配請求的第一個路由。下面場景中,由於沒有配置任何高級路由,因此只有一條可以匹配的路由,該路由告訴Envoy將請求發送到 outbound|9080||reviews.default.svc.cluster.local集群。

    $ istioctl proxy-config routes productpage-v1-85b9bf9cd7-d8hm8.default  --name 9080 -o json
    [
        {
            "name": "9080",
            "virtualHosts": [
    ...
                {
                    "name": "reviews.default.svc.cluster.local:9080", 
                    "domains": [
                        "reviews.default.svc.cluster.local",
                        "reviews.default.svc.cluster.local:9080",
                        "reviews",
                        "reviews:9080",
                        "reviews.default.svc.cluster",
                        "reviews.default.svc.cluster:9080",
                        "reviews.default.svc",
                        "reviews.default.svc:9080",
                        "reviews.default",
                        "reviews.default:9080",
                        "10.84.110.152",
                        "10.84.110.152:9080"
                    ],
                    "routes": [
                        {
                            "name": "default",
                            "match": {
                                "prefix": "/"
                            },
                            "route": {
                                "cluster": "outbound|9080||reviews.default.svc.cluster.local",
                                "timeout": "0s",
                                "retryPolicy": {
                                    "retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes",
                                    "numRetries": 2,
                                    "retryHostPredicate": [
                                        {
                                            "name": "envoy.retry_host_predicates.previous_hosts"
                                        }
                                    ],
                                    "hostSelectionRetryMaxAttempts": "5",
                                    "retriableStatusCodes": [
                                        503
                                    ]
                                },
                                "maxGrpcTimeout": "0s"
                            },
                            "decorator": {
                                "operation": "reviews.default.svc.cluster.local:9080/*"
                            }
                        }
                    ],
                    "includeRequestAttemptCount": true
                }
            ],
            "validateClusters": false
        }
    ]
    
  5. cluster的配置用於從istiod中檢索後端。Envoy會使用serviceName作為key在Endpoints列表中進行查找,並將請求傳遞到這些後端。

    $  istioctl pc cluster productpage-v1-85b9bf9cd7-d8hm8.default  --fqdn reviews.default.svc.cluster.local -o json
    [
        {
           ...
            "name": "outbound|9080||reviews.default.svc.cluster.local",
            "type": "EDS",
            "edsClusterConfig": {
                "edsConfig": {
                    "ads": {}
                },
                "serviceName": "outbound|9080||reviews.default.svc.cluster.local"
            },
            "connectTimeout": "10s",
            "circuitBreakers": {
                "thresholds": [
                    {
                        "maxConnections": 4294967295,
                        "maxPendingRequests": 4294967295,
                        "maxRequests": 4294967295,
                        "maxRetries": 4294967295
                    }
                ]
            },
            "filters": [
                {
                    "name": "istio.metadata_exchange",
                    "typedConfig": {
                        "@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
                        "typeUrl": "type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange",
                        "value": {
                            "protocol": "istio-peer-exchange"
                        }
                    }
                }
            ]
        }
    ]
    
  6. 使用proxy-config endpoint命令查看本集群中當前可用的後端

    $ istioctl pc endpoint productpage-v1-85b9bf9cd7-d8hm8.default --cluster "outbound|9080||reviews.default.svc.cluster.local"
    ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
    10.80.3.55:9080     HEALTHY     OK                outbound|9080||reviews.default.svc.cluster.local
    10.80.3.56:9080     HEALTHY     OK                outbound|9080||reviews.default.svc.cluster.local
    10.80.3.58:9080     HEALTHY     OK                outbound|9080||reviews.default.svc.cluster.local
    

流量方向為:listener(應用出站port)->route(routeConfigName)->cluster(domain)->endpoint(serviceName)

檢查bootstrap配置

到目前為止已經查看了istiod的大部分配置,然而,Envoy需要一些如哪裡可以發現istiod的bootstrap配置,使用如下方式查看:

$  istioctl proxy-config bootstrap -n istio-system istio-ingressgateway-569669bb67-dsd5h.istio-system
{
    "bootstrap": {
        "node": {
            "id": "router~10.83.0.14~istio-ingressgateway-569669bb67-dsd5h.istio-system~istio-system.svc.cluster.local",
            "cluster": "istio-ingressgateway",
            "metadata": {
                    "CLUSTER_ID": "Kubernetes",
                    "CONFIG_NAMESPACE": "istio-system",
                    "EXCHANGE_KEYS": "NAME,NAMESPACE,INSTANCE_IPS,LABELS,OWNER,PLATFORM_METADATA,WORKLOAD_NAME,MESH_ID,SERVICE_ACCOUNT,CLUSTER_ID",
                    "INSTANCE_IPS": "10.83.0.14,fe80::6871:95ff:fe5b:9e3e",
                    "ISTIO_PROXY_SHA": "istio-proxy:12cfbda324320f99e0e39d7c393109fcd824591f",
                    "ISTIO_VERSION": "1.6.0",
                    "LABELS": {
                                "app": "istio-ingressgateway",
                                "chart": "gateways",
                                "heritage": "Tiller",
                                "istio": "ingressgateway",
                                "pod-template-hash": "569669bb67",
                                "release": "istio",
                                "service.istio.io/canonical-name": "istio-ingressgateway",
                                "service.istio.io/canonical-revision": "latest"
                            },
                    "MESH_ID": "cluster.local",
                    "NAME": "istio-ingressgateway-569669bb67-dsd5h",
                    "NAMESPACE": "istio-system",
                    "OWNER": "kubernetes://apis/apps/v1/namespaces/istio-system/deployments/istio-ingressgateway",
...
                    "ROUTER_MODE": "sni-dnat",
                    "SDS": "true",
                    "SERVICE_ACCOUNT": "istio-ingressgateway-service-account",
                    "TRUSTJWT": "true",
                    "WORKLOAD_NAME": "istio-ingressgateway"
                },
...
}

校驗到istiod的連通性

服務網格中的所有Envoy代理容器都應該連接到istiod,使用如下步驟測試:

  1. 創建一個sleep pod

    $ kubectl create namespace foo
    $ kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml) -n foo
    
  2. 使用curl測試到istiod的連通性。下面調用v1註冊API,使用默認的istiod配置參數,並啟用雙向TLS認證

    $ kubectl exec $(kubectl get pod -l app=sleep -n foo -o jsonpath={.items..metadata.name}) -c sleep -n foo -- curl -sS istiod.istio-system:15014/debug/endpointz
    

通過istioctl的輸出理解網格

如下內容是一個實驗特性,僅用於評估

istio 1.3中包含一個istioctl experimental describe命令。該CLI命令提供了解影響pod的配置所需的信息。本節展示如何使用該experimental 子命令查看一個pod是否在網格中,以及檢查該pod的配置。該命令的基本使用方式為:

$ istioctl experimental describe pod <pod-name>[.<namespace>] #或
$ istioctl experimental describe pod <pod-name> -n <namespace> 

校驗pod是否在網格中

如果一個pod不在網格中,istioctl describe會显示一個告警信息,此外,如果pod缺少istio需要的配置時也會給出告警信息。

$ istioctl experimental describe pod  mutatepodimages-7575797d95-qn7p5
Pod: mutatepodimages-7575797d95-qn7p5
   Pod does not expose ports
WARNING: mutatepodimages-7575797d95-qn7p5 is not part of mesh; no Istio sidecar
--------------------
Error: failed to execute command on sidecar: error 'execing into mutatepodimages-7575797d95-qn7p5/default istio-proxy container: container istio-proxy is not valid for pod mutatepodimages-7575797d95-qn7p5

如果一個pod在網格中,則不會產生告警。

$ istioctl x describe pod ratings-v1-6c9dbf6b45-xlf2q
Pod: ratings-v1-6c9dbf6b45-xlf2q
   Pod Ports: 9080 (details), 15090 (istio-proxy)
--------------------
Service: details
   Port: http 9080/HTTP targets pod port 9080
Pilot reports that pod enforces HTTP/mTLS and clients speak HTTP

輸出為:

  • pod的服務容器端口,上述為ratings的9080端口
  • pod中的istio-proxy端口,15090
  • pod服務使用的協議,9080端口的http協議
  • pod設置的mutual TLS

校驗destination rule配置

可以使用istioctl describe檢查應用到一個pod的destination rule。例如執行如下命令部署destination rule

$ kubectl apply -f samples/bookinfo/networking/destination-rule-all-mtls.yaml

查看ratings pod

$ export RATINGS_POD=$(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}')
$ istioctl x describe pod $RATINGS_POD
Pod: ratings-v1-6c9dbf6b45-xlf2q
   Pod Ports: 9080 (ratings), 15090 (istio-proxy)
--------------------
Service: ratings
   Port: http 9080/HTTP targets pod port 9080
DestinationRule: ratings for "ratings"
   Matching subsets: v1
      (Non-matching subsets v2,v2-mysql,v2-mysql-vm)
   Traffic Policy TLS Mode: ISTIO_MUTUAL
Pilot reports that pod enforces HTTP/mTLS and clients speak mTLS

輸出為:

  • 應用到ratings 服務的ratings destination rule
  • 匹配pod的ratings destination rule,上述為v1
  • destination rule定義的其他subset
  • pod接收HTTP或mutual TLS,但客戶端使用mutual TLS

校驗virtual service配置

部署如下virtual service

$ kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml

查看v1版本的reviews服務:

$ export REVIEWS_V1_POD=$(kubectl get pod -l app=reviews,version=v1 -o jsonpath='{.items[0].metadata.name}')
istioctl x describe pod $REVIEWS_V1_POD
$ istioctl x describe pod $REVIEWS_V1_POD
Pod: reviews-v1-564b97f875-q5l9r
   Pod Ports: 9080 (reviews), 15090 (istio-proxy)
--------------------
Service: reviews
   Port: http 9080/HTTP targets pod port 9080
DestinationRule: reviews for "reviews"
   Matching subsets: v1
      (Non-matching subsets v2,v3)
   Traffic Policy TLS Mode: ISTIO_MUTUAL
VirtualService: reviews
   1 HTTP route(s)

輸出結果與前面的ratings pod類型,但多了到pod的virtual service路由。

istioctl describe命令不僅僅显示了影響pod的virtual service。如果一個virtual service配置的host為一個pod,但流量不可達,會輸出告警信息,這種請求可能發生在當一個virtual service如果沒有可達的pod subset時。例如:

$ export REVIEWS_V2_POD=$(kubectl get pod -l app=reviews,version=v2 -o jsonpath='{.items[0].metadata.name}')
istioctl x describe pod $REVIEWS_V2_POD
[root@bastion istio-1.6.0]# istioctl x describe pod $REVIEWS_V2_POD
Pod: reviews-v2-568c7c9d8f-vcd94
...
VirtualService: reviews
   WARNING: No destinations match pod subsets (checked 1 HTTP routes)
      Route to non-matching subset v1 for (everything)

告警信息給出導致問題的原因,檢查的路由數目,以及其他路由信息。例如,由於virtual service將所有的流量到導入了v1 subset,因此v2 pod無法接收到任何流量。

如果刪除如下destination rule:

$ kubectl delete -f samples/bookinfo/networking/destination-rule-all-mtls.yaml

可以看到如下信息:

$ istioctl x describe pod $REVIEWS_V1_POD
Pod: reviews-v1-564b97f875-q5l9r
   Pod Ports: 9080 (reviews), 15090 (istio-proxy)
--------------------
Service: reviews
   Port: http 9080/HTTP targets pod port 9080
VirtualService: reviews
   WARNING: No destinations match pod subsets (checked 1 HTTP routes)
      Warning: Route to subset v1 but NO DESTINATION RULE defining subsets!

輸出展示了已刪除destination rule,但沒有刪除依賴它的virtual service。該virtual service將流量路由到v1 subset,但沒有定義v1 subset的destination rule。因此流量無法分發到v1版本的pod。

恢復環境:

$ kubectl apply -f samples/bookinfo/networking/destination-rule-all-mtls.yaml

校驗流量路由

istioctl describe也可以展示流量權重。如理如下命令會將90%的流量導入reviews服務的v1 subset,將10%的流量導入reviews服務的v2 subset。

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-90-10.yaml

查看reviews v1` pod:

$ istioctl x describe pod $REVIEWS_V1_POD
...
VirtualService: reviews
   Weight 90%

輸出显示90%的reviews服務的流量導入到了v1 subset中。

部署其他類型的路由,如部署指定HTTP首部的路由:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-jason-v2-v3.yaml

再次查看pod:

$ istioctl x describe pod $REVIEWS_V1_POD
...
VirtualService: reviews
   WARNING: No destinations match pod subsets (checked 2 HTTP routes)
      Route to non-matching subset v2 for (when headers are end-user=jason)
      Route to non-matching subset v3 for (everything)

由於查看了位於v1 subset的pod,而virtual service將包含 end-user=jason 的流量分發給v2 subset,其他流量分發給v3 subset,v1 subset沒有任何流量導入,此時會輸出告警信息。

檢查strict mutual TLS(官方文檔待更新)

根據mutual TLS遷移指南,可以給ratings服務啟用strict mutual TLS。

$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: ratings-strict
spec:
  selector:
    matchLabels:
      app: ratings
  mtls:
    mode: STRICT
EOF

執行如下命令查看ratings pod,輸出显示ratings pod已經使用mutual TLS防護。

$ istioctl x describe pod $RATINGS_POD
Pilot reports that pod enforces mTLS and clients speak mTLS

有時,將mutual TLS切換位STRICT模式時會對部署的組件造成影響,通常是因為destination rule不匹配新配置造成的。例如,如果配置Bookinfo客戶端不使用mutualTLS,而使用明文的HTTP destination rules:

$ kubectl apply -f samples/bookinfo/networking/destination-rule-all.yaml

如果在瀏覽器傻瓜打開Bookinfo,會显示Ratings service is currently unavailable,使用如下命令查看原因:

$ istioctl x describe pod $RATINGS_POD
...
WARNING Pilot predicts TLS Conflict on ratings-v1-f745cf57b-qrxl2 port 9080 (pod enforces mTLS, clients speak HTTP)
  Check DestinationRule ratings/default and AuthenticationPolicy ratings-strict/default

輸出中有一個描述destination rule和authentication policy衝突的告警信息。

使用如下方式恢復:

$ kubectl apply -f samples/bookinfo/networking/destination-rule-all-mtls.yaml

卸載

$ kubectl delete -f samples/bookinfo/platform/kube/bookinfo.yaml
$ kubectl delete -f samples/bookinfo/networking/bookinfo-gateway.yaml
$ kubectl delete -f samples/bookinfo/networking/destination-rule-all-mtls.yaml
$ kubectl delete -f samples/bookinfo/networking/virtual-service-all-v1.yaml

使用istioctl analyse診斷配置

istioctl analyze是一個可以探測istio配置中潛在錯誤的診斷工具,它可以診斷現有的集群或一組本地配置文件,會同時診斷這兩者。

可以使用如下方式診斷當前的kubernetes:

$ istioctl analyze --all-namespaces

例如,如果某些命名空間沒有啟用istiod注入,會打印如下告警信息:

Warn [IST0102] (Namespace openshift) The namespace is not enabled for Istio injection. Run 'kubectl label namespace openshift istio-injection=enabled' to enable it, or 'kubectl label namespace openshift istio-injection=disabled' to explicitly mark it as not needing injection

分析現有的集群/本地文件或二者

上述的例子用於分析一個存在的集群,但該工具也可以支持分析本地kubernetes yaml的配置文件集,或同時分析本地文件和集群。當分析一個本地文件集時,這些文件集應該是完全自包含的。通常用於分析需要部署到集群的一個完整的配置文件集。

分析特定的本地kubernetes yaml文件集:

$ istioctl analyze --use-kube=false a.yaml b.yaml

分析當前目錄中的所有yaml文件:

$ istioctl analyze --use-kube=false *.yaml

模擬將當前目錄中的files部署到當前集群中:

$ istioctl analyze *.yaml

使用istioctl analyze --help命令查看完整的選項。更多analyse的使用參見Q&A.

組件內省

Istio組件是用一個靈活的內省框架構建的,它使檢查和操作運行組件的內部狀態變得簡單。組件會開放一個端口,用於通過web瀏覽器的交互式視圖獲取組件的狀態,或使用外部工具通過REST訪問。

Mixer, PilotGalley 都實現了ControlZ 功能(1.6版本可以查看istiod)。當啟用這些組件時將記錄一條消息,指示要連接的IP地址和端口,以便與ControlZ交互。

2018-07-26T23:28:48.889370Z     info    ControlZ available at 100.76.122.230:9876

可以使用如下命令進行端口轉發,類似kubectl的port-forward,用於遠程訪問。

$ istioctl dashboard controlz <podname> -n <namespaces>

組件日誌

日誌作用域

組件的日誌按照作用域進行分類。取決於組件提供的功能,不同的組件有不同的作用域。所有的組件都有一個default的作用域,用於未分類的日誌消息。

各個組件的日誌作用域參見:reference documentation。

每個作用域對應一個唯一的日誌級別:

  1. none
  2. error
  3. warning
  4. info
  5. debug

其中none表示沒有劃分作用域的輸出,debug會最大化輸出。默認的作用域為info,用於在一般情況下為istio提供何時的日誌輸出。

可以使用 --log_output_level 控制輸出級別:

控制輸出

日誌信息通常會發送到組件的標準輸出流中。 --log_target選項可以將輸出重定向到任意(數量的)位置,可以通過逗號分割的列表給出文件系統的路徑。stdoutstderr分別表示標準輸出和標準錯誤輸出流。

日誌滾動

istio組件能夠自動管理日誌滾動,將大的日誌切分為小的日誌文件。--log_rotate選項允許指定用於滾動的基本文件名。派生的名稱將用於單個日誌文件。

--log_rotate_max_age選項指定文件發生滾動前的最大時間(天為單位),--log_rotate_max_size選項用於指定文件滾動發生前的最大文件大小(MB為單位), --log_rotate_max_backups選項控制保存的滾動文件的最大數量,超過該值的老文件會被自動刪除。

組件調試

--log_caller--log_stacktrace_level選項可以控制日誌信息是否包含程序員級別的信息。在跟蹤組件bug時很有用,但日常用不到。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

.NET 技術棧 思維導圖

    背景介紹

  • 根據網上招聘網站的一些.NET技能需求,畫了一個圖,便於在自修和學習的過程當中有一個方向。

    技能棧

  • Web front-end
    o 框架技術

    ▣ Vue

    ▣ Bootstrap

    ▣ LayUI

    ▣ EasyUI

    ▣ Node.js

    ▣ ReactJS

    ▣ Angular

    ▣ Webpack

    o 開發工具

    ▨ 編碼工具
    ▣ HBuilder X

    ▨ 設計工具
    ▣ PxCook — 像素大廚
    ▣ 藍湖
    ▣ Visio
    ▣ 迅捷流程圖軟件

  • Website
    o 框架技術

    ▨ 雲服務模型
    ▣ Saas — 軟件即服務
    ▣ IaaS — 基礎設施即服務
    ▣ PaaS — 平台即服務

    ▨ 緩存技術
    ▣ Redis
    ▣ MemoryCache
    ▣ Session

    ▨ 消息隊列
    ▣ Windows Message Queue
    ▣ RabbitMQ
    ▣ Kafka

    ▨ 數據操作
    ▤ ORM — 對象關係映射(Object Relational Mapping)
    ▣ Entity framework
    〼 Code First
    〼 Database First
    〼 Model First
    ▣ NHibernate
    ▣ Dapper

    ▨ 框架概念
    ▤ OOD — 面向對象設計(Object-Oriented Design)
    ▤ DDD — 領域驅動設計(Domain-DrivenDesign)
    ▤ AOP — 面向切面編程(Aspect Oriented Programming)
    ▤ IOC — 控制反轉(Inversion of Control)
    ▣ 概念
    〼 依賴注入DI(Dependency Injection)
    〼 依賴查找(Dependency Lookup)
    ▣ 框架
    〼 Autofac
    〼 Spring.NET
    〼 MEF(Managed Extensibility Framework)
    〼 Unity
    〼 PostSharp

  • Windows app
    o 框架技術

    ▣ Winform

    ▣ WPF

    ▤ 通訊技術
    〼 即時通訊
    ▣ SignalR
    ▣ Socket
    〼 通訊協議
    ▣ TCP/IP
    ▣ UDP
    ▣ SSH
    ▣ PCI/PCIE
    ▣ Canbus
    ▣ Modbus
    〼 串口通訊

    ▤ 異步編程
    ▣ 多線程
    ▣ 隊列

    o 控件倉庫

    ▣ DevExpress

    ▣ Metroframework UI

    o 打包加密

    ▤ 打包
    ▣ InnoSetup

    ▤ 混淆/加密
    ▣ .NET Reactor

    ▤ 簽名

  • Web api
    o 框架技術

    ▤ 接口規範
    ▣ Resultful
    ▣ OpenAPI

    ▤ 接口管理
    ▣ Yapi
    ▣ Swagger

  • Windows services
    o WCF
    o Windows services
  • Devops
    o CI — 持續集成(Continuous integration)

    ▤ 代碼倉庫
    ▣ Git
    ▣ Svn

    ▤ 構建工具
    〼 Maven
    〼 Jenkins
    ▣ SonarQube — 自動化測試工具
    〼 Daily build
    〼 Puppet
    ▣ 供應(Provisioning)
    ▣ 配置(Configuration)
    ▣ 聯動(Orchestration)
    ▣ 報告(Reporting)

    ▤ 測試工具
    ▣ Selenium
    ▣ QTP
    ▣ Loadrunner
    ▣ Robot Framework
    ▣ Postman
    ▣ Soapui

    ▤ 配置管理
    ▣ Zookeeper

    ▤ 日誌監控
    〼 ELK
    ▣ Elasticsearch — 實時搜索
    ▣ Logstash — 中央數據流引擎
    ▣ Kibana — 實時分析

    ▤ 文件存儲
    ▣ TFS — 淘寶分佈式文件存儲(Taobao File System)
    ▣ NAS網絡存儲
    ▣ 阿里雲OSS對象存儲

    ▤ 高可用性
    〼 容器技術 — Docker + kubernetes
    〼 讀寫分離
    ▣ 數據庫複製和訂閱
    ▣ 集群服務
    〼 分庫分表
    ▣ 水平拆分
    ▣ 垂直拆分
    〼 邏輯分區

    o CD

    ▤ 持續交付(Continuous Delivery)
    ▣ 預發布環境
    ▣ 灰度環境

    ▤ 持續部署(Continuous Deployment)
    ▣ 自動發布到生產環境

    o 雲

    ▤ 雲平台
    ▣ 阿里雲
    ▣ 華為雲
    ▣ 騰訊雲
    ▣ Microsoft Azure

    ▤ 雲概念
    ▣ 公有雲
    ▣ 私有雲

    o SRE

    ▤ 網站可靠性工程師

    o 開發語言

    ▤ Python — 人工智能、系統運維

    ▤ Go語言 — 服務器編程、分佈式系統、網絡編程、雲平台

    思維導圖

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

香港環團呼籲 支援回收廢棄塑膠

摘錄自2020年6月22日蘋果日報香港報導

香港政府近年力倡回收減廢,惟廢塑膠回收率每況愈下。垃圾徵費遲未通過,有環保團體促加快落實中央回收廢棄塑膠、加強社區回收網,同時加快推出「飲品塑膠瓶生產者責任計劃」,提高回收率。

香港環保廢料再造業總會會長劉耀成,在大埔工業邨設廠回收、再造塑膠瓶,年底投入運作,劉批評政府沒提供經濟誘因,吸引前線人員回收廢棄塑膠。

他指出,香港2018年PET塑膠瓶每日平均棄置量達139噸,若被棄置的塑膠瓶得以回收,可讓至少五間塑膠廠房生存。香港環保署回覆指,政府今年下半年將就塑膠飲料容器推展生產者責任計劃進行公眾諮詢,並會視「塑膠可回收物料回收服務先導計劃」成效,考慮長遠將服務擴展至全港。

公害污染
污染治理
國際新聞
香港
一次性塑膠製品
回收
廢棄物

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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