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

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


  • 首页

  • 归档

  • 搜索

「Git 内核」对象模型

发表于 2021-11-19

「这是我参与11月更文挑战的第 12 天,活动详情查看:2021最后一次更文挑战」


虽然很多程序员都熟悉使用git,但我更想深入了解git是如何实现的。git采用了许多巧妙的点子来优化常见的版本控制操作。学习git应该是不断练习而不是阅读大量文档,所谓纸上谈兵。所以这也是我写这篇文章的原因。

本篇文章是用 rust remake git,但你可以找到很多其他语言的git实现(比如: go)。

对象模型

这个系列并不是对git的介绍。网上有很多关于如何使用git的好的解释。但我确实想回顾一下git的几个核心概念,因为它们对理解git的工作原理很重要。

版本控制

git 是许多 “版本控制软件” 的软件之一,不过它是目前最流行的(Linus NB)。尽管这些VCS的实现在细节上有所不同,但它们有许多相同的核心思想。

版本控制的目的不仅仅是存储一组文件的当前状态,还包括这些文件随时间变化的历史。

这个历史可以被浏览、更新和共享,这使得版本控制在编辑历史很重要的应用中成为一个非常有用的工具,特别是在软件开发中。

提交

git 将其历史存储为一个快照的集合,称为 “commit“。你可以浏览任何提交的文件状态;这个操作被称为 “检查” 提交。

你可以把提交看作是你的代码的一系列备份,尽管我们后面会看到git有一些技巧来减少所有这些备份所需的存储空间。

每个提交都建立在先前的提交之上,这些提交被称为其 “父级” 提交。

在最简单的情况下,提交历史是 “线性” 的(也就是说成为一条直线),即每个提交都有一个父级和子级(除了第一个和最后一个提交)。

例如,可能有三个提交,依次是 A、B、C。下面我们可以用一个提交图来说明这一点:

1
css复制代码A --- B --- C

然而,即使在小项目中,提交历史也很少是完全线性的。git允许提交历史的 “分支”,即多个提交建立在同一个父提交之上。 这很有用,比如说,当开发人员在并行开发功能时,不希望在他们的功能最终完成之前影响对方的代码。这可能会导致如下的提交图:

1
2
3
css复制代码A --- B --- C --- D
\
E --- F --- G

**提交历史也可以 “合并”,即一个提交合并多个父提交。**在上面的例子中,提交G可能被合并到D,产生一个新的提交H。

1
2
3
css复制代码A --- B --- C --- D --- H
\ /
E --- F --- G

分支和标签

每个提交都有一个ID,叫做 “提交hash”(待会看到这个哈希值的来源)。尽管我们可以用哈希值来指代任何一个提交,但给提交命名往往是很方便的。

分支和标签就是完成上述的两种方法。两者都是对提交的引用。

不同的是,在分支上提交会更新分支,使其指向新的提交,而标签则始终指向同一个提交。

传统的做法是用主分支指向最新的可生产的提交,用 feature/xyz、fix/xyz 等来跟踪添加功能或修复错误的进展。标签主要用于标记对应于特定版本的发布。

本文转载自: 掘金

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

浅谈内存管理和虚拟内存 1 内存管理

发表于 2021-11-19

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」

前言

1
复制代码 大家好,我是程序猿小白 GW_gw,很高兴能和大家一起学习进步。

以下内容部分来自于网络,如有侵权,请联系我删除,本文仅用于学习交流,不用作任何商业用途。

摘要

1
复制代码 本文主要介绍内存管理和虚拟内存的一些基本概念。
  1. 内存管理

1.1 为什么要有内存管理

在很久以前,程序是放在卡片上的,计算机从卡片上读取指令来运行程序,但这种做法效率很低,并且灵活性很差。为了解决这个问题,人们发明了内存储器,将所需要运行的程序先加载到内存中去,然后执行。这就需要对程序进行存储,因此有了以下的内存架构。

1.1.1 内存架构

现代计算机的内存架构是:缓存,主存,磁盘组成。来看下面一张图来理解。

image-20211119103624818

而为了有效的管理内存架构,就有了内存管理的概念。

内存管理就是对内存架构进行管理,使程序在内存架构中存放的层次对用户来说使透明的。

1.2 内存管理的目标

首先如果是多个程序同时存放在内存中,那么它们之间不能访问互相访问对方的地址,否则是件很危险的事情。其次,我们在编写程序时不能确定将来我们的程序会在什么环境下运行,因此我们在使用地址时不能使用实际的物理地址。我们使用的其实是程序的虚拟空间地址。关于虚拟地址我们后面再说。

小结一下,内存管理要达到的两个目标:

  • 地址保护:一个程序不能访问另一个程序的地址空间。
  • 地址独立:程序发出的地址与物理主存地址无关。

1.3 虚拟内存

虚拟内存其实是一个抽象的概念,看名字就知道,虚拟内存是虚拟出来的,并不是真实存在的。那为什么会有虚拟内存呢?

我们知道,内存的容量是很小的,而程序又需要加载到内存中才能运行,那我们就只能把程序写的很小,为了解决这个问题,于是就提出了虚拟内存的概念。虚拟内存就相当于把磁盘也当作了主存来使用,那怎么来实现这一目的呢,当然是通过地址啦。通过虚拟地址来找到程序的实际物理地址,从而达到了”扩充主存”的目的。

小结

以上就是我对内存管理和虚拟内存的一些理解,希望能对读者有所帮助,如有不正之处,欢迎留言指正。

本文转载自: 掘金

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

删除链表的倒数第 N 个结点

发表于 2021-11-19

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

leetcode 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

1
2
ini复制代码输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]

示例 2:

1
2
ini复制代码输入: head = [1], n = 1
输出: []

示例 3:

1
2
ini复制代码输入: head = [1,2], n = 1
输出: [1]

解题:首先要遍历链表,获取链表的长度,然后再根据链表长度和n找到要删除的节点上一个节点位置,就是要定位倒数n+1个节点位置,对于倒数n+1位置来说要删除的是倒数n位置的节点,那么只要把倒数n+1位置的下一个节点指向倒数n-1位置的节点,修改一下这个指针指向就可以完成删除倒数n位置的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码 public ListNode removeNthFromEnd(ListNode head, int n) {
// 获取链表长度
int length = 0;
ListNode lenNode = new ListNode(0, head);
while (lenNode.next != null) {
length++;
lenNode = lenNode.next;
}
// 这是要删第一个节点的节奏啊
if (length - n - 1 == -1) {
return head.next;
}
ListNode copyNode = head;
// 从头到尾一直找到要删除的节点 的上一个节点
for (int i = 0; i < length - n - 1; i++) {
copyNode = copyNode.next;
}
// 删除那个节点 改变next指向
copyNode.next = copyNode.next.next;
return head;
}

