问题描述

我们常用的存储软件(比如 NFS、Ceph、EdgeFS、YugabyteDB 等等)并不具备(或仅具备部分)高可用、自愈、自动扩展等等特性。

解决方案

Rook,是开源的云原生存储编排器,提供平台,框架,支持“多种本机存储解决方案”与“云原生环境”集成。Rook 基于底层云原生平台对这些存储软件进行强化。通过使用“__底层的云原生容器管理、调度、协调平台提供的__”基础设施,来为存储服务添加 自我管理、自我缩放、自愈的 等等特性。它通过自动部署、引导、配置、部署、缩放、升级、迁移、灾难恢复,监控,资源管理来实现。

该笔记将记录在 Kubernetes 中如何部署 Rook 服务,底层使用 NFS 存储,以及常见问题解决办法。

环境要求

  1. Kubernetes v1.16 or higher
  2. The desired volume to export needs to be attached to the NFS * server pod via a PVC
  3. NFS client packages must be installed on all nodes where * Kubernetes might run pods with NFS mounted.

环境信息

  1. Rook NFS v1.7(03/14/2022),建议阅读 NFS Docs/v1.7 文档以了解更多细节,这里我们仅记录适用于我们环境的部署过程。
  2. Kubernetes HA Cluster 1.18.20, worker k8s-storage as dedicated storage node

关于存储

  1. 简单的拓扑结构为 Normal Pod ⇒ Storage Class ⇒ NFS Server ⇒ PVC ⇒ PV (hostPath) 所以我们以 hostPath 方式来提供最终的存储;
  2. 通过专用的存储节点,即 Kubernetes Worker 但是不会向该节点调度 Pod 实例(通过 Taint 及 Namespace defaultTolerations 来实现);

准备工作

1
2
3
4
5
# Taint node,以专用于存储
kubectl taint nodes k8s-storage dedicated=storage:NoSchedule

# 开启 PodNodeSelector,PodTolerationRestriction 插件(不再细述)
kube-apiserver ... --enable-admission-plugins=NodeRestriction,PodNodeSelector,PodTolerationRestriction ...

第一步、部署 NFS Operator 组件

1
2
3
4
5
6
7
8
git clone --single-branch --branch v1.7.3 https://github.com/rook/nfs.git
cd nfs/cluster/examples/kubernetes/nfs
kubectl create -f crds.yaml
kubectl create -f operator.yaml

# kubectl get pods -n rook-nfs-system
NAME READY STATUS RESTARTS AGE
rook-nfs-operator-794b5c98bd-rc8lv 1/1 Running 0 8m31s

补充说明:

  • Operator 是否调度到 k8s-storage(专用存储节点)并不重要;

第二步、创建 NFS Server 服务

