【K8S】基於Docker+K8S+GitLab/SVN+Jenkins+Harbor搭建持續集成交付環境(環境搭建篇)

環境搭建概述

1.K8S是什麼?

K8S全稱是Kubernetes,是一個全新的基於容器技術的分佈式架構領先方案,基於容器技術,目的是實現資源管理的自動化,以及跨多個數據中心的資源利用率的最大化。

如果我們的系統設計遵循了kubernetes的設計思想,那麼傳統系統架構中那些和業務沒有多大關係的底層代碼或功能模塊,都可以使用K8S來管理,我們不必再費心於負載均衡的選型和部署實施問題,不必再考慮引入或自己開發一個複雜的服務治理框架,不必再頭疼與服務監控和故障處理模塊的開發。總之,使用kubernetes提供的解決方案,會大大減少開發成本,同時可以將精力更加集中於業務本身,而且由於kubernetes提供了強大的自動化機制,所以系統後期的運維難度和運維成本大幅降低。

2.為什麼要用K8S?

Docker 這個新興的容器化技術當前已經被很多公司所採用,其從單機走向集群已成必然,而雲計算的蓬勃發展正在加速這一進程。Kubernetes 作為當前唯一被業界廣泛認可和看好的 Docker 分佈式系統解決方案。可以預見,在未來幾年內,會有大量的新系統選擇它,不管是運行在企業本地服務器上還是被託管到公有雲上。

3.使用K8S有哪些好處?

使用Kubernetes就是在全面部署微服務架構。微服務架構的核心就是將一個巨大的單體應用分解為很多小的互相連接的微服務,一個微服務背後可能有多個實例副本在支撐,副本的數量可能會隨着系統的負荷變化而進行調整,內嵌的負載均衡器在 k8s 平台中有多個實例副本在支撐,副本的數量可能會隨着系統的負荷變化而進行調整,內嵌的負載均衡器 在k8s 平台中發揮了重要的作用。微服務架構使得每個服務都可以由專門的開發團隊來開發,開發者可以自由選擇開發技術,這對於大規模團隊來說很有價值。另外,每個微服務獨立開發、升級、擴展,使得系統具備很高的穩定性和快速迭代進化能力。

4.環境構成

整套環境的搭建包含:Docker環境的搭建、docker-compose環境的搭建、K8S集群的搭建、GitLab代碼倉庫的搭建、SVN倉庫的搭建、Jenkins自動化部署環境的搭建、Harbor私有倉庫的搭建。

本文檔中,整套環境的搭建包括:

  • 安裝Docker環境
  • 安裝docker-compose
  • 安裝K8S集群環境
  • 重啟K8S集群引起的問題
  • K8S安裝ingress-nginx
  • K8S安裝gitlab代碼倉庫
  • 安裝Harbor私有倉庫
  • 安裝Jenkins
  • 物理機安裝SVN(推薦)
  • 物理機安裝Jenkins(推薦)
  • 配置Jenkins運行環境
  • Jenkins發布Docker項目到K8S

服務器規劃

IP 主機名 節點 操作系統
192.168.0.10 test10 K8S Master CentOS 8.0.1905
192.168.0.11 test11 K8S Worker CentOS 8.0.1905
192.168.0.12 test12 K8S Worker CentOS 8.0.1905

安裝環境版本

軟件名稱 軟件版本 說明
Docker 19.03.8 提供容器環境
docker-compose 1.25.5 定義和運行由多個容器組成的應用
K8S 1.18.2 是一個開源的,用於管理雲平台中多個主機上的容器化的應用,Kubernetes的目標是讓部署容器化的應用簡單並且高效(powerful),Kubernetes提供了應用部署,規劃,更新,維護的一種機制。
GitLab 12.1.6 代碼倉庫
Harbor 1.10.2 私有鏡像倉庫
Jenkins 2.222.3 持續集成交付

安裝Docker環境

Docker 是一個開源的應用容器引擎,基於 Go 語言 並遵從 Apache2.0 協議開源。

Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,然後發布到任何流行的 Linux 機器上,也可以實現虛擬化。

本文檔基於Docker 19.03.8 版本搭建Docker環境。

在所有服務器上創建install_docker.sh腳本,腳本內容如下所示。

#使用阿里雲鏡像中心
export REGISTRY_MIRROR=https://registry.cn-hangzhou.aliyuncs.com
#安裝yum工具
dnf install yum*
#安裝docker環境
yum install -y yum-utils device-mapper-persistent-data lvm2
#配置Docker的yum源
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
#安裝容器插件
dnf install https://mirrors.aliyun.com/docker-ce/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.13-3.1.el7.x86_64.rpm
#指定安裝docker 19.03.8版本
yum install -y docker-ce-19.03.8 docker-ce-cli-19.03.8
#設置Docker開機啟動
systemctl enable docker.service
#啟動Docker
systemctl start docker.service
#查看Docker版本
docker version

在每台服務器上為install_docker.sh腳本賦予可執行權限,並執行腳本,如下所示。

# 賦予install_docker.sh腳本可執行權限
chmod a+x ./install_docker.sh
# 執行install_docker.sh腳本
./install_docker.sh

安裝docker-compose

Compose 是用於定義和運行多容器 Docker 應用程序的工具。通過 Compose,您可以使用 YML 文件來配置應用程序需要的所有服務。然後,使用一個命令,就可以從 YML 文件配置中創建並啟動所有服務。

注意:在每台服務器上安裝docker-compose

1.下載docker-compose文件

#下載並安裝docker-compose
curl -L https://github.com/docker/compose/releases/download/1.25.5/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 

2.為docker-compose文件賦予可執行權限

#賦予docker-compose可執行權限
chmod a+x /usr/local/bin/docker-compose

3.查看docker-compose版本

#查看docker-compose版本
[root@binghe ~]# docker-compose version
docker-compose version 1.25.5, build 8a1c60f6
docker-py version: 4.1.0
CPython version: 3.7.5
OpenSSL version: OpenSSL 1.1.0l  10 Sep 2019

安裝K8S集群環境

Kubernetes是一個開源的,用於管理雲平台中多個主機上的容器化的應用,Kubernetes的目標是讓部署容器化的應用簡單並且高效(powerful),Kubernetes提供了應用部署,規劃,更新,維護的一種機制。

本文檔基於K8S 1.8.12版本來搭建K8S集群

安裝K8S基礎環境

在所有服務器上創建install_k8s.sh腳本文件,腳本文件的內容如下所示。

#################配置阿里雲鏡像加速器開始########################
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://zz3sblpi.mirror.aliyuncs.com"]
}
EOF
systemctl daemon-reload
systemctl restart docker
######################配置阿里雲鏡像加速器結束#########################
#安裝nfs-utils
yum install -y nfs-utils
#安裝wget軟件下載命令
yum install -y wget

#啟動nfs-server
systemctl start nfs-server
#配置nfs-server開機自啟動
systemctl enable nfs-server

#關閉防火牆
systemctl stop firewalld
#取消防火牆開機自啟動
systemctl disable firewalld

#關閉SeLinux
setenforce 0
sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config

# 關閉 swap
swapoff -a
yes | cp /etc/fstab /etc/fstab_bak
cat /etc/fstab_bak |grep -v swap > /etc/fstab

############################修改 /etc/sysctl.conf開始###########################
# 如果有配置,則修改
sed -i "s#^net.ipv4.ip_forward.*#net.ipv4.ip_forward=1#g"  /etc/sysctl.conf
sed -i "s#^net.bridge.bridge-nf-call-ip6tables.*#net.bridge.bridge-nf-call-ip6tables=1#g"  /etc/sysctl.conf
sed -i "s#^net.bridge.bridge-nf-call-iptables.*#net.bridge.bridge-nf-call-iptables=1#g"  /etc/sysctl.conf
sed -i "s#^net.ipv6.conf.all.disable_ipv6.*#net.ipv6.conf.all.disable_ipv6=1#g"  /etc/sysctl.conf
sed -i "s#^net.ipv6.conf.default.disable_ipv6.*#net.ipv6.conf.default.disable_ipv6=1#g"  /etc/sysctl.conf
sed -i "s#^net.ipv6.conf.lo.disable_ipv6.*#net.ipv6.conf.lo.disable_ipv6=1#g"  /etc/sysctl.conf
sed -i "s#^net.ipv6.conf.all.forwarding.*#net.ipv6.conf.all.forwarding=1#g"  /etc/sysctl.conf
# 可能沒有,追加
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.conf
echo "net.bridge.bridge-nf-call-iptables = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.default.disable_ipv6 = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.lo.disable_ipv6 = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.all.forwarding = 1"  >> /etc/sysctl.conf
############################修改 /etc/sysctl.conf結束###########################
# 執行命令使修改后的/etc/sysctl.conf文件生效
sysctl -p

################# 配置K8S的yum源開始#############################
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
repo_gpgcheck=0
gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg
       http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF
################# 配置K8S的yum源結束#############################

# 卸載舊版本K8S
yum remove -y kubelet kubeadm kubectl

# 安裝kubelet、kubeadm、kubectl,這裏我安裝的是1.18.2版本,你也可以安裝1.17.2版本
yum install -y kubelet-1.18.2 kubeadm-1.18.2 kubectl-1.18.2

# 修改docker Cgroup Driver為systemd
# # 將/usr/lib/systemd/system/docker.service文件中的這一行 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
# # 修改為 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd
# 如果不修改,在添加 worker 節點時可能會碰到如下錯誤
# [WARNING IsDockerSystemdCheck]: detected "cgroupfs" as the Docker cgroup driver. The recommended driver is "systemd". 
# Please follow the guide at https://kubernetes.io/docs/setup/cri/
sed -i "s#^ExecStart=/usr/bin/dockerd.*#ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd#g" /usr/lib/systemd/system/docker.service

# 設置 docker 鏡像,提高 docker 鏡像下載速度和穩定性
# 如果訪問 https://hub.docker.io 速度非常穩定,也可以跳過這個步驟,一般不需配置
# curl -sSL https://kuboard.cn/install-script/set_mirror.sh | sh -s ${REGISTRY_MIRROR}

# 重新加載配置文件
systemctl daemon-reload
#重啟 docker
systemctl restart docker
# 將kubelet設置為開機啟動並啟動kubelet
systemctl enable kubelet && systemctl start kubelet
# 查看docker版本
docker version

在每台服務器上為install_k8s.sh腳本賦予可執行權限,並執行腳本

# 賦予install_k8s.sh腳本可執行權限
chmod a+x ./install_k8s.sh
# 運行install_k8s.sh腳本
./install_k8s.sh

初始化Master節點

只在test10服務器上執行的操作。

1.初始化Master節點的網絡環境

注意:下面的命令需要在命令行手動執行。

# 只在 master 節點執行
# export 命令只在當前 shell 會話中有效,開啟新的 shell 窗口后,如果要繼續安裝過程,請重新執行此處的 export 命令
export MASTER_IP=192.168.0.10
# 替換 k8s.master 為 您想要的 dnsName
export APISERVER_NAME=k8s.master
# Kubernetes 容器組所在的網段,該網段安裝完成后,由 kubernetes 創建,事先並不存在於物理網絡中
export POD_SUBNET=172.18.0.1/16
echo "${MASTER_IP}    ${APISERVER_NAME}" >> /etc/hosts

2.初始化Master節點

在test10服務器上創建init_master.sh腳本文件,文件內容如下所示。

#!/bin/bash
# 腳本出錯時終止執行
set -e

if [ ${#POD_SUBNET} -eq 0 ] || [ ${#APISERVER_NAME} -eq 0 ]; then
  echo -e "\033[31;1m請確保您已經設置了環境變量 POD_SUBNET 和 APISERVER_NAME \033[0m"
  echo 當前POD_SUBNET=$POD_SUBNET
  echo 當前APISERVER_NAME=$APISERVER_NAME
  exit 1
fi


# 查看完整配置選項 https://godoc.org/k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2
rm -f ./kubeadm-config.yaml
cat <<EOF > ./kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
kubernetesVersion: v1.18.2
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
controlPlaneEndpoint: "${APISERVER_NAME}:6443"
networking:
  serviceSubnet: "10.96.0.0/16"
  podSubnet: "${POD_SUBNET}"
  dnsDomain: "cluster.local"
EOF

# kubeadm init
# 初始化kebeadm
kubeadm init --config=kubeadm-config.yaml --upload-certs

# 配置 kubectl
rm -rf /root/.kube/
mkdir /root/.kube/
cp -i /etc/kubernetes/admin.conf /root/.kube/config

# 安裝 calico 網絡插件
# 參考文檔 https://docs.projectcalico.org/v3.13/getting-started/kubernetes/self-managed-onprem/onpremises
echo "安裝calico-3.13.1"
rm -f calico-3.13.1.yaml
wget https://kuboard.cn/install-script/calico/calico-3.13.1.yaml
kubectl apply -f calico-3.13.1.yaml

賦予init_master.sh腳本文件可執行權限並執行腳本。

# 賦予init_master.sh文件可執行權限
chmod a+x ./init_master.sh
# 運行init_master.sh腳本
./init_master.sh

3.查看Master節點的初始化結果

(1)確保所有容器組處於Running狀態

# 執行如下命令,等待 3-10 分鐘,直到所有的容器組處於 Running 狀態
watch kubectl get pod -n kube-system -o wide

具體執行如下所示。

[root@test10 ~]# watch kubectl get pod -n kube-system -o wide
Every 2.0s: kubectl get pod -n kube-system -o wide                                                                                                                          test10: Sun May 10 11:01:32 2020

NAME                                       READY   STATUS    RESTARTS   AGE    IP                NODE        NOMINATED NODE   READINESS GATES          
calico-kube-controllers-5b8b769fcd-5dtlp   1/1     Running   0          118s   172.18.203.66     test10   <none>           <none>          
calico-node-fnv8g                          1/1     Running   0          118s   192.168.0.10   test10   <none>           <none>          
coredns-546565776c-27t7h                   1/1     Running   0          2m1s   172.18.203.67     test10   <none>           <none>          
coredns-546565776c-hjb8z                   1/1     Running   0          2m1s   172.18.203.65     test10   <none>           <none>          
etcd-test10                             1/1     Running   0          2m7s   192.168.0.10   test10   <none>           <none>          
kube-apiserver-test10                   1/1     Running   0          2m7s   192.168.0.10   test10   <none>           <none>          
kube-controller-manager-test10          1/1     Running   0          2m7s   192.168.0.10   test10   <none>           <none>          
kube-proxy-dvgsr                           1/1     Running   0          2m1s   192.168.0.10   test10   <none>           <none>          
kube-scheduler-test10                   1/1     Running   0          2m7s   192.168.0.10   test10   <none>           <none>

(2) 查看 Master 節點初始化結果

# 查看Master節點的初始化結果
kubectl get nodes -o wide

具體執行如下所示。

[root@test10 ~]# kubectl get nodes -o wide
NAME        STATUS   ROLES    AGE     VERSION   INTERNAL-IP       EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION         CONTAINER-RUNTIME
test10   Ready    master   3m28s   v1.18.2   192.168.0.10   <none>        CentOS Linux 8 (Core)   4.18.0-80.el8.x86_64   docker://19.3.8

初始化Worker節點

1.獲取join命令參數

在Master節點(test10服務器)上執行如下命令獲取join命令參數。

kubeadm token create --print-join-command

具體執行如下所示。

[root@test10 ~]# kubeadm token create --print-join-command
W0510 11:04:34.828126   56132 configset.go:202] WARNING: kubeadm cannot validate component configs for API groups [kubelet.config.k8s.io kubeproxy.config.k8s.io]
kubeadm join k8s.master:6443 --token 8nblts.62xytoqufwsqzko2     --discovery-token-ca-cert-hash sha256:1717cc3e34f6a56b642b5751796530e367aa73f4113d09994ac3455e33047c0d 

其中,有如下一行輸出。

kubeadm join k8s.master:6443 --token 8nblts.62xytoqufwsqzko2     --discovery-token-ca-cert-hash sha256:1717cc3e34f6a56b642b5751796530e367aa73f4113d09994ac3455e33047c0d 

這行代碼就是獲取到的join命令。

注意:join命令中的token的有效時間為 2 個小時,2小時內,可以使用此 token 初始化任意數量的 worker 節點。

2.初始化Worker節點

針對所有的 worker 節點執行,在這裏,就是在test11服務器和test12服務器上執行。

在命令分別手動執行如下命令。

# 只在 worker 節點執行
# 192.168.0.10 為 master 節點的內網 IP
export MASTER_IP=192.168.0.10
# 替換 k8s.master 為初始化 master 節點時所使用的 APISERVER_NAME
export APISERVER_NAME=k8s.master
echo "${MASTER_IP}    ${APISERVER_NAME}" >> /etc/hosts

# 替換為 master 節點上 kubeadm token create 命令輸出的join
kubeadm join k8s.master:6443 --token 8nblts.62xytoqufwsqzko2     --discovery-token-ca-cert-hash sha256:1717cc3e34f6a56b642b5751796530e367aa73f4113d09994ac3455e33047c0d 

具體執行如下所示。

[root@test11 ~]# export MASTER_IP=192.168.0.10
[root@test11 ~]# export APISERVER_NAME=k8s.master
[root@test11 ~]# echo "${MASTER_IP}    ${APISERVER_NAME}" >> /etc/hosts
[root@test11 ~]# kubeadm join k8s.master:6443 --token 8nblts.62xytoqufwsqzko2     --discovery-token-ca-cert-hash sha256:1717cc3e34f6a56b642b5751796530e367aa73f4113d09994ac3455e33047c0d 
W0510 11:08:27.709263   42795 join.go:346] [preflight] WARNING: JoinControlPane.controlPlane settings will be ignored when control-plane flag is not set.
[preflight] Running pre-flight checks
        [WARNING FileExisting-tc]: tc not found in system path
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.18" ConfigMap in the kube-system namespace
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

