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 Stream 和 OperatorHub 中的所有 RedHat Operators。

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

1. 離線環境

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

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

很多人誤以為必須聯繫 Red Hat 銷售,簽單之後才能使用 OCP4,其實不然,註冊一個開發者賬號后就可以獲得 quay.io 和 registry.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.io 的 pull 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_REPO 和 RELEASE_NAME 都不需要改,這些都是一些版本特徵,保持不變即可。
  • LOCAL_SECRET_JSON : 密鑰路徑,就是上面 pull-secret.json 的存放路徑。

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

最後一步就是同步鏡像,這一步的動作就是把 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,查看其 ClusterServiceVersion 的 spec.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.io、registry.redhat.io 和 registry.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的動態。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

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

Rust異步之Future

對異步的學習,我們先從Future開始,學習異步的實現原理。等理解了異步是怎麼實現的后,再學習Rust異步編程涉及的2個庫(futures、tokio)的時候就容易理解多了。

Future

rust中Future的定義如下,一個Future可以理解為一段供將來調度執行的代碼。我們為什麼需要異步呢,異步相比同步高效在哪裡呢?就是異步環境下,當前調用就緒時則執行,沒有就緒時則不等待任務就緒,而是返回一個Future,等待將來任務就緒時再調度執行。當然,這裏返回Future時關鍵的是要聲明事件什麼時候就緒,就緒后怎麼喚醒這個任務到調度器去調度執行。

#[must_use = "futures do nothing unless you `.await` or poll them"]
#[lang = "future_trait"]
pub trait Future {  // A future represents an asynchronous computation.
    type Output;
    /* The core method of future, poll, attempts to resolve the future into a final value. This method does not block if the value is not ready. Instead, the current task is scheduled to be woken up when it's possible to make further progress by polling again. */ 
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}

可以看到執行后的返回結果,一個是就緒返回執行結果,另一個是未就緒待定。

#[must_use = "this `Poll` may be a `Pending` variant, which should be handled"]
pub enum Poll<T> {
    Ready(T),
    Pending,
}

可能到這裏你還是雲里霧裡,我們寫一段代碼,幫助你理解。完整代碼見:future_study

use futures;
use std::{future::Future, pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll, Waker}, thread, time::Duration};

fn main() {
    // 我們現在還沒有實現調度器,所以要用一下futues庫里的一個調度器。
    futures::executor::block_on(TimerFuture::new(Duration::new(10, 0)));    
}

struct SharedState {
    completed: bool,
    waker: Option<Waker>,
}

// 我們想要實現一個定時器Future
pub struct TimerFuture {
    share_state: Arc<Mutex<SharedState>>,
}

// impl Future trait for TimerFuture.
impl Future for TimerFuture {
    type Output = ();
    // executor will run this poll ,and Context is to tell future how to wakeup the task.
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut share_state = self.share_state.lock().unwrap();
        if share_state.completed {
            println!("future ready. execute poll to return.");
            Poll::Ready(())
        } else {
            println!("future not ready, tell the future task how to wakeup to executor");
            // 你要告訴future,當事件就緒后怎麼喚醒任務去調度執行,而這個waker根具體的調度器有關
            // 調度器執行的時候會將上下文信息傳進來,裏面最重要的一項就是Waker
            share_state.waker = Some(cx.waker().clone());
            Poll::Pending
        }
    }
}

impl TimerFuture {
    pub fn new(duration: Duration) -> Self {
        let share_state = Arc::new(Mutex::new(SharedState{completed:false, waker:None}));
        let thread_shared_state = share_state.clone();
        thread::spawn(move || {
            thread::sleep(duration);
            let mut share_state = thread_shared_state.lock().unwrap();
            share_state.completed = true;
            if let Some(waker) = share_state.waker.take() {
                println!("detect future is ready, wakeup the future task to executor.");
                waker.wake()    // wakeup the future task to executor.
            }
        });

        TimerFuture {share_state}
    }
}

執行結果如下:

future not ready, tell the future task how to wakeup to executor
detect future is ready, wakeup the future task to executor.
future ready. execute poll to return.

可以看到,剛開始的時候,定時10s事件還未完成,處在Pending狀態,這時要告訴這個任務後面就緒后怎麼喚醒去調度執行。等10s后,定時事件完成了,通過前面的設置的Waker,喚醒這個Future任務去調度執行。這裏,我們看一下Context和Waker是怎麼定義的:

/// The `Context` of an asynchronous task.
///
/// Currently, `Context` only serves to provide access to a `&Waker`
/// which can be used to wake the current task.
#[stable(feature = "futures_api", since = "1.36.0")]
pub struct Context<'a> {
    waker: &'a Waker,
    // Ensure we future-proof against variance changes by forcing
    // the lifetime to be invariant (argument-position lifetimes
    // are contravariant while return-position lifetimes are
    // covariant).
    _marker: PhantomData<fn(&'a ()) -> &'a ()>,
}

// A Waker is a handle for waking up a task by notifying its executor that it is ready to be run.
#[repr(transparent)]
#[stable(feature = "futures_api", since = "1.36.0")]
pub struct Waker {
    waker: RawWaker,
}

現在你應該對Future有新的理解了,上面的代碼,我們並沒有實現調度器,而是使用的futures庫中提供的一個調度器去執行,下面自己實現一個調度器,看一下它的原理。而在Rust中,真正要用的話,還是要學習tokio庫,這裏我們只是為了講述一下實現原理,以便於理解異步是怎麼一回事。完整代碼見:future_study, 關鍵代碼如下:

use std::{future::Future, pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll, Waker}, thread, time::Duration};
use std::sync::mpsc::{sync_channel, SyncSender, Receiver};
use futures::{future::{FutureExt, BoxFuture}, task::{ArcWake, waker_ref}};

use super::timefuture::*;

pub fn run_executor() {
    let (executor, spawner) = new_executor_and_spawner();
    // 將Future封裝成一個任務,分發到調度器去執行
    spawner.spawn( async {
        let v = TimerFuture::new(Duration::new(10, 0)).await;
        println!("return value: {}", v);
        v
    });

    drop(spawner);
    executor.run();
}

fn new_executor_and_spawner() -> (Executor, Spawner) {
    const MAX_QUEUE_TASKS: usize = 10_000;
    let (task_sender, ready_queue) = sync_channel(MAX_QUEUE_TASKS);
    (Executor{ready_queue}, Spawner{task_sender})
}

// executor , received ready task to execute.
struct Executor {
    ready_queue: Receiver<Arc<Task>>,
}

impl Executor {
    // 實際運行具體的Future任務,不斷的接收Future task執行。
    fn run(&self) {
        let mut count = 0;
        while let Ok(task) = self.ready_queue.recv() {
            count = count + 1;
            println!("received task. {}", count);
            let mut future_slot = task.future.lock().unwrap();
            if let Some(mut future) = future_slot.take() {
                let waker = waker_ref(&task);
                let context = &mut Context::from_waker(&*waker);
                if let Poll::Pending = future.as_mut().poll(context) {
                    *future_slot = Some(future);
                    println!("executor run the future task, but is not ready, create a future again.");
                } else {
                    println!("executor run the future task, is ready. the future task is done.");
                }
            }
        }
    }
}

// 負責將一個Future封裝成一個Task,分發到調度器去執行。
#[derive(Clone)]
struct Spawner {
    task_sender: SyncSender<Arc<Task>>,
}

impl Spawner {
    // encapsul a future object to task , wakeup to executor.
    fn spawn(&self, future: impl Future<Output = String> + 'static + Send) {
        let future = future.boxed();
        let task = Arc::new(Task {
            future: Mutex::new(Some(future)),
            task_sender: self.task_sender.clone(),
        });
        println!("first dispatch the future task to executor.");
        self.task_sender.send(task).expect("too many tasks queued.");
    }
}

// 等待調度執行的Future任務,這個任務必須要實現ArcWake,表明怎麼去喚醒任務去調度執行。
struct Task {
    future: Mutex<Option<BoxFuture<'static, String>>>,
    task_sender: SyncSender<Arc<Task>>,
}

impl ArcWake for Task {
    // A way of waking up a specific task.
    fn wake_by_ref(arc_self: &Arc<Self>) {
        let clone = arc_self.clone();
        arc_self.task_sender.send(clone).expect("too many tasks queued");
    }
}

運行結果如下:

first dispatch the future task to executor.     
received task. 1                                
future not ready, tell the future task how to wakeup to executor
executor run the future task, but is not ready, create a future again.
detect future is ready, wakeup the future task to executor.     
received task. 2
future ready. execute poll to return.
return value: timer done.
executor run the future task, is ready. the future task is done.

第一次調度的時候,因為還沒有就緒,在Pending狀態,告訴這個任務,後面就緒是怎麼喚醒該任務。然後當事件就緒的時候,因為前面告訴了如何喚醒,按方法喚醒了該任務去調度執行。其實,在實際應用場景中,難的地方還在於,你怎麼知道什麼時候事件就緒,去喚醒任務,我們很容易聯想到Linux系統的epoll,tokio等底層,也是基於epoll實現的。通過epoll,我們就能方便的知道事件什麼時候就緒了。