rbac.yaml

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
55
56
57
58
59
60
61
62
---
apiVersion: v1
kind: Namespace
metadata:
name: rook-nfs
annotations:
scheduler.alpha.kubernetes.io/node-selector: kubernetes.io/hostname=k8s-storage
scheduler.alpha.kubernetes.io/defaultTolerations: '[{"operator": "Exists", "effect": "NoSchedule", "key": "dedicated"}]'
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rook-nfs-server
namespace: rook-nfs
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-nfs-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get"]
- apiGroups: ["policy"]
resources: ["podsecuritypolicies"]
resourceNames: ["rook-nfs-policy"]
verbs: ["use"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups:
- nfs.rook.io
resources:
- "*"
verbs:
- "*"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-nfs-provisioner-runner
subjects:
- kind: ServiceAccount
name: rook-nfs-server
# replace with namespace where provisioner is deployed
namespace: rook-nfs
roleRef:
kind: ClusterRole
name: rook-nfs-provisioner-runner
apiGroup: rbac.authorization.k8s.io

nfs-server.yaml

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
55
56
57
58
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: rook-nfs-pv
namespace: rook-nfs
labels:
type: local
spec:
storageClassName: manual
claimRef:
name: rook-nfs-pvc
namespace: rook-nfs
capacity:
storage: 500Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/srv/k8s-storage-nfs-rook-pv"
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k8s-storage
---
# A default storageclass must be present
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rook-nfs-pvc
namespace: rook-nfs
spec:
volumeName: "rook-nfs-pv"
accessModes:
- ReadWriteMany
resources:
requests:
storage: 500Gi
---
apiVersion: nfs.rook.io/v1alpha1
kind: NFSServer
metadata:
name: rook-nfs
namespace: rook-nfs
spec:
replicas: 1
exports:
- name: share-01
server:
accessMode: ReadWrite
squash: "none"
persistentVolumeClaim:
claimName: rook-nfs-pvc
annotations:
rook: nfs

查看结果:

1
2
3
4
5
6
7
# kubectl -n rook-nfs get nfsservers.nfs.rook.io
NAME AGE STATE
rook-nfs 2m Running

# kubectl -n rook-nfs get pod -l app=rook-nfs -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
rook-nfs-0 2/2 Running 0 6m8s 192.168.59.130 k8s-w03 <none> <none>

第三步、使用 NFS 存储

storage-class.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
labels:
app: rook-nfs
name: rook-nfs-share-01
parameters:
exportName: share-01
nfsServerName: rook-nfs
nfsServerNamespace: rook-nfs
provisioner: nfs.rook.io/rook-nfs-provisioner
reclaimPolicy: Retain
volumeBindingMode: Immediate

testing.yaml

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
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rook-nfs-pv-claim
spec:
storageClassName: "rook-nfs-share-01"
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi
---
# cat nfs/cluster/examples/kubernetes/nfs/busybox-rc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nfs-demo
role: busybox
name: nfs-busybox
spec:
replicas: 2
selector:
matchLabels:
app: nfs-demo
role: busybox
template:
metadata:
labels:
app: nfs-demo
role: busybox
spec:
containers:
- image: busybox
command:
- sh
- -c
- "while true; do date > /mnt/index.html; hostname >> /mnt/index.html; sleep $(($RANDOM % 5 + 5)); done"
imagePullPolicy: IfNotPresent
name: busybox
volumeMounts:
# name must match the volume name below
- name: rook-nfs-vol
mountPath: "/mnt"
volumes:
- name: rook-nfs-vol
persistentVolumeClaim:
claimName: rook-nfs-pv-claim

补充说明

Pod 通过 Service 进行 NFS 挂载:

1
2
3
# kubectl -n rook-nfs get service rook-nfs 
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
rook-nfs ClusterIP 10.111.156.207 <none> 2049/TCP,111/TCP 135m

调试追踪

1
2
3
4
5
6
7
kubectl -n rook-nfs-system logs -l app=rook-nfs-operator

# NFS Server
kubectl -n rook-nfs logs rook-nfs-0 nfs-server

# Storage Class
kubectl -n rook-nfs logs rook-nfs-0 nfs-provisioner

参考文献

1.什么是OAuth2

  OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。

2.应用场景

  • 第三方应用授权登录:在APP或者网页接入一些第三方应用时,时长会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录。
  • 原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、请求后台数据。
  • 前后端分离应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证,比如使用vue、react后者h5开发的app。

3.应用场景

  • Third-party application:第三方应用程序,本文中又称”客户端”(client),比如打开知乎,使用第三方登录,选择qq登录,这时候知乎就是客户端。
  • HTTP service:HTTP服务提供商,本文中简称”服务提供商”,即上例的qq。
  • Resource Owner:资源所有者,本文中又称”用户”(user),即登录用户。
  • User Agent:浏览器 或 App。
  • Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
  • Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

4.运行流程

(A)用户打开客户端以后,客户端要求用户给予授权。

(B)用户同意给予客户端授权。

(C)客户端使用上一步获得的授权,向认证服务器申请令牌。

(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

(E)客户端使用令牌,向资源服务器申请获取资源。

(F)资源服务器确认令牌无误,同意向客户端开放资源。

5.授权模式

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)
1
2
3
4
5
6
7
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。

(1)用户访问客户端,后者将前者导向认证服务器,假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(2)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌:
GET /oauth/token?response_type=code&client_id=test&redirect_uri=重定向页面链接。请求成功返回code授权码,一般有效时间是10分钟。
(3)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
POST /oauth/token?response_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=重定向页面链接。
1
2
3
4
5
6
7
8
9
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
1
2
3
4
5
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下。一般不支持refresh token。

(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
1
2
3
4
5
6
客户端模式(Client Credentials Grant)

指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。

区块链扩容方案

区块链自诞生一来,一直受性能问题困扰。

因为区块链跟传统的分布式系统(比如分布式数据库)有一个很大的不同,就是每个节点都是副本,要同步所有的数据并执行同样的处理,而传统的分布式数据库一般是三副本。这就导致分布式数据库增加节点可以提升性能,而区块链增加节点,不但不能提升性能,反而会降低性能(通信量变大)。

横向扩展不行,那还有纵向扩展,即提升节点的硬件配置。

可惜这一条路也被堵死了,因为在区块链里,去中心化是绝对的政治正确,提升节点的硬件要求,会把一部分人挡在外面,导致链更中心化了。

大区块方案其实就是间接的纵向扩展。因为区块扩大了,能打包的交易多了,但是出块间隔不变。加(工作)量不加价,对打工人的要求当然就更高了。

所谓开源节流,既然开源这边横竖都不行,自然只能节流了。怎么节流?把一部分工作推出去,担子轻省了,自然跑得就快了。

状态通道就是最早的这一类方案,类似的还有闪电网络,雷电网络等等,原理都差不多。

因为早期区块链(就是比特币)只有转账操作,这些方案也基本上只支持转账操作,所以也叫支付通道。

打个比方,比如5个同事去饭店聚餐,总共消费200元,AA一下每个人40。如果每个人都直接给老板支付40,加上找零什么的,老板一下就忙不过来了。有没有更省事的办法呢?老板说你们5个人先把200凑出来,没有零钱,相互找零都内部搞定,完了再一起给我。

这里老板就相当于区块链,5个人内部相互支付就是支付通道。

状态通道的缺点:

  1. 人不好凑。几个人是同事还好,几个陌生人就不能这么搞了。
  2. 相比而言资金还是有风险的。你给了同事50,同事要找你10,但是现在没零钱,说回头给你,结果回头就忘记了。但是老板是不敢这么做的,吃饭多收钱店都开不下去了。

接下来是侧链方案,有点类似于影分身,一个人工作压力山大,多召唤几个分身一起分担。这个方案跟状态通道不一样,这个是直接把整个链都搞了多套,说白了就是多链方案。

这类方案的问题是,工作倒是分担了,扯皮也跟着多了起来。山头林立,到底听谁的?

分片,Plasma都属于这一类的变种,为了弥补其缺点各种打补丁,搞得方案巨复杂,这里就不详述了。

终于,本文的主角Rollup出现了。

跟前面的横向扩展,纵向扩展类似。多链可以认为是横向扩展,而Rollup则是纵向扩展。所以Rollup被称为layer 2(两层)方案。

1
其实一般侧链,Plasma等也会被划入layer 2方案。但是我觉得它们的区别还是挺明显的,所以这里我并没有按照惯用的说法,请大家注意。

前面大家折腾了很久的多链方案之后,终于发现这些漏洞是按下葫芦浮起瓢,怎么都堵不住。专业的说法是,数据可用性和计算正确性无法同时保证。

因为Plasma想把数据和计算都平分到多条链里面,而Rollup则是分工一下,layer 1负责数据可用性,layer 2只负责计算。

Rollup

该方案对链的性能提升来自几个方面:

  1. 聚合器收集用户交易,然后批量打包发送到layer 1上,可以节省gas。在同样的gas limit下,区块可以打包更多的交易。
    • 对交易进行压缩。去除非必要字段,甚至直接数据压缩。
    • calldata的方式将交易发送到链上,这种存储形式的gas消耗更低。
  2. 链外计算,layer 1的负担减轻了。
  3. Arbitrum项目还提出了序列器方案,由中心化系统对交易进行排序,进一步提升系统性能。

当然这里面也是有安全问题的,因此还设置了复杂的挑战机制,通过举报有奖的方式来解决可能出现的欺诈问题。

联盟链中Rollup

然而,对于联盟链来说,Rollup提升性能方面的作用并不重要。

一方面,联盟链的性能压力不大;另外一方面,联盟链里没有gas,方案里那些节省gas的奇技淫巧根本没用。

那么我们为什么要花这么多精力去研究它呢?

因为Rollup方案有一个被忽视的,其实非常重要的作用,就是它提供了区块链进一步解耦的思路:

  1. 用户直接与聚合器进行交互,而不是链。用户接口和相应的数据结构(交易结构)与链解耦。
  2. 合约引擎与链解耦。
    • Layer 1链只提供存证功能,保证原始交易的数据有效性,和对计算结果的存证。
    • 合约部分将跟传统应用开发类似,开发者可以用更灵活的方式开发智能合约。
  3. 序列器提供了去中心化与中心化结合的思路。用户可以根据信任程度走不同的路径,甚至在信任的前提下,数据有效性可以由单一中心化系统保证,更好的与传统系统结合。

然后我们就可以让整个联盟链系统更加贴近应用场景:

  1. 用户接口可以更贴近应用,设计为更适合应用的方式。比如:
    • 数据结构可以针对特定应用优化。
    • 验证方式不一定使用数字签名,可以用更传统的用户名/密码等方式。
  2. 智能合约开发形式更接近传统应用。
    • 使用通用编程语言。
    • 智能合约是一个单独的系统,设计更加灵活。
  3. 实现去中心化和中心化相结合的方式,更加贴近现实场景。
    • 用户信任中心化系统时,可以走中心化系统,作为fast path
    • 用户不信任中心化系统的时候,走去中心化系统作为slow path

当然这里面还有很多技术挑战,比如:

  1. 用通用语言编写智能合约,怎么保证其执行的确定性?
  2. layer 2要证明计算的正确性,就要给出相应的密码学证据,是否有比以太坊基于默克尔树的方案更轻量的方案?
  3. 用户在不同的路径间切换时,如何处理回退等情况,如何让用户体验更好,甚至无感?

对具体方案有兴趣的小伙伴欢迎关注CITA-Cloud中相应的RFC,也欢迎大家一起来探讨相关的话题。

区块链的抽象

引入Rollup的思路之后,联盟链从抽象角度会变得更像可信计算和存证的结合。

这里的calleradd_server就是很传统的可信计算的关系。

可信计算已经发展出了非常多的先进技术,比如零知识证明之类。但是欺诈者足够无耻的话,你总是拿他没办法,而区块链可以以仲裁者的角色,填补上这个漏洞。

问题描述

以前,在操作系统的安装过程中:首先,我们需要准备操作系统镜像(通常是 ISO 镜像),并制作启动盘以从中启动。然后,在系统安装的过程中,用户与安装界面交互,填写必要的操作系统安装参数。最后,操作系统安装工具根据用户填写的参数完成系统安装与配置。

后来,出现无人值守安装:即预先将在以往安装过程中需要填写的操作参数写入配置文件。然后,向操作系统安装工具传递该配置文件的路径或读取方法。最后,操作系统安装工具在启动时,自动读取配置文件,以自动完成操作系统安装。

现在,我们发现:对于已安装完成的操作系统,能够通过克隆复制,直接得到可运行的操作系统。例如:对于安装到硬盘的操作系统,我们能够使用硬盘工具,直接克隆到新硬盘;对于安装到虚拟机的操作系统,我们能够直接复制虚拟机磁盘文件。所有没有必要再进行复杂、冗长的操作系统安装过程。

解决方案

cloud-init,正是这样一个工具,通过 cloud-init 装置,我们能够彻底省略安装过程,利用已完成安装的虚拟磁盘文件,更快速的完成操作系统部署过程。

cloud-init 是行业标准的跨平台云实例初始化的多分发方法,所有主要的公共云提供商、私有云基础设施的供应系统和裸机安装都支持它。多数云环境都是通过这种方式来完成操作系统的快速安装部署。

原理简述

为了使用 cloud-init 装置,我们需要准备 镜像 与 配置 两样东西:

  • 1)镜像:是已经安装完成的操作系统镜像文件(比如虚拟机磁盘 VMDK 文件),而不再需要操作系统安装镜像;
  • 2)配置:我们再编写描述操作系统信息的配置文件,该配置文件包含 主机名、帐号密码、网络配置、磁盘配置 等等配置信息;
  • 3)当镜像启动时,在镜像内置的 cloud-init 进程随之启动,通过读取该配置文件,在启动过程中直接完成操作系统的配置;