根據輸出結果可以看出,Worker節點加入了K8S集群。

注意:kubeadm join…就是master 節點上 kubeadm token create 命令輸出的join。

3.查看初始化結果

在Master節點(test10服務器)執行如下命令查看初始化結果。

kubectl get nodes -o wide

具體執行如下所示。

[root@test10 ~]# kubectl get nodes
NAME        STATUS   ROLES    AGE     VERSION
test10   Ready    master   20m     v1.18.2
test11   Ready    <none>   2m46s   v1.18.2
test12   Ready    <none>   2m46s   v1.18.2

注意:kubectl get nodes命令後面加上-o wide參數可以輸出更多的信息。

重啟K8S集群引起的問題

1.Worker節點故障不能啟動

Master 節點的 IP 地址發生變化,導致 worker 節點不能啟動。需要重新安裝K8S集群,並確保所有節點都有固定的內網 IP 地址。

2.Pod崩潰或不能正常訪問

重啟服務器后使用如下命令查看Pod的運行狀態。

#查看所有pod的運行情況
kubectl get pods --all-namespaces

發現很多 Pod 不在 Running 狀態,此時,需要使用如下命令刪除運行不正常的Pod。

kubectl delete pod <pod-name> -n <pod-namespece>

注意:如果Pod 是使用 Deployment、StatefulSet 等控制器創建的,K8S 將創建新的 Pod 作為替代,重新啟動的 Pod 通常能夠正常工作。

其中,pod-name表示運行在K8S中的pod的名稱,pod-namespece表示命名空間。例如,需要刪除pod名稱為pod-test,命名空間為pod-test-namespace的pod,可以使用下面的命令。

kubectl delete pod pod-test -n pod-test-namespace

K8S安裝ingress-nginx

作為反向代理將外部流量導入集群內部,將 Kubernetes 內部的 Service 暴露給外部,在 Ingress 對象中通過域名匹配 Service,這樣就可以直接通過域名訪問到集群內部的服務了。相對於 traefik 來說,nginx-ingress 性能更加優秀。

注意:在Master節點(test10服務器上執行)

1.創建ingress-nginx命名空間

創建ingress-nginx-namespace.yaml文件,主要的作用是創建ingress-nginx命名空間,文件內容如下所示。

apiVersion: v1
kind: Namespace
metadata:
  name: ingress-nginx
  labels:
    name: ingress-nginx

執行如下命令創建ingress-nginx命名空間。

kubectl apply -f ingress-nginx-namespace.yaml

2.安裝ingress controller

創建ingress-nginx-mandatory.yaml文件,主要的作用是安裝ingress-nginx。文件內容如下所示。

apiVersion: v1
kind: Namespace
metadata:
  name: ingress-nginx

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: default-http-backend
  labels:
    app.kubernetes.io/name: default-http-backend
    app.kubernetes.io/part-of: ingress-nginx
  namespace: ingress-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: default-http-backend
      app.kubernetes.io/part-of: ingress-nginx
  template:
    metadata:
      labels:
        app.kubernetes.io/name: default-http-backend
        app.kubernetes.io/part-of: ingress-nginx
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: default-http-backend
          # Any image is permissible as long as:
          # 1. It serves a 404 page at /
          # 2. It serves 200 on a /healthz endpoint
          image: registry.cn-qingdao.aliyuncs.com/kubernetes_xingej/defaultbackend-amd64:1.5
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 30
            timeoutSeconds: 5
          ports:
            - containerPort: 8080
          resources:
            limits:
              cpu: 10m
              memory: 20Mi
            requests:
              cpu: 10m
              memory: 20Mi

---
apiVersion: v1
kind: Service
metadata:
  name: default-http-backend
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: default-http-backend
    app.kubernetes.io/part-of: ingress-nginx
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app.kubernetes.io/name: default-http-backend
    app.kubernetes.io/part-of: ingress-nginx

---

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-configuration
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

---

kind: ConfigMap
apiVersion: v1
metadata:
  name: tcp-services
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

---

kind: ConfigMap
apiVersion: v1
metadata:
  name: udp-services
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

---

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccount
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: nginx-ingress-clusterrole
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - endpoints
      - nodes
      - pods
      - secrets
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - services
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - "extensions"
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - events
    verbs:
      - create
      - patch
  - apiGroups:
      - "extensions"
    resources:
      - ingresses/status
    verbs:
      - update

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: nginx-ingress-role
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - pods
      - secrets
      - namespaces
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - configmaps
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-nginx"
    verbs:
      - get
      - update
  - apiGroups:
      - ""
    resources:
      - configmaps
    verbs:
      - create
  - apiGroups:
      - ""
    resources:
      - endpoints
    verbs:
      - get

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: nginx-ingress-role-nisa-binding
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nginx-ingress-role
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: ingress-nginx

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: nginx-ingress-clusterrole-nisa-binding
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nginx-ingress-clusterrole
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: ingress-nginx

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
      app.kubernetes.io/part-of: ingress-nginx
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/part-of: ingress-nginx
      annotations:
        prometheus.io/port: "10254"
        prometheus.io/scrape: "true"
    spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller
          image: registry.cn-qingdao.aliyuncs.com/kubernetes_xingej/nginx-ingress-controller:0.20.0
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
            - --configmap=$(POD_NAMESPACE)/nginx-configuration
            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx
            - --annotations-prefix=nginx.ingress.kubernetes.io
          securityContext:
            capabilities:
              drop:
                - ALL
              add:
                - NET_BIND_SERVICE
            # www-data -> 33
            runAsUser: 33
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
            - name: https
              containerPort: 443
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            initialDelaySeconds: 10
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1

---

執行如下命令安裝ingress controller。

kubectl apply -f ingress-nginx-mandatory.yaml

3.安裝K8S SVC:ingress-nginx

主要是用來用於暴露pod:nginx-ingress-controller。

創建service-nodeport.yaml文件,文件內容如下所示。

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
      nodePort: 30080
    - name: https
      port: 443
      targetPort: 443
      protocol: TCP
      nodePort: 30443
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

執行如下命令安裝。

kubectl apply -f service-nodeport.yaml

4.訪問K8S SVC:ingress-nginx

查看ingress-nginx命名空間的部署情況,如下所示。

[root@test10 k8s]# kubectl get pod -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
default-http-backend-796ddcd9b-vfmgn        1/1     Running   1          10h
nginx-ingress-controller-58985cc996-87754   1/1     Running   2          10h

在命令行服務器命令行輸入如下命令查看ingress-nginx的端口映射情況。

kubectl get svc -n ingress-nginx 

具體如下所示。

[root@test10 k8s]# kubectl get svc -n ingress-nginx 
NAME                   TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE
default-http-backend   ClusterIP   10.96.247.2   <none>        80/TCP                       7m3s
ingress-nginx          NodePort    10.96.40.6    <none>        80:30080/TCP,443:30443/TCP   4m35s

所以,可以通過Master節點(test10服務器)的IP地址和30080端口號來訪問ingress-nginx,如下所示。

[root@test10 k8s]# curl 192.168.0.10:30080       
default backend - 404

也可以在瀏覽器打開http://192.168.0.10:30080 來訪問ingress-nginx,如下所示。

K8S安裝gitlab代碼倉庫

GitLab是由GitLabInc.開發,使用MIT許可證的基於網絡的Git倉庫管理工具,且具有Wiki和issue跟蹤功能。使用Git作為代碼管理工具,並在此基礎上搭建起來的web服務。

注意:在Master節點(test10服務器上執行)

1.創建k8s-ops命名空間

創建k8s-ops-namespace.yaml文件,主要作用是創建k8s-ops命名空間。文件內容如下所示。

apiVersion: v1
kind: Namespace
metadata:
  name: k8s-ops
  labels:
    name: k8s-ops

執行如下命令創建命名空間。

kubectl apply -f k8s-ops-namespace.yaml 

2.安裝gitlab-redis

創建gitlab-redis.yaml文件,文件的內容如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: k8s-ops
  labels:
    name: redis
spec:
  selector:
    matchLabels:
      name: redis
  template:
    metadata:
      name: redis
      labels:
        name: redis
    spec:
      containers:
      - name: redis
        image: sameersbn/redis
        imagePullPolicy: IfNotPresent
        ports:
        - name: redis
          containerPort: 6379
        volumeMounts:
        - mountPath: /var/lib/redis
          name: data
        livenessProbe:
          exec:
            command:
            - redis-cli
            - ping
          initialDelaySeconds: 30
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - redis-cli
            - ping
          initialDelaySeconds: 10
          timeoutSeconds: 5
      volumes:
      - name: data
        hostPath:
          path: /data1/docker/xinsrv/redis

---
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: k8s-ops
  labels:
    name: redis
spec:
  ports:
    - name: redis
      port: 6379
      targetPort: redis
  selector:
    name: redis

首先,在命令行執行如下命令創建/data1/docker/xinsrv/redis目錄。

mkdir -p /data1/docker/xinsrv/redis

執行如下命令安裝gitlab-redis。

kubectl apply -f gitlab-redis.yaml 

3.安裝gitlab-postgresql

創建gitlab-postgresql.yaml,文件內容如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgresql
  namespace: k8s-ops
  labels:
    name: postgresql
spec:
  selector:
    matchLabels:
      name: postgresql
  template:
    metadata:
      name: postgresql
      labels:
        name: postgresql
    spec:
      containers:
      - name: postgresql
        image: sameersbn/postgresql
        imagePullPolicy: IfNotPresent
        env:
        - name: DB_USER
          value: gitlab
        - name: DB_PASS
          value: passw0rd
        - name: DB_NAME
          value: gitlab_production
        - name: DB_EXTENSION
          value: pg_trgm
        ports:
        - name: postgres
          containerPort: 5432
        volumeMounts:
        - mountPath: /var/lib/postgresql
          name: data
        livenessProbe:
          exec:
            command:
            - pg_isready
            - -h
            - localhost
            - -U
            - postgres
          initialDelaySeconds: 30
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - pg_isready
            - -h
            - localhost
            - -U
            - postgres
          initialDelaySeconds: 5
          timeoutSeconds: 1
      volumes:
      - name: data
        hostPath:
          path: /data1/docker/xinsrv/postgresql
---
apiVersion: v1
kind: Service
metadata:
  name: postgresql
  namespace: k8s-ops
  labels:
    name: postgresql
spec:
  ports:
    - name: postgres
      port: 5432
      targetPort: postgres
  selector:
    name: postgresql

首先,執行如下命令創建/data1/docker/xinsrv/postgresql目錄。

mkdir -p /data1/docker/xinsrv/postgresql

接下來,安裝gitlab-postgresql,如下所示。

kubectl apply -f gitlab-postgresql.yaml

4.安裝gitlab

(1)配置用戶名和密碼

首先,在命令行使用base64編碼為用戶名和密碼進行轉碼,本示例中,使用的用戶名為admin,密碼為admin.1231

轉碼情況如下所示。

[root@test10 k8s]# echo -n 'admin' | base64 
YWRtaW4=
[root@test10 k8s]# echo -n 'admin.1231' | base64 
YWRtaW4uMTIzMQ==

轉碼后的用戶名為:YWRtaW4= 密碼為:YWRtaW4uMTIzMQ==

也可以對base64編碼后的字符串解碼,例如,對密碼字符串解碼,如下所示。

[root@test10 k8s]# echo 'YWRtaW4uMTIzMQ==' | base64 --decode 
admin.1231

接下來,創建secret-gitlab.yaml文件,主要是用戶來配置GitLab的用戶名和密碼,文件內容如下所示。

apiVersion: v1
kind: Secret
metadata:
  namespace: k8s-ops
  name: git-user-pass
type: Opaque
data:
  username: YWRtaW4=
  password: YWRtaW4uMTIzMQ==

執行配置文件的內容,如下所示。

kubectl create -f ./secret-gitlab.yaml

(2)安裝GitLab

創建gitlab.yaml文件,文件的內容如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gitlab
  namespace: k8s-ops
  labels:
    name: gitlab
spec:
  selector:
    matchLabels:
      name: gitlab
  template:
    metadata:
      name: gitlab
      labels:
        name: gitlab
    spec:
      containers:
      - name: gitlab
        image: sameersbn/gitlab:12.1.6
        imagePullPolicy: IfNotPresent
        env:
        - name: TZ
          value: Asia/Shanghai
        - name: GITLAB_TIMEZONE
          value: Beijing
        - name: GITLAB_SECRETS_DB_KEY_BASE
          value: long-and-random-alpha-numeric-string
        - name: GITLAB_SECRETS_SECRET_KEY_BASE
          value: long-and-random-alpha-numeric-string
        - name: GITLAB_SECRETS_OTP_KEY_BASE
          value: long-and-random-alpha-numeric-string
        - name: GITLAB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: git-user-pass
              key: password
        - name: GITLAB_ROOT_EMAIL
          value: 12345678@qq.com
        - name: GITLAB_HOST
          value: gitlab.binghe.com
        - name: GITLAB_PORT
          value: "80"
        - name: GITLAB_SSH_PORT
          value: "30022"
        - name: GITLAB_NOTIFY_ON_BROKEN_BUILDS
          value: "true"
        - name: GITLAB_NOTIFY_PUSHER
          value: "false"
        - name: GITLAB_BACKUP_SCHEDULE
          value: daily
        - name: GITLAB_BACKUP_TIME
          value: 01:00
        - name: DB_TYPE
          value: postgres
        - name: DB_HOST
          value: postgresql
        - name: DB_PORT
          value: "5432"
        - name: DB_USER
          value: gitlab
        - name: DB_PASS
          value: passw0rd
        - name: DB_NAME
          value: gitlab_production
        - name: REDIS_HOST
          value: redis
        - name: REDIS_PORT
          value: "6379"
        ports:
        - name: http
          containerPort: 80
        - name: ssh
          containerPort: 22
        volumeMounts:
        - mountPath: /home/git/data
          name: data
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 180
          timeoutSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5
          timeoutSeconds: 1
      volumes:
      - name: data
        hostPath:
          path: /data1/docker/xinsrv/gitlab
---
apiVersion: v1
kind: Service
metadata:
  name: gitlab
  namespace: k8s-ops
  labels:
    name: gitlab
spec:
  ports:
    - name: http
      port: 80
      nodePort: 30088
    - name: ssh
      port: 22
      targetPort: ssh
      nodePort: 30022
  type: NodePort
  selector:
    name: gitlab

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gitlab
  namespace: k8s-ops
  annotations:
    kubernetes.io/ingress.class: traefik
spec:
  rules:
  - host: gitlab.binghe.com
    http:
      paths:
      - backend:
          serviceName: gitlab
          servicePort: http

注意:在配置GitLab時,監聽主機時,不能使用IP地址,需要使用主機名或者域名,上述配置中,我使用的是gitlab.binghe.com主機名。

