开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

呕心沥血!3月整理233Python面试题,助你4月一举通关

发表于 2021-03-31

前言:

在过去的2020年,Python赢得了年度TIOBE编程语言奖,成为过去一年最受欢迎的编程语言。在数据科学和机器学习等领域中,被广泛使用。

正值“金三银四”招聘季,小编给大家整理了233道Python面试题,还有相对应的答案,来帮助大家更好的了解和学习Python。

image

请注意:鹅厂大佬 熬夜吐血整理最新Python面试题,后台私信小编“01’ 免费领取 即可

《233道python高频面试+解析》做了个分类,总共包含以下几个部分的内容

正值春招时期,小编熬夜整理的 233道大致包含(python基础、高级、web开发、架构等)

详细目录如下图所示,小编整理不易,麻烦各位认可的小伙伴 点个赞 评论+转发 感谢

image

欢迎大家交流学习。
如果真的遇到好的同事,那算你走运,加油,抓紧学到手。
python资源分享QQ群:766610200
包含pythonx, 爬虫、人工智能、大数据 软件,以及pythonweb、自动化、人工智能等python编程的学习方法。
打造从零基础到项目开发上手实战全方位解析!
点击加入我们的 python资料分享集地

Python基础

image

Python高级

image

Python web开发

image

Python 爬虫

image

Python数据库

image

Python测试+数据结构+大数据+架构

image

由于题目过多,篇幅有限,小编这边以图片的形式展现给大家看,然后小编这边已准备好完整版的pdf 如有需要的小伙伴 请关注下 免费领取的方式:

———转发+点赞 并私信小编@python教程入门学习 口令为【01】

希望能够帮助到更多正在学习Python的小伙伴们,笔芯~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Ubuntu18041上安装k8s120的简明教程 安

发表于 2021-03-31

安装 kubernetes

k8s官网

1
2
3
bash复制代码#查看系统
honglei@k8s-master-ubuntu:~$ uname -a
Linux k8s-master-ubuntu 5.4.0-58-generic #64~18.04.1-Ubuntu SMP Wed Dec 9 17:11:11 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

准备开始

  • 一台或多台运行着下列系统的机器:
    • Ubuntu 16.04+
    • Debian 9+
    • CentOS 7
    • Red Hat Enterprise Linux (RHEL) 7
    • Fedora 25+
    • HypriotOS v1.0.1+
    • Container Linux (测试 1800.6.0 版本)
  • 每台机器 2 GB 或更多的 RAM (如果少于这个数字将会影响您应用的运行内存)
  • 2 CPU 核或更多
  • 集群中的所有机器的网络彼此均能相互连接(公网和内网都可以)
  • 节点之中不可以有重复的主机名、MAC 地址或 product_uuid。请参见这里 了解更多详细信息。
  • 开启机器上的某些端口。请参见这里 了解更多详细信息。
  • 禁用交换分区。为了保证 kubelet 正常工作,您 必须 禁用交换分区。

关闭防火墙、swap

需要关闭所有主机的防火墙

1
bash复制代码sudo ufw disable

需要关闭所有主机的关闭 swap

Kubernetes 1.8开始要求必须禁用Swap,如果不关闭,默认配置下kubelet将无法启动。将 /etc/fstab 中的这一行注释掉

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码root@master:/etc/docker# cat /etc/fstab
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point> <type> <options> <dump> <pass>
# / was on /dev/sda1 during installation
UUID=d1cc0144-5acb-4e88-95f5-965ce4d0bc30 / ext4 errors=remount-ro 0 1
#/swapfile none swap sw 0 0
/dev/fd0 /media/floppy0 auto rw,user,noauto,exec,utf8 0 0
1
2
bash复制代码#执行
sudo swapoff -a

Ubuntu中的root帐号

Ubuntu中的root帐号默认是被禁用了的,所以登陆的时候没有这个账号

打开终端开启root账户

1
2
3
4
5
6
7
bash复制代码sudo passwd -u root
#设置root密码,输入两次
sudo passwd root
#切换root账号
su -
#退出root账户
exit

确保每个节点上 MAC 地址和 product_uuid 的唯一性

  • 您可以使用命令 ip link 或 ifconfig -a 来获取网络接口的 MAC 地址
  • 可以使用 sudo cat /sys/class/dmi/id/product_uuid 命令对 product_uuid 校验

一般来讲,硬件设备会拥有唯一的地址,但是有些虚拟机的地址可能会重复。Kubernetes 使用这些值来唯一确定集群中的节点。 如果这些值在每个节点上不唯一,可能会导致安装失败。

检查网络适配器

如果您有一个以上的网络适配器,同时您的 Kubernetes 组件通过默认路由不可达,我们建议您预先添加 IP 路由规则,这样 Kubernetes 集群就可以通过对应的适配器完成连接。

确保 iptables 工具不使用 nftables 后端

在 Linux 中,nftables 当前可以作为内核 iptables 子系统的替代品。 iptables 工具可以充当兼容性层,其行为类似于 iptables 但实际上是在配置 nftables。 nftables 后端与当前的 kubeadm 软件包不兼容:它会导致重复防火墙规则并破坏 kube-proxy。

如果您系统的 iptables 工具使用 nftables 后端,则需要把 iptables 工具切换到“旧版”模式来避免这些问题。 默认情况下,至少在 Debian 10 (Buster)、Ubuntu 19.04、Fedora 29 和较新的发行版本中会出现这种问题。RHEL 8 不支持切换到旧版本模式,因此与当前的 kubeadm 软件包不兼容。

1
2
3
4
bash复制代码update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
update-alternatives --set arptables /usr/sbin/arptables-legacy
update-alternatives --set ebtables /usr/sbin/ebtables-legacy

修改主机名、更新host

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码sudo vi /etc/hostname
#修改完hostname以后,建议同时修改下/etc/hosts:
sudo vi /etc/hosts
#/etc/hosts存放的是域名和ip的对应关系,域名和主机名并没有直接关系,可以为一个ip指定任意对应名称,但是建议解析一个ip对应hostname。

#在 master 的 /etc/hosts 中进行如下修改:
192.168.39.3 master
192.168.39.4 node1
192.168.39.5 node2
#在 node1 的 /etc/hosts 中进行如下修改:
192.168.39.3 master
192.168.39.4 node1
192.168.39.5 node2
#在 node2 的 /etc/hosts 中进行如下修改:
192.168.39.3 master
192.168.39.4 node1
192.168.39.5 node2

安装 runtime

从 v1.6.0 版本起,Kubernetes 开始默认允许使用 CRI(容器运行时接口)。

从 v1.14.0 版本起,kubeadm 将通过观察已知的 UNIX 域套接字来自动检测 Linux 节点上的容器运行时。 下表中是可检测到的正在运行的 runtime 和 socket 路径。

运行时 域套接字
Docker /var/run/docker.sock
containerd /run/containerd/containerd.sock
CRI-O /var/run/crio/crio.sock

如果同时检测到 docker 和 containerd,则优先选择 docker。 这是必然的,因为 docker 18.09 附带了 containerd 并且两者都是可以检测到的。 如果检测到其他两个或多个运行时,kubeadm 将以一个合理的错误信息退出。

在非 Linux 节点上,默认使用 docker 作为容器 runtime。

如果选择的容器 runtime 是 docker,则通过内置 dockershim CRI 在 kubelet 的内部实现其的应用。

基于 CRI 的其他 runtimes 有:

  • containerd (containerd 的内置 CRI 插件)
  • cri-o
  • frakti

请参考 CRI 安装指南获取更多信息。

安装Docker

在你每个节点(所有主机)上安装 Docker CE.

Kubernetes 发行说明中列出了 Docker 的哪些版本与该版本的 Kubernetes 相兼容。

在你的操作系统上使用如下命令安装 Docker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
bash复制代码# (安装 Docker CE)
## 设置仓库:
### 安装软件包以允许 apt 通过 HTTPS 使用存储库
sudo apt-get update && sudo apt-get install -y \
apt-transport-https ca-certificates curl software-properties-common gnupg2

### 新增 Docker 的官方 GPG 秘钥:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
#如果上面的地址不能下载,可以使用国内镜像
### 新增 Docker 的 中国科学技术大学 GPG 秘钥:
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

### 添加 Docker apt 仓库:
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"

sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"

## 安装 Docker CE
sudo apt-get update && sudo apt-get install -y \
containerd.io=1.2.13-2 \
docker-ce=5:19.03.11~3-0~ubuntu-$(lsb_release -cs) \
docker-ce-cli=5:19.03.11~3-0~ubuntu-$(lsb_release -cs)
#或者下面的命令安装也是一样的。
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

#kubernetes 官方建议 docker 驱动采用 systemd,当然可以不修改,只是kubeadm init时会有warning提示
# 设置 Docker daemon
cat <<EOF | sudo tee /etc/docker/daemon.conf
{
"registry-mirrors": [
"https://dr6xf1z7.mirror.aliyuncs.com",
],
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}
EOF

# Create /etc/systemd/system/docker.service.d
sudo mkdir -p /etc/systemd/system/docker.service.d

# 重启 docker.
sudo systemctl daemon-reload
sudo systemctl restart docker
#查看信息
docker version
docker info | grep "Cgroup Driver"

如果你想开机即启动 docker 服务,执行以下命令:

1
bash复制代码sudo systemctl enable docker

请参阅 官方 Docker 安装指南 来获取更多的信息。

安装 kubeadm、kubelet 和 kubectl

您需要在每台机器上安装以下的软件包:

  • kubeadm:用来初始化集群的指令。
  • kubelet:在集群中的每个节点上用来启动 pod 和容器等。
  • kubectl:用来与集群通信的命令行工具。

kubeadm 不能 帮您安装或者管理 kubelet 或 kubectl,所以您需要确保它们与通过 kubeadm 安装的控制平面的版本相匹配。 如果不这样做,则存在发生版本偏差的风险,可能会导致一些预料之外的错误和问题。 然而,控制平面与 kubelet 间的相差一个次要版本不一致是支持的,但 kubelet 的版本不可以超过 API 服务器的版本。 例如,1.7.0 版本的 kubelet 可以完全兼容 1.8.0 版本的 API 服务器,反之则不可以。

有关安装 kubectl 的信息,请参阅安装和设置 kubectl文档。

警告:

这些指南不包括系统升级时使用的所有 Kubernetes 程序包。这是因为 kubeadm 和 Kubernetes 有特殊的升级注意事项。

添加阿里源

由于国外网站访问较慢容易卡死,所以需要添加源。

在 /etc/apt/sources.list 中添加:

1
2
bash复制代码vi /etc/apt/sources.list
deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main

添加源的 key :

1
bash复制代码curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -

更新源:

1
bash复制代码sudo apt-get update

开始安装

1
2
3
4
5
6
bash复制代码sudo apt-get install -y kubelet kubeadm kubectl

#查看版本号:
root@master:/etc/docker# kubelet --version
Kubernetes v1.20.0
root@master:/etc/docker#

启动 kubelet:

1
2
3
bash复制代码systemctl start kubelet
# 如果你想开机即启动 docker 服务,执行以下命令
sudo systemctl enable kubelet

部署(master)节点

查看 kubernetes 所需镜像列表

1
2
3
4
5
6
7
8
9
bash复制代码#取最新版所需镜像列表
root@master:/etc/docker# kubeadm config images list
k8s.gcr.io/kube-apiserver:v1.20.1
k8s.gcr.io/kube-controller-manager:v1.20.1
k8s.gcr.io/kube-scheduler:v1.20.1
k8s.gcr.io/kube-proxy:v1.20.1
k8s.gcr.io/pause:3.2
k8s.gcr.io/etcd:3.4.13-0
k8s.gcr.io/coredns:1.7.0

执行 init

1
2
3
4
bash复制代码kubeadm init --kubernetes-version=v1.20.0 --image-repository registry.aliyuncs.com/google_containers --pod-network-cidr=10.244.0.0/16
#当出现如下内容时**(要保存下来,后边有用)**,执行成功:
kubeadm join 192.168.39.3:6443 --token 973slm.bl1aa33bx0wns5sj \
--discovery-token-ca-cert-hash sha256:9ca9b8eb33e291f83acc2245d919443431692be5ab8527bd8cf58f57c5a18be5

失败重试

若执行失败,可执行如下命令,清除执行 init 产生的垃圾

1
2
bash复制代码kubeadm reset
rm -rf /etc/kubernetes

root 用户

Alternatively, if you are the root user, you can run:

1
bash复制代码export KUBECONFIG=/etc/kubernetes/admin.conf

非root用户操作 kubectl

部署成功后,如果我们想使用非root用户操作 kubectl,可以使用以下命令,这也是 kubeadm init 输出的一部分

不然会报: The connection to the server localhost:8080 was refused

1
2
3
bash复制代码mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

node节点执行join

在node节点执行命令

1
2
bash复制代码kubeadm join 192.168.39.3:6443 --token 973slm.bl1aa33bx0wns5sj \
--discovery-token-ca-cert-hash sha256:9ca9b8eb33e291f83acc2245d919443431692be5ab8527bd8cf58f57c5a18be5