概念术语

Cloud Image

我们前面提到的“镜像”,在 cloud-init 中,被称为“Cloud Image”:

  • 1)它是个已完成操作系统安装的磁盘文件;
  • 2)每个虚拟机的磁盘都是 Cloud Image 的克隆;

Datasource

我们前面提到的“配置”,在 cloud-init 中,被称为“Datasource”:

  • 1)为每个虚拟机提供各种配置,比如 Hostname、Network Configuration、Password 等等;
  • 2)该配置由用户负责编写;

Datasource 主要提供两个配置文件:user-data;meta-data;

获取 Datasource 的常用方法有两种:

  • 1)HTTP:通过 HTTP 获取配置文件地址,而地址已预先硬编码到 Cloud Image 中(很多云厂商使用这种方式);
  • 2)NoCloud:将配置文件打包到 ISO 镜像,并挂载到虚拟机中;

cloud-init

被集成到 Cloud Image 中,当镜像启动时,将运行 cloud-init 进程;

当运行 cloud-init 服务时,主要完成两项工作:

  • 1)探测并读取 Datasource 配置;
  • 2)将这些配置应用到当前的虚拟机实例中;

安装使用

第一步、创建镜像

下载 Cloud Image 文件:https://cloud-images.ubuntu.com/

# .img 文件为 QCOW2 格式
wget http://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img

# 创建磁盘文件 
qemu-img convert -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw
qemu-img resize focal-server-cloudimg-amd64.raw 100G

# 创建磁盘文件:此方式使用 QCOW2 的写时复制特性,hal9000.img 引用 focal-server-cloudimg-amd64.img 文件,空间占用小
qemu-img create -b focal-server-cloudimg-amd64.img -f qcow2 -F qcow2 hal9000.img 10G

第二步、创建配置

apt-get install whois cloud-image-utils

mkpasswd -m sha512crypt 123456 -S "12345342"

cat > user-data <<EOF
#cloud-config

hostname: focal-server
manage_etc_hosts: localhost

users:
  - name: root
    lock_passwd: false
    hashed_passwd: '<the output of mkpassword...>'
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1y...

# SSH
ssh_pwauth: True
disable_root: false
EOF

cloud-localds user-data.iso user-data

第三步、创建虚拟机

然后,在虚拟机中同时挂载 focal-server-cloudimg-amd64.raw 与 user-data.iso 文件;