在命令行執行如下命令創建/data1/docker/xinsrv/gitlab目錄。

mkdir -p /data1/docker/xinsrv/gitlab

安裝GitLab,如下所示。

kubectl apply -f gitlab.yaml

5.安裝完成

查看k8s-ops命名空間部署情況,如下所示。

[root@test10 k8s]# kubectl get pod -n k8s-ops
NAME                          READY   STATUS    RESTARTS   AGE
gitlab-7b459db47c-5vk6t       0/1     Running   0          11s
postgresql-79567459d7-x52vx   1/1     Running   0          30m
redis-67f4cdc96c-h5ckz        1/1     Running   1          10h

也可以使用如下命令查看。

[root@test10 k8s]# kubectl get pod --namespace=k8s-ops
NAME                          READY   STATUS    RESTARTS   AGE
gitlab-7b459db47c-5vk6t       0/1     Running   0          36s
postgresql-79567459d7-x52vx   1/1     Running   0          30m
redis-67f4cdc96c-h5ckz        1/1     Running   1          10h

二者效果一樣。

接下來,查看GitLab的端口映射,如下所示。

[root@test10 k8s]# kubectl get svc -n k8s-ops
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                     AGE
gitlab       NodePort    10.96.153.100   <none>        80:30088/TCP,22:30022/TCP   2m42s
postgresql   ClusterIP   10.96.203.119   <none>        5432/TCP                    32m
redis        ClusterIP   10.96.107.150   <none>        6379/TCP                    10h

此時,可以看到,可以通過Master節點(test10)的主機名gitlab.binghe.com和端口30088就能夠訪問GitLab。由於我這裏使用的是虛擬機來搭建相關的環境,在本機訪問虛擬機映射的gitlab.binghe.com時,需要配置本機的hosts文件,在本機的hosts文件中加入如下配置項。

192.168.0.10 gitlab.binghe.com

注意:在Windows操作系統中,hosts文件所在的目錄如下。

C:\Windows\System32\drivers\etc

接下來,就可以在瀏覽器中通過鏈接:http://gitlab.binghe.com:30088 來訪問GitLab了,如下所示。

此時,可以通過用戶名root和密碼admin.1231來登錄GitLab了。

注意:這裏的用戶名是root而不是admin,因為root是GitLab默認的超級用戶。

到此,K8S安裝gitlab完成。

安裝Harbor私有倉庫

Habor是由VMWare公司開源的容器鏡像倉庫。事實上,Habor是在Docker Registry上進行了相應的企業級擴展,從而獲得了更加廣泛的應用,這些新的企業級特性包括:管理用戶界面,基於角色的訪問控制 ,AD/LDAP集成以及審計日誌等,足以滿足基本企業需求。

注意:這裏將Harbor私有倉庫安裝在Master節點(test10服務器)上,實際生產環境中建議安裝在其他服務器。

1.下載Harbor的離線安裝版本

wget https://github.com/goharbor/harbor/releases/download/v1.10.2/harbor-offline-installer-v1.10.2.tgz

2.解壓Harbor的安裝包

tar -zxvf harbor-offline-installer-v1.10.2.tgz

解壓成功后,會在服務器當前目錄生成一個harbor目錄。

3.配置Harbor

注意:這裏,我將Harbor的端口修改成了1180,如果不修改Harbor的端口,默認的端口是80。

(1)修改harbor.yml文件

cd harbor
vim harbor.yml

修改的配置項如下所示。

hostname: 192.168.0.10
http:
  port: 1180
harbor_admin_password: binghe123
###並把https註釋掉,不然在安裝的時候會報錯:ERROR:root:Error: The protocol is https but attribute ssl_cert is not set
#https:
  #port: 443
  #certificate: /your/certificate/path
  #private_key: /your/private/key/path

(2)修改daemon.json文件

修改/etc/docker/daemon.json文件,沒有的話就創建,在/etc/docker/daemon.json文件中添加如下內容。

[root@binghe~]# cat /etc/docker/daemon.json
{
  "registry-mirrors": ["https://zz3sblpi.mirror.aliyuncs.com"],
  "insecure-registries":["192.168.0.10:1180"]
}

也可以在服務器上使用 ip addr 命令查看本機所有的IP地址段,將其配置到/etc/docker/daemon.json文件中。這裏,我配置后的文件內容如下所示。

{
    "registry-mirrors": ["https://zz3sblpi.mirror.aliyuncs.com"],
    "insecure-registries":["192.168.175.0/16","172.17.0.0/16", "172.18.0.0/16", "172.16.29.0/16", "192.168.0.10:1180"]
}

4.安裝並啟動harbor

配置完成后,輸入如下命令即可安裝並啟動Harbor

[root@binghe harbor]# ./install.sh 

5.登錄Harbor並添加賬戶

安裝成功后,在瀏覽器地址欄輸入http://192.168.0.10:1180打開鏈接,輸入用戶名admin和密碼binghe123,登錄系統。

接下來,我們選擇用戶管理,添加一個管理員賬戶,為後續打包Docker鏡像和上傳Docker鏡像做準備。

密碼為Binghe123。點擊確,此時,賬戶binghe還不是管理員,此時選中binghe賬戶,點擊“設置為管理員”。

此時,binghe賬戶就被設置為管理員了。到此,Harbor的安裝就完成了。

6.修改Harbor端口

如果安裝Harbor后,大家需要修改Harbor的端口,可以按照如下步驟修改Harbor的端口,這裏,我以將80端口修改為1180端口為例

(1)修改harbor.yml文件

cd harbor
vim harbor.yml

修改的配置項如下所示。

hostname: 192.168.0.10
http:
  port: 1180
harbor_admin_password: binghe123
###並把https註釋掉,不然在安裝的時候會報錯:ERROR:root:Error: The protocol is https but attribute ssl_cert is not set
#https:
  #port: 443
  #certificate: /your/certificate/path
  #private_key: /your/private/key/path

(2)修改docker-compose.yml文件

vim docker-compose.yml

修改的配置項如下所示。

ports:
      - 1180:80

(3)修改config.yml文件

cd common/config/registry
vim config.yml

修改的配置項如下所示。

realm: http://192.168.0.10:1180/service/token

(4)重啟Docker

systemctl daemon-reload
systemctl restart docker.service

(5)重啟Harbor

[root@binghe harbor]# docker-compose down
Stopping harbor-log ... done
Removing nginx             ... done
Removing harbor-portal     ... done
Removing harbor-jobservice ... done
Removing harbor-core       ... done
Removing redis             ... done
Removing registry          ... done
Removing registryctl       ... done
Removing harbor-db         ... done
Removing harbor-log        ... done
Removing network harbor_harbor
 
[root@binghe harbor]# ./prepare
prepare base dir is set to /mnt/harbor
Clearing the configuration file: /config/log/logrotate.conf
Clearing the configuration file: /config/nginx/nginx.conf
Clearing the configuration file: /config/core/env
Clearing the configuration file: /config/core/app.conf
Clearing the configuration file: /config/registry/root.crt
Clearing the configuration file: /config/registry/config.yml
Clearing the configuration file: /config/registryctl/env
Clearing the configuration file: /config/registryctl/config.yml
Clearing the configuration file: /config/db/env
Clearing the configuration file: /config/jobservice/env
Clearing the configuration file: /config/jobservice/config.yml
Generated configuration file: /config/log/logrotate.conf
Generated configuration file: /config/nginx/nginx.conf
Generated configuration file: /config/core/env
Generated configuration file: /config/core/app.conf
Generated configuration file: /config/registry/config.yml
Generated configuration file: /config/registryctl/env
Generated configuration file: /config/db/env
Generated configuration file: /config/jobservice/env
Generated configuration file: /config/jobservice/config.yml
loaded secret from file: /secret/keys/secretkey
Generated configuration file: /compose_location/docker-compose.yml
Clean up the input dir
 
[root@binghe harbor]# docker-compose up -d
Creating network "harbor_harbor" with the default driver
Creating harbor-log ... done
Creating harbor-db   ... done
Creating redis       ... done
Creating registry    ... done
Creating registryctl ... done
Creating harbor-core ... done
Creating harbor-jobservice ... done
Creating harbor-portal     ... done
Creating nginx             ... done
 
[root@binghe harbor]# docker ps -a
CONTAINER ID        IMAGE                                               COMMAND                  CREATED             STATUS                             PORTS

安裝Jenkins(一般的做法)

Jenkins是一個開源的、提供友好操作界面的持續集成(CI)工具,起源於Hudson(Hudson是商用的),主要用於持續、自動的構建/測試軟件項目、監控外部任務的運行(這個比較抽象,暫且寫上,不做解釋)。Jenkins用Java語言編寫,可在Tomcat等流行的servlet容器中運行,也可獨立運行。通常與版本管理工具(SCM)、構建工具結合使用。常用的版本控制工具有SVN、GIT,構建工具有Maven、Ant、Gradle。

1.安裝nfs(之前安裝過的話,可以省略此步)

使用 nfs 最大的問題就是寫權限,可以使用 kubernetes 的 securityContext/runAsUser 指定 jenkins 容器中運行 jenkins 的用戶 uid,以此來指定 nfs 目錄的權限,讓 jenkins 容器可寫;也可以不限制,讓所有用戶都可以寫。這裏為了簡單,就讓所有用戶可寫了。

如果之前已經安裝過nfs,則這一步可以省略。找一台主機,安裝 nfs,這裏,我以在Master節點(test10服務器)上安裝nfs為例。

在命令行輸入如下命令安裝並啟動nfs。

yum install nfs-utils -y
systemctl start nfs-server
systemctl enable nfs-server

2.創建nfs共享目錄

在Master節點(test10服務器)上創建 /opt/nfs/jenkins-data目錄作為nfs的共享目錄,如下所示。

mkdir -p /opt/nfs/jenkins-data

接下來,編輯/etc/exports文件,如下所示。

vim /etc/exports

在/etc/exports文件文件中添加如下一行配置。

/opt/nfs/jenkins-data 192.168.175.0/24(rw,all_squash)

這裏的 ip 使用 kubernetes node 節點的 ip 範圍,後面的 all_squash 選項會將所有訪問的用戶都映射成 nfsnobody 用戶,不管你是什麼用戶訪問,最終都會壓縮成 nfsnobody,所以只要將 /opt/nfs/jenkins-data 的屬主改為 nfsnobody,那麼無論什麼用戶來訪問都具有寫權限。

這個選項在很多機器上由於用戶 uid 不規範導致啟動進程的用戶不同,但是同時要對一個共享目錄具有寫權限時很有效。

接下來,為 /opt/nfs/jenkins-data目錄授權,並重新加載nfs,如下所示。

#為/opt/nfs/jenkins-data/目錄授權
chown -R 1000 /opt/nfs/jenkins-data/
#重新加載nfs-server
systemctl reload nfs-server

在K8S集群中任意一個節點上使用如下命令進行驗證:

#查看nfs系統的目錄權限
showmount -e NFS_IP

如果能夠看到 /opt/nfs/jenkins-data 就表示 ok 了。

具體如下所示。

[root@test10 ~]# showmount -e 192.168.0.10
Export list for 192.168.0.10:
/opt/nfs/jenkins-data 192.168.175.0/24

[root@test11 ~]# showmount -e 192.168.0.10
Export list for 192.168.0.10:
/opt/nfs/jenkins-data 192.168.175.0/24

3.創建PV

Jenkins 其實只要加載對應的目錄就可以讀取之前的數據,但是由於 deployment 無法定義存儲卷,因此我們只能使用 StatefulSet。

首先創建 pv,pv 是給 StatefulSet 使用的,每次 StatefulSet 啟動都會通過 volumeClaimTemplates 這個模板去創建 pvc,因此必須得有 pv,才能供 pvc 綁定。

創建jenkins-pv.yaml文件,文件內容如下所示。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins
spec:
  nfs:
    path: /opt/nfs/jenkins-data
    server: 192.168.0.10
  accessModes: ["ReadWriteOnce"]
  capacity:
    storage: 1Ti

我這裏給了 1T存儲空間,可以根據實際配置。

執行如下命令創建pv。

kubectl apply -f jenkins-pv.yaml 

4.創建serviceAccount

創建service account,因為 jenkins 後面需要能夠動態創建 slave,因此它必須具備一些權限。

創建jenkins-service-account.yaml文件,文件內容如下所示。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins

---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: jenkins
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: jenkins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins
subjects:
  - kind: ServiceAccount
    name: jenkins

上述配置中,創建了一個 RoleBinding 和一個 ServiceAccount,並且將 RoleBinding 的權限綁定到這個用戶上。所以,jenkins 容器必須使用這個 ServiceAccount 運行才行,不然 RoleBinding 的權限它將不具備。

RoleBinding 的權限很容易就看懂了,因為 jenkins 需要創建和刪除 slave,所以才需要上面這些權限。至於 secrets 權限,則是 https 證書。

執行如下命令創建serviceAccount。

kubectl apply -f jenkins-service-account.yaml 

5.安裝Jenkins

創建jenkins-statefulset.yaml文件,文件內容如下所示。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: jenkins
  labels:
    name: jenkins
spec:
  selector:
    matchLabels:
      name: jenkins
  serviceName: jenkins
  replicas: 1
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      name: jenkins
      labels:
        name: jenkins
    spec:
      terminationGracePeriodSeconds: 10
      serviceAccountName: jenkins
      containers:
        - name: jenkins
          image: docker.io/jenkins/jenkins:lts
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
            - containerPort: 32100
          resources:
            limits:
              cpu: 4
              memory: 4Gi
            requests:
              cpu: 4
              memory: 4Gi
          env:
            - name: LIMITS_MEMORY
              valueFrom:
                resourceFieldRef:
                  resource: limits.memory
                  divisor: 1Mi
            - name: JAVA_OPTS
              # value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
              value: -Xmx$(LIMITS_MEMORY)m -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
          volumeMounts:
            - name: jenkins-home
              mountPath: /var/jenkins_home
          livenessProbe:
            httpGet:
              path: /login
              port: 8080
            initialDelaySeconds: 60
            timeoutSeconds: 5
            failureThreshold: 12 # ~2 minutes
          readinessProbe:
            httpGet:
              path: /login
              port: 8080
            initialDelaySeconds: 60
            timeoutSeconds: 5
            failureThreshold: 12 # ~2 minutes
  # pvc 模板,對應之前的 pv
  volumeClaimTemplates:
    - metadata:
        name: jenkins-home
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Ti

jenkins 部署時需要注意它的副本數,你的副本數有多少就要有多少個 pv,同樣,存儲會有多倍消耗。這裏我只使用了一個副本,因此前面也只創建了一個 pv。

使用如下命令安裝Jenkins。

kubectl apply -f jenkins-statefulset.yaml 

6.創建Service

創建jenkins-service.yaml文件,主要用於後台運行Jenkins,文件內容如下所示。

apiVersion: v1
kind: Service
metadata:
  name: jenkins
spec:
  # type: LoadBalancer
  selector:
    name: jenkins
  # ensure the client ip is propagated to avoid the invalid crumb issue when using LoadBalancer (k8s >=1.7)
  #externalTrafficPolicy: Local
  ports:
    - name: http
      port: 80
      nodePort: 31888
      targetPort: 8080
      protocol: TCP
    - name: jenkins-agent
      port: 32100
      nodePort: 32100
      targetPort: 32100
      protocol: TCP
  type: NodePort

使用如下命令安裝Service。

kubectl apply -f jenkins-service.yaml 

7.安裝 ingress

jenkins 的 web 界面需要從集群外訪問,這裏我們選擇的是使用 ingress。創建jenkins-ingress.yaml文件,文件內容如下所示。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: jenkins
spec:
  rules:
    - http:
        paths:
          - path: /
            backend:
              serviceName: jenkins
              servicePort: 31888
      host: jekins.binghe.com

這裏,需要注意的是host必須配置為域名或者主機名,否則會報錯,如下所示。

The Ingress "jenkins" is invalid: spec.rules[0].host: Invalid value: "192.168.0.10": must be a DNS name, not an IP address

使用如下命令安裝ingress。

kubectl apply -f jenkins-ingress.yaml 

最後,由於我這裏使用的是虛擬機來搭建相關的環境,在本機訪問虛擬機映射的jekins.binghe.com時,需要配置本機的hosts文件,在本機的hosts文件中加入如下配置項。

192.168.0.10 jekins.binghe.com