或者不获取链表长度,使用两个链表来遍历,需要删除倒数第n个位置,遍历两个链表时,使得这两个链表的当前位置相差为n,当一个链表遍历到最后时,另外一个链表的位置就是倒数第n个位置。先复制一个链表2,遍历原链表节点,直到遍历n位置,然后现在同时遍历原链表和链表2,直到原链表结束(没有下一个节点),此时链表2刚好走到倒数第n个节点,因为链表2头部加了个哑节点,所以链表2的当前位置就是倒数第n节点的前一个节点,再将链表2的当前下一个节点指向下下个节点,就删除了倒数第n个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码 public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode listNode = new ListNode(0, head);
ListNode copyNode = listNode;
// 先走一个 走到n位置为止
for (int i = 0; i < n; i++) {
head = head.next;
}
// 然后再一起走,直到先走的走到头为止
// 这个时候后走的就走了链表长度-n的位置,因为后走的链表多个节点,所以刚好是要删除节点的前一个节点
while (head != null) {
head = head.next;
copyNode = copyNode.next;
}
// 删除节点
copyNode.next = copyNode.next.next;

return listNode.next;
}

本文转载自: 掘金

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

Mysql 温故知新系列「group by having

发表于 2021-11-19

「这是我参与11月更文挑战的第 19 天,活动详情查看:2021最后一次更文挑战」

group by

mysql 中对数据进行分组的关键字,需要搭配上 count, avg, max, min 等聚合函数进行查询,sql 的模版语句为

1
sql复制代码SELECT groupName, count(groupName) FROM table GROUP BY groupName

以 count 这个统计的聚合函数为例,上面的 sql 翻译为:在 table 中按照 groupName 进行分组,返回分组的字段 groupName 以及对这个字段对应的行记录进行数据汇总

如下是一个初学的错误示范:

image.png

翻译过来就是,我们对非聚合的字段使用了 group by 操作

group by 的常规操作就是对一个字段进行分组,然后统计这个字段中某个值相同的有多少条记录,如文末的示例

或者是,我们统计各个部门中薪水最高/最低/平均 多少,伪 sql 如下:

1
sql复制代码select dept, max|min|avg(dept) from salary group by dept

having

mysql 中提供了条件查询的关键字 where ,会帮助我们过滤掉不符合条件的记录

但是,对于聚合分组后的查询结果,where 无能为力,这里需要使用 having 来实现条件过滤的功能


这里聊聊 where 和 having 的区别:

  • 在我们获取查询的结果之前,where 会帮助我们过滤掉不符合条件的记录
  • having 是在我们获取到查询的结果之后,对现有的结果进行过滤,需要与 group by 搭配使用

案例分享

在某工厂,上线了我们的系统,并运行了一段时间之后,发现数据里出现了重复编码的异常,工厂的负责人需要帮忙从数据库里找出哪些是重复编码

对这个场景做分析,首先,我们对这个编码字段进行分组,并统计他出现的次数,然后再次过滤次数超过 1 的数据,这种操作拉到的编码就是客户期望的数据

sql 大致如下

1
2
3
4
5
6
7
8
9
10
11
sql复制代码SELECT
CODE,
count(CODE) count
FROM
rfid_info
WHERE
factory = '01'
GROUP BY
CODE
HAVING
count > 1;

我们先获取他对应的工厂的数据记录,然后根据编码进行分组,最后在分组的结果上使用 having 过滤得到重复编码

原创文章,未经允许,禁止转载

– by 安逸的咸鱼

本文转载自: 掘金

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

从源码上看k8s创建pod全流程(上) 简介 kubectl

发表于 2021-11-19

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

pexels-helena-lopes-2253275.jpg

简介

在K8s中最重要的基础资源类型是pod,其他高级的使用模式都是基于pod来运作的,如果想对K8s有更深入清晰的认识,我们有必要来看看pod到底是怎么被创建出来的。创建pod最简单的方式是通过kubectl run命令,根据官方给出的说明,”Create and run a particular image in a pod”,意思是在一个pod中创建并运行一个指定的镜像。

Create and run a particular image in a pod.

Examples:

Start a nginx pod.

kubectl run nginx –image=nginx

接下来我们将从开始敲回车之后,到我们可以查看到pod创建并启动成功,这背后到底发生了什么。从宏观层面看,整个故事发生在三个场地,一个是本地,主角当然是kubectl,一个则是远端的K8s集群主节点,主角是kube-apiserver、etcd以及调度器,另一个是K8s集群的工作节点,主角是kubelet。

首先我们先看一下整体大的流程,在每个环节都做了些什么,然后我们再逐个查看各个环节的代码实现。由于K8s核心代码已经多到两百多万行,而且还使用了很多高级的设计模式,本文将尽量对这份优秀的代码进行解读,一家之言可能有很多谬误,不妥之处,请不吝指出,我们可以一起来讨论学习。

以下代码均是基于K8s正式发布版V1.20.2进行分析。

整体流程图

从整体具体的上看,kubectl负责接收用户的输入,做初步的处理后,按照kube-apiserver的处理要求生成具体的请求;kube-apiserver是所有请求的入口,它负责所有请求的通用检查和分发以及对外提供资源状态的查询等等;kubelet负责具体pod的生命周期管理和节点整体状态数据的上报。

Kubernetes是基于事件+控制器模式实现的,因此在代码中并没有一个贯穿pod创建控制流程始终的代码存在。涉及到的组件更像是团队接力赛选手,大家在完成自己的工作后,就把棒交出去了(更新信息/触发某种事件),下一个选手根据自己感兴趣的事件去选择要做的事情(例如watch监听),并努力把自己分内的事做好,然后再交出去(更新信息/触发某种事件)。

kubectl run命令的历史简介

在早期的版本中,run子命令不仅可以创建pod,还可以创建job(含cronjob)、deployment以及replication controller

11453E16-163C-40BB-A43A-BA24AB67F197

在v1.20.2版本中kubectl run命令已经大大被简化,generator选项被移除,只能创建最基础的pod。在一些细节上也做了改进,例如dry-run选项增加了区分client还是server,控制更加细腻。

kubectl

客户端参数检查验证和对象生成

代码入口:

vendor/k8s.io/kubectl/pkg/cmd/run/run.go#L246

参数检查与验证

主要包含镜像名称校验,镜像拉取策略校验等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// vendor/k8s.io/kubectl/pkg/cmd/run/run.go#L276 
imageName := o.Image
if imageName == "" {
return fmt.Errorf("--image is required")
}
validImageRef := reference.ReferenceRegexp.MatchString(imageName)
if !validImageRef {
return fmt.Errorf("Invalid image name %q: %v", imageName, reference.ErrReferenceInvalidFormat)
}

// vendor/k8s.io/kubectl/pkg/cmd/run/run.go#L310
if err := verifyImagePullPolicy(cmd); err != nil {
return err
}

对象生成

获取pod默认生成器