virt-install              \
    --vcpus=4             \
    --ram=8192            \
    --import              \
    --os-variant=ubuntu20.04      \
    --network network=cluster-network,model=virtio                \
    --graphics vnc,listen=0.0.0.0 \
    --noautoconsole               \
    --disk path=/srv/isos/user-data.iso,device=cdrom              \
    --disk path=/srv/image/focal-server-cloudimg-amd64.raw,format=qcow2   \
    --name=focal-server

当开机启动完成后,便可通过 root/123456 进行登录;

参考文献

cloud-init Documentation — cloud-init 22.1 documentation

12.04 - Default username/password for Ubuntu Cloud image? - Ask Ubuntu

Networking Config Version 2 — cloud-init 22.1 documentation

linux - How do I set a password on an Ubuntu cloud image? - Server Fault

How to set root password with cloud-config? · Issue #659 · vmware/photon · GitHub

12.04 - Default username/password for Ubuntu Cloud image? - Ask Ubuntu

Cloud-init To Enable Cloud Image Root Login – TCC Consulting Limited

mcwhirter.com.au/craige/blog/2015/Enable Root Login Over SSH With Cloud-Init on OpenStack

Creating a VM using Libvirt, Cloud Image and Cloud-Init | Sumit’s Dreams of Electric Sheeps

绚磊:

做一个简单的开发过程中遇到的使用Mybatis plus更新统计数据,存在线程安全问题,也是分布式事务问题的分享吧。[呲牙]

绚磊:

前提:
在我们开发中常常会遇到统计需求。例如,针对一个文件的查看次数的统计。统计的逻辑,挺简单就是数据库数据自增。
在我们的系统中,现在大部分使用的ORM框架是Mybatis plus。Mybatis plus的特点是,将对数据库操作的sql封装成java
程序员熟悉的class对象方法。
通过Mybatis plus来对 数据库自增往往会有如下的操作。

绚磊:

假设数据存在t_file表,创建File对象如下

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@TableName("t_file")
@AllArgsConstructor
@NoArgsConstructor
public class File {

private Integer id;

private String filename;

private Integer times;
}

绚磊:

生成Mybatis plus的 mapper 接口类

1
2
3
@Mapper
public interface FileMapper extends BaseMapper<File> {
}

绚磊:

生成service层对象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class FileServiceImpl extends ServiceImpl<FileMapper, File> implements FileService {


@Override
public Integer increase(){
//获取 文件 在数据库存储的信息
File file = this.baseMapper.selectById(1);
Integer times = file.getTimes();
//对文件的获取次数进行 增加 1 次
file.setTimes(file.getTimes() 1);
//更新数据库数据
this.baseMapper.updateById(file);
//返回增加后的获取次数
return file.getTimes();
}
}

绚磊:

咋一看可能没发现问题,通过简单的单个请求测试也会发现功能正常。但实际上,大量请求并发执行时,会造成统计数据小于实际请求数据量。

绚磊:

下面进行下问题复现吧,我这边新建了一个测试项目。

绚磊:

启动项目,打上断点 。断点挂在 Thread线程上

绚磊:

表里 初始数据

绚磊:

从浏览器发送 两次请求

绚磊:

可以看到 两个请求进来了。

绚磊:

后续的结果很明显了,http-nio-7777-exec-1和http-nio-7777-exec-2获取到的 id=1 的times 都是0

绚磊:

后续进行了 times= times 1,然后更新表后,会发现虽然进行了两次请求,但实际数据库表里的数据为1次。

绚磊:

整理下逻辑:

绚磊:

1.当A请求,请求到服务器,开启线程1执行 File file = this.baseMapper.selectById(1);语句完成后,进入阻塞,此时file.getTimes() 的值为 0。
2.此时B请求,请求到服务器,开启线程2,也执行到File file = this.baseMapper.selectById(1);语句完成后,进入阻塞,此时file.getTimes() 的值也为 0。
3.此时可以明显发现问题出现了
4.后续A请求,B请求无论哪个先执行完,更新到数据库中的file.times 的 值 只会是1 【但请求数是2】

绚磊:

接下详细分析出现问题的原因:

绚磊:

这是一个很明显的线程安全问题,多个线程操作同一个资源。此外,由于这是对数据库记录的操作,这也涉及到数据库的事务,事务隔离级别,我们发现上面的代码,没有使用到数据库事务,
那么是否有可能加上事务就会解决问题了呢,这里先把结论说出来吧,实际上加上事务也是会存在问题的。总结下,上面线程安全问题的原因是,多线程通过了一系列非原子性的操作,修改了
相同的资源。接下来我们分下下数据库层面,默认情况下,MySQL。默认情况下,MySQL 的 innodb数据引擎的事务隔离级别是 RR,也就是Repeatable Read,
rr的原理是 MVCC多版本并发控制 悲观锁 乐观锁。从数据库事务角度分析上面问题,其实是事务A通过select * from file 获取到的对象 file id =1; name=”file”,times=1【这是一个快照snapshot】。
事务B 获取到的也是一个快照,获取到的也是 file.times = 1。所以也最终导致了最后 统计数据 小于 实际请求数量。

绚磊:

说了这么多,还没说解决方法。接下去说说几种解决方法,根据问题分析得出造成问题的原因有两点:1.多线程 2.非原子性操作。
那么可以从两个维度来解决问题:1.对多线程操作的方法加锁 2.将操作修改成 原子性操作。
先说说第一个维度的几个方案,加锁:分别从数据库,java单个应用,分布式多个应用做出方案。
首先由于这个场景是针对数据库的数据做统计自增操作,上面也分析了,也有数据库事务隔离级别的影响,那么是否能考虑将事务隔离级别调整下,原本默认是REPEATABLE_READ,调整成SERIALIZABLE。然后测试下看。

绚磊:

修改 FileServiceImpl类

绚磊:

测试结果,两个请求获取到的仍然是0

绚磊:

第一个请求,update数据库成功,第二个请求,报死锁错误。

绚磊:

这里可能涉及到 mysql 锁机制,后续我再测试分析下。

绚磊:

总结此方法:能实现数据的安全。但是只能有成功一个请求,实际编码中不推荐此方法。

绚磊:

第二个方案:加synchronized 或者 ReentrantLock 来解决,多线程问题。

绚磊:

修改Service类

绚磊:

测试打上断点,两个请求并发执行,会发现第一个请求进入了断点,第二个请求没有进入断点,但是浏览器上第二个请求阻塞了。