注意:在Windows操作系統中,hosts文件所在的目錄如下。

C:\Windows\System32\drivers\etc

接下來,就可以在瀏覽器中通過鏈接:http://jekins.binghe.com:31888 來訪問Jekins了。

物理機安裝SVN

Apache Subversion 通常被縮寫成 SVN,是一個開放源代碼的版本控制系統,Subversion 在 2000 年由 CollabNet Inc 開發,現在發展成為 Apache 軟件基金會的一個項目,同樣是一個豐富的開發者和用戶社區的一部分。

SVN相對於的RCS、CVS,採用了分支管理系統,它的設計目標就是取代CVS。互聯網上免費的版本控制服務多基於Subversion。

這裏,以在Master節點(binghe101服務器)上安裝SVN為例。

1.使用yum安裝SVN

在命令行執行如下命令安裝SVN。

yum -y install subversion 

2.創建SVN庫

依次執行如下命令。

#創建/data/svn
mkdir -p /data/svn 
#初始化svn
svnserve -d -r /data/svn
#創建代碼倉庫
svnadmin create /data/svn/test

3.配置SVN

mkdir /data/svn/conf
cp /data/svn/test/conf/* /data/svn/conf/
cd /data/svn/conf/
[root@binghe101 conf]# ll
總用量 20
-rw-r--r-- 1 root root 1080 5月  12 02:17 authz
-rw-r--r-- 1 root root  885 5月  12 02:17 hooks-env.tmpl
-rw-r--r-- 1 root root  309 5月  12 02:17 passwd
-rw-r--r-- 1 root root 4375 5月  12 02:17 svnserve.conf
  • 配置authz文件,
vim authz

配置后的內容如下所示。

[aliases]
# joe = /C=XZ/ST=Dessert/L=Snake City/O=Snake Oil, Ltd./OU=Research Institute/CN=Joe Average

[groups]
# harry_and_sally = harry,sally
# harry_sally_and_joe = harry,sally,&joe
SuperAdmin = admin
binghe = admin,binghe

# [/foo/bar]
# harry = rw
# &joe = r
# * =

# [repository:/baz/fuz]
# @harry_and_sally = rw
# * = r

[test:/]
@SuperAdmin=rw
@binghe=rw
  • 配置passwd文件
vim passwd

配置后的內容如下所示。

[users]
# harry = harryssecret
# sally = sallyssecret
admin = admin123
binghe = binghe123
  • 配置 svnserve.conf
vim svnserve.conf

配置后的文件如下所示。

### This file controls the configuration of the svnserve daemon, if you
### use it to allow access to this repository.  (If you only allow
### access through http: and/or file: URLs, then this file is
### irrelevant.)

### Visit http://subversion.apache.org/ for more information.

[general]
### The anon-access and auth-access options control access to the
### repository for unauthenticated (a.k.a. anonymous) users and
### authenticated users, respectively.
### Valid values are "write", "read", and "none".
### Setting the value to "none" prohibits both reading and writing;
### "read" allows read-only access, and "write" allows complete 
### read/write access to the repository.
### The sample settings below are the defaults and specify that anonymous
### users have read-only access to the repository, while authenticated
### users have read and write access to the repository.
anon-access = none
auth-access = write
### The password-db option controls the location of the password
### database file.  Unless you specify a path starting with a /,
### the file's location is relative to the directory containing
### this configuration file.
### If SASL is enabled (see below), this file will NOT be used.
### Uncomment the line below to use the default password file.
password-db = /data/svn/conf/passwd
### The authz-db option controls the location of the authorization
### rules for path-based access control.  Unless you specify a path
### starting with a /, the file's location is relative to the
### directory containing this file.  The specified path may be a
### repository relative URL (^/) or an absolute file:// URL to a text
### file in a Subversion repository.  If you don't specify an authz-db,
### no path-based access control is done.
### Uncomment the line below to use the default authorization file.
authz-db = /data/svn/conf/authz
### The groups-db option controls the location of the file with the
### group definitions and allows maintaining groups separately from the
### authorization rules.  The groups-db file is of the same format as the
### authz-db file and should contain a single [groups] section with the
### group definitions.  If the option is enabled, the authz-db file cannot
### contain a [groups] section.  Unless you specify a path starting with
### a /, the file's location is relative to the directory containing this
### file.  The specified path may be a repository relative URL (^/) or an
### absolute file:// URL to a text file in a Subversion repository.
### This option is not being used by default.
# groups-db = groups
### This option specifies the authentication realm of the repository.
### If two repositories have the same authentication realm, they should
### have the same password database, and vice versa.  The default realm
### is repository's uuid.
realm = svn
### The force-username-case option causes svnserve to case-normalize
### usernames before comparing them against the authorization rules in the
### authz-db file configured above.  Valid values are "upper" (to upper-
### case the usernames), "lower" (to lowercase the usernames), and
### "none" (to compare usernames as-is without case conversion, which
### is the default behavior).
# force-username-case = none
### The hooks-env options specifies a path to the hook script environment 
### configuration file. This option overrides the per-repository default
### and can be used to configure the hook script environment for multiple 
### repositories in a single file, if an absolute path is specified.
### Unless you specify an absolute path, the file's location is relative
### to the directory containing this file.
# hooks-env = hooks-env

[sasl]
### This option specifies whether you want to use the Cyrus SASL
### library for authentication. Default is false.
### Enabling this option requires svnserve to have been built with Cyrus
### SASL support; to check, run 'svnserve --version' and look for a line
### reading 'Cyrus SASL authentication is available.'
# use-sasl = true
### These options specify the desired strength of the security layer
### that you want SASL to provide. 0 means no encryption, 1 means
### integrity-checking only, values larger than 1 are correlated
### to the effective key length for encryption (e.g. 128 means 128-bit
### encryption). The values below are the defaults.
# min-encryption = 0
# max-encryption = 256

接下來,將/data/svn/conf目錄下的svnserve.conf文件複製到/data/svn/test/conf/目錄下。如下所示。

[root@binghe101 conf]# cp /data/svn/conf/svnserve.conf /data/svn/test/conf/
cp:是否覆蓋'/data/svn/test/conf/svnserve.conf'? y

4.啟動SVN服務

(1)創建svnserve.service服務

創建svnserve.service文件

vim /usr/lib/systemd/system/svnserve.service

文件的內容如下所示。

[Unit]
Description=Subversion protocol daemon
After=syslog.target network.target
Documentation=man:svnserve(8)

[Service]
Type=forking
EnvironmentFile=/etc/sysconfig/svnserve
#ExecStart=/usr/bin/svnserve --daemon --pid-file=/run/svnserve/svnserve.pid $OPTIONS
ExecStart=/usr/bin/svnserve --daemon $OPTIONS
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

接下來執行如下命令使配置生效。

systemctl daemon-reload

命令執行成功后,修改 /etc/sysconfig/svnserve 文件。

vim /etc/sysconfig/svnserve 

修改后的文件內容如下所示。

# OPTIONS is used to pass command-line arguments to svnserve.
# 
# Specify the repository location in -r parameter:
OPTIONS="-r /data/svn"

(2)啟動SVN

首先查看SVN狀態,如下所示。

[root@test10 conf]# systemctl status svnserve.service
● svnserve.service - Subversion protocol daemon
   Loaded: loaded (/usr/lib/systemd/system/svnserve.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
     Docs: man:svnserve(8)

可以看到,此時SVN並沒有啟動,接下來,需要啟動SVN。

systemctl start svnserve.service

設置SVN服務開機自啟動。

systemctl enable svnserve.service

接下來,就可以下載安裝TortoiseSVN,輸入鏈接svn://192.168.0.10/test 並輸入用戶名binghe,密碼binghe123來連接SVN了。

Docker安裝SVN

拉取SVN鏡像

docker pull docker.io/elleflorio/svn-server

啟動SVN容器

docker run -v /usr/local/svn:/home/svn -v /usr/local/svn/passwd:/etc/subversion/passwd -v /usr/local/apache2:/run/apache2 --name svn_server -p 3380:80 -p 3690:3960 -e SVN_REPONAME=repos -d docker.io/elleflorio/svn-server

進入SVN容器內部

docker exec -it svn_server bash

進入容器后,可以參照物理機安裝SVN的方式配置SVN倉庫。

物理機安裝Jenkins

注意:安裝Jenkins之前需要安裝JDK和Maven,我這裏同樣將Jenkins安裝在Master節點(binghe101服務器)。

1.啟用Jenkins庫

運行以下命令以下載repo文件並導入GPG密鑰:

wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo
rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key

2.安裝Jenkins

執行如下命令安裝Jenkis。

yum install jenkins

接下來,修改Jenkins默認端口,如下所示。

vim /etc/sysconfig/jenkins

修改后的兩項配置如下所示。

JENKINS_JAVA_CMD="/usr/local/jdk1.8.0_212/bin/java"
JENKINS_PORT="18080"

此時,已經將Jenkins的端口由8080修改為18080

3.啟動Jenkins

在命令行輸入如下命令啟動Jenkins。

systemctl start jenkins

配置Jenkins開機自啟動。

systemctl enable jenkins

查看Jenkins的運行狀態。

[root@test10 ~]# systemctl status jenkins
● jenkins.service - LSB: Jenkins Automation Server
   Loaded: loaded (/etc/rc.d/init.d/jenkins; generated)
   Active: active (running) since Tue 2020-05-12 04:33:40 EDT; 28s ago
     Docs: man:systemd-sysv-generator(8)
    Tasks: 71 (limit: 26213)
   Memory: 550.8M

說明,Jenkins啟動成功。

配置Jenkins運行環境

1.登錄Jenkins

首次安裝后,需要配置Jenkins的運行環境。首先,在瀏覽器地址欄訪問鏈接http://192.168.0.10:18080,打開Jenkins界面。

根據提示使用如下命令到服務器上找密碼值,如下所示。

[root@binghe101 ~]# cat /var/lib/jenkins/secrets/initialAdminPassword
71af861c2ab948a1b6efc9f7dde90776

將密碼71af861c2ab948a1b6efc9f7dde90776複製到文本框,點擊繼續。會跳轉到自定義Jenkins頁面,如下所示。

這裏,可以直接選擇“安裝推薦的插件”。之後會跳轉到一個安裝插件的頁面,如下所示。

此步驟可能有下載失敗的情況,可直接忽略。

2.安裝插件

需要安裝的插件

  • Kubernetes Cli Plugin:該插件可直接在Jenkins中使用kubernetes命令行進行操作。
  • Kubernetes plugin: 使用kubernetes則需要安裝該插件
  • Kubernetes Continuous Deploy Plugin:kubernetes部署插件,可根據需要使用

還有更多的插件可供選擇,可點擊 系統管理->管理插件進行管理和添加,安裝相應的Docker插件、SSH插件、Maven插件。其他的插件可以根據需要進行安裝。如下圖所示。

3.配置Jenkins

(1)配置JDK和Maven

在Global Tool Configuration中配置JDK和Maven,如下所示,打開Global Tool Configuration界面。

接下來就開始配置JDK和Maven了。

由於我在服務器上將Maven安裝在/usr/local/maven-3.6.3目錄下,所以,需要在“Maven 配置”中進行配置,如下圖所示。

接下來,配置JDK,如下所示。

注意:不要勾選“Install automatically”

接下來,配置Maven,如下所示。

注意:不要勾選“Install automatically”

(2)配置SSH

進入Jenkins的Configure System界面配置SSH,如下所示。

找到 SSH remote hosts 進行配置。

配置完成后,點擊Check connection按鈕,會显示 Successfull connection。如下所示。

至此,Jenkins的基本配置就完成了。

Jenkins發布Docker項目到K8s集群

1.調整SpringBoot項目的配置

實現,SpringBoot項目中啟動類所在的模塊的pom.xml需要引入打包成Docker鏡像的配置,如下所示。

  <properties>
  	 	<docker.repostory>192.168.0.10:1180</docker.repostory>
        <docker.registry.name>test</docker.registry.name>
        <docker.image.tag>1.0.0</docker.image.tag>
        <docker.maven.plugin.version>1.4.10</docker.maven.plugin.version>
  </properties>

<build>
  		<finalName>test-starter</finalName>
		<plugins>
            <plugin>
			    <groupId>org.springframework.boot</groupId>
			    <artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			
			<!-- docker的maven插件,官網:https://github.com/spotify/docker‐maven‐plugin -->
			<!-- Dockerfile maven plugin -->
			<plugin>
			    <groupId>com.spotify</groupId>
			    <artifactId>dockerfile-maven-plugin</artifactId>
			    <version>${docker.maven.plugin.version}</version>
			    <executions>
			        <execution>
			        <id>default</id>
			        <goals>
			            <!--如果package時不想用docker打包,就註釋掉這個goal-->
			            <goal>build</goal>
			            <goal>push</goal>
			        </goals>
			        </execution>
			    </executions>
			    <configuration>
			    	<contextDirectory>${project.basedir}</contextDirectory>
			        <!-- harbor 倉庫用戶名及密碼-->
			        <useMavenSettingsForAuth>useMavenSettingsForAuth>true</useMavenSettingsForAuth>
			        <repository>${docker.repostory}/${docker.registry.name}/${project.artifactId}</repository>
			        <tag>${docker.image.tag}</tag>
			        <buildArgs>
			            <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
			        </buildArgs>
			    </configuration>
			</plugin>

        </plugins>
        
		<resources>
			<!-- 指定 src/main/resources下所有文件及文件夾為資源文件 -->
			<resource>
				<directory>src/main/resources</directory>
				<targetPath>${project.build.directory}/classes</targetPath>
				<includes>
					<include>**/*</include>
				</includes>
				<filtering>true</filtering>
			</resource>
		</resources>
	</build>

接下來,在SpringBoot啟動類所在模塊的根目錄創建Dockerfile,內容示例如下所示。