1
2
3
4
5
6
7
8
9
10
go复制代码// vendor/k8s.io/kubectl/pkg/cmd/run/run.go#L314 
generators := generateversioned.GeneratorFn("run")
// 加载run下说有的生成器,目前只剩下一个pod的生成器,历史版本上还有job、deployment等等,参考kubectl run命令历史小节
generator, found := generators[generateversioned.RunPodV1GeneratorName] // "run-pod/v1"
if !found {
return cmdutil.UsageErrorf(cmd, "generator %q not found", o.Generator)
}
// vendor/k8s.io/kubectl/pkg/generate/versioned/generator.go#L94
case "run": // run子命令下注册的默认生成器
generator = map[string]generate.Generator{ RunPodV1GeneratorName: BasicPod{},

生成运行时对象

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
go复制代码// vendor/k8s.io/kubectl/pkg/cmd/run/run.go#L330 
var createdObjects = []*RunObject{}
runObject, err := o.createGeneratedObject(f, cmd, generator, names, params, cmdutil.GetFlagString(cmd, "overrides")) // 这里开始发起创建运行时对象
if err != nil {
return err
}
createdObjects = append(createdObjects, runObject)
// vendor/k8s.io/kubectl/pkg/cmd/run/run.go#L616
func (o *RunOptions) createGeneratedObject(f cmdutil.Factory, cmd *cobra.Command, generator generate.Generator, names []generate.GeneratorParam, params map[string]interface{}, overrides string) (*RunObject, error) {
// 验证生成器参数
err := generate.ValidateParams(names, params)
// 生成器生成对象 obj, err := generator.Generate(params)
// API分组和版本协调
mapper, err := f.ToRESTMapper()
// run has compiled knowledge of the thing is creating
gvks, _, err := scheme.Scheme.ObjectKinds(obj)
mapping, err := mapper.RESTMapping(gvks[0].GroupKind(), gvks[0].Version)
if o.DryRunStrategy != cmdutil.DryRunClient {
// 客户端实例构建
client, err := f.ClientForMapping(mapping)
// 具体实例取决于f是怎么实例化的 // 发送HTTP请求
actualObj, err = resource. NewHelper(client, mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). // 动态配置server side dry run
WithFieldManager(o.fieldManager). // 更新管理者
Create(o.Namespace, false, obj)
}
}

关于API groups和version发现与协商

Kubernetes使用的API是带版本号并且被分成了API groups。一个API group是指一组操作资源类似的API集合。Kubernetes一般支持多版本的API groups,kubectl为了找到最合适的API,需要只通过发现机制来获取kube-api暴露的schema文档(通过OpenAPI格式)。为了提高性能,一般kubectl会在本地~/.kube/cache/discovery目录缓存这些schema文件。

处理返回结果

得到api-server返回值后,进行后续处理。按照正确的格式输出创建的对象。

1
2
3
4
5
6
go复制代码// vendor/k8s.io/kubectl/pkg/cmd/run/run.go#L430 
if runObject != nil {
if err := o.PrintObj(runObject.Object); err != nil {
return err
}
}

客户端认证支持

为了确保请求发送成功,kubectl需要具备认证的能力。用户凭证几乎总是存储在本地磁盘的kubeconfig文件中。为了定位该文件,kubectl会按照以下步骤加载该文件

  1. 如果–kubeconfig指定了文件,则使用这个文件
  2. 如果$KUBECONFIG环境变量定义了,则使用该环境变量指向的文件
  3. 在本地home目录下,例如~/.kube,搜索并使用第一个找到的文件

在文件解析完成后,kubectl就可以确定当前使用的上下文,指向的集群以及当前用户关联的认证信息。

kube-apiserver

认证

kube-apiserver是客户端与系统组件之间主要的持久化和查询集群状态界面。首要的kube-apiserver需要知道请求的发起者是谁。

apiserver如何对请求做认证?当服务第一次启动时,它会查看用户提供的所有命令行参数,然后组装成一个合适的认证器列表。每个请求到来后都要逐个通过认证器的检查,直到有一个认证通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// vendor/k8s.io/apiserver/pkg/authentication/request/union/union.go#L53 
// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects.
func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
var errlist []error
for _, currAuthRequestHandler := range authHandler.Handlers {
resp, ok, err := currAuthRequestHandler.AuthenticateRequest(req)
if err != nil {
if authHandler.FailOnError {
return resp, ok, err
}
errlist = append(errlist, err) continue
}
if ok {
return resp, ok, err
}
}
return nil, false, utilerrors.NewAggregate(errlist)
}

认证器的初始化流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码// pkg/kubeapiserver/authenticator/config.go#L95
// New returns an authenticator.Request or an error that supports the standard
// Kubernetes authentication mechanisms.
func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
var authenticators []authenticator.Request
authenticator := union.New(authenticators...)
// 各种初始化
authenticator = group.NewAuthenticatedGroupAdder(authenticator)
if config.Anonymous {
// If the authenticator chain returns an error, return an error (don't consider a bad bearer token
// or invalid username/password combination anonymous).
authenticator = union.NewFailOnError(authenticator, anonymous.NewAuthenticator())
}
return authenticator, &securityDefinitions, nil
}

如下图所示,假设所有的认证器都被启用,当客户端发送请求到kube-apiserver服务,该请求会进入Authentication Handler函数(处理认证相关的Handler函数),在Authentication Handler函数中,会遍历已启用的认证器列表,尝试执行每个认证器,当有一个认证器返回true时,则认证成功,否则继续尝试下一个认证器。

当所有认证器都认证失败后,请求将会被拒绝,合并后的错误会返回给客户端。如果认证成功,Authorization头信息将会从请求中移除,用户信息会添加到请求的上下文信息中。这样后续步骤就可以访问到认证阶段确定的请求用户的信息了。

鉴权

虽然现在kube-apiserver已经成功地验证了请求者的身份信息,但是在进行下一步之前还得确保请求者是否有权限去操作。身份认证和鉴权不是同一个事情,要想进一步使用,kube-apiserver需要对我们进行鉴权。

类似认证器的处理方法,kube-apiserver需要基于用户提供的命令行参数,来组装一个合适的鉴权器列表来处理每一个请求。当所有的鉴权器都拒绝该请求时,请求会终止,并且请求方会得到Forbidden的答复。如果任何一个鉴权器批准了请求,那么请求鉴权成功,将会进入下一阶段处理。

鉴权器初始化

kube-apiserver目前提供了6种授权机制,分别是AlwaysAllow、AlwaysDeny、ABAC、Webhook、RBAC、Node,可通过指定–authorization-mode参数设置授权机制,至少需要指定一个。

1
2
3
4
5
6
7
go复制代码// pkg/kubeapiserver/authorizer/config.go#L71
// New returns the right sort of union of multiple authorizer.Authorizer objects // based on the authorizationMode or an error.
func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, error) {
if len(config.AuthorizationModes) == 0 {
return nil, nil, fmt.Errorf("at least one authorization mode must be passed")
}
}