join过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码root@node2:/# kubectl get nodes
The connection to the server localhost:8080 was refused - did you specify the right host or port?
root@node2:/etc/apt# kubeadm join 192.168.39.3:6443 --token 973slm.bl1aa33bx0wns5sj \
> --discovery-token-ca-cert-hash sha256:9ca9b8eb33e291f83acc2245d919443431692be5ab8527bd8cf58f57c5a18be5
[preflight] Running pre-flight checks
[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/
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[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.

查看节点状态

在 master 节点查看节点状态:

1
2
3
4
5
6
bash复制代码#可以看到所有节点都处于 未就绪 状态。
root@master:/# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master NotReady control-plane,master 24m v1.20.0
node1 NotReady <none> 14s v1.20.0
node2 NotReady <none> 24s v1.20.0

安装 Pod 网络附加组件

注意:

本节包含有关网络设置和部署顺序的重要信息。 在继续之前,请仔细阅读所有建议。

你必须部署一个基于 Pod 网络插件的 容器网络接口 (CNI),以便你的 Pod 可以相互通信。 在安装网络之前,集群 DNS (CoreDNS) 将不会启动。

  • 注意你的 Pod 网络不得与任何主机网络重叠: 如果有重叠,你很可能会遇到问题。 (如果你发现网络插件的首选 Pod 网络与某些主机网络之间存在冲突, 则应考虑使用一个合适的 CIDR 块来代替, 然后在执行 kubeadm init 时使用 --pod-network-cidr 参数并在你的网络插件的 YAML 中替换它)。
  • 默认情况下,kubeadm 将集群设置为使用和强制使用 RBAC(基于角色的访问控制)。 确保你的 Pod 网络插件支持 RBAC,以及用于部署它的 manifests 也是如此。
  • 如果要为集群使用 IPv6(双协议栈或仅单协议栈 IPv6 网络), 请确保你的Pod网络插件支持 IPv6。 IPv6 支持已在 CNI v0.6.0 版本中添加。

说明: 目前 Calico 是 kubeadm 项目中执行 e2e 测试的唯一 CNI 插件。 如果你发现与 CNI 插件相关的问题,应在其各自的问题跟踪器中记录而不是在 kubeadm 或 kubernetes 问题跟踪器中记录。

Kubernetes 网络模型 的附加组件列表

Flannel 是一个非常简单的能够满足 Kubernetes 所需要的覆盖网络。已经有许多人报告了使用 Flannel 和 Kubernetes 的成功案例。

你可以使用以下命令在控制平面节点或具有 kubeconfig 凭据的节点上安装 Pod 网络附加组件:

1
2
3
bash复制代码#搭建 flannel 网络集群,原文内容在:https://github.com/coreos/flannel#flannel
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
#每个集群只能安装一个 Pod 网络。

执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码root@master:/opt/k8syaml# kubectl apply -f kube-flannel.yml 
podsecuritypolicy.policy/psp.flannel.unprivileged created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds created
1234567
#这个过程比较,会下载几个quay.io/coreos/flannel镜像,请耐心等待下。
root@master:/opt/k8syaml# kubectl get pods --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-7f89b7bc75-7vwsj 1/1 Running 0 47m
kube-system coredns-7f89b7bc75-sb9d2 1/1 Running 0 47m
kube-system etcd-master 1/1 Running 0 47m
kube-system kube-apiserver-master 1/1 Running 0 47m
kube-system kube-controller-manager-master 1/1 Running 0 47m
kube-system kube-flannel-ds-8qd6j 1/1 Running 0 13m
kube-system kube-flannel-ds-cwjpg 1/1 Running 0 13m
kube-system kube-flannel-ds-ql9kn 1/1 Running 0 13m
kube-system kube-proxy-hcgqb 1/1 Running 0 24m
kube-system kube-proxy-qbl9b 1/1 Running 0 47m
kube-system kube-proxy-t9zd9 1/1 Running 0 24m
kube-system kube-scheduler-master 1/1 Running 0 47m

此时再次查看节点状态,全部已就绪:

1
2
3
4
5
bash复制代码root@master:/opt/k8syaml# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready control-plane,master 49m v1.20.0
node1 Ready <none> 25m v1.20.0
node2 Ready <none> 25m v1.20.0

如果出现以下报错:

Unable to connect to the server: read tcp 192.168.20.5:37246->151.101.228.133:443: read: connection reset by peer

可以安装 apt-get install ca-certificates 和 apt-get install ssl-cert 解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码# 若是下载不了flannel镜像但是本地docker有的话, yml文件修改镜像源优先来源本地imagePullPolicy: IfNotPresent
# 获取镜像id
docker images
# 其中-o和>表示输出到文件 源镜像可使用镜像id或者镜像名(name:tag)
docker save ImagesId > /opt/flannel.tar
docker save -o /opt/flannelrar quay.io/coreos/flannel:v0.13.1-rc1
# 文件传输
scp flannel.tar root@192.168.146.131:/opt
# 需要镜像的机器上载入镜像 -i和<表示从文件输入
docker load -i /opt/flannel.tar
docker load < flannel.tar
# 若是镜像名不一致,则修改
docker tag ImagesId quay.io/coreos/flannel:v0.13.1-rc1

测试k8s集群

1
2
3
4
bash复制代码kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80 --type=NodePort
kubectl get pod,svc
# 访问地址 http://NodeIp:Port

Dashboard UI

Dashboard 是基于网页的 Kubernetes 用户界面。 你可以使用 Dashboard 将容器应用部署到 Kubernetes 集群中,也可以对容器应用排错,还能管理集群资源。 你可以使用 Dashboard 获取运行在集群中的应用的概览信息,也可以创建或者修改 Kubernetes 资源 (如 Deployment,Job,DaemonSet 等等)。 例如,你可以对 Deployment 实现弹性伸缩、发起滚动升级、重启 Pod 或者使用向导创建新的应用。

Dashboard 同时展示了 Kubernetes 集群中的资源状态信息和所有报错信息

部署 Dashboard UI

部署 Dashboard UI

默认情况下不会部署 Dashboard。可以通过以下命令部署:

1
bash复制代码kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml

虚拟机访问不了,可以在网页打开,在虚拟机内创建个文件,把内容copy进去,即可。

如果远程下载不了,可以本地加载镜像,在recommended.yaml中只需要在image下一行,加上:imagePullPolicy: IfNotPresent 意思是优先使用本地镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
bash复制代码root@master:/opt/k8syaml# vi recommended.yaml
root@master:/opt/k8syaml# kubectl apply -f recommended.yaml
namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created

#查看所有的pod情况
root@master:/opt/k8syaml# kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-7f89b7bc75-7vwsj 1/1 Running 0 14h
kube-system coredns-7f89b7bc75-sb9d2 1/1 Running 0 14h
kube-system etcd-master 1/1 Running 0 14h
kube-system kube-apiserver-master 1/1 Running 0 14h
kube-system kube-controller-manager-master 1/1 Running 0 14h
kube-system kube-flannel-ds-8qd6j 1/1 Running 0 14h
kube-system kube-flannel-ds-cwjpg 1/1 Running 0 14h
kube-system kube-flannel-ds-ql9kn 1/1 Running 0 14h
kube-system kube-proxy-hcgqb 1/1 Running 0 14h
kube-system kube-proxy-qbl9b 1/1 Running 0 14h
kube-system kube-proxy-t9zd9 1/1 Running 0 14h
kube-system kube-scheduler-master 1/1 Running 0 14h
kubernetes-dashboard dashboard-metrics-scraper-79c5968bdc-49b5z 1/1 Running 0 142m
kubernetes-dashboard kubernetes-dashboard-b8995f9f8-nvz8f 1/1 Running 0 142m
#查看没有启动的原因,可以看出,是一直在下载镜像
kubectl describe pod dashboard-metrics-scraper-7b59f7d4df-5vmn2 -n kubernetes-dashboard

有时候镜像下载比较慢,可以单独下载docker pull kubernetesui/metrics-scraper:v1.0.4 docker pull kubernetesui/dashboard:v2.0.0

访问 Dashboard UI

为了保护你的集群数据,默认情况下,Dashboard 会使用最少的 RBAC 配置进行部署。 当前,Dashboard 仅支持使用 Bearer 令牌登录。 要为此样本演示创建令牌,你可以按照 创建示例用户 上的指南进行操作。

kubernetes dashboard登录后,权限不够问题

创建一个群集管理服务帐户
在此步骤中,我们将为仪表板创建服务帐户并获取其凭据。

运行以下命令:

此命令将在默认名称空间中为仪表板创建服务帐户

1
sql复制代码kubectl create serviceaccount admin-user -n kubernetes-dashboard

将集群绑定规则添加到您的仪表板帐户

1
sql复制代码kubectl create clusterrolebinding admin-user -n kubernetes-dashboard  --clusterrole=cluster-admin  --serviceaccount=kubernetes-dashboard:admin-user

使用以下命令复制仪表板登录所需的令牌:

1
2
3
4
bash复制代码kubectl -n kubernetes-dashboard  describe secret admin-user | awk '$1=="token:"{print $2}'

# 更全信息
kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print $1}')

复制令牌,然后通过选择令牌选项将其粘贴到仪表板登录页面中

命令行代理

你可以使用 kubectl 命令行工具访问 Dashboard,命令如下:

1
bash复制代码kubectl proxy

kubectl 会使得 Dashboard 可以通过 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ 访问。

UI 只能 通过执行这条命令的机器进行访问。更多选项参见 kubectl proxy --help。

1
2
bash复制代码#获取token,进行登录
kubectl describe secret admin-user -n kubernetes-dashboard

出现跨域问题

1
2
3
4
5
6
7
8
9
10
11
bash复制代码# 页面访问出现error trying to reach service: dial tcp 10.244.1.18:8443: connect: connectio
# 使用这个来解决:kubectl --namespace=kube-system port-forward <kubernetes-dashboard-podname> 8443
[root@master01 ~]kubectl -n kubernetes-dashboard get pod -o name | grep dashboard
pod/dashboard-metrics-scraper-6cd59dd9c7-tbh2h
pod/kubernetes-dashboard-5b9d976b79-7clvr
[root@master01 ~]kubectl --namespace=kubernetes-dashboard port-forward pod/kubernetes-dashboard-5b9d976b79-7clvr 8443
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443

#访问地址
https://localhost:8443

Q&A

安装docker时,couldn’t be verified because the public key is not available

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码honglei@master:~$ sudo apt-get update && sudo apt-get install -y   apt-transport-https ca-certificates curl software-properties-common gnupg2
Hit:1 http://mirrors.aliyun.com/ubuntu bionic InRelease
Hit:2 http://mirrors.aliyun.com/ubuntu bionic-security InRelease
Hit:3 http://mirrors.aliyun.com/ubuntu bionic-updates InRelease
Hit:4 http://mirrors.aliyun.com/ubuntu bionic-backports InRelease
Hit:5 http://mirrors.aliyun.com/ubuntu bionic-proposed InRelease
Get:6 https://download.docker.com/linux/ubuntu bionic InRelease [64.4 kB]
Err:6 https://download.docker.com/linux/ubuntu bionic InRelease
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 7EA0A9C3F273FCD8
Reading package lists... Done
W: Skipping acquire of configured file 'multivers/source/Sources' as repository 'http://mirrors.aliyun.com/ubuntu bionic-backports InRelease' doesn't have the component 'multivers' (component misspelt in sources.list?)
W: GPG error: https://download.docker.com/linux/ubuntu bionic InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 7EA0A9C3F273FCD8
E: The repository 'https://download.docker.com/linux/ubuntu bionic InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
honglei@master:~$
12345678910111213141516

解决办法:

在sources.list添加下面这一条就可以:

1
2
3
4
bash复制代码deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable

#更新apt源:
apt-get update

启动docker时Dec 19 20:33:46 master systemd[1]: docker.socket: Failed with result ‘service-start-limit-hit’.

1
2
bash复制代码#/etc/docker/daemon.json,修改daemon.json文件后缀为daemon.conf,才可以正常启动
mv daemon.json daemon.conf

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go+Python双语言混合开发课程 第02部分 go语言基

发表于 2021-03-31

第二部分 go语言基础

2.1 go语言介绍

www.cnblogs.com/qfru/p/1060…

2.1.1 go语言介绍

google开源

太多互联网技术都是google开源出来的

go核心团队

cloud.tencent.com/developer/a…

go创作背景

google内部很多系统都是c++完成的, 但是编译过程太慢了,加上语言本身复杂, 很多语言发展的过程都是不断的加功能,

导致语言越来越臃肿, 加上互联网的发展,不如单独开发一门语言, 这种语言的代表是js,补丁语言,一开始是7天搞出来的

go语言初期目的是替代c++, 后面会看到go提倡简单、高效: 继承但是摒弃了很多c++用的不多的特性, 不是功能越多越好, c和c++就是一个例子

目前主流的编程语言基本上都是2000年以前的:

1
2
3
4
5
6
yaml复制代码 c 1972
c++ 1983
python 1991
java 1995
php 1995
javascript 1995

中间在2005年amd发布了双核处理器,多核开始流行,但是目前主流编程语言已经发布 go语言的发展节点

2.1.2 go语言核心特征

静态语言(编译型语言)

2.1.3 go语言的优势

1
2
3
4
5
6
r复制代码语法简单 - 参考了多门语言, 可以大量看到 c、和python的影子
开发效率高
执行性能好 - 赶上并超过了java了
并发编程
编译速度
部署方便

2.1.4 go语言能做什么

服务端开发gin beego

1
2
3
4
5
6
7
8
9
10
复制代码容器虚拟化dockerk8s
存储etcd
tidb
influxdb
groupcache
区块链
以太坊
fabric
微服务
生态越来越完善

2.1.5 go目前的发展趋势

  1. 国内的go发展比国外快
  2. 国内的招聘需求增长很迅速
1
2
3
4
5
6
7
8
9
10
11
12
css复制代码阿里巴巴
腾讯
百度
字节跳动
B站
小米
滴滴
京东
360
七牛云
知乎
美团

dubbo-go 很多国内厂商开始支持go,go已经成为首批支持的语言

2.2 变量与常量

2.2.1 如何定义变量

1单声明变量

var名称类型是声明单个变量的语法。

  1. 第一种,指定变量类型,声明后若不赋值,使用默认值
1
2
csharp复制代码var name string
name ="bobby"
  1. 第二种,根据值自行判定变量类型(类型推断Type inference)

如果一个变量有一个初始值,Go将自动能够使用初始值来推断该变量的类型。因此,如果变量具有初始值,则可以省略变量声明中的类型。

1
ini复制代码var name ="bobby"
  1. 第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误(简短声明)
1
2
3
css复制代码var a int = 10
var b = 10
c : = 10

这种方式它只能被用在函数体内,而不可以用于全局变量的声明与赋值

1
2
3
4
5
6
7
8
go复制代码package main
var a = "bobby"
var b string = "imooc"
var c bool

func main(){
println(a, b, c)
}

2 多变量声明

**
**

  1. 第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值
1
2
go复制代码var name1, name2, name3 type
name1, name2, name3 = v1, v2, v3
  1. 第二种,直接赋值,下面的变量类型可以是不同的类型
1
ini复制代码var name1, name2, name3 = v1, v2, v3
  1. 第三种,集合类型
1
2
3
4
csharp复制代码var (
name string
age int
)

注意:

  • 变量必须先定义才能使用
  • go语言是静态语言,要求变量的类型和赋值的类型必须一致。
  • 变量名不能冲突。(同一个作用于域内不能冲突)
  • 简短定义方式,左边的变量名至少有一个是新的
  • 简短定义方式,不能定义全局变量。
  • 变量的零值。也叫默认值。
  • 变量定义了就要使用,否则无法通过编译。

如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。

如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a:

1
2
3
4
csharp复制代码func main() {
var name string = "bobby"
fmt.Println("bobby, imooc")
}

尝试编译这段代码将得到错误 a declared and not used

单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用在同一个作用域中,已存在同名的变量,则之后的声明初始化,则退化为赋值操作。但这个前提是,最少要有一个新的变量被定义,且在同一作用域,例如,下面的y就是新定义的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

import (
"fmt"
)

func main() {
x := 140
fmt.Println(&x)
x, y := 200, "abc"
fmt.Println(&x, x)
fmt.Print(y)
}

3 匿名变量

在使用多重赋值时候,如果不需要在左值中接收变量,可以使用匿名变量 _, python中也经常有这种用法

1
2
3
4
5
6
7
scss复制代码for data, _ in range list {
}

//python 中
data = [2,4,1,6]
for _, item in enumerate(data):
print(item)

2.2.2 常量的定义

1 常量

常量是一个简单值的标识符,在程序运行时,不会被修改的量。

1
2
ini复制代码显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package main

import "fmt"

func main() {
const LENGTH int = 10
const WIDTH int = 5
var area int
const a, b, c = 1, false, "str" //多重赋值

area = LENGTH * WIDTH
fmt.Printf("面积为 : %d", area)
fmt.Println(a, b, c)
}

常量可以作为枚举,常量组

1
2
3
4
5
ini复制代码const (
Unknown = 0
Female = 1
Male = 2
)

常量组中如不指定类型和初始化值,则与上一行非空常量右值相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import (
"fmt"
)

func main() {
const (
x uint16 = 16
y
s = "abc"
z
)
fmt.Printf("%T,%v\n", y, y)
fmt.Printf("%T,%v\n", z, z)
}

常量的注意事项:

  • 常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
  • 不曾使用的常量,在编译的时候,是不会报错的
  • 显示指定类型的时候,必须确保常量左右值类型一致,需要时可做显示类型转换。这与变量就不一样了,变量是可以是不同的类型值

2 iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量

iota 可以被用作枚举值:

1
2
3
4
5
ini复制代码const (
a = iota
b = iota
c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

1
2
3
4
5
css复制代码const (
a = iota
b
c
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

如果中断iota自增,则必须显式恢复。且后续自增值按行序递增

自增默认是int类型,可以自行进行显示指定类型

数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址

使用iota能简化定义,在定义枚举时很有用。

每次 const 出现时,都会让 iota 初始化为0.

1
2
3
4
5
go复制代码const a = iota // a=0
const (
b = iota //b=0
c //c=1 相当于c=iota
)

2.2.3 匿名变量是什么

1. python中如何定义匿名变量?

1. python中如何定义匿名变量?

1
2
3
4
scss复制代码my_list = ["bobby", "imooc", "test"]
#不打印 index
for index, item in enumerate(my_list):
print(item)

上面的第5行并没有打印index,实际上有些情况下,我们并不会使用index这个变量。但是这个时候index的变量不使用但是index这个名称却被占用了,所以这个时候我们就可以使用匿名变量。将上面的代码改成:

1
2
3
4
5
ini复制代码#定义匿名变量
my_list = ["bobby", "imooc", "test"]
#不打印 index
for _, item in enumerate(my_list):
print(item)

将上面的index改为下划线 “_“,表示这个地方的变量是一个匿名变量。这样既不用占用变量名又可以多一个占位符。

注意:python中申明了变量名后续代码中不使用并没有问题,但是go语言中声明了变量不使用就会报错,所以匿名变量在go语言中会更加的常用。

2. go语言中定义匿名变量

1
2
3
4
5
6
7
8
9
10
go复制代码func test() (int, error)  {
return 0, nil
}

func main() {
_, err := test()
if err != nil {
fmt.Println("函数调用成功")
}
}

说明: 第6行代码中接收函数返回值的时候使用到了匿名变量。因为此处我们并不打印返回的值而只是关心函数调用是否成功

2.2.4 本章小结

此处为语雀文档,点击链接查看:www.yuque.com/bobby-zpcyu…

2.3 计算机组成原理快速扫盲

2.3.1 为什么我们要学习计算机组成原理

  • 为什么计算机组成原理重要
    a. 考研必考 - 重要性不言而喻
    b. 没有计算机组成原理,操作系统原理学起来就费劲
    c. 操作系统原理有多重要?
    ⅰ. 什么是虚拟内存
    ⅱ. 多进程和多线程如何调度
    ⅲ. 什么是锁和死锁
    ⅳ. 不懂操作系统原理-就别想深入理解各种组件-redis,es,rabbitmq等
    d. 什么是中断,以及为什么io操作的时候实际上不耗费cpu?
    e. 很多组件和框架都会变,但是计算机组成原理和操作系统原理不会变
  • 计算机组成原理有哪些知识
    book.douban.com/subject/123…
  • 我们会讲解到的计算机组成原理基础
    a. 课程中会学习到的计算机组成原理最基本的知识点
    b. 进制以及进制转换
    c. ascii码表和内存的一些基础知识
  • 为什么我们会安排这些
    a. python为我们屏蔽了不少的底层细节
    b. 这些细节在静态语言中我们需要知道的更多
    c. 如果实现不知道,对于静态语言的学习会造成一些困扰
  • 如何学习计算机组成原理
    这些知识对于我们来说必须要记住,但是你得知道大概,别到时候别人给你提起或者网上看到资料省略了这些基础你不知道
    这么多知识点记住不现实,当你看到很多深入讲解原理的文章的时候会或多或少的涉及到操作系统原理和计算机组成原理

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用 Go + WebSocket 快速实现一个 chat 服

发表于 2021-03-31

前言

在 go-zero 开源之后,非常多的用户询问是否可以支持以及什么时候支持 websocket,终于在 v1.1.6 里面我们从框架层面让 websocket 的支持落地了,下面我们就以 chat 作为一个示例来讲解如何用 go-zero 来实现一个 websocket 服务。

整体设计

我们以 zero-example 中的 chat 聊天室为例来一步步一讲解 websocket 的实现,分为如下几个部分:

  1. 多客户端接入
  2. 消息广播
  3. 客户端的及时上线下线
  4. 全双工通信【客户端本身是发送端,也是接收端】

先放一张图,大致的数据传输:

image.png

中间有个 select loop 就是整个 chat 的 engine。首先要支撑双方通信:

  • 得有一个交流数据的管道。客户端只管从 管道 读取/输送数据;
  • 客户端在线情况。不能说你下线了,还往你那传输数据;

数据流

数据流是 engine 的主要功能,先不急着看代码,我们先想 client 怎么接入并被 engine 感知:

  1. 首先是从前端发 websocket 请求;
  2. 建立连接;准备接收/发送通道;
  3. 注册到 engine;

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
go复制代码// HTML 操作 {js}
if (window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + "/ws");
conn.onclose = function (evt) {
var item = document.createElement("div");
item.innerHTML = "<b>Connection closed.</b>";
appendLog(item);
};
...
}

// 路由
engine.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/ws",
Handler: func(w http.ResponseWriter, r *http.Request) {
internal.ServeWs(hub, w, r)
},
})