#添加依賴環境,前提是將Java8的Docker鏡像從官方鏡像倉庫pull下來,然後上傳到自己的Harbor私有倉庫中
FROM 192.168.0.10:1180/library/java:8
#指定鏡像製作作者
MAINTAINER binghe
#運行目錄
VOLUME /tmp
#將本地的文件拷貝到容器
ADD target/*jar app.jar
#啟動容器后自動執行的命令
ENTRYPOINT [ "java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar" ]

根據實際情況,自行修改。

注意:FROM 192.168.0.10:1180/library/java:8的前提是執行如下命令。

docker pull java:8
docker tag java:8 192.168.0.10:1180/library/java:8
docker login 192.168.0.10:1180
docker push 192.168.0.10:1180/library/java:8

在SpringBoot啟動類所在模塊的根目錄創建yaml文件,錄入叫做test.yaml文件,內容如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-starter
  labels:
    app: test-starter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-starter
  template:
    metadata:
      labels:
        app: test-starter
    spec:
      containers:
      - name: test-starter
        image: 192.168.0.10:1180/test/test-starter:1.0.0
        ports:
        - containerPort: 8088
      nodeSelector:
        clustertype: node12

---
apiVersion: v1
kind: Service
metadata:
  name: test-starter
  labels:
    app: test-starter
spec:
  ports:
    - name: http
      port: 8088
      nodePort: 30001
  type: NodePort
  selector:
    app: test-starter

2.Jenkins配置發布項目

將項目上傳到SVN代碼庫,例如地址為svn://192.168.0.10/test

接下來,在Jenkins中配置自動發布。步驟如下所示。

點擊新建Item。

在描述文本框中輸入描述信息,如下所示。

接下來,配置SVN信息。

注意:配置GitLab的步驟與SVN相同,不再贅述。

定位到Jenkins的“構建模塊”,使用Execute Shell來構建發布項目到K8S集群。

執行的命令依次如下所示。

#刪除本地原有的鏡像,不會影響Harbor倉庫中的鏡像
docker rmi 192.168.0.10:1180/test/test-starter:1.0.0
#使用Maven編譯、構建Docker鏡像,執行完成后本地Docker容器中會重新構建鏡像文件
/usr/local/maven-3.6.3/bin/mvn -f ./pom.xml clean install -Dmaven.test.skip=true
#登錄 Harbor倉庫
docker login 192.168.0.10:1180 -u binghe -p Binghe123
#上傳鏡像到Harbor倉庫
docker push 192.168.0.10:1180/test/test-starter:1.0.0
#停止並刪除K8S集群中運行的
/usr/bin/kubectl delete -f test.yaml
#將Docker鏡像重新發布到K8S集群
/usr/bin/kubectl apply -f test.yaml

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

信息泄漏時代,如何讓自己的密碼更安全?

密碼的重要性,相信大家都不言而喻。而密碼泄漏或信息泄漏,經常是層出不窮地出現,令人防不勝防。所以,一個強大而複雜的密碼是保證自己賬戶安全的第一步。

為了防止信息泄漏,我們可以做些什麼呢?

  • 密碼足夠複雜;
  • 每個平台密碼都不一樣,比如QQ,微信,郵箱等;
  • 定期更換密碼。

那怎樣的密碼才算是比較可靠的密碼?一般而言,一個密碼至少12位字符,包含数字,包含大小寫,包含特殊符號,不使用現有單詞,即是一個比較複雜的密碼。

那你自認為比較安全的密碼,是否真正的安全呢?這裏良許介紹兩個工具可以用來評估你的密碼的安全性—— cracklib 和 pwscore 。

cracklib介紹

1. cracklib 的安裝

cracklib 可以用來檢測你的密碼是否可靠,在大部分發行版里都可以直接安裝這個工具。

對於 Fedora 系的發行版,可以使用 dnf 命令安裝 cracklib:

$ sudo dnf install cracklib

對於 Debian/Ubuntu 系的發行版,可以使用 apt-get 命令安裝:

$ sudo apt install libcrack2

對於 Arch 系統的發行版,可以使用 pacman 命令安裝:

$ sudo pacman -S cracklib

對於 RHEL/CentOS 系的發行版,可以使用 yum 命令安裝:

$ sudo yum install cracklib

對於 openSUSE 系的發行版,可以使用 zypper 命令安裝:

$ sudo zypper install cracklib

2. cracklib 的使用

我們直接來看幾個實例。

如果你的密碼里包含了人名、地名,或者我們常用的單詞,那麼會被提示 it is based on a dictionary word :

$ echo "password" | cracklib-check
password: it is based on a dictionary word

Linux 下默認的密碼長度是 7 個字符,如果你的密碼長度小於 7 個字符,會被提示 it is WAY too short :

$ echo "123" | cracklib-check 
123: it is WAY too short

如果你的密碼比較強壯,則會提示 OK :

$ echo "ME$2w!@fgty6723" | cracklib-check
ME!@fgty6723: OK

pwscore 介紹

我們使用 cracklib 工具只能判斷一個密碼是否安全,但具體也不知道它有多安全。而 pwscore 工具就能告訴你,你的密碼強度可以打幾分。

1. pwscore 的安裝

同樣地,對於大部分 Linux 發行版,可以直接安裝 pwscore 工具。安裝過程與 cracklib 類似,只需將 cracklib 改成 pwscore 即可。這裏介紹 Debian/Ubuntu 系發行版的安裝,其餘的類似:

$ sudo apt install libpwquality

2. pwscore 的使用

同樣直接來看幾個實例。

與 cracklib 類似,如果你的密碼里包含了人名、地名,或者我們常用的單詞,那麼會被提示 it is based on a dictionary word ;如果密碼長度短於 7 個字符,會被提示 it is WAY too short 。

$ echo "password" | pwscore
Password quality check failed:
 The password fails the dictionary check - it is based on a dictionary word

$ echo "123" | pwscore
Password quality check failed:
 The password is shorter than 8 characters

如果你的密碼合乎規範,那麼它就會給你打相應的分數:

$ echo "ME!@fgty6723" | pwscore
90

小結

雖然黑客有一萬種竊取你的私人數據的方法,但一個強壯的密碼是你保護你敏感數據的第一步。網絡環境本身就不是 100% 安全,如果你再使用一個很容易玫破的密碼,那下一個艷照門可能很快就會再次出現……

公眾號:良許Linux

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

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

0基礎學算法 第七彈 位運算

  今天要講的內容主要分為兩個大塊,一個呢就是位運算,另一個呢就是關於二進制的原碼反碼補碼,位運算的應用也算是比較多的了,進行位運算有時候可以省略不少事情呢,當然,也不是說不會位運算就會天崩地裂,畢竟事實上,在學位運算之前,我都是用其他方法來強制性模擬位運算(當時我並不知道什麼是位運算)不過多學一些也是好的,省事兒,而且更快

  一,二進制的原碼反碼和補碼

  相信二進制沒有人不熟悉的,逢二進一,一個二進制數由0和1組成,他的最高位就是他的正負符號,0是正數,1是負數,例如10001000,這就是一個負的二進制數,當然最高位是0就是正數

  原碼之所以叫做原碼,必然是因為它就像是萬物之源,補碼、反碼都是在他的基礎上變化的,當最高位是0的時候,也就是正數的時候,原碼==反碼==補碼

  以下關於反碼及補碼的概念是基於最高位是1的情況(負數的情況)

  反碼,顧名思義,是原碼顛倒過來的樣子,除了最高位表示負號的那個“1”不變以外,其他的數位上的數  全部取反,1變0,0變1,假設原碼為1001010100,它的反碼就是1110101011,再假設原碼是0111001,它的反碼就是0111001,是不是有點奇怪,哈,我前面說過了,當原碼是正數的時候,原碼==反碼==補碼

  補碼,在原碼是正數的情況下是等於原碼或者反碼的,但是在負數情況下,他恰好是反碼加1,比如原碼是100000,反碼就是111111,補碼就是1000000

  二,進入正題,位運算

  在系統的講位運算之前,先給大家一張圖表,方便大家查詢

  

   註明:位運算符號在c++中用法同算數運算符

  首先是&

  其實位運算不複雜,理解了就好,比如說&,把他理解成1代表true,2代表false,1&&2,明顯為false(2),2&&2明顯也為false(2),只有1&&1的時候才為true(1);

  但是這是if語句裏面的判斷方法,而‘&’的運算方法是這樣的,比如1010&0011,運算過程如下圖

  過程如右圖

  至於|的話。。。就是上下兩個數只要有一個為1,結果就為1啦,如1100|0011=1111

  ‘~’!也比較好理解,做它的運算的時候,只需要傳一個二進制數,針對每一位進行運算可以從在每一位上如~1001=0110;

  ^,亦或,算起來也比較簡單,只要兩位不相等,結果就為1,舉例,1001^0101=1100

我覺得有圖就不用我多講了吧/狗頭/滑稽

   最後一組”<<“和”>>”這個是左移和右移的操作

  請看

  int類型的二進制是32位的,而long long是64位,左移和右移

  例如,給出一個標準的32的int類型00000000000000000000000000000001,當我們使用左移的時候,整體忘左移,並在後面補零

右移也相似,只不過是低位被擠掉了,高位補0

 

 

  有一道題,可以通過位運算做的很簡便,但如果你不會位運算的話。。。就另當別論了

  如果你不用位運算,你要寫很複雜的代碼,但是用了,你就只要10行代碼,如假包換(雖然我絕對不換)

  題目鏈接→https://www.luogu.com.cn/problem/P1100

   看到題目,很清晰了,首先錄入兩個字符串,然後將它轉成二進制,接着把前16位和后16位的數交換位置,最後結果轉成十進制輸出!好,完美。。。

  停!

 今天利用我們學的位運算壓根不用這麼麻煩!

  直接用上我們的左移”<<“和右移”>>”啊!

  只要左移后16位,右移前16位不就好了?

  廢話不多說,大家請看最短代碼!

#include<bits/stdc++.h>
using namespace std;
unsigned int x;
int main(){
    cin>>x;//錄入x
    cout<<((x<<16)|(x>>16));//將左移16位后的x和右移16位的x用或"|"將他們重新連起來
    return 0;//完美
}

  完美撒花

  如果覺得我講的不錯的,麻煩點個關注,點個贊,以後還會持續更新的

ps:歡迎大家進群 群號:1031457671

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

【其他文章推薦】

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

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

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

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

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

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

Alink漫談(七) : 如何劃分訓練數據集和測試數據集

Alink漫談(七) : 如何劃分訓練數據集和測試數據集

目錄

  • Alink漫談(七) : 如何劃分訓練數據集和測試數據集
    • 0x00 摘要
    • 0x01 訓練數據集和測試數據集
    • 0x02 Alink示例代碼
    • 0x03 批處理
      • 3.1 得到記錄數
      • 3.2 隨機選取記錄
        • 3.2.1 得到總記錄數
        • 3.2.2 決定每個task選擇記錄數
        • 3.2.3 每個task選擇記錄
      • 3.3 設置訓練數據集和測試數據集
    • 0x04 流處理
    • 0x05 參考

0x00 摘要

Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習算法平台,是業界首個同時支持批式算法、流式算法的機器學習平台。本文將為大家展現Alink如何劃分訓練數據集和測試數據集。

0x01 訓練數據集和測試數據集

兩分法

一般做預測分析時,會將數據分為兩大部分。一部分是訓練數據,用於構建模型,一部分是測試數據,用於檢驗模型。

三分法

但有時候模型的構建過程中也需要檢驗模型/輔助模型構建,這時會將訓練數據再分為兩個部分:1)訓練數據;2)驗證數據(Validation Data)。所以這種情況下會把數據分為三部分。

  • 訓練數據(Train Data):用於模型構建。
  • 驗證數據(Validation Data):可選,用於輔助模型構建,可以重複使用。
  • 測試數據(Test Data):用於檢測模型構建,此數據只在模型檢驗時使用,用於評估模型的準確率。絕對不允許用於模型構建過程,否則會導致過渡擬合。

Training set是用來訓練模型或確定模型參數的,如ANN中權值等;

Validation set是用來做模型選擇(model selection),即做模型的最終優化及確定,如ANN的結構;

Test set則純粹是為了測試已經訓練好的模型的推廣能力。當然test set並不能保證模型的正確性,他只是說相似的數據用此模型會得出相似的結果。

實際應用

實際應用中,一般只將數據集分成兩類,即training set 和test set,大多數文章並不涉及validation set。我們這裏也不涉及。大家常用的sklearn的train_test_split函數就是將矩陣隨機劃分為訓練子集和測試子集,並返回劃分好的訓練集測試集樣本和訓練集測試集標籤。

0x02 Alink示例代碼

首先我們給出示例代碼,然後會深入剖析:

public class SplitExample {
  public static void main(String[] args) throws Exception {
    String url = "iris.csv";
    String schema = "sepal_length double, sepal_width double, petal_length double, petal_width double, category string";

    //這裡是批處理
    BatchOperator data = new CsvSourceBatchOp().setFilePath(url).setSchemaStr(schema);
    SplitBatchOp spliter = new SplitBatchOp().setFraction(0.8);
    spliter.linkFrom(data);
    BatchOperator trainData = spliter;
    BatchOperator testData = spliter.getSideOutput(0);

    // 這裡是流處理
    CsvSourceStreamOp dataS = new CsvSourceStreamOp().setFilePath(url).setSchemaStr(schema);
    SplitStreamOp spliterS = new SplitStreamOp().setFraction(0.4);
    spliterS.linkFrom(dataS);
    StreamOperator train_data = spliterS;
    StreamOperator test_data = spliterS.getSideOutput(0);
  }
}

0x03 批處理

SplitBatchOp是分割批處理的主要類,具體構建DAG的工作是在其linkFrom完成的。

總體思路比較簡單:

  1. 假定有一個採樣比例 fraction
  2. 將數據集分區,并行計算每個分區上的記錄數
  3. 把每個分區上的記錄數累積,得到所有記錄總數 totCount
  4. 從上而下計算出一個採樣總數:numTarget = totCount * fraction
  5. 因為具體選擇元素是在每個分區上做的,所以在每個分區上,分別計算出來這個分區應該採樣的記錄數,比如第n個分區上應採樣記錄數:task_n_count * fraction
  6. 把這些分區 “應該採樣的記錄數” 累積,得出來從下而上計算出的採樣總數: totSelect = task_1_count * fraction + task_2_count * fraction + ... task_n_count * fraction
  7. numTarget 和 totSelect 可能不相等,所以隨機決定把多出來的 numTarget - totSelect 加入到某一個task中。
  8. 在每個task上採樣得到具體的記錄。

3.1 得到記錄數

如果要分割數據,首先必須知道數據集的記錄數。比如這個DataSet的記錄是1萬個?還是十萬個?因為數據集可能會很大,所以這一步操作也使用了并行處理,即把數據分區,然後通過mapPartition操作得到每一個分區上元素的數目。

DataSet<Tuple2<Integer, Long>> countsPerPartition = DataSetUtils.countElementsPerPartition(rows); //返回哪個task有哪些記錄數

DataSet<long[]> numPickedPerPartition = countsPerPartition
    .mapPartition(new CountInPartition(fraction)) //計算總數
    .setParallelism(1)
    .name("decide_count_of_each_partition");

因為每個分區就對應了一個task,所以我們也可以認為,這是獲取了每個task的記錄數。

具體工作是在 DataSetUtils.countElementsPerPartition 中完成的。返回類型是<index of this subtask, record count in this subtask>,比如3號task擁有30個記錄。

public static <T> DataSet<Tuple2<Integer, Long>> countElementsPerPartition(DataSet<T> input) {
   return input.mapPartition(new RichMapPartitionFunction<T, Tuple2<Integer, Long>>() {
      @Override
      public void mapPartition(Iterable<T> values, Collector<Tuple2<Integer, Long>> out) throws Exception {
         long counter = 0;
         for (T value : values) {
            counter++; //計算本task的記錄總數
         }
         out.collect(new Tuple2<>(getRuntimeContext().getIndexOfThisSubtask(), counter));
      }
   });
}

計算總數的工作其實是在下一階段算子中完成的。

3.2 隨機選取記錄

接下來的工作主要是在 CountInPartition.mapPartition 完成的,其作用是隨機決定每個task選擇多少個記錄。

這時候就不需要并行了,所以 .setParallelism(1)

3.2.1 得到總記錄數

得到了每個分區記錄數之後,我們遍歷每個task的記錄數,然後累積得到總記錄數 totCount(就是從上而下計算出來的總數)。

public void mapPartition(Iterable<Tuple2<Integer, Long>> values, Collector<long[]> out) throws Exception {
	long totCount = 0L;
	List<Tuple2<Integer, Long>> buffer = new ArrayList<>();
	for (Tuple2<Integer, Long> value : values) { //遍歷輸入的所有分區記錄
    totCount += value.f1; //f1是Long類型的記錄數
    buffer.add(value);
	}
  ...
  //後續代碼在下面分析。  
}

3.2.2 決定每個task選擇記錄數

然後CountInPartition.mapPartition函數中會隨機決定每個task會選擇的記錄數。mapPartition的參數 Iterable<Tuple2<Integer, Long>> values 就是前一階段的結果 :一個元祖<task id, 每個task的記錄數目>。

把這些元祖結合在一起,記錄在buffer這個列表中。

buffer = {ArrayList@8972}  size = 4
 0 = {Tuple2@8975} "(3,38)" // 3號task,其對應的partition記錄數是38個。
 1 = {Tuple2@8976} "(2,0)"
 2 = {Tuple2@8977} "(0,38)"
 3 = {Tuple2@8978} "(1,74)"

系統的task數目就是buffer大小。

int npart = buffer.size(); // num tasks

然後,根據”記錄總數“計算出來 “隨機訓練數據的個數numTarget”。比如總數1萬,應該隨機分配20%,於是numTarget就應該是2千。這個数字以後會用到。

long numTarget = Math.round((totCount * fraction));

得到每個task的記錄數目,比如是上面buffer中的 38,0,38,還是74,記錄在 eachCount 中。

for (Tuple2<Integer, Long> value : buffer) {
    eachCount[value.f0] = value.f1;
}

得到每個task中隨機選中的訓練記錄數,記錄在 eachSelect 中。就是每個task目前 “記錄数字 * fraction”。比如3號task記錄數是38個,應該選20%,則38*20%=8個。

然後把這些task自己的“隨機訓練記錄數”再累加起來得到 totSelect(就是從下而上計算出來的總數)。

long totSelect = 0L;
for (int i = 0; i < npart; i++) {
    eachSelect[i] = Math.round(Math.floor(eachCount[i] * fraction));
    totSelect += eachSelect[i];
}

請注意,這時候 totSelect 和 之前計算的numTarget就有具體細微出入了,就是理論上的一個数字,但是我們 從上而下 計算 和 從下而上 計算,其結果可能不一樣。通過下面我們可以看出來。

numTarget = all count * fraction

totSelect = task_1_count * fraction + task_2_count * fraction + ...

所以我們下一步要處理這個細微出入,就得到remain,這是”總體算出來的隨機數目” numTarget 和 “從所有task選中的隨機訓練記錄數累積” totSelect 的差。

if (totSelect < numTarget) {
    long remain = numTarget - totSelect;
    remain = Math.min(remain, totCount - totSelect);

如果剛好個數相等,則就正常分配。

if (remain == totCount - totSelect) {

如果數目不等,隨機決定把”多出來的remain”加入到eachSelect數組中的隨便一個記錄上。

for (int i = 0; i < Math.min(remain, npart); i++) {
    int taskId = shuffle.get(i);
    while (eachSelect[taskId] >= eachCount[taskId]) {
          taskId = (taskId + 1) % npart;
    }
    eachSelect[taskId]++;
}

最後給出所有信息

long[] statistics = new long[npart * 2];
for (int i = 0; i < npart; i++) {
    statistics[i] = eachCount[i];
    statistics[i + npart] = eachSelect[i];
}
out.collect(statistics);

// 我們這裡是4核,所以前面四項是eachCount,後面是eachSelect
statistics = {long[8]@9003} 
 0 = 38 //eachCount
 1 = 38
 2 = 36
 3 = 38
   
 4 = 31 //eachSelect
 5 = 31
 6 = 28
 7 = 30

這些信息是作為廣播變量存儲起來的,馬上下面就會用到。

 .withBroadcastSet(numPickedPerPartition, "counts")

3.2.3 每個task選擇記錄

CountInPartition.PickInPartition函數中會隨機在每個task選擇記錄。

首先得到task數目 和 之前存儲的廣播變量(就是之前剛剛存儲的)。

int npart = getRuntimeContext().getNumberOfParallelSubtasks();
List<long[]> bc = getRuntimeContext().getBroadcastVariable("counts");

分離count和select。

long[] eachCount = Arrays.copyOfRange(bc.get(0), 0, npart);
long[] eachSelect = Arrays.copyOfRange(bc.get(0), npart, npart * 2);

得到總task數目

int taskId = getRuntimeContext().getIndexOfThisSubtask();

得到自己 task 對應的 count, select

long count = eachCount[taskId];
long select = eachSelect[taskId];

添加本task對應的記錄,隨機洗牌打亂順序

for (int i = 0; i < count; i++) {
     shuffle.add(i); //就是把count內的数字加到數組
}
Collections.shuffle(shuffle, new Random(taskId)); //洗牌打亂順序

// suffle舉例
shuffle = {ArrayList@8987}  size = 38
 0 = {Integer@8994} 17
 1 = {Integer@8995} 8
 2 = {Integer@8996} 33
 3 = {Integer@8997} 34
 4 = {Integer@8998} 20
 5 = {Integer@8999} 0
 6 = {Integer@9000} 26
 7 = {Integer@9001} 27
 8 = {Integer@9002} 23
 9 = {Integer@9003} 28
 10 = {Integer@9004} 9
 11 = {Integer@9005} 16
 12 = {Integer@9006} 13
 13 = {Integer@9007} 2
 14 = {Integer@9008} 5
 15 = {Integer@9009} 31
 16 = {Integer@9010} 15
 17 = {Integer@9011} 22
 18 = {Integer@9012} 18
 19 = {Integer@9013} 35
 20 = {Integer@9014} 36
 21 = {Integer@9015} 12
 22 = {Integer@9016} 7
 23 = {Integer@9017} 21
 24 = {Integer@9018} 14
 25 = {Integer@9019} 1
 26 = {Integer@9020} 10
 27 = {Integer@9021} 30
 28 = {Integer@9022} 29
 29 = {Integer@9023} 19
 30 = {Integer@9024} 25
 31 = {Integer@9025} 32
 32 = {Integer@9026} 37
 33 = {Integer@9027} 4
 34 = {Integer@9028} 11
 35 = {Integer@9029} 6
 36 = {Integer@9030} 3
 37 = {Integer@9031} 24

隨機選擇,把選擇后的再排序回來

for (int i = 0; i < select; i++) {
    selected[i] = shuffle.get(i); //這時候select看起來是按照順序選擇,但是實際上suffle裏面已經是亂序
}
Arrays.sort(selected); //這次再排序

// selected舉例,一共30個
selected = {int[30]@8991} 
 0 = 0
 1 = 1
 2 = 2
 3 = 5
 4 = 7
 5 = 8
 6 = 9
 7 = 10
 8 = 12
 9 = 13
 10 = 14
 11 = 15
 12 = 16
 13 = 17
 14 = 18
 15 = 19
 16 = 20
 17 = 21
 18 = 22
 19 = 23
 20 = 26
 21 = 27
 22 = 28
 23 = 29
 24 = 30
 25 = 31
 26 = 33
 27 = 34
 28 = 35
 29 = 36

發送選擇的數據

if (numEmits < selected.length && iRow == selected[numEmits]) {
    out.collect(row);
    numEmits++;
}

3.3 設置訓練數據集和測試數據集

output是訓練數據集,SideOutput是測試數據集。因為這兩個數據集在Alink內部都是Table類型,所以直接使用了SQL算子 minusAll 來完成分割。

this.setOutput(out, in.getSchema());
this.setSideOutputTables(new Table[]{in.getOutputTable().minusAll(this.getOutputTable())});

0x04 流處理

訓練是在SplitStreamOp類完成的,其通過linkFrom完成了模型的構建。

流處理依賴SplitStream 和 SelectTransformation 這兩個類來完成分割流。具體並沒有建立一個物理操作,而只是影響了上游算子如何與下游算子聯繫,如何選擇記錄。

SplitStream <Row> splited = in.getDataStream().split(new RandomSelectorOp(getFraction()));

首先,用RandomSelectorOp來隨機決定輸出時候選擇哪個流。我們可以看到,這裏就是隨便起了”a”, “b” 這兩個名字而已。

class RandomSelectorOp implements OutputSelector <Row> {
   private double fraction;
   private Random random = null;
   @Override
   public Iterable <String> select(Row value) {
      if (null == random) {
         random = new Random(System.currentTimeMillis());
      }
      List <String> output = new ArrayList <String>(1);
      output.add((random.nextDouble() < fraction ? "a" : "b")); //隨機選取数字分配,隨意起的名字
      return output;
   }
}

其次,得到那兩個隨機生成的流。

DataStream <Row> partA = splited.select("a");
DataStream <Row> partB = splited.select("b");

最後把這兩個流分別設置為output和sideOutput。

this.setOutput(partA, in.getSchema()); //訓練集
this.setSideOutputTables(new Table[]{
DataStreamConversionUtil.toTable(getMLEnvironmentId(), partB, in.getSchema())}); //驗證集

最後返回本身,這時候SplitStreamOp擁有兩個成員變量:

this.output就是訓練集。

this.sideOutPut就是驗證集。

return this;

0x05 參考

訓練數據,驗證數據和測試數據分析

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

ThreadLocal的使用場景分析

目錄

一.ThreadLocal介紹

二.使用場景1——數據庫事務問題

  2.1 問題背景

  2.2 方案1-修改接口傳參

  2.3 方案2-使用ThreadLocal

三.使用場景2——日誌追蹤問題

四.其他使用場景

 

 

一.ThreadLocal介紹

  我們知道,變量從作用域範圍進行分類,可以分為“全局變量”、“局部變量”兩種:

  1.全局變量(global variable),比如類的靜態屬性(加static關鍵字),在類的整個生命周期都有效;

  2.局部變量(local variable),比如在一個方法中定義的變量,作用域只是在當前方法內,方法執行完畢后,變量就銷毀(釋放)了;

  使用全局變量,當多個線程同時修改靜態屬性,就容易出現併發問題,導致臟數據;而局部變量一般來說不會出現併發問題(在方法中開啟多線程併發修改局部變量,仍可能引起併發問題);

  再看ThreadLocal,從名稱上就能知道,它可以用來保存局部變量,只不過這個“局部”是指“線程”作用域,也就是說,該變量在該線程的整個生命周期中有效。

 

二.使用場景1——數據庫事務問題

2.1問題背景

  下面介紹示例,UserService調用UserDao刪除用戶信息,涉及到兩張表的操作,所以用到了數據庫事務:

  數據庫封裝類DbUtils

public class DbUtils {

    // 使用C3P0連接池
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");

    public static Connection getConnectionFromPool() throws SQLException {
        return dataSource.getConnection();
    }

    // 省略其他方法.....
}

  UserService代碼如下:  

public class UserService {

    private UserDao userDao;

    public void deleteUserInfo(Integer id, String operator) {
        Connection connection = null;
        try {
            // 從連接池中獲取一個連接
            connection = DbUtils.getConnectionFromPool();
            // 因為涉及事務操作,所以需要關閉自動提交
            connection.setAutoCommit(false);

            // 事務涉及兩步操作,刪除用戶表,增加操作日誌
            userDao.deleteUserById(id);
            userDao.addOperateLog(id, operator);

            connection.commit();
        } catch (SQLException e) {
            // 回滾操作
            try {
                if (connection != null) {
                    connection.rollback();
                }
            } catch (SQLException ex) {
            }
        } finally {
            DbUtils.freeConnection(connection);
        }
    }
}

  下面是UserDao,省略了部分代碼:

package cn.ganlixin.dao;
import cn.ganlixin.util.DbUtils;
import java.sql.Connection;

/**
 * @author ganlixin
 * @create 2020-06-12
 */