鉴权器决策状态

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码// vendor/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go#L149
type Decision int const
(
// DecisionDeny means that an authorizer decided to deny the action.
DecisionDeny
Decision = iota
// DecisionAllow means that an authorizer decided to allow the action.
DecisionAllow
// DecisionNoOpionion means that an authorizer has no opinion on whether
// to allow or deny an action.
DecisionNoOpinion
)

当决策状态是DecisionDeny或DecisionNoOpinion时会交由下一个鉴权器继续处理,如果没有下一个鉴权器则鉴权失败。当决策状态是DecisionAllow时鉴权成功,请求被接受。

Admission control

在认证和授权之后,对象被持久化之前,拦截kube-apiserver的请求,对请求的资源对象进行自定义操作(校验、修改或者拒绝请求)。为什么需要有这一个环节?为了集群的稳定性,在资源对象被正式接纳前,需要由系统内其他组件对待创建的资源先进行一系列的检查,确保符合整个集群的预期和规则,从而防患于未然。这是在etcd创建资源前的最后一道保障。

插件实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码// vendor/k8s.io/apiserver/pkg/admission/interfaces.go#L123
// Interface is an abstract, pluggable interface for Admission Control decisions.
type Interface interface {
// Handles returns true if this admission controller can handle the given operation
// where operation can be one of CREATE, UPDATE, DELETE, or CONNECT
Handles(operation Operation) bool
}

type MutationInterface interface {
Interface

// Admit makes an admission decision based on the request attributes.
// Context is used only for timeout/deadline/cancellation and tracing information.
Admit(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}

// ValidationInterface is an abstract, pluggable interface for Admission Control decisions.
type ValidationInterface interface {
Interface

// Validate makes an admission decision based on the request attributes.  It is NOT allowed to mutate
// Context is used only for timeout/deadline/cancellation and tracing information.
Validate(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}

两种准入控制器

  1. 变更准入控制器(Mutating Admission Controller)用于变更信息,能够修改用户提交的资源对象信息。
  2. 验证准入控制器(Validating Admission Controller)用于身份验证,能够验证用户提交的资源对象信息。

变更准入控制器运行在验证准入控制器之前。

准入控制器的运行方式与认证和鉴权方式类似,不同的地方是任何一个准入控制器失败后,整个准入控制流程就会结束,请求失败。

准入控制器以插件的形式运行在kube-apiserver进程中,插件化的好处在于可扩展插件并单独启用/禁用指定插件,也可以将每个准入控制器称为准入控制器插件。

客户端发起一个请求,在请求经过准入控制器列表时,只要有一个准入控制器拒绝了该请求,则整个请求被拒绝(HTTP 403Forbidden)并返回一个错误给客户端。

ETCD存储

经过身份认证、鉴权以及准入控制检查后,kube-apiserver将反序列化HTTP请求(解码),构造运行时对象(runtime object),并将它持久化到etcd。

横向扩展

kube-apiserver如何知道某一个资源的操作该如何处理呢?这在服务刚启动的时候会有非常复杂的配置步骤,让我们粗略看一下:

  1. 当kube-apiserver启动时,会创建一个服务链(server chain),允许apiserver进行聚合,这是提供多个apiserver的基础方式
1
2
go复制代码// cmd/kube-apiserver/app/server.go#L184
server, err := CreateServerChain(completeOptions, stopCh)
  1. 作为默认实现的通用apiserver会被创建
1
2
go复制代码// cmd/kube-apiserver/app/server.go#L215
apiExtensionsServer, err := createAPIExtensionsServer(apiExtensionsConfig, genericapiserver.NewEmptyDelegate())
  1. 生成的OpenAPI信息(schema)会填充到apiserver的配置中
1
2
go复制代码// cmd/kube-apiserver/app/server.go#L477
genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(generatedopenapi.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(legacyscheme.Scheme, extensionsapiserver.Scheme, aggregatorscheme.Scheme))
  1. kube-apiserver为每个API组配置一个存储服务提供器,它就是kube-apiserver访问和修改资源状态时的代理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// pkg/controlplane/instance.go#L591
for _, restStorageBuilder := range restStorageProviders {
groupName := restStorageBuilder.GroupName()
if !apiResourceConfigSource.AnyVersionForGroupEnabled(groupName) {
klog.V(1).Infof("Skipping disabled API group %q.", groupName)
continue
}
apiGroupInfo, enabled, err := restStorageBuilder.NewRESTStorage(apiResourceConfigSource, restOptionsGetter)
if err != nil {
return fmt.Errorf("problem initializing API group %q : %v", groupName, err)
}
if !enabled {
klog.Warningf("API group %q is not enabled, skipping.", groupName)
continue
        }
}
  1. 为每一个不同版本的API组添加REST路由映射信息。这会运行kube-apiserver将请求映射到所匹配到的正确代理。
1
2
go复制代码// vendor/k8s.io/apiserver/pkg/server/genericapiserver.go#L439
r, err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer)
  1. 在我们这个特定场景下,POST处理器会被注册,它将代理资源的创建操作
1
2
3
4
5
6
7
8
go复制代码// vendor/k8s.io/apiserver/pkg/endpoints/installer.go#816
case "POST": // Create a resource.
var handler restful.RouteFunction
if isNamedCreater {
handler = restfulCreateNamedResource(namedCreater, reqScope, admit)
} else {
handler = restfulCreateResource(creater, reqScope, admit)
      }

小结一下,至此kube-apiserver完成了路由到内部资源操作代理的映射配置,当请求匹配后,就可以触发指定的操作代理了。

pod存储流程

我们继续看pod创建的流程:

  1. 基于注册的路由信息,当请求匹配到处理器链条中的某一个时,就会交由该处理器去处理。如果没有匹配的处理器,就返回给基于路径的处理器进行处理。但是如果没有注册路径处理器,则由notfound处理器返回404错误信息。
  2. 幸运的是我们已经注册过createHandler了。它会做些什么呢?首先,它会解码HTTP请求体,并进行基础的验证,如提供的json是否符合相关版本API资源的要求
  3. 进行审计和最终的准入检查
  4. 通过存储代理将资源存储到etcd中。通常etcd的key是如下格式:/,它可以通过配置继续修改
1
go复制代码Create(ctx context.Context, name string, obj runtime.Object, createValidation ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error)
  1. 检查是否有任何错误,没有错误时存储代理会通过get调用来确保资源对象确实被创建了。然后它会触发post-create handler和其他额外要求的装饰器。
  2. 构造HTTP请求返回内容并发送
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
go复制代码// vendor/k8s.io/apiserver/pkg/endpoints/handlers/create.go#L49
func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Interface, includeName bool) http.HandlerFunc {
gv := scope.Kind.GroupVersion()
s, err := negotiation.NegotiateInputSerializer(req, false, scope.Serializer)
if err != nil {
scope.err(err, w, req)
return
}

// 这个decoder构建比较关键,后边对body的解析就是通过它来完成的
decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion)


// 读取包体:序列化后的runtime
body, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
if err != nil {
scope.err(err, w, req)
return
        }