绚磊:

最后的执行结果:正确

绚磊:

但是 此方法:仅适用于单节点应用,现在基本上都是前后端分离的分布式系统。可以想象下,启动多个节点服务,此时两个请求分别打在两个服务上。实际上并不存在,线程竞争。此时问题变成了,分布式事务问题。

绚磊:

所以 就有个第三个方案:方案加分布式锁,例如利用redis加分布式锁。这里就不演示了,此方法肯定是可行的,但是增加了逻辑上的复杂度,带来了分布式锁的加锁和解锁问题,还涉及到等待请求的公平与非公平。

绚磊:

最后 思考下,是否还有其他方案呢,其实从原子性操作角度分析,由于我们在进行自增操作的时候是先获取了数据库的值,然后再进行java逻辑层的自增,再更新到mysql数据库中。这样获取值和更新不是一个原子操作。通过自定义 sql 。 不使用mybatis plus提供的方法,来做操作 。这样就能保证自增的原子性了。

绚磊:

修改方法为:

绚磊:

测试结果:

绚磊:

分析:有同学可能会问,两个事务下线程A和线程B同时阻塞在,update操作;然后线程A先commit,接着线程B再commit,是否会有问题。

由于update 在本身就是X锁排他锁,线程A,线程B并发执行,只有一个线程能获取到锁。假设线程B获取到锁,那么线程A就进入等待。

线程B执行完 update 语句后,但是未提交,生成一条redo log;

线程A执行完 update 语句后,但是未提交,生成一条redo log;

通过sql可以查询到获取锁的sql

1
2
3
SELECT * FROM performance_schema.events_statements_history WHERE thread_id IN(
SELECT b.`THREAD_ID` FROM sys.`innodb_lock_waits` AS a , performance_schema.threads AS b
WHERE a.`blocking_pid` = b.`PROCESSLIST_ID`)

绚磊:

  • 此时执行select times from t_vc where id=5 for update;会阻塞。因为在采用INNODB的MySQL中,更新操作默认会加行级锁.所以窗口二这里会卡住。

  • 然后线程A commit,释放锁;线程Bcommit,释放锁。

绚磊:

从执行日志看:

绚磊:

先后执行了更新操作。

绚磊:

最后总结下: 通过自定义sql update table set a=a 1 where id=1 来解决 统计数据的自增问题,最为合适

.now:

hello大家好,这两天考虑了很久要分享什么,最后决定就从最近身边发现的可优化的小方案入手吧。今天给大家分享的是基于Spring AOP实现某些重要信息的加解密跟脱敏。

.now:

AOP面向切面编程,通过预编译方式和运行期间动态代理实现程序功能统一维护,这些原理网上一搜一大把,这次就不多搬那些原理过来了,主要就讲讲应用于加解密脱敏方面。

.now:

因为日常业务开发中,经常有碰到一些比较重要的用户信息,比如手机号,身份证号,邮箱,我们就直接大咧咧地明文保存到数据库里,万一数据库被黑,这些信息就全暴露了,用户就可能遭受到短信轰炸,身份证号被冒用等恶劣行为,数据安全很重要,所以就有必要进行加密后保存,查询时解密返回,页面脱敏显示比如手机号只展示前三后四,如:18812349876 置换为 188****9876。

.now:

之前看到有些项目返回时是手动调用工具类替换后再返回,如下这样:

.now:

如果多个业务需要做加解密脱敏处理,每次新增都自己手动调用加密方法,每次查询都自己手动进行解密或者脱敏处理,就会多出很多重复代码,代码就显得很笨重。特别是后续万一修改了公共方法,就有可能要对应所有调用它的地方。

.now:

本次介绍的就是基于Spring AOP 注解的方式,实现加解密脱敏,对service层的业务代码无侵入。

.now:

涉及的代码如下,DecryptField,DesensitizedField,EncryptField三个注解用于标记需要处理的字段,NeedDecrypt,NeedDesensitized,NeedEncrypt三个注解用于标记切入点,DecryptAspect,DesensitizedAspect,EncryptAspect三个切面配置类,具体实现真正的加解密脱敏。

.now:

这里先提一下Spring AOP 中的通知类型,Spring AOP 中有5种通知类型分别如下:

.now:

这次介绍的主要就用到了Before和AfterReturning,在请求过来时,新增方法调用前进行加密配置,在查询返回后进行解密脱敏

.now:

接下来具体使用看看,首先看一下加密的切面类EncryptAspect,切面里定义了切入点是指定的注解@NeedEncrypt,在before方法里进行具体的加密处理,先找到被注解@EncryptField标记的字段,然后对该字段的值进行加密处理。里面具体的加密方式就可以自己自定义了,看自己需求。

.now:

我们只需要在方法添加【@NeedEncrypt】注解

.now:

在需要加密字段添加注解【@EncryptField】

.now:

调用接口,查看数据库,就可以看到对应的字段加密成功

.now:

同样的,只需要在方法添加【@NeedDecrypt】注解,

.now:

需要解密字段添加注解【@DecryptField】

.now:

调用接口,就可以看到对应的字段解密成功

.now:

脱敏也是如此,在方法添加【@NeedDesensitized】注解,需要脱敏的字段添加注解【@DesensitizedField】

.now:

最后调用接口可以看到返回脱敏成功

.now:

由于都是一样的原理,就没有对解密跟脱敏的切面类做截图说明了,具体的规则可以根据需求自由发挥下

.now:

这样,我们就在不修改service层源代码的前提下,去为系统的重要用户信息添加了加解密脱敏通用功能,提高程序的可重用性,同时提高了开发的效率。

.now:

我的介绍就到这里了,介绍的比较简单,但我觉得在日常开发中还是比较实用的,希望能对大家有所帮助。

渲染:

给大家分享下低代码。也是最近我看到的一篇从实现上讲低代码的好文,整理了下,加上了一些自己的看法,分享给大家

渲染:

那么什么是低代码呢?低代码是指通过少量代码就可以快速生成应用程序的开发平台。.通过可视化进行应用程序开发的方法,使具有不同经验水平的开发人员可以通过图形化的用户界面,使用拖拽组件和模型驱动的逻辑来创建网页和移动应用程序。

渲染:

也就是说相对明确的一点是 可视化 是低代码唯一不可缺少的功能

渲染:

那么对于实现可视化编辑来说,必要条件又是什么呢?

渲染:

是声明式