public class UserDao {

    public void deleteUserById(Integer id) {
        // 從連接池中獲取一個數據連接
        Connection connection = DbUtils.getConnectionFromPool();

        // 利用獲取的數據庫連接,執行sql...........刪除用戶表的一條數據

        // 歸還連接給連接池
        DbUtils.freeConnection(connection);
    }

    public void addOperateLog(Integer id, String operator) {
        // 從連接池中獲取一個數據連接
        Connection connection = DbUtils.getConnectionFromPool();

        // 利用獲取的數據庫連接,執行sql...........插入一條記錄到操作日誌表

        // 歸還連接給連接池
        DbUtils.freeConnection(connection);
    }
}

  上面的代碼乍一看,好像沒啥問題,但是仔細看,其實是存在問題的!!問題出在哪兒呢?就出在從數據庫連接池獲取連接哪個位置。

  1.UserService會從數據庫連接池獲取一個連接,關閉該連接的自動提交;

  2.UserService然後調用UserDao的兩個接口進行數據庫操作;

  3.UserDao的兩個接口,都會從數據庫連接池獲取一個連接,然後執行sql;

  注意,第1步和第3步獲得的連接不一定是同一個!!!!這才是關鍵。

  如果UserService和UserDao獲取的數據庫連接不是同一個,那麼UserService中關閉自動提交的數據庫連接,並不是UserDao接口中執行sql的數據庫連接,當userService中捕獲異常,即使執行rollback,userDao中的sql已經執行完了,並不會回滾,所以數據已經出現不一致!!!

 

2.2方案1-修改接口傳參

  上面的例子中,因為UserService和UserDao獲取的連接不是同一個,所以並不能保證事務原子性;那麼只要能夠解決這個問題,就可以保證了

  可以修改userDao中的代碼,不要每次在UserDao中從數據庫連接池獲取連接,而是增加一個參數,該參數就是數據庫連接,有UserService傳入,這樣就能保證UserService和UserDao使用同一個數據庫連接了

public class UserDao {

    public void deleteUserById(Connection connection, Integer id) {
        // 利用傳入的數據庫連接,執行sql...........刪除用戶表的一條數據
    }

    public void addOperateLog(Connection connection, Integer id, String operator) {
        // 利用傳入的數據庫連接,執行sql...........插入一條記錄到操作日誌表
    }
}

  UserService調用接口時,傳入數據庫連接,修改代碼后如下:

// 事務涉及兩步操作,刪除用戶表,增加操作日誌
// 新增參數傳入數據庫連接,保證UserService和UserDao使用同一個連接
userDao.deleteUserById(connection, id);
userDao.addOperateLog(connection, id, operator);

  這樣做,的確是能解決數據庫事務的問題,但是並不推薦這樣做,耦合度太高,不利於維護,修改起來也不方便;

 

2.3使用ThreadLocal解決

  ThreadLocal可以保存當前線程有效的變量,正好適合解決這個問題,而且改動的點也特別小,只需要在DbUtils獲取連接的時候,將獲取到的連接存到ThreadLocal中即可:

public class DbUtils {

    // 使用C3P0連接池
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");

    // 創建threadLocal對象,保存每個線程的數據庫連接對象
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    public static Connection getConnectionFromPool() throws SQLException {
        if (threadLocal.get() == null) {
            threadLocal.set(dataSource.getConnection());
        }

        return threadLocal.get();
    }

    // 省略其他方法.....
}

  然後UserService和UserDao中,恢復最初的版本,UserService和UserDao中都調用DbUtils獲取數據庫連接,此時他們獲取到的連接則是同一個Connection對象,就可以解決數據庫事務問題了。

 

三.使用場景2——日誌追蹤問題

  如果理解了場景1的數據庫事務問題,那麼對於本小節的日誌追蹤,光看標題就知道是怎麼回事了;

  開發過程時,會在項目中打很多的日誌,一般來說,查看日誌的時候,都是通過關鍵字去找日誌,這就需要我們在打日誌的時候明確的寫入某些標識,比如用戶ID、訂單號、流水號…

  如果業務比較複雜,那麼一個請求的處理流程就會比較長,如果將這麼一長串的流程給串起來,也可以通過前面說的用戶ID、訂單號、流水號來串,但有個問題,某些接口沒有用戶ID或者訂單號作為參數!!!!這個時候,雖然可以像場景1中給接口增加用戶ID或者訂單號作為參數,但是並不推薦這麼做。

  此時可以就可以使用ThreadLocal,封裝一個工具類,提供唯一標識(可以是用戶ID、訂單號、或者是分佈式全局ID),示例如下:

package cn.ganlixin.util;

/**
 * 描述:
 * 日誌追蹤工具類,設置和獲取traceId,
 * 此處的traceId使用snowFlake雪花數算法,詳情可以參考:https://www.cnblogs.com/-beyond/p/12452632.html
 *
 * @author ganlixin
 * @create 2020-06-12
 */
public class TraceUtils {
    // 創建ThreadLocal靜態屬性,存Long類型的uuid
    private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    // 全局id生成器(雪花數算法)
    private static final SnowFlakeIdGenerator generator = new SnowFlakeIdGenerator(1, 1);

    public static void setUuid(String uuid) {
        // 雪花數算法
        threadLocal.set(generator.nextId());
    }

    public static Long getUuid() {
        if (threadLocal.get() == null) {
            threadLocal.set(generator.nextId());
        }
        return threadLocal.get();
    }
}

  

  使用示例:

@Slf4j
public class UserService {

    private UserDao userDao;

    public void deleteUserInfo(Integer id, String operator) {
        log.info("traceId:{}, id:{}, operator:{}", TraceUtils.getUuid(), id, operator);
        
        //.....
    }
}

@Slf4j
public class UserDao {

    public void deleteUserById(Connection connection, Integer id) {
        log.info("traceId:{}, id:{}", TraceUtils.getUuid(), id);
    }
}

  

 四.其他場景

  其他場景,其實就是利用ThreadLocal“線程私有且線程間互不影響”特性,除了上面的兩個場景,常見的還有用來記錄用戶的登錄狀態(當然也可以用session或者cookie實現)。

 

  原文地址:https://www.cnblogs.com/-beyond/p/13111015.html 

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

【其他文章推薦】

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

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

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

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

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

redis 主從複製

redis 主從複製

master 節點提供數據,也就是寫。slave 節點負責讀。

不是說master 分支不能讀數據,也能只是我們希望將讀寫進行分離。

slave 是不能寫數據的,只能處理讀請求

主從實現

客戶端 127.0.0.1:6379 服務器 212.64.89.173:6379

方式一

客戶端發送請求同步命令

slaveof masterip masterport

slaveof 212.64.89.173 6379

方式二

客戶端啟動服務器參數

redis-server --slaveof masterip masterport

redis-server --slaveof 212.64.89.173 6379

方式三

在客戶端的配置文件中寫入 slaveof 信息

redis.conf

slaveof 212.64.89.173 6379