// 检查创建参数

// 开始解码转换
defaultGVK := scope.Kind
original := r.New()
trace.Step("About to convert to expected version")
obj, gvk, err := decoder.Decode(body, &defaultGVK, original)
// 省略
trace.Step("Conversion done")

// admission

// 开始存储etcd
trace.Step("About to store object in database")
admissionAttributes := admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, options, dryrun.IsDryRun(options.DryRun), userInfo)
requestFunc := func() (runtime.Object, error) {
return r.Create(
ctx,
name,
obj,
rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope),
options,
)
}
// 省略
trace.Step("Object stored in database")

// 构造HTTP返回结果
code := http.StatusCreated
status, ok := result.(*metav1.Status)
if ok && err == nil && status.Code == 0 {
status.Code = int32(code)
}

transformResponseObject(ctx, scope, trace, req, w, code, outputMediaType, result)
  // 至此,整个创建pod的HTTP请求就会返回,同时会返回创建后的对象

总结

到现在为止,kube-apiserver完成了大量的工作,pod资源已经存储到etcd中了,但是它对外还不可见,还需要最后一步。篇幅原因,后续文章我们再继续讲解pod的调度和实际创建。

参考文档

书籍

《Kubernetes源码剖析》郑东旭

网文

Kubernetes 弃用 Docker 来龙去脉

kubelet创建pod工作流程

v1.14版的kubectl run创建pod流程

本文转载自: 掘金

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

【每日算法打卡】100 相同的树

发表于 2021-11-19

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」。

【每日打卡系列】LeetCode 简单题 200 道

题目描述

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

输入: p = [1,2,3], q = [1,2,3]

输出: true

示例 2:

输入: p = [1,2], q = [1,null,2]

输出: false

示例 3:

输入: p = [1,2,1], q = [1,1,2]

输出: false

提示

  • 两棵树上的节点数目都在范围 [0, 100] 内
  • -104 <= Node.val <= 104

解题思路

本题适合用深度优先搜索,广度优先搜索较为复杂。
首先如果两个二叉树都为空,则两个二叉树相同,返回 True。如果两个二叉树中有且只有一个为空或是两个二叉树的 val 不相同,则两个二叉树一定不相同,不符合题意,返回 False ,若以上条件均不满足,则分别判断两个二叉树的左子树和右子树是否相同。这是一个典型的递归过程,因此可以使用深度优先搜索,递归地判断两个二叉树是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def isSameTree(self, p, q):
"""
:type p: TreeNode
:type q: TreeNode
:rtype: bool
"""
# 如果p与q都为空,返回True
if not p and not q:
return True
# p与q中有且只有一个为空,或是p与q的 `val` 不相同,返回False
elif (not p or not q) or (p.val != q.val):
return False
# 判断p与q的左子树和右子树是否相同
else:
return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)
  • 时间复杂度:O(min(m,n))
  • 空间复杂度:O(min(m,n))

今日打卡完成,目前进度 9/200。

这就是今天要分享的内容,微信搜 Python新视野,每天带你了解更多有用的知识。更有整理的近千套简历模板,几百册电子书等你来领取哦!另外还有Python小白交流群,如果有兴趣可以通过上面的方式联系我哦!

本文转载自: 掘金

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

MySQL中的锁 概述 分类 死锁 InnoDB中的锁 资料

发表于 2021-11-19

概述

为了支持对共享资源进行并发访问,保证提供数据的完整性和一致性

保证了事务的隔离性,通过表锁,行锁等方式可以实现事务的各个隔离等级。

分类

MySQL中锁大致可以按照数据库的层级分为DB级别锁、表级别锁以及行级别锁,而不同的数据库引擎支持的锁类型也不同

全局锁

在DB级别对整个数据库实例加锁,加锁之后:

  • 数据库处于只读状态
  • 阻塞对数据的增删改以及DDL

加锁方式:lock Flush tables with read lock
释放锁:unlock tables(发生异常时会自动释放)

全局锁主要用于做全库的逻辑备份,和设置数据库只读(set global readonly=true)相比,全局锁在发生异常时会自动释放

MyISAM、InnoDB都支持全局锁,但InnoDB一般不使用
基于InnoDB对事物的支持以及MVCC多版本并发的实现,InnoDB可以选择mysqldump工具加 –single-transaction参数,在不阻塞写操作的同时做全库的逻辑备份

表级别锁

表级别对操作的整张表加锁,锁定颗粒度大,资源消耗少,不会出现死锁,但并发度低,表级锁有两种模式:

  • 表共享锁:对同一表的操作不阻塞读,阻塞写
  • 表独占锁:对同一表的操作读写阻塞

在数据库中,读锁和写锁是互斥的,读写操作是串行。

  • 如果某个进程想要获取读锁,同时另外一个进程想要获取写锁。在mysql里边,写锁是优先于读锁的
  • 写锁和读锁优先级的问题是可以通过参数调节的:max_write_lock_count和low-priority-updates

MyISAM引擎默认支持表级别锁

  • 表级别的锁有两种:表锁
  • 元数据锁(MDL)

表锁

  • 显示加锁方式:lock tables {tb_name} read/write
  • 释放锁:unlock table {tb_name} (连接中断也会自动释放)

MyISAM引擎下隐式加锁:

  • 执行SELECT查询自动加共享锁(读锁)
  • 执行INSERT、UPDATA、DELETE操作自动加独占锁(写锁)

MyISAM读写锁优先级:
默认情况下写锁比读锁具有更高的优先级,即使读请求先到等待队列,写锁也会插入到读锁之前,优先执行写操作,但MyISAM也支持依据生产环境通过修改参数的设置改变读写的优先级

元数据锁(MDL)

隐式锁,主要针对对表结构改变的操作(DDL),没有显示加锁方式,访问表时自动加锁:

  • 执行DML(SELECT, INSERT…) 操作加共享锁(读锁)
  • 执行DDL(ALTER, DROP…) 操作加独占锁(写锁)

当向表里增加一个字段隐式加MDL写锁,那么线上所有对这个表的增删改查(DML)操作都会阻塞

MySQL在5.6之后引入online DDL,也就是进行DDL操作时MDL写锁会降级成读锁,线上DML操作不会被阻塞,DDL操作完成之后升级回MDL写锁然后释放

查看表级锁争用情况:SHOW STATUS LIKE ‘table%’

总之表级锁因为锁的粒度大,若一个事物执行时间过长,很可能会导致后面对这个表的请求全部阻塞

行级别锁

InnoDB支持行级别锁,锁粒度小并发度高,但是加锁开销大也很可能会出现死锁,锁模式:

  • 共享锁(读锁) S:对同一行的操作读不阻塞,阻塞写
  • 排它锁(写锁) X:对同一行的操作读写都会阻塞
  • 意向共享锁 IS:一个事物想要加S锁时必须先获得该表的IS
  • 意向排它锁 IX:一个事物想要加X锁时必须先获得该表的IX