// 接入逻辑
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
// 将http请求升级为websocket
conn, err := upgrader.Upgrade(w, r, nil)
...
// 构建client:hub{engine}, con{websocker conn}, send{channel buff}
client := &Client{
hub: hub,
conn: conn,
send: make(chan []byte, bufSize),
}
client.hub.register <- client
// 开始客户端双工的通信,接收和写入数据
go client.writePump()
go client.readPump()
}

这样,新接入的 client 就被加入到 注册 通道中。

hub engine

发出了 注册 的动作,engine 会怎么处理呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
go复制代码type Hub struct {
clients map[*Client]bool // 上线clients
broadcast chan []byte // 客户端发送的消息 ->广播给其他的客户端
register chan *Client // 注册channel,接收注册msg
unregister chan *Client // 下线channel
}

func (h *Hub) Run() {
for {
select {
// 注册channel:存放到注册表中,数据流也就在这些client中发生
case client := <-h.register:
h.clients[client] = true
// 下线channel:从注册表里面删除
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
// 广播消息:发送给注册表中的client中,send接收到并显示到client上
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
  1. 接收注册消息 -> 加入全局注册表
  2. 如果 engine.broadcast 接收到,会将 msg 传递给 注册表 的 client.sendChan

这样从 HTML -> client -> hub -> other client 的整个数据流就清晰了。

广播数据

上面说到 engine.broadcast 接收到数据,那从页面开始,数据是怎么发送到这?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码func (c *Client) readPump() {
...
for {
// 1
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
// 2.
c.hub.broadcast <- message
}
}
  1. 从 conn 中不断读取 msg【页面点击后传递】
  2. 将 msg 传入 engine.broadcast,从而广播到其他的 client
  3. 当出现发送异常或者是超时,异常退出时,会触发下线 client

同时要知道,此时发送消息的 client 不止有一个,可能会有很多个。那发送到其他client,client 从自己的 send channel 中读取消息即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
go复制代码func (c *Client) writePump() {
// 写超时控制
ticker := time.NewTicker(pingPeriod)
...
for {
select {
case message, ok := <-c.send:
// 当接收消息写入时,延长写超时时间。
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
...
w, err := c.conn.NextWriter(websocket.TextMessage)
...
w.Write(message)

// 依次读取 send 中消息,并write
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
...
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
...
}
}
}

上面也说了,send 有来自各自客户端中发送的msg:所以当检测到 send 有数据,就不断接收消息并写入当前 client;同时当写超时,会检测websocket长连接是否还存活,存活则继续读 send chan,断开则直接返回。

完整示例代码

github.com/zeromicro/z…

总结

本篇文章从使用上介绍如何结合 go-zero 开始你的 websocket 项目,开发者可以按照自己的需求改造。

关于 go-zero 更多的设计和实现文章,可以持续关注我们。

github.com/tal-tech/go…

欢迎使用 go-zero 并 star 支持我们!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

CountDownLatch:别浪,等人齐再团!

发表于 2021-03-31

一入王者深似海,从此对象是路人。

哈喽观众老爷们你们好,在下战神吕布字奉先,今天给大家来一部吕布的教学视频!

咳咳,不对。大家好,我是磊哥,今天给大家来一篇 CountDownLatch 的文章。

在开始之前,先问大家一个非常专业的技术性问题:打团战最怕_____?

一道非常简单的送分题,如果答不对,那磊哥就要批评你了,哈哈。

可能有人会说:打团战最怕猪队友,但比猪队友更可怕的是打团战人不齐啊兄弟,想想在打团时如果是 5V2 是怎么一幅画面,心痛到不敢想🤦🏻‍♀️。

等人齐再团

磊哥在儿子没有出生之前,也是资深的农药玩家,至于段位吗?别问!问就是青铜。虽然磊哥的段位不是很高,但基本的大局观还是有的,毕竟也是打过几年 Dota 和 LOL 的青铜玩家是吧?哈哈。

农药和其他 Moba 类游戏是一样的,想要取胜,必须要把握好每次团战,而每次团战的关键在于等人齐了再开团,是吧?而这个思想正好和咱们要讲得 CountDownLatch 的思想是一致的,咱们来看看是怎么回事吧。

吾有上将“CountDownLatch”

想象一下这样一个场景,当我们需要等待某些线程执行完之后,再执行主线程的代码,要怎么实现?

可能有人会说,简单,用 join() 方法等待线程执行完成之后再执行主线程就行了,实现代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码// 创建线程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// do something
}
});
t1.start();