注意 斷開主從鏈接方式: 客戶端執行 slaveof no one

設置鏈接密碼

server 端

服務啟動後設置

config set requierpass <password>

配置文件添加密碼

# redis.conf
requirepass <password>

client 端

命令設置密碼

auth <password>

配置文件設置密碼

masterauth <password>

啟動客戶端設置密碼

redis-cli -a <password>

建立連接

建立鏈接的過程就是希望 master 和 slave 都保有對方的 IP 和 Port。

數據同步

數據的同步分兩部分,全量同步和增量同步,在增量同步結束后,master 應當保存Slave 同步數據的位置。

複製(積壓)緩衝區

它有兩部分組成 偏移量 + 字節值

結構

創建

1 當啟動AOF 時就會創建 複製積壓緩衝區

2 當被選為 master 節點,必須創建積壓緩衝區

作用

保存所有的對數據修改或數據庫修改的指令,查詢指令不會被記錄。

數據源

所有的進入master 的對數據修改或數據庫修改的指令都會被填充到積壓緩衝區中。

偏移量

1 Master 和 Slave 都會記錄 offset 值, 每次複製都會對比offset 是否一致。如果一致,Master直接從 offset 處開始傳緩衝區數據,如果不一致,那麼Master將遵循 Slave 的offset 來傳。當然會保證命令是完整的。

2 Master 保存有多個 offset 而 Slave 僅保存自己的。

3 Master發送一次,記錄一次, Slave 接受一次記錄一次。

關於Master注意

1如果master 數據量過大,應該避免業務高峰期進行數據同步。避免造成 master 阻塞

2 數據緩衝區滿, 此時將會丟棄最早的記錄(FIFO),如果全量複製的時間開銷過大,則可能在開增量複製時候已經存在數據丟失,這會導致Master 和 Slave 數據不一致,為了保證一致性,必須開始新一輪的全量複製,完成后緩衝區又被填滿並存在丟棄,則會讓Slave進入死循環。

因此數據緩衝區要設置的大小合適(依具體情況而定)。

repl-backlog-size 1mb   # 默認的大小為 1MB

3 master 單機內存不應該佔用主機內存過多。一般的 50 ~ 70% 預留下 30% ~ 50%來進行bgsave 、 創建複製緩衝區、執行其他業務等。

關於Slave注意

1 為避免slave進行全量複製、部分複製時服務器響應阻塞或數據不同步,建議關閉此期間的對外服務。

slave-server-stale-data yes|no

2 數據同步階段,master 發送給 slave 信息可以理解 master 是 slave 的一個客戶端,主動向slave發送命令。

3 多個slave同時對master請求數據同步,master發送的RDB文件增多,會對帶寬造成巨大衝擊,如果master寬帶不足,因此數據同步需要根據業務需求,適量錯峰。

4 slave過多時,應該對拓撲結構進行調整,由一主多從結構變為樹狀結構,中間結點即是master,也是slave。但是使用樹狀結構時,因為層級越深,數據同步時延越大,因此將強一致性的數據放在頂層節點,一致性稍弱的數據放在靠底層的節點。

命令傳播

當master數據庫狀態被修改后,導致主從服務器數據庫狀態不一致,此時需要讓主從數據同步到一致的狀態,同步的動作成為命令傳播
master將接受到的數據變更命令發送給slave,slave接受命令后執行命令。

網絡閃斷閃連 忽略
短時間網絡中斷 部分複製
長時間網絡中斷 全量複製

服務器運行ID(runid)

每台服務器每次運行都會產生的身份識別碼,同一個服務器多次運行產生的runid是不一樣的。

形式:runid 由 40 個字符組成 一般是16進制的字符串

info server
run_id:409b6e9ea2e5c32958de8f365711598c98489f13

心跳機制

master

指令 PING

周期 repl-ping-slave-period 默認是 10s

作用 判斷 slave 是否在線

查詢 INFO replication 獲取最後一次 slave 連接時間間隔 lag = 0 / 1 屬於正常

slave

指令 REPLCONF{offset}

周期: 1s

作用1: 彙報自己的複製偏移量 獲取最新的數據變更指令

作用2: 判斷 master 是否存活

心跳注意事項

當 salve多數掉線 或者網絡延時過高時,master 會拒絕所有的同步信息。

min-slaves-to-write 2   # 最小的 slave 數量

min-slaves-max-lag 8  # 最長的

當 slave 的數量小於2 ,或者所有的時延都大於等於 8 時,會強 關閉 master 的血功能來停止數據同步。

Slave 的數量和延時由REPLCONF{offset} 命令確認。

完整的主從複製流程

讀寫分離

在redis主從架構中,Master節點負責處理寫請求,Slave節點只處理讀請求。對於寫請求少,讀請求多的場景,例如電商詳情頁,通過這種讀寫分離的操作可以大幅提高併發量,通過增加redis從節點的數量可以使得redis的QPS達到10W+。

負載均衡

基於主從結構,配合讀寫分離,由slave分擔master負載,並根據需求的變化,改變slave的數量,通過多個從節點分擔數據讀取負載,大大提高Redis服務器併發量與數據吞吐量

故障恢復

當master出現問題時,由slave提供服務,實現快速的故障恢復

數據冗餘

實現數據熱備份,時持久化之外的一種數據冗餘方式

高可用基石

基於主從複製,構建哨兵模式與集群,實現redis 的高可用方案。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【JAVA進階架構師指南】之五:JVM性能調優

前言

  首先給大家說聲對不起,最近屬實太忙了,白天上班,晚上加班,回家還要收拾家裡,基本每天做完所有事兒都是凌晨一兩點了,沒有精力再搞其他的了.
  好了,進入正題,讓我們來聊聊JVM篇最後一個章節—-JVM性能調優.童鞋們隨便打開一個大廠的招聘崗位JD,應該都會有JVM調優相關的描述,其實招聘方不一定要求候選人真的對JVM調優有實際調優經驗,但是至少得有思路,知道應該怎樣進行JVM層面的性能調優,說實話,知道如何進行JVM層面的性能調優的人,在面試中確實是有加分的.
  筆者在公司擔任面試官的時候,經常會看到候選人簡歷描述有JVM性能調優經驗,每當這個時候我都會問候選人一個問題,你是如何進行JVM性能調優的,很多童鞋的回答就是:噢,就是調整一下初始堆大小,新生代大小.這明顯不是筆者想要的答案,因為這根本就不叫JVM性能調優.童鞋們對號入座一下,對JVM調優僅僅是我上述說的那樣的,趕緊改一下簡歷,不要說自己會JVM性能調優.說實話,對JVM進行性能調優是對架構師的要求,甚至我敢說很多架構師都不一定有實際的JVM性能調優經驗.話不多說,讓我們進入正題,我們將從以下幾點來講解如何進行性能調優:

  • JVM性能調優的前提
  • JVM性能調優的預備知識
  • STW現象–Stop-The-World
  • 垃圾回收器的種類
  • 性能調優的目的
  • 如何進行性能調優

JVM性能調優的前提

  所有有經驗的架構師一定會有一個共識,JVM層面的性能調優一定是作為最後的調優手段,在此之前,一定要確保系統其它方面都已經做到了極致,無法再進行調優了,在這個前提下,才會考慮JVM性能調優.這裏的其他方面包括從前端到架構到代碼層面,我舉一些例子
從瀏覽器/APP角度可進行的優化有:

  • 減少HTTP請求次數.
  • 使用客戶端緩存.
  • 瀏覽器啟用壓縮
  • 使用CDN加速
  • 動態資源和靜態資源分離

從系統層面可進行的優化有:

  • 啟用緩存,比如redis緩存數據
  • 使用集群
  • 異步處理,比如引入消息隊列
  • 對代碼的優化

  以上列出的每一點,都能單獨拎出來講很久,但是博文篇幅問題,就不細說了,有興趣的童鞋可以自行下去了解,我們只着重說一下代碼優化,相信大家都知道,業界比較公認的編碼規範是阿里巴巴發布的<java開發手冊>,裏面的內容都是阿里集團多年來血的教訓積累的精華,阿里集團內部有一個組織專門負責手冊的編寫和推廣,並且在不斷進行優化,最新版發布到泰山版了(我看的時候還是華山版,哈哈).如果有童鞋還不知道的,我建議去下載下來看一看.另外阿里雲上有<java開發手冊>的考試,如果通過了這個考試,說明你的編碼規範還是不錯的,有興趣的童鞋可以去試一下.
  除此之外,童鞋們還應該了解JVM本身為我們悄悄做的各種優化,其中最重要的是JIT編譯器的優化.我舉幾個例子,比如:方法內聯,逃逸分析等.默認情況下,這些都是開啟的,如果不開啟這些功能,JVM性能會下降50%以上.除此之外,還有一些比如:棧上分配,TLAB等優化.這些內容由於平時我們開發中不會用到,是JVM在背後悄悄為我們做了優化,因此可能很多童鞋都不知道,但是如果想成為一個合格的架構師,這些內容都是必須要知道的,畢竟架構師的知識廣度和深度決定了架構師的高度.
  除此之外,性能調優一定是基於性能測試的,空口說進行性能調優的都是耍流氓,只有在經過了實際的性能測試后,我們才知道系統的瓶頸在哪裡,才知道那些方面需要進行調優,如何進行調優.常用的性能測試指標有TPS/QPS/吞吐量.並且默認所有的接口訪問都遵循二八原則(接口每天80%的訪問量集中在20%的時間內).

JVM性能調優的預備知識

  在進行JVM性能調優之前,我們還得了解JVM,比如我前面的幾篇有關JVM的博文,都是需要掌握的,比如JVM內存模型,垃圾回收機制等等.另外我們還需要掌握一些進行JVM分析的工具.其實在我們安裝JDK的時候,JDK已經為我們準備了許多有用的性能調優監控工具,我們可以看一下JDK安裝目錄下的bin目錄:

  • jps: 主要用來輸出JVM中運行的進程狀態信息
  • jstack: 主要用來查看某個Java進程內的線程堆棧信息
  • jmap: 用來查看堆內存使用狀況,一般結合jhat使用
  • jstat: JVM統計監測工具
  • jconsole: 圖形化的統計工具
  • jvisualvm: 比jconsole功能更強的圖形化的監控工具

  另外我們需要知道,當JVM發生OOM異常時,可以使用命令生成一個.hprof後綴的dump文件,該文件是某個時間節點的heap的快照,我們可以使用VisualVM來查看,也可以用第三方提供的一些工具,比如eclipse的MAT來查看內存溢出的原因.

STW現象

  所謂STW現象(Stop-The-World)是指在執行垃圾收集算法時,Java應用程序的其他所有線程(除了垃圾收集線程之外的線程)都被掛起.此時,系統只允許GC線程繼續運行,其他線程全部暫停,等待GC線程執行完畢后才能繼續執行.這些工作都是由虛擬機在後台自動發起和自動完成的,是在用戶不可見的情況下把用戶正常工作的線程全部停掉,舉個例子,某個接口平時可能只需要50ms的RT,忽然某次調用花費了200ms.因此STW對實時性要求很高的系統來說是難以接受的.

垃圾回收器的種類

  既然有STW現象,那麼有沒有解決方案呢?這就是我們接下來要講的,垃圾回收器的種類,目前為止,JVM一共為我們提供了七種垃圾回收器,其中年輕代有三種,老年代有三種,另外還有一種特殊的G1:

  當然,在更新版本的JDK中還有一種ZGC,為未來垃圾回收器提供了一種趨勢,有興趣的童鞋可以自行了解.

性能調優的目的和具體過程

  有了前面的鋪墊,終於來到了我們最重要的正題:如何進行JVM性能調優?在我看來JVM調優的具體步驟分為如下幾步:

  • 確定調優的目的,選擇合適的GC collector
  • 調整JVM heap的大小
  • 調整young generation在整個JVM heap中所佔的比重.

確定調優的目的,選擇合適的GC collector

  由於每種垃圾回收器的特性並不相同,因此我們需要根據我們的調優目的選擇合適的垃圾回收器,比如,我們需要降低STW的停頓時間,那我們就不能選用串行和并行的垃圾回收器,而應該選用併發的垃圾回收器,即CMS,與之搭配的新生代垃圾回收器就應該選用ParNew.目前一般主流互聯網公司都是用CMS垃圾回收器.

調整JVM heap的大小

  確定了垃圾回收器的類型,就需要調整JVM heap的大小,在這一步的時候,首先我們需要了解JVM相關的一些指令,比如可以在啟動java程序時加上
-XX:+HeapDumpOnOutOfMemoryError,當發生OOM時,JVM會自動為我們生成DUMP文件
-XX:+PrintGCDetails -Xloggc:D:\gclogger\gc.log -XX:+PrintGCDateStamps 生成GC日誌
-Xms2g -Xmx2g 當然,還應該要根據實際情況設置heap的最大最小值,童鞋們要知道,默認情況下,java程序啟動的最小heap大小為1/64物理內存,最大值為1/4物理內存,一般要求我們最大最小值保持一致,避免JVM頻繁擴容和縮容導致不必要的性能浪費.

調整young generation在整個JVM heap中所佔的比重.

  確定了heap的大小,還需要確定新生代的比重
–Xmn1500m -XX:MetaspaceSize=150M

  當然,再厲害的架構師也不可能一次就調整得出最佳的JVM配置參數,而是應該多設置幾組不同的值,放到生產環境(或者和生產環境一樣的環境,比如阿里內部有預發環境,和生產環境保持一致)進行性能測試,通過對比結果得出最佳的JVM性能調優參數,完成JVM性能調優.

總結

  讀完了本篇文章,我相信童鞋們應該或多或少會有些收穫.掌握JVM調整的核心步驟:

  • 確定調優的目的,選擇合適的GC collector
  • 調整JVM heap的大小
  • 調整young generation在整個JVM heap中所佔的比重.
      不了解如何進行JVM的調優的人,把本文內容好好理解后,能讓面試官刮目相看;了解JVM調優,但是條理不清晰的童鞋,可能會對JVM調優有更清晰的認識.總而言之,筆者認為本文是一篇滿滿的乾貨,網上許多講JVM調優的博文,並沒有這麼系統的講解過真正應該如何進行JVM調優.當然筆者能力有限,如文章有錯誤,歡迎指正,畢竟是人就會犯錯.
      PS:一入JVM深似海,從此再也出不來.對JVM有興趣的童鞋,可以鑽研,但是在經驗不足之前,不建議太過深入了解JVM,否則會耽誤自己.當然,立志要成為一名牛逼的架構師,這些都是必須要會的.
      本文我們講完了JVM,下一篇開始,讓我們繼續學習JAVA鎖相關的內容.
      如果覺得博主寫的不錯,歡迎關注博主微信公眾號,博主會不定期分享技術乾貨!

本文由博客一文多發平台 OpenWrite 發布!

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

小師妹學JavaIO之:NIO中那些奇怪的Buffer

目錄

  • 簡介
  • Buffer的分類
  • Big Endian 和 Little Endian
  • aligned內存對齊
  • 總結

簡介

妖魔鬼怪快快顯形,今天F師兄幫助小師妹來斬妖除魔啦,什麼BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU統統給你剖析個清清楚楚明明白白。

Buffer的分類

小師妹:F師兄不都說JDK源碼是最好的java老師嗎?為程不識源碼,就稱牛人也枉然。但是我最近在學習NIO的時候竟然發現有些Buffer類居然沒有註釋,就那麼突兀的寫在哪裡,讓人好生心煩。

更多內容請訪問www.flydean.com

居然還有這樣的事情?快帶F師兄去看看。

小師妹:F師兄你看,以ShortBuffer為例,它的子類怎麼後面都帶一些奇奇怪怪的字符:

什麼什麼BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU都來了,點進去看他們的源碼也沒有說明這些類到底是做什麼的。

還真有這種事情,給我一個小時,讓我仔細研究研究。

一個小時后,小師妹,經過我一個小時的辛苦勘察,結果發現,確實沒有官方文檔介紹這幾個類到底是什麼含義,但是師兄我掐指一算,好像發現了這些類之間的小秘密,且聽為兄娓娓道來。

之前的文章,我們講到Buffer根據類型可以分為ShortBuffer,LongBuffer,DoubleBuffer等等。

但是根據本質和使用習慣,我們又可以分為三類,分別是:ByteBufferAsXXXBuffer,DirectXXXBuffer和HeapXXXBuffer。