为什么需要意向锁:
意向锁是表级别的锁,作为一个标识量,用来标识该表上有数据被锁住或即将被锁,对于表级别的请求(LOCK TABLE…),就可以直接判断是否有锁冲突,不需要逐行检查锁的状态

行级锁算法

记录锁

只对索引上锁,锁定某一行

间隙锁

锁定某一范围内的闭区间

Next-Key Locks

结合记录锁和间隙锁,实现innodb默认加锁单位(左开右闭区间)。

死锁

InnoDB中的锁

全局锁

加锁方式:lock Flush tables with read lock

释放锁:unlock tables(发生异常时会自动释放)

表锁

显示加锁方式:lock tables {tb_name} read/write

释放锁:unlock table {tb_name} (连接中断也会自动释放)

行锁

InnoDB的默认隔离级别RR(可重复读),在RR下读数据有两种方式:

  • 快照读:在MVCC下,事物开启执行第一个SELECT语句后会获取一个数据快照,直到事物结束读取到的数据都是一致的,普通的 select… 查询都是快照读
  • 当前读:读取的数据的最新版本,并且在读的时候不允许其它事物修改当前记录
  • select… lock in share mode(读锁)
  • select… for update(写锁)
    加锁方式:
  • 普通 select… 查询 (不加锁)
  • 普通 insert、update、delete… (隐式加写锁)
  • select…lock in share mode (加读锁)
  • select…for update (加写锁)

行级锁默认加 next-key lock,查询过程中访问到的索引项都会加锁,而根据不同的索引也有不同的加锁规则:

  • 唯一索引等值查询:当索引项存在时,next-key lock 退化为 record lock;当索引项不存在时,默认 next-key lock,访问到不满足条件的第一个值后next-key lock退化成gap lock
  • 唯一索引范围查询:默认 next-key lock,(特殊’<=’ 范围查询直到访问不满足条件的第一个值为止)
  • 非唯一索引等值查询:默认next-key lock ,索引项存在/不存在都是访问到不满足条件的第一个值后next-key lock退化成gap lock
  • 非唯一索引范围查询:默认 next-key lock,向右访问到不满足条件的第一个值为止

锁的数据结构

image.png

资料来源

(32条消息) MySQL锁机制,行锁竟然加在索引上!!_guozizi1718的博客-CSDN博客

InnoDB技术内幕

本文转载自: 掘金

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

Spring全家通之SpringMVC核心及源码分析(一)

发表于 2021-11-19

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。

👨‍🎓作者:Java学术趴

🏦仓库:Github、Gitee

✏️博客:CSDN、掘金、InfoQ、云+社区

💌公众号:Java学术趴

🚫特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系小编授权。

🙏版权声明:文章里的部分文字或者图片来自于互联网以及百度百科,如有侵权请尽快联系小编。微信搜索公众号Java学术趴联系小编。

☠️每日毒鸡汤:微笑拥抱每一天,做像向日葵般温暖的女子。

👋大家好!我是你们的老朋友Java学术趴。最近小编又在整了Spring全家桶笔记,笔记会每天定时的进行发放,喜欢的大佬们欢迎收藏点赞关注呦。小编会每天分享的呦。今天给大家带来新的框架技术SpringMVC。

Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的Spring MVC框架或集成其他MVC开发框架。

第一章 SpringMVC概述

1.1 SpringMVC概念

  • SpringMVC也叫Spring Web MVC。是Spring框架的一部分,是在Spring3.0之后发布的。其实就是Spring框架的一个模块,所以可以也是使用Spring的IoC技术实现对象的创建和赋值,使用AOP的切面技术为方法添加功能。
  • SpringMVC基于spring的一个框架, 实际上就是spring的一个模块, 专门是做web开发的。其实就是一个Servlet的升级版。
  • web开发底层是servlet , SpringMVC框架是在servlet基础上面加入一些功能,让你做web开发方便。
  • SpringMVC就是一个Spring。 Spring是容器,IoC能够管理对象,使用, @Component, @Repository, @Service, @Controller等标签实现对象的创建。SpringMVC能够创建对象, 放入到容器中(SpringMVC容器), springmvc容器中放的是控制器对象。

注意:使用@Collection注解创建的对象其实就是一个普通的类,并不是一个Servlet类,因为真正好的Servlet类需要继承HttpServlet类。SpringMVC只是赋予了这个普通类的一些额外的功能。

  • web开发底层使用的一定是Servlet对象, springmvc中有一个对象是Servlet : DispatherServlet(中央调度器)

请求原理:

DispatherServlet: 负责接收用户的所有请求, 用户把请求给了DispatherServlet, 之后DispatherServlet把请求转发给我们的Controller对象, 最后是Controller对象处理请求。中央调度器就相当于一个中转站,用于接收和发放用户的请求。

实现步骤:

index.jsp –> DispatherServlet(Spring提供的一个Servlet对象) –> 转发、分配给Collection对象(即使用@Controller注解创建的对象)

image-20211118102329984

1.2 SpringMVC优点

  • 基于MVC结构: 基于 MVC 架构,功能分工明确。解耦合。
  • 容易理解,上手快;使用简单: 就可以开发一个注解的 SpringMVC 项目,SpringMVC 也是轻量级的,jar 很小。不依赖的 特定的接口和类。
  • 作为Spring框架一部分,能够使用 Spring 的 IoC 和 Aop 方便整合。Strtus,MyBatis,Hiberate,JPA 等其他框架。
  • SpringMVC 强化注解的使用,在控制器,Service,Dao 都可以使用注解。方便灵活: 使用@Controller 创建处理器对象,@Service 创建业务对象,@Autowired 或者@Resource 在控制器类中注入Service, Service类中注入Dao。

1.3 第一个注解SpringMVC程序

  • 所谓 SpringMVC 的注解式开发是指:在代码中通过对类与方法的注解,便可完成处理器 在 springmvc容器的注册。注解式开发是重点。

见项目Hello-SpringMVC

springmvc请求的处理流程

1)发起some.do 2)tomcat(web.xml–url-pattern知道 *.do的请求给DispatcherServlet) 3)DispatcherServlet(根据springmvc.xml配置知道 some.do—doSome()) 4)DispatcherServlet把some.do转发个MyController.doSome()方法 5)框架执行doSome()把得到ModelAndView进行处理, 转发到show.jsp

上面的过程简化的方式 some.do—DispatcherServlet—MyController

核心原理

image-20211118163958761

SpringMVC执行过程的源码分析

  1. tomcat启动,创建容器的过程通过load-on-start标签指定的1,创建DisaptcherServlet对象, DisaptcherServlet它的父类是继承HttpServlet的, 它是一个serlvet, 在被创建时,会执行init()方法。在init()方法中。
1
2
3
4
scss复制代码 //创建容器,读取配置文件
WebApplicationContext ctx = new ClassPathXmlApplicationContext("springmvc.xml");
//把容器对象放入到ServletContext中
getServletContext().setAttribute(key, ctx);