// 创建线程2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// do something
}
});
t2.start();

// 等待线程 1和线程 2 执行完
t1.join();
t2.join();

当然,如果使用的是 Thread 来执行任务,那这种写法也是可行的。然而真实的(编码)环境中我们是不会使用 Thread 来执行多任务的,而是会使用线程池来执行多任务,这样可以避免线程重复启动和销毁所带来的性能开销,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 创建固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 任务一
executorService.submit(new Runnable() {
@Override
public void run() {
// do something
}
});
// 任务二
executorService.submit(new Runnable() {
@Override
public void run() {
// do something
}
});

那么这时候问题来了,线程池是没有 join() 方法的,那要怎么实现等待呢?

这个时候就要派出我方大将“CountDownLatch”啦。

吾有上将潘凤,可斩华雄… 出场数秒,潘凤…“卒”。

等等导演,我觉得剧情应该是这样的…

CountDownLatch使用

为了实现等待所有线程池执行完之后再执行主线程的逻辑,我决定使用 AQS(AbstractQueuedSynchronizer,抽象同步框架)下的著名类 CountDownLatch 来实现此功能,具体的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码public static void main(String[] args) throws InterruptedException {
// 创建 CountDownLatch
CountDownLatch countDownLatch = new CountDownLatch(2);

// 创建固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 任务一
executorService.submit(new Runnable() {
@Override
public void run() {
// do something
try {
// 让此任务执行 1.2s
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是任务一");
countDownLatch.countDown();
}
});
// 任务二
executorService.submit(new Runnable() {
@Override
public void run() {
// do something
try {
// 让此任务执行 1.2s
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是任务二");
countDownLatch.countDown();
}
});

// 等待任务执行完成
countDownLatch.await();
System.out.println("程序执行完成~");
}

以上程序执行结果如下:
image.png
从上述结果可以看出,主线程的执行是等待任务一和任务二都执行完成之后才执行的。

CountDownLatch实现原理

CountDownLatch 中 count down 是倒数的意思,latch 则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有点“321,芝麻开门”的感觉,CountDownLatch 的作用也正是如此。

CountDownLatch 在创建的时候需要传入一个整数,在这个整数“倒数”到 0 之前,主线程需要一直挂起等待,直到其他的线程都执行之后,主线才能继续执行。

CountDownLatch执行流程

CountDownLatch 的实现是在其内部创建并维护了一个 volatile 类型的整数计数器,当调用 countDown() 方法时,会尝试将整数计数器 -1,当调用 wait() 方法时,当前线程就会判断整数计数器是否为 0,如果为 0,则继续往下执行,如果不为 0,则使当前线程进入等待状态,直到某个线程将计数器设置为 0,才会唤醒在 await() 方法中等待的线程继续执行。

CountDownLatch常用方法

1
2
3
4
5
6
7
8
java复制代码// 线程被挂起直到 count 值为 0 才继续执行
public void await() throws InterruptedException { };

// 和 await() 类似,只不过等待一定的时间后 count 值还没变为 0 的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };

// 将 count 值减 1
public void countDown() { };

总结

使用 CountDownLatch 可以实现等待所有任务执行完成之后再执行主任务的功能,它就好像比赛中要等待所有运动员都完成比赛之后再公布排名一样,当然我们在玩农药的时候也是一样,要等所有人集合完毕之后再开团,这是制胜的关键。而 CountDownLatch 是通过计数器来实现等待功能的,当创建 CountDownLatch 时会设置一个大于 0 的计数器,每次调用 countDown() 方法时计数器的值会 -1,直到计数器值变为 0 时,等待的任务就可以继续执行了。

参考 & 鸣谢

www.jianshu.com/p/128476015…

关注公号「Java中文社群」查看更多精彩且有趣的文章!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go new 和 make 是什么,差异在哪?

发表于 2021-03-31

若有任何问题或建议,欢迎及时交流和碰撞。我的公众号是 【脑子进煎鱼了】,GitHub 地址:github.com/eddycjy。

大家好,我是煎鱼。

在 Go 语言中,有两个比较雷同的内置函数,分别是 new 和 make 方法,其主要用途都是用于分配相应类型的内存空间。

看上去 new 和 make 都是分配内存的,那他们有什么区别呢?这个细节点也成为了不少 Go 语言工程师的面试题之一,值得大家一看。

在今天这篇文章中我们将来解答这个问题。

基本特性

make

在 Go 语言中,内置函数 make 仅支持 slice、map、channel 三种数据类型的内存创建,其返回值是所创建类型的本身,而不是新的指针引用。

函数签名如下:

1
golang复制代码func make(t Type, size ...IntegerType) Type

具体使用示例:

1
2
3
4
5
6
7
golang复制代码func main() {
v1 := make([]int, 1, 5)
v2 := make(map[int]bool, 5)
v3 := make(chan int, 1)

fmt.Println(v1, v2, v3)
}

在代码中,我们分别对三种类型调用了 make 函数进行了初始化。你会发现有的入参是有多个长度指定,有的没有。

这块的区别主要是长度(len)和容量(cap)的指定,有的类型是没有容量这一说法,因此自然也就无法指定。

输出结果:

1
css复制代码[0] map[] 0xc000044070

有一个细节点要注意,调用 make 函数去初始化切片(slice)的类型时,会带有零值,需要明确是否需要。

见过不少的小伙伴在这上面踩坑。

new

在 Go 语言中,内置函数 new 可以对类型进行内存创建和初始化。其返回值是所创建类型的指针引用,与 make 函数在实质细节上存在区别。

函数签名如下:

1
golang复制代码func new(Type) *Type

具体使用示例:

1
2
3
4
5
6
7
8
golang复制代码type T struct {
Name string
}

func main() {
v := new(T)
v.Name = "煎鱼"
}

从上面的例子的效果来看,是不是似曾相似?其实与下面这种方式的一样的:

1
2
3
4
go复制代码func main() {
v := T{}
v.Name = "煎鱼"
}

输出结果均是:

1
css复制代码&{Name:煎鱼}

其实 new 函数在日常工程代码中是比较少见的,因为他可被替代。

一般会直接用快捷的 T{} 来进行初始化,因为常规的结构体都会带有结构体的字面属性:

1
2
3
csharp复制代码func NewT() *T {
return &T{Name: "煎鱼"}
}

这种初始化方式更方便。

区别在哪里

可能会有的小伙伴会疑惑一点,就是 new 函数也能初始化 make 的三种类型:

1
2
golang复制代码	v1 := new(chan bool)
v2 := new(map[string]struct{})

那 make 函数的区别,优势是什么呢?

本质上在于 make 函数在初始化时,会初始化 slice、chan、map 类型的内部数据结构,new 函数并不会。

例如:在 map 类型中,合理的长度(len)和容量(cap)可以提高效率和减少开销。

更进一步的区别:

  • make 函数:
    • 能够创建类型所需的内存空间,返回引用类型的本身。
    • 具有使用范围的局限性,仅支持 channel、map、slice 三种类型。
    • 具有独特的优势,make 函数会对三种类型的内部数据结构(长度、容量等)赋值。
  • new 函数:
    • 能够创建并分配类型所需的内存空间,返回指针引用(指向内存的指针)。
    • 可被替代,能够通过字面值快速初始化。

总结

在这篇文章中,我们介绍了 Go 语言中 make 和 new 函数的使用,并针对其区别点进行了分析。

可能会有小伙伴疑惑,那 new 和 make 函数所初始化出来的内存,是分配在堆还是栈上呢?

这就涉及到 Go 语言中的 “逃逸分析” 了(我公众号前几天的文章有发),如果所初始化的变量不需要在当前作用域外生存,那么理论上就不需要初始化在堆上。

我的公众号

分享 Go 语言、微服务架构和奇怪的系统设计,欢迎大家关注我的公众号和我进行交流和沟通。

最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊一聊 Go 的接口 Go主题月

发表于 2021-03-30

什么是接口?接口是抽象,是没有实现的方法集合,可以帮助我们隐藏具体实现,从而达到解耦的作用。

这篇文章将会聊一聊 Go 接口相关的内容。

隐式接口

在 Go 语言中定义接口需要使用 interface 关键字,而且只能定义方法,不能包含成员变量,例如:

1
2
3
go复制代码type error interface {
Error() string
}

我们可以通过实现 Error() string 方法,从而间接地实现了 error 接口,而不需要显式地去实现接口:

1
2
3
4
5
6
7
8
go复制代码type RPCError struct {
Code int64
Message string
}

func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}

结构体与指针

我们可以使用结构体或指针作为接口实现的接收者,但这两种类型是不一样的,而且两种实现是不可以同时存在的。

1
2
3
4
5
6
7
8
go复制代码type Cat struct {}
type Duck interface { ... }

func (c Cat) Quack {} // 使用结构体实现接口
func (c *Cat) Quack {} // 使用结构体指针实现接口

var d Duck = Cat{} // 使用结构体初始化变量
var d Duck = &Cat{} // 使用结构体指针初始化变量

这里就会发生结构体指针实现的接口,在使用结构体初始化变量时,编译是不能通过的。

指针类型

Go 语言中有两种不同类型的接口

  • runtime.iface 表示带有一组方法的接口
  • runtime.eface 表示不包含任何方法的接口 interface{}

注意了,interface{} 类型并不是表示任意类型。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点认证协议 普及篇之Kerberos

发表于 2021-03-30

总文档 :文章目录

Github : github.com/black-ant

  • 纯约束型协议 : OAuth , SAML , OIDC , CAS ,LTPA
  • 服务器类协议 : RADIUS , Kerberos , ADFS
  • 认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)
  • 认证服务器(附带) : AD , LDAP , ADFS