ByteBufferAsXXXBuffer主要將ByteBuffer轉換成為特定類型的Buffer,比如CharBuffer,IntBuffer等等。

而DirectXXXBuffer則是和虛擬內存映射打交道的Buffer。

最後HeapXXXBuffer是在堆空間上面創建的Buffer。

Big Endian 和 Little Endian

小師妹,F師兄,你剛剛講的都不重要,我就想知道類後面的B,L,R,S,U是做什麼的。

好吧,在給你講解這些內容之前,師兄我給你講一個故事。

話說在明末浙江才女吳絳雪寫過一首詩:《春 景 詩》

鶯啼岸柳弄春晴,
柳弄春晴夜月明。
明月夜晴春弄柳,
晴春弄柳岸啼鶯。

小師妹,可有看出什麼特異之處?最好是多讀幾遍,讀出聲來。

小師妹:哇,F師兄,這首詩從頭到尾和從尾到頭讀起來是一樣的呀,又對稱又有意境!

不錯,這就是中文的魅力啦,根據讀的方式不同,得出的結果也不同,其實在計算機世界也存在這樣的問題。

我們知道在java中底層的最小存儲單元是Byte,一個Byte是8bits,用16進製表示就是Ox00-OxFF。

java中除了byte,boolean是佔一個字節以外,好像其他的類型都會佔用多個字節。

如果以int來舉例,int佔用4個字節,其範圍是從Ox00000000-OxFFFFFFFF,假如我們有一個int=Ox12345678,存到內存地址裏面就有這樣兩種方式。

第一種Big Endian將高位的字節存儲在起始地址

第二種Little Endian將地位的字節存儲在起始地址

其實Big Endian更加符合人類的讀寫習慣,而Little Endian更加符合機器的讀寫習慣。

目前主流的兩大CPU陣營中,PowerPC系列採用big endian方式存儲數據,而x86系列則採用little endian方式存儲數據。

如果不同的CPU架構直接進行通信,就由可能因為讀取順序的不同而產生問題。

java的設計初衷就是一次編寫處處運行,所以自然也做了設計。

所以BufferB表示的是Big Endian的buffer,BufferL表示的是Little endian的Buffer。

而BufferRB,BufferRL表示的是兩種只讀Buffer。

aligned內存對齊

小師妹:F師兄,那這幾個又是做什麼用的呢? BufferS,BufferU,BufferRS,BufferRU。

在講解這幾個類之前,我們先要回顧一下JVM中對象的存儲方式。

還記得我們是怎麼使用JOL來分析JVM的信息的嗎?代碼非常非常簡單:

log.info("{}", VM.current().details());

輸出結果:

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

上面的輸出中,我們可以看到:Objects are 8 bytes aligned,這意味着所有的對象分配的字節都是8的整數倍。

再注意上面輸出的一個關鍵字aligned,確認過眼神,是對的那個人。

aligned對齊的意思,表示JVM中的對象都是以8字節對齊的,如果對象本身佔用的空間不足8字節或者不是8字節的倍數,則補齊。

還是用JOL來分析String對象:

[main] INFO com.flydean.JolUsage - java.lang.String object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     4    byte[] String.value                              N/A
     16     4       int String.hash                               N/A
     20     1      byte String.coder                              N/A
     21     1   boolean String.hashIsZero                         N/A
     22     2           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

可以看到一個String對象佔用24字節,但是真正有意義的是22字節,有兩個2字節是補齊用的。

對齊的好處顯而易見,就是CPU在讀取數據的時候更加方便和快捷,因為CPU設定是一次讀取多少字節來的,如果你存儲是沒有對齊的,則CPU讀取起來效率會比較低。

現在可以回答部分問題:BufferU表示是unaligned,BufferRU表示是只讀的unaligned。

小師妹:那BufferS和BufferRS呢?

這個問題其實還是很難回答的,但是經過師兄我的不斷研究和探索,終於找到了答案:

先看下DirectShortBufferRU和DirectShortBufferRS的區別,兩者的區別在兩個地方,先看第一個Order:

DirectShortBufferRU:

public ByteOrder order() {
        return ((ByteOrder.nativeOrder() != ByteOrder.BIG_ENDIAN)
                ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
    }
DirectShortBufferRS:

public ByteOrder order() {
        return ((ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN)
                ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
    }

可以看到DirectShortBufferRU的Order是跟nativeOrder是一致的。而DirectShortBufferRS的Order跟nativeOrder是相反的。

為什麼相反?再看兩者get方法的不同:

DirectShortBufferU:

public short get() {
        try {
            checkSegment();
            return ((UNSAFE.getShort(ix(nextGetIndex()))));
        } finally {
            Reference.reachabilityFence(this);
        }
    }
DirectShortBufferS:

public short get() {
        try {
            checkSegment();
            return (Bits.swap(UNSAFE.getShort(ix(nextGetIndex()))));
        } finally {
            Reference.reachabilityFence(this);
        }
    }

區別出來了,DirectShortBufferS在返回的時候做了一個bits的swap操作。

所以BufferS表示的是swap過後的Buffer,和BufferRS表示的是只讀的swap過後的Buffer。

總結

不寫註釋實在是害死人啊!尤其是JDK自己也不寫註釋的情況下!

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/java-io-nio-kinds-of-buffer/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

計算機網絡之傳輸層

傳輸層概述

  從信息處理得角度上去看,傳輸層主要是給上面得應用層提供通信服務得。我們平時再對網絡進行編程得時候,我們很多時候都是直接對接得傳輸層,也就是我們使用傳輸層所提供得接口來進行網絡編程,所以我們常說傳輸層是用戶功能得最底層,是面向通信部份得最高層。

  傳輸層負責將上層數據分段並提供端到端的、可靠的或不可靠的傳輸以及端到端的差錯控制和流量控制問題;包含的主要協議:TCP協議(Transmission Control Protocol,傳輸控制協議)、UDP協議(User Datagram Protocol,用戶數據報協議)。

UDP協議

  UDP(User Datagram Protocol: 用戶數據報協議),是一個非常簡單的協議,它對接收到的數據報不合併也不拆分,如下圖:

UDP協議格式:

16位源端口號 16位目的端口號
16位UDP長度 16位UDP校驗和
UDP數據

  UDP協議的特點:UDP是無連接協議;UDP不能保證可靠的交付數據,即UDP協議不會感知網絡是否擁塞,UDP協議不管網絡是否擁塞,都會把數據交付出去,給這個網絡就完了。無法保證數據在網絡中是否丟失;UDP是面向報文傳輸的;UDP沒有擁塞控制,而且UDP首部開銷很小。

TCP協議

  TCP(Transmission Control Protocol: 傳輸控制協議),是計算機網絡中非常複雜的一個協議。

  TCP協議特點:TCP是面向連接的協議;TCP的一個連接有兩端(點對點通信);TCP提供可靠的傳輸服務;TCP協議提供全雙工的通信;TCP是面向字節流的協議。

TCP協議格式:

16位源端口 16位目的端口
序號
確認號
數據偏移 保留字段 TCP標記 窗口
校驗和 緊急指針
TCP選項 填充

序號:0~\(2^{32}\)-1,一個字節一個序號, 數據首字節序號
確認號:0~\(2^{32}\)-1,一個字節一個序號, 期望收到數據的首字節序號。確認號為N:則表示N-1序號的數據都已經收到。
數據偏移:佔4位:0~15,單位為:32位字,數據偏離首部的距離。
TCP標記:佔6位,每位各有不同意義

標記 含義
URG Urgent: 緊急位,URG=1,表示緊急數據
ACK Acknowledgement: 確認位,ACK=1,確認號才生效
PSH Push: 推送位,PSH=1,儘快地把數據交付給應用層
RST Reset: 重置位,RST=1,重新建立連接
SYN Synchronization: 同步位,SYN=1 表示連接請求報文
FIN Finish: 終止位,FIN=1 表示釋放連接

窗口:佔16位:0~\(2^{16}\)-1,窗口指明允許對方發送的數據量
緊急指針:緊急數據(URG=1),指定緊急數據在報文的位置
TCP選項:最多40字節,支持未來的拓展

可靠傳輸的基本原理

  • 停止等待協議:發送方生成新的消息,發送給接收方,並且此時不會產生新的消息,需要收到接受方的確認消息后,才會產生新的消息。超時重傳:如果發送方的消息在傳輸的過程種丟失了,接收方沒有收到消息,就會進行超時重傳;如果接收方發送的確認消息,在傳輸的過程中丟失,也會進行超時重傳,因此 每發送一個消息,都需要設置一個定時器。停止等待協議是最簡單的可靠傳輸協議,但停止等待協議對信道的利用效率不高。
  • 連續ARQ協議:ARQ(Automatic Repeat reQuest:自動重傳請求),由於單個發送和確認效率低,我們可以通過批量發送和確認來提升效率。

  TCP的可靠傳輸基於連續ARQ協議,TCP的滑動窗口以字節為單位,窗口滑動過程如下圖:

如果接收到的序號沒有按序收到確認號,在超時時間內就會進行重新傳送,如下圖:

為了避免對整個窗口中的字節進行重傳,因此TCP協議使用了選擇重傳來提高傳輸效率。選擇重傳:重傳的是一段字節流,而不是某個字節,在TCP選項里存儲的是需要重傳的字節流的邊界。選擇重傳需要指定需要重傳的字節,每一個字節都有唯一的32位序號。

TCP協議的流量控制

   流量控制指讓發送方發送速率不要太快,是使用滑動窗口來實現的,即通過窗口大小控制對方發送速率。當接收到窗口為0的消息,則啟動堅持定時器,堅持定時器每隔一段時間發送一個窗口探測報文。

TCP協議的擁塞控制

  一條數據鏈路經過非常多的設備,數據鏈路中各個部分都有可能成為網路傳輸的瓶頸。流量控制考慮點對點的通信量的控制,擁塞控制考慮整個網絡,是全局性的考慮。如何判斷是否發生了網絡擁塞?根據報文超時來判斷髮生了擁塞是不成立的,如果我們在傳輸的過程中,把光纖或者網絡斷了,這個時候也會導致報文超時,但這是因為網絡故障造成的
  慢啟動算法: 由小到大逐漸增加發送數據量,每收到一個報文確認,就加一。例如:發送的數據量以此為:1 2 4 8 16…,是指數增長的。當使用慢啟動算法增長到慢啟動閾值時,就會使用擁塞避免算法;擁塞避免算法:維護一個擁塞窗口的變量,只要網絡不擁塞,就試探着擁塞窗口調大,如1 2 4 8 16 17 18 19。

TCP連接的建立

標記 含義
URG Urgent: 緊急位,URG=1,表示緊急數據
ACK Acknowledgement: 確認位,ACK=1,確認號才生效
PSH Push: 推送位,PSH=1,儘快地把數據交付給應用層
RST Reset: 重置位,RST=1,重新建立連接
SYN Synchronization: 同步位,SYN=1 表示連接請求報文
FIN Finish: 終止位,FIN=1 表示釋放連接

TCP三次握手的過程:

  1. 第一次握手:建立連接時,客戶端(發送方)發送syn包(seq=j)到服務器,並進入SYN_SENT狀態,等待服務器(接收方)確認;
  2. 第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(seq=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態。
  3. 第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED(TCP連接成功)狀態,完成三次握手

  在客戶端與服務器端傳輸的TCP報文中,雙方的確認號Ack和序號Seq的值,都是在彼此Ack和Seq值的基礎上進行計算的,這樣做保證了TCP報文傳輸的連貫性。一旦出現某一方發出的TCP報文丟失,便無法繼續”握手”,以此確保了”三次握手”的順利完成。此後客戶端和服務器端進行正常的數據傳輸。這就是“三次握手”的過程。

為什麼發送方要發出第三個確認報文呢?

  • 為了避免已經失效的連接請求報文傳送到對方,引起錯誤

TCP連接的釋放

TCP四次揮手的過程:TCP連接斷開過程:假設Client端發起中斷連接請求,也就是發送FIN報文。Server端接到FIN報文後,意思是說”我Client端沒有數據要發給你了”,但是如果你還有數據沒有發送完成,則不必急着關閉Socket,可以繼續發送數據。所以你先發送ACK,”告訴Client端,你的請求我收到了,但是我還沒準備好,請繼續你等我的消息”。這個時候Client端就進入FIN_WAIT狀態,繼續等待Server端的FIN報文。當Server端確定數據已發送完成,則向Client端發送FIN報文,”告訴Client端,好了,我這邊數據發完了,準備好關閉連接了”。Client端收到FIN報文後,”就知道可以關閉連接了,但是他還是不相信網絡,怕Server端不知道要關閉,所以發送ACK後進入TIME_WAIT狀態,如果Server端沒有收到ACK則可以重傳。”,Server端收到ACK后,”就知道可以斷開連接了”。Client端等待了2MSL后依然沒有收到回復,則證明Server端已正常關閉,那好,我Client端也可以關閉連接了。Ok,TCP連接就這樣關閉了!

  MSL(Max Segment Lifetime): 最長報文段壽命,MSL建議設置為2分鐘。為什麼需要等待2MSL?其實在釋放連接的過程中,客戶端最後一次發送的報文,服務端是沒有確認的,為了確保發送方的ACK可以達到接收方,如果2MSL時間內沒有收到,則接收方會重發。這也是等待計時器的作用,主要是為了確保發送方發送的第四次揮手報文可以正確的到達接收方,如果沒有到達的話,接收方就會重新放鬆第三次揮手的報文,以正確得到釋放這次連接。等待計時器的另一個作用就是確保當前連接的所有報文都已經過期。

為什麼關閉連接需要四次揮手呢?
  這是因為服務端的LISTEN狀態下的SOCKET當收到SYN報文的建連請求后,它可以把ACK和SYN(ACK起應答作用,而SYN起同步作用)放在一個報文里來發送。但關閉連接時,當收到對方的FIN報文通知時,它僅僅表示對方沒有數據發送給你了;但未必你所有的數據都全部發送給對方了,所以你可能未必會馬上會關閉SOCKET,也即你可能還需要發送一些數據給對方之後,再發送FIN報文給對方來表示你同意現在可以關閉連接了,所以它這裏的ACK報文和FIN報文多數情況下都是分開發送的。

傳輸層總結:第一個端到端,即主機到主機的層次。傳輸層負責將上層數據分段並提供端到端的、可靠的或不可靠的傳輸。此外,傳輸層還要處理端到端的差錯控制和流量控制問題。 傳輸層的任務是根據通信子網的特性,最佳的利用網絡資源,為兩個端系統的會話層之間,提供建立、維護和取消傳輸連接的功能,負責端到端的可靠數據傳輸。在這一層,信息傳送的協議數據單元稱為段或報文。 網絡層只是根據網絡地址將源結點發出的數據包傳送到目的結點,而傳輸層則負責將數據可靠地傳送到相應的端口。 有關網絡層的重點:

  • 傳輸層負責將上層數據分段並提供端到端的、可靠的或不可靠的傳輸以及端到端的差錯控制和流量控制問題;
  • 包含的主要協議:TCP協議(Transmission Control Protocol,傳輸控制協議)、UDP協議(User Datagram Protocol,用戶數據報協議);
  • 重要設備:網關。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

這不是紀念品!法遊客欲帶走1.8公斤沙子 挨罰逾3萬

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

法國1名遊客日前疑將4磅重(約1.8公斤)的當地海灘沙子裝在行李中的瓶子,試圖帶離薩丁尼亞島,被查獲後於卡利亞里埃爾馬斯機場被捕,據稱被罰1000歐元(約新台幣3萬4558元)。

《CNN》報導,薩丁尼亞島2017年起規定,從島上取沙屬非法行為,若帶走恐面臨罰款甚至入獄。當地森林保護員發言人表示,該遊客裝紗的瓶子已沒收,並指祭出罰則是因類似情形層出不窮,尤其是針對粉紅、白色等顏色特殊的沙子。據稱,有網站將島上沙子作為紀念品出售。

報導指出,該發言人提及,罰金落在500至3000歐元(約新台幣1萬7275至10萬3649元)區間,視取走來源與數量決定,過去3年,管制、裁罰變得更加嚴格。除了會與警方合作,也接受民眾檢舉。

海洋
環境新聞
國際新聞
法國
海灘
保護海洋

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

【其他文章推薦】

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

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

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

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

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

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