參考資料

主要學習資料如下:

  • Asynchronous Programming in Rust
  • Futures Explained in 200 Lines of Rust
  • 200行代碼講透RUST FUTURES

上面的文章主要是學習異步的實現原理,理解異步是怎麼實現的,而進行Rust異步編程時的具體實現,則主要依賴下面2個庫:

  • future —— 主要完成了對異步的抽象
  • tokio —— 異步Future運行時

學習這兩個庫的時候,一定要注意版本問題,這兩個庫最近變化的比較快,一定要學最新的。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

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

※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

※台中搬家公司費用怎麼算?

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維修中心

※超省錢租車方案

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

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

區分http請求狀態碼來理解緩存(協商緩存和強制緩存)

什麼是http緩存呢,當我們使用chrome瀏覽器,按F12打開控制台,在網絡請求中有時候看到狀態碼是200,有時候狀態碼是304,當我們去看這種請求的時候,我們會發現狀態碼為304的狀態結果是:Status Code: 304 Not Modified,而狀態碼為200的時候一般會有四種情況,一種是直接返回200,沒有任何其他的標誌,另一種是Status Code: 200 OK (from memory cache),還有一種是Status Code: 200 (from disk cache)。最後一種不是太常見,Status Code: 200 (from Service Worker).後面這三種狀態碼看到的效果是灰色的,其實從給出的信息也能看出來是從緩存中獲取上數據。下面我們來詳細介紹一下他們都分別是什麼時候出現的。

其實我們可以按狀態碼來區分其為兩大類,分別是協商緩存–304和強制緩存–200

協商緩存(304)

這種方式使用到了headers請求頭裡的兩個字段,Last-Modified & If-Modified-Since 。服務器通過響應頭Last-Modified告知瀏覽器,資源最後被修改的時間:

Last-Modified: Thu, 20 Jun 2019 15:58:05 GMT 

當再次請求該資源時,瀏覽器需要再次向服務器確認,資源是否過期,其中的憑證就是請求頭If-Modified-Since字段,值為上次請求中響應頭Last-Modified字段的值:

If-Modified-Since: Thu, 20 Jun 2019 15:58:05 GMT 

瀏覽器在發送請求的時候服務器會檢查請求頭request header裏面的If-modified-Since,如果最後修改時間相同則返回304,否則給返回頭(response header)添加last-Modified並且返回數據(response body)。

另外,瀏覽器在發送請求的時候服務器會檢查請求頭(request header)裏面的if-none-match的值與當前文件的內容通過hash算法(例如 nodejs: cryto.createHash(‘sha1’))生成的內容摘要字符對比,相同則直接返回304,否則給返回頭(response header)添加etag屬性為當前的內容摘要字符,並且返回內容。

綜上總結為:

  • 請求頭last-modified的日期與響應頭的last-modified一致
  • 請求頭if-none-match的hash與響應頭的etag一致
    這兩種情況會返回Status Code: 304

強制緩存(200(from …))

這裏我們使用到了Cache-Control和Expires這兩個字段來進行控制,Cache-Control裏面可以有多個屬性,可以組合設置,其屬性有如下幾種:

  • max-age=xxx,最大的有效時間 單位秒,是一個相對時間
  • must-revalidate,如果超過了max-age的時間,必須向服務器發送請求,驗證資源的有效性
  • no-cache,基本等價於max-age=0,由協商緩存來決定是否緩存資源
  • no-store,真正意義上的不緩存
  • public,代表 http 請求返回的內容所經過的任何路徑當中(包括中間一些http代理服務器以及發出請求的客戶端瀏覽器),都可以對返回內容進行緩存操作
  • private,代表只有發起請求的瀏覽器才可以進行緩存。默認值

比如我們設置

Catch-Control:public,max-age=360000 

我們在之前說了強制緩存有三種情況,上面說返回200有四種,第一種其實是不緩存,正常服務器返迴響應。

Service Worker

這個東西其實本質上時服務器和客戶端之間的代理服務器,一般我們在使用react開發的時候,會發現在根目錄出現了一個server-worker.js文件,這個東西就是和service Worker緩存相關的,他會根據網絡的狀態做出不同的緩存策略,有時候斷網了,之前訪問過的接口有可能依然會返回數據,其數據來源就是從其緩存中讀取。

memory cache

這個是將資源緩存在了內存中。事實上,所有的網絡請求都會被瀏覽器緩存到內存中,當然,內存容量有限,緩存不能無限存放在內存中,因此,註定是個短期緩存。而其控制權在於瀏覽器。

disk cache

與內存緩存相對的,這個是將資源緩存在硬盤中。雖然相比於內存,硬盤的讀取速度要慢很多,但總比沒有強。硬盤緩存的控制權在後端,通過什麼控制呢?通過HTTP響應頭控制,也就是我們在上面說到的catche-control和expires

Expires設置的過期時間是一個絕對的GMT時間,例如:Expires:Thu,20 Jun 2019 20:00:00 GMT; 他告訴瀏覽器緩存有效性持續到2019年6月20日為止,一直都使用緩存來處理。

Expires有一個非常大的缺陷,它使用一個固定的時間,要求服務器與客戶端的時鐘保持嚴格的同步,並且這一天到來后,服務器還得重新設定新的時間。

HTTP1.1引入了Cathe-Control,它使用max-age指定組件被緩存多久,從請求開始在max-age時間內瀏覽器使用緩存,之外的使用請求,這樣就可以消除Expires的限制,

如果對瀏覽器兼容性要求很高的話,可以兩個都使用。

其實在上面說到的Last-Modified對比最後修改時間與Expires一樣是有缺陷的,如果,資源的變化的時間間隔小於秒級,比如說是毫秒級的,或者說資源直接是動態生成的,那根據Last-Modified判斷,資源就是每時每刻都最新的,即被修改過!

所以,Etag & If-Node-Match 就是來解決這個問題的。
Etag字段的值為文件的特殊標識,一般都是hash生成的,服務器存儲着資源的Etag值。接下來的流程都與Lst-Modified & If-Modified-Since一致,只不過,比較的值從最後修改時間變成了Etag值。

Etag的優點在於,對於動態資源或者現在流行的Restful API返回的JSON數據,這些是沒有修改時間這一說法的,但是Http標準並沒有規定Etag值如何生成,因此我們通過代碼自己生成Etag值。當然,計算Etag值會消耗服務器性能。

Cache-Control+Last-Modified+ETag 的優先級會如何?

因為http1.1>http1.0,所以Cache-Control>Expires,ETag>Last-Modified。依照就近原則,先找本地緩存,沒有再向服務器發請求,所以Expires>Last-Modified,Cache-Control>ETag,

如果瀏覽器只支持http1.0,那麼瀏覽器只會攜帶Last-Modified發送給後台,
如果服務器只支持http1.0,那麼服務器會以Last-Modified為標準。
如果瀏覽器支持http1.1,那麼瀏覽器會攜帶Cache-Control+Last-Modified+ETag發送給後台,
如果服務器支持http1.1,那麼服務器會以Cache-Control+ETag為標準。

200狀態碼和304狀態碼何時出現

在沒有設置Cache-Contral的情況下,設置Last-Modified和ETag緩存,會出現200(from cache)和304 交替出現的情況。

設置Cache-Contral的情況下,過期刷新會出現304(如果有更新內容,則是200),之後再過期之前刷新都是200(from cache)。如果要確保要向服務端確認,可以將Cache-Contral的max-age設置為0。

通過下圖我們可以清晰的明白200和304

https://www.oecom.cn/http-header-control/

 

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

【其他文章推薦】

※回頭車貨運收費標準

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

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

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

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

※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

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

解惑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

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

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

Vue中key的作用

Vue中key的作用

key的特殊attribute主要用在Vue的虛擬DOM算法,在新舊Nodes對比時辨識VNodes。如果不使用key,Vue會使用一種最大限度減少動態元素並且盡可能的嘗試就地修改、復用相同類型元素的算法,而使用key時,它會基於key的變化重新排列元素順序,並且會移除key不存在的元素。此外有相同父元素的子元素必須有獨特的key,重複的key會造成渲染錯誤。

描述