这一篇来聊一下 Kerberos 协议 , 已经基于Kerberos的 AD 域单点

一 . 前言

Kerberos 最初是由麻省理工学院(MIT)开发的,是雅典娜计划(projectathena)的一部分 , Kerberos 提供了一个集中的身份验证服务器,其功能是对用户到服务器的身份验证,以及对用户到服务器的身份验证。在 Kerberos 身份验证中,服务器和数据库用于客户端身份验证。Kerberos 作为第三方受信任服务器(称为密钥分发中心(KDC))运行。网络上的每个用户和服务都是一个主体。

Kerberos 优点

  1. 密码从不通过网络发送,因为只有密钥以加密形式发送;
  2. 身份验证是相互的,因此客户端和服务器在相同的步骤进行身份验证,并且它们都确信自己正在与正确的对应方进行通信;
  3. 身份验证可重用且不会过期;
  4. Kerberos 完全基于开放的互联网标准
  5. Kerberos 被许多行业采用,因此其安全协议或底层模块中的任何新缺陷都会很快得到纠正

Kerberos 缺点

  1. 如果未经授权的用户可以访问密钥分发中心,则整个身份验证系统将受到威胁
  2. Kerberos 只能被支持 Kerberos 的应用程序采用。为了让某些应用程序能够识别 Kerberos,重写这些应用程序的代码可能是个问题

Kerberos 关键词

  • 安全认证协议
  • tickets 验证
  • 密码保护(本地 不保存,链路不传输 )
  • 对称加密
  • Server - client 可以相互验证
  • 有可信第三方

二 . Kerberos 基础要点

2.1 Kerberos 成员

认证体系成员

  • Client 成员
  • 应用程序服务器 (AP , ApplicationServer , Resource)
  • 密钥分配中心 (KDC) : AS + TGS + DB
    • 发票的可信第三方。在活动目录中,每个网域控制器都充当一个 KDC。
    • KDC 提供两个核心服务:
      • 身份验证服务(AS)对客户机进行身份验证并向其发出票证;
      • 票证授予服务(TGS)接受经过身份验证的客户机并向其发出票证以访问其他资源
    • KDC 存在一个数据中心 (Database , db)

2.2 Kerberos 架构

架构特点 :

  • 消息 = 可解码部分 + 不可解码部分
  • 服务端 不与 KDC 直接交流
  • KDC 中拥有 所有用户及密码

涉及概念 :

  • principal : 认证主体 , 类似于用户名
  • realm : 作用域 ,一个 principal 只在 指定的 realm 中起作用
  • password : 用户密码 ,对应 于 kerberos 中的 master_key ,可存在于 keytab文件中
  • credential : 凭证 ,用于证明用户 / 行为的有效性 (password / ticket)
  • Long-term Key/Master Key :长期不变的 key , 他的原则是 不能在网络上传输
  • Short-term Key/Session Key : 可在网络上进行传输的key , 这种 key 有时效性

TGT 和 TGS 的区别

  • TGT KDC 加密部分(不可解读) : name/ID + TGS的 name /ID + 时间戳 + IP 地址 + TGT 生命周期 + TGS session key
  • TGT 个人加密部分(可解读) :TGS 的 name / ID + 时间错 + 生命周期 + TGS session key

2.3 Kerberos 请求流程

Kerberos 协议过程主要有两个阶段,第一个阶段是 KDC 对 Client 身份认证,第二个阶段是Service对Client身份认证。

  • 第一次 : 客户端输入登录信息 , Kerberos 客户机创建一个加密密钥并向身份验证服务器(AS)发送一条消息
  • 第二次 : AS 使用这个密钥创建临时会话密钥,并向票据授予服务(TGS)发送消息
  • 第三次 : TGS 向客户机授予票据和服务器会话密钥 , 客户端使用这些来与服务器进行身份验证并获得访问权

以下是 Kerberos 访问详情 :

kdc001.jpg

  1. KRB_AS_REQ: 从身份验证服务(AS)请求TGT
    • 客户机的请求包括用户的用户主体名(UPN)和时间戳。它使用用户的密码散列进行加密
  2. KRB_AS_REP : 从身份验证服务接收TGT
    • KDC 使用 UPN 在其数据库中查找客户机,并使用用户的密码 hashto 尝试解密消息
    • 如果 KDC 成功地解密 TGT 请求,并且时间戳位于 KDC 配置的时间偏差内,则身份验证成功
    • 身份认证成功后 , KDC将TGT 和 TGS会话密钥被发送回客户端。TGS 会话密钥用于加密后续请求
  3. KRB_TGS_REQ : 发送当前的 TGT 并请求TGS
    • 客户机显示它的 TGT 以及一个请求,包括它想要访问的服务的服务主体名称(Service Principal Name,SPN)
    • TGS 请求使用TGS会话密钥进行加密
  4. KRB_TGS_REP : 从 KDC 接收 TGS
    • KDC 验证 TGT,如果成功,则生成 TGS。TGS 包含有关请求者的信息(如请求者的 SID 和组成员身份) ,并使用服务的密码散列进行加密
    • TGS 和服务会话密钥使用 TGS 会话密钥加密,然后发送回客户机
  5. KRB_TGS_REP : 将 TGS 提交给应用服务器进行授权
    • 客户机将从 KDC 接收的 TGS 连同验证者消息一起发送到应用服务器,验证者消息使用服务会话密钥进行加密 (App 此时会拿着 TGS 去 KDC 认证)
  6. KRB_AP_REP : 授予客户端访问服务的权限
    • 客户端接收消息并用服务会话密钥对其进行解密
    • 应用服务器从服务票据中提取特权属性证书(PAC) ,用网域控制器验证其内容
    • 仅当票据授予票据(TGT)超过20分钟时,才会验证票据/PAC