渲染:

与声明式相对的,还有一种编码模式叫命令式。下面先来解释下什么是声明式、什么是命令式?

渲染:

比如我们要实现一个蓝色的方块,就拿我所用的前端的语言来说吧,HTML、CSS就是声明式的,来实现它,我只需要这么写

渲染:

渲染:

而如果换成使用命令式的javascript来实现,则可能会这么写

渲染:

渲染:

两种方式最终展现效果是一样的,但我们可以看到,这两种代码在实现思路上有本质区别

渲染:

声明式直接描述最终效果,不关心如何实现。
而命令式则关注如何实现,明确怎么一步步达到这个效果。

渲染:

回到可视化编辑器的角度看,它们的最大区别是:声明式可以直接从展现结果反向推导回源码,而命令式则无法做到反向推导

渲染:

反向推导是编辑器必备功能,比如编辑器里的常见操作是点选这个区块,然后修改它的颜色,在这两种代码中如何实现?

渲染:

如果是声明式的 HTML CSS,可以直接改style的background值,而基于 Canvas 的命令式代码则无法实现这个功能,因为无法从展现找到实现它的代码,因为命令式代码实现同样效果的可能方式是无数的,除了前面的示例,下面这段代码也可以实现一样的效果:画一条长100,粗100的线段,在最终视觉呈现上也可以看做是一个矩形。

渲染:

渲染:

因此我可以简单得到一下结论:命令式代码无法实现可视化编辑,而可视化编辑是低代码唯一不可少的功能,所以所有低代码平台必然只能采用声明式代码,这也是为什么所有低代码平台都会有内置的DSL。

渲染:

这些声明式语言有以下优点:
1、容易上手,因为描述的是结果,语法可以做得简单,非研发也能快速上手 HTML 及 SQL。
2、支持可视化编辑,微软的 HTML 可视化编辑 FrontPage 在 1995 年就有了,现在各种 BI 软件可以认为是 SQL 的可视化编辑。
3、容易优化性能,无论是浏览器还是数据库都在不断优化,比如可以自动改成并行执行,这是命令式语言无法自动实现的。
4、容易移植,容易向下兼容,现在的浏览器能轻松渲染 30 年前的HTML,而现在的编译器没法编译 30 年前的浏览器引擎代码。

渲染:

而这些语言的缺点是:
1、只适合特定领域,命令式的语言比如 JavaScript 可以用在各种领域,但 HTML CSS 只适合渲染文档及界面,SQL 只适合做查询。
2、灵活性差,比如 SQL 虽然内置了很多函数,但想只靠它实现业务是远远不够的
3、调试困难,遇到问题时如缺乏工具会难以排查,如果你在Firefox出现前开发过页面就会知道,由于IE6没有开发工具,编写复杂页面体验很差,遇到问题要看很久代码才发现是某个标签没闭合或者 CSS 类名写错了。

渲染:

4、强依赖运行环境,因为声明式只描述结果而不关注实现,因此强依赖运行环境,但这也带来了以下问题:

  • 1)、功能取决于运行环境,比如浏览器对 CSS 的支持程度决定某个属性是否有人用,虽然出现了新的CSS提案,但 Firefox 和 Safari 都不支持,而且上手成本太高,预计以后也不会流行。
  • 2)、性能取决于运行环境,比如同一个 SQL 在不同数据库下性能有很大区别。
  • 3)、对使用者是黑盒,使用者难以知道最终实现,就像很少人知道数据库及浏览器的实现细节,完全当成黑盒来使用,一旦遇到性能问题可能就不知所了。
  • 4)、技术锁定,因为即便是最开放的 HTML 也无法解决,很多年前许多网站只支持 IE,现在又变成了只支持 Chrome,微软和 Opera 在挣扎了很多年后也干脆直接转向用 Chromium。同样的即便有 SQL 标准,现在用的 Oracle/SQL Server 应用也没法轻松迁移到 Postgres/MySQL 上。低代码行业未来也一样,即便出了标准也解决不了锁定问题,更有可能是像小程序标准那样发展缓慢,功能远落后于微信。

渲染:

因为低代码就是一种声明式编程,所以这些声明式优缺点,其实就是低代码的优缺点。

渲染:

了解了声明式,下面来说说低代码的实现方案

渲染:

以前端的实现来说,其核心是界面渲染。前面提到前端 HTML CSS 可以看成一种描述界面的低代码 DSL,因此前端界面实现低代码会比较容易,只需要对 HTML CSS 进行更进一步封装,定义JSON schema。比如用类似如下的方式来描述页面:

渲染:

渲染:

这里大家几乎全都使用 JSON主要是两方面原因:

  1. 低代码平台编辑器几乎都是基于 Web 实现,JavaScript 可以方便操作 JSON。
  2. JSON 可以支持双向编辑,它的读取和写入是一一对应的。

渲染:

因此界面呈现上的低代码实现起来,我们只需要丰富物料库,通过拖拽这些物料拼出想要的东西后生成json描述即可。

渲染:

再来说说交互逻辑的实现。

渲染:

前面说到前端界面低代码是比较容易,但交互及逻辑处理却很难低代码化,目前常见实现有三种方案:

1、使用图形化编程;

2、固化交互行为;

3、使用 JavaScript;

渲染:

先说第一种图形化编程,这是非常自然的想法,既然低代码的关键是可视化,那直接使用图形化的方式编程不就行了?

渲染:

但我们发现这么做局限性很大,本质的原因是命令式的代码无法可视化。即便我们将循环、分支判断、或操作符等等这些抽象为一块块的积木,我们也难以像拼接积木一样得到我们想要的东西,因为积木拼接这种方式只适合用来实现简单的逻辑,对于复杂的交互逻辑非常难以实现。

渲染:

再来说固化交互行为,如果是面向特定领域,低代码平台可以先将这个领域难以图形化的逻辑预置好,让使用者只需做简单的处理,使用的时候只需要调整参数就行。当然这个方案最大的缺点是灵活性很低。

渲染:

因此要实现更灵活的控制,还是得支持第三个方案:JavaScript,目前很多低代码平台只在界面编辑提供可视化编辑,一旦涉及到交互就得写 JavaScript,但该方案脱离了低代码范畴、不是低代码。

渲染:

下面来看个实例,以阿里的datav中的蓝图编辑器为例,它就是同时支持了3种方案进行互补:

渲染:

渲染:

一些简单逻辑用户可以自己通过蓝图编辑器去添加然后串并联这些节点来实现。对于使用场景较多的一些较复杂的行为可以内置固话。而对于较复杂的逻辑用户可以自己通过js处理。

渲染:

最后总结一下吧。其实可以看出在定制化较强的业务中低代码可以说是毫无用武之地,但基于特定领域或方向的低代码平台还是很有意义的。将其作为一类工具,趁手时就用。

涛涛:

今天给大家分享的是一个分布式链路追踪工具SkyWalking

涛涛:

介绍这个工具是根据之前space项目优化tps 导致程序中需要频繁记录日志排查影响tps,所以了解了下链路是追踪工具(未投入使用)

涛涛:

提到分布式链路追踪工具大家第一点想到的是zipkin ,对比zipkin,SkyWalking 的优势接下来会逐一介绍

涛涛:

skywalking支持dubbo,SpringCloud,SpringBoot集成,代码无侵入,通信方式采用GRPC,性能较好,实现方式是java探针,支持告警,支持JVM监控,支持全局调用统计等等,功能较完善

涛涛:

skywalking采用字节码增强的技术实现代码无侵入,zipKin代码侵入性比较高
skywalking功能比较丰富,报表统计,UI界面更加人性化
所以针对目前服务未使用过链路追踪工具,建议使用SkyWalking

涛涛:

上边的是SkyWalking的一个架构图

涛涛:

上面的Agent:负责收集日志数据,并且传递给中间的OAP服务器
中间的OAP:负责接收 Agent 发送的 Tracing 和Metric的数据信息,然后进行分析(Analysis Core) ,存储到外部存储器( Storage ),最终提供查询( Query )功能。
左面的UI:负责提供web控制台,查看链路,查看各种指标,性能等等。
右面Storage:负责数据的存储,支持多种存储类型(根据系统使用量决定存储类型)。

涛涛:

skywalking在性能剖析方面真的是非常强大,提供到基于堆栈的分析结果,能够让运维人员一眼定位到问题。

涛涛:

我们在代码中故意休眠了2秒,看看如何在skywalking中定位这个问题。

涛涛:

在性能剖析模块->新建任务->选择服务、填写端点、监控时间,操作如下图:

涛涛:

上图中选择了最大采样数为5,则直接访问5次:http://localhost:8888/order/list,然后选择这个任务将会出现监控到的数据

涛涛:

可以看到{GET}/order/list这个接口上耗费了2秒以上,因此选择这个接口点击分析,可以看到详细的堆栈信息

涛涛:

直接可以定位到代码中睡眠2秒钟

涛涛:

skywalking 监控带有默认的规则 同时还适配了一些钩子(webhooks)。其实就是相当于一个回调,一旦触发了上述规则告警,skywalking则会调用配置的webhook,这样开发者就可以定制一些处理方法,比如发送邮件、微信、钉钉通知运维人员处理。

涛涛:

这就是国产软件的优势

涛涛:

对于代码入侵 skywalking 只是提供了agent 只要是普通的微服务即可,不需要引入什么依赖

涛涛:

只需要启动的时候增加启动命令即可

涛涛:

最后给大家提一个小建议 在我们选型中间件或者设计中间件的时候 尽量减少代码入侵行,可以大幅度的减少二次开发, 在满足其他中间件功能的同时 考虑新组件的兼容性、易用性

忆忆:

那我今天就分享BLS签名吧

忆忆:

BLS 签名不需要随机数,区块中的所有签名都可以组合成单个签名,m-of-n 类型的多重签名比较简单,不需要签名者之间进行多轮通信。

忆忆:

BLS签名需要用到两个结构:哈希到曲线以及曲线配对。哈希到曲线:一般签名将消息哈希视作数字,在这里将消息哈希到椭圆曲线(比较直接的方法是将哈希结果视作x坐标,选择曲线上对应的y较小的点); 曲线配对:一个将两个不同点映射成一个数字的特殊函数,假设该函数为e( · ,· ),两个任意的点为P,Q,则e( , )满足如下性质:e(a·P ,b·Q )= e(ab·P , Q )= e(P , ab·Q )=e(P , Q)^ (ab)

忆忆:

签名的具体方案:假设私钥为pk,公钥为P=pk️G,签名的消息为m。首先将消息哈希到曲线H(m),然后得到签名S=pk️H(m);验证签名:用公钥检查e(P, H(m))=e(G,S)为真即可

忆忆:

聚合签名:假设有(公钥Pi,已签名的消息mi),聚合签名为所有签名的简单和S=Sum (Si),只需验证e(G,S)=e(P1,H(m1))·…·e(Pn,H(mn))即可; n-of-n多重签名与Schnorr签名类似,也可以在简单加和时添加系数; m-of-n签名:聚合公钥P=a1·P1 … an·Pn,ai=hash(Pi,{P1,P2,…,Pn}),成员密钥MKi=Sum (ai·pki)️H(P,i),成员签名Si=pki️H(P,m) MKi,连个签名与联合公钥(S’,P’)只需将成员签名和公钥简单相加即可,验证e(G,S’)= e(P’,H(P,m))· e(P, Sum (H(P,i)))

忆忆:

BLS的弊端在于配对效率低下,函数e(P,Q)的验证难度较大,验证时间长。 我的分享就到这里

大勇:

今天分享与云端的流量入口、流量代理有关。

大勇:

也看过一些云平台的部署架构、拓扑结构图之类的,发现很多,对流量代理、入口的界定不是很清晰。 有的把ingress 当成流量代理、有的认为是K8S service、有的认为是kube-proxy 等。 我这里再梳理下,其实以上都不是流量代理。

大勇:

ingress 是K8S 的一种原生资源(有别于CRUD的自定义资源),是描述具体的路由规则的。

大勇:

Ingress Controller 是才是流量的入口,是名副其实的流量代理,也可以称为边缘服务(集群内与集群外流量网关),纵向流量(有叫南北流量的说法), 一般是Nginx 、traefik和 Haproxy(较少使用)。

大勇:

Ingerss 描述了一个或者多个 域名的路由规则,以 ingress 资源的形式存在。
简单说: Ingress 描述路由规则。

大勇:

可以认为ingress 其实就是一个配置文件,而已,在你真正访问pod应用的时候 是没起任何作用的

大勇:

但是这个配置文件又不能删除, 这是因为Ingress Controller 实时实现规则。Ingress Controller 会监听 api server上的 /ingresses 资源 并实时生效。