首先是官方文檔的描述,當Vue正在更新使用v-for渲染的元素列表時,它默認使用就地更新的策略,如果數據項的順序被改變,Vue將不會移動DOM元素來匹配數據項的順序,而是就地更新每個元素,並且確保它們在每個索引位置正確渲染。這個默認的模式是高效的,但是只適用於不依賴子組件狀態或臨時DOM狀態的列表渲染輸出,例如表單輸入值。為了給Vue一個提示,以便它能跟蹤每個節點的身份,從而重用和重新排序現有元素,你需要為每項提供一個唯一 key attribute,建議盡可能在使用v-for時提供key attribute,除非遍歷輸出的DOM內容非常簡單,或者是刻意依賴默認行為以獲取性能上的提升。
簡單來說,當在列表循環中使用key時,需要使用key來給每個節點做一個唯一標識,diff算法就可以正確的識別此節點,找到正確的位置直接操作節點,盡可能地進行重用元素,key的作用主要是為了高效的更新虛擬DOM。此外,使用index作為key是並不推薦的做法,其只能保證Vue在數據變化時強制更新組件,以避免原地復用帶來的副作用,但不能保證最大限度的元素重用,且使用index作為key在數據更新方面和不使用key的效果基本相同。

示例

首先定義一個Vue實例,渲染四個列表,分別為簡單列表與複雜列表,以及其分別攜帶key與不攜帶key時對比其更新渲染時的速度,本次測試使用的是Chrome 81.0,每次在Console執行代碼時首先會進行刷新重新加載界面,避免瀏覽器以及Vue自身優化帶來的影響。

<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>
</head>
<body>
    <div id="app">
        <ul>
            <li v-for="item in simpleListWithoutKey" >{{item}}</li>
        </ul>

        <ul>
            <li v-for="item in simpleListWithKey" :key="item" >{{item}}</li>
        </ul>

        <ul>
            <li v-for="item in complexListWithoutKey">
                <span v-for="value in item.list" v-if="value > 5">{{value}}</span>
            </li>
        </ul>

        <ul>
            <li v-for="item in complexListWithKey" :key="item.id">
                <span v-for="value in item.list" :key="value" v-if="value > 5">{{value}}</span>
            </li>
        </ul>

    </div>
</body>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data: {
            simpleListWithoutKey: [1, 2, 3, 4, 5, 6],
            simpleListWithKey: [1, 2, 3, 4, 5, 6],
            complexListWithoutKey:[
                {id: 1, list: [1, 2, 3]},
                {id: 2, list: [4, 5, 6]},
                {id: 3, list: [7, 8, 9]}
            ],
            complexListWithKey:[
                {id: 1, list: [1, 2, 3]},
                {id: 2, list: [4, 5, 6]},
                {id: 3, list: [7, 8, 9]}
            ],
        }
    })
</script>
</html>

簡單列表

在簡單列表的情況下,不使用key可能會比使用key的情況下在更新時的渲染速度更快,這也就是官方文檔中提到的,除非遍歷輸出的DOM內容非常簡單,或者是刻意依賴默認行為以獲取性能上的提升。在下面的例子中可以看到沒有key的情況下列表更新時渲染速度會快,當不存在key的情況下,這個列表直接進行原地復用,原有的節點的位置不變,原地復用元素,將內容更新為5、6、7、8、9、10,並添加了11與12兩個節點,而存在key的情況下,原有的1、2、3、4節點被刪除,5、6節點保留,添加了7、8、9、10、11、12六個節點,由於在DOM的增刪操作上比較耗時,所以表現為不帶key的情況下速度更快一些。

// 沒有key的情況下
console.time();
vm.simpleListWithoutKey = [5, 6, 7, 8, 9, 10, 11, 12];
vm.$nextTick(() => console.timeEnd());
// default: 2.193056640625ms
// 存在key的情況下
console.time();
vm.simpleListWithKey = [5, 6, 7, 8, 9, 10, 11, 12];
vm.$nextTick(() => console.timeEnd());
// default: 3.2138671875ms

原地復用可能會帶來一些副作用,文檔中提到原地復用這個默認的模式是高效的,但是只適用於不依賴子組件狀態或臨時DOM狀態的列表渲染輸出,例如表單輸入值。在不設置key的情況下,元素中沒有與數據data綁定的部分,Vue會默認使用已經渲染的DOM,而綁定了數據data的部分會進行跟隨數據渲染,假如操作了元素位置,則元素中未綁定data的部分會停留在原地,而綁定了data的部分會跟隨操作進行移動,在下面的例子中首先需要將兩個A之後的輸入框添加數據信息,這樣就製作了一個臨時狀態,如果此時點擊下移按鈕,那麼不使用key的組中的輸入框將不會跟隨下移,且B到了頂端並成為了紅色,而使用key的組中會將輸入框進行下移,且A依舊是紅色跟隨下移。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>就地復用</title>
</head>
<body>

  <div id="app">
    <h3>採用就地復用策略(vuejs默認情況)</h3>
    <div  v-for='(p, i) in persons'>
      <span>{{p.name}}<span>  
      <input type="text"/>  
      <button @click='down(i)' v-if='i != persons.length - 1'>下移</button>
    </div> 

    <h3>不採用就地復用策略(設置key)</h3>
    <div  v-for='(p, i) in persons' :key='p.id'>
      <span>{{p.name}}<span> 
      <input type="text"/>  
      <button @click='down(i)' v-if='i != persons.length - 1'>下移</button>
    </div>

  </div>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        persons: [
            { id: 1, name: 'A' },
            { id: 2, name: 'B' },
            { id: 3, name: 'C' }
        ]
      },
      mounted: function(){
        // 此DOM操作將兩個A的顏色設置為紅色 主要是為了演示原地復用
        document.querySelectorAll("h3 + div > span:first-child").forEach( v => v.style.color="red");
      },
      methods: {
        down: function(i) {
            if (i == this.persons.length - 1) return;
          var listClone = this.persons.slice();
          var one = listClone[i];
          listClone[i] = listClone[i + 1];
          listClone[i + 1] = one;
          this.persons = listClone;
        }
      }
    });
  </script>
</body>
</html>
<!-- 源於 https://www.zhihu.com/question/61078310 @霸都丶傲天 有修改-->

複雜列表

使用key不僅能夠避免上述的原地復用的副作用,且在一些操作上可能能夠提高渲染的效率,主要體現在重新排序的情況,包括在中間插入和刪除節點的操作,在下面的例子中沒有key的情況下重新排序會原地復用元素,但是由於v-if綁定了data所以會一併進行操作,在這個DOM操作上比較消耗時間,而使用key得情況則直接復用元素,v-if控制的元素在初次渲染就已經決定,在本例中沒有對其進行更新,所以不涉及v-if的DOM操作,所以在效率上會高一些。

console.time();
vm.complexListWithoutKey = [
        {id: 3, list: [7, 8, 9]},
        {id: 2, list: [4, 5, 6]},
        {id: 1, list: [1, 2, 3]},
    ];
vm.$nextTick(() => console.timeEnd());
vm.$nextTick(() => console.timeEnd());
// default: 4.100244140625ms
console.time();
vm.complexListWithKey = [
        {id: 3, list: [7, 8, 9]},
        {id: 2, list: [4, 5, 6]},
        {id: 1, list: [1, 2, 3]},
    ];
vm.$nextTick(() => console.timeEnd());
// default: 3.016064453125ms

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://cn.vuejs.org/v2/api/#key
https://www.jianshu.com/p/4bdd2690859c
https://www.zhihu.com/question/61078310
https://segmentfault.com/a/1190000012861862
https://www.cnblogs.com/zhumingzhenhao/p/7688336.html
https://blog.csdn.net/hl18730262380/article/details/89306500
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/1
https://cn.vuejs.org/v2/guide/list.html#%E7%BB%B4%E6%8A%A4%E7%8A%B6%E6%80%81

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

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

※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

※台中搬家公司費用怎麼算?

.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維修中心

※超省錢租車方案

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

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

[源碼解析] Flink的groupBy和reduce究竟做了什麼

[源碼解析] Flink的groupBy和reduce究竟做了什麼

目錄

  • [源碼解析] Flink的groupBy和reduce究竟做了什麼
    • 0x00 摘要
    • 0x01 問題和概括
      • 1.1 問題
      • 1.2 概括
    • 0x02 背景概念
      • 2.1 MapReduce細分
      • 2.2 MapReduce細分
      • 2.3 Combine
      • 2.4 Partition
      • 2.5 Shuffle
      • 2.6 Reducer
    • 0x03 代碼
    • 0x04 從Flink JAVA API入手挖掘
      • 4.1 GroupBy是個輔助概念
        • 4.1.1 Grouping
        • 4.1.2 UnsortedGrouping
      • 4.2 reduce才是算子
    • 0x05 批處理執行計劃(Plan)
    • 0x06 批處理優化計劃(Optimized Plan)
    • 0x07 JobGraph
    • 0x08 Runtime
      • 8.1 FlatMap
        • 8.1.1 Combine
        • 8.1.2 Partition
      • 8.2 UnilateralSortMerger
        • 8.2.1 三種線程
        • 8.2.2 MutableObjectIterator
      • 8.3 ReduceDriver
    • 0x09 參考

0x00 摘要

Groupby和reduce是大數據領域常見的算子,但是很多同學應該對其背後機制不甚了解。本文將從源碼入手,為大家解析Flink中Groupby和reduce的原理,看看他們在背後做了什麼。

0x01 問題和概括