2.5 KDC 流程详情

基础成员 :

1
2
3
4
5
6
7
8
9
10
11
java复制代码-》 组成角色
> KDC : key distributed center 密钥配置中心 , 整个安全认证过程的票据生成管理服务 , 包含 AS 和 TGS
> AD :account database ,存储所有client的白名单

-》 主要角色
> C : Client
> AS : Authentication Server 认证服务器 ,完成用户认证
> TGS : Ticket Granting Server 凭证服务器
> ST : Http Service Ticket
> SS : Service Server
> RS : Resource server

Step 1 : KRB_AS_REQ 第一次 申请 TGT

  • 请求 C->SS : 通过 明文(Name/身份信息 , IP/client 消息 , TGT 有效时间 )访问 (亦可使用 Master key 进行加密 ,AD 中保存有 Master key)
  • 处理 IN SS : SS 判断 该 对象 是否 在 AD 中存在 , 并且 产生 Session Key 用于 TGS 之间通信
  • 返回 SS->C:返还TGT (TGT 服务端部分 + TGT 个人部分)

image.png

Step 2 : KRB_TGS_REQ 第二次生成 TGS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JAVA复制代码> 请求 C -> TGS :  
-> TGS Session key 加密部分(Name/ID + 时间戳 + client Info),明文 (服务Name/ID+生命周期),TGT

> 处理 IN TGS (对TGT 第一部分解密 ):
-> 1. 用户名对比 (TGT <-> 认证器)
-> 2. 时间戳对比
-> 3. 是否过期
-> 4. IP是否一致
-> 5. 认证器是否已存在于缓存
-> 6. 添加权限和认证服务
-> 7. 产生 Http Service Session Key
-> 8. 准备 ST

> 返回 TGS -> C:
-> ST ( Http 服务密码 进行加密 ) = 个人name/id + Http 服务name /id + IP + 时间戳 + ST 生命周期 + Http Service Session Key
-> TGS Session Key 加密部分 = Http 服务name /id + 时间戳 + ST 生命周期 + Http Service Session Key

image.png

Step 3 : 资源服务器处理

1
2
3
4
5
6
7
8
9
java复制代码> 请求 C -> RS : 
-> Http Service Session Key加密部分 : 个人 name / ID + 时间戳

> Resource 服务器 中 :
-> 1. 对比用户名
-> 2. 比较时间戳
-> 3. 检查是否过期
-> 4. 检查IP地址
-> 5. 是否已经存在于缓存

2.5 KDC 的使用前提

  1. 域控制器之间的复制 :
* 如果部署了多个域控制器(即多个 KDC) ,则必须启用复制并及时回收。
* 如果复制失败或回收被延迟,当用户更改密码时,身份验证可能
  1. 客户端和 kdc 必须将他们的时钟同步
* 在 Kerberos 中,时间的准确度量对于防止重放攻击非常重要。
* Kerberos 支持可配置的时间偏移(默认5分钟) ,超过这个时间,身份验证将失败
  1. 客户端和 kdc 必须能够在网络上进行通信
* Kerberos 流量发生在 TCP 和 UDP 端口88上,所有客户端都必须能够访问至少一个 KDC (网域控制器)
  1. 客户端、用户和服务必须具有唯一的名称
* 计算机、用户或服务主体名称的重复名称可能导致身份验证失败
  1. 客户端和 kdc 必须使用 NETBIOS 和 DNS 名称解析
* 客户端和 kdc 必须使用 NETBIOS 和 DNS 名称解析
* Kerberos 服务主体名称通常包括 NETBIOS 和 DNS 地址,这意味着 KDC 和 Client 必须能够以相同的方式解析这些名称
* 某些情况下 , IP 地址也可用于服务主体名称

三 . Kerberos AD域配置

3.1 配置 KDC DB 部分

Step 1 : 创建Kerberos SPN 用户
kdc002.jpg

Step 2 : 配置用户属性 , 设置不要求验证 , 密码不过期
KDC003.jpg

Step 3 :生成 kerberos.keytab

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码ktpass.exe /out c:\kerberos.keytab /princ HTTP/antblack.com@ADSERVER.COM.CN /pass zzy19950810 /mapuser kerberos@ADSERVER.COM.CN /ptype KRB5_NT_PRINCIPAL /crypto RC4-HMAC-NT

ADSERVER.COM.CN
//- 当前域名
antblack.com
//- KDC Client 端域名 (即应用服务器域名)
kerberos@ADSERVER.COM.CN
//- 绑定的用户
zzy19950810
//- 绑定的密码
RC4-HMAC-NT
// -加密方式

kdc004.jpg

Step 4 :生成 后用户会多委派属性 ,选择信任

kdc005.jpg

同时可以看到用户已经绑定了多个(PS : 这里实际上应该是ADSERVER.COM.CN , 截图问题)
kdc006.jpg

3.2 配置 KDC

CentOS 7 可以不用安装 ,如果 klist 不存在 , 执行以下命令

yum install krb5-server krb5-libs krb5-auth-dialog

修改 /etc/krb5.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
conf复制代码# Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/

[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log

[libdefaults]
dns_lookup_realm = false
ticket_lifetime = 24h
default_realm = ADSERVER.COM.CN
default_keytab_name = /opt/kerberos.keytab
default_tkt_enctypes = rc4-hmac
default_tgs_enctypes = rc4-hmac

[realms]
ADSERVER.COM.CN= {
kdc = 192.168.158.9
}

[domain_realm]
.adserver.com.cn = ADSERVER.COM.CN
adserver.com.cn = ADSERVER.COM.CN
  • /opt/kerberos.keytab : windows AD 之前生成的 , 拖入应用服务器
  • 192.168.158.9 : KDC DB 地址
  • ADSERVER.COM.CN : KDC AD 域信息
  • rc4-hmac : 加密方式

Step 3 : 测试 KDC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码klist -k    
[root@localhost ~]# klist -k
Keytab name: FILE:/opt/kerberos.keytab
KVNO Principal
---- --------------------------------------------------------------------------
3 HTTP/antblack.com@ADSERVER.COM.CN

// 测试 KeyTab 是否连接
// 这个 ANTBLACK.CN 会去查询 kerb5.conf 中的 realm , 并且去其配置的 kdc 进行认证
kinit -k HTTP/antblack.cn@ANTBLACK.CN
klist -k
// 执行后会出现票据


// PS : 此时 AD 中运行 : klist tickets
>>>>>>>>>>>>>>>>
当前登录 ID 是 0:0x12de650
缓存的票证: (2)
#0> 客户端: administrator @ WDHACPOC.COM.CN
服务器: krbtgt/WDHACPOC.COM.CN @ WDHACPOC.COM.CN
Kerberos 票证加密类型: AES-256-CTS-HMAC-SHA1-96
票证标志 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
开始时间: 3/30/2021 16:35:39 (本地)
结束时间: 3/31/2021 2:35:39 (本地)
续订时间: 4/6/2021 16:35:39 (本地)
会话密钥类型: AES-256-CTS-HMAC-SHA1-96
缓存标志: 0x1 -> PRIMARY
调用的 KDC: WIN-U76BKIQFGGJ

#1> 客户端: administrator @ WDHACPOC.COM.CN
服务器: host/win-u76bkiqfggj.wdhacpoc.com.cn @ WDHACPOC.COM.CN
Kerberos 票证加密类型: AES-256-CTS-HMAC-SHA1-96
票证标志 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
开始时间: 3/30/2021 16:35:39 (本地)
结束时间: 3/31/2021 2:35:39 (本地)
续订时间: 4/6/2021 16:35:39 (本地)
会话密钥类型: AES-256-CTS-HMAC-SHA1-96
缓存标志: 0
调用的 KDC: WIN-U76BKIQFGGJ

四 . Java 实现方式

// TODO : 行业代码不便于整理 , 后续会做一个简化的 demo 填坑

总结

Kerberos 对外主推的是安全性 , 这个也属于常见但是用的不多的协议 , 结合 AD 域单点部分厂家还是有涉及.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL深度分页的问题及优化方案:千万级数据量如何快速分页

发表于 2021-03-30

前言

后端开发中为了防止一次性加载太多数据导致内存、磁盘IO都开销过大,经常需要分页展示,这个时候就需要用到MySQL的LIMIT关键字。但你以为LIMIT分页就万事大吉了么,Too young,too simple啊,LIMIT在数据量大的时候极可能造成的一个问题就是深度分页。

案例

这里我以显示电商订单详情为背景举个例子,新建表如下:

1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `cps_user_order_detail` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` varchar(32) NOT NULL DEFAULT '' COMMENT '用户ID',
`order_id` bigint(20) DEFAULT NULL COMMENT '订单id',
`sku_id` bigint(20) unsigned NOT NULL COMMENT '商品ID',
`order_time` datetime DEFAULT NULL COMMENT '下单时间,格式yyyy-MM-dd HH:mm:ss',
PRIMARY KEY (`id`),
KEY `idx_time_user` (`order_time`,`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户订单详情';

然后手动向表里插入120W条数据。

现在有个需求:分页展示用户的订单详情,按照下单时间倒序。

表结构精简了,需求也简单。于是哗哗哗的写完代码,提测上线了。早期运行一切正常,可随着订单量的不断增大,发现系统越发的缓慢,还时不时报出几个慢查询。

这个时候你就该想到是LIMIT偏移的问题了,没错,不是你的SQL不够优美,就是MySQL自身的机制。

这里我就简单以两条SQL为例,如下图,分别是从100和100W的位置偏移分页,可以看到时间相差很大。这还不算其它数据运算和处理的时间,单一条SQL的查询就耗时一秒以上,在对用户提供的功能里这是不能容忍的(电商里经常要求一个接口的RT不超过200ms)。

在这里插入图片描述

这里我们再看下执行计划,如下图所示:

在这里插入图片描述

在此先介绍一下执行计划Extra列可能出现的值及含义:

  1. Using where:表示优化器需要通过索引回表查询数据。
  2. Using index:即覆盖索引,表示直接访问索引就足够获取到所需要的数据,不需要通过索引回表,通常是通过将待查询字段建立联合索引实现。
  3. Using index condition:在5.6版本后加入的新特性,即大名鼎鼎的索引下推,是MySQL关于减少回表次数的重大优化。
  4. Using filesort:文件排序,这个一般在ORDER BY时候,数据量过大,MySQL会将所有数据召回内存中排序,比较消耗资源。

再看看上图,同样的语句,只因为偏移量不同,就造成了执行计划的千差万别(且容我小小的夸张一下)。第一条语句LIMIT 100,6type列的值是range,表示范围扫描,性能比ref差一个级别,但是也算走了索引,并且还应用了索引下推:就是说在WHERE之后的下单时间删选走了索引,并且之后的ORDER BY也是根据索引下推优化,在执行WHERE条件筛选时同步进行的(没有回表)。

而第二条语句LIMIT 1000000,6压根就没走索引,type列的值是ALL,显然是全表扫描。并且Extra列字段里的Using where表示发生了回表,Using filesort表示ORDER BY时发生了文件排序。所以这里慢在了两点:一是文件排序耗时过大,二是根据条件筛选了相关的数据之后,需要根据偏移量回表获取全部值。无论是上面的哪一点,都是LIMIT偏移量过大导致的,所以实际开发环境经常遇到非统计表量级不得超过一百万的要求。

优化

原因分析完了,那么LIMIT深度分页在实际开发中怎么优化呢?这里少侠给两点方案。

一是通过主键索引优化。什么意思呢?就是把上面的语句修改成:

1
sql复制代码SELECT * FROM cps_user_order_detail d WHERE d.id > #{maxId} AND d.order_time>'2020-8-5 00:00:00' ORDER BY d.order_time LIMIT 6;

如上代码所示,同样也是分页,但是有个maxId的限制条件,这个是什么意思呢,maxId就是上一页中的最大主键Id。所以采用此方式的前提:1)主键必须自增不能是UUID并且前端除了传基本分页参数pageNo,pageSize外,还必须把每次上一页的最大Id带过来,2)该方式不支持随机跳页,也就是说只能上下翻页。如下图所示是某知名电商中的实际页面。