上面创建容器作用: 创建@controller注解所在的类的对象, 创建MyController对象,这个对象放入到 springmvc的容器中, 容器是map ,类似map.put(“myController”,MyController对象)

  1. 执行servlet的service()
1
2
arduino复制代码protected void doService(HttpServletRequest request, HttpServletResponse response);
protected void service(HttpServletRequest request, HttpServletResponse response);
1
2
3
4
javascript复制代码  DispatcherServlet.doDispatch(request, response){
​
     调用MyController的.doSome()方法
}

第二章 SpringMVC注解式开发(重点)

  • 所谓 SpringMVC 的注解式开发是指,在代码中通过对类与方法的注解,便可完成处理器 在 springmvc 容器的注册。注解式开发是重点。

我们使用一个项目来举例子:primary-annotation

这个项目所要完成的功能:用户提交一个请求,服务端处理器在接收到这个请求后,给出一条欢迎信息, 在响应页面中显示该信息。

第一步:新建一个Maven项目

image-20211119123118975

第二步:pom.xml

  • 在创建好的web项目后,加入Servlet依赖,SpringMVC依赖
1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!--spring-webmvc中包含spring-web依赖-->

第三步:注册中央调度器

image-20211119123449104

(1) 全限定性类名

  • 该中央调度器为一个 Servlet,名称为 DispatcherServlet。中央调度器的全限定性类名在 导入的 Jar 文件 spring-webmvc-5.2.5.RELEASE.jar 的第一个包 org.springframework.web.servlet 下可找到。

(2) < load-on-startup/>

  • 在中添加的作用是,标记是否在Web服务器(这里是Tomcat) 启动时会创建这个 Servlet 实例,即是否在 Web 服务器启动时调用执行该 Servlet 的init()方 法,而不是在真正访问时才创建。它的值必须是一个整数。

Servlet对象被加载创建的优先级

➢ 当值大于等于 0 时,表示容器在启动时就加载并初始化这个 servlet,数值越小,该 Servlet 的优先级就越高,其被创建的也就越早;

➢ 当值小于 0 或者没有指定时,则表示该 Servlet 在真正被使用时才会去创建。

➢ 当值相同时,容器会自己选择创建顺序。

(3) < url-pattern/>

  • *对于,可以写为 / ,建议写为.do 的形式。\

(4)配置文件位置与名称

  • 注册完毕后,可直接在服务器上发布运行。此时,访问浏览器页面,控制台均会抛出 FileNotFoundException 异常。即默认要从项目根下的 WEB-INF 目录下找名称为 Servlet 名称 -servlet.xml 的配置文件。这里的“Servlet 名称”指的是注册中央调度器标签 中指定的 Servlet 的 name 值。本例配置文件名为 springmvc-servlet.xml。

  • 而一般情况下,配置文件是放在类路径下,即 resources 目录下。所以,在注册中央调 度器时,还需要为中央调度器设置查找 SpringMVC 配置文件路径,及文件名。

image-20211119154250361

  • 打开 DispatcherServlet 的源码,其继承自 FrameworkServlet,而该类中有一个属性 contextConfigLocation,用于设置 SpringMVC 配置文件的路径及文件名。该初始化参数的属性 就来自于这里。

image-20211119154321095

1.3.4 创建 SpringMVC 配置文件

  • 在工程的类路径即 src 目录下创建 SpringMVC 的配置文件 springmvc.xml。该文件名可以 任意命名。

1.3.5 创建处理器

在类上与方法上添加相应注解即可。

  • @Controller:表示当前类为处理器
  • @RequestMapping:表示当前方法为处理器方法。该方法要对 value 属性所指定的 URI 进行处理与响应。被注解的方法的方法名可以随意。

image-20211119154753449

  • 若有多个请求路径均可匹配该处理器方法的执行,则@RequestMapping 的 value 属性中 可以写上一个数组。
  • ModelAndView 类中的 addObject()方法用于向其 Model 中添加数据。Model 的底层为一 个 HashMap

Model 中的数据存储在 request 作用域中,SringMVC 默认采用转发的方式跳转到视图, 本次请求结束,模型中的数据被销毁。

1.3.6 声明组件扫描器

在 springmvc.xml 中注册组件扫描器

1.3.7 定义目标页面

在 webapp 目录下新建一个子目录 jsp,在其中新建一个 jsp 页面 show.jsp。

image-20211119154916725

1.3.8 修改视图解析器的注册

  • SpringMVC 框架为了避免对于请求资源路径与扩展名上的冗余,在视图解析器 InternalResouceViewResolver 中引入了请求的前辍与后辍。而 ModelAndView 中只需给出要跳 转页面的文件名即可,对于具体的文件路径与文件扩展名,视图解析器会自动完成拼接。

image-20211119154947554

把 show.jsp 文件放到 /WEB-INF/jsp/路径中

1.3.9 修改处理器

image-20211119155008074

使用逻辑视图名称,show 是逻辑视图名称。

1.3.10 使用 SpringMVC 框架 web 请求处理顺序

1.4 SpringMVC 的 MVC 组件

1.5 SpringMVC 执行流程(理解)

1.5.1 流程图

1.5.2 执行流程简单分析

(1)浏览器提交请求到中央调度器

(2)中央调度器直接将请求转给处理器映射器。

(3)处理器映射器会根据请求,找到处理该请求的处理器,并将其封装为处理器执行链后 返回给中央调度器。

(4)中央调度器根据处理器执行链中的处理器,找到能够执行该处理器的处理器适配器。

(5)处理器适配器调用执行处理器。

(6)处理器将处理结果及要跳转的视图封装到一个对象 ModelAndView 中,并将其返回给 处理器适配器。

(7)处理器适配器直接将结果返回给中央调度器。

(8)中央调度器调用视图解析器,将 ModelAndView 中的视图名称封装为视图对象。

(9)视图解析器将封装了的视图对象返回给中央调度器

(10)中央调度器调用视图对象,让其自己进行渲染,即进行数据填充,形成响应对象。

(11)中央调度器响应浏览器。

今天关于SpringMVC的笔记就先分享到这里啦,明天给大家分享SpringMVC如何接收和处理参数值。

以上项目的源代码,点击星球进行免费获取 星球 (Github地址)如果没有Github的小伙伴儿。可以关注本人微信公众号:Java学术趴,发送SpringMVC,免费给发给大家项目源码,代码是经过小编亲自测试的,绝对可靠。免费拿去使用。

本文转载自: 掘金

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

Hi,你想要的在线创建架构图都在这儿!(二)

发表于 2021-11-19

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

🤞 个人主页:@青Cheng序员石头

🤞 粉丝福利:加粉丝群 一对一问题解答,获取免费的丰富简历模板、提高学习资料等,做好新时代的卷卷王!

上一篇文章Hi,你想要的在线创建架构图都在这儿!(一),点击穿越过去。

四、Terrastruct