1.1 問題

探究的原因是想到了幾個問題 :

  • groupby的算子會對數據進行排序嘛。
  • groupby和reduce過程中究竟有幾次排序。
  • 如果有多個groupby task,什麼機制保證所有這些grouby task的輸出中,同樣的key都分配給同一個reducer。
  • groupby和reduce時候,有沒有Rebalance 重新分配。
  • reduce算子會不會重新劃分task。
  • reduce算子有沒有可能和前後的其他算子組成Operator Chain。

1.2 概括

為了便於大家理解,我們先總結下,對於一個Groupby + Reduce的操作,Flink做了如下處理:

  • Group其實沒有真實對應的算子,它只是在在reduce過程之前的一个中間步驟或者輔助步驟。
  • 在Flink生成批處理執行計劃后,有意義的結果是Reduce算子。
  • 為了更好的reduce,Flink在reduce之前大量使用了Combine操作。Combine可以理解為是在map端的reduce的操作,對單個map任務的輸出結果數據進行合併的操作。
  • 在Flink生成批處理優化計劃(Optimized Plan)之後,會把reduce分割成兩段,一段是SORTED_PARTIAL_REDUCE,一段是SORTED_REDUCE。
  • SORTED_PARTIAL_REDUCE就是Combine。
  • Flink生成JobGraph之後,Flink形成了一個Operator Chain:Reduce(SORTED_PARTIAL_REDUCE)和其上游合併在一起。
  • Flink用Partitioner來保證多個 grouby task 的輸出中同樣的key都分配給同一個reducer。
  • groupby和reduce過程中至少有三次排序:
    • combine
    • sort + merge
    • reduce

這樣之前的疑問就基本得到了解釋。

0x02 背景概念

2.1 MapReduce細分

MapReduce是一種編程模型,用於大規模數據集的并行運算。概念 “Map(映射)”和”Reduce(歸約)” 是它們的主要思想,其是從函數式編程語言,矢量編程語言里借來的特性。

我們目前使用的Flink,Spark都出自於MapReduce,所以我們有必有追根溯源,看看MapReduce是如何區分各個階段的。

2.2 MapReduce細分

如果把MapReduce細分,可以分為一下幾大過程:

  • Input-Split(輸入分片):此過程是將從HDFS上讀取的文件分片,然後送給Map端。有多少分片就有多少Mapper,一般分片的大小和HDFS中的塊大小一致。
  • Shuffle-Spill(溢寫):每個Map任務都有一個環形緩衝區。一旦緩衝區達到閾值80%,一個後台線程便開始把內容“溢寫”-“spill”到磁盤。在溢寫過程中,map將繼續輸出到剩餘的20%空間中,互不影響,如果緩衝區被填滿map會被堵塞直到寫磁盤完成。
  • Shuffle-Partition(分區):由於每個Map可能處理的數據量不同,所以到達reduce有可能會導致數據傾斜。分區可以幫助我們解決這一問題,在shuffle過程中會按照默認key的哈希碼對分區數量取余,reduce便根據分區號來拉取對應的數據,達到數據均衡。分區數量對應Reduce個數。
  • Shuffle-Sort(排序):在分區后,會對此分區的數據進行內排序,排序過程會穿插在整個MapReduce中,在很多地方都存在。
  • Shuffle-Group(分組):分組過程會把key相同的value分配到一個組中,wordcount程序就利用了分組這一過程。
  • Shuffle-Combiner(組合):這一過程我們可以理解為一個小的Reduce階段,當數據量大的時候可以在map過程中執行一次combine,這樣就相當於在map階段執行了一次reduce。由於reduce和map在不同的節點上運行,所以reduce需要遠程拉取數據,combine就可以有效降低reduce拉取數據的量,減少網絡負荷(這一過程默認是不開啟的,在如求平均值的mapreduce程序中不要使用combine,因為會影響結果)。
  • Compress(壓縮):在緩衝區溢寫磁盤的時候,可以對數據進行壓縮,節約磁盤空間,同樣減少給reducer傳遞的數據量。
  • Reduce-Merge(合併):reduce端會拉取各個map輸出結果對應的分區文件,這樣reduce端就會有很多文件,所以在此階段,reduce再次將它們合併/排序再送入reduce執行。
  • Output(輸出):在reduce階段,對已排序輸出中的每個鍵調用reduce函數。此階段的輸出直接寫到輸出文件系統,一般為HDFS。

2.3 Combine

Combine是我們需要特殊注意的。在mapreduce中,map多,reduce少。在reduce中由於數據量比較多,所以我們乾脆在map階段中先把自己map裏面的數據歸類,這樣到了reduce的時候就減輕了壓力。

Combine可以理解為是在map端的reduce的操作,對單個map任務的輸出結果數據進行合併的操作。combine是對一個map的,而reduce合併的對象是對於多個map。

map函數操作所產生的鍵值對會作為combine函數的輸入,經combine函數處理后再送到reduce函數進行處理,減少了寫入磁盤的數據量,同時也減少了網絡中鍵值對的傳輸量。在Map端,用戶自定義實現的Combine優化機制類Combiner在執行Map端任務的節點本身運行,相當於對map函數的輸出做了一次reduce。

集群上的可用帶寬往往是有限的,產生的中間臨時數據量很大時就會出現性能瓶頸,因此應該盡量避免Map端任務和Reduce端任務之間大量的數據傳輸。使用Combine機制的意義就在於使Map端輸出更緊湊,使得寫到本地磁盤和傳給Reduce端的數據更少。

2.4 Partition

Partition是分割map每個節點的結果,按照key分別映射給不同的reduce,mapreduce使用哈希HashPartitioner幫我們歸類了。這個我們也可以自定義。

這裏其實可以理解歸類。我們對於錯綜複雜的數據歸類。比如在動物園裡有牛羊雞鴨鵝,他們都是混在一起的,但是到了晚上他們就各自牛回牛棚,羊回羊圈,雞回雞窩。partition的作用就是把這些數據歸類。只不過是在寫程序的時候,

在經過mapper的運行后,我們得知mapper的輸出是這樣一個key/value對: key是“aaa”, value是數值1。因為當前map端只做加1的操作,在reduce task里才去合併結果集。假如我們知道這個job有3個reduce task,到底當前的“aaa”應該交由哪個reduce task去做呢,是需要立刻決定的。

MapReduce提供Partitioner接口,它的作用就是根據key或value及reduce task的數量來決定當前的這對輸出數據最終應該交由哪個reduce task處理。默認對key hash后再以reduce task數量取模。默認的取模方式只是為了平均reduce的處理能力,如果用戶自己對Partitioner有需求,可以訂製並設置到job上。

在我們的例子中,假定 “aaa”經過Partitioner后返回0,也就是這對值應當交由第一個reducer來處理。

2.5 Shuffle

shuffle就是map和reduce之間的過程,包含了兩端的combine和partition。它比較難以理解,因為我們摸不着,看不到它。它屬於mapreduce的框架,編程的時候,我們用不到它。

Shuffle的大致範圍就是:怎樣把map task的輸出結果有效地傳送到reduce端。也可以這樣理解, Shuffle描述着數據從map task輸出到reduce task輸入的這段過程。

2.6 Reducer

簡單地說,reduce task在執行之前的工作就是不斷地拉取當前job里每個map task的最終結果,然後對從不同地方拉取過來的數據不斷地做merge,最終形成一個文件作為reduce task的輸入文件。

0x03 代碼

我們以Flink的KMeans算法作為樣例,具體摘要如下:

public class WordCountExampleReduce {

    DataStream ds;

    public static void main(String[] args) throws Exception {
        //構建環境
        final ExecutionEnvironment env =
                ExecutionEnvironment.getExecutionEnvironment();
        //通過字符串構建數據集
        DataSet<String> text = env.fromElements(
                "Who‘s there?",
                "I think I hear them. Stand, ho! Who‘s there?");
        //分割字符串、按照key進行分組、統計相同的key個數
        DataSet<Tuple2<String, Integer>> wordCounts = text
                .flatMap(new LineSplitter())
                .groupBy(0)
                .reduce(new ReduceFunction<Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1,
                                          Tuple2<String, Integer> value2) throws Exception {
                        return new Tuple2(value1.f0, value1.f1 + value2.f1);
                    }
                });
        //打印
        wordCounts.print();
    }
    //分割字符串的方法
    public static class LineSplitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String line, Collector<Tuple2<String, Integer>> out) {
            for (String word : line.split(" ")) {
                out.collect(new Tuple2<String, Integer>(word, 1));
            }
        }
    }
}

輸出是:

(hear,1)
(ho!,1)
(them.,1)
(I,2)
(Stand,,1)
(Who‘s,2)
(there?,2)
(think,1)

0x04 從Flink JAVA API入手挖掘

首先,我們從Flink基本JAVA API來入手開始挖掘。

4.1 GroupBy是個輔助概念

4.1.1 Grouping