大勇:

是Ingress Controller 是实时监听 api server 上ingresses ,如果没有了,会马上删除 自身的 反向代理配置 ,才让ingress 好像干掉了 就访问不了了。

大勇:

Ingress Controller 流量代理 自身的部署方式 以及服务类型 会决定了,整个K8S 的流量瓶颈,是每个节点的流量决定,还是整个集群所有的节点一起决定的。 这个先放后面,接下来讲,service与 kube-proxy 是否是流量代理的分析

大勇:

ingerss 与service的关系:

大勇:

ingress 在前,service在后, 前者不是流量代理, 顺利成章的会认为是后者,其实也不然。

大勇:

看K8S 对service的概念介绍:Service 概念
Kubernetes Service定义了这样一种抽象: Service是一种可以访问 Pod逻辑分组的策略, Service通常是通过 Label Selector访问 Pod组。
Service能够提供负载均衡的能力。

大勇:

service 有以下种类型:

大勇:

ClusterIp、NodePort、LoadBalancer(升级版nodeport)

大勇:

Service 也只仅仅是一种策略, 也不是应用每次被访问时,需要经过的组件,也不负载流量的转发。

大勇:

这里可能会有疑问,不负载流量转发,怎么又能实现这么多类型的服务,又怎么实现负载均衡的。

大勇:

这时候就该神奇的kube-proxy K8S组件闪亮登场了

大勇:

kube-proxy是Kubernetes的核心组件,部署在每个Node节点上,它是实现Kubernetes Service的通信与负载均衡机制的重要组件; kube-proxy负责为Pod创建代理服务,从apiserver获取所有server信息,并根据server信息创建代理服务,实现server到Pod的请求路由和转发,从而实现K8s层级的虚拟转发网络。

在k8s中,提供相同服务的一组pod可以抽象成一个service,通过service提供的统一入口对外提供服务,每个service都有一个虚拟IP地址(VIP)和端口号供客户端访问。kube-proxy存在于各个node节点上,主要用于Service功能的实现,具体来说,就是实现集群内的客户端pod访问service,或者是集群外的主机通过NodePort等方式访问service。在当前版本的k8s中,kube-proxy默认使用的是iptables模式,通过各个node节点上的iptables规则来实现service的负载均衡,但是随着service数量的增大,iptables模式由于线性查找匹配、全量更新等特点,其性能会显著下降。

大勇:

kube-proxy当前实现了三种代理模式:userspace, iptables, ipvs。其中userspace mode是v1.0及之前版本的默认模式,从v1.1版本中开始增加了iptables mode,在v1.2版本中正式替代userspace模式成为默认模式。也就是说kubernetes在v1.2版本之前是默认模式, v1.2版本之后默认模式是iptables。

大勇:

linux 分User space(用户空间)和 Kernel space(内核空间)。 在K8S 1.0 版本之前,kube-proxy 确实是流量代理

大勇:

每次的访问都是 从User space 到Kernel space 再到 User space 里的 kube-proxy

大勇:

显然这种方式会 因为K8S 底层组件的流量瓶颈,会影响整个上层应用生态

大勇:

使用userspace 的资源,比使用kernelspace的资源要昂贵的多, 这里引申一下, 能用TCP的场景,要比用HTTP的 节省资源。其他先不分析, 就工作层级来讲 TCP是工作在 kernelspace ,http是userspace 是应用层协议。

大勇:

第二个阶段,iptables mode, 该模式完全利用内核iptables来实现service的代理和LB, 这是K8s在v1.2及之后版本默认模式

大勇:

kube-proxy 在第二阶段 使用iptables mode 时,已经部署流量代理了,这些都交给 节点机器的iptables 路由表 来流量转换了, 而kube-proxy 只需要从api server 拿到service的策略配置,来实时维护好iptables 即可

大勇:

第二阶段,随着服务越来越多,维护Node上的iptables rules将会非常庞大,性能还会再打折扣

大勇:

故迎来了 第三阶段ipvs mode. 当下 一般的云都是这种模式

大勇:

在kubernetes 1.8以上的版本中,对于kube-proxy组件增加了除iptables模式和用户模式之外还支持ipvs模式。kube-proxy ipvs 是基于 NAT 实现的,通过ipvs的NAT模式,对访问k8s service的请求进行虚IP到POD IP的转发。

大勇:

kubedns/coredns 这个就是K8S的 dns服务器而已,不是流量代理,只提供了一个服务名、主机名等的DNS解析

大勇:

kube-proxy 经过以上阶段,也不是流量代理了,不会是应用访问的流量影响者

大勇:

综合以上分析,现在来看,流量控制代理瓶颈,不会出现在 K8S的service

大勇:

kube-proxy

大勇:

只有可能出现在 ingress-controller

大勇:

实际k8s 官方也提供了 ingress-controller的实现,只是也不太好用,

大勇:

现在基本都用 Nginx 、traefik、Haproxy

大勇:

来说下Nginx 吧, 早期版本部署是通过 DaemonSet 的方式

大勇:

这种方式就是每个节点启用一个 nginx代理

大勇:

但是会把当前节点机器的 80、443 端口占用掉

大勇:

因为Nginx 自身的 service 的暴露类型是 采用的 hostport 方式 类似与nodeprot 但是没负载均衡,就是只能用POD所在的节点机器IP 访问到

大勇:

好在是 DaemonSet方式,反正每个机器都有,用每个节点的IP 都能访问到nginx服务,然后反向代理到 应用的服务

大勇:

后面流量网关也进行了部署升级,先看下华为云的把:

大勇:

可以看到 不用DaemonSet,不用每台都启了, 启动几个pod就行, 自身的 service 也通过LoadBalancer 类型暴露了

大勇:

LoadBalancer 类型是需要负载均衡器来支持了,这种类型,如果采用类似BGP模式,理论上相当于 K8S集群每个节点的都可以成为流量入口。 相当于ingress-controller 作为流量入口,流量网关,它理论上可以做到,K8S的 所有节点都能充当入口。 以达到流量的最大化。

大勇:

今天的分析就到这了,梳理了一个论点,即:ingress-controller 才是流量代理,ingress、service、kube-proxy都不是,理论上可以做到流量的最大化,K8S 从机制上来看,没有流量瓶颈的桎梏。 要有也是 节点机器、其它方面的。

0%