对比于其它生成静态图像的图设计生成工具,Terrastruct能表达更加复杂的软件设计。其亮点是可以根据抽象级别对图进行分层,也可以定义场景来描述每个边界情况。

其官方网站对其定义为Programmable diagrams to visualize software systems,也就是具备通过编程方式去绘制图去表达可视化软件系统。

其有三大特点,包括:

多图层,其允许图层的堆叠,即支持多图层,这样就允许了图表由全局到各个细节全都能展示出来。

image.png

场景切换,支持动态的展示图片在多业务场景下的不同呈现,有利于向用户表达各种场景。

image.png

实时数据,对于静态的图表,其能通过实时生产数据来丰富您的组件图。并且能与AWS,GCP和GitHub等流行服务集成,以使您的图表动态反映系统的状态。

image.png

除了上面讲到的,其还支持多人协作,团队模式,图标加密等功能。当然有一些增强功能是要收费的,有兴趣的可以去体验一把,毕竟它是有别于静态图表,能让你的图表动起来,表达更加丰富。

五、Gliffy

Gliffy是一个梦幻般的绘图工具,它可以帮助您创建多种图表,如流程图,org图表,venn图,线框,mindmap,网络设计等。用户界面类似于draw.io。但是,我看到更多的颜色形状,这非常方便,看起来很有吸引力。它也能做到商业支持、软件设计、UI设计等。

image.png

这款工具支持免费试用,后续当然是要收费的。对于工具,我只想说,如果不嫌弃draw.io UI的同学就没必要去使用这款工具。

接下来的在线创建架构图的工具还会有:

  • Edraw Max
  • Cacoo
  • Lucidchart
  • Creately
  • Coggle
  • Mindmeister
  • yED

朋友,你用的是哪一种呢?评论留言你常用的工具,你认为最好的在线画图工具!


少年,没看够?点击石头的详情介绍,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

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

SpringBoot系列之Prometheus自定义埋点姿势

发表于 2021-11-19

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

SpringBoot系列之Prometheus自定义埋点姿势二

关于Prometheus的自定义埋点,前一篇博文已经介绍了,为啥这里又来一次?

看过前文的小伙伴可能会知道,之前采用的simpleclient包定义的几个metric来实现的,实际上有更简单方便的姿势,那就是直接借助MeterRegistry来创建Metric来实现数据采集即可

相比较于前文的实现,总的来说简易程度可见一般,上篇文章可以点击下文查看

  • 【中间件】Prometheus自定义埋点上报 | 一灰灰Blog

II. 自定义上报

依然是搭建一个基础项目工程,本文演示的项目主要为SpringBoot2.2.1版本,更高的版本使用姿势没有太大的区别,至于1.x版本的不确保可行(因为我并没有测试)

1.依赖

pom依赖,主要是下面几个包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>

2. 配置信息

其次是配置文件,注册下Prometheus的相关信息

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码spring:
application:
name: prometheus-example
management:
endpoints:
web:
exposure:
include: "*"
metrics:
tags:
application: ${spring.application.name}

上面配置中,有两个关键信息,前面博文也有介绍,这里简单说明

  • management.endpoints.web.exposure.include 这里指定所有的web接口都会上报
  • metrics.tags.application 这个应用所有上报的metrics 都会带上application这个标签

配置完毕之后,会提供一个 /actuator/prometheus的端点,供prometheus来拉取Metrics信息

3. 自定义拦截器实现采集上报

实现一个基础的拦截器,用来拦截所有的http请求,然后收集请求信息上报

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
java复制代码public class MetricInterceptor extends HandlerInterceptorAdapter {
@Autowired
private MeterRegistry meterRegistry;
private ThreadLocal<Timer.Sample> threadLocal = new ThreadLocal<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 总计数 + 1
meterRegistry.counter("micro_req_total", Tags.of("url", request.getRequestURI(), "method", request.getMethod())).increment();
// 处理中计数 +1
meterRegistry.gauge("micro_process_req", Tags.of("url", request.getRequestURI(), "method", request.getMethod()), 1);

Timer.Sample sample = Timer.start();
threadLocal.set(sample);
return super.preHandle(request, response, handler);
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
try {
super.postHandle(request, response, handler, modelAndView);
} finally {
meterRegistry.gauge("micro_process_req", Tags.of("url", request.getRequestURI(), "method", request.getMethod()), -1);
// Timer timer = meterRegistry.timer("micro_req_histogram", Tags.of("url", request.getRequestURI(), "method", request.getMethod(), "code", String.valueOf(response.getStatus())));
Timer timer = Timer.builder("micro_req_histogram").minimumExpectedValue(Duration.ofMillis(1)).maximumExpectedValue(Duration.ofMinutes(3))
.sla(Duration.ofMillis(10), Duration.ofMillis(50), Duration.ofMillis(100), Duration.ofMillis(300), Duration.ofMillis(1000))
.tags(Tags.of("url", request.getRequestURI(), "method", request.getMethod(), "code", String.valueOf(response.getStatus())))
.register(meterRegistry);
threadLocal.get().stop(timer);
threadLocal.remove();
}
}
}

注意上面的三种Metric的创建方式

  • Counter: 直接使用 meterRegistry.counter()来创建metric并实现计数+1
    • 传参中,Tags组成的就是propmetheus中定义的label,kv格式,第一个参数用来定义MetricName
  • Gauge: 使用姿势与上面基本相同,不过需要注意计数的加减是直接在传参中
  • Histogram: 它的使用姿势就需要特别注意下了,在preHander中定义的是 Timer.Sampler对象,在 postHandler中实现的数据采集

上面短短一点代码,就实现了一个简单的自定义信息上报;接下来就是注册拦截器了

4. 注册并测试

拦截器依赖了Spring的bean对象,因此需要将它定义为bean对象

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
java复制代码@RestController
@SpringBootApplication
public class Application implements WebMvcConfigurer {
private Random random = new Random();

@Bean
public MetricInterceptor metricInterceptor() {
return new MetricInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(metricInterceptor()).addPathPatterns("/**");
}

@GetMapping(path = "hello")
public String hello(String name) {
int sleep = random.nextInt(200);
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello sleep: " + sleep + " for " + name;
}

public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

基于此一个简单的自定义采集上报就完成了;项目启动之后,通过访问采集端点查看是否有数据上报

最后小结一下,虽然SpringBoot可以非常方便的接入prometheus来采集一些常见的指标,但是当我们有自定义上报指标的需求时,直接使用MeterRegistry来收集信息,创建Metric是个不错的选择,通常我们选择的三种类型作用如下

  • 总的请求数:采用Counter
  • 当前正在处理的请求数:采用Gauge
  • 请求耗时直方图: Histogram

III. 不能错过的源码和相关知识点

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 源码:github.com/liuyueyi/sp…

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 blog.hhui.top
  • 一灰灰Blog-Spring专题博客 spring.hhui.top

本文转载自: 掘金

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

1…276277278…956

开发者博客

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