我們需要留意的是:GroupBy並沒有對應的Operator。GroupBy只是生成DataSet轉換的一个中間步驟或者輔助步驟。

GroupBy功能的基類是Grouping,其只是DataSet轉換的一个中間步驟。其幾個主要成員是:

  • 對應的輸入數據DataSet
  • 分組所基於的keys
  • 用戶自定義的Partitioner
// Grouping is an intermediate step for a transformation on a grouped DataSet.
public abstract class Grouping<T> {
   protected final DataSet<T> inputDataSet;
   protected final Keys<T> keys;
   protected Partitioner<?> customPartitioner;
}

Grouping並沒有任何業務相關的API,具體API都是在其派生類中,比如UnsortedGrouping。

4.1.2 UnsortedGrouping

我們代碼中對應的就是UnsortedGrouping類。我們看到它提供了很多業務API,比如:sum,max,min,reduce,aggregate,reduceGroup,combineGroup…..

回到我們的示例,groupBy做了如下操作

  • 首先,groupBy返回的就是一個UnsortedGrouping,這個UnsortedGrouping是用來轉換DataSet。
  • 其次,.groupBy(0).reduce(new CentroidAccumulator()) 返回的是ReduceOperator。這就對應了前面我們提到的,groupBy只是中間步驟,reduce才能返回一個Operator。
public class UnsortedGrouping<T> extends Grouping<T> {
  
    // groupBy返回一個UnsortedGrouping
    public UnsortedGrouping<T> groupBy(int... fields) {
       return new UnsortedGrouping<>(this, new Keys.ExpressionKeys<>(fields, getType()));
    }
  
    // reduce返回一個ReduceOperator
 		public ReduceOperator<T> reduce(ReduceFunction<T> reducer) {
      return new ReduceOperator<T>(this, inputDataSet.clean(reducer), Utils.getCallLocationName());
    } 
}

4.2 reduce才是算子

對於業務來說,reduce才是真正有意義的邏輯算子。

從前文的函數調用和ReduceOperator定義可以看出,.groupBy(0).reduce() 的調用結果是生成一個ReduceOperator,而 UnsortedGrouping 被設置為 ReduceOperator 的 grouper 成員變量,作為輔助操作。

public class ReduceOperator<IN> extends SingleInputUdfOperator<IN, IN, ReduceOperator<IN>> {
  
	private final ReduceFunction<IN> function;
	private final Grouping<IN> grouper; // UnsortedGrouping被設置在這裏,後續reduce操作中會用到。

	public ReduceOperator(Grouping<IN> input, ReduceFunction<IN> function, 
                        String defaultName) {
		this.function = function;
		this.grouper = input; // UnsortedGrouping被設置在這裏,後續reduce操作中會用到。
    this.hint = CombineHint.OPTIMIZER_CHOOSES; // 優化時候會用到。
	}
}

讓我們順着Flink程序執行階段繼續看看系統都做了些什麼。

0x05 批處理執行計劃(Plan)

程序執行的第一步是:當程序運行時候,首先會根據java API的結果來生成執行plan。

public JobClient executeAsync(String jobName) throws Exception {
   final Plan plan = createProgramPlan(jobName);
} 

其中重要的函數是translateToDataFlow,因為在translateToDataFlow方法中,會從批處理Java API模塊中operators包往核心模塊中operators包的轉換。

對於我們的示例程序,在生成 Graph時,translateToDataFlow會生成一個 SingleInputOperator,為後續runtime使用。下面是代碼縮減版。

protected org.apache.flink.api.common.operators.SingleInputOperator<?, IN, ?> translateToDataFlow(Operator<IN> input) {
    
    ......
      
    // UnsortedGrouping中的keys被取出,  
		else if (grouper.getKeys() instanceof Keys.ExpressionKeys) {

			// reduce with field positions
			ReduceOperatorBase<IN, ReduceFunction<IN>> po =
					new ReduceOperatorBase<>(function, operatorInfo, logicalKeyPositions, name);

			po.setCustomPartitioner(grouper.getCustomPartitioner());
			po.setInput(input);
			po.setParallelism(getParallelism()); // 沒有并行度的變化

			return po;//translateToDataFlow會生成一個 SingleInputOperator,為後續runtime使用
		}	    
  }  
}

我們代碼最終生成的執行計劃如下,我們可以看出來,執行計劃基本符合我們的估計:簡單的從輸入到輸出。中間有意義的算子其實只有Reduce。

GenericDataSourceBase ——> FlatMapOperatorBase ——> ReduceOperatorBase ——> GenericDataSinkBase

具體在代碼中體現如下是:

plan = {Plan@1296} 
 sinks = {ArrayList@1309}  size = 1
  0 = {GenericDataSinkBase@1313} "collect()"
   formatWrapper = {UserCodeObjectWrapper@1315} 
   input = {ReduceOperatorBase@1316} "ReduceOperatorBase - Reduce at main(WordCountExampleReduceCsv.java:25)"
    hint = {ReduceOperatorBase$CombineHint@1325} "OPTIMIZER_CHOOSES"
    customPartitioner = null
    input = {FlatMapOperatorBase@1326} "FlatMapOperatorBase - FlatMap at main(WordCountExampleReduceCsv.java:23)"
     input = {GenericDataSourceBase@1339} "at main(WordCountExampleReduceCsv.java:20) (org.apache.flink.api.java.io.TextInputFormat)"

0x06 批處理優化計劃(Optimized Plan)

程序執行的第二步是:Flink對於Plan會繼續優化,生成Optimized Plan。其核心代碼位於PlanTranslator.compilePlan 函數,這裏得到了Optimized Plan。

這個編譯的過程不作任何決策與假設,也就是說作業最終如何被執行早已被優化器確定,而編譯也是在此基礎上做確定性的映射。所以我們將集中精力看如何優化plan。

private JobGraph compilePlan(Plan plan, Configuration optimizerConfiguration) {
   Optimizer optimizer = new Optimizer(new DataStatistics(), optimizerConfiguration);
   OptimizedPlan optimizedPlan = optimizer.compile(plan);

   JobGraphGenerator jobGraphGenerator = new JobGraphGenerator(optimizerConfiguration);
   return jobGraphGenerator.compileJobGraph(optimizedPlan, plan.getJobId());
}

在內部調用plan的accept方法遍歷它。accept會挨個在每個sink上調用accept。對於每個sink會先preVisit,然後 postVisit。

這裏優化時候有幾個注意點:

  1. 在 GraphCreatingVisitor.preVisit 中,當發現Operator是 ReduceOperatorBase 類型的時候,會建立ReduceNode。

    else if (c instanceof ReduceOperatorBase) {
       n = new ReduceNode((ReduceOperatorBase<?, ?>) c);
    }
    
  2. ReduceNode是Reducer Operator的Optimizer表示。

    public class ReduceNode extends SingleInputNode {
    	private final List<OperatorDescriptorSingle> possibleProperties;	
    	private ReduceNode preReduceUtilityNode;
    }
    
  3. 生成ReduceNode時候,會根據之前提到的 hint 來決定 combinerStrategy = DriverStrategy.SORTED_PARTIAL_REDUCE;

    public ReduceNode(ReduceOperatorBase<?, ?> operator) {
    			DriverStrategy combinerStrategy;
    			switch(operator.getCombineHint()) {
    				case OPTIMIZER_CHOOSES:
    					combinerStrategy = DriverStrategy.SORTED_PARTIAL_REDUCE;
    					break;
          }  
    }
    

生成的優化執行計劃如下,我們可以看到,這時候設置了并行度,也把reduce分割成兩段,一段是SORTED_PARTIAL_REDUCE,一段是SORTED_REDUCE。

Data Source  ——> FlatMap ——> Reduce(SORTED_PARTIAL_REDUCE)   ——> Reduce(SORTED_REDUCE)  ——> Data Sink

具體在代碼中體現如下是:

optimizedPlan = {OptimizedPlan@1506} 
 
 allNodes = {HashSet@1510}  size = 5
   
  0 = {SourcePlanNode@1512} "Data Source "at main(WordCountExampleReduceCsv.java:20) (org.apache.flink.api.java.io.TextInputFormat)" : NONE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  1 = {SingleInputPlanNode@1513} "FlatMap "FlatMap at main(WordCountExampleReduceCsv.java:23)" : FLAT_MAP [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  2 = {SingleInputPlanNode@1514} "Reduce "Reduce at main(WordCountExampleReduceCsv.java:25)" : SORTED_REDUCE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  3 = {SinkPlanNode@1515} "Data Sink "collect()" : NONE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  4 = {SingleInputPlanNode@1516} "Reduce "Reduce at main(WordCountExampleReduceCsv.java:25)" : SORTED_PARTIAL_REDUCE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

0x07 JobGraph

程序執行的第三步是:建立JobGraph。LocalExecutor.execute中會生成JobGraph。Optimized Plan 經過優化後生成了 JobGraph, JobGraph是提交給 JobManager 的數據結構。

主要的優化為,將多個符合條件的節點 chain 在一起作為一個節點,這樣可以減少數據在節點之間流動所需要的序列化/反序列化/傳輸消耗。

JobGraph是唯一被Flink的數據流引擎所識別的表述作業的數據結構,也正是這一共同的抽象體現了流處理和批處理在運行時的統一。

public CompletableFuture<JobClient> execute(Pipeline pipeline, Configuration configuration) throws Exception {
   final JobGraph jobGraph = getJobGraph(pipeline, configuration);
}

我們可以看出來,這一步形成了一個Operator Chain:

CHAIN DataSource -> FlatMap -> Combine (Reduce) 

於是我們看到,Reduce(SORTED_PARTIAL_REDUCE)和其上游合併在一起。

具體在程序中打印出來:

jobGraph = {JobGraph@1739} "JobGraph(jobId: 30421d78d7eedee6be2c5de39d416eb7)"
 taskVertices = {LinkedHashMap@1742}  size = 3
  
  {JobVertexID@1762} "e2c43ec0df647ea6735b2421fb7330fb" -> {InputOutputFormatVertex@1763} "CHAIN DataSource (at main(WordCountExampleReduceCsv.java:20) (org.apache.flink.api.java.io.TextInputFormat)) -> FlatMap (FlatMap at main(WordCountExampleReduceCsv.java:23)) -> Combine (Reduce at main(WordCountExampleReduceCsv.java:25)) (org.apache.flink.runtime.operators.DataSourceTask)"
  
  {JobVertexID@1764} "2de11f497e827e48dda1d63b458dead7" -> {JobVertex@1765} "Reduce (Reduce at main(WordCountExampleReduceCsv.java:25)) (org.apache.flink.runtime.operators.BatchTask)"
  
  {JobVertexID@1766} "2bee17f2c86aa1e9439e3dedea58007b" -> {InputOutputFormatVertex@1767} "DataSink (collect()) (org.apache.flink.runtime.operators.DataSinkTask)"

0x08 Runtime

Job提交之後,就是程序正式運行了。這裏實際上涉及到了三次排序,

  • 一次是在FlatMap發送時候調用到了ChainedReduceCombineDriver.sortAndCombine。這部分對應了我們之前提到的MapReduce中的Combine和Partition。
  • 一次是在 ReduceDriver 所在的 BatchTask中,由UnilateralSortMerger完成了sort & merge操作。
  • 一次是在ReduceDriver,這裏做了最後的reducer排序。

8.1 FlatMap

這裡是第一次排序。

當一批數據處理完成之後,在ChainedFlatMapDriver中調用到close函數進行發送數據給下游。

public void close() {
   this.outputCollector.close();
}

Operator Chain會調用到ChainedReduceCombineDriver.close

public void close() {
   // send the final batch
   try {
      switch (strategy) {
         case SORTED_PARTIAL_REDUCE:
            sortAndCombine(); // 我們是在這裏
            break;
         case HASHED_PARTIAL_REDUCE:
            reduceFacade.emit();
            break;
      }
   } catch (Exception ex2) {
      throw new ExceptionInChainedStubException(taskName, ex2);
   }

   outputCollector.close();
   dispose(false);
}

8.1.1 Combine

sortAndCombine中先排序,然後做combine,最後會不斷髮送數據。

private void sortAndCombine() throws Exception {
   final InMemorySorter<T> sorter = this.sorter;

   if (!sorter.isEmpty()) {
      sortAlgo.sort(sorter); // 這裡會先排序

      final TypeSerializer<T> serializer = this.serializer;
      final TypeComparator<T> comparator = this.comparator;
      final ReduceFunction<T> function = this.reducer;
      final Collector<T> output = this.outputCollector;
      final MutableObjectIterator<T> input = sorter.getIterator();

      if (objectReuseEnabled) {
        ......
      } else {
         T value = input.next();

         // 這裏就是combine
         // iterate over key groups
         while (running && value != null) {
            comparator.setReference(value);
            T res = value;

            // iterate within a key group
            while ((value = input.next()) != null) {
               if (comparator.equalToReference(value)) {
                  // same group, reduce
                  res = function.reduce(res, value);
               } else {
                  // new key group
                  break;
               }
            }

            output.collect(res); //發送數據
         }
      }
   }
}

8.1.2 Partition

最後發送給哪個下游,是由OutputEmitter.selectChannel決定的。有如下幾種決定方式:

hash-partitioning, broadcasting, round-robin, custom partition functions。這裏採用的是PARTITION_HASH。

每個task都會把同樣字符串統計結果發送給同樣的下游ReduceDriver。這就保證了下游Reducer一定不會出現統計出錯。

public final int selectChannel(SerializationDelegate<T> record) {
   switch (strategy) {
   ...
   case PARTITION_HASH:
      return hashPartitionDefault(record.getInstance(), numberOfChannels);
   ...
   }
}

private int hashPartitionDefault(T record, int numberOfChannels) {
	int hash = this.comparator.hash(record);
	return MathUtils.murmurHash(hash) % numberOfChannels;
}

具體調用棧:

hash:50, TupleComparator (org.apache.flink.api.java.typeutils.runtime)
hash:30, TupleComparator (org.apache.flink.api.java.typeutils.runtime)
hashPartitionDefault:187, OutputEmitter (org.apache.flink.runtime.operators.shipping)
selectChannel:147, OutputEmitter (org.apache.flink.runtime.operators.shipping)
selectChannel:36, OutputEmitter (org.apache.flink.runtime.operators.shipping)
emit:60, ChannelSelectorRecordWriter (org.apache.flink.runtime.io.network.api.writer)
collect:65, OutputCollector (org.apache.flink.runtime.operators.shipping)
collect:35, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
sortAndCombine:254, ChainedReduceCombineDriver (org.apache.flink.runtime.operators.chaining)
close:266, ChainedReduceCombineDriver (org.apache.flink.runtime.operators.chaining)
close:40, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
close:88, ChainedFlatMapDriver (org.apache.flink.runtime.operators.chaining)
invoke:215, DataSourceTask (org.apache.flink.runtime.operators)
doRun:707, Task (org.apache.flink.runtime.taskmanager)
run:532, Task (org.apache.flink.runtime.taskmanager)
run:748, Thread (java.lang)

8.2 UnilateralSortMerger

這裡是第二次排序。

在 BatchTask中,會先Sort, Merge輸入,然後才會交由Reduce來具體完成過。sort & merge操作具體是在UnilateralSortMerger類中完成的。

getIterator:646, UnilateralSortMerger (org.apache.flink.runtime.operators.sort)
getInput:1110, BatchTask (org.apache.flink.runtime.operators)
prepare:95, ReduceDriver (org.apache.flink.runtime.operators)
run:474, BatchTask (org.apache.flink.runtime.operators)
invoke:369, BatchTask (org.apache.flink.runtime.operators)
doRun:707, Task (org.apache.flink.runtime.taskmanager)
run:532, Task (org.apache.flink.runtime.taskmanager)
run:748, Thread (java.lang)

UnilateralSortMerger是一個full fledged sorter,它實現了一個多路merge sort。其內部的邏輯被劃分到三個線程上(read, sort, spill),這三個線程彼此之間通過一系列blocking queues來構成了一個閉環。

其內存通過MemoryManager分配,所以這個組件不會超過給其分配的內存。

該類主要變量摘錄如下:

public class UnilateralSortMerger<E> implements Sorter<E> {
	// ------------------------------------------------------------------------
	//                                  Threads
	// ------------------------------------------------------------------------

	/** The thread that reads the input channels into buffers and passes them on to the merger. */
	private final ThreadBase<E> readThread;

	/** The thread that merges the buffer handed from the reading thread. */
	private final ThreadBase<E> sortThread;

	/** The thread that handles spilling to secondary storage. */
	private final ThreadBase<E> spillThread;
	
	// ------------------------------------------------------------------------
	//                                   Memory
	// ------------------------------------------------------------------------
	
	/** The memory segments used first for sorting and later for reading/pre-fetching
	 * during the external merge. */
	protected final List<MemorySegment> sortReadMemory;
	
	/** The memory segments used to stage data to be written. */
	protected final List<MemorySegment> writeMemory;
	
	/** The memory manager through which memory is allocated and released. */
	protected final MemoryManager memoryManager;
	
	// ------------------------------------------------------------------------
	//                            Miscellaneous Fields
	// ------------------------------------------------------------------------
	/**
	 * Collection of all currently open channels, to be closed and deleted during cleanup.
	 */
	private final HashSet<FileIOChannel> openChannels;
	
	/**
	 * The monitor which guards the iterator field.
	 */
	protected final Object iteratorLock = new Object();
	
	/**
	 * The iterator to be returned by the sort-merger. This variable is null, while receiving and merging is still in
	 * progress and it will be set once we have &lt; merge factor sorted sub-streams that will then be streamed sorted.
	 */
	protected volatile MutableObjectIterator<E> iterator; 	// 如果大家經常調試,就會發現driver中的input都是這個兄弟。

	private final Collection<InMemorySorter<?>> inMemorySorters;
}

8.2.1 三種線程

ReadingThread:這種線程持續讀取輸入,然後把數據放入到一個待排序的buffer中。The thread that consumes the input data and puts it into a buffer that will be sorted.

SortingThread : 這種線程對於上游填充好的buffer進行排序。The thread that sorts filled buffers.

SpillingThread:這種線程進行歸併操作。The thread that handles the spilling of intermediate results and sets up the merging. It also merges the channels until sufficiently few channels remain to perform the final streamed merge.

8.2.2 MutableObjectIterator

UnilateralSortMerger有一個特殊變量:

protected volatile MutableObjectIterator<E> iterator;

這個變量就是最終sort-merger的輸出。如果大家調試過算子,就會發現這個變量就是具體算子的輸入input類型。最終算子的輸入就是來自於此。

8.3 ReduceDriver

這裡是第三次排序,我們可以看出來reduce是怎麼和groupby一起運作的。

  1. 針對 .groupBy(0),ReduceDriver就是單純獲取輸入的第一個數值 T value = input.next();
  2. 後續代碼中有嵌套的兩個while,分別是 :遍歷各種key,以及某一key中reduce。
  3. 遍歷 group keys的時候,把value賦於比較算子comparator(這個算子概念不是Flink算子,就是為了說明邏輯概念) comparator.setReference(value); 因為groubBy只是指定按照第一個位置比較,沒有指定具體key數值,所以這個value就是key了。此處記為while (1) ,代碼中有註解。
  4. 從輸入中讀取後續的數值value,如果下一個數值是同一個key,就reduce;如果下一個數值不是同一個key,就跳出循環。放棄比較,把reduce結果輸出。此處記為 while (2)
  5. 跳出 while (2) 之後,代碼依然在 while (1) ,此時value是新值,所以繼續在 while (1)中運行 。把value繼續賦於比較算子 comparator.setReference(value);,於是進行新的key比較。
public class ReduceDriver<T> implements Driver<ReduceFunction<T>, T> {
	@Override
	public void run() throws Exception {

		final Counter numRecordsIn = this.taskContext.getMetricGroup().getIOMetricGroup().getNumRecordsInCounter();
		final Counter numRecordsOut = this.taskContext.getMetricGroup().getIOMetricGroup().getNumRecordsOutCounter();

		// cache references on the stack
		final MutableObjectIterator<T> input = this.input;
		final TypeSerializer<T> serializer = this.serializer;
		final TypeComparator<T> comparator = this.comparator;		
		final ReduceFunction<T> function = this.taskContext.getStub();		
		final Collector<T> output = new CountingCollector<>(this.taskContext.getOutputCollector(), numRecordsOut);

		if (objectReuseEnabled) {
      ......
		} else {
      // 針對 `.groupBy(0)`,ReduceDriver就是單純獲取輸入的第一個數值 `T value = input.next();`
			T value = input.next();

      // while (1)
			// iterate over key groups
			while (this.running && value != null) {
				numRecordsIn.inc();
        // 把value賦於比較算子,這個value就是key了。
				comparator.setReference(value);
				T res = value;

        // while (2)
				// iterate within a key group,循環比較這個key
				while ((value = input.next()) != null) {
					numRecordsIn.inc();
					if (comparator.equalToReference(value)) {
						// same group, reduce,如果下一個數值是同一個key,就reduce
						res = function.reduce(res, value);
					} else {
						// new key group,如果下一個數值不是同一個key,就跳出循環,放棄比較。
						break;
					}
				}
        // 把reduce結果輸出
				output.collect(res);
			}
		}
	}  
}

0x09 參考

mapreduce里的shuffle 里的 sort merge 和combine

實戰錄 | Hadoop Mapreduce shuffle之Combine探討

Hadoop中MapReduce中combine、partition、shuffle的作用是什麼?在程序中怎麼運用?

Flink運行時之生成作業圖

mapreduce過程

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

【其他文章推薦】

※回頭車貨運收費標準

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

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

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

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

※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

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

誰說國產SUV都是樣子貨?這款SUV開去哪裡都不怕!

與我換手的教練兼拉力賽賽車手,僅憑着車燈照探着鋪滿冰渣的路面。儘管開得再小心、再緩慢,但無數的暗坑或是前車遺留的巨大冰碴還是讓我們難以躲避。幸好瑞虎7前麥弗遜后多連桿的懸挂結構加上偏向於運動的底盤調校風格,讓顛起的車身被迅速拉住,車身的晃動被有效地控制,這才讓我們這群處於黑暗的趕路人不顯狼狽。

我對東北的印象,最早建立於08年在央視播出的電視劇《闖關東》。在19世紀的特殊歷史條件下,劇中的主角朱家一眾從山東舉家搬遷至東北的元寶鎮。在廣袤荒涼的土地上,面對極寒、土匪、日軍侵略者等磨難,朱家不屈不撓地用雙手在這片東北土地紮下了根。而闖關東這個詞,既成為東北地區最為顯著的民俗文化,也成為對東北人拼搏精神最深刻的刻畫。

這次,隨着瑞虎7“虎眼看中國”第二站的步伐,從長春到長白山,我得以通過自駕瑞虎7的方式,更深入地遊走東北。雖然出發的意義沒有了過去歷史背景的沉甸,但對於極少在寒冬自駕東北的人以及瑞虎7這款奇瑞最新的旗艦SUV來說,這也是對闖關東的一次探究。

消失的“北國春城”

因為高達41.5%的城區綠化覆蓋,長城也被譽為“北國春城“。但在12月低至零下-16℃的背景下,北國春城的愜意怡人變得不復存在。唯一能夠讓人產生浪漫情緒的,大概也就只剩被積雪裹覆的路邊盆栽以及平房屋頂紅磚煙窗上飄着的白煙。所以冬天的長春街頭並不熱鬧,長春人更喜歡待在熱氣瀰漫的室內。反倒是長春的龍嘉機場人頭涌動,因為各地直飛長白山的機票早已售罄,這個距離長白山最近的機場成為了那些奔赴長白山的旅遊者最理想的中轉點。

內在比外在更豐富的瑞虎7

我們由長春驅車前往長白山,全程大概500餘公里。儘管去年通車的松江河高速已經使得兩地的直線距離大大縮短,但飄雪以及光滑的冰面道路仍然使這趟旅程顯得異常險峻。而奇瑞希望這一段旅程能夠給旗下的旗艦SUV—瑞虎7添加更多高性能、高品質的註腳。在此之前,瑞虎7的原型車—TX概念車已經在第83屆的日內瓦車展上榮獲年度最佳概念車獎而走紅。這款誕生自奇瑞2.0時代的產品,其實有更多內在的亮點可以說道。

在整段的松江河高速路段,始終是較為狹窄的三車道,而連日的降雪又讓原本的三車道實際只剩下兩車道。加之覆蓋在路面上的大面積冰塊,提前制動以及不斷修正跑偏的方向是冰雪駕駛時需要學會的技巧。不過全系配備ABS防抱死、ESp車身穩定控制系統、EBA剎車輔助的瑞虎7在整段的行程里都更讓人放心。我不用擔心車輛的側滑,更不用擔心車輪的抱死。除此以外,2.0L自然吸氣發動機+CVT無級變速箱的動力組合也提供了順滑、理想的動力輸出,動力的響應程度要比那些半路出家的小排量增壓發動機+雙離合讓人滿意太多了。這讓我更快、更安全地達到長白山。

盛產刨花板的“烏托邦小鎮”

從松江河高速轉下的第一個小鎮是位於撫松縣的露水河鎮,實際上這也是我在達到長白山之前少有的具有規模的居民區。比起長春的街頭,這裏的街頭行人要明顯更多一些,露天的市集里是熙熙攘攘前來購買日用百貨品的當地百姓。根據百度百科的資料显示,截至2003年,全鎮區的總人口數為41692人,由漢、滿、錫伯、朝、回等多個民族構成。

因為地理上的邊緣化,這裏居民的生活已經完全實現了自給自足。而木材業則成為了露水河鎮與外界打通的交流途徑,經濟產業的單一化甚至造就了“露水河”牌刨花板這樣的 “中國馳名商標”,其代表性完全不亞於我們常說的“茅台酒”。在整個東北地區,大大小小分佈着與露水河鎮這樣民族結構、經濟條件相類似的集中居民區,依靠生活物質上的自給自足以及單一的經濟產業,繁榮而又不受外界打擾,儼然有着烏托邦小鎮的味道。而這些類似露水河鎮的小鎮,也最能體現闖關東的精髓所在。

一個更具生命力的奇瑞

1999年12月18日,第一輛奇瑞轎車下線。而隨後2007年,憑藉奇瑞QQ的熱銷,奇瑞迅速成為自主品牌為數不多產銷規模達到百萬輛的中國車企。在鮮花與掌聲中,奇瑞開啟了多品牌戰略的時代,但這也成為奇瑞多年發展里的一次波折。而讓人高興的是,在這次的波折以後,有了一個更具生命力的奇瑞出現。標志著奇瑞2.0時代到來的奇瑞T1X模塊化平台讓奇瑞成為第一個應用模塊化平台生產的自主品牌。“未來五年,奇瑞將形成全新三大產品平台,開發10款全新產品。”奇瑞掌門人尹同躍在某次公開場合上如此表示,有了模塊化平台的基礎,奇瑞的產品規模有了更豐富的可能性,正向研發的自給自足將成為奇瑞的標誌之一。而瑞虎7,則是奇瑞闖關東所收穫的第一顆果實。

運動懸挂所帶來的安心

吉林的入夜時間遠比我們想象中的要早,大概是4點,天色就已經大有被黑色吞沒之勢。在露水河到長白山的百餘公里縣道,路燈成了稀缺的物品。與我換手的教練兼拉力賽賽車手,僅憑着車燈照探着鋪滿冰渣的路面。儘管開得再小心、再緩慢,但無數的暗坑或是前車遺留的巨大冰碴還是讓我們難以躲避。幸好瑞虎7前麥弗遜后多連桿的懸挂結構加上偏向於運動的底盤調校風格,讓顛起的車身被迅速拉住,車身的晃動被有效地控制,這才讓我們這群處於黑暗的趕路人不顯狼狽。甚至那不錯的隔音、密封工程,還讓瑞虎7凸顯出了不俗的德系車的高級感。

在睡意將近佔據頭腦全部的時候,我們終於走完了將近500公里的冰雪路,並且在長白山腳的酒店落了腳。雖然長白山的美名遠揚,但商業化的氣息並不過分濃郁。除了長白山萬達國際度假區以外,在夜晚幾乎難以找到能夠提供夜宵的餐館。在某家特色東北餐館囫圇大吃以後,我便早早入睡,為明早登頂長白山蓄力。

長白山的“色彩”

長白山的稱呼來源,源於山上的常年積雪,年均的氣溫-7℃至3℃之間。在朝鮮以及韓國,長白山又被稱呼為白頭山。從最初的汪洋,到後來地殼上升,再經歷前後數次的火山爆發,最終形成了如今的長白山山脈。

歷來居住在長白山周邊的民眾相信長白山的形成是天意使然,長白山是德高望重的山神的化身。因此,祭拜長白山山神成了當地民眾的一種精神寄託。明清時期,長白山的人蔘為朝庭貢品所用,普通民眾嚴禁進山採挖。但仍然有不少山東以及河北地區闖關東的农民出於生活所迫,在官兵的緝拿以及零下數十度的嚴寒中偷采人蔘。為了採挖順利,逐漸衍生出了拜山神的民俗。“三月十六,山神之壽。祭祀山神,把頭保佑。放山快當,棒槌拿夠。風調雨順,年豐人壽。”這樣表達敬重山神的歌謠一直在長白山地區長久傳唱。如今長白山的人蔘產業已經成為當地的一大經濟支柱,甚至還有了“不買長白山人蔘,枉到長白山”一說。所以,闖關東也或多或少地對長白山人蔘這種名貴藥材的興起貢獻良多。

而在朝鮮人的眼中,長白山則充滿了政治的味道,是共產主義勝利的標誌。在第二次世界大戰的1936年至1943年期間,朝鮮共產黨領導人金日成曾率領數萬名朝鮮共產主義戰士與侵朝日軍進行抗爭。在長白山上,修建了後方聯絡站、修械所、出版所、醫院等設施。長白山中一座高1791米的山峰為朝鮮人稱為“正日峰”,以彰顯朝鮮的氣概。在金正日五十壽辰的時候,金日成曾寫下這樣的一首詩:“白頭山頂,正日峰。小白水河,碧溪流。光明星誕,五十周。皆贊文武,忠孝備。萬民稱頌,齊同心。歡呼聲高,震天地,白頭山密營舊居。”

當然,歷史上的長白山全境一直屬中國所有,直到上世紀60年代,才以長白山頂峰的天池為界,把部分山脈劃分給朝鮮,由此長白山成為中朝兩國界山。這種出於兩國友誼而無償劃分國土的行為並不多見,而大多數的中國人也對長白山領土的出讓頗具爭議。但無論如何,至此中國與朝鮮一直是社會主義陣營里關係最堅實的一對。

登上長白山山頂的過程有些艱巨,先是乘觀光大巴爬一小時的山路,爾後再轉乘雪地摩托穿越陡峭的雪峰,最後踩着那些能把小腿陷大半的雪走上數十分鐘。我在即將達到山峰的位置停下了腳步,因為身後那些被夕陽的餘暉所染紅的雪峰、那些繚繞在半山腰的滾滾霧氣已經足夠讓我徹底拋去親近天池的慾望。拿起手機拍照記下這些畫面,然後向著天池的方向虔誠地許上一個願,我認為這是比登頂更具有意義的事。

闖關東就是闖

當然,對於奇瑞來說,攀登自主品牌頂峰才是它最終的目標。不過在登頂之前,瑞虎7產品本身所展現出的自信已經足夠讓我相信:奇瑞能夠造出一台媲美合資品牌的SUV。在《闖關東》里,朱開山說:“闖關東就是闖,這不行,咱到別出去,哪能活我就到哪去。”之於瑞虎7,我想是闖出中國SUV的名堂了。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

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

都說發飄的車不是好車,為什麼現在的車還越造越輕?

一般來說升力:Y= Cy1/2 ρv2sρ—空氣密度s—機翼面積Cy—升力係數v—氣流速度Cy = Y/(qS)在公式中Cy:升力係數Y :升力(升力垂直於氣流速度方向,向上為正)q :動壓,q=ρv*v/2 (ρ為空氣密度,v為氣流相對於物體的流速)S :參考面積(飛機一般選取機翼面積為參考面積)所以機翼的形狀決定空氣流速產生壓強差從而產生升力。

老一輩的人買衣服,除了看面料,還要看裡子,老一輩的司機除了看車質量好不好,還要聽關門聲音厚不厚重,關門聲音好聽,恩~~~好車。

中國的汽車發展說快也不快,但是說慢也不慢了。真正的發展就在近20年。如風起雲涌。以前的車少,能開上車的不是土豪官宦就是司機。所以關於車的一切都是老司機說了算,因為車開得多,說什麼都對,你TM連車都沒碰過居然敢質疑我的觀點?

那麼這些年老司機說過哪些經典段子做過哪些經典事情呢?

1.XX國的車太輕,開起來發飄。

輕飄飄輕飄飄,輕所以飄,聽起來沒毛病啊!但是為什麼這麼簡單的道理這些國際大廠就是不懂呢?

反而各家的鋁製車身越來越多,把車子越造越輕。難道他們都不知道“車子輕了會發飄”?你看開卡車就從不會發飄,就是因為重。

為什麼飛機那麼重照樣可以上天呢?最基本的原理我們都知道,物理課吹紙的實驗誰都做過。空氣流速快的地方壓強小。

一般來說升力:Y= Cy1/2 ρv2s

ρ—空氣密度

s—機翼面積

Cy—升力係數

v—氣流速度

Cy = Y/(qS)

在公式中

Cy:升力係數

Y :升力(升力垂直於氣流速度方向,向上為正)

q :動壓,q=ρv*v/2 (ρ為空氣密度,v為氣流相對於物體的流速)

S :參考面積(飛機一般選取機翼面積為參考面積)

所以機翼的形狀決定空氣流速產生壓強差從而產生升力。

而我們看到汽車的側面和機翼也一樣,會產生上下不一的氣流。到達一定車速的時候也會產生升力。

這就是我們廣義上說的發飄。競速摩托車之所以不需要尾翼增加下壓力就是因為不會產生這兩股相對氣流。

在上述公式當中V和S是變量,而我們說車飄的時候也多是跑高速。所以最後就只有S這一個變量了。怎麼搞?把車頂做平,底盤做凸出來?很顯然不可能。這個時候就有尾翼的出現了。

尾翼的效果就是反過來裝的機翼,簡單粗暴,原理就是在車屁股產生一個下壓力,所以就算跑車很輕只要尾翼能夠產生足夠的下壓力,車也不會發飄。所以就算車再輕只要有相對的下壓力,都不會發飄。但是這樣一來問題又來了。

你想下要是你走路的時候有個人拚命按着你的頭你會走得輕鬆嗎?這個時候下壓力大又影響油耗,所以,怎樣在下壓力和油耗之間達到一個平衡才是汽車廠商考慮的。

和車身重量並沒有直接關係,而車的體重減下來,腰不酸了,油耗低了,腿不疼了,操控好了,一箱油能跑更遠了,油價上漲也不怕了。但是覺得鋁製車身修起來太貴的贊一個。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

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

※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

※台中搬家公司費用怎麼算?