在这里插入图片描述

二是通过Elastic Search搜索引擎优化(基于倒排索引),实际上类似于淘宝这样的电商基本上都是把所有商品放进ES搜索引擎里的(那么海量的数据,放进MySQL是不可能的,放进Redis也不现实)。但即使用了ES搜索引擎,也还是有可能发生深度分页的问题的,这时怎么办呢?答案是通过游标scroll。关于此点这里不做深入,感兴趣的可以做研究。

小结

写这篇博客是因为前段时间在开发中真实经历到了,并且之前在字节面试中确实也和面试官探讨了一番。知道LIMIT的限制以及优化,在面试中能提到是加分项,不能说到MySQL优化就是建索引,调整SQL(实际上在真实开发中这两种优化方案的成效微乎其微)。毕竟MySQL优化那么牛X的话,就不会有那么多中间件产生了。

我是少侠露飞,爱技术,爱分享。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【搞定面试官】系列:如何保证缓存与数据库的双写一致性

发表于 2021-03-30

面试开始

小伙子你好,看你简历上写到了MySQL和Redis。今天我们就围绕他们两个展开吧。Redis和MySQL是后端开发中举重若轻的重要角色。实际开发中二者也基本上如影随行,为了提高性能和响应,Redis常常存放热点数据,MySQL存放所有数据,保证数据持久化。所以Redis可以说是MySQL的一部分数据。

那么问题来了,当MySQL中的持久化数据发生改变,如何通知Redis呢?也即如何保证缓存和数据库数据的双写一致性呢?


面试官您好,我们在开发中采取的方案是:先更新数据库,然后删除相应缓存,直到下次请求缓存发现没有数据,再从MySQL中读取,同时将数据更新到Redis。

那为什么采用删除缓存而不是更新缓存呢?


如下图所示,如果采取更新缓存的方式,可能出现请求A先于请求B发生,更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存,这就导致了脏数据。

在这里插入图片描述

其次,如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用更新缓存的方案就会导致数据压根还没被读取过,但缓存已被频繁的更新,浪费性能。

那先删除缓存,再更新数据库会不会有什么问题?


如下图所示,请求A进行写操作,删除缓存,请求B查询发现缓存不存在,就去数据库查询得到旧值,之后将旧值写入缓存,此时请求A再将新值写入数据库。

这种情况就会导致数据不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该缓存数据永远都是脏数据。

在这里插入图片描述

好的,刚刚说的方案确实在并发环境中都会有问题。那你们采用先更新数据库,再删除缓存,这种方案一定不会出现并发问题吗?


答案是不一定,也可能会出现并发问题。如下图所示,

当步骤Step3的更新数据库操作比步骤Step2的读取数据库操作耗时更短,就有可能使得步骤Step4先于步骤Step5,此时缓存中的就是脏数据。但一般情况下数据库的读操作的速度是远快于写操作的(此点从MySQL的并发读写量就可看出,相同硬件配置下并发读的效率是并发写的数倍) 。

在这里插入图片描述

因此,如果你想实现基础的缓存和数据库双写一致的逻辑,那么在大多数情况下,在不想做过多设计、增加太大工作量的情况下,请先更新数据库,再删除缓存!

先更新数据库,再删除缓存,除了你刚刚说得问题,还会有别的问题吗?


如果MySQL采用了读写分离架构,当请求A更新数据在master库上并删除了缓存,但此时数据库主从同步还未完成。请求B查询缓存发生Cache miss之后从slave库上读取到的还是旧值,此时也会造成数据不一致。

在这里插入图片描述

你刚刚说到先更新数据库,再删除缓存也有可能造成数据不一致,怎么解决呢?


采用延时双删。如下图所示,请求A更新数据库之后,为防止删除缓存先行发生于请求B的将缓存写入旧值,可以通过将请求A更新完数据库之后休眠一会(例如100ms,200ms,根据实际业务场景拟定),再删除缓存,这样基本能保证缓存中存放的不会是脏数据。主从架构也是这个原理,就是请求A在更新master之后不用立即删除缓存,通过延时双删保证主从同步已经完成,最后删除缓存数据。

在这里插入图片描述

但你这种方案,请求A休眠一段时间的话,可能会影响到接口的RT,降低系统的吞吐量,如何解决呢?


这里比较优雅的方案是通过异步实现。即开启一个线程池,在请求A的时候开启一个单独的线程,异步的休眠一段时间然后执行缓存删除。当然也可以通过将缓存中相应的key扔到消息队列,通过MQ异步删除,但仅为了异步删除缓存就多加了一层消息队列,可能会造成系统设计更加复杂,并且会带来别的问题。

前面一直有提到删除缓存,如果删除缓存失败了怎么办呢?


再加一个重试机制,保证删除缓存成功。

如果我一定要数据库和缓存数据一致性怎么办?


没有办法做到绝对的一致性,这是由CAP理论决定的,缓存系统适用的场景就是非强一致性的场景,所以它属于CAP中的AP。

CAP理论是分布式系统中的经典理论,即一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。

根据BASE(Basically Available,Soft State和Eventually Consistent)理论,缓存和数据库只能做到数据的最终一致性。

面试结束

时间也不早了,今天就到这里吧,看出来小伙子对这块掌握的比较深入。我们公司就缺少你这种人才,要不现在就签把Offer签了吧。

这个时候你肯定欲绝还迎,一手接Offer一边摆摆手:不行不行,深圳马那边也急着等我给回复呢,催了我好几天了。

面试官一听,payroll组何在,加价!

小结

使用缓存并不是一个很简单的事情,尤其在需要缓存与数据库保持强一致的场景,才知道让数据库数据和缓存数据保持一致性是一门很高深的学问。

从远古的硬件缓存,操作系统缓存开始,缓存就是一门独特的学问。这个问题也被业界探讨了非常久,争论至今也无果,因为其实这是一个权衡的问题。

我是少侠露飞,爱技术,爱分享。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…694695